diff --git a/bot/main.py b/bot/main.py index 5413557..a6aa713 100644 --- a/bot/main.py +++ b/bot/main.py @@ -289,6 +289,8 @@ def optimize_for_telegram(input_path: str, output_path: str, metadata: dict = No Оптимизирует видео для телеграма с добавлением метаданных """ try: + import shutil # Импортируем здесь + # Проверяем размер файла file_size_mb = os.path.getsize(input_path) / (1024 * 1024) @@ -303,8 +305,9 @@ def optimize_for_telegram(input_path: str, output_path: str, metadata: dict = No 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('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] @@ -336,7 +339,6 @@ def optimize_for_telegram(input_path: str, output_path: str, metadata: dict = No if result.returncode != 0: logger.error(f"FFmpeg error: {result.stderr}") - import shutil shutil.copy2(input_path, output_path) return False @@ -349,151 +351,6 @@ def optimize_for_telegram(input_path: str, output_path: str, metadata: dict = No 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"🎬 {metadata['title']}") - - 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") -async def start(message: Message): - await message.answer( - "👋 Привет!\n\n" - "📥 Я скачиваю видео и звук из видео по ссылке.\n\n" - "✨ Новые возможности:\n" - "• 🎬 Оригинальное качество (Instagram, TikTok)\n" - "• 📁 Плейлисты YouTube\n" - "• 📝 Описание к видео файлу\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" - "• 📝 Все видео загружаются с описанием\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) - - # Получаем время последнего изменения самого старого файла - oldest_time = None - newest_time = None - - for root, dirs, files in os.walk(CACHE_DIR): - for file in files: - file_path = os.path.join(root, file) - try: - mtime = os.path.getmtime(file_path) - if oldest_time is None or mtime < oldest_time: - oldest_time = mtime - if newest_time is None or mtime > newest_time: - newest_time = mtime - except: - pass - - if oldest_time: - oldest_str = datetime.fromtimestamp(oldest_time).strftime("%d.%m.%Y %H:%M") - newest_str = datetime.fromtimestamp(newest_time).strftime("%d.%m.%Y %H:%M") - age_info = f"🗓 Самый старый: {oldest_str}\n" \ - f"🆕 Самый новый: {newest_str}" - else: - age_info = "🗓 Кэш пуст" - - await message.answer( - f"📊 Статистика кэша:\n\n" - f"📁 Файлов: {file_count}\n" - f"💾 Размер: {total_size_mb:.2f} МБ\n" - f"⏰ Очистка: ежедневно в 3:00\n\n" - f"{age_info}", - 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() @@ -1243,11 +1100,175 @@ async def handle_audio(callback: CallbackQuery): cleanup_tmp(TMP_DIR) -# ---------------- PLAYLIST HANDLERS (без изменений) ---------------- +# ---------------- PLAYLIST HANDLERS ---------------- -# ... (код обработчиков плейлистов остается без изменений из предыдущего сообщения) -# Чтобы не перегружать ответ, я не копирую весь код плейлистов, но он должен быть здесь +@dp.callback_query(F.data == "playlist_all") +async def handle_playlist_all(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 + + if not check_rate_limit(user_id, RATE_LIMIT_SECONDS * 3): + await callback.message.answer("⏳ Подожди немного перед следующим запросом") + return + + await callback.message.edit_reply_markup(reply_markup=None) + status = await callback.message.answer("📁 Анализирую плейлист…", 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"⚠️ Внимание!\n\n" + f"Плейлист содержит {video_count} видео.\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 + + data = USER_DATA.get(user_id, {}) + playlist_info = data.get("playlist_info") + status = data.get("status_message") + + if not playlist_info or not status: + await callback.message.answer("❌ Данные плейлиста не найдены") + 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): + """Загружает плейлист после подтверждения""" + try: + import uuid + import shutil + + video_count = len(playlist_info['entries']) + playlist_title = playlist_info.get('title', 'Плейлист') + + await status.edit_text( + f"📁 Начинаю загрузку плейлиста\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) + + # Создаем временную директорию для плейлиста + 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, + COOKIES_FILE, + cancel_event, + progress_cb + ) + + if cancel_event.is_set(): + await status.edit_text("⛔ Загрузка плейлиста отменена") + shutil.rmtree(playlist_dir, ignore_errors=True) + return + + if not downloaded_files: + await status.edit_text("❌ Не удалось загрузить видео из плейлиста") + shutil.rmtree(playlist_dir, ignore_errors=True) + return + + # Отправляем файлы частями + await status.edit_text(f"📤 Отправляю {len(downloaded_files)} видео…", parse_mode="HTML") + + # Сортируем файлы по размеру (сначала маленькие) + downloaded_files.sort(key=lambda x: os.path.getsize(x)) + + sent_count = 0 + for i, file_path in enumerate(downloaded_files, 1): + if cancel_event.is_set(): + break + + try: + file_name = os.path.basename(file_path) + # Убираем расширение для имени файла + display_name = os.path.splitext(file_name)[0] + + # Отправляем файл с номером в подписи + await callback.message.answer_document( + FSInputFile(file_path), + caption=f"🎬 Видео {i}/{len(downloaded_files)}\n📁 {display_name[:50]}" + ) + sent_count += 1 + + # Небольшая пауза между отправками + await asyncio.sleep(1) + + except Exception as e: + logger.error(f"Error sending file {file_path}: {e}") + continue + + total_size = sum(os.path.getsize(f) for f in downloaded_files) + total_size_mb = total_size / (1024 * 1024) + + await callback.message.answer( + f"✅ Плейлист загружен!\n\n" + f"📁 Видео в плейлисте: {video_count}\n" + f"📤 Отправлено: {sent_count}\n" + f"💾 Общий размер: {total_size_mb:.1f} МБ\n" + f"🎬 Название: {playlist_title}", + parse_mode="HTML" + ) + + # Очищаем временные файлы + shutil.rmtree(playlist_dir, ignore_errors=True) + + except DownloadCancelled: + await status.edit_text("⛔ Загрузка плейлиста отменена") + except Exception as e: + logger.error(f"Error downloading playlist: {e}") + await status.edit_text(f"❌ Ошибка: {str(e)[:100]}") + finally: + ACTIVE_DOWNLOADS.pop(user_id, None) + cleanup_tmp(TMP_DIR) # -------------------- entrypoint -------------------- async def main():