diff --git a/Cargo.toml b/Cargo.toml index 8ba936b..493ee1c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,6 +12,7 @@ tracing-subscriber = "0.3.16" tracing-futures = "0.2.5" openai = "1.0.0-alpha.6" rand = "0.8.5" +thiserror = "1.0.39" [dependencies.songbird] version = "0.3.0" diff --git a/src/commands.rs b/src/commands.rs index cdcdcf8..e87b1c4 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -1,6 +1,5 @@ use anyhow::{Context, Result}; use poise::serenity_prelude::{EmbedMessageBuilding, MessageBuilder}; -use songbird::create_player; use tracing::{debug, log::warn}; use crate::{personality, CommandContext, Error}; @@ -116,10 +115,86 @@ pub async fn play( response.edit(ctx, |r| r.content(msg.build())).await?; - let (audio, track_handle) = create_player(source); - let mut currently_playing = ctx.data().currently_playing.lock(); - *currently_playing = Some(track_handle); - handler.play_only(audio); + let mut queue = ctx.data().queue.lock(); + if !queue.is_empty() { + let _ = queue.stop(); + } + queue.add_next(source, &mut handler); + queue.resume()?; + } else { + ctx.say("Neither of us are in a voice channel, silly.") + .await?; + } + + Ok(()) +} + +#[poise::command(slash_command)] +pub async fn queue( + ctx: CommandContext<'_>, + #[description = "The URL of the song to play"] url: Option, +) -> Result<(), Error> { + let Some(url) = url else { + ctx.say("You need to give me a URL to play!").await?; + return Ok(()); + }; + + let Some(guild) = ctx.guild() else { + ctx.say("You're not in a server, silly.").await?; + return Ok(()); + }; + + let manager = songbird::get(ctx.serenity_context()) + .await + .context("Expected a songbird manager")? + .clone(); + + if manager.get(guild.id).is_none() { + let Some(Some(channel_id)) = guild.voice_states.get(&ctx.author().id).map(|vs| vs.channel_id) else { + ctx.say("Neither of us are in a voice channel, silly.").await?; + return Ok(()); + }; + let _handler = manager.join(guild.id, channel_id).await; + } + + if let Some(handler_lock) = manager.get(guild.id) { + let mut msg = MessageBuilder::new(); + msg.push_line(String::from(personality::get_random_loading_message())) + .push_named_link( + "", + "https://media.giphy.com/media/H1dxi6xdh4NGQCZSvz/giphy.gif", + ); + let response = ctx.send(|r| r.content(msg.build())).await?; + + let mut handler = handler_lock.lock().await; + + debug!("Trying to play: {}", url); + let source = songbird::ytdl(&url).await?; + debug!("Playing: {:?}", source.metadata); + let title = source + .metadata + .title + .clone() + .unwrap_or(String::from("This video")); + + let mut msg = MessageBuilder::new(); + + // Optional sassy commentary! + match personality::get_sassy_commentary(&title).await { + Ok(commentary) => { + msg.push_line(&commentary).push_line(""); + } + Err(e) => { + warn!("Failed to get sassy commentary for \"{title}\": {e}"); + } + }; + + msg.push_bold("Queued: ").push_named_link(title, url); + + response.edit(ctx, |r| r.content(msg.build())).await?; + + let mut queue = ctx.data().queue.lock(); + queue.add_to_end(source, &mut handler); } else { ctx.say("Neither of us are in a voice channel, silly.") .await?; @@ -140,12 +215,10 @@ pub async fn stop(ctx: CommandContext<'_>) -> Result<(), Error> { .context("Expected a songbird manager")? .clone(); - if let Some(handler_lock) = manager.get(guild.id) { - let mut handler = handler_lock.lock().await; - handler.stop(); + if manager.get(guild.id).is_some() { { - let mut currently_playing = ctx.data().currently_playing.lock(); - *currently_playing = None; + let mut queue = ctx.data().queue.lock(); + queue.stop()?; } ctx.say("Alright, I guess I'll stop.").await?; } else { @@ -155,3 +228,108 @@ pub async fn stop(ctx: CommandContext<'_>) -> Result<(), Error> { Ok(()) } + +#[poise::command(slash_command)] +pub async fn skip(ctx: CommandContext<'_>) -> Result<(), Error> { + let Some(guild) = ctx.guild() else { + ctx.say("You're not in a server, silly.").await?; + return Ok(()); + }; + + let manager = songbird::get(ctx.serenity_context()) + .await + .context("Expected a songbird manager")? + .clone(); + + if manager.get(guild.id).is_some() { + { + let mut queue = ctx.data().queue.lock(); + let _ = queue.stop(); + queue.resume()?; + } + ctx.say("Skipping!").await?; + } else { + ctx.say("I'm not even in a channel to begin with. Silly.") + .await?; + } + + Ok(()) +} + +#[poise::command(slash_command)] +pub async fn pause(ctx: CommandContext<'_>) -> Result<(), Error> { + let Some(guild) = ctx.guild() else { + ctx.say("You're not in a server, silly.").await?; + return Ok(()); + }; + + let manager = songbird::get(ctx.serenity_context()) + .await + .context("Expected a songbird manager")? + .clone(); + + if manager.get(guild.id).is_some() { + { + let mut queue = ctx.data().queue.lock(); + queue.pause()?; + } + ctx.say("Pausing!").await?; + } else { + ctx.say("I'm not even in a channel to begin with. Silly.") + .await?; + } + + Ok(()) +} + +#[poise::command(slash_command)] +pub async fn resume(ctx: CommandContext<'_>) -> Result<(), Error> { + let Some(guild) = ctx.guild() else { + ctx.say("You're not in a server, silly.").await?; + return Ok(()); + }; + + let manager = songbird::get(ctx.serenity_context()) + .await + .context("Expected a songbird manager")? + .clone(); + + if manager.get(guild.id).is_some() { + { + let mut queue = ctx.data().queue.lock(); + queue.resume()?; + } + ctx.say("Resuming!").await?; + } else { + ctx.say("I'm not even in a channel to begin with. Silly.") + .await?; + } + + Ok(()) +} + +#[poise::command(slash_command)] +pub async fn clear(ctx: CommandContext<'_>) -> Result<(), Error> { + let Some(guild) = ctx.guild() else { + ctx.say("You're not in a server, silly.").await?; + return Ok(()); + }; + + let manager = songbird::get(ctx.serenity_context()) + .await + .context("Expected a songbird manager")? + .clone(); + + if manager.get(guild.id).is_some() { + { + let mut queue = ctx.data().queue.lock(); + queue.clear(); + } + ctx.say("Cleared the queue!").await?; + } else { + ctx.say("I'm not even in a channel to begin with. Silly.") + .await?; + } + + Ok(()) +} diff --git a/src/main.rs b/src/main.rs index 1519f3a..7a0e67d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,6 +2,7 @@ mod commands; mod personality; +mod queue; use commands::*; @@ -13,7 +14,7 @@ use poise::serenity_prelude::{self as serenity}; use songbird::serenity::SerenityInit; pub struct Data { - currently_playing: Arc>>, + queue: Arc>, } pub type Error = Box; pub type CommandContext<'a> = poise::Context<'a, Data, Error>; @@ -47,7 +48,18 @@ async fn main() -> Result<()> { env::var("DISCORD_TOKEN").expect("Expected a bot token in the environment: DISCORD_TOKEN"); let options = poise::FrameworkOptions { - commands: vec![register(), join(), leave(), play(), stop()], + commands: vec![ + register(), + join(), + leave(), + play(), + queue(), + stop(), + skip(), + pause(), + resume(), + clear(), + ], event_handler: |ctx, event, framework, user_data| { Box::pin(event_event_handler(ctx, event, framework, user_data)) }, @@ -63,7 +75,7 @@ async fn main() -> Result<()> { .setup(|_ctx, _data, _framework| { Box::pin(async move { Ok(Data { - currently_playing: Arc::new(Mutex::new(None)), + queue: Arc::new(Mutex::new(queue::Queue::new())), }) }) }) diff --git a/src/queue.rs b/src/queue.rs new file mode 100644 index 0000000..36dd1c1 --- /dev/null +++ b/src/queue.rs @@ -0,0 +1,102 @@ +//! Implements a queue for the bot to play songs in order + +use anyhow::Result; +use songbird::{ + input::Input, + tracks::{self, TrackHandle}, + Driver, +}; +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum QueueError { + #[error("Nothing is in the queue.")] + EmptyQueue, +} + +pub struct Queue { + queue: Vec, +} + +impl Queue { + #[must_use] + pub fn new() -> Self { + Self { queue: Vec::new() } + } + + /// Resumes the current track + pub fn resume(&mut self) -> Result<()> { + let Some(track) = self.queue.first() else { + return Err(QueueError::EmptyQueue.into()); + }; + track.play()?; + Ok(()) + } + + /// Stops the current track and removes it from the queue + pub fn stop(&mut self) -> Result<()> { + let Some(track) = self.queue.first() else { + return Err(QueueError::EmptyQueue.into()); + }; + track.stop()?; + self.queue.remove(0); + Ok(()) + } + + /// Pauses the current track + pub fn pause(&mut self) -> Result<()> { + let Some(track) = self.queue.first() else { + return Err(QueueError::EmptyQueue.into()); + }; + track.pause()?; + Ok(()) + } + + /// Adds a track to the end of the queue + pub fn add_to_end(&mut self, source: Input, handler: &mut Driver) -> TrackHandle { + let (mut track, handle) = tracks::create_player(source); + track.pause(); + self.queue.push(handle.clone()); + handler.play(track); + handle + } + + /// Adds multiple tracks to the end of the queue + pub fn add_all_to_end( + &mut self, + sources: Vec, + handler: &mut Driver, + ) -> Vec { + let mut handles = Vec::new(); + for source in sources { + handles.push(self.add_to_end(source, handler)); + } + handles + } + + /// Adds a track to play next + pub fn add_next(&mut self, source: Input, handler: &mut Driver) -> TrackHandle { + let (mut track, handle) = tracks::create_player(source); + track.pause(); + if self.queue.is_empty() { + self.queue.push(handle.clone()); + } else { + self.queue.insert(1, handle.clone()); + } + handler.play(track); + handle + } + + /// Clears the queue + pub fn clear(&mut self) { + for track in self.queue.drain(..) { + let _ = track.stop(); + } + } + + /// Returns whether the queue is empty + #[must_use] + pub fn is_empty(&self) -> bool { + self.queue.is_empty() + } +}