Merge pull request #25 from SpoticordMusic/dev

PR: Update Spoticord to v2.1.2
main
Daniel 2023-09-28 14:18:14 +02:00 committed by GitHub
commit d1c01e9274
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 361 additions and 417 deletions

View File

@ -4,3 +4,5 @@ protocol = "sparse"
[target.x86_64-pc-windows-gnu]
rustflags = "-C link-args=-lssp" # Does not compile without this line
[target.aarch64-unknown-linux-gnu]
rustflags = "-C linker=aarch64-linux-gnu-gcc"

View File

@ -1,34 +1,66 @@
name: Build and push to repository
name: Build and push to registry
on:
push:
branches: [ "main" ]
branches: [ "main", "dev" ]
tags: [ "v*.*.*" ]
pull_request:
branches: [ "main", "dev" ]
workflow_dispatch:
permissions:
packages: write
contents: read
jobs:
build-and-push:
name: Build Docker image and push to repository
name: Build Docker image and push to registry
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Set up Docker buildx
id: buildx
uses: docker/setup-buildx-action@v2
- name: Login to repository
- name: Login to GitHub's container registry
if: github.event_name != 'pull_request'
uses: docker/login-action@v2
with:
registry: ${{ secrets.REGISTRY_URL }}
username: ${{ secrets.REGISTRY_USERNAME }}
password: ${{ secrets.REGISTRY_PASSWORD }}
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- uses: Entepotenz/change-string-case-action-min-dependencies@v1 # https://github.com/orgs/community/discussions/10553
id: repo-uri-string
with:
string: ghcr.io/${{ github.repository }}
- name: Generate image metadata
id: docker-meta # used in next step
uses: docker/metadata-action@v5
with:
# list of Docker images to use as base name for tags
images: ${{ steps.repo-uri-string.outputs.lowercase }}
# Docker tags based on the following events/attributes
tags: |
type=raw,value=latest,enable={{is_default_branch}}
type=ref,event=branch
type=ref,event=pr
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{major}}
type=sha
- name: Build image and push to registry
uses: docker/build-push-action@v2
uses: docker/build-push-action@v5
with:
context: .
file: ./Dockerfile
tags: |
${{ secrets.REGISTRY_URL }}/spoticord/spoticord:latest
push: ${{ github.ref == 'refs/heads/main' }}
platforms: linux/amd64,linux/arm64
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.docker-meta.outputs.tags }}
labels: ${{ steps.docker-meta.outputs.labels }}
# Some basic caching of the layers...
cache-from: ${{ steps.repo-uri-string.outputs.lowercase }}:latest-cache
cache-to: ${{ steps.repo-uri-string.outputs.lowercase }}:latest-cache

View File

@ -1,3 +1,47 @@
# Changelog
## 2.1.2 | September 28th 2023
### Changes
* Removed OpenSSL dependency
* Added aarch64 support
* Added cross compilation to Github Actions
* Added `dev` branch to Github Actions
* Removed hardcoded URL in the /join command
* Fixed an issue in /playing where the bot showed it was playing even though it was paused
**Full Changelog**: https://github.com/SpoticordMusic/spoticord/compare/v2.1.1...v2.1.2
## 2.1.1 | September 23rd 2023
Reduced the amount of CPU that the bot uses from ~15%-25% per user to 1%-2% per user (percentage per core, benched on an AMD Ryzen 9 5950X).
### Changes
* Fixed issue #20
**Full Changelog**: https://github.com/SpoticordMusic/spoticord/compare/v2.1.0...v2.1.1
## 2.1.0 | September 20th 2023
So, it's been a while since I worked on this project, and some bugs have since been discovered.
The main focus for this version is to stop using multiple processes for every player, and instead do everything in threads.
### Changes
- Remove metrics, as I wasn't using this feature anyways
- Bring back KV for storing total/active sessions, as prometheus is no longer being used
- Allocate new players in-memory, instead of using subprocesses
- Fix issue #17
- Fix some issues with the auto-disconnect
- Removed the automatic device switching on bot join, which was causing some people to not be able to use the bot
- Force communication through the closest Spotify AP, reducing latency
- Potential jitter reduction
- Enable autoplay
- After skipping a song, you will no longer hear a tiny bit of the previous song after the silence
**Full Changelog**: https://github.com/SpoticordMusic/spoticord/compare/v2.0.0...v2.1.0
### Issues
- Currently, the CPU usage is much higher than it used to be. I really wanted to push this update out before taking the time to do some optimizations, as the bot and server are still easily able to hold up the limited amount of Spoticord users (and v2.0.0 was just falling apart). Issue is being tracked in #20
## 2.0.0 | June 8th 2023
- Initial Release
- Initial Release

