Add music commands with songbird

Signed-off-by: trivernis <trivernis@protonmail.com>
pull/2/head
trivernis 3 years ago
parent 6729a61190
commit a6a603dbb5
Signed by: Trivernis
GPG Key ID: DFFFCC2C7A02DB45

@ -10,6 +10,20 @@
</Attribute>
</value>
</entry>
<entry key="/src/commands/music/current.rs">
<value>
<Attribute>
<option name="separator" value=":" />
</Attribute>
</value>
</entry>
<entry key="/src/commands/music/utils.rs">
<value>
<Attribute>
<option name="separator" value=":" />
</Attribute>
</value>
</entry>
<entry key="/src/database/scripts/update_tables.sql">
<value>
<Attribute>

705
Cargo.lock generated

File diff suppressed because it is too large Load Diff

@ -16,4 +16,7 @@ serde_derive = "1.0.125"
serde = "1.0.125"
thiserror = "1.0.24"
parking_lot = "0.11.1"
minecraft-data-rs = "0.2.0"
minecraft-data-rs = "0.2.0"
songbird = {version = "0.1.5", features=["builtin-queue"]}
serde_json = "1.0.64"
rand = "0.8.3"

@ -1,4 +1,4 @@
use crate::commands::minecraft::Minecraft;
use crate::commands::*;
use crate::database::{get_database, Database};
use crate::utils::error::{BotError, BotResult};
use crate::utils::store::{Store, StoreData};
@ -9,6 +9,7 @@ use serenity::framework::standard::CommandResult;
use serenity::framework::StandardFramework;
use serenity::model::channel::Message;
use serenity::Client;
use songbird::SerenityInit;
struct Handler;
@ -19,7 +20,10 @@ pub async fn get_client() -> BotResult<Client> {
let token = dotenv::var("BOT_TOKEN").map_err(|_| BotError::MissingToken)?;
let database = get_database()?;
let client = Client::builder(token).framework(get_framework()).await?;
let client = Client::builder(token)
.framework(get_framework())
.register_songbird()
.await?;
{
let mut data = client.data.write().await;
data.insert::<Store>(StoreData::new(database))
@ -39,9 +43,11 @@ pub fn get_framework() -> StandardFramework {
.allow_dm(true)
.ignore_bots(true)
})
.group(&crate::commands::minecraft::MINECRAFT_GROUP)
.group(&crate::GENERAL_GROUP)
.group(&MINECRAFT_GROUP)
.group(&MISC_GROUP)
.group(&MUSIC_GROUP)
.after(after_hook)
.before(before_hook)
}
#[hook]
@ -52,3 +58,9 @@ async fn after_hook(ctx: &Context, msg: &Message, cmd_name: &str, error: Command
println!("Error in {}: {:?}", cmd_name, why);
}
}
#[hook]
async fn before_hook(ctx: &Context, msg: &Message, _: &str) -> bool {
let _ = msg.channel_id.broadcast_typing(ctx).await;
true
}

@ -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."))
}

@ -12,10 +12,6 @@ pub(crate) mod database;
mod providers;
pub(crate) mod utils;
#[group]
#[commands(ping)]
struct General;
struct Handler;
#[tokio::main]
@ -27,10 +23,3 @@ async fn main() {
println!("An error occurred while running the client: {:?}", why);
}
}
#[command]
async fn ping(ctx: &Context, msg: &Message) -> CommandResult {
msg.reply(ctx, "Pong!").await?;
Ok(())
}

@ -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,
}

@ -16,4 +16,16 @@ pub enum BotError {
#[error("Minecraft Data Error: {0}")]
MinecraftDataError(#[from] minecraft_data_rs::DataError),
#[error("IO Error: {0}")]
IOError(#[from] std::io::Error),
#[error("{0}")]
Msg(String),
}
impl From<&str> for BotError {
fn from(s: &str) -> Self {
Self::Msg(s.to_string())
}
}

Loading…
Cancel
Save