Рефакторинг: микросервисная архитектура
- Разделение на микросервисы: 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
|
youtube-downloader/
|
||||||
*.pyo
|
instagram-downloader/
|
||||||
*.pyd
|
vk-downloader/
|
||||||
.Python
|
|
||||||
downloads/
|
|
||||||
*.log
|
|
||||||
.git
|
|
||||||
.gitignore
|
|
||||||
README.md
|
|
||||||
|
|
||||||
|
# Исключаем данные и временные файлы
|
||||||
|
video/
|
||||||
|
data/
|
||||||
|
*.db
|
||||||
|
*.db-journal
|
||||||
|
|
||||||
|
# Исключаем git
|
||||||
|
.git/
|
||||||
|
.gitignore
|
||||||
|
|
||||||
|
# Исключаем документацию
|
||||||
|
*.md
|
||||||
|
ARCHITECTURE.md
|
||||||
|
|
||||||
|
# Исключаем скрипты
|
||||||
|
*.sh
|
||||||
|
|
||||||
|
# Исключаем env файлы (будут переданы через docker-compose)
|
||||||
|
.env
|
||||||
|
.env.example
|
||||||
|
|
||||||
|
# Исключаем cookies (будут переданы через volume)
|
||||||
|
instagram_cookies.txt
|
||||||
|
|
||||||
|
# Исключаем старые файлы
|
||||||
|
bot.db/
|
||||||
|
|
|
||||||
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_TOKEN=your_telegram_bot_token_here
|
||||||
|
TELEGRAM_BOT_USERNAME=your_bot_username
|
||||||
|
|
||||||
# Имя бота (username без @, используется в подписи видео)
|
# Downloader Services URLs
|
||||||
TELEGRAM_BOT_USERNAME=vrubelVideoDownload_bot
|
# Для локальной разработки через docker-compose используются внутренние имена сервисов с портом 5000
|
||||||
|
# Для продакшена или отдельного запуска сервисов укажите IP адреса или домены с внешними портами
|
||||||
# URL VK сервиса для скачивания видео (должен быть на хосте без VPN)
|
YOUTUBE_DOWNLOADER_URL=http://localhost:5557
|
||||||
# Для локальной разработки: http://localhost:5555
|
INSTAGRAM_DOWNLOADER_URL=http://localhost:5556
|
||||||
# Для продакшена: http://<ip_хоста_с_vk_сервисом>:5555
|
|
||||||
VK_DOWNLOADER_URL=http://localhost:5555
|
VK_DOWNLOADER_URL=http://localhost:5555
|
||||||
|
|
||||||
# Количество дней до истечения cookies, когда начинать автоматическое обновление (по умолчанию: 3)
|
# Примечание: Если используете docker-compose из корня проекта, можно использовать:
|
||||||
INSTAGRAM_AUTO_UPDATE_DAYS=3
|
# YOUTUBE_DOWNLOADER_URL=http://youtube-downloader:5000
|
||||||
|
# INSTAGRAM_DOWNLOADER_URL=http://instagram-downloader:5000
|
||||||
|
# VK_DOWNLOADER_URL=http://vk-downloader:5000
|
||||||
|
|
|
||||||
350
ARCHITECTURE.md
350
ARCHITECTURE.md
|
|
@ -4,10 +4,12 @@
|
||||||
|
|
||||||
## Общая архитектура
|
## Общая архитектура
|
||||||
|
|
||||||
Система состоит из двух основных компонентов:
|
Система состоит из микросервисной архитектуры с раздельными сервисами для каждого источника видео:
|
||||||
|
|
||||||
1. **Основной бот** (`bot.py`) — Telegram бот, обрабатывающий запросы пользователей
|
1. **Основной бот** (`bot.py` в корне проекта) — Telegram бот, обрабатывающий запросы пользователей и оркестрирующий запросы к сервисам
|
||||||
2. **VK Downloader Service** (`vk-downloader/`) — отдельный микросервис для скачивания видео с VK
|
2. **YouTube Downloader Service** (`youtube-downloader/`) — микросервис для скачивания видео с YouTube
|
||||||
|
3. **Instagram Downloader Service** (`instagram-downloader/`) — микросервис для скачивания видео с Instagram
|
||||||
|
4. **VK Downloader Service** (`vk-downloader/`) — микросервис для скачивания видео с VK
|
||||||
|
|
||||||
```
|
```
|
||||||
┌─────────────────┐
|
┌─────────────────┐
|
||||||
|
|
@ -18,44 +20,55 @@
|
||||||
┌─────────────────────────────────────┐
|
┌─────────────────────────────────────┐
|
||||||
│ Основной бот (bot.py) │
|
│ Основной бот (bot.py) │
|
||||||
│ ┌───────────────────────────────┐ │
|
│ ┌───────────────────────────────┐ │
|
||||||
│ │ YouTube Download Handler │ │
|
│ │ Message Handler │ │
|
||||||
|
│ │ - URL extraction │ │
|
||||||
|
│ │ - Source detection │ │
|
||||||
│ └───────────────────────────────┘ │
|
│ └───────────────────────────────┘ │
|
||||||
│ ┌───────────────────────────────┐ │
|
│ ┌───────────────────────────────┐ │
|
||||||
│ │ Instagram Download Handler │ │
|
│ │ HTTP API Clients │──┼──┐
|
||||||
│ └───────────────────────────────┘ │
|
│ │ - YouTube API Client │ │ │
|
||||||
│ ┌───────────────────────────────┐ │
|
│ │ - Instagram API Client │ │ │
|
||||||
│ │ VK API Client │──┼──┐
|
│ │ - VK API Client │ │ │
|
||||||
│ └───────────────────────────────┘ │ │
|
│ └───────────────────────────────┘ │ │
|
||||||
│ ┌───────────────────────────────┐ │ │
|
│ ┌───────────────────────────────┐ │ │
|
||||||
│ │ SQLite Database │ │ │
|
│ │ SQLite Database │ │ │
|
||||||
│ │ - Users │ │ │
|
│ │ - Users │ │ │
|
||||||
│ │ - Stats │ │ │
|
│ │ - Stats │ │ │
|
||||||
│ └───────────────────────────────┘ │ │
|
│ └───────────────────────────────┘ │ │
|
||||||
|
│ ┌───────────────────────────────┐ │ │
|
||||||
|
│ │ Video Storage │ │ │
|
||||||
|
│ │ - video/ directory │ │ │
|
||||||
|
│ └───────────────────────────────┘ │ │
|
||||||
└─────────────────────────────────────┘ │
|
└─────────────────────────────────────┘ │
|
||||||
│ HTTP API
|
│ HTTP API
|
||||||
│ POST /download/stream
|
│ POST /download/stream
|
||||||
▼
|
│
|
||||||
┌──────────────────────┐
|
┌──────────────────────────────────┼──────────────────┐
|
||||||
│ VK Downloader │
|
│ │ │
|
||||||
│ Service │
|
▼ ▼ ▼
|
||||||
│ ┌────────────────┐ │
|
┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐
|
||||||
│ │ Flask API │ │
|
│ YouTube Downloader│ │Instagram Download│ │ VK Downloader │
|
||||||
│ └────────────────┘ │
|
│ Service │ │ Service │ │ Service │
|
||||||
│ ┌────────────────┐ │
|
│ ┌──────────────┐ │ │ ┌──────────────┐ │ │ ┌──────────────┐│
|
||||||
│ │ yt-dlp │ │
|
│ │ Flask API │ │ │ │ Flask API │ │ │ │ Flask API ││
|
||||||
│ │ (VK only) │ │
|
│ └──────────────┘ │ │ └──────────────┘ │ │ └──────────────┘│
|
||||||
│ └────────────────┘ │
|
│ ┌──────────────┐ │ │ ┌──────────────┐ │ │ ┌──────────────┐│
|
||||||
└──────────────────────┘
|
│ │ yt-dlp │ │ │ │ yt-dlp │ │ │ │ yt-dlp ││
|
||||||
|
│ │ (YouTube) │ │ │ │ (Instagram) │ │ │ │ (VK only) ││
|
||||||
|
│ └──────────────┘ │ │ └──────────────┘ │ │ └──────────────┘│
|
||||||
|
│ Port: 5557 │ │ Port: 5556 │ │ Port: 5555 │
|
||||||
|
└──────────────────┘ └──────────────────┘ └──────────────────┘
|
||||||
```
|
```
|
||||||
|
|
||||||
## Компоненты системы
|
## Компоненты системы
|
||||||
|
|
||||||
### 1. Основной бот (bot.py)
|
### 1. Основной бот (bot.py)
|
||||||
|
|
||||||
|
**Расположение:** Корень проекта
|
||||||
|
|
||||||
**Технологии:**
|
**Технологии:**
|
||||||
- `python-telegram-bot` (v20.7) — асинхронный фреймворк для Telegram Bot API
|
- `python-telegram-bot` (v20.7) — асинхронный фреймворк для Telegram Bot API
|
||||||
- `yt-dlp` — библиотека для скачивания видео
|
- `httpx` — асинхронный HTTP клиент для запросов к сервисам загрузчиков
|
||||||
- `httpx` — асинхронный HTTP клиент для запросов к VK сервису
|
|
||||||
- `sqlite3` — база данных для хранения статистики
|
- `sqlite3` — база данных для хранения статистики
|
||||||
|
|
||||||
**Архитектурные решения:**
|
**Архитектурные решения:**
|
||||||
|
|
@ -64,10 +77,20 @@
|
||||||
- Асинхронная обработка через `asyncio`
|
- Асинхронная обработка через `asyncio`
|
||||||
- Каждый пользовательский запрос обрабатывается независимой корутиной
|
- Каждый пользовательский запрос обрабатывается независимой корутиной
|
||||||
- Параллельная обработка нескольких запросов
|
- Параллельная обработка нескольких запросов
|
||||||
|
- Автоматическое извлечение URL из текста сообщений (работа в группах)
|
||||||
|
|
||||||
#### Скачивание видео
|
#### Скачивание видео
|
||||||
- **YouTube/Instagram**: Прямое скачивание через `yt-dlp` в executor (не блокирует event loop)
|
- **Все источники**: HTTP запросы к соответствующим микросервисам через `httpx`
|
||||||
- **VK**: HTTP запрос к внешнему микросервису через `httpx`
|
- **YouTube**: `POST http://youtube-downloader:5000/download/stream`
|
||||||
|
- **Instagram**: `POST http://instagram-downloader:5000/download/stream`
|
||||||
|
- **VK**: `POST http://vk-downloader:5000/download/stream`
|
||||||
|
- Получение бинарных данных и сохранение во временный файл
|
||||||
|
- Отправка через Telegram API
|
||||||
|
|
||||||
|
#### Хранение видео
|
||||||
|
- Все скачанные видео сохраняются в папке `video/` на хосте
|
||||||
|
- Файлы не удаляются автоматически (сохраняются для пользователей)
|
||||||
|
- Автоматическая очистка только `.part` файлов (недокачанные)
|
||||||
|
|
||||||
#### База данных
|
#### База данных
|
||||||
- SQLite с двумя таблицами:
|
- SQLite с двумя таблицами:
|
||||||
|
|
@ -80,13 +103,67 @@
|
||||||
- Retry механизм для всех источников (3 попытки по умолчанию)
|
- Retry механизм для всех источников (3 попытки по умолчанию)
|
||||||
- Логирование всех ошибок
|
- Логирование всех ошибок
|
||||||
- Информативные сообщения пользователю
|
- Информативные сообщения пользователю
|
||||||
|
- Автоматическая очистка `.part` файлов при ошибках
|
||||||
|
|
||||||
### 2. VK Downloader Service
|
### 2. YouTube Downloader Service
|
||||||
|
|
||||||
|
**Расположение:** `youtube-downloader/`
|
||||||
|
|
||||||
|
**Технологии:**
|
||||||
|
- Flask — веб-фреймворк для REST API
|
||||||
|
- `yt-dlp` — библиотека для скачивания видео с YouTube
|
||||||
|
- Flask-CORS — для поддержки CORS
|
||||||
|
|
||||||
|
**API Endpoints:**
|
||||||
|
|
||||||
|
1. `GET /health` — проверка работоспособности сервиса
|
||||||
|
- Возвращает: `{"status": "ok", "service": "youtube-downloader"}`
|
||||||
|
|
||||||
|
2. `POST /download/stream` — скачивание видео
|
||||||
|
- Request: `{"url": "https://youtube.com/watch?v=..."}`
|
||||||
|
- Response: Бинарные данные видео (200 OK) или JSON с ошибкой (400/500)
|
||||||
|
|
||||||
|
**Особенности:**
|
||||||
|
- Использует специальные настройки yt-dlp для YouTube (player_client, headers)
|
||||||
|
- Поддержка различных форматов видео (mp4, webm, mkv)
|
||||||
|
- Временные файлы сохраняются в `downloads/` и удаляются после отправки
|
||||||
|
|
||||||
|
**Порт:** 5557 (внешний) → 5000 (внутренний)
|
||||||
|
|
||||||
|
### 3. Instagram Downloader Service
|
||||||
|
|
||||||
|
**Расположение:** `instagram-downloader/`
|
||||||
|
|
||||||
|
**Технологии:**
|
||||||
|
- Flask — веб-фреймворк для REST API
|
||||||
|
- `yt-dlp` — библиотека для скачивания видео с Instagram
|
||||||
|
- Flask-CORS — для поддержки CORS
|
||||||
|
|
||||||
|
**API Endpoints:**
|
||||||
|
|
||||||
|
1. `GET /health` — проверка работоспособности сервиса
|
||||||
|
- Возвращает: `{"status": "ok", "service": "instagram-downloader"}`
|
||||||
|
|
||||||
|
2. `POST /download/stream` — скачивание видео
|
||||||
|
- Request: `{"url": "https://instagram.com/p/..."}`
|
||||||
|
- Response: Бинарные данные видео (200 OK) или JSON с ошибкой (400/500)
|
||||||
|
|
||||||
|
**Особенности:**
|
||||||
|
- Требует файл с cookies Instagram (`instagram_cookies.txt` в папке сервиса)
|
||||||
|
- Автоматическая проверка срока действия cookies
|
||||||
|
- Поддержка CSRF токенов и session cookies
|
||||||
|
- Скрипты для получения/обновления cookies в папке сервиса
|
||||||
|
|
||||||
|
**Порт:** 5556 (внешний) → 5000 (внутренний)
|
||||||
|
|
||||||
|
### 4. VK Downloader Service
|
||||||
|
|
||||||
|
**Расположение:** `vk-downloader/`
|
||||||
|
|
||||||
**Технологии:**
|
**Технологии:**
|
||||||
- Flask — веб-фреймворк для REST API
|
- Flask — веб-фреймворк для REST API
|
||||||
- `yt-dlp` — библиотека для скачивания видео с VK
|
- `yt-dlp` — библиотека для скачивания видео с VK
|
||||||
- Flask-CORS — для поддержки CORS (если нужно)
|
- Flask-CORS — для поддержки CORS
|
||||||
|
|
||||||
**API Endpoints:**
|
**API Endpoints:**
|
||||||
|
|
||||||
|
|
@ -97,22 +174,14 @@
|
||||||
- Request: `{"url": "https://vk.com/clip-..."}`
|
- Request: `{"url": "https://vk.com/clip-..."}`
|
||||||
- Response: Бинарные данные видео (200 OK) или JSON с ошибкой (400/500)
|
- Response: Бинарные данные видео (200 OK) или JSON с ошибкой (400/500)
|
||||||
|
|
||||||
**Особенности реализации:**
|
**Особенности:**
|
||||||
|
- Специальные заголовки для VK (User-Agent, Referer)
|
||||||
|
- Обработка кириллицы в именах файлов
|
||||||
|
- Временные файлы сохраняются в `downloads/` и удаляются после отправки
|
||||||
|
|
||||||
#### Обработка кириллицы
|
**Порт:** 5555 (внешний) → 5000 (внутренний)
|
||||||
- Имена файлов с кириллицей конвертируются в ASCII для HTTP заголовков
|
|
||||||
- Оригинальное имя сохраняется в файловой системе
|
|
||||||
|
|
||||||
#### Управление файлами
|
### 5. Определение источника видео
|
||||||
- Временные файлы сохраняются в `downloads/`
|
|
||||||
- Файлы удаляются после отправки клиенту
|
|
||||||
- Использование UUID для уникальности имен
|
|
||||||
|
|
||||||
#### Ограничения
|
|
||||||
- Flask dev server (однопоточный) — обрабатывает запросы последовательно
|
|
||||||
- Для продакшена рекомендуется использовать Gunicorn с несколькими worker'ами
|
|
||||||
|
|
||||||
### 3. Определение источника видео
|
|
||||||
|
|
||||||
Функция `detect_video_source(url)` анализирует домен URL:
|
Функция `detect_video_source(url)` анализирует домен URL:
|
||||||
|
|
||||||
|
|
@ -123,29 +192,56 @@
|
||||||
- иначе → 'unknown'
|
- иначе → 'unknown'
|
||||||
```
|
```
|
||||||
|
|
||||||
### 4. Потоки данных
|
### 6. Потоки данных
|
||||||
|
|
||||||
#### Скачивание с YouTube/Instagram
|
#### Общий поток скачивания
|
||||||
|
|
||||||
```
|
```
|
||||||
User → Bot → detect_video_source() → download_youtube_video() / download_instagram_video()
|
User → Bot → extract_urls_from_text() → detect_video_source()
|
||||||
↓
|
↓
|
||||||
yt-dlp (executor) → video file → Telegram API → User
|
HTTP POST /download/stream → [YouTube/Instagram/VK] Service
|
||||||
```
|
|
||||||
|
|
||||||
#### Скачивание с VK
|
|
||||||
|
|
||||||
```
|
|
||||||
User → Bot → detect_video_source() → download_vk_video()
|
|
||||||
↓
|
|
||||||
HTTP POST /download/stream → VK Service
|
|
||||||
↓
|
↓
|
||||||
yt-dlp → video file → HTTP Response (binary)
|
yt-dlp → video file → HTTP Response (binary)
|
||||||
↓
|
↓
|
||||||
Bot receives binary → saves to disk → Telegram API → User
|
Bot receives binary → saves to video/ → Telegram API → User
|
||||||
```
|
```
|
||||||
|
|
||||||
### 5. База данных
|
#### Детальный поток для каждого источника
|
||||||
|
|
||||||
|
**YouTube:**
|
||||||
|
```
|
||||||
|
User → Bot → download_youtube_video()
|
||||||
|
↓
|
||||||
|
httpx.AsyncClient → POST http://youtube-downloader:5000/download/stream
|
||||||
|
↓
|
||||||
|
YouTube Service → yt-dlp → binary data
|
||||||
|
↓
|
||||||
|
Bot saves to video/ → sends to User
|
||||||
|
```
|
||||||
|
|
||||||
|
**Instagram:**
|
||||||
|
```
|
||||||
|
User → Bot → download_instagram_video()
|
||||||
|
↓
|
||||||
|
httpx.AsyncClient → POST http://instagram-downloader:5000/download/stream
|
||||||
|
↓
|
||||||
|
Instagram Service → yt-dlp (with cookies) → binary data
|
||||||
|
↓
|
||||||
|
Bot saves to video/ → sends to User
|
||||||
|
```
|
||||||
|
|
||||||
|
**VK:**
|
||||||
|
```
|
||||||
|
User → Bot → download_vk_video()
|
||||||
|
↓
|
||||||
|
httpx.AsyncClient → POST http://vk-downloader:5000/download/stream
|
||||||
|
↓
|
||||||
|
VK Service → yt-dlp → binary data
|
||||||
|
↓
|
||||||
|
Bot saves to video/ → sends to User
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7. База данных
|
||||||
|
|
||||||
**Схема:**
|
**Схема:**
|
||||||
|
|
||||||
|
|
@ -179,21 +275,72 @@ CREATE TABLE stats (
|
||||||
|
|
||||||
### Основной бот
|
### Основной бот
|
||||||
|
|
||||||
|
**Расположение:** Корень проекта
|
||||||
|
|
||||||
**Образ:**
|
**Образ:**
|
||||||
- Базовый: `python:3.11-slim`
|
- Базовый: `python:3.11-slim`
|
||||||
- Зависимости: `ffmpeg`, `wget`
|
- Python пакеты из `requirements.txt` (python-telegram-bot, httpx)
|
||||||
- Python пакеты из `requirements.txt`
|
|
||||||
|
|
||||||
**Volumes:**
|
**Volumes:**
|
||||||
- `./video:/app/video` — временные файлы видео
|
- `./video:/app/video` — сохраненные видео (не удаляются)
|
||||||
- `./instagram_cookies.txt:/app/instagram_cookies.txt` — cookies для Instagram
|
|
||||||
- `./data:/app/data:Z` — база данных (SELinux relabel)
|
- `./data:/app/data:Z` — база данных (SELinux relabel)
|
||||||
|
|
||||||
**Network:**
|
**Network:**
|
||||||
- `network_mode: host` — для доступа к VK сервису через localhost
|
- `network_mode: host` — для доступа к сервисам по localhost или IP
|
||||||
|
|
||||||
|
**Запуск:**
|
||||||
|
```bash
|
||||||
|
docker compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
### YouTube Downloader Service
|
||||||
|
|
||||||
|
**Расположение:** `youtube-downloader/`
|
||||||
|
|
||||||
|
**Образ:**
|
||||||
|
- Базовый: `python:3.11-slim`
|
||||||
|
- Зависимости: `ffmpeg`, `wget`
|
||||||
|
- Python пакеты: Flask, flask-cors, yt-dlp
|
||||||
|
|
||||||
|
**Volumes:**
|
||||||
|
- `./downloads:/app/downloads` — временные файлы
|
||||||
|
|
||||||
|
**Ports:**
|
||||||
|
- `5557:5000` — внешний порт 5557, внутренний 5000
|
||||||
|
|
||||||
|
**Запуск:**
|
||||||
|
```bash
|
||||||
|
cd youtube-downloader && docker compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
### Instagram Downloader Service
|
||||||
|
|
||||||
|
**Расположение:** `instagram-downloader/`
|
||||||
|
|
||||||
|
**Образ:**
|
||||||
|
- Базовый: `python:3.11-slim`
|
||||||
|
- Зависимости: `ffmpeg`, `wget`
|
||||||
|
- Python пакеты: Flask, flask-cors, yt-dlp
|
||||||
|
|
||||||
|
**Volumes:**
|
||||||
|
- `./downloads:/app/downloads` — временные файлы
|
||||||
|
- `./instagram_cookies.txt:/app/instagram_cookies.txt:ro` — cookies (read-only)
|
||||||
|
|
||||||
|
**Environment:**
|
||||||
|
- `INSTAGRAM_COOKIES_FILE=/app/instagram_cookies.txt`
|
||||||
|
|
||||||
|
**Ports:**
|
||||||
|
- `5556:5000` — внешний порт 5556, внутренний 5000
|
||||||
|
|
||||||
|
**Запуск:**
|
||||||
|
```bash
|
||||||
|
cd instagram-downloader && docker compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
### VK Downloader Service
|
### VK Downloader Service
|
||||||
|
|
||||||
|
**Расположение:** `vk-downloader/`
|
||||||
|
|
||||||
**Образ:**
|
**Образ:**
|
||||||
- Базовый: `python:3.11-slim`
|
- Базовый: `python:3.11-slim`
|
||||||
- Зависимости: `ffmpeg`, `wget`
|
- Зависимости: `ffmpeg`, `wget`
|
||||||
|
|
@ -205,22 +352,27 @@ CREATE TABLE stats (
|
||||||
**Ports:**
|
**Ports:**
|
||||||
- `5555:5000` — внешний порт 5555, внутренний 5000
|
- `5555:5000` — внешний порт 5555, внутренний 5000
|
||||||
|
|
||||||
**Network:**
|
**Запуск:**
|
||||||
- Отдельная сеть `vk_network` (можно использовать host network для доступа)
|
```bash
|
||||||
|
cd vk-downloader && docker compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
## Безопасность
|
## Безопасность
|
||||||
|
|
||||||
### Переменные окружения
|
### Переменные окружения
|
||||||
- Токен бота хранится в `.env` (не коммитится в Git)
|
- Токен бота хранится в `.env` (не коммитится в Git)
|
||||||
- Cookies для Instagram хранятся в файле (не коммитится)
|
- Cookies для Instagram хранятся в файле `instagram-downloader/instagram_cookies.txt` (не коммитится)
|
||||||
|
- URL сервисов настраиваются через `.env`
|
||||||
|
|
||||||
### Ограничения доступа
|
### Ограничения доступа
|
||||||
- VK сервис доступен только по указанному IP/URL
|
- Сервисы загрузчиков доступны только по указанным IP/URL
|
||||||
- Нет аутентификации между ботом и VK сервисом (можно добавить API key)
|
- Нет аутентификации между ботом и сервисами (можно добавить API key)
|
||||||
|
- Cookies файл монтируется read-only
|
||||||
|
|
||||||
### Файловая система
|
### Файловая система
|
||||||
- Временные файлы удаляются после отправки
|
- Видео сохраняются в `video/` и не удаляются автоматически
|
||||||
- Cookies файл монтируется read-only (опционально)
|
- Временные файлы в сервисах удаляются после отправки
|
||||||
|
- Автоматическая очистка только `.part` файлов
|
||||||
|
|
||||||
## Масштабирование
|
## Масштабирование
|
||||||
|
|
||||||
|
|
@ -228,15 +380,17 @@ CREATE TABLE stats (
|
||||||
|
|
||||||
1. **Основной бот:**
|
1. **Основной бот:**
|
||||||
- Один экземпляр (можно запустить несколько с разными токенами)
|
- Один экземпляр (можно запустить несколько с разными токенами)
|
||||||
- Параллельная обработка запросов ограничена ресурсами CPU/сети
|
- Параллельная обработка запросов через asyncio
|
||||||
|
- Ограничения: ресурсы CPU/сети
|
||||||
|
|
||||||
2. **VK сервис:**
|
2. **Сервисы загрузчиков:**
|
||||||
- Flask dev server — последовательная обработка
|
- Flask dev server — последовательная обработка
|
||||||
- Один экземпляр контейнера
|
- Один экземпляр контейнера каждого сервиса
|
||||||
|
- Можно масштабировать горизонтально
|
||||||
|
|
||||||
### Рекомендации для масштабирования
|
### Рекомендации для масштабирования
|
||||||
|
|
||||||
1. **VK сервис:**
|
1. **Сервисы загрузчиков:**
|
||||||
- Использовать Gunicorn с несколькими worker'ами:
|
- Использовать Gunicorn с несколькими worker'ами:
|
||||||
```bash
|
```bash
|
||||||
gunicorn -w 4 -b 0.0.0.0:5000 app:app
|
gunicorn -w 4 -b 0.0.0.0:5000 app:app
|
||||||
|
|
@ -267,7 +421,7 @@ CREATE TABLE stats (
|
||||||
- Размер скачанных файлов
|
- Размер скачанных файлов
|
||||||
|
|
||||||
### Health checks
|
### Health checks
|
||||||
- VK сервис: `GET /health`
|
- Все сервисы: `GET /health`
|
||||||
- Основной бот: проверка через статус контейнера
|
- Основной бот: проверка через статус контейнера
|
||||||
|
|
||||||
## Производительность
|
## Производительность
|
||||||
|
|
@ -276,11 +430,11 @@ CREATE TABLE stats (
|
||||||
|
|
||||||
1. **Асинхронная обработка:**
|
1. **Асинхронная обработка:**
|
||||||
- Основной бот использует asyncio для неблокирующих операций
|
- Основной бот использует asyncio для неблокирующих операций
|
||||||
- yt-dlp запускается в executor для YouTube/Instagram
|
- HTTP запросы к сервисам через httpx (асинхронный)
|
||||||
|
|
||||||
2. **Временные файлы:**
|
2. **Хранение видео:**
|
||||||
- Хранение в памяти (tmpfs) для быстрого доступа (опционально)
|
- Видео сохраняются на хосте (volume), не в образе контейнера
|
||||||
- Автоматическая очистка после отправки
|
- Файлы доступны для пользователей после скачивания
|
||||||
|
|
||||||
3. **Кеширование:**
|
3. **Кеширование:**
|
||||||
- Можно добавить кеш информации о видео (title, duration)
|
- Можно добавить кеш информации о видео (title, duration)
|
||||||
|
|
@ -288,34 +442,44 @@ CREATE TABLE stats (
|
||||||
|
|
||||||
### Узкие места
|
### Узкие места
|
||||||
|
|
||||||
1. **VK сервис:**
|
1. **Сервисы загрузчиков:**
|
||||||
- Последовательная обработка запросов
|
- Последовательная обработка запросов (Flask dev server)
|
||||||
- Скачивание файла перед отправкой (занимает память)
|
- Скачивание файла перед отправкой (занимает память)
|
||||||
|
|
||||||
2. **Сеть:**
|
2. **Сеть:**
|
||||||
- Зависимость от скорости интернета
|
- Зависимость от скорости интернета
|
||||||
- Задержки при обращении к внешнему VK сервису
|
- Задержки при обращении к внешним сервисам
|
||||||
|
- Задержки между ботом и сервисами загрузчиков
|
||||||
|
|
||||||
## Развертывание
|
## Развертывание
|
||||||
|
|
||||||
### Локальная разработка
|
### Локальная разработка
|
||||||
```bash
|
|
||||||
# Основной бот
|
|
||||||
docker compose up -d
|
|
||||||
|
|
||||||
# VK сервис
|
```bash
|
||||||
cd vk-downloader && docker compose up -d
|
# Запуск сервисов загрузчиков (каждый в своей папке)
|
||||||
|
cd youtube-downloader && docker compose up -d
|
||||||
|
cd ../instagram-downloader && docker compose up -d
|
||||||
|
cd ../vk-downloader && docker compose up -d
|
||||||
|
|
||||||
|
# Запуск основного бота (из корня проекта)
|
||||||
|
cd ..
|
||||||
|
docker compose up -d
|
||||||
```
|
```
|
||||||
|
|
||||||
### Продакшен (раздельное развертывание)
|
### Продакшен (раздельное развертывание)
|
||||||
|
|
||||||
**Хост 1 (с VPN):**
|
**Хост 1 (с VPN для YouTube/Instagram):**
|
||||||
- Основной бот
|
- Основной бот
|
||||||
|
- YouTube сервис (порт 5557)
|
||||||
|
- Instagram сервис (порт 5556)
|
||||||
- VPN для доступа к YouTube/Instagram
|
- VPN для доступа к YouTube/Instagram
|
||||||
- `.env`: `VK_DOWNLOADER_URL=http://<host2_ip>:5555`
|
- `.env`:
|
||||||
|
- `YOUTUBE_DOWNLOADER_URL=http://localhost:5557`
|
||||||
|
- `INSTAGRAM_DOWNLOADER_URL=http://localhost:5556`
|
||||||
|
- `VK_DOWNLOADER_URL=http://<host2_ip>:5555`
|
||||||
|
|
||||||
**Хост 2 (без VPN):**
|
**Хост 2 (без VPN для VK):**
|
||||||
- VK Downloader Service
|
- VK Downloader Service (порт 5555)
|
||||||
- Доступен по IP для хоста 1
|
- Доступен по IP для хоста 1
|
||||||
- Можно масштабировать горизонтально
|
- Можно масштабировать горизонтально
|
||||||
|
|
||||||
|
|
@ -328,7 +492,7 @@ cd vk-downloader && docker compose up -d
|
||||||
## Будущие улучшения
|
## Будущие улучшения
|
||||||
|
|
||||||
1. **Аутентификация между сервисами:**
|
1. **Аутентификация между сервисами:**
|
||||||
- API key для VK сервиса
|
- API key для сервисов загрузчиков
|
||||||
- JWT токены
|
- JWT токены
|
||||||
|
|
||||||
2. **Очередь задач:**
|
2. **Очередь задач:**
|
||||||
|
|
@ -339,8 +503,12 @@ cd vk-downloader && docker compose up -d
|
||||||
- Prometheus метрики
|
- Prometheus метрики
|
||||||
- Grafana дашборды
|
- Grafana дашборды
|
||||||
|
|
||||||
4. **Улучшение VK сервиса:**
|
4. **Улучшение сервисов:**
|
||||||
- Переход на FastAPI (более производительный)
|
- Переход на FastAPI (более производительный)
|
||||||
- Поддержка WebSockets для прогресса
|
- Поддержка WebSockets для прогресса
|
||||||
- Stream ответа вместо полной загрузки в память
|
- Stream ответа вместо полной загрузки в память
|
||||||
|
|
||||||
|
5. **Хранение видео:**
|
||||||
|
- Организация по датам/пользователям
|
||||||
|
- Автоматическая очистка старых файлов (опционально)
|
||||||
|
- Интеграция с облачным хранилищем
|
||||||
|
|
|
||||||
16
Dockerfile
16
Dockerfile
|
|
@ -1,25 +1,17 @@
|
||||||
FROM python:3.11-slim
|
FROM python:3.11-slim
|
||||||
|
|
||||||
# Устанавливаем зависимости для yt-dlp
|
|
||||||
RUN apt-get update && apt-get install -y \
|
|
||||||
ffmpeg \
|
|
||||||
wget \
|
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
# Копируем requirements и устанавливаем зависимости
|
# Копируем requirements и устанавливаем зависимости
|
||||||
COPY requirements.txt .
|
COPY requirements.txt .
|
||||||
RUN pip install --no-cache-dir -r requirements.txt
|
RUN pip install --no-cache-dir -r requirements.txt
|
||||||
|
|
||||||
# Копируем код приложения
|
# Копируем код приложения (только bot.py, не копируем папки сервисов)
|
||||||
COPY . .
|
COPY bot.py .
|
||||||
|
|
||||||
# Создаем директорию для загрузок
|
# Создаем директории для данных
|
||||||
RUN mkdir -p video
|
RUN mkdir -p video data
|
||||||
|
|
||||||
# Увеличиваем таймауты для SSL (в секундах)
|
|
||||||
ENV PYTHONUNBUFFERED=1
|
ENV PYTHONUNBUFFERED=1
|
||||||
|
|
||||||
CMD ["python", "bot.py"]
|
CMD ["python", "bot.py"]
|
||||||
|
|
||||||
|
|
|
||||||
259
README.md
259
README.md
|
|
@ -1,14 +1,26 @@
|
||||||
# Telegram Video Download Bot
|
# Telegram Video Download Bot
|
||||||
|
|
||||||
Telegram бот для скачивания видео с YouTube, Instagram и VK. Поддерживает раздельное развертывание сервисов для работы с VPN и без VPN.
|
Telegram бот для скачивания видео с YouTube, Instagram и VK. Микросервисная архитектура с раздельными сервисами для каждого источника.
|
||||||
|
|
||||||
|
## Архитектура
|
||||||
|
|
||||||
|
Проект разделен на микросервисы:
|
||||||
|
|
||||||
|
- **Основной бот** (в корне проекта) - Telegram бот, обрабатывает сообщения и оркестрирует запросы к сервисам
|
||||||
|
- **youtube-downloader** - сервис для скачивания с YouTube (порт 5557)
|
||||||
|
- **instagram-downloader** - сервис для скачивания с Instagram (порт 5556)
|
||||||
|
- **vk-downloader** - сервис для скачивания с VK (порт 5555)
|
||||||
|
|
||||||
|
Каждый сервис работает в отдельном Docker контейнере и может быть развернут независимо.
|
||||||
|
|
||||||
## Возможности
|
## Возможности
|
||||||
|
|
||||||
- 📹 Скачивание видео с YouTube
|
- 📹 Скачивание видео с YouTube
|
||||||
- 📸 Скачивание видео с Instagram (требуются cookies)
|
- 📸 Скачивание видео с Instagram (требуются cookies)
|
||||||
- 🎬 Скачивание видео с VK (через отдельный микросервис)
|
- 🎬 Скачивание видео с VK
|
||||||
- 📊 Статистика скачанных видео и пользователей
|
- 📊 Статистика скачанных видео и пользователей
|
||||||
- 🔄 Автоматическое сохранение статистики в базу данных
|
- 🔄 Автоматическое сохранение статистики в базу данных
|
||||||
|
- 👥 Работа в группах с автоматическим обнаружением ссылок
|
||||||
|
|
||||||
## Требования
|
## Требования
|
||||||
|
|
||||||
|
|
@ -27,7 +39,7 @@ cd videoDownloadBot
|
||||||
|
|
||||||
### 2. Настройка переменных окружения
|
### 2. Настройка переменных окружения
|
||||||
|
|
||||||
Скопируйте `.env.example` в `.env` и заполните:
|
Скопируйте `.env.example` в `.env` в корне проекта и заполните:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cp .env.example .env
|
cp .env.example .env
|
||||||
|
|
@ -39,56 +51,119 @@ nano .env # или используйте любой редактор
|
||||||
```env
|
```env
|
||||||
TELEGRAM_BOT_TOKEN=your_telegram_bot_token_here
|
TELEGRAM_BOT_TOKEN=your_telegram_bot_token_here
|
||||||
TELEGRAM_BOT_USERNAME=your_bot_username
|
TELEGRAM_BOT_USERNAME=your_bot_username
|
||||||
VK_DOWNLOADER_URL=http://localhost:5555
|
|
||||||
|
# Downloader Services URLs
|
||||||
|
# Для локальной разработки через docker-compose используются внутренние имена сервисов
|
||||||
|
YOUTUBE_DOWNLOADER_URL=http://youtube-downloader:5000
|
||||||
|
INSTAGRAM_DOWNLOADER_URL=http://instagram-downloader:5000
|
||||||
|
VK_DOWNLOADER_URL=http://vk-downloader:5000
|
||||||
```
|
```
|
||||||
|
|
||||||
- **TELEGRAM_BOT_TOKEN** — токен бота от @BotFather
|
- **TELEGRAM_BOT_TOKEN** — токен бота от @BotFather
|
||||||
- **TELEGRAM_BOT_USERNAME** — username бота (без @), используется в подписи видео
|
- **TELEGRAM_BOT_USERNAME** — username бота (без @), используется в подписи видео
|
||||||
- **VK_DOWNLOADER_URL** — URL VK сервиса (для локальной разработки: `http://localhost:5555`, для продакшена: `http://<ip>:5555`)
|
- **YOUTUBE_DOWNLOADER_URL** — URL YouTube сервиса (для docker-compose: `http://youtube-downloader:5000`, для продакшена: `http://<ip>:5557`)
|
||||||
|
- **INSTAGRAM_DOWNLOADER_URL** — URL Instagram сервиса (для docker-compose: `http://instagram-downloader:5000`, для продакшена: `http://<ip>:5556`)
|
||||||
|
- **VK_DOWNLOADER_URL** — URL VK сервиса (для docker-compose: `http://vk-downloader:5000`, для продакшена: `http://<ip>:5555`)
|
||||||
|
|
||||||
### 3. Настройка Instagram (опционально)
|
### 3. Настройка Instagram (опционально)
|
||||||
|
|
||||||
Если планируете скачивать видео с Instagram:
|
Если планируете скачивать видео с Instagram:
|
||||||
|
|
||||||
1. Экспортируйте cookies из браузера (см. `INSTAGRAM_COOKIES_INSTRUCTIONS.md`)
|
1. Экспортируйте cookies из браузера (см. `instagram-downloader/INSTAGRAM_COOKIES_INSTRUCTIONS.md`)
|
||||||
2. Сохраните файл как `instagram_cookies.txt` в корне проекта
|
2. Сохраните файл как `instagram_cookies.txt` в папке `instagram-downloader/`
|
||||||
3. Формат: Netscape cookies file
|
3. Формат: Netscape cookies file
|
||||||
|
|
||||||
**Автоматическое обновление cookies:**
|
**Быстрое получение cookies через скрипт:**
|
||||||
- Бот автоматически проверяет срок действия cookies каждые 24 часа
|
|
||||||
- Если cookies истекают через 3 дня (настраивается через `INSTAGRAM_AUTO_UPDATE_DAYS`), бот попытается автоматически обновить их из браузера
|
|
||||||
- Поддерживаются браузеры: Chrome, Firefox, Edge, Opera (по приоритету)
|
|
||||||
- Для автоматического обновления браузер должен быть установлен и доступен
|
|
||||||
|
|
||||||
**Примечание:** Без cookies Instagram может блокировать запросы. При первом запуске или если автоматическое обновление не сработало, обновите cookies вручную.
|
|
||||||
|
|
||||||
### 4. Запуск основного бота
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
cd instagram-downloader
|
||||||
|
./get_instagram_cookies.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Или обновление существующих cookies:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd instagram-downloader
|
||||||
|
./update_instagram_cookies.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
**Примечание:** Без cookies Instagram может блокировать запросы. При первом запуске обновите cookies вручную.
|
||||||
|
|
||||||
|
### 4. Запуск сервисов
|
||||||
|
|
||||||
|
**Важно:** Каждый сервис запускается отдельно на своем хосте (или на одном хосте, но отдельными командами).
|
||||||
|
|
||||||
|
#### Запуск основного бота
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Из корня проекта
|
||||||
docker compose up -d
|
docker compose up -d
|
||||||
```
|
```
|
||||||
|
|
||||||
Бот запустится и будет готов к работе!
|
Это запустит только основной Telegram бот.
|
||||||
|
|
||||||
|
#### Запуск сервисов загрузчиков
|
||||||
|
|
||||||
|
Каждый сервис запускается отдельно в своей папке:
|
||||||
|
|
||||||
Проверить статус:
|
|
||||||
```bash
|
```bash
|
||||||
|
# Запуск YouTube сервиса (порт 5557)
|
||||||
|
cd youtube-downloader
|
||||||
|
docker compose up -d
|
||||||
|
|
||||||
|
# Запуск Instagram сервиса (порт 5556)
|
||||||
|
cd ../instagram-downloader
|
||||||
|
docker compose up -d
|
||||||
|
|
||||||
|
# Запуск VK сервиса (порт 5555)
|
||||||
|
cd ../vk-downloader
|
||||||
|
docker compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
**Порядок запуска:** Сначала запустите сервисы загрузчиков, затем основной бот.
|
||||||
|
|
||||||
|
#### Раздельное развертывание на разных хостах (рекомендуется для продакшена)
|
||||||
|
|
||||||
|
**Хост 1 (с VPN для YouTube/Instagram):**
|
||||||
|
- YouTube сервис (порт 5557)
|
||||||
|
- Instagram сервис (порт 5556)
|
||||||
|
- Основной бот
|
||||||
|
|
||||||
|
**Хост 2 (без VPN для VK):**
|
||||||
|
- VK сервис (порт 5555)
|
||||||
|
|
||||||
|
В `.env` файле в корне проекта на хосте 1 укажите IP адреса сервисов:
|
||||||
|
```env
|
||||||
|
YOUTUBE_DOWNLOADER_URL=http://localhost:5557
|
||||||
|
INSTAGRAM_DOWNLOADER_URL=http://localhost:5556
|
||||||
|
VK_DOWNLOADER_URL=http://<ip_хоста_2>:5555
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. Проверка статуса
|
||||||
|
|
||||||
|
Проверить статус сервисов:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Основной бот (из корня проекта)
|
||||||
docker compose ps
|
docker compose ps
|
||||||
docker compose logs -f bot
|
|
||||||
|
# Сервисы загрузчиков (из соответствующих папок)
|
||||||
|
cd youtube-downloader && docker compose ps
|
||||||
|
cd ../instagram-downloader && docker compose ps
|
||||||
|
cd ../vk-downloader && docker compose ps
|
||||||
```
|
```
|
||||||
|
|
||||||
### 5. Запуск VK сервиса (опционально)
|
Просмотр логов:
|
||||||
|
|
||||||
VK сервис можно запустить на том же хосте или на отдельном хосте без VPN:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd vk-downloader
|
# Основной бот (из корня проекта)
|
||||||
docker compose up -d
|
docker compose logs -f bot
|
||||||
```
|
|
||||||
|
|
||||||
Для работы на отдельном хосте:
|
# Сервисы загрузчиков (из соответствующих папок)
|
||||||
1. Скопируйте папку `vk-downloader` на целевой хост
|
cd youtube-downloader && docker compose logs -f
|
||||||
2. Запустите: `docker compose up -d`
|
cd ../instagram-downloader && docker compose logs -f
|
||||||
3. Обновите `VK_DOWNLOADER_URL` в `.env` основного бота: `http://<ip_хоста>:5555`
|
cd ../vk-downloader && docker compose logs -f
|
||||||
|
```
|
||||||
|
|
||||||
## Использование
|
## Использование
|
||||||
|
|
||||||
|
|
@ -97,6 +172,10 @@ docker compose up -d
|
||||||
3. Отправьте ссылку на видео (YouTube, Instagram или VK)
|
3. Отправьте ссылку на видео (YouTube, Instagram или VK)
|
||||||
4. Дождитесь скачивания и получите файл
|
4. Дождитесь скачивания и получите файл
|
||||||
|
|
||||||
|
### Работа в группах
|
||||||
|
|
||||||
|
Добавьте бота в группу и дайте ему права администратора (нужно право на удаление сообщений). После этого бот будет автоматически находить ссылки на видео в сообщениях участников, скачивать их и отправлять прямо в группу, заменяя исходное сообщение со ссылкой.
|
||||||
|
|
||||||
### Команды
|
### Команды
|
||||||
|
|
||||||
- `/start` — начало работы с ботом
|
- `/start` — начало работы с ботом
|
||||||
|
|
@ -106,50 +185,82 @@ docker compose up -d
|
||||||
|
|
||||||
```
|
```
|
||||||
videoDownloadBot/
|
videoDownloadBot/
|
||||||
├── bot.py # Основной код бота
|
├── bot.py # Код основного Telegram бота
|
||||||
├── requirements.txt # Python зависимости
|
├── requirements.txt # Python зависимости бота
|
||||||
├── Dockerfile # Образ для основного бота
|
├── Dockerfile # Образ для бота
|
||||||
├── docker-compose.yml # Конфигурация основного бота
|
├── docker-compose.yml # Оркестратор всех сервисов
|
||||||
├── .env.example # Пример конфигурации
|
├── .env.example # Пример конфигурации
|
||||||
├── instagram_cookies.txt # Cookies для Instagram (создать вручную)
|
├── youtube-downloader/ # Сервис для YouTube
|
||||||
├── data/ # База данных SQLite (создается автоматически)
|
│ ├── app.py # Flask API сервис
|
||||||
├── video/ # Временные файлы видео (создается автоматически)
|
│ ├── requirements.txt # Python зависимости
|
||||||
└── vk-downloader/ # Микросервис для VK
|
│ ├── Dockerfile # Образ для YouTube сервиса
|
||||||
├── app.py # Flask API сервис
|
│ └── docker-compose.yml # Конфигурация YouTube сервиса
|
||||||
├── Dockerfile # Образ для VK сервиса
|
├── instagram-downloader/ # Сервис для Instagram
|
||||||
└── docker-compose.yml # Конфигурация VK сервиса
|
│ ├── app.py # Flask API сервис
|
||||||
|
│ ├── requirements.txt # Python зависимости
|
||||||
|
│ ├── Dockerfile # Образ для Instagram сервиса
|
||||||
|
│ ├── docker-compose.yml # Конфигурация Instagram сервиса
|
||||||
|
│ ├── instagram_cookies.txt # Cookies для Instagram (создать вручную)
|
||||||
|
│ ├── get_instagram_cookies.sh # Скрипт для получения cookies
|
||||||
|
│ ├── update_instagram_cookies.sh # Скрипт для обновления cookies
|
||||||
|
│ ├── INSTAGRAM_COOKIES_INSTRUCTIONS.md # Инструкции по cookies
|
||||||
|
│ └── README.md # Документация сервиса
|
||||||
|
├── vk-downloader/ # Сервис для VK
|
||||||
|
│ ├── app.py # Flask API сервис
|
||||||
|
│ ├── requirements.txt # Python зависимости
|
||||||
|
│ ├── Dockerfile # Образ для VK сервиса
|
||||||
|
│ └── docker-compose.yml # Конфигурация VK сервиса
|
||||||
|
└── README.md # Этот файл
|
||||||
|
```
|
||||||
|
|
||||||
|
## Порты сервисов
|
||||||
|
|
||||||
|
- **Основной бот**: не требует внешних портов (работает через Telegram API)
|
||||||
|
- **YouTube Downloader**: порт 5557
|
||||||
|
- **Instagram Downloader**: порт 5556
|
||||||
|
- **VK Downloader**: порт 5555
|
||||||
|
|
||||||
|
## API Endpoints
|
||||||
|
|
||||||
|
Все сервисы загрузчиков предоставляют одинаковый API:
|
||||||
|
|
||||||
|
- `GET /health` - проверка здоровья сервиса
|
||||||
|
- `POST /download/stream` - скачивание видео (возвращает бинарные данные)
|
||||||
|
|
||||||
|
Пример запроса:
|
||||||
|
```json
|
||||||
|
POST /download/stream
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"url": "https://youtube.com/watch?v=..."
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## Обновление
|
## Обновление
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git pull
|
git pull
|
||||||
docker compose build
|
|
||||||
docker compose up -d
|
|
||||||
```
|
|
||||||
|
|
||||||
## Логи
|
# Пересобрать и перезапустить основной бот (из корня проекта)
|
||||||
|
docker compose build && docker compose up -d
|
||||||
|
|
||||||
Просмотр логов основного бота:
|
# Пересобрать и перезапустить каждый сервис загрузчика отдельно
|
||||||
```bash
|
cd youtube-downloader && docker compose build && docker compose up -d
|
||||||
docker compose logs -f bot
|
cd ../instagram-downloader && docker compose build && docker compose up -d
|
||||||
```
|
cd ../vk-downloader && docker compose build && docker compose up -d
|
||||||
|
|
||||||
Просмотр логов VK сервиса:
|
|
||||||
```bash
|
|
||||||
cd vk-downloader
|
|
||||||
docker compose logs -f
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Остановка
|
## Остановка
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Основной бот
|
# Остановить основной бот (из корня проекта)
|
||||||
docker compose down
|
docker compose down
|
||||||
|
|
||||||
# VK сервис
|
# Остановить каждый сервис загрузчика отдельно
|
||||||
cd vk-downloader
|
cd youtube-downloader && docker compose down
|
||||||
docker compose down
|
cd ../instagram-downloader && docker compose down
|
||||||
|
cd ../vk-downloader && docker compose down
|
||||||
```
|
```
|
||||||
|
|
||||||
## Развертывание на продакшене
|
## Развертывание на продакшене
|
||||||
|
|
@ -157,41 +268,41 @@ docker compose down
|
||||||
### Вариант 1: Все на одном хосте (с VPN)
|
### Вариант 1: Все на одном хосте (с VPN)
|
||||||
|
|
||||||
1. Настройте VPN для доступа к YouTube и Instagram
|
1. Настройте VPN для доступа к YouTube и Instagram
|
||||||
2. Запустите основной бот и VK сервис на одном хосте
|
2. Запустите каждый сервис отдельно в своей папке
|
||||||
3. В `.env` укажите: `VK_DOWNLOADER_URL=http://localhost:5555`
|
3. В `.env` в корне проекта укажите: `http://localhost:5557`, `http://localhost:5556`, `http://localhost:5555`
|
||||||
|
|
||||||
### Вариант 2: Раздельное развертывание (рекомендуется)
|
### Вариант 2: Раздельное развертывание (рекомендуется)
|
||||||
|
|
||||||
**Хост 1 (с VPN):**
|
**Хост 1 (с VPN):**
|
||||||
- Основной бот (YouTube, Instagram)
|
- Основной бот
|
||||||
- В `.env`: `VK_DOWNLOADER_URL=http://<ip_хоста_2>:5555`
|
- YouTube сервис
|
||||||
|
- Instagram сервис
|
||||||
|
- В `.env` в корне: `YOUTUBE_DOWNLOADER_URL=http://localhost:5557`, `INSTAGRAM_DOWNLOADER_URL=http://localhost:5556`, `VK_DOWNLOADER_URL=http://<ip_хоста_2>:5555`
|
||||||
|
|
||||||
**Хост 2 (без VPN):**
|
**Хост 2 (без VPN):**
|
||||||
- VK сервис (`vk-downloader/`)
|
- VK сервис
|
||||||
- В `.env` основного бота: IP этого хоста
|
- В `.env` corebot на хосте 1: IP этого хоста
|
||||||
|
|
||||||
**Преимущества:**
|
**Преимущества:**
|
||||||
- VK работает быстрее без VPN
|
- VK работает быстрее без VPN
|
||||||
- Меньше нагрузка на VPN канал
|
- Меньше нагрузка на VPN канал
|
||||||
- Возможность масштабирования VK сервиса отдельно
|
- Возможность масштабирования сервисов отдельно
|
||||||
|
|
||||||
## Troubleshooting
|
## Troubleshooting
|
||||||
|
|
||||||
### Бот не отвечает
|
### Бот не отвечает
|
||||||
- Проверьте логи: `docker compose logs bot`
|
- Проверьте логи: `docker compose logs bot`
|
||||||
- Убедитесь, что токен правильный в `.env`
|
- Убедитесь, что токен правильный в `.env`
|
||||||
|
- Проверьте, что все сервисы запущены и доступны
|
||||||
|
|
||||||
### Instagram не работает
|
### YouTube/Instagram/VK не работает
|
||||||
- Проверьте наличие `instagram_cookies.txt`
|
- Проверьте, что соответствующий сервис запущен: `docker compose ps`
|
||||||
- Обновите cookies (они могут истечь)
|
- Проверьте URL в `.env` corebot
|
||||||
|
- Проверьте логи сервиса: `docker compose logs -f`
|
||||||
### VK не работает
|
- Для Instagram: проверьте наличие и валидность `instagram-downloader/instagram_cookies.txt`
|
||||||
- Проверьте, что VK сервис запущен: `cd vk-downloader && docker compose ps`
|
|
||||||
- Проверьте URL в `.env`: `VK_DOWNLOADER_URL`
|
|
||||||
- Проверьте доступность порта 5555
|
|
||||||
|
|
||||||
### База данных не сохраняется
|
### База данных не сохраняется
|
||||||
- Проверьте права на папку `data/`
|
- Проверьте права на папку `data/` в корне проекта
|
||||||
- Убедитесь, что volume смонтирован в `docker-compose.yml`
|
- Убедитесь, что volume смонтирован в `docker-compose.yml`
|
||||||
|
|
||||||
## Лицензия
|
## Лицензия
|
||||||
|
|
|
||||||
530
bot.py
530
bot.py
|
|
@ -1,17 +1,13 @@
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import json
|
|
||||||
import logging
|
import logging
|
||||||
import asyncio
|
import asyncio
|
||||||
import sqlite3
|
import sqlite3
|
||||||
import time
|
import time
|
||||||
import subprocess
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from concurrent.futures import ThreadPoolExecutor
|
|
||||||
|
|
||||||
import yt_dlp
|
|
||||||
import httpx
|
import httpx
|
||||||
from telegram import Update
|
from telegram import Update
|
||||||
from telegram.ext import Application, MessageHandler, filters, ContextTypes, CommandHandler
|
from telegram.ext import Application, MessageHandler, filters, ContextTypes, CommandHandler
|
||||||
|
|
@ -26,7 +22,10 @@ logger = logging.getLogger(__name__)
|
||||||
# Токен бота и имя бота из переменных окружения
|
# Токен бота и имя бота из переменных окружения
|
||||||
TELEGRAM_BOT_TOKEN = os.getenv('TELEGRAM_BOT_TOKEN')
|
TELEGRAM_BOT_TOKEN = os.getenv('TELEGRAM_BOT_TOKEN')
|
||||||
TELEGRAM_BOT_USERNAME = os.getenv('TELEGRAM_BOT_USERNAME', 'vrubelVideoDownload_bot')
|
TELEGRAM_BOT_USERNAME = os.getenv('TELEGRAM_BOT_USERNAME', 'vrubelVideoDownload_bot')
|
||||||
# URL VK сервиса для скачивания видео
|
|
||||||
|
# URL сервисов для скачивания видео
|
||||||
|
YOUTUBE_DOWNLOADER_URL = os.getenv('YOUTUBE_DOWNLOADER_URL', 'http://localhost:5557')
|
||||||
|
INSTAGRAM_DOWNLOADER_URL = os.getenv('INSTAGRAM_DOWNLOADER_URL', 'http://localhost:5556')
|
||||||
VK_DOWNLOADER_URL = os.getenv('VK_DOWNLOADER_URL', 'http://localhost:5555')
|
VK_DOWNLOADER_URL = os.getenv('VK_DOWNLOADER_URL', 'http://localhost:5555')
|
||||||
|
|
||||||
# Базовая директория проекта (абсолютный путь), чтобы не зависеть от рабочей директории процесса
|
# Базовая директория проекта (абсолютный путь), чтобы не зависеть от рабочей директории процесса
|
||||||
|
|
@ -41,9 +40,6 @@ DATA_DIR = BASE_DIR / 'data'
|
||||||
DATA_DIR.mkdir(parents=True, exist_ok=True)
|
DATA_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
DB_FILE = DATA_DIR / 'bot.db'
|
DB_FILE = DATA_DIR / 'bot.db'
|
||||||
|
|
||||||
# ThreadPoolExecutor для выполнения блокирующих операций (скачивание видео)
|
|
||||||
# Позволяет обрабатывать несколько запросов параллельно
|
|
||||||
DOWNLOAD_EXECUTOR = ThreadPoolExecutor(max_workers=5, thread_name_prefix="download")
|
|
||||||
|
|
||||||
def init_database():
|
def init_database():
|
||||||
"""Инициализирует базу данных и создает таблицы если их нет"""
|
"""Инициализирует базу данных и создает таблицы если их нет"""
|
||||||
|
|
@ -174,458 +170,144 @@ def extract_urls_from_text(text: str) -> list[str]:
|
||||||
return urls
|
return urls
|
||||||
|
|
||||||
|
|
||||||
def cleanup_old_files(max_age_hours: int = 24):
|
def cleanup_old_files():
|
||||||
"""Удаляет старые файлы и .part файлы из папки загрузок"""
|
"""Удаляет только .part файлы (недокачанные) из папки загрузок"""
|
||||||
try:
|
try:
|
||||||
current_time = time.time()
|
|
||||||
max_age_seconds = max_age_hours * 3600
|
|
||||||
|
|
||||||
for file_path in DOWNLOADS_DIR.glob('*'):
|
for file_path in DOWNLOADS_DIR.glob('*'):
|
||||||
if not file_path.is_file():
|
if not file_path.is_file():
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Удаляем все .part файлы (недокачанные)
|
# Удаляем только .part файлы (недокачанные)
|
||||||
if file_path.suffix == '.part':
|
if file_path.suffix == '.part':
|
||||||
try:
|
try:
|
||||||
file_path.unlink()
|
file_path.unlink()
|
||||||
logger.info(f"Удален .part файл: {file_path.name}")
|
logger.info(f"Удален .part файл: {file_path.name}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning(f"Не удалось удалить .part файл {file_path.name}: {e}")
|
logger.warning(f"Не удалось удалить .part файл {file_path.name}: {e}")
|
||||||
continue
|
|
||||||
|
|
||||||
# Удаляем старые файлы (старше max_age_hours)
|
|
||||||
try:
|
|
||||||
file_age = current_time - file_path.stat().st_mtime
|
|
||||||
if file_age > max_age_seconds:
|
|
||||||
file_path.unlink()
|
|
||||||
logger.info(f"Удален старый файл: {file_path.name} (возраст: {file_age/3600:.1f} часов)")
|
|
||||||
except Exception as e:
|
|
||||||
logger.warning(f"Не удалось проверить/удалить файл {file_path.name}: {e}")
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Ошибка при очистке старых файлов: {e}")
|
logger.error(f"Ошибка при очистке .part файлов: {e}")
|
||||||
|
|
||||||
|
|
||||||
def _safe_filename(title: str, chat_id: int) -> str:
|
|
||||||
"""Создает безопасное имя файла"""
|
|
||||||
safe_title = re.sub(r'[<>:"/\\|?*]', '', title)[:100]
|
|
||||||
return str(DOWNLOADS_DIR / f'{chat_id}_{safe_title}.%(ext)s')
|
|
||||||
|
|
||||||
|
|
||||||
async def download_youtube_video(url: str, chat_id: int, max_retries: int = 3) -> str:
|
async def download_youtube_video(url: str, chat_id: int, max_retries: int = 3) -> str:
|
||||||
"""Скачивает видео с YouTube"""
|
"""Скачивает видео с YouTube через внешний сервис"""
|
||||||
user_agent = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
|
logger.info(f"YouTube: отправка запроса на внешний сервис {YOUTUBE_DOWNLOADER_URL}")
|
||||||
|
|
||||||
last_error = None
|
last_error = None
|
||||||
for attempt in range(max_retries):
|
for attempt in range(max_retries):
|
||||||
try:
|
try:
|
||||||
# Получаем информацию о видео
|
async with httpx.AsyncClient(timeout=600.0) as client: # Увеличенный таймаут для YouTube
|
||||||
ydl_opts_info = {
|
# Отправляем запрос на YouTube сервис
|
||||||
'quiet': False,
|
response = await client.post(
|
||||||
'no_warnings': False,
|
f"{YOUTUBE_DOWNLOADER_URL}/download/stream",
|
||||||
'user_agent': user_agent,
|
json={"url": url},
|
||||||
'socket_timeout': 30,
|
headers={"Content-Type": "application/json"}
|
||||||
'extractor_args': {
|
)
|
||||||
'youtube': {
|
|
||||||
'player_client': ['android', 'web'],
|
|
||||||
'player_skip': ['webpage'],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
'http_headers': {
|
|
||||||
'User-Agent': user_agent,
|
|
||||||
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
|
|
||||||
'Accept-Language': 'en-us,en;q=0.5',
|
|
||||||
'Accept-Encoding': 'gzip, deflate',
|
|
||||||
'Connection': 'keep-alive',
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
# Получаем информацию о видео в executor (неблокирующе)
|
if response.status_code != 200:
|
||||||
def extract_info_sync():
|
error_text = response.text
|
||||||
with yt_dlp.YoutubeDL(ydl_opts_info) as ydl:
|
try:
|
||||||
return ydl.extract_info(url, download=False)
|
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_data = response.content
|
||||||
video_title = info.get('title', 'video')
|
video_ext = 'mp4' # По умолчанию mp4
|
||||||
logger.info(f"YouTube: получена информация о видео: {video_title}")
|
|
||||||
|
|
||||||
# Скачиваем видео
|
# Пробуем определить расширение из заголовков
|
||||||
ydl_opts_download = {
|
content_type = response.headers.get('Content-Type', '')
|
||||||
'format': 'bestvideo[ext=mp4]+bestaudio[ext=m4a]/best[ext=mp4]/best',
|
if 'video/' in content_type:
|
||||||
'outtmpl': _safe_filename(video_title, chat_id),
|
video_ext = content_type.split('/')[-1].split(';')[0]
|
||||||
'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})")
|
# Получаем имя файла из заголовка или создаем случайное
|
||||||
|
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():
|
video_path = DOWNLOADS_DIR / video_filename
|
||||||
with yt_dlp.YoutubeDL(ydl_opts_download) as ydl:
|
with open(video_path, 'wb') as f:
|
||||||
ydl.download([url])
|
f.write(video_data)
|
||||||
|
|
||||||
await loop.run_in_executor(DOWNLOAD_EXECUTOR, download_sync)
|
logger.info(f"YouTube: видео скачано через внешний сервис: {video_path}")
|
||||||
|
return str(video_path)
|
||||||
# Находим скачанный файл (тоже в 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("Файл не был найден после скачивания")
|
|
||||||
|
|
||||||
|
except httpx.TimeoutException:
|
||||||
|
last_error = Exception(f"Таймаут при запросе к YouTube сервису (попытка {attempt + 1}/{max_retries})")
|
||||||
|
logger.warning(f"YouTube: таймаут при запросе к сервису: {last_error}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
last_error = e
|
last_error = e
|
||||||
logger.warning(f"YouTube: попытка {attempt + 1}/{max_retries} не удалась: {e}")
|
logger.warning(f"YouTube: попытка {attempt + 1}/{max_retries} не удалась: {e}")
|
||||||
if attempt < max_retries - 1:
|
|
||||||
await asyncio.sleep((attempt + 1) * 2)
|
|
||||||
|
|
||||||
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:
|
async def download_instagram_video(url: str, chat_id: int, max_retries: int = 3) -> str:
|
||||||
"""Скачивает видео с Instagram - используем cookies с правильными заголовками"""
|
"""Скачивает видео с Instagram через внешний сервис"""
|
||||||
cookies_file = os.getenv('INSTAGRAM_COOKIES_FILE', 'instagram_cookies.txt')
|
logger.info(f"Instagram: отправка запроса на внешний сервис {INSTAGRAM_DOWNLOADER_URL}")
|
||||||
cookies_file_path = Path(cookies_file)
|
|
||||||
|
|
||||||
# Проверяем срок действия cookies перед использованием
|
|
||||||
if cookies_file_path.exists():
|
|
||||||
is_valid, days_left = check_instagram_cookies_expiry()
|
|
||||||
if not is_valid:
|
|
||||||
logger.error("Instagram cookies истекли! Необходимо обновить cookies.")
|
|
||||||
raise Exception("Instagram cookies истекли. Пожалуйста, обновите cookies в файле instagram_cookies.txt")
|
|
||||||
elif days_left < 7:
|
|
||||||
logger.warning(f"Instagram cookies истекают через {days_left} дней. Рекомендуется обновить.")
|
|
||||||
|
|
||||||
# Парсим cookies для получения csrf token (формат Netscape)
|
|
||||||
csrf_token = None
|
|
||||||
sessionid = None
|
|
||||||
if cookies_file_path.exists():
|
|
||||||
try:
|
|
||||||
with open(cookies_file_path, 'r') as f:
|
|
||||||
for line in f:
|
|
||||||
line = line.strip()
|
|
||||||
if not line or line.startswith('#'):
|
|
||||||
continue
|
|
||||||
parts = line.split('\t')
|
|
||||||
if len(parts) >= 7:
|
|
||||||
domain = parts[0]
|
|
||||||
# Ищем только cookies от instagram.com
|
|
||||||
if 'instagram' in domain.lower():
|
|
||||||
cookie_name = parts[5] # Имя cookie
|
|
||||||
cookie_value = parts[6] # Значение cookie
|
|
||||||
if cookie_name == 'csrftoken':
|
|
||||||
csrf_token = cookie_value
|
|
||||||
elif cookie_name == 'sessionid':
|
|
||||||
sessionid = cookie_value
|
|
||||||
# Если нашли оба - можно выходить
|
|
||||||
if csrf_token and sessionid:
|
|
||||||
break
|
|
||||||
except Exception as e:
|
|
||||||
logger.warning(f"Не удалось прочитать cookies: {e}")
|
|
||||||
|
|
||||||
last_error = None
|
last_error = None
|
||||||
for attempt in range(max_retries):
|
for attempt in range(max_retries):
|
||||||
try:
|
try:
|
||||||
# Базовые настройки
|
async with httpx.AsyncClient(timeout=600.0) as client: # Увеличенный таймаут для Instagram
|
||||||
ydl_opts = {
|
# Отправляем запрос на Instagram сервис
|
||||||
'format': 'best',
|
response = await client.post(
|
||||||
'outtmpl': str(DOWNLOADS_DIR / f'{chat_id}_%(title)s.%(ext)s'),
|
f"{INSTAGRAM_DOWNLOADER_URL}/download/stream",
|
||||||
'quiet': False,
|
json={"url": url},
|
||||||
'no_warnings': False,
|
headers={"Content-Type": "application/json"}
|
||||||
'socket_timeout': 30,
|
)
|
||||||
}
|
|
||||||
|
|
||||||
# Если есть файл с cookies, используем его
|
if response.status_code != 200:
|
||||||
if cookies_file_path.exists():
|
error_text = response.text
|
||||||
# Используем абсолютный путь к cookies
|
try:
|
||||||
ydl_opts['cookiefile'] = str(cookies_file_path.absolute())
|
error_json = response.json()
|
||||||
logger.info(f"Instagram: используем cookies из {cookies_file_path}")
|
error_text = error_json.get('error', error_text)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
raise Exception(f"Instagram сервис вернул ошибку {response.status_code}: {error_text}")
|
||||||
|
|
||||||
# Добавляем заголовки с csrf token если есть
|
# Сохраняем видео во временный файл
|
||||||
headers = {
|
video_data = response.content
|
||||||
'Referer': 'https://www.instagram.com/',
|
video_ext = 'mp4' # По умолчанию mp4
|
||||||
'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
|
# Пробуем определить расширение из заголовков
|
||||||
|
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():
|
video_path = DOWNLOADS_DIR / video_filename
|
||||||
with yt_dlp.YoutubeDL(ydl_opts) as ydl:
|
with open(video_path, 'wb') as f:
|
||||||
ydl.download([url])
|
f.write(video_data)
|
||||||
|
|
||||||
loop = asyncio.get_event_loop()
|
logger.info(f"Instagram: видео скачано через внешний сервис: {video_path}")
|
||||||
await loop.run_in_executor(DOWNLOAD_EXECUTOR, download_sync)
|
return str(video_path)
|
||||||
|
|
||||||
# Находим скачанный файл (тоже в 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("Файл не был найден после скачивания")
|
|
||||||
|
|
||||||
|
except httpx.TimeoutException:
|
||||||
|
last_error = Exception(f"Таймаут при запросе к Instagram сервису (попытка {attempt + 1}/{max_retries})")
|
||||||
|
logger.warning(f"Instagram: таймаут при запросе к сервису: {last_error}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
last_error = e
|
last_error = e
|
||||||
logger.warning(f"Instagram: попытка {attempt + 1}/{max_retries} не удалась: {e}")
|
logger.warning(f"Instagram: попытка {attempt + 1}/{max_retries} не удалась: {e}")
|
||||||
if attempt < max_retries - 1:
|
|
||||||
await asyncio.sleep((attempt + 1) * 2)
|
|
||||||
|
|
||||||
raise last_error or Exception("Неизвестная ошибка при скачивании с Instagram. Возможно, нужно обновить cookies.")
|
if attempt < max_retries - 1:
|
||||||
|
await asyncio.sleep((attempt + 1) * 2)
|
||||||
|
|
||||||
|
raise last_error or Exception("Неизвестная ошибка при скачивании с Instagram через внешний сервис")
|
||||||
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 час
|
|
||||||
|
|
||||||
|
|
||||||
async def download_vk_video(url: str, chat_id: int, max_retries: int = 3) -> str:
|
async def download_vk_video(url: str, chat_id: int, max_retries: int = 3) -> str:
|
||||||
|
|
@ -771,12 +453,8 @@ async def handle_message(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||||
# Увеличиваем счетчик скачанных видео
|
# Увеличиваем счетчик скачанных видео
|
||||||
increment_downloads()
|
increment_downloads()
|
||||||
|
|
||||||
# Удаляем временный файл
|
# Сохраняем видео в папку video (не удаляем)
|
||||||
try:
|
logger.info(f"Видео сохранено: {video_path}")
|
||||||
os.remove(video_path)
|
|
||||||
logger.info(f"Удален временный файл: {video_path}")
|
|
||||||
except Exception as e:
|
|
||||||
logger.warning(f"Не удалось удалить файл {video_path}: {e}")
|
|
||||||
|
|
||||||
# Удаляем статусное сообщение и исходное сообщение со ссылкой
|
# Удаляем статусное сообщение и исходное сообщение со ссылкой
|
||||||
try:
|
try:
|
||||||
|
|
@ -853,9 +531,9 @@ def main():
|
||||||
# Инициализируем базу данных
|
# Инициализируем базу данных
|
||||||
init_database()
|
init_database()
|
||||||
|
|
||||||
# Очищаем старые файлы при старте
|
# Очищаем .part файлы при старте
|
||||||
logger.info("Очистка старых файлов при старте...")
|
logger.info("Очистка .part файлов при старте...")
|
||||||
cleanup_old_files(max_age_hours=1) # Удаляем файлы старше 1 часа
|
cleanup_old_files() # Удаляем только недокачанные .part файлы
|
||||||
|
|
||||||
# Создаем приложение
|
# Создаем приложение
|
||||||
application = Application.builder().token(TELEGRAM_BOT_TOKEN).build()
|
application = Application.builder().token(TELEGRAM_BOT_TOKEN).build()
|
||||||
|
|
@ -865,19 +543,15 @@ def main():
|
||||||
application.add_handler(CommandHandler("start", start_command))
|
application.add_handler(CommandHandler("start", start_command))
|
||||||
application.add_handler(CommandHandler("stat", stat_command))
|
application.add_handler(CommandHandler("stat", stat_command))
|
||||||
|
|
||||||
# Запускаем фоновую задачу для поддержания сессии Instagram
|
# Запускаем периодическую очистку файлов
|
||||||
async def post_init(application: Application):
|
async def post_init(application: Application):
|
||||||
"""Выполняется после инициализации приложения"""
|
"""Выполняется после инициализации приложения"""
|
||||||
# Запускаем задачу поддержания сессии Instagram в фоне
|
# Запускаем периодическую очистку .part файлов (каждые 6 часов)
|
||||||
asyncio.create_task(keep_instagram_session_alive())
|
|
||||||
logger.info("Фоновая задача поддержания сессии Instagram запущена")
|
|
||||||
|
|
||||||
# Запускаем периодическую очистку файлов (каждые 6 часов)
|
|
||||||
async def periodic_cleanup():
|
async def periodic_cleanup():
|
||||||
while True:
|
while True:
|
||||||
await asyncio.sleep(6 * 3600) # 6 часов
|
await asyncio.sleep(6 * 3600) # 6 часов
|
||||||
cleanup_old_files(max_age_hours=1)
|
cleanup_old_files()
|
||||||
logger.info("Периодическая очистка старых файлов выполнена")
|
logger.info("Периодическая очистка .part файлов выполнена")
|
||||||
|
|
||||||
asyncio.create_task(periodic_cleanup())
|
asyncio.create_task(periodic_cleanup())
|
||||||
logger.info("Фоновая задача периодической очистки файлов запущена")
|
logger.info("Фоновая задача периодической очистки файлов запущена")
|
||||||
|
|
|
||||||
|
|
@ -8,14 +8,10 @@ services:
|
||||||
environment:
|
environment:
|
||||||
- TELEGRAM_BOT_TOKEN=${TELEGRAM_BOT_TOKEN}
|
- TELEGRAM_BOT_TOKEN=${TELEGRAM_BOT_TOKEN}
|
||||||
- TELEGRAM_BOT_USERNAME=${TELEGRAM_BOT_USERNAME}
|
- TELEGRAM_BOT_USERNAME=${TELEGRAM_BOT_USERNAME}
|
||||||
|
- YOUTUBE_DOWNLOADER_URL=${YOUTUBE_DOWNLOADER_URL}
|
||||||
|
- INSTAGRAM_DOWNLOADER_URL=${INSTAGRAM_DOWNLOADER_URL}
|
||||||
- VK_DOWNLOADER_URL=${VK_DOWNLOADER_URL}
|
- VK_DOWNLOADER_URL=${VK_DOWNLOADER_URL}
|
||||||
volumes:
|
volumes:
|
||||||
- ./video:/app/video
|
- ./video:/app/video
|
||||||
- ./instagram_cookies.txt:/app/instagram_cookies.txt
|
|
||||||
- ./data:/app/data:Z
|
- ./data:/app/data:Z
|
||||||
network_mode: host
|
network_mode: host
|
||||||
|
|
||||||
networks:
|
|
||||||
bot_network:
|
|
||||||
driver: bridge
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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 1799925684 _ga_B9CY1C9VBC GS2.1.s1765365263$o1$g1$t1765365684$j60$l0$h0
|
||||||
.mozilla.org TRUE / FALSE 1799925263 _ga GA1.2.1451822324.1765365263
|
.mozilla.org TRUE / FALSE 1799925263 _ga GA1.2.1451822324.1765365263
|
||||||
.mozilla.org TRUE / FALSE 1765451663 _gid GA1.2.878207985.1765365263
|
.mozilla.org TRUE / FALSE 1765451663 _gid GA1.2.878207985.1765365263
|
||||||
.instagram.com TRUE / TRUE 1799949717 csrftoken CnChQ6nTz8cfm_U7q2ur9w
|
.instagram.com TRUE / TRUE 1799964275 csrftoken CnChQ6nTz8cfm_U7q2ur9w
|
||||||
.instagram.com TRUE / TRUE 1799925292 datr LFY5aVDEvvzQRTypNm_NZ0d3
|
.instagram.com TRUE / TRUE 1799925292 datr LFY5aVDEvvzQRTypNm_NZ0d3
|
||||||
.instagram.com TRUE / TRUE 1796901312 ig_did B0879634-89D6-4098-9B3E-958B6BC00183
|
.instagram.com TRUE / TRUE 1796901312 ig_did B0879634-89D6-4098-9B3E-958B6BC00183
|
||||||
.instagram.com TRUE / TRUE 1765970112 dpr 2
|
.instagram.com TRUE / TRUE 1765970112 dpr 2
|
||||||
.instagram.com TRUE / TRUE 1799925293 mid aTlWLAAEAAEBRoS_PfrA_i5UP0w1
|
.instagram.com TRUE / TRUE 1799925293 mid aTlWLAAEAAEBRoS_PfrA_i5UP0w1
|
||||||
.instagram.com TRUE / TRUE 1765980504 wd 1920x944
|
.instagram.com TRUE / TRUE 1766008776 wd 1920x944
|
||||||
.instagram.com TRUE / TRUE 1796911697 sessionid 42059678244%3AD0GdfKmaFZWqXp%3A10%3AAYieDJrvoWIE9WW--tzjgv-3EyrgI9XT6seopSdHFw
|
.instagram.com TRUE / TRUE 1796939890 sessionid 42059678244%3AD0GdfKmaFZWqXp%3A10%3AAYgpCODjycI3EWMR6G5Uh6kXjroGZ6pb1IRJmXGX3g
|
||||||
.instagram.com TRUE / TRUE 1773165717 ds_user_id 42059678244
|
.instagram.com TRUE / TRUE 1773180275 ds_user_id 42059678244
|
||||||
.instagram.com TRUE / TRUE 0 rur "LDC\05442059678244\0541796925717:01fef99bf0a6a2eec6207d44260971856a393246d5f7590afa05e8b7183b7b873f243693"
|
.instagram.com TRUE / TRUE 0 rur "LDC\05442059678244\0541796940275:01fef3bd7d6beb547023be3c45c01ebfd5726050a4cced10a3a4af9ff39a731103df7c88"
|
||||||
addons.mozilla.org FALSE / TRUE 0 taarId 4dffa50e49cca797bb48f2f4f11803c251746ad45af1fef3ba1ad37379a24fea
|
addons.mozilla.org FALSE / TRUE 0 taarId 4dffa50e49cca797bb48f2f4f11803c251746ad45af1fef3ba1ad37379a24fea
|
||||||
|
.facebook.com TRUE / TRUE 1799963979 datr S-05aRMEAJEaLLwYCMb4y3JM
|
||||||
|
.facebook.com TRUE / TRUE 1766008781 wd 1920x944
|
||||||
|
.facebook.com TRUE / TRUE 1766008795 dpr 2
|
||||||
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
|
python-telegram-bot==20.7
|
||||||
yt-dlp>=2024.12.13
|
|
||||||
requests==2.31.0
|
|
||||||
httpx==0.25.2
|
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