Refactor: extract Spotify Worker to separate file
This commit is contained in:
@@ -64,6 +64,7 @@ mod sharing;
|
|||||||
mod show;
|
mod show;
|
||||||
mod spotify;
|
mod spotify;
|
||||||
mod spotify_url;
|
mod spotify_url;
|
||||||
|
mod spotify_worker;
|
||||||
mod theme;
|
mod theme;
|
||||||
mod track;
|
mod track;
|
||||||
mod traits;
|
mod traits;
|
||||||
|
|||||||
16
src/mpris.rs
16
src/mpris.rs
@@ -19,7 +19,7 @@ use crate::playable::Playable;
|
|||||||
use crate::playlist::Playlist;
|
use crate::playlist::Playlist;
|
||||||
use crate::queue::{Queue, RepeatSetting};
|
use crate::queue::{Queue, RepeatSetting};
|
||||||
use crate::show::Show;
|
use crate::show::Show;
|
||||||
use crate::spotify::{PlayerEvent, Spotify, URIType, VOLUME_PERCENT};
|
use crate::spotify::{PlayerEvent, Spotify, UriType, VOLUME_PERCENT};
|
||||||
use crate::track::Track;
|
use crate::track::Track;
|
||||||
use crate::traits::ListItem;
|
use crate::traits::ListItem;
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
@@ -538,9 +538,9 @@ fn run_dbus_server(
|
|||||||
None => "".to_string(),
|
None => "".to_string(),
|
||||||
};
|
};
|
||||||
let id = &uri[uri.rfind(':').unwrap_or(0) + 1..uri.len()];
|
let id = &uri[uri.rfind(':').unwrap_or(0) + 1..uri.len()];
|
||||||
let uri_type = URIType::from_uri(&uri);
|
let uri_type = UriType::from_uri(&uri);
|
||||||
match uri_type {
|
match uri_type {
|
||||||
Some(URIType::Album) => {
|
Some(UriType::Album) => {
|
||||||
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();
|
||||||
@@ -553,14 +553,14 @@ fn run_dbus_server(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
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(Playable::Track(Track::from(&t)));
|
queue.append(Playable::Track(Track::from(&t)));
|
||||||
queue.play(0, false, false)
|
queue.play(0, false, false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Some(URIType::Playlist) => {
|
Some(UriType::Playlist) => {
|
||||||
if let Some(p) = spotify.playlist(&id) {
|
if let Some(p) = spotify.playlist(&id) {
|
||||||
let mut playlist = Playlist::from(&p);
|
let mut playlist = Playlist::from(&p);
|
||||||
let spotify = spotify.clone();
|
let spotify = spotify.clone();
|
||||||
@@ -576,7 +576,7 @@ fn run_dbus_server(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Some(URIType::Show) => {
|
Some(UriType::Show) => {
|
||||||
if let Some(s) = spotify.get_show(&id) {
|
if let Some(s) = spotify.get_show(&id) {
|
||||||
let mut show = Show::from(&s);
|
let mut show = Show::from(&s);
|
||||||
let spotify = spotify.clone();
|
let spotify = spotify.clone();
|
||||||
@@ -594,14 +594,14 @@ fn run_dbus_server(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Some(URIType::Episode) => {
|
Some(UriType::Episode) => {
|
||||||
if let Some(e) = spotify.episode(&id) {
|
if let Some(e) = spotify.episode(&id) {
|
||||||
queue.clear();
|
queue.clear();
|
||||||
queue.append(Playable::Episode(Episode::from(&e)));
|
queue.append(Playable::Episode(Episode::from(&e)));
|
||||||
queue.play(0, false, false)
|
queue.play(0, false, false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Some(URIType::Artist) => {
|
Some(UriType::Artist) => {
|
||||||
if let Some(a) = spotify.artist_top_tracks(&id) {
|
if let Some(a) = spotify.artist_top_tracks(&id) {
|
||||||
queue.clear();
|
queue.clear();
|
||||||
queue.append_next(a.iter().map(|track| Playable::Track(track.clone())).collect());
|
queue.append_next(a.iter().map(|track| Playable::Track(track.clone())).collect());
|
||||||
|
|||||||
203
src/spotify.rs
203
src/spotify.rs
@@ -4,13 +4,11 @@ use librespot_core::config::SessionConfig;
|
|||||||
use librespot_core::keymaster::Token;
|
use librespot_core::keymaster::Token;
|
||||||
use librespot_core::mercury::MercuryError;
|
use librespot_core::mercury::MercuryError;
|
||||||
use librespot_core::session::Session;
|
use librespot_core::session::Session;
|
||||||
use librespot_core::spotify_id::{SpotifyAudioType, SpotifyId};
|
|
||||||
use librespot_playback::config::PlayerConfig;
|
use librespot_playback::config::PlayerConfig;
|
||||||
|
|
||||||
use librespot_playback::audio_backend;
|
use librespot_playback::audio_backend;
|
||||||
use librespot_playback::config::Bitrate;
|
use librespot_playback::config::Bitrate;
|
||||||
use librespot_playback::mixer::Mixer;
|
use librespot_playback::player::Player;
|
||||||
use librespot_playback::player::{Player, PlayerEvent as LibrespotPlayerEvent};
|
|
||||||
|
|
||||||
use rspotify::blocking::client::Spotify as SpotifyAPI;
|
use rspotify::blocking::client::Spotify as SpotifyAPI;
|
||||||
use rspotify::model::album::{FullAlbum, SavedAlbum, SimplifiedAlbum};
|
use rspotify::model::album::{FullAlbum, SavedAlbum, SimplifiedAlbum};
|
||||||
@@ -28,23 +26,15 @@ use serde_json::{json, Map};
|
|||||||
use failure::Error;
|
use failure::Error;
|
||||||
|
|
||||||
use futures_01::future::Future as v01_Future;
|
use futures_01::future::Future as v01_Future;
|
||||||
use futures_01::stream::Stream as v01_Stream;
|
|
||||||
use futures_01::sync::mpsc::UnboundedReceiver;
|
|
||||||
use futures_01::Async as v01_Async;
|
|
||||||
|
|
||||||
use futures::channel::mpsc;
|
use futures::channel::mpsc;
|
||||||
use futures::channel::oneshot;
|
use futures::channel::oneshot;
|
||||||
use futures::compat::Future01CompatExt;
|
use futures::compat::Future01CompatExt;
|
||||||
use futures::compat::Stream01CompatExt;
|
|
||||||
use futures::task::Context;
|
|
||||||
use futures::Future;
|
use futures::Future;
|
||||||
use futures::Stream;
|
|
||||||
|
|
||||||
use tokio_core::reactor::Core;
|
use tokio_core::reactor::Core;
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
use core::task::Poll;
|
|
||||||
|
|
||||||
use std::pin::Pin;
|
use std::pin::Pin;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
use std::sync::{Arc, RwLock};
|
use std::sync::{Arc, RwLock};
|
||||||
@@ -56,6 +46,7 @@ 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::playable::Playable;
|
||||||
|
use crate::spotify_worker::{Worker, WorkerCommand};
|
||||||
use crate::track::Track;
|
use crate::track::Track;
|
||||||
|
|
||||||
use rspotify::model::recommend::Recommendations;
|
use rspotify::model::recommend::Recommendations;
|
||||||
@@ -63,17 +54,6 @@ use rspotify::model::show::{FullEpisode, FullShow, 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 {
|
|
||||||
Load(Playable),
|
|
||||||
Play,
|
|
||||||
Pause,
|
|
||||||
Stop,
|
|
||||||
Seek(u32),
|
|
||||||
SetVolume(u16),
|
|
||||||
RequestToken(oneshot::Sender<Token>),
|
|
||||||
Shutdown,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq)]
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
pub enum PlayerEvent {
|
pub enum PlayerEvent {
|
||||||
Playing,
|
Playing,
|
||||||
@@ -96,165 +76,6 @@ pub struct Spotify {
|
|||||||
country: Option<Country>,
|
country: Option<Country>,
|
||||||
}
|
}
|
||||||
|
|
||||||
struct Worker {
|
|
||||||
events: EventManager,
|
|
||||||
player_events: UnboundedReceiver<LibrespotPlayerEvent>,
|
|
||||||
commands: Pin<Box<mpsc::UnboundedReceiver<WorkerCommand>>>,
|
|
||||||
session: Session,
|
|
||||||
player: Player,
|
|
||||||
refresh_task: Pin<Box<dyn Stream<Item = Result<(), tokio_timer::Error>>>>,
|
|
||||||
token_task: Pin<Box<dyn Future<Output = Result<(), MercuryError>>>>,
|
|
||||||
active: bool,
|
|
||||||
mixer: Box<dyn Mixer>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Worker {
|
|
||||||
fn new(
|
|
||||||
events: EventManager,
|
|
||||||
player_events: UnboundedReceiver<LibrespotPlayerEvent>,
|
|
||||||
commands: Pin<Box<mpsc::UnboundedReceiver<WorkerCommand>>>,
|
|
||||||
session: Session,
|
|
||||||
player: Player,
|
|
||||||
mixer: Box<dyn Mixer>,
|
|
||||||
) -> Worker {
|
|
||||||
Worker {
|
|
||||||
events,
|
|
||||||
player_events,
|
|
||||||
commands,
|
|
||||||
player,
|
|
||||||
session,
|
|
||||||
refresh_task: Box::pin(futures::stream::empty()),
|
|
||||||
token_task: Box::pin(futures::future::pending()),
|
|
||||||
active: false,
|
|
||||||
mixer,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Worker {
|
|
||||||
fn create_refresh(&self) -> Pin<Box<dyn Stream<Item = Result<(), tokio_timer::Error>>>> {
|
|
||||||
let ev = self.events.clone();
|
|
||||||
let future =
|
|
||||||
tokio_timer::Interval::new_interval(Duration::from_millis(400)).map(move |_| {
|
|
||||||
ev.trigger();
|
|
||||||
});
|
|
||||||
Box::pin(future.compat())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl futures::Future for Worker {
|
|
||||||
type Output = Result<(), ()>;
|
|
||||||
|
|
||||||
fn poll(mut self: Pin<&mut Self>, cx: &mut Context) -> futures::task::Poll<Self::Output> {
|
|
||||||
loop {
|
|
||||||
let mut progress = false;
|
|
||||||
|
|
||||||
if self.session.is_invalid() {
|
|
||||||
self.events.send(Event::Player(PlayerEvent::Stopped));
|
|
||||||
return Poll::Ready(Result::Err(()));
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Poll::Ready(Some(cmd)) = self.commands.as_mut().poll_next(cx) {
|
|
||||||
progress = true;
|
|
||||||
debug!("message received!");
|
|
||||||
match cmd {
|
|
||||||
WorkerCommand::Load(playable) => match SpotifyId::from_uri(&playable.uri()) {
|
|
||||||
Ok(id) => {
|
|
||||||
info!("player loading track: {:?}", id);
|
|
||||||
if id.audio_type == SpotifyAudioType::NonPlayable {
|
|
||||||
warn!("track is not playable");
|
|
||||||
self.events.send(Event::Player(PlayerEvent::FinishedTrack));
|
|
||||||
} else {
|
|
||||||
self.player.load(id, true, 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
error!("error parsing uri: {:?}", e);
|
|
||||||
self.events.send(Event::Player(PlayerEvent::FinishedTrack));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
WorkerCommand::Play => {
|
|
||||||
self.player.play();
|
|
||||||
}
|
|
||||||
WorkerCommand::Pause => {
|
|
||||||
self.player.pause();
|
|
||||||
}
|
|
||||||
WorkerCommand::Stop => {
|
|
||||||
self.player.stop();
|
|
||||||
}
|
|
||||||
WorkerCommand::Seek(pos) => {
|
|
||||||
self.player.seek(pos);
|
|
||||||
}
|
|
||||||
WorkerCommand::SetVolume(volume) => {
|
|
||||||
self.mixer.set_volume(volume);
|
|
||||||
}
|
|
||||||
WorkerCommand::RequestToken(sender) => {
|
|
||||||
self.token_task = Spotify::get_token(&self.session, sender);
|
|
||||||
progress = true;
|
|
||||||
}
|
|
||||||
WorkerCommand::Shutdown => {
|
|
||||||
self.player.stop();
|
|
||||||
self.session.shutdown();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Ok(v01_Async::Ready(Some(event))) = self.player_events.poll() {
|
|
||||||
debug!("librespot player event: {:?}", event);
|
|
||||||
match event {
|
|
||||||
LibrespotPlayerEvent::Started { .. }
|
|
||||||
| LibrespotPlayerEvent::Loading { .. }
|
|
||||||
| LibrespotPlayerEvent::Changed { .. } => {
|
|
||||||
progress = true;
|
|
||||||
}
|
|
||||||
LibrespotPlayerEvent::Playing { .. } => {
|
|
||||||
self.events.send(Event::Player(PlayerEvent::Playing));
|
|
||||||
self.refresh_task = self.create_refresh();
|
|
||||||
self.active = true;
|
|
||||||
}
|
|
||||||
LibrespotPlayerEvent::Paused { .. } => {
|
|
||||||
self.events.send(Event::Player(PlayerEvent::Paused));
|
|
||||||
self.active = false;
|
|
||||||
}
|
|
||||||
LibrespotPlayerEvent::Stopped { .. } => {
|
|
||||||
self.events.send(Event::Player(PlayerEvent::Stopped));
|
|
||||||
self.active = false;
|
|
||||||
}
|
|
||||||
LibrespotPlayerEvent::EndOfTrack { .. } => {
|
|
||||||
self.events.send(Event::Player(PlayerEvent::FinishedTrack));
|
|
||||||
progress = true;
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Poll::Ready(Some(Ok(_))) = self.refresh_task.as_mut().poll_next(cx) {
|
|
||||||
self.refresh_task = if self.active {
|
|
||||||
progress = true;
|
|
||||||
self.create_refresh()
|
|
||||||
} else {
|
|
||||||
Box::pin(futures::stream::empty())
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
match self.token_task.as_mut().poll(cx) {
|
|
||||||
Poll::Ready(Ok(_)) => {
|
|
||||||
info!("token updated!");
|
|
||||||
self.token_task = Box::pin(futures::future::pending())
|
|
||||||
}
|
|
||||||
Poll::Ready(Err(e)) => {
|
|
||||||
error!("could not generate token: {:?}", e);
|
|
||||||
}
|
|
||||||
_ => (),
|
|
||||||
}
|
|
||||||
|
|
||||||
if !progress {
|
|
||||||
return Poll::Pending;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Spotify {
|
impl Spotify {
|
||||||
pub fn new(
|
pub fn new(
|
||||||
events: EventManager,
|
events: EventManager,
|
||||||
@@ -365,7 +186,7 @@ impl Spotify {
|
|||||||
.expect("could not open spotify session")
|
.expect("could not open spotify session")
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_token(
|
pub(crate) fn get_token(
|
||||||
session: &Session,
|
session: &Session,
|
||||||
sender: oneshot::Sender<Token>,
|
sender: oneshot::Sender<Token>,
|
||||||
) -> Pin<Box<dyn Future<Output = Result<(), MercuryError>>>> {
|
) -> Pin<Box<dyn Future<Output = Result<(), MercuryError>>>> {
|
||||||
@@ -955,7 +776,7 @@ impl Spotify {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, PartialEq)]
|
#[derive(Debug, PartialEq)]
|
||||||
pub enum URIType {
|
pub enum UriType {
|
||||||
Album,
|
Album,
|
||||||
Artist,
|
Artist,
|
||||||
Track,
|
Track,
|
||||||
@@ -964,20 +785,20 @@ pub enum URIType {
|
|||||||
Episode,
|
Episode,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl URIType {
|
impl UriType {
|
||||||
pub fn from_uri(s: &str) -> Option<URIType> {
|
pub fn from_uri(s: &str) -> Option<UriType> {
|
||||||
if s.starts_with("spotify:album:") {
|
if s.starts_with("spotify:album:") {
|
||||||
Some(URIType::Album)
|
Some(UriType::Album)
|
||||||
} else if s.starts_with("spotify:artist:") {
|
} else if s.starts_with("spotify:artist:") {
|
||||||
Some(URIType::Artist)
|
Some(UriType::Artist)
|
||||||
} else if s.starts_with("spotify:track:") {
|
} else if s.starts_with("spotify:track:") {
|
||||||
Some(URIType::Track)
|
Some(UriType::Track)
|
||||||
} else if s.starts_with("spotify:") && s.contains(":playlist:") {
|
} else if s.starts_with("spotify:") && s.contains(":playlist:") {
|
||||||
Some(URIType::Playlist)
|
Some(UriType::Playlist)
|
||||||
} else if s.starts_with("spotify:show:") {
|
} else if s.starts_with("spotify:show:") {
|
||||||
Some(URIType::Show)
|
Some(UriType::Show)
|
||||||
} else if s.starts_with("spotify:episode:") {
|
} else if s.starts_with("spotify:episode:") {
|
||||||
Some(URIType::Episode)
|
Some(UriType::Episode)
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
use crate::spotify::URIType;
|
use crate::spotify::UriType;
|
||||||
|
|
||||||
use url::{Host, Url};
|
use url::{Host, Url};
|
||||||
|
|
||||||
pub struct SpotifyURL {
|
pub struct SpotifyUrl {
|
||||||
pub id: String,
|
pub id: String,
|
||||||
pub uri_type: URIType,
|
pub uri_type: UriType,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SpotifyURL {
|
impl SpotifyUrl {
|
||||||
fn new(id: &str, uri_type: URIType) -> SpotifyURL {
|
fn new(id: &str, uri_type: UriType) -> SpotifyUrl {
|
||||||
SpotifyURL {
|
SpotifyUrl {
|
||||||
id: id.to_string(),
|
id: id.to_string(),
|
||||||
uri_type,
|
uri_type,
|
||||||
}
|
}
|
||||||
@@ -22,7 +22,7 @@ impl SpotifyURL {
|
|||||||
/// assert_eq!(result.id, "4uLU6hMCjMI75M1A2tKUQC");
|
/// assert_eq!(result.id, "4uLU6hMCjMI75M1A2tKUQC");
|
||||||
/// assert_eq!(result.uri_type, URIType::Track);
|
/// assert_eq!(result.uri_type, URIType::Track);
|
||||||
/// ```
|
/// ```
|
||||||
pub fn from_url(s: &str) -> Option<SpotifyURL> {
|
pub fn from_url(s: &str) -> Option<SpotifyUrl> {
|
||||||
let url = Url::parse(s).ok()?;
|
let url = Url::parse(s).ok()?;
|
||||||
if url.host() != Some(Host::Domain("open.spotify.com")) {
|
if url.host() != Some(Host::Domain("open.spotify.com")) {
|
||||||
return None;
|
return None;
|
||||||
@@ -33,12 +33,12 @@ impl SpotifyURL {
|
|||||||
let entity = path_segments.next()?;
|
let entity = path_segments.next()?;
|
||||||
|
|
||||||
let uri_type = match entity.to_lowercase().as_str() {
|
let uri_type = match entity.to_lowercase().as_str() {
|
||||||
"album" => Some(URIType::Album),
|
"album" => Some(UriType::Album),
|
||||||
"artist" => Some(URIType::Artist),
|
"artist" => Some(UriType::Artist),
|
||||||
"episode" => Some(URIType::Episode),
|
"episode" => Some(UriType::Episode),
|
||||||
"playlist" => Some(URIType::Playlist),
|
"playlist" => Some(UriType::Playlist),
|
||||||
"show" => Some(URIType::Show),
|
"show" => Some(UriType::Show),
|
||||||
"track" => Some(URIType::Track),
|
"track" => Some(UriType::Track),
|
||||||
"user" => {
|
"user" => {
|
||||||
let _user_id = path_segments.next()?;
|
let _user_id = path_segments.next()?;
|
||||||
let entity = path_segments.next()?;
|
let entity = path_segments.next()?;
|
||||||
@@ -47,14 +47,14 @@ impl SpotifyURL {
|
|||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
Some(URIType::Playlist)
|
Some(UriType::Playlist)
|
||||||
}
|
}
|
||||||
_ => None,
|
_ => None,
|
||||||
}?;
|
}?;
|
||||||
|
|
||||||
let id = path_segments.next()?;
|
let id = path_segments.next()?;
|
||||||
|
|
||||||
Some(SpotifyURL::new(id, uri_type))
|
Some(SpotifyUrl::new(id, uri_type))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,39 +62,39 @@ impl SpotifyURL {
|
|||||||
mod tests {
|
mod tests {
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
use super::SpotifyURL;
|
use super::SpotifyUrl;
|
||||||
use crate::spotify::URIType;
|
use crate::spotify::UriType;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_urls() {
|
fn test_urls() {
|
||||||
let mut test_cases = HashMap::new();
|
let mut test_cases = HashMap::new();
|
||||||
test_cases.insert(
|
test_cases.insert(
|
||||||
"https://open.spotify.com/playlist/1XFxe8bkTryTODn0lk4CNa?si=FfSpZ6KPQdieClZbwHakOQ",
|
"https://open.spotify.com/playlist/1XFxe8bkTryTODn0lk4CNa?si=FfSpZ6KPQdieClZbwHakOQ",
|
||||||
SpotifyURL::new("1XFxe8bkTryTODn0lk4CNa", URIType::Playlist),
|
SpotifyUrl::new("1XFxe8bkTryTODn0lk4CNa", UriType::Playlist),
|
||||||
);
|
);
|
||||||
test_cases.insert(
|
test_cases.insert(
|
||||||
"https://open.spotify.com/track/6fRJg3R90w0juYoCJXxj2d",
|
"https://open.spotify.com/track/6fRJg3R90w0juYoCJXxj2d",
|
||||||
SpotifyURL::new("6fRJg3R90w0juYoCJXxj2d", URIType::Track),
|
SpotifyUrl::new("6fRJg3R90w0juYoCJXxj2d", UriType::Track),
|
||||||
);
|
);
|
||||||
test_cases.insert(
|
test_cases.insert(
|
||||||
"https://open.spotify.com/user/~villainy~/playlist/0OgoSs65CLDPn6AF6tsZVg",
|
"https://open.spotify.com/user/~villainy~/playlist/0OgoSs65CLDPn6AF6tsZVg",
|
||||||
SpotifyURL::new("0OgoSs65CLDPn6AF6tsZVg", URIType::Playlist),
|
SpotifyUrl::new("0OgoSs65CLDPn6AF6tsZVg", UriType::Playlist),
|
||||||
);
|
);
|
||||||
test_cases.insert(
|
test_cases.insert(
|
||||||
"https://open.spotify.com/show/4MZfJbM2MXzZdPbv6gi5lJ",
|
"https://open.spotify.com/show/4MZfJbM2MXzZdPbv6gi5lJ",
|
||||||
SpotifyURL::new("4MZfJbM2MXzZdPbv6gi5lJ", URIType::Show),
|
SpotifyUrl::new("4MZfJbM2MXzZdPbv6gi5lJ", UriType::Show),
|
||||||
);
|
);
|
||||||
test_cases.insert(
|
test_cases.insert(
|
||||||
"https://open.spotify.com/episode/3QE6rfmjRaeqXSqeWcIWF6",
|
"https://open.spotify.com/episode/3QE6rfmjRaeqXSqeWcIWF6",
|
||||||
SpotifyURL::new("3QE6rfmjRaeqXSqeWcIWF6", URIType::Episode),
|
SpotifyUrl::new("3QE6rfmjRaeqXSqeWcIWF6", UriType::Episode),
|
||||||
);
|
);
|
||||||
test_cases.insert(
|
test_cases.insert(
|
||||||
"https://open.spotify.com/artist/6LEeAFiJF8OuPx747e1wxR",
|
"https://open.spotify.com/artist/6LEeAFiJF8OuPx747e1wxR",
|
||||||
SpotifyURL::new("6LEeAFiJF8OuPx747e1wxR", URIType::Artist),
|
SpotifyUrl::new("6LEeAFiJF8OuPx747e1wxR", UriType::Artist),
|
||||||
);
|
);
|
||||||
|
|
||||||
for case in test_cases {
|
for case in test_cases {
|
||||||
let result = SpotifyURL::from_url(case.0).unwrap();
|
let result = SpotifyUrl::from_url(case.0).unwrap();
|
||||||
assert_eq!(result.id, case.1.id);
|
assert_eq!(result.id, case.1.id);
|
||||||
assert_eq!(result.uri_type, case.1.uri_type);
|
assert_eq!(result.uri_type, case.1.uri_type);
|
||||||
}
|
}
|
||||||
|
|||||||
188
src/spotify_worker.rs
Normal file
188
src/spotify_worker.rs
Normal file
@@ -0,0 +1,188 @@
|
|||||||
|
use crate::events::{Event, EventManager};
|
||||||
|
use crate::playable::Playable;
|
||||||
|
use crate::spotify::{PlayerEvent, Spotify};
|
||||||
|
use futures::channel::{mpsc, oneshot};
|
||||||
|
use futures::compat::Stream01CompatExt;
|
||||||
|
use futures::task::{Context, Poll};
|
||||||
|
use futures::{Future, Stream};
|
||||||
|
use futures_01::stream::Stream as v01_Stream;
|
||||||
|
use futures_01::sync::mpsc::UnboundedReceiver;
|
||||||
|
use futures_01::Async as v01_Async;
|
||||||
|
use librespot_core::keymaster::Token;
|
||||||
|
use librespot_core::mercury::MercuryError;
|
||||||
|
use librespot_core::session::Session;
|
||||||
|
use librespot_core::spotify_id::{SpotifyAudioType, SpotifyId};
|
||||||
|
use librespot_playback::mixer::Mixer;
|
||||||
|
use librespot_playback::player::{Player, PlayerEvent as LibrespotPlayerEvent};
|
||||||
|
use std::pin::Pin;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
pub(crate) enum WorkerCommand {
|
||||||
|
Load(Playable),
|
||||||
|
Play,
|
||||||
|
Pause,
|
||||||
|
Stop,
|
||||||
|
Seek(u32),
|
||||||
|
SetVolume(u16),
|
||||||
|
RequestToken(oneshot::Sender<Token>),
|
||||||
|
Shutdown,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Worker {
|
||||||
|
events: EventManager,
|
||||||
|
player_events: UnboundedReceiver<LibrespotPlayerEvent>,
|
||||||
|
commands: Pin<Box<mpsc::UnboundedReceiver<WorkerCommand>>>,
|
||||||
|
session: Session,
|
||||||
|
player: Player,
|
||||||
|
refresh_task: Pin<Box<dyn Stream<Item = Result<(), tokio_timer::Error>>>>,
|
||||||
|
token_task: Pin<Box<dyn Future<Output = Result<(), MercuryError>>>>,
|
||||||
|
active: bool,
|
||||||
|
mixer: Box<dyn Mixer>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Worker {
|
||||||
|
pub(crate) fn new(
|
||||||
|
events: EventManager,
|
||||||
|
player_events: UnboundedReceiver<LibrespotPlayerEvent>,
|
||||||
|
commands: Pin<Box<mpsc::UnboundedReceiver<WorkerCommand>>>,
|
||||||
|
session: Session,
|
||||||
|
player: Player,
|
||||||
|
mixer: Box<dyn Mixer>,
|
||||||
|
) -> Worker {
|
||||||
|
Worker {
|
||||||
|
events,
|
||||||
|
player_events,
|
||||||
|
commands,
|
||||||
|
player,
|
||||||
|
session,
|
||||||
|
refresh_task: Box::pin(futures::stream::empty()),
|
||||||
|
token_task: Box::pin(futures::future::pending()),
|
||||||
|
active: false,
|
||||||
|
mixer,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Worker {
|
||||||
|
fn create_refresh(&self) -> Pin<Box<dyn Stream<Item = Result<(), tokio_timer::Error>>>> {
|
||||||
|
let ev = self.events.clone();
|
||||||
|
let future =
|
||||||
|
tokio_timer::Interval::new_interval(Duration::from_millis(400)).map(move |_| {
|
||||||
|
ev.trigger();
|
||||||
|
});
|
||||||
|
Box::pin(future.compat())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl futures::Future for Worker {
|
||||||
|
type Output = Result<(), ()>;
|
||||||
|
|
||||||
|
fn poll(mut self: Pin<&mut Self>, cx: &mut Context) -> futures::task::Poll<Self::Output> {
|
||||||
|
loop {
|
||||||
|
let mut progress = false;
|
||||||
|
|
||||||
|
if self.session.is_invalid() {
|
||||||
|
self.events.send(Event::Player(PlayerEvent::Stopped));
|
||||||
|
return Poll::Ready(Result::Err(()));
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Poll::Ready(Some(cmd)) = self.commands.as_mut().poll_next(cx) {
|
||||||
|
progress = true;
|
||||||
|
debug!("message received!");
|
||||||
|
match cmd {
|
||||||
|
WorkerCommand::Load(playable) => match SpotifyId::from_uri(&playable.uri()) {
|
||||||
|
Ok(id) => {
|
||||||
|
info!("player loading track: {:?}", id);
|
||||||
|
if id.audio_type == SpotifyAudioType::NonPlayable {
|
||||||
|
warn!("track is not playable");
|
||||||
|
self.events.send(Event::Player(PlayerEvent::FinishedTrack));
|
||||||
|
} else {
|
||||||
|
self.player.load(id, true, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
error!("error parsing uri: {:?}", e);
|
||||||
|
self.events.send(Event::Player(PlayerEvent::FinishedTrack));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
WorkerCommand::Play => {
|
||||||
|
self.player.play();
|
||||||
|
}
|
||||||
|
WorkerCommand::Pause => {
|
||||||
|
self.player.pause();
|
||||||
|
}
|
||||||
|
WorkerCommand::Stop => {
|
||||||
|
self.player.stop();
|
||||||
|
}
|
||||||
|
WorkerCommand::Seek(pos) => {
|
||||||
|
self.player.seek(pos);
|
||||||
|
}
|
||||||
|
WorkerCommand::SetVolume(volume) => {
|
||||||
|
self.mixer.set_volume(volume);
|
||||||
|
}
|
||||||
|
WorkerCommand::RequestToken(sender) => {
|
||||||
|
self.token_task = Spotify::get_token(&self.session, sender);
|
||||||
|
progress = true;
|
||||||
|
}
|
||||||
|
WorkerCommand::Shutdown => {
|
||||||
|
self.player.stop();
|
||||||
|
self.session.shutdown();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Ok(v01_Async::Ready(Some(event))) = self.player_events.poll() {
|
||||||
|
debug!("librespot player event: {:?}", event);
|
||||||
|
match event {
|
||||||
|
LibrespotPlayerEvent::Started { .. }
|
||||||
|
| LibrespotPlayerEvent::Loading { .. }
|
||||||
|
| LibrespotPlayerEvent::Changed { .. } => {
|
||||||
|
progress = true;
|
||||||
|
}
|
||||||
|
LibrespotPlayerEvent::Playing { .. } => {
|
||||||
|
self.events.send(Event::Player(PlayerEvent::Playing));
|
||||||
|
self.refresh_task = self.create_refresh();
|
||||||
|
self.active = true;
|
||||||
|
}
|
||||||
|
LibrespotPlayerEvent::Paused { .. } => {
|
||||||
|
self.events.send(Event::Player(PlayerEvent::Paused));
|
||||||
|
self.active = false;
|
||||||
|
}
|
||||||
|
LibrespotPlayerEvent::Stopped { .. } => {
|
||||||
|
self.events.send(Event::Player(PlayerEvent::Stopped));
|
||||||
|
self.active = false;
|
||||||
|
}
|
||||||
|
LibrespotPlayerEvent::EndOfTrack { .. } => {
|
||||||
|
self.events.send(Event::Player(PlayerEvent::FinishedTrack));
|
||||||
|
progress = true;
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Poll::Ready(Some(Ok(_))) = self.refresh_task.as_mut().poll_next(cx) {
|
||||||
|
self.refresh_task = if self.active {
|
||||||
|
progress = true;
|
||||||
|
self.create_refresh()
|
||||||
|
} else {
|
||||||
|
Box::pin(futures::stream::empty())
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
match self.token_task.as_mut().poll(cx) {
|
||||||
|
Poll::Ready(Ok(_)) => {
|
||||||
|
info!("token updated!");
|
||||||
|
self.token_task = Box::pin(futures::future::pending())
|
||||||
|
}
|
||||||
|
Poll::Ready(Err(e)) => {
|
||||||
|
error!("could not generate token: {:?}", e);
|
||||||
|
}
|
||||||
|
_ => (),
|
||||||
|
}
|
||||||
|
|
||||||
|
if !progress {
|
||||||
|
return Poll::Pending;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -26,7 +26,7 @@ use crate::ui::album::AlbumView;
|
|||||||
use crate::ui::artist::ArtistView;
|
use crate::ui::artist::ArtistView;
|
||||||
use crate::ui::contextmenu::ContextMenu;
|
use crate::ui::contextmenu::ContextMenu;
|
||||||
use crate::ui::pagination::Pagination;
|
use crate::ui::pagination::Pagination;
|
||||||
use crate::{album::Album, spotify::URIType, spotify_url::SpotifyURL};
|
use crate::{album::Album, spotify::UriType, spotify_url::SpotifyUrl};
|
||||||
|
|
||||||
pub struct ListView<I: ListItem> {
|
pub struct ListView<I: ListItem> {
|
||||||
content: Arc<RwLock<Vec<I>>>,
|
content: Arc<RwLock<Vec<I>>>,
|
||||||
@@ -535,26 +535,26 @@ impl<I: ListItem + Clone> ViewExt for ListView<I> {
|
|||||||
|
|
||||||
let spotify = self.queue.get_spotify();
|
let spotify = self.queue.get_spotify();
|
||||||
|
|
||||||
let url = SpotifyURL::from_url(&url);
|
let url = SpotifyUrl::from_url(&url);
|
||||||
|
|
||||||
if let Some(url) = url {
|
if let Some(url) = url {
|
||||||
let target: Option<Box<dyn ListItem>> = match url.uri_type {
|
let target: Option<Box<dyn ListItem>> = match url.uri_type {
|
||||||
URIType::Track => spotify
|
UriType::Track => spotify
|
||||||
.track(&url.id)
|
.track(&url.id)
|
||||||
.map(|track| Track::from(&track).as_listitem()),
|
.map(|track| Track::from(&track).as_listitem()),
|
||||||
URIType::Album => spotify
|
UriType::Album => spotify
|
||||||
.album(&url.id)
|
.album(&url.id)
|
||||||
.map(|album| Album::from(&album).as_listitem()),
|
.map(|album| Album::from(&album).as_listitem()),
|
||||||
URIType::Playlist => spotify
|
UriType::Playlist => spotify
|
||||||
.playlist(&url.id)
|
.playlist(&url.id)
|
||||||
.map(|playlist| Playlist::from(&playlist).as_listitem()),
|
.map(|playlist| Playlist::from(&playlist).as_listitem()),
|
||||||
URIType::Artist => spotify
|
UriType::Artist => spotify
|
||||||
.artist(&url.id)
|
.artist(&url.id)
|
||||||
.map(|artist| Artist::from(&artist).as_listitem()),
|
.map(|artist| Artist::from(&artist).as_listitem()),
|
||||||
URIType::Episode => spotify
|
UriType::Episode => spotify
|
||||||
.episode(&url.id)
|
.episode(&url.id)
|
||||||
.map(|episode| Episode::from(&episode).as_listitem()),
|
.map(|episode| Episode::from(&episode).as_listitem()),
|
||||||
URIType::Show => spotify
|
UriType::Show => spotify
|
||||||
.get_show(&url.id)
|
.get_show(&url.id)
|
||||||
.map(|show| Show::from(&show).as_listitem()),
|
.map(|show| Show::from(&show).as_listitem()),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ 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::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};
|
||||||
use crate::ui::layout::Layout;
|
use crate::ui::layout::Layout;
|
||||||
|
|||||||
@@ -8,8 +8,8 @@ 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::show::Show;
|
||||||
use crate::spotify::{Spotify, URIType};
|
use crate::spotify::{Spotify, UriType};
|
||||||
use crate::spotify_url::SpotifyURL;
|
use crate::spotify_url::SpotifyUrl;
|
||||||
use crate::track::Track;
|
use crate::track::Track;
|
||||||
use crate::traits::{ListItem, ViewExt};
|
use crate::traits::{ListItem, ViewExt};
|
||||||
use crate::ui::listview::ListView;
|
use crate::ui::listview::ListView;
|
||||||
@@ -381,9 +381,9 @@ impl SearchResultsView {
|
|||||||
self.spotify.refresh_token();
|
self.spotify.refresh_token();
|
||||||
|
|
||||||
// is the query a Spotify URI?
|
// is the query a Spotify URI?
|
||||||
if let Some(uritype) = URIType::from_uri(&query) {
|
if let Some(uritype) = UriType::from_uri(&query) {
|
||||||
match uritype {
|
match uritype {
|
||||||
URIType::Track => {
|
UriType::Track => {
|
||||||
self.perform_search(
|
self.perform_search(
|
||||||
Box::new(Self::get_track),
|
Box::new(Self::get_track),
|
||||||
&self.results_tracks,
|
&self.results_tracks,
|
||||||
@@ -392,7 +392,7 @@ impl SearchResultsView {
|
|||||||
);
|
);
|
||||||
self.tabs.move_focus_to(0);
|
self.tabs.move_focus_to(0);
|
||||||
}
|
}
|
||||||
URIType::Album => {
|
UriType::Album => {
|
||||||
self.perform_search(
|
self.perform_search(
|
||||||
Box::new(Self::get_album),
|
Box::new(Self::get_album),
|
||||||
&self.results_albums,
|
&self.results_albums,
|
||||||
@@ -401,7 +401,7 @@ impl SearchResultsView {
|
|||||||
);
|
);
|
||||||
self.tabs.move_focus_to(1);
|
self.tabs.move_focus_to(1);
|
||||||
}
|
}
|
||||||
URIType::Artist => {
|
UriType::Artist => {
|
||||||
self.perform_search(
|
self.perform_search(
|
||||||
Box::new(Self::get_artist),
|
Box::new(Self::get_artist),
|
||||||
&self.results_artists,
|
&self.results_artists,
|
||||||
@@ -410,7 +410,7 @@ impl SearchResultsView {
|
|||||||
);
|
);
|
||||||
self.tabs.move_focus_to(2);
|
self.tabs.move_focus_to(2);
|
||||||
}
|
}
|
||||||
URIType::Playlist => {
|
UriType::Playlist => {
|
||||||
self.perform_search(
|
self.perform_search(
|
||||||
Box::new(Self::get_playlist),
|
Box::new(Self::get_playlist),
|
||||||
&self.results_playlists,
|
&self.results_playlists,
|
||||||
@@ -419,7 +419,7 @@ impl SearchResultsView {
|
|||||||
);
|
);
|
||||||
self.tabs.move_focus_to(3);
|
self.tabs.move_focus_to(3);
|
||||||
}
|
}
|
||||||
URIType::Show => {
|
UriType::Show => {
|
||||||
self.perform_search(
|
self.perform_search(
|
||||||
Box::new(Self::get_show),
|
Box::new(Self::get_show),
|
||||||
&self.results_shows,
|
&self.results_shows,
|
||||||
@@ -428,7 +428,7 @@ impl SearchResultsView {
|
|||||||
);
|
);
|
||||||
self.tabs.move_focus_to(4);
|
self.tabs.move_focus_to(4);
|
||||||
}
|
}
|
||||||
URIType::Episode => {
|
UriType::Episode => {
|
||||||
self.perform_search(
|
self.perform_search(
|
||||||
Box::new(Self::get_episode),
|
Box::new(Self::get_episode),
|
||||||
&self.results_episodes,
|
&self.results_episodes,
|
||||||
@@ -440,9 +440,9 @@ impl SearchResultsView {
|
|||||||
}
|
}
|
||||||
// Is the query a spotify URL?
|
// Is the query a spotify URL?
|
||||||
// https://open.spotify.com/track/4uLU6hMCjMI75M1A2tKUQC
|
// https://open.spotify.com/track/4uLU6hMCjMI75M1A2tKUQC
|
||||||
} else if let Some(url) = SpotifyURL::from_url(&query) {
|
} else if let Some(url) = SpotifyUrl::from_url(&query) {
|
||||||
match url.uri_type {
|
match url.uri_type {
|
||||||
URIType::Track => {
|
UriType::Track => {
|
||||||
self.perform_search(
|
self.perform_search(
|
||||||
Box::new(Self::get_track),
|
Box::new(Self::get_track),
|
||||||
&self.results_tracks,
|
&self.results_tracks,
|
||||||
@@ -451,7 +451,7 @@ impl SearchResultsView {
|
|||||||
);
|
);
|
||||||
self.tabs.move_focus_to(0);
|
self.tabs.move_focus_to(0);
|
||||||
}
|
}
|
||||||
URIType::Album => {
|
UriType::Album => {
|
||||||
self.perform_search(
|
self.perform_search(
|
||||||
Box::new(Self::get_album),
|
Box::new(Self::get_album),
|
||||||
&self.results_albums,
|
&self.results_albums,
|
||||||
@@ -460,7 +460,7 @@ impl SearchResultsView {
|
|||||||
);
|
);
|
||||||
self.tabs.move_focus_to(1);
|
self.tabs.move_focus_to(1);
|
||||||
}
|
}
|
||||||
URIType::Artist => {
|
UriType::Artist => {
|
||||||
self.perform_search(
|
self.perform_search(
|
||||||
Box::new(Self::get_artist),
|
Box::new(Self::get_artist),
|
||||||
&self.results_artists,
|
&self.results_artists,
|
||||||
@@ -469,7 +469,7 @@ impl SearchResultsView {
|
|||||||
);
|
);
|
||||||
self.tabs.move_focus_to(2);
|
self.tabs.move_focus_to(2);
|
||||||
}
|
}
|
||||||
URIType::Playlist => {
|
UriType::Playlist => {
|
||||||
self.perform_search(
|
self.perform_search(
|
||||||
Box::new(Self::get_playlist),
|
Box::new(Self::get_playlist),
|
||||||
&self.results_playlists,
|
&self.results_playlists,
|
||||||
@@ -478,7 +478,7 @@ impl SearchResultsView {
|
|||||||
);
|
);
|
||||||
self.tabs.move_focus_to(3);
|
self.tabs.move_focus_to(3);
|
||||||
}
|
}
|
||||||
URIType::Show => {
|
UriType::Show => {
|
||||||
self.perform_search(
|
self.perform_search(
|
||||||
Box::new(Self::get_show),
|
Box::new(Self::get_show),
|
||||||
&self.results_shows,
|
&self.results_shows,
|
||||||
@@ -487,7 +487,7 @@ impl SearchResultsView {
|
|||||||
);
|
);
|
||||||
self.tabs.move_focus_to(4);
|
self.tabs.move_focus_to(4);
|
||||||
}
|
}
|
||||||
URIType::Episode => {
|
UriType::Episode => {
|
||||||
self.perform_search(
|
self.perform_search(
|
||||||
Box::new(Self::get_episode),
|
Box::new(Self::get_episode),
|
||||||
&self.results_episodes,
|
&self.results_episodes,
|
||||||
|
|||||||
Reference in New Issue
Block a user