Commit Graph
215 Commits
Author SHA1 Message Date
vicrodh f539149a30 fix(playback): retain acquisition quality across bounded audio storage
Pair cached audio with the requested and resolved quality, reject incompatible or unverified entries, and enforce disk budgets at startup and admission. Reuse the same policy for local, gapless and external playback while assembling CMAF segments in order with bounded concurrency.

cx
2026-09-14 13:44:13 -06:00
vicrodh 32fd77f75e feat(qt): Wallpaper follows the window on Plasma Wayland; its blurred twin drifts
"Wallpaper" now behaves like a window that sees straight through to the
desktop: it always paints the part of the wallpaper that lies under the
window, wherever the window is moved, with a stronger blur so nothing in
the picture competes with the text. X11, macOS and Windows already told
Qt where the window is. Wayland does not, so on KDE Plasma a tracker
reads the window's geometry from org_kde_plasma_window_management on its
own client connection and thread (wayland-client + the plasma protocol
crate, pure Rust backend). It follows this process's windows only,
drops every other window's object after its initial state, and
republishes only when the geometry list changes; the field picks the
window by size and ignores updates while another background is showing.
Where the protocol is not offered the crop stays centred and one line is
logged.

KWin hands that protocol only to a binary whose desktop entry names it by
an absolute Exec and lists it in X-KDE-Wayland-Interfaces, so the Linux
desktop entries now carry both (the deb/rpm /usr/bin/qbz symlink resolves
to the same binary). The AppImage step restores the bare Exec name that
linuxdeploy resolves inside the AppDir, and the Nix package rewrites Exec
to its store path.

"Wallpaper, blurred" is Blurred art with the wallpaper in the cover's
place, so it now drifts while the transport plays and holds still when it
does not.

