Merge pull request #36 from SpoticordMusic/dev

PR: Update Spoticord to v2.2.1
main
Daniel 2024-08-22 10:25:21 +02:00 committed by GitHub
commit e383b4ab41
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
21 changed files with 274 additions and 94 deletions

View File

@ -18,15 +18,15 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v3 uses: actions/checkout@v4
- name: Set up Docker buildx - name: Set up Docker buildx
id: buildx id: buildx
uses: docker/setup-buildx-action@v2 uses: docker/setup-buildx-action@v3
- name: Login to GitHub's container registry - name: Login to GitHub's container registry
if: github.event_name != 'pull_request' if: github.event_name != 'pull_request'
uses: docker/login-action@v2 uses: docker/login-action@v3
with: with:
registry: ghcr.io registry: ghcr.io
username: ${{ github.repository_owner }} username: ${{ github.repository_owner }}

View File

@ -13,7 +13,7 @@ jobs:
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v2 uses: actions/checkout@v4
- name: Set up Rust - name: Set up Rust
uses: actions-rs/toolchain@v1 uses: actions-rs/toolchain@v1

View File

@ -1,6 +1,17 @@
# Changelog # Changelog
## 2.2.0 | August 8th 2024 ## 2.2.1 | August 22nd 2024
- Added new option: `/playing` can now receive an updating behavior parameter
- Added album name to `/playing` embed
- Fixed a bug where uncached guilds would panic the bot
- Fixed small issue with embed styling
- Updated to Rust 1.80.1 (from 1.79.0)
- Updated `diesel` and addons to latest versions
- Removed `lazy_static` in favor of `LazyLock` (Rust 1.80.0+ feature)
- Bumped MSRV to 1.80.0 due to the introduction of `LazyLock`
## 2.2.0 | August 13th 2024
### Changes ### Changes

View File

@ -86,4 +86,4 @@ cargo build [--release] --features stats
# MSRV # MSRV
The current minimum supported rust version is `1.75.0` _(Checked with `cargo-msrv`)_. The current minimum supported rust version is `1.80.0` _(Checked with `cargo-msrv`)_.

70
Cargo.lock generated
View File

@ -592,14 +592,12 @@ checksum = "e8566979429cf69b49a5c740c60791108e86440e8be149bbea4fe54d2c32d6e2"
[[package]] [[package]]
name = "deadpool" name = "deadpool"
version = "0.9.5" version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "421fe0f90f2ab22016f32a9881be5134fdd71c65298917084b0c7477cbc3856e" checksum = "6541a3916932fe57768d4be0b1ffb5ec7cbf74ca8c903fdfd5c0fe8aa958f0ed"
dependencies = [ dependencies = [
"async-trait",
"deadpool-runtime", "deadpool-runtime",
"num_cpus", "num_cpus",
"retain_mut",
"tokio", "tokio",
] ]
@ -681,22 +679,23 @@ dependencies = [
[[package]] [[package]]
name = "diesel" name = "diesel"
version = "2.1.6" version = "2.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ff236accb9a5069572099f0b350a92e9560e8e63a9b8d546162f4a5e03026bb2" checksum = "bf97ee7261bb708fa3402fa9c17a54b70e90e3cb98afb3dc8999d5512cb03f94"
dependencies = [ dependencies = [
"bitflags 2.6.0", "bitflags 2.6.0",
"byteorder", "byteorder",
"chrono", "chrono",
"diesel_derives", "diesel_derives",
"itoa", "itoa",
"pq-sys",
] ]
[[package]] [[package]]
name = "diesel-async" name = "diesel-async"
version = "0.4.1" version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "acada1517534c92d3f382217b485db8a8638f111b0e3f2a2a8e26165050f77be" checksum = "fcb799bb6f8ca6a794462125d7b8983b0c86e6c93a33a9c55934a4a5de4409d3"
dependencies = [ dependencies = [
"async-trait", "async-trait",
"deadpool", "deadpool",
@ -709,9 +708,9 @@ dependencies = [
[[package]] [[package]]
name = "diesel_async_migrations" name = "diesel_async_migrations"
version = "0.12.0" version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a700d6b83a17973b94d3065970fd2b36f1036c3fe08adcbdce1c9beb8fb25553" checksum = "377dd8e9d0fdab3dbb66236f8f71206b98e121991449f3b363a19d7948333160"
dependencies = [ dependencies = [
"diesel", "diesel",
"diesel-async", "diesel-async",
@ -732,11 +731,12 @@ dependencies = [
[[package]] [[package]]
name = "diesel_derives" name = "diesel_derives"
version = "2.1.4" version = "2.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "14701062d6bed917b5c7103bdffaee1e4609279e240488ad24e7bd979ca6866c" checksum = "d6ff2be1e7312c858b2ef974f5c7089833ae57b5311b334b30923af58e5718d8"
dependencies = [ dependencies = [
"diesel_table_macro_syntax", "diesel_table_macro_syntax",
"dsl_auto_type",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.72", "syn 2.0.72",
@ -744,9 +744,9 @@ dependencies = [
[[package]] [[package]]
name = "diesel_table_macro_syntax" name = "diesel_table_macro_syntax"
version = "0.1.0" version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc5557efc453706fed5e4fa85006fe9817c224c3f480a34c7e5959fd700921c5" checksum = "209c735641a413bc68c4923a9d6ad4bcb3ca306b794edaa7eb0b3228a99ffb25"
dependencies = [ dependencies = [
"syn 2.0.72", "syn 2.0.72",
] ]
@ -779,6 +779,20 @@ version = "0.15.7"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b"
[[package]]
name = "dsl_auto_type"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c5d9abe6314103864cc2d8901b7ae224e0ab1a103a0a416661b4097b0779b607"
dependencies = [
"darling 0.20.10",
"either",
"heck 0.5.0",
"proc-macro2",
"quote",
"syn 2.0.72",
]
[[package]] [[package]]
name = "either" name = "either"
version = "1.13.0" version = "1.13.0"
@ -2515,6 +2529,15 @@ dependencies = [
"zerocopy", "zerocopy",
] ]
[[package]]
name = "pq-sys"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a24ff9e4cf6945c988f0db7005d87747bf72864965c3529d259ad155ac41d584"
dependencies = [
"vcpkg",
]
[[package]] [[package]]
name = "primal-check" name = "primal-check"
version = "0.3.4" version = "0.3.4"
@ -2904,12 +2927,6 @@ dependencies = [
"winreg 0.52.0", "winreg 0.52.0",
] ]
[[package]]
name = "retain_mut"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4389f1d5789befaf6029ebd9f7dac4af7f7e3d61b69d4f30e2ac02b57e7712b0"
[[package]] [[package]]
name = "ring" name = "ring"
version = "0.16.20" version = "0.16.20"
@ -3721,7 +3738,7 @@ dependencies = [
[[package]] [[package]]
name = "spoticord" name = "spoticord"
version = "2.2.0" version = "2.2.1"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"dotenvy", "dotenvy",
@ -3753,7 +3770,6 @@ dependencies = [
name = "spoticord_config" name = "spoticord_config"
version = "2.2.0" version = "2.2.0"
dependencies = [ dependencies = [
"lazy_static",
"rspotify", "rspotify",
"serenity", "serenity",
] ]
@ -4217,9 +4233,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
[[package]] [[package]]
name = "tokio" name = "tokio"
version = "1.39.2" version = "1.39.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "daa4fb1bc778bd6f04cbfc4bb2d06a7396a8f299dc33ea1900cedaa316f467b1" checksum = "9babc99b9923bfa4804bd74722ff02c0381021eafa4db9949217e3be8e84fff5"
dependencies = [ dependencies = [
"backtrace", "backtrace",
"bytes", "bytes",
@ -4792,6 +4808,12 @@ dependencies = [
"ryu", "ryu",
] ]
[[package]]
name = "vcpkg"
version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
[[package]] [[package]]
name = "vergen" name = "vergen"
version = "8.3.2" version = "8.3.2"

View File

@ -1,8 +1,8 @@
[package] [package]
name = "spoticord" name = "spoticord"
version = "2.2.0" version = "2.2.1"
edition = "2021" edition = "2021"
rust-version = "1.75.0" rust-version = "1.80.0"
[[bin]] [[bin]]
name = "spoticord" name = "spoticord"
@ -38,7 +38,7 @@ log = "0.4.22"
poise = "0.6.1" poise = "0.6.1"
serenity = "0.12.2" serenity = "0.12.2"
songbird = { version = "0.4.3", features = ["simd-json"] } songbird = { version = "0.4.3", features = ["simd-json"] }
tokio = { version = "1.39.2", features = ["full"] } tokio = { version = "1.39.3", features = ["full"] }
[profile.release] [profile.release]
opt-level = 3 opt-level = 3

View File

@ -1,11 +1,29 @@
# This Dockerfile has been specifically crafted to be run on an AMD64 build host, where
# the build should compile for both amd64 and arm64 targets
#
# Building on any other platform, or building for only a single target will be significantly
# slower compared to a platform agnostic Dockerfile, or might not work at all
#
# This has been done to make this file be optimized for use within GitHub Actions,
# as using QEMU to compile takes way too long (multiple hours)
# Builder # Builder
FROM --platform=linux/amd64 rust:1.79.0-buster AS builder FROM --platform=linux/amd64 rust:1.80.1-slim AS builder
WORKDIR /app WORKDIR /app
# Add extra build dependencies here # Add extra build dependencies here
RUN apt-get update && apt-get install -yqq \ RUN apt-get update && apt install -yqq \
cmake gcc-aarch64-linux-gnu binutils-aarch64-linux-gnu cmake gcc-aarch64-linux-gnu binutils-aarch64-linux-gnu libpq-dev curl bzip2
# Manually compile an arm64 build of libpq
ENV PGVER=16.4
RUN curl -o postgresql.tar.bz2 https://ftp.postgresql.org/pub/source/v${PGVER}/postgresql-${PGVER}.tar.bz2 && \
tar xjf postgresql.tar.bz2 && \
cd postgresql-${PGVER} && \
./configure --host=aarch64-linux-gnu --enable-shared --disable-static --without-readline --without-zlib --without-icu && \
cd src/interfaces/libpq && \
make
COPY . . COPY . .
@ -14,13 +32,14 @@ RUN rustup target add x86_64-unknown-linux-gnu aarch64-unknown-linux-gnu
# Add `--no-default-features` if you don't want stats collection # Add `--no-default-features` if you don't want stats collection
RUN --mount=type=cache,target=/usr/local/cargo/registry \ RUN --mount=type=cache,target=/usr/local/cargo/registry \
--mount=type=cache,target=/app/target \ --mount=type=cache,target=/app/target \
cargo build --release --target=x86_64-unknown-linux-gnu --target=aarch64-unknown-linux-gnu && \ cargo build --release --target=x86_64-unknown-linux-gnu && \
RUSTFLAGS="-L /app/postgresql-${PGVER}/src/interfaces/libpq -C linker=aarch64-linux-gnu-gcc" cargo build --release --target=aarch64-unknown-linux-gnu && \
# Copy the executables outside of /target as it'll get unmounted after this RUN command # Copy the executables outside of /target as it'll get unmounted after this RUN command
cp /app/target/x86_64-unknown-linux-gnu/release/spoticord /app/x86_64 && \ cp /app/target/x86_64-unknown-linux-gnu/release/spoticord /app/x86_64 && \
cp /app/target/aarch64-unknown-linux-gnu/release/spoticord /app/aarch64 cp /app/target/aarch64-unknown-linux-gnu/release/spoticord /app/aarch64
# Runtime # Runtime
FROM debian:buster-slim FROM debian:bookworm-slim
ARG TARGETPLATFORM ARG TARGETPLATFORM
ENV TARGETPLATFORM=${TARGETPLATFORM} ENV TARGETPLATFORM=${TARGETPLATFORM}

View File

@ -8,5 +8,5 @@ edition = "2021"
[dependencies] [dependencies]
librespot = { git = "https://github.com/SpoticordMusic/librespot.git", version = "0.5.0-dev", default-features = false } librespot = { git = "https://github.com/SpoticordMusic/librespot.git", version = "0.5.0-dev", default-features = false }
songbird = { version = "0.4.3", features = ["simd-json"] } songbird = { version = "0.4.3", features = ["simd-json"] }
tokio = { version = "1.39.2", features = ["sync"], default-features = false } tokio = { version = "1.39.3", features = ["sync"], default-features = false }
zerocopy = "0.7.35" zerocopy = "0.7.35"

View File

@ -4,7 +4,6 @@ version = "2.2.0"
edition = "2021" edition = "2021"
[dependencies] [dependencies]
lazy_static = "1.5.0"
rspotify = { version = "0.13.2", default-features = false, features = [ rspotify = { version = "0.13.2", default-features = false, features = [
"client-reqwest", "client-reqwest",
"reqwest-rustls-tls", "reqwest-rustls-tls",

View File

@ -1,14 +1,17 @@
use lazy_static::lazy_static; use std::sync::LazyLock;
lazy_static! { pub static DISCORD_TOKEN: LazyLock<String> = LazyLock::new(|| {
pub static ref DISCORD_TOKEN: String = std::env::var("DISCORD_TOKEN").expect("missing DISCORD_TOKEN environment variable")
std::env::var("DISCORD_TOKEN").expect("missing DISCORD_TOKEN environment variable"); });
pub static ref DATABASE_URL: String = pub static DATABASE_URL: LazyLock<String> = LazyLock::new(|| {
std::env::var("DATABASE_URL").expect("missing DATABASE_URL environment variable"); std::env::var("DATABASE_URL").expect("missing DATABASE_URL environment variable")
pub static ref LINK_URL: String = });
std::env::var("LINK_URL").expect("missing LINK_URL environment variable"); pub static LINK_URL: LazyLock<String> =
pub static ref SPOTIFY_CLIENT_ID: String = LazyLock::new(|| std::env::var("LINK_URL").expect("missing LINK_URL environment variable"));
std::env::var("SPOTIFY_CLIENT_ID").expect("missing SPOTIFY_CLIENT_ID environment variable"); pub static SPOTIFY_CLIENT_ID: LazyLock<String> = LazyLock::new(|| {
pub static ref SPOTIFY_CLIENT_SECRET: String = std::env::var("SPOTIFY_CLIENT_SECRET") std::env::var("SPOTIFY_CLIENT_ID").expect("missing SPOTIFY_CLIENT_ID environment variable")
.expect("missing SPOTIFY_CLIENT_SECRET environment variable"); });
} pub static SPOTIFY_CLIENT_SECRET: LazyLock<String> = LazyLock::new(|| {
std::env::var("SPOTIFY_CLIENT_SECRET")
.expect("missing SPOTIFY_CLIENT_SECRET environment variable")
});

View File

@ -6,8 +6,8 @@ edition = "2021"
[dependencies] [dependencies]
spoticord_config = { path = "../spoticord_config" } spoticord_config = { path = "../spoticord_config" }
diesel = { version = "2.1.6", features = ["chrono"] } diesel = { version = "2.2.2", features = ["chrono"] }
diesel-async = { version = "0.4.1", features = ["deadpool", "postgres"] } diesel-async = { version = "0.5.0", features = ["deadpool", "postgres"] }
rspotify = { version = "0.13.2", default-features = false, features = [ rspotify = { version = "0.13.2", default-features = false, features = [
"client-reqwest", "client-reqwest",
"reqwest-rustls-tls", "reqwest-rustls-tls",
@ -15,4 +15,4 @@ rspotify = { version = "0.13.2", default-features = false, features = [
chrono = "0.4.38" chrono = "0.4.38"
thiserror = "1.0.63" thiserror = "1.0.63"
rand = "0.8.5" rand = "0.8.5"
diesel_async_migrations = "0.12.0" diesel_async_migrations = "0.13.0"

View File

@ -11,7 +11,7 @@ spoticord_utils = { path = "../spoticord_utils" }
librespot = { git = "https://github.com/SpoticordMusic/librespot.git", version = "0.5.0-dev", default-features = false } librespot = { git = "https://github.com/SpoticordMusic/librespot.git", version = "0.5.0-dev", default-features = false }
songbird = { version = "0.4.3", features = ["simd-json"] } songbird = { version = "0.4.3", features = ["simd-json"] }
tokio = { version = "1.39.2", features = ["full"] } tokio = { version = "1.39.3", features = ["full"] }
anyhow = "1.0.86" anyhow = "1.0.86"
log = "0.4.22" log = "0.4.22"
symphonia = { version = "0.5.4", default-features = false, features = ["pcm"] } symphonia = { version = "0.5.4", default-features = false, features = ["pcm"] }

View File

@ -65,6 +65,13 @@ impl PlaybackInfo {
} }
} }
pub fn album_name(&self) -> Option<String> {
match &self.audio_item.unique_fields {
UniqueFields::Episode { .. } => None,
UniqueFields::Track { album, .. } => Some(album.to_string()),
}
}
pub fn thumbnail(&self) -> String { pub fn thumbnail(&self) -> String {
self.audio_item self.audio_item
.covers .covers

View File

@ -11,7 +11,7 @@ spoticord_database = { path = "../spoticord_database" }
spoticord_player = { path = "../spoticord_player" } spoticord_player = { path = "../spoticord_player" }
spoticord_utils = { path = "../spoticord_utils" } spoticord_utils = { path = "../spoticord_utils" }
tokio = { version = "1.39.2", features = ["full"] } tokio = { version = "1.39.3", features = ["full"] }
librespot = { git = "https://github.com/SpoticordMusic/librespot.git", version = "0.5.0-dev", default-features = false } librespot = { git = "https://github.com/SpoticordMusic/librespot.git", version = "0.5.0-dev", default-features = false }
serenity = "0.12.2" serenity = "0.12.2"
songbird = { version = "0.4.3", features = ["simd-json"] } songbird = { version = "0.4.3", features = ["simd-json"] }

View File

@ -31,7 +31,11 @@ pub enum SessionCommand {
GetPlayer(oneshot::Sender<PlayerHandle>), GetPlayer(oneshot::Sender<PlayerHandle>),
GetActive(oneshot::Sender<bool>), GetActive(oneshot::Sender<bool>),
CreatePlaybackEmbed(SessionHandle, CommandInteraction), CreatePlaybackEmbed(
SessionHandle,
CommandInteraction,
playback_embed::UpdateBehavior,
),
CreateLyricsEmbed(SessionHandle, CommandInteraction), CreateLyricsEmbed(SessionHandle, CommandInteraction),
Reactivate(UserId, oneshot::Sender<Result<()>>), Reactivate(UserId, oneshot::Sender<Result<()>>),
@ -207,8 +211,8 @@ impl Session {
SessionCommand::GetPlayer(sender) => _ = sender.send(self.player.clone()), SessionCommand::GetPlayer(sender) => _ = sender.send(self.player.clone()),
SessionCommand::GetActive(sender) => _ = sender.send(self.active), SessionCommand::GetActive(sender) => _ = sender.send(self.active),
SessionCommand::CreatePlaybackEmbed(handle, interaction) => { SessionCommand::CreatePlaybackEmbed(handle, interaction, behavior) => {
match PlaybackEmbed::create(self, handle, interaction).await { match PlaybackEmbed::create(self, handle, interaction, behavior).await {
Ok(Some(playback_embed)) => { Ok(Some(playback_embed)) => {
self.playback_embed = Some(playback_embed); self.playback_embed = Some(playback_embed);
} }
@ -274,8 +278,10 @@ impl Session {
PlayerEvent::TrackChanged(_) => {} PlayerEvent::TrackChanged(_) => {}
} }
let force_edit = !matches!(event, PlayerEvent::TrackChanged(_));
if let Some(playback_embed) = &self.playback_embed { if let Some(playback_embed) = &self.playback_embed {
if playback_embed.invoke_update().await.is_err() { if playback_embed.invoke_update(force_edit).await.is_err() {
self.playback_embed = None; self.playback_embed = None;
} }
} }
@ -457,11 +463,16 @@ impl SessionHandle {
/// Create a playback embed as a response to an interaction /// Create a playback embed as a response to an interaction
/// ///
/// This playback embed will automatically update when certain events happen /// This playback embed will automatically update when certain events happen
pub async fn create_playback_embed(&self, interaction: CommandInteraction) -> Result<()> { pub async fn create_playback_embed(
&self,
interaction: CommandInteraction,
behavior: playback_embed::UpdateBehavior,
) -> Result<()> {
self.commands self.commands
.send(SessionCommand::CreatePlaybackEmbed( .send(SessionCommand::CreatePlaybackEmbed(
self.clone(), self.clone(),
interaction, interaction,
behavior,
)) ))
.await?; .await?;

View File

@ -1,11 +1,12 @@
use anyhow::{anyhow, Result}; use anyhow::{anyhow, Result};
use log::{error, trace}; use log::{error, trace};
use poise::ChoiceParameter;
use serenity::{ use serenity::{
all::{ all::{
ButtonStyle, CommandInteraction, ComponentInteraction, ComponentInteractionCollector, ButtonStyle, CommandInteraction, ComponentInteraction, ComponentInteractionCollector,
Context, CreateActionRow, CreateButton, CreateEmbed, CreateEmbedAuthor, CreateEmbedFooter, Context, CreateActionRow, CreateButton, CreateEmbed, CreateEmbedAuthor, CreateEmbedFooter,
CreateInteractionResponse, CreateInteractionResponseFollowup, CreateInteractionResponse, CreateInteractionResponseFollowup,
CreateInteractionResponseMessage, EditMessage, Message, User, CreateInteractionResponseMessage, CreateMessage, EditMessage, Message, User,
}, },
futures::StreamExt, futures::StreamExt,
}; };
@ -18,7 +19,30 @@ use crate::{Session, SessionHandle};
#[derive(Debug)] #[derive(Debug)]
pub enum Command { pub enum Command {
InvokeUpdate, InvokeUpdate(bool),
}
#[derive(Debug, Default, ChoiceParameter)]
pub enum UpdateBehavior {
#[default]
#[name = "Automatically update the embed"]
Default,
#[name = "Do not update the embed"]
Static,
#[name = "Re-send the embed after track changes"]
Pinned,
}
impl UpdateBehavior {
pub fn is_static(&self) -> bool {
matches!(self, Self::Static)
}
pub fn is_pinned(&self) -> bool {
matches!(self, Self::Pinned)
}
} }
pub struct PlaybackEmbed { pub struct PlaybackEmbed {
@ -29,6 +53,8 @@ pub struct PlaybackEmbed {
last_update: Instant, last_update: Instant,
update_in: Option<Duration>, update_in: Option<Duration>,
force_edit: bool,
update_behavior: UpdateBehavior,
rx: mpsc::Receiver<Command>, rx: mpsc::Receiver<Command>,
} }
@ -38,6 +64,7 @@ impl PlaybackEmbed {
session: &Session, session: &Session,
handle: SessionHandle, handle: SessionHandle,
interaction: CommandInteraction, interaction: CommandInteraction,
update_behavior: UpdateBehavior,
) -> Result<Option<PlaybackEmbedHandle>> { ) -> Result<Option<PlaybackEmbedHandle>> {
let ctx = session.context.clone(); let ctx = session.context.clone();
@ -69,6 +96,11 @@ impl PlaybackEmbed {
) )
.await?; .await?;
// If this is a static embed, we don't need to return any handles
if update_behavior.is_static() {
return Ok(None);
}
// Retrieve message instead of editing interaction response, as those tokens are only valid for 15 minutes // Retrieve message instead of editing interaction response, as those tokens are only valid for 15 minutes
let message = interaction.get_response(&ctx).await?; let message = interaction.get_response(&ctx).await?;
@ -84,6 +116,8 @@ impl PlaybackEmbed {
message, message,
last_update: Instant::now(), last_update: Instant::now(),
update_in: None, update_in: None,
force_edit: false,
update_behavior,
rx, rx,
}; };
@ -121,7 +155,7 @@ impl PlaybackEmbed {
tokio::time::sleep(update_in).await; tokio::time::sleep(update_in).await;
} }
}, if self.update_in.is_some() => { }, if self.update_in.is_some() => {
if self.update_embed().await.is_break() { if self.update_embed(self.force_edit).await.is_break() {
break; break;
} }
} }
@ -133,15 +167,16 @@ impl PlaybackEmbed {
trace!("Received command: {command:?}"); trace!("Received command: {command:?}");
match command { match command {
Command::InvokeUpdate => { Command::InvokeUpdate(force_edit) => {
if self.last_update.elapsed() < Duration::from_secs(2) { if self.last_update.elapsed() < Duration::from_secs(2) {
if self.update_in.is_some() { if self.update_in.is_some() {
return ControlFlow::Continue(()); return ControlFlow::Continue(());
} }
self.update_in = Some(Duration::from_secs(2) - self.last_update.elapsed()); self.update_in = Some(Duration::from_secs(2) - self.last_update.elapsed());
self.force_edit = force_edit;
} else { } else {
self.update_embed().await?; self.update_embed(force_edit).await?;
} }
} }
} }
@ -216,7 +251,7 @@ impl PlaybackEmbed {
Ok((player, playback_info, owner)) Ok((player, playback_info, owner))
} }
async fn update_embed(&mut self) -> ControlFlow<(), ()> { async fn update_embed(&mut self, force_edit: bool) -> ControlFlow<(), ()> {
self.update_in = None; self.update_in = None;
let Ok(owner) = self.session.owner().await else { let Ok(owner) = self.session.owner().await else {
@ -246,7 +281,30 @@ impl PlaybackEmbed {
} }
}; };
if let Err(why) = self let should_pin = !force_edit && self.update_behavior.is_pinned();
if should_pin {
self.message.delete(&self.ctx).await.ok();
match self
.message
.channel_id
.send_message(
&self.ctx,
CreateMessage::new()
.embed(build_embed(&playback_info, &owner))
.components(vec![build_buttons(self.id, playback_info.playing())]),
)
.await
{
Ok(message) => self.message = message,
Err(why) => {
error!("Failed to update playback embed: {why}");
return ControlFlow::Break(());
}
};
} else if let Err(why) = self
.message .message
.edit( .edit(
&self.ctx, &self.ctx,
@ -259,7 +317,7 @@ impl PlaybackEmbed {
error!("Failed to update playback embed: {why}"); error!("Failed to update playback embed: {why}");
return ControlFlow::Break(()); return ControlFlow::Break(());
}; }
self.last_update = Instant::now(); self.last_update = Instant::now();
@ -267,6 +325,18 @@ impl PlaybackEmbed {
} }
async fn update_not_playing(&mut self) -> Result<()> { async fn update_not_playing(&mut self) -> Result<()> {
// If pinned, try to delete old message and send new one
if self.update_behavior.is_pinned() {
self.message.delete(&self.ctx).await.ok();
self.message = self
.message
.channel_id
.send_message(&self.ctx, CreateMessage::new().embed(not_playing_embed()))
.await?;
return Ok(());
}
self.message self.message
.edit(&self.ctx, EditMessage::new().embed(not_playing_embed())) .edit(&self.ctx, EditMessage::new().embed(not_playing_embed()))
.await?; .await?;
@ -284,8 +354,8 @@ impl PlaybackEmbedHandle {
!self.tx.is_closed() !self.tx.is_closed()
} }
pub async fn invoke_update(&self) -> Result<()> { pub async fn invoke_update(&self, force_edit: bool) -> Result<()> {
self.tx.send(Command::InvokeUpdate).await?; self.tx.send(Command::InvokeUpdate(force_edit)).await?;
Ok(()) Ok(())
} }
@ -331,21 +401,27 @@ fn build_embed(playback_info: &PlaybackInfo, owner: &User) -> CreateEmbed {
.collect::<Vec<_>>() .collect::<Vec<_>>()
.join(", "); .join(", ");
description += &format!("By {artists}\n\n"); description += &format!("By {artists}\n");
}
if let Some(album_name) = playback_info.album_name() {
description += &format!("Album: **{album_name}**\n");
} }
if let Some(show_name) = playback_info.show_name() { if let Some(show_name) = playback_info.show_name() {
description += &format!("On {show_name}\n\n"); description += &format!("On {show_name}\n");
} }
description += "\n";
let position = playback_info.current_position(); let position = playback_info.current_position();
let index = position * 20 / playback_info.duration(); let index = position * 20 / playback_info.duration();
description.push_str(if playback_info.playing() { description += if playback_info.playing() {
"▶️ " "▶️ "
} else { } else {
"⏸️ " "⏸️ "
}); };
for i in 0..20 { for i in 0..20 {
if i == index { if i == index {
@ -355,12 +431,12 @@ fn build_embed(playback_info: &PlaybackInfo, owner: &User) -> CreateEmbed {
} }
} }
description.push_str("\n:alarm_clock: "); description += "\n:alarm_clock: ";
description.push_str(&format!( description += &format!(
"{} / {}", "{} / {}",
spoticord_utils::time_to_string(position / 1000), spoticord_utils::time_to_string(position / 1000),
spoticord_utils::time_to_string(playback_info.duration() / 1000) spoticord_utils::time_to_string(playback_info.duration() / 1000)
)); );
CreateEmbed::new() CreateEmbed::new()
.author( .author(

View File

@ -9,7 +9,7 @@ use crate::bot::Context;
#[poise::command(slash_command, guild_only)] #[poise::command(slash_command, guild_only)]
pub async fn disconnect(ctx: Context<'_>) -> Result<(), Error> { pub async fn disconnect(ctx: Context<'_>) -> Result<(), Error> {
let manager = ctx.data(); let manager = ctx.data();
let guild = ctx.guild().expect("poise lied to me").id; let guild = ctx.guild_id().expect("poise lied to me");
let Some(session) = manager.get_session(SessionQuery::Guild(guild)) else { let Some(session) = manager.get_session(SessionQuery::Guild(guild)) else {
ctx.send( ctx.send(

View File

@ -4,7 +4,7 @@ use anyhow::Result;
use log::error; use log::error;
use poise::CreateReply; use poise::CreateReply;
use serenity::all::{ use serenity::all::{
Channel, ChannelId, CreateEmbed, CreateEmbedAuthor, CreateEmbedFooter, Guild, UserId, Channel, ChannelId, CreateEmbed, CreateEmbedAuthor, CreateEmbedFooter, UserId,
}; };
use spoticord_database::error::DatabaseError; use spoticord_database::error::DatabaseError;
use spoticord_session::manager::SessionQuery; use spoticord_session::manager::SessionQuery;
@ -15,9 +15,30 @@ use crate::bot::Context;
/// Join the current voice channel /// Join the current voice channel
#[poise::command(slash_command, guild_only)] #[poise::command(slash_command, guild_only)]
pub async fn join(ctx: Context<'_>) -> Result<()> { pub async fn join(ctx: Context<'_>) -> Result<()> {
let guild: Guild = ctx.guild().expect("poise lied to me").clone(); let guild = ctx.guild_id().expect("poise lied to me");
let manager = ctx.data(); let manager = ctx.data();
let Some(guild) = guild
.to_guild_cached(ctx.serenity_context())
.map(|guild| guild.clone())
else {
error!("Unable to fetch guild from cache, how did we get here?");
ctx.send(
CreateReply::default()
.embed(
CreateEmbed::new()
.title("An error occured")
.description("This server hasn't been cached yet?")
.color(Colors::Error),
)
.ephemeral(true),
)
.await?;
return Ok(());
};
let Some(channel) = guild let Some(channel) = guild
.voice_states .voice_states
.get(&ctx.author().id) .get(&ctx.author().id)
@ -98,8 +119,6 @@ pub async fn join(ctx: Context<'_>) -> Result<()> {
return Ok(()); return Ok(());
} }
ctx.defer().await?;
let mut session_opt = manager.get_session(SessionQuery::Guild(guild.id)); let mut session_opt = manager.get_session(SessionQuery::Guild(guild.id));
// Check if this server already has a session active // Check if this server already has a session active
@ -134,7 +153,8 @@ pub async fn join(ctx: Context<'_>) -> Result<()> {
"You are already using Spoticord in `{}`\n\n\ "You are already using Spoticord in `{}`\n\n\
Stop playing in that server first before starting a new session.", Stop playing in that server first before starting a new session.",
spoticord_utils::discord::escape(server_name) spoticord_utils::discord::escape(server_name)
)), ))
.color(Colors::Error),
) )
.ephemeral(true), .ephemeral(true),
) )
@ -143,6 +163,8 @@ pub async fn join(ctx: Context<'_>) -> Result<()> {
return Ok(()); return Ok(());
} }
ctx.defer().await?;
if let Some(session) = &session_opt { if let Some(session) = &session_opt {
if session.voice_channel() != channel { if session.voice_channel() != channel {
session.disconnect().await; session.disconnect().await;
@ -163,7 +185,7 @@ pub async fn join(ctx: Context<'_>) -> Result<()> {
CreateEmbed::new() CreateEmbed::new()
.title("Failed to reactivate session") .title("Failed to reactivate session")
.description( .description(
"An error occured whilst trying to reactivate the session.", "An error occured whilst trying to reactivate the session. Please try again.",
) )
.color(Colors::Error), .color(Colors::Error),
) )
@ -190,7 +212,9 @@ pub async fn join(ctx: Context<'_>) -> Result<()> {
.embed( .embed(
CreateEmbed::new() CreateEmbed::new()
.title("Failed to create session") .title("Failed to create session")
.description("An error occured whilst trying to create a session.") .description(
"An error occured whilst trying to create a session. Please try again.",
)
.color(Colors::Error), .color(Colors::Error),
) )
.ephemeral(true), .ephemeral(true),

View File

@ -10,7 +10,7 @@ use crate::bot::Context;
#[poise::command(slash_command, guild_only)] #[poise::command(slash_command, guild_only)]
pub async fn lyrics(ctx: Context<'_>) -> Result<()> { pub async fn lyrics(ctx: Context<'_>) -> Result<()> {
let manager = ctx.data(); let manager = ctx.data();
let guild = ctx.guild().expect("poise lied to me").id; let guild = ctx.guild_id().expect("poise lied to me");
let Some(session) = manager.get_session(SessionQuery::Guild(guild)) else { let Some(session) = manager.get_session(SessionQuery::Guild(guild)) else {
ctx.send( ctx.send(

View File

@ -1,16 +1,21 @@
use anyhow::Result; use anyhow::Result;
use poise::CreateReply; use poise::CreateReply;
use serenity::all::CreateEmbed; use serenity::all::CreateEmbed;
use spoticord_session::manager::SessionQuery; use spoticord_session::{manager::SessionQuery, playback_embed::UpdateBehavior};
use spoticord_utils::discord::Colors; use spoticord_utils::discord::Colors;
use crate::bot::Context; use crate::bot::Context;
/// Show details of the current song that is being played /// Show details of the current song that is being played
#[poise::command(slash_command, guild_only)] #[poise::command(slash_command, guild_only)]
pub async fn playing(ctx: Context<'_>) -> Result<()> { pub async fn playing(
ctx: Context<'_>,
#[description = "How Spoticord should update this information"] update_behavior: Option<
UpdateBehavior,
>,
) -> Result<()> {
let manager = ctx.data(); let manager = ctx.data();
let guild = ctx.guild().expect("poise lied to me").id; let guild = ctx.guild_id().expect("poise lied to me");
let Some(session) = manager.get_session(SessionQuery::Guild(guild)) else { let Some(session) = manager.get_session(SessionQuery::Guild(guild)) else {
ctx.send( ctx.send(
@ -33,7 +38,10 @@ pub async fn playing(ctx: Context<'_>) -> Result<()> {
}; };
session session
.create_playback_embed(context.interaction.clone()) .create_playback_embed(
context.interaction.clone(),
update_behavior.unwrap_or_default(),
)
.await?; .await?;
Ok(()) Ok(())

View File

@ -9,7 +9,7 @@ use crate::bot::Context;
#[poise::command(slash_command, guild_only)] #[poise::command(slash_command, guild_only)]
pub async fn stop(ctx: Context<'_>) -> Result<(), Error> { pub async fn stop(ctx: Context<'_>) -> Result<(), Error> {
let manager = ctx.data(); let manager = ctx.data();
let guild = ctx.guild().expect("poise lied to me").id; let guild = ctx.guild_id().expect("poise lied to me");
let Some(session) = manager.get_session(SessionQuery::Guild(guild)) else { let Some(session) = manager.get_session(SessionQuery::Guild(guild)) else {
ctx.send( ctx.send(