diff --git a/CHANGELOG.md b/CHANGELOG.md index 1045f12c3..2b8c32df9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed - Crash on Android (Termux) due to unknown user runtime directory +- Crash due to misconfigured or unavailable audio backend ## [1.0.0] - 2023-12-16 diff --git a/src/application.rs b/src/application.rs index 692281b65..0f7e885b1 100644 --- a/src/application.rs +++ b/src/application.rs @@ -1,3 +1,4 @@ +use std::error::Error; use std::path::Path; use std::rc::Rc; use std::sync::{Arc, OnceLock}; @@ -83,7 +84,7 @@ impl Application { /// # Arguments /// /// * `configuration_file_path` - Relative path to the configuration file inside the base path - pub fn new(configuration_file_path: Option) -> Result { + pub fn new(configuration_file_path: Option) -> Result> { // Things here may cause the process to abort; we must do them before creating curses // windows otherwise the error message will not be seen by a user @@ -115,7 +116,7 @@ impl Application { let event_manager = EventManager::new(cursive.cb_sink().clone()); let spotify = - spotify::Spotify::new(event_manager.clone(), credentials, configuration.clone()); + spotify::Spotify::new(event_manager.clone(), credentials, configuration.clone())?; let library = Arc::new(Library::new( event_manager.clone(), @@ -252,7 +253,16 @@ impl Application { Event::Queue(event) => { self.queue.handle_event(event); } - Event::SessionDied => self.spotify.start_worker(None), + Event::SessionDied => { + if self.spotify.start_worker(None).is_err() { + let data: UserData = self + .cursive + .user_data() + .cloned() + .expect("user data should be set"); + data.cmd.handle(&mut self.cursive, Command::Quit); + }; + } Event::IpcInput(input) => match command::parse(&input) { Ok(commands) => { if let Some(data) = self.cursive.user_data::().cloned() { diff --git a/src/main.rs b/src/main.rs index b045e0d90..b8798fd51 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,10 +3,11 @@ extern crate cursive; #[macro_use] extern crate serde; -use std::path::PathBuf; +use std::{path::PathBuf, process::exit}; use application::{setup_logging, Application}; use config::set_configuration_base_path; +use log::error; use ncspot::program_arguments; mod application; @@ -60,10 +61,20 @@ fn main() -> Result<(), String> { Some((_, _)) => unreachable!(), None => { // Create the application. - let mut application = Application::new(matches.get_one::("config").cloned())?; + let mut application = + match Application::new(matches.get_one::("config").cloned()) { + Ok(application) => application, + Err(error) => { + eprintln!("{error}"); + error!("{error}"); + exit(-1); + } + }; // Start the application event loop. application.run() } - } + }?; + + Ok(()) } diff --git a/src/spotify.rs b/src/spotify.rs index 9d1d099a1..3e7bc7943 100644 --- a/src/spotify.rs +++ b/src/spotify.rs @@ -60,7 +60,11 @@ pub struct Spotify { } impl Spotify { - pub fn new(events: EventManager, credentials: Credentials, cfg: Arc) -> Self { + pub fn new( + events: EventManager, + credentials: Credentials, + cfg: Arc, + ) -> Result> { let mut spotify = Self { events, credentials, @@ -73,7 +77,7 @@ impl Spotify { }; let (user_tx, user_rx) = oneshot::channel(); - spotify.start_worker(Some(user_tx)); + spotify.start_worker(Some(user_tx))?; let user = ASYNC_RUNTIME.get().unwrap().block_on(user_rx).ok(); let volume = cfg.state().volume; spotify.set_volume(volume); @@ -83,30 +87,35 @@ impl Spotify { spotify.api.set_user(user); - spotify + Ok(spotify) } /// Start the worker thread. If `user_tx` is given, it will receive the username of the logged /// in user. - pub fn start_worker(&self, user_tx: Option>) { + pub fn start_worker( + &self, + user_tx: Option>, + ) -> Result<(), Box> { let (tx, rx) = mpsc::unbounded_channel(); *self.channel.write().unwrap() = Some(tx); - { - let worker_channel = self.channel.clone(); - let cfg = self.cfg.clone(); - let events = self.events.clone(); - let volume = self.volume(); - let credentials = self.credentials.clone(); - ASYNC_RUNTIME.get().unwrap().spawn(Self::worker( - worker_channel, - events, - rx, - cfg, - credentials, - user_tx, - volume, - )); - } + let worker_channel = self.channel.clone(); + let cfg = self.cfg.clone(); + let events = self.events.clone(); + let volume = self.volume(); + let credentials = self.credentials.clone(); + let backend_name = cfg.values().backend.clone(); + let backend = Self::init_backend(backend_name)?; + ASYNC_RUNTIME.get().unwrap().spawn(Self::worker( + worker_channel, + events, + rx, + cfg, + credentials, + user_tx, + volume, + backend, + )); + Ok(()) } /// Generate the librespot [SessionConfig] used when creating a [Session]. @@ -161,14 +170,19 @@ impl Spotify { } /// Create and initialize the requested audio backend. - fn init_backend(desired_backend: Option) -> Option { + fn init_backend(desired_backend: Option) -> Result> { let backend = if let Some(name) = desired_backend { audio_backend::BACKENDS .iter() .find(|backend| name == backend.0) + .ok_or(format!( + r#"configured audio backend "{name}" can't be found"# + ))? } else { - audio_backend::BACKENDS.first() - }?; + audio_backend::BACKENDS + .first() + .ok_or("no available audio backends found")? + }; let backend_name = backend.0; @@ -179,10 +193,11 @@ impl Spotify { env::set_var("PULSE_PROP_media.role", "music"); } - Some(backend.1) + Ok(backend.1) } /// Create and run the worker thread. + #[allow(clippy::too_many_arguments)] async fn worker( worker_channel: Arc>>>, events: EventManager, @@ -191,6 +206,7 @@ impl Spotify { credentials: Credentials, user_tx: Option>, volume: u16, + backend: SinkBuilder, ) { let bitrate_str = cfg.values().bitrate.unwrap_or(320).to_string(); let bitrate = Bitrate::from_str(&bitrate_str); @@ -216,9 +232,6 @@ impl Spotify { let mixer = create_mixer(MixerConfig::default()); mixer.set_volume(volume); - let backend_name = cfg.values().backend.clone(); - let backend = - Self::init_backend(backend_name).expect("Could not find an audio playback backend"); let audio_format: librespot_playback::config::AudioFormat = Default::default(); let (player, player_events) = Player::new( player_config,