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>
Controls (prev/play/next) and progress bar are now stacked vertically
to the right of the album art + track label, all in the left section.
Right side keeps volume, shuffle, queue, search toggles.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Three-column layout matching spotify-qt:
- Left: album art thumbnail + track title/artist (stacked, 2 lines)
- Center: prev/play/next controls above elapsed / progress / total
- Right: volume button, shuffle, queue toggle, search toggle
Controls and toggles use QToolButton directly instead of QAction so the
layout is driven by HBoxLayout + VBoxLayout with equal stretch on left
and right, keeping the player controls perfectly centred at all widths.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Queue panel:
- Double-clicking an upcoming track skips to it immediately: drops all
tracks before it from the queue and starts playback (skipToUpcoming)
- Items can be dragged to reorder; rowsMoved rebuilds the queue via
setUpcomingOrder()
- Track JSON stored per-item so order survives drag operations
- New PlayQueue methods: skipToUpcoming(), setUpcomingOrder()
- New QueuePanel signal: skipToTrackRequested(qint64) wired to MainWindow
Remove visualizer:
- Drop VisualizerWidget, Qt6::OpenGLWidgets, projectM CMake detection
- Remove qobuz_backend_read_pcm FFI (Rust + C header + Qt wrapper)
- Remove pcm_visualizer from PlayerStatus and PCM tap from AudioOutput
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When the H badge column was added, TypeRole was accidentally stored in
col 1 but the double-click handler reads it from col 0, breaking album
navigation from search. Move TypeRole back to col 0.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add QPalette::PlaceholderText (#8C8C8C) to the dark palette so
placeholder text in search box and settings fields is readable
- Add H column to search album results (same gold badge as album list view)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds a narrow first column showing a small bold gold "H" for any album
with hires_streamable=true. Applies to both fav albums and artist pages.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Seeking:
- Rust player: seek_requested/seek_target_secs atomics on PlayerStatus
- Decoder loop checks for seek each iteration, calls format.seek() and resets decoder
- New qobuz_backend_seek C FFI + QobuzBackend::seek(quint64)
- Progress slider onProgressReleased now seeks to the dragged position
Last.fm:
- LastFmScrobbler: now-playing + scrobble (50% or 240s threshold, min 30s)
- API signature follows Last.fm spec (sorted params, md5)
- Settings dialog: API key/secret, username/password, Connect button with status
- AppSettings: lastfm/enabled, api_key, api_secret, session_key
- Scrobbler wired to trackChanged, positionChanged, trackFinished in MainWindow
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Removed the sortApplied→syncQueueToModel connection. Sorting the track
list only reorders the view; the queue is only set when a track is
actually played (double-click or Play Now), at which point the current
sorted order and clicked row are used.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Fav albums: now shows a sortable list (title/artist/year/tracks);
double-click opens the album
- Fav artists: now shows a sortable list; double-click opens the artist
- Artist detail page: name, biography summary, and their album list
- Rust ArtistDto gains albums field; get_artist fixed to extra=albums only
- Volume popup minimum width set so "100%" label is never clipped
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Opening a playlist/album no longer resets the queue. The queue context
is now set inside onDoubleClicked and the "Play Now" context menu action,
using the current (possibly sorted) model order at the moment of play.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
advance() was removing the playNext item then calling current(), which
fell back to m_queue[m_index] — the same track already playing. Fix:
use takeFirst() and return that item directly, only advancing m_index
when the playNext list is empty.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Lightweight Qt6 desktop client for Qobuz with a Rust audio backend
(Symphonia/CPAL via staticlib FFI). Mirrors the spotify-qt layout:
toolbar with playback controls, library/context docks on the left,
tabbed search side panel on the right, queue panel, now-playing dock.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
ClickableSlider previously accepted the mouse press without forwarding
to QSlider, so the drag mechanism never started. Now setSliderPosition
is called first (jumping the handle to the click point) and the event
is always forwarded so Qt enters normal drag mode. mouseMoveEvent is
also overridden to keep snapping to the cursor during a drag.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>