前置き
「あのVTuber事務所の配信スケジュール、一週間以上落ちてるんだよね」
「仕方ない、俺が作るか」
そんなノリで作ろうとしたら翌日には復旧した俺の気持ち。
要件
・YoutubeAPIのリクエストは日単位での回数制限があるため、一度取得した結果は保存しておく
・変更・削除に対応するため、再取得する際、前回の結果は削除する
・チャンネル数は20程度またはそれ以下を想定
・アクセス数もそんなに大量にない
・移行の容易性を考えデータはDBではなくファイルに保持
・ちゃんと配信予定(枠)を事前に作ってくれると効果大
作り方
API keyの発行
今回はyoutube APIを使用しているのですが、使用にはAPI keyを発行する必要があります。
一日の使用回数に制限がありますが、googleアカウントがあれば無料で発行できます。
下記の記事に詳しい手順が記載してあります。
https://qiita.com/shinkai_/items/10a400c25de270cb02e4
チャンネルの枠の一覧を取得
下記が公式リファレンスなのですが、ちょっと情報量としては心許ないです。
https://developers.google.com/youtube/v3/docs/search/list?hl=ja
リクエスト
※変数は${}で括ってます。
https://content-youtube.googleapis.com/youtube/v3/search?order=date&channelId=${ch_id}&part=id&part=snippet&key=${api_key}&eventType=upcoming&type=video
eventType=upcomingとtype=videoで予定のみに絞り込んでいます。
レスポンス
サンプルは2022/05/16時点での月紫アリアさんのチャンネルです。
毎週月曜に一週間分の予定を作るタイプの方なので、いつも見栄えのする検証に使わせて頂いています。感謝を込めて宣伝。
{
"kind": "youtube#searchListResponse",
"etag": "xx5NiXUjsXpks1JuRmEQ_i8t2GM",
"nextPageToken": "CAUQAA",
"regionCode": "JP",
"pageInfo": {
"totalResults": 7,
"resultsPerPage": 5
},
"items": [
{
"kind": "youtube#searchResult",
"etag": "aAckZdUnkx9QlNxuwUyU_5JA5NA",
"id": {
"kind": "youtube#video",
"videoId": "96RcK_TkG-4"
},
"snippet": {
"publishedAt": "2022-05-16T09:12:59Z",
"channelId": "UC5XQhzMH08PgWa4Zp02Gcsw",
"title": "【歌枠/singing】初見歓迎💜日曜日のかわいいおうた、聴きに来てよ~💜 karaoke【月紫アリア/新人Vtuber】",
"description": "5月ボイス ♡ ⸝ ▶️ https://react.booth.pm/items/3799773 新衣装アリアと初めてのデートっ! そのあとはおうちで…甘えても ...",
"thumbnails": {
"default": {
"url": "https://i.ytimg.com/vi/96RcK_TkG-4/default_live.jpg",
"width": 120,
"height": 90
},
"medium": {
"url": "https://i.ytimg.com/vi/96RcK_TkG-4/mqdefault_live.jpg",
"width": 320,
"height": 180
},
"high": {
"url": "https://i.ytimg.com/vi/96RcK_TkG-4/hqdefault_live.jpg",
"width": 480,
"height": 360
}
},
"channelTitle": "月紫アリアch / Tsukushi Aria",
"liveBroadcastContent": "upcoming",
"publishTime": "2022-05-16T09:12:59Z"
}
},
{
"kind": "youtube#searchResult",
"etag": "1ku9uEzetM2hpoPBcTADzzdU8R0",
"id": {
"kind": "youtube#video",
"videoId": "_UfGM-2wQq0"
},
"snippet": {
"publishedAt": "2022-05-16T04:49:34Z",
"channelId": "UC5XQhzMH08PgWa4Zp02Gcsw",
"title": "【雀魂】ルーレットで決められた属性で麻雀🀄💜色んなアリアを見て…💜そしてボコす【月紫アリア/夢川かなう/姫熊りぼん/九楽ライ/新人Vtuber】",
"description": "夢川かなう/Kanau ch @Himekuma Ribon Ch. 姫熊 りぼん @Kulaku Lie ch.九楽ライ すべてはノリ #ぴちくまねくろにーと ...",
"thumbnails": {
"default": {
"url": "https://i.ytimg.com/vi/_UfGM-2wQq0/default_live.jpg",
"width": 120,
"height": 90
},
"medium": {
"url": "https://i.ytimg.com/vi/_UfGM-2wQq0/mqdefault_live.jpg",
"width": 320,
"height": 180
},
"high": {
"url": "https://i.ytimg.com/vi/_UfGM-2wQq0/hqdefault_live.jpg",
"width": 480,
"height": 360
}
},
"channelTitle": "月紫アリアch / Tsukushi Aria",
"liveBroadcastContent": "upcoming",
"publishTime": "2022-05-16T04:49:34Z"
}
},
{
"kind": "youtube#searchResult",
"etag": "qr8tKI6lbK9P68A3NwfXgbURYHo",
"id": {
"kind": "youtube#video",
"videoId": "x09-gfZITm0"
},
"snippet": {
"publishedAt": "2022-05-16T04:44:24Z",
"channelId": "UC5XQhzMH08PgWa4Zp02Gcsw",
"title": "【Escape Simulator】謎解きで脱出ゲーム🔓❕頭やわらかぁくするぞ~💜【月紫アリア/フンボルトペンギン/Vtuber】",
"description": "初コラボ @フンボルトペンギン / Humboldt Penguin ⋱⋰ ⋱⋰ ⋱⋰ ⋱⋰⋱⋰ ⋱⋰ ⸜ ♡ 5月ボイス ...",
"thumbnails": {
"default": {
"url": "https://i.ytimg.com/vi/x09-gfZITm0/default_live.jpg",
"width": 120,
"height": 90
},
"medium": {
"url": "https://i.ytimg.com/vi/x09-gfZITm0/mqdefault_live.jpg",
"width": 320,
"height": 180
},
"high": {
"url": "https://i.ytimg.com/vi/x09-gfZITm0/hqdefault_live.jpg",
"width": 480,
"height": 360
}
},
"channelTitle": "月紫アリアch / Tsukushi Aria",
"liveBroadcastContent": "upcoming",
"publishTime": "2022-05-16T04:44:24Z"
}
},
{
"kind": "youtube#searchResult",
"etag": "FxMUGIK9_ZGVNfLvpZTb_SzE1jA",
"id": {
"kind": "youtube#video",
"videoId": "TIdlzHDjVFQ"
},
"snippet": {
"publishedAt": "2022-05-16T04:36:46Z",
"channelId": "UC5XQhzMH08PgWa4Zp02Gcsw",
"title": "【朝活】初見歓迎💜週の真ん中!可愛いアリアと気合い入れるぞ💜 good moning【月紫アリア/新人Vtuber】",
"description": "5月ボイス ♡ ⸝ ▶️ https://react.booth.pm/items/3799773 新衣装アリアと初めてのデートっ! そのあとはおうちで…甘えても ...",
"thumbnails": {
"default": {
"url": "https://i.ytimg.com/vi/TIdlzHDjVFQ/default_live.jpg",
"width": 120,
"height": 90
},
"medium": {
"url": "https://i.ytimg.com/vi/TIdlzHDjVFQ/mqdefault_live.jpg",
"width": 320,
"height": 180
},
"high": {
"url": "https://i.ytimg.com/vi/TIdlzHDjVFQ/hqdefault_live.jpg",
"width": 480,
"height": 360
}
},
"channelTitle": "月紫アリアch / Tsukushi Aria",
"liveBroadcastContent": "upcoming",
"publishTime": "2022-05-16T04:36:46Z"
}
},
{
"kind": "youtube#searchResult",
"etag": "ISSqY8DCr-Lb8frnKTLV6dHd4Ss",
"id": {
"kind": "youtube#video",
"videoId": "P4PdMaItZ1w"
},
"snippet": {
"publishedAt": "2022-05-16T04:35:26Z",
"channelId": "UC5XQhzMH08PgWa4Zp02Gcsw",
"title": "【ASMR】心音耐久💜ぎゅってして寝たいから、こっちおいで?💜 whisper/heart beat/sleeping sound【月紫アリア/新人Vtuber】",
"description": "すやすやしてってね~ #ASMR #バイノーラル #睡眠誘導 ⋱⋰ ⋱⋰ ⋱⋰ ⋱⋰⋱⋰ ⋱⋰ ⸜ ♡ 5月ボイス ...",
"thumbnails": {
"default": {
"url": "https://i.ytimg.com/vi/P4PdMaItZ1w/default_live.jpg",
"width": 120,
"height": 90
},
"medium": {
"url": "https://i.ytimg.com/vi/P4PdMaItZ1w/mqdefault_live.jpg",
"width": 320,
"height": 180
},
"high": {
"url": "https://i.ytimg.com/vi/P4PdMaItZ1w/hqdefault_live.jpg",
"width": 480,
"height": 360
}
},
"channelTitle": "月紫アリアch / Tsukushi Aria",
"liveBroadcastContent": "upcoming",
"publishTime": "2022-05-16T04:35:26Z"
}
}
]
}
ざっくりレスポンス解説
totalResults: 7
resultsPerPage: 5
となっているので、ページに続きがあるようです。
必要に応じて「maxResults」パラメータを追加するか、「pageToken」を使ってループさせましょう。
items
:枠の配列
items[].id.videoId
:動画のID
https://www.youtube.com/watch?v=${videoId}
で動画へのリンクになる。
items[].snippet.publishedAt
:登録日時 更新の場合にどうなるかは不明
items[].snippet.publishTime
:publishedAtとの違いは不明
items[].snippet.title
:動画・配信のタイトル
items[].snippet.description
:いわゆる概要欄 80文字くらいで省略される
items[].snippet.thumbnails
:サムネイル 大きさによって3つに分かれている
枠の詳細を取得
一覧だけでもそれなりの情報は得られますが、肝心な配信・公開日時は別のAPIで取得しなければならないようです。
勿論、リファレンスも一応あります。
https://developers.google.com/youtube/v3/docs/videos/list?hl=ja
リクエスト
先のAPIで取得したitems[].id.videoId
を使用して検索します。
https://www.googleapis.com/youtube/v3/videos?key=${apikey}&id=${videoId}&part=liveStreamingDetails
レスポンス
{
"kind": "youtube#videoListResponse",
"etag": "3VLGoEdDbvn1ADPHHeO4ZEmq2ik",
"items": [
{
"kind": "youtube#video",
"etag": "cWvV7JhHFmOqagMLX8GHXKBuIoE",
"id": "TIdlzHDjVFQ",
"liveStreamingDetails": {
"scheduledStartTime": "2022-05-17T22:00:00Z",
"activeLiveChatId": "Cg0KC1RJZGx6SERqVkZRKicKGFVDNVhRaHpNSDA4UGdXYTRacDAyR2NzdxILVElkbHpIRGpWRlE"
}
}
],
"pageInfo": {
"totalResults": 1,
"resultsPerPage": 1
}
}
videoIdを配列で指定したり未指定だとitems
が複数になりますが、今回は単一のvideoIdを指定しているので、items
は常に0-1件です。
items[0].liveStreamingDetails.scheduledStartTime
が配信開始日時になります。
サンプルプログラム
まずバッチを一定周期で実行することでデータを取得し、'./files/schedule'に保存します。
チャンネルIDの一覧は./files/ch_list.txt
に改行区切りで保存しています。
実行周期を短くすることでyoutubeの最新情報とのタイムラグを軽減できますが、短くしすぎるとAPIの実行回数が上限に達しやすくなります。
<?php
date_default_timezone_set('Asia/Tokyo');
$apikey = "hoge";
// 前回実行時のファイルを削除
foreach(glob("./files/schedule/*") as $val) {
unlink($val);
}
// チャンネルidの一覧を取得
$ch_ids = explode("\r\n", file_get_contents('./files/ch_list.txt'));
foreach($ch_ids as $ch_id){
if(empty($ch_id)){
continue;
}
// チャンネル毎に枠の一覧を取得
$upcomings = json_decode(`curl -XGET "https://content-youtube.googleapis.com/youtube/v3/search?order=date&channelId=$ch_id&part=id&part=snippet&key=$apikey&eventType=upcoming&type=video&maxResults=10"`, true);
foreach($upcomings['items'] as $item){
$videoid = $item['id']['videoId'];
// 枠の詳細を取得
$video = json_decode(`curl -XGET "https://www.googleapis.com/youtube/v3/videos?key=$apikey&id=$videoid&part=liveStreamingDetails"`, true);
$item['video'] = $video;
file_put_contents("./files/schedule/" . $item['id']['videoId'] . ".json", json_encode($item));
}
}
保存した情報をWEBで参照します。
サンプルでは、24時間前(配信中の枠を考慮)~7日後を表示対象に、予定のない日は非表示にしています。
<?php
date_default_timezone_set('Asia/Tokyo');
// 1日前~7日後
$min_time = (time() - (24 * 60 * 60));
$max_time = (time() + (7 * 24 * 60 * 60));
$items = [];
foreach(glob("./files/schedule/*.json") as $file){
$item = json_decode(file_get_contents($file), true);
$scheduledStartTime = $item['video']['items'][0]['liveStreamingDetails']['scheduledStartTime'];
$start_time = strtotime($scheduledStartTime);
if($min_time < $start_time && $start_time < $max_time){
$start_date = date('Y/m/d', strtotime($scheduledStartTime));
$videoid = $item['id']['videoId'];
//$sortkey = $scheduledStartTime . '_' . $item['snippet']['channelId'];
$sortkey = $scheduledStartTime;
if(!isset($items[$start_date])){
$items[$start_date] = [];
}
$items[$start_date][$sortkey] = $item;
}
}
// 配信開始時刻・チャンネルID順でソート
$sorted_items = [];
foreach($items as $key => $item){
ksort($item);
$sorted_items[$key] = $item;
}
// 日付順でソート
ksort($sorted_items);
?>
<html lang="ja">
<head>
<meta charset="UTF-8">
</head>
<body>
<table style="border:solid 1px">
<thead>
<tr>
<?php foreach($sorted_items as $key => $item){ ?>
<td style="border:solid 1px"><?= $key ?></td>
<?php } ?>
</tr>
</thead>
<tbody>
<tr>
<?php foreach($sorted_items as $item){ ?>
<td style="vertical-align:top;border:solid 1px">
<?php foreach($item as $column){ ?>
<?= $column['snippet']['channelTitle'] ?><br>
<?= $column['snippet']['title'] ?><br>
<?= date('Y/m/d H:i:s',strtotime($column['video']['items'][0]['liveStreamingDetails']['scheduledStartTime'])); ?><br>
<a href="https://www.youtube.com/watch?v=<?=$column['id']['videoId'] ?>"><img src="<?= $column['snippet']['thumbnails']['default']['url'] ?>"><br></a>
<br>
<?php } ?>
</td>
<?php } ?>
</tr>
</tbody>
</table>
</body>
</html>
課題
複数推しのファンや新興の事務所運営さん等に提供したいが、オンプレにするかAWSサーバーでも立ててクラウド提供するか。
・オンプレの場合は自前でサーバーを用意する必要があるため、導入の敷居がやや高くなる。
・クラウドの場合はAPI実行回数の制約がある。利用者のAPIkeyを使わせてもらう手もあるが、人様のkeyを預かれるほどセキュリティを強固にするのも面倒臭い。
参考
https://schedule.hololive.tv/
大手の割にはシンプルですが、これくらい余計な情報はないほうがいいのかもしれません。
https://schedule.v-react.com/
概要欄を自動スクロールで流しているのがオシャレですね。