YouTubeAPIを使ってみた

大量の映像ファイルをアップする必要があり、いちいちYouTubeの画面から手で行うのは大変なのでツールをつくることに。
APIどうやって使うの？状態から、ぐぐって出てきた公式サンプルをいじりながら四苦八苦した形跡です。

### 【Developer Concole登録】
[Google Developer Console](https://console.developers.google.com)に登録し、認証情報を作成する必要がある。
ここで発行されたClientID/ClientSecretを使う。
[⇒参照](https://developers.google.com/api-client-library/python/guide/aaa_client_secrets)

※URLやUIはちょくちょく変更されている気がします。

### 【Python用Libraryインストール】
たまたま見つけた[サンプル](https://developers.google.com/youtube/v3/code_samples/python?hl=ja#upload_a_video)がPythonだったこと、Windowsローカルで利用したかったことがあり、不慣れながらPythonでの実装をすることに。

APIがPython3系に対応していないので、Python2系を。
ライブラリのインストールはpipで一発。

```
pip install --upgrade google-api-python-client
```

[⇒参照](https://developers.google.com/api-client-library/python/start/installation?hl=ja)

### 【実装】
ツリー構造の複数のファイルをアップロードする。
以下のような構成でファイルが整理されるので、フォルダツリーの名前で動画タイトルにすることに。

---
1_hoge
　└11_hogehoge
　　└1.mp4
　└12_hogefuga
　　└2.mp4
2_fuga
　└21_fugafuga
　　└2.mp4

↓それぞれ以下のタイトルでアップロード

hoge_hogehoge
hoge_hogefuga
fuga_fugafuga

---

サンプルとにらめっこしながら最終的にはこんな感じに。
コマンドプロンプトから引数にルートフォルダ渡す形で実行。
アップロード済みのファイルは頭に印をつけて、重複アップロードしないようにした。

```upload.py
#!/usr/bin/python
# -*- coding: utf-8 -*-

import httplib
import httplib2
import os
import random
import sys
import time
import logging
import datetime
import csv
import argparse
import re

from apiclient.discovery import build
from apiclient.errors import HttpError
from apiclient.http import MediaFileUpload
from oauth2client.client import flow_from_clientsecrets
from oauth2client.file import Storage
#from oauth2client.tools import argparser, run_flow
from oauth2client.tools import run_flow

httplib2.RETRIES = 1

MAX_RETRIES = 10

RETRIABLE_EXCEPTIONS = (httplib2.HttpLib2Error, IOError, httplib.NotConnected,
  httplib.IncompleteRead, httplib.ImproperConnectionState,
  httplib.CannotSendRequest, httplib.CannotSendHeader,
  httplib.ResponseNotReady, httplib.BadStatusLine)

RETRIABLE_STATUS_CODES = [500, 502, 503, 504]

CLIENT_SECRETS_FILE = "client_secrets.json"

YOUTUBE_FULL_SCOPE = "https://www.googleapis.com/auth/youtube"
YOUTUBE_API_SERVICE_NAME = "youtube"
YOUTUBE_API_VERSION = "v3"

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(sys.argv[0]),
                                   CLIENT_SECRETS_FILE))

VALID_PRIVACY_STATUSES = ("public", "private", "unlisted")

UPLOADED_MARK = "==="

def get_authenticated_service(args):
  flow = flow_from_clientsecrets(CLIENT_SECRETS_FILE,
    scope=YOUTUBE_FULL_SCOPE,
    message=MISSING_CLIENT_SECRETS_MESSAGE)

  path, ext = os.path.splitext(CLIENT_SECRETS_FILE)
  storage = Storage("%s-oauth2.json" % path)
  credentials = storage.get()

  if credentials is None or credentials.invalid:
    credentials = run_flow(flow, storage, args)

  return build(YOUTUBE_API_SERVICE_NAME, YOUTUBE_API_VERSION,
    http=credentials.authorize(httplib2.Http()))

def initialize_upload(youtube, options):
  tags = None
  if options.keywords:
    tags = options.keywords.split("^")

  body=dict(
    snippet=dict(
      title=options.title,
      description=options.description,
      tags=tags,
      categoryId=options.category
    ),
    status=dict(
      privacyStatus=options.privacyStatus
    )
  )

  # Call the API's videos.insert method to create and upload the video.
  insert_request = youtube.videos().insert(
    part=",".join(body.keys()),
    body=body,
    # The chunksize parameter specifies the size of each chunk of data, in
    # bytes, that will be uploaded at a time. Set a higher value for
    # reliable connections as fewer chunks lead to faster uploads. Set a lower
    # value for better recovery on less reliable connections.
    #
    # Setting "chunksize" equal to -1 in the code below means that the entire
    # file will be uploaded in a single HTTP request. (If the upload fails,
    # it will still be retried where it left off.) This is usually a best
    # practice, but if you're using Python older than 2.6 or if you're
    # running on App Engine, you should set the chunksize to something like
    # 1024 * 1024 (1 megabyte).
    media_body=MediaFileUpload(options.file, chunksize=-1, resumable=True)
  )

  return resumable_upload(insert_request)

# This method implements an exponential backoff strategy to resume a
# failed upload.
def resumable_upload(insert_request):
  response = None
  error = None
  retry = 0
  while response is None:
    try:
      print("Uploading file...")
      status, response = insert_request.next_chunk()
      if 'id' in response:
        print("Video id '%s' was successfully uploaded." % response['id'])
        return response
      else:
        print("The upload failed with an unexpected response: %s" % response)
        sys.exit(1)
    except HttpError as e:
      if e.resp.status in RETRIABLE_STATUS_CODES:
        error = "A retriable HTTP error %d occurred:\n%s" % (e.resp.status, e.content)
      else:
        raise
    except RETRIABLE_EXCEPTIONS as e:
      error = "A retriable error occurred: %s" % e

    if error is not None:
      #print(error)
      logging.warning(error)
      retry += 1
      if retry > MAX_RETRIES:
        print("No longer attempting to retry.")
        sys.exit(1)

      max_sleep = 2 ** retry
      sleep_seconds = random.random() * max_sleep
      print "Sleeping %f seconds and then retrying..." % sleep_seconds
      time.sleep(sleep_seconds)

def get_argparser():
  #copy of oauth2client\tools.py#_CreateArgumentParser()
  argparser = argparse.ArgumentParser(add_help=False, conflict_handler='resolve')
  argparser.add_argument('--auth_host_name', default='localhost',
                         help='Hostname when running a local web server.')
  argparser.add_argument('--noauth_local_webserver', action='store_true',
                         default=False, help='Do not run a local web server.')
  argparser.add_argument('--auth_host_port', default=[8080, 8090], type=int,
                         nargs='*', help='Port web server should listen on.')
  argparser.add_argument('--logging_level', default='ERROR',
                         choices=['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'],
                         help='Set the logging level of detail.') 
  return argparser

def decode_windows(str_p):
  return str_p.decode('cp932', errors='replace')

def encode_windows(str_u):
  return str_u.encode('cp932', errors='replace')

if __name__ == '__main__':
  pdir = sys.argv[1]

  try:
    argparser = get_argparser()
    argparser.add_argument("pdir", default=decode_windows(pdir))

    cnt = 0

    target = os.path.join(os.pardir, pdir)
    for root, dirs, files in os.walk(decode_windows(target)):
      if len(files) > 1: #命名上、フォルダ内に複数ファイルがあったらダメ
        print 'SKIP - multi files in a directory. ' + root
        continue

      for f in files:
        if f.endswith('.lnk'): #リンクは無視
          continue

        if f.startswith(UPLOADED_MARK): #アップロード済みの印がついていたらスキップ
          continue

        cnt += 1

        afile = os.path.join(root, f)
        atitle = re.sub(r'^.*' + decode_windows(pdir), '', root) #ルートフォルダ除去
        atitle = re.sub(r'\\[^\\_]*_', ' ', atitle).replace('\\', ' ') #「_」以前の除去、区切りの空白化
        atitle = re.sub(r'^ ', '', atitle) #先頭空白除去
        adescription = atitle
        acategory = "17" #固定
        akeywords = atitle.replace(' ', '^')
        aprivacy = "private" #非公開

        argparser.add_argument("--file", help="Video file to upload", default=afile)
        argparser.add_argument("--title", help="Video title", default=atitle)
        argparser.add_argument("--description", help="Video description", default=adescription)
        argparser.add_argument("--category", help="Numeric video category. " +
            "See https://developers.google.com/youtube/v3/docs/videoCategories/list",
             default=acategory)
        argparser.add_argument("--keywords", help="Video keywords, caret separated",
             default=akeywords)
        argparser.add_argument("--privacyStatus", choices=VALID_PRIVACY_STATUSES,
             help="Video privacy status.", default=aprivacy)
        args = argparser.parse_args()

        if not os.path.exists(afile):
          sys.exit(1)

        youtube = get_authenticated_service(args)
        try:
          print "Uploading File: %s" % atitle
          res = initialize_upload(youtube, args)
          print "Uploaded!! total: %d" % cnt
        except HttpError as e:
          sys.exit(1)

        renamed = UPLOADED_MARK + res['id'] + UPLOADED_MARK + f
        os.rename(encode_windows(afile), encode_windows(os.path.join(root, renamed)))

  except Exception as ex:
    print '\n'
    print ex
    print '\n'
    sys.exit(1)
```

client_secrets.json は最初に発行したアカウントの情報を格納。

これを実行するとブラウザが立ち上がり、アップロード先のYouTubeアカウントを選択後、認証画面が表示されるので承認すると、アップロードが開始される。

### 【もやもや】
元のサンプルが1ファイルを対象としたものだったので、複数ファイルを回すために無理矢理argumentを使い回している。
多分、本来はもっとスマートな方法があるんだろうけど…