Compare commits

...

3 commits

16 changed files with 683 additions and 591 deletions

645
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -9,11 +9,12 @@ edition = "2021"
[dependencies] [dependencies]
serenity = "0.12" serenity = "0.12"
tokio = { version = "1.36", features = ["macros", "rt-multi-thread"] } tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0" serde_json = "1.0"
confy = "0.6.0" confy = "0.6.0"
songbird = "0.4" songbird = { version = "0.4", features = ["gateway", "serenity", "native", "driver"], default-features = false }
lavalink-rs = { version = "0.12", features = ["songbird", "serenity", "native-tls"], default-features = false }
chrono = "0.4" chrono = "0.4"
reqwest = "0.11" reqwest = "0.11"
symphonia = "0.5" symphonia = "0.5"

View file

@ -1,9 +1,12 @@
use crate::music::music_queue; use std::time::Duration;
use crate::music::preview::Preview;
use crate::util::embed::Embed; use crate::util::embed::Embed;
use serenity::all::{CommandInteraction, Context}; use lavalink_rs::client::LavalinkClient;
use serenity::all::{CommandInteraction, Context, GuildId};
use serenity::builder::{CreateCommand, CreateEmbed}; use serenity::builder::{CreateCommand, CreateEmbed};
pub async fn run(ctx: &Context, command: &CommandInteraction) -> CreateEmbed { pub async fn run(ctx: &Context, command: &CommandInteraction, llc: &LavalinkClient) -> CreateEmbed {
let username = command.user.name.as_str(); let username = command.user.name.as_str();
let guild_id = match &command.guild_id { let guild_id = match &command.guild_id {
@ -17,47 +20,27 @@ pub async fn run(ctx: &Context, command: &CommandInteraction) -> CreateEmbed {
} }
}; };
let now_plaing = match music_queue::get_now_playing(&guild_id).await { let Some(player_context) = llc.get_player_context(guild_id.get()) else {
Some(ytdl) => ytdl, return Embed::create_error_respose(username, "Not playing", "There is no player context and therefore there is nothing playing.");
None => {
return Embed::create_error_respose(
username,
"Not playing",
"I'm not playing anything!",
);
}
}; };
let now_handle = match music_queue::get_now_playing_track_handle(&guild_id).await { let Ok(player) = player_context.get_player().await else {
Some(handle) => handle, return Embed::create_error_respose(username, "Can't get player", "Cannot get player from player context.");
None => {
return Embed::create_error_respose(
username,
"Cannot get TrackHandle",
"The TrackHandle is empty.",
);
}
}; };
let manager = songbird::get(ctx)
.await
.expect("Cannot get Songbird")
.clone();
let _ = match manager.get(*guild_id) { if let Some(npt) = player.track {
Some(handler) => handler, let npti = npt.info;
None => { let preview_data = Preview {
return Embed::create_error_respose( title: npti.title,
username, artist: Some(npti.author.to_string()),
"Error", duration: Some(npti.length),
"Error while getting the audio handler.", thumbnail: npti.artwork_url,
); link: npti.uri
} };
}; let position = Duration::from_millis(player_context.get_player().await.expect("Can't get player").state.position);
let position = now_handle.get_info().await.unwrap().position; return Embed::create_playing(preview_data, username, "Now playing")
Embed::create_yt_playing(now_plaing, username, "Currently playing")
.await .await
.field( .field(
"Position", "Position",
@ -66,8 +49,11 @@ pub async fn run(ctx: &Context, command: &CommandInteraction) -> CreateEmbed {
position.as_secs() / 60, position.as_secs() / 60,
position.as_secs() % 60 position.as_secs() % 60
), ),
true, true
) );
}
return Embed::create_error_respose(username, "Not playing", "I'm currently not playing anything.");
} }
pub fn register() -> CreateCommand { pub fn register() -> CreateCommand {

View file

@ -1,12 +1,13 @@
use crate::music::music_manager; use crate::music::music_manager;
use lavalink_rs::client::LavalinkClient;
use serenity::all::{CommandDataOptionValue, CommandInteraction, Context}; use serenity::all::{CommandDataOptionValue, CommandInteraction, Context};
use serenity::builder::{CreateCommand, CreateCommandOption, CreateEmbed}; use serenity::builder::{CreateCommand, CreateCommandOption, CreateEmbed};
use serenity::model::application::CommandOptionType; use serenity::model::application::CommandOptionType;
use crate::util::embed::Embed; use crate::util::embed::Embed;
pub async fn run(ctx: &Context, command: &CommandInteraction) -> CreateEmbed { pub async fn run(ctx: &Context, command: &CommandInteraction, llc: &LavalinkClient) -> CreateEmbed {
let username = command.user.name.as_str(); let username = command.user.name.as_str();
let options = &command.data.options; let options = &command.data.options;
@ -39,6 +40,7 @@ pub async fn run(ctx: &Context, command: &CommandInteraction) -> CreateEmbed {
music_manager::attempt_to_queue_song( music_manager::attempt_to_queue_song(
ctx, ctx,
llc,
guild_id, guild_id,
&command.user.id, &command.user.id,
&command.user.name, &command.user.name,

View file

@ -1,9 +1,10 @@
use crate::music::music_manager; use crate::music::music_manager;
use crate::util::embed::Embed; use crate::util::embed::Embed;
use lavalink_rs::client::LavalinkClient;
use serenity::all::{CommandInteraction, Context}; use serenity::all::{CommandInteraction, Context};
use serenity::builder::{CreateCommand, CreateEmbed}; use serenity::builder::{CreateCommand, CreateEmbed};
pub async fn run(ctx: &Context, command: &CommandInteraction) -> CreateEmbed { pub async fn run(ctx: &Context, command: &CommandInteraction, llc: &LavalinkClient) -> CreateEmbed {
let username = command.user.name.as_str(); let username = command.user.name.as_str();
let guild_id = match &command.guild_id { let guild_id = match &command.guild_id {
@ -17,7 +18,7 @@ pub async fn run(ctx: &Context, command: &CommandInteraction) -> CreateEmbed {
} }
}; };
music_manager::attempt_to_skip_current_song(ctx, guild_id, &command.user.id, &command.user.name) music_manager::attempt_to_skip_current_song(ctx, llc, guild_id, &command.user.id, &command.user.name)
.await .await
} }

View file

@ -1,9 +1,10 @@
use crate::music::music_manager; use crate::music::music_manager;
use crate::util::embed::Embed; use crate::util::embed::Embed;
use lavalink_rs::client::LavalinkClient;
use serenity::all::{CommandInteraction, Context}; use serenity::all::{CommandInteraction, Context};
use serenity::builder::{CreateCommand, CreateEmbed}; use serenity::builder::{CreateCommand, CreateEmbed};
pub async fn run(ctx: &Context, command: &CommandInteraction) -> CreateEmbed { pub async fn run(ctx: &Context, command: &CommandInteraction, llc: &LavalinkClient) -> CreateEmbed {
let username = command.user.name.as_str(); let username = command.user.name.as_str();
let guild_id = match &command.guild_id { let guild_id = match &command.guild_id {
@ -17,7 +18,7 @@ pub async fn run(ctx: &Context, command: &CommandInteraction) -> CreateEmbed {
} }
}; };
music_manager::attempt_to_stop(ctx, guild_id, &command.user.id, &command.user.name).await music_manager::attempt_to_stop(ctx, llc, guild_id, &command.user.id, &command.user.name).await
} }
pub fn register() -> CreateCommand { pub fn register() -> CreateCommand {

View file

@ -0,0 +1,30 @@
use lavalink_rs::{client::LavalinkClient, model::events};
use serenity::all::{standard::macros::hook, Sticker};
#[hook]
pub async fn ready_event(client: LavalinkClient, _session_id: String, event: &events::Ready) {
client.delete_all_player_contexts().await.unwrap();
println!("Lavalink is ready for Freddy:: {:?}", event);
}
#[hook]
pub async fn track_start(_client: LavalinkClient, _session_id: String, _event: &events::TrackStart) {
}
#[hook]
pub async fn track_end_event(_client: LavalinkClient, _session_id: String, _event: &events::TrackEnd) {
}
#[hook]
pub async fn track_exception(_client: LavalinkClient, _session_id: String, _event: &events::TrackException) {
}
#[hook]
pub async fn track_stuck(_client: LavalinkClient, _session_id: String, _event: &events::TrackStuck) {
}

1
src/events/mod.rs Normal file
View file

@ -0,0 +1 @@
pub mod lavalink_event_handler;

View file

@ -1,7 +1,15 @@
mod commands; mod commands;
mod music; mod music;
mod util; mod util;
mod events;
use std::ops::Deref;
use lavalink_rs::client::LavalinkClient;
use lavalink_rs::model::client::NodeDistributionStrategy;
use lavalink_rs::model::events::Events;
use lavalink_rs::model::UserId;
use lavalink_rs::node::NodeBuilder;
use serenity::all::{ use serenity::all::{
CommandInteraction, CreateInteractionResponseFollowup, OnlineStatus, VoiceState, CommandInteraction, CreateInteractionResponseFollowup, OnlineStatus, VoiceState,
}; };
@ -18,19 +26,15 @@ use util::{config, embed::Embed, user_util};
// The voice client can be retrieved in any command using `songbird::get(ctx).await`. // The voice client can be retrieved in any command using `songbird::get(ctx).await`.
use songbird::SerenityInit; use songbird::SerenityInit;
// YtDl requests need an HTTP client to operate -- we'll create and store our own. use crate::events::lavalink_event_handler;
use reqwest::Client as HttpClient;
struct HttpKey;
impl TypeMapKey for HttpKey {
type Value = HttpClient;
}
// lazy static stuff. I don't like it, but it has to be here, bc it has to be @ root // lazy static stuff. I don't like it, but it has to be here, bc it has to be @ root
#[macro_use] #[macro_use]
extern crate lazy_static; extern crate lazy_static;
struct Handler; struct Handler {
pub llc: LavalinkClient
}
#[async_trait] #[async_trait]
impl EventHandler for Handler { impl EventHandler for Handler {
@ -40,10 +44,10 @@ impl EventHandler for Handler {
let content = Some(match command.data.name.as_str() { let content = Some(match command.data.name.as_str() {
"info" => commands::info::run(&ctx, &command).await, "info" => commands::info::run(&ctx, &command).await,
"play" => commands::play::run(&ctx, &command).await, "play" => commands::play::run(&ctx, &command, &self.llc).await,
"stop" => commands::stop::run(&ctx, &command).await, "stop" => commands::stop::run(&ctx, &command, &self.llc).await,
"skip" => commands::skip::run(&ctx, &command).await, "skip" => commands::skip::run(&ctx, &command, &self.llc).await,
"nowplaying" => commands::now_playing::run(&ctx, &command).await, "nowplaying" => commands::now_playing::run(&ctx, &command, &self.llc).await,
_ => respond_with_error(&ctx, &command).await, _ => respond_with_error(&ctx, &command).await,
}); });
@ -120,13 +124,35 @@ async fn main() {
let status = OnlineStatus::DoNotDisturb; let status = OnlineStatus::DoNotDisturb;
let activity = ActivityData::streaming("music", "https://twitch.tv/moonleaytv").unwrap(); let activity = ActivityData::streaming("music", "https://twitch.tv/moonleaytv").unwrap();
let event = Events {
ready: Some(lavalink_event_handler::ready_event),
// track_start: Some(lavalink_event_handler::track_start),
// track_end: Some(lavalink_event_handler::track_end_event),
// track_exception: Some(lavalink_event_handler::track_exception),
// track_stuck: Some(lavalink_event_handler::track_stuck),
..Default::default()
};
let node_builder = NodeBuilder {
hostname: config.lavalink_address,
password: config.lavalink_password,
user_id: UserId(config.user_id),
is_ssl: false,
events: event.clone(),
session_id: None
};
let lavalink_client = LavalinkClient::new(event, vec![node_builder], NodeDistributionStrategy::round_robin()).await;
// Build the client // Build the client
let mut client = Client::builder(config.discord_token, GatewayIntents::empty()) let mut client = Client::builder(config.discord_token, GatewayIntents::empty())
.event_handler(Handler) .event_handler(Handler{
llc: lavalink_client
})
.status(status) .status(status)
.activity(activity) .activity(activity)
.register_songbird() .register_songbird()
.type_map_insert::<HttpKey>(HttpClient::new())
.intents(GatewayIntents::all()) .intents(GatewayIntents::all())
.await .await
.expect("Error creating client"); .expect("Error creating client");

View file

@ -1,3 +1,2 @@
pub mod music_events;
pub mod music_manager; pub mod music_manager;
pub mod music_queue; pub mod preview;

View file

@ -1,56 +0,0 @@
use crate::music::{music_manager, music_queue};
use serenity::all::{ChannelId, GuildId, Http};
use serenity::async_trait;
use songbird::input::Compose;
use songbird::{Event, EventContext, EventHandler};
use std::sync::Arc;
pub struct TrackEndNotifier {
pub guild_id: GuildId,
pub channel_id: ChannelId,
pub http: Arc<Http>,
pub cmdctx: Arc<serenity::client::Context>,
}
#[async_trait]
impl EventHandler for TrackEndNotifier {
async fn act(&self, ctx: &EventContext<'_>) -> Option<Event> {
// TODO: Does this need to be unsafe?
if let EventContext::Track(..) = ctx {
println!("The track ended!");
if music_queue::is_empty(&self.guild_id).await {
// No more songs in queue, exit the vc
let stopped = match music_manager::leave(&self.cmdctx, &self.guild_id).await {
Ok(stopped) => stopped,
Err(e) => {
println!("Cannot stop: {:?}", e);
return None;
}
};
if stopped {
music_queue::delete_queue(&self.guild_id).await;
println!("Stopped playing successfully.");
} else {
println!("Failed to stop playing.");
}
return None;
}
let mut head = match music_queue::next(&self.guild_id).await {
Some(head) => head,
None => {
println!("Cannot get head of queue");
return None;
}
};
println!(
"Now playing: {}",
head.aux_metadata().await.unwrap().title.unwrap()
);
music_manager::play_song(&self.cmdctx, &self.guild_id, &head).await;
}
None
}
}

View file

@ -1,17 +1,20 @@
use crate::music::{music_events, music_queue};
use crate::util::embed::Embed; use crate::util::embed::Embed;
use crate::util::user_util; use crate::util::user_util;
use crate::util::user_util::get_vc_id; use crate::util::user_util::get_vc_id;
use crate::HttpKey; use crate::music::preview::Preview;
use serenity::all::{Context, CreateEmbed, GuildId, UserId}; use lavalink_rs::client::LavalinkClient;
use lavalink_rs::model::search::SearchEngines;
use lavalink_rs::model::track::TrackLoadData;
use lavalink_rs::model::ChannelId;
use lavalink_rs::player_context::TrackInQueue;
use serenity::all::{Context, CreateEmbed, GuildId, Http, UserId};
use songbird::error::JoinError; use songbird::error::JoinError;
use songbird::input::YoutubeDl;
use songbird::{Event, TrackEvent};
use std::sync::Arc; use std::sync::Arc;
/// Either queues the song, or start playing it instantly, depending on if there is already a song playing /// Either queues the song, or start playing it instantly, depending on if there is already a song playing
pub async fn attempt_to_queue_song( pub async fn attempt_to_queue_song(
ctx: &Context, ctx: &Context,
llc: &LavalinkClient,
guild_id: &GuildId, guild_id: &GuildId,
user_id: &UserId, user_id: &UserId,
username: &str, username: &str,
@ -32,21 +35,38 @@ pub async fn attempt_to_queue_song(
} }
}; };
let manager: &Arc<songbird::Songbird> = &songbird::get(ctx) // TODO match let songbird: &Arc<songbird::Songbird> = &songbird::get(ctx) // TODO match
.await .await
.expect("Cannot get Songbird.") .expect("Cannot get Songbird.")
.clone(); .clone();
let self_channel = user_util::get_self_vc_id(ctx, guild_id).await; let self_channel = user_util::get_self_vc_id(ctx, guild_id).await;
if !user_util::is_self_connected_to_vc(ctx, guild_id).await { if !user_util::is_self_connected_to_vc(ctx, &guild_id).await {
// self is connected to vc, check if user is in same vc // self is connected to vc, check if user is in same vc
if self_channel.is_none() { if self_channel.is_none() {
// Connect to VC // Connect to VC
manager // TODO match let handler = songbird.join_gateway(GuildId::new(guild_id.get()), connect_to).await;
.join(*guild_id, connect_to) //
.await match handler {
.expect("Cannot connect>..."); Ok((connection_info, _)) => {
let Ok(_) = llc
.create_player_context_with_data::<(ChannelId, std::sync::Arc<Http>)>(
guild_id.get(),
connection_info,
std::sync::Arc::new((
ChannelId::from(connect_to.get()),
ctx.http.clone(),
)),
).await else {
return Embed::create_error_respose(username, "error_title", "error_desc");
};
}
Err(_) => {
return Embed::create_error_respose(username, "Cannot join", "Could not join the channel.");
}
}
} }
} else { } else {
let self_channel = self_channel.expect("Cannot get self channel"); // TODO: match let self_channel = self_channel.expect("Cannot get self channel"); // TODO: match
@ -63,87 +83,91 @@ pub async fn attempt_to_queue_song(
// Get query // Get query
let do_search = !query.starts_with("http"); let do_search = !query.starts_with("http");
let http_client = { let search_query = if do_search {
let data = ctx.data.read().await; match SearchEngines::YouTube.to_query(&query) {
data.get::<HttpKey>() Ok(x) => x,
.cloned() Err(_) => {
.expect("Guaranteed to exist in the typemap.") return Embed::create_error_respose(username, "Cannot generate query", "Could not generate a seach query..");
}
}
} else { // Allow piped links
query.to_string().replace("https://piped.moonleay.net/", "https://youtube.com/")
}; };
// Create source // Create source
let src = if do_search { let loaded_tracks = llc.load_tracks(guild_id.get(), &search_query).await.expect("Fuck.");
YoutubeDl::new_search(http_client, query.to_string())
} else {
YoutubeDl::new(http_client, query.to_string())
};
let currently_playing = music_queue::get_now_playing(guild_id).await; let mut playlist_info = None;
music_queue::add_to_queue(guild_id, src.clone()).await;
if currently_playing.is_some() {
// Add to queue
return Embed::create_yt_playing(src, username, "Added to queue").await;
}
let _query = music_queue::next(guild_id) let tracks: Vec<TrackInQueue> = match loaded_tracks.data {
.await Some(TrackLoadData::Track(x)) => vec![x.into()],
.expect("Cannot get head of queue"); Some(TrackLoadData::Search(x)) => vec![x[0].clone().into()],
music_queue::set_now_playing(guild_id, Some(src.clone())).await; Some(TrackLoadData::Playlist(x)) => {
playlist_info = Some(x.info);
let handler_lock = match manager.get(*guild_id) { x.tracks.iter().map(|x| x.clone().into()).collect()
Some(handler) => handler, }
None => { _ => {
return Embed::create_error_respose( return Embed::create_error_respose(
username, username,
"Error", "500: Server failure",
"Cannot get handler of this guild.", "Something went wrong when loading the track...");
);
} }
}; };
// Start playing let mut response_title = "";
let mut handler = handler_lock.lock().await; let mut preview_data = None;
let track_handle = handler.play_input(src.clone().into()); // TODO: Add event handlers
music_queue::set_now_playing_track_handle(guild_id, Some(track_handle)).await;
handler.add_global_event(
Event::Track(TrackEvent::End),
music_events::TrackEndNotifier {
guild_id: *guild_id,
channel_id: connect_to,
http: Arc::clone(&ctx.http),
cmdctx: Arc::new(ctx.clone()),
},
);
Embed::create_yt_playing(src, username, "Now playing").await if let Some(info) = playlist_info {
} response_title = "Added playlist to queue";
preview_data = Some(Preview {
title: info.name.to_string(),
artist: None,
duration: None,
thumbnail: None,
link: None,
});
} else {
let track = tracks[0].track.clone();
/// Play the provided song response_title = "Added to queue";
pub async fn play_song(ctx: &Context, guild_id: &GuildId, target: &YoutubeDl) { preview_data = Some(Preview {
let manager = &songbird::get(ctx) // TODO match title: track.info.title,
.await artist: Some(track.info.author),
.expect("Cannot get Songbird.") duration: Some(track.info.length),
.clone(); thumbnail: track.info.artwork_url,
link: track.info.uri
if !user_util::is_self_connected_to_vc(ctx, guild_id).await { });
println!("Bot is not connected to a VC, cannot play.");
return;
} }
music_queue::set_now_playing(guild_id, Some(target.clone())).await; let player = match llc.get_player_context(guild_id.get()) {
let handler_lock = match manager.get(*guild_id) { Some(player) => player,
Some(handler) => handler, None => {
None => return, return Embed::create_error_respose(username, "Cannot get player", "Could not get player context.");
}
}; };
let mut handler = handler_lock.lock().await; // let q = player.get_queue();
handler.stop(); // Stop playing the current song // q.append(tracks.into());
let track_handle = handler.play_input(target.clone().into()); // TODO: Add event handlers
music_queue::set_now_playing_track_handle(guild_id, Some(track_handle)).await; let Ok(_) = player.play_now(&tracks[0].track).await else {
return Embed::create_error_respose(username, "Error playing", "Could not play track.");
};
// if let Ok(player_data) = player.get_player().await {
// if player_data.track.is_none() && q.get_track(0).await.is_ok_and(|x| x.is_some()) {
// player.skip();
// }
// }
Embed::create_playing(preview_data.unwrap(), username, &response_title).await
} }
/// Attempt to skip the song, which is currently playing. Do nothing if there is no next song /// Attempt to skip the song, which is currently playing. Do nothing if there is no next song
pub async fn attempt_to_skip_current_song( pub async fn attempt_to_skip_current_song(
ctx: &Context, ctx: &Context,
llc: &LavalinkClient,
guild_id: &GuildId, guild_id: &GuildId,
user_id: &UserId, user_id: &UserId,
username: &str, username: &str,
@ -156,59 +180,82 @@ pub async fn attempt_to_skip_current_song(
); );
} }
let connect_to = match get_vc_id(ctx, guild_id, user_id).await { let user_channel = match get_vc_id(ctx, guild_id, user_id).await {
Some(channel_id) => channel_id, Some(channel_id) => channel_id,
None => { None => {
return Embed::create_error_respose(username, "Error", "Cannot find channel_id."); return Embed::create_error_respose(username, "Error", "Cannot find channel_id.");
} }
}; };
let manager: &Arc<songbird::Songbird> = &songbird::get(ctx) // TODO match
.await
.expect("Cannot get Songbird.")
.clone();
let self_channel = user_util::get_self_vc_id(ctx, guild_id).await; let self_channel = user_util::get_self_vc_id(ctx, guild_id).await;
if !user_util::is_self_connected_to_vc(ctx, guild_id).await { if !user_util::is_self_connected_to_vc(ctx, guild_id).await {
// self is connected to vc, check if user is in same vc // Self is not connected to vc, cannot skip.
if self_channel.is_none() { return Embed::create_error_respose(username, "Not connected", "I am not connected and I am not playing anything, therefore you cannot skip.");
// Connect to VC
manager // TODO match
.join(*guild_id, connect_to)
.await
.expect("Cannot connect>...");
}
} else { } else {
let self_channel = self_channel.expect("Cannot get self channel"); // TODO: match let self_channel = self_channel.expect("Cannot get self channel");
// Check if user is in the same VC as the bot // Check if user is in the same VC as the bot
if self_channel != connect_to { if self_channel != user_channel {
return Embed::create_error_respose( return Embed::create_error_respose(
username, username,
"You are not in my VC", "You are not in my VC",
"You have to be in my VC in order to controll the music.", "You have to be in my VC in order to control the music.",
); );
} }
} }
let head = music_queue::next(guild_id).await; // TODO match let Some(player_context) = llc.get_player_context(guild_id.get()) else {
if head.is_none() { return Embed::create_error_respose(username, "Not playing", "There is no player context and therefore there is nothing playing.");
return Embed::create_error_respose( };
username,
"Cannot find a song to play", let Ok(player) = player_context.get_player().await else {
"The queue is empty.", return Embed::create_error_respose(username, "Can't get player", "Cannot get player from player context.");
); };
}
let head = head.unwrap();
play_song(ctx, guild_id, &head).await; if let Some(_) = player.track {
let Ok(_) = player_context.skip() else {
return Embed::create_error_respose(username, "Cannot skip", "Could not skip track.");
};
let ct = match player_context.get_player().await.expect("Can't get player").track {
Some(data) => data.info,
None => { // Disconnect
let songbird = songbird::get(ctx).await.unwrap().clone();
let Ok(_) = llc.delete_player(guild_id.get()).await else {
return Embed::create_error_respose(username, "Cannot delete player", "Could not delete player.");
};
if songbird.get(GuildId::new(guild_id.get())).is_some() {
let Ok(_) = songbird.remove(GuildId::new(guild_id.get())).await else {
return Embed::create_error_respose(username, "Cannot remove ref", "Cannot remove songbird ref.");
};
}
return Embed::create_error_respose(username, "No track to skip to", "Therefore I am leaving the channel.")
}
};
let preview_data = Preview {
title: ct.title,
artist: Some(ct.author.to_string()),
duration: Some(ct.length),
thumbnail: ct.artwork_url,
link: ct.uri
};
return Embed::create_playing(preview_data, username, "Skipped; Now playing").await;
}
return Embed::create_error_respose(username, "Not playing", "I'm currently not playing anything.");
Embed::create_yt_playing(head, username, "Song skipped; Now playing").await
} }
/// Try to clear the queue and stop playing. Also leave the vc /// Try to clear the queue and stop playing. Also leave the vc
pub async fn attempt_to_stop( pub async fn attempt_to_stop(
ctx: &Context, ctx: &Context,
llc: &LavalinkClient,
guild_id: &GuildId, guild_id: &GuildId,
user_id: &UserId, user_id: &UserId,
username: &str, username: &str,
@ -237,7 +284,10 @@ pub async fn attempt_to_stop(
); );
} }
let stopped = match leave(ctx, guild_id).await { let player = llc.get_player_context(guild_id.get()).expect("Can't get player context.");
player.get_queue().clear();
let stopped = match leave(ctx, llc, guild_id).await {
Ok(stopped) => stopped, Ok(stopped) => stopped,
Err(e) => { Err(e) => {
println!("Error while stopping: {:?}", e); println!("Error while stopping: {:?}", e);
@ -256,26 +306,28 @@ pub async fn attempt_to_stop(
"I am not connected.\nI cant stop doing something, when I'm not doing it.", "I am not connected.\nI cant stop doing something, when I'm not doing it.",
) )
} else { } else {
music_queue::delete_queue(guild_id).await; // Clear queue
Embed::create_success_response(username, "I stopped and left", "Just like you girlfriend.") Embed::create_success_response(username, "I stopped and left", "Just like you girlfriend.")
} }
} }
/// Make the bot leave the voice channel. Returns Ok(true) if bot was connected, returns Ok(false) if bot was not connected. Returns Err if something went wrong. /// Make the bot leave the voice channel. Returns Ok(true) if bot was connected, returns Ok(false) if bot was not connected. Returns Err if something went wrong.
pub async fn leave(ctx: &Context, guild_id: &GuildId) -> Result<bool, JoinError> { pub async fn leave(ctx: &Context, llc: &LavalinkClient, guild_id: &GuildId) -> Result<bool, JoinError> {
let manager = songbird::get(ctx) let Some(songbird) = songbird::get(ctx)
.await .await.clone() else {
.expect("Cannot get Songbird") return Err(JoinError::NoSender);
.clone(); };
let handler = manager.get(*guild_id); if llc.get_player_context(guild_id.get()).is_some() {
let has_handler = handler.is_some(); let Ok(_) = llc.delete_player(guild_id.get()).await else {
return Err(JoinError::NoCall)
if has_handler { };
handler.unwrap().lock().await.stop();
manager.remove(*guild_id).await?;
return Ok(true); // Handler removed
} }
Ok(false) // No handler, so it's already stopped
if songbird.get(GuildId::new(guild_id.get())).is_some() {
let Ok(_) = songbird.remove(GuildId::new(guild_id.get())).await else {
return Err(JoinError::Dropped);
};
}
return Ok(true)
} }

View file

@ -1,84 +0,0 @@
use serenity::all::GuildId;
use songbird::input::YoutubeDl;
use songbird::tracks::TrackHandle;
use tokio::sync::Mutex;
use std::collections::{HashMap, VecDeque};
use std::sync::Arc;
type MusicQueueItem = Arc<Mutex<MusicQueue>>;
#[derive(Debug)]
pub struct MusicQueue {
pub queue: VecDeque<YoutubeDl>,
pub now_playing: Option<YoutubeDl>,
pub now_playing_track_handle: Option<TrackHandle>,
}
lazy_static! {
static ref HASHMAP: Mutex<HashMap<GuildId, MusicQueueItem>> = Mutex::new(HashMap::new());
}
async fn get_music_queue(guild_id: &GuildId) -> MusicQueueItem {
let mut queues = HASHMAP.lock().await;
queues
.entry(*guild_id)
.or_insert(Arc::new(Mutex::new(MusicQueue {
queue: VecDeque::new(),
now_playing: None,
now_playing_track_handle: None,
})))
.clone()
}
pub async fn with_music_queue<F, T>(guild_id: &GuildId, f: F) -> T
where
F: FnOnce(&mut MusicQueue) -> T,
T: Send,
{
let queue = get_music_queue(guild_id).await;
let mut queue = queue.lock().await;
f(&mut queue)
}
pub async fn delete_queue(guild_id: &GuildId) {
with_music_queue(guild_id, |queue| {
queue.now_playing = None;
queue.queue.clear();
})
.await;
}
pub async fn add_to_queue(guild_id: &GuildId, input: YoutubeDl) {
with_music_queue(guild_id, |queue| queue.queue.push_back(input)).await;
}
/// Get next track in queue
pub async fn next(guild_id: &GuildId) -> Option<YoutubeDl> {
with_music_queue(guild_id, |queue| queue.queue.pop_front()).await
}
pub async fn set_now_playing(guild_id: &GuildId, now_playing: Option<YoutubeDl>) {
with_music_queue(guild_id, |queue| queue.now_playing = now_playing).await;
}
pub async fn get_now_playing(guild_id: &GuildId) -> Option<YoutubeDl> {
with_music_queue(guild_id, |queue| queue.now_playing.to_owned()).await
}
pub async fn set_now_playing_track_handle(guild_id: &GuildId, track_handle: Option<TrackHandle>) {
with_music_queue(guild_id, |queue| {
queue.now_playing_track_handle = track_handle
})
.await
}
pub async fn get_now_playing_track_handle(guild_id: &GuildId) -> Option<TrackHandle> {
with_music_queue(guild_id, |queue| queue.now_playing_track_handle.to_owned()).await
}
pub async fn is_empty(guild_id: &GuildId) -> bool {
with_music_queue(guild_id, |queue| queue.queue.is_empty()).await
}

7
src/music/preview.rs Normal file
View file

@ -0,0 +1,7 @@
pub struct Preview {
pub title: String,
pub artist: Option<String>,
pub duration: Option<u64>,
pub thumbnail: Option<String>,
pub link: Option<String>,
}

View file

@ -6,6 +6,9 @@ use std::error::Error;
#[derive(Deserialize, Serialize)] #[derive(Deserialize, Serialize)]
pub struct Config { pub struct Config {
pub discord_token: String, pub discord_token: String,
pub lavalink_address: String,
pub lavalink_password: String,
pub user_id: u64
} }
const CONFIG_FILE: &str = "./data/config.json"; const CONFIG_FILE: &str = "./data/config.json";
@ -23,6 +26,9 @@ pub fn load() -> Result<Config, Box<dyn Error>> {
fn create_empty() -> fs::File { fn create_empty() -> fs::File {
let example_config = Config { let example_config = Config {
discord_token: "paste_your_token".to_string(), discord_token: "paste_your_token".to_string(),
lavalink_address: "paste_your_lavalink_address".to_string(),
lavalink_password: "paste_your_lavalink_password".to_string(),
user_id: 976119987330777168
}; };
let mut config_file = fs::File::create(CONFIG_FILE).unwrap(); let mut config_file = fs::File::create(CONFIG_FILE).unwrap();

View file

@ -2,7 +2,8 @@ use std::time::Duration;
use chrono::Local; use chrono::Local;
use serenity::all::{Color, CreateEmbed, CreateEmbedAuthor, CreateEmbedFooter}; use serenity::all::{Color, CreateEmbed, CreateEmbedAuthor, CreateEmbedFooter};
use songbird::input::{Compose, YoutubeDl};
use crate::music::preview::Preview;
pub struct Embed; pub struct Embed;
@ -38,23 +39,21 @@ impl Embed {
))) )))
} }
pub async fn create_yt_playing( pub async fn create_playing(
mut src: YoutubeDl, preview: Preview,
username: &str, username: &str,
show_as_author: &str, show_as_author: &str,
) -> CreateEmbed { ) -> CreateEmbed {
let current_time = Local::now().format("%Y-%m-%d @ %H:%M:%S"); let current_time = Local::now().format("%Y-%m-%d @ %H:%M:%S");
// Get metadata // Get metadata
let metadata = src.aux_metadata().await.expect("Cannot get metadata"); let title = preview.title;
let title = metadata.title.unwrap_or("Unknown title".to_string()); let artist = preview.artist.unwrap_or("t".to_string());
let artist = metadata.artist.unwrap_or("Unknown artist".to_string()); let duration = Duration::from_millis(preview.duration.unwrap_or(0));
let duration = metadata.duration.unwrap_or(Duration::from_millis(0)); let thumbnail = preview
let thumbnail = metadata
.thumbnail .thumbnail
.unwrap_or("https://http.cat/images/403.jpg".to_string()); .unwrap_or("https://http.cat/images/403.jpg".to_string());
let link = metadata let link = preview.link
.source_url
.unwrap_or("https://piped.moonleay.net/403".to_string()); .unwrap_or("https://piped.moonleay.net/403".to_string());
CreateEmbed::new() CreateEmbed::new()