Skip to content

Commit

Permalink
Merge pull request #19 from aquelemiguel/revamp-queue
Browse files Browse the repository at this point in the history
Revamp queue command
  • Loading branch information
João Dias Conde Azevedo authored Oct 12, 2021
2 parents 1e15d8c + 1b80ca5 commit c9cd5e8
Show file tree
Hide file tree
Showing 6 changed files with 198 additions and 49 deletions.
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ features = ["builtin-queue"]

[dependencies.serenity]
version = "0.10"
features = ["cache", "framework", "standard_framework", "voice"]
features = ["cache", "framework", "standard_framework", "voice", "collector"]

[dependencies.tokio]
version = "1.0"
Expand Down
177 changes: 158 additions & 19 deletions src/commands/queue.rs
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)
}
2 changes: 1 addition & 1 deletion src/commands/skip.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ async fn skip(ctx: &Context, msg: &Message) -> CommandResult {
send_simple_message(&ctx.http, msg, QUEUE_IS_EMPTY).await;
}
else if handler.queue().skip().is_ok() {
send_simple_message(&ctx.http, msg, "Skipped!").await;
send_simple_message(&ctx.http, msg, "⏭️ Skipped!").await;
}
} else {
send_simple_message(&ctx.http, msg, NO_VOICE_CONNECTION).await;
Expand Down
14 changes: 10 additions & 4 deletions src/main.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
use serenity::{Client, async_trait, client::{Context, EventHandler}, framework::{standard::macros::group, StandardFramework}, model::{gateway::Ready, prelude::Activity}};
use serenity::{
async_trait,
client::{Context, EventHandler},
framework::{standard::macros::group, StandardFramework},
model::{gateway::Ready, prelude::Activity},
Client,
};
use songbird::SerenityInit;
use std::env;

use parrot::commands::{
clear::*, leave::*, now_playing::*, pause::*, play::*, queue::*, repeat::*, resume::*, seek::*,
shuffle::*, skip::*, stop::*, summon::*, remove::*,
clear::*, leave::*, now_playing::*, pause::*, play::*, queue::*, remove::*, repeat::*,
resume::*, seek::*, shuffle::*, skip::*, stop::*, summon::*,
};

#[group]
Expand All @@ -31,7 +37,7 @@ struct Handler;
#[async_trait]
impl EventHandler for Handler {
async fn ready(&self, ctx: Context, ready: Ready) {
println!("{} is connected!", ready.user.name);
println!("🦜 {} is connected!", ready.user.name);
ctx.set_activity(Activity::listening("!play")).await;
}
}
Expand Down
29 changes: 12 additions & 17 deletions src/strings.rs
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!";
23 changes: 16 additions & 7 deletions src/utils.rs
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)
}

0 comments on commit c9cd5e8

Please sign in to comment.