diff --git a/src/album.rs b/src/album.rs index be24925..2be3b1e 100644 --- a/src/album.rs +++ b/src/album.rs @@ -6,6 +6,7 @@ use rspotify::model::album::{FullAlbum, SavedAlbum, SimplifiedAlbum}; use crate::artist::Artist; use crate::library::Library; +use crate::playable::Playable; use crate::queue::Queue; use crate::spotify::Spotify; use crate::track::Track; @@ -136,8 +137,8 @@ impl ListItem for Album { .read() .unwrap() .iter() - .filter(|t| t.id.is_some()) - .map(|t| t.id.clone().unwrap()) + .filter(|t| t.id().is_some()) + .map(|t| t.id().clone().unwrap()) .collect(); let ids: Vec = tracks .iter() @@ -175,7 +176,10 @@ impl ListItem for Album { self.load_tracks(queue.get_spotify()); if let Some(tracks) = self.tracks.as_ref() { - let tracks: Vec<&Track> = tracks.iter().collect(); + let tracks: Vec = tracks + .iter() + .map(|track| Playable::Track(track.clone())) + .collect(); let index = queue.append_next(tracks); queue.play(index, true, true); } @@ -186,7 +190,7 @@ impl ListItem for Album { if let Some(tracks) = self.tracks.as_ref() { for t in tracks { - queue.append(&t); + queue.append(Playable::Track(t.clone())); } } } diff --git a/src/artist.rs b/src/artist.rs index eb45754..3d35f47 100644 --- a/src/artist.rs +++ b/src/artist.rs @@ -5,6 +5,7 @@ use rspotify::model::artist::{FullArtist, SimplifiedArtist}; use crate::album::Album; use crate::library::Library; +use crate::playable::Playable; use crate::queue::Queue; use crate::spotify::Spotify; use crate::track::Track; @@ -125,8 +126,8 @@ impl ListItem for Artist { .read() .unwrap() .iter() - .filter(|t| t.id.is_some()) - .map(|t| t.id.clone().unwrap()) + .filter(|t| t.id().is_some()) + .map(|t| t.id().clone().unwrap()) .collect(); let ids: Vec = tracks .iter() @@ -170,7 +171,11 @@ impl ListItem for Artist { fn play(&mut self, queue: Arc) { self.load_albums(queue.get_spotify()); - if let Some(tracks) = self.tracks() { + if let Some(tracks) = self.tracks.as_ref() { + let tracks: Vec = tracks + .iter() + .map(|track| Playable::Track(track.clone())) + .collect(); let index = queue.append_next(tracks); queue.play(index, true, true); } @@ -181,7 +186,7 @@ impl ListItem for Artist { if let Some(tracks) = self.tracks() { for t in tracks { - queue.append(t); + queue.append(Playable::Track(t.clone())); } } } diff --git a/src/episode.rs b/src/episode.rs new file mode 100644 index 0000000..44987d7 --- /dev/null +++ b/src/episode.rs @@ -0,0 +1,90 @@ +use crate::library::Library; +use crate::playable::Playable; +use crate::queue::Queue; +use crate::traits::{ListItem, ViewExt}; +use rspotify::model::show::SimplifiedEpisode; +use std::fmt; +use std::sync::Arc; + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct Episode { + pub id: String, + pub uri: String, + pub duration: u32, + pub name: String, + pub description: String, + pub release_date: String, + pub cover_url: Option, +} + +impl Episode { + pub fn duration_str(&self) -> String { + let minutes = self.duration / 60_000; + let seconds = (self.duration / 1000) % 60; + format!("{:02}:{:02}", minutes, seconds) + } +} + +impl From<&SimplifiedEpisode> for Episode { + fn from(episode: &SimplifiedEpisode) -> Self { + Self { + id: episode.id.clone(), + uri: episode.uri.clone(), + duration: episode.duration_ms, + name: episode.name.clone(), + description: episode.description.clone(), + release_date: episode.release_date.clone(), + cover_url: episode.images.get(0).map(|img| img.url.clone()), + } + } +} + +impl fmt::Display for Episode { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", self.name) + } +} + +impl ListItem for Episode { + fn is_playing(&self, queue: Arc) -> bool { + let current = queue.get_current(); + current + .map(|t| t.id() == Some(self.id.clone())) + .unwrap_or(false) + } + + fn display_left(&self) -> String { + self.name.clone() + } + + fn display_right(&self, _library: Arc) -> String { + format!("{} [{}]", self.duration_str(), self.release_date) + } + + fn play(&mut self, queue: Arc) { + let index = queue.append_next(vec![Playable::Episode(self.clone())]); + queue.play(index, true, false); + } + + fn queue(&mut self, queue: Arc) { + queue.append(Playable::Episode(self.clone())); + } + + fn toggle_saved(&mut self, _library: Arc) {} + + fn save(&mut self, _library: Arc) {} + + fn unsave(&mut self, _library: Arc) {} + + fn open(&self, _queue: Arc, _library: Arc) -> Option> { + None + } + + fn share_url(&self) -> Option { + Some(format!("https://open.spotify.com/episode/{}", self.id)) + } + + fn as_listitem(&self) -> Box { + Box::new(self.clone()) + } +} diff --git a/src/library.rs b/src/library.rs index 4a75c4e..102011e 100644 --- a/src/library.rs +++ b/src/library.rs @@ -13,7 +13,9 @@ use crate::album::Album; use crate::artist::Artist; use crate::config; use crate::events::EventManager; +use crate::playable::Playable; use crate::playlist::Playlist; +use crate::show::Show; use crate::spotify::Spotify; use crate::track::Track; @@ -28,6 +30,7 @@ pub struct Library { pub albums: Arc>>, pub artists: Arc>>, pub playlists: Arc>>, + pub shows: Arc>>, pub is_done: Arc>, user_id: Option, ev: EventManager, @@ -44,6 +47,7 @@ impl Library { albums: Arc::new(RwLock::new(Vec::new())), artists: Arc::new(RwLock::new(Vec::new())), playlists: Arc::new(RwLock::new(Vec::new())), + shows: Arc::new(RwLock::new(Vec::new())), is_done: Arc::new(RwLock::new(false)), user_id, ev: ev.clone(), @@ -140,15 +144,15 @@ impl Library { } } - pub fn overwrite_playlist(&self, id: &str, tracks: &[Track]) { - debug!("saving {} tracks to {}", tracks.len(), id); + pub fn overwrite_playlist(&self, id: &str, tracks: &[Playable]) { + debug!("saving {} tracks to list {}", tracks.len(), id); self.spotify.overwrite_playlist(id, &tracks); self.fetch_playlists(); self.save_cache(config::cache_path(CACHE_PLAYLISTS), self.playlists.clone()); } - pub fn save_playlist(&self, name: &str, tracks: &[Track]) { + pub fn save_playlist(&self, name: &str, tracks: &[Playable]) { debug!("saving {} tracks to new list {}", tracks.len(), name); match self.spotify.create_playlist(name, None, None) { Some(id) => self.overwrite_playlist(&id, &tracks), @@ -202,6 +206,13 @@ impl Library { }) }; + let t_shows = { + let library = library.clone(); + thread::spawn(move || { + library.fetch_shows(); + }) + }; + t_tracks.join().unwrap(); t_artists.join().unwrap(); @@ -210,6 +221,7 @@ impl Library { t_albums.join().unwrap(); t_playlists.join().unwrap(); + t_shows.join().unwrap(); let mut is_done = library.is_done.write().unwrap(); *is_done = true; @@ -218,6 +230,29 @@ impl Library { }); } + fn fetch_shows(&self) { + debug!("loading shows"); + + let mut saved_shows: Vec = Vec::new(); + let mut shows_result = self.spotify.get_saved_shows(0); + + while let Some(shows) = shows_result.as_ref() { + saved_shows.extend(shows.items.iter().map(|show| (&show.show).into())); + + // load next batch if necessary + shows_result = match shows.next { + Some(_) => { + debug!("requesting shows again.."); + self.spotify + .get_saved_shows(shows.offset + shows.items.len() as u32) + } + None => None, + } + } + + *self.shows.write().unwrap() = saved_shows; + } + fn fetch_playlists(&self) { debug!("loading playlists"); let mut stale_lists = self.playlists.read().unwrap().clone(); @@ -512,13 +547,13 @@ impl Library { } } - pub fn is_saved_track(&self, track: &Track) -> bool { + pub fn is_saved_track(&self, track: &Playable) -> bool { if !*self.is_done.read().unwrap() { return false; } let tracks = self.tracks.read().unwrap(); - tracks.iter().any(|t| t.id == track.id) + tracks.iter().any(|t| t.id == track.id()) } pub fn save_tracks(&self, tracks: Vec<&Track>, api: bool) { @@ -773,6 +808,43 @@ impl Library { self.save_cache(config::cache_path(CACHE_PLAYLISTS), self.playlists.clone()); } + pub fn is_saved_show(&self, show: &Show) -> bool { + if !*self.is_done.read().unwrap() { + return false; + } + + let shows = self.shows.read().unwrap(); + shows.iter().any(|s| s.id == show.id) + } + + pub fn save_show(&self, show: &Show) { + if !*self.is_done.read().unwrap() { + return; + } + + if self.spotify.save_shows(vec![show.id.clone()]) { + { + let mut store = self.shows.write().unwrap(); + if !store.iter().any(|s| s.id == show.id) { + store.insert(0, show.clone()); + } + } + } + } + + pub fn unsave_show(&self, show: &Show) { + if !*self.is_done.read().unwrap() { + return; + } + + if self.spotify.unsave_shows(vec![show.id.clone()]) { + { + let mut store = self.shows.write().unwrap(); + *store = store.iter().filter(|s| s.id != show.id).cloned().collect(); + } + } + } + pub fn trigger_redraw(&self) { self.ev.trigger(); } diff --git a/src/main.rs b/src/main.rs index 80b908e..3ae0d01 100644 --- a/src/main.rs +++ b/src/main.rs @@ -57,10 +57,13 @@ mod authentication; mod command; mod commands; mod config; +mod episode; mod events; mod library; +mod playable; mod playlist; mod queue; +mod show; mod spotify; mod theme; mod track; diff --git a/src/mpris.rs b/src/mpris.rs index 88b7b93..11682bc 100644 --- a/src/mpris.rs +++ b/src/mpris.rs @@ -9,13 +9,15 @@ use dbus::tree::{Access, Factory}; use dbus::{Path, SignalArgs}; use crate::album::Album; +use crate::playable::Playable; use crate::playlist::Playlist; use crate::queue::{Queue, RepeatSetting}; use crate::spotify::{PlayerEvent, Spotify, URIType}; use crate::track::Track; +use crate::traits::ListItem; type Metadata = HashMap>>; -struct MprisState(String, Option); +struct MprisState(String, Option); fn get_playbackstatus(spotify: Arc) -> String { match spotify.get_current_status() { @@ -26,18 +28,18 @@ fn get_playbackstatus(spotify: Arc) -> String { .to_string() } -fn get_metadata(track: Option) -> Metadata { +fn get_metadata(playable: Option) -> Metadata { let mut hm: Metadata = HashMap::new(); - let track = track.as_ref(); + let playable = playable.as_ref(); hm.insert( "mpris:trackid".to_string(), Variant(Box::new( - track + playable .map(|t| { format!( "spotify:track:{}", - t.id.clone().unwrap_or_else(|| "0".to_string()) + t.id().unwrap_or_else(|| "0".to_string()) ) }) .unwrap_or_default(), @@ -46,47 +48,77 @@ fn get_metadata(track: Option) -> Metadata { hm.insert( "mpris:length".to_string(), Variant(Box::new(i64::from( - track.map(|t| t.duration * 1_000).unwrap_or(0), + playable.map(|t| t.duration() * 1_000).unwrap_or(0), ))), ); hm.insert( "mpris:artUrl".to_string(), Variant(Box::new( - track.map(|t| t.cover_url.clone()).unwrap_or_default(), + playable + .map(|t| t.cover_url().unwrap_or_default()) + .unwrap_or_default(), )), ); hm.insert( "xesam:album".to_string(), - Variant(Box::new(track.map(|t| t.album.clone()).unwrap_or_default())), + Variant(Box::new( + playable + .and_then(|p| p.track()) + .map(|t| t.album.clone()) + .unwrap_or_default(), + )), ); hm.insert( "xesam:albumArtist".to_string(), Variant(Box::new( - track.map(|t| t.album_artists.clone()).unwrap_or_default(), + playable + .and_then(|p| p.track()) + .map(|t| t.album_artists.clone()) + .unwrap_or_default(), )), ); hm.insert( "xesam:artist".to_string(), Variant(Box::new( - track.map(|t| t.artists.clone()).unwrap_or_default(), + playable + .and_then(|p| p.track()) + .map(|t| t.artists.clone()) + .unwrap_or_default(), )), ); hm.insert( "xesam:discNumber".to_string(), - Variant(Box::new(track.map(|t| t.disc_number).unwrap_or(0))), + Variant(Box::new( + playable + .and_then(|p| p.track()) + .map(|t| t.disc_number) + .unwrap_or(0), + )), ); hm.insert( "xesam:title".to_string(), - Variant(Box::new(track.map(|t| t.title.clone()).unwrap_or_default())), + Variant(Box::new( + playable + .map(|t| match t { + Playable::Track(t) => t.title.clone(), + Playable::Episode(ep) => ep.name.clone(), + }) + .unwrap_or_default(), + )), ); hm.insert( "xesam:trackNumber".to_string(), - Variant(Box::new(track.map(|t| t.track_number).unwrap_or(0) as i32)), + Variant(Box::new( + playable + .and_then(|p| p.track()) + .map(|t| t.track_number) + .unwrap_or(0) as i32, + )), ); hm.insert( "xesam:url".to_string(), - Variant(Box::new(track.map(|t| t.url.clone()).unwrap_or_default())), + Variant(Box::new(playable.map(|t| t.uri()).unwrap_or_default())), ); hm @@ -411,7 +443,11 @@ fn run_dbus_server(spotify: Arc, queue: Arc, rx: mpsc::Receiver< if let Some(a) = spotify.album(&id) { if let Some(t) = &Album::from(&a).tracks { queue.clear(); - let index = queue.append_next(t.iter().collect()); + let index = queue.append_next( + t.iter() + .map(|track| Playable::Track(track.clone())) + .collect(), + ); queue.play(index, false, false) } } @@ -419,7 +455,7 @@ fn run_dbus_server(spotify: Arc, queue: Arc, rx: mpsc::Receiver< Some(URIType::Track) => { if let Some(t) = spotify.track(&id) { queue.clear(); - queue.append(&Track::from(&t)); + queue.append(Playable::Track(Track::from(&t))); queue.play(0, false, false) } } @@ -430,7 +466,11 @@ fn run_dbus_server(spotify: Arc, queue: Arc, rx: mpsc::Receiver< playlist.load_tracks(spotify); if let Some(t) = &playlist.tracks { queue.clear(); - let index = queue.append_next(t.iter().collect()); + let index = queue.append_next( + t.iter() + .map(|track| Playable::Track(track.clone())) + .collect(), + ); queue.play(index, false, false) } } diff --git a/src/playable.rs b/src/playable.rs new file mode 100644 index 0000000..c1560fd --- /dev/null +++ b/src/playable.rs @@ -0,0 +1,126 @@ +use crate::album::Album; +use crate::artist::Artist; +use crate::episode::Episode; +use crate::library::Library; +use crate::queue::Queue; +use crate::track::Track; +use crate::traits::{ListItem, ViewExt}; +use std::fmt; +use std::sync::Arc; + +#[derive(Clone, Debug)] +pub enum Playable { + Track(Track), + Episode(Episode), +} + +impl Playable { + pub fn id(&self) -> Option { + match self { + Playable::Track(track) => track.id.clone(), + Playable::Episode(episode) => Some(episode.id.clone()), + } + } + + pub fn uri(&self) -> String { + match self { + Playable::Track(track) => track.uri.clone(), + Playable::Episode(episode) => episode.uri.clone(), + } + } + + pub fn cover_url(&self) -> Option { + match self { + Playable::Track(track) => track.cover_url.clone(), + Playable::Episode(episode) => episode.cover_url.clone(), + } + } + + pub fn duration(&self) -> u32 { + match self { + Playable::Track(track) => track.duration, + Playable::Episode(episode) => episode.duration, + } + } + + pub fn duration_str(&self) -> String { + let duration = self.duration(); + let minutes = duration / 60_000; + let seconds = (duration / 1000) % 60; + format!("{:02}:{:02}", minutes, seconds) + } + + pub fn as_listitem(&self) -> Box { + match self { + Playable::Track(track) => track.as_listitem(), + Playable::Episode(episode) => episode.as_listitem(), + } + } +} + +impl fmt::Display for Playable { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + Playable::Track(track) => track.fmt(f), + Playable::Episode(episode) => episode.fmt(f), + } + } +} + +impl ListItem for Playable { + fn is_playing(&self, queue: Arc) -> bool { + self.as_listitem().is_playing(queue) + } + + fn display_left(&self) -> String { + self.as_listitem().display_left() + } + + fn display_right(&self, library: Arc) -> String { + self.as_listitem().display_right(library) + } + + fn play(&mut self, queue: Arc) { + self.as_listitem().play(queue) + } + + fn queue(&mut self, queue: Arc) { + self.as_listitem().queue(queue) + } + + fn toggle_saved(&mut self, library: Arc) { + self.as_listitem().toggle_saved(library) + } + + fn save(&mut self, library: Arc) { + self.as_listitem().save(library) + } + + fn unsave(&mut self, library: Arc) { + self.as_listitem().unsave(library) + } + + fn open(&self, queue: Arc, library: Arc) -> Option> { + self.as_listitem().open(queue, library) + } + + fn share_url(&self) -> Option { + self.as_listitem().share_url() + } + + fn album(&self, queue: Arc) -> Option { + self.as_listitem().album(queue) + } + + fn artist(&self) -> Option { + self.as_listitem().artist() + } + + fn track(&self) -> Option { + self.as_listitem().track() + } + + fn as_listitem(&self) -> Box { + self.as_listitem() + } +} diff --git a/src/playlist.rs b/src/playlist.rs index 0e5e0ca..89d1936 100644 --- a/src/playlist.rs +++ b/src/playlist.rs @@ -4,6 +4,7 @@ use std::sync::Arc; use rspotify::model::playlist::{FullPlaylist, SimplifiedPlaylist}; use crate::library::Library; +use crate::playable::Playable; use crate::queue::Queue; use crate::spotify::Spotify; use crate::track::Track; @@ -105,8 +106,8 @@ impl ListItem for Playlist { .read() .unwrap() .iter() - .filter(|t| t.id.is_some()) - .map(|t| t.id.clone().unwrap()) + .filter(|t| t.id().is_some()) + .map(|t| t.id().clone().unwrap()) .collect(); let ids: Vec = tracks .iter() @@ -150,8 +151,12 @@ impl ListItem for Playlist { fn play(&mut self, queue: Arc) { self.load_tracks(queue.get_spotify()); - if let Some(tracks) = self.tracks.as_ref() { - let index = queue.append_next(tracks.iter().collect()); + if let Some(tracks) = &self.tracks { + let tracks: Vec = tracks + .iter() + .map(|track| Playable::Track(track.clone())) + .collect(); + let index = queue.append_next(tracks); queue.play(index, true, true); } } @@ -161,7 +166,7 @@ impl ListItem for Playlist { if let Some(tracks) = self.tracks.as_ref() { for track in tracks.iter() { - queue.append(track); + queue.append(Playable::Track(track.clone())); } } } diff --git a/src/queue.rs b/src/queue.rs index 84fb36b..09eb254 100644 --- a/src/queue.rs +++ b/src/queue.rs @@ -4,8 +4,8 @@ use std::sync::{Arc, RwLock}; use rand::prelude::*; use strum_macros::Display; +use crate::playable::Playable; use crate::spotify::Spotify; -use crate::track::Track; #[derive(Display, Clone, Copy, PartialEq, Debug, Serialize, Deserialize)] pub enum RepeatSetting { @@ -15,7 +15,7 @@ pub enum RepeatSetting { } pub struct Queue { - pub queue: Arc>>, + pub queue: Arc>>, random_order: RwLock>>, current_track: RwLock>, repeat: RwLock, @@ -82,7 +82,7 @@ impl Queue { } } - pub fn get_current(&self) -> Option { + pub fn get_current(&self) -> Option { match self.get_current_index() { Some(index) => Some(self.queue.read().unwrap()[index].clone()), None => None, @@ -93,7 +93,7 @@ impl Queue { *self.current_track.read().unwrap() } - pub fn append(&self, track: &Track) { + 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); @@ -101,10 +101,10 @@ impl Queue { } let mut q = self.queue.write().unwrap(); - q.push(track.clone()); + q.push(track); } - pub fn append_next(&self, tracks: Vec<&Track>) -> usize { + pub fn append_next(&self, tracks: Vec) -> usize { let mut q = self.queue.write().unwrap(); { diff --git a/src/show.rs b/src/show.rs new file mode 100644 index 0000000..da13feb --- /dev/null +++ b/src/show.rs @@ -0,0 +1,144 @@ +use crate::episode::Episode; +use crate::library::Library; +use crate::playable::Playable; +use crate::queue::Queue; +use crate::spotify::Spotify; +use crate::traits::{IntoBoxedViewExt, ListItem, ViewExt}; +use crate::ui::show::ShowView; +use rspotify::model::show::SimplifiedShow; +use std::fmt; +use std::sync::Arc; + +#[derive(Clone, Deserialize, Serialize)] +pub struct Show { + pub id: String, + pub uri: String, + pub name: String, + pub publisher: String, + pub description: String, + pub cover_url: Option, + pub episodes: Option>, +} + +impl Show { + pub fn load_episodes(&mut self, spotify: Arc) { + if self.episodes.is_some() { + return; + } + + let mut collected_episodes = Vec::new(); + + let mut episodes_result = spotify.show_episodes(&self.id, 0); + while let Some(ref episodes) = episodes_result.clone() { + for item in &episodes.items { + collected_episodes.push(item.into()) + } + debug!("got {} episodes", episodes.items.len()); + + // load next batch if necessary + episodes_result = match episodes.next { + Some(_) => { + debug!("requesting episodes again.."); + spotify.show_episodes(&self.id, episodes.offset + episodes.items.len() as u32) + } + None => None, + } + } + + self.episodes = Some(collected_episodes); + } +} + +impl From<&SimplifiedShow> for Show { + fn from(show: &SimplifiedShow) -> Self { + Self { + id: show.id.clone(), + uri: show.uri.clone(), + name: show.name.clone(), + publisher: show.publisher.clone(), + description: show.description.clone(), + cover_url: show.images.get(0).map(|i| i.url.clone()), + episodes: None, + } + } +} + +impl fmt::Display for Show { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{} - {}", self.publisher, self.name) + } +} + +impl ListItem for Show { + fn is_playing(&self, _queue: Arc) -> bool { + false + } + + fn display_left(&self) -> String { + format!("{}", self) + } + + fn display_right(&self, library: Arc) -> String { + let saved = if library.is_saved_show(self) { + if library.use_nerdfont { + "\u{f62b} " + } else { + "✓ " + } + } else { + "" + }; + saved.to_owned() + } + + fn play(&mut self, queue: Arc) { + self.load_episodes(queue.get_spotify()); + + let playables = self + .episodes + .as_ref() + .unwrap_or(&Vec::new()) + .iter() + .map(|ep| Playable::Episode(ep.clone())) + .collect(); + + let index = queue.append_next(playables); + queue.play(index, true, true); + } + + fn queue(&mut self, queue: Arc) { + self.load_episodes(queue.get_spotify()); + + for ep in self.episodes.as_ref().unwrap_or(&Vec::new()) { + queue.append(Playable::Episode(ep.clone())); + } + } + + fn toggle_saved(&mut self, library: Arc) { + if library.is_saved_show(self) { + self.unsave(library); + } else { + self.save(library); + } + } + + fn save(&mut self, library: Arc) { + library.save_show(self); + } + + fn unsave(&mut self, library: Arc) { + library.unsave_show(self); + } + + fn open(&self, queue: Arc, library: Arc) -> Option> { + Some(ShowView::new(queue, library, self).as_boxed_view_ext()) + } + + fn share_url(&self) -> Option { + Some(format!("https://open.spotify.com/show/{}", self.id)) + } + + fn as_listitem(&self) -> Box { + Box::new(self.clone()) + } +} diff --git a/src/spotify.rs b/src/spotify.rs index ef72578..57e1b96 100644 --- a/src/spotify.rs +++ b/src/spotify.rs @@ -56,13 +56,15 @@ use std::{env, io}; use crate::artist::Artist; use crate::config; use crate::events::{Event, EventManager}; +use crate::playable::Playable; use crate::queue; use crate::track::Track; +use rspotify::model::show::{Show, SimplifiedEpisode}; pub const VOLUME_PERCENT: u16 = ((u16::max_value() as f64) * 1.0 / 100.0) as u16; enum WorkerCommand { - Load(Box), + Load(Playable), Play, Pause, Stop, @@ -159,15 +161,16 @@ impl futures::Future for Worker { progress = true; debug!("message received!"); match cmd { - WorkerCommand::Load(track) => { - if let Some(track_id) = &track.id { - let id = SpotifyId::from_base62(track_id).expect("could not parse id"); + WorkerCommand::Load(playable) => match SpotifyId::from_uri(&playable.uri()) { + Ok(id) => { self.play_task = Box::pin(self.player.load(id, true, 0).compat()); - info!("player loading track: {:?}", track); - } else { + info!("player loading track: {:?}", playable); + } + Err(e) => { + error!("error parsing uri: {:?}", e); self.events.send(Event::Player(PlayerEvent::FinishedTrack)); } - } + }, WorkerCommand::Play => { self.player.play(); } @@ -588,12 +591,12 @@ impl Spotify { .is_some() } - pub fn overwrite_playlist(&self, id: &str, tracks: &[Track]) { + pub fn overwrite_playlist(&self, id: &str, tracks: &[Playable]) { // extract only track IDs let mut tracks: Vec = tracks .iter() - .filter(|track| track.id.is_some()) - .map(|track| track.id.clone().unwrap()) + .filter(|track| track.id().is_some()) + .map(|track| track.id().clone().unwrap()) .collect(); // we can only send 100 tracks per request @@ -712,6 +715,24 @@ impl Spotify { }) } + pub fn show_episodes(&self, show_id: &str, offset: u32) -> Option> { + self.api_with_retry(|api| api.get_shows_episodes(show_id.to_string(), 50, offset, None)) + } + + pub fn get_saved_shows(&self, offset: u32) -> Option> { + self.api_with_retry(|api| api.get_saved_show(50, offset)) + } + + pub fn save_shows(&self, ids: Vec) -> bool { + self.api_with_retry(|api| api.save_shows(ids.clone())) + .is_some() + } + + pub fn unsave_shows(&self, ids: Vec) -> bool { + self.api_with_retry(|api| api.remove_users_saved_shows(ids.clone(), None)) + .is_some() + } + pub fn current_user_followed_artists( &self, last: Option, @@ -770,9 +791,9 @@ impl Spotify { self.api_with_retry(|api| api.current_user()) } - pub fn load(&self, track: &Track) { + pub fn load(&self, track: &Playable) { info!("loading track: {:?}", track); - self.send_worker(WorkerCommand::Load(Box::new(track.clone()))); + self.send_worker(WorkerCommand::Load(track.clone())); } pub fn update_status(&self, new_status: PlayerEvent) { diff --git a/src/track.rs b/src/track.rs index e87e8a0..f23b50a 100644 --- a/src/track.rs +++ b/src/track.rs @@ -8,12 +8,14 @@ use rspotify::model::track::{FullTrack, SavedTrack, SimplifiedTrack}; use crate::album::Album; use crate::artist::Artist; use crate::library::Library; +use crate::playable::Playable; use crate::queue::Queue; use crate::traits::{ListItem, ViewExt}; #[derive(Clone, Deserialize, Serialize)] pub struct Track { pub id: Option, + pub uri: String, pub title: String, pub track_number: u32, pub disc_number: i32, @@ -23,7 +25,7 @@ pub struct Track { pub album: String, pub album_id: Option, pub album_artists: Vec, - pub cover_url: String, + pub cover_url: Option, pub url: String, pub added_at: Option>, } @@ -47,13 +49,9 @@ impl Track { .map(|ref artist| artist.name.clone()) .collect::>(); - let cover_url = match album.images.get(0) { - Some(image) => image.url.clone(), - None => "".to_owned(), - }; - Self { id: track.id.clone(), + uri: track.uri.clone(), title: track.name.clone(), track_number: track.track_number, disc_number: track.disc_number, @@ -63,7 +61,7 @@ impl Track { album: album.name.clone(), album_id: Some(album.id.clone()), album_artists, - cover_url, + cover_url: album.images.get(0).map(|img| img.url.clone()), url: track.uri.clone(), added_at: None, } @@ -96,13 +94,9 @@ impl From<&FullTrack> for Track { .map(|ref artist| artist.name.clone()) .collect::>(); - let cover_url = match track.album.images.get(0) { - Some(image) => image.url.clone(), - None => "".to_owned(), - }; - Self { id: track.id.clone(), + uri: track.uri.clone(), title: track.name.clone(), track_number: track.track_number, disc_number: track.disc_number, @@ -112,7 +106,7 @@ impl From<&FullTrack> for Track { album: track.album.name.clone(), album_id: track.album.id.clone(), album_artists, - cover_url, + cover_url: track.album.images.get(0).map(|img| img.url.clone()), url: track.uri.clone(), added_at: None, } @@ -148,7 +142,7 @@ impl fmt::Debug for Track { impl ListItem for Track { fn is_playing(&self, queue: Arc) -> bool { let current = queue.get_current(); - current.map(|t| t.id == self.id).unwrap_or(false) + current.map(|t| t.id() == self.id).unwrap_or(false) } fn as_listitem(&self) -> Box { @@ -160,7 +154,7 @@ impl ListItem for Track { } fn display_right(&self, library: Arc) -> String { - let saved = if library.is_saved_track(self) { + let saved = if library.is_saved_track(&Playable::Track(self.clone())) { if library.use_nerdfont { "\u{f62b} " } else { @@ -173,12 +167,12 @@ impl ListItem for Track { } fn play(&mut self, queue: Arc) { - let index = queue.append_next(vec![self]); + let index = queue.append_next(vec![Playable::Track(self.clone())]); queue.play(index, true, false); } fn queue(&mut self, queue: Arc) { - queue.append(self); + queue.append(Playable::Track(self.clone())); } fn save(&mut self, library: Arc) { @@ -190,7 +184,7 @@ impl ListItem for Track { } fn toggle_saved(&mut self, library: Arc) { - if library.is_saved_track(self) { + if library.is_saved_track(&Playable::Track(self.clone())) { library.unsave_tracks(vec![self], true); } else { library.save_tracks(vec![self], true); diff --git a/src/ui/library.rs b/src/ui/library.rs index 8b0b298..447cc86 100644 --- a/src/ui/library.rs +++ b/src/ui/library.rs @@ -37,7 +37,12 @@ impl LibraryView { .tab( "playlists", "Playlists", - PlaylistsView::new(queue, library.clone()), + PlaylistsView::new(queue.clone(), library.clone()), + ) + .tab( + "podcasts", + "Podcasts", + ListView::new(library.shows.clone(), queue, library.clone()), ); Self { tabs } diff --git a/src/ui/listview.rs b/src/ui/listview.rs index 9a1ab74..8fef3bb 100644 --- a/src/ui/listview.rs +++ b/src/ui/listview.rs @@ -12,6 +12,7 @@ use unicode_width::UnicodeWidthStr; use crate::command::{Command, GotoMode, MoveAmount, MoveMode, TargetMode}; use crate::commands::CommandResult; use crate::library::Library; +use crate::playable::Playable; use crate::queue::Queue; use crate::track::Track; use crate::traits::{IntoBoxedViewExt, ListItem, ViewExt}; @@ -146,7 +147,10 @@ impl ListView { let content = self.content.read().unwrap(); let any = &(*content) as &dyn std::any::Any; if let Some(tracks) = any.downcast_ref::>() { - let tracks: Vec<&Track> = tracks.iter().collect(); + let tracks: Vec = tracks + .iter() + .map(|track| Playable::Track(track.clone())) + .collect(); let index = self.queue.append_next(tracks); self.queue.play(index + self.selected, true, false); true @@ -351,7 +355,10 @@ impl ViewExt for ListView { TargetMode::Selected => self.content.read().ok().and_then(|content| { content.get(self.selected).and_then(ListItem::share_url) }), - TargetMode::Current => self.queue.get_current().and_then(|t| t.share_url()), + TargetMode::Current => self + .queue + .get_current() + .and_then(|t| t.as_listitem().share_url()), }; if let Some(url) = url { diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 4a31cd5..b455fc8 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -10,5 +10,6 @@ pub mod playlist; pub mod playlists; pub mod queue; pub mod search; +pub mod show; pub mod statusbar; pub mod tabview; diff --git a/src/ui/queue.rs b/src/ui/queue.rs index 49307a7..515b058 100644 --- a/src/ui/queue.rs +++ b/src/ui/queue.rs @@ -9,14 +9,14 @@ use std::sync::Arc; use crate::command::{Command, MoveMode, ShiftMode}; use crate::commands::CommandResult; use crate::library::Library; +use crate::playable::Playable; use crate::queue::Queue; -use crate::track::Track; use crate::traits::ViewExt; use crate::ui::listview::ListView; use crate::ui::modal::Modal; pub struct QueueView { - list: ListView, + list: ListView, library: Arc, queue: Arc, } @@ -85,7 +85,7 @@ impl QueueView { } impl ViewWrapper for QueueView { - wrap_impl!(self.list: ListView); + wrap_impl!(self.list: ListView); } impl ViewExt for QueueView { diff --git a/src/ui/search.rs b/src/ui/search.rs index 4649e77..d775396 100644 --- a/src/ui/search.rs +++ b/src/ui/search.rs @@ -17,6 +17,7 @@ use crate::events::EventManager; use crate::library::Library; use crate::playlist::Playlist; use crate::queue::Queue; +use crate::show::Show; use crate::spotify::{Spotify, URIType}; use crate::track::Track; use crate::traits::{ListItem, ViewExt}; @@ -34,6 +35,8 @@ pub struct SearchView { pagination_artists: Pagination, results_playlists: Arc>>, pagination_playlists: Pagination, + results_shows: Arc>>, + pagination_shows: Pagination, edit: NamedView, tabs: NamedView, edit_focused: bool, @@ -57,6 +60,7 @@ impl SearchView { let results_albums = Arc::new(RwLock::new(Vec::new())); let results_artists = Arc::new(RwLock::new(Vec::new())); let results_playlists = Arc::new(RwLock::new(Vec::new())); + let results_shows = Arc::new(RwLock::new(Vec::new())); let searchfield = EditView::new() .on_submit(move |s, input| { @@ -75,14 +79,18 @@ impl SearchView { let pagination_albums = list_albums.get_pagination().clone(); let list_artists = ListView::new(results_artists.clone(), queue.clone(), library.clone()); let pagination_artists = list_artists.get_pagination().clone(); - let list_playlists = ListView::new(results_playlists.clone(), queue, library); + let list_playlists = + ListView::new(results_playlists.clone(), queue.clone(), library.clone()); let pagination_playlists = list_playlists.get_pagination().clone(); + let list_shows = ListView::new(results_shows.clone(), queue, library); + let pagination_shows = list_shows.get_pagination().clone(); let tabs = TabView::new() .tab("tracks", "Tracks", list_tracks) .tab("albums", "Albums", list_albums) .tab("artists", "Artists", list_artists) - .tab("playlists", "Playlists", list_playlists); + .tab("playlists", "Playlists", list_playlists) + .tab("shows", "Podcasts", list_shows); SearchView { results_tracks, @@ -93,6 +101,8 @@ impl SearchView { pagination_artists, results_playlists, pagination_playlists, + results_shows, + pagination_shows, edit: searchfield, tabs: tabs.with_name(LIST_ID), edit_focused: true, @@ -264,6 +274,29 @@ impl SearchView { 0 } + fn search_show( + spotify: &Arc, + shows: &Arc>>, + query: &str, + offset: usize, + append: bool, + ) -> u32 { + if let Some(SearchResult::Shows(results)) = + spotify.search(SearchType::Show, &query, 50, offset as u32) + { + let mut pls = results.items.iter().map(|sp| sp.into()).collect(); + let mut r = shows.write().unwrap(); + + if append { + r.append(&mut pls); + } else { + *r = pls; + } + return results.total; + } + 0 + } + fn perform_search( &self, handler: SearchHandler, @@ -331,6 +364,8 @@ impl SearchView { *results_artists.write().unwrap() = Vec::new(); let results_playlists = self.results_playlists.clone(); *results_playlists.write().unwrap() = Vec::new(); + let results_shows = self.results_shows.clone(); + *results_shows.write().unwrap() = Vec::new(); let mut tab_view = self.tabs.get_mut(); match uritype { @@ -396,6 +431,12 @@ impl SearchView { &query, Some(&self.pagination_playlists), ); + self.perform_search( + Box::new(Self::search_show), + &self.results_shows, + &query, + Some(&self.pagination_shows), + ); } } } diff --git a/src/ui/show.rs b/src/ui/show.rs new file mode 100644 index 0000000..cc985bd --- /dev/null +++ b/src/ui/show.rs @@ -0,0 +1,46 @@ +use std::sync::{Arc, RwLock}; + +use cursive::view::ViewWrapper; +use cursive::Cursive; + +use crate::command::Command; +use crate::commands::CommandResult; +use crate::episode::Episode; +use crate::library::Library; +use crate::queue::Queue; +use crate::show::Show; +use crate::traits::ViewExt; +use crate::ui::listview::ListView; + +pub struct ShowView { + list: ListView, + show: Show, +} + +impl ShowView { + pub fn new(queue: Arc, library: Arc, show: &Show) -> Self { + let mut show = show.clone(); + show.load_episodes(queue.get_spotify()); + + let episodes = show.episodes.clone().unwrap_or_default(); + + Self { + list: ListView::new(Arc::new(RwLock::new(episodes)), queue, library), + show, + } + } +} + +impl ViewWrapper for ShowView { + wrap_impl!(self.list: ListView); +} + +impl ViewExt for ShowView { + fn title(&self) -> String { + self.show.name.clone() + } + + fn on_command(&mut self, s: &mut Cursive, cmd: &Command) -> Result { + self.list.on_command(s, cmd) + } +} diff --git a/src/ui/statusbar.rs b/src/ui/statusbar.rs index f17673c..ed5ffc0 100644 --- a/src/ui/statusbar.rs +++ b/src/ui/statusbar.rs @@ -9,6 +9,7 @@ use cursive::Printer; use unicode_width::UnicodeWidthStr; use crate::library::Library; +use crate::playable::Playable; use crate::queue::{Queue, RepeatSetting}; use crate::spotify::{PlayerEvent, Spotify}; @@ -132,51 +133,54 @@ impl View for StatusBar { printer.print((0, 0), &"┉".repeat(printer.size.x)); }); - if let Some(ref t) = self.queue.get_current() { - let elapsed = self.spotify.get_current_progress(); - let elapsed_ms = elapsed.as_millis() as u32; + let elapsed = self.spotify.get_current_progress(); + let elapsed_ms = elapsed.as_millis() as u32; - let formatted_elapsed = format!( - "{:02}:{:02}", - elapsed.as_secs() / 60, - elapsed.as_secs() % 60 - ); + let formatted_elapsed = format!( + "{:02}:{:02}", + elapsed.as_secs() / 60, + elapsed.as_secs() % 60 + ); - let saved = if self.library.is_saved_track(t) { - if self.use_nerdfont { - "\u{f62b} " - } else { - "✓ " - } - } else { - "" - }; + let playback_duration_status = match self.queue.get_current() { + Some(ref t) => format!("{} / {}", formatted_elapsed, t.duration_str()), + None => "".to_string(), + }; - let right = updating.to_string() - + repeat - + shuffle - + saved - + &format!("{} / {}", formatted_elapsed, t.duration_str()) - + &volume; - let offset = HAlign::Right.get_offset(right.width(), printer.size.x); + let right = updating.to_string() + + repeat + + shuffle + // + saved + + &playback_duration_status + + &volume; + let offset = HAlign::Right.get_offset(right.width(), printer.size.x); - printer.with_color(style, |printer| { + printer.with_color(style, |printer| { + if let Some(ref t) = self.queue.get_current() { printer.print((4, 1), &t.to_string()); - printer.print((offset, 1), &right); - }); + } + printer.print((offset, 1), &right); + }); + if let Some(t) = self.queue.get_current() { printer.with_color(style_bar, |printer| { - let duration_width = (((printer.size.x as u32) * elapsed_ms) / t.duration) as usize; + let duration_width = + (((printer.size.x as u32) * elapsed_ms) / t.duration()) as usize; printer.print((0, 0), &"━".repeat(duration_width + 1)); }); - } else { - let right = updating.to_string() + repeat + shuffle + &volume; - let offset = HAlign::Right.get_offset(right.width(), printer.size.x); - - printer.with_color(style, |printer| { - printer.print((offset, 1), &right); - }); } + + // if let Some(Playable::Track(ref t)) = self.queue.get_current() { + // let saved = if self.library.is_saved_track(&Playable::Track(t.clone())) { + // if self.use_nerdfont { + // "\u{f62b} " + // } else { + // "✓ " + // } + // } else { + // "" + // }; + // } } fn layout(&mut self, size: Vec2) { @@ -208,7 +212,7 @@ impl View for StatusBar { if event == MouseEvent::Press(MouseButton::Left) || event == MouseEvent::Hold(MouseButton::Left) { - if let Some(ref t) = self.queue.get_current() { + if let Some(Playable::Track(ref t)) = self.queue.get_current() { let f: f32 = position.x as f32 / self.last_size.x as f32; let new = t.duration as f32 * f; self.spotify.seek(new as u32);