Skip to content

Commit 38010b4

Browse files
ThomasFranshrkfdn
authored andcommitted
fix: gracefully exit when misconfigured or unavailable audio backend
When the user has an error in their audio backend configuration or doesn't have audio backends available, gracefully exit instead of panicking.
1 parent 15515c2 commit 38010b4

File tree

4 files changed

+68
-33
lines changed

4 files changed

+68
-33
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1010
### Fixed
1111

1212
- Crash on Android (Termux) due to unknown user runtime directory
13+
- Crash due to misconfigured or unavailable audio backend
1314

1415
## [1.0.0] - 2023-12-16
1516

src/application.rs

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
use std::error::Error;
12
use std::path::Path;
23
use std::rc::Rc;
34
use std::sync::{Arc, OnceLock};
@@ -83,7 +84,7 @@ impl Application {
8384
/// # Arguments
8485
///
8586
/// * `configuration_file_path` - Relative path to the configuration file inside the base path
86-
pub fn new(configuration_file_path: Option<String>) -> Result<Self, String> {
87+
pub fn new(configuration_file_path: Option<String>) -> Result<Self, Box<dyn Error>> {
8788
// Things here may cause the process to abort; we must do them before creating curses
8889
// windows otherwise the error message will not be seen by a user
8990

@@ -115,7 +116,7 @@ impl Application {
115116
let event_manager = EventManager::new(cursive.cb_sink().clone());
116117

117118
let spotify =
118-
spotify::Spotify::new(event_manager.clone(), credentials, configuration.clone());
119+
spotify::Spotify::new(event_manager.clone(), credentials, configuration.clone())?;
119120

120121
let library = Arc::new(Library::new(
121122
event_manager.clone(),
@@ -252,7 +253,16 @@ impl Application {
252253
Event::Queue(event) => {
253254
self.queue.handle_event(event);
254255
}
255-
Event::SessionDied => self.spotify.start_worker(None),
256+
Event::SessionDied => {
257+
if self.spotify.start_worker(None).is_err() {
258+
let data: UserData = self
259+
.cursive
260+
.user_data()
261+
.cloned()
262+
.expect("user data should be set");
263+
data.cmd.handle(&mut self.cursive, Command::Quit);
264+
};
265+
}
256266
Event::IpcInput(input) => match command::parse(&input) {
257267
Ok(commands) => {
258268
if let Some(data) = self.cursive.user_data::<UserData>().cloned() {

src/main.rs

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,11 @@ extern crate cursive;
33
#[macro_use]
44
extern crate serde;
55

6-
use std::path::PathBuf;
6+
use std::{path::PathBuf, process::exit};
77

88
use application::{setup_logging, Application};
99
use config::set_configuration_base_path;
10+
use log::error;
1011
use ncspot::program_arguments;
1112

1213
mod application;
@@ -60,10 +61,20 @@ fn main() -> Result<(), String> {
6061
Some((_, _)) => unreachable!(),
6162
None => {
6263
// Create the application.
63-
let mut application = Application::new(matches.get_one::<String>("config").cloned())?;
64+
let mut application =
65+
match Application::new(matches.get_one::<String>("config").cloned()) {
66+
Ok(application) => application,
67+
Err(error) => {
68+
eprintln!("{error}");
69+
error!("{error}");
70+
exit(-1);
71+
}
72+
};
6473

6574
// Start the application event loop.
6675
application.run()
6776
}
68-
}
77+
}?;
78+
79+
Ok(())
6980
}

src/spotify.rs

Lines changed: 40 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,11 @@ pub struct Spotify {
6060
}
6161

6262
impl Spotify {
63-
pub fn new(events: EventManager, credentials: Credentials, cfg: Arc<config::Config>) -> Self {
63+
pub fn new(
64+
events: EventManager,
65+
credentials: Credentials,
66+
cfg: Arc<config::Config>,
67+
) -> Result<Self, Box<dyn Error>> {
6468
let mut spotify = Self {
6569
events,
6670
credentials,
@@ -73,7 +77,7 @@ impl Spotify {
7377
};
7478

7579
let (user_tx, user_rx) = oneshot::channel();
76-
spotify.start_worker(Some(user_tx));
80+
spotify.start_worker(Some(user_tx))?;
7781
let user = ASYNC_RUNTIME.get().unwrap().block_on(user_rx).ok();
7882
let volume = cfg.state().volume;
7983
spotify.set_volume(volume);
@@ -83,30 +87,35 @@ impl Spotify {
8387

8488
spotify.api.set_user(user);
8589

86-
spotify
90+
Ok(spotify)
8791
}
8892

8993
/// Start the worker thread. If `user_tx` is given, it will receive the username of the logged
9094
/// in user.
91-
pub fn start_worker(&self, user_tx: Option<oneshot::Sender<String>>) {
95+
pub fn start_worker(
96+
&self,
97+
user_tx: Option<oneshot::Sender<String>>,
98+
) -> Result<(), Box<dyn Error>> {
9299
let (tx, rx) = mpsc::unbounded_channel();
93100
*self.channel.write().unwrap() = Some(tx);
94-
{
95-
let worker_channel = self.channel.clone();
96-
let cfg = self.cfg.clone();
97-
let events = self.events.clone();
98-
let volume = self.volume();
99-
let credentials = self.credentials.clone();
100-
ASYNC_RUNTIME.get().unwrap().spawn(Self::worker(
101-
worker_channel,
102-
events,
103-
rx,
104-
cfg,
105-
credentials,
106-
user_tx,
107-
volume,
108-
));
109-
}
101+
let worker_channel = self.channel.clone();
102+
let cfg = self.cfg.clone();
103+
let events = self.events.clone();
104+
let volume = self.volume();
105+
let credentials = self.credentials.clone();
106+
let backend_name = cfg.values().backend.clone();
107+
let backend = Self::init_backend(backend_name)?;
108+
ASYNC_RUNTIME.get().unwrap().spawn(Self::worker(
109+
worker_channel,
110+
events,
111+
rx,
112+
cfg,
113+
credentials,
114+
user_tx,
115+
volume,
116+
backend,
117+
));
118+
Ok(())
110119
}
111120

112121
/// Generate the librespot [SessionConfig] used when creating a [Session].
@@ -161,14 +170,19 @@ impl Spotify {
161170
}
162171

163172
/// Create and initialize the requested audio backend.
164-
fn init_backend(desired_backend: Option<String>) -> Option<SinkBuilder> {
173+
fn init_backend(desired_backend: Option<String>) -> Result<SinkBuilder, Box<dyn Error>> {
165174
let backend = if let Some(name) = desired_backend {
166175
audio_backend::BACKENDS
167176
.iter()
168177
.find(|backend| name == backend.0)
178+
.ok_or(format!(
179+
r#"configured audio backend "{name}" can't be found"#
180+
))?
169181
} else {
170-
audio_backend::BACKENDS.first()
171-
}?;
182+
audio_backend::BACKENDS
183+
.first()
184+
.ok_or("no available audio backends found")?
185+
};
172186

173187
let backend_name = backend.0;
174188

@@ -179,10 +193,11 @@ impl Spotify {
179193
env::set_var("PULSE_PROP_media.role", "music");
180194
}
181195

182-
Some(backend.1)
196+
Ok(backend.1)
183197
}
184198

185199
/// Create and run the worker thread.
200+
#[allow(clippy::too_many_arguments)]
186201
async fn worker(
187202
worker_channel: Arc<RwLock<Option<mpsc::UnboundedSender<WorkerCommand>>>>,
188203
events: EventManager,
@@ -191,6 +206,7 @@ impl Spotify {
191206
credentials: Credentials,
192207
user_tx: Option<oneshot::Sender<String>>,
193208
volume: u16,
209+
backend: SinkBuilder,
194210
) {
195211
let bitrate_str = cfg.values().bitrate.unwrap_or(320).to_string();
196212
let bitrate = Bitrate::from_str(&bitrate_str);
@@ -216,9 +232,6 @@ impl Spotify {
216232
let mixer = create_mixer(MixerConfig::default());
217233
mixer.set_volume(volume);
218234

219-
let backend_name = cfg.values().backend.clone();
220-
let backend =
221-
Self::init_backend(backend_name).expect("Could not find an audio playback backend");
222235
let audio_format: librespot_playback::config::AudioFormat = Default::default();
223236
let (player, player_events) = Player::new(
224237
player_config,

0 commit comments

Comments
 (0)