Aligns with Qobuz's own Library rename in their mobile and desktop
beta apps. Command names, store names, i18n keys, file names, and
internal route identifiers are all unchanged — this is strictly a
user-visible text + icon rewrite.
- Sidebar / TitleBar nav Favorites entry uses /static/music-library.svg
instead of the lucide Heart
- Artist and label favorite buttons use /static/user-add.svg (albums
and tracks keep the Heart affordance)
- 5 locale files: all values containing "Favorites" / "Favoritos" /
"Favoriten" / "Favoris" / "Favoritos" rewritten to "Library" and
locale-appropriate equivalents (Biblioteca / Bibliothek /
Bibliothèque / biblioteca)
- "Add to Favorites" / "Remove from Favorites" buttons and toasts
renamed to "Add to Library" / "Remove from Library"
- Hardcoded title= tooltip strings on track Heart buttons replaced
with $t() calls
cc
Zero visual change. The Mixtape/Collection detail view will import the
same component. Adds qobuz_streaming as a new value alongside the four
existing LocalLibrary values; AlbumCard's own prop type is unchanged.
cc
Disambiguates Qobuz's auto-generated "Your Mixes" section from the
upcoming user-owned Mixtapes feature (spec
qbz-nix-docs/specs/2026-04-18-mixtapes-collections-design.md).
- Renamed HomeSectionId type union entry from 'yourMixes' to 'qobuzMixes'
- Updated DEFAULT_SECTIONS in homeSettingsStore.ts
- Renamed translation keys in all 5 locale files (en, es, de, fr, pt)
- Updated section labels to "Qobuz Mixes", "Mixes de Qobuz", etc.
- Updated all component references in HomeView, DynamicSuggestView, FavQView,
ForYouTab, TopQView, and WeeklySuggestView
cc
Add two persisted prefs to the lyrics sidebar display controls:
- activeColor: hex color override for the karaoke/active-line accent.
Empty string means follow the theme's --accent-primary. Rendered via
a new --lyrics-active-color CSS custom property set inline on the
LyricsLines container; all four references to --accent-primary in
the active-line gradient and glow shadows now fall back through it.
Popover row shows a native color input plus a 'Theme' clear button
that appears only when a custom color is set.
- uppercase: render all lines as uppercase in the sidebar. Applied via
text-transform on .lyrics-lines.compact.uppercase. Switch toggle in
the popover.
Bump L and XL sidebar sizes by the requested amount:
- L: 17/20 -> 18/21
- XL: 20/24 -> 22/26
(small and medium are unchanged.)
Both new prefs persist per-user in the same localStorage payload via
the existing sanitize/reload flow, and the hex validator rejects
malformed values on read.
cc
A Copy icon button to the left of the Display settings trigger writes
the current track's lyrics (joined with newlines) to the clipboard via
the Tauri clipboard-manager plugin. The icon briefly swaps to a Check
for 1.5s on success, and a success/error toast is shown. The button is
disabled while lyrics are loading, when an error is displayed, or when
no lyrics are available.
cc
Swap the native <select> in LyricsControlsPopover for the shared Dropdown
component so the font picker matches the app's settings style. Widen the
popover to fit the Dropdown trigger and shrink its chrome to keep the
overall scale compact.
Add an XL font size (20/24 px) alongside S/M/L, extending the
LyricsFontSize type, the sanitize validator, the size class in
LyricsLines, and the i18n entry across all five locales.
cc
Pin the API-usage + trademark disclaimer to the bottom of the initializing
state of LoginView so the legal notice is visible during startup, not only
on the login screen proper. Update legal.trademarkNotice in all five locales
to mention the MIT license explicitly.
cc
Matches Qobuz's own mobile app posture. The previous 3-day window was
both user-hostile (punishes anyone on a flaky network) and a compliance
red flag (shorter windows read as 'we're hiding something', longer
windows read as 'we're lenient for a reason'). 30 days is the exact
parity answer.
The useful protection against bulk-download-and-ghost attacks comes
from the CMAF-at-rest offline cache (v2 format), not from aggressive
revalidation. This grace window is a belt over the suspenders.
Changes:
- GRACE_PERIOD_SECS: 3 → 30 days in config/subscription_state.rs, with
a comment explaining the reasoning for future readers.
- Log message in session_lifecycle no longer says "3 days" (it quoted
a constant, now says "grace window").
- Disclaimer string in all 5 locales (en/es/de/fr/pt) updated from
"3 days" to "30 days" so the Offline Playback settings screen
reflects the actual behavior.
No behavior changes for users with an active subscription. Users who
were within the old 3-day window don't lose cache — the window
extended, not contracted.
cc
Settings menu > Report Issue now opens a modal instead of jumping
straight to the GitHub new-issue page. The modal explains that
sharing the (manually masked) logs speeds up triage and offers two
actions:
- Go to logs → navigates to Settings > Developer Mode and
auto-opens the Logs modal so the user can review and upload.
- Create issue report → opens the GitHub issues page.
A persistent "Do not show this again" checkbox stores the preference
in localStorage (qbz-hide-report-issue-modal). When the flag is set,
the menu item skips the modal and goes straight to GitHub, matching
the previous behaviour.
Wiring: new settingsIntentStore lets UserCard cue SettingsView to
open at the `developer` section with showLogsModal=true, consumed and
cleared on mount so a plain Settings navigation afterwards uses the
normal default section.
i18n: new reportIssue block added to all 5 locales (en/es/de/fr/pt).
cc
Settings > Developer Mode > View Logs now has a toggle (default ON,
persisted to localStorage as qbz-paste-bundle-diagnostics) that
prepends the full System Diagnostics snapshot plus a 10-second cast
scan to the terminal paste upload, so bug reporters share one link
instead of two. Console-tab uploads are unaffected.
- New service module diagnosticsSnapshot.ts centralises the snapshot
payload (runtime + system + playback + qconnect + optional cast
scan) so the panel and the paste flow don't drift apart.
- LogsModal shows "Collecting diagnostics..." while the scan is
running, then "Uploading..." once it hands off to the paste server.
Also revert the auto-run cast scan on the diagnostics panel: bundling
into the paste covers the "one-shot bug report" use case, and the
panel opens instantly again.
cc
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
Extend the System Diagnostics panel with three new sections aimed at
what debug tickets usually ask for, and include them in the clipboard
export:
- Playback: is-playing, volume, position/duration, current track
(title, artist, album, source, local flag, quality/format/bit-depth/
sample-rate). No track IDs or UUIDs — defensive regex redacts any
hex/UUID-shaped strings before export so paste sites and CodeQL
don't flag them as secrets.
- Qobuz Connect: running/transport_connected, role (controller /
local-renderer / observer / none), active renderer friendly
name/brand/model (no UUIDs or IPs), visible renderer count, last
error (id-redacted).
- Cast Discovery: on-demand 10-second multicast scan for Chromecast
and DLNA; reports per-protocol count and the friendly names that
were discovered. No hosts, no ports, no UUIDs.
Playback rows stay live via a player-store subscription while the
panel is open.
cc
Add an opt-in setting under Appearance that lets the OS window title
reflect the currently playing track. Template supports {artist},
{track}, {album} placeholders; defaults to "{artist} — {track}".
Falls back to "QBZ" when disabled or with no track.
Persisted to localStorage (pure UX preference — no backend state).
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
Four accessibility-focused themes with WCAG AAA contrast ratios:
- WCAG Light/Dark: 7:1+ contrast for all text levels
- High Contrast: pure white on pure black, yellow accent (21:1)
- Colorblind: deuteranopia-safe palette (Wong colors), no red/green
Addresses discussion #313 (user with severe vision impairment).
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
1. Press ribbon restyled to a solid gold gradient (#f5c042 → #d49511)
with dark (#1f1407) uppercase text and a subtle white text-shadow.
Drops the red border-left. Qobuzissime / Album of the Week keep
their dark-background treatment with coloured left accent.
2. AwardView now loads /award/explore on mount (first 30 entries,
current award filtered out) and renders them as a horizontal
'Other awards' carousel below the album grid — same layout as
LabelView's 'More labels'. Each card shows the award laurel
(or magazine cover when present) + name + magazine name; clicking
routes into that award's AwardView.
i18n key award.otherAwards added for all five locales.
cc
Previously disabled={!onAwardClick || !award.id} grayed out the award
card whenever /album/get returned an award entry without an id. User
reported the award was 'no longer clickable' on albums opened from the
artist page. Now the button always accepts clicks and the handler
shows a toast when no id is available instead of silently doing
nothing — keeps the affordance honest without sending 'null' to
/award/page.
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
Adds the frontend for award detail pages. The hero header mirrors
LabelView's proportions 1:1 — 180px circular avatar left, inline
subtitle/name/magazine stack right, same 32px heading, same 12px
uppercase kicker, same 24px gap, same 40px bottom margin, same back
button treatment. Outer container uses the same paddings and scroll
behaviour as LabelView/AlbumDetailView.
Body pulls /award/page and builds one HorizontalScrollRow per
container (releases, new-releases, classics…). Container titles
localize via award.section.<id> with fallback to 'Acclaimed Releases'.
Wiring:
- 'award' added to ViewType and session restore map
- handleAwardClick in +page.svelte navigates to the view
- AlbumDetailView sidebar now passes onAwardClick so Phase 2 award
cards become interactive
- Frontend AlbumAward.id is optional to match tolerant backend struct;
sidebar disables the award button when id is absent
- i18n keys added in all five locales
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
Ports the Qobuz mobile 'Radar de Novedades' / Release Watch screen 1:1
(see rr0/i.java): a standalone view with three lazy-loaded tabs —
Artists, Labels, Awards — each hitting /favorite/getNewReleases with
the matching type parameter (limit 150 per tab, matching the mobile
cap).
Home's Release Watch rail (which still shows the Artists feed by
default, matching the mobile's default tab) now has a 'See all' link
that routes to the new view.
Adds view 'discover-release-watch' to the navigation type, wires the
route in +page.svelte, and adds tab/empty-state i18n keys in all five
locales.
cc
Adds a small top-left ribbon on AlbumCard that surfaces Qobuz's two
locale-stable editorial awards: 'Qobuzissime' (award id 88, purple) and
'Álbum de la semana Qobuz' (award id 151, yellow). The ribbon is
z-index 4, above the hover action-overlay (z-index 2), so it stays
visible when the user hovers the card.
HomeView maps every /discover/index rail via discoverAlbumToCardData
through a new pickAlbumRibbon(awards) helper that matches by stable
award id rather than translated name. Essential Discography and
Forgotten Favorites (which render DiscoverAlbum raw) call
pickAlbumRibbon inline.
i18n keys added to all five locales under 'ribbon.*'.
cc
Swaps the top-right heart-in-circle on 'More labels' cards for a
pill-shaped Follow/Following button underneath the label name, matching
the Qobuz mobile app's affordance. Uses UserPlus icon + short label
('Sigue' / 'Follow' / etc.) when not followed, Check icon + 'Siguiendo'
when followed. Adds 'label.followShort' and 'label.followingShort'
i18n keys for all five locales.
cc
Introduces 'labels' as a first-class favorites tab alongside
tracks/albums/artists/playlists. Adds the navigation view
'favorites-labels', plumbs it through sidebar, titlebar dropdown,
favorites preferences modal and navigation store, and renders a
circular grid of followed labels with search and empty states.
Clicking a card navigates to the existing LabelView detail page.
Spanish copy uses 'Sellos' and Portuguese 'Selos' to match
Qobuz mobile terminology.
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
HomeSettingsModal renders the section label with $t(`home.${section.id}`),
so the home.releaseWatch key was missing and the settings row showed
the raw key. Added it to all 5 locales (Spanish 'Radar de Novedades'
per the iOS screenshot; other languages match Qobuz's own strings
from the Android resources).
In ForYouTab, the section is now placed immediately after Your Mixes
instead of before Essentials — personal-feed sections (DailyQ / WeeklyQ
/ FavQ / TopQ + followed-artist releases) now sit together at the top.
HomeView section title switched to $t('home.releaseWatch') to match
the naming convention other sections already use.
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
When the now-playing bar is narrower than 1100px (reported overlap at
half a 1866px display), the right-section now collapses into two
elements:
- An overflow button (…) that opens a popup menu containing Cast,
Qobuz Connect, Lyrics, Mini-player, Full-screen, and Normalization.
- A mute/volume button that opens a vertical XP-style slider popup
upward; the horizontal inline slider is only rendered at wide
widths.
Detection uses a ResizeObserver on the bar element so the breakpoint
reacts to the actual space the bar receives (sidebar state, window
chrome, etc.) rather than a viewport media query. In the same narrow
mode QualityBadge and QconnectBadge switch to their compact variants
so the badges remain visible without the controls overlapping.
Queue button and offline/degraded indicators stay visible in both
layouts. Volume lock (ALSA hw:) keeps the existing inline disabled
variant so the 100% indicator is always visible.
i18n key player.moreControls added for all 5 locales.
cc
QualityBadge now has three new visual states wired off the
bit_perfect_mode field the backend emits in PlaybackEvent:
- DirectHardware → subtle green check next to the tier label,
border tinted green. Reassurance that audiophile listeners are
getting the stream untouched.
- PluginFallback → purple down-arrow + new tooltip explaining the
DAC doesn't support the source rate so ALSA (plughw) is doing
software resampling. The tooltip's suggested fix is explicit
about the mitigation (pick lower-rate source or a broader DAC)
since this is a hardware-capability constraint, not a user
setting issue.
- Disabled / null → no change.
Plumbed through the existing playerStore subscription pattern so
the indicator reflects the stream the moment it opens. i18n keys
added for all 5 locales (en/es/de/fr/pt).
cc
Adds updates.autoUpdate.* keys (title, checking, downloading,
downloadingProgress, installing, restarting, failed) and
actions.downloadAndInstall / actions.downloadManually to all 5
locales (en, es, de, fr, pt).
cc
CastPicker: manual IP:port input for connecting to daemons when
mDNS/avahi is unavailable. Saves recent connections to localStorage
(max 5). One-click reconnect from saved list. Remove button on hover.
Layout: manual input field at top, saved daemons as 'Recent',
mDNS discovered daemons below with scanning indicator.
Login: binds to 0.0.0.0 with auto-detected LAN IP so the OAuth
callback URL works from any browser on the network. 5 min timeout
for remote copy-paste workflow.
i18n: recentDaemons, discoveredDaemons in all 5 locales.
cc
QBZ Daemon tab appears first in the device picker alongside
Chromecast and DLNA. Discovered daemons show name + host:port.
Clicking a daemon:
1. Pings to verify it's reachable
2. Connects via playbackTargetStore
3. Starts SSE event subscription
4. Closes picker
Connected daemon shows in the connected banner with disconnect.
Disconnecting returns to local playback mode.
i18n: noQbzdFound + noQbzdFoundHint in all 5 locales.
cc
Accent-colored banner shows 'Controlling: {device_name}' with a
disconnect button when the app is targeting a remote qbzd daemon.
Hidden in local mode.
i18n: player.controllingRemote in all 5 locales.
cc
WebView OAuth: cancel button appears during loading, captcha hint
shows after 25s or when user closes WebView. Hint suggests trying
system browser instead with one-click action.
System browser OAuth: cancel link appears next to loading text,
immediately aborts the wait.
i18n: new keys captchaHint and trySystemBrowser in all 5 locales.
cc
Off by default. Toggle in Audio settings controls whether QBZ
silently downgrades quality when the requested tier is unavailable.
i18n keys added to all 5 locales.
cc
Secondary link-style option below the main OAuth button. Opens the
user's default browser (Firefox, Chrome, etc.) instead of the Tauri
WebView. Useful for password managers and saved sessions.
i18n keys added to all 5 locales.
cc