From 3b0121699f58a803936ea78112f672b0da842da0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Czy=C5=BC?= Date: Mon, 12 Feb 2024 22:27:14 +0100 Subject: [PATCH] code cleanup and play command polish --- src/commands.rs | 3 + src/commands/kashi.rs | 3 + src/commands/kashi/mod.rs | 1 - src/commands/music.rs | 24 ++++++ src/commands/music/mod.rs | 12 --- src/commands/music/play.rs | 138 ++++++++++++++++++++++++++++----- src/commands/tools.rs | 5 ++ src/commands/tools/mod.rs | 1 - src/commands/tools/register.rs | 12 +++ 9 files changed, 166 insertions(+), 33 deletions(-) create mode 100644 src/commands.rs create mode 100644 src/commands/kashi.rs delete mode 100644 src/commands/kashi/mod.rs create mode 100644 src/commands/music.rs delete mode 100644 src/commands/music/mod.rs create mode 100644 src/commands/tools.rs delete mode 100644 src/commands/tools/mod.rs create mode 100644 src/commands/tools/register.rs diff --git a/src/commands.rs b/src/commands.rs new file mode 100644 index 0000000..08f5ddf --- /dev/null +++ b/src/commands.rs @@ -0,0 +1,3 @@ +pub mod kashi; +pub mod music; +pub mod tools; diff --git a/src/commands/kashi.rs b/src/commands/kashi.rs new file mode 100644 index 0000000..a4affc8 --- /dev/null +++ b/src/commands/kashi.rs @@ -0,0 +1,3 @@ +pub mod kashi; + +pub use kashi::kashi; diff --git a/src/commands/kashi/mod.rs b/src/commands/kashi/mod.rs deleted file mode 100644 index b52d95f..0000000 --- a/src/commands/kashi/mod.rs +++ /dev/null @@ -1 +0,0 @@ -pub mod kashi; \ No newline at end of file diff --git a/src/commands/music.rs b/src/commands/music.rs new file mode 100644 index 0000000..cbb6a02 --- /dev/null +++ b/src/commands/music.rs @@ -0,0 +1,24 @@ +pub mod deafen; +pub mod join; +pub mod leave; +pub mod misc; +pub mod mute; +pub mod pause; +pub mod play; +pub mod queue; +pub mod repeat; +pub mod resume; +pub mod skip; +pub mod stop; + +pub use deafen::deafen; +pub use join::join; +pub use leave::leave; +pub use mute::mute; +pub use pause::pause; +pub use play::play; +pub use queue::queue; +pub use repeat::repeat; +pub use resume::resume; +pub use skip::skip; +pub use stop::stop; diff --git a/src/commands/music/mod.rs b/src/commands/music/mod.rs deleted file mode 100644 index f983108..0000000 --- a/src/commands/music/mod.rs +++ /dev/null @@ -1,12 +0,0 @@ -pub mod deafen; -pub mod join; -pub mod leave; -pub mod misc; -pub mod mute; -pub mod play; -pub mod queue; -pub mod skip; -pub mod stop; -pub mod repeat; -pub mod pause; -pub mod resume; diff --git a/src/commands/music/play.rs b/src/commands/music/play.rs index 2e456ec..397dff8 100644 --- a/src/commands/music/play.rs +++ b/src/commands/music/play.rs @@ -1,14 +1,38 @@ use crate::{Context, Error}; +use fancy_regex::Regex; +use regex::Regex as Regex_Classic; +use std::process::Command; +use std::time::Duration; +use poise::CreateReply; +use poise::serenity_prelude::CreateEmbed; +use poise::serenity_prelude::Colour; +use poise::serenity_prelude::model::Timestamp; +use serenity::builder::CreateEmbedAuthor; +use serenity::builder::CreateEmbedFooter; +use songbird::input::AuxMetadata; use songbird::input::{Compose, YoutubeDl}; use songbird::events::TrackEvent; use crate::commands::music::misc::TrackErrorNotifier; use crate::http::HttpKey; -#[poise::command(prefix_command, slash_command)] -pub async fn play(ctx: Context<'_>, url: String) -> Result<(), Error> { - let is_search = !url.starts_with("http"); +#[poise::command( + prefix_command, + slash_command, + aliases("p", "enqueue") +)] +pub async fn play( + ctx: Context<'_>, + #[description = "Provide a query or an url"] #[rest] mut song: String, +) -> Result<(), Error> { + let regex_spotify = Regex::new(r"https?:\/\/(?:embed\.|open\.)(?:spotify\.com\/)(?:track\/|\?uri=spotify:track:)((\w|-)+)(?:(?=\?)(?:[?&]foo=(\d*)(?=[&#]|$)|(?![?&]foo=)[^#])+)?(?=#|$)").unwrap(); + let regex_youtube = Regex_Classic::new(r#""url": "(https://www.youtube.com/watch\?v=[A-Za-z0-9]{11})""#).unwrap(); + let regex_youtube_playlist = Regex::new(r"^((?:https?:)\/\/)?((?:www|m)\.)?((?:youtube\.com)).*(youtu.be\/|list=)([^#&?]*).*").unwrap(); + + let is_playlist = regex_youtube_playlist.is_match(&song).unwrap(); + let is_spotify = regex_spotify.is_match(&song).unwrap(); + let is_query = !song.starts_with("http"); let guild_id = ctx.guild_id().unwrap(); let channel_id = ctx.guild().unwrap().voice_states.get(&ctx.author().id).and_then(|voice_state| voice_state.channel_id); @@ -17,7 +41,7 @@ pub async fn play(ctx: Context<'_>, url: String) -> Result<(), Error> { Some(channel) => channel, None => { ctx.say("Not in a voice channel").await?; - + return Ok(()); } }; @@ -36,25 +60,101 @@ pub async fn play(ctx: Context<'_>, url: String) -> Result<(), Error> { if let Ok(handler_lock) = manager.join(guild_id, connect_to).await { let mut handler = handler_lock.lock().await; - - // if let Err(err) = handler.deafen(true).await {println!("Failed to deafen: {:?}", err)}; + handler.add_global_event(TrackEvent::Error.into(), TrackErrorNotifier); - let mut src = if is_search { - println!("ytsearch:{}", url); - YoutubeDl::new_ytdl_like("yt-dlp", http_client, format!("ytsearch:{}", url)) - } else { - YoutubeDl::new_ytdl_like("yt-dlp", http_client, url) - }; - - let _ = handler.enqueue_input(src.clone().into()).await; - - let _metadata = src.aux_metadata().await.unwrap(); + if is_playlist { + let raw_list = Command::new("yt-dlp") + .args(["-j", "--flat-playlist", &song]) + .output() + .expect("failed to execute process") + .stdout; - // ctx.say(format!("Playing song: {}", metadata.title.unwrap())).await?; - } else { - ctx.say("Not in a voice channel to play in").await?; + let list = String::from_utf8(raw_list.clone()).expect("Invalid UTF-8"); + + let urls: Vec = regex_youtube.captures_iter(&list).map(|capture| capture[1].to_string()).collect(); + + let mut sources: Vec = vec![]; + + for url in urls { + let src = YoutubeDl::new_ytdl_like("yt-dlp", http_client.clone(), url); + let _ = handler.enqueue_input(src.clone().into()).await; + sources.push(src); + } + + let embed = generate_playlist_embed(ctx, sources).await; + let response = CreateReply::default().embed(embed.unwrap()); + ctx.send(response).await?; + } else { + if is_spotify { + let exec = format!("node ./src/spotify --url {}", song); + let query = Command::new("sh").arg("-c").arg(exec).output().expect("failed to execute process").stdout; + let query_str = String::from_utf8(query.clone()).expect("Invalid UTF-8"); + song = format!("ytsearch:{}", query_str.to_string()); + + } + + if is_query { + song = format!("ytsearch:{}", song); + } + + let src = YoutubeDl::new_ytdl_like("yt-dlp", http_client, song); + let _ = handler.enqueue_input(src.clone().into()).await; + + let embed = generate_embed(ctx, src).await; + let response = CreateReply::default().embed(embed.unwrap()); + ctx.send(response).await?; + } } Ok(()) } + +async fn generate_embed(ctx: Context<'_>, src: YoutubeDl) -> Result { + let metadata = src.clone().aux_metadata().await.unwrap(); + let AuxMetadata {title, thumbnail, source_url, artist, duration, ..} = metadata; + let timestamp = Timestamp::now(); + let duration_minutes = duration.unwrap_or(Duration::new(0, 0)).clone().as_secs() / 60; + let duration_seconds = duration.unwrap_or(Duration::new(0, 0)).clone().as_secs() % 60; + + let embed = CreateEmbed::default() + .author(CreateEmbedAuthor::new("Track enqueued").icon_url(ctx.author().clone().face())) + .colour(Colour::from_rgb(255, 58, 97)) + .title(title.unwrap()) + .url(source_url.unwrap()) + .thumbnail(thumbnail.unwrap_or(ctx.cache().current_user().face())) + .field("Artist", artist.unwrap_or("Unknown Artist".to_string()), true) + .field("Duration", format!("{:02}:{:02}", duration_minutes, duration_seconds), true) + .field("DJ", ctx.author().name.clone(), true) + .timestamp(timestamp) + .footer(CreateEmbedFooter::new(ctx.cache().current_user().name.to_string()).icon_url(ctx.cache().current_user().face())); + + Ok(embed) +} + +async fn generate_playlist_embed(ctx: Context<'_>, sources: Vec) -> Result { + let src = sources.get(0).unwrap(); + + let metadata = src.clone().aux_metadata().await.unwrap(); + let AuxMetadata {title, thumbnail, source_url, artist, duration, ..} = metadata; + let timestamp = Timestamp::now(); + let duration_minutes = duration.unwrap_or(Duration::new(0, 0)).clone().as_secs() / 60; + let duration_seconds = duration.unwrap_or(Duration::new(0, 0)).clone().as_secs() % 60; + + let description = format!("Enqueued tracks: {}", sources.len() - 1); + + let embed = CreateEmbed::default() + .author(CreateEmbedAuthor::new("Playlist enqueued").icon_url(ctx.author().clone().face())) + .colour(Colour::from_rgb(255, 58, 97)) + .title(title.unwrap()) + .url(source_url.unwrap()) + .thumbnail(thumbnail.unwrap_or(ctx.cache().current_user().face())) + .field("Artist", artist.unwrap_or("Unknown Artist".to_string()), true) + .field("Duration", format!("{:02}:{:02}", duration_minutes, duration_seconds), true) + .field("DJ", ctx.author().name.clone(), true) + .description(description) + .timestamp(timestamp) + .footer(CreateEmbedFooter::new(ctx.cache().current_user().name.to_string()).icon_url(ctx.cache().current_user().face())); + + Ok(embed) +} diff --git a/src/commands/tools.rs b/src/commands/tools.rs new file mode 100644 index 0000000..349b23b --- /dev/null +++ b/src/commands/tools.rs @@ -0,0 +1,5 @@ +pub mod ping; +pub mod register; + +pub use ping::ping; +pub use register::register; diff --git a/src/commands/tools/mod.rs b/src/commands/tools/mod.rs deleted file mode 100644 index 924dabf..0000000 --- a/src/commands/tools/mod.rs +++ /dev/null @@ -1 +0,0 @@ -pub mod ping; \ No newline at end of file diff --git a/src/commands/tools/register.rs b/src/commands/tools/register.rs new file mode 100644 index 0000000..a1edd76 --- /dev/null +++ b/src/commands/tools/register.rs @@ -0,0 +1,12 @@ +use crate::{Context, Error}; + +#[poise::command(prefix_command, check = "check")] +pub async fn register(ctx: Context<'_>) -> Result<(), Error> { + poise::builtins::register_application_commands_buttons(ctx).await?; + Ok(()) +} + +async fn check(ctx: Context<'_>) -> Result { + let owner = std::env::var("OWNER_ID").expect("Environment variable `OWNER_ID` not found"); + Ok(ctx.author().id.to_string() == owner) +}