podcast support (#203)

* implement search for shows/podcasts

* create Playable supertype for queue to contain tracks and episodes

* wip: implement playback of episodes

* load spotify id from uri instead of raw id to fix podcast playback

* show duration for podcast episodes

* implement generic status bar for playables (tracks and episodes)

omit saved indicator for now as the library does not yet support podcasts

* instead of only the last 50 fetch all episodes of a show

* refactor: extract Playable code to separate file

* implement playback/queuing of shows + sharing url

* implement podcast library

* migrate mpris code to Playable supertype
This commit is contained in:
Henrik Friedrichsen
2020-07-14 10:38:22 +02:00
committed by GitHub
parent 8bf06147e2
commit 1b1d392ab8
19 changed files with 723 additions and 115 deletions

View File

@@ -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<String> = 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<Playable> = 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()));
}
}
}

View File

@@ -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<String> = tracks
.iter()
@@ -170,7 +171,11 @@ impl ListItem for Artist {
fn play(&mut self, queue: Arc<Queue>) {
self.load_albums(queue.get_spotify());
if let Some(tracks) = self.tracks() {
if let Some(tracks) = self.tracks.as_ref() {
let tracks: Vec<Playable> = 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()));
}
}
}

90
src/episode.rs Normal file
View File

@@ -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<String>,
}
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<Queue>) -> 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<Library>) -> String {
format!("{} [{}]", self.duration_str(), self.release_date)
}
fn play(&mut self, queue: Arc<Queue>) {
let index = queue.append_next(vec![Playable::Episode(self.clone())]);
queue.play(index, true, false);
}
fn queue(&mut self, queue: Arc<Queue>) {
queue.append(Playable::Episode(self.clone()));
}
fn toggle_saved(&mut self, _library: Arc<Library>) {}
fn save(&mut self, _library: Arc<Library>) {}
fn unsave(&mut self, _library: Arc<Library>) {}
fn open(&self, _queue: Arc<Queue>, _library: Arc<Library>) -> Option<Box<dyn ViewExt>> {
None
}
fn share_url(&self) -> Option<String> {
Some(format!("https://open.spotify.com/episode/{}", self.id))
}
fn as_listitem(&self) -> Box<dyn ListItem> {
Box::new(self.clone())
}
}

View File

@@ -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<RwLock<Vec<Album>>>,
pub artists: Arc<RwLock<Vec<Artist>>>,
pub playlists: Arc<RwLock<Vec<Playlist>>>,
pub shows: Arc<RwLock<Vec<Show>>>,
pub is_done: Arc<RwLock<bool>>,
user_id: Option<String>,
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<Show> = 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();
}

View File

@@ -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;

View File

@@ -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<String, Variant<Box<dyn RefArg>>>;
struct MprisState(String, Option<Track>);
struct MprisState(String, Option<Playable>);
fn get_playbackstatus(spotify: Arc<Spotify>) -> String {
match spotify.get_current_status() {
@@ -26,18 +28,18 @@ fn get_playbackstatus(spotify: Arc<Spotify>) -> String {
.to_string()
}
fn get_metadata(track: Option<Track>) -> Metadata {
fn get_metadata(playable: Option<Playable>) -> 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<Track>) -> 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<Spotify>, queue: Arc<Queue>, 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<Spotify>, queue: Arc<Queue>, 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<Spotify>, queue: Arc<Queue>, 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)
}
}

126
src/playable.rs Normal file
View File

@@ -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<String> {
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<String> {
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<dyn ListItem> {
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<Queue>) -> bool {
self.as_listitem().is_playing(queue)
}
fn display_left(&self) -> String {
self.as_listitem().display_left()
}
fn display_right(&self, library: Arc<Library>) -> String {
self.as_listitem().display_right(library)
}
fn play(&mut self, queue: Arc<Queue>) {
self.as_listitem().play(queue)
}
fn queue(&mut self, queue: Arc<Queue>) {
self.as_listitem().queue(queue)
}
fn toggle_saved(&mut self, library: Arc<Library>) {
self.as_listitem().toggle_saved(library)
}
fn save(&mut self, library: Arc<Library>) {
self.as_listitem().save(library)
}
fn unsave(&mut self, library: Arc<Library>) {
self.as_listitem().unsave(library)
}
fn open(&self, queue: Arc<Queue>, library: Arc<Library>) -> Option<Box<dyn ViewExt>> {
self.as_listitem().open(queue, library)
}
fn share_url(&self) -> Option<String> {
self.as_listitem().share_url()
}
fn album(&self, queue: Arc<Queue>) -> Option<Album> {
self.as_listitem().album(queue)
}
fn artist(&self) -> Option<Artist> {
self.as_listitem().artist()
}
fn track(&self) -> Option<Track> {
self.as_listitem().track()
}
fn as_listitem(&self) -> Box<dyn ListItem> {
self.as_listitem()
}
}

View File

@@ -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<String> = tracks
.iter()
@@ -150,8 +151,12 @@ impl ListItem for Playlist {
fn play(&mut self, queue: Arc<Queue>) {
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<Playable> = 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()));
}
}
}

