Skip to content

Commit 5eb790f

Browse files
committed
feat: Use Spirc for player logic
**Heavily WIP, only does basic playback so far** The idea is to use librespot `Spirc` instead of handling the player logic within ncspot. This should reduce complexity a bit, but also allows us to integrate with Spotify Connect, which in turn would enable ncspot to be controlled via Spotify Apps.
1 parent 2d4507d commit 5eb790f

File tree

4 files changed

+67
-27
lines changed

4 files changed

+67
-27
lines changed

Cargo.lock

Lines changed: 23 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ fern = "0.7"
5252
futures = "0.3"
5353
ioctl-rs = {version = "0.2", optional = true}
5454
libc = "0.2.175"
55+
librespot-connect = "0.7.0"
5556
librespot-core = "0.7.0"
5657
librespot-oauth = "0.7.0"
5758
librespot-playback = {version = "0.7.0", default-features = false, features = ["native-tls"]}

src/spotify.rs

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -163,10 +163,7 @@ impl Spotify {
163163

164164
/// Create a [Session] that respects the user configuration in `cfg` and with the given
165165
/// credentials.
166-
async fn create_session(
167-
cfg: &config::Config,
168-
credentials: Credentials,
169-
) -> Result<Session, librespot_core::Error> {
166+
async fn create_session(cfg: &config::Config) -> Session {
170167
let librespot_cache_path = config::cache_path("librespot");
171168
let audio_cache_path = match cfg.values().audio_cache {
172169
Some(false) => None,
@@ -183,8 +180,7 @@ impl Spotify {
183180
.expect("Could not create cache");
184181
debug!("opening spotify session");
185182
let session_config = Self::session_config(cfg);
186-
let session = Session::new(session_config, Some(cache));
187-
session.connect(credentials, true).await.map(|_| session)
183+
Session::new(session_config, Some(cache))
188184
}
189185

190186
/// Create and initialize the requested audio backend.
@@ -243,9 +239,7 @@ impl Spotify {
243239
..Default::default()
244240
};
245241

246-
let session = Self::create_session(&cfg, credentials)
247-
.await
248-
.expect("Could not create session");
242+
let session = Self::create_session(&cfg).await;
249243
user_tx.map(|tx| tx.send(session.username()));
250244

251245
let mixer_factory_opt = librespot_playback::mixer::find(Some(SoftMixer::NAME));
@@ -265,12 +259,14 @@ impl Spotify {
265259

266260
let mut worker = Worker::new(
267261
events.clone(),
262+
credentials,
268263
player_events,
269264
commands,
270265
session,
271266
player,
272267
mixer,
273-
);
268+
)
269+
.await;
274270
debug!("worker thread ready.");
275271
worker.run_loop().await;
276272

src/spotify_worker.rs

Lines changed: 37 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,13 @@ use crate::model::playable::Playable;
33
use crate::queue::QueueEvent;
44
use crate::spotify::PlayerEvent;
55
use futures::Future;
6+
use librespot_connect::{ConnectConfig, LoadRequest, LoadRequestOptions, Spirc};
67
use librespot_core::session::Session;
78
use librespot_core::spotify_id::SpotifyId;
89
use librespot_core::token::Token;
910
use librespot_playback::mixer::Mixer;
1011
use librespot_playback::player::{Player, PlayerEvent as LibrespotPlayerEvent};
12+
use librespot_protocol::credentials;
1113
use log::{debug, error, info, warn};
1214
use std::sync::Arc;
1315
use std::sync::mpsc::Sender;
@@ -41,31 +43,39 @@ pub struct Worker {
4143
events: EventManager,
4244
player_events: UnboundedReceiverStream<LibrespotPlayerEvent>,
4345
commands: UnboundedReceiverStream<WorkerCommand>,
44-
session: Session,
45-
player: Arc<Player>,
4646
token_task: Pin<Box<dyn Future<Output = ()> + Send>>,
4747
player_status: PlayerStatus,
48-
mixer: Arc<dyn Mixer>,
48+
session: Session,
49+
spirc: Spirc,
50+
spirc_task: Pin<Box<dyn Future<Output = ()> + Send>>,
4951
}
5052

5153
impl Worker {
52-
pub(crate) fn new(
54+
pub(crate) async fn new(
5355
events: EventManager,
56+
credentials: librespot_core::authentication::Credentials,
5457
player_events: mpsc::UnboundedReceiver<LibrespotPlayerEvent>,
5558
commands: mpsc::UnboundedReceiver<WorkerCommand>,
5659
session: Session,
5760
player: Arc<Player>,
5861
mixer: Arc<dyn Mixer>,
5962
) -> Self {
63+
let config = ConnectConfig {
64+
name: "ncspot".to_string(),
65+
..Default::default()
66+
};
67+
let (spirc, spirc_task) = Spirc::new(config, session.clone(), credentials, player, mixer)
68+
.await
69+
.expect("Spirc should be initialized");
6070
Self {
6171
events,
6272
player_events: UnboundedReceiverStream::new(player_events),
6373
commands: UnboundedReceiverStream::new(commands),
64-
player,
65-
session,
6674
token_task: Box::pin(futures::future::pending()),
6775
player_status: PlayerStatus::Stopped,
68-
mixer,
76+
session,
77+
spirc,
78+
spirc_task: Box::pin(spirc_task),
6979
}
7080
}
7181

@@ -98,14 +108,22 @@ impl Worker {
98108
tokio::select! {
99109
cmd = self.commands.next() => match cmd {
100110
Some(WorkerCommand::Load(playable, start_playing, position_ms)) => {
111+
self.spirc.activate();
101112
match SpotifyId::from_uri(&playable.uri()) {
102113
Ok(id) => {
103114
info!("player loading track: {id:?}");
104115
if !id.is_playable() {
105116
warn!("track is not playable");
106117
self.events.send(Event::Player(PlayerEvent::FinishedTrack));
107118
} else {
108-
self.player.load(id, start_playing, position_ms);
119+
let options = LoadRequestOptions {
120+
start_playing,
121+
seek_to: position_ms,
122+
context_options: None,
123+
playing_track: None,
124+
};
125+
let req = LoadRequest::from_tracks(vec![id.to_uri().expect("uri")], options);
126+
self.spirc.load(req);
109127
}
110128
}
111129
Err(e) => {
@@ -115,32 +133,31 @@ impl Worker {
115133
}
116134
}
117135
Some(WorkerCommand::Play) => {
118-
self.player.play();
136+
self.spirc.play();
119137
}
120138
Some(WorkerCommand::Pause) => {
121-
self.player.pause();
139+
self.spirc.pause();
122140
}
123141
Some(WorkerCommand::Stop) => {
124-
self.player.stop();
142+
//todo!("stop spirc");
125143
}
126144
Some(WorkerCommand::Seek(pos)) => {
127-
self.player.seek(pos);
145+
self.spirc.set_position_ms(pos);
128146
}
129147
Some(WorkerCommand::SetVolume(volume)) => {
130-
self.mixer.set_volume(volume);
148+
self.spirc.set_volume(volume);
131149
}
132150
Some(WorkerCommand::RequestToken(sender)) => {
133151
self.token_task = Box::pin(Self::get_token(self.session.clone(), sender));
134152
}
135153
Some(WorkerCommand::Preload(playable)) => {
136154
if let Ok(id) = SpotifyId::from_uri(&playable.uri()) {
137155
debug!("Preloading {id:?}");
138-
self.player.preload(id);
156+
// self.player.preload(id);
139157
}
140158
}
141159
Some(WorkerCommand::Shutdown) => {
142-
self.player.stop();
143-
self.session.shutdown();
160+
self.spirc.shutdown();
144161
}
145162
None => info!("empty stream")
146163
},
@@ -197,6 +214,9 @@ impl Worker {
197214
break
198215
},
199216
},
217+
_ = self.spirc_task.as_mut() => {
218+
info!("spirc task tick");
219+
},
200220
// Update animated parts of the UI (e.g. statusbar during playback).
201221
_ = ui_refresh.tick() => {
202222
if !matches!(self.player_status, PlayerStatus::Stopped) {
@@ -215,6 +235,6 @@ impl Worker {
215235
impl Drop for Worker {
216236
fn drop(&mut self) {
217237
debug!("Worker thread is shutting down, stopping player");
218-
self.player.stop();
238+
self.spirc.shutdown();
219239
}
220240
}

0 commit comments

Comments
 (0)