Или почему стандартная библиотека Google не работает, когда вам это действительно нужно

Вступление: зачем вообще это нужно

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

Звучит просто? OAuth2, токен, пару строк кода — и готово.

Я тоже так думал. Пока не столкнулся с реальностью: стандартная библиотека google_auth_oauthlib не работает на headless сервере (сервере без графического интерфейса).

Спойлер: мне понадобилось 17 итераций, чтобы получить рабочий токен. Но зато теперь у меня есть решение, которое работает гарантированно. И я расскажу вам всё — от настройки Google Cloud Console до финального скрипта.


Шаг 1: Создание проекта в Google Cloud Console

Заходим на console.cloud.google.com

Если у вас нет проекта — создайте новый:

  1. Нажмите на выпадающий список проектов вверху страницы
  2. Нажмите "Новый проект" (New Project)
  3. Введите название (например, YouTube Bot Project)
  4. Нажмите "Создать"

⏳ Подождите 10-20 секунд, пока проект создастся.


Шаг 2: Включение YouTube Data API v3

Это критически важный шаг, который я пропустил в первый раз и потратил час на отладку.

Путь: Левое меню → APIs & ServicesLibrary

Или напрямую: console.cloud.google.com/apis/library

  1. В поиске введите: YouTube Data API v3
  2. Нажмите на результат
  3. Нажмите синюю кнопку "ENABLE" (Включить)

⏳ Подождите 2-3 минуты. Google пишет, что изменения распространяются по системе. Не торопитесь.

Проверка: После включения вы должны увидеть кнопку "MANAGE" вместо "ENABLE".


Шаг 3: Настройка OAuth Consent Screen

Путь: Левое меню → APIs & ServicesOAuth consent screen

  1. Выберите External (Внешний) и нажмите Create
  2. Заполните обязательные поля:
    • App name: Название вашего бота (например, YouTube Automation Bot)
    • User support email: Ваш email
    • Developer contact information: Ваш email
  3. Нажмите "SAVE AND CONTINUE"
  4. Scopes — пропустите (нажмите "SAVE AND CONTINUE")
  5. Test users — добавьте свой Google аккаунт как тестового пользователя
  6. Нажмите "SAVE AND CONTINUE""BACK TO DASHBOARD"

Важно: Пока приложение в режиме Testing, только добавленные тестовые пользователи могут авторизовываться. Для продакшена нужно опубликовать приложение (кнопка "PUBLISH APP").


Шаг 4: Создание OAuth 2.0 Client ID

Путь: Левое меню → APIs & ServicesCredentials

  1. Нажмите "+ CREATE CREDENTIALS""OAuth client ID"
  2. Application type: выберите Desktop app
  3. Name: любое (например, YouTube Desktop Client)
  4. Нажмите "CREATE"

Появится окно с Client ID и Client Secret. Не закрывайте его!

Нажмите кнопку "DOWNLOAD JSON" (Скачать JSON).

Файл скачается с названием типа client_secret_XXXXX.apps.googleusercontent.com.json.

Переименуйте его в client_secret.json и положите в корень вашего проекта.


Шаг 5: Структура проекта

К этому моменту у вас должно быть:

your_project/
├── client_secret.json          # Скачанный файл от Google
├── scripts/
│   ├── get_youtube_token.py    # Скрипт получения токена
│   └── youtube_channels.py     # Модуль работы с каналами
├── tests/
│   └── test_youtube_token.py   # Тест прав доступа
└── .env                        # Файл с токенами (создастся автоматически)

Шаг 6: Почему стандартная библиотека не работает

Стандартный подход выглядит так:

from google_auth_oauthlib.flow import InstalledAppFlow

flow = InstalledAppFlow.from_client_secrets_file(
    'client_secret.json',
    scopes=['https://www.googleapis.com/auth/youtube']
)
credentials = flow.run_local_server(port=8080)

