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 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