Commit Graph
1202 Commits
Author SHA1 Message Date
vicrodh 2a870e619a chore(release): bump version to 1.2.7 + release notes
Bumped:
- src-tauri/Cargo.toml + Cargo.lock
- src-tauri/tauri.conf.json
- package.json + package-lock.json
- snapcraft.yaml
- flake.nix (qbzVersion, hashes cleared for nix users to regenerate)

Added:
- packaging/flatpak/com.blitzfc.qbz.metainfo.xml (1.2.7 entry)
- docs/release-1.2.7/TAG_DETAILS.md (release notes — 1.2.7 preamble
  on top of the full 1.2.6 + 1.2.5 content)

cc
2026-04-16 12:03:18 -06:00
vicrodh d5e3fff45c feat(diagnostics): add OS / distro / install method / deps section
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
2026-04-16 11:14:26 -06:00
vicrodh fa7758f5bb fix(window,login): grant set-title capability + shrink login logo 20%
- 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
2026-04-16 09:48:43 -06:00
vicrodh 570532ce90 fix(branding): refresh tray icons + snap icon to match new vinyl Q
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
2026-04-15 21:49:25 -06:00
vicrodh f6377d5df9 chore(release): bump version to 1.2.6 + release notes
Bumped:
- src-tauri/Cargo.toml + Cargo.lock
- src-tauri/tauri.conf.json
- package.json + package-lock.json
- snapcraft.yaml
- flake.nix (qbzVersion, hashes cleared for nix users to regenerate)

Added:
- packaging/flatpak/com.blitzfc.qbz.metainfo.xml (1.2.6 entry)
- docs/release-1.2.6/TAG_DETAILS.md (release notes)