Проблемы:

  1. Address already in use — порт 8080 может быть занят (Apache, Nginx, другой сервис)
  2. could not locate runnable browser — на сервере нет браузера, а run_local_server() пытается его открыть
  3. Missing required parameter: redirect_uri — библиотека требует, чтобы redirect_uri был в JSON файле
  4. Нельзя править JSON — если вы скачали JSON из Google Console, его лучше не трогать

Я потратил 15 кругов на попытки заставить стандартную библиотеку работать. Не тратьте время — используйте ручную реализацию.


Шаг 7: Ручная реализация OAuth2 с PKCE

Вот полный скрипт get_youtube_token.py:

import os
import re
import json
import base64
import secrets
import hashlib
import requests
from urllib.parse import urlparse, parse_qs
from pathlib import Path

# ============================================
# НАСТРОЙКИ
# ============================================
PROJECT_ROOT = Path(__file__).parent.parent
JSON_FILE = PROJECT_ROOT / 'client_secret.json'
ENV_FILE = PROJECT_ROOT / '.env'
CHANNEL_NAME = 'SUKKOGRAD'  # Имя для переменных окружения
REDIRECT_URI = 'http://localhost:8080/'
SCOPES = [
    'https://www.googleapis.com/auth/youtube',
    'https://www.googleapis.com/auth/youtube.force-ssl'
]

# ============================================
# 1. Читаем JSON (только чтение, без правки!)
# ============================================
with open(JSON_FILE, 'r', encoding='utf-8') as f:
    data = json.load(f)

if 'installed' in data:
    creds = data['installed']
elif 'web' in data:
    creds = data['web']
else:
    raise ValueError('Неизвестная структура JSON')

CLIENT_ID = creds['client_id']
CLIENT_SECRET = creds['client_secret']
AUTH_URI = creds['auth_uri']
TOKEN_URI = creds['token_uri']

# ============================================
# 2. Генерируем PKCE вручную
# ============================================
code_verifier = secrets.token_urlsafe(96)[:128]
code_challenge = base64.urlsafe_b64encode(
    hashlib.sha256(code_verifier.encode()).digest()
).rstrip(b'=').decode()

# ============================================
# 3. Собираем URL авторизации
# ============================================
state = secrets.token_urlsafe(32)
params = {
    'client_id': CLIENT_ID,
    'redirect_uri': REDIRECT_URI,
    'response_type': 'code',
    'scope': ' '.join(SCOPES),
    'state': state,
    'code_challenge': code_challenge,
    'code_challenge_method': 'S256',
    'access_type': 'offline',
    'prompt': 'consent',
}
auth_url = f"{AUTH_URI}?{'&'.join(f'{k}={requests.utils.quote(v)}' for k, v in params.items())}"

# ============================================
# 4. Пользователь открывает URL локально
# ============================================
print('\n' + '='*60)
print('ОТКРОЙ ЭТУ ССЫЛКУ В БРАУЗЕРЕ НА СВОЕЙ ЛОКАЛЬНОЙ МАШИНЕ:')
print('='*60)
print(auth_url)
print('='*60)
print('\nПосле авторизации ты будешь перенаправлен на:')
print(f'  {REDIRECT_URI}?code=...&state=...')
print('\nБраузер покажет ошибку "не удалось подключиться" — это нормально.')
print('Скопируй ПОЛНЫЙ URL из адресной строки:')

redirected_url = input('\nВставь сюда весь URL: ').strip()

# ============================================
# 5. Парсим код из URL
# ============================================
parsed = parse_qs(urlparse(redirected_url).query)

if 'code' not in parsed:
    print('❌ В URL нет параметра code. Проверь ссылку.')
    exit(1)

if parsed.get('state', [None])[0] != state:
    print('❌ State не совпадает. Возможна атака CSRF.')
    exit(1)

code = parsed['code'][0]
print(f'✅ Код получен: {code[:20]}...')

# ============================================
# 6. Обмениваем код на токен через requests
# ============================================
token_data = {
    'client_id': CLIENT_ID,
    'client_secret': CLIENT_SECRET,
    'code': code,
    'code_verifier': code_verifier,
    'grant_type': 'authorization_code',
    'redirect_uri': REDIRECT_URI,
}

