Core22 ships SQLite 3.37 but rusqlite needs 3.38+ for
sqlite3_error_offset. The RUSTFLAGS -L flags cause the linker
to find the system libsqlite3.a before the bundled one.
Both were creating stacking contexts that trapped the sidebar playlist
dropdown behind them. Removing z-index entirely avoids creating stacking
contexts — sticky positioning handles scroll overlap natively, and the
dropdown-menu child manages its own z-index.
The sticky .favorites-nav header had z-index: 4 which created a stacking
context that trapped the sidebar playlist dropdown menu behind it. Lower
to z-index: 1, matching the pattern used in ArtistDetailView and
LocalLibraryView.
Wrap title + See All in a flex column group so the link sits directly
under the section heading, left-aligned. Remove margin-left: auto that
was pushing it to the right edge.
Remove the pipe divider that looked bad. Replace ChevronRight with
ArrowRight for a cleaner See All > appearance. Revert HorizontalScrollRow
gap change that was only needed for the pipe.
- Add 3 new Rust endpoint constants and command mappings for
/discover/qobuzissims, /discover/albumOfTheWeek, /discover/pressAward
- Add 3 new navigation ViewType values and genre filter contexts
- Extend DiscoverBrowseView to handle the 3 new endpoint types
- Add See All links with chevron caret and pipe separator to all 6
discover sections in HomeView (newReleases, mostStreamed,
essentialDiscography, qobuzissimes, editorPicks, pressAwards)
- Wire up 3 new views in +page.svelte with full album action callbacks
- Rename Editor's Picks to Albums of the Week, Press Awards to Press
Accolades across all 4 locale files (en, es, de, fr)
- Fix rank badge: top-right position, Impact font, 35px size
- See All styling: inline-flex with ChevronRight icon, pipe divider,
15px gap before nav buttons, #666666 color matching nav buttons
- Replace custom genre <select> with GenreFilterButton component, each
discover page gets independent persistence via separate contexts
- Rank badge in grid mode: 3x bigger (33px), positioned on right side
of album card, no # symbol
- See All link: pushed to right edge (margin-left: auto), color matches
nav buttons (#666666), hover transitions to text-primary
- Add proper page padding to DiscoverBrowseView (18px left, 8px right)
Add full-page browsing views for the three discover sections that were
previously only shown as limited carousels on the Home page.
Backend: add get_discover_albums Tauri command with endpoints for
newReleases, idealDiscography, and mostStreamed. Uses paginated API
calls with genre_ids filtering.
Frontend: new DiscoverBrowseView component with virtualized grid/list,
genre filter dropdown, client-side search, and infinite scroll.
VirtualizedFavoritesAlbumGrid gains ranking support (showRanking),
infinite scroll (onLoadMore), and loading indicator.
HomeView sections now show "See All" links that navigate to the
dedicated browse pages. i18n keys added to all 4 locales.
Cached entries with plain-only lyrics were served forever, even after
the search-first strategy can now find synced versions. Treat plain-only
cache entries as a miss so the provider chain runs again. If synced
lyrics are found, the cache is updated via upsert.
GET /api/get often returns results without synced lyrics even when they
exist in the database. Switch to search-first: /api/search returns
multiple candidates so pick_best_match can select one with synced lyrics.
GET is now only a fallback when search yields nothing.
Also pass track_name + artist_name as separate search filters instead
of a combined q param for more precise results.
Search tabs (albums, tracks, artists) had their own overflow-y scroll
containers, causing double scrollbars and preventing the global
back-to-top button and sticky search bar from working.
- Remove height, min-height, overflow from .virtual-scroll-container
- Remove scrollbar pseudo-element CSS for virtual containers
- Feed virtual scroll calculations from parent .search-view scroll
- Remove per-container onscroll handlers (unused)
- ResizeObservers now only track width for grid column calculations
- Container height comes from parent scroll viewport
Replaced the per-view back-to-top in PlaylistDetailView with a
global one in +page.svelte. Uses capture-phase scroll listener
on .main-content to track scroll from any child view. Appears
after 800px scroll on any view. Resets on view change.
Content elements (track artwork z:1, play overlay z:2) were rendering
on top of the sticky jump-nav (also z:1). Bumped to z:10, still below
dropdown menus (z:50+) and context menus (z:100).
Floating button appears after scrolling 600px on playlists with
100+ tracks. Smooth scrolls back to top. Positioned bottom-right
with sticky positioning inside the scroll container.
QobuzClient has no &mut self methods — all API calls are read-only.
The Mutex serialized all concurrent API requests (favorites sync,
sidebar playlists, playlist loading) causing 6+ second wait times
at startup.
RwLock allows concurrent reads. All 94 call sites across 20 files
changed from .lock() to .read(). ListenBrainz/LastFm clients keep
their own Mutex (different state, different lock).
$state() creates deep reactive proxies for every object in the array.
With 2000 tracks, each background batch reassignment triggered proxy
creation for all existing + new objects, growing quadratically.
$state.raw() only tracks the reference — no deep proxying. Combined
with a fast path in displayTracks that skips the full copy/interleave
when there are no local tracks, search, or custom sort.
The black screen was caused by Svelte 5 creating deep reactive proxies
for 2000 objects (200 real + 1800 placeholders) synchronously, blocking
the main thread for ~27s.
Now: start with only loaded tracks (200), no placeholders. Background
batches append via single array reassignment per group (not N individual
mutations) and yield to the browser between groups. The displayTracks
derived no longer re-spreads 2000 objects on every mutation.
For large playlists (>500 tracks), the spinner now stays visible
until the first group of tracks (up to 200) has real data. This
eliminates the placeholder flash — users see spinner then real
tracks. Remaining batches load in the background.
Small playlists (≤500 tracks) now use get_playlist_track_ids for
metadata then get_playlist for full data in one shot — no placeholders,
no visible two-phase loading. Progressive batched loading only kicks
in for large playlists (>500 tracks) where it actually helps.
The Qobuz track/getList endpoint requires a POST request with
JSON body {"tracks_id": [...]} — not GET with query params or
form-encoded POST. This matches the Qobuz web player behavior.
The Qobuz track/getList endpoint may require POST instead of GET.
Try GET first, fall back to POST with form body if response is an error.
Also handle multiple response structures and log the actual response
keys for debugging.
Replace monolithic get_playlist call with two-phase approach:
- Phase 1: playlist/get?extra=track_ids returns metadata + all track
IDs in a single lightweight call (~50KB vs ~5MB)
- Phase 2: track/getList fetches full details in batches of 50,
4 concurrent groups, with tracks rendering progressively
Backend changes:
- Add TRACK_GET_LIST endpoint and PlaylistWithTrackIds model
- Add get_playlist_track_ids and get_tracks_batch API client methods
- Add corresponding Tauri commands
- Update remove_tracks_from_playlist to accept either
playlist_track_ids or regular track_ids (resolves internally)
Frontend changes:
- Rewrite loadPlaylist() for two-phase progressive loading
- Show playlist header + placeholder tracks after Phase 1 (~0.5s)
- Fill in full track details as batches arrive
- Add placeholder track row CSS (gray bars, no shimmer)
- Use Qobuz pre-made collage images from playlist metadata
- Update track removal to handle missing playlist_track_id
First page is fetched normally to get metadata and total track count.
Remaining pages are launched concurrently via futures::join_all and
reassembled in offset order. For a 2000-track playlist this means
3 concurrent requests instead of 3 sequential ones (~3x faster).
- Reset trackListScrollTop and scrollContainer.scrollTop to 0 when
playlistId changes. Fixes tracks not rendering until user scrolls
(old scroll offset exceeded new playlist's track count).
- Recalculate trackListViewHeight after content mounts via rAF.
- Set SUGGESTIONS_AUTO_THRESHOLD to 0: all playlists require manual
activation to rule out suggestions as unresponsiveness cause.
- Virtualize playlist track list using absolute positioning and viewport
culling. Only ~20 track rows are in the DOM at any time instead of all
2000. Eliminates main-thread blocking when navigating between playlists.
- Gate extractAdaptiveArtists and excludeTrackIds computation: playlists
>=2000 tracks never compute (Qobuz limit), >500 tracks defer until
manual activation. The computation itself is skipped, not just the UI.
- Move manual launch button from PlaylistSuggestions to PlaylistDetailView
so the component only mounts after activation (zero overhead when idle).
- Simplify PlaylistSuggestions by removing redundant manual launch logic.
Playlists at the 2000-track Qobuz limit no longer mount the
suggestions component at all (nothing can be added).
Playlists with >500 tracks require a manual button press to launch
the suggestion engine instead of auto-loading on mount. This avoids
heavy background work that causes UI freezes on large playlists.
Qobuz limits playlists to 2000 tracks. When matched tracks exceed
this limit, the importer now automatically splits into multiple
playlists named "Playlist (Part 1)", "Playlist (Part 2)", etc.
- models.rs: qobuz_playlist_id -> qobuz_playlist_ids Vec, add parts_created
- importer.rs: chunk matched tracks by 2000, create playlist per chunk
- Frontend: show parts count in summary, move all parts to folder
- i18n: add partsCreated key in en/es/de/fr
The Spotify API path was failing silently (no visible logs) and falling
back to embed which caps at ~100 tracks. Root cause: HTTP error
responses (401/429) were parsed as JSON, then failed with the
misleading "missing items" error.
Changes:
- Add error_for_status() to all HTTP calls (proxy, metadata, tracks)
so 401/429/5xx errors surface immediately with real status codes
- Retry token + fetch once before falling back to embed (handles
transient proxy failures and token expiry)
- Descriptive error messages include which request failed
The previous commit blocked the embed fallback when the API token was
obtained but the fetch failed, causing a hard error instead of
returning partial results. Restore the original fallback behavior
with logging to diagnose API failures.
The embed scraping fallback silently caps at ~100 tracks. When the API
token was obtained successfully but the fetch failed (transient error,
rate limit), the code fell through to embed without logging, returning
incomplete results.
Changes:
- Propagate API errors instead of silently falling through to embed
when the token was successfully obtained
- Fix next-page detection: use is_null() instead of as_str() which
could miss non-string truthy values
- Use response.total field as backup pagination signal
- Add safety checks for empty pages to prevent infinite loops
- Validate fetched count against expected total from metadata
- Add logging at every decision point for diagnosability
Replace sequential track matching with buffer_unordered(8) for 8
concurrent Qobuz API calls, reducing import time ~8x. Add real-time
progress events (import:progress, import:phase) emitted from Rust
backend and consumed by frontend via Tauri event listeners.
Frontend shows progress bar, matching status text, and current track
name during import. Individual search failures are now logged and
skipped instead of aborting the entire import.
The Qobuz API get_playlist call was hardcoded with limit=500, silently
dropping tracks beyond that. Now paginates in 500-track pages using
the TracksContainer.total field until all tracks are loaded.
Remove max_total_tracks (was 100) cap and all truncation/early-break
logic from smart playlist generation. Playlists now include all
tracks found across related artists. Increase per-artist sampling
from 10 to 25 for richer results.
Replace 5 hardcoded validation messages ('Please enter a playlist
name', 'Please select a playlist') with i18n keys playlist.enterName
and playlist.selectPlaylist across all 4 locales.
Migrate PlaylistDuplicateConfirmModal from Svelte 4 (export let,
createEventDispatcher, on:click) to Svelte 5 ($props, callback
props, onclick). Replace hardcoded English strings with i18n keys
under playlist.duplicates for all 4 locales (en, es, de, fr).
Distinguish not-found (Ok(None)) from network error (Err) in
fetch_lrclib. On network error, retry the full LRCLIB flow once
before falling back to lyrics.ovh. Not-found skips directly to
the fallback with no retry.
Network errors in LRCLIB were propagated as hard Err via ? operator,
preventing the lyrics.ovh fallback from ever being reached. Changed
provider functions to return Option instead of Result so errors are
treated as soft failures. Added 10s HTTP timeout. Fixed internal
LRCLIB fallback (GET failure now falls through to search). Replaced
hardcoded UI strings with i18n keys.
Runtime decoration toggling doesn't work reliably in either direction
on Linux/Wayland. Always restart the app so the window is recreated
with the correct decoration state.
Revert visible:false approach which broke WM buttons on Wayland.
Instead, set html/body background to #0f0f0f (dark theme default)
so the brief frame before webview renders shows a dark surface
instead of a white flash.
Create the window with visible:false so the empty frame + title bar
don't flash while the webview loads. The frontend calls
show_main_window from onMount (after first DOM paint) to reveal the
window with content already rendered.
Disabling system titlebar works at runtime (decorations:true -> false
is fine on Linux/Wayland). Only enabling requires a restart because
the window must be born with decorations:true for WM buttons to work.
When disabling: remove OS decorations via setDecorations(false) and
restore the custom title bar immediately via notifyListeners().
Since the window must be created with the correct decoration state
(runtime toggling doesn't work on Linux/Wayland), persist the setting
to the Rust backend and restart the app immediately.
Add restart_app Tauri command using AppHandle::restart(). Update i18n
descriptions to inform the user the app will restart.
On Linux/Wayland, windows created with decorations:false cannot
reliably switch to decorations:true at runtime - the WM buttons
render but don't function until a maximize/restore cycle.
Fix by removing the declarative window from tauri.conf.json and
creating it programmatically in the setup hook with the correct
decorations value from the start. The WM sees the right state when
the window is first mapped, so buttons work immediately.
Also fixes the empty frame flash from visible:false + early show().
When the frontend initializes and finds useSystemTitleBar=true in
localStorage, sync it to the Rust SQLite store. This handles migration
from the localStorage-only era and ensures the Rust backend has the
correct value for next startup.
Move decoration setting to Rust side so the WM sees correct state when
the window is first mapped. Window starts hidden (visible: false in
tauri.conf.json), Rust reads the persisted setting from SQLite, applies
decorations, then shows the window.
Frontend now persists the setting via Tauri invoke (in addition to
localStorage) so Rust can read it at startup. Inline script in app.html
prevents flash of custom title bar before Svelte mounts.
Add option to use OS native window decorations instead of the custom
QBZ title bar. Uses Tauri setDecorations() API to toggle at runtime.
Disables "Hide title bar" toggle when system bar is active.
The toggle was removed from the template in a previous redesign but all
backing logic (state, handler, localStorage key, i18n) remained intact.
Re-add the setting-row with Toggle before the disconnect/clear actions.
- Disable Plex in isPlexLibraryEnabled() when offline reason is
no_network (manual_override keeps Plex active for local servers)
- Add background hydration after album list load: fetch quality for
tracks with NULL sampling_rate_hz/bit_depth, persist to cache, then
refresh album list so quality badges update without opening each album
- Add likely_single_file_album detection to LocalAlbum/PlexCachedAlbum
interfaces and mapPlexAlbum mapping
- Show info notice in album detail view for single-file albums
- Add i18n keys for single-file album notice (en + es)
- Save existing quality data before DELETE in plex_cache_save_tracks,
restore hydrated container/sampling_rate_hz/bit_depth after re-insert
so bulk sync no longer overwrites per-track quality from hydration
- Add plex_cache_get_tracks_needing_hydration command returning
rating_keys where sampling_rate_hz or bit_depth is NULL (limit param)
- Add likely_single_file_album field to PlexCachedAlbum struct, set true
when track_count == 1 and total_duration_secs > 600
Was incorrectly removed during merge conflict resolution of
feature/ui-fixes. Without it, blacklisted tracks lack visual
feedback (opacity, Ban icon, cursor) in album detail view.
HomeView ML sections now use a single IPC call that returns fully
resolved card data from Rust. The 3-tier cache (reco meta → API cache
→ Qobuz API) handles all resolution server-side, eliminating the
per-item fetch loop on the frontend.
Genre filtering and section visibility still applied client-side.
Existing fetchAlbums/fetchTracks/fetchArtists kept for other uses.
Add persistent metadata cache tables (reco_album_meta, reco_track_meta,
reco_artist_meta) with no TTL for stable played-item metadata.
Implement reco_get_home_resolved command that returns fully-resolved
card data in a single IPC call using 3-tier resolution:
1. Reco meta cache (persistent, sub-ms)
2. API cache (24h TTL, sub-ms)
3. Qobuz API (parallel HTTP fallback)
Also refactors reco_get_home_ml to use shared get_home_seeds_internal
to avoid seed-gathering duplication.
These functions are no longer called from +page.svelte after switching
to convertPageArtist and appendPageReleases. The function definitions
remain in qobuzAdapters.ts as categorizeAlbum (used by FavoritesView)
depends on the same helper functions.
Replace monolithic loadMoreArtistAlbums (get_artist_albums) with
per-category loadMoreArtistReleases using get_releases_grid endpoint.
Each section (albums, EPs, live, others) gets its own "load more"
button driven by has_more flags from the API response.
Replace get_artist_detail with get_artist_page in handleArtistClick()
and fetchAlbumArtistAlbums(). Pass pre-loaded top_tracks and
similar_artists as props to ArtistDetailView, eliminating 2 extra
API calls (search_tracks text search + get_similar_artists).
Add PageArtist* interfaces matching the Rust types. Add
convertPageArtist() adapter that maps API release_type categories
instead of heuristic regex, filters foreign discs, and builds
the existing ArtistDetail UI model from a single API response.
Add PageArtistResponse and ReleasesGridResponse types with all
supporting structs for the /artist/page and /artist/getReleasesGrid
Qobuz API endpoints. Add client methods and Tauri commands.
Sidebar now uses stale-while-revalidate for playlists, settings,
and local track counts. On mount, cached data is shown immediately
while fresh data loads in the background.
- Create sidebarDataCache.ts with sessionStorage persistence
- Sidebar restores from cache on mount (fresh/stale), loads in background
- Cache cleared only on real playlist changes (create/delete/track changes)
- Sort order, play counts, and UI state do NOT invalidate the cache
- Coordinated loadSidebarData() replaces three independent load calls
homeDataCache now has three states:
- fresh (<5 min): use cache, no revalidation
- stale (5-60 min): show cache immediately, revalidate in background
- empty (>60 min / genre mismatch / no data): full skeleton load
Changes:
- Replace isHomeCacheValid() with getHomeCacheStatus() -> fresh/stale/empty
- Add sessionStorage persistence for cross-navigation survival
- Stop destroying cache data on staleness (was nullifying at 5 min)
- HomeView onMount uses three-state logic
- loadHome() accepts showSkeletons option for silent revalidation
- Extract restoreFromCache() helper for DRY cache restoration
The discover/index endpoint already returns all editorial content
(new_releases, press_awards, most_streamed, qobuzissims, album_of_the_week)
in a single response. Previously we made 5 separate get_featured_albums
calls plus 1 discover call.
- Add missing containers to Rust DiscoverContainers struct
- Add missing containers to TypeScript DiscoverContainers interface
- Replace fetchFeaturedAlbums x5 + fetchDiscoverData with single
fetchAllDiscoverData that extracts all sections from one API call
- Add discoverAlbumToCardData() converter for DiscoverAlbum -> AlbumCardData
- Genre filtering now handled by discover/index genre_ids parameter
- Add playlistsById Map for O(1) lookups in getLocalContentStatus
(was O(n) playlists.find() per call, called inside filter loops)
- Consolidate 3 sequential .filter() passes (search, visibility,
folder) into a single pass over the array
- Fix Map type for displayPlaylistIndexMap (number, not string)
Replace displayPlaylists.findIndex() called inside {#each} (O(n)
per item = O(n^2) total) with a pre-computed Map<id, index> that
provides O(1) lookups. Affects both grid and list view reorder
controls.
Bug report now asks for installation method, display server, GPU,
and includes step-by-step instructions for sharing logs via the
built-in log uploader. Crash recovery hint for startup crashes.
Feedback template also adds installation method field.
GPU rendering is opt-in via a toggle with restart-required toast.
Recovery from crash: QBZ_HARDWARE_ACCEL=0 qbz (env var always
overrides the stored setting). i18n keys added to all 4 locales.
Hardware acceleration is OFF by default for maximum AppImage
compatibility. Users can enable it in Settings > Appearance.
Env var QBZ_HARDWARE_ACCEL=1|0 always overrides the DB value,
providing crash recovery from the terminal.