forked from github-mirror/Verome-API
Update Deno API with fallback mechanism
This commit is contained in:
@@ -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
15
lib.ts
@@ -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
117
mod.ts
@@ -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
27
ui.ts
@@ -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()}
|
||||||
|
|||||||
Reference in New Issue
Block a user