Conversation
Migrated all cloudsync related functions to edge-functions to resolve environment issues
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the You can disable this status message by setting the WalkthroughIntroduces three new Netlify Edge Functions for artist lookup, list preview generation, and content synchronization. Migrates three existing Netlify Functions (sync-related handlers) from serverless runtime to Edge Functions. Removes corresponding serverless function implementations. Changes
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes
Poem
Pre-merge checks and finishing touches✅ Passed checks (3 passed)
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 |
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
There was a problem hiding this comment.
Actionable comments posted: 7
♻️ Duplicate comments (1)
netlify/edge-functions/syncCleanup.ts (1)
5-13: Same type definition issues as other files.Duplicated types and missing comma after
duration: string. See the comment onsyncContent.tsfor the recommended refactor to extract shared types.
🧹 Nitpick comments (8)
netlify/edge-functions/listPreview.ts (2)
5-6: Query string parsing is fragile and may not work as expected.The parsing logic
url.search.substring(1).split('=')assumes the query string format is?type=id, but this doesn't follow standard URL query parameter conventions (?key=value). If the query is?playlist=ABC123, this would settype = "playlist"andid = "ABC123", which seems intentional but non-standard.Consider using
URLSearchParamsfor more robust parsing:- const [type, id] = url.search.substring(1).split('='); + const params = new URLSearchParams(url.search); + const type = params.keys().next().value || ''; + const id = params.get(type) || '';Or if specific parameters are expected:
const type = params.get('type') || ''; const id = params.get('id') || '';
22-22: PreferencodeURIComponentoverencodeURIfor query parameters.
encodeURIdoes not encode characters like&,=,+, which could break URL parsing. UseencodeURIComponentfor encoding values that will be used in query parameters.-<script>location.replace('/?e=' + encodeURI(location.pathname + location.search))</script></html> +<script>location.replace('/?e=' + encodeURIComponent(location.pathname + location.search))</script></html>netlify/edge-functions/syncBulkTracks.ts (1)
61-66: Consider reporting skipped tracks to aid debugging.Tracks without a valid
idare silently skipped. While this prevents failures, it may hide data quality issues from the client. Consider including a count of skipped tracks in the response.+ let skippedCount = 0; // Update or add tracks for (const track of addedTrackItems) { if (track && track.id) { userTracks[track.id] = track; + } else { + skippedCount++; } } ... return new Response(JSON.stringify({ - message: `Successfully synced ${addedTrackItems.length} tracks for user ${userId}` + message: `Successfully synced ${addedTrackItems.length - skippedCount} tracks for user ${userId}`, + skipped: skippedCount }), {netlify/edge-functions/artist.ts (2)
93-104: Consider adding timeout and caching for external API calls.The fetch to YouTube Music API has no timeout, which could cause the edge function to hang. Also, artist data is relatively stable and could benefit from caching to reduce API load.
- return fetch(YOUTUBE_MUSIC_API_URL, { + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), 10000); // 10s timeout + + return fetch(YOUTUBE_MUSIC_API_URL, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(requestBody), + signal: controller.signal, }) .then((response) => { + clearTimeout(timeoutId); if (!response.ok) {
163-166: Inconsistent config formatting compared to other edge functions.The config object has inconsistent indentation compared to the patterns in other edge functions like
listPreview.tsandsyncBulkTracks.ts.export const config: Config = { - path: '/artist/:id', - }; + path: '/artist/:id', +};netlify/edge-functions/syncContent.ts (2)
17-26: Consider using Context.params instead of manual URL parsing.Other edge functions like
syncBulkTracks.tsusecontext.paramsto extract route parameters. This function manually parses the URL path, which is less robust and inconsistent with the codebase pattern.-export default async (req: Request): Promise<Response> => { +export default async (req: Request, context: Context): Promise<Response> => { // GET: Retrieve content by timestamp if (req.method === 'GET') { - const url = new URL(req.url); - const pathParts = url.pathname.split('/'); - const timestamp = pathParts[pathParts.length - 1]; + const timestamp = context.params.timestamp; if (!timestamp) { return new Response('Timestamp is required', { status: 400 }); }Also update the import:
-import type { Config } from "@netlify/edge-functions"; +import type { Config, Context } from "@netlify/edge-functions";
28-42: Duplicated store initialization error handling.The store initialization try-catch block is duplicated for GET and POST handlers. Consider extracting to a helper function.
const getContentStore = () => { try { return { store: getStore('content'), error: null }; } catch (storeError) { console.error("Failed to initialize content store:", storeError); return { store: null, error: new Response(JSON.stringify({ error: 'Netlify Blobs configuration error', details: 'Could not initialize "content" store.', rawError: String(storeError) }), { status: 500, headers: { 'Content-Type': 'application/json' } }) }; } };Also applies to: 78-91
netlify/edge-functions/syncCleanup.ts (1)
42-76: Consider batching deletions to avoid timeouts on large datasets.The cleanup iterates through all blobs sequentially with individual delete operations. For large datasets, this could exceed edge function time limits. Consider batching or using
Promise.allfor parallel deletions.// Example: Batch deletions in parallel chunks const BATCH_SIZE = 10; const blobsToDelete = userMetaBlobs.filter(/* inactive criteria */); for (let i = 0; i < blobsToDelete.length; i += BATCH_SIZE) { const batch = blobsToDelete.slice(i, i + BATCH_SIZE); await Promise.all(batch.map(blob => Promise.all([ metaStore.delete(blob.key), trackStore.delete(blob.key) ]))); }Also applies to: 85-97
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (8)
netlify/edge-functions/artist.ts(1 hunks)netlify/edge-functions/listPreview.ts(1 hunks)netlify/edge-functions/syncBulkTracks.ts(1 hunks)netlify/edge-functions/syncCleanup.ts(1 hunks)netlify/edge-functions/syncContent.ts(1 hunks)netlify/functions/syncBulkTracks.ts(0 hunks)netlify/functions/syncCleanup.ts(0 hunks)netlify/functions/syncContent.ts(0 hunks)
💤 Files with no reviewable changes (3)
- netlify/functions/syncBulkTracks.ts
- netlify/functions/syncCleanup.ts
- netlify/functions/syncContent.ts
🧰 Additional context used
🧬 Code graph analysis (4)
netlify/edge-functions/listPreview.ts (5)
netlify/edge-functions/artist.ts (1)
config(164-166)netlify/edge-functions/syncBulkTracks.ts (1)
config(89-91)netlify/edge-functions/syncCleanup.ts (1)
config(123-125)netlify/edge-functions/syncContent.ts (1)
config(118-120)netlify/edge-functions/linkPreview.ts (1)
config(42-44)
netlify/edge-functions/syncBulkTracks.ts (4)
netlify/edge-functions/syncContent.ts (2)
req(17-116)config(118-120)netlify/edge-functions/artist.ts (1)
config(164-166)netlify/edge-functions/listPreview.ts (1)
config(28-30)netlify/edge-functions/syncCleanup.ts (1)
config(123-125)
netlify/edge-functions/syncCleanup.ts (4)
netlify/edge-functions/artist.ts (1)
config(164-166)netlify/edge-functions/listPreview.ts (1)
config(28-30)netlify/edge-functions/syncBulkTracks.ts (1)
config(89-91)netlify/edge-functions/syncContent.ts (1)
config(118-120)
netlify/edge-functions/syncContent.ts (2)
netlify/edge-functions/syncBulkTracks.ts (2)
req(21-87)config(89-91)netlify/edge-functions/blobs.ts (2)
req(5-42)config(45-47)
🔇 Additional comments (2)
netlify/edge-functions/syncBulkTracks.ts (1)
21-86: LGTM: Solid error handling and validation.The function has comprehensive validation (method, body, userId, payload structure) and properly structured error responses with appropriate status codes. The nested try-catch for store initialization is a good pattern for distinguishing configuration errors from runtime errors.
netlify/edge-functions/syncCleanup.ts (1)
21-109: Good structured cleanup logic with comprehensive logging.The two-phase approach (user retention, content cleanup) is well-organized with clear logging for debugging. Error handling prevents a single failure from aborting the entire cleanup cycle.
netlify/edge-functions/artist.ts
Outdated
| const requestBody = { | ||
| browseId: artistId, | ||
| context: { | ||
| client: { | ||
| clientName: 'WEB_REMIX', | ||
| clientVersion: '1.20250915.03.00', | ||
| gl: countryCode, | ||
| }, | ||
| }, | ||
| }; |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
YouTube Music API clientVersion requirements 2024 2025 outdated
💡 Result:
Short answer: YouTube Music's undocumented "innertube" API requires a current clientName/clientVersion in requests; these values are time-sensitive and Google can and does change/validate them, so using an outdated clientVersion often causes requests to fail or return degraded data. Keep clientVersion matched to a recent official YouTube/YouTube Music app build (or use the official YouTube Data API where possible) and monitor YouTube's developer revision history for breaking changes. [1][2]
Sources:
- YouTube API Services — Required Minimum Functionality (notes about API client identity/identification and that requirements change). [1]
- Public reverse-engineered clients / code examples showing innertube clientVersion strings used for YouTube Music/YouTube requests (examples of how clientVersion is set and updated). [2]
Hardcoded clientVersion should be externalized or regularly updated.
The clientVersion: '1.20250915.03.00' is hardcoded and YouTube Music's innertube API validates client versions; outdated versions cause requests to fail or return degraded data. Either move this to configuration, implement version detection from the YouTube app, or establish a process to update it when YouTube requires newer versions.
🤖 Prompt for AI Agents
In netlify/edge-functions/artist.ts around lines 82 to 91, the clientVersion
value is hardcoded which will break when YouTube changes/validates versions;
replace the literal with a configurable value (read from an environment variable
or a shared config file), add a sensible default/fallback and a short validation
check (e.g., non-empty string and pattern) so failures surface quickly, and
document/update the repo README or config doc with the process for keeping
clientVersion current (or implement an automated fetch/upgrade step) so
maintainers can update it when YouTube requires a newer version.
netlify/edge-functions/artist.ts
Outdated
| .then((data: ResponseData) => { | ||
| const contents = data.contents.singleColumnBrowseResultsRenderer.tabs[0].tabRenderer.content.sectionListRenderer.contents; | ||
|
|
||
| const artistName = data.header.musicImmersiveHeaderRenderer.title.runs[0].text; | ||
|
|
||
| const topSongsShelf = contents.find( | ||
| (item): item is MusicShelfRendererContainer => | ||
| 'musicShelfRenderer' in item && | ||
| item.musicShelfRenderer.title.runs[0].text === 'Top songs' | ||
| ); | ||
| const playlistId = topSongsShelf?.musicShelfRenderer.contents[0].musicResponsiveListItemRenderer.flexColumns[0].musicResponsiveListItemFlexColumnRenderer.text.runs[0].navigationEndpoint?.watchEndpoint.playlistId; |
There was a problem hiding this comment.
Potential null pointer exceptions from deeply nested access.
The response parsing accesses deeply nested properties without null checks. If the YouTube API response structure changes or returns incomplete data, this will throw runtime errors instead of returning gracefully.
Add optional chaining throughout the parsing logic:
- const contents = data.contents.singleColumnBrowseResultsRenderer.tabs[0].tabRenderer.content.sectionListRenderer.contents;
- const artistName = data.header.musicImmersiveHeaderRenderer.title.runs[0].text;
+ const contents = data.contents?.singleColumnBrowseResultsRenderer?.tabs?.[0]?.tabRenderer?.content?.sectionListRenderer?.contents;
+ const artistName = data.header?.musicImmersiveHeaderRenderer?.title?.runs?.[0]?.text;
+
+ if (!contents || !artistName) {
+ return new Response(JSON.stringify({ error: 'Unexpected API response structure' }), {
+ status: 502,
+ headers: { 'content-type': 'application/json' },
+ });
+ }🤖 Prompt for AI Agents
In netlify/edge-functions/artist.ts around lines 106 to 116, the code performs
deeply nested property access on the YouTube response which can throw if any
node is missing; update the parsing to use optional chaining and nullish
coalescing and add guards: replace direct property chains with optional chaining
(?.) and provide safe defaults (?? []) or undefined where appropriate, check
that contents and arrays exist and have expected length before indexing, verify
topSongsShelf is truthy before accessing its musicShelfRenderer.contents[0], and
treat playlistId extraction with optional chaining so the function returns
gracefully when fields are absent.
| return new Response(` | ||
| <!doctype html> | ||
| <html lang="en"> | ||
| <head> | ||
| <meta charset="UTF-8"> | ||
| <meta name="description" | ||
| content="View ${type} in ytify"> | ||
| <meta name="author" content="n-ce"> | ||
| <meta property="og:type" content="website"> | ||
| <meta property="og:title" content="${type} | ytify"> | ||
| <meta property="og:url" content="https://ytify.pp.ua"> | ||
| <meta property="og:site_name" content="ytify"> | ||
| <meta property="og:description" content="View ${id} - ${type} in ytify"> | ||
| <title>${type} | ytify</title> | ||
| </head> | ||
| <script>location.replace('/?e=' + encodeURI(location.pathname + location.search))</script></html> |
There was a problem hiding this comment.
XSS vulnerability: User input is directly interpolated into HTML without sanitization.
The type and id values from the query string are directly embedded into HTML meta tags and content. A malicious query like ?"><script>alert(1)</script>=test could inject arbitrary HTML/JavaScript.
Sanitize or escape the values before embedding in HTML:
+const escapeHtml = (str: string) =>
+ str.replace(/&/g, '&')
+ .replace(/</g, '<')
+ .replace(/>/g, '>')
+ .replace(/"/g, '"')
+ .replace(/'/g, ''');
+
+const safeType = escapeHtml(type);
+const safeId = escapeHtml(id);
+
return new Response(`
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="description"
- content="View ${type} in ytify">
+ content="View ${safeType} in ytify">
...Committable suggestion skipped: line range outside the PR's diff.
| type CollectionItem = { | ||
| id: string, | ||
| title: string, | ||
| author: string, | ||
| duration: string | ||
| authorId: string, | ||
| albumId?: string, | ||
| plays?: number | ||
| } |
There was a problem hiding this comment.
Missing comma in type definition causes syntax error.
There's a missing comma after duration: string on line 9, which would cause a TypeScript compilation error.
type CollectionItem = {
id: string,
title: string,
author: string,
- duration: string
+ duration: string,
authorId: string,
albumId?: string,
plays?: number
}📝 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.
| type CollectionItem = { | |
| id: string, | |
| title: string, | |
| author: string, | |
| duration: string | |
| authorId: string, | |
| albumId?: string, | |
| plays?: number | |
| } | |
| type CollectionItem = { | |
| id: string, | |
| title: string, | |
| author: string, | |
| duration: string, | |
| authorId: string, | |
| albumId?: string, | |
| plays?: number | |
| } |
🤖 Prompt for AI Agents
In netlify/edge-functions/syncBulkTracks.ts around lines 5 to 13, the TypeScript
type CollectionItem is missing a comma after the property declaration duration:
string which causes a compilation syntax error; fix it by adding a trailing
comma after duration: string so the property list is properly separated (then
re-run type checking to confirm the error is resolved).
| // Access the relevant stores | ||
| const metaStore = getStore('meta'); | ||
| const trackStore = getStore('tracks'); |
There was a problem hiding this comment.
Store initialization outside try-catch could cause unhandled exceptions.
The metaStore and trackStore are initialized outside the main try-catch block. If getStore throws (e.g., Netlify Blobs not configured), the function will crash without returning a proper error response.
+ let metaStore, trackStore;
+ try {
+ metaStore = getStore('meta');
+ trackStore = getStore('tracks');
+ } catch (storeError) {
+ console.error("Failed to initialize stores:", storeError);
+ return new Response(JSON.stringify({
+ error: 'Netlify Blobs configuration error',
+ details: 'Could not initialize required stores.',
+ rawError: String(storeError)
+ }), {
+ status: 500,
+ headers: { 'Content-Type': 'application/json' }
+ });
+ }
+
- // Access the relevant stores
- const metaStore = getStore('meta');
- const trackStore = getStore('tracks');📝 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.
| // Access the relevant stores | |
| const metaStore = getStore('meta'); | |
| const trackStore = getStore('tracks'); | |
| let metaStore, trackStore; | |
| try { | |
| metaStore = getStore('meta'); | |
| trackStore = getStore('tracks'); | |
| } catch (storeError) { | |
| console.error("Failed to initialize stores:", storeError); | |
| return new Response(JSON.stringify({ | |
| error: 'Netlify Blobs configuration error', | |
| details: 'Could not initialize required stores.', | |
| rawError: String(storeError) | |
| }), { | |
| status: 500, | |
| headers: { 'Content-Type': 'application/json' } | |
| }); | |
| } |
🤖 Prompt for AI Agents
In netlify/edge-functions/syncCleanup.ts around lines 25 to 27, the calls to
getStore('meta') and getStore('tracks') are created outside the main try-catch
and can throw, causing an unhandled crash; move those getStore calls into the
try block (or wrap them in their own try/catch) so any exceptions are caught and
return a proper error response (e.g., log the error and return a 500/appropriate
Response) instead of letting the function crash.
| // Array to collect track maps from active users for the GC check | ||
| const activeUserTrackMaps: Collection[] = []; |
There was a problem hiding this comment.
activeUserTrackMaps is collected but never used.
The array activeUserTrackMaps is populated with track maps from active users, but it's never used after collection. The comment mentions "GC reference" but no garbage collection logic consumes this data.
Either remove the dead code or implement the intended GC logic:
- // Array to collect track maps from active users for the GC check
- const activeUserTrackMaps: Collection[] = [];
...
- // Collect all track references from the user's collections
- try {
- // Fetch the user's track map for GC reference
- const userTrackMap: Collection = await trackStore.get(userIdHash, { type: 'json' }) || {};
- if (userTrackMap) {
- activeUserTrackMaps.push(userTrackMap);
- }
-
- } catch (error) {
- console.error(`Could not process track map for active user ${userIdHash}.`, error);
- }Also applies to: 66-75
🤖 Prompt for AI Agents
In netlify/edge-functions/syncCleanup.ts around lines 29-30 (and similarly lines
66-75), the activeUserTrackMaps array is being built but never used; either
remove the dead variable and the pushes that populate it, or implement the
intended GC retention logic by using the collected track maps during the
garbage-collection check (e.g., pass activeUserTrackMaps to the GC routine or
hold references while computing live vs. collectible tracks), and ensure any
dependent code (comments, function signatures) is updated to reflect the chosen
approach.
| type CollectionItem = { | ||
| id: string, | ||
| title: string, | ||
| author: string, | ||
| duration: string | ||
| authorId: string, | ||
| albumId?: string, | ||
| plays?: number | ||
| } | ||
|
|
||
| type Collection = { [index: string]: CollectionItem }; |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major
Duplicated type definitions across edge functions.
CollectionItem and Collection types are duplicated in syncBulkTracks.ts, syncContent.ts, and syncCleanup.ts. This violates DRY and risks divergence. Also, there's a missing comma after duration: string on line 9.
Extract shared types to a common module:
// netlify/edge-functions/types.ts
export type CollectionItem = {
id: string,
title: string,
author: string,
duration: string, // Note: add missing comma
authorId: string,
albumId?: string,
plays?: number
}
export type Collection = { [index: string]: CollectionItem };Then import in each file:
import type { Collection, CollectionItem } from './types';🤖 Prompt for AI Agents
In netlify/edge-functions/syncContent.ts around lines 5 to 15, the
CollectionItem and Collection type definitions are duplicated across multiple
edge-function files and the CollectionItem type is missing a comma after
duration: string; extract these shared types into a new module
netlify/edge-functions/types.ts exporting CollectionItem (with the missing comma
added) and Collection, then remove the local type definitions in syncContent.ts
and replace them with an import line: import type { Collection, CollectionItem }
from './types'; ensure other files (syncBulkTracks.ts, syncCleanup.ts) do the
same to avoid duplication.
This implementation provides a search handler for the YouTube Music API, enabling users to search for music content based on various parameters. It includes error handling and supports filters for different types of media.
This PR fixes the issue where cloud sync was failing to update and fetch user data reliably.
Summary by CodeRabbit
Release Notes
New Features
Chores
✏️ Tip: You can customize this high-level summary in your review settings.