response = requests.post(TOKEN_URI, data=token_data)

if response.status_code != 200:
    print(f'❌ Ошибка получения токена: {response.status_code}')
    print(response.text)
    exit(1)

token_info = response.json()
access_token = token_info['access_token']
refresh_token = token_info.get('refresh_token')

print(f'✅ Access token получен')
print(f'✅ Refresh token: {refresh_token[:20]}...' if refresh_token else '⚠️ Refresh token отсутствует')

# ============================================
# 7. Сохраняем в .env
# ============================================
def update_env(key: str, value: str, path: str = ENV_FILE):
    lines = []
    if os.path.exists(path):
        with open(path, 'r') as f:
            lines = f.readlines()
    
    pattern = re.compile(rf'^{key}=')
    updated = False
    for i, line in enumerate(lines):
        if pattern.match(line.strip()):
            lines[i] = f'{key}={value}\n'
            updated = True
            break
    
    if not updated:
        lines.append(f'\n{key}={value}\n')
    
    with open(path, 'w') as f:
        f.writelines(lines)

update_env(f'YOUTUBE_ACCESS_TOKEN_{CHANNEL_NAME}', access_token)
if refresh_token:
    update_env(f'YOUTUBE_REFRESH_TOKEN_{CHANNEL_NAME}', refresh_token)

print(f'\n✅ Токены сохранены в {ENV_FILE}')
print(f'   YOUTUBE_ACCESS_TOKEN_{CHANNEL_NAME}')
print(f'   YOUTUBE_REFRESH_TOKEN_{CHANNEL_NAME}')

Шаг 8: Как работает скрипт

  1. Читает client_secret.json — получает client_id, client_secret, auth_uri, token_uri
  2. Генерирует PKCE — создает code_verifier и code_challenge (требование Google с 2026 года)
  3. Формирует URL авторизации — добавляет все параметры, включая динамический redirect_uri
  4. Пользователь открывает URL — на своей локальной машине (не на сервере!)
  5. Парсит код из redirect URL — даже если браузер показывает ошибку
  6. Обменивает код на токен — прямой POST запрос через requests
  7. Сохраняет токены в .env — с канал-специфичными именами

Шаг 9: Запуск скрипта

python3 scripts/get_youtube_token.py

Скрипт выведет ссылку:

============================================================
ОТКРОЙ ЭТУ ССЫЛКУ В БРАУЗЕРЕ НА СВОЕЙ ЛОКАЛЬНОЙ МАШИНЕ:
============================================================
https://accounts.google.com/o/oauth2/auth?client_id=XXXXX&redirect_uri=http://localhost:8080/&...
============================================================

Скопируйте ссылку и откройте в браузере на своей локальной машине.


Шаг 10: Авторизация (ВАЖНЫЙ МОМЕНТ!)

Когда откроется страница Google, вы увидите список аккаунтов:

Выберите аккаунт
для продолжения работы с приложением YouTube Automation Bot

👤 Ваш основной аккаунт
   Адрес электронной почты защищен от спам-ботов. Для просмотра адреса в браузере должен быть включен Javascript.

🏢 Бренд-аккаунт
   Название канала
   Адрес электронной почты защищен от спам-ботов. Для просмотра адреса в браузере должен быть включен Javascript.

⚠️ КРИТИЧЕСКИ ВАЖНО:

Если у вас бренд-аккаунт (Brand Account) — выбирайте именно его, а не основной аккаунт!

Бренд-аккаунты имеют отдельный email вида Адрес электронной почты защищен от спам-ботов. Для просмотра адреса в браузере должен быть включен Javascript.. Если выберете основной аккаунт — токен будет работать, но только для вашего личного канала, а не для бренда.

Я потратил час, чтобы понять, почему токен не даёт доступ к каналу с 2600 подписчиками. Оказалось, я просто выбирал не тот аккаунт.


Шаг 11: Разрешение доступа

После выбора аккаунта Google покажет экран:

YouTube Automation Bot запрашивает доступ к вашему аккаунту Google

