From de3544f916de41224c688f9345a84a1953b4495d Mon Sep 17 00:00:00 2001 From: trivernis Date: Mon, 19 Apr 2021 13:28:26 +0200 Subject: [PATCH 1/7] Rename docker build to Build Docker Container Signed-off-by: trivernis --- .github/workflows/build-docker.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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: From e6d89289b7785e425fe952d5b40bdbf6eab8c775 Mon Sep 17 00:00:00 2001 From: trivernis Date: Tue, 20 Apr 2021 09:44:12 +0200 Subject: [PATCH 2/7] Add clear command Signed-off-by: trivernis --- src/commands/misc/clear.rs | 38 ++++++++++++++++++++++++++++++++++++++ src/commands/misc/mod.rs | 4 +++- 2 files changed, 41 insertions(+), 1 deletion(-) create mode 100644 src/commands/misc/clear.rs 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; From 6876a1bb1a83752e0216a2f6b0ed0e516ee964de Mon Sep 17 00:00:00 2001 From: trivernis Date: Tue, 20 Apr 2021 12:46:57 +0200 Subject: [PATCH 3/7] Change music backend to lavalink Signed-off-by: trivernis --- Cargo.lock | 51 ++++++++++++++++++++++ Cargo.toml | 3 +- src/client.rs | 18 +++++++- src/commands/music/current.rs | 4 +- src/commands/music/leave.rs | 17 ++------ src/commands/music/lyrics.rs | 7 ++- src/commands/music/mod.rs | 63 ++++++++++++++++++--------- src/commands/music/pause.rs | 23 +++++----- src/commands/music/play.rs | 19 ++++----- src/commands/music/play_next.rs | 17 +++----- src/commands/music/skip.rs | 15 +++---- src/messages/music/now_playing.rs | 71 ++++++++++++++++++------------- src/providers/music/lavalink.rs | 56 ++++++++++++++++++++++++ src/providers/music/mod.rs | 1 + src/providers/music/queue.rs | 45 +++++++++----------- src/utils/error.rs | 4 ++ 16 files changed, 278 insertions(+), 136 deletions(-) create mode 100644 src/providers/music/lavalink.rs 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..4775e5e 100644 --- a/src/client.rs +++ b/src/client.rs @@ -12,28 +12,42 @@ use songbird::SerenityInit; use crate::commands::*; use crate::handler::Handler; +use crate::providers::music::lavalink::{Lavalink, LavalinkHandler}; use crate::utils::context_data::{get_database_from_context, DatabaseContainer, 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 http = client.cache_and_http.http.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_PORT").expect("Missing lavalink port")) + .set_password(env::var("LAVALINK_PASSWORD").expect("Missing lavalink password")) + .build(LavalinkHandler { data, http }) + .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::(lava_client); } Ok(client) diff --git a/src/commands/music/current.rs b/src/commands/music/current.rs index 33f8ce4..caa3aeb 100644 --- a/src/commands/music/current.rs +++ b/src/commands/music/current.rs @@ -30,9 +30,7 @@ async fn current(ctx: &Context, msg: &Message) -> CommandResult { queue_lock.current().clone() }; - if let Some((current, _)) = current { - let metadata = current.metadata().clone(); - log::trace!("Metadata is {:?}", metadata); + if let Some(_) = current { let np_msg = create_now_playing_msg(ctx, queue.clone(), msg.channel_id).await?; let mut queue_lock = queue.lock().await; diff --git a/src/commands/music/leave.rs b/src/commands/music/leave.rs index 59431c7..bd252e1 100644 --- a/src/commands/music/leave.rs +++ b/src/commands/music/leave.rs @@ -5,7 +5,7 @@ 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::providers::music::lavalink::Lavalink; use bot_serenityutils::core::SHORT_TIMEOUT; use bot_serenityutils::ephemeral_message::EphemeralMessage; @@ -21,15 +21,6 @@ async fn leave(ctx: &Context, msg: &Message) -> CommandResult { 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 { @@ -38,10 +29,10 @@ async fn leave(ctx: &Context, msg: &Message) -> CommandResult { } if manager.get(guild.id).is_some() { - if let Some((current, _)) = queue_lock.current() { - current.stop()?; - } manager.remove(guild.id).await?; + let data = ctx.data.read().await; + let player = data.get::().unwrap(); + player.destroy(guild.id.0).await?; EphemeralMessage::create(&ctx.http, msg.channel_id, SHORT_TIMEOUT, |m| { m.content("👋 Left the Voice Channel") }) diff --git a/src/commands/music/lyrics.rs b/src/commands/music/lyrics.rs index 9115d69..b07fc10 100644 --- a/src/commands/music/lyrics.rs +++ b/src/commands/music/lyrics.rs @@ -23,11 +23,10 @@ async fn lyrics(ctx: &Context, msg: &Message) -> CommandResult { ); let queue_lock = queue.lock().await; - if let Some((current, _)) = queue_lock.current() { + if let Some(song) = 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 title = song.title().clone(); + let author = song.author().clone(); if let Some(lyrics) = get_lyrics(&*author, &*title).await? { log::trace!("Lyrics for '{}' are {}", title, lyrics); diff --git a/src/commands/music/mod.rs b/src/commands/music/mod.rs index 1e0ab1d..fad8b7a 100644 --- a/src/commands/music/mod.rs +++ b/src/commands/music/mod.rs @@ -35,6 +35,7 @@ use shuffle::SHUFFLE_COMMAND; use skip::SKIP_COMMAND; use crate::messages::music::now_playing::update_now_playing_msg; +use crate::providers::music::lavalink::Lavalink; use crate::providers::music::queue::{MusicQueue, Song}; use crate::providers::music::{add_youtube_song_to_database, youtube_dl}; use crate::providers::settings::{get_setting, Setting}; @@ -43,7 +44,9 @@ use crate::utils::error::{BotError, BotResult}; use bot_database::Database; use futures::future::BoxFuture; use futures::FutureExt; +use lavalink_rs::LavalinkClient; use serenity::framework::standard::{Args, CommandOptions, Reason}; +use serenity::prelude::{RwLock, TypeMap}; use youtube_metadata::get_video_information; mod clear_queue; @@ -84,16 +87,27 @@ pub struct Music; struct SongEndNotifier { channel_id: ChannelId, + guild_id: GuildId, http: Arc, queue: Arc>, - handler: Arc>, + data: 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 { + let data = self.data.read().await; + let player = data.get::().unwrap(); + while !play_next_in_queue( + &self.http, + &self.channel_id, + &self.guild_id, + &self.queue, + player, + ) + .await + { tokio::time::sleep(Duration::from_millis(100)).await; } @@ -129,9 +143,6 @@ impl VoiceEventHandler for ChannelDurationNotifier { 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"); } @@ -156,11 +167,17 @@ async fn join_channel(ctx: &Context, channel_id: ChannelId, guild_id: GuildId) - .expect("Songbird Voice client placed in at initialisation.") .clone(); - let (handler, _) = manager.join(guild_id, channel_id).await; + let (handler, connection) = manager.join_gateway(guild_id, channel_id).await; + let connection = connection.expect("Failed to join gateway"); let mut data = ctx.data.write().await; + let lava_client = data.get::().unwrap(); + lava_client + .create_session(&connection) + .await + .expect("Failed to create lava session"); let store = data.get_mut::().unwrap(); log::debug!("Creating new queue"); - let queue = Arc::new(Mutex::new(MusicQueue::new())); + let queue = Arc::new(Mutex::new(MusicQueue::new(channel_id))); store.music_queues.insert(guild_id, queue.clone()); { @@ -170,10 +187,11 @@ async fn join_channel(ctx: &Context, channel_id: ChannelId, guild_id: GuildId) - handler_lock.add_global_event( Event::Track(TrackEvent::End), SongEndNotifier { - channel_id: channel_id.clone(), + channel_id, + guild_id, http: ctx.http.clone(), queue: Arc::clone(&queue), - handler: handler.clone(), + data: ctx.data.clone(), }, ); @@ -228,11 +246,12 @@ pub(crate) async fn get_queue_for_guild( } /// Plays the next song in the queue -async fn play_next_in_queue( +pub async fn play_next_in_queue( http: &Arc, channel_id: &ChannelId, + guild_id: &GuildId, queue: &Arc>, - handler: &Arc>, + player: &LavalinkClient, ) -> bool { let mut queue_lock = queue.lock().await; @@ -247,7 +266,8 @@ async fn play_next_in_queue( } }; log::debug!("Getting source for song '{}'", url); - let source = match songbird::ytdl(&url).await { + + let query_information = match player.auto_search_tracks(url).await { Ok(s) => s, Err(e) => { let _ = channel_id @@ -259,22 +279,25 @@ async fn play_next_in_queue( return false; } }; - let mut handler_lock = handler.lock().await; - let track = handler_lock.play_only_source(source); - log::trace!("Track is {:?}", track); + if let Err(e) = player + .play(guild_id.0, query_information.tracks[0].clone()) + .start() + .await + { + log::error!("Failed to play song: {:?}", e); + } + log::trace!("Track is {:?}", query_information.tracks[0]); if queue_lock.paused() { - let _ = track.pause(); + let _ = player.pause(guild_id.0).await; } 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 - { + if let Err(e) = update_now_playing_msg(http, np, &mut next, queue_lock.paused()).await { log::error!("Failed to update now playing message: {:?}", e); } } - queue_lock.set_current(track, next); + queue_lock.set_current(next); } else { if let Some(np) = mem::take(&mut queue_lock.now_playing_msg) { let np = np.read().await; diff --git a/src/commands/music/pause.rs b/src/commands/music/pause.rs index 3b0de63..33deacf 100644 --- a/src/commands/music/pause.rs +++ b/src/commands/music/pause.rs @@ -6,6 +6,7 @@ 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::providers::music::lavalink::Lavalink; use bot_serenityutils::core::SHORT_TIMEOUT; use bot_serenityutils::ephemeral_message::EphemeralMessage; @@ -27,17 +28,21 @@ async fn pause(ctx: &Context, msg: &Message) -> CommandResult { let mut queue_lock = queue.lock().await; if let Some(_) = queue_lock.current() { - queue_lock.pause(); - if queue_lock.paused() { + let is_paused = { + let data = ctx.data.read().await; + let player = data.get::().unwrap(); + player.set_pause(guild.id.0, !queue_lock.paused()).await?; + !queue_lock.paused() + }; + queue_lock.set_paused(is_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?; + if let (Some(menu), Some(song)) = (&queue_lock.now_playing_msg, queue_lock.current()) { + update_now_playing_msg(&ctx.http, menu, &mut song.clone(), true).await?; } } else { log::debug!("Resumed"); @@ -45,10 +50,8 @@ async fn pause(ctx: &Context, msg: &Message) -> CommandResult { 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?; + if let (Some(menu), Some(song)) = (&queue_lock.now_playing_msg, queue_lock.current()) { + update_now_playing_msg(&ctx.http, menu, &mut song.clone(), true).await?; } } } else { diff --git a/src/commands/music/play.rs b/src/commands/music/play.rs index 00e022e..10d274e 100644 --- a/src/commands/music/play.rs +++ b/src/commands/music/play.rs @@ -1,6 +1,6 @@ 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; @@ -9,6 +9,7 @@ use crate::commands::music::{ join_channel, play_next_in_queue, }; use crate::messages::music::now_playing::create_now_playing_msg; +use crate::providers::music::lavalink::Lavalink; use crate::providers::settings::{get_setting, Setting}; #[command] @@ -25,21 +26,15 @@ async fn play(ctx: &Context, msg: &Message, args: Args) -> CommandResult { log::debug!("Play request received for guild {}", guild.id); let manager = get_voice_manager(ctx).await; - let mut handler = manager.get(guild.id); + let handler = manager.get(guild.id); if handler.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); + join_channel(ctx, channel_id, guild.id).await; } - let handler_lock = forward_error!( - ctx, - msg.channel_id, - handler.ok_or(CommandError::from("I'm not in a voice channel")) - ); - let songs = get_songs_for_query(&ctx, msg, query).await?; let queue = get_queue_for_guild(ctx, &guild.id).await?; @@ -66,7 +61,11 @@ async fn play(ctx: &Context, msg: &Message, args: Args) -> CommandResult { 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 data = ctx.data.read().await; + let lava_player = data.get::().unwrap(); + while !play_next_in_queue(&ctx.http, &msg.channel_id, &guild.id, &queue, &lava_player).await + { + } } if create_now_playing { let handle = create_now_playing_msg(ctx, queue.clone(), msg.channel_id).await?; diff --git a/src/commands/music/play_next.rs b/src/commands/music/play_next.rs index b188bfc..d668241 100644 --- a/src/commands/music/play_next.rs +++ b/src/commands/music/play_next.rs @@ -1,6 +1,6 @@ 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; @@ -9,6 +9,7 @@ use crate::commands::music::{ join_channel, play_next_in_queue, DJ_CHECK, }; use crate::messages::music::now_playing::create_now_playing_msg; +use crate::providers::music::lavalink::Lavalink; #[command] #[only_in(guilds)] @@ -24,21 +25,15 @@ 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); + let 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 channel_id = get_channel_for_author(&msg.author.id, &guild)?; - handler = Some(join_channel(ctx, channel_id, guild.id).await); + join_channel(ctx, channel_id, guild.id).await; } - let handler = forward_error!( - ctx, - msg.channel_id, - handler.ok_or(CommandError::from("I'm not in a voice channel")) - ); - let mut songs = get_songs_for_query(&ctx, msg, query).await?; let queue = get_queue_for_guild(ctx, &guild.id).await?; @@ -57,7 +52,9 @@ async fn play_next(ctx: &Context, msg: &Message, args: Args) -> CommandResult { }; if play_first { - while !play_next_in_queue(&ctx.http, &msg.channel_id, &queue, &handler).await {} + let data = ctx.data.read().await; + let player = data.get::().unwrap(); + while !play_next_in_queue(&ctx.http, &msg.channel_id, &guild.id, &queue, &player).await {} } if create_now_playing { let handle = create_now_playing_msg(ctx, queue.clone(), msg.channel_id).await?; diff --git a/src/commands/music/skip.rs b/src/commands/music/skip.rs index 596e136..987abdb 100644 --- a/src/commands/music/skip.rs +++ b/src/commands/music/skip.rs @@ -4,7 +4,8 @@ use serenity::framework::standard::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::DJ_CHECK; +use crate::providers::music::lavalink::Lavalink; use bot_serenityutils::core::SHORT_TIMEOUT; use bot_serenityutils::ephemeral_message::EphemeralMessage; @@ -18,15 +19,11 @@ 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 data = ctx.data.read().await; + let player = data.get::().unwrap(); + player.stop(guild.id.0).await?; } EphemeralMessage::create(&ctx.http, msg.channel_id, SHORT_TIMEOUT, |m| { diff --git a/src/messages/music/now_playing.rs b/src/messages/music/now_playing.rs index 2e8a7c8..67489ac 100644 --- a/src/messages/music/now_playing.rs +++ b/src/messages/music/now_playing.rs @@ -3,12 +3,12 @@ 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::messages::add_ephemeral_handle_to_database; use crate::providers::music::add_youtube_song_to_database; -use crate::providers::music::queue::MusicQueue; +use crate::providers::music::lavalink::Lavalink; +use crate::providers::music::queue::{MusicQueue, Song}; use crate::utils::context_data::{DatabaseContainer, Store}; use crate::utils::error::*; use bot_serenityutils::core::MessageHandle; @@ -68,9 +68,12 @@ pub async fn create_now_playing_msg( log::debug!("Queue locked"); let mut page = CreateMessage::default(); - if let Some((current, _)) = queue.current() { + if let Some(mut current) = queue.current().clone() { + let mut embed = CreateEmbed::default(); + create_now_playing_embed(&mut current, &mut embed, queue.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 +97,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 +105,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 +121,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 +144,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); } } @@ -162,7 +171,16 @@ async fn play_pause_button_action( let (current, message, paused) = { log::debug!("Queue is locked"); let mut queue = queue.lock().await; - queue.pause(); + { + let pause = !queue.paused(); + let data = ctx.data.read().await; + let player = data.get::().unwrap(); + player + .set_pause(guild_id.0, pause) + .await + .map_err(BotError::from)?; + queue.set_paused(pause); + } ( queue.current().clone(), queue.now_playing_msg.clone().unwrap(), @@ -171,8 +189,8 @@ async fn play_pause_button_action( }; 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 +209,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 data = ctx.data.read().await; + let player = data.get::().unwrap(); + player.stop(guild_id.0).await.map_err(BotError::from)?; } Ok(()) @@ -220,8 +233,6 @@ 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); @@ -229,12 +240,12 @@ async fn stop_button_action( 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)?; - } if manager.get(guild_id).is_some() { manager.remove(guild_id).await.map_err(BotError::from)?; + let data = ctx.data.read().await; + let player = data.get::().unwrap(); + player.destroy(guild_id.0).await.map_err(BotError::from)?; log::debug!("Left the voice channel"); } else { log::debug!("Not in a voice channel"); @@ -260,7 +271,7 @@ async fn good_pick_action( let queue = get_queue_for_guild(ctx, &guild_id).await?; let queue = queue.lock().await; - if let Some((_, song)) = queue.current() { + if let Some(song) = queue.current() { let data = ctx.data.read().await; let store = data.get::().unwrap(); let database = data.get::().unwrap(); diff --git a/src/providers/music/lavalink.rs b/src/providers/music/lavalink.rs new file mode 100644 index 0000000..3cbf50e --- /dev/null +++ b/src/providers/music/lavalink.rs @@ -0,0 +1,56 @@ +use crate::commands::music::play_next_in_queue; +use crate::utils::context_data::Store; +use lavalink_rs::gateway::LavalinkEventHandler; +use lavalink_rs::model::{TrackFinish, TrackStart}; +use lavalink_rs::LavalinkClient; +use serenity::async_trait; +use serenity::http::Http; +use serenity::model::id::GuildId; +use serenity::prelude::TypeMapKey; +use std::sync::Arc; +use tokio::sync::RwLock; +use typemap_rev::TypeMap; + +pub struct LavalinkHandler { + pub data: Arc>, + pub http: 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, client: LavalinkClient, event: TrackFinish) { + log::info!("Track finished!\nGuild: {}", event.guild_id); + let queue = { + let data = self.data.read().await; + let store = data.get::().unwrap(); + + store + .music_queues + .get(&GuildId(event.guild_id)) + .unwrap() + .clone() + }; + let channel_id = { + let queue = queue.lock().await; + queue.channel_id() + }; + while !play_next_in_queue( + &self.http, + &channel_id, + &GuildId(event.guild_id), + &queue, + &client, + ) + .await + {} + } +} + +pub struct Lavalink; + +impl TypeMapKey for Lavalink { + type Value = LavalinkClient; +} diff --git a/src/providers/music/mod.rs b/src/providers/music/mod.rs index ec17eaa..00991a6 100644 --- a/src/providers/music/mod.rs +++ b/src/providers/music/mod.rs @@ -7,6 +7,7 @@ use regex::Regex; use responses::VideoInformation; use youtube_dl::search_video_information; +pub(crate) mod lavalink; pub(crate) mod lyrics; pub(crate) mod queue; pub(crate) mod responses; diff --git a/src/providers/music/queue.rs b/src/providers/music/queue.rs index 7929a95..382238c 100644 --- a/src/providers/music/queue.rs +++ b/src/providers/music/queue.rs @@ -1,7 +1,6 @@ use std::collections::VecDeque; use aspotify::Track; -use songbird::tracks::TrackHandle; use bot_coreutils::shuffle::Shuffle; @@ -9,26 +8,29 @@ 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 serenity::model::id::ChannelId; use std::sync::Arc; use tokio::sync::RwLock; #[derive(Clone)] pub struct MusicQueue { inner: VecDeque, - current: Option<(TrackHandle, Song)>, + current: Option, paused: bool, pub now_playing_msg: Option>>, pub leave_flag: bool, + channel_id: ChannelId, } impl MusicQueue { - pub fn new() -> Self { + pub fn new(channel_id: ChannelId) -> Self { Self { inner: VecDeque::new(), current: None, paused: false, leave_flag: false, now_playing_msg: None, + channel_id, } } @@ -58,8 +60,8 @@ impl MusicQueue { } /// Sets the currently playing song - pub fn set_current(&mut self, handle: TrackHandle, song: Song) { - self.current = Some((handle, song)) + pub fn set_current(&mut self, song: Song) { + self.current = Some(song) } /// Clears the currently playing song @@ -68,10 +70,20 @@ impl MusicQueue { } /// Returns the reference to the currently playing song - pub fn current(&self) -> &Option<(TrackHandle, Song)> { + pub fn current(&self) -> &Option { &self.current } + /// Returns if the queue is paused + pub fn paused(&self) -> bool { + self.paused + } + + /// Sets if the queue is paused + pub fn set_paused(&mut self, paused: bool) { + self.paused = paused; + } + /// Clears the queue pub fn clear(&mut self) { self.inner.clear(); @@ -89,24 +101,9 @@ impl MusicQueue { 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 + /// The channel id where the music messages should be sent to + pub fn channel_id(&self) -> ChannelId { + self.channel_id } } 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), } From b2ba31a9e94eb7dcebfa221f07de3d1d6ebf53c4 Mon Sep 17 00:00:00 2001 From: trivernis Date: Tue, 20 Apr 2021 15:03:22 +0200 Subject: [PATCH 4/7] Reimplement all music related functionality in MusicPlayer struct Signed-off-by: trivernis --- src/client.rs | 11 +- src/commands/misc/stats.rs | 26 +-- src/commands/music/clear_queue.rs | 21 ++- src/commands/music/current.rs | 36 ++-- src/commands/music/join.rs | 5 +- src/commands/music/leave.rs | 40 ++--- src/commands/music/lyrics.rs | 63 +++---- src/commands/music/mod.rs | 235 ++------------------------ src/commands/music/move_song.rs | 21 ++- src/commands/music/pause.rs | 41 ++--- src/commands/music/play.rs | 42 ++--- src/commands/music/play_next.rs | 39 +++-- src/commands/music/queue.rs | 22 ++- src/commands/music/remove_song.rs | 21 ++- src/commands/music/shuffle.rs | 21 ++- src/commands/music/skip.rs | 18 +- src/handler.rs | 8 +- src/messages/music/mod.rs | 1 + src/messages/music/no_voicechannel.rs | 19 +++ src/messages/music/now_playing.rs | 64 +++---- src/providers/music/lavalink.rs | 42 ++--- src/providers/music/mod.rs | 13 +- src/providers/music/player.rs | 181 ++++++++++++++++++++ src/providers/music/queue.rs | 32 +--- src/utils/context_data.rs | 11 +- 25 files changed, 480 insertions(+), 553 deletions(-) create mode 100644 src/messages/music/no_voicechannel.rs create mode 100644 src/providers/music/player.rs diff --git a/src/client.rs b/src/client.rs index 4775e5e..a81ad0a 100644 --- a/src/client.rs +++ b/src/client.rs @@ -13,7 +13,9 @@ use songbird::SerenityInit; use crate::commands::*; use crate::handler::Handler; use crate::providers::music::lavalink::{Lavalink, LavalinkHandler}; -use crate::utils::context_data::{get_database_from_context, DatabaseContainer, Store, StoreData}; +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; @@ -35,19 +37,20 @@ pub async fn get_client() -> BotResult { .register_songbird() .await?; let data = client.data.clone(); - let http = client.cache_and_http.http.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_PORT").expect("Missing lavalink port")) .set_password(env::var("LAVALINK_PASSWORD").expect("Missing lavalink password")) - .build(LavalinkHandler { data, http }) + .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::(lava_client); + data.insert::(HashMap::new()); + data.insert::(Arc::new(lava_client)); } Ok(client) 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 caa3aeb..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,27 +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 { - 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; - } - } + 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..717acac 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, 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; @@ -34,7 +35,7 @@ async fn join(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult { ) }; 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).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 bd252e1..ba601c2 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::providers::music::lavalink::Lavalink; +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,31 +20,21 @@ 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 handler = manager.get(guild.id); - - if let Some(handler) = handler { - let mut handler_lock = handler.lock().await; - handler_lock.remove_all_global_events(); + 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?; + } } - if manager.get(guild.id).is_some() { - manager.remove(guild.id).await?; - let data = ctx.data.read().await; - let player = data.get::().unwrap(); - player.destroy(guild.id.0).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"); - } handle_autodelete(ctx, msg).await?; Ok(()) diff --git a/src/commands/music/lyrics.rs b/src/commands/music/lyrics.rs index b07fc10..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,39 +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(song) = queue_lock.current() { - log::debug!("Playing music. Fetching lyrics for currently playing song..."); - let title = song.title().clone(); - let author = song.author().clone(); + 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 fad8b7a..3ff6b3e 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,19 +27,16 @@ 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::lavalink::Lavalink; -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; use futures::FutureExt; -use lavalink_rs::LavalinkClient; use serenity::framework::standard::{Args, CommandOptions, Reason}; -use serenity::prelude::{RwLock, TypeMap}; use youtube_metadata::get_video_information; mod clear_queue; @@ -85,131 +75,12 @@ mod skip; )] pub struct Music; -struct SongEndNotifier { - channel_id: ChannelId, - guild_id: GuildId, - http: Arc, - queue: Arc>, - data: 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); - let data = self.data.read().await; - let player = data.get::().unwrap(); - while !play_next_in_queue( - &self.http, - &self.channel_id, - &self.guild_id, - &self.queue, - player, - ) - .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(); - } - 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, connection) = manager.join_gateway(guild_id, channel_id).await; - let connection = connection.expect("Failed to join gateway"); - let mut data = ctx.data.write().await; - let lava_client = data.get::().unwrap(); - lava_client - .create_session(&connection) - .await - .expect("Failed to create lava session"); - let store = data.get_mut::().unwrap(); - log::debug!("Creating new queue"); - let queue = Arc::new(Mutex::new(MusicQueue::new(channel_id))); - - 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, - guild_id, - http: ctx.http.clone(), - queue: Arc::clone(&queue), - data: ctx.data.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 @@ -221,93 +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 queue = store - .music_queues - .get(guild_id) - .ok_or(BotError::from("I'm not in a Voice Channel"))? - .clone(); - Ok(queue) -} + let players = data.get::().unwrap(); -/// Plays the next song in the queue -pub async fn play_next_in_queue( - http: &Arc, - channel_id: &ChannelId, - guild_id: &GuildId, - queue: &Arc>, - player: &LavalinkClient, -) -> 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 query_information = match player.auto_search_tracks(url).await { - Ok(s) => s, - Err(e) => { - let _ = channel_id - .say( - &http, - format!("Failed to enqueue {}: {:?}", next.title(), e), - ) - .await; - return false; - } - }; - if let Err(e) = player - .play(guild_id.0, query_information.tracks[0].clone()) - .start() - .await - { - log::error!("Failed to play song: {:?}", e); - } - log::trace!("Track is {:?}", query_information.tracks[0]); - - if queue_lock.paused() { - let _ = player.pause(guild_id.0).await; - } - - if let Some(np) = &queue_lock.now_playing_msg { - if let Err(e) = update_now_playing_msg(http, np, &mut next, queue_lock.paused()).await { - log::error!("Failed to update now playing message: {:?}", e); - } - } - queue_lock.set_current(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 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 33deacf..99862c4 100644 --- a/src/commands/music/pause.rs +++ b/src/commands/music/pause.rs @@ -1,12 +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::providers::music::lavalink::Lavalink; +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; @@ -20,39 +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() { - let is_paused = { - let data = ctx.data.read().await; - let player = data.get::().unwrap(); - player.set_pause(guild.id.0, !queue_lock.paused()).await?; - !queue_lock.paused() - }; - queue_lock.set_paused(is_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(song)) = (&queue_lock.now_playing_msg, queue_lock.current()) { - update_now_playing_msg(&ctx.http, menu, &mut song.clone(), 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(song)) = (&queue_lock.now_playing_msg, queue_lock.current()) { - update_now_playing_msg(&ctx.http, menu, &mut song.clone(), 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 10d274e..5f2ec75 100644 --- a/src/commands/music/play.rs +++ b/src/commands/music/play.rs @@ -5,12 +5,12 @@ 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::lavalink::Lavalink; +use crate::providers::music::player::MusicPlayer; use crate::providers::settings::{get_setting, Setting}; +use std::sync::Arc; #[command] #[only_in(guilds)] @@ -25,25 +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 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)?; - join_channel(ctx, channel_id, guild.id).await; + let music_player = MusicPlayer::join(ctx, guild.id, channel_id).await?; + player = Some(music_player); } - + 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? @@ -51,26 +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"); - let data = ctx.data.read().await; - let lava_player = data.get::().unwrap(); - while !play_next_in_queue(&ctx.http, &msg.channel_id, &guild.id, &queue, &lava_player).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 d668241..e738dd3 100644 --- a/src/commands/music/play_next.rs +++ b/src/commands/music/play_next.rs @@ -5,11 +5,11 @@ 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::lavalink::Lavalink; +use crate::providers::music::player::MusicPlayer; +use std::sync::Arc; #[command] #[only_in(guilds)] @@ -24,42 +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 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)?; - join_channel(ctx, channel_id, guild.id).await; + let music_player = MusicPlayer::join(ctx, guild.id, channel_id).await?; + player = Some(music_player); } + 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 { - let data = ctx.data.read().await; - let player = data.get::().unwrap(); - while !play_next_in_queue(&ctx.http, &msg.channel_id, &guild.id, &queue, &player).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 987abdb..f16333d 100644 --- a/src/commands/music/skip.rs +++ b/src/commands/music/skip.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::DJ_CHECK; -use crate::providers::music::lavalink::Lavalink; +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; @@ -20,10 +20,16 @@ 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 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 data = ctx.data.read().await; - let player = data.get::().unwrap(); - player.stop(guild.id.0).await?; + 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 67489ac..546c895 100644 --- a/src/messages/music/now_playing.rs +++ b/src/messages/music/now_playing.rs @@ -4,11 +4,11 @@ use serenity::builder::CreateEmbed; use serenity::http::Http; use serenity::model::prelude::ChannelId; -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::lavalink::Lavalink; -use crate::providers::music::queue::{MusicQueue, Song}; +use crate::providers::music::player::MusicPlayer; +use crate::providers::music::queue::Song; use crate::utils::context_data::{DatabaseContainer, Store}; use crate::utils::error::*; use bot_serenityutils::core::MessageHandle; @@ -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,17 @@ 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(mut current) = queue.current().clone() { + if let Some(mut current) = player.queue().current().clone() { let mut embed = CreateEmbed::default(); - create_now_playing_embed(&mut current, &mut embed, queue.paused(), nsfw).await; + create_now_playing_embed(&mut current, &mut embed, player.is_paused(), nsfw) + .await; page.embed(|e| { e.0.clone_from(&embed.0); e @@ -166,25 +167,16 @@ 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; - { - let pause = !queue.paused(); - let data = ctx.data.read().await; - let player = data.get::().unwrap(); - player - .set_pause(guild_id.0, pause) - .await - .map_err(BotError::from)?; - queue.set_paused(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"); @@ -211,9 +203,9 @@ async fn skip_button_action( } { - let data = ctx.data.read().await; - let player = data.get::().unwrap(); - player.stop(guild_id.0).await.map_err(BotError::from)?; + let player = get_music_player_for_guild(ctx, guild_id).await.unwrap(); + let mut player = player.lock().await; + player.skip().await?; } Ok(()) @@ -243,9 +235,9 @@ async fn stop_button_action( if manager.get(guild_id).is_some() { manager.remove(guild_id).await.map_err(BotError::from)?; - let data = ctx.data.read().await; - let player = data.get::().unwrap(); - player.destroy(guild_id.0).await.map_err(BotError::from)?; + let player = get_music_player_for_guild(ctx, guild_id).await.unwrap(); + let mut player = player.lock().await; + player.stop().await?; log::debug!("Left the voice channel"); } else { log::debug!("Not in a voice channel"); @@ -268,10 +260,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(); @@ -292,9 +284,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 index 3cbf50e..ea10954 100644 --- a/src/providers/music/lavalink.rs +++ b/src/providers/music/lavalink.rs @@ -1,11 +1,8 @@ -use crate::commands::music::play_next_in_queue; -use crate::utils::context_data::Store; +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::http::Http; -use serenity::model::id::GuildId; use serenity::prelude::TypeMapKey; use std::sync::Arc; use tokio::sync::RwLock; @@ -13,7 +10,6 @@ use typemap_rev::TypeMap; pub struct LavalinkHandler { pub data: Arc>, - pub http: Arc, } #[async_trait] @@ -21,36 +17,28 @@ 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, client: LavalinkClient, event: TrackFinish) { + async fn track_finish(&self, _: LavalinkClient, event: TrackFinish) { log::info!("Track finished!\nGuild: {}", event.guild_id); - let queue = { + let player = { let data = self.data.read().await; - let store = data.get::().unwrap(); + let players = data.get::().unwrap(); - store - .music_queues - .get(&GuildId(event.guild_id)) - .unwrap() - .clone() + players.get(&event.guild_id).cloned() }; - let channel_id = { - let queue = queue.lock().await; - queue.channel_id() - }; - while !play_next_in_queue( - &self.http, - &channel_id, - &GuildId(event.guild_id), - &queue, - &client, - ) - .await - {} + 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 = LavalinkClient; + type Value = Arc; } diff --git a/src/providers/music/mod.rs b/src/providers/music/mod.rs index 00991a6..13d07d6 100644 --- a/src/providers/music/mod.rs +++ b/src/providers/music/mod.rs @@ -7,12 +7,13 @@ use regex::Regex; use responses::VideoInformation; use youtube_dl::search_video_information; -pub(crate) mod lavalink; -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> { diff --git a/src/providers/music/player.rs b/src/providers/music/player.rs new file mode 100644 index 0000000..a68b5fc --- /dev/null +++ b/src/providers/music/player.rs @@ -0,0 +1,181 @@ +use crate::messages::music::now_playing::update_now_playing_msg; +use crate::providers::music::lavalink::Lavalink; +use crate::providers::music::lyrics::get_lyrics; +use crate::providers::music::queue::MusicQueue; +use crate::utils::context_data::MusicPlayers; +use crate::utils::error::BotResult; +use bot_serenityutils::core::MessageHandle; +use lavalink_rs::LavalinkClient; +use serenity::client::Context; +use serenity::http::Http; +use serenity::model::id::{ChannelId, GuildId}; +use std::mem; +use std::sync::Arc; +use tokio::sync::{Mutex, RwLock}; + +pub struct MusicPlayer { + client: Arc, + http: Arc, + queue: MusicQueue, + guild_id: GuildId, + now_playing_msg: Option>>, + leave_flag: bool, + paused: bool, +} + +impl MusicPlayer { + /// Creates a new music player + pub fn new(client: Arc, http: Arc, guild_id: GuildId) -> Self { + Self { + client, + http, + guild_id, + queue: MusicQueue::new(), + 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, + ) -> 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); + let player = Arc::new(Mutex::new(player)); + let players = data.get_mut::().unwrap(); + players.insert(guild_id.0, Arc::clone(&player)); + player + }; + + 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() { + n + } else { + return Ok(true); + }; + let url = if let Some(url) = next.url().await { + url + } else { + 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); + 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; + } +} diff --git a/src/providers/music/queue.rs b/src/providers/music/queue.rs index 382238c..cd9f375 100644 --- a/src/providers/music/queue.rs +++ b/src/providers/music/queue.rs @@ -7,30 +7,20 @@ 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 serenity::model::id::ChannelId; -use std::sync::Arc; -use tokio::sync::RwLock; #[derive(Clone)] pub struct MusicQueue { inner: VecDeque, current: Option, - paused: bool, - pub now_playing_msg: Option>>, pub leave_flag: bool, - channel_id: ChannelId, } impl MusicQueue { - pub fn new(channel_id: ChannelId) -> Self { + pub fn new() -> Self { Self { inner: VecDeque::new(), current: None, - paused: false, leave_flag: false, - now_playing_msg: None, - channel_id, } } @@ -64,26 +54,11 @@ impl MusicQueue { self.current = Some(song) } - /// Clears the currently playing song - pub fn clear_current(&mut self) { - self.current = None; - } - /// Returns the reference to the currently playing song pub fn current(&self) -> &Option { &self.current } - /// Returns if the queue is paused - pub fn paused(&self) -> bool { - self.paused - } - - /// Sets if the queue is paused - pub fn set_paused(&mut self, paused: bool) { - self.paused = paused; - } - /// Clears the queue pub fn clear(&mut self) { self.inner.clear(); @@ -100,11 +75,6 @@ impl MusicQueue { pub fn remove(&mut self, index: usize) { self.inner.remove(index); } - - /// The channel id where the music messages should be sent to - pub fn channel_id(&self) -> ChannelId { - self.channel_id - } } #[derive(Clone, Debug)] 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>>; +} From 18cd1e7d28422d067dbcd9d5018ddd3599cf6939 Mon Sep 17 00:00:00 2001 From: trivernis Date: Tue, 20 Apr 2021 21:49:28 +0200 Subject: [PATCH 5/7] Add autoleave and fix some bugs Signed-off-by: trivernis --- src/client.rs | 7 +++- src/commands/music/join.rs | 9 ++++- src/commands/music/leave.rs | 9 +++++ src/messages/music/now_playing.rs | 15 ++++--- src/providers/music/player.rs | 66 +++++++++++++++++++++++++++++-- 5 files changed, 96 insertions(+), 10 deletions(-) diff --git a/src/client.rs b/src/client.rs index a81ad0a..51e3eb4 100644 --- a/src/client.rs +++ b/src/client.rs @@ -40,8 +40,13 @@ pub async fn get_client() -> BotResult { 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_PORT").expect("Missing lavalink port")) .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?; { diff --git a/src/commands/music/join.rs b/src/commands/music/join.rs index 717acac..c643460 100644 --- a/src/commands/music/join.rs +++ b/src/commands/music/join.rs @@ -4,7 +4,7 @@ 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}; +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; @@ -34,6 +34,13 @@ 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); MusicPlayer::join(ctx, guild.id, channel_id).await?; EphemeralMessage::create(&ctx.http, msg.channel_id, SHORT_TIMEOUT, |m| { diff --git a/src/commands/music/leave.rs b/src/commands/music/leave.rs index ba601c2..740e67c 100644 --- a/src/commands/music/leave.rs +++ b/src/commands/music/leave.rs @@ -20,8 +20,15 @@ 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 = songbird::get(ctx).await.unwrap(); + if let Some(handler) = manager.get(guild.id) { + let mut handler_lock = handler.lock().await; + let _ = handler_lock.leave().await; + } + 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| { @@ -32,8 +39,10 @@ async fn leave(ctx: &Context, msg: &Message) -> CommandResult { Some(player) => { let mut player = player.lock().await; player.stop().await?; + player.delete_now_playing().await?; } } + manager.remove(guild.id).await?; handle_autodelete(ctx, msg).await?; diff --git a/src/messages/music/now_playing.rs b/src/messages/music/now_playing.rs index 546c895..01b2182 100644 --- a/src/messages/music/now_playing.rs +++ b/src/messages/music/now_playing.rs @@ -9,7 +9,7 @@ use crate::messages::add_ephemeral_handle_to_database; use crate::providers::music::add_youtube_song_to_database; use crate::providers::music::player::MusicPlayer; use crate::providers::music::queue::Song; -use crate::utils::context_data::{DatabaseContainer, Store}; +use crate::utils::context_data::{DatabaseContainer, MusicPlayers, Store}; use crate::utils::error::*; use bot_serenityutils::core::MessageHandle; use bot_serenityutils::error::SerenityUtilsResult; @@ -230,14 +230,19 @@ async fn stop_button_action( if let Some(handler) = handler { 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() { manager.remove(guild_id).await.map_err(BotError::from)?; - let player = get_music_player_for_guild(ctx, guild_id).await.unwrap(); - let mut player = player.lock().await; - player.stop().await?; + 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"); diff --git a/src/providers/music/player.rs b/src/providers/music/player.rs index a68b5fc..c5474ed 100644 --- a/src/providers/music/player.rs +++ b/src/providers/music/player.rs @@ -6,11 +6,16 @@ use crate::utils::context_data::MusicPlayers; use crate::utils::error::BotResult; use bot_serenityutils::core::MessageHandle; use lavalink_rs::LavalinkClient; -use serenity::client::Context; -use serenity::http::Http; -use serenity::model::id::{ChannelId, GuildId}; +use serenity::prelude::TypeMap; +use serenity::{ + client::Context, + http::Http, + model::id::{ChannelId, GuildId}, +}; +use songbird::Songbird; use std::mem; use std::sync::Arc; +use std::time::Duration; use tokio::sync::{Mutex, RwLock}; pub struct MusicPlayer { @@ -58,6 +63,13 @@ impl MusicPlayer { player }; + wait_for_disconnect( + Arc::clone(&ctx.data), + Arc::clone(&player), + manager, + guild_id, + ); + Ok(player) } @@ -118,6 +130,9 @@ impl MusicPlayer { } }; + 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); @@ -179,3 +194,48 @@ impl MusicPlayer { self.leave_flag = flag; } } + +/// 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 + } + } + }); +} From af4a83e9ed7c6ceb6e54a005c2bd6ec56680259d Mon Sep 17 00:00:00 2001 From: trivernis Date: Tue, 20 Apr 2021 22:04:24 +0200 Subject: [PATCH 6/7] Add error messages to unsuccessful plays Signed-off-by: trivernis --- src/commands/music/join.rs | 2 +- src/commands/music/play.rs | 2 +- src/commands/music/play_next.rs | 2 +- src/providers/music/mod.rs | 2 ++ src/providers/music/player.rs | 45 ++++++++++++++++++++++++++++++--- 5 files changed, 47 insertions(+), 6 deletions(-) diff --git a/src/commands/music/join.rs b/src/commands/music/join.rs index c643460..544e61f 100644 --- a/src/commands/music/join.rs +++ b/src/commands/music/join.rs @@ -42,7 +42,7 @@ async fn join(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult { return Ok(()); } log::debug!("Joining channel {} for guild {}", channel_id, guild.id); - MusicPlayer::join(ctx, guild.id, channel_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/play.rs b/src/commands/music/play.rs index 5f2ec75..9396cdc 100644 --- a/src/commands/music/play.rs +++ b/src/commands/music/play.rs @@ -30,7 +30,7 @@ async fn play(ctx: &Context, msg: &Message, args: Args) -> CommandResult { if player.is_none() { log::debug!("Not in a channel. Joining authors channel..."); let channel_id = get_channel_for_author(&msg.author.id, &guild)?; - let music_player = MusicPlayer::join(ctx, guild.id, channel_id).await?; + let music_player = MusicPlayer::join(ctx, guild.id, channel_id, msg.channel_id).await?; player = Some(music_player); } let player = player.unwrap(); diff --git a/src/commands/music/play_next.rs b/src/commands/music/play_next.rs index e738dd3..ab62242 100644 --- a/src/commands/music/play_next.rs +++ b/src/commands/music/play_next.rs @@ -30,7 +30,7 @@ async fn play_next(ctx: &Context, msg: &Message, args: Args) -> CommandResult { if player.is_none() { log::debug!("Not in a channel. Joining authors channel..."); let channel_id = get_channel_for_author(&msg.author.id, &guild)?; - let music_player = MusicPlayer::join(ctx, guild.id, channel_id).await?; + let music_player = MusicPlayer::join(ctx, guild.id, channel_id, msg.channel_id).await?; player = Some(music_player); } diff --git a/src/providers/music/mod.rs b/src/providers/music/mod.rs index 13d07d6..a9cf669 100644 --- a/src/providers/music/mod.rs +++ b/src/providers/music/mod.rs @@ -40,10 +40,12 @@ pub(crate) async fn song_to_youtube_video(song: &Song) -> BotResult>>, + 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) -> Self { + 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, @@ -47,6 +55,7 @@ impl MusicPlayer { 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; @@ -56,7 +65,12 @@ impl MusicPlayer { 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); + 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)); @@ -113,6 +127,7 @@ impl MusicPlayer { /// 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); @@ -120,12 +135,26 @@ impl MusicPlayer { 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); } }; @@ -193,6 +222,16 @@ impl MusicPlayer { 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 From 4f250b537536ca3037fb133f5683c276d0d28c74 Mon Sep 17 00:00:00 2001 From: trivernis Date: Tue, 20 Apr 2021 22:16:38 +0200 Subject: [PATCH 7/7] Fix database loaded songs not having a thumbnail Signed-off-by: trivernis --- src/commands/music/mod.rs | 10 ++++++---- src/providers/music/queue.rs | 10 +++++----- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/src/commands/music/mod.rs b/src/commands/music/mod.rs index 3ff6b3e..1e011c2 100644 --- a/src/commands/music/mod.rs +++ b/src/commands/music/mod.rs @@ -289,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/providers/music/queue.rs b/src/providers/music/queue.rs index cd9f375..d4cee95 100644 --- a/src/providers/music/queue.rs +++ b/src/providers/music/queue.rs @@ -85,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 {