""" YouTube Video Downloader Service Отдельный микросервис для скачивания видео с YouTube """ import os import logging from pathlib import Path from flask import Flask, request, jsonify from flask_cors import CORS import yt_dlp import uuid import re # Настройка логирования logging.basicConfig( format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', level=logging.INFO ) logger = logging.getLogger(__name__) app = Flask(__name__) CORS(app) # Разрешаем CORS для взаимодействия с основным ботом # Директория для временных файлов DOWNLOADS_DIR = Path('downloads') DOWNLOADS_DIR.mkdir(exist_ok=True) def _safe_filename(title: str) -> str: """Создает безопасное имя файла""" safe_title = re.sub(r'[<>:"/\\|?*]', '', title)[:100] return str(DOWNLOADS_DIR / f'{uuid.uuid4()}_{safe_title}.%(ext)s') def _is_valid_cookies_file(cookies_path: Path) -> bool: """Проверяет, что файл cookies существует и содержит данные (не только заголовки)""" if not cookies_path.exists(): return False try: with open(cookies_path, 'r', encoding='utf-8', errors='ignore') as f: lines = [line.strip() for line in f.readlines() if line.strip() and not line.strip().startswith('#')] # Проверяем, что есть хотя бы одна строка с данными cookie return len(lines) > 0 except Exception as e: logger.warning(f"Ошибка при проверке файла cookies: {e}") return False def download_youtube_video(url: str, max_retries: int = 3) -> Path: """Скачивает видео с YouTube - используем cookies для обхода блокировок""" cookies_file = os.getenv('YOUTUBE_COOKIES_FILE', 'youtube_cookies.txt') cookies_file_path = Path(cookies_file) cookies_valid = _is_valid_cookies_file(cookies_file_path) if not cookies_valid: logger.warning(f"YouTube: файл cookies не найден или невалиден ({cookies_file_path}). " f"Работаем без cookies. Для лучшей работы рекомендуется обновить cookies через скрипт get_youtube_cookies.sh") user_agent = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36' last_error = None for attempt in range(max_retries): try: # Определяем, это Shorts или обычное видео is_shorts = '/shorts/' in url # Базовые настройки для получения информации ydl_opts_info = { 'quiet': False, 'no_warnings': False, 'user_agent': user_agent, 'socket_timeout': 60, # Увеличиваем таймаут 'extractor_args': { 'youtube': { 'player_client': ['android', 'web'] if not is_shorts else ['android', 'ios', 'web'], 'player_skip': ['webpage'], }, }, 'http_headers': { 'User-Agent': user_agent, 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', 'Accept-Language': 'en-us,en;q=0.5', 'Accept-Encoding': 'gzip, deflate', 'Connection': 'keep-alive', }, } # Если есть валидный файл с cookies, используем его if cookies_valid: ydl_opts_info['cookiefile'] = str(cookies_file_path.absolute()) logger.info(f"YouTube: используем cookies из {cookies_file_path.absolute()} (попытка {attempt + 1})") else: logger.info(f"YouTube: работаем без cookies (попытка {attempt + 1})") with yt_dlp.YoutubeDL(ydl_opts_info) as ydl: info = ydl.extract_info(url, download=False) video_title = info.get('title', 'video') logger.info(f"YouTube: получена информация о видео: {video_title}") # Настройки для скачивания ydl_opts_download = { 'format': 'bestvideo[ext=mp4]+bestaudio[ext=m4a]/best[ext=mp4]/best', 'outtmpl': _safe_filename(video_title), 'quiet': False, 'no_warnings': False, 'user_agent': user_agent, 'socket_timeout': 60, # Увеличиваем таймаут 'extractor_args': { 'youtube': { 'player_client': ['android', 'web'] if not is_shorts else ['android', 'ios', 'web'], 'player_skip': ['webpage'], }, }, 'http_headers': { 'User-Agent': user_agent, 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', 'Accept-Language': 'en-us,en;q=0.5', 'Accept-Encoding': 'gzip, deflate', 'Connection': 'keep-alive', }, } # Если есть валидный файл с cookies, используем его для скачивания if cookies_valid: ydl_opts_download['cookiefile'] = str(cookies_file_path.absolute()) logger.info(f"YouTube: начинаем скачивание (попытка {attempt + 1}/{max_retries}, Shorts: {is_shorts})") with yt_dlp.YoutubeDL(ydl_opts_download) as ydl: ydl.download([url]) # Находим скачанный файл downloaded_files = list(DOWNLOADS_DIR.glob('*')) if downloaded_files: downloaded_files.sort(key=lambda x: x.stat().st_mtime, reverse=True) return downloaded_files[0] else: raise Exception("Файл не был найден после скачивания") except Exception as e: last_error = e error_str = str(e) logger.warning(f"YouTube: попытка {attempt + 1}/{max_retries} не удалась: {error_str}") # Если ошибка связана с cookies и они были использованы, попробуем без cookies на следующей попытке if 'cookies' in error_str.lower() or 'bot' in error_str.lower() or 'sign in' in error_str.lower(): if cookies_valid and attempt == 0: logger.warning("YouTube: ошибка с cookies, попробуем обновить cookies или работать без них") # На следующей попытке попробуем без cookies cookies_valid = False if attempt < max_retries - 1: import time time.sleep((attempt + 1) * 2) raise last_error or Exception("Неизвестная ошибка при скачивании с YouTube") @app.route('/health', methods=['GET']) def health(): """Health check endpoint""" return jsonify({'status': 'ok', 'service': 'youtube-downloader'}), 200 @app.route('/download/stream', methods=['POST']) def download_stream(): """Скачивает видео с YouTube и возвращает бинарные данные""" try: data = request.get_json() if not data or 'url' not in data: return jsonify({'error': 'URL is required'}), 400 url = data['url'] logger.info(f"Получен запрос на скачивание (stream): {url}") # Проверяем, что это YouTube URL if 'youtube.com' not in url and 'youtu.be' not in url: return jsonify({'error': 'Only YouTube URLs are supported'}), 400 # Скачиваем видео video_path = download_youtube_video(url) logger.info(f"Видео скачано: {video_path}") # Читаем файл и отправляем with open(video_path, 'rb') as f: video_data = f.read() # Безопасное имя файла без кириллицы для заголовка safe_filename = video_path.name.encode('ascii', 'ignore').decode('ascii') or 'youtube_video.mp4' if not safe_filename.endswith(('.mp4', '.webm', '.mkv')): safe_filename = 'youtube_video.mp4' # Определяем content-type content_type = 'video/mp4' if video_path.suffix == '.webm': content_type = 'video/webm' elif video_path.suffix == '.mkv': content_type = 'video/x-matroska' # Удаляем временный файл video_path.unlink() return video_data, 200, { 'Content-Type': content_type, 'Content-Disposition': f'attachment; filename="{safe_filename}"' } except Exception as e: error_str = str(e) logger.error(f"Ошибка при скачивании: {error_str}") # Улучшаем сообщение об ошибке, если проблема с cookies if 'cookies' in error_str.lower() or 'bot' in error_str.lower() or 'sign in' in error_str.lower(): error_msg = ( f"{error_str}\n\n" "💡 Совет: Cookies устарели или недействительны. " "Обновите cookies, запустив скрипт:\n" " ./youtube-downloader/get_youtube_cookies.sh\n" "Затем перезапустите сервис." ) else: error_msg = error_str return jsonify({'error': error_msg}), 500 if __name__ == '__main__': port = int(os.getenv('PORT', 5000)) # Внутренний порт контейнера host = os.getenv('HOST', '0.0.0.0') logger.info(f"Запуск YouTube Downloader сервиса на {host}:{port}") app.run(host=host, port=port, debug=False)