Returning an empty `icon_name` in `tray_linux_ksni.rs` forces SNI
panels (e.g., KDE Plasma) to render the explicit `IconPixmap` data.
This prevents panels from incorrectly resolving the full-color app icon
from the system theme instead of the intended monochrome tray glyph.
Also updates `src-tauri/icons/icon-symbolic.svg` and regenerates
all `tray-dark-*.png` and `tray-light-*.png` assets.
Adds `static/logo.svg` as the main application logo.
The previous tray-light.png / tray-dark.png were 128x128 RGBA glyphs with a
black radial background. GNOME's StatusNotifierItem path expects monochrome
silhouettes around 22px base size and was rendering ours as the "three dots"
fallback.
Regenerate from icons/icon-symbolic.svg using Inkscape with a corrected
square viewBox (149 1 944 944, centred on the path bbox) and emit four
sizes per theme (22/32/44/64). decode_tray_icons() now returns Vec<Icon>
so ksni can offer the panel the best match for its bar height and HiDPI.
cc
Adds a separate "Resume playback position" toggle in Settings >
Playback. Default is OFF — opening qbz the next morning lands on a
clean 0:00 of the saved track, which is the common preference. Users
who do want bookmark-style resume turn the toggle on.
Independent of "Restore session on startup" in the UI (top-level
toggle, not a sub-setting). The seek only applies when both are on:
without persistSession there's no saved track to resume to, so the
flag is inert. Keeping the toggles separate matches the Settings UX
elsewhere and lets users flip persistence on without inheriting the
old #317 behavior we never had.
Backend
-------
- New `PlaybackPreferences::resume_playback_position: bool` (default
false). Migration adds the column to the existing
`playback_preferences.db` and the default row gets 0; existing
installs upgrade transparently.
- Setter `set_resume_playback_position(bool)` on both
`PlaybackPreferencesStore` and the wrapping `State`.
- Tauri command `v2_set_resume_playback_position` registered in
lib.rs invoke handler. Get-side already reads the new field via
the existing `v2_get_playback_preferences`.
Frontend
--------
- `playbackPreferencesStore.ts` — new field on the type and cache,
new `setResumePlaybackPosition()` exported.
- `SettingsView.svelte` — separate toggle row right below
"Restore session on startup" (matches the existing Settings rhythm).
- `playerStore.ts` — `pendingSessionRestore` extended with optional
`positionSecs`. When present, after the restored track loads, the
position is queued through the existing `pendingSeekPosition` /
`flushPendingSeek` path so the seek runs against an already-loaded
stream and respects the in-flight guard. Skip the queue when the
saved position is at or near 0 (the load already starts there).
- `routes/+page.svelte` — at session restore, reads
`getCachedPreferences().resume_playback_position` and forwards
`session.current_position_secs` only when the user opted in. The
field has been persisted in the schema since v1.2.5 — what changes
is whether we read it back.
i18n
----
- New keys in 5 locales: `settings.playback.resumePlaybackPosition` /
`…Desc`.
- `persistSessionDesc` updated everywhere — it used to claim the
feature restored playback position, which was always misleading
(the code restored the track but paused at 0:00). The text now
matches what the toggle does on its own: queue + view, paused at
0:00. Resume-to-position becomes the explicit ask of the new toggle.
Tests
-----
- Backend: 196 tests pass. Frontend: 0 TS errors.
cc
Qobuz returns a separate `version` field for tracks belonging to remix
albums, reissues, and deluxe editions (e.g. "Player's Ball Mix",
"Nine Inch Noize Version", "Remastered 2024"). The mobile app shows it
parenthesized after the title so remix albums are distinguishable from
originals; qbz only showed the bare title, making remix albums
impossible to navigate (issue #360).
Backend
-------
- Add `version: Option<String>` to qbz_models::Track (the V2 model
consumed by qbz-qobuz/qbz-library) and to api::models::Track (the
legacy struct still used by some V2 command paths).
- Add `version: Option<String>` to qbz_models::QueueTrack so the field
travels through the local queue, persistence, QConnect remote queue,
and frontend snapshots without being lost in conversion.
- Propagate `version` in every Track→QueueTrack converter:
qconnect::model_track_to_core_queue_track,
link_resolver::track_to_queue_track_from_api,
mixtape::enqueue::track_to_queue_track_from_api and the inline
builders. Plex/local builders set version=None (no edition concept
outside Qobuz).
- V2QueueTrack (the wire shape exposed to the frontend) gains the
field with serde default so older clients keep deserializing.
Frontend
--------
- New `formatTrackTitle(track)` helper in src/lib/utils/trackTitle.ts:
returns `title` when version is empty, else `title (version)`.
- Add `version?: string | null` to QobuzTrack, Track (UI), PlaylistTrack,
DisplayTrack, BackendQueueTrack, QueueTrack (UI), and PlayingTrack
so the field flows from the wire to the renderer.
- Propagate `version` through the four buildQueueTrackFrom* builders
in trackActions.ts and through the QobuzAlbum→AlbumDetail adapter.
- Use formatTrackTitle in the high-traffic renderers:
- NowPlayingBar (currentTrack derived in routes/+page.svelte)
- QueuePanel: now-playing label, upcoming-track rows, history rows
- VirtualizedTrackList: default getTrackTitle accessor
- AlbumDetailView: TrackRow title binding
- PlaylistDetailView: TrackRow title binding
- SearchView: popular-track card label and the two track-list
rows (Tracks tab + cross-search "All" view)
Tests
-----
- 196 lib tests pass (no behavior change for the no-version path).
- npm run check: 0 errors.
Follow-up: more views still pass `{track.title}` directly (HomeView,
ArtistDetailView, FavoritesView, ForYouTab, MixtapeCollectionDetailView,
LocalLibraryView, the various Suggest/Top/Label views, the immersive
panels, the miniplayer surface, and the Track*Modal stack). They all
render UI Track-shaped objects that now carry version, so each is a
mechanical "import formatTrackTitle + replace {track.title} with
{formatTrackTitle(track)}". Doing them in a follow-up commit to keep
this one reviewable.
cc
The final extraction of Alcance A. Move the public service facade
(QconnectServiceState struct + impl + Default impl, plus the spawned
event-loop task, the renderer/queue snapshot queries, the controller-
handoff helpers, send_command/send_renderer_report, the deferred
renderer-join plumbing, and the bootstrap_remote_presence pending-
action machinery) from mod.rs into a dedicated service submodule.
Items moved:
- struct QconnectRuntime, QconnectServiceInner
- pub struct QconnectServiceState (re-exported via `pub use`)
- impl QconnectServiceState (~50 methods) and impl Default
- bootstrap_remote_presence, deferred_renderer_join,
clear_pending_if_matches
- queue_payload_track_preview, update_lifecycle_state_if_running,
emit_qconnect_diagnostic (private helpers used only by the service)
- Constants: JOIN_SESSION_REASON_CONTROLLER_REQUEST,
QCONNECT_PLAY_TRACK_HANDOFF_WAIT_MS,
QCONNECT_PLAY_TRACK_HANDOFF_POLL_MS
All methods on QconnectServiceState are pub(super) so the commands
submodule (which was already extracted) can dispatch through them.
The Default impl uses plain `fn default()` (visibility qualifiers are
not permitted in trait impls).
mod.rs now contains only the cross-submodule glue (~140 lines):
shared constants (PLAYING_STATE_*, AUDIO_QUALITY_*, BUFFER_STATE_OK,
DEFAULT_QCONNECT_CHANNEL_COUNT, QCONNECT_REMOTE_QUEUE_SOURCE), the
QconnectRemoteSyncState accumulator, qconnect_now_ms,
qconnect_repeat_mode_from_loop_mode, normalize_volume_to_fraction,
model_track_to_core_queue_track, and the module declarations.
session.rs and commands.rs imports re-pathed to point at submodules
directly (super::transport::*, super::queue_resolution::*,
super::session::*, super::service::*) instead of going through
re-exports in mod.rs.
mod.rs: 1820 → 142 lines (-92%). service.rs: 1689 lines.
Final layout:
src-tauri/src/qconnect/
├── mod.rs ( 142) module glue
├── types.rs ( 273) wire types, payloads
├── session.rs ( 374) topology, renderer snapshots
├── queue_resolution.rs ( 481) cursors, shuffle order
├── corebridge.rs ( 566) apply_renderer_command, materialize
├── track_loading.rs ( 337) streaming, FLAC probe, dedup
├── event_sink.rs ( 461) QconnectEventSink + dispatch
├── transport.rs ( 386) credentials, device identity
├── service.rs (1689) QconnectServiceState facade
├── commands.rs ( 849) Tauri invoke handlers
└── tests.rs (1044) 37 unit tests
From 6371 lines in 1 monolith → 6602 lines across 11 focused files,
each with a single clear responsibility. mod.rs went 6371 → 142 lines
(-98%). All 37 tests pass. Alcance A complete; the codebase is ready
for unified-binary Phase 0 to resume.
cc
Move the 37 unit tests (1044 lines, all #[cfg(test)] inside the old
mod tests {} block in mod.rs) into a dedicated tests.rs submodule.
mod.rs keeps only the declaration `#[cfg(test)] mod tests;`.
Test imports are unchanged — they still use `super::*` paths into the
focused submodules established in steps 2-9, which is exactly the
intended layout.
This concludes Alcance A of the qconnect refactor. Full layout:
src-tauri/src/qconnect/
├── mod.rs (1820 lines) — QconnectServiceState impl,
│ glue, internal structs
├── types.rs ( 273 lines) — wire types, request payloads
├── session.rs ( 374 lines) — topology, renderer snapshots
├── queue_resolution.rs ( 481 lines) — cursors, indices, shuffle order
├── corebridge.rs ( 566 lines) — apply_renderer_command,
│ materialize, projection
├── track_loading.rs ( 337 lines) — streaming, FLAC probe, dedup
├── event_sink.rs ( 461 lines) — QconnectEventSink + dispatch
├── transport.rs ( 386 lines) — credentials, device identity
├── commands.rs ( 849 lines) — Tauri invoke handlers + report
│ helpers
└── tests.rs (1044 lines) — 37 unit tests
From 6371 lines in 1 monolith → 7591 lines across 10 focused files
with clear responsibility boundaries. mod.rs went 6371 → 1820 (-71%).
Each submodule is independently navigable; visibility is constrained
via pub(super) so re-exports stay intentional. All 37 tests pass.
cc
Move all 30 #[tauri::command] handlers (registered in lib.rs's
invoke_handler!) plus the renderer-report helpers they own into a
dedicated commands submodule:
Tauri commands (re-exported via `pub use commands::*` so lib.rs and
external callers keep their import paths unchanged):
- v2_qconnect_connect, _disconnect, _status, _send_command
- v2_qconnect_evaluate_queue_admission,
_send_command_with_admission, _join_session, _set_player_state
- v2_qconnect_toggle_play_if_remote, _play_track_if_remote,
_skip_next_if_remote, _skip_previous_if_remote,
_set_volume_if_remote, _mute_if_remote, _stop_if_remote,
_toggle_shuffle_if_remote, _cycle_repeat_if_remote,
_set_autoplay_mode_if_remote, _autoplay_load_tracks_if_remote
- v2_qconnect_set_active_renderer, _set_volume, _set_loop_mode,
_mute_volume, _set_max_audio_quality, _ask_for_renderer_state
- v2_qconnect_queue_snapshot, _renderer_snapshot, _session_snapshot
- v2_qconnect_report_playback_state, _report_volume
- v2_qconnect_get_device_name, _set_device_name
- v2_get_hostname
Renderer-report helpers moved alongside the report command:
- classify_qconnect_audio_quality,
build_qconnect_file_audio_quality_snapshot,
resolve_active_playback_audio_quality
- should_skip_renderer_report_due_to_stale_snapshot,
determine_queue_lookup_report_strategy,
should_report_queue_item_ids_for_renderer_state
mod.rs deps that commands.rs reaches via super:: are now pub(super):
- BUFFER_STATE_OK, DEFAULT_QCONNECT_CHANNEL_COUNT
- AUDIO_QUALITY_UNKNOWN/CD/HIRES_LEVEL1/LEVEL3 (the rest were
already pub(super))
- QconnectServiceState.custom_device_name field
Test imports re-pathed to super::commands::* for the helpers that
moved with the report command.
Pure relocation. mod.rs: 3673 → 2865 lines. commands.rs: 847 lines.
All 37 qconnect tests pass.
cc
Steps 4-8 moved items into focused submodules but only verified `cargo
check` (lib only) — tests under #[cfg(test)] in mod.rs continued
referring to items via plain `super::FunctionName`, which only resolved
when the items were still in mod.rs.
This commit re-paths the test imports to point at their actual
submodules:
- decode_hex_channel, parse_subscribe_channels, default_qconnect_device_info
→ super::transport::*
- find_unique_renderer_id, refresh_local_renderer_id,
QconnectFileAudioQualitySnapshot → super::session::*
- resolve_controller_queue_item_from_snapshots,
resolve_queue_item_ids_from_queue_state, QconnectRemoteSkipDirection,
resolve_core_shuffle_order, QconnectControllerQueueItemResolution
→ super::queue_resolution::*
- build_session_renderer_snapshot → super::session::*
- should_reload_remote_track → super::track_loading::*
- queue_state_needs_materialization → super::corebridge::*
Promoted decode_hex_channel and parse_subscribe_channels to pub(super)
in transport.rs, and should_reload_remote_track to pub(super) in
track_loading.rs so the tests can reach them across the submodule
boundary.
All 37 qconnect tests pass.
cc
Move the Tauri-side event sink (struct TauriQconnectEventSink + the
QconnectEventSink trait impl + the apply_session_management_event
dispatcher) into a dedicated event_sink submodule.
This is the seam between the qconnect-app crate's event stream and
qbz's local state — every WS-driven event (renderer updated, queue
updated, command applied, session-management messages) flows through
on_event. Splitting it out makes the dispatch graph navigable
without scrolling through 4000+ unrelated lines.
apply_session_management_event handles 9 message types:
SESSION_STATE, ADD_RENDERER, UPDATE_RENDERER, REMOVE_RENDERER,
ACTIVE_RENDERER_CHANGED, RENDERER_STATE_UPDATED, VOLUME_CHANGED,
VOLUME_MUTED, MAX_AUDIO_QUALITY_CHANGED, LOOP_MODE_SET — plus three
post-dispatch hooks (apply_remote_loop_mode_to_corebridge,
sync_local_playback_for_renderer_ownership, sync_active_renderer_
projection).
mod.rs:
- TauriQconnectEventSink moved to event_sink module
- qconnect_now_ms marked pub(super) for cross-module access
- async_trait, qconnect_app::{QconnectAppEvent, QconnectEventSink}
imports removed (now confined to event_sink.rs)
- The session/queue_resolution imports trimmed to only what mod.rs
itself still references
Pure relocation. mod.rs: 4460 → 4022 lines. event_sink.rs: 425 lines.
Total qconnect/ submodules now: 7 (types, session, queue_resolution,
corebridge, track_loading, event_sink, plus the residual mod.rs).
cc
Move 6 functions that bridge cloud-emitted QConnect events to the
local CoreBridge audio engine into a dedicated corebridge submodule:
- queue_state_needs_materialization (dedup comparator)
- apply_remote_loop_mode_to_corebridge
- apply_renderer_command_to_corebridge (SetState/SetVolume/MuteVolume/
SetLoopMode/SetActive handler — ~180 lines, the busiest function)
- sync_corebridge_remote_shuffle_projection (shuffle projection with
identity-shuffle deferral; remains private to corebridge)
- materialize_remote_queue_to_corebridge (queue replication, ~210 lines)
- align_corebridge_queue_cursor
mod.rs deps that corebridge.rs reaches via super:: are now pub(super):
- PLAYING_STATE_PLAYING/PAUSED/STOPPED/UNKNOWN constants
- normalize_volume_to_fraction, qconnect_repeat_mode_from_loop_mode,
model_track_to_core_queue_track
- ensure_remote_track_loaded (still in mod.rs, will move to
track_loading.rs in a follow-up step)
corebridge.rs imports queue_resolution::* directly for the cursor and
order helpers, and session::is_peer_renderer_active for the echo-reset
gate.
Pure relocation. mod.rs: 5304 → 4777 lines. corebridge.rs: 565 lines.
cc
Move 22 public types (QconnectConnectOptions, QconnectLifecycleState,
QconnectConnectionStatus, QconnectVisibleQueueProjection, the 20-variant
QconnectOutboundCommandType + 5 request payload structs, the 4 admission/
handoff types, the 8 device/session/queue payloads) into a dedicated
types.rs submodule. mod.rs re-exports via `pub use types::*` so external
callers (lib.rs invoke handlers, commands_v2/queue.rs) keep their import
paths.
Pure data and enum-mapping impls only. Two helper methods on
QconnectOutboundCommandType (to_queue_command_type,
requires_remote_queue_admission) become pub(super) since they are only
used from within the qconnect module tree.
mod.rs: 6371 → 6109 lines. types.rs: 273 lines.
cc
Pure rename to enable subsequent splits into focused submodules. No
behavior change: 34 invoke handler refs in lib.rs and 2 use statements
in commands_v2/queue.rs updated from qconnect_service:: to qconnect::.
Foundation for the qconnect_service.rs → qconnect/ module split: the
file is 6371 lines and mixes 12 distinct responsibilities (types,
session topology, event sink, queue resolution, corebridge bridging,
track loading, transport, Tauri commands, etc). Splitting into focused
submodules makes the area navigable for both authors and reviewers,
enables independent reasoning per concern, and creates a clean target
for the eventual qbz-qconnect-proto crate extraction.
cc
Bundle of related fixes addressing QConnect echo handling and shuffle
authority. All four converge on the same architectural principle:
qbz must not invent state when the cloud is the source of truth.
1. Remove should_force_remote_track_restart and its echo-driven reload
The function fired whenever the cloud echoed SetState with
target_position_ms=0 for the same track currently playing. The cloud
re-emits this routinely on secondary field changes (next_track
corrections, queue_item_id refresh) — every echo triggered a
load_remote_track_into_player that interrupted in-progress audio.
Track-change cases continue to load via align_corebridge_queue_cursor
plus ensure_remote_track_loaded.
2. Reject echo seeks in apply_renderer_command_to_corebridge
Authoritative seek-to-zero is only honored when the command targets
a different track, or when local position is already near zero. If
command.current_track matches playback_state.track_id and target<=1s
while local_pos>2s, the seek is treated as echo and skipped. The
prior 2s threshold was insufficient to absorb cloud round-trip during
queue changes.
3. Drop has_loaded_audio gate from should_reload_remote_track
playback_state.track_id is updated by the audio thread only after
engine.append(source) succeeds. During the 1-3s buffer/decode window
of an initial load, has_loaded_audio is false even though loading is
in flight — the prior gate caused ensure_remote_track_loaded to
redundantly re-trigger load on every echo SetState arriving in that
window. This was the residual first-track hiccup.
4. Add load-attempt dedup across V2 play and ensure_remote_track_loaded
New sync_state.last_load_attempt: Option<(track_id, Instant)> with a
5s window. Two write sites:
- play_remote_renderer_track_if_active when active_renderer_is_local
(V2 frontend just dispatched a local play)
- ensure_remote_track_loaded when it triggers its own load
Read site: ensure_remote_track_loaded skips if last attempt for the
same track was within the window. Eliminates redundant loads when an
echo SetState arrives while a local play is still buffering.
5. Defer engine shuffle activation until cloud sends a valid order
The cloud broadcasts shuffle toggles in two stages: first a
QueueUpdated with shuffle_mode=true and shuffle_order=null, then
~400ms later a QueueUpdated with the computed shuffle_order. The
prior code applied set_queue_with_order with shuffle_enabled=true on
the first event, falling through to set_identity_shuffle_order_internal
— qbz inventing a sequence (0,1,2,...) that diverges from the order
the cloud is about to authorize. If the second event was delayed,
dropped, or raced against the first, qbz drifted from peer clients
(the long-standing "doble shuffle" symptom).
Now: effective_shuffle_enabled = shuffle_mode && core_shuffle_order.
is_some(). When the order is absent, the engine stays unshuffled and
waits for the second QueueUpdated. sync_corebridge_remote_shuffle_
projection short-circuits identically. Updated diagnostic log
surfaces remote_shuffle_mode, shuffle_order_present, and engine_
shuffle_enabled separately for future triage.
Tests: removed should_force_remote_track_restart test, simplified
should_reload_remote_track test to reflect the track-id-only contract.
cc
The inner `pub mod commands` block in network/mod.rs hosted three
unregistered Tauri commands (get_network_mounts_cmd,
check_mount_accessible, check_network_paths_batch) and was uncalled.
The non-command helpers in the parent module (get_network_mounts,
check_mount_accessibility, find_mount_for_path, parse_mount_info)
are also no longer referenced now that the commands are gone, but
they remain pub for potential future use without warnings.
- Remove the inner `pub mod commands { ... }` block.
- Drop the now-unused std::collections::HashMap import.
cc
src-tauri/src/commands/user_session.rs hosted three unregistered
#[tauri::command] entries (activate_user_session, deactivate_user_session,
get_last_user_id) plus two non-command helpers (init_type_alias_state,
teardown_type_alias_state) consumed only by session_lifecycle.rs.
- Move init_type_alias_state and teardown_type_alias_state into
session_lifecycle.rs (their only caller) as plain pub helpers.
- Delete src-tauri/src/commands/user_session.rs and
src-tauri/src/commands/mod.rs.
- Remove pub mod commands; from lib.rs — the legacy commands
namespace is now empty.
- Refresh the activate_session doc comment to match the new layout.
cc
The 1,375-line library/commands.rs hosted only unregistered Tauri
commands (54 of them) plus a path-compat pub use of state types.
Every #[tauri::command] in the file is V2-replaced and uncalled.
The state-types pub use was the only external surface and is
already exported canonically from library/mod.rs.
- Delete src-tauri/src/library/commands.rs entirely
- Drop pub mod commands; from library/mod.rs
- Update session_lifecycle.rs to use crate::library::LibraryState
(the canonical path) instead of crate::library::commands::LibraryState
- Refresh the doc comment in library/state.rs to reflect the
post-cleanup layout
cc
All 17 #[tauri::command] entries in offline_cache/commands.rs are
unregistered (V2 owns the live cache commands) and uncalled. The only
surviving consumer was session_lifecycle.rs reaching for
purge_all_cached_files (a non-command helper).
- Move purge_all_cached_files to a new offline_cache/purge.rs module
(drops the LibraryState path through library::commands; uses the
canonical crate::library::LibraryState directly)
- Delete offline_cache/commands.rs along with its dead siblings:
cache_track_for_offline + post_process_cached_track,
is_track_cached, get_cached_track_path, get_cached_track,
get_cached_tracks, get_offline_cache_stats, remove_cached_track,
clear_offline_cache + cleanup_empty_folder, set_offline_cache_limit
+ evict_if_needed, open_offline_cache_folder, open_album_folder,
open_track_folder, check_offline_root_mounted, validate_offline_path,
move_offline_cache_to_path, detect_legacy_cached_files (the command
shim — the helper of the same name lives in migration.rs and is
still consumed by V2), start_legacy_migration,
sync_offline_cache_to_library + SyncResult.
- Update session_lifecycle.rs to import from offline_cache::purge.
cc
The reco_store/commands.rs module no longer hosts any registered Tauri
commands. Its surviving content is purely helper logic consumed by V2
home recommendations in commands_v2/library.rs.
- Rename reco_store/commands.rs -> reco_store/helpers.rs
- Drop the unregistered/uncalled #[tauri::command] entries
(reco_log_event, reco_train_scores, get_playlist_suggestions,
reco_backfill_genres, reco_needs_genre_backfill) and their dead
scoring helpers (build_scores, build_track_entries, decay_factor,
current_timestamp, etc.) along with RecoTrainStats / RecoScoreCounts
/ BackfillStats structs that only those commands referenced.
- Update commands_v2/library.rs to import from
crate::reco_store::helpers::* (still uses get_home_seeds_internal,
resolve_albums, resolve_tracks, resolve_artists, album_to_card_meta).
cc
The Tauri commands in cast/commands.rs and cast/dlna/commands.rs are
unregistered (handler list is V2-only) and uncalled. The CastState /
DlnaState path-compat re-exports are also unused (canonical paths are
crate::cast::CastState and crate::cast::dlna::DlnaState). Remove the
files entirely and drop pub mod declarations.
cc
The last consumer (api_server REST handlers) was migrated to
commands_v2::v2_play_track / v2_seek / v2_set_volume in the previous
commit, so the file's three remaining #[tauri::command] functions
(play_track, seek, set_volume) plus the unregistered prefetch_track,
get_playback_state, and all helper symbols (parse_quality,
limit_quality_for_device, PlayTrackResult, StreamInfo, download_audio,
get_stream_info, extract_audio_format_from_header, download_and_stream,
spawn_prefetch, prefetch constants, PREFETCH_SEMAPHORE) had zero
remaining callers.
Drops the `pub mod playback;` and `pub use playback::*;` lines from
commands/mod.rs. Only user_session.rs remains in the legacy commands
module.
cc
Switches the REST endpoints serving the PWA remote control from the
legacy commands::playback::* helpers to their V2 equivalents:
- play_queue_track / play_queue_index / play_album:
commands::playback::play_track -> commands_v2::v2_play_track
- seek: commands::playback::seek -> commands_v2::v2_seek
- set_volume: commands::playback::set_volume -> commands_v2::v2_set_volume
The HTTP contract (paths, methods, payload shapes, response codes,
emit_playback_update side-effects) is unchanged. Behaviour gains the
V2 features that legacy lacked (CMAF cache hits, ALSA hardware
sample-rate guard, manual-offline-mode awareness) because the PWA
remote now flows through the same playback path as the desktop UI.
Drops the `commands` import from this module (now only `commands_v2`
is used) and adds the State acquisitions needed by v2_play_track:
CoreBridgeState, OfflineState, RuntimeManagerState.
cc
Inlines bodies of get_offline_status, get_offline_settings,
set_manual_offline, check_network, and get_pending_playlists from
offline::commands directly into the v2 wrappers in commands_v2/library.rs.
Deletes the entire offline::commands submodule (the inlined commands and
the additional dead Tauri command shims that lived alongside them — none
were registered with Tauri or referenced outside the submodule). Prunes
the now-unused tauri AppHandle/Emitter imports from offline/mod.rs.
Step 8 of legacy cleanup, group H.
cc
Inlines bodies of reco_get_home, reco_get_home_ml, and reco_get_home_resolved
from reco_store/commands.rs directly into the v2 wrappers in
commands_v2/library.rs. Promotes their shared helpers
(get_home_seeds_internal, resolve_tracks, resolve_artists,
album_to_card_meta) from private to pub(crate) so the v2 wrappers can
call them without crossing back into a legacy command module.
Deletes the corresponding legacy #[tauri::command] definitions and
prunes now-unused imports from reco_store/commands.rs.
Step 8 of legacy cleanup, group G.
cc
Inlines bodies of playlist_get_custom_order, playlist_has_custom_order,
and playlist_get_tracks_with_local_copies from library/commands.rs
directly into the v2 wrappers in commands_v2/playlists.rs, mapping
LibraryError to RuntimeError::Internal as needed.
Deletes the corresponding legacy #[tauri::command] definitions and
removes their re-exports from library/mod.rs.
Step 8 of legacy cleanup, group F.
cc
Inlines bodies of library_get_album_tracks, library_get_tracks_by_ids,
and update_playlist_folder from library/commands.rs directly into the
v2 wrappers in commands_v2/library.rs.
Deletes the corresponding legacy #[tauri::command] definitions and
removes their re-exports from library/mod.rs.
Step 8 of legacy cleanup, group E.
cc
Inlines bodies of library_fetch_missing_artwork, library_get_thumbnail,
and library_get_thumbnails_cache_size from library/commands.rs directly
into the v2 wrappers in commands_v2/library.rs.
Deletes the corresponding legacy #[tauri::command] definitions and the
unused legacy library_clear_thumbnails command. Removes their re-exports
from library/mod.rs.
Step 8 of legacy cleanup, group D.
cc
Inlines bodies of library_get_scan_progress, library_get_artists,
library_search, and library_get_stats from library/commands.rs directly
into the v2 wrappers in commands_v2/library.rs.
Deletes the corresponding legacy #[tauri::command] definitions and
removes their re-exports from library/mod.rs.
Step 8 of legacy cleanup, group C.
cc
Inlines bodies of library_add_folder, library_cleanup_missing_files,
library_get_folders, and library_get_folders_with_metadata from
library/commands.rs directly into the v2 wrappers in commands_v2/library.rs.
Deletes the corresponding legacy #[tauri::command] definitions and
removes their re-exports from library/mod.rs.
Step 8 of legacy cleanup, group B.
cc
Inlines bodies of library_scan_impl, library_stop_scan_impl,
library_scan_folder_impl, library_clear_impl, library_update_album_metadata_impl,
library_write_album_metadata_to_files_impl, and library_refresh_album_metadata_from_files_impl
from library/commands.rs directly into the v2 wrappers in commands_v2/legacy_compat.rs.
Moves their helpers (process_cue_file, apply_sidecar_override_if_present,
compute_track_artist_match, normalize_library_path) alongside the inlined bodies.
Deletes the corresponding legacy #[tauri::command] _impl definitions and
the dead public command siblings (library_scan, library_stop_scan,
library_scan_folder, library_clear, library_update_album_metadata,
library_write_album_metadata_to_files, library_refresh_album_metadata_from_files)
that were only ever called from the deleted _impl variants.
Updates library/mod.rs to drop the corresponding re-exports.
Step 8 of legacy cleanup, group A.
cc
Inlines `check_network_path` from crate::network::commands into
commands_v2/legacy_compat.rs. The legacy command was a one-line wrapper
over crate::network::is_network_path; the inlined v2 form calls
is_network_path directly with no behavior change.
Deletes the legacy #[tauri::command] definition in network/mod.rs.
Other tauri commands inside crate::network::commands remain (they have
no v2 delegates and keep being exported as their own legacy commands;
they will be addressed in a separate cleanup step if needed).
No frontend invoke changes required.
Step 6 of the legacy cleanup plan, group G (network path).
cc
Inlines the bodies of `library_update_folder_settings` and
`get_track_by_path` from crate::library::commands::* directly into
commands_v2/legacy_compat.rs. Maps the legacy String error type to
RuntimeError::Internal at every return path.
Deletes the legacy #[tauri::command] definitions in
library/commands.rs. The data-layer methods on LibraryDatabase
(update_folder_settings, get_folder_by_id, get_track_by_path) are
unchanged.
No frontend invoke changes required.
Step 6 of the legacy cleanup plan, group F (folder settings + track
lookup).
cc
Inlines the body of `library_backfill_downloads` from
crate::library::commands::* directly into commands_v2/legacy_compat.rs,
mapping the legacy String error type to RuntimeError::Internal at every
return path. Adjusts the v2 return type to refer to
crate::library::BackfillReport (the canonical re-export from
qbz-library) instead of the deleted crate::library::commands path.
Deletes the legacy #[tauri::command] definition in
library/commands.rs. MetadataExtractor and BackfillReport are reused
unchanged from their crate::library re-exports.
No frontend invoke changes required.
Step 6 of the legacy cleanup plan, group E (backfill downloads).
cc
Inlines the bodies of `check_album_fully_cached` and
`check_albums_fully_cached_batch` from
crate::offline_cache::commands::* directly into
commands_v2/legacy_compat.rs, mapping the legacy String error type to
RuntimeError::Internal at the v2 boundary.
Note: v2_cache_notification_artwork is NOT a delegate — it lives in
commands_v2/helpers.rs and is a true helper used by other v2 commands.
The investigation list flagged it as a possible delegate, but
verification showed it owns its implementation.
Deletes the legacy #[tauri::command] definitions in
offline_cache/commands.rs. Other functions in the offline_cache::commands
module remain (still called from commands_v2/library.rs delegates,
which are step 8 scope).
No frontend invoke changes required.
Step 6 of the legacy cleanup plan, group D (offline cache).
cc
Inlines the bodies of `create_pending_playlist`,
`get_pending_playlist_count`, `queue_scrobble`, `get_queued_scrobbles`,
`get_queued_scrobble_count`, and `cleanup_sent_scrobbles` from
crate::offline::commands::* directly into commands_v2/legacy_compat.rs.
The legacy bodies were uniform: lock state.store, unwrap optional store,
delegate to a method on the store struct. Inlined verbatim with the
String error type mapped to RuntimeError::Internal at each return path.
The store-side OfflineStore methods (data layer) are untouched.
Deletes the legacy #[tauri::command] definitions in offline/mod.rs.
Other functions in the offline::commands module remain (still called
from commands_v2/library.rs delegates, which are step 8 scope).
No frontend invoke changes required.
Step 6 of the legacy cleanup plan, group C (offline scrobble/playlist).
cc
Inlines the bodies of `discogs_has_credentials`,
`discogs_search_artwork`, and `discogs_download_artwork` from
crate::library::commands::* directly into commands_v2/legacy_compat.rs.
The discogs delegates were single-line bodies (or trivial proxy logic);
DiscogsClient and library::get_artwork_cache_dir are reused as-is.
The String error type from the underlying DiscogsClient methods is
mapped to RuntimeError::Internal at the v2 boundary, matching the
existing v2 contracts.
Deletes the legacy #[tauri::command] definitions from
library/commands.rs. No frontend invoke changes required.
Step 6 of the legacy cleanup plan, group B (discogs).
cc
Inlines the bodies of `lyrics_get` and `lyrics_get_cache_stats` from
crate::lyrics::commands::* directly into commands_v2/legacy_compat.rs,
mapping the legacy String error type to RuntimeError::Internal at the
return boundary for v2_lyrics_get. Renames LyricsCacheStats to
V2LyricsCacheStats, hosted next to the inlined v2_lyrics_get_cache_stats.
Deletes lyrics/commands.rs entirely (also drops the dead
lyrics_clear_cache helper which had no callers; v2_lyrics_clear_cache in
commands_v2/library.rs already inlines its own logic).
The v2 functions retain their existing names, parameter shapes, and
Tauri::State signatures; only the body changes. No frontend invoke
changes required.
Step 6 of the legacy cleanup plan, group A (lyrics).
cc
Relocates CastState (struct + impl) from cast/commands.rs to
cast/state.rs, and DlnaState (struct + impl) from
cast/dlna/commands.rs to cast/dlna/state.rs.
Both types are consumed by commands_v2/library.rs (and the legacy
Tauri commands themselves) via the existing re-exports in
cast/mod.rs and cast/dlna/mod.rs. The re-exports are updated to
source from the new state modules, and each commands.rs re-exports
its state type so historical
crate::cast::commands::CastState / crate::cast::dlna::commands::DlnaState
paths continue to compile.
This unblocks future deletion of cast/commands.rs and
cast/dlna/commands.rs in a later cleanup step (the
#[tauri::command] functions in those files are already
unregistered and have zero Rust call sites).
cc
Relocates LibraryState (struct + impl), CleanupResult, BackfillReport,
LibraryAlbumMetadataUpdateRequest, and LibraryAlbumTrackMetadataUpdate
from library/commands.rs to a new library/state.rs module.
These types are consumed by commands_v2/library.rs,
commands_v2/legacy_compat.rs, commands_v2/mixtapes.rs,
commands_v2/link_resolver.rs, cast/commands.rs, and
session_lifecycle.rs. The existing pub use re-exports in
library/mod.rs are updated to source from state, and commands.rs
re-exports them in turn so historical
crate::library::commands::* paths (still used by
commands_v2/legacy_compat.rs, which is out of scope for this task)
continue to compile.
This unblocks future deletion of library/commands.rs once the
remaining #[tauri::command] definitions there are inlined into
their commands_v2 callers (separate task).
No serde shape changes; struct fields, derives and serde
attributes are preserved verbatim, so the JSON the frontend
receives is identical.
cc
Removes 11 #[tauri::command] functions that are unregistered and have
zero Rust call sites:
- pause_playback, resume_playback, stop_playback
- play_next_gapless, set_media_metadata
- get_audio_devices, reinit_audio_device
- get_audio_output_status, get_pipewire_sinks
- set_pipewire_default_sink, debug_get_cpal_devices
play_track, seek, and set_volume are preserved - they are still
consumed by src-tauri/src/api_server/mod.rs (REST endpoints for
the PWA remote). They will be migrated to call commands_v2/playback
or core_bridge in a later step.
Helper functions used solely by the deleted commands are also
removed: AudioDevice, AudioOutputStatus, PipewireSink structs,
get_pipewire_device_descriptions (linux + non-linux), and
cpal_device_name. Helpers reachable from play_track / seek /
set_volume / prefetch_track / get_playback_state (parse_quality,
limit_quality_for_device, download_audio, get_stream_info,
extract_audio_format_from_header, download_and_stream,
spawn_prefetch, prefetch constants and PREFETCH_SEMAPHORE) are
preserved.
cc
All 20 #[tauri::command] functions in this file are unregistered
(no longer in invoke_handler) and have zero Rust call sites. V2
equivalents in commands_v2/search.rs and commands_v2/catalog.rs
cover all use cases.
Removed:
- search_albums, search_tracks, search_artists, search_all
- get_album, get_featured_albums, get_track, get_artist
- get_similar_artists, get_genres
- get_discover_index, get_discover_playlists, get_playlist_tags, get_discover_albums
- get_artist_page, get_artist_basic, get_artist_detail
- get_artist_tracks, get_artist_albums, get_releases_grid
The corresponding pub mod search; declaration in commands/mod.rs is
also removed.
The SearchAllResults / MostPopularItem types (used by api_server's
REST /search/all endpoint, the PWA remote control surface) are
relocated into api_server/mod.rs since that is the only remaining
consumer; this avoids leaving a near-empty file just to host two
serde DTOs.
cc
These commands had v2_* equivalents registered alongside them; the
frontend has been migrated and no longer invokes them. The legacy
function definitions remain in their respective modules - they will
be removed in subsequent steps once we verify nothing internally
calls them.
cc
Path C: legacy CPAL output-sink enumeration was inlined directly in
commands/playback.rs (cfg-gated Linux + non-Linux variants), with
no equivalent in any domain crate. Extract a domain function
`qbz_audio::output_sinks::list_output_sinks` that returns the same
{name, description, volume, is_default} shape (mirrored as the new
public `OutputSinkInfo` struct) for both Linux and non-Linux
platforms, preserving the original logging cadence and the rule
that name == CPAL device name (so saved IDs reopen correctly).
Add v2_get_pipewire_sinks in commands_v2/audio.rs that calls into
qbz_audio directly — no `crate::commands::*` delegation. Updates the
two frontend call sites (AudioOutputBadges.loadStatus and
SettingsView.loadAudioDevices). Legacy command in
commands/playback.rs is preserved untouched; the audio-protected
files (pipewire_backend.rs, audio_settings.rs, player init_device)
are not modified.
cc
Path B: legacy command read state.player.state.current_device() and
state.player.state.is_playing() — both methods already exist on the
qbz-player Player.state in the V2 architecture. No domain extraction
needed; the v2 command reads them via CoreBridge.player() like the
existing v2_get_hardware_audio_status does.
Adds v2_get_audio_output_status returning V2AudioOutputStatus
(serialized to the same {device_name, is_playing} shape so the
AudioOutputBadges frontend type is unchanged). Updates both
AudioOutputBadges call sites (initial loadStatus + the 1s polling
interval). The legacy command in commands/playback.rs is preserved
untouched.
cc
Path C: legacy command called client.get_releases_grid for the
artist detail view's release-type pagination grid. No existing
v2 equivalent, so add v2_get_releases_grid via QbzCore ->
QobuzClient::get_releases_grid directly.
cc
Path C: legacy command called client.get_artist_tracks (the
/artist/get?extra=tracks path) and returned a TracksContainer.
No existing v2 equivalent, so add v2_get_artist_tracks via
QbzCore -> QobuzClient::get_artist_tracks directly.
cc