From 73aefca205143b65deed31378b9c346b9c4eab6c Mon Sep 17 00:00:00 2001 From: Jontze <42588836+jontze@users.noreply.github.com> Date: Thu, 30 Nov 2023 14:08:41 +0100 Subject: [PATCH 1/8] fix(yt-dlprc): Update yt-dlp version to latest --- .yt-dlprc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.yt-dlprc b/.yt-dlprc index 92d83c0..c002223 100644 --- a/.yt-dlprc +++ b/.yt-dlprc @@ -1 +1 @@ -2023.07.06 +2023.11.16 From 985e0e8ae6e80d910b70061a0272dc0b40a9e68d Mon Sep 17 00:00:00 2001 From: Jontze <42588836+jontze@users.noreply.github.com> Date: Thu, 30 Nov 2023 14:09:07 +0100 Subject: [PATCH 2/8] chore(docker): Remove ffmpeg dependency This is not needed anymore after the latest songbird v4.0.0 release. --- Dockerfile | 16 ++-------------- 1 file changed, 2 insertions(+), 14 deletions(-) diff --git a/Dockerfile b/Dockerfile index 6943609..0c7b3fc 100644 --- a/Dockerfile +++ b/Dockerfile @@ -28,28 +28,16 @@ ENV CARGO_TERM_COLOR=always # Build and cache only the cadency app with the previously builded dependencies RUN cargo build --release --bin cadency +# Downloads yt-dlp FROM bitnami/minideb:bullseye as packages -# Downloads both ffmpeg and yt-dlp WORKDIR /packages COPY --from=builder /cadency/.yt-dlprc . -# tar: (x) extract, (J) from .xz, (f) a file. (--wildcards */bin/ffmpeg) any path with /bin/ffmpeg, (--transform) remove all previous paths -# FFMPEG is staticly compiled, so platform specific -# If statement: converts architecture from docker to a correct link. Default is amd64 = desktop 64 bit -ARG TARGETARCH -RUN if [ "$TARGETARCH" = "arm64" ]; then \ - export LINK="https://github.com/yt-dlp/FFmpeg-Builds/releases/download/latest/ffmpeg-master-latest-linuxarm64-gpl.tar.xz"; \ - else \ - export LINK="https://github.com/yt-dlp/FFmpeg-Builds/releases/download/latest/ffmpeg-master-latest-linux64-gpl.tar.xz"; \ - fi && \ - apt-get update && apt-get install -y curl tar xz-utils && \ - curl -L $LINK > ffmpeg.tar.xz && \ - tar -xJf ffmpeg.tar.xz --wildcards */bin/ffmpeg --transform='s/^.*\///' && rm ffmpeg.tar.xz RUN YTDLP_VERSION=$(cat .yt-dlprc) && \ curl -L https://github.com/yt-dlp/yt-dlp/releases/download/$YTDLP_VERSION/yt-dlp_linux > yt-dlp && chmod +x yt-dlp -FROM bitnami/minideb:bullseye as python-builder # Based on: https://github.com/zarmory/docker-python-minimal/blob/master/Dockerfile # Removes Python build and developmenttools like pip. +FROM bitnami/minideb:bullseye as python-builder RUN apt-get update && apt-get install -y python3-minimal binutils && \ rm -rf /usr/local/lib/python*/ensurepip && \ rm -rf /usr/local/lib/python*/idlelib && \ From 4116aa92d66956583aa2d20e542553f7db48ec55 Mon Sep 17 00:00:00 2001 From: Jontze <42588836+jontze@users.noreply.github.com> Date: Thu, 30 Nov 2023 14:09:49 +0100 Subject: [PATCH 3/8] chore(deps): Update songbird to v0.4.0 and serentiy to v0.12.0 This brings major changes to audio processiond and the public serentiy API. Therefore the app is refactored to adjust to the new API. --- Cargo.toml | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index b7048f8..e017452 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,6 +7,7 @@ members = [ "cadency", "examples/*" ] +resolver = "2" [workspace.dependencies] env_logger = "0.10.0" @@ -16,13 +17,13 @@ serde_json = "1.0.99" derive_builder = "0.12.0" [workspace.dependencies.serenity] -version = "0.11.6" +version = "0.12.0" default-features = false features = ["client", "gateway", "rustls_backend", "model", "voice", "cache"] [workspace.dependencies.songbird] -version = "0.3.2" -features = ["builtin-queue", "yt-dlp"] +version = "0.4.0" +features = ["builtin-queue"] [workspace.dependencies.tokio] version = "1.29.0" @@ -35,4 +36,4 @@ features = ["derive"] [workspace.dependencies.reqwest] version = "0.11.18" default-features = false -features = ["rustls-tls"] +features = ["rustls-tls", "json"] From 7b5965f98409ecc0d76b029d1679169e66cc0395 Mon Sep 17 00:00:00 2001 From: Jontze <42588836+jontze@users.noreply.github.com> Date: Thu, 30 Nov 2023 14:13:25 +0100 Subject: [PATCH 4/8] refactor: Update code to latest songbird and serenity version --- cadency_codegen/src/derive.rs | 2 +- cadency_commands/src/fib.rs | 6 +- cadency_commands/src/inspire.rs | 7 +- cadency_commands/src/lib.rs | 12 +-- cadency_commands/src/now.rs | 27 ++++-- cadency_commands/src/pause.rs | 7 +- cadency_commands/src/ping.rs | 7 +- cadency_commands/src/play.rs | 51 ++++------ cadency_commands/src/resume.rs | 7 +- cadency_commands/src/skip.rs | 7 +- cadency_commands/src/slap.rs | 26 ++--- cadency_commands/src/stop.rs | 7 +- cadency_commands/src/track_loop.rs | 31 ++++-- cadency_commands/src/tracks.rs | 50 ++++++---- cadency_commands/src/urban.rs | 44 ++++----- cadency_core/src/client.rs | 10 +- cadency_core/src/command.rs | 56 +++++------ cadency_core/src/handler/command.rs | 14 +-- cadency_core/src/http.rs | 14 +++ cadency_core/src/lib.rs | 1 + cadency_core/src/response.rs | 59 +++++++----- cadency_core/src/utils/mod.rs | 19 +--- cadency_core/src/utils/voice.rs | 94 ++++++++++++------- .../examples/custom_commands.rs | 13 ++- 24 files changed, 296 insertions(+), 275 deletions(-) create mode 100644 cadency_core/src/http.rs diff --git a/cadency_codegen/src/derive.rs b/cadency_codegen/src/derive.rs index 95bb0d4..19a2292 100644 --- a/cadency_codegen/src/derive.rs +++ b/cadency_codegen/src/derive.rs @@ -186,7 +186,7 @@ pub(crate) fn impl_command_baseline(derive_input: DeriveInput) -> TokenStream { let deferred = command.deferred; quote! { use cadency_core::{CadencyCommandBaseline as __CadencyCommandBaseline, CadencyCommandOption as __CadencyCommandOption}; - use serenity::model::application::command::CommandOptionType as __CommandOptionType; + use serenity::model::application::CommandOptionType as __CommandOptionType; impl __CadencyCommandBaseline for #struct_name { fn name(&self) -> String { String::from(#command_name) diff --git a/cadency_commands/src/fib.rs b/cadency_commands/src/fib.rs index 083ab61..751e10c 100644 --- a/cadency_commands/src/fib.rs +++ b/cadency_commands/src/fib.rs @@ -5,9 +5,7 @@ use cadency_core::{ use serenity::{ async_trait, client::Context, - model::application::interaction::application_command::{ - ApplicationCommandInteraction, CommandDataOptionValue, - }, + model::application::{CommandDataOptionValue, CommandInteraction}, }; #[derive(CommandBaseline, Default)] @@ -34,7 +32,7 @@ impl CadencyCommand for Fib { async fn execute<'a>( &self, _ctx: &Context, - command: &'a mut ApplicationCommandInteraction, + command: &'a mut CommandInteraction, response_builder: &'a mut ResponseBuilder, ) -> Result { let number = utils::get_option_value_at_position(command.data.options.as_ref(), 0) diff --git a/cadency_commands/src/inspire.rs b/cadency_commands/src/inspire.rs index a7cf345..7bffcaa 100644 --- a/cadency_commands/src/inspire.rs +++ b/cadency_commands/src/inspire.rs @@ -2,10 +2,7 @@ use cadency_core::{ response::{Response, ResponseBuilder}, CadencyCommand, CadencyError, }; -use serenity::{ - async_trait, client::Context, - model::application::interaction::application_command::ApplicationCommandInteraction, -}; +use serenity::{async_trait, client::Context, model::application::CommandInteraction}; #[derive(CommandBaseline, Default)] #[description = "Say something really inspiring!"] @@ -26,7 +23,7 @@ impl CadencyCommand for Inspire { async fn execute<'a>( &self, _ctx: &Context, - _command: &'a mut ApplicationCommandInteraction, + _command: &'a mut CommandInteraction, response_builder: &'a mut ResponseBuilder, ) -> Result { let inspire_url = Self::request_inspire_image_url().await.map_err(|err| { diff --git a/cadency_commands/src/lib.rs b/cadency_commands/src/lib.rs index 1ba5d24..ee5c9d4 100644 --- a/cadency_commands/src/lib.rs +++ b/cadency_commands/src/lib.rs @@ -38,7 +38,6 @@ mod test { fn impl_commandbaseline_trait_with_macro() { #[derive(cadency_codegen::CommandBaseline)] struct Test {} - assert!(true) } #[test] @@ -97,9 +96,8 @@ mod test { #[description = "123"] struct Test {} let test = Test {}; - assert_eq!( - test.deferred(), - false, + assert!( + !test.deferred(), "Test command should not be deferred by default" ) } @@ -128,7 +126,7 @@ mod test { #[test] fn return_derived_option() { - use serenity::model::application::command::CommandOptionType; + use serenity::model::application::CommandOptionType; #[derive(cadency_codegen::CommandBaseline)] #[argument( name = "say", @@ -144,7 +142,7 @@ mod test { assert_eq!(argument.name, "say"); assert_eq!(argument.description, "Word to say"); assert_eq!(argument.kind, CommandOptionType::String); - assert_eq!(argument.required, false); + assert!(!argument.required); } #[test] @@ -166,7 +164,7 @@ mod test { #[test] fn return_multiple_options() { - use serenity::model::application::command::CommandOptionType; + use serenity::model::application::CommandOptionType; #[derive(cadency_codegen::CommandBaseline)] #[argument(name = "say", description = "Word to say", kind = "String")] diff --git a/cadency_commands/src/now.rs b/cadency_commands/src/now.rs index c1f2036..1c4a101 100644 --- a/cadency_commands/src/now.rs +++ b/cadency_commands/src/now.rs @@ -1,11 +1,9 @@ use cadency_core::{ response::{Response, ResponseBuilder}, - utils, CadencyCommand, CadencyError, -}; -use serenity::{ - async_trait, client::Context, - model::application::interaction::application_command::ApplicationCommandInteraction, + utils::{self, voice::TrackMetaKey}, + CadencyCommand, CadencyError, }; +use serenity::{async_trait, client::Context, model::application::CommandInteraction}; #[derive(CommandBaseline, Default)] #[description = "Shows current song"] @@ -16,7 +14,7 @@ impl CadencyCommand for Now { async fn execute<'a>( &self, ctx: &Context, - command: &'a mut ApplicationCommandInteraction, + command: &'a mut CommandInteraction, response_builder: &'a mut ResponseBuilder, ) -> Result { let guild_id = command.guild_id.ok_or(CadencyError::Command { @@ -30,11 +28,20 @@ impl CadencyCommand for Now { let track = handler.queue().current().ok_or(CadencyError::Command { message: ":x: **No song is playing**".to_string(), })?; - Ok(response_builder - .message(Some(track.metadata().title.as_ref().map_or( + + // Create message from track metadata. This is scoped to drop the read lock on the + // trackmeta as soon as possible. + let message = { + let track_map = track.typemap().read().await; + let metadata = track_map + .get::() + .expect("Metadata to be present in track map"); + metadata.title.as_ref().map_or( String::from(":x: **Could not add audio source to the queue!**"), |title| format!(":newspaper: `{title}`"), - ))) - .build()?) + ) + }; + + Ok(response_builder.message(Some(message)).build()?) } } diff --git a/cadency_commands/src/pause.rs b/cadency_commands/src/pause.rs index e979149..dee3e29 100644 --- a/cadency_commands/src/pause.rs +++ b/cadency_commands/src/pause.rs @@ -2,10 +2,7 @@ use cadency_core::{ response::{Response, ResponseBuilder}, utils, CadencyCommand, CadencyError, }; -use serenity::{ - async_trait, client::Context, - model::application::interaction::application_command::ApplicationCommandInteraction, -}; +use serenity::{async_trait, client::Context, model::application::CommandInteraction}; #[derive(CommandBaseline, Default)] #[description = "Pause the current song"] @@ -17,7 +14,7 @@ impl CadencyCommand for Pause { async fn execute<'a>( &self, ctx: &Context, - command: &'a mut ApplicationCommandInteraction, + command: &'a mut CommandInteraction, response_builder: &'a mut ResponseBuilder, ) -> Result { let guild_id = command.guild_id.ok_or(CadencyError::Command { diff --git a/cadency_commands/src/ping.rs b/cadency_commands/src/ping.rs index 8a4ece1..56a4353 100644 --- a/cadency_commands/src/ping.rs +++ b/cadency_commands/src/ping.rs @@ -2,10 +2,7 @@ use cadency_core::{ response::{Response, ResponseBuilder}, CadencyCommand, CadencyError, }; -use serenity::{ - async_trait, client::Context, - model::application::interaction::application_command::ApplicationCommandInteraction, -}; +use serenity::{async_trait, client::Context, model::application::CommandInteraction}; #[derive(CommandBaseline, Default)] #[description = "Play Ping-Pong"] @@ -16,7 +13,7 @@ impl CadencyCommand for Ping { async fn execute<'a>( &self, _ctx: &Context, - _command: &'a mut ApplicationCommandInteraction, + _command: &'a mut CommandInteraction, response_builder: &'a mut ResponseBuilder, ) -> Result { Ok(response_builder diff --git a/cadency_commands/src/play.rs b/cadency_commands/src/play.rs index a56f9fa..1a133c5 100644 --- a/cadency_commands/src/play.rs +++ b/cadency_commands/src/play.rs @@ -7,9 +7,7 @@ use reqwest::Url; use serenity::{ async_trait, client::Context, - model::application::interaction::application_command::{ - ApplicationCommandInteraction, CommandDataOptionValue, - }, + model::application::{CommandDataOptionValue, CommandInteraction}, }; use songbird::events::Event; @@ -42,7 +40,7 @@ impl CadencyCommand for Play { async fn execute<'a>( &self, ctx: &Context, - command: &'a mut ApplicationCommandInteraction, + command: &'a mut CommandInteraction, response_builder: &'a mut ResponseBuilder, ) -> Result { let (search_payload, is_url, is_playlist) = @@ -67,10 +65,7 @@ impl CadencyCommand for Play { message: ":x: **No search string provided**".to_string(), })?; let (manager, call, guild_id) = utils::voice::join(ctx, command).await?; - let mut is_queue_empty = { - let call_handler = call.lock().await; - call_handler.queue().is_empty() - }; + let response_builder = if is_playlist { let playlist_items = cadency_yt_playlist::fetch_playlist_songs(search_payload.clone()).unwrap(); @@ -87,19 +82,11 @@ impl CadencyCommand for Play { if amount_added_playlist_songs <= self.playlist_song_limit && song.duration <= self.song_length_limit { - match utils::voice::add_song( - call.clone(), - song.url, - true, - !is_queue_empty, // Don't add first song lazy to the queue - ) - .await - { - Ok(added_song) => { + match utils::voice::add_song(ctx, call.clone(), song.url, true).await { + Ok((added_song_meta, _)) => { amount_added_playlist_songs += 1; amount_total_added_playlist_duration += song.duration; - is_queue_empty = false; - debug!("➕ Added song '{:?}' from playlist", added_song.title); + debug!("➕ Added song '{:?}' from playlist", added_song_meta.title); } Err(err) => { error!("❌ Failed to add song: {err}"); @@ -119,19 +106,15 @@ impl CadencyCommand for Play { ":white_check_mark: **Added ___{amount_added_playlist_songs}___ songs to the queue with a duration of ___{amount_total_added_playlist_duration:.2}___ mins** \n**Playing** :notes: `{search_payload}`", ))) } else { - let added_song = utils::voice::add_song( - call.clone(), - search_payload.clone(), - is_url, - !is_queue_empty, // Don't add first song lazy to the queue - ) - .await - .map_err(|err| { - error!("❌ Failed to add song to queue: {}", err); - CadencyError::Command { - message: ":x: **Couldn't add audio source to the queue!**".to_string(), - } - })?; + let (added_song_meta, _) = + utils::voice::add_song(ctx, call.clone(), search_payload.clone(), is_url) + .await + .map_err(|err| { + error!("❌ Failed to add song to queue: {}", err); + CadencyError::Command { + message: ":x: **Couldn't add audio source to the queue!**".to_string(), + } + })?; let mut handler = call.lock().await; handler.remove_all_global_events(); handler.add_global_event( @@ -141,7 +124,7 @@ impl CadencyCommand for Play { let song_url = if is_url { search_payload } else { - added_song + added_song_meta .source_url .as_ref() .map_or("unknown url", |url| url) @@ -149,7 +132,7 @@ impl CadencyCommand for Play { response_builder.message(Some(format!( ":white_check_mark: **Added song to the queue and started playing:** \n:notes: `{}` \n:link: `{}`", song_url, - added_song + added_song_meta .title .as_ref() .map_or(":x: **Unknown title**", |title| title) diff --git a/cadency_commands/src/resume.rs b/cadency_commands/src/resume.rs index 7ffa5cb..0b3f1e2 100644 --- a/cadency_commands/src/resume.rs +++ b/cadency_commands/src/resume.rs @@ -2,10 +2,7 @@ use cadency_core::{ response::{Response, ResponseBuilder}, utils, CadencyCommand, CadencyError, }; -use serenity::{ - async_trait, client::Context, - model::application::interaction::application_command::ApplicationCommandInteraction, -}; +use serenity::{async_trait, client::Context, model::application::CommandInteraction}; #[derive(CommandBaseline, Default)] #[description = "Resume current song if paused"] @@ -17,7 +14,7 @@ impl CadencyCommand for Resume { async fn execute<'a>( &self, ctx: &Context, - command: &'a mut ApplicationCommandInteraction, + command: &'a mut CommandInteraction, response_builder: &'a mut ResponseBuilder, ) -> Result { let guild_id = command.guild_id.ok_or(CadencyError::Command { diff --git a/cadency_commands/src/skip.rs b/cadency_commands/src/skip.rs index 23f2adf..87a22a2 100644 --- a/cadency_commands/src/skip.rs +++ b/cadency_commands/src/skip.rs @@ -2,10 +2,7 @@ use cadency_core::{ response::{Response, ResponseBuilder}, utils, CadencyCommand, CadencyError, }; -use serenity::{ - async_trait, client::Context, - model::application::interaction::application_command::ApplicationCommandInteraction, -}; +use serenity::{async_trait, client::Context, model::application::CommandInteraction}; #[derive(CommandBaseline, Default)] #[description = "Skip current song"] @@ -17,7 +14,7 @@ impl CadencyCommand for Skip { async fn execute<'a>( &self, ctx: &Context, - command: &'a mut ApplicationCommandInteraction, + command: &'a mut CommandInteraction, response_builder: &'a mut ResponseBuilder, ) -> Result { let guild_id = command.guild_id.ok_or(CadencyError::Command { diff --git a/cadency_commands/src/slap.rs b/cadency_commands/src/slap.rs index 2770a85..d8881c2 100644 --- a/cadency_commands/src/slap.rs +++ b/cadency_commands/src/slap.rs @@ -3,12 +3,12 @@ use cadency_core::{ utils, CadencyCommand, CadencyError, }; use serenity::{ + all::Mentionable, async_trait, client::Context, - model::application::interaction::application_command::{ - ApplicationCommandInteraction, CommandDataOptionValue, - }, + model::application::{CommandDataOptionValue, CommandInteraction}, }; +use std::num::NonZeroU64; #[derive(CommandBaseline, Default)] #[description = "Slap someone with a large trout!"] @@ -24,13 +24,13 @@ impl CadencyCommand for Slap { async fn execute<'a>( &self, _ctx: &Context, - command: &'a mut ApplicationCommandInteraction, + command: &'a mut CommandInteraction, response_builder: &'a mut ResponseBuilder, ) -> Result { - let user = utils::get_option_value_at_position(command.data.options.as_ref(), 0) + let user_id = utils::get_option_value_at_position(command.data.options.as_ref(), 0) .and_then(|option_value| { - if let CommandDataOptionValue::User(user, _) = option_value { - Some(user) + if let CommandDataOptionValue::User(user_id) = option_value { + Some(user_id) } else { error!("Command option is not a user"); None @@ -40,20 +40,22 @@ impl CadencyCommand for Slap { message: ":x: *Invalid user provided*".to_string(), })?; - let response_builder = if user.id == command.user.id { + let response_builder = if user_id == &command.user.id { response_builder.message(Some(format!( "**Why do you want to slap yourself, {}?**", - command.user + command.user.mention() ))) - } else if user.id.0 == command.application_id.0 { + } else if NonZeroU64::from(*user_id) == NonZeroU64::from(command.application_id) { response_builder.message(Some(format!( "**Nope!\n{} slaps {} around a bit with a large trout!**", - user, command.user + user_id.mention(), + command.user.mention() ))) } else { response_builder.message(Some(format!( "**{} slaps {} around a bit with a large trout!**", - command.user, user + command.user.mention(), + user_id.mention() ))) }; Ok(response_builder.build()?) diff --git a/cadency_commands/src/stop.rs b/cadency_commands/src/stop.rs index 5fe44a8..7b0bc77 100644 --- a/cadency_commands/src/stop.rs +++ b/cadency_commands/src/stop.rs @@ -2,10 +2,7 @@ use cadency_core::{ response::{Response, ResponseBuilder}, utils, CadencyCommand, CadencyError, }; -use serenity::{ - async_trait, client::Context, - model::application::interaction::application_command::ApplicationCommandInteraction, -}; +use serenity::{async_trait, client::Context, model::application::CommandInteraction}; #[derive(CommandBaseline, Default)] #[description = "Stop music and clear the track list"] @@ -17,7 +14,7 @@ impl CadencyCommand for Stop { async fn execute<'a>( &self, ctx: &Context, - command: &'a mut ApplicationCommandInteraction, + command: &'a mut CommandInteraction, response_builder: &'a mut ResponseBuilder, ) -> Result { let guild_id = command.guild_id.ok_or(CadencyError::Command { diff --git a/cadency_commands/src/track_loop.rs b/cadency_commands/src/track_loop.rs index 4f2b01f..6cd85a1 100644 --- a/cadency_commands/src/track_loop.rs +++ b/cadency_commands/src/track_loop.rs @@ -3,11 +3,8 @@ use cadency_core::{ utils, CadencyCommand, CadencyError, }; use serenity::{ - async_trait, - client::Context, - model::application::{ - command::CommandOptionType, interaction::application_command::ApplicationCommandInteraction, - }, + all::CommandDataOptionValue, async_trait, client::Context, + model::application::CommandInteraction, }; #[derive(Default, CommandBaseline)] @@ -32,7 +29,7 @@ impl CadencyCommand for TrackLoop { async fn execute<'a>( &self, ctx: &Context, - command: &'a mut ApplicationCommandInteraction, + command: &'a mut CommandInteraction, response_builder: &'a mut ResponseBuilder, ) -> Result { // Validate if command can be executed @@ -53,14 +50,28 @@ impl CadencyCommand for TrackLoop { .data .options .iter() - .find(|option| option.kind == CommandOptionType::Integer) - .and_then(|option_amount| option_amount.value.as_ref().unwrap().as_u64()); + .find(|option| option.name == "amount") + .and_then(|option_amount| { + if let CommandDataOptionValue::Integer(amount) = option_amount.value { + Some(amount) + } else { + error!("Command option 'amount' is not a integer"); + None + } + }); let stop_argument = command .data .options .iter() - .find(|option| option.kind == CommandOptionType::Boolean) - .and_then(|option_stop| option_stop.value.as_ref().unwrap().as_bool()); + .find(|option| option.name == "stop") + .and_then(|option_stop| { + if let CommandDataOptionValue::Boolean(stop) = option_stop.value { + Some(stop) + } else { + error!("Command option 'stop' is not a boolean"); + None + } + }); // Cancel looping if the stop argument is true if let Some(stop) = stop_argument { diff --git a/cadency_commands/src/tracks.rs b/cadency_commands/src/tracks.rs index bc20f3e..b2efe67 100644 --- a/cadency_commands/src/tracks.rs +++ b/cadency_commands/src/tracks.rs @@ -1,11 +1,13 @@ use cadency_core::{ response::{Response, ResponseBuilder}, - utils, CadencyCommand, CadencyError, + utils::{self, voice::TrackMetaKey}, + CadencyCommand, CadencyError, }; use serenity::{ - async_trait, builder::CreateEmbed, client::Context, - model::application::interaction::application_command::ApplicationCommandInteraction, - utils::Color, + async_trait, + builder::CreateEmbed, + client::Context, + model::{application::CommandInteraction, Color}, }; #[derive(CommandBaseline, Default)] @@ -18,7 +20,7 @@ impl CadencyCommand for Tracks { async fn execute<'a>( &self, ctx: &Context, - command: &'a mut ApplicationCommandInteraction, + command: &'a mut CommandInteraction, response_builder: &'a mut ResponseBuilder, ) -> Result { let guild_id = command.guild_id.ok_or(CadencyError::Command { @@ -33,22 +35,30 @@ impl CadencyCommand for Tracks { response_builder.message(Some(":x: **No tracks in the queue**".to_string())) } else { let queue_snapshot = handler.queue().current_queue(); - let mut embeded_tracks = CreateEmbed::default(); - embeded_tracks.color(Color::BLURPLE); - embeded_tracks.title("Track List"); + let mut embeded_tracks = CreateEmbed::default() + .color(Color::BLURPLE) + .title("Track List"); for (index, track) in queue_snapshot.into_iter().enumerate() { - let position = index + 1; - let metadata = track.metadata(); - let title = metadata - .title - .as_ref() - .map_or("**No title provided**", |t| t); - let url = metadata - .source_url - .as_ref() - .map_or("**No url provided**", |u| u); - embeded_tracks.field( - format!("{position}. :newspaper: `{title}`"), + let track_position = index + 1; + // Extract title and url of the track. This is scoped to drop the read lock on + // the track meta as soon as possible. + let (title, url) = { + let track_map = track.typemap().read().await; + let metadata = track_map + .get::() + .expect("Metadata to be present in track map"); + let title = metadata + .title + .as_ref() + .map_or("**No title provided**", |t| t); + let url = metadata + .source_url + .as_ref() + .map_or("**No url provided**", |u| u); + (title.to_owned(), url.to_owned()) + }; + embeded_tracks = embeded_tracks.field( + format!("{track_position}. :newspaper: `{title}`"), format!(":notes: `{url}`"), false, ); diff --git a/cadency_commands/src/urban.rs b/cadency_commands/src/urban.rs index ed89130..177e426 100644 --- a/cadency_commands/src/urban.rs +++ b/cadency_commands/src/urban.rs @@ -6,10 +6,10 @@ use serenity::{ async_trait, builder::CreateEmbed, client::Context, - model::application::interaction::application_command::{ - ApplicationCommandInteraction, CommandDataOptionValue, + model::{ + application::{CommandDataOptionValue, CommandInteraction}, + Color, }, - utils::Color, }; #[derive(Deserialize, Debug)] @@ -52,24 +52,24 @@ impl Urban { if index >= 3 { break; } - let mut embed_urban_entry = CreateEmbed::default(); - embed_urban_entry.color(Color::from_rgb(255, 255, 0)); - embed_urban_entry.title(&urban.word.replace(['[', ']'], "")); - embed_urban_entry.url(&urban.permalink); - embed_urban_entry.field( - "Definition", - &urban.definition.replace(['[', ']'], ""), - false, - ); - embed_urban_entry.field("Example", &urban.example.replace(['[', ']'], ""), false); - embed_urban_entry.field( - "Rating", - format!( - "{} :thumbsup: {} :thumbsdown:", - urban.thumbs_up, urban.thumbs_down - ), - false, - ); + let embed_urban_entry = CreateEmbed::default() + .color(Color::from_rgb(255, 255, 0)) + .title(&urban.word.replace(['[', ']'], "")) + .url(&urban.permalink) + .field( + "Definition", + &urban.definition.replace(['[', ']'], ""), + false, + ) + .field("Example", &urban.example.replace(['[', ']'], ""), false) + .field( + "Rating", + format!( + "{} :thumbsup: {} :thumbsdown:", + urban.thumbs_up, urban.thumbs_down + ), + false, + ); embeds.push(embed_urban_entry); } embeds @@ -81,7 +81,7 @@ impl CadencyCommand for Urban { async fn execute<'a>( &self, _ctx: &Context, - command: &'a mut ApplicationCommandInteraction, + command: &'a mut CommandInteraction, respone_builder: &'a mut ResponseBuilder, ) -> Result { let query = utils::get_option_value_at_position(command.data.options.as_ref(), 0) diff --git a/cadency_core/src/client.rs b/cadency_core/src/client.rs index 1a49385..c50b648 100644 --- a/cadency_core/src/client.rs +++ b/cadency_core/src/client.rs @@ -1,6 +1,6 @@ use crate::{ - command::Commands, error::CadencyError, handler::command::Handler, intents::CadencyIntents, - CadencyCommand, + command::Commands, error::CadencyError, handler::command::Handler, http::HttpClientKey, + intents::CadencyIntents, CadencyCommand, }; use serenity::{client::Client, model::gateway::GatewayIntents}; use songbird::SerenityInit; @@ -25,12 +25,10 @@ impl Cadency { let mut client = Client::builder(self.token, self.intents) .event_handler(Handler) .register_songbird() + .type_map_insert::(self.commands) + .type_map_insert::(reqwest::Client::new()) .await .map_err(|err| CadencyError::Start { source: err })?; - { - let mut data = client.data.write().await; - data.insert::(self.commands); - } client .start() .await diff --git a/cadency_core/src/command.rs b/cadency_core/src/command.rs index 0c8186e..cdb4cb9 100644 --- a/cadency_core/src/command.rs +++ b/cadency_core/src/command.rs @@ -5,16 +5,12 @@ use crate::{ }; use serenity::{ async_trait, - client::Context, - model::{ - application::{ - command::Command, - interaction::{ - application_command::ApplicationCommandInteraction, InteractionResponseType, - }, - }, - prelude::command::CommandOptionType, + builder::{ + CreateCommand, CreateCommandOption, CreateInteractionResponse, + CreateInteractionResponseMessage, }, + client::Context, + model::application::{Command, CommandInteraction, CommandOptionType}, prelude::TypeMapKey, }; use std::sync::Arc; @@ -51,27 +47,23 @@ pub struct CadencyCommandOption { pub trait CadencyCommand: Sync + Send + CadencyCommandBaseline { /// Construct the slash command that will be submited to the discord api async fn register(&self, ctx: &Context) -> Result { - Ok( - Command::create_global_application_command(&ctx.http, |command| { - let command_builder = command.name(self.name()).description(self.description()); - for cadency_option in self.options() { - command_builder.create_option(|option_res| { - option_res - .name(cadency_option.name) - .description(cadency_option.description) - .kind(cadency_option.kind) - .required(cadency_option.required) - }); - } - command_builder + let command_options: Vec = self + .options() + .into_iter() + .map(|option| { + CreateCommandOption::new(option.kind, option.name, option.description) + .required(option.required) }) - .await?, - ) + .collect(); + let command_builder = CreateCommand::new(self.name()) + .description(self.description()) + .set_options(command_options); + Ok(Command::create_global_command(&ctx.http, command_builder).await?) } async fn execute<'a>( &self, ctx: &Context, - command: &'a mut ApplicationCommandInteraction, + command: &'a mut CommandInteraction, response_builder: &'a mut ResponseBuilder, ) -> Result; } @@ -96,15 +88,17 @@ pub(crate) async fn setup_commands(ctx: &Context) -> Result<(), serenity::Error> pub(crate) async fn command_not_implemented( ctx: &Context, - command: &ApplicationCommandInteraction, + command: &CommandInteraction, ) -> Result<(), CadencyError> { error!("The following command is not known: {:?}", command); + command - .create_interaction_response(&ctx.http, |response| { - response - .kind(InteractionResponseType::ChannelMessageWithSource) - .interaction_response_data(|message| message.content("Unknown command")) - }) + .create_response( + &ctx.http, + CreateInteractionResponse::Message( + CreateInteractionResponseMessage::new().content("Unknown command"), + ), + ) .await .map_err(|err| { error!("Interaction response failed: {}", err); diff --git a/cadency_core/src/handler/command.rs b/cadency_core/src/handler/command.rs index b42a1d4..7a18d8d 100644 --- a/cadency_core/src/handler/command.rs +++ b/cadency_core/src/handler/command.rs @@ -1,14 +1,14 @@ use crate::{ command::{command_not_implemented, setup_commands}, response::{ResponseBuilder, ResponseTiming}, - utils, - utils::set_bot_presence, - CadencyError, + utils, CadencyError, }; use serenity::{ + all::OnlineStatus, async_trait, client::{Context, EventHandler}, - model::{application::interaction::Interaction, event::ResumedEvent, gateway::Ready}, + gateway::ActivityData, + model::{application::Interaction, event::ResumedEvent, gateway::Ready}, }; pub(crate) struct Handler; @@ -17,7 +17,9 @@ pub(crate) struct Handler; impl EventHandler for Handler { async fn ready(&self, ctx: Context, _data_about_bot: Ready) { info!("🚀 Start Cadency Discord Bot"); - set_bot_presence(&ctx).await; + // Set the bot presence to "Listening to music" + ctx.set_presence(Some(ActivityData::listening("music")), OnlineStatus::Online); + info!("⏳ Started to submit commands, please wait..."); match setup_commands(&ctx).await { Ok(_) => info!("✅ Application commands submitted"), @@ -30,7 +32,7 @@ impl EventHandler for Handler { } async fn interaction_create(&self, ctx: Context, interaction: Interaction) { - if let Interaction::ApplicationCommand(mut command) = interaction { + if let Interaction::Command(mut command) = interaction { let cmd_target = utils::get_commands(&ctx) .await .into_iter() diff --git a/cadency_core/src/http.rs b/cadency_core/src/http.rs new file mode 100644 index 0000000..a5c21d2 --- /dev/null +++ b/cadency_core/src/http.rs @@ -0,0 +1,14 @@ +use serenity::prelude::TypeMapKey; + +pub struct HttpClientKey; + +impl TypeMapKey for HttpClientKey { + type Value = reqwest::Client; +} + +pub(crate) async fn get_http_client(ctx: &serenity::client::Context) -> reqwest::Client { + let data = ctx.data.read().await; + data.get::() + .expect("Expected HttpClientKey in TypeMap.") + .clone() +} diff --git a/cadency_core/src/lib.rs b/cadency_core/src/lib.rs index 50b296d..0c31dcf 100644 --- a/cadency_core/src/lib.rs +++ b/cadency_core/src/lib.rs @@ -9,6 +9,7 @@ pub use command::{CadencyCommand, CadencyCommandBaseline, CadencyCommandOption}; mod error; pub use error::CadencyError; pub mod handler; +pub mod http; mod intents; pub mod response; pub mod utils; diff --git a/cadency_core/src/response.rs b/cadency_core/src/response.rs index f650ff2..6fa2f5f 100644 --- a/cadency_core/src/response.rs +++ b/cadency_core/src/response.rs @@ -1,10 +1,11 @@ use crate::CadencyError; use derive_builder::Builder; use serenity::{ - builder::CreateEmbed, - model::prelude::interaction::{ - application_command::ApplicationCommandInteraction, InteractionResponseType, + builder::{ + CreateEmbed, CreateInteractionResponse, CreateInteractionResponseMessage, + EditInteractionResponse, }, + model::prelude::CommandInteraction, prelude::Context, }; @@ -37,39 +38,45 @@ impl Response { pub async fn submit<'a>( self, ctx: &Context, - command: &'a mut ApplicationCommandInteraction, + command: &'a mut CommandInteraction, ) -> Result<(), CadencyError> { match self.timing { + // Create a regular text response that might has embeds ResponseTiming::Instant => { + let response = if let Some(msg) = self.message { + CreateInteractionResponseMessage::new() + .content(msg) + .add_embeds(self.embeds) + } else { + CreateInteractionResponseMessage::new().add_embeds(self.embeds) + }; command - .create_interaction_response(&ctx.http, |response| { - response - .kind(InteractionResponseType::ChannelMessageWithSource) - .interaction_response_data(|message| { - if let Some(msg) = self.message { - message.content(msg); - } - message.add_embeds(self.embeds) - }) - }) + .create_response(&ctx.http, CreateInteractionResponse::Message(response)) .await } + // Just indicate that the command is being processed ResponseTiming::DeferredInfo => { command - .create_interaction_response(&ctx.http, |response| { - response.kind(InteractionResponseType::DeferredChannelMessageWithSource) - }) + .create_response( + &ctx.http, + CreateInteractionResponse::Defer(CreateInteractionResponseMessage::new()), + ) .await } - ResponseTiming::Deferred => command - .edit_original_interaction_response(&ctx.http, |previous_response| { - if let Some(msg) = self.message { - previous_response.content(msg); - } - previous_response.add_embeds(self.embeds) - }) - .await - .map(|_| ()), + // Edit the deferred response with the actual response + ResponseTiming::Deferred => { + let edit_response = if let Some(msg) = self.message { + EditInteractionResponse::new() + .content(msg) + .add_embeds(self.embeds) + } else { + EditInteractionResponse::new().add_embeds(self.embeds) + }; + command + .edit_response(&ctx.http, edit_response) + .await + .map(|_| ()) + } } .map_err(|err| { error!("Failed to submit response: {}", err); diff --git a/cadency_core/src/utils/mod.rs b/cadency_core/src/utils/mod.rs index 17d202a..a896516 100644 --- a/cadency_core/src/utils/mod.rs +++ b/cadency_core/src/utils/mod.rs @@ -1,25 +1,12 @@ use crate::{command::Commands, CadencyCommand}; use serenity::{ client::Context, - model::{ - application::interaction::application_command::{ - CommandDataOption, CommandDataOptionValue, - }, - gateway::Activity, - user::OnlineStatus, - }, + model::application::{CommandDataOption, CommandDataOptionValue}, }; use std::sync::Arc; pub mod voice; -/// Set the online status and activity of the bot. -/// Should not be set before the `ready` event. -pub(crate) async fn set_bot_presence(ctx: &Context) { - ctx.set_presence(Some(Activity::listening("music")), OnlineStatus::Online) - .await; -} - pub(crate) async fn get_commands(ctx: &Context) -> Vec> { let data_read = ctx.data.read().await; data_read @@ -32,7 +19,5 @@ pub fn get_option_value_at_position( options: &[CommandDataOption], position: usize, ) -> Option<&CommandDataOptionValue> { - options - .get(position) - .and_then(|option| option.resolved.as_ref()) + options.get(position).map(|option| &option.value) } diff --git a/cadency_core/src/utils/voice.rs b/cadency_core/src/utils/voice.rs index fee7ab1..5217a57 100644 --- a/cadency_core/src/utils/voice.rs +++ b/cadency_core/src/utils/voice.rs @@ -1,16 +1,27 @@ -use crate::{error::CadencyError, utils}; +use crate::{error::CadencyError, http::get_http_client, utils}; use reqwest::Url; use serenity::{ + all::{Guild, GuildId}, + cache::CacheRef, client::Context, model, - model::application::interaction::application_command::{ - ApplicationCommandInteraction, CommandDataOption, CommandDataOptionValue, - }, + model::application::{CommandDataOption, CommandDataOptionValue, CommandInteraction}, }; -use songbird::{input::Input, input::Restartable, Songbird}; +use songbird::{ + input::{AuxMetadata, Input, YoutubeDl}, + tracks::TrackHandle, + typemap::TypeMapKey, + Songbird, +}; + +pub struct TrackMetaKey; + +impl TypeMapKey for TrackMetaKey { + type Value = AuxMetadata; +} pub fn get_active_voice_channel_id( - guild: model::guild::Guild, + guild: CacheRef<'_, GuildId, Guild>, user_id: model::id::UserId, ) -> Option { guild @@ -21,7 +32,7 @@ pub fn get_active_voice_channel_id( pub async fn join( ctx: &Context, - command: &ApplicationCommandInteraction, + command: &CommandInteraction, ) -> Result< ( std::sync::Arc, @@ -37,7 +48,9 @@ pub async fn join( let channel_id = ctx .cache .guild(guild_id) - .and_then(|guild| utils::voice::get_active_voice_channel_id(guild, command.user.id)) + .and_then(|guild_cache_ref| { + utils::voice::get_active_voice_channel_id(guild_cache_ref, command.user.id) + }) .ok_or(CadencyError::Join)?; debug!("Try to join guild with id: {:?}", guild_id); // Skip channel join if already connected @@ -51,48 +64,65 @@ pub async fn join( return Ok((manager, call, guild_id)); } } - // join the channel - let (call, join) = manager.join(guild_id, channel_id).await; - join.map_err(|err| { - error!("Voice channel join failed: {err:?}"); + // Construct the call for the channel, don't join just yet + let call = manager.join(guild_id, channel_id).await.map_err(|err| { + error!("Unable to construct call for channel: {err:?}"); CadencyError::Join })?; + // Join the channel, this is scoped to drop the lock as soon as possible + { + let mut locked_call = call.lock().await; + locked_call.join(channel_id).await.map_err(|err| { + error!("Voice channel join failed: {err:?}"); + CadencyError::Join + })?; + } Ok((manager, call, guild_id)) } pub async fn add_song( + context: &Context, call: std::sync::Arc>, payload: String, is_url: bool, - add_lazy: bool, -) -> Result { +) -> Result<(songbird::input::AuxMetadata, TrackHandle), songbird::input::AuxMetadataError> { debug!("Add song to playlist: '{payload}'"); + let request_client = get_http_client(context).await; + // Create the YoutubeDL source from url or search string let source = if is_url { - Restartable::ytdl(payload, add_lazy).await? + YoutubeDl::new(request_client, payload) } else { - Restartable::ytdl_search(payload, add_lazy).await? + YoutubeDl::new(request_client, format!("ytsearch1:{payload}")) }; let mut handler = call.lock().await; - let track: Input = source.into(); - let metadata = *track.metadata.clone(); - handler.enqueue_source(track); - Ok(metadata) + + // Extract metadata and enqueue the source + let mut input: Input = source.into(); + let metadata = input.aux_metadata().await?; + + let track_handle = handler.enqueue_input(input).await; + // Store the metadata for later use + track_handle + .typemap() + .write() + .await + .insert::(metadata.clone()); + + Ok((metadata, track_handle)) } pub fn parse_valid_url(command_options: &[CommandDataOption]) -> Option { - command_options - .get(0) - .and_then(|option| match option.resolved.as_ref() { - Some(value) => { - if let CommandDataOptionValue::String(url) = value { - Some(url) - } else { - None - } + match utils::get_option_value_at_position(command_options, 0) { + Some(value) => { + if let CommandDataOptionValue::String(url) = value { + Some(url) + } else { + None } - None => None, - }) - .and_then(|url| Url::parse(url).ok()) + } + None => None, + } + .and_then(|url| Url::parse(url).ok()) } pub async fn get_songbird(ctx: &Context) -> std::sync::Arc { diff --git a/examples/custom_commands/examples/custom_commands.rs b/examples/custom_commands/examples/custom_commands.rs index 0ad54ce..aeb9409 100644 --- a/examples/custom_commands/examples/custom_commands.rs +++ b/examples/custom_commands/examples/custom_commands.rs @@ -9,11 +9,10 @@ use cadency_core::{ setup_commands, utils, Cadency, CadencyCommand, CadencyError, }; use serenity::{ + all::Mentionable, async_trait, client::Context, - model::application::interaction::application_command::{ - ApplicationCommandInteraction, CommandDataOptionValue, - }, + model::application::{CommandDataOptionValue, CommandInteraction}, }; // This is your custom command with the name "hello" @@ -28,13 +27,13 @@ impl CadencyCommand for Hello { async fn execute<'a>( &self, _ctx: &Context, - command: &'a mut ApplicationCommandInteraction, + command: &'a mut CommandInteraction, response_builder: &'a mut ResponseBuilder, ) -> Result { let user_arg = utils::get_option_value_at_position(command.data.options.as_ref(), 0) .and_then(|option_value| { - if let CommandDataOptionValue::User(user, _) = option_value { - Some(user) + if let CommandDataOptionValue::User(user_id) = option_value { + Some(user_id) } else { error!("Command argument is not a user"); None @@ -42,7 +41,7 @@ impl CadencyCommand for Hello { }) .expect("A user as command argument"); Ok(response_builder - .message(Some(format!("**Hello {user_arg}!**"))) + .message(Some(format!("**Hello {}!**", user_arg.mention()))) .build()?) } } From 4c39a673d191e0ca538205bf415c1e8fd40766df Mon Sep 17 00:00:00 2001 From: Jontze <42588836+jontze@users.noreply.github.com> Date: Thu, 30 Nov 2023 14:14:30 +0100 Subject: [PATCH 5/8] chore(docker): Update rust version in docker image to latest --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 0c7b3fc..a3ce6c0 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM lukemathwalker/cargo-chef:latest-rust-1.68-slim-bullseye as build_base +FROM lukemathwalker/cargo-chef:latest-rust-1.74-slim-bullseye as build_base FROM build_base as planner WORKDIR /cadency From d20575ec057f20a6e242a9b18698286b8d7b4c81 Mon Sep 17 00:00:00 2001 From: Jontze <42588836+jontze@users.noreply.github.com> Date: Fri, 1 Dec 2023 10:45:51 +0100 Subject: [PATCH 6/8] chore(deps): Add all by symphonia supported audio formats to play audio --- Cargo.toml | 4 ++++ cadency_core/Cargo.toml | 1 + 2 files changed, 5 insertions(+) diff --git a/Cargo.toml b/Cargo.toml index e017452..29d6bd5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -37,3 +37,7 @@ features = ["derive"] version = "0.11.18" default-features = false features = ["rustls-tls", "json"] + +[workspace.dependencies.symphonia] +version = "0.5" +features = ["all-formats"] diff --git a/cadency_core/Cargo.toml b/cadency_core/Cargo.toml index 006c76a..0cc10d9 100644 --- a/cadency_core/Cargo.toml +++ b/cadency_core/Cargo.toml @@ -14,3 +14,4 @@ tokio = { workspace = true } reqwest = { workspace = true } thiserror = { workspace = true } derive_builder = { workspace = true } +symphonia = { workspace = true } From aad656b36f00bf8fa305331f4abe58505e77a234 Mon Sep 17 00:00:00 2001 From: Jontze <42588836+jontze@users.noreply.github.com> Date: Fri, 1 Dec 2023 10:47:46 +0100 Subject: [PATCH 7/8] refactor(play): Scope call locks to a minimum --- cadency_commands/src/play.rs | 32 +++++++++++++++++++------------- 1 file changed, 19 insertions(+), 13 deletions(-) diff --git a/cadency_commands/src/play.rs b/cadency_commands/src/play.rs index 1a133c5..d2a02c8 100644 --- a/cadency_commands/src/play.rs +++ b/cadency_commands/src/play.rs @@ -95,13 +95,15 @@ impl CadencyCommand for Play { } } amount_total_added_playlist_duration /= 60_f32; - let mut handler = call.lock().await; - handler.remove_all_global_events(); - handler.add_global_event( - Event::Periodic(std::time::Duration::from_secs(120), None), - InactiveHandler { guild_id, manager }, - ); - drop(handler); + // This call interaction is scoped to drop the mutex lock as soon as possible + { + let mut handler = call.lock().await; + handler.remove_all_global_events(); + handler.add_global_event( + Event::Periodic(std::time::Duration::from_secs(120), None), + InactiveHandler { guild_id, manager }, + ); + } response_builder.message(Some(format!( ":white_check_mark: **Added ___{amount_added_playlist_songs}___ songs to the queue with a duration of ___{amount_total_added_playlist_duration:.2}___ mins** \n**Playing** :notes: `{search_payload}`", ))) @@ -115,12 +117,16 @@ impl CadencyCommand for Play { message: ":x: **Couldn't add audio source to the queue!**".to_string(), } })?; - let mut handler = call.lock().await; - handler.remove_all_global_events(); - handler.add_global_event( - Event::Periodic(std::time::Duration::from_secs(120), None), - InactiveHandler { guild_id, manager }, - ); + // This call interaction is scoped to drop the mutex lock as soon as possible + { + let mut handler = call.lock().await; + handler.remove_all_global_events(); + handler.add_global_event( + Event::Periodic(std::time::Duration::from_secs(120), None), + InactiveHandler { guild_id, manager }, + ); + } + let song_url = if is_url { search_payload } else { From e70e4825de49de3e5b1bfdf9272b3cd3c3ade61d Mon Sep 17 00:00:00 2001 From: Jontze <42588836+jontze@users.noreply.github.com> Date: Fri, 1 Dec 2023 12:32:08 +0100 Subject: [PATCH 8/8] feat(loop): Display amount of remaining loops for track --- cadency_commands/src/now.rs | 20 +++++++++++++++++++- cadency_commands/src/tracks.rs | 22 +++++++++++++++++++--- 2 files changed, 38 insertions(+), 4 deletions(-) diff --git a/cadency_commands/src/now.rs b/cadency_commands/src/now.rs index 1c4a101..43e097c 100644 --- a/cadency_commands/src/now.rs +++ b/cadency_commands/src/now.rs @@ -4,6 +4,7 @@ use cadency_core::{ CadencyCommand, CadencyError, }; use serenity::{async_trait, client::Context, model::application::CommandInteraction}; +use songbird::tracks::LoopState; #[derive(CommandBaseline, Default)] #[description = "Shows current song"] @@ -29,6 +30,9 @@ impl CadencyCommand for Now { message: ":x: **No song is playing**".to_string(), })?; + // Extract Loop State from Track + let loop_state = track.get_info().await.unwrap().loops; + // Create message from track metadata. This is scoped to drop the read lock on the // trackmeta as soon as possible. let message = { @@ -36,9 +40,23 @@ impl CadencyCommand for Now { let metadata = track_map .get::() .expect("Metadata to be present in track map"); + metadata.title.as_ref().map_or( String::from(":x: **Could not add audio source to the queue!**"), - |title| format!(":newspaper: `{title}`"), + |title| { + let mut track_info = format!(":newspaper: `{title}`"); + match loop_state { + LoopState::Infinite => { + track_info.push_str("\n:repeat: `Infinite`"); + } + LoopState::Finite(loop_amount) => { + if loop_amount > 0 { + track_info.push_str(&format!("\n:repeat: `{}`", loop_amount)); + } + } + } + track_info + }, ) }; diff --git a/cadency_commands/src/tracks.rs b/cadency_commands/src/tracks.rs index b2efe67..aba5d6c 100644 --- a/cadency_commands/src/tracks.rs +++ b/cadency_commands/src/tracks.rs @@ -9,6 +9,7 @@ use serenity::{ client::Context, model::{application::CommandInteraction, Color}, }; +use songbird::tracks::LoopState; #[derive(CommandBaseline, Default)] #[description = "List all tracks in the queue"] @@ -42,7 +43,8 @@ impl CadencyCommand for Tracks { let track_position = index + 1; // Extract title and url of the track. This is scoped to drop the read lock on // the track meta as soon as possible. - let (title, url) = { + let (title, url, loop_state) = { + // Extract track Metadata from tracks TyeMap let track_map = track.typemap().read().await; let metadata = track_map .get::() @@ -55,11 +57,25 @@ impl CadencyCommand for Tracks { .source_url .as_ref() .map_or("**No url provided**", |u| u); - (title.to_owned(), url.to_owned()) + + // Extract loop state from track state + let track_info = track.get_info().await.unwrap(); + (title.to_owned(), url.to_owned(), track_info.loops) }; + let mut embed_value = format!(":notes: `{url}`"); + match loop_state { + LoopState::Infinite => { + embed_value.push_str("\n:repeat: `Infinite`"); + } + LoopState::Finite(loop_amount) => { + if loop_amount > 0 { + embed_value.push_str(&format!("\n:repeat: `{}`", loop_amount)); + } + } + } embeded_tracks = embeded_tracks.field( format!("{track_position}. :newspaper: `{title}`"), - format!(":notes: `{url}`"), + embed_value, false, ); }