はじめに
この記事の続きになります。
GitHub Pagesを本Webサイトのサブドメインとして使用できるようにしましたので、この記事ではそのトップページっぽいものを作成していきます。
まず、ネタを考えます。
ネタといっても最初から新規に考えるのではなく、今までに記事を書くときにネタにしたものの中から使えそうなものを探すことにします。
ところがどっこい、使えなさそうなものならたくさんあるわけです… (´・ω・…
そんな中から、YouTube Data API (v3)を使ってpandanote.infoチャンネルの動画の一覧のページを作り、それをトップページとしてGitHubに半自動でアップロードするシステムを作ってみることにしました。
ところで、YouTube Data API (v3)って何ですか?
…という方もいらっしゃると思いますので、そんなこともあろうかと以下の記事を書いておきました。拙稿ではありますがご覧いただけると幸いです。
仕様を考えます。
処理の手順を考える。
YouTubeのデータをGitHub Pagesで使える形式に変換する処理を以下の手順で行うことを考えます。
- (初回のみ)GitHubにWebサイト用のリポジトリを作成します(こちらをご参照いただければと思います)。
- git clone(初回のみ)またはgit pullを実行して、ローカルのPC等にリポジトリの中身をダウンロードします。
- YouTube Data API (v3)を使ったプログラムを実行して、必要な情報を取得します。
- 手順3で取得した情報と別途用意しておいたmarkdown形式のテンプレートファイルを組み合わせて、トップページ用のmarkdownファイルを作成します。
- git commitします。
- git pushします。
- pushしたmarkdown形式のファイルはGitHub PagesのJekyllがコンパイルしてくれます。コンパイルには数分程度の時間がかかることがありますので、気長に待ちます。
- コンパイルが正常に終了すれば、ページが表示されます。
運用の方針を考える。
運用と言うほどのものではないかもしれませんが、前節で書いた処理を一撃で実行できるシェルスクリプトを準備して、当面の間はFedoraのPCから適宜手動で実行します。
YouTubeは少なくとも本Webサイトほどは更新を行いませんし、APIを用もなく呼び出しまくるようなシステム設計にはできないので、自動で実行するにしても1日1回程度の実行を前提とすることにします。
YouTube Data API (v3)を使ったデータ取得用プログラムの実装例。
実装例は以下の通りになります。例によってgoogle-api-python-clientを利用しています。
[2018/11/03追記] 164行目のISOフォーマットへの変換部でタイムゾーンが表示できていなかったので、タイムゾーンを表示できるようにコードを修正しました。
#!/usr/bin/python | |
import httplib2 | |
import os | |
import sys | |
import json | |
import argparse | |
import re | |
import random | |
from pytz import timezone | |
from dateutil import parser | |
from datetime import datetime | |
# For an older version than google_api_python_client-1.12.8 | |
# from apiclient.discovery import build | |
# For google_api_python_client-1.12.8 or newer. | |
from googleapiclient.discovery import build | |
from oauth2client.client import flow_from_clientsecrets | |
from oauth2client.file import Storage | |
from oauth2client.tools import argparser, run_flow | |
def append_link(matchobj): | |
s = matchobj.group(0) | |
return "<a href=\"{0:s}\" style=\"color: #1b3b8a\">{1:s}</a>".format(s,s) | |
random.seed() | |
#parser = argparse.ArgumentParser(description='Command line options of moviefilelist.py') | |
#parser.add_argument('-o','--output-file', type=str, default='UploadedVideoDataList.json', help='File name of output.') | |
#args = parser.parse_args() | |
#params = vars(args) | |
#uvdl = params['output_file']; | |
# Youtubeから動画のID及び元のファイル名のリスト等を取り出して、 | |
# チャンネルリストのmarkdownファイルに変換するためのPython3の | |
# スクリプト。 | |
# URL: https://sidestory.pandanote.info/ | |
# | |
# The CLIENT_SECRETS_FILE variable specifies the name of a file that contains | |
# the OAuth 2.0 information for this application, including its client_id and | |
# client_secret. You can acquire an OAuth 2.0 client ID and client secret from | |
# the Google Developers Console at | |
# https://console.developers.google.com/. | |
# Please ensure that you have enabled the YouTube Data API for your project. | |
# For more information about using OAuth2 to access the YouTube Data API, see: | |
# https://developers.google.com/youtube/v3/guides/authentication | |
# For more information about the client_secrets.json file format, see: | |
# https://developers.google.com/api-client-library/python/guide/aaa_client_secrets | |
CLIENT_SECRETS_FILE = "client_secrets.json" | |
# This variable defines a message to display if the CLIENT_SECRETS_FILE is | |
# missing. | |
MISSING_CLIENT_SECRETS_MESSAGE = """ | |
WARNING: Please configure OAuth 2.0 | |
To make this sample run you will need to populate the client_secrets.json file | |
found at: | |
%s | |
with information from the Developers Console | |
https://console.developers.google.com/ | |
For more information about the client_secrets.json file format, please visit: | |
https://developers.google.com/api-client-library/python/guide/aaa_client_secrets | |
""" % os.path.abspath(os.path.join(os.path.dirname(__file__), | |
CLIENT_SECRETS_FILE)) | |
# This OAuth 2.0 access scope allows for read-only access to the authenticated | |
# user's account, but not other types of account access. | |
YOUTUBE_READONLY_SCOPE = "https://www.googleapis.com/auth/youtube.readonly" | |
YOUTUBE_API_SERVICE_NAME = "youtube" | |
YOUTUBE_API_VERSION = "v3" | |
flow = flow_from_clientsecrets(CLIENT_SECRETS_FILE, | |
message=MISSING_CLIENT_SECRETS_MESSAGE, | |
scope=YOUTUBE_READONLY_SCOPE) | |
storage = Storage("%s-oauth2.json" % sys.argv[0]) | |
credentials = storage.get() | |
if credentials is None or credentials.invalid: | |
flags = argparser.parse_args() | |
credentials = run_flow(flow, storage, flags) | |
youtube = build(YOUTUBE_API_SERVICE_NAME, YOUTUBE_API_VERSION, | |
http=credentials.authorize(httplib2.Http())) | |
# Retrieve the contentDetails part of the channel resource for the | |
# authenticated user's channel. | |
channels_response = youtube.channels().list( | |
mine=True, | |
part="contentDetails" | |
).execute() | |
uploadedVideoDataList = [] | |
for channel in channels_response["items"]: | |
# From the API response, extract the playlist ID that identifies the list | |
# of videos uploaded to the authenticated user's channel. | |
uploads_list_id = channel["contentDetails"]["relatedPlaylists"]["uploads"] | |
print("Videos in list {0:s}".format(uploads_list_id),file=sys.stderr) | |
# Retrieve the list of videos uploaded to the authenticated user's channel. | |
# You can get the information of 50 videos at most. | |
playlistitems_list_request = youtube.playlistItems().list( | |
playlistId=uploads_list_id, | |
part="snippet", | |
maxResults=50 | |
) | |
while playlistitems_list_request: | |
playlistitems_list_response = playlistitems_list_request.execute() | |
# Print information about each video. | |
for playlist_item in playlistitems_list_response["items"]: | |
title = playlist_item["snippet"]["title"] | |
video_id = playlist_item["snippet"]["resourceId"]["videoId"] | |
#print("{0:s} ({1:s})".format(title, video_id)) | |
video_list_request = youtube.videos().list( | |
id=video_id, | |
part="snippet,fileDetails,statistics,status", | |
maxResults=50 | |
) | |
video_list_response = video_list_request.execute() | |
for video_list_item in video_list_response["items"]: | |
# if 'tags'in video_list_item["snippet"]: | |
# print(" --> タグ: {0:s}".format(','.join(video_list_item["snippet"]["tags"]))) | |
# print(" --> ファイル名: {0:s}".format(video_list_item["fileDetails"]["fileName"])) | |
uploadedVideoData = {} | |
uploadedVideoData["title"] = title | |
uploadedVideoData["videoid"] = video_id | |
uploadedVideoData["filename"] = video_list_item["fileDetails"]["fileName"] | |
uploadedVideoData["viewCount"] = video_list_item["statistics"]["viewCount"] | |
uploadedVideoData["likeCount"] = video_list_item["statistics"]["likeCount"] | |
uploadedVideoData["description"] = video_list_item["snippet"]["description"] | |
uploadedVideoData["publishedAt"] = video_list_item["snippet"]["publishedAt"] | |
uploadedVideoData["privacyStatus"] = video_list_item["status"]["privacyStatus"] | |
uploadedVideoDataList.append(uploadedVideoData) | |
playlistitems_list_request = youtube.playlistItems().list_next( | |
playlistitems_list_request, playlistitems_list_response) | |
#outputFile = 'UploadedVideoDataList.json' | |
#with open(uvdl,'w') as outfile: | |
#json.dump(uploadedVideoDataList,outfile) | |
templateFile = "index.md.template" | |
templateAppendFile = "index.md.append" | |
templateText = "" | |
templateAppendText = "" | |
with open(templateFile) as infile: | |
templateText = infile.read() | |
print(templateText) | |
pattern = re.compile('\n{2,}') | |
pattern2 = re.compile('\n') | |
urlpattern = re.compile('https?://[\w/:%#$&\? |
|
admod = random.randint(2,4) | |
pos = 0 | |
print("<p class=\"stats\">Count data is as of {0:s} with delay. Please check my <a href=\"https://www.youtube.com/channel/UC2CV_cEjBd81csrHy24Kytg\">YouTube channel</a> to see more accurate data.</p>".format(datetime.now(timezone('Asia/Tokyo')).isoformat())) | |
for uvd in uploadedVideoDataList: | |
if uvd["privacyStatus"] == 'public': | |
description = urlpattern.sub(append_link,pattern2.sub('<br/>',pattern.sub('</p><p>',uvd["description"]))) | |
attop = "<a href=\"https://youtu.be/{0:s}\">".format(uvd["videoid"]) | |
atop = "<a href=\"https://youtu.be/{0:s}\" style=\"color:#1b3b8a; font-size:1.5em; font-weight: bold;\">".format(uvd["videoid"]) | |
abottom = "</a>" | |
thumbnail = "<img src=\"https://i.ytimg.com/vi/{0:s}/default.jpg\" align=\"left\" style=\"border:0;margin-right:0.5em;\"/>".format(uvd["videoid"]) | |
viewCount = "{0:s} view".format(uvd["viewCount"]) | |
if int(uvd["viewCount"]) > 1: | |
viewCount = viewCount + "s" | |
likeCount = "{0:s} like".format(uvd["likeCount"]) | |
if int(uvd["likeCount"]) > 1: | |
likeCount = likeCount + "s" | |
tt = parser.parse(uvd["publishedAt"]) | |
print("<div style=\"border-top: solid 1px #1b3b8a; \">") | |
print("<div style=\"border: 0pt; margin-right:10px;\">{0:s}{1:s}{2:s}</div><div><p>{3:s}{4:s}{5:s}</p><p>{6:s}</p><p class=\"stats\">{7:s} & {8:s}, Published at: {9:s}</p><div style=\"clear:both;\"></div></div>".format(attop,thumbnail,abottom,atop,uvd["title"],abottom,description,viewCount,likeCount,tt.astimezone(timezone('Asia/Tokyo')).isoformat())) | |
print("</div>") | |
pos = pos + 1 | |
with open(templateAppendFile) as infile2: | |
templateAppendText = infile2.read() | |
print(templateAppendText) |
使用上注意した方が良いと思われる点は以下の通りです。
- thumbnailはytimg.comから取得しています。APIでも取得できますが、好みの問題です。
- 1回のAPIリクエストで取得できる動画の情報は50件までです。50件を超える動画の情報を取得したい場合にはnextPageTokenやprevPageTokenの値として返されるトークンを使用するようなのですが、pandanote.infoの動画の投稿件数がそこまで達していないということもあって、使ってみたことがありません。
- サンプルコードをそのまま実行すると、非公開や限定公開に設定した動画のタイトルや説明も取得できてしまうので、privacyStatusの値を確認して、それが”public”である動画のみ出力の対象としています。
privacyStatusの取り得る値は以下の通りです。- public: 公開
- unlisted: 限定公開
- private: 非公開
上記以外の細かい調整。
favicon
本Webサイトと同じfaviconをリポジトリのトップディレクトリに置きました。
“Developed with YouTube”ロゴの配置
YouTube API ServicesのBranding Guidelinesによりますと、APIを使って開発を行った場合には”Developed with YouTube”ロゴを表示すべきであると書いてあるような気がしますので、”Developed with YouTube”ロゴのPNGファイルをここからダウンロードして、サイズを変更した後でリポジトリのトップディレクトリに置き、”Developed with YouTube”ロゴをページのタイトルと本文の間に入れてみました。
スポンサーリンク
これはこれで、ページが引き締まった感じがします。(`・ω・´)
Webサイトの出来上がりです。
Webサイトはこんな感じになりました。どちらかというとスマホで見たほうがカッコよく見えますね。
PCで表示したときのWebサイト作成時点でのスクリーンショットは↓のような感じです。
[2019/02/19 追記] Youtubeのチャンネル紹介のページをこちらに変更しました。
まとめ
ロゴを使った本記事で書いたシステムの概念図を書いて掲載する予定でしたが、YouTubeのロゴは利用条件が意外に厳しい(特に背景が無地の場合にはロゴの周囲に十分にスペースをとることとされています)ために、ロゴのレイアウトが難しくなりそうだったので、断念しました。
その分、文章による説明が増えてしまい、回りくどい書き方が他の記事よりも多くなってしまっているかもしれませんが、その点についてはあしからずご了承願います。
PCで動画をYouTubeにアップロードしようとするとコメントなどを入力することができる入力フィールドが現れ、コメントを入力できます。しかしながら、このコメントを一覧形式で確認することができなかったので、動画を管理する視点から見ても有益なページに仕上がったものと固く信じております。
…そのおかげで、コメントに何ヶ所か誤りが見つかりましたので、修正していきます。(´・ω・`)
この記事は以上です。