168 lines
5.7 KiB
Rust
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(())
|
|
})
|
|
} |