diff --git a/.DS_Store b/.DS_Store
index 8bdfd39..b730846 100644
Binary files a/.DS_Store and b/.DS_Store differ
diff --git a/bot/downloader.py b/bot/downloader.py
index 441726d..7510fe8 100644
--- a/bot/downloader.py
+++ b/bot/downloader.py
@@ -4,7 +4,7 @@ import os
import logging
import subprocess
import json
-import shutil # Импортируем shutil глобально
+import shutil
from typing import List, Optional
logger = logging.getLogger(__name__)
@@ -47,20 +47,12 @@ def download_video(
"tiktok": {"skip_impersonation": True},
"instagram": {"skip_impersonation": True},
},
- "writedescription": True,
- "writeinfojson": True,
+ "concurrent_fragment_downloads": 4,
+ "http_chunk_size": 10485760,
}
with yt_dlp.YoutubeDL(ydl_opts) as ydl:
- info = ydl.extract_info(url, download=True)
-
- # Сохраняем информацию о видео рядом с файлом
- info_file = out_path.replace('.mp4', '.info.json')
- try:
- with open(info_file, 'w', encoding='utf-8') as f:
- json.dump(info, f, ensure_ascii=False, indent=2)
- except:
- pass
+ ydl.download([url])
# ---------------- ORIGINAL QUALITY ----------------
@@ -86,20 +78,12 @@ def download_original_quality(
"instagram": {"skip_impersonation": True},
},
"format_sort": ["quality", "res", "codec", "size"],
- "writedescription": True,
- "writeinfojson": True,
+ "concurrent_fragment_downloads": 4,
+ "http_chunk_size": 10485760,
}
with yt_dlp.YoutubeDL(ydl_opts) as ydl:
- info = ydl.extract_info(url, download=True)
-
- # Сохраняем информацию о видео
- info_file = out_path.replace('.mp4', '.info.json')
- try:
- with open(info_file, 'w', encoding='utf-8') as f:
- json.dump(info, f, ensure_ascii=False, indent=2)
- except:
- pass
+ ydl.download([url])
# ---------------- TIKTOK MUSIC ONLY ----------------
@@ -121,114 +105,78 @@ def download_tiktok_music(
{
"key": "FFmpegExtractAudio",
"preferredcodec": "mp3",
- "preferredquality": "320",
+ "preferredquality": "192",
},
- {"key": "FFmpegMetadata"},
],
"quiet": True,
"no_warnings": True,
"extractor_args": {
- "tiktok": {
- "skip_impersonation": True,
- },
+ "tiktok": {"skip_impersonation": True},
},
- "writethumbnail": True,
- "writeinfojson": True,
+ "concurrent_fragment_downloads": 4,
+ "http_chunk_size": 10485760,
}
with yt_dlp.YoutubeDL(ydl_opts) as ydl:
- info = ydl.extract_info(url, download=True)
-
- # Сохраняем информацию о звуке
- info_file = out_path.replace('.mp3', '.info.json')
- try:
- with open(info_file, 'w', encoding='utf-8') as f:
- json.dump(info, f, ensure_ascii=False, indent=2)
- except:
- pass
+ ydl.download([url])
-# ---------------- ADD METADATA TO VIDEO/AUDIO ----------------
+# ---------------- ADD METADATA TO AUDIO ----------------
-def add_metadata_to_video(
+def add_metadata_to_audio(
input_path: str,
output_path: str,
metadata: dict,
):
- """Добавляет метаданные к видео или аудио файлу"""
+ """Добавляет метаданные к аудио файлу"""
try:
- # Определяем тип файла
- is_video = input_path.lower().endswith(('.mp4', '.mkv', '.avi', '.mov'))
- is_audio = input_path.lower().endswith(('.mp3', '.m4a', '.flac', '.wav'))
-
- if not (is_video or is_audio):
- # Если не поддерживаемый формат, просто копируем файл
+ # Проверяем metadata
+ if metadata is None:
+ metadata = {}
+
+ if not input_path.lower().endswith('.mp3'):
shutil.copy2(input_path, output_path)
return
- # Подготавливаем аргументы метаданных для ffmpeg
metadata_args = []
+ # Добавляем метаданные
if metadata.get('title'):
- metadata_args.extend(['-metadata', f'title={metadata["title"]}'])
+ metadata_args.extend(['-metadata', f'title={metadata["title"][:100]}'])
- if metadata.get('artist') or metadata.get('uploader'):
- artist = metadata.get('artist') or metadata.get('uploader')
- metadata_args.extend(['-metadata', f'artist={artist}'])
+ if metadata.get('artist'):
+ metadata_args.extend(['-metadata', f'artist={metadata["artist"][:100]}'])
- if metadata.get('description'):
- # Обрезаем описание если слишком длинное
- desc = metadata['description'][:500]
- metadata_args.extend(['-metadata', f'comment={desc}'])
+ if metadata.get('album'):
+ metadata_args.extend(['-metadata', f'album={metadata["album"][:100]}'])
- if metadata.get('url'):
- metadata_args.extend(['-metadata', f'copyright={metadata["url"]}'])
-
- # Для TikTok добавляем дополнительную информацию о музыке
- if metadata.get('track') and metadata.get('artist'):
- metadata_args.extend(['-metadata', f'album={metadata["track"]}'])
-
- if is_video:
- # Для видео файлов
- cmd = [
- 'ffmpeg',
- '-i', input_path,
- '-c', 'copy',
- '-movflags', '+faststart',
- '-y',
- *metadata_args,
- output_path
- ]
- else:
- # Для аудио файлов
- cmd = [
- 'ffmpeg',
- '-i', input_path,
- '-c', 'copy',
- '-id3v2_version', '3',
- '-y',
- *metadata_args,
- output_path
- ]
+ cmd = [
+ 'ffmpeg',
+ '-i', input_path,
+ '-c', 'copy',
+ '-id3v2_version', '3',
+ '-loglevel', 'error',
+ '-y',
+ *metadata_args,
+ output_path
+ ]
result = subprocess.run(
cmd,
capture_output=True,
text=True,
- timeout=60
+ timeout=30
)
if result.returncode != 0:
- logger.error(f"FFmpeg metadata error: {result.stderr}")
- # Если не удалось добавить метаданные, копируем файл как есть
shutil.copy2(input_path, output_path)
except Exception as e:
- logger.error(f"Error adding metadata: {e}")
+ logger.error(f"Metadata error: {e}")
shutil.copy2(input_path, output_path)
-# ---------------- PLAYLIST DOWNLOAD (ИСПРАВЛЕННАЯ ВЕРСИЯ) ----------------
+# ---------------- PLAYLIST DOWNLOAD ----------------
def download_playlist_videos(
playlist_info: dict,
@@ -237,13 +185,12 @@ def download_playlist_videos(
cancel_event: threading.Event,
progress_cb,
) -> List[str]:
- """Скачивает все видео из плейлиста - ИСПРАВЛЕННАЯ ВЕРСИЯ"""
+ """Скачивает все видео из плейлиста"""
downloaded_files = []
- # Создаем yt-dlp объект для загрузки каждого видео отдельно
ydl_opts = {
"format": "best[height<=1080]/best",
- "outtmpl": os.path.join(output_dir, "%(title)s [%(id)s].%(ext)s"), # Добавляем ID для уникальности
+ "outtmpl": os.path.join(output_dir, "%(title)s [%(id)s].%(ext)s"),
"merge_output_format": "mp4",
"cookiefile": cookies,
"progress_hooks": [_progress_hook(cancel_event, progress_cb)],
@@ -251,9 +198,9 @@ def download_playlist_videos(
"no_warnings": True,
"ignoreerrors": True,
"extract_flat": False,
- "writedescription": True,
- "writeinfojson": True,
- "nooverwrites": True, # Не перезаписывать существующие файлы
+ "nooverwrites": True,
+ "concurrent_fragment_downloads": 4,
+ "http_chunk_size": 10485760,
}
try:
@@ -333,29 +280,18 @@ def download_audio(
{
"key": "FFmpegExtractAudio",
"preferredcodec": "mp3",
- "preferredquality": "320",
+ "preferredquality": "192",
},
- {"key": "FFmpegMetadata"},
- {"key": "EmbedThumbnail"},
],
- "writethumbnail": True,
"quiet": True,
"no_warnings": True,
"extractor_args": {
"tiktok": {"skip_impersonation": True},
"instagram": {"skip_impersonation": True},
},
- "writedescription": True,
- "writeinfojson": True,
+ "concurrent_fragment_downloads": 4,
+ "http_chunk_size": 10485760,
}
with yt_dlp.YoutubeDL(ydl_opts) as ydl:
- info = ydl.extract_info(url, download=True)
-
- # Сохраняем информацию о аудио
- info_file = out_path.replace('.mp3', '.info.json')
- try:
- with open(info_file, 'w', encoding='utf-8') as f:
- json.dump(info, f, ensure_ascii=False, indent=2)
- except:
- pass
\ No newline at end of file
+ ydl.download([url])
\ No newline at end of file
diff --git a/bot/keyboards.py b/bot/keyboards.py
index 84cedf8..6a3274c 100644
--- a/bot/keyboards.py
+++ b/bot/keyboards.py
@@ -5,6 +5,7 @@ def quality_keyboard():
"""Клавиатура выбора качества"""
keyboard = [
[
+ InlineKeyboardButton(text="📹 480p", callback_data="q:480"),
InlineKeyboardButton(text="📹 720p", callback_data="q:720"),
InlineKeyboardButton(text="🎬 1080p", callback_data="q:1080"),
InlineKeyboardButton(text="🎥 1440p", callback_data="q:1440"),
@@ -48,19 +49,13 @@ def playlist_keyboard(confirm=False):
def platform_keyboard(platform):
- """Клавиатура для Instagram/TikTok"""
- if platform == "instagram":
- text = "📸 Instagram"
- elif platform == "tiktok":
- text = "🎵 TikTok"
- else:
- text = platform.capitalize()
-
+ """Клавиатура для Instagram"""
keyboard = [
[
InlineKeyboardButton(text=f"🎬 Оригинальное качество", callback_data="original_quality"),
],
[
+ InlineKeyboardButton(text="📹 480p", callback_data="q:480"),
InlineKeyboardButton(text="📹 720p", callback_data="q:720"),
InlineKeyboardButton(text="🎬 1080p", callback_data="q:1080"),
],
@@ -69,4 +64,26 @@ def platform_keyboard(platform):
InlineKeyboardButton(text="❌ Отмена", callback_data="cancel"),
]
]
+ return InlineKeyboardMarkup(inline_keyboard=keyboard)
+
+
+def tiktok_keyboard():
+ """Специальная клавиатура для TikTok"""
+ keyboard = [
+ [
+ InlineKeyboardButton(text="🎬 Оригинальное качество", callback_data="original_quality"),
+ ],
+ [
+ InlineKeyboardButton(text="📹 480p", callback_data="q:480"),
+ InlineKeyboardButton(text="📹 720p", callback_data="q:720"),
+ InlineKeyboardButton(text="🎬 1080p", callback_data="q:1080"),
+ ],
+ [
+ InlineKeyboardButton(text="🎧 Аудио из видео", callback_data="audio"),
+ InlineKeyboardButton(text="🎵 Только звук (TikTok)", callback_data="tiktok_music"),
+ ],
+ [
+ InlineKeyboardButton(text="❌ Отмена", callback_data="cancel"),
+ ]
+ ]
return InlineKeyboardMarkup(inline_keyboard=keyboard)
\ No newline at end of file
diff --git a/bot/main.py b/bot/main.py
index a6aa713..4575f2b 100644
--- a/bot/main.py
+++ b/bot/main.py
@@ -4,7 +4,6 @@ import threading
import logging
import subprocess
import time
-import json
from datetime import datetime
from typing import Optional
@@ -36,8 +35,8 @@ from downloader import (
download_original_quality,
download_playlist_videos,
download_tiktok_music,
- add_metadata_to_video,
- DownloadCancelled
+ add_metadata_to_audio, # Только для аудио
+ DownloadCancelled,
)
from middleware import PrivateMiddleware
from rate_limit import check_rate_limit
@@ -123,7 +122,7 @@ def cleanup_old_cache():
files_with_mtime.sort(key=lambda x: x[1])
# Удаляем старые файлы пока не достигнем лимита
- target_size_mb = CACHE_MAX_SIZE_MB * 0.8 # Оставляем 80% от лимита
+ target_size_mb = CACHE_MAX_SIZE_MB * 0.8
for file_path, mtime, size in files_with_mtime:
if total_size_mb <= target_size_mb:
@@ -203,13 +202,11 @@ def make_progress_cb(loop, message):
downloaded = d.get("downloaded_bytes", 0)
total = d.get("total_bytes") or d.get("total_bytes_estimate") or 1
- # Избегаем деления на ноль
if total <= 0:
return
percent = min(100, downloaded * 100 / total)
- # Обновляем раз в ~2% и не чаще чем раз в 2 секунды
current_time = time.time()
if percent - last_percent["value"] < 2 and current_time - last_update["time"] < 2:
return
@@ -220,7 +217,6 @@ def make_progress_cb(loop, message):
bar = render_bar(percent)
eta = d.get("eta")
- # Безопасное форматирование ETA
if eta is None or eta == "?":
eta_str = "?"
else:
@@ -255,7 +251,6 @@ def make_playlist_progress_cb(loop, message, total_videos: int):
async def update(d):
try:
- # Обновляем не чаще чем раз в 3 секунды
current_time = time.time()
if current_time - last_update["time"] < 3:
return
@@ -284,12 +279,12 @@ def make_playlist_progress_cb(loop, message, total_videos: int):
return cb
-def optimize_for_telegram(input_path: str, output_path: str, metadata: dict = None) -> bool:
+def optimize_for_telegram(input_path: str, output_path: str) -> bool:
"""
- Оптимизирует видео для телеграма с добавлением метаданных
+ Оптимизирует видео для телеграма (без метаданных)
"""
try:
- import shutil # Импортируем здесь
+ import shutil
# Проверяем размер файла
file_size_mb = os.path.getsize(input_path) / (1024 * 1024)
@@ -300,21 +295,6 @@ def optimize_for_telegram(input_path: str, output_path: str, metadata: dict = No
else:
crf = 23
- # Подготавливаем метаданные для ffmpeg
- metadata_args = []
- if metadata:
- if metadata.get('title'):
- metadata_args.extend(['-metadata', f'title={metadata["title"]}'])
- if metadata.get('artist') or metadata.get('uploader'):
- artist = metadata.get('artist') or metadata.get('uploader')
- metadata_args.extend(['-metadata', f'artist={artist}'])
- if metadata.get('description'):
- # Обрезаем описание если слишком длинное для метаданных
- desc = metadata['description'][:1000]
- metadata_args.extend(['-metadata', f'comment={desc}'])
- if metadata.get('url'):
- metadata_args.extend(['-metadata', f'copyright={metadata["url"]}'])
-
cmd = [
'ffmpeg',
'-i', input_path,
@@ -326,7 +306,6 @@ def optimize_for_telegram(input_path: str, output_path: str, metadata: dict = No
'-movflags', '+faststart',
'-vf', 'scale=trunc(iw/2)*2:trunc(ih/2)*2',
'-y',
- *metadata_args,
output_path
]
@@ -351,6 +330,88 @@ def optimize_for_telegram(input_path: str, output_path: str, metadata: dict = No
return False
+def get_video_info_for_audio(video_info: dict) -> dict:
+ """Получает информацию о видео для метаданных аудио"""
+ if not video_info:
+ return {}
+
+ metadata = {
+ 'title': video_info.get('title', ''),
+ 'artist': video_info.get('uploader', ''),
+ 'album': video_info.get('title', '')[:50],
+ 'url': video_info.get('webpage_url', ''),
+ }
+
+ # Для TikTok добавляем информацию о музыке
+ if video_info.get('extractor') == 'TikTok':
+ if video_info.get('track'):
+ metadata['title'] = video_info.get('track', metadata['title'])
+ if video_info.get('artist'):
+ metadata['artist'] = video_info.get('artist', metadata['artist'])
+
+ return metadata
+
+
+# -------------------- handlers --------------------
+
+@dp.message(F.text == "/start")
+async def start(message: Message):
+ await message.answer(
+ "👋 Привет!\n\n"
+ "📥 Я скачиваю видео и звук из видео по ссылке.\n\n"
+ "✨ Новые возможности:\n"
+ "• 🎬 Оригинальное качество (Instagram, TikTok)\n"
+ "• 📁 Плейлисты YouTube\n"
+ "• 🎵 Отдельный звук для TikTok\n"
+ "• 🔄 Автоочистка кэша\n\n"
+ "👉 Просто отправь ссылку.",
+ parse_mode="HTML"
+ )
+
+
+@dp.message(F.text == "/help")
+async def help_command(message: Message):
+ await message.answer(
+ "📚 Справка по командам:\n\n"
+ "• /start - Начать работу\n"
+ "• /help - Эта справка\n"
+ "• /cache_stats - Статистика кэша\n\n"
+ "✨ Поддерживаемые платформы:\n"
+ "• YouTube (видео и плейлисты)\n"
+ "• TikTok (оригинальное качество + звук отдельно)\n"
+ "• Instagram (Reels, видео, IGTV)\n"
+ "• Twitter/X, Facebook, VK и другие\n\n"
+ "🎯 Особенности:\n"
+ "• 🎵 TikTok: можно скачать отдельно звук\n"
+ "• 🎬 Instagram/TikTok: оригинальное качество\n"
+ "• 📁 Плейлисты: загрузка всех видео\n"
+ "• 🎧 Аудио: извлечение звука из любого видео",
+ parse_mode="HTML"
+ )
+
+
+@dp.message(F.text == "/cache_stats")
+async def cache_stats(message: Message):
+ """Показывает статистику кэша"""
+ try:
+ total_size_mb = get_cache_size_mb()
+ file_count = 0
+
+ for root, dirs, files in os.walk(CACHE_DIR):
+ file_count += len(files)
+
+ await message.answer(
+ f"📊 Статистика кэша:\n\n"
+ f"📁 Файлов: {file_count}\n"
+ f"💾 Размер: {total_size_mb:.2f} МБ\n"
+ f"⏰ Очистка: ежедневно в 3:00",
+ parse_mode="HTML"
+ )
+ except Exception as e:
+ logger.error(f"Error getting cache stats: {e}")
+ await message.answer("❌ Ошибка при получении статистики кэша")
+
+
@dp.message(F.text.startswith("http"))
async def handle_link(message: Message):
url = message.text.strip()
@@ -451,25 +512,11 @@ async def handle_tiktok_music(callback: CallbackQuery):
await status.edit_text("📤 Отправляю звук из кэша…", parse_mode="HTML")
try:
await callback.message.answer_audio(FSInputFile(final_path))
-
- # Получаем информацию о звуке для описания
- try:
- info = await asyncio.to_thread(extract_info, url, COOKIES_FILE)
- if info:
- metadata = get_video_description(info)
- if metadata.get('track') or metadata.get('artist'):
- desc = f"🎵 Звук из TikTok\n\n"
- if metadata.get('track'):
- desc += f"🎶 Трек: {metadata['track']}\n"
- if metadata.get('artist'):
- desc += f"👤 Исполнитель: {metadata['artist']}\n"
- if metadata.get('url'):
- desc += f"🔗 Оригинал: {metadata['url']}"
-
- await callback.message.answer(desc, parse_mode="HTML")
- except:
- pass
-
+ size_mb = os.path.getsize(final_path) / 1024 / 1024
+ await callback.message.answer(
+ f"✅ Готово! (Звук из TikTok)\n📦 Размер: {size_mb:.1f} МБ",
+ parse_mode="HTML"
+ )
except Exception as e:
logger.error(f"Error sending cached audio: {e}")
await status.edit_text("❌ Ошибка при отправке звука")
@@ -512,13 +559,13 @@ async def handle_tiktok_music(callback: CallbackQuery):
try:
info = await asyncio.to_thread(extract_info, url, COOKIES_FILE)
if info:
- metadata = get_video_description(info)
- await asyncio.to_thread(add_metadata_to_video, tmp_path, tmp_path + "_meta.mp3", metadata)
+ metadata = get_video_info_for_audio(info)
+ await asyncio.to_thread(add_metadata_to_audio, tmp_path, tmp_path + "_meta.mp3", metadata)
if os.path.exists(tmp_path + "_meta.mp3"):
os.remove(tmp_path)
os.rename(tmp_path + "_meta.mp3", tmp_path)
except Exception as e:
- logger.error(f"Error adding metadata: {e}")
+ logger.error(f"Error adding metadata to audio: {e}")
# Перемещаем файл в кэш
if os.path.exists(final_path):
@@ -546,29 +593,11 @@ async def handle_tiktok_music(callback: CallbackQuery):
try:
await callback.message.answer_audio(FSInputFile(final_path))
-
- # Отправляем описание звука
- try:
- info = await asyncio.to_thread(extract_info, url, COOKIES_FILE)
- if info:
- metadata = get_video_description(info)
- if metadata.get('track') or metadata.get('artist'):
- desc = f"🎵 Звук из TikTok\n\n"
- if metadata.get('track'):
- desc += f"🎶 Трек: {metadata['track']}\n"
- if metadata.get('artist'):
- desc += f"👤 Исполнитель: {metadata['artist']}\n"
- if metadata.get('title'):
- desc += f"📝 Видео: {metadata['title'][:100]}...\n"
- if metadata.get('uploader'):
- desc += f"👤 Автор: {metadata['uploader']}\n"
- if metadata.get('url'):
- desc += f"🔗 Оригинал: {metadata['url']}"
-
- await callback.message.answer(desc, parse_mode="HTML")
- except:
- pass
-
+ size_mb = os.path.getsize(final_path) / 1024 / 1024
+ await callback.message.answer(
+ f"✅ Готово! (Звук из TikTok)\n📦 Размер: {size_mb:.1f} МБ",
+ parse_mode="HTML"
+ )
except Exception as e:
logger.error(f"Error sending audio: {e}")
await status.edit_text("❌ Ошибка при отправке звука")
@@ -595,13 +624,6 @@ async def handle_original_quality(callback: CallbackQuery):
await callback.message.edit_reply_markup(reply_markup=None)
status = await callback.message.answer("🎬 Загрузка в оригинальном качестве…", parse_mode="HTML")
- # Получаем информацию о видео для метаданных
- video_info = None
- try:
- video_info = await asyncio.to_thread(extract_info, url, COOKIES_FILE)
- except:
- pass
-
key = cache_key(url, "original", audio=False)
final_path = cache_path(CACHE_DIR, key, "mp4")
tmp_path = os.path.join(TMP_DIR, f"{key}.mp4")
@@ -612,28 +634,10 @@ async def handle_original_quality(callback: CallbackQuery):
try:
await callback.message.answer_video(FSInputFile(final_path))
size_mb = os.path.getsize(final_path) / 1024 / 1024
-
- # Отправляем описание
- if video_info:
- metadata = get_video_description(video_info)
- desc = format_description(metadata)
- if desc:
- await callback.message.answer(
- f"✅ Готово! (Оригинальное качество)\n"
- f"📦 Размер: {size_mb:.1f} МБ\n\n"
- f"{desc}",
- parse_mode="HTML"
- )
- else:
- await callback.message.answer(
- f"✅ Готово! (Оригинальное качество)\n📦 Размер: {size_mb:.1f} МБ",
- parse_mode="HTML"
- )
- else:
- await callback.message.answer(
- f"✅ Готово! (Оригинальное качество)\n📦 Размер: {size_mb:.1f} МБ",
- parse_mode="HTML"
- )
+ await callback.message.answer(
+ f"✅ Готово! (Оригинальное качество)\n📦 Размер: {size_mb:.1f} МБ",
+ parse_mode="HTML"
+ )
except Exception as e:
logger.error(f"Error sending cached file: {e}")
await status.edit_text("❌ Ошибка при отправке файла")
@@ -660,14 +664,6 @@ async def handle_original_quality(callback: CallbackQuery):
await status.edit_text("⛔ Загрузка отменена")
return
- # Добавляем метаданные к видео
- if video_info:
- metadata = get_video_description(video_info)
- await asyncio.to_thread(add_metadata_to_video, tmp_path, tmp_path + "_meta.mp4", metadata)
- if os.path.exists(tmp_path + "_meta.mp4"):
- os.remove(tmp_path)
- os.rename(tmp_path + "_meta.mp4", tmp_path)
-
os.rename(tmp_path, final_path)
except DownloadCancelled:
@@ -695,55 +691,19 @@ async def handle_original_quality(callback: CallbackQuery):
supports_streaming=True
)
size_mb = os.path.getsize(final_path) / 1024 / 1024
-
- # Отправляем описание
- if video_info:
- metadata = get_video_description(video_info)
- desc = format_description(metadata)
- if desc:
- await callback.message.answer(
- f"✅ Готово! (Оригинальное качество)\n"
- f"📦 Размер: {size_mb:.1f} МБ\n\n"
- f"{desc}",
- parse_mode="HTML"
- )
- else:
- await callback.message.answer(
- f"✅ Готово! (Оригинальное качество)\n📦 Размер: {size_mb:.1f} МБ",
- parse_mode="HTML"
- )
- else:
- await callback.message.answer(
- f"✅ Готово! (Оригинальное качество)\n📦 Размер: {size_mb:.1f} МБ",
- parse_mode="HTML"
- )
+ await callback.message.answer(
+ f"✅ Готово! (Оригинальное качество)\n📦 Размер: {size_mb:.1f} МБ",
+ parse_mode="HTML"
+ )
except Exception as e:
logger.error(f"Error sending video: {e}")
try:
await callback.message.answer_document(FSInputFile(final_path))
size_mb = os.path.getsize(final_path) / 1024 / 1024
-
- # Отправляем описание даже если отправлено как документ
- if video_info:
- metadata = get_video_description(video_info)
- desc = format_description(metadata)
- if desc:
- await callback.message.answer(
- f"✅ Отправлено как документ (Оригинальное качество)\n"
- f"📦 Размер: {size_mb:.1f} МБ\n\n"
- f"{desc}",
- parse_mode="HTML"
- )
- else:
- await callback.message.answer(
- f"✅ Отправлено как документ (Оригинальное качество)\n📦 Размер: {size_mb:.1f} МБ",
- parse_mode="HTML"
- )
- else:
- await callback.message.answer(
- f"✅ Отправлено как документ (Оригинальное качество)\n📦 Размер: {size_mb:.1f} МБ",
- parse_mode="HTML"
- )
+ await callback.message.answer(
+ f"✅ Отправлено как документ (Оригинальное качество)\n📦 Размер: {size_mb:.1f} МБ",
+ parse_mode="HTML"
+ )
except Exception as e2:
logger.error(f"Error sending as document: {e2}")
await status.edit_text("❌ Ошибка при отправке файла")
@@ -773,17 +733,6 @@ async def handle_video(callback: CallbackQuery):
await callback.message.edit_reply_markup(reply_markup=None)
status = await callback.message.answer("🔍 Анализирую ссылку…", parse_mode="HTML")
- # Извлекаем информацию о видео для метаданных
- try:
- video_info = await asyncio.to_thread(extract_info, url, COOKIES_FILE)
- if not video_info:
- await status.edit_text("❌ Не удалось получить информацию о видео")
- return
- except Exception as e:
- logger.error(f"Error extracting info: {e}")
- await status.edit_text("❌ Ошибка при анализе ссылки")
- return
-
key = cache_key(url, quality, audio=False)
final_path = cache_path(CACHE_DIR, key, "mp4")
tmp_path = os.path.join(TMP_DIR, f"{key}.mp4")
@@ -795,22 +744,10 @@ async def handle_video(callback: CallbackQuery):
try:
await callback.message.answer_video(FSInputFile(final_path))
size_mb = os.path.getsize(final_path) / 1024 / 1024
-
- # Отправляем описание
- metadata = get_video_description(video_info)
- desc = format_description(metadata)
- if desc:
- await callback.message.answer(
- f"✅ Готово!\n"
- f"📦 Размер: {size_mb:.1f} МБ\n\n"
- f"{desc}",
- parse_mode="HTML"
- )
- else:
- await callback.message.answer(
- f"✅ Готово!\n📦 Размер: {size_mb:.1f} МБ",
- parse_mode="HTML"
- )
+ await callback.message.answer(
+ f"✅ Готово!\n📦 Размер: {size_mb:.1f} МБ",
+ parse_mode="HTML"
+ )
except Exception as e:
logger.error(f"Error sending cached file: {e}")
await status.edit_text("❌ Ошибка при отправке файла")
@@ -839,13 +776,10 @@ async def handle_video(callback: CallbackQuery):
await status.edit_text("⛔ Загрузка отменена")
return
- # Получаем метаданные для видео
- metadata = get_video_description(video_info)
-
- # Оптимизируем видео для телеграма с добавлением метаданных
+ # Оптимизируем видео для телеграма (кроме оригинального качества)
if quality != "original":
await status.edit_text("⚙️ Оптимизирую видео для телеграма…", parse_mode="HTML")
- await asyncio.to_thread(optimize_for_telegram, tmp_path, optimized_path, metadata)
+ await asyncio.to_thread(optimize_for_telegram, tmp_path, optimized_path)
# Удаляем исходный файл и используем оптимизированный
if os.path.exists(tmp_path):
@@ -853,11 +787,7 @@ async def handle_video(callback: CallbackQuery):
os.rename(optimized_path, final_path)
else:
- # Для оригинального качества не оптимизируем, но добавляем метаданны
- await asyncio.to_thread(add_metadata_to_video, tmp_path, tmp_path + "_meta.mp4", metadata)
- if os.path.exists(tmp_path + "_meta.mp4"):
- os.remove(tmp_path)
- os.rename(tmp_path + "_meta.mp4", tmp_path)
+ # Для оригинального качества не оптимизируем
os.rename(tmp_path, final_path)
except DownloadCancelled:
@@ -887,41 +817,19 @@ async def handle_video(callback: CallbackQuery):
supports_streaming=True
)
size_mb = os.path.getsize(final_path) / 1024 / 1024
-
- # Отправляем описание
- desc = format_description(metadata)
- if desc:
- await callback.message.answer(
- f"✅ Готово!\n"
- f"📦 Размер: {size_mb:.1f} МБ\n\n"
- f"{desc}",
- parse_mode="HTML"
- )
- else:
- await callback.message.answer(
- f"✅ Готово!\n📦 Размер: {size_mb:.1f} МБ",
- parse_mode="HTML"
- )
+ await callback.message.answer(
+ f"✅ Готово!\n📦 Размер: {size_mb:.1f} МБ",
+ parse_mode="HTML"
+ )
except Exception as e:
logger.error(f"Error sending video: {e}")
try:
await callback.message.answer_document(FSInputFile(final_path))
size_mb = os.path.getsize(final_path) / 1024 / 1024
-
- # Отправляем описание даже если отправлено как документ
- desc = format_description(metadata)
- if desc:
- await callback.message.answer(
- f"✅ Отправлено как документ\n"
- f"📦 Размер: {size_mb:.1f} МБ\n\n"
- f"{desc}",
- parse_mode="HTML"
- )
- else:
- await callback.message.answer(
- f"✅ Отправлено как документ\n📦 Размер: {size_mb:.1f} МБ",
- parse_mode="HTML"
- )
+ await callback.message.answer(
+ f"✅ Отправлено как документ\n📦 Размер: {size_mb:.1f} МБ",
+ parse_mode="HTML"
+ )
except Exception as e2:
logger.error(f"Error sending as document: {e2}")
await status.edit_text("❌ Ошибка при отправке файла")
@@ -971,29 +879,10 @@ async def handle_audio(callback: CallbackQuery):
try:
await callback.message.answer_audio(FSInputFile(final_path))
size_mb = os.path.getsize(final_path) / 1024 / 1024
-
- # Отправляем описание
- if video_info:
- metadata = get_video_description(video_info)
- desc = f"🎧 Аудио из видео\n\n"
- if metadata.get('title'):
- desc += f"🎬 {metadata['title']}\n"
- if metadata.get('uploader'):
- desc += f"👤 Автор: {metadata['uploader']}\n"
- if metadata.get('url'):
- desc += f"🔗 Оригинал: {metadata['url']}"
-
- await callback.message.answer(
- f"✅ Готово!\n"
- f"📦 Размер: {size_mb:.1f} МБ\n\n"
- f"{desc}",
- parse_mode="HTML"
- )
- else:
- await callback.message.answer(
- f"✅ Готово!\n📦 Размер: {size_mb:.1f} МБ",
- parse_mode="HTML"
- )
+ await callback.message.answer(
+ f"✅ Готово!\n📦 Размер: {size_mb:.1f} МБ",
+ parse_mode="HTML"
+ )
except Exception as e:
logger.error(f"Error sending cached audio: {e}")
await status.edit_text("❌ Ошибка при отправке аудио")
@@ -1033,8 +922,8 @@ async def handle_audio(callback: CallbackQuery):
# Добавляем метаданные к аудио
if video_info:
- metadata = get_video_description(video_info)
- await asyncio.to_thread(add_metadata_to_video, tmp_path, tmp_path + "_meta.mp3", metadata)
+ metadata = get_video_info_for_audio(video_info)
+ await asyncio.to_thread(add_metadata_to_audio, tmp_path, tmp_path + "_meta.mp3", metadata)
if os.path.exists(tmp_path + "_meta.mp3"):
os.remove(tmp_path)
os.rename(tmp_path + "_meta.mp3", tmp_path)
@@ -1066,33 +955,10 @@ async def handle_audio(callback: CallbackQuery):
try:
await callback.message.answer_audio(FSInputFile(final_path))
size_mb = os.path.getsize(final_path) / 1024 / 1024
-
- # Отправляем описание
- if video_info:
- metadata = get_video_description(video_info)
- desc = f"🎧 Аудио из видео\n\n"
- if metadata.get('title'):
- desc += f"🎬 {metadata['title']}\n"
- if metadata.get('uploader'):
- desc += f"👤 Автор: {metadata['uploader']}\n"
- if metadata.get('duration'):
- minutes = metadata['duration'] // 60
- seconds = metadata['duration'] % 60
- desc += f"⏱ Длительность: {minutes}:{seconds:02d}\n"
- if metadata.get('url'):
- desc += f"🔗 Оригинал: {metadata['url']}"
-
- await callback.message.answer(
- f"✅ Готово!\n"
- f"📦 Размер: {size_mb:.1f} МБ\n\n"
- f"{desc}",
- parse_mode="HTML"
- )
- else:
- await callback.message.answer(
- f"✅ Готово!\n📦 Размер: {size_mb:.1f} МБ",
- parse_mode="HTML"
- )
+ await callback.message.answer(
+ f"✅ Готово!\n📦 Размер: {size_mb:.1f} МБ",
+ parse_mode="HTML"
+ )
except Exception as e:
logger.error(f"Error sending audio: {e}")
await status.edit_text("❌ Ошибка при отправке аудио")
@@ -1269,6 +1135,47 @@ async def download_playlist_confirm(callback, user_id, playlist_info, status):
finally:
ACTIVE_DOWNLOADS.pop(user_id, None)
cleanup_tmp(TMP_DIR)
+
+
+@dp.callback_query(F.data == "playlist_confirm_no")
+async def handle_playlist_cancel(callback: CallbackQuery):
+ await callback.answer("Отменено", show_alert=True)
+ await callback.message.edit_reply_markup(reply_markup=None)
+
+
+@dp.callback_query(F.data == "playlist_first")
+async def handle_playlist_first(callback: CallbackQuery):
+ await callback.answer()
+ user_id = callback.from_user.id
+ url = USER_URLS.get(user_id)
+
+ if not url:
+ await callback.message.answer("❌ Ссылка не найдена")
+ return
+
+ # Получаем первое видео из плейлиста
+ try:
+ from info import get_first_video_from_playlist
+ video_url = await asyncio.to_thread(get_first_video_from_playlist, url)
+
+ if not video_url:
+ await callback.message.answer("❌ Не удалось получить видео из плейлиста")
+ return
+
+ # Сохраняем новую ссылку и показываем меню выбора качества
+ USER_URLS[user_id] = video_url
+ await callback.message.edit_reply_markup(reply_markup=None)
+ await callback.message.answer(
+ "🔽 Выбери формат загрузки для первого видео:",
+ reply_markup=quality_keyboard(),
+ parse_mode="HTML"
+ )
+
+ except Exception as e:
+ logger.error(f"Error getting first video: {e}")
+ await callback.message.answer("❌ Ошибка при получении видео из плейлиста")
+
+
# -------------------- entrypoint --------------------
async def main():