""" Instagram Video Downloader Service Отдельный микросервис для скачивания видео с Instagram """ import os import logging import time 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 check_instagram_cookies_expiry() -> tuple[bool, int]: """ Проверяет срок действия Instagram cookies Returns: (is_valid, days_until_expiry) """ cookies_file = os.getenv('INSTAGRAM_COOKIES_FILE', 'instagram_cookies.txt') cookies_file_path = Path(cookies_file) if not cookies_file_path.exists(): return False, 0 try: current_time = time.time() valid_expiries = [] # Важные cookies для Instagram (проверяем их в первую очередь) important_cookies = ['sessionid', 'csrftoken', 'ds_user_id'] with open(cookies_file_path, 'r') as f: for line in f: line = line.strip() if not line or line.startswith('#'): continue parts = line.split('\t') if len(parts) >= 7: domain = parts[0] if 'instagram' in domain.lower(): try: expiry = int(parts[4]) # Unix timestamp cookie_name = parts[5] if len(parts) > 5 else '' # Игнорируем невалидные expiry (0, отрицательные, или слишком старые) # Session cookies (expiry = 0) также игнорируем для проверки срока if expiry > 0 and expiry > 946684800: # Фильтр: после 2000-01-01 (избегаем epoch 0) # Для важных cookies проверяем строже if cookie_name in important_cookies: if expiry > current_time: valid_expiries.append(expiry) else: valid_expiries.append(expiry) except (ValueError, IndexError): continue if not valid_expiries: logger.warning("Не найдено валидных Instagram cookies с нормальным сроком действия") # Если нет валидных expiry, но есть cookies - считаем их действительными # (возможно, это session cookies) return True, 30 # Возвращаем разумное значение по умолчанию # Берем минимальный валидный expiry min_expiry = min(valid_expiries) days_until_expiry = (min_expiry - current_time) / 86400 is_valid = min_expiry > current_time return is_valid, int(days_until_expiry) except Exception as e: logger.error(f"Ошибка при проверке срока действия cookies: {e}") # В случае ошибки считаем cookies действительными (не блокируем работу) return True, 30 def download_instagram_video(url: str, max_retries: int = 3) -> Path: """Скачивает видео с Instagram - используем cookies с правильными заголовками""" cookies_file = os.getenv('INSTAGRAM_COOKIES_FILE', 'instagram_cookies.txt') cookies_file_path = Path(cookies_file) # Проверяем срок действия cookies перед использованием if cookies_file_path.exists(): is_valid, days_left = check_instagram_cookies_expiry() if not is_valid: logger.error("Instagram cookies истекли! Необходимо обновить cookies.") raise Exception("Instagram cookies истекли. Пожалуйста, обновите cookies в файле instagram_cookies.txt") elif days_left < 7: logger.warning(f"Instagram cookies истекают через {days_left} дней. Рекомендуется обновить.") # Парсим cookies для получения csrf token (формат Netscape) csrf_token = None sessionid = None if cookies_file_path.exists(): try: with open(cookies_file_path, 'r') as f: for line in f: line = line.strip() if not line or line.startswith('#'): continue parts = line.split('\t') if len(parts) >= 7: domain = parts[0] # Ищем только cookies от instagram.com if 'instagram' in domain.lower(): cookie_name = parts[5] # Имя cookie cookie_value = parts[6] # Значение cookie if cookie_name == 'csrftoken': csrf_token = cookie_value elif cookie_name == 'sessionid': sessionid = cookie_value # Если нашли оба - можно выходить if csrf_token and sessionid: break except Exception as e: logger.warning(f"Не удалось прочитать cookies: {e}") last_error = None for attempt in range(max_retries): try: # Базовые настройки ydl_opts = { 'format': 'best', 'outtmpl': str(DOWNLOADS_DIR / f'{uuid.uuid4()}_%(title)s.%(ext)s'), 'quiet': False, 'no_warnings': False, 'socket_timeout': 30, } # Если есть файл с cookies, используем его if cookies_file_path.exists(): # Используем абсолютный путь к cookies ydl_opts['cookiefile'] = str(cookies_file_path.absolute()) logger.info(f"Instagram: используем cookies из {cookies_file_path}") # Добавляем заголовки с csrf token если есть headers = { 'Referer': 'https://www.instagram.com/', 'X-Requested-With': 'XMLHttpRequest', } if csrf_token: headers['X-CSRFToken'] = csrf_token logger.info(f"Instagram: добавлен csrf token в заголовки") if sessionid: logger.info(f"Instagram: sessionid найден (длина: {len(sessionid)})") ydl_opts['http_headers'] = headers logger.info(f"Instagram: начинаем скачивание (попытка {attempt + 1}/{max_retries})") with yt_dlp.YoutubeDL(ydl_opts) 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 logger.warning(f"Instagram: попытка {attempt + 1}/{max_retries} не удалась: {e}") if attempt < max_retries - 1: time.sleep((attempt + 1) * 2) raise last_error or Exception("Неизвестная ошибка при скачивании с Instagram. Возможно, нужно обновить cookies.") @app.route('/health', methods=['GET']) def health(): """Health check endpoint""" return jsonify({'status': 'ok', 'service': 'instagram-downloader'}), 200 @app.route('/download/stream', methods=['POST']) def download_stream(): """Скачивает видео с Instagram и возвращает бинарные данные""" 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}") # Проверяем, что это Instagram URL if 'instagram.com' not in url: return jsonify({'error': 'Only Instagram URLs are supported'}), 400 # Скачиваем видео video_path = download_instagram_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 'instagram_video.mp4' if not safe_filename.endswith(('.mp4', '.webm', '.mkv')): safe_filename = 'instagram_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: logger.error(f"Ошибка при скачивании: {e}") return jsonify({'error': str(e)}), 500 if __name__ == '__main__': port = int(os.getenv('PORT', 5000)) # Внутренний порт контейнера host = os.getenv('HOST', '0.0.0.0') logger.info(f"Запуск Instagram Downloader сервиса на {host}:{port}") app.run(host=host, port=port, debug=False)