cc
2026-09-14 05:22:55 -06:00
vicrodh daf449baec fix(music-link): give the Tidal/proxy fast path a timeout and a shared client
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
2026-09-13 03:36:50 -06:00
vicrodh e291eb3af4 fix: stabilize playback memory, local library sync and Qt settings
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
2026-09-13 02:57:21 -06:00
vicrodh 7a22fd6867 feat(updater): add native update channels and one-line installation
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
2026-09-12 16:25:08 -06:00
vicrodh 64ec3e2bc8 fix(updater): restore Qt release checks and signed installation
cx
2026-09-12 15:20:23 -06:00
vicrodh 74e1610433 feat(orbit): add shared library foundations and gated host tools
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
2026-09-12 13:02:29 -06:00
vicrodh 6b81cfa84d fix(share): copy links through QClipboard, not arboard (#684)
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
2026-09-11 13:53:27 -06:00
vicrodh c82e624188 release: bump workspace version to 2.1.1
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
2026-09-10 20:33:15 -06:00
vicrodh 857cb16e87 fix(mb): retry MusicBrainz 503s and transport failures behind a shared limiter
- 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
2026-09-10 13:23:53 -06:00
vicrodh 3aed91fa0a fix(cast): localize UPnP URL compatibility and harden crash diagnostics
Keep http 1.5 and tolerate empty or relative service URLs in a local rupnp patch. Cover standard DLNA SOAP requests and malformed descriptions.

Avoid logger locks in fatal handlers, flush emitted log batches, and preserve CI symbols after smoke failures.

cx
2026-09-08 12:08:53 -06:00
vicrodh a7629dc962 feat(log): report fatal signals instead of dying as a bare status
The packaged AppDir has an intermittent SIGSEGV during startup, in the
window between QGuiApplication construction and the QML engine coming up,
with the tokio workers, the audio thread, the loudness analyzer, the
visualizer producer and connectivity all running beside it. It hit twice
on 2026-09-08 on unrelated commits: the v2.1.0 tag's own release-flatpak
attempt 1, which then passed on a re-run of the identical commit, and the
#745 bugfix branch. Both left the same evidence - "status -11" and a log
that stops mid-startup - which is nothing to act on, and a user who hits
this has no re-run to reach for.

The process now reports its own death. A handler for SIGSEGV, SIGBUS,
SIGILL, SIGFPE and SIGABRT writes the signal, the faulting address, the
thread and a backtrace to stderr and flushes the file sink, so the next
occurrence names a frame instead of costing another round trip. It runs
on a dedicated signal stack so a stack-overflow fault can still report,
latches against re-entry, and arms alarm() BEFORE capturing anything:
taking a backtrace allocates and takes loader locks, so a fault inside
malloc could otherwise deadlock the handler and turn a crash into a hang.

The signal is re-raised with the default disposition restored, so the
wait status stays the signal and CI, the smoke harness and any core dump
still see a crash. The integration test asserts exactly that pair on a
child segfaulted on purpose, and it is a named gate in test-crates and
its local mirror.

qbzd installs it too: a headless daemon that dies of a signal leaves even
less behind than the GUI.

cc
2026-09-08 09:51:12 -06:00
vicrodh a995025f4e fix(cast): pin http to 1.4.0 so DLNA renderers stop vanishing
rupnp parses a device description's SCPDURL / controlURL / eventSubURL as
http::uri::PathAndQuery. http 1.5.0 rewrote scan_path_and_query and now
rejects two shapes 1.4.0 accepted: an empty element (ErrorKind::Empty,
printed as "empty string") and any relative URL without a leading slash
(PathDoesNotStartWithSlash). Both are common in old UPnP firmware, so the
transitive bump made rupnp drop those renderers during discovery.

Measured in the lockfiles: v2.0.2 resolved http 1.4.0, v2.1.0 resolved
1.5.0, and nothing in qbz-cast changed between them. A KEF LSX gen 1 that
worked in 2.0.2 reported "Visible Renderers: 0" in 2.1.0 with
"Invalid response: empty string", which is exactly
InvalidResponse(InvalidUri(Empty)) reproduced by the new test.

qbz-cast takes http as a direct dependency at "=1.4.0" so cargo unifies
the workspace on it, and device.rs now uses that crate instead of the
rupnp re-export. The new integration test serves two renderer
descriptions over loopback, one with empty and one with relative service
sub-URLs, and requires both to parse: it is red on 1.5.0 and green on
1.4.0, so it fails loudly if the pin is ever lifted. test-crates and its
local mirror gain the named gate.

Closes-when-confirmed: #745

cc
2026-09-08 08:37:20 -06:00
vicrodh fd421044bc fix(renderer): rescue failed Linux Auto presentation per launch
Probe Qt's default backend in a disposable graphics-only child before runtime and the main window. After proven failure, allow one software presentation attempt without persisting a renderer preference. Preserve explicit backend policy and saved-GPU validation.

Bound and reap children, keep deep links in the parent, and avoid blocked diagnostic pipes. Cover healthy/failing decisions, overrides, missing executables, paths with spaces and watchdog cleanup in the passing Qt suite.

cx
2026-09-06 05:08:17 -06:00
vicrodh 7ea01a7e67 fix(startup): bound and cancel the Linux instance probe
Use one two-second deadline across native Unix connect, D-Bus authentication, name ownership and URL/window forwarding. Own and shut down the transport on cancellation, preserve unconfirmed links, and keep the primary server alive.

Cover stalled and delayed phases, filesystem/abstract buses, ownership release and early UI handoff with isolated fixtures. All 733 Qt tests pass; async-io features remain unchanged.

cx
2026-09-06 05:05:27 -06:00
vicrodh 4bb4982312 feat(sacd): support raw and DST-compressed ISO playback
Parse resilient SACD images, decode DST, scan ISO releases into the local library, and keep explicit opens ephemeral.

Ship decoder notices across distribution formats and align DSD album filtering and version selection.

cx
2026-09-05 10:00:24 -06:00
vicrodh 024cc121a9 feat(library): adopt SACD images found by the folder scan
The scan never treated an .iso as audio; images only entered the library
through Open > disc image. Each root's scan now walks its .iso files,
asks qbz_disc::sacd::is_sacd_image for the SACDMTOC signature (PVD plus
Master TOC sector, four 2 KB reads at most, no directory parsing) so a DVD
or data ISO leaves silently, skips images already imported with the same
size and mtime without any I/O, and adopts the rest through the
generation-based import. The row builder moved from sacd_qt into
qbz_library::sacd_scan and the manual open is now its client, with the
translated fallback labels. Parser and import rejections are scan errors.

cc
2026-09-02 20:34:59 -06:00
vicrodh 800255ac58 feat(account-migration): snapshot, plan, apply and local-profile copy
A frontend-agnostic crate for moving a Qobuz account's favorites and
playlists into another account, additively (contract
qbz-nix-docs/specs/2026-09-02-account-migration-contract.md §9):

- snapshot: capture the signed-in account (five favorite kinds with
  favorited_at, owned playlists with track ids in order, followed
  playlists) into users/<uid>/account_migration/snapshot-*.json; the same
  capture is the target's live state at migration time.
- plan: the pure delta against the target and the ledger. Same-name
  playlists holding nothing foreign are the same playlist and get the
  missing tracks appended; anything foreign means a different playlist and
  a " (migrated)" copy. Favorites are written oldest first so the target's
  own stamps keep the chronology.
- apply: one favorite per call with pacing and bounded retries, 50-track
  chunks, subscriptions re-followed; every success lands in the ledger
  before the next write, so a run resumes and a second run adds nothing.
- local: copy the old profile directory row by row (library folders and
  scanned tracks on the fast path, playlist folders, settings keyed by
  qobuz_playlist_id remapped through the ledger, pinned items, orders,
  preferences, blacklist; media servers, scrobblers and listening history
  behind options). Never the favorites cache, the subscription clock, the
  session or the purchases.

The writes are the ones the app already issues when a user hearts or
creates a playlist; nothing touches subscriptions, purchases or account
data.

cc
2026-09-02 08:24:01 -06:00
vicrodh db1ac051f2 fix(local-catalog): prune obsolete generations
cx
2026-09-01 20:12:36 -06:00
vicrodh 4f065eb45f chore(release): bump workspace version to 2.1.0
Workspace version + lock, and the metainfo gains its 2.1.0 release entry
(the release workflow re-stamps version and date at tag time). flake.nix
and the Flathub manifest are post-tag and stay untouched; the snap-qt
recipe stamps its version from the tag.

cc
2026-08-31 11:39:20 -06:00
mazipani 658524c404 fix(cast): accept X.509 v1 device certificates in TLS handshake
Connecting to Cast devices failed with:
Connection error: invalid peer certificate: Other(OtherError(UnsupportedCertVersion))

Many Cast devices (notably AV receivers with Chromecast built-in) serve a
self-signed X.509 version 1 certificate on port 8009. rust_cast 0.21's
NoCertificateVerification verifier accepts any certificate in
verify_server_cert, but its signature checks delegate to rustls helpers
that parse the certificate with webpki, which only supports version 3
certificates. The handshake therefore aborts for v1 device certificates;
this affects every rustls-based rust_cast release (0.19+) and only devices
happening to serve v3 certificates can connect.

Vendors rust_cast 0.21.0 under crates/vendor with a minimal patch: the
verifier's verify_tls12_signature/verify_tls13_signature now assert
validity, matching the verifier's no-validation contract and the rustls
danger verifier examples. Wired up via [patch.crates-io]; see
crates/vendor/rust_cast/QBZ_PATCH.md for the full analysis and how to drop
the patch once upstream releases the fix.

Verified against a real device serving a v1 certificate (TLS 1.2,
ECDHE-RSA-CHACHA20-POLY1305): handshake, receiver channel and heartbeat
now establish; unpatched fails at the first channel write. cargo
check/test -p qbz-cast pass (13 tests).

Refs: rustls/rustls#1298, rustls/webpki#29, #439
2026-08-31 14:50:01 +02:00
vicrodh bb1b1a5648 merge: library favorites freshness and reco cleanup
cc
2026-08-31 03:35:43 -06:00
vicrodh a9046df218 fix(qconnect-lan): harden discovery compatibility
Advertise every eligible IPv4 interface and tolerate future handoff fields while retaining canonical-field validation.

cx
2026-08-31 03:26:24 -06:00
vicrodh 2e55f01e26 refactor(qconnect): share delegation lifecycle
Move authority fences, preflight, rejoin watchdogs, LAN projection, and physical runtime teardown into qconnect-app for both Qt and qbzd.

cx
2026-08-31 03:26:01 -06:00
vicrodh d3cf7ae53d chore(qconnect): wire LAN runtime dependencies
cx
2026-08-30 22:22:31 -06:00
vicrodh 0740a7368f chore(reco): drop the unused rand dependency
Declared since the identity-guardrail round, never used; verified by
grep and a clean build without it.

cc
2026-08-30 22:17:41 -06:00
vicrodh 74a41150e9 feat(qconnect): add transactional LAN delegation
cx
2026-08-30 15:33:23 -06:00
vicrodh f30abd12ac feat(qconnect): add official LAN wire service
cx
2026-08-30 13:50:32 -06:00
vicrodh 0e8614fe37 feat(audio): add guarded ALSA hardware volume control
Probe exact mixer element identities, retain stable device routes, and fail closed for ambiguous or switch-only controls. Synchronize player volume through ALSA events without changing direct PCM configuration.

cx
2026-08-30 09:14:16 -06:00
vicrodh 8964544891 chore(deps): reqwest 0.13 (rustls-no-provider; query/form are opt-in features now)
cc
2026-08-29 09:00:56 -06:00
vicrodh 3785a7eb40 chore(deps): rfd 0.17 (xdg-portal; the async-std feature is gone)
cc
2026-08-29 08:53:42 -06:00
vicrodh 6d8c951a38 chore(deps): rusqlite 0.40 (u64 casts; a latent two-statement execute fixed)
rusqlite 0.40 removed ToSql/FromSql for u64 (SQLite integers are i64):
every u64 column now reads/writes through an explicit i64 cast. It also
rejects a multi-statement execute() instead of silently running only the
first statement — which is what catalog.rs's editions/logical_albums
prune had been doing all along; it is an execute_batch now, so the
logical_albums prune finally runs.

cc
2026-08-29 08:36:21 -06:00
vicrodh 51e496e47f chore(deps): tokio-tungstenite 0.30
cc
2026-08-29 08:26:25 -06:00
vicrodh 0cded1e395 chore(deps): rand 0.10 everywhere (qbz-player, qbz-secrets were on 0.9)
RngCore/fill_bytes -> RngExt/fill, the trait names rand 0.10 exports.

cc
2026-08-29 08:24:28 -06:00
vicrodh c4d3fc7e3b chore(deps): cargo update — 194 semver-compatible bumps
cc
2026-08-29 08:23:36 -06:00
vicrodh 912638c387 merge: integrate Windows port from PR #725
cx
2026-08-28 04:18:06 -06:00
vicrodh 6e62724b4b fix(qt): preflight preferred GPU presentation
cx
2026-08-27 17:54:27 -06:00
vicrodh afa32d9bba feat(windows): single instance and protocol deep links
The Windows sibling of single_instance_qt.rs. Same three entry points and the
same present-or-defer behaviour, so main.rs reads identically on both.

A named mutex arbitrates and a named pipe carries the message, because those
are different problems. CreateMutexW answers "did it already exist" atomically,
with no window for a second launch to slip through. A pipe cannot be asked that
without FILE_FLAG_FIRST_PIPE_INSTANCE, and QLocalServer never passes it
(PIPE_UNLIMITED_INSTANCES), so two processes could both believe they were the
server.

The plan asked for QLocalServer here, which meant a new Q_OBJECT file in the
moc list and .qt_module("Network"). A raw named pipe is the same primitive
underneath with none of that build surface, for a protocol two verbs long.

The pipe name is QUALIFIED BY SESSION ID, and that is not cosmetic: the mutex
is `Local\`, which is per-session, while pipe names are machine-global. With
one fixed name and two users signed in, each takes its own mutex but only the
first creates the pipe -- the second primary then serves nothing and ITS later
launches would forward to the OTHER USER'S process. PIPE_REJECT_REMOTE_CLIENTS
because the namespace is reachable over SMB and nothing about a second launch
is remote.

What arrives on the pipe is VALIDATED before it is acted on. It is not argv:
any process running as this user can write a line, and the payload otherwise
went straight to link resolution. The read is bounded at 4 KB for the same
reason.

`qbz://` is rewritten to `qobuzapp://` rather than taught downstream. The
resolver knows qobuzapp and the Qobuz web shapes and nothing else -- a live
test of the forwarding path came back "unsupported or invalid music link" for
a qbz:// URL, and a test in this file already pinned that is_qobuz_link
excludes it. The two carry the same album/track/artist shape, so a prefix swap
is the whole translation, and the MSI's registration finally means something.

A parse alone is not a gate: Url::parse("qbz://") SUCCEEDS, a valid scheme with
an empty everything, so the check requires a non-empty authority -- which is
where every accepted shape carries its target.

The URL validation in select_link is Windows-gated. ShellExecute can split a
link across argv there; Linux and macOS have accepted whatever matched the
prefix since this shipped, and tightening that from a Windows port would change
behaviour on platforms that never asked.

present_or_defer re-reads UI_READY after publishing PENDING_PRESENT. bind_ui
can flip the flag and run its own swap in between, leaving the pending bit set
with nobody to consume it and a second launch looking ignored.

Verified on the box: a second launch carrying a deep link exits 0 in about four
milliseconds, the primary logs the handoff and resolves the rewritten URL.
571 tests pass.

cc
2026-08-27 17:42:53 -06:00
vicrodh 114824b7bc feat(media-controls): SMTC with a real HWND, and a native cover path
Windows media keys never worked because SMTC never started. souvlaki's Windows
backend does `config.hwnd.expect(..)`, which PANICS rather than degrading, and
`MediaControls::new` runs inside a tokio task, so a missing handle aborted the
task instead of returning the Err the caller was written for. Phase A bowed out
before reaching it and logged "SMTC disabled: no window handle wired yet".

`spawn` now takes a `NativeWindow`, and `cxx/win_shell.cpp` supplies the
top-level HWND through `QGuiApplication::topLevelWindows()`. The guard stays:
a missing integration is a missing feature, a panic in a tokio task is a dead
session.

Windows reads the handle from a bounded retry on the GUI thread. Both things it
waits for -- the tray bridge's Qt thread and a shown window -- are "not yet",
never "not ever": `tray_bridge::ui` is a no-op until `QbzTray::boot` registers
the thread, and `QbzSession.boot()` runs first (Main.qml:254 vs :380).

NON-WINDOWS IS UNCHANGED, deliberately. An earlier draft routed every platform
through that hop and it was a Linux/macOS regression three ways: initialization
could be dropped outright before boot, MPRIS would start only when the Qt queue
drained (losing the first metadata push), and `main_window_hwnd` calls
`QWindow::winId()`, which CREATES the platform window as a side effect on
platforms with no reason to.

Cover art needed its own fix. souvlaki 0.8.3 branches on
`url.starts_with("file://")`, then trims exactly that prefix and hands the rest
to `GetFileFromPathAsync`. So `file:///C:/x.jpg` arrives as `/C:/x.jpg` -- a
slash before a drive letter, which never opens -- while a bare `C:\x.jpg` no
longer matches the prefix and takes the remote-URI branch instead. The value
that works is the hybrid `file://C:\x.jpg`. UNC is handled too: `file://nas/m/a`
is a real Windows path and was being passed through as the relative `nas/m/a`.

The percent-decode is full and applied once: `art_url` arrives from
`url::Url::from_file_path` (space as %20) and from `fs_url::file_url` (only
% # ? escaped), and one full decode reads both -- `%2520` gives back a literal
`%20` rather than a space. `fs_url::local_path` is deliberately not used here;
its three-escape decode would leave the other producer's %20 intact.

Eight tests cover the matrix. Reviewed adversarially before commit, which is
where the inverted cover conversion and the non-Windows regression were caught;
the souvlaki branch was then read at the source to confirm.

cc
2026-08-27 13:37:24 -06:00
vicrodh 1503b8236f feat(core): read the Windows memory figures from GlobalMemoryStatusEx
detect_profile fell back to MemoryProfile::from_total_kb(u64::MAX) -- the top
tier -- on any machine without /proc/meminfo, and read_memory_pressure
returned None forever. The prefetch and buffer tiers were therefore pinned at
the top on Windows and the memory watchdog was blind: it continues on None, so
evict_l1_audio_cache and the prefetch halt could never fire at any pressure.

Both halves had to land together. With the old u64::MAX total, a working
pressure probe would have computed roughly 0 per cent available and reported
critical permanently.

ullAvailPhys is the analogue of MemAvailable: what an allocation can take
without going to disk. The Normal-fallback contract is preserved, so a probe
that fails still never throttles the machine. windows-sys 0.61 is already in
the workspace lock via qbz-local-catalog and qbzd, so nothing new is pulled.

Measured on a 32 GB box: 57.9 per cent available, class Normal, which is the
same class the old fallback produced -- the change is the true total and a
pressure figure that exists at all.

cc
2026-08-27 12:44:20 -06:00
vicrodh be3f423bc4 feat(audio): WASAPI exclusive-mode direct stream (Windows)
The render thread and the handle. COM objects are !Send, so one thread owns
every one of them for the life of the stream and this struct is a bounded
channel plus atomics - the same back-pressure snd_pcm_writei gives the ALSA
path for free.

Opens by ENDPOINT ID, never by friendly name: Windows gives several
endpoints the same name, which is what hid this DAC from the picker until
249adc77b. Walks the format ladder, pre-aligns the period to 128 bytes,
keeps the documented NOT_ALIGNED recovery for the devices that still need
it, joins MMCSS Pro Audio, and refuses S_FALSE loudly - a "closest match" is
by definition not the format we asked for.

Verified against the owner's Cambridge Audio USB Audio 2.0 with the new
example, and it matches the spike exactly:

  rate 96000, S24In32 (24/32), period 30000 hns, buffer 288 frames,
  realigned false, timing Events, mode DirectHardware,
  960000 frames, 0 underruns, 0 event timeouts

Three faults were found on the way, and all three were in the MEASUREMENT
rather than the audio - worth recording because each looked like a hardware
problem:

  - 339 "underruns" were event timeouts counted in the same counter. Split
    them; they are different faults and were hiding each other.
  - 337 event timeouts came from a 2x-period deadline. At a 3 ms period that
    expired on 10% of wakeups because Windows scheduling is not that
    punctual. The deadline exists to stop the thread hanging on a dead
    device, not to detect glitches, and the spike waited 1000 ms and saw
    none in ten minutes. It is generous now.
  - The last 2 were a drain's trailing silence. A drain emitting silence is
    the drain working.

The stub for every other platform mirrors alsa_direct's: constructors refuse,
teardown succeeds, so a caller written against this compiles and unwinds
everywhere. `cargo tree` shows exactly one windows v0.62.2 - no second
windows tree entered the graph.

cc
2026-08-27 09:10:37 -06:00
vicrodh 7135c841cb build(windows): embed the icon, VERSIONINFO and a longPathAware manifest
winresource, not winres: winres has been unmaintained since 2021 and this
is its maintained fork. Gated to cfg(windows) as a target build-dependency,
so no other platform resolves it at all.

The manifest declares longPathAware and NOTHING else. Qt 6 sets
PerMonitorV2 itself at startup and a manifest DPI declaration is immutable,
so the two together either log a permanent COM error 0x5 or degrade
scaling - Qt owns DPI awareness.

The .ico carries 256/128/64/48/32. The four small entries are BMP and only
256 is PNG: measured on this box, System.Drawing.Icon (GDI+) throws
ArgumentOutOfRangeException in ToBitmap() on a PNG-payload entry, while WIC
decodes it. 256 has to be PNG because the directory encodes its size as 0.
Pixels verified against the source PNG at four points including an
asymmetric one, which is what proves the BGRA order and the bottom-up flip.

cc
2026-08-27 03:46:29 -06:00
vicrodh f06ffc7141 fix(qbzd): gate the instance lock per OS
lock.rs imported std::os::unix and called libc::flock unconditionally, so
a workspace test run could not compile on Windows.

The Windows arm locks ONE byte at offset 1 GiB rather than the whole file.
Byte-range locks are mandatory there, not advisory like flock: measured on
this box, a whole-file lock from offset 0 makes the losing instance's
read_to_string fail with ERROR_LOCK_VIOLATION, so AlreadyRunning would
always carry None - the pid is the only reason that variant exists. With
the lock byte past any content the pid still reads back, the same handle
conflict is still detected, and the lock still releases when the File
drops. Reserving a region past EOF is what SQLite's Windows VFS does.

Only ERROR_LOCK_VIOLATION means contention; a filesystem that does not
implement byte-range locks answers ERROR_INVALID_FUNCTION and must surface
as Io, not as a daemon that is not running. The unix arm keeps its
existing any-failure-means-busy behaviour unchanged.

cc
2026-08-26 23:07:02 -06:00
vicrodh 57f900bec4 fix(local-catalog): Windows arms for free-space preflight and bootstrap lock
The not(unix) arm returned Err and both preflight() and prepare()
propagate it, so the catalog never bootstrapped on Windows and the Local
Library died at preflight.

GetDiskFreeSpaceExW on the nearest existing ancestor, since the catalog
root may not exist on first run; LockFileEx with FAIL_IMMEDIATELY, mapping
ERROR_LOCK_VIOLATION to BootstrapBusy like the flock arm and any other
error to Io. Nothing reads this lock file's contents, so the whole-file
range is safe here.

cc
2026-08-26 23:06:52 -06:00
vicrodh d9ed19eb64 refactor(mixtape): move provider resolution to source registry
cx
2026-08-26 21:38:58 -06:00
vicrodh b6c69509df build(deps): unify lofty at 0.25.1
cx
2026-08-26 21:31:47 -06:00
vicrodh c039f0d0ee build(deps): integrate lofty 0.24 update (#679)
cx
2026-08-26 21:29:02 -06:00
vicrodh b60ed2c268 fix(secrets): integrate real desktop keyring backends (#697)
Preserve Afonso’s restart regression test while selecting native stores only on Linux, macOS and Windows. Unsupported targets take the persisted KDF path instead of keyring’s successful in-memory mock; the stale workflow change is deliberately excluded.

cx
2026-08-26 20:59:27 -06:00
vicrodh 388a3591e2 fix(qobuz): integrate atomic bundle cache replacement (#707)
Preserve Afonso’s same-directory temp-file replacement on top of the configurable cache root from #708. Cover the complete explicit-root round trip and document the exact process-crash and concurrent-writer guarantees.

cx
2026-08-26 20:50:33 -06:00
vicrodh b292e84abb chore(deps): integrate serde_json 1.0.151 (#650)
cx
2026-08-26 17:45:51 -06:00