fix: отправка видео как документ (без сжатия Telegram) и исправление format_id для точного выбора качества
- Замена reply_video() на reply_document() в bot.py — Telegram больше не сжимает видео - Исправление format_id в get_youtube_formats(): конкретные format codes + fallback best[height<=N] - Замена bestvideo[height<=N]+bestaudio на best[height<=N] — гарантированно работает когда YouTube не отдаёт отдельные video-only потоки для низких разрешений - Добавлено логирование реально скачанного формата для диагностики
This commit is contained in:
parent
4b7cc403b2
commit
4629535e97
6 changed files with 632 additions and 95 deletions
160
bot.py
160
bot.py
|
|
@ -9,8 +9,8 @@ from urllib.parse import urlparse
|
|||
from datetime import datetime
|
||||
|
||||
import httpx
|
||||
from telegram import Update, Message, Bot
|
||||
from telegram.ext import Application, MessageHandler, filters, ContextTypes, CommandHandler, Defaults
|
||||
from telegram import Update, Message, Bot, InlineKeyboardButton, InlineKeyboardMarkup, CallbackQuery
|
||||
from telegram.ext import Application, MessageHandler, filters, ContextTypes, CommandHandler, Defaults, CallbackQueryHandler
|
||||
from telegram.request import HTTPXRequest
|
||||
from dataclasses import dataclass
|
||||
from typing import Optional
|
||||
|
|
@ -110,6 +110,9 @@ TEXTS = {
|
|||
'error_file_too_large': "❌ Видео слишком большое ({size_mb:.1f} МБ, max = 50)",
|
||||
'queue_position': "🕐 Ваше видео #{position} в очереди\nВаш запрос очень важен для нас!",
|
||||
'queue_first': "⬇️ Скачиваю видео...",
|
||||
'select_quality': "Выберите качество видео:",
|
||||
'quality_cancelled': "❌ Выбор отменён",
|
||||
'fetching_formats': "🔍 Получаю доступные форматы...",
|
||||
},
|
||||
'en': {
|
||||
'start': (
|
||||
|
|
@ -164,6 +167,9 @@ 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:",
|
||||
'quality_cancelled': "❌ Cancelled",
|
||||
'fetching_formats': "🔍 Fetching available formats...",
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -491,6 +497,7 @@ class QueueItem:
|
|||
chat_id: int
|
||||
chat_type: str
|
||||
locale: str
|
||||
format_id: str | None = None
|
||||
|
||||
|
||||
# Глобальная очередь и список элементов для отслеживания позиций
|
||||
|
|
@ -521,7 +528,7 @@ async def process_queue_item(item: QueueItem):
|
|||
"""Обрабатывает один элемент очереди"""
|
||||
try:
|
||||
# Скачиваем видео
|
||||
video_path = await download_video(item.url, item.chat_id, item.locale)
|
||||
video_path = await download_video(item.url, item.chat_id, item.locale, format_id=item.format_id)
|
||||
|
||||
# Проверяем размер файла (лимит Telegram Bot API - 50 МБ)
|
||||
file_size = Path(video_path).stat().st_size
|
||||
|
|
@ -548,10 +555,16 @@ async def process_queue_item(item: QueueItem):
|
|||
|
||||
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,
|
||||
|
||||
# Определяем имя файла для отправки
|
||||
video_filename = Path(video_path).name
|
||||
|
||||
# Отправляем как документ, чтобы Telegram НЕ сжимал видео
|
||||
# (reply_video сжимает, что приводит к потере качества и одинаковому размеру)
|
||||
await item.original_message.reply_document(
|
||||
document=video_file,
|
||||
filename=video_filename,
|
||||
caption=caption,
|
||||
supports_streaming=True,
|
||||
read_timeout=600, # 10 минут на ответ от Telegram
|
||||
write_timeout=600, # 10 минут на отправку файла
|
||||
connect_timeout=60,
|
||||
|
|
@ -661,7 +674,7 @@ async def add_to_queue(item: QueueItem) -> int:
|
|||
# ФУНКЦИИ СКАЧИВАНИЯ
|
||||
# ============================================================================
|
||||
|
||||
async def download_youtube_video(url: str, chat_id: int, max_retries: int = 3) -> str:
|
||||
async def download_youtube_video(url: str, chat_id: int, max_retries: int = 3, format_id: str | None = None) -> str:
|
||||
"""Скачивает видео с YouTube через внешний сервис"""
|
||||
logger.info(f"YouTube: отправка запроса на внешний сервис {YOUTUBE_DOWNLOADER_URL}")
|
||||
|
||||
|
|
@ -669,9 +682,13 @@ async def download_youtube_video(url: str, chat_id: int, max_retries: int = 3) -
|
|||
for attempt in range(max_retries):
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=HTTP_TIMEOUT) as client:
|
||||
# Формируем тело запроса, опционально с format_id
|
||||
body = {"url": url}
|
||||
if format_id:
|
||||
body["format_id"] = format_id
|
||||
response = await client.post(
|
||||
f"{YOUTUBE_DOWNLOADER_URL}/download/stream",
|
||||
json={"url": url},
|
||||
json=body,
|
||||
headers={"Content-Type": "application/json"}
|
||||
)
|
||||
|
||||
|
|
@ -941,13 +958,116 @@ async def download_tiktok_video(url: str, chat_id: int, max_retries: int = 3) ->
|
|||
raise last_error or Exception("Неизвестная ошибка при скачивании с TikTok через внешний сервис")
|
||||
|
||||
|
||||
async def download_video(url: str, chat_id: int, locale: str, max_retries: int = 3) -> str:
|
||||
# ============================================================================
|
||||
# ВЫБОР КАЧЕСТВА (только для YouTube)
|
||||
# ============================================================================
|
||||
|
||||
|
||||
async def get_formats_from_service(url: str) -> list[dict] | None:
|
||||
"""Получает список доступных форматов для YouTube URL через сервис youtube-downloader"""
|
||||
logger.info(f"Получение форматов для YouTube: {url}")
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=HTTP_TIMEOUT) as client:
|
||||
response = await client.post(
|
||||
f"{YOUTUBE_DOWNLOADER_URL}/formats",
|
||||
json={"url": url},
|
||||
headers={"Content-Type": "application/json"}
|
||||
)
|
||||
if response.status_code == 200:
|
||||
data = response.json()
|
||||
return data.get('formats', [])
|
||||
logger.warning(f"Не удалось получить форматы: {response.status_code}")
|
||||
return None
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при получении форматов: {e}")
|
||||
return None
|
||||
|
||||
|
||||
async def show_quality_selection(status_message: Message, formats: list[dict], locale: str):
|
||||
"""Показывает inline клавиатуру с выбором качества видео"""
|
||||
keyboard = []
|
||||
for fmt in 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
|
||||
keyboard.append([InlineKeyboardButton(
|
||||
text=button_text,
|
||||
callback_data=f"quality:{fmt['format_id']}"
|
||||
)])
|
||||
|
||||
# Кнопка отмены
|
||||
keyboard.append([InlineKeyboardButton(
|
||||
text=get_text(locale, 'quality_cancelled'),
|
||||
callback_data="quality:cancel"
|
||||
)])
|
||||
|
||||
reply_markup = InlineKeyboardMarkup(keyboard)
|
||||
await status_message.edit_text(
|
||||
get_text(locale, 'select_quality'),
|
||||
reply_markup=reply_markup
|
||||
)
|
||||
|
||||
|
||||
async def handle_format_selection(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||
"""Обрабатывает выбор качества пользователем через callback query"""
|
||||
query = update.callback_query
|
||||
await query.answer()
|
||||
|
||||
chat_id = query.message.chat_id
|
||||
callback_data = query.data
|
||||
|
||||
# Получаем сохраненные данные
|
||||
data = context.user_data.pop(f'quality_{chat_id}', None)
|
||||
if not data:
|
||||
await query.edit_message_text("Session expired, please send the link again")
|
||||
return
|
||||
|
||||
locale = data['locale']
|
||||
status_message = data['status_message']
|
||||
|
||||
if callback_data == "quality:cancel":
|
||||
await status_message.edit_text(get_text(locale, 'quality_cancelled'))
|
||||
return
|
||||
|
||||
# Извлекаем format_id
|
||||
format_id = callback_data.replace('quality:', '')
|
||||
|
||||
# Обновляем сообщение - добавляем в очередь
|
||||
await status_message.edit_text(get_text(locale, 'processing'))
|
||||
|
||||
# Создаём элемент очереди с выбранным format_id
|
||||
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)
|
||||
logger.info(f"Определен источник: {source} для URL: {url}")
|
||||
|
||||
if source == 'youtube':
|
||||
return await download_youtube_video(url, chat_id, max_retries)
|
||||
return await download_youtube_video(url, chat_id, max_retries, format_id=format_id)
|
||||
elif source == 'instagram':
|
||||
return await download_instagram_video(url, chat_id, max_retries)
|
||||
elif source == 'vk':
|
||||
|
|
@ -1007,6 +1127,25 @@ async def handle_message(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
|||
# Отправляем сообщение о начале обработки
|
||||
status_message = await update.message.reply_text(get_text(locale, 'processing'))
|
||||
|
||||
# Для YouTube - показываем выбор качества перед добавлением в очередь
|
||||
if source == 'youtube':
|
||||
await status_message.edit_text(get_text(locale, 'fetching_formats'))
|
||||
formats = await get_formats_from_service(url)
|
||||
if formats:
|
||||
# Сохраняем данные для обработки в колбэке
|
||||
context.user_data[f'quality_{chat_id}'] = {
|
||||
'url': url,
|
||||
'locale': locale,
|
||||
'chat_id': chat_id,
|
||||
'chat_type': chat_type,
|
||||
'original_message': update.message,
|
||||
'status_message': status_message
|
||||
}
|
||||
await show_quality_selection(status_message, formats, locale)
|
||||
return
|
||||
# Если не удалось получить форматы, скачиваем как обычно (без выбора качества)
|
||||
await status_message.edit_text(get_text(locale, 'processing'))
|
||||
|
||||
# Создаём элемент очереди
|
||||
item = QueueItem(
|
||||
original_message=update.message,
|
||||
|
|
@ -1091,6 +1230,7 @@ def main():
|
|||
application.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, handle_message))
|
||||
application.add_handler(CommandHandler("start", start_command))
|
||||
application.add_handler(CommandHandler("support", support_command))
|
||||
application.add_handler(CallbackQueryHandler(handle_format_selection, pattern=r'^quality:'))
|
||||
|
||||
# Инициализация очереди и запуск воркера
|
||||
async def post_init(application: Application):
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue