Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
95 changes: 95 additions & 0 deletions netlify/edge-functions/fallback.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import { Config, Context } from '@netlify/edge-functions';
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Use the correct Netlify Edge types import (type-only) to avoid runtime import errors.

Netlify Edge typically exposes Context/Config via the edge runtime import and should be imported as types to prevent bundling:

-import { Config, Context } from '@netlify/edge-functions';
+import type { Config, Context } from 'netlify:edge'

If your project uses a different import path (e.g., https://edge.netlify.com), adjust accordingly.


🌐 Web query:

What is the correct TypeScript import path for Context/Config in Netlify Edge Functions?

💡 Result:

Use:

import type { Config, Context } from "@netlify/edge-functions";

(Example in Netlify docs.) [1][2]

Sources:
[1] Netlify Edge Functions API docs.
[2] Netlify Edge Functions get-started guide.


Use type-only import for Netlify Edge types to prevent runtime import errors

-import { Config, Context } from '@netlify/edge-functions';
+import type { Config, Context } from '@netlify/edge-functions';
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import { Config, Context } from '@netlify/edge-functions';
import type { Config, Context } from '@netlify/edge-functions';
🤖 Prompt for AI Agents
In netlify/edge-functions/fallback.ts around line 1, the current import brings
Netlify types into runtime which can cause runtime import errors; change the
statement to a type-only import by replacing it with "import type { Config,
Context } from '@netlify/edge-functions';" so that these symbols are erased at
emit time and do not produce a runtime import. Ensure no other code relies on
these as values.


export default async (_: Request, context: Context) => {

const { id } = context.params;
const cgeo = context.geo.country?.code || 'IN';

if (!id || id.length < 11) return;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Return a 400 response for invalid/missing id instead of undefined.

-  if (!id || id.length < 11) return;
+  if (!id || id.length < 11) {
+    return new Response(JSON.stringify({ error: 'invalid id' }), {
+      status: 400,
+      headers: { 'content-type': 'application/json' },
+    });
+  }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (!id || id.length < 11) return;
if (!id || id.length < 11) {
return new Response(JSON.stringify({ error: 'invalid id' }), {
status: 400,
headers: { 'content-type': 'application/json' },
});
}
🤖 Prompt for AI Agents
In netlify/edge-functions/fallback.ts around line 8, the handler currently
returns undefined when id is missing or shorter than 11; change this to return
an HTTP 400 response. Replace the early return with code that constructs and
returns a Response with status 400 (and a short JSON or text body like {
"error": "invalid or missing id" } or "Invalid or missing id"), and set an
appropriate Content-Type header so callers receive a proper error response.

const raw = process.env.rkeys;
if (!raw) {
throw new Error('Missing environment variable: rkeys');
}
const keys = raw.split(',');

Comment on lines +9 to +14
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

process.env is not available in Netlify Edge; read env via context.env.

This will always be undefined on Edge. Use context.env (or Deno.env if enabled) and validate.

-  const raw = process.env.rkeys;
+  const raw = context.env?.rkeys as string | undefined;
   if (!raw) {
     throw new Error('Missing environment variable: rkeys');
   }
   const keys = raw.split(',');
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const raw = process.env.rkeys;
if (!raw) {
throw new Error('Missing environment variable: rkeys');
}
const keys = raw.split(',');
const raw = context.env?.rkeys as string | undefined;
if (!raw) {
throw new Error('Missing environment variable: rkeys');
}
const keys = raw.split(',');
🤖 Prompt for AI Agents
In netlify/edge-functions/fallback.ts around lines 9 to 14, the code reads
process.env.rkeys which is unavailable in Netlify Edge; change to read rkeys
from the Edge function context (e.g., const raw = context?.env?.rkeys) or, if
Deno environment is enabled, fall back to Deno.env.get('rkeys'); validate that
raw exists and throw the same error if missing, then split into keys (trim and
filter out empty entries) so the Edge runtime correctly obtains and validates
the environment variable.

shuffle(keys);

const streamData = await fetcher(cgeo, keys, id);
const data = {
title: streamData.title,
uploader: streamData.channelTitle,
uploaderUrl: '/channel/' + streamData.channelId,
duration: streamData.lengthSeconds,
audioStreams: streamData.adaptiveFormats
.filter(_ => _.mimeType.startsWith('audio'))
.map(_ => ({
url: _.url + '&fallback',
quality: `${Math.floor(_.bitrate / 1000)} kbps`,
mimeType: _.mimeType,
codec: _.mimeType.split('codecs="')[1]?.split('"')[0],
bitrate: _.bitrate,
contentLength: _.contentLength
})),
videoStreams: streamData.adaptiveFormats
.filter(_ => _.mimeType.startsWith('video'))
.map(_ => ({
url: _.url + '&fallback', // fallback parameter to indicate it's source
resolution: _.qualityLabel,
codec: _.mimeType,
})),
relatedStreams: [], // empty array for compatibility
subtitles: [], // empty array for compatibility
livestream: streamData.isLiveContent
};

return new Response(JSON.stringify(data), {
headers: { 'content-type': 'application/json' },
});
};

export const config: Config = {
path: '/streams/:id',
};

const host = 'yt-api.p.rapidapi.com';
export const fetcher = (cgeo: string, keys: string[], id: string): Promise<{
title: string,
channelTitle: string,
channelId: string,
lengthSeconds: number,
isLiveContent: boolean,
adaptiveFormats: {
mimeType: string,
url: string,
bitrate: number,
contentLength: string,
qualityLabel: string
}[]
}> => fetch(`https://${host}/dl?id=${id}&cgeo=${cgeo}`, {
headers: {
'X-RapidAPI-Key': <string>keys.shift(),
'X-RapidAPI-Host': host
}
})
.then(res => res.json())
.then(data => {
if (data && 'adaptiveFormats' in data && data.adaptiveFormats.length)
return data;
else throw new Error(data.message);
})
.catch(() => fetcher(cgeo, keys, id));

Comment on lines +55 to +81
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Infinite retry when keys are exhausted; last call uses undefined header.

keys.shift() can return undefined and the .catch recursively calls fetcher indefinitely. Replace with a bounded, iterative approach and check res.ok.

-export const fetcher = (cgeo: string, keys: string[], id: string): Promise<{
+export const fetcher = async (cgeo: string, keys: string[], id: string): Promise<{
   title: string,
   channelTitle: string,
   channelId: string,
   lengthSeconds: number,
   isLiveContent: boolean,
   adaptiveFormats: {
     mimeType: string,
     url: string,
     bitrate: number,
     contentLength: string,
     qualityLabel: string
   }[]
-}> => fetch(`https://${host}/dl?id=${id}&cgeo=${cgeo}`, {
-  headers: {
-    'X-RapidAPI-Key': <string>keys.shift(),
-    'X-RapidAPI-Host': host
-  }
-})
-  .then(res => res.json())
-  .then(data => {
-    if (data && 'adaptiveFormats' in data && data.adaptiveFormats.length)
-      return data;
-    else throw new Error(data.message);
-  })
-  .catch(() => fetcher(cgeo, keys, id));
+}> => {
+  if (!keys.length) throw new Error('No API keys available');
+  for (let i = 0; i < keys.length; i++) {
+    const apiKey = keys[i];
+    try {
+      const res = await fetch(`https://${host}/dl?id=${id}&cgeo=${cgeo}`, {
+        headers: {
+          'X-RapidAPI-Key': apiKey,
+          'X-RapidAPI-Host': host,
+        },
+      });
+      if (!res.ok) throw new Error(`RapidAPI error: ${res.status}`);
+      const data = await res.json();
+      if (data && Array.isArray(data.adaptiveFormats) && data.adaptiveFormats.length) return data;
+      throw new Error(data?.message || 'No adaptive formats');
+    } catch (e) {
+      if (i === keys.length - 1) throw e;
+      // try next key
+    }
+  }
+  throw new Error('Exhausted API keys');
+}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export const fetcher = (cgeo: string, keys: string[], id: string): Promise<{
title: string,
channelTitle: string,
channelId: string,
lengthSeconds: number,
isLiveContent: boolean,
adaptiveFormats: {
mimeType: string,
url: string,
bitrate: number,
contentLength: string,
qualityLabel: string
}[]
}> => fetch(`https://${host}/dl?id=${id}&cgeo=${cgeo}`, {
headers: {
'X-RapidAPI-Key': <string>keys.shift(),
'X-RapidAPI-Host': host
}
})
.then(res => res.json())
.then(data => {
if (data && 'adaptiveFormats' in data && data.adaptiveFormats.length)
return data;
else throw new Error(data.message);
})
.catch(() => fetcher(cgeo, keys, id));
export const fetcher = async (cgeo: string, keys: string[], id: string): Promise<{
title: string,
channelTitle: string,
channelId: string,
lengthSeconds: number,
isLiveContent: boolean,
adaptiveFormats: {
mimeType: string,
url: string,
bitrate: number,
contentLength: string,
qualityLabel: string
}[]
}> => {
if (!keys.length) throw new Error('No API keys available');
for (let i = 0; i < keys.length; i++) {
const apiKey = keys[i];
try {
const res = await fetch(`https://${host}/dl?id=${id}&cgeo=${cgeo}`, {
headers: {
'X-RapidAPI-Key': apiKey,
'X-RapidAPI-Host': host,
},
});
if (!res.ok) throw new Error(`RapidAPI error: ${res.status}`);
const data = await res.json();
if (data && Array.isArray(data.adaptiveFormats) && data.adaptiveFormats.length) {
return data;
}
throw new Error(data?.message || 'No adaptive formats');
} catch (e) {
if (i === keys.length - 1) throw e;
// try next key
}
}
throw new Error('Exhausted API keys');
}



export function shuffle(array: string[]) {
let currentIndex = array.length;

while (currentIndex != 0) {

const randomIndex = Math.floor(Math.random() * currentIndex);
currentIndex--;

[array[currentIndex], array[randomIndex]] = [
array[randomIndex], array[currentIndex]];
}
}
3 changes: 2 additions & 1 deletion src/locales/ru.json
Original file line number Diff line number Diff line change
Expand Up @@ -173,5 +173,6 @@
"settings_audio_quality": "Качество звука",
"settings_prefetch": "Предскачивание очереди",
"settings_enforce_piped": "Приниудительно Piped для проигрывания",
"settings_reload": "Перегрузить страницу для применения изменений"
"settings_reload": "Перегрузить страницу для применения изменений",
"settings_jiosaavn": "Предпочитать JioSaavn для Музыки"
}