Update Deno API with fallback mechanism

This commit is contained in:
Your Name
2026-01-11 18:31:57 +01:00
parent a9ef873868
commit 7b69ea7f94
4 changed files with 144 additions and 21 deletions

View File

@@ -15,7 +15,11 @@
"deploy": { "deploy": {
"project": "85252de3-9b36-4d8b-b250-e491b4131838", "project": "85252de3-9b36-4d8b-b250-e491b4131838",
"exclude": [ "exclude": [
"**/node_modules" "**/node_modules",
"Music/**",
"virome-music/**",
".git/**",
".vscode/**"
], ],
"include": [], "include": [],
"entrypoint": "mod.ts" "entrypoint": "mod.ts"

15
lib.ts
View File

@@ -989,14 +989,20 @@ export async function fetchFromPiped(videoId: string) {
const data = await response.json(); const data = await response.json();
if (data?.audioStreams?.length) { if (data?.audioStreams?.length) {
// Get the proxy host from the instance (e.g., pipedapi.kavin.rocks -> pipedproxy.kavin.rocks)
const instanceUrl = new URL(instance);
const proxyHost = instanceUrl.host.replace('pipedapi', 'pipedproxy').replace('api.', 'proxy.');
return { return {
success: true, success: true,
instance, instance,
streamingUrls: data.audioStreams.map((s: any) => ({ streamingUrls: data.audioStreams.map((s: any) => ({
// Piped streams are already proxied through their CDN
url: s.url, url: s.url,
quality: s.quality, quality: s.quality,
mimeType: s.mimeType, mimeType: s.mimeType,
bitrate: s.bitrate, bitrate: s.bitrate,
proxyHost,
})), })),
metadata: { metadata: {
id: videoId, id: videoId,
@@ -1006,6 +1012,8 @@ export async function fetchFromPiped(videoId: string) {
duration: data.duration, duration: data.duration,
views: data.views, views: data.views,
}, },
// Include HLS stream if available (better for streaming)
hlsUrl: data.hls,
}; };
} }
} catch { } catch {
@@ -1030,14 +1038,19 @@ export async function fetchFromInvidious(videoId: string) {
f.type?.includes("audio") || f.mimeType?.includes("audio") f.type?.includes("audio") || f.mimeType?.includes("audio")
); );
// Use Invidious proxy URLs instead of direct googlevideo URLs
// This bypasses IP restrictions by having Invidious proxy the stream
return { return {
success: true, success: true,
instance, instance,
streamingUrls: audioFormats.map((f: any) => ({ streamingUrls: audioFormats.map((f: any) => ({
url: f.url, // Use the instance's proxy endpoint
url: `${instance}/latest_version?id=${videoId}&itag=${f.itag}`,
directUrl: f.url, // Keep original for reference
bitrate: f.bitrate, bitrate: f.bitrate,
type: f.type, type: f.type,
audioQuality: f.audioQuality, audioQuality: f.audioQuality,
itag: f.itag,
})), })),
metadata: { metadata: {
id: videoId, id: videoId,

117
mod.ts
View File

@@ -164,12 +164,13 @@ async function handler(req: Request): Promise<Response> {
// ============ SEARCH ENDPOINTS ============ // ============ SEARCH ENDPOINTS ============
// YouTube Music Search // YouTube Music Search with YouTube fallback video IDs
if (pathname === "/api/search") { if (pathname === "/api/search") {
const query = searchParams.get("q"); const query = searchParams.get("q");
const filter = searchParams.get("filter") || undefined; const filter = searchParams.get("filter") || undefined;
const continuationToken = searchParams.get("continuationToken") || undefined; const continuationToken = searchParams.get("continuationToken") || undefined;
const ignoreSpelling = searchParams.get("ignore_spelling") === "true"; const ignoreSpelling = searchParams.get("ignore_spelling") === "true";
const withFallback = searchParams.get("fallback") !== "0"; // Enable by default
// Get region from param or detect from IP // Get region from param or detect from IP
let region = searchParams.get("region") || searchParams.get("gl") || undefined; let region = searchParams.get("region") || searchParams.get("gl") || undefined;
@@ -189,6 +190,43 @@ async function handler(req: Request): Promise<Response> {
} }
const results = await ytmusic.search(query || "", filter, continuationToken, ignoreSpelling, region, language); const results = await ytmusic.search(query || "", filter, continuationToken, ignoreSpelling, region, language);
// For songs, add fallback YouTube video IDs (embeddable versions)
if (withFallback && filter === "songs" && results.results?.length > 0) {
// Get YouTube video alternatives for the top results
const enhancedResults = await Promise.all(
results.results.slice(0, 10).map(async (song: any) => {
try {
// Search YouTube for official video
const searchQuery = `${song.title} ${song.artists?.[0]?.name || ''} official`;
const ytResults = await youtubeSearch.searchVideos(searchQuery);
// Find a non-Topic channel video
const alternative = ytResults.results?.find((v: any) =>
v.channel?.name && !v.channel.name.includes('Topic') && v.id
);
if (alternative) {
return {
...song,
fallbackVideoId: alternative.id,
fallbackTitle: alternative.title,
};
}
} catch {
// Ignore errors, just return original
}
return song;
})
);
// Replace first 10 results with enhanced ones, keep the rest
results.results = [
...enhancedResults,
...results.results.slice(10)
];
}
return json({ query, filter, region, language, ...results }); return json({ query, filter, region, language, ...results });
} }
@@ -461,7 +499,7 @@ async function handler(req: Request): Promise<Response> {
if (!audioUrl) { if (!audioUrl) {
return error("Missing url parameter"); return error("Missing url parameter");
} }
return proxyAudio(audioUrl); return proxyAudio(audioUrl, req);
} }
if (pathname === "/api/feed/unauthenticated" || pathname.startsWith("/api/feed/channels=")) { if (pathname === "/api/feed/unauthenticated" || pathname.startsWith("/api/feed/channels=")) {
@@ -601,32 +639,75 @@ async function handler(req: Request): Promise<Response> {
} }
} }
// Audio proxy endpoint to bypass CORS // Audio proxy endpoint to bypass CORS with range request support
async function proxyAudio(url: string): Promise<Response> { async function proxyAudio(url: string, req: Request): Promise<Response> {
try { try {
const response = await fetch(url, { const headers: Record<string, string> = {
headers: { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36", "Accept": "*/*",
}, "Accept-Language": "en-US,en;q=0.9",
}); "Referer": "https://www.youtube.com/",
"Origin": "https://www.youtube.com",
};
if (!response.ok) { // Forward range header for seeking support
return new Response("Failed to fetch audio", { status: 502 }); const rangeHeader = req.headers.get("Range");
if (rangeHeader) {
headers["Range"] = rangeHeader;
} }
const headers = new Headers(); const response = await fetch(url, { headers });
headers.set("Content-Type", response.headers.get("Content-Type") || "audio/mp4");
headers.set("Access-Control-Allow-Origin", "*"); if (!response.ok && response.status !== 206) {
headers.set("Cache-Control", "public, max-age=3600"); console.error(`[Proxy] Upstream error: ${response.status} ${response.statusText}`);
return new Response(`Failed to fetch audio: ${response.status}`, {
status: 502,
headers: corsHeaders
});
}
const responseHeaders = new Headers();
responseHeaders.set("Access-Control-Allow-Origin", "*");
responseHeaders.set("Access-Control-Allow-Methods", "GET, HEAD, OPTIONS");
responseHeaders.set("Access-Control-Allow-Headers", "Range, Content-Type");
responseHeaders.set("Access-Control-Expose-Headers", "Content-Length, Content-Range, Accept-Ranges");
responseHeaders.set("Cache-Control", "public, max-age=3600");
// Copy important headers from upstream
const contentType = response.headers.get("Content-Type");
if (contentType) {
responseHeaders.set("Content-Type", contentType);
} else {
responseHeaders.set("Content-Type", "audio/mp4");
}
const contentLength = response.headers.get("Content-Length"); const contentLength = response.headers.get("Content-Length");
if (contentLength) { if (contentLength) {
headers.set("Content-Length", contentLength); responseHeaders.set("Content-Length", contentLength);
} }
return new Response(response.body, { headers }); const contentRange = response.headers.get("Content-Range");
if (contentRange) {
responseHeaders.set("Content-Range", contentRange);
}
const acceptRanges = response.headers.get("Accept-Ranges");
if (acceptRanges) {
responseHeaders.set("Accept-Ranges", acceptRanges);
} else {
responseHeaders.set("Accept-Ranges", "bytes");
}
return new Response(response.body, {
status: response.status,
headers: responseHeaders
});
} catch (err) { } catch (err) {
return new Response("Proxy error: " + String(err), { status: 502 }); console.error("[Proxy] Error:", err);
return new Response("Proxy error: " + String(err), {
status: 502,
headers: corsHeaders
});
} }
} }

27
ui.ts
View File

@@ -278,7 +278,32 @@ function showTab(t){
function onYouTubeIframeAPIReady(){ function onYouTubeIframeAPIReady(){
yt=new YT.Player('ytplayer',{height:'0',width:'0',playerVars:{autoplay:1,controls:0},events:{onReady:()=>ready=true,onStateChange:onState,onError:onErr}}); yt=new YT.Player('ytplayer',{height:'0',width:'0',playerVars:{autoplay:1,controls:0},events:{onReady:()=>ready=true,onStateChange:onState,onError:onErr}});
} }
function onErr(e){if(e.data===150||e.data===101)setTimeout(()=>play(idx+1),500)} function onErr(e){
if(e.data===150||e.data===101||e.data===100){
// Try fallbackVideoId first, then search YouTube
var s=songs[idx];
if(s&&s.fallbackVideoId&&!s.triedFallback){
console.log('Using fallback:',s.fallbackVideoId);
s.triedFallback=true;
yt.loadVideoById(s.fallbackVideoId);
}else if(s&&!s.triedSearch){
// Search YouTube for playable version
s.triedSearch=true;
searchYouTube(s.title,s.artists?.[0]?.name||'').then(vid=>{
if(vid){console.log('Found alternative:',vid);yt.loadVideoById(vid)}
});
}
}
}
async function searchYouTube(title,artist){
try{
var q=title+' '+artist+' official';
var res=await fetch('/api/yt_search?q='+encodeURIComponent(q)+'&filter=videos');
var data=await res.json();
var alt=data.results?.find(v=>v.channel?.name&&!v.channel.name.includes('Topic')&&v.id);
return alt?.id||null;
}catch(e){return null}
}
function onState(e){ function onState(e){
if(e.data===1){playing=true;document.getElementById('playBtn').textContent='⏸';startProgress()} if(e.data===1){playing=true;document.getElementById('playBtn').textContent='⏸';startProgress()}
else if(e.data===2){playing=false;document.getElementById('playBtn').textContent='▶';stopProgress()} else if(e.data===2){playing=false;document.getElementById('playBtn').textContent='▶';stopProgress()}