Port spotify part of titlebot
This commit is contained in:
parent
5d6b67baa8
commit
0d6c5ba9cb
|
@ -14,7 +14,7 @@ fn separate_to_space(str: &str, prefix_len: usize) -> (&str, Option<&str>) {
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
pub trait Trigger {
|
pub trait Trigger {
|
||||||
async fn execute<'a>(&mut self, msg: Message<'a>, matches: Captures<'a>) -> anyhow::Result<String>;
|
async fn execute<'a>(&mut self, msg: Message<'a>, captures: Captures<'a>) -> anyhow::Result<String>;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
|
|
|
@ -8,11 +8,12 @@ pub struct Eval {
|
||||||
last_eval: HashMap<String, f64>
|
last_eval: HashMap<String, f64>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
impl Command for Eval {
|
impl Command for Eval {
|
||||||
//noinspection RsNeedlessLifetimes
|
//noinspection RsNeedlessLifetimes
|
||||||
async fn execute<'a>(&mut self, msg: Message<'a>) -> anyhow::Result<String> {
|
async fn execute<'a>(&mut self, msg: Message<'a>) -> anyhow::Result<String> {
|
||||||
if let Some(expr) = msg.content {
|
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 mut meval_ctx = Context::new();
|
||||||
let value = meval::eval_str_with_context(expr, meval_ctx.var("x", *last_eval))?;
|
let value = meval::eval_str_with_context(expr, meval_ctx.var("x", *last_eval))?;
|
||||||
*last_eval = value;
|
*last_eval = value;
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
|
#[cfg(feature = "debug")]
|
||||||
|
pub mod debug;
|
||||||
|
|
||||||
pub mod help;
|
pub mod help;
|
||||||
pub mod leek;
|
pub mod leek;
|
||||||
pub mod waifu;
|
pub mod waifu;
|
||||||
pub mod sed;
|
pub mod sed;
|
||||||
pub mod eval;
|
pub mod eval;
|
||||||
#[cfg(feature = "debug")]
|
pub mod spotify;
|
||||||
pub mod debug;
|
|
|
@ -6,9 +6,9 @@ pub struct Sed;
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl Trigger for Sed {
|
impl Trigger for Sed {
|
||||||
async fn execute<'a>(&mut self, msg: Message<'a>, matches: Captures<'a>) -> anyhow::Result<String> {
|
async fn execute<'a>(&mut self, msg: Message<'a>, captures: Captures<'a>) -> anyhow::Result<String> {
|
||||||
let foreign_author;
|
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;
|
foreign_author = true;
|
||||||
author
|
author
|
||||||
} else {
|
} else {
|
||||||
|
@ -21,7 +21,7 @@ impl Trigger for Sed {
|
||||||
} else {
|
} else {
|
||||||
return Ok("No previous messages found.".into());
|
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
|
// TODO: karx plz add flags
|
||||||
//let flags = matches.name("f").map(|m| m.as_str());
|
//let flags = matches.name("f").map(|m| m.as_str());
|
||||||
let result = message.replace(find.as_str(), replace.as_str());
|
let result = message.replace(find.as_str(), replace.as_str());
|
||||||
|
|
104
src/commands/spotify.rs
Normal file
104
src/commands/spotify.rs
Normal file
|
@ -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<Self> {
|
||||||
|
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<String> {
|
||||||
|
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<String> {
|
||||||
|
// 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<String> = 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(|| "<empty>".into())))
|
||||||
|
}
|
||||||
|
_ => Ok("\x037[Spotify]\x03 Error: Invalid resource type".into()),
|
||||||
|
}
|
||||||
|
}
|
|
@ -3,14 +3,14 @@ use serde::Deserialize;
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
pub struct UberConfig {
|
pub struct UberConfig {
|
||||||
pub irc: IrcConfig,
|
pub irc: IrcConfig,
|
||||||
pub spotify: SpotifyConfig, // TODO: make optional
|
pub spotify: Option<SpotifyConfig>,
|
||||||
pub db_path: Option<String>,
|
pub db_path: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
pub struct SpotifyConfig {
|
pub struct SpotifyConfig {
|
||||||
pub spotify_client_id: String,
|
pub client_id: String,
|
||||||
pub spotify_client_secret: String,
|
pub client_secret: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
|
|
21
src/main.rs
21
src/main.rs
|
@ -1,5 +1,3 @@
|
||||||
#![allow(clippy::match_wildcard_for_single_variants)]
|
|
||||||
|
|
||||||
use std::env;
|
use std::env;
|
||||||
use std::fs::File;
|
use std::fs::File;
|
||||||
use std::io::Read;
|
use std::io::Read;
|
||||||
|
@ -13,13 +11,16 @@ use futures_util::stream::StreamExt;
|
||||||
use irc::client::prelude::Config;
|
use irc::client::prelude::Config;
|
||||||
use irc::client::{Client, ClientStream};
|
use irc::client::{Client, ClientStream};
|
||||||
use irc::proto::{ChannelExt, Command, Prefix};
|
use irc::proto::{ChannelExt, Command, Prefix};
|
||||||
|
use rspotify::Credentials;
|
||||||
use tokio::select;
|
use tokio::select;
|
||||||
use tokio::sync::broadcast;
|
use tokio::sync::broadcast;
|
||||||
use tokio::sync::mpsc::unbounded_channel;
|
use tokio::sync::mpsc::unbounded_channel;
|
||||||
use tracing_subscriber::EnvFilter;
|
use tracing_subscriber::EnvFilter;
|
||||||
|
use crate::commands::eval::Eval;
|
||||||
use crate::commands::help::Help;
|
use crate::commands::help::Help;
|
||||||
use crate::commands::leek::Owo;
|
use crate::commands::leek::Owo;
|
||||||
use crate::commands::sed::Sed;
|
use crate::commands::sed::Sed;
|
||||||
|
use crate::commands::spotify::Spotify;
|
||||||
|
|
||||||
use crate::config::UberConfig;
|
use crate::config::UberConfig;
|
||||||
use crate::database::{DbExecutor, ExecutorConnection};
|
use crate::database::{DbExecutor, ExecutorConnection};
|
||||||
|
@ -69,10 +70,7 @@ async fn main() -> anyhow::Result<()> {
|
||||||
let cfg: UberConfig = toml::from_str(&client_conf)?;
|
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 (db_exec, db_conn) = DbExecutor::create(cfg.db_path.as_deref().unwrap_or("uberbot.db3"))?;
|
||||||
let exec_thread = thread::spawn(move || {
|
let exec_thread = thread::spawn(move || db_exec.run());
|
||||||
db_exec.run();
|
|
||||||
tracing::info!("Database executor has been shut down");
|
|
||||||
});
|
|
||||||
|
|
||||||
let uber_ver = concat!("Überbot ", env!("CARGO_PKG_VERSION"));
|
let uber_ver = concat!("Überbot ", env!("CARGO_PKG_VERSION"));
|
||||||
let irc_config = Config {
|
let irc_config = Config {
|
||||||
|
@ -104,6 +102,7 @@ async fn main() -> anyhow::Result<()> {
|
||||||
bot.add_command("help".into(), Help);
|
bot.add_command("help".into(), Help);
|
||||||
bot.add_command("waifu".into(), Waifu);
|
bot.add_command("waifu".into(), Waifu);
|
||||||
bot.add_command("owo".into(), Owo);
|
bot.add_command("owo".into(), Owo);
|
||||||
|
bot.add_command("ev".into(), Eval::default());
|
||||||
bot.add_trigger(Regex::new(r"^(?:(?<u>\S+):\s+)?s/(?<r>[^/]*)/(?<w>[^/]*)(?:/(?<f>[a-z]*))?\s*")?, Sed);
|
bot.add_trigger(Regex::new(r"^(?:(?<u>\S+):\s+)?s/(?<r>[^/]*)/(?<w>[^/]*)(?:/(?<f>[a-z]*))?\s*")?, Sed);
|
||||||
#[cfg(feature = "debug")]
|
#[cfg(feature = "debug")]
|
||||||
{
|
{
|
||||||
|
@ -111,6 +110,14 @@ async fn main() -> anyhow::Result<()> {
|
||||||
bot.add_command("lastmsg".into(), LastMsg);
|
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 {
|
let state = AppState {
|
||||||
client: client.clone(),
|
client: client.clone(),
|
||||||
stream,
|
stream,
|
||||||
|
@ -145,7 +152,7 @@ async fn main() -> anyhow::Result<()> {
|
||||||
exec_thread
|
exec_thread
|
||||||
.join()
|
.join()
|
||||||
.unwrap_or_else(|e| tracing::warn!("Couldn't join the database: {:?}", e));
|
.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!");
|
tracing::info!("Shutdown complete!");
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
Loading…
Reference in a new issue