Persist volume and shuffle/repeat state
This commit is contained in:
17
README.md
17
README.md
@@ -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).
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
23
src/queue.rs
23
src/queue.rs
@@ -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 {
|
||||||
|
|||||||
@@ -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)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user