224 lines
6.7 KiB
Rust
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
|
|
}
|
|
}
|