Add CMAF fallback to streaming probe failure, standard download,
and individual prefetch paths. Legacy nginx CDN path kept as
automatic fallback for each. Cast/DLNA/offline-cache paths marked
with TODO (require CoreBridge parameter plumbing).
cc
Prefetch was still using the legacy nginx CDN path, causing "1 byte
EOF" failures on tracks that only work via the new Akamai CDN. Now
tries CMAF full download first, falls back to legacy on failure.
cc
Replace full-download CMAF with streaming: fetch init segment, start
playback, then download+decrypt audio segments in background. Player
begins playing after ~500ms instead of waiting for full download.
cc
Download tracks via Qobuz Akamai CDN with encrypted CMAF segments.
Derives session key (HKDF), unwraps content key (AES-CBC), decrypts
frames (AES-CTR), reassembles into standard FLAC. Falls back to legacy
track/getFileUrl if CMAF fails.
- Add qbz-cmaf dependency to src-tauri
- Make ensure_cmaf_session public on QobuzClient
- Add ensure_cmaf_session and get_file_url passthroughs to CoreBridge
- Add download_audio_cmaf and try_cmaf_download helpers
- Wire CMAF as primary download path in v2_play_track with legacy fallback
cc
Auto-advance from a previous track could arrive after the user
switched to a new album/playlist, advancing the new queue's index
and skipping a track (observed: Sorceress track skipped).
Added queue epoch counter that increments on setQueue/clearQueue.
nextTrackGuarded() captures epoch before the invoke and discards
the result if the queue changed while in-flight. Applied to all
auto-advance paths (track-end, gapless transition) but not to
manual skip (user-initiated).
cc
download_audio() was sending bare GET requests without User-Agent,
while all other CDN request paths (probe, streaming) sent Mozilla/5.0.
CDNs commonly reject or truncate responses from requests without
User-Agent, explaining the "1 byte then EOF" failures on tracks
that work fine in the web player.
Also removed the 120s total timeout that contradicted the comment
saying "no total timeout" — Hi-Res+ files can be 200MB+ and need
more than 120s on slower connections.
cc
When skipping tracks quickly, the previous PCM handle may not be
fully released by the kernel before the new track tries to open
the same ALSA device (snd_pcm_open fails with EBUSY).
Previous behavior: single retry after 200ms PipeWire suspend, then
immediate failure. This didn't cover our own PCM race condition.
New behavior: 4 retries with progressive backoff (50/100/200/400ms)
for both hw and plughw paths. PipeWire suspend still attempted once
upfront. Total worst-case wait: 750ms (vs 200ms before), but the
early retries at 50-100ms cover the common fast-skip case.
cc
StreamFetcher used a persistent reqwest::Client for the entire app
session. When a CDN connection broke mid-transfer, the HTTP/2 pool
kept reusing the dead connection, causing all subsequent downloads to
fail after 1 byte ("end of file before message length reached").
Changes:
- StreamFetcher now creates a fresh client per download attempt,
matching the pattern used by download_audio() which never had
this issue
- Added retry logic with exponential backoff (3 retries, 1s/3s/5s)
— offline cache previously had zero retries for multi-hundred-MB
downloads
- Clean up partial .tmp files on failure before retrying
- Extracted try_download() for clean retry loop
Root cause: issue #268 / reported in #270 testing
cc
Remove dead alsa dependency from src-tauri/Cargo.toml (code lives in
qbz-audio since V2 migration) and bump qbz-audio to alsa 0.10.
Eliminates duplicate alsa versions in Cargo.lock.
Breaking changes in 0.10 (Format/ChmapPosition reorder) do not affect
QBZ — we use catch-all matches and never rely on PartialOrd.
Closes#260
cc
Artwork bumped from min(45vh, 360px) to min(52vh, 460px), matching
coverflow center cover. Padding reduced to 52px/88px consistent with
other panels. Responsive breakpoints updated accordingly.
cc
The side covers partially overlapping the center is correct coverflow
behavior — they just need to render BEHIND the center cover. Center
has z-index: 10, sides have 4/3. Restored original-style offsets
(200px/340px) with 45deg rotation for the classic coverflow look.
cc
Remove transform-style: preserve-3d and translateZ which caused center
cover to render as ghost/transparent. Remove duplicate perspective on
parent panel. Add dark backdrop to quality badge in all immersive views
for legibility on light backgrounds.
cc
Three changes to prevent overlap:
1. translateX increased from 280px to 400px for adjacent covers
2. rotateY reduced from 45deg to 30deg (less inward swing)
3. Added translateZ(-120px) to push side items behind the Z-plane
The 3D perspective projection with rotateY causes the near edge of
side covers to swing toward center. Reducing rotation and adding
Z-depth separation fixes the visual overlap.
cc
Center cover bumped from 360px to 460px. Side covers pushed out to
280px/460px offsets (from 180/300) to prevent overlapping the center.
Removed broken slideLeft/slideRight keyframe animations that
referenced unset --translate-x variable and conflicted with the CSS
transitions already on each item. Transitions alone handle all
position changes smoothly on click/skip.
cc
Bump artwork vh constraint from 60vh to 72vh — the previous value was
the bottleneck on laptop screens (461px on 768px viewport). Now yields
~553px. Reduce vertical padding from 164px to 140px total. Compact
track info margins below artwork.
cc
Change from center-center floating layout to stretch-fill approach.
Artwork section now gets fixed 42% width proportion and fills column
height. Artwork max-width overridden to 100% of column (no viewport
caps). Columns stretch to fill available height, eliminating dead
space above/below. Track info made more compact. Panel section
removes max-width cap for lyrics to breathe.
cc
Remove fixed px caps on artwork — now scales purely with viewport
(42vw/60vh). Switch lyrics font from condensed Oswald to Montserrat
for better legibility. Hide scrollbar in immersive lyrics (scrolling
still works). Add padding to artwork column for breathing room.
cc
Artwork scales with viewport (min of 500px/38vw/58vh) instead of fixed
380px cap. Lyrics use responsive clamp() sizing (up to 34px inactive,
42px active) with better line-height. Reduced padding/gaps throughout
split mode for less dead space. Tighter mask gradient on lyrics panel.
cc
The cargo eclass requires pre-generated offline crate sources which
the CI workflow never set up. Revert to the working approach: inherit
xdg only, RESTRICT network-sandbox, direct npm ci + cargo build.
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
Off by default. When disabled, if the requested quality is unavailable
the download/stream fails instead of silently downgrading. Persisted
to audio_settings DB. Applied to offline cache downloads.
cc
When a track is cached at lower quality than requested (e.g. CD quality
cached during a Qobuz outage, but user wants Hi-Res), the cache hit is
now detected via FLAC header inspection (sample_rate + bit_depth).
If quality is insufficient, QBZ attempts a fresh download at the
requested quality. If the download fails (network error), the cached
lower-quality version is played instead of failing entirely.
cc
Bootstrap no longer attempts email/password login — only OAuth token
restore. v2_manual_login and v2_auto_login now return immediately with
deprecation errors. Users with old saved credentials will see the
login screen instead of hitting Qobuz's deprecated endpoint.
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
Refactor: shared finalize_oauth_login() handles code exchange, CoreBridge
injection, session activation, and token persistence. Frontend-agnostic,
reusable by GUI, TUI, and headless clients.
New command: v2_start_system_browser_oauth spawns a local axum listener,
opens the user's default browser for Qobuz OAuth, and captures the
redirect code automatically.
cc
Qobuz no longer accepts basic auth. Remove email/password fields,
manual login handler, and "Remember me" checkbox from LoginView.
OAuth browser login is now the sole authentication method.
cc
setCurrentTrack during session restore was missing albumId and artistId,
causing Go to Album / Go to Artist clicks to silently fail until a new
track was played. Also restore isLocal, source, and parental_warning for
full parity with the normal playback path.
Closes#252
cc
handleBulkMakeOffline was calling v2_cache_favorite_track which only
saves metadata to the favorites cache. Replaced with
cacheTracksForOfflineBatch which triggers the real offline download.
cc
Three fixes:
- resolve_artist now prefers exact name matches over highest score,
fixing "The Warning" resolving to "Fates Warning" (score=100 but
wrong name vs score=97 exact match)
- resolve_area_country now returns ISO country code alongside name,
fixing flag display (was showing US flag for German artists)
- country_code updated in location metadata for correct flag rendering
cc
Add resolve_area_country() that walks the MB area hierarchy up to
the country level (e.g., Frankfurt -> Hessen -> Germany).
Use it in v2_musicbrainz_get_artist_metadata to get the real birth
country instead of MB's "country" field (which is where the artist
is active, not where they were born).
Hans Zimmer now shows "Frankfurt am Main, Germany" and clicking
the location shows German composers, not American ones.
cc
MB's country field is where the artist is active, not where they
were born. When begin_area exists (city-level), display only the
city name to avoid incorrect attribution (e.g., Hans Zimmer born
in Frankfurt showing "United States").
cc
Two bugs in location_discovery.rs:
- Display format was "Country, City" instead of "City, Country"
- Country was derived from area (where artist is active) instead
of country code (where artist was born/formed)
Hans Zimmer now shows "Frankfurt am Main, Germany" instead of
"United States, Frankfurt am Main"
Closes#235
cc
- Send track metadata to MPRIS on session restore so playerctl
shows title/artist/album immediately after app restart
- Persist playback context (album, playlist, etc.) to localStorage
on session save and restore it on startup
- Add buffer_progress to frontend PlaybackEvent type
Closes#240
cc
Show a subtle buffer progress bar in the seekbar when streaming
tracks (before they are fully cached). The bar disappears once
the track is fully buffered.
Backend: expose buffer_progress from BufferedMediaSource through
SharedState -> PlaybackEvent -> playerStore.
Frontend: render .seekbar-buffer bar behind the playback fill.
cc
F11 now toggles window fullscreen from anywhere in the app, not just
inside the immersive player. ESC exits window fullscreen when no UI
overlays are open.
Closes#202
cc