feat(import): playlist files, JSON, ListenBrainz and Last.fm as import sources
Implements qbz-nix-docs/deferred-2.0.3/playlist-importer-expansion-design.md, whose §11 the owner ratified 2026-07-31. PlaylistSource is an enum, not a trait (ADR-007): the set is closed, there is no plugin story, and every arm resolves to the same ImportPlaylist the URL scrapers already produce — so the matcher, the 2000-track split, the sink and the summary never learn a second kind of source exists. import_prepared_playlist takes the playlist BY VALUE and import_public_playlist becomes a shim. That kills the double scrape the Tauri re-fetch-on-execute left behind, and it is not optional for the new sources: the rfd bytes of a picked file are gone by execute time and the dialog cannot silently reopen. It also closes a real race — open() resets the session, so a modal reopened between the Import click and the task's read handed a detached task None. FILES read the track list ONLY. The paths inside are never opened, copied or added to the Local Library, and the modal says so in a persistent row rather than a tooltip. Content sniffing beats the extension, with an HLS guard because .m3u8 is also the stream-manifest extension, and a utf8-then-Windows-1252 fallback because .m3u and .pls predate UTF-8 and "Björk" has to stay matchable. JSON locates the track list BY SHAPE. Depth is a DoS bound, not a locator: the array sits at 0, 1, 2 or 3 levels depending on the export, and a hard two-level rule drops an ordinary GraphQL envelope. Container demotion keeps a discography from importing album names as track titles. Walls at 16 MiB, depth 8, 2M nodes and 10k tracks. SERVICES are public reads with no authentication; a connected account only prefills the field. No qbz-integrations edge — its ListenBrainz client returns tracks with no duration and drops the playlist title, both verified in its source, and a direct GET /1/playlist/<mbid> reads all three in one request. A Last.fm STATION IS A RADIO, and ?page=N is not pagination. Measured against a live profile: pages 1 and 2 overlapped and batch sizes varied (20, 20, 36). The first cut fetched 20 pages, never hit its empty-page exit, and returned 469 rows holding 198 distinct tracks. It now dedupes as it draws, stops after two dry draws, and stops at MAX_STATION_TRACKS = 200 — the number shown in "Found N tracks" has to be one we can deliver, and a station is dynamic enough that a bigger single import buys little (owner ruling: no false expectations of "traeré casi 500" when we bring half). A failed draw no longer fails an import that already has rows; Last.fm rate-limits a burst of these. The summary reports THREE DISJOINT NUMBERS that sum to the total. skipped is now counted from the matches that found nothing, not inferred as total - matched, which folded duplicates into failures: that same import told the user 271 tracks were skipped when 255 of them had matched perfectly well and been deduplicated before the add. 23 new strings across the seven translated locales; en is the sparse fallback catalogue and needs none. cc
This commit is contained in:
27 files changed
+4212
-34
No files matched your search
@@ -23,6 +23,17 @@ thiserror = { workspace = true }
|
||||
# Logging
|
||||
log = { workspace = true }
|
||||
|
||||
# Playlist-FILE sources (2.0.3 expansion). Both were already resolved in the
|
||||
# workspace lock, so neither adds a download or a new compilation unit to the
|
||||
# tree — only an edge from this crate.
|
||||
# roxmltree : XSPF. A read-only DOM walked by local element name is ~30 lines;
|
||||
# quick-xml's event state machine only pays off for streaming,
|
||||
# which the 16 MiB byte wall forecloses.
|
||||
# encoding_rs: the utf8-then-Windows-1252 fallback for legacy .m3u / .pls,
|
||||
# which is what keeps "Björk" and "Sigur Rós" matchable.
|
||||
roxmltree = "0.20"
|
||||
encoding_rs = "0.8"
|
||||
|
||||
# Async runtime + concurrency
|
||||
tokio = { workspace = true }
|
||||
futures-util = { workspace = true }
|
||||
Reference in new issue
Block a user