Merge pull request #8 from SpoticordMusic/dev

Merge some bugfixes and QoL updates to main
main
DaXcess 2022-11-07 20:45:17 +01:00 committed by GitHub
commit e61ef877a3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 418 additions and 252 deletions

View File

@ -0,0 +1,2 @@
[target.x86_64-pc-windows-gnu]
rustflags = "-C link-args=-lssp" # Does does compile without this line

72
COMPILING.md 100644
View File

@ -0,0 +1,72 @@
# Compiling from source
## Initial setup
Spoticord is built using [rust](https://www.rust-lang.org/), so you'll need to install that first. It is cross-platform, so it should work on Windows, Linux and MacOS. You can find more info about how to install rust [here](https://www.rust-lang.org/tools/install).
### Rust formatter
Spoticord uses [rustfmt](https://github.com/rust-lang/rustfmt) to format the code, and we ask everyone that contributes to Spoticord to use it as well. You can install it by running the following command in your terminal:
```sh
rustup component add rustfmt
```
If you are using VSCode, you can install the [rust-analyzer](https://marketplace.visualstudio.com/items?itemName=matklad.rust-analyzer) extension, which will automatically format your code when you save it (if you have `format on save` enabled). Although rust-analyzer is recommended anyway, as it provides a lot of useful features.
## Build dependencies
On Windows you'll need to install the [Visual Studio Build Tools](https://visualstudio.microsoft.com/downloads/#build-tools-for-visual-studio-2019) to be able to compile executables in rust (this will also be explained during the rust installation).
If you are on Linux, you can use your package manager to install the following dependencies:
```sh
# Debian/Ubuntu
sudo apt install build-essential
# Arch
sudo pacman -S base-devel
# Fedora
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:
```sh
# Debian/Ubuntu
sudo apt install cmake libssl-dev
# Arch
sudo pacman -S cmake openssl
# Fedora
sudo dnf install cmake openssl-devel
```
## Compiling
Now that you have all the dependencies installed, you can compile Spoticord. To do this, you'll first need to clone the repository:
```sh
git clone https://github.com/SpoticordMusic/spoticord.git
```
After cloning the repo run the following command in the root of the repository:
```sh
cargo build
```
Or if you want to build a release version:
```sh
cargo build --release
```
This will compile the bot and place the executable in `target/release`. You can now run the bot by running the following command:
```sh
./target/release/spoticord
```
If you are actively developing Spoticord, you can use the following command to build and run the bot (this is easier than building and running the bot manually):
```sh
cargo run
```

View File

@ -12,6 +12,7 @@ lto = true
codegen-units = 1
strip = true
opt-level = "z"
panic = "abort"
[dependencies]
chrono = "0.4.22"

View File

@ -1,3 +1,35 @@
# Spoticord
Spoticord is a Discord music bot that allows you to control your music using the Spotify app.
Spoticord is a Discord music bot that allows you to control your music using the Spotify app.
Spoticord is built on top of [librespot](https://github.com/librespot-org/librespot) (with tiny additional changes), to allow full control using the Spotify client, with [serenity](https://github.com/serenity-rs/serenity) and [songbird](https://github.com/serenity-rs/songbird) for Discord communication.
Being built on top of rust, Spoticord is relatively lightweight and can run on low-spec hardware.
## How to use
### Official bot
Spoticord is being hosted as an official bot. You can find more info about how to use this bot over at [the Spoticord website](https://spoticord.com/).
### Environment variables
Spoticord uses environment variables to configure itself. The following variables are required:
- `DISCORD_TOKEN`: The Discord bot token used for authenticating with Discord.
- `DATABASE_URL`: The base URL of the database API used for storing user data. This base URL must point to an instance of [the Spoticord Database API](https://github.com/SpoticordMusic/spoticord-database).
- `SPOTICORD_ACCOUNTS_URL`: The base URL of the accounts frontend used for authenticating with Spotify. This base URL must point to an instance of [the Spoticord Accounts frontend](https://github.com/SpoticordMusic/spoticord-accounts).
Additionally you can configure the following variables:
- `GUILD_ID`: The ID of the Discord server where this bot will create commands for. This is used during testing to prevent the bot from creating slash commands in other servers, as well as getting the commands quicker. This variable is optional, and if not set, the bot will create commands in all servers it is in (this may take up to 15 minutes).
- `KV_URL`: The connection URL of a redis-server instance used for storing realtime data. While not required, not providing one will cause the bot to spit out quite a bit of errors. You might want to comment out those error lines in `main.rs`.
#### Providing environment variables
You can provide environment variables in a `.env` file at the root of the working directory of Spoticord.
You can also provide environment variables the normal way, e.g. the command line, using `export` (or `set` for Windows) or using docker.
Environment variables set this way take precedence over those in the `.env` file (if one exists).
# Compiling
For information about how to compile Spoticord from source, check out [COMPILING.md](COMPILING.md).
# Contact
![Discord Shield](https://discordapp.com/api/guilds/779292533053456404/widget.png?style=shield)
If you have any questions, feel free to join the [Spoticord Discord server](https://discord.gg/wRCyhVqBZ5)!
# License
Spoticord is licensed under the [Apache License 2.0](LICENSE).

View File

@ -1,125 +1,30 @@
use librespot::playback::audio_backend::{Sink, SinkAsBytes, SinkResult};
use librespot::playback::convert::Converter;
use librespot::playback::decoder::AudioPacket;
use log::{error, trace};
use std::io::Write;
use std::sync::{Arc, Mutex};
use std::thread::JoinHandle;
use std::time::Duration;
use crate::ipc;
use crate::ipc::packet::IpcPacket;
pub struct StdoutSink {
client: ipc::Client,
buffer: Arc<Mutex<Vec<u8>>>,
is_stopped: Arc<Mutex<bool>>,
handle: Option<JoinHandle<()>>,
}
const BUFFER_SIZE: usize = 7680;
impl StdoutSink {
pub fn start_writer(&mut self) {
// With 48khz, 32-bit float, 2 channels, 1 second of audio is 384000 bytes
// 384000 / 50 = 7680 bytes per 20ms
let buffer = self.buffer.clone();
let is_stopped = self.is_stopped.clone();
let client = self.client.clone();
let handle = std::thread::spawn(move || {
let mut output = std::io::stdout();
let mut act_buffer = [0u8; BUFFER_SIZE];
// Use closure to make sure lock is released as fast as possible
let is_stopped = || {
let is_stopped = is_stopped.lock().unwrap();
*is_stopped
};
// Start songbird's playback
client.send(IpcPacket::StartPlayback).unwrap();
loop {
if is_stopped() {
break;
}
std::thread::sleep(Duration::from_millis(15));
let mut buffer = buffer.lock().unwrap();
let to_drain: usize;
if buffer.len() < BUFFER_SIZE {
// Copy the buffer into the action buffer
// Fill remaining length with zeroes
act_buffer[..buffer.len()].copy_from_slice(&buffer[..]);
act_buffer[buffer.len()..].fill(0);
to_drain = buffer.len();
} else {
act_buffer.copy_from_slice(&buffer[..BUFFER_SIZE]);
to_drain = BUFFER_SIZE;
}
output.write_all(&act_buffer).unwrap_or(());
buffer.drain(..to_drain);
}
});
self.handle = Some(handle);
}
pub fn stop_writer(&mut self) -> std::thread::Result<()> {
// Use closure to avoid deadlocking the mutex
let set_stopped = |value| {
let mut is_stopped = self.is_stopped.lock().unwrap();
*is_stopped = value;
};
// Notify thread to stop
set_stopped(true);
// Wait for thread to stop
let result = match self.handle.take() {
Some(handle) => handle.join(),
None => Ok(()),
};
// Reset stopped value
set_stopped(false);
result
}
pub fn new(client: ipc::Client) -> Self {
StdoutSink {
client,
is_stopped: Arc::new(Mutex::new(false)),
buffer: Arc::new(Mutex::new(Vec::new())),
handle: None,
}
StdoutSink { client }
}
}
impl Sink for StdoutSink {
fn start(&mut self) -> SinkResult<()> {
self.start_writer();
// TODO: Handle error
self.client.send(IpcPacket::StartPlayback).unwrap();
Ok(())
}
fn stop(&mut self) -> SinkResult<()> {
// Stop the writer thread
// This is done before pausing songbird, because else the writer thread
// might hang on writing to stdout
if let Err(why) = self.stop_writer() {
error!("Failed to stop stdout writer: {:?}", why);
} else {
trace!("Stopped stdout writer");
}
// Stop songbird's playback
self.client.send(IpcPacket::StopPlayback).unwrap();
@ -140,7 +45,11 @@ impl Sink for StdoutSink {
&samples_f32,
)
.unwrap();
self.write_bytes(resampled.as_bytes())?;
let samples_i16 =
&converter.f64_to_s16(&resampled.iter().map(|v| *v as f64).collect::<Vec<f64>>());
self.write_bytes(samples_i16.as_bytes())?;
}
Ok(())
@ -149,18 +58,7 @@ impl Sink for StdoutSink {
impl SinkAsBytes for StdoutSink {
fn write_bytes(&mut self, data: &[u8]) -> SinkResult<()> {
let get_buffer_len = || {
let buffer = self.buffer.lock().unwrap();
buffer.len()
};
while get_buffer_len() > BUFFER_SIZE * 2 {
std::thread::sleep(Duration::from_millis(15));
}
let mut buffer = self.buffer.lock().unwrap();
buffer.extend_from_slice(data);
std::io::stdout().write_all(data).unwrap();
Ok(())
}

View File

@ -1,3 +1,4 @@
use log::trace;
use serenity::{
builder::CreateApplicationCommand,
model::prelude::interaction::application_command::ApplicationCommandInteraction,
@ -45,7 +46,7 @@ pub fn run(ctx: Context, command: ApplicationCommandInteraction) -> CommandOutpu
let mut session_manager = data.get::<SessionManager>().unwrap().clone();
// Check if another session is already active in this server
let session_opt = session_manager.get_session(guild.id).await;
let mut session_opt = session_manager.get_session(guild.id).await;
if let Some(session) = &session_opt {
if let Some(owner) = session.get_owner().await {
@ -94,6 +95,17 @@ pub fn run(ctx: Context, command: ApplicationCommandInteraction) -> CommandOutpu
defer_message(&ctx, &command, false).await;
if let Some(session) = &session_opt {
trace!("{} != {}", session.get_channel_id(), channel_id);
if session.get_channel_id() != channel_id {
session.disconnect().await;
session_opt = None;
// Give serenity/songbird some time to register the disconnect
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
}
}
if let Some(session) = &session_opt {
if let Err(why) = session.update_owner(&ctx, command.user.id).await {
// Need to link first
@ -131,7 +143,13 @@ pub fn run(ctx: Context, command: ApplicationCommandInteraction) -> CommandOutpu
} else {
// Create the session, and handle potential errors
if let Err(why) = session_manager
.create_session(&ctx, guild.id, channel_id, command.user.id)
.create_session(
&ctx,
guild.id,
channel_id,
command.channel_id,
command.user.id,
)
.await
{
// Need to link first
@ -176,7 +194,7 @@ pub fn run(ctx: Context, command: ApplicationCommandInteraction) -> CommandOutpu
.icon_url("https://spoticord.com/static/image/speaker.png")
.description(format!("Come listen along in <#{}>", channel_id))
.footer("Spotify will automatically start playing on Spoticord")
.status(Status::Success)
.status(Status::Info)
.build(),
)
.await;

View File

@ -11,7 +11,10 @@ use serenity::{
use crate::{
bot::commands::{respond_message, CommandOutput},
session::manager::SessionManager,
utils::{embed::{EmbedBuilder, Status}, self},
utils::{
self,
embed::{EmbedBuilder, Status},
},
};
pub const NAME: &str = "playing";
@ -81,7 +84,11 @@ pub fn run(ctx: Context, command: ApplicationCommandInteraction) -> CommandOutpu
};
// Create title
let title = format!("{} - {}", pbi.get_artists().unwrap(), pbi.get_name().unwrap());
let title = format!(
"{} - {}",
pbi.get_artists().unwrap(),
pbi.get_name().unwrap()
);
// Create description
let mut description = String::new();
@ -100,7 +107,11 @@ pub fn run(ctx: Context, command: ApplicationCommandInteraction) -> CommandOutpu
}
description.push_str("\n:alarm_clock: ");
description.push_str(&format!("{} / {}", utils::time_to_str(position / 1000), utils::time_to_str(pbi.duration_ms / 1000)));
description.push_str(&format!(
"{} / {}",
utils::time_to_str(position / 1000),
utils::time_to_str(pbi.duration_ms / 1000)
));
// Get owner of session
let owner = match utils::discord::get_user(&ctx, owner).await {
@ -116,7 +127,10 @@ pub fn run(ctx: Context, command: ApplicationCommandInteraction) -> CommandOutpu
&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))
.description(format!(
"Could not find user with id {}\nThis is an issue with the bot!",
owner
))
.status(Status::Error)
.build(),
true,
@ -136,23 +150,20 @@ pub fn run(ctx: Context, command: ApplicationCommandInteraction) -> CommandOutpu
.kind(InteractionResponseType::ChannelMessageWithSource)
.interaction_response_data(|message| {
message
.embed(|embed|
embed
.author(|author|
author
.embed(|embed| embed
.author(|author| author
.name("Currently Playing")
.icon_url("https://www.freepnglogos.com/uploads/spotify-logo-png/file-spotify-logo-png-4.png")
)
.title(title)
.url(format!("https://open.spotify.com/{}/{}", audio_type, spotify_id.to_base62().unwrap()))
.description(description)
.footer(|footer|
footer
.footer(|footer| footer
.text(&owner.name)
.icon_url(owner.face())
)
.thumbnail(&thumbnail)
.color(Status::Success as u64)
.color(Status::Info as u64)
)
})
})

View File

@ -1,3 +1,7 @@
pub const VERSION: &str = env!("CARGO_PKG_VERSION");
pub const MOTD: &str = "OPEN BETA (v2)";
/// The time it takes for Spoticord to disconnect when no music is being played
pub const DISCONNECT_TIME: u64 = 5 * 60;
// pub const MOTD: &str = "some good 'ol music";

View File

@ -4,14 +4,16 @@ use dotenv::dotenv;
use log::*;
use serenity::{framework::StandardFramework, prelude::GatewayIntents, Client};
use songbird::SerenityInit;
use std::{env, process::exit};
use tokio::signal::unix::SignalKind;
use std::{any::Any, env, process::exit};
use crate::{
bot::commands::CommandManager, database::Database, session::manager::SessionManager,
stats::StatsManager,
};
#[cfg(unix)]
use tokio::signal::unix::SignalKind;
mod audio;
mod bot;
mod consts;
@ -39,14 +41,6 @@ async fn main() {
env_logger::init();
let orig_hook = std::panic::take_hook();
std::panic::set_hook(Box::new(move |panic_info| {
error!("Panic: {}", panic_info);
orig_hook(panic_info);
std::process::exit(1);
}));
let args: Vec<String> = env::args().collect();
if args.len() > 2 {
@ -74,7 +68,7 @@ async fn main() {
warn!("No .env file found, expecting all necessary environment variables");
}
let token = env::var("TOKEN").expect("a token in the environment");
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");
let kv_url = env::var("KV_URL").expect("a redis URL in the environment");
@ -104,7 +98,12 @@ async fn main() {
let cache = client.cache_and_http.cache.clone();
#[cfg(unix)]
let mut sigterm = tokio::signal::unix::signal(SignalKind::terminate()).unwrap();
let mut term: Option<Box<dyn Any + Send>> = Some(Box::new(
tokio::signal::unix::signal(SignalKind::terminate()).unwrap(),
));
#[cfg(not(unix))]
let term: Option<Box<dyn Any + Send>> = None;
// Background tasks
tokio::spawn(async move {
@ -134,7 +133,18 @@ async fn main() {
break;
}
_ = sigterm.recv() => {
_ = async {
#[cfg(unix)]
match term {
Some(ref mut term) => {
let term = term.downcast_mut::<tokio::signal::unix::Signal>().unwrap();
term.recv().await
}
_ => None
}
}, if term.is_some() => {
info!("Received terminate signal, shutting down...");
shard_manager.lock().await.shutdown_all().await;

View File

@ -66,7 +66,10 @@ impl SpoticordPlayer {
self.session = Some(session.clone());
// Volume mixer
let mixer = (mixer::find(Some("softvol")).unwrap())(MixerConfig::default());
let mixer = (mixer::find(Some("softvol")).unwrap())(MixerConfig {
volume_ctrl: librespot::playback::config::VolumeCtrl::Linear,
..MixerConfig::default()
});
let client = self.client.clone();

View File

@ -47,10 +47,12 @@ impl SessionManager {
ctx: &Context,
guild_id: GuildId,
channel_id: ChannelId,
text_channel_id: ChannelId,
owner_id: UserId,
) -> Result<(), SessionCreateError> {
// Create session first to make sure locks are kept for as little time as possible
let session = SpoticordSession::new(ctx, guild_id, channel_id, owner_id).await?;
let session =
SpoticordSession::new(ctx, guild_id, channel_id, text_channel_id, owner_id).await?;
let mut sessions = self.sessions.write().await;
let mut owner_map = self.owner_map.write().await;

View File

@ -1,20 +1,25 @@
use self::manager::{SessionCreateError, SessionManager};
use self::{
manager::{SessionCreateError, SessionManager},
pbi::PlaybackInfo,
};
use crate::{
consts::DISCONNECT_TIME,
database::{Database, DatabaseError},
ipc::{self, packet::IpcPacket, Client},
utils::{self, spotify},
utils::{embed::Status, spotify},
};
use ipc_channel::ipc::{IpcError, TryRecvError};
use librespot::core::spotify_id::{SpotifyAudioType, SpotifyId};
use log::*;
use serenity::{
async_trait,
http::Http,
model::prelude::{ChannelId, GuildId, UserId},
prelude::{Context, RwLock},
};
use songbird::{
create_player,
input::{children_to_reader, Input},
input::{children_to_reader, Codec, Container, Input},
tracks::TrackHandle,
Call, Event, EventContext, EventHandler,
};
@ -26,114 +31,16 @@ use std::{
use tokio::sync::Mutex;
pub mod manager;
#[derive(Clone)]
pub struct PlaybackInfo {
last_updated: u128,
position_ms: u32,
pub track: Option<spotify::Track>,
pub episode: Option<spotify::Episode>,
pub spotify_id: Option<SpotifyId>,
pub duration_ms: u32,
pub is_playing: bool,
}
impl PlaybackInfo {
fn new(duration_ms: u32, position_ms: u32, is_playing: bool) -> Self {
Self {
last_updated: utils::get_time_ms(),
track: None,
episode: None,
spotify_id: None,
duration_ms,
position_ms,
is_playing,
}
}
// Update position, duration and playback state
async fn update_pos_dur(&mut self, position_ms: u32, duration_ms: u32, is_playing: bool) {
self.position_ms = position_ms;
self.duration_ms = duration_ms;
self.is_playing = is_playing;
self.last_updated = utils::get_time_ms();
}
// Update spotify id, track and episode
fn update_track_episode(
&mut self,
spotify_id: SpotifyId,
track: Option<spotify::Track>,
episode: Option<spotify::Episode>,
) {
self.spotify_id = Some(spotify_id);
self.track = track;
self.episode = episode;
}
pub fn get_position(&self) -> u32 {
if self.is_playing {
let now = utils::get_time_ms();
let diff = now - self.last_updated;
self.position_ms + diff as u32
} else {
self.position_ms
}
}
pub fn get_name(&self) -> Option<String> {
if let Some(track) = &self.track {
Some(track.name.clone())
} else if let Some(episode) = &self.episode {
Some(episode.name.clone())
} else {
None
}
}
pub fn get_artists(&self) -> Option<String> {
if let Some(track) = &self.track {
Some(
track
.artists
.iter()
.map(|a| a.name.clone())
.collect::<Vec<String>>()
.join(", "),
)
} else if let Some(episode) = &self.episode {
Some(episode.show.name.clone())
} else {
None
}
}
pub fn get_thumbnail_url(&self) -> Option<String> {
if let Some(track) = &self.track {
let mut images = track.album.images.clone();
images.sort_by(|a, b| b.width.cmp(&a.width));
Some(images.get(0).unwrap().url.clone())
} else if let Some(episode) = &self.episode {
let mut images = episode.show.images.clone();
images.sort_by(|a, b| b.width.cmp(&a.width));
Some(images.get(0).unwrap().url.clone())
} else {
None
}
}
}
mod pbi;
#[derive(Clone)]
pub struct SpoticordSession {
owner: Arc<RwLock<Option<UserId>>>,
guild_id: GuildId,
channel_id: ChannelId,
text_channel_id: ChannelId,
http: Arc<Http>,
session_manager: SessionManager,
@ -142,6 +49,8 @@ pub struct SpoticordSession {
playback_info: Arc<RwLock<Option<PlaybackInfo>>>,
disconnect_handle: Arc<Mutex<Option<tokio::task::JoinHandle<()>>>>,
client: Client,
}
@ -150,6 +59,7 @@ impl SpoticordSession {
ctx: &Context,
guild_id: GuildId,
channel_id: ChannelId,
text_channel_id: ChannelId,
owner_id: UserId,
) -> Result<SpoticordSession, SessionCreateError> {
// Get the Spotify token of the owner
@ -227,7 +137,8 @@ impl SpoticordSession {
let reader = children_to_reader::<f32>(vec![child]);
// Create track (paused, fixes audio glitches)
let (mut track, track_handle) = create_player(Input::float_pcm(true, reader));
let (mut track, track_handle) =
create_player(Input::new(true, reader, Codec::Pcm, Container::Raw, None));
track.pause();
// Set call audio to track
@ -237,10 +148,13 @@ impl SpoticordSession {
owner: Arc::new(RwLock::new(Some(owner_id.clone()))),
guild_id,
channel_id,
text_channel_id,
http: ctx.http.clone(),
session_manager: session_manager.clone(),
call: call.clone(),
track: track_handle.clone(),
playback_info: Arc::new(RwLock::new(None)),
disconnect_handle: Arc::new(Mutex::new(None)),
client: client.clone(),
};
@ -336,6 +250,8 @@ impl SpoticordSession {
}
IpcPacket::Paused(track, position_ms, duration_ms) => {
ipc_instance.start_disconnect_timer().await;
// Convert to SpotifyId
let track_id = SpotifyId::from_uri(&track).unwrap();
@ -527,10 +443,8 @@ impl SpoticordSession {
*playback_info = None;
}
// Disconnect from voice channel and remove session from manager
pub async fn disconnect(&self) {
info!("Disconnecting from voice channel {}", self.channel_id);
/// Internal version of disconnect, which does not abort the disconnect timer
async fn disconnect_no_abort(&self) {
self
.session_manager
.clone()
@ -547,6 +461,48 @@ impl SpoticordSession {
}
}
// Disconnect from voice channel and remove session from manager
pub async fn disconnect(&self) {
info!("Disconnecting from voice channel {}", self.channel_id);
self.disconnect_no_abort().await;
// Stop the disconnect timer, if one is running
let mut dc_handle = self.disconnect_handle.lock().await;
if let Some(handle) = dc_handle.take() {
handle.abort();
}
}
/// Disconnect from voice channel with a message
pub async fn disconnect_with_message(&self, content: &str) {
self.disconnect_no_abort().await;
if let Err(why) = self
.text_channel_id
.send_message(&self.http, |message| {
message.embed(|embed| {
embed.title("Disconnected from voice channel");
embed.description(content);
embed.color(Status::Warning as u64);
embed
})
})
.await
{
error!("Failed to send disconnect message: {:?}", why);
}
// Stop the disconnect timer, if one is running
let mut dc_handle = self.disconnect_handle.lock().await;
if let Some(handle) = dc_handle.take() {
handle.abort();
}
}
// Update playback info (duration, position, playing state)
async fn update_playback(&self, duration_ms: u32, position_ms: u32, playing: bool) -> bool {
let is_none = {
@ -572,6 +528,54 @@ impl SpoticordSession {
is_none
}
/// Start the disconnect timer, which will disconnect the bot from the voice channel after a
/// certain amount of time
async fn start_disconnect_timer(&self) {
let pbi = self.playback_info.clone();
let instance = self.clone();
let mut handle = self.disconnect_handle.lock().await;
// Abort the previous timer, if one is running
if let Some(handle) = handle.take() {
handle.abort();
}
*handle = Some(tokio::spawn(async move {
let mut timer = tokio::time::interval(Duration::from_secs(DISCONNECT_TIME));
// Ignore first (immediate) tick
timer.tick().await;
loop {
timer.tick().await;
// Make sure this task has not been aborted, if it has this will automatically stop execution.
tokio::task::yield_now().await;
let is_playing = {
let pbi = pbi.read().await;
if let Some(pbi) = &*pbi {
pbi.is_playing
} else {
false
}
};
if !is_playing {
info!("Player is not playing, disconnecting");
instance
.disconnect_with_message(
"The player has been inactive for too long, and has been disconnected.",
)
.await;
break;
}
}
}));
}
// Get the playback info for the current track
pub async fn get_playback_info(&self) -> Option<PlaybackInfo> {
self.playback_info.read().await.clone()

110
src/session/pbi.rs 100644
View File

@ -0,0 +1,110 @@
use librespot::core::spotify_id::SpotifyId;
use crate::utils::{self, spotify};
#[derive(Clone)]
pub struct PlaybackInfo {
last_updated: u128,
position_ms: u32,
pub track: Option<spotify::Track>,
pub episode: Option<spotify::Episode>,
pub spotify_id: Option<SpotifyId>,
pub duration_ms: u32,
pub is_playing: bool,
}
impl PlaybackInfo {
/// Create a new instance of PlaybackInfo
pub fn new(duration_ms: u32, position_ms: u32, is_playing: bool) -> Self {
Self {
last_updated: utils::get_time_ms(),
track: None,
episode: None,
spotify_id: None,
duration_ms,
position_ms,
is_playing,
}
}
/// Update position, duration and playback state
pub async fn update_pos_dur(&mut self, position_ms: u32, duration_ms: u32, is_playing: bool) {
self.position_ms = position_ms;
self.duration_ms = duration_ms;
self.is_playing = is_playing;
self.last_updated = utils::get_time_ms();
}
/// Update spotify id, track and episode
pub fn update_track_episode(
&mut self,
spotify_id: SpotifyId,
track: Option<spotify::Track>,
episode: Option<spotify::Episode>,
) {
self.spotify_id = Some(spotify_id);
self.track = track;
self.episode = episode;
}
/// Get the current playback position
pub fn get_position(&self) -> u32 {
if self.is_playing {
let now = utils::get_time_ms();
let diff = now - self.last_updated;
self.position_ms + diff as u32
} else {
self.position_ms
}
}
/// Get the name of the track or episode
pub fn get_name(&self) -> Option<String> {
if let Some(track) = &self.track {
Some(track.name.clone())
} else if let Some(episode) = &self.episode {
Some(episode.name.clone())
} else {
None
}
}
/// Get the artist(s) or show name of the current track
pub fn get_artists(&self) -> Option<String> {
if let Some(track) = &self.track {
Some(
track
.artists
.iter()
.map(|a| a.name.clone())
.collect::<Vec<String>>()
.join(", "),
)
} else if let Some(episode) = &self.episode {
Some(episode.show.name.clone())
} else {
None
}
}
/// Get the album art url
pub fn get_thumbnail_url(&self) -> Option<String> {
if let Some(track) = &self.track {
let mut images = track.album.images.clone();
images.sort_by(|a, b| b.width.cmp(&a.width));
Some(images.get(0).unwrap().url.clone())
} else if let Some(episode) = &self.episode {
let mut images = episode.show.images.clone();
images.sort_by(|a, b| b.width.cmp(&a.width));
Some(images.get(0).unwrap().url.clone())
} else {
None
}
}
}

View File

@ -1,6 +1,5 @@
use serenity::builder::CreateEmbed;
#[allow(dead_code)]
pub enum Status {
Info = 0x0773D6,
Success = 0x3BD65D,