Рефакторинг: микросервисная архитектура
- Разделение на микросервисы: youtube-downloader, instagram-downloader, vk-downloader - Основной бот в корне проекта, работает через HTTP API с сервисами - Каждый сервис запускается отдельно в своей папке - Видео сохраняются в папке video/ и не удаляются - Обновлена документация и архитектура - Скрипты для Instagram cookies перенесены в instagram-downloader/
This commit is contained in:
parent
8024eea868
commit
436e0cd541
41 changed files with 1348 additions and 693 deletions
|
|
@ -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/
|
||||
|
|
|
|||
20
.env.example
20
.env.example
|
|
@ -1,13 +1,15 @@
|
|||
# Токен Telegram бота (получить у @BotFather)
|
||||
# Telegram Bot Configuration
|
||||
TELEGRAM_BOT_TOKEN=your_telegram_bot_token_here
|
||||
TELEGRAM_BOT_USERNAME=your_bot_username
|
||||
|
||||
# Имя бота (username без @, используется в подписи видео)
|
||||
TELEGRAM_BOT_USERNAME=vrubelVideoDownload_bot
|
||||
|
||||
# URL VK сервиса для скачивания видео (должен быть на хосте без VPN)
|
||||
# Для локальной разработки: http://localhost:5555
|
||||
# Для продакшена: http://<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
|
||||
|
|
|
|||
350
ARCHITECTURE.md
350
ARCHITECTURE.md
|
|
@ -4,10 +4,12 @@
|
|||
|
||||
## Общая архитектура
|
||||
|
||||
Система состоит из двух основных компонентов:
|
||||
Система состоит из микросервисной архитектуры с раздельными сервисами для каждого источника видео:
|
||||
|
||||
1. **Основной бот** (`bot.py`) — Telegram бот, обрабатывающий запросы пользователей
|
||||
2. **VK Downloader Service** (`vk-downloader/`) — отдельный микросервис для скачивания видео с VK
|
||||
1. **Основной бот** (`bot.py` в корне проекта) — Telegram бот, обрабатывающий запросы пользователей и оркестрирующий запросы к сервисам
|
||||
2. **YouTube Downloader Service** (`youtube-downloader/`) — микросервис для скачивания видео с YouTube
|
||||
3. **Instagram Downloader Service** (`instagram-downloader/`) — микросервис для скачивания видео с Instagram
|
||||
4. **VK Downloader Service** (`vk-downloader/`) — микросервис для скачивания видео с VK
|
||||
|
||||
```
|
||||
┌─────────────────┐
|
||||
|
|
@ -18,44 +20,55 @@
|
|||
┌─────────────────────────────────────┐
|
||||
│ Основной бот (bot.py) │
|
||||
│ ┌───────────────────────────────┐ │
|
||||
│ │ YouTube Download Handler │ │
|
||||
│ │ Message Handler │ │
|
||||
│ │ - URL extraction │ │
|
||||
│ │ - Source detection │ │
|
||||
│ └───────────────────────────────┘ │
|
||||
│ ┌───────────────────────────────┐ │
|
||||
│ │ Instagram Download Handler │ │
|
||||
│ └───────────────────────────────┘ │
|
||||
│ ┌───────────────────────────────┐ │
|
||||
│ │ VK API Client │──┼──┐
|
||||
│ │ HTTP API Clients │──┼──┐
|
||||
│ │ - YouTube API Client │ │ │
|
||||
│ │ - Instagram API Client │ │ │
|
||||
│ │ - VK API Client │ │ │
|
||||
│ └───────────────────────────────┘ │ │
|
||||
│ ┌───────────────────────────────┐ │ │
|
||||
│ │ SQLite Database │ │ │
|
||||
│ │ - Users │ │ │
|
||||
│ │ - Stats │ │ │
|
||||
│ └───────────────────────────────┘ │ │
|
||||
│ ┌───────────────────────────────┐ │ │
|
||||
│ │ Video Storage │ │ │
|
||||
│ │ - video/ directory │ │ │
|
||||
│ └───────────────────────────────┘ │ │
|
||||
└─────────────────────────────────────┘ │
|
||||
│ HTTP API
|
||||
│ POST /download/stream
|
||||
▼
|
||||
┌──────────────────────┐
|
||||
│ VK Downloader │
|
||||
│ Service │
|
||||
│ ┌────────────────┐ │
|
||||
│ │ Flask API │ │
|
||||
│ └────────────────┘ │
|
||||
│ ┌────────────────┐ │
|
||||
│ │ yt-dlp │ │
|
||||
│ │ (VK only) │ │
|
||||
│ └────────────────┘ │
|
||||
└──────────────────────┘
|
||||
│ HTTP API
|
||||
│ POST /download/stream
|
||||
│
|
||||
┌──────────────────────────────────┼──────────────────┐
|
||||
│ │ │
|
||||
▼ ▼ ▼
|
||||
┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐
|
||||
│ YouTube Downloader│ │Instagram Download│ │ VK Downloader │
|
||||
│ Service │ │ Service │ │ Service │
|
||||
│ ┌──────────────┐ │ │ ┌──────────────┐ │ │ ┌──────────────┐│
|
||||
│ │ Flask API │ │ │ │ Flask API │ │ │ │ Flask API ││
|
||||
│ └──────────────┘ │ │ └──────────────┘ │ │ └──────────────┘│
|
||||
│ ┌──────────────┐ │ │ ┌──────────────┐ │ │ ┌──────────────┐│
|
||||
│ │ yt-dlp │ │ │ │ yt-dlp │ │ │ │ yt-dlp ││
|
||||
│ │ (YouTube) │ │ │ │ (Instagram) │ │ │ │ (VK only) ││
|
||||
│ └──────────────┘ │ │ └──────────────┘ │ │ └──────────────┘│
|
||||
│ Port: 5557 │ │ Port: 5556 │ │ Port: 5555 │
|
||||
└──────────────────┘ └──────────────────┘ └──────────────────┘
|
||||
```
|
||||
|
||||
## Компоненты системы
|
||||
|
||||
### 1. Основной бот (bot.py)
|
||||
|
||||
**Расположение:** Корень проекта
|
||||
|
||||
**Технологии:**
|
||||
- `python-telegram-bot` (v20.7) — асинхронный фреймворк для Telegram Bot API
|
||||
- `yt-dlp` — библиотека для скачивания видео
|
||||
- `httpx` — асинхронный HTTP клиент для запросов к VK сервису
|
||||
- `httpx` — асинхронный HTTP клиент для запросов к сервисам загрузчиков
|
||||
- `sqlite3` — база данных для хранения статистики
|
||||
|
||||
**Архитектурные решения:**
|
||||
|
|
@ -64,10 +77,20 @@
|
|||
- Асинхронная обработка через `asyncio`
|
||||
- Каждый пользовательский запрос обрабатывается независимой корутиной
|
||||
- Параллельная обработка нескольких запросов
|
||||
- Автоматическое извлечение URL из текста сообщений (работа в группах)
|
||||
|
||||
#### Скачивание видео
|
||||
- **YouTube/Instagram**: Прямое скачивание через `yt-dlp` в executor (не блокирует event loop)
|
||||
- **VK**: HTTP запрос к внешнему микросервису через `httpx`
|
||||
- **Все источники**: HTTP запросы к соответствующим микросервисам через `httpx`
|
||||
- **YouTube**: `POST http://youtube-downloader:5000/download/stream`
|
||||
- **Instagram**: `POST http://instagram-downloader:5000/download/stream`
|
||||
- **VK**: `POST http://vk-downloader:5000/download/stream`
|
||||
- Получение бинарных данных и сохранение во временный файл
|
||||
- Отправка через Telegram API
|
||||
|
||||
#### Хранение видео
|
||||
- Все скачанные видео сохраняются в папке `video/` на хосте
|
||||
- Файлы не удаляются автоматически (сохраняются для пользователей)
|
||||
- Автоматическая очистка только `.part` файлов (недокачанные)
|
||||
|
||||
#### База данных
|
||||
- SQLite с двумя таблицами:
|
||||
|
|
@ -80,13 +103,67 @@
|
|||
- Retry механизм для всех источников (3 попытки по умолчанию)
|
||||
- Логирование всех ошибок
|
||||
- Информативные сообщения пользователю
|
||||
- Автоматическая очистка `.part` файлов при ошибках
|
||||
|
||||
### 2. VK Downloader Service
|
||||
### 2. YouTube Downloader Service
|
||||
|
||||
**Расположение:** `youtube-downloader/`
|
||||
|
||||
**Технологии:**
|
||||
- Flask — веб-фреймворк для REST API
|
||||
- `yt-dlp` — библиотека для скачивания видео с YouTube
|
||||
- Flask-CORS — для поддержки CORS
|
||||
|
||||
**API Endpoints:**
|
||||
|
||||
1. `GET /health` — проверка работоспособности сервиса
|
||||
- Возвращает: `{"status": "ok", "service": "youtube-downloader"}`
|
||||
|
||||
2. `POST /download/stream` — скачивание видео
|
||||
- Request: `{"url": "https://youtube.com/watch?v=..."}`
|
||||
- Response: Бинарные данные видео (200 OK) или JSON с ошибкой (400/500)
|
||||
|
||||
**Особенности:**
|
||||
- Использует специальные настройки yt-dlp для YouTube (player_client, headers)
|
||||
- Поддержка различных форматов видео (mp4, webm, mkv)
|
||||
- Временные файлы сохраняются в `downloads/` и удаляются после отправки
|
||||
|
||||
**Порт:** 5557 (внешний) → 5000 (внутренний)
|
||||
|
||||
### 3. Instagram Downloader Service
|
||||
|
||||
**Расположение:** `instagram-downloader/`
|
||||
|
||||
**Технологии:**
|
||||
- Flask — веб-фреймворк для REST API
|
||||
- `yt-dlp` — библиотека для скачивания видео с Instagram
|
||||
- Flask-CORS — для поддержки CORS
|
||||
|
||||
**API Endpoints:**
|
||||
|
||||
1. `GET /health` — проверка работоспособности сервиса
|
||||
- Возвращает: `{"status": "ok", "service": "instagram-downloader"}`
|
||||
|
||||
2. `POST /download/stream` — скачивание видео
|
||||
- Request: `{"url": "https://instagram.com/p/..."}`
|
||||
- Response: Бинарные данные видео (200 OK) или JSON с ошибкой (400/500)
|
||||
|
||||
**Особенности:**
|
||||
- Требует файл с cookies Instagram (`instagram_cookies.txt` в папке сервиса)
|
||||
- Автоматическая проверка срока действия cookies
|
||||
- Поддержка CSRF токенов и session cookies
|
||||
- Скрипты для получения/обновления cookies в папке сервиса
|
||||
|
||||
**Порт:** 5556 (внешний) → 5000 (внутренний)
|
||||
|
||||
### 4. VK Downloader Service
|
||||
|
||||
**Расположение:** `vk-downloader/`
|
||||
|
||||
**Технологии:**
|
||||
- Flask — веб-фреймворк для REST API
|
||||
- `yt-dlp` — библиотека для скачивания видео с VK
|
||||
- Flask-CORS — для поддержки CORS (если нужно)
|
||||
- Flask-CORS — для поддержки CORS
|
||||
|
||||
**API Endpoints:**
|
||||
|
||||
|
|
@ -97,22 +174,14 @@
|
|||
- Request: `{"url": "https://vk.com/clip-..."}`
|
||||
- Response: Бинарные данные видео (200 OK) или JSON с ошибкой (400/500)
|
||||
|
||||
**Особенности реализации:**
|
||||
**Особенности:**
|
||||
- Специальные заголовки для VK (User-Agent, Referer)
|
||||
- Обработка кириллицы в именах файлов
|
||||
- Временные файлы сохраняются в `downloads/` и удаляются после отправки
|
||||
|
||||
#### Обработка кириллицы
|
||||
- Имена файлов с кириллицей конвертируются в ASCII для HTTP заголовков
|
||||
- Оригинальное имя сохраняется в файловой системе
|
||||
**Порт:** 5555 (внешний) → 5000 (внутренний)
|
||||
|
||||
#### Управление файлами
|
||||
- Временные файлы сохраняются в `downloads/`
|
||||
- Файлы удаляются после отправки клиенту
|
||||
- Использование UUID для уникальности имен
|
||||
|
||||
#### Ограничения
|
||||
- Flask dev server (однопоточный) — обрабатывает запросы последовательно
|
||||
- Для продакшена рекомендуется использовать Gunicorn с несколькими worker'ами
|
||||
|
||||
### 3. Определение источника видео
|
||||
### 5. Определение источника видео
|
||||
|
||||
Функция `detect_video_source(url)` анализирует домен URL:
|
||||
|
||||
|
|
@ -123,29 +192,56 @@
|
|||
- иначе → 'unknown'
|
||||
```
|
||||
|
||||
### 4. Потоки данных
|
||||
### 6. Потоки данных
|
||||
|
||||
#### Скачивание с YouTube/Instagram
|
||||
#### Общий поток скачивания
|
||||
|
||||
```
|
||||
User → Bot → detect_video_source() → download_youtube_video() / download_instagram_video()
|
||||
User → Bot → extract_urls_from_text() → detect_video_source()
|
||||
↓
|
||||
yt-dlp (executor) → video file → Telegram API → User
|
||||
```
|
||||
|
||||
#### Скачивание с VK
|
||||
|
||||
```
|
||||
User → Bot → detect_video_source() → download_vk_video()
|
||||
↓
|
||||
HTTP POST /download/stream → VK Service
|
||||
HTTP POST /download/stream → [YouTube/Instagram/VK] Service
|
||||
↓
|
||||
yt-dlp → video file → HTTP Response (binary)
|
||||
↓
|
||||
Bot receives binary → saves to disk → Telegram API → User
|
||||
Bot receives binary → saves to video/ → Telegram API → User
|
||||
```
|
||||
|
||||
### 5. База данных
|
||||
#### Детальный поток для каждого источника
|
||||
|
||||
**YouTube:**
|
||||
```
|
||||
User → Bot → download_youtube_video()
|
||||
↓
|
||||
httpx.AsyncClient → POST http://youtube-downloader:5000/download/stream
|
||||
↓
|
||||
YouTube Service → yt-dlp → binary data
|
||||
↓
|
||||
Bot saves to video/ → sends to User
|
||||
```
|
||||
|
||||
**Instagram:**
|
||||
```
|
||||
User → Bot → download_instagram_video()
|
||||
↓
|
||||
httpx.AsyncClient → POST http://instagram-downloader:5000/download/stream
|
||||
↓
|
||||
Instagram Service → yt-dlp (with cookies) → binary data
|
||||
↓
|
||||
Bot saves to video/ → sends to User
|
||||
```
|
||||
|
||||
**VK:**
|
||||
```
|
||||
User → Bot → download_vk_video()
|
||||
↓
|
||||
httpx.AsyncClient → POST http://vk-downloader:5000/download/stream
|
||||
↓
|
||||
VK Service → yt-dlp → binary data
|
||||
↓
|
||||
Bot saves to video/ → sends to User
|
||||
```
|
||||
|
||||
### 7. База данных
|
||||
|
||||
**Схема:**
|
||||
|
||||
|
|
@ -179,21 +275,72 @@ CREATE TABLE stats (
|
|||
|
||||
### Основной бот
|
||||
|
||||
**Расположение:** Корень проекта
|
||||
|
||||
**Образ:**
|
||||
- Базовый: `python:3.11-slim`
|
||||
- Зависимости: `ffmpeg`, `wget`
|
||||
- Python пакеты из `requirements.txt`
|
||||
- Python пакеты из `requirements.txt` (python-telegram-bot, httpx)
|
||||
|
||||
**Volumes:**
|
||||
- `./video:/app/video` — временные файлы видео
|
||||
- `./instagram_cookies.txt:/app/instagram_cookies.txt` — cookies для Instagram
|
||||
- `./video:/app/video` — сохраненные видео (не удаляются)
|
||||
- `./data:/app/data:Z` — база данных (SELinux relabel)
|
||||
|
||||
**Network:**
|
||||
- `network_mode: host` — для доступа к VK сервису через localhost
|
||||
- `network_mode: host` — для доступа к сервисам по localhost или IP
|
||||
|
||||
**Запуск:**
|
||||
```bash
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
### YouTube Downloader Service
|
||||
|
||||
**Расположение:** `youtube-downloader/`
|
||||
|
||||
**Образ:**
|
||||
- Базовый: `python:3.11-slim`
|
||||
- Зависимости: `ffmpeg`, `wget`
|
||||
- Python пакеты: Flask, flask-cors, yt-dlp
|
||||
|
||||
**Volumes:**
|
||||
- `./downloads:/app/downloads` — временные файлы
|
||||
|
||||
**Ports:**
|
||||
- `5557:5000` — внешний порт 5557, внутренний 5000
|
||||
|
||||
**Запуск:**
|
||||
```bash
|
||||
cd youtube-downloader && docker compose up -d
|
||||
```
|
||||
|
||||
### Instagram Downloader Service
|
||||
|
||||
**Расположение:** `instagram-downloader/`
|
||||
|
||||
**Образ:**
|
||||
- Базовый: `python:3.11-slim`
|
||||
- Зависимости: `ffmpeg`, `wget`
|
||||
- Python пакеты: Flask, flask-cors, yt-dlp
|
||||
|
||||
**Volumes:**
|
||||
- `./downloads:/app/downloads` — временные файлы
|
||||
- `./instagram_cookies.txt:/app/instagram_cookies.txt:ro` — cookies (read-only)
|
||||
|
||||
**Environment:**
|
||||
- `INSTAGRAM_COOKIES_FILE=/app/instagram_cookies.txt`
|
||||
|
||||
**Ports:**
|
||||
- `5556:5000` — внешний порт 5556, внутренний 5000
|
||||
|
||||
**Запуск:**
|
||||
```bash
|
||||
cd instagram-downloader && docker compose up -d
|
||||
```
|
||||
|
||||
### VK Downloader Service
|
||||
|
||||
**Расположение:** `vk-downloader/`
|
||||
|
||||
**Образ:**
|
||||
- Базовый: `python:3.11-slim`
|
||||
- Зависимости: `ffmpeg`, `wget`
|
||||
|
|
@ -205,22 +352,27 @@ CREATE TABLE stats (
|
|||
**Ports:**
|
||||
- `5555:5000` — внешний порт 5555, внутренний 5000
|
||||
|
||||
**Network:**
|
||||
- Отдельная сеть `vk_network` (можно использовать host network для доступа)
|
||||
**Запуск:**
|
||||
```bash
|
||||
cd vk-downloader && docker compose up -d
|
||||
```
|
||||
|
||||
## Безопасность
|
||||
|
||||
### Переменные окружения
|
||||
- Токен бота хранится в `.env` (не коммитится в Git)
|
||||
- Cookies для Instagram хранятся в файле (не коммитится)
|
||||
- Cookies для Instagram хранятся в файле `instagram-downloader/instagram_cookies.txt` (не коммитится)
|
||||
- URL сервисов настраиваются через `.env`
|
||||
|
||||
### Ограничения доступа
|
||||
- VK сервис доступен только по указанному IP/URL
|
||||
- Нет аутентификации между ботом и VK сервисом (можно добавить API key)
|
||||
- Сервисы загрузчиков доступны только по указанным IP/URL
|
||||
- Нет аутентификации между ботом и сервисами (можно добавить API key)
|
||||
- Cookies файл монтируется read-only
|
||||
|
||||
### Файловая система
|
||||
- Временные файлы удаляются после отправки
|
||||
- Cookies файл монтируется read-only (опционально)
|
||||
- Видео сохраняются в `video/` и не удаляются автоматически
|
||||
- Временные файлы в сервисах удаляются после отправки
|
||||
- Автоматическая очистка только `.part` файлов
|
||||
|
||||
## Масштабирование
|
||||
|
||||
|
|
@ -228,15 +380,17 @@ CREATE TABLE stats (
|
|||
|
||||
1. **Основной бот:**
|
||||
- Один экземпляр (можно запустить несколько с разными токенами)
|
||||
- Параллельная обработка запросов ограничена ресурсами CPU/сети
|
||||
- Параллельная обработка запросов через asyncio
|
||||
- Ограничения: ресурсы CPU/сети
|
||||
|
||||
2. **VK сервис:**
|
||||
2. **Сервисы загрузчиков:**
|
||||
- Flask dev server — последовательная обработка
|
||||
- Один экземпляр контейнера
|
||||
- Один экземпляр контейнера каждого сервиса
|
||||
- Можно масштабировать горизонтально
|
||||
|
||||
### Рекомендации для масштабирования
|
||||
|
||||
1. **VK сервис:**
|
||||
1. **Сервисы загрузчиков:**
|
||||
- Использовать Gunicorn с несколькими worker'ами:
|
||||
```bash
|
||||
gunicorn -w 4 -b 0.0.0.0:5000 app:app
|
||||
|
|
@ -267,7 +421,7 @@ CREATE TABLE stats (
|
|||
- Размер скачанных файлов
|
||||
|
||||
### Health checks
|
||||
- VK сервис: `GET /health`
|
||||
- Все сервисы: `GET /health`
|
||||
- Основной бот: проверка через статус контейнера
|
||||
|
||||
## Производительность
|
||||
|
|
@ -276,11 +430,11 @@ CREATE TABLE stats (
|
|||
|
||||
1. **Асинхронная обработка:**
|
||||
- Основной бот использует asyncio для неблокирующих операций
|
||||
- yt-dlp запускается в executor для YouTube/Instagram
|
||||
- HTTP запросы к сервисам через httpx (асинхронный)
|
||||
|
||||
2. **Временные файлы:**
|
||||
- Хранение в памяти (tmpfs) для быстрого доступа (опционально)
|
||||
- Автоматическая очистка после отправки
|
||||
2. **Хранение видео:**
|
||||
- Видео сохраняются на хосте (volume), не в образе контейнера
|
||||
- Файлы доступны для пользователей после скачивания
|
||||
|
||||
3. **Кеширование:**
|
||||
- Можно добавить кеш информации о видео (title, duration)
|
||||
|
|
@ -288,34 +442,44 @@ CREATE TABLE stats (
|
|||
|
||||
### Узкие места
|
||||
|
||||
1. **VK сервис:**
|
||||
- Последовательная обработка запросов
|
||||
1. **Сервисы загрузчиков:**
|
||||
- Последовательная обработка запросов (Flask dev server)
|
||||
- Скачивание файла перед отправкой (занимает память)
|
||||
|
||||
2. **Сеть:**
|
||||
- Зависимость от скорости интернета
|
||||
- Задержки при обращении к внешнему VK сервису
|
||||
- Задержки при обращении к внешним сервисам
|
||||
- Задержки между ботом и сервисами загрузчиков
|
||||
|
||||
## Развертывание
|
||||
|
||||
### Локальная разработка
|
||||
```bash
|
||||
# Основной бот
|
||||
docker compose up -d
|
||||
|
||||
# VK сервис
|
||||
cd vk-downloader && docker compose up -d
|
||||
```bash
|
||||
# Запуск сервисов загрузчиков (каждый в своей папке)
|
||||
cd youtube-downloader && docker compose up -d
|
||||
cd ../instagram-downloader && docker compose up -d
|
||||
cd ../vk-downloader && docker compose up -d
|
||||
|
||||
# Запуск основного бота (из корня проекта)
|
||||
cd ..
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
### Продакшен (раздельное развертывание)
|
||||
|
||||
**Хост 1 (с VPN):**
|
||||
**Хост 1 (с VPN для YouTube/Instagram):**
|
||||
- Основной бот
|
||||
- YouTube сервис (порт 5557)
|
||||
- Instagram сервис (порт 5556)
|
||||
- VPN для доступа к YouTube/Instagram
|
||||
- `.env`: `VK_DOWNLOADER_URL=http://<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. **Хранение видео:**
|
||||
- Организация по датам/пользователям
|
||||
- Автоматическая очистка старых файлов (опционально)
|
||||
- Интеграция с облачным хранилищем
|
||||
|
|
|
|||
16
Dockerfile
16
Dockerfile
|
|
@ -1,25 +1,17 @@
|
|||
FROM python:3.11-slim
|
||||
|
||||
# Устанавливаем зависимости для yt-dlp
|
||||
RUN apt-get update && apt-get install -y \
|
||||
ffmpeg \
|
||||
wget \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Копируем requirements и устанавливаем зависимости
|
||||
COPY requirements.txt .
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
# Копируем код приложения
|
||||
COPY . .
|
||||
# Копируем код приложения (только bot.py, не копируем папки сервисов)
|
||||
COPY bot.py .
|
||||
|
||||
# Создаем директорию для загрузок
|
||||
RUN mkdir -p video
|
||||
# Создаем директории для данных
|
||||
RUN mkdir -p video data
|
||||
|
||||
# Увеличиваем таймауты для SSL (в секундах)
|
||||
ENV PYTHONUNBUFFERED=1
|
||||
|
||||
CMD ["python", "bot.py"]
|
||||
|
||||
|
|
|
|||
259
README.md
259
README.md
|
|
@ -1,14 +1,26 @@
|
|||
# Telegram Video Download Bot
|
||||
|
||||
Telegram бот для скачивания видео с YouTube, Instagram и VK. Поддерживает раздельное развертывание сервисов для работы с VPN и без VPN.
|
||||
Telegram бот для скачивания видео с YouTube, Instagram и VK. Микросервисная архитектура с раздельными сервисами для каждого источника.
|
||||
|
||||
## Архитектура
|
||||
|
||||
Проект разделен на микросервисы:
|
||||
|
||||
- **Основной бот** (в корне проекта) - Telegram бот, обрабатывает сообщения и оркестрирует запросы к сервисам
|
||||
- **youtube-downloader** - сервис для скачивания с YouTube (порт 5557)
|
||||
- **instagram-downloader** - сервис для скачивания с Instagram (порт 5556)
|
||||
- **vk-downloader** - сервис для скачивания с VK (порт 5555)
|
||||
|
||||
Каждый сервис работает в отдельном Docker контейнере и может быть развернут независимо.
|
||||
|
||||
## Возможности
|
||||
|
||||
- 📹 Скачивание видео с YouTube
|
||||
- 📸 Скачивание видео с Instagram (требуются cookies)
|
||||
- 🎬 Скачивание видео с VK (через отдельный микросервис)
|
||||
- 🎬 Скачивание видео с VK
|
||||
- 📊 Статистика скачанных видео и пользователей
|
||||
- 🔄 Автоматическое сохранение статистики в базу данных
|
||||
- 👥 Работа в группах с автоматическим обнаружением ссылок
|
||||
|
||||
## Требования
|
||||
|
||||
|
|
@ -27,7 +39,7 @@ cd videoDownloadBot
|
|||
|
||||
### 2. Настройка переменных окружения
|
||||
|
||||
Скопируйте `.env.example` в `.env` и заполните:
|
||||
Скопируйте `.env.example` в `.env` в корне проекта и заполните:
|
||||
|
||||
```bash
|
||||
cp .env.example .env
|
||||
|
|
@ -39,56 +51,119 @@ nano .env # или используйте любой редактор
|
|||
```env
|
||||
TELEGRAM_BOT_TOKEN=your_telegram_bot_token_here
|
||||
TELEGRAM_BOT_USERNAME=your_bot_username
|
||||
VK_DOWNLOADER_URL=http://localhost:5555
|
||||
|
||||
# Downloader Services URLs
|
||||
# Для локальной разработки через docker-compose используются внутренние имена сервисов
|
||||
YOUTUBE_DOWNLOADER_URL=http://youtube-downloader:5000
|
||||
INSTAGRAM_DOWNLOADER_URL=http://instagram-downloader:5000
|
||||
VK_DOWNLOADER_URL=http://vk-downloader:5000
|
||||
```
|
||||
|
||||
- **TELEGRAM_BOT_TOKEN** — токен бота от @BotFather
|
||||
- **TELEGRAM_BOT_USERNAME** — username бота (без @), используется в подписи видео
|
||||
- **VK_DOWNLOADER_URL** — URL VK сервиса (для локальной разработки: `http://localhost:5555`, для продакшена: `http://<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`
|
||||
|
||||
## Лицензия
|
||||
|
|
|
|||
530
bot.py
530
bot.py
|
|
@ -1,17 +1,13 @@
|
|||
import os
|
||||
import re
|
||||
import json
|
||||
import logging
|
||||
import asyncio
|
||||
import sqlite3
|
||||
import time
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
from urllib.parse import urlparse
|
||||
from datetime import datetime
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
|
||||
import yt_dlp
|
||||
import httpx
|
||||
from telegram import Update
|
||||
from telegram.ext import Application, MessageHandler, filters, ContextTypes, CommandHandler
|
||||
|
|
@ -26,7 +22,10 @@ logger = logging.getLogger(__name__)
|
|||
# Токен бота и имя бота из переменных окружения
|
||||
TELEGRAM_BOT_TOKEN = os.getenv('TELEGRAM_BOT_TOKEN')
|
||||
TELEGRAM_BOT_USERNAME = os.getenv('TELEGRAM_BOT_USERNAME', 'vrubelVideoDownload_bot')
|
||||
# URL VK сервиса для скачивания видео
|
||||
|
||||
# URL сервисов для скачивания видео
|
||||
YOUTUBE_DOWNLOADER_URL = os.getenv('YOUTUBE_DOWNLOADER_URL', 'http://localhost:5557')
|
||||
INSTAGRAM_DOWNLOADER_URL = os.getenv('INSTAGRAM_DOWNLOADER_URL', 'http://localhost:5556')
|
||||
VK_DOWNLOADER_URL = os.getenv('VK_DOWNLOADER_URL', 'http://localhost:5555')
|
||||
|
||||
# Базовая директория проекта (абсолютный путь), чтобы не зависеть от рабочей директории процесса
|
||||
|
|
@ -41,9 +40,6 @@ DATA_DIR = BASE_DIR / 'data'
|
|||
DATA_DIR.mkdir(parents=True, exist_ok=True)
|
||||
DB_FILE = DATA_DIR / 'bot.db'
|
||||
|
||||
# ThreadPoolExecutor для выполнения блокирующих операций (скачивание видео)
|
||||
# Позволяет обрабатывать несколько запросов параллельно
|
||||
DOWNLOAD_EXECUTOR = ThreadPoolExecutor(max_workers=5, thread_name_prefix="download")
|
||||
|
||||
def init_database():
|
||||
"""Инициализирует базу данных и создает таблицы если их нет"""
|
||||
|
|
@ -174,458 +170,144 @@ def extract_urls_from_text(text: str) -> list[str]:
|
|||
return urls
|
||||
|
||||
|
||||
def cleanup_old_files(max_age_hours: int = 24):
|
||||
"""Удаляет старые файлы и .part файлы из папки загрузок"""
|
||||
def cleanup_old_files():
|
||||
"""Удаляет только .part файлы (недокачанные) из папки загрузок"""
|
||||
try:
|
||||
current_time = time.time()
|
||||
max_age_seconds = max_age_hours * 3600
|
||||
|
||||
for file_path in DOWNLOADS_DIR.glob('*'):
|
||||
if not file_path.is_file():
|
||||
continue
|
||||
|
||||
# Удаляем все .part файлы (недокачанные)
|
||||
# Удаляем только .part файлы (недокачанные)
|
||||
if file_path.suffix == '.part':
|
||||
try:
|
||||
file_path.unlink()
|
||||
logger.info(f"Удален .part файл: {file_path.name}")
|
||||
except Exception as e:
|
||||
logger.warning(f"Не удалось удалить .part файл {file_path.name}: {e}")
|
||||
continue
|
||||
|
||||
# Удаляем старые файлы (старше max_age_hours)
|
||||
try:
|
||||
file_age = current_time - file_path.stat().st_mtime
|
||||
if file_age > max_age_seconds:
|
||||
file_path.unlink()
|
||||
logger.info(f"Удален старый файл: {file_path.name} (возраст: {file_age/3600:.1f} часов)")
|
||||
except Exception as e:
|
||||
logger.warning(f"Не удалось проверить/удалить файл {file_path.name}: {e}")
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при очистке старых файлов: {e}")
|
||||
|
||||
|
||||
def _safe_filename(title: str, chat_id: int) -> str:
|
||||
"""Создает безопасное имя файла"""
|
||||
safe_title = re.sub(r'[<>:"/\\|?*]', '', title)[:100]
|
||||
return str(DOWNLOADS_DIR / f'{chat_id}_{safe_title}.%(ext)s')
|
||||
logger.error(f"Ошибка при очистке .part файлов: {e}")
|
||||
|
||||
|
||||
async def download_youtube_video(url: str, chat_id: int, max_retries: int = 3) -> str:
|
||||
"""Скачивает видео с YouTube"""
|
||||
user_agent = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
|
||||
"""Скачивает видео с YouTube через внешний сервис"""
|
||||
logger.info(f"YouTube: отправка запроса на внешний сервис {YOUTUBE_DOWNLOADER_URL}")
|
||||
|
||||
last_error = None
|
||||
for attempt in range(max_retries):
|
||||
try:
|
||||
# Получаем информацию о видео
|
||||
ydl_opts_info = {
|
||||
'quiet': False,
|
||||
'no_warnings': False,
|
||||
'user_agent': user_agent,
|
||||
'socket_timeout': 30,
|
||||
'extractor_args': {
|
||||
'youtube': {
|
||||
'player_client': ['android', 'web'],
|
||||
'player_skip': ['webpage'],
|
||||
},
|
||||
},
|
||||
'http_headers': {
|
||||
'User-Agent': user_agent,
|
||||
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
|
||||
'Accept-Language': 'en-us,en;q=0.5',
|
||||
'Accept-Encoding': 'gzip, deflate',
|
||||
'Connection': 'keep-alive',
|
||||
},
|
||||
}
|
||||
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"}
|
||||
)
|
||||
|
||||
# Получаем информацию о видео в executor (неблокирующе)
|
||||
def extract_info_sync():
|
||||
with yt_dlp.YoutubeDL(ydl_opts_info) as ydl:
|
||||
return ydl.extract_info(url, download=False)
|
||||
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}")
|
||||
|
||||
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}")
|
||||
# Сохраняем видео во временный файл
|
||||
video_data = response.content
|
||||
video_ext = 'mp4' # По умолчанию mp4
|
||||
|
||||
# Скачиваем видео
|
||||
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',
|
||||
},
|
||||
}
|
||||
# Пробуем определить расширение из заголовков
|
||||
content_type = response.headers.get('Content-Type', '')
|
||||
if 'video/' in content_type:
|
||||
video_ext = content_type.split('/')[-1].split(';')[0]
|
||||
|
||||
logger.info(f"YouTube: начинаем скачивание (попытка {attempt + 1}/{max_retries})")
|
||||
# Получаем имя файла из заголовка или создаем случайное
|
||||
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}'
|
||||
|
||||
# Скачиваем в executor (неблокирующе)
|
||||
def download_sync():
|
||||
with yt_dlp.YoutubeDL(ydl_opts_download) as ydl:
|
||||
ydl.download([url])
|
||||
# Сохраняем файл
|
||||
video_path = DOWNLOADS_DIR / video_filename
|
||||
with open(video_path, 'wb') as f:
|
||||
f.write(video_data)
|
||||
|
||||
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("Файл не был найден после скачивания")
|
||||
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)
|
||||
|
||||
raise last_error or Exception("Неизвестная ошибка при скачивании с YouTube")
|
||||
if attempt < max_retries - 1:
|
||||
await asyncio.sleep((attempt + 1) * 2)
|
||||
|
||||
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,
|
||||
}
|
||||
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"}
|
||||
)
|
||||
|
||||
# Если есть файл с cookies, используем его
|
||||
if cookies_file_path.exists():
|
||||
# Используем абсолютный путь к cookies
|
||||
ydl_opts['cookiefile'] = str(cookies_file_path.absolute())
|
||||
logger.info(f"Instagram: используем cookies из {cookies_file_path}")
|
||||
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}")
|
||||
|
||||
# Добавляем заголовки с 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)})")
|
||||
# Сохраняем видео во временный файл
|
||||
video_data = response.content
|
||||
video_ext = 'mp4' # По умолчанию mp4
|
||||
|
||||
ydl_opts['http_headers'] = headers
|
||||
# Пробуем определить расширение из заголовков
|
||||
content_type = response.headers.get('Content-Type', '')
|
||||
if 'video/' in content_type:
|
||||
video_ext = content_type.split('/')[-1].split(';')[0]
|
||||
|
||||
logger.info(f"Instagram: начинаем скачивание (попытка {attempt + 1}/{max_retries})")
|
||||
# Получаем имя файла из заголовка или создаем случайное
|
||||
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}'
|
||||
|
||||
# Скачиваем в executor (неблокирующе)
|
||||
def download_sync():
|
||||
with yt_dlp.YoutubeDL(ydl_opts) as ydl:
|
||||
ydl.download([url])
|
||||
# Сохраняем файл
|
||||
video_path = DOWNLOADS_DIR / video_filename
|
||||
with open(video_path, 'wb') as f:
|
||||
f.write(video_data)
|
||||
|
||||
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("Файл не был найден после скачивания")
|
||||
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.")
|
||||
if attempt < max_retries - 1:
|
||||
await asyncio.sleep((attempt + 1) * 2)
|
||||
|
||||
|
||||
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 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("Фоновая задача периодической очистки файлов запущена")
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
24
instagram-downloader/Dockerfile
Normal file
24
instagram-downloader/Dockerfile
Normal 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"]
|
||||
|
||||
144
instagram-downloader/README.md
Normal file
144
instagram-downloader/README.md
Normal 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
244
instagram-downloader/app.py
Normal 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)
|
||||
|
||||
19
instagram-downloader/docker-compose.yml
Normal file
19
instagram-downloader/docker-compose.yml
Normal 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
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
39
instagram-downloader/get_instagram_cookies.sh
Executable file
39
instagram-downloader/get_instagram_cookies.sh
Executable 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
|
||||
|
|
@ -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
|
||||
4
instagram-downloader/requirements.txt
Normal file
4
instagram-downloader/requirements.txt
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
Flask==3.0.0
|
||||
flask-cors==4.0.0
|
||||
yt-dlp>=2024.12.13
|
||||
|
||||
47
instagram-downloader/update_instagram_cookies.sh
Executable file
47
instagram-downloader/update_instagram_cookies.sh
Executable 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
|
||||
|
|
@ -1,5 +1,3 @@
|
|||
python-telegram-bot==20.7
|
||||
yt-dlp>=2024.12.13
|
||||
requests==2.31.0
|
||||
httpx==0.25.2
|
||||
|
||||
|
|
|
|||
|
|
@ -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."
|
||||
24
youtube-downloader/Dockerfile
Normal file
24
youtube-downloader/Dockerfile
Normal 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
172
youtube-downloader/app.py
Normal 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)
|
||||
|
||||
16
youtube-downloader/docker-compose.yml
Normal file
16
youtube-downloader/docker-compose.yml
Normal 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
|
||||
|
||||
4
youtube-downloader/requirements.txt
Normal file
4
youtube-downloader/requirements.txt
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
Flask==3.0.0
|
||||
flask-cors==4.0.0
|
||||
yt-dlp>=2024.12.13
|
||||
|
||||
Loading…
Add table
Add a link
Reference in a new issue