feat: add youtube search #8
| @ -14,6 +14,7 @@ rspotify = { version = "0.11.5", features = ["default"]} | |||||||
| sentry = "0.26.0" | sentry = "0.26.0" | ||||||
| invidious = "0.2.1" | invidious = "0.2.1" | ||||||
| chrono = "0.4.19" | chrono = "0.4.19" | ||||||
|  | itertools = "0.10.3" | ||||||
| 
 | 
 | ||||||
| [dev-dependencies] | [dev-dependencies] | ||||||
| tokio-test = "0.4.2" | tokio-test = "0.4.2" | ||||||
|  | |||||||
							
								
								
									
										133
									
								
								src/main.rs
									
									
									
									
									
								
							
							
						
						
									
										133
									
								
								src/main.rs
									
									
									
									
									
								
							| @ -1,20 +1,20 @@ | |||||||
| use log::LevelFilter; | use log::{info, log, LevelFilter}; | ||||||
|  | use search::spotify; | ||||||
| use sentry::ClientInitGuard; | use sentry::ClientInitGuard; | ||||||
| use std::env; | use std::env; | ||||||
|  | use std::sync::Arc; | ||||||
| use teloxide::prelude::*; | use teloxide::prelude::*; | ||||||
| 
 | 
 | ||||||
| use crate::spotify::{PlayableKind, TrackInfo}; | use search::spotify::ContentKind::Track; | ||||||
| use spotify::ContentKind::Track; | use search::spotify::{PlayableKind, TrackInfo}; | ||||||
| 
 | 
 | ||||||
| use crate::spotify::ContentKind::{Album, Episode, Playlist, Podcast}; | use crate::search::get_spotify_kind; | ||||||
| use crate::utils::{human_readable_duration, truncate_with_dots}; | use crate::spotify::ContentKind; | ||||||
|  | use search::spotify::ContentKind::{Album, Episode, Playlist, Podcast}; | ||||||
|  | use tgformatter::utils::{human_readable_duration, truncate_with_dots}; | ||||||
| 
 | 
 | ||||||
| mod engine; | mod search; | ||||||
| mod spotify; | mod tgformatter; | ||||||
| mod utils; |  | ||||||
| mod youtube; |  | ||||||
| 
 |  | ||||||
| static MAX_ARTISTS_CHARS: usize = 140; |  | ||||||
| 
 | 
 | ||||||
