Fork me on GitHub

Загрузка видео на youtube Aug 22, 2015

В одном из моих собственных проектов, возникла задача автоматической загрузки видео на канал на youtube. Делается это достаточно просто, при помощи гугловского api клиента для python. Единственное затруднение вызвало полумагическое получение ключей доступа к api.

Авторизация

Все коды доступа и ключи авторизации, использованные в статье, вымышленные.
Любое совпадение с реально существующими или когда-либо существовавшими ключами случайно.

Создание нового проекта

Для авторизации в сервисах google с помощью протокола oauth2 необходимо зарегистрировать приложение и дать ему соответсвующие права. Для этого нужно перейти в консоль разработчика.

Нажимаем на кнопку Create Project, выбираем имя и создаем новое приложение. После того как приложение будет создано, нужно добавить ему необходимые доступы к google API.

Create Project

Для загрузки видео на youtube нужно добавить YouTube Data API. Для этого переходим во вкладку APIs & authAPIs. Также во вкладке APIs & authCredentials нужно добавить доступы для oauth2 авторизации.

Add Oauth2

Указываем тип приложения Other. Получаем доступы для авторизации: идентификатор и пароль.

  • Client ID 230452130504-3uca1rp4ntlh06hdnsdbj50sqagaqfkt.apps.googleusercontent.com
  • Client secret qawsWCd3J6HTRvnqsjYUpgH9

Права доступа к аккаунту

Получив данные для авторизации, нужно перейти по следующей ссылке, заменив в ней параметр client_id на тот, что Вы получили в предыдущем шаге.

    https://accounts.google.com/o/oauth2/auth?
        client_id=230452130504-3uca1rp4ntlh06hdnsdbj50sqagaqfkt.apps.googleusercontent.com&
        redirect_uri=urn:ietf:wg:oauth:2.0:oob&
        scope=https://www.googleapis.com/auth/youtube&
        response_type=code&
        access_type=offline

Далее выбираем к какому аккаунту гугл будет иметь доступ приложение, и соответсвенно к какому каналу на ютубе.

account choice

ch choice

Соглашаемся с доступом к управлению каналом.

access

Получаем токен авторизации следующего вида 4/Rw6A9raJQ3PrPWL0Q9z49guYu89FZoz322RySVFtzNc.

code

Обновляемый токен

После этого необходимо получить, так называемый, refresh_token, для этого нужно отправить POST запрос с токеном авторизации по адресу https://accounts.google.com/o/oauth2/token. Сделать это легко, при помощи консольной утилиты curl.

data=""\
"code=4/Rw6A9raJQ3PrPWL0Q9z49guYu89FZoz322RySVFtzNc&"\
"client_id=230452130504-3uca1rp4ntlh06hdnsdbj50sqagaqfkt.apps.googleusercontent.com&"\
"client_secret=qawsWCd3J6HTRvnqsjYUpgH9&"\
"redirect_uri=urn:ietf:wg:oauth:2.0:oob&"\
"grant_type=authorization_code"

curl --data $data "https://accounts.google.com/o/oauth2/token"

Токен авторизации сработает только один раз, при повторной попытке отправить его будет получено Code was already redeemed.. В ответ на корректный запрос, гугл возвращает json с временным токеном доступа и постоянным обновляемым токеном (собственно он нам и нужен).

{
    "access_token" : "ya29.1wGYJU7NP7Ul69c13aE1Vuvbx0LfxrsgMiBjXdNY3sU3tuE9LmuJ3nOGHeb3e_824LH0",
    "token_type" : "Bearer",
    "expires_in" : 3600,
    "refresh_token" : "1/g1ixyts83iMrtR71oFqwGp3LSGbHz6ByxsBThrHRWCNIgOrJDtdun6zK6XiATCKT"
}

Токен доступа

Получив обновляемый токен, можем с его помощью каждый раз получать рабочий токен доступа, который предоставляется временем на 3600 секунд.

