Или почему стандартная библиотека Google не работает, когда вам это действительно нужно
Вступление: зачем вообще это нужно
Представьте ситуацию: у вас есть YouTube канал с парой тысяч подписчиков, и вы хотите автоматизировать работу с ним. Загружать видео по расписанию, получать аналитику, управлять плейлистами через бота.
Звучит просто? OAuth2, токен, пару строк кода — и готово.
Я тоже так думал. Пока не столкнулся с реальностью: стандартная библиотека google_auth_oauthlib не работает на headless сервере (сервере без графического интерфейса).
Спойлер: мне понадобилось 17 итераций, чтобы получить рабочий токен. Но зато теперь у меня есть решение, которое работает гарантированно. И я расскажу вам всё — от настройки Google Cloud Console до финального скрипта.
Шаг 1: Создание проекта в Google Cloud Console
Заходим на console.cloud.google.com
Если у вас нет проекта — создайте новый:
- Нажмите на выпадающий список проектов вверху страницы
- Нажмите "Новый проект" (New Project)
- Введите название (например,
YouTube Bot Project) - Нажмите "Создать"
⏳ Подождите 10-20 секунд, пока проект создастся.
Шаг 2: Включение YouTube Data API v3
Это критически важный шаг, который я пропустил в первый раз и потратил час на отладку.
Путь: Левое меню → APIs & Services → Library
Или напрямую: console.cloud.google.com/apis/library
- В поиске введите:
YouTube Data API v3 - Нажмите на результат
- Нажмите синюю кнопку "ENABLE" (Включить)
⏳ Подождите 2-3 минуты. Google пишет, что изменения распространяются по системе. Не торопитесь.
Проверка: После включения вы должны увидеть кнопку "MANAGE" вместо "ENABLE".
Шаг 3: Настройка OAuth Consent Screen
Путь: Левое меню → APIs & Services → OAuth consent screen
- Выберите External (Внешний) и нажмите Create
- Заполните обязательные поля:
- App name: Название вашего бота (например,
YouTube Automation Bot) - User support email: Ваш email
- Developer contact information: Ваш email
- App name: Название вашего бота (например,
- Нажмите "SAVE AND CONTINUE"
- Scopes — пропустите (нажмите "SAVE AND CONTINUE")
- Test users — добавьте свой Google аккаунт как тестового пользователя
- Нажмите "SAVE AND CONTINUE" → "BACK TO DASHBOARD"
Важно: Пока приложение в режиме Testing, только добавленные тестовые пользователи могут авторизовываться. Для продакшена нужно опубликовать приложение (кнопка "PUBLISH APP").
Шаг 4: Создание OAuth 2.0 Client ID
Путь: Левое меню → APIs & Services → Credentials
- Нажмите "+ CREATE CREDENTIALS" → "OAuth client ID"
- Application type: выберите Desktop app
- Name: любое (например,
YouTube Desktop Client) - Нажмите "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)
Проблемы:
Address already in use— порт 8080 может быть занят (Apache, Nginx, другой сервис)could not locate runnable browser— на сервере нет браузера, аrun_local_server()пытается его открытьMissing required parameter: redirect_uri— библиотека требует, чтобыredirect_uriбыл в JSON файле- Нельзя править 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: Как работает скрипт
- Читает
client_secret.json— получаетclient_id,client_secret,auth_uri,token_uri - Генерирует PKCE — создает
code_verifierиcode_challenge(требование Google с 2026 года) - Формирует URL авторизации — добавляет все параметры, включая динамический
redirect_uri - Пользователь открывает URL — на своей локальной машине (не на сервере!)
- Парсит код из redirect URL — даже если браузер показывает ошибку
- Обменивает код на токен — прямой POST запрос через
requests - Сохраняет токены в
.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 вида . Если выберете основной аккаунт — токен будет работать, но только для вашего личного канала, а не для бренда.
Я потратил час, чтобы понять, почему токен не даёт доступ к каналу с 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 не существует.
Что нужно сделать:
- Скопируйте ПОЛНЫЙ URL из адресной строки браузера
- Вставьте его в терминал, где запущен скрипт
Пример:
Вставь сюда весь 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
- Зайдите на studio.youtube.com
- Выберите нужный канал
- В левом меню: Настройки → Канал → Расширенные настройки
- Скопируйте Идентификатор канала (начинается с
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
Решение:
- Зайдите в Credentials → ваш OAuth client
- В разделе Authorized redirect URIs добавьте:
http://localhost:8080/ - Сохраните
Ошибка: invalid_grant или Malformed auth code
Причина: Код авторизации истек (живет ~10 минут) или скопирован неправильно
Решение: Запустите скрипт заново и скопируйте полный URL из адресной строки
Ошибка: access_denied
Причина: Приложение не опубликовано, а ваш аккаунт не добавлен в тестовые пользователи
Решение:
- Зайдите в OAuth consent screen
- В разделе Test users добавьте свой email
- Или опубликуйте приложение (кнопка PUBLISH APP)
Ошибка: токен работает, но нет доступа к каналу
Причина: Вы выбрали основной аккаунт вместо бренд-аккаунта
Решение: Запустите скрипт заново и при авторизации выберите бренд-аккаунт (с email )
Шаг 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 не рассчитана на такие сценарии.
Что мы сделали:
- Создали проект в Google Cloud Console
- Включили YouTube Data API v3
- Настроили OAuth consent screen
- Создали OAuth 2.0 Client ID
- Написали ручную реализацию OAuth2 с PKCE
- Научились выбирать бренд-аккаунты
- Протестировали права доступа
Ключевые моменты:
- ✅ PKCE обязателен с 2026 года
- ✅ Бренд-аккаунты требуют отдельного выбора при авторизации
- ✅ API нужно включать вручную в Google Cloud Console
- ✅ Ручная реализация надежнее стандартной библиотеки
Полезные ссылки
- Google Cloud Console
- YouTube Data API v3 Documentation
- OAuth 2.0 для веб-серверных приложений
- PKCE (Proof Key for Code Exchange)
P.S. Если статья была полезна — ставьте лайк и подписывайтесь. В следующих статьях расскажу, как загружать видео через API, получать аналитику и автоматизировать публикацию контента.
P.P.S. Да, я потратил 17 кругов на то, что должно было занять 17 минут. Зато теперь у вас есть рабочая инструкция, которая сэкономит вам часы отладки.
Удачи! 🚀