From 0d6c5ba9cb12d0120fd742cfdf9786b6e1a96904 Mon Sep 17 00:00:00 2001 From: lemonsh Date: Sun, 17 Jul 2022 00:05:21 +0200 Subject: [PATCH] Port spotify part of titlebot --- src/bot.rs | 2 +- src/commands/eval.rs | 3 +- src/commands/mod.rs | 6 ++- src/commands/sed.rs | 6 +-- src/commands/spotify.rs | 104 ++++++++++++++++++++++++++++++++++++++++ src/config.rs | 6 +-- src/main.rs | 21 +++++--- 7 files changed, 131 insertions(+), 17 deletions(-) create mode 100644 src/commands/spotify.rs diff --git a/src/bot.rs b/src/bot.rs index ec91358..f052cac 100644 --- a/src/bot.rs +++ b/src/bot.rs @@ -14,7 +14,7 @@ fn separate_to_space(str: &str, prefix_len: usize) -> (&str, Option<&str>) { #[async_trait] pub trait Trigger { - async fn execute<'a>(&mut self, msg: Message<'a>, matches: Captures<'a>) -> anyhow::Result; + async fn execute<'a>(&mut self, msg: Message<'a>, captures: Captures<'a>) -> anyhow::Result; } #[async_trait] diff --git a/src/commands/eval.rs b/src/commands/eval.rs index aaf8372..e170420 100644 --- a/src/commands/eval.rs +++ b/src/commands/eval.rs @@ -8,11 +8,12 @@ pub struct Eval { last_eval: HashMap } +#[async_trait] impl Command for Eval { //noinspection RsNeedlessLifetimes async fn execute<'a>(&mut self, msg: Message<'a>) -> anyhow::Result { if let Some(expr) = msg.content { - let last_eval = self.last_eval.entry(author).or_insert(0.0); + let last_eval = self.last_eval.entry(msg.author.into()).or_insert(0.0); let mut meval_ctx = Context::new(); let value = meval::eval_str_with_context(expr, meval_ctx.var("x", *last_eval))?; *last_eval = value; diff --git a/src/commands/mod.rs b/src/commands/mod.rs index 2b53c91..24e8b2f 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -1,7 +1,9 @@ +#[cfg(feature = "debug")] +pub mod debug; + pub mod help; pub mod leek; pub mod waifu; pub mod sed; pub mod eval; -#[cfg(feature = "debug")] -pub mod debug; \ No newline at end of file +pub mod spotify; \ No newline at end of file diff --git a/src/commands/sed.rs b/src/commands/sed.rs index 9db6798..d5e60ee 100644 --- a/src/commands/sed.rs +++ b/src/commands/sed.rs @@ -6,9 +6,9 @@ pub struct Sed; #[async_trait] impl Trigger for Sed { - async fn execute<'a>(&mut self, msg: Message<'a>, matches: Captures<'a>) -> anyhow::Result { + async fn execute<'a>(&mut self, msg: Message<'a>, captures: Captures<'a>) -> anyhow::Result { let foreign_author; - let author = if let Some(author) = matches.name("u").map(|m| m.as_str()) { + let author = if let Some(author) = captures.name("u").map(|m| m.as_str()) { foreign_author = true; author } else { @@ -21,7 +21,7 @@ impl Trigger for Sed { } else { return Ok("No previous messages found.".into()); }; - if let (Some(find), Some(replace)) = (matches.name("r"), matches.name("w")) { + if let (Some(find), Some(replace)) = (captures.name("r"), captures.name("w")) { // TODO: karx plz add flags //let flags = matches.name("f").map(|m| m.as_str()); let result = message.replace(find.as_str(), replace.as_str()); diff --git a/src/commands/spotify.rs b/src/commands/spotify.rs new file mode 100644 index 0000000..967d311 --- /dev/null +++ b/src/commands/spotify.rs @@ -0,0 +1,104 @@ +use rspotify::{ClientCredsSpotify, Credentials}; +use async_trait::async_trait; +use fancy_regex::Captures; +use rspotify::clients::BaseClient; +use rspotify::model::{Id, PlayableItem}; +use crate::bot::{Message, Trigger}; + +pub struct Spotify { + spotify: ClientCredsSpotify, +} + +impl Spotify { + pub async fn new(creds: Credentials) -> anyhow::Result { + let mut spotify = ClientCredsSpotify::new(creds); + spotify.request_token().await?; + Ok(Self { + spotify + }) + } +} + +#[async_trait] +impl Trigger for Spotify { + async fn execute<'a>(&mut self, msg: Message<'a>, captures: Captures<'a>) -> anyhow::Result { + let tp_group = captures.get(1).unwrap(); + let id_group = captures.get(2).unwrap(); + resolve_spotify( + &mut self.spotify, + &msg.content.unwrap()[tp_group.start()..tp_group.end()], + &msg.content.unwrap()[id_group.start()..id_group.end()], + ).await + } +} + +fn calculate_playtime(secs: u64) -> (u64, u64) { + let mut dur_sec = secs; + let dur_min = dur_sec / 60; + dur_sec -= dur_min * 60; + (dur_min, dur_sec) +} + +async fn resolve_spotify( + spotify: &mut ClientCredsSpotify, + resource_type: &str, + resource_id: &str, +) -> anyhow::Result { + // uncomment this if titlebot commits suicide after exactly 30 minutes + + /*if spotify.token.lock().await.unwrap().as_ref().unwrap().is_expired() { + spotify.request_token().await?; + }*/ + tracing::debug!( + "Resolving Spotify resource '{}' with id '{}'", + resource_type, + resource_id + ); + match resource_type { + "track" => { + let track = spotify.track(&Id::from_id(resource_id)?).await?; + let playtime = calculate_playtime(track.duration.as_secs()); + let artists: Vec = track.artists.into_iter().map(|x| x.name).collect(); + Ok(format!("\x037[Spotify]\x03 Track: \x039\"{}\"\x03 - \x039\"{}\" \x0311|\x03 Album: \x039\"{}\" \x0311|\x03 Length:\x0315 {}:{:02} \x0311|", artists.join(", "), track.name, track.album.name, playtime.0, playtime.1)) + } + "artist" => { + let artist = spotify.artist(&Id::from_id(resource_id)?).await?; + Ok(format!( + "\x037[Spotify]\x03 Artist: \x039\"{}\" \x0311|\x03 Genres:\x039 {} \x0311|", + artist.name, + artist.genres.join(", ") + )) + } + "album" => { + let album = spotify.album(&Id::from_id(resource_id)?).await?; + let playtime = calculate_playtime( + album + .tracks + .items + .iter() + .fold(0, |acc, x| acc + x.duration.as_secs()), + ); + Ok(format!("\x037[Spotify]\x03 Album: \x039\"{}\" \x0311|\x03 Tracks:\x0315 {} \x0311|\x03 Release date:\x039 {} \x0311|\x03 Length:\x0315 {}:{:02} \x0311|", album.name, album.tracks.total, album.release_date, playtime.0, playtime.1)) + } + "playlist" => { + let playlist = spotify + .playlist(&Id::from_id(resource_id)?, None, None) + .await?; + let mut tracks = 0; + let playtime = calculate_playtime(playlist.tracks.items.iter().fold(0, |acc, x| { + x.track.as_ref().map_or(acc, |item| match item { + PlayableItem::Track(t) => { + tracks += 1; + acc + t.duration.as_secs() + } + PlayableItem::Episode(e) => { + tracks += 1; + acc + e.duration.as_secs() + } + }) + })); + Ok(format!("\x037[Spotify]\x03 Playlist: \x039\"{}\" \x0311|\x03 Tracks/Episodes:\x0315 {} \x0311|\x03 Length:\x0315 {}:{:02} \x0311|\x03 Description: \x039\"{}\" \x0311|", playlist.name, tracks, playtime.0, playtime.1, playlist.description.unwrap_or_else(|| "".into()))) + } + _ => Ok("\x037[Spotify]\x03 Error: Invalid resource type".into()), + } +} diff --git a/src/config.rs b/src/config.rs index ee7bbd9..f1db7d5 100644 --- a/src/config.rs +++ b/src/config.rs @@ -3,14 +3,14 @@ use serde::Deserialize; #[derive(Deserialize)] pub struct UberConfig { pub irc: IrcConfig, - pub spotify: SpotifyConfig, // TODO: make optional + pub spotify: Option, pub db_path: Option, } #[derive(Deserialize)] pub struct SpotifyConfig { - pub spotify_client_id: String, - pub spotify_client_secret: String, + pub client_id: String, + pub client_secret: String, } #[derive(Deserialize)] diff --git a/src/main.rs b/src/main.rs index b8861c4..584cb4a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,5 +1,3 @@ -#![allow(clippy::match_wildcard_for_single_variants)] - use std::env; use std::fs::File; use std::io::Read; @@ -13,13 +11,16 @@ use futures_util::stream::StreamExt; use irc::client::prelude::Config; use irc::client::{Client, ClientStream}; use irc::proto::{ChannelExt, Command, Prefix}; +use rspotify::Credentials; use tokio::select; use tokio::sync::broadcast; use tokio::sync::mpsc::unbounded_channel; use tracing_subscriber::EnvFilter; +use crate::commands::eval::Eval; use crate::commands::help::Help; use crate::commands::leek::Owo; use crate::commands::sed::Sed; +use crate::commands::spotify::Spotify; use crate::config::UberConfig; use crate::database::{DbExecutor, ExecutorConnection}; @@ -69,10 +70,7 @@ async fn main() -> anyhow::Result<()> { let cfg: UberConfig = toml::from_str(&client_conf)?; let (db_exec, db_conn) = DbExecutor::create(cfg.db_path.as_deref().unwrap_or("uberbot.db3"))?; - let exec_thread = thread::spawn(move || { - db_exec.run(); - tracing::info!("Database executor has been shut down"); - }); + let exec_thread = thread::spawn(move || db_exec.run()); let uber_ver = concat!("Überbot ", env!("CARGO_PKG_VERSION")); let irc_config = Config { @@ -104,6 +102,7 @@ async fn main() -> anyhow::Result<()> { bot.add_command("help".into(), Help); bot.add_command("waifu".into(), Waifu); bot.add_command("owo".into(), Owo); + bot.add_command("ev".into(), Eval::default()); bot.add_trigger(Regex::new(r"^(?:(?\S+):\s+)?s/(?[^/]*)/(?[^/]*)(?:/(?[a-z]*))?\s*")?, Sed); #[cfg(feature = "debug")] { @@ -111,6 +110,14 @@ async fn main() -> anyhow::Result<()> { bot.add_command("lastmsg".into(), LastMsg); } + if let Some(spotcfg) = cfg.spotify { + let creds = Credentials::new(&spotcfg.client_id, &spotcfg.client_secret); + let spotify = Spotify::new(creds).await?; + bot.add_trigger(Regex::new(r"(?:https?|spotify):(?://open\.spotify\.com/)?(track|artist|album|playlist)[/:]([a-zA-Z0-9]*)")?, spotify); + } else { + tracing::warn!("Spotify module is disabled, because the config is missing") + } + let state = AppState { client: client.clone(), stream, @@ -145,7 +152,7 @@ async fn main() -> anyhow::Result<()> { exec_thread .join() .unwrap_or_else(|e| tracing::warn!("Couldn't join the database: {:?}", e)); - tracing::info!("Executor thread finished"); + tracing::info!("DB Executor thread finished"); tracing::info!("Shutdown complete!"); Ok(())