Only the margin was asked for. The frame-tier backdrop behind the inset
bar read as a bar glued onto a slab; it is gone, and the bar floats over
the shell's own ground like the content pane's margins do.
cc
The editor gains a Folder row — the same folders the sidebar and the
manager file playlists into, "No folder" for the root — seeded from the
playlist's current folder and applied on Save through the manager's own
move verb, so an existing playlist no longer needs the sidebar's row menu
to be filed (#785). "Delete playlist" joins the sidebar row menu (own and
local playlists; sidebar entries now carry isOwner) and the playlist card
menu (own playlists), behind a shell-level "Delete playlist?" confirmation
that runs the same delete the editor's button runs and leaves the detail
page only when it was the deleted one (#776). Eight catalogs updated.
cc
Library > Releases (and All) "Date added" was position-based: every source
got a rank of index / len and the merge interleaved sources by that
relative rank, so a purchase from yesterday could sit behind favourites
from months ago. The favourites API carries favorited_at (never modelled)
and purchases purchased_at; local favourites keep their own stamp. Rows now
carry addedAt, the merge and release-sort.js order by it newest first, and
only undated rows fall back to the rank, always after the dated ones.
The playlist view's Default and Date added never sorted: they reversed
whatever order the rows were in (after a Title sort, a reversed title
order), which is why flipping the direction took two clicks and switching
fields kept the old order (#782). Rows now carry their loaded position —
Qobuz's insertion order is its added order — Default and Date added sort
by it, and the direction toggle reverses a canonical order.
cc
The workflow_run path only ever prepared metadata, and its artifact skipped
.SRCINFO (a dotfile needs include-hidden-files), so a green run after a
release left AUR untouched — 2.1.1 sat at 2.1.0. Tag runs now publish when
the repository holds AUR_SSH_PRIVATE_KEY (a passphrase-less deploy key on
the maintainer's AUR account) and print a notice pointing at the manual path
otherwise. scripts/packaging/aur_publish.sh is that manual path in one
command: stamp from packaging/aur, hash the published assets at their final
URLs, regenerate .SRCINFO in a clean Arch container, commit per repo, and
push only with --push (the personal key's passkey is asked interactively).
cc
A LAN-only receiver — a BluOS player, another QBZ — was invisible until an
official app paired it into the session; that is the "it showed up once,
for a day" report. The Qt LAN runtime now browses _qobuz-connect._tcp
alongside its own receiver (same lifetime: Qobuz Connect enabled and
ready), probes every announcement that is not us, and the picker shows the
answers under "On this network" with a Pair action. Pairing mints the
delegated pair with /qws/delegateAuth for the renderer's app id and hands
it over with this controller's session uuid and become_active, exactly as
the official controllers do; the cloud then lists the device and its LAN
row drops out by uuid. scripts/qconnect-lan-fake-renderer.sh is a receiver
that never joins the cloud, for exercising this without a Node. Pending:
qbzd parity, the 5-minute retry backoff, become_active=false.
cc
qconnect-lan gains what QBZ calls on another receiver: a
_qobuz-connect._tcp browser (own device_uuid filtered, Electron's SDK
floor), the get-display-info / get-connect-info probe (IPv4 first, 10 s,
Connection: close) and the connect-to-qconnect handoff body — the
controller's session uuid, the delegated API token and the delegated QWS
token under its LAN name jwt_qconnect, become_active. qbz-qobuz mints the
delegated pair with POST /qws/delegateAuth (q_app_id), the step every
official controller takes before the handoff. The receiver DTOs learn to
deserialize (unknown enum values collapse to Unknown). Pure helpers tested.
cc
Every now-playing bar (New / Classic / Large / Small) is inset by the same
8 px the content pane keeps to its frame — left, right and bottom — behind
a frame-tier backdrop, so the bar's ends line up with the pane's, nothing
in it touches the window edge, and the gutter reads as one continuous
frame with the sidebar rather than a band of raw background. The Large
cover dock lifts with the bar. On macOS the gutter is 16 px: Tahoe's
Liquid Glass windows round their corners far more than before (larger
still around a toolbar) and clip whatever sits inside the curve — the
cover at the bar's left end lost its corner — and Qt has no LayoutRegion
to nest content into the curve, so the margin is the fix.
cc
The full text tabs kept an 11 px label at every width. The size now comes
from measuring the labels: 11 px stays the floor (the tabs beside the search
box at the 1140 px breakpoint), 12 or 13 px when every label plus its chrome
fits in the room left of the centred search box — 13 px being what the
sidebar rows and the flyout entries use, so with space the two hosts read
the same. Measured rather than a width breakpoint because the labels are
translated and their count changes (offline hides the Qobuz sections,
Purchases is opt-in).
cc
The separate "Purchases in title bar" placement toggle is gone: with Show
Purchases on, the entry sits wherever the other sections are — in the
sidebar while the navigation lives there, in the header beside the full
tabs or the compact buttons otherwise — so it is never the one entry left
behind. The nav_tb_purchases pref and its bridge key are retired; the
Show Purchases description says where the entry goes.
cc
The bars' audio flyout keeps its switch; Settings > Audio gains the same
key with its two companions: a Target loudness select (-11 / -14 default /
-16 / -18 / -23 LUFS, a value set elsewhere shows as a display-only
"Custom (N LUFS)" entry instead of being re-mapped) and the Prevent
clipping toggle. Eight catalogs updated.
cc
Start gain resolves from the loudness cache, then from ReplayGain tags in
the bytes; the catalog's own ReplayGain is fetched once per track alongside
the CMAF setup (and the cached replays, the disk replay and the gapless
download), and a prefetched track the catalog cannot describe is
pre-analysed in full (EBU R128 + true peak) off the audio thread, reading
the bytes back from the playback cache since the prefetch streams them
straight in. DynamicAmplify now starts AT that gain instead of ramping from
unity, the published normalization gain is the real one, and the four
seeding sites share one resolver (normalization::plan_start_gain).
cc
A track that needs no boost keeps unity (or its own attenuation) even when
its peaks already sit above the -1 dBTP headroom: that is the master's
doing, not ours. Without this, every hot modern master exactly at the
target lost ~0.5-1 dB the moment the guard saw its peak.
cc
A new audio setting, on by default: the normalization boost is capped so the
track's known peak stays 1 dB under full scale. Store column + migration,
portable in the settings bundle, shown in diagnostics, and settable from
qbzd (settings get/set audio.normalization_prevent_clipping).
cc
The cache kept a target-relative gain (a target change invalidated every
row) and was rewritten every 5 s, so a skip left a 5-second window behind.
Rows now hold integrated LUFS + true/sample peak + a ranked source that a
weaker measurement cannot overwrite; the last window of a track is stored
as the full-track figure, and nothing shorter than 30 s is stored at all.
The analyser no longer re-levels a track that started with a known gain,
moves the live gain at most once (a seek keeps it), measures true peak, and
DynamicAmplify glides over 400 ms instead of 50 while polling the shared
atomic every 1024 samples instead of every sample. loudness_scan is the
pure full-track measurement the player's pre-analysis will feed.
cc
Audio, Playback, Appearance, Navigation, Local Library, Offline,
Integrations and Updates stay in the column; Blacklist, Storage,
Import / Export, Developer (and Orbit while enabled) move under an ADVANCED
disclosure row with a chevron. The fold is not persisted: it opens itself
whenever the current section lives inside it, and reads as active while
closed over the selected page. Appearance takes lucide's
paintbrush-vertical, Integrations unplug and Storage database — baked into
every tint and registered as masters. The kiosk selector follows the same
order, flattened. "Share logs" stays pinned at the bottom.
cc
Settings > Offline loses its "Clear cache" row: it ran the same
QbzOffline.clearAll() as the trash glyph in the Offline Cache Manager's
stats bar, so the whole cache had two entry points and one misleading text
("your downloaded albums are kept" — what is kept are the purchased
downloads in the music folder). The glyph now carries a hover tooltip (the
manager mounts the shared QbzTooltip overlay; the limit and folder glyphs
get one too) and its confirmation says exactly what goes — the live track
count and size — and what stays. The Tauri-era string leaves the eight
catalogs; the new ones enter them.
cc
Settings > Storage keeps only the caches that have no section of their own:
the artwork cache, the lyrics cache and the Plex mirror. The offline
downloads group goes back to Offline and the playback cache (memory profile,
usage, storage folder) back to Playback, since both configure the feature
they sit next to rather than disk in general. Rows and bridge verbs are
unchanged; the playback cache QML test instantiates PlaybackSettings again
(16/16) and the .po location comments follow the artwork rows to Storage.
cc
Offline downloads, the artwork cache, the lyrics cache, the Plex mirror and
the playback cache (memory profile + storage folder) move to a new Storage
section, each row keeping the bridge verb it had. Offline keeps the mode
rows, Playback keeps the behaviour rows, Plex keeps Disconnect.
cc
The boot trim alone let a long session grow the folder past the budget
again (an early release reached 1 GB). A trim now also runs on the
blocking pool every 16 MB of new originals, the budget comes from
ui_prefs image_cache_max_mb (100/200/500/1000 MB, 200 default), .img files
with no row older than an hour are swept at boot, and the settings
document carries the stats, the size options and a clear action.
cc
With featured performers shown, the line is one elided StyledText whose
names are separate links: the main artist opens its page as before; a
featured name opens its page when the album credits carry its id and the
search for that name otherwise. Rows send the names with ids resolved.
cc
Mark A, mark B, loop between them until cleared; local playback only,
never persisted, cleared on track change or a seek outside the section.
A 100 ms watcher reads the millisecond position and reissues the local
seek; the seekbar shows the section and its two marks.
cc
Both were rendered from a hardcoded English month table in Rust, invisible
to i18n. Rust now hands QML the raw timestamp / civil date and QML formats
it with Qt.formatDate(..., Locale.LongFormat) like the purchase views
already do.
cc
Volume restore (desktop session and qbzd driver), a playback-thread panic
at stop, and a credentials decrypt failure (now naming the file and the
likely cause, without the inert legacy retry).
cc
get_tracks_for_eviction had no caller; the resolve-path comment claimed
check_cache_limit evicts by last_accessed_at, but it only refuses new
downloads — downloaded albums are kept by design.
cc
The public README never mentioned that the control API binds 0.0.0.0 by
default nor how to restrict it; the boot line also told users who had set
[server] token that anyone could control playback.
cc
plan() matched same-name twins but never looked for the copy name, so a
run interrupted between create_playlist and the ledger save produced a
duplicate copy on the next run.
cc
The streaming sources unwrap NonZero::new on these values on the audio
thread; the decoded path already guarded both, the codec-params path did
not.
cc
Both requests built bare reqwest clients with no timeout; a stalled
connection parked the fast path forever and the Odesli fallback, which only
runs afterwards, never got its turn. One OnceLock client with the same
10 s / 5 s budget as odesli.rs.
cc
classify_reqwest formatted the raw reqwest error, whose Display embeds the
request URL with its hmac= token; the string is logged on every retry and
in the player's CMAF-failed warning. It now uses the URL-free helper the
legacy stream path already had, moved to qbz-qobuz::net_diag so both paths
share it. The log redaction layer additionally scrubs hmac=, Signature=,
Policy= and Key-Pair-Id= values.
cc
open_log_file_at renamed the live file to .prev on every open, so a second
qbz launch (the #749 two-line qbz.log) or a qbzd in the same directory took
the running process's log away. Writers now hold a shared advisory lock on
<name>.lock; an opener that cannot take it exclusively appends instead of
rotating. The daemon gets its own qbzd.log.
cc
LoudnessCache::new failing (full disk, read-only profile, corrupt
loudness_cache.db) used to panic inside the audio thread, which is created
once in Player:🆕 playback was dead until restart while the log promised
the opposite. The cache now degrades to an in-memory session cache, then to
a disabled cache, and normalization keeps running.
cc
ImageCacheService::evict had no caller since 69ed61207 retired the Slint
app's spawn_evict, so ~/.cache/qbz/images grew without bound in 2.1.0 and
2.1.1. housekeeping() now trims it to the same 200 MB budget once per run,
in 64-row batches so the GUI thread is never parked behind the cache
mutex. evict is batched and covered by unit tests.
cc
Add shared playback memory profiles, bounded disk-backed playback and storage
selection for desktop and daemon. Preserve gapless during streaming-only playback
and provide explicit cache eviction with playback confirmation.
Stop watcher read events and unchanged scans from rebuilding the local catalog.
Restore playlist/sidebar updates, folder availability feedback and player layout.
Add appearance presets and a radio-based cache settings panel with translated
trade-offs, plus consistent clipboard menus across visible Qt text editors.
Validate with Rust regressions, QML interaction tests, isolated release startup
and idle-library checks; extend the shared local and CI gates.
cx
Support Flatpak portal updates and signed Windows MSI upgrades. Add the Linux/macOS installer for 2.1.2, release signing, translations and regression coverage.
cx
Extract the HTTP host and catalog layer into qbz-control, preserve the
existing daemon API, and keep private session credentials out of SSE.
Scope library stores and scan workers to their host. Share bounded source
search and show Local results alongside Qobuz in the Qt search page.
Add the launch-gated Qt Orbit laboratory and persistent remote-context
banner. Let qbzd run --orbit own and administer its file library through
instance-scoped API and CLI operations. Full remote UI control remains
pending.
Validate library/control/daemon suites, Qt search and QML regressions,
two isolated daemon processes, Qt release build and final cargo check.
cx
Resizable panels (#771), collapsible My QBZ sidebar, artist identity
matching (#768), album-card editions, queue ordering (#442), custom artwork
on cards, GNOME clipboard (#684) and dock icon, MPRIS after restore (#683),
and the prebuilt-tarball flake (#744). Release date moved to 2026-09-11.
cc
GNOME Shell pairs a live window with its launcher through the Wayland app_id,
and Qt sends the executable's name (`qbz`) unless the application sets its
desktop-file name. Every Linux package installs the entry as
`com.blitzfc.qbz.desktop`, so the dock painted the generic icon while the app
grid, which reads the .desktop directly, showed the right one; the D-Bus
notification `desktop-entry` hint had the same gap. StartupWMClass covers
X11 only.
Set `QGuiApplication::desktopFileName` to `com.blitzfc.qbz` right after the
application exists (Linux only). Verified the mechanism on an Ubuntu
GNOME/Wayland VM: a `qbz.desktop` symlink made the dock icon appear.
cc
Two tag_editor_qt tests compared editor_directory() — the parent() of a
file inside std::env::temp_dir() — against temp_dir() itself. On Windows
temp_dir() ends in a separator ("C:\...\Temp\") and Path::parent never
yields one, so the strings differ and the Windows Qt gate failed on every
run since 2026-08-28, the v2.1.0 tag push included. Linux never saw it.
Both tests now use a created subdirectory of temp, which normalises the
two sides on every platform.
cc
test_core_then_signal_crash_is_rejected_without_core_dump flaked on the
2026-09-11 pre-flight: its fake child (a fresh Python interpreter that
prints the startup marker and SIGSEGVs itself) has to reach the crash
inside a 250 ms observation window. On a loaded runner it did not; the
deadline expired with the child alive, the shutdown SIGTERM raced the
self-inflicted SIGSEGV, and the smoke rejected the run as "unexpected
process exit status -11" instead of the "exited before" the test asserts.
The early-exit fakes now get a 5 s window — free, since observe() returns
the moment the child exits — while the four live-process fakes keep their
explicit 250 ms so the suite still runs in ~2 s.
cc
A restored session runs `refresh_now_playing`, whose MPRIS push is the
optimistic `Playing` at position 0 — correct for the real play path, where
a stream is about to open and the poll loop's play/pause edge corrects it.
Nothing opens on restore, so `playerctl status` answered "Playing" from
boot until the first real play, and the sleep inhibitor was armed for a
paused app. Publish Paused at the resume position once the restore is done.
Verified: playerctl reports Paused on a restored, unplayed session.
cc
Copy link failed on GNOME + Flatpak: arboard's Wayland path needs the
wlr/ext-data-control protocol, which Mutter does not implement, so it fell
back to X11 — reachable natively through XWayland, unreachable inside the
sandbox ("clipboard unavailable: X11 server connection timed out"). KWin
has the protocol, which is why the failure never reproduced on Plasma.
Replace it with a C++ shim (cxx/clipboard.cpp) that queues the write onto
the GUI thread and hands it to QClipboard: the base wl_data_device protocol
every compositor implements, no extra sandbox permission, and the offer
lives as long as the application (the #514 lifetime rule, without the
long-lived arboard instance). arboard is dropped from the crate.
Verified on Plasma/Wayland and on an Ubuntu GNOME/Wayland VM with DISPLAY
unset (the sandbox condition), where the copy now lands.
cc
Both panels were fixed-width (240 / 300); playlist names elided on wide
monitors. Each panel's visible edge (the content pane's edge, 8px past the
panel — the bezel between them shares the panel colour) now carries a 5px
grab strip with a 2x28 notch, split cursor, and a one-shot accent pulse when
a drag lands on a snap point.
Rules live in `panel_resize.rs` (pure, unit-tested), the bridge applies them
live and persists on release (`ui_prefs` `sidebar_width` / `queue_panel_width`,
clamped on read):
- sidebar: 240..480 while open; below 200 it snaps to the mini rail, below 24
it closes through the same state the header button uses (with the Large-NPB
fallback); the mini rail reopens at 220 — a 20px hysteresis band so no
pointer x can flip open<->mini on every move (the flapping seen in the
2026-09-11 screencast). The stored width never drops below 240, so
reopening restores the last open width. A state snap during a drag still
animates: the bridge raises `sidebarSnapping` before the state change and
Sidebar's width Behavior honours it while the drag otherwise disables it.
- column: 300..600; below 260 it closes queue and lyrics as their X would.
With the queue in the column and the width parked at 600, the notch shows a
steady larger glow toward the content; pushing 60px further opens the
Listen List (queue-view) and the queue leaves the column, lyrics staying
if open. A lyrics-only column never pushes through.
The Large-NPB dock is untouched: it is mounted with absolute x/width in
AppShell and reads nothing from the sidebar width.
Verified: panel_resize 9/9, qmllint + the six qt-run.sh audits, release
builds v1-v4, four owner smoke rounds.
cc
Swim Deep's Artist page showed Deep Purple's founding year and members. Two
defects combined: `search_artist` emitted `artist:Swim Deep` unquoted (the
second word leaks into the global query; Deep Purple ranks first at 100,
Swim Deep second at 88), and `resolve_artist` fell back unconditionally to
the first result when no exact name scored >= 90. Name-keyed caches then
made such guesses durable and shareable across homonyms.
New `qbz-integrations::musicbrainz::identity` — pure decisions plus
orchestration over injected fetcher/store/catalog boundaries:
- query `artist:"<lucene-escaped name>"`, URL-encoded once;
- containment rule: exactly one exact-name candidate with score >= 90, else
None — the first-result fallback no longer exists;
- source direction (Qobuz artist id -> MBID): the page's own Popular-Track
ISRCs (max 3, guests excluded) vote among the exact-name candidates using
every credit of every recording that carries the ISRC; ties, conflicts and
failures fail closed;
- scene direction (MBID + catalog scope -> Qobuz id): a unique exact-name
Qobuz artist is provisional; several homonyms are split by their own
top-track ISRCs (max 2 each); no evidence omits the row instead of taking
max(albums_count);
- four `*_v2` cache tables keyed by the stable identifier + matcher version,
carrying evidence kind, expiring at read time (verified 30 d, provisional
7 d, search windows 7 d, ISRC credits 30 d); legacy name-keyed rows are
neither read nor written; scene cache key bumped to v2 with the real
account territory (no scope -> no cache), rows re-derived on replay.
Core: `catalog_scope()`, `musicbrainz_resolve_source_artist`, store/catalog
adapters; `musicbrainz_resolve_artist` and the musician resolver go through
the containment rule. Artist page passes {qobuz id, own ISRCs} to core and
keeps its generation guard; QML, playback, auth and reco crates untouched.
Tests: qbz-integrations 59 (query encoding, #768 ordering fixture, ISRC
voting incl. collaborations/dupes/conflicts, cache keys/TTL/version/legacy,
bounded fan-out with request counting), qbz-core 40, qbz-qt 782.
cc
Every album card producer copied `title` raw although the model carries
`version` ("50th Anniversary", "Deluxe"); the album page, info modal and quick
view already used `format_album_title`. The Artist page listed two identical
"All Things Must Pass" cards for two different editions.
- Route the 8 card producers through `album_qt::format_album_title`
(artist page buckets, album carousels, label grid, Home rails, Library ALL
feed, Artist Scene); search_qt drops its private duplicate of the helper.
- AlbumCard.qml: the title is always the full string, elided at the card
width; when Qt reports `truncated`, hovering shows the whole title in a
Controls ToolTip (QualityBadge pattern, no character counting, no ticker).
Verified: qmllint clean, qbz-qt suite 782/782, release build.
cc
Split the manual insertion block into two tiers so "Play later" no longer
behaves identically to "Play next":
- add a `manual_later_count` to InternalState tracking the trailing
"Play later" sub-block: [now][play-next block][play-later block][source].
- play-next inserts at the FRONT of the manual block; play-later appends at
its END (after any earlier play-later tracks), in order.
- fix the idle + shuffle case: with no current track, both actions anchor at
base 0 instead of the end of shuffle_order, so an idle-built queue plays
play-next first and play-later right after, and does not collapse the rest
of the queue on the first transport play.
- next()/remove_at()/jump decrement drain the play-next tier before the
play-later tier; set_queue/clear/shuffle-enable dissolve both.
- the public QueueState.manual_next_count exposes mnc+mlc so both tiers render
inside the single "Next in queue" visual container.
NorinB's shuffle-randomization report keeps a debug log in next() to capture
a repro if it recurs; the temporary INFO instrumentation is lowered to debug.
MixView Q-mixes reach transport parity with playlists: functional multiselect
(SelectionModel + multi-select bar), header queue menu (play next / later /
add to queue via bulkTracksAction), and create-playlist-from-selection.
Queue engine covered by 86 passing tests (play-later ordering non-shuffle,
under shuffle, and idle-shuffle cases added).
cc
The stale test still asserted the pre-#766 behavior (em-dash for an unknown
album count, "1" for a track). #766 made the tracks column empty for a track
and empty for an unknown album count (filled only once resolve_items backfills)
— the shipped, smoke-validated behavior. Update the test to match; the CI qt
gate was red on this alone.
cc
A user-set custom cover (add_custom_cover) or artist portrait (add_custom_image)
showed only on the album/artist page, never on the grid cards. The card
builders emitted the source artwork URL and skipped the override map.
Library album and artist cards now prefer the override; Home (all tabs) and
recommendations pick it up at the shared attach_cached choke point — one store
read per pass, skipped entirely when a profile has no overrides — keying albums
by id and artist rails by display name; and Search artist rows prefer the
portrait too. Albums already routed through prefer_album_cover on the album,
artist, playlist and search-album surfaces.
cc
The About modal reads CARGO_PKG_VERSION, so it showed 2.1.0. Bump the workspace
version (and the Cargo.lock members) to 2.1.1 and add the 2.1.1 metainfo release
entry.
cc
The source build failed under Nix at cxx-qt / qt-build-utils with "Could not
find qmltyperegistrar": the Qt host tools live across several outputs and the
sandbox PATH did not carry qtdeclarative's. Rather than chase that, the flake
now repacks the published bare-binary tarball (the same asset AUR-bin / Gentoo
/ Flathub consume) — matching the project's binary-repack packaging doctrine and
dropping the whole Rust/Qt toolchain from the closure.
autoPatchelfHook rewrites the binary against nixpkgs and wrapQtAppsHook wires
the Qt plugin/QML paths; qt6-base/declarative/svg/wayland mirror the qbz-bin
depends. Per-arch tarball hashes for x86_64 and aarch64, computed from the
published v2.1.0 assets. Verified: nix build succeeds and the wrapped binary
launches against nixpkgs Qt 6.10.2. Bump version + hashes post-tag.
cc
Opt-in collapsible My QBZ tree in the sidebar (Settings > Navigation): a
branding-titled parent over Mixtapes and Collections sections, each header a
link to its grid with a chevron that only collapses. Header text and icon on
the left, a Playlists-parity toolbar floating right (search, new, sort A-Z/Z-A,
refresh, collapse) with the chevron aligned to the playlist carets. The tree
feed rides QbzShell (QbzMyQbz is not reliably registered in the shell context)
and is published through publish_sidebar, so it lands reliably at startup.
Per-element visibility reuses the mixtape hidden flag (repo::set_hidden): a
row's right-click hides it from the sidebar, and the detail hero menu toggles
"Hide from sidebar" / "Show in sidebar". Collapse state for the playlist tree
and all three My QBZ sections is persisted. The collapsible tree and the flat
My QBZ entry are mutually exclusive — the classic row is suppressed whenever the
tree is on. Settings gains a Navigation section (Navigation + Search moved out
of Appearance). Detail view also gains a discreet, column-aligned sub-header
over the inline track list. Eight locales updated.
cc
Song-to-album routing, source-aware "Remove from Mixtape", per-source glyphs,
tracks count, "Add to mixtape" from the playlist header with live update, and
stop importing mixtapes/collections until their ids remap. Validated by owner
smoke.
cc
Mixtape/collection items reference playlist and album ids that change during an
account import, so copying mixtape_collections / mixtape_collection_items
verbatim left garbage rows pointing at ids that no longer exist. Drop them from
both copy paths (fast and remapped) and drop the collection view-state JSON,
with a note on how to re-enable once the importer remaps ids (import elements
first, note the new ids, then import + update the collections). Elements
(playlists, local albums) still import.
cc
- PlaylistView's header menu gains "Add to mixtape" (source-aware: local vs
qobuz), matching the cards and the sidebar which already had it.
- Adding an item to a mixtape now re-derives and republishes the OPEN detail
(reload_if_open), so a just-added playlist appears without reopening. (#766)
cc
- A song row opens the album it is on (its resolved, source-aware albumId)
instead of a blank page; the album id is plumbed from the first resolved track
through the resolve pass, and an unresolved click is ignored like the artist
link. (#766 problem 1)
- The row menu says "Remove from Mixtape" on a mixtape, "Remove from collection"
elsewhere. (#766 problem 2)
- A playlist inside a mixtape hides its quality cell (per-track quality is
varied); collections keep it.
- The source column shows one glyph per DISTINCT source across the item's
tracks, so a mixed-source playlist shows them all.
- The tracks column is empty only for a track; an album or playlist shows its
resolved track count instead of a dash.
cc
- AUR qbz and qbzd: makedepends jack2 -> jack, so pipewire-jack can satisfy the
build-time JACK dependency (both provide `jack`; only jack2 provides `jack2`).
- Flathub: bump runtime/SDK from the EOL org.kde.Platform 6.9 to 6.11 (verified
available in the KDE flatpak-kde-runtime branches). The binary is built
against Qt 6.8 and loads on 6.11 by forward ABI compatibility. (#755)
cc
Retry 503s and transient transport hangs behind a shared per-process rate
limiter, cache resolved artists, drop the redundant scene backoff, and add a
scene loading heads-up + 5-minute retry timeout. Client tuning and scene
verified by owner smoke.
cc
The ArtistScene discovery fans out many MusicBrainz requests, so a throttled MB
can keep it loading a while. After ~10s the loading screen shows a "this can
take a few minutes" note; after 5 minutes it swaps the spinner for a Retry (a
late success still swaps in the grid on its own). Both derive from the existing
pulse-driven clock, so no new Timer and the repaint law holds. Two new strings
across all eight locales.
cc
- musicbrainz_resolve_artist is now cache-first + write-after via
MusicBrainzCache::get_artist/put_artist (resolved_artists), the same shape as
the metadata path, so reopening an ArtistPage no longer re-runs a live
artist?query= search. Negatives are never cached; the existing cleanup(ttl)
governs expiry.
- Remove the scene's one-shot Retry-After backoff: the client already retries
503s, so stacking another wait could push the total past ~10s.
cc
- Route every endpoint through a new send_with_retry: it waits on the rate
limiter, GETs, logs the x-ratelimit budget, and on a 503/429 backs off the
server's Retry-After (capped 8s) and retries, up to 3 attempts. get_artist_tags
and get_recording_isrcs, which skipped status handling entirely, now go
through it too.
- Also retry transient TRANSPORT failures (a hung connection to the request
timeout, a drop or refusal). MusicBrainz under load hangs instead of returning
503, which surfaced as "error sending request for url". Capped at 2 attempts
with a 6s request timeout so a total failure is ~13s, not ~33s, while a hang
that clears still recovers on the second try.
- Share ONE process-wide direct rate limiter across every client (core, the tag
editor, remote metadata) so several clients on one public IP pace as one 1.1s
stream instead of each pacing itself.
- Log MusicBrainz's x-ratelimit-limit/remaining/reset headers (debug on 2xx,
warn on 429/503) as the only signal of whether the bucket was ours or MB's.
Tests: 503-then-200, a dropped connection, and shared-limiter serialization,
each red-to-green. Dev-only deps (tokio test features + rustls/aws-lc-rs to
install the crypto provider); no runtime dependency added.
cc
Bundles the header-select homologation, seekbar fluidity, per-platform app-menu
and keyboard shortcuts, per-tab Library filter persistence, the local track-info
routing, the playlist removal and remote-cover-art fixes, and a Ko-fi credit.
All verified by smoke test.
cc
- Remove from playlist did nothing for a Qobuz row. The membership-id split
made a row's display id (the catalog id QML sends) differ from its
playlist_track_id (the membership id), but remove_track still matched the
open document and called playlist/deleteTracks by the catalog id, so the
lookup found nothing and the endpoint got a non-membership id. Resolve the
catalog id to the row's membership id, remove exactly that row, and delete by
membership; same-catalog duplicates keep their other copy. The local and
mixed-sidecar arms were already correct. Verified by smoke test. (#764)
- Media-server rows (Jellyfin, Subsonic/Navidrome and Plex) showed no cover in
the playlist TrackRow while the queue and NowPlayingBar did. The row fed
cached_path the raw artwork token, which classify cannot resolve, whereas the
queue namespaces it through portable_artwork_ref. Build the row cover from
that same ref, and warm uncached covers on the local-playlist load (the Qobuz
loader already did) so the cover appears without playing the track.
cc
- Homologate the three Library header selects to the ambient Group style; Sort
gains an arrow-down-up glyph, a "Sort" placeholder and an explicit Default,
and every select's border clears when it returns to its default value.
- Seekbar fluidity: a drag scrubs locally and commits a single seek on release;
the time readout is a floating opaque tooltip reparented to the overlay so the
content-frame shadow no longer bleeds noise over it.
- Route the NowPlayingBar info button and title to the LOCAL track-info modal
for a local row, so a local track no longer opens an empty Qobuz modal.
- App menu shows each entry's shortcut (muted, smaller, right-aligned, resolved
per platform — Cmd on macOS, Ctrl elsewhere) and gains a Quit entry; the
keyboard-shortcuts modal adds Close Window and Quit under Navigation, moves
Immersive below Interface, and tightens row heights. i18n for Quit/Close
Window across all eight locales.
- Persist each Library tab's filter toggles to disk, independently per tab.
cc
cpal 0.17's ALSA worker reports `alsa::poll() returned POLLERR` and polls
again immediately, without snd_pcm_recover: a PCM left in XRUN/SUSPENDED/
DISCONNECTED (CPU starvation at a track change on a Pi, a suspend, a pulled
cable) becomes a tight loop that never plays again until the stream is
dropped. rodio's default callback eprintln!s every one of them, which is the
POLLERR flood in the issue. The System default and PipeWire routes installed
no error callback at all, and nothing rebuilt a wedged stream.
- New qbz-audio::stream_health: classify (Wedged vs Transient), a flood
limiter (one log line per 5s window with a suppressed count), a wedge
latch, and one shared error_callback installed on every CPAL route
(System default x4, PipeWire, ALSA) and the legacy player path.
- The audio thread checks the wedge latch at the TOP of its loop, every
iteration, not in the recv_timeout Timeout arm: under QConnect the
renderer-state command flow keeps recv returning Ok(...) and the Timeout
arm barely runs, which left a wedged worker spinning a core for ~22s.
On a wedge it drops the stream (joining cpal's worker), and when playing
rebuilds and resumes at the current position; a second wedge within a 10s
backoff releases the device and stops instead of looping.
Bit-perfect paths (ALSA Direct, DSD, WASAPI) are untouched; they never use
cpal's worker. The low-memory profile from 2.1.0 already addressed the exit
137 OOM; this is the separate flood/wedge half.
Verified on a Pi 4B + HiFiBerry DAC+ over the System default route with a
192 kHz 835 MB track and SIGSTOP-induced wedges: rebuild+resume in ~40 ms,
auto-recovery after the backoff, release+stop on a repeat wedge, and 7
POLLERR log lines instead of a flood. qbz-audio 125 and qbz-player 139 tests
pass; new stream_health and wedge_recovery tests included.
cc
The TUI cascade cleared `exclusive_mode` on any non-ALSA backend. On macOS
System default honours it through CoreAudio Hog Mode, so mirror the Qt
shell's cascade (#748): the flag survives onto ALSA and, on macOS, System
default; every other backend still resets it. The platform is a parameter so
the macOS arm runs on the Linux gate.
cc
System default on macOS is the CoreAudio exclusive backend: with
`exclusive_mode` set, qbz-audio takes Hog Mode, pins the device and switches
its nominal rate to the track (PR #391, shipped in 1.2.11 and never removed).
The Tauri settings gated the toggle on `ALSA || (macos && SystemDefault)`;
the Slint port copied only the ALSA half, and fc0d35e29 then hid the row on
macOS on the premise that no backend there could honour it. So 2.0.x showed
a grey row and 2.1.0 none, while the DB flag still engaged Hog Mode.
- SettingsDoc publishes `backendIsCoreAudio` (macOS && System default); the
row is visible and enabled on it. Linux and Windows are unchanged.
- The backend-switch cascade goes through one predicate
(`output_labels::backend_honours_exclusive`), so re-selecting Auto or
System default on macOS no longer clears the flag.
- The output LED reads EXCL on macOS System default + exclusive_mode instead
of DEFAULT while the device is hogged.
- Node test `scripts/test_qt_exclusive_gate.mjs` evaluates the row's real
gate expressions across platforms; added to qt-runtime-gate.sh. Rust tests
cover the predicate, the LED and the serde name QML reads.
Verified by smoke on the Mac mini with a USB DAC: the row appears, Hog Mode
is acquired, and browser audio moves to the built-in speakers while QBZ
plays. qbz-qt suite 782/782 on Linux; 13/13 with cfg(macos) on the Mac.
cc
Restore tray-aware close and quit confirmation, circular label covers, and missing label artist portraits. Publish artwork progressively, preload two grid rows, and skip redundant motion reports while preserving data refreshes.
Include regression tests and isolated artwork benchmarks. Verified Linux build, QML checks, and scrolling smoke tests.
cx