From 2e2b10890cbf18ef25de7c7597ce6172e9ae546f Mon Sep 17 00:00:00 2001 From: YOLANDO Date: Thu, 26 Mar 2026 08:58:08 +0700 Subject: [PATCH] init awal 30 lagu --- .env | 2 +- __pycache__/yt_engine.cpython-312.pyc | Bin 5118 -> 6801 bytes bot.py | 144 ++++++++++++++++++-------- cookies.txt | 10 +- yt_engine.py | 40 +++++++ 5 files changed, 148 insertions(+), 48 deletions(-) diff --git a/.env b/.env index 7430102..90890e9 100644 --- a/.env +++ b/.env @@ -1,6 +1,6 @@ # --- TELEGRAM BOT --- BOT_TOKEN=8747085830:AAHsOTdZbK40daE1Lxmjvw5CcKHY_A7sucI -AUTHORIZED_USER_ID=951506682 +AUTHORIZED_USER_IDS=951506682 # --- POSTGRESQL --- # Gunakan 'localhost' jika aplikasi jalan langsung di VPS (tanpa Docker), diff --git a/__pycache__/yt_engine.cpython-312.pyc b/__pycache__/yt_engine.cpython-312.pyc index 416a00d9980b8fdde704c8d8a31a154f0c4f1e51..e4ec8192aefa1a06b6c66548a1717aaafcb43f90 100644 GIT binary patch delta 1388 zcmZ`(Urbw77(b`&?Y)=w_TQyI>5vu%wB1lAFdV}y2y-uUii`(TESKKPpRL@Mb6ZPi zflZAg1{3g_*kv;o6C(x_y9XmLCPPRx@ukQNwef*B<6HS-maxQlPHElt;JNvJzx(|; z-}jw+znp)X7TZj}a9kO{EBx`s_&eo`rYg9+*z_v2E>5+2M4M=o^rA_kM|7h3ed=}N z6BY}$aB=A6@22CnQ#r$gFy%**5)DR_fQIBOE(Ws}%YGxaehNg0AI%+&x~p%qYFFS< z6rx0JLL=(bcDjY867V(<_39+;ZlJ$mR*KoCsZlVk7uf{$Hb9XCC9)2@xd#je)gNfL z>f%`SPue+XDCJb(ye~mt;f^Xij-s&?GX)hp1pgDoJr<>bKr~G=s+n<|C?j+u$=zsR9I}AfOolqGg($ zVf%_4gE26r9RM-twOu>=vLufR6T{;{mne)x1mqeM6s&|LIUt~ML3Tw1IV`v)Bv}l_ zoy3r1;t@p>P+;s3cD#g|iTxP@&k{IQh=_(s zDhcK2z?dM*QZPsN%U5Mw!mE80hXOeG(lt|GA)8A=D1lTiu9Q3o;6#UVqTMB_^B`DI~3Qa`j* z&pJk=NMNj(rZkd%B`ych6}&~lq8!I_cOB&Qz*dp1I<`ycD)rZ2IWPjI>}Kpn?B>jk znT3h_^zp2xZO)uDZZZvdT{&g@gX6cknoX`I8QS8W&I1eUOxN$!G^DPk2Gj2J`E+~6 zd3JX0VwSJi=Ib~4`qahMwhUjt#dpp1>{L|EUC5fNl0BPdSGLBT^4{|A0mkZFIG?LL zma>24SaK}4ep#FD|9;@^z-sKbwhKF+=5N`r*|cxV^TMy5;d}O<9BYoB&un$|V=Xf< zoaC}qwV#`AnX+|F%eti#*#_To`|Q$Nxb0n9=c)c1=p4>Gi)-X+N>qqRvJcT`n$|d~0R_xu)U=5t?c5By~tpv{z z{L$K_$FyF_V)we6>h{s=jc2q(@fEO{?XhUrPn&xT+6@Zg_YDJu@rnC^!uj9BvVU0} zvbE?Ea7C;BZg8pJ*=(KUe|a^JrbHoEfy;&5m~wn^HsYaZBswhlgX2mB1+nJ?EJ~dU zdA_A{Rl4`ambG`=(3{cqW(>WDiV=I?t@Xeq*r@x0@<1>Ai?o30DMBXoIlhZ7ObQCB zVZORd!DezzDf8+l{7~yYfbiU|sTSH&eR+T>t?U!peGx+V8YD)-Y2~262;r^&0=$fA A5dZ)H delta 104 zcmbPe`cIwjG%qg~0}vc5Ih=WbcOstzW6MPK07kxv6(%f|jGFu#uT%tP#V diff --git a/bot.py b/bot.py index f7324e2..69834a6 100644 --- a/bot.py +++ b/bot.py @@ -11,7 +11,8 @@ from aiogram.client.session.aiohttp import AiohttpSession from dotenv import load_dotenv from apscheduler.schedulers.asyncio import AsyncIOScheduler -from yt_engine import process_youtube_request, get_recommendations +# Import fungsi baru search_youtube_list +from yt_engine import process_youtube_request, get_recommendations, search_youtube_list from db_manager import db from s3_manager import upload_audio, download_audio, delete_audio @@ -20,7 +21,9 @@ logger = logging.getLogger(__name__) load_dotenv() BOT_TOKEN = os.getenv("BOT_TOKEN") -AUTHORIZED_USER_ID = int(os.getenv("AUTHORIZED_USER_ID", 0)) + +auth_users_str = os.getenv("AUTHORIZED_USER_IDS", "") +AUTHORIZED_USER_IDS = {int(uid.strip()) for uid in auth_users_str.split(",") if uid.strip().isdigit()} session = AiohttpSession(timeout=300) bot = Bot(token=BOT_TOKEN, session=session, default=DefaultBotProperties(parse_mode=None)) @@ -28,6 +31,10 @@ dp = Dispatcher() music_queue = asyncio.Queue() +# --- PENYIMPANAN SEMENTARA UNTUK PAGING --- +# Format: { chat_id: {'query': 'judul', 'results': [list_lagu]} } +user_searches = {} + async def queue_worker(): while True: query, chat_id, msg_id = await music_queue.get() @@ -40,69 +47,135 @@ async def queue_worker(): class SecurityMiddleware(BaseMiddleware): async def __call__(self, handler, event: types.Message | CallbackQuery, data: dict): - if event.from_user.id != AUTHORIZED_USER_ID: return + user_id = event.from_user.id + if user_id not in AUTHORIZED_USER_IDS: + logger.warning(f"⚠️ Akses ditolak untuk User ID: {user_id}") + return return await handler(event, data) dp.message.middleware(SecurityMiddleware()) dp.callback_query.middleware(SecurityMiddleware()) -# Ekstraktor ID YouTube def extract_video_id(query): match = re.search(r"(?:v=|\/)([0-9A-Za-z_-]{11}).*", query) return match.group(1) if match else None +# --- FUNGSI PEMBUAT TOMBOL PAGING --- +def get_search_keyboard(chat_id, page=0): + data = user_searches.get(chat_id) + if not data: return None + + results = data['results'] + start_idx = page * 10 + end_idx = start_idx + 10 + page_results = results[start_idx:end_idx] + + kb = [] + for rec in page_results: + # Tampilkan Judul | Nama Channel (Biar tahu itu lagu asli atau cover) + display_text = f"🎵 {rec['title']} | {rec['uploader']}" + short_text = display_text[:55] + "..." if len(display_text) > 55 else display_text + + # Menggunakan format callback "play_" agar otomatis masuk antrean download + kb.append([InlineKeyboardButton(text=short_text, callback_data=f"play_{rec['id']}")]) + + # Tombol Navigasi Next/Prev + nav_buttons = [] + if page > 0: + nav_buttons.append(InlineKeyboardButton(text="⬅️ Prev", callback_data=f"page_{page-1}")) + if end_idx < len(results): + nav_buttons.append(InlineKeyboardButton(text="Next ➡️", callback_data=f"page_{page+1}")) + + if nav_buttons: + kb.append(nav_buttons) + + return InlineKeyboardMarkup(inline_keyboard=kb) + +# --- COMMANDS --- @dp.message(Command("start")) async def cmd_start(message: types.Message): - await message.answer("🎧 **Music Bot Ready (M4A Turbo)!**\nKirim format: `/play `\nSetelah lagu terkirim, klik rekomendasi di bawahnya.", parse_mode="Markdown") + await message.answer("🎧 **Music Bot Ready!**\nKirim format: `/play `", 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.") - status_msg = await message.answer("⏳ Memproses pesananmu...") - await music_queue.put((query, message.chat.id, status_msg.message_id)) + + status_msg = await message.answer("🔍 *Mencari daftar lagu...*", parse_mode="Markdown") + + # Ambil 30 hasil sekaligus + results = await search_youtube_list(query, max_results=30) + + if not results: + return await status_msg.edit_text("❌ Lagu tidak ditemukan.") + + # Simpan ke memori sementara untuk paging + user_searches[message.chat.id] = { + 'query': query, + 'results': results + } + + # Tampilkan halaman pertama (index 0) + kb = get_search_keyboard(message.chat.id, page=0) + await status_msg.edit_text( + text=f"🔎 Hasil pencarian: **{query}**\n_Pilih lagu untuk diunduh:_", + reply_markup=kb, + parse_mode="Markdown" + ) +# --- HANDLER TOMBOL PAGING (NEXT/PREV) --- +@dp.callback_query(F.data.startswith("page_")) +async def handle_page_click(callback: CallbackQuery): + chat_id = callback.message.chat.id + if chat_id not in user_searches: + return await callback.answer("Pencarian kadaluarsa. Ketik /play lagi.", show_alert=True) + + page = int(callback.data.split("_")[1]) + kb = get_search_keyboard(chat_id, page) + + if kb: + query = user_searches[chat_id]['query'] + await callback.message.edit_text( + text=f"🔎 Hasil pencarian: **{query}**\n_Halaman {page+1}_", + reply_markup=kb, + parse_mode="Markdown" + ) + await callback.answer() + +# --- HANDLER DOWNLOAD LAGU --- @dp.callback_query(F.data.startswith("play_")) async def handle_suggestion_click(callback: CallbackQuery): video_id = callback.data.split("_", 1)[1] + + # Opsional: Jika tidak mau layarnya penuh tombol setelah diklik, hapus tombolnya await callback.message.edit_reply_markup(reply_markup=None) - status_msg = await bot.send_message(callback.message.chat.id, "⏳ Memproses lagu pilihanmu...") + + status_msg = await bot.send_message(callback.message.chat.id, "⏳ Mengunduh lagu pilihanmu...") query_url = f"https://www.youtube.com/watch?v={video_id}" await music_queue.put((query_url, callback.message.chat.id, status_msg.message_id)) await callback.answer("Siapp! Diproses...") -# --- CORE LOGIC (DENGAN SMART CACHE) --- +# --- CORE LOGIC (DOWNLOAD & S3 CACHE) --- async def process_music_request(query: str, chat_id: int, status_msg_id: int): local_file = None - total_start_time = time.time() - try: - logger.info(f"▶️ MEMULAI PROSES: {query}") - - # 1. SMART CACHE CHECK (CEK DB DULUAN) video_id = extract_video_id(query) cached_data = None if video_id: - t_s3_check = time.time() cached_data = await db.get_cache(video_id) if cached_data: - logger.info(f"⚡ CACHE HIT! File sudah ada di MinIO. (Cek butuh {time.time() - t_s3_check:.2f}s)") - title = cached_data['title'] - s3_object_name = cached_data['s3_object_key'] - # Bisa saja file lama berekstensi mp3, atau baru berekstensi m4a + title = cached_data.get('title', 'Lagu') + s3_object_name = cached_data.get('s3_object_key', f"{video_id}.m4a") ext = s3_object_name.split('.')[-1] local_file = f"downloads/cache_{video_id}.{ext}" - await bot.edit_message_text("⚡ Mengambil langsung dari Cache (MinIO)...", chat_id, status_msg_id) + await bot.edit_message_text("⚡ Mengambil dari Cache...", chat_id, status_msg_id) await download_audio(s3_object_name, local_file) - # 2. JIKA TIDAK ADA DI CACHE, BARU PANGGIL YOUTUBE if not cached_data: - await bot.edit_message_text("🔍 Mencari dan mengunduh dari YouTube...", chat_id, status_msg_id) - t_yt_start = time.time() + await bot.edit_message_text("🔍 Mengunduh dari YouTube...", chat_id, status_msg_id) yt_result = await process_youtube_request(query) - logger.info(f"⏱️ yt-dlp (Tanpa FFmpeg) memakan waktu: {time.time() - 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) @@ -110,42 +183,29 @@ async def process_music_request(query: str, chat_id: int, status_msg_id: int): video_id = yt_result["video_id"] title = yt_result["title"] local_file = yt_result["file_path"] - - # Simpan sesuai ekstensi aslinya (sekarang biasanya m4a) s3_object_name = f"{video_id}.{yt_result['ext']}" - await bot.edit_message_text("☁️ Mengunggah ke MinIO Server...", chat_id, status_msg_id) + await bot.edit_message_text("☁️ Menyimpan ke Server...", chat_id, status_msg_id) if local_file and os.path.exists(local_file): - t_s3_up = time.time() await upload_audio(local_file, s3_object_name) await db.save_cache(video_id, title, s3_object_name) - logger.info(f"⏱️ Upload ke MinIO memakan waktu: {time.time() - t_s3_up:.2f} detik") - # 3. KIRIM KE TELEGRAM - await bot.edit_message_text("📤 Mengirim file ke Telegram...", chat_id, status_msg_id) + await bot.edit_message_text("📤 Mengirim 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) - logger.info(f"⏱️ Upload ke Telegram memakan waktu: {time.time() - t_tele_start:.2f} detik") - if status_msg_id: await bot.delete_message(chat_id, status_msg_id) - # 4. SUGGEST 5 LAGU (Judul lebih panjang) - t_recs = time.time() + # --- TETAP TAMPILKAN 5 SUGGESTION SETELAH LAGU SELESAI DIPUTAR --- recs = await get_recommendations(video_id) - logger.info(f"⏱️ Fetch Rekomendasi memakan waktu: {time.time() - t_recs:.2f} detik") - if recs: kb = [] for rec in recs[:5]: - short_title = rec['title'][:60] + "..." if len(rec['title']) > 60 else rec['title'] + short_title = rec['title'][:55] + "..." if len(rec['title']) > 55 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, 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}") finally: @@ -163,7 +223,7 @@ async def main(): scheduler = AsyncIOScheduler() scheduler.add_job(cleanup_expired_cache, 'cron', hour=3, minute=0) scheduler.start() - logger.info("🚀 Bot Music Turbo Berjalan!") + logger.info("🚀 Bot Music V3 (Search & Paging) Berjalan!") await bot.delete_webhook(drop_pending_updates=True) await dp.start_polling(bot) diff --git a/cookies.txt b/cookies.txt index 728960e..43d043f 100644 --- a/cookies.txt +++ b/cookies.txt @@ -3,12 +3,12 @@ .youtube.com TRUE / TRUE 2147483647 __Secure-1PAPISID jX9HIeAZEXX3bP9f/AopL252XpVmN6w-fU .youtube.com TRUE / TRUE 2147483647 __Secure-1PSID g.a0008AhqPQ4vlKVPuJB4BY8zzdimzFGpjCZI3PtorsbzCvPCb3-UNiEKAu2Nem3IRBTeL7M4jQACgYKAWsSARESFQHGX2MiBR4lu_TMGVkDg6nuR2v3KxoVAUF8yKpUhZ9s0gTmijqUQGCrrAbz0076 -.youtube.com TRUE / TRUE 1806024377 __Secure-1PSIDCC AKEyXzWzicfhafmNedOTnB5yBriXagyKfyIEQYIkhivz2G_stNbGK8uupR-Q9WaCEF6zOz1d +.youtube.com TRUE / TRUE 1806026268 __Secure-1PSIDCC AKEyXzX-CL1XOf6OeNeSLRiwwlfwxttYfujFl_Cgwant9zdr6x0pJNiNPlM3ehq1FGS36kyC .youtube.com TRUE / TRUE 2147483647 __Secure-1PSIDRTS sidts-CjYBWhotCR9GvMAvgfIj2VO7w2GS8jUm45hvgjK75mkpM7qlEO-z2RIGvuP3FX0ApZ3UEnRHAaIQAA .youtube.com TRUE / TRUE 2147483647 __Secure-1PSIDTS sidts-CjYBWhotCR9GvMAvgfIj2VO7w2GS8jUm45hvgjK75mkpM7qlEO-z2RIGvuP3FX0ApZ3UEnRHAaIQAA .youtube.com TRUE / TRUE 2147483647 __Secure-3PAPISID jX9HIeAZEXX3bP9f/AopL252XpVmN6w-fU .youtube.com TRUE / TRUE 2147483647 __Secure-3PSID g.a0008AhqPQ4vlKVPuJB4BY8zzdimzFGpjCZI3PtorsbzCvPCb3-UVw0LJPNy1xBeKd3GjL_nrgACgYKAd8SARESFQHGX2Mi4oiwDW0agUlDxti69hv6zBoVAUF8yKqTGo8oCprmktCz7NrkNVpx0076 -.youtube.com TRUE / TRUE 1806024377 __Secure-3PSIDCC AKEyXzXoNsYyfFthEbcFq88fQFsmt-Qraq1XS_2EA6ZkjjP3cNn0AEt1tF2sVtRrGU0CjWzwvg +.youtube.com TRUE / TRUE 1806026268 __Secure-3PSIDCC AKEyXzV4asU3xodaOGcneKNBezILDXAjNbLEZhJZv479YWYlAECeXq-5QgNsMibF-b3qu2d33g .youtube.com TRUE / TRUE 2147483647 __Secure-3PSIDRTS sidts-CjYBWhotCR9GvMAvgfIj2VO7w2GS8jUm45hvgjK75mkpM7qlEO-z2RIGvuP3FX0ApZ3UEnRHAaIQAA .youtube.com TRUE / TRUE 2147483647 __Secure-3PSIDTS sidts-CjYBWhotCR9GvMAvgfIj2VO7w2GS8jUm45hvgjK75mkpM7qlEO-z2RIGvuP3FX0ApZ3UEnRHAaIQAA .youtube.com TRUE / TRUE 2147483647 __Secure-ROLLOUT_TOKEN CMbM-7XNqZzV5QEQmYbS_rO8kwMY5PKr_7O8kwM%3D @@ -18,6 +18,6 @@ .youtube.com TRUE / TRUE 2147483647 LOGIN_INFO AFmmF2swRAIgDHRqApvISbyAZq7Jw7ACZDaYENyiLfRJhkSxgsaSzHUCIDBqEp7Lla6YL_9yHKzX5OuwRv6eSuDP7wN4eu2h8boE:QUQ3MjNmenlDUldaUlpCVmlMc2ZFdWxtWVlzX2lVRTRic2o4djgzSlJiaHhHYnN4dmh0YTU2NFdpS1lRSkZIUlN3RVBRWmZMTFdTU1pjQ3gtU09LTmRUX3l6WEpCWGdpeFpiVkptS1BHRlBlQXU1R0RXZjR3cnR4LW94YlREbzFITzdOaWctTEJLcjN5UWpCM3VXVTQzUWZiMl9RLXhNRERR .youtube.com TRUE / TRUE 2147483647 NID 530=J1je9vBs7k1AZSF4I6E-32SX_vniEVSvWt6vRJC19rznxuHBniKAUk91MSxJ9KY6sPEIGqw29RYQvsglruzNcyGUpG4RIb4Fk50UUrFHbXG28v34nj9rkikmhXi7N84D_-1l8im9-Qm9zkPfxKaL8V8xBaBjK6a1p2et2q3_n5wM0hOI7R24Gw4hQbX9Rcj-oUTuBbvIxb_VevmQ .youtube.com TRUE / FALSE 0 PREF hl=en&tz=UTC -.youtube.com TRUE / TRUE 0 YSC YNBoD1tlo5I -.youtube.com TRUE / TRUE 1790040377 VISITOR_INFO1_LIVE H2nqd5gOIN0 -.youtube.com TRUE / TRUE 1790040377 VISITOR_PRIVACY_METADATA CgJJRBIEGgAgYQ%3D%3D +.youtube.com TRUE / TRUE 0 YSC T0iFTYMzz_E +.youtube.com TRUE / TRUE 1790042268 VISITOR_INFO1_LIVE fHx_hZBK2nA +.youtube.com TRUE / TRUE 1790042268 VISITOR_PRIVACY_METADATA CgJJRBIEGgAgQw%3D%3D diff --git a/yt_engine.py b/yt_engine.py index 6f4fa7e..ec9dd8f 100644 --- a/yt_engine.py +++ b/yt_engine.py @@ -93,3 +93,43 @@ def search_and_download(query_or_url: str): async def process_youtube_request(query: str): return await asyncio.to_thread(search_and_download, query) + + +# ========================================== +# FITUR TAMBAHAN: PENCARIAN LIST LAGU +# ========================================== +def fetch_search_results_sync(query: str, max_results: int = 30): + """Mengambil daftar hasil pencarian tanpa mendownload audionya""" + ydl_opts_search = { + 'quiet': True, + 'extract_flat': True, # Mengambil metadata saja biar super cepat + # KITA HAPUS extractor_args (player_client) di sini karena bikin search error + } + + if os.path.exists("cookies.txt"): + ydl_opts_search['cookiefile'] = 'cookies.txt' + + try: + with yt_dlp.YoutubeDL(ydl_opts_search) as ydl: + # Memaksa yt-dlp menggunakan mode pencarian (ytsearch) secara eksplisit + search_query = f"ytsearch{max_results}:{query}" + info = ydl.extract_info(search_query, download=False) + + results = [] + if 'entries' in info: + for entry in info['entries']: + if entry and entry.get('id') and entry.get('title'): + # Terkadang di hasil pencarian nama key-nya 'channel', bukan 'uploader' + uploader_name = entry.get('uploader') or entry.get('channel') or 'Unknown' + results.append({ + 'id': entry['id'], + 'title': entry['title'], + 'uploader': uploader_name + }) + return results + except Exception as e: + print(f"Search error: {e}") + return [] + +async def search_youtube_list(query: str, max_results: int = 30): + return await asyncio.to_thread(fetch_search_results_sync, query, max_results)