Add music commands with songbird
Signed-off-by: trivernis <trivernis@protonmail.com>pull/2/head
parent
6729a61190
commit
a6a603dbb5
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,8 @@
|
|||||||
|
pub(crate) mod ping;
|
||||||
|
|
||||||
|
use ping::PING_COMMAND;
|
||||||
|
use serenity::framework::standard::macros::group;
|
||||||
|
|
||||||
|
#[group]
|
||||||
|
#[commands(ping)]
|
||||||
|
pub struct Misc;
|
@ -0,0 +1,14 @@
|
|||||||
|
use serenity::client::Context;
|
||||||
|
use serenity::framework::standard::macros::command;
|
||||||
|
use serenity::framework::standard::CommandResult;
|
||||||
|
use serenity::model::channel::Message;
|
||||||
|
|
||||||
|
#[command]
|
||||||
|
#[description("Simple ping test command")]
|
||||||
|
#[usage("ping")]
|
||||||
|
#[example("ping")]
|
||||||
|
async fn ping(ctx: &Context, msg: &Message) -> CommandResult {
|
||||||
|
msg.reply(ctx, "Pong!").await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
@ -1 +1,7 @@
|
|||||||
pub(crate) mod minecraft;
|
pub(crate) mod minecraft;
|
||||||
|
pub(crate) mod misc;
|
||||||
|
pub(crate) mod music;
|
||||||
|
|
||||||
|
pub use minecraft::MINECRAFT_GROUP;
|
||||||
|
pub use misc::MISC_GROUP;
|
||||||
|
pub use music::MUSIC_GROUP;
|
||||||
|
@ -0,0 +1,47 @@
|
|||||||
|
use serenity::client::Context;
|
||||||
|
use serenity::framework::standard::macros::command;
|
||||||
|
use serenity::framework::standard::{CommandError, CommandResult};
|
||||||
|
use serenity::model::channel::Message;
|
||||||
|
|
||||||
|
#[command]
|
||||||
|
#[only_in(guilds)]
|
||||||
|
#[description("Displays the currently playing song")]
|
||||||
|
#[usage("current")]
|
||||||
|
#[aliases("nowplaying", "np")]
|
||||||
|
async fn current(ctx: &Context, msg: &Message) -> CommandResult {
|
||||||
|
let guild = msg.guild(&ctx.cache).await.unwrap();
|
||||||
|
|
||||||
|
let manager = songbird::get(ctx)
|
||||||
|
.await
|
||||||
|
.expect("Songbird Voice client placed in at initialisation.")
|
||||||
|
.clone();
|
||||||
|
|
||||||
|
let handler_lock = manager
|
||||||
|
.get(guild.id)
|
||||||
|
.ok_or(CommandError::from("Not in a voice channel"))?;
|
||||||
|
let handler = handler_lock.lock().await;
|
||||||
|
|
||||||
|
if let Some(current) = handler.queue().current() {
|
||||||
|
let metadata = current.metadata().clone();
|
||||||
|
msg.channel_id
|
||||||
|
.send_message(ctx, |m| {
|
||||||
|
m.embed(|mut e| {
|
||||||
|
e = e.description(format!(
|
||||||
|
"Now Playing [{}]({}) by {}",
|
||||||
|
metadata.title.unwrap(),
|
||||||
|
metadata.source_url.unwrap(),
|
||||||
|
metadata.artist.unwrap()
|
||||||
|
));
|
||||||
|
|
||||||
|
if let Some(thumb) = metadata.thumbnail {
|
||||||
|
e = e.thumbnail(thumb);
|
||||||
|
}
|
||||||
|
|
||||||
|
e
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
@ -0,0 +1,17 @@
|
|||||||
|
use crate::commands::music::utils::{get_channel_for_author, join_channel};
|
||||||
|
use serenity::client::Context;
|
||||||
|
use serenity::framework::standard::macros::command;
|
||||||
|
use serenity::framework::standard::{CommandError, CommandResult};
|
||||||
|
use serenity::model::channel::Message;
|
||||||
|
|
||||||
|
#[command]
|
||||||
|
#[only_in(guilds)]
|
||||||
|
#[description("Joins a voice channel")]
|
||||||
|
#[usage("join")]
|
||||||
|
async fn join(ctx: &Context, msg: &Message) -> CommandResult {
|
||||||
|
let guild = msg.guild(&ctx.cache).await.unwrap();
|
||||||
|
let channel_id = get_channel_for_author(&msg.author.id, &guild)?;
|
||||||
|
join_channel(ctx, channel_id, guild.id).await;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
@ -0,0 +1,28 @@
|
|||||||
|
use serenity::client::Context;
|
||||||
|
use serenity::framework::standard::macros::command;
|
||||||
|
use serenity::framework::standard::CommandResult;
|
||||||
|
use serenity::model::channel::Message;
|
||||||
|
|
||||||
|
#[command]
|
||||||
|
#[only_in(guilds)]
|
||||||
|
#[description("Leaves a voice channel")]
|
||||||
|
#[usage("leave")]
|
||||||
|
#[aliases("stop")]
|
||||||
|
async fn leave(ctx: &Context, msg: &Message) -> CommandResult {
|
||||||
|
let guild = msg.guild(&ctx.cache).await.unwrap();
|
||||||
|
let guild_id = guild.id;
|
||||||
|
|
||||||
|
let manager = songbird::get(ctx)
|
||||||
|
.await
|
||||||
|
.expect("Songbird Voice client placed in at initialisation.")
|
||||||
|
.clone();
|
||||||
|
|
||||||
|
if manager.get(guild_id).is_some() {
|
||||||
|
manager.remove(guild_id).await?;
|
||||||
|
msg.channel_id.say(ctx, "Left the voice channel").await?;
|
||||||
|
} else {
|
||||||
|
msg.channel_id.say(ctx, "Not in a voice channel").await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
@ -0,0 +1,23 @@
|
|||||||
|
mod current;
|
||||||
|
mod join;
|
||||||
|
mod leave;
|
||||||
|
mod play;
|
||||||
|
mod queue;
|
||||||
|
mod shuffle;
|
||||||
|
mod skip;
|
||||||
|
mod utils;
|
||||||
|
|
||||||
|
use serenity::framework::standard::macros::group;
|
||||||
|
|
||||||
|
use current::CURRENT_COMMAND;
|
||||||
|
use join::JOIN_COMMAND;
|
||||||
|
use leave::LEAVE_COMMAND;
|
||||||
|
use play::PLAY_COMMAND;
|
||||||
|
use queue::QUEUE_COMMAND;
|
||||||
|
use shuffle::SHUFFLE_COMMAND;
|
||||||
|
use skip::SKIP_COMMAND;
|
||||||
|
|
||||||
|
#[group]
|
||||||
|
#[commands(join, leave, play, queue, skip, shuffle, current)]
|
||||||
|
#[prefix("m")]
|
||||||
|
pub struct Music;
|
@ -0,0 +1,91 @@
|
|||||||
|
use crate::commands::music::utils::{get_channel_for_author, join_channel};
|
||||||
|
use crate::providers::ytdl::get_videos_for_url;
|
||||||
|
use serenity::client::Context;
|
||||||
|
use serenity::framework::standard::macros::command;
|
||||||
|
use serenity::framework::standard::{Args, CommandError, CommandResult};
|
||||||
|
use serenity::model::channel::Message;
|
||||||
|
|
||||||
|
#[command]
|
||||||
|
#[only_in(guilds)]
|
||||||
|
#[description("Plays a song in a voice channel")]
|
||||||
|
#[usage("play <url>")]
|
||||||
|
#[min_args(1)]
|
||||||
|
#[max_args(1)]
|
||||||
|
#[aliases("p")]
|
||||||
|
async fn play(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult {
|
||||||
|
let url = args.message();
|
||||||
|
|
||||||
|
if !url.starts_with("http") {
|
||||||
|
return Err(CommandError::from("The provided url is not valid"));
|
||||||
|
}
|
||||||
|
|
||||||
|
let guild = msg.guild(&ctx.cache).await.unwrap();
|
||||||
|
|
||||||
|
let manager = songbird::get(ctx)
|
||||||
|
.await
|
||||||
|
.expect("Songbird Voice client placed in at initialisation.")
|
||||||
|
.clone();
|
||||||
|
|
||||||
|
let mut handler = manager.get(guild.id);
|
||||||
|
|
||||||
|
if handler.is_none() {
|
||||||
|
msg.guild(&ctx.cache).await.unwrap();
|
||||||
|
let channel_id = get_channel_for_author(&msg.author.id, &guild)?;
|
||||||
|
handler = Some(join_channel(ctx, channel_id, guild.id).await);
|
||||||
|
}
|
||||||
|
|
||||||
|
let handler_lock = handler.ok_or(CommandError::from("Not in a voice channel"))?;
|
||||||
|
let mut handler = handler_lock.lock().await;
|
||||||
|
let mut videos: Vec<String> = get_videos_for_url(url)?
|
||||||
|
.into_iter()
|
||||||
|
.map(|v| format!("https://www.youtube.com/watch?v={}", v.url))
|
||||||
|
.collect();
|
||||||
|
if videos.len() == 0 {
|
||||||
|
videos.push(url.to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut metadata = None;
|
||||||
|
|
||||||
|
for video in &videos {
|
||||||
|
let source = match songbird::ytdl(video).await {
|
||||||
|
Ok(s) => s,
|
||||||
|
Err(e) => {
|
||||||
|
msg.channel_id
|
||||||
|
.say(ctx, format!("Failed to enqueue {}: {:?}", video, e))
|
||||||
|
.await?;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
metadata = Some(source.metadata.clone());
|
||||||
|
|
||||||
|
handler.enqueue_source(source);
|
||||||
|
}
|
||||||
|
if videos.len() == 1 {
|
||||||
|
let metadata = metadata.unwrap();
|
||||||
|
msg.channel_id
|
||||||
|
.send_message(&ctx.http, |m| {
|
||||||
|
m.embed(|mut e| {
|
||||||
|
e = e.description(format!(
|
||||||
|
"Added [{}]({}) to the queue",
|
||||||
|
metadata.title.unwrap(),
|
||||||
|
url
|
||||||
|
));
|
||||||
|
if let Some(thumb) = metadata.thumbnail {
|
||||||
|
e = e.thumbnail(thumb);
|
||||||
|
}
|
||||||
|
|
||||||
|
e
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
} else {
|
||||||
|
msg.channel_id
|
||||||
|
.send_message(&ctx.http, |m| {
|
||||||
|
m.embed(|e| e.description(format!("Added {} songs to the queue", videos.len())))
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
@ -0,0 +1,62 @@
|
|||||||
|
use serenity::client::Context;
|
||||||
|
use serenity::framework::standard::macros::command;
|
||||||
|
use serenity::framework::standard::{CommandError, CommandResult};
|
||||||
|
use serenity::model::channel::Message;
|
||||||
|
use std::cmp::min;
|
||||||
|
|
||||||
|
#[command]
|
||||||
|
#[only_in(guilds)]
|
||||||
|
#[description("Shows the song queue")]
|
||||||
|
#[usage("queue")]
|
||||||
|
#[aliases("q")]
|
||||||
|
async fn queue(ctx: &Context, msg: &Message) -> CommandResult {
|
||||||
|
let guild = msg.guild(&ctx.cache).await.unwrap();
|
||||||
|
|
||||||
|
let manager = songbird::get(ctx)
|
||||||
|
.await
|
||||||
|
.expect("Songbird Voice client placed in at initialisation.")
|
||||||
|
.clone();
|
||||||
|
|
||||||
|
let handler_lock = manager
|
||||||
|
.get(guild.id)
|
||||||
|
.ok_or(CommandError::from("Not in a voice channel"))?;
|
||||||
|
let handler = handler_lock.lock().await;
|
||||||
|
let songs: Vec<(usize, String)> = handler
|
||||||
|
.queue()
|
||||||
|
.current_queue()
|
||||||
|
.into_iter()
|
||||||
|
.map(|t| t.metadata().title.clone().unwrap())
|
||||||
|
.enumerate()
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
if songs.len() == 0 {
|
||||||
|
msg.channel_id
|
||||||
|
.send_message(ctx, |m| {
|
||||||
|
m.embed(|e| e.title("Queue").description("*The queue is empty*"))
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut song_list = Vec::new();
|
||||||
|
|
||||||
|
for i in 0..min(10, songs.len() - 1) {
|
||||||
|
song_list.push(format!("{:0>3} - {}", songs[i].0, songs[i].1))
|
||||||
|
}
|
||||||
|
if songs.len() > 10 {
|
||||||
|
song_list.push("...".to_string());
|
||||||
|
let last = songs.last().unwrap();
|
||||||
|
song_list.push(format!("{:0>3} - {}", last.0, last.1))
|
||||||
|
}
|
||||||
|
msg.channel_id
|
||||||
|
.send_message(ctx, |m| {
|
||||||
|
m.embed(|e| {
|
||||||
|
e.title("Queue")
|
||||||
|
.description(format!("```\n{}\n```", song_list.join("\n")))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
@ -0,0 +1,41 @@
|
|||||||
|
use rand::Rng;
|
||||||
|
use serenity::client::Context;
|
||||||
|
use serenity::framework::standard::macros::command;
|
||||||
|
use serenity::framework::standard::{CommandError, CommandResult};
|
||||||
|
use serenity::model::channel::Message;
|
||||||
|
use std::collections::VecDeque;
|
||||||
|
|
||||||
|
#[command]
|
||||||
|
#[only_in(guilds)]
|
||||||
|
#[description("Shuffles the queue")]
|
||||||
|
#[usage("shuffle")]
|
||||||
|
#[aliases("sh")]
|
||||||
|
async fn shuffle(ctx: &Context, msg: &Message) -> CommandResult {
|
||||||
|
let guild = msg.guild(&ctx.cache).await.unwrap();
|
||||||
|
|
||||||
|
let manager = songbird::get(ctx)
|
||||||
|
.await
|
||||||
|
.expect("Songbird Voice client placed in at initialisation.")
|
||||||
|
.clone();
|
||||||
|
|
||||||
|
let handler_lock = manager
|
||||||
|
.get(guild.id)
|
||||||
|
.ok_or(CommandError::from("Not in a voice channel"))?;
|
||||||
|
let handler = handler_lock.lock().await;
|
||||||
|
handler.queue().modify_queue(shuffle_vec_deque);
|
||||||
|
msg.channel_id
|
||||||
|
.say(ctx, "The queue has been shuffled")
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Fisher-Yates shuffle for VecDeque
|
||||||
|
fn shuffle_vec_deque<T>(deque: &mut VecDeque<T>) {
|
||||||
|
let mut rng = rand::thread_rng();
|
||||||
|
let mut i = deque.len();
|
||||||
|
while i >= 2 {
|
||||||
|
i -= 1;
|
||||||
|
deque.swap(i, rng.gen_range(0..i + 1))
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,31 @@
|
|||||||
|
use serenity::client::Context;
|
||||||
|
use serenity::framework::standard::macros::command;
|
||||||
|
use serenity::framework::standard::{CommandError, CommandResult};
|
||||||
|
use serenity::model::channel::Message;
|
||||||
|
|
||||||
|
#[command]
|
||||||
|
#[only_in(guilds)]
|
||||||
|
#[description("Skips to the next song")]
|
||||||
|
#[usage("skip")]
|
||||||
|
#[aliases("next")]
|
||||||
|
async fn skip(ctx: &Context, msg: &Message) -> CommandResult {
|
||||||
|
let guild = msg.guild(&ctx.cache).await.unwrap();
|
||||||
|
|
||||||
|
let manager = songbird::get(ctx)
|
||||||
|
.await
|
||||||
|
.expect("Songbird Voice client placed in at initialisation.")
|
||||||
|
.clone();
|
||||||
|
|
||||||
|
let handler_lock = manager
|
||||||
|
.get(guild.id)
|
||||||
|
.ok_or(CommandError::from("Not in a voice channel"))?;
|
||||||
|
let handler = handler_lock.lock().await;
|
||||||
|
|
||||||
|
if let Some(current) = handler.queue().current() {
|
||||||
|
current.stop()?;
|
||||||
|
}
|
||||||
|
handler.queue().skip()?;
|
||||||
|
msg.channel_id.say(ctx, "Skipped to the next song").await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
@ -0,0 +1,31 @@
|
|||||||
|
use crate::utils::error::{BotError, BotResult};
|
||||||
|
use serenity::client::Context;
|
||||||
|
use serenity::model::guild::Guild;
|
||||||
|
use serenity::model::id::{ChannelId, GuildId, UserId};
|
||||||
|
use songbird::Call;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use tokio::sync::Mutex;
|
||||||
|
|
||||||
|
/// Joins a voice channel
|
||||||
|
pub(crate) async fn join_channel(
|
||||||
|
ctx: &Context,
|
||||||
|
channel_id: ChannelId,
|
||||||
|
guild_id: GuildId,
|
||||||
|
) -> Arc<Mutex<Call>> {
|
||||||
|
let manager = songbird::get(ctx)
|
||||||
|
.await
|
||||||
|
.expect("Songbird Voice client placed in at initialisation.")
|
||||||
|
.clone();
|
||||||
|
|
||||||
|
let (handler, _) = manager.join(guild_id, channel_id).await;
|
||||||
|
handler
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the voice channel the author is in
|
||||||
|
pub(crate) fn get_channel_for_author(author_id: &UserId, guild: &Guild) -> BotResult<ChannelId> {
|
||||||
|
guild
|
||||||
|
.voice_states
|
||||||
|
.get(author_id)
|
||||||
|
.and_then(|voice_state| voice_state.channel_id)
|
||||||
|
.ok_or(BotError::from("Not in a voice channel."))
|
||||||
|
}
|
@ -0,0 +1 @@
|
|||||||
|
pub(crate) mod ytdl;
|
@ -0,0 +1,31 @@
|
|||||||
|
use crate::providers::ytdl::playlist_entry::PlaylistEntry;
|
||||||
|
use crate::utils::error::{BotError, BotResult};
|
||||||
|
use std::io::Read;
|
||||||
|
use std::process::{Command, Stdio};
|
||||||
|
|
||||||
|
mod playlist_entry;
|
||||||
|
|
||||||
|
/// Returns a list of youtube videos for a given url
|
||||||
|
pub(crate) fn get_videos_for_url(url: &str) -> BotResult<Vec<PlaylistEntry>> {
|
||||||
|
let ytdl = Command::new("youtube-dl")
|
||||||
|
.args(&[
|
||||||
|
"-f",
|
||||||
|
"--no-warnings",
|
||||||
|
"--flat-playlist",
|
||||||
|
"--dump-json",
|
||||||
|
"-i",
|
||||||
|
url,
|
||||||
|
])
|
||||||
|
.stdout(Stdio::piped())
|
||||||
|
.spawn()?;
|
||||||
|
|
||||||
|
let mut output = String::new();
|
||||||
|
ytdl.stdout.unwrap().read_to_string(&mut output)?;
|
||||||
|
|
||||||
|
let videos = output
|
||||||
|
.lines()
|
||||||
|
.map(|l| serde_json::from_str::<PlaylistEntry>(l).unwrap())
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
Ok(videos)
|
||||||
|
}
|
@ -0,0 +1,9 @@
|
|||||||
|
use serde_derive::Deserialize;
|
||||||
|
|
||||||
|
#[derive(Deserialize, Clone, Debug)]
|
||||||
|
pub(crate) struct PlaylistEntry {
|
||||||
|
ie_key: String,
|
||||||
|
id: String,
|
||||||
|
pub url: String,
|
||||||
|
pub title: String,
|
||||||
|
}
|
Loading…
Reference in New Issue