diff --git a/.dockerignore b/.dockerignore index 8d1e1fa..40d911c 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,11 +1,31 @@ -__pycache__ -*.pyc -*.pyo -*.pyd -.Python -downloads/ -*.log -.git -.gitignore -README.md +# Исключаем папки сервисов +youtube-downloader/ +instagram-downloader/ +vk-downloader/ +# Исключаем данные и временные файлы +video/ +data/ +*.db +*.db-journal + +# Исключаем git +.git/ +.gitignore + +# Исключаем документацию +*.md +ARCHITECTURE.md + +# Исключаем скрипты +*.sh + +# Исключаем env файлы (будут переданы через docker-compose) +.env +.env.example + +# Исключаем cookies (будут переданы через volume) +instagram_cookies.txt + +# Исключаем старые файлы +bot.db/ diff --git a/.env.example b/.env.example index 68481d3..d109bb0 100644 --- a/.env.example +++ b/.env.example @@ -1,13 +1,15 @@ -# Токен Telegram бота (получить у @BotFather) +# Telegram Bot Configuration TELEGRAM_BOT_TOKEN=your_telegram_bot_token_here +TELEGRAM_BOT_USERNAME=your_bot_username -# Имя бота (username без @, используется в подписи видео) -TELEGRAM_BOT_USERNAME=vrubelVideoDownload_bot - -# URL VK сервиса для скачивания видео (должен быть на хосте без VPN) -# Для локальной разработки: http://localhost:5555 -# Для продакшена: http://:5555 +# Downloader Services URLs +# Для локальной разработки через docker-compose используются внутренние имена сервисов с портом 5000 +# Для продакшена или отдельного запуска сервисов укажите IP адреса или домены с внешними портами +YOUTUBE_DOWNLOADER_URL=http://localhost:5557 +INSTAGRAM_DOWNLOADER_URL=http://localhost:5556 VK_DOWNLOADER_URL=http://localhost:5555 -# Количество дней до истечения cookies, когда начинать автоматическое обновление (по умолчанию: 3) -INSTAGRAM_AUTO_UPDATE_DAYS=3 +# Примечание: Если используете docker-compose из корня проекта, можно использовать: +# YOUTUBE_DOWNLOADER_URL=http://youtube-downloader:5000 +# INSTAGRAM_DOWNLOADER_URL=http://instagram-downloader:5000 +# VK_DOWNLOADER_URL=http://vk-downloader:5000 diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 3c1185a..a9a8210 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -4,10 +4,12 @@ ## Общая архитектура -Система состоит из двух основных компонентов: +Система состоит из микросервисной архитектуры с раздельными сервисами для каждого источника видео: -1. **Основной бот** (`bot.py`) — Telegram бот, обрабатывающий запросы пользователей -2. **VK Downloader Service** (`vk-downloader/`) — отдельный микросервис для скачивания видео с VK +1. **Основной бот** (`bot.py` в корне проекта) — Telegram бот, обрабатывающий запросы пользователей и оркестрирующий запросы к сервисам +2. **YouTube Downloader Service** (`youtube-downloader/`) — микросервис для скачивания видео с YouTube +3. **Instagram Downloader Service** (`instagram-downloader/`) — микросервис для скачивания видео с Instagram +4. **VK Downloader Service** (`vk-downloader/`) — микросервис для скачивания видео с VK ``` ┌─────────────────┐ @@ -18,44 +20,55 @@ ┌─────────────────────────────────────┐ │ Основной бот (bot.py) │ │ ┌───────────────────────────────┐ │ -│ │ YouTube Download Handler │ │ +│ │ Message Handler │ │ +│ │ - URL extraction │ │ +│ │ - Source detection │ │ │ └───────────────────────────────┘ │ │ ┌───────────────────────────────┐ │ -│ │ Instagram Download Handler │ │ -│ └───────────────────────────────┘ │ -│ ┌───────────────────────────────┐ │ -│ │ VK API Client │──┼──┐ +│ │ HTTP API Clients │──┼──┐ +│ │ - YouTube API Client │ │ │ +│ │ - Instagram API Client │ │ │ +│ │ - VK API Client │ │ │ │ └───────────────────────────────┘ │ │ │ ┌───────────────────────────────┐ │ │ │ │ SQLite Database │ │ │ │ │ - Users │ │ │ │ │ - Stats │ │ │ │ └───────────────────────────────┘ │ │ +│ ┌───────────────────────────────┐ │ │ +│ │ Video Storage │ │ │ +│ │ - video/ directory │ │ │ +│ └───────────────────────────────┘ │ │ └─────────────────────────────────────┘ │ - │ HTTP API - │ POST /download/stream - ▼ - ┌──────────────────────┐ - │ VK Downloader │ - │ Service │ - │ ┌────────────────┐ │ - │ │ Flask API │ │ - │ └────────────────┘ │ - │ ┌────────────────┐ │ - │ │ yt-dlp │ │ - │ │ (VK only) │ │ - │ └────────────────┘ │ - └──────────────────────┘ + │ HTTP API + │ POST /download/stream + │ + ┌──────────────────────────────────┼──────────────────┐ + │ │ │ + ▼ ▼ ▼ +┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐ +│ YouTube Downloader│ │Instagram Download│ │ VK Downloader │ +│ Service │ │ Service │ │ Service │ +│ ┌──────────────┐ │ │ ┌──────────────┐ │ │ ┌──────────────┐│ +│ │ Flask API │ │ │ │ Flask API │ │ │ │ Flask API ││ +│ └──────────────┘ │ │ └──────────────┘ │ │ └──────────────┘│ +│ ┌──────────────┐ │ │ ┌──────────────┐ │ │ ┌──────────────┐│ +│ │ yt-dlp │ │ │ │ yt-dlp │ │ │ │ yt-dlp ││ +│ │ (YouTube) │ │ │ │ (Instagram) │ │ │ │ (VK only) ││ +│ └──────────────┘ │ │ └──────────────┘ │ │ └──────────────┘│ +│ Port: 5557 │ │ Port: 5556 │ │ Port: 5555 │ +└──────────────────┘ └──────────────────┘ └──────────────────┘ ``` ## Компоненты системы ### 1. Основной бот (bot.py) +**Расположение:** Корень проекта + **Технологии:** - `python-telegram-bot` (v20.7) — асинхронный фреймворк для Telegram Bot API -- `yt-dlp` — библиотека для скачивания видео -- `httpx` — асинхронный HTTP клиент для запросов к VK сервису +- `httpx` — асинхронный HTTP клиент для запросов к сервисам загрузчиков - `sqlite3` — база данных для хранения статистики **Архитектурные решения:** @@ -64,10 +77,20 @@ - Асинхронная обработка через `asyncio` - Каждый пользовательский запрос обрабатывается независимой корутиной - Параллельная обработка нескольких запросов +- Автоматическое извлечение URL из текста сообщений (работа в группах) #### Скачивание видео -- **YouTube/Instagram**: Прямое скачивание через `yt-dlp` в executor (не блокирует event loop) -- **VK**: HTTP запрос к внешнему микросервису через `httpx` +- **Все источники**: HTTP запросы к соответствующим микросервисам через `httpx` +- **YouTube**: `POST http://youtube-downloader:5000/download/stream` +- **Instagram**: `POST http://instagram-downloader:5000/download/stream` +- **VK**: `POST http://vk-downloader:5000/download/stream` +- Получение бинарных данных и сохранение во временный файл +- Отправка через Telegram API + +#### Хранение видео +- Все скачанные видео сохраняются в папке `video/` на хосте +- Файлы не удаляются автоматически (сохраняются для пользователей) +- Автоматическая очистка только `.part` файлов (недокачанные) #### База данных - SQLite с двумя таблицами: @@ -80,13 +103,67 @@ - Retry механизм для всех источников (3 попытки по умолчанию) - Логирование всех ошибок - Информативные сообщения пользователю +- Автоматическая очистка `.part` файлов при ошибках -### 2. VK Downloader Service +### 2. YouTube Downloader Service + +**Расположение:** `youtube-downloader/` + +**Технологии:** +- Flask — веб-фреймворк для REST API +- `yt-dlp` — библиотека для скачивания видео с YouTube +- Flask-CORS — для поддержки CORS + +**API Endpoints:** + +1. `GET /health` — проверка работоспособности сервиса + - Возвращает: `{"status": "ok", "service": "youtube-downloader"}` + +2. `POST /download/stream` — скачивание видео + - Request: `{"url": "https://youtube.com/watch?v=..."}` + - Response: Бинарные данные видео (200 OK) или JSON с ошибкой (400/500) + +**Особенности:** +- Использует специальные настройки yt-dlp для YouTube (player_client, headers) +- Поддержка различных форматов видео (mp4, webm, mkv) +- Временные файлы сохраняются в `downloads/` и удаляются после отправки + +**Порт:** 5557 (внешний) → 5000 (внутренний) + +### 3. Instagram Downloader Service + +**Расположение:** `instagram-downloader/` + +**Технологии:** +- Flask — веб-фреймворк для REST API +- `yt-dlp` — библиотека для скачивания видео с Instagram +- Flask-CORS — для поддержки CORS + +**API Endpoints:** + +1. `GET /health` — проверка работоспособности сервиса + - Возвращает: `{"status": "ok", "service": "instagram-downloader"}` + +2. `POST /download/stream` — скачивание видео + - Request: `{"url": "https://instagram.com/p/..."}` + - Response: Бинарные данные видео (200 OK) или JSON с ошибкой (400/500) + +**Особенности:** +- Требует файл с cookies Instagram (`instagram_cookies.txt` в папке сервиса) +- Автоматическая проверка срока действия cookies +- Поддержка CSRF токенов и session cookies +- Скрипты для получения/обновления cookies в папке сервиса + +**Порт:** 5556 (внешний) → 5000 (внутренний) + +### 4. VK Downloader Service + +**Расположение:** `vk-downloader/` **Технологии:** - Flask — веб-фреймворк для REST API - `yt-dlp` — библиотека для скачивания видео с VK -- Flask-CORS — для поддержки CORS (если нужно) +- Flask-CORS — для поддержки CORS **API Endpoints:** @@ -97,22 +174,14 @@ - Request: `{"url": "https://vk.com/clip-..."}` - Response: Бинарные данные видео (200 OK) или JSON с ошибкой (400/500) -**Особенности реализации:** +**Особенности:** +- Специальные заголовки для VK (User-Agent, Referer) +- Обработка кириллицы в именах файлов +- Временные файлы сохраняются в `downloads/` и удаляются после отправки -#### Обработка кириллицы -- Имена файлов с кириллицей конвертируются в ASCII для HTTP заголовков -- Оригинальное имя сохраняется в файловой системе +**Порт:** 5555 (внешний) → 5000 (внутренний) -#### Управление файлами -- Временные файлы сохраняются в `downloads/` -- Файлы удаляются после отправки клиенту -- Использование UUID для уникальности имен - -#### Ограничения -- Flask dev server (однопоточный) — обрабатывает запросы последовательно -- Для продакшена рекомендуется использовать Gunicorn с несколькими worker'ами - -### 3. Определение источника видео +### 5. Определение источника видео Функция `detect_video_source(url)` анализирует домен URL: @@ -123,29 +192,56 @@ - иначе → 'unknown' ``` -### 4. Потоки данных +### 6. Потоки данных -#### Скачивание с YouTube/Instagram +#### Общий поток скачивания ``` -User → Bot → detect_video_source() → download_youtube_video() / download_instagram_video() +User → Bot → extract_urls_from_text() → detect_video_source() ↓ - yt-dlp (executor) → video file → Telegram API → User -``` - -#### Скачивание с VK - -``` -User → Bot → detect_video_source() → download_vk_video() - ↓ - HTTP POST /download/stream → VK Service + HTTP POST /download/stream → [YouTube/Instagram/VK] Service ↓ yt-dlp → video file → HTTP Response (binary) ↓ - Bot receives binary → saves to disk → Telegram API → User + Bot receives binary → saves to video/ → Telegram API → User ``` -### 5. База данных +#### Детальный поток для каждого источника + +**YouTube:** +``` +User → Bot → download_youtube_video() + ↓ + httpx.AsyncClient → POST http://youtube-downloader:5000/download/stream + ↓ + YouTube Service → yt-dlp → binary data + ↓ + Bot saves to video/ → sends to User +``` + +**Instagram:** +``` +User → Bot → download_instagram_video() + ↓ + httpx.AsyncClient → POST http://instagram-downloader:5000/download/stream + ↓ + Instagram Service → yt-dlp (with cookies) → binary data + ↓ + Bot saves to video/ → sends to User +``` + +**VK:** +``` +User → Bot → download_vk_video() + ↓ + httpx.AsyncClient → POST http://vk-downloader:5000/download/stream + ↓ + VK Service → yt-dlp → binary data + ↓ + Bot saves to video/ → sends to User +``` + +### 7. База данных **Схема:** @@ -179,21 +275,72 @@ CREATE TABLE stats ( ### Основной бот +**Расположение:** Корень проекта + **Образ:** - Базовый: `python:3.11-slim` -- Зависимости: `ffmpeg`, `wget` -- Python пакеты из `requirements.txt` +- Python пакеты из `requirements.txt` (python-telegram-bot, httpx) **Volumes:** -- `./video:/app/video` — временные файлы видео -- `./instagram_cookies.txt:/app/instagram_cookies.txt` — cookies для Instagram +- `./video:/app/video` — сохраненные видео (не удаляются) - `./data:/app/data:Z` — база данных (SELinux relabel) **Network:** -- `network_mode: host` — для доступа к VK сервису через localhost +- `network_mode: host` — для доступа к сервисам по localhost или IP + +**Запуск:** +```bash +docker compose up -d +``` + +### YouTube Downloader Service + +**Расположение:** `youtube-downloader/` + +**Образ:** +- Базовый: `python:3.11-slim` +- Зависимости: `ffmpeg`, `wget` +- Python пакеты: Flask, flask-cors, yt-dlp + +**Volumes:** +- `./downloads:/app/downloads` — временные файлы + +**Ports:** +- `5557:5000` — внешний порт 5557, внутренний 5000 + +**Запуск:** +```bash +cd youtube-downloader && docker compose up -d +``` + +### Instagram Downloader Service + +**Расположение:** `instagram-downloader/` + +**Образ:** +- Базовый: `python:3.11-slim` +- Зависимости: `ffmpeg`, `wget` +- Python пакеты: Flask, flask-cors, yt-dlp + +**Volumes:** +- `./downloads:/app/downloads` — временные файлы +- `./instagram_cookies.txt:/app/instagram_cookies.txt:ro` — cookies (read-only) + +**Environment:** +- `INSTAGRAM_COOKIES_FILE=/app/instagram_cookies.txt` + +**Ports:** +- `5556:5000` — внешний порт 5556, внутренний 5000 + +**Запуск:** +```bash +cd instagram-downloader && docker compose up -d +``` ### VK Downloader Service +**Расположение:** `vk-downloader/` + **Образ:** - Базовый: `python:3.11-slim` - Зависимости: `ffmpeg`, `wget` @@ -205,22 +352,27 @@ CREATE TABLE stats ( **Ports:** - `5555:5000` — внешний порт 5555, внутренний 5000 -**Network:** -- Отдельная сеть `vk_network` (можно использовать host network для доступа) +**Запуск:** +```bash +cd vk-downloader && docker compose up -d +``` ## Безопасность ### Переменные окружения - Токен бота хранится в `.env` (не коммитится в Git) -- Cookies для Instagram хранятся в файле (не коммитится) +- Cookies для Instagram хранятся в файле `instagram-downloader/instagram_cookies.txt` (не коммитится) +- URL сервисов настраиваются через `.env` ### Ограничения доступа -- VK сервис доступен только по указанному IP/URL -- Нет аутентификации между ботом и VK сервисом (можно добавить API key) +- Сервисы загрузчиков доступны только по указанным IP/URL +- Нет аутентификации между ботом и сервисами (можно добавить API key) +- Cookies файл монтируется read-only ### Файловая система -- Временные файлы удаляются после отправки -- Cookies файл монтируется read-only (опционально) +- Видео сохраняются в `video/` и не удаляются автоматически +- Временные файлы в сервисах удаляются после отправки +- Автоматическая очистка только `.part` файлов ## Масштабирование @@ -228,15 +380,17 @@ CREATE TABLE stats ( 1. **Основной бот:** - Один экземпляр (можно запустить несколько с разными токенами) - - Параллельная обработка запросов ограничена ресурсами CPU/сети + - Параллельная обработка запросов через asyncio + - Ограничения: ресурсы CPU/сети -2. **VK сервис:** +2. **Сервисы загрузчиков:** - Flask dev server — последовательная обработка - - Один экземпляр контейнера + - Один экземпляр контейнера каждого сервиса + - Можно масштабировать горизонтально ### Рекомендации для масштабирования -1. **VK сервис:** +1. **Сервисы загрузчиков:** - Использовать Gunicorn с несколькими worker'ами: ```bash gunicorn -w 4 -b 0.0.0.0:5000 app:app @@ -267,7 +421,7 @@ CREATE TABLE stats ( - Размер скачанных файлов ### Health checks -- VK сервис: `GET /health` +- Все сервисы: `GET /health` - Основной бот: проверка через статус контейнера ## Производительность @@ -276,11 +430,11 @@ CREATE TABLE stats ( 1. **Асинхронная обработка:** - Основной бот использует asyncio для неблокирующих операций - - yt-dlp запускается в executor для YouTube/Instagram + - HTTP запросы к сервисам через httpx (асинхронный) -2. **Временные файлы:** - - Хранение в памяти (tmpfs) для быстрого доступа (опционально) - - Автоматическая очистка после отправки +2. **Хранение видео:** + - Видео сохраняются на хосте (volume), не в образе контейнера + - Файлы доступны для пользователей после скачивания 3. **Кеширование:** - Можно добавить кеш информации о видео (title, duration) @@ -288,34 +442,44 @@ CREATE TABLE stats ( ### Узкие места -1. **VK сервис:** - - Последовательная обработка запросов +1. **Сервисы загрузчиков:** + - Последовательная обработка запросов (Flask dev server) - Скачивание файла перед отправкой (занимает память) 2. **Сеть:** - Зависимость от скорости интернета - - Задержки при обращении к внешнему VK сервису + - Задержки при обращении к внешним сервисам + - Задержки между ботом и сервисами загрузчиков ## Развертывание ### Локальная разработка -```bash -# Основной бот -docker compose up -d -# VK сервис -cd vk-downloader && docker compose up -d +```bash +# Запуск сервисов загрузчиков (каждый в своей папке) +cd youtube-downloader && docker compose up -d +cd ../instagram-downloader && docker compose up -d +cd ../vk-downloader && docker compose up -d + +# Запуск основного бота (из корня проекта) +cd .. +docker compose up -d ``` ### Продакшен (раздельное развертывание) -**Хост 1 (с VPN):** +**Хост 1 (с VPN для YouTube/Instagram):** - Основной бот +- YouTube сервис (порт 5557) +- Instagram сервис (порт 5556) - VPN для доступа к YouTube/Instagram -- `.env`: `VK_DOWNLOADER_URL=http://:5555` +- `.env`: + - `YOUTUBE_DOWNLOADER_URL=http://localhost:5557` + - `INSTAGRAM_DOWNLOADER_URL=http://localhost:5556` + - `VK_DOWNLOADER_URL=http://:5555` -**Хост 2 (без VPN):** -- VK Downloader Service +**Хост 2 (без VPN для VK):** +- VK Downloader Service (порт 5555) - Доступен по IP для хоста 1 - Можно масштабировать горизонтально @@ -328,7 +492,7 @@ cd vk-downloader && docker compose up -d ## Будущие улучшения 1. **Аутентификация между сервисами:** - - API key для VK сервиса + - API key для сервисов загрузчиков - JWT токены 2. **Очередь задач:** @@ -339,8 +503,12 @@ cd vk-downloader && docker compose up -d - Prometheus метрики - Grafana дашборды -4. **Улучшение VK сервиса:** +4. **Улучшение сервисов:** - Переход на FastAPI (более производительный) - Поддержка WebSockets для прогресса - Stream ответа вместо полной загрузки в память +5. **Хранение видео:** + - Организация по датам/пользователям + - Автоматическая очистка старых файлов (опционально) + - Интеграция с облачным хранилищем diff --git a/Dockerfile b/Dockerfile index 81dc737..8a9a5e9 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,25 +1,17 @@ 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 . . +# Копируем код приложения (только bot.py, не копируем папки сервисов) +COPY bot.py . -# Создаем директорию для загрузок -RUN mkdir -p video +# Создаем директории для данных +RUN mkdir -p video data -# Увеличиваем таймауты для SSL (в секундах) ENV PYTHONUNBUFFERED=1 CMD ["python", "bot.py"] - diff --git a/README.md b/README.md index b2b078c..ab1ede7 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,26 @@ # Telegram Video Download Bot -Telegram бот для скачивания видео с YouTube, Instagram и VK. Поддерживает раздельное развертывание сервисов для работы с VPN и без VPN. +Telegram бот для скачивания видео с YouTube, Instagram и VK. Микросервисная архитектура с раздельными сервисами для каждого источника. + +## Архитектура + +Проект разделен на микросервисы: + +- **Основной бот** (в корне проекта) - Telegram бот, обрабатывает сообщения и оркестрирует запросы к сервисам +- **youtube-downloader** - сервис для скачивания с YouTube (порт 5557) +- **instagram-downloader** - сервис для скачивания с Instagram (порт 5556) +- **vk-downloader** - сервис для скачивания с VK (порт 5555) + +Каждый сервис работает в отдельном Docker контейнере и может быть развернут независимо. ## Возможности - 📹 Скачивание видео с YouTube - 📸 Скачивание видео с Instagram (требуются cookies) -- 🎬 Скачивание видео с VK (через отдельный микросервис) +- 🎬 Скачивание видео с VK - 📊 Статистика скачанных видео и пользователей - 🔄 Автоматическое сохранение статистики в базу данных +- 👥 Работа в группах с автоматическим обнаружением ссылок ## Требования @@ -27,7 +39,7 @@ cd videoDownloadBot ### 2. Настройка переменных окружения -Скопируйте `.env.example` в `.env` и заполните: +Скопируйте `.env.example` в `.env` в корне проекта и заполните: ```bash cp .env.example .env @@ -39,56 +51,119 @@ nano .env # или используйте любой редактор ```env TELEGRAM_BOT_TOKEN=your_telegram_bot_token_here TELEGRAM_BOT_USERNAME=your_bot_username -VK_DOWNLOADER_URL=http://localhost:5555 + +# Downloader Services URLs +# Для локальной разработки через docker-compose используются внутренние имена сервисов +YOUTUBE_DOWNLOADER_URL=http://youtube-downloader:5000 +INSTAGRAM_DOWNLOADER_URL=http://instagram-downloader:5000 +VK_DOWNLOADER_URL=http://vk-downloader:5000 ``` - **TELEGRAM_BOT_TOKEN** — токен бота от @BotFather - **TELEGRAM_BOT_USERNAME** — username бота (без @), используется в подписи видео -- **VK_DOWNLOADER_URL** — URL VK сервиса (для локальной разработки: `http://localhost:5555`, для продакшена: `http://:5555`) +- **YOUTUBE_DOWNLOADER_URL** — URL YouTube сервиса (для docker-compose: `http://youtube-downloader:5000`, для продакшена: `http://:5557`) +- **INSTAGRAM_DOWNLOADER_URL** — URL Instagram сервиса (для docker-compose: `http://instagram-downloader:5000`, для продакшена: `http://:5556`) +- **VK_DOWNLOADER_URL** — URL VK сервиса (для docker-compose: `http://vk-downloader:5000`, для продакшена: `http://:5555`) ### 3. Настройка Instagram (опционально) Если планируете скачивать видео с Instagram: -1. Экспортируйте cookies из браузера (см. `INSTAGRAM_COOKIES_INSTRUCTIONS.md`) -2. Сохраните файл как `instagram_cookies.txt` в корне проекта +1. Экспортируйте cookies из браузера (см. `instagram-downloader/INSTAGRAM_COOKIES_INSTRUCTIONS.md`) +2. Сохраните файл как `instagram_cookies.txt` в папке `instagram-downloader/` 3. Формат: Netscape cookies file -**Автоматическое обновление cookies:** -- Бот автоматически проверяет срок действия cookies каждые 24 часа -- Если cookies истекают через 3 дня (настраивается через `INSTAGRAM_AUTO_UPDATE_DAYS`), бот попытается автоматически обновить их из браузера -- Поддерживаются браузеры: Chrome, Firefox, Edge, Opera (по приоритету) -- Для автоматического обновления браузер должен быть установлен и доступен - -**Примечание:** Без cookies Instagram может блокировать запросы. При первом запуске или если автоматическое обновление не сработало, обновите cookies вручную. - -### 4. Запуск основного бота +**Быстрое получение cookies через скрипт:** ```bash +cd instagram-downloader +./get_instagram_cookies.sh +``` + +Или обновление существующих cookies: + +```bash +cd instagram-downloader +./update_instagram_cookies.sh +``` + +**Примечание:** Без cookies Instagram может блокировать запросы. При первом запуске обновите cookies вручную. + +### 4. Запуск сервисов + +**Важно:** Каждый сервис запускается отдельно на своем хосте (или на одном хосте, но отдельными командами). + +#### Запуск основного бота + +```bash +# Из корня проекта docker compose up -d ``` -Бот запустится и будет готов к работе! +Это запустит только основной Telegram бот. + +#### Запуск сервисов загрузчиков + +Каждый сервис запускается отдельно в своей папке: -Проверить статус: ```bash +# Запуск YouTube сервиса (порт 5557) +cd youtube-downloader +docker compose up -d + +# Запуск Instagram сервиса (порт 5556) +cd ../instagram-downloader +docker compose up -d + +# Запуск VK сервиса (порт 5555) +cd ../vk-downloader +docker compose up -d +``` + +**Порядок запуска:** Сначала запустите сервисы загрузчиков, затем основной бот. + +#### Раздельное развертывание на разных хостах (рекомендуется для продакшена) + +**Хост 1 (с VPN для YouTube/Instagram):** +- YouTube сервис (порт 5557) +- Instagram сервис (порт 5556) +- Основной бот + +**Хост 2 (без VPN для VK):** +- VK сервис (порт 5555) + +В `.env` файле в корне проекта на хосте 1 укажите IP адреса сервисов: +```env +YOUTUBE_DOWNLOADER_URL=http://localhost:5557 +INSTAGRAM_DOWNLOADER_URL=http://localhost:5556 +VK_DOWNLOADER_URL=http://:5555 +``` + +### 5. Проверка статуса + +Проверить статус сервисов: + +```bash +# Основной бот (из корня проекта) docker compose ps -docker compose logs -f bot + +# Сервисы загрузчиков (из соответствующих папок) +cd youtube-downloader && docker compose ps +cd ../instagram-downloader && docker compose ps +cd ../vk-downloader && docker compose ps ``` -### 5. Запуск VK сервиса (опционально) - -VK сервис можно запустить на том же хосте или на отдельном хосте без VPN: +Просмотр логов: ```bash -cd vk-downloader -docker compose up -d -``` +# Основной бот (из корня проекта) +docker compose logs -f bot -Для работы на отдельном хосте: -1. Скопируйте папку `vk-downloader` на целевой хост -2. Запустите: `docker compose up -d` -3. Обновите `VK_DOWNLOADER_URL` в `.env` основного бота: `http://:5555` +# Сервисы загрузчиков (из соответствующих папок) +cd youtube-downloader && docker compose logs -f +cd ../instagram-downloader && docker compose logs -f +cd ../vk-downloader && docker compose logs -f +``` ## Использование @@ -97,6 +172,10 @@ docker compose up -d 3. Отправьте ссылку на видео (YouTube, Instagram или VK) 4. Дождитесь скачивания и получите файл +### Работа в группах + +Добавьте бота в группу и дайте ему права администратора (нужно право на удаление сообщений). После этого бот будет автоматически находить ссылки на видео в сообщениях участников, скачивать их и отправлять прямо в группу, заменяя исходное сообщение со ссылкой. + ### Команды - `/start` — начало работы с ботом @@ -106,50 +185,82 @@ docker compose up -d ``` videoDownloadBot/ -├── bot.py # Основной код бота -├── requirements.txt # Python зависимости -├── Dockerfile # Образ для основного бота -├── docker-compose.yml # Конфигурация основного бота -├── .env.example # Пример конфигурации -├── instagram_cookies.txt # Cookies для Instagram (создать вручную) -├── data/ # База данных SQLite (создается автоматически) -├── video/ # Временные файлы видео (создается автоматически) -└── vk-downloader/ # Микросервис для VK - ├── app.py # Flask API сервис - ├── Dockerfile # Образ для VK сервиса - └── docker-compose.yml # Конфигурация VK сервиса +├── bot.py # Код основного Telegram бота +├── requirements.txt # Python зависимости бота +├── Dockerfile # Образ для бота +├── docker-compose.yml # Оркестратор всех сервисов +├── .env.example # Пример конфигурации +├── youtube-downloader/ # Сервис для YouTube +│ ├── app.py # Flask API сервис +│ ├── requirements.txt # Python зависимости +│ ├── Dockerfile # Образ для YouTube сервиса +│ └── docker-compose.yml # Конфигурация YouTube сервиса +├── instagram-downloader/ # Сервис для Instagram +│ ├── app.py # Flask API сервис +│ ├── requirements.txt # Python зависимости +│ ├── Dockerfile # Образ для Instagram сервиса +│ ├── docker-compose.yml # Конфигурация Instagram сервиса +│ ├── instagram_cookies.txt # Cookies для Instagram (создать вручную) +│ ├── get_instagram_cookies.sh # Скрипт для получения cookies +│ ├── update_instagram_cookies.sh # Скрипт для обновления cookies +│ ├── INSTAGRAM_COOKIES_INSTRUCTIONS.md # Инструкции по cookies +│ └── README.md # Документация сервиса +├── vk-downloader/ # Сервис для VK +│ ├── app.py # Flask API сервис +│ ├── requirements.txt # Python зависимости +│ ├── Dockerfile # Образ для VK сервиса +│ └── docker-compose.yml # Конфигурация VK сервиса +└── README.md # Этот файл +``` + +## Порты сервисов + +- **Основной бот**: не требует внешних портов (работает через Telegram API) +- **YouTube Downloader**: порт 5557 +- **Instagram Downloader**: порт 5556 +- **VK Downloader**: порт 5555 + +## API Endpoints + +Все сервисы загрузчиков предоставляют одинаковый API: + +- `GET /health` - проверка здоровья сервиса +- `POST /download/stream` - скачивание видео (возвращает бинарные данные) + +Пример запроса: +```json +POST /download/stream +Content-Type: application/json + +{ + "url": "https://youtube.com/watch?v=..." +} ``` ## Обновление ```bash git pull -docker compose build -docker compose up -d -``` -## Логи +# Пересобрать и перезапустить основной бот (из корня проекта) +docker compose build && docker compose up -d -Просмотр логов основного бота: -```bash -docker compose logs -f bot -``` - -Просмотр логов VK сервиса: -```bash -cd vk-downloader -docker compose logs -f +# Пересобрать и перезапустить каждый сервис загрузчика отдельно +cd youtube-downloader && docker compose build && docker compose up -d +cd ../instagram-downloader && docker compose build && docker compose up -d +cd ../vk-downloader && docker compose build && docker compose up -d ``` ## Остановка ```bash -# Основной бот +# Остановить основной бот (из корня проекта) docker compose down -# VK сервис -cd vk-downloader -docker compose down +# Остановить каждый сервис загрузчика отдельно +cd youtube-downloader && docker compose down +cd ../instagram-downloader && docker compose down +cd ../vk-downloader && docker compose down ``` ## Развертывание на продакшене @@ -157,41 +268,41 @@ docker compose down ### Вариант 1: Все на одном хосте (с VPN) 1. Настройте VPN для доступа к YouTube и Instagram -2. Запустите основной бот и VK сервис на одном хосте -3. В `.env` укажите: `VK_DOWNLOADER_URL=http://localhost:5555` +2. Запустите каждый сервис отдельно в своей папке +3. В `.env` в корне проекта укажите: `http://localhost:5557`, `http://localhost:5556`, `http://localhost:5555` ### Вариант 2: Раздельное развертывание (рекомендуется) **Хост 1 (с VPN):** -- Основной бот (YouTube, Instagram) -- В `.env`: `VK_DOWNLOADER_URL=http://:5555` +- Основной бот +- YouTube сервис +- Instagram сервис +- В `.env` в корне: `YOUTUBE_DOWNLOADER_URL=http://localhost:5557`, `INSTAGRAM_DOWNLOADER_URL=http://localhost:5556`, `VK_DOWNLOADER_URL=http://:5555` **Хост 2 (без VPN):** -- VK сервис (`vk-downloader/`) -- В `.env` основного бота: IP этого хоста +- VK сервис +- В `.env` corebot на хосте 1: IP этого хоста **Преимущества:** - VK работает быстрее без VPN - Меньше нагрузка на VPN канал -- Возможность масштабирования VK сервиса отдельно +- Возможность масштабирования сервисов отдельно ## Troubleshooting ### Бот не отвечает - Проверьте логи: `docker compose logs bot` - Убедитесь, что токен правильный в `.env` +- Проверьте, что все сервисы запущены и доступны -### Instagram не работает -- Проверьте наличие `instagram_cookies.txt` -- Обновите cookies (они могут истечь) - -### VK не работает -- Проверьте, что VK сервис запущен: `cd vk-downloader && docker compose ps` -- Проверьте URL в `.env`: `VK_DOWNLOADER_URL` -- Проверьте доступность порта 5555 +### YouTube/Instagram/VK не работает +- Проверьте, что соответствующий сервис запущен: `docker compose ps` +- Проверьте URL в `.env` corebot +- Проверьте логи сервиса: `docker compose logs -f` +- Для Instagram: проверьте наличие и валидность `instagram-downloader/instagram_cookies.txt` ### База данных не сохраняется -- Проверьте права на папку `data/` +- Проверьте права на папку `data/` в корне проекта - Убедитесь, что volume смонтирован в `docker-compose.yml` ## Лицензия diff --git a/bot.py b/bot.py index 0bf9199..bfb5de6 100644 --- a/bot.py +++ b/bot.py @@ -1,17 +1,13 @@ import os import re -import json import logging import asyncio import sqlite3 import time -import subprocess from pathlib import Path from urllib.parse import urlparse from datetime import datetime -from concurrent.futures import ThreadPoolExecutor -import yt_dlp import httpx from telegram import Update from telegram.ext import Application, MessageHandler, filters, ContextTypes, CommandHandler @@ -26,7 +22,10 @@ logger = logging.getLogger(__name__) # Токен бота и имя бота из переменных окружения TELEGRAM_BOT_TOKEN = os.getenv('TELEGRAM_BOT_TOKEN') TELEGRAM_BOT_USERNAME = os.getenv('TELEGRAM_BOT_USERNAME', 'vrubelVideoDownload_bot') -# URL VK сервиса для скачивания видео + +# URL сервисов для скачивания видео +YOUTUBE_DOWNLOADER_URL = os.getenv('YOUTUBE_DOWNLOADER_URL', 'http://localhost:5557') +INSTAGRAM_DOWNLOADER_URL = os.getenv('INSTAGRAM_DOWNLOADER_URL', 'http://localhost:5556') VK_DOWNLOADER_URL = os.getenv('VK_DOWNLOADER_URL', 'http://localhost:5555') # Базовая директория проекта (абсолютный путь), чтобы не зависеть от рабочей директории процесса @@ -41,9 +40,6 @@ DATA_DIR = BASE_DIR / 'data' DATA_DIR.mkdir(parents=True, exist_ok=True) DB_FILE = DATA_DIR / 'bot.db' -# ThreadPoolExecutor для выполнения блокирующих операций (скачивание видео) -# Позволяет обрабатывать несколько запросов параллельно -DOWNLOAD_EXECUTOR = ThreadPoolExecutor(max_workers=5, thread_name_prefix="download") def init_database(): """Инициализирует базу данных и создает таблицы если их нет""" @@ -174,458 +170,144 @@ def extract_urls_from_text(text: str) -> list[str]: return urls -def cleanup_old_files(max_age_hours: int = 24): - """Удаляет старые файлы и .part файлы из папки загрузок""" +def cleanup_old_files(): + """Удаляет только .part файлы (недокачанные) из папки загрузок""" try: - current_time = time.time() - max_age_seconds = max_age_hours * 3600 - for file_path in DOWNLOADS_DIR.glob('*'): if not file_path.is_file(): continue - # Удаляем все .part файлы (недокачанные) + # Удаляем только .part файлы (недокачанные) if file_path.suffix == '.part': try: file_path.unlink() logger.info(f"Удален .part файл: {file_path.name}") except Exception as e: logger.warning(f"Не удалось удалить .part файл {file_path.name}: {e}") - continue - - # Удаляем старые файлы (старше max_age_hours) - try: - file_age = current_time - file_path.stat().st_mtime - if file_age > max_age_seconds: - file_path.unlink() - logger.info(f"Удален старый файл: {file_path.name} (возраст: {file_age/3600:.1f} часов)") - except Exception as e: - logger.warning(f"Не удалось проверить/удалить файл {file_path.name}: {e}") except Exception as e: - logger.error(f"Ошибка при очистке старых файлов: {e}") - - -def _safe_filename(title: str, chat_id: int) -> str: - """Создает безопасное имя файла""" - safe_title = re.sub(r'[<>:"/\\|?*]', '', title)[:100] - return str(DOWNLOADS_DIR / f'{chat_id}_{safe_title}.%(ext)s') + logger.error(f"Ошибка при очистке .part файлов: {e}") async def download_youtube_video(url: str, chat_id: int, max_retries: int = 3) -> str: - """Скачивает видео с YouTube""" - 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' + """Скачивает видео с YouTube через внешний сервис""" + logger.info(f"YouTube: отправка запроса на внешний сервис {YOUTUBE_DOWNLOADER_URL}") last_error = None for attempt in range(max_retries): try: - # Получаем информацию о видео - ydl_opts_info = { - 'quiet': False, - 'no_warnings': False, - 'user_agent': user_agent, - 'socket_timeout': 30, - 'extractor_args': { - 'youtube': { - 'player_client': ['android', 'web'], - 'player_skip': ['webpage'], - }, - }, - 'http_headers': { - 'User-Agent': user_agent, - 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', - 'Accept-Language': 'en-us,en;q=0.5', - 'Accept-Encoding': 'gzip, deflate', - 'Connection': 'keep-alive', - }, - } - - # Получаем информацию о видео в executor (неблокирующе) - def extract_info_sync(): - with yt_dlp.YoutubeDL(ydl_opts_info) as ydl: - return ydl.extract_info(url, download=False) - - loop = asyncio.get_event_loop() - info = await loop.run_in_executor(DOWNLOAD_EXECUTOR, extract_info_sync) - video_title = info.get('title', 'video') - logger.info(f"YouTube: получена информация о видео: {video_title}") - - # Скачиваем видео - ydl_opts_download = { - 'format': 'bestvideo[ext=mp4]+bestaudio[ext=m4a]/best[ext=mp4]/best', - 'outtmpl': _safe_filename(video_title, chat_id), - 'quiet': False, - 'no_warnings': False, - 'user_agent': user_agent, - 'socket_timeout': 30, - 'extractor_args': { - 'youtube': { - 'player_client': ['android', 'web'], - 'player_skip': ['webpage'], - }, - }, - 'http_headers': { - 'User-Agent': user_agent, - 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', - 'Accept-Language': 'en-us,en;q=0.5', - 'Accept-Encoding': 'gzip, deflate', - 'Connection': 'keep-alive', - }, - } - - logger.info(f"YouTube: начинаем скачивание (попытка {attempt + 1}/{max_retries})") - - # Скачиваем в executor (неблокирующе) - def download_sync(): - with yt_dlp.YoutubeDL(ydl_opts_download) as ydl: - ydl.download([url]) - - await loop.run_in_executor(DOWNLOAD_EXECUTOR, download_sync) - - # Находим скачанный файл (тоже в executor для консистентности) - def find_downloaded_file(): - 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]) - return None - - video_path = await loop.run_in_executor(DOWNLOAD_EXECUTOR, find_downloaded_file) - if video_path: - return video_path - else: - raise Exception("Файл не был найден после скачивания") + async with httpx.AsyncClient(timeout=600.0) as client: # Увеличенный таймаут для YouTube + # Отправляем запрос на YouTube сервис + response = await client.post( + f"{YOUTUBE_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"YouTube сервис вернул ошибку {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}_youtube_video.{video_ext}' + + # Сохраняем файл + video_path = DOWNLOADS_DIR / video_filename + with open(video_path, 'wb') as f: + f.write(video_data) + + logger.info(f"YouTube: видео скачано через внешний сервис: {video_path}") + return str(video_path) + + except httpx.TimeoutException: + last_error = Exception(f"Таймаут при запросе к YouTube сервису (попытка {attempt + 1}/{max_retries})") + logger.warning(f"YouTube: таймаут при запросе к сервису: {last_error}") except Exception as e: last_error = e logger.warning(f"YouTube: попытка {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("Неизвестная ошибка при скачивании с YouTube") + raise last_error or Exception("Неизвестная ошибка при скачивании с YouTube через внешний сервис") async def download_instagram_video(url: str, chat_id: int, max_retries: int = 3) -> str: - """Скачивает видео с 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}") + """Скачивает видео с Instagram через внешний сервис""" + logger.info(f"Instagram: отправка запроса на внешний сервис {INSTAGRAM_DOWNLOADER_URL}") last_error = None for attempt in range(max_retries): try: - # Базовые настройки - ydl_opts = { - 'format': 'best', - 'outtmpl': str(DOWNLOADS_DIR / f'{chat_id}_%(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}") + async with httpx.AsyncClient(timeout=600.0) as client: # Увеличенный таймаут для Instagram + # Отправляем запрос на Instagram сервис + response = await client.post( + f"{INSTAGRAM_DOWNLOADER_URL}/download/stream", + json={"url": url}, + headers={"Content-Type": "application/json"} + ) - # Добавляем заголовки с 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)})") + 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"Instagram сервис вернул ошибку {response.status_code}: {error_text}") - ydl_opts['http_headers'] = headers - - logger.info(f"Instagram: начинаем скачивание (попытка {attempt + 1}/{max_retries})") - - # Скачиваем в executor (неблокирующе) - def download_sync(): - with yt_dlp.YoutubeDL(ydl_opts) as ydl: - ydl.download([url]) - - loop = asyncio.get_event_loop() - await loop.run_in_executor(DOWNLOAD_EXECUTOR, download_sync) - - # Находим скачанный файл (тоже в executor для консистентности) - def find_downloaded_file(): - 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]) - return None - - video_path = await loop.run_in_executor(DOWNLOAD_EXECUTOR, find_downloaded_file) - if video_path: - return video_path - else: - raise Exception("Файл не был найден после скачивания") + # Сохраняем видео во временный файл + 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}_instagram_video.{video_ext}' + + # Сохраняем файл + video_path = DOWNLOADS_DIR / video_filename + with open(video_path, 'wb') as f: + f.write(video_data) + + logger.info(f"Instagram: видео скачано через внешний сервис: {video_path}") + return str(video_path) + + except httpx.TimeoutException: + last_error = Exception(f"Таймаут при запросе к Instagram сервису (попытка {attempt + 1}/{max_retries})") + logger.warning(f"Instagram: таймаут при запросе к сервису: {last_error}") except Exception as e: last_error = e logger.warning(f"Instagram: попытка {attempt + 1}/{max_retries} не удалась: {e}") - if attempt < max_retries - 1: - await asyncio.sleep((attempt + 1) * 2) - - raise last_error or Exception("Неизвестная ошибка при скачивании с Instagram. Возможно, нужно обновить cookies.") - - -async def update_instagram_cookies_from_browser(browser: str = 'chrome') -> bool: - """ - Автоматически обновляет Instagram cookies из браузера - Returns: True если успешно, False если ошибка - """ - cookies_file = os.getenv('INSTAGRAM_COOKIES_FILE', 'instagram_cookies.txt') - cookies_file_path = Path(cookies_file) - - try: - logger.info(f"Попытка обновления Instagram cookies из браузера {browser}...") - # Пробуем обновить cookies из браузера - loop = asyncio.get_event_loop() - result = await loop.run_in_executor( - None, - lambda: subprocess.run( - [ - 'yt-dlp', - '--cookies-from-browser', browser, - '--cookies', str(cookies_file_path.absolute()), - '--no-download', - 'https://www.instagram.com/' - ], - capture_output=True, - timeout=30, - text=True - ) - ) - - if result.returncode == 0: - logger.info(f"✅ Instagram cookies успешно обновлены из браузера {browser}") - return True - else: - logger.warning(f"Не удалось обновить cookies из {browser}: {result.stderr[:200]}") - return False - - except subprocess.TimeoutExpired: - logger.warning(f"Таймаут при обновлении cookies из {browser}") - return False - except FileNotFoundError: - logger.warning(f"yt-dlp не найден для обновления cookies") - return False - except Exception as e: - logger.error(f"Ошибка при обновлении cookies из браузера: {e}") - return False - - -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 attempt < max_retries - 1: + await asyncio.sleep((attempt + 1) * 2) - 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 - - -async def keep_instagram_session_alive(): - """Поддерживает сессию Instagram активной через периодические запросы и автоматически обновляет cookies""" - cookies_file = os.getenv('INSTAGRAM_COOKIES_FILE', 'instagram_cookies.txt') - cookies_file_path = Path(cookies_file) - AUTO_UPDATE_DAYS_BEFORE_EXPIRY = int(os.getenv('INSTAGRAM_AUTO_UPDATE_DAYS', '3')) # Обновлять за 3 дня до истечения - - if not cookies_file_path.exists(): - logger.info("Instagram cookies не найдены, пропускаем поддержание сессии") - return - - # Список браузеров для попытки обновления (по приоритету) - browsers_to_try = ['chrome', 'firefox', 'edge', 'opera'] - - # Проверяем cookies при старте - is_valid, days_left = check_instagram_cookies_expiry() - if not is_valid: - logger.warning("Instagram cookies истекли! Пытаемся автоматически обновить...") - # Пытаемся обновить из браузера - updated = False - for browser in browsers_to_try: - if await update_instagram_cookies_from_browser(browser): - updated = True - break - - if not updated: - logger.error("Не удалось автоматически обновить cookies! Необходимо обновить вручную.") - return - else: - # Перепроверяем после обновления - is_valid, days_left = check_instagram_cookies_expiry() - - if days_left < AUTO_UPDATE_DAYS_BEFORE_EXPIRY: - logger.warning(f"Instagram cookies истекают через {days_left} дней! Пытаемся автоматически обновить...") - # Пытаемся обновить заранее - for browser in browsers_to_try: - if await update_instagram_cookies_from_browser(browser): - # Перепроверяем - _, days_left = check_instagram_cookies_expiry() - logger.info(f"Cookies обновлены! Новый срок: {days_left} дней") - break - else: - logger.info(f"Instagram cookies действительны еще {days_left} дней") - - # Интервал проверки: 24 часа (86400 секунд) - check_interval = 86400 - - while True: - try: - await asyncio.sleep(check_interval) - - # Проверяем срок действия перед каждым запросом - is_valid, days_left = check_instagram_cookies_expiry() - - # Автоматическое обновление за N дней до истечения - if days_left < AUTO_UPDATE_DAYS_BEFORE_EXPIRY and days_left > 0: - logger.warning(f"Instagram cookies истекают через {days_left} дней. Автоматическое обновление...") - updated = False - for browser in browsers_to_try: - if await update_instagram_cookies_from_browser(browser): - updated = True - _, days_left = check_instagram_cookies_expiry() - logger.info(f"✅ Cookies обновлены автоматически! Новый срок: {days_left} дней") - break - - if not updated: - logger.warning("Не удалось автоматически обновить cookies. Попробуйте обновить вручную.") - - if not is_valid: - logger.error("Instagram cookies истекли! Пытаемся автоматически обновить...") - updated = False - for browser in browsers_to_try: - if await update_instagram_cookies_from_browser(browser): - updated = True - is_valid, days_left = check_instagram_cookies_expiry() - break - - if not updated: - logger.error("Не удалось автоматически обновить cookies! Остановка поддержания сессии.") - break - - # Делаем легкий запрос к Instagram для поддержания активности - logger.info("Поддерживаем активность сессии Instagram...") - try: - ydl_opts = { - 'cookiefile': str(cookies_file_path.absolute()), - 'quiet': True, - 'no_warnings': True, - 'socket_timeout': 10, - } - - def extract_info_sync(): - with yt_dlp.YoutubeDL(ydl_opts) as ydl: - return ydl.extract_info('https://www.instagram.com/', download=False) - - loop = asyncio.get_event_loop() - await loop.run_in_executor(DOWNLOAD_EXECUTOR, extract_info_sync) - - logger.info(f"Сессия Instagram успешно обновлена. Cookies действительны еще {days_left} дней") - except Exception as e: - logger.warning(f"Не удалось обновить сессию Instagram: {e}") - # Продолжаем работу, попробуем в следующий раз - - except asyncio.CancelledError: - logger.info("Поддержание сессии Instagram остановлено") - break - except Exception as e: - logger.error(f"Ошибка в задаче поддержания сессии Instagram: {e}") - # Ждем перед следующей попыткой - await asyncio.sleep(3600) # 1 час + raise last_error or Exception("Неизвестная ошибка при скачивании с Instagram через внешний сервис") async def download_vk_video(url: str, chat_id: int, max_retries: int = 3) -> str: @@ -771,12 +453,8 @@ async def handle_message(update: Update, context: ContextTypes.DEFAULT_TYPE): # Увеличиваем счетчик скачанных видео increment_downloads() - # Удаляем временный файл - try: - os.remove(video_path) - logger.info(f"Удален временный файл: {video_path}") - except Exception as e: - logger.warning(f"Не удалось удалить файл {video_path}: {e}") + # Сохраняем видео в папку video (не удаляем) + logger.info(f"Видео сохранено: {video_path}") # Удаляем статусное сообщение и исходное сообщение со ссылкой try: @@ -853,9 +531,9 @@ def main(): # Инициализируем базу данных init_database() - # Очищаем старые файлы при старте - logger.info("Очистка старых файлов при старте...") - cleanup_old_files(max_age_hours=1) # Удаляем файлы старше 1 часа + # Очищаем .part файлы при старте + logger.info("Очистка .part файлов при старте...") + cleanup_old_files() # Удаляем только недокачанные .part файлы # Создаем приложение application = Application.builder().token(TELEGRAM_BOT_TOKEN).build() @@ -865,19 +543,15 @@ def main(): application.add_handler(CommandHandler("start", start_command)) application.add_handler(CommandHandler("stat", stat_command)) - # Запускаем фоновую задачу для поддержания сессии Instagram + # Запускаем периодическую очистку файлов async def post_init(application: Application): """Выполняется после инициализации приложения""" - # Запускаем задачу поддержания сессии Instagram в фоне - asyncio.create_task(keep_instagram_session_alive()) - logger.info("Фоновая задача поддержания сессии Instagram запущена") - - # Запускаем периодическую очистку файлов (каждые 6 часов) + # Запускаем периодическую очистку .part файлов (каждые 6 часов) async def periodic_cleanup(): while True: await asyncio.sleep(6 * 3600) # 6 часов - cleanup_old_files(max_age_hours=1) - logger.info("Периодическая очистка старых файлов выполнена") + cleanup_old_files() + logger.info("Периодическая очистка .part файлов выполнена") asyncio.create_task(periodic_cleanup()) logger.info("Фоновая задача периодической очистки файлов запущена") diff --git a/docker-compose.yml b/docker-compose.yml index 03c1b14..214a309 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -8,14 +8,10 @@ services: environment: - TELEGRAM_BOT_TOKEN=${TELEGRAM_BOT_TOKEN} - TELEGRAM_BOT_USERNAME=${TELEGRAM_BOT_USERNAME} + - YOUTUBE_DOWNLOADER_URL=${YOUTUBE_DOWNLOADER_URL} + - INSTAGRAM_DOWNLOADER_URL=${INSTAGRAM_DOWNLOADER_URL} - VK_DOWNLOADER_URL=${VK_DOWNLOADER_URL} volumes: - ./video:/app/video - - ./instagram_cookies.txt:/app/instagram_cookies.txt - ./data:/app/data:Z network_mode: host - -networks: - bot_network: - driver: bridge - diff --git a/get_instagram_cookies.sh b/get_instagram_cookies.sh deleted file mode 100755 index c393a1b..0000000 --- a/get_instagram_cookies.sh +++ /dev/null @@ -1,24 +0,0 @@ -#!/bin/bash -# Скрипт для получения cookies Instagram через yt-dlp - -echo "Получение cookies Instagram из браузера..." -echo "" -echo "Выберите браузер:" -echo "1) Chrome" -echo "2) Firefox" -read -p "Введите номер (1 или 2): " browser - -if [ "$browser" = "1" ]; then - BROWSER="chrome" -elif [ "$browser" = "2" ]; then - BROWSER="firefox" -else - echo "Неверный выбор" - exit 1 -fi - -echo "Получаю cookies из $BROWSER..." -docker compose exec bot yt-dlp --cookies-from-browser $BROWSER --cookies /app/instagram_cookies.txt https://www.instagram.com 2>&1 | head -5 - -echo "" -echo "Cookies должны быть сохранены в instagram_cookies.txt" diff --git a/instagram-downloader/Dockerfile b/instagram-downloader/Dockerfile new file mode 100644 index 0000000..760c32a --- /dev/null +++ b/instagram-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/INSTAGRAM_COOKIES_INSTRUCTIONS.md b/instagram-downloader/INSTAGRAM_COOKIES_INSTRUCTIONS.md similarity index 100% rename from INSTAGRAM_COOKIES_INSTRUCTIONS.md rename to instagram-downloader/INSTAGRAM_COOKIES_INSTRUCTIONS.md diff --git a/instagram-downloader/README.md b/instagram-downloader/README.md new file mode 100644 index 0000000..27d2da7 --- /dev/null +++ b/instagram-downloader/README.md @@ -0,0 +1,144 @@ +# Instagram Downloader Service + +Микросервис для скачивания видео с Instagram. + +## Требования + +- Docker и Docker Compose +- Файл с cookies Instagram (`instagram_cookies.txt` в папке `instagram-downloader/`) + +## Быстрый старт + +### 1. Настройка cookies + +Перед запуском сервиса необходимо получить cookies Instagram. Есть несколько способов: + +#### Способ 1: Через скрипт (рекомендуется) + +```bash +cd instagram-downloader +./get_instagram_cookies.sh +``` + +Скрипт попросит выбрать браузер и автоматически извлечет cookies. + +#### Способ 2: Обновление существующих cookies + +```bash +cd instagram-downloader +./update_instagram_cookies.sh +``` + +#### Способ 3: Вручную + +См. подробные инструкции в `INSTAGRAM_COOKIES_INSTRUCTIONS.md` + +### 2. Запуск сервиса + +#### Вариант 1: Через корневой docker-compose (рекомендуется) + +```bash +cd .. # вернуться в корень проекта +docker compose up -d instagram-downloader +``` + +#### Вариант 2: Отдельно + +```bash +docker compose up -d +``` + +### 3. Проверка работы + +```bash +# Проверка здоровья сервиса +curl http://localhost:5556/health + +# Должен вернуть: {"status":"ok","service":"instagram-downloader"} +``` + +## API Endpoints + +### GET /health + +Проверка здоровья сервиса. + +**Ответ:** +```json +{ + "status": "ok", + "service": "instagram-downloader" +} +``` + +### POST /download/stream + +Скачивание видео с Instagram. + +**Запрос:** +```json +{ + "url": "https://www.instagram.com/p/..." +} +``` + +**Ответ:** +- Успех: бинарные данные видео (Content-Type: video/mp4) +- Ошибка: JSON с описанием ошибки + +## Порты + +- Внешний порт: **5556** +- Внутренний порт контейнера: **5000** + +## Обновление cookies + +Cookies Instagram имеют ограниченный срок действия. Рекомендуется обновлять их раз в несколько недель. + +Для обновления: + +```bash +cd instagram-downloader +./update_instagram_cookies.sh +``` + +После обновления перезапустите сервис: + +```bash +docker compose restart instagram-downloader +``` + +## Troubleshooting + +### Сервис не может скачать видео + +1. Проверьте наличие файла `instagram_cookies.txt` в папке `instagram-downloader/` +2. Проверьте срок действия cookies (они могут истечь) +3. Обновите cookies через скрипт `update_instagram_cookies.sh` +4. Проверьте логи: `docker compose logs instagram-downloader` + +### Cookies истекли + +Если видите ошибку "Instagram cookies истекли", выполните: + +```bash +cd instagram-downloader +./update_instagram_cookies.sh +``` + +Затем перезапустите сервис. + +## Структура файлов + +``` +instagram-downloader/ +├── app.py # Основной код сервиса +├── Dockerfile # Образ Docker +├── docker-compose.yml # Конфигурация для отдельного запуска +├── requirements.txt # Python зависимости +├── get_instagram_cookies.sh # Скрипт для получения cookies +├── update_instagram_cookies.sh # Скрипт для обновления cookies +├── INSTAGRAM_COOKIES_INSTRUCTIONS.md # Подробные инструкции по cookies +└── README.md # Этот файл +``` + diff --git a/instagram-downloader/app.py b/instagram-downloader/app.py new file mode 100644 index 0000000..90474bc --- /dev/null +++ b/instagram-downloader/app.py @@ -0,0 +1,244 @@ +""" +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) + diff --git a/instagram-downloader/docker-compose.yml b/instagram-downloader/docker-compose.yml new file mode 100644 index 0000000..43e91c7 --- /dev/null +++ b/instagram-downloader/docker-compose.yml @@ -0,0 +1,19 @@ +services: + instagram-downloader: + build: . + container_name: instagram_downloader_service + restart: unless-stopped + ports: + - "5556:5000" + volumes: + - ./downloads:/app/downloads + - ./instagram_cookies.txt:/app/instagram_cookies.txt + environment: + - INSTAGRAM_COOKIES_FILE=/app/instagram_cookies.txt + networks: + - downloader_network + +networks: + downloader_network: + driver: bridge + diff --git a/instagram-downloader/downloads/042e64da-259b-49c3-b3ba-a501d37c6966_Video by nowness.mp4 b/instagram-downloader/downloads/042e64da-259b-49c3-b3ba-a501d37c6966_Video by nowness.mp4 new file mode 100644 index 0000000..94ca537 Binary files /dev/null and b/instagram-downloader/downloads/042e64da-259b-49c3-b3ba-a501d37c6966_Video by nowness.mp4 differ diff --git a/instagram-downloader/downloads/13417acb-22e4-4230-9ccc-d573cf82c838_Video by nowness.mp4 b/instagram-downloader/downloads/13417acb-22e4-4230-9ccc-d573cf82c838_Video by nowness.mp4 new file mode 100644 index 0000000..94ca537 Binary files /dev/null and b/instagram-downloader/downloads/13417acb-22e4-4230-9ccc-d573cf82c838_Video by nowness.mp4 differ diff --git a/instagram-downloader/downloads/190af748-d84f-4090-af45-2da38a219d5c_Video by nowness.mp4 b/instagram-downloader/downloads/190af748-d84f-4090-af45-2da38a219d5c_Video by nowness.mp4 new file mode 100644 index 0000000..94ca537 Binary files /dev/null and b/instagram-downloader/downloads/190af748-d84f-4090-af45-2da38a219d5c_Video by nowness.mp4 differ diff --git a/instagram-downloader/downloads/3c4a2edf-20f9-4dbd-9d82-4424c4880dcb_Video by nowness.mp4 b/instagram-downloader/downloads/3c4a2edf-20f9-4dbd-9d82-4424c4880dcb_Video by nowness.mp4 new file mode 100644 index 0000000..94ca537 Binary files /dev/null and b/instagram-downloader/downloads/3c4a2edf-20f9-4dbd-9d82-4424c4880dcb_Video by nowness.mp4 differ diff --git a/instagram-downloader/downloads/48a8cc89-a03e-4a77-9eee-2561b16b124e_Video by nowness.mp4 b/instagram-downloader/downloads/48a8cc89-a03e-4a77-9eee-2561b16b124e_Video by nowness.mp4 new file mode 100644 index 0000000..94ca537 Binary files /dev/null and b/instagram-downloader/downloads/48a8cc89-a03e-4a77-9eee-2561b16b124e_Video by nowness.mp4 differ diff --git a/instagram-downloader/downloads/5785a12c-8282-408b-951e-8bb0ce2841a6_Video by nowness.mp4 b/instagram-downloader/downloads/5785a12c-8282-408b-951e-8bb0ce2841a6_Video by nowness.mp4 new file mode 100644 index 0000000..94ca537 Binary files /dev/null and b/instagram-downloader/downloads/5785a12c-8282-408b-951e-8bb0ce2841a6_Video by nowness.mp4 differ diff --git a/instagram-downloader/downloads/59bf3903-481a-48dc-9a7c-18ae91da4cc3_Video by nowness.mp4 b/instagram-downloader/downloads/59bf3903-481a-48dc-9a7c-18ae91da4cc3_Video by nowness.mp4 new file mode 100644 index 0000000..94ca537 Binary files /dev/null and b/instagram-downloader/downloads/59bf3903-481a-48dc-9a7c-18ae91da4cc3_Video by nowness.mp4 differ diff --git a/instagram-downloader/downloads/72b87ce3-e40a-4e14-afb7-d17a57456ff9_Video by nowness.mp4 b/instagram-downloader/downloads/72b87ce3-e40a-4e14-afb7-d17a57456ff9_Video by nowness.mp4 new file mode 100644 index 0000000..94ca537 Binary files /dev/null and b/instagram-downloader/downloads/72b87ce3-e40a-4e14-afb7-d17a57456ff9_Video by nowness.mp4 differ diff --git a/instagram-downloader/downloads/764bf4c9-7de8-41e9-a284-b4e7ca60e9b3_Video by nowness.mp4 b/instagram-downloader/downloads/764bf4c9-7de8-41e9-a284-b4e7ca60e9b3_Video by nowness.mp4 new file mode 100644 index 0000000..94ca537 Binary files /dev/null and b/instagram-downloader/downloads/764bf4c9-7de8-41e9-a284-b4e7ca60e9b3_Video by nowness.mp4 differ diff --git a/instagram-downloader/downloads/9cbc9b14-e4a9-4e04-b2d4-282e920b149c_Video by nowness.mp4 b/instagram-downloader/downloads/9cbc9b14-e4a9-4e04-b2d4-282e920b149c_Video by nowness.mp4 new file mode 100644 index 0000000..94ca537 Binary files /dev/null and b/instagram-downloader/downloads/9cbc9b14-e4a9-4e04-b2d4-282e920b149c_Video by nowness.mp4 differ diff --git a/instagram-downloader/downloads/a1143b8c-0921-4f92-a98a-7eadd6a18769_Video by nowness.mp4 b/instagram-downloader/downloads/a1143b8c-0921-4f92-a98a-7eadd6a18769_Video by nowness.mp4 new file mode 100644 index 0000000..94ca537 Binary files /dev/null and b/instagram-downloader/downloads/a1143b8c-0921-4f92-a98a-7eadd6a18769_Video by nowness.mp4 differ diff --git a/instagram-downloader/downloads/a210227b-eebf-4d09-9653-9ca9191f8bd2_Video by nowness.mp4 b/instagram-downloader/downloads/a210227b-eebf-4d09-9653-9ca9191f8bd2_Video by nowness.mp4 new file mode 100644 index 0000000..94ca537 Binary files /dev/null and b/instagram-downloader/downloads/a210227b-eebf-4d09-9653-9ca9191f8bd2_Video by nowness.mp4 differ diff --git a/instagram-downloader/downloads/b7488005-23b1-4192-a049-4388308ede59_Video by nowness.mp4 b/instagram-downloader/downloads/b7488005-23b1-4192-a049-4388308ede59_Video by nowness.mp4 new file mode 100644 index 0000000..94ca537 Binary files /dev/null and b/instagram-downloader/downloads/b7488005-23b1-4192-a049-4388308ede59_Video by nowness.mp4 differ diff --git a/instagram-downloader/downloads/c662de78-f881-4ad9-ac7d-cb5a676379f8_Video by nowness.mp4 b/instagram-downloader/downloads/c662de78-f881-4ad9-ac7d-cb5a676379f8_Video by nowness.mp4 new file mode 100644 index 0000000..94ca537 Binary files /dev/null and b/instagram-downloader/downloads/c662de78-f881-4ad9-ac7d-cb5a676379f8_Video by nowness.mp4 differ diff --git a/instagram-downloader/downloads/db640d83-6a01-4a20-80f8-dd1900553409_Video by nowness.mp4 b/instagram-downloader/downloads/db640d83-6a01-4a20-80f8-dd1900553409_Video by nowness.mp4 new file mode 100644 index 0000000..94ca537 Binary files /dev/null and b/instagram-downloader/downloads/db640d83-6a01-4a20-80f8-dd1900553409_Video by nowness.mp4 differ diff --git a/instagram-downloader/downloads/e38550ee-c4ce-4f99-ba4e-32c5cf3d2dfb_Video by nowness.mp4 b/instagram-downloader/downloads/e38550ee-c4ce-4f99-ba4e-32c5cf3d2dfb_Video by nowness.mp4 new file mode 100644 index 0000000..94ca537 Binary files /dev/null and b/instagram-downloader/downloads/e38550ee-c4ce-4f99-ba4e-32c5cf3d2dfb_Video by nowness.mp4 differ diff --git a/instagram-downloader/downloads/f9b65585-6b70-41dc-b9b2-a0051529314b_Video by nowness.mp4 b/instagram-downloader/downloads/f9b65585-6b70-41dc-b9b2-a0051529314b_Video by nowness.mp4 new file mode 100644 index 0000000..94ca537 Binary files /dev/null and b/instagram-downloader/downloads/f9b65585-6b70-41dc-b9b2-a0051529314b_Video by nowness.mp4 differ diff --git a/instagram-downloader/downloads/fbaa51c4-0626-4a0c-9f92-61a77d7ed05a_Video by nowness.mp4 b/instagram-downloader/downloads/fbaa51c4-0626-4a0c-9f92-61a77d7ed05a_Video by nowness.mp4 new file mode 100644 index 0000000..94ca537 Binary files /dev/null and b/instagram-downloader/downloads/fbaa51c4-0626-4a0c-9f92-61a77d7ed05a_Video by nowness.mp4 differ diff --git a/instagram-downloader/get_instagram_cookies.sh b/instagram-downloader/get_instagram_cookies.sh new file mode 100755 index 0000000..8c76af1 --- /dev/null +++ b/instagram-downloader/get_instagram_cookies.sh @@ -0,0 +1,39 @@ +#!/bin/bash +# Скрипт для получения cookies Instagram через yt-dlp + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +COOKIES_FILE="$SCRIPT_DIR/instagram_cookies.txt" + +echo "Получение cookies Instagram из браузера..." +echo "" +echo "Выберите браузер:" +echo "1) Chrome" +echo "2) Firefox" +echo "3) Edge" +echo "4) Opera" +read -p "Введите номер (1-4): " browser + +case "$browser" in + 1) BROWSER="chrome" ;; + 2) BROWSER="firefox" ;; + 3) BROWSER="edge" ;; + 4) BROWSER="opera" ;; + *) + echo "Неверный выбор" + exit 1 + ;; +esac + +echo "Получаю cookies из $BROWSER..." +echo "Файл cookies будет сохранен в: $COOKIES_FILE" + +yt-dlp --cookies-from-browser "$BROWSER" --cookies "$COOKIES_FILE" --no-download https://www.instagram.com 2>&1 | head -10 + +if [ -f "$COOKIES_FILE" ]; then + echo "" + echo "✅ Cookies успешно сохранены в $COOKIES_FILE" +else + echo "" + echo "❌ Ошибка: файл cookies не был создан" + exit 1 +fi diff --git a/instagram_cookies.txt b/instagram-downloader/instagram_cookies.txt similarity index 86% rename from instagram_cookies.txt rename to instagram-downloader/instagram_cookies.txt index 721cba7..9fc6ff6 100644 --- a/instagram_cookies.txt +++ b/instagram-downloader/instagram_cookies.txt @@ -39,13 +39,16 @@ rusoska.com FALSE / FALSE 1799795493 userToken a01e24c3-c94f-4a4e-b11b-751b72046 .mozilla.org TRUE / FALSE 1799925684 _ga_B9CY1C9VBC GS2.1.s1765365263$o1$g1$t1765365684$j60$l0$h0 .mozilla.org TRUE / FALSE 1799925263 _ga GA1.2.1451822324.1765365263 .mozilla.org TRUE / FALSE 1765451663 _gid GA1.2.878207985.1765365263 -.instagram.com TRUE / TRUE 1799949717 csrftoken CnChQ6nTz8cfm_U7q2ur9w +.instagram.com TRUE / TRUE 1799964275 csrftoken CnChQ6nTz8cfm_U7q2ur9w .instagram.com TRUE / TRUE 1799925292 datr LFY5aVDEvvzQRTypNm_NZ0d3 .instagram.com TRUE / TRUE 1796901312 ig_did B0879634-89D6-4098-9B3E-958B6BC00183 .instagram.com TRUE / TRUE 1765970112 dpr 2 .instagram.com TRUE / TRUE 1799925293 mid aTlWLAAEAAEBRoS_PfrA_i5UP0w1 -.instagram.com TRUE / TRUE 1765980504 wd 1920x944 -.instagram.com TRUE / TRUE 1796911697 sessionid 42059678244%3AD0GdfKmaFZWqXp%3A10%3AAYieDJrvoWIE9WW--tzjgv-3EyrgI9XT6seopSdHFw -.instagram.com TRUE / TRUE 1773165717 ds_user_id 42059678244 -.instagram.com TRUE / TRUE 0 rur "LDC\05442059678244\0541796925717:01fef99bf0a6a2eec6207d44260971856a393246d5f7590afa05e8b7183b7b873f243693" +.instagram.com TRUE / TRUE 1766008776 wd 1920x944 +.instagram.com TRUE / TRUE 1796939890 sessionid 42059678244%3AD0GdfKmaFZWqXp%3A10%3AAYgpCODjycI3EWMR6G5Uh6kXjroGZ6pb1IRJmXGX3g +.instagram.com TRUE / TRUE 1773180275 ds_user_id 42059678244 +.instagram.com TRUE / TRUE 0 rur "LDC\05442059678244\0541796940275:01fef3bd7d6beb547023be3c45c01ebfd5726050a4cced10a3a4af9ff39a731103df7c88" addons.mozilla.org FALSE / TRUE 0 taarId 4dffa50e49cca797bb48f2f4f11803c251746ad45af1fef3ba1ad37379a24fea +.facebook.com TRUE / TRUE 1799963979 datr S-05aRMEAJEaLLwYCMb4y3JM +.facebook.com TRUE / TRUE 1766008781 wd 1920x944 +.facebook.com TRUE / TRUE 1766008795 dpr 2 diff --git a/instagram-downloader/requirements.txt b/instagram-downloader/requirements.txt new file mode 100644 index 0000000..3fc2993 --- /dev/null +++ b/instagram-downloader/requirements.txt @@ -0,0 +1,4 @@ +Flask==3.0.0 +flask-cors==4.0.0 +yt-dlp>=2024.12.13 + diff --git a/instagram-downloader/update_instagram_cookies.sh b/instagram-downloader/update_instagram_cookies.sh new file mode 100755 index 0000000..2f1a0ee --- /dev/null +++ b/instagram-downloader/update_instagram_cookies.sh @@ -0,0 +1,47 @@ +#!/bin/bash +# Обновление cookies Instagram через браузер + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +COOKIES_FILE="$SCRIPT_DIR/instagram_cookies.txt" + +echo "Обновление cookies Instagram..." +echo "" +echo "Выберите браузер:" +echo "1) Chrome" +echo "2) Firefox" +echo "3) Edge" +echo "4) Opera" +read -p "Введите номер (1-4): " browser + +case "$browser" in + 1) BROWSER="chrome" ;; + 2) BROWSER="firefox" ;; + 3) BROWSER="edge" ;; + 4) BROWSER="opera" ;; + *) + echo "Неверный выбор" + exit 1 + ;; +esac + +echo "" +echo "ВАЖНО: Перед обновлением cookies убедитесь, что вы авторизованы в Instagram в выбранном браузере!" +echo "" +read -p "Нажмите Enter для продолжения или Ctrl+C для отмены..." + +echo "Обновляю cookies из $BROWSER..." +echo "Файл cookies будет сохранен в: $COOKIES_FILE" + +yt-dlp --cookies-from-browser "$BROWSER" --cookies "$COOKIES_FILE" --no-download https://www.instagram.com 2>&1 | head -10 + +if [ -f "$COOKIES_FILE" ]; then + echo "" + echo "✅ Cookies успешно обновлены в $COOKIES_FILE" + echo "" + echo "Для применения изменений перезапустите instagram-downloader сервис:" + echo " cd $(cd "$SCRIPT_DIR/.." && pwd) && docker compose restart instagram-downloader" +else + echo "" + echo "❌ Ошибка: файл cookies не был создан" + exit 1 +fi diff --git a/requirements.txt b/requirements.txt index 166dce9..ac38c7e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,3 @@ python-telegram-bot==20.7 -yt-dlp>=2024.12.13 -requests==2.31.0 httpx==0.25.2 diff --git a/update_instagram_cookies.sh b/update_instagram_cookies.sh deleted file mode 100755 index 776cc4e..0000000 --- a/update_instagram_cookies.sh +++ /dev/null @@ -1,22 +0,0 @@ -#!/bin/bash -# Обновление cookies Instagram с использованием логина/пароля - -LOGIN="vrubelroman@gmail.com" -PASSWORD="VRKshtein07" - -echo "Обновление cookies Instagram..." -echo "Логин: $LOGIN" - -# Используем yt-dlp для обновления cookies через браузер -# Но сначала нужно зайти в браузер вручную, так как yt-dlp не поддерживает прямую авторизацию - -echo "" -echo "ВАЖНО: yt-dlp не поддерживает прямую авторизацию через логин/пароль." -echo "Нужно:" -echo "1. Откройте Instagram в браузере: https://www.instagram.com" -echo "2. Войдите с вашими учетными данными" -echo "3. Затем выполните:" -echo "" -echo " yt-dlp --cookies-from-browser chrome --cookies ./instagram_cookies.txt --no-download https://www.instagram.com" -echo "" -echo "Или используйте расширение браузера для экспорта cookies." diff --git a/youtube-downloader/Dockerfile b/youtube-downloader/Dockerfile new file mode 100644 index 0000000..760c32a --- /dev/null +++ b/youtube-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/youtube-downloader/app.py b/youtube-downloader/app.py new file mode 100644 index 0000000..1ad1a37 --- /dev/null +++ b/youtube-downloader/app.py @@ -0,0 +1,172 @@ +""" +YouTube Video Downloader Service +Отдельный микросервис для скачивания видео с YouTube +""" +import os +import logging +from pathlib import Path +from flask import Flask, request, jsonify +from flask_cors import CORS +import yt_dlp +import uuid +import re + +# Настройка логирования +logging.basicConfig( + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', + level=logging.INFO +) +logger = logging.getLogger(__name__) + +app = Flask(__name__) +CORS(app) # Разрешаем CORS для взаимодействия с основным ботом + +# Директория для временных файлов +DOWNLOADS_DIR = Path('downloads') +DOWNLOADS_DIR.mkdir(exist_ok=True) + + +def _safe_filename(title: str) -> str: + """Создает безопасное имя файла""" + safe_title = re.sub(r'[<>:"/\\|?*]', '', title)[:100] + return str(DOWNLOADS_DIR / f'{uuid.uuid4()}_{safe_title}.%(ext)s') + + +def download_youtube_video(url: str, max_retries: int = 3) -> Path: + """Скачивает видео с YouTube""" + user_agent = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36' + + last_error = None + for attempt in range(max_retries): + try: + # Получаем информацию о видео + ydl_opts_info = { + 'quiet': False, + 'no_warnings': False, + 'user_agent': user_agent, + 'socket_timeout': 30, + 'extractor_args': { + 'youtube': { + 'player_client': ['android', 'web'], + 'player_skip': ['webpage'], + }, + }, + 'http_headers': { + 'User-Agent': user_agent, + 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', + 'Accept-Language': 'en-us,en;q=0.5', + 'Accept-Encoding': 'gzip, deflate', + 'Connection': 'keep-alive', + }, + } + + with yt_dlp.YoutubeDL(ydl_opts_info) as ydl: + info = ydl.extract_info(url, download=False) + video_title = info.get('title', 'video') + logger.info(f"YouTube: получена информация о видео: {video_title}") + + # Скачиваем видео + ydl_opts_download = { + 'format': 'bestvideo[ext=mp4]+bestaudio[ext=m4a]/best[ext=mp4]/best', + 'outtmpl': _safe_filename(video_title), + 'quiet': False, + 'no_warnings': False, + 'user_agent': user_agent, + 'socket_timeout': 30, + 'extractor_args': { + 'youtube': { + 'player_client': ['android', 'web'], + 'player_skip': ['webpage'], + }, + }, + 'http_headers': { + 'User-Agent': user_agent, + 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', + 'Accept-Language': 'en-us,en;q=0.5', + 'Accept-Encoding': 'gzip, deflate', + 'Connection': 'keep-alive', + }, + } + + logger.info(f"YouTube: начинаем скачивание (попытка {attempt + 1}/{max_retries})") + with yt_dlp.YoutubeDL(ydl_opts_download) as ydl: + ydl.download([url]) + + # Находим скачанный файл + downloaded_files = list(DOWNLOADS_DIR.glob('*')) + if downloaded_files: + downloaded_files.sort(key=lambda x: x.stat().st_mtime, reverse=True) + return downloaded_files[0] + else: + raise Exception("Файл не был найден после скачивания") + + except Exception as e: + last_error = e + logger.warning(f"YouTube: попытка {attempt + 1}/{max_retries} не удалась: {e}") + if attempt < max_retries - 1: + import time + time.sleep((attempt + 1) * 2) + + raise last_error or Exception("Неизвестная ошибка при скачивании с YouTube") + + +@app.route('/health', methods=['GET']) +def health(): + """Health check endpoint""" + return jsonify({'status': 'ok', 'service': 'youtube-downloader'}), 200 + + +@app.route('/download/stream', methods=['POST']) +def download_stream(): + """Скачивает видео с YouTube и возвращает бинарные данные""" + try: + data = request.get_json() + if not data or 'url' not in data: + return jsonify({'error': 'URL is required'}), 400 + + url = data['url'] + logger.info(f"Получен запрос на скачивание (stream): {url}") + + # Проверяем, что это YouTube URL + if 'youtube.com' not in url and 'youtu.be' not in url: + return jsonify({'error': 'Only YouTube URLs are supported'}), 400 + + # Скачиваем видео + video_path = download_youtube_video(url) + logger.info(f"Видео скачано: {video_path}") + + # Читаем файл и отправляем + with open(video_path, 'rb') as f: + video_data = f.read() + + # Безопасное имя файла без кириллицы для заголовка + safe_filename = video_path.name.encode('ascii', 'ignore').decode('ascii') or 'youtube_video.mp4' + if not safe_filename.endswith(('.mp4', '.webm', '.mkv')): + safe_filename = 'youtube_video.mp4' + + # Определяем content-type + content_type = 'video/mp4' + if video_path.suffix == '.webm': + content_type = 'video/webm' + elif video_path.suffix == '.mkv': + content_type = 'video/x-matroska' + + # Удаляем временный файл + video_path.unlink() + + return video_data, 200, { + 'Content-Type': content_type, + 'Content-Disposition': f'attachment; filename="{safe_filename}"' + } + + except Exception as e: + 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"Запуск YouTube Downloader сервиса на {host}:{port}") + app.run(host=host, port=port, debug=False) + diff --git a/youtube-downloader/docker-compose.yml b/youtube-downloader/docker-compose.yml new file mode 100644 index 0000000..11cdb8a --- /dev/null +++ b/youtube-downloader/docker-compose.yml @@ -0,0 +1,16 @@ +services: + youtube-downloader: + build: . + container_name: youtube_downloader_service + restart: unless-stopped + ports: + - "5557:5000" + volumes: + - ./downloads:/app/downloads + networks: + - downloader_network + +networks: + downloader_network: + driver: bridge + diff --git a/youtube-downloader/requirements.txt b/youtube-downloader/requirements.txt new file mode 100644 index 0000000..3fc2993 --- /dev/null +++ b/youtube-downloader/requirements.txt @@ -0,0 +1,4 @@ +Flask==3.0.0 +flask-cors==4.0.0 +yt-dlp>=2024.12.13 +