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

167
bot.py
View File

@@ -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 <judul/link>`\n\n"
"Contoh: `/play Dewa 19 Kangen`"
)
await message.answer("🎧 **Kantor-Bypass Music Bot**\nKirim format: `/play <judul>`")
@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)

View File

@@ -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

View File

@@ -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)