From 31d4a72e62338dd978edc6737fa8811ae6ac5ef3 Mon Sep 17 00:00:00 2001 From: Honey Date: Mon, 17 Aug 2026 17:07:34 +0200 Subject: [PATCH] fix(backend): download via default client (web_music media 403s); add optional proxy MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The PO token provider works (web_music gets a token) but web_music's media URLs still 403 from a datacenter IP. Prefer the default player clients for the DOWNLOAD (their URLs go through with a PO token), keeping web_music only as a fallback for Art-Track extraction; the YTM search still leads with web_music. Also add an optional YTDLP_PROXY (--proxy) — the durable fix if the IP stays flagged (403/429) even with a PO token. Empty → unchanged. Co-Authored-By: Claude Opus 4.8 --- backend/lib/config.js | 3 +++ backend/lib/ytdlp.js | 15 +++++++++++---- backend/novasonic-server.js | 1 + 3 files changed, 15 insertions(+), 4 deletions(-) diff --git a/backend/lib/config.js b/backend/lib/config.js index dd6945e..1e861d7 100644 --- a/backend/lib/config.js +++ b/backend/lib/config.js @@ -13,6 +13,9 @@ const CONFIG = { // provider service; set its URL here (e.g. http://bgutil-provider:4416). Empty → no PO token used // (yt-dlp runs exactly as before). See docker-compose.yml. potProviderUrl: process.env.POT_PROVIDER_URL || '', + // Optional proxy for yt-dlp (e.g. a residential/rotating proxy) — the durable fix when the + // datacenter IP stays flagged (403/429) even with a PO token. Empty → yt-dlp runs directly. + ytdlpProxy: process.env.YTDLP_PROXY || '', }; module.exports = CONFIG; diff --git a/backend/lib/ytdlp.js b/backend/lib/ytdlp.js index f5bebd7..c596007 100644 --- a/backend/lib/ytdlp.js +++ b/backend/lib/ytdlp.js @@ -142,6 +142,10 @@ async function downloadTrack({ jobId, artist, rawArtist, albumArtist, title, alb ? ['--extractor-args', `youtubepot-bgutilhttp:base_url=${CONFIG.potProviderUrl}`] : []; + // Optional proxy — the durable fix when a datacenter IP stays flagged (403/429) even with a PO + // token. Route yt-dlp through a residential/rotating proxy via YTDLP_PROXY. Empty → not used. + const proxyArgs = () => (CONFIG.ytdlpProxy ? ['--proxy', CONFIG.ytdlpProxy] : []); + const baseArgs = [ '--extract-audio', '--audio-format', CONFIG.audioFmt, @@ -152,11 +156,13 @@ async function downloadTrack({ jobId, artist, rawArtist, albumArtist, title, alb '--newline', // Hard upper size limit — prevents saving a wrong 28-min video as a ~52 MB mp3 '--max-filesize', '30M', - // Use the web_music player client (falling back to default) so YouTube Music - // "Art Track" / topic uploads are reachable. The android_vr client returns - // "This video is not available" for many YTM tracks that play fine in the browser. - '--extractor-args', 'youtube:player_client=web_music,default', + // For the DOWNLOAD, prefer the default clients (their media URLs go through with a PO token from + // a datacenter IP) and keep web_music only as a fallback for Art Tracks that only it can extract. + // web_music's own media URLs 403 from datacenter IPs even with a valid PO token. (The YTM *search* + // in getYtmVideoUrls still leads with web_music, which is where Art-Track discovery matters.) + '--extractor-args', 'youtube:player_client=default,web_music', ...potArgs(), + ...proxyArgs(), ]; const spawnYtdlp = (args) => new Promise((resolve, reject) => { @@ -230,6 +236,7 @@ async function downloadTrack({ jobId, artist, rawArtist, albumArtist, title, alb // native Songs shelf (where the actual artist track lives). '--extractor-args', 'youtube:player_client=web_music,default', ...potArgs(), + ...proxyArgs(), ], { env: { ...process.env, PYTHONUNBUFFERED: '1' } }); proc.stdout.on('data', d => { const text = d.toString(); diff --git a/backend/novasonic-server.js b/backend/novasonic-server.js index c945989..1b43b14 100644 --- a/backend/novasonic-server.js +++ b/backend/novasonic-server.js @@ -535,6 +535,7 @@ app.post('/api/youtube/resolve', async (req, res) => { '--playlist-items', '1-50', // PO token provider (if configured) to get past YouTube's datacenter-IP block. Empty → omitted. ...(CONFIG.potProviderUrl ? ['--extractor-args', `youtubepot-bgutilhttp:base_url=${CONFIG.potProviderUrl}`] : []), + ...(CONFIG.ytdlpProxy ? ['--proxy', CONFIG.ytdlpProxy] : []), url, ], { timeout: 45000, maxBuffer: 20 * 1024 * 1024 }, (err, out, stderr) => { if (err && !out?.trim()) return reject(new Error(stderr?.trim() || err.message));