Это позволит приложению:
- Просматривать и управлять вашими видео на YouTube
- Просматривать и управлять вашими плейлистами
- Просматривать аналитику YouTube

Нажмите "Разрешить" для продолжения.

Нажмите "Разрешить" (Allow).


Шаг 12: Копирование redirect URL

После нажатия "Разрешить" браузер попытается перенаправить вас на:

http://localhost:8080/?code=4/0AXXXXX...&scope=...&state=...

Браузер покажет ошибку:

Не удается получить доступ к сайту
Сайт localhost не позволяет установить подключение.
ERR_CONNECTION_REFUSED

Это нормально! Локального сервера на порту 8080 не существует.

Что нужно сделать:

  1. Скопируйте ПОЛНЫЙ URL из адресной строки браузера
  2. Вставьте его в терминал, где запущен скрипт

Пример:

Вставь сюда весь URL: http://localhost:8080/?code=4/0AXXXXX&scope=https://www.googleapis.com/auth/youtube&state=abc123

Скрипт сам извлечёт code из URL через parse_qs.


Шаг 13: Результат

Если всё прошло успешно:

✅ Код получен: 4/0AXXXXX...
✅ Access token получен
✅ Refresh token: 1//0cKfLrXMIRH...

✅ Токены сохранены в .env
   YOUTUBE_ACCESS_TOKEN_SUKKOGRAD
   YOUTUBE_REFRESH_TOKEN_SUKKOGRAD

Проверьте файл .env:

cat .env

Должно быть:

YOUTUBE_ACCESS_TOKEN_SUKKOGRAD=ya29.a0AdMD6Ei...
YOUTUBE_REFRESH_TOKEN_SUKKOGRAD=1//0cKfLrXMIRH...

Шаг 14: Модуль для работы с каналами

Создайте файл scripts/youtube_channels.py:

import os
import json
import requests
from pathlib import Path
from dotenv import load_dotenv

PROJECT_ROOT = Path(__file__).parent.parent
ENV_FILE = PROJECT_ROOT / '.env'

load_dotenv(ENV_FILE)

# Конфигурация каналов
CHANNELS = {
    'SUKKOGRAD': {
        'channel_id': 'UCXXXXXXXXXXXXXXXX',  # Ваш Channel ID
        'handle': '@yourchannel',
        'name': 'Название канала',
    },
    # Можно добавить другие каналы
}

class YouTubeChannel:
    def __init__(self, channel_key: str):
        config = CHANNELS.get(channel_key.upper())
        if not config:
            raise ValueError(f'Канал {channel_key} не найден в CHANNELS')
        
        self.key = channel_key.upper()
        self.channel_id = config['channel_id']
        self.handle = config['handle']
        self.name = config['name']
        
        self.access_token = os.getenv(f'YOUTUBE_ACCESS_TOKEN_{self.key}')
        self.refresh_token = os.getenv(f'YOUTUBE_REFRESH_TOKEN_{self.key}')
        
        if not self.access_token:
            raise ValueError(f'YOUTUBE_ACCESS_TOKEN_{self.key} не установлен в .env')
    
    def get_headers(self):
        return {'Authorization': f'Bearer {self.access_token}'}
    
    def get_channel_info(self):
        """Получить информацию о канале"""
        url = 'https://www.googleapis.com/youtube/v3/channels'
        params = {
            'part': 'snippet,statistics,contentDetails',
            'id': self.channel_id
        }
        response = requests.get(url, params=params, headers=self.get_headers())
        if response.status_code == 200:
            return response.json().get('items', [None])[0]
        return None
    
    def test_write_access(self):
        """Проверить, есть ли права на запись"""
        url = 'https://www.googleapis.com/youtube/v3/channels'
        params = {'part': 'brandingSettings'}
        update_data = {
            'id': self.channel_id,
            'brandingSettings': {'channel': {}}
        }
        response = requests.put(
            url,
            params=params,
            json=update_data,
            headers={**self.get_headers(), 'Content-Type': 'application/json'}
        )
        return response.status_code == 200
    
    def refresh_access_token(self):
        """Обновить access token через refresh token"""
        if not self.refresh_token:
            return False
        
        json_file = PROJECT_ROOT / 'client_secret.json'
        with open(json_file, 'r') as f:
            data = json.load(f)
        
        creds = data.get('installed') or data.get('web')
        
        refresh_url = 'https://oauth2.googleapis.com/token'
        refresh_data = {
            'client_id': creds['client_id'],
            'client_secret': creds['client_secret'],
            'refresh_token': self.refresh_token,
            'grant_type': 'refresh_token'
        }
        
        response = requests.post(refresh_url, data=refresh_data)
        
        if response.status_code == 200:
            new_token = response.json()['access_token']
            self.access_token = new_token
            return True
        return False

