Skip to content

YouTube Music provider: source, target, video fallback, reorder - #15

Closed
samlehoy wants to merge 3 commits into
feat/ytmusic-credentialfrom
feat/ytmusic-provider
Closed

samlehoy wants to merge 3 commits into
feat/ytmusic-credentialfrom
feat/ytmusic-provider

Conversation

@samlehoy

Copy link
Copy Markdown
Owner

Closes #3, #7, #8, #9, #10 — and with them the epic, #1. All six directions now run.

Stacked on #14 (feat/ytmusic-credential). Merge order: #11 → #12 → #13 → #14 → this.

Two commits: #3 the report is independent; #7–#10 are grouped because they are one provider — split, each would be a PR against the same new file that cannot run without the next.

What landed

#3 — provider-neutral report. deezer_id → target_id; every row records its source and target, stamped by Conversion so no caller can forget.

#7 — YouTube Music as a source. Paged browse, mixes refused up front by id with a reason, and the music-video rule: only MUSIC_VIDEO_TYPE_ATV is a catalog song — OMV, UGC, OFFICIAL_SOURCE_MUSIC and entries with no type are read but returned as skipped, becoming their own report row rather than being silently dropped or mislabelled "no match".

#8 — YouTube Music as a target. Private playlist creation, adds, dedupe, and verification via player → videoDetails (flat, unlike the ~6-level renderer nesting everything else needs).

#9 — music video fallback. Lives in the matcher, where match rules belong (ADR 0003). Artist relaxes to containment; in exchange duration becomes mandatory, and a candidate with no duration is rejected. MatchMethod is widened rather than left closed — the gate I flagged in #11 is now cleared.

#10 — reorder. By setVideoId, an opaque per-item handle mapped from track id inside the provider. It never reaches the conversion interface (ADR 0002).

ProviderSpec.convertible — the temporary flag from #14 — is deleted, as promised.

Verified live, then cleaned up

Not just stubs. Against the real service:

Direction Result
YouTube Music → Deezer 180-entry playlist read with paging: 38 songs converted, 141 music videos skipped and reported
Spotify → YouTube Music 4/4 matched and verified, private playlist created, deleted after
Deezer → YouTube Music same
Re-run into the same playlist 0 added — dedupe holds
Reorder order matched exactly; second call returned false without editing
Mix / link forms mix refused with its reason; music.youtube.com and youtube.com resolve to the same playlist

The account was left exactly as it was — every playlist created for testing was deleted.

Fixtures caught a real bug

Parsing is pinned by four responses recorded from the live service and trimmed by hand, as #7 and #8 both demand. That immediately caught one: parseDuration returned 0 rather than null for a non-duration, which silently swallowed the search-result duration fallback. A hand-written literal would have encoded my assumption and stayed green.

One old bug, now load-bearing

Conversion recorded error.name — always the literal "Error", since every throw here is a plain Error. I flagged this in the #11 review as cosmetic. It is not: it swallowed the mix refusal's entire explanation, leaving failed to read playlist: Error and failing #7's acceptance criterion. Now records error.message and prints it.

79 → 116 tests. Typecheck, lint, build clean.

🤖 Generated with Claude Code

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Aug 17, 2026 •

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Preview URL Updated (UTC)
✅ Deployment successful!
View logs
plx 661981b Commit Preview URL

Branch Preview URL
Aug 17 2026, 10:56 PM

samlehoy and others added 2 commits August 17, 2026 23:10
Closes #3.

- The matched-identifier column is renamed deezer_id -> target_id. It holds the
  match in the target's own terms, which means the same thing in all six
  directions rather than being right in one of them.
- Every row records the source and target provider, stamped by Conversion
  itself so no caller can forget.
- Column order is now:
  playlist,source,target,title,artist,isrc,matched,target_id,method,note
- Report content is otherwise unchanged: the matched flag, the match tier, and
  the notes keep their meanings.

Both existing directions already produce a report in the test suite, so both
are covered: the Spotify->Deezer test asserts the row's source/target, and the
Deezer->Spotify test asserts the full CSV line.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Closes #7, #8, #9, #10. All six directions now run, completing #1.

