Podcast support improvements. (#229)
* Added OpenUri D-BUS MPRIS support. Removed "user:" from URIType check because Spotify doesn't always provide it. * Added tags to .gitignore * Changed mpris metadata to actually return the track's url instead of the Spotify URI so that it matches the functionality of the official Spotify client. * Changed mpris:trackid and xesam:url to not use static naming so it can support podcasts. * Changed xesam:url to default to an empty string instead of "0" * Added possibility to start playing Shows and Episodes via MPRIS. Added possibility to search for Podcast Episodes.
This commit is contained in:
@@ -2,7 +2,7 @@ use crate::library::Library;
|
||||
use crate::playable::Playable;
|
||||
use crate::queue::Queue;
|
||||
use crate::traits::{ListItem, ViewExt};
|
||||
use rspotify::model::show::SimplifiedEpisode;
|
||||
use rspotify::model::show::{SimplifiedEpisode, FullEpisode};
|
||||
use std::fmt;
|
||||
use std::sync::Arc;
|
||||
|
||||
@@ -39,6 +39,20 @@ impl From<&SimplifiedEpisode> for Episode {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&FullEpisode> for Episode {
|
||||
fn from(episode: &FullEpisode) -> 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)
|
||||
|
||||
36
src/mpris.rs
36
src/mpris.rs
@@ -11,12 +11,15 @@ use dbus::{Path, SignalArgs};
|
||||
use crate::album::Album;
|
||||
use crate::playable::Playable;
|
||||
use crate::playlist::Playlist;
|
||||
use crate::show::Show;
|
||||
use crate::queue::{Queue, RepeatSetting};
|
||||
use crate::spotify::{PlayerEvent, Spotify, URIType};
|
||||
use crate::track::Track;
|
||||
use crate::traits::ListItem;
|
||||
use crate::episode::Episode;
|
||||
|
||||
type Metadata = HashMap<String, Variant<Box<dyn RefArg>>>;
|
||||
|
||||
struct MprisState(String, Option<Playable>);
|
||||
|
||||
fn get_playbackstatus(spotify: Arc<Spotify>) -> String {
|
||||
@@ -25,7 +28,7 @@ fn get_playbackstatus(spotify: Arc<Spotify>) -> String {
|
||||
PlayerEvent::Paused => "Paused",
|
||||
_ => "Stopped",
|
||||
}
|
||||
.to_string()
|
||||
.to_string()
|
||||
}
|
||||
|
||||
fn get_metadata(playable: Option<Playable>) -> Metadata {
|
||||
@@ -127,7 +130,7 @@ fn run_dbus_server(spotify: Arc<Spotify>, queue: Arc<Queue>, rx: mpsc::Receiver<
|
||||
"org.mpris.MediaPlayer2.ncspot",
|
||||
dbus::NameFlag::ReplaceExisting as u32,
|
||||
)
|
||||
.expect("Failed to register dbus player name");
|
||||
.expect("Failed to register dbus player name");
|
||||
|
||||
let f = Factory::new_fn::<()>();
|
||||
|
||||
@@ -220,7 +223,7 @@ fn run_dbus_server(spotify: Arc<Spotify>, queue: Arc<Queue>, rx: mpsc::Receiver<
|
||||
RepeatSetting::RepeatTrack => "Track",
|
||||
RepeatSetting::RepeatPlaylist => "Playlist",
|
||||
}
|
||||
.to_string(),
|
||||
.to_string(),
|
||||
);
|
||||
Ok(())
|
||||
})
|
||||
@@ -470,6 +473,31 @@ fn run_dbus_server(spotify: Arc<Spotify>, queue: Arc<Queue>, rx: mpsc::Receiver<
|
||||
}
|
||||
}
|
||||
}
|
||||
Some(URIType::Show) => {
|
||||
if let Some(s) = spotify.get_show(&id) {
|
||||
let mut show = Show::from(&s);
|
||||
let spotify = spotify.clone();
|
||||
show.load_episodes(spotify);
|
||||
if let Some(e) = &show.episodes {
|
||||
queue.clear();
|
||||
let mut ep = e.clone();
|
||||
ep.reverse();
|
||||
let index = queue.append_next(
|
||||
ep.iter()
|
||||
.map(|episode| Playable::Episode(episode.clone()))
|
||||
.collect(),
|
||||
);
|
||||
queue.play(index, false, false)
|
||||
}
|
||||
}
|
||||
}
|
||||
Some(URIType::Episode) => {
|
||||
if let Some(e) = spotify.episode(&id) {
|
||||
queue.clear();
|
||||
queue.append(Playable::Episode(Episode::from(&e)));
|
||||
queue.play(0, false, false)
|
||||
}
|
||||
}
|
||||
Some(URIType::Artist) => {}
|
||||
None => {}
|
||||
}
|
||||
@@ -545,7 +573,7 @@ fn run_dbus_server(spotify: Arc<Spotify>, queue: Arc<Queue>, rx: mpsc::Receiver<
|
||||
conn.send(
|
||||
changed.to_emit_message(&Path::new("/org/mpris/MediaPlayer2".to_string()).unwrap()),
|
||||
)
|
||||
.unwrap();
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
16
src/show.rs
16
src/show.rs
@@ -5,7 +5,7 @@ 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 rspotify::model::show::{SimplifiedShow, FullShow};
|
||||
use std::fmt;
|
||||
use std::sync::Arc;
|
||||
|
||||
@@ -63,6 +63,20 @@ impl From<&SimplifiedShow> for Show {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&FullShow> for Show {
|
||||
fn from(show: &FullShow) -> 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)
|
||||
|
||||
@@ -58,7 +58,7 @@ use crate::events::{Event, EventManager};
|
||||
use crate::playable::Playable;
|
||||
use crate::queue;
|
||||
use crate::track::Track;
|
||||
use rspotify::model::show::{Show, SimplifiedEpisode};
|
||||
use rspotify::model::show::{Show, SimplifiedEpisode, FullShow, FullEpisode};
|
||||
|
||||
pub const VOLUME_PERCENT: u16 = ((u16::max_value() as f64) * 1.0 / 100.0) as u16;
|
||||
|
||||
@@ -663,6 +663,14 @@ impl Spotify {
|
||||
self.api_with_retry(|api| api.track(track_id))
|
||||
}
|
||||
|
||||
pub fn get_show(&self, show_id: &str) -> Option<FullShow> {
|
||||
self.api_with_retry(|api| api.get_a_show(show_id.to_string(), None))
|
||||
}
|
||||
|
||||
pub fn episode(&self, episode_id: &str) -> Option<FullEpisode> {
|
||||
self.api_with_retry(|api| api.get_an_episode(episode_id.to_string(), None))
|
||||
}
|
||||
|
||||
pub fn search(
|
||||
&self,
|
||||
searchtype: SearchType,
|
||||
@@ -904,6 +912,8 @@ pub enum URIType {
|
||||
Artist,
|
||||
Track,
|
||||
Playlist,
|
||||
Show,
|
||||
Episode
|
||||
}
|
||||
|
||||
impl URIType {
|
||||
@@ -916,6 +926,10 @@ impl URIType {
|
||||
Some(URIType::Track)
|
||||
} else if s.starts_with("spotify:") && s.contains(":playlist:") {
|
||||
Some(URIType::Playlist)
|
||||
} else if s.starts_with("spotify:show:"){
|
||||
Some(URIType::Show)
|
||||
} else if s.starts_with("spotify:episode:"){
|
||||
Some(URIType::Episode)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ use crate::ui::listview::{ListView, Pagination};
|
||||
use crate::ui::tabview::TabView;
|
||||
use rspotify::model::search::SearchResult;
|
||||
use rspotify::senum::SearchType;
|
||||
use crate::episode::Episode;
|
||||
|
||||
pub struct SearchView {
|
||||
results_tracks: Arc<RwLock<Vec<Track>>>,
|
||||
@@ -37,6 +38,8 @@ pub struct SearchView {
|
||||
pagination_playlists: Pagination<Playlist>,
|
||||
results_shows: Arc<RwLock<Vec<Show>>>,
|
||||
pagination_shows: Pagination<Show>,
|
||||
results_episodes: Arc<RwLock<Vec<Episode>>>,
|
||||
pagination_episodes: Pagination<Episode>,
|
||||
edit: NamedView<EditView>,
|
||||
tabs: NamedView<TabView>,
|
||||
edit_focused: bool,
|
||||
@@ -61,6 +64,7 @@ impl SearchView {
|
||||
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 results_episodes = Arc::new(RwLock::new(Vec::new()));
|
||||
|
||||
let searchfield = EditView::new()
|
||||
.on_submit(move |s, input| {
|
||||
@@ -82,15 +86,18 @@ impl SearchView {
|
||||
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 list_shows = ListView::new(results_shows.clone(), queue.clone(), library.clone());
|
||||
let pagination_shows = list_shows.get_pagination().clone();
|
||||
let list_episodes = ListView::new(results_episodes.clone(), queue, library);
|
||||
let pagination_episodes = list_episodes.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("shows", "Podcasts", list_shows);
|
||||
.tab("shows", "Podcasts", list_shows)
|
||||
.tab("episodes", "Podcast Episodes", list_episodes);
|
||||
|
||||
SearchView {
|
||||
results_tracks,
|
||||
@@ -103,6 +110,8 @@ impl SearchView {
|
||||
pagination_playlists,
|
||||
results_shows,
|
||||
pagination_shows,
|
||||
results_episodes,
|
||||
pagination_episodes,
|
||||
edit: searchfield,
|
||||
tabs: tabs.with_name(LIST_ID),
|
||||
edit_focused: true,
|
||||
@@ -274,6 +283,22 @@ impl SearchView {
|
||||
0
|
||||
}
|
||||
|
||||
fn get_show(
|
||||
spotify: &Arc<Spotify>,
|
||||
shows: &Arc<RwLock<Vec<Show>>>,
|
||||
query: &str,
|
||||
_offset: usize,
|
||||
_append: bool,
|
||||
) -> u32 {
|
||||
if let Some(result) = spotify.get_show(&query).as_ref() {
|
||||
let pls = vec![result.into()];
|
||||
let mut r = shows.write().unwrap();
|
||||
*r = pls;
|
||||
return 1;
|
||||
}
|
||||
0
|
||||
}
|
||||
|
||||
fn search_show(
|
||||
spotify: &Arc<Spotify>,
|
||||
shows: &Arc<RwLock<Vec<Show>>>,
|
||||
@@ -297,6 +322,45 @@ impl SearchView {
|
||||
0
|
||||
}
|
||||
|
||||
fn get_episode(
|
||||
spotify: &Arc<Spotify>,
|
||||
episodes: &Arc<RwLock<Vec<Episode>>>,
|
||||
query: &str,
|
||||
_offset: usize,
|
||||
_append: bool,
|
||||
) -> u32 {
|
||||
if let Some(result) = spotify.episode(&query).as_ref() {
|
||||
let e = vec![result.into()];
|
||||
let mut r = episodes.write().unwrap();
|
||||
*r = e;
|
||||
return 1
|
||||
}
|
||||
0
|
||||
}
|
||||
|
||||
fn search_episode(
|
||||
spotify: &Arc<Spotify>,
|
||||
episodes: &Arc<RwLock<Vec<Episode>>>,
|
||||
query: &str,
|
||||
offset: usize,
|
||||
append: bool,
|
||||
) -> u32 {
|
||||
if let Some(SearchResult::Episodes(results)) =
|
||||
spotify.search(SearchType::Episode, &query, 50, offset as u32)
|
||||
{
|
||||
let mut e = results.items.iter().map(|se| se.into()).collect();
|
||||
let mut r = episodes.write().unwrap();
|
||||
|
||||
if append {
|
||||
r.append(&mut e);
|
||||
} else {
|
||||
*r = e;
|
||||
}
|
||||
return results.total;
|
||||
}
|
||||
0
|
||||
}
|
||||
|
||||
fn perform_search<I: ListItem>(
|
||||
&self,
|
||||
handler: SearchHandler<I>,
|
||||
@@ -366,6 +430,8 @@ impl SearchView {
|
||||
*results_playlists.write().unwrap() = Vec::new();
|
||||
let results_shows = self.results_shows.clone();
|
||||
*results_shows.write().unwrap() = Vec::new();
|
||||
let results_episodes = self.results_episodes.clone();
|
||||
*results_episodes.write().unwrap() = Vec::new();
|
||||
|
||||
let mut tab_view = self.tabs.get_mut();
|
||||
match uritype {
|
||||
@@ -405,6 +471,24 @@ impl SearchView {
|
||||
);
|
||||
tab_view.move_focus_to(3);
|
||||
}
|
||||
URIType::Show => {
|
||||
self.perform_search(
|
||||
Box::new(Self::get_show),
|
||||
&self.results_shows,
|
||||
&query,
|
||||
None,
|
||||
);
|
||||
tab_view.move_focus_to(4);
|
||||
}
|
||||
URIType::Episode => {
|
||||
self.perform_search(
|
||||
Box::new(Self::get_episode),
|
||||
&self.results_episodes,
|
||||
&query,
|
||||
None,
|
||||
);
|
||||
tab_view.move_focus_to(5);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
self.perform_search(
|
||||
@@ -437,6 +521,12 @@ impl SearchView {
|
||||
&query,
|
||||
Some(&self.pagination_shows),
|
||||
);
|
||||
self.perform_search(
|
||||
Box::new(Self::search_episode),
|
||||
&self.results_episodes,
|
||||
&query,
|
||||
Some(&self.pagination_episodes),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user