// src/commands/play.rs use super::{CommandHandler, SlashCommand}; use crate::utils::get_voice_state; // Needed if join logic is integrated use anyhow::{Context as _, Result}; use lavalink_rs::{ model::search::{SearchResult, SearchType}, LavalinkClient }; use serenity::{ builder::{CreateApplicationCommandOption, EditInteractionResponse}, client::Context, model::prelude::{ interaction::application_command::{ ApplicationCommandInteraction, CommandDataOptionValue, ApplicationCommandOptionType, }, Id, }, }; use tracing::{info, instrument}; use url::Url; use futures::future::BoxFuture; use tokio::time::Duration; // Needed for sleep if join logic integrated // Command metadata and registration inventory::submit! { SlashCommand { name: "play", description: "Plays a song from a URL or search query.", options: play_options, // Function pointer to build options handler: handle_play, } } // Function to define options for the /play command fn play_options() -> Vec { vec![ CreateApplicationCommandOption::new( ApplicationCommandOptionType::String, "query", "The URL or search query for the song." ) .required(true) ] } // Helper to join channel if not already in one (avoids duplicating join logic) // Note: This is slightly simplified, might need more robust error handling from join async fn ensure_connected( ctx: &Context, interaction: &ApplicationCommandInteraction, guild_id: serenity::model::id::GuildId, lavalink: &LavalinkClient, ) -> Result<()> { if lavalink.get_player(guild_id).is_some() { return Ok(()); // Already connected } info!("Play command used but bot not connected. Attempting auto-join."); let user_id = interaction.user.id; let voice_state = get_voice_state(ctx, guild_id, user_id).await .context("Bot not connected & failed to get your voice state. Are you in a channel?")?; let channel_id = voice_state.channel_id .context("Bot not connected & you're not in a voice channel. Use /join first.")?; lavalink.create_session(guild_id, channel_id).await?; // Give a moment for connection events to process tokio::time::sleep(Duration::from_secs(1)).await; // Check if connection succeeded lavalink.get_player(guild_id) .context("Failed to establish voice connection after attempting to join.")?; info!("Auto-join successful for play command."); Ok(()) } // The actual logic for the /play command #[instrument(skip(ctx, interaction, lavalink))] fn handle_play<'a>( ctx: &'a Context, interaction: &'a ApplicationCommandInteraction, lavalink: &'a LavalinkClient, ) -> BoxFuture<'a, Result<()>> { Box::pin(async move { let guild_id = interaction.guild_id.context("Command must be used in a guild")?; // Ensure the bot is connected, joining if necessary ensure_connected(ctx, interaction, guild_id, lavalink).await?; // Get the query string from the interaction options let query = interaction.data.options.iter() .find(|opt| opt.name == "query") .and_then(|opt| opt.value.as_ref()) .and_then(|v| v.as_str()) // Use as_str for simplicity .context("Missing 'query' option")?; info!("Searching for: {}", query); // Determine search type let search_type = if Url::parse(query).is_ok() { SearchType::Track(query.to_string()) } else { SearchType::YouTube(query.to_string()) }; // Perform the search let search_result = lavalink.search_tracks(search_type).await?; // Process results (simplified from previous version for brevity) let (tracks_to_play, response_prefix) = match search_result { SearchResult::Track(track) => { info!("Found track: {}", track.info.title); (vec![track], "Now playing:".to_string()) } SearchResult::Playlist(playlist) => { info!("Found playlist: {} ({} tracks)", playlist.info.name, playlist.tracks.len()); (playlist.tracks, format!("Queued playlist **{}**: Playing first track:", playlist.info.name)) } SearchResult::Search(tracks) => { if let Some(track) = tracks.first() { info!("Found search result: {}", track.info.title); (vec![track.clone()], "Now playing:".to_string()) } else { (vec![], String::new()) } // No tracks found } SearchResult::Empty => (vec![], String::new()), // No tracks found SearchResult::LoadFailed(e) => { return Err(anyhow::anyhow!("Failed to load track: {}", e.message)); } }; if tracks_to_play.is_empty() { interaction.edit_original_response(&ctx.http, EditInteractionResponse::new().content(format!("No results found for: `{}`", query)) ).await?; return Ok(()); } // Get the player (should exist after ensure_connected) let player = lavalink.get_player(guild_id) .context("Player disappeared unexpectedly after connecting.")?; // Play first track, queue the rest let first_track = tracks_to_play.first().unwrap(); // Safe due to is_empty check player.play_track(&first_track.track).await?; let mut final_response = format!("{} **{}** by **{}**", response_prefix, first_track.info.title, first_track.info.author); if tracks_to_play.len() > 1 { for track in tracks_to_play.iter().skip(1) { player.queue(&track.track).await?; } final_response.push_str(&format!("\n...and queued {} other tracks.", tracks_to_play.len() - 1)); } // Update interaction response interaction.edit_original_response(&ctx.http, EditInteractionResponse::new().content(final_response) ).await?; Ok(()) }) }