Рефакторинг: микросервисная архитектура

- Разделение на микросервисы: youtube-downloader, instagram-downloader, vk-downloader
- Основной бот в корне проекта, работает через HTTP API с сервисами
- Каждый сервис запускается отдельно в своей папке
- Видео сохраняются в папке video/ и не удаляются
- Обновлена документация и архитектура
- Скрипты для Instagram cookies перенесены в instagram-downloader/
This commit is contained in:
vrubelroman 2025-12-11 01:07:04 +03:00
parent 8024eea868
commit 436e0cd541
41 changed files with 1348 additions and 693 deletions

View file

@ -1,11 +1,31 @@
__pycache__ # Исключаем папки сервисов
*.pyc youtube-downloader/
*.pyo instagram-downloader/
*.pyd vk-downloader/
.Python
downloads/
*.log
.git
.gitignore
README.md
# Исключаем данные и временные файлы
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/

View file

@ -1,13 +1,15 @@
# Токен Telegram бота (получить у @BotFather) # Telegram Bot Configuration
TELEGRAM_BOT_TOKEN=your_telegram_bot_token_here TELEGRAM_BOT_TOKEN=your_telegram_bot_token_here
TELEGRAM_BOT_USERNAME=your_bot_username
# Имя бота (username без @, используется в подписи видео) # Downloader Services URLs
TELEGRAM_BOT_USERNAME=vrubelVideoDownload_bot # Для локальной разработки через docker-compose используются внутренние имена сервисов с портом 5000
# Для продакшена или отдельного запуска сервисов укажите IP адреса или домены с внешними портами
# URL VK сервиса для скачивания видео (должен быть на хосте без VPN) YOUTUBE_DOWNLOADER_URL=http://localhost:5557
# Для локальной разработки: http://localhost:5555 INSTAGRAM_DOWNLOADER_URL=http://localhost:5556
# Для продакшена: http://<ip_хоста_с_vk_сервисом>:5555
VK_DOWNLOADER_URL=http://localhost:5555 VK_DOWNLOADER_URL=http://localhost:5555
# Количество дней до истечения cookies, когда начинать автоматическое обновление (по умолчанию: 3) # Примечание: Если используете docker-compose из корня проекта, можно использовать:
INSTAGRAM_AUTO_UPDATE_DAYS=3 # YOUTUBE_DOWNLOADER_URL=http://youtube-downloader:5000
# INSTAGRAM_DOWNLOADER_URL=http://instagram-downloader:5000
# VK_DOWNLOADER_URL=http://vk-downloader:5000

View file

