import express from 'express'; import cors from 'cors'; import ytdl from '@distube/ytdl-core'; import yts from 'youtube-yts'; import { HttpsProxyAgent } from 'https-proxy-agent'; import { exec } from 'child_process'; import { promisify } from 'util'; import fs from 'fs'; import path from 'path'; const execAsync = promisify(exec); const COOKIES_PATH = path.resolve('./cookies.txt'); // Dynamically write cookies from environment secret if present if (process.env.YT_COOKIES) { console.log('[Cookies] Dynamically writing cookies.txt from environment variable...'); try { fs.writeFileSync(COOKIES_PATH, process.env.YT_COOKIES, 'utf-8'); console.log('[Cookies] Successfully wrote cookies.txt to', COOKIES_PATH); } catch (err) { console.error('[Cookies] Failed to write cookies.txt:', err.message); } } else { console.log('[Cookies] YT_COOKIES environment secret not set, checking for local cookies.txt file...'); } const app = express(); app.use(cors()); app.use(express.json()); const PORT = process.env.PORT || 7860; // High-Availability Cobalt Pool const COBALT_INSTANCES = [ "https://cobalt.omega.wolfy.love", "https://apicobalt.mgytr.top", "https://api.qwkuns.me", "https://subito-c.meowing.de", "https://cobalt.alpha.wolfy.love" ]; // LAYER 4: Invidious Proxy Fallback (Ultimate 429/Age-Gate Bypass) const INVIDIOUS_INSTANCES = [ "https://invidious.f5.si", "https://invidious.tiekoetter.com", "https://invidious.nerdvpn.de", "https://yt.chocolatemoo53.com", "https://inv.thepixora.com", "https://inv.nadeko.net" ]; // LAYER 5: Piped API Fallback (Independent of YouTube's auth challenges) const PIPED_INSTANCES = [ "https://pipedapi.kavin.rocks", "https://pipedapi.adminforge.de", "https://piped-api.lunar.icu", "https://api.piped.projectsegfau.lt", "https://pipedapi.r4fo.com", "https://pipedapi.darkness.services" ]; // Innertube clients raced in parallel — same approach as the CF worker. // ANDROID_MUSIC is fastest; ANDROID_VR and TVHTML5 are independent fallbacks // that historically bypass the "Sign in" challenge that hits ANDROID_MUSIC. const INNERTUBE_CLIENTS = [ { name: "ANDROID_MUSIC", version: "6.19.52", userAgent: "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Mobile Safari/537.36" }, { name: "ANDROID_VR", version: "1.55.18", userAgent: "Mozilla/5.0 (Linux; Android 10; Quest 2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Mobile Safari/537.36" }, { name: "TVHTML5_SIMPLY_EMBEDDED_PLAYER", version: "2.0", userAgent: "Mozilla/5.0 (PlayStation; PlayStation 4/12.00) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.0 Safari/605.1.15" } ]; // ============================================================ // LAYER 0 (PRIMARY): IZGIV-Botto 3rd-Party API Cascade // Ported from IZGIV-Botto/commands/song.js + play.js — these // downloader APIs (EliteProTech, Yupra, Okatsu, apis-keith) // are battle-tested in production and bypass every // signatureCipher / "Sign in" / 403 challenge. Raced in // parallel with 3-attempt retry (1s/2s/3s backoff) per // the IZGIV-Botto tryRequest pattern. // ============================================================ // Sanity check the upstream audio URL before returning it. // Some 3rd-party APIs (notably EliteProTech right now) return URLs to // well-formed MP4 headers but with empty/zeroed `mdat` boxes — the // file looks valid but plays silence. A HEAD probe of Content-Length // against a sane floor (100KB — a real 3-min song at 64kbps is ~1.4MB) // catches this without a full body download. const MIN_AUDIO_BYTES = 100_000; async function validateAudioSize(url, label) { try { const head = await fetchWithTimeout(url, { method: 'HEAD', headers: { 'User-Agent': 'Mozilla/5.0' }, timeout: 5000 }); if (!head.ok) throw new Error(`HEAD status ${head.status}`); const cl = parseInt(head.headers.get('content-length') || '0', 10); if (!cl || cl < MIN_AUDIO_BYTES) { throw new Error(`Too-small file: ${cl} bytes (min ${MIN_AUDIO_BYTES})`); } return true; } catch (err) { console.warn(`[Izgiv:${label}] size validation failed: ${err.message}`); throw err; } } async function getEliteProTechUrl(videoUrl) { const apiUrl = `https://eliteprotech-apis.zone.id/ytdown?url=${encodeURIComponent(videoUrl)}&format=mp3`; const res = await fetchWithTimeout(apiUrl, { headers: { 'User-Agent': 'Mozilla/5.0' }, timeout: 6000 }); if (!res.ok) throw new Error(`Status ${res.status}`); const data = await res.json(); // EliteProTech shape: { success: true, title, downloadURL } | { success: false, ... } if (!data?.success || !data?.downloadURL) throw new Error('No downloadURL in response'); await validateAudioSize(data.downloadURL, 'EliteProTech'); console.log(`[Izgiv:EliteProTech] ✅ resolved (title="${data.title}")`); return { url: data.downloadURL, title: data.title }; } async function getYupraUrl(videoUrl) { const apiUrl = `https://api.yupra.my.id/api/downloader/ytmp3?url=${encodeURIComponent(videoUrl)}`; const res = await fetchWithTimeout(apiUrl, { headers: { 'User-Agent': 'Mozilla/5.0' }, timeout: 6000 }); if (!res.ok) throw new Error(`Status ${res.status}`); const data = await res.json(); // Yupra shape: { success: true, data: { title, thumbnail, download_url, ... } } if (!data?.success || !data?.data?.download_url) throw new Error('No download_url in response'); await validateAudioSize(data.data.download_url, 'Yupra'); console.log(`[Izgiv:Yupra] ✅ resolved (title="${data.data.title}")`); return { url: data.data.download_url, title: data.data.title, thumbnail: data.data.thumbnail }; } async function getOkatsuUrl(videoUrl) { const apiUrl = `https://okatsu-rolezapiiz.vercel.app/downloader/ytmp3?url=${encodeURIComponent(videoUrl)}`; const res = await fetchWithTimeout(apiUrl, { headers: { 'User-Agent': 'Mozilla/5.0' }, timeout: 6000 }); if (res.status === 451) { console.warn(`[Izgiv:Okatsu] ⛔ 451 region/copyright block — skipping API`); return { _skipped: true }; } if (!res.ok) throw new Error(`Status ${res.status}`); const data = await res.json(); // Okatsu audio shape: { status, creator, title, format, thumb, duration, cached, dl } if (!data?.dl) throw new Error('No dl URL in response'); await validateAudioSize(data.dl, 'Okatsu'); console.log(`[Izgiv:Okatsu] ✅ resolved (title="${data.title}")`); return { url: data.dl, title: data.title, thumbnail: data.thumb }; } async function getApisKeithUrl(videoUrl) { const apiUrl = `https://apis-keith.vercel.app/download/dlmp3?url=${encodeURIComponent(videoUrl)}`; const res = await fetchWithTimeout(apiUrl, { headers: { 'User-Agent': 'Mozilla/5.0' }, timeout: 6000 }); if (!res.ok) throw new Error(`Status ${res.status}`); const data = await res.json(); // ApisKeith shape: { status: true, result: { title, mp3, image, duration, quality } } if (!data?.result?.mp3) throw new Error('No mp3 URL in response'); await validateAudioSize(data.result.mp3, 'ApisKeith'); console.log(`[Izgiv:ApisKeith] ✅ resolved (title="${data.result.title}")`); return { url: data.result.mp3, title: data.result.title, thumbnail: data.result.image }; } async function getA2ZConverterUrl(videoUrl) { const apiUrl = `https://www.a2zconverter.com/api/get-proxy-data?url=${encodeURIComponent(videoUrl)}`; const res = await fetchWithTimeout(apiUrl, { headers: { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36', 'Referer': 'https://www.a2zconverter.com/youtube-video-downloader' }, timeout: 6000 }); if (!res.ok) throw new Error(`Status ${res.status}`); const data = await res.json(); if (!data?.status || !data?.data?.items?.audio) { throw new Error('Invalid or failed response from a2zconverter'); } const audios = data.data.items.audio; if (!audios || audios.length === 0) { throw new Error('No audio streams found in a2zconverter data'); } const bestAudio = audios[0]; if (!bestAudio.url) { throw new Error('No audio URL in a2zconverter item'); } await validateAudioSize(bestAudio.url, 'A2ZConverter'); console.log(`[Izgiv:A2ZConverter] ✅ resolved (title="${data.data.title}")`); return { url: bestAudio.url, title: data.data.title, thumbnail: data.data.cover }; } async function tryRequest(getter, label) { try { const data = await getter(); if (data && data._skipped) return { skipped: true }; if (data && data.url) return data; } catch (err) { console.warn(`[Izgiv:${label}] failed: ${err.message}`); } return null; } async function resolveIzgivBotto(videoId) { const videoUrl = `https://www.youtube.com/watch?v=${videoId}`; console.log(`[Izgiv] Racing all 5 APIs in parallel for ${videoId}...`); try { const winner = await Promise.any([ tryRequest(() => getEliteProTechUrl(videoUrl), 'EliteProTech').then(res => { if (res && res.url) { res.strategy = 'izgiv:eliteprotech'; return res; } throw new Error('EliteProTech returned empty'); }), tryRequest(() => getYupraUrl(videoUrl), 'Yupra').then(res => { if (res && res.url) { res.strategy = 'izgiv:yupra'; return res; } throw new Error('Yupra returned empty'); }), tryRequest(() => getOkatsuUrl(videoUrl), 'Okatsu').then(res => { if (res && res.url) { res.strategy = 'izgiv:okatsu'; return res; } throw new Error('Okatsu returned empty'); }), tryRequest(() => getApisKeithUrl(videoUrl), 'ApisKeith').then(res => { if (res && res.url) { res.strategy = 'izgiv:apis-keith'; return res; } throw new Error('ApisKeith returned empty'); }), tryRequest(() => getA2ZConverterUrl(videoUrl), 'A2ZConverter').then(res => { if (res && res.url) { res.strategy = 'izgiv:a2zconverter'; return res; } throw new Error('A2ZConverter returned empty'); }) ]); console.log(`[Izgiv] ✅ Resolved ${videoId} via ${winner.strategy}`); return winner; } catch (err) { console.warn('[Izgiv] All IZGIV APIs failed — falling through to next Tier'); return null; } } // --- HIGH-PERFORMANCE OUTBOUND PROXY POOL ROTATION --- const PROXY_POOL = process.env.PROXY_POOL ? process.env.PROXY_POOL.split(',').map(p => p.trim()) : []; let proxyIndex = 0; function getNextProxy() { if (PROXY_POOL.length === 0) return null; const proxy = PROXY_POOL[proxyIndex]; proxyIndex = (proxyIndex + 1) % PROXY_POOL.length; return proxy; } // Gold-Standard Fetch Timeout Utility with Proxy Rotation async function fetchWithTimeout(url, options = {}) { const { timeout = 5000, ...fetchOptions } = options; const controller = new AbortController(); const id = setTimeout(() => controller.abort(), timeout); // Dynamic proxy injection const proxy = getNextProxy(); if (proxy && !fetchOptions.agent) { console.log(`[Proxy Rotator] Directing request through: ${proxy}`); fetchOptions.agent = new HttpsProxyAgent(proxy); } try { const response = await fetch(url, { ...fetchOptions, signal: controller.signal }); clearTimeout(id); return response; } catch (err) { clearTimeout(id); throw err; } } // Memory cache for resolved stream URLs to handle range/concurrency requests const streamCache = new Map(); const CACHE_TTL_MS = 120 * 60 * 1000; // 120 minutes (2 hours) to minimize upstream API quota usage // ── Bot-side Telegram-channel cache (LONG-TERM, persistent) ──────────────── // The AriseRobot bot runs a small aiohttp sidecar that maps yt_id → a // Cloudflare-R2-hosted MP3 (mirrored from the bot's Telegram cache // channel). When a song is cached, we can serve the R2 URL directly and // skip the entire 5-layer resolution cascade. Hits are essentially free; // misses fall through to the cascade unchanged. const BOT_CACHE_URL = (process.env.CACHE_SERVER_URL || 'http://127.0.0.1:7861').replace(/\/$/, ''); const BOT_CACHE_TOKEN = process.env.CACHE_SERVER_TOKEN || 'arise-music-cache'; const BOT_CACHE_TIMEOUT_MS = parseInt(process.env.CACHE_SERVER_TIMEOUT_MS || '2500', 10); async function checkBotCache(videoId) { if (!videoId) return null; const url = `${BOT_CACHE_URL}/cache/lookup?yt_id=${encodeURIComponent(videoId)}`; const controller = new AbortController(); const id = setTimeout(() => controller.abort(), BOT_CACHE_TIMEOUT_MS); try { const res = await fetch(url, { method: 'GET', headers: { Authorization: `Bearer ${BOT_CACHE_TOKEN}` }, signal: controller.signal, }); if (res.status === 404) return null; if (!res.ok) { console.warn(`[BotCache] lookup ${videoId} status=${res.status}`); return null; } const data = await res.json(); if (data && data.hit) { console.log(`[BotCache] ✅ HIT for ${videoId} (msg=${data.messageId})`); return data; } return null; } catch (err) { // Bot not running, slow, or unreachable → fall through to cascade. if (err.name !== 'AbortError') { console.warn(`[BotCache] lookup ${videoId} failed: ${err.message}`); } return null; } finally { clearTimeout(id); } } // ── Bot-cache audio proxy ────────────────────────────────────────────────── // Streams the cached audio from the bot's /cache/audio endpoint and // re-serves it to the public. The node_proxy becomes the streaming // hot path — that's the trade-off for not using an external CDN like R2. // // In-memory LRU (~50 MB cap) avoids hitting the bot for repeat // requests of the same track. The bot already keeps its own on-disk // copy in scratch/music_cache_readout/, so this is just a second-tier // cache to absorb the bursty Range-request pattern from