fix final

This commit is contained in:
YOLANDO
2026-03-25 16:05:26 +07:00
parent b5b6135cc3
commit c710a9bdd5
5 changed files with 105 additions and 174 deletions

2
.env
View File

@@ -15,7 +15,7 @@ DB_NAME=default_db
# --- MINIO S3 --- # --- MINIO S3 ---
# Catatan: Pastikan port 9000 (API MinIO) bisa diakses, bukan cuma port 9001 (Console) # Catatan: Pastikan port 9000 (API MinIO) bisa diakses, bukan cuma port 9001 (Console)
# Endpoint tidak perlu memakai awalan https:// atau http:// # Endpoint tidak perlu memakai awalan https:// atau http://
MINIO_ENDPOINT=localhost:9000 MINIO_ENDPOINT=127.0.0.1:9000
MINIO_ACCESS_KEY=admin_ando MINIO_ACCESS_KEY=admin_ando
MINIO_SECRET_KEY=PasswordSuperKuat123! MINIO_SECRET_KEY=PasswordSuperKuat123!
# SECURE diset ke True karena lalu lintasnya dilewatkan melalui NPM yang menggunakan HTTPS (Let's Encrypt). # SECURE diset ke True karena lalu lintasnya dilewatkan melalui NPM yang menggunakan HTTPS (Let's Encrypt).

111
bot.py
View File

