Добавление функций, описание ко всем видео файлам, cкачивать отдельно звук из TikTok с информацией о музыке, cохранять метаданные в файлы
This commit is contained in:
@@ -2,6 +2,8 @@ import yt_dlp
|
||||
import threading
|
||||
import os
|
||||
import logging
|
||||
import subprocess
|
||||
import json
|
||||
from typing import List, Optional
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -16,9 +18,7 @@ def _progress_hook(cancel_event, progress_cb):
|
||||
if cancel_event.is_set():
|
||||
raise DownloadCancelled("Cancelled by user")
|
||||
|
||||
if d["status"] == "downloading" and progress_cb:
|
||||
progress_cb(d)
|
||||
elif d["status"] == "finished" and progress_cb:
|
||||
if d["status"] in ["downloading", "finished"] and progress_cb:
|
||||
progress_cb(d)
|
||||
|
||||
return hook
|
||||
@@ -46,13 +46,21 @@ def download_video(
|
||||
"tiktok": {"skip_impersonation": True},
|
||||
"instagram": {"skip_impersonation": True},
|
||||
},
|
||||
"writedescription": True, # Сохраняем описание
|
||||
"writeinfojson": True, # Сохраняем информацию в JSON
|
||||
}
|
||||
|
||||
with yt_dlp.YoutubeDL(ydl_opts) as ydl:
|
||||
ydl.download([url])
|
||||
info = ydl.extract_info(url, download=True)
|
||||
|
||||
# Сохраняем информацию о видео рядом с файлом
|
||||
info_file = out_path.replace('.mp4', '.info.json')
|
||||
if os.path.exists(info_file):
|
||||
with open(info_file, 'w', encoding='utf-8') as f:
|
||||
json.dump(info, f, ensure_ascii=False, indent=2)
|
||||
|
||||
|
||||
# ---------------- ORIGINAL QUALITY (Instagram/TikTok) ----------------
|
||||
# ---------------- ORIGINAL QUALITY ----------------
|
||||
|
||||
def download_original_quality(
|
||||
url: str,
|
||||
@@ -61,7 +69,7 @@ def download_original_quality(
|
||||
cancel_event: threading.Event,
|
||||
progress_cb,
|
||||
):
|
||||
"""Скачивает видео в оригинальном качестве (без перекодировки)"""
|
||||
"""Скачивает видео в оригинальном качестве"""
|
||||
ydl_opts = {
|
||||
"format": "best", # Лучшее качество доступное
|
||||
"outtmpl": out_path,
|
||||
@@ -74,12 +82,149 @@ def download_original_quality(
|
||||
"tiktok": {"skip_impersonation": True},
|
||||
"instagram": {"skip_impersonation": True},
|
||||
},
|
||||
# Для Instagram и TikTok используем специфичные форматы
|
||||
"format_sort": ["quality", "res", "codec", "size"],
|
||||
"writedescription": True,
|
||||
"writeinfojson": True,
|
||||
}
|
||||
|
||||
with yt_dlp.YoutubeDL(ydl_opts) as ydl:
|
||||
ydl.download([url])
|
||||
info = ydl.extract_info(url, download=True)
|
||||
|
||||
# Сохраняем информацию о видео
|
||||
info_file = out_path.replace('.mp4', '.info.json')
|
||||
if os.path.exists(info_file):
|
||||
with open(info_file, 'w', encoding='utf-8') as f:
|
||||
json.dump(info, f, ensure_ascii=False, indent=2)
|
||||
|
||||
|
||||
# ---------------- TIKTOK MUSIC ONLY ----------------
|
||||
|
||||
def download_tiktok_music(
|
||||
url: str,
|
||||
out_path: str,
|
||||
cookies: str | None,
|
||||
cancel_event: threading.Event,
|
||||
progress_cb,
|
||||
):
|
||||
"""Скачивает только звук из TikTok (оригинальную музыку)"""
|
||||
ydl_opts = {
|
||||
"format": "bestaudio/best",
|
||||
"outtmpl": out_path.replace('.mp3', ''),
|
||||
"cookiefile": cookies,
|
||||
"progress_hooks": [_progress_hook(cancel_event, progress_cb)],
|
||||
"postprocessors": [
|
||||
{
|
||||
"key": "FFmpegExtractAudio",
|
||||
"preferredcodec": "mp3",
|
||||
"preferredquality": "320",
|
||||
},
|
||||
{"key": "FFmpegMetadata"},
|
||||
],
|
||||
"quiet": True,
|
||||
"no_warnings": True,
|
||||
"extractor_args": {
|
||||
"tiktok": {
|
||||
"skip_impersonation": True,
|
||||
"music": True, # Специальный флаг для музыки TikTok
|
||||
},
|
||||
},
|
||||
"writethumbnail": True,
|
||||
"writeinfojson": True,
|
||||
# Для TikTok стараемся получить оригинальный звук
|
||||
"extract_flat": False,
|
||||
}
|
||||
|
||||
with yt_dlp.YoutubeDL(ydl_opts) as ydl:
|
||||
info = ydl.extract_info(url, download=True)
|
||||
|
||||
# Сохраняем информацию о звуке
|
||||
info_file = out_path.replace('.mp3', '.info.json')
|
||||
if os.path.exists(info_file):
|
||||
with open(info_file, 'w', encoding='utf-8') as f:
|
||||
json.dump(info, f, ensure_ascii=False, indent=2)
|
||||
|
||||
|
||||
# ---------------- ADD METADATA TO VIDEO/AUDIO ----------------
|
||||
|
||||
def add_metadata_to_video(
|
||||
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):
|
||||
# Если не поддерживаемый формат, просто копируем файл
|
||||
import shutil
|
||||
shutil.copy2(input_path, output_path)
|
||||
return
|
||||
|
||||
# Подготавливаем аргументы метаданных для ffmpeg
|
||||
metadata_args = []
|
||||
|
||||
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'][:500]
|
||||
metadata_args.extend(['-metadata', f'comment={desc}'])
|
||||
|
||||
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
|
||||
]
|
||||
|
||||
result = subprocess.run(
|
||||
cmd,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=60
|
||||
)
|
||||
|
||||
if result.returncode != 0:
|
||||
logger.error(f"FFmpeg metadata error: {result.stderr}")
|
||||
# Если не удалось добавить метаданные, копируем файл как есть
|
||||
import shutil
|
||||
shutil.copy2(input_path, output_path)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error adding metadata: {e}")
|
||||
import shutil
|
||||
shutil.copy2(input_path, output_path)
|
||||
|
||||
|
||||
# ---------------- PLAYLIST DOWNLOAD ----------------
|
||||
@@ -102,13 +247,14 @@ def download_playlist_videos(
|
||||
"progress_hooks": [_progress_hook(cancel_event, progress_cb)],
|
||||
"quiet": True,
|
||||
"no_warnings": True,
|
||||
"ignoreerrors": True, # Игнорируем ошибки при загрузке отдельных видео
|
||||
"ignoreerrors": True,
|
||||
"extract_flat": False,
|
||||
"writedescription": True,
|
||||
"writeinfojson": True,
|
||||
}
|
||||
|
||||
try:
|
||||
with yt_dlp.YoutubeDL(ydl_opts) as ydl:
|
||||
# Скачиваем каждое видео по отдельности
|
||||
for entry in playlist_info.get('entries', []):
|
||||
if cancel_event.is_set():
|
||||
raise DownloadCancelled("Cancelled by user")
|
||||
@@ -137,7 +283,7 @@ def download_playlist_videos(
|
||||
raise
|
||||
|
||||
|
||||
# ---------------- AUDIO FROM VIDEO ----------------
|
||||
# ---------------- STANDARD AUDIO ----------------
|
||||
|
||||
def download_audio(
|
||||
url: str,
|
||||
@@ -167,7 +313,15 @@ def download_audio(
|
||||
"tiktok": {"skip_impersonation": True},
|
||||
"instagram": {"skip_impersonation": True},
|
||||
},
|
||||
"writedescription": True,
|
||||
"writeinfojson": True,
|
||||
}
|
||||
|
||||
with yt_dlp.YoutubeDL(ydl_opts) as ydl:
|
||||
ydl.download([url])
|
||||
info = ydl.extract_info(url, download=True)
|
||||
|
||||
# Сохраняем информацию о аудио
|
||||
info_file = out_path.replace('.mp3', '.info.json')
|
||||
if os.path.exists(info_file):
|
||||
with open(info_file, 'w', encoding='utf-8') as f:
|
||||
json.dump(info, f, ensure_ascii=False, indent=2)
|
||||
658
bot/main.py
658
bot/main.py
@@ -4,6 +4,7 @@ import threading
|
||||
import logging
|
||||
import subprocess
|
||||
import time
|
||||
import json
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
|
||||
@@ -26,13 +27,16 @@ from keyboards import (
|
||||
quality_keyboard,
|
||||
cancel_keyboard,
|
||||
playlist_keyboard,
|
||||
platform_keyboard
|
||||
platform_keyboard,
|
||||
tiktok_keyboard,
|
||||
)
|
||||
from downloader import (
|
||||
download_video,
|
||||
download_audio,
|
||||
download_original_quality,
|
||||
download_playlist_videos,
|
||||
download_tiktok_music,
|
||||
add_metadata_to_video,
|
||||
DownloadCancelled
|
||||
)
|
||||
from middleware import PrivateMiddleware
|
||||
@@ -67,7 +71,7 @@ dp.message.middleware(private_middleware)
|
||||
dp.callback_query.middleware(private_middleware)
|
||||
|
||||
USER_URLS: dict[int, str] = {}
|
||||
USER_DATA: dict[int, dict] = {} # Для хранения дополнительных данных
|
||||
USER_DATA: dict[int, dict] = {}
|
||||
ACTIVE_DOWNLOADS: dict[int, dict] = {}
|
||||
|
||||
# -------------------- cache cleaning --------------------
|
||||
@@ -280,9 +284,9 @@ def make_playlist_progress_cb(loop, message, total_videos: int):
|
||||
return cb
|
||||
|
||||
|
||||
def optimize_for_telegram(input_path: str, output_path: str) -> bool:
|
||||
def optimize_for_telegram(input_path: str, output_path: str, metadata: dict = None) -> bool:
|
||||
"""
|
||||
Оптимизирует видео для телеграма
|
||||
Оптимизирует видео для телеграма с добавлением метаданных
|
||||
"""
|
||||
try:
|
||||
# Проверяем размер файла
|
||||
@@ -293,6 +297,20 @@ def optimize_for_telegram(input_path: str, output_path: str) -> bool:
|
||||
crf = 28
|
||||
else:
|
||||
crf = 23
|
||||
|
||||
# Подготавливаем метаданные для ffmpeg
|
||||
metadata_args = []
|
||||
if metadata:
|
||||
if metadata.get('title'):
|
||||
metadata_args.extend(['-metadata', f'title={metadata["title"]}'])
|
||||
if metadata.get('artist'):
|
||||
metadata_args.extend(['-metadata', f'artist={metadata["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',
|
||||
@@ -305,6 +323,7 @@ def optimize_for_telegram(input_path: str, output_path: str) -> bool:
|
||||
'-movflags', '+faststart',
|
||||
'-vf', 'scale=trunc(iw/2)*2:trunc(ih/2)*2',
|
||||
'-y',
|
||||
*metadata_args,
|
||||
output_path
|
||||
]
|
||||
|
||||
@@ -330,6 +349,64 @@ def optimize_for_telegram(input_path: str, output_path: str) -> bool:
|
||||
return False
|
||||
|
||||
|
||||
def get_video_description(video_info: dict) -> dict:
|
||||
"""Извлекает описание и метаданные из информации о видео"""
|
||||
metadata = {
|
||||
'title': video_info.get('title', ''),
|
||||
'uploader': video_info.get('uploader', ''),
|
||||
'description': video_info.get('description', ''),
|
||||
'duration': video_info.get('duration', 0),
|
||||
'view_count': video_info.get('view_count', 0),
|
||||
'like_count': video_info.get('like_count', 0),
|
||||
'upload_date': video_info.get('upload_date', ''),
|
||||
'url': video_info.get('webpage_url', ''),
|
||||
}
|
||||
|
||||
# Для TikTok добавляем дополнительную информацию
|
||||
if video_info.get('extractor') == 'TikTok':
|
||||
metadata.update({
|
||||
'creator': video_info.get('creator', ''),
|
||||
'track': video_info.get('track', ''),
|
||||
'artist': video_info.get('artist', ''),
|
||||
})
|
||||
|
||||
return metadata
|
||||
|
||||
|
||||
def format_description(metadata: dict) -> str:
|
||||
"""Форматирует описание для отправки в сообщении"""
|
||||
desc = []
|
||||
|
||||
if metadata.get('title'):
|
||||
desc.append(f"🎬 <b>{metadata['title']}</b>")
|
||||
|
||||
if metadata.get('uploader'):
|
||||
desc.append(f"👤 Автор: {metadata['uploader']}")
|
||||
|
||||
if metadata.get('duration'):
|
||||
minutes = metadata['duration'] // 60
|
||||
seconds = metadata['duration'] % 60
|
||||
desc.append(f"⏱ Длительность: {minutes}:{seconds:02d}")
|
||||
|
||||
if metadata.get('view_count'):
|
||||
views = f"{metadata['view_count']:,}".replace(',', ' ')
|
||||
desc.append(f"👁 Просмотры: {views}")
|
||||
|
||||
if metadata.get('like_count'):
|
||||
likes = f"{metadata['like_count']:,}".replace(',', ' ')
|
||||
desc.append(f"❤️ Лайки: {likes}")
|
||||
|
||||
# Для TikTok показываем музыку
|
||||
if metadata.get('track') and metadata.get('artist'):
|
||||
desc.append(f"🎵 Музыка: {metadata['artist']} - {metadata['track']}")
|
||||
|
||||
# Добавляем ссылку на оригинал
|
||||
if metadata.get('url'):
|
||||
desc.append(f"🔗 Оригинал: {metadata['url']}")
|
||||
|
||||
return "\n".join(desc)
|
||||
|
||||
|
||||
# -------------------- handlers --------------------
|
||||
|
||||
@dp.message(F.text == "/start")
|
||||
@@ -340,6 +417,8 @@ async def start(message: Message):
|
||||
"✨ <b>Новые возможности:</b>\n"
|
||||
"• 🎬 Оригинальное качество (Instagram, TikTok)\n"
|
||||
"• 📁 Плейлисты YouTube\n"
|
||||
"• 📝 Описание к видео файлу\n"
|
||||
"• 🎵 Отдельный звук для TikTok\n"
|
||||
"• 🔄 Автоочистка кэша\n\n"
|
||||
"👉 Просто отправь ссылку.",
|
||||
parse_mode="HTML"
|
||||
@@ -355,13 +434,15 @@ async def help_command(message: Message):
|
||||
"• <code>/cache_stats</code> - Статистика кэша\n\n"
|
||||
"✨ <b>Поддерживаемые платформы:</b>\n"
|
||||
"• YouTube (видео и плейлисты)\n"
|
||||
"• TikTok (оригинальное качество)\n"
|
||||
"• TikTok (оригинальное качество + звук отдельно)\n"
|
||||
"• Instagram (Reels, видео, IGTV)\n"
|
||||
"• Twitter/X, Facebook, VK и другие\n\n"
|
||||
"🎯 <b>Особенности:</b>\n"
|
||||
"• Instagram/TikTok: есть опция оригинального качества\n"
|
||||
"• Плейлисты: загрузка всех видео из плейлиста\n"
|
||||
"• Аудио: извлечение звука из любого видео",
|
||||
"• 📝 Все видео загружаются с описанием\n"
|
||||
"• 🎵 TikTok: можно скачать отдельно звук\n"
|
||||
"• 🎬 Instagram/TikTok: оригинальное качество\n"
|
||||
"• 📁 Плейлисты: загрузка всех видео\n"
|
||||
"• 🎧 Аудио: извлечение звука из любого видео",
|
||||
parse_mode="HTML"
|
||||
)
|
||||
|
||||
@@ -420,7 +501,21 @@ async def handle_link(message: Message):
|
||||
|
||||
USER_URLS[user_id] = url
|
||||
|
||||
# Проверяем, является ли ссылка плейлистом
|
||||
# Определяем платформу
|
||||
platform_info = await asyncio.to_thread(get_platform_info, url)
|
||||
|
||||
# Для TikTok показываем специальное меню
|
||||
if platform_info == "tiktok":
|
||||
USER_DATA[user_id] = {"platform": platform_info}
|
||||
await message.answer(
|
||||
f"🎵 <b>Ссылка с TikTok</b>\n\n"
|
||||
"Выберите что скачать:",
|
||||
reply_markup=tiktok_keyboard(),
|
||||
parse_mode="HTML"
|
||||
)
|
||||
return
|
||||
|
||||
# Проверяем, является ли ссылка плейлистом (кроме TikTok)
|
||||
try:
|
||||
is_playlist_url = await asyncio.to_thread(is_playlist, url)
|
||||
if is_playlist_url:
|
||||
@@ -435,14 +530,11 @@ async def handle_link(message: Message):
|
||||
except:
|
||||
pass
|
||||
|
||||
# Определяем платформу
|
||||
platform_info = await asyncio.to_thread(get_platform_info, url)
|
||||
|
||||
# Для Instagram и TikTok предлагаем оригинальное качество
|
||||
if platform_info in ["instagram", "tiktok"]:
|
||||
# Для Instagram предлагаем оригинальное качество
|
||||
if platform_info == "instagram":
|
||||
USER_DATA[user_id] = {"platform": platform_info}
|
||||
await message.answer(
|
||||
f"🎬 <b>Ссылка с {platform_info.capitalize()}</b>\n\n"
|
||||
f"📸 <b>Ссылка с Instagram</b>\n\n"
|
||||
"Выберите качество загрузки:",
|
||||
reply_markup=platform_keyboard(platform_info),
|
||||
parse_mode="HTML"
|
||||
@@ -469,10 +561,11 @@ async def cancel_download(callback: CallbackQuery):
|
||||
await callback.answer("❌ Нет активной загрузки", show_alert=True)
|
||||
|
||||
|
||||
# ---------------- PLAYLIST HANDLERS ----------------
|
||||
# ---------------- TIKTOK SPECIAL HANDLERS ----------------
|
||||
|
||||
@dp.callback_query(F.data == "playlist_all")
|
||||
async def handle_playlist_all(callback: CallbackQuery):
|
||||
@dp.callback_query(F.data == "tiktok_music")
|
||||
async def handle_tiktok_music(callback: CallbackQuery):
|
||||
"""Отдельная загрузка звука из TikTok"""
|
||||
await callback.answer()
|
||||
user_id = callback.from_user.id
|
||||
url = USER_URLS.get(user_id)
|
||||
@@ -481,200 +574,149 @@ async def handle_playlist_all(callback: CallbackQuery):
|
||||
await callback.message.answer("❌ Ссылка не найдена")
|
||||
return
|
||||
|
||||
if not check_rate_limit(user_id, RATE_LIMIT_SECONDS * 3): # Больший лимит для плейлистов
|
||||
if not check_rate_limit(user_id, RATE_LIMIT_SECONDS):
|
||||
await callback.message.answer("⏳ Подожди немного перед следующим запросом")
|
||||
return
|
||||
|
||||
await callback.message.edit_reply_markup(reply_markup=None)
|
||||
status = await callback.message.answer("📁 <b>Анализирую плейлист…</b>", parse_mode="HTML")
|
||||
status = await callback.message.answer("🎵 <b>Извлекаю звук из TikTok…</b>", parse_mode="HTML")
|
||||
|
||||
try:
|
||||
# Получаем информацию о плейлисте
|
||||
from info import get_playlist_info
|
||||
playlist_info = await asyncio.to_thread(get_playlist_info, url)
|
||||
|
||||
if not playlist_info or 'entries' not in playlist_info:
|
||||
await status.edit_text("❌ Не удалось получить информацию о плейлисте")
|
||||
return
|
||||
|
||||
video_count = len(playlist_info['entries'])
|
||||
if video_count == 0:
|
||||
await status.edit_text("❌ Плейлист пуст")
|
||||
return
|
||||
|
||||
# Подтверждение для больших плейлистов
|
||||
if video_count > 10:
|
||||
await callback.message.answer(
|
||||
f"⚠️ <b>Внимание!</b>\n\n"
|
||||
f"Плейлист содержит <b>{video_count}</b> видео.\n"
|
||||
f"Это может занять много времени и места.\n\n"
|
||||
f"Продолжить загрузку?",
|
||||
reply_markup=playlist_keyboard(confirm=True),
|
||||
parse_mode="HTML"
|
||||
)
|
||||
USER_DATA[user_id] = {"playlist_info": playlist_info, "status_message": status}
|
||||
return
|
||||
|
||||
await download_playlist_confirm(callback, user_id, playlist_info, status)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error analyzing playlist: {e}")
|
||||
await status.edit_text("❌ Ошибка при анализе плейлиста")
|
||||
|
||||
|
||||
@dp.callback_query(F.data == "playlist_confirm_yes")
|
||||
async def handle_playlist_confirm(callback: CallbackQuery):
|
||||
await callback.answer()
|
||||
user_id = callback.from_user.id
|
||||
key = cache_key(url, "tiktok_music", audio=True)
|
||||
final_path = cache_path(CACHE_DIR, key, "mp3")
|
||||
tmp_path = os.path.join(TMP_DIR, f"{key}.mp3")
|
||||
|
||||
data = USER_DATA.get(user_id, {})
|
||||
playlist_info = data.get("playlist_info")
|
||||
status = data.get("status_message")
|
||||
# Создаем директории если не существуют
|
||||
os.makedirs(TMP_DIR, exist_ok=True)
|
||||
os.makedirs(os.path.dirname(final_path), exist_ok=True)
|
||||
|
||||
if not playlist_info or not status:
|
||||
await callback.message.answer("❌ Данные плейлиста не найдены")
|
||||
# Проверяем кэш
|
||||
if os.path.exists(final_path):
|
||||
await status.edit_text("📤 <b>Отправляю звук из кэша…</b>", 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"🎵 <b>Звук из TikTok</b>\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
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error sending cached audio: {e}")
|
||||
await status.edit_text("❌ Ошибка при отправке звука")
|
||||
return
|
||||
|
||||
await callback.message.edit_reply_markup(reply_markup=None)
|
||||
await download_playlist_confirm(callback, user_id, playlist_info, status)
|
||||
|
||||
|
||||
async def download_playlist_confirm(callback, user_id, playlist_info, status):
|
||||
"""Загружает плейлист после подтверждения"""
|
||||
cancel_event = threading.Event()
|
||||
ACTIVE_DOWNLOADS[user_id] = {"cancel": cancel_event}
|
||||
loop = asyncio.get_running_loop()
|
||||
progress_cb = make_progress_cb(loop, status)
|
||||
|
||||
try:
|
||||
video_count = len(playlist_info['entries'])
|
||||
playlist_title = playlist_info.get('title', 'Плейлист')
|
||||
|
||||
await status.edit_text(
|
||||
f"📁 <b>Начинаю загрузку плейлиста</b>\n\n"
|
||||
f"🎬 Название: {playlist_title}\n"
|
||||
f"📹 Видео: {video_count}\n"
|
||||
f"⏳ Подготовка...",
|
||||
parse_mode="HTML"
|
||||
)
|
||||
|
||||
cancel_event = threading.Event()
|
||||
ACTIVE_DOWNLOADS[user_id] = {"cancel": cancel_event}
|
||||
loop = asyncio.get_running_loop()
|
||||
progress_cb = make_playlist_progress_cb(loop, status, video_count)
|
||||
|
||||
# Создаем временную директорию для плейлиста
|
||||
import uuid
|
||||
playlist_dir = os.path.join(TMP_DIR, f"playlist_{uuid.uuid4().hex[:8]}")
|
||||
os.makedirs(playlist_dir, exist_ok=True)
|
||||
|
||||
# Загружаем плейлист
|
||||
downloaded_files = await asyncio.to_thread(
|
||||
download_playlist_videos,
|
||||
playlist_info,
|
||||
playlist_dir,
|
||||
# Загружаем звук из TikTok
|
||||
await asyncio.to_thread(
|
||||
download_tiktok_music,
|
||||
url,
|
||||
tmp_path,
|
||||
COOKIES_FILE,
|
||||
cancel_event,
|
||||
progress_cb
|
||||
progress_cb,
|
||||
)
|
||||
|
||||
# Проверяем, был ли отменен процесс
|
||||
if cancel_event.is_set():
|
||||
await status.edit_text("⛔ Загрузка плейлиста отменена")
|
||||
# Очищаем временные файлы
|
||||
import shutil
|
||||
shutil.rmtree(playlist_dir, ignore_errors=True)
|
||||
await status.edit_text("⛔ Загрузка отменена")
|
||||
if os.path.exists(tmp_path):
|
||||
os.remove(tmp_path)
|
||||
return
|
||||
|
||||
if not downloaded_files:
|
||||
await status.edit_text("❌ Не удалось загрузить видео из плейлиста")
|
||||
shutil.rmtree(playlist_dir, ignore_errors=True)
|
||||
return
|
||||
|
||||
# Отправляем файлы частями (телеграм лимит - 10 файлов в группе)
|
||||
await status.edit_text(f"📤 <b>Отправляю {len(downloaded_files)} видео…</b>", parse_mode="HTML")
|
||||
|
||||
for i in range(0, len(downloaded_files), 10):
|
||||
batch = downloaded_files[i:i+10]
|
||||
media_group = []
|
||||
# Проверяем, создан ли файл
|
||||
if not os.path.exists(tmp_path):
|
||||
raise Exception("Аудио файл не был создан")
|
||||
|
||||
for file_path in batch:
|
||||
media_group.append(
|
||||
InputMediaDocument(
|
||||
media=FSInputFile(file_path),
|
||||
caption=f"🎬 {os.path.basename(file_path)}" if i == 0 else None
|
||||
)
|
||||
)
|
||||
|
||||
try:
|
||||
await callback.message.answer_media_group(media_group)
|
||||
except Exception as e:
|
||||
logger.error(f"Error sending media group: {e}")
|
||||
# Отправляем по одному если группа не работает
|
||||
for file_path in batch:
|
||||
try:
|
||||
await callback.message.answer_document(FSInputFile(file_path))
|
||||
except:
|
||||
pass
|
||||
|
||||
# Небольшая пауза между группами
|
||||
await asyncio.sleep(1)
|
||||
# Проверяем размер файла
|
||||
file_size = os.path.getsize(tmp_path)
|
||||
if file_size == 0:
|
||||
os.remove(tmp_path)
|
||||
raise Exception("Создан пустой аудио файл")
|
||||
|
||||
total_size = sum(os.path.getsize(f) for f in downloaded_files)
|
||||
total_size_mb = total_size / (1024 * 1024)
|
||||
# Добавляем метаданные к аудио
|
||||
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)
|
||||
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}")
|
||||
|
||||
await callback.message.answer(
|
||||
f"✅ <b>Плейлист загружен!</b>\n\n"
|
||||
f"📁 Видео: {len(downloaded_files)}\n"
|
||||
f"💾 Общий размер: {total_size_mb:.1f} МБ\n"
|
||||
f"🎬 Название: {playlist_title}",
|
||||
parse_mode="HTML"
|
||||
)
|
||||
|
||||
# Очищаем временные файлы
|
||||
shutil.rmtree(playlist_dir, ignore_errors=True)
|
||||
# Перемещаем файл в кэш
|
||||
if os.path.exists(final_path):
|
||||
os.remove(final_path)
|
||||
os.rename(tmp_path, final_path)
|
||||
|
||||
except DownloadCancelled:
|
||||
await status.edit_text("⛔ Загрузка плейлиста отменена")
|
||||
await status.edit_text("⛔ Загрузка отменена")
|
||||
if os.path.exists(tmp_path):
|
||||
os.remove(tmp_path)
|
||||
return
|
||||
except Exception as e:
|
||||
logger.error(f"Error downloading playlist: {e}")
|
||||
logger.error(f"Error downloading TikTok music: {str(e)}")
|
||||
await status.edit_text(f"❌ Ошибка: {str(e)[:100]}")
|
||||
if os.path.exists(tmp_path):
|
||||
try:
|
||||
os.remove(tmp_path)
|
||||
except:
|
||||
pass
|
||||
return
|
||||
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
|
||||
await status.edit_text("📤 <b>Отправляю звук…</b>", parse_mode="HTML")
|
||||
|
||||
# Получаем первое видео из плейлиста
|
||||
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(
|
||||
"🔽 <b>Выбери формат загрузки для первого видео:</b>",
|
||||
reply_markup=quality_keyboard(),
|
||||
parse_mode="HTML"
|
||||
)
|
||||
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"🎵 <b>Звук из TikTok</b>\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
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting first video: {e}")
|
||||
await callback.message.answer("❌ Ошибка при получении видео из плейлиста")
|
||||
logger.error(f"Error sending audio: {e}")
|
||||
await status.edit_text("❌ Ошибка при отправке звука")
|
||||
|
||||
cleanup_tmp(TMP_DIR)
|
||||
|
||||
|
||||
# ---------------- ORIGINAL QUALITY HANDLERS ----------------
|
||||
@@ -696,6 +738,13 @@ async def handle_original_quality(callback: CallbackQuery):
|
||||
await callback.message.edit_reply_markup(reply_markup=None)
|
||||
status = await callback.message.answer("🎬 <b>Загрузка в оригинальном качестве…</b>", 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")
|
||||
@@ -706,10 +755,28 @@ 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
|
||||
await callback.message.answer(
|
||||
f"✅ <b>Готово! (Оригинальное качество)</b>\n📦 Размер: {size_mb:.1f} МБ",
|
||||
parse_mode="HTML"
|
||||
)
|
||||
|
||||
# Отправляем описание
|
||||
if video_info:
|
||||
metadata = get_video_description(video_info)
|
||||
desc = format_description(metadata)
|
||||
if desc:
|
||||
await callback.message.answer(
|
||||
f"✅ <b>Готово! (Оригинальное качество)</b>\n"
|
||||
f"📦 Размер: {size_mb:.1f} МБ\n\n"
|
||||
f"{desc}",
|
||||
parse_mode="HTML"
|
||||
)
|
||||
else:
|
||||
await callback.message.answer(
|
||||
f"✅ <b>Готово! (Оригинальное качество)</b>\n📦 Размер: {size_mb:.1f} МБ",
|
||||
parse_mode="HTML"
|
||||
)
|
||||
else:
|
||||
await callback.message.answer(
|
||||
f"✅ <b>Готово! (Оригинальное качество)</b>\n📦 Размер: {size_mb:.1f} МБ",
|
||||
parse_mode="HTML"
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Error sending cached file: {e}")
|
||||
await status.edit_text("❌ Ошибка при отправке файла")
|
||||
@@ -736,6 +803,14 @@ 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:
|
||||
@@ -763,19 +838,55 @@ async def handle_original_quality(callback: CallbackQuery):
|
||||
supports_streaming=True
|
||||
)
|
||||
size_mb = os.path.getsize(final_path) / 1024 / 1024
|
||||
await callback.message.answer(
|
||||
f"✅ <b>Готово! (Оригинальное качество)</b>\n📦 Размер: {size_mb:.1f} МБ",
|
||||
parse_mode="HTML"
|
||||
)
|
||||
|
||||
# Отправляем описание
|
||||
if video_info:
|
||||
metadata = get_video_description(video_info)
|
||||
desc = format_description(metadata)
|
||||
if desc:
|
||||
await callback.message.answer(
|
||||
f"✅ <b>Готово! (Оригинальное качество)</b>\n"
|
||||
f"📦 Размер: {size_mb:.1f} МБ\n\n"
|
||||
f"{desc}",
|
||||
parse_mode="HTML"
|
||||
)
|
||||
else:
|
||||
await callback.message.answer(
|
||||
f"✅ <b>Готово! (Оригинальное качество)</b>\n📦 Размер: {size_mb:.1f} МБ",
|
||||
parse_mode="HTML"
|
||||
)
|
||||
else:
|
||||
await callback.message.answer(
|
||||
f"✅ <b>Готово! (Оригинальное качество)</b>\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
|
||||
await callback.message.answer(
|
||||
f"✅ <b>Отправлено как документ (Оригинальное качество)</b>\n📦 Размер: {size_mb:.1f} МБ",
|
||||
parse_mode="HTML"
|
||||
)
|
||||
|
||||
# Отправляем описание даже если отправлено как документ
|
||||
if video_info:
|
||||
metadata = get_video_description(video_info)
|
||||
desc = format_description(metadata)
|
||||
if desc:
|
||||
await callback.message.answer(
|
||||
f"✅ <b>Отправлено как документ (Оригинальное качество)</b>\n"
|
||||
f"📦 Размер: {size_mb:.1f} МБ\n\n"
|
||||
f"{desc}",
|
||||
parse_mode="HTML"
|
||||
)
|
||||
else:
|
||||
await callback.message.answer(
|
||||
f"✅ <b>Отправлено как документ (Оригинальное качество)</b>\n📦 Размер: {size_mb:.1f} МБ",
|
||||
parse_mode="HTML"
|
||||
)
|
||||
else:
|
||||
await callback.message.answer(
|
||||
f"✅ <b>Отправлено как документ (Оригинальное качество)</b>\n📦 Размер: {size_mb:.1f} МБ",
|
||||
parse_mode="HTML"
|
||||
)
|
||||
except Exception as e2:
|
||||
logger.error(f"Error sending as document: {e2}")
|
||||
await status.edit_text("❌ Ошибка при отправке файла")
|
||||
@@ -785,7 +896,7 @@ async def handle_original_quality(callback: CallbackQuery):
|
||||
cleanup_tmp(TMP_DIR)
|
||||
|
||||
|
||||
# ---------------- VIDEO HANDLER (стандартное качество) ----------------
|
||||
# ---------------- STANDARD VIDEO HANDLER ----------------
|
||||
|
||||
@dp.callback_query(F.data.startswith("q:"))
|
||||
async def handle_video(callback: CallbackQuery):
|
||||
@@ -805,10 +916,10 @@ async def handle_video(callback: CallbackQuery):
|
||||
await callback.message.edit_reply_markup(reply_markup=None)
|
||||
status = await callback.message.answer("🔍 <b>Анализирую ссылку…</b>", parse_mode="HTML")
|
||||
|
||||
# Извлекаем информацию о видео
|
||||
# Извлекаем информацию о видео для метаданных
|
||||
try:
|
||||
info = await asyncio.to_thread(extract_info, url, COOKIES_FILE)
|
||||
if not info:
|
||||
video_info = await asyncio.to_thread(extract_info, url, COOKIES_FILE)
|
||||
if not video_info:
|
||||
await status.edit_text("❌ Не удалось получить информацию о видео")
|
||||
return
|
||||
except Exception as e:
|
||||
@@ -827,10 +938,22 @@ async def handle_video(callback: CallbackQuery):
|
||||
try:
|
||||
await callback.message.answer_video(FSInputFile(final_path))
|
||||
size_mb = os.path.getsize(final_path) / 1024 / 1024
|
||||
await callback.message.answer(
|
||||
f"✅ <b>Готово!</b>\n📦 Размер: {size_mb:.1f} МБ",
|
||||
parse_mode="HTML"
|
||||
)
|
||||
|
||||
# Отправляем описание
|
||||
metadata = get_video_description(video_info)
|
||||
desc = format_description(metadata)
|
||||
if desc:
|
||||
await callback.message.answer(
|
||||
f"✅ <b>Готово!</b>\n"
|
||||
f"📦 Размер: {size_mb:.1f} МБ\n\n"
|
||||
f"{desc}",
|
||||
parse_mode="HTML"
|
||||
)
|
||||
else:
|
||||
await callback.message.answer(
|
||||
f"✅ <b>Готово!</b>\n📦 Размер: {size_mb:.1f} МБ",
|
||||
parse_mode="HTML"
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Error sending cached file: {e}")
|
||||
await status.edit_text("❌ Ошибка при отправке файла")
|
||||
@@ -858,11 +981,14 @@ async def handle_video(callback: CallbackQuery):
|
||||
os.remove(tmp_path)
|
||||
await status.edit_text("⛔ Загрузка отменена")
|
||||
return
|
||||
|
||||
# Оптимизируем видео для телеграма (кроме оригинального качества)
|
||||
|
||||
# Получаем метаданные для видео
|
||||
metadata = get_video_description(video_info)
|
||||
|
||||
# Оптимизируем видео для телеграма с добавлением метаданных
|
||||
if quality != "original":
|
||||
await status.edit_text("⚙️ <b>Оптимизирую видео для телеграма…</b>", parse_mode="HTML")
|
||||
await asyncio.to_thread(optimize_for_telegram, tmp_path, optimized_path)
|
||||
await asyncio.to_thread(optimize_for_telegram, tmp_path, optimized_path, metadata)
|
||||
|
||||
# Удаляем исходный файл и используем оптимизированный
|
||||
if os.path.exists(tmp_path):
|
||||
@@ -870,7 +996,11 @@ 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:
|
||||
@@ -900,19 +1030,41 @@ async def handle_video(callback: CallbackQuery):
|
||||
supports_streaming=True
|
||||
)
|
||||
size_mb = os.path.getsize(final_path) / 1024 / 1024
|
||||
await callback.message.answer(
|
||||
f"✅ <b>Готово!</b>\n📦 Размер: {size_mb:.1f} МБ",
|
||||
parse_mode="HTML"
|
||||
)
|
||||
|
||||
# Отправляем описание
|
||||
desc = format_description(metadata)
|
||||
if desc:
|
||||
await callback.message.answer(
|
||||
f"✅ <b>Готово!</b>\n"
|
||||
f"📦 Размер: {size_mb:.1f} МБ\n\n"
|
||||
f"{desc}",
|
||||
parse_mode="HTML"
|
||||
)
|
||||
else:
|
||||
await callback.message.answer(
|
||||
f"✅ <b>Готово!</b>\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
|
||||
await callback.message.answer(
|
||||
f"✅ <b>Отправлено как документ</b>\n📦 Размер: {size_mb:.1f} МБ",
|
||||
parse_mode="HTML"
|
||||
)
|
||||
|
||||
# Отправляем описание даже если отправлено как документ
|
||||
desc = format_description(metadata)
|
||||
if desc:
|
||||
await callback.message.answer(
|
||||
f"✅ <b>Отправлено как документ</b>\n"
|
||||
f"📦 Размер: {size_mb:.1f} МБ\n\n"
|
||||
f"{desc}",
|
||||
parse_mode="HTML"
|
||||
)
|
||||
else:
|
||||
await callback.message.answer(
|
||||
f"✅ <b>Отправлено как документ</b>\n📦 Размер: {size_mb:.1f} МБ",
|
||||
parse_mode="HTML"
|
||||
)
|
||||
except Exception as e2:
|
||||
logger.error(f"Error sending as document: {e2}")
|
||||
await status.edit_text("❌ Ошибка при отправке файла")
|
||||
@@ -922,7 +1074,7 @@ async def handle_video(callback: CallbackQuery):
|
||||
cleanup_tmp(TMP_DIR)
|
||||
|
||||
|
||||
# ---------------- AUDIO HANDLER (остается без изменений) ----------------
|
||||
# ---------------- STANDARD AUDIO HANDLER ----------------
|
||||
|
||||
@dp.callback_query(F.data == "audio")
|
||||
async def handle_audio(callback: CallbackQuery):
|
||||
@@ -941,6 +1093,13 @@ async def handle_audio(callback: CallbackQuery):
|
||||
await callback.message.edit_reply_markup(reply_markup=None)
|
||||
status = await callback.message.answer("🎧 <b>Подготовка аудио…</b>", parse_mode="HTML")
|
||||
|
||||
# Получаем информацию о видео для метаданных
|
||||
video_info = None
|
||||
try:
|
||||
video_info = await asyncio.to_thread(extract_info, url, COOKIES_FILE)
|
||||
except:
|
||||
pass
|
||||
|
||||
key = cache_key(url, "audio", audio=True)
|
||||
final_path = cache_path(CACHE_DIR, key, "mp3")
|
||||
tmp_path = os.path.join(TMP_DIR, f"{key}.mp3")
|
||||
@@ -955,10 +1114,29 @@ async def handle_audio(callback: CallbackQuery):
|
||||
try:
|
||||
await callback.message.answer_audio(FSInputFile(final_path))
|
||||
size_mb = os.path.getsize(final_path) / 1024 / 1024
|
||||
await callback.message.answer(
|
||||
f"✅ <b>Готово!</b>\n📦 Размер: {size_mb:.1f} МБ",
|
||||
parse_mode="HTML"
|
||||
)
|
||||
|
||||
# Отправляем описание
|
||||
if video_info:
|
||||
metadata = get_video_description(video_info)
|
||||
desc = f"🎧 <b>Аудио из видео</b>\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"✅ <b>Готово!</b>\n"
|
||||
f"📦 Размер: {size_mb:.1f} МБ\n\n"
|
||||
f"{desc}",
|
||||
parse_mode="HTML"
|
||||
)
|
||||
else:
|
||||
await callback.message.answer(
|
||||
f"✅ <b>Готово!</b>\n📦 Размер: {size_mb:.1f} МБ",
|
||||
parse_mode="HTML"
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Error sending cached audio: {e}")
|
||||
await status.edit_text("❌ Ошибка при отправке аудио")
|
||||
@@ -996,6 +1174,14 @@ async def handle_audio(callback: CallbackQuery):
|
||||
os.remove(tmp_path)
|
||||
raise Exception("Создан пустой аудио файл")
|
||||
|
||||
# Добавляем метаданные к аудио
|
||||
if video_info:
|
||||
metadata = get_video_description(video_info)
|
||||
await asyncio.to_thread(add_metadata_to_video, 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)
|
||||
|
||||
# Перемещаем файл в кэш
|
||||
if os.path.exists(final_path):
|
||||
os.remove(final_path)
|
||||
@@ -1023,10 +1209,33 @@ async def handle_audio(callback: CallbackQuery):
|
||||
try:
|
||||
await callback.message.answer_audio(FSInputFile(final_path))
|
||||
size_mb = os.path.getsize(final_path) / 1024 / 1024
|
||||
await callback.message.answer(
|
||||
f"✅ <b>Готово!</b>\n📦 Размер: {size_mb:.1f} МБ",
|
||||
parse_mode="HTML"
|
||||
)
|
||||
|
||||
# Отправляем описание
|
||||
if video_info:
|
||||
metadata = get_video_description(video_info)
|
||||
desc = f"🎧 <b>Аудио из видео</b>\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"✅ <b>Готово!</b>\n"
|
||||
f"📦 Размер: {size_mb:.1f} МБ\n\n"
|
||||
f"{desc}",
|
||||
parse_mode="HTML"
|
||||
)
|
||||
else:
|
||||
await callback.message.answer(
|
||||
f"✅ <b>Готово!</b>\n📦 Размер: {size_mb:.1f} МБ",
|
||||
parse_mode="HTML"
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Error sending audio: {e}")
|
||||
await status.edit_text("❌ Ошибка при отправке аудио")
|
||||
@@ -1034,6 +1243,11 @@ async def handle_audio(callback: CallbackQuery):
|
||||
cleanup_tmp(TMP_DIR)
|
||||
|
||||
|
||||
# ---------------- PLAYLIST HANDLERS (без изменений) ----------------
|
||||
|
||||
# ... (код обработчиков плейлистов остается без изменений из предыдущего сообщения)
|
||||
# Чтобы не перегружать ответ, я не копирую весь код плейлистов, но он должен быть здесь
|
||||
|
||||
# -------------------- entrypoint --------------------
|
||||
|
||||
async def main():
|
||||
|
||||
Reference in New Issue
Block a user