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:
committed by
Henrik Friedrichsen
parent
15515c27b5
commit
38010b4c76
@@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
- Crash on Android (Termux) due to unknown user runtime directory
|
- Crash on Android (Termux) due to unknown user runtime directory
|
||||||
|
- Crash due to misconfigured or unavailable audio backend
|
||||||
|
|
||||||
## [1.0.0] - 2023-12-16
|
## [1.0.0] - 2023-12-16
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
use std::error::Error;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
use std::sync::{Arc, OnceLock};
|
use std::sync::{Arc, OnceLock};
|
||||||
@@ -83,7 +84,7 @@ impl Application {
|
|||||||
/// # Arguments
|
/// # Arguments
|
||||||
///
|
///
|
||||||
/// * `configuration_file_path` - Relative path to the configuration file inside the base path
|
/// * `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
|
// 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
|
// 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 event_manager = EventManager::new(cursive.cb_sink().clone());
|
||||||
|
|
||||||
let spotify =
|
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(
|
let library = Arc::new(Library::new(
|
||||||
event_manager.clone(),
|
event_manager.clone(),
|
||||||
@@ -252,7 +253,16 @@ impl Application {
|
|||||||
Event::Queue(event) => {
|
Event::Queue(event) => {
|
||||||
self.queue.handle_event(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) {
|
Event::IpcInput(input) => match command::parse(&input) {
|
||||||
Ok(commands) => {
|
Ok(commands) => {
|
||||||
if let Some(data) = self.cursive.user_data::<UserData>().cloned() {
|
if let Some(data) = self.cursive.user_data::<UserData>().cloned() {
|
||||||
|
|||||||
17
src/main.rs
17
src/main.rs
@@ -3,10 +3,11 @@ extern crate cursive;
|
|||||||
#[macro_use]
|
#[macro_use]
|
||||||
extern crate serde;
|
extern crate serde;
|
||||||
|
|
||||||
use std::path::PathBuf;
|
use std::{path::PathBuf, process::exit};
|
||||||
|
|
||||||
use application::{setup_logging, Application};
|
use application::{setup_logging, Application};
|
||||||
use config::set_configuration_base_path;
|
use config::set_configuration_base_path;
|
||||||
|
use log::error;
|
||||||
use ncspot::program_arguments;
|
use ncspot::program_arguments;
|
||||||
|
|
||||||
mod application;
|
mod application;
|
||||||
@@ -60,10 +61,20 @@ fn main() -> Result<(), String> {
|
|||||||
Some((_, _)) => unreachable!(),
|
Some((_, _)) => unreachable!(),
|
||||||
None => {
|
None => {
|
||||||
// Create the application.
|
// 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.
|
// Start the application event loop.
|
||||||
application.run()
|
application.run()
|
||||||
}
|
}
|
||||||
}
|
}?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -60,7 +60,11 @@ pub struct Spotify {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl 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 {
|
let mut spotify = Self {
|
||||||
events,
|
events,
|
||||||
credentials,
|
credentials,
|
||||||
@@ -73,7 +77,7 @@ impl Spotify {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let (user_tx, user_rx) = oneshot::channel();
|
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 user = ASYNC_RUNTIME.get().unwrap().block_on(user_rx).ok();
|
||||||
let volume = cfg.state().volume;
|
let volume = cfg.state().volume;
|
||||||
spotify.set_volume(volume);
|
spotify.set_volume(volume);
|
||||||
@@ -83,30 +87,35 @@ impl Spotify {
|
|||||||
|
|
||||||
spotify.api.set_user(user);
|
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
|
/// Start the worker thread. If `user_tx` is given, it will receive the username of the logged
|
||||||
/// in user.
|
/// 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();
|
let (tx, rx) = mpsc::unbounded_channel();
|
||||||
*self.channel.write().unwrap() = Some(tx);
|
*self.channel.write().unwrap() = Some(tx);
|
||||||
{
|
let worker_channel = self.channel.clone();
|
||||||
let worker_channel = self.channel.clone();
|
let cfg = self.cfg.clone();
|
||||||
let cfg = self.cfg.clone();
|
let events = self.events.clone();
|
||||||
let events = self.events.clone();
|
let volume = self.volume();
|
||||||
let volume = self.volume();
|
let credentials = self.credentials.clone();
|
||||||
let credentials = self.credentials.clone();
|
let backend_name = cfg.values().backend.clone();
|
||||||
ASYNC_RUNTIME.get().unwrap().spawn(Self::worker(
|
let backend = Self::init_backend(backend_name)?;
|
||||||
worker_channel,
|
ASYNC_RUNTIME.get().unwrap().spawn(Self::worker(
|
||||||
events,
|
worker_channel,
|
||||||
rx,
|
events,
|
||||||
cfg,
|
rx,
|
||||||
credentials,
|
cfg,
|
||||||
user_tx,
|
credentials,
|
||||||
volume,
|
user_tx,
|
||||||
));
|
volume,
|
||||||
}
|
backend,
|
||||||
|
));
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Generate the librespot [SessionConfig] used when creating a [Session].
|
/// Generate the librespot [SessionConfig] used when creating a [Session].
|
||||||
@@ -161,14 +170,19 @@ impl Spotify {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Create and initialize the requested audio backend.
|
/// 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 {
|
let backend = if let Some(name) = desired_backend {
|
||||||
audio_backend::BACKENDS
|
audio_backend::BACKENDS
|
||||||
.iter()
|
.iter()
|
||||||
.find(|backend| name == backend.0)
|
.find(|backend| name == backend.0)
|
||||||
|
.ok_or(format!(
|
||||||
|
r#"configured audio backend "{name}" can't be found"#
|
||||||
|
))?
|
||||||
} else {
|
} else {
|
||||||
audio_backend::BACKENDS.first()
|
audio_backend::BACKENDS
|
||||||
}?;
|
.first()
|
||||||
|
.ok_or("no available audio backends found")?
|
||||||
|
};
|
||||||
|
|
||||||
let backend_name = backend.0;
|
let backend_name = backend.0;
|
||||||
|
|
||||||
@@ -179,10 +193,11 @@ impl Spotify {
|
|||||||
env::set_var("PULSE_PROP_media.role", "music");
|
env::set_var("PULSE_PROP_media.role", "music");
|
||||||
}
|
}
|
||||||
|
|
||||||
Some(backend.1)
|
Ok(backend.1)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create and run the worker thread.
|
/// Create and run the worker thread.
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
async fn worker(
|
async fn worker(
|
||||||
worker_channel: Arc<RwLock<Option<mpsc::UnboundedSender<WorkerCommand>>>>,
|
worker_channel: Arc<RwLock<Option<mpsc::UnboundedSender<WorkerCommand>>>>,
|
||||||
events: EventManager,
|
events: EventManager,
|
||||||
@@ -191,6 +206,7 @@ impl Spotify {
|
|||||||
credentials: Credentials,
|
credentials: Credentials,
|
||||||
user_tx: Option<oneshot::Sender<String>>,
|
user_tx: Option<oneshot::Sender<String>>,
|
||||||
volume: u16,
|
volume: u16,
|
||||||
|
backend: SinkBuilder,
|
||||||
) {
|
) {
|
||||||
let bitrate_str = cfg.values().bitrate.unwrap_or(320).to_string();
|
let bitrate_str = cfg.values().bitrate.unwrap_or(320).to_string();
|
||||||
let bitrate = Bitrate::from_str(&bitrate_str);
|
let bitrate = Bitrate::from_str(&bitrate_str);
|
||||||
@@ -216,9 +232,6 @@ impl Spotify {
|
|||||||
let mixer = create_mixer(MixerConfig::default());
|
let mixer = create_mixer(MixerConfig::default());
|
||||||
mixer.set_volume(volume);
|
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 audio_format: librespot_playback::config::AudioFormat = Default::default();
|
||||||
let (player, player_events) = Player::new(
|
let (player, player_events) = Player::new(
|
||||||
player_config,
|
player_config,
|
||||||
|
|||||||
Reference in New Issue
Block a user