use std::sync::Arc; use serenity::all::{CommandInteraction, CommandOptionType, CreateCommand, CreateCommandOption}; use serenity::model::prelude::GuildId; use serenity::{prelude::*, async_trait}; use songbird::input::{Input, YoutubeDl}; use songbird::tracks::TrackHandle; use songbird::{Event, EventHandler, Songbird, TrackEvent}; use crate::data::guilds::GuildCache; use crate::bot::ytdlp::{YtDlp, YtDlpItem}; use crate::error::{SirenResult, Error as SirenError}; use crate::{signal_shutdown, HttpKey}; use super::{get_songbird, is_valid_url, join_voice_channel}; use crate::bot::chat::{create_message_response, edit_response, process_message}; pub async fn run(ctx: &Context, command: &CommandInteraction) { // Process the command options let track_url = match command.data.options.first() { Some(o) => o.value.as_str().unwrap(), None => { log::warn!( "{} attempted to play a track without a track option", command.user.id.get() ); create_message_response(&ctx, &command, "Track option is missing".to_string(), false).await; return; } }; // Create the initial response process_message(&ctx, &command, false).await; // Get the songbird manager let manager = get_songbird(ctx).await; // Extract the guild ID let guild_id = match &command.guild_id { Some(guild_id) => guild_id, None => { edit_response( &ctx, &command, "Unable to find the current server ID".to_string(), ) .await; return; } }; // Join the user's voice channel match join_voice_channel(&ctx.cache, &manager, guild_id, &command.user).await { Ok(channel_id) => { log::debug!( "<{guild_id}> Play command executed on channel {channel_id} with track: {track_url:?}" ); // Handle the track url match enqueue_track(ctx, manager, guild_id.to_owned(), track_url).await { Ok(items) => { let mut message = format!("Added {} tracks", items.len()); if items.len() == 0 { message = "No tracks were played".to_string(); } else if items.len() == 1 { message = format!("Added **{}**", items[0].get_title()); } edit_response(&ctx, &command, message).await; } Err(err) => { log::warn!("Failed to play track: {}", err); edit_response(&ctx, &command, format!("Failed to play track: {}", err)).await; } }; } Err(err) => { log::warn!("<{guild_id}> Failed to join voice channel: {}", err); edit_response(&ctx, &command, format!("{}", err)).await; } } } pub async fn enqueue_track( ctx: &Context, manager: Arc, guild_id: GuildId, track_url: &str, ) -> SirenResult> { let mut playlist_items: Vec = Vec::new(); if let Some(handler_lock) = manager.get(guild_id) { let mut handler = handler_lock.lock().await; let guild = GuildCache::get_by_id(guild_id.get() as i64).await?.unwrap(); let valid = is_valid_url(&track_url); // Check if the URL is valid if !valid { log::warn!("<{guild_id}> Invalid track url: {}", track_url); return Err(SirenError::new( 422, format!("Invalid track url: {}", track_url), )); } playlist_items = match get_ytdlp_items(&track_url) { Ok(items) => items, Err(err) => { log::warn!("<{guild_id}> Failed to get playlist urls: {}", err); return Err(SirenError::new(422, err.to_string())); } }; // Add each track to the queue for item in &playlist_items { let volume = guild.volume as f32 / 100.0; let http_client = { let data = ctx.data.read().await; data .get::() .cloned() .expect("Guaranteed to exist in the typemap.") }; let source = YoutubeDl::new(http_client, item.get_url().to_owned()); let input: Input = source.into(); let track_title = item.get_title().to_owned(); let track_handle: TrackHandle; track_handle = handler.enqueue_input(input).await; // Set the volume let _ = track_handle.set_volume(volume); log::debug!("<{guild_id}> Added track: {}", track_title); handler.remove_all_global_events(); handler.add_global_event( Event::Track(TrackEvent::End), TrackEndNotifier { guild_id, call: manager.clone(), }, ); } if handler.queue().is_empty() { let _ = handler.queue().resume(); } } Ok(playlist_items) } pub fn get_ytdlp_items(url: &str) -> SirenResult> { let output = YtDlp::new() .arg("--flat-playlist") .arg("--dump-json") .arg(url) .execute()?; let items: Vec = String::from_utf8(output.stdout)? .split('\n') .filter_map(|line| { if line.is_empty() { None } else { Some( serde_json::from_slice::(line.as_bytes()) .map_err(|err| SirenError::new(500, err.to_string())), ) } }) .filter_map(|parsed| match parsed { Ok(item) => Some(item), Err(err) => { log::warn!("Failed to parse playlist item: {}", err); None } }) .collect(); Ok(items) } pub fn register() -> CreateCommand { CreateCommand::new("play") .description("Plays the given track") .add_option( CreateCommandOption::new(CommandOptionType::String, "track", "The track to be played") .required(true), ) } struct TrackEndNotifier { pub call: Arc, pub guild_id: GuildId, } #[async_trait] impl EventHandler for TrackEndNotifier { async fn act(&self, ctx: &songbird::events::EventContext<'_>) -> Option { if let songbird::EventContext::Track(_track_list) = ctx { if let Some(call) = self.call.get(self.guild_id) { let mut handler = call.lock().await; if handler.queue().is_empty() { log::debug!("Queue is empty, leaving voice channel"); handler.leave().await.unwrap(); } } } None } }