SungJinWooX's picture
Upload server.js with huggingface_hub
394183f verified
Raw
History Blame Contribute Delete
45.8 kB
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 <audio>.
const AUDIO_CACHE = new Map();
const AUDIO_CACHE_CAP = 50 * 1024 * 1024; // 50 MB total
let audioCacheBytes = 0;
async function proxyBotAudio(videoId, req, res) {
// LRU: refresh key order on access
if (AUDIO_CACHE.has(videoId)) {
const entry = AUDIO_CACHE.get(videoId);
AUDIO_CACHE.delete(videoId);
AUDIO_CACHE.set(videoId, entry);
return sendCachedAudio(entry, req, res);
}
const url = `${BOT_CACHE_URL}/cache/audio?yt_id=${encodeURIComponent(videoId)}`;
const fetchHeaders = {};
if (req.headers.range) {
fetchHeaders['Range'] = req.headers.range;
}
const controller = new AbortController();
const id = setTimeout(() => controller.abort(), 30_000);
let upstream;
try {
upstream = await fetch(url, {
method: 'GET',
headers: { Authorization: `Bearer ${BOT_CACHE_TOKEN}`, ...fetchHeaders },
signal: controller.signal,
});
} catch (err) {
clearTimeout(id);
console.warn(`[BotCache] audio fetch ${videoId} failed: ${err.message}`);
if (!res.headersSent) {
res.status(502).json({ error: `Bot cache unreachable: ${err.message}` });
}
return;
}
clearTimeout(id);
if (!upstream.ok && upstream.status !== 206) {
if (upstream.status === 404) {
console.log(`[BotCache] audio ${videoId} → 404 (miss)`);
} else {
console.warn(`[BotCache] audio ${videoId}${upstream.status}`);
}
const body = await upstream.text().catch(() => '');
res.status(upstream.status).json({ error: body || `Bot cache ${upstream.status}` });
return;
}
const contentType = upstream.headers.get('content-type') || 'audio/mpeg';
const contentLength = parseInt(upstream.headers.get('content-length') || '0', 10);
// Build a tiny CORS-friendly header bag
const headers = {
'Content-Type': contentType,
'Accept-Ranges': 'bytes',
'Cache-Control': 'public, max-age=3600',
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, OPTIONS',
'Access-Control-Allow-Headers': 'Range, Content-Type',
'Access-Control-Expose-Headers': 'Content-Length, Content-Range, Accept-Ranges',
};
for (const h of ['content-length', 'content-range']) {
const v = upstream.headers.get(h);
if (v) headers[h] = v;
}
res.writeHead(upstream.status, headers);
// Full-body cache: only when the response is small enough to keep in
// the in-memory budget. We buffer, then either send or set up streaming
// with an "on close" cache write.
if (contentLength > 0 && contentLength <= 8 * 1024 * 1024) {
try {
const ab = await upstream.arrayBuffer();
const buf = Buffer.from(ab);
res.end(buf);
// Promote to LRU cache if there's still room
evictIfNeeded(contentLength);
if (audioCacheBytes + contentLength <= AUDIO_CACHE_CAP) {
AUDIO_CACHE.set(videoId, { buf, contentType });
audioCacheBytes += contentLength;
}
return;
} catch (err) {
if (!res.headersSent) {
res.status(502).json({ error: `Bot cache read failed: ${err.message}` });
}
return;
}
}
// Large response: stream straight through (no in-memory cache).
const reader = upstream.body.getReader();
const push = async () => {
while (true) {
const { done, value } = await reader.read();
if (done) break;
res.write(value);
}
res.end();
};
push().catch(err => {
console.warn(`[BotCache] stream ${videoId} error: ${err.message}`);
res.end();
});
req.on('close', () => reader.cancel());
}
function sendCachedAudio(entry, req, res) {
const headers = {
'Content-Type': entry.contentType || 'audio/mpeg',
'Content-Length': String(entry.buf.length),
'Accept-Ranges': 'bytes',
'Cache-Control': 'public, max-age=3600',
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Headers': 'Range, Content-Type',
'Access-Control-Expose-Headers': 'Content-Length, Content-Range, Accept-Ranges',
};
// Honor Range for already-buffered responses
if (req.headers.range && entry.buf) {
const m = /^bytes=(\d+)-(\d+)?$/.exec(req.headers.range);
if (m) {
const start = parseInt(m[1], 10);
const end = m[2] ? parseInt(m[2], 10) : entry.buf.length - 1;
if (start <= end && end < entry.buf.length) {
const slice = entry.buf.subarray(start, end + 1);
headers['Content-Range'] = `bytes ${start}-${end}/${entry.buf.length}`;
headers['Content-Length'] = String(slice.length);
res.writeHead(206, headers);
res.end(slice);
return;
}
}
}
res.writeHead(200, headers);
res.end(entry.buf);
}
function evictIfNeeded(incoming) {
while (audioCacheBytes + incoming > AUDIO_CACHE_CAP && AUDIO_CACHE.size > 0) {
const oldestKey = AUDIO_CACHE.keys().next().value;
const oldest = AUDIO_CACHE.get(oldestKey);
if (!oldest) break;
audioCacheBytes -= oldest.buf.length;
AUDIO_CACHE.delete(oldestKey);
}
}
// Health check
app.get('/health', (req, res) => {
try {
res.status(200).json({
status: 'healthy',
service: 'node-ytdl-proxy',
timestamp: new Date().toISOString()
});
} catch (err) {
res.status(500).json({ error: err.message });
}
});
// Public cache audio proxy. Streams from the bot's /cache/audio endpoint
// over loopback. The node_proxy is the public-facing endpoint; the bot
// stays on a private network. Hit LRU is 50 MB total.
app.get('/cache/audio', async (req, res) => {
const ytId = (req.query.yt_id || '').trim();
if (!ytId) {
return res.status(400).json({ error: 'Missing required query parameter "yt_id"' });
}
return proxyBotAudio(ytId, req, res);
});
// LAYER 1: Multi-Client Innertube API Extractor — race ANDROID_MUSIC + ANDROID_VR + TVHTML5
// Returns the first direct audio URL any client gives. Skips signatureCipher entries
// (those require n/sig deciphering which the yt-dlp/pytubefix fallback handles).
async function innertubeFetchOnce(videoId, client) {
const url = "https://www.youtube.com/youtubei/v1/player";
const payload = {
videoId,
context: {
client: {
clientName: client.name,
clientVersion: client.version,
hl: "en",
gl: "US"
}
},
contentCheckOk: true,
racyCheckOk: true
};
try {
const response = await fetchWithTimeout(url, {
method: "POST",
headers: {
"Content-Type": "application/json",
"User-Agent": client.userAgent
},
body: JSON.stringify(payload),
timeout: 5000
});
if (!response.ok) {
console.warn(`[Innertube:${client.name}] status ${response.status}`);
return null;
}
const data = await response.json();
const formats = data?.streamingData?.adaptiveFormats || [];
const directFormats = formats.filter(f => f.url && !f.signatureCipher);
// Prefer format 140 (128kbps AAC m4a) — broadest device compatibility
let bestAudio = directFormats.find(f => f.mimeType?.includes("audio/mp4") && f.itag === 140)
|| directFormats.find(f => f.mimeType?.includes("audio/mp4"))
|| directFormats.find(f => f.mimeType?.includes("audio/webm"))
|| directFormats.find(f => f.mimeType?.includes("audio/"));
if (bestAudio && bestAudio.url) {
console.log(`[Innertube:${client.name}] ✅ direct stream for ${videoId}`);
return { url: bestAudio.url, contentLength: bestAudio.contentLength };
}
return null;
} catch (err) {
console.warn(`[Innertube:${client.name}] failed: ${err.message}`);
return null;
}
}
async function extractDirectStreamUrl(videoId) {
console.log(`[Innertube] Racing ${INNERTUBE_CLIENTS.length} clients for: ${videoId}`);
const attempts = INNERTUBE_CLIENTS.map(c => innertubeFetchOnce(videoId, c));
const results = await Promise.all(attempts);
const winner = results.find(r => r && r.url);
if (winner) {
return winner;
}
console.warn("[Innertube] All clients failed");
return null;
}
// Spawn pytubefix Python extractor
async function resolvePytubefix(videoId) {
try {
console.log(`[Pytubefix] Spawning pytubefix extraction for: ${videoId}`);
const cmd = `python3 resolve_pytubefix.py "${videoId}"`;
const { stdout } = await execAsync(cmd, { timeout: 6000 });
const data = JSON.parse(stdout);
if (data && data.url) {
console.log(`[Pytubefix] Successfully resolved stream!`);
return {
url: data.url,
contentLength: data.contentLength || null
};
}
if (data && data.error) {
console.warn(`[Pytubefix] Extraction warning: ${data.error}`);
}
return null;
} catch (err) {
console.error(`[Pytubefix] Execution failed: ${err.message}`);
return null;
}
}
// LAYER 2: Double-Engine CLI Extractor (Pytubefix Stage 2.1 & yt-dlp Stage 2.2)
async function resolveYtdl(videoId) {
// Stage 2.1: Try pytubefix First (Extremely robust & native JS-free decryption)
const pytubeResult = await resolvePytubefix(videoId);
if (pytubeResult) return pytubeResult;
// Stage 2.2: Fall back to direct yt-dlp CLI
const videoUrl = `https://www.youtube.com/watch?v=${videoId}`;
try {
console.log(`[yt-dlp CLI] Stage 2.2: Falling back to direct yt-dlp extraction for: ${videoId}`);
let cmd = `yt-dlp -j --format "140/ba/best" --socket-timeout 10 --no-check-certificate --legacy-server-connect --remote-components ejs:github "${videoUrl}"`;
if (fs.existsSync(COOKIES_PATH)) {
cmd += ` --cookies "${COOKIES_PATH}"`;
}
const { stdout } = await execAsync(cmd, { timeout: 15000 });
const data = JSON.parse(stdout);
if (data && data.url) {
console.log(`[yt-dlp CLI] Stage 2.2: DIRECT extraction succeeded!`);
return {
url: data.url,
contentLength: data.filesize || data.filesize_approx || null
};
}
return null;
} catch (err) {
console.error(`[yt-dlp CLI] Stage 2.2: DIRECT extraction failed: ${err.message}`);
return null;
}
}
// LAYER 3: Cobalt API Fallback — race ALL instances in parallel, first success wins
async function resolveCobalt(videoId) {
const promises = COBALT_INSTANCES.map(async (api) => {
try {
console.log(`[Cobalt] Attempting resolution via: ${api} for: ${videoId}`);
const res = await fetchWithTimeout(`${api}/api/json`, {
method: 'POST',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
'User-Agent': 'Mozilla/5.0'
},
body: JSON.stringify({
url: `https://www.youtube.com/watch?v=${videoId}`,
downloadMode: 'audio',
audioFormat: 'best'
}),
timeout: 5000
});
if (res.ok) {
const data = await res.json();
if (data && data.url) {
console.log(`[Cobalt] Successfully resolved stream from ${api}`);
return { url: data.url };
}
}
throw new Error(`Status ${res.status}`);
} catch (err) {
console.warn(`[Cobalt] API ${api} failed: ${err.message}`);
throw err;
}
});
try {
return await Promise.any(promises);
} catch (err) {
console.warn(`[Cobalt] All ${COBALT_INSTANCES.length} instances exhausted.`);
return null;
}
}
// LAYER 4: Invidious Proxy Fallback — race ALL instances in parallel
async function resolveInvidious(videoId) {
const promises = INVIDIOUS_INSTANCES.map(async (instance) => {
const streamUrl = `${instance}/latest_version?id=${videoId}&itag=140&local=true`;
try {
console.log(`[Invidious] Attempting proxy stream check via: ${streamUrl}`);
const resp = await fetchWithTimeout(streamUrl, {
method: 'HEAD',
timeout: 5000
});
if (resp.ok && resp.headers.get('content-type')?.includes('audio')) {
console.log(`[Invidious] Successfully found active stream source: ${streamUrl}`);
return {
url: streamUrl,
contentLength: resp.headers.get('content-length')
};
}
throw new Error('Bad content-type or status');
} catch (err) {
console.warn(`[Invidious] Instance ${instance} failed: ${err.message}`);
throw err;
}
});
try {
return await Promise.any(promises);
} catch (err) {
console.warn(`[Invidious] All ${INVIDIOUS_INSTANCES.length} instances exhausted.`);
return null;
}
}
// LAYER 5: Piped API Fallback — race ALL Piped instances, return lowest-bitrate audio stream
async function resolvePiped(videoId) {
const promises = PIPED_INSTANCES.map(async (instance) => {
try {
console.log(`[Piped] Resolving via: ${instance}/streams/${videoId}`);
const resp = await fetchWithTimeout(`${instance}/streams/${videoId}`, {
headers: { 'User-Agent': 'Mozilla/5.0' },
timeout: 5000
});
if (!resp.ok) throw new Error(`Status ${resp.status}`);
const data = await resp.json();
const streams = data?.audioStreams || [];
if (streams.length === 0) throw new Error('No audio streams');
// Pick the lowest bitrate for fastest first-byte to the audio element
const sorted = [...streams].sort((a, b) => (a.bitrate || 0) - (b.bitrate || 0));
const audio = sorted.find(s => s.mimeType?.includes('audio/mp4')) || sorted[0];
if (!audio?.url) throw new Error('No audio URL in response');
console.log(`[Piped] ✅ Resolved from ${instance}`);
return { url: audio.url, contentLength: null };
} catch (err) {
console.warn(`[Piped] ${instance} failed: ${err.message}`);
throw err;
}
});
try {
return await Promise.any(promises);
} catch (err) {
console.warn(`[Piped] All ${PIPED_INSTANCES.length} instances exhausted.`);
return null;
}
}
// Helper to fetch robust YouTube Metadata via YouTube's official, unblocked oEmbed API
async function getVideoMetadata(videoId) {
try {
console.log(`[Metadata] Fetching oEmbed for video: ${videoId}`);
const oembedUrl = `https://www.youtube.com/oembed?url=https://www.youtube.com/watch?v=${videoId}&format=json`;
const resp = await fetchWithTimeout(oembedUrl, { timeout: 3000 });
if (resp.ok) {
const data = await resp.json();
return {
title: data.title || 'YouTube Audio Feed',
duration: 0,
thumbnail: data.thumbnail_url || `https://i.ytimg.com/vi/${videoId}/hqdefault.jpg`,
artist: data.author_name || 'Various Artists'
};
}
} catch (err) {
console.warn(`[Metadata] oEmbed lookup failed: ${err.message}`);
}
// Safe fallback (avoid youtube-yts scraping as it gets blocked/hangs in cloud environments)
return {
title: 'YouTube Audio Feed',
duration: 0,
thumbnail: `https://i.ytimg.com/vi/${videoId}/hqdefault.jpg`,
artist: 'Various Artists'
};
}
// Master Resolver Orchestrator — runs the cheaper/faster strategies in a tiered cascade,
// prioritizing high-quality direct CDN URLs and falling back sequentially to avoid spamming subrequests.
async function masterResolve(videoId) {
// Group 1: High-Quality Direct APIs raced in parallel (IZGIV, Innertube, Cobalt)
try {
console.log(`[Resolve] Group 1: Racing IZGIV, Innertube, and Cobalt in parallel for ${videoId}...`);
const res = await Promise.any([
resolveIzgivBotto(videoId).then(r => {
if (r && r.url) return r;
throw new Error('Izgiv failed');
}),
extractDirectStreamUrl(videoId).then(r => {
if (r && r.url) return r;
throw new Error('Innertube failed');
}),
resolveCobalt(videoId).then(r => {
if (r && r.url) return r;
throw new Error('Cobalt failed');
})
]);
if (res && res.url) {
console.log(`[Resolve] ✅ Group 1 resolved stream for ${videoId}`);
return res;
}
} catch (err) {
console.warn(`[Resolve] Group 1 failed for ${videoId}: ${err.message}. Trying Group 2...`);
}
// Group 2: Public Pools raced in parallel (Piped, Invidious)
try {
console.log(`[Resolve] Group 2: Racing Piped and Invidious in parallel for ${videoId}...`);
const res = await Promise.any([
resolvePiped(videoId).then(r => {
if (r && r.url) return r;
throw new Error('Piped failed');
}),
resolveInvidious(videoId).then(r => {
if (r && r.url) return r;
throw new Error('Invidious failed');
})
]);
if (res && res.url) {
console.log(`[Resolve] ✅ Group 2 resolved stream for ${videoId}`);
return res;
}
} catch (err) {
console.warn(`[Resolve] Group 2 failed for ${videoId}: ${err.message}`);
}
return null;
}
// Search YouTube
app.get('/api/search', async (req, res) => {
const query = req.query.q;
if (!query) {
return res.status(400).json({ error: 'Missing search query parameter "q"' });
}
try {
console.log(`🔎 Searching YouTube for: ${query}`);
const searchResult = await yts({ query, hl: 'en', gl: 'US' });
const videos = (searchResult.videos || []).slice(0, 10).map(video => ({
id: video.videoId,
title: video.title,
artist: video.author.name,
coverUrl: video.thumbnail || video.image,
duration: video.seconds,
url: video.url
}));
console.log(`✅ Found ${videos.length} search results`);
res.status(200).json({ results: videos });
} catch (err) {
console.error('❌ Search failed:', err);
res.status(500).json({ error: err.message });
}
});
// Resolve Stream URL
app.get('/api/resolve', async (req, res) => {
try {
const videoId = req.query.v || req.query.url;
if (!videoId) {
return res.status(400).json({ error: 'Missing parameter "v" or "url"' });
}
const cleanId = videoId.startsWith('http') ? ytdl.getVideoID(videoId) : videoId;
// Check if cached
const cached = streamCache.get(cleanId);
if (cached && Date.now() < cached.expiresAt) {
console.log(`🎯 Cache HIT for resolve: ${cleanId}`);
const meta = await getVideoMetadata(cleanId).catch(() => ({}));
return res.status(200).json({
url: cached.url,
title: meta.title || 'YouTube Audio Feed',
duration: meta.duration || 0,
thumbnail: meta.thumbnail || `https://i.ytimg.com/vi/${cleanId}/hqdefault.jpg`,
artist: meta.artist || 'Various Artists',
format: 'm4a',
source: cached.source || 'memory'
});
}
// ── LAYER -1 (PRIMARY): Bot-side Telegram-channel cache ──
// The bot maintains a yt_id → Telegram-channel → /cache/audio cache.
// On hit we skip the entire 5-layer cascade (YouTube, IZGIV APIs,
// Cobalt, etc.). Bot-preheat is run when the user types /play, so by
// the time playback reaches the track, the cache is almost always
// warm. The resolved URL points at this proxy's own /cache/audio
// endpoint, which streams from the bot's loopback.
const botHit = await checkBotCache(cleanId).catch(() => null);
if (botHit) {
const proxyAudioUrl = `/cache/audio?yt_id=${encodeURIComponent(cleanId)}`;
streamCache.set(cleanId, {
url: proxyAudioUrl,
expiresAt: Date.now() + CACHE_TTL_MS,
source: 'bot_cache'
});
console.log(`[Resolve] Serving from bot cache: ${proxyAudioUrl}`);
return res.status(200).json({
url: proxyAudioUrl,
title: botHit.title || 'YouTube Audio Feed',
duration: botHit.duration || 0,
thumbnail: `https://i.ytimg.com/vi/${cleanId}/hqdefault.jpg`,
artist: botHit.artist || 'Various Artists',
format: 'mp3',
source: 'bot_cache',
cached: true
});
}
// Resolve stream and metadata in parallel safely
const [resolved, meta] = await Promise.all([
masterResolve(cleanId).catch(err => {
console.error('Error in masterResolve:', err);
return null;
}),
getVideoMetadata(cleanId).catch(err => {
console.error('Error in getVideoMetadata:', err);
return {
title: 'YouTube Audio Feed',
duration: 0,
thumbnail: `https://i.ytimg.com/vi/${cleanId}/hqdefault.jpg`,
artist: 'Various Artists'
}
})
]);
if (!resolved || !resolved.url) {
return res.status(500).json({ error: 'Failed to extract downloadable audio stream from all stages' });
}
// Cache the resolved URL!
streamCache.set(cleanId, {
url: resolved.url,
expiresAt: Date.now() + CACHE_TTL_MS,
source: 'cascade'
});
res.status(200).json({
url: resolved.url,
title: meta.title,
duration: meta.duration || resolved.duration || 0,
thumbnail: meta.thumbnail,
artist: meta.artist,
format: 'm4a',
source: 'cascade'
});
} catch (err) {
console.error('❌ Master resolution failed:', err);
res.status(500).json({ error: err.message });
}
});
// PREHEAT — resolve and cache stream URL without proxying the actual audio bytes.
// Called by the CF Worker (or frontend) to warm the cache so that when the
// queue advances, /api/stream serves from cache instantly.
app.get('/api/preheat', async (req, res) => {
try {
const videoId = req.query.v || req.query.url;
if (!videoId) {
return res.status(400).json({ error: 'Missing parameter "v" or "url"' });
}
const cleanId = videoId.startsWith('http') ? ytdl.getVideoID(videoId) : videoId;
const wasCached = (() => {
const c = streamCache.get(cleanId);
return !!(c && Date.now() < c.expiresAt);
})();
if (wasCached) {
console.log(`🔥 Preheat cache HIT for ${cleanId}`);
return res.status(200).json({ success: true, alreadyCached: true, strategy: 'cache' });
}
const resolved = await masterResolve(cleanId).catch(err => {
console.error(`[Preheat] masterResolve error for ${cleanId}:`, err);
return null;
});
if (!resolved || !resolved.url) {
console.warn(`[Preheat] All layers failed for ${cleanId}`);
return res.status(404).json({ success: false, error: 'All resolvers failed' });
}
streamCache.set(cleanId, {
url: resolved.url,
expiresAt: Date.now() + CACHE_TTL_MS
});
console.log(`🔥 Preheated ${cleanId} (cached for ${CACHE_TTL_MS / 60000} min)`);
res.status(200).json({ success: true, alreadyCached: false, strategy: 'fresh' });
} catch (err) {
console.error('❌ Preheat failed:', err);
res.status(500).json({ success: false, error: err.message });
}
});
// Stream Audio Direct Proxy
app.get('/api/stream', async (req, res) => {
try {
const videoId = req.query.v || req.query.url;
if (!videoId) {
return res.status(400).json({ error: 'Missing parameter "v" or "url"' });
}
const cleanId = videoId.startsWith('http') ? ytdl.getVideoID(videoId) : videoId;
console.log(`🎧 Master S-Rank streaming request for: ${cleanId}`);
// ── LAYER -1 (PRIMARY): Bot-side Telegram-channel cache ──
// Skip the entire 5-layer cascade on a hit. This proxy exposes its
// own /cache/audio endpoint that streams from the bot's loopback
// (see proxyBotAudio above). The redirect target is on the same
// public host, so the browser stays connected to one origin.
const botHit = await checkBotCache(cleanId).catch(() => null);
if (botHit) {
const audioProxy = `/cache/audio?yt_id=${encodeURIComponent(cleanId)}`;
console.log(`[Stream] Bot cache HIT → 302 to ${audioProxy}`);
return res.redirect(302, audioProxy);
}
// Fast-path direct redirect to source CDN if requested
if (req.query.redirect === 'true') {
console.log(`⚡ Fast-path redirect requested for: ${cleanId}`);
const cached = streamCache.get(cleanId);
if (cached && Date.now() < cached.expiresAt) {
console.log(`🎯 Cache HIT! Fast-redirecting to cached stream URL: ${cached.url}`);
return res.redirect(302, cached.url);
}
const resolved = await masterResolve(cleanId).catch(() => null);
if (resolved && resolved.url) {
streamCache.set(cleanId, {
url: resolved.url,
expiresAt: Date.now() + CACHE_TTL_MS
});
console.log(`✅ Success! Fast-redirecting to resolved stream URL: ${resolved.url}`);
return res.redirect(302, resolved.url);
}
}
// Handle HEAD requests immediately for pre-flight stability
if (req.method === 'HEAD') {
res.setHeader('Content-Type', 'audio/mp4');
res.setHeader('Accept-Ranges', 'bytes');
res.setHeader('Cache-Control', 'public, max-age=3600');
return res.status(200).end();
}
const rangeHeader = req.headers.range;
// --- Stream Cache HIT ---
const cached = streamCache.get(cleanId);
if (cached && Date.now() < cached.expiresAt) {
console.log(`🎯 Cache HIT! Serving cached stream URL inside HF Node Proxy for: ${cleanId}`);
try {
const responseHeaders = {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, OPTIONS',
'Access-Control-Allow-Headers': 'Range, Content-Type',
'Access-Control-Expose-Headers': 'Content-Length, Content-Range, Accept-Ranges',
'Accept-Ranges': 'bytes',
'Cache-Control': 'public, max-age=3600',
'Content-Type': 'audio/mp4'
};
const fetchHeaders = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
};
if (rangeHeader) {
fetchHeaders['Range'] = rangeHeader;
}
const streamResponse = await fetchWithTimeout(cached.url, {
headers: fetchHeaders,
timeout: 15000
});
const contentType = streamResponse.headers.get('content-type') || '';
if (streamResponse.ok && !contentType.toLowerCase().includes('text/html')) {
// Mirror content headers
for (const h of ['content-type', 'content-length', 'content-range']) {
const val = streamResponse.headers.get(h);
if (val) responseHeaders[h] = val;
}
res.writeHead(streamResponse.status, responseHeaders);
const reader = streamResponse.body.getReader();
const push = async () => {
while (true) {
const { done, value } = await reader.read();
if (done) break;
res.write(value);
}
res.end();
};
push().catch(err => {
console.error('❌ Pipelining error during read on cached stream:', err);
res.end();
});
req.on('close', () => {
reader.cancel();
});
return;
} else {
console.warn(`⚠️ Cached stream URL returned non-OK status ${streamResponse.status}. Invalidating cache.`);
streamCache.delete(cleanId);
}
} catch (err) {
console.error(`❌ Cached stream fetch failed inside HF Node Proxy: ${err.message}. Invalidating cache.`);
streamCache.delete(cleanId);
}
}
const resolved = await masterResolve(cleanId).catch(err => {
console.error('Error in masterResolve inside stream:', err);
return null;
});
if (!resolved || !resolved.url) {
return res.status(500).json({ error: 'Failed to extract audio stream from all stages' });
}
// Cache the resolved working URL!
streamCache.set(cleanId, {
url: resolved.url,
expiresAt: Date.now() + CACHE_TTL_MS
});
// Configure response headers matching cross-origin standards for WebViews
const responseHeaders = {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, OPTIONS',
'Access-Control-Allow-Headers': 'Range, Content-Type',
'Access-Control-Expose-Headers': 'Content-Length, Content-Range, Accept-Ranges',
'Accept-Ranges': 'bytes',
'Cache-Control': 'public, max-age=3600',
'Content-Type': 'audio/mp4'
};
const fetchHeaders = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
};
if (rangeHeader) {
fetchHeaders['Range'] = rangeHeader;
console.log(`⚡ Proxying stream with range: ${rangeHeader}`);
}
// Proxy the stream response directly from the resolved URL
const streamResponse = await fetchWithTimeout(resolved.url, {
headers: fetchHeaders,
timeout: 15000
});
const contentType = streamResponse.headers.get('content-type') || '';
const ct = contentType.toLowerCase();
if (!streamResponse.ok || ct.includes('text/html') || ct.includes('application/json') || ct.includes('text/plain')) {
console.warn(`⚠️ Upstream stream fetch failed or returned invalid content-type ${contentType} (status: ${streamResponse.status}). Falling back to HTTP 302 redirect directly to source CDN!`);
return res.redirect(302, resolved.url);
}
// Mirror content headers
for (const h of ['content-type', 'content-length', 'content-range']) {
const val = streamResponse.headers.get(h);
if (val) responseHeaders[h] = val;
}
res.writeHead(streamResponse.status, responseHeaders);
// Convert ReadableStream to Node.js stream and pipe to express response
const reader = streamResponse.body.getReader();
const push = async () => {
while (true) {
const { done, value } = await reader.read();
if (done) break;
res.write(value);
}
res.end();
};
push().catch(err => {
console.error('❌ Pipelining error during read:', err);
res.end();
});
req.on('close', () => {
console.log('🔌 Client disconnected, closing reader');
reader.cancel();
});
} catch (err) {
console.error('❌ Master streaming error:', err);
if (!res.headersSent) {
res.status(500).json({ error: err.message });
}
}
});
app.get('/api/test-ytdlp', async (req, res) => {
try {
let cmd = req.query.cmd;
if (cmd) {
console.log(`[Diagnostic] Executing custom command: ${cmd}`);
const { stdout, stderr } = await execAsync(cmd).catch(err => ({ stdout: '', stderr: err.message }));
return res.json({ command: cmd, stdout, stderr });
}
const videoId = req.query.v || 'jotdDIy1F90';
console.log(`[Diagnostic] Running double-engine test for: ${videoId}`);
// 1. Test pytubefix
const pytubeCmd = `python3 resolve_pytubefix.py "${videoId}"`;
const pytubeRes = await execAsync(pytubeCmd).catch(err => ({ stdout: '', stderr: err.message }));
// 2. Test yt-dlp
const videoUrl = `https://www.youtube.com/watch?v=${videoId}`;
const ytdlpCmd = `yt-dlp -j --format "140/ba/best" --socket-timeout 10 --no-check-certificate --legacy-server-connect --remote-components ejs:github "${videoUrl}"` +
(fs.existsSync(COOKIES_PATH) ? ` --cookies "${COOKIES_PATH}"` : '');
const ytdlpRes = await execAsync(ytdlpCmd).catch(err => ({ stdout: '', stderr: err.message }));
res.json({
videoId,
pytubefix: {
command: pytubeCmd,
stdout: pytubeRes.stdout,
stderr: pytubeRes.stderr
},
ytdlp: {
command: ytdlpCmd,
stdout: ytdlpRes.stdout.substring(0, 1000),
stderr: ytdlpRes.stderr.substring(0, 1000)
}
});
} catch (err) {
res.status(500).json({ error: err.message });
}
});
app.listen(PORT, '0.0.0.0', () => {
console.log(`🚀 Node S-Rank YT-DLP / Innertube Proxy listening on port ${PORT}`);
});