511 lines
18 KiB
Rust
511 lines
18 KiB
Rust
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;
|
|
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<RwLock<Vec<Playable>>>,
|
|
/// The playback order of the queue, as indices into `self.queue`.
|
|
random_order: RwLock<Option<Vec<usize>>>,
|
|
current_track: RwLock<Option<usize>>,
|
|
spotify: Spotify,
|
|
cfg: Arc<Config>,
|
|
library: Arc<Library>,
|
|
}
|
|
|
|
impl Queue {
|
|
pub fn new(spotify: Spotify, cfg: Arc<Config>, library: Arc<Library>) -> Self {
|
|
let queue_state = cfg.state().queuestate.clone();
|
|
|
|
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,
|
|
}
|
|
}
|
|
|
|
/// 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<usize> {
|
|
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<usize> {
|
|
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<Playable> {
|
|
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<usize> {
|
|
*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<Playable>) -> 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::rng();
|
|
index = rng.random_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)
|
|
});
|
|
}
|
|
|
|
// Send a Seeked signal at start of new track
|
|
#[cfg(feature = "mpris")]
|
|
self.spotify.notify_seeked(0);
|
|
}
|
|
|
|
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<Vec<usize>> {
|
|
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<usize> = Vec::with_capacity(q.len());
|
|
let mut random: Vec<usize> = (0..q.len()).collect();
|
|
|
|
if let Some(current) = *self.current_track.read().unwrap() {
|
|
order.push(current);
|
|
random.remove(current);
|
|
}
|
|
|
|
let mut rng = rand::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<String>) {
|
|
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),
|
|
}
|
|
}
|