Skip to content

feat: native TS parser, drop .py bridge 🐍🌉, restruc __tests__ - #83

Merged
remcostoeten merged 4 commits into
masterfrom
feat/ytmusic-native-parser
Aug 3, 2026
Merged

feat: native TS parser, drop .py bridge 🐍🌉, restruc __tests__#83
remcostoeten merged 4 commits into
masterfrom
feat/ytmusic-native-parser

Conversation

@remcostoeten

@remcostoeten remcostoeten commented Aug 1, 2026

Copy link
Copy Markdown
Owner

Rewrites the YT Music integration as pure TypeScript and cleans up surrounding areas.

YT Music (main change)

  • Replaces the Python bridge (python-bridge.ts) with a native TS parser
    (src/server/ytmusic/parser.ts) — no more external process dependency
  • Adds a server-side cache layer (src/server/ytmusic/cache.ts)
  • New /ytmusic diagnostics page for inspecting the integration
  • Reworked auth and track fetching (auth.ts, tracks.ts)

Tests

  • Restructures the flat __tests__/ directory into app/, components/, features/, server/
    mirroring the source tree (all detected as renames)
  • Adds coverage for the new ytmusic parser, tracks, and recent-route

Media tools

  • New use-media-session hook and media-persistence utility
  • Tools-hub / SEO content cleanup, removes the unused use-tool-usage hook

Why

The Python bridge required a Python runtime on the host, which complicated deploys and was a
fragile external dependency. Parsing natively in TS removes that entirely and lets responses be
cached in-process.

Summary by Sourcery

Replace the YouTube Music Python bridge with a native TypeScript integration, add diagnostics and caching, and improve media tools persistence, accessibility, and SEO/content structure.

New Features:

  • Introduce a native TypeScript YouTube Music parser with structured result metadata and a diagnostics page.
  • Add IndexedDB-backed media session persistence and a shared hook used by video/GIF tools to restore files, trims, and outputs across visits.

Enhancements:

  • Rework YouTube Music auth, caching, and API routing to handle configuration, authorization, and errors explicitly and to reuse a persistent cache when appropriate.
  • Use YouTube Music tracks as a fallback in combined activity when Spotify has no data and expose richer track metadata for downstream consumers.
  • Improve media tool UX with clear-file actions, remux compatibility detection, inline output previews, and more resilient trim handling.
  • Tighten accessibility for activity graphs and feeds, tool navigation, and media trim panels via keyboard navigation, skip links, focus treatments, and clearer messaging.
  • Simplify tool and package SEO content by removing FAQ sections and their structured-data representations.
  • Allow additional YouTube image host domains and slightly adjust animated icon CSS for overflow handling.

Build:

  • Extend Next.js image configuration to support i.ytimg.com assets.

Documentation:

  • Touch a marketing blog post to fix code formatting and clarify wording around function hoisting.

Tests:

  • Add unit tests for the YouTube Music parser, track retrieval logic, and recent-route API to validate parsing, caching, and status mapping.
  • Update combined-activity tests to cover the new YouTube Music fallback behaviour when Spotify returns no tracks.

Chores:

  • Add YouTube Music-related environment variables to the server env schema and example configuration.
  • Remove the legacy Python bridge implementation and tool-usage tracking hook now that the integration and tools hub no longer rely on them.

Summary by CodeRabbit

  • New Features

    • Added a YouTube Music diagnostics page showing connection status, cache details, request latency, and recent tracks.
    • Added live refresh support and clearer YouTube Music status and fallback handling.
    • Media tools now restore files, trim settings, and converted outputs between sessions.
    • Added video and GIF previews after export.
    • Added keyboard navigation and skip-link accessibility improvements for activity views.
  • Improvements

    • Enabled YouTube Music artwork loading.
    • Removed outdated FAQ sections from package and tool pages.
    • Corrected and clarified an article’s code examples and explanations.

@vercel

vercel Bot commented Aug 1, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
remcostoeten Ready Ready Preview Aug 1, 2026 9:24pm

@sourcery-ai

sourcery-ai Bot commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

Reviewer's Guide

Rewrites the YouTube Music integration to a native TypeScript pipeline with structured result types and server-side caching, introduces a diagnostics page and updated activity aggregation, and adds media-session persistence and accessibility improvements across several miscellaneous tools while simplifying SEO and package FAQs.

Sequence diagram for YT Music recent tracks fetch and caching

sequenceDiagram
  participant Api as api_ytmusic_recent_GET
  participant Tracks as getYTMusicResult
  participant Cache as readYTMusicCache/writeYTMusicCache
  participant Auth as fetchInnertube
  participant Parser as parser_ts

  Api->>Tracks: getYTMusicResult(limit, GetYTMusicOptions)
  Tracks->>Cache: readYTMusicCache()
  Cache-->>Tracks: YTMusicCacheEntry | null

  alt [cache is fresh and has tracks]
    Tracks-->>Api: YTMusicResult(status ok, source database)
  else [need live YouTube Music fetch]
    Tracks->>Auth: fetchInnertube("browse", {browseId: "FEmusic_history"})
    Auth-->>Tracks: innertubeResponse
    Tracks->>Parser: parseInnertubeTracks(innertubeResponse, limit)
    Parser-->>Tracks: YTMusicTrack[]
    Tracks->>Parser: stabilizeTrackTimestamps(freshTracks, cachedTracks)
    Parser-->>Tracks: YTMusicTrack[]
    Tracks->>Cache: writeYTMusicCache(tracks)
    Cache-->>Tracks: updatedAt
    Tracks-->>Api: YTMusicResult(status ok | empty | unauthorized | error, source youtube-music)
  end
Loading

File-Level Changes

Change Details Files
Replace Python-based YT Music bridge with native TS parsing, auth bootstrap, and structured result + caching API.
  • Remove python bridge module and legacy DB cache helpers from YT Music server layer.
  • Implement Innertube response parser with auth error detection and timestamp stabilization utilities.
  • Add dedicated DB-backed YT Music cache read/write helpers and fresh-cache short-circuiting.
  • Expose getYTMusicResult/getYTMusicTracks APIs returning rich YTMusicResult metadata instead of raw arrays.
  • Update env typing and exports to include YTM_COOKIE and YTM_AUTH_USER and Next image domains for YouTube artwork.
src/server/ytmusic/tracks.ts
src/server/ytmusic/auth.ts
src/server/ytmusic/cache.ts
src/server/ytmusic/parser.ts
src/server/ytmusic/python-bridge.ts
src/server/ytmusic/index.ts
src/server/env.ts
next.config.ts
src/features/ytmusic/types.ts
Add YT Music diagnostics page and API response mapping for introspecting integration status.
  • Implement marketing /ytmusic page rendering a diagnostics client with Suspense and server-side initial load.
  • Build YTMusicDiagnostics UI to show status, source, credentials, latency, and normalized track list.
  • Change /api/ytmusic/recent route to return YTMusicResult, support refresh param, and map service status to HTTP codes with no-store caching.
  • Add tests around recent route limit bounding and status-to-HTTP mapping.
src/app/(marketing)/ytmusic/page.tsx
src/features/ytmusic/components/ytmusic-diagnostics.tsx
src/app/api/ytmusic/recent/route.ts
__tests__/app/api/ytmusic/recent/route.test.ts
Adjust combined activity aggregation to use YT Music as a fallback when Spotify has no tracks, updating tests accordingly.
  • Simplify getCombinedActivity to always call Spotify first and fall back to YT Music tracks when Spotify returns empty.
  • Remove hasYTMusicCredentials from combined activity aggregation and mocks.
  • Extend tests to verify fallback behavior and no redundant YTM call when Spotify has data.
src/app/api/activity/combined/combine.ts
__tests__/app/api/activity/combined/combine.test.ts
Introduce IndexedDB-backed media session persistence hook and utilities, wiring into GIF/video tools and Sendable Video.
  • Create media-persistence utilities for loading/saving file, trim and output per tool via IndexedDB.
  • Add useMediaSession hook to restore prior sessions on mount and expose non-blocking persistFile/persistTrim/persistOutput.
  • Integrate persistence into sendable-video, video-to-gif, gif-to-video stores (restoring file/trim/output, debounced trim saves, clearing on new file).
  • Update FFmpeg-related logic to probe input codecs and decide between remux and re-encode, including isRemuxCompatible and probeLogs helpers.
  • Surface restored session messages and adjust output naming and status messaging in stores.
