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 search::spotify; | ||||||
| use sentry::ClientInitGuard; | use sentry::ClientInitGuard; | ||||||
| use std::env; | use std::env; | ||||||
|  | use std::process::id; | ||||||
| use std::sync::Arc; | use std::sync::Arc; | ||||||
| use teloxide::prelude::*; | use teloxide::prelude::*; | ||||||
| 
 | 
 | ||||||
| @ -11,7 +12,6 @@ use search::spotify::{PlayableKind, TrackInfo}; | |||||||
| use crate::search::get_spotify_kind; | use crate::search::get_spotify_kind; | ||||||
| use crate::spotify::ContentKind; | use crate::spotify::ContentKind; | ||||||
| use search::spotify::ContentKind::{Album, Episode, Playlist, Podcast}; | use search::spotify::ContentKind::{Album, Episode, Playlist, Podcast}; | ||||||
| use tgformatter::utils::{human_readable_duration, truncate_with_dots}; |  | ||||||
| 
 | 
 | ||||||
| mod search; | mod search; | ||||||
| mod tgformatter; | mod tgformatter; | ||||||
| @ -65,15 +65,25 @@ async fn main() { | |||||||
|                 } |                 } | ||||||
|                 Album(id) => { |                 Album(id) => { | ||||||
|                     info!("Processing album with spotify id: {}", 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) |                     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() { |         if option_reply.is_some() { | ||||||
|             info!("Got value to send back"); |             debug!("Got reply to send back"); | ||||||
|             let reply = option_reply.unwrap(); |             let reply = option_reply.unwrap(); | ||||||
|             bot.send_message(message.chat.id, reply) |             bot.send_message(message.chat.id, reply) | ||||||
|                 .reply_to_message_id(message.id) |                 .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 crate::TrackInfo; | ||||||
| use log::info; | use log::info; | ||||||
| use spotify::ContentKind; | use spotify::ContentKind; | ||||||
|  | use std::collections::HashSet; | ||||||
| use youtube::Video; | use youtube::Video; | ||||||
| 
 | 
 | ||||||
| pub mod spotify; | pub mod spotify; | ||||||
| mod youtube; | mod youtube; | ||||||
| 
 | 
 | ||||||
|  | pub(crate) trait ArtistComposed { | ||||||
|  |     fn get_artists_name(&self) -> HashSet<String>; | ||||||
|  | } | ||||||
|  | 
 | ||||||
| #[derive(Debug, Clone, PartialEq, Eq, Hash)] | #[derive(Debug, Clone, PartialEq, Eq, Hash)] | ||||||
| pub(crate) struct TrackItem { | pub(crate) struct TrackItem { | ||||||
|     pub(crate) spotify_track: Option<TrackInfo>, |     pub(crate) spotify_track: Option<TrackInfo>, | ||||||
|     pub(crate) youtube_track: Option<Vec<Video>>, |     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)] | #[derive(Debug, Clone, PartialEq, Eq, Hash)] | ||||||
| pub(crate) struct AlbumItem { | pub(crate) struct AlbumItem { | ||||||
|     pub(crate) spotify_track: Option<AlbumInfo>, |     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
 | // The enum holds all the currently supported type of Id which the engine can search for
 | ||||||
| #[derive(Debug, Clone, PartialEq, Eq, Hash)] | #[derive(Debug, Clone, PartialEq, Eq, Hash)] | ||||||
| pub(crate) enum ServiceIdKind { | 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 entry_kind = spotify::get_entry_kind(message); | ||||||
| 
 | 
 | ||||||
|         let album_info = match entry_kind { |         let album_info = match entry_kind { | ||||||
| @ -114,6 +185,22 @@ impl Engine { | |||||||
|             spotify_track: album_info, |             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> { | pub(crate) fn get_spotify_kind(spotify_id: &str) -> Option<ContentKind> { | ||||||
| @ -128,4 +215,14 @@ mod tests { | |||||||
|     fn should_search_track_by_spotify_id() { |     fn should_search_track_by_spotify_id() { | ||||||
|         todo!("Implement me!") |         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) name: String, | ||||||
|     pub(crate) artists: Vec<String>, |     pub(crate) artists: Vec<String>, | ||||||
|     pub(crate) tracks: Vec<PlayableKind>, |     pub(crate) tracks: Vec<PlayableKind>, | ||||||
|  |     pub(crate) owner: Option<String>, | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| #[derive(Clone, Debug)] | #[derive(Clone, Debug)] | ||||||
| @ -118,8 +119,8 @@ impl Client { | |||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     pub async fn get_playlist(&self, id: &String) -> Option<PlaylistInfo> { |     pub async fn get_playlist(&self, id: &str) -> Option<PlaylistInfo> { | ||||||
|         let playlist_id = match PlaylistId::from_id(id.as_str()) { |         let playlist_id = match PlaylistId::from_id(id) { | ||||||
|             Ok(playlist) => playlist, |             Ok(playlist) => playlist, | ||||||
|             Err(_e) => return None, |             Err(_e) => return None, | ||||||
|         }; |         }; | ||||||
| @ -167,6 +168,7 @@ impl Client { | |||||||
|                     .filter(|i| i.is_some()) |                     .filter(|i| i.is_some()) | ||||||
|                     .map(|i| i.unwrap()) |                     .map(|i| i.unwrap()) | ||||||
|                     .collect(), |                     .collect(), | ||||||
|  |                 owner: playlist.owner.display_name, | ||||||
|             }), |             }), | ||||||
|             Err(_e) => None, |             Err(_e) => None, | ||||||
|         } |         } | ||||||
|  | |||||||
| @ -39,7 +39,8 @@ pub(crate) struct Client { | |||||||
| impl Client { | impl Client { | ||||||
|     pub(crate) async fn new() -> Self { |     pub(crate) async fn new() -> Self { | ||||||
|         // TODO check for a stable instance
 |         // 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 { | ||||||
|             client: Arc::new(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 crate::TrackInfo; | ||||||
| use log::{info, log}; | use log::{info, log}; | ||||||
| use std::borrow::Borrow; | use std::borrow::Borrow; | ||||||
| @ -7,9 +9,7 @@ use std::fmt::format; | |||||||
| use std::time::Duration; | use std::time::Duration; | ||||||
| use teloxide::repl; | use teloxide::repl; | ||||||
| 
 | 
 | ||||||
| pub mod utils; | static MAX_ARTISTS_CHARS: usize = 200; | ||||||
| 
 |  | ||||||
| static MAX_ARTISTS_CHARS: usize = 140; |  | ||||||
| 
 | 
 | ||||||
| pub(crate) fn format_track_message(track_info: TrackItem) -> Option<String> { | 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
 |     // 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\ |                                 🧑🎤 Artist(s): {}\n\ | ||||||
|                                 ⏳ Duration: {}",
 |                                 ⏳ Duration: {}",
 | ||||||
|         get_track_name(boxed_info.clone()), |         get_track_name(boxed_info.clone()), | ||||||
|         get_track_artists(boxed_info.clone()), |         get_artists(boxed_info.clone()), | ||||||
|         get_track_duration(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; |         return None; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     let mut artists = get_album_artists(boxed_info.clone()); |     let artists = boxed_info.get_artists_name().len(); | ||||||
|     let mut artists_names = "Unknown artist list".to_string(); |  | ||||||
| 
 |  | ||||||
|     if artists.len() > 0 { |  | ||||||
|         artists_names = itertools::join(&artists, ", "); |  | ||||||
|     } |  | ||||||
| 
 | 
 | ||||||
|     let mut reply = format!( |     let mut reply = format!( | ||||||
|         "Album information:\n\ |         "Album information:\n\ | ||||||
|                                     🎵 Album name: {}\n\ |                                     🎵 Album name: {}\n\ | ||||||
|                                     🧑🎤 {} artist(s): {}",
 |                                     🧑🎤 {} artist(s): {}",
 | ||||||
|         get_album_name(boxed_info.clone()), |         get_album_name(boxed_info.clone()), | ||||||
|         artists.len(), |         artists, | ||||||
|         artists_names |         get_artists(boxed_info.clone()) | ||||||
|     ); |     ); | ||||||
| 
 | 
 | ||||||
|     let mut album_genres = get_album_genres(boxed_info.clone()); |     let mut album_genres = get_album_genres(boxed_info.clone()); | ||||||
|     if album_genres.len() > 0 { |     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); |     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 { | fn get_album_name(ai: Box<AlbumItem>) -> String { | ||||||
|     ai.spotify_track |     ai.spotify_track | ||||||
|         .map(|s| s.name) |         .map(|s| s.name) | ||||||
| @ -85,14 +107,6 @@ fn get_album_genres(ai: Box<AlbumItem>) -> HashSet<String> { | |||||||
|     return HashSet::new(); |     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 { | fn get_track_name(ti: Box<TrackItem>) -> String { | ||||||
|     if ti.spotify_track.is_some() { |     if ti.spotify_track.is_some() { | ||||||
|         ti.spotify_track.unwrap().name |         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 { | fn get_artists(ti: Box<dyn ArtistComposed>) -> String { | ||||||
|     if ti.spotify_track.is_some() { |     let artists = ti.get_artists_name(); | ||||||
|         utils::truncate_with_dots( |     let mut artists_names = "Unknown artist list".to_string(); | ||||||
|             ti.spotify_track.unwrap().artists.join(", "), | 
 | ||||||
|             MAX_ARTISTS_CHARS, |     if artists.len() > 0 { | ||||||
|         ) |         artists_names = itertools::join(&artists, ", "); | ||||||
|     } else { |  | ||||||
|         ti.youtube_track |  | ||||||
|             .and_then(|youtube_tracks| youtube_tracks.get(0).map(|t| t.author.clone())) |  | ||||||
|             .unwrap_or("Unknown artist".to_string()) |  | ||||||
|     } |     } | ||||||
|  | 
 | ||||||
|  |     utils::truncate_with_dots(artists_names, MAX_ARTISTS_CHARS) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| fn get_track_duration(ti: Box<TrackItem>) -> String { | fn get_track_duration(ti: Box<TrackItem>) -> String { | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user