Reinstate fallback#342
Conversation
Currently translated at 100.0% (174 of 174 strings) Translation: ytify/web Translate-URL: https://hosted.weblate.org/projects/ytify/web/ru/
Translations update from Hosted Weblate
WalkthroughIntroduces 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
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
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Poem
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.jsoncheckov: 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.tsbiome: error while loading shared libraries: libpthread.so.0: cannot open shared object file: No such file or directory src/locales/ru.jsonbiome: error while loading shared libraries: libpthread.so.0: cannot open shared object file: No such file or directory ✨ Finishing Touches
🧪 Generate unit tests
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. Comment |
There was a problem hiding this comment.
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
📒 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'; | |||
There was a problem hiding this comment.
💡 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.
| 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; |
There was a problem hiding this comment.
🛠️ 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.
| 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(','); | ||
|
|
There was a problem hiding this comment.
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.
| 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.
| 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)); | ||
|
|
There was a problem hiding this comment.
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.
| 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'); | |
| } |
Summary by CodeRabbit
New Features
Localization