Updates to pages
This commit is contained in:
@@ -20,3 +20,4 @@ chrono = { workspace = true }
|
||||
regex = { workspace = true }
|
||||
uuid = { workspace = true }
|
||||
lazy_static = { workspace = true }
|
||||
dashmap = { workspace = true }
|
||||
|
||||
@@ -11,6 +11,7 @@ use std::sync::Arc;
|
||||
pub mod mute;
|
||||
pub mod pause;
|
||||
pub mod play;
|
||||
pub mod queue;
|
||||
pub mod resume;
|
||||
pub mod skip;
|
||||
pub mod stop;
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
use super::{is_valid_url, join_voice_channel, leave_voice_channel};
|
||||
use super::{
|
||||
is_valid_url,
|
||||
join_voice_channel,
|
||||
leave_voice_channel,
|
||||
queue::{TrackInfo, enqueue_tracks, pop_front},
|
||||
};
|
||||
use crate::{
|
||||
chat::{create_message_response, edit_response, process_message},
|
||||
error::{Error, Result},
|
||||
@@ -113,6 +118,15 @@ pub async fn enqueue_track(
|
||||
|
||||
playlist_items = get_ytdlp_items(track_url)?;
|
||||
|
||||
// Collect TrackInfo for the queue store before borrowing `item` in the loop
|
||||
let track_infos: Vec<TrackInfo> = playlist_items
|
||||
.iter()
|
||||
.map(|item| TrackInfo {
|
||||
title: item.get_title().to_owned(),
|
||||
url: item.get_url().to_owned(),
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Add each track to the queue
|
||||
for item in &playlist_items {
|
||||
let volume = guild.volume as f32 / 100.0;
|
||||
@@ -137,6 +151,10 @@ pub async fn enqueue_track(
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// Store track metadata so the REST API can expose queue info
|
||||
enqueue_tracks(guild_id.get(), track_infos);
|
||||
|
||||
if handler.queue().is_empty() {
|
||||
let _ = handler.queue().resume();
|
||||
}
|
||||
@@ -204,6 +222,9 @@ struct TrackEndNotifier {
|
||||
impl EventHandler for TrackEndNotifier {
|
||||
async fn act(&self, ctx: &songbird::events::EventContext<'_>) -> Option<songbird::events::Event> {
|
||||
if let songbird::EventContext::Track(_track_list) = ctx {
|
||||
// Remove the finished track from our metadata store
|
||||
pop_front(self.guild_id.get());
|
||||
|
||||
if let Some(call) = self.call.get(self.guild_id) {
|
||||
let mut handler = call.lock().await;
|
||||
if handler.queue().is_empty() {
|
||||
|
||||
88
crates/siren-bot/src/commands/audio/queue.rs
Normal file
88
crates/siren-bot/src/commands/audio/queue.rs
Normal file
@@ -0,0 +1,88 @@
|
||||
use crate::handler::get_songbird;
|
||||
use dashmap::DashMap;
|
||||
use serde::Serialize;
|
||||
use serenity::model::prelude::GuildId;
|
||||
use songbird::tracks::PlayMode;
|
||||
use std::{
|
||||
collections::VecDeque,
|
||||
sync::{Arc, OnceLock},
|
||||
};
|
||||
|
||||
/// Metadata for a single track stored in our queue.
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct TrackInfo {
|
||||
pub title: String,
|
||||
pub url: String,
|
||||
}
|
||||
|
||||
/// Global map of guild_id → ordered queue of TrackInfo.
|
||||
/// Initialised once by the bot handler's `ready` event.
|
||||
static TRACK_QUEUES: OnceLock<Arc<DashMap<u64, VecDeque<TrackInfo>>>> = OnceLock::new();
|
||||
|
||||
/// Call once from the `ready` event handler to initialise the store.
|
||||
pub fn init_track_queues() {
|
||||
TRACK_QUEUES
|
||||
.set(Arc::new(DashMap::new()))
|
||||
.ok();
|
||||
}
|
||||
|
||||
/// Returns a reference to the global TRACK_QUEUES map.
|
||||
fn queues() -> &'static Arc<DashMap<u64, VecDeque<TrackInfo>>> {
|
||||
TRACK_QUEUES
|
||||
.get()
|
||||
.expect("TRACK_QUEUES not initialised – call init_track_queues() in the ready handler")
|
||||
}
|
||||
|
||||
/// Append one or more tracks to the end of a guild's queue.
|
||||
pub fn enqueue_tracks(guild_id: u64, tracks: Vec<TrackInfo>) {
|
||||
let mut entry = queues().entry(guild_id).or_default();
|
||||
for t in tracks {
|
||||
entry.push_back(t);
|
||||
}
|
||||
}
|
||||
|
||||
/// Remove and return the front track (called when a track finishes).
|
||||
pub fn pop_front(guild_id: u64) -> Option<TrackInfo> {
|
||||
queues()
|
||||
.get_mut(&guild_id)
|
||||
.and_then(|mut q: dashmap::mapref::one::RefMut<u64, VecDeque<TrackInfo>>| q.pop_front())
|
||||
}
|
||||
|
||||
/// Clear the entire queue for a guild (called on stop).
|
||||
pub fn clear_queue(guild_id: u64) {
|
||||
if let Some(mut q) = queues().get_mut(&guild_id) {
|
||||
let q: &mut VecDeque<TrackInfo> = q.value_mut();
|
||||
q.clear();
|
||||
}
|
||||
}
|
||||
|
||||
/// Return a snapshot of the current queue for a guild.
|
||||
/// Index 0 is the currently-playing track, index 1+ are upcoming.
|
||||
pub fn get_queue(guild_id: u64) -> Vec<TrackInfo> {
|
||||
queues()
|
||||
.get(&guild_id)
|
||||
.map(|q: dashmap::mapref::one::Ref<u64, VecDeque<TrackInfo>>| {
|
||||
q.iter().cloned().collect()
|
||||
})
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Returns `true` if the bot is currently paused in the given guild.
|
||||
/// Encapsulates the songbird dependency so `siren-api` doesn't need it directly.
|
||||
pub async fn get_is_paused(guild_id: u64) -> bool {
|
||||
let manager = get_songbird();
|
||||
let serenity_guild_id = GuildId::from(guild_id);
|
||||
if let Some(handler_lock) = manager.get(serenity_guild_id) {
|
||||
let handler = handler_lock.lock().await;
|
||||
let current = handler.queue().current();
|
||||
drop(handler);
|
||||
if let Some(track) = current {
|
||||
return track
|
||||
.get_info()
|
||||
.await
|
||||
.map(|info| info.playing == PlayMode::Pause)
|
||||
.unwrap_or(false);
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
@@ -1,11 +1,14 @@
|
||||
use crate::{
|
||||
chat::{edit_response, process_message},
|
||||
commands::audio::queue::pop_front,
|
||||
handler::get_songbird,
|
||||
};
|
||||
use serenity::{
|
||||
all::{CommandInteraction, CreateCommand},
|
||||
all::{CommandInteraction, CreateCommand, GuildId},
|
||||
prelude::*,
|
||||
};
|
||||
use songbird::Songbird;
|
||||
use std::sync::Arc;
|
||||
|
||||
pub async fn run(ctx: &Context, command: &CommandInteraction) {
|
||||
// Create the initial response
|
||||
@@ -29,17 +32,27 @@ pub async fn run(ctx: &Context, command: &CommandInteraction) {
|
||||
};
|
||||
|
||||
// Skip the track
|
||||
match skip_track(manager, guild_id).await {
|
||||
Ok(_) => {
|
||||
log::debug!("<{guild_id}> Skipped the track");
|
||||
edit_response(ctx, command, "Skipping the track".to_string()).await;
|
||||
}
|
||||
Err(err) => edit_response(ctx, command, format!("Failed to skip: {}", err)).await,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn skip_track(manager: &Arc<Songbird>, guild_id: &GuildId) -> Result<(), String> {
|
||||
if let Some(handler_lock) = manager.get(guild_id.to_owned()) {
|
||||
let handler = handler_lock.lock().await;
|
||||
match handler.queue().skip() {
|
||||
Ok(_) => {
|
||||
log::debug!("<{guild_id}> Skipped the track");
|
||||
edit_response(ctx, command, "Skipping the track".to_string()).await;
|
||||
}
|
||||
Err(err) => {
|
||||
edit_response(ctx, command, format!("Failed to skip: {}", err)).await;
|
||||
}
|
||||
}
|
||||
handler
|
||||
.queue()
|
||||
.skip()
|
||||
.map_err(|e| e.to_string())?;
|
||||
// Pop the current track from our metadata store; the next track (if any) moves to front
|
||||
pop_front(guild_id.get());
|
||||
Ok(())
|
||||
} else {
|
||||
Err("No active audio session in this guild".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
use crate::{
|
||||
chat::{edit_response, process_message},
|
||||
commands::audio::queue::clear_queue,
|
||||
handler::get_songbird,
|
||||
};
|
||||
use serenity::{
|
||||
all::{CommandInteraction, CreateCommand},
|
||||
all::{CommandInteraction, CreateCommand, GuildId},
|
||||
prelude::*,
|
||||
};
|
||||
use songbird::Songbird;
|
||||
use std::sync::Arc;
|
||||
|
||||
pub async fn run(ctx: &Context, command: &CommandInteraction) {
|
||||
// Create the initial response
|
||||
@@ -29,11 +32,23 @@ pub async fn run(ctx: &Context, command: &CommandInteraction) {
|
||||
};
|
||||
|
||||
// Stop the track and clear the queue
|
||||
if let Some(handler_lock) = manager.get(guild_id) {
|
||||
match stop_track(manager, &guild_id).await {
|
||||
Ok(_) => {
|
||||
log::debug!("<{guild_id}> Stopped the track");
|
||||
edit_response(ctx, command, "Stopping the tracks".to_string()).await;
|
||||
}
|
||||
Err(err) => edit_response(ctx, command, format!("Failed to stop: {}", err)).await,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn stop_track(manager: &Arc<Songbird>, guild_id: &GuildId) -> Result<(), String> {
|
||||
if let Some(handler_lock) = manager.get(guild_id.to_owned()) {
|
||||
let handler = handler_lock.lock().await;
|
||||
handler.queue().stop();
|
||||
log::debug!("<{guild_id}> Stopped the track");
|
||||
edit_response(ctx, command, "Stopping the tracks".to_string()).await;
|
||||
clear_queue(guild_id.get());
|
||||
Ok(())
|
||||
} else {
|
||||
Err("No active audio session in this guild".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use super::{chat::create_modal_response, commands};
|
||||
use crate::{
|
||||
HttpKey,
|
||||
commands::fun::roll::{format_roll, roll_dice, send_roll_message},
|
||||
commands::{audio::queue::init_track_queues, fun::roll::{format_roll, roll_dice, send_roll_message}},
|
||||
};
|
||||
use serenity::{
|
||||
all::{
|
||||
@@ -64,6 +64,9 @@ impl EventHandler for BotHandler {
|
||||
log::warn!("No ready guilds found");
|
||||
}
|
||||
|
||||
// Initialise the track-queue metadata store (idempotent)
|
||||
init_track_queues();
|
||||
|
||||
if SONGBIRD.get().is_none() {
|
||||
let songbird = songbird::get(&ctx).await.unwrap();
|
||||
SONGBIRD
|
||||
|
||||
Reference in New Issue
Block a user