@ -4,10 +4,12 @@
## Общая архитектура ## Общая архитектура
Система состоит из двух основных компонентов: Система состоит из микросервисной архитектуры с раздельными сервисами для каждого источника видео:
1. **Основной бот** (`bot.py`) — Telegram бот, обрабатывающий запросы пользователей 1. **Основной бот** (`bot.py` в корне проекта) — Telegram бот, обрабатывающий запросы пользователей и оркестрирующий запросы к сервисам
2. **VK Downloader Service** (`vk-downloader/`) — отдельный микросервис для скачивания видео с VK 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) │ │ Основной бот (bot.py) │
│ ┌───────────────────────────────┐ │ │ ┌───────────────────────────────┐ │
│ │ YouTube Download Handler │ │ │ │ Message Handler │ │
│ │ - URL extraction │ │
│ │ - Source detection │ │
│ └───────────────────────────────┘ │ │ └───────────────────────────────┘ │
│ ┌───────────────────────────────┐ │ │ ┌───────────────────────────────┐ │
│ │ Instagram Download Handler │ │ │ │ HTTP API Clients │──┼──┐
└───────────────────────────────┘ │ - YouTube API Client │ │
┌───────────────────────────────┐ │ - Instagram API Client │ │
│ │ VK API Client │──┼──┐ │ │ - VK API Client │ │ │
│ └───────────────────────────────┘ │ │ │ └───────────────────────────────┘ │ │
│ ┌───────────────────────────────┐ │ │ │ ┌───────────────────────────────┐ │ │
│ │ SQLite Database │ │ │ │ │ SQLite Database │ │ │
│ │ - Users │ │ │ │ │ - Users │ │ │
│ │ - Stats │ │ │ │ │ - Stats │ │ │
│ └───────────────────────────────┘ │ │ │ └───────────────────────────────┘ │ │
│ ┌───────────────────────────────┐ │ │
│ │ Video Storage │ │ │
│ │ - video/ directory │ │ │
│ └───────────────────────────────┘ │ │
└─────────────────────────────────────┘ │ └─────────────────────────────────────┘ │
│ HTTP API │ HTTP API
│ POST /download/stream │ POST /download/stream
┌──────────────────────┐ ┌──────────────────────────────────┼──────────────────┐
│ VK Downloader │ │ │ │
│ Service │ ▼ ▼ ▼
│ ┌────────────────┐ │ ┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐
│ │ Flask API │ │ │ YouTube Downloader│ │Instagram Download│ │ VK Downloader │
│ └────────────────┘ │ │ Service │ │ Service │ │ Service │
│ ┌────────────────┐ │ │ ┌──────────────┐ │ │ ┌──────────────┐ │ │ ┌──────────────┐│
│ │ yt-dlp │ │ │ │ Flask API │ │ │ │ Flask API │ │ │ │ Flask API ││
│ │ (VK only) │ │ │ └──────────────┘ │ │ └──────────────┘ │ │ └──────────────┘│
│ └────────────────┘ │ │ ┌──────────────┐ │ │ ┌──────────────┐ │ │ ┌──────────────┐│
└──────────────────────┘ │ │ yt-dlp │ │ │ │ yt-dlp │ │ │ │ yt-dlp ││
│ │ (YouTube) │ │ │ │ (Instagram) │ │ │ │ (VK only) ││
│ └──────────────┘ │ │ └──────────────┘ │ │ └──────────────┘│
│ Port: 5557 │ │ Port: 5556 │ │ Port: 5555 │
└──────────────────┘ └──────────────────┘ └──────────────────┘
``` ```
## Компоненты системы ## Компоненты системы
### 1. Основной бот (bot.py) ### 1. Основной бот (bot.py)
**Расположение:** Корень проекта
**Технологии:** **Технологии:**
- `python-telegram-bot` (v20.7) — асинхронный фреймворк для Telegram Bot API - `python-telegram-bot` (v20.7) — асинхронный фреймворк для Telegram Bot API
- `yt-dlp` — библиотека для скачивания видео - `httpx` — асинхронный HTTP клиент для запросов к сервисам загрузчиков
- `httpx` — асинхронный HTTP клиент для запросов к VK сервису
- `sqlite3` — база данных для хранения статистики - `sqlite3` — база данных для хранения статистики
**Архитектурные решения:** **Архитектурные решения:**
@ -64,10 +77,20 @@
- Асинхронная обработка через `asyncio` - Асинхронная обработка через `asyncio`
- Каждый пользовательский запрос обрабатывается независимой корутиной - Каждый пользовательский запрос обрабатывается независимой корутиной
- Параллельная обработка нескольких запросов - Параллельная обработка нескольких запросов
- Автоматическое извлечение URL из текста сообщений (работа в группах)
#### Скачивание видео #### Скачивание видео
- **YouTube/Instagram**: Прямое скачивание через `yt-dlp` в executor (не блокирует event loop) - **Все источники**: HTTP запросы к соответствующим микросервисам через `httpx`
- **VK**: 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 с двумя таблицами: - SQLite с двумя таблицами:
@ -80,13 +103,67 @@
- Retry механизм для всех источников (3 попытки по умолчанию) - 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 - Flask — веб-фреймворк для REST API
- `yt-dlp` — библиотека для скачивания видео с VK - `yt-dlp` — библиотека для скачивания видео с VK
- Flask-CORS — для поддержки CORS (если нужно) - Flask-CORS — для поддержки CORS
**API Endpoints:** **API Endpoints:**
@ -97,22 +174,14 @@
- Request: `{"url": "https://vk.com/clip-..."}` - Request: `{"url": "https://vk.com/clip-..."}`
- Response: Бинарные данные видео (200 OK) или JSON с ошибкой (400/500) - Response: Бинарные данные видео (200 OK) или JSON с ошибкой (400/500)
**Особенности реализации:** **Особенности:**
- Специальные заголовки для VK (User-Agent, Referer)
- Обработка кириллицы в именах файлов
- Временные файлы сохраняются в `downloads/` и удаляются после отправки
#### Обработка кириллицы **Порт:** 5555 (внешний) → 5000 (внутренний)
- Имена файлов с кириллицей конвертируются в ASCII для HTTP заголовков
- Оригинальное имя сохраняется в файловой системе
#### Управление файлами ### 5. Определение источника видео
- Временные файлы сохраняются в `downloads/`
- Файлы удаляются после отправки клиенту
- Использование UUID для уникальности имен
#### Ограничения
- Flask dev server (однопоточный) — обрабатывает запросы последовательно
- Для продакшена рекомендуется использовать Gunicorn с несколькими worker'ами
### 3. Определение источника видео
Функция `detect_video_source(url)` анализирует домен URL: Функция `detect_video_source(url)` анализирует домен URL:
@ -123,29 +192,56 @@
- иначе → 'unknown' - иначе → '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 HTTP POST /download/stream → [YouTube/Instagram/VK] Service
```
#### Скачивание с VK
```
User → Bot → detect_video_source() → download_vk_video()
HTTP POST /download/stream → VK Service
yt-dlp → video file → HTTP Response (binary) 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` - Базовый: `python:3.11-slim`
- Зависимости: `ffmpeg`, `wget` - Python пакеты из `requirements.txt` (python-telegram-bot, httpx)
- Python пакеты из `requirements.txt`
**Volumes:** **Volumes:**
- `./video:/app/video` — временные файлы видео - `./video:/app/video` — сохраненные видео (не удаляются)
- `./instagram_cookies.txt:/app/instagram_cookies.txt` — cookies для Instagram
- `./data:/app/data:Z` — база данных (SELinux relabel) - `./data:/app/data:Z` — база данных (SELinux relabel)
**Network:** **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 Service
**Расположение:** `vk-downloader/`
**Образ:** **Образ:**
- Базовый: `python:3.11-slim` - Базовый: `python:3.11-slim`
- Зависимости: `ffmpeg`, `wget` - Зависимости: `ffmpeg`, `wget`
@ -205,22 +352,27 @@ CREATE TABLE stats (
**Ports:** **Ports:**
- `5555:5000` — внешний порт 5555, внутренний 5000 - `5555:5000` — внешний порт 5555, внутренний 5000
**Network:** **Запуск:**
- Отдельная сеть `vk_network` (можно использовать host network для доступа) ```bash
cd vk-downloader && docker compose up -d
```
## Безопасность ## Безопасность
### Переменные окружения ### Переменные окружения
- Токен бота хранится в `.env` (не коммитится в Git) - Токен бота хранится в `.env` (не коммитится в Git)
- Cookies для Instagram хранятся в файле (не коммитится) - Cookies для Instagram хранятся в файле `instagram-downloader/instagram_cookies.txt` (не коммитится)
- URL сервисов настраиваются через `.env`
### Ограничения доступа ### Ограничения доступа
- VK сервис доступен только по указанному IP/URL - Сервисы загрузчиков доступны только по указанным IP/URL
- Нет аутентификации между ботом и VK сервисом (можно добавить API key) - Нет аутентификации между ботом и сервисами (можно добавить API key)
- Cookies файл монтируется read-only
### Файловая система ### Файловая система
- Временные файлы удаляются после отправки - Видео сохраняются в `video/` и не удаляются автоматически
- Cookies файл монтируется read-only (опционально) - Временные файлы в сервисах удаляются после отправки
- Автоматическая очистка только `.part` файлов
## Масштабирование ## Масштабирование
@ -228,15 +380,17 @@ CREATE TABLE stats (
1. **Основной бот:** 1. **Основной бот:**
- Один экземпляр (можно запустить несколько с разными токенами) - Один экземпляр (можно запустить несколько с разными токенами)
- Параллельная обработка запросов ограничена ресурсами CPU/сети - Параллельная обработка запросов через asyncio
- Ограничения: ресурсы CPU/сети
2. **VK сервис:** 2. **Сервисы загрузчиков:**
- Flask dev server — последовательная обработка - Flask dev server — последовательная обработка
- Один экземпляр контейнера - Один экземпляр контейнера каждого сервиса
- Можно масштабировать горизонтально
### Рекомендации для масштабирования ### Рекомендации для масштабирования
1. **VK сервис:** 1. **Сервисы загрузчиков:**
- Использовать Gunicorn с несколькими worker'ами: - Использовать Gunicorn с несколькими worker'ами:
```bash ```bash
gunicorn -w 4 -b 0.0.0.0:5000 app:app gunicorn -w 4 -b 0.0.0.0:5000 app:app
@ -267,7 +421,7 @@ CREATE TABLE stats (
- Размер скачанных файлов - Размер скачанных файлов
### Health checks ### Health checks
- VK сервис: `GET /health` - Все сервисы: `GET /health`
- Основной бот: проверка через статус контейнера - Основной бот: проверка через статус контейнера
## Производительность ## Производительность
@ -276,11 +430,11 @@ CREATE TABLE stats (
1. **Асинхронная обработка:** 1. **Асинхронная обработка:**
- Основной бот использует asyncio для неблокирующих операций - Основной бот использует asyncio для неблокирующих операций
- yt-dlp запускается в executor для YouTube/Instagram - HTTP запросы к сервисам через httpx (асинхронный)
2. **Временные файлы:** 2. **Хранение видео:**
- Хранение в памяти (tmpfs) для быстрого доступа (опционально) - Видео сохраняются на хосте (volume), не в образе контейнера
- Автоматическая очистка после отправки - Файлы доступны для пользователей после скачивания
3. **Кеширование:** 3. **Кеширование:**
- Можно добавить кеш информации о видео (title, duration) - Можно добавить кеш информации о видео (title, duration)
@ -288,34 +442,44 @@ CREATE TABLE stats (
### Узкие места ### Узкие места
1. **VK сервис:** 1. **Сервисы загрузчиков:**
- Последовательная обработка запросов - Последовательная обработка запросов (Flask dev server)
- Скачивание файла перед отправкой (занимает память) - Скачивание файла перед отправкой (занимает память)
2. **Сеть:** 2. **Сеть:**
- Зависимость от скорости интернета - Зависимость от скорости интернета
- Задержки при обращении к внешнему VK сервису - Задержки при обращении к внешним сервисам
- Задержки между ботом и сервисами загрузчиков
## Развертывание ## Развертывание
### Локальная разработка ### Локальная разработка
```bash
# Основной бот
docker compose up -d
# VK сервис ```bash
cd vk-downloader && docker compose up -d # Запуск сервисов загрузчиков (каждый в своей папке)
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 - VPN для доступа к YouTube/Instagram
- `.env`: `VK_DOWNLOADER_URL=http://<host2_ip>:5555` - `.env`:
- `YOUTUBE_DOWNLOADER_URL=http://localhost:5557`
- `INSTAGRAM_DOWNLOADER_URL=http://localhost:5556`
- `VK_DOWNLOADER_URL=http://<host2_ip>:5555`
**Хост 2 (без VPN):** **Хост 2 (без VPN для VK):**
- VK Downloader Service - VK Downloader Service (порт 5555)
- Доступен по IP для хоста 1 - Доступен по IP для хоста 1
- Можно масштабировать горизонтально - Можно масштабировать горизонтально
@ -328,7 +492,7 @@ cd vk-downloader && docker compose up -d
## Будущие улучшения ## Будущие улучшения
1. **Аутентификация между сервисами:** 1. **Аутентификация между сервисами:**
- API key для VK сервиса - API key для сервисов загрузчиков
- JWT токены - JWT токены
2. **Очередь задач:** 2. **Очередь задач:**
@ -339,8 +503,12 @@ cd vk-downloader && docker compose up -d
- Prometheus метрики - Prometheus метрики
- Grafana дашборды - Grafana дашборды
4. **Улучшение VK сервиса:** 4. **Улучшение сервисов:**
- Переход на FastAPI (более производительный) - Переход на FastAPI (более производительный)
- Поддержка WebSockets для прогресса - Поддержка WebSockets для прогресса
- Stream ответа вместо полной загрузки в память - Stream ответа вместо полной загрузки в память
5. **Хранение видео:**
- Организация по датам/пользователям
- Автоматическая очистка старых файлов (опционально)
- Интеграция с облачным хранилищем

