-
Notifications
You must be signed in to change notification settings - Fork 43
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #19 from aquelemiguel/revamp-queue
Revamp queue command
- Loading branch information
Showing
6 changed files
with
198 additions
and
49 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,43 +1,182 @@ | ||
use crate::{strings::NO_VOICE_CONNECTION, utils::{get_human_readable_timestamp, send_simple_message}}; | ||
use std::{ | ||
cmp::{max, min}, | ||
time::Duration, | ||
}; | ||
|
||
use crate::{ | ||
strings::{NO_VOICE_CONNECTION, QUEUE_EXPIRED, QUEUE_IS_EMPTY}, | ||
utils::{get_full_username, get_human_readable_timestamp, send_simple_message}, | ||
}; | ||
use serenity::{ | ||
builder::CreateEmbed, | ||
client::Context, | ||
framework::standard::{macros::command, CommandResult}, | ||
model::channel::Message, | ||
futures::StreamExt, | ||
model::channel::{Message, ReactionType}, | ||
}; | ||
use songbird::tracks::TrackHandle; | ||
|
||
const EMBED_TIMEOUT: u64 = 60 * 60; | ||
const EMBED_PAGE_SIZE: usize = 6; | ||
|
||
#[command] | ||
async fn queue(ctx: &Context, msg: &Message) -> CommandResult { | ||
let guild_id = msg.guild(&ctx.cache).await.unwrap().id; | ||
let manager = songbird::get(ctx).await.expect("Could not retrieve Songbird voice client"); | ||
let manager = songbird::get(ctx) | ||
.await | ||
.expect("Could not retrieve Songbird voice client"); | ||
|
||
let author_id = msg.author.id; | ||
let author_username = get_full_username(&msg.author); | ||
|
||
if let Some(call) = manager.get(guild_id) { | ||
let handler = call.lock().await; | ||
let tracks = handler.queue().current_queue(); | ||
|
||
msg.channel_id.send_message(&ctx.http, |m| { | ||
m.embed(|e| { | ||
e.title("Queue"); | ||
// If the queue is empty, end the command. | ||
if tracks.is_empty() { | ||
send_simple_message(&ctx.http, msg, QUEUE_IS_EMPTY).await; | ||
return Ok(()); | ||
} | ||
|
||
let mut message = msg | ||
.channel_id | ||
.send_message(&ctx.http, |m| { | ||
m.embed(|e| create_queue_embed(e, &author_username, &tracks, 0)) | ||
}) | ||
.await?; | ||
|
||
reset_reactions(ctx, &message).await; | ||
drop(handler); // Release the handler for other commands to use it. | ||
|
||
let mut current_page: usize = 0; | ||
let mut stream = message | ||
.await_reactions(&ctx) | ||
.timeout(Duration::from_secs(EMBED_TIMEOUT)) // Stop collecting reactions after an hour. | ||
.author_id(author_id) // Only collect reactions from the invoker. | ||
.await; | ||
|
||
let top_track = tracks.first().unwrap(); | ||
e.thumbnail(top_track.metadata().thumbnail.as_ref().unwrap()); | ||
while let Some(reaction) = stream.next().await { | ||
let handler = call.lock().await; | ||
let emoji = &reaction.as_inner_ref().emoji; | ||
|
||
for (i, t) in tracks.iter().enumerate() { | ||
let title = t.metadata().title.as_ref().unwrap(); | ||
let duration = get_human_readable_timestamp(t.metadata().duration.unwrap()); | ||
// Refetch the queue in case it changed. | ||
let tracks = handler.queue().current_queue(); | ||
let num_pages = calculate_num_pages(&tracks); | ||
|
||
e.field( | ||
format!("[{}] {}", i + 1, title), | ||
format!("Duration: `{}`\nRequested by: `{}`", duration, msg.author.name), | ||
false, | ||
); | ||
} | ||
current_page = match emoji.as_data().as_str() { | ||
"⏪" => 0, | ||
"◀️" => min(current_page.saturating_sub(1), num_pages - 1), | ||
"▶️" => min(current_page + 1, num_pages - 1), | ||
"⏩" => num_pages - 1, | ||
_ => continue, | ||
}; | ||
|
||
e | ||
message | ||
.edit(&ctx, |m| { | ||
m.embed(|e| create_queue_embed(e, &author_username, &tracks, current_page)) | ||
}) | ||
.await?; | ||
|
||
// Cleanup message for next loop. | ||
reset_reactions(ctx, &message).await; | ||
} | ||
|
||
// If it reaches this point, the stream has expired. | ||
message.delete_reactions(&ctx.http).await.unwrap(); | ||
message | ||
.edit(&ctx, |m| { | ||
m.embed(|e| e.title("Queue").description(QUEUE_EXPIRED)) | ||
}) | ||
}).await?; | ||
.await?; | ||
} else { | ||
send_simple_message(&ctx.http, msg, NO_VOICE_CONNECTION).await; | ||
} | ||
|
||
Ok(()) | ||
} | ||
|
||
async fn reset_reactions(ctx: &Context, message: &Message) { | ||
message.delete_reactions(&ctx.http).await.unwrap(); | ||
|
||
let reactions = vec!["⏪", "◀️", "▶️", "⏩"] | ||
.iter() | ||
.map(|r| ReactionType::Unicode(r.to_string())) | ||
.collect::<Vec<ReactionType>>(); | ||
|
||
for reaction in reactions.clone() { | ||
message.react(&ctx.http, reaction).await.unwrap(); | ||
} | ||
} | ||
|
||
pub fn create_queue_embed<'a>( | ||
embed: &'a mut CreateEmbed, | ||
author: &str, | ||
tracks: &[TrackHandle], | ||
page: usize, | ||
) -> &'a mut CreateEmbed { | ||
embed.title("Queue"); | ||
let description; | ||
|
||
if !tracks.is_empty() { | ||
let metadata = tracks[0].metadata(); | ||
embed.thumbnail(tracks[0].metadata().thumbnail.as_ref().unwrap()); | ||
|
||
description = format!( | ||
"[{}]({}) • `{}`", | ||
metadata.title.as_ref().unwrap(), | ||
metadata.source_url.as_ref().unwrap(), | ||
get_human_readable_timestamp(metadata.duration.unwrap()) | ||
); | ||
} else { | ||
description = String::from("Nothing is playing!"); | ||
} | ||
|
||
embed.field("🔊 Now playing", description, false); | ||
embed.field("⌛ Up next", build_queue_page(tracks, page), false); | ||
|
||
embed.footer(|f| { | ||
f.text(format!( | ||
"Page {} of {} • Requested by {}", | ||
page + 1, | ||
calculate_num_pages(tracks), | ||
author | ||
)) | ||
}) | ||
} | ||
|
||
fn build_queue_page(tracks: &[TrackHandle], page: usize) -> String { | ||
let start_idx = EMBED_PAGE_SIZE * page; | ||
let queue: Vec<&TrackHandle> = tracks | ||
.iter() | ||
.skip(start_idx + 1) | ||
.take(EMBED_PAGE_SIZE) | ||
.collect(); | ||
|
||
if queue.is_empty() { | ||
return String::from("There's no songs up next!"); | ||
} | ||
|
||
let mut description = String::new(); | ||
|
||
for (i, t) in queue.iter().enumerate() { | ||
let title = t.metadata().title.as_ref().unwrap(); | ||
let url = t.metadata().source_url.as_ref().unwrap(); | ||
let duration = get_human_readable_timestamp(t.metadata().duration.unwrap()); | ||
|
||
description.push_str(&format!( | ||
"`{}.` [{}]({}) • `{}`\n", | ||
i + start_idx + 1, | ||
title, | ||
url, | ||
duration | ||
)); | ||
} | ||
|
||
description | ||
} | ||
|
||
fn calculate_num_pages(tracks: &[TrackHandle]) -> usize { | ||
let num_pages = ((tracks.len() as f64 - 1.0) / EMBED_PAGE_SIZE as f64).ceil() as usize; | ||
max(1, num_pages) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,26 +1,21 @@ | ||
pub const IDLE_ALERT: &'static str = | ||
pub const IDLE_ALERT: &str = | ||
"I've been idle for over 5 minutes, so I'll leave for now. Feel free to summon me back any time!"; | ||
|
||
pub const AUTHOR_NOT_FOUND: &'static str = | ||
"Could not find you in any voice channel!"; | ||
pub const AUTHOR_NOT_FOUND: &str = "Could not find you in any voice channel!"; | ||
|
||
pub const NO_VOICE_CONNECTION: &'static str = | ||
"I'm not connected to any voice channel!"; | ||
pub const NO_VOICE_CONNECTION: &str = "I'm not connected to any voice channel!"; | ||
|
||
pub const QUEUE_IS_EMPTY: &'static str = | ||
"Queue is empty!"; | ||
pub const QUEUE_IS_EMPTY: &str = "Queue is empty!"; | ||
|
||
pub const MISSING_TIMESTAMP: &'static str = | ||
"Include a timestamp!"; | ||
pub const QUEUE_EXPIRED: &str = | ||
"In order to save resources, this command has expired.\nPlease feel free to reinvoke it!"; | ||
|
||
pub const TIMESTAMP_PARSING_FAILED: &'static str = | ||
"Invalid parsing formatting!"; | ||
pub const MISSING_TIMESTAMP: &str = "Include a timestamp!"; | ||
|
||
pub const MISSING_PLAY_QUERY: &'static str = | ||
"Missing query for this command! Either add a URL or keywords."; | ||
pub const TIMESTAMP_PARSING_FAILED: &str = "Invalid parsing formatting!"; | ||
|
||
pub const MISSING_INDEX_QUEUE: &'static str = | ||
"Missing an index!"; | ||
pub const MISSING_PLAY_QUERY: &str = | ||
"Missing query for this command! Either add a URL or keywords."; | ||
|
||
pub const NO_SONG_ON_INDEX: &'static str = | ||
"There is no queued song on that index!"; | ||
pub const MISSING_INDEX_QUEUE: &str = "Missing an index!"; | ||
pub const NO_SONG_ON_INDEX: &str = "There is no queued song on that index!"; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,22 +1,31 @@ | ||
use serenity::{http::Http, model::channel::Message, utils::Colour}; | ||
use serenity::{ | ||
http::Http, | ||
model::{channel::Message, prelude::User}, | ||
utils::Color, | ||
}; | ||
use std::{sync::Arc, time::Duration}; | ||
|
||
pub async fn send_simple_message(http: &Arc<Http>, msg: &Message, content: &str) -> Message { | ||
msg.channel_id.send_message(http, |m| { | ||
m.embed(|e| e.description(content).colour(Colour::ORANGE)) | ||
}).await.expect("Unable to send message") | ||
msg.channel_id | ||
.send_message(http, |m| { | ||
m.embed(|e| e.description(format!("**{}**", content)).color(Color::RED)) | ||
}) | ||
.await | ||
.expect("Unable to send message") | ||
} | ||
|
||
pub fn get_human_readable_timestamp(duration: Duration) -> String { | ||
let seconds = duration.as_secs() % 60; | ||
let minutes = (duration.as_secs() / 60) % 60; | ||
let hours = duration.as_secs() / 3600; | ||
|
||
let timestamp = if hours < 1 { | ||
if hours < 1 { | ||
format!("{}:{:02}", minutes, seconds) | ||
} else { | ||
format!("{}:{:02}:{:02}", hours, minutes, seconds) | ||
}; | ||
} | ||
} | ||
|
||
timestamp | ||
pub fn get_full_username(user: &User) -> String { | ||
format!("{}#{:04}", user.name, user.discriminator) | ||
} |