@@ -1,7 +1,8 @@
import asyncio import asyncio
import os import os
import logging
import time import time
import logging
import re
from aiogram import Bot, Dispatcher, types, BaseMiddleware, F 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, CallbackQuery from aiogram.types import FSInputFile, InlineKeyboardMarkup, InlineKeyboardButton, CallbackQuery
@@ -22,15 +23,12 @@ BOT_TOKEN = os.getenv("BOT_TOKEN")
AUTHORIZED_USER_ID = int(os.getenv("AUTHORIZED_USER_ID", 0)) AUTHORIZED_USER_ID = int(os.getenv("AUTHORIZED_USER_ID", 0))
session = AiohttpSession(timeout=300) session = AiohttpSession(timeout=300)
# Matikan parse_mode default agar aman dari error karakter aneh di judul lagu YouTube
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()
# Antrean sederhana (FIFO)
music_queue = asyncio.Queue() music_queue = asyncio.Queue()
async def queue_worker(): async def queue_worker():
"""Memproses lagu satu per satu dari antrean agar server tidak meledak"""
while True: while True:
query, chat_id, msg_id = await music_queue.get() query, chat_id, msg_id = await music_queue.get()
try: try:
@@ -40,7 +38,6 @@ async def queue_worker():
finally: finally:
music_queue.task_done() music_queue.task_done()
# Satpam Bot
class SecurityMiddleware(BaseMiddleware): class SecurityMiddleware(BaseMiddleware):
async def __call__(self, handler, event: types.Message | CallbackQuery, data: dict): async def __call__(self, handler, event: types.Message | CallbackQuery, data: dict):
if event.from_user.id != AUTHORIZED_USER_ID: return if event.from_user.id != AUTHORIZED_USER_ID: return
@@ -49,50 +46,63 @@ class SecurityMiddleware(BaseMiddleware):
dp.message.middleware(SecurityMiddleware()) dp.message.middleware(SecurityMiddleware())
dp.callback_query.middleware(SecurityMiddleware()) dp.callback_query.middleware(SecurityMiddleware())
# --- COMMANDS --- # 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
@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("🎧 **Music Bot Ready!**\nKirim format: `/play <judul lagu>`\nSetelah lagu terkirim, klik rekomendasi di bawahnya untuk melanjutkan.", parse_mode="Markdown") await message.answer("🎧 **Music Bot Ready (M4A Turbo)!**\nKirim format: `/play <judul lagu>`\nSetelah lagu terkirim, klik rekomendasi di bawahnya.", parse_mode="Markdown")
@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.")
return await message.answer("⚠️ Masukkan judul lagu. Contoh: /play Dewa 19 Kangen")
status_msg = await message.answer("⏳ Memproses pesananmu...") status_msg = await message.answer("⏳ Memproses pesananmu...")
await music_queue.put((query, message.chat.id, status_msg.message_id)) await music_queue.put((query, message.chat.id, status_msg.message_id))
# --- HANDLER TOMBOL SUGGESTION ---
@dp.callback_query(F.data.startswith("play_")) @dp.callback_query(F.data.startswith("play_"))
async def handle_suggestion_click(callback: CallbackQuery): async def handle_suggestion_click(callback: CallbackQuery):
video_id = callback.data.split("_", 1)[1] video_id = callback.data.split("_", 1)[1]
# 1. Hapus tombol di pesan sebelumnya biar chat nggak penuh tombol mati
await callback.message.edit_reply_markup(reply_markup=None) await callback.message.edit_reply_markup(reply_markup=None)
status_msg = await bot.send_message(callback.message.chat.id, "⏳ Memproses lagu pilihanmu...")
# 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}" 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 music_queue.put((query_url, callback.message.chat.id, status_msg.message_id))
await callback.answer("Siapp! Diproses...") await callback.answer("Siapp! Diproses...")
# --- CORE LOGIC --- # --- CORE LOGIC (DENGAN SMART CACHE) ---
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 local_file = None
total_start_time = time.time() # ⏱️ START TIMER TOTAL total_start_time = time.time()
try: try:
logger.info(f"▶️ MEMULAI PROSES LAGU: {query}") logger.info(f"▶️ MEMULAI PROSES: {query}")
await bot.edit_message_text("🔍 Mencari ke YouTube...", chat_id, status_msg_id)
# --- FASE 1: YOUTUBE & FFMPEG --- # 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
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 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() 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"⏱️ yt-dlp (Tanpa FFmpeg) memakan waktu: {time.time() - t_yt_start:.2f} detik")
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)
@@ -100,83 +110,60 @@ async def process_music_request(query: str, chat_id: int, status_msg_id: int):
video_id = yt_result["video_id"] video_id = yt_result["video_id"]
title = yt_result["title"] title = yt_result["title"]
local_file = yt_result["file_path"] local_file = yt_result["file_path"]
s3_object_name = f"{video_id}.mp3"
# --- FASE 2: MINIO S3 --- # Simpan sesuai ekstensi aslinya (sekarang biasanya m4a)
t_s3_start = time.time() s3_object_name = f"{video_id}.{yt_result['ext']}"
cached_data = await db.get_cache(video_id)
if cached_data:
await bot.edit_message_text("⚡ Mengambil dari Cache (MinIO)...", chat_id, status_msg_id)
local_file = f"downloads/cache_{video_id}.mp3"
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:
await bot.edit_message_text("☁️ Mengunggah ke MinIO Server...", chat_id, status_msg_id) await bot.edit_message_text("☁️ Mengunggah ke MinIO Server...", chat_id, status_msg_id)
if local_file and os.path.exists(local_file): if local_file and os.path.exists(local_file):
t_s3_up = time.time()
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") logger.info(f"⏱️ Upload ke MinIO memakan waktu: {time.time() - t_s3_up:.2f} detik")
# --- FASE 3: TELEGRAM UPLOAD --- # 3. KIRIM KE TELEGRAM
await bot.edit_message_text("📤 Mengirim file ke Telegram...", 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() 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) 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"⏱️ Upload ke Telegram memakan waktu: {time.time() - t_tele_start:.2f} detik")
logger.info(f"⏱️ [FASE 3] Upload ke Telegram memakan waktu: {t_tele_end - t_tele_start:.2f} detik")
if status_msg_id: if status_msg_id: await bot.delete_message(chat_id, status_msg_id)
await bot.delete_message(chat_id, status_msg_id)
# --- SUGGEST 5 LAGU --- # 4. SUGGEST 5 LAGU (Judul lebih panjang)
t_recs_start = time.time() t_recs = time.time()
recs = await get_recommendations(video_id) recs = await get_recommendations(video_id)
logger.info(f"⏱️ [FASE 4] Fetch Rekomendasi memakan waktu: {time.time() - t_recs_start:.2f} detik") logger.info(f"⏱️ Fetch Rekomendasi memakan waktu: {time.time() - t_recs:.2f} detik")
if recs: if recs:
kb = [] kb = []
for rec in recs[:5]: for rec in recs[:5]:
short_title = rec['title'][:60] + "..." if len(rec['title']) > 60 else rec['title'] 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']}")]) kb.append([InlineKeyboardButton(text=f"▶️ {short_title}", callback_data=f"play_{rec['id']}")])
reply_markup = InlineKeyboardMarkup(inline_keyboard=kb) reply_markup = InlineKeyboardMarkup(inline_keyboard=kb)
await bot.send_message( await bot.send_message(chat_id, text=f"💡 **Rekomendasi dari:**\n_{title}_", reply_markup=reply_markup, parse_mode="Markdown")
chat_id=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}") logger.info(f"✅ TOTAL WAKTU E2E: {time.time() - total_start_time:.2f} detik\n{'-'*40}")
except Exception as e: except Exception as e:
logger.error(f"Error: {e}") logger.error(f"Error: {e}")
try:
await bot.edit_message_text("❌ Terjadi kesalahan internal saat memproses lagu.", chat_id, status_msg_id)
except: pass
finally: finally:
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)
# --- AUTO CLEANUP CACHE ---
async def cleanup_expired_cache(): async def cleanup_expired_cache():
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:
await delete_audio(item['s3_object_key']) await delete_audio(item['s3_object_key'])
await db.delete_cache(item['youtube_id']) await db.delete_cache(item['youtube_id'])
# --- MAIN RUNNER ---
async def main(): async def main():
await db.connect() await db.connect()
asyncio.create_task(queue_worker()) 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 Turbo Berjalan!")
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 1805951136 __Secure-1PSIDCC AKEyXzUIxLQmOaltTf1fGcQk8YPiD-LeYgDwpvoYnXyN2XpMqdyv8dRk8RMnPnEuNpcNaJpiuA .youtube.com TRUE / TRUE 1805960813 __Secure-1PSIDCC AKEyXzVBNsZGFVBGm7Jq43bWKEYSJcOyy52A3OIRQCnexDS9NpOnNSWjTeQiJ7OCtCEBXCHB0Q
.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 1837487124 __Secure-3PAPISID CABFFJNnCe9Ocqz0/ANX4rXmqpbZzZy8y1 .youtube.com TRUE / TRUE 1837496799 __Secure-3PAPISID CABFFJNnCe9Ocqz0/ANX4rXmqpbZzZy8y1
.youtube.com TRUE / TRUE 1837487124 __Secure-3PSID g.a0008AhqPWcg3-Syf1o_JEl3LCHzWFKn_n3Y8jgh3RnwDW83PtpvC14W2htdab0jQBsnf1YkqgACgYKAdcSARESFQHGX2MiTK5ZxIBltpFTHCsEt_xiuxoVAUF8yKrG5FgxNyKwsIayRODxp8Fo0076 .youtube.com TRUE / TRUE 1837496799 __Secure-3PSID g.a0008AhqPWcg3-Syf1o_JEl3LCHzWFKn_n3Y8jgh3RnwDW83PtpvC14W2htdab0jQBsnf1YkqgACgYKAdcSARESFQHGX2MiTK5ZxIBltpFTHCsEt_xiuxoVAUF8yKrG5FgxNyKwsIayRODxp8Fo0076
.youtube.com TRUE / TRUE 1805951136 __Secure-3PSIDCC AKEyXzUEL9zBsIFlxJP8kImHFhOK8FA5ZzEE4j6_lcjzKUrODAt1IS4GDcyMK2tbSJFdpBMnaA .youtube.com TRUE / TRUE 1805960813 __Secure-3PSIDCC AKEyXzXHve8Yb8sdfRSUlbjoV_c57YcVrr5v0212bsaovAxJM9FYPiprF0MaES5UJnphVf4KDw
.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 1xjiT5Oikbs .youtube.com TRUE / TRUE 0 YSC QnlEbYOnrII
.youtube.com TRUE / TRUE 1837487125 LOGIN_INFO AFmmF2swRQIhAL69BVhiHN7pI8NRR4HKWf4y1nB2NymBs8b4SapeMuYBAiBXnlXmRsm7fl_vzwvgkCRSs-81PNyApm33E11JW8RoJA:QUQ3MjNmejFnV0xvdHdQeXktQUJoY1pZZ3lDT1N4U1ZMUUNYVmdDNG02YzZTSjM2ejltODhFWFBjTXduNXNyamQ5OEZRRzU3RzVpWTJVM051T3pwbHEzZkZTcjJtS1RTcXFoMVpDLWRiR1g0YVN6VnpBQTFUOUxhSTZiMUlMdzB4bWtjaVV2dklrcW05TlVKOE0tcGpRVnRrSE9rX1czUE9B .youtube.com TRUE / TRUE 1837496799 LOGIN_INFO AFmmF2swRQIgWQjJwQq0c2xX8SWB-0s3rNzcdakVHkJYAd2HJG1eL0QCIQCoMq32m3ixCgIhqGAZtCDBn5s8WqeRzU5TU9vp4GwH5w:QUQ3MjNmeTRWMmwxaWp1Um90ZWdfWnVVcHVZbEM0UWZWaHhnVU9YX1EtME5lcnVYZGNMUGxQeDhwV3VfWU5wZUpHUzdkWHB0Vnp5dHEzMDdQTGxWZXJmS0Z0NUlqWXV3bkFwZlRuMGRNQ2FBMTNoNElzVmpJUXZRVFNVcjhGOWI1X0JrSkFsTkxtWkgtS1Foa1NBX0tjSllhbHRzR1JfZERn
.youtube.com TRUE / TRUE 1789967136 VISITOR_INFO1_LIVE 68v_ALXnekk .youtube.com TRUE / TRUE 1789976813 VISITOR_INFO1_LIVE DuAD9wwYOM0
.youtube.com TRUE / TRUE 1789967136 VISITOR_PRIVACY_METADATA CgJJRBIEGgAgIA%3D%3D .youtube.com TRUE / TRUE 1789976813 VISITOR_PRIVACY_METADATA CgJJRBIEGgAgYQ%3D%3D

