Skip to content

Commit 5a31f3c

Browse files
authored
Custom playback integration phase 1: add CustomQueue scaffolding (#959)
## Add `CustomQueue` scaffolding (Phase 1 — Foundation) Phase 1 of the custom playback integration. Pure scaffolding with **zero behavioral change** — all existing codepaths remain untouched. ### Motivation Issues #766, #878, #378, #767, #153, #739 all stem from the same root cause: the app delegates queue management to Spotify/librespot, which only exposes a ~100-track window. The custom queue system will replace this with app-managed batched URIs playback when streaming is active ### Changes **New: `state/queue.rs`** - `CustomQueue` struct — full track list, batch window (`batch_start..batch_end`), shuffle/repeat state, source context - `AdvanceResult` / `RetreatResult` / `ShuffleMode` enums - Core methods: `advance`, `retreat`, `truncate_batch_to_current`, `set_shuffle_mode`, `append_radio_tracks`, `next_batch`, `expected_next_track`, etc. **Modified: `state/player.rs`** - Added `custom_queue` fields to `PlayerState` - `playing_context_id()` falls back to `custom_queue.source_context` for URIs playback **Modified: `state/mod.rs`** - Wired `mod queue; pub use queue::*` - Added `State::should_use_custom_queue()` (requires streaming + config) **Modified: `config/mod.rs`** - Added `custom_queue: bool` to `AppConfig` (default `true`) **Docs: `docs/config.md`, `examples/app.toml`** - Documented the new config option ### Design notes - **Batch manager model** — only intervenes at batch boundaries; librespot handles within-batch transitions - **`batch_end` over fixed size** — enables `truncate_batch_to_current()` for non-interrupting shuffle/repeat changes - **`EndOfTrack` as sole position tracker** — skip commands don't advance queue position, avoiding desync with user-queued tracks - **Streaming-only + config-gated** — `should_use_custom_queue()` = `is_streaming_enabled() && custom_queue`
1 parent 9c4a595 commit 5a31f3c

7 files changed

Lines changed: 698 additions & 4 deletions

File tree

.vscode/settings.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
"rust-analyzer.cargo.features": ["image", "notify", "fzf"],
33
"chat.tools.terminal.autoApprove": {
44
"cargo clippy": true,
5-
"cargo fmt": true
5+
"cargo fmt": true,
6+
"cargo test": true
67
}
78
}

docs/config.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ A sample `app.toml` is available at [examples/app.toml](../examples/app.toml).
6767
| `sort_artist_albums_by_type` | Sort albums by type on artist pages. | `false` |
6868
| `volume_scroll_step` | Volume change step when using mouse scroll. | `5` |
6969
| `enable_mouse_scroll_volume` | Enable volume control via mouse scroll. | `true` |
70+
| `custom_queue` | Enable app-managed queue for custom playback integration (requires `streaming` feature). | `true` |
7071
| `device` | Device configuration (see below). | See below |
7172

7273
### Notes

examples/app.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ cover_img_length = 9
2525
cover_img_width = 5
2626
cover_img_pixels = 16
2727
seek_duration_secs = 5
28+
custom_queue = true
2829

2930
[device]
3031
name = "spotify-player"

spotify_player/src/config/mod.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,11 @@ pub struct AppConfig {
133133

134134
pub volume_scroll_step: u8,
135135
pub enable_mouse_scroll_volume: bool,
136+
137+
/// Enable app-managed queue for full playlist playback.
138+
/// Requires streaming. When disabled, playback uses Spotify-native queue
139+
/// management.
140+
pub custom_queue: bool,
136141
}
137142

138143
#[derive(Debug, Deserialize, Serialize, Clone)]
@@ -384,6 +389,8 @@ impl Default for AppConfig {
384389

385390
volume_scroll_step: 5,
386391
enable_mouse_scroll_volume: true,
392+
393+
custom_queue: true,
387394
}
388395
}
389396
}

spotify_player/src/state/mod.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ mod constant;
22
mod data;
33
mod model;
44
mod player;
5+
mod queue;
56
mod ui;
67

78
use std::{collections::VecDeque, sync::Arc};
@@ -10,6 +11,8 @@ pub use constant::*;
1011
pub use data::*;
1112
pub use model::*;
1213
pub use player::*;
14+
#[allow(unused_imports)]
15+
pub use queue::*;
1316
pub use ui::*;
1417

1518
use crate::config;
@@ -74,6 +77,16 @@ impl State {
7477
&& self.is_daemon)
7578
}
7679

80+
/// Returns `true` when the custom queue system should be used for new playback.
81+
///
82+
/// Requires streaming to be enabled and the `custom_queue` config option
83+
/// to be `true`.
84+
#[cfg(feature = "streaming")]
85+
#[allow(dead_code)]
86+
pub fn should_use_custom_queue(&self) -> bool {
87+
self.is_streaming_enabled() && config::get_config().app_config.custom_queue
88+
}
89+
7790
/// Returns `true` when the local librespot player is actively streaming
7891
/// audio (i.e. a `Playing` event has been received and no `Paused` / `stop`
7992
/// has occurred since). Used by the UI to decide whether to allocate and

spotify_player/src/state/player.rs

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
use super::model::{
22
AlbumId, ArtistId, ContextId, Device, PlaybackMetadata, PlaylistId, ShowId, TracksId,
33
};
4+
use super::queue::CustomQueue;
45

56
/// Player state
67
#[derive(Default, Debug)]
@@ -17,6 +18,11 @@ pub struct PlayerState {
1718

1819
/// The currently playing Tracks context (for contexts not tracked by Spotify's playback, e.g. liked/top tracks)
1920
pub currently_playing_tracks_id: Option<TracksId>,
21+
22+
/// App-managed custom queue for full playlist/album playback.
23+
/// Active when the integrated librespot player is streaming and the user
24+
/// started playback from a track-table context.
25+
pub custom_queue: Option<CustomQueue>,
2026
}
2127

2228
impl PlayerState {
@@ -95,9 +101,14 @@ impl PlayerState {
95101
}
96102
}
97103
None => self
98-
.currently_playing_tracks_id
99-
.clone()
100-
.map(ContextId::Tracks),
104+
.custom_queue
105+
.as_ref()
106+
.and_then(|q| q.source_context().cloned())
107+
.or_else(|| {
108+
self.currently_playing_tracks_id
109+
.clone()
110+
.map(ContextId::Tracks)
111+
}),
101112
},
102113
None => None,
103114
}

0 commit comments

Comments
 (0)