2025-12-10 14:46:09 +03:00
import os
import re
import logging
import asyncio
2025-12-10 15:36:27 +03:00
import sqlite3
2025-12-10 17:02:01 +03:00
import time
2025-12-10 14:46:09 +03:00
from pathlib import Path
from urllib . parse import urlparse
2025-12-10 15:36:27 +03:00
from datetime import datetime
2025-12-10 14:46:09 +03:00
2025-12-10 16:14:26 +03:00
import httpx
2025-12-12 15:41:46 +03:00
from telegram import Update , Message
from telegram . ext import Application , MessageHandler , filters , ContextTypes , CommandHandler , Defaults
from telegram . request import HTTPXRequest
from dataclasses import dataclass
from typing import Optional
# Таймаут для HTTP запросов
# В с е таймауты убраны - видео может качаться и отправляться очень долго
HTTP_TIMEOUT = httpx . Timeout ( connect = None , read = None , write = None , pool = None )
2025-12-10 14:46:09 +03:00
# Настройка логирования
logging . basicConfig (
format = ' %(asctime)s - %(name)s - %(levelname)s - %(message)s ' ,
level = logging . INFO
)
logger = logging . getLogger ( __name__ )
2025-12-10 15:14:01 +03:00
# Токен бота и имя бота из переменных окружения
2025-12-10 14:46:09 +03:00
TELEGRAM_BOT_TOKEN = os . getenv ( ' TELEGRAM_BOT_TOKEN ' )
2025-12-10 15:14:01 +03:00
TELEGRAM_BOT_USERNAME = os . getenv ( ' TELEGRAM_BOT_USERNAME ' , ' vrubelVideoDownload_bot ' )
2025-12-11 01:07:04 +03:00
# URL сервисов для скачивания видео
YOUTUBE_DOWNLOADER_URL = os . getenv ( ' YOUTUBE_DOWNLOADER_URL ' , ' http://localhost:5557 ' )
INSTAGRAM_DOWNLOADER_URL = os . getenv ( ' INSTAGRAM_DOWNLOADER_URL ' , ' http://localhost:5556 ' )
2025-12-10 16:14:26 +03:00
VK_DOWNLOADER_URL = os . getenv ( ' VK_DOWNLOADER_URL ' , ' http://localhost:5555 ' )
2025-12-12 10:32:06 +03:00
YAPFILES_DOWNLOADER_URL = os . getenv ( ' YAPFILES_DOWNLOADER_URL ' , ' http://localhost:5558 ' )
2025-12-12 12:36:23 +03:00
TIKTOK_DOWNLOADER_URL = os . getenv ( ' TIKTOK_DOWNLOADER_URL ' , ' http://localhost:5559 ' )
2025-12-10 14:46:09 +03:00
2025-12-10 15:36:27 +03:00
# Базовая директория проекта (абсолютный путь), чтобы не зависеть от рабочей директории процесса
BASE_DIR = Path ( __file__ ) . resolve ( ) . parent
2025-12-10 14:46:09 +03:00
# Директория для временных файлов
2025-12-10 15:36:27 +03:00
DOWNLOADS_DIR = BASE_DIR / ' video '
DOWNLOADS_DIR . mkdir ( parents = True , exist_ok = True )
2025-12-10 14:46:09 +03:00
2025-12-10 15:36:27 +03:00
# База данных (внутри папки data)
DATA_DIR = BASE_DIR / ' data '
DATA_DIR . mkdir ( parents = True , exist_ok = True )
DB_FILE = DATA_DIR / ' bot.db '
2025-12-10 14:46:09 +03:00
2025-12-12 11:11:51 +03:00
# ============================================================================
# ЛОКАЛИЗАЦИЯ
# ============================================================================
TEXTS = {
' ru ' : {
' start ' : (
" 👋 Привет! Я бот для скачивания видео. \n \n "
" Просто отправь мне ссылку на видео, и я скачаю е г о для тебя. \n \n "
" Поддерживаемые источники: \n "
" • YouTube (youtube.com, youtu.be) \n "
" • Instagram (instagram.com) \n "
2025-12-12 12:36:23 +03:00
" • TikTok (tiktok.com) \n "
2025-12-12 11:11:51 +03:00
" • VK (vk.com) \n "
" • Yapfiles (yapfiles.ru) \n \n "
" 👥 Работа в группах: \n "
" Добавь меня в группу и дай права администратора (нужно право на удаление сообщений). "
" После этого я буду автоматически находить ссылки на видео в сообщениях участников, "
" скачивать их и отправлять прямо в группу, заменяя исходное сообщение с о ссылкой. \n \n "
" Команды: \n "
" /start - Начать работу \n "
" /stat - Статистика бота \n "
" /support - Поддержка и информация \n \n "
" Отправь ссылку на видео: "
) ,
' support ' : (
" ℹ ️ <b>О боте</b>\n \n "
" Этот бот позволяет скачивать видео из популярных источников: \n "
" • YouTube — видео и shorts \n "
" • Instagram — reels и посты с видео \n "
2025-12-12 12:36:23 +03:00
" • TikTok — видео \n "
2025-12-12 11:11:51 +03:00
" • VK — видеозаписи \n "
" • Yapfiles — видеофайлы \n \n "
" 🔧 <b>Как использовать:</b> \n "
" 1. Отправьте ссылку на видео в личный чат с ботом \n "
" 2. Дождитесь скачивания \n "
" 3. Получите видео прямо в Telegram! \n \n "
" 👥 <b>В группах:</b> \n "
" Добавьте бота в группу с правами администратора — "
" он будет автоматически скачивать видео из сообщений участников. \n \n "
" ❓ Есть вопросы или предложения? \n "
" Пишите автору: @rvrubel "
) ,
' stat ' : " 📊 Статистика бота: \n \n 👥 В с е г о пользователей: {users} \n 📹 В с е г о скачано видео: {downloads} " ,
' send_link ' : (
" Пожалуйста, отправьте ссылку на видео. \n "
" Поддерживаемые источники: \n "
" • YouTube (youtube.com, youtu.be) \n "
" • Instagram (instagram.com) \n "
2025-12-12 12:36:23 +03:00
" • TikTok (tiktok.com) \n "
2025-12-12 11:11:51 +03:00
" • VK (vk.com) \n "
" • Yapfiles (yapfiles.ru) \n \n "
" Для других источников: Пардон, не умеем 😅 "
) ,
' unsupported_source ' : " Пардон, не умеем работать с этим источником 😅 " ,
' processing ' : " 🔍 Обрабатываю ссылку... " ,
' downloading ' : " ⬇️ Скачиваю видео... " ,
' sending ' : " 📤 Отправляю видео... " ,
' caption ' : " Видео скачано с @ {bot_username} " ,
' error ' : " ❌ Произошла ошибка при обработке видео: \n {error} " ,
' error_unknown_source ' : " Пардон, не умеем работать с этим источником " ,
2025-12-12 15:41:46 +03:00
' error_file_too_large ' : " ❌ Видео слишком большое ( {size_mb:.1f} МБ) \n \n Telegram Bot API позволяет отправлять файлы до 50 МБ. \n \n Попробуйте выбрать видео покороче или в меньшем качестве. " ,
' queue_position ' : " 🕐 Ваше видео # {position} в очереди \n Ваш запрос очень важен для нас! " ,
' queue_first ' : " ⬇️ Скачиваю видео... " ,
2025-12-12 11:11:51 +03:00
} ,
' en ' : {
' start ' : (
" 👋 Hi! I ' m a video download bot. \n \n "
" Just send me a video link, and I ' ll download it for you. \n \n "
" Supported sources: \n "
" • YouTube (youtube.com, youtu.be) \n "
" • Instagram (instagram.com) \n "
2025-12-12 12:36:23 +03:00
" • TikTok (tiktok.com) \n "
2025-12-12 11:11:51 +03:00
" • VK (vk.com) \n "
" • Yapfiles (yapfiles.ru) \n \n "
" 👥 Group usage: \n "
" Add me to a group with admin rights (message deletion required). "
" I ' ll automatically find video links in messages, "
" download them and send directly to the group. \n \n "
" Commands: \n "
" /start - Get started \n "
" /stat - Bot statistics \n "
" /support - Support and info \n \n "
" Send a video link: "
) ,
' support ' : (
" ℹ ️ <b>About the bot</b>\n \n "
" This bot allows you to download videos from popular sources: \n "
" • YouTube — videos and shorts \n "
" • Instagram — reels and video posts \n "
2025-12-12 12:36:23 +03:00
" • TikTok — videos \n "
2025-12-12 11:11:51 +03:00
" • VK — video recordings \n "
" • Yapfiles — video files \n \n "
" 🔧 <b>How to use:</b> \n "
" 1. Send a video link in a private chat with the bot \n "
" 2. Wait for the download \n "
" 3. Get the video right in Telegram! \n \n "
" 👥 <b>In groups:</b> \n "
" Add the bot to a group with admin rights — "
" it will automatically download videos from participants ' messages. \n \n "
" ❓ Questions or suggestions? \n "
" Contact the author: @rvrubel "
) ,
' stat ' : " 📊 Bot statistics: \n \n 👥 Total users: {users} \n 📹 Total downloads: {downloads} " ,
' send_link ' : (
" Please send a video link. \n "
" Supported sources: \n "
" • YouTube (youtube.com, youtu.be) \n "
" • Instagram (instagram.com) \n "
2025-12-12 12:36:23 +03:00
" • TikTok (tiktok.com) \n "
2025-12-12 11:11:51 +03:00
" • VK (vk.com) \n "
" • Yapfiles (yapfiles.ru) \n \n "
" Other sources: Sorry, not supported 😅 "
) ,
' unsupported_source ' : " Sorry, this source is not supported 😅 " ,
' processing ' : " 🔍 Processing link... " ,
' downloading ' : " ⬇️ Downloading video... " ,
' sending ' : " 📤 Sending video... " ,
' caption ' : " Video downloaded via @ {bot_username} " ,
' error ' : " ❌ Error processing video: \n {error} " ,
' error_unknown_source ' : " Sorry, this source is not supported " ,
2025-12-12 15:41:46 +03:00
' error_file_too_large ' : " ❌ Video is too large ( {size_mb:.1f} MB) \n \n Telegram Bot API allows files up to 50 MB. \n \n Try a shorter video or lower quality. " ,
' queue_position ' : " 🕐 Your video is # {position} in queue \n Your request is very important to us! " ,
' queue_first ' : " ⬇️ Downloading video... " ,
2025-12-12 11:11:51 +03:00
}
}
def get_locale_from_language_code ( language_code : str | None ) - > str :
""" Определяет локаль на основе language_code из Telegram """
if language_code and language_code . lower ( ) . startswith ( ' ru ' ) :
return ' ru '
return ' en '
def get_text ( locale : str , key : str , * * kwargs ) - > str :
""" Возвращает локализованный текст """
if locale not in TEXTS :
locale = ' en '
text = TEXTS [ locale ] . get ( key , TEXTS [ ' en ' ] . get ( key , key ) )
if kwargs :
text = text . format ( * * kwargs )
return text
# ============================================================================
# БАЗА ДАННЫХ
# ============================================================================
2025-12-10 21:05:27 +03:00
2025-12-10 15:36:27 +03:00
def init_database ( ) :
""" Инициализирует базу данных и создает таблицы если их нет """
try :
conn = sqlite3 . connect ( str ( DB_FILE ) )
cursor = conn . cursor ( )
# Таблица пользователей
cursor . execute ( '''
CREATE TABLE IF NOT EXISTS users (
chat_id INTEGER PRIMARY KEY ,
username TEXT ,
first_name TEXT ,
first_seen TEXT NOT NULL ,
last_seen TEXT NOT NULL
)
''' )
2025-12-12 11:11:51 +03:00
# Проверяем, есть ли колонка locale (миграция для существующей базы)
cursor . execute ( " PRAGMA table_info(users) " )
columns = [ col [ 1 ] for col in cursor . fetchall ( ) ]
if ' locale ' not in columns :
cursor . execute ( " ALTER TABLE users ADD COLUMN locale TEXT DEFAULT ' en ' " )
logger . info ( " Добавлена колонка locale в таблицу users " )
2025-12-10 15:36:27 +03:00
# Таблица статистики
cursor . execute ( '''
CREATE TABLE IF NOT EXISTS stats (
id INTEGER PRIMARY KEY CHECK ( id = 1 ) ,
total_downloads INTEGER DEFAULT 0
)
''' )
# Инициализируем stats если е г о нет
cursor . execute ( ' SELECT COUNT(*) FROM stats ' )
if cursor . fetchone ( ) [ 0 ] == 0 :
cursor . execute ( ' INSERT INTO stats (id, total_downloads) VALUES (1, 0) ' )
conn . commit ( )
conn . close ( )
logger . info ( " База данных инициализирована " )
except Exception as e :
logger . error ( f " Ошибка при инициализации базы данных: { e } " )
2025-12-10 14:46:09 +03:00
2025-12-12 11:11:51 +03:00
2025-12-10 15:36:27 +03:00
def get_total_downloads ( ) - > int :
""" Возвращает общее количество скачанных видео """
2025-12-10 14:46:09 +03:00
try :
2025-12-10 15:36:27 +03:00
conn = sqlite3 . connect ( str ( DB_FILE ) )
cursor = conn . cursor ( )
cursor . execute ( ' SELECT total_downloads FROM stats WHERE id = 1 ' )
result = cursor . fetchone ( )
conn . close ( )
return result [ 0 ] if result else 0
2025-12-10 14:46:09 +03:00
except Exception as e :
2025-12-10 15:36:27 +03:00
logger . error ( f " Ошибка при получении количества скачанных видео: { e } " )
return 0
2025-12-10 14:46:09 +03:00
2025-12-12 11:11:51 +03:00
2025-12-10 14:46:09 +03:00
def increment_downloads ( ) :
""" Увеличивает счетчик скачанных видео """
2025-12-10 15:36:27 +03:00
try :
conn = sqlite3 . connect ( str ( DB_FILE ) )
cursor = conn . cursor ( )
cursor . execute ( ' UPDATE stats SET total_downloads = total_downloads + 1 WHERE id = 1 ' )
conn . commit ( )
new_total = get_total_downloads ( )
conn . close ( )
logger . info ( f " Общее количество скачанных видео: { new_total } " )
except Exception as e :
logger . error ( f " Ошибка при увеличении счетчика скачанных видео: { e } " )
2025-12-12 11:11:51 +03:00
2025-12-10 15:36:27 +03:00
def get_total_users ( ) - > int :
""" Возвращает общее количество уникальных пользователей """
try :
conn = sqlite3 . connect ( str ( DB_FILE ) )
cursor = conn . cursor ( )
cursor . execute ( ' SELECT COUNT(*) FROM users ' )
result = cursor . fetchone ( )
conn . close ( )
return result [ 0 ] if result else 0
except Exception as e :
logger . error ( f " Ошибка при получении количества пользователей: { e } " )
return 0
2025-12-12 11:11:51 +03:00
def get_user_locale ( chat_id : int ) - > str :
""" Возвращает локаль пользователя из базы данных """
try :
conn = sqlite3 . connect ( str ( DB_FILE ) )
cursor = conn . cursor ( )
cursor . execute ( ' SELECT locale FROM users WHERE chat_id = ? ' , ( chat_id , ) )
result = cursor . fetchone ( )
conn . close ( )
return result [ 0 ] if result and result [ 0 ] else ' en '
except Exception as e :
logger . error ( f " Ошибка при получении локали пользователя: { e } " )
return ' en '
def add_user ( chat_id : int , username : str = None , first_name : str = None , locale : str = ' en ' ) :
2025-12-10 15:36:27 +03:00
""" Добавляет пользователя в базу данных или обновляет информацию о нем """
try :
now = datetime . now ( ) . isoformat ( )
conn = sqlite3 . connect ( str ( DB_FILE ) )
cursor = conn . cursor ( )
# Проверяем, существует ли пользователь
cursor . execute ( ' SELECT chat_id FROM users WHERE chat_id = ? ' , ( chat_id , ) )
exists = cursor . fetchone ( )
if exists :
2025-12-12 11:11:51 +03:00
# Обновляем last_seen и locale
2025-12-10 15:36:27 +03:00
cursor . execute (
2025-12-12 11:11:51 +03:00
' UPDATE users SET last_seen = ?, username = ?, first_name = ?, locale = ? WHERE chat_id = ? ' ,
( now , username , first_name , locale , chat_id )
2025-12-10 15:36:27 +03:00
)
else :
# Добавляем нового пользователя
cursor . execute (
2025-12-12 11:11:51 +03:00
' INSERT INTO users (chat_id, username, first_name, first_seen, last_seen, locale) VALUES (?, ?, ?, ?, ?, ?) ' ,
( chat_id , username , first_name , now , now , locale )
2025-12-10 15:36:27 +03:00
)
total_users = get_total_users ( )
2025-12-12 11:11:51 +03:00
logger . info ( f " Добавлен новый пользователь (chat_id: { chat_id } , locale: { locale } ). В с е г о пользователей: { total_users } " )
2025-12-10 15:36:27 +03:00
conn . commit ( )
conn . close ( )
except Exception as e :
logger . error ( f " Ошибка при добавлении пользователя: { e } " )
2025-12-10 15:14:01 +03:00
2025-12-10 14:46:09 +03:00
2025-12-12 11:11:51 +03:00
# ============================================================================
# УТИЛИТЫ
# ============================================================================
2025-12-10 14:46:09 +03:00
def detect_video_source ( url : str ) - > str :
""" Определяет источник видео по URL """
domain = urlparse ( url ) . netloc . lower ( )
if ' youtube.com ' in domain or ' youtu.be ' in domain :
return ' youtube '
elif ' instagram.com ' in domain :
return ' instagram '
elif ' vk.com ' in domain or ' vkontakte.ru ' in domain :
return ' vk '
2025-12-12 10:32:06 +03:00
elif ' yapfiles.ru ' in domain :
return ' yapfiles '
2025-12-12 12:36:23 +03:00
elif ' tiktok.com ' in domain :
return ' tiktok '
2025-12-10 14:46:09 +03:00
else :
return ' unknown '
2025-12-10 20:35:38 +03:00
def extract_urls_from_text ( text : str ) - > list [ str ] :
""" Извлекает все URL из текста сообщения """
url_pattern = r ' https?://[^ \ s<> " {} | \\ ^` \ [ \ ]]+ '
urls = re . findall ( url_pattern , text )
return urls
2025-12-11 01:07:04 +03:00
def cleanup_old_files ( ) :
""" Удаляет только .part файлы (недокачанные) из папки загрузок """
2025-12-10 21:05:27 +03:00
try :
for file_path in DOWNLOADS_DIR . glob ( ' * ' ) :
if not file_path . is_file ( ) :
continue
if file_path . suffix == ' .part ' :
try :
file_path . unlink ( )
logger . info ( f " Удален .part файл: { file_path . name } " )
except Exception as e :
logger . warning ( f " Н е удалось удалить .part файл { file_path . name } : { e } " )
except Exception as e :
2025-12-11 01:07:04 +03:00
logger . error ( f " Ошибка при очистке .part файлов: { e } " )
2025-12-10 14:46:09 +03:00
2025-12-12 15:41:46 +03:00
# ============================================================================
# СИСТЕМА ОЧЕРЕДЕЙ
# ============================================================================
@dataclass
class QueueItem :
""" Элемент очереди для скачивания видео """
original_message : Message
status_message : Message
url : str
chat_id : int
chat_type : str
locale : str
# Глобальная очередь и список элементов для отслеживания позиций
download_queue : asyncio . Queue = None
queue_items : list [ QueueItem ] = [ ]
queue_lock = asyncio . Lock ( )
async def update_queue_positions ( ) :
""" Обновляет статусы позиций в очереди для всех ожидающих """
async with queue_lock :
for i , item in enumerate ( queue_items ) :
position = i + 1
try :
if position == 1 :
# Первый в очереди - сейчас качается
await item . status_message . edit_text ( get_text ( item . locale , ' queue_first ' ) )
else :
# Остальные в очереди
await item . status_message . edit_text (
get_text ( item . locale , ' queue_position ' , position = position )
)
except Exception as e :
logger . warning ( f " Н е удалось обновить статус очереди: { e } " )
async def process_queue_item ( item : QueueItem ) :
""" Обрабатывает один элемент очереди """
try :
# Скачиваем видео
video_path = await download_video ( item . url , item . chat_id , item . locale )
# Проверяем размер файла (лимит Telegram Bot API - 50 МБ)
file_size = Path ( video_path ) . stat ( ) . st_size
max_size = 50 * 1024 * 1024 # 50 MB
if file_size > max_size :
size_mb = file_size / ( 1024 * 1024 )
error_msg = get_text ( item . locale , ' error_file_too_large ' , size_mb = size_mb )
await item . status_message . edit_text ( error_msg )
# Удаляем слишком большой файл
try :
Path ( video_path ) . unlink ( )
logger . info ( f " Удалён слишком большой файл: { video_path } ( { size_mb : .1f } MB) " )
except :
pass
return
# Отправляем файл пользователю
await item . status_message . edit_text ( get_text ( item . locale , ' sending ' ) )
video_file = open ( video_path , ' rb ' )
caption = get_text ( item . locale , ' caption ' , bot_username = TELEGRAM_BOT_USERNAME )
await item . original_message . reply_video (
video = video_file ,
caption = caption ,
supports_streaming = True
)
video_file . close ( )
# Увеличиваем счетчик скачанных видео
increment_downloads ( )
logger . info ( f " Видео сохранено: { video_path } " )
# Удаляем статусное сообщение и исходное сообщение с о ссылкой
try :
await item . status_message . delete ( )
await item . original_message . delete ( )
logger . info ( f " Удалено сообщение пользователя с ссылкой (chat_id: { item . chat_id } ) " )
except Exception as e :
logger . warning ( f " Н е удалось удалить сообщение: { e } " )
except Exception as e :
logger . error ( f " Ошибка при обработке { item . url } : { e } " )
error_msg = get_text ( item . locale , ' error ' , error = str ( e ) )
try :
await item . status_message . edit_text ( error_msg )
except :
try :
await item . original_message . reply_text ( error_msg )
except :
pass
try :
for part_file in DOWNLOADS_DIR . glob ( f ' { item . chat_id } _*.part ' ) :
part_file . unlink ( )
logger . info ( f " Удален .part файл после ошибки: { part_file . name } " )
except Exception as cleanup_error :
logger . warning ( f " Н е удалось удалить .part файлы после ошибки: { cleanup_error } " )
async def queue_worker ( ) :
""" Воркер, обрабатывающий очередь последовательно """
global download_queue , queue_items
logger . info ( " Воркер очереди запущен " )
while True :
try :
# Ждём элемент из очереди
item = await download_queue . get ( )
logger . info ( f " Начинаю обработку: { item . url } (в очереди: { len ( queue_items ) } ) " )
# Обновляем статусы - первый теперь качается
await update_queue_positions ( )
# Обрабатываем элемент
await process_queue_item ( item )
# Удаляем из списка отслеживания
async with queue_lock :
if item in queue_items :
queue_items . remove ( item )
# Обновляем позиции оставшихся
await update_queue_positions ( )
# Сообщаем что задача выполнена
download_queue . task_done ( )
logger . info ( f " Обработка завершена. Осталось в очереди: { len ( queue_items ) } " )
except Exception as e :
logger . error ( f " Ошибка в воркере очереди: { e } " )
async def add_to_queue ( item : QueueItem ) - > int :
""" Добавляет элемент в очередь и возвращает позицию """
global queue_items
async with queue_lock :
queue_items . append ( item )
position = len ( queue_items )
await download_queue . put ( item )
logger . info ( f " Добавлено в очередь: { item . url } , позиция: { position } " )
return position
2025-12-12 11:11:51 +03:00
# ============================================================================
# ФУНКЦИИ СКАЧИВАНИЯ
# ============================================================================
2025-12-10 14:46:09 +03:00
async def download_youtube_video ( url : str , chat_id : int , max_retries : int = 3 ) - > str :
2025-12-11 01:07:04 +03:00
""" Скачивает видео с YouTube через внешний сервис """
logger . info ( f " YouTube: отправка запроса на внешний сервис { YOUTUBE_DOWNLOADER_URL } " )
2025-12-10 14:46:09 +03:00
last_error = None
for attempt in range ( max_retries ) :
try :
2025-12-12 15:41:46 +03:00
async with httpx . AsyncClient ( timeout = HTTP_TIMEOUT ) as client :
2025-12-11 01:07:04 +03:00
response = await client . post (
f " { YOUTUBE_DOWNLOADER_URL } /download/stream " ,
json = { " url " : url } ,
headers = { " Content-Type " : " application/json " }
)
2025-12-10 14:46:09 +03:00
2025-12-11 01:07:04 +03:00
if response . status_code != 200 :
error_text = response . text
try :
error_json = response . json ( )
error_text = error_json . get ( ' error ' , error_text )
except :
pass
raise Exception ( f " YouTube сервис вернул ошибку { response . status_code } : { error_text } " )
video_data = response . content
2025-12-12 11:11:51 +03:00
video_ext = ' mp4 '
2025-12-11 01:07:04 +03:00
content_type = response . headers . get ( ' Content-Type ' , ' ' )
if ' video/ ' in content_type :
video_ext = content_type . split ( ' / ' ) [ - 1 ] . split ( ' ; ' ) [ 0 ]
filename = response . headers . get ( ' Content-Disposition ' , ' ' )
if filename and ' filename= ' in filename :
video_filename = filename . split ( ' filename= ' ) [ 1 ] . strip ( ' " \' ' )
else :
video_filename = f ' { chat_id } _youtube_video. { video_ext } '
video_path = DOWNLOADS_DIR / video_filename
with open ( video_path , ' wb ' ) as f :
f . write ( video_data )
logger . info ( f " YouTube: видео скачано через внешний сервис: { video_path } " )
return str ( video_path )
except httpx . TimeoutException :
last_error = Exception ( f " Таймаут при запросе к YouTube сервису (попытка { attempt + 1 } / { max_retries } ) " )
logger . warning ( f " YouTube: таймаут при запросе к сервису: { last_error } " )
2025-12-10 14:46:09 +03:00
except Exception as e :
last_error = e
logger . warning ( f " YouTube: попытка { attempt + 1 } / { max_retries } не удалась: { e } " )
2025-12-11 01:07:04 +03:00
if attempt < max_retries - 1 :
await asyncio . sleep ( ( attempt + 1 ) * 2 )
2025-12-10 14:46:09 +03:00
2025-12-11 01:07:04 +03:00
raise last_error or Exception ( " Неизвестная ошибка при скачивании с YouTube через внешний сервис " )
2025-12-10 14:46:09 +03:00
async def download_instagram_video ( url : str , chat_id : int , max_retries : int = 3 ) - > str :
2025-12-11 01:07:04 +03:00
""" Скачивает видео с Instagram через внешний сервис """
logger . info ( f " Instagram: отправка запроса на внешний сервис { INSTAGRAM_DOWNLOADER_URL } " )
2025-12-10 14:46:09 +03:00
last_error = None
for attempt in range ( max_retries ) :
try :
2025-12-12 15:41:46 +03:00
async with httpx . AsyncClient ( timeout = HTTP_TIMEOUT ) as client :
2025-12-11 01:07:04 +03:00
response = await client . post (
f " { INSTAGRAM_DOWNLOADER_URL } /download/stream " ,
json = { " url " : url } ,
headers = { " Content-Type " : " application/json " }
)
2025-12-10 14:46:09 +03:00
2025-12-11 01:07:04 +03:00
if response . status_code != 200 :
error_text = response . text
try :
error_json = response . json ( )
error_text = error_json . get ( ' error ' , error_text )
except :
pass
raise Exception ( f " Instagram сервис вернул ошибку { response . status_code } : { error_text } " )
2025-12-10 17:02:01 +03:00
2025-12-11 01:07:04 +03:00
video_data = response . content
2025-12-12 11:11:51 +03:00
video_ext = ' mp4 '
2025-12-10 17:02:01 +03:00
2025-12-11 01:07:04 +03:00
content_type = response . headers . get ( ' Content-Type ' , ' ' )
if ' video/ ' in content_type :
video_ext = content_type . split ( ' / ' ) [ - 1 ] . split ( ' ; ' ) [ 0 ]
2025-12-10 17:02:01 +03:00
2025-12-11 01:07:04 +03:00
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 } '
2025-12-10 21:05:27 +03:00
2025-12-11 01:07:04 +03:00
video_path = DOWNLOADS_DIR / video_filename
with open ( video_path , ' wb ' ) as f :
f . write ( video_data )
2025-12-10 17:02:01 +03:00
2025-12-11 01:07:04 +03:00
logger . info ( f " Instagram: видео скачано через внешний сервис: { video_path } " )
return str ( video_path )
2025-12-10 17:02:01 +03:00
2025-12-11 01:07:04 +03:00
except httpx . TimeoutException :
last_error = Exception ( f " Таймаут при запросе к Instagram сервису (попытка { attempt + 1 } / { max_retries } ) " )
logger . warning ( f " Instagram: таймаут при запросе к сервису: { last_error } " )
2025-12-10 17:02:01 +03:00
except Exception as e :
2025-12-11 01:07:04 +03:00
last_error = e
logger . warning ( f " Instagram: попытка { attempt + 1 } / { max_retries } не удалась: { e } " )
if attempt < max_retries - 1 :
await asyncio . sleep ( ( attempt + 1 ) * 2 )
raise last_error or Exception ( " Неизвестная ошибка при скачивании с Instagram через внешний сервис " )
2025-12-10 17:02:01 +03:00
2025-12-10 14:46:09 +03:00
async def download_vk_video ( url : str , chat_id : int , max_retries : int = 3 ) - > str :
2025-12-10 16:14:26 +03:00
""" Скачивает видео с VK через внешний сервис """
logger . info ( f " VK: отправка запроса на внешний сервис { VK_DOWNLOADER_URL } " )
2025-12-10 14:46:09 +03:00
last_error = None
for attempt in range ( max_retries ) :
try :
2025-12-12 15:41:46 +03:00
async with httpx . AsyncClient ( timeout = HTTP_TIMEOUT ) as client :
2025-12-10 16:14:26 +03:00
response = await client . post (
f " { VK_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 " VK сервис вернул ошибку { response . status_code } : { error_text } " )
video_data = response . content
2025-12-12 11:11:51 +03:00
video_ext = ' mp4 '
2025-12-10 16:14:26 +03:00
content_type = response . headers . get ( ' Content-Type ' , ' ' )
if ' video/ ' in content_type :
video_ext = content_type . split ( ' / ' ) [ - 1 ] . split ( ' ; ' ) [ 0 ]
2025-12-10 14:46:09 +03:00
2025-12-10 16:14:26 +03:00
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 } _vk_video. { video_ext } '
video_path = DOWNLOADS_DIR / video_filename
with open ( video_path , ' wb ' ) as f :
f . write ( video_data )
logger . info ( f " VK: видео скачано через внешний сервис: { video_path } " )
return str ( video_path )
except httpx . TimeoutException :
last_error = Exception ( f " Таймаут при запросе к VK сервису (попытка { attempt + 1 } / { max_retries } ) " )
logger . warning ( f " VK: таймаут при запросе к сервису: { last_error } " )
2025-12-10 14:46:09 +03:00
except Exception as e :
last_error = e
logger . warning ( f " VK: попытка { attempt + 1 } / { max_retries } не удалась: { e } " )
2025-12-10 16:14:26 +03:00
if attempt < max_retries - 1 :
await asyncio . sleep ( ( attempt + 1 ) * 2 )
2025-12-10 14:46:09 +03:00
2025-12-10 16:14:26 +03:00
raise last_error or Exception ( " Неизвестная ошибка при скачивании с VK через внешний сервис " )
2025-12-10 14:46:09 +03:00
2025-12-12 10:32:06 +03:00
async def download_yapfiles_video ( url : str , chat_id : int , max_retries : int = 3 ) - > str :
""" Скачивает видео с Yapfiles через внешний сервис """
logger . info ( f " Yapfiles: отправка запроса на внешний сервис { YAPFILES_DOWNLOADER_URL } " )
last_error = None
for attempt in range ( max_retries ) :
try :
2025-12-12 15:41:46 +03:00
async with httpx . AsyncClient ( timeout = HTTP_TIMEOUT ) as client :
2025-12-12 10:32:06 +03:00
response = await client . post (
f " { YAPFILES_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 " Yapfiles сервис вернул ошибку { 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 } _yapfiles_video. { video_ext } '
video_path = DOWNLOADS_DIR / video_filename
with open ( video_path , ' wb ' ) as f :
f . write ( video_data )
logger . info ( f " Yapfiles: видео скачано через внешний сервис: { video_path } " )
return str ( video_path )
except httpx . TimeoutException :
last_error = Exception ( f " Таймаут при запросе к Yapfiles сервису (попытка { attempt + 1 } / { max_retries } ) " )
logger . warning ( f " Yapfiles: таймаут при запросе к сервису: { last_error } " )
except Exception as e :
last_error = e
logger . warning ( f " Yapfiles: попытка { attempt + 1 } / { max_retries } не удалась: { e } " )
if attempt < max_retries - 1 :
await asyncio . sleep ( ( attempt + 1 ) * 2 )
raise last_error or Exception ( " Неизвестная ошибка при скачивании с Yapfiles через внешний сервис " )
2025-12-12 12:36:23 +03:00
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 :
2025-12-12 15:41:46 +03:00
async with httpx . AsyncClient ( timeout = HTTP_TIMEOUT ) as client :
2025-12-12 12:36:23 +03:00
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 через внешний сервис " )
2025-12-12 11:11:51 +03:00
async def download_video ( url : str , chat_id : int , locale : str , max_retries : int = 3 ) - > str :
2025-12-10 14:46:09 +03:00
""" Главная функция скачивания - вызывает нужную функцию в зависимости от источника """
source = detect_video_source ( url )
logger . info ( f " Определен источник: { source } для URL: { url } " )
if source == ' youtube ' :
return await download_youtube_video ( url , chat_id , max_retries )
elif source == ' instagram ' :
return await download_instagram_video ( url , chat_id , max_retries )
elif source == ' vk ' :
return await download_vk_video ( url , chat_id , max_retries )
2025-12-12 10:32:06 +03:00
elif source == ' yapfiles ' :
return await download_yapfiles_video ( url , chat_id , max_retries )
2025-12-12 12:36:23 +03:00
elif source == ' tiktok ' :
return await download_tiktok_video ( url , chat_id , max_retries )
2025-12-10 14:46:09 +03:00
else :
2025-12-12 11:11:51 +03:00
raise Exception ( get_text ( locale , ' error_unknown_source ' ) )
2025-12-10 14:46:09 +03:00
2025-12-12 11:11:51 +03:00
# ============================================================================
# ОБРАБОТЧИКИ КОМАНД И СООБЩЕНИЙ
# ============================================================================
2025-12-10 14:46:09 +03:00
async def handle_message ( update : Update , context : ContextTypes . DEFAULT_TYPE ) :
""" Обрабатывает сообщения от пользователей """
if not update . message or not update . message . text :
return
2025-12-10 20:35:38 +03:00
text = update . message . text . strip ( )
2025-12-10 14:46:09 +03:00
chat_id = update . message . chat_id
2025-12-12 11:11:51 +03:00
chat_type = update . message . chat . type
2025-12-10 15:36:27 +03:00
username = update . message . from_user . username if update . message . from_user else None
first_name = update . message . from_user . first_name if update . message . from_user else None
2025-12-12 11:11:51 +03:00
language_code = update . message . from_user . language_code if update . message . from_user else None
2025-12-10 14:46:09 +03:00
2025-12-12 11:11:51 +03:00
# Определяем локаль
locale = get_locale_from_language_code ( language_code )
# Добавляем пользователя в статистику
add_user ( chat_id , username , first_name , locale )
2025-12-10 15:14:01 +03:00
2025-12-10 20:35:38 +03:00
# Извлекаем все URL из текста
urls = extract_urls_from_text ( text )
# Если это личный чат и нет ссылок, отправляем инструкцию
if not urls and chat_type == ' private ' :
2025-12-12 11:11:51 +03:00
await update . message . reply_text ( get_text ( locale , ' send_link ' ) )
2025-12-10 14:46:09 +03:00
return
2025-12-10 20:35:38 +03:00
# Если нет ссылок в группе, просто игнорируем сообщение
if not urls :
return
# Обрабатываем первую найденную ссылку
url = urls [ 0 ]
2025-12-10 14:46:09 +03:00
# Проверяем источник до начала обработки
source = detect_video_source ( url )
if source == ' unknown ' :
2025-12-10 20:35:38 +03:00
if chat_type == ' private ' :
2025-12-12 11:11:51 +03:00
await update . message . reply_text ( get_text ( locale , ' unsupported_source ' ) )
2025-12-10 14:46:09 +03:00
return
# Отправляем сообщение о начале обработки
2025-12-12 11:11:51 +03:00
status_message = await update . message . reply_text ( get_text ( locale , ' processing ' ) )
2025-12-10 14:46:09 +03:00
2025-12-12 15:41:46 +03:00
# Создаём элемент очереди
item = QueueItem (
original_message = update . message ,
status_message = status_message ,
url = url ,
chat_id = chat_id ,
chat_type = chat_type ,
locale = locale
)
# Добавляем в очередь
position = await add_to_queue ( item )
# Показываем позицию в очереди
if position == 1 :
# Первый - сразу начинаем качать
await status_message . edit_text ( get_text ( locale , ' queue_first ' ) )
else :
# В очереди - показываем позицию
await status_message . edit_text (
get_text ( locale , ' queue_position ' , position = position )
2025-12-10 14:46:09 +03:00
)
async def start_command ( update : Update , context : ContextTypes . DEFAULT_TYPE ) :
""" Обрабатывает команду /start """
2025-12-10 15:14:01 +03:00
chat_id = update . message . chat_id
2025-12-10 15:36:27 +03:00
username = update . message . from_user . username if update . message . from_user else None
first_name = update . message . from_user . first_name if update . message . from_user else None
2025-12-12 11:11:51 +03:00
language_code = update . message . from_user . language_code if update . message . from_user else None
2025-12-10 15:14:01 +03:00
2025-12-12 11:11:51 +03:00
locale = get_locale_from_language_code ( language_code )
add_user ( chat_id , username , first_name , locale )
await update . message . reply_text ( get_text ( locale , ' start ' ) )
2025-12-10 14:46:09 +03:00
async def stat_command ( update : Update , context : ContextTypes . DEFAULT_TYPE ) :
""" Обрабатывает команду /stat """
2025-12-12 11:11:51 +03:00
language_code = update . message . from_user . language_code if update . message . from_user else None
locale = get_locale_from_language_code ( language_code )
2025-12-10 15:36:27 +03:00
total_downloads = get_total_downloads ( )
total_users = get_total_users ( )
2025-12-10 15:14:01 +03:00
2025-12-10 14:46:09 +03:00
await update . message . reply_text (
2025-12-12 11:11:51 +03:00
get_text ( locale , ' stat ' , users = total_users , downloads = total_downloads )
)
async def support_command ( update : Update , context : ContextTypes . DEFAULT_TYPE ) :
""" Обрабатывает команду /support """
language_code = update . message . from_user . language_code if update . message . from_user else None
locale = get_locale_from_language_code ( language_code )
await update . message . reply_text (
get_text ( locale , ' support ' ) ,
parse_mode = ' HTML '
2025-12-10 14:46:09 +03:00
)
2025-12-12 11:11:51 +03:00
# ============================================================================
# MAIN
# ============================================================================
2025-12-10 14:46:09 +03:00
def main ( ) :
""" Главная функция для запуска бота """
if not TELEGRAM_BOT_TOKEN :
logger . error ( " TELEGRAM_BOT_TOKEN не установлен! " )
return
2025-12-10 15:36:27 +03:00
# Инициализируем базу данных
init_database ( )
2025-12-11 01:07:04 +03:00
# Очищаем .part файлы при старте
logger . info ( " Очистка .part файлов при старте... " )
2025-12-12 11:11:51 +03:00
cleanup_old_files ( )
2025-12-10 21:05:27 +03:00
2025-12-12 15:41:46 +03:00
# Создаем приложение с максимальными таймаутами для больших файлов
request = HTTPXRequest (
read_timeout = 3600 , # 1 час на получение ответа
write_timeout = 3600 , # 1 час на отправку видео
connect_timeout = 300 , # 5 минут на соединение
pool_timeout = 300 # 5 минут на получение соединения из пула
)
application = (
Application . builder ( )
. token ( TELEGRAM_BOT_TOKEN )
. request ( request )
. get_updates_request ( HTTPXRequest ( read_timeout = 120 , connect_timeout = 60 ) )
. build ( )
)
2025-12-10 14:46:09 +03:00
# Регистрируем обработчики
application . add_handler ( MessageHandler ( filters . TEXT & ~ filters . COMMAND , handle_message ) )
application . add_handler ( CommandHandler ( " start " , start_command ) )
application . add_handler ( CommandHandler ( " stat " , stat_command ) )
2025-12-12 11:11:51 +03:00
application . add_handler ( CommandHandler ( " support " , support_command ) )
2025-12-10 14:46:09 +03:00
2025-12-12 15:41:46 +03:00
# Инициализация очереди и запуск воркера
2025-12-10 17:02:01 +03:00
async def post_init ( application : Application ) :
""" Выполняется после инициализации приложения """
2025-12-12 15:41:46 +03:00
global download_queue
# Инициализируем очередь
download_queue = asyncio . Queue ( )
# Запускаем воркер очереди
asyncio . create_task ( queue_worker ( ) )
logger . info ( " Воркер очереди запущен " )
# Запускаем периодическую очистку .part файлов (каждые 6 часов)
2025-12-10 21:05:27 +03:00
async def periodic_cleanup ( ) :
while True :
2025-12-12 11:11:51 +03:00
await asyncio . sleep ( 6 * 3600 )
2025-12-11 01:07:04 +03:00
cleanup_old_files ( )
logger . info ( " Периодическая очистка .part файлов выполнена " )
2025-12-10 21:05:27 +03:00
asyncio . create_task ( periodic_cleanup ( ) )
logger . info ( " Фоновая задача периодической очистки файлов запущена " )
2025-12-10 17:02:01 +03:00
application . post_init = post_init
2025-12-10 14:46:09 +03:00
# Запускаем бота
logger . info ( " Бот запущен " )
application . run_polling ( allowed_updates = Update . ALL_TYPES )
if __name__ == ' __main__ ' :
main ( )