From 66bb5d1333a22a6f8baf2220c5f6dc600c70716e Mon Sep 17 00:00:00 2001 From: Davide Polonio Date: Fri, 15 Jul 2022 17:46:14 +0200 Subject: [PATCH] feat: add back playlist support --- src/main.rs | 20 ++++++-- src/search/mod.rs | 101 +++++++++++++++++++++++++++++++++++++- src/search/spotify/mod.rs | 6 ++- src/search/youtube/mod.rs | 3 +- src/tgformatter/mod.rs | 76 ++++++++++++++++------------ 5 files changed, 164 insertions(+), 42 deletions(-) diff --git a/src/main.rs b/src/main.rs index 8037b05..e463ee1 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,7 +1,8 @@ -use log::{info, log, LevelFilter}; +use log::{debug, info, log, LevelFilter}; use search::spotify; use sentry::ClientInitGuard; use std::env; +use std::process::id; use std::sync::Arc; use teloxide::prelude::*; @@ -11,7 +12,6 @@ use search::spotify::{PlayableKind, TrackInfo}; use crate::search::get_spotify_kind; use crate::spotify::ContentKind; use search::spotify::ContentKind::{Album, Episode, Playlist, Podcast}; -use tgformatter::utils::{human_readable_duration, truncate_with_dots}; mod search; mod tgformatter; @@ -65,15 +65,25 @@ async fn main() { } Album(id) => { info!("Processing album with spotify id: {}", id); - let album_item = music_engine.get_album_by_spotify_id(text_message).await; + let album_item = music_engine.get_album_from_spotify_id(text_message).await; tgformatter::format_album_message(album_item) } - _ => None, + Playlist(id) => { + info!("Processing playlist with spotify id: {}", id); + let playlist_item = music_engine + .get_playlist_from_spotify_id(text_message) + .await; + tgformatter::format_playlist_message(playlist_item) + } + _ => { + log::warn!("This kind of media has been not supported yet"); + None + } }, }; if option_reply.is_some() { - info!("Got value to send back"); + debug!("Got reply to send back"); let reply = option_reply.unwrap(); bot.send_message(message.chat.id, reply) .reply_to_message_id(message.id) diff --git a/src/search/mod.rs b/src/search/mod.rs index 09beaf1..6579582 100644 --- a/src/search/mod.rs +++ b/src/search/mod.rs @@ -1,23 +1,94 @@ -use crate::spotify::{get_entry_kind, AlbumInfo}; +use crate::spotify::{get_entry_kind, AlbumInfo, PlaylistInfo}; use crate::TrackInfo; use log::info; use spotify::ContentKind; +use std::collections::HashSet; use youtube::Video; pub mod spotify; mod youtube; +pub(crate) trait ArtistComposed { + fn get_artists_name(&self) -> HashSet; +} + #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub(crate) struct TrackItem { pub(crate) spotify_track: Option, pub(crate) youtube_track: Option>, } +impl ArtistComposed for TrackItem { + fn get_artists_name(&self) -> HashSet { + if self.spotify_track.is_some() { + return self + .spotify_track + .clone() + .unwrap() + .artists + .into_iter() + .collect(); + } else { + self.youtube_track + .clone() + .and_then(|youtube_tracks| { + youtube_tracks.get(0).map(|t| { + let mut hash = HashSet::new(); + hash.insert(t.author.clone()); + return hash; + }) + }) + .unwrap_or_else(|| { + let mut hash = HashSet::new(); + hash.insert("Unknown artist".to_string()); + return hash; + }) + } + } +} + #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub(crate) struct AlbumItem { pub(crate) spotify_track: Option, } +impl ArtistComposed for AlbumItem { + fn get_artists_name(&self) -> HashSet { + if self.spotify_track.is_some() { + return self + .spotify_track + .clone() + .unwrap() + .artists + .into_iter() + .collect(); + } + + return HashSet::new(); + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub(crate) struct PlaylistItem { + pub(crate) spotify_playlist: Option, +} + +impl ArtistComposed for PlaylistItem { + fn get_artists_name(&self) -> HashSet { + if self.spotify_playlist.is_some() { + return self + .spotify_playlist + .clone() + .unwrap() + .artists + .into_iter() + .collect(); + } + + return HashSet::new(); + } +} + // The enum holds all the currently supported type of Id which the engine can search for #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub(crate) enum ServiceIdKind { @@ -99,7 +170,7 @@ impl Engine { }; } - pub(crate) async fn get_album_by_spotify_id(&self, message: &str) -> AlbumItem { + pub(crate) async fn get_album_from_spotify_id(&self, message: &str) -> AlbumItem { let entry_kind = spotify::get_entry_kind(message); let album_info = match entry_kind { @@ -114,6 +185,22 @@ impl Engine { spotify_track: album_info, } } + + pub(crate) async fn get_playlist_from_spotify_id(&self, message: &str) -> PlaylistItem { + let entry_kind = spotify::get_entry_kind(message); + + let playlist_info = match entry_kind { + Some(entry) => match entry { + ContentKind::Playlist(id) => self.spotify.get_playlist(id.as_str()).await, + _ => None, + }, + None => None, + }; + + PlaylistItem { + spotify_playlist: playlist_info, + } + } } pub(crate) fn get_spotify_kind(spotify_id: &str) -> Option { @@ -128,4 +215,14 @@ mod tests { 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!") + } } diff --git a/src/search/spotify/mod.rs b/src/search/spotify/mod.rs index e54436c..7485b44 100644 --- a/src/search/spotify/mod.rs +++ b/src/search/spotify/mod.rs @@ -51,6 +51,7 @@ pub struct PlaylistInfo { pub(crate) name: String, pub(crate) artists: Vec, pub(crate) tracks: Vec, + pub(crate) owner: Option, } #[derive(Clone, Debug)] @@ -118,8 +119,8 @@ impl Client { } } - pub async fn get_playlist(&self, id: &String) -> Option { - let playlist_id = match PlaylistId::from_id(id.as_str()) { + pub async fn get_playlist(&self, id: &str) -> Option { + let playlist_id = match PlaylistId::from_id(id) { Ok(playlist) => playlist, Err(_e) => return None, }; @@ -167,6 +168,7 @@ impl Client { .filter(|i| i.is_some()) .map(|i| i.unwrap()) .collect(), + owner: playlist.owner.display_name, }), Err(_e) => None, } diff --git a/src/search/youtube/mod.rs b/src/search/youtube/mod.rs index 2e9ad68..da645dc 100644 --- a/src/search/youtube/mod.rs +++ b/src/search/youtube/mod.rs @@ -39,7 +39,8 @@ pub(crate) struct Client { impl Client { pub(crate) async fn new() -> Self { // TODO check for a stable instance - let client = invidious::asynchronous::Client::new(String::from("https://vid.puffyan.us")); + let client = + invidious::asynchronous::Client::new(String::from("https://inv.bp.projectsegfau.lt")); Client { client: Arc::new(client), diff --git a/src/tgformatter/mod.rs b/src/tgformatter/mod.rs index 930ecaa..1e7b11b 100644 --- a/src/tgformatter/mod.rs +++ b/src/tgformatter/mod.rs @@ -1,4 +1,6 @@ -use crate::search::{AlbumItem, TrackItem}; +mod utils; + +use crate::search::{AlbumItem, ArtistComposed, PlaylistItem, TrackItem}; use crate::TrackInfo; use log::{info, log}; use std::borrow::Borrow; @@ -7,9 +9,7 @@ use std::fmt::format; use std::time::Duration; use teloxide::repl; -pub mod utils; - -static MAX_ARTISTS_CHARS: usize = 140; +static MAX_ARTISTS_CHARS: usize = 200; pub(crate) fn format_track_message(track_info: TrackItem) -> Option { // Let's avoid copying all the structure...we place it in the heap and pass the pointer to all the other functions @@ -24,7 +24,7 @@ pub(crate) fn format_track_message(track_info: TrackItem) -> Option { šŸ§‘ā€šŸŽ¤ Artist(s): {}\n\ ā³ Duration: {}", get_track_name(boxed_info.clone()), - get_track_artists(boxed_info.clone()), + get_artists(boxed_info.clone()), get_track_duration(boxed_info.clone()) ); @@ -47,30 +47,52 @@ pub(crate) fn format_album_message(album_info: AlbumItem) -> Option { return None; } - let mut artists = get_album_artists(boxed_info.clone()); - let mut artists_names = "Unknown artist list".to_string(); - - if artists.len() > 0 { - artists_names = itertools::join(&artists, ", "); - } + let artists = boxed_info.get_artists_name().len(); let mut reply = format!( "Album information:\n\ šŸŽµ Album name: {}\n\ šŸ§‘ā€šŸŽ¤ {} artist(s): {}", get_album_name(boxed_info.clone()), - artists.len(), - artists_names + artists, + get_artists(boxed_info.clone()) ); let mut album_genres = get_album_genres(boxed_info.clone()); if album_genres.len() > 0 { - reply.push_str(format!("\nšŸ’æ Genre(s): {}", itertools::join(&artists, ", ")).as_str()); + reply.push_str(format!("\nšŸ’æ Genre(s): {}", itertools::join(&album_genres, ", ")).as_str()); } return Some(reply); } +pub(crate) fn format_playlist_message(playlist_info: PlaylistItem) -> Option { + let boxed_info = Box::new(playlist_info); + if boxed_info.spotify_playlist.is_none() { + return None; + } + + let artists = boxed_info.clone().get_artists_name().len(); + + let mut reply = format!( + "Playlist information:\n\ +āœ’ļø Playlist name: {}\n\ +šŸ§ž Made by: {}\n\ +šŸ§‘ā€šŸŽ¤ {} artist(s): {}", + boxed_info.clone().spotify_playlist.unwrap().name, + boxed_info + .clone() + .spotify_playlist + .unwrap() + .owner + .unwrap_or("Unknown šŸ¤·".to_string()), + artists, + get_artists(boxed_info) + ); + + return Some(reply); +} + fn get_album_name(ai: Box) -> String { ai.spotify_track .map(|s| s.name) @@ -85,14 +107,6 @@ fn get_album_genres(ai: Box) -> HashSet { return HashSet::new(); } -fn get_album_artists(ai: Box) -> HashSet { - if ai.spotify_track.is_some() { - return ai.spotify_track.unwrap().artists.into_iter().collect(); - } - - return HashSet::new(); -} - fn get_track_name(ti: Box) -> String { if ti.spotify_track.is_some() { ti.spotify_track.unwrap().name @@ -103,17 +117,15 @@ fn get_track_name(ti: Box) -> String { } } -fn get_track_artists(ti: Box) -> String { - if ti.spotify_track.is_some() { - utils::truncate_with_dots( - ti.spotify_track.unwrap().artists.join(", "), - MAX_ARTISTS_CHARS, - ) - } else { - ti.youtube_track - .and_then(|youtube_tracks| youtube_tracks.get(0).map(|t| t.author.clone())) - .unwrap_or("Unknown artist".to_string()) +fn get_artists(ti: Box) -> String { + let artists = ti.get_artists_name(); + let mut artists_names = "Unknown artist list".to_string(); + + if artists.len() > 0 { + artists_names = itertools::join(&artists, ", "); } + + utils::truncate_with_dots(artists_names, MAX_ARTISTS_CHARS) } fn get_track_duration(ti: Box) -> String {