Grouped because they are one provider: splitting them would mean four PRs
against the same new file, each unable to run without the next.

#7 — read a YouTube Music playlist
  - browse VL<id> with continuation paging; title from microformat.
  - Only MUSIC_VIDEO_TYPE_ATV counts as a catalog song. OMV, UGC,
    OFFICIAL_SOURCE_MUSIC and entries with no type are music videos or
    unavailable: read, but returned as `skipped`, since a video's stated artist
    is the uploading channel and matching on it produces confident wrong
    answers. Conversion records them as their own report row.
  - Mixes are refused up front by id (RD…) with an explicit reason.
  - music.youtube.com and youtube.com links resolve to the same playlist;
    Liked Music (LM) and album playlists (OLAK5uy_) are accepted.

#8 — write songs to a YouTube Music target
  - playlist/create with privacyStatus PRIVATE, browse/edit_playlist for adds.
  - Verification via player -> videoDetails, which is flat rather than the
    ~6-level renderer nesting; a catalog track's author is the "<Artist> -
    Topic" channel, stripped.

#9 — music video fallback
  - matchVideoCandidates in the matcher, where match rules live (ADR 0003).
    Artist relaxes to containment (source artist must appear in the video's
    title or channel); in exchange duration tolerance becomes mandatory and a
    candidate with no duration is rejected. Records its own tier, 'video'.
  - MatchMethod is widened rather than left closed — the tier set is open.

#10 — reorder a YouTube Music target
  - Moves by setVideoId, an opaque per-item handle mapped from track id inside
    the provider. The handle never reaches the conversion interface (ADR 0002).

Also here:
  - ProviderSpec.convertible, the temporary flag from #6, is deleted now that
    every provider converts.
  - Conversion records error.message, not error.name. Every throw here is a
    plain Error, so the report's one diagnostic always read the literal "Error"
    — which silently swallowed the mix refusal's explanation. Flagged in the #2
    review; it only became an AC failure here.
  - Provider.playlistName is an optional capability, so a provider that can
    resolve its own titles does better than the registry's id fallback.

Parsing is pinned by four responses recorded from the live service and trimmed
by hand (tests/fixtures/ytmusic/). That caught a real bug: parseDuration
returned 0 rather than null for a non-duration, which silently swallowed the
search-result duration fallback.

Verified live, then cleaned up: YouTube Music -> Deezer read 180 entries with
paging (38 songs converted, 141 videos skipped); Spotify -> YouTube Music and
Deezer -> YouTube Music each matched and verified 4/4, created a private
playlist, added 0 on a second run into the same playlist, and were deleted.
Reorder applied to a real playlist matched the requested order exactly and
no-opped on the second call. 79 -> 116 tests.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@samlehoy
samlehoy force-pushed the feat/ytmusic-credential branch from 0bc8b4f to b97720d Compare August 17, 2026 16:10
@samlehoy
samlehoy force-pushed the feat/ytmusic-provider branch from 6e7a935 to ffe7d3e Compare August 17, 2026 16:10
Found in a ready-to-run audit, not by a ticket:

- --to's help said "required unless --dry-run". It has been required in every
  case since #5; a dry run still needs to know what it is matching into.
- Help examples and package.json description named two providers.
- CONFIG.md still said "at most two cookies", listed a Deezer account as a
  prerequisite, and named the auto-fetch menu entry in Indonesian ("Ambil
  kredensial otomatis") from before the UI was translated.
- matchTrack had no callers anywhere. Deleted.

Verified by packing the tarball and installing it clean: the binary links,
--help is accurate, and a credential-free dry run from Spotify into YouTube
Music matched 50/50 (both services' search endpoints are public).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@samlehoy
samlehoy deleted the branch feat/ytmusic-credential August 17, 2026 23:33
@samlehoy samlehoy closed this Aug 17, 2026
@samlehoy
samlehoy deleted the feat/ytmusic-provider branch August 17, 2026 23:33
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