Добавлен TikTok загрузчик: обновлены конфигурации, добавлены функции для скачивания видео с TikTok и обновлены текстовые сообщения для поддержки нового источника.

This commit is contained in:
vrubelroman 2025-12-12 12:36:23 +03:00
parent 95d1ce4f9a
commit da98462bbc
8 changed files with 315 additions and 0 deletions

View file

@ -14,3 +14,4 @@ VK_DOWNLOADER_URL=http://localhost:5555
# INSTAGRAM_DOWNLOADER_URL=http://instagram-downloader:5000 # INSTAGRAM_DOWNLOADER_URL=http://instagram-downloader:5000
# VK_DOWNLOADER_URL=http://vk-downloader:5000 # VK_DOWNLOADER_URL=http://vk-downloader:5000
YAPFILES_DOWNLOADER_URL=http://localhost:5558 YAPFILES_DOWNLOADER_URL=http://localhost:5558
TIKTOK_DOWNLOADER_URL=http://localhost:5559

67
bot.py
View file

@ -28,6 +28,7 @@ YOUTUBE_DOWNLOADER_URL = os.getenv('YOUTUBE_DOWNLOADER_URL', 'http://localhost:5
INSTAGRAM_DOWNLOADER_URL = os.getenv('INSTAGRAM_DOWNLOADER_URL', 'http://localhost:5556') 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')
YAPFILES_DOWNLOADER_URL = os.getenv('YAPFILES_DOWNLOADER_URL', 'http://localhost:5558') YAPFILES_DOWNLOADER_URL = os.getenv('YAPFILES_DOWNLOADER_URL', 'http://localhost:5558')
TIKTOK_DOWNLOADER_URL = os.getenv('TIKTOK_DOWNLOADER_URL', 'http://localhost:5559')
# Базовая директория проекта (абсолютный путь), чтобы не зависеть от рабочей директории процесса # Базовая директория проекта (абсолютный путь), чтобы не зависеть от рабочей директории процесса
BASE_DIR = Path(__file__).resolve().parent BASE_DIR = Path(__file__).resolve().parent
@ -53,6 +54,7 @@ TEXTS = {
"Поддерживаемые источники:\n" "Поддерживаемые источники:\n"
"• YouTube (youtube.com, youtu.be)\n" "• YouTube (youtube.com, youtu.be)\n"
"• Instagram (instagram.com)\n" "• Instagram (instagram.com)\n"
"• TikTok (tiktok.com)\n"
"• VK (vk.com)\n" "• VK (vk.com)\n"
"• Yapfiles (yapfiles.ru)\n\n" "• Yapfiles (yapfiles.ru)\n\n"
"👥 Работа в группах:\n" "👥 Работа в группах:\n"
@ -70,6 +72,7 @@ TEXTS = {
"Этот бот позволяет скачивать видео из популярных источников:\n" "Этот бот позволяет скачивать видео из популярных источников:\n"
"• YouTube — видео и shorts\n" "• YouTube — видео и shorts\n"
"• Instagram — reels и посты с видео\n" "• Instagram — reels и посты с видео\n"
"• TikTok — видео\n"
"• VK — видеозаписи\n" "• VK — видеозаписи\n"
"• Yapfiles — видеофайлы\n\n" "• Yapfiles — видеофайлы\n\n"
"🔧 <b>Как использовать:</b>\n" "🔧 <b>Как использовать:</b>\n"
@ -88,6 +91,7 @@ TEXTS = {
"Поддерживаемые источники:\n" "Поддерживаемые источники:\n"
"• YouTube (youtube.com, youtu.be)\n" "• YouTube (youtube.com, youtu.be)\n"
"• Instagram (instagram.com)\n" "• Instagram (instagram.com)\n"
"• TikTok (tiktok.com)\n"
"• VK (vk.com)\n" "• VK (vk.com)\n"
"• Yapfiles (yapfiles.ru)\n\n" "• Yapfiles (yapfiles.ru)\n\n"
"Для других источников: Пардон, не умеем 😅" "Для других источников: Пардон, не умеем 😅"
@ -107,6 +111,7 @@ TEXTS = {
"Supported sources:\n" "Supported sources:\n"
"• YouTube (youtube.com, youtu.be)\n" "• YouTube (youtube.com, youtu.be)\n"
"• Instagram (instagram.com)\n" "• Instagram (instagram.com)\n"
"• TikTok (tiktok.com)\n"
"• VK (vk.com)\n" "• VK (vk.com)\n"
"• Yapfiles (yapfiles.ru)\n\n" "• Yapfiles (yapfiles.ru)\n\n"
"👥 Group usage:\n" "👥 Group usage:\n"
@ -124,6 +129,7 @@ TEXTS = {
"This bot allows you to download videos from popular sources:\n" "This bot allows you to download videos from popular sources:\n"
"• YouTube — videos and shorts\n" "• YouTube — videos and shorts\n"
"• Instagram — reels and video posts\n" "• Instagram — reels and video posts\n"
"• TikTok — videos\n"
"• VK — video recordings\n" "• VK — video recordings\n"
"• Yapfiles — video files\n\n" "• Yapfiles — video files\n\n"
"🔧 <b>How to use:</b>\n" "🔧 <b>How to use:</b>\n"
@ -142,6 +148,7 @@ TEXTS = {
"Supported sources:\n" "Supported sources:\n"
"• YouTube (youtube.com, youtu.be)\n" "• YouTube (youtube.com, youtu.be)\n"
"• Instagram (instagram.com)\n" "• Instagram (instagram.com)\n"
"• TikTok (tiktok.com)\n"
"• VK (vk.com)\n" "• VK (vk.com)\n"
"• Yapfiles (yapfiles.ru)\n\n" "• Yapfiles (yapfiles.ru)\n\n"
"Other sources: Sorry, not supported 😅" "Other sources: Sorry, not supported 😅"
@ -326,6 +333,8 @@ def detect_video_source(url: str) -> str:
return 'vk' return 'vk'
elif 'yapfiles.ru' in domain: elif 'yapfiles.ru' in domain:
return 'yapfiles' return 'yapfiles'
elif 'tiktok.com' in domain:
return 'tiktok'
else: else:
return 'unknown' return 'unknown'
@ -582,6 +591,62 @@ async def download_yapfiles_video(url: str, chat_id: int, max_retries: int = 3)
raise last_error or Exception("Неизвестная ошибка при скачивании с Yapfiles через внешний сервис") raise last_error or Exception("Неизвестная ошибка при скачивании с Yapfiles через внешний сервис")
async def download_tiktok_video(url: str, chat_id: int, max_retries: int = 3) -> str:
"""Скачивает видео с TikTok через внешний сервис"""
logger.info(f"TikTok: отправка запроса на внешний сервис {TIKTOK_DOWNLOADER_URL}")
last_error = None
for attempt in range(max_retries):
try:
async with httpx.AsyncClient(timeout=600.0) as client:
response = await client.post(
f"{TIKTOK_DOWNLOADER_URL}/download/stream",
json={"url": url},
headers={"Content-Type": "application/json"}
)
if response.status_code != 200:
error_text = response.text
try:
error_json = response.json()
error_text = error_json.get('error', error_text)
except:
pass
raise Exception(f"TikTok сервис вернул ошибку {response.status_code}: {error_text}")
video_data = response.content
video_ext = 'mp4'
content_type = response.headers.get('Content-Type', '')
if 'video/' in content_type:
video_ext = content_type.split('/')[-1].split(';')[0]
filename = response.headers.get('Content-Disposition', '')
if filename and 'filename=' in filename:
video_filename = filename.split('filename=')[1].strip('"\'')
else:
video_filename = f'{chat_id}_tiktok_video.{video_ext}'
video_path = DOWNLOADS_DIR / video_filename
with open(video_path, 'wb') as f:
f.write(video_data)
logger.info(f"TikTok: видео скачано через внешний сервис: {video_path}")
return str(video_path)
except httpx.TimeoutException:
last_error = Exception(f"Таймаут при запросе к TikTok сервису (попытка {attempt + 1}/{max_retries})")
logger.warning(f"TikTok: таймаут при запросе к сервису: {last_error}")
except Exception as e:
last_error = e
logger.warning(f"TikTok: попытка {attempt + 1}/{max_retries} не удалась: {e}")
if attempt < max_retries - 1:
await asyncio.sleep((attempt + 1) * 2)
raise last_error or Exception("Неизвестная ошибка при скачивании с TikTok через внешний сервис")
async def download_video(url: str, chat_id: int, locale: str, max_retries: int = 3) -> str: async def download_video(url: str, chat_id: int, locale: str, max_retries: int = 3) -> str:
"""Главная функция скачивания - вызывает нужную функцию в зависимости от источника""" """Главная функция скачивания - вызывает нужную функцию в зависимости от источника"""
source = detect_video_source(url) source = detect_video_source(url)
@ -595,6 +660,8 @@ async def download_video(url: str, chat_id: int, locale: str, max_retries: int =
return await download_vk_video(url, chat_id, max_retries) return await download_vk_video(url, chat_id, max_retries)
elif source == 'yapfiles': elif source == 'yapfiles':
return await download_yapfiles_video(url, chat_id, max_retries) return await download_yapfiles_video(url, chat_id, max_retries)
elif source == 'tiktok':
return await download_tiktok_video(url, chat_id, max_retries)
else: else:
raise Exception(get_text(locale, 'error_unknown_source')) raise Exception(get_text(locale, 'error_unknown_source'))

View file

@ -12,6 +12,7 @@ services:
- INSTAGRAM_DOWNLOADER_URL=${INSTAGRAM_DOWNLOADER_URL} - INSTAGRAM_DOWNLOADER_URL=${INSTAGRAM_DOWNLOADER_URL}
- VK_DOWNLOADER_URL=${VK_DOWNLOADER_URL} - VK_DOWNLOADER_URL=${VK_DOWNLOADER_URL}
- YAPFILES_DOWNLOADER_URL=${YAPFILES_DOWNLOADER_URL} - YAPFILES_DOWNLOADER_URL=${YAPFILES_DOWNLOADER_URL}
- TIKTOK_DOWNLOADER_URL=${TIKTOK_DOWNLOADER_URL}
volumes: volumes:
- ./video:/app/video - ./video:/app/video
- ./data:/app/data:Z - ./data:/app/data:Z

View file

@ -0,0 +1,22 @@
FROM python:3.11-slim
WORKDIR /app
# Установка зависимостей системы
RUN apt-get update && apt-get install -y --no-install-recommends \
ffmpeg \
&& rm -rf /var/lib/apt/lists/*
# Установка зависимостей Python
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Копирование приложения
COPY app.py .
# Создание директории для загрузок
RUN mkdir -p downloads
# Запуск приложения
CMD ["python", "app.py"]

View file

@ -0,0 +1,47 @@
# TikTok Video Downloader
Микросервис для скачивания видео с TikTok
## Порт
- Внутренний порт: 5000
- Внешний порт: 5559
## API
### Health Check
```
GET /health
```
Ответ:
```json
{"status": "ok", "service": "tiktok-downloader"}
```
### Скачать видео
```
POST /download/stream
Content-Type: application/json
{
"url": "https://www.tiktok.com/@username/video/1234567890"
}
```
Возвращает бинарные данные видео.
## Запуск
```bash
docker-compose up -d --build
```
## Поддерживаемые URL
- `https://www.tiktok.com/@username/video/1234567890`
- `https://vm.tiktok.com/ZM...` (короткие ссылки)
- `https://m.tiktok.com/v/1234567890`

156
tiktok-downloader/app.py Normal file
View file

@ -0,0 +1,156 @@
"""
TikTok Video Downloader Service
Отдельный микросервис для скачивания видео с TikTok
"""
import os
import logging
import re
import uuid
from pathlib import Path
from flask import Flask, request, jsonify
from flask_cors import CORS
import yt_dlp
# Настройка логирования
logging.basicConfig(
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
level=logging.INFO
)
logger = logging.getLogger(__name__)
app = Flask(__name__)
CORS(app)
# Директория для временных файлов
DOWNLOADS_DIR = Path('downloads')
DOWNLOADS_DIR.mkdir(exist_ok=True)
# User-Agent для TikTok
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'
def download_tiktok_video(url: str, max_retries: int = 3) -> Path:
"""Скачивает видео с TikTok"""
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,
'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',
},
}
with yt_dlp.YoutubeDL(ydl_opts_info) as ydl:
info = ydl.extract_info(url, download=False)
video_title = info.get('title', 'tiktok_video')
logger.info(f"TikTok: получена информация о видео: {video_title}")
# Безопасное имя файла
safe_title = re.sub(r'[<>:"/\\|?*]', '', video_title)[:80]
output_template = str(DOWNLOADS_DIR / f'{uuid.uuid4()}_{safe_title}.%(ext)s')
# Скачиваем видео
ydl_opts_download = {
'format': 'best[ext=mp4]/best',
'outtmpl': output_template,
'quiet': False,
'no_warnings': False,
'user_agent': USER_AGENT,
'socket_timeout': 30,
'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',
},
}
logger.info(f"TikTok: начинаем скачивание (попытка {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"TikTok: попытка {attempt + 1}/{max_retries} не удалась: {e}")
if attempt < max_retries - 1:
import time
time.sleep((attempt + 1) * 2)
raise last_error or Exception("Неизвестная ошибка при скачивании с TikTok")
@app.route('/health', methods=['GET'])
def health():
"""Health check endpoint"""
return jsonify({'status': 'ok', 'service': 'tiktok-downloader'}), 200
@app.route('/download/stream', methods=['POST'])
def download_stream():
"""Скачивает видео с TikTok и возвращает бинарные данные"""
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}")
# Проверяем, что это TikTok URL
if 'tiktok.com' not in url:
return jsonify({'error': 'Only TikTok URLs are supported'}), 400
# Скачиваем видео
video_path = download_tiktok_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 'tiktok_video.mp4'
if not safe_filename.endswith(('.mp4', '.webm', '.mkv')):
safe_filename = 'tiktok_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"Запуск TikTok Downloader сервиса на {host}:{port}")
app.run(host=host, port=port, debug=False)

View file

@ -0,0 +1,16 @@
services:
tiktok-downloader:
build: .
container_name: tiktok_downloader_service
restart: unless-stopped
ports:
- "5559:5000"
volumes:
- ./downloads:/app/downloads
networks:
- tiktok_network
networks:
tiktok_network:
driver: bridge

View file

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