| #[tokio::main] | #[tokio::main] | ||||||
| async fn main() { | async fn main() { | ||||||
| @ -48,94 +48,39 @@ async fn main() { | |||||||
| 
 | 
 | ||||||
|     let bot = Bot::from_env().auto_send(); |     let bot = Bot::from_env().auto_send(); | ||||||
|     teloxide::repl(bot, |message: Message, bot: AutoSend<Bot>| async move { |     teloxide::repl(bot, |message: Message, bot: AutoSend<Bot>| async move { | ||||||
|         let text = message.text().and_then(spotify::get_entry_kind); |         let music_engine = search::Engine::new().await; | ||||||
|         match text { |         let opt_text_message = message.text(); | ||||||
|             Some(spotify) => { |         if opt_text_message.is_none() { | ||||||
|                 let spotify_client = spotify::Client::new().await; |             return respond(()); | ||||||
|                 match spotify { |         } | ||||||
|  |         let text_message = opt_text_message.unwrap(); | ||||||
|  |         let content_kind = opt_text_message.and_then(|x| get_spotify_kind(x)); | ||||||
|  |         let option_reply = match content_kind { | ||||||
|  |             None => return respond(()), | ||||||
|  |             Some(content) => match content { | ||||||
|                 Track(id) => { |                 Track(id) => { | ||||||
|                         log::debug!("Parsing spotify song: {}", id); |                     info!("Processing song with spotify id: {}", id); | ||||||
|                         let track_info = spotify_client.get_track(&id).await; |                     let track_item = music_engine.get_song_from_spotify_id(text_message).await; | ||||||
|                         match track_info { |                     tgformatter::format_track_message(track_item) | ||||||
|                             Some(info) => { |  | ||||||
|                                 let reply = format!( |  | ||||||
|                                     "Track information:\n\ |  | ||||||
|                                 🎵 Track name: {}\n\ |  | ||||||
|                                 🧑🎤 Artist(s): {}\n\ |  | ||||||
|                                 ⏳ Duration: {}",
 |  | ||||||
|                                     info.name, |  | ||||||
|                                     truncate_with_dots(info.artists.join(", "), MAX_ARTISTS_CHARS), |  | ||||||
|                                     human_readable_duration(info.duration) |  | ||||||
|                                 ); |  | ||||||
|                                 bot.send_message(message.chat.id, reply) |  | ||||||
|                                     .reply_to_message_id(message.id) |  | ||||||
|                                     .await?; |  | ||||||
|                                 Some(respond(())) |  | ||||||
|                             } |  | ||||||
|                             None => None, |  | ||||||
|                         } |  | ||||||
|                 } |                 } | ||||||
|                 Album(id) => { |                 Album(id) => { | ||||||
|                         log::debug!("Parsing spotify album: {}", id); |                     info!("Processing album with spotify id: {}", id); | ||||||
|                         let album_info = spotify_client.get_album(&id).await; |                     let album_item = music_engine.get_album_by_spotify_id(text_message).await; | ||||||
|                         match album_info { |                     tgformatter::format_album_message(album_item) | ||||||
|                             Some(info) => { |  | ||||||
|                                 let mut reply = format!( |  | ||||||
|                                     "Album information:\n\ |  | ||||||
|                                     🎵 Album name: {}\n\ |  | ||||||
|                                     🧑🎤 {} artist(s): {}",
 |  | ||||||
|                                     info.name, |  | ||||||
|                                     info.artists.len(), |  | ||||||
|                                     truncate_with_dots(info.artists.join(", "), MAX_ARTISTS_CHARS) |  | ||||||
|                                 ); |  | ||||||
|                                 if !info.genres.is_empty() { |  | ||||||
|                                     reply.push_str( |  | ||||||
|                                         format!("\n💿 Genre(s): {}", info.genres.join(", ")) |  | ||||||
|                                             .as_str(), |  | ||||||
|                                     ); |  | ||||||
|                 } |                 } | ||||||
|                                 bot.send_message(message.chat.id, reply) |                 _ => None, | ||||||
|                                     .reply_to_message_id(message.id) |             }, | ||||||
|                                     .await?; |  | ||||||
|                                 Some(respond(())) |  | ||||||
|                             } |  | ||||||
|                             None => None, |  | ||||||
|                         } |  | ||||||
|                     } |  | ||||||
|                     Playlist(id) => { |  | ||||||
|                         log::debug!("Parsing spotify playlist: {}", id); |  | ||||||
|                         let playlist_info = spotify_client.get_playlist(&id).await; |  | ||||||
|                         match playlist_info { |  | ||||||
|                             Some(info) => { |  | ||||||
|                                 let reply = format!( |  | ||||||
|                                     "Playlist information:\n\ |  | ||||||
|                                     ✒️ Playlist name: {}\n\ |  | ||||||
|                                     🧑🎤 {} artist(s): {}",
 |  | ||||||
|                                     info.name, |  | ||||||
|                                     info.artists.len(), |  | ||||||
|                                     truncate_with_dots(info.artists.join(", "), MAX_ARTISTS_CHARS) |  | ||||||
|                                 ); |  | ||||||
|                                 bot.send_message(message.chat.id, reply) |  | ||||||
|                                     .reply_to_message_id(message.id) |  | ||||||
|                                     .await?; |  | ||||||
|                                 Some(respond(())) |  | ||||||
|                             } |  | ||||||
|                             None => None, |  | ||||||
|                         } |  | ||||||
|                     } |  | ||||||
|                     Episode(id) => { |  | ||||||
|                         log::warn!("Support for episodes ({}) has not be implemented yet!", id); |  | ||||||
|                         None |  | ||||||
|                     } |  | ||||||
|                     Podcast(id) => { |  | ||||||
|                         log::warn!("Support for podcasts ({}) has not be implemented yet!", id); |  | ||||||
|                         None |  | ||||||
|                     } |  | ||||||
|                 } |  | ||||||
|             } |  | ||||||
|             None => None, |  | ||||||
|         }; |         }; | ||||||
|         respond(()) | 
 | ||||||
|  |         if option_reply.is_some() { | ||||||
|  |             info!("Got value to send back"); | ||||||
|  |             let reply = option_reply.unwrap(); | ||||||
|  |             bot.send_message(message.chat.id, reply) | ||||||
|  |                 .reply_to_message_id(message.id) | ||||||
|  |                 .await?; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         return respond(()); | ||||||
|     }) |     }) | ||||||
|     .await; |     .await; | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -1,15 +1,25 @@ | |||||||
| use crate::spotify::ContentKind; | use crate::spotify::{get_entry_kind, AlbumInfo}; | ||||||
| use crate::youtube::Video; | use crate::TrackInfo; | ||||||
| use crate::{spotify, youtube, TrackInfo}; | use log::info; | ||||||
|  | use spotify::ContentKind; | ||||||
|  | use youtube::Video; | ||||||
| 
 | 
 | ||||||
| #[derive(Debug, Clone)] | pub mod spotify; | ||||||
|  | mod youtube; | ||||||
|  | 
 | ||||||
|  | #[derive(Debug, Clone, PartialEq, Eq, Hash)] | ||||||
| pub(crate) struct TrackItem { | pub(crate) struct TrackItem { | ||||||
|     spotify_track: Option<TrackInfo>, |     pub(crate) spotify_track: Option<TrackInfo>, | ||||||
|     youtube_track: Option<Vec<Video>>, |     pub(crate) youtube_track: Option<Vec<Video>>, | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | #[derive(Debug, Clone, PartialEq, Eq, Hash)] | ||||||
|  | pub(crate) struct AlbumItem { | ||||||
|  |     pub(crate) spotify_track: Option<AlbumInfo>, | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // 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)] | #[derive(Debug, Clone, PartialEq, Eq, Hash)] | ||||||
| pub(crate) enum ServiceIdKind { | pub(crate) enum ServiceIdKind { | ||||||
|     Spotify(String), |     Spotify(String), | ||||||
|     Youtube(String), |     Youtube(String), | ||||||
| @ -19,14 +29,14 @@ pub(crate) enum ServiceIdKind { | |||||||
| // This struct will allow us in the future to search, cache and store data and metadata regarding
 | // This struct will allow us in the future to search, cache and store data and metadata regarding
 | ||||||
| // tracks, albums and playlists
 | // tracks, albums and playlists
 | ||||||
| #[derive(Debug, Clone)] | #[derive(Debug, Clone)] | ||||||
| pub(crate) struct MusicEngine { | pub(crate) struct Engine { | ||||||
|     spotify: spotify::Client, |     spotify: spotify::Client, | ||||||
|     youtube: youtube::Client, |     youtube: youtube::Client, | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| impl MusicEngine { | impl Engine { | ||||||
|     pub(crate) async fn new() -> Self { |     pub(crate) async fn new() -> Self { | ||||||
|         MusicEngine { |         Engine { | ||||||
|             spotify: spotify::Client::new().await, |             spotify: spotify::Client::new().await, | ||||||
|             youtube: youtube::Client::new().await, |             youtube: youtube::Client::new().await, | ||||||
|         } |         } | ||||||
| @ -36,18 +46,18 @@ impl MusicEngine { | |||||||
|         spotify_client: spotify::Client, |         spotify_client: spotify::Client, | ||||||
|         youtube_client: youtube::Client, |         youtube_client: youtube::Client, | ||||||
|     ) -> Self { |     ) -> Self { | ||||||
|         MusicEngine { |         Engine { | ||||||
|             spotify: spotify_client, |             spotify: spotify_client, | ||||||
|             youtube: youtube_client, |             youtube: youtube_client, | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     pub(crate) async fn search_song_by_name(&self, name: &str) { |     pub(crate) async fn get_song_by_name(&self, name: &str) -> TrackItem { | ||||||
|         todo!("In the future it would be possible to search for all metadata on a record from this call") |         todo!("In the future it would be possible to search for all metadata on a record from this call") | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     pub(crate) async fn search_song_by_id(&self, id: &str) -> Option<TrackItem> { |     pub(crate) async fn get_song_from_spotify_id(&self, message: &str) -> TrackItem { | ||||||
|         let entry_kind = spotify::get_entry_kind(id); |         let entry_kind = spotify::get_entry_kind(message); | ||||||
|         let track_info = match entry_kind { |         let track_info = match entry_kind { | ||||||
|             Some(entry) => match entry { |             Some(entry) => match entry { | ||||||
|                 ContentKind::Track(id) => self.spotify.get_track(id.as_str()).await, |                 ContentKind::Track(id) => self.spotify.get_track(id.as_str()).await, | ||||||
| @ -78,13 +88,36 @@ impl MusicEngine { | |||||||
|                 Ok(search) => Some(search), |                 Ok(search) => Some(search), | ||||||
|             }; |             }; | ||||||
| 
 | 
 | ||||||
|             return Some(TrackItem { |             return TrackItem { | ||||||
|                 spotify_track: Some(ti), |                 spotify_track: Some(ti), | ||||||
|                 youtube_track: youtube_search.map(|search| search.items), |                 youtube_track: youtube_search.map(|search| search.items), | ||||||
|             }); |             }; | ||||||
|         } |         } | ||||||
|         return None; |         return TrackItem { | ||||||
|  |             spotify_track: None, | ||||||
|  |             youtube_track: None, | ||||||
|  |         }; | ||||||
|     } |     } | ||||||
|  | 
 | ||||||
|  |     pub(crate) async fn get_album_by_spotify_id(&self, message: &str) -> AlbumItem { | ||||||
|  |         let entry_kind = spotify::get_entry_kind(message); | ||||||
|  | 
 | ||||||
|  |         let album_info = match entry_kind { | ||||||
|  |             Some(entry) => match entry { | ||||||
|  |                 ContentKind::Album(id) => self.spotify.get_album(id.as_str()).await, | ||||||
|  |                 _ => None, | ||||||
|  |             }, | ||||||
|  |             None => None, | ||||||
|  |         }; | ||||||
|  | 
 | ||||||
|  |         AlbumItem { | ||||||
|  |             spotify_track: album_info, | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | pub(crate) fn get_spotify_kind(spotify_id: &str) -> Option<ContentKind> { | ||||||
|  |     get_entry_kind(spotify_id) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| #[cfg(test)] | #[cfg(test)] | ||||||
| @ -6,7 +6,7 @@ use std::any::Any; | |||||||
| use std::sync::Arc; | use std::sync::Arc; | ||||||
| use std::time::Duration; | use std::time::Duration; | ||||||
| 
 | 
 | ||||||
| #[derive(Debug, Clone)] | #[derive(Debug, Clone, PartialEq, Eq, Hash)] | ||||||
| pub enum ContentKind { | pub enum ContentKind { | ||||||
|     Track(String), |     Track(String), | ||||||
|     Album(String), |     Album(String), | ||||||
| @ -15,20 +15,20 @@ pub enum ContentKind { | |||||||
|     Episode(String), |     Episode(String), | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| #[derive(Debug, Clone)] | #[derive(Debug, Clone, PartialEq, Eq, Hash)] | ||||||
| pub enum PlayableKind { | pub enum PlayableKind { | ||||||
|     Track(TrackInfo), |     Track(TrackInfo), | ||||||
|     Podcast(EpisodeInfo), |     Podcast(EpisodeInfo), | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| #[derive(Debug, Clone)] | #[derive(Debug, Clone, PartialEq, Eq, Hash)] | ||||||
| pub struct TrackInfo { | pub struct TrackInfo { | ||||||
|     pub(crate) name: String, |     pub(crate) name: String, | ||||||
|     pub(crate) artists: Vec<String>, |     pub(crate) artists: Vec<String>, | ||||||
|     pub(crate) duration: Duration, |     pub(crate) duration: Duration, | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| #[derive(Debug, Clone)] | #[derive(Debug, Clone, PartialEq, Eq, Hash)] | ||||||
| pub struct EpisodeInfo { | pub struct EpisodeInfo { | ||||||
|     pub(crate) name: String, |     pub(crate) name: String, | ||||||
|     pub(crate) show: String, |     pub(crate) show: String, | ||||||
| @ -38,7 +38,7 @@ pub struct EpisodeInfo { | |||||||
|     pub(crate) release_date: String, |     pub(crate) release_date: String, | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| #[derive(Debug, Clone)] | #[derive(Debug, Clone, PartialEq, Eq, Hash)] | ||||||
| pub struct AlbumInfo { | pub struct AlbumInfo { | ||||||
|     pub(crate) name: String, |     pub(crate) name: String, | ||||||
|     pub(crate) artists: Vec<String>, |     pub(crate) artists: Vec<String>, | ||||||
| @ -46,7 +46,7 @@ pub struct AlbumInfo { | |||||||
|     pub(crate) tracks: Vec<TrackInfo>, |     pub(crate) tracks: Vec<TrackInfo>, | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| #[derive(Debug, Clone)] | #[derive(Debug, Clone, PartialEq, Eq, Hash)] | ||||||
| pub struct PlaylistInfo { | pub struct PlaylistInfo { | ||||||
|     pub(crate) name: String, |     pub(crate) name: String, | ||||||
|     pub(crate) artists: Vec<String>, |     pub(crate) artists: Vec<String>, | ||||||
| @ -1,26 +1,27 @@ | |||||||
| use std::error::Error; | use std::error::Error; | ||||||
| use std::sync::Arc; | use std::sync::Arc; | ||||||
| 
 | 
 | ||||||
|  | #[derive(Debug, Clone, PartialEq, Eq, Hash)] | ||||||
| pub(crate) struct VideoSearch { | pub(crate) struct VideoSearch { | ||||||
|     pub(crate) items: Vec<Video>, |     pub(crate) items: Vec<Video>, | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| #[derive(Debug, Clone)] | #[derive(Debug, Clone, PartialEq, Eq, Hash)] | ||||||
| pub(crate) struct Video { | pub(crate) struct Video { | ||||||
|     title: String, |     pub(crate) title: String, | ||||||
|     video_id: String, |     pub(crate) video_id: String, | ||||||
|     author: String, |     pub(crate) author: String, | ||||||
|     author_id: String, |     pub(crate) author_id: String, | ||||||
|     author_url: String, |     pub(crate) author_url: String, | ||||||
|     length_seconds: u64, |     pub(crate) length_seconds: u64, | ||||||
|     description: String, |     pub(crate) description: String, | ||||||
|     description_html: String, |     pub(crate) description_html: String, | ||||||
|     view_count: u64, |     pub(crate) view_count: u64, | ||||||
|     published: u64, |     pub(crate) published: u64, | ||||||
|     published_text: String, |     pub(crate) published_text: String, | ||||||
|     live_now: bool, |     pub(crate) live_now: bool, | ||||||
|     paid: bool, |     pub(crate) paid: bool, | ||||||
|     premium: bool, |     pub(crate) premium: bool, | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| type SearchSortBy = str; | type SearchSortBy = str; | ||||||
							
								
								
									
										132
									
								
								src/tgformatter/mod.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										132
									
								
								src/tgformatter/mod.rs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,132 @@ | |||||||
|  | use crate::search::{AlbumItem, TrackItem}; | ||||||
|  | use crate::TrackInfo; | ||||||
|  | use log::{info, log}; | ||||||
|  | use std::borrow::Borrow; | ||||||
|  | use std::collections::HashSet; | ||||||
|  | use std::fmt::format; | ||||||
|  | use std::time::Duration; | ||||||
|  | use teloxide::repl; | ||||||
|  | 
 | ||||||
|  | pub mod utils; | ||||||
|  | 
 | ||||||
|  | static MAX_ARTISTS_CHARS: usize = 140; | ||||||
|  | 
 | ||||||
|  | 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 boxed_info = Box::new(track_info); | ||||||
|  |     if boxed_info.spotify_track.is_none() && boxed_info.youtube_track.is_none() { | ||||||
|  |         return None; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     let mut reply = format!( | ||||||
|  |         "Track information:\n\ | ||||||
|  |                                 🎵 Track name: {}\n\ | ||||||
|  |                                 🧑🎤 Artist(s): {}\n\ | ||||||
|  |                                 ⏳ Duration: {}",
 | ||||||
|  |         get_track_name(boxed_info.clone()), | ||||||
|  |         get_track_artists(boxed_info.clone()), | ||||||
|  |         get_track_duration(boxed_info.clone()) | ||||||
|  |     ); | ||||||
|  | 
 | ||||||
|  |     let possible_first_video_link = boxed_info | ||||||
|  |         .youtube_track | ||||||
|  |         .and_then(|x| x.get(0).map(|x| format!("https://youtu.be/{}", x.video_id))); | ||||||
|  | 
 | ||||||
|  |     if possible_first_video_link.is_some() { | ||||||
|  |         reply.push_str( | ||||||
|  |             format!("\n\n🔗Youtube link: {}", possible_first_video_link.unwrap()).as_str(), | ||||||
|  |         ); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     Some(reply) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | pub(crate) fn format_album_message(album_info: AlbumItem) -> Option<String> { | ||||||
|  |     let boxed_info = Box::new(album_info); | ||||||
|  |     if boxed_info.spotify_track.is_none() { | ||||||
|  |         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 mut reply = format!( | ||||||
|  |         "Album information:\n\ | ||||||
|  |                                     🎵 Album name: {}\n\ | ||||||
|  |                                     🧑🎤 {} artist(s): {}",
 | ||||||
|  |         get_album_name(boxed_info.clone()), | ||||||
|  |         artists.len(), | ||||||
|  |         artists_names | ||||||
|  |     ); | ||||||
|  | 
 | ||||||
|  |     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()); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     return Some(reply); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | fn get_album_name(ai: Box<AlbumItem>) -> String { | ||||||
|  |     ai.spotify_track | ||||||
|  |         .map(|s| s.name) | ||||||
|  |         .unwrap_or("Unknown album name".to_string()) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | fn get_album_genres(ai: Box<AlbumItem>) -> HashSet<String> { | ||||||
|  |     if ai.spotify_track.is_some() { | ||||||
|  |         return ai.spotify_track.unwrap().genres.into_iter().collect(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     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 | ||||||
|  |     } else { | ||||||
|  |         ti.youtube_track | ||||||
|  |             .and_then(|youtube_tracks| youtube_tracks.get(0).map(|t| t.title.clone())) | ||||||
|  |             .unwrap_or("Unknown title".to_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_track_duration(ti: Box<TrackItem>) -> String { | ||||||
|  |     if ti.spotify_track.is_some() { | ||||||
|  |         utils::human_readable_duration(ti.spotify_track.unwrap().duration) | ||||||
|  |     } else { | ||||||
|  |         ti.youtube_track | ||||||
|  |             .map(|youtube_tracks| { | ||||||
|  |                 let seconds_in_string = | ||||||
|  |                     youtube_tracks.get(0).map(|t| t.length_seconds).unwrap_or(0); | ||||||
|  | 
 | ||||||
|  |                 utils::human_readable_duration(Duration::from_secs(seconds_in_string)) | ||||||
|  |             }) | ||||||
|  |             .unwrap_or("Unknown duration".to_string()) | ||||||
|  |     } | ||||||
|  | } | ||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user