Simplified enqueue_track to parse track type (playlist or single track)

This commit is contained in:
2024-12-18 19:05:13 -05:00
parent c82fee0dd3
commit aa7bad945a
5 changed files with 144 additions and 116 deletions

View File

@@ -56,14 +56,8 @@ pub async fn leave_voice_channel(manager: &Arc<Songbird>, guild_id: &GuildId) ->
* 1st tuple value is if the URL is valid.
* 2nd tuple value is if the URL is a playlist.
*/
fn is_valid_url(url: &str) -> (bool, bool) {
Url::parse(url).ok().map_or((false, false), |valid_url| {
let is_playlist: bool = valid_url
.query_pairs()
.find(|(key, _)| key == "list")
.map_or(false, |_| true);
(true, is_playlist)
})
fn is_valid_url(url: &str) -> bool {
Url::parse(url).ok().map_or(false, |valid_url| true)
}
/**

View File

@@ -9,7 +9,7 @@ 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::bot::ytdlp::{YtDlp, YtDlpItem};
use crate::error::{SirenResult, Error as SirenError};
use crate::HttpKey;
@@ -57,12 +57,12 @@ pub async fn run(ctx: &Context, command: &CommandInteraction) {
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).await {
Ok(count) => {
let mut message = format!("Playing {} tracks", count);
if count == 0 {
Ok(items) => {
let mut message = format!("Added {} tracks", items.len());
if items.len() == 0 {
message = "No tracks were played".to_string();
} else if count == 1 {
message = "Playing 1 track".to_string();
} else if items.len() == 1 {
message = format!("Added **{}**", items[0].get_title());
}
edit_response(&ctx, &command, message).await;
}
@@ -84,42 +84,32 @@ pub async fn enqueue_track(
manager: Arc<Songbird>,
guild_id: GuildId,
track_url: &str,
) -> SirenResult<i32> {
let mut track_count = 0;
) -> 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.0 {
if !valid {
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) {
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()));
}
};
} 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 {
for item in &playlist_items {
let volume = guild.volume as f32 / 100.0;
let http_client = {
let data = ctx.data.read().await;
@@ -128,21 +118,17 @@ pub async fn enqueue_track(
.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 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);
let track_title = metadata.title.unwrap();
log::debug!("<{guild_id}> Added track: {}", track_title);
handler.remove_all_global_events();
handler.add_global_event(
@@ -152,29 +138,28 @@ pub async fn enqueue_track(
call: manager.clone(),
},
);
track_count += 1;
}
if handler.queue().is_empty() {
let _ = handler.queue().resume();
}
}
Ok(track_count)
Ok(playlist_items)
}
pub fn get_playlist_urls(url: &str) -> SirenResult<Vec<PlaylistItem>> {
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<PlaylistItem> = String::from_utf8(output.stdout)?
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::<PlaylistItem>(line.as_bytes())
serde_json::from_slice::<YtDlpItem>(line.as_bytes())
.map_err(|err| SirenError::new(500, err.to_string())),
)
}

View File

@@ -1,25 +1,38 @@
use rand::Rng;
use serenity::all::{
CommandInteraction, CommandOptionType, Context, CreateCommand, CreateCommandOption, Mentionable, UserId
CommandInteraction, CommandOptionType, Context, CreateCommand, CreateCommandOption, Mentionable,
UserId,
};
use crate::bot::commands::{create_response, edit_response, user_id_dm};
pub async fn run(ctx: &Context, command: &CommandInteraction) {
// Check if the roll result is private
let private = command.data.options.iter().find(|opt| opt.name == "private")
let private = command
.data
.options
.iter()
.find(|opt| opt.name == "private")
.and_then(|o| o.value.as_bool())
.unwrap_or(true);
// Retrieve the DM's name or ID from the options (optional)
let user = command.data.options.iter().find(|opt| opt.name == "user")
let user = command
.data
.options
.iter()
.find(|opt| opt.name == "user")
.and_then(|o| o.value.as_mentionable());
create_response(&ctx, &command, format!("Rolling..."), private).await;
let dice_string = match command.data.options.get(0)
let dice_string = match command
.data
.options
.get(0)
.and_then(|o| o.value.as_str())
.map(|s| s.split_whitespace().collect::<String>()) {
.map(|s| s.split_whitespace().collect::<String>())
{
Some(dice_value) => dice_value,
None => {
log::warn!("Missing or invalid dice option");
@@ -55,10 +68,20 @@ pub async fn run(ctx: &Context, command: &CommandInteraction) {
match user {
Some(id) => {
let user_id = UserId::new(id.get());
user_id_dm(&ctx, &user_id, format!("Dice roll from {}: {}", &command.user.mention(), response)).await;
edit_response(&ctx, command, format!("Sending dice roll results to {}", &user_id.mention())).await;
},
None => edit_response(&ctx, &command, response).await
user_id_dm(
&ctx,
&user_id,
format!("Dice roll from {}: {}", &command.user.mention(), response),
)
.await;
edit_response(
&ctx,
command,
format!("Sending dice roll results to {}", &user_id.mention()),
)
.await;
}
None => edit_response(&ctx, &command, response).await,
};
}
Err(why) => {
@@ -94,7 +117,9 @@ fn parse_dice(dice: &str) -> Result<(u32, u32, i32), String> {
};
// Parse the number of sides
let sides_part = parts.next().ok_or_else(|| format!("Invalid dice string: {}", dice))?;
let sides_part = parts
.next()
.ok_or_else(|| format!("Invalid dice string: {}", dice))?;
let sides = match sides_part.parse::<u32>() {
Ok(n) => {
if [4, 6, 8, 10, 12, 20, 100].contains(&n) {
@@ -106,10 +131,12 @@ fn parse_dice(dice: &str) -> Result<(u32, u32, i32), String> {
));
}
}
Err(_) => return Err(format!(
Err(_) => {
return Err(format!(
"Expected one of d4, d6, d8, d10, d12, d20, d100 but received d{}",
sides_part
)),
))
}
};
// Determine if there's a modifier (+ or -)
@@ -152,7 +179,11 @@ pub fn register() -> CreateCommand {
.required(false),
)
.add_option(
CreateCommandOption::new(CommandOptionType::Mentionable, "user", "User to receive the roll results")
CreateCommandOption::new(
CommandOptionType::Mentionable,
"user",
"User to receive the roll results",
)
.required(false),
)
}

View File

@@ -1,6 +1,7 @@
use serenity::prelude::*;
use serenity::all::{
CommandInteraction, CreateInteractionResponse, CreateInteractionResponseMessage, CreateMessage, EditInteractionResponse, InteractionResponseFlags, Message, User, UserId
CommandInteraction, CreateInteractionResponse, CreateInteractionResponseMessage, CreateMessage,
EditInteractionResponse, InteractionResponseFlags, Message, User, UserId,
};
pub mod audio;
@@ -13,11 +14,7 @@ pub async fn process_message(ctx: &Context, command: &CommandInteraction, privat
create_response(&ctx, &command, format!("Processing..."), private).await;
}
pub async fn user_id_dm(
ctx: &Context,
user_id: &UserId,
content: String,
) -> Option<Message> {
pub async fn user_id_dm(ctx: &Context, user_id: &UserId, content: String) -> Option<Message> {
let data = CreateMessage::new().content(content.to_owned());
return match user_id.dm(ctx, data).await {
Ok(message) => Some(message),
@@ -28,11 +25,7 @@ pub async fn user_id_dm(
};
}
pub async fn user_dm(
ctx: &Context,
user: &User,
content: String,
) -> Option<Message> {
pub async fn user_dm(ctx: &Context, user: &User, content: String) -> Option<Message> {
let data = CreateMessage::new().content(content.to_owned());
return match user.direct_message(ctx, data).await {
Ok(message) => Some(message),

View File

@@ -1,10 +1,35 @@
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize)]
pub struct PlaylistItem {
pub id: String,
pub url: String,
pub title: String,
pub duration: i32,
pub playlist_index: i32,
#[serde(untagged)]
pub enum YtDlpItem {
PlaylistItem {
id: String,
url: String,
title: String,
duration: i32,
playlist_index: i32,
},
VideoItem {
id: String,
webpage_url: String,
title: String,
duration: i32,
},
}
impl YtDlpItem {
pub fn get_title(&self) -> &str {
match self {
YtDlpItem::PlaylistItem { title, .. } => title,
YtDlpItem::VideoItem { title, .. } => title,
}
}
pub fn get_url(&self) -> &str {
match self {
YtDlpItem::PlaylistItem { url, .. } => url,
YtDlpItem::VideoItem { webpage_url, .. } => webpage_url,
}
}
}