diff --git a/.TODO b/.TODO new file mode 100644 index 0000000..f6e1060 --- /dev/null +++ b/.TODO @@ -0,0 +1,2 @@ +handle disconnect by someone, other than command +disconnect after some time when not playing \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index dd16aa1..52a49e8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -753,6 +753,12 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "extended" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af9673d8203fcb076b19dfd17e38b3d4ae9f44959416ea532ce72415a6020365" + [[package]] name = "fastrand" version = "2.1.0" @@ -1268,6 +1274,19 @@ dependencies = [ "tower-service", ] +[[package]] +name = "hyper-tls" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" +dependencies = [ + "bytes", + "hyper 0.14.30", + "native-tls", + "tokio", + "tokio-native-tls", +] + [[package]] name = "hyper-tls" version = "0.6.0" @@ -1286,9 +1305,9 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.6" +version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ab92f4f49ee4fb4f997c784b7a2e0fa70050211e0b6a287f898c3c9785ca956" +checksum = "df2dcfbe0677734ab2f3ffa7fa7bfd4706bfdc1ef393f2ee30184aed67e631b4" dependencies = [ "bytes", "futures-channel", @@ -1299,7 +1318,6 @@ dependencies = [ "pin-project-lite", "socket2", "tokio", - "tower", "tower-service", "tracing", ] @@ -1552,9 +1570,11 @@ dependencies = [ "poise", "rand", "regex", + "reqwest 0.11.27", "serenity", "songbird", "sqlx", + "symphonia", "tenorv2", "tokio", "tokio-cron-scheduler", @@ -2163,11 +2183,13 @@ dependencies = [ "http-body 0.4.6", "hyper 0.14.30", "hyper-rustls 0.24.2", + "hyper-tls 0.5.0", "ipnet", "js-sys", "log", "mime", "mime_guess", + "native-tls", "once_cell", "percent-encoding", "pin-project-lite", @@ -2179,6 +2201,7 @@ dependencies = [ "sync_wrapper 0.1.2", "system-configuration 0.5.1", "tokio", + "tokio-native-tls", "tokio-rustls 0.24.1", "tokio-util", "tower-service", @@ -2193,9 +2216,9 @@ dependencies = [ [[package]] name = "reqwest" -version = "0.12.8" +version = "0.12.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f713147fbe92361e52392c73b8c9e48c04c6625bce969ef54dc901e58e042a7b" +checksum = "43e734407157c3c2034e0258f5e4473ddb361b1e85f95a66690d67264d7cd1da" dependencies = [ "base64 0.22.1", "bytes", @@ -2208,7 +2231,7 @@ dependencies = [ "http-body-util", "hyper 1.4.1", "hyper-rustls 0.27.2", - "hyper-tls", + "hyper-tls 0.6.0", "hyper-util", "ipnet", "js-sys", @@ -2226,6 +2249,7 @@ dependencies = [ "system-configuration 0.6.1", "tokio", "tokio-native-tls", + "tower", "tower-service", "url", "wasm-bindgen", @@ -3189,8 +3213,95 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "815c942ae7ee74737bb00f965fa5b5a2ac2ce7b6c01c0cc169bbeaf7abd5f5a9" dependencies = [ "lazy_static", + "symphonia-bundle-flac", + "symphonia-bundle-mp3", + "symphonia-codec-aac", + "symphonia-codec-adpcm", + "symphonia-codec-alac", + "symphonia-codec-pcm", + "symphonia-codec-vorbis", + "symphonia-core", + "symphonia-format-isomp4", + "symphonia-format-mkv", + "symphonia-format-ogg", + "symphonia-format-riff", + "symphonia-metadata", +] + +[[package]] +name = "symphonia-bundle-flac" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72e34f34298a7308d4397a6c7fbf5b84c5d491231ce3dd379707ba673ab3bd97" +dependencies = [ + "log", "symphonia-core", "symphonia-metadata", + "symphonia-utils-xiph", +] + +[[package]] +name = "symphonia-bundle-mp3" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c01c2aae70f0f1fb096b6f0ff112a930b1fb3626178fba3ae68b09dce71706d4" +dependencies = [ + "lazy_static", + "log", + "symphonia-core", + "symphonia-metadata", +] + +[[package]] +name = "symphonia-codec-aac" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdbf25b545ad0d3ee3e891ea643ad115aff4ca92f6aec472086b957a58522f70" +dependencies = [ + "lazy_static", + "log", + "symphonia-core", +] + +[[package]] +name = "symphonia-codec-adpcm" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c94e1feac3327cd616e973d5be69ad36b3945f16b06f19c6773fc3ac0b426a0f" +dependencies = [ + "log", + "symphonia-core", +] + +[[package]] +name = "symphonia-codec-alac" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d8a6666649a08412906476a8b0efd9b9733e241180189e9f92b09c08d0e38f3" +dependencies = [ + "log", + "symphonia-core", +] + +[[package]] +name = "symphonia-codec-pcm" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f395a67057c2ebc5e84d7bb1be71cce1a7ba99f64e0f0f0e303a03f79116f89b" +dependencies = [ + "log", + "symphonia-core", +] + +[[package]] +name = "symphonia-codec-vorbis" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a98765fb46a0a6732b007f7e2870c2129b6f78d87db7987e6533c8f164a9f30" +dependencies = [ + "log", + "symphonia-core", + "symphonia-utils-xiph", ] [[package]] @@ -3206,6 +3317,56 @@ dependencies = [ "log", ] +[[package]] +name = "symphonia-format-isomp4" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abfdf178d697e50ce1e5d9b982ba1b94c47218e03ec35022d9f0e071a16dc844" +dependencies = [ + "encoding_rs", + "log", + "symphonia-core", + "symphonia-metadata", + "symphonia-utils-xiph", +] + +[[package]] +name = "symphonia-format-mkv" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bb43471a100f7882dc9937395bd5ebee8329298e766250b15b3875652fe3d6f" +dependencies = [ + "lazy_static", + "log", + "symphonia-core", + "symphonia-metadata", + "symphonia-utils-xiph", +] + +[[package]] +name = "symphonia-format-ogg" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ada3505789516bcf00fc1157c67729eded428b455c27ca370e41f4d785bfa931" +dependencies = [ + "log", + "symphonia-core", + "symphonia-metadata", + "symphonia-utils-xiph", +] + +[[package]] +name = "symphonia-format-riff" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f7be232f962f937f4b7115cbe62c330929345434c834359425e043bfd15f50" +dependencies = [ + "extended", + "log", + "symphonia-core", + "symphonia-metadata", +] + [[package]] name = "symphonia-metadata" version = "0.5.4" @@ -3218,6 +3379,16 @@ dependencies = [ "symphonia-core", ] +[[package]] +name = "symphonia-utils-xiph" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "484472580fa49991afda5f6550ece662237b00c6f562c7d9638d1b086ed010fe" +dependencies = [ + "symphonia-core", + "symphonia-metadata", +] + [[package]] name = "syn" version = "1.0.109" @@ -3323,7 +3494,7 @@ dependencies = [ "form_urlencoded", "json", "regex", - "reqwest 0.12.8", + "reqwest 0.12.12", "thiserror", ] @@ -3557,14 +3728,14 @@ dependencies = [ [[package]] name = "tower" -version = "0.4.13" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" +checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" dependencies = [ "futures-core", "futures-util", - "pin-project", "pin-project-lite", + "sync_wrapper 1.0.1", "tokio", "tower-layer", "tower-service", @@ -3572,15 +3743,15 @@ dependencies = [ [[package]] name = "tower-layer" -version = "0.3.2" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c20c8dbed6283a09604c3e69b4b7eeb54e298b8a600d4d5ecb5ad39de609f1d0" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" [[package]] name = "tower-service" -version = "0.3.2" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" [[package]] name = "tracing" diff --git a/Cargo.toml b/Cargo.toml index 63a2b9c..999bd3f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,7 +20,12 @@ sqlx = {version="0.8.2", features=["runtime-tokio", "sqlite"]} form_urlencoded = "1.2.1" tenorv2 = { path = "./tenor-v2/tenorv2" } # librespot = { version = "0.6.0", default-features = false, features = ["rodio-backend"] } -songbird = { version = "0.4.6", features = ["driver"] } +songbird = { version = "0.4.6", features = ["driver", "builtin-queue"] } +reqwest = "0.11.27" # songbird depends on ^0.11 + +[dependencies.symphonia] +version = "0.5.2" +features = ["aac", "mp3", "isomp4", "alac"] [features] DEBUG = [] diff --git a/src/commands/voice/mod.rs b/src/commands/voice/mod.rs index d44230b..0df6866 100644 --- a/src/commands/voice/mod.rs +++ b/src/commands/voice/mod.rs @@ -1 +1,5 @@ -pub mod player; \ No newline at end of file +pub mod util; +pub mod player; +pub mod radio; +// pub mod spotify; +// pub mod yt; \ No newline at end of file diff --git a/src/commands/voice/player.rs b/src/commands/voice/player.rs index ddffb7d..5444b86 100644 --- a/src/commands/voice/player.rs +++ b/src/commands/voice/player.rs @@ -1,122 +1,26 @@ use std::vec; -use serenity::{all::ChannelId, async_trait}; -use songbird::events::{Event, EventContext, EventHandler as VoiceEventHandler}; use songbird::input::{File, Input}; -use songbird::TrackEvent; use crate::util::debug::send_error; use crate::util::poise_context_extension::ContextExt; use crate::util::utilities::get_local_songs; -use crate::{types::{Context, Error}, util::utilities::get_channel_by_name}; +use crate::types::{Context, Error}; -#[poise::command( - slash_command, - description_localized("en-US", "Play song") -)] -pub async fn play(ctx: Context<'_>, - #[description = "Song: "] - _url: String -) -> Result<(), Error> { +use super::util::{connect, autocomplete_channel}; - ctx.reply("Done!").await?; - Ok(()) -} +// #[poise::command( +// slash_command, +// description_localized("en-US", "Play song") +// )] +// pub async fn play(ctx: Context<'_>, +// #[description = "Song: "] +// _url: String +// ) -> Result<(), Error> { -/// Returns either voice channel to which the user is currently connected to or the one passed via name -async fn get_voice_channel(ctx: &Context<'_>, name: Option) -> Result { - if name.is_none() || name.as_ref().is_some_and(|n| n.is_empty()) { - match ctx.guild().and_then(|guild| - guild.voice_states.get(&ctx.author().id).and_then(|voice_state| - voice_state.channel_id - ) - ) { - Some(c) => Ok(c), - None => Err("You must be in a voice channel or specify explicit voice channel by name".to_string()) - } - } - else { - match ctx.guild().and_then(|guild| - get_channel_by_name(guild, name.unwrap()) - ) { - Some(c) => Ok(c), - None => Err("Channel with this name does not exist".to_string()) - } - } -} - -#[poise::command( - slash_command, - description_localized("en-US", "Connect to channel") -)] -pub async fn connect(ctx: Context<'_>, - #[autocomplete = "autocomplete_channel"] - #[description = "Voice channel name: "] - name: Option -) -> Result<(), Error> { - - if ctx.guild().is_none() { - ctx.reply_ephemeral("Can't use this outside of guild").await?; - return Ok(()); - } - - let voice_channel = match get_voice_channel(&ctx, name).await { - Ok(c) => c, - Err(e) => { - ctx.reply_ephemeral(e.as_str()).await?; - return Ok(()) - } - }; - - let manager = songbird::get(ctx.serenity_context()) - .await - .expect("Songbird Voice client placed in at initialisation.") - .clone(); - - let Some(guild_id) = ctx.guild_id() else { - ctx.reply_ephemeral("Guild id not found").await?; - return Ok(()) - }; - - if let Ok(handler_lock) = manager.join(guild_id, voice_channel).await { - let mut handler = handler_lock.lock().await; - handler.add_global_event(TrackEvent::Error.into(), TrackErrorNotifier); - } - - ctx.reply_ephemeral("Done!").await?; - Ok(()) -} - -struct TrackErrorNotifier; - -#[async_trait] -impl VoiceEventHandler for TrackErrorNotifier { - async fn act(&self, ctx: &EventContext<'_>) -> Option { - if let EventContext::Track(track_list) = ctx { - for (state, handle) in *track_list { - println!( - "Track {:?} encountered an error: {:?}", - handle.uuid(), - state.playing - ); - } - } - - None - } -} - -async fn autocomplete_channel( - ctx: Context<'_>, - _partial: &str, -) -> Vec { - - use crate::util::utilities::get_vc_names; - match ctx.guild() { - Some(guild) => get_vc_names(guild), - None => [].to_vec() - } -} +// ctx.reply("Done!").await?; +// Ok(()) +// } #[poise::command( slash_command, @@ -168,12 +72,12 @@ async fn autocomplete_song( #[poise::command( slash_command, - description_localized("en-US", "Connect to channel") + description_localized("en-US", "Play song from server storage") )] pub async fn play_local(ctx: Context<'_>, #[autocomplete = "autocomplete_channel"] #[description = "Voice channel name: "] - _channel: Option, + channel: Option, #[autocomplete = "autocomplete_song"] #[description = "Filename of local song: "] file_name: String @@ -194,17 +98,24 @@ pub async fn play_local(ctx: Context<'_>, return Ok(()) }; - + if manager.get(guild_id).is_none() { + match connect(&ctx, guild_id, channel).await { + Ok(_) => (), + Err(e) => { + ctx.reply_ephemeral(&e.to_string()).await?; + return Ok(()) + } + } + } if let Some(handler_lock) = manager.get(guild_id) { let mut handler = handler_lock.lock().await; let input_file = File::new(format!("/home/emil/Music/{file_name}")); let input = Input::Lazy(Box::new(input_file)); - let _ = handler.play_input(input); + let _ = handler.play_only_input(input); } else { - ctx.reply_ephemeral("Not in a voice channel").await?; return Ok(()) } @@ -212,44 +123,3 @@ pub async fn play_local(ctx: Context<'_>, ctx.reply_ephemeral("Done!").await?; Ok(()) } - -// #[poise::command( -// slash_command, -// description_localized("en-US", "Connect to channel") -// )] -// pub async fn play_local(ctx: Context<'_>, -// #[autocomplete = "autocomplete_song"] -// #[description = "Voice channel name: "] -// file_name: String -// ) -> Result<(), Error> { - -// if ctx.guild().is_none() { -// ctx.reply_ephemeral("Can't use this outside of guild").await?; -// return Ok(()); -// } - -// let manager = songbird::get(ctx.serenity_context()) -// .await -// .expect("Songbird Voice client placed in at initialisation.") -// .clone(); - -// let Some(guild_id) = ctx.guild_id() else { -// ctx.reply_ephemeral("Guild id not found").await?; -// return Ok(()) -// }; - -// if let Some(handler_lock) = manager.get(guild_id) { -// let mut handler = handler_lock.lock().await; - -// let input_file = File::new(format!("/home/emil/Music/{file_name}")); -// let input = Input::Lazy(Box::new(input_file)); -// let _ = handler.play_input(input); -// } -// else { -// ctx.reply_ephemeral("Not in a voice channel").await?; -// return Ok(()) -// } - -// ctx.reply_ephemeral("Done!").await?; -// Ok(()) -// } diff --git a/src/commands/voice/radio/mod.rs b/src/commands/voice/radio/mod.rs new file mode 100644 index 0000000..27ead6c --- /dev/null +++ b/src/commands/voice/radio/mod.rs @@ -0,0 +1,3 @@ +pub use radio_player::*; + +pub mod radio_player; diff --git a/src/commands/voice/radio/radio_player.rs b/src/commands/voice/radio/radio_player.rs new file mode 100644 index 0000000..b9ed39d --- /dev/null +++ b/src/commands/voice/radio/radio_player.rs @@ -0,0 +1,66 @@ +use std::vec; + +use reqwest::Client; + +use songbird::input::Input; +use songbird::input::HttpRequest; + +use crate::commands::util::connect; +use crate::util::poise_context_extension::ContextExt; +use crate::types::{Context, Error}; +use crate::commands::voice::util::autocomplete_channel; + +// TODO: autocomplete radio stream URLs +#[poise::command( + slash_command, + description_localized("en-US", "Plays music from URL source") +)] +pub async fn radio(ctx: Context<'_>, + #[autocomplete = "autocomplete_channel"] + #[description = "Voice channel name: "] + channel: Option, + #[description = "Source URL: "] + url: String, +) -> Result<(), Error> { + + if ctx.guild().is_none() { + ctx.reply_ephemeral("Can't use this outside of guild").await?; + return Ok(()); + } + + let manager = songbird::get(ctx.serenity_context()) + .await + .expect("Songbird Voice client placed in at initialisation.") + .clone(); + + let Some(guild_id) = ctx.guild_id() else { + ctx.reply_ephemeral("Guild id not found").await?; + return Ok(()) + }; + + if manager.get(guild_id).is_none() { + match connect(&ctx, guild_id, channel).await { + Ok(_) => (), + Err(e) => { + ctx.reply_ephemeral(&e.to_string()).await?; + return Ok(()) + } + } + } + + if let Some(handler_lock) = manager.get(guild_id) { + let mut handler = handler_lock.lock().await; + + let client = Client::new(); + let request = HttpRequest::new(client, url); + let input = Input::from(request); + handler.play_only_input(input); + } + else { + ctx.reply_ephemeral("Not in a voice channel").await?; + return Ok(()) + } + + ctx.reply_ephemeral("Done!").await?; + Ok(()) +} diff --git a/src/commands/voice/spotify/player.rs b/src/commands/voice/spotify/player.rs deleted file mode 100644 index 5c00980..0000000 --- a/src/commands/voice/spotify/player.rs +++ /dev/null @@ -1,211 +0,0 @@ -use std::vec; - -use poise::{self, CreateReply}; -use serenity::all::{CacheHttp, Guild, PartialGuild, UserId}; -use serenity::{all::ChannelId, async_trait}; -use songbird::events::{Event, EventContext, EventHandler as VoiceEventHandler}; -use songbird::input::{File, Input}; -use songbird::tracks::Track; -use songbird::TrackEvent; - -use crate::util::debug::send_error; -use crate::util::poise_context_extension::ContextExt; -use crate::util::utilities::get_local_songs; -use crate::{types::{Context, Error}, util::utilities::get_channel_by_name}; - -#[poise::command( - slash_command, - description_localized("en-US", "Play song") -)] -pub async fn play(ctx: Context<'_>, - #[description = "Song: "] - _url: String -) -> Result<(), Error> { - - ctx.reply("Done!").await?; - Ok(()) -} - -/// Returns either voice channel to which the user is currently connected to or the one passed via name -async fn get_voice_channel(ctx: &Context<'_>, name: Option) -> Result { - if name.is_none() || name.as_ref().is_some_and(|n| n.is_empty()) { - match ctx.guild().and_then(|guild| - guild.voice_states.get(&ctx.author().id).and_then(|voice_state| - voice_state.channel_id - ) - ) { - Some(c) => Ok(c), - None => Err("You must be in a voice channel or specify explicit voice channel by name".to_string()) - } - } - else { - match ctx.guild().and_then(|guild| - get_channel_by_name(guild, name.unwrap()) - ) { - Some(c) => Ok(c), - None => Err("Channel with this name does not exist".to_string()) - } - } -} - -#[poise::command( - slash_command, - description_localized("en-US", "Connect to channel") -)] -pub async fn connect(ctx: Context<'_>, - #[autocomplete = "autocomplete_channel"] - #[description = "Voice channel name: "] - name: Option -) -> Result<(), Error> { - - if ctx.guild().is_none() { - ctx.reply_ephemeral("Can't use this outside of guild").await?; - return Ok(()); - } - - let voice_channel = match get_voice_channel(&ctx, name).await { - Ok(c) => c, - Err(e) => { - ctx.reply_ephemeral(e.as_str()).await?; - return Ok(()) - } - }; - - let manager = songbird::get(ctx.serenity_context()) - .await - .expect("Songbird Voice client placed in at initialisation.") - .clone(); - - let Some(guild_id) = ctx.guild_id() else { - ctx.reply_ephemeral("Guild id not found").await?; - return Ok(()) - }; - - if let Ok(handler_lock) = manager.join(guild_id, voice_channel).await { - let mut handler = handler_lock.lock().await; - handler.add_global_event(TrackEvent::Error.into(), TrackErrorNotifier); - } - - ctx.reply_ephemeral("Done!").await?; - Ok(()) -} - -struct TrackErrorNotifier; - -#[async_trait] -impl VoiceEventHandler for TrackErrorNotifier { - async fn act(&self, ctx: &EventContext<'_>) -> Option { - if let EventContext::Track(track_list) = ctx { - for (state, handle) in *track_list { - println!( - "Track {:?} encountered an error: {:?}", - handle.uuid(), - state.playing - ); - } - } - - None - } -} - -async fn autocomplete_channel( - ctx: Context<'_>, - _partial: &str, -) -> Vec { - - use crate::util::utilities::get_vc_names; - match ctx.guild() { - Some(guild) => get_vc_names(guild), - None => [].to_vec() - } -} - -#[poise::command( - slash_command, - description_localized("en-US", "Disconnect from voice channel") -)] -pub async fn disconnect( - ctx: Context<'_> -) -> Result<(), Error> { - - let Some(guild_id) = ctx.guild_id() else { - ctx.reply("Can't use this outside of guild").await?; - return Ok(()); - }; - - let manager = songbird::get(ctx.serenity_context()) - .await - .expect("Songbird Voice client placed in at initialisation.") - .clone(); - - let has_handler = manager.get(guild_id).is_some(); - - if ! has_handler { - ctx.reply("I am not connected to a channel!").await?; - return Ok(()) - } - - match manager.remove(guild_id).await { - Ok(()) => { - ctx.reply("Disconnected").await?; - return Ok(()) - } - Err(e) => { - let _ = send_error(ctx.serenity_context().http.clone(), e.to_string()).await; - return Ok(()) - } - } -} - -async fn autocomplete_song( - _ctx: Context<'_>, - partial: &str, -) -> Vec { - - match get_local_songs(partial) { - Ok(names) => names, - Err(_) => vec![] - } -} - -#[poise::command( - slash_command, - description_localized("en-US", "Connect to channel") -)] -pub async fn play_local(ctx: Context<'_>, - #[autocomplete = "autocomplete_song"] - #[description = "Voice channel name: "] - file_name: String -) -> Result<(), Error> { - - if ctx.guild().is_none() { - ctx.reply_ephemeral("Can't use this outside of guild").await?; - return Ok(()); - } - - let manager = songbird::get(ctx.serenity_context()) - .await - .expect("Songbird Voice client placed in at initialisation.") - .clone(); - - let Some(guild_id) = ctx.guild_id() else { - ctx.reply_ephemeral("Guild id not found").await?; - return Ok(()) - }; - - if let Some(handler_lock) = manager.get(guild_id) { - let mut handler = handler_lock.lock().await; - - let input_file = File::new(format!("/home/emil/Music/{file_name}")); - let input = Input::Lazy(Box::new(input_file)); - let _ = handler.play_input(input); - } - else { - ctx.reply_ephemeral("Not in a voice channel").await?; - return Ok(()) - } - - ctx.reply_ephemeral("Done!").await?; - Ok(()) -} \ No newline at end of file diff --git a/src/commands/voice/util.rs b/src/commands/voice/util.rs new file mode 100644 index 0000000..4208b51 --- /dev/null +++ b/src/commands/voice/util.rs @@ -0,0 +1,76 @@ +use serenity::all::{ChannelId, GuildId}; +use serenity::async_trait; + +use songbird::events::{Event, EventContext, EventHandler as VoiceEventHandler}; +use songbird::TrackEvent; + +use crate::{types::{Context, Error}, util::utilities::get_channel_by_name}; + +/// Returns either voice channel to which the user is currently connected to or the one passed via name +async fn get_voice_channel(ctx: &Context<'_>, name: Option) -> Result { + if name.is_none() || name.as_ref().is_some_and(|n| n.is_empty()) { + match ctx.guild().and_then(|guild| + guild.voice_states.get(&ctx.author().id).and_then(|voice_state| + voice_state.channel_id + ) + ) { + Some(c) => Ok(c), + None => Err("You must be in a voice channel or specify explicit voice channel by name".to_string()) + } + } + else { + match ctx.guild().and_then(|guild| + get_channel_by_name(guild, name.unwrap()) + ) { + Some(c) => Ok(c), + None => Err("Channel with this name does not exist".to_string()) + } + } +} + +struct TrackErrorNotifier; + +#[async_trait] +impl VoiceEventHandler for TrackErrorNotifier { + async fn act(&self, ctx: &EventContext<'_>) -> Option { + if let EventContext::Track(track_list) = ctx { + for (state, handle) in *track_list { + println!( + "Track {:?} encountered an error: {:?}", + handle.uuid(), + state.playing + ); + } + } + + None + } +} + +pub async fn connect(ctx: &Context<'_>, guild_id: GuildId, channel: Option) -> Result<(), Error> { + let voice_channel = get_voice_channel(&ctx, channel).await?; + + let manager = songbird::get(ctx.serenity_context()) + .await + .expect("Songbird Voice client placed in at initialisation.") + .clone(); + + if let Ok(handler_lock) = manager.join(guild_id, voice_channel).await { + let mut handler = handler_lock.lock().await; + handler.add_global_event(TrackEvent::Error.into(), TrackErrorNotifier); + } + + Ok(()) +} + +pub async fn autocomplete_channel( + ctx: Context<'_>, + _partial: &str, +) -> Vec { + + use crate::util::utilities::get_vc_names; + match ctx.guild() { + Some(guild) => get_vc_names(guild), + None => [].to_vec() + } +} diff --git a/src/main.rs b/src/main.rs index 3eedf7d..6db17f5 100644 --- a/src/main.rs +++ b/src/main.rs @@ -86,6 +86,7 @@ async fn main() -> anyhow::Result<()> { commands::hug(), commands::player::play_local(), commands::player::disconnect(), + commands::radio::radio() ], prefix_options: poise::PrefixFrameworkOptions { prefix: Some("/".into()),