src/features/miscellaneous/utils/media-persistence.ts
src/features/miscellaneous/hooks/use-media-session.ts
src/features/miscellaneous/sendable-video/hooks/use-sendable-video-store.ts
src/features/miscellaneous/video-to-gif/hooks/use-video-to-gif-store.ts
src/features/miscellaneous/gif-to-video/hooks/use-gif-to-video-store.ts
src/features/miscellaneous/utils/ffmpeg.ts
src/features/miscellaneous/sendable-video/utils/ffmpeg.ts
Improve media trimming UX and accessibility in Sendable Video and related components.
  • Extend trim state metadata handler to accept an initial trim and maintain a history including persisted ranges.
  • Wrap MediaTrimPanel video play in a promise to avoid stale preview state when playback fails and preload metadata explicitly.
  • Add a clear-video button to SendableVideo, and normalize copy to use hyphens instead of en-dashes.
  • Update MediaDropzone copy from em dash to hyphen for consistency.
src/features/miscellaneous/hooks/use-trim-state.ts
src/features/miscellaneous/components/media-trim-panel.tsx
src/features/miscellaneous/sendable-video/index.tsx
src/features/miscellaneous/components/media-dropzone.tsx
Enhance tools hub, tool renderer, and SEO content by removing usage tracking and FAQ/structured data, simplifying UI.
  • Remove use-tool-usage hook and recent-tools section; tools hub now shows only search and category filters.
  • Drop FaqStructuredData usage and FAQ sections from tool and package marketing pages and tool SEO content model.
  • Clean up ToolsHub implementation (remove deferred query, Clock icon, recent-tools wiring) and quick nav button styles.
  • Adjust packages data to remove FAQs, and blog copy typo fixes in yappin post.
src/features/miscellaneous/components/tools-hub.tsx
src/features/miscellaneous/components/tool-renderer.tsx
src/features/miscellaneous/constants/tool-seo.ts
src/features/miscellaneous/components/tool-seo-content.tsx
src/features/packages/data.ts
src/app/(marketing)/packages/[slug]/page.tsx
src/app/(tools)/tools/[slug]/page.tsx
src/features/miscellaneous/hooks/use-tool-usage.ts
src/app/(marketing)/blog/posts/yappin/stop-using-arrow-fnc-and-HUGE-REVEAL.md
Improve accessibility and keyboard navigation in activity components and tool navigation.
  • Add skip link to ActivitySection and landmark for activity feed, with focus management.
  • Introduce keyboard grid navigation for ActivityContributionGraph, tracking focused cell and using ref map for focus movement.
  • Update tool quick nav buttons with ai-trigger/overflow-visible for icon animation and focus styling.
  • Ensure animated icon CSS allows overflow for icons.
src/components/landing/activity/section.tsx
src/components/landing/activity/contribution-graph.tsx
src/features/miscellaneous/components/tool-quick-nav.tsx
src/features/miscellaneous/components/icons/animated-icons.css
Add and adjust tests around YT Music parser and tracks integration, including auth error handling and cache behavior.
  • Add parser tests to confirm normalization, unauthorized detection, and timestamp stabilization with cache.
  • Add tracks tests validating stale cache serving without credentials, fresh-cache short-circuiting, and unauthorized behavior when YouTube rejects the session.
__tests__/server/ytmusic/parser.test.ts
__tests__/server/ytmusic/tracks.test.ts

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@coderabbitai

coderabbitai Bot commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@remcostoeten, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 33 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 1b2004d8-cc36-456b-a41f-acec8c35103d

📥 Commits

Reviewing files that changed from the base of the PR and between a750734 and 952ebf4.

📒 Files selected for processing (12)
  • __tests__/server/ytmusic/tracks.test.ts
  • scripts/ytmusic/get_history.py
  • scripts/ytmusic/patched_ytmusic.py
  • scripts/ytmusic/server.py
  • scripts/ytmusic/setup_oauth.py
  • src/components/landing/activity/contribution-graph.tsx
  • src/features/miscellaneous/hooks/use-media-session.ts
  • src/features/miscellaneous/sendable-video/utils/ffmpeg.ts
  • src/features/miscellaneous/utils/media-persistence.ts
  • src/features/ytmusic/components/ytmusic-diagnostics.tsx
  • src/server/ytmusic/auth.ts
  • src/server/ytmusic/tracks.ts
📝 Walkthrough

Walkthrough

Changes

The PR adds a structured YouTube Music diagnostics and retrieval flow, IndexedDB persistence for media tools, activity accessibility improvements, FAQ and recent-tool removal, and broad Vitest coverage across routes and utilities.

YouTube Music

Layer / File(s) Summary
Diagnostics and retrieval pipeline
src/server/ytmusic/*, src/features/ytmusic/*, src/app/(marketing)/ytmusic/page.tsx, src/app/api/ytmusic/recent/route.ts
Adds bootstrapped authentication, Innertube parsing, typed cache results, status mapping, and a diagnostics page.
Activity integration and validation
src/app/api/activity/combined/combine.ts, __tests__/server/ytmusic/*, __tests__/app/api/ytmusic/*
Uses YouTube Music only when Spotify has no tracks and tests cache, authorization, refresh, and fallback behavior.

Media tools

Layer / File(s) Summary
Media-session persistence
src/features/miscellaneous/utils/media-persistence.ts, src/features/miscellaneous/hooks/use-media-session.ts, src/features/miscellaneous/*/hooks/*store.ts
Persists and restores files, trim ranges, and generated outputs through IndexedDB.
Export and playback updates
src/features/miscellaneous/sendable-video/*, src/features/miscellaneous/components/media-trim-panel.tsx
Adds output previews, clear controls, playback-state handling, and codec-aware remux checks.

Accessibility and cleanup

Layer / File(s) Summary
Activity accessibility
src/components/landing/activity/*
Adds roving keyboard navigation and a skip link for the activity feed.
FAQ and recent-tool removal
src/features/miscellaneous/constants/tool-seo.ts, src/features/miscellaneous/components/*, src/features/packages/data.ts, src/app/(marketing)/packages/[slug]/page.tsx
Removes FAQ data and rendering, plus recent-tool tracking and display.

Test coverage and content

Layer / File(s) Summary
Route and utility tests
__tests__/**/*
Adds coverage for application routes, integrations, server services, and miscellaneous utilities.
Configuration and article correction
next.config.ts, src/app/(marketing)/blog/posts/...
Allows YouTube artwork hosts and corrects the blog code example.

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 18.52% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly identifies the main YT Music rewrite, Python bridge removal, and test reorganization.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/ytmusic-native-parser

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.

@remcostoeten remcostoeten changed the title feat(ytmusic): native TS parser and drop Python bridge 🐍🌉 feat: native TS parser, drop .py bridge 🐍🌉, restruc __tests__ Aug 1, 2026

@sourcery-ai sourcery-ai Bot left a comment

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.

Hey - I've found 2 issues, and left some high level feedback:

  • In the /api/ytmusic/recent route, the unstable_rethrow(error) inside the catch block makes the subsequent logging and JSON error response unreachable; either drop the rethrow or handle rethrow-only cases separately so the control flow is clear.
  • The IndexedDB helpers in media-persistence.ts assume window.indexedDB is always available; adding a feature check and short-circuiting when IndexedDB is unavailable would avoid runtime errors in unsupported environments.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In the `/api/ytmusic/recent` route, the `unstable_rethrow(error)` inside the catch block makes the subsequent logging and JSON error response unreachable; either drop the rethrow or handle rethrow-only cases separately so the control flow is clear.
- The IndexedDB helpers in `media-persistence.ts` assume `window.indexedDB` is always available; adding a feature check and short-circuiting when IndexedDB is unavailable would avoid runtime errors in unsupported environments.

## Individual Comments

### Comment 1
<location path="src/app/api/ytmusic/recent/route.ts" line_range="2" />
<code_context>
 import { NextResponse } from 'next/server'
-import { getYTMusicTracks } from '@/server/ytmusic/tracks'
-import { hasYTMusicCredentials } from '@/server/ytmusic/auth'
+import { unstable_rethrow } from 'next/navigation'
+import { getYTMusicResult } from '@/server/ytmusic/tracks'
 import { parseBoundedIntParam } from '@/shared/lib/request-params'
</code_context>
<issue_to_address>
**issue (bug_risk):** Using `unstable_rethrow` and returning a custom error response in the same catch block is contradictory.

In this handler `unstable_rethrow(error)` exits the catch block immediately, so the `console.error` and `NextResponse.json(...)` never run. This creates misleading control flow and may result in the error being handled/logged twice by different layers. Please either remove `unstable_rethrow` and rely on the explicit 500 response, or drop the custom JSON response and let the framework handle the rethrown error.
</issue_to_address>