Шаг 15: Тест прав доступа

Создайте файл tests/test_youtube_token.py:

import os
import requests
from pathlib import Path
from dotenv import load_dotenv

PROJECT_ROOT = Path(__file__).parent.parent
ENV_FILE = PROJECT_ROOT / '.env'

load_dotenv(ENV_FILE)

ACCESS_TOKEN = os.getenv('YOUTUBE_ACCESS_TOKEN_SUKKOGRAD')
CHANNEL_ID = 'UCXXXXXXXXXXXXXXXX'  # Ваш Channel ID

print(f'🔍 Проверяю права управления каналом {CHANNEL_ID}...\n')

if not ACCESS_TOKEN:
    print('❌ YOUTUBE_ACCESS_TOKEN_SUKKOGRAD не установлен в .env')
    exit(1)

headers = {'Authorization': f'Bearer {ACCESS_TOKEN}'}

# Тест 1: Получение плейлистов
print('📋 Тест 1: Плейлисты канала...\n')
url = 'https://www.googleapis.com/youtube/v3/playlists'
params = {'part': 'snippet', 'channelId': CHANNEL_ID, 'maxResults': 50}
response = requests.get(url, params=params, headers=headers)

if response.status_code == 200:
    playlists = response.json().get('items', [])
    print(f'   ✅ Плейлистов найдено: {len(playlists)}')
else:
    print(f'   ❌ Ошибка: {response.status_code}')

# Тест 2: Проверка прав на изменение настроек
print('\n⚙️ Тест 2: Проверка прав на изменение настроек...\n')
url = 'https://www.googleapis.com/youtube/v3/channels'
params = {'part': 'id,snippet,contentDetails,statistics,brandingSettings', 'id': CHANNEL_ID}
response = requests.get(url, params=params, headers=headers)

if response.status_code == 200:
    data = response.json()
    if 'items' in data and len(data['items']) > 0:
        channel = data['items'][0]
        
        # Попытка обновления настроек
        update_url = 'https://www.googleapis.com/youtube/v3/channels'
        update_params = {'part': 'brandingSettings'}
        
        current_description = channel['snippet']['description']
        update_data = {
            'id': CHANNEL_ID,
            'brandingSettings': {
                'channel': {'description': current_description}
            }
        }
        
        update_headers = headers.copy()
        update_headers['Content-Type'] = 'application/json'
        
        update_response = requests.put(
            update_url,
            params=update_params,
            json=update_data,
            headers=update_headers
        )
        
        if update_response.status_code == 200:
            print('   ✅ У тебя есть права на изменение настроек канала!')
        elif update_response.status_code == 403:
            print('   ❌ Изменение настроек запрещено')
            print('   ⚠️ У тебя нет прав на управление каналом')

print('\n' + '='*70)

Шаг 16: Как узнать Channel ID

Способ 1: Через YouTube Studio

  1. Зайдите на studio.youtube.com
  2. Выберите нужный канал
  3. В левом меню: НастройкиКаналРасширенные настройки
  4. Скопируйте Идентификатор канала (начинается с UC)

Способ 2: Через скрипт

Если у вас уже есть токен, можно получить список всех доступных каналов:

from scripts.youtube_channels import YouTubeChannel

channel = YouTubeChannel('SUKKOGRAD')
info = channel.get_channel_info()

