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

- Разделение на микросервисы: 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
*.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/

View file

@ -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://<ip_хоста_с_vk_сервисом>: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

View file

@ -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://<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):**
- 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. **Хранение видео:**
- Организация по датам/пользователям
- Автоматическая очистка старых файлов (опционально)
- Интеграция с облачным хранилищем

View file

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

259
README.md
View file

@ -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://<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 (опционально)
Если планируете скачивать видео с 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://<ip_хоста_2>: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://<ip_хоста>: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://<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):**
- 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`
## Лицензия

550
bot.py
View file

@ -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("Фоновая задача периодической очистки файлов запущена")

View file

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

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

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
yt-dlp>=2024.12.13
requests==2.31.0
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