Root cause: server pause/resume commands send None for position and
tracks, which the renderer state blindly overwrote. Then renderer
reports sent null position to the server, making controllers show the
track as finished.
Three-part fix:
1. renderer.rs: only overwrite position/tracks when server provides
a non-None value (retain last known values through pause/resume)
2. app.rs + qconnect_service.rs: update renderer state position from
frontend periodic reports so the internal state always has the
real playback position
3. +page.svelte: increase position report frequency from 5s to 2s
for smoother seekbar updates on controllers
ErrorMessage.code was defined as int32 but server sends string codes
(e.g. 'ERROR_QUEUE_INSERT_TRACKS'). Changed to string type and added
logging for queue error responses.
Added context_uuid (random UUID v4) to queue insert and add commands -
the server likely requires this to associate tracks with a context.
track_ids in QueueLoadTracks, QueueInsertTracks, QueueAddTracks, and
AutoplayLoadTracks was int32 packed=false but the server expects
fixed32 (packed by default). This silently broke all outbound queue
manipulation commands.
Server sends track_id as fixed32 (wire type 5) and shuffle_seed as
fixed32, but our structs declared them as int32 (wire type 0/varint),
causing decode failures on QueueState, TracksLoaded, and
RendererSetState messages.
Root cause: inner_payload from CloudPayload.payload IS a QConnectBatch
(QConnectMessages) with repeated QConnectMessage entries in field 3.
The QCloudInnerEnvelope struct incorrectly treated field 3 as opaque
bytes, keeping only the last repeated entry and losing all messages.
Additionally, QConnectMessages.messages_time was declared as uint64
(wire type 0/varint) but the server sends fixed64 (wire type 1),
causing a wire type mismatch that would fail decoding.
Changes:
- Remove QCloudInnerEnvelope struct entirely
- Pass inner_payload directly to decode_queue_server_events/decode_renderer_server_commands
- Fix messages_time from uint64 to fixed64 to match server wire format
Verified against qonductor proto definitions (QConnectBatch) used in
working POC v1 and v2 implementations.
Root cause: server wraps QConnectMessage inside an inner envelope with
fields 1 (correlation_id), 2 (sequence), 3 (actual message bytes).
We were passing the envelope directly to QConnectMessage::decode(),
which silently skipped the unknown wrapper fields and found nothing.
Also restructure bootstrap: controller join first (works without
session_uuid), renderer join deferred until SESSION_STATE (type 81)
arrives with the session_uuid the server requires.
Add missing SRVR_CTRL session management message types (81-87, 97-101)
that were completely absent from the protobuf definitions. Without these,
prost silently discarded all session topology messages from the server.
- Add 12 new proto structs for session/renderer/volume/quality events
- Add decoder branches and map functions for all new message types
- Add SessionManagementEvent variant to bypass queue reducer
- Add v2_qconnect_report_playback_state and v2_qconnect_report_volume
- Add comprehensive logging for inbound message diagnostics
- Add DeviceCapabilitiesMessage to protobuf (tag 7 on DeviceInfo) with
volume_remote_control=ALLOWED, min/max audio quality
- Extend JoinSessionMessage with reason, initial_state, is_active fields
matching Android renderer proto (tags 3-5)
- Bootstrap sends renderer JoinSession with is_active=true and initial
RendererState (STOPPED) so other devices see QBZ immediately
- Send initial StateUpdated, VolumeChanged, MaxAudioQualityChanged
reports after join for full renderer presence
- Default subscribe channels to 0x01,0x02,0x03 instead of empty
- Request queue state from server after bootstrap
cc
The save was broken in two ways:
1. Only current_track + upcoming (capped at 20) were saved, dropping
all history tracks. Queue was reconstructed incomplete on restore.
2. Track metadata (hires, bit_depth, sample_rate, source, etc.) was
not being persisted, only the basic 6 fields.
Add v2_get_all_queue_tracks command that returns the full uncapped
track list with correct current_index. Use it in saveSessionBeforeClose
instead of the capped v2_get_queue_state. Map all metadata fields.
Replace the flat album grid LabelView with a rich detail page featuring:
- Header with label image, name, description, and play button
- Popular Tracks section (5/10/all expand, same UX as ArtistDetailView)
- Releases horizontal scroll row with "See all" → LabelReleasesView
- Critics' Picks section from second releases container
- Playlists, Artists, and More Labels sections
Backend: add /label/page and /label/explore API endpoints with V2
commands (v2_get_label_page, v2_get_label_explore). Models use
serde_json::Value for flexible section parsing.
Old LabelView renamed to LabelReleasesView for the "See all" flow.
i18n keys added to all 4 locales (en, es, de, fr).
Add experimental toggle to force PipeWire clock.force-quantum alongside
clock.force-rate for true bit-perfect playback. Quantum is matched to
sample rate (1024 for <=48kHz, 2048 for <=96kHz, etc). Both values
reset to 0 on stop and pause-suspend to avoid affecting other apps.
Requires DAC Passthrough to be enabled, PipeWire backend only.
- downloaded_purchases table in library DB (permanent, per-user)
- V2 commands: mark_downloaded, remove_downloaded, get_downloaded_track_ids
- Frontend enriches purchase data with download status from backend
- Grid overlay for unavailable albums centered on artwork
- Quality filter expanded: All / Hi-Res / CD / Lossy (radio group)
- Album download status derived from track-level registry
- add Spectral Ribbon analyzer/render path and immersive panel
- tune ribbon density/interpolation and bars edge-bin rendering
- apply artist blacklist filtering to discover index home containers
- include supporting i18n/settings/player wiring changes
cx
Previously, V2 queue commands had a dual-queue bug:
- Writes (v2_set_queue, v2_add_to_queue, etc.) went to AppState.queue (legacy)
- Reads (v2_get_queue_state) came from CoreBridge/QbzCore queue
- Result: frontend sets queue, syncs, gets empty/stale data
This commit fixes the architecture:
- All V2 queue commands now use CoreBridge methods
- Added queue methods to QbzCore: add_track, add_tracks, add_track_next,
set_queue, remove_track, remove_upcoming_track, move_track, play_index,
next_track, previous_track, peek_upcoming
- Added corresponding methods to CoreBridge
- Fixed v2_set_shuffle to use CoreBridge (was using legacy AppState)
- Fixed runtime requirements: v2_toggle_shuffle and v2_clear_queue now
use RequiresUserSession like other queue commands (consistency)
- Prefetch now gets upcoming tracks from CoreBridge, not legacy queue
- Updated tests to match V2 behavior (v2_toggle_shuffle, available field)
Extract casting functionality (Chromecast, AirPlay, DLNA) to standalone
qbz-cast crate. This crate is Tauri-agnostic and can be used by TUI/CLI
clients.
- Move device connection, discovery, threading, and media server to crate
- Keep only Tauri commands in src-tauri/src/cast/
- Thin re-exports in src-tauri for backwards compatibility
- Remove duplicate dependencies from src-tauri Cargo.toml
Create qbz-library crate with:
- LibraryScanner: directory scanning for audio files
- MetadataExtractor: audio metadata extraction via lofty
- LibraryDatabase: SQLite persistence with WAL mode
- CueParser: CUE sheet parsing for single-file albums
- Thumbnails: artwork extraction and thumbnail generation
- Tag sidecar: album metadata override files
src-tauri re-exports from qbz-library for backwards compatibility.
Enables TUI/headless clients to manage local libraries without Tauri.
Create qbz-cache crate with:
- AudioCache: L1 in-memory LRU cache (~300MB)
- PlaybackCache: L2 disk-based cache (~500MB)
- LRU eviction with spillover from L1 to L2
src-tauri now re-exports from qbz-cache for backwards compatibility.
This enables TUI/headless clients to use caching without Tauri.
New crate for Last.fm, ListenBrainz, and MusicBrainz integrations.
Designed to work WITHOUT Tauri for TUI/headless clients.
Modules:
- lastfm: Scrobbling and now-playing via Cloudflare Workers proxy
- listenbrainz: Direct API client with user token auth
- musicbrainz: Entity resolution with rate limiting
Features:
- `cache` (default): SQLite persistence for offline support
- ListenBrainz: Credential storage, offline listen queue
- MusicBrainz: Resolved entity cache with TTL
Architecture:
- Pure async Rust, no Tauri dependencies
- Shared error types via IntegrationError enum
- Rate limiting for MusicBrainz (1 req/sec direct, faster via proxy)
Add get_discover_albums to qbz-core, CoreBridge, and commands_v2.
Supports endpoints: newReleases, idealDiscography, mostStreamed,
qobuzissimes, albumOfTheWeek, pressAward.
Migrate DiscoverBrowseView to use v2_get_discover_albums.
Add missing API methods to QbzCore, CoreBridge, and V2 commands:
- Playlist management: create, delete, update, search
- Catalog: genres, discover index/playlists, playlist tags, featured albums
- Artists: artist page, similar artists, artist with albums pagination
- Labels: label details
- Tracks: batch fetch
All methods follow the V2 architecture with RuntimeManager requirements.
- Add playlist methods to QbzCore (get_user_playlists, get_playlist,
add_tracks_to_playlist, remove_tracks_from_playlist)
- Add playlist methods to CoreBridge
- Create V2 Tauri commands for all playlist operations
- Update frontend to use V2 playlist commands
- Support both playlistTrackIds and trackIds in remove command
(with automatic resolution like legacy)
QbzCore::init() was creating the QobuzClient but not calling init()
to extract bundle tokens. This caused "Client not initialized" errors
for all V2 API commands.
Core playback now flows through V2 architecture:
- Added get_stream_url to QbzCore and CoreBridge
- Implemented v2_play_track with full cache support:
- Offline cache (persistent disk)
- Memory cache (L1)
- Playback cache (L2 - disk)
- Downloads from Qobuz via CoreBridge.get_stream_url()
- Plays via CoreBridge.player() (qbz-player crate)
- Respects quality limits and streaming_only settings
Still uses AppState.audio_cache for caching (to be migrated later).
No dependency on legacy player module for playback.
- QbzCore now accepts Player in constructor and owns playback
- Added playback methods to QbzCore (pause, resume, stop, seek, volume)
- CoreBridge loads audio settings and creates Player from qbz-player
- V2 playback commands use CoreBridge instead of AppState
This closes the "operational playback core" gap identified by Codex.
V2 commands now flow: frontend -> commands_v2 -> CoreBridge -> QbzCore -> Player
Legacy player in src-tauri/src/player/ remains as emergency fallback.
Update search commands to return full SearchResultsPage<T> with pagination
info instead of just Vec<T>. This makes V2 commands compatible with the
frontend's expected response format.
Reorganize qbz-player to export the complete Player struct with all
playback functionality.
Changes:
- Restructure: mod.rs -> player/mod.rs with submodules
- Add rodio, serde, reqwest, qbz-qobuz dependencies
- Export Player, SharedState, PlaybackState, PlaybackEvent
- Export streaming types (BufferedMediaSource, etc.)
The Player module is now fully available from qbz-player crate.
Full workspace and src-tauri compile successfully.
Add QbzCore<A: FrontendAdapter> as the main orchestrator that ties
all subsystems together and provides a unified API for frontends.
Changes:
- Add core.rs with QbzCore struct
- Add error.rs with CoreError enum
- Update lib.rs with re-exports
QbzCore provides:
- Initialization and shutdown
- Authentication (login/logout)
- Queue operations (shuffle, repeat, clear)
- Event emission through FrontendAdapter
- Access to underlying subsystems (client, queue)
Full workspace compiles with cargo check.