@ -9,8 +9,15 @@ from urllib.parse import urlparse
from datetime import datetime
import httpx
from telegram import Update
from telegram . ext import Application , MessageHandler , filters , ContextTypes , CommandHandler
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 )
# Настройка логирования
logging . basicConfig (
@ -103,6 +110,9 @@ TEXTS = {
' caption ' : " Видео скачано с @ {bot_username} " ,
' error ' : " ❌ Произошла ошибка при обработке видео: \n {error} " ,
' error_unknown_source ' : " Пардон, не умеем работать с этим источником " ,
' error_file_too_large ' : " ❌ Видео слишком большое ( {size_mb:.1f} МБ) \n \n Telegram Bot API позволяет отправлять файлы до 50 МБ. \n \n Попробуйте выбрать видео покороче или в меньшем качестве. " ,
' queue_position ' : " 🕐 Ваше видео # {position} в очереди \n Ваш запрос очень важен для нас! " ,
' queue_first ' : " ⬇️ Скачиваю видео... " ,
} ,
' en ' : {
' start ' : (
@ -160,6 +170,9 @@ TEXTS = {
' caption ' : " Video downloaded via @ {bot_username} " ,
' error ' : " ❌ Error processing video: \n {error} " ,
' error_unknown_source ' : " Sorry, this source is not supported " ,
' 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... " ,
}
}
@ -363,6 +376,162 @@ def cleanup_old_files():
logger . error ( f " Ошибка при очистке .part файлов: { e } " )
# ============================================================================
# СИСТЕМА ОЧЕРЕДЕЙ
# ============================================================================
@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
# ============================================================================
# ФУНКЦИИ СКАЧИВАНИЯ
# ============================================================================
@ -374,7 +543,7 @@ async def download_youtube_video(url: str, chat_id: int, max_retries: int = 3) -
last_error = None
for attempt in range ( max_retries ) :
try :
async with httpx . AsyncClient ( timeout = 600.0 ) as client :
async with httpx . AsyncClient ( timeout = HTTP_TIMEOUT ) as client :
response = await client . post (
f " { YOUTUBE_DOWNLOADER_URL } /download/stream " ,
json = { " url " : url } ,
@ -430,7 +599,7 @@ async def download_instagram_video(url: str, chat_id: int, max_retries: int = 3)
last_error = None
for attempt in range ( max_retries ) :
try :
async with httpx . AsyncClient ( timeout = 600.0 ) as client :
async with httpx . AsyncClient ( timeout = HTTP_TIMEOUT ) as client :
response = await client . post (
f " { INSTAGRAM_DOWNLOADER_URL } /download/stream " ,
json = { " url " : url } ,
@ -486,7 +655,7 @@ async def download_vk_video(url: str, chat_id: int, max_retries: int = 3) -> str
last_error = None
for attempt in range ( max_retries ) :
try :
async with httpx . AsyncClient ( timeout = 600.0 ) as client :
async with httpx . AsyncClient ( timeout = HTTP_TIMEOUT ) as client :
response = await client . post (
f " { VK_DOWNLOADER_URL } /download/stream " ,
json = { " url " : url } ,
@ -542,7 +711,7 @@ async def download_yapfiles_video(url: str, chat_id: int, max_retries: int = 3)
last_error = None
for attempt in range ( max_retries ) :
try :
async with httpx . AsyncClient ( timeout = 600.0 ) as client :
async with httpx . AsyncClient ( timeout = HTTP_TIMEOUT ) as client :
response = await client . post (
f " { YAPFILES_DOWNLOADER_URL } /download/stream " ,
json = { " url " : url } ,
@ -598,7 +767,7 @@ async def download_tiktok_video(url: str, chat_id: int, max_retries: int = 3) ->
last_error = None
for attempt in range ( max_retries ) :
try :
async with httpx . AsyncClient ( timeout = 600.0 ) as client :
async with httpx . AsyncClient ( timeout = HTTP_TIMEOUT ) as client :
response = await client . post (
f " { TIKTOK_DOWNLOADER_URL } /download/stream " ,
json = { " url " : url } ,
@ -713,50 +882,28 @@ async def handle_message(update: Update, context: ContextTypes.DEFAULT_TYPE):
# Отправляем сообщение о начале обработки
status_message = await update . message . reply_text ( get_text ( locale , ' processing ' ) )
try :
# Скачиваем видео
await status_message . edit_text ( get_text ( locale , ' downloading ' ) )
video_path = await download_video ( url , chat_id , locale )
# Отправляем файл пользователю
await status_message . edit_text ( get_text ( locale , ' sending ' ) )
video_file = open ( video_path , ' rb ' )
caption = get_text ( locale , ' caption ' , bot_username = TELEGRAM_BOT_USERNAME )
await update . message . reply_video (
video = video_file ,
caption = caption ,
supports_streaming = True
# Создаём элемент очереди
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 )
)
video_file . close ( )
# Увеличиваем счетчик скачанных видео
increment_downloads ( )
logger . info ( f " Видео сохранено: { video_path } " )
# Удаляем статусное сообщение и исходное сообщение с о ссылкой
try :
await status_message . delete ( )
await update . message . delete ( )
logger . info ( f " Удалено сообщение пользователя с ссылкой (chat_id: { chat_id } , тип чата: { chat_type } ) " )
except Exception as e :
logger . warning ( f " Н е удалось удалить сообщение: { e } " )
except Exception as e :
logger . error ( f " Ошибка: { e } " )
error_msg = get_text ( locale , ' error ' , error = str ( e ) )
try :
await status_message . edit_text ( error_msg )
except :
await update . message . reply_text ( error_msg )
try :
for part_file in DOWNLOADS_DIR . glob ( f ' { 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 start_command ( update : Update , context : ContextTypes . DEFAULT_TYPE ) :
@ -813,8 +960,20 @@ def main():
logger . info ( " Очистка .part файлов при старте... " )
cleanup_old_files ( )
# Создаем приложение
application = Application . builder ( ) . token ( TELEGRAM_BOT_TOKEN ) . build ( )
# Создаем приложение с максимальными таймаутами для больших файлов
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 ( )
)
# Регистрируем обработчики
application . add_handler ( MessageHandler ( filters . TEXT & ~ filters . COMMAND , handle_message ) )
@ -822,9 +981,19 @@ def main():
application . add_handler ( CommandHandler ( " stat " , stat_command ) )
application . add_handler ( CommandHandler ( " support " , support_command ) )
# Запускаем периодическую очистку файлов
# Инициализация очереди и запуск воркера
async def post_init ( application : Application ) :
""" Выполняется после инициализации приложения """
global download_queue
# Инициализируем очередь
download_queue = asyncio . Queue ( )
# Запускаем воркер очереди
asyncio . create_task ( queue_worker ( ) )
logger . info ( " Воркер очереди запущен " )
# Запускаем периодическую очистку .part файлов (каждые 6 часов)
async def periodic_cleanup ( ) :
while True :
await asyncio . sleep ( 6 * 3600 )