mirror of
https://github.com/Kirazul/Verome-API.git
synced 2026-03-08 00:05:22 +00:00
Update Deno API with fallback mechanism
This commit is contained in:
@@ -15,7 +15,11 @@
|
||||
"deploy": {
|
||||
"project": "85252de3-9b36-4d8b-b250-e491b4131838",
|
||||
"exclude": [
|
||||
"**/node_modules"
|
||||
"**/node_modules",
|
||||
"Music/**",
|
||||
"virome-music/**",
|
||||
".git/**",
|
||||
".vscode/**"
|
||||
],
|
||||
"include": [],
|
||||
"entrypoint": "mod.ts"
|
||||
|
||||
15
lib.ts
15
lib.ts
@@ -989,14 +989,20 @@ export async function fetchFromPiped(videoId: string) {
|
||||
const data = await response.json();
|
||||
|
||||
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 {
|
||||
success: true,
|
||||
instance,
|
||||
streamingUrls: data.audioStreams.map((s: any) => ({
|
||||
// Piped streams are already proxied through their CDN
|
||||
url: s.url,
|
||||
quality: s.quality,
|
||||
mimeType: s.mimeType,
|
||||
bitrate: s.bitrate,
|
||||
proxyHost,
|
||||
})),
|
||||
metadata: {
|
||||
id: videoId,
|
||||
@@ -1006,6 +1012,8 @@ export async function fetchFromPiped(videoId: string) {
|
||||
duration: data.duration,
|
||||
views: data.views,
|
||||
},
|
||||
// Include HLS stream if available (better for streaming)
|
||||
hlsUrl: data.hls,
|
||||
};
|
||||
}
|
||||
} catch {
|
||||
@@ -1030,14 +1038,19 @@ export async function fetchFromInvidious(videoId: string) {
|
||||
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 {
|
||||
success: true,
|
||||
instance,
|
||||
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,
|
||||
type: f.type,
|
||||
audioQuality: f.audioQuality,
|
||||
itag: f.itag,
|
||||
})),
|
||||
metadata: {
|
||||
id: videoId,
|
||||
|
||||
117
mod.ts
117
mod.ts
@@ -164,12 +164,13 @@ async function handler(req: Request): Promise<Response> {
|
||||
|
||||
// ============ SEARCH ENDPOINTS ============
|
||||
|
||||
// YouTube Music Search
|
||||
// YouTube Music Search with YouTube fallback video IDs
|
||||
if (pathname === "/api/search") {
|
||||
const query = searchParams.get("q");
|
||||
const filter = searchParams.get("filter") || undefined;
|
||||
const continuationToken = searchParams.get("continuationToken") || undefined;
|
||||
const ignoreSpelling = searchParams.get("ignore_spelling") === "true";
|
||||
const withFallback = searchParams.get("fallback") !== "0"; // Enable by default
|
||||
|
||||
// Get region from param or detect from IP
|
||||
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);
|
||||
|
||||
// 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 });
|
||||
}
|
||||
|
||||
@@ -461,7 +499,7 @@ async function handler(req: Request): Promise<Response> {
|
||||
if (!audioUrl) {
|
||||
return error("Missing url parameter");
|
||||
}
|
||||
return proxyAudio(audioUrl);
|
||||
return proxyAudio(audioUrl, req);
|
||||
}
|
||||
|
||||
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
|
||||
async function proxyAudio(url: string): Promise<Response> {
|
||||
// Audio proxy endpoint to bypass CORS with range request support
|
||||
async function proxyAudio(url: string, req: Request): Promise<Response> {
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
headers: {
|
||||
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
|
||||
},
|
||||
});
|
||||
const headers: Record<string, string> = {
|
||||
"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",
|
||||
"Accept": "*/*",
|
||||
"Accept-Language": "en-US,en;q=0.9",
|
||||
"Referer": "https://www.youtube.com/",
|
||||
"Origin": "https://www.youtube.com",
|
||||
};
|
||||
|
||||
if (!response.ok) {
|
||||
return new Response("Failed to fetch audio", { status: 502 });
|
||||
// Forward range header for seeking support
|
||||
const rangeHeader = req.headers.get("Range");
|
||||
if (rangeHeader) {
|
||||
headers["Range"] = rangeHeader;
|
||||
}
|
||||
|
||||
const headers = new Headers();
|
||||
headers.set("Content-Type", response.headers.get("Content-Type") || "audio/mp4");
|
||||
headers.set("Access-Control-Allow-Origin", "*");
|
||||
headers.set("Cache-Control", "public, max-age=3600");
|
||||
const response = await fetch(url, { headers });
|
||||
|
||||
if (!response.ok && response.status !== 206) {
|
||||
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");
|
||||
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) {
|
||||
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(){
|
||||
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){
|
||||
if(e.data===1){playing=true;document.getElementById('playBtn').textContent='⏸';startProgress()}
|
||||
else if(e.data===2){playing=false;document.getElementById('playBtn').textContent='▶';stopProgress()}
|
||||
|
||||
Reference in New Issue
Block a user