Add download video by link
This commit is contained in:
@@ -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)
|
logger.error(f"Error getting media info: {e}", exc_info=True)
|
||||||
return None
|
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
|
||||||
|
|
||||||
|
|||||||
@@ -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.edit_message_text(stats_text, reply_markup=keyboard)
|
||||||
await callback_query.answer()
|
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:
|
else:
|
||||||
await callback_query.answer("❓ Неизвестная команда")
|
await callback_query.answer("❓ Неизвестная команда")
|
||||||
|
|
||||||
|
|||||||
@@ -535,6 +535,9 @@ async def url_handler(client: Client, message: Message):
|
|||||||
from bot.modules.access_control.auth import is_authorized
|
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.queue import task_queue, Task, TaskStatus
|
||||||
from bot.modules.task_scheduler.executor import task_executor, set_app_client
|
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
|
import time
|
||||||
|
|
||||||
# Check authorization
|
# Check authorization
|
||||||
@@ -554,6 +557,68 @@ async def url_handler(client: Client, message: Message):
|
|||||||
)
|
)
|
||||||
return
|
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:<video_url>
|
||||||
|
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
|
# Check concurrent tasks count
|
||||||
from bot.config import settings
|
from bot.config import settings
|
||||||
active_tasks_count = await task_queue.get_user_active_tasks_count(user_id)
|
active_tasks_count = await task_queue.get_user_active_tasks_count(user_id)
|
||||||
|
|||||||
Reference in New Issue
Block a user