Implement queue
This commit is contained in:
parent
eb56c024cd
commit
07618676db
4 changed files with 306 additions and 13 deletions
|
@ -12,6 +12,7 @@ tracing-subscriber = "0.3.16"
|
||||||
tracing-futures = "0.2.5"
|
tracing-futures = "0.2.5"
|
||||||
openai = "1.0.0-alpha.6"
|
openai = "1.0.0-alpha.6"
|
||||||
rand = "0.8.5"
|
rand = "0.8.5"
|
||||||
|
thiserror = "1.0.39"
|
||||||
|
|
||||||
[dependencies.songbird]
|
[dependencies.songbird]
|
||||||
version = "0.3.0"
|
version = "0.3.0"
|
||||||
|
|
198
src/commands.rs
198
src/commands.rs
|
@ -1,6 +1,5 @@
|
||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
use poise::serenity_prelude::{EmbedMessageBuilding, MessageBuilder};
|
use poise::serenity_prelude::{EmbedMessageBuilding, MessageBuilder};
|
||||||
use songbird::create_player;
|
|
||||||
use tracing::{debug, log::warn};
|
use tracing::{debug, log::warn};
|
||||||
|
|
||||||
use crate::{personality, CommandContext, Error};
|
use crate::{personality, CommandContext, Error};
|
||||||
|
@ -116,10 +115,86 @@ pub async fn play(
|
||||||
|
|
||||||
response.edit(ctx, |r| r.content(msg.build())).await?;
|
response.edit(ctx, |r| r.content(msg.build())).await?;
|
||||||
|
|
||||||
let (audio, track_handle) = create_player(source);
|
let mut queue = ctx.data().queue.lock();
|
||||||
let mut currently_playing = ctx.data().currently_playing.lock();
|
if !queue.is_empty() {
|
||||||
*currently_playing = Some(track_handle);
|
let _ = queue.stop();
|
||||||
handler.play_only(audio);
|
}
|
||||||
|
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<String>,
|
||||||
|
) -> 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 {
|
} else {
|
||||||
ctx.say("Neither of us are in a voice channel, silly.")
|
ctx.say("Neither of us are in a voice channel, silly.")
|
||||||
.await?;
|
.await?;
|
||||||
|
@ -140,12 +215,10 @@ pub async fn stop(ctx: CommandContext<'_>) -> Result<(), Error> {
|
||||||
.context("Expected a songbird manager")?
|
.context("Expected a songbird manager")?
|
||||||
.clone();
|
.clone();
|
||||||
|
|
||||||
if let Some(handler_lock) = manager.get(guild.id) {
|
if manager.get(guild.id).is_some() {
|
||||||
let mut handler = handler_lock.lock().await;
|
|
||||||
handler.stop();
|
|
||||||
{
|
{
|
||||||
let mut currently_playing = ctx.data().currently_playing.lock();
|
let mut queue = ctx.data().queue.lock();
|
||||||
*currently_playing = None;
|
queue.stop()?;
|
||||||
}
|
}
|
||||||
ctx.say("Alright, I guess I'll stop.").await?;
|
ctx.say("Alright, I guess I'll stop.").await?;
|
||||||
} else {
|
} else {
|
||||||
|
@ -155,3 +228,108 @@ pub async fn stop(ctx: CommandContext<'_>) -> Result<(), Error> {
|
||||||
|
|
||||||
Ok(())
|
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(())
|
||||||
|
}
|
||||||
|
|
18
src/main.rs
18
src/main.rs
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
mod commands;
|
mod commands;
|
||||||
mod personality;
|
mod personality;
|
||||||
|
mod queue;
|
||||||
|
|
||||||
use commands::*;
|
use commands::*;
|
||||||
|
|
||||||
|
@ -13,7 +14,7 @@ use poise::serenity_prelude::{self as serenity};
|
||||||
use songbird::serenity::SerenityInit;
|
use songbird::serenity::SerenityInit;
|
||||||
|
|
||||||
pub struct Data {
|
pub struct Data {
|
||||||
currently_playing: Arc<Mutex<Option<songbird::tracks::TrackHandle>>>,
|
queue: Arc<Mutex<queue::Queue>>,
|
||||||
}
|
}
|
||||||
pub type Error = Box<dyn std::error::Error + Send + Sync>;
|
pub type Error = Box<dyn std::error::Error + Send + Sync>;
|
||||||
pub type CommandContext<'a> = poise::Context<'a, Data, Error>;
|
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");
|
env::var("DISCORD_TOKEN").expect("Expected a bot token in the environment: DISCORD_TOKEN");
|
||||||
|
|
||||||
let options = poise::FrameworkOptions {
|
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| {
|
event_handler: |ctx, event, framework, user_data| {
|
||||||
Box::pin(event_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| {
|
.setup(|_ctx, _data, _framework| {
|
||||||
Box::pin(async move {
|
Box::pin(async move {
|
||||||
Ok(Data {
|
Ok(Data {
|
||||||
currently_playing: Arc::new(Mutex::new(None)),
|
queue: Arc::new(Mutex::new(queue::Queue::new())),
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
102
src/queue.rs
Normal file
102
src/queue.rs
Normal file
|
@ -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<TrackHandle>,
|
||||||
|
}
|
||||||
|
|
||||||
|
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<Input>,
|
||||||
|
handler: &mut Driver,
|
||||||
|
) -> Vec<TrackHandle> {
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
Loading…
Add table
Reference in a new issue