View file

@ -1,25 +1,17 @@
FROM python:3.11-slim 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 WORKDIR /app
# Копируем requirements и устанавливаем зависимости # Копируем requirements и устанавливаем зависимости
COPY requirements.txt . COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt RUN pip install --no-cache-dir -r requirements.txt
# Копируем код приложения # Копируем код приложения (только bot.py, не копируем папки сервисов)
COPY . . COPY bot.py .
# Создаем директорию для загрузок # Создаем директории для данных
RUN mkdir -p video RUN mkdir -p video data
# Увеличиваем таймауты для SSL (в секундах)
ENV PYTHONUNBUFFERED=1 ENV PYTHONUNBUFFERED=1
CMD ["python", "bot.py"] CMD ["python", "bot.py"]

259
README.md
View file

@ -1,14 +1,26 @@
# Telegram Video Download Bot # 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 - 📹 Скачивание видео с YouTube
- 📸 Скачивание видео с Instagram (требуются cookies) - 📸 Скачивание видео с Instagram (требуются cookies)
- 🎬 Скачивание видео с VK (через отдельный микросервис) - 🎬 Скачивание видео с VK
- 📊 Статистика скачанных видео и пользователей - 📊 Статистика скачанных видео и пользователей
- 🔄 Автоматическое сохранение статистики в базу данных - 🔄 Автоматическое сохранение статистики в базу данных
- 👥 Работа в группах с автоматическим обнаружением ссылок
## Требования ## Требования
@ -27,7 +39,7 @@ cd videoDownloadBot
### 2. Настройка переменных окружения ### 2. Настройка переменных окружения
Скопируйте `.env.example` в `.env` и заполните: Скопируйте `.env.example` в `.env` в корне проекта и заполните:
```bash ```bash
cp .env.example .env cp .env.example .env
@ -39,56 +51,119 @@ nano .env # или используйте любой редактор
```env ```env
TELEGRAM_BOT_TOKEN=your_telegram_bot_token_here TELEGRAM_BOT_TOKEN=your_telegram_bot_token_here
TELEGRAM_BOT_USERNAME=your_bot_username 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_TOKEN** — токен бота от @BotFather
- **TELEGRAM_BOT_USERNAME** — username бота (без @), используется в подписи видео - **TELEGRAM_BOT_USERNAME** — username бота (без @), используется в подписи видео
- **VK_DOWNLOADER_URL** — URL VK сервиса (для локальной разработки: `http://localhost:5555`, для продакшена: `http://<ip>:5555`) - **YOUTUBE_DOWNLOADER_URL** — URL YouTube сервиса (для docker-compose: `http://youtube-downloader:5000`, для продакшена: `http://<ip>:5557`)
- **INSTAGRAM_DOWNLOADER_URL** — URL Instagram сервиса (для docker-compose: `http://instagram-downloader:5000`, для продакшена: `http://<ip>:5556`)
- **VK_DOWNLOADER_URL** — URL VK сервиса (для docker-compose: `http://vk-downloader:5000`, для продакшена: `http://<ip>:5555`)
### 3. Настройка Instagram (опционально) ### 3. Настройка Instagram (опционально)
Если планируете скачивать видео с Instagram: Если планируете скачивать видео с Instagram:
1. Экспортируйте cookies из браузера (см. `INSTAGRAM_COOKIES_INSTRUCTIONS.md`) 1. Экспортируйте cookies из браузера (см. `instagram-downloader/INSTAGRAM_COOKIES_INSTRUCTIONS.md`)
2. Сохраните файл как `instagram_cookies.txt` в корне проекта 2. Сохраните файл как `instagram_cookies.txt` в папке `instagram-downloader/`
3. Формат: Netscape cookies file 3. Формат: Netscape cookies file
**Автоматическое обновление cookies:** **Быстрое получение cookies через скрипт:**
- Бот автоматически проверяет срок действия cookies каждые 24 часа
- Если cookies истекают через 3 дня (настраивается через `INSTAGRAM_AUTO_UPDATE_DAYS`), бот попытается автоматически обновить их из браузера
- Поддерживаются браузеры: Chrome, Firefox, Edge, Opera (по приоритету)
- Для автоматического обновления браузер должен быть установлен и доступен
**Примечание:** Без cookies Instagram может блокировать запросы. При первом запуске или если автоматическое обновление не сработало, обновите cookies вручную.
### 4. Запуск основного бота
```bash ```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 docker compose up -d
``` ```
Бот запустится и будет готов к работе! Это запустит только основной Telegram бот.
#### Запуск сервисов загрузчиков
Каждый сервис запускается отдельно в своей папке:
Проверить статус:
```bash ```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://<ip_хоста_2>:5555
```
### 5. Проверка статуса
Проверить статус сервисов:
```bash
# Основной бот (из корня проекта)
docker compose ps 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 ```bash
cd vk-downloader # Основной бот (из корня проекта)
docker compose up -d docker compose logs -f bot
```
Для работы на отдельном хосте: # Сервисы загрузчиков (из соответствующих папок)
1. Скопируйте папку `vk-downloader` на целевой хост cd youtube-downloader && docker compose logs -f
2. Запустите: `docker compose up -d` cd ../instagram-downloader && docker compose logs -f
3. Обновите `VK_DOWNLOADER_URL` в `.env` основного бота: `http://<ip_хоста>:5555` cd ../vk-downloader && docker compose logs -f
```
## Использование ## Использование
@ -97,6 +172,10 @@ docker compose up -d
3. Отправьте ссылку на видео (YouTube, Instagram или VK) 3. Отправьте ссылку на видео (YouTube, Instagram или VK)
4. Дождитесь скачивания и получите файл 4. Дождитесь скачивания и получите файл
### Работа в группах
Добавьте бота в группу и дайте ему права администратора (нужно право на удаление сообщений). После этого бот будет автоматически находить ссылки на видео в сообщениях участников, скачивать их и отправлять прямо в группу, заменяя исходное сообщение со ссылкой.
### Команды ### Команды
- `/start` — начало работы с ботом - `/start` — начало работы с ботом
@ -106,50 +185,82 @@ docker compose up -d
``` ```
videoDownloadBot/ videoDownloadBot/
├── bot.py # Основной код бота ├── bot.py # Код основного Telegram бота
├── requirements.txt # Python зависимости ├── requirements.txt # Python зависимости бота
├── Dockerfile # Образ для основного бота ├── Dockerfile # Образ для бота
├── docker-compose.yml # Конфигурация основного бота ├── docker-compose.yml # Оркестратор всех сервисов
├── .env.example # Пример конфигурации ├── .env.example # Пример конфигурации
├── instagram_cookies.txt # Cookies для Instagram (создать вручную) ├── youtube-downloader/ # Сервис для YouTube
├── data/ # База данных SQLite (создается автоматически) │ ├── app.py # Flask API сервис
├── video/ # Временные файлы видео (создается автоматически) │ ├── requirements.txt # Python зависимости
└── vk-downloader/ # Микросервис для VK │ ├── Dockerfile # Образ для YouTube сервиса
├── app.py # Flask API сервис │ └── docker-compose.yml # Конфигурация YouTube сервиса
├── Dockerfile # Образ для VK сервиса ├── instagram-downloader/ # Сервис для Instagram
└── docker-compose.yml # Конфигурация VK сервиса │ ├── 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 ```bash
git pull git pull
docker compose build
docker compose up -d
```
## Логи # Пересобрать и перезапустить основной бот (из корня проекта)
docker compose build && docker compose up -d
Просмотр логов основного бота: # Пересобрать и перезапустить каждый сервис загрузчика отдельно
```bash cd youtube-downloader && docker compose build && docker compose up -d
docker compose logs -f bot cd ../instagram-downloader && docker compose build && docker compose up -d
``` cd ../vk-downloader && docker compose build && docker compose up -d
Просмотр логов VK сервиса:
```bash
cd vk-downloader
docker compose logs -f
``` ```
## Остановка ## Остановка
```bash ```bash
# Основной бот # Остановить основной бот (из корня проекта)
docker compose down docker compose down
# VK сервис # Остановить каждый сервис загрузчика отдельно
cd vk-downloader cd youtube-downloader && docker compose down
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)
1. Настройте VPN для доступа к YouTube и Instagram 1. Настройте VPN для доступа к YouTube и Instagram
2. Запустите основной бот и VK сервис на одном хосте 2. Запустите каждый сервис отдельно в своей папке
3. В `.env` укажите: `VK_DOWNLOADER_URL=http://localhost:5555` 3. В `.env` в корне проекта укажите: `http://localhost:5557`, `http://localhost:5556`, `http://localhost:5555`
### Вариант 2: Раздельное развертывание (рекомендуется) ### Вариант 2: Раздельное развертывание (рекомендуется)
**Хост 1 (с VPN):** **Хост 1 (с VPN):**
- Основной бот (YouTube, Instagram) - Основной бот
- В `.env`: `VK_DOWNLOADER_URL=http://<ip_хоста_2>:5555` - YouTube сервис
- Instagram сервис
- В `.env` в корне: `YOUTUBE_DOWNLOADER_URL=http://localhost:5557`, `INSTAGRAM_DOWNLOADER_URL=http://localhost:5556`, `VK_DOWNLOADER_URL=http://<ip_хоста_2>:5555`
**Хост 2 (без VPN):** **Хост 2 (без VPN):**
- VK сервис (`vk-downloader/`) - VK сервис
- В `.env` основного бота: IP этого хоста - В `.env` corebot на хосте 1: IP этого хоста
**Преимущества:** **Преимущества:**
- VK работает быстрее без VPN - VK работает быстрее без VPN
- Меньше нагрузка на VPN канал - Меньше нагрузка на VPN канал
- Возможность масштабирования VK сервиса отдельно - Возможность масштабирования сервисов отдельно
## Troubleshooting ## Troubleshooting
### Бот не отвечает ### Бот не отвечает
- Проверьте логи: `docker compose logs bot` - Проверьте логи: `docker compose logs bot`
- Убедитесь, что токен правильный в `.env` - Убедитесь, что токен правильный в `.env`
- Проверьте, что все сервисы запущены и доступны
### Instagram не работает ### YouTube/Instagram/VK не работает
- Проверьте наличие `instagram_cookies.txt` - Проверьте, что соответствующий сервис запущен: `docker compose ps`
- Обновите cookies (они могут истечь) - Проверьте URL в `.env` corebot
- Проверьте логи сервиса: `docker compose logs -f`
### VK не работает - Для Instagram: проверьте наличие и валидность `instagram-downloader/instagram_cookies.txt`
- Проверьте, что VK сервис запущен: `cd vk-downloader && docker compose ps`
- Проверьте URL в `.env`: `VK_DOWNLOADER_URL`
- Проверьте доступность порта 5555
### База данных не сохраняется ### База данных не сохраняется
- Проверьте права на папку `data/` - Проверьте права на папку `data/` в корне проекта
- Убедитесь, что volume смонтирован в `docker-compose.yml` - Убедитесь, что volume смонтирован в `docker-compose.yml`
## Лицензия ## Лицензия

