From 5c280a4d3a696c6913ad04047d2a2d0fc3fbb254 Mon Sep 17 00:00:00 2001 From: qsethuk Date: Fri, 5 Dec 2025 21:57:25 +0300 Subject: [PATCH] Add download video by link --- bot/modules/media_loader/ytdlp.py | 118 +++++++++++++++++++++++ bot/modules/message_handler/callbacks.py | 118 +++++++++++++++++++++++ bot/modules/message_handler/commands.py | 65 +++++++++++++ 3 files changed, 301 insertions(+) diff --git a/bot/modules/media_loader/ytdlp.py b/bot/modules/media_loader/ytdlp.py index 0b10818..c3c00d9 100644 --- a/bot/modules/media_loader/ytdlp.py +++ b/bot/modules/media_loader/ytdlp.py @@ -648,3 +648,121 @@ async def get_media_info(url: str, cookies_file: Optional[str] = None) -> Option logger.error(f"Error getting media info: {e}", exc_info=True) return None + +async def get_videos_list(url: str, cookies_file: Optional[str] = None) -> Optional[Dict]: + """ + Get list of videos from webpage + + Args: + url: Webpage URL + cookies_file: Path to cookies file (optional) + + Returns: + Dictionary with: + - 'type': 'playlist' or 'video' + - 'videos': List of video dictionaries with 'id', 'url', 'title', 'duration', 'thumbnail' + - 'playlist_title': Title of playlist/page (if playlist) + or None if error + """ + try: + loop = asyncio.get_running_loop() + ydl_opts = { + 'quiet': True, + 'no_warnings': True, + 'extract_flat': 'in_playlist', # Extract flat for playlist entries, full for single videos + } + + # Add cookies if specified + if cookies_file: + cookies_path = None + original_path = Path(cookies_file) + + search_paths = [] + if original_path.is_absolute(): + search_paths.append(original_path) + else: + project_root = Path(__file__).parent.parent.parent.parent + search_paths.append(project_root / cookies_file) + import os + cwd = Path(os.getcwd()) + search_paths.append(cwd / cookies_file) + search_paths.append(Path(cookies_file).resolve()) + + for path in search_paths: + if path.exists() and path.is_file(): + cookies_path = path + break + + if cookies_path and cookies_path.exists(): + ydl_opts['cookiefile'] = str(cookies_path) + + def extract_info_sync(): + """Synchronous function for extracting information""" + with yt_dlp.YoutubeDL(ydl_opts) as ydl: + return ydl.extract_info(url, download=False) + + # Extract info without downloading + info = await loop.run_in_executor(None, extract_info_sync) + + if not info: + return None + + # Check if it's a playlist or single video + _type = info.get('_type', 'video') + entries = info.get('entries', []) + + if _type == 'playlist' and entries: + # It's a playlist - extract entries + videos = [] + + for entry in entries[:20]: # Limit to 20 videos to avoid timeout + if entry: + entry_url = entry.get('url') or entry.get('webpage_url') + if not entry_url: + continue + + videos.append({ + 'id': entry.get('id'), + 'url': entry_url, + 'title': entry.get('title', 'Unknown'), + 'duration': entry.get('duration'), + 'thumbnail': entry.get('thumbnail'), + }) + + if videos: + return { + 'type': 'playlist', + 'videos': videos, + 'playlist_title': info.get('title', 'Playlist'), + } + else: + # No valid entries found, treat as single video + return { + 'type': 'video', + 'videos': [{ + 'id': info.get('id'), + 'url': url, + 'title': info.get('title', 'Video'), + 'duration': info.get('duration'), + 'thumbnail': info.get('thumbnail'), + }], + 'playlist_title': None, + } + else: + # Single video + return { + 'type': 'video', + 'videos': [{ + 'id': info.get('id'), + 'url': url, + 'title': info.get('title', 'Video'), + 'duration': info.get('duration'), + 'thumbnail': info.get('thumbnail'), + }], + 'playlist_title': None, + } + + except Exception as e: + logger.error(f"Error getting videos list: {e}", exc_info=True) + return None + diff --git a/bot/modules/message_handler/callbacks.py b/bot/modules/message_handler/callbacks.py index d26826c..7cde142 100644 --- a/bot/modules/message_handler/callbacks.py +++ b/bot/modules/message_handler/callbacks.py @@ -212,6 +212,124 @@ async def callback_handler(client: Client, callback_query: CallbackQuery): await callback_query.edit_message_text(stats_text, reply_markup=keyboard) await callback_query.answer() + elif data.startswith("video_select:"): + # Handle video selection from webpage + video_url = data.replace("video_select:", "", 1) + + # Create task for selected video + try: + from bot.modules.task_scheduler.queue import task_queue, Task, TaskStatus + from bot.modules.task_scheduler.executor import task_executor, set_app_client + from bot.utils.helpers import generate_unique_task_id + from shared.database.models import Task as DBTask + from shared.database.session import get_async_session_local + from shared.database.user_helpers import ensure_user_exists + from datetime import datetime + from sqlalchemy.exc import IntegrityError + from bot.modules.task_scheduler.executor import set_task_message + + user_id = callback_query.from_user.id + + # Check concurrent tasks count + from bot.config import settings + active_tasks_count = await task_queue.get_user_active_tasks_count(user_id) + if active_tasks_count >= settings.MAX_CONCURRENT_TASKS: + await callback_query.answer( + f"❌ Превышен лимит одновременных задач ({settings.MAX_CONCURRENT_TASKS})", + show_alert=True + ) + return + + # Generate task_id + task_id = generate_unique_task_id() + existing_task = await task_queue.get_task_by_id(task_id) + max_retries = 10 + retries = 0 + while existing_task and retries < max_retries: + task_id = generate_unique_task_id() + existing_task = await task_queue.get_task_by_id(task_id) + retries += 1 + + if existing_task: + await callback_query.answer("❌ Ошибка при создании задачи", show_alert=True) + return + + # Create task + task = Task( + id=task_id, + user_id=user_id, + task_type="download", + url=video_url, + status=TaskStatus.PENDING + ) + + # Save to database + try: + async with get_async_session_local()() as session: + await ensure_user_exists(user_id, session) + db_task = DBTask( + id=task_id, + user_id=user_id, + task_type=task.task_type, + status=task.status.value, + url=task.url, + progress=0, + created_at=datetime.utcnow(), + updated_at=datetime.utcnow() + ) + session.add(db_task) + await session.commit() + except Exception as e: + logger.error(f"Error saving task to database: {e}", exc_info=True) + await callback_query.answer("❌ Ошибка при создании задачи", show_alert=True) + return + + # Add to queue + success = await task_queue.add_task(task, check_duplicate_url=True) + if not success: + try: + async with get_async_session_local()() as session: + db_task = await session.get(DBTask, task_id) + if db_task: + await session.delete(db_task) + await session.commit() + except Exception as e: + logger.error(f"Error deleting task from database: {e}") + await callback_query.answer("⚠️ Задача с этим URL уже обрабатывается", show_alert=True) + return + + # Start executor if needed + set_app_client(client) + if not task_executor._running: + await task_executor.start() + + # Send status message + status_message = await callback_query.message.reply( + f"📥 **Загрузка начата**\n\n" + f"🔗 {video_url[:50]}...\n\n" + f"📊 Прогресс: **0%**\n" + f"⏳ Ожидание начала загрузки..." + ) + set_task_message(task_id, status_message.id) + + await callback_query.answer("✅ Загрузка начата") + + # Update original message to show selection + try: + await callback_query.edit_message_reply_markup( + reply_markup=None # Remove buttons + ) + original_text = callback_query.message.text or "" + await callback_query.message.edit_text( + original_text + f"\n\n✅ Выбрано: {video_url[:50]}..." + ) + except Exception as e: + logger.debug(f"Failed to update selection message: {e}") + + except Exception as e: + logger.error(f"Error handling video selection: {e}", exc_info=True) + await callback_query.answer("❌ Ошибка при запуске загрузки", show_alert=True) + else: await callback_query.answer("❓ Неизвестная команда") diff --git a/bot/modules/message_handler/commands.py b/bot/modules/message_handler/commands.py index 75fa284..f9e8da4 100644 --- a/bot/modules/message_handler/commands.py +++ b/bot/modules/message_handler/commands.py @@ -535,6 +535,9 @@ async def url_handler(client: Client, message: Message): from bot.modules.access_control.auth import is_authorized from bot.modules.task_scheduler.queue import task_queue, Task, TaskStatus from bot.modules.task_scheduler.executor import task_executor, set_app_client + from bot.modules.message_handler.filters import is_youtube_url, is_instagram_url + from bot.modules.media_loader.ytdlp import get_videos_list + from pyrogram.types import InlineKeyboardMarkup, InlineKeyboardButton import time # Check authorization @@ -554,6 +557,68 @@ async def url_handler(client: Client, message: Message): ) return + # Check if URL is YouTube or Instagram (direct download) + is_youtube = is_youtube_url(url) + is_instagram = is_instagram_url(url) + + # For non-YouTube/Instagram URLs, check if there are multiple videos + if not is_youtube and not is_instagram: + # Check if URL contains video selection callback data + if url.startswith('video_select:'): + # This is a callback from video selection - extract actual URL + actual_url = url.replace('video_select:', '', 1) + url = actual_url + else: + # Try to get list of videos from webpage + try: + status_msg = await message.reply("🔍 Анализирую страницу...") + from shared.config import settings + videos_info = await get_videos_list(url, cookies_file=settings.COOKIES_FILE) + + if videos_info and videos_info.get('videos'): + videos = videos_info['videos'] + + if len(videos) > 1: + # Multiple videos found - show selection menu + await status_msg.delete() + + playlist_title = videos_info.get('playlist_title', 'Найдено видео') + text = f"📹 **{playlist_title}**\n\n" + text += f"Найдено видео: **{len(videos)}**\n\n" + text += "Выберите видео для загрузки:\n\n" + + # Create inline keyboard with video selection buttons + buttons = [] + for idx, video in enumerate(videos[:10], 1): # Limit to 10 videos + title = video.get('title', f'Видео {idx}')[:50] # Limit title length + duration = video.get('duration') + if duration: + from bot.utils.helpers import format_duration + duration_str = format_duration(duration) + title += f" ({duration_str})" + + # Use callback data format: video_select: + callback_data = f"video_select:{video['url']}" + buttons.append([InlineKeyboardButton(f"{idx}. {title}", callback_data=callback_data)]) + + keyboard = InlineKeyboardMarkup(buttons) + await message.reply(text, reply_markup=keyboard) + return + elif len(videos) == 1: + # Single video found - use its URL + url = videos[0]['url'] + await status_msg.delete() + else: + # No videos found, but continue with original URL (might be direct video link) + await status_msg.delete() + except Exception as e: + logger.error(f"Error getting videos list: {e}", exc_info=True) + # Continue with original URL if error occurs + try: + await status_msg.delete() + except: + pass + # Check concurrent tasks count from bot.config import settings active_tasks_count = await task_queue.get_user_active_tasks_count(user_id)