Files
siren/src/bot/commands/audio/play.rs

208 lines
6.0 KiB
Rust

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<Songbird>,
guild_id: GuildId,
track_url: &str,
) -> SirenResult<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::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::<HttpKey>()
.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<Vec<YtDlpItem>> {
let output = YtDlp::new()
.arg("--flat-playlist")
.arg("--dump-json")
.arg(url)
.execute()?;
let items: Vec<YtDlpItem> = String::from_utf8(output.stdout)?
.split('\n')
.filter_map(|line| {
if line.is_empty() {
None
} else {
Some(
serde_json::from_slice::<YtDlpItem>(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<Songbird>,
pub guild_id: GuildId,
}
#[async_trait]
impl EventHandler for TrackEndNotifier {
async fn act(&self, ctx: &songbird::events::EventContext<'_>) -> Option<songbird::events::Event> {
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
}
}