feat: add youtube search #8
| @ -1,6 +1,6 @@ | |||||||
| [package] | [package] | ||||||
| name = "songlify" | name = "songlify" | ||||||
| version = "0.3.3-beta" | version = "0.3.4" | ||||||
| edition = "2018" | edition = "2018" | ||||||
| 
 | 
 | ||||||
| # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html | ||||||
| @ -15,6 +15,8 @@ sentry = "0.27.0" | |||||||
| invidious = "0.2.1" | invidious = "0.2.1" | ||||||
| chrono = "0.4.19" | chrono = "0.4.19" | ||||||
| itertools = "0.10.3" | itertools = "0.10.3" | ||||||
|  | async-trait = "0.1.56" | ||||||
| 
 | 
 | ||||||
| [dev-dependencies] | [dev-dependencies] | ||||||
| tokio-test = "0.4.2" | tokio-test = "0.4.2" | ||||||
|  | mockall = "0.11.1" | ||||||
|  | |||||||
| @ -1,4 +1,4 @@ | |||||||
| FROM rust:1.59.0-slim-bullseye as builder | FROM rust:1.62.0-slim-bullseye as builder | ||||||
| 
 | 
 | ||||||
| WORKDIR /build | WORKDIR /build | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -7,6 +7,9 @@ use youtube::Video; | |||||||
| pub mod spotify; | pub mod spotify; | ||||||
| mod youtube; | mod youtube; | ||||||
| 
 | 
 | ||||||
|  | #[cfg(test)] | ||||||
|  | mod tests; | ||||||
|  | 
 | ||||||
| pub(crate) trait ArtistComposed { | pub(crate) trait ArtistComposed { | ||||||
|     fn get_artists_name(&self) -> HashSet<String>; |     fn get_artists_name(&self) -> HashSet<String>; | ||||||
| } | } | ||||||
| @ -48,14 +51,14 @@ impl ArtistComposed for TrackItem { | |||||||
| 
 | 
 | ||||||
| #[derive(Debug, Clone, PartialEq, Eq, Hash)] | #[derive(Debug, Clone, PartialEq, Eq, Hash)] | ||||||
| pub(crate) struct AlbumItem { | pub(crate) struct AlbumItem { | ||||||
|     pub(crate) spotify_track: Option<AlbumInfo>, |     pub(crate) spotify_album: Option<AlbumInfo>, | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| impl ArtistComposed for AlbumItem { | impl ArtistComposed for AlbumItem { | ||||||
|     fn get_artists_name(&self) -> HashSet<String> { |     fn get_artists_name(&self) -> HashSet<String> { | ||||||
|         if self.spotify_track.is_some() { |         if self.spotify_album.is_some() { | ||||||
|             return self |             return self | ||||||
|                 .spotify_track |                 .spotify_album | ||||||
|                 .clone() |                 .clone() | ||||||
|                 .unwrap() |                 .unwrap() | ||||||
|                 .artists |                 .artists | ||||||
| @ -90,23 +93,25 @@ impl ArtistComposed for PlaylistItem { | |||||||
| 
 | 
 | ||||||
| // This struct will allow us in the future to search, cache and store data and metadata regarding
 | // This struct will allow us in the future to search, cache and store data and metadata regarding
 | ||||||
| // tracks, albums and playlists
 | // tracks, albums and playlists
 | ||||||
| #[derive(Debug, Clone)] |  | ||||||
| pub(crate) struct Engine { | pub(crate) struct Engine { | ||||||
|     spotify: spotify::Client, |     spotify: Box<dyn spotify::SearchableClient + Send + Sync>, | ||||||
|     youtube: youtube::Client, |     youtube: Box<dyn youtube::SearchableClient + Send + Sync>, | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| impl Engine { | impl Engine { | ||||||
|     pub(crate) async fn new() -> Self { |     pub(crate) async fn new() -> Self { | ||||||
|         Engine { |         Engine { | ||||||
|             spotify: spotify::Client::new().await, |             spotify: Box::new(spotify::Client::new().await), | ||||||
|             youtube: youtube::Client::new().await, |             youtube: Box::new(youtube::Client::new().await), | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     #[allow(dead_code)] | ||||||
|  |     #[allow(unused_variables)] | ||||||
|  |     #[cfg(test)] | ||||||
|     pub(crate) fn new_with_dependencies( |     pub(crate) fn new_with_dependencies( | ||||||
|         spotify_client: spotify::Client, |         spotify_client: Box<dyn spotify::SearchableClient + Send + Sync>, | ||||||
|         youtube_client: youtube::Client, |         youtube_client: Box<dyn youtube::SearchableClient + Send + Sync>, | ||||||
|     ) -> Self { |     ) -> Self { | ||||||
|         Engine { |         Engine { | ||||||
|             spotify: spotify_client, |             spotify: spotify_client, | ||||||
| @ -173,7 +178,7 @@ impl Engine { | |||||||
|         }; |         }; | ||||||
| 
 | 
 | ||||||
|         AlbumItem { |         AlbumItem { | ||||||
|             spotify_track: album_info, |             spotify_album: album_info, | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
| @ -197,23 +202,3 @@ impl Engine { | |||||||
| pub(crate) fn get_spotify_kind(spotify_id: &str) -> Option<ContentKind> { | pub(crate) fn get_spotify_kind(spotify_id: &str) -> Option<ContentKind> { | ||||||
|     get_entry_kind(spotify_id) |     get_entry_kind(spotify_id) | ||||||
| } | } | ||||||
| 
 |  | ||||||
| #[cfg(test)] |  | ||||||
| mod tests { |  | ||||||
|     use super::*; |  | ||||||
| 
 |  | ||||||
|     #[test] |  | ||||||
|     fn should_search_track_by_spotify_id() { |  | ||||||
|         todo!("Implement me!") |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     #[test] |  | ||||||
|     fn should_search_album_by_spotify_id() { |  | ||||||
|         todo!("Implement me!") |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     #[test] |  | ||||||
|     fn should_search_playlist_by_spotify_id() { |  | ||||||
|         todo!("Implement me!") |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  | |||||||
| @ -1,3 +1,6 @@ | |||||||
|  | use async_trait::async_trait; | ||||||
|  | #[cfg(test)] | ||||||
|  | use mockall::{automock, mock, predicate::*}; | ||||||
| use rspotify::model::PlayableItem::{Episode, Track}; | use rspotify::model::PlayableItem::{Episode, Track}; | ||||||
| use rspotify::model::{AlbumId, PlaylistId, TrackId}; | use rspotify::model::{AlbumId, PlaylistId, TrackId}; | ||||||
| use rspotify::prelude::*; | use rspotify::prelude::*; | ||||||
| @ -5,6 +8,14 @@ use rspotify::{ClientCredsSpotify, Credentials}; | |||||||
| use std::sync::Arc; | use std::sync::Arc; | ||||||
| use std::time::Duration; | use std::time::Duration; | ||||||
| 
 | 
 | ||||||
|  | #[cfg_attr(test, automock)] | ||||||
|  | #[async_trait] | ||||||
|  | pub(crate) trait SearchableClient { | ||||||
|  |     async fn get_track(&self, id: &str) -> Option<TrackInfo>; | ||||||
|  |     async fn get_album(&self, id: &str) -> Option<AlbumInfo>; | ||||||
|  |     async fn get_playlist(&self, id: &str) -> Option<PlaylistInfo>; | ||||||
|  | } | ||||||
|  | 
 | ||||||
| #[derive(Debug, Clone, PartialEq, Eq, Hash)] | #[derive(Debug, Clone, PartialEq, Eq, Hash)] | ||||||
| pub enum ContentKind { | pub enum ContentKind { | ||||||
|     Track(String), |     Track(String), | ||||||
| @ -69,13 +80,19 @@ impl Client { | |||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     #[allow(dead_code)] | ||||||
|  |     #[allow(unused_variables)] | ||||||
|  |     #[cfg(test)] | ||||||
|     pub(crate) fn new_with_dependencies(client: ClientCredsSpotify) -> Self { |     pub(crate) fn new_with_dependencies(client: ClientCredsSpotify) -> Self { | ||||||
|         Client { |         Client { | ||||||
|             client: Arc::new(client), |             client: Arc::new(client), | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  | } | ||||||
| 
 | 
 | ||||||
|     pub async fn get_track(&self, id: &str) -> Option<TrackInfo> { | #[async_trait] | ||||||
|  | impl SearchableClient for Client { | ||||||
|  |     async fn get_track(&self, id: &str) -> Option<TrackInfo> { | ||||||
|         // FIXME should we really return Option here? We're hiding a possible error or a entry not found
 |         // FIXME should we really return Option here? We're hiding a possible error or a entry not found
 | ||||||
|         let track_id = match TrackId::from_id(id) { |         let track_id = match TrackId::from_id(id) { | ||||||
|             Ok(track) => track, |             Ok(track) => track, | ||||||
| @ -92,7 +109,7 @@ impl Client { | |||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     pub async fn get_album(&self, id: &str) -> Option<AlbumInfo> { |     async fn get_album(&self, id: &str) -> Option<AlbumInfo> { | ||||||
|         let album_id = match AlbumId::from_id(id) { |         let album_id = match AlbumId::from_id(id) { | ||||||
|             Ok(album) => album, |             Ok(album) => album, | ||||||
|             Err(_e) => return None, |             Err(_e) => return None, | ||||||
| @ -118,7 +135,7 @@ impl Client { | |||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     pub async fn get_playlist(&self, id: &str) -> Option<PlaylistInfo> { |     async fn get_playlist(&self, id: &str) -> Option<PlaylistInfo> { | ||||||
|         let playlist_id = match PlaylistId::from_id(id) { |         let playlist_id = match PlaylistId::from_id(id) { | ||||||
|             Ok(playlist) => playlist, |             Ok(playlist) => playlist, | ||||||
|             Err(_e) => return None, |             Err(_e) => return None, | ||||||
| @ -186,6 +203,7 @@ fn get_id_in_uri(uri: &str) -> Option<&str> { | |||||||
| } | } | ||||||
| 
 | 
 | ||||||
| pub fn get_entry_kind(uri: &str) -> Option<ContentKind> { | pub fn get_entry_kind(uri: &str) -> Option<ContentKind> { | ||||||
|  |     // TODO WE SHOULD PROPERLY TEST THIS FUNCTION
 | ||||||
|     if uri.contains("spotify:track:") { |     if uri.contains("spotify:track:") { | ||||||
|         let track_id = get_id_in_uri(uri); |         let track_id = get_id_in_uri(uri); | ||||||
|         return match track_id { |         return match track_id { | ||||||
| @ -217,7 +235,7 @@ pub fn get_entry_kind(uri: &str) -> Option<ContentKind> { | |||||||
|     if uri.contains("spotify:playlist:") { |     if uri.contains("spotify:playlist:") { | ||||||
|         let track_id = get_id_in_uri(uri); |         let track_id = get_id_in_uri(uri); | ||||||
|         return match track_id { |         return match track_id { | ||||||
|             Some(id) => Some(ContentKind::Album(id.to_string())), |             Some(id) => Some(ContentKind::Playlist(id.to_string())), | ||||||
|             None => None, |             None => None, | ||||||
|         }; |         }; | ||||||
|     } |     } | ||||||
|  | |||||||
							
								
								
									
										147
									
								
								src/search/tests.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										147
									
								
								src/search/tests.rs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,147 @@ | |||||||
|  | use super::*; | ||||||
|  | use crate::search::youtube::VideoSearch; | ||||||
|  | use crate::spotify::PlayableKind; | ||||||
|  | use mockall::predicate; | ||||||
|  | 
 | ||||||
|  | #[tokio::test] | ||||||
|  | async fn should_search_track_by_spotify_id() { | ||||||
|  |     let spotify_id = "spotify:track:no-value-kek"; | ||||||
|  |     let mut spotify_mock = spotify::MockSearchableClient::new(); | ||||||
|  |     spotify_mock | ||||||
|  |         .expect_get_track() | ||||||
|  |         .with(predicate::eq("no-value-kek")) | ||||||
|  |         .returning(|_id| { | ||||||
|  |             Some(TrackInfo { | ||||||
|  |                 name: "A name".to_string(), | ||||||
|  |                 artists: vec!["Art1".to_string()], | ||||||
|  |                 duration: Default::default(), | ||||||
|  |             }) | ||||||
|  |         }); | ||||||
|  |     let mut youtube_mock = youtube::MockSearchableClient::new(); | ||||||
|  |     youtube_mock | ||||||
|  |         .expect_search_video() | ||||||
|  |         .returning(|_id, _sort_by| { | ||||||
|  |             Ok(VideoSearch { | ||||||
|  |                 items: vec![Video { | ||||||
|  |                     title: "An example".to_string(), | ||||||
|  |                     video_id: "id123".to_string(), | ||||||
|  |                     author: "An Art".to_string(), | ||||||
|  |                     author_id: "artId123".to_string(), | ||||||
|  |                     author_url: "https://example.com".to_string(), | ||||||
|  |                     length_seconds: 42, | ||||||
|  |                     description: "A song".to_string(), | ||||||
|  |                     description_html: "A song 2".to_string(), | ||||||
|  |                     view_count: 0, | ||||||
|  |                     published: 0, | ||||||
|  |                     published_text: "".to_string(), | ||||||
|  |                     live_now: false, | ||||||
|  |                     paid: false, | ||||||
|  |                     premium: false, | ||||||
|  |                 }], | ||||||
|  |             }) | ||||||
|  |         }); | ||||||
|  | 
 | ||||||
|  |     let engine = Engine::new_with_dependencies(Box::new(spotify_mock), Box::new(youtube_mock)); | ||||||
|  |     let got = engine.get_song_from_spotify_id(spotify_id).await; | ||||||
|  | 
 | ||||||
|  |     assert_eq!(true, got.spotify_track.is_some()); | ||||||
|  |     let boxed_st = Box::new(got.spotify_track.unwrap()); | ||||||
|  |     assert_eq!(1, boxed_st.artists.len()); | ||||||
|  |     assert_eq!("Art1".to_string(), boxed_st.artists.get(0).unwrap().clone()); | ||||||
|  |     assert_eq!("A name".to_string(), boxed_st.name); | ||||||
|  | 
 | ||||||
|  |     assert_eq!(true, got.youtube_track.is_some()); | ||||||
|  |     let boxed_yt = Box::new(got.youtube_track.unwrap()); | ||||||
|  |     assert_eq!(1, boxed_yt.len()); | ||||||
|  |     let got_video = boxed_yt.get(0).unwrap(); | ||||||
|  |     assert_eq!("An example".to_string(), got_video.title); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | #[tokio::test] | ||||||
|  | async fn should_search_album_by_spotify_id() { | ||||||
|  |     let spotify_id = "spotify:album:no-value-kek"; | ||||||
|  |     let mut spotify_mock = spotify::MockSearchableClient::new(); | ||||||
|  |     spotify_mock | ||||||
|  |         .expect_get_album() | ||||||
|  |         .with(predicate::eq("no-value-kek")) | ||||||
|  |         .returning(|_id| { | ||||||
|  |             Some(AlbumInfo { | ||||||
|  |                 name: "An album".to_string(), | ||||||
|  |                 artists: vec!["Art1".to_string(), "Art2".to_string()], | ||||||
|  |                 genres: vec!["Rock".to_string(), "Hip-hop".to_string()], | ||||||
|  |                 tracks: vec![TrackInfo { | ||||||
|  |                     name: "Track info 1".to_string(), | ||||||
|  |                     artists: vec!["Art1".to_string()], | ||||||
|  |                     duration: Default::default(), | ||||||
|  |                 }], | ||||||
|  |             }) | ||||||
|  |         }); | ||||||
|  |     let youtube_mock = youtube::MockSearchableClient::new(); | ||||||
|  | 
 | ||||||
|  |     let engine = Engine::new_with_dependencies(Box::new(spotify_mock), Box::new(youtube_mock)); | ||||||
|  |     let got = engine.get_album_from_spotify_id(spotify_id).await; | ||||||
|  | 
 | ||||||
|  |     assert_eq!(true, got.spotify_album.is_some()); | ||||||
|  |     let boxed_st = Box::new(got.spotify_album.unwrap()); | ||||||
|  |     assert_eq!(2, boxed_st.artists.len()); | ||||||
|  |     assert_eq!("Art1".to_string(), boxed_st.artists.get(0).unwrap().clone()); | ||||||
|  |     assert_eq!("Art2".to_string(), boxed_st.artists.get(1).unwrap().clone()); | ||||||
|  |     assert_eq!("An album".to_string(), boxed_st.name); | ||||||
|  |     assert_eq!(2, boxed_st.genres.len()); | ||||||
|  |     assert_eq!("Rock".to_string(), boxed_st.genres.get(0).unwrap().clone()); | ||||||
|  |     assert_eq!( | ||||||
|  |         "Hip-hop".to_string(), | ||||||
|  |         boxed_st.genres.get(1).unwrap().clone() | ||||||
|  |     ); | ||||||
|  |     assert_eq!(1, boxed_st.tracks.len()); | ||||||
|  |     assert_eq!( | ||||||
|  |         TrackInfo { | ||||||
|  |             name: "Track info 1".to_string(), | ||||||
|  |             artists: vec!["Art1".to_string()], | ||||||
|  |             duration: Default::default(), | ||||||
|  |         }, | ||||||
|  |         boxed_st.tracks.get(0).unwrap().clone() | ||||||
|  |     ); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | #[tokio::test] | ||||||
|  | async fn should_search_playlist_by_spotify_id() { | ||||||
|  |     let spotify_id = "spotify:playlist:no-value-kek"; | ||||||
|  |     let mut spotify_mock = spotify::MockSearchableClient::new(); | ||||||
|  |     spotify_mock | ||||||
|  |         .expect_get_playlist() | ||||||
|  |         .with(predicate::eq("no-value-kek")) | ||||||
|  |         .returning(|_id| { | ||||||
|  |             Some(PlaylistInfo { | ||||||
|  |                 name: "A playlist".to_string(), | ||||||
|  |                 artists: vec!["Art1".to_string(), "Art2".to_string()], | ||||||
|  |                 tracks: vec![PlayableKind::Track(TrackInfo { | ||||||
|  |                     name: "A track".to_string(), | ||||||
|  |                     artists: vec!["Art1".to_string()], | ||||||
|  |                     duration: Default::default(), | ||||||
|  |                 })], | ||||||
|  |                 owner: Some("Frodo".to_string()), | ||||||
|  |             }) | ||||||
|  |         }); | ||||||
|  |     let youtube_mock = youtube::MockSearchableClient::new(); | ||||||
|  | 
 | ||||||
|  |     let engine = Engine::new_with_dependencies(Box::new(spotify_mock), Box::new(youtube_mock)); | ||||||
|  |     let got = engine.get_playlist_from_spotify_id(spotify_id).await; | ||||||
|  | 
 | ||||||
|  |     assert_eq!(true, got.spotify_playlist.is_some()); | ||||||
|  |     let boxed_st = Box::new(got.spotify_playlist.unwrap()); | ||||||
|  |     assert_eq!(2, boxed_st.artists.len()); | ||||||
|  |     assert_eq!("Art1".to_string(), boxed_st.artists.get(0).unwrap().clone()); | ||||||
|  |     assert_eq!("Art2".to_string(), boxed_st.artists.get(1).unwrap().clone()); | ||||||
|  |     assert_eq!("A playlist".to_string(), boxed_st.name); | ||||||
|  |     assert_eq!(1, boxed_st.tracks.len()); | ||||||
|  |     assert_eq!( | ||||||
|  |         PlayableKind::Track(TrackInfo { | ||||||
|  |             name: "A track".to_string(), | ||||||
|  |             artists: vec!["Art1".to_string()], | ||||||
|  |             duration: Default::default(), | ||||||
|  |         }), | ||||||
|  |         boxed_st.tracks.get(0).unwrap().clone() | ||||||
|  |     ); | ||||||
|  |     assert_eq!(Some("Frodo".to_string()), boxed_st.owner); | ||||||
|  | } | ||||||
| @ -1,6 +1,19 @@ | |||||||
|  | use async_trait::async_trait; | ||||||
|  | #[cfg(test)] | ||||||
|  | use mockall::{automock, mock, predicate::*}; | ||||||
| use std::error::Error; | use std::error::Error; | ||||||
| use std::sync::Arc; | use std::sync::Arc; | ||||||
| 
 | 
 | ||||||
|  | #[cfg_attr(test, automock)] | ||||||
|  | #[async_trait] | ||||||
|  | pub(crate) trait SearchableClient { | ||||||
|  |     async fn search_video<'a>( | ||||||
|  |         &self, | ||||||
|  |         id: &str, | ||||||
|  |         sort_by: Option<&'a SearchSortBy>, | ||||||
|  |     ) -> Result<VideoSearch, Box<dyn Error>>; | ||||||
|  | } | ||||||
|  | 
 | ||||||
| #[derive(Debug, Clone, PartialEq, Eq, Hash)] | #[derive(Debug, Clone, PartialEq, Eq, Hash)] | ||||||
| pub(crate) struct VideoSearch { | pub(crate) struct VideoSearch { | ||||||
|     pub(crate) items: Vec<Video>, |     pub(crate) items: Vec<Video>, | ||||||
| @ -26,9 +39,17 @@ pub(crate) struct Video { | |||||||
| 
 | 
 | ||||||
| type SearchSortBy = str; | type SearchSortBy = str; | ||||||
| 
 | 
 | ||||||
|  | #[allow(dead_code)] | ||||||
|  | #[allow(unused_variables)] | ||||||
| const BY_RELEVANCE: &SearchSortBy = "relevance"; | const BY_RELEVANCE: &SearchSortBy = "relevance"; | ||||||
|  | #[allow(dead_code)] | ||||||
|  | #[allow(unused_variables)] | ||||||
| const BY_RATING: &SearchSortBy = "rating"; | const BY_RATING: &SearchSortBy = "rating"; | ||||||
|  | #[allow(dead_code)] | ||||||
|  | #[allow(unused_variables)] | ||||||
| const BY_UPLOAD_DATE: &SearchSortBy = "upload_date"; | const BY_UPLOAD_DATE: &SearchSortBy = "upload_date"; | ||||||
|  | #[allow(dead_code)] | ||||||
|  | #[allow(unused_variables)] | ||||||
| const BY_VIEW_COUNT: &SearchSortBy = "view_count"; | const BY_VIEW_COUNT: &SearchSortBy = "view_count"; | ||||||
| 
 | 
 | ||||||
| #[derive(Debug, Clone)] | #[derive(Debug, Clone)] | ||||||
| @ -47,16 +68,22 @@ impl Client { | |||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     #[allow(dead_code)] | ||||||
|  |     #[allow(unused_variables)] | ||||||
|  |     #[cfg(test)] | ||||||
|     pub(crate) fn new_with_dependencies(client: invidious::asynchronous::Client) -> Self { |     pub(crate) fn new_with_dependencies(client: invidious::asynchronous::Client) -> Self { | ||||||
|         Client { |         Client { | ||||||
|             client: Arc::new(client), |             client: Arc::new(client), | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  | } | ||||||
| 
 | 
 | ||||||
|     pub async fn search_video( | #[async_trait] | ||||||
|  | impl SearchableClient for Client { | ||||||
|  |     async fn search_video<'a>( | ||||||
|         &self, |         &self, | ||||||
|         keyword: &str, |         keyword: &str, | ||||||
|         sort_by: Option<&SearchSortBy>, |         sort_by: Option<&'a SearchSortBy>, | ||||||
|     ) -> Result<VideoSearch, Box<dyn Error>> { |     ) -> Result<VideoSearch, Box<dyn Error>> { | ||||||
|         let mut query = Vec::<String>::new(); |         let mut query = Vec::<String>::new(); | ||||||
|         query.push(format!("{}={}", "q", keyword)); |         query.push(format!("{}={}", "q", keyword)); | ||||||
|  | |||||||
| @ -38,7 +38,7 @@ pub(crate) fn format_track_message(track_info: TrackItem) -> Option<String> { | |||||||
| 
 | 
 | ||||||
| pub(crate) fn format_album_message(album_info: AlbumItem) -> Option<String> { | pub(crate) fn format_album_message(album_info: AlbumItem) -> Option<String> { | ||||||
|     let boxed_info = Box::new(album_info); |     let boxed_info = Box::new(album_info); | ||||||
|     if boxed_info.spotify_track.is_none() { |     if boxed_info.spotify_album.is_none() { | ||||||
|         return None; |         return None; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
| @ -89,14 +89,14 @@ pub(crate) fn format_playlist_message(playlist_info: PlaylistItem) -> Option<Str | |||||||
| } | } | ||||||
| 
 | 
 | ||||||
| fn get_album_name(ai: Box<AlbumItem>) -> String { | fn get_album_name(ai: Box<AlbumItem>) -> String { | ||||||
|     ai.spotify_track |     ai.spotify_album | ||||||
|         .map(|s| s.name) |         .map(|s| s.name) | ||||||
|         .unwrap_or("Unknown album name".to_string()) |         .unwrap_or("Unknown album name".to_string()) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| fn get_album_genres(ai: Box<AlbumItem>) -> HashSet<String> { | fn get_album_genres(ai: Box<AlbumItem>) -> HashSet<String> { | ||||||
|     if ai.spotify_track.is_some() { |     if ai.spotify_album.is_some() { | ||||||
|         return ai.spotify_track.unwrap().genres.into_iter().collect(); |         return ai.spotify_album.unwrap().genres.into_iter().collect(); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     return HashSet::new(); |     return HashSet::new(); | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user