""" Yapfiles Video Downloader Service Отдельный микросервис для скачивания видео с yapfiles.ru """ import os import logging import re import uuid from pathlib import Path from flask import Flask, request, jsonify from flask_cors import CORS import requests from bs4 import BeautifulSoup # Настройка логирования logging.basicConfig( format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', level=logging.INFO ) logger = logging.getLogger(__name__) app = Flask(__name__) CORS(app) # Директория для временных файлов DOWNLOADS_DIR = Path('downloads') DOWNLOADS_DIR.mkdir(exist_ok=True) # User-Agent для запросов USER_AGENT = 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36' def extract_download_url(page_url: str) -> tuple[str, str]: """ Извлекает прямую ссылку на скачивание видео со страницы yapfiles.ru Возвращает tuple (download_url, filename) """ headers = { 'User-Agent': USER_AGENT, 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', 'Accept-Language': 'ru-RU,ru;q=0.9,en-US;q=0.8,en;q=0.7', 'Referer': 'https://www.yapfiles.ru/', } logger.info(f"Загружаю страницу: {page_url}") response = requests.get(page_url, headers=headers, timeout=30) response.raise_for_status() soup = BeautifulSoup(response.text, 'html.parser') # Ищем ссылку на скачивание в разных местах download_url = None filename = 'yapfiles_video.mp4' # Способ 1: Ищем прямую ссылку в source тегах видеоплеера video_tag = soup.find('video') if video_tag: source = video_tag.find('source') if source and source.get('src'): download_url = source['src'] logger.info(f"Найдена ссылка в video/source: {download_url}") # Способ 2: Ищем ссылку "Скачать файл" или подобную if not download_url: download_links = soup.find_all('a', href=True) for link in download_links: href = link.get('href', '') # Ищем ссылки на /files/ с token if '/files/' in href and 'token=' in href: download_url = href if not download_url.startswith('http'): download_url = 'https://www.yapfiles.ru' + download_url logger.info(f"Найдена ссылка на скачивание: {download_url}") break # Способ 3: Ищем в JavaScript коде if not download_url: scripts = soup.find_all('script') for script in scripts: script_text = script.string or '' # Ищем паттерны вида /files/...mp4?token=... matches = re.findall(r'(https?://[^\s"\'<>]+/files/[^\s"\'<>]+\.mp4\?token=[^\s"\'<>]+)', script_text) if matches: download_url = matches[0] logger.info(f"Найдена ссылка в скрипте: {download_url}") break # Или относительные пути matches = re.findall(r'["\']?(/files/[^\s"\'<>]+\.mp4\?token=[^\s"\'<>]+)["\']?', script_text) if matches: download_url = 'https://www.yapfiles.ru' + matches[0] logger.info(f"Найдена относительная ссылка в скрипте: {download_url}") break # Способ 4: Формируем ссылку на основе URL страницы # URL страницы: https://www.yapfiles.ru/show/3532099/30faa897f5a34bb58c018f909a6f1fae.mp4.html # URL файла: https://www.yapfiles.ru/files/3532099/30faa897f5a34bb58c018f909a6f1fae.mp4 if not download_url: match = re.search(r'/show/(\d+)/([^/]+)\.html', page_url) if match: file_id = match.group(1) file_name = match.group(2) # Пробуем без токена - иногда работает для публичных файлов download_url = f'https://www.yapfiles.ru/files/{file_id}/{file_name}' logger.info(f"Сформирована ссылка на основе URL: {download_url}") if not download_url: raise Exception("Не удалось найти ссылку на скачивание видео") # Извлекаем имя файла из URL url_path = download_url.split('?')[0] if '/' in url_path: filename = url_path.split('/')[-1] if not filename.endswith(('.mp4', '.webm', '.avi', '.mov')): filename = 'yapfiles_video.mp4' return download_url, filename def download_yapfiles_video(url: str, max_retries: int = 3) -> Path: """Скачивает видео с yapfiles.ru""" last_error = None for attempt in range(max_retries): try: # Получаем прямую ссылку на скачивание download_url, original_filename = extract_download_url(url) headers = { 'User-Agent': USER_AGENT, 'Accept': '*/*', 'Accept-Language': 'ru-RU,ru;q=0.9,en-US;q=0.8,en;q=0.7', 'Referer': url, } logger.info(f"Yapfiles: скачивание (попытка {attempt + 1}/{max_retries}): {download_url}") response = requests.get(download_url, headers=headers, stream=True, timeout=120) response.raise_for_status() # Проверяем что это действительно видео content_type = response.headers.get('Content-Type', '') if 'text/html' in content_type: raise Exception("Получен HTML вместо видео. Возможно, требуется авторизация или ссылка устарела.") # Определяем расширение ext = '.mp4' if 'video/webm' in content_type: ext = '.webm' elif 'video/x-matroska' in content_type: ext = '.mkv' # Создаем имя файла safe_filename = re.sub(r'[<>:"/\\|?*]', '', original_filename)[:100] if not safe_filename.endswith(ext): safe_filename = safe_filename.rsplit('.', 1)[0] + ext output_file = DOWNLOADS_DIR / f'{uuid.uuid4()}_{safe_filename}' # Сохраняем файл with open(output_file, 'wb') as f: for chunk in response.iter_content(chunk_size=8192): if chunk: f.write(chunk) logger.info(f"Yapfiles: видео скачано: {output_file}") return output_file except Exception as e: last_error = e logger.warning(f"Yapfiles: попытка {attempt + 1}/{max_retries} не удалась: {e}") if attempt < max_retries - 1: import time time.sleep((attempt + 1) * 2) raise last_error or Exception("Неизвестная ошибка при скачивании с Yapfiles") @app.route('/health', methods=['GET']) def health(): """Health check endpoint""" return jsonify({'status': 'ok', 'service': 'yapfiles-downloader'}), 200 @app.route('/download/stream', methods=['POST']) def download_stream(): """Скачивает видео с Yapfiles и возвращает бинарные данные""" 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}") # Проверяем, что это Yapfiles URL if 'yapfiles.ru' not in url: return jsonify({'error': 'Only Yapfiles URLs are supported'}), 400 # Скачиваем видео video_path = download_yapfiles_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 'yapfiles_video.mp4' if not safe_filename.endswith(('.mp4', '.webm', '.mkv')): safe_filename = 'yapfiles_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"Запуск Yapfiles Downloader сервиса на {host}:{port}") app.run(host=host, port=port, debug=False)