if info:
    print(f'Channel ID: {info["id"]}')
    print(f'Название: {info["snippet"]["title"]}')

Шаг 17: Типичные ошибки и решения

Ошибка: YouTube Data API v3 has not been used in project

Причина: API не включен в Google Cloud Console

Решение: Включите API (Шаг 2) и подождите 2-3 минуты

Ошибка: redirect_uri_mismatch

Причина: redirect_uri в запросе не совпадает с разрешенными в Google Cloud Console

Решение:

  1. Зайдите в Credentials → ваш OAuth client
  2. В разделе Authorized redirect URIs добавьте: http://localhost:8080/
  3. Сохраните

Ошибка: invalid_grant или Malformed auth code

Причина: Код авторизации истек (живет ~10 минут) или скопирован неправильно

Решение: Запустите скрипт заново и скопируйте полный URL из адресной строки

Ошибка: access_denied

Причина: Приложение не опубликовано, а ваш аккаунт не добавлен в тестовые пользователи

Решение:

  1. Зайдите в OAuth consent screen
  2. В разделе Test users добавьте свой email
  3. Или опубликуйте приложение (кнопка PUBLISH APP)

Ошибка: токен работает, но нет доступа к каналу

Причина: Вы выбрали основной аккаунт вместо бренд-аккаунта

Решение: Запустите скрипт заново и при авторизации выберите бренд-аккаунт (с email Адрес электронной почты защищен от спам-ботов. Для просмотра адреса в браузере должен быть включен Javascript.)


Шаг 18: Как использовать токены

from scripts.youtube_channels import YouTubeChannel

# Создаём объект канала
channel = YouTubeChannel('SUKKOGRAD')

# Получаем информацию о канале
info = channel.get_channel_info()
print(f'Канал: {info["snippet"]["title"]}')
print(f'Подписчиков: {info["statistics"]["subscriberCount"]}')

# Проверяем права
if channel.test_write_access():
    print('✅ Есть права на изменение настроек')
else:
    print('❌ Нет прав')

# Обновляем токен (если истек)
if channel.refresh_access_token():
    print('✅ Токен обновлен')

Шаг 19: Безопасность токенов

Никогда не коммитьте .env и client_secret.json в git!

Добавьте в .gitignore:

.env
client_secret.json

Проверьте, что файлы исключены:

git status .env client_secret.json

Должно показать: Untracked files или файлы не должны отображаться.


Шаг 20: Ограничения YouTube API

Бесплатная квота: 10,000 единиц в день

Стоимость операций:

  • Чтение видео: 1 единица
  • Загрузка видео: 1,600 единиц
  • Обновление видео: 50 единиц
  • Поиск: 100 единиц

Пример: Вы можете загрузить ~6 видео в день (10,000 / 1,600 = 6.25)

Для увеличения квоты нужно подать заявку на аудит приложения.


Заключение

Получение YouTube API токена на headless сервере — это не тривиальная задача. Стандартная библиотека google_auth_oauthlib не рассчитана на такие сценарии.

Что мы сделали:

  1. Создали проект в Google Cloud Console
  2. Включили YouTube Data API v3
  3. Настроили OAuth consent screen
  4. Создали OAuth 2.0 Client ID
  5. Написали ручную реализацию OAuth2 с PKCE
  6. Научились выбирать бренд-аккаунты
  7. Протестировали права доступа

Ключевые моменты:

  • ✅ PKCE обязателен с 2026 года
  • ✅ Бренд-аккаунты требуют отдельного выбора при авторизации
  • ✅ API нужно включать вручную в Google Cloud Console
  • ✅ Ручная реализация надежнее стандартной библиотеки

Полезные ссылки


P.S. Если статья была полезна — ставьте лайк и подписывайтесь. В следующих статьях расскажу, как загружать видео через API, получать аналитику и автоматизировать публикацию контента.

P.P.S. Да, я потратил 17 кругов на то, что должно было занять 17 минут. Зато теперь у вас есть рабочая инструкция, которая сэкономит вам часы отладки.

Удачи! 🚀