### Comment 2
<location path="src/features/miscellaneous/utils/media-persistence.ts" line_range="33-35" />
<code_context>
+	return `${toolKey}:output`
+}
+
+function openDatabase(): Promise<IDBDatabase> {
+	return new Promise((resolve, reject) => {
+		const request = window.indexedDB.open(DB_NAME, DB_VERSION)
+		request.onupgradeneeded = () => {
+			if (!request.result.objectStoreNames.contains(STORE_NAME)) {
</code_context>
<issue_to_address>
**issue:** Directly using `window.indexedDB` without capability checks can throw in unsupported or restricted environments.

These helpers assume `window.indexedDB` always exists, but in some environments (e.g. older Safari, private browsing, hardened contexts) it can be missing or fail synchronously. A direct `window.indexedDB.open(...)` will then throw before any promise rejection handling. Please add a capability check (e.g. `if (!('indexedDB' in window)) { /* no-op persistence */ }`) so the UI keeps working when IndexedDB isn’t available.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread src/app/api/ytmusic/recent/route.ts
Comment thread src/features/miscellaneous/utils/media-persistence.ts

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 14

🧹 Nitpick comments (8)
src/features/miscellaneous/utils/media-persistence.ts (1)

97-106: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Read all three keys in one transaction.

loadMediaSession calls readKey three times. Each call opens and closes a separate IDBDatabase connection. Use one connection and one read transaction instead. This reduces open/close churn on the restore path and gives a consistent snapshot of the three keys.

♻️ Proposed refactor
+async function readKeys<T extends unknown[]>(keys: string[]): Promise<unknown[]> {
+	const db = await openDatabase()
+	try {
+		return await new Promise<unknown[]>((resolve, reject) => {
+			const store = db
+				.transaction(STORE_NAME)
+				.objectStore(STORE_NAME)
+			const requests = keys.map(key => store.get(key))
+			const transaction = store.transaction
+			transaction.oncomplete = () =>
+				resolve(requests.map(request => request.result))
+			transaction.onerror = () => reject(transaction.error)
+			transaction.onabort = () => reject(transaction.error)
+		})
+	} finally {
+		db.close()
+	}
+}
+
 export async function loadMediaSession(
 	toolKey: string
 ): Promise<TPersistedSession> {
-	const [file, trim, output] = await Promise.all([
-		readKey<File>(inputKey(toolKey)),
-		readKey<TTrimRange>(trimKey(toolKey)),
-		readKey<TPersistedOutput>(outputKey(toolKey))
-	])
+	const [file, trim, output] = (await readKeys([
+		inputKey(toolKey),
+		trimKey(toolKey),
+		outputKey(toolKey)
+	])) as [File?, TTrimRange?, TPersistedOutput?]
 	return { file: file ?? null, trim: trim ?? null, output: output ?? null }
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/features/miscellaneous/utils/media-persistence.ts` around lines 97 - 106,
Update loadMediaSession to open a single IndexedDB connection and execute one
read-only transaction covering inputKey(toolKey), trimKey(toolKey), and
outputKey(toolKey), rather than invoking readKey separately for each key. Reuse
the transaction’s object store reads to build the existing { file, trim, output
} result with null fallbacks, preserving the current return shape.
src/features/miscellaneous/sendable-video/hooks/use-sendable-video-store.ts (1)

71-95: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚖️ Poor tradeoff

Extract the duplicated restore and trim-persistence logic. Three stores repeat the same block: a restoredRef guard, setFile plus URL.createObjectURL, output reconstruction from session.output, and the identical "Restored your previous session from this browser." status. Two of them also repeat the same 300 ms debounced persistTrim effect. Move the shared parts into useMediaSession or a small helper so a future change to the restore contract needs one edit.

  • src/features/miscellaneous/sendable-video/hooks/use-sendable-video-store.ts#L71-L95: move the guard, file/URL restore, and status message into the shared helper, and keep only the kind mapping local.
  • src/features/miscellaneous/video-to-gif/hooks/use-video-to-gif-store.ts#L74-L98: use the shared helper, and move the debounced persistTrim effect (lines 185-191) into a shared useTrimPersistence hook.
  • src/features/miscellaneous/gif-to-video/hooks/use-gif-to-video-store.ts#L62-L84: use the shared helper; this store needs no trim handling.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/features/miscellaneous/sendable-video/hooks/use-sendable-video-store.ts`
around lines 71 - 95, Extract the duplicated session restoration into a shared
helper or useMediaSession, including the restoredRef guard, file and object URL
restoration, output reconstruction, and restored-session status; retain only
each store’s local output kind mapping. In
src/features/miscellaneous/sendable-video/hooks/use-sendable-video-store.ts
lines 71-95, adopt the helper. In
src/features/miscellaneous/video-to-gif/hooks/use-video-to-gif-store.ts lines
74-98, adopt it and move the 300 ms persistTrim effect from lines 185-191 into a
shared useTrimPersistence hook. In
src/features/miscellaneous/gif-to-video/hooks/use-gif-to-video-store.ts lines
62-84, adopt the helper without adding trim handling.
__tests__/components/projects/server/github.test.ts (1)

10-20: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Restore environment and global stubs after each test.

beforeEach clears state only before a test starts. If Vitest shares globals across files, the final test leaves NEXT_PHASE and fetch stubbed. Add afterEach cleanup with vi.unstubAllEnvs() and vi.unstubAllGlobals().

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@__tests__/components/projects/server/github.test.ts` around lines 10 - 20,
Add an afterEach hook in the GitHub component test setup that calls
vi.unstubAllEnvs() and vi.unstubAllGlobals() to restore environment variables
and globals after every test; keep the existing beforeEach reset behavior
unchanged.
src/server/ytmusic/auth.ts (3)

46-49: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Widen the config value unescaping.

readConfigValue only converts \u003d. YouTube inlines other escape sequences in these JSON string literals, for example \u0026 and \/. VISITOR_DATA is the most likely value to carry them, and a wrong visitor ID is passed silently to both the payload and the X-Goog-Visitor-Id header.

Decode the captured value as a JSON string instead of a single hand-rolled replacement.

♻️ Proposed refactor
 function readConfigValue(html: string, key: string): string {
-	const match = html.match(new RegExp(`"${key}"\\s*:\\s*"([^"]+)"`))
-	return match?.[1]?.replace(/\\u003d/gi, '=') ?? ''
+	const match = html.match(new RegExp(`"${key}"\\s*:\\s*"([^"]*)"`))
+	if (!match?.[1]) return ''
+	try {
+		return JSON.parse(`"${match[1]}"`) as string
+	} catch {
+		return match[1]
+	}
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/server/ytmusic/auth.ts` around lines 46 - 49, Update readConfigValue to
decode the captured config value using JSON string parsing, so all standard JSON
escapes such as \u003d, \u0026, and escaped slashes are handled instead of only
replacing equals signs. Preserve the existing empty-string fallback when no
matching value is found.

99-120: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Make generateAuthHeader synchronous.

The function contains no await. It performs only cookie lookups and SHA-1 hashing. Declare it as a plain function and drop the await at the call site on Line 145.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/server/ytmusic/auth.ts` around lines 99 - 120, Make generateAuthHeader a
synchronous function by removing async, and remove the corresponding await from
its call site around line 145. Preserve the existing cookie validation, hash
generation, and returned authorization header unchanged.

135-142: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Deduplicate concurrent bootstrap requests.

fetchPageConfig is awaited without a shared in-flight promise. The diagnostics page and the /api/ytmusic/recent route can run at the same time, so several concurrent calls each fetch https://music.youtube.com and each overwrites cachedPageConfig. This multiplies outbound bootstrap requests and increases the risk of throttling by YouTube.

Store the pending promise and reuse it.

♻️ Proposed refactor
 let cachedPageConfig: PageConfig | null = null
+let pendingPageConfig: Promise<PageConfig> | null = null
 	if (!cachedPageConfig) {
-		cachedPageConfig = await fetchPageConfig(cachedCookieMap)
+		pendingPageConfig ??= fetchPageConfig(cachedCookieMap).finally(() => {
+			pendingPageConfig = null
+		})
+		cachedPageConfig = await pendingPageConfig
 	}

Also clear pendingPageConfig inside invalidateYTMusicClient.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/server/ytmusic/auth.ts` around lines 135 - 142, Deduplicate concurrent
page-config bootstrap calls in the authentication flow by introducing a shared
pending promise for fetchPageConfig, reusing it whenever cachedPageConfig is
absent, and assigning the resolved result to the cache without starting
duplicate requests. Update invalidateYTMusicClient to clear pendingPageConfig
alongside the other cached client state.
src/server/ytmusic/tracks.ts (1)

152-153: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Remove the identity wrapper.

createResult returns its argument unchanged. It only adds a call layer. The YTMusicResult type annotation on each object literal gives the same type checking.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/server/ytmusic/tracks.ts` around lines 152 - 153, Remove the redundant
createResult function and its call sites in the result construction flow,
returning the YTMusicResult object literals directly. Preserve the existing
YTMusicResult annotations on those literals so type checking remains unchanged.
src/app/api/ytmusic/recent/route.ts (1)

44-49: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Type the status parameter with the union.

getHttpStatus accepts string. YTMusicResult.status is typed as YTMusicStatus in src/features/ytmusic/types.ts. With the wider parameter type, a misspelled literal in this function compiles and silently maps to 200. Use the union type and keep the mapping in a record so new statuses surface as type errors.

♻️ Proposed refactor
+import type { YTMusicStatus } from '`@/features/ytmusic/types`'
-function getHttpStatus(status: string): number {
-	if (status === 'unauthorized') return 401
-	if (status === 'unconfigured') return 503
-	if (status === 'error') return 502
-	return 200
-}
+const HTTP_STATUS_BY_RESULT: Record<YTMusicStatus, number> = {
+	ok: 200,
+	empty: 200,
+	stale: 200,
+	unauthorized: 401,
+	unconfigured: 503,
+	error: 502
+}
+
+function getHttpStatus(status: YTMusicStatus): number {
+	return HTTP_STATUS_BY_RESULT[status] ?? 200
+}

Confirm the member list against YTMusicStatus.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/app/api/ytmusic/recent/route.ts` around lines 44 - 49, Update
getHttpStatus to accept the YTMusicStatus union instead of string, and replace
the conditional mapping with a typed record that covers every YTMusicStatus
member, preserving the existing HTTP status values and default behavior as
appropriate.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@__tests__/app/api/spotify/auth-url/route.test.ts`:
- Around line 26-44: Update the test around the GET request and redirect_uri
assertion to stub SPOTIFY_REDIRECT_URI with a value different from the route’s
hardcoded fallback, then assert that distinct value is returned in
authUrl.searchParams. Keep the existing client ID and response checks unchanged.

In `@__tests__/app/api/spotify/now-playing/route.test.ts`:
- Around line 98-119: Update the fetch mock in the “retries once on 401 and
returns a non-playing state when spotify stays unavailable” test so both
responses explicitly represent failure, including setting the second response’s
ok value to false rather than relying on an omitted field. Keep the 401 retry
and non-playing response assertions unchanged.

In `@__tests__/server/ytmusic/tracks.test.ts`:
- Around line 56-58: Add an afterEach cleanup hook in the test suite containing
“does not call YouTube while the persistent cache is fresh” that calls
vi.useRealTimers(), ensuring fake timers and the mocked system time are restored
after every test.

In @.env.example:
- Around line 50-54: Remove the obsolete Python bridge scripts under
scripts/ytmusic/ if no runtime or build path still uses them, while preserving
the TypeScript server handling of YTM_AUTH_USER and YTM_COOKIE and the
corresponding environment validation and ignored generated auth/session files.

In
`@src/app/`(marketing)/blog/posts/yappin/stop-using-arrow-fnc-and-HUGE-REVEAL.md:
- Around line 27-32: Separate the two SomeView alternatives in the documentation
so they are not interpreted as duplicate declarations when copied. Use distinct
code fences or comment out one declaration, while preserving both alternative
implementations for readers.
- Around line 27-37: Update the SomeView example so it no longer returns an
arrow function: return the intended renderable view value directly, or revise
the surrounding prose if returning a function is intentional. Keep the
comparison and guidance consistent with the chosen behavior.

In `@src/components/landing/activity/contribution-graph.tsx`:
- Around line 434-460: Update handleGridKeyDown so ArrowDown and ArrowUp only
move within the active cell’s current week, preventing transitions between a
week’s last and first day. Preserve the existing clamping behavior for
horizontal navigation, Home, End, and valid vertical movement.

In `@src/features/miscellaneous/hooks/use-media-session.ts`:
- Around line 36-45: Update the useEffect restore flow around loadMediaSession
to track cancellation and return a cleanup that marks the effect inactive,
preventing onRestoreRef.current from running after unmount or toolKey changes.
Also replace the global startedRef guard with toolKey-aware tracking so each
dependency value can load once when toolKey changes.

In `@src/features/miscellaneous/sendable-video/utils/ffmpeg.ts`:
- Around line 23-27: Update isRemuxCompatible to inspect every audio stream in
probe rather than relying on probe.match’s first result. Reject remux
compatibility when any audio codec is not AAC or MP3, while preserving
compatibility for videos with no audio streams and for sources whose audio
streams are all supported.

In `@src/features/miscellaneous/utils/media-persistence.ts`:
- Around line 113-123: Update saveMediaFile to skip persistence when the input
file exceeds the configured MAX_INPUT_MB limit, preserving the existing clear
behavior for null files. Replace the separate deleteKeys and writeKey calls with
one readwrite transaction that removes trimKey(toolKey) and outputKey(toolKey)
and stores inputKey(toolKey) atomically, so a failed write cannot leave
inconsistent session data.

In `@src/features/ytmusic/components/ytmusic-diagnostics.tsx`:
- Around line 382-391: Update formatTimestamp to pass an explicit stable
timeZone, such as 'UTC', in the Intl.DateTimeFormat options so SSR and client
hydration produce identical timestamp text; preserve the existing invalid-date
handling and formatting fields.

In `@src/server/ytmusic/auth.ts`:
- Around line 194-196: Update the response failure handling in the YouTube Music
request flow to invalidate cachedPageConfig when authentication fails, ensuring
subsequent requests rebuild the bootstrap configuration. Import
YTMusicUnauthorizedError from parser and throw it specifically for 401 and 403
responses; preserve the existing generic error handling for other unsuccessful
statuses.

In `@src/server/ytmusic/cache.ts`:
- Around line 33-50: Update the getYTMusicResult flow to fetch and parse a fixed
maximum track window for writeYTMusicCache, rather than using the caller’s
limit. Persist that full window, then slice the cached or freshly parsed tracks
to the requested limit only when returning the result; keep writeYTMusicCache
unchanged as the persistence boundary.

In `@src/server/ytmusic/tracks.ts`:
- Around line 68-97: Parse and stabilize a fixed maximum history independently
of the request limit, then persist the complete stabilized collection via
writeYTMusicCache. Update the empty-result check to use allTracks, and return
only the requested limit from the response while retaining the full cache for
stale fallback and timestamp stabilization.

---

Nitpick comments:
In `@__tests__/components/projects/server/github.test.ts`:
- Around line 10-20: Add an afterEach hook in the GitHub component test setup
that calls vi.unstubAllEnvs() and vi.unstubAllGlobals() to restore environment
variables and globals after every test; keep the existing beforeEach reset
behavior unchanged.

In `@src/app/api/ytmusic/recent/route.ts`:
- Around line 44-49: Update getHttpStatus to accept the YTMusicStatus union
instead of string, and replace the conditional mapping with a typed record that
covers every YTMusicStatus member, preserving the existing HTTP status values
and default behavior as appropriate.

In `@src/features/miscellaneous/sendable-video/hooks/use-sendable-video-store.ts`:
- Around line 71-95: Extract the duplicated session restoration into a shared
helper or useMediaSession, including the restoredRef guard, file and object URL
restoration, output reconstruction, and restored-session status; retain only
each store’s local output kind mapping. In
src/features/miscellaneous/sendable-video/hooks/use-sendable-video-store.ts
lines 71-95, adopt the helper. In
src/features/miscellaneous/video-to-gif/hooks/use-video-to-gif-store.ts lines
74-98, adopt it and move the 300 ms persistTrim effect from lines 185-191 into a
shared useTrimPersistence hook. In
src/features/miscellaneous/gif-to-video/hooks/use-gif-to-video-store.ts lines
62-84, adopt the helper without adding trim handling.

In `@src/features/miscellaneous/utils/media-persistence.ts`:
- Around line 97-106: Update loadMediaSession to open a single IndexedDB
connection and execute one read-only transaction covering inputKey(toolKey),
trimKey(toolKey), and outputKey(toolKey), rather than invoking readKey
separately for each key. Reuse the transaction’s object store reads to build the
existing { file, trim, output } result with null fallbacks, preserving the
current return shape.

In `@src/server/ytmusic/auth.ts`:
- Around line 46-49: Update readConfigValue to decode the captured config value
using JSON string parsing, so all standard JSON escapes such as \u003d, \u0026,
and escaped slashes are handled instead of only replacing equals signs. Preserve
the existing empty-string fallback when no matching value is found.
- Around line 99-120: Make generateAuthHeader a synchronous function by removing
async, and remove the corresponding await from its call site around line 145.
Preserve the existing cookie validation, hash generation, and returned
authorization header unchanged.
- Around line 135-142: Deduplicate concurrent page-config bootstrap calls in the
authentication flow by introducing a shared pending promise for fetchPageConfig,
reusing it whenever cachedPageConfig is absent, and assigning the resolved
result to the cache without starting duplicate requests. Update
invalidateYTMusicClient to clear pendingPageConfig alongside the other cached
client state.

In `@src/server/ytmusic/tracks.ts`:
- Around line 152-153: Remove the redundant createResult function and its call
sites in the result construction flow, returning the YTMusicResult object
literals directly. Preserve the existing YTMusicResult annotations on those
literals so type checking remains unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 7ebe6e5d-6968-4a38-b8ad-40ad4643613e

📥 Commits

Reviewing files that changed from the base of the PR and between fbfe8b3 and a750734.

📒 Files selected for processing (65)
  • .env.example
  • __tests__/app/(marketing)/rss/route.test.ts
  • __tests__/app/api/activity/combined/combine.test.ts
  • __tests__/app/api/activity/combined/route.test.ts
  • __tests__/app/api/github/contributions/route.test.ts
  • __tests__/app/api/github/events/route.test.ts
  • __tests__/app/api/spotify/auth-url/route.test.ts
  • __tests__/app/api/spotify/callback/route.test.ts
  • __tests__/app/api/spotify/dev-token/route.test.ts
  • __tests__/app/api/spotify/now-playing/route.test.ts
  • __tests__/app/api/spotify/recent/route.test.ts
  • __tests__/app/api/sync/route.test.ts
  • __tests__/app/api/ytmusic/recent/route.test.ts
  • __tests__/components/landing/activity/activity-section-client.test.ts
  • __tests__/components/projects/components/project-preview.test.tsx
  • __tests__/components/projects/server/github.test.ts
  • __tests__/components/providers/providers.test.ts
  • __tests__/features/miscellaneous/diff-checker/utils/diff.test.ts
  • __tests__/features/miscellaneous/find-replace/utils/search.test.ts
  • __tests__/features/miscellaneous/find-replace/utils/text-transforms.test.ts
  • __tests__/features/miscellaneous/json-tool/utils/json-tool.test.ts
  • __tests__/features/miscellaneous/link-extractor/utils/link-extractor.test.ts
  • __tests__/features/miscellaneous/svg-converter/utilities/svg-converter.test.ts
  • __tests__/server/github/service.test.ts
  • __tests__/server/spotify/tracks.test.ts
  • __tests__/server/ytmusic/parser.test.ts
  • __tests__/server/ytmusic/tracks.test.ts
  • next.config.ts
  • src/app/(marketing)/blog/posts/yappin/stop-using-arrow-fnc-and-HUGE-REVEAL.md
  • src/app/(marketing)/packages/[slug]/page.tsx
  • src/app/(marketing)/ytmusic/page.tsx
  • src/app/(tools)/tools/[slug]/page.tsx
  • src/app/api/activity/combined/combine.ts
  • src/app/api/ytmusic/recent/route.ts
  • src/components/landing/activity/contribution-graph.tsx
  • src/components/landing/activity/section.tsx
  • src/features/miscellaneous/components/icons/animated-icons.css
  • src/features/miscellaneous/components/media-dropzone.tsx
  • src/features/miscellaneous/components/media-trim-panel.tsx
  • src/features/miscellaneous/components/tool-quick-nav.tsx
  • src/features/miscellaneous/components/tool-renderer.tsx
  • src/features/miscellaneous/components/tool-seo-content.tsx
  • src/features/miscellaneous/components/tools-hub.tsx
  • src/features/miscellaneous/constants/tool-seo.ts
  • src/features/miscellaneous/gif-to-video/hooks/use-gif-to-video-store.ts
  • src/features/miscellaneous/hooks/use-media-session.ts
  • src/features/miscellaneous/hooks/use-tool-usage.ts
  • src/features/miscellaneous/hooks/use-trim-state.ts
  • src/features/miscellaneous/sendable-video/components/export-panel.tsx
  • src/features/miscellaneous/sendable-video/hooks/use-sendable-video-store.ts
  • src/features/miscellaneous/sendable-video/index.tsx
  • src/features/miscellaneous/sendable-video/utils/ffmpeg.ts
  • src/features/miscellaneous/utils/ffmpeg.ts
  • src/features/miscellaneous/utils/media-persistence.ts
  • src/features/miscellaneous/video-to-gif/hooks/use-video-to-gif-store.ts
  • src/features/packages/data.ts
  • src/features/ytmusic/components/ytmusic-diagnostics.tsx
  • src/features/ytmusic/types.ts
  • src/server/env.ts
  • src/server/ytmusic/auth.ts
  • src/server/ytmusic/cache.ts
  • src/server/ytmusic/index.ts
  • src/server/ytmusic/parser.ts
  • src/server/ytmusic/python-bridge.ts
  • src/server/ytmusic/tracks.ts
💤 Files with no reviewable changes (7)
  • src/features/miscellaneous/hooks/use-tool-usage.ts
  • src/features/miscellaneous/components/tool-seo-content.tsx
  • src/app/(tools)/tools/[slug]/page.tsx
  • src/server/ytmusic/python-bridge.ts
  • src/app/(marketing)/packages/[slug]/page.tsx
  • src/features/miscellaneous/components/tool-renderer.tsx
  • src/features/packages/data.ts

Comment thread __tests__/server/ytmusic/tracks.test.ts
Comment thread .env.example
Comment on lines +50 to 54
# Browser profile index used by YouTube Music (usually 0).
YTM_AUTH_USER="0"
# Full Cookie request header copied from an authenticated music.youtube.com request.
# Keep this server-side and replace it when the diagnostic page reports unauthorized.
YTM_COOKIE=""

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.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Check whether legacy Python YTM bridge scripts still exist and are referenced elsewhere.
fd . scripts/ytmusic
rg -n 'scripts/ytmusic|get_history\.py|patched_ytmusic\.py|server\.py' --type=json --type=md -g '!node_modules' -g '!scripts/ytmusic/**'

Repository: remcostoeten/remcostoeten.nl

Length of output: 289


🏁 Script executed:

#!/bin/bash
# Description: Check repository references to the Python scripts and YTM scripts across tracked text files.
set -u

echo "== tracked files containing ytmusic references =="
git ls-files -z | tr '\0' '\n' | grep 'scripts/ytmusic' || true

echo
echo "== references to scripts/ytmusic or Python script names outside scripts/ytmusic =="
if command -v rg >/dev/null; then
  rg -n --hidden --glob '!node_modules' --glob '!scripts/ytmusic/**' \
    'scripts/ytmusic|get_history\.py|patched_ytmusic\.py|server\.py|setup_oauth\.py|YTM_AUTH_USER|YTM_COOKIE' . || true
else
  echo "rg not available"
fi

echo
echo "== references to scripts/ytmusic or Python script names in git diff/stat =="
git diff --stat || true
git diff --name-only || true
git diff -- scripts | sed -n '1,220p' || true

Repository: remcostoeten/remcostoeten.nl

Length of output: 1467


Remove the legacy Python bridge scripts if they are no longer used.

YTM_AUTH_USER and YTM_COOKIE are now handled by the TypeScript server paths, while the only remaining references outside scripts/ytmusic/ are .env.example, environment validation, and generated auth/session JSON ignored by .gitignore. Delete scripts/ytmusic/*.py if this migration removes the Python bridge.

🧰 Tools
🪛 dotenv-linter (4.0.0)

[warning] 51-51: [QuoteCharacter] The value has quote characters (', ")

(QuoteCharacter)


[warning] 54-54: [QuoteCharacter] The value has quote characters (', ")

(QuoteCharacter)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.env.example around lines 50 - 54, Remove the obsolete Python bridge scripts
under scripts/ytmusic/ if no runtime or build path still uses them, while
preserving the TypeScript server handling of YTM_AUTH_USER and YTM_COOKIE and
the corresponding environment validation and ignored generated auth/session
files.

Comment thread src/components/landing/activity/contribution-graph.tsx
Comment thread src/features/miscellaneous/utils/media-persistence.ts
Comment thread src/features/ytmusic/components/ytmusic-diagnostics.tsx
Comment thread src/server/ytmusic/auth.ts
Comment thread src/server/ytmusic/cache.ts
Comment thread src/server/ytmusic/tracks.ts Outdated

@coderabbitai coderabbitai Bot left a comment

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.

Caution

Inline review comments failed to post. This is likely due to GitHub's internal server error or limits when posting large numbers of comments. If you are seeing this consistently it is likely a permissions issue. Please check "Moderation" -> "Code review limits" under your organization settings.

Actionable comments posted: 14

🧹 Nitpick comments (8)
src/features/miscellaneous/utils/media-persistence.ts (1)

97-106: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Read all three keys in one transaction.

loadMediaSession calls readKey three times. Each call opens and closes a separate IDBDatabase connection. Use one connection and one read transaction instead. This reduces open/close churn on the restore path and gives a consistent snapshot of the three keys.

♻️ Proposed refactor
+async function readKeys<T extends unknown[]>(keys: string[]): Promise<unknown[]> {
+	const db = await openDatabase()
+	try {
+		return await new Promise<unknown[]>((resolve, reject) => {
+			const store = db
+				.transaction(STORE_NAME)
+				.objectStore(STORE_NAME)
+			const requests = keys.map(key => store.get(key))
+			const transaction = store.transaction
+			transaction.oncomplete = () =>
+				resolve(requests.map(request => request.result))
+			transaction.onerror = () => reject(transaction.error)
+			transaction.onabort = () => reject(transaction.error)
+		})
+	} finally {
+		db.close()
+	}
+}
+
 export async function loadMediaSession(
 	toolKey: string
 ): Promise<TPersistedSession> {
-	const [file, trim, output] = await Promise.all([
-		readKey<File>(inputKey(toolKey)),
-		readKey<TTrimRange>(trimKey(toolKey)),
-		readKey<TPersistedOutput>(outputKey(toolKey))
-	])
+	const [file, trim, output] = (await readKeys([
+		inputKey(toolKey),
+		trimKey(toolKey),
+		outputKey(toolKey)
+	])) as [File?, TTrimRange?, TPersistedOutput?]
 	return { file: file ?? null, trim: trim ?? null, output: output ?? null }
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/features/miscellaneous/utils/media-persistence.ts` around lines 97 - 106,
Update loadMediaSession to open a single IndexedDB connection and execute one
read-only transaction covering inputKey(toolKey), trimKey(toolKey), and
outputKey(toolKey), rather than invoking readKey separately for each key. Reuse
the transaction’s object store reads to build the existing { file, trim, output
} result with null fallbacks, preserving the current return shape.
src/features/miscellaneous/sendable-video/hooks/use-sendable-video-store.ts (1)

71-95: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚖️ Poor tradeoff

Extract the duplicated restore and trim-persistence logic. Three stores repeat the same block: a restoredRef guard, setFile plus URL.createObjectURL, output reconstruction from session.output, and the identical "Restored your previous session from this browser." status. Two of them also repeat the same 300 ms debounced persistTrim effect. Move the shared parts into useMediaSession or a small helper so a future change to the restore contract needs one edit.

  • src/features/miscellaneous/sendable-video/hooks/use-sendable-video-store.ts#L71-L95: move the guard, file/URL restore, and status message into the shared helper, and keep only the kind mapping local.
  • src/features/miscellaneous/video-to-gif/hooks/use-video-to-gif-store.ts#L74-L98: use the shared helper, and move the debounced persistTrim effect (lines 185-191) into a shared useTrimPersistence hook.
  • src/features/miscellaneous/gif-to-video/hooks/use-gif-to-video-store.ts#L62-L84: use the shared helper; this store needs no trim handling.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/features/miscellaneous/sendable-video/hooks/use-sendable-video-store.ts`
around lines 71 - 95, Extract the duplicated session restoration into a shared
helper or useMediaSession, including the restoredRef guard, file and object URL
restoration, output reconstruction, and restored-session status; retain only
each store’s local output kind mapping. In
src/features/miscellaneous/sendable-video/hooks/use-sendable-video-store.ts
lines 71-95, adopt the helper. In
src/features/miscellaneous/video-to-gif/hooks/use-video-to-gif-store.ts lines
74-98, adopt it and move the 300 ms persistTrim effect from lines 185-191 into a
shared useTrimPersistence hook. In
src/features/miscellaneous/gif-to-video/hooks/use-gif-to-video-store.ts lines
62-84, adopt the helper without adding trim handling.
__tests__/components/projects/server/github.test.ts (1)

10-20: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Restore environment and global stubs after each test.

beforeEach clears state only before a test starts. If Vitest shares globals across files, the final test leaves NEXT_PHASE and fetch stubbed. Add afterEach cleanup with vi.unstubAllEnvs() and vi.unstubAllGlobals().

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@__tests__/components/projects/server/github.test.ts` around lines 10 - 20,
Add an afterEach hook in the GitHub component test setup that calls
vi.unstubAllEnvs() and vi.unstubAllGlobals() to restore environment variables
and globals after every test; keep the existing beforeEach reset behavior
unchanged.
src/server/ytmusic/auth.ts (3)

46-49: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Widen the config value unescaping.

readConfigValue only converts \u003d. YouTube inlines other escape sequences in these JSON string literals, for example \u0026 and \/. VISITOR_DATA is the most likely value to carry them, and a wrong visitor ID is passed silently to both the payload and the X-Goog-Visitor-Id header.

Decode the captured value as a JSON string instead of a single hand-rolled replacement.

♻️ Proposed refactor
 function readConfigValue(html: string, key: string): string {
-	const match = html.match(new RegExp(`"${key}"\\s*:\\s*"([^"]+)"`))
-	return match?.[1]?.replace(/\\u003d/gi, '=') ?? ''
+	const match = html.match(new RegExp(`"${key}"\\s*:\\s*"([^"]*)"`))
+	if (!match?.[1]) return ''
+	try {
+		return JSON.parse(`"${match[1]}"`) as string
+	} catch {
+		return match[1]
+	}
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/server/ytmusic/auth.ts` around lines 46 - 49, Update readConfigValue to
decode the captured config value using JSON string parsing, so all standard JSON
escapes such as \u003d, \u0026, and escaped slashes are handled instead of only
replacing equals signs. Preserve the existing empty-string fallback when no
matching value is found.

99-120: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Make generateAuthHeader synchronous.

The function contains no await. It performs only cookie lookups and SHA-1 hashing. Declare it as a plain function and drop the await at the call site on Line 145.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/server/ytmusic/auth.ts` around lines 99 - 120, Make generateAuthHeader a
synchronous function by removing async, and remove the corresponding await from
its call site around line 145. Preserve the existing cookie validation, hash
generation, and returned authorization header unchanged.

135-142: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Deduplicate concurrent bootstrap requests.

fetchPageConfig is awaited without a shared in-flight promise. The diagnostics page and the /api/ytmusic/recent route can run at the same time, so several concurrent calls each fetch https://music.youtube.com and each overwrites cachedPageConfig. This multiplies outbound bootstrap requests and increases the risk of throttling by YouTube.

Store the pending promise and reuse it.

♻️ Proposed refactor
 let cachedPageConfig: PageConfig | null = null
+let pendingPageConfig: Promise<PageConfig> | null = null
 	if (!cachedPageConfig) {
-		cachedPageConfig = await fetchPageConfig(cachedCookieMap)
+		pendingPageConfig ??= fetchPageConfig(cachedCookieMap).finally(() => {
+			pendingPageConfig = null
+		})
+		cachedPageConfig = await pendingPageConfig
 	}

Also clear pendingPageConfig inside invalidateYTMusicClient.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/server/ytmusic/auth.ts` around lines 135 - 142, Deduplicate concurrent
page-config bootstrap calls in the authentication flow by introducing a shared
pending promise for fetchPageConfig, reusing it whenever cachedPageConfig is
absent, and assigning the resolved result to the cache without starting
duplicate requests. Update invalidateYTMusicClient to clear pendingPageConfig
alongside the other cached client state.
src/server/ytmusic/tracks.ts (1)

152-153: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Remove the identity wrapper.

createResult returns its argument unchanged. It only adds a call layer. The YTMusicResult type annotation on each object literal gives the same type checking.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/server/ytmusic/tracks.ts` around lines 152 - 153, Remove the redundant
createResult function and its call sites in the result construction flow,
returning the YTMusicResult object literals directly. Preserve the existing
YTMusicResult annotations on those literals so type checking remains unchanged.
src/app/api/ytmusic/recent/route.ts (1)

44-49: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Type the status parameter with the union.

getHttpStatus accepts string. YTMusicResult.status is typed as YTMusicStatus in src/features/ytmusic/types.ts. With the wider parameter type, a misspelled literal in this function compiles and silently maps to 200. Use the union type and keep the mapping in a record so new statuses surface as type errors.

♻️ Proposed refactor
+import type { YTMusicStatus } from '`@/features/ytmusic/types`'
-function getHttpStatus(status: string): number {
-	if (status === 'unauthorized') return 401
-	if (status === 'unconfigured') return 503
-	if (status === 'error') return 502
-	return 200
-}
+const HTTP_STATUS_BY_RESULT: Record<YTMusicStatus, number> = {
+	ok: 200,
+	empty: 200,
+	stale: 200,
+	unauthorized: 401,
+	unconfigured: 503,
+	error: 502
+}
+
+function getHttpStatus(status: YTMusicStatus): number {
+	return HTTP_STATUS_BY_RESULT[status] ?? 200
+}

Confirm the member list against YTMusicStatus.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/app/api/ytmusic/recent/route.ts` around lines 44 - 49, Update
getHttpStatus to accept the YTMusicStatus union instead of string, and replace
the conditional mapping with a typed record that covers every YTMusicStatus
member, preserving the existing HTTP status values and default behavior as
appropriate.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@__tests__/app/api/spotify/auth-url/route.test.ts`:
- Around line 26-44: Update the test around the GET request and redirect_uri
assertion to stub SPOTIFY_REDIRECT_URI with a value different from the route’s
hardcoded fallback, then assert that distinct value is returned in
authUrl.searchParams. Keep the existing client ID and response checks unchanged.

In `@__tests__/app/api/spotify/now-playing/route.test.ts`:
- Around line 98-119: Update the fetch mock in the “retries once on 401 and
returns a non-playing state when spotify stays unavailable” test so both
responses explicitly represent failure, including setting the second response’s
ok value to false rather than relying on an omitted field. Keep the 401 retry
and non-playing response assertions unchanged.

In `@__tests__/server/ytmusic/tracks.test.ts`:
- Around line 56-58: Add an afterEach cleanup hook in the test suite containing
“does not call YouTube while the persistent cache is fresh” that calls
vi.useRealTimers(), ensuring fake timers and the mocked system time are restored
after every test.

In @.env.example:
- Around line 50-54: Remove the obsolete Python bridge scripts under
scripts/ytmusic/ if no runtime or build path still uses them, while preserving
the TypeScript server handling of YTM_AUTH_USER and YTM_COOKIE and the
corresponding environment validation and ignored generated auth/session files.

In
`@src/app/`(marketing)/blog/posts/yappin/stop-using-arrow-fnc-and-HUGE-REVEAL.md:
- Around line 27-32: Separate the two SomeView alternatives in the documentation
so they are not interpreted as duplicate declarations when copied. Use distinct
code fences or comment out one declaration, while preserving both alternative
implementations for readers.
- Around line 27-37: Update the SomeView example so it no longer returns an
arrow function: return the intended renderable view value directly, or revise
the surrounding prose if returning a function is intentional. Keep the
comparison and guidance consistent with the chosen behavior.

In `@src/components/landing/activity/contribution-graph.tsx`:
- Around line 434-460: Update handleGridKeyDown so ArrowDown and ArrowUp only
move within the active cell’s current week, preventing transitions between a
week’s last and first day. Preserve the existing clamping behavior for
horizontal navigation, Home, End, and valid vertical movement.

In `@src/features/miscellaneous/hooks/use-media-session.ts`:
- Around line 36-45: Update the useEffect restore flow around loadMediaSession
to track cancellation and return a cleanup that marks the effect inactive,
preventing onRestoreRef.current from running after unmount or toolKey changes.
Also replace the global startedRef guard with toolKey-aware tracking so each
dependency value can load once when toolKey changes.

In `@src/features/miscellaneous/sendable-video/utils/ffmpeg.ts`:
- Around line 23-27: Update isRemuxCompatible to inspect every audio stream in
probe rather than relying on probe.match’s first result. Reject remux
compatibility when any audio codec is not AAC or MP3, while preserving
compatibility for videos with no audio streams and for sources whose audio
streams are all supported.

In `@src/features/miscellaneous/utils/media-persistence.ts`:
- Around line 113-123: Update saveMediaFile to skip persistence when the input
file exceeds the configured MAX_INPUT_MB limit, preserving the existing clear
behavior for null files. Replace the separate deleteKeys and writeKey calls with
one readwrite transaction that removes trimKey(toolKey) and outputKey(toolKey)
and stores inputKey(toolKey) atomically, so a failed write cannot leave
inconsistent session data.

In `@src/features/ytmusic/components/ytmusic-diagnostics.tsx`:
- Around line 382-391: Update formatTimestamp to pass an explicit stable
timeZone, such as 'UTC', in the Intl.DateTimeFormat options so SSR and client
hydration produce identical timestamp text; preserve the existing invalid-date
handling and formatting fields.

In `@src/server/ytmusic/auth.ts`:
- Around line 194-196: Update the response failure handling in the YouTube Music
request flow to invalidate cachedPageConfig when authentication fails, ensuring
subsequent requests rebuild the bootstrap configuration. Import
YTMusicUnauthorizedError from parser and throw it specifically for 401 and 403
responses; preserve the existing generic error handling for other unsuccessful
statuses.

In `@src/server/ytmusic/cache.ts`:
- Around line 33-50: Update the getYTMusicResult flow to fetch and parse a fixed
maximum track window for writeYTMusicCache, rather than using the caller’s
limit. Persist that full window, then slice the cached or freshly parsed tracks
to the requested limit only when returning the result; keep writeYTMusicCache
unchanged as the persistence boundary.

In `@src/server/ytmusic/tracks.ts`:
- Around line 68-97: Parse and stabilize a fixed maximum history independently
of the request limit, then persist the complete stabilized collection via
writeYTMusicCache. Update the empty-result check to use allTracks, and return
only the requested limit from the response while retaining the full cache for
stale fallback and timestamp stabilization.

---

Nitpick comments:
In `@__tests__/components/projects/server/github.test.ts`:
- Around line 10-20: Add an afterEach hook in the GitHub component test setup
that calls vi.unstubAllEnvs() and vi.unstubAllGlobals() to restore environment
variables and globals after every test; keep the existing beforeEach reset
behavior unchanged.

In `@src/app/api/ytmusic/recent/route.ts`:
- Around line 44-49: Update getHttpStatus to accept the YTMusicStatus union
instead of string, and replace the conditional mapping with a typed record that
covers every YTMusicStatus member, preserving the existing HTTP status values
and default behavior as appropriate.

In `@src/features/miscellaneous/sendable-video/hooks/use-sendable-video-store.ts`:
- Around line 71-95: Extract the duplicated session restoration into a shared
helper or useMediaSession, including the restoredRef guard, file and object URL
restoration, output reconstruction, and restored-session status; retain only
each store’s local output kind mapping. In
src/features/miscellaneous/sendable-video/hooks/use-sendable-video-store.ts
lines 71-95, adopt the helper. In
src/features/miscellaneous/video-to-gif/hooks/use-video-to-gif-store.ts lines
74-98, adopt it and move the 300 ms persistTrim effect from lines 185-191 into a
shared useTrimPersistence hook. In
src/features/miscellaneous/gif-to-video/hooks/use-gif-to-video-store.ts lines
62-84, adopt the helper without adding trim handling.

In `@src/features/miscellaneous/utils/media-persistence.ts`:
- Around line 97-106: Update loadMediaSession to open a single IndexedDB
connection and execute one read-only transaction covering inputKey(toolKey),
trimKey(toolKey), and outputKey(toolKey), rather than invoking readKey
separately for each key. Reuse the transaction’s object store reads to build the
existing { file, trim, output } result with null fallbacks, preserving the
current return shape.

In `@src/server/ytmusic/auth.ts`:
- Around line 46-49: Update readConfigValue to decode the captured config value
using JSON string parsing, so all standard JSON escapes such as \u003d, \u0026,
and escaped slashes are handled instead of only replacing equals signs. Preserve
the existing empty-string fallback when no matching value is found.
- Around line 99-120: Make generateAuthHeader a synchronous function by removing
async, and remove the corresponding await from its call site around line 145.
Preserve the existing cookie validation, hash generation, and returned
authorization header unchanged.
- Around line 135-142: Deduplicate concurrent page-config bootstrap calls in the
authentication flow by introducing a shared pending promise for fetchPageConfig,
reusing it whenever cachedPageConfig is absent, and assigning the resolved
result to the cache without starting duplicate requests. Update
invalidateYTMusicClient to clear pendingPageConfig alongside the other cached
client state.

In `@src/server/ytmusic/tracks.ts`:
- Around line 152-153: Remove the redundant createResult function and its call
sites in the result construction flow, returning the YTMusicResult object
literals directly. Preserve the existing YTMusicResult annotations on those
literals so type checking remains unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 7ebe6e5d-6968-4a38-b8ad-40ad4643613e

📥 Commits

Reviewing files that changed from the base of the PR and between fbfe8b3 and a750734.

📒 Files selected for processing (65)
  • .env.example
  • __tests__/app/(marketing)/rss/route.test.ts
  • __tests__/app/api/activity/combined/combine.test.ts
  • __tests__/app/api/activity/combined/route.test.ts
  • __tests__/app/api/github/contributions/route.test.ts
  • __tests__/app/api/github/events/route.test.ts
  • __tests__/app/api/spotify/auth-url/route.test.ts
  • __tests__/app/api/spotify/callback/route.test.ts
  • __tests__/app/api/spotify/dev-token/route.test.ts
  • __tests__/app/api/spotify/now-playing/route.test.ts
  • __tests__/app/api/spotify/recent/route.test.ts
  • __tests__/app/api/sync/route.test.ts
  • __tests__/app/api/ytmusic/recent/route.test.ts
  • __tests__/components/landing/activity/activity-section-client.test.ts
  • __tests__/components/projects/components/project-preview.test.tsx
  • __tests__/components/projects/server/github.test.ts
  • __tests__/components/providers/providers.test.ts
  • __tests__/features/miscellaneous/diff-checker/utils/diff.test.ts
  • __tests__/features/miscellaneous/find-replace/utils/search.test.ts
  • __tests__/features/miscellaneous/find-replace/utils/text-transforms.test.ts
  • __tests__/features/miscellaneous/json-tool/utils/json-tool.test.ts
  • __tests__/features/miscellaneous/link-extractor/utils/link-extractor.test.ts
  • __tests__/features/miscellaneous/svg-converter/utilities/svg-converter.test.ts
  • __tests__/server/github/service.test.ts
  • __tests__/server/spotify/tracks.test.ts
  • __tests__/server/ytmusic/parser.test.ts
  • __tests__/server/ytmusic/tracks.test.ts
  • next.config.ts
  • src/app/(marketing)/blog/posts/yappin/stop-using-arrow-fnc-and-HUGE-REVEAL.md
  • src/app/(marketing)/packages/[slug]/page.tsx
  • src/app/(marketing)/ytmusic/page.tsx
  • src/app/(tools)/tools/[slug]/page.tsx
  • src/app/api/activity/combined/combine.ts
  • src/app/api/ytmusic/recent/route.ts
  • src/components/landing/activity/contribution-graph.tsx
  • src/components/landing/activity/section.tsx
  • src/features/miscellaneous/components/icons/animated-icons.css
  • src/features/miscellaneous/components/media-dropzone.tsx
  • src/features/miscellaneous/components/media-trim-panel.tsx
  • src/features/miscellaneous/components/tool-quick-nav.tsx
  • src/features/miscellaneous/components/tool-renderer.tsx
  • src/features/miscellaneous/components/tool-seo-content.tsx
  • src/features/miscellaneous/components/tools-hub.tsx
  • src/features/miscellaneous/constants/tool-seo.ts
  • src/features/miscellaneous/gif-to-video/hooks/use-gif-to-video-store.ts
  • src/features/miscellaneous/hooks/use-media-session.ts
  • src/features/miscellaneous/hooks/use-tool-usage.ts
  • src/features/miscellaneous/hooks/use-trim-state.ts
  • src/features/miscellaneous/sendable-video/components/export-panel.tsx
  • src/features/miscellaneous/sendable-video/hooks/use-sendable-video-store.ts
  • src/features/miscellaneous/sendable-video/index.tsx
  • src/features/miscellaneous/sendable-video/utils/ffmpeg.ts
  • src/features/miscellaneous/utils/ffmpeg.ts
  • src/features/miscellaneous/utils/media-persistence.ts
  • src/features/miscellaneous/video-to-gif/hooks/use-video-to-gif-store.ts
  • src/features/packages/data.ts
  • src/features/ytmusic/components/ytmusic-diagnostics.tsx
  • src/features/ytmusic/types.ts
  • src/server/env.ts
  • src/server/ytmusic/auth.ts
  • src/server/ytmusic/cache.ts
  • src/server/ytmusic/index.ts
  • src/server/ytmusic/parser.ts
  • src/server/ytmusic/python-bridge.ts
  • src/server/ytmusic/tracks.ts
💤 Files with no reviewable changes (7)
  • src/features/miscellaneous/hooks/use-tool-usage.ts
  • src/features/miscellaneous/components/tool-seo-content.tsx
  • src/app/(tools)/tools/[slug]/page.tsx
  • src/server/ytmusic/python-bridge.ts
  • src/app/(marketing)/packages/[slug]/page.tsx
  • src/features/miscellaneous/components/tool-renderer.tsx
  • src/features/packages/data.ts
🛑 Comments failed to post (2)
__tests__/app/api/spotify/auth-url/route.test.ts (1)

26-44: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Test does not distinguish configured value from the fallback.

SPOTIFY_REDIRECT_URI is stubbed to the same string as the route's hardcoded fallback ('http://127.0.0.1:3000/api/spotify/callback'). The assertion on redirect_uri at Line 42-44 passes even if process.env.SPOTIFY_REDIRECT_URI is never read, because the fallback produces the identical value. Use a distinct URI for the stub to prove the environment variable actually flows through.

🔧 Proposed fix
 	it('builds an auth url with the required scopes and configured redirect uri', async () => {
 		vi.stubEnv('SPOTIFY_CLIENT_ID', 'client-123')
 		vi.stubEnv(
 			'SPOTIFY_REDIRECT_URI',
-			'http://127.0.0.1:3000/api/spotify/callback'
+			'https://example.com/api/spotify/callback'
 		)

 		const { GET } = await import('`@/app/api/spotify/auth-url/route`')
 		const response = await GET()
 		const data = await response.json()
 		const authUrl = new URL(data.authUrl)

 		expect(response.status).toBe(200)
 		expect(authUrl.origin).toBe('https://accounts.spotify.com')
 		expect(authUrl.pathname).toBe('/authorize')
 		expect(authUrl.searchParams.get('client_id')).toBe('client-123')
 		expect(authUrl.searchParams.get('redirect_uri')).toBe(
-			'http://127.0.0.1:3000/api/spotify/callback'
+			'https://example.com/api/spotify/callback'
 		)
📝 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.

	it('builds an auth url with the required scopes and configured redirect uri', async () => {
		vi.stubEnv('SPOTIFY_CLIENT_ID', 'client-123')
		vi.stubEnv(
			'SPOTIFY_REDIRECT_URI',
			'https://example.com/api/spotify/callback'
		)

		const { GET } = await import('`@/app/api/spotify/auth-url/route`')
		const response = await GET()
		const data = await response.json()
		const authUrl = new URL(data.authUrl)

		expect(response.status).toBe(200)
		expect(authUrl.origin).toBe('https://accounts.spotify.com')
		expect(authUrl.pathname).toBe('/authorize')
		expect(authUrl.searchParams.get('client_id')).toBe('client-123')
		expect(authUrl.searchParams.get('redirect_uri')).toBe(
			'https://example.com/api/spotify/callback'
		)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@__tests__/app/api/spotify/auth-url/route.test.ts` around lines 26 - 44,
Update the test around the GET request and redirect_uri assertion to stub
SPOTIFY_REDIRECT_URI with a value different from the route’s hardcoded fallback,
then assert that distinct value is returned in authUrl.searchParams. Keep the
existing client ID and response checks unchanged.
__tests__/app/api/spotify/now-playing/route.test.ts (1)

98-119: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Retry-test mock does not represent "stays unavailable".

The second fetch mock at Line 109 returns { status: 204 } without an explicit ok field. Status 204 is a success status; a real fetch Response with status 204 has ok: true. The test only passes because ok is undefined here, not because the mock represents an unavailable upstream, as the test name claims. Use a mock that reflects a genuine failure so the test verifies the intended scenario.

🔧 Proposed fix
 		vi.stubGlobal(
 			'fetch',
 			vi
 				.fn()
 				.mockResolvedValueOnce({ status: 401 })
-				.mockResolvedValueOnce({ status: 204 })
+				.mockResolvedValueOnce({ ok: false, status: 500 })
 		)
📝 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.

	it('retries once on 401 and returns a non-playing state when spotify stays unavailable', async () => {
		authMocks.hasSpotifyCredentials.mockReturnValue(true)
		authMocks.getSpotifyAccessToken
			.mockResolvedValueOnce('token-1')
			.mockResolvedValueOnce('token-2')

		vi.stubGlobal(
			'fetch',
			vi
				.fn()
				.mockResolvedValueOnce({ status: 401 })
				.mockResolvedValueOnce({ ok: false, status: 500 })
		)

		const { GET } = await import('`@/app/api/spotify/now-playing/route`')
		const response = await GET()
		const data = await response.json()

		expect(authMocks.invalidateSpotifyTokenCache).toHaveBeenCalledTimes(1)
		expect(response.status).toBe(200)
		expect(data).toEqual({ isPlaying: false })
	})
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@__tests__/app/api/spotify/now-playing/route.test.ts` around lines 98 - 119,
Update the fetch mock in the “retries once on 401 and returns a non-playing
state when spotify stays unavailable” test so both responses explicitly
represent failure, including setting the second response’s ok value to false
rather than relying on an omitted field. Keep the 401 retry and non-playing
response assertions unchanged.

- persist full 50-track window in ytmusic cache regardless of request limit
- throw YTMusicUnauthorizedError on 401/403 and invalidate cached page config
- guard media persistence behind indexedDB availability, atomic session writes, 150MB cap
- cancel media session restore on unmount, drop StrictMode-breaking started guard
- check every audio stream in isRemuxCompatible
- pin diagnostics timestamps to UTC to avoid hydration mismatch
- restore real timers after fake-timer test
- keep contribution graph arrow navigation within the week
- drop legacy python ytmusic bridge scripts
@remcostoeten

remcostoeten commented Aug 1, 2026

Copy link
Copy Markdown
Owner Author

@remcostoeten
remcostoeten merged commit 2edc404 into master Aug 3, 2026
4 checks passed
@remcostoeten
remcostoeten deleted the feat/ytmusic-native-parser branch August 3, 2026 00:49
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.

1 participant