Persist volume and shuffle/repeat state

This commit is contained in:
Henrik Friedrichsen
2021-02-21 21:24:46 +01:00
parent 610a6190b2
commit a880ffd1f6
4 changed files with 57 additions and 75 deletions

View File

@@ -203,23 +203,6 @@ See the help screen by pressing `?` for a list of possible commands.
ncspot will respect system proxy settings defined via the `http_proxy` ncspot will respect system proxy settings defined via the `http_proxy`
environment variable. environment variable.
### Initial state
The initial state can be specified in the configuration.
It allows for example enabling shuffle per default.
Following entries can be added to the configuration file:
```
[saved_state]
volume = 80
repeat = "track"
shuffle = true
```
- `volume` needs to be an integer value between 0 and 100
- `repeat` can be `"track"`, `"playlist"` or any other value which defaults to no
- `shuffle` must be `"true"` or `"false"`
### Theming ### Theming
[Theme generator](https://ncspot-theme-generator.vaa.red/) by [@vaarad](https://github.com/vaared). [Theme generator](https://ncspot-theme-generator.vaa.red/) by [@vaarad](https://github.com/vaared).

View File

@@ -1,11 +1,13 @@
use std::collections::HashMap; use std::collections::HashMap;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::sync::{RwLock, RwLockReadGuard}; use std::sync::{RwLock, RwLockReadGuard, RwLockWriteGuard};
use std::{fs, process}; use std::{fs, process};
use cursive::theme::Theme; use cursive::theme::Theme;
use platform_dirs::AppDirs; use platform_dirs::AppDirs;
use crate::queue;
pub const CLIENT_ID: &str = "d420a117a32841c2b3474932e49fb54b"; pub const CLIENT_ID: &str = "d420a117a32841c2b3474932e49fb54b";
#[derive(Clone, Serialize, Deserialize, Debug, Default)] #[derive(Clone, Serialize, Deserialize, Debug, Default)]
@@ -14,7 +16,6 @@ pub struct ConfigValues {
pub keybindings: Option<HashMap<String, String>>, pub keybindings: Option<HashMap<String, String>>,
pub theme: Option<ConfigTheme>, pub theme: Option<ConfigTheme>,
pub use_nerdfont: Option<bool>, pub use_nerdfont: Option<bool>,
pub saved_state: Option<SavedState>,
pub audio_cache: Option<bool>, pub audio_cache: Option<bool>,
pub backend: Option<String>, pub backend: Option<String>,
pub backend_device: Option<String>, pub backend_device: Option<String>,
@@ -26,13 +27,6 @@ pub struct ConfigValues {
pub gapless: Option<bool>, pub gapless: Option<bool>,
} }
#[derive(Serialize, Deserialize, Debug, Default, Clone)]
pub struct SavedState {
pub volume: Option<u8>,
pub shuffle: Option<bool>,
pub repeat: Option<String>,
}
#[derive(Serialize, Deserialize, Debug, Default, Clone)] #[derive(Serialize, Deserialize, Debug, Default, Clone)]
pub struct ConfigTheme { pub struct ConfigTheme {
pub background: Option<String>, pub background: Option<String>,
@@ -55,12 +49,30 @@ pub struct ConfigTheme {
pub search_match: Option<String>, pub search_match: Option<String>,
} }
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct UserState {
pub volume: u16,
pub shuffle: bool,
pub repeat: queue::RepeatSetting,
}
impl Default for UserState {
fn default() -> Self {
UserState {
volume: u16::max_value(),
shuffle: false,
repeat: queue::RepeatSetting::None,
}
}
}
lazy_static! { lazy_static! {
pub static ref BASE_PATH: RwLock<Option<PathBuf>> = RwLock::new(None); pub static ref BASE_PATH: RwLock<Option<PathBuf>> = RwLock::new(None);
} }
pub struct Config { pub struct Config {
values: RwLock<ConfigValues>, values: RwLock<ConfigValues>,
state: RwLock<UserState>,
} }
impl Config { impl Config {
@@ -70,8 +82,15 @@ impl Config {
process::exit(1); process::exit(1);
}); });
let userstate = {
let path = config_path("userstate.toml");
load_or_generate_default(path, |_| Ok(UserState::default()), false)
.expect("could not load user state")
};
Self { Self {
values: RwLock::new(values), values: RwLock::new(values),
state: RwLock::new(userstate),
} }
} }
@@ -79,6 +98,23 @@ impl Config {
self.values.read().expect("can't readlock config values") self.values.read().expect("can't readlock config values")
} }
pub fn state(&self) -> RwLockReadGuard<UserState> {
self.state.read().expect("can't readlock user state")
}
pub fn with_state_mut<F>(&self, cb: F)
where
F: Fn(RwLockWriteGuard<UserState>),
{
let state_guard = self.state.write().expect("can't writelock user state");
cb(state_guard);
let path = config_path("userstate.toml");
if let Err(e) = write_content_helper(path, self.state().clone()) {
error!("Could not save user state: {}", e);
}
}
pub fn build_theme(&self) -> Theme { pub fn build_theme(&self) -> Theme {
let theme = &self.values().theme; let theme = &self.values().theme;
crate::theme::load(theme) crate::theme::load(theme)

View File

@@ -22,24 +22,19 @@ pub struct Queue {
pub queue: Arc<RwLock<Vec<Playable>>>, pub queue: Arc<RwLock<Vec<Playable>>>,
random_order: RwLock<Option<Vec<usize>>>, random_order: RwLock<Option<Vec<usize>>>,
current_track: RwLock<Option<usize>>, current_track: RwLock<Option<usize>>,
repeat: RwLock<RepeatSetting>,
spotify: Arc<Spotify>, spotify: Arc<Spotify>,
cfg: Arc<Config>, cfg: Arc<Config>,
} }
impl Queue { impl Queue {
pub fn new(spotify: Arc<Spotify>, cfg: Arc<Config>) -> Queue { pub fn new(spotify: Arc<Spotify>, cfg: Arc<Config>) -> Queue {
let q = Queue { Queue {
queue: Arc::new(RwLock::new(Vec::new())), queue: Arc::new(RwLock::new(Vec::new())),
spotify, spotify,
current_track: RwLock::new(None), current_track: RwLock::new(None),
repeat: RwLock::new(RepeatSetting::None),
random_order: RwLock::new(None), random_order: RwLock::new(None),
cfg, cfg,
}; }
q.set_repeat(q.spotify.repeat);
q.set_shuffle(q.spotify.shuffle);
q
} }
pub fn next_index(&self) -> Option<usize> { pub fn next_index(&self) -> Option<usize> {
@@ -285,7 +280,7 @@ impl Queue {
pub fn next(&self, manual: bool) { pub fn next(&self, manual: bool) {
let q = self.queue.read().unwrap(); let q = self.queue.read().unwrap();
let current = *self.current_track.read().unwrap(); let current = *self.current_track.read().unwrap();
let repeat = *self.repeat.read().unwrap(); let repeat = self.cfg.state().repeat;
if repeat == RepeatSetting::RepeatTrack && !manual { if repeat == RepeatSetting::RepeatTrack && !manual {
if let Some(index) = current { if let Some(index) = current {
@@ -311,7 +306,7 @@ impl Queue {
pub fn previous(&self) { pub fn previous(&self) {
let q = self.queue.read().unwrap(); let q = self.queue.read().unwrap();
let current = *self.current_track.read().unwrap(); let current = *self.current_track.read().unwrap();
let repeat = *self.repeat.read().unwrap(); let repeat = self.cfg.state().repeat;
if let Some(index) = self.previous_index() { if let Some(index) = self.previous_index() {
self.play(index, false, false); self.play(index, false, false);
@@ -332,18 +327,15 @@ impl Queue {
} }
pub fn get_repeat(&self) -> RepeatSetting { pub fn get_repeat(&self) -> RepeatSetting {
let repeat = self.repeat.read().unwrap(); self.cfg.state().repeat
*repeat
} }
pub fn set_repeat(&self, new: RepeatSetting) { pub fn set_repeat(&self, new: RepeatSetting) {
let mut repeat = self.repeat.write().unwrap(); self.cfg.with_state_mut(|mut s| s.repeat = new);
*repeat = new;
} }
pub fn get_shuffle(&self) -> bool { pub fn get_shuffle(&self) -> bool {
let random_order = self.random_order.read().unwrap(); self.cfg.state().shuffle
random_order.is_some()
} }
fn generate_random_order(&self) { fn generate_random_order(&self) {
@@ -365,6 +357,7 @@ impl Queue {
} }
pub fn set_shuffle(&self, new: bool) { pub fn set_shuffle(&self, new: bool) {
self.cfg.with_state_mut(|mut s| s.shuffle = new);
if new { if new {
self.generate_random_order(); self.generate_random_order();
} else { } else {

View File

@@ -47,7 +47,6 @@ use core::task::Poll;
use std::pin::Pin; use std::pin::Pin;
use std::str::FromStr; use std::str::FromStr;
use std::sync::atomic::{AtomicU16, Ordering};
use std::sync::{Arc, RwLock}; use std::sync::{Arc, RwLock};
use std::thread; use std::thread;
use std::time::{Duration, SystemTime}; use std::time::{Duration, SystemTime};
@@ -57,7 +56,6 @@ use crate::artist::Artist;
use crate::config; use crate::config;
use crate::events::{Event, EventManager}; use crate::events::{Event, EventManager};
use crate::playable::Playable; use crate::playable::Playable;
use crate::queue;
use crate::track::Track; use crate::track::Track;
use rspotify::model::recommend::Recommendations; use rspotify::model::recommend::Recommendations;
@@ -95,9 +93,6 @@ pub struct Spotify {
channel: RwLock<Option<mpsc::UnboundedSender<WorkerCommand>>>, channel: RwLock<Option<mpsc::UnboundedSender<WorkerCommand>>>,
user: Option<String>, user: Option<String>,
country: Option<Country>, country: Option<Country>,
pub volume: AtomicU16,
pub repeat: queue::RepeatSetting,
pub shuffle: bool,
} }
struct Worker { struct Worker {
@@ -261,33 +256,10 @@ impl Spotify {
credentials: Credentials, credentials: Credentials,
cfg: Arc<config::Config>, cfg: Arc<config::Config>,
) -> Spotify { ) -> Spotify {
let volume = match &cfg.values().saved_state {
Some(state) => match state.volume {
Some(vol) => ((std::cmp::min(vol, 100) as f32) / 100.0 * 65535_f32).ceil() as u16,
None => 0xFFFF_u16,
},
None => 0xFFFF_u16,
};
let repeat = match &cfg.values().saved_state {
Some(state) => match &state.repeat {
Some(s) => match s.as_str() {
"track" => queue::RepeatSetting::RepeatTrack,
"playlist" => queue::RepeatSetting::RepeatPlaylist,
_ => queue::RepeatSetting::None,
},
_ => queue::RepeatSetting::None,
},
_ => queue::RepeatSetting::None,
};
let shuffle = match &cfg.values().saved_state {
Some(state) => matches!(&state.shuffle, Some(true)),
None => false,
};
let mut spotify = Spotify { let mut spotify = Spotify {
events, events,
credentials, credentials,
cfg, cfg: cfg.clone(),
status: RwLock::new(PlayerEvent::Stopped), status: RwLock::new(PlayerEvent::Stopped),
api: RwLock::new(SpotifyAPI::default()), api: RwLock::new(SpotifyAPI::default()),
elapsed: RwLock::new(None), elapsed: RwLock::new(None),
@@ -296,14 +268,12 @@ impl Spotify {
channel: RwLock::new(None), channel: RwLock::new(None),
user: None, user: None,
country: None, country: None,
volume: AtomicU16::new(volume),
repeat,
shuffle,
}; };
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));
spotify.user = futures::executor::block_on(user_rx).ok(); spotify.user = futures::executor::block_on(user_rx).ok();
let volume = cfg.state().volume;
spotify.set_volume(volume); spotify.set_volume(volume);
spotify.country = spotify spotify.country = spotify
@@ -937,7 +907,7 @@ impl Spotify {
} }
pub fn volume(&self) -> u16 { pub fn volume(&self) -> u16 {
self.volume.load(Ordering::Relaxed) as u16 self.cfg.state().volume
} }
fn log_scale(volume: u16) -> u16 { fn log_scale(volume: u16) -> u16 {
@@ -959,7 +929,7 @@ impl Spotify {
pub fn set_volume(&self, volume: u16) { pub fn set_volume(&self, volume: u16) {
info!("setting volume to {}", volume); info!("setting volume to {}", volume);
self.volume.store(volume, Ordering::Relaxed); self.cfg.with_state_mut(|mut s| s.volume = volume);
self.send_worker(WorkerCommand::SetVolume(Self::log_scale(volume))); self.send_worker(WorkerCommand::SetVolume(Self::log_scale(volume)));
} }
} }