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, /// Total duration in seconds, if known (from yt-dlp metadata). pub duration_secs: Option, /// Whether this track should loop indefinitely. pub loop_enabled: bool, } /// Global map of guild_id → ordered queue of TrackInfo. /// Initialised once by the bot handler's `ready` event. static TRACK_QUEUES: OnceLock>>> = 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>> { 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) { 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 { queues() .get_mut(&guild_id) .and_then(|mut q: dashmap::mapref::one::RefMut>| 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 = 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 { queues() .get(&guild_id) .map(|q: dashmap::mapref::one::Ref>| 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 } /// Toggle or set loop on the currently playing track pub async fn set_loop_current(guild_id: u64, enabled: bool) -> bool { // Update our metadata store first let updated = { if let Some(mut q) = queues().get_mut(&guild_id) { if let Some(front) = q.front_mut() { front.loop_enabled = enabled; true } else { false } } else { false } }; if !updated { return false; } // Tell songbird to loop / unloop the live track handle 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 { if enabled { let _ = track.enable_loop(); } else { let _ = track.disable_loop(); } return true; } } false } /// Returns the current playback position (in seconds) for the active track in the /// given guild, or `0.0` if nothing is playing or the info is unavailable. pub async fn get_current_position(guild_id: u64) -> f64 { 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.position.as_secs_f64()) .unwrap_or(0.0); } } 0.0 }