From 469991d20b8b4a30efaf6e07216b015ee7ae6768 Mon Sep 17 00:00:00 2001 From: Davide Polonio Date: Tue, 31 May 2022 22:07:53 +0200 Subject: [PATCH] feat: add first engine draft for music search --- .idea/runConfigurations/Run_all_tests.xml | 19 ++ Cargo.toml | 4 + src/engine.rs | 76 ++++++ src/main.rs | 38 +-- src/spotify/mod.rs | 289 +++++++++++++--------- src/youtube/mod.rs | 57 +++-- 6 files changed, 314 insertions(+), 169 deletions(-) create mode 100644 .idea/runConfigurations/Run_all_tests.xml create mode 100644 src/engine.rs diff --git a/.idea/runConfigurations/Run_all_tests.xml b/.idea/runConfigurations/Run_all_tests.xml new file mode 100644 index 0000000..abc81eb --- /dev/null +++ b/.idea/runConfigurations/Run_all_tests.xml @@ -0,0 +1,19 @@ + + + + diff --git a/Cargo.toml b/Cargo.toml index c46a54d..f7e7809 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,3 +13,7 @@ tokio = { version = "1.18.2", features = ["rt-multi-thread", "macros"] } rspotify = { version = "0.11.5", features = ["default"]} sentry = "0.26.0" invidious = "0.2.1" +chrono = "0.4.19" + +[dev-dependencies] +tokio-test = "0.4.2" diff --git a/src/engine.rs b/src/engine.rs new file mode 100644 index 0000000..83f0cfa --- /dev/null +++ b/src/engine.rs @@ -0,0 +1,76 @@ +use crate::{spotify, youtube}; +use chrono::{Date, Utc}; +use std::time::Duration; + +pub(crate) enum MusicData { + Track(Track), + Album(Album), +} + +pub(crate) struct Track { + name: String, + authors: Vec, + duration: Duration, + album: Vec, + description: String, + lyrics: String, +} + +pub(crate) struct Album { + name: String, + authors: Vec, + description: String, + year: Date, +} + +pub(crate) struct Author { + name: String, + surname: String, + date_of_birth: Date, +} + +// The enum holds all the currently supported type of Id which the engine can search for +pub(crate) enum ServiceIdKind { + Spotify(String), + Youtube(String), + Automatic(String), +} + +// This struct will allow us in the future to search, cache and store data and metadata regarding +// tracks, albums and playlists +pub(crate) struct MusicEngine { + spotify: spotify::Client, + youtube: youtube::Client, +} + +impl MusicEngine { + pub(crate) async fn new() -> Self { + MusicEngine { + spotify: spotify::Client::new().await, + youtube: youtube::Client::new().await, + } + } + + pub(crate) fn new_with_dependencies( + spotify_client: spotify::Client, + youtube_client: youtube::Client, + ) -> Self { + MusicEngine { + spotify: spotify_client, + youtube: youtube_client, + } + } + + pub(crate) async fn search_by_name(&self, name: &str) { + todo!("In the future it would be possible to search for all metadata on a record from this call") + } + pub(crate) async fn search_by_id(&self, id: ServiceIdKind) { + match id { + ServiceIdKind::Spotify(id) => { + let entry_kind = spotify::get_entry_kind(id.as_str()); + } + ServiceIdKind::Youtube(id) => {} + ServiceIdKind::Automatic(id) => {} + } + } +} diff --git a/src/main.rs b/src/main.rs index 27b24f2..01fa125 100644 --- a/src/main.rs +++ b/src/main.rs @@ -9,6 +9,7 @@ use spotify::SpotifyKind::Track; use crate::spotify::SpotifyKind::{Album, Episode, Playlist, Podcast}; use crate::utils::{human_readable_duration, truncate_with_dots}; +mod engine; mod spotify; mod utils; mod youtube; @@ -50,11 +51,11 @@ async fn main() { let text = message.text().and_then(spotify::get_entry_kind); match text { Some(spotify) => { - let spotify_client = spotify::get_client().await; + let spotify_client = spotify::Client::new().await; match spotify { Track(id) => { log::debug!("Parsing spotify song: {}", id); - let track_info = spotify::get_track(spotify_client, &id).await; + let track_info = spotify_client.get_track(&id).await; match track_info { Some(info) => { let reply = format!( @@ -76,7 +77,7 @@ async fn main() { } Album(id) => { log::debug!("Parsing spotify album: {}", id); - let album_info = spotify::get_album(spotify_client, &id).await; + let album_info = spotify_client.get_album(&id).await; match album_info { Some(info) => { let mut reply = format!( @@ -103,7 +104,7 @@ async fn main() { } Playlist(id) => { log::debug!("Parsing spotify playlist: {}", id); - let playlist_info = spotify::get_playlist(spotify_client, &id).await; + let playlist_info = spotify_client.get_playlist(&id).await; match playlist_info { Some(info) => { let reply = format!( @@ -140,32 +141,3 @@ async fn main() { log::info!("Exiting..."); } - -fn add_track_section_for_playlist(tracks: Vec, reply: String) -> String { - if !tracks.is_empty() { - let songs = tracks - .iter() - .map(|x| match x { - PlayableKind::Track(t) => t.name.clone(), - PlayableKind::Podcast(e) => e.name.clone() - } + "\n") - .collect::(); - reply - .clone() - .push_str(format!("\n🎶 {} Track(s): {}", tracks.len(), songs).as_str()) - } - reply -} - -fn add_track_section(tracks: Vec, reply: String) -> String { - if !tracks.is_empty() { - let songs = tracks - .iter() - .map(|x| x.name.clone() + "\n") - .collect::(); - reply - .clone() - .push_str(format!("\n🎶 {} Track(s): {}", tracks.len(), songs).as_str()) - } - reply -} diff --git a/src/spotify/mod.rs b/src/spotify/mod.rs index b27de44..6635de9 100644 --- a/src/spotify/mod.rs +++ b/src/spotify/mod.rs @@ -2,6 +2,8 @@ use rspotify::model::PlayableItem::{Episode, Track}; use rspotify::model::{AlbumId, FullTrack, PlaylistId, TrackId}; use rspotify::prelude::*; use rspotify::{ClientCredsSpotify, Credentials}; +use std::any::Any; +use std::sync::Arc; use std::time::Duration; pub enum SpotifyKind { @@ -45,6 +47,125 @@ pub struct PlaylistInfo { pub(crate) tracks: Vec, } +#[derive(Clone, Debug)] +pub(crate) struct Client { + client: Arc, +} + +impl Client { + pub(crate) async fn new() -> Self { + let spotify_creds = Credentials::from_env() + .expect("RSPOTIFY_CLIENT_ID and RSPOTIFY_CLIENT_SECRET not found."); + let mut spotify = ClientCredsSpotify::new(spotify_creds); + spotify.request_token().await.unwrap(); + Client { + client: Arc::new(spotify), + } + } + + pub(crate) fn new_with_dependencies(client: ClientCredsSpotify) -> Self { + Client { + client: Arc::new(client), + } + } + + pub async fn get_track(&self, id: &str) -> Option { + let track_id = match TrackId::from_id(id) { + Ok(track) => track, + Err(_e) => return None, + }; + + match self.client.track(&track_id).await { + Ok(track) => Some(TrackInfo { + name: track.name, + artists: track.artists.iter().map(|x| x.name.clone()).collect(), + duration: track.duration, + }), + Err(_e) => None, + } + } + + pub async fn get_album(&self, id: &str) -> Option { + let album_id = match AlbumId::from_id(id) { + Ok(album) => album, + Err(_e) => return None, + }; + + match self.client.album(&album_id).await { + Ok(album) => Some(AlbumInfo { + name: album.name, + artists: album.artists.iter().map(|x| x.name.clone()).collect(), + genres: album.genres, + tracks: album + .tracks + .items + .iter() + .map(|t| TrackInfo { + name: t.name.clone(), + artists: t.artists.iter().map(|x| x.name.clone()).collect(), + duration: t.duration, + }) + .collect(), + }), + Err(_e) => None, + } + } + + pub async fn get_playlist(&self, id: &String) -> Option { + let playlist_id = match PlaylistId::from_id(id.as_str()) { + Ok(playlist) => playlist, + Err(_e) => return None, + }; + + match self.client.playlist(&playlist_id, None, None).await { + Ok(playlist) => Some(PlaylistInfo { + name: playlist.name, + artists: playlist + .tracks + .items + .iter() + .flat_map(|p| { + match &p.track { + Some(t) => match t { + Track(t) => t.artists.iter().map(|a| a.name.clone()).collect(), + Episode(e) => vec![e.show.publisher.clone()], + }, + None => Vec::new(), + } + .into_iter() + }) + .collect(), + tracks: playlist + .tracks + .items + .iter() + .map(|p| match &p.track { + Some(t) => match t { + Track(t) => Some(PlayableKind::Track(TrackInfo { + name: t.name.clone(), + artists: t.artists.iter().map(|a| a.name.clone()).collect(), + duration: t.duration, + })), + Episode(e) => Some(PlayableKind::Podcast(EpisodeInfo { + name: e.name.clone(), + show: e.show.name.clone(), + duration: e.duration, + description: e.description.clone(), + languages: e.languages.clone(), + release_date: e.release_date.clone(), + })), + }, + None => None, + }) + .filter(|i| i.is_some()) + .map(|i| i.unwrap()) + .collect(), + }), + Err(_e) => None, + } + } +} + fn get_id_in_url(url: &str) -> Option<&str> { url.rsplit('/') .next() @@ -52,44 +173,76 @@ fn get_id_in_url(url: &str) -> Option<&str> { .and_then(|x| x.split('?').next()) } -fn extract_artists_from_tracks(tracks: Vec) -> Vec { - tracks - .iter() - .flat_map(|t| t.artists.iter().map(|a| a.name.clone())) - .collect() +fn get_id_in_uri(uri: &str) -> Option<&str> { + uri.rsplit(':').next() } -pub fn get_entry_kind(url: &str) -> Option { - if url.contains("https://open.spotify.com/track/") { - let track_id = get_id_in_url(url); +pub fn get_entry_kind(uri: &str) -> Option { + if uri.contains("spotify:track:") { + let track_id = get_id_in_uri(uri); return match track_id { Some(id) => Some(SpotifyKind::Track(id.to_string())), None => None, }; } - if url.contains("https://open.spotify.com/album/") { - let album_id = get_id_in_url(url); + if uri.contains("https://open.spotify.com/track/") { + let track_id = get_id_in_url(uri); + return match track_id { + Some(id) => Some(SpotifyKind::Track(id.to_string())), + None => None, + }; + } + if uri.contains("spotify:album:") { + let track_id = get_id_in_uri(uri); + return match track_id { + Some(id) => Some(SpotifyKind::Album(id.to_string())), + None => None, + }; + } + if uri.contains("https://open.spotify.com/album/") { + let album_id = get_id_in_url(uri); return match album_id { Some(id) => Some(SpotifyKind::Album(id.to_string())), None => None, }; } - if url.contains("https://open.spotify.com/playlist/") { - let playlist_id = get_id_in_url(url); + if uri.contains("spotify:playlist:") { + let track_id = get_id_in_uri(uri); + return match track_id { + Some(id) => Some(SpotifyKind::Album(id.to_string())), + None => None, + }; + } + if uri.contains("https://open.spotify.com/playlist/") { + let playlist_id = get_id_in_url(uri); return match playlist_id { Some(id) => Some(SpotifyKind::Playlist(id.to_string())), None => None, }; } - if url.contains("https://open.spotify.com/show/") { - let playlist_id = get_id_in_url(url); + if uri.contains("spotify:show:") { + let track_id = get_id_in_uri(uri); + return match track_id { + Some(id) => Some(SpotifyKind::Album(id.to_string())), + None => None, + }; + } + if uri.contains("https://open.spotify.com/show/") { + let playlist_id = get_id_in_url(uri); return match playlist_id { Some(id) => Some(SpotifyKind::Podcast(id.to_string())), None => None, }; } - if url.contains("https://open.spotify.com/episode/") { - let playlist_id = get_id_in_url(url); + if uri.contains("spotify:episode:") { + let track_id = get_id_in_uri(uri); + return match track_id { + Some(id) => Some(SpotifyKind::Album(id.to_string())), + None => None, + }; + } + if uri.contains("https://open.spotify.com/episode/") { + let playlist_id = get_id_in_url(uri); return match playlist_id { Some(id) => Some(SpotifyKind::Episode(id.to_string())), None => None, @@ -97,107 +250,3 @@ pub fn get_entry_kind(url: &str) -> Option { } return None; } - -pub async fn get_client() -> Box { - let spotify_creds = - Credentials::from_env().expect("RSPOTIFY_CLIENT_ID and RSPOTIFY_CLIENT_SECRET not found."); - let mut spotify = ClientCredsSpotify::new(spotify_creds); - spotify.request_token().await.unwrap(); - Box::new(spotify) -} - -pub async fn get_track(spotify: Box, id: &String) -> Option { - let track_id = match TrackId::from_id(id.as_str()) { - Ok(track) => track, - Err(_e) => return None, - }; - - match spotify.track(&track_id).await { - Ok(track) => Some(TrackInfo { - name: track.name, - artists: track.artists.iter().map(|x| x.name.clone()).collect(), - duration: track.duration, - }), - Err(_e) => None, - } -} - -pub async fn get_album(spotify: Box, id: &String) -> Option { - let album_id = match AlbumId::from_id(id.as_str()) { - Ok(album) => album, - Err(_e) => return None, - }; - - match spotify.album(&album_id).await { - Ok(album) => Some(AlbumInfo { - name: album.name, - artists: album.artists.iter().map(|x| x.name.clone()).collect(), - genres: album.genres, - tracks: album - .tracks - .items - .iter() - .map(|t| TrackInfo { - name: t.name.clone(), - artists: t.artists.iter().map(|x| x.name.clone()).collect(), - duration: t.duration, - }) - .collect(), - }), - Err(_e) => None, - } -} - -pub async fn get_playlist(spotify: Box, id: &String) -> Option { - let playlist_id = match PlaylistId::from_id(id.as_str()) { - Ok(playlist) => playlist, - Err(_e) => return None, - }; - - match spotify.playlist(&playlist_id, None, None).await { - Ok(playlist) => Some(PlaylistInfo { - name: playlist.name, - artists: playlist - .tracks - .items - .iter() - .flat_map(|p| { - match &p.track { - Some(t) => match t { - Track(t) => t.artists.iter().map(|a| a.name.clone()).collect(), - Episode(e) => vec![e.show.publisher.clone()], - }, - None => Vec::new(), - } - .into_iter() - }) - .collect(), - tracks: playlist - .tracks - .items - .iter() - .map(|p| match &p.track { - Some(t) => match t { - Track(t) => Some(PlayableKind::Track(TrackInfo { - name: t.name.clone(), - artists: t.artists.iter().map(|a| a.name.clone()).collect(), - duration: t.duration, - })), - Episode(e) => Some(PlayableKind::Podcast(EpisodeInfo { - name: e.name.clone(), - show: e.show.name.clone(), - duration: e.duration, - description: e.description.clone(), - languages: e.languages.clone(), - release_date: e.release_date.clone(), - })), - }, - None => None, - }) - .filter(|i| i.is_some()) - .map(|i| i.unwrap()) - .collect(), - }), - Err(_e) => None, - } -} diff --git a/src/youtube/mod.rs b/src/youtube/mod.rs index aa7456b..2077244 100644 --- a/src/youtube/mod.rs +++ b/src/youtube/mod.rs @@ -1,10 +1,5 @@ -use invidious::asynchronous::Client; use std::error::Error; - -#[derive(Debug, Clone)] -pub(crate) struct YoutubeClient { - client: Client, -} +use std::sync::Arc; pub(crate) struct VideoSearch { items: Vec