Decided 2026-08-29: the arm64 Qt build only works on ubuntu-24.04-arm
(aqt's arm64 host tools segfault on 22.04-arm), so the arm64 desktop
app requires glibc 2.39; qbzd keeps building on 22.04-arm at 2.35 so a
trimmed Pi OS bookworm image stays a valid target. The floor gate asserts
per arch instead of warning; README states both floors.
cc
Qt 6.9's macdeployqt leaves @rpath/Qt*.framework references resolved
through LC_RPATH @executable_path/../Frameworks; the old assert only
accepted a literal @executable_path and failed a correct bundle. Reject
absolute paths, require the rpath, and let the offscreen smoke prove it.
cc
Both 6.8.3 and 6.9.3 arm64 qmake die with SIGSEGV on ubuntu-22.04-arm
with no missing libraries. Try the 24.04-arm image; the glibc floor gate
warns instead of failing for aarch64 until the floor is decided.
cc
The QMAKE handoff exposed the real failure: aqt's linux_gcc_arm64 6.8.3
qmake dies with SIGSEGV on ubuntu-22.04-arm, which cxx-qt-build reported
as QtMissing. The arm line pins 6.9.3 (QT_VERSION_LINUX_ARM64 overrides);
x86_64 keeps the 6.8 LTS floor. file/ldd diagnostics run before the
qmake query so the next such failure names itself.
cc
aarch64 runner: cxx-qt-build reported QtMissing with a correct
QT_ROOT_DIR — set QMAKE/CMAKE_PREFIX_PATH explicitly (as macOS/Windows
already do) and print the qmake version before the build.
macOS: StreamType::output_sample_rate (macOS-only) matched Rodio only;
Direct is intentionally not cfg-gated, so the macOS build failed with
E0004. A direct sink answers with its opened rate.
cc
Under gapless the engine never reports a not-playing tick between two
tracks, so the poll loop's track_ended edge does not fire and the next
track's edge found the previous row still open and closed it as skip
(smoke: 229 s of a 230 s track recorded as skip). The Qt edge now uses
the tracker's inference (last observed position within 2 s of the
duration = natural), the same rule qbzd already applies.
cc
Linux: scripts/packaging/desktop_entry_check.sh asserts the MPRIS
DesktopEntry constant against the recipes (nfpm basename + Icon=,
StartupWMClass, reverse-DNS basenames) and against the built deb/rpm/
AppDir. The AppDir's desktop file now carries the reverse-DNS basename
so an integrated AppImage resolves the media-widget icon too. This
invariant regressed more than once; it is a gate now, not a rename.
macOS: compile packaging/icons/QBZ.icon (Icon Composer, issue #712) with
Xcode 26's actool into Assets.car + CFBundleIconName=QBZ; icon.icns stays
as CFBundleIconFile for older macOS. The Intel leg moves to macos-26-intel
so both legs carry actool.
cc
An ISRC or fingerprint column plus a partial index, with
set_content_key / get_by_content_key helpers, so a local or Plex copy of
the same recording can reuse an analysis. The analyser and every audio
path are untouched; set/get keep their track_id contract.
cc
Section listings ask for includeGuids=1 and the track parser reads the
<Guid id="mbid://…"/> child (only the Plex Music agent writes it; other
schemes are ignored). Stored in a new plex_cache_tracks.recording_mbid
column (additive; COALESCE on upsert) and read back by the PlexTrack
mappers. PlexCachedTrack and the qbz-source Plex constructor do not
carry it yet — the catalog projection does.
cc
Jellyfin requests Fields=ProviderIds on both listing passes and maps
ProviderIds.MusicBrainzTrack (the recording id) into JellyfinTrack;
Subsonic reads OpenSubsonic musicBrainzId and the first isrc. Both land
in two additive remote_cache_tracks columns (isrc, recording_mbid), kept
across upserts with COALESCE so a known id is never overwritten by a
server that stops sending it, and travel to the queue through
qbz-source's remote constructor.
cc
The scanner now keeps the identity tags Picard writes (ISRC/TSRC and the
MusicBrainz recording, track, release, release-group and artist ids —
the same lofty ItemKeys the tag writer already knows) in six additive
local_tracks columns, migrated idempotently (also after the sample_rate
table rebuild) and indexed on isrc and the recording id. Rows fill on
the next scan. The catalog projection (legacy.rs) stops hardcoding
isrc / musicbrainz_recording_id to None for local, remote and Plex rows
(the upsert already bound both), treating an empty string as no id.
qbz-source stamps both keys on local queue tracks.
cc
cargo-test.sh and test-crates.yml assert qbz-app::listen_log lists at
least 25 tests and passes, so a refactor that drops the module's tests
fails loudly instead of shrinking coverage. Bounded: the binary is
already built by the workspace run.
cc
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
A CoreEvent-bus sibling of scrobble_engine WITHOUT its enabled gate: the
log is local and on by default, so a headless streamer finally records
history. TrackStarted opens a row and closes the previous one with an
inferred reason (natural within 2 s of the end, skip otherwise — nobody
emits TrackEnded); PositionUpdated feeds the accumulator; Stopped closes
with the same inference. origin_id = qbzd:<hostname>; the shutdown path
aborts the task and closes the open row.
cc
listen_log_qt binds the per-user ListenLogger at login/restore/offline
entry and drops it at logout. Hooks: the de-duped track edge opens a row
(closing the previous as skip), every 1 Hz tick feeds the accumulator,
the natural end-of-track predicate closes as natural, the stream-failure
toast closes as error, three consecutive empty ticks are the stop, and
main.rs closes the row in flight as shutdown after the event loop (and
on the Windows session-commit path). Cast and peer QConnect stay out.
Ephemeral ids never reach the log (same guard as recently_qt).
cc
QueueTrack gains two additive, serde-default identity keys (the ISRC and
the MusicBrainz recording id) that the listen log stamps; every
constructor in the workspace sets them (Qobuz-backed ones from
Track.isrc, local ones from the scanned tags, the rest None). They are
JOIN keys, not the row identity: numeric ids collide across sources, so
(source, native item id) stays the identity.
Track.audio_info (replaygain_track_gain / peak on DiscoverAudioInfo) is
modelled from the captured search envelopes; no reader yet and no
popularity field (0 in every sample).
cc
New qbz-app::listen_log (contract §12.1): listen_events with PK = event
(never the track), snapshot text per row, source + native item id,
context, stream facts; listen_meta with schema_version, origin_id,
paused, retention_days (unlimited). The tracker is a pure state machine:
played_ms accumulates only while playing from monotonic deltas <= 5 s,
so pauses, seeks and stalls add nothing; flush every 10 s of play; rows
close as natural/skip/stop/error/shutdown and orphans close as shutdown
on the next open. No threshold at write time: is_play / is_skip are
pure reader rules with boundary tests. ListenLogger is the async facade
(spawn_blocking everywhere); paused writes nothing, clear is
DELETE + VACUUM + checkpoint. 29 tests.
cc
- INCOMPATIBLE_GENRES / INCOMPATIBLE_TITLE_KEYWORDS removed: a global
blocklist papered over an identity bug (name search returning a
homonym) and punished every user's taste for it.
- artist_guardrail: ArtistLookup seam (mockable), id before text
(qobuz:{id} nodes resolve by id; MBIDs hit the mb_qobuz_artist_match
cache before search_artists), validation relative to the playlist
seeds (shared level-1 genre root or a shared MusicBrainz tag), homonym
tie-break by seed neighbourhood / label, reject when no signal exists.
- suggestions: stable sort by similarity_score desc, dedup keeping the
higher score, THEN truncate (the shuffle threw the score away and the
dedup kept the first hit while claiming the best).
- Eight network-free tests cover the rules; rand is now unused here.
cx
The boot comment claimed suggestions work un-cached without the store;
suggestions.rs and builder.rs both answer "No active session" when it
is None. Warn on failure and say what the symptom will be.
cc
Removing a library folder pruned Recently Played but not
album_play_history.db, so Most Played kept cards onto deleted tracks.
radio_history had no delete path at all; sessions idle for more than
30 days (by last played_at, so a running radio is never pulled) are
dropped on open.
cc
playlist_stats.play_count had no writer anywhere in the workspace
(increment_playlist_play_count: zero callers), so the badge and the
Play Count sort were 0 for everyone. Derive the counts from the store
the track-start edge actually writes; the library table is untouched.
cc
OfflineCacheDb::touch had no caller, so LRU eviction ordered by the
download date and evicted the most-played tracks first. Touch in
resolve_offline_bytes, the one resolver every offline play goes through.
cc
The delayed scrobble used unix_now() when it fired (up to 240 s late on
a long track) and slept on wall-clock, so a track paused two minutes in
still scrobbled. Capture started_at on the track edge and wait on the
player's own position while it is playing: pause holds the scrobble, a
stop drops it, a newer track edge still self-cancels via the generation.
Same rule qbzd's scrobble_engine already applies.
cc
recommendations_qt and external_reco_qt built private MusicBrainzClient
instances, which start enabled and ignore the musicbrainz_enabled
opt-out that integrations_qt applies to the core's client. Expose the
core's Arc<MusicBrainzClient> and borrow it instead, so a disabled
MusicBrainz sends zero requests from Discover > Recommendations. The tag
editor's three private clients stay: those are explicit manual lookups
the user triggers by hand.
cc
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