Files
siren/src/bot/commands/audio/play.rs
2024-10-13 17:38:18 -04:00

224 lines
6.7 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::bot::commands::audio::leave_voice_channel;
use crate::data::guilds::GuildCache;
use crate::bot::ytdlp::{PlaylistItem, YtDlp};
use crate::error::{SirenResult, Error as SirenError};
use crate::HttpKey;
use super::{
create_response, edit_response, get_songbird, is_valid_url, join_voice_channel, 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_response(&ctx, &command, format!("Track option is missing")).await;
return;
}
};
// Create the initial response
process_message(&ctx, &command).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_id} with track: {track_url:?}");
// Handle the track url
match enqueue_track(ctx, manager, guild_id.to_owned(), track_url, true).await {
Ok(count) => {
let mut message = format!("Playing {} tracks", count);
if count == 0 {
message = "No tracks were played".to_string();
} else if count == 1 {
message = "Playing 1 track".to_string();
}
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,
play_now: bool,
) -> SirenResult<i32> {
let mut track_count = 0;
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.0 {
log::warn!("<{guild_id}> Invalid track url: {}", track_url);
return Err(SirenError::new(
422,
format!("Invalid track url: {}", track_url),
));
}
let mut playlist_items: Vec<PlaylistItem> = Vec::new();
// Check if the URL is a playlist or a single track
if valid.1 {
playlist_items = match get_playlist_urls(&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()));
}
};
} else {
let playlist_item = PlaylistItem {
id: "".to_string(),
url: track_url.to_string(),
title: "".to_string(),
duration: 0,
playlist_index: 0,
};
playlist_items.push(playlist_item);
}
// 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.url.to_owned());
let mut input: Input = source.into();
let metadata = match input.aux_metadata().await {
Ok(metadata) => metadata,
Err(err) => {
log::warn!("<{guild_id}> Failed to get metadata for track: {err}");
let _ = leave_voice_channel(&manager, &guild_id).await;
return Err(SirenError::new(422, err.to_string()));
}
};
let track_handle: TrackHandle;
let is_queue_empty = handler.queue().is_empty();
track_handle = handler.enqueue_input(input).await;
// Set the volume
let _ = track_handle.set_volume(volume);
let track_title = metadata.title.unwrap();
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(),
},
);
track_count += 1;
}
if play_now && !handler.queue().is_empty() {
handler.queue().resume();
}
}
Ok(track_count)
}
pub fn get_playlist_urls(url: &str) -> SirenResult<Vec<PlaylistItem>> {
let output = YtDlp::new()
.arg("--flat-playlist")
.arg("--dump-json")
.arg(url)
.execute()?;
let items: Vec<PlaylistItem> = String::from_utf8(output.stdout)?
.split('\n')
.filter_map(|line| {
if line.is_empty() {
None
} else {
Some(
serde_json::from_slice::<PlaylistItem>(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
}
}