data=""\
"refresh_token=1/g1ixyts83iMrtR71oFqwGp3LSGbHz6ByxsBThrHRWCNIgOrJDtdun6zK6XiATCKT&"\
"client_id=230452130504-3uca1rp4ntlh06hdnsdbj50sqagaqfkt.apps.googleusercontent.com&"\
"client_secret=qawsWCd3J6HTRvnqsjYUpgH9&"\
"grant_type=refresh_token"

curl --data $data "https://accounts.google.com/o/oauth2/token"

В ответ гугл возвращает json с временным токеном доступа.

{
  "access_token" : "ya29.2AHpPjacO0prQkip0svapohuZtoK0wqdh7u0ohH49l0WWwrSyss7CWiwzMy5wX967tWsjQ",
  "token_type" : "Bearer",
  "expires_in" : 3600
}
Автоматическое получение токена доступа

Получать этот токен доступа нужно будет каждые раз, при подключении к api. Для этого напишем простую функцию на python 3 с использованием стандартной библиотеки urllib.request

import json
import urllib
import urllib.request


def get_auth_code():
    """ Get access token for connect to youtube api """
    oauth_url = 'https://accounts.google.com/o/oauth2/token'
    # create post data
    data = dict(
        refresh_token=settings.YOUTUBE_REFRESH_TOKEN,
        client_id=settings.YOUTUBE_CLIENT_ID,
        client_secret=settings.YOUTUBE_CLIENT_SECRET,
        grant_type='refresh_token',
    )

    headers = {
        'Content-Type': 'application/x-www-form-urlencoded',
        'Accept': 'application/json'
    }

    data = urllib.parse.urlencode(data).encode('utf-8')
    # make request and take response
    request = urllib.request.Request(oauth_url, data=data, headers=headers)
    response = urllib.request.urlopen(request)

    # get access_token from response
    response = json.loads(response.read().decode('utf-8'))
    return response['access_token']

Oauth2 авторизация

Вот теперь, мы наконец подошли к самой oauth2 авторизации в сервисах гугл. Для этого необходимо использовать следующие дополнительные библиотеки:

Далее, используя выше описанную функцию получения временного токена, создаем подключение к youtube api.

Вообще в руководстве по работе с youtube api рекомендуют использовать построение oauth2 подключения с использованием объекта flow_from_clientsecrets, примерно так:

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

Но, как выяснилось на практике, такой подход, требует при каждой загрузки, давать разрешение на подключение к аккаунту youtube вручную, это не очень удобно. Учитывая, что можно замечательным образом получать токен авторизации, из обновляемого токена, мы будем использовать для создания oauth2 подключения - объект AccessTokenCredentials.

import httplib2

from oauth2client.client import AccessTokenCredentials
from apiclient.discovery import build


def get_authenticated_service():
    """ Create youtube oauth2 connection """
    # make credentials with refresh_token auth
    credentials = AccessTokenCredentials(
        access_token=get_auth_code(), user_agent='my-awesome-project/1.0'
    )
    # create connection to youtube api
    return build(
        'youtube', 'v3', http=credentials.authorize(httplib2.Http())
    )

Теперь мы имеем созданное подключение, которое можно использовать для работы с api.

Загрузка видео

Имея готовое подключение к api загрузка видео происходит элементарно.

Определим функцию инициализации загрузки, которая принимает в качестве аргументов подключение к youtube api и объект с информацией о видео.

from apiclient.http import MediaFileUpload


def initialize_upload(youtube, card):
    """ Create youtube upload data """
    # create video meta data
    body = card.youtube_meta_data()
    # Call the API's videos.insert method to create and upload the video
    insert_request = youtube.videos().insert(
        part=",".join(body.keys()), body=body,
        media_body=MediaFileUpload(card.video.path, chunksize=-1, resumable=True))
    # wait for file uploading
    return resumable_upload(insert_request)

