WIP: started working on lavalink rewrite, this is not compiling yet

This commit is contained in:
moonleay 2024-05-19 04:24:40 +02:00
parent 242fa52170
commit 6685163c96
Signed by: moonleay
GPG key ID: 82667543CCD715FB
16 changed files with 442 additions and 503 deletions

507
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -9,11 +9,12 @@ edition = "2021"
[dependencies]
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_json = "1.0"
confy = "0.6.0"
songbird = "0.4"
songbird = { version = "0.4", features = ["gateway", "serenity", "native", "driver"], default-features = false }
lavalink-rs = { version = "0.10", features = ["songbird", "serenity-native", "native-tls"], default-features = false }
chrono = "0.4"
reqwest = "0.11"
symphonia = "0.5"

View file

@ -1,9 +1,9 @@
use crate::music::music_queue;
use crate::util::embed::Embed;
use lavalink_rs::client::LavalinkClient;
use serenity::all::{CommandInteraction, Context};
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 guild_id = match &command.guild_id {
@ -57,7 +57,7 @@ pub async fn run(ctx: &Context, command: &CommandInteraction) -> CreateEmbed {
let position = now_handle.get_info().await.unwrap().position;
Embed::create_yt_playing(now_plaing, username, "Currently playing")
Embed::create_playing(now_plaing, username, "Currently playing")
.await
.field(
"Position",

View file

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

View file

@ -1,9 +1,10 @@
use crate::music::music_manager;
use crate::util::embed::Embed;
use lavalink_rs::client::LavalinkClient;
use serenity::all::{CommandInteraction, Context};
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 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
}

View file

@ -1,9 +1,10 @@
use crate::music::music_manager;
use crate::util::embed::Embed;
use lavalink_rs::client::LavalinkClient;
use serenity::all::{CommandInteraction, Context};
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 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 {

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

View file

@ -1,3 +1,2 @@
pub mod music_events;
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,18 @@
use crate::music::{music_events, music_queue};
use crate::util::embed::Embed;
use crate::util::user_util;
use crate::util::user_util::get_vc_id;
use crate::HttpKey;
use crate::music::preview::Preview;
use lavalink_rs::client::LavalinkClient;
use lavalink_rs::model::search::SearchEngines;
use lavalink_rs::model::track::TrackLoadData;
use lavalink_rs::player_context::TrackInQueue;
use serenity::all::{Context, CreateEmbed, GuildId, UserId};
use songbird::error::JoinError;
use songbird::input::YoutubeDl;
use songbird::{Event, TrackEvent};
use std::sync::Arc;
/// 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(
ctx: &Context,
llc: &LavalinkClient,
guild_id: &GuildId,
user_id: &UserId,
username: &str,
@ -32,21 +33,33 @@ 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
.expect("Cannot get Songbird.")
.clone();
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
if self_channel.is_none() {
// Connect to VC
manager // TODO match
.join(*guild_id, connect_to)
let handler = songbird.join_gateway(GuildId::new(guild_id.get()), connect_to).await;
//
match handler {
Ok((connection_info, _)) => {
llc.create_player_context(
guild_id.get(),
connection_info
)
.await
.expect("Cannot connect>...");
.unwrap();
}
Err(why) => {
return Embed::create_error_respose(username, "Cannot join", "Could not join the channel.");
}
}
}
} else {
let self_channel = self_channel.expect("Cannot get self channel"); // TODO: match
@ -63,87 +76,84 @@ pub async fn attempt_to_queue_song(
// Get query
let do_search = !query.starts_with("http");
let http_client = {
let data = ctx.data.read().await;
data.get::<HttpKey>()
.cloned()
.expect("Guaranteed to exist in the typemap.")
let search_query = if do_search {
match SearchEngines::YouTube.to_query(&query) {
Ok(x) => x,
Err(why) => {
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
let src = if do_search {
YoutubeDl::new_search(http_client, query.to_string())
} else {
YoutubeDl::new(http_client, query.to_string())
};
let loaded_tracks = llc.load_tracks(guild_id.get(), &search_query).await.expect("Fuck.");
let currently_playing = music_queue::get_now_playing(guild_id).await;
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 mut playlist_info = None;
let tracks: Vec<TrackInQueue> = match loaded_tracks.data {
Some(TrackLoadData::Track(x)) => vec![x.into()],
Some(TrackLoadData::Search(x)) => vec![x[0].clone().into()],
Some(TrackLoadData::Playlist(x)) => {
playlist_info = Some(x.info);
x.tracks.iter().map(|x| x.clone().into()).collect()
}
let _query = music_queue::next(guild_id)
.await
.expect("Cannot get head of queue");
music_queue::set_now_playing(guild_id, Some(src.clone())).await;
let handler_lock = match manager.get(*guild_id) {
Some(handler) => handler,
None => {
_ => {
return Embed::create_error_respose(
username,
"Error",
"Cannot get handler of this guild.",
);
"500: Server failure",
"Something went wrong when loading the track...");
}
};
// Start playing
let mut handler = handler_lock.lock().await;
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()),
},
);
let mut response_title = "";
let mut preview_data = None;
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
pub async fn play_song(ctx: &Context, guild_id: &GuildId, target: &YoutubeDl) {
let manager = &songbird::get(ctx) // TODO match
.await
.expect("Cannot get Songbird.")
.clone();
if !user_util::is_self_connected_to_vc(ctx, guild_id).await {
println!("Bot is not connected to a VC, cannot play.");
return;
response_title = "Added to queue";
preview_data = Some(Preview {
title: track.info.title,
artist: Some(track.info.author),
duration: Some(track.info.length),
thumbnail: track.info.artwork_url,
link: track.info.uri
});
}
music_queue::set_now_playing(guild_id, Some(target.clone())).await;
let handler_lock = match manager.get(*guild_id) {
Some(handler) => handler,
None => return,
let player = match llc.get_player_context(guild_id.get()) {
Some(player) => player,
None => {
return Embed::create_error_respose(username, "Cannot get player", "Could not get player context.");
}
};
let mut handler = handler_lock.lock().await;
handler.stop(); // Stop playing the current song
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 mut q = match player.get_queue().await {
Ok(q) => q,
Err(why) => {
return Embed::create_error_respose(username, "Cannont get queue", "Could not get queue.");
}
};
q.append(&mut tracks.into());
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
pub async fn attempt_to_skip_current_song(
ctx: &Context,
llc: &LavalinkClient,
guild_id: &GuildId,
user_id: &UserId,
username: &str,
@ -201,14 +211,15 @@ pub async fn attempt_to_skip_current_song(
);
}
let head = head.unwrap();
play_song(ctx, guild_id, &head).await;
play_song(ctx, guild_id, llc, &head).await;
Embed::create_yt_playing(head, username, "Song skipped; Now playing").await
Embed::create_playing(head, username, "Song skipped; Now playing").await
}
/// Try to clear the queue and stop playing. Also leave the vc
pub async fn attempt_to_stop(
ctx: &Context,
llc: &LavalinkClient,
guild_id: &GuildId,
user_id: &UserId,
username: &str,
@ -237,7 +248,7 @@ pub async fn attempt_to_stop(
);
}
let stopped = match leave(ctx, guild_id).await {
let stopped = match leave(ctx, llc, guild_id).await {
Ok(stopped) => stopped,
Err(e) => {
println!("Error while stopping: {:?}", e);
@ -263,7 +274,7 @@ pub async fn attempt_to_stop(
}
/// 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)
.await
.expect("Cannot get Songbird")

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

View file

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