Closed6

PythonからYouTubeに動画をアップロードする

tdtomatotdtomato

事前準備

動画

なにかしらの動画を用意しておく。

必要なパッケージのインストール

pip install --upgrade google-api-python-client google-auth-oauthlib google-auth-httplib2

YouTube Data API v3を有効化する

Google Cloud Consoleから、YouTubeのAPIを有効化する。

https://console.cloud.google.com/apis/library/youtube.googleapis.com?hl=ja&authuser=3&project=atomic-unity-429912-h7

OAuthクライアントIDを発行する

引き続きコンソールからOAuthの設定を進めていく。

「OAuthクライアントIDを発行」を選択する。

OAuthクライアントIDの発行のためには、まずOAuth同意画面の作成が必要だと言われる。
「OAuth同意画面の作成」を押下し、作成画面に移動。

「External」を選択して作成。

アプリ名、メールアドレスなど必要事項を入力して進めていく。
この時点でテストユーザー(自分のアカウントでOK)を追加しておくこと。

再度「認証情報」から「OAuthクライアントIDを発行」を選択して、発行を行う。

アプリケーションの種類は「デスクトップアプリ」を選択。
名前は任意(内部的な識別のみに利用)

作成したらJSONをダウンロードし、「client_secrets.json」という名前にしてプロジェクトディレクトリに配置する。

以上。

tdtomatotdtomato

upload_video.pyの作成

公式ドキュメントのコードをPython3と最新のパッケージを使うように書き換える。

upload_video.py
#!/usr/bin/env python3

import os
import random
import sys
import time

from googleapiclient.discovery import build
from googleapiclient.errors import HttpError
from googleapiclient.http import MediaFileUpload
from google.oauth2.credentials import Credentials
from google_auth_oauthlib.flow import InstalledAppFlow
from google.auth.transport.requests import Request
import argparse

# Maximum number of times to retry before giving up.
MAX_RETRIES = 10

# Always retry when these exceptions are raised.
RETRIABLE_EXCEPTIONS = (HttpError, IOError, ConnectionError)

# Always retry when an apiclient.errors.HttpError with one of these status
# codes is raised.
RETRIABLE_STATUS_CODES = [500, 502, 503, 504]

# 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 API Console at
# https://console.cloud.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 OAuth 2.0 access scope allows an application to upload files to the
# authenticated user's YouTube channel, but doesn't allow other types of access.
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():
    credentials = None
    if os.path.exists("token.json"):
        credentials = Credentials.from_authorized_user_file("token.json", YOUTUBE_UPLOAD_SCOPE)
    
    if not credentials or not credentials.valid:
        if credentials and credentials.expired and credentials.refresh_token:
            credentials.refresh(Request())
        else:
            flow = InstalledAppFlow.from_client_secrets_file(CLIENT_SECRETS_FILE, YOUTUBE_UPLOAD_SCOPE)
            credentials = flow.run_local_server(port=0)
        
        with open("token.json", "w") as token:
            token.write(credentials.to_json())

    return build(YOUTUBE_API_SERVICE_NAME, YOUTUBE_API_VERSION, credentials=credentials)


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)
    )

    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 response is not None:
                if 'id' in response:
                    print(f"Video id '{response['id']}' was successfully uploaded.")
                else:
                    exit(f"The upload failed with an unexpected response: {response}")
        except HttpError as e:
            if e.resp.status in RETRIABLE_STATUS_CODES:
                error = f"A retriable HTTP error {e.resp.status} occurred:\n{e.content}"
            else:
                raise
        except RETRIABLE_EXCEPTIONS as e:
            error = f"A retriable error occurred: {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(f"Sleeping {sleep_seconds} seconds and then retrying...")
            time.sleep(sleep_seconds)


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

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

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

実行

python upload_video.py --file="sample.mp4" --title="Sample Video" --description="sample description" --keywords="sample" --category="22" --privacyStatus="private"

ブラウザが開いて認可要求されるので、許可する。
設定がちゃんとできていれば、この時点で動画投稿できる。
プロジェクトディレクトリにトークン情報がtoken.jsonとして保存されており、以降はこのトークンを使ってリクエストを行うようになる。

tdtomatotdtomato

トラブルシュート: 403エラーの解消

初回の認可要求で403エラーになってしまった。

原因: OAuth同意画面のステータスがテストだった

参照: https://support.google.com/cloud/answer/10311615?hl=ja#publishing-status

解決方法

Publishするか、テスト用のユーザーを追加することで解消する。

今回はテストユーザーの追加で対応。(ただし、テストステータスだと一定期間後にリフレッシュ トークンが無効になるようなのでPublishすることを推奨する)

ユーザーを追加したら、再度upload_video.pyを実行して、認可する。

無事、動画を投稿できた。

tdtomatotdtomato

投稿数の制限(ユニット)について

YouTube Data APIの各リクエストには「ユニット」と呼ばれるリソース量が割り当てられている。
たとえば、動画のアップロード 1 回あたりの費用は 1600ユニットである。
デフォルトで 1 日あたり10,000 ユニットまで使うことができるので、1日あたりの動画投稿上限は6本まで

https://developers.google.com/youtube/v3/getting-started?hl=ja#quota

tdtomatotdtomato

アップロードした動画が強制で非公開になる問題

API経由で動画を投稿し続けていたら、急に投稿した動画が自動的に非公開になるようになった。
(手動アップロードは問題なし)
※数時間後に再度テストしたら特に問題なく公開できた。上限(1日6回)に達すると一定時間BANされる?

非公開にされた動画がスパム扱いされている旨のメールが届いていた。

未確認の API サービス経由だと、動画が自動的にロックされるらしい。
https://support.google.com/youtube/answer/7300965?hl=ja

なので下記の記事を参考に審査申請を行った。
https://qiita.com/Erytheia/items/3ae377b8c27a40690333

2日後にユースケースの詳細の提出を求める旨の連絡が来た。実際にプログラムを実行している動画と、システムのフロー図を送ったら、無事申請が完了した。

Please complete and submit the following information within seven (7) business days:

  • Kindly provide us with a detailed step by step visual reference of the complete use case i.e, screencast (video recording) as we need to understand how you are uploading videos to YouTube by using YouTube API services.
このスクラップは4ヶ月前にクローズされました