From 4d575059b03ee75ce5dbaf9df1769291947aae30 Mon Sep 17 00:00:00 2001 From: vrubelroman Date: Mon, 8 Jun 2026 16:17:02 +0000 Subject: [PATCH] =?UTF-8?q?fix(youtube-downloader):=20subprocess=20yt-dlp?= =?UTF-8?q?=20CLI=20=E2=80=94=20bypass=20=5Fssl=20handshake=20timeout?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- youtube-downloader/Dockerfile | 2 +- youtube-downloader/app.py | 1029 +++++++----------------- youtube-downloader/youtube_cookies.txt | 111 ++- 3 files changed, 356 insertions(+), 786 deletions(-) diff --git a/youtube-downloader/Dockerfile b/youtube-downloader/Dockerfile index 1c5e61a..52a2a8b 100644 --- a/youtube-downloader/Dockerfile +++ b/youtube-downloader/Dockerfile @@ -6,4 +6,4 @@ RUN pip install --no-cache-dir -r requirements.txt COPY . . RUN mkdir -p downloads ENV PYTHONUNBUFFERED=1 -CMD sh -c "gunicorn --workers=1 --timeout=600 --preload --bind=0.0.0.0:\${PORT:-5000} app:app" +CMD sh -c "gunicorn --workers=1 --timeout=600 --preload --max-requests=1 --bind=0.0.0.0:\${PORT:-5000} app:app" diff --git a/youtube-downloader/app.py b/youtube-downloader/app.py index 821d170..2754212 100644 --- a/youtube-downloader/app.py +++ b/youtube-downloader/app.py @@ -1,23 +1,20 @@ """ YouTube Video Downloader Service Отдельный микросервис для скачивания видео с YouTube + +Версия 2: subprocess-based yt-dlp CLI (обход SSL бага в gunicorn pre-fork) """ import os import time import logging import traceback +import subprocess +import json as json_lib from pathlib import Path from flask import Flask, request, jsonify from flask_cors import CORS -import yt_dlp import uuid import re -import copy - -# Цепочка загрузчиков: пробуем по очереди. -# aria2c/curl используют системный OpenSSL (обходит Python SSL handshake timeout), -# native — последний рубеж, работает без внешних зависимостей. -DOWNLOADER_CHAIN = ['aria2c', 'curl', 'native'] # Настройка логирования logging.basicConfig( @@ -27,7 +24,7 @@ logging.basicConfig( logger = logging.getLogger(__name__) app = Flask(__name__) -CORS(app) # Разрешаем CORS для взаимодействия с основным ботом +CORS(app) # Директория для временных файлов DOWNLOADS_DIR = Path('downloads') @@ -49,191 +46,33 @@ def _cleanup_downloads(): pass -def _is_valid_cookies_file(cookies_path: Path) -> bool: - """Проверяет, что файл cookies существует и содержит данные (не только заголовки)""" - logger.info(f"[COOKIES CHECK] Проверка файла cookies: {cookies_path.absolute()}") - - if not cookies_path.exists(): - logger.warning(f"[COOKIES CHECK] Файл не существует: {cookies_path.absolute()}") - return False - - try: - file_size = cookies_path.stat().st_size - logger.info(f"[COOKIES CHECK] Размер файла: {file_size} байт") - - with open(cookies_path, 'r', encoding='utf-8', errors='ignore') as f: - all_lines = f.readlines() - lines = [line.strip() for line in all_lines if line.strip() and not line.strip().startswith('#')] - - logger.info(f"[COOKIES CHECK] Всего строк в файле: {len(all_lines)}, валидных строк (не комментариев): {len(lines)}") - - # Логируем первые 3 строки для диагностики (без чувствительных данных) - if lines: - preview_lines = [] - for i, line in enumerate(lines[:3]): - # Маскируем значения cookie для безопасности - if '\t' in line: - parts = line.split('\t') - if len(parts) > 6: - masked_line = '\t'.join(parts[:6]) + '\t***MASKED***' - preview_lines.append(f" Строка {i+1}: {masked_line[:100]}") - else: - preview_lines.append(f" Строка {i+1}: {line[:100]}") - logger.info(f"[COOKIES CHECK] Превью первых строк:\n" + "\n".join(preview_lines)) - - # Проверяем, что есть хотя бы одна строка с данными cookie - is_valid = len(lines) > 0 - logger.info(f"[COOKIES CHECK] Результат проверки: {'VALID' if is_valid else 'INVALID'}") - return is_valid - except Exception as e: - logger.error(f"[COOKIES CHECK] Ошибка при проверке файла cookies: {e}") - logger.error(f"[COOKIES CHECK] Traceback:\n{traceback.format_exc()}") - return False - - -def _parse_height(format_dict: dict) -> int: - """Извлекает реальную высоту из формата: height/width/format_note/resolution""" - h = format_dict.get('height') - w = format_dict.get('width') - # Для вертикальных видео (Shorts) height и width могут быть перепутаны — - # берём меньшее значение как честный показатель разрешения - if h and w and isinstance(h, (int, float)) and isinstance(w, (int, float)): - real_h = min(int(h), int(w)) - if real_h > 0: - return real_h - if h and isinstance(h, (int, float)) and h > 0: - return int(h) - if w and isinstance(w, (int, float)) and w > 0: - return int(w) - # Если вообще нет размеров — парсим format_note (например "360p" или "1080x1920") - note = str(format_dict.get('format_note', '') or '') - match = re.search(r'(\d+)\s*p', note) - if match: - return int(match.group(1)) - match = re.search(r'(\d+)\s*x\s*(\d+)', note, re.IGNORECASE) - if match: - return min(int(match.group(1)), int(match.group(2))) - # Парсим поле resolution (например "1920x1080") - res = str(format_dict.get('resolution', '') or '') - match = re.search(r'(\d+)\s*x\s*(\d+)', res, re.IGNORECASE) - if match: - return min(int(match.group(1)), int(match.group(2))) - return 0 - - -def _extract_height_from_format_id(format_id: str) -> int | None: - """Извлекает ограничение по высоте из format_id (например 'best[height<=360]' -> 360)""" - match = re.search(r'height<[=]?\s*(\d+)', format_id) - if match: - return int(match.group(1)) - return None - - -def _make_base_ydl_opts(user_agent: str, cookies_file_path: Path | None = None, force_android: bool = False) -> dict: - """Формирует базовые опции yt-dlp, общие для info и download - - Стратегия выбора player_client: - - Всегда используем android + mweb клиенты — они работают без n-challenge - и не требуют валидных кук (YouTube всё чаще блокирует web клиенты). - - Если есть валидные куки — подключаем их как дополнение для расширенного - набора форматов, но основным остаётся android/mweb. - - force_android=True — только android (fallback при проблемах с mweb). - """ - cookies_available = cookies_file_path is not None and cookies_file_path.exists() - - # Стратегия player_client: - # - Если есть cookies + Node.js — используем web (основной) + android (fallback). - # Web клиент решает n-challenge через Node.js и не подвержен SABR-эксперименту. - # - Если нет cookies — android + mweb без зависимостей от JS runtime. - if cookies_available: - player_clients = ['web', 'android'] - else: - player_clients = ['android'] if force_android else ['android', 'mweb'] - - extractor_args = { - 'youtube': { - 'player_client': player_clients, - 'skip': ['translated_subs', 'hls'], - }, - } - - opts = { - 'quiet': False, - 'no_warnings': False, - 'user_agent': user_agent, - 'socket_timeout': 30, # уменьшен с 60 для раннего выявления зависаний - 'extractor_retries': 3, - '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', - 'Referer': 'https://www.youtube.com/', - }, - 'extractor_args': extractor_args, - } - - if cookies_available: - opts['cookiefile'] = str(cookies_file_path.absolute()) - # Включаем n-challenge решение через Node.js + EJS скрипт с GitHub - # yt-dlp скачает challenge solver скрипт из официального репозитория - opts['js_runtimes'] = {'node': {}} - opts['remote_components'] = ['ejs:github'] - - return opts - - -def _find_video_file() -> Path | None: - """Находит видеофайл среди загрузок. Если видео+аудио раздельные — мержит ffmpeg. - Возвращает путь к готовому видеофайлу или None если видео нет.""" - files = list(DOWNLOADS_DIR.glob('*')) +def _find_latest_downloaded() -> Path | None: + """Возвращает самый свежий файл в папке загрузок (не .part/.ytdl).""" + files = [f for f in DOWNLOADS_DIR.glob('*') + if f.suffix not in ('.part', '.ytdl')] if not files: return None files.sort(key=lambda x: x.stat().st_mtime, reverse=True) - - video_file = None - audio_file = None - - for f in files: - if f.suffix in ('.part', '.ytdl'): - continue - if _file_has_video_stream(f): - has_audio = _file_has_audio_stream(f) - if has_audio: - return f # уже смерженный или combined — сразу возвращаем - if video_file is None: - video_file = f - elif not audio_file and _file_has_audio_stream(f): - audio_file = f - - if video_file is None: - return None - - if audio_file: - # Мержим видео + аудио через ffmpeg - import subprocess - merged = DOWNLOADS_DIR / f"{video_file.stem}_merged{video_file.suffix}" - logger.info(f"[MERGE] Мержим {video_file.name} + {audio_file.name} → {merged.name}") + return files[0] + + +def _file_has_video_stream(filepath: Path) -> bool: + """Проверяет через ffprobe, содержит ли файл видео-поток.""" + try: result = subprocess.run( - ['ffmpeg', '-y', '-i', str(video_file), '-i', str(audio_file), - '-c', 'copy', '-map', '0:v:0', '-map', '1:a:0', str(merged)], - capture_output=True, text=True, timeout=120 + ['ffprobe', '-v', 'error', '-select_streams', 'v:0', + '-show_entries', 'stream=codec_type', '-of', 'csv=p=0', + str(filepath)], + capture_output=True, text=True, timeout=15 ) - if result.returncode != 0: - logger.error(f"[MERGE] Ошибка ffmpeg: {result.stderr[-300:]}") - return video_file # fallback — только видео - - video_file.unlink(missing_ok=True) - audio_file.unlink(missing_ok=True) - return merged - - return video_file + return result.stdout.strip() == 'video' + except Exception as e: + logger.warning(f"[VALIDATE] Не удалось проверить видео-поток в {filepath.name}: {e}") + return True def _file_has_audio_stream(filepath: Path) -> bool: """Проверяет через ffprobe, содержит ли файл аудио-поток.""" - import subprocess try: result = subprocess.run( ['ffprobe', '-v', 'error', '-select_streams', 'a:0', @@ -246,500 +85,213 @@ def _file_has_audio_stream(filepath: Path) -> bool: return False -def _find_latest_downloaded() -> Path | None: - """Возвращает самый свежий файл в папке загрузок.""" - files = list(DOWNLOADS_DIR.glob('*')) +def _find_video_file() -> Path | None: + """Находит видеофайл среди загрузок. Если видео+аудио раздельные — мержит ffmpeg.""" + files = [f for f in DOWNLOADS_DIR.glob('*') if f.suffix not in ('.part', '.ytdl')] if not files: return None files.sort(key=lambda x: x.stat().st_mtime, reverse=True) - return files[0] + video_file = None + audio_file = None -def _file_has_video_stream(filepath: Path) -> bool: - """Проверяет через ffprobe, содержит ли файл видео-поток.""" - import subprocess - try: + for f in files: + if _file_has_video_stream(f): + if _file_has_audio_stream(f): + return f # combined stream + if video_file is None: + video_file = f + elif not audio_file and _file_has_audio_stream(f): + audio_file = f + + if video_file is None: + return None + + if audio_file: + merged = DOWNLOADS_DIR / f"{video_file.stem}_merged{video_file.suffix}" + logger.info(f"[MERGE] Мержим {video_file.name} + {audio_file.name} -> {merged.name}") result = subprocess.run( - ['ffprobe', '-v', 'error', '-select_streams', 'v:0', - '-show_entries', 'stream=codec_type', '-of', 'csv=p=0', - str(filepath)], - capture_output=True, text=True, timeout=15 + ['ffmpeg', '-y', '-i', str(video_file), '-i', str(audio_file), + '-c', 'copy', '-map', '0:v:0', '-map', '1:a:0', str(merged)], + capture_output=True, text=True, timeout=120 ) - return result.stdout.strip() == 'video' - except Exception as e: - logger.warning(f"[VALIDATE] Не удалось проверить видео-поток в {filepath.name}: {e}") - return True # в случае ошибки считаем, что видео есть + if result.returncode == 0: + video_file.unlink(missing_ok=True) + audio_file.unlink(missing_ok=True) + return merged + logger.error(f"[MERGE] Ошибка ffmpeg: {result.stderr[-300:]}") + return video_file + + return video_file -def download_youtube_video(url: str, max_retries: int = 3, format_id: str | None = None) -> tuple[Path, str]: - """Скачивает видео с YouTube - используем cookies для обхода блокировок. - Возвращает (путь_к_файлу, использованный_загрузчик).""" - logger.info(f"[DOWNLOAD] Начало скачивания: {url}") - - cookies_file = os.getenv('YOUTUBE_COOKIES_FILE', 'youtube_cookies.txt') - cookies_file_path = Path(cookies_file) - logger.info(f"[DOWNLOAD] Путь к файлу cookies из env: {cookies_file}, абсолютный: {cookies_file_path.absolute()}") - - cookies_valid = _is_valid_cookies_file(cookies_file_path) - if not cookies_valid: - logger.warning(f"[DOWNLOAD] Файл cookies не найден или невалиден ({cookies_file_path}). " - f"Работаем без cookies. Для лучшей работы рекомендуется обновить cookies через скрипт get_youtube_cookies.sh") - else: - logger.info(f"[DOWNLOAD] Cookies файл валиден, будет использован: {cookies_file_path.absolute()}") - - 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 - last_download_errors = [] # собираем ошибки по форматам для диагностики - for attempt in range(max_retries): - try: - is_shorts = '/shorts/' in url - - # Базовые настройки для получения информации - ydl_opts_info = _make_base_ydl_opts(user_agent, cookies_file_path if cookies_valid else None) - - logger.info(f"[DOWNLOAD] Попытка {attempt + 1}: {'cookies включены' if cookies_valid else 'работаем без cookies'}") - - # Пробуем получить информацию о видео - info = None - try: - logger.info(f"[DOWNLOAD] Попытка {attempt + 1}: извлечение информации о видео с URL: {url}") - with yt_dlp.YoutubeDL(ydl_opts_info) as ydl: - info = ydl.extract_info(url, download=False) - logger.info(f"[DOWNLOAD] Попытка {attempt + 1}: информация о видео успешно получена") - except Exception as info_error: - error_str = str(info_error) - error_lower = error_str.lower() - logger.error(f"[DOWNLOAD] Попытка {attempt + 1}: ошибка при получении информации о видео: {error_str}") - logger.error(f"[DOWNLOAD] Полный traceback:\n{traceback.format_exc()}") - - # Если не получилось с cookies, пробуем без них - # Проверяем различные признаки проблем с cookies: - # - явные ошибки с cookies/bot/sign in - # - ошибки формата (могут быть из-за блокировки YouTube при неполных cookies) - # - "Only images are available" (признак блокировки YouTube) - # - "Missing required Data Sync ID" (неполные cookies) - should_retry_without_cookies = cookies_valid and ( - 'cookies' in error_lower or - 'bot' in error_lower or - 'sign in' in error_lower or - 'authentication' in error_lower or - 'format is not available' in error_lower or - 'only images are available' in error_lower or - 'missing required data sync id' in error_lower or - 'challenge solving failed' in error_lower - ) - - if should_retry_without_cookies: - logger.warning(f"[DOWNLOAD] Попытка {attempt + 1}: обнаружена ошибка, возможно связанная с cookies: {error_str[:200]}") - logger.warning(f"[DOWNLOAD] Попытка {attempt + 1}: пробуем без cookies...") - ydl_opts_info.pop('cookiefile', None) - try: - with yt_dlp.YoutubeDL(ydl_opts_info) as ydl: - info = ydl.extract_info(url, download=False) - logger.info(f"[DOWNLOAD] Попытка {attempt + 1}: успешно получена информация без cookies!") - cookies_valid = False # Отключаем cookies для скачивания тоже - except Exception as retry_error: - logger.error(f"[DOWNLOAD] Попытка {attempt + 1}: ошибка даже без cookies: {retry_error}") - logger.error(f"[DOWNLOAD] Полный traceback без cookies:\n{traceback.format_exc()}") - raise - else: - raise - - video_title = info.get('title', 'video') if info else 'video' - logger.info(f"YouTube: получена информация о видео: {video_title}") - - # Настройки для скачивания - # Если передан format_id — это может быть: - # 1) Конкретный format code (число, например "18" или "137+140") — точный выбор качества - # 2) Format selector (например "bestvideo[height<=240]+bestaudio/best") — старый формат - # - # Для конкретных format codes: если формат недоступен, НЕ падаем на best, - # а пробуем format selector для того же разрешения (извлекаем height из запроса пользователя). - # Это важно, т.к. format_id из get_youtube_formats() может не совпадать - # с format_id при повторном extract_info() в download_youtube_video(). - default_format_options = [ - 'bestvideo[ext=mp4]+bestaudio[ext=m4a]/best[ext=mp4]/best', - 'best[ext=mp4]/best', - 'bestvideo+bestaudio/best', - 'best', - ] +# ═══════════════════════════════════════════════════════════════ +# CORE: subprocess-based yt-dlp +# ═══════════════════════════════════════════════════════════════ - # Добавляем fallback на combined форматы (например 18), которые всегда доступны - combined_fallback = ['best[ext=mp4]/best', 'best'] - - requested_height = None # высота, запрошенная пользователем - is_audio_only = False - - if format_id: - is_specific_code = not ('[' in format_id or ']' in format_id) - # Если format_id запрашивает только аудио, не подмешиваем видео-форматы - # и не валидируем наличие видео-потока - first_selector = format_id.split('/')[0] - is_audio_only = 'bestaudio' in first_selector - requested_height = _extract_height_from_format_id(format_id) - - if is_audio_only: - format_options = [format_id] - logger.info(f"[DOWNLOAD] Аудио-only режим, format_id: {format_id}") - elif requested_height is not None: - # Конкретный format_id (из /formats) ставим ПЕРВЫМ — - # он точно указывает выбранные пользователем format codes. - # Height-ограниченный селектор идёт как fallback - # (c исключением av01, чтобы yt-dlp не выбрал unplayable формат 400). - format_options = [ - format_id, - f"bestvideo[height<={requested_height}][vcodec!=av01]+bestaudio/best[height<={requested_height}]", - ] - logger.info(f"[DOWNLOAD] Размерное ограничение: {requested_height}p, format_id: {format_id}") - format_options.extend(default_format_options) - format_options.extend(combined_fallback) - else: - format_options = [format_id] - format_options.extend(default_format_options) - - logger.info(f"[DOWNLOAD] Итоговый список format_options ({len(format_options)} шт.): {format_options}") - else: - format_options = default_format_options - - download_success = False - used_downloader = None # для трекинга, какой загрузчик сработал - for format_option in format_options: - for downloader in DOWNLOADER_CHAIN: - ydl_opts_download = _make_base_ydl_opts(user_agent, cookies_file_path if cookies_valid else None) - ydl_opts_download.update({ - 'format': format_option, - 'outtmpl': _safe_filename(video_title), - 'fragment_retries': 3, - 'postprocessors': [{'key': 'FFmpegFixupStretched'}], - 'downloader': downloader, - # Таймауты для внешних загрузчиков (socket_timeout на них не действует) - 'downloader_args': { - 'curl': ['--connect-timeout', '15', '--max-time', '120'], - 'aria2c': ['--connect-timeout=15', '--timeout=120', '--max-tries=1'], - }, - }) +YTDLP_CMD = 'yt-dlp' +DOWNLOAD_TIMEOUT = 300 +INFO_TIMEOUT = 60 - use_cookies_this_attempt = cookies_valid - logger.info(f"[DOWNLOAD] Попытка {attempt + 1}/{max_retries}: формат={format_option}, загрузчик={downloader}, cookies={use_cookies_this_attempt}") +PLAYER_CLIENTS = 'web,android' +EXTRACTOR_ARGS = 'youtube:player_client=web,android:skip=translated_subs,hls' - try: - with yt_dlp.YoutubeDL(ydl_opts_download) as ydl: - result_info = ydl.download([url]) - # Логируем, что реально скачалось - if result_info: - for entry in result_info: - if entry: - actual_format_id = entry.get('format_id', 'unknown') - actual_height = entry.get('height', 'unknown') - actual_ext = entry.get('ext', 'unknown') - actual_filesize = entry.get('filesize') or entry.get('filesize_approx') or 'unknown' - logger.info(f"[DOWNLOAD] Попытка {attempt + 1}: реально скачан формат: id={actual_format_id}, height={actual_height}, ext={actual_ext}, size={actual_filesize}") +def _build_ytdlp_base_cmd() -> list: + """Базовые аргументы yt-dlp CLI.""" + cookies_file = Path(os.getenv('YOUTUBE_COOKIES_FILE', '/app/youtube_cookies.txt')) + cmd = [ + YTDLP_CMD, + '--socket-timeout', '15', + '--extractor-args', EXTRACTOR_ARGS, + '--js-runtimes', 'node', + '--remote-components', 'ejs:github', + '--no-playlist', + ] + if cookies_file.exists() and cookies_file.stat().st_size > 0: + cmd += ['--cookies', str(cookies_file.absolute())] + return cmd - logger.info(f"[DOWNLOAD] Попытка {attempt + 1}: УСПЕХ — загрузчик={downloader}, формат={format_option}") - download_success = True - used_downloader = downloader - break # выходим из цикла загрузчиков - except Exception as download_error: - error_str = str(download_error) - error_lower = error_str.lower() - last_download_errors.append(f"[{downloader}|{format_option}] {error_str[:250]}") - logger.error(f"[DOWNLOAD] Попытка {attempt + 1}: загрузчик={downloader} ошибка: {error_str[:200]}") - # Классификация ошибки: сеть/SSL → пробуем следующий загрузчик - is_network_error = any(kw in error_lower for kw in [ - 'ssl', 'handshake', 'timeout', 'connection', - 'eof', 'reset', 'broken pipe', 'no route', - ]) +def _run_ytdlp(args: list, timeout: int = DOWNLOAD_TIMEOUT) -> subprocess.CompletedProcess: + """Запускает yt-dlp CLI как subprocess (чистый SSL стек).""" + logger.info(f"[YTDLP] {' '.join(args)}") + return subprocess.run(args, capture_output=True, text=True, timeout=timeout) - # Ошибка формата → следующий формат (не загрузчик) - is_format_error = 'format is not available' in error_lower or 'requested format' in error_lower - # Ошибка cookies → пробуем без cookies - is_cookies_error = use_cookies_this_attempt and any(kw in error_lower for kw in [ - 'cookies', 'bot', 'sign in', 'authentication', - ]) +# ═══════════════════════════════════════════════════════════════ +# YouTube formatter parser (shared with old codebase) +# ═══════════════════════════════════════════════════════════════ - if is_format_error: - logger.warning(f"[DOWNLOAD] Формат {format_option} недоступен → следующий формат") - break # выходим из цикла загрузчиков, идём к следующему формату +def _parse_height(format_dict: dict) -> int: + """Извлекает реальную высоту из формата.""" + h = format_dict.get('height') + w = format_dict.get('width') + if h and w and isinstance(h, (int, float)) and isinstance(w, (int, float)): + return min(int(h), int(w)) + if h and isinstance(h, (int, float)) and h > 0: + return int(h) + if w and isinstance(w, (int, float)) and w > 0: + return int(w) + note = str(format_dict.get('format_note', '') or '') + match = re.search(r'(\d+)\s*p', note) + if match: + return int(match.group(1)) + match = re.search(r'(\d+)\s*x\s*(\d+)', note, re.IGNORECASE) + if match: + return min(int(match.group(1)), int(match.group(2))) + res = str(format_dict.get('resolution', '') or '') + match = re.search(r'(\d+)\s*x\s*(\d+)', res, re.IGNORECASE) + if match: + return min(int(match.group(1)), int(match.group(2))) + return 0 - if is_cookies_error: - logger.warning(f"[DOWNLOAD] Ошибка cookies с загрузчиком {downloader} → пробуем без cookies") - ydl_opts_no_cookies = _make_base_ydl_opts(user_agent, None) - ydl_opts_no_cookies.update({ - 'format': format_option, - 'outtmpl': _safe_filename(video_title), - 'fragment_retries': 3, - 'downloader': downloader, - 'downloader_args': { - 'curl': ['--connect-timeout', '15', '--max-time', '120'], - 'aria2c': ['--connect-timeout=15', '--timeout=120', '--max-tries=1'], - }, - }) - try: - with yt_dlp.YoutubeDL(ydl_opts_no_cookies) as ydl: - ydl.download([url]) - logger.info(f"[DOWNLOAD] УСПЕХ без cookies — загрузчик={downloader}, формат={format_option}") - download_success = True - used_downloader = downloader - cookies_valid = False - break # выходим из цикла загрузчиков - except Exception as retry_error: - retry_str = str(retry_error) - last_download_errors.append(f"[{downloader}|без cookies] {retry_str[:250]}") - logger.error(f"[DOWNLOAD] Ошибка без cookies: {retry_str[:200]}") - continue # пробуем следующий загрузчик - - if is_network_error and downloader != DOWNLOADER_CHAIN[-1]: - logger.warning(f"[DOWNLOAD] Сеть/SSL ошибка с {downloader} → следующий загрузчик") - continue # следующий загрузчик в цепочке - - # Последний загрузчик или неизвестная ошибка → следующий формат - logger.warning(f"[DOWNLOAD] Загрузчик {downloader} не сработал, формат {format_option} → следующий формат") - break # выходим из цикла загрузчиков - - if download_success: - break # выходим из цикла форматов - - if not download_success: - # Собираем детальный отчёт об ошибках - errors_summary = "; ".join(last_download_errors[-10:]) # последние 10 ошибок - raise Exception(f"Не удалось скачать видео ни с одним из доступных форматов. Ошибки: {errors_summary}") - - # Находим скачанный файл - 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], used_downloader or 'unknown' - else: - raise Exception("Файл не был найден после скачивания") - - except Exception as e: - last_error = e - error_str = str(e) - error_lower = error_str.lower() - logger.error(f"[DOWNLOAD] Попытка {attempt + 1}/{max_retries} не удалась: {error_str}") - logger.error(f"[DOWNLOAD] Полный traceback попытки {attempt + 1}:\n{traceback.format_exc()}") - - # Если ошибка связана с форматом, пробуем другие настройки - if 'format is not available' in error_lower or 'requested format' in error_lower: - logger.warning(f"[DOWNLOAD] Попытка {attempt + 1}: проблема с форматом, пробуем другие настройки на следующей попытке") - if attempt < max_retries - 1: - sleep_time = (attempt + 1) * 2 - logger.info(f"[DOWNLOAD] Ожидание {sleep_time} секунд перед следующей попыткой...") - time.sleep(sleep_time) - continue - - # Если ошибка связана с cookies и они были использованы, попробуем без cookies на следующей попытке - if 'cookies' in error_lower or 'bot' in error_lower or 'sign in' in error_lower or 'authentication' in error_lower: - if cookies_valid and attempt == 0: - logger.warning(f"[DOWNLOAD] Попытка {attempt + 1}: обнаружена ошибка связанная с cookies: {error_str[:200]}") - logger.warning(f"[DOWNLOAD] Попытка {attempt + 1}: на следующей попытке попробуем без cookies") - cookies_valid = False - - if attempt < max_retries - 1: - sleep_time = (attempt + 1) * 2 - logger.info(f"[DOWNLOAD] Ожидание {sleep_time} секунд перед следующей попыткой...") - time.sleep(sleep_time) - - # Включаем в итоговую ошибку сводку по форматам - errors_summary = "; ".join(last_download_errors[-10:]) if last_download_errors else "" - error_msg = str(last_error) if last_error else "Неизвестная ошибка при скачивании с YouTube" - if errors_summary: - error_msg += f" | Ошибки форматов: {errors_summary}" - raise Exception(error_msg) +# ═══════════════════════════════════════════════════════════════ +# Форматы (--dump-json) +# ═══════════════════════════════════════════════════════════════ def get_youtube_formats(url: str) -> list[dict]: - """Получает список доступных форматов видео с YouTube""" + """Получает список доступных форматов через subprocess yt-dlp --dump-json.""" logger.info(f"[FORMATS] Получение списка форматов для: {url}") - - cookies_file = os.getenv('YOUTUBE_COOKIES_FILE', 'youtube_cookies.txt') - cookies_file_path = Path(cookies_file) - 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' - - cookies_valid = _is_valid_cookies_file(cookies_file_path) - if not cookies_valid: - logger.warning(f"[FORMATS] Cookies файл не найден или невалиден. Работаем без cookies.") - - # Пробуем сначала с cookies (если есть), потом без - attempts_configs = [] - - if cookies_valid: - attempts_configs.append({ - 'use_cookies': True, - 'label': 'с cookies' - }) - - attempts_configs.append({ - 'use_cookies': False, - 'label': 'без cookies' - }) - - last_error = None - info = None - - for config in attempts_configs: - try: - logger.info(f"[FORMATS] Попытка: {config['label']}") - - # Для /formats используем те же улучшенные опции (player_client, retries и т.д.), - # но с quiet=True чтобы не засорять логи - ydl_opts = _make_base_ydl_opts( - user_agent, - cookies_file_path if config['use_cookies'] else None - ) - ydl_opts['quiet'] = True - ydl_opts['no_warnings'] = True - ydl_opts['socket_timeout'] = 30 - - with yt_dlp.YoutubeDL(ydl_opts) as ydl: - info = ydl.extract_info(url, download=False) - - logger.info(f"[FORMATS] Успешно получена информация {config['label']}") - break # Успех - выходим из цикла - - except Exception as e: - error_str = str(e) - last_error = e - logger.warning(f"[FORMATS] Ошибка {config['label']}: {error_str[:200]}") - - # Если это была попытка с cookies, и ошибка похожа на проблему с cookies - - # продолжаем дальше (следующая попытка будет без cookies) - if config['use_cookies'] and ( - 'cookiefile' in error_str.lower() - or 'requested format' in error_str.lower() - or 'http error' in error_str.lower() - or 'only images are available' in error_str.lower() - or 'n challenge' in error_str.lower() - or 'challenge solving' in error_str.lower() - ): - logger.info(f"[FORMATS] Ошибка с cookies, пробуем без cookies...") - continue - continue - - if info is None: - logger.error(f"[FORMATS] Все попытки получения информации не удались: {last_error}") - raise last_error or Exception("Не удалось получить информацию о видео") - + + cmd = _build_ytdlp_base_cmd() + ['--dump-json', '--quiet', '--no-warnings', url] + try: + result = _run_ytdlp(cmd, timeout=INFO_TIMEOUT) + except Exception as e: + logger.error(f"[FORMATS] Ошибка subprocess: {e}") + raise Exception(f"Не удалось получить информацию о видео: {e}") + + if result.returncode != 0: + err = result.stderr.strip()[-500:] + logger.error(f"[FORMATS] yt-dlp failed: {err}") + raise Exception(f"yt-dlp error: {err}") + + try: + info = json_lib.loads(result.stdout) + except json_lib.JSONDecodeError as e: + raise Exception(f"Failed to parse --dump-json: {e}") + formats = info.get('formats', []) - logger.info(f"[FORMATS] Всего форматов: {len(formats)}") - - duration = info.get('duration') # длительность видео в секундах - logger.info(f"[FORMATS] Длительность видео: {duration} сек") - + duration = info.get('duration') + logger.info(f"[FORMATS] Всего форматов: {len(formats)}, длительность: {duration}с") + def _get_filesize(f: dict) -> int: - """Пытается получить размер файла в байтах: filesize -> filesize_approx -> оценка по битрейту""" size = f.get('filesize') or f.get('filesize_approx') or 0 if size: return size - - # Если размер неизвестен, оцениваем по битрейту и длительности if duration: - # Для форматов, которые содержат и видео и аудио, используем tbr tbr = f.get('tbr') or 0 if tbr: return int(tbr * 1024 / 8 * duration) - - # Для видео-без-аудио: vbr видео + abr аудио vbr = f.get('vbr') or 0 abr = f.get('abr') or 0 if vbr or abr: return int((vbr + abr) * 1024 / 8 * duration) - return 0 - - # Стандартные разрешения для группировки (от большего к меньшему) + quality_tiers = [ - (2160, '4K'), - (1440, '1440p'), - (1080, '1080p'), - (720, '720p'), - (480, '480p'), - (360, '360p'), - (240, '240p'), - (144, '144p'), + (2160, '4K'), (1440, '1440p'), (1080, '1080p'), (720, '720p'), + (480, '480p'), (360, '360p'), (240, '240p'), (144, '144p'), ] - - # Собираем уникальные высоты из форматов с видео + available_heights = set() best_audio_info = {'size': 0, 'ext': 'm4a', 'format_id': None} - + for f in formats: vcodec = f.get('vcodec', 'none') acodec = f.get('acodec', 'none') height = _parse_height(f) - format_id = f.get('format_id', '') - if vcodec != 'none' and height > 0: available_heights.add(height) - if vcodec == 'none' and acodec != 'none' and best_audio_info['format_id'] is None: - # Берём первый попавшийся аудиоформат — yt-dlp отдаёт оригинал первым - best_audio_info = {'size': _get_filesize(f), 'ext': f.get('ext', 'm4a'), 'format_id': format_id} - - logger.info(f"[FORMATS] Доступные разрешения: {sorted(available_heights)}") - logger.info(f"[FORMATS] Лучший аудиопоток: {best_audio_info['size']} bytes, {best_audio_info['ext']}, format_id={best_audio_info['format_id']}") - + best_audio_info = {'size': _get_filesize(f), 'ext': f.get('ext', 'm4a'), + 'format_id': f.get('format_id', '')} + + max_actual_height = max(available_heights) if available_heights else 2160 result = [] used_heights = set() - - # Определяем реальную максимальную высоту видео - max_actual_height = max(available_heights) if available_heights else 2160 - + for max_height, label in quality_tiers: if max_height > max_actual_height: - continue # не показываем 4K для видео с макс высотой 1080p - + continue + best_video = None best_video_height = 0 is_best_dash = False - + for f in formats: vcodec = f.get('vcodec', 'none') height = _parse_height(f) - if vcodec == 'none' or height <= 0 or height > max_height: continue - is_dash = (f.get('acodec', 'none') == 'none') pick = False - if height > best_video_height: pick = True elif height == best_video_height and is_dash and not is_best_dash: pick = True - if pick: best_video = f best_video_height = height is_best_dash = is_dash - - if not best_video: - continue - - if best_video_height in used_heights: + + if not best_video or best_video_height in used_heights: continue used_heights.add(best_video_height) - + video_size = _get_filesize(best_video) has_audio = best_video.get('acodec', 'none') != 'none' total_size = video_size + (best_audio_info['size'] if not has_audio else 0) - video_ext = best_video.get('ext', 'mp4') video_format_id = best_video.get('format_id', '') - - # Честный лейбл из реальной высоты + format_note = best_video.get('format_note', '') or '' if format_note and str(best_video_height) in format_note: display_label = format_note else: display_label = f"{best_video_height}p" - - logger.info(f"[FORMATS] {display_label} (height={best_video_height}): video_size={video_size}, has_audio={has_audio}, total={total_size}, format_id={video_format_id}") - + if has_audio: format_selector = f"{video_format_id}/best[height<={best_video_height}]/best" elif best_audio_info['format_id']: @@ -750,7 +302,7 @@ def get_youtube_formats(url: str) -> list[dict]: ) else: format_selector = f"{video_format_id}+bestaudio/best[height<={best_video_height}]/best" - + result.append({ 'format_id': format_selector, 'label': f"{display_label} ({video_ext})", @@ -758,8 +310,7 @@ def get_youtube_formats(url: str) -> list[dict]: 'ext': video_ext, 'filesize_mb': round(total_size / 1024 / 1024, 1) if total_size else None, }) - - # Добавляем аудиодорожку + if best_audio_info['size']: result.append({ 'format_id': 'bestaudio/best', @@ -768,153 +319,166 @@ def get_youtube_formats(url: str) -> list[dict]: 'ext': best_audio_info['ext'], 'filesize_mb': round(best_audio_info['size'] / 1024 / 1024, 1) if best_audio_info['size'] else None, }) - - # --------------------------------------------------------------- - # Если реальных форматов совсем нет — генерируем оценочные - # (бывает при очень плохих cookies, когда даже format_note пустой) - # --------------------------------------------------------------- + + # Fallsback: если форматов нет — оценочные if len(result) == 0: logger.info(f"[FORMATS] Реальных форматов не найдено, генерируем оценочные") - - # Пытаемся определить реальную максимальную высоту из всех полей - max_possible_height = 0 - for f in formats: - height = _parse_height(f) - if height > max_possible_height: - max_possible_height = height - if max_possible_height == 0: - # Если ничего не смогли определить — используем format_note напрямую - for f in formats: - note = str(f.get('format_note', '') or '') - numbers = re.findall(r'(\d+)', note) - for num in numbers: - n = int(num) - if 100 < n < 10000 and n > max_possible_height: - max_possible_height = n - if max_possible_height == 0: - max_possible_height = 2160 - - available_tiers = [(h, l) for h, l in quality_tiers if h <= max_possible_height] - - TYPICAL_VIDEO_BITRATES: dict[int, int] = { - 2160: 40000, 1440: 20000, 1080: 10000, 720: 5000, - 480: 2500, 360: 1200, 240: 600, 144: 300, - } - AUDIO_BITRATE = 128 - - result = [] - + max_possible_height = max_actual_height if duration: - for max_height, label in available_tiers: - video_kbps = TYPICAL_VIDEO_BITRATES.get(max_height, 1000) - total_kbps = video_kbps + AUDIO_BITRATE - estimated_bytes = total_kbps * 1000 / 8 * duration - estimated_mb = round(estimated_bytes / 1024 / 1024, 1) - - format_selector = f"bestvideo[height<={max_height}]+bestaudio/best[height<={max_height}]" - + typical_bitrates = {2160: 40000, 1440: 20000, 1080: 10000, 720: 5000, + 480: 2500, 360: 1200, 240: 600, 144: 300} + for max_height, label in quality_tiers: + if max_height > max_possible_height: + continue + video_kbps = typical_bitrates.get(max_height, 1000) + total_kbps = video_kbps + 128 + bytes_est = total_kbps * 1000 / 8 * duration result.append({ - 'format_id': format_selector, + 'format_id': f"bestvideo[height<={max_height}]+bestaudio/best[height<={max_height}]", 'label': f"{label} (mp4)", 'quality': label, 'ext': 'mp4', - 'filesize_mb': estimated_mb, - }) - logger.info(f"[FORMATS] Оценка: {label}: ~{estimated_mb} МБ (битрейт {video_kbps} кбит/с)") - - audio_bytes = AUDIO_BITRATE * 1000 / 8 * duration - audio_mb = round(audio_bytes / 1024 / 1024, 1) - result.append({ - 'format_id': 'bestaudio/best', - 'label': f"Audio only (m4a)", - 'quality': 'audio', - 'ext': 'm4a', - 'filesize_mb': audio_mb, - }) - else: - for max_height, label in available_tiers: - format_selector = f"bestvideo[height<={max_height}]+bestaudio/best[height<={max_height}]" - result.append({ - 'format_id': format_selector, - 'label': label, - 'quality': label, - 'ext': 'mp4', - 'filesize_mb': None, + 'filesize_mb': round(bytes_est / 1024 / 1024, 1), }) + audio_bytes = 128 * 1000 / 8 * duration result.append({ 'format_id': 'bestaudio/best', 'label': 'Audio only (m4a)', 'quality': 'audio', 'ext': 'm4a', - 'filesize_mb': None, + 'filesize_mb': round(audio_bytes / 1024 / 1024, 1), }) - + logger.info(f"[FORMATS] Возвращаем {len(result)} форматов") return result -# Простой кэш форматов: {normalized_url: (timestamp, list_of_formats)} -# Форматы YouTube не меняются часто, кэшируем на 30 минут +# ═══════════════════════════════════════════════════════════════ +# Скачивание (subprocess yt-dlp CLI) +# ═══════════════════════════════════════════════════════════════ + +def download_youtube_video(url: str, max_retries: int = 3, format_id: str | None = None) -> tuple[Path, str]: + """Скачивает видео через subprocess yt-dlp CLI. + Возвращает (путь_к_файлу, 'cli').""" + logger.info(f"[DOWNLOAD] Начало скачивания: {url} (format={format_id})") + + if not format_id: + # Fallback chain через yt-dlp format selector + format_id = 'bestvideo[ext=mp4]+bestaudio[ext=m4a]/best[ext=mp4]/best' + + safe_tmpl = str(DOWNLOADS_DIR / f'%(title)s_%(id)s.%(ext)s') + + for attempt in range(max_retries): + _cleanup_downloads() + + cmd = _build_ytdlp_base_cmd() + [ + '--downloader', 'aria2c', + '--downloader-args', + 'aria2c:--connect-timeout=15 --timeout=120 --max-tries=1', + '-f', format_id, + '-o', safe_tmpl, + url, + ] + + try: + result = _run_ytdlp(cmd, timeout=DOWNLOAD_TIMEOUT) + except subprocess.TimeoutExpired: + logger.error(f"[DOWNLOAD] yt-dlp timeout ({DOWNLOAD_TIMEOUT}s)") + if attempt < max_retries - 1: + time.sleep((attempt + 1) * 2) + continue + raise Exception(f"Превышен таймаут скачивания ({DOWNLOAD_TIMEOUT}с)") + + if result.returncode == 0: + for line in result.stdout.split('\n'): + if 'Destination:' in line: + logger.info(f"[DOWNLOAD] {line.strip()}") + + file = _find_latest_downloaded() + if file: + logger.info(f"[DOWNLOAD] Скачан файл: {file.name} ({file.stat().st_size} bytes)") + return file, 'cli' + + logger.error("[DOWNLOAD] Файл не найден после успешного yt-dlp") + raise Exception("Файл не найден после скачивания") + + # Обработка ошибок + stderr = result.stderr.strip()[-800:] + logger.error(f"[DOWNLOAD] Попытка {attempt + 1}: yt-dlp failed: {stderr[:300]}") + + # Try without cookies on cookies-related errors + if ('cookies' in stderr.lower() or 'bot' in stderr.lower() or 'sign in' in stderr.lower()) \ + and '--cookies' in ' '.join(cmd): + logger.warning("[DOWNLOAD] Пробуем без cookies") + cmd_no_cookies = [a for i, a in enumerate(cmd) if a != '--cookies' and cmd[i-1] != '--cookies'] + try: + result2 = _run_ytdlp(cmd_no_cookies, timeout=DOWNLOAD_TIMEOUT) + if result2.returncode == 0: + file = _find_latest_downloaded() + if file: + return file, 'cli-no-cookies' + except Exception: + pass + + if attempt < max_retries - 1: + time.sleep((attempt + 1) * 2) + + raise Exception(f"Не удалось скачать видео после {max_retries} попыток") + + +# ═══════════════════════════════════════════════════════════════ +# Кэш форматов +# ═══════════════════════════════════════════════════════════════ + _formats_cache: dict[str, tuple[float, list[dict]]] = {} -_FORMATS_CACHE_TTL = 30 * 60 # 30 минут в секундах +_FORMATS_CACHE_TTL = 30 * 60 # 30 минут def _normalize_youtube_url(url: str) -> str: - """Нормализует YouTube URL: убирает tracking параметры (?si=...), - приводит к единому виду для кэширования.""" - import re - # Оставляем только video ID из youtu.be или youtube.com - # youtu.be/VIDEO_ID?si=... -> youtu.be/VIDEO_ID m = re.search(r'(youtu\.be/|youtube\.com/watch\?v=)([a-zA-Z0-9_-]{11})', url) if m: prefix, video_id = m.group(1), m.group(2) return f"https://www.youtube.com/watch?v={video_id}" - return url # если не распознали, кэшируем как есть + return url +# ═══════════════════════════════════════════════════════════════ +# Flask endpoints +# ═══════════════════════════════════════════════════════════════ + @app.route('/health', methods=['GET']) def health(): - """Health check endpoint""" return jsonify({'status': 'ok', 'service': 'youtube-downloader'}), 200 @app.route('/formats', methods=['POST']) def formats(): - """Возвращает список доступных форматов для YouTube URL""" request_id = str(uuid.uuid4())[:8] logger.info(f"[FORMATS {request_id}] ========== ЗАПРОС ФОРМАТОВ ==========") - + try: data = request.get_json() if not data or 'url' not in data: return jsonify({'error': 'URL is required'}), 400 - + url = data['url'] - if 'youtube.com' not in url and 'youtu.be' not in url: return jsonify({'error': 'Only YouTube URLs are supported'}), 400 - - # Нормализуем URL и проверяем кэш + cache_key = _normalize_youtube_url(url) now = time.time() - + if cache_key in _formats_cache: cached_time, cached_formats = _formats_cache[cache_key] if now - cached_time < _FORMATS_CACHE_TTL: - logger.info(f"[FORMATS {request_id}] Возвращаем из кэша ({len(cached_formats)} форматов, возраст {now - cached_time:.0f}с)") + logger.info(f"[FORMATS {request_id}] Кэш: {len(cached_formats)} форматов ({now - cached_time:.0f}с)") return jsonify({'formats': cached_formats}), 200 - else: - logger.info(f"[FORMATS {request_id}] Кэш устарел ({now - cached_time:.0f}с > {_FORMATS_CACHE_TTL}с), обновляем...") - del _formats_cache[cache_key] - + del _formats_cache[cache_key] + format_list = get_youtube_formats(url) - - # Сохраняем в кэш _formats_cache[cache_key] = (time.time(), format_list) - logger.info(f"[FORMATS {request_id}] Сохранено в кэш {len(format_list)} форматов для {cache_key}") - return jsonify({'formats': format_list}), 200 - + except Exception as e: logger.error(f"[FORMATS {request_id}] Ошибка: {e}") logger.error(traceback.format_exc()) @@ -923,100 +487,69 @@ def formats(): @app.route('/download/stream', methods=['POST']) def download_stream(): - """Скачивает видео с YouTube и возвращает бинарные данные""" request_id = str(uuid.uuid4())[:8] logger.info(f"[REQUEST {request_id}] ========== НОВЫЙ ЗАПРОС ==========") - logger.info(f"[REQUEST {request_id}] Метод: {request.method}") - logger.info(f"[REQUEST {request_id}] URL: {request.url}") - logger.info(f"[REQUEST {request_id}] Remote Address: {request.remote_addr}") - logger.info(f"[REQUEST {request_id}] Headers: {dict(request.headers)}") - + try: data = request.get_json() - logger.info(f"[REQUEST {request_id}] Body (JSON): {data}") - if not data or 'url' not in data: - logger.warning(f"[REQUEST {request_id}] Ошибка: URL не предоставлен в запросе") return jsonify({'error': 'URL is required'}), 400 - + url = data['url'] - format_id = data.get('format_id') # Опциональный параметр - logger.info(f"[REQUEST {request_id}] Получен запрос на скачивание (stream): {url}, format_id: {format_id}") - - # Проверяем, что это YouTube URL + format_id = data.get('format_id') + logger.info(f"[REQUEST {request_id}] Скачивание: {url}, format_id={format_id}") + if 'youtube.com' not in url and 'youtu.be' not in url: - logger.warning(f"[REQUEST {request_id}] Ошибка: URL не является YouTube URL: {url}") return jsonify({'error': 'Only YouTube URLs are supported'}), 400 - - # Скачиваем видео - logger.info(f"[REQUEST {request_id}] Начинаем скачивание видео...") + video_path, used_downloader = download_youtube_video(url, format_id=format_id) - logger.info(f"[REQUEST {request_id}] Видео успешно скачано: {video_path} (загрузчик: {used_downloader})") - - # Читаем файл и отправляем + logger.info(f"[REQUEST {request_id}] Видео скачано: {video_path} ({used_downloader})") + file_size = video_path.stat().st_size - logger.info(f"[REQUEST {request_id}] Размер файла: {file_size} байт") - 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', '.m4a', '.mp3')): + if not any(safe_filename.endswith(ext) for ext in ('.mp4', '.webm', '.mkv', '.m4a', '.mp3')): safe_filename = 'youtube_video.mp4' - - # Определяем content-type по реальному расширению файла + ext = video_path.suffix.lower() content_type_map = { - '.webm': 'video/webm', - '.mkv': 'video/x-matroska', - '.mp4': 'video/mp4', - '.m4a': 'audio/mp4', - '.mp3': 'audio/mpeg', + '.webm': 'video/webm', '.mkv': 'video/x-matroska', + '.mp4': 'video/mp4', '.m4a': 'audio/mp4', '.mp3': 'audio/mpeg', } content_type = content_type_map.get(ext, 'video/mp4') - - logger.info(f"[REQUEST {request_id}] Отправляем файл: {safe_filename}, Content-Type: {content_type}, размер: {len(video_data)} байт") - - # Удаляем временный файл + video_path.unlink() - logger.info(f"[REQUEST {request_id}] Временный файл удален") logger.info(f"[REQUEST {request_id}] ========== ЗАПРОС УСПЕШНО ЗАВЕРШЕН ==========") - + return video_data, 200, { 'Content-Type': content_type, 'Content-Disposition': f'attachment; filename="{safe_filename}"' } - + except Exception as e: error_str = str(e) - error_lower = error_str.lower() - logger.error(f"[REQUEST {request_id}] ========== ОШИБКА В ЗАПРОСЕ ==========") - logger.error(f"[REQUEST {request_id}] Ошибка при скачивании: {error_str}") - logger.error(f"[REQUEST {request_id}] Полный traceback:\n{traceback.format_exc()}") - - # Улучшаем сообщение об ошибке, если проблема с cookies - if 'cookies' in error_lower or 'bot' in error_lower or 'sign in' in error_lower or 'authentication' in error_lower: - logger.error(f"[REQUEST {request_id}] Обнаружена ошибка связанная с cookies!") + logger.error(f"[REQUEST {request_id}] ========== ОШИБКА ==========") + logger.error(f"[REQUEST {request_id}] {error_str}") + logger.error(traceback.format_exc()) + + if any(kw in error_str.lower() for kw in ('cookies', 'bot', 'sign in', 'authentication')): error_msg = ( f"{error_str}\n\n" "💡 Совет: Cookies устарели или недействительны. " - "Обновите cookies, запустив скрипт:\n" + "Обновите cookies через скрипт:\n" " ./youtube-downloader/get_youtube_cookies.sh\n" "Затем перезапустите сервис." ) else: error_msg = error_str - - logger.error(f"[REQUEST {request_id}] Возвращаем 500 ошибку клиенту") - logger.error(f"[REQUEST {request_id}] ========== КОНЕЦ ОБРАБОТКИ ОШИБКИ ==========") - + return jsonify({'error': error_msg}), 500 if __name__ == '__main__': - port = int(os.getenv('PORT', 5000)) # Внутренний порт контейнера + 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) - diff --git a/youtube-downloader/youtube_cookies.txt b/youtube-downloader/youtube_cookies.txt index d5f4b61..a2b37ef 100644 --- a/youtube-downloader/youtube_cookies.txt +++ b/youtube-downloader/youtube_cookies.txt @@ -1,42 +1,6 @@ # Netscape HTTP Cookie File # This file is generated by yt-dlp. Do not edit. -.youtube.com TRUE / TRUE 1766682211722 GPS 1 -.youtube.com TRUE / TRUE 1801240447479 LOGIN_INFO AFmmF2swRQIgZPfEOdmfC8u5sHvE1aOagKEvp5rRUe5hRUeLiYmxLDwCIQDqFIR59yZ_aBb5BLYSpK7LGdJ6YZqnh32USuOyMZTC5g:QUQ3MjNmd0ZzX01fTjViQ2kzMDJEWG5Ed09zMGF1TlhJcm81YWt3WWdKS2RCZkY3Z2NmMVhudUF4MFVZdFlHd0YtaEU0R3VHNHQ3VmFSZHdfR1RIcnBJNUtXeWhKWVVScE1ZcXNJdzRfdkFGVi1lZzY2dWxCcVVGZ0FPSjNzVmFjTVg1YTBYS0xBajEzU1REM3dnbUc5U3E3NHVtLVRLLXRn -.youtube.com TRUE / FALSE 1766680454000 ST-l3hjtt session_logininfo=AFmmF2swRQIgZPfEOdmfC8u5sHvE1aOagKEvp5rRUe5hRUeLiYmxLDwCIQDqFIR59yZ_aBb5BLYSpK7LGdJ6YZqnh32USuOyMZTC5g%3AQUQ3MjNmd0ZzX01fTjViQ2kzMDJEWG5Ed09zMGF1TlhJcm81YWt3WWdKS2RCZkY3Z2NmMVhudUF4MFVZdFlHd0YtaEU0R3VHNHQ3VmFSZHdfR1RIcnBJNUtXeWhKWVVScE1ZcXNJdzRfdkFGVi1lZzY2dWxCcVVGZ0FPSjNzVmFjTVg1YTBYS0xBajEzU1REM3dnbUc5U3E3NHVtLVRLLXRn -.youtube.com TRUE / FALSE 1766680833000 ST-c5kgne itct=CKEDEIf2BBgEIhMI7ciKxJXZkQMVkHX2CB1YMhVxWg9GRXdoYXRfdG9fd2F0Y2iaAQUIJBCOHsoBBBx5ENY%3D&csn=-28VKqawlbGtJA_i&session_logininfo=AFmmF2swRQIgZPfEOdmfC8u5sHvE1aOagKEvp5rRUe5hRUeLiYmxLDwCIQDqFIR59yZ_aBb5BLYSpK7LGdJ6YZqnh32USuOyMZTC5g%3AQUQ3MjNmd0ZzX01fTjViQ2kzMDJEWG5Ed09zMGF1TlhJcm81YWt3WWdKS2RCZkY3Z2NmMVhudUF4MFVZdFlHd0YtaEU0R3VHNHQ3VmFSZHdfR1RIcnBJNUtXeWhKWVVScE1ZcXNJdzRfdkFGVi1lZzY2dWxCcVVGZ0FPSjNzVmFjTVg1YTBYS0xBajEzU1REM3dnbUc5U3E3NHVtLVRLLXRn&endpoint=%7B%22clickTrackingParams%22%3A%22CKEDEIf2BBgEIhMI7ciKxJXZkQMVkHX2CB1YMhVxWg9GRXdoYXRfdG9fd2F0Y2iaAQUIJBCOHsoBBBx5ENY%3D%22%2C%22commandMetadata%22%3A%7B%22webCommandMetadata%22%3A%7B%22url%22%3A%22%2Fshorts%2FL_ssdybrIEg%22%2C%22webPageType%22%3A%22WEB_PAGE_TYPE_SHORTS%22%2C%22rootVe%22%3A37414%7D%7D%2C%22reelWatchEndpoint%22%3A%7B%22videoId%22%3A%22L_ssdybrIEg%22%2C%22playerParams%22%3A%228AEBoAMByAMkuAQFogYVAV9WnuN4cdq_CrzWOW9SPJmZqqj_kAcC%22%2C%22thumbnail%22%3A%7B%22thumbnails%22%3A%5B%7B%22url%22%3A%22https%3A%2F%2Fi.ytimg.com%2Fvi%2FL_ssdybrIEg%2Fframe0.jpg%22%2C%22width%22%3A720%2C%22height%22%3A1280%7D%5D%2C%22isOriginalAspectRatio%22%3Atrue%7D%2C%22overlay%22%3A%7B%22reelPlayerOverlayRenderer%22%3A%7B%22style%22%3A%22REEL_PLAYER_OVERLAY_STYLE_SHORTS%22%2C%22trackingParams%22%3A%22CKUDELC1BCITCO3IisSV2ZEDFZB19ggdWDIVcQ%3D%3D%22%2C%22reelPlayerNavigationModel%22%3A%22REEL_PLAYER_NAVIGATION_MODEL_UNSPECIFIED%22%7D%7D%2C%22params%22%3A%22CAUwAroBGFVDaEhvTmNZREV3TkVDNHlxTE1zd1Rfdw%253D%253D%22%2C%22sequenceProvider%22%3A%22REEL_WATCH_SEQUENCE_PROVIDER_RPC%22%2C%22sequenceParams%22%3A%22CgtMX3NzZHlicklFZyoCGAVQGWgA%22%2C%22loggingContext%22%3A%7B%22vssLoggingContext%22%3A%7B%22serializedContextData%22%3A%22CgIIDA%253D%253D%22%7D%2C%22qoeLoggingContext%22%3A%7B%22serializedContextData%22%3A%22CgIIDA%253D%253D%22%7D%7D%2C%22ustreamerConfig%22%3A%22CAw%3D%22%7D%7D -.youtube.com TRUE / FALSE 1766680920000 ST-12qny8p itct=CLsDEIf2BBgAIhMI5Mfy_5bZkQMVdKYnAh2_LhBIWg9GRXdoYXRfdG9fd2F0Y2iaAQUIJBCOHsoBBBx5ENY%3D&csn=jdKcWNZwnjJNcfRK&session_logininfo=AFmmF2swRQIgZPfEOdmfC8u5sHvE1aOagKEvp5rRUe5hRUeLiYmxLDwCIQDqFIR59yZ_aBb5BLYSpK7LGdJ6YZqnh32USuOyMZTC5g%3AQUQ3MjNmd0ZzX01fTjViQ2kzMDJEWG5Ed09zMGF1TlhJcm81YWt3WWdKS2RCZkY3Z2NmMVhudUF4MFVZdFlHd0YtaEU0R3VHNHQ3VmFSZHdfR1RIcnBJNUtXeWhKWVVScE1ZcXNJdzRfdkFGVi1lZzY2dWxCcVVGZ0FPSjNzVmFjTVg1YTBYS0xBajEzU1REM3dnbUc5U3E3NHVtLVRLLXRn&endpoint=%7B%22clickTrackingParams%22%3A%22CLsDEIf2BBgAIhMI5Mfy_5bZkQMVdKYnAh2_LhBIWg9GRXdoYXRfdG9fd2F0Y2iaAQUIJBCOHsoBBBx5ENY%3D%22%2C%22commandMetadata%22%3A%7B%22webCommandMetadata%22%3A%7B%22url%22%3A%22%2Fshorts%2FN0qGefDGh1g%22%2C%22webPageType%22%3A%22WEB_PAGE_TYPE_SHORTS%22%2C%22rootVe%22%3A37414%7D%7D%2C%22reelWatchEndpoint%22%3A%7B%22videoId%22%3A%22N0qGefDGh1g%22%2C%22playerParams%22%3A%228AEBoAMByAMkuAQFogYVAV9WnuPoQ-rLcmTTs6scHZ-JYaKVkAcC%22%2C%22thumbnail%22%3A%7B%22thumbnails%22%3A%5B%7B%22url%22%3A%22https%3A%2F%2Fi.ytimg.com%2Fvi%2FN0qGefDGh1g%2Fframe0.jpg%22%2C%22width%22%3A1080%2C%22height%22%3A1920%7D%5D%2C%22isOriginalAspectRatio%22%3Atrue%7D%2C%22overlay%22%3A%7B%22reelPlayerOverlayRenderer%22%3A%7B%22style%22%3A%22REEL_PLAYER_OVERLAY_STYLE_SHORTS%22%2C%22trackingParams%22%3A%22CL8DELC1BCITCOTH8v-W2ZEDFXSmJwIdvy4QSA%3D%3D%22%2C%22reelPlayerNavigationModel%22%3A%22REEL_PLAYER_NAVIGATION_MODEL_UNSPECIFIED%22%7D%7D%2C%22params%22%3A%22CAUwAroBGFVDUXVDdkExSmpFVzhZRmpEM2hKOVppUQ%253D%253D%22%2C%22sequenceProvider%22%3A%22REEL_WATCH_SEQUENCE_PROVIDER_RPC%22%2C%22sequenceParams%22%3A%22CgtOMHFHZWZER2gxZyoCGAVQGWgA%22%2C%22loggingContext%22%3A%7B%22vssLoggingContext%22%3A%7B%22serializedContextData%22%3A%22CgIIDA%253D%253D%22%7D%2C%22qoeLoggingContext%22%3A%7B%22serializedContextData%22%3A%22CgIIDA%253D%253D%22%7D%7D%2C%22ustreamerConfig%22%3A%22CAw%3D%22%7D%7D -.youtube.com TRUE / TRUE 1766681517000 CONSISTENCY APeVyi9lOfhC2Ta5yM1yn4DTAYRRHcOo9i7wdXBcPbloVqCxTId1mvCwO3dFKSNLh3UHggSmH5xpiF33YG_7Agc-dpZgOmVYBH_698K8ZqGlitQrYuYLSbgf_TU -.youtube.com TRUE / FALSE 1767884327000 ST-1supwba session_logininfo=AFmmF2swRQIgZPfEOdmfC8u5sHvE1aOagKEvp5rRUe5hRUeLiYmxLDwCIQDqFIR59yZ_aBb5BLYSpK7LGdJ6YZqnh32USuOyMZTC5g%3AQUQ3MjNmd0ZzX01fTjViQ2kzMDJEWG5Ed09zMGF1TlhJcm81YWt3WWdKS2RCZkY3Z2NmMVhudUF4MFVZdFlHd0YtaEU0R3VHNHQ3VmFSZHdfR1RIcnBJNUtXeWhKWVVScE1ZcXNJdzRfdkFGVi1lZzY2dWxCcVVGZ0FPSjNzVmFjTVg1YTBYS0xBajEzU1REM3dnbUc5U3E3NHVtLVRLLXRn -.youtube.com TRUE / FALSE 1771971074000 ST-bvum61 csn=4GzEyTOTHuK_0tEJ&itct=CKgEEIf2BBgCIhMI3dzc8ZHzkgMVXw6iAx39fglgWg9GRXdoYXRfdG9fd2F0Y2iaAQUIJBCOHsoBBLFi_DM%3D&session_logininfo=AFmmF2swRQIgZPfEOdmfC8u5sHvE1aOagKEvp5rRUe5hRUeLiYmxLDwCIQDqFIR59yZ_aBb5BLYSpK7LGdJ6YZqnh32USuOyMZTC5g%3AQUQ3MjNmd0ZzX01fTjViQ2kzMDJEWG5Ed09zMGF1TlhJcm81YWt3WWdKS2RCZkY3Z2NmMVhudUF4MFVZdFlHd0YtaEU0R3VHNHQ3VmFSZHdfR1RIcnBJNUtXeWhKWVVScE1ZcXNJdzRfdkFGVi1lZzY2dWxCcVVGZ0FPSjNzVmFjTVg1YTBYS0xBajEzU1REM3dnbUc5U3E3NHVtLVRLLXRn -.youtube.com TRUE / FALSE 1771971074000 ST-1dsf764 session_logininfo=AFmmF2swRQIgZPfEOdmfC8u5sHvE1aOagKEvp5rRUe5hRUeLiYmxLDwCIQDqFIR59yZ_aBb5BLYSpK7LGdJ6YZqnh32USuOyMZTC5g%3AQUQ3MjNmd0ZzX01fTjViQ2kzMDJEWG5Ed09zMGF1TlhJcm81YWt3WWdKS2RCZkY3Z2NmMVhudUF4MFVZdFlHd0YtaEU0R3VHNHQ3VmFSZHdfR1RIcnBJNUtXeWhKWVVScE1ZcXNJdzRfdkFGVi1lZzY2dWxCcVVGZ0FPSjNzVmFjTVg1YTBYS0xBajEzU1REM3dnbUc5U3E3NHVtLVRLLXRn -.youtube.com TRUE / FALSE 1772057340000 ST-hcbf8d session_logininfo=AFmmF2swRQIgZPfEOdmfC8u5sHvE1aOagKEvp5rRUe5hRUeLiYmxLDwCIQDqFIR59yZ_aBb5BLYSpK7LGdJ6YZqnh32USuOyMZTC5g%3AQUQ3MjNmd0ZzX01fTjViQ2kzMDJEWG5Ed09zMGF1TlhJcm81YWt3WWdKS2RCZkY3Z2NmMVhudUF4MFVZdFlHd0YtaEU0R3VHNHQ3VmFSZHdfR1RIcnBJNUtXeWhKWVVScE1ZcXNJdzRfdkFGVi1lZzY2dWxCcVVGZ0FPSjNzVmFjTVg1YTBYS0xBajEzU1REM3dnbUc5U3E3NHVtLVRLLXRn -.youtube.com TRUE / FALSE 1772093274000 ST-1b disableCache=true&session_logininfo=AFmmF2swRQIgZPfEOdmfC8u5sHvE1aOagKEvp5rRUe5hRUeLiYmxLDwCIQDqFIR59yZ_aBb5BLYSpK7LGdJ6YZqnh32USuOyMZTC5g%3AQUQ3MjNmd0ZzX01fTjViQ2kzMDJEWG5Ed09zMGF1TlhJcm81YWt3WWdKS2RCZkY3Z2NmMVhudUF4MFVZdFlHd0YtaEU0R3VHNHQ3VmFSZHdfR1RIcnBJNUtXeWhKWVVScE1ZcXNJdzRfdkFGVi1lZzY2dWxCcVVGZ0FPSjNzVmFjTVg1YTBYS0xBajEzU1REM3dnbUc5U3E3NHVtLVRLLXRn&endpoint=%7B%22browseEndpoint%22%3A%7B%22browseId%22%3A%22FEwhat_to_watch%22%7D%2C%22commandMetadata%22%3A%7B%22webCommandMetadata%22%3A%7B%22url%22%3A%22%2F%22%2C%22rootVe%22%3A3854%2C%22webPageType%22%3A%22WEB_PAGE_TYPE_BROWSE%22%7D%7D%7D -.youtube.com TRUE / FALSE 1840832909 HSID AyQ5v_SYe7XVSwk4B -.youtube.com TRUE / TRUE 1840832909 SSID A6URSCEMDAehLdZmX -.youtube.com TRUE / FALSE 1840832909 APISID 8dbTFmLBSXBgxwR5/Aqxn9OCBXLwhMCr-P -.youtube.com TRUE / TRUE 1840832909 SAPISID T-VywQwW6YYwPZ05/AVcBJlwHBEyhqZuI6 -.youtube.com TRUE / TRUE 1840832909 __Secure-1PAPISID T-VywQwW6YYwPZ05/AVcBJlwHBEyhqZuI6 -.youtube.com TRUE / TRUE 1840832909 __Secure-3PAPISID T-VywQwW6YYwPZ05/AVcBJlwHBEyhqZuI6 -.youtube.com TRUE / FALSE 1840832909 SID g.a0009ghwIHcIZqqcY1WV989v420rAlDDepZEj46RPYROUv0etocZfpJzYL10nsGcwt3tO1SfpQACgYKAcoSARISFQHGX2Mi8GOe9epkX1gj-mPmGiEkqhoVAUF8yKqrUaL07JB8aPrxKDzH094X0076 -.youtube.com TRUE / TRUE 1840832909 __Secure-1PSID g.a0009ghwIHcIZqqcY1WV989v420rAlDDepZEj46RPYROUv0etocZUxys_3LZCIbQ68z8wQ2c_wACgYKAa0SARISFQHGX2MijinAP4ZtZI0DQThrXUqIpBoVAUF8yKpJ3DA2uat4crjoKJ6Yo6zB0076 -.youtube.com TRUE / TRUE 1840832909 __Secure-3PSID g.a0009ghwIHcIZqqcY1WV989v420rAlDDepZEj46RPYROUv0etocZGrDGBah2HczPmxnQjYdK1gACgYKAQQSARISFQHGX2MiACXkSAqve--bS36VrXAmBhoVAUF8yKrUoj5-724_fZGNhKeh8uds0076 -.youtube.com TRUE / TRUE 1791136352432 __Secure-BUCKET CMoC -.youtube.com TRUE / TRUE 1793886853 __Secure-YNID 18.YT=HI8YHeAKQNimdb5LgRIak_GASZ4Wh5RTxg2Wxl3jX_nQk7nUzmgM8MBzMMFzsXHCbpxvvhwa60OjexrdexVxJX1wLjnuUJIiMli1BVTumhhrAbZJ4jh1piZ6XcxgkeGJa7b1NmLbOKo8PviUQiLyHjbs5sFYwufNov8shQZdO2CM6yiBL0feOdLihR9bw8HOIxpbJczRhEpYEbvpYnI3frOeds1xbCEpf3Abr7yxs6XxrZEpxa3djITU1nYva2Gjug-72qEaOm7uhS4V-A-mkqIheEswKC9X3P1OPyMPK6DS6-95mqkeI8lkDcyhb0jqR5gidkMH8ZotxNpNSmPB7w -.youtube.com TRUE / TRUE 1793886853 __Secure-ROLLOUT_TOKEN CKPS2eDK6Lu50QEQwdv1spXZkQMYvYeHyq2slAM%3D -.youtube.com TRUE / FALSE 1776287761000 ST-yve142 session_logininfo=AFmmF2swRQIgZPfEOdmfC8u5sHvE1aOagKEvp5rRUe5hRUeLiYmxLDwCIQDqFIR59yZ_aBb5BLYSpK7LGdJ6YZqnh32USuOyMZTC5g%3AQUQ3MjNmd0ZzX01fTjViQ2kzMDJEWG5Ed09zMGF1TlhJcm81YWt3WWdKS2RCZkY3Z2NmMVhudUF4MFVZdFlHd0YtaEU0R3VHNHQ3VmFSZHdfR1RIcnBJNUtXeWhKWVVScE1ZcXNJdzRfdkFGVi1lZzY2dWxCcVVGZ0FPSjNzVmFjTVg1YTBYS0xBajEzU1REM3dnbUc5U3E3NHVtLVRLLXRn -.youtube.com TRUE / TRUE 1807824503515 __Secure-1PSIDTS sidts-CjUBWhotCSAL5EMsgNfc0JD8UVvU5vyCYbx9ZFc0Nnry9Qc7YHRzl6a7o8Zm6bPYHoFyKALKlBAA -.youtube.com TRUE / TRUE 1807824503517 __Secure-3PSIDTS sidts-CjUBWhotCSAL5EMsgNfc0JD8UVvU5vyCYbx9ZFc0Nnry9Qc7YHRzl6a7o8Zm6bPYHoFyKALKlBAA -.youtube.com TRUE / TRUE 1793886874 VISITOR_INFO1_LIVE vFr43YvHJaE -.youtube.com TRUE / TRUE 1793886874 VISITOR_PRIVACY_METADATA CgJSVRIEGgAgMg%3D%3D -.youtube.com TRUE / FALSE 1776288519000 ST-tladcw session_logininfo=AFmmF2swRQIgZPfEOdmfC8u5sHvE1aOagKEvp5rRUe5hRUeLiYmxLDwCIQDqFIR59yZ_aBb5BLYSpK7LGdJ6YZqnh32USuOyMZTC5g%3AQUQ3MjNmd0ZzX01fTjViQ2kzMDJEWG5Ed09zMGF1TlhJcm81YWt3WWdKS2RCZkY3Z2NmMVhudUF4MFVZdFlHd0YtaEU0R3VHNHQ3VmFSZHdfR1RIcnBJNUtXeWhKWVVScE1ZcXNJdzRfdkFGVi1lZzY2dWxCcVVGZ0FPSjNzVmFjTVg1YTBYS0xBajEzU1REM3dnbUc5U3E3NHVtLVRLLXRn -.youtube.com TRUE / FALSE 0 PREF tz=UTC&f7=100&f6=40000000&hl=en -.youtube.com TRUE / FALSE 1776288527000 ST-xuwub9 session_logininfo=AFmmF2swRQIgZPfEOdmfC8u5sHvE1aOagKEvp5rRUe5hRUeLiYmxLDwCIQDqFIR59yZ_aBb5BLYSpK7LGdJ6YZqnh32USuOyMZTC5g%3AQUQ3MjNmd0ZzX01fTjViQ2kzMDJEWG5Ed09zMGF1TlhJcm81YWt3WWdKS2RCZkY3Z2NmMVhudUF4MFVZdFlHd0YtaEU0R3VHNHQ3VmFSZHdfR1RIcnBJNUtXeWhKWVVScE1ZcXNJdzRfdkFGVi1lZzY2dWxCcVVGZ0FPSjNzVmFjTVg1YTBYS0xBajEzU1REM3dnbUc5U3E3NHVtLVRLLXRn -.youtube.com TRUE / FALSE 1809870874 SIDCC AKEyXzXxD9dGVTRHii-ZSRXa5S1jG0nc_r5gPRZM_Q7R_SLG6inpYAArIdKqekxyCRG2fYQBknBS -.youtube.com TRUE / TRUE 1809870874 __Secure-1PSIDCC AKEyXzWA_Zl95hX_7PFnMrNh13Lx3gpntyOhJBaDO_WSRz37gqrtGtfMYVlovN3fCHG4evRFjRgO -.youtube.com TRUE / TRUE 1809870874 __Secure-3PSIDCC AKEyXzUd2Cpu9M26M0TfDtVLBG_-eqaREqqYreCv-9GHeMq4qPIt97VsHDJUTWkws5O_8vxSQjpC -.youtube.com TRUE / FALSE 1776288585000 ST-3opvp5 session_logininfo=AFmmF2swRQIgZPfEOdmfC8u5sHvE1aOagKEvp5rRUe5hRUeLiYmxLDwCIQDqFIR59yZ_aBb5BLYSpK7LGdJ6YZqnh32USuOyMZTC5g%3AQUQ3MjNmd0ZzX01fTjViQ2kzMDJEWG5Ed09zMGF1TlhJcm81YWt3WWdKS2RCZkY3Z2NmMVhudUF4MFVZdFlHd0YtaEU0R3VHNHQ3VmFSZHdfR1RIcnBJNUtXeWhKWVVScE1ZcXNJdzRfdkFGVi1lZzY2dWxCcVVGZ0FPSjNzVmFjTVg1YTBYS0xBajEzU1REM3dnbUc5U3E3NHVtLVRLLXRn -.youtube.com TRUE / TRUE 0 YSC KTvrS45hA30 .instagram.com TRUE / TRUE 1801240452128 datr hGdNaS-QqakSYV8X2eqVTIyA .instagram.com TRUE / TRUE 1798216452128 ig_did 2C886E85-30B9-4495-B882-D9F545DF28E4 .instagram.com TRUE / TRUE 1801240453000 mid aU1nhAAEAAGuKRzTGE9SdmhLzZ5Z @@ -49,4 +13,77 @@ .instagram.com TRUE / TRUE 1784064574939 ds_user_id 42059678244 .paddle.com TRUE / TRUE 1767873458801 __cf_bm kyvXKCFXO7PSDduU6JPNr6Ir2Erz.SSEgs9otf4WVVI-1767871658-1.0.1.1-zr41azuyHRb5xBt5aQUyjvYKNeH6aPQ1bzhDzHbuVvUQJrNn4GWWn2q6vpiKjMOLOaasz5FP7sRzRylD8i4WdvhAvy.co9MGPEFA6xMKO0E firefox.autorefresh.page FALSE / TRUE 1772117935856 app_session eyJpdiI6IjlmckZ0bkdVZUJ4SXgwZUh4Vm9FVXc9PSIsInZhbHVlIjoidmozQTZmWHJ3dlBxdHBETy9ZWUJmTkkvbTFGdDM4dlhRS0VvSnBSM2RNZ3dPaStSbUJXTTNCMVRnZE84UW9XUXB5NVlzUVZ1MEYxZHg5OTdtMjhUSWg1UCtXV1h0UTlkS1lGblFKQXRzK3dDQ1RUMUxzd0tKUFZ2UmpQaSs4OEIiLCJtYWMiOiJjZjY4YmVhMWMwOTRiNzI1MzkwNjdhOTllZGYyMWE2NGI1NDVmODFkZmU5ZDk5NmMxMWYyNTA3YjExMDY2M2ViIiwidGFnIjoiIn0%3D -.facebook.com TRUE / TRUE 1784064559497 fr 0AcR44m5KTXADbTc8.AWehfS67NxYY9SrAf1c-itas_z26hWpd9OyZ9HwNKd41qG_Mxag.Bp4AMv..AAA.0.0.Bp4AMv.AWdIcrGHWEZWg8TB5d4kBwxLyHA +.google.com TRUE / TRUE 1793956296355 AEC AaJma5ujEHtwwu426QfYJYS1u___e4k45nnyXypfttr-bHMS4gkXvLQHHpI +.google.com TRUE / TRUE 1793956296355 __Secure-BUCKET CKgD +.google.com TRUE / FALSE 1778415106513 GOOGLE_ABUSE_EXEMPTION ID=7464237b22409821:TM=1778404306:C=R:IP=89.111.27.33-:S=GjXiSvGnm4v6eztkSSfaKXg +.google.com TRUE / TRUE 1778404617984 __Secure-STRP AEEP7gIoxfZ4dhqrI6bdyKj9-zf8aRnoToi4o65DGWOO1NhXXWHxnr85Vonuw4yu2Krlm4xRKi5kam7KVEdxACQRt2Zv993YC-v2 +.google.com TRUE / FALSE 1793956418000 SEARCH_SAMESITE CgQI56AB +.google.com TRUE / FALSE 1813436107561 HSID AZbmeMqR5tYb_Qvq7 +.google.com TRUE / TRUE 1813436107564 SSID AE4gWoWznYB_ywXCC +.google.com TRUE / FALSE 1813436107564 APISID 29VneHfJm4ayiIDC/AulmGROlxxpjqDQ3I +.google.com TRUE / TRUE 1813436107564 SAPISID ZMuzHsMj98vPCMyJ/ArsbIyY--iqLkvtUV +.google.com TRUE / TRUE 1813436107564 __Secure-1PAPISID ZMuzHsMj98vPCMyJ/ArsbIyY--iqLkvtUV +.google.com TRUE / TRUE 1813436107564 __Secure-3PAPISID ZMuzHsMj98vPCMyJ/ArsbIyY--iqLkvtUV +.google.com TRUE / FALSE 1813436107564 SID g.a000-AhwIKfnU_xfjF03z1a3MWhcI1Hql4mPVyVvqfLa_st6_TfXfFBugDSmLD2HMYMk8CXFewACgYKARESARISFQHGX2MitNuTeeVBSF7CijFkj1Hc7hoVAUF8yKpvT4_7yGAQB3j-kMyKrdPm0076 +.google.com TRUE / TRUE 1813436107564 __Secure-1PSID g.a000-AhwIKfnU_xfjF03z1a3MWhcI1Hql4mPVyVvqfLa_st6_TfXg1H6-6-cMspTM3qWNFrKBQACgYKASkSARISFQHGX2Miikxcnz7qwbhoy4ZmrPmlHRoVAUF8yKr7kVycIEgX3VNAejJQp5d50076 +.google.com TRUE / TRUE 1813436107564 __Secure-3PSID g.a000-AhwIKfnU_xfjF03z1a3MWhcI1Hql4mPVyVvqfLa_st6_TfXp7rOiQ1cmq6CcZfRmoWFKwACgYKAeMSARISFQHGX2MixryF_qgLOtpAj2YrqYUCfhoVAUF8yKpS9-bnBgImbgJi0LyQYd1T0076 +.google.com TRUE / TRUE 1795082095750 NID 531=UFytwvIl0ZqsUHFXvBOjcpU6U8KWUJrqwv-JvL-kGoLQ25Sm5vY3IeiUxx2962W9Ta1lslWlWy7d3CAt1Z3I7uSognzMfftiN5K_dVm1qRMiSfDffUfiOtsFS8eZkczNfBhtUSdTRiCW1QuoU4IsTzj7KUK-AJ7UQpIJi4Cl1VNvyFV5lHI9HonmVPzp3D2UgHlfkh0xBf3WQWBz7wLnl2CFXFXnOm2EldDvnR16KydJ_rjSUnGfc6_EyOlfDi7sZ8Snj2XnpRcBGKk9V8tGkm5ppFc95E0IrxHcdLQJBTQNHu6-ZT3PPGycNBjwhbQy8lQiXz6UPoZmPruBoc0HV5v5GYs_neagJSIF0z2TXpwn318uMaPSFhCHZx3gF_eSJMM8oxtmr5QwjCnC5Pp0IoIvKOz_yyqWVBGZhwepAj5rKiUta8KZHbr-vNl15v-2p1mV2KdpZewBiGkPswjAQwWYg4fMMgBi8WQqD4IvA-9nzA4R2gDj5sOWJ8meTTg5tkq7DdbOP92U-J6Ul71LKq3FwwFK4kQfVwOxWL2UcHgd2bib3fPBpEEhdKPCIv4lUifhCaLp64mSzGYyvgGM20cCkEqXDIWYsxP3UeTRotVgmmHhvCQ_WhVA-0fcVLFFCAHtwxYeUviIcd37bohb3AGDzMhDj15PEgLtxDfM7RgWdbOfTIYOhdwEO5iHEl28RpodCT0qtF9YbYxoZGXPajgbpzU +.google.com TRUE / TRUE 1810890816743 __Secure-1PSIDTS sidts-CjEBhkeRd_-LvZ5Zg9j_DUYlFxYF93J1RiAbWunLifxePeMFWFMlE9sfnGOpXg3vq5nAEAA +.google.com TRUE / TRUE 1810890816745 __Secure-3PSIDTS sidts-CjEBhkeRd_-LvZ5Zg9j_DUYlFxYF93J1RiAbWunLifxePeMFWFMlE9sfnGOpXg3vq5nAEAA +.google.com TRUE / FALSE 1810891252698 SIDCC AKEyXzVzicRDz2q4NBYxew1k2_2EKE1BrRD9e5Yj12zgxFTSjbh2hj_Ac75bUtE_kwWAoJW-Ags +.google.com TRUE / TRUE 1810891252698 __Secure-1PSIDCC AKEyXzWlYR10DYiqeFilF1UwP6z5oV7V1X0Yt-wAcV2IVrtWSNaeGQy7_OK4Pn5TwrmoGf1Si25a +.google.com TRUE / TRUE 1810891252698 __Secure-3PSIDCC AKEyXzXX0qGkkXvppfikPcpH8OyHehXmtSrkHyg2QMDIXInspv7bWcD_6fU9iMKYpvb6yrIIy_Yh +.google.com TRUE /verify TRUE 1794215497053 SNID AEDUgYbS6t3ds0u85ixPJQngAxX0_xZUL7a3aggsVG9siQ-va5ewFW_x4GjzWUSLeLL0bnHmsKFRCFRlkqYO0o8iMY0ylCCYhgM +www.google.com FALSE /recaptcha TRUE 1793956309759 _GRECAPTCHA 09AKhCRwiDW_fB_EMDYSz2ihlZ7oz4N9f3T6ZyAt_DTpbeImhKVAo6hL_9xhdrwc80QvGt8zoqVhVL0onLGup1YBo +www.google.com FALSE / FALSE 1778404917000 DV s2Jea-aA6XUgQIF0XFQkBVjtboUS4VnDPM3RSFa0BQMAAAA +amnezia.org FALSE / FALSE 1794172320000 _pk_ref.1.ec26 %5B%22%22%2C%22%22%2C1778404320%2C%22https%3A%2F%2Fwww.google.com%2F%22%5D +amnezia.org FALSE / FALSE 1812359520000 _pk_id.1.ec26 fca74fa6c0147537.1778404320. +amnezia.org FALSE / FALSE 1778406125000 _pk_ses.1.ec26 1 +accounts.google.com FALSE / TRUE 1780996393000 OTZ 8601673_44_44_123780_40_436260 +accounts.google.com FALSE / TRUE 1812964413928 __Host-GAPS 1:B6GlYJ_WdyuLJYjf5YRaGCcD7rXk5RTq4HN9-W0PTPA0EIjL-B8oGDVLMFQK-ucl4A2fCUMDISVN8SDy1FdMT6KOd3NvJA:clOB3o9ku4U2b_c1 +accounts.google.com FALSE / TRUE 1812964413928 SMSV ADHTe-DsjXpTrtrda4vdYOVDIZ8zRZT0kpi1UNRM3zxuhXtzF1JBrMQzZqt-nWLkSOae4egW0apH7eWQDES_VrEYYfJ26cZyRWvITSmqSf-n1NoTM2X_Fio +accounts.google.com FALSE / TRUE 1812964414088 ACCOUNT_CHOOSER AFx_qI5jVzeLVNAOJ9mR6IXi7LNPDUP6FoIG6ClzTLhkAGykwS7dGx6AihZbZnveMfqeF8xOT6hreJh-5GKE1dls7txTFNNYaWW46Nw-gZAT1P0PnJQExrFwsQsfky2LqwXz7cDw8bX3ln-V1mZRuJkuVqCxy8PyAg +accounts.google.com FALSE / TRUE 1812964440647 LSID o.chat.google.com|o.mail.google.com|o.meet.google.com|s.RU|s.youtube:g.a0009whwIGF9mC8b8qBwIClhgEZFyHr4Rq4bn8l32V9f0zeIDQWFhA6Z-kRv-x1u1dqNuzl-cwACgYKAcUSARISFQHGX2MieEgGVtxXuIUtjbzxfxiMExoVAUF8yKqUTnKuHX2X3kH7Uq5pNVIb0076 +accounts.google.com FALSE / TRUE 1812964440648 __Host-1PLSID o.chat.google.com|o.mail.google.com|o.meet.google.com|s.RU|s.youtube:g.a0009whwIGF9mC8b8qBwIClhgEZFyHr4Rq4bn8l32V9f0zeIDQWFoUHWtKibj2lS56wrQhTpYgACgYKAU8SARISFQHGX2MiZlEuU0vmGwgRq0aGiWQXEBoVAUF8yKrxxr5oINBT6vP7i3mBGb1n0076 +accounts.google.com FALSE / TRUE 1812964440648 __Host-3PLSID o.chat.google.com|o.mail.google.com|o.meet.google.com|s.RU|s.youtube:g.a0009whwIGF9mC8b8qBwIClhgEZFyHr4Rq4bn8l32V9f0zeIDQWFZ_iWMJPFn34sx5dIK-1IqAACgYKAQQSARISFQHGX2Mixfz8oTSpOjugcI6mgJaZOBoVAUF8yKrcYX6ONy4TbG9Q51aFyv2z0076 +mail.google.com FALSE /mail TRUE 1779268414841 COMPASS gmail_ps=CrMBAAlriVffIAHOcbwLKxw82DFHMAFFBBdJRx54IGpsLubMlKvrJFgLAUMeW0uCOXj9FpYDWorA_cgtMyIrIMdQ0Rcsi4161w2R4Z3nyd_uraFfEo1G0i9BV0xsqz0mbJL7nysbJCJ9HKAV0FCMQkzmO8g86BHMDaSzp-1sdCWCbGNjU5POMkySDzM-sn25-iFBgVwtSncpqMJJjZ5T5dT9glVQU21xmV0MIiychMxEj7wXh6QQ_LqG0AYa6gEACWuJV-XHQThSTjU0px5eSprcf2SF6nl7SnMwhTku6s96Bw4ipAxrkg84RGEuWVERB08yO8n2CDk5PbX8xrXwureDzasX2UN09krYVTMOm0ZqQoJIV5gL4afzCQRmD8D-11lXjv1ngTKWi5bxOHHXjNaDG4SSbC2oABv2ykQAOiyY8aUE8GNCjYOTnJgMf3_7XmA5obGaSjEmhIYXICK7OQjdufzLLf3q7wku8_4aknTKJVjO_5a2_rc__bJdSR9s3xQTFnLVYlyn0bO4me3sWUdMd-enAlTMqYBUDv15gmCp0AScVRX3A0owAQ +mail.google.com FALSE / TRUE 1780996415228 __Host-GMAIL_SCH_GMN 1 +mail.google.com FALSE / TRUE 1780996415230 __Host-GMAIL_SCH_GMS 1 +mail.google.com FALSE / TRUE 1780996415230 __Host-GMAIL_SCH_GML 1 +mail.google.com FALSE / TRUE 1779268419976 COMPASS appsfrontendserver=CgAQ07SB0AYaewAJa4lXcZMcX7MMXP_l_kqt_9_sckql7O7vfboUgIxNJVYHEuUkykcNPS_SFDYwO4YHfOI_8tMVe21ppDll3gQDGnr1tmraorrLmosDvQJXfpjvk8-90bBg_gXatn4jGizBVab_jibgd_5D-zhNJhKGcXhL3YKsw3Dl3yABMAE +mail.google.com FALSE / TRUE 1813436107564 OSID g.a000-AhwIJ9Zi54fxErD9GDllWL2nxtfwpoLszg9W8gw23voeOO0SsiSFzoKLeGJ_Pw20l1jHQACgYKAWMSARISFQHGX2MieaVvbDV1rMJZeyZQDlmtrRoVAUF8yKq7uokDCUURG7lQYcCKi-1q0076 +mail.google.com FALSE / TRUE 1813436107564 __Secure-OSID g.a000-AhwIJ9Zi54fxErD9GDllWL2nxtfwpoLszg9W8gw23voeOO0m4h6QqN_dp4YzoWnhoEogQACgYKAXUSARISFQHGX2MibO8sh8oG7EORDGI7S4p0HRoVAUF8yKpnHKRLohB6RZ45z_Y9PHkW0076 +mail.google.com FALSE /sync/u/0 TRUE 1780150904636 COMPASS appsfrontendserver=CgAQ07SB0AYaewAJa4lXcZMcX7MMXP_l_kqt_9_sckql7O7vfboUgIxNJVYHEuUkykcNPS_SFDYwO4YHfOI_8tMVe21ppDll3gQDGnr1tmraorrLmosDvQJXfpjvk8-90bBg_gXatn4jGizBVab_jibgd_5D-zhNJhKGcXhL3YKsw3Dl3yABMAE:bigtop-sync=CsMBAAlriVeewMZI6-LAeBh6WXwenM_9wqc2cBTx6hnc0EN3Zfzlp4ZSvvo9kgvrPG6iFg8_Xczbg98G_pULUQjgIaQPSk5pm77vi_ZgrenL1M2uHDy0Qh7hT9lLLN01ScwjXe3J4AA0mw0UWtXwPApySzHkSGOQdqvAltDqZn0DV4cXXGOwy6JRJLaRfPjRiChvuoaxmquYpGlZ66tpR57U1tU6CHUBgaIvvbDqEXxN-Lots7pnoj4k8f3vIBBQmi58oOSyEN-lvNAGGvoBAAlriVdiIFetSftmMAWJHkiEyeyjo4lQSrKAa-GDhQxMDvUBYpflWK1QoOags3rO5YiJAqNEvFJtLsiLEcNMc1QYJOvP4mNPRSwShO-2L-ncL18ukT4yz_w5KdzVL-7wjo92eIMHrLCE0HriqRNhTVZP5Vze4hTahwQzbppd9eyJ3jx26Aq6GDxEWCxvq7Hg35ROf1L70OeCaGNhNONdVFcfvXJQGdM1-KLHxLg7neptLZYdvIU7tJj-m9x1z7mM76N-mizcqYG7A_wChonYBP_vMo8z9QQneLjo9v_FcfQO4ToD89vxCTXFKAszYWHm7KaByDKk-o-oZTAB +mail.google.com FALSE /mail/u/0 TRUE 1780218898671 COMPASS gmail_ps=CrMBAAlriVffIAHOcbwLKxw82DFHMAFFBBdJRx54IGpsLubMlKvrJFgLAUMeW0uCOXj9FpYDWorA_cgtMyIrIMdQ0Rcsi4161w2R4Z3nyd_uraFfEo1G0i9BV0xsqz0mbJL7nysbJCJ9HKAV0FCMQkzmO8g86BHMDaSzp-1sdCWCbGNjU5POMkySDzM-sn25-iFBgVwtSncpqMJJjZ5T5dT9glVQU21xmV0MIiychMxEj7wXh6QQ6ZW80AYa6gEACWuJV-XHQThSTjU0px5eSprcf2SF6nl7SnMwhTku6s96Bw4ipAxrkg84RGEuWVERB08yO8n2CDk5PbX8xrXwureDzasX2UN09krYVTMOm0ZqQoJIV5gL4afzCQRmD8D-11lXjv1ngTKWi5bxOHHXjNaDG4SSbC2oABv2ykQAOiyY8aUE8GNCjYOTnJgMf3_7XmA5obGaSjEmhIYXICK7OQjdufzLLf3q7wku8_4aknTKJVjO_5a2_rc__bJdSR9s3xQTFnLVYlyn0bO4me3sWUdMd-enAlTMqYBUDv15gmCp0AScVRX3A0owAQ:gmail=CsIBAAlriVccNx06pbIAWSI37Uq4CShCPg0egAH7eAqsKRkCNpg9t5hzIP7hI18i00eUUE4CihgPdAQ5yskCrxM2RdXQXTxPyUo-5H44PqwhBmxLq3ST-iZaFMLiblHTJEUP9mRg8k-tky8_596h9pJ18JACGJsqVOg6Rk_vxmGHQ_nMHWQKcDGUJJ_aHWlWLExmubsSVqjD6E93IaxFbOQK2rCJpnZzzLN62OMSUCegAL1qfYP-35zbhVgipxygGsMd4RUQv7_A0AYa-QEACWuJV5p-CjJvagUuzoNmxqe6Nqnlx7T35uRISQtzDjb3IK7jMf4fwl1E7MdO78Iup5Aa3BlkscMu8SnPKyiPsQzoKTmSMA-8J3qrQNOPP-biMs4QO3XXe_THlNa8-jBz6iICSdoFVokuhjax8Yt9NsoABqrVNVPY0B2M_WUEm3wvggdJ4JB8gqclSRNAC4Pgng6gFAu1zIJb9Qf3WPX_5sq2pwV4TrkBcEHxDouiqZVuLd7tEBwQX9gblXq4see_gIdI_l2NhTV11wRrKzIZQLlP-3xp-vG92P9Ove-bY-4xNToBol3ktJ8LEA4BqJmdL4v7Qb9XRiYwAQ +chat.google.com FALSE / TRUE 1812964416060 OSID g.a0009whwIIXQNdC6qpPY4RRVOXKyc9FGmkI3xHGaVqZhqvqg-AxjfKiBVoPP522JR-Sd7vKvBgACgYKAQcSARISFQHGX2Mi2G3j0oSCBWzco_fpaTag-BoVAUF8yKpzzAUVmYzl68rtcTxoF25e0076 +chat.google.com FALSE / TRUE 1812964416066 __Secure-OSID g.a0009whwIIXQNdC6qpPY4RRVOXKyc9FGmkI3xHGaVqZhqvqg-AxjffAG33KMENiO9cXPZERaHgACgYKAWISARISFQHGX2Mi0_SLBnqoB6-GyawqkhB48xoVAUF8yKpJjXR03PK-RCfGrQHrwwzP0076 +chat.google.com FALSE / TRUE 1780996420000 OTZ 8601674_44_44_123780_40_436260 +chat.google.com FALSE / TRUE 1780215762299 COMPASS dynamite-ui=CgAQ4p270AYaZQAJa4lX0PHy33nrOUkzsKPzJs2IEKDv1E-1Dht6FlJ9P-ZtWAzeV4LoJZ20C2gy0KoyKGWikyN95Q4k92jIzHHx0GSTBZVmuSgM6QG0b1PObHpZUmILLwlvlax5CEyUwRooEFqqIAEwAQ:dynamite-frontend=CgAQ9Jy70AYaZAAJa4lXTSV5k0rMqXDsdd8Sm7zGTDKws42hwXrHQK23723aulxD0HiUtc5B8VbGLgnRQbkxcOHFNcaqi8G91bC28Mu6mh76H0ZPXG4kzN5BZ6SmO1AO3HxuHrGn45wANDE-HoYwAQ +chat.google.com FALSE /u/0/webchannel/ TRUE 1780217746257 COMPASS dynamite-ui=CgAQ0LSB0AYaZQAJa4lX0PHy33nrOUkzsKPzJs2IEKDv1E-1Dht6FlJ9P-ZtWAzeV4LoJZ20C2gy0KoyKGWikyN95Q4k92jIzHHx0GSTBZVmuSgM6QG0b1PObHpZUmILLwlvlax5CEyUwRooEFqqIAEwAQ:dynamite=CgAQoq270AYahwEACWuJVz8m-bFvg7avN7tfl-NhirF79lEv-zZegMHN2AU7VUdlMiC3NqSIQlrFMKbyWeVJqDjZgP8wQ2tYk-lBtqnb4XjVBsMUTXKxC-umAP6o1HjdZV7FUW_L_H8vkLwYOqOmdshEkD4CixDkarcYX-e21aME0fwtzXGz6sXKtNkPW-p624EwAQ +ogs.google.com FALSE / TRUE 1780996422000 OTZ 8601674_44_44_123780_40_436260 +contacts.google.com FALSE / TRUE 1780996427000 OTZ 8601674_44_44_123780_40_436260 +meet.google.com FALSE / TRUE 1812964440744 OSID g.a0009whwIOnMBVgT1YTsd63h54w5OIENqUO6BZs4NHLfGDNGbHi6t4FU8xeTkmULgi16ZYzHNgACgYKASASARISFQHGX2MiA4ELStx6GN3y7Aq3EN3nrxoVAUF8yKqDyxuTCLOfD8ueJ7MgcuiD0076 +meet.google.com FALSE / TRUE 1812964440746 __Secure-OSID g.a0009whwIOnMBVgT1YTsd63h54w5OIENqUO6BZs4NHLfGDNGbHi60J_AeNGuTT1kq7D_v9yLUgACgYKAQwSARISFQHGX2MiAgAb_QG2r06F7gU81wcR_hoVAUF8yKrD_SGVMxcdqWDC6g3asSak0076 +meet.google.com FALSE / TRUE 1779268440910 COMPASS meet-ui=CgAQsJ2B0AYacQAJa4lXSoz8LRFky3cJhYFO5acVt1M88JXUofFd7Bt7GPiuJ1lgy3WKVKJgW9sFuLuioPHnoSUafDcgQ270cmJF_wo3BMiv31U_zXYyzapPiCtiTtn_w8-nHcAUOPb_dFypHoqHzzsuYUHo73gIjBGnMAE +meet.google.com FALSE / TRUE 1780996441000 OTZ 8601674_44_44_123780_40_436260 +.first-am.ru TRUE / TRUE 1778405196000 _sas.ca3141d462a5d666c3e8fee89bb793f2b5f8aa3cfdeea7d629a1d6f0d9bc44d9 SA1.98feab1d-a04a-4eee-ab55-65b8d10a41f1.1778404593.1778404593 +.chatgpt.com TRUE / FALSE 1809508769842 oai-did bdcd7dde-b637-4999-a5ea-02434b01fe60 +.chatgpt.com TRUE / TRUE 1809940897692 cf_clearance 0PgOpUDpzT8RBlYa2TV9tXhLJQbON5A72.Q50yF1G.0-1778404897-1.2.1.1-gKS0ghb4VD_8wHSA3vdEYcoHdXg7guxD8LZRvZjG0Q7Hv.beUtfzzdM2BIyFpZeskFyom1yE3sK0ZhLi5MIF9BTO2eO.c8ccZpIvrVFlhVblO5uGpsusL7UkgCIPcFOQMlUnQTCZv0ujF6zevC2HyxO8sxDBl3kqmbXJeJDa_Fw.dPLTJgAbcGGyc1Kcd8DBbzJzKilWPlUSunJJC2G5a3FoqfJSIMS.UaJegkhxo_ibpGkgA2QmZZ5fEOyb7AnNazeSYdvx5a.iiA6DbtsXfQWAvr7UTVLYN7EVWMQpQ8yqfJLydcev4viT2Nf.q787miHij54J1XhMJFX1_fwpew +.chatgpt.com TRUE / TRUE 1809940900983 oai-sc 0gAAAAABqAE4kYiLfUjbRc2xSXOQ_80W52mgsGlShW65bpX8DpvSHLCkJbgPrKZKafXAiqbOJmkST9SBsstyp48su9yZjqNMW7jLcgf2C0ZN5jaopMCyqLqlk0Hci46y6QPBh_dnXHRBWcABYVNZEmk_fAlK2P91lUq7MSzomAINUUqKjsBoeGCHIYcKGkuKtAKPxRm1foTuxslexAiyp1usAs7QB-rjQm0pV0mob9-cunOEACPa30_M +.chatgpt.com TRUE / FALSE 1793956902763 oai_consent_analytics true +.chatgpt.com TRUE / FALSE 1793956902763 oai_consent_marketing true +.chatgpt.com TRUE / FALSE 1793956902764 oai-allow-ne true +.chatgpt.com TRUE / TRUE 1778864508851 __cf_bm XHODcczujoJbi3ArOgBxMkAVHm53K.wzhsAgC6H7J2w-1778862709.1585546-1.0.1.1-mJPiuKAlCE6Pp.3u5Qm0mTqoUIuMFBGLep8SELbqy1zw_JWBsh7p9QMB7qQ286bP4ZjBl9TMx8X7HXMqdM2xvMCJYokZOQV5JaohselVrlsUtEBZz3.fwLrGNGIdrNGW +chatgpt.com FALSE / FALSE 1779009569842 oai-chat-web-route "ChMxMC4xMjguMjI5LjEwNzozMDAwEPm5oQE=" +chatgpt.com FALSE / FALSE 1793956899000 g_state {"i_l":0,"i_ll":1778404899504,"i_b":"ELKRdJBKNN/mBa5HeveE9Qk2Ct6jgVz42UvSBHh06Cs","i_e":{"enable_itp_optimization":0},"i_et":1778404778504} +chatgpt.com FALSE / TRUE 1778866309246 __cflb 0H28vzvP5FJafnkHxj4bgBpHuC4ehSRtoje5uosSyo9 +www.tampermonkey.net FALSE / FALSE 1779467486514 geo LV_UNKNOWN +www.tampermonkey.net FALSE / FALSE 1779467487073 _dtm p_ +.youtube.com TRUE / FALSE 0 PREF hl=en&tz=UTC +.youtube.com TRUE / TRUE 0 SOCS CAI +.youtube.com TRUE / TRUE 1796431357 __Secure-YNID 19.YT=isEmmtHSC21zVpTt4IRSfL7unp3ffGoSLnfCSQuPaS1_zsJm2uaOpM6x6B07-GgRJWjcCobrfDTYfEaLtQtuhuC-AG_VbLpl5ufiehuf1jTLK_6e-QcxaEPq23s3xxyeZtchb1DACsSbJ8BL3y6EXkZ_xR3iojrLjbFuiKauOvsJKTeTMUTpnrD1WPnFlreSdYR2d9XapogEjF5wd1xGAuJDKPpCgRxcVLsE7ArfixoElOAdSpUYTLjLpUCCq3h7D-8TJlSbjhLHc3djv-8cyBUO0LZo7D-GhxBhPZHbVZ5x-nmDL-VdSa8DR45JYO3YK5gfwOJXhKDQNB4deg37oA +.youtube.com TRUE / TRUE 0 YSC o0TCOzuqu4M +.youtube.com TRUE / TRUE 1796487357 VISITOR_INFO1_LIVE yv5QtACFMuQ +.youtube.com TRUE / TRUE 1796487357 VISITOR_PRIVACY_METADATA CgJSVRIEGgAgaA%3D%3D +.youtube.com TRUE / TRUE 1796431357 __Secure-ROLLOUT_TOKEN CLTU25jzhYq4lwEQ7unAueDglAMYoY2LzLT2lAM%3D +.youtube.com TRUE / TRUE 1780935859 GPS 1