cc
2026-04-15 21:36:11 -06:00
vicrodh 33d65681c4 fix(queue): restore infinite play (V2 reimplementation)
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
2026-04-15 21:20:04 -06:00
vicrodh 1fe7d44311 fix(branding): resize icon to fill canvas + add blue edge rim
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
2026-04-15 20:22:00 -06:00
vicrodh a867d77528 fix(build): gate macOS-unreachable imports to cfg(target_os = "linux")
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
2026-04-15 15:46:06 -06:00
vicrodh 6049240640 chore(release): bump version to 1.2.5 "Accolade Watch"
* 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
2026-04-15 13:16:36 -06:00
vicrodh 57e63e3ef5 feat(appearance): phase 2 of match-system-window-chrome (transparent)
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
2026-04-15 11:31:11 -06:00
vicrodh 8502c2fdb3 feat(appearance): match system window chrome (phase 1) + ribbon cleanup
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
2026-04-15 11:17:05 -06:00
vicrodh 6a5cfc7235 feat(titlebar): auto-detect KDE Plasma/Klassy theme for window controls
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
2026-04-15 09:43:00 -06:00
vicrodh 1cb8d2c305 feat(branding): refresh app icon, tray theming, and login branding
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
2026-04-15 03:55:28 -06:00
vicrodh c63c3ab160 chore(audio-watch): drop deprecated DeviceTrait::name fallback
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
2026-04-14 22:59:22 -06:00
vicrodh 8871262377 chore(deps): bump rand to silence dependabot advisories
- 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
2026-04-14 22:47:42 -06:00
vicrodh f7e51b980e fix(audio-watch): stop pre-flight device toast — stored name format never matched cpal enum
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
2026-04-14 21:12:03 -06:00
vicrodh b62a4b01f5 feat(audio): notify user when selected output device goes missing (issue #307)
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
2026-04-14 17:58:59 -06:00
vicrodh c4c4bdf244 fix(album-awards): accept LegacyAwardDto's awardId/awardedAt field names
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
2026-04-14 16:02:08 -06:00
vicrodh 854eaaa0a4 feat(award): resolve award id by name via /award/explore when /album/get omits it
/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
2026-04-14 15:06:15 -06:00
vicrodh 037e18423a fix(award): normalize award.id as string end-to-end so /award/* calls stop sending 'null'
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
2026-04-14 14:39:20 -06:00
vicrodh a273174716 feat(award): use /award/getAlbums for the grid + follow award as entity
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
2026-04-14 13:02:37 -06:00
vicrodh ee2f413ec1 fix(album): use dedicated tolerant AlbumAward struct instead of mutating PageArtistAward
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
2026-04-14 12:30:18 -06:00
vicrodh fe3213ece5 feat(award): backend V2 commands for /award/page and /award/getAlbums
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
2026-04-14 12:12:01 -06:00
vicrodh f0b4499dab fix(album): accept awarded_at as integer or string on PageArtistAward
/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
2026-04-14 11:28:19 -06:00
vicrodh 7bf9972846 feat(album-view): right sidebar with label and full awards stack
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
2026-04-14 11:11:24 -06:00
vicrodh b738c27507 fix(home): plumb album awards through backend + invalidate stale home cache
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
2026-04-14 10:32:28 -06:00
vicrodh a9a9ba5050 feat(labels): follow / unfollow labels (mirrors Follow Artist)
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
2026-04-14 09:00:11 -06:00
vicrodh ce375008ab fix(api): Release Watch lives at /favorite/getNewReleases?type=artists
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
2026-04-14 07:47:42 -06:00
vicrodh b0e724d428 fix(api): call /albums/releaseWatch as REST, not signed RPC
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
2026-04-13 23:23:07 -06:00
vicrodh 60e0861e1d refactor(discover): read Release Watch from /discover/index response
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
2026-04-13 22:49:34 -06:00
vicrodh ea216adf0f feat(discover): add Release Watch section to Home and For You
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
2026-04-13 22:33:59 -06:00
vicrodh 808b9db0db fix(offline): real offline mode — snapshot streaming, block network, diag logs (#279)
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
2026-04-13 22:13:16 -06:00
vicrodh 722696df34 fix(window): clamp persisted size to fit the largest available monitor
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
2026-04-13 21:51:40 -06:00
vicrodh 10bc0eb660 feat(tray): use ksni on Linux for working left-click toggle (#310)
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
2026-04-13 21:41:20 -06:00
vicrodh 014af6fbfb fix(build): stop infinite tauri dev rebuild loop on auto-updater.json
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
2026-04-13 20:27:44 -06:00
vicrodh d7c8119953 fix(audio): downgrade track quality when DAC rate unsupported (#288)
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
2026-04-13 19:12:32 -06:00
vicrodh 031da1792d feat(updater): gate auto-updater behind Cargo feature updater
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
2026-04-13 18:32:28 -06:00
vicrodh 1777760aec chore(updater): set real minisign public key
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
2026-04-13 15:16:00 -06:00
vicrodh 96cfb93956 feat(updater): add tauri-plugin-updater backend integration
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
2026-04-13 14:53:08 -06:00
vicrodh 2802527912 chore(deps): update jsdom 29.0.2, notify-rust 4.14.0
Also closed PR #298 (md-5 0.11) — breaking API change across 9
call sites for zero functionality gain.

cc
2026-04-09 08:38:19 -06:00
Victor RH b196a299dc Merge pull request #300 from vicrodh/dependabot/cargo/src-tauri/pre-release/tauri-plugin-dialog-2.7.0 2026-04-09 08:20:17 -06:00
Victor RH 1bc3eaf759 Merge pull request #299 from vicrodh/dependabot/cargo/src-tauri/pre-release/rodio-0.22.2 2026-04-09 08:20:13 -06:00
Victor RH 15c926ea6f Merge pull request #295 from vicrodh/dependabot/cargo/src-tauri/pre-release/tauri-plugin-deep-link-2.4.8 2026-04-09 08:20:02 -06:00
vicrodh 8fc6111782 feat(qbzd): mDNS discovery + registration for LAN pairing
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
2026-04-09 04:30:08 -06:00
dependabot[bot] e6bacb7507 chore(deps): bump tauri-plugin-dialog from 2.6.0 to 2.7.0 in /src-tauri
Bumps [tauri-plugin-dialog](https://github.com/tauri-apps/plugins-workspace) from 2.6.0 to 2.7.0.
- [Release notes](https://github.com/tauri-apps/plugins-workspace/releases)
- [Commits](https://github.com/tauri-apps/plugins-workspace/compare/log-v2.6.0...log-v2.7.0)

---
updated-dependencies:
- dependency-name: tauri-plugin-dialog
  dependency-version: 2.7.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-09 05:37:52 +00:00
dependabot[bot] 608f9508ef chore(deps): bump rodio from 0.22.1 to 0.22.2 in /src-tauri
Bumps [rodio](https://github.com/RustAudio/rodio) from 0.22.1 to 0.22.2.
- [Changelog](https://github.com/RustAudio/rodio/blob/master/CHANGELOG.md)
- [Commits](https://github.com/RustAudio/rodio/compare/v0.22.1...v0.22.2)

---
updated-dependencies:
- dependency-name: rodio
  dependency-version: 0.22.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-09 05:37:36 +00:00
dependabot[bot] b2527d829b chore(deps): bump tauri-plugin-deep-link in /src-tauri
Bumps [tauri-plugin-deep-link](https://github.com/tauri-apps/plugins-workspace) from 2.4.7 to 2.4.8.
- [Release notes](https://github.com/tauri-apps/plugins-workspace/releases)
- [Commits](https://github.com/tauri-apps/plugins-workspace/compare/deep-link-v2.4.7...deep-link-v2.4.8)

---
updated-dependencies:
- dependency-name: tauri-plugin-deep-link
  dependency-version: 2.4.8
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-09 05:36:58 +00:00
vicrodh 242b27bb1b refactor: clean up commands_v2/mod.rs
cc
2026-04-08 19:10:23 -06:00
vicrodh 77f12075da refactor: extract commands_v2/diagnostics.rs
cc
2026-04-08 18:07:54 -06:00
vicrodh 8fa11a4317 refactor: extract commands_v2/discovery.rs
cc
2026-04-08 18:05:50 -06:00