Repository navigation
Conversation
Deploying with
|
| 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 |
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
force-pushed
the
feat/ytmusic-credential
branch
from
August 17, 2026 16:10
0bc8b4f to
b97720d
Compare
samlehoy
force-pushed
the
feat/ytmusic-provider
branch
from
August 17, 2026 16:10
6e7a935 to
ffe7d3e
Compare
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #3, #7, #8, #9, #10 — and with them the epic, #1. All six directions now run.
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 itssourceandtarget, stamped byConversionso 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: onlyMUSIC_VIDEO_TYPE_ATVis a catalog song —OMV,UGC,OFFICIAL_SOURCE_MUSICand entries with no type are read but returned asskipped, 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.
MatchMethodis 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:
falsewithout editingmusic.youtube.comandyoutube.comresolve to the same playlistThe 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:
parseDurationreturned0rather thannullfor 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
Conversionrecordederror.name— always the literal"Error", since every throw here is a plainError. I flagged this in the #11 review as cosmetic. It is not: it swallowed the mix refusal's entire explanation, leavingfailed to read playlist: Errorand failing #7's acceptance criterion. Now recordserror.messageand prints it.79 → 116 tests. Typecheck, lint, build clean.
🤖 Generated with Claude Code