The owner asked whether the remaining ~1 s was the sweet spot. It was not —
it was an artefact of fixing this with a push instead of a getter.
MPRIS `Position` is read on demand, so what matters is the answer when the
client ASKS, not when the app last spoke. The backend stored a snapshot, so
the answer was only ever as fresh as the last push, and the only lever left
was polling faster — which would have meant a second timer the perf rules
refuse.
Storing the instant alongside the value removes the tradeoff: the getter
returns `position + elapsed` while Playing, exact to the microsecond however
rarely the app pushes. The 1 Hz push stops being the clock and becomes what
it should have been all along — a re-anchor against drift.
Every write re-stamps, including a status change that carries no position:
without that the base would keep running across a pause and the clock would
count while the music did not.
cc
The owner's observation is what separated the two halves of this: "el
contador del seekbar del widget solo empieza a contar hasta que lo abro."
A client reads Position ONCE, when it opens, and extrapolates from there —
it does not poll. So two different things were broken and only one was fixed
by keeping the stored position fresh.
Fresh storage fixes what a client sees when it OPENS. `Seeked` is the only
thing that reaches one that is ALREADY open: it tells the client to stop
extrapolating and re-read. Without it, a seek moved the music and the widget
kept counting from where it had been.
So the seek path emits Seeked instead of re-asserting the playback state,
which was the previous commit's guess at a cure inside an API that had no
signal. `mpris_server::Signal::Seeked` was there all along.
Defaulted on the trait to `set_position`, so a backend without the signal
still gets the value right on the next read and no other consumer changes:
cargo check passes for qbz-app and qbzd.
cc
The owner measured QBZ at 4:03 with Plasma's widget stuck at 0:14. The
earlier seek fix was right about seeks and wrong about the shape of the
problem: the position was not drifting, it was FROZEN.
MPRIS `Position` is a read-on-demand property — the spec deliberately keeps
it out of `PropertiesChanged`, so a client reads it when it wants it. The
Linux backend serves whatever it last stored, and it only ever stored a
position when `set_playback` carried one: at a track start (zero) and at a
play/pause edge. Between those, every read returned the same stale number
for as long as the track ran.
This is in the SHARED crate, so the shipping Slint build has it identically —
nobody had watched the widget's clock there.
`MediaIntegration::set_position` stores and emits nothing, so the Qt poll
loop refreshes it on the tick it already computes the position for: one
channel send per second, no bus signal, so it does not become the per-tick
work the perf rules refuse. The method is DEFAULTED, so no other consumer has
to change — `cargo check` passes for qbz-app and qbzd, and the Slint frontend
needs no edit (it cannot be built here, per the qbz-ui wall, so it is
verified statically as R4 requires).
cc
playerctl scripts/widgets can now identify the exact song:
https://open.qobuz.com/track/<id> on every Qobuz catalog row (offline
qobuz_download keeps the real id, so it qualifies too). Local/Plex rows
omit the field — QueueTrack carries no file path to map to file:// and
a synthetic local id would mint a bogus Qobuz link. Wired in both the
app (refresh_now_playing_meta) and qbzd (track_meta).
km
QBZ let the machine suspend mid-playback. Take an org.freedesktop.login1 Inhibit (sleep:idle, block) fd while the MPRIS status is Playing and drop it on pause/stop, hooked to the same play-state channel MPRIS already uses. Linux-only, all failures log-and-continue. Resolves#522.
cc
zbus features unify across the dependency graph and "tokio" wins over
"async-io": enabling it via rfd, ashpd (secret portal) and ksni's
default features flipped the shared zbus 5 executor to tokio for
accesskit_unix/atspi too — Slint's accessibility backend opens its
D-Bus connection on a plain thread and panicked at startup ("there is
no reactor running, must be called from the context of a Tokio 1.x
runtime"), killing AT-SPI support. Switch every zbus-5 consumer we
control to the async-io backend (rfd "async-std", ashpd "async-io",
ksni default-features off + "async-io"): async-io connections
self-drive from any thread and the futures still await fine inside our
tokio runtime. The zbus 4 group (keyring/secret-service/mpris-server/
qbz-audio) is a separate feature graph and stays on tokio by design.
cc
image::open resolves the format from the path extension only (no content
sniffing in image 0.25), so every notification artwork served from the
shared image cache - files named {md5}.img since slice 3b/B11 - failed as
Unsupported and the toast fired icon-less on online Qobuz plays. Decode
via fs::read + image::load_from_memory (magic bytes, extension-agnostic),
which also heals mislabeled downloads in the md5 cache. file:// paths now
percent-decode (cached_file_url_for builds them via Url::from_file_path),
fixing the latent spaces/non-ASCII fall-through to an HTTP GET of a
file:// URL. Offline semantics unchanged.
cc
Shared artwork HTTP client gains the missing 10s total timeout (half-up
networks could pin the 16-permit semaphore). fetch_cached_http short-
circuits while offline: disk hits serve, misses return the placeholder
immediately with zero network and no negative caching. Now-playing meta
resolves MPRIS art to a cached file:// URL (or none) instead of an
unfetchable https URL while offline; system notifications skip their
blocking artwork download offline via an injected flag, keeping
qbz-media-controls frontend-agnostic.
Part of offline-mode slice 3; main.rs wiring rides the slice-3 wiring
commit.
cc
Port the Tauri desktop-notification path to a frontend-agnostic crate so the
Slint app (and any future TUI/CLI) fires it from native Rust instead of a
webview invoke.
New qbz-media-controls::notify module:
- Linux: XDG notification portal via ashpd (D-Bus); album art passed as
Icon::Bytes after center-crop + downscale to <=512px + PNG re-encode (<=4 MiB).
- macOS: notify_rust with image_path (cover cached to disk).
- Windows: no-op (parity with Tauri).
- Self-contained artwork download + md5 cache + resize, matching the Tauri
helpers, with no dependency on the frontend (ADR-006).
Wired in playback::refresh_now_playing_meta (the same point that pushes the
tray tooltip + OS media controls):
- De-dupe by track id so only an actual track change fires (the refresh also
runs on resume/seek).
- Skip while a remote QConnect renderer drives playback (matches the Svelte
skipIfRemote). Fire-and-forget on a tokio task.
Enabled by default, matching the Tauri default; a settings toggle is a
follow-up.
cc
New frontend-agnostic crate qbz-media-controls (ADR-006): one MediaIntegration
trait, cfg-split backends. Linux uses mpris-server and publishes the MPRIS
org.mpris.MediaPlayer2.DesktopEntry property as com.blitzfc.qbz — the only way
GNOME Shell resolves the app icon for its media widget (souvlaki, the old
cross-platform crate, never set it, so GNOME showed no icon; KDE is lenient).
macOS/Windows keep souvlaki (MediaRemote/SMTC), where the icon comes from the
app bundle, not a DesktopEntry. The Linux server runs on a dedicated thread
with its own tokio runtime; state arrives over an async channel.
Wired into qbz-slint via src/media_controls.rs: started on shell entry,
metadata/playback pushed from the playback paths (mirroring the tray), and
inbound control events (media keys, the desktop media widget, Now Playing)
dispatched to the player through the existing QConnect-aware helpers. Both
Linux and macOS compile clean.
cc