From ff86b4c26dddb310be5825b1191005e57b759df4 Mon Sep 17 00:00:00 2001 From: Davide Polonio Date: Thu, 23 Jun 2022 18:33:16 +0200 Subject: [PATCH] feat: add youtube search for tracks. Still WIP * missing tests, docs and other stuff * missing playlist porting and other content too (maybe) --- Cargo.toml | 1 + src/main.rs | 139 +++++++++-------------------- src/{engine.rs => search/mod.rs} | 71 +++++++++++---- src/{ => search}/spotify/mod.rs | 12 +-- src/{ => search}/youtube/mod.rs | 31 +++---- src/tgformatter/mod.rs | 132 +++++++++++++++++++++++++++ src/{ => tgformatter}/utils/mod.rs | 0 7 files changed, 249 insertions(+), 137 deletions(-) rename src/{engine.rs => search/mod.rs} (56%) rename src/{ => search}/spotify/mod.rs (96%) rename src/{ => search}/youtube/mod.rs (86%) create mode 100644 src/tgformatter/mod.rs rename src/{ => tgformatter}/utils/mod.rs (100%) diff --git a/Cargo.toml b/Cargo.toml index f7e7809..a6a988c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,6 +14,7 @@ rspotify = { version = "0.11.5", features = ["default"]} sentry = "0.26.0" invidious = "0.2.1" chrono = "0.4.19" +itertools = "0.10.3" [dev-dependencies] tokio-test = "0.4.2" diff --git a/src/main.rs b/src/main.rs index 4932ca4..8037b05 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,20 +1,20 @@ -use log::LevelFilter; +use log::{info, log, LevelFilter}; +use search::spotify; use sentry::ClientInitGuard; use std::env; +use std::sync::Arc; use teloxide::prelude::*; -use crate::spotify::{PlayableKind, TrackInfo}; -use spotify::ContentKind::Track; +use search::spotify::ContentKind::Track; +use search::spotify::{PlayableKind, TrackInfo}; -use crate::spotify::ContentKind::{Album, Episode, Playlist, Podcast}; -use crate::utils::{human_readable_duration, truncate_with_dots}; +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 engine; -mod spotify; -mod utils; -mod youtube; - -static MAX_ARTISTS_CHARS: usize = 140; +mod search; +mod tgformatter; #[tokio::main] async fn main() { @@ -48,94 +48,39 @@ async fn main() { let bot = Bot::from_env().auto_send(); teloxide::repl(bot, |message: Message, bot: AutoSend| async move { - let text = message.text().and_then(spotify::get_entry_kind); - match text { - Some(spotify) => { - let spotify_client = spotify::Client::new().await; - match spotify { - Track(id) => { - log::debug!("Parsing spotify song: {}", id); - let track_info = spotify_client.get_track(&id).await; - match track_info { - 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) => { - log::debug!("Parsing spotify album: {}", id); - let album_info = spotify_client.get_album(&id).await; - match album_info { - 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) - .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 - } + let music_engine = search::Engine::new().await; + let opt_text_message = message.text(); + if opt_text_message.is_none() { + return respond(()); + } + 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) => { + info!("Processing song with spotify id: {}", id); + let track_item = music_engine.get_song_from_spotify_id(text_message).await; + tgformatter::format_track_message(track_item) } - } - None => None, + Album(id) => { + info!("Processing album with spotify id: {}", id); + let album_item = music_engine.get_album_by_spotify_id(text_message).await; + tgformatter::format_album_message(album_item) + } + _ => 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; diff --git a/src/engine.rs b/src/search/mod.rs similarity index 56% rename from src/engine.rs rename to src/search/mod.rs index 9348114..09beaf1 100644 --- a/src/engine.rs +++ b/src/search/mod.rs @@ -1,15 +1,25 @@ -use crate::spotify::ContentKind; -use crate::youtube::Video; -use crate::{spotify, youtube, TrackInfo}; +use crate::spotify::{get_entry_kind, AlbumInfo}; +use crate::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 { - spotify_track: Option, - youtube_track: Option>, + pub(crate) spotify_track: Option, + pub(crate) youtube_track: Option>, +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub(crate) struct AlbumItem { + pub(crate) spotify_track: Option, } // 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 { Spotify(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 // tracks, albums and playlists #[derive(Debug, Clone)] -pub(crate) struct MusicEngine { +pub(crate) struct Engine { spotify: spotify::Client, youtube: youtube::Client, } -impl MusicEngine { +impl Engine { pub(crate) async fn new() -> Self { - MusicEngine { + Engine { spotify: spotify::Client::new().await, youtube: youtube::Client::new().await, } @@ -36,18 +46,18 @@ impl MusicEngine { spotify_client: spotify::Client, youtube_client: youtube::Client, ) -> Self { - MusicEngine { + Engine { spotify: spotify_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") } - pub(crate) async fn search_song_by_id(&self, id: &str) -> Option { - let entry_kind = spotify::get_entry_kind(id); + pub(crate) async fn get_song_from_spotify_id(&self, message: &str) -> TrackItem { + let entry_kind = spotify::get_entry_kind(message); let track_info = match entry_kind { Some(entry) => match entry { ContentKind::Track(id) => self.spotify.get_track(id.as_str()).await, @@ -62,10 +72,10 @@ impl MusicEngine { .youtube .search_video( format!( - "{} {}", + "{}{}", ti.artists .get(0) - .map(|artist| format!("{} -", artist)) + .map(|artist| format!("{} - ", artist)) .unwrap_or("".to_string()), ti.name ) @@ -78,13 +88,36 @@ impl MusicEngine { Ok(search) => Some(search), }; - return Some(TrackItem { + return TrackItem { spotify_track: Some(ti), 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 { + get_entry_kind(spotify_id) } #[cfg(test)] diff --git a/src/spotify/mod.rs b/src/search/spotify/mod.rs similarity index 96% rename from src/spotify/mod.rs rename to src/search/spotify/mod.rs index 9d04991..e54436c 100644 --- a/src/spotify/mod.rs +++ b/src/search/spotify/mod.rs @@ -6,7 +6,7 @@ use std::any::Any; use std::sync::Arc; use std::time::Duration; -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq, Eq, Hash)] pub enum ContentKind { Track(String), Album(String), @@ -15,20 +15,20 @@ pub enum ContentKind { Episode(String), } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq, Eq, Hash)] pub enum PlayableKind { Track(TrackInfo), Podcast(EpisodeInfo), } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct TrackInfo { pub(crate) name: String, pub(crate) artists: Vec, pub(crate) duration: Duration, } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct EpisodeInfo { pub(crate) name: String, pub(crate) show: String, @@ -38,7 +38,7 @@ pub struct EpisodeInfo { pub(crate) release_date: String, } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct AlbumInfo { pub(crate) name: String, pub(crate) artists: Vec, @@ -46,7 +46,7 @@ pub struct AlbumInfo { pub(crate) tracks: Vec, } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct PlaylistInfo { pub(crate) name: String, pub(crate) artists: Vec, diff --git a/src/youtube/mod.rs b/src/search/youtube/mod.rs similarity index 86% rename from src/youtube/mod.rs rename to src/search/youtube/mod.rs index 3d2d98d..2e9ad68 100644 --- a/src/youtube/mod.rs +++ b/src/search/youtube/mod.rs @@ -1,26 +1,27 @@ use std::error::Error; use std::sync::Arc; +#[derive(Debug, Clone, PartialEq, Eq, Hash)] pub(crate) struct VideoSearch { pub(crate) items: Vec