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::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()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
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::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();
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
74
src/mpris.rs
74
src/mpris.rs
@@ -9,13 +9,15 @@ use dbus::tree::{Access, Factory};
|
||||
use dbus::{Path, SignalArgs};
|
||||
|
||||
use crate::album::Album;
|
||||
use crate::playable::Playable;
|
||||
use crate::playlist::Playlist;
|
||||
use crate::queue::{Queue, RepeatSetting};
|
||||
use crate::spotify::{PlayerEvent, Spotify, URIType};
|
||||
use crate::track::Track;
|
||||
use crate::traits::ListItem;
|
||||
|
||||
type Metadata = HashMap<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
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 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()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
12
src/queue.rs
12
src/queue.rs
@@ -4,8 +4,8 @@ use std::sync::{Arc, RwLock};
|
||||
use rand::prelude::*;
|
||||
use strum_macros::Display;
|
||||
|
||||
use crate::playable::Playable;
|
||||
use crate::spotify::Spotify;
|
||||
use crate::track::Track;
|
||||
|
||||
#[derive(Display, Clone, Copy, PartialEq, Debug, Serialize, Deserialize)]
|
||||
pub enum RepeatSetting {
|
||||
@@ -15,7 +15,7 @@ pub enum RepeatSetting {
|
||||
}
|
||||
|
||||
pub struct Queue {
|
||||
pub queue: Arc<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
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::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) {
|
||||
|
||||
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::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);
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
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 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);
|
||||
|
||||
Reference in New Issue
Block a user