diff --git a/__pycache__/yt_engine.cpython-312.pyc b/__pycache__/yt_engine.cpython-312.pyc index 31c9533..3ffee21 100644 Binary files a/__pycache__/yt_engine.cpython-312.pyc and b/__pycache__/yt_engine.cpython-312.pyc differ diff --git a/bot.py b/bot.py index c63d737..e4d24fb 100644 --- a/bot.py +++ b/bot.py @@ -1,7 +1,7 @@ import asyncio import os -import time import logging +import time from aiogram import Bot, Dispatcher, types, BaseMiddleware, F from aiogram.filters import Command from aiogram.types import FSInputFile, InlineKeyboardMarkup, InlineKeyboardButton, CallbackQuery @@ -10,7 +10,6 @@ from aiogram.client.session.aiohttp import AiohttpSession from dotenv import load_dotenv from apscheduler.schedulers.asyncio import AsyncIOScheduler -# Import modul lokal from yt_engine import process_youtube_request, get_recommendations from db_manager import db from s3_manager import upload_audio, download_audio, delete_audio @@ -23,17 +22,17 @@ BOT_TOKEN = os.getenv("BOT_TOKEN") AUTHORIZED_USER_ID = int(os.getenv("AUTHORIZED_USER_ID", 0)) session = AiohttpSession(timeout=300) +# Matikan parse_mode default agar aman dari error karakter aneh di judul lagu YouTube bot = Bot(token=BOT_TOKEN, session=session, default=DefaultBotProperties(parse_mode=None)) dp = Dispatcher() -# --- SISTEM ANTREAN PINTAR (PRIORITY QUEUE) --- -# Format isi antrean: (priority, timestamp, query, chat_id, status_msg_id) -music_queue = asyncio.PriorityQueue() +# Antrean sederhana (FIFO) +music_queue = asyncio.Queue() async def queue_worker(): - """Pekerja di balik layar yang memproses antrean satu per satu""" + """Memproses lagu satu per satu dari antrean agar server tidak meledak""" while True: - priority, _, query, chat_id, msg_id = await music_queue.get() + query, chat_id, msg_id = await music_queue.get() try: await process_music_request(query, chat_id, msg_id) except Exception as e: @@ -41,59 +40,59 @@ async def queue_worker(): finally: music_queue.task_done() -# --- MIDDLEWARE / SECURITY --- +# Satpam Bot class SecurityMiddleware(BaseMiddleware): async def __call__(self, handler, event: types.Message | CallbackQuery, data: dict): - user_id = event.from_user.id - if user_id != AUTHORIZED_USER_ID: - return + if event.from_user.id != AUTHORIZED_USER_ID: return return await handler(event, data) dp.message.middleware(SecurityMiddleware()) dp.callback_query.middleware(SecurityMiddleware()) -# --- COMMAND HANDLERS --- +# --- COMMANDS --- @dp.message(Command("start")) async def cmd_start(message: types.Message): - await message.answer("🎧 **Kantor-Bypass Music Bot**\nKirim format: `/play `") + await message.answer("🎧 **Music Bot Ready!**\nKirim format: `/play `\nSetelah lagu terkirim, klik rekomendasi di bawahnya untuk melanjutkan.", parse_mode="Markdown") @dp.message(Command("play")) async def cmd_play(message: types.Message): query = message.text.replace("/play", "").strip() if not query: - return await message.answer("⚠️ Masukkan judul lagu. Contoh: `/play Dewa 19 Kangen`") + return await message.answer("⚠️ Masukkan judul lagu. Contoh: /play Dewa 19 Kangen") - status_msg = await message.answer("⏳ *Dimasukkan ke antrean reguler...*") - # Priority 1 = Reguler (Add to Queue) - await music_queue.put((1, time.time(), query, message.chat.id, status_msg.message_id)) + status_msg = await message.answer("⏳ Memproses pesananmu...") + await music_queue.put((query, message.chat.id, status_msg.message_id)) -# --- CALLBACK HANDLERS (TOMBOL INTERAKTIF) --- -@dp.callback_query(F.data.startswith("play_") | F.data.startswith("queue_")) -async def handle_action_buttons(callback: CallbackQuery): - action, video_id = callback.data.split("_", 1) +# --- HANDLER TOMBOL SUGGESTION --- +@dp.callback_query(F.data.startswith("play_")) +async def handle_suggestion_click(callback: CallbackQuery): + video_id = callback.data.split("_", 1)[1] - # Hapus tombol di pesan yang diklik biar gak di-spam klik + # 1. Hapus tombol di pesan sebelumnya biar chat nggak penuh tombol mati await callback.message.edit_reply_markup(reply_markup=None) + # 2. Kirim status baru + status_msg = await bot.send_message(callback.message.chat.id, "⏳ Mengunduh lagu pilihanmu...") query_url = f"https://www.youtube.com/watch?v={video_id}" - if action == "play": - status_msg = await bot.send_message(callback.message.chat.id, "🚀 *VIP Prioritas! Memproses sekarang...*") - # Priority 0 = VIP (Bypass Antrean) - await music_queue.put((0, time.time(), query_url, callback.message.chat.id, status_msg.message_id)) - await callback.answer("Memutar langsung!") - elif action == "queue": - status_msg = await bot.send_message(callback.message.chat.id, "📥 *Ditambahkan ke antrean...*") - # Priority 1 = Reguler - await music_queue.put((1, time.time(), query_url, callback.message.chat.id, status_msg.message_id)) - await callback.answer("Masuk antrean!") + # 3. Masukkan ke antrean + await music_queue.put((query_url, callback.message.chat.id, status_msg.message_id)) + await callback.answer("Siapp! Diproses...") # --- CORE LOGIC --- async def process_music_request(query: str, chat_id: int, status_msg_id: int): local_file = None + total_start_time = time.time() # ⏱️ START TIMER TOTAL + try: - await bot.edit_message_text("🔍 Mencari dan memproses lagu...", chat_id, status_msg_id) + logger.info(f"▶️ MEMULAI PROSES LAGU: {query}") + await bot.edit_message_text("🔍 Mencari ke YouTube...", chat_id, status_msg_id) + + # --- FASE 1: YOUTUBE & FFMPEG --- + t_yt_start = time.time() yt_result = await process_youtube_request(query) + t_yt_end = time.time() + logger.info(f"⏱️ [FASE 1] yt-dlp & FFmpeg memakan waktu: {t_yt_end - t_yt_start:.2f} detik") if yt_result["status"] == "error": return await bot.edit_message_text(f"❌ Gagal: {yt_result['message']}", chat_id, status_msg_id) @@ -103,65 +102,81 @@ async def process_music_request(query: str, chat_id: int, status_msg_id: int): local_file = yt_result["file_path"] s3_object_name = f"{video_id}.mp3" + # --- FASE 2: MINIO S3 --- + t_s3_start = time.time() cached_data = await db.get_cache(video_id) if cached_data: - await bot.edit_message_text("⚡ Mengambil dari Cache S3...", chat_id, status_msg_id) + await bot.edit_message_text("⚡ Mengambil dari Cache (MinIO)...", chat_id, status_msg_id) local_file = f"downloads/cache_{video_id}.mp3" await download_audio(s3_object_name, local_file) + logger.info(f"⏱️ [FASE 2] Download dari MinIO memakan waktu: {time.time() - t_s3_start:.2f} detik") else: - await bot.edit_message_text("☁️ Mengunggah ke S3 Storage...", chat_id, status_msg_id) + await bot.edit_message_text("☁️ Mengunggah ke MinIO Server...", chat_id, status_msg_id) if local_file and os.path.exists(local_file): await upload_audio(local_file, s3_object_name) await db.save_cache(video_id, title, s3_object_name) + logger.info(f"⏱️ [FASE 2] Upload ke MinIO memakan waktu: {time.time() - t_s3_start:.2f} detik") - await bot.edit_message_text("📤 Mengirim audio ke Telegram...", chat_id, status_msg_id) + # --- FASE 3: TELEGRAM UPLOAD --- + await bot.edit_message_text("📤 Mengirim file ke Telegram...", chat_id, status_msg_id) if local_file and os.path.exists(local_file): + t_tele_start = time.time() audio = FSInputFile(local_file) await bot.send_audio(chat_id=chat_id, audio=audio, title=title, performer="Music Bot", request_timeout=300) - await bot.delete_message(chat_id, status_msg_id) + t_tele_end = time.time() + logger.info(f"⏱️ [FASE 3] Upload ke Telegram memakan waktu: {t_tele_end - t_tele_start:.2f} detik") - # --- MENCARI REKOMENDASI LAGU (MIX ALGORITHM) --- + if status_msg_id: + await bot.delete_message(chat_id, status_msg_id) + + # --- SUGGEST 5 LAGU --- + t_recs_start = time.time() recs = await get_recommendations(video_id) + logger.info(f"⏱️ [FASE 4] Fetch Rekomendasi memakan waktu: {time.time() - t_recs_start:.2f} detik") + if recs: kb = [] - # Buat baris tombol untuk setiap lagu rekomendasi - for rec in recs: - short_title = rec['title'][:30] + "..." if len(rec['title']) > 30 else rec['title'] - kb.append([InlineKeyboardButton(text=f"🎵 {short_title}", callback_data="ignore")]) - kb.append([ - InlineKeyboardButton(text="▶️ Play Now", callback_data=f"play_{rec['id']}"), - InlineKeyboardButton(text="➕ Queue", callback_data=f"queue_{rec['id']}") - ]) + for rec in recs[:5]: + short_title = rec['title'][:60] + "..." if len(rec['title']) > 60 else rec['title'] + kb.append([InlineKeyboardButton(text=f"▶️ {short_title}", callback_data=f"play_{rec['id']}")]) reply_markup = InlineKeyboardMarkup(inline_keyboard=kb) - await bot.send_message(chat_id, f"💡 **Rekomendasi Selanjutnya untukmu:**", reply_markup=reply_markup, parse_mode="Markdown") + await bot.send_message( + chat_id=chat_id, + text=f"💡 **Rekomendasi dari:**\n_{title}_", + reply_markup=reply_markup, + parse_mode="Markdown" + ) + + logger.info(f"✅ TOTAL WAKTU E2E: {time.time() - total_start_time:.2f} detik\n{'-'*40}") except Exception as e: logger.error(f"Error: {e}") + try: + await bot.edit_message_text("❌ Terjadi kesalahan internal saat memproses lagu.", chat_id, status_msg_id) + except: pass finally: if local_file and os.path.exists(local_file): os.remove(local_file) -# --- AUTO CLEANUP --- +# --- AUTO CLEANUP CACHE --- async def cleanup_expired_cache(): expired_items = await db.get_expired_cache(days=7) for item in expired_items: await delete_audio(item['s3_object_key']) await db.delete_cache(item['youtube_id']) -# --- MAIN LOOP --- +# --- MAIN RUNNER --- async def main(): await db.connect() - - # Menyalakan Background Worker untuk Queue asyncio.create_task(queue_worker()) scheduler = AsyncIOScheduler() scheduler.add_job(cleanup_expired_cache, 'cron', hour=3, minute=0) scheduler.start() - - logger.info("🚀 Bot Music Ready dengan Antrean & Rekomendasi!") + + logger.info("🚀 Bot Music - Mode Interactive Playlist Berjalan!") await bot.delete_webhook(drop_pending_updates=True) await dp.start_polling(bot) diff --git a/cookies.txt b/cookies.txt index ed8ec8e..e96bafc 100644 --- a/cookies.txt +++ b/cookies.txt @@ -3,12 +3,12 @@ .youtube.com TRUE / TRUE 2147483647 __Secure-1PAPISID CABFFJNnCe9Ocqz0/ANX4rXmqpbZzZy8y1 .youtube.com TRUE / TRUE 2147483647 __Secure-1PSID g.a0008AhqPWcg3-Syf1o_JEl3LCHzWFKn_n3Y8jgh3RnwDW83PtpvDZ6ZjTqI52V0CZWm40gj1wACgYKAYYSARESFQHGX2MigEiR2_gNFFV-QFRM2YthnxoVAUF8yKp0aiEfXAKcPOTZdsLVsdG00076 -.youtube.com TRUE / TRUE 1805949648 __Secure-1PSIDCC AKEyXzXscwi6BSxDoeuZ7ScUtDA6oVx7r92n-r3mTU3Htk4sV9EpzXwavLppIPUDaV5LUM9wcw +.youtube.com TRUE / TRUE 1805951136 __Secure-1PSIDCC AKEyXzUIxLQmOaltTf1fGcQk8YPiD-LeYgDwpvoYnXyN2XpMqdyv8dRk8RMnPnEuNpcNaJpiuA .youtube.com TRUE / TRUE 2147483647 __Secure-1PSIDRTS sidts-CjcBWhotCb-4c6IuQXRvmRVYyon3cb4LfaOkQmhdlWz2FTjey8tLFTyGymAQr1fbHPACgLGOXKsXEAA .youtube.com TRUE / TRUE 2147483647 __Secure-1PSIDTS sidts-CjUBWhotCXH5m0kFaAC1lhqf8kaotcZDxFxOty6XBxNsYxySai6J15Fi8BHklGtKOGZKSNYrLxAA -.youtube.com TRUE / TRUE 1837485634 __Secure-3PAPISID CABFFJNnCe9Ocqz0/ANX4rXmqpbZzZy8y1 -.youtube.com TRUE / TRUE 1837485634 __Secure-3PSID g.a0008AhqPWcg3-Syf1o_JEl3LCHzWFKn_n3Y8jgh3RnwDW83PtpvC14W2htdab0jQBsnf1YkqgACgYKAdcSARESFQHGX2MiTK5ZxIBltpFTHCsEt_xiuxoVAUF8yKrG5FgxNyKwsIayRODxp8Fo0076 -.youtube.com TRUE / TRUE 1805949648 __Secure-3PSIDCC AKEyXzU266eQwSCnNxn6ExAq3rRybQLzE4Z8R5O0uaTVV9E5h7LZ9FtpNU2MulJbDaDuM8Bhcw +.youtube.com TRUE / TRUE 1837487124 __Secure-3PAPISID CABFFJNnCe9Ocqz0/ANX4rXmqpbZzZy8y1 +.youtube.com TRUE / TRUE 1837487124 __Secure-3PSID g.a0008AhqPWcg3-Syf1o_JEl3LCHzWFKn_n3Y8jgh3RnwDW83PtpvC14W2htdab0jQBsnf1YkqgACgYKAdcSARESFQHGX2MiTK5ZxIBltpFTHCsEt_xiuxoVAUF8yKrG5FgxNyKwsIayRODxp8Fo0076 +.youtube.com TRUE / TRUE 1805951136 __Secure-3PSIDCC AKEyXzUEL9zBsIFlxJP8kImHFhOK8FA5ZzEE4j6_lcjzKUrODAt1IS4GDcyMK2tbSJFdpBMnaA .youtube.com TRUE / TRUE 2147483647 __Secure-3PSIDRTS sidts-CjcBWhotCb-4c6IuQXRvmRVYyon3cb4LfaOkQmhdlWz2FTjey8tLFTyGymAQr1fbHPACgLGOXKsXEAA .youtube.com TRUE / TRUE 2147483647 __Secure-3PSIDTS sidts-CjUBWhotCXH5m0kFaAC1lhqf8kaotcZDxFxOty6XBxNsYxySai6J15Fi8BHklGtKOGZKSNYrLxAA .youtube.com TRUE / TRUE 2147483647 __Secure-BUCKET CJsE @@ -19,7 +19,7 @@ .youtube.com TRUE / TRUE 2147483647 APISID PbWG-XrqTeJJFiPn/ACQ0hTzgL5lCMuKP5 .youtube.com TRUE / FALSE 0 PREF hl=en&tz=UTC .youtube.com TRUE / TRUE 0 SOCS CAI -.youtube.com TRUE / TRUE 0 YSC P4gtt98KyR4 -.youtube.com TRUE / TRUE 1789965648 VISITOR_INFO1_LIVE simeqjbyhfg -.youtube.com TRUE / TRUE 1789965648 VISITOR_PRIVACY_METADATA CgJJRBIEGgAgbg%3D%3D -.youtube.com TRUE / TRUE 1837485634 LOGIN_INFO AFmmF2swRgIhALLG_pjzu2VHqpJ3nvTgO4BBFATaF3B4-ynZF1X3zIZ7AiEAxxUAkMU1uCiUa0ieIKye_J-fFQZUfYU0UWWYlfDdjMY:QUQ3MjNmeVhrOEFad0h6TkRFZEFwdl9FZU15ejZuT0VVV0cwNmgtSVljMGFQRmlxQkppa0pCQ1VaXzZXTEtYU2JaSF9sLS1uemN5YWUzNG9WZlA3Z1NlWENpSnNkSDdTRjJHY0hFZ2xuaUVBaXJTTXAzNDNxY2wtVFFFMzRJNmdXbXpDazZhVmt1dHJndGhfSDF3U181QlI5UDdvVmhxc1VB +.youtube.com TRUE / TRUE 0 YSC 1xjiT5Oikbs +.youtube.com TRUE / TRUE 1837487125 LOGIN_INFO AFmmF2swRQIhAL69BVhiHN7pI8NRR4HKWf4y1nB2NymBs8b4SapeMuYBAiBXnlXmRsm7fl_vzwvgkCRSs-81PNyApm33E11JW8RoJA:QUQ3MjNmejFnV0xvdHdQeXktQUJoY1pZZ3lDT1N4U1ZMUUNYVmdDNG02YzZTSjM2ejltODhFWFBjTXduNXNyamQ5OEZRRzU3RzVpWTJVM051T3pwbHEzZkZTcjJtS1RTcXFoMVpDLWRiR1g0YVN6VnpBQTFUOUxhSTZiMUlMdzB4bWtjaVV2dklrcW05TlVKOE0tcGpRVnRrSE9rX1czUE9B +.youtube.com TRUE / TRUE 1789967136 VISITOR_INFO1_LIVE 68v_ALXnekk +.youtube.com TRUE / TRUE 1789967136 VISITOR_PRIVACY_METADATA CgJJRBIEGgAgIA%3D%3D diff --git a/downloads/7SqNVv98e8Q.mp3 b/downloads/7SqNVv98e8Q.mp3 new file mode 100644 index 0000000..0aaafcf Binary files /dev/null and b/downloads/7SqNVv98e8Q.mp3 differ diff --git a/downloads/grp6FCnioMM.mp3 b/downloads/grp6FCnioMM.mp3 new file mode 100644 index 0000000..9d50375 Binary files /dev/null and b/downloads/grp6FCnioMM.mp3 differ diff --git a/downloads/gvunApwKIiY.mp3 b/downloads/gvunApwKIiY.mp3 new file mode 100644 index 0000000..7717513 Binary files /dev/null and b/downloads/gvunApwKIiY.mp3 differ diff --git a/downloads/gvunApwKIiY.webm b/downloads/gvunApwKIiY.webm new file mode 100644 index 0000000..490c125 Binary files /dev/null and b/downloads/gvunApwKIiY.webm differ diff --git a/yt_engine.py b/yt_engine.py index 1011223..5630428 100644 --- a/yt_engine.py +++ b/yt_engine.py @@ -110,7 +110,7 @@ async def process_youtube_request(query: str): # ========================================== # FITUR TAMBAHAN: YOUTUBE MIX RECOMMENDATION # ========================================== -def fetch_recommendations_sync(video_id: str, limit: int = 3): +def fetch_recommendations_sync(video_id: str, limit: int = 5): """ Mengambil rekomendasi lagu dari YouTube Mix algoritma (Vibes/Genre serupa) """