YouTubeAPIを利用して動画をアップロードする

## 概要
youtubeのAPIを利用して動画をアップロードする記事です。言語はPython3です。
以下の公式ドキュメントに沿って進めれば良いのですが、一部ハマった点などを追記しています。

[YouTube API - 動画のアップロード](https://developers.google.com/youtube/v3/guides/uploading_a_video?hl=ja)

## 環境
- MacOS
- Python3.8
- Pycharm

前提として、GCPでプロジェクトは作成済みとします。

## 事前準備

### google-api-python-clientのインストール

googleのAPIを利用するためのライブラリをインストールします。

```bash:
$ pip install google-api-python-client
```

### GCPでのアプリケーション登録

####  1. 承認情報の作成
以下のリンクを開き、GCPのコンソールから認証情報を作成します。今回はOAuth2.0を利用します。
[GCPコンソール - 認証情報](https://console.developers.google.com/apis/credentials?hl=ja)

(1) 認証情報を作成>OAuthクライアントID と進む (左上のプロジェクトは選びましょう)
<img width="765" alt="スクリーンショット 2020-01-21 23.30.36.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/451175/2431fdc6-b76a-7d1c-fac1-a61e8e7def13.png">

(2) 「その他」を選択。名前は任意の値を入力して作成
<img width="642" alt="スクリーンショット 2020-01-21 23.31.32.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/451175/dc4dd104-b943-4796-9e32-a793665005e9.png">

(3) クライアントID, クライアントシークレットが取得できる
<img width="739" alt="スクリーンショット 2020-01-21 23.32.38.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/451175/bb2de800-1b86-4ccd-c8cb-832a1f64e8ad.png">

また、OAuth同意画面が未作成の場合、作成しておきます。
<img width="813" alt="スクリーンショット 2020-01-22 0.13.48.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/451175/866fe916-c117-0f35-4339-48a6504e9775.png">

アプリケーション名、サポートメール欄を埋めて、画面下の作成ボタンを押します。
<img width="837" alt="スクリーンショット 2020-01-22 0.14.13.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/451175/614354cc-b3ce-e647-0f83-374a3d0fd16d.png">


#### 2. YouTubeAPIの有効化
以下のリンクを開き、GCPのコンソールからYouTube Data APIを検索します。
2020.1時点では「YouTube Data API v3」です。開いたら有効化のボタンがあるため有効化します。
[GCPコンソール - APIライブラリ](https://console.developers.google.com/apis/library?hl=ja)

<img width="885" alt="スクリーンショット 2020-01-21 23.43.01.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/451175/e9c9852e-aeb5-bd39-55af-27a53c3c1467.png">



## アップロード用ソースの作成

### フォルダ構成

```bash:作業フォルダ
$ tree
.
├── movies
│   └── sample001.MP4  # アップロード対象の動画
├── client_secrets.json
└── upload_video.py

```

### ソース

ソースは認証情報用のJSONとPythonの2つを用意します。

#### 認証情報
client_secrets.jsonを作成します。`client_id`と`client_secret`には、「1. 承認情報の作成」で作成したクライアントID, クライアントシークレットの値を設定します。

```json:client_secrets.json
{
  "web": {
    "client_id": "[[INSERT CLIENT ID HERE]]",
    "client_secret": "[[INSERT CLIENT SECRET HERE]]",
    "redirect_uris": [],
    "auth_uri": "https://accounts.google.com/o/oauth2/auth",
    "token_uri": "https://accounts.google.com/o/oauth2/token"
  }
}
```

#### Python

基本は[公式](https://developers.google.com/youtube/v3/guides/uploading_a_video?hl=ja)のソースを使えば良いのですが、Python2系で書かれていてPython3だとそのままでは動きません。なので、Python3で動くように少し書き換えます。書き換え箇所は以下の2つ。
・httplibからhttp.clientへ移行
・print文の書き換え
（余談ですが、インデントを4バイトにも変更）


```python:upload_video.py(スペースの都合上、公式のコメントは削除しています)
import http.client  # httplibはPython3はhttp.clientへ移行
import httplib2
import os
import random
import sys
import time

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


httplib2.RETRIES = 1
MAX_RETRIES = 10
RETRIABLE_EXCEPTIONS = (httplib2.HttpLib2Error,
                        IOError,
                        http.client.NotConnected,
                        http.client.IncompleteRead,
                        http.client.ImproperConnectionState,
                        http.client.CannotSendRequest,
                        http.client.CannotSendHeader,
                        http.client.ResponseNotReady,
                        http.client.BadStatusLine)
RETRIABLE_STATUS_CODES = [500, 502, 503, 504]
CLIENT_SECRETS_FILE = "client_secrets.json"
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 API 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))

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


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


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

    storage = Storage("%s-oauth2.json" % sys.argv[0])
    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
        )
    )

    insert_request = youtube.videos().insert(
        part=",".join(body.keys()),
        body=body,
        media_body=MediaFileUpload(options.file, chunksize=-1, resumable=True)
    )

    resumable_upload(insert_request)


def resumable_upload(insert_request):
    response = None
    error = None
    retry = 0
    while response is None:
        try:
            print("Uploading file...")  # print文
            status, response = insert_request.next_chunk()
            if response is not None:
                if 'id' in response:
                    print("Video id '%s' was successfully uploaded." % response['id'])
                else:
                    exit("The upload failed with an unexpected response: %s" % response)
        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)
            retry += 1
            if retry > MAX_RETRIES:
              exit("No longer attempting to retry.")
            max_sleep = 2 ** retry
            sleep_seconds = random.random() * max_sleep
            print("Sleeping %f seconds and then retrying..." % sleep_seconds)
            time.sleep(sleep_seconds)


if __name__ == '__main__':
    argparser.add_argument("--file", required=True, help="Video file to upload")
    argparser.add_argument("--title", help="Video title", default="Test Title")
    argparser.add_argument("--description",
                           help="Video description",
                           default="Test Description")
    argparser.add_argument("--category", default="22",
                           help="Numeric video category. " +
                                "See https://developers.google.com/youtube/v3/docs/videoCategories/list")
    argparser.add_argument("--keywords", help="Video keywords, comma separated",
                           default="")
    argparser.add_argument("--privacyStatus", choices=VALID_PRIVACY_STATUSES,
                           default=VALID_PRIVACY_STATUSES[0],
                           help="Video privacy status.")
    args = argparser.parse_args()

    if not os.path.exists(args.file):
        exit("Please specify a valid file using the --file= parameter.")

    youtube = get_authenticated_service(args)
    try:
        initialize_upload(youtube, args)
    except HttpError as e:
        print("An HTTP error %d occurred:\n%s" % (e.resp.status, e.content))

```

### 少しソース解説
- YouTube Data APIはsnippetに設定した内容がAPIに渡されます。`initialize_upload`内のbodyでsnippetとして渡す値のdictを作っています。
- snippetに渡す値は引数から`args`として取得しています。
- `get_authenticated_service`では、YouTube Data APIのリソース名と操作名(insert等)から、APIを利用するオブジェクト(?)をbuildしてreturnしています。今回の場合、buildしたオブジェクトを`initialize_upload`に渡して、`youtube.videos().insert`でアップロードを実行します。


## アップロードの実行

コマンドラインからだと以下の通り実行します。引数が多いので、何回か実行する場合シェルスクリプトにした方が楽かもしれません。

```bash:動画アップロードの実行
$ python upload_video.py --file="./movies/sample001.MP4" \
                       --title="Sample Movie" \
                       --description="This is a sample movie." \
                       --category="22" \
                       --privacyStatus="private"
```

引数の意味は以下の通りです。

- file: アップロードする動画のパス
- title: アップロード後の動画タイトル
- description: 動画の説明
- category: カテゴリー。デフォルトが22
- privacyStatus: 動画の公開設定。privateだと非公開で投稿

snippetとして指定できる項目や意味は以下に記載があります。

[YouTube Data API Videos](https://developers.google.com/youtube/v3/docs/videos?hl=ja#resource)
[YouTube Data API Videos: insert]
(https://developers.google.com/youtube/v3/docs/videos/insert?hl=ja)

なお、初めて実行した際に、ブラウザに遷移してGoogleアカウントの選択とYouTubeの管理許可を求める画面が表示されるので、画面に従って許可してください。
<img width="649" alt="スクリーンショット 2020-01-22 1.06.45.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/451175/20e89bdc-70ee-c4d9-0deb-d4d3a7b38d38.png">

Chromeで以下のような画面が表示された場合は、詳細を表示して左下の安全ではないページに移動

<img width="718" alt="スクリーンショット 2020-01-22 1.07.11.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/451175/f255d05a-c1a0-7b23-80e9-574a00597e58.png">

動画の管理を許可する。（もう一画面表示されることがあるが、引き続き許可する。）
<img width="558" alt="スクリーンショット 2020-01-22 1.07.22.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/451175/1598028d-9cfe-e6cf-12f0-9b33a4ffcc0f.png">


動画のサイズにもよりますが、少し経つと以下の通りアップロードが完了します。

```bash:実行結果
Uploading file...
Video id '[video id]' was successfully uploaded.
```
YouTubeの画面からも動画が投稿されていることが確認できます。
※イメージは投稿途中
<img width="788" alt="スクリーンショット 2020-01-22 0.01.48.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/451175/1e82e5ec-3e0a-fbb6-9086-3fd9f8906428.png">




## ハマったこと：APIの利用制限
もともと、大量の動画を一括でアップロードするためにAPIを利用したかったのですが、繰り返しAPIを実行するようなプログラムを書いて実行すると、以下のエラーが発生しました。

```bash
Uploading file...
An HTTP error 403 occurred:
b'{\n "error": {\n  "errors": [\n   {\n    "domain": "youtube.quota",\n    "reason": "quotaExceeded",\n    "message": "The request cannot be completed because you have exceeded your \\u003ca href=\\"/youtube/v3/getting-started#quota\\"\\u003equota\\u003c/a\\u003e."\n   }\n  ],\n  "code": 403,\n  "message": "The request cannot be completed because you have exceeded your \\u003ca href=\\"/youtube/v3/getting-started#quota\\"\\u003equota\\u003c/a\\u003e."\n }\n}\n'
```

調べてみると、APIでは1日あたり10,000で制限されており、動画のアップロードでは約1,600ユニットを利用するとのこと。なので、YouTubeにAPIを経由して動画をあげられるのは、おおよそ6本/1日のようです...

[YouTube Data API - エラー](https://developers.google.com/youtube/v3/docs/errors?hl=ja)
[YouTube Data API の概要 - クォータの使用量](https://developers.google.com/youtube/v3/getting-started?hl=ja#quota)

これを引き上げるには申請が必要となりますが、ただの個人利用だったため断念しました。せめて、30本/1日だったらやりたいことはできたのですが...6本だと手作業の方が早いかもですね...

アプリケーションの中で動画のアップロードとかしたい場合に役立つかもしれません。

## 参考
[(公式)YouTube Data API](https://developers.google.com/youtube/v3?hl=ja)