Skip to content

refactor: one canonical YouTube URL rule in server/lib/youtubeUrl.js (#6014) - #6122

Merged
atomantic merged 1 commit into
mainfrom
claim/issue-6014
Sep 3, 2026
Merged

refactor: one canonical YouTube URL rule in server/lib/youtubeUrl.js (#6014)#6122
atomantic merged 1 commit into
mainfrom
claim/issue-6014

Conversation

@atomantic

Copy link
Copy Markdown
Owner

Summary

Pasting a music.youtube.com, /shorts/, /live/, or /embed/ link into Music Video track import returned 400 YOUTUBE_URL_INVALID ("Not a recognized YouTube URL") — even though yt-dlp handles all of them and both the brain YouTube ingest and the Google Takeout importer already accepted them.

The cause was copy-paste drift. The same question — is this one YouTube video, and which one? — was answered by four separate copies of the regexes:

  • an older regex in services/trackYoutubeImport.js, duplicated verbatim in routes/tracks.js, that predated music. / shorts / live / embed support;
  • a newer one in services/youtubeIngest.js (YOUTUBE_INGEST_URL_RE);
  • the id parser sitting inside services/youtubeImport.js (the Takeout ZIP importer);
  • a fifth mirror in client/src/lib/youtubeUrl.js.

Because the id parser lived in a service, server/lib/youtubeUrl.mirror.test.js had to import from server/services/ — a library reaching into services, inverting the dependency rule.

This PR extracts the rule to server/lib/youtubeUrl.js:

Export Purpose
YOUTUBE_VIDEO_URL_RE Accepts watch / shorts / live / embed across the www. / m. / music. hosts; still rejects playlists, channels, and /@handle feeds so a batch paste can't kick off a 300-video download.
youtubeVideoIdFromUrl(url) (alias youtubeVideoId) The video id, or null.
isYoutubeVideoUrl(url) Regex + id parse, the way every validation caller needs it.
assertYoutubeVideoUrl(url) Returns the id, else throws 400 YOUTUBE_URL_INVALID.
YOUTUBE_URL_INVALID_MESSAGE Shared rejection copy, so the Zod schema and the services name one rule.

Every server caller now imports from there. The established service-level names (YOUTUBE_INGEST_URL_RE, assertYoutubeIngestUrl, YOUTUBE_URL_RE, assertYoutubeUrl) remain as re-export aliases, so no downstream import had to change. Net effect for users: track import accepts every URL shape the other two pipelines do, and the rejection message names all of them.

Behavior-neutral for the brain ingest and the Takeout importer — the regex and the id parser they get are byte-identical to what they declared before.

Files

  • new server/lib/youtubeUrl.js, re-exported from server/lib/index.js and catalogued in server/lib/README.md.
  • services/trackYoutubeImport.js, routes/tracks.js — drop the drifted regex, import the canonical one (this is the user-visible fix). ServerError was left unused in trackYoutubeImport.js and removed.
  • services/youtubeIngest.js, services/youtubeImport.js, services/youtubeSync.js — import the shared rule/parser.
  • server/lib/youtubeUrl.mirror.test.js — now compares server/lib/youtubeUrl.js against the client mirror; no server/services/ imports remain.
  • client/src/lib/youtubeUrl.js — doc comment only, repointed at the new authoritative module.

Test plan

  • server/lib/youtubeUrl.mirror.test.js — unchanged case matrix, still asserts server↔client parity on both the predicate and the extracted id; now with the illegal cross-layer import gone.
  • server/services/trackYoutubeImport.test.js — new regression cases asserting music.youtube.com, youtube.com/shorts, /live/, and /embed/ are accepted (each 400'd before this change), that assertYoutubeUrl hands back the video id, and that playlists / channels / feeds are still refused with /single-video YouTube URL/.
  • server/routes/tracks.test.js — the route-level counterpart: the trackYoutubeImport mock no longer stubs YOUTUBE_URL_RE (the route reads the real rule from lib/), and POST /api/tracks/import/youtube is asserted to return 202 and reach the service for all four previously-rejected shapes.
  • Full server suite: 1917 files passed / 1 skipped, 38686 tests passed (cd server && npm test).
  • Client src/lib suite: 136 files / 2133 tests passed.
  • No DB-backed suites run.

Closes #6014

https://claude.ai/code/session_01VjkWVTfzKyRuAv3HEsspwN

…6014)

Music Video track import rejected `music.youtube.com`, `/shorts/`, `/live/`,
and `/embed/` links with `400 YOUTUBE_URL_INVALID` even though yt-dlp handles
them and both the brain ingest and the Google Takeout importer already accepted
them. The cause was drift: the same "is this one YouTube video, and which one?"
question was answered by four separate copies of the regexes — an older one in
`trackYoutubeImport.js` (duplicated again in `routes/tracks.js`) that predated
`music.`/shorts/live/embed support, a newer one in `youtubeIngest.js`, the id
parser buried in `youtubeImport.js`, and a fifth mirror on the client.

Extract the rule to `server/lib/youtubeUrl.js` (`YOUTUBE_VIDEO_URL_RE`,
`youtubeVideoIdFromUrl` / `youtubeVideoId`, `isYoutubeVideoUrl`,
`assertYoutubeVideoUrl`, `YOUTUBE_URL_INVALID_MESSAGE`) and point every server
caller at it. The existing service-level names (`YOUTUBE_INGEST_URL_RE`,
`assertYoutubeIngestUrl`, `YOUTUBE_URL_RE`, `assertYoutubeUrl`) stay as
aliases, so nothing downstream had to change.

This also removes the layering violation the duplication forced: the id parser
lived in a service, so `server/lib/youtubeUrl.mirror.test.js` had to import
from `server/services/` — libraries reaching into services.

Track import now accepts every URL shape the other two pipelines do, and the
rejection message names all of them.

Claude-Session: https://claude.ai/code/session_01VjkWVTfzKyRuAv3HEsspwN
@atomantic
atomantic merged commit f61ec20 into main Sep 3, 2026
7 checks passed
@atomantic
atomantic deleted the claim/issue-6014 branch September 3, 2026 22:29
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.

Extract canonical server/lib/youtubeUrl.js to eliminate duplicated YouTube regexes, URL parsers, and cross-layer imports

1 participant