New v2_get_system_info command and matching UI section in the
System Diagnostics panel. What it reports:
- os (linux/macos/windows)
- arch (x86_64, aarch64)
- kernel_version from /proc/sys/kernel/osrelease
- Linux distro: id, version_id, pretty_name from /etc/os-release
(falls back to /run/host/os-release so it works inside Flatpak)
- install_method: flatpak / snap / appimage / dev / native, detected
via FLATPAK_ID + /.flatpak-info, SNAP, APPIMAGE env vars, and
debug_assertions as the last fallback
- flatpak_runtime + version: parsed from /.flatpak-info
- loaded library versions (from /proc/self/maps, symlink-resolved):
webkit2gtk-4.1, gtk-3 (or 4), glibc, alsa, pipewire, pulseaudio
All info is derived from local files/env; no sensitive data, no IDs,
no hashes. Included in the clipboard export.
cc
- Add core:window:allow-set-title to capabilities/default.json. Tauri 2
rejects setTitle() from the frontend unless the capability is
explicitly granted; without this the window-title feature silently
fell back to "QBZ" in every case.
- Login logo: 175px -> 140px (-20%) per feedback.
- Drop the debug console logging on the title effect now that the
wiring is confirmed working; keep the warn-on-failure.
cc
Tray icons (tray.png, tray-light.png, tray-dark.png) and the Snap
launcher icon were left at the v1.2.4 hand-drawn outline Q during the
v1.2.5 branding refresh. Regenerate them from the canonical
icon-symbolic.svg / 256x256.png so the system tray, KSNI panel and
Snap launcher show the matte-vinyl Q.
Also drop the drop-shadow filter on the login screen logo: with the
new icon's edge rim and inner highlight, the extra ambient shadow is
redundant and visually muddies the artwork.
cc
Issue #319: Infinite Play toggle had no backend wiring after the V2
migration — toggle only wrote to localStorage and the queue-end handler
ignored it, so playback stopped at the last track.
Reimplemented in V2 (no legacy wrappers):
- New `v2_create_infinite_radio` command that takes the last few played
track IDs, uses the most recent track's artist as the radio seed via
`RadioPoolBuilder::create_artist_radio` (Qobuz `/radio/artist`
endpoint), and returns 50 V2QueueTrack ready to append. Does NOT
mutate the queue or playback context — caller decides how to use them.
- `playbackPreferencesStore`: expose `'infinite'` in `AutoplayMode`
type and add `isInfinitePlayEnabled()` helper.
- `+page.svelte`:
- `handleToggleInfinitePlay` now syncs to backend via
`setAutoplayMode('infinite' | 'continue')` so the choice persists
in `playback_preferences.db` across restarts (was localStorage only).
- On startup load infinite play from cached preferences (was
localStorage).
- When `nextTrack()` returns null AND infinite play is on: collect
the current track + last 5 history tracks, call
`v2_create_infinite_radio`, append the result via
`v2_bulk_add_to_queue`, then advance — playback continues without
interruption.
i18n: add `toast.failedToSavePreference` to all 5 locales.
cc
Tighten the viewBox so the vinyl fills ~97% of the canvas (matching
Steam/Chrome/Papirus sizing). Add a #3968be stroke rim around the
vinyl edge so the icon remains visible on pure-black backgrounds.
Include a subtle radial inner-light from top-left for depth.
Also adds 512x512.png for Flathub compatibility.
Regenerate all PNG exports: Tauri standard sizes, Windows Store
squares, ICO, Android mipmaps, and static logos.
cc
The v1.2.5 tag push flagged three macOS-only compile errors on the
release-macos workflow (Linux, aarch64 Linux, flatpak still building
successfully). All three are Linux-only symbols pulled unconditionally
into files that are compiled on every target:
* src-tauri/src/tray.rs: imports PathBuf inside the
`cfg(not(target_os = "linux"))` branch without `use std::path::PathBuf`.
Add the import under the matching cfg so macOS / Windows see it.
* src-tauri/src/commands_v2/playback.rs: `cached_audio_incompatible_with_hw`
is defined only on Linux (ALSA). Callsites are already
`cfg(target_os = "linux")`, but the shared `use super::{...}` block
was not. Move that one symbol out into a Linux-only import.
* src-tauri/src/commands_v2/legacy_compat.rs: same story with
`v2_prepare_notification_icon_bytes` — only the Linux notification
path calls it, but the import was unconditional.
No runtime behaviour change; this only unblocks the release-macos
(and any future Apple-target) workflow so it can build.
cc
* src-tauri/Cargo.toml and Cargo.lock: 1.2.4 -> 1.2.5
* src-tauri/tauri.conf.json: 1.2.4 -> 1.2.5
* package.json (+ package-lock.json regenerated): 1.2.4 -> 1.2.5
* snapcraft.yaml: 1.2.4 -> 1.2.5
* packaging/flatpak/com.blitzfc.qbz.metainfo.xml: new <release> entry
dated 2026-04-15 covering the visual refresh (new logo + login
branding), Plasma/Klassy auto-detect for window controls,
opt-in match-system-chrome rounded corners, Editor's Picks
ribbon cleanup, the two sidebar menu positioning fixes shipped
alongside this bump, and the DAC wizard contrast fix.
Tag not applied per request — release notes are staged, ready for
the screenshots pass and manual tag afterward.
cc
Rounded-corner window chrome now actually works. The toggle in
Settings > Appearance persists to the window_settings SQLite store and
gates the Tauri window transparency decision at startup, so on next
launch the main window is rebuilt with `transparent: true` and the
border-radius plus edge outline apply without painting white corners.
Flow:
* Toggle in Settings > Appearance -> windowChromeStore saves to
localStorage and calls v2_set_match_system_window_chrome which
writes match_system_window_chrome to window_settings.db.
* Toast reminds the user to restart.
* On next launch, should_use_main_window_transparency() reads the
setting and returns true, Tauri builds the window transparent.
* Setup stores the decision in MAIN_WINDOW_TRANSPARENT. A new command
v2_main_window_is_transparent reports it back to the frontend.
* +page.svelte only applies .app.match-chrome when the window is
actually transparent AND the custom titlebar is visible, so
stale-state toggling never produces white wedges.
* CSS sets html/body transparent via a global class on <html>, then
.app.match-chrome.floating paints the rounded corners plus a
12/28 rgba drop shadow and a 1px darkening outline.
* Maximized state clears the radius automatically.
Env vars QBZ_FORCE_TRANSPARENT_WINDOWS / QBZ_FORCE_OPAQUE_WINDOWS still
take precedence over the DB-persisted toggle.
cc
Adds an opt-in "Match system window chrome" toggle in Settings >
Appearance. When enabled (and the custom title bar is active, i.e.
neither "Use system title bar" nor "Hide title bar"), QBZ rounds its
own corners to match the detected desktop's decoration radius and
paints a thin edge outline so the window reads as its own surface
against the desktop instead of a bare rectangle.
Phase 1 scope:
* New windowChromeStore (localStorage) holding the toggle and the
detected corner radius.
* desktop_theme module gains a best-effort window_corner_radius_px:
10 for Klassy, 6 for Breeze Plasma, 12 for GNOME/Adwaita, 8 fallback.
* +page.svelte subscribes to the detector; .app.match-chrome.floating
applies the radius via a CSS variable plus a 1px darkening outline
and a deeper shadow. Maximized state removes the radius.
* SettingsView: new toggle, disabled when hideTitleBar or
useSystemTitleBar is on; i18n for en/es.
External shadow that matches each DE's compositor remains a phase 2
item — it requires `transparent: true` on the Tauri window, which has
compositor stability trade-offs and must be restart-gated.
Also cleans up redundant AlbumCard ribbons in the Editor's Picks
TAB (distinct from the home tab that was fixed earlier): removes
ribbon={album.ribbon} from the `editorPicks` and `qobuzissimes`
sections inside the editor-picks tab block. Press Accolades and
Popular Albums keep their ribbons (they vary per card).
cc
Adds a runtime-detected "Klassy (auto-detect)" preset for the custom
window controls that reads the user's Plasma decoration theme and
applies matching colors and shape. When Klassy itself is not installed
but Plasma is, the option re-labels to "Plasma (auto-detect)" and
still applies the desktop color scheme. Hidden on non-KDE / non-Linux.
Backend:
* New desktop_theme module parses kwinrc, kdeglobals, and klassyrc
without external deps. Exposes detect_desktop_theme Tauri command
returning DesktopThemeInfo: desktop id, isKlassy flag, WM active/
inactive background + foreground, accent, DecorationHover, and the
user's Klassy ButtonIconStyle / ButtonShape when present.
Frontend:
* windowControlsStore gains applyKlassyPreset(), a cached detection
helper, and mapKlassyShapeToQbz() that maps Klassy's button shapes
(FullHeightRectangle, FullHeightRoundedRectangle, Tab, Circle,
Square, IntegratedRoundedRectangle, ...) onto QBZ's shape set.
* Adds the full-height-rounded shape to ButtonShape to cover the
most common Klassy style that QBZ was missing; CSS matches Klassy
FullHeightRoundedRectangle visual (full-height tab with ~7px
radius and margin).
* SettingsView fires detection on mount, derives the preset list
reactively so the auto-detect option only appears when relevant,
and adds the new shape to the Style picker.
* i18n keys added for the new shape and both preset labels in en/es.
cc
Apply the new matte-vinyl QBZ logo (Flathub-compliant: square canvas,
74% footprint, no baked shadows/gloss) across every icon asset: Tauri
standard sizes, Windows Store squares, Android icons and mipmaps,
static/, README, and the tray.
Ship two tray variants (black and white glyphs) and pick the right
one at runtime by reading GNOME color-scheme, GTK settings, and KDE
kdeglobals. Defaults to light-theme glyph when nothing matches.
Login screen: logo 80 -> 175 px with a subtle var(--text-muted)
drop-shadow using color-mix() for alpha, plus QBZ and "Qobuz(TM)
Player" under the icon. Update app.tagline i18n accordingly.
cc
rodio deprecated `DeviceTrait::name()` in favour of `description()`
(name) / `id()` (stable identifier). The legacy fallback in
`preferred_name()` was unreachable on our target platforms anyway —
the only path that matters is the `description()` one, since that is
what `get_audio_devices()` persists in settings.
Skip the device if `description()` fails instead of falling back to
the deprecated API. Silences the build warning.
cc
- src-tauri: rand 0.10.0 → 0.10.1 via cargo update (alert #36)
- qbz-player: rand 0.8 → 0.9, `gen_range` → `random_range` (alert #38)
The underlying RUSTSEC issue only triggers for code that calls
`rand::rng()` alongside a custom logger — we don't do that anywhere,
so actual impact is nil. Bumping closes the alerts.
cc
Level A of issue #307 was firing 'device no longer available' spuriously
because settings.output_device stores the ALSA-normalized form
(front:CARD=X,DEV=0 per normalize_device_id_to_stable) while cpal's
output_devices() enumeration returns different strings entirely —
DeviceTrait::description() (friendly) and DeviceTrait::name() both
produce values that don't match what the UI persisted. Comparing them
marked every configured device as 'missing' on normal playback.
Removes the pre-flight emit_missing_if_needed() calls from
v2_play_track and v2_resume_playback so the false-positive toast
stops firing. The init_device fallback still routes audio to default
on a truly missing device — we just no longer surface that to the UI,
which is net neutral for the user vs. wrong notifications.
Keeps audio_device_watch module, DeviceMissingThrottle state and the
frontend listener in place so Level B (emit from inside the audio
thread when the fallback actually triggers) can reuse the plumbing
without another refactor. Hardens the presence check with
empty-enumeration = inconclusive and description()-based matching for
whenever Level B actually wires up a call site that has a comparable
device name at hand.
cc
Level A of issue #307. When the configured output device has
disappeared from the host device list at the time of play/resume
(KVM switched away, USB sink unplugged, PipeWire sink removed…)
the backend emits an 'audio:device-missing' event and the frontend
shows a warning toast:
'Output device "XYZ" is no longer available. Using system default.'
The existing init_device fallback already routes audio to the system
default, so this is purely informational — the user now understands
why audio came out of a different sink instead of staring at a silent
progress bar.
Does NOT touch the sacred audio init path (pipewire_backend.rs,
init_device, audio_settings.rs). New module audio_device_watch.rs
adds a pure presence check, a new-state throttle so we don't emit on
every resume once the user's been notified, and a v2 command
v2_check_audio_device_presence so the frontend can poll on demand.
Ready for Level B (background watchdog) — the same helpers plug into
a timer without further refactor; Level B only needs to add a tokio
task that calls emit_missing_if_needed() every N seconds while
playback is active.
cc
Root cause of 'Discoteca Ideal Qobuz not clickable': /album/get
returns awards as LegacyAwardDto (per a20/c.java + LegacyAlbumDto
in the Android decompilation) whose id field is named awardId, not
id. Our AlbumAward struct only looked for 'id', so every award from
/album/get deserialized with id=None and the click fell back to the
catalog resolver — which was itself incomplete.
Adds serde aliases 'awardId' / 'award_id' for the id field and
'awardedAt' for awarded_at. Applies to both qbz-models (V2) and the
src-tauri legacy mirror.
Endpoint coverage:
- /discover/index → {id, name, awarded_at} (primary match)
- /album/get → {awardId, name, awardedAt, …} (alias match)
- /artist/page → {id, name, awarded_at} (primary match)
cc
/album/get's AwardDto has id: String? — Qobuz sometimes returns an
award entry with only a name and no id. Rather than disabling the
click or toasting, we look up the id in the Qobuz award catalog.
- Adds v2_get_award_explore (paginated /award/explore) all the way
through qbz-qobuz → core → bridge → command → invoke_handler.
- New frontend awardCatalogStore caches the full (id, name) list
lazily, one fetch per session, resolved on first click that needs
it. Further clicks are instant.
- handleAwardClick goes straight to navigation when id is present,
falls back to resolveAwardIdByName otherwise. Toast only fires if
the name isn't in the catalog either (genuinely unknown award).
cc
The log showed 'award=null' reaching /award/getAlbums and Qobuz
replying '400 Bad Request: Invalid argument: award_id (accepted type
are number)'. Root cause: AlbumAward.id was typed Option<u64> and
/album/get returns award ids as strings; serde dropped them to None,
frontend stringified null → 'null', we sent that as the query param.
Fix:
- AlbumAward.id becomes Option<String> with a custom deserializer
that accepts both string and integer JSON values.
- TS types + AlbumDetailView Award interface + pickAlbumRibbon all
use string id (compared as '88'/'151' now).
- Sidebar button is disabled when id is falsy; handleAwardClick
early-returns on empty id so we never navigate to award 'null'.
- Better body/status logging in get_award_albums stays so future
4xx responses are visible in the log.
Also switches /award/page and /award/getAlbums to signed_get_auth
(app_id + user token). Mobile does the same — catalog visibility is
region-scoped and the unsigned variant returns 400.
cc
Two behavior fixes for AwardView based on user feedback.
1. Grid content. The previous implementation rendered the releases
array from /award/page, which is user-scoped (only surfaces awards
on albums the user already favorited), so the grid came up empty
in almost every case. Switch to /award/getAlbums which is the
public catalog listing with pagination. /award/page is still
called, but only to hydrate the hero (name, image, magazine).
2. Follow-the-award. Awards themselves are followable on Qobuz mobile
(it's what feeds the 'Awards' tab of Release Watch). Add:
- favorite_awards table + store methods in favorites_cache.rs
- 4 V2 commands (get/sync/cache/uncache) mirroring the label ones
- awardFavoritesStore.ts — clone of labelFavoritesStore with string
ids (Qobuz mobile API passes award_ids as strings)
- Follow/Unfollow heart button in AwardView hero, matching the
ArtistDetailView / LabelView visual
- loadAwardFavorites() wired into the login bootstrap alongside
loadArtistFavorites / loadLabelFavorites
- i18n keys (award.follow / award.unfollow)
Also adds a Ver más button for paginated loading of the album grid.
cc
Previous attempt loosened PageArtistAward (id u64 -> Option<u64>,
awarded_at custom deserializer) to accept /album/get's inconsistent
award shape. That rippled into /artist/page consumers, which the user
rightly called out ('no eliminar lo que ya funcionaba, adaptar pues').
Restores PageArtistAward to its original shape (id: u64, name: String,
awarded_at: Option<String>) and introduces a parallel AlbumAward type
used only on DiscoverAlbum and Album. AlbumAward tolerates missing id,
accepts awarded_at as int or string, and never fails deserialization
of the parent payload. /artist/page keeps its strict contract.
cc
Backend plumbing for Phase 3 AwardView. New endpoints:
- v2_get_award_page(awardId) → hero + categorized releases
- v2_get_award_albums(awardId, …) → paginated album list
Mirrors Android's AwardDto shape (id, name, image, awarded_at,
magazine, releases, playlists). Follows the label/page pattern for
signing and error handling. Deserializer accepts the V2 generic
{has_more, items} envelope as well as the legacy search-style
{albums: {items, total}} in case Qobuz swaps shapes per account.
cc
/album/get returns award.awarded_at as a Unix timestamp integer while
/discover/index returns it as a YYYY-MM-DD string — our struct only
accepted String, so /album/get started failing with
'invalid type: integer `…`, expected a string' as soon as awards were
added to Album in Phase 2. Every 'Load album' call broke.
Adds a custom deserializer that accepts both shapes and normalizes to
String downstream. Applied in both qbz-models (V2) and src-tauri legacy
api::models for symmetry.
cc
Ports the Qobuz web 'label + awards' sidebar pattern to AlbumView.
Track list and sidebar sit side-by-side above 1100px and collapse
vertically below it. Label card navigates to the existing LabelView.
Award cards call onAwardClick when provided (disabled until Phase 3
lands AwardView).
Wiring:
- Adds awards (Vec<PageArtistAward>) to both qbz-models Album and
src-tauri api::models Album so /album/get payloads hydrate the
field instead of silently dropping it.
- Adds awards to QobuzAlbum and AlbumDetail frontend types plus the
convertQobuzAlbum adapter.
- New album.sidebar i18n keys in all five locales.
Card ribbon still shows only the LAST award (Phase 1 behavior);
sidebar renders every award as a tap target — mobile-style.
cc
The ribbon feature relied on album.awards arriving on the frontend, but
the DiscoverAlbum Rust struct in both qbz-models and src-tauri/api was
missing the 'awards' field, so serde silently dropped it during
deserialization of /discover/index. Result: frontend always saw an
empty awards array and pickAlbumRibbon returned undefined.
Also bumps the home sessionStorage cache to schemaVersion 2 so pre-fix
caches (which persisted AlbumCardData without ribbon) are discarded on
next launch instead of rehydrating with missing fields.
cc
Adds the same follow flow we already have for artists, applied to
labels. Confirmed against the inferred OpenAPI spec
(qbz-nix-docs/qobuz-api-inferred-openapi-v9.7.0.3.yaml) — the
existing /favorite/create, /favorite/delete and
/favorite/getUserFavorites endpoints already accept fav_type='label'
and label_ids, so no new Qobuz endpoints were needed.
Backend
- favorites_cache: new favorite_labels SQLite table + add / remove /
sync / get methods alongside the artist counterparts. clear_all
drops the new table on logout.
- commands_v2::favorites: 4 new commands —
v2_get_cached_favorite_labels, v2_sync_cached_favorite_labels,
v2_cache_favorite_label, v2_uncache_favorite_label. Same wiring as
the artist commands.
- lib.rs invoke_handler registers the four new commands.
Frontend
- src/lib/stores/labelFavoritesStore.ts: clone of
artistFavoritesStore — same in-memory Set, same isLoaded flag,
toggle calls /favorite/create with fav_type=label, sync paginates
/favorite/getUserFavorites?type=labels.
- routes/+page.svelte: loadLabelFavorites() called alongside the
other three after login is confirmed.
- LabelView.svelte: heart-shape Follow button in the label header
info column, mirroring ArtistDetailView's .artist-actions
treatment. Reactive to the store via labelFavoritesVersion bump
so any future Favorites view that toggles a label will keep this
view in sync.
- i18n: label.follow / label.unfollow in all five locales.
cc
The inferred OpenAPI spec
(qbz-nix-docs/qobuz-api-inferred-openapi-v9.7.0.3.yaml, source:
x20/b.java) documents the endpoint as /favorite/getNewReleases with
three required query params:
@GET favorite/getNewReleases
- type: String (artists | labels | awards — matches the three
tabs in the mobile UI)
- limit: Integer
- offset: Integer
My earlier attempts at /albums/releaseWatch were chasing the Android
route-table entry for the Release Watch *screen*, not the HTTP call
that screen makes. Different layer.
Also removes my made-up speculation about mobile app_id gating — the
/favorite/* endpoints are standard web-API calls the existing client
already knows how to sign (via authenticated_headers + /0.2/ base URL).
v2_get_release_watch now takes an optional releaseType arg defaulting
to 'artists' so callers can switch tabs later without another command.
cc
Third try's the charm. The previous attempt called the endpoint via
signed_get_auth (RPC-style with request_sig + request_ts), which
yielded 'no Service found with those values' — the RPC dispatcher
simply doesn't know this path.
The /albums/* plural namespace (releaseWatch, favoriteAlbums,
purchasedAlbums, similarTo, essential, artistAlbums, discoverAlbums)
is REST-style, not RPC. QBZ already uses the same pattern for the
/radio/* endpoints in src-tauri/src/api/client.rs: plain
.get(url).headers(authenticated_headers()).query(...) with X-App-Id
and X-User-Auth-Token, and no signing. Same pattern here.
Also reverted the release_watch container addition to
DiscoverContainers — /discover/index does not return it alongside
the editorial sections, confirmed by the empty result when reading
from there even with follows set.
Radio and Artist Spotlight work in QBZ via this same REST + auth
headers pattern despite being 'mobile' features; Release Watch
should too.
cc
The initial implementation added a dedicated get_release_watch client
method calling /albums/releaseWatch, but that path is only routed on
the mobile API; the web API (api.json/0.2) that qbz-qobuz speaks to
returned 'no Service found with those values'.
The correct source is already available: /discover/index — which we
were already calling for newReleases/pressAwards/qobuzissimes/etc —
also returns a release_watch container when the user has follows. We
were silently dropping it because DiscoverContainers didn't declare
the field.
Now:
- DiscoverContainers (Rust + TS mirror) declares the release_watch
field with a serde alias that accepts both camelCase and snake_case
spellings, so future naming churn upstream is tolerated
- HomeView reads it in fetchAllDiscoverData next to the other editorial
sections (one less network round-trip)
- ForYouTab calls v2_get_discover_index and extracts only the
release_watch container
- Reverted the custom /albums/releaseWatch endpoint, client method,
core passthrough, bridge wrapper and v2_get_release_watch command
Section identity (homeSettingsStore id, i18n keys, render blocks)
is unchanged.
cc
The Qobuz mobile client (v9.7) surfaces a 'Release Watch' /
'Radar de Novedades' feed in its Discover screens — new releases
from the artists, labels and award series the user follows. We
didn't have parity for it. This adds it as a module in both
Discover tabs.
Backend:
- qbz-qobuz: new ALBUMS_RELEASE_WATCH endpoint constant
(/albums/releaseWatch, plural as in mobile) and signed_get_auth
client method returning a SearchResultsPage<Album>. Signing
method is 'albumsreleaseWatch' per the Qobuz secret protocol.
- qbz-core: passthrough get_release_watch on QbzCore.
- src-tauri: core_bridge wrapper, new v2_get_release_watch Tauri
command registered in lib.rs. Filters through the artist
blacklist like other album endpoints.
Frontend:
- HomeView: new renderable section 'releaseWatch' in the home tab
(default visible, placed right after New Releases). Parallel
fetch alongside the existing discover_index call; mapped via
the existing toAlbumCard helper.
- ForYouTab: section appears ahead of 'Essentials' since followed
artists/labels are typically more personal.
- homeSettingsStore: new HomeSectionId 'releaseWatch' in the type
union and DEFAULT_SECTIONS so the user can toggle/reorder it
from home customization.
- i18n: discover.releaseWatch.title + subtitle added to all five
locales, using Qobuz's own translations where they exist
('Radar de Novedades' for Spanish, 'Release-Radar' for German)
with the 'followed artists/labels/awards' subtitle copy adapted
from the mobile app's strings.xml resources.
cc
Addresses the reported behavior where tracks marked available offline
still buffered from Qobuz after the v1.2.2 upgrade. Three layers:
1. **Snapshot / restore stream-first-track.** offline_settings gains a
new nullable column pre_offline_stream_first_track (SQLite ALTER
migration). v2_set_manual_offline now coordinates with
AudioSettingsState: entering offline mode stashes the user's current
stream_first_track preference and forces it to false; exiting
restores the snapshot. User's preference survives the round-trip
even across app restarts.
2. **Explicit offline refusal.** v2_play_track now takes
OfflineState, reads manual_offline_mode up front, and — when every
local cache miss has been exhausted — returns a new
RuntimeError::TrackNotAvailableOffline instead of silently reaching
for the network. Low-quality offline hits still play (better than
nothing). The playback path also masks stream_first_enabled while
offline as a belt-and-suspenders defense.
3. **Cache-miss visibility.** Every call site that consults the
offline cache DB now logs [V2/CACHE STALE] at warn level when the
DB says a track is cached but the filesystem path is missing. This
made the issue #279 behavior invisible; the orphan entries from
per-user isolation migration (aab3e6764) are now diagnosable from
logs.
Frontend:
- playbackService surfaces TrackNotAvailableOffline as a toast ('…
isn't saved for offline playback') instead of the generic error
- SettingsView listens for a new audio-settings-changed event so the
stream-first-track toggle in the UI flips when the offline toggle
mutates it
- i18n key offline.trackNotAvailableOffline added to all 5 locales
cc
The old Linux-only guard only rejected absurd sizes (>8K) as corruption
sentinels; anything under that was passed through to the builder
verbatim. So a size saved on a 4K display would open on a 1080p display
at full 4K dimensions — off-screen, unreachable when decorations are
hidden.
Added a second clamp inside setup() that queries app.available_monitors()
after Tauri has the display connection, takes the largest logical size
across all connected monitors, and shrinks the persisted size to 95% of
that when it overflows. The original 8K corruption guard stays as a
first pass (runs before setup, catches corrupt DB rows before we even
open a display connection).
Works on all platforms — no cfg gating, since 'saved size bigger than
any monitor' is a user-visible problem everywhere, and the 8K guard
also protects macOS/Windows against a corrupt settings row.
cc
Tauri's default tray uses libayatana-appindicator, which by design does
not dispatch primary-click events to its client — libayatana delegates
every left-click to the libdbusmenu-managed menu. The existing
show_menu_on_left_click(false) setting and TrayIconEvent::Click handler
never fire on Linux. Confirmed present in tray-icon 0.21.x and 0.22.0
(the newest release).
Switch the Linux backend to ksni, a pure-Rust StatusNotifierItem
implementation that exposes the full SNI protocol including Activate
(left-click), SecondaryActivate (middle), and ContextMenu. Same SNI
compatibility as the previous backend — works wherever the panel
already handled our tray (KDE, GNOME+AppIndicator, XFCE/Cinnamon/MATE,
wlroots trays). Flatpak just needs the --talk-name=org.kde.
StatusNotifierWatcher permission which is already in our manifest.
Tauri's tray stays on macOS (no Windows client is shipped). The
dispatch lives in tray.rs behind cfg(target_os); Linux delegates to
the new tray_linux_ksni module. Playback-control menu events
(tray:play_pause, tray:next, tray:previous) still emit into the main
webview window, so frontend handlers keep working unchanged.
cc
The build script unconditionally rewrote capabilities/auto-updater.json
on every run, which bumped the file's mtime. tauri-build emits
cargo:rerun-if-changed=capabilities so any touch in that directory
re-runs this script, and 'tauri dev' also watches that folder for
source reloads. The two watchers fed each other: compile → rewrite
(same bytes, fresh mtime) → cargo marks build.rs dirty → recompile
→ tauri dev sees mtime change → request another build → repeat.
Now we read the existing file first and only write when the content
actually differs. mtime stays stable when the generated text is
identical across runs, so neither cargo nor tauri dev observe a
change and the loop terminates.
cc
Previously the quality downgrade only ran when the requested quality
was UltraHiRes and only tried a single step down to HiRes. Since Qobuz
returns up to 96 kHz content for both UltraHiRes and HiRes tiers, that
single step was a no-op for DACs that top out at 48 kHz (e.g. Vanatoo
Transparent Zero, many entry-level USB DACs): playback still failed
with the misleading 'Device not found' error.
This change iterates down the quality ladder after checking the
actual stream sample rate, continuing to Lossless (44.1 kHz) when
HiRes still produces an unsupported rate. Applies to both the current
track (v2_play_track_with_options) and prefetched tracks
(spawn_v2_prefetch).
Lossy MP3 is never selected automatically — if even Lossless is
unsupported, the stream continues at the original rate and Fix 2
(software resample fallback) takes over.
cc
Makes tauri-plugin-updater and tauri-plugin-process optional
dependencies enabled by a default-on `updater` Cargo feature.
Sandboxed packaging targets (Flatpak, Snap) will build with
`--no-default-features` to exclude the plugins entirely,
satisfying store policies that prohibit competing auto-updaters.
Changes:
- Cargo.toml: add `updater` feature; plugins are now optional deps
- lib.rs: gate plugin registration on the feature
- updates/mod.rs: is_auto_update_eligible() returns false when
the feature is off (catch-all for unusual build configs)
- capabilities/default.json: remove updater/process permissions
- build.rs: emit capabilities/auto-updater.json only when the
feature is enabled; remove it otherwise. The file is gitignored
since it is fully build-driven.
cc
Replaces PLACEHOLDER_PUBLIC_KEY with the real minisign public key
generated via 'tauri signer generate'. The corresponding private key
and password are stored locally; they must be added to GitHub Actions
secrets (TAURI_SIGNING_PRIVATE_KEY and TAURI_SIGNING_PRIVATE_KEY_PASSWORD)
before the first signed release.
Minisign public keys are intended to be public and embedded in the
binary; the updater uses this key to verify the signature of the
downloaded update bundle.
cc
Adds tauri-plugin-updater and tauri-plugin-process crates, the updater
config block in tauri.conf.json (with PLACEHOLDER_PUBLIC_KEY to be
replaced before release), capabilities for updater/process, and the
v2_is_auto_update_eligible command that returns true on macOS and
Linux AppImage installs only.
Cherry-picked from #305 by @afonsojramos.
cc
Daemon side: registers _qbz._tcp.local mDNS service on startup
so desktop apps can discover it on the LAN. Uses hostname as
service name (configurable via qbzd.toml).
Desktop side: new qbzd_discovery.rs module with Tauri commands:
- v2_qbzd_start_discovery: browse _qbz._tcp services
- v2_qbzd_stop_discovery: stop browsing
- v2_qbzd_get_devices: list discovered daemons
Uses same mdns-sd crate already used for AirPlay discovery.
cc