diff --git a/Cargo.lock b/Cargo.lock index 659ff2d..0a03307 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -325,6 +325,7 @@ dependencies = [ "serenity", "songbird", "sunk", + "symphonia", "tokio", ] @@ -1698,7 +1699,7 @@ checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" [[package]] name = "sunk" version = "0.1.2" -source = "git+https://github.com/Misterio77/sunk#2f6372e48738a89aa3b343433ca95f17e231db32" +source = "git+https://github.com/Misterio77/sunk#16bc4f782924ee794977238458a4ad8ad7197364" dependencies = [ "async-trait", "log", @@ -1720,8 +1721,84 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "62e48dba70095f265fdb269b99619b95d04c89e619538138383e63310b14d941" dependencies = [ "lazy_static", + "symphonia-bundle-flac", + "symphonia-bundle-mp3", + "symphonia-codec-aac", + "symphonia-codec-adpcm", + "symphonia-codec-pcm", + "symphonia-codec-vorbis", + "symphonia-core", + "symphonia-format-mkv", + "symphonia-format-ogg", + "symphonia-format-wav", + "symphonia-metadata", +] + +[[package]] +name = "symphonia-bundle-flac" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f23b0482a7cb18fcdf9981ab0b78df800ef0080187d294650023c462439058d" +dependencies = [ + "log", "symphonia-core", "symphonia-metadata", + "symphonia-utils-xiph", +] + +[[package]] +name = "symphonia-bundle-mp3" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f31d7fece546f1e6973011a9eceae948133bbd18fd3d52f6073b1e38ae6368a" +dependencies = [ + "bitflags", + "lazy_static", + "log", + "symphonia-core", + "symphonia-metadata", +] + +[[package]] +name = "symphonia-codec-aac" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68bdd75b25ce4b84b12a4bd20bfea2460c2dbd7fc1d227ef5533504d3168109d" +dependencies = [ + "lazy_static", + "log", + "symphonia-core", +] + +[[package]] +name = "symphonia-codec-adpcm" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "870e7dc1865d818c7b6318879d060553a73a3b2a3b8443dff90910f10ac41150" +dependencies = [ + "log", + "symphonia-core", +] + +[[package]] +name = "symphonia-codec-pcm" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47f1fbd220a06a641c8ce2ddad10f5ef6ee5cc0c54d9044d25d43b0d3119deaa" +dependencies = [ + "log", + "symphonia-core", +] + +[[package]] +name = "symphonia-codec-vorbis" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3953397e3506aa01350c4205817e4f95b58d476877a42f0458d07b665749e203" +dependencies = [ + "log", + "symphonia-core", + "symphonia-utils-xiph", ] [[package]] @@ -1737,6 +1814,42 @@ dependencies = [ "log", ] +[[package]] +name = "symphonia-format-mkv" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5c61dfc851ad25d4043d8c231d8617e8f7cd02a6cc0edad21ade21848d58895" +dependencies = [ + "lazy_static", + "log", + "symphonia-core", + "symphonia-metadata", + "symphonia-utils-xiph", +] + +[[package]] +name = "symphonia-format-ogg" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bf1a00ccd11452d44048a0368828040f778ae650418dbd9d8765b7ee2574c8d" +dependencies = [ + "log", + "symphonia-core", + "symphonia-metadata", + "symphonia-utils-xiph", +] + +[[package]] +name = "symphonia-format-wav" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da76614728fa27c003bdcdfbac51396bd8fcbf94c95fe8e62f1d2bac58ef03a4" +dependencies = [ + "log", + "symphonia-core", + "symphonia-metadata", +] + [[package]] name = "symphonia-metadata" version = "0.5.3" @@ -1749,6 +1862,16 @@ dependencies = [ "symphonia-core", ] +[[package]] +name = "symphonia-utils-xiph" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a450ca645b80d69aff8b35576cbfdc7f20940b29998202aab910045714c951f8" +dependencies = [ + "symphonia-core", + "symphonia-metadata", +] + [[package]] name = "syn" version = "1.0.109" diff --git a/Cargo.toml b/Cargo.toml index ac0e07a..e121637 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,7 +28,7 @@ features = [ "rustls-tls" ] version = "0.11" git = "https://github.com/serenity-rs/serenity" branch = "next" -features = [ "voice" ] +features = [ "client", "standard_framework", "voice", "rustls_backend" ] [dependencies.songbird] version = "0.3" @@ -36,9 +36,13 @@ git = "https://github.com/serenity-rs/songbird" branch = "next" features = [ "builtin-queue" ] +[dependencies.symphonia] +version = "0.5" +features = ["aac", "mp3"] + [dependencies.tokio] version = "1.0" -features = ["full"] +features = ["full", "rt-multi-thread", "signal"] [dependencies.anyhow] version = "1.0" diff --git a/default.nix b/default.nix index 537ad53..fee2598 100644 --- a/default.nix +++ b/default.nix @@ -13,7 +13,7 @@ in rustPlatform.buildRustPackage { cargoLock = { lockFile = ./Cargo.lock; outputHashes = { - "sunk-0.1.2" = "sha256-AFmMGiLcgBLni60x2dFgfCCz9oswYcqm3yLO1Kt3ltA="; + "sunk-0.1.2" = "sha256-0kCNEb38NbrhfgFB+aj3vcpWZhWjfHBP7V/CjUrMkF4="; "serenity-0.11.5" = "sha256-10s0kflNYEMwUXAgrh6d1IUk3ZRSCkAilz9m1lVhXhA="; "songbird-0.3.2" = "sha256-8wzCcV9W6K0MHqZ8yhTIMjh165NV8OQ9zlgrRrIhlOI="; }; diff --git a/src/client.rs b/src/client.rs index 66cd09c..4cd820e 100644 --- a/src/client.rs +++ b/src/client.rs @@ -49,10 +49,11 @@ impl Client { pub async fn discord(&self, ss: SubsonicClient) -> Result { let intents = GatewayIntents::non_privileged() | GatewayIntents::MESSAGE_CONTENT; let framework = StandardFramework::new() - .configure(|c| c.prefix("~")) .group(&GENERAL_GROUP) .after(after_hook); + framework.configure(|c| c.prefix("~")); + let client = DiscordClient::builder(&self.discord_token, intents) .event_handler(Handler) .framework(framework) diff --git a/src/discord.rs b/src/discord.rs index c3d82ba..0fb279e 100644 --- a/src/discord.rs +++ b/src/discord.rs @@ -1,5 +1,6 @@ use anyhow::{anyhow, Context as ErrContext, Result}; use serenity::{ + all::{ChannelId, GuildId}, async_trait, client::{Context, EventHandler}, framework::standard::{ @@ -7,12 +8,13 @@ use serenity::{ Args, CommandResult, }, model::{channel::Message, gateway::Ready}, - prelude::Mutex, + prelude::TypeMapKey, utils::MessageBuilder, }; use songbird::{ - input::{Input, Metadata}, - Call, + input::HttpRequest, + tracks::{Track, TrackHandle}, + Songbird, }; use sunk::{ search::{self, SearchPage}, @@ -48,37 +50,26 @@ pub async fn after_hook(ctx: &Context, msg: &Message, cmd_name: &str, error: Com } #[command] +#[only_in(guilds)] async fn leave(ctx: &Context, msg: &Message) -> CommandResult { - let guild = msg.guild(&ctx.cache).unwrap(); - let guild_id = guild.id; - - let manager = songbird::get(ctx) - .await - .ok_or_else(|| anyhow!("Couldn't start manager"))?; - - manager.remove(guild_id).await?; + let guild = get_guild(ctx, msg)?; + let manager = get_manager(ctx).await?; + manager.remove(guild).await?; Ok(()) } #[command] +#[only_in(guilds)] async fn join(ctx: &Context, msg: &Message) -> CommandResult { - let guild = msg.guild(&ctx.cache).unwrap(); - let guild_id = guild.id; - - let channel = guild - .voice_states - .get(&msg.author.id) - .and_then(|voice_state| voice_state.channel_id) - .ok_or_else(|| anyhow!("You must be in a voice channel to use this command"))?; - - let manager = songbird::get(ctx) - .await - .ok_or_else(|| anyhow!("Couldn't start manager"))?; - let _handler = manager.join(guild_id, channel).await; + let guild = get_guild(ctx, msg)?; + let channel = get_channel(ctx, msg)?; + let manager = get_manager(ctx).await?; + let _handler = manager.join(guild, channel).await; Ok(()) } #[command] +#[only_in(guilds)] #[aliases(s, p, play)] /// Play a named song async fn song(ctx: &Context, msg: &Message, args: Args) -> CommandResult { @@ -98,6 +89,7 @@ async fn song(ctx: &Context, msg: &Message, args: Args) -> CommandResult { let song = result .first() + .map(ToOwned::to_owned) .ok_or_else(|| anyhow!("No song matching search found"))?; log::info!("Found song {song:?}"); @@ -107,6 +99,7 @@ async fn song(ctx: &Context, msg: &Message, args: Args) -> CommandResult { } #[command] +#[only_in(guilds)] #[aliases(a, album)] /// Play a named album async fn album(ctx: &Context, msg: &Message, args: Args) -> CommandResult { @@ -128,13 +121,14 @@ async fn album(ctx: &Context, msg: &Message, args: Args) -> CommandResult { .ok_or_else(|| anyhow!("No albums matching search found"))?; for song in album.songs(music_client).await? { - queue_song(ctx, msg, &song, music_client).await?; + queue_song(ctx, msg, song, music_client).await?; } Ok(()) } #[command] +#[only_in(guilds)] #[aliases(r, rand)] /// Play a random song async fn random(ctx: &Context, msg: &Message) -> CommandResult { @@ -145,8 +139,9 @@ async fn random(ctx: &Context, msg: &Message) -> CommandResult { let result = Song::random(music_client, 1).await?; - let song = result + let song: Song = result .first() + .map(ToOwned::to_owned) .ok_or_else(|| anyhow!("No song matching search found"))?; queue_song(ctx, msg, song, music_client).await?; @@ -154,9 +149,14 @@ async fn random(ctx: &Context, msg: &Message) -> CommandResult { } #[command] +#[only_in(guilds)] /// Skip current song async fn skip(ctx: &Context, msg: &Message) -> CommandResult { - let call = get_call(ctx, msg).await?; + let manager = get_manager(ctx).await?; + let guild = get_guild(ctx, msg)?; + let call = manager + .get(guild) + .ok_or_else(|| anyhow!("Not in a voice channel"))?; let handler = call.lock().await; let queue = handler.queue(); @@ -168,9 +168,14 @@ async fn skip(ctx: &Context, msg: &Message) -> CommandResult { } #[command] +#[only_in(guilds)] /// Clear queue and stop playing async fn stop(ctx: &Context, msg: &Message) -> CommandResult { - let call = get_call(ctx, msg).await?; + let manager = get_manager(ctx).await?; + let guild = get_guild(ctx, msg)?; + let call = manager + .get(guild) + .ok_or_else(|| anyhow!("Not in a voice channel"))?; let handler = call.lock().await; let queue = handler.queue(); @@ -182,9 +187,14 @@ async fn stop(ctx: &Context, msg: &Message) -> CommandResult { } #[command] +#[only_in(guilds)] /// Pause playing current song async fn pause(ctx: &Context, msg: &Message) -> CommandResult { - let call = get_call(ctx, msg).await?; + let manager = get_manager(ctx).await?; + let guild = get_guild(ctx, msg)?; + let call = manager + .get(guild) + .ok_or_else(|| anyhow!("Not in a voice channel"))?; let handler = call.lock().await; let queue = handler.queue(); @@ -199,10 +209,15 @@ async fn pause(ctx: &Context, msg: &Message) -> CommandResult { } #[command] +#[only_in(guilds)] #[aliases(resume)] /// Resume playing current song async fn resume(ctx: &Context, msg: &Message) -> CommandResult { - let call = get_call(ctx, msg).await?; + let manager = get_manager(ctx).await?; + let guild = get_guild(ctx, msg)?; + let call = manager + .get(guild) + .ok_or_else(|| anyhow!("Not in a voice channel"))?; let handler = call.lock().await; let queue = handler.queue(); @@ -214,10 +229,15 @@ async fn resume(ctx: &Context, msg: &Message) -> CommandResult { } #[command] +#[only_in(guilds)] #[aliases(nowplaying, now, np, playing)] /// Show currently playing song async fn nowplaying(ctx: &Context, msg: &Message) -> CommandResult { - let call = get_call(ctx, msg).await?; + let manager = get_manager(ctx).await?; + let guild = get_guild(ctx, msg)?; + let call = manager + .get(guild) + .ok_or_else(|| anyhow!("Not in a voice channel"))?; let handler = call.lock().await; let queue = handler.queue(); @@ -225,17 +245,27 @@ async fn nowplaying(ctx: &Context, msg: &Message) -> CommandResult { .current() .ok_or_else(|| anyhow!("Not currently playing"))?; - msg.reply(&ctx.http, song_message(None, current.metadata())) - .await?; + let song = get_song(¤t).await?; + let text = format!( + "**Currently playing**: {} - {}", + song.title, + song.artist.as_deref().unwrap_or_default() + ); + msg.reply(&ctx.http, text).await?; Ok(()) } #[command] +#[only_in(guilds)] #[aliases(q)] /// Show song queue async fn queue(ctx: &Context, msg: &Message) -> CommandResult { - let call = get_call(ctx, msg).await?; + let manager = get_manager(ctx).await?; + let guild = get_guild(ctx, msg)?; + let call = manager + .get(guild) + .ok_or_else(|| anyhow!("Not in a voice channel"))?; let handler = call.lock().await; let current_queue = handler.queue().current_queue(); @@ -246,7 +276,14 @@ async fn queue(ctx: &Context, msg: &Message) -> CommandResult { let mut text = String::new(); text.push_str("Next songs in queue:\n"); for (i, track) in current_queue.iter().enumerate() { - text.push_str(&song_message(Some(i), track.metadata())); + let song = get_song(track).await?; + let song_text = format!( + "**{}.** {} - {}", + i, + song.title, + song.artist.as_deref().unwrap_or_default() + ); + text.push_str(&song_text); text.push('\n'); } text @@ -257,74 +294,58 @@ async fn queue(ctx: &Context, msg: &Message) -> CommandResult { } #[command] +#[only_in(guilds)] /// Remove song from queue, given id async fn remove(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult { - let call = get_call(ctx, msg).await?; + let manager = get_manager(ctx).await?; + let guild = get_guild(ctx, msg)?; + let call = manager + .get(guild) + .ok_or_else(|| anyhow!("Not in a voice channel"))?; let handler = call.lock().await; let index = args.single()?; let queue = handler.queue(); - let current_queue = queue.current_queue(); - - let track = current_queue - .get(index) - .ok_or_else(|| anyhow!("No song with that index"))?; - - queue.dequeue(index); - - let metadata = track.metadata(); - let text = format!( - "Removed track: {} - {} ", - metadata.artist.to_owned().unwrap_or_default(), - metadata.track.to_owned().unwrap_or_default(), - ); + let track = queue + .dequeue(index) + .ok_or_else(|| anyhow!("Song not found"))?; + let song = get_song(&track).await?; + let text = format!("Removed track: {}", song.title); msg.reply(&ctx.http, text).await?; Ok(()) } -fn song_message(index: Option, metadata: &Metadata) -> String { - let prefix = match index { - Some(i) => format!("{}. ", i), - None => "Current: ".into(), - }; +// ========================== +// ========================== +// ========================== - let duration = metadata - .duration - .map(|d| format!("{}:{}", d.as_secs() / 60, d.as_secs() % 60)); - - let song_info = format!( - "{} - {} ({})", - metadata.artist.to_owned().unwrap_or_default(), - metadata.track.to_owned().unwrap_or_default(), - duration.unwrap_or_default(), - ); - - MessageBuilder::new() - .push_bold(prefix) - .push(song_info) - .build() +struct SongHandler; +impl TypeMapKey for SongHandler { + type Value = Song; } -async fn queue_song( - ctx: &Context, - msg: &Message, - song: &Song, - client: &sunk::Client, -) -> Result<()> { - log::info!("Queueing song {song:?}"); - let call = get_call(ctx, msg).await?; - log::info!("Will play in call {call:?}"); +async fn queue_song(ctx: &Context, msg: &Message, song: Song, client: &sunk::Client) -> Result<()> { + let manager = get_manager(ctx).await?; + let guild = get_guild(ctx, msg)?; + let call = manager + .get(guild) + .ok_or_else(|| anyhow!("Not in a voice channel"))?; let mut handler = call.lock().await; - let input = load_song(song, client).await?; - handler.enqueue_source(input); let song_info = format!( "{} - {} ", - song.artist.to_owned().unwrap_or_default(), + song.artist.as_deref().unwrap_or_default(), song.title, ); + let track = load_song(&song, client).await?; + let track_handle = handler.enqueue(track).await; + { + let mut type_map = track_handle.typemap().write().await; + type_map.insert::(song) + } + msg.reply( &ctx.http, &MessageBuilder::new() @@ -340,26 +361,43 @@ async fn queue_song( Ok(()) } -async fn get_call(ctx: &Context, msg: &Message) -> Result>> { - let guild = msg - .guild(&ctx.cache) - .ok_or_else(|| anyhow!("Couldn't get guild id"))?; +fn get_guild(ctx: &Context, msg: &Message) -> Result { + let guild = msg.guild(&ctx.cache).unwrap(); + Ok(guild.id) +} + +fn get_channel(ctx: &Context, msg: &Message) -> Result { + let guild = msg.guild(&ctx.cache).unwrap(); + let channel_id = guild + .voice_states + .get(&msg.author.id) + .and_then(|voice_state| voice_state.channel_id) + .ok_or_else(|| anyhow!("You must be in a voice channel to use this command"))?; + + Ok(channel_id) +} + +async fn get_song(track: &TrackHandle) -> Result { + let song = track + .typemap() + .read() + .await + .get::() + .map(ToOwned::to_owned) + .ok_or_else(|| anyhow!("Sound information not found"))?; + Ok(song) +} + +async fn get_manager(ctx: &Context) -> Result> { let manager = songbird::get(ctx) .await .ok_or_else(|| anyhow!("Couldn't start manager"))?; - Ok(manager - .get(guild.id) - .ok_or_else(|| anyhow!("Not currently in a channel"))?) + Ok(manager) } -async fn load_song(song: &Song, client: &sunk::Client) -> Result { +async fn load_song(song: &Song, client: &sunk::Client) -> Result { log::info!("Loading song {song:?}"); let url = song.stream_url(client)?; - log::info!("With url {url:?}"); - let mut input = songbird::ffmpeg(&url).await?; - log::info!("Created songbird input"); - input.metadata.track = Some(song.title.clone()); - input.metadata.artist = song.artist.clone(); - input.metadata.source_url = Some(url); - Ok(input) + let track: Track = HttpRequest::new(client.reqclient.clone(), url).into(); + Ok(track) }