This commit is contained in:
YOLANDO
2026-03-26 09:05:08 +07:00
parent 2e2b10890c
commit 61d721b514
5 changed files with 110 additions and 66 deletions

3
.env
View File

@@ -1,6 +1,7 @@
# --- TELEGRAM BOT --- # --- TELEGRAM BOT ---
BOT_TOKEN=8747085830:AAHsOTdZbK40daE1Lxmjvw5CcKHY_A7sucI BOT_TOKEN=8747085830:AAHsOTdZbK40daE1Lxmjvw5CcKHY_A7sucI
AUTHORIZED_USER_IDS=951506682 ADMIN_ID=951506682
AUTHORIZED_USER_IDS=
# --- POSTGRESQL --- # --- POSTGRESQL ---
# Gunakan 'localhost' jika aplikasi jalan langsung di VPS (tanpa Docker), # Gunakan 'localhost' jika aplikasi jalan langsung di VPS (tanpa Docker),

161
bot.py
View File

@@ -11,7 +11,6 @@ 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 fungsi baru search_youtube_list
from yt_engine import process_youtube_request, get_recommendations, search_youtube_list from yt_engine import process_youtube_request, get_recommendations, search_youtube_list
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
@@ -22,17 +21,22 @@ logger = logging.getLogger(__name__)
load_dotenv() load_dotenv()
BOT_TOKEN = os.getenv("BOT_TOKEN") BOT_TOKEN = os.getenv("BOT_TOKEN")
# --- KONFIGURASI ADMIN & USERS ---
ADMIN_ID = int(os.getenv("ADMIN_ID", 0))
auth_users_str = os.getenv("AUTHORIZED_USER_IDS", "") auth_users_str = os.getenv("AUTHORIZED_USER_IDS", "")
AUTHORIZED_USER_IDS = {int(uid.strip()) for uid in auth_users_str.split(",") if uid.strip().isdigit()} AUTHORIZED_USER_IDS = {int(uid.strip()) for uid in auth_users_str.split(",") if uid.strip().isdigit()}
# Pastikan Admin selalu masuk whitelist
if ADMIN_ID != 0:
AUTHORIZED_USER_IDS.add(ADMIN_ID)
# Penyimpanan sementara user yang sedang menunggu persetujuan (agar admin tidak di-spam)
pending_users = set()
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()
music_queue = asyncio.Queue() music_queue = asyncio.Queue()
# --- PENYIMPANAN SEMENTARA UNTUK PAGING ---
# Format: { chat_id: {'query': 'judul', 'results': [list_lagu]} }
user_searches = {} user_searches = {}
async def queue_worker(): async def queue_worker():
@@ -45,26 +49,96 @@ async def queue_worker():
finally: finally:
music_queue.task_done() music_queue.task_done()
# --- SATPAM BOT DENGAN FITUR NOTIFIKASI ADMIN ---
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):
user_id = event.from_user.id user_id = event.from_user.id
if user_id not in AUTHORIZED_USER_IDS: if user_id not in AUTHORIZED_USER_IDS:
logger.warning(f"⚠️ Akses ditolak untuk User ID: {user_id}") # Jika user baru dan belum ada di antrean pending
if user_id not in pending_users and ADMIN_ID != 0:
pending_users.add(user_id)
username = event.from_user.username or event.from_user.first_name or "Unknown"
# 1. Kirim pesan ke Admin dengan tombol
kb = InlineKeyboardMarkup(inline_keyboard=[
[
InlineKeyboardButton(text="✅ Izinkan", callback_data=f"approve_{user_id}"),
InlineKeyboardButton(text="❌ Tolak", callback_data=f"reject_{user_id}")
]
])
try:
await bot.send_message(
ADMIN_ID,
f"🔔 **IZIN AKSES BARU!**\n\n👤 Nama: `{username}`\n🆔 ID: `{user_id}`\n\n_Apakah kamu mengizinkan orang ini menggunakan bot musikmu?_",
reply_markup=kb, parse_mode="Markdown"
)
except Exception as e:
logger.error(f"Gagal mengirim notif ke admin: {e}")
# 2. Kirim pesan ke User asing
if isinstance(event, types.Message):
await event.answer("🔒 **Akses Terkunci.**\nBot ini bersifat private. Permintaan izin telah dikirim ke Admin. Harap tunggu persetujuan.", parse_mode="Markdown")
elif isinstance(event, types.CallbackQuery):
await event.answer("Akses terkunci! Permintaan izin dikirim ke Admin.", show_alert=True)
# Abaikan semua perintah dari user asing ini
return return
return await handler(event, data) return await handler(event, data)
dp.message.middleware(SecurityMiddleware()) dp.message.middleware(SecurityMiddleware())
dp.callback_query.middleware(SecurityMiddleware()) dp.callback_query.middleware(SecurityMiddleware())
# --- FUNGSI UPDATE .ENV OTOMATIS ---
def add_user_to_env(new_id):
try:
with open(".env", "r") as f:
lines = f.readlines()
with open(".env", "w") as f:
for line in lines:
if line.startswith("AUTHORIZED_USER_IDS="):
clean_line = line.strip().rstrip(',')
line = f"{clean_line},{new_id}\n"
f.write(line)
logger.info(f"User {new_id} berhasil ditambahkan permanen ke .env")
except Exception as e:
logger.error(f"Gagal update .env: {e}")
# --- HANDLER TOMBOL ADMIN (APPROVE / REJECT) ---
@dp.callback_query(F.data.startswith("approve_") | F.data.startswith("reject_"))
async def handle_admin_action(callback: CallbackQuery):
if callback.from_user.id != ADMIN_ID:
return await callback.answer("Kamu bukan Admin!", show_alert=True)
action, target_id_str = callback.data.split("_")
target_id = int(target_id_str)
if target_id in pending_users:
pending_users.remove(target_id)
await callback.message.edit_reply_markup(reply_markup=None)
if action == "approve":
AUTHORIZED_USER_IDS.add(target_id)
add_user_to_env(target_id)
await callback.message.edit_text(f"✅ User `{target_id}` telah **DISETUJUI** dan ditambahkan permanen ke sistem.", parse_mode="Markdown")
try:
await bot.send_message(target_id, "🎉 **AKSES DISETUJUI!**\nAdmin telah memberikan izin. Silakan ketik /start atau /play <judul> untuk mulai mencari lagu.", parse_mode="Markdown")
except: pass
else:
await callback.message.edit_text(f"❌ User `{target_id}` telah **DITOLAK**.", parse_mode="Markdown")
try:
await bot.send_message(target_id, "❌ **AKSES DITOLAK.**\nAdmin tidak memberikan izin untuk menggunakan bot ini.", parse_mode="Markdown")
except: pass
def extract_video_id(query): def extract_video_id(query):
match = re.search(r"(?:v=|\/)([0-9A-Za-z_-]{11}).*", query) match = re.search(r"(?:v=|\/)([0-9A-Za-z_-]{11}).*", query)
return match.group(1) if match else None return match.group(1) if match else None
# --- FUNGSI PEMBUAT TOMBOL PAGING ---
def get_search_keyboard(chat_id, page=0): def get_search_keyboard(chat_id, page=0):
data = user_searches.get(chat_id) data = user_searches.get(chat_id)
if not data: return None if not data: return None
results = data['results'] results = data['results']
start_idx = page * 10 start_idx = page * 10
end_idx = start_idx + 10 end_idx = start_idx + 10
@@ -72,29 +146,20 @@ def get_search_keyboard(chat_id, page=0):
kb = [] kb = []
for rec in page_results: for rec in page_results:
# Tampilkan Judul | Nama Channel (Biar tahu itu lagu asli atau cover) display_text = f"🎵 {rec['title']} | {rec.get('uploader', 'YT')}"
display_text = f"🎵 {rec['title']} | {rec['uploader']}"
short_text = display_text[:55] + "..." if len(display_text) > 55 else display_text short_text = display_text[:55] + "..." if len(display_text) > 55 else display_text
# Menggunakan format callback "play_" agar otomatis masuk antrean download
kb.append([InlineKeyboardButton(text=short_text, callback_data=f"play_{rec['id']}")]) kb.append([InlineKeyboardButton(text=short_text, callback_data=f"play_{rec['id']}")])
# Tombol Navigasi Next/Prev
nav_buttons = [] nav_buttons = []
if page > 0: if page > 0: nav_buttons.append(InlineKeyboardButton(text="⬅️ Prev", callback_data=f"page_{page-1}"))
nav_buttons.append(InlineKeyboardButton(text="⬅️ Prev", callback_data=f"page_{page-1}")) if end_idx < len(results): nav_buttons.append(InlineKeyboardButton(text="Next ➡️", callback_data=f"page_{page+1}"))
if end_idx < len(results): if nav_buttons: kb.append(nav_buttons)
nav_buttons.append(InlineKeyboardButton(text="Next ➡️", callback_data=f"page_{page+1}"))
if nav_buttons:
kb.append(nav_buttons)
return InlineKeyboardMarkup(inline_keyboard=kb) return InlineKeyboardMarkup(inline_keyboard=kb)
# --- 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("🎧 **Music Bot Ready!**\nKirim format: `/play <judul lagu>`", parse_mode="Markdown") await message.answer("🎧 **Music Bot VIP Ready!**\nKirim format: `/play <judul lagu>`", 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):
@@ -102,60 +167,41 @@ async def cmd_play(message: types.Message):
if not query: return await message.answer("⚠️ Masukkan judul lagu.") if not query: return await message.answer("⚠️ Masukkan judul lagu.")
status_msg = await message.answer("🔍 *Mencari daftar lagu...*", parse_mode="Markdown") status_msg = await message.answer("🔍 *Mencari daftar lagu...*", parse_mode="Markdown")
# Ambil 30 hasil sekaligus
results = await search_youtube_list(query, max_results=30) results = await search_youtube_list(query, max_results=30)
if not results: if not results: return await status_msg.edit_text("❌ Lagu tidak ditemukan.")
return await status_msg.edit_text("❌ Lagu tidak ditemukan.")
# Simpan ke memori sementara untuk paging
user_searches[message.chat.id] = { user_searches[message.chat.id] = {
'query': query, 'header': f"🔎 Hasil pencarian: **{query}**",
'results': results 'results': results
} }
# Tampilkan halaman pertama (index 0)
kb = get_search_keyboard(message.chat.id, page=0) kb = get_search_keyboard(message.chat.id, page=0)
await status_msg.edit_text( await status_msg.edit_text(text=f"{user_searches[message.chat.id]['header']}\n_Halaman 1_", reply_markup=kb, parse_mode="Markdown")
text=f"🔎 Hasil pencarian: **{query}**\n_Pilih lagu untuk diunduh:_",
reply_markup=kb,
parse_mode="Markdown"
)
# --- HANDLER TOMBOL PAGING (NEXT/PREV) ---
@dp.callback_query(F.data.startswith("page_")) @dp.callback_query(F.data.startswith("page_"))
async def handle_page_click(callback: CallbackQuery): async def handle_page_click(callback: CallbackQuery):
chat_id = callback.message.chat.id chat_id = callback.message.chat.id
if chat_id not in user_searches: if chat_id not in user_searches:
return await callback.answer("Pencarian kadaluarsa. Ketik /play lagi.", show_alert=True) return await callback.answer("Sesi kadaluarsa. Ketik /play lagi.", show_alert=True)
page = int(callback.data.split("_")[1]) page = int(callback.data.split("_")[1])
kb = get_search_keyboard(chat_id, page) kb = get_search_keyboard(chat_id, page)
if kb: if kb:
query = user_searches[chat_id]['query'] header = user_searches[chat_id]['header']
await callback.message.edit_text( await callback.message.edit_text(text=f"{header}\n_Halaman {page+1}_", reply_markup=kb, parse_mode="Markdown")
text=f"🔎 Hasil pencarian: **{query}**\n_Halaman {page+1}_",
reply_markup=kb,
parse_mode="Markdown"
)
await callback.answer() await callback.answer()
# --- HANDLER DOWNLOAD LAGU ---
@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]
# Opsional: Jika tidak mau layarnya penuh tombol setelah diklik, hapus tombolnya
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, "⏳ Mengunduh lagu pilihanmu...") 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}"
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 (DOWNLOAD & S3 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
try: try:
@@ -169,22 +215,18 @@ async def process_music_request(query: str, chat_id: int, status_msg_id: int):
s3_object_name = cached_data.get('s3_object_key', f"{video_id}.m4a") s3_object_name = cached_data.get('s3_object_key', f"{video_id}.m4a")
ext = s3_object_name.split('.')[-1] ext = s3_object_name.split('.')[-1]
local_file = f"downloads/cache_{video_id}.{ext}" local_file = f"downloads/cache_{video_id}.{ext}"
await bot.edit_message_text("⚡ Mengambil dari Cache...", chat_id, status_msg_id) await bot.edit_message_text("⚡ Mengambil dari Cache...", chat_id, status_msg_id)
await download_audio(s3_object_name, local_file) await download_audio(s3_object_name, local_file)
if not cached_data: if not cached_data:
await bot.edit_message_text("🔍 Mengunduh dari YouTube...", chat_id, status_msg_id) await bot.edit_message_text("🔍 Mengunduh dari YouTube...", 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":
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)
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}.{yt_result['ext']}" s3_object_name = f"{video_id}.{yt_result['ext']}"
await bot.edit_message_text("☁️ Menyimpan ke Server...", chat_id, status_msg_id) await bot.edit_message_text("☁️ Menyimpan ke Server...", chat_id, status_msg_id)
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)
@@ -196,15 +238,16 @@ async def process_music_request(query: str, chat_id: int, status_msg_id: int):
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)
if status_msg_id: await bot.delete_message(chat_id, status_msg_id) if status_msg_id: await bot.delete_message(chat_id, status_msg_id)
# --- TETAP TAMPILKAN 5 SUGGESTION SETELAH LAGU SELESAI DIPUTAR --- # --- TAMPILKAN 30 REKOMENDASI DENGAN PAGING ---
recs = await get_recommendations(video_id) recs = await get_recommendations(video_id)
if recs: if recs:
kb = [] user_searches[chat_id] = {
for rec in recs[:5]: 'header': f"💡 Rekomendasi lanjutan dari: **{title}**",
short_title = rec['title'][:55] + "..." if len(rec['title']) > 55 else rec['title'] 'results': recs
kb.append([InlineKeyboardButton(text=f"▶️ {short_title}", callback_data=f"play_{rec['id']}")]) }
reply_markup = InlineKeyboardMarkup(inline_keyboard=kb) kb = get_search_keyboard(chat_id, page=0)
await bot.send_message(chat_id, text=f"💡 **Rekomendasi dari:**\n_{title}_", reply_markup=reply_markup, parse_mode="Markdown") if kb:
await bot.send_message(chat_id, text=f"{user_searches[chat_id]['header']}\n_Halaman 1_", reply_markup=kb, parse_mode="Markdown")
except Exception as e: except Exception as e:
logger.error(f"Error: {e}") logger.error(f"Error: {e}")
@@ -223,7 +266,7 @@ async def main():
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 V3 (Search & Paging) Berjalan!") logger.info("🚀 Bot Music V4 (Admin Approval & Super Paging) 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 jX9HIeAZEXX3bP9f/AopL252XpVmN6w-fU .youtube.com TRUE / TRUE 2147483647 __Secure-1PAPISID jX9HIeAZEXX3bP9f/AopL252XpVmN6w-fU
.youtube.com TRUE / TRUE 2147483647 __Secure-1PSID g.a0008AhqPQ4vlKVPuJB4BY8zzdimzFGpjCZI3PtorsbzCvPCb3-UNiEKAu2Nem3IRBTeL7M4jQACgYKAWsSARESFQHGX2MiBR4lu_TMGVkDg6nuR2v3KxoVAUF8yKpUhZ9s0gTmijqUQGCrrAbz0076 .youtube.com TRUE / TRUE 2147483647 __Secure-1PSID g.a0008AhqPQ4vlKVPuJB4BY8zzdimzFGpjCZI3PtorsbzCvPCb3-UNiEKAu2Nem3IRBTeL7M4jQACgYKAWsSARESFQHGX2MiBR4lu_TMGVkDg6nuR2v3KxoVAUF8yKpUhZ9s0gTmijqUQGCrrAbz0076
.youtube.com TRUE / TRUE 1806026268 __Secure-1PSIDCC AKEyXzX-CL1XOf6OeNeSLRiwwlfwxttYfujFl_Cgwant9zdr6x0pJNiNPlM3ehq1FGS36kyC .youtube.com TRUE / TRUE 1806026697 __Secure-1PSIDCC AKEyXzVzj8BHVLDosFha11ymmFBOV4r1hRS1ta4nWZcHgZkBacg6rhh1hkprOJcA7YSDScfX
.youtube.com TRUE / TRUE 2147483647 __Secure-1PSIDRTS sidts-CjYBWhotCR9GvMAvgfIj2VO7w2GS8jUm45hvgjK75mkpM7qlEO-z2RIGvuP3FX0ApZ3UEnRHAaIQAA .youtube.com TRUE / TRUE 2147483647 __Secure-1PSIDRTS sidts-CjYBWhotCR9GvMAvgfIj2VO7w2GS8jUm45hvgjK75mkpM7qlEO-z2RIGvuP3FX0ApZ3UEnRHAaIQAA
.youtube.com TRUE / TRUE 2147483647 __Secure-1PSIDTS sidts-CjYBWhotCR9GvMAvgfIj2VO7w2GS8jUm45hvgjK75mkpM7qlEO-z2RIGvuP3FX0ApZ3UEnRHAaIQAA .youtube.com TRUE / TRUE 2147483647 __Secure-1PSIDTS sidts-CjYBWhotCR9GvMAvgfIj2VO7w2GS8jUm45hvgjK75mkpM7qlEO-z2RIGvuP3FX0ApZ3UEnRHAaIQAA
.youtube.com TRUE / TRUE 2147483647 __Secure-3PAPISID jX9HIeAZEXX3bP9f/AopL252XpVmN6w-fU .youtube.com TRUE / TRUE 2147483647 __Secure-3PAPISID jX9HIeAZEXX3bP9f/AopL252XpVmN6w-fU
.youtube.com TRUE / TRUE 2147483647 __Secure-3PSID g.a0008AhqPQ4vlKVPuJB4BY8zzdimzFGpjCZI3PtorsbzCvPCb3-UVw0LJPNy1xBeKd3GjL_nrgACgYKAd8SARESFQHGX2Mi4oiwDW0agUlDxti69hv6zBoVAUF8yKqTGo8oCprmktCz7NrkNVpx0076 .youtube.com TRUE / TRUE 2147483647 __Secure-3PSID g.a0008AhqPQ4vlKVPuJB4BY8zzdimzFGpjCZI3PtorsbzCvPCb3-UVw0LJPNy1xBeKd3GjL_nrgACgYKAd8SARESFQHGX2Mi4oiwDW0agUlDxti69hv6zBoVAUF8yKqTGo8oCprmktCz7NrkNVpx0076
.youtube.com TRUE / TRUE 1806026268 __Secure-3PSIDCC AKEyXzV4asU3xodaOGcneKNBezILDXAjNbLEZhJZv479YWYlAECeXq-5QgNsMibF-b3qu2d33g .youtube.com TRUE / TRUE 1806026697 __Secure-3PSIDCC AKEyXzURj6FH3Hh60cp98zUCM49GiTZJ9mcBkypQpiCq5neND5iTw7dWvfmk3CCUrnyqDksmyw
.youtube.com TRUE / TRUE 2147483647 __Secure-3PSIDRTS sidts-CjYBWhotCR9GvMAvgfIj2VO7w2GS8jUm45hvgjK75mkpM7qlEO-z2RIGvuP3FX0ApZ3UEnRHAaIQAA .youtube.com TRUE / TRUE 2147483647 __Secure-3PSIDRTS sidts-CjYBWhotCR9GvMAvgfIj2VO7w2GS8jUm45hvgjK75mkpM7qlEO-z2RIGvuP3FX0ApZ3UEnRHAaIQAA
.youtube.com TRUE / TRUE 2147483647 __Secure-3PSIDTS sidts-CjYBWhotCR9GvMAvgfIj2VO7w2GS8jUm45hvgjK75mkpM7qlEO-z2RIGvuP3FX0ApZ3UEnRHAaIQAA .youtube.com TRUE / TRUE 2147483647 __Secure-3PSIDTS sidts-CjYBWhotCR9GvMAvgfIj2VO7w2GS8jUm45hvgjK75mkpM7qlEO-z2RIGvuP3FX0ApZ3UEnRHAaIQAA
.youtube.com TRUE / TRUE 2147483647 __Secure-ROLLOUT_TOKEN CMbM-7XNqZzV5QEQmYbS_rO8kwMY5PKr_7O8kwM%3D .youtube.com TRUE / TRUE 2147483647 __Secure-ROLLOUT_TOKEN CMbM-7XNqZzV5QEQmYbS_rO8kwMY5PKr_7O8kwM%3D
@@ -18,6 +18,6 @@
.youtube.com TRUE / TRUE 2147483647 LOGIN_INFO AFmmF2swRAIgDHRqApvISbyAZq7Jw7ACZDaYENyiLfRJhkSxgsaSzHUCIDBqEp7Lla6YL_9yHKzX5OuwRv6eSuDP7wN4eu2h8boE:QUQ3MjNmenlDUldaUlpCVmlMc2ZFdWxtWVlzX2lVRTRic2o4djgzSlJiaHhHYnN4dmh0YTU2NFdpS1lRSkZIUlN3RVBRWmZMTFdTU1pjQ3gtU09LTmRUX3l6WEpCWGdpeFpiVkptS1BHRlBlQXU1R0RXZjR3cnR4LW94YlREbzFITzdOaWctTEJLcjN5UWpCM3VXVTQzUWZiMl9RLXhNRERR .youtube.com TRUE / TRUE 2147483647 LOGIN_INFO AFmmF2swRAIgDHRqApvISbyAZq7Jw7ACZDaYENyiLfRJhkSxgsaSzHUCIDBqEp7Lla6YL_9yHKzX5OuwRv6eSuDP7wN4eu2h8boE:QUQ3MjNmenlDUldaUlpCVmlMc2ZFdWxtWVlzX2lVRTRic2o4djgzSlJiaHhHYnN4dmh0YTU2NFdpS1lRSkZIUlN3RVBRWmZMTFdTU1pjQ3gtU09LTmRUX3l6WEpCWGdpeFpiVkptS1BHRlBlQXU1R0RXZjR3cnR4LW94YlREbzFITzdOaWctTEJLcjN5UWpCM3VXVTQzUWZiMl9RLXhNRERR
.youtube.com TRUE / TRUE 2147483647 NID 530=J1je9vBs7k1AZSF4I6E-32SX_vniEVSvWt6vRJC19rznxuHBniKAUk91MSxJ9KY6sPEIGqw29RYQvsglruzNcyGUpG4RIb4Fk50UUrFHbXG28v34nj9rkikmhXi7N84D_-1l8im9-Qm9zkPfxKaL8V8xBaBjK6a1p2et2q3_n5wM0hOI7R24Gw4hQbX9Rcj-oUTuBbvIxb_VevmQ .youtube.com TRUE / TRUE 2147483647 NID 530=J1je9vBs7k1AZSF4I6E-32SX_vniEVSvWt6vRJC19rznxuHBniKAUk91MSxJ9KY6sPEIGqw29RYQvsglruzNcyGUpG4RIb4Fk50UUrFHbXG28v34nj9rkikmhXi7N84D_-1l8im9-Qm9zkPfxKaL8V8xBaBjK6a1p2et2q3_n5wM0hOI7R24Gw4hQbX9Rcj-oUTuBbvIxb_VevmQ
.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 YSC T0iFTYMzz_E .youtube.com TRUE / TRUE 0 YSC 6K5oY8QB4bs
.youtube.com TRUE / TRUE 1790042268 VISITOR_INFO1_LIVE fHx_hZBK2nA .youtube.com TRUE / TRUE 1790042697 VISITOR_INFO1_LIVE JbfxjpcD228
.youtube.com TRUE / TRUE 1790042268 VISITOR_PRIVACY_METADATA CgJJRBIEGgAgQw%3D%3D .youtube.com TRUE / TRUE 1790042697 VISITOR_PRIVACY_METADATA CgJJRBIEGgAgXw%3D%3D

View File

@@ -25,7 +25,7 @@ def generate_netscape_cookies():
print(f"Error parsing cookies: {e}") print(f"Error parsing cookies: {e}")
return False return False
def fetch_recommendations_sync(video_id: str, limit: int = 5): def fetch_recommendations_sync(video_id: str, limit: int = 30):
ydl_opts_recs = { ydl_opts_recs = {
'quiet': True, 'quiet': True,
'extract_flat': True, 'extract_flat': True,