550
bot.py
View file

@ -1,17 +1,13 @@
import os import os
import re import re
import json
import logging import logging
import asyncio import asyncio
import sqlite3 import sqlite3
import time import time
import subprocess
from pathlib import Path from pathlib import Path
from urllib.parse import urlparse from urllib.parse import urlparse
from datetime import datetime from datetime import datetime
from concurrent.futures import ThreadPoolExecutor
import yt_dlp
import httpx import httpx
from telegram import Update from telegram import Update
from telegram.ext import Application, MessageHandler, filters, ContextTypes, CommandHandler 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_TOKEN = os.getenv('TELEGRAM_BOT_TOKEN')
TELEGRAM_BOT_USERNAME = os.getenv('TELEGRAM_BOT_USERNAME', 'vrubelVideoDownload_bot') 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') 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) DATA_DIR.mkdir(parents=True, exist_ok=True)
DB_FILE = DATA_DIR / 'bot.db' DB_FILE = DATA_DIR / 'bot.db'
# ThreadPoolExecutor для выполнения блокирующих операций (скачивание видео)
# Позволяет обрабатывать несколько запросов параллельно
DOWNLOAD_EXECUTOR = ThreadPoolExecutor(max_workers=5, thread_name_prefix="download")
def init_database(): def init_database():
"""Инициализирует базу данных и создает таблицы если их нет""" """Инициализирует базу данных и создает таблицы если их нет"""
@ -174,458 +170,144 @@ def extract_urls_from_text(text: str) -> list[str]:
return urls return urls
def cleanup_old_files(max_age_hours: int = 24): def cleanup_old_files():
"""Удаляет старые файлы и .part файлы из папки загрузок""" """Удаляет только .part файлы (недокачанные) из папки загрузок"""
try: try:
current_time = time.time()
max_age_seconds = max_age_hours * 3600
for file_path in DOWNLOADS_DIR.glob('*'): for file_path in DOWNLOADS_DIR.glob('*'):
if not file_path.is_file(): if not file_path.is_file():
continue continue
# Удаляем все .part файлы (недокачанные) # Удаляем только .part файлы (недокачанные)
if file_path.suffix == '.part': if file_path.suffix == '.part':
try: try:
file_path.unlink() file_path.unlink()
logger.info(f"Удален .part файл: {file_path.name}") logger.info(f"Удален .part файл: {file_path.name}")
except Exception as e: except Exception as e:
logger.warning(f"Не удалось удалить .part файл {file_path.name}: {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: except Exception as e:
logger.error(f"Ошибка при очистке старых файлов: {e}") logger.error(f"Ошибка при очистке .part файлов: {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')
async def download_youtube_video(url: str, chat_id: int, max_retries: int = 3) -> str: async def download_youtube_video(url: str, chat_id: int, max_retries: int = 3) -> str:
"""Скачивает видео с YouTube""" """Скачивает видео с 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' logger.info(f"YouTube: отправка запроса на внешний сервис {YOUTUBE_DOWNLOADER_URL}")
last_error = None last_error = None
for attempt in range(max_retries): for attempt in range(max_retries):
try: try:
# Получаем информацию о видео async with httpx.AsyncClient(timeout=600.0) as client: # Увеличенный таймаут для YouTube
ydl_opts_info = { # Отправляем запрос на YouTube сервис
'quiet': False, response = await client.post(
'no_warnings': False, f"{YOUTUBE_DOWNLOADER_URL}/download/stream",
'user_agent': user_agent, json={"url": url},
'socket_timeout': 30, headers={"Content-Type": "application/json"}
'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("Файл не был найден после скачивания")
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: except Exception as e:
last_error = e last_error = e
logger.warning(f"YouTube: попытка {attempt + 1}/{max_retries} не удалась: {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: async def download_instagram_video(url: str, chat_id: int, max_retries: int = 3) -> str:
"""Скачивает видео с Instagram - используем cookies с правильными заголовками""" """Скачивает видео с Instagram через внешний сервис"""
cookies_file = os.getenv('INSTAGRAM_COOKIES_FILE', 'instagram_cookies.txt') logger.info(f"Instagram: отправка запроса на внешний сервис {INSTAGRAM_DOWNLOADER_URL}")
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 last_error = None
for attempt in range(max_retries): for attempt in range(max_retries):
try: try:
# Базовые настройки async with httpx.AsyncClient(timeout=600.0) as client: # Увеличенный таймаут для Instagram
ydl_opts = { # Отправляем запрос на Instagram сервис
'format': 'best', response = await client.post(
'outtmpl': str(DOWNLOADS_DIR / f'{chat_id}_%(title)s.%(ext)s'), f"{INSTAGRAM_DOWNLOADER_URL}/download/stream",
'quiet': False, json={"url": url},
'no_warnings': False, headers={"Content-Type": "application/json"}
'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 если есть if response.status_code != 200:
headers = { error_text = response.text
'Referer': 'https://www.instagram.com/', try:
'X-Requested-With': 'XMLHttpRequest', error_json = response.json()
} error_text = error_json.get('error', error_text)
if csrf_token: except:
headers['X-CSRFToken'] = csrf_token pass
logger.info(f"Instagram: добавлен csrf token в заголовки") raise Exception(f"Instagram сервис вернул ошибку {response.status_code}: {error_text}")
if sessionid:
logger.info(f"Instagram: sessionid найден (длина: {len(sessionid)})")
ydl_opts['http_headers'] = headers # Сохраняем видео во временный файл
video_data = response.content
logger.info(f"Instagram: начинаем скачивание (попытка {attempt + 1}/{max_retries})") video_ext = 'mp4' # По умолчанию mp4
# Скачиваем в 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("Файл не был найден после скачивания")
# Пробуем определить расширение из заголовков
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: except Exception as e:
last_error = e last_error = e
logger.warning(f"Instagram: попытка {attempt + 1}/{max_retries} не удалась: {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 из браузера if attempt < max_retries - 1:
loop = asyncio.get_event_loop() await asyncio.sleep((attempt + 1) * 2)
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 not cookies_file_path.exists(): raise last_error or Exception("Неизвестная ошибка при скачивании с Instagram через внешний сервис")
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 час
async def download_vk_video(url: str, chat_id: int, max_retries: int = 3) -> str: 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() increment_downloads()
# Удаляем временный файл # Сохраняем видео в папку video (не удаляем)
try: logger.info(f"Видео сохранено: {video_path}")
os.remove(video_path)
logger.info(f"Удален временный файл: {video_path}")
except Exception as e:
logger.warning(f"Не удалось удалить файл {video_path}: {e}")
# Удаляем статусное сообщение и исходное сообщение со ссылкой # Удаляем статусное сообщение и исходное сообщение со ссылкой
try: try:
@ -853,9 +531,9 @@ def main():
# Инициализируем базу данных # Инициализируем базу данных
init_database() init_database()
# Очищаем старые файлы при старте # Очищаем .part файлы при старте
logger.info("Очистка старых файлов при старте...") logger.info("Очистка .part файлов при старте...")
cleanup_old_files(max_age_hours=1) # Удаляем файлы старше 1 часа cleanup_old_files() # Удаляем только недокачанные .part файлы
# Создаем приложение # Создаем приложение
application = Application.builder().token(TELEGRAM_BOT_TOKEN).build() 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("start", start_command))
application.add_handler(CommandHandler("stat", stat_command)) application.add_handler(CommandHandler("stat", stat_command))
# Запускаем фоновую задачу для поддержания сессии Instagram # Запускаем периодическую очистку файлов
async def post_init(application: Application): async def post_init(application: Application):
"""Выполняется после инициализации приложения""" """Выполняется после инициализации приложения"""
# Запускаем задачу поддержания сессии Instagram в фоне # Запускаем периодическую очистку .part файлов (каждые 6 часов)
asyncio.create_task(keep_instagram_session_alive())
logger.info("Фоновая задача поддержания сессии Instagram запущена")
# Запускаем периодическую очистку файлов (каждые 6 часов)
async def periodic_cleanup(): async def periodic_cleanup():
while True: while True:
await asyncio.sleep(6 * 3600) # 6 часов await asyncio.sleep(6 * 3600) # 6 часов
cleanup_old_files(max_age_hours=1) cleanup_old_files()
logger.info("Периодическая очистка старых файлов выполнена") logger.info("Периодическая очистка .part файлов выполнена")
asyncio.create_task(periodic_cleanup()) asyncio.create_task(periodic_cleanup())
logger.info("Фоновая задача периодической очистки файлов запущена") logger.info("Фоновая задача периодической очистки файлов запущена")

View file

@ -8,14 +8,10 @@ services:
environment: environment:
- TELEGRAM_BOT_TOKEN=${TELEGRAM_BOT_TOKEN} - TELEGRAM_BOT_TOKEN=${TELEGRAM_BOT_TOKEN}
- TELEGRAM_BOT_USERNAME=${TELEGRAM_BOT_USERNAME} - TELEGRAM_BOT_USERNAME=${TELEGRAM_BOT_USERNAME}
- YOUTUBE_DOWNLOADER_URL=${YOUTUBE_DOWNLOADER_URL}
- INSTAGRAM_DOWNLOADER_URL=${INSTAGRAM_DOWNLOADER_URL}
- VK_DOWNLOADER_URL=${VK_DOWNLOADER_URL} - VK_DOWNLOADER_URL=${VK_DOWNLOADER_URL}
volumes: volumes:
- ./video:/app/video - ./video:/app/video
- ./instagram_cookies.txt:/app/instagram_cookies.txt
- ./data:/app/data:Z - ./data:/app/data:Z
network_mode: host network_mode: host
networks:
bot_network:
driver: bridge

View file

@ -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"

View file

@ -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"]

View file

@ -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 # Этот файл
```

244
instagram-downloader/app.py Normal file
View file

@ -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)

View file

@ -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

View file

@ -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

View file

@ -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 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 1799925263 _ga GA1.2.1451822324.1765365263
.mozilla.org TRUE / FALSE 1765451663 _gid GA1.2.878207985.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 1799925292 datr LFY5aVDEvvzQRTypNm_NZ0d3
.instagram.com TRUE / TRUE 1796901312 ig_did B0879634-89D6-4098-9B3E-958B6BC00183 .instagram.com TRUE / TRUE 1796901312 ig_did B0879634-89D6-4098-9B3E-958B6BC00183
.instagram.com TRUE / TRUE 1765970112 dpr 2 .instagram.com TRUE / TRUE 1765970112 dpr 2
.instagram.com TRUE / TRUE 1799925293 mid aTlWLAAEAAEBRoS_PfrA_i5UP0w1 .instagram.com TRUE / TRUE 1799925293 mid aTlWLAAEAAEBRoS_PfrA_i5UP0w1
.instagram.com TRUE / TRUE 1765980504 wd 1920x944 .instagram.com TRUE / TRUE 1766008776 wd 1920x944
.instagram.com TRUE / TRUE 1796911697 sessionid 42059678244%3AD0GdfKmaFZWqXp%3A10%3AAYieDJrvoWIE9WW--tzjgv-3EyrgI9XT6seopSdHFw .instagram.com TRUE / TRUE 1796939890 sessionid 42059678244%3AD0GdfKmaFZWqXp%3A10%3AAYgpCODjycI3EWMR6G5Uh6kXjroGZ6pb1IRJmXGX3g
.instagram.com TRUE / TRUE 1773165717 ds_user_id 42059678244 .instagram.com TRUE / TRUE 1773180275 ds_user_id 42059678244
.instagram.com TRUE / TRUE 0 rur "LDC\05442059678244\0541796925717:01fef99bf0a6a2eec6207d44260971856a393246d5f7590afa05e8b7183b7b873f243693" .instagram.com TRUE / TRUE 0 rur "LDC\05442059678244\0541796940275:01fef3bd7d6beb547023be3c45c01ebfd5726050a4cced10a3a4af9ff39a731103df7c88"
addons.mozilla.org FALSE / TRUE 0 taarId 4dffa50e49cca797bb48f2f4f11803c251746ad45af1fef3ba1ad37379a24fea 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

View file

@ -0,0 +1,4 @@
Flask==3.0.0
flask-cors==4.0.0
yt-dlp>=2024.12.13

View file

@ -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

View file

@ -1,5 +1,3 @@
python-telegram-bot==20.7 python-telegram-bot==20.7
yt-dlp>=2024.12.13
requests==2.31.0
httpx==0.25.2 httpx==0.25.2

View file

@ -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."

View file

@ -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"]

172
youtube-downloader/app.py Normal file
View file

@ -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)

View file

@ -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

View file

@ -0,0 +1,4 @@
Flask==3.0.0
flask-cors==4.0.0
yt-dlp>=2024.12.13