feat: add youtube search #8
							
								
								
									
										20
									
								
								src/main.rs
									
									
									
									
									
								
							
							
						
						
									
										20
									
								
								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) | ||||
|  | ||||
| @ -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<String>; | ||||
| } | ||||
| 
 | ||||
| #[derive(Debug, Clone, PartialEq, Eq, Hash)] | ||||
| pub(crate) struct TrackItem { | ||||
|     pub(crate) spotify_track: Option<TrackInfo>, | ||||
|     pub(crate) youtube_track: Option<Vec<Video>>, | ||||
| } | ||||
| 
 | ||||
| impl ArtistComposed for TrackItem { | ||||
|     fn get_artists_name(&self) -> HashSet<String> { | ||||
|         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<AlbumInfo>, | ||||
| } | ||||
| 
 | ||||
| impl ArtistComposed for AlbumItem { | ||||
|     fn get_artists_name(&self) -> HashSet<String> { | ||||
|         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<PlaylistInfo>, | ||||
| } | ||||
| 
 | ||||
| impl ArtistComposed for PlaylistItem { | ||||
|     fn get_artists_name(&self) -> HashSet<String> { | ||||
|         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<ContentKind> { | ||||
| @ -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!") | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -51,6 +51,7 @@ pub struct PlaylistInfo { | ||||
|     pub(crate) name: String, | ||||
|     pub(crate) artists: Vec<String>, | ||||
|     pub(crate) tracks: Vec<PlayableKind>, | ||||
|     pub(crate) owner: Option<String>, | ||||
| } | ||||
| 
 | ||||
| #[derive(Clone, Debug)] | ||||
| @ -118,8 +119,8 @@ impl Client { | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     pub async fn get_playlist(&self, id: &String) -> Option<PlaylistInfo> { | ||||
|         let playlist_id = match PlaylistId::from_id(id.as_str()) { | ||||
|     pub async fn get_playlist(&self, id: &str) -> Option<PlaylistInfo> { | ||||
|         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, | ||||
|         } | ||||
|  | ||||
| @ -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), | ||||
|  | ||||
| @ -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<String> { | ||||
|     // 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<String> { | ||||
|                                 🧑🎤 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<String> { | ||||
|         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<String> { | ||||
|     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<AlbumItem>) -> String { | ||||
|     ai.spotify_track | ||||
|         .map(|s| s.name) | ||||
| @ -85,14 +107,6 @@ fn get_album_genres(ai: Box<AlbumItem>) -> HashSet<String> { | ||||
|     return HashSet::new(); | ||||
| } | ||||
| 
 | ||||
| fn get_album_artists(ai: Box<AlbumItem>) -> HashSet<String> { | ||||
|     if ai.spotify_track.is_some() { | ||||
|         return ai.spotify_track.unwrap().artists.into_iter().collect(); | ||||
|     } | ||||
| 
 | ||||
|     return HashSet::new(); | ||||
| } | ||||
| 
 | ||||
| fn get_track_name(ti: Box<TrackItem>) -> String { | ||||
|     if ti.spotify_track.is_some() { | ||||
|         ti.spotify_track.unwrap().name | ||||
| @ -103,17 +117,15 @@ fn get_track_name(ti: Box<TrackItem>) -> String { | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| fn get_track_artists(ti: Box<TrackItem>) -> 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<dyn ArtistComposed>) -> 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<TrackItem>) -> String { | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user