Tiêu đề thì hơi lủng củng, đại khái bài toán như sau:
- Tôi có một chiếc youtube playlist với n video trong đó
- Tôi có một chiếc link đặt ở đâu đó
- Mỗi khi người dùng bấm vào chiếc link, sẽ ra 1 bài ngẫu nhiên trong playlist đó
- Việc xem video sẽ ở trên youtube, không embed vào trang của tôi
- Hết rồi
Vậy đó, sau khi tìm 7749 cách mà không được, tôi đành bào chế một chiếc API đơn giản, nếu anh chị em cô dì chú bác có nhu cầu random nhưng embed thì có thể tham khảo iframe_api
của cụ Youtube và customize bằng javascript hook nha.
Giờ thì bắt đầu quá trình bào chế thôi
Tìm cách lấy danh sách video
Đầu tiên phải tìm cách lấy danh sách video đã, chứ không random kiểu gì được.
Ngó lên Google API cái là thấy liền Youtube Data API v3, nơi này sẽ trả về danh sách các video có trong playlist.
Điều dĩ nhiên dùng API của Google thì cần API key rồi. Cái này thì tự tìm hiểu chứ không đề cập ở đây, mặc định coi là có rồi nhé.
Lựa chọn ngôn ngữ để phát triển
Python
Hiện tại thì mình làm việc nhiều nhất là Python, code Python cũng dễ, nên được nghĩ đến đầu tiên, cơ mà deploy Python web thì hơi lằng nhằng, nên tạm gác đấy.
Javascript (NodeJS)
Lựa chọn số 2 là javascript, chạy nodejs. Cơ mà server thì hiện tại không cài nodejs, mà có cài thì deploy nó cũng chả khác gì python, tự nhiên thêm 1 process trên server.
Tuy nhiên nếu bạn deploy lên AWS lambda, GCP cloudrun, heroku, hoặc CloudFlare Workers thì nó cũng không phải vấn đề lắm.
PHP
Cuối cùng thì mình nghĩ đến PHP, server hiện tại đã có sẵn PHP, mà deploy thì chỉ cần nhét file vô là chạy. Chủ yếu là mình deploy lên server có sẵn của mình nên cũng tiện edit cũng như debug.
Tương lai nếu muốn nó tách bạch, có thể nghĩ đến code bằng javascript rồi deploy lên CloudFlare workers.
Lựa chọn thư viện
Mặc định thì Google có thư viện cho nhiều ngôn ngữ, dùng khá tiện, import vào là dùng thôi.
Tuy nhiên do muốn làm tối giản hết mức, vả lại thì mình cũng chỉ query đơn giản, nên mình nghĩ bỏ luôn thư viện này mà request thẳng lên API của Google.
Query lên thì mình hay dùng thư viện php-curl-class. Cái này chắc cũng phổ biến với anh em code PHP xưa giờ, sau này bị guzzle chiếm ngôi thì không nói, nhưng nó vẫn là 1 thư viện quốc dân vì tính nhẹ và dễ dùng.
Nhưng mà như nói ở trên, mình muốn nhẹ nhất có thể, và cũng nhắc ở trên là mình query khá đơn giản nên là thôi kiếp này coi như bỏ, à nhầm thư viện này coi như bỏ.
Chốt lại mình sẽ tự bào chế 1 hàm gửi request lên Google API bằng curl của PHP luôn.
Bắt tay vào việc
1. Khai báo dữ liệu
Đầu tiên là khai báo các dữ liệu cần thiết để sử dụng như playlist nào, api key là gì, API endpoint là gì, ...
Draft nhanh mình có đoạn code như này
<?php
const API_ENDPOINT = 'https://www.googleapis.com/youtube/v3/playlistItems';
const PLAYLIST_ID = 'enter your youtube playlist id here';
const API_KEY = 'enter your api key here';
2. Lấy dữ liệu
Sau đó mình sẽ cần 1 hàm dùng curl để gửi request và lấy dữ liệu về. Cái này thì cũng khá cơ bản. Dựa vào document của PHP thì sẽ dựng được 1 hàm như thế này
function send_request($url)
{
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
$response = curl_exec($ch);
$curl_errno = curl_errno($ch);
$curl_error = curl_error($ch);
curl_close($ch);
if ($curl_errno > 0) {
echo "cURL Error ($curl_errno): $curl_error";
exit();
}
echo "Data received: $response";
return $response;
}
Một chiếc hàm đơn giản gửi request lên, in ra response hoặc lỗi.
Tạm thời để thế để test xem là dùng curl có đúng không, tại lâu ngày quen dùng thư viện rồi riết không biết dùng chay thế nào 😢
3. Build URL
Có hàm lấy dữ liệu rồi, thì cần xem url là gì.
Theo như document thì chúng ta có 1 chiếc url đơn giản như này:
https://www.googleapis.com/youtube/v3/playlistItems?part=snippet&playlistId={playlist_id_here}&key={api_key_here}&maxResults=50
Chỉ cần thay playlist id và api key đã khai báo ở trên kia, dán vào trình duyệt là ta có thể xem được mình build URL có đúng không, dữ liệu trả về sẽ dạng gì để còn biết đường xử lý.
Dữ liệu mẫu sẽ như này (một số trường thông tin đã bị loại bỏ cho gọn):
{
"kind": "youtube#playlistItemListResponse",
"etag": "xxx",
"nextPageToken": "xxx",
"items": [
{
"kind": "youtube#playlistItem",
"etag": "nctAAkttFXMLeJxSXZseu9H5U3A",
"id": "UExWMXhxTHVyLW15cHQ2SlJCTzNmWnVabV9uV0JSTWFwSy5GNjNDRDREMDQxOThCMDQ2",
"snippet": {
"publishedAt": "2021-09-04T18:20:36Z",
"channelId": "UCblgd9iVUWFJnzHUPJnfodA",
"title": "[Vietsub] Thành Đô 成都 - Triệu Lôi 赵雷 | Tôi là ca sĩ mùa 5 | The Singer 2017",
"description": "",
"thumbnails": {},
"channelTitle": "Dũng Nguyễn",
"playlistId": "PLV1xqLur-mypt6JRBO3fZuZm_nWBRMapK",
"position": 1,
"resourceId": {
"kind": "youtube#video",
"videoId": "loiCoXVfmCk"
},
"videoOwnerChannelTitle": "",
"videoOwnerChannelId": "UCRaht7u8t2BPSwd32z9zL5w"
}
}
],
"pageInfo": {
"totalResults": 22,
"resultsPerPage": 50
}
}
Chúng ta cần chú ý đến items
: nơi chứa danh sách các video trong playlist. Và nextPageToken
trong trường hợp số video nhiều hơn 50, kết quả bị phân thành nhiều trang.
4. Xử lý dữ liệu
Xây dựng hàm xử lý kết quả trả về nào
<?php
$video_ids = [];
function get_playlist_videos($page_token = '')
{
global $video_ids;
$query_url = API_ENDPOINT . '?part=snippet&maxResults=50&playlistId=' . PLAYLIST_ID . '&key=' . API_KEY;
if ($page_token != '') {
$query_url .= "&pageToken={$page_token}";
}
$response = send_request($query_url);
$data = json_decode($response, true);
foreach($data['items'] as $item) {
$video_id = $item['snippet']['resourceId']['videoId'];
$video_title = $item['snippet']['title'];
$video_ids[$video_id] = $video_title;
}
if (isset($data['nextPageToken']) && $data['nextPageToken'] != '') {
return get_playlist_videos($data['nextPageToken']);
}
}
Hàm này thì cơ bản có 4 việc:
- build url
- lấy data trả về
- lưu video id và video title vào
$video_ids
thực ra thì bạn chỉ cần video id là đủ rồi, mình lưu video title với mục đích tracking riêng, nên mọi người có thể đơn giản hóa việc này lại bằng cách chỉ chèn video id vào$video_ids
- kiểm tra xem có page token không để query lấy dữ liệu tiếp
Chưa xét đến việc handle lỗi vội, tạm thời chúng ta cứ thế đã.
5. Lấy ngẫu nhiên video id
Chuyển sang bước tiếp theo, chọn ngẫu nhiên 1 id trong danh sách video id vừa nãy.
<?php
function random_video()
{
$video_count = count($video_ids);
$random_number = rand(0, $video_count - 1);
return $video_ids[$random_number];
}
Cái này thì cực đơn giản, dùng hàm rand để lấy ngẫu nhiên index của array thôi. Tuy nhiên vì $video_ids
của mình là dạng key-value
nên cần chình một chút như sau
<?php
function random_video()
{
$video_list = array_keys($video_ids);
$video_count = count($video_list);
$random_number = rand(0, $video_count - 1);
return $video_list[$random_number];
}
Lưu ý một chút là PHP có cảnh báo hàm rand()
không thực sự ngẫu nhiên
This function does not generate cryptographically secure values, and should not be used for cryptographic purposes.
Nên bạn nào muốn chắc cú, có thể đổi sang hàmrandom_int()
, mình thì thấy không cần lắm nên bỏ qua.
6. Quay lại một chút lên trên để xử lý các lỗi có thể phát sinh
Phía trên chỉ đơn giản tính đến các case thành công, tuy nhiên nhiều khi cũng thất bại.
Ví dụ như build url lỗi gì đó khiến Google API trả về error, json_decode
bị lỗi, dữ liệu trả về bị rỗng, ...
Chúng ta cần handle tất cả những thứ đó.
Mình sẽ nhét hết các phần đó vào hàm send_request()
cho nó gọn.
Kết quả sẽ như này:
<?php
date_default_timezone_set('Asia/Ho_Chi_Minh');
const LOG_FILE = 'youtube-playlist-randomizer.log';
const ERROR_MSG = "Some thing went wrong. Please try again!";
$video_ids = [];
function write_log($msg)
{
$now = date("Y-m-d H:i:s");
file_put_contents(
LOG_FILE,
"[$now] $msg" . PHP_EOL,
FILE_APPEND
);
}
function send_request($url)
{
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
$response = curl_exec($ch);
$curl_errno = curl_errno($ch);
$curl_error = curl_error($ch);
curl_close($ch);
if ($curl_errno > 0) {
write_log("cURL Error ($curl_errno): $curl_error");
exit(ERROR_MSG);
}
try {
$data = json_decode($response, true);
}
catch (Exception $e) {
write_log($e);
exit(ERROR_MSG);
}
if (isset($data['error']) && !empty($data['error']))
{
write_log("Response error: " . $data['error']['code'] . PHP_EOL . $response);
exit(ERROR_MSG);
}
return $data;
}
function get_playlist_videos($page_token = '')
{
global $video_ids;
$query_url = API_ENDPOINT . '?part=snippet&maxResults=50&playlistId=' . PLAYLIST_ID . '&key=' . API_KEY;
if ($page_token != '') {
$query_url .= "&pageToken={$page_token}";
}
$data = send_request($query_url);
foreach($data['items'] as $item) {
$video_id = $item['snippet']['resourceId']['videoId'];
$video_title = $item['snippet']['title'];
$video_ids[$video_id] = $video_title;
}
if (isset($data['nextPageToken']) && $data['nextPageToken'] != '') {
return get_playlist_videos($data['nextPageToken']);
}
}
7. Caching
Ok coi như hòm hòm, hệ thống với các hàm cơ bản đã chạy ngon nghẻ rồi.
Nhưng mà mỗi 1 lần có khách vào lại request lên Google thì cũng hơi thốn, anh Google thì limit, mà request thì cũng chậm, vậy là ta phải tính đến đoạn caching thôi.
Đơn giản nhất là ta lưu lại danh sách video id đã lấy được ở trên vào file json, khi truy cập vô thì đọc file json đó rồi random thôi.
Quạ quạ một chút thì có được đoạn như này
<?php
const DATA_FILE = 'youtube-playlist-randomizer.json';
if (!file_exists(DATA_FILE)) {
get_playlist_videos();
file_put_contents(DATA_FILE, json_encode($video_ids));
} else {
$cache_content = file_get_contents(DATA_FILE);
$video_ids = json_decode($cache_content, true);
}
$redirect_video_id = random_video();
header("Location: https://www.youtube.com/watch?v={$redirect_video_id}&list=" . PLAYLIST_ID);
die();
Còn việc invalid cache thì có nhiều cách:
- Viết cron tự xóa file json, lần sau khách vô sẽ tạo ra file json mới
- Viết thêm 1 param để chạy CLI chủ động update nội dung file json
- ...
Cái này thì tùy cách của mỗi người, mình sẽ không đề cập ở đây nữa.
Chốt lại thì chúng ta sẽ có file như này
<?php
date_default_timezone_set('Asia/Ho_Chi_Minh');
const API_ENDPOINT = 'https://www.googleapis.com/youtube/v3/playlistItems';
const PLAYLIST_ID = 'enter your youtube playlist id here';
const API_KEY = 'enter your api key here';
const DATA_FILE = 'youtube-playlist-randomizer.json';
const LOG_FILE = 'youtube-playlist-randomizer.log';
$video_ids = []; // format: video_id => video_title
const ERROR_MSG = "Some thing went wrong. Please try again!";
function write_log($msg)
{
$now = date("Y-m-d H:i:s");
file_put_contents(
LOG_FILE,
"[$now] $msg" . PHP_EOL,
FILE_APPEND
);
}
function send_request($url)
{
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
$response = curl_exec($ch);
$curl_errno = curl_errno($ch);
$curl_error = curl_error($ch);
curl_close($ch);
if ($curl_errno > 0) {
write_log("cURL Error ($curl_errno): $curl_error");
exit(ERROR_MSG);
}
try {
$data = json_decode($response, true);
}
catch (Exception $e) {
write_log($e);
exit(ERROR_MSG);
}
if (isset($data['error']) && !empty($data['error']))
{
write_log("Response error: " . $data['error']['code'] . PHP_EOL . $response);
exit(ERROR_MSG);
}
return $data;
}
function get_playlist_videos($page_token = '')
{
global $video_ids;
$query_url = API_ENDPOINT . '?part=snippet&maxResults=50&playlistId=' . PLAYLIST_ID . '&key=' . API_KEY;
if ($page_token != '') {
$query_url .= "&pageToken={$page_token}";
}
$data = send_request($query_url);
foreach($data['items'] as $item) {
$video_id = $item['snippet']['resourceId']['videoId'];
$video_title = $item['snippet']['title'];
$video_ids[$video_id] = $video_title;
}
if (isset($data['nextPageToken']) && $data['nextPageToken'] != '') {
return get_playlist_videos($data['nextPageToken']);
}
}
function random_video()
{
global $video_ids;
$video_list = array_keys($video_ids);
$video_count = count($video_list);
$random_number = rand(0, $video_count - 1);
return $video_list[$random_number];
}
if (!file_exists(DATA_FILE)) {
get_playlist_videos();
file_put_contents(DATA_FILE, json_encode($video_ids));
} else {
$cache_content = file_get_contents(DATA_FILE);
$video_ids = json_decode($cache_content, true);
}
$redirect_video_id = random_video();
header("Location: https://www.youtube.com/watch?v={$redirect_video_id}&list=" . PLAYLIST_ID);
die();
Việc còn lại là đẩy file lên server và chạy thôi.
Vì là file dùng riêng cho cá nhân nên mình đã hard-code playlist id
luôn. Bản nâng cấp mọi người có thể cho phép thay playlist id
bằng param trên URL. caching file thì đặt theo tên của playlist id
....
Kết bài
Một chiếc script đơn giản, logic cũng không có gì quá phức tạp.
Bài thì đơn sơ, lâu rồi không code PHP cũng không biết dính lỗi bảo mật gì không, cơ mà mình viết bài cho vui thôi.
Mong rằng có chút ích gì cho đời, còn không thì thôi 🤭