This commit is contained in:
YOLANDO
2026-03-25 11:44:55 +07:00
parent 4ec369c18e
commit f962f16308
4 changed files with 136 additions and 90 deletions

163
bot.py
View File

@@ -1,77 +1,98 @@
import asyncio import asyncio
import os import os
import time
import logging import logging
from aiogram import Bot, Dispatcher, types, BaseMiddleware from aiogram import Bot, Dispatcher, types, BaseMiddleware, F
from aiogram.filters import Command 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.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 dotenv import load_dotenv
from apscheduler.schedulers.asyncio import AsyncIOScheduler from apscheduler.schedulers.asyncio import AsyncIOScheduler
# Import modul lokal yang sudah kita buat # Import modul lokal
from yt_engine import process_youtube_request from yt_engine import process_youtube_request, get_recommendations
from db_manager import db from db_manager import db
from s3_manager import upload_audio, download_audio, delete_audio from s3_manager import upload_audio, download_audio, delete_audio
# Setup Logging
logging.basicConfig(level=logging.INFO) logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
load_dotenv() load_dotenv()
BOT_TOKEN = os.getenv("BOT_TOKEN") 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) session = AiohttpSession(timeout=300)
bot = Bot(token=BOT_TOKEN, session=session, default=DefaultBotProperties(parse_mode=None))
bot = Bot(
token=BOT_TOKEN,
session=session,
default=DefaultBotProperties(parse_mode=None)
)
dp = Dispatcher() 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): class SecurityMiddleware(BaseMiddleware):
async def __call__(self, handler, event: types.Message, data: dict): async def __call__(self, handler, event: types.Message | CallbackQuery, data: dict):
# Memblokir semua orang kecuali Kamu (Single-User Mode) user_id = event.from_user.id
if event.from_user.id != AUTHORIZED_USER_ID: if user_id != AUTHORIZED_USER_ID:
logger.warning(f"Akses ditolak untuk User ID: {event.from_user.id}") return
return # Abaikan pesan
return await handler(event, data) return await handler(event, data)
# Daftarkan Satpam ke sistem
dp.message.middleware(SecurityMiddleware()) dp.message.middleware(SecurityMiddleware())
dp.callback_query.middleware(SecurityMiddleware())
# --- COMMAND HANDLERS --- # --- COMMAND HANDLERS ---
@dp.message(Command("start")) @dp.message(Command("start"))
async def cmd_start(message: types.Message): async def cmd_start(message: types.Message):
await message.answer( await message.answer("🎧 **Kantor-Bypass Music Bot**\nKirim format: `/play <judul>`")
"🎧 **Kantor-Bypass Music Bot Ready!**\n\n"
"Kirim judul lagu atau link YouTube dengan format:\n"
"`/play <judul/link>`\n\n"
"Contoh: `/play Dewa 19 Kangen`"
)
@dp.message(Command("play")) @dp.message(Command("play"))
async def cmd_play(message: types.Message): async def cmd_play(message: types.Message):
query = message.text.replace("/play", "").strip() query = message.text.replace("/play", "").strip()
if not query: 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("⏳ *Dimasukkan ke antrean reguler...*")
status_msg = await message.answer("🔍 *Mencari dan memproses...*") # Priority 1 = Reguler (Add to Queue)
await music_queue.put((1, time.time(), query, message.chat.id, status_msg.message_id))
# Gunakan asyncio.create_task agar tidak nge-block queue bot (bisa antre banyak lagu) # --- CALLBACK HANDLERS (TOMBOL INTERAKTIF) ---
asyncio.create_task(process_music_request(query, message.chat.id, status_msg.message_id)) @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)
# --- CORE LOGIC: PROCESS MUSIC --- # 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 ---
async def process_music_request(query: str, chat_id: int, status_msg_id: int): 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: 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) yt_result = await process_youtube_request(query)
if yt_result["status"] == "error": 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"] local_file = yt_result["file_path"]
s3_object_name = f"{video_id}.mp3" s3_object_name = f"{video_id}.mp3"
# 2. Cek Cache di Database
cached_data = await db.get_cache(video_id) cached_data = await db.get_cache(video_id)
if cached_data: 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 S3...", chat_id, status_msg_id)
# Download dari S3 ke local temporary
local_file = f"downloads/cache_{video_id}.mp3" local_file = f"downloads/cache_{video_id}.mp3"
await download_audio(s3_object_name, local_file) await download_audio(s3_object_name, local_file)
else: else:
await bot.edit_message_text("☁️ *Mengunggah ke S3 Storage...*", chat_id, status_msg_id) await bot.edit_message_text("☁️ Mengunggah ke S3 Storage...", chat_id, status_msg_id)
# Upload file baru ke S3
if local_file and os.path.exists(local_file): if local_file and os.path.exists(local_file):
await upload_audio(local_file, s3_object_name) await upload_audio(local_file, s3_object_name)
await db.save_cache(video_id, title, 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...", chat_id, status_msg_id)
await bot.edit_message_text("📤 *Mengirim audio ke Telegram... (Mungkin butuh waktu)*", chat_id, status_msg_id)
if local_file and os.path.exists(local_file): if local_file and os.path.exists(local_file):
audio = FSInputFile(local_file) audio = FSInputFile(local_file)
await bot.send_audio(chat_id=chat_id, audio=audio, title=title, performer="Music Bot", request_timeout=300)
# 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.delete_message(chat_id, status_msg_id) 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: except Exception as e:
logger.error(f"Error processing music: {e}") logger.error(f"Error: {e}")
try:
await bot.edit_message_text(f"❌ Terjadi kesalahan internal: {e}", chat_id, status_msg_id)
except:
pass # Abaikan jika pesan sudah terhapus
finally: finally:
# 4. Hapus file temporary di VPS (Hemat Disk!)
if local_file and os.path.exists(local_file): if local_file and os.path.exists(local_file):
os.remove(local_file) os.remove(local_file)
# --- BACKGROUND JOB: AUTO CLEANUP (7 HARI) --- # --- AUTO CLEANUP ---
async def cleanup_expired_cache(): async def cleanup_expired_cache():
logger.info("🧹 Menjalankan tugas pembersihan cache otomatis...")
expired_items = await db.get_expired_cache(days=7) expired_items = await db.get_expired_cache(days=7)
for item in expired_items: for item in expired_items:
# Hapus dari S3
await delete_audio(item['s3_object_key']) await delete_audio(item['s3_object_key'])
# Hapus dari Database
await db.delete_cache(item['youtube_id']) await db.delete_cache(item['youtube_id'])
logger.info(f"🗑️ Dihapus: {item['title']} (Usia > 7 Hari)")
# --- MAIN LOOP --- # --- MAIN LOOP ---
async def main(): async def main():
# Konek ke database
await db.connect() 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 = AsyncIOScheduler()
scheduler.add_job(cleanup_expired_cache, 'cron', hour=3, minute=0) scheduler.add_job(cleanup_expired_cache, 'cron', hour=3, minute=0)
scheduler.start() scheduler.start()
logger.info("🚀 Bot Music Started!") logger.info("🚀 Bot Music Ready dengan Antrean & Rekomendasi!")
# Hapus webhook lama (jika ada) dan mulai polling
await bot.delete_webhook(drop_pending_updates=True) await bot.delete_webhook(drop_pending_updates=True)
await dp.start_polling(bot) await dp.start_polling(bot)

View File

@@ -3,12 +3,12 @@
.youtube.com TRUE / TRUE 2147483647 __Secure-1PAPISID CABFFJNnCe9Ocqz0/ANX4rXmqpbZzZy8y1 .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 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-1PSIDRTS sidts-CjcBWhotCb-4c6IuQXRvmRVYyon3cb4LfaOkQmhdlWz2FTjey8tLFTyGymAQr1fbHPACgLGOXKsXEAA
.youtube.com TRUE / TRUE 2147483647 __Secure-1PSIDTS sidts-CjUBWhotCXH5m0kFaAC1lhqf8kaotcZDxFxOty6XBxNsYxySai6J15Fi8BHklGtKOGZKSNYrLxAA .youtube.com TRUE / TRUE 2147483647 __Secure-1PSIDTS sidts-CjUBWhotCXH5m0kFaAC1lhqf8kaotcZDxFxOty6XBxNsYxySai6J15Fi8BHklGtKOGZKSNYrLxAA
.youtube.com TRUE / TRUE 1837483738 __Secure-3PAPISID CABFFJNnCe9Ocqz0/ANX4rXmqpbZzZy8y1 .youtube.com TRUE / TRUE 1837485634 __Secure-3PAPISID CABFFJNnCe9Ocqz0/ANX4rXmqpbZzZy8y1
.youtube.com TRUE / TRUE 1837483738 __Secure-3PSID g.a0008AhqPWcg3-Syf1o_JEl3LCHzWFKn_n3Y8jgh3RnwDW83PtpvC14W2htdab0jQBsnf1YkqgACgYKAdcSARESFQHGX2MiTK5ZxIBltpFTHCsEt_xiuxoVAUF8yKrG5FgxNyKwsIayRODxp8Fo0076 .youtube.com TRUE / TRUE 1837485634 __Secure-3PSID g.a0008AhqPWcg3-Syf1o_JEl3LCHzWFKn_n3Y8jgh3RnwDW83PtpvC14W2htdab0jQBsnf1YkqgACgYKAdcSARESFQHGX2MiTK5ZxIBltpFTHCsEt_xiuxoVAUF8yKrG5FgxNyKwsIayRODxp8Fo0076
.youtube.com TRUE / TRUE 1805947740 __Secure-3PSIDCC AKEyXzVld0GspLJe8l6JS4cfa4h4ElnWgBoAB5kiHdSs0gCerB-quDuuc4JEn63Voja8hi9ZSQ .youtube.com TRUE / TRUE 1805949648 __Secure-3PSIDCC AKEyXzU266eQwSCnNxn6ExAq3rRybQLzE4Z8R5O0uaTVV9E5h7LZ9FtpNU2MulJbDaDuM8Bhcw
.youtube.com TRUE / TRUE 2147483647 __Secure-3PSIDRTS sidts-CjcBWhotCb-4c6IuQXRvmRVYyon3cb4LfaOkQmhdlWz2FTjey8tLFTyGymAQr1fbHPACgLGOXKsXEAA .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-3PSIDTS sidts-CjUBWhotCXH5m0kFaAC1lhqf8kaotcZDxFxOty6XBxNsYxySai6J15Fi8BHklGtKOGZKSNYrLxAA
.youtube.com TRUE / TRUE 2147483647 __Secure-BUCKET CJsE .youtube.com TRUE / TRUE 2147483647 __Secure-BUCKET CJsE
@@ -19,7 +19,7 @@
.youtube.com TRUE / TRUE 2147483647 APISID PbWG-XrqTeJJFiPn/ACQ0hTzgL5lCMuKP5 .youtube.com TRUE / TRUE 2147483647 APISID PbWG-XrqTeJJFiPn/ACQ0hTzgL5lCMuKP5
.youtube.com TRUE / FALSE 0 PREF hl=en&tz=UTC .youtube.com TRUE / FALSE 0 PREF hl=en&tz=UTC
.youtube.com TRUE / TRUE 0 SOCS CAI .youtube.com TRUE / TRUE 0 SOCS CAI
.youtube.com TRUE / TRUE 0 YSC a2WtW-hP4Ck .youtube.com TRUE / TRUE 0 YSC P4gtt98KyR4
.youtube.com TRUE / TRUE 1789963740 VISITOR_INFO1_LIVE BIF7sNwBpLI .youtube.com TRUE / TRUE 1789965648 VISITOR_INFO1_LIVE simeqjbyhfg
.youtube.com TRUE / TRUE 1789963740 VISITOR_PRIVACY_METADATA CgJJRBIEGgAgFA%3D%3D .youtube.com TRUE / TRUE 1789965648 VISITOR_PRIVACY_METADATA CgJJRBIEGgAgbg%3D%3D
.youtube.com TRUE / TRUE 1837483740 LOGIN_INFO AFmmF2swRgIhAMFCn3UGB5h-2Wf8Rnv4LIWp0LiVwf6ibiPVQhmFufMQAiEA-q7C22E801epMI4Lk54Xb9uNUHWVKkjNLHJ-auj0p7c:QUQ3MjNmelp0RkxSamFrTTZnOVBLelN1ZjVpeVRBWC1TTUc1d0NPUm9PXzNPQmx0dUNEdjBDOEF0cGsxc3p1SWhwMWtIZDdyMjRxYVFKZWRIWnkwMkxzLWVnakVCdFlxcVpFclc3N3VfNmpIWWh2aUd2eUxydzJMekZ6eFo4MzZUMDJROVRtTHZXdjFyZGlqV3dWVFZnUTlTWWJvbTZHRHRR .youtube.com TRUE / TRUE 1837485634 LOGIN_INFO AFmmF2swRgIhALLG_pjzu2VHqpJ3nvTgO4BBFATaF3B4-ynZF1X3zIZ7AiEAxxUAkMU1uCiUa0ieIKye_J-fFQZUfYU0UWWYlfDdjMY:QUQ3MjNmeVhrOEFad0h6TkRFZEFwdl9FZU15ejZuT0VVV0cwNmgtSVljMGFQRmlxQkppa0pCQ1VaXzZXTEtYU2JaSF9sLS1uemN5YWUzNG9WZlA3Z1NlWENpSnNkSDdTRjJHY0hFZ2xuaUVBaXJTTXAzNDNxY2wtVFFFMzRJNmdXbXpDazZhVmt1dHJndGhfSDF3U181QlI5UDdvVmhxc1VB

View File

@@ -106,3 +106,46 @@ async def process_youtube_request(query: str):
saat menunggu proses download selesai. saat menunggu proses download selesai.
""" """
return await asyncio.to_thread(search_and_download, query) 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)