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:
committed by
GitHub
parent
8bf06147e2
commit
1b1d392ab8
12
src/album.rs
12
src/album.rs
@@ -6,6 +6,7 @@ use rspotify::model::album::{FullAlbum, SavedAlbum, SimplifiedAlbum};
|
|||||||
|
|
||||||
use crate::artist::Artist;
|
use crate::artist::Artist;
|
||||||
use crate::library::Library;
|
use crate::library::Library;
|
||||||
|
use crate::playable::Playable;
|
||||||
use crate::queue::Queue;
|
use crate::queue::Queue;
|
||||||
use crate::spotify::Spotify;
|
use crate::spotify::Spotify;
|
||||||
use crate::track::Track;
|
use crate::track::Track;
|
||||||
@@ -136,8 +137,8 @@ impl ListItem for Album {
|
|||||||
.read()
|
.read()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|t| t.id.is_some())
|
.filter(|t| t.id().is_some())
|
||||||
.map(|t| t.id.clone().unwrap())
|
.map(|t| t.id().clone().unwrap())
|
||||||
.collect();
|
.collect();
|
||||||
let ids: Vec<String> = tracks
|
let ids: Vec<String> = tracks
|
||||||
.iter()
|
.iter()
|
||||||
@@ -175,7 +176,10 @@ impl ListItem for Album {
|
|||||||
self.load_tracks(queue.get_spotify());
|
self.load_tracks(queue.get_spotify());
|
||||||
|
|
||||||
if let Some(tracks) = self.tracks.as_ref() {
|
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);
|
let index = queue.append_next(tracks);
|
||||||
queue.play(index, true, true);
|
queue.play(index, true, true);
|
||||||
}
|
}
|
||||||
@@ -186,7 +190,7 @@ impl ListItem for Album {
|
|||||||
|
|
||||||
if let Some(tracks) = self.tracks.as_ref() {
|
if let Some(tracks) = self.tracks.as_ref() {
|
||||||
for t in tracks {
|
for t in tracks {
|
||||||
queue.append(&t);
|
queue.append(Playable::Track(t.clone()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ use rspotify::model::artist::{FullArtist, SimplifiedArtist};
|
|||||||
|
|
||||||
use crate::album::Album;
|
use crate::album::Album;
|
||||||
use crate::library::Library;
|
use crate::library::Library;
|
||||||
|
use crate::playable::Playable;
|
||||||
use crate::queue::Queue;
|
use crate::queue::Queue;
|
||||||
use crate::spotify::Spotify;
|
use crate::spotify::Spotify;
|
||||||
use crate::track::Track;
|
use crate::track::Track;
|
||||||
@@ -125,8 +126,8 @@ impl ListItem for Artist {
|
|||||||
.read()
|
.read()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|t| t.id.is_some())
|
.filter(|t| t.id().is_some())
|
||||||
.map(|t| t.id.clone().unwrap())
|
.map(|t| t.id().clone().unwrap())
|
||||||
.collect();
|
.collect();
|
||||||
let ids: Vec<String> = tracks
|
let ids: Vec<String> = tracks
|
||||||
.iter()
|
.iter()
|
||||||
@@ -170,7 +171,11 @@ impl ListItem for Artist {
|
|||||||
fn play(&mut self, queue: Arc<Queue>) {
|
fn play(&mut self, queue: Arc<Queue>) {
|
||||||
self.load_albums(queue.get_spotify());
|
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);
|
let index = queue.append_next(tracks);
|
||||||
queue.play(index, true, true);
|
queue.play(index, true, true);
|
||||||
}
|
}
|
||||||
@@ -181,7 +186,7 @@ impl ListItem for Artist {
|
|||||||
|
|
||||||
if let Some(tracks) = self.tracks() {
|
if let Some(tracks) = self.tracks() {
|
||||||
for t in tracks {
|
for t in tracks {
|
||||||
queue.append(t);
|
queue.append(Playable::Track(t.clone()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
90
src/episode.rs
Normal file
90
src/episode.rs
Normal 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())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -13,7 +13,9 @@ use crate::album::Album;
|
|||||||
use crate::artist::Artist;
|
use crate::artist::Artist;
|
||||||
use crate::config;
|
use crate::config;
|
||||||
use crate::events::EventManager;
|
use crate::events::EventManager;
|
||||||
|
use crate::playable::Playable;
|
||||||
use crate::playlist::Playlist;
|
use crate::playlist::Playlist;
|
||||||
|
use crate::show::Show;
|
||||||
use crate::spotify::Spotify;
|
use crate::spotify::Spotify;
|
||||||
use crate::track::Track;
|
use crate::track::Track;
|
||||||
|
|
||||||
@@ -28,6 +30,7 @@ pub struct Library {
|
|||||||
pub albums: Arc<RwLock<Vec<Album>>>,
|
pub albums: Arc<RwLock<Vec<Album>>>,
|
||||||
pub artists: Arc<RwLock<Vec<Artist>>>,
|
pub artists: Arc<RwLock<Vec<Artist>>>,
|
||||||
pub playlists: Arc<RwLock<Vec<Playlist>>>,
|
pub playlists: Arc<RwLock<Vec<Playlist>>>,
|
||||||
|
pub shows: Arc<RwLock<Vec<Show>>>,
|
||||||
pub is_done: Arc<RwLock<bool>>,
|
pub is_done: Arc<RwLock<bool>>,
|
||||||
user_id: Option<String>,
|
user_id: Option<String>,
|
||||||
ev: EventManager,
|
ev: EventManager,
|
||||||
@@ -44,6 +47,7 @@ impl Library {
|
|||||||
albums: Arc::new(RwLock::new(Vec::new())),
|
albums: Arc::new(RwLock::new(Vec::new())),
|
||||||
artists: Arc::new(RwLock::new(Vec::new())),
|
artists: Arc::new(RwLock::new(Vec::new())),
|
||||||
playlists: 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)),
|
is_done: Arc::new(RwLock::new(false)),
|
||||||
user_id,
|
user_id,
|
||||||
ev: ev.clone(),
|
ev: ev.clone(),
|
||||||
@@ -140,15 +144,15 @@ impl Library {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn overwrite_playlist(&self, id: &str, tracks: &[Track]) {
|
pub fn overwrite_playlist(&self, id: &str, tracks: &[Playable]) {
|
||||||
debug!("saving {} tracks to {}", tracks.len(), id);
|
debug!("saving {} tracks to list {}", tracks.len(), id);
|
||||||
self.spotify.overwrite_playlist(id, &tracks);
|
self.spotify.overwrite_playlist(id, &tracks);
|
||||||
|
|
||||||
self.fetch_playlists();
|
self.fetch_playlists();
|
||||||
self.save_cache(config::cache_path(CACHE_PLAYLISTS), self.playlists.clone());
|
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);
|
debug!("saving {} tracks to new list {}", tracks.len(), name);
|
||||||
match self.spotify.create_playlist(name, None, None) {
|
match self.spotify.create_playlist(name, None, None) {
|
||||||
Some(id) => self.overwrite_playlist(&id, &tracks),
|
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_tracks.join().unwrap();
|
||||||
t_artists.join().unwrap();
|
t_artists.join().unwrap();
|
||||||
|
|
||||||
@@ -210,6 +221,7 @@ impl Library {
|
|||||||
|
|
||||||
t_albums.join().unwrap();
|
t_albums.join().unwrap();
|
||||||
t_playlists.join().unwrap();
|
t_playlists.join().unwrap();
|
||||||
|
t_shows.join().unwrap();
|
||||||
|
|
||||||
let mut is_done = library.is_done.write().unwrap();
|
let mut is_done = library.is_done.write().unwrap();
|
||||||
*is_done = true;
|
*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) {
|
fn fetch_playlists(&self) {
|
||||||
debug!("loading playlists");
|
debug!("loading playlists");
|
||||||
let mut stale_lists = self.playlists.read().unwrap().clone();
|
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() {
|
if !*self.is_done.read().unwrap() {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
let tracks = self.tracks.read().unwrap();
|
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) {
|
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());
|
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) {
|
pub fn trigger_redraw(&self) {
|
||||||
self.ev.trigger();
|
self.ev.trigger();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -57,10 +57,13 @@ mod authentication;
|
|||||||
mod command;
|
mod command;
|
||||||
mod commands;
|
mod commands;
|
||||||
mod config;
|
mod config;
|
||||||
|
mod episode;
|
||||||
mod events;
|
mod events;
|
||||||
mod library;
|
mod library;
|
||||||
|
mod playable;
|
||||||
mod playlist;
|
mod playlist;
|
||||||
mod queue;
|
mod queue;
|
||||||
|
mod show;
|
||||||
mod spotify;
|
mod spotify;
|
||||||
mod theme;
|
mod theme;
|
||||||
mod track;
|
mod track;
|
||||||
|
|||||||
74
src/mpris.rs
74
src/mpris.rs
@@ -9,13 +9,15 @@ use dbus::tree::{Access, Factory};
|
|||||||
use dbus::{Path, SignalArgs};
|
use dbus::{Path, SignalArgs};
|
||||||
|
|
||||||
use crate::album::Album;
|
use crate::album::Album;
|
||||||
|
use crate::playable::Playable;
|
||||||
use crate::playlist::Playlist;
|
use crate::playlist::Playlist;
|
||||||
use crate::queue::{Queue, RepeatSetting};
|
use crate::queue::{Queue, RepeatSetting};
|
||||||
use crate::spotify::{PlayerEvent, Spotify, URIType};
|
use crate::spotify::{PlayerEvent, Spotify, URIType};
|
||||||
use crate::track::Track;
|
use crate::track::Track;
|
||||||
|
use crate::traits::ListItem;
|
||||||
|
|
||||||
type Metadata = HashMap<String, Variant<Box<dyn RefArg>>>;
|
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 {
|
fn get_playbackstatus(spotify: Arc<Spotify>) -> String {
|
||||||
match spotify.get_current_status() {
|
match spotify.get_current_status() {
|
||||||
@@ -26,18 +28,18 @@ fn get_playbackstatus(spotify: Arc<Spotify>) -> String {
|
|||||||
.to_string()
|
.to_string()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_metadata(track: Option<Track>) -> Metadata {
|
fn get_metadata(playable: Option<Playable>) -> Metadata {
|
||||||
let mut hm: Metadata = HashMap::new();
|
let mut hm: Metadata = HashMap::new();
|
||||||
let track = track.as_ref();
|
let playable = playable.as_ref();
|
||||||
|
|
||||||
hm.insert(
|
hm.insert(
|
||||||
"mpris:trackid".to_string(),
|
"mpris:trackid".to_string(),
|
||||||
Variant(Box::new(
|
Variant(Box::new(
|
||||||
track
|
playable
|
||||||
.map(|t| {
|
.map(|t| {
|
||||||
format!(
|
format!(
|
||||||
"spotify:track:{}",
|
"spotify:track:{}",
|
||||||
t.id.clone().unwrap_or_else(|| "0".to_string())
|
t.id().unwrap_or_else(|| "0".to_string())
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
.unwrap_or_default(),
|
.unwrap_or_default(),
|
||||||
@@ -46,47 +48,77 @@ fn get_metadata(track: Option<Track>) -> Metadata {
|
|||||||
hm.insert(
|
hm.insert(
|
||||||
"mpris:length".to_string(),
|
"mpris:length".to_string(),
|
||||||
Variant(Box::new(i64::from(
|
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(
|
hm.insert(
|
||||||
"mpris:artUrl".to_string(),
|
"mpris:artUrl".to_string(),
|
||||||
Variant(Box::new(
|
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(
|
hm.insert(
|
||||||
"xesam:album".to_string(),
|
"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(
|
hm.insert(
|
||||||
"xesam:albumArtist".to_string(),
|
"xesam:albumArtist".to_string(),
|
||||||
Variant(Box::new(
|
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(
|
hm.insert(
|
||||||
"xesam:artist".to_string(),
|
"xesam:artist".to_string(),
|
||||||
Variant(Box::new(
|
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(
|
hm.insert(
|
||||||
"xesam:discNumber".to_string(),
|
"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(
|
hm.insert(
|
||||||
"xesam:title".to_string(),
|
"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(
|
hm.insert(
|
||||||
"xesam:trackNumber".to_string(),
|
"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(
|
hm.insert(
|
||||||
"xesam:url".to_string(),
|
"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
|
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(a) = spotify.album(&id) {
|
||||||
if let Some(t) = &Album::from(&a).tracks {
|
if let Some(t) = &Album::from(&a).tracks {
|
||||||
queue.clear();
|
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)
|
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) => {
|
Some(URIType::Track) => {
|
||||||
if let Some(t) = spotify.track(&id) {
|
if let Some(t) = spotify.track(&id) {
|
||||||
queue.clear();
|
queue.clear();
|
||||||
queue.append(&Track::from(&t));
|
queue.append(Playable::Track(Track::from(&t)));
|
||||||
queue.play(0, false, false)
|
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);
|
playlist.load_tracks(spotify);
|
||||||
if let Some(t) = &playlist.tracks {
|
if let Some(t) = &playlist.tracks {
|
||||||
queue.clear();
|
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)
|
queue.play(index, false, false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
126
src/playable.rs
Normal file
126
src/playable.rs
Normal 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@ use std::sync::Arc;
|
|||||||
use rspotify::model::playlist::{FullPlaylist, SimplifiedPlaylist};
|
use rspotify::model::playlist::{FullPlaylist, SimplifiedPlaylist};
|
||||||
|
|
||||||
use crate::library::Library;
|
use crate::library::Library;
|
||||||
|
use crate::playable::Playable;
|
||||||
use crate::queue::Queue;
|
use crate::queue::Queue;
|
||||||
use crate::spotify::Spotify;
|
use crate::spotify::Spotify;
|
||||||
use crate::track::Track;
|
use crate::track::Track;
|
||||||
@@ -105,8 +106,8 @@ impl ListItem for Playlist {
|
|||||||
.read()
|
.read()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|t| t.id.is_some())
|
.filter(|t| t.id().is_some())
|
||||||
.map(|t| t.id.clone().unwrap())
|
.map(|t| t.id().clone().unwrap())
|
||||||
.collect();
|
.collect();
|
||||||
let ids: Vec<String> = tracks
|
let ids: Vec<String> = tracks
|
||||||
.iter()
|
.iter()
|
||||||
@@ -150,8 +151,12 @@ impl ListItem for Playlist {
|
|||||||
fn play(&mut self, queue: Arc<Queue>) {
|
fn play(&mut self, queue: Arc<Queue>) {
|
||||||
self.load_tracks(queue.get_spotify());
|
self.load_tracks(queue.get_spotify());
|
||||||
|
|
||||||
if let Some(tracks) = self.tracks.as_ref() {
|
if let Some(tracks) = &self.tracks {
|
||||||
let index = queue.append_next(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);
|
queue.play(index, true, true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -161,7 +166,7 @@ impl ListItem for Playlist {
|
|||||||
|
|
||||||
if let Some(tracks) = self.tracks.as_ref() {
|
if let Some(tracks) = self.tracks.as_ref() {
|
||||||
for track in tracks.iter() {
|
for track in tracks.iter() {
|
||||||
queue.append(track);
|
queue.append(Playable::Track(track.clone()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
12
src/queue.rs
12
src/queue.rs
@@ -4,8 +4,8 @@ use std::sync::{Arc, RwLock};
|
|||||||
use rand::prelude::*;
|
use rand::prelude::*;
|
||||||
use strum_macros::Display;
|
use strum_macros::Display;
|
||||||
|
|
||||||
|
use crate::playable::Playable;
|
||||||
use crate::spotify::Spotify;
|
use crate::spotify::Spotify;
|
||||||
use crate::track::Track;
|
|
||||||
|
|
||||||
#[derive(Display, Clone, Copy, PartialEq, Debug, Serialize, Deserialize)]
|
#[derive(Display, Clone, Copy, PartialEq, Debug, Serialize, Deserialize)]
|
||||||
pub enum RepeatSetting {
|
pub enum RepeatSetting {
|
||||||
@@ -15,7 +15,7 @@ pub enum RepeatSetting {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub struct Queue {
|
pub struct Queue {
|
||||||
pub queue: Arc<RwLock<Vec<Track>>>,
|
pub queue: Arc<RwLock<Vec<Playable>>>,
|
||||||
random_order: RwLock<Option<Vec<usize>>>,
|
random_order: RwLock<Option<Vec<usize>>>,
|
||||||
current_track: RwLock<Option<usize>>,
|
current_track: RwLock<Option<usize>>,
|
||||||
repeat: RwLock<RepeatSetting>,
|
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() {
|
match self.get_current_index() {
|
||||||
Some(index) => Some(self.queue.read().unwrap()[index].clone()),
|
Some(index) => Some(self.queue.read().unwrap()[index].clone()),
|
||||||
None => None,
|
None => None,
|
||||||
@@ -93,7 +93,7 @@ impl Queue {
|
|||||||
*self.current_track.read().unwrap()
|
*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();
|
let mut random_order = self.random_order.write().unwrap();
|
||||||
if let Some(order) = random_order.as_mut() {
|
if let Some(order) = random_order.as_mut() {
|
||||||
let index = order.len().saturating_sub(1);
|
let index = order.len().saturating_sub(1);
|
||||||
@@ -101,10 +101,10 @@ impl Queue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let mut q = self.queue.write().unwrap();
|
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();
|
let mut q = self.queue.write().unwrap();
|
||||||
|
|
||||||
{
|
{
|
||||||
|
|||||||
144
src/show.rs
Normal file
144
src/show.rs
Normal 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())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -56,13 +56,15 @@ use std::{env, io};
|
|||||||
use crate::artist::Artist;
|
use crate::artist::Artist;
|
||||||
use crate::config;
|
use crate::config;
|
||||||
use crate::events::{Event, EventManager};
|
use crate::events::{Event, EventManager};
|
||||||
|
use crate::playable::Playable;
|
||||||
use crate::queue;
|
use crate::queue;
|
||||||
use crate::track::Track;
|
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;
|
pub const VOLUME_PERCENT: u16 = ((u16::max_value() as f64) * 1.0 / 100.0) as u16;
|
||||||
|
|
||||||
enum WorkerCommand {
|
enum WorkerCommand {
|
||||||
Load(Box<Track>),
|
Load(Playable),
|
||||||
Play,
|
Play,
|
||||||
Pause,
|
Pause,
|
||||||
Stop,
|
Stop,
|
||||||
@@ -159,15 +161,16 @@ impl futures::Future for Worker {
|
|||||||
progress = true;
|
progress = true;
|
||||||
debug!("message received!");
|
debug!("message received!");
|
||||||
match cmd {
|
match cmd {
|
||||||
WorkerCommand::Load(track) => {
|
WorkerCommand::Load(playable) => match SpotifyId::from_uri(&playable.uri()) {
|
||||||
if let Some(track_id) = &track.id {
|
Ok(id) => {
|
||||||
let id = SpotifyId::from_base62(track_id).expect("could not parse id");
|
|
||||||
self.play_task = Box::pin(self.player.load(id, true, 0).compat());
|
self.play_task = Box::pin(self.player.load(id, true, 0).compat());
|
||||||
info!("player loading track: {:?}", track);
|
info!("player loading track: {:?}", playable);
|
||||||
} else {
|
}
|
||||||
|
Err(e) => {
|
||||||
|
error!("error parsing uri: {:?}", e);
|
||||||
self.events.send(Event::Player(PlayerEvent::FinishedTrack));
|
self.events.send(Event::Player(PlayerEvent::FinishedTrack));
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
WorkerCommand::Play => {
|
WorkerCommand::Play => {
|
||||||
self.player.play();
|
self.player.play();
|
||||||
}
|
}
|
||||||
@@ -588,12 +591,12 @@ impl Spotify {
|
|||||||
.is_some()
|
.is_some()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn overwrite_playlist(&self, id: &str, tracks: &[Track]) {
|
pub fn overwrite_playlist(&self, id: &str, tracks: &[Playable]) {
|
||||||
// extract only track IDs
|
// extract only track IDs
|
||||||
let mut tracks: Vec<String> = tracks
|
let mut tracks: Vec<String> = tracks
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|track| track.id.is_some())
|
.filter(|track| track.id().is_some())
|
||||||
.map(|track| track.id.clone().unwrap())
|
.map(|track| track.id().clone().unwrap())
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
// we can only send 100 tracks per request
|
// 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(
|
pub fn current_user_followed_artists(
|
||||||
&self,
|
&self,
|
||||||
last: Option<String>,
|
last: Option<String>,
|
||||||
@@ -770,9 +791,9 @@ impl Spotify {
|
|||||||
self.api_with_retry(|api| api.current_user())
|
self.api_with_retry(|api| api.current_user())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn load(&self, track: &Track) {
|
pub fn load(&self, track: &Playable) {
|
||||||
info!("loading track: {:?}", track);
|
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) {
|
pub fn update_status(&self, new_status: PlayerEvent) {
|
||||||
|
|||||||
30
src/track.rs
30
src/track.rs
@@ -8,12 +8,14 @@ use rspotify::model::track::{FullTrack, SavedTrack, SimplifiedTrack};
|
|||||||
use crate::album::Album;
|
use crate::album::Album;
|
||||||
use crate::artist::Artist;
|
use crate::artist::Artist;
|
||||||
use crate::library::Library;
|
use crate::library::Library;
|
||||||
|
use crate::playable::Playable;
|
||||||
use crate::queue::Queue;
|
use crate::queue::Queue;
|
||||||
use crate::traits::{ListItem, ViewExt};
|
use crate::traits::{ListItem, ViewExt};
|
||||||
|
|
||||||
#[derive(Clone, Deserialize, Serialize)]
|
#[derive(Clone, Deserialize, Serialize)]
|
||||||
pub struct Track {
|
pub struct Track {
|
||||||
pub id: Option<String>,
|
pub id: Option<String>,
|
||||||
|
pub uri: String,
|
||||||
pub title: String,
|
pub title: String,
|
||||||
pub track_number: u32,
|
pub track_number: u32,
|
||||||
pub disc_number: i32,
|
pub disc_number: i32,
|
||||||
@@ -23,7 +25,7 @@ pub struct Track {
|
|||||||
pub album: String,
|
pub album: String,
|
||||||
pub album_id: Option<String>,
|
pub album_id: Option<String>,
|
||||||
pub album_artists: Vec<String>,
|
pub album_artists: Vec<String>,
|
||||||
pub cover_url: String,
|
pub cover_url: Option<String>,
|
||||||
pub url: String,
|
pub url: String,
|
||||||
pub added_at: Option<DateTime<Utc>>,
|
pub added_at: Option<DateTime<Utc>>,
|
||||||
}
|
}
|
||||||
@@ -47,13 +49,9 @@ impl Track {
|
|||||||
.map(|ref artist| artist.name.clone())
|
.map(|ref artist| artist.name.clone())
|
||||||
.collect::<Vec<String>>();
|
.collect::<Vec<String>>();
|
||||||
|
|
||||||
let cover_url = match album.images.get(0) {
|
|
||||||
Some(image) => image.url.clone(),
|
|
||||||
None => "".to_owned(),
|
|
||||||
};
|
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
id: track.id.clone(),
|
id: track.id.clone(),
|
||||||
|
uri: track.uri.clone(),
|
||||||
title: track.name.clone(),
|
title: track.name.clone(),
|
||||||
track_number: track.track_number,
|
track_number: track.track_number,
|
||||||
disc_number: track.disc_number,
|
disc_number: track.disc_number,
|
||||||
@@ -63,7 +61,7 @@ impl Track {
|
|||||||
album: album.name.clone(),
|
album: album.name.clone(),
|
||||||
album_id: Some(album.id.clone()),
|
album_id: Some(album.id.clone()),
|
||||||
album_artists,
|
album_artists,
|
||||||
cover_url,
|
cover_url: album.images.get(0).map(|img| img.url.clone()),
|
||||||
url: track.uri.clone(),
|
url: track.uri.clone(),
|
||||||
added_at: None,
|
added_at: None,
|
||||||
}
|
}
|
||||||
@@ -96,13 +94,9 @@ impl From<&FullTrack> for Track {
|
|||||||
.map(|ref artist| artist.name.clone())
|
.map(|ref artist| artist.name.clone())
|
||||||
.collect::<Vec<String>>();
|
.collect::<Vec<String>>();
|
||||||
|
|
||||||
let cover_url = match track.album.images.get(0) {
|
|
||||||
Some(image) => image.url.clone(),
|
|
||||||
None => "".to_owned(),
|
|
||||||
};
|
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
id: track.id.clone(),
|
id: track.id.clone(),
|
||||||
|
uri: track.uri.clone(),
|
||||||
title: track.name.clone(),
|
title: track.name.clone(),
|
||||||
track_number: track.track_number,
|
track_number: track.track_number,
|
||||||
disc_number: track.disc_number,
|
disc_number: track.disc_number,
|
||||||
@@ -112,7 +106,7 @@ impl From<&FullTrack> for Track {
|
|||||||
album: track.album.name.clone(),
|
album: track.album.name.clone(),
|
||||||
album_id: track.album.id.clone(),
|
album_id: track.album.id.clone(),
|
||||||
album_artists,
|
album_artists,
|
||||||
cover_url,
|
cover_url: track.album.images.get(0).map(|img| img.url.clone()),
|
||||||
url: track.uri.clone(),
|
url: track.uri.clone(),
|
||||||
added_at: None,
|
added_at: None,
|
||||||
}
|
}
|
||||||
@@ -148,7 +142,7 @@ impl fmt::Debug for Track {
|
|||||||
impl ListItem for Track {
|
impl ListItem for Track {
|
||||||
fn is_playing(&self, queue: Arc<Queue>) -> bool {
|
fn is_playing(&self, queue: Arc<Queue>) -> bool {
|
||||||
let current = queue.get_current();
|
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> {
|
fn as_listitem(&self) -> Box<dyn ListItem> {
|
||||||
@@ -160,7 +154,7 @@ impl ListItem for Track {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn display_right(&self, library: Arc<Library>) -> String {
|
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 {
|
if library.use_nerdfont {
|
||||||
"\u{f62b} "
|
"\u{f62b} "
|
||||||
} else {
|
} else {
|
||||||
@@ -173,12 +167,12 @@ impl ListItem for Track {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn play(&mut self, queue: Arc<Queue>) {
|
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);
|
queue.play(index, true, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn queue(&mut self, queue: Arc<Queue>) {
|
fn queue(&mut self, queue: Arc<Queue>) {
|
||||||
queue.append(self);
|
queue.append(Playable::Track(self.clone()));
|
||||||
}
|
}
|
||||||
|
|
||||||
fn save(&mut self, library: Arc<Library>) {
|
fn save(&mut self, library: Arc<Library>) {
|
||||||
@@ -190,7 +184,7 @@ impl ListItem for Track {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn toggle_saved(&mut self, library: Arc<Library>) {
|
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);
|
library.unsave_tracks(vec![self], true);
|
||||||
} else {
|
} else {
|
||||||
library.save_tracks(vec![self], true);
|
library.save_tracks(vec![self], true);
|
||||||
|
|||||||
@@ -37,7 +37,12 @@ impl LibraryView {
|
|||||||
.tab(
|
.tab(
|
||||||
"playlists",
|
"playlists",
|
||||||
"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 }
|
Self { tabs }
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ use unicode_width::UnicodeWidthStr;
|
|||||||
use crate::command::{Command, GotoMode, MoveAmount, MoveMode, TargetMode};
|
use crate::command::{Command, GotoMode, MoveAmount, MoveMode, TargetMode};
|
||||||
use crate::commands::CommandResult;
|
use crate::commands::CommandResult;
|
||||||
use crate::library::Library;
|
use crate::library::Library;
|
||||||
|
use crate::playable::Playable;
|
||||||
use crate::queue::Queue;
|
use crate::queue::Queue;
|
||||||
use crate::track::Track;
|
use crate::track::Track;
|
||||||
use crate::traits::{IntoBoxedViewExt, ListItem, ViewExt};
|
use crate::traits::{IntoBoxedViewExt, ListItem, ViewExt};
|
||||||
@@ -146,7 +147,10 @@ impl<I: ListItem> ListView<I> {
|
|||||||
let content = self.content.read().unwrap();
|
let content = self.content.read().unwrap();
|
||||||
let any = &(*content) as &dyn std::any::Any;
|
let any = &(*content) as &dyn std::any::Any;
|
||||||
if let Some(tracks) = any.downcast_ref::<Vec<Track>>() {
|
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);
|
let index = self.queue.append_next(tracks);
|
||||||
self.queue.play(index + self.selected, true, false);
|
self.queue.play(index + self.selected, true, false);
|
||||||
true
|
true
|
||||||
@@ -351,7 +355,10 @@ impl<I: ListItem + Clone> ViewExt for ListView<I> {
|
|||||||
TargetMode::Selected => self.content.read().ok().and_then(|content| {
|
TargetMode::Selected => self.content.read().ok().and_then(|content| {
|
||||||
content.get(self.selected).and_then(ListItem::share_url)
|
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 {
|
if let Some(url) = url {
|
||||||
|
|||||||
@@ -10,5 +10,6 @@ pub mod playlist;
|
|||||||
pub mod playlists;
|
pub mod playlists;
|
||||||
pub mod queue;
|
pub mod queue;
|
||||||
pub mod search;
|
pub mod search;
|
||||||
|
pub mod show;
|
||||||
pub mod statusbar;
|
pub mod statusbar;
|
||||||
pub mod tabview;
|
pub mod tabview;
|
||||||
|
|||||||
@@ -9,14 +9,14 @@ use std::sync::Arc;
|
|||||||
use crate::command::{Command, MoveMode, ShiftMode};
|
use crate::command::{Command, MoveMode, ShiftMode};
|
||||||
use crate::commands::CommandResult;
|
use crate::commands::CommandResult;
|
||||||
use crate::library::Library;
|
use crate::library::Library;
|
||||||
|
use crate::playable::Playable;
|
||||||
use crate::queue::Queue;
|
use crate::queue::Queue;
|
||||||
use crate::track::Track;
|
|
||||||
use crate::traits::ViewExt;
|
use crate::traits::ViewExt;
|
||||||
use crate::ui::listview::ListView;
|
use crate::ui::listview::ListView;
|
||||||
use crate::ui::modal::Modal;
|
use crate::ui::modal::Modal;
|
||||||
|
|
||||||
pub struct QueueView {
|
pub struct QueueView {
|
||||||
list: ListView<Track>,
|
list: ListView<Playable>,
|
||||||
library: Arc<Library>,
|
library: Arc<Library>,
|
||||||
queue: Arc<Queue>,
|
queue: Arc<Queue>,
|
||||||
}
|
}
|
||||||
@@ -85,7 +85,7 @@ impl QueueView {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl ViewWrapper for QueueView {
|
impl ViewWrapper for QueueView {
|
||||||
wrap_impl!(self.list: ListView<Track>);
|
wrap_impl!(self.list: ListView<Playable>);
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ViewExt for QueueView {
|
impl ViewExt for QueueView {
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ use crate::events::EventManager;
|
|||||||
use crate::library::Library;
|
use crate::library::Library;
|
||||||
use crate::playlist::Playlist;
|
use crate::playlist::Playlist;
|
||||||
use crate::queue::Queue;
|
use crate::queue::Queue;
|
||||||
|
use crate::show::Show;
|
||||||
use crate::spotify::{Spotify, URIType};
|
use crate::spotify::{Spotify, URIType};
|
||||||
use crate::track::Track;
|
use crate::track::Track;
|
||||||
use crate::traits::{ListItem, ViewExt};
|
use crate::traits::{ListItem, ViewExt};
|
||||||
@@ -34,6 +35,8 @@ pub struct SearchView {
|
|||||||
pagination_artists: Pagination<Artist>,
|
pagination_artists: Pagination<Artist>,
|
||||||
results_playlists: Arc<RwLock<Vec<Playlist>>>,
|
results_playlists: Arc<RwLock<Vec<Playlist>>>,
|
||||||
pagination_playlists: Pagination<Playlist>,
|
pagination_playlists: Pagination<Playlist>,
|
||||||
|
results_shows: Arc<RwLock<Vec<Show>>>,
|
||||||
|
pagination_shows: Pagination<Show>,
|
||||||
edit: NamedView<EditView>,
|
edit: NamedView<EditView>,
|
||||||
tabs: NamedView<TabView>,
|
tabs: NamedView<TabView>,
|
||||||
edit_focused: bool,
|
edit_focused: bool,
|
||||||
@@ -57,6 +60,7 @@ impl SearchView {
|
|||||||
let results_albums = Arc::new(RwLock::new(Vec::new()));
|
let results_albums = Arc::new(RwLock::new(Vec::new()));
|
||||||
let results_artists = 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_playlists = Arc::new(RwLock::new(Vec::new()));
|
||||||
|
let results_shows = Arc::new(RwLock::new(Vec::new()));
|
||||||
|
|
||||||
let searchfield = EditView::new()
|
let searchfield = EditView::new()
|
||||||
.on_submit(move |s, input| {
|
.on_submit(move |s, input| {
|
||||||
@@ -75,14 +79,18 @@ impl SearchView {
|
|||||||
let pagination_albums = list_albums.get_pagination().clone();
|
let pagination_albums = list_albums.get_pagination().clone();
|
||||||
let list_artists = ListView::new(results_artists.clone(), queue.clone(), library.clone());
|
let list_artists = ListView::new(results_artists.clone(), queue.clone(), library.clone());
|
||||||
let pagination_artists = list_artists.get_pagination().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 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()
|
let tabs = TabView::new()
|
||||||
.tab("tracks", "Tracks", list_tracks)
|
.tab("tracks", "Tracks", list_tracks)
|
||||||
.tab("albums", "Albums", list_albums)
|
.tab("albums", "Albums", list_albums)
|
||||||
.tab("artists", "Artists", list_artists)
|
.tab("artists", "Artists", list_artists)
|
||||||
.tab("playlists", "Playlists", list_playlists);
|
.tab("playlists", "Playlists", list_playlists)
|
||||||
|
.tab("shows", "Podcasts", list_shows);
|
||||||
|
|
||||||
SearchView {
|
SearchView {
|
||||||
results_tracks,
|
results_tracks,
|
||||||
@@ -93,6 +101,8 @@ impl SearchView {
|
|||||||
pagination_artists,
|
pagination_artists,
|
||||||
results_playlists,
|
results_playlists,
|
||||||
pagination_playlists,
|
pagination_playlists,
|
||||||
|
results_shows,
|
||||||
|
pagination_shows,
|
||||||
edit: searchfield,
|
edit: searchfield,
|
||||||
tabs: tabs.with_name(LIST_ID),
|
tabs: tabs.with_name(LIST_ID),
|
||||||
edit_focused: true,
|
edit_focused: true,
|
||||||
@@ -264,6 +274,29 @@ impl SearchView {
|
|||||||
0
|
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>(
|
fn perform_search<I: ListItem>(
|
||||||
&self,
|
&self,
|
||||||
handler: SearchHandler<I>,
|
handler: SearchHandler<I>,
|
||||||
@@ -331,6 +364,8 @@ impl SearchView {
|
|||||||
*results_artists.write().unwrap() = Vec::new();
|
*results_artists.write().unwrap() = Vec::new();
|
||||||
let results_playlists = self.results_playlists.clone();
|
let results_playlists = self.results_playlists.clone();
|
||||||
*results_playlists.write().unwrap() = Vec::new();
|
*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();
|
let mut tab_view = self.tabs.get_mut();
|
||||||
match uritype {
|
match uritype {
|
||||||
@@ -396,6 +431,12 @@ impl SearchView {
|
|||||||
&query,
|
&query,
|
||||||
Some(&self.pagination_playlists),
|
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
46
src/ui/show.rs
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,6 +9,7 @@ use cursive::Printer;
|
|||||||
use unicode_width::UnicodeWidthStr;
|
use unicode_width::UnicodeWidthStr;
|
||||||
|
|
||||||
use crate::library::Library;
|
use crate::library::Library;
|
||||||
|
use crate::playable::Playable;
|
||||||
use crate::queue::{Queue, RepeatSetting};
|
use crate::queue::{Queue, RepeatSetting};
|
||||||
use crate::spotify::{PlayerEvent, Spotify};
|
use crate::spotify::{PlayerEvent, Spotify};
|
||||||
|
|
||||||
@@ -132,51 +133,54 @@ impl View for StatusBar {
|
|||||||
printer.print((0, 0), &"┉".repeat(printer.size.x));
|
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 = self.spotify.get_current_progress();
|
let elapsed_ms = elapsed.as_millis() as u32;
|
||||||
let elapsed_ms = elapsed.as_millis() as u32;
|
|
||||||
|
|
||||||
let formatted_elapsed = format!(
|
let formatted_elapsed = format!(
|
||||||
"{:02}:{:02}",
|
"{:02}:{:02}",
|
||||||
elapsed.as_secs() / 60,
|
elapsed.as_secs() / 60,
|
||||||
elapsed.as_secs() % 60
|
elapsed.as_secs() % 60
|
||||||
);
|
);
|
||||||
|
|
||||||
let saved = if self.library.is_saved_track(t) {
|
let playback_duration_status = match self.queue.get_current() {
|
||||||
if self.use_nerdfont {
|
Some(ref t) => format!("{} / {}", formatted_elapsed, t.duration_str()),
|
||||||
"\u{f62b} "
|
None => "".to_string(),
|
||||||
} else {
|
};
|
||||||
"✓ "
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
""
|
|
||||||
};
|
|
||||||
|
|
||||||
let right = updating.to_string()
|
let right = updating.to_string()
|
||||||
+ repeat
|
+ repeat
|
||||||
+ shuffle
|
+ shuffle
|
||||||
+ saved
|
// + saved
|
||||||
+ &format!("{} / {}", formatted_elapsed, t.duration_str())
|
+ &playback_duration_status
|
||||||
+ &volume;
|
+ &volume;
|
||||||
let offset = HAlign::Right.get_offset(right.width(), printer.size.x);
|
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((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| {
|
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));
|
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) {
|
fn layout(&mut self, size: Vec2) {
|
||||||
@@ -208,7 +212,7 @@ impl View for StatusBar {
|
|||||||
if event == MouseEvent::Press(MouseButton::Left)
|
if event == MouseEvent::Press(MouseButton::Left)
|
||||||
|| event == MouseEvent::Hold(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 f: f32 = position.x as f32 / self.last_size.x as f32;
|
||||||
let new = t.duration as f32 * f;
|
let new = t.duration as f32 * f;
|
||||||
self.spotify.seek(new as u32);
|
self.spotify.seek(new as u32);
|
||||||
|
|||||||
Reference in New Issue
Block a user