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.
This commit is contained in:
Thomas Frans
2024-02-04 23:22:32 +01:00
committed by Henrik Friedrichsen
parent 15515c27b5
commit 38010b4c76
4 changed files with 68 additions and 33 deletions

View File

@@ -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

View File

@@ -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<String>) -> Result<Self, String> {
pub fn new(configuration_file_path: Option<String>) -> Result<Self, Box<dyn Error>> {
// 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::<UserData>().cloned() {

View File

@@ -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::<String>("config").cloned())?;
let mut application =
match Application::new(matches.get_one::<String>("config").cloned()) {
Ok(application) => application,
Err(error) => {
eprintln!("{error}");
error!("{error}");
exit(-1);
}
};
// Start the application event loop.
application.run()
}
}
}?;
Ok(())
}

View File

@@ -60,7 +60,11 @@ pub struct Spotify {
}
impl Spotify {
pub fn new(events: EventManager, credentials: Credentials, cfg: Arc<config::Config>) -> Self {
pub fn new(
events: EventManager,
credentials: Credentials,
cfg: Arc<config::Config>,
) -> Result<Self, Box<dyn Error>> {
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<oneshot::Sender<String>>) {
pub fn start_worker(
&self,
user_tx: Option<oneshot::Sender<String>>,
) -> Result<(), Box<dyn Error>> {
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<String>) -> Option<SinkBuilder> {
fn init_backend(desired_backend: Option<String>) -> Result<SinkBuilder, Box<dyn Error>> {
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<RwLock<Option<mpsc::UnboundedSender<WorkerCommand>>>>,
events: EventManager,
@@ -191,6 +206,7 @@ impl Spotify {
credentials: Credentials,
user_tx: Option<oneshot::Sender<String>>,
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,