Updated Grid

This commit is contained in:
2026-04-08 09:15:01 -04:00
parent ca95582d92
commit a900e5e96a
45 changed files with 2731 additions and 429 deletions

View File

@@ -69,7 +69,7 @@ pub async fn run(ctx: &Context, command: &CommandInteraction) {
"<{guild_id}> Play command executed on channel {channel_id} with track: {track_url:?}"
);
// Handle the track url
match enqueue_track(manager, guild_id.to_owned(), track_url).await {
match enqueue_track(manager, guild_id.to_owned(), track_url, false).await {
Ok(items) => {
let mut message = format!("Added {} tracks", items.len());
if items.is_empty() {
@@ -103,45 +103,59 @@ pub async fn enqueue_track(
manager: &Arc<Songbird>,
guild_id: GuildId,
track_url: &str,
loop_enabled: bool,
) -> Result<Vec<YtDlpItem>> {
let mut playlist_items: Vec<YtDlpItem> = Vec::new();
if let Some(handler_lock) = manager.get(guild_id) {
let mut handler = handler_lock.lock().await;
let guild = GuildCache::find_by_id(guild_id.get() as i64).await.unwrap();
let valid = is_valid_url(track_url);
// Validate URL before doing any I/O
if !is_valid_url(track_url) {
log::warn!("<{guild_id}> Invalid track url: {}", track_url);
return Err(Error::new(422, format!("Invalid track url: {}", track_url)));
}
// Check if the URL is valid
if !valid {
log::warn!("<{guild_id}> Invalid track url: {}", track_url);
return Err(Error::new(422, format!("Invalid track url: {}", track_url)));
}
// Verify there is an active voice session
if manager.get(guild_id).is_none() {
return Ok(Vec::new());
}
playlist_items = get_ytdlp_items(track_url)?;
// Fetch yt-dlp metadata
let playlist_items = get_ytdlp_items(track_url).await?;
if playlist_items.is_empty() {
return Ok(playlist_items);
}
// 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();
// Fetch guild config
let guild = GuildCache::find_by_id(guild_id.get() as i64).await.unwrap();
let volume = guild.volume as f32 / 100.0;
// Add each track to the queue
for item in &playlist_items {
let volume = guild.volume as f32 / 100.0;
let http_client = get_client();
// Store track metadata
let track_infos: Vec<TrackInfo> = playlist_items
.iter()
.map(|item| TrackInfo {
title: item.get_title().to_owned(),
url: item.get_url().to_owned(),
duration_secs: item.get_duration(),
loop_enabled,
})
.collect();
enqueue_tracks(guild_id.get(), track_infos);
// Enqueue the tracks
let http_client = get_client();
for item in &playlist_items {
if let Some(handler_lock) = manager.get(guild_id) {
let source = YoutubeDl::new(http_client.to_owned(), item.get_url().to_owned());
let input: Input = source.into();
let track_title = item.get_title().to_owned();
let mut handler = handler_lock.lock().await;
let track_handle: TrackHandle = handler.enqueue_input(input).await;
// Set the volume
let _ = track_handle.set_volume(volume);
log::debug!("<{guild_id}> Added track: {}", track_title);
if let Err(err) = track_handle.set_volume(volume) {
log::warn!("Failed to set volume for track {}: {}", track_title, err);
};
if loop_enabled {
if let Err(err) = track_handle.enable_loop() {
log::warn!("Failed to enable loop for track {}: {}", track_title, err);
};
}
handler.remove_all_global_events();
handler.add_global_event(
Event::Track(TrackEvent::End),
@@ -150,25 +164,23 @@ pub async fn enqueue_track(
call: manager.clone(),
},
);
}
// 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();
// Release the lock
drop(handler);
log::debug!("<{guild_id}> Added track: {}", track_title);
}
}
Ok(playlist_items)
}
pub fn get_ytdlp_items(url: &str) -> Result<Vec<YtDlpItem>> {
pub async fn get_ytdlp_items(url: &str) -> Result<Vec<YtDlpItem>> {
let output = YtDlp::new()
.arg("--flat-playlist")
.arg("--dump-json")
.arg("--no-check-formats")
.arg(url)
.execute()?;
.execute()
.await?;
// Check if yt-dlp exited successfully; log stderr if not
if !output.status.success() {

View File

@@ -13,6 +13,10 @@ use std::{
pub struct TrackInfo {
pub title: String,
pub url: String,
/// Total duration in seconds, if known (from yt-dlp metadata).
pub duration_secs: Option<f64>,
/// Whether this track should loop indefinitely.
pub loop_enabled: bool,
}
/// Global map of guild_id → ordered queue of TrackInfo.
@@ -21,9 +25,7 @@ static TRACK_QUEUES: OnceLock<Arc<DashMap<u64, VecDeque<TrackInfo>>>> = OnceLock
/// Call once from the `ready` event handler to initialise the store.
pub fn init_track_queues() {
TRACK_QUEUES
.set(Arc::new(DashMap::new()))
.ok();
TRACK_QUEUES.set(Arc::new(DashMap::new())).ok();
}
/// Returns a reference to the global TRACK_QUEUES map.
@@ -61,9 +63,7 @@ pub fn clear_queue(guild_id: u64) {
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()
})
.map(|q: dashmap::mapref::one::Ref<u64, VecDeque<TrackInfo>>| q.iter().cloned().collect())
.unwrap_or_default()
}
@@ -86,3 +86,61 @@ pub async fn get_is_paused(guild_id: u64) -> bool {
}
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
}

View File

@@ -44,12 +44,10 @@ pub async fn run(ctx: &Context, command: &CommandInteraction) {
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;
handler
.queue()
.skip()
.map_err(|e| e.to_string())?;
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());
drop(handler);
Ok(())
} else {
Err("No active audio session in this guild".to_string())