podcast support (#203)

* implement search for shows/podcasts

* create Playable supertype for queue to contain tracks and episodes

* wip: implement playback of episodes

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

* show duration for podcast episodes

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

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

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

* refactor: extract Playable code to separate file

* implement playback/queuing of shows + sharing url

* implement podcast library

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

View File

@@ -37,7 +37,12 @@ impl LibraryView {
.tab(
"playlists",
"Playlists",
PlaylistsView::new(queue, library.clone()),
PlaylistsView::new(queue.clone(), library.clone()),
)
.tab(
"podcasts",
"Podcasts",
ListView::new(library.shows.clone(), queue, library.clone()),
);
Self { tabs }

View File

@@ -12,6 +12,7 @@ use unicode_width::UnicodeWidthStr;
use crate::command::{Command, GotoMode, MoveAmount, MoveMode, TargetMode};
use crate::commands::CommandResult;
use crate::library::Library;
use crate::playable::Playable;
use crate::queue::Queue;
use crate::track::Track;
use crate::traits::{IntoBoxedViewExt, ListItem, ViewExt};
@@ -146,7 +147,10 @@ impl<I: ListItem> ListView<I> {
let content = self.content.read().unwrap();
let any = &(*content) as &dyn std::any::Any;
if let Some(tracks) = any.downcast_ref::<Vec<Track>>() {
let tracks: Vec<&Track> = tracks.iter().collect();
let tracks: Vec<Playable> = tracks
.iter()
.map(|track| Playable::Track(track.clone()))
.collect();
let index = self.queue.append_next(tracks);
self.queue.play(index + self.selected, true, false);
true
@@ -351,7 +355,10 @@ impl<I: ListItem + Clone> ViewExt for ListView<I> {
TargetMode::Selected => self.content.read().ok().and_then(|content| {
content.get(self.selected).and_then(ListItem::share_url)
}),
TargetMode::Current => self.queue.get_current().and_then(|t| t.share_url()),
TargetMode::Current => self
.queue
.get_current()
.and_then(|t| t.as_listitem().share_url()),
};
if let Some(url) = url {

View File

@@ -10,5 +10,6 @@ pub mod playlist;
pub mod playlists;
pub mod queue;
pub mod search;
pub mod show;
pub mod statusbar;
pub mod tabview;

View File

@@ -9,14 +9,14 @@ use std::sync::Arc;
use crate::command::{Command, MoveMode, ShiftMode};
use crate::commands::CommandResult;
use crate::library::Library;
use crate::playable::Playable;
use crate::queue::Queue;
use crate::track::Track;
use crate::traits::ViewExt;
use crate::ui::listview::ListView;
use crate::ui::modal::Modal;
pub struct QueueView {
list: ListView<Track>,
list: ListView<Playable>,
library: Arc<Library>,
queue: Arc<Queue>,
}
@@ -85,7 +85,7 @@ impl QueueView {
}
impl ViewWrapper for QueueView {
wrap_impl!(self.list: ListView<Track>);
wrap_impl!(self.list: ListView<Playable>);
}
impl ViewExt for QueueView {

View File

@@ -17,6 +17,7 @@ use crate::events::EventManager;
use crate::library::Library;
use crate::playlist::Playlist;
use crate::queue::Queue;
use crate::show::Show;
use crate::spotify::{Spotify, URIType};
use crate::track::Track;
use crate::traits::{ListItem, ViewExt};
@@ -34,6 +35,8 @@ pub struct SearchView {
pagination_artists: Pagination<Artist>,
results_playlists: Arc<RwLock<Vec<Playlist>>>,
pagination_playlists: Pagination<Playlist>,
results_shows: Arc<RwLock<Vec<Show>>>,
pagination_shows: Pagination<Show>,
edit: NamedView<EditView>,
tabs: NamedView<TabView>,
edit_focused: bool,
@@ -57,6 +60,7 @@ impl SearchView {
let results_albums = Arc::new(RwLock::new(Vec::new()));
let results_artists = Arc::new(RwLock::new(Vec::new()));
let results_playlists = Arc::new(RwLock::new(Vec::new()));
let results_shows = Arc::new(RwLock::new(Vec::new()));
let searchfield = EditView::new()
.on_submit(move |s, input| {
@@ -75,14 +79,18 @@ impl SearchView {
let pagination_albums = list_albums.get_pagination().clone();
let list_artists = ListView::new(results_artists.clone(), queue.clone(), library.clone());
let pagination_artists = list_artists.get_pagination().clone();
let list_playlists = ListView::new(results_playlists.clone(), queue, library);
let list_playlists =
ListView::new(results_playlists.clone(), queue.clone(), library.clone());
let pagination_playlists = list_playlists.get_pagination().clone();
let list_shows = ListView::new(results_shows.clone(), queue, library);
let pagination_shows = list_shows.get_pagination().clone();
let tabs = TabView::new()
.tab("tracks", "Tracks", list_tracks)
.tab("albums", "Albums", list_albums)
.tab("artists", "Artists", list_artists)
.tab("playlists", "Playlists", list_playlists);
.tab("playlists", "Playlists", list_playlists)
.tab("shows", "Podcasts", list_shows);
SearchView {
results_tracks,
@@ -93,6 +101,8 @@ impl SearchView {
pagination_artists,
results_playlists,
pagination_playlists,
results_shows,
pagination_shows,
edit: searchfield,
tabs: tabs.with_name(LIST_ID),
edit_focused: true,
@@ -264,6 +274,29 @@ impl SearchView {
0
}
fn search_show(
spotify: &Arc<Spotify>,
shows: &Arc<RwLock<Vec<Show>>>,
query: &str,
offset: usize,
append: bool,
) -> u32 {
if let Some(SearchResult::Shows(results)) =
spotify.search(SearchType::Show, &query, 50, offset as u32)
{
let mut pls = results.items.iter().map(|sp| sp.into()).collect();
let mut r = shows.write().unwrap();
if append {
r.append(&mut pls);
} else {
*r = pls;
}
return results.total;
}
0
}
fn perform_search<I: ListItem>(
&self,
handler: SearchHandler<I>,
@@ -331,6 +364,8 @@ impl SearchView {
*results_artists.write().unwrap() = Vec::new();
let results_playlists = self.results_playlists.clone();
*results_playlists.write().unwrap() = Vec::new();
let results_shows = self.results_shows.clone();
*results_shows.write().unwrap() = Vec::new();
let mut tab_view = self.tabs.get_mut();
match uritype {
@@ -396,6 +431,12 @@ impl SearchView {
&query,
Some(&self.pagination_playlists),
);
self.perform_search(
Box::new(Self::search_show),
&self.results_shows,
&query,
Some(&self.pagination_shows),
);
}
}
}

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

@@ -0,0 +1,46 @@
use std::sync::{Arc, RwLock};
use cursive::view::ViewWrapper;
use cursive::Cursive;
use crate::command::Command;
use crate::commands::CommandResult;
use crate::episode::Episode;
use crate::library::Library;
use crate::queue::Queue;
use crate::show::Show;
use crate::traits::ViewExt;
use crate::ui::listview::ListView;
pub struct ShowView {
list: ListView<Episode>,
show: Show,
}
impl ShowView {
pub fn new(queue: Arc<Queue>, library: Arc<Library>, show: &Show) -> Self {
let mut show = show.clone();
show.load_episodes(queue.get_spotify());
let episodes = show.episodes.clone().unwrap_or_default();
Self {
list: ListView::new(Arc::new(RwLock::new(episodes)), queue, library),
show,
}
}
}
impl ViewWrapper for ShowView {
wrap_impl!(self.list: ListView<Episode>);
}
impl ViewExt for ShowView {
fn title(&self) -> String {
self.show.name.clone()
}
fn on_command(&mut self, s: &mut Cursive, cmd: &Command) -> Result<CommandResult, String> {
self.list.on_command(s, cmd)
}
}

View File

@@ -9,6 +9,7 @@ use cursive::Printer;
use unicode_width::UnicodeWidthStr;
use crate::library::Library;
use crate::playable::Playable;
use crate::queue::{Queue, RepeatSetting};
use crate::spotify::{PlayerEvent, Spotify};
@@ -132,51 +133,54 @@ impl View for StatusBar {
printer.print((0, 0), &"".repeat(printer.size.x));
});
if let Some(ref t) = self.queue.get_current() {
let elapsed = self.spotify.get_current_progress();
let elapsed_ms = elapsed.as_millis() as u32;
let elapsed = self.spotify.get_current_progress();
let elapsed_ms = elapsed.as_millis() as u32;
let formatted_elapsed = format!(
"{:02}:{:02}",
elapsed.as_secs() / 60,
elapsed.as_secs() % 60
);
let formatted_elapsed = format!(
"{:02}:{:02}",
elapsed.as_secs() / 60,
elapsed.as_secs() % 60
);
let saved = if self.library.is_saved_track(t) {
if self.use_nerdfont {
"\u{f62b} "
} else {
""
}
} else {
""
};
let playback_duration_status = match self.queue.get_current() {
Some(ref t) => format!("{} / {}", formatted_elapsed, t.duration_str()),
None => "".to_string(),
};
let right = updating.to_string()
+ repeat
+ shuffle
+ saved
+ &format!("{} / {}", formatted_elapsed, t.duration_str())
+ &volume;
let offset = HAlign::Right.get_offset(right.width(), printer.size.x);
let right = updating.to_string()
+ repeat
+ shuffle
// + saved
+ &playback_duration_status
+ &volume;
let offset = HAlign::Right.get_offset(right.width(), printer.size.x);
printer.with_color(style, |printer| {
printer.with_color(style, |printer| {
if let Some(ref t) = self.queue.get_current() {
printer.print((4, 1), &t.to_string());
printer.print((offset, 1), &right);
});
}
printer.print((offset, 1), &right);
});
if let Some(t) = self.queue.get_current() {
printer.with_color(style_bar, |printer| {
let duration_width = (((printer.size.x as u32) * elapsed_ms) / t.duration) as usize;
let duration_width =
(((printer.size.x as u32) * elapsed_ms) / t.duration()) as usize;
printer.print((0, 0), &"".repeat(duration_width + 1));
});
} else {
let right = updating.to_string() + repeat + shuffle + &volume;
let offset = HAlign::Right.get_offset(right.width(), printer.size.x);
printer.with_color(style, |printer| {
printer.print((offset, 1), &right);
});
}
// if let Some(Playable::Track(ref t)) = self.queue.get_current() {
// let saved = if self.library.is_saved_track(&Playable::Track(t.clone())) {
// if self.use_nerdfont {
// "\u{f62b} "
// } else {
// "✓ "
// }
// } else {
// ""
// };
// }
}
fn layout(&mut self, size: Vec2) {
@@ -208,7 +212,7 @@ impl View for StatusBar {
if event == MouseEvent::Press(MouseButton::Left)
|| event == MouseEvent::Hold(MouseButton::Left)
{
if let Some(ref t) = self.queue.get_current() {
if let Some(Playable::Track(ref t)) = self.queue.get_current() {
let f: f32 = position.x as f32 / self.last_size.x as f32;
let new = t.duration as f32 * f;
self.spotify.seek(new as u32);