View File

@ -27,17 +27,17 @@ sudo pacman -S base-devel
sudo dnf install gcc
```
Additionally, you will need to install CMake and OpenSSL (Linux only). On Windows, you can download CMake [here](https://cmake.org/download/). On Linux, you can use your package manager to install them:
Additionally, you will need to install CMake. On Windows, you can download CMake [here](https://cmake.org/download/). On Linux, you can use your package manager to install it:
```sh
# Debian/Ubuntu
sudo apt install cmake libssl-dev
sudo apt install cmake
# Arch
sudo pacman -S cmake openssl
sudo pacman -S cmake
# Fedora
sudo dnf install cmake openssl-devel
sudo dnf install cmake
```
## Compiling

179
Cargo.lock generated
View File

@ -328,16 +328,6 @@ dependencies = [
"syn 1.0.109",
]
[[package]]
name = "core-foundation"
version = "0.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146"
dependencies = [
"core-foundation-sys",
"libc",
]
[[package]]
name = "core-foundation-sys"
version = "0.8.4"
@ -541,9 +531,9 @@ dependencies = [
[[package]]
name = "fastrand"
version = "2.0.0"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6999dc1837253364c2ebb0704ba97994bd874e8f195d665c50b7548f6ea92764"
checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5"
[[package]]
name = "fixedbitset"
@ -580,21 +570,6 @@ version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
[[package]]
name = "foreign-types"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1"
dependencies = [
"foreign-types-shared",
]
[[package]]
name = "foreign-types-shared"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
[[package]]
name = "form_urlencoded"
version = "1.2.0"
@ -940,19 +915,6 @@ dependencies = [
"tokio-rustls 0.24.1",
]
[[package]]
name = "hyper-tls"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905"
dependencies = [
"bytes",
"hyper",
"native-tls",
"tokio",
"tokio-native-tls",
]
[[package]]
name = "iana-time-zone"
version = "0.1.57"
@ -1409,24 +1371,6 @@ dependencies = [
"getrandom",
]
[[package]]
name = "native-tls"
version = "0.2.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "07226173c32f2926027b63cce4bcd8076c3552846cbe7925f3aaffeac0a3b92e"
dependencies = [
"lazy_static",
"libc",
"log",
"openssl",
"openssl-probe",
"openssl-sys",
"schannel",
"security-framework",
"security-framework-sys",
"tempfile",
]
[[package]]
name = "nix"
version = "0.23.2"
@ -1531,50 +1475,6 @@ version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5"
[[package]]
name = "openssl"
version = "0.10.57"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bac25ee399abb46215765b1cb35bc0212377e58a061560d8b29b024fd0430e7c"
dependencies = [
"bitflags 2.4.0",
"cfg-if",
"foreign-types",
"libc",
"once_cell",
"openssl-macros",
"openssl-sys",
]
[[package]]
name = "openssl-macros"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.37",
]
[[package]]
name = "openssl-probe"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf"
[[package]]
name = "openssl-sys"
version = "0.9.93"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "db4d56a4c0478783083cfafcc42493dd4a981d41669da64b4572a2a089b51b1d"
dependencies = [
"cc",
"libc",
"pkg-config",
"vcpkg",
]
[[package]]
name = "ordered-float"
version = "2.10.0"
@ -1827,8 +1727,6 @@ dependencies = [
"itoa",
"percent-encoding",
"ryu",
"sha1_smol",
"socket2 0.4.9",
"url",
]
@ -1910,13 +1808,11 @@ dependencies = [
"http-body",
"hyper",
"hyper-rustls",
"hyper-tls",
"ipnet",
"js-sys",
"log",
"mime",
"mime_guess",
"native-tls",
"once_cell",
"percent-encoding",
"pin-project-lite",
@ -1926,7 +1822,6 @@ dependencies = [
"serde_json",
"serde_urlencoded",
"tokio",
"tokio-native-tls",
"tokio-rustls 0.24.1",
"tokio-util",
"tower-service",
@ -2068,15 +1963,6 @@ dependencies = [
"libsamplerate-sys",
]
[[package]]
name = "schannel"
version = "0.1.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c3733bf4cf7ea0880754e19cb5a462007c4a8c1914bff372ccc95b464f1df88"
dependencies = [
"windows-sys",
]
[[package]]
name = "scoped-tls"
version = "1.0.1"
@ -2099,29 +1985,6 @@ dependencies = [
"untrusted",
]
[[package]]
name = "security-framework"
version = "2.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05b64fb303737d99b81884b2c63433e9ae28abebe5eb5045dcdd175dc2ecf4de"
dependencies = [
"bitflags 1.3.2",
"core-foundation",
"core-foundation-sys",
"libc",
"security-framework-sys",
]
[[package]]
name = "security-framework-sys"
version = "2.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e932934257d3b408ed8f30db49d85ea163bfe74961f017f405b025af298f0c7a"
dependencies = [
"core-foundation-sys",
"libc",
]
[[package]]
name = "semver"
version = "1.0.19"
@ -2274,12 +2137,6 @@ dependencies = [
"digest 0.10.7",
]
[[package]]
name = "sha1_smol"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae1a47186c03a32177042e55dbc5fd5aee900b8e0069a8d70fba96a9375cd012"
[[package]]
name = "shannon"
version = "0.2.0"
@ -2398,12 +2255,13 @@ dependencies = [
[[package]]
name = "spoticord"
version = "2.1.1"
version = "2.1.2"
dependencies = [
"anyhow",
"dotenv",
"env_logger 0.10.0",
"hex",
"lazy_static",
"librespot",
"log",
"protobuf",
@ -2415,7 +2273,6 @@ dependencies = [
"serenity",
"songbird",
"thiserror",
"time",
"tokio",
"zerocopy 0.7.5",
]
@ -2543,9 +2400,9 @@ dependencies = [
[[package]]
name = "time"
version = "0.3.28"
version = "0.3.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "17f6bb557fd245c28e6411aa56b6403c689ad95061f50e4be16c274e70a17e48"
checksum = "426f806f4089c493dcac0d24c29c01e2c38baf8e30f1b716ee37e83d200b18fe"
dependencies = [
"deranged",
"itoa",
@ -2556,15 +2413,15 @@ dependencies = [
[[package]]
name = "time-core"
version = "0.1.1"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7300fbefb4dadc1af235a9cef3737cea692a9d97e1b9cbcd4ebdae6f8868e6fb"
checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3"
[[package]]
name = "time-macros"
version = "0.2.14"
version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a942f44339478ef67935ab2bbaec2fb0322496cf3cbe84b261e06ac3814c572"
checksum = "4ad70d68dba9e1f8aceda7aa6711965dfec1cac869f311a51bd08b3a2ccbce20"
dependencies = [
"time-core",
]
@ -2614,16 +2471,6 @@ dependencies = [
"syn 2.0.37",
]
[[package]]
name = "tokio-native-tls"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2"
dependencies = [
"native-tls",
"tokio",
]
[[package]]
name = "tokio-rustls"
version = "0.23.4"
@ -2888,12 +2735,6 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d"
[[package]]
name = "vcpkg"
version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
[[package]]
name = "vergen"
version = "3.2.0"

View File

@ -1,6 +1,6 @@
[package]
name = "spoticord"
version = "2.1.1"
version = "2.1.2"
edition = "2021"
rust-version = "1.65.0"
@ -16,18 +16,18 @@ anyhow = "1.0.75"
dotenv = "0.15.0"
env_logger = "0.10.0"
hex = "0.4.3"
lazy_static = "1.4.0"
librespot = { version = "0.4.2", default-features = false }
log = "0.4.20"
protobuf = "2.28.0"
redis = { version = "0.23.3", optional = true }
reqwest = "0.11.20"
redis = { version = "0.23.3", optional = true, default-features = false }
reqwest = { version = "0.11.20", default-features = false }
samplerate = "0.2.4"
serde = "1.0.188"
serde_json = "1.0.107"
serenity = { version = "0.11.6", features = ["framework", "cache", "standard_framework"], default-features = false }
songbird = "0.3.2"
serenity = { version = "0.11.6", features = ["framework", "cache", "standard_framework", "rustls_backend", "gateway"], default-features = false }
songbird = { version = "0.3.2", features = ["driver", "serenity-rustls"], default-features = false }
thiserror = "1.0.48"
time = "0.3.28"
tokio = { version = "1.32.0", features = ["rt", "full"] }
zerocopy = "0.7.5"

View File

@ -1,25 +1,44 @@
# Builder
FROM rust:1.72.1-buster as builder
FROM --platform=linux/amd64 rust:1.72.1-buster as builder
WORKDIR /app
# Add extra build dependencies here
RUN apt-get update && apt-get install -y cmake
RUN apt-get update && apt-get install -yqq \
cmake gcc-aarch64-linux-gnu binutils-aarch64-linux-gnu
COPY . .
# Remove `--features stats` if you want to deploy without stats collection
RUN cargo install --path . --features stats
RUN rustup target add x86_64-unknown-linux-gnu aarch64-unknown-linux-gnu
# Remove `--features=stats` if you want to deploy without stats collection
RUN cargo build --features=stats --release \
--target=x86_64-unknown-linux-gnu --target=aarch64-unknown-linux-gnu
# Runtime
FROM debian:buster-slim
WORKDIR /app
ARG TARGETPLATFORM
ENV TARGETPLATFORM=$TARGETPLATFORM
# Add extra runtime dependencies here
RUN apt-get update && apt-get install -y openssl ca-certificates && rm -rf /var/lib/apt/lists/*
# RUN apt-get update && apt-get install -yqq --no-install-recommends \
# openssl ca-certificates && rm -rf /var/lib/apt/lists/*
# Copy spoticord binary from builder
COPY --from=builder /usr/local/cargo/bin/spoticord ./spoticord
# Copy spoticord binaries from builder to /tmp
COPY --from=builder \
/app/target/x86_64-unknown-linux-gnu/release/spoticord /tmp/x86_64
COPY --from=builder \
/app/target/aarch64-unknown-linux-gnu/release/spoticord /tmp/aarch64
CMD ["./spoticord"]
# Copy appropiate binary for target arch from /tmp
RUN if [ "$TARGETPLATFORM" = "linux/amd64" ]; then \
cp /tmp/x86_64 /usr/local/bin/spoticord; \
elif [ "$TARGETPLATFORM" = "linux/arm64" ]; then \
cp /tmp/aarch64 /usr/local/bin/spoticord; \
fi
# Delete unused binaries
RUN rm -rvf /tmp/x86_64 /tmp/aarch64
ENTRYPOINT [ "/usr/local/bin/spoticord" ]

View File

@ -8,6 +8,7 @@ use serenity::{
use crate::{
bot::commands::{respond_message, CommandOutput},
consts::SPOTICORD_ACCOUNTS_URL,
database::{Database, DatabaseError},
utils::embed::{EmbedBuilder, Status},
};
@ -39,8 +40,11 @@ pub fn command(ctx: Context, command: ApplicationCommandInteraction) -> CommandO
}
if let Ok(request) = database.get_user_request(command.user.id.to_string()).await {
let base = std::env::var("SPOTICORD_ACCOUNTS_URL").expect("to be present");
let link = format!("{}/spotify/{}", base, request.token);
let link = format!(
"{}/spotify/{}",
SPOTICORD_ACCOUNTS_URL.as_str(),
request.token
);
respond_message(
&ctx,
@ -106,8 +110,11 @@ pub fn command(ctx: Context, command: ApplicationCommandInteraction) -> CommandO
.await
{
Ok(request) => {
let base = std::env::var("SPOTICORD_ACCOUNTS_URL").expect("to be present");
let link = format!("{}/spotify/{}", base, request.token);
let link = format!(
"{}/spotify/{}",
SPOTICORD_ACCOUNTS_URL.as_str(),
request.token
);
respond_message(
&ctx,

View File

@ -7,6 +7,7 @@ use serenity::{
use crate::{
bot::commands::{defer_message, respond_message, update_message, CommandOutput},
consts::SPOTICORD_ACCOUNTS_URL,
session::manager::{SessionCreateError, SessionManager},
utils::embed::{EmbedBuilder, Status},
};
@ -241,7 +242,7 @@ pub fn command(ctx: Context, command: ApplicationCommandInteraction) -> CommandO
&command,
EmbedBuilder::new()
.title("Cannot join voice channel")
.description("You need to link your Spotify account. Use </link:1036714850367320136> or go to [the accounts website](https://account.spoticord.com/) to get started.")
.description(format!("You need to link your Spotify account. Use </link:1036714850367320136> or go to [the accounts website]({}) to get started.", SPOTICORD_ACCOUNTS_URL.as_str()))
.status(Status::Error)
.build(),
)
@ -255,7 +256,7 @@ pub fn command(ctx: Context, command: ApplicationCommandInteraction) -> CommandO
&command,
EmbedBuilder::new()
.title("Cannot join voice channel")
.description("Spoticord no longer has access to your Spotify account. Use </link:1036714850367320136> or go to [the accounts website](https://account.spoticord.com/) to relink your Spotify account.")
.description(format!("Spoticord no longer has access to your Spotify account. Use </link:1036714850367320136> or go to [the accounts website]({}) to relink your Spotify account.", SPOTICORD_ACCOUNTS_URL.as_str()))
.status(Status::Error)
.build(),
).await;

View File

@ -30,20 +30,22 @@ 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;
};
macro_rules! not_playing {
() => {
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
@ -51,62 +53,50 @@ pub fn command(ctx: Context, command: ApplicationCommandInteraction) -> CommandO
.expect("to contain a value")
.clone();
let session = match session_manager
let Some(session) = session_manager
.get_session(command.guild_id.expect("to contain a value"))
.await
{
Some(session) => session,
None => {
not_playing.await;
else {
not_playing!();
return;
}
return;
};
let owner = match session.owner().await {
Some(owner) => owner,
None => {
not_playing.await;
let Some(owner) = session.owner().await else {
not_playing!();
return;
}
return;
};
// Get Playback Info from session
let pbi = match session.playback_info().await {
Some(pbi) => pbi,
None => {
not_playing.await;
let Some(pbi) = session.playback_info().await else {
not_playing!();
return;
}
return;
};
// Get owner of session
let owner = match utils::discord::get_user(&ctx, owner).await {
Some(user) => user,
None => {
// This shouldn't happen
let Some(owner) = utils::discord::get_user(&ctx, owner).await else {
// This shouldn't happen
error!("Could not find user with ID: {owner}");
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;
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;
}
return;
};
// Get metadata
@ -188,48 +178,39 @@ pub fn component(ctx: Context, mut interaction: MessageComponentInteraction) ->
.clone();
// Check if session still exists
let mut session = match session_manager
let Some(mut session) = 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;
else {
error_edit(
"Cannot perform action",
"I'm currently not playing any music in this server",
)
.await;
return;
}
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;
let Some(owner) = session.owner().await else {
error_edit(
"Cannot change playback state",
"I'm currently not playing any music in this server",
)
.await;
return;
}
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;
let Some(pbi) = session.playback_info().await else {
error_edit(
"Cannot change playback state",
"I'm currently not playing any music in this server",
)
.await;
return;
}
return;
};
// Check if the user is the owner of the session
@ -244,30 +225,27 @@ pub fn component(ctx: Context, mut interaction: MessageComponentInteraction) ->
}
// Get owner of session
let owner = match utils::discord::get_user(&ctx, owner).await {
Some(user) => user,
None => {
// This shouldn't happen
let Some(owner) = utils::discord::get_user(&ctx, owner).await else {
// This shouldn't happen
error!("Could not find user with ID: {owner}");
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;
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;
}
return;
};
// Send the desired command to the session
@ -370,34 +348,28 @@ async fn update_embed(interaction: &mut MessageComponentInteraction, ctx: &Conte
.clone();
// Check if session still exists
let session = match session_manager
let Some(session) = 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;
else {
error_edit(
"Cannot perform action",
"I'm currently not playing any music in this server",
)
.await;
return;
}
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;
let Some(pbi) = session.playback_info().await else {
error_edit(
"Cannot change playback state",
"I'm currently not playing any music in this server",
)
.await;
return;
}
return;
};
let (title, description, thumbnail) = get_metadata(&pbi);

View File

@ -54,23 +54,22 @@ impl EventHandler for Handler {
// INTERACTION_CREATE event, emitted when the bot receives an interaction (slash command, button, etc.)
async fn interaction_create(&self, ctx: Context, interaction: Interaction) {
match interaction {
Interaction::ApplicationCommand(command) => self.handle_command(ctx, command).await,
Interaction::MessageComponent(component) => self.handle_component(ctx, component).await,
Interaction::ApplicationCommand(command) => handle_command(ctx, command).await,
Interaction::MessageComponent(component) => handle_component(ctx, component).await,
_ => {}
}
}
}
impl Handler {
async fn handle_command(&self, ctx: Context, command: ApplicationCommandInteraction) {
enforce_guild!(command);
async fn handle_command(ctx: Context, command: ApplicationCommandInteraction) {
enforce_guild!(command);
// Commands must only be executed inside of guilds
// Commands must only be executed inside of guilds
let guild_id = match command.guild_id {
Some(guild_id) => guild_id,
None => {
if let Err(why) = command
let guild_id = match command.guild_id {
Some(guild_id) => guild_id,
None => {
if let Err(why) = command
.create_interaction_response(&ctx.http, |response| {
response
.kind(serenity::model::prelude::interaction::InteractionResponseType::ChannelMessageWithSource)
@ -82,32 +81,32 @@ impl Handler {
error!("Failed to send run-in-guild-only error message: {}", why);
}
return;
}
};
return;
}
};
trace!(
"Received command interaction: command={} user={} guild={}",
command.data.name,
command.user.id,
guild_id
);
trace!(
"Received command interaction: command={} user={} guild={}",
command.data.name,
command.user.id,
guild_id
);
let data = ctx.data.read().await;
let command_manager = data.get::<CommandManager>().expect("to contain a value");
let data = ctx.data.read().await;
let command_manager = data.get::<CommandManager>().expect("to contain a value");
command_manager.execute_command(&ctx, command).await;
}
command_manager.execute_command(&ctx, command).await;
}
async fn handle_component(&self, ctx: Context, component: MessageComponentInteraction) {
enforce_guild!(component);
async fn handle_component(ctx: Context, component: MessageComponentInteraction) {
enforce_guild!(component);
// Components can only be interacted with inside of guilds
// Components can only be interacted with inside of guilds
let guild_id = match component.guild_id {
Some(guild_id) => guild_id,
None => {
if let Err(why) = component
let guild_id = match component.guild_id {
Some(guild_id) => guild_id,
None => {
if let Err(why) = component
.create_interaction_response(&ctx.http, |response| {
response
.kind(serenity::model::prelude::interaction::InteractionResponseType::ChannelMessageWithSource)
@ -119,20 +118,19 @@ impl Handler {
error!("Failed to send run-in-guild-only error message: {}", why);
}
return;
}
};
return;
}
};
trace!(
"Received component interaction: command={} user={} guild={}",
component.data.custom_id,
component.user.id,
guild_id
);
trace!(
"Received component interaction: command={} user={} guild={}",
component.data.custom_id,
component.user.id,
guild_id
);
let data = ctx.data.read().await;
let command_manager = data.get::<CommandManager>().expect("to contain a value");
let data = ctx.data.read().await;
let command_manager = data.get::<CommandManager>().expect("to contain a value");
command_manager.execute_component(&ctx, component).await;
}
command_manager.execute_component(&ctx, component).await;
}

View File

@ -1,3 +1,5 @@
use lazy_static::lazy_static;
#[cfg(not(debug_assertions))]
pub const VERSION: &str = env!("CARGO_PKG_VERSION");
@ -8,3 +10,18 @@ pub const MOTD: &str = "some good 'ol music";
/// The time it takes for Spoticord to disconnect when no music is being played
pub const DISCONNECT_TIME: u64 = 5 * 60;
lazy_static! {
pub static ref DISCORD_TOKEN: String =
std::env::var("DISCORD_TOKEN").expect("missing DISCORD_TOKEN environment variable");
pub static ref DATABASE_URL: String =
std::env::var("DATABASE_URL").expect("missing DATABASE_URL environment variable");
pub static ref SPOTICORD_ACCOUNTS_URL: String = std::env::var("SPOTICORD_ACCOUNTS_URL")
.expect("missing SPOTICORD_ACCOUNTS_URL environment variable");
}
#[cfg(feature = "stats")]
lazy_static! {
pub static ref KV_URL: String =
std::env::var("KV_URL").expect("missing KV_URL environment variable");
}

View File

@ -1,10 +1,18 @@
use dotenv::dotenv;
use crate::{bot::commands::CommandManager, database::Database, session::manager::SessionManager};
#[cfg(feature = "stats")]
use crate::consts::KV_URL;
use crate::{
bot::commands::CommandManager,
consts::{DATABASE_URL, DISCORD_TOKEN, MOTD},
database::Database,
session::manager::SessionManager,
};
use log::*;
use serenity::{framework::StandardFramework, prelude::GatewayIntents, Client};
use songbird::SerenityInit;
use std::{any::Any, env, process::exit};
use std::{any::Any, process::exit};
#[cfg(unix)]
use tokio::signal::unix::SignalKind;
@ -41,7 +49,7 @@ async fn main() {
env_logger::init();
info!("It's a good day");
info!(" - Spoticord {}", time::OffsetDateTime::now_utc().year());
info!(" - Spoticord, {}", MOTD);
let result = dotenv();
@ -54,19 +62,14 @@ async fn main() {
warn!("No .env file found, expecting all necessary environment variables");
}
let token = env::var("DISCORD_TOKEN").expect("a token in the environment");
let db_url = env::var("DATABASE_URL").expect("a database URL in the environment");
#[cfg(feature = "stats")]
let stats_manager =
StatsManager::new(env::var("KV_URL").expect("a redis URL in the environment"))
.expect("Failed to connect to redis");
let stats_manager = StatsManager::new(KV_URL.as_str()).expect("Failed to connect to redis");
let session_manager = SessionManager::new();
// Create client
let mut client = Client::builder(
token,
DISCORD_TOKEN.as_str(),
GatewayIntents::GUILDS | GatewayIntents::GUILD_VOICE_STATES,
)
.event_handler(crate::bot::events::Handler)
@ -78,16 +81,13 @@ async fn main() {
{
let mut data = client.data.write().await;
data.insert::<Database>(Database::new(db_url, None));
data.insert::<Database>(Database::new(DATABASE_URL.as_str(), None));
data.insert::<CommandManager>(CommandManager::new());
data.insert::<SessionManager>(session_manager.clone());
}
let shard_manager = client.shard_manager.clone();
#[cfg(feature = "stats")]
let cache = client.cache_and_http.cache.clone();
#[cfg(unix)]
let mut term: Option<Box<dyn Any + Send>> = Some(Box::new(
tokio::signal::unix::signal(SignalKind::terminate())
@ -104,13 +104,8 @@ async fn main() {
_ = tokio::time::sleep(std::time::Duration::from_secs(60)) => {
#[cfg(feature = "stats")]
{
let guild_count = cache.guilds().len();
let active_count = session_manager.get_active_session_count().await;
if let Err(why) = stats_manager.set_server_count(guild_count) {
error!("Failed to update server count: {why}");
}
if let Err(why) = stats_manager.set_active_count(active_count) {
error!("Failed to update active count: {why}");
}

View File

@ -318,13 +318,13 @@ impl PlayerTask {
match pbi.as_mut() {
Some(pbi) => {
pbi.update_track(spotify_id, current);
pbi.update_pos_dur(position_ms, duration_ms, true);
pbi.update_pos_dur(position_ms, duration_ms, playing);
}
None => {
*pbi = Some(PlaybackInfo::new(
duration_ms,
position_ms,
true,
playing,
current,
spotify_id,
));

View File

@ -56,11 +56,9 @@ impl InnerSessionManager {
session: SpoticordSession,
guild_id: GuildId,
owner_id: UserId,
) -> Result<(), SessionCreateError> {
) {
self.sessions.insert(guild_id, session);
self.owner_map.insert(owner_id, guild_id);
Ok(())
}
/// Remove a session
@ -147,7 +145,9 @@ impl SessionManager {
.write()
.await
.create_session(session, guild_id, owner_id)
.await
.await;
Ok(())
}
/// Remove a session

View File

@ -36,6 +36,12 @@ use tokio::sync::{Mutex, RwLockReadGuard, RwLockWriteGuard};
#[derive(Clone)]
pub struct SpoticordSession(Arc<RwLock<InnerSpoticordSession>>);
impl Drop for SpoticordSession {
fn drop(&mut self) {
log::trace!("drop SpoticordSession");
}
}
struct InnerSpoticordSession {
owner: Option<UserId>,
guild_id: GuildId,
@ -97,7 +103,11 @@ impl SpoticordSession {
};
let mut instance = Self(Arc::new(RwLock::new(inner)));
instance.create_player(ctx).await?;
if let Err(why) = instance.create_player(ctx).await {
songbird.remove(guild_id).await.ok();
return Err(why);
}
let mut call = call.lock().await;
@ -336,6 +346,8 @@ impl SpoticordSession {
timer.tick().await;
timer.tick().await;
trace!("Ring ring, time to check :)");
// Make sure this task has not been aborted, if it has this will automatically stop execution.
tokio::task::yield_now().await;
@ -345,6 +357,8 @@ impl SpoticordSession {
.map(|pbi| pbi.is_playing)
.unwrap_or(false);
trace!("is_playing = {is_playing}");
if !is_playing {
info!("Player is not playing, disconnecting");
session
@ -499,19 +513,19 @@ impl InnerSpoticordSession {
.remove_session(self.guild_id, self.owner)
.await;
let mut call = self.call.lock().await;
if let Some(track) = self.track.take() {
if let Err(why) = track.stop() {
error!("Failed to stop track: {:?}", why);
}
}
};
call.remove_all_global_events();
let mut call = self.call.lock().await;
if let Err(why) = call.leave().await {
error!("Failed to leave voice channel: {:?}", why);
}
call.remove_all_global_events();
}
}
@ -521,10 +535,12 @@ impl EventHandler for SpoticordSession {
match ctx {
EventContext::DriverDisconnect(_) => {
debug!("Driver disconnected, leaving voice channel");
trace!("Arc strong count: {}", Arc::strong_count(&self.0));
self.disconnect().await;
}
EventContext::ClientDisconnect(who) => {
trace!("Client disconnected, {}", who.user_id.to_string());
trace!("Arc strong count: {}", Arc::strong_count(&self.0));
if let Some(session) = self
.session_manager()
@ -545,3 +561,9 @@ impl EventHandler for SpoticordSession {
return None;
}
}
impl Drop for InnerSpoticordSession {
fn drop(&mut self) {
log::trace!("drop InnerSpoticordSession");
}
}

View File

@ -12,12 +12,6 @@ impl StatsManager {
Ok(StatsManager { redis })
}
pub fn set_server_count(&self, count: usize) -> Result<()> {
let mut con = self.redis.get_connection()?;
con.set("sc-bot-total-servers", count.to_string())
}
pub fn set_active_count(&self, count: usize) -> Result<()> {
let mut con = self.redis.get_connection()?;