View File

@@ -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<RwLock<Vec<Track>>>,
pub queue: Arc<RwLock<Vec<Playable>>>,
random_order: RwLock<Option<Vec<usize>>>,
current_track: RwLock<Option<usize>>,
repeat: RwLock<RepeatSetting>,
@@ -82,7 +82,7 @@ impl Queue {
}
}
pub fn get_current(&self) -> Option<Track> {
pub fn get_current(&self) -> Option<Playable> {
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<Playable>) -> usize {
let mut q = self.queue.write().unwrap();
{

144
src/show.rs Normal file
View File

@@ -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<String>,
pub episodes: Option<Vec<Episode>>,
}
impl Show {
pub fn load_episodes(&mut self, spotify: Arc<Spotify>) {
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<Queue>) -> bool {
false
}
fn display_left(&self) -> String {
format!("{}", self)
}
fn display_right(&self, library: Arc<Library>) -> 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<Queue>) {
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<Queue>) {
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<Library>) {
if library.is_saved_show(self) {
self.unsave(library);
} else {
self.save(library);
}
}
fn save(&mut self, library: Arc<Library>) {
library.save_show(self);
}
fn unsave(&mut self, library: Arc<Library>) {
library.unsave_show(self);
}
fn open(&self, queue: Arc<Queue>, library: Arc<Library>) -> Option<Box<dyn ViewExt>> {
Some(ShowView::new(queue, library, self).as_boxed_view_ext())
}
fn share_url(&self) -> Option<String> {
Some(format!("https://open.spotify.com/show/{}", self.id))
}
fn as_listitem(&self) -> Box<dyn ListItem> {
Box::new(self.clone())
}
}

View File

@@ -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<Track>),
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<String> = 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<Page<SimplifiedEpisode>> {
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<Page<Show>> {
self.api_with_retry(|api| api.get_saved_show(50, offset))
}
pub fn save_shows(&self, ids: Vec<String>) -> bool {
self.api_with_retry(|api| api.save_shows(ids.clone()))
.is_some()
}
pub fn unsave_shows(&self, ids: Vec<String>) -> 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<String>,
@@ -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) {

View File

@@ -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<String>,
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<String>,
pub album_artists: Vec<String>,
pub cover_url: String,
pub cover_url: Option<String>,
pub url: String,
pub added_at: Option<DateTime<Utc>>,
}
@@ -47,13 +49,9 @@ impl Track {
.map(|ref artist| artist.name.clone())
.collect::<Vec<String>>();
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::<Vec<String>>();
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<Queue>) -> 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<dyn ListItem> {
@@ -160,7 +154,7 @@ impl ListItem for Track {
}
fn display_right(&self, library: Arc<Library>) -> 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<Queue>) {
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>) {
queue.append(self);
queue.append(Playable::Track(self.clone()));
}
fn save(&mut self, library: Arc<Library>) {
@@ -190,7 +184,7 @@ impl ListItem for Track {
}
fn toggle_saved(&mut self, library: Arc<Library>) {
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);

View File

@@ -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 }

View File

@@ -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<I: ListItem> ListView<I> {
let content = self.content.read().unwrap();
let any = &(*content) as &dyn std::any::Any;
if let Some(tracks) = any.downcast_ref::<Vec<Track>>() {
let tracks: Vec<&Track> = tracks.iter().collect();
let tracks: Vec<Playable> = 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<I: ListItem + Clone> ViewExt for ListView<I> {
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 {

View File

@@ -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;

View File

@@ -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<Track>,
list: ListView<Playable>,
library: Arc<Library>,
queue: Arc<Queue>,
}
@@ -85,7 +85,7 @@ impl QueueView {
}
impl ViewWrapper for QueueView {
wrap_impl!(self.list: ListView<Track>);
wrap_impl!(self.list: ListView<Playable>);
}
impl ViewExt for QueueView {

View File

@@ -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<Artist>,
results_playlists: Arc<RwLock<Vec<Playlist>>>,
pagination_playlists: Pagination<Playlist>,
results_shows: Arc<RwLock<Vec<Show>>>,
pagination_shows: Pagination<Show>,
edit: NamedView<EditView>,
tabs: NamedView<TabView>,
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<Spotify>,
shows: &Arc<RwLock<Vec<Show>>>,
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<I: ListItem>(
&self,
handler: SearchHandler<I>,
@@ -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),
);
}
}
}

46
src/ui/show.rs Normal file
View File

@@ -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<Episode>,
show: Show,
}
impl ShowView {
pub fn new(queue: Arc<Queue>, library: Arc<Library>, 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<Episode>);
}
impl ViewExt for ShowView {
fn title(&self) -> String {
self.show.name.clone()
}
fn on_command(&mut self, s: &mut Cursive, cmd: &Command) -> Result<CommandResult, String> {
self.list.on_command(s, cmd)
}
}

View File

@@ -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);