Extract the ~300-line constructor body into setupDocks(), setupScrobbler(),
setupGapless(), setupMpris(), connectBackendSignals(), connectLibrarySignals(),
connectContentSignals(), and connectToolbarSignals(). No behavioral changes;
all signal/slot connections and widget creation remain identical.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Extract parseTrackItem() static helper in TrackListModel to eliminate
~45 lines of duplicated JSON parsing between setTracks() and appendTracks()
- Extract notifyFavChanged() helper to deduplicate addFavId/removeFavId loops
- Add StackPage enum to MainContent replacing magic integers 0-5 with
named constants (PageWelcome, PageTracks, PageAlbumList, etc.)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Introduce paged loading with early prefetch for genre albums/playlists and playlist tracks, while preserving full-data behavior for deep shuffle and playlist play-all actions.
Improve album workflow with in-header favorite toggle, keep favorite state synced from backend, and normalize browse top-bar sizing while increasing audio output buffer headroom to reduce glitches.
Add /most-popular/get endpoint support (discovered via Burp):
- Returns mixed artists/albums/tracks ordered by popularity for a query
- New 'Top Results' tab shown first/default in the search panel
- Badge column: A (artist, blue), H (hi-res album, gold), A (album, gray), T (track, green)
- Full context menu support on all item types in the new tab
- Both most-popular and regular search fire on submit in parallel
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add right-click context menus to search panel tracks (play, queue,
favorites, playlist, go to album/artist, track info) and albums
(open, favorite, go to artist)
- Add "Track info..." dialog showing metadata (title, performer,
composer, album, quality, hi-res status) — available everywhere:
playlists, albums, favorites, search results
- Split artist page shuffle into "Shuffle" (popular tracks) and
"Shuffle All" (deep shuffle across all releases)
- Remove magnifying glass emoji from welcome text
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Lock sidebar width (setFixedWidth) so it doesn't jump between views
- Remove back/forward navigation buttons and all NavPage history code
- Uniform column layout on artist page: hide Artist column from both
Popular Tracks and release sections, set matching fixed column widths
so columns align vertically across all sections
- Deep shuffle: new Rust FFI endpoint fetches tracks from all albums
in parallel, combines them, and returns via EV_DEEP_SHUFFLE_OK
- Auto-paginate artist releases in Rust (loop until has_more=false)
so all releases load at once sorted newest-first
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
**Section toggles left-aligned**
- Replace QToolButton with flat QPushButton for all section headers;
QPushButton properly respects text-align: left in stylesheets
**Pagination via "Load More" button**
- QTreeWidget expands to fit all items so the scrollbar-based infinite
scroll never triggered; replaced with an explicit "Load more…" button
that appears when has_more is true and emits loadMoreRequested
**Favourite button reflects actual state**
- MainWindow preloads fav artist IDs on session restore (getFavArtists)
and caches them in m_favArtistIds
- ArtistView receives the full set via setFavArtistIds() and checks
it on every setArtist() call so the button starts gold if already faved
- Toggling updates the local cache immediately for back/forward nav
**Playback error → queue advances**
- player_loop now sets track_finished on Err (was only set on Ok(None)),
so the toolbar's onTrackFinished handler advances to the next track
instead of stalling on an unplayable track
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
**Artist portrait**
- Fix CDN URL: images.portrait.{hash,format} →
https://static.qobuz.com/images/artists/covers/large/{hash}.{format}
**Section alignment**
- Qt::ToolButtonTextOnly on all section toggles so text is truly left-aligned
**Auth 401 race condition**
- qobuz_backend_set_token now uses blocking_lock() instead of spawning an
async task, guaranteeing the token is set before any subsequent API call
**Pagination (infinite scroll)**
- Release sections load 50 at a time (was 500)
- ArtistSection tracks has_more + loaded count; scrolling to the bottom
emits loadMoreRequested → ArtistView calls getArtistReleases(offset=N)
- AlbumListView gains addAlbums() for append; setReleases routes to
setAlbums (offset=0) or appendAlbums (offset>0)
**Back/Forward navigation**
- MainToolBar exposes Back/Forward QActions (go-previous/go-next icons)
- MainWindow keeps a NavPage vector + index; pushNav() on every album/artist
navigation; goBack/goForward re-navigate without pushing history
**Context menu on now-playing label**
- Right-click on track label in toolbar → "Go to Album" / "Go to Artist"
- MainToolBar stores current track; emits albumRequested/artistRequested
signals wired to MainWindow's existing handlers
**Artist favourites button**
- ♡ Favourite / ♥ Favourited toggle in artist header
- Calls new addFavArtist / removeFavArtist (Rust + Qt backend wiring)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Artist header now mirrors TrackContextHeader: 120×120 portrait with
rounded rect background, large bold name, compact scrollable bio,
Play/Shuffle buttons in the same row (gold #FFB232 style)
- Play/Shuffle moved out of Popular Tracks into the header area
- Popular Tracks becomes a collapsible toggle section identical in
appearance to Albums/EPs/Live/Compilations/Other
- All section headers share kToggleStyle: left-aligned bold text,
▼/▶ arrow, subtle bottom border, hover highlight
- Portrait loading tries hash-based CDN URL first, falls back to
image.large / image.small direct URL fields
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Load artist portrait from images.portrait.hash via QNetworkAccessManager
- Fix race condition: fire getArtistReleases after setArtist() clears sections,
not before (from onArtistLoaded instead of onSearchArtistSelected)
- Apply uniform gold (#FFB232) play/shuffle button style matching album view
- Make biography scrollable (QTextEdit with max height + scroll on overflow)
- Extend track artist name parsing to handle top_tracks {display:...} format
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- top_tracks is a flat array in the API response, not {items:[...]}
- Replace bio QLabel with scrollable QTextEdit (max 110px, scrolls if longer)
- Track model: handle artist.name as {display:...} object for top_tracks format
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Qobuz API uses snake_case so the field is top_tracks, not topTracks.
Also remove the extra=topTracks query param — top tracks are included
in the default artist/page response without it.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Instead of relying on the limited preview in artist/page, fire a
separate artist/getReleasesList request per release type (album,
epSingle, live, compilation) in parallel when loading an artist.
Each result updates its section independently as it arrives, so the
page populates progressively without a single large request.
Also fixes the artist name in the status bar (was reading wrong field).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Adds extra=topTracks to artist/page API request
- Embeds a List::Tracks widget at the top of ArtistView showing
the artist's most popular tracks, with Play and Shuffle buttons
- Bubbles playTrackRequested through MainContent up to MainWindow
- Also adds the viz PCM ring buffer FFI infrastructure (for future
spectrum widget) to the Rust backend
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Move updateToggleText() after m_list creation to avoid null connects
- Show version (e.g. "Deluxe") in album header title
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Build hardening:
- Add -fstack-protector-strong, -D_FORTIFY_SOURCE=2, PIE, full RELRO
- Enable overflow-checks in Rust release profile
Rust backend:
- Return null (not panic) if Tokio runtime or QobuzClient init fails
- Strip null bytes in FFI JSON callback to prevent CString panics
- Document MD5 and password-in-query as Qobuz API constraints
C++ frontend:
- Validate JSON document before accessing fields in onEvent()
- Handle null backend pointer from failed init
- Set biography label to PlainText and decode HTML entities to prevent
rendering injected content from API responses
- Clamp slider position and guard negative durations
- Use qint64 for duration formatting to avoid int truncation
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Switch artist view to artist/page API (proper sections: Albums, Singles & EPs,
Live, Compilations; version in titles like "Deluxe")
- Fix artist sections categorization using releases[].type from artist/page
- Add getUser() backend call; fetch on session restore when userId=0 to fix
playlist ownership (Remove from playlist / Delete playlist were missing)
- Fix multi-disc double-click / Play Now queue start index (disc headers were
counted in row index but excluded from currentTracksJson)
- Hide redundant Album column when viewing an album
- Make artist name in context header clickable (navigates to artist page)
- Fix gap between title and artist name in context header (addStretch)
- Fix queue panel track titles to include version field
- Fix album list to show version in title column
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Features and fixes from this branch:
- Album/playlist context header with art, metadata, Play & Shuffle buttons
- Playlist ownership filtering (delete, remove-from-playlist, add-to-playlist)
- Gapless toggle that actually works (controls audio output lifecycle)
- Resolution display in album header (e.g. 24-bit / 96 kHz)
- Now-playing art scales to dock width via ArtWidget (hasHeightForWidth)
- Skip unavailable tracks in queue; non-streamable tracks filtered out
- Artist profile: collapsible Albums / EPs & Singles / Other sections
- Favorites star indicator + context menu shows Add or Remove only
- Shuffle button does one-time shuffle without setting global flag
- Volume popup replaced with Qt::Popup QFrame (no lag, correct alignment)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Artist profile: collapsible Albums / EPs & Singles / Other sections
keyed on release_type; fetches up to 200 albums per artist
- Favorites: starred icon on favorited tracks, context menu shows
Add or Remove (not both); IDs cached when fav tracks are loaded
- Shuffle button: one-time shuffle via shuffleNow() without touching
global shuffle flag, so double-click still plays in order
- Now-playing art: replaced setFixedHeight hack with ArtWidget that
overrides hasHeightForWidth() — scales smoothly up and down, no min-size
- Volume popup: replaced QMenu (laggy, broken drag) with Qt::Popup QFrame;
appears below button; fixed size locked at 100% label width
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Fix playlist art: API returns images/images150/images300 arrays, not
image_rectangle; update Rust model and Qt header to use images300 →
images150 → images with fallback
- Add Play (▶) and Shuffle (⇄) buttons to the album/playlist header;
Play starts from track 1, Shuffle enables shuffle mode and plays from
a randomised position
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Now-playing art: use setFixedHeight (not setFixedSize) so the dock
width constraint is removed and can shrink freely; use event->size()
in resizeEvent to get the correct new width without layout lag
- PlayQueue: filter non-streamable tracks out of setContext and
reorderContext so unavailable tracks are never in the queue and
pressing Next always lands on a playable track
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Stores the raw downloaded pixmap and rescales it (smooth, aspect-ratio
preserved) whenever the dock is resized, so the image always fills
the available width.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When gapless is off, the AudioOutput is dropped after each track ends
naturally, producing a real gap on the next play. When on, the output
stays alive so tracks transition seamlessly. Also re-adds URL prefetch
gating behind the same toggle.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The player keeps the audio output alive between tracks unconditionally.
The toggle only controlled URL prefetching, not actual audio gaplessness.
Remove the setting and always prefetch the next track URL.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add TrackContextHeader widget: shows album art (fetched via NAM),
title, subtitle (artist/description), and metadata (year · tracks · duration)
above the track list when an album or playlist is opened
- Hide header for favorite tracks and search results
- Store user ID in AppSettings on login
- Only show "Delete playlist" for playlists the user owns
- "Add to playlist" submenu only lists owned playlists
- "Remove from this playlist" only appears when viewing an owned playlist
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add EV_PLAYLIST_TRACK_ADDED (22) — emitted when a track is
successfully added to a playlist
- If the currently-open playlist is the one modified, re-fetch it
so the track appears in the list immediately
- After creating a playlist, open it automatically so the user
lands in the new (empty) playlist view right away
- Sidebar already refreshes on create/delete; this ensures the
track list view also stays in sync
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Playlist management:
- Add/remove tracks from playlists via right-click context menu
- Create new playlists (right-click Playlists sidebar header)
- Delete playlists with confirmation dialog (right-click playlist item)
- Playlist view removes track immediately on delete (optimistic)
- Deleting currently-open playlist clears the track view
Gapless playback:
- Single long-running audio thread owns AudioOutput; CPAL stream stays
open between tracks eliminating device teardown/startup gap
- Decode runs inline on the audio thread; command channel polled via
try_recv() so Pause/Resume/Seek/Stop/Play all work without spawning
- New Play command arriving mid-decode is handled immediately,
reusing the same audio output for zero-gap transition
- Position timer reduced from 500 ms to 50 ms for faster track-end detection
- URL/metadata prefetch: when gapless is enabled Qt pre-fetches the next
track while the current one is still playing
ReplayGain:
- Toggled in Settings → Playback
- replaygain_track_gain (dB) from track audio_info converted to linear
gain factor and applied per-sample alongside volume
Qobuz dark theme:
- Background #191919, base #141414, accent #FFB232 (yellow-orange)
- Selection highlight, slider fill, scrollbar hover all use #FFB232
- Links use Qobuz blue #46B3EE
- Hi-res H badges updated to #FFB232 (from #FFD700)
- Now-playing row uses #FFB232 (was Spotify green)
- QSS stylesheet for scrollbars, menus, inputs, buttons, groups
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Compact single row: title in medium weight, separator dot and artist
name dimmed, duration right-aligned. Height scales with font.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Height is now 2 line-heights + 10px padding instead of a fixed 52px,
keeping the two-line layout without wasting vertical space.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Each queue item now shows:
- Track title (medium weight) top-left
- Duration (small, dimmed) top-right
- Artist name (small, dimmed) bottom-left
Replaces the flat "Artist — Track" single-line text.
Play-next items get a slightly brighter title tint.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Title shown in semi-bold, artist below in smaller grey text — matching
the previous two-line style the user preferred.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Flat QToolBar with sequential addAction/addWidget — no nested containers.
Order: [art][track] | [prev][play][next] [leftSpacer] [progress][time] [rightSpacer] [shuffle][vol][queue][search]
Centering via resizeEvent: both spacers get width/6 of total bar width,
mirroring the exact approach used in spotify-qt.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>