Метод youtube_meta_data должен возвращать словарь описания видео согласно формату, например:

{
    "snippet": {
        "title": "Summer vacation in California",
        "description": "Had fun surfing in Santa Cruz",
        "tags": ["surfing", "Santa Cruz"],
        "categoryId": "22"
    },
    "status": {
        "privacyStatus": "private"
    }
}

В моем случае данный метод имел следующий вид:

    def youtube_meta_data(self):
        """ Create metadata dict for youtube video upload """
        return dict(
            snippet=dict(
                title=settings.YOUTUBE_TITLE.format(coord=self.position),
                tags=settings.YOUTUBE_TAGS,
                categoryId=settings.YOUTUBE_CATEGORY_ID,
                description='{desc}\n{site_url}/{card_id}'.format(
                    desc=self.description, site_url=settings.SITE_URL, card_id=self.get_absolute_url()),
            ),
            status=dict(
                privacyStatus=settings.YOUTUBE_PRIVACY_STATUS,
            ),
            recordingDetails=dict(
                location=dict(
                    latitude=str(self.position.latitude),
                    longitude=str(self.position.longitude),
                ),
            ),
        )

После инициализации загрузки, необходимо поддерживать соединение и дождаться ответа от ютуба с идентификатором видео. Для этого будем использовать следующую функцию.

import random
import http
import httplib2

# Explicitly tell the underlying HTTP transport library not to retry, since we are handling retry logic ourselves.
httplib2.RETRIES = 1

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

# Always retry when these exceptions are raised.
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)

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


def resumable_upload(insert_request):
    response = None
    error = None
    retry = 0
    while response is None:
        try:
            status, response = insert_request.next_chunk()
            if 'id' in response:
                return response['id']
        except HttpError as err:
            if err.resp.status in RETRIABLE_STATUS_CODES:
                error = True
            else:
                raise
        except RETRIABLE_EXCEPTIONS:
            error = True

        if error:
            retry += 1
            if retry > MAX_RETRIES:
                raise Exception('Maximum retry are fail')

            sleep_seconds = random.random() * 2 ** retry
            time.sleep(sleep_seconds)

Таким образом, загрузка видео запускается функцией initialize_upload:

video_id = initialize_upload(get_authenticated_service(), card)

Полный код загрузки видео можно посмотреть в gist.

Санкции

Поскольку капиталистический запад, в лице корпорации зла, наложил на меня свои, безосновательные, санкции. Ограничив тем самым мое право доступа к свободной информации. Для работы с youtube api мне необходимо использовать vpn подключение.

VPN соединение

В качестве vpn соединения я использую ssh туннель и локальное socks5 прокси на 1080 порту. Включаю/отключаю ssh тунель при помощи библиотеки subprocess.

import subprocess

# init ssh connection
subprocess.Popen(['ssh', '-fN', '-D', '1080', 'forward@vpn_connection'])

# desctroy ssh connection
subprocess.Popen(['pkill', '-f', 'forward@vpn_connection'])

Не правильно

Что бы подключиться к локальному socks5 прокси, необходимо использовать библиотеку socksipy, как показано в примере работы с httplib2:

import httplib2
import socks

h = httplib2.Http(proxy_info = httplib2.ProxyInfo(socks.PROXY_TYPE_SOCKS5, 'localhost', 1080))
r, c = h.request('https://l2.io/ip')

Правильно

Но, вышеуказанный способ не работает. Библиотека socksipy не поддерживает python 3, поэтому необходимо делать по-другому. Использовать библиотеку socksipy-branch (gist зеркало). И оборачивать httplib2 с помощью метода wrapmodule:

import httplib2
import socks

socks.setdefaultproxy(socks.PROXY_TYPE_SOCKS5, 'localhost', 1080)
socks.wrapmodule(httplib2)
h = httplib2.Http()
r, c = h.request('https://l2.io/ip')