use std::cmp::Ordering; use std::sync::{Arc, RwLock}; use log::{debug, info}; #[cfg(feature = "notify")] use notify_rust::Notification; use rand::prelude::*; use strum_macros::Display; use crate::config::{Config, PlaybackState}; use crate::library::Library; use crate::model::playable::Playable; use crate::spotify::PlayerEvent; use crate::spotify::Spotify; /// Repeat behavior for the [Queue]. #[derive(Display, Clone, Copy, PartialEq, Eq, Debug, Serialize, Deserialize)] pub enum RepeatSetting { #[serde(rename = "off")] None, #[serde(rename = "playlist")] RepeatPlaylist, #[serde(rename = "track")] RepeatTrack, } /// Events that are specific to the [Queue]. #[derive(Clone, Debug, PartialEq, Eq)] pub enum QueueEvent { /// Request the player to 'preload' a track, basically making sure that /// transitions between tracks can be uninterrupted. PreloadTrackRequest, } /// The queue determines the playback order of [Playable] items, and is also used to control /// playback itself. pub struct Queue { /// The internal data, which doesn't change with shuffle or repeat. This is /// the raw data only. pub queue: Arc>>, /// The playback order of the queue, as indices into `self.queue`. random_order: RwLock>>, current_track: RwLock>, spotify: Spotify, cfg: Arc, library: Arc, } impl Queue { pub fn new(spotify: Spotify, cfg: Arc, library: Arc) -> Self { let queue_state = cfg.state().queuestate.clone(); let playback_state = cfg.state().playback_state.clone(); let queue = Self { queue: Arc::new(RwLock::new(queue_state.queue)), spotify: spotify.clone(), current_track: RwLock::new(queue_state.current_track), random_order: RwLock::new(queue_state.random_order), cfg, library, }; if let Some(playable) = queue.get_current() { spotify.load( &playable, playback_state == PlaybackState::Playing, queue_state.track_progress.as_millis() as u32, ); spotify.update_track(); match playback_state { PlaybackState::Stopped => { spotify.stop(); } PlaybackState::Paused | PlaybackState::Playing | PlaybackState::Default => { spotify.pause(); } } } queue } /// The index of the next item in `self.queue` that should be played. None /// if at the end of the queue. pub fn next_index(&self) -> Option { match *self.current_track.read().unwrap() { Some(mut index) => { let random_order = self.random_order.read().unwrap(); if let Some(order) = random_order.as_ref() { index = order.iter().position(|&i| i == index).unwrap(); } let mut next_index = index + 1; if next_index < self.queue.read().unwrap().len() { if let Some(order) = random_order.as_ref() { next_index = order[next_index]; } Some(next_index) } else { None } } None => None, } } /// The index of the previous item in `self.queue` that should be played. /// None if at the start of the queue. pub fn previous_index(&self) -> Option { match *self.current_track.read().unwrap() { Some(mut index) => { let random_order = self.random_order.read().unwrap(); if let Some(order) = random_order.as_ref() { index = order.iter().position(|&i| i == index).unwrap(); } if index > 0 { let mut next_index = index - 1; if let Some(order) = random_order.as_ref() { next_index = order[next_index]; } Some(next_index) } else { None } } None => None, } } /// The currently playing item from `self.queue`. pub fn get_current(&self) -> Option { self.get_current_index() .map(|index| self.queue.read().unwrap()[index].clone()) } /// The index of the currently playing item from `self.queue`. pub fn get_current_index(&self) -> Option { *self.current_track.read().unwrap() } /// Insert `track` as the item that should logically follow the currently /// playing item, taking into account shuffle status. pub fn insert_after_current(&self, track: Playable) { if let Some(index) = self.get_current_index() { let mut random_order = self.random_order.write().unwrap(); if let Some(order) = random_order.as_mut() { let next_i = order.iter().position(|&i| i == index).unwrap(); // shift everything after the insertion in order for item in order.iter_mut() { if *item > index { *item += 1; } } // finally, add the next track index order.insert(next_i + 1, index + 1); } let mut q = self.queue.write().unwrap(); q.insert(index + 1, track); } else { self.append(track); } } /// Add `track` to the end of the queue. pub fn append(&self, track: Playable) { let mut random_order = self.random_order.write().unwrap(); if let Some(order) = random_order.as_mut() { let index = order.len().saturating_sub(1); order.push(index); } let mut q = self.queue.write().unwrap(); q.push(track); } /// Append `tracks` after the currently playing item, taking into account /// shuffle status. Returns the amount of added items. pub fn append_next(&self, tracks: &Vec) -> usize { let mut q = self.queue.write().unwrap(); { let mut random_order = self.random_order.write().unwrap(); if let Some(order) = random_order.as_mut() { order.extend((q.len().saturating_sub(1))..(q.len() + tracks.len())); } } let first = match *self.current_track.read().unwrap() { Some(index) => index + 1, None => q.len(), }; let mut i = first; for track in tracks { q.insert(i, track.clone()); i += 1; } first } /// Remove the item at `index`. This doesn't take into account shuffle /// status, and will literally remove the item at `index` in `self.queue`. pub fn remove(&self, index: usize) { { let mut q = self.queue.write().unwrap(); if q.len() == 0 { info!("queue is empty"); return; } q.remove(index); } // if the queue is empty stop playback let len = self.queue.read().unwrap().len(); if len == 0 { self.stop(); return; } // if we are deleting the currently playing track, play the track with // the same index again, because the next track is now at the position // of the one we deleted let current = *self.current_track.read().unwrap(); if let Some(current_track) = current { match current_track.cmp(&index) { Ordering::Equal => { // if we have deleted the last item and it was playing // stop playback, unless repeat playlist is on, play next if current_track == len { if self.get_repeat() == RepeatSetting::RepeatPlaylist { self.next(false); } else { self.stop(); } } else { self.play(index, false, false); } } Ordering::Greater => { let mut current = self.current_track.write().unwrap(); current.replace(current_track - 1); } _ => (), } } if self.get_shuffle() { self.generate_random_order(); } } /// Clear all the items from the queue and stop playback. pub fn clear(&self) { self.stop(); let mut q = self.queue.write().unwrap(); q.clear(); let mut random_order = self.random_order.write().unwrap(); if let Some(o) = random_order.as_mut() { o.clear() } } /// The amount of items in `self.queue`. pub fn len(&self) -> usize { self.queue.read().unwrap().len() } /// Shift the item at `from` in `self.queue` to `to`. pub fn shift(&self, from: usize, to: usize) { let mut queue = self.queue.write().unwrap(); let item = queue.remove(from); queue.insert(to, item); // if the currently playing track is affected by the shift, update its // index let mut current = self.current_track.write().unwrap(); if let Some(index) = *current { if index == from { current.replace(to); } else if index == to && from > index { current.replace(to + 1); } else if index == to && from < index { current.replace(to - 1); } } } /// Play the item at `index` in `self.queue`. /// /// `reshuffle`: Reshuffle the current order of the queue. /// `shuffle_index`: If this is true, `index` isn't actually used, but is /// chosen at random as a valid index in the queue. pub fn play(&self, mut index: usize, reshuffle: bool, shuffle_index: bool) { let queue_length = self.queue.read().unwrap().len(); // The length of the queue must be bigger than 0 or gen_range panics! if queue_length > 0 && shuffle_index && self.get_shuffle() { let mut rng = rand::thread_rng(); index = rng.gen_range(0..queue_length); } if let Some(track) = &self.queue.read().unwrap().get(index) { self.spotify.load(track, true, 0); let mut current = self.current_track.write().unwrap(); current.replace(index); self.spotify.update_track(); #[cfg(feature = "notify")] if self.cfg.values().notify.unwrap_or(false) { std::thread::spawn({ // use same parser as track_format, Playable::format let format = self .cfg .values() .notification_format .clone() .unwrap_or_default(); let default_title = crate::config::NotificationFormat::default().title.unwrap(); let title = format.title.unwrap_or_else(|| default_title.clone()); let default_body = crate::config::NotificationFormat::default().body.unwrap(); let body = format.body.unwrap_or_else(|| default_body.clone()); let summary_txt = Playable::format(track, &title, &self.library); let body_txt = Playable::format(track, &body, &self.library); let cover_url = track.cover_url(); move || send_notification(&summary_txt, &body_txt, cover_url) }); } } if reshuffle && self.get_shuffle() { self.generate_random_order() } } /// Toggle the playback. If playback is currently stopped, this will either /// play the next song if one is available, or restart from the start. pub fn toggleplayback(&self) { match self.spotify.get_current_status() { PlayerEvent::Playing(_) | PlayerEvent::Paused(_) => { self.spotify.toggleplayback(); } PlayerEvent::Stopped => match self.next_index() { Some(_) => self.next(false), None => self.play(0, false, false), }, _ => (), } } /// Stop playback. pub fn stop(&self) { let mut current = self.current_track.write().unwrap(); *current = None; self.spotify.stop(); } /// Play the next song in the queue. /// /// `manual`: If this is true, normal queue logic like repeat will not be /// used, and the next track will actually be played. This should be used /// when going to the next entry in the queue is the wanted behavior. pub fn next(&self, manual: bool) { let q = self.queue.read().unwrap(); let current = *self.current_track.read().unwrap(); let repeat = self.cfg.state().repeat; if repeat == RepeatSetting::RepeatTrack && !manual { if let Some(index) = current { self.play(index, false, false); } } else if let Some(index) = self.next_index() { self.play(index, false, false); if repeat == RepeatSetting::RepeatTrack && manual { self.set_repeat(RepeatSetting::RepeatPlaylist); } } else if repeat == RepeatSetting::RepeatPlaylist && q.len() > 0 { let random_order = self.random_order.read().unwrap(); self.play( random_order.as_ref().map(|o| o[0]).unwrap_or(0), false, false, ); } else { self.spotify.stop(); } } /// Play the previous item in the queue. pub fn previous(&self) { let q = self.queue.read().unwrap(); let current = *self.current_track.read().unwrap(); let repeat = self.cfg.state().repeat; if let Some(index) = self.previous_index() { self.play(index, false, false); } else if repeat == RepeatSetting::RepeatPlaylist && q.len() > 0 { if self.get_shuffle() { let random_order = self.random_order.read().unwrap(); self.play( random_order.as_ref().map(|o| o[q.len() - 1]).unwrap_or(0), false, false, ); } else { self.play(q.len() - 1, false, false); } } else if let Some(index) = current { self.play(index, false, false); } } /// Get the current repeat behavior. pub fn get_repeat(&self) -> RepeatSetting { self.cfg.state().repeat } /// Set the current repeat behavior and save it to the configuration. pub fn set_repeat(&self, new: RepeatSetting) { self.cfg.with_state_mut(|s| s.repeat = new); } /// Get the current shuffle behavior. pub fn get_shuffle(&self) -> bool { self.cfg.state().shuffle } /// Get the current order that is used to shuffle. pub fn get_random_order(&self) -> Option> { self.random_order.read().unwrap().clone() } /// (Re)generate the random shuffle order. fn generate_random_order(&self) { let q = self.queue.read().unwrap(); let mut order: Vec = Vec::with_capacity(q.len()); let mut random: Vec = (0..q.len()).collect(); if let Some(current) = *self.current_track.read().unwrap() { order.push(current); random.remove(current); } let mut rng = rand::thread_rng(); random.shuffle(&mut rng); order.extend(random); let mut random_order = self.random_order.write().unwrap(); *random_order = Some(order); } /// Set the current shuffle behavior. pub fn set_shuffle(&self, new: bool) { self.cfg.with_state_mut(|s| s.shuffle = new); if new { self.generate_random_order(); } else { let mut random_order = self.random_order.write().unwrap(); *random_order = None; } } /// Handle events that are specific to the queue. pub fn handle_event(&self, event: QueueEvent) { match event { QueueEvent::PreloadTrackRequest => { if let Some(next_index) = self.next_index() { let track = self.queue.read().unwrap()[next_index].clone(); debug!("Preloading track {} as requested by librespot", track); self.spotify.preload(&track); } } } } /// Get the spotify session. pub fn get_spotify(&self) -> Spotify { self.spotify.clone() } } /// Send a notification using the desktops default notification method. /// /// `summary_txt`: A short title for the notification. /// `body_txt`: The actual content of the notification. /// `cover_url`: A URL to an image to show in the notification. /// `notification_id`: Unique id for a notification, that can be used to operate /// on a previous notification (for example to close it). #[cfg(feature = "notify")] pub fn send_notification(summary_txt: &str, body_txt: &str, cover_url: Option) { let mut n = Notification::new(); n.appname("ncspot").summary(summary_txt).body(body_txt); // album cover image if let Some(u) = cover_url { let path = crate::utils::cache_path_for_url(u.to_string()); if !path.exists() { if let Err(e) = crate::utils::download(u, path.clone()) { log::error!("Failed to download cover: {}", e); } } n.icon(path.to_str().unwrap()); } // XDG desktop entry hints #[cfg(all(unix, not(target_os = "macos")))] n.urgency(notify_rust::Urgency::Low) .hint(notify_rust::Hint::Transient(true)) .hint(notify_rust::Hint::DesktopEntry("ncspot".into())); match n.show() { Ok(handle) => { // only available for XDG #[cfg(all(unix, not(target_os = "macos")))] info!("Created notification: {}", handle.id()); } Err(e) => log::error!("Failed to send notification cover: {}", e), } }