diff --git a/bot.py b/bot.py index 7818e79..805a9dd 100644 --- a/bot.py +++ b/bot.py @@ -117,7 +117,7 @@ TEXTS = { 'error_file_too_large': "❌ Видео слишком большое ({size_mb:.1f} МБ, max = 50)", 'queue_position': "🕐 Ваше видео #{position} в очереди\nВаш запрос очень важен для нас!", 'queue_first': "⬇️ Скачиваю видео...", - 'select_quality': "Выберите качество видео:", + 'select_quality': "Выберите качество видео:\n(через 10 сек — автоскачивание лучшего)", 'quality_cancelled': "❌ Выбор отменён", 'fetching_formats': "🔍 Получаю доступные форматы...", }, @@ -174,7 +174,7 @@ TEXTS = { 'error_file_too_large': "❌ Video is too large ({size_mb:.1f} MB, max = 50)", 'queue_position': "🕐 Your video is #{position} in queue\nYour request is very important to us!", 'queue_first': "⬇️ Downloading video...", - 'select_quality': "Select video quality:", + 'select_quality': "Select video quality:\n(10 sec auto-download of best quality)", 'quality_cancelled': "❌ Cancelled", 'fetching_formats': "🔍 Fetching available formats...", } @@ -567,11 +567,12 @@ async def process_queue_item(item: QueueItem): # Определяем имя файла для отправки video_filename = Path(video_path).name - # Отправляем как документ — Telegram сам определит тип по расширению - await item.original_message.reply_document( - document=video_file, + # Отправляем как видео со streaming — встроенный плеер Telegram + await item.original_message.reply_video( + video=video_file, filename=video_filename, caption=caption, + supports_streaming=True, read_timeout=600, write_timeout=600, connect_timeout=60, @@ -1021,12 +1022,7 @@ async def show_quality_selection(status_message: Message, formats: list[dict], l """ keyboard = [] for idx, fmt in enumerate(formats): - label = fmt.get('label', fmt.get('quality', 'Unknown')) - filesize = fmt.get('filesize_mb') - if filesize: - button_text = f"{label} ({filesize:.0f} MB)" - else: - button_text = label + button_text = fmt.get('label', fmt.get('quality', 'Unknown')) keyboard.append([InlineKeyboardButton( text=button_text, callback_data=f"quality:{idx}" @@ -1055,6 +1051,10 @@ async def handle_format_selection(update: Update, context: ContextTypes.DEFAULT_ # Получаем сохраненные данные data = context.user_data.pop(f'quality_{chat_id}', None) + # Отменяем авто-выбор качества + auto_task = context.user_data.pop(f'quality_auto_{chat_id}', None) + if auto_task: + auto_task.cancel() if not data: await query.edit_message_text("Session expired, please send the link again") return @@ -1104,6 +1104,66 @@ async def handle_format_selection(update: Update, context: ContextTypes.DEFAULT_ ) +async def _auto_select_after_delay(context: ContextTypes.DEFAULT_TYPE, chat_id: int, delay: int = 10): + """Автовыбор лучшего качества через delay секунд, если пользователь не выбрал""" + try: + await asyncio.sleep(delay) + except asyncio.CancelledError: + return # пользователь выбрал вручную + + data = context.user_data.pop(f'quality_{chat_id}', None) + if not data: + return # уже обработано + + context.user_data.pop(f'quality_auto_{chat_id}', None) # чистим + + formats_list = data.get('formats_list', []) + if not formats_list: + return + + locale = data['locale'] + status_message = data['status_message'] + + # Автовыбор: ищем 480p или ближайшее ниже + preferred_qualities = ['480p', '360p', '240p', '144p'] + selected = None + for pq in preferred_qualities: + for fmt in formats_list: + if fmt.get('quality', '') == pq: + selected = fmt + break + if selected: + break + if not selected: + selected = formats_list[0] # fallback на лучшее + + format_id = selected.get('format_id', '') + quality_label = selected.get('quality', selected.get('label', 'best')) + + logger.info(f"Автовыбор качества для chat_id={chat_id}: {quality_label}") + + await status_message.edit_text(get_text(locale, 'processing')) + + item = QueueItem( + original_message=data['original_message'], + status_message=status_message, + url=data['url'], + chat_id=chat_id, + chat_type=data['chat_type'], + locale=locale, + format_id=format_id + ) + + 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) + ) + + async def download_video(url: str, chat_id: int, locale: str, max_retries: int = 3, format_id: str | None = None) -> str: """Главная функция скачивания - вызывает нужную функцию в зависимости от источника""" source = detect_video_source(url) @@ -1186,6 +1246,9 @@ async def handle_message(update: Update, context: ContextTypes.DEFAULT_TYPE): 'formats_list': formats, # для lookup по индексу в callback } await show_quality_selection(status_message, formats, locale) + # Автовыбор лучшего качества через 10 сек + auto_task = asyncio.create_task(_auto_select_after_delay(context, chat_id, 10)) + context.user_data[f'quality_auto_{chat_id}'] = auto_task return # Если не удалось получить форматы, скачиваем как обычно (без выбора качества) await status_message.edit_text(get_text(locale, 'processing')) diff --git a/youtube-downloader/app.py b/youtube-downloader/app.py index b6988aa..87223b9 100644 --- a/youtube-downloader/app.py +++ b/youtube-downloader/app.py @@ -334,12 +334,7 @@ def download_youtube_video(url: str, max_retries: int = 3, format_id: str | None ydl_opts_download.update({ 'format': format_option, 'outtmpl': _safe_filename(video_title), - # fragment_retries — для DASH форматов (видео без аудио), - # YouTube может разрывать фрагменты; увеличиваем retries 'fragment_retries': 3, - # allow_unplayable_formats — позволяет скачивать форматы, - # которые YouTube помечает как "недоступные" для сторонних клиентов - 'allow_unplayable_formats': True, }) use_cookies_this_attempt = cookies_valid @@ -392,7 +387,6 @@ def download_youtube_video(url: str, max_retries: int = 3, format_id: str | None 'format': format_option, 'outtmpl': _safe_filename(video_title), 'fragment_retries': 3, - 'allow_unplayable_formats': True, }) try: with yt_dlp.YoutubeDL(ydl_opts_download_no_cookies) as ydl: