From d05fc6f522c1be974805591c96928bea3d650cc3 Mon Sep 17 00:00:00 2001 From: vrubelroman Date: Wed, 10 Dec 2025 16:14:26 +0300 Subject: [PATCH] =?UTF-8?q?=D0=B2=D0=BA=20=D0=B2=D1=8B=D0=BD=D0=B5=D1=81?= =?UTF-8?q?=D0=B5=D0=BD=20=D0=B2=20=D0=BE=D1=82=D0=B4=D0=B5=D0=BB=D1=8C?= =?UTF-8?q?=D0=BD=D1=8B=D0=B9=20=D1=81=D0=B5=D1=80=D0=B2=D0=B8=D1=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bot.py | 113 +++++++++---------- docker-compose.yml | 4 +- requirements.txt | 1 + vk-downloader/.gitignore | 14 +++ vk-downloader/Dockerfile | 24 ++++ vk-downloader/README.md | 46 ++++++++ vk-downloader/app.py | 188 +++++++++++++++++++++++++++++++ vk-downloader/docker-compose.yml | 16 +++ vk-downloader/requirements.txt | 5 + 9 files changed, 348 insertions(+), 63 deletions(-) create mode 100644 vk-downloader/.gitignore create mode 100644 vk-downloader/Dockerfile create mode 100644 vk-downloader/README.md create mode 100644 vk-downloader/app.py create mode 100644 vk-downloader/docker-compose.yml create mode 100644 vk-downloader/requirements.txt diff --git a/bot.py b/bot.py index c97273b..7d821b7 100644 --- a/bot.py +++ b/bot.py @@ -9,6 +9,7 @@ from urllib.parse import urlparse from datetime import datetime import yt_dlp +import httpx from telegram import Update from telegram.ext import Application, MessageHandler, filters, ContextTypes, CommandHandler @@ -22,6 +23,8 @@ logger = logging.getLogger(__name__) # Токен бота и имя бота из переменных окружения TELEGRAM_BOT_TOKEN = os.getenv('TELEGRAM_BOT_TOKEN') TELEGRAM_BOT_USERNAME = os.getenv('TELEGRAM_BOT_USERNAME', 'vrubelVideoDownload_bot') +# URL VK сервиса для скачивания видео +VK_DOWNLOADER_URL = os.getenv('VK_DOWNLOADER_URL', 'http://localhost:5555') # Базовая директория проекта (абсолютный путь), чтобы не зависеть от рабочей директории процесса BASE_DIR = Path(__file__).resolve().parent @@ -327,76 +330,64 @@ async def download_instagram_video(url: str, chat_id: int, max_retries: int = 3) async def download_vk_video(url: str, chat_id: int, max_retries: int = 3) -> str: - """Скачивает видео с VK""" - vk_user_agent = 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36' + """Скачивает видео с VK через внешний сервис""" + logger.info(f"VK: отправка запроса на внешний сервис {VK_DOWNLOADER_URL}") last_error = None for attempt in range(max_retries): try: - # Получаем информацию о видео - ydl_opts_info = { - 'quiet': False, - 'no_warnings': False, - 'user_agent': vk_user_agent, - 'socket_timeout': 60, # Увеличенный таймаут для VK - 'extractor_args': { - 'vk': {}, - }, - 'http_headers': { - 'User-Agent': vk_user_agent, - 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', - 'Accept-Language': 'ru-RU,ru;q=0.9', - 'Referer': 'https://vk.com/', - 'Connection': 'keep-alive', - }, - # Пробуем использовать более надежные настройки SSL - 'nocheckcertificate': False, - } - - with yt_dlp.YoutubeDL(ydl_opts_info) as ydl: - info = ydl.extract_info(url, download=False) - video_title = info.get('title', 'vk_video') - logger.info(f"VK: получена информация о видео: {video_title}") - - # Скачиваем видео - ydl_opts_download = { - 'format': 'best', - 'outtmpl': _safe_filename(video_title, chat_id), - 'quiet': False, - 'no_warnings': False, - 'user_agent': vk_user_agent, - 'socket_timeout': 60, # Увеличенный таймаут для VK - 'extractor_args': { - 'vk': {}, - }, - 'http_headers': { - 'User-Agent': vk_user_agent, - 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', - 'Accept-Language': 'ru-RU,ru;q=0.9', - 'Referer': 'https://vk.com/', - }, - } - - logger.info(f"VK: начинаем скачивание (попытка {attempt + 1}/{max_retries})") - with yt_dlp.YoutubeDL(ydl_opts_download) as ydl: - loop = asyncio.get_event_loop() - await loop.run_in_executor(None, lambda: ydl.download([url])) - - # Находим скачанный файл - downloaded_files = list(DOWNLOADS_DIR.glob(f'{chat_id}_*')) - if downloaded_files: - downloaded_files.sort(key=lambda x: x.stat().st_mtime, reverse=True) - return str(downloaded_files[0]) - else: - raise Exception("Файл не был найден после скачивания") + async with httpx.AsyncClient(timeout=600.0) as client: # Увеличенный таймаут для VK + # Отправляем запрос на VK сервис + response = await client.post( + f"{VK_DOWNLOADER_URL}/download/stream", + json={"url": url}, + headers={"Content-Type": "application/json"} + ) + if response.status_code != 200: + error_text = response.text + try: + error_json = response.json() + error_text = error_json.get('error', error_text) + except: + pass + raise Exception(f"VK сервис вернул ошибку {response.status_code}: {error_text}") + + # Сохраняем видео во временный файл + video_data = response.content + video_ext = 'mp4' # По умолчанию mp4 + + # Пробуем определить расширение из заголовков + content_type = response.headers.get('Content-Type', '') + if 'video/' in content_type: + video_ext = content_type.split('/')[-1].split(';')[0] + + # Получаем имя файла из заголовка или создаем случайное + filename = response.headers.get('Content-Disposition', '') + if filename and 'filename=' in filename: + video_filename = filename.split('filename=')[1].strip('"\'') + else: + video_filename = f'{chat_id}_vk_video.{video_ext}' + + # Сохраняем файл + video_path = DOWNLOADS_DIR / video_filename + with open(video_path, 'wb') as f: + f.write(video_data) + + logger.info(f"VK: видео скачано через внешний сервис: {video_path}") + return str(video_path) + + except httpx.TimeoutException: + last_error = Exception(f"Таймаут при запросе к VK сервису (попытка {attempt + 1}/{max_retries})") + logger.warning(f"VK: таймаут при запросе к сервису: {last_error}") except Exception as e: last_error = e logger.warning(f"VK: попытка {attempt + 1}/{max_retries} не удалась: {e}") - if attempt < max_retries - 1: - await asyncio.sleep((attempt + 1) * 2) + + if attempt < max_retries - 1: + await asyncio.sleep((attempt + 1) * 2) - raise last_error or Exception("Неизвестная ошибка при скачивании с VK") + raise last_error or Exception("Неизвестная ошибка при скачивании с VK через внешний сервис") async def download_video(url: str, chat_id: int, max_retries: int = 3) -> str: diff --git a/docker-compose.yml b/docker-compose.yml index b1d15c9..03c1b14 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -8,12 +8,12 @@ services: environment: - TELEGRAM_BOT_TOKEN=${TELEGRAM_BOT_TOKEN} - TELEGRAM_BOT_USERNAME=${TELEGRAM_BOT_USERNAME} + - VK_DOWNLOADER_URL=${VK_DOWNLOADER_URL} volumes: - ./video:/app/video - ./instagram_cookies.txt:/app/instagram_cookies.txt - ./data:/app/data:Z - networks: - - bot_network + network_mode: host networks: bot_network: diff --git a/requirements.txt b/requirements.txt index 57f5db3..166dce9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,5 @@ python-telegram-bot==20.7 yt-dlp>=2024.12.13 requests==2.31.0 +httpx==0.25.2 diff --git a/vk-downloader/.gitignore b/vk-downloader/.gitignore new file mode 100644 index 0000000..f417d92 --- /dev/null +++ b/vk-downloader/.gitignore @@ -0,0 +1,14 @@ +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python +downloads/ +*.log +.env +.venv +env/ +venv/ +*.db +*.db-journal + diff --git a/vk-downloader/Dockerfile b/vk-downloader/Dockerfile new file mode 100644 index 0000000..760c32a --- /dev/null +++ b/vk-downloader/Dockerfile @@ -0,0 +1,24 @@ +FROM python:3.11-slim + +# Устанавливаем зависимости для yt-dlp +RUN apt-get update && apt-get install -y \ + ffmpeg \ + wget \ + && rm -rf /var/lib/apt/lists/* + +WORKDIR /app + +# Копируем requirements и устанавливаем зависимости +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +# Копируем код приложения +COPY . . + +# Создаем директорию для загрузок +RUN mkdir -p downloads + +ENV PYTHONUNBUFFERED=1 + +CMD ["python", "app.py"] + diff --git a/vk-downloader/README.md b/vk-downloader/README.md new file mode 100644 index 0000000..e1d787d --- /dev/null +++ b/vk-downloader/README.md @@ -0,0 +1,46 @@ +# VK Video Downloader Service + +Отдельный микросервис для скачивания видео с VK. Предназначен для работы без VPN на отдельном хосте. + +## Запуск + +```bash +docker compose up -d +``` + +## API Endpoints + +### Health Check +``` +GET /health +``` + +### Скачать видео (возвращает файл) +``` +POST /download +Content-Type: application/json + +{ + "url": "https://vk.com/clip-123456_789012" +} +``` + +### Скачать видео (возвращает бинарные данные) +``` +POST /download/stream +Content-Type: application/json + +{ + "url": "https://vk.com/clip-123456_789012" +} +``` + +## Переменные окружения + +- `PORT` - внутренний порт контейнера (по умолчанию: 5000, внешний порт: 5555) +- `HOST` - хост для запуска сервиса (по умолчанию: 0.0.0.0) + +## Использование + +Основной бот отправляет POST запрос на этот сервис с URL видео VK и получает готовый файл для отправки пользователю. + diff --git a/vk-downloader/app.py b/vk-downloader/app.py new file mode 100644 index 0000000..5e08033 --- /dev/null +++ b/vk-downloader/app.py @@ -0,0 +1,188 @@ +""" +VK Video Downloader Service +Отдельный микросервис для скачивания видео с VK +""" +import os +import logging +from pathlib import Path +from flask import Flask, request, jsonify, send_file +from flask_cors import CORS +import yt_dlp +import tempfile +import uuid + +# Настройка логирования +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 download_vk_video(url: str, max_retries: int = 3) -> Path: + """Скачивает видео с VK""" + vk_user_agent = 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36' + + last_error = None + for attempt in range(max_retries): + try: + # Получаем информацию о видео + ydl_opts_info = { + 'quiet': False, + 'no_warnings': False, + 'user_agent': vk_user_agent, + 'socket_timeout': 60, # Увеличенный таймаут для VK + 'extractor_args': { + 'vk': {}, + }, + 'http_headers': { + 'User-Agent': vk_user_agent, + 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', + 'Accept-Language': 'ru-RU,ru;q=0.9', + 'Referer': 'https://vk.com/', + 'Connection': 'keep-alive', + }, + 'nocheckcertificate': False, + } + + with yt_dlp.YoutubeDL(ydl_opts_info) as ydl: + info = ydl.extract_info(url, download=False) + video_title = info.get('title', 'vk_video') + logger.info(f"VK: получена информация о видео: {video_title}") + + # Создаем уникальное имя файла + safe_title = video_title.replace('/', '_').replace('\\', '_')[:100] + output_file = DOWNLOADS_DIR / f'{uuid.uuid4()}_{safe_title}.%(ext)s' + + # Скачиваем видео + ydl_opts_download = { + 'format': 'best', + 'outtmpl': str(output_file), + 'quiet': False, + 'no_warnings': False, + 'user_agent': vk_user_agent, + 'socket_timeout': 60, + 'extractor_args': { + 'vk': {}, + }, + 'http_headers': { + 'User-Agent': vk_user_agent, + 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', + 'Accept-Language': 'ru-RU,ru;q=0.9', + 'Referer': 'https://vk.com/', + }, + } + + logger.info(f"VK: начинаем скачивание (попытка {attempt + 1}/{max_retries})") + with yt_dlp.YoutubeDL(ydl_opts_download) as ydl: + ydl.download([url]) + + # Находим скачанный файл + downloaded_files = list(DOWNLOADS_DIR.glob(f'{output_file.stem}*')) + 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"VK: попытка {attempt + 1}/{max_retries} не удалась: {e}") + if attempt < max_retries - 1: + import time + time.sleep((attempt + 1) * 2) + + raise last_error or Exception("Неизвестная ошибка при скачивании с VK") + + +@app.route('/health', methods=['GET']) +def health(): + """Health check endpoint""" + return jsonify({'status': 'ok', 'service': 'vk-downloader'}), 200 + + +@app.route('/download', methods=['POST']) +def download(): + """Скачивает видео с VK и возвращает файл""" + 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"Получен запрос на скачивание: {url}") + + # Проверяем, что это VK URL + if 'vk.com' not in url and 'vkontakte.ru' not in url: + return jsonify({'error': 'Only VK URLs are supported'}), 400 + + # Скачиваем видео + video_path = download_vk_video(url) + logger.info(f"Видео скачано: {video_path}") + + # Отправляем файл + return send_file( + str(video_path), + as_attachment=True, + download_name=video_path.name, + mimetype='video/mp4' + ) + + except Exception as e: + logger.error(f"Ошибка при скачивании: {e}") + return jsonify({'error': str(e)}), 500 + + +@app.route('/download/stream', methods=['POST']) +def download_stream(): + """Скачивает видео с VK и возвращает бинарные данные""" + 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}") + + # Проверяем, что это VK URL + if 'vk.com' not in url and 'vkontakte.ru' not in url: + return jsonify({'error': 'Only VK URLs are supported'}), 400 + + # Скачиваем видео + video_path = download_vk_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 'vk_video.mp4' + if not safe_filename.endswith('.mp4'): + safe_filename = 'vk_video.mp4' + + # Удаляем временный файл + video_path.unlink() + + return video_data, 200, { + 'Content-Type': 'video/mp4', + '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"Запуск VK Downloader сервиса на {host}:{port}") + app.run(host=host, port=port, debug=False) + diff --git a/vk-downloader/docker-compose.yml b/vk-downloader/docker-compose.yml new file mode 100644 index 0000000..2eae25b --- /dev/null +++ b/vk-downloader/docker-compose.yml @@ -0,0 +1,16 @@ +services: + vk-downloader: + build: . + container_name: vk_downloader_service + restart: unless-stopped + ports: + - "5555:5000" + volumes: + - ./downloads:/app/downloads + networks: + - vk_network + +networks: + vk_network: + driver: bridge + diff --git a/vk-downloader/requirements.txt b/vk-downloader/requirements.txt new file mode 100644 index 0000000..8962917 --- /dev/null +++ b/vk-downloader/requirements.txt @@ -0,0 +1,5 @@ +Flask==3.0.0 +flask-cors==4.0.0 +yt-dlp>=2024.12.13 +requests==2.31.0 +