What's Changed
ListenBrainz playlist sync
You can now connect ListenBrainz as a full playlist sync target alongside Spotify and Apple Music, with cross-service collaboration as the marquee feature - which means your playlists are now live and shareable on Achordion.xyz too.
- Push and pull playlists. Local playlists mirror to LB; LB playlists pull into Parachord. JSPF format, recording MBID as the per-track identifier.
- Multi-platform collaboration. A Spotify user and an Apple Music user can share an LB playlist where either's edits propagate to both streaming services via LB as the hub. Bob edits on Apple Music → LB picks it up → Alice's Parachord pulls and pushes to Spotify. A "SHARED" badge in the playlist UI shows which playlists you're collaborating on.
- Default private. Playlists pushed to LB start private. Make them public on listenbrainz.org if you want others to see them.
- MBID-required tracks are surfaced clearly. Tracks without a resolvable recording MBID are listed as
unresolvedTracksso you can see what didn't push.
The sync setup wizard now includes ListenBrainz alongside the other providers.
Background sync stays out of your way
Several invariants were broken in earlier versions — background sync would spike CPU to 120%, lock the UI with a pinwheel, and step on your active playback. The subsystem has been rebuilt around eight reinforcing fixes:
- Skip-unchanged playlists. Inbound updates short-circuit when the remote snapshotId and our local one match — no spread-rewrites for playlists that haven't changed. With a log line so you can tell "short-circuited, no work" apart from "never reached" when debugging.
- Concurrency cap on resolver searches. Non-Apple/Spotify resolvers (Bandcamp, SoundCloud, YouTube, etc.) now go through a global limiter — 4 concurrent, 100ms gap, exponential backoff on throttle. Stops the "374 local files all fire a Bandcamp search at once" pattern that pinned CPU.
- Push loop yields to idle. The renderer-side push loop wraps each iteration in
requestIdleCallback, so it does work only when the renderer is otherwise quiet. - Cancel-on-focus, fixed. When you cmd-tab back from another app mid-sync, sync now cancels at the next yield point and writes its partial result instead of hanging the UI for tens of seconds. An earlier version of this feature was firing on every in-app interaction too (sidebar clicks, panel switches), starving sync for active users — that's fixed: cancellation now only fires on actual blur → focus transitions.
- Cancel honored deep inside library fetches. Spotify's paginated library fetch loop now checks the cancel signal at every page boundary, so cmd-tab-back doesn't get stuck waiting for the next 50-track page.
- Per-provider playlist mutex. Two concurrent code paths (timer-driven sync + post-wizard sync) used to race for the same playlist creation — could create duplicate remote playlists. They now coordinate via a per-provider mutex.
- Staggered playlist sync. Instead of churning all selected playlists every 15 minutes, the inbound loop processes a small batch per cycle, oldest-stale first. A user with 50 playlists rolls through the whole set in ~2.5h worst-case instead of pinning a CPU core every quarter-hour. Manual "Sync Now" still does everything immediately.
- Hosted XSPF poll yields to idle. The 5-minute external playlist check now runs during a quiet renderer frame instead of fighting whatever else is on the main thread.
Net effect: open the app, work, sync happens while you're away. No spinning fan when you walk back to your desk, no killed sync runs because you clicked the sidebar.
Faster lists, everywhere
Virtualization now applies to the last few places that were still rendering every row:
- Library tracks list. Previously sluggish on libraries of 10k+ tracks; now smooth at any size.
- Playlist tracks. Long playlists no longer jank when you scroll.
- Search results. Big result sets render incrementally.
Library album art shows up immediately
Tracks loaded into the Library view now surface album art from three sources on first paint instead of waiting for play-time resolution:
- Folder art (
cover.jpg/folder.jpgnext to your audio files) - Embedded art baked into ID3 tags (when the cache file has been previously extracted)
- Cover Art Archive art that was fetched and cached during a previous playback session
Previously only the play-time resolver path saw all three — bulk Library loads saw none of them, so well-tagged collections showed placeholders even though the art was on disk. Mature collections will see the difference most.
Achordion playlist mirror links
After any sync write that creates or updates a ListenBrainz-anchored playlist, Parachord pushes the LB MBID and its mirror links (Spotify, Apple Music) to Achordion. The result: a public https://achordion.xyz/playlist/<mbid> URL where anyone can see all the streaming-service mirrors of a shared playlist in one place — handy for sharing a collaborative playlist across services when each collaborator uses a different one.
Playlist share URLs in the app now point through Achordion for the same reason — richer metadata, single canonical link.
Hosted XSPF playlists no longer duplicate on sync
If you imported the same hosted XSPF URL on both desktop and Android, sync used to create two remote copies. Fixed: syncedTo and sourceUrl are preserved across re-imports, so the second device recognizes the first device's already-synced mirror and reuses it instead of creating a duplicate.
Collection file is now corruption-resistant
collection.json is written via an atomic-rename pattern (write to .tmp, then rename). If the app crashes or is force-quit mid-write, the canonical file is left in either the old or new state — never partially-written. The "Unterminated string in JSON at position N" load failure that some users were hitting after force-quits is gone.
Smaller things
- Diagnostic logs around sync. Local/remote track counts and snapshot previews are logged when a "has updates" diff fires, so it's easier to tell whether a flagged update is real or noise.
- ListenBrainz username caching avoids redundant token-validate calls — two per sync cycle instead of 2N.
- Heal pass for imported playlists with the wrong
syncedFromresolver runs on every startup. Fixes a class of corruption that a now-removed code path used to introduce; cross-platform sync is more robust as a result. - Auto-creating LB mirrors of local playlists is opt-in. Previously this happened silently for some playlist shapes; now it requires an explicit toggle.
- Removed log spam around Apple Music false-positive "has updates" detection.
Full Changelog: v0.9.2...v0.9.3