- Add v2_save_session_state and v2_load_session_state commands
- Update favoritesStore to use V2 cache commands
- Update albumFavoritesStore to use V2 cache commands
- Update artistFavoritesStore to use V2 cache commands
- Update sessionService to use V2 session commands
- Update playbackContextStore to use V2 context commands
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)
- HomeView: fix OfflineCacheStatus type for getTrackOfflineCacheStatus prop
- PlaylistDetailView: fix trackToReplace null access in console.log
- SearchView: add id and isViewMore to Album/Artist interfaces
- Toast: add 'warning' type support
- playbackContextStore: add 'search' to ContextType union
- queueStore: add trackId to QueueTrack interface
- trackActions: add queueDisplayTrackNext/Later and buildQueueTrackFromDisplayTrack
- +page.svelte: fix numerous type issues including:
- selectedAlbum closure captures for TypeScript narrowing
- handleDisplayTrackDownload signature for DisplayTrack
- handleSearchTrackDownload for SearchView's Track type
- ImportSummary callback signature
- QobuzTrack hires_streamable property usage
- album.tracks.items instead of .data
npm run check now passes with 0 errors (138 warnings remain, mostly a11y)
- SearchView.svelte: Fix async onMount pattern to properly return cleanup
- vite.config.js: Add ts-nocheck to suppress Node.js global errors
Reduces npm run check errors from 42 to 40.
ListenBrainzV2State::clear_credentials() now resets internal client to
fresh state, not just wrapper fields.
LastFmV2State:
- Added clear_session() method that clears session_key AND pending_token
- init_with_session(None) now calls clear_session() instead of no-op
Ensures no credentials leak across sessions on logout/deactivate.
Closes Must-Fix #2 from MUST_FIX_BEFORE_RELEASE_RUNTIME_V2.md
Added rollback_auth_state() helper that clears legacy_auth, corebridge_auth,
and emits AuthChanged event on partial login failures.
Applied to all error paths in v2_auto_login, v2_manual_login, and v2_login:
- CoreBridge auth failure now rolls back
- Session activation failure now rolls back
Ensures runtime_get_status never reports half-authenticated state.
Closes Must-Fix #1 from MUST_FIX_BEFORE_RELEASE_RUNTIME_V2.md
V2 ListenBrainz now_playing and scrobble commands now accept optional
MusicBrainz identifiers (recording_mbid, release_mbid, artist_mbids),
ISRC, and duration_ms parameters for richer scrobbles.
The AdditionalInfo struct is populated when any metadata is provided,
automatically including QBZ media player identification.
Integrates ListenBrainzV2State, MusicBrainzV2State, and LastFmV2State
from qbz-integrations crate into the session activation/deactivation
lifecycle.
In activate_session:
- After legacy integrations init (which loads from SQLite), sync
credentials to V2 states
- ListenBrainz V2: gets token, user_name, enabled from legacy client
- MusicBrainz V2: gets enabled state (use_proxy defaults to true)
- LastFm V2: initialized to clean state (no legacy equivalent)
In deactivate_session:
- Clear V2 state credentials to prevent cross-user leaks
- Reset states to defaults
This ensures V2 commands (using qbz-integrations crate) have access
to the same credentials as legacy commands, enabling a smooth
migration path.
Add centralized require_tos_accepted() helper function that checks
ToS acceptance before any authentication attempt. This prevents
non-UI clients (TUI/CLI/tests) from bypassing the ToS gate.
Affected commands:
- v2_auto_login: Returns tos_not_accepted error code
- v2_manual_login: Returns tos_not_accepted error code
- v2_login: Returns RuntimeError::Internal
The ToS check was already in runtime_bootstrap, but these commands
could be called directly. Now the gate is enforced at the backend
level regardless of entry point.
Also updates obsolete comment about frontend handling ToS.
Fixes login consistency issues:
1. ToS gate enforcement:
- LegalSettingsState now initialized GLOBALLY at app startup (not per-session)
- runtime_bootstrap checks ToS BEFORE attempting auto-login
- If ToS not accepted, skip auto-login and return to login screen
2. Session activation severity parity:
- runtime_bootstrap now treats activate_session failure as FATAL
- On failure: rollback auth state, set degraded, return error
- Matches behavior of v2_auto_login/v2_manual_login
3. LegalSettings is now truly global:
- Removed from session_lifecycle activate/deactivate
- ToS acceptance persists across sessions
- Can be checked before user is logged in
Acceptance criteria:
- ToS false + credentials -> no auto-login
- ToS true + credentials + activation OK -> session restored
- ToS true + credentials + activation fails -> error, no session
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
Backend:
- v2_auto_login/v2_manual_login now FAIL if activate_session fails
(was: log and continue with success:true, causing "logged in but not ready")
- Session activation is now REQUIRED, not optional
Frontend:
- Remove duplicate activate_user_session call from handleLoginSuccess
(V2 login already activates session internally)
- Remove duplicate deactivate_user_session call from handleLogout
(v2_logout already deactivates session internally)
- LoginView now uses runtime_bootstrap as SINGLE SOURCE OF TRUTH
(was: v2_init_client + legacy is_logged_in/get_user_info checks)
- PlaylistDetailView uses runtime_get_status for user_id
(was: legacy get_current_user_id)
This eliminates the "double-brain" where frontend used both legacy
and V2 paths, causing state divergence and intermittent failures.
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.
Convert queue commands (16) and playback commands (10) from String
errors to typed RuntimeError:
- Queue: get_queue_state, set_repeat_mode, toggle_shuffle, etc.
- Playback: pause, resume, stop, seek, volume, get_state, etc.
Runtime checks now use direct ? propagation instead of map_err.
Bridge method errors wrapped with RuntimeError::Internal.
Wire qbz-integrations to src-tauri Cargo.toml. The crate provides
Tauri-independent implementations of Last.fm, ListenBrainz, and
MusicBrainz integrations.
Next step: Create V2 commands that use the new crate, then gradually
deprecate the existing in-tree implementations.
Replace String errors with typed RuntimeError variants across all 17
audio settings V2 commands for consistent error handling:
- Lock errors: RuntimeError::Internal
- Session not active: RuntimeError::UserSessionNotActivated
- Store errors: RuntimeError::Internal
This aligns with the runtime contract (ADR_RUNTIME_SESSION_CONTRACT.md)
for typed errors that clients can pattern match.
Addresses multiple review findings for V2 architecture:
1. Create session_lifecycle.rs module:
- activate_session(): Full session activation callable from anywhere
- deactivate_session(): Full teardown with proper runtime state cleanup
- activate_offline_session(): Minimal session for offline/local-only use
2. Make runtime_bootstrap autocontenido:
- Now activates session after successful auth (Step 4)
- No longer delegates to frontend for session activation
3. Fix v2_login to be canonical:
- Performs full flow: legacy auth -> corebridge auth -> session activation
- Updates all runtime state flags
4. Fix v2_logout to be canonical:
- Calls deactivate_session (teardown stores + clear runtime state)
- Proper cleanup of legacy and corebridge auth
5. Fix deactivate_session:
- Now clears session_activated flag (was missing)
- Now clears corebridge_auth flag (was missing)
- Emits UserSessionDeactivated event
6. Add v2_set_repeat_mode runtime check:
- Now requires RequiresUserSession like other queue mutations
7. Fix LoginView early session activation:
- Removed premature activate_user_session call before auto-login confirmed
- ToS check uses localStorage fallback if no session
8. Fix offline mode:
- Add v2_activate_offline_session command
- Offline mode now activates minimal session so queue commands work
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.
The toggleCurrentTrackFavorite function was calling non-existent
add_track_to_favorites/remove_track_from_favorites commands.
Updated to use v2_add_favorite/v2_remove_favorite with proper params.
- HomeView: use v2_get_discover_index and v2_get_discover_playlists
- DiscoverPlaylistsBrowseView: use v2_get_discover_playlists
- PlaylistModal: use v2_delete_playlist
Part of V2 migration to enable TUI/headless usage without Tauri.
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.
Migrate all frontend get_favorites invoke calls to use V2 command:
- FavoritesView.svelte
- ArtistDetailView.svelte
- favoritesStore.ts
- albumFavoritesStore.ts
- artistFavoritesStore.ts
- playbackService.ts
All favorites API calls now go through CoreBridge V2 architecture.
Add RuntimeManagerState and check_requirements to:
- v2_get_album/track/artist (RequiresCoreBridgeAuth)
- v2_pause/resume/stop_playback (RequiresClientInit)
- v2_seek, v2_set_volume (RequiresClientInit)
Playback commands use RequiresClientInit since they operate on
the local player and don't need API auth, just initialized state.
Add RuntimeManagerState parameter and CommandRequirement check to:
- v2_play_track (RequiresCoreBridgeAuth)
- v2_search_albums/tracks/artists (RequiresCoreBridgeAuth)
- v2_get_favorites/add_favorite/remove_favorite (RequiresCoreBridgeAuth)
- v2_get_user_playlists/get_playlist (RequiresCoreBridgeAuth)
- v2_add_tracks_to_playlist/remove_tracks_from_playlist (RequiresCoreBridgeAuth)
This enforces the ADR Runtime Session Contract at the command level.
V2 commands now fail with typed RuntimeError if auth state is invalid.
Add V2 commands that implement the ADR Runtime Session Contract:
- v2_init_client: Phase 1 - init client only (bundle tokens)
- v2_auto_login: Phase 2 - auto-login with blocking V2 auth
- v2_manual_login: Manual login with blocking V2 auth
This allows frontend to control ToS check between init and auth:
1. v2_init_client() -> InitializedNoAuth
2. activate_user_session() -> restore session
3. loadTosAcceptance() -> check ToS
4. v2_auto_login() or v2_manual_login() -> Ready
V2 CoreBridge auth is now REQUIRED per ADR - login fails if V2 fails.
Migrate LoginView.svelte to use new V2 commands:
- Replace init_client with v2_init_client
- Replace auto_login with v2_auto_login
- Replace login with v2_manual_login
Add i18n keys for V2 auth errors in all 4 locales.
- auto_login now fails if V2 CoreBridge auth fails
- runtime_bootstrap now fails if V2 CoreBridge auth fails
- Add V2AuthFailed and V2NotInitialized runtime error variants
- Consistent with login command which was already blocking
- 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)
- Login now requires V2 CoreBridge auth to succeed (per ADR)
- V2 auth failure returns typed error, not silent non-blocking warning
- Update RuntimeManager state on login/logout for canonical tracking
- Add runtime_manager parameter to login and logout commands
This implements Phase 4 of the runtime contract: Login Semantics Hardening
- Add RuntimeManagerState to activate_user_session
- Add RuntimeManagerState to deactivate_user_session
- Update runtime state on session activation/deactivation
- Add v2_remove_upcoming_track command
This implements Phase 2 of the runtime contract by ensuring session
state changes are reflected in the canonical RuntimeManager.
- Add V2 queue commands: v2_set_shuffle, v2_move_queue_track,
v2_add_tracks_to_queue, v2_add_tracks_to_queue_next,
v2_remove_upcoming_track
- Migrate queueStore.ts to use all V2 queue commands
- Update all view components to use V2 queue commands:
- HomeView, SearchView, ArtistDetailView, FavoritesView
- LocalLibraryView, PlaylistDetailView
- Update +page.svelte to use V2 shuffle, repeat, and remove commands
No legacy queue commands are called from frontend anymore.
Runtime contract (ADR Phase 1+2):
- Add RuntimeManager with canonical state machine
- Add RuntimeStatus, RuntimeError (typed errors)
- Add RuntimeEvent for lifecycle events
- Add runtime_get_status and runtime_bootstrap commands
Queue V2 commands:
- v2_add_to_queue, v2_add_to_queue_next
- v2_set_queue, v2_remove_from_queue
- v2_next_track, v2_previous_track, v2_play_queue_index
- V2QueueTrack type with proper field mapping
All queue operations now have V2 equivalents.
Frontend migration to use these commands is next.
The event loop now checks both V2 (qbz-player) and legacy player states.
V2 player takes priority when it has an active track (track_id != 0).
This ensures the frontend receives correct playback position, duration,
sample rate, and bit depth when using v2_play_track commands.
Changes:
- Store V2 player SharedState after CoreBridge initializes
- Event loop checks V2 state first, falls back to legacy
- All playback event values now come from the active player
Addresses race condition and invalid state issues identified by Codex diagnostic:
1. CoreBridge: Add try_get() and is_initialized() methods to gracefully
handle uninitialized state instead of panicking
2. auth.rs: Use try_get() for V2 auth in login/auto_login/logout to
avoid panic if CoreBridge init is still in progress
3. LoginView.svelte: Reject userId=0 as fallback - if userInfo or userId
is missing/invalid, don't call onLoginSuccess (require re-login)
4. +page.svelte:
- Move get_audio_settings call to after sessionReady is set
- Add explicit userId > 0 check before activate_user_session
- Return early if activation fails to prevent sessionReady from
being set in broken state
These are tactical fixes aligned with ADR_RUNTIME_SESSION_CONTRACT.md
invariants. Full runtime state machine implementation to follow.
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.
- Added v2_reinit_audio_device for V2 player device reinitialization
- SettingsView now calls both legacy and V2 reinit commands
- Converts legacy AudioSettings to qbz_audio::AudioSettings for reload
- Added spawn_v2_prefetch helper function
- v2_play_track now prefetches next 2 Qobuz tracks after playback starts
- Respects streaming_only mode (skips prefetch when enabled)
- Uses semaphore to limit concurrent prefetch downloads to 1
- Prefetch works with cache (offline, memory, disk) paths
- Modified login/auto_login commands to also authenticate CoreBridge
- Modified logout command to also logout from CoreBridge
- Removed redundant v2_login call from LoginView.svelte frontend
This ensures both legacy AppState.client and V2 CoreBridge.QbzCore.client
are authenticated in sync, allowing V2 commands to work after login.
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.
Added DEPRECATED headers to src-tauri/src/player/mod.rs and
src-tauri/src/queue/mod.rs indicating these will be removed
after migration to qbz-player crate is complete.
New code should use:
- qbz_player::Player (via CoreBridge -> QbzCore)
- qbz_player::QueueManager (via CoreBridge -> QbzCore)
- 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.
- Use camelCase parameter names (albumId, trackId, artistId) to match
frontend expectations
- Add #[allow(non_snake_case)] to suppress warnings
V2 commands are ready for testing. Full frontend migration pending:
- Search commands need searchType parameter
- Need to add get_user_info, login with full flow, etc.
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.
- Remove duplicate audio module files from src-tauri
- Update audio/mod.rs to re-export from qbz_audio
- Single source of truth for audio types now in qbz-audio crate
- Compilation passes, types are aligned
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.