はじめに
今年(2017年)もあとわずかとなったわけですが、12月25日まではクリスマス用の飾り付けやイルミネーションが飾られていたところが、26日になると正月用の飾り付けに切り替わるのを見るたびに、毎年「中の人は大変だなぁ」と思います。
でも、クリスマス関係の動画を24日までに作って、年末までに今年のまとめ的な動画でそれなりのクオリティのものを作ろうとすると、なかなか大変です。特に、今年のまとめ的な動画の作成スケジュールは年賀状書きなどのスケジュールと重なるので、それはもう大変です。
そこで、今年自分のチャンネルにアップロードした動画のメタ情報をYouTube Data API v3を使って取り出し、その情報を利用してまとめ的な動画を作ることにしましたので、作り方について書いていきます。
動画の作り方の概要
なぜYouTube Data API v3を使うのか?
なぜアップロードした動画のメタ情報をわざわざYouTube Data API v3を使って取り出すのかというと、
「アップロードした数十個(またはそれ以上)ものメタ動画の情報を手元で管理するのが面倒だから。」
です。アップロードした動画は一応手元にはあるのですが、それを一個ずつ手元のPCから探してリストアップするのはそれなりに手間がかかります。年末の忙しい時期にその手の作業はあまりやりたくないところです。
そもそも、どんな動画を作るのか?
あまり人手をかけずに作れて(=エンコーディングは多少時間がかかってもよい)、ちょっとおしゃれそうな動画ということで、以下のようなものを考えてみました。
- 今までにアップロードした動画が右から左に流れていく。
- ただし、動画の大きさや流すタイミングやそのスピードなどは乱数で決定する。
これで勝てると思います!!
手作業よりもプログラミング。
手作業は正直言ってモチベーションが上がらないのと、プログラムを書く方が効率が良さそうだったり、正確に処理ができそうだったら、プログラムを書いた方がいいわけです。そこで、Python3で以下の2つのプログラムを書いてみます。
- YouTube Data API v3を使ってメタ情報を取り出し、JSONファイルに書き出すプログラム。
- 前項のJSONファイルを読み込んで、AviUtlエクスポートファイル(exoファイル)を出力するプログラム。
なぜ2個のプログラムに分割したのかというと、YouTube Data API v3は1回実行すれば十分なことと、2個目のプログラムについては試行錯誤的な調整が必要だったからです。2個目のプログラムの処理内容については後述します。
アップロードしたファイルの情報を取り出すプログラム
書くべきプログラムは決まったので、YouTube Data API v3のドキュメントや前の記事前の記事を参考にしてサクサクとプログラムを書いていきます。まずは、アップロードしたファイルの情報のうち、YouTubeにおけるID、タイトル名、ファイル名及び再生回数をJSONファイルに保存するプログラムを以下の通り書いてみました。
なお、プログラム中にあります”client_secrets.json”はYouTube Data APIの認証情報で、事前に作成しておく必要があります。
#!/usr/bin/python | |
import httplib2 | |
import os | |
import sys | |
import json | |
import argparse | |
from apiclient.discovery import build | |
from oauth2client.client import flow_from_clientsecrets | |
from oauth2client.file import Storage | |
from oauth2client.tools import argparser, run_flow | |
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及び元のファイル名のリスト等を取り出すための | |
# Pythonのテストプログラム。 | |
# URL: https://pandanote.info/?p=1510 | |
# | |
# 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, args) | |
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)) | |
# Retrieve the list of videos uploaded to the authenticated user's channel. | |
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", | |
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"] | |
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) |
スポンサーリンク
上記のプログラムをコマンドラインプロンプトから以下のように実行すると”UploadedVideoDataList.json”という名前の出力ファイルが生成(すでに同名のファイルが存在する場合には上書き)されます。
なお、出力ファイル名については以下のコマンドラインオプションで変更することもできます。
- -o, –output-file-prefix: 出力されるファイル名を指定します。指定しない場合には”UploadedVideoDataList.json”という名前の出力ファイルが生成(すでに同名のファイルが存在する場合には上書き)されます。
JSONのファイルがサクサクと作れるのはありがたいです。
exoファイルを出力するプログラムの作成。
次に、前項のプログラムで出力したJSONファイルを読み込んで、exoファイルを出力するプログラムを書きます。
以下のような感じになります。例によって色々と決め打ちですが、流す動画の特徴などを意識しながら調整できるところはこの段階で調整しておきます。
#!/usr/bin/env python | |
import io | |
import os | |
import sys | |
import json | |
import subprocess | |
import codecs | |
import re | |
import math | |
import random | |
import argparse | |
def dumpexo(): | |
global s | |
global fileid | |
global layer_count | |
global total_layer_count | |
global group_count | |
global outputPrefix | |
with open(outputPrefix+str(fileid)+'.exo','w',encoding='cp932') as f: | |
f.write('\r\n'.join(s)) | |
s = [] | |
s.append("[exedit]") | |
s.append("width={0:d}".format(width)) | |
s.append("height={0:d}".format(height)) | |
s.append("rate=30") | |
s.append("scale=1") | |
s.append("length=470") | |
s.append("audio_rate=44100") | |
s.append("audio_ch=2") | |
fileid += 1 | |
layer_count = 0 | |
group_count = 1 | |
parser = argparse.ArgumentParser(description='Command line options of findmp4inuvdl.py') | |
parser.add_argument('-o','--output-file-prefix', type=str, default='output', help='Prefix of the output file.') | |
parser.add_argument('-c','--cmd-file', type=str, default='cmd.txt', help='Filename of the command to find a mp4 file.') | |
parser.add_argument('-u','--uploaded-json-file-list', type=str, default='UploadedVideoDataList.json', help='Filename of a file which stores the list of videos.') | |
parser.add_argument('-l','--length-in-frames', type=int, default=3600, help='Length in frames.') | |
args = parser.parse_args() | |
params = vars(args) | |
outputPrefix = params['output_file_prefix'] | |
cmdFile = params['cmd_file'] | |
uvdl = params['uploaded_json_file_list'] | |
lengthInFrames = params['length_in_frames'] | |
cmdText = 'where /r c:\Users\Administrator' | |
with open(uvdl) as infile: | |
text = infile.read() | |
uvdl = json.loads(text) | |
with open(cmdFile) as cmdinfile: | |
cmdText = cmdinfile.read() | |
sys.stdout = io.TextIOWrapper(sys.stdout.buffer,encoding='utf-8') | |
sys.stderr = io.TextIOWrapper(sys.stderr.buffer,encoding='utf-8') | |
maxViewCount = -1 | |
# path 1 | |
for uploadedVideoData in uvdl: | |
if (uploadedVideoData["filename"] != '' and uploadedVideoData["filename"] != 'livestream.str'): | |
filepath = '' | |
cmd = cmdText.format(uploadedVideoData["filename"]) | |
#print(" cmd: {0:s}".format(cmd)); | |
try: | |
filepath = subprocess.check_output(cmd.split()) | |
#print(re.sub(r'[\r\n]',"",filepath.decode('utf-8'))) | |
uploadedVideoData["filepath"] = re.sub(r'[\r\n]+.*$',"",filepath.decode('utf-8')) | |
except: | |
print(" Error in {0:s}:".format(uploadedVideoData["filename"])) | |
if (maxViewCount < 0 or maxViewCount < int(uploadedVideoData["viewCount"])): | |
maxViewCount = int(uploadedVideoData["viewCount"]) | |
# path 2 | |
fileid=0 | |
s = [] | |
width=1920 | |
height=1080 | |
s.append("[exedit]") | |
s.append("width={0:d}".format(width)) | |
s.append("height={0:d}".format(height)) | |
s.append("rate=30") | |
s.append("scale=1") | |
s.append("length=3900") | |
s.append("audio_rate=44100") | |
s.append("audio_ch=2") | |
total_layer_count = 0 | |
layer_count = 0 | |
group_count = 1 | |
random.seed() | |
for uploadedVideoData in uvdl: | |
if 'filepath' in uploadedVideoData: | |
if layer_count > 98: | |
dumpexo() | |
s.append("[{0:d}]".format(layer_count)) | |
offset = random.randint(0,lengthInFrames-100) | |
length = random.randint(100,400) | |
startpos = 1+offset | |
s.append("start={0:d}".format(startpos)) | |
endpos = startpos+length | |
s.append("end={0:d}".format(endpos)) | |
s.append("layer={0:d}".format(layer_count+1)) | |
s.append("group={0:d}".format(group_count)) | |
s.append("overlay=1") | |
s.append("camera=0") | |
s.append("[{0:d}.0]".format(layer_count)) | |
s.append("_name=動画ファイル") | |
s.append("再生位置=1") | |
playspeed = random.randint(300,600) | |
s.append("再生速度={0:d}".format(playspeed)) | |
s.append("ループ再生=1") | |
s.append("アルファチャンネルを読み込む=0") | |
s.append("file={0:s}".format(uploadedVideoData["filepath"])) | |
s.append("[{0:d}.1]".format(layer_count)) | |
s.append("_name=標準描画") | |
ratio = 50.0*math.pow((float(uploadedVideoData["viewCount"]) if (int(uploadedVideoData["viewCount"])>1.0) else 1.0)/maxViewCount,0.2) | |
xlimit = -width*(1.0+ratio/100.0)/2 | |
s.append("X={0:f},{1:f},1".format(-xlimit,xlimit)) | |
#s.append("X=0.0,0.0,1") | |
ylimit = int(height*(1.0-ratio/100.0)/2.0) | |
y = random.randint(-ylimit,ylimit) | |
s.append("Y={0:d},{1:d},1".format(y,y)) | |
#s.append("Y=0.0,0.0,1") | |
s.append("Z=0.0,0.0,1") | |
s.append("拡大率={0:f}".format(ratio)) | |
#s.append("拡大率=100.0") | |
s.append("透明度=20.0") | |
s.append("回転=0.00") | |
s.append("blend=0") | |
layer_count += 1 | |
total_layer_count += 1 | |
s.append("[{0:d}]".format(layer_count)) | |
s.append("start={0:d}".format(startpos)) | |
s.append("end={0:d}".format(endpos)) | |
s.append("layer={0:d}".format(layer_count+1)) | |
s.append("group={0:d}".format(group_count)) | |
s.append("overlay=1") | |
s.append("audio=1") | |
s.append("[{0:d}.0]".format(layer_count)) | |
s.append("_name=音声ファイル") | |
s.append("再生位置=0.00") | |
s.append("再生速度={0:d}".format(playspeed)) | |
s.append("ループ再生=1") | |
s.append("動画ファイルと連携=1") | |
s.append("file={0:s}".format(uploadedVideoData["filepath"])) | |
s.append("[{0:d}.1]".format(layer_count)) | |
s.append("_name=標準再生") | |
s.append("音量=0.0") | |
s.append("左右=0.0") | |
layer_count += 1 | |
total_layer_count += 1 | |
group_count += 1 | |
with open(outputPrefix+str(fileid)+'.exo','w',encoding='cp932') as f: | |
f.write(re.sub('\n$','\n','\n'.join(s))) |
なお、以下のコマンドラインオプションを指定することもできます。
- -o, –output-file-prefix: 出力されるexoファイルの先頭の文字列を指定します。AviUtlでは1つのシーンに設定できるフレームの上限は100フレームですので、挿入するフレームの調整をしないで50枚以上の動画を使おうとすると、exoファイルを2個以上に分割する必要があります。本プログラムではこれに対応するため、出力するexoファイルの先頭の文字列のみを指定する方式としています。したがって、出力されるファイル名の末尾に50枚ごとに”0.exo”,”1.exo”,…で終わる文字列が付加されます。
- -c, –cmd-line: 読み込んだJSONファイルに記述されているファイルを探すためのコマンドが書かれたファイルを指定します。例えば以下のように記述したファイルを作成し、本コマンドラインオプションの引数として指定します。
where /r c:\User\Administrator
- -u, –uploaded-json-file-list: 動画のファイル名が記述されているJSONファイルを指定します。
- -l, –length-in-frames: 作成される動画の長さをフレーム数で指定します。
例えば、pandanote-infoチャンネル(2017年12月31日現在)に対して以下のような感じで実行する(入力用のJSONファイルはすでに作成されているものとします。)と、”pandanote_0.exo”という名前のフレーム数が1200(30fpsの場合は40秒の長さ)のファイルが生成されます。
exoファイルをAviUtlに読み込ませて、ひたすら試行錯誤。
前項で作成したexoファイルをAviUtlに読み込ませます。これは、AviUtlを起動して拡張機能のタイムラインへexoファイルをドラッグ&ドロップすると以下のような感じで読み込ませることができます。
読み込ませたら、エンコーディングを行って動画ファイルを作成します。
出来具合がイマイチだと思ったら、いろいろと調整してexoファイルを作成→エンコーディング→出来具合を確認の手順を繰り返します。
できあがった動画
前節で作成した動画にシャドーエフェクトを加えると、ちょっとおしゃれな動画の出来上がりです。
できあがった動画は以下のような感じになります。作成に要した時間はエンコーディングの時間を除くと1時間くらいです。
まとめ
この記事では、年末年始のクソ忙しい時にちょっとおしゃれな動画を作る方法について考察し、実際に作ってみました。
途中で乱数を使ったりするので、試行錯誤的な要素が入ってしまいますが、何かできるかわからないという意味でのワクワク感は出せると思います。
この記事は以上です。