View File

@@ -2,150 +2,94 @@ import yt_dlp
import os import os
import asyncio import asyncio
# Buat folder penampungan sementara jika belum ada
DOWNLOAD_DIR = "downloads" DOWNLOAD_DIR = "downloads"
os.makedirs(DOWNLOAD_DIR, exist_ok=True) os.makedirs(DOWNLOAD_DIR, exist_ok=True)
def generate_netscape_cookies(): def generate_netscape_cookies():
"""
Mengubah data tabel cookie (copy-paste dari tab Application browser)
menjadi format baku Netscape HTTP Cookie agar bisa dibaca yt-dlp.
"""
if not os.path.exists("raw_cookie.txt"): if not os.path.exists("raw_cookie.txt"):
print("⚠️ raw_cookie.txt tidak ditemukan. Lanjut tanpa cookies.")
return False return False
try: try:
with open("raw_cookie.txt", "r", encoding="utf-8") as f: with open("raw_cookie.txt", "r", encoding="utf-8") as f:
lines = f.readlines() lines = f.readlines()
with open("cookies.txt", "w", encoding="utf-8") as f: with open("cookies.txt", "w", encoding="utf-8") as f:
f.write("# Netscape HTTP Cookie File\n") f.write("# Netscape HTTP Cookie File\n")
f.write("# This is a generated file! Do not edit.\n\n") f.write("# This is a generated file! Do not edit.\n\n")
for line in lines: for line in lines:
# Pisahkan kolom berdasarkan tombol Tab (\t) bawaan copy-paste tabel
parts = line.split('\t') parts = line.split('\t')
# Pastikan baris tersebut valid (minimal punya Nama dan Value)
if len(parts) >= 2: if len(parts) >= 2:
name = parts[0].strip() name, value = parts[0].strip(), parts[1].strip()
value = parts[1].strip() if name.lower() == "name" or not name: continue
# Abaikan jika yang ter-copy adalah Header tabelnya ("Name", "Value")
if name.lower() == "name" or not name:
continue
# Format baku Netscape: domain, subdomains, path, secure, expiration, name, value
# Kita paksa domainnya .youtube.com agar yt-dlp bisa membacanya
f.write(f".youtube.com\tTRUE\t/\tTRUE\t2147483647\t{name}\t{value}\n") f.write(f".youtube.com\tTRUE\t/\tTRUE\t2147483647\t{name}\t{value}\n")
print("✅ Cookies berhasil di-generate dari raw_cookie.txt")
return True return True
except Exception as e: except Exception as e:
print(f"Error saat memproses cookies: {e}") print(f"Error parsing cookies: {e}")
return False return False
def fetch_recommendations_sync(video_id: str, limit: int = 5):
ydl_opts_recs = {
'quiet': True,
'extract_flat': True,
'playlistend': limit + 2,
'extractor_args': {'youtube': ['player_client=android,ios']},
}
if os.path.exists("cookies.txt"):
ydl_opts_recs['cookiefile'] = 'cookies.txt'
try:
with yt_dlp.YoutubeDL(ydl_opts_recs) as ydl:
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:
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:
return []
async def get_recommendations(video_id: str):
return await asyncio.to_thread(fetch_recommendations_sync, video_id)
def search_and_download(query_or_url: str): def search_and_download(query_or_url: str):
"""
Fungsi utama untuk mencari dan mengunduh audio dari YouTube.
Berjalan secara sinkronus.
"""
# 1. Konversi cookie setiap kali mau download
has_cookies = generate_netscape_cookies() has_cookies = generate_netscape_cookies()
# 2. Pengaturan yt-dlp
ydl_opts = { ydl_opts = {
'format': 'bestaudio/best/m4a/mp3', # ❌ FFMPEG POSTPROCESSORS DIHAPUS ❌
# Langsung tembak format asli m4a (AAC) agar CPU tidak tersiksa
'format': 'bestaudio[ext=m4a]/bestaudio/best',
'outtmpl': f'{DOWNLOAD_DIR}/%(id)s.%(ext)s', 'outtmpl': f'{DOWNLOAD_DIR}/%(id)s.%(ext)s',
'max_filesize': 45000000, # Batas aman Telegram 45MB 'max_filesize': 45000000,
'postprocessors': [{
'key': 'FFmpegExtractAudio',
'preferredcodec': 'mp3',
'preferredquality': '128',
}],
'quiet': True, 'quiet': True,
'noplaylist': True, 'noplaylist': True,
'default_search': 'ytsearch1', 'default_search': 'ytsearch1',
'extractor_args': {'youtube': ['player_client=android,ios']},
'js_runtimes': {'node': {}}, 'js_runtimes': {'node': {}},
# 'extractor_args': {'youtube': ['player_client=android,ios']},
} }
# 3. Sisipkan cookie jika berhasil digenerate
if has_cookies and os.path.exists("cookies.txt"): if has_cookies and os.path.exists("cookies.txt"):
ydl_opts['cookiefile'] = 'cookies.txt' ydl_opts['cookiefile'] = 'cookies.txt'
try: try:
with yt_dlp.YoutubeDL(ydl_opts) as ydl: with yt_dlp.YoutubeDL(ydl_opts) as ydl:
# Mulai proses ekstraksi & download
info = ydl.extract_info(query_or_url, download=True) info = ydl.extract_info(query_or_url, download=True)
# Jika hasil berupa playlist/pencarian, ambil item pertama
if 'entries' in info: if 'entries' in info:
info = info['entries'][0] info = info['entries'][0]
video_id = info.get('id') video_id = info.get('id')
title = info.get('title') title = info.get('title')
duration = info.get('duration') # Ambil ekstensi aslinya (kemungkinan besar m4a)
ext = info.get('ext', 'm4a')
file_path = f"{DOWNLOAD_DIR}/{video_id}.mp3" file_path = f"{DOWNLOAD_DIR}/{video_id}.{ext}"
return { return {
"status": "success", "status": "success",
"video_id": video_id, "video_id": video_id,
"title": title, "title": title,
"duration": duration, "file_path": file_path if os.path.exists(file_path) else None,
"file_path": file_path if os.path.exists(file_path) else None "ext": ext
} }
except Exception as e: except Exception as e:
return {"status": "error", "message": str(e)} return {"status": "error", "message": str(e)}
async def process_youtube_request(query: str): async def process_youtube_request(query: str):
"""
Bungkus (Wrapper) Async agar bot Telegram tidak nge-hang
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)