Skip to content

Add Rekordbox Sync: compare SoundCloud likes and playlists to a collection - #28

Draft
cole-hackman wants to merge 2 commits into
mainfrom
claude/rekordbox-soundcloud-sync-75ej4r
Draft

cole-hackman wants to merge 2 commits into
mainfrom
claude/rekordbox-soundcloud-sync-75ej4r

Conversation

@cole-hackman

Copy link
Copy Markdown
Owner

What this does

Surfaces the gap between what a DJ saves on SoundCloud and what actually made it into rekordbox.

Drop a rekordbox collection XML export (File → Export Collection in xml format), pick your likes and/or playlists, and get four reports:

Report Answers
Missing from rekordbox What have I liked but never imported? (the crate-digging backlog — flags free downloads and buy links)
Needs review Which matches are uncertain enough that I should look?
Playlist drift Where has a SoundCloud playlist diverged from its rekordbox crate?
Only in rekordbox What's in my collection that I never saved on SoundCloud?

Every list exports to CSV. New page at /rekordbox-sync, wired into the sidebar, dashboard, and recent-tools tracking.

Runs entirely in the browser

No upload. The XML holds absolute paths to the user's own music files, so sending it to the server is neither necessary nor ours to do — and a 20k-track export runs to tens of MB the backend has no reason to carry. The only network calls are the existing GET /api/likes and GET /api/playlists/:id.

No new server routes, no schema changes, no new dependencies.

How matching works

The two sides share no track identifier, so every match is inferred from text — and the sides are shaped differently:

  • rekordbox — clean ID3 tags, artist and title in separate fields
  • SoundCloud — one free-text title plus an uploader who is frequently a label or promo channel

So each SoundCloud track yields several candidate readings (uploader-as-artist, artist-split-from-title, and the reverse) and the best-scoring one wins. Normalization folds accents, expands &and, strips promo decoration ([PREMIERE], FREE DOWNLOAD |, catalogue numbers like [WARP001]), and separates featured artists.

Two decisions worth flagging for review:

Versions never collapse. Version text is extracted and compared as its own field, so an original and a remix can't match each other. A DJ who owns both has two records, and reporting either as the other is worse than reporting a miss. Original Mix normalizes to "no version" so it agrees with a bare title.

Uncertainty is surfaced, not decided. Matches are graded exactstrongfuzzy → none. exact/strong count as owned; fuzzy goes to a review bucket. An exact title match with a disagreeing artist lands there rather than counting as a miss — otherwise a label upload sends you off to re-download a track you already have. Runtime disagreement over 20s penalizes a match, which is what separates a radio edit from an extended mix.

Comparing everything against everything is O(n·m) — 5k likes against a 20k collection is 100M comparisons, too slow for a browser — so candidates are narrowed by shared title token via an inverted index that prefers rare tokens.

Where the code lives

Parsing and matching sit in server/lib/ and are imported by the page across the directory boundary. That's deliberate: the repo's existing pattern for testing frontend logic is to duplicate it into the test file (see the header of tests/export.test.js), which would be a bad trade for a matching engine whose correctness is the whole feature. This way Jest exercises the same code the browser runs. It resolves because Vercel builds from the repo root (vercel.jsonnpm run build), and it's verified by a real next build below.

Module Responsibility
server/lib/rekordbox-xml.js Dependency-free scanner for the collection export. Handles XML entities, UTF-8 BOM, self-closing nodes, nested playlist folders.
server/lib/rekordbox-match.js Normalization + tiered matching.
server/lib/rekordbox-report.js Diffing, playlist pairing, assembled report.
frontend-UI/src/lib/rekordbox/index.ts Browser entry point + TypeScript types.

Testing

78 new tests, including an end-to-end pass from raw XML text through to a finished report using the messy title formats real libraries are full of — promo prefix + artist-in-title + catalogue number, & vs and, and accent folding (Björk – JógaBjork - Joga).

Tests:       201 passed, 201 total
  • npm test — 201 pass, 0 failures. One suite is marked failed: tests/soundcloud-client.test.js, which fails identically on a clean tree (verified by stashing) and is unrelated to this change.
  • npx tsc --noEmit — clean
  • npx next lint — clean
  • npx next build — succeeds, /rekordbox-sync exports at 13.2 kB

Also covered: a 5,000-track collection matched against 300 queries to confirm the index avoids quadratic blowup.

Limitations

  • The XML is a point-in-time export, so a report is only as fresh as the user's last export. Nothing writes back to rekordbox — acting on a report means importing by hand.
  • Badly tagged rekordbox files (empty Artist, filename-as-title) match poorly, and a track uploaded to SoundCloud under a wholly different name won't match at all.
  • Match decisions aren't persisted; re-running re-derives everything. If review-bucket triage turns out to be repetitive in practice, remembering confirmations is the natural follow-up.

Generated by Claude Code

…ction

Surfaces the gap between what a DJ saves on SoundCloud and what actually
made it into rekordbox. Drop a collection XML export and get back the
tracks you liked but never imported, ambiguous matches worth a look,
per-playlist drift, and collection tracks nothing on SoundCloud
accounted for. Every list exports to CSV.

Runs entirely in the browser. The XML holds absolute paths to the user's
own music files, so uploading it is neither necessary nor ours to do —
and a 20k-track export is tens of MB, which the backend has no reason to
carry. The only network calls are the existing likes and playlist
endpoints.

The two sides share no track identifier, so matching is inferred from
text. rekordbox has clean ID3 artist/title fields; SoundCloud has one
free-text title plus an uploader who is frequently a label or promo
channel. Each SoundCloud track therefore yields several candidate
readings and the best-scoring one wins. Normalization folds accents,
reconciles "&" with "and", and strips promo decoration and catalogue
numbers.

Version text is compared as its own field so an original and a remix
never match each other — reporting one as the other is worse than
reporting a miss. Matching is narrowed by an inverted title-token index,
since comparing 5k likes against a 20k collection outright would be 100M
comparisons.

Matches are graded, and the uncertain ones go to a review bucket rather
than being silently decided either way.

The parsing and matching logic lives in server/lib/ and is imported by
the page across the directory boundary, so the Jest suite exercises the
same code the browser runs instead of a mirrored copy. Adds 78 tests
covering the messy title formats real libraries are full of.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N3LQgRH2WGiHjEiBY3GoX5
@vercel

vercel Bot commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

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

Project Deployment Actions Updated (UTC)
soundcloud-tool Ready Ready Preview Aug 11, 2026 5:44pm

Two labeled corpora with explicit ground truth, so "how accurate is it"
has an answer that can be re-checked rather than argued.

The first covers the distortions the matcher was built for. The second is
deliberately hostile: untagged rips where rekordbox falls back to a
filename, multi-artist spellings, titles that differ only by a numeral,
generic titles like "Intro", and a track renamed beyond recovery.

Both currently pass in full, but the headline number is weak evidence and
the file says so: the SoundCloud side is synthetic and written by the same
hand as the matcher, so it measures the failure modes we anticipated and
guards against regressing them. It cannot measure the ones we missed.

The assertions are therefore weighted toward the failure that matters.
Recall has a floor; false ownership has none permitted at all. A miss is
visible and recoverable, a wrong "you already own this" is neither.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N3LQgRH2WGiHjEiBY3GoX5
@coderabbitai

coderabbitai Bot commented Aug 11, 2026

Copy link
Copy Markdown

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: b96d33c1-0297-45cb-8530-c261eb025d1f

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

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.

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.

2 participants