diff --git a/.github/workflows/build-docker.yml b/.github/workflows/build-docker.yml index 1027958..dcc01aa 100644 --- a/.github/workflows/build-docker.yml +++ b/.github/workflows/build-docker.yml @@ -1,4 +1,4 @@ -name: Build and Test +name: Build Docker Container on: workflow_dispatch: diff --git a/Cargo.lock b/Cargo.lock index 62ab676..d5d9c00 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -89,8 +89,10 @@ dependencies = [ "futures-io", "futures-util", "log 0.4.14", + "native-tls", "pin-project-lite", "tokio", + "tokio-native-tls", "tokio-rustls", "tungstenite 0.13.0", "webpki-roots 0.21.1", @@ -1077,6 +1079,31 @@ dependencies = [ "winapi-build", ] +[[package]] +name = "lavalink-rs" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c63ca28d0378fa5e51d24a2cb6cc900a5f654d7931a53784ae01ff9a94b443e1" +dependencies = [ + "async-trait", + "async-tungstenite 0.13.1", + "dashmap", + "futures", + "http", + "regex", + "reqwest", + "serde", + "serde-aux", + "serde_json", + "songbird", + "tokio", + "tokio-native-tls", + "tracing", + "tracing-log", + "typemap_rev", + "version_check", +] + [[package]] name = "lazy_static" version = "1.4.0" @@ -2047,6 +2074,17 @@ dependencies = [ "serde_derive", ] +[[package]] +name = "serde-aux" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77eb8c83f6ebaedf5e8f970a8a44506b180b8e6268de03885c8547031ccaee00" +dependencies = [ + "chrono", + "serde", + "serde_json", +] + [[package]] name = "serde_derive" version = "1.0.125" @@ -2495,6 +2533,7 @@ dependencies = [ "dotenv", "fern", "futures", + "lavalink-rs", "lazy_static", "log 0.4.14", "minecraft-data-rs", @@ -2642,6 +2681,17 @@ dependencies = [ "tracing", ] +[[package]] +name = "tracing-log" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6923477a48e41c1951f1999ef8bb5a3023eb723ceadafe78ffb65dc366761e3" +dependencies = [ + "lazy_static", + "log 0.4.14", + "tracing-core", +] + [[package]] name = "trigram" version = "0.4.4" @@ -2690,6 +2740,7 @@ dependencies = [ "httparse", "input_buffer 0.4.0", "log 0.4.14", + "native-tls", "rand 0.8.3", "rustls", "sha-1", diff --git a/Cargo.toml b/Cargo.toml index 998ba74..3739499 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -38,4 +38,5 @@ sauce-api = "0.7.1" rustc_version_runtime = "0.2.0" trigram = "0.4.4" typemap_rev = "0.1.5" -youtube-metadata = "0.1.1" \ No newline at end of file +youtube-metadata = "0.1.1" +lavalink-rs = {version="0.7.1", features=["native"]} \ No newline at end of file diff --git a/src/client.rs b/src/client.rs index 64b239a..51e3eb4 100644 --- a/src/client.rs +++ b/src/client.rs @@ -12,28 +12,50 @@ use songbird::SerenityInit; use crate::commands::*; use crate::handler::Handler; -use crate::utils::context_data::{get_database_from_context, DatabaseContainer, Store, StoreData}; +use crate::providers::music::lavalink::{Lavalink, LavalinkHandler}; +use crate::utils::context_data::{ + get_database_from_context, DatabaseContainer, MusicPlayers, Store, StoreData, +}; use crate::utils::error::{BotError, BotResult}; use bot_serenityutils::menu::EventDrivenMessageContainer; +use lavalink_rs::LavalinkClient; use serenity::framework::standard::buckets::LimitedFor; +use serenity::http::Http; +use std::env; use std::sync::Arc; use std::time::SystemTime; use tokio::sync::Mutex; pub async fn get_client() -> BotResult { - let token = dotenv::var("BOT_TOKEN").map_err(|_| BotError::MissingToken)?; + let token = env::var("BOT_TOKEN").map_err(|_| BotError::MissingToken)?; let database = get_database()?; - + let http = Http::new_with_token(&token); + let current_application = http.get_current_application_info().await?; let client = Client::builder(token) .event_handler(Handler) .framework(get_framework().await) .register_songbird() .await?; + let data = client.data.clone(); + + let lava_client = LavalinkClient::builder(current_application.id.0) + .set_host(env::var("LAVALINK_HOST").unwrap_or("172.0.0.1".to_string())) + .set_password(env::var("LAVALINK_PASSWORD").expect("Missing lavalink password")) + .set_port( + env::var("LAVALINK_PORT") + .ok() + .and_then(|s| s.parse().ok()) + .expect("Missing lavalink port"), + ) + .build(LavalinkHandler { data }) + .await?; { let mut data = client.data.write().await; data.insert::(StoreData::new()); data.insert::(database); data.insert::(Arc::new(Mutex::new(HashMap::new()))); + data.insert::(HashMap::new()); + data.insert::(Arc::new(lava_client)); } Ok(client) diff --git a/src/commands/misc/clear.rs b/src/commands/misc/clear.rs new file mode 100644 index 0000000..b62e41c --- /dev/null +++ b/src/commands/misc/clear.rs @@ -0,0 +1,38 @@ +use bot_serenityutils::core::SHORT_TIMEOUT; +use bot_serenityutils::ephemeral_message::EphemeralMessage; +use futures::future::BoxFuture; +use serenity::client::Context; +use serenity::framework::standard::macros::command; +use serenity::framework::standard::{Args, CommandResult}; +use serenity::model::channel::Message; +use serenity::Result as SerenityResult; + +#[command] +#[description("Clears the chat (maximum 100 messages)")] +#[usage("[]")] +#[example("20")] +#[min_args(0)] +#[max_args(1)] +#[bucket("general")] +#[required_permissions("MANAGE_MESSAGES")] +async fn clear(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult { + let limit = args.single::().unwrap_or(20); + log::debug!("Deleting messages for channel {}", msg.channel_id); + let messages = msg.channel_id.messages(ctx, |b| b.limit(limit)).await?; + log::debug!("Deleting {} messages", messages.len()); + let futures: Vec>> = messages + .into_iter() + .map(|m| async move { ctx.http.delete_message(m.channel_id.0, m.id.0).await }.boxed()) + .collect(); + log::debug!("Waiting for all messages to be deleted"); + let deleted = futures::future::join_all(futures).await; + let deleted_count = deleted.into_iter().filter(|d| d.is_ok()).count(); + log::debug!("{} Messages deleted", deleted_count); + + EphemeralMessage::create(&ctx.http, msg.channel_id, SHORT_TIMEOUT, |f| { + f.content(format!("Deleted {} messages", deleted_count)) + }) + .await?; + + Ok(()) +} diff --git a/src/commands/misc/mod.rs b/src/commands/misc/mod.rs index 1061916..0e4ad76 100644 --- a/src/commands/misc/mod.rs +++ b/src/commands/misc/mod.rs @@ -2,6 +2,7 @@ use serenity::framework::standard::macros::group; use about::ABOUT_COMMAND; use add_gif::ADD_GIF_COMMAND; +use clear::CLEAR_COMMAND; use gifs::GIFS_COMMAND; use pain::PAIN_COMMAND; use ping::PING_COMMAND; @@ -13,6 +14,7 @@ use timezones::TIMEZONES_COMMAND; mod about; mod add_gif; +mod clear; mod gifs; pub(crate) mod help; mod pain; @@ -25,6 +27,6 @@ mod timezones; #[group] #[commands( - ping, stats, shutdown, time, timezones, qalc, about, add_gif, gifs, pain + ping, stats, shutdown, time, timezones, qalc, about, add_gif, gifs, pain, clear )] pub struct Misc; diff --git a/src/commands/misc/stats.rs b/src/commands/misc/stats.rs index 9575ba4..259c11f 100644 --- a/src/commands/misc/stats.rs +++ b/src/commands/misc/stats.rs @@ -9,9 +9,7 @@ use serenity::prelude::*; use sysinfo::{ProcessExt, SystemExt}; use crate::commands::common::handle_autodelete; -use crate::providers::music::queue::MusicQueue; -use crate::utils::context_data::{get_database_from_context, Store}; -use std::sync::Arc; +use crate::utils::context_data::{get_database_from_context, MusicPlayers}; #[command] #[description("Shows some statistics about the bot")] @@ -94,23 +92,7 @@ async fn stats(ctx: &Context, msg: &Message) -> CommandResult { /// Returns the total number of queues that are not /// flagged to leave async fn get_queue_count(ctx: &Context) -> usize { - let queues: Vec>> = { - let data = ctx.data.read().await; - let store = data.get::().unwrap(); - - store - .music_queues - .iter() - .map(|(_, q)| Arc::clone(q)) - .collect() - }; - let mut count = 0; - for queue in queues { - let queue = queue.lock().await; - if !queue.leave_flag { - count += 1; - } - } - - count + let data = ctx.data.read().await; + let players = data.get::().unwrap(); + players.len() } diff --git a/src/commands/music/clear_queue.rs b/src/commands/music/clear_queue.rs index cf315c0..bd1db27 100644 --- a/src/commands/music/clear_queue.rs +++ b/src/commands/music/clear_queue.rs @@ -1,12 +1,13 @@ use serenity::client::Context; use serenity::framework::standard::macros::command; -use serenity::framework::standard::CommandResult; +use serenity::framework::standard::{CommandResult, CommandError}; use serenity::model::channel::Message; use crate::commands::common::handle_autodelete; -use crate::commands::music::{get_queue_for_guild, DJ_CHECK}; +use crate::commands::music::{get_music_player_for_guild, DJ_CHECK}; use bot_serenityutils::core::SHORT_TIMEOUT; use bot_serenityutils::ephemeral_message::EphemeralMessage; +use crate::messages::music::no_voicechannel::create_no_voicechannel_message; #[command] #[only_in(guilds)] @@ -19,14 +20,16 @@ async fn clear_queue(ctx: &Context, msg: &Message) -> CommandResult { let guild = msg.guild(&ctx.cache).await.unwrap(); log::debug!("Clearing queue for guild {}", guild.id); - let queue = forward_error!( - ctx, - msg.channel_id, - get_queue_for_guild(ctx, &guild.id).await - ); + let player = if let Some(player) = get_music_player_for_guild(ctx, guild.id).await { + player + } else { + return create_no_voicechannel_message(&ctx.http, msg.channel_id) + .await + .map_err(CommandError::from); + }; { - let mut queue_lock = queue.lock().await; - queue_lock.clear(); + let mut player = player.lock().await; + player.queue().clear(); } EphemeralMessage::create(&ctx.http, msg.channel_id, SHORT_TIMEOUT, |m| { diff --git a/src/commands/music/current.rs b/src/commands/music/current.rs index 33f8ce4..8a75007 100644 --- a/src/commands/music/current.rs +++ b/src/commands/music/current.rs @@ -1,12 +1,11 @@ -use std::mem; - use serenity::client::Context; use serenity::framework::standard::macros::command; -use serenity::framework::standard::CommandResult; +use serenity::framework::standard::{CommandError, CommandResult}; use serenity::model::channel::Message; use crate::commands::common::handle_autodelete; -use crate::commands::music::get_queue_for_guild; +use crate::commands::music::get_music_player_for_guild; +use crate::messages::music::no_voicechannel::create_no_voicechannel_message; use crate::messages::music::now_playing::create_now_playing_msg; #[command] @@ -19,29 +18,22 @@ async fn current(ctx: &Context, msg: &Message) -> CommandResult { let guild = msg.guild(&ctx.cache).await.unwrap(); log::debug!("Displaying current song for queue in {}", guild.id); - let queue = forward_error!( - ctx, - msg.channel_id, - get_queue_for_guild(ctx, &guild.id).await - ); - + let player = if let Some(player) = get_music_player_for_guild(ctx, guild.id).await { + player + } else { + return create_no_voicechannel_message(&ctx.http, msg.channel_id) + .await + .map_err(CommandError::from); + }; let current = { - let queue_lock = queue.lock().await; - queue_lock.current().clone() + let mut player = player.lock().await; + player.queue().current().clone() }; - if let Some((current, _)) = current { - let metadata = current.metadata().clone(); - log::trace!("Metadata is {:?}", metadata); - let np_msg = create_now_playing_msg(ctx, queue.clone(), msg.channel_id).await?; - - let mut queue_lock = queue.lock().await; - if let Some(old_np) = mem::replace(&mut queue_lock.now_playing_msg, Some(np_msg)) { - let old_np = old_np.read().await; - if let Ok(message) = old_np.get_message(&ctx.http).await { - let _ = message.delete(ctx).await; - } - } + if let Some(_) = current { + let np_msg = create_now_playing_msg(ctx, player.clone(), msg.channel_id).await?; + let mut player = player.lock().await; + player.set_now_playing(np_msg).await; } handle_autodelete(ctx, msg).await?; diff --git a/src/commands/music/join.rs b/src/commands/music/join.rs index 22b9d37..544e61f 100644 --- a/src/commands/music/join.rs +++ b/src/commands/music/join.rs @@ -4,7 +4,8 @@ use serenity::framework::standard::{Args, CommandResult}; use serenity::model::channel::Message; use crate::commands::common::handle_autodelete; -use crate::commands::music::{get_channel_for_author, is_dj, join_channel}; +use crate::commands::music::{get_channel_for_author, get_music_player_for_guild, is_dj}; +use crate::providers::music::player::MusicPlayer; use bot_serenityutils::core::SHORT_TIMEOUT; use bot_serenityutils::ephemeral_message::EphemeralMessage; use serenity::model::id::ChannelId; @@ -33,8 +34,15 @@ async fn join(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult { get_channel_for_author(&msg.author.id, &guild) ) }; + if get_music_player_for_guild(ctx, guild.id).await.is_some() { + EphemeralMessage::create(&ctx.http, msg.channel_id, SHORT_TIMEOUT, |m| { + m.content("‼️ I'm already in a Voice Channel") + }) + .await?; + return Ok(()); + } log::debug!("Joining channel {} for guild {}", channel_id, guild.id); - join_channel(ctx, channel_id, guild.id).await; + MusicPlayer::join(ctx, guild.id, channel_id, msg.channel_id).await?; EphemeralMessage::create(&ctx.http, msg.channel_id, SHORT_TIMEOUT, |m| { m.content("🎤 Joined the Voice Channel") }) diff --git a/src/commands/music/leave.rs b/src/commands/music/leave.rs index 59431c7..740e67c 100644 --- a/src/commands/music/leave.rs +++ b/src/commands/music/leave.rs @@ -4,8 +4,8 @@ use serenity::framework::standard::CommandResult; use serenity::model::channel::Message; use crate::commands::common::handle_autodelete; -use crate::commands::music::{get_voice_manager, DJ_CHECK}; -use crate::utils::context_data::Store; +use crate::commands::music::DJ_CHECK; +use crate::utils::context_data::MusicPlayers; use bot_serenityutils::core::SHORT_TIMEOUT; use bot_serenityutils::ephemeral_message::EphemeralMessage; @@ -20,40 +20,30 @@ async fn leave(ctx: &Context, msg: &Message) -> CommandResult { let guild = msg.guild(&ctx.cache).await.unwrap(); log::debug!("Leave request received for guild {}", guild.id); - let manager = get_voice_manager(ctx).await; - let queue = { - let mut data = ctx.data.write().await; - let store = data.get_mut::().unwrap(); - store - .music_queues - .remove(&guild.id) - .expect("No queue for guild.") - }; - let queue_lock = queue.lock().await; - let handler = manager.get(guild.id); - - if let Some(handler) = handler { + let manager = songbird::get(ctx).await.unwrap(); + if let Some(handler) = manager.get(guild.id) { let mut handler_lock = handler.lock().await; - handler_lock.remove_all_global_events(); + let _ = handler_lock.leave().await; } - if manager.get(guild.id).is_some() { - if let Some((current, _)) = queue_lock.current() { - current.stop()?; + let mut data = ctx.data.write().await; + let players = data.get_mut::().unwrap(); + + match players.remove(&guild.id.0) { + None => { + EphemeralMessage::create(&ctx.http, msg.channel_id, SHORT_TIMEOUT, |m| { + m.content("‼️ I'm not in a Voice Channel") + }) + .await?; + } + Some(player) => { + let mut player = player.lock().await; + player.stop().await?; + player.delete_now_playing().await?; } - manager.remove(guild.id).await?; - EphemeralMessage::create(&ctx.http, msg.channel_id, SHORT_TIMEOUT, |m| { - m.content("👋 Left the Voice Channel") - }) - .await?; - log::debug!("Left the voice channel"); - } else { - EphemeralMessage::create(&ctx.http, msg.channel_id, SHORT_TIMEOUT, |m| { - m.content("‼️ I'm not in a Voice Channel") - }) - .await?; - log::debug!("Not in a voice channel"); } + manager.remove(guild.id).await?; + handle_autodelete(ctx, msg).await?; Ok(()) diff --git a/src/commands/music/lyrics.rs b/src/commands/music/lyrics.rs index 9115d69..98e63d7 100644 --- a/src/commands/music/lyrics.rs +++ b/src/commands/music/lyrics.rs @@ -1,11 +1,11 @@ use serenity::client::Context; use serenity::framework::standard::macros::command; -use serenity::framework::standard::CommandResult; +use serenity::framework::standard::{CommandError, CommandResult}; use serenity::model::channel::Message; use crate::commands::common::handle_autodelete; -use crate::commands::music::get_queue_for_guild; -use crate::providers::music::lyrics::get_lyrics; +use crate::commands::music::get_music_player_for_guild; +use crate::messages::music::no_voicechannel::create_no_voicechannel_message; #[command] #[only_in(guilds)] @@ -16,40 +16,40 @@ async fn lyrics(ctx: &Context, msg: &Message) -> CommandResult { let guild = msg.guild(&ctx.cache).await.unwrap(); log::debug!("Fetching lyrics for song playing in {}", guild.id); - let queue = forward_error!( - ctx, - msg.channel_id, - get_queue_for_guild(ctx, &guild.id).await - ); - let queue_lock = queue.lock().await; - - if let Some((current, _)) = queue_lock.current() { - log::debug!("Playing music. Fetching lyrics for currently playing song..."); - let metadata = current.metadata(); - let title = metadata.title.clone().unwrap(); - let author = metadata.artist.clone().unwrap(); + let player = if let Some(player) = get_music_player_for_guild(ctx, guild.id).await { + player + } else { + return create_no_voicechannel_message(&ctx.http, msg.channel_id) + .await + .map_err(CommandError::from); + }; - if let Some(lyrics) = get_lyrics(&*author, &*title).await? { - log::trace!("Lyrics for '{}' are {}", title, lyrics); + let (lyrics, current) = { + let mut player = player.lock().await; + let current = player.queue().current().clone(); + (player.lyrics().await?, current) + }; - msg.channel_id - .send_message(ctx, |m| { - m.embed(|e| { - e.title(format!("Lyrics for {} by {}", title, author)) - .description(lyrics) - .footer(|f| f.text("Powered by lyricsovh")) - }) - }) - .await?; - } else { - log::debug!("No lyrics found"); - msg.channel_id.say(ctx, "No lyrics found").await?; - } - } else { + if let Some(lyrics) = lyrics { + let current = current.unwrap(); msg.channel_id - .say(ctx, "I'm not playing music right now") + .send_message(ctx, |m| { + m.embed(|e| { + e.title(format!( + "Lyrics for {} by {}", + current.title(), + current.author() + )) + .description(lyrics) + .footer(|f| f.text("Powered by lyricsovh")) + }) + }) .await?; + } else { + log::debug!("No lyrics found"); + msg.channel_id.say(ctx, "No lyrics found").await?; } + handle_autodelete(ctx, msg).await?; Ok(()) diff --git a/src/commands/music/mod.rs b/src/commands/music/mod.rs index 1e0ab1d..1e011c2 100644 --- a/src/commands/music/mod.rs +++ b/src/commands/music/mod.rs @@ -1,21 +1,14 @@ -use std::mem; -use std::sync::atomic::{AtomicIsize, AtomicUsize, Ordering}; use std::sync::Arc; -use std::time::Duration; use aspotify::Track; use regex::Regex; -use serenity::async_trait; use serenity::client::Context; use serenity::framework::standard::macros::{check, group}; -use serenity::http::Http; use serenity::model::channel::Message; use serenity::model::guild::Guild; use serenity::model::id::{ChannelId, GuildId, UserId}; use serenity::model::user::User; -use songbird::{ - Call, Event, EventContext, EventHandler as VoiceEventHandler, Songbird, TrackEvent, -}; +use songbird::Songbird; use tokio::sync::Mutex; use clear_queue::CLEAR_QUEUE_COMMAND; @@ -34,11 +27,11 @@ use save_playlist::SAVE_PLAYLIST_COMMAND; use shuffle::SHUFFLE_COMMAND; use skip::SKIP_COMMAND; -use crate::messages::music::now_playing::update_now_playing_msg; -use crate::providers::music::queue::{MusicQueue, Song}; +use crate::providers::music::player::MusicPlayer; +use crate::providers::music::queue::Song; use crate::providers::music::{add_youtube_song_to_database, youtube_dl}; use crate::providers::settings::{get_setting, Setting}; -use crate::utils::context_data::{DatabaseContainer, Store}; +use crate::utils::context_data::{DatabaseContainer, MusicPlayers, Store}; use crate::utils::error::{BotError, BotResult}; use bot_database::Database; use futures::future::BoxFuture; @@ -82,116 +75,12 @@ mod skip; )] pub struct Music; -struct SongEndNotifier { - channel_id: ChannelId, - http: Arc, - queue: Arc>, - handler: Arc>, -} - -#[async_trait] -impl VoiceEventHandler for SongEndNotifier { - async fn act(&self, _ctx: &EventContext<'_>) -> Option { - log::debug!("Song ended in {}. Playing next one", self.channel_id); - while !play_next_in_queue(&self.http, &self.channel_id, &self.queue, &self.handler).await { - tokio::time::sleep(Duration::from_millis(100)).await; - } - - None - } -} - -struct ChannelDurationNotifier { - channel_id: ChannelId, - guild_id: GuildId, - count: Arc, - queue: Arc>, - leave_in: Arc, - handler: Arc>, - manager: Arc, -} - -#[async_trait] -impl VoiceEventHandler for ChannelDurationNotifier { - async fn act(&self, _ctx: &EventContext<'_>) -> Option { - let count_before = self.count.fetch_add(1, Ordering::Relaxed); - log::debug!( - "Playing in channel {} for {} minutes", - self.channel_id, - count_before - ); - let queue_lock = self.queue.lock().await; - if queue_lock.leave_flag { - log::debug!("Waiting to leave"); - if self.leave_in.fetch_sub(1, Ordering::Relaxed) <= 0 { - log::debug!("Leaving voice channel"); - { - let mut handler_lock = self.handler.lock().await; - handler_lock.remove_all_global_events(); - } - if let Some((current, _)) = queue_lock.current() { - let _ = current.stop(); - } - let _ = self.manager.remove(self.guild_id).await; - log::debug!("Left the voice channel"); - } - } else { - log::debug!("Resetting leave value"); - self.leave_in.store(5, Ordering::Relaxed) - } - - None - } -} - -/// Joins a voice channel -async fn join_channel(ctx: &Context, channel_id: ChannelId, guild_id: GuildId) -> Arc> { - log::debug!( - "Attempting to join channel {} in guild {}", - channel_id, - guild_id - ); - let manager = songbird::get(ctx) +/// Returns the voice manager from the context +pub async fn get_voice_manager(ctx: &Context) -> Arc { + songbird::get(ctx) .await .expect("Songbird Voice client placed in at initialisation.") - .clone(); - - let (handler, _) = manager.join(guild_id, channel_id).await; - let mut data = ctx.data.write().await; - let store = data.get_mut::().unwrap(); - log::debug!("Creating new queue"); - let queue = Arc::new(Mutex::new(MusicQueue::new())); - - store.music_queues.insert(guild_id, queue.clone()); - { - let mut handler_lock = handler.lock().await; - - log::debug!("Registering track end handler"); - handler_lock.add_global_event( - Event::Track(TrackEvent::End), - SongEndNotifier { - channel_id: channel_id.clone(), - http: ctx.http.clone(), - queue: Arc::clone(&queue), - handler: handler.clone(), - }, - ); - - handler_lock.add_global_event( - Event::Periodic(Duration::from_secs(60), None), - ChannelDurationNotifier { - channel_id, - guild_id, - count: Default::default(), - queue: Arc::clone(&queue), - handler: handler.clone(), - leave_in: Arc::new(AtomicIsize::new(5)), - manager: manager.clone(), - }, - ); - } - - handler + .clone() } /// Returns the voice channel the author is in @@ -203,88 +92,15 @@ fn get_channel_for_author(author_id: &UserId, guild: &Guild) -> BotResult Arc { - songbird::get(ctx) - .await - .expect("Songbird Voice client placed in at initialisation.") - .clone() -} - -/// Returns a reference to a guilds music queue -pub(crate) async fn get_queue_for_guild( +/// Returns the music player for a given guild +pub async fn get_music_player_for_guild( ctx: &Context, - guild_id: &GuildId, -) -> BotResult>> { + guild_id: GuildId, +) -> Option>> { let data = ctx.data.read().await; - let store = data.get::().unwrap(); + let players = data.get::().unwrap(); - let queue = store - .music_queues - .get(guild_id) - .ok_or(BotError::from("I'm not in a Voice Channel"))? - .clone(); - Ok(queue) -} - -/// Plays the next song in the queue -async fn play_next_in_queue( - http: &Arc, - channel_id: &ChannelId, - queue: &Arc>, - handler: &Arc>, -) -> bool { - let mut queue_lock = queue.lock().await; - - if let Some(mut next) = queue_lock.next() { - let url = match next.url().await { - Some(url) => url, - None => { - let _ = channel_id - .say(&http, format!("'{}' not found", next.title())) - .await; - return false; - } - }; - log::debug!("Getting source for song '{}'", url); - let source = match songbird::ytdl(&url).await { - Ok(s) => s, - Err(e) => { - let _ = channel_id - .say( - &http, - format!("Failed to enqueue {}: {:?}", next.title(), e), - ) - .await; - return false; - } - }; - let mut handler_lock = handler.lock().await; - let track = handler_lock.play_only_source(source); - log::trace!("Track is {:?}", track); - - if queue_lock.paused() { - let _ = track.pause(); - } - - if let Some(np) = &queue_lock.now_playing_msg { - if let Err(e) = - update_now_playing_msg(http, np, track.metadata(), queue_lock.paused()).await - { - log::error!("Failed to update now playing message: {:?}", e); - } - } - queue_lock.set_current(track, next); - } else { - if let Some(np) = mem::take(&mut queue_lock.now_playing_msg) { - let np = np.read().await; - if let Ok(message) = np.get_message(http).await { - let _ = message.delete(http).await; - } - } - queue_lock.clear_current(); - } - true + players.get(&guild_id.0).cloned() } /// Returns the list of songs for a given url @@ -473,16 +289,18 @@ async fn get_youtube_song_for_track(database: &Database, track: Track) -> BotRes if let Some(id) = track.id { let entry = database.get_song(&id).await?; - if let Some(song) = &entry { + if let Some(song) = entry { // check if the video is still available - if get_video_information(&song.url).await.is_err() { + log::trace!("Found entry is {:?}", song); + if let Ok(info) = get_video_information(&song.url).await { + return Ok(Some(info.into())); + } else { log::debug!("Video '{}' is not available. Deleting entry", song.url); database.delete_song(song.id).await?; return Ok(None); } } - log::trace!("Found entry is {:?}", entry); - Ok(entry.map(Song::from)) + Ok(None) } else { log::debug!("Track has no ID"); Ok(None) diff --git a/src/commands/music/move_song.rs b/src/commands/music/move_song.rs index 7472ec8..434ec50 100644 --- a/src/commands/music/move_song.rs +++ b/src/commands/music/move_song.rs @@ -1,10 +1,11 @@ use crate::commands::common::handle_autodelete; -use crate::commands::music::{get_queue_for_guild, DJ_CHECK}; +use crate::commands::music::{get_music_player_for_guild, DJ_CHECK}; +use crate::messages::music::no_voicechannel::create_no_voicechannel_message; use bot_serenityutils::core::SHORT_TIMEOUT; use bot_serenityutils::ephemeral_message::EphemeralMessage; use serenity::client::Context; use serenity::framework::standard::macros::command; -use serenity::framework::standard::{Args, CommandResult}; +use serenity::framework::standard::{Args, CommandError, CommandResult}; use serenity::model::channel::Message; #[command] @@ -22,15 +23,17 @@ async fn move_song(ctx: &Context, msg: &Message, mut args: Args) -> CommandResul let pos1 = args.single::()?; let pos2 = args.single::()?; + let player = if let Some(player) = get_music_player_for_guild(ctx, guild.id).await { + player + } else { + return create_no_voicechannel_message(&ctx.http, msg.channel_id) + .await + .map_err(CommandError::from); + }; { - let queue = forward_error!( - ctx, - msg.channel_id, - get_queue_for_guild(ctx, &guild.id).await - ); - let mut queue_lock = queue.lock().await; - queue_lock.move_position(pos1, pos2); + let mut player = player.lock().await; + player.queue().move_position(pos1, pos2); } EphemeralMessage::create(&ctx.http, msg.channel_id, SHORT_TIMEOUT, |m| { m.content(format!( diff --git a/src/commands/music/pause.rs b/src/commands/music/pause.rs index 3b0de63..99862c4 100644 --- a/src/commands/music/pause.rs +++ b/src/commands/music/pause.rs @@ -1,11 +1,11 @@ use serenity::framework::standard::macros::command; -use serenity::framework::standard::CommandResult; +use serenity::framework::standard::{CommandError, CommandResult}; use serenity::model::channel::Message; use serenity::prelude::*; use crate::commands::common::handle_autodelete; -use crate::commands::music::{get_queue_for_guild, DJ_CHECK}; -use crate::messages::music::now_playing::update_now_playing_msg; +use crate::commands::music::{get_music_player_for_guild, DJ_CHECK}; +use crate::messages::music::no_voicechannel::create_no_voicechannel_message; use bot_serenityutils::core::SHORT_TIMEOUT; use bot_serenityutils::ephemeral_message::EphemeralMessage; @@ -19,37 +19,33 @@ async fn pause(ctx: &Context, msg: &Message) -> CommandResult { let guild = msg.guild(&ctx.cache).await.unwrap(); log::debug!("Pausing playback for guild {}", guild.id); - let queue = forward_error!( - ctx, - msg.channel_id, - get_queue_for_guild(ctx, &guild.id).await - ); - let mut queue_lock = queue.lock().await; + let player = if let Some(player) = get_music_player_for_guild(ctx, guild.id).await { + player + } else { + return create_no_voicechannel_message(&ctx.http, msg.channel_id) + .await + .map_err(CommandError::from); + }; + let mut player = player.lock().await; + + if let Some(_) = player.queue().current() { + player.toggle_paused().await?; + let is_paused = player.is_paused(); - if let Some(_) = queue_lock.current() { - queue_lock.pause(); - if queue_lock.paused() { + if is_paused { log::debug!("Paused"); EphemeralMessage::create(&ctx.http, msg.channel_id, SHORT_TIMEOUT, |m| { m.content("⏸️ Paused playback️") }) .await?; - if let (Some(menu), Some((current, _))) = - (&queue_lock.now_playing_msg, queue_lock.current()) - { - update_now_playing_msg(&ctx.http, menu, current.metadata(), true).await?; - } + player.update_now_playing().await?; } else { log::debug!("Resumed"); EphemeralMessage::create(&ctx.http, msg.channel_id, SHORT_TIMEOUT, |m| { m.content("▶ Resumed playback️") }) .await?; - if let (Some(menu), Some((current, _))) = - (&queue_lock.now_playing_msg, queue_lock.current()) - { - update_now_playing_msg(&ctx.http, menu, current.metadata(), true).await?; - } + player.update_now_playing().await?; } } else { msg.channel_id.say(ctx, "Nothing to pause").await?; diff --git a/src/commands/music/play.rs b/src/commands/music/play.rs index 00e022e..9396cdc 100644 --- a/src/commands/music/play.rs +++ b/src/commands/music/play.rs @@ -1,15 +1,16 @@ use serenity::client::Context; use serenity::framework::standard::macros::command; -use serenity::framework::standard::{Args, CommandError, CommandResult}; +use serenity::framework::standard::{Args, CommandResult}; use serenity::model::channel::Message; use crate::commands::common::handle_autodelete; use crate::commands::music::{ - get_channel_for_author, get_queue_for_guild, get_songs_for_query, get_voice_manager, - join_channel, play_next_in_queue, + get_channel_for_author, get_music_player_for_guild, get_songs_for_query, }; use crate::messages::music::now_playing::create_now_playing_msg; +use crate::providers::music::player::MusicPlayer; use crate::providers::settings::{get_setting, Setting}; +use std::sync::Arc; #[command] #[only_in(guilds)] @@ -24,31 +25,22 @@ async fn play(ctx: &Context, msg: &Message, args: Args) -> CommandResult { let guild = msg.guild(&ctx.cache).await.unwrap(); log::debug!("Play request received for guild {}", guild.id); - let manager = get_voice_manager(ctx).await; - let mut handler = manager.get(guild.id); + let mut player = get_music_player_for_guild(ctx, guild.id).await; - if handler.is_none() { + if player.is_none() { log::debug!("Not in a channel. Joining authors channel..."); - 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 music_player = MusicPlayer::join(ctx, guild.id, channel_id, msg.channel_id).await?; + player = Some(music_player); } - - let handler_lock = forward_error!( - ctx, - msg.channel_id, - handler.ok_or(CommandError::from("I'm not in a voice channel")) - ); - + let player = player.unwrap(); let songs = get_songs_for_query(&ctx, msg, query).await?; - let queue = get_queue_for_guild(ctx, &guild.id).await?; - let (play_first, create_now_playing) = { log::debug!("Adding song to queue"); - let mut queue_lock = queue.lock().await; + let mut player_lock = player.lock().await; for song in songs { - queue_lock.add(song); + player_lock.queue().add(song); } let autoshuffle = get_setting(ctx, guild.id, Setting::MusicAutoShuffle) .await? @@ -56,22 +48,23 @@ async fn play(ctx: &Context, msg: &Message, args: Args) -> CommandResult { if autoshuffle { log::debug!("Autoshuffeling"); - queue_lock.shuffle(); + player_lock.queue().shuffle(); } ( - queue_lock.current().is_none(), - queue_lock.now_playing_msg.is_none(), + player_lock.queue().current().is_none(), + player_lock.now_playing_message().is_none(), ) }; if play_first { log::debug!("Playing first song in queue"); - while !play_next_in_queue(&ctx.http, &msg.channel_id, &queue, &handler_lock).await {} + let mut player_lock = player.lock().await; + player_lock.play_next().await?; } if create_now_playing { - let handle = create_now_playing_msg(ctx, queue.clone(), msg.channel_id).await?; - let mut queue_lock = queue.lock().await; - queue_lock.now_playing_msg = Some(handle); + let handle = create_now_playing_msg(ctx, Arc::clone(&player), msg.channel_id).await?; + let mut player_lock = player.lock().await; + player_lock.set_now_playing(handle).await; } handle_autodelete(ctx, msg).await?; diff --git a/src/commands/music/play_next.rs b/src/commands/music/play_next.rs index b188bfc..ab62242 100644 --- a/src/commands/music/play_next.rs +++ b/src/commands/music/play_next.rs @@ -1,14 +1,15 @@ use serenity::client::Context; use serenity::framework::standard::macros::command; -use serenity::framework::standard::{Args, CommandError, CommandResult}; +use serenity::framework::standard::{Args, CommandResult}; use serenity::model::channel::Message; use crate::commands::common::handle_autodelete; use crate::commands::music::{ - get_channel_for_author, get_queue_for_guild, get_songs_for_query, get_voice_manager, - join_channel, play_next_in_queue, DJ_CHECK, + get_channel_for_author, get_music_player_for_guild, get_songs_for_query, DJ_CHECK, }; use crate::messages::music::now_playing::create_now_playing_msg; +use crate::providers::music::player::MusicPlayer; +use std::sync::Arc; #[command] #[only_in(guilds)] @@ -23,46 +24,41 @@ async fn play_next(ctx: &Context, msg: &Message, args: Args) -> CommandResult { let guild = msg.guild(&ctx.cache).await.unwrap(); log::debug!("Playing song as next song for guild {}", guild.id); - let manager = get_voice_manager(ctx).await; - let mut handler = manager.get(guild.id); - if handler.is_none() { - log::debug!("Not in a voice channel. Joining authors channel"); - msg.guild(&ctx.cache).await.unwrap(); + let mut player = get_music_player_for_guild(ctx, guild.id).await; + + if player.is_none() { + log::debug!("Not in a channel. Joining authors channel..."); let channel_id = get_channel_for_author(&msg.author.id, &guild)?; - handler = Some(join_channel(ctx, channel_id, guild.id).await); + let music_player = MusicPlayer::join(ctx, guild.id, channel_id, msg.channel_id).await?; + player = Some(music_player); } - let handler = forward_error!( - ctx, - msg.channel_id, - handler.ok_or(CommandError::from("I'm not in a voice channel")) - ); - + let player = player.unwrap(); let mut songs = get_songs_for_query(&ctx, msg, query).await?; - let queue = get_queue_for_guild(ctx, &guild.id).await?; let (play_first, create_now_playing) = { - let mut queue_lock = queue.lock().await; + let mut player_lock = player.lock().await; songs.reverse(); log::debug!("Enqueueing songs as next songs in the queue"); for song in songs { - queue_lock.add_next(song); + player_lock.queue().add_next(song); } ( - queue_lock.current().is_none(), - queue_lock.now_playing_msg.is_none(), + player_lock.queue().current().is_none(), + player_lock.now_playing_message().is_none(), ) }; if play_first { - while !play_next_in_queue(&ctx.http, &msg.channel_id, &queue, &handler).await {} + let mut player_lock = player.lock().await; + player_lock.play_next().await?; } if create_now_playing { - let handle = create_now_playing_msg(ctx, queue.clone(), msg.channel_id).await?; - let mut queue_lock = queue.lock().await; - queue_lock.now_playing_msg = Some(handle); + let handle = create_now_playing_msg(ctx, Arc::clone(&player), msg.channel_id).await?; + let mut player_lock = player.lock().await; + player_lock.set_now_playing(handle).await; } handle_autodelete(ctx, msg).await?; diff --git a/src/commands/music/queue.rs b/src/commands/music/queue.rs index cb7ba95..2e611a2 100644 --- a/src/commands/music/queue.rs +++ b/src/commands/music/queue.rs @@ -1,10 +1,11 @@ use serenity::client::Context; use serenity::framework::standard::macros::command; -use serenity::framework::standard::{Args, CommandResult}; +use serenity::framework::standard::{Args, CommandError, CommandResult}; use serenity::model::channel::Message; use crate::commands::common::handle_autodelete; -use crate::commands::music::get_queue_for_guild; +use crate::commands::music::get_music_player_for_guild; +use crate::messages::music::no_voicechannel::create_no_voicechannel_message; use crate::messages::music::queue::create_queue_menu; use crate::providers::music::queue::Song; @@ -23,13 +24,16 @@ async fn queue(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult { .map(|s| s.unwrap().to_lowercase()) .collect::>(); - let queue = forward_error!( - ctx, - msg.channel_id, - get_queue_for_guild(ctx, &guild.id).await - ); - let queue_lock = queue.lock().await; - let songs: Vec<(usize, Song)> = queue_lock + let player = if let Some(player) = get_music_player_for_guild(ctx, guild.id).await { + player + } else { + return create_no_voicechannel_message(&ctx.http, msg.channel_id) + .await + .map_err(CommandError::from); + }; + let mut player = player.lock().await; + let songs: Vec<(usize, Song)> = player + .queue() .entries() .into_iter() .enumerate() diff --git a/src/commands/music/remove_song.rs b/src/commands/music/remove_song.rs index a244921..7537fc5 100644 --- a/src/commands/music/remove_song.rs +++ b/src/commands/music/remove_song.rs @@ -1,10 +1,11 @@ use crate::commands::common::handle_autodelete; -use crate::commands::music::{get_queue_for_guild, DJ_CHECK}; +use crate::commands::music::{get_music_player_for_guild, DJ_CHECK}; +use crate::messages::music::no_voicechannel::create_no_voicechannel_message; use bot_serenityutils::core::SHORT_TIMEOUT; use bot_serenityutils::ephemeral_message::EphemeralMessage; use serenity::client::Context; use serenity::framework::standard::macros::command; -use serenity::framework::standard::{Args, CommandResult}; +use serenity::framework::standard::{Args, CommandError, CommandResult}; use serenity::model::channel::Message; #[command] @@ -21,15 +22,17 @@ async fn remove_song(ctx: &Context, msg: &Message, mut args: Args) -> CommandRes log::debug!("Moving song for guild {}", guild.id); let pos = args.single::()?; + let player = if let Some(player) = get_music_player_for_guild(ctx, guild.id).await { + player + } else { + return create_no_voicechannel_message(&ctx.http, msg.channel_id) + .await + .map_err(CommandError::from); + }; { - let queue = forward_error!( - ctx, - msg.channel_id, - get_queue_for_guild(ctx, &guild.id).await - ); - let mut queue_lock = queue.lock().await; - queue_lock.remove(pos); + let mut player = player.lock().await; + player.queue().remove(pos); } EphemeralMessage::create(&ctx.http, msg.channel_id, SHORT_TIMEOUT, |m| { diff --git a/src/commands/music/shuffle.rs b/src/commands/music/shuffle.rs index 115e3ea..5980cca 100644 --- a/src/commands/music/shuffle.rs +++ b/src/commands/music/shuffle.rs @@ -1,10 +1,11 @@ use serenity::client::Context; use serenity::framework::standard::macros::command; -use serenity::framework::standard::CommandResult; +use serenity::framework::standard::{CommandError, CommandResult}; use serenity::model::channel::Message; use crate::commands::common::handle_autodelete; -use crate::commands::music::{get_queue_for_guild, DJ_CHECK}; +use crate::commands::music::{get_music_player_for_guild, DJ_CHECK}; +use crate::messages::music::no_voicechannel::create_no_voicechannel_message; use bot_serenityutils::core::SHORT_TIMEOUT; use bot_serenityutils::ephemeral_message::EphemeralMessage; @@ -19,14 +20,16 @@ async fn shuffle(ctx: &Context, msg: &Message) -> CommandResult { let guild = msg.guild(&ctx.cache).await.unwrap(); log::debug!("Shuffling queue for guild {}", guild.id); - let queue = forward_error!( - ctx, - msg.channel_id, - get_queue_for_guild(ctx, &guild.id).await - ); + let player = if let Some(player) = get_music_player_for_guild(ctx, guild.id).await { + player + } else { + return create_no_voicechannel_message(&ctx.http, msg.channel_id) + .await + .map_err(CommandError::from); + }; { - let mut queue_lock = queue.lock().await; - queue_lock.shuffle(); + let mut player = player.lock().await; + player.queue().shuffle(); } EphemeralMessage::create(&ctx.http, msg.channel_id, SHORT_TIMEOUT, |m| { diff --git a/src/commands/music/skip.rs b/src/commands/music/skip.rs index 596e136..f16333d 100644 --- a/src/commands/music/skip.rs +++ b/src/commands/music/skip.rs @@ -1,10 +1,11 @@ use serenity::client::Context; use serenity::framework::standard::macros::command; -use serenity::framework::standard::CommandResult; +use serenity::framework::standard::{CommandError, CommandResult}; use serenity::model::channel::Message; use crate::commands::common::handle_autodelete; -use crate::commands::music::{get_queue_for_guild, DJ_CHECK}; +use crate::commands::music::{get_music_player_for_guild, DJ_CHECK}; +use crate::messages::music::no_voicechannel::create_no_voicechannel_message; use bot_serenityutils::core::SHORT_TIMEOUT; use bot_serenityutils::ephemeral_message::EphemeralMessage; @@ -18,15 +19,17 @@ use bot_serenityutils::ephemeral_message::EphemeralMessage; async fn skip(ctx: &Context, msg: &Message) -> CommandResult { let guild = msg.guild(&ctx.cache).await.unwrap(); log::debug!("Skipping song for guild {}", guild.id); - let queue = forward_error!( - ctx, - msg.channel_id, - get_queue_for_guild(ctx, &guild.id).await - ); - let queue_lock = queue.lock().await; - if let Some((current, _)) = queue_lock.current() { - current.stop()?; + let player = if let Some(player) = get_music_player_for_guild(ctx, guild.id).await { + player + } else { + return create_no_voicechannel_message(&ctx.http, msg.channel_id) + .await + .map_err(CommandError::from); + }; + { + let mut player = player.lock().await; + player.skip().await?; } EphemeralMessage::create(&ctx.http, msg.channel_id, SHORT_TIMEOUT, |m| { diff --git a/src/handler.rs b/src/handler.rs index 5bd3da4..99fa0e7 100644 --- a/src/handler.rs +++ b/src/handler.rs @@ -8,7 +8,7 @@ use serenity::model::id::{ChannelId, GuildId, MessageId}; use serenity::model::voice::VoiceState; use serenity::prelude::*; -use crate::commands::music::get_queue_for_guild; +use crate::commands::music::get_music_player_for_guild; use crate::utils::delete_messages_from_database; use bot_serenityutils::menu::{ handle_message_delete, handle_message_delete_bulk, handle_reaction_add, handle_reaction_remove, @@ -120,10 +120,10 @@ impl EventHandler for Handler { if let Some(count) = member_count { log::debug!("{} Members in channel", count); - if let Ok(queue) = get_queue_for_guild(&ctx, &guild_id).await { - let mut queue_lock = queue.lock().await; + if let Some(player) = get_music_player_for_guild(&ctx, guild_id).await { + let mut player = player.lock().await; log::debug!("Setting leave flag to {}", count == 0); - queue_lock.leave_flag = count == 0; + player.set_leave_flag(count == 0); } } } diff --git a/src/messages/music/mod.rs b/src/messages/music/mod.rs index 10bc688..0a2d747 100644 --- a/src/messages/music/mod.rs +++ b/src/messages/music/mod.rs @@ -1,2 +1,3 @@ +pub mod no_voicechannel; pub mod now_playing; pub mod queue; diff --git a/src/messages/music/no_voicechannel.rs b/src/messages/music/no_voicechannel.rs new file mode 100644 index 0000000..9616cd9 --- /dev/null +++ b/src/messages/music/no_voicechannel.rs @@ -0,0 +1,19 @@ +use crate::utils::error::BotResult; +use bot_serenityutils::core::SHORT_TIMEOUT; +use bot_serenityutils::ephemeral_message::EphemeralMessage; +use serenity::http::Http; +use serenity::model::prelude::ChannelId; +use std::sync::Arc; + +/// Creates a not in a voicechannel message +pub async fn create_no_voicechannel_message( + http: &Arc, + channel_id: ChannelId, +) -> BotResult<()> { + EphemeralMessage::create(http, channel_id, SHORT_TIMEOUT, |m| { + m.content("‼️ I'm not in a Voice Channel") + }) + .await?; + + Ok(()) +} diff --git a/src/messages/music/now_playing.rs b/src/messages/music/now_playing.rs index 2e8a7c8..01b2182 100644 --- a/src/messages/music/now_playing.rs +++ b/src/messages/music/now_playing.rs @@ -3,13 +3,13 @@ use std::sync::Arc; use serenity::builder::CreateEmbed; use serenity::http::Http; use serenity::model::prelude::ChannelId; -use songbird::input::Metadata; -use crate::commands::music::{get_queue_for_guild, get_voice_manager, is_dj}; +use crate::commands::music::{get_music_player_for_guild, get_voice_manager, is_dj}; use crate::messages::add_ephemeral_handle_to_database; use crate::providers::music::add_youtube_song_to_database; -use crate::providers::music::queue::MusicQueue; -use crate::utils::context_data::{DatabaseContainer, Store}; +use crate::providers::music::player::MusicPlayer; +use crate::providers::music::queue::Song; +use crate::utils::context_data::{DatabaseContainer, MusicPlayers, Store}; use crate::utils::error::*; use bot_serenityutils::core::MessageHandle; use bot_serenityutils::error::SerenityUtilsResult; @@ -30,7 +30,7 @@ static GOOD_PICK_BUTTON: &str = "👍"; /// Creates a new now playing message and returns the embed for that message pub async fn create_now_playing_msg( ctx: &Context, - queue: Arc>, + player: Arc>, channel_id: ChannelId, ) -> BotResult>> { log::debug!("Creating now playing menu"); @@ -61,16 +61,20 @@ pub async fn create_now_playing_msg( ) .show_help() .add_page(Page::new_builder(move || { - let queue = Arc::clone(&queue); + let player = Arc::clone(&player); Box::pin(async move { log::debug!("Creating now playing embed for page"); - let queue = queue.lock().await; - log::debug!("Queue locked"); + let mut player = player.lock().await; + log::debug!("player locked"); let mut page = CreateMessage::default(); - if let Some((current, _)) = queue.current() { + if let Some(mut current) = player.queue().current().clone() { + let mut embed = CreateEmbed::default(); + create_now_playing_embed(&mut current, &mut embed, player.is_paused(), nsfw) + .await; page.embed(|e| { - create_now_playing_embed(current.metadata(), e, queue.paused(), nsfw) + e.0.clone_from(&embed.0); + e }); } else { page.embed(|e| e.description("Queue is empty")); @@ -94,7 +98,7 @@ pub async fn create_now_playing_msg( pub async fn update_now_playing_msg( http: &Arc, handle: &Arc>, - meta: &Metadata, + song: &mut Song, paused: bool, ) -> BotResult<()> { log::debug!("Updating now playing message"); @@ -102,9 +106,14 @@ pub async fn update_now_playing_msg( let mut message = handle.get_message(http).await?; let nsfw = http.get_channel(handle.channel_id).await?.is_nsfw(); + let mut embed = CreateEmbed::default(); + create_now_playing_embed(song, &mut embed, paused, nsfw).await; message .edit(http, |m| { - m.embed(|e| create_now_playing_embed(meta, e, paused, nsfw)) + m.embed(|e| { + e.0.clone_from(&embed.0); + e + }) }) .await?; log::debug!("Message updated."); @@ -113,19 +122,20 @@ pub async fn update_now_playing_msg( } /// Creates the embed of the now playing message -fn create_now_playing_embed<'a>( - meta: &Metadata, +async fn create_now_playing_embed<'a>( + song: &mut Song, mut embed: &'a mut CreateEmbed, paused: bool, nsfw: bool, ) -> &'a mut CreateEmbed { + let url = song.url().await.unwrap(); embed = embed .title(if paused { "Paused" } else { "Playing" }) .description(format!( "[{}]({}) by {}", - meta.title.clone().unwrap(), - meta.source_url.clone().unwrap(), - meta.artist.clone().unwrap() + song.title().clone(), + url, + song.author().clone() )) .footer(|f| { f.text(format!( @@ -135,7 +145,7 @@ fn create_now_playing_embed<'a>( }); if nsfw { - if let Some(thumb) = meta.thumbnail.clone() { + if let Some(thumb) = song.thumbnail().clone() { embed = embed.thumbnail(thumb); } } @@ -157,22 +167,22 @@ async fn play_pause_button_action( return Ok(()); } { - let queue = get_queue_for_guild(ctx, &guild_id).await?; + let player = get_music_player_for_guild(ctx, guild_id).await.unwrap(); let (current, message, paused) = { log::debug!("Queue is locked"); - let mut queue = queue.lock().await; - queue.pause(); + let mut player = player.lock().await; + player.toggle_paused().await?; ( - queue.current().clone(), - queue.now_playing_msg.clone().unwrap(), - queue.paused(), + player.queue().current().clone(), + player.now_playing_message().clone().unwrap(), + player.is_paused(), ) }; log::debug!("Queue is unlocked"); - if let Some((current, _)) = current { - update_now_playing_msg(&ctx.http, &message, current.metadata(), paused).await?; + if let Some(mut current) = current { + update_now_playing_msg(&ctx.http, &message, &mut current, paused).await?; } } @@ -191,16 +201,11 @@ async fn skip_button_action( if !is_dj(ctx, guild_id, &user).await? { return Ok(()); } - { - let current = { - let queue = get_queue_for_guild(ctx, &guild_id).await?; - let queue = queue.lock().await; - queue.current().clone() - }; - if let Some((current, _)) = current { - let _ = current.stop(); - } + { + let player = get_music_player_for_guild(ctx, guild_id).await.unwrap(); + let mut player = player.lock().await; + player.skip().await?; } Ok(()) @@ -220,21 +225,24 @@ async fn stop_button_action( } { let manager = get_voice_manager(ctx).await; - let queue = get_queue_for_guild(ctx, &guild_id).await?; - let queue = queue.lock().await; let handler = manager.get(guild_id); if let Some(handler) = handler { let mut handler_lock = handler.lock().await; - handler_lock.remove_all_global_events(); - } - if let Some(current) = queue.current() { - current.0.stop().map_err(BotError::from)?; + let _ = handler_lock.leave().await; } if manager.get(guild_id).is_some() { manager.remove(guild_id).await.map_err(BotError::from)?; + let mut data = ctx.data.write().await; + let players = data.get_mut::().unwrap(); + + if let Some(player) = players.remove(&guild_id.0) { + let mut player = player.lock().await; + player.stop().await?; + } + log::debug!("Left the voice channel"); } else { log::debug!("Not in a voice channel"); @@ -257,10 +265,10 @@ async fn good_pick_action( reaction: Reaction, ) -> SerenityUtilsResult<()> { let guild_id = reaction.guild_id.unwrap(); - let queue = get_queue_for_guild(ctx, &guild_id).await?; - let queue = queue.lock().await; + let player = get_music_player_for_guild(ctx, guild_id).await.unwrap(); + let mut player = player.lock().await; - if let Some((_, song)) = queue.current() { + if let Some(song) = player.queue().current() { let data = ctx.data.read().await; let store = data.get::().unwrap(); let database = data.get::().unwrap(); @@ -281,9 +289,9 @@ async fn delete_action( handle.clone() }; { - let queue = get_queue_for_guild(ctx, &guild_id).await?; - let mut queue = queue.lock().await; - queue.now_playing_msg = None; + let player = get_music_player_for_guild(ctx, guild_id).await.unwrap(); + let mut player = player.lock().await; + player.clear_now_playing(); } ctx.http .delete_message(handle.channel_id, handle.message_id) diff --git a/src/providers/music/lavalink.rs b/src/providers/music/lavalink.rs new file mode 100644 index 0000000..ea10954 --- /dev/null +++ b/src/providers/music/lavalink.rs @@ -0,0 +1,44 @@ +use crate::utils::context_data::MusicPlayers; +use lavalink_rs::gateway::LavalinkEventHandler; +use lavalink_rs::model::{TrackFinish, TrackStart}; +use lavalink_rs::LavalinkClient; +use serenity::async_trait; +use serenity::prelude::TypeMapKey; +use std::sync::Arc; +use tokio::sync::RwLock; +use typemap_rev::TypeMap; + +pub struct LavalinkHandler { + pub data: Arc>, +} + +#[async_trait] +impl LavalinkEventHandler for LavalinkHandler { + async fn track_start(&self, _client: LavalinkClient, event: TrackStart) { + log::info!("Track started!\nGuild: {}", event.guild_id); + } + async fn track_finish(&self, _: LavalinkClient, event: TrackFinish) { + log::info!("Track finished!\nGuild: {}", event.guild_id); + let player = { + let data = self.data.read().await; + let players = data.get::().unwrap(); + + players.get(&event.guild_id).cloned() + }; + if let Some(player) = player { + let mut player = player.lock().await; + if let Err(e) = player.play_next().await { + log::error!("Failed to play next song: {:?}", e); + } + if let Err(e) = player.update_now_playing().await { + log::error!("Failed to update now playing embed: {:?}", e); + } + } + } +} + +pub struct Lavalink; + +impl TypeMapKey for Lavalink { + type Value = Arc; +} diff --git a/src/providers/music/mod.rs b/src/providers/music/mod.rs index ec17eaa..a9cf669 100644 --- a/src/providers/music/mod.rs +++ b/src/providers/music/mod.rs @@ -7,11 +7,13 @@ use regex::Regex; use responses::VideoInformation; use youtube_dl::search_video_information; -pub(crate) mod lyrics; -pub(crate) mod queue; -pub(crate) mod responses; -pub(crate) mod spotify; -pub(crate) mod youtube_dl; +pub mod lavalink; +pub mod lyrics; +pub mod player; +pub mod queue; +pub mod responses; +pub mod spotify; +pub mod youtube_dl; /// Searches for a youtube video for the specified song pub(crate) async fn song_to_youtube_video(song: &Song) -> BotResult> { @@ -38,10 +40,12 @@ pub(crate) async fn song_to_youtube_video(song: &Song) -> BotResult, + http: Arc, + queue: MusicQueue, + guild_id: GuildId, + now_playing_msg: Option>>, + msg_channel: ChannelId, + leave_flag: bool, + paused: bool, +} + +impl MusicPlayer { + /// Creates a new music player + pub fn new( + client: Arc, + http: Arc, + guild_id: GuildId, + msg_channel: ChannelId, + ) -> Self { + Self { + client, + http, + guild_id, + queue: MusicQueue::new(), + msg_channel, + now_playing_msg: None, + leave_flag: false, + paused: false, + } + } + + /// Joins a given voice channel + pub async fn join( + ctx: &Context, + guild_id: GuildId, + voice_channel_id: ChannelId, + msg_channel_id: ChannelId, + ) -> BotResult>> { + let manager = songbird::get(ctx).await.unwrap(); + let (_, connection) = manager.join_gateway(guild_id, voice_channel_id).await; + let connection = connection?; + + let player = { + let mut data = ctx.data.write().await; + let client = data.get::().unwrap(); + client.create_session(&connection).await?; + let player = MusicPlayer::new( + Arc::clone(client), + Arc::clone(&ctx.http), + guild_id, + msg_channel_id, + ); + let player = Arc::new(Mutex::new(player)); + let players = data.get_mut::().unwrap(); + players.insert(guild_id.0, Arc::clone(&player)); + player + }; + + wait_for_disconnect( + Arc::clone(&ctx.data), + Arc::clone(&player), + manager, + guild_id, + ); + + Ok(player) + } + + /// Returns a mutable reference to the inner queue + pub fn queue(&mut self) -> &mut MusicQueue { + &mut self.queue + } + + /// Skips to the next song + pub async fn skip(&mut self) -> BotResult<()> { + self.client.stop(self.guild_id.0).await?; + + Ok(()) + } + + /// Stops playback and leaves the channel + pub async fn stop(&mut self) -> BotResult<()> { + self.queue.clear(); + self.client.stop(self.guild_id.0).await?; + Ok(()) + } + + /// Returns the lyrics for the currently playing song + pub async fn lyrics(&self) -> BotResult> { + if let Some(current) = self.queue.current() { + let title = current.title(); + let artist = current.author(); + get_lyrics(artist, title).await + } else { + Ok(None) + } + } + + /// Plays the next song in the queue + pub async fn play_next(&mut self) -> BotResult<()> { + while !self.try_play_next().await? {} + + Ok(()) + } + + /// Tries to play the next song + pub async fn try_play_next(&mut self) -> BotResult { + let mut next = if let Some(n) = self.queue.next() { + log::trace!("Next is {:?}", n); + n + } else { + return Ok(true); + }; + let url = if let Some(url) = next.url().await { + url + } else { + self.send_error_message(format!( + "‼️ Could not find a video to play for '{}' by '{}'", + next.title(), + next.author() + )) + .await?; + log::debug!("Could not find playable candidate for song."); + return Ok(false); + }; + let query_information = match self.client.auto_search_tracks(url).await { + Ok(i) => i, + Err(e) => { + log::error!("Failed to search for song: {}", e); + self.send_error_message(format!( + "‼️ Failed to retrieve information for song '{}' by '{}': {:?}", + next.title(), + next.author(), + e + )) + .await?; + return Ok(false); + } + }; + + if query_information.tracks.len() == 0 { + return Ok(false); + } + let track = query_information.tracks[0].clone(); + self.client.play(self.guild_id.0, track).start().await?; + self.queue.set_current(next); + + Ok(true) + } + + /// Sets the new now playing message of the queue + pub async fn set_now_playing(&mut self, message: Arc>) { + let _ = self.delete_now_playing().await; + self.now_playing_msg = Some(message) + } + + /// Updates the now playing message + pub async fn update_now_playing(&self) -> BotResult<()> { + if let (Some(current), Some(np)) = (self.queue.current(), &self.now_playing_msg) { + update_now_playing_msg(&self.http, np, &mut current.clone(), self.is_paused()).await?; + } + + Ok(()) + } + + /// Deletes the now playing message + pub async fn delete_now_playing(&mut self) -> BotResult<()> { + if let Some(np) = mem::take(&mut self.now_playing_msg) { + let np = np.read().await; + let msg = np.get_message(&self.http).await?; + msg.delete(&self.http).await?; + } + + Ok(()) + } + + /// Pauses playback + pub async fn toggle_paused(&mut self) -> BotResult<()> { + self.paused = !self.paused; + self.client.set_pause(self.guild_id.0, self.paused).await?; + + Ok(()) + } + + /// Returns if playback is paused + pub fn is_paused(&self) -> bool { + self.paused + } + + /// Returns the now playing message of the player + pub fn now_playing_message(&self) -> &Option>> { + &self.now_playing_msg + } + + /// Deletes the now playing message from the player + pub fn clear_now_playing(&mut self) { + self.now_playing_msg = None; + } + + /// Sets the leave flag to the given value + pub fn set_leave_flag(&mut self, flag: bool) { + self.leave_flag = flag; + } + + /// Sends a play error message to the players test channel + async fn send_error_message(&self, content: String) -> BotResult<()> { + EphemeralMessage::create(&self.http, self.msg_channel, SHORT_TIMEOUT, |m| { + m.content(content) + }) + .await?; + + Ok(()) + } +} + +/// Stats a tokio coroutine to check for player disconnect conditions +fn wait_for_disconnect( + data: Arc>, + player: Arc>, + manager: Arc, + guild_id: GuildId, +) { + let mut leave_in: i32 = 5; + tokio::spawn(async move { + loop { + tokio::time::sleep(Duration::from_secs(60)).await; + if manager.get(guild_id).is_none() { + return; // leave when there's no connection to handle + } + let mut player_lock = player.lock().await; + + if player_lock.leave_flag { + log::debug!("Waiting to leave"); + + if leave_in <= 0 { + log::debug!("Leaving voice channel"); + + if let Some(handler) = manager.get(guild_id) { + let mut handler_lock = handler.lock().await; + let _ = handler_lock.leave().await; + } + + let _ = manager.remove(guild_id).await; + let mut data = data.write().await; + let players = data.get_mut::().unwrap(); + players.remove(&guild_id.0); + let _ = player_lock.stop().await; + let _ = player_lock.delete_now_playing().await; + log::debug!("Left the voice channel"); + return; + } + leave_in -= 1; + } else { + log::debug!("Resetting leave value"); + leave_in = 5 + } + } + }); +} diff --git a/src/providers/music/queue.rs b/src/providers/music/queue.rs index 7929a95..d4cee95 100644 --- a/src/providers/music/queue.rs +++ b/src/providers/music/queue.rs @@ -1,23 +1,17 @@ use std::collections::VecDeque; use aspotify::Track; -use songbird::tracks::TrackHandle; use bot_coreutils::shuffle::Shuffle; use crate::providers::music::responses::{PlaylistEntry, VideoInformation}; use crate::providers::music::song_to_youtube_video; use bot_database::models::YoutubeSong; -use bot_serenityutils::core::MessageHandle; -use std::sync::Arc; -use tokio::sync::RwLock; #[derive(Clone)] pub struct MusicQueue { inner: VecDeque, - current: Option<(TrackHandle, Song)>, - paused: bool, - pub now_playing_msg: Option>>, + current: Option, pub leave_flag: bool, } @@ -26,9 +20,7 @@ impl MusicQueue { Self { inner: VecDeque::new(), current: None, - paused: false, leave_flag: false, - now_playing_msg: None, } } @@ -58,17 +50,12 @@ impl MusicQueue { } /// Sets the currently playing song - pub fn set_current(&mut self, handle: TrackHandle, song: Song) { - self.current = Some((handle, song)) - } - - /// Clears the currently playing song - pub fn clear_current(&mut self) { - self.current = None; + pub fn set_current(&mut self, song: Song) { + self.current = Some(song) } /// Returns the reference to the currently playing song - pub fn current(&self) -> &Option<(TrackHandle, Song)> { + pub fn current(&self) -> &Option { &self.current } @@ -88,26 +75,6 @@ impl MusicQueue { pub fn remove(&mut self, index: usize) { self.inner.remove(index); } - - /// Toggles pause - pub fn pause(&mut self) { - if let Some(current) = &self.current { - if self.paused { - let _ = current.0.play(); - } else { - let _ = current.0.pause(); - } - - self.paused = !self.paused; - } else { - self.paused = false; - } - } - - /// Returns if the queue is paused - pub fn paused(&self) -> bool { - self.paused - } } #[derive(Clone, Debug)] @@ -118,11 +85,11 @@ pub enum SongSource { #[derive(Clone, Debug)] pub struct Song { - url: Option, - title: String, - author: String, - thumbnail: Option, - source: SongSource, + pub(crate) url: Option, + pub(crate) title: String, + pub(crate) author: String, + pub(crate) thumbnail: Option, + pub(crate) source: SongSource, } impl Song { diff --git a/src/utils/context_data.rs b/src/utils/context_data.rs index c2c4751..381304b 100644 --- a/src/utils/context_data.rs +++ b/src/utils/context_data.rs @@ -5,18 +5,16 @@ use std::sync::Arc; use bot_database::Database; use sauce_api::prelude::SauceNao; use serenity::client::Context; -use serenity::model::id::GuildId; use serenity::prelude::TypeMapKey; use tokio::sync::Mutex; -use crate::providers::music::queue::MusicQueue; +use crate::providers::music::player::MusicPlayer; use crate::providers::music::spotify::SpotifyApi; pub struct Store; pub struct StoreData { pub minecraft_data_api: minecraft_data_rs::api::Api, - pub music_queues: HashMap>>, pub spotify_api: SpotifyApi, pub sauce_nao: SauceNao, } @@ -31,7 +29,6 @@ impl StoreData { minecraft_data_api: minecraft_data_rs::api::Api::new( minecraft_data_rs::api::versions::latest_stable().unwrap(), ), - music_queues: HashMap::new(), spotify_api: SpotifyApi::new(), sauce_nao, } @@ -57,3 +54,9 @@ pub async fn get_database_from_context(ctx: &Context) -> Database { database.clone() } + +pub struct MusicPlayers; + +impl TypeMapKey for MusicPlayers { + type Value = HashMap>>; +} diff --git a/src/utils/error.rs b/src/utils/error.rs index 664d77b..ebdc370 100644 --- a/src/utils/error.rs +++ b/src/utils/error.rs @@ -1,4 +1,5 @@ use bot_serenityutils::error::SerenityUtilsError; +use lavalink_rs::error::LavalinkError; use thiserror::Error; pub type BotResult = Result; @@ -44,6 +45,9 @@ pub enum BotError { #[error("YouTube Error: {0}")] YoutubeError(#[from] youtube_metadata::error::YoutubeError), + #[error("Lavalink Error: {0}")] + LavalinkError(#[from] LavalinkError), + #[error("{0}")] Msg(String), }