From f962f16308a3a16c84c10f5ec10f4577edc1226f Mon Sep 17 00:00:00 2001 From: YOLANDO Date: Wed, 25 Mar 2026 11:44:55 +0700 Subject: [PATCH] v2 --- __pycache__/yt_engine.cpython-312.pyc | Bin 4254 -> 5964 bytes bot.py | 167 +++++++++++++------------- cookies.txt | 16 +-- yt_engine.py | 43 +++++++ 4 files changed, 136 insertions(+), 90 deletions(-) diff --git a/__pycache__/yt_engine.cpython-312.pyc b/__pycache__/yt_engine.cpython-312.pyc index 31553d7f5ff8043fe2e5dc963b117712fcaad119..31c953349f47e4dc6cbb7034a784dd5f1be75d28 100644 GIT binary patch delta 1684 zcmZWpUu;uV7(b`?-uCv-UE6gVU0L;RWt8In2#Ah96QPC8=oZ-q$w;)h>$xkJ_0Kr> zmaa=nhaq86Ou!rBh^X6xu?8PTcyLb!+zW{$b^)`d@nJ?E)TfSNF!16z-4=;{O@HS* z-@o&n^L_XGy!n2I{|7;^076rdMP;Y~fM4lmlei6L?c!B`v<1~5i)=(z**wZ3+ePk_ zh3-cUNVv$JvSwhwe(U3QEBso&V?AlyPh9ZjY0(??gpDPT)n@?XS_*_C}9p6vj${3NO&{QXpkzM;q_m5 zmtN$BI>cwervRBLe#Yk5EOW+6l?Pa4`Nn#4BZnLp1vw+KWw~!d*#1zBT+97TWns@a zo*&#;(=ZLzV+Cp_hb$tYJ~$rDq>8ErbHxRMI271@OdohNXxW9*s#_*-GtWimH)J$~9G0KB6UYSi(^i^TYw~B`_(T z*0gbTe`jYpo$lB;tRs?)ccvvRa{AD@?n!<-4bwdkI)-1RZe0ZW@jlAZjtv<89{f&pHR>+`O zI61DVVJt^f{1%m&;>tugg=6?_x?`!L%#mlp87vZsY9=6xd0HnzxS3jcMrCqA8YCI7 zmP=ET2{o){5)m9G24|_GhkOuIpkf2H4JCI|X-m`0*{hk-)@?=qk>d7X(GyxVbM?ll zzQ+#0TP~$9q%WPnaQ;gCp=n!b`<`jxN^FVuRoGgt_GiJhoEN-Hf_EnVKxnIg8nb(z zS#ftQxECyUcHHZ_x3k!AV!Ef~YFKvpmR!Eso_V?G@;z`JoDQwH>Zd~`yL%?IWEWSw z&9i~);llC4K+*g1BfvSgUO8Ic(llHDRpVS^;gv66oImmH@U7v6^iO+xNYYiG*Wjaw@maVOWSb3gHtjGoZ69#MGq_yu6Ae*_F9bzUes z+6z))uvoL}A-}t1cYb7kR^f>1sY-{_?fRe{+~qri40E^3N$`716`Q=2-)AP;MTQI7 zT1|^QLpQc6+Ree;%%a;KY-bkzkn-&uL4BqsnQuOlN<_y}s(6^zLnKXw#gU{YY6_CZ z#M4qD+8JLGVDoB}NvnUQn~s&s~J)Yui3wf|NaOGJN-8j1vd2Y*k2-<8Vd8^}V=u+v02KXi{ MFpmLU@hEZm2cz7%=l}o! delta 242 zcmX@3H&0RhG%qg~0}#{%9nP%dXJB{?;=lk4l<_%jqIx<@C8H+S#tpjMjGB}0afiuU z0ws&AfW$4%w9K5;_=3cej9aX!MMe2VnoLEulY@D>1nfX+;0lZEC%@$}V)WUpz}wBl z;RBS@WGwQXyqRBvF>vx7em$pR9-vs0oI2wIMpc%Dtm=%KoJGzcBY+NLDDnppPC()o yM{Z(vYD#8Nagh>8%pF94j4KM9yh}`qD-_6N1ma?s$@j#ZxV|v3GTIk~00jW-GBwx$ diff --git a/bot.py b/bot.py index 09dc612..c63d737 100644 --- a/bot.py +++ b/bot.py @@ -1,77 +1,98 @@ import asyncio import os +import time import logging -from aiogram import Bot, Dispatcher, types, BaseMiddleware +from aiogram import Bot, Dispatcher, types, BaseMiddleware, F from aiogram.filters import Command -from aiogram.types import FSInputFile, InlineKeyboardMarkup, InlineKeyboardButton +from aiogram.types import FSInputFile, InlineKeyboardMarkup, InlineKeyboardButton, CallbackQuery from aiogram.client.default import DefaultBotProperties -from aiogram.client.session.aiohttp import AiohttpSession # <-- TAMBAH INI +from aiogram.client.session.aiohttp import AiohttpSession from dotenv import load_dotenv from apscheduler.schedulers.asyncio import AsyncIOScheduler -# Import modul lokal yang sudah kita buat -from yt_engine import process_youtube_request +# 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 -# Setup Logging logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) load_dotenv() BOT_TOKEN = os.getenv("BOT_TOKEN") -# Pastikan ini mengambil angka ID Telegram kamu -AUTHORIZED_USER_ID = int(os.getenv("AUTHORIZED_USER_ID", 0)) +AUTHORIZED_USER_ID = int(os.getenv("AUTHORIZED_USER_ID", 0)) -# --- INISIALISASI BOT --- -# Timeout diatur 300 detik (5 menit) agar Telegram tidak memutus koneksi saat mengirim file MP3 yang besar -session = AiohttpSession(timeout=300) - -bot = Bot( - token=BOT_TOKEN, - session=session, - default=DefaultBotProperties(parse_mode=None) -) +session = AiohttpSession(timeout=300) +bot = Bot(token=BOT_TOKEN, session=session, default=DefaultBotProperties(parse_mode=None)) dp = Dispatcher() -# --- MIDDLEWARE / SECURITY CHECK --- + +# --- SISTEM ANTREAN PINTAR (PRIORITY QUEUE) --- +# Format isi antrean: (priority, timestamp, query, chat_id, status_msg_id) +music_queue = asyncio.PriorityQueue() + +async def queue_worker(): + """Pekerja di balik layar yang memproses antrean satu per satu""" + while True: + priority, _, query, chat_id, msg_id = await music_queue.get() + try: + await process_music_request(query, chat_id, msg_id) + except Exception as e: + logger.error(f"Worker Error: {e}") + finally: + music_queue.task_done() + +# --- MIDDLEWARE / SECURITY --- class SecurityMiddleware(BaseMiddleware): - async def __call__(self, handler, event: types.Message, data: dict): - # Memblokir semua orang kecuali Kamu (Single-User Mode) - if event.from_user.id != AUTHORIZED_USER_ID: - logger.warning(f"Akses ditolak untuk User ID: {event.from_user.id}") - return # Abaikan pesan + async def __call__(self, handler, event: types.Message | CallbackQuery, data: dict): + user_id = event.from_user.id + if user_id != AUTHORIZED_USER_ID: + return return await handler(event, data) -# Daftarkan Satpam ke sistem dp.message.middleware(SecurityMiddleware()) +dp.callback_query.middleware(SecurityMiddleware()) # --- COMMAND HANDLERS --- @dp.message(Command("start")) async def cmd_start(message: types.Message): - await message.answer( - "๐ŸŽง **Kantor-Bypass Music Bot Ready!**\n\n" - "Kirim judul lagu atau link YouTube dengan format:\n" - "`/play `\n\n" - "Contoh: `/play Dewa 19 Kangen`" - ) + await message.answer("๐ŸŽง **Kantor-Bypass Music Bot**\nKirim format: `/play `") @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 Nadin Amizah`") + return await message.answer("โš ๏ธ Masukkan judul lagu. Contoh: `/play Dewa 19 Kangen`") - # Kirim status loading - status_msg = await message.answer("๐Ÿ” *Mencari dan memproses...*") + 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)) + +# --- 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) - # Gunakan asyncio.create_task agar tidak nge-block queue bot (bisa antre banyak lagu) - asyncio.create_task(process_music_request(query, message.chat.id, status_msg.message_id)) + # Hapus tombol di pesan yang diklik biar gak di-spam klik + await callback.message.edit_reply_markup(reply_markup=None) + + 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!") -# --- CORE LOGIC: PROCESS MUSIC --- +# --- CORE LOGIC --- async def process_music_request(query: str, chat_id: int, status_msg_id: int): - local_file = None # Mencegah error 'UnboundLocalError' jika gagal di awal + local_file = None try: - # 1. Download dari YouTube + await bot.edit_message_text("๐Ÿ” Mencari dan memproses lagu...", chat_id, status_msg_id) yt_result = await process_youtube_request(query) if yt_result["status"] == "error": @@ -82,83 +103,65 @@ 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" - # 2. Cek Cache di Database 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) - # Download dari S3 ke local temporary + await bot.edit_message_text("โšก Mengambil dari Cache S3...", chat_id, status_msg_id) local_file = f"downloads/cache_{video_id}.mp3" await download_audio(s3_object_name, local_file) else: - await bot.edit_message_text("โ˜๏ธ *Mengunggah ke S3 Storage...*", chat_id, status_msg_id) - # Upload file baru ke S3 + await bot.edit_message_text("โ˜๏ธ Mengunggah ke S3 Storage...", 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) - # 3. Kirim Audio ke Telegram - await bot.edit_message_text("๐Ÿ“ค *Mengirim audio ke Telegram... (Mungkin butuh waktu)*", chat_id, status_msg_id) + await bot.edit_message_text("๐Ÿ“ค Mengirim audio ke Telegram...", chat_id, status_msg_id) if local_file and os.path.exists(local_file): audio = FSInputFile(local_file) - - # Opsi: Tombol pencarian selanjutnya - kb = [[InlineKeyboardButton(text="๐ŸŽต Putar Lagu Acak Lainnya", switch_inline_query_current_chat="")]] - reply_markup = InlineKeyboardMarkup(inline_keyboard=kb) - - # Ekstra Pengaman Timeout di Request Level - await bot.send_audio( - chat_id=chat_id, - audio=audio, - title=title, - performer="Music Bot", - reply_markup=reply_markup, - request_timeout=300 - ) - - # Hapus pesan status yang muter-muter + 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) - else: - await bot.edit_message_text("โŒ File audio tidak ditemukan di server lokal.", chat_id, status_msg_id) + + # --- MENCARI REKOMENDASI LAGU (MIX ALGORITHM) --- + recs = await get_recommendations(video_id) + 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']}") + ]) + + reply_markup = InlineKeyboardMarkup(inline_keyboard=kb) + await bot.send_message(chat_id, f"๐Ÿ’ก **Rekomendasi Selanjutnya untukmu:**", reply_markup=reply_markup, parse_mode="Markdown") except Exception as e: - logger.error(f"Error processing music: {e}") - try: - await bot.edit_message_text(f"โŒ Terjadi kesalahan internal: {e}", chat_id, status_msg_id) - except: - pass # Abaikan jika pesan sudah terhapus - + logger.error(f"Error: {e}") finally: - # 4. Hapus file temporary di VPS (Hemat Disk!) if local_file and os.path.exists(local_file): os.remove(local_file) -# --- BACKGROUND JOB: AUTO CLEANUP (7 HARI) --- +# --- AUTO CLEANUP --- async def cleanup_expired_cache(): - logger.info("๐Ÿงน Menjalankan tugas pembersihan cache otomatis...") expired_items = await db.get_expired_cache(days=7) - for item in expired_items: - # Hapus dari S3 await delete_audio(item['s3_object_key']) - # Hapus dari Database await db.delete_cache(item['youtube_id']) - logger.info(f"๐Ÿ—‘๏ธ Dihapus: {item['title']} (Usia > 7 Hari)") # --- MAIN LOOP --- async def main(): - # Konek ke database await db.connect() - # Jalankan Scheduler untuk bersih-bersih tiap jam 3 pagi + # 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 Started!") - - # Hapus webhook lama (jika ada) dan mulai polling + logger.info("๐Ÿš€ Bot Music Ready dengan Antrean & Rekomendasi!") await bot.delete_webhook(drop_pending_updates=True) await dp.start_polling(bot) diff --git a/cookies.txt b/cookies.txt index 075ae55..ed8ec8e 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 1805947740 __Secure-1PSIDCC AKEyXzUORFVg_0ZBDrcdDFVznCjghYk0kFgXPgu6_nGbfqQb8JQApHGnBqt7NkD7fgCea-TTmg +.youtube.com TRUE / TRUE 1805949648 __Secure-1PSIDCC AKEyXzXscwi6BSxDoeuZ7ScUtDA6oVx7r92n-r3mTU3Htk4sV9EpzXwavLppIPUDaV5LUM9wcw .youtube.com TRUE / TRUE 2147483647 __Secure-1PSIDRTS sidts-CjcBWhotCb-4c6IuQXRvmRVYyon3cb4LfaOkQmhdlWz2FTjey8tLFTyGymAQr1fbHPACgLGOXKsXEAA .youtube.com TRUE / TRUE 2147483647 __Secure-1PSIDTS sidts-CjUBWhotCXH5m0kFaAC1lhqf8kaotcZDxFxOty6XBxNsYxySai6J15Fi8BHklGtKOGZKSNYrLxAA -.youtube.com TRUE / TRUE 1837483738 __Secure-3PAPISID CABFFJNnCe9Ocqz0/ANX4rXmqpbZzZy8y1 -.youtube.com TRUE / TRUE 1837483738 __Secure-3PSID g.a0008AhqPWcg3-Syf1o_JEl3LCHzWFKn_n3Y8jgh3RnwDW83PtpvC14W2htdab0jQBsnf1YkqgACgYKAdcSARESFQHGX2MiTK5ZxIBltpFTHCsEt_xiuxoVAUF8yKrG5FgxNyKwsIayRODxp8Fo0076 -.youtube.com TRUE / TRUE 1805947740 __Secure-3PSIDCC AKEyXzVld0GspLJe8l6JS4cfa4h4ElnWgBoAB5kiHdSs0gCerB-quDuuc4JEn63Voja8hi9ZSQ +.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 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 a2WtW-hP4Ck -.youtube.com TRUE / TRUE 1789963740 VISITOR_INFO1_LIVE BIF7sNwBpLI -.youtube.com TRUE / TRUE 1789963740 VISITOR_PRIVACY_METADATA CgJJRBIEGgAgFA%3D%3D -.youtube.com TRUE / TRUE 1837483740 LOGIN_INFO AFmmF2swRgIhAMFCn3UGB5h-2Wf8Rnv4LIWp0LiVwf6ibiPVQhmFufMQAiEA-q7C22E801epMI4Lk54Xb9uNUHWVKkjNLHJ-auj0p7c:QUQ3MjNmelp0RkxSamFrTTZnOVBLelN1ZjVpeVRBWC1TTUc1d0NPUm9PXzNPQmx0dUNEdjBDOEF0cGsxc3p1SWhwMWtIZDdyMjRxYVFKZWRIWnkwMkxzLWVnakVCdFlxcVpFclc3N3VfNmpIWWh2aUd2eUxydzJMekZ6eFo4MzZUMDJROVRtTHZXdjFyZGlqV3dWVFZnUTlTWWJvbTZHRHRR +.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 diff --git a/yt_engine.py b/yt_engine.py index ad1ab5f..1011223 100644 --- a/yt_engine.py +++ b/yt_engine.py @@ -106,3 +106,46 @@ async def process_youtube_request(query: str): saat menunggu proses download selesai. """ return await asyncio.to_thread(search_and_download, query) + +# ========================================== +# FITUR TAMBAHAN: YOUTUBE MIX RECOMMENDATION +# ========================================== +def fetch_recommendations_sync(video_id: str, limit: int = 3): + """ + Mengambil rekomendasi lagu dari YouTube Mix algoritma (Vibes/Genre serupa) + """ + # Gunakan settingan ringan karena kita cuma mau ambil teks judul, BUKAN download + ydl_opts_recs = { + 'quiet': True, + 'extract_flat': True, + 'playlistend': limit + 1, # +1 karena urutan pertama biasanya lagu aslinya + 'extractor_args': {'youtube': ['player_client=android,ios']}, + } + + # Sisipkan cookie jika ada + if os.path.exists("cookies.txt"): + ydl_opts_recs['cookiefile'] = 'cookies.txt' + + try: + with yt_dlp.YoutubeDL(ydl_opts_recs) as ydl: + # RD = Playlist YouTube Mix khusus untuk video tersebut + mix_url = f"https://www.youtube.com/watch?v={video_id}&list=RD{video_id}" + info = ydl.extract_info(mix_url, download=False) + + recs = [] + if 'entries' in info: + # Loop dan ambil lagunya (Lewati index 0 karena itu lagu yg sedang diputar) + for entry in info['entries'][1:]: + if entry and entry.get('id') and entry.get('title'): + recs.append({ + 'id': entry['id'], + 'title': entry['title'] + }) + return recs + except Exception as e: + print(f"Gagal mengambil rekomendasi: {e}") + return [] + +async def get_recommendations(video_id: str): + """Bungkus Async agar bot tidak hang""" + return await asyncio.to_thread(fetch_recommendations_sync, video_id)