2023-02-21 14:45:59 +01:00

534 lines
13 KiB
Rust

use std::time::Duration;
use librespot::core::spotify_id::{SpotifyAudioType, SpotifyId};
use log::error;
use serenity::{
builder::{CreateApplicationCommand, CreateButton, CreateComponents, CreateEmbed},
model::{
prelude::{
component::ButtonStyle,
interaction::{
application_command::ApplicationCommandInteraction,
message_component::MessageComponentInteraction, InteractionResponseType,
},
},
user::User,
},
prelude::Context,
};
use crate::{
bot::commands::{respond_component_message, respond_message, CommandOutput},
session::{manager::SessionManager, pbi::PlaybackInfo},
utils::{
self,
embed::{EmbedBuilder, Status},
},
};
pub const NAME: &str = "playing";
pub fn command(ctx: Context, command: ApplicationCommandInteraction) -> CommandOutput {
Box::pin(async move {
let not_playing = async {
respond_message(
&ctx,
&command,
EmbedBuilder::new()
.title("Cannot get track info")
.icon_url("https://spoticord.com/forbidden.png")
.description("I'm currently not playing any music in this server")
.status(Status::Error)
.build(),
true,
)
.await;
};
let data = ctx.data.read().await;
let session_manager = data
.get::<SessionManager>()
.expect("to contain a value")
.clone();
let session = match session_manager
.get_session(command.guild_id.expect("to contain a value"))
.await
{
Some(session) => session,
None => {
not_playing.await;
return;
}
};
let owner = match session.owner().await {
Some(owner) => owner,
None => {
not_playing.await;
return;
}
};
// Get Playback Info from session
let pbi = match session.playback_info().await {
Some(pbi) => pbi,
None => {
not_playing.await;
return;
}
};
let spotify_id = match pbi.spotify_id {
Some(spotify_id) => spotify_id,
None => {
not_playing.await;
return;
}
};
// Get owner of session
let owner = match utils::discord::get_user(&ctx, owner).await {
Some(user) => user,
None => {
// This shouldn't happen
error!("Could not find user with ID: {owner}");
respond_message(
&ctx,
&command,
EmbedBuilder::new()
.title("[INTERNAL ERROR] Cannot get track info")
.description(format!(
"Could not find user with ID `{}`\nThis is an issue with the bot!",
owner
))
.status(Status::Error)
.build(),
true,
)
.await;
return;
}
};
// Get metadata
let (title, description, audio_type, thumbnail) = get_metadata(spotify_id, &pbi);
if let Err(why) = command
.create_interaction_response(&ctx.http, |response| {
response
.kind(InteractionResponseType::ChannelMessageWithSource)
.interaction_response_data(|message| {
message
.set_embed(build_playing_embed(
title,
audio_type,
spotify_id,
description,
owner,
thumbnail,
))
.components(|components| create_button(components, pbi.is_playing))
})
})
.await
{
error!("Error sending message: {why:?}");
}
})
}
pub fn component(ctx: Context, mut interaction: MessageComponentInteraction) -> CommandOutput {
Box::pin(async move {
let error_message = |title: &'static str, description: &'static str| async {
respond_component_message(
&ctx,
&interaction,
EmbedBuilder::new()
.title(title.to_string())
.icon_url("https://spoticord.com/forbidden.png")
.description(description.to_string())
.status(Status::Error)
.build(),
true,
)
.await;
};
let error_edit = |title: &'static str, description: &'static str| {
let mut interaction = interaction.clone();
let ctx = ctx.clone();
async move {
interaction.defer(&ctx.http).await.ok();
if let Err(why) = interaction
.message
.edit(&ctx, |message| {
message.embed(|embed| {
embed
.description(description)
.author(|author| {
author
.name(title)
.icon_url("https://spoticord.com/forbidden.png")
})
.color(Status::Error)
})
})
.await
{
error!("Failed to update playing message: {why}");
}
}
};
let data = ctx.data.read().await;
let session_manager = data
.get::<SessionManager>()
.expect("to contain a value")
.clone();
// Check if session still exists
let mut session = match session_manager
.get_session(interaction.guild_id.expect("to contain a value"))
.await
{
Some(session) => session,
None => {
error_edit(
"Cannot perform action",
"I'm currently not playing any music in this server",
)
.await;
return;
}
};
// Check if the session contains an owner
let owner = match session.owner().await {
Some(owner) => owner,
None => {
error_edit(
"Cannot change playback state",
"I'm currently not playing any music in this server",
)
.await;
return;
}
};
// Get Playback Info from session
let pbi = match session.playback_info().await {
Some(pbi) => pbi,
None => {
error_edit(
"Cannot change playback state",
"I'm currently not playing any music in this server",
)
.await;
return;
}
};
// Check if the user is the owner of the session
if owner != interaction.user.id {
error_message(
"Cannot change playback state",
"You must be the host to use the media buttons",
)
.await;
return;
}
// Get owner of session
let owner = match utils::discord::get_user(&ctx, owner).await {
Some(user) => user,
None => {
// This shouldn't happen
error!("Could not find user with ID: {owner}");
respond_component_message(
&ctx,
&interaction,
EmbedBuilder::new()
.title("[INTERNAL ERROR] Cannot get track info")
.description(format!(
"Could not find user with ID `{}`\nThis is an issue with the bot!",
owner
))
.status(Status::Error)
.build(),
true,
)
.await;
return;
}
};
// Send the desired command to the session
let success = match interaction.data.custom_id.as_str() {
"playing::btn_pause_play" => {
if pbi.is_playing {
session.pause().await.is_ok()
} else {
session.resume().await.is_ok()
}
}
"playing::btn_previous_track" => session.previous().await.is_ok(),
"playing::btn_next_track" => session.next().await.is_ok(),
_ => {
error!("Unknown custom_id: {}", interaction.data.custom_id);
false
}
};
if !success {
error_message(
"Cannot change playback state",
"An error occurred while trying to change the playback state",
)
.await;
return;
}
interaction.defer(&ctx.http).await.ok();
tokio::time::sleep(Duration::from_millis(
if interaction.data.custom_id == "playing::btn_pause_play" {
0
} else {
2500
},
))
.await;
update_embed(&mut interaction, &ctx, owner).await;
})
}
pub fn register(command: &mut CreateApplicationCommand) -> &mut CreateApplicationCommand {
command
.name(NAME)
.description("Display which song is currently being played")
}
fn create_button(components: &mut CreateComponents, playing: bool) -> &mut CreateComponents {
let mut prev_btn = CreateButton::default();
prev_btn
.style(ButtonStyle::Primary)
.label("<<")
.custom_id("playing::btn_previous_track");
let mut toggle_btn = CreateButton::default();
toggle_btn
.style(ButtonStyle::Secondary)
.label(if playing { "Pause" } else { "Play" })
.custom_id("playing::btn_pause_play");
let mut next_btn = CreateButton::default();
next_btn
.style(ButtonStyle::Primary)
.label(">>")
.custom_id("playing::btn_next_track");
components.create_action_row(|ar| {
ar.add_button(prev_btn)
.add_button(toggle_btn)
.add_button(next_btn)
})
}
async fn update_embed(interaction: &mut MessageComponentInteraction, ctx: &Context, owner: User) {
let error_edit = |title: &'static str, description: &'static str| {
let mut interaction = interaction.clone();
let ctx = ctx.clone();
async move {
interaction.defer(&ctx.http).await.ok();
if let Err(why) = interaction
.message
.edit(&ctx, |message| {
message.embed(|embed| {
embed
.description(description)
.author(|author| {
author
.name(title)
.icon_url("https://spoticord.com/forbidden.png")
})
.color(Status::Error)
})
})
.await
{
error!("Failed to update playing message: {why}");
}
}
};
let data = ctx.data.read().await;
let session_manager = data
.get::<SessionManager>()
.expect("to contain a value")
.clone();
// Check if session still exists
let session = match session_manager
.get_session(interaction.guild_id.expect("to contain a value"))
.await
{
Some(session) => session,
None => {
error_edit(
"Cannot perform action",
"I'm currently not playing any music in this server",
)
.await;
return;
}
};
// Get Playback Info from session
let pbi = match session.playback_info().await {
Some(pbi) => pbi,
None => {
error_edit(
"Cannot change playback state",
"I'm currently not playing any music in this server",
)
.await;
return;
}
};
let spotify_id = match pbi.spotify_id {
Some(spotify_id) => spotify_id,
None => {
error_edit(
"Cannot change playback state",
"I'm currently not playing any music in this server",
)
.await;
return;
}
};
let (title, description, audio_type, thumbnail) = get_metadata(spotify_id, &pbi);
if let Err(why) = interaction
.message
.edit(&ctx, |message| {
message
.set_embed(build_playing_embed(
title,
audio_type,
spotify_id,
description,
owner,
thumbnail,
))
.components(|components| create_button(components, pbi.is_playing));
message
})
.await
{
error!("Failed to update playing message: {why}");
}
}
fn build_playing_embed(
title: impl Into<String>,
audio_type: impl Into<String>,
spotify_id: SpotifyId,
description: impl Into<String>,
owner: User,
thumbnail: impl Into<String>,
) -> CreateEmbed {
let mut embed = CreateEmbed::default();
embed
.author(|author| {
author
.name("Currently Playing")
.icon_url("https://spoticord.com/spotify-logo.png")
})
.title(title.into())
.url(format!(
"https://open.spotify.com/{}/{}",
audio_type.into(),
spotify_id
.to_base62()
.expect("to be able to convert to base62")
))
.description(description.into())
.footer(|footer| footer.text(&owner.name).icon_url(owner.face()))
.thumbnail(thumbnail.into())
.color(Status::Info);
embed
}
fn get_metadata(spotify_id: SpotifyId, pbi: &PlaybackInfo) -> (String, String, String, String) {
// Get audio type
let audio_type = if spotify_id.audio_type == SpotifyAudioType::Track {
"track"
} else {
"episode"
};
// Create title
let title = format!(
"{} - {}",
pbi.get_artists().as_deref().unwrap_or("ID"),
pbi.get_name().as_deref().unwrap_or("ID")
);
// Create description
let mut description = String::new();
let position = pbi.get_position();
let spot = position * 20 / pbi.duration_ms;
description.push_str(if pbi.is_playing { "▶️ " } else { "⏸️ " });
for i in 0..20 {
if i == spot {
description.push('🔵');
} else {
description.push('▬');
}
}
description.push_str("\n:alarm_clock: ");
description.push_str(&format!(
"{} / {}",
utils::time_to_str(position / 1000),
utils::time_to_str(pbi.duration_ms / 1000)
));
// Get the thumbnail image
let thumbnail = pbi.get_thumbnail_url().expect("to contain a value");
(title, description, audio_type.to_string(), thumbnail)
}