Updated Grid
This commit is contained in:
@@ -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() {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
use super::{chat::create_modal_response, commands};
|
||||
use crate::{
|
||||
HttpKey,
|
||||
commands::{audio::queue::init_track_queues, 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::{
|
||||
@@ -97,9 +100,7 @@ impl EventHandler for BotHandler {
|
||||
}
|
||||
}
|
||||
|
||||
async fn resume(&self, _: Context, _: ResumedEvent) {
|
||||
log::trace!("Resumed");
|
||||
}
|
||||
async fn resume(&self, _: Context, _: ResumedEvent) {}
|
||||
|
||||
async fn interaction_create(&self, ctx: Context, interaction: Interaction) {
|
||||
if let Interaction::Command(command) = interaction {
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
mod model;
|
||||
|
||||
pub use model::*;
|
||||
use std::process::{Child, Command, Output, Stdio};
|
||||
use std::process::{Output, Stdio};
|
||||
|
||||
const YOUTUBE_DL_COMMAND: &str = "yt-dlp";
|
||||
|
||||
pub struct YtDlp {
|
||||
command: Command,
|
||||
args: Vec<String>,
|
||||
}
|
||||
|
||||
@@ -17,29 +16,26 @@ impl Default for YtDlp {
|
||||
}
|
||||
|
||||
impl YtDlp {
|
||||
/// Create a new yt-dlp command builder
|
||||
pub fn new() -> Self {
|
||||
let mut cmd = Command::new(YOUTUBE_DL_COMMAND);
|
||||
cmd
|
||||
.env("LC_ALL", "en_US.UTF-8")
|
||||
.stdout(Stdio::piped())
|
||||
.stdin(Stdio::piped())
|
||||
.stderr(Stdio::piped());
|
||||
Self {
|
||||
command: cmd,
|
||||
args: Vec::new(),
|
||||
}
|
||||
Self { args: Vec::new() }
|
||||
}
|
||||
|
||||
/// Add an argument to the yt-dlp command
|
||||
pub fn arg(&mut self, arg: &str) -> &mut Self {
|
||||
self.args.push(arg.to_owned());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn execute(&mut self) -> std::io::Result<Output> {
|
||||
self
|
||||
.command
|
||||
.args(self.args.clone())
|
||||
.spawn()
|
||||
.and_then(Child::wait_with_output)
|
||||
/// Execute the yt-dlp command asynchronously
|
||||
pub async fn execute(&mut self) -> std::io::Result<Output> {
|
||||
tokio::process::Command::new(YOUTUBE_DL_COMMAND)
|
||||
.env("LC_ALL", "en_US.UTF-8")
|
||||
.stdout(Stdio::piped())
|
||||
.stdin(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
.args(&self.args)
|
||||
.output()
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,4 +32,11 @@ impl YtDlpItem {
|
||||
YtDlpItem::VideoItem { webpage_url, .. } => webpage_url,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_duration(&self) -> Option<f64> {
|
||||
match self {
|
||||
YtDlpItem::PlaylistItem { duration, .. } => *duration,
|
||||
YtDlpItem::VideoItem { duration, .. } => *duration,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user