From 3afb843316f2396a3b500091f1fa08dc8c5c3c4f Mon Sep 17 00:00:00 2001 From: Your Name Date: Sun, 11 Jan 2026 21:06:23 +0100 Subject: [PATCH] Fix download: return URL for client-side download --- mod.ts | 26 +++++++++++--------------- ui.ts | 22 ++++++++++++++++++---- 2 files changed, 29 insertions(+), 19 deletions(-) diff --git a/mod.ts b/mod.ts index 72126e6..6d6ece6 100644 --- a/mod.ts +++ b/mod.ts @@ -256,11 +256,12 @@ async function handler(req: Request): Promise { return proxyAudio(audioUrl, req); } - // Download audio as M4A + // Download audio - returns URL for client-side download if (pathname === "/api/download") { const id = searchParams.get("id"); const title = searchParams.get("title") || "audio"; const artist = searchParams.get("artist") || ""; + const redirect = searchParams.get("redirect") !== "0"; if (!id) return error("Missing id"); // Get stream URL - try piped first, then invidious @@ -269,12 +270,10 @@ async function handler(req: Request): Promise { const piped = await fetchFromPiped(id); if (piped.success && piped.streamingUrls) { - // Find best audio - prefer mp4/m4a with MEDIUM quality const audio = piped.streamingUrls.find((s: any) => s.type?.includes("audio/mp4") && s.audioQuality === "AUDIO_QUALITY_MEDIUM" ) || piped.streamingUrls.find((s: any) => s.type?.includes("audio")); if (audio) { - // Use proxy URL (url field) - directUrl is IP-locked audioUrl = audio.url; contentType = audio.type?.split(";")[0] || "audio/mp4"; } @@ -295,25 +294,22 @@ async function handler(req: Request): Promise { if (!audioUrl) return json({ success: false, error: "No audio stream found" }, 404); - // Create filename const ext = contentType.includes("webm") ? ".webm" : ".m4a"; const filename = `${artist ? artist + " - " : ""}${title}`.replace(/[<>:"/\\|?*]/g, "").trim() + ext; - try { - const response = await fetch(audioUrl); - if (!response.ok) return json({ success: false, error: "Failed to fetch audio: " + response.status }, 502); - - return new Response(response.body, { + // Redirect to audio URL - browser will handle download + if (redirect) { + return new Response(null, { + status: 302, headers: { - "Content-Type": contentType, - "Content-Disposition": `attachment; filename="${encodeURIComponent(filename)}"`, - "Access-Control-Allow-Origin": "*", - "Access-Control-Expose-Headers": "Content-Disposition", + "Location": audioUrl, + ...corsHeaders, }, }); - } catch (err) { - return json({ success: false, error: "Download failed: " + String(err) }, 500); } + + // Return URL for client to handle + return json({ success: true, url: audioUrl, filename, contentType }); } // ============ LYRICS & INFO ============ diff --git a/ui.ts b/ui.ts index 55026da..82312fa 100644 --- a/ui.ts +++ b/ui.ts @@ -327,11 +327,25 @@ function render(f,append){ else el.innerHTML=html; } -function download(i){ +async function download(i){ var s=songs[i];if(!s||!s.videoId)return; - var title=encodeURIComponent(s.title||'audio'); - var artist=encodeURIComponent(s.artists?.map(a=>a.name).join(', ')||''); - window.open('/api/download?id='+s.videoId+'&title='+title+'&artist='+artist,'_blank'); + var btn=document.querySelectorAll('.dl-btn')[i]; + if(btn){btn.textContent='...';btn.disabled=true;} + try{ + var res=await fetch('/api/download?id='+s.videoId+'&redirect=0'); + var data=await res.json(); + if(!data.success||!data.url){alert('Download failed: '+(data.error||'No URL'));return;} + // Create download link + var a=document.createElement('a'); + a.href=data.url; + a.download=data.filename||'audio.m4a'; + a.target='_blank'; + a.rel='noopener'; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + }catch(e){alert('Download error');} + finally{if(btn){btn.textContent='↓';btn.disabled=false;}} } function play(i){