Commit Graph
106 Commits
Author SHA1 Message Date
vicrodh a39ea8c12d feat(settings): Import / Export section with portable blacklist and account migration
A new top-level Settings section, Import / Export, gathers everything that
moves data in or out of this install:

- SETTINGS PORTABILITY: the settings bundle export moves here from
  Developer, with a note on what the bundle does not hold (favorites,
  playlists, blacklist, purchases).
- BLACKLIST: export the blocked artists and albums as a JSON one user can
  hand to another; import merges additively and never touches the enabled
  flag (qbz_app::settings::blacklist_portable, frontend-agnostic).
- ACCOUNT MIGRATION: create a snapshot while signed in with the old
  account, then migrate it into the account signed in now, with the local
  profile copy and its three opt-in toggles (media servers, scrobblers,
  listening history, all on). The disclaimer states that subscriptions and
  purchases do not move. Progress and results are status lines in the
  settings document; the favorites cache re-warms and the sidebar reloads
  after a run, the local half asks for a restart.

Also: the header shows a translated "Member" for accounts without a
subscription, and the login screen says what a member can and cannot do.
50 new strings in all eight catalogs.

cc
2026-09-02 08:24:02 -06:00
vicrodh 07e683f5ef feat(purchases): detail polish and a Downloads section in the nav flyout
The purchase detail badges what was bought on every row (DSD128 on a
DSD purchase, not the catalog's 24/352.8), marks the formats already on
disk with a check in the dropdown, offers a track filter, links to the
album in the Local Library once its folder is indexed, and gives the
download the accent while Play drops to a plain control of the same
size — on this screen the download is the point.

Downloads are process-wide and survive leaving the page; the nav flyout
now lists the purchases with a download in flight under "Downloads · N"
so the user can find them again, each row reopening its detail.

cc
2026-09-02 01:30:12 -06:00
vicrodh e41235f791 feat(purchases): readable dates, a release-date column and sort, column labels
The list printed the purchase date as "9/1/26" and nothing else, so a
row said neither when the record came out nor, at a glance, what the
number meant. Rows now carry the catalog's original release date next to
the purchase date, both as "Sep 1, 2026", under a column-label strip
like the album list's, and the toolbar sorts by release date (`released`)
beside the four existing keys.

cc
2026-09-01 23:59:36 -06:00
vicrodh 3c8ab80be5 refactor(purchases): drop the blind-mode disclaimer modal
Purchases is now exercised against an account with real purchases, so
the one-time "built in blind mode" notice no longer describes the
feature. Remove the modal, its controller state, the bridge invokable,
the persisted pref and the three strings from the seven catalogs that
carried them.

cc
2026-09-01 23:40:33 -06:00
vicrodh f6e1c4e023 fix(qconnect): harden queue ownership handoffs
Preserve local playback when it is authoritative, make renderer conflicts explicit and persist the selected policy. Filter unsupported queue rows, restore portable Qobuz identities after disconnect, and fence takeover commands to avoid stale queue and playback loops.

cx
2026-09-01 17:23:14 -06:00
vicrodh bb3a971c0c feat(qt): About for 2.1.0 — codename, contributor sweep, sponsors
Codename becomes 'Rebuild Q (You Can (Not) Redo)'. The contributor list
gains the ten people whose work shipped since the last sweep — merged
PRs (b0bbywan, Ronjar, eldios, herder, PhilipVinc, Mazipani) and the
house rule for adopted work without a merge (pbaart's Dutch locale,
stshow's DLNA strict-renderer handling, RayneGit's Wayland clipboard
feature flag, LuckyTheCoder's Liquid Glass icon from issue #712) — and
the README list is now identical to the modal's.

New Sponsors section: public GitHub sponsors (linked chips) then Ko-fi
supporters by their public display names, in a fixed three-row strip
that scrolls itself, closed by the translated anonymous-sponsors line.
The panel grows to 920x860 and the modal now eats the wheel on scrim
and panel.

cc
2026-08-31 11:50:32 -06:00
vicrodh ad4d48eeef feat(qt): one-time blind-mode disclaimer on Purchases
The WindowsDisclaimerModal mechanism on the purchases pref seam: a
purchases_disclaimer_seen key seeded on entry, a disclaimerVisible field
in the list document, and an OK that persists the dismissal for good. The
overlay uses the DiscoverConfigModal wheel-lock shape; the scrim does not
dismiss, so a stray click can never acknowledge it. Body ships as msgids
in all locales with the Star Wars line kept in English everywhere.

cc
2026-08-31 11:35:45 -06:00
vicrodh 5e6daf0e81 feat(qt): Open containing album on cortinilla track rows
CortRow gains an albumId — the Qobuz album id for catalog tracks, the
album group key for local ones (both are what crate::open_album routes),
empty elsewhere. The track menu gates its new entry on the field, the
action funnels through cortinillaMenuAction like its siblings, and the
new msgid ships in the seven non-English locales.

cc
2026-08-31 11:26:17 -06:00
vicrodh 7f702f118d feat(qt): edit context menu on the header search input
QML TextInput has no system edit menu; a RightButton overlay opens a
CardMenu with Cut / Copy / Paste / Select all. Menu cut/paste are
programmatic edits that never emit textEdited, so the live-query hook is
factored out and re-run after them. New scissors/clipboard glyphs baked
across the six tints; Cut and Paste msgids added to the seven non-English
locales.

cc
2026-08-31 11:26:12 -06:00
vicrodh 6cdbd12bd0 feat(qt): Play all later in the Popular Tracks group menu
The section-level menu offered Play all next and Add all to queue with no
block-tail option; enqueue_track_list_mode already carries the "later"
arm, so only the entry was missing. New msgid translated in the seven
non-English locales.

cc
2026-08-31 11:09:53 -06:00
vicrodh 4c4647bdf4 merge: add-to-playlist redesign — membership index, picker, local drag
cc
2026-08-31 03:35:35 -06:00
vicrodh c37768459a merge: library explorer collapse tooltip and filter strip
cc

# Conflicts:
#	crates/qbz-i18n/translations/de/LC_MESSAGES/qbz-ui.po
#	crates/qbz-i18n/translations/en/LC_MESSAGES/qbz-ui.po
#	crates/qbz-i18n/translations/es/LC_MESSAGES/qbz-ui.po
#	crates/qbz-i18n/translations/fr/LC_MESSAGES/qbz-ui.po
#	crates/qbz-i18n/translations/ja/LC_MESSAGES/qbz-ui.po
#	crates/qbz-i18n/translations/nl/LC_MESSAGES/qbz-ui.po
#	crates/qbz-i18n/translations/pt/LC_MESSAGES/qbz-ui.po
#	crates/qbz-i18n/translations/ru/LC_MESSAGES/qbz-ui.po
2026-08-31 03:35:35 -06:00
vicrodh 0c874404b6 fix(qt): explain blocked QConnect logout
Keep fail-closed teardown semantics while telling the user how to recover when logout cannot proceed safely.

cx
2026-08-31 03:26:43 -06:00
vicrodh 4984736370 feat(qt): picker modal pins last playlist and shows containment strip
The dropdown pins the session's last successful target above the
results (clock glyph, de-duplicated by Rust, retained under a search
only when it matches). The body gains the collapsible Already in strip:
one card per writable playlist already holding carried tracks, with an
N-of-M badge on partial multi-track containment; clicking a card
selects that playlist so completing a partial add is one click plus the
duplicate-confirm's only-new answer. The strip renders only when the
index can back the claim — complete with matches, or updating under an
explicit caveat — and an empty answer from an incomplete index renders
nothing. Four new strings in all eight locales.

cc
2026-08-30 17:23:58 -06:00
vicrodh 9486346130 feat(qt): explorer collapse caret tooltip and collapsed filter strip
The Library Explorer caret now says what it does on hover (Show/Hide
filter panels, all eight locales). While the panels are collapsed with
filters applied, a single-line strip of removable chips stays visible
next to the caret (top of the results area for the left/right rail
layouts), so hiding the panels never hides what is filtering the
results. The chip is the empty-state delegate extracted into a shared
component; removal reuses clearFilterChip verbatim.

cc
2026-08-30 16:17:29 -06:00
vicrodh 96342e23bc feat(qt): expose ALSA hardware volume settings
Add capability-aware enablement, exact control selection, diagnostics, live volume synchronization, and translated unsupported-state feedback to the Qt settings surface.

cx
2026-08-30 09:14:29 -06:00
vicrodh 9b14740e32 feat(settings): Listening history toggle and clear; history read gate
Settings > Integrations > PRIVACY: a Listening history toggle (default
ON; the state lives in the per-user listen_meta, so it follows the
account) and a confirmed Clear (DELETE + VACUUM). Strings in all eight
catalogs; README names where the data lives.

ScrobblerSettings::history_read_allowed(service) = *_active(): the
scrobbling switch now governs reading a service's history for
Recommendations as well as sending to it, and its Settings text says
so. recommendations_qt uses it instead of *_is_authed().

cc
2026-08-28 20:30:43 -06:00
vicrodh c7a3012955 feat(local-library): complete catalog and metadata workflows
Add bounded native library models, complete grouping rails and scan progress. Bring source-aware metadata editing, media information and collection offline preflight to Qt, and repair inherited Plex album artwork without overriding healthy tokens.

cx
2026-08-28 18:50:05 -06:00
vicrodh e993bc1e4d merge: integrate PR #724 hotkeys and Vim keymap
Resolve translation conflicts against pre-release and harden miniplayer dispatch, direct-output volume locks, preset collision state, and Spanish labels during review.

cx
2026-08-28 07:02:33 -06:00
vicrodh 912638c387 merge: integrate Windows port from PR #725
cx
2026-08-28 04:18:06 -06:00
vicrodh 2df2dafe67 fix(audio): make ALSA transitions capability-aware
cx
2026-08-28 02:58:04 -06:00
Niklas Herder b781ed283f feat(hotkeys): selectable Vim keymap preset
The preset swaps the BASE shortcut table underneath the user's overrides
instead of rewriting them, so Default <-> Vim is lossless. Vim binds single
modifier-free keys: n/p, h/l, Shift+J/K, /, and Ctrl+o / Ctrl+i for the jump
list; rows with no vim idiom keep their defaults.

j and k are deliberately left UNBOUND. They belong to a per-list cursor,
which QBZ has no notion of yet, and a global action bound there now would
have to be taken back the day one lands. A test holds them free.

Where an override has already claimed a shortcut the new base wants, that
action goes unbound rather than double-binding a combo that
action_for_shortcut would otherwise resolve by BTreeMap id order.

Seven new tests, including a gate that fails if any preset ever binds one
shortcut to two actions.
2026-08-28 07:27:43 +02:00
Niklas Herder 04f9afe83f feat(hotkeys): Spotify-parity playback actions
Eight Playback actions the table never had, on Spotify's desktop defaults:
volume up/down (Ctrl+Up/Down), mute (M), shuffle (Ctrl+S), repeat (Ctrl+R),
like (Alt+Shift+B), and a surface-independent seek (Ctrl+Shift+Left/Right).

Each reuses an existing transport seam, and the immersive bare-arrow seek
rows are untouched. Volume honours the player bars' volLocked predicate, so
the hotkey cannot move the slider on an ALSA-Direct hw path where the engine
documents the write as a no-op. Like rides a QML seam, as nav.search does,
because the track id is a QbzPlayer property rather than part of the Rust
now-playing model.

Two new tests; labels added to all eight catalogs.
2026-08-28 07:27:03 +02:00
vicrodh a21d33fe3b i18n: the Windows disclaimer's chrome, in the seven translated locales
Three strings: the modal's title, its "Don't show this again" box and its
button. The BODY is not among them and stays English on purpose -- it is the
author's statement of what the Windows build does and does not promise, and the
wording is the point.

`en` is deliberately untouched. That catalogue is sparse, carrying only the
strings whose English differs from the msgid, and a missing lookup falls back
to the msgid, which is already the English text.

The register follows what each file already had, "I understand and accept these
terms" in particular, so the button reads like the rest of the app rather than
like a second voice arriving.

cc
2026-08-27 19:38:24 -06:00
vicrodh b1e27457e8 feat(qt): expand immersive visualizer experience
cx
2026-08-27 19:16:27 -06:00
vicrodh 6e62724b4b fix(qt): preflight preferred GPU presentation
cx
2026-08-27 17:54:27 -06:00
vicrodh d555b8fde4 feat(qt): add album quick view and queue focus controls
cx
2026-08-26 16:29:16 -06:00
vicrodh edcb12b8a1 feat(qt): close migration parity gaps
cx
2026-08-26 11:11:50 -06:00
vicrodh aadad1da54 feat(qbz-qt): restore offline and logged-out scrobbling
Port the Tauri immediate and accumulated offline policies into the active Qt fire path, retain independent scrobbler sessions across Qobuz logout behind a default-on opt-out, and flush queued Last.fm/ListenBrainz plays only when the effective policy permits it.

cx
2026-08-25 21:21:37 -06:00
vicrodh 1517b4c567 feat(qbz-qt): add global compact album headers
cx
2026-08-25 20:50:39 -06:00
vicrodh 82953c72da fix(qbz-qt): honor system font with script fallback
cx
2026-08-25 20:15:38 -06:00
vicrodh dcf4509aa0 refactor(ui): rename the queue view
Present the expanded queue surface as Listen list across its header, launcher, and supported translations.\n\ncx
2026-08-25 02:52:11 -06:00
vicrodh 0a3bdc3ec8 feat(player): add seekbar waveforms
Analyze audible tracks into a progressive 512-bin RMS document, cache complete results, and preserve gapless track boundaries. Render the waveform through a scenegraph item in both player bars behind a default-off preference.\n\ncx
2026-08-25 01:16:58 -06:00
vicrodh d11e946227 feat(visualizer): add stereo scopes
Add a theme-aware goniometer and pitch-locked oscilloscope to the large dock and immersive focus modes.

Gate scope DSP and scenegraph updates by actual panel visibility so hidden modes remain idle, including while shader scenes cover immersive panels.

cx
2026-08-25 00:22:40 -06:00
vicrodh 284db1bfd5 chore(i18n): translate local library workflows
Cover the metadata editor, genre browsing, local tab ordering, and related validation feedback across all eight maintained locales.

cx
2026-08-24 19:47:21 -06:00
vicrodh 5a14eef258 fix(metadata): complete local editor sessions
Consume preflight data when loading clears so the modal cannot spin indefinitely.

Reuse the editor for ephemeral folders and bind sidecar or direct writes to the exact active session.

cx
2026-08-24 17:28:54 -06:00
vicrodh 0e53837582 feat(metadata): restore local tag editor in Qt
Restore the selected-version editor with sidecar-safe defaults, inspected tag layers, guarded MusicBrainz and Discogs lookup, and confirmed verified direct writes. Keep paths Rust-owned and reject stale or injected drafts.

cx
2026-08-24 16:41:06 -06:00
vicrodh 04184d9d8f feat(navigation): add cascading local media menu
cx
2026-08-22 21:25:29 -06:00
vicrodh 5e4d7ca033 fix(queue): preserve metadata and polish extended view
cx
2026-08-22 20:58:46 -06:00
vicrodh ef333cd7ec fix(login): point a never-had-an-account user at the door that is for them
Local-only mode already WORKED — verified by driving a genuinely fresh
profile (no credentials, keyring unreachable): "Start offline" enters the
guest profile, Local Library opens with its proper empty state, Settings
renders in full, zero QML complaints. The capability was never the
problem. Two pieces of the screen were.

1. THE SIGNPOST WAS OFF FOR EXACTLY THE RIGHT PERSON. The callout with
   the caret pointing at the Start-offline link was gated on
   `hasPreviousSession && connectivity === 2`. `hasPreviousSession` is
   seeded from `load_last_user_id()` — None on a fresh profile — and only
   ever set true by a successful Qobuz login. So the one piece of UI that
   says "you can use this without an account" could not be seen by
   anybody who had never signed in. The gate was written for the
   returning-user-with-no-wifi case and the guest case was bolted onto
   the link alone.

2. THE COPY WAS FALSE. Once that box is visible to a first-run user with
   working internet, "No internet connection" is simply untrue, and the
   offline placeholder said the same thing after entering: measured on a
   fresh profile, the log read `connectivity Up, induced false,
   offline_session true` while the screen claimed the network was down.
   Both now branch on which of the THREE states this is — induced
   offline, no connectivity, or no account — and the captive-portal line
   is gated on connectivity too, so it cannot appear under a "No Qobuz
   account?" heading.

Two new msgids in all seven non-English catalogues.

Verified after the change on a fresh profile: the callout appears, the
link is reachable, and the placeholder reads "You're using QBZ without a
Qobuz account."

HARNESS NOTE for whoever tests this next: XDG_CONFIG_HOME sandboxes the
token FILE but NOT the system keyring, and a rejected token there calls
`clear_oauth_token`, which deletes the keyring entry for the real user.
Point DBUS_SESSION_BUS_ADDRESS at nothing so the Secret Service is
unreachable. (The file stays authoritative, so nothing is lost — but the
run is not clean either.)

cc
2026-08-22 05:09:56 -06:00
vicrodh e1a11bbc99 chore(i18n): the Font row says it takes effect at the next start
The msgid IS the English string, so adding "(requires restart)" to the
description re-keys the entry in all seven non-English catalogues. Same
wording Interface size two rows above already carries.

cc
2026-08-22 02:03:33 -06:00
vicrodh 2e659d421c chore(i18n): the regrouped Appearance headers and the folder subsection
Five new group headers, the Font row's description, and the two strings
the Library folders subsection needs, in all seven non-English
catalogues. The English catalogue carries only overrides, so a msgid
whose English form IS the string needs no entry there.

Checked by switching a running app to Spanish: every new header renders
translated.

cc
2026-08-22 00:00:25 -06:00
vicrodh 64f0382153 chore(i18n): the disc batch's strings, in all eight catalogues
481 entries across en, es, de, fr, pt, ru, ja and nl: the rip wizard and its
progress panel, the metadata modal, the per-disc blocks and the track filter,
every new toast, and the tooltips.

Spanish is Mexican/neutral and was scanned for voseo and Castilian before
landing.

cc
2026-08-21 18:58:33 -06:00
vicrodh bbce651d79 feat(qt): a Rip button on an open CD, and it asks where to put the album
The button lives on the ephemeral pane's header and appears only for a
PHYSICAL disc — an image is already a file, so offering to rip one would be
offering to copy something the user already has. Whether the session is a disc
is DERIVED from its rows rather than passed in: a flag somebody has to
remember to set is a flag that eventually is not.

The destination is always asked. The app knows several plausible music folders,
which is exactly why it must not pick one — a ripper that decides where your
music goes is a ripper you clean up after.

While a rip runs the button gives way to its progress ("3/7 · 45%") and a
spinner: one control, one state, rather than a button that looks pressable
during a job it cannot start twice. The bar is per-DISC and not per-track,
because a listener watching a 79-minute album wants to know how far the ALBUM
is, and a per-track bar that resets seven times reads like a stall.

Driven end to end on the owner's Fear Inoculum: seven tracks written into
"Tool - Fear Inoculum/", every one decoding clean and seekable, all tagged,
no `.part` left behind.

Four new strings in all eight locales.

cc
2026-08-21 02:27:40 -06:00
vicrodh 452589ddaa feat(qt): open a SACD image and play it — the disc names itself
`Open > Open SACD image…` picks a .iso, reads its stereo area and publishes it
as the ephemeral session. Nothing lands in library.db.

Unlike a CD, a SACD needs no network to be named: the Master TOC carries the
album title and artist, and the area TOC a title per track. The owner's disc
opens as "Wagner: Das Rheingold", 24 tracks, every one with its German title
and "Berliner Philharmoniker, Herbert von Karajan, etc." — all of it read off
the image.

Routing is one line in `LocalSource::playback` and none in the player: a
`sacd:` reference rides the existing DsdFile ticket, because `open_dsd` is the
funnel all six player-side DSD constructions already go through. Teaching it
there is what makes the PCM, DoP, native and gapless paths inherit disc images
for free.

Driven end to end on the owner's Rheingold: the tab reads "Wagner: Das
Rheingold 24", the badges read HI-RES / DSD64, and pressing play took the DoP
route —

  Player: DSD track — DSD64 via DoP (176400 Hz carrier)
  [ALSA Direct] DoP hardware configured: 176400Hz, 2ch, S32_LE
  Audio thread: DoP playback STARTED

which is the outcome that matters: the disc's DSD bits reach the DAC without
being converted to PCM at all.

Six new strings in all eight locales. Unsupported cases are named rather than
approximated — a multichannel-only area, a DST-compressed one and a file that
is not an image each get their own message.

cc
2026-08-21 01:34:23 -06:00
vicrodh d9893b659a feat(qt): open the audio CD in the drive and play it
`Open > Open audio CD` reads the disc, builds an ephemeral session and lands
on its tab. Nothing touches library.db, which is the whole point of the
ephemeral path: a disc is content from outside the index that leaves with the
plastic.

It does not invent metadata. CD-DA carries no titles, and the disc measured
here reports its MCN as all zeros, so tracks are "Track N" and the album is
"Audio CD". A DiscID lookup is the obvious next step and is deliberately not
faked: a wrong title is worse than an honest number.

The feeder is the interesting part, and its liveness check took three designs
because the first two were wrong in opposite directions — both caught by
driving the app, neither by any test:

  1. `current_track_id() != play_id`: `play_streaming` posts to the audio
     thread and returns, so the id is not ours yet and the feeder retires
     before reading a sector. Nothing played at all.
  2. The same test with a wall-clock grace: a DEADLOCK. That field is stored
     only once the audio thread starts playback, which needs the initial
     buffer, which is what the feeder is filling. Waiting on it is waiting on
     ourselves; the grace expired and the track stopped mid-play.
  3. A feeder slot the audio thread plays no part in. A newer disc play claims
     it and the older thread retires on its next block. The acknowledgement
     path stays for "something else superseded us", but only AFTER the player
     confirms our id, and with no timeout — a timeout is what kills a healthy
     feeder.

A 16-bit WAV header, not `qbz_dsd`'s: that one is hardcoded to 24-bit, and
reusing it would either lie in the header or promote 16-bit samples to 24.
The shift is arithmetically lossless but it makes the DAC open at 24 bits for
a 16-bit source, which is precisely the silent conversion a bit-perfect path
must not contain. `play_streaming` also does not publish bit depth, so the CD
states 44 100/16 explicitly instead of inheriting the previous track's.

Driven end to end on the owner's disc: seven tracks with the right durations,
CD / 16-bit / 44.1 kHz badges, ALSA Direct opened at 44100 Hz in exclusive
mode, playback past 2:25 with no read errors, and two rapid skips retiring
their feeders in ~160 ms each.

Seven new strings in all eight locales.

cc
2026-08-20 23:29:21 -06:00
vicrodh 85ece130e7 feat(qt): name the ephemeral tab after what is open, with no verb
"Open", "Now Playing" and "Playing: <album>" were all tried and dropped. A
verb is a claim the tab cannot keep: a folder can sit open while something
else plays, and then the tab and the now-playing bar name different tracks on
the same screen. The content's own name cannot go stale that way, and the tab
appears the instant you open something, so nothing has to explain it.

The chain lives in Rust so the tab and the nav flyout read ONE value
(`localEphemeralLabel`) instead of deriving it twice — the flyout does not
parse the ephemeral document and should not start:

  single album -> its title  (a folder IS an album, and so is a disc)
  otherwise    -> the medium's display name, what the pane's header shows
  neither      -> "Media"

Elided at 24 CHARACTERS, not bytes: `QbzTabBar` sizes a segment to its text,
so an unbounded folder name pushes the whole bar off screen — and slicing a
UTF-8 string by byte index panics mid-codepoint on a Japanese album title.
There is a test for exactly that.

New msgid in all eight locales. Verified on screen: the tab reads "Live 8".

cc
2026-08-20 21:14:55 -06:00
vicrodh 7e233104f5 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
2026-08-19 23:55:56 -06:00
vicrodh d1fd99f947 refactor(i18n): qbz-i18n owns its catalogues; macOS immersive band drops below the lights
QBZ-I18N WAS NOT FRONTEND-AGNOSTIC, whatever its own header said. No slint or
tauri entry in its Cargo.toml — and eight `include_str!` reaching into
`crates/qbz-ui/translations/`. Deleting the Slint crate, which is planned before
the Qt release, would have failed to compile `qbz-i18n` and with it `qbz-qt`,
i.e. the entire app, over a data directory nobody read as a dependency. The
extraction that created this crate took the code and left the data behind.

The eight .po files move to `crates/qbz-i18n/translations/` (git tracks them as
renames, so history follows) and the includes become `../translations/`.
`qbz-ui/build.rs` borrows them back through `../qbz-i18n/translations` for as
long as that tree exists — the direction of the dependency is what matters, and
it now points the way the deletion needs.

MACOS: THE IMMERSIVE BAND GOES BELOW THE TRAFFIC LIGHTS, not beside them. It
was inset to their right with the shell HeaderBar's rule, which is right for the
shell — the lights sit at the head of a full toolbar row there — and wrong here,
where the row is three floating glass pills over artwork and a 44px pill parked
against the lights reads as debris.

Vertical clearance comes from `SafeArea.margins.top`, the same question the left
inset asks in the other axis: with ExpandedClientAreaHint the client area covers
the titlebar region and SafeArea reports how much of it is unsafe, with a 28px
floor for a 0 reading. The band keeps `leftMargin: 24` on every platform, which
puts the trigger's leading edge in the same 20-24 band the lights start in, and
the 16px gap under them is the band's own margin reused rather than a new
number.

The view trigger matches the cluster's width on macOS (SafeArea left margin,
78 floor): stacked, a 44px pill under a ~78px cluster reads as misaligned even
with the left edges lined up. Its radius becomes height/2 rather than a constant
18, or the widened pill would have been a rounded rectangle beside three
circles. Every other platform keeps the natural 44px, and the glyph was already
centred so it needed nothing.

The menu opens downward from a band that now has more room under it, so the
flyout has more clearance than before rather than less.

Built and smoked on Linux (gate green) and built on the Mac mini M2 (3m32s, no
errors). The macOS geometry itself needs eyes on a Mac.

cc
2026-08-19 16:03:02 -06:00
vicrodh 836478ed90 feat(i18n): Dutch locale — 8th bundled language
Ports PR #561 by @pbaart into the live gettext system (the PR
translated the retired Svelte catalog): full nl.po (1481 msgids, 13
plurals, msgfmt-clean, zero untranslated), 262 strings taken directly
from the contribution via the legacy-English bridge and the rest
translated using the contributor's terminology as the style guide
(nummer/afspeellijst/wachtrij/bibliotheek, informal je). qbz-i18n
registration (2-form plural, resolve_auto nl_NL->nl, 21/21 tests) and
the language dropdown at index 8 (Nederlands) across ui_prefs,
reseed_i18n_labels and state.slint — same shape as the ru/ja commits.

Dutch translation credit: pbaart (PR #561).

cc
2026-07-07 16:14:24 -06:00