2025-04-20 00:49:17 +08:00

168 lines
5.7 KiB
Rust

// 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<CreateApplicationCommandOption> {
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(())
})
}