diff --git a/Cargo.lock b/Cargo.lock index a3ee3d1..1858265 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -713,6 +713,18 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "enum_dispatch" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa18ce2bc66555b3218614519ac839ddb759a7d6720732f979ef8d13be147ecd" +dependencies = [ + "once_cell", + "proc-macro2", + "quote", + "syn 2.0.101", +] + [[package]] name = "equivalent" version = "1.0.2" @@ -979,8 +991,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" dependencies = [ "cfg-if", + "js-sys", "libc", "wasi 0.11.0+wasi-snapshot-preview1", + "wasm-bindgen", ] [[package]] @@ -1766,6 +1780,17 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "maybe-async" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cf92c10c7e361d6b99666ec1c6f9805b0bea2c3bd8c78dc6fe98ac5bd78db11" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.101", +] + [[package]] name = "maybe-rayon" version = "0.1.1" @@ -2574,6 +2599,7 @@ dependencies = [ "system-configuration 0.6.1", "tokio", "tokio-native-tls", + "tokio-socks", "tower", "tower-service", "url", @@ -2638,6 +2664,64 @@ dependencies = [ "zeroize", ] +[[package]] +name = "rspotify" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77beedc33ecff4c39e8ef0e6f7ebc8d849f3ffebbeb786f9997d96f0d9cf4017" +dependencies = [ + "async-stream", + "async-trait", + "base64 0.22.1", + "chrono", + "futures", + "getrandom 0.2.16", + "log", + "maybe-async", + "rspotify-http", + "rspotify-macros", + "rspotify-model", + "serde", + "serde_json", + "sha2", + "thiserror 2.0.12", + "url", +] + +[[package]] +name = "rspotify-http" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fde1ea9e2a49698cffbc994a83f5f909b37736c31cccb202f9577e8a32df3a63" +dependencies = [ + "async-trait", + "log", + "maybe-async", + "reqwest 0.12.15", + "serde_json", + "thiserror 2.0.12", +] + +[[package]] +name = "rspotify-macros" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee3dfb51ee54bd754ad76e96ad60a3b64bc70ae33a89261d9dbabc4c148a496f" + +[[package]] +name = "rspotify-model" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "018f29a6a8c47cfe7923c48140ed546a395f660c7af05b73e6001d4505f89c8d" +dependencies = [ + "chrono", + "enum_dispatch", + "serde", + "serde_json", + "strum", + "thiserror 2.0.12", +] + [[package]] name = "rustc-demangle" version = "0.1.24" @@ -3347,6 +3431,27 @@ dependencies = [ "unicode-properties", ] +[[package]] +name = "strum" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.101", +] + [[package]] name = "subtle" version = "2.6.1" @@ -3684,6 +3789,18 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio-socks" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d4770b8024672c1101b3f6733eab95b18007dbe0847a8afe341fcf79e06043f" +dependencies = [ + "either", + "futures-util", + "thiserror 1.0.69", + "tokio", +] + [[package]] name = "tokio-stream" version = "0.1.17" @@ -4153,7 +4270,7 @@ dependencies = [ [[package]] name = "watcat" -version = "0.2.0" +version = "0.3.0" dependencies = [ "arabic_reshaper", "dotenvy", @@ -4165,6 +4282,7 @@ dependencies = [ "palette", "reqwest 0.12.15", "reqwest-middleware", + "rspotify", "serenity", "sqlx", "tokio", diff --git a/Cargo.toml b/Cargo.toml index 3e8ad50..c1dfa14 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "watcat" -version = "0.3.0" +version = "0.4.0" edition = "2024" [dependencies] @@ -11,6 +11,7 @@ reqwest = "0.12" reqwest-middleware = "0.4.2" http-cache-reqwest = "0.15.1" palette = "0.7.6" +rspotify = "0.15.0" # text rendering dependencies arabic_reshaper = "0.4.2" unicode-bidi = "0.3.18" diff --git a/README.md b/README.md index 022c756..4cfa302 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,9 @@ TODO + ~~add art and lastfm url fetching~~ + add help command -+ refactor to combine repeated parts of art, url, and fmi. also use macro for commands, and use Option instead of empty string sentinel value -+ add spotify link fetching ++ ~~refactor to combine repeated parts of art, url, and fmi.~~ ++ use macro for commands ++ use Option instead of empty string sentinel value ++ ~~add spotify link fetching~~ + make various parameters user-configurable + use tiny-skia instead of magick-rust? diff --git a/src/main.rs b/src/main.rs index a425fa2..6de8a63 100644 --- a/src/main.rs +++ b/src/main.rs @@ -14,7 +14,8 @@ extern crate magick_rust; use magick_rust::{ColorspaceType, CompositeOperator, MagickWand, PixelWand, magick_wand_genesis, FilterType}; use std::env; -use std::sync::Once; +use std::sync::{Once, Arc}; +use tokio::sync::Mutex; use palette::color_difference::ImprovedCiede2000; use palette::lab::Lab; @@ -23,6 +24,9 @@ use palette::white_point::D65; extern crate sqlx; use sqlx::sqlite::SqlitePool; +use rspotify::{ClientCredsSpotify, Credentials, clients::BaseClient,}; +use rspotify::model::{enums::types::SearchType, search::SearchResult}; + use imagetext::fontdb::FontDB; use imagetext::superfont::SuperFont; @@ -114,23 +118,31 @@ fn validate_color(col: &PixelWand) -> Option { Some(Lab::::from_components(color_raw)) } -async fn art(ctx: &Context, arg: &str, id: UserId) -> Reply { +async fn get_track(ctx: &Context, arg: &str, id: UserId) -> Result { let lastfm_user = match arg { "" => get_lastfm_username(ctx, id).await, _ => Some(arg.to_string()), }; let lastfm_client = match lastfm_user { Some(s) => lastfm::Client::::from_env(s), - None => return Reply::Text("No last.fm username set.".to_string()), + None => return Err(Reply::Text("No last.fm username set.".to_string())), }; let now_playing = match lastfm_client.now_playing().await { Ok(np) => np, - Err(e) => return Reply::Text(format!("Error: grabbing last.fm user data failed {e}")), + Err(e) => return Err(Reply::Text(format!("Error: grabbing last.fm user data failed {e}"))), }; let track = match now_playing { Some(track) => track, - None => return Reply::Text("Nothing playing.".to_string()), + None => return Err(Reply::Text("Nothing playing.".to_string())), }; + Ok(track) +} + +async fn art(ctx: &Context, arg: &str, id: UserId) -> Reply { + let track = match get_track(ctx, arg, id).await { + Ok(track) => track, + Err(e) => return e, + }; let track_art = match track.image.extralarge { Some(track_art) => track_art, None => return Reply::Text("Error: getting image uri failed".to_string()), @@ -139,42 +151,44 @@ async fn art(ctx: &Context, arg: &str, id: UserId) -> Reply { } async fn url(ctx: &Context, arg: &str, id: UserId) -> Reply { - let lastfm_user = match arg { - "" => get_lastfm_username(ctx, id).await, - _ => Some(arg.to_string()), - }; - let lastfm_client = match lastfm_user { - Some(s) => lastfm::Client::::from_env(s), - None => return Reply::Text("No last.fm username set.".to_string()), - }; - let now_playing = match lastfm_client.now_playing().await { - Ok(np) => np, - Err(e) => return Reply::Text(format!("Error: grabbing last.fm user data failed {e}")), - }; - let track = match now_playing { - Some(track) => track, - None => return Reply::Text("Nothing playing.".to_string()), - }; + let track = match get_track(ctx, arg, id).await { + Ok(track) => track, + Err(e) => return e, + }; Reply::Text(track.url) } +async fn spot(ctx: &Context, arg: &str, id: UserId) -> Reply { + let track = match get_track(ctx, arg, id).await { + Ok(track) => track, + Err(e) => return e, + }; + let mut data = ctx.data.write().await; + let spotify_option = data.get_mut::().expect("Failed to have spotify option"); + if spotify_option.is_none() { + return Reply::Text("Unable to use Spotify command: Contact bot Administrator.".to_owned()) + } + let spotify_arc_mutex = spotify_option.as_mut().unwrap(); + //let spotify_arc_mutex_ref = Arc::clone(&spotify_arc_mutex); + let spotify_client = spotify_arc_mutex.lock().await; + spotify_client.request_token().await.unwrap(); + let search = spotify_client.search(format!("{} {}", track.name, track.artist.name).as_str(), SearchType::Track, None, None, None, None).await; + let tracks = match search { + Ok(SearchResult::Tracks(track_page)) => track_page, + Err(e) => return Reply::Text(format!("Failed to get track {e}")), + _ => return Reply::Text("Spotify search failed".to_owned()), + }; + match &tracks.items[0].external_urls.get("spotify") { + Some(url) => Reply::Text(url.to_owned().to_owned()), + None => Reply::Text("Unable to get spotify url".to_owned()), + } +} + async fn fmi(ctx: &Context, arg: &str, id: UserId, avatar: Option) -> Reply { - let lastfm_user = match arg { - "" => get_lastfm_username(ctx, id).await, - _ => Some(arg.to_string()), - }; - let lastfm_client = match lastfm_user { - Some(s) => lastfm::Client::::from_env(s), - None => return Reply::Text("No last.fm username set.".to_string()), - }; - let now_playing = match lastfm_client.now_playing().await { - Ok(np) => np, - Err(e) => return Reply::Text(format!("Error: grabbing last.fm user data failed {e}")), - }; - let track = match now_playing { - Some(track) => track, - None => return Reply::Text("Nothing playing.".to_string()), - }; + let track = match get_track(ctx, arg, id).await { + Ok(track) => track, + Err(e) => return e, + }; let track_info = text::TrackInfo { title: track.name, artist: track.artist.name, @@ -256,7 +270,6 @@ async fn fmi(ctx: &Context, arg: &str, id: UserId, avatar: Option) -> Re "Failed to set accent color" ); } - let use_dark_color = match validate_color(&colors[0]) { Some(color) => { @@ -382,6 +395,12 @@ async fn set(ctx: &Context, arg: &str, id: UserId) -> Reply { Reply::Text(format!("set user {arg}")) } +//macro_rules! cmd { +// (c:lit, abbr:lit, a:ident) => { +// (".k", $c, $a) | (".k", $a, $c) | (concat!(".k", $abbr), $ident, "") +// } +//} + #[async_trait] impl EventHandler for Handler { async fn message(&self, ctx: Context, msg: Message) { @@ -402,6 +421,10 @@ impl EventHandler for Handler { log!("{} received", msg.content); Some(url(&ctx, arg, msg.author.id).await) }, + (".k", "spot" | "spotify", arg) | (".k", arg, "spot" | "spotify") | (".ks" | ".kspot", arg, "") => { + log!("{} received", msg.content); + Some(spot(&ctx, arg, msg.author.id).await) + }, (".fmi" | ".k" | ".kf" | ".ki" | ".kfmi", arg, "") | (".k", "fm" | "fmi" | "get" | "img", arg) => { log!("{} received", msg.content); Some(fmi(&ctx, arg, msg.author.id, msg.author.avatar_url()).await) @@ -440,6 +463,11 @@ impl TypeMapKey for FontsHaver { type Value = (SuperFont<'static>, SuperFont<'static>); } +struct SpotifyHaver; +impl TypeMapKey for SpotifyHaver { + type Value = Option>>; +} + struct DBResponse { lastfm_username: String, } @@ -563,6 +591,9 @@ async fn main() { Unifont ").expect("Failed to load bold fonts"); + let spotify_creds = Credentials::from_env(); + let spotify = spotify_creds.map(ClientCredsSpotify::new).map(Mutex::new).map(Arc::new); + let mut discord_client = Client::builder(&token, intents) .event_handler(Handler) .await @@ -573,6 +604,7 @@ async fn main() { data.insert::(pool); data.insert::(http); data.insert::((regular_fonts, bold_fonts)); + data.insert::(spotify); } if let Err(why) = discord_client.start().await {