Compare commits
3 commits
master
...
refactor/l
Author | SHA1 | Date | |
---|---|---|---|
6d4319e9e5 | |||
880e81646d | |||
6685163c96 |
16 changed files with 683 additions and 591 deletions
645
Cargo.lock
generated
645
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
|
@ -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"
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
30
src/events/lavalink_event_handler.rs
Normal file
30
src/events/lavalink_event_handler.rs
Normal 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
1
src/events/mod.rs
Normal file
|
@ -0,0 +1 @@
|
||||||
|
pub mod lavalink_event_handler;
|
54
src/main.rs
54
src/main.rs
|
@ -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");
|
||||||
|
|
|
@ -1,3 +1,2 @@
|
||||||
pub mod music_events;
|
|
||||||
pub mod music_manager;
|
pub mod music_manager;
|
||||||
pub mod music_queue;
|
pub mod preview;
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
7
src/music/preview.rs
Normal 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>,
|
||||||
|
}
|
|
@ -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();
|
||||||
|
|
|
@ -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()
|
||||||
|
|
Loading…
Reference in a new issue