Commit Graph

21 Commits

Author SHA1 Message Date
joren
3346b424b3 refactor: resync with qbqt baseline and restore genre browser
Some checks failed
Build for Windows / build-windows (push) Has been cancelled
2026-03-30 22:36:39 +02:00
joren
70810bd4b1 feat: qbz-1 streaming, gapless prefetch, accurate scrobbling, Range-seek
Port proven playback architecture from qbqt fork:
- Bounded VecDeque buffer with condvar backpressure (4MB cap)
- decrypt_and_extract_frames for clean FLAC frame extraction from ISOBMFF
- Cancel+restart seeking with sub-segment sample skipping
- start_prefetch / QueueNext for gapless transitions with pre-started downloads
- track_transitioned signaling for scrobbler during gapless playback
- Range-request HTTP seeking for non-segmented (MP3) tracks
- OnceLock HTTP client singleton with cancel-aware chunked downloads
- Accumulated listening time scrobbling (prevents false scrobbles from seeking)
- Array-format Last.fm scrobble params (artist[0], track[0], etc.)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-30 20:12:23 +02:00
joren
6c8d032ce9 feat: context menus on search panel, track info dialog, separate shuffle buttons
- 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>
2026-03-25 23:57:42 +01:00
joren
a21d0c8a33 refactor: UI polish — lock sidebar, remove nav buttons, uniform artist tables, deep shuffle
- 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>
2026-03-25 23:42:00 +01:00
joren
333a620be2 fix: section alignment, pagination, fav state, playback error handling
**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>
2026-03-25 23:22:30 +01:00
joren
3e96b6d7a8 feat: pagination, back/forward nav, context menu, artist fav, image fix
**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>
2026-03-25 18:25:12 +01:00
joren
8310eceeb2 feat: artist portrait, race condition fix, and uniform button styling
- 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>
2026-03-25 14:17:36 +01:00
joren
5ae18afa08 feat: full artist release list via artist/getReleasesList
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>
2026-03-25 13:53:57 +01:00
joren
6f11b364aa feat: show top tracks on artist profile with play/shuffle
- 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>
2026-03-25 13:45:19 +01:00
joren
872fdecdce feat: artist/page endpoint, multi-disc fix, playlist ownership, UX improvements
- 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>
2026-03-24 23:09:04 +01:00
joren
56473cae6f feat: artist sections, fav indicator, art scaling fix, volume popup fix
- 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>
2026-03-24 17:56:47 +01:00
joren
1e4c234b5c fix: gapless toggle now actually controls audio output lifecycle
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>
2026-03-24 11:39:24 +01:00
joren
c035ce2dee fix: remove gapless toggle — architecture is always gapless
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>
2026-03-24 11:36:54 +01:00
joren
8088412d4b feat: album/playlist header and playlist ownership filtering
- 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>
2026-03-24 11:34:04 +01:00
joren
75e2b623b8 feat: refresh views on all playlist mutations
- 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>
2026-03-24 11:23:45 +01:00
joren
f30e2058c1 feat: playlist management, gapless playback, ReplayGain, Qobuz theme
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>
2026-03-24 11:19:32 +01:00
joren
373fc2b43c feat: queue panel skip-to-track and drag reorder; remove visualizer
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>
2026-03-24 10:12:30 +01:00
joren
b9b47f80e7 feat: seeking support and Last.fm scrobbling
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>
2026-03-24 01:13:06 +01:00
joren
d5dedacc36 feat: album list, artist list, and artist detail views
- 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>
2026-03-24 00:59:02 +01:00
joren
cb2323bc32 feat: initial qobuz-qt source
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>
2026-03-24 00:41:04 +01:00
joren
9402dca7ed Initial implementation of qobuz-qt
- Rust backend (qobuz-backend static lib): Qobuz API client (reqwest/tokio),
  Symphonia audio decoder, CPAL audio output, extern "C" FFI bridge
- Qt 6 frontend mirroring spotify-qt layout: toolbar with playback controls,
  left library dock, central track list, right search panel
- Auth: email/password login with MD5-signed requests; session token persisted
  via QSettings
- Playback: double-click a track → Rust fetches stream URL → Symphonia decodes
  → CPAL outputs to default audio device
- Dark Fusion palette matching spotify-qt feel

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-23 23:34:23 +01:00