diff --git a/src/main.rs b/src/main.rs index 0c6d80c..5c64e25 100644 --- a/src/main.rs +++ b/src/main.rs @@ -65,6 +65,7 @@ mod queue; mod sharing; mod show; mod spotify; +mod spotify_url; mod theme; mod track; mod traits; diff --git a/src/spotify.rs b/src/spotify.rs index 4c3d22c..eaf9a85 100644 --- a/src/spotify.rs +++ b/src/spotify.rs @@ -59,6 +59,7 @@ use crate::events::{Event, EventManager}; use crate::playable::Playable; use crate::queue; use crate::track::Track; + use rspotify::model::recommend::Recommendations; use rspotify::model::show::{FullEpisode, FullShow, Show, SimplifiedEpisode}; @@ -965,6 +966,7 @@ impl Spotify { } } +#[derive(Debug, PartialEq)] pub enum URIType { Album, Artist, diff --git a/src/spotify_url.rs b/src/spotify_url.rs new file mode 100644 index 0000000..6f25f23 --- /dev/null +++ b/src/spotify_url.rs @@ -0,0 +1,102 @@ +use crate::spotify::URIType; + +use url::{Host, Url}; + +pub struct SpotifyURL { + pub id: String, + pub uri_type: URIType, +} + +impl SpotifyURL { + fn new(id: &str, uri_type: URIType) -> SpotifyURL { + SpotifyURL { + id: id.to_string(), + uri_type, + } + } + + /// Get media id and type from open.spotify.com url + /// + /// ``` + /// let result = spotify_url::SpotifyURL::from_url("https://open.spotify.com/track/4uLU6hMCjMI75M1A2tKUQC").unwrap(); + /// assert_eq!(result.id, "4uLU6hMCjMI75M1A2tKUQC"); + /// assert_eq!(result.uri_type, URIType::Track); + /// ``` + pub fn from_url(s: &str) -> Option { + let url = Url::parse(s).ok()?; + if url.host() != Some(Host::Domain("open.spotify.com")) { + return None; + } + + let mut path_segments = url.path_segments()?; + + let entity = path_segments.next()?; + + let uri_type = match entity.to_lowercase().as_str() { + "album" => Some(URIType::Album), + "artist" => Some(URIType::Artist), + "episode" => Some(URIType::Episode), + "playlist" => Some(URIType::Playlist), + "show" => Some(URIType::Show), + "track" => Some(URIType::Track), + "user" => { + let _user_id = path_segments.next()?; + let entity = path_segments.next()?; + + if entity != "playlist" { + return None; + } + + Some(URIType::Playlist) + } + _ => None, + }?; + + let id = path_segments.next()?; + + Some(SpotifyURL::new(id, uri_type)) + } +} + +#[cfg(test)] +mod tests { + use std::collections::HashMap; + + use super::SpotifyURL; + use crate::spotify::URIType; + + #[test] + fn test_urls() { + let mut test_cases = HashMap::new(); + test_cases.insert( + "https://open.spotify.com/playlist/1XFxe8bkTryTODn0lk4CNa?si=FfSpZ6KPQdieClZbwHakOQ", + SpotifyURL::new("1XFxe8bkTryTODn0lk4CNa", URIType::Playlist), + ); + test_cases.insert( + "https://open.spotify.com/track/6fRJg3R90w0juYoCJXxj2d", + SpotifyURL::new("6fRJg3R90w0juYoCJXxj2d", URIType::Track), + ); + test_cases.insert( + "https://open.spotify.com/user/~villainy~/playlist/0OgoSs65CLDPn6AF6tsZVg", + SpotifyURL::new("0OgoSs65CLDPn6AF6tsZVg", URIType::Playlist), + ); + test_cases.insert( + "https://open.spotify.com/show/4MZfJbM2MXzZdPbv6gi5lJ", + SpotifyURL::new("4MZfJbM2MXzZdPbv6gi5lJ", URIType::Show), + ); + test_cases.insert( + "https://open.spotify.com/episode/3QE6rfmjRaeqXSqeWcIWF6", + SpotifyURL::new("3QE6rfmjRaeqXSqeWcIWF6", URIType::Episode), + ); + test_cases.insert( + "https://open.spotify.com/artist/6LEeAFiJF8OuPx747e1wxR", + SpotifyURL::new("6LEeAFiJF8OuPx747e1wxR", URIType::Artist), + ); + + for case in test_cases { + let result = SpotifyURL::from_url(case.0).unwrap(); + assert_eq!(result.id, case.1.id); + assert_eq!(result.uri_type, case.1.uri_type); + } + } +} diff --git a/src/ui/listview.rs b/src/ui/listview.rs index 87260d8..e6f5d27 100644 --- a/src/ui/listview.rs +++ b/src/ui/listview.rs @@ -9,7 +9,6 @@ use cursive::view::ScrollBase; use cursive::{Cursive, Printer, Rect, Vec2}; use unicode_width::UnicodeWidthStr; -use crate::album::Album; use crate::artist::Artist; use crate::command::{ Command, GotoMode, JumpMode, MoveAmount, MoveMode, SortDirection, SortKey, TargetMode, @@ -28,7 +27,7 @@ use crate::traits::{IntoBoxedViewExt, ListItem, ViewExt}; use crate::ui::album::AlbumView; use crate::ui::artist::ArtistView; use crate::ui::contextmenu::ContextMenu; -use regex::Regex; +use crate::{album::Album, spotify::URIType, spotify_url::SpotifyURL}; pub type Paginator = Box>>) + Send + Sync>; @@ -653,32 +652,28 @@ impl ViewExt for ListView { let spotify = self.queue.get_spotify(); - let re = - Regex::new(r"https?://open\.spotify\.com/(user/[^/]+/)?(\S+)/(\S+)(\?si=\S+)?") - .unwrap(); - let captures = re.captures(&url); + let url = SpotifyURL::from_url(&url); - if let Some(captures) = captures { - let target: Option> = match &captures[2] { - "track" => spotify - .track(&captures[3]) + if let Some(url) = url { + let target: Option> = match url.uri_type { + URIType::Track => spotify + .track(&url.id) .map(|track| Track::from(&track).as_listitem()), - "album" => spotify - .album(&captures[3]) + URIType::Album => spotify + .album(&url.id) .map(|album| Album::from(&album).as_listitem()), - "playlist" => spotify - .playlist(&captures[3]) + URIType::Playlist => spotify + .playlist(&url.id) .map(|playlist| Playlist::from(&playlist).as_listitem()), - "artist" => spotify - .artist(&captures[3]) + URIType::Artist => spotify + .artist(&url.id) .map(|artist| Artist::from(&artist).as_listitem()), - "episode" => spotify - .episode(&captures[3]) + URIType::Episode => spotify + .episode(&url.id) .map(|episode| Episode::from(&episode).as_listitem()), - "show" => spotify - .get_show(&captures[3]) + URIType::Show => spotify + .get_show(&url.id) .map(|show| Show::from(&show).as_listitem()), - _ => None, }; let queue = self.queue.clone(); diff --git a/src/ui/search_results.rs b/src/ui/search_results.rs index a2373da..3c99cb4 100644 --- a/src/ui/search_results.rs +++ b/src/ui/search_results.rs @@ -9,6 +9,7 @@ use crate::playlist::Playlist; use crate::queue::Queue; use crate::show::Show; use crate::spotify::{Spotify, URIType}; +use crate::spotify_url::SpotifyURL; use crate::track::Track; use crate::traits::{ListItem, ViewExt}; use crate::ui::listview::{ListView, Pagination}; @@ -380,22 +381,6 @@ impl SearchResultsView { // is the query a Spotify URI? if let Some(uritype) = URIType::from_uri(&query) { - // Clear the results if we are going to process a Spotify URI. We need - // to do this since we are only calling the search function for the - // given URI type which leaves the previous search results intact. - let results_tracks = self.results_tracks.clone(); - *results_tracks.write().unwrap() = Vec::new(); - let results_albums = self.results_albums.clone(); - *results_albums.write().unwrap() = Vec::new(); - let results_artists = self.results_artists.clone(); - *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 results_episodes = self.results_episodes.clone(); - *results_episodes.write().unwrap() = Vec::new(); - match uritype { URIType::Track => { self.perform_search( @@ -452,6 +437,65 @@ impl SearchResultsView { self.tabs.move_focus_to(5); } } + // Is the query a spotify URL? + // https://open.spotify.com/track/4uLU6hMCjMI75M1A2tKUQC + } else if let Some(url) = SpotifyURL::from_url(&query) { + match url.uri_type { + URIType::Track => { + self.perform_search( + Box::new(Self::get_track), + &self.results_tracks, + &url.id, + None, + ); + self.tabs.move_focus_to(0); + } + URIType::Album => { + self.perform_search( + Box::new(Self::get_album), + &self.results_albums, + &url.id, + None, + ); + self.tabs.move_focus_to(1); + } + URIType::Artist => { + self.perform_search( + Box::new(Self::get_artist), + &self.results_artists, + &url.id, + None, + ); + self.tabs.move_focus_to(2); + } + URIType::Playlist => { + self.perform_search( + Box::new(Self::get_playlist), + &self.results_playlists, + &url.id, + None, + ); + self.tabs.move_focus_to(3); + } + URIType::Show => { + self.perform_search( + Box::new(Self::get_show), + &self.results_shows, + &url.id, + None, + ); + self.tabs.move_focus_to(4); + } + URIType::Episode => { + self.perform_search( + Box::new(Self::get_episode), + &self.results_episodes, + &url.id, + None, + ); + self.tabs.move_focus_to(5); + } + } } else { self.perform_search( Box::new(Self::search_track),