Skip to content

Commit

Permalink
fix: gracefully exit when misconfigured or unavailable audio backend
Browse files Browse the repository at this point in the history
When the user has an error in their audio backend configuration or
doesn't have audio backends available, gracefully exit instead of
panicking.
  • Loading branch information
ThomasFrans committed Feb 5, 2024
1 parent 98210e2 commit 7eaf748
Show file tree
Hide file tree
Showing 4 changed files with 68 additions and 33 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
16 changes: 13 additions & 3 deletions src/application.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use std::error::Error;
use std::path::Path;
use std::rc::Rc;
use std::sync::{Arc, OnceLock};
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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(),
Expand Down Expand Up @@ -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() {
Expand Down
17 changes: 14 additions & 3 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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(())
}
67 changes: 40 additions & 27 deletions src/spotify.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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);
Expand All @@ -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].
Expand Down Expand Up @@ -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;

Expand All @@ -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,
Expand All @@ -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);
Expand All @@ -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,
Expand Down

0 comments on commit 7eaf748

Please sign in to comment.