Add music link translator

This commit is contained in:
Joey Hines 2025-12-07 21:40:25 -07:00
parent 68711f9ee6
commit 0639effde1
Signed by: joeyahines
GPG Key ID: 38BA6F25C94C9382
8 changed files with 166 additions and 16 deletions

29
Cargo.lock generated
View File

@ -640,6 +640,15 @@ dependencies = [
"unicode-segmentation",
]
[[package]]
name = "convert_case"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "633458d4ef8c78b72454de2d54fd6ab2e60f9e02be22f3c6104cdc8a4e0fceb9"
dependencies = [
"unicode-segmentation",
]
[[package]]
name = "core-foundation"
version = "0.9.4"
@ -1084,22 +1093,23 @@ checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
[[package]]
name = "form_urlencoded"
version = "1.2.1"
version = "1.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456"
checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf"
dependencies = [
"percent-encoding",
]
[[package]]
name = "fren"
version = "2.6.1"
version = "2.7.0"
dependencies = [
"axum 0.8.1",
"base64 0.22.1",
"chrono",
"chrono-tz 0.10.1",
"config",
"convert_case 0.10.0",
"cta-api",
"emojis",
"j_db",
@ -1127,6 +1137,7 @@ dependencies = [
"tonic",
"tracing-core",
"tracing-subscriber",
"url",
]
[[package]]
@ -1830,9 +1841,9 @@ checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
[[package]]
name = "idna"
version = "1.0.3"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e"
checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de"
dependencies = [
"idna_adapter",
"smallvec",
@ -2437,9 +2448,9 @@ dependencies = [
[[package]]
name = "percent-encoding"
version = "2.3.1"
version = "2.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
[[package]]
name = "pest"
@ -4928,9 +4939,9 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
[[package]]
name = "url"
version = "2.5.4"
version = "2.5.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60"
checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b"
dependencies = [
"form_urlencoded",
"idna",

View File

@ -1,6 +1,6 @@
[package]
name = "fren"
version = "2.6.1"
version = "2.7.0"
edition = "2024"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
@ -35,6 +35,8 @@ thiserror = "2.0.12"
tracing-core = "0.1.33"
strum = { version = "0.27.2", features = ["derive"] }
thousands = "0.2.0"
url = { version = "2.5.7", features = ["serde"] }
convert_case = "0.10.0"
[dependencies.tokio]
version = "1.35.1"

View File

@ -48,6 +48,8 @@ pub struct BotConfig {
pub raas_server: String,
pub cta_key: String,
pub omdb_key: String,
pub music_streaming_site_urls: Vec<String>,
pub music_streaming_sites: Vec<String>,
pub picox: PicOxConfig,
}

View File

@ -75,11 +75,7 @@ pub async fn buy_improvement(
return Ok(());
}
User::try_take_funds(
&ctx.data().db,
ctx.author().id,
amount,
)?;
User::try_take_funds(&ctx.data().db, ctx.author().id, amount)?;
let (has_improved, _) =
Improvements::contribute_to_improvement(&ctx.data().db, improvement_type, amount as u64)?;

View File

@ -1,4 +1,5 @@
use rand::seq::IteratorRandom;
use std::str::FromStr;
mod admin;
mod album;
mod birthday;
@ -24,14 +25,19 @@ use crate::error::Error;
use crate::event_listener::{Listener, TriggerEvent, TriggerType};
use crate::models::social_credit::SocialCreditPhrase;
use crate::models::task::Task;
use crate::music::songlink::get_song_urls;
use crate::user::{User, UserRole};
use log::{debug, error, info};
use poise::serenity_prelude::{GuildId, Http, Message, MessageBuilder, ReactionType, RoleId};
use poise::serenity_prelude::{
CreateMessage, GuildId, Http, Message, MessageBuilder, ReactionType, RoleId,
};
use poise::{FrameworkOptions, find_command, serenity_prelude as serenity};
use rand::{Rng, rng};
use regex::Regex;
use songbird::SerenityInit;
use std::sync::Arc;
use std::time::Duration;
use url::Url;
pub type Context<'a> = poise::Context<'a, Arc<GlobalData>, Error>;
@ -75,6 +81,42 @@ async fn event_handler(
Ok(())
}
async fn check_message_for_music_link(
ctx: &serenity::Context,
data: &Arc<GlobalData>,
new_message: &Message,
) -> Result<(), Error> {
let re = Regex::new(r"(?:https?://.)?(?:www\.)?[-a-zA-Z0-9@%._+~#=]{2,256}\.[a-z]{2,6}\b[-a-zA-Z0-9@:%_+.~#?&/=]*").unwrap();
if let Some(url_match) = re.find(&new_message.content) {
let url = url_match.as_str();
if data
.cfg
.music_streaming_site_urls
.iter()
.any(|site| url.contains(site))
{
match get_song_urls(Url::from_str(url).unwrap()).await {
Ok(resp) => {
new_message
.channel_id
.send_message(
&ctx.http,
CreateMessage::default()
.embed(resp.to_discord_message(&data.cfg.music_streaming_sites)),
)
.await?;
}
Err(err) => {
error!("Failed to handle music link: {}", err);
}
}
}
}
Ok(())
}
async fn handle_message(
ctx: &serenity::Context,
data: &Arc<GlobalData>,
@ -151,6 +193,8 @@ async fn handle_message(
}
}
check_message_for_music_link(ctx, data, new_message).await?;
Ok(())
}

View File

@ -7,6 +7,7 @@ mod image_manipulation;
mod inventory;
mod migrations;
mod models;
mod music;
mod user;
use crate::config::{Args, BotConfig, GlobalData};

1
src/music/mod.rs Normal file
View File

@ -0,0 +1 @@
pub mod songlink;

93
src/music/songlink.rs Normal file
View File

@ -0,0 +1,93 @@
use convert_case::{Case, Casing};
use poise::serenity_prelude::{Color, CreateEmbed, MessageBuilder};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use url::Url;
const API_URL: &str = "https://api.song.link/v1-alpha.1/links";
#[derive(Serialize, Debug)]
pub struct SonglinkRequest {
url: Url,
#[serde(rename = "songIfSingle")]
song_if_single: bool,
}
#[derive(Deserialize, Hash, Debug)]
pub struct Platform {
url: String,
#[serde(rename = "nativeAppUriMobile", default)]
native_app_uri_mobile: Option<String>,
#[serde(rename = "nativeAppUriDesktop", default)]
native_app_uri_desktop: Option<String>,
}
#[derive(Deserialize, Hash, Debug)]
pub struct Entity {
title: String,
#[serde(rename = "artistName", default)]
artist_name: String,
#[serde(rename = "thumbnailUrl", default)]
thumbnail_url: String,
}
#[allow(dead_code)]
#[derive(Deserialize, Debug)]
pub struct SonglinkResponse {
#[serde(rename = "entityUniqueId")]
pub entity_unique_id: String,
#[serde(rename = "userCountry")]
pub user_country: String,
#[serde(rename = "linksByPlatform")]
pub links_by_platform: HashMap<String, Platform>,
#[serde(rename = "entitiesByUniqueId")]
pub entities_by_unique_id: HashMap<String, Entity>,
}
impl SonglinkResponse {
pub fn to_discord_message(&self, platforms: &[String]) -> CreateEmbed {
let embed = if let Some((_id, entity)) = self.entities_by_unique_id.iter().next() {
CreateEmbed::new()
.title(entity.title.clone())
.field("Artist:", entity.artist_name.clone(), true)
.thumbnail(entity.thumbnail_url.clone())
.color(Color::from_rgb(0xf2, 0x55, 0x43))
} else {
CreateEmbed::new()
.title("Alternative Links")
.color(Color::from_rgb(0xf2, 0x55, 0x43))
};
let mut msg_builder = MessageBuilder::new();
for (platform, link) in &self.links_by_platform {
let platform_search_name = platform.to_lowercase();
if platforms.contains(&platform_search_name) {
msg_builder.push_line(format!(
"* [{}]({})",
platform.to_case(Case::Title),
link.url
));
}
}
embed.field("Available At", msg_builder.build(), false)
}
}
pub async fn get_song_urls(url: Url) -> Result<SonglinkResponse, reqwest::Error> {
let request = SonglinkRequest {
url,
song_if_single: true,
};
let client = reqwest::Client::new();
let resp = client
.execute(client.get(API_URL).query(&request).build()?)
.await?;
let resp: SonglinkResponse = resp.json().await?;
Ok(resp)
}