/home/php-youtube-playlist-randomizer

Tôi đã chế bộ phát ngẫu nhiên video trong youtube playlist như thế nào?

Published on | Updated

Tiêu đề thì hơi lủng củng, đại khái bài toán như sau:

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:

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àm random_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:

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 🤭