diff --git a/.env.example b/.env.example
index 9d87bd5..379b87a 100644
--- a/.env.example
+++ b/.env.example
@@ -14,3 +14,4 @@ VK_DOWNLOADER_URL=http://localhost:5555
# INSTAGRAM_DOWNLOADER_URL=http://instagram-downloader:5000
# VK_DOWNLOADER_URL=http://vk-downloader:5000
YAPFILES_DOWNLOADER_URL=http://localhost:5558
+TIKTOK_DOWNLOADER_URL=http://localhost:5559
diff --git a/bot.py b/bot.py
index 20b0d2f..5d82b85 100644
--- a/bot.py
+++ b/bot.py
@@ -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')
VK_DOWNLOADER_URL = os.getenv('VK_DOWNLOADER_URL', 'http://localhost:5555')
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
@@ -53,6 +54,7 @@ TEXTS = {
"Поддерживаемые источники:\n"
"• YouTube (youtube.com, youtu.be)\n"
"• Instagram (instagram.com)\n"
+ "• TikTok (tiktok.com)\n"
"• VK (vk.com)\n"
"• Yapfiles (yapfiles.ru)\n\n"
"👥 Работа в группах:\n"
@@ -70,6 +72,7 @@ TEXTS = {
"Этот бот позволяет скачивать видео из популярных источников:\n"
"• YouTube — видео и shorts\n"
"• Instagram — reels и посты с видео\n"
+ "• TikTok — видео\n"
"• VK — видеозаписи\n"
"• Yapfiles — видеофайлы\n\n"
"🔧 Как использовать:\n"
@@ -88,6 +91,7 @@ TEXTS = {
"Поддерживаемые источники:\n"
"• YouTube (youtube.com, youtu.be)\n"
"• Instagram (instagram.com)\n"
+ "• TikTok (tiktok.com)\n"
"• VK (vk.com)\n"
"• Yapfiles (yapfiles.ru)\n\n"
"Для других источников: Пардон, не умеем 😅"
@@ -107,6 +111,7 @@ TEXTS = {
"Supported sources:\n"
"• YouTube (youtube.com, youtu.be)\n"
"• Instagram (instagram.com)\n"
+ "• TikTok (tiktok.com)\n"
"• VK (vk.com)\n"
"• Yapfiles (yapfiles.ru)\n\n"
"👥 Group usage:\n"
@@ -124,6 +129,7 @@ TEXTS = {
"This bot allows you to download videos from popular sources:\n"
"• YouTube — videos and shorts\n"
"• Instagram — reels and video posts\n"
+ "• TikTok — videos\n"
"• VK — video recordings\n"
"• Yapfiles — video files\n\n"
"🔧 How to use:\n"
@@ -142,6 +148,7 @@ TEXTS = {
"Supported sources:\n"
"• YouTube (youtube.com, youtu.be)\n"
"• Instagram (instagram.com)\n"
+ "• TikTok (tiktok.com)\n"
"• VK (vk.com)\n"
"• Yapfiles (yapfiles.ru)\n\n"
"Other sources: Sorry, not supported 😅"
@@ -326,6 +333,8 @@ def detect_video_source(url: str) -> str:
return 'vk'
elif 'yapfiles.ru' in domain:
return 'yapfiles'
+ elif 'tiktok.com' in domain:
+ return 'tiktok'
else:
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 через внешний сервис")
+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:
"""Главная функция скачивания - вызывает нужную функцию в зависимости от источника"""
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)
elif source == 'yapfiles':
return await download_yapfiles_video(url, chat_id, max_retries)
+ elif source == 'tiktok':
+ return await download_tiktok_video(url, chat_id, max_retries)
else:
raise Exception(get_text(locale, 'error_unknown_source'))
diff --git a/docker-compose.yml b/docker-compose.yml
index e49260f..0825957 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -12,6 +12,7 @@ services:
- INSTAGRAM_DOWNLOADER_URL=${INSTAGRAM_DOWNLOADER_URL}
- VK_DOWNLOADER_URL=${VK_DOWNLOADER_URL}
- YAPFILES_DOWNLOADER_URL=${YAPFILES_DOWNLOADER_URL}
+ - TIKTOK_DOWNLOADER_URL=${TIKTOK_DOWNLOADER_URL}
volumes:
- ./video:/app/video
- ./data:/app/data:Z
diff --git a/tiktok-downloader/Dockerfile b/tiktok-downloader/Dockerfile
new file mode 100644
index 0000000..8edf75d
--- /dev/null
+++ b/tiktok-downloader/Dockerfile
@@ -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"]
+
diff --git a/tiktok-downloader/README.md b/tiktok-downloader/README.md
new file mode 100644
index 0000000..8dba668
--- /dev/null
+++ b/tiktok-downloader/README.md
@@ -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`
+
diff --git a/tiktok-downloader/app.py b/tiktok-downloader/app.py
new file mode 100644
index 0000000..4c6e4ec
--- /dev/null
+++ b/tiktok-downloader/app.py
@@ -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)
+
diff --git a/tiktok-downloader/docker-compose.yml b/tiktok-downloader/docker-compose.yml
new file mode 100644
index 0000000..8fe8a7d
--- /dev/null
+++ b/tiktok-downloader/docker-compose.yml
@@ -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
+
diff --git a/tiktok-downloader/requirements.txt b/tiktok-downloader/requirements.txt
new file mode 100644
index 0000000..8962917
--- /dev/null
+++ b/tiktok-downloader/requirements.txt
@@ -0,0 +1,5 @@
+Flask==3.0.0
+flask-cors==4.0.0
+yt-dlp>=2024.12.13
+requests==2.31.0
+