Compare commits

...

2 Commits

Author SHA1 Message Date
YOLANDO
b5b6135cc3 final 1 2026-03-25 12:37:26 +07:00
YOLANDO
f962f16308 v2 2026-03-25 11:44:55 +07:00
8 changed files with 151 additions and 90 deletions

182
bot.py
View File

@@ -1,78 +1,98 @@
import asyncio import asyncio
import os import os
import logging import logging
from aiogram import Bot, Dispatcher, types, BaseMiddleware import time
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 from yt_engine import process_youtube_request, get_recommendations
from yt_engine import process_youtube_request
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 --- session = AiohttpSession(timeout=300)
# Timeout diatur 300 detik (5 menit) agar Telegram tidak memutus koneksi saat mengirim file MP3 yang besar # Matikan parse_mode default agar aman dari error karakter aneh di judul lagu YouTube
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 ---
# Antrean sederhana (FIFO)
music_queue = asyncio.Queue()
async def queue_worker():
"""Memproses lagu satu per satu dari antrean agar server tidak meledak"""
while True:
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()
# Satpam Bot
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) if event.from_user.id != AUTHORIZED_USER_ID: return
if event.from_user.id != AUTHORIZED_USER_ID:
logger.warning(f"Akses ditolak untuk User ID: {event.from_user.id}")
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 --- # --- COMMANDS ---
@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("🎧 **Music Bot Ready!**\nKirim format: `/play <judul lagu>`\nSetelah lagu terkirim, klik rekomendasi di bawahnya untuk melanjutkan.", parse_mode="Markdown")
"🎧 **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("⏳ Memproses pesananmu...")
status_msg = await message.answer("🔍 *Mencari dan memproses...*") await music_queue.put((query, message.chat.id, status_msg.message_id))
# --- HANDLER TOMBOL SUGGESTION ---
@dp.callback_query(F.data.startswith("play_"))
async def handle_suggestion_click(callback: CallbackQuery):
video_id = callback.data.split("_", 1)[1]
# Gunakan asyncio.create_task agar tidak nge-block queue bot (bisa antre banyak lagu) # 1. Hapus tombol di pesan sebelumnya biar chat nggak penuh tombol mati
asyncio.create_task(process_music_request(query, message.chat.id, status_msg.message_id)) 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}"
# 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: PROCESS MUSIC --- # --- 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
total_start_time = time.time() # ⏱️ START TIMER TOTAL
try: try:
# 1. Download dari YouTube 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) 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": if yt_result["status"] == "error":
return await bot.edit_message_text(f"❌ Gagal: {yt_result['message']}", chat_id, status_msg_id) return await bot.edit_message_text(f"❌ Gagal: {yt_result['message']}", chat_id, status_msg_id)
@@ -82,83 +102,81 @@ 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 # --- FASE 2: MINIO S3 ---
t_s3_start = time.time()
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 (MinIO)...", 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)
logger.info(f"⏱️ [FASE 2] Download dari MinIO memakan waktu: {time.time() - t_s3_start:.2f} detik")
else: 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)
# 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)
logger.info(f"⏱️ [FASE 2] Upload ke MinIO memakan waktu: {time.time() - t_s3_start:.2f} detik")
# 3. Kirim Audio ke Telegram # --- FASE 3: TELEGRAM UPLOAD ---
await bot.edit_message_text("📤 *Mengirim audio ke Telegram... (Mungkin butuh waktu)*", chat_id, status_msg_id) await bot.edit_message_text("📤 Mengirim file ke Telegram...", chat_id, status_msg_id)
if local_file and os.path.exists(local_file): if local_file and os.path.exists(local_file):
t_tele_start = time.time()
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)
t_tele_end = time.time()
logger.info(f"⏱️ [FASE 3] Upload ke Telegram memakan waktu: {t_tele_end - t_tele_start:.2f} detik")
# Opsi: Tombol pencarian selanjutnya if status_msg_id:
kb = [[InlineKeyboardButton(text="🎵 Putar Lagu Acak Lainnya", switch_inline_query_current_chat="")]] await bot.delete_message(chat_id, status_msg_id)
reply_markup = InlineKeyboardMarkup(inline_keyboard=kb)
# --- 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 = []
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=chat_id,
text=f"💡 **Rekomendasi dari:**\n_{title}_",
reply_markup=reply_markup,
parse_mode="Markdown"
)
# Ekstra Pengaman Timeout di Request Level logger.info(f"✅ TOTAL WAKTU E2E: {time.time() - total_start_time:.2f} detik\n{'-'*40}")
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)
else:
await bot.edit_message_text("❌ File audio tidak ditemukan di server lokal.", chat_id, status_msg_id)
except Exception as e: except Exception as e:
logger.error(f"Error processing music: {e}") logger.error(f"Error: {e}")
try: try:
await bot.edit_message_text(f"❌ Terjadi kesalahan internal: {e}", chat_id, status_msg_id) await bot.edit_message_text("❌ Terjadi kesalahan internal saat memproses lagu.", chat_id, status_msg_id)
except: except: pass
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 CACHE ---
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 RUNNER ---
async def main(): async def main():
# Konek ke database
await db.connect() await db.connect()
asyncio.create_task(queue_worker())
# Jalankan Scheduler untuk bersih-bersih tiap jam 3 pagi
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!")
# Hapus webhook lama (jika ada) dan mulai polling logger.info("🚀 Bot Music - Mode Interactive Playlist Berjalan!")
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 1805951136 __Secure-1PSIDCC AKEyXzUIxLQmOaltTf1fGcQk8YPiD-LeYgDwpvoYnXyN2XpMqdyv8dRk8RMnPnEuNpcNaJpiuA
.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 1837487124 __Secure-3PAPISID CABFFJNnCe9Ocqz0/ANX4rXmqpbZzZy8y1
.youtube.com TRUE / TRUE 1837483738 __Secure-3PSID g.a0008AhqPWcg3-Syf1o_JEl3LCHzWFKn_n3Y8jgh3RnwDW83PtpvC14W2htdab0jQBsnf1YkqgACgYKAdcSARESFQHGX2MiTK5ZxIBltpFTHCsEt_xiuxoVAUF8yKrG5FgxNyKwsIayRODxp8Fo0076 .youtube.com TRUE / TRUE 1837487124 __Secure-3PSID g.a0008AhqPWcg3-Syf1o_JEl3LCHzWFKn_n3Y8jgh3RnwDW83PtpvC14W2htdab0jQBsnf1YkqgACgYKAdcSARESFQHGX2MiTK5ZxIBltpFTHCsEt_xiuxoVAUF8yKrG5FgxNyKwsIayRODxp8Fo0076
.youtube.com TRUE / TRUE 1805947740 __Secure-3PSIDCC AKEyXzVld0GspLJe8l6JS4cfa4h4ElnWgBoAB5kiHdSs0gCerB-quDuuc4JEn63Voja8hi9ZSQ .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-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 1xjiT5Oikbs
.youtube.com TRUE / TRUE 1789963740 VISITOR_INFO1_LIVE BIF7sNwBpLI .youtube.com TRUE / TRUE 1837487125 LOGIN_INFO AFmmF2swRQIhAL69BVhiHN7pI8NRR4HKWf4y1nB2NymBs8b4SapeMuYBAiBXnlXmRsm7fl_vzwvgkCRSs-81PNyApm33E11JW8RoJA:QUQ3MjNmejFnV0xvdHdQeXktQUJoY1pZZ3lDT1N4U1ZMUUNYVmdDNG02YzZTSjM2ejltODhFWFBjTXduNXNyamQ5OEZRRzU3RzVpWTJVM051T3pwbHEzZkZTcjJtS1RTcXFoMVpDLWRiR1g0YVN6VnpBQTFUOUxhSTZiMUlMdzB4bWtjaVV2dklrcW05TlVKOE0tcGpRVnRrSE9rX1czUE9B
.youtube.com TRUE / TRUE 1789963740 VISITOR_PRIVACY_METADATA CgJJRBIEGgAgFA%3D%3D .youtube.com TRUE / TRUE 1789967136 VISITOR_INFO1_LIVE 68v_ALXnekk
.youtube.com TRUE / TRUE 1837483740 LOGIN_INFO AFmmF2swRgIhAMFCn3UGB5h-2Wf8Rnv4LIWp0LiVwf6ibiPVQhmFufMQAiEA-q7C22E801epMI4Lk54Xb9uNUHWVKkjNLHJ-auj0p7c:QUQ3MjNmelp0RkxSamFrTTZnOVBLelN1ZjVpeVRBWC1TTUc1d0NPUm9PXzNPQmx0dUNEdjBDOEF0cGsxc3p1SWhwMWtIZDdyMjRxYVFKZWRIWnkwMkxzLWVnakVCdFlxcVpFclc3N3VfNmpIWWh2aUd2eUxydzJMekZ6eFo4MzZUMDJROVRtTHZXdjFyZGlqV3dWVFZnUTlTWWJvbTZHRHRR .youtube.com TRUE / TRUE 1789967136 VISITOR_PRIVACY_METADATA CgJJRBIEGgAgIA%3D%3D

BIN
downloads/7SqNVv98e8Q.mp3 Normal file

Binary file not shown.

BIN
downloads/grp6FCnioMM.mp3 Normal file

Binary file not shown.

BIN
downloads/gvunApwKIiY.mp3 Normal file

Binary file not shown.

BIN
downloads/gvunApwKIiY.webm Normal file

Binary file not shown.

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 = 5):
"""
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)