Skip to content

Reinstate fallback#342

Merged
n-ce merged 4 commits into
mainfrom
Reinstate-fallback
Sep 7, 2025
Merged

Reinstate fallback#342
n-ce merged 4 commits into
mainfrom
Reinstate-fallback

Conversation

@n-ce
Copy link
Copy Markdown
Owner

@n-ce n-ce commented Sep 7, 2025

Summary by CodeRabbit

  • New Features

    • Added an edge-powered /streams/:id endpoint that returns video metadata (title, uploader, duration, livestream flag) and curated audio/video stream options with quality, codec, bitrate, and size. Region is auto-detected for better results, with robust fallback behavior to ensure reliable responses.
  • Localization

    • Added Russian translation for the “Prefer JioSaavn for Music” setting.

Yurt Page and others added 4 commits September 7, 2025 00:05
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Sep 7, 2025

Walkthrough

Introduces a Netlify Edge Function mapped to /streams/:id that fetches YouTube metadata via RapidAPI using rotated API keys, builds audio/video stream arrays from adaptive formats, and returns a JSON payload. Adds a Russian locale string for a JioSaavn preference setting.

Changes

Cohort / File(s) Summary of Changes
Edge Function: Streams Fallback
netlify/edge-functions/fallback.ts
New Edge Function handling GET /streams/:id. Reads id and geo, loads rkeys, shuffles keys, fetches YouTube metadata from yt-api.p.rapidapi.com with per-key retries. Builds JSON with title/uploader/duration, audioStreams and videoStreams, empty relatedStreams/subtitles, and livestream flag. Exports fetcher, shuffle, config, and default handler.
Localization (RU)
src/locales/ru.json
Adds settings_jiosaavn: "Предпочитать JioSaavn для Музыки". Adjusts trailing comma to maintain valid JSON.

Sequence Diagram(s)

sequenceDiagram
    autonumber
    participant C as Client
    participant E as Netlify Edge (fallback.ts)
    participant R as RapidAPI YT Endpoint

    C->>E: GET /streams/:id
    E->>E: Extract id, geo (default 'IN')
    E->>E: Load rkeys, shuffle
    alt For each key until success
        E->>R: GET /video/info?id=:id&geo=:geo (with current key)
        R-->>E: Metadata JSON
        alt Payload valid
            E->>E: Build audioStreams, videoStreams
            E-->>C: 200 JSON {title, uploader, duration, audioStreams, videoStreams, ...}
        else Missing fields / error
            E->>E: Try next key
        end
    else All keys failed
        E-->>C: 502/500 error JSON
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Poem

I twitch my ears at streaming skies,
New keys hop round in shuffled guise;
From geo winds the packets fly,
We fetch, retry, and never sigh.
In Russian fields, a song to choose—
JioSaavn blooms, no time to lose.
🐇✨

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 Checkov (3.2.334)
src/locales/ru.json

checkov: error while loading shared libraries: libdl.so.2: cannot open shared object file: No such file or directory

🔧 Biome (2.1.2)
netlify/edge-functions/fallback.ts

biome: error while loading shared libraries: libpthread.so.0: cannot open shared object file: No such file or directory

src/locales/ru.json

biome: error while loading shared libraries: libpthread.so.0: cannot open shared object file: No such file or directory

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch Reinstate-fallback

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🧹 Nitpick comments (3)
src/locales/ru.json (1)

177-178: Fix casing in the new Russian string.

"Музыки" should be lowercase in Russian UI text.

-    "settings_jiosaavn": "Предпочитать JioSaavn для Музыки"
+    "settings_jiosaavn": "Предпочитать JioSaavn для музыки"
netlify/edge-functions/fallback.ts (2)

17-43: Normalize numeric types and codec extraction; make fallback flag robust.

  • Ensure duration is a number.
  • For audio/video URLs, append ?fallback=1 or &fallback=1 depending on existing query.
  • Extract codec for video same as audio; avoid returning the whole mimeType.
  • Prefer numeric contentLength.
-    duration: streamData.lengthSeconds,
+    duration: Number(streamData.lengthSeconds),
     audioStreams: streamData.adaptiveFormats
       .filter(_ => _.mimeType.startsWith('audio'))
       .map(_ => ({
-        url: _.url + '&fallback',
+        url: `${_.url}${_.url.includes('?') ? '&' : '?'}fallback=1`,
         quality: `${Math.floor(_.bitrate / 1000)} kbps`,
         mimeType: _.mimeType,
-        codec: _.mimeType.split('codecs="')[1]?.split('"')[0],
+        codec: _.mimeType.split('codecs="')[1]?.split('"')[0] || (_.mimeType.includes('webm') ? 'opus' : 'aac'),
         bitrate: _.bitrate,
-        contentLength: _.contentLength
+        contentLength: Number(_.contentLength) || undefined
       })),
     videoStreams: streamData.adaptiveFormats
       .filter(_ => _.mimeType.startsWith('video'))
       .map(_ => ({
-        url: _.url + '&fallback', // fallback parameter to indicate it's source
+        url: `${_.url}${_.url.includes('?') ? '&' : '?'}fallback=1`, // mark fallback source
         resolution: _.qualityLabel,
-        codec: _.mimeType,
+        codec: _.mimeType.split('codecs="')[1]?.split('"')[0] || (_.mimeType.includes('webm') ? 'vp9' : 'h264'),
       })),

85-95: Use strict inequality in shuffle loop.

-  while (currentIndex != 0) {
+  while (currentIndex !== 0) {
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 64e68ed and ab40d51.

📒 Files selected for processing (2)
  • netlify/edge-functions/fallback.ts (1 hunks)
  • src/locales/ru.json (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
netlify/edge-functions/fallback.ts (1)
src/modules/getStreamData.ts (2)
  • id (3-132)
  • api (22-63)

@@ -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.

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.

Comment on lines +9 to +14
const raw = process.env.rkeys;
if (!raw) {
throw new Error('Missing environment variable: rkeys');
}
const keys = raw.split(',');

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.

Comment on lines +55 to +81
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));

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');
}

@n-ce n-ce merged commit 6f0d654 into main Sep 7, 2025
2 checks passed
@n-ce n-ce deleted the Reinstate-fallback branch September 7, 2025 08:15
@n-ce n-ce restored the Reinstate-fallback branch September 7, 2025 08:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants