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