Updates, bugfixes. You name it.
parent
9798a178d7
commit
b7c85455ce
|
@ -143,7 +143,13 @@ pub fn run(ctx: Context, command: ApplicationCommandInteraction) -> CommandOutpu
|
||||||
} else {
|
} else {
|
||||||
// Create the session, and handle potential errors
|
// Create the session, and handle potential errors
|
||||||
if let Err(why) = session_manager
|
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
|
.await
|
||||||
{
|
{
|
||||||
// Need to link first
|
// Need to link first
|
||||||
|
@ -188,7 +194,7 @@ pub fn run(ctx: Context, command: ApplicationCommandInteraction) -> CommandOutpu
|
||||||
.icon_url("https://spoticord.com/static/image/speaker.png")
|
.icon_url("https://spoticord.com/static/image/speaker.png")
|
||||||
.description(format!("Come listen along in <#{}>", channel_id))
|
.description(format!("Come listen along in <#{}>", channel_id))
|
||||||
.footer("Spotify will automatically start playing on Spoticord")
|
.footer("Spotify will automatically start playing on Spoticord")
|
||||||
.status(Status::Success)
|
.status(Status::Info)
|
||||||
.build(),
|
.build(),
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
|
|
|
@ -1,3 +1,7 @@
|
||||||
pub const VERSION: &str = env!("CARGO_PKG_VERSION");
|
pub const VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||||
pub const MOTD: &str = "OPEN BETA (v2)";
|
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";
|
// pub const MOTD: &str = "some good 'ol music";
|
||||||
|
|
|
@ -41,14 +41,6 @@ async fn main() {
|
||||||
|
|
||||||
env_logger::init();
|
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();
|
let args: Vec<String> = env::args().collect();
|
||||||
|
|
||||||
if args.len() > 2 {
|
if args.len() > 2 {
|
||||||
|
@ -143,6 +135,7 @@ async fn main() {
|
||||||
}
|
}
|
||||||
|
|
||||||
_ = async {
|
_ = async {
|
||||||
|
#[cfg(unix)]
|
||||||
match term {
|
match term {
|
||||||
Some(ref mut term) => {
|
Some(ref mut term) => {
|
||||||
let term = term.downcast_mut::<tokio::signal::unix::Signal>().unwrap();
|
let term = term.downcast_mut::<tokio::signal::unix::Signal>().unwrap();
|
||||||
|
|
|
@ -66,7 +66,10 @@ impl SpoticordPlayer {
|
||||||
self.session = Some(session.clone());
|
self.session = Some(session.clone());
|
||||||
|
|
||||||
// Volume mixer
|
// 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();
|
let client = self.client.clone();
|
||||||
|
|
|
@ -47,10 +47,12 @@ impl SessionManager {
|
||||||
ctx: &Context,
|
ctx: &Context,
|
||||||
guild_id: GuildId,
|
guild_id: GuildId,
|
||||||
channel_id: ChannelId,
|
channel_id: ChannelId,
|
||||||
|
text_channel_id: ChannelId,
|
||||||
owner_id: UserId,
|
owner_id: UserId,
|
||||||
) -> Result<(), SessionCreateError> {
|
) -> Result<(), SessionCreateError> {
|
||||||
// Create session first to make sure locks are kept for as little time as possible
|
// 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 sessions = self.sessions.write().await;
|
||||||
let mut owner_map = self.owner_map.write().await;
|
let mut owner_map = self.owner_map.write().await;
|
||||||
|
|
|
@ -1,14 +1,19 @@
|
||||||
use self::manager::{SessionCreateError, SessionManager};
|
use self::{
|
||||||
|
manager::{SessionCreateError, SessionManager},
|
||||||
|
pbi::PlaybackInfo,
|
||||||
|
};
|
||||||
use crate::{
|
use crate::{
|
||||||
|
consts::DISCONNECT_TIME,
|
||||||
database::{Database, DatabaseError},
|
database::{Database, DatabaseError},
|
||||||
ipc::{self, packet::IpcPacket, Client},
|
ipc::{self, packet::IpcPacket, Client},
|
||||||
utils::{self, spotify},
|
utils::{embed::Status, spotify},
|
||||||
};
|
};
|
||||||
use ipc_channel::ipc::{IpcError, TryRecvError};
|
use ipc_channel::ipc::{IpcError, TryRecvError};
|
||||||
use librespot::core::spotify_id::{SpotifyAudioType, SpotifyId};
|
use librespot::core::spotify_id::{SpotifyAudioType, SpotifyId};
|
||||||
use log::*;
|
use log::*;
|
||||||
use serenity::{
|
use serenity::{
|
||||||
async_trait,
|
async_trait,
|
||||||
|
http::Http,
|
||||||
model::prelude::{ChannelId, GuildId, UserId},
|
model::prelude::{ChannelId, GuildId, UserId},
|
||||||
prelude::{Context, RwLock},
|
prelude::{Context, RwLock},
|
||||||
};
|
};
|
||||||
|
@ -26,114 +31,16 @@ use std::{
|
||||||
use tokio::sync::Mutex;
|
use tokio::sync::Mutex;
|
||||||
|
|
||||||
pub mod manager;
|
pub mod manager;
|
||||||
|
mod pbi;
|
||||||
#[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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct SpoticordSession {
|
pub struct SpoticordSession {
|
||||||
owner: Arc<RwLock<Option<UserId>>>,
|
owner: Arc<RwLock<Option<UserId>>>,
|
||||||
guild_id: GuildId,
|
guild_id: GuildId,
|
||||||
channel_id: ChannelId,
|
channel_id: ChannelId,
|
||||||
|
text_channel_id: ChannelId,
|
||||||
|
|
||||||
|
http: Arc<Http>,
|
||||||
|
|
||||||
session_manager: SessionManager,
|
session_manager: SessionManager,
|
||||||
|
|
||||||
|
@ -150,6 +57,7 @@ impl SpoticordSession {
|
||||||
ctx: &Context,
|
ctx: &Context,
|
||||||
guild_id: GuildId,
|
guild_id: GuildId,
|
||||||
channel_id: ChannelId,
|
channel_id: ChannelId,
|
||||||
|
text_channel_id: ChannelId,
|
||||||
owner_id: UserId,
|
owner_id: UserId,
|
||||||
) -> Result<SpoticordSession, SessionCreateError> {
|
) -> Result<SpoticordSession, SessionCreateError> {
|
||||||
// Get the Spotify token of the owner
|
// Get the Spotify token of the owner
|
||||||
|
@ -237,6 +145,8 @@ impl SpoticordSession {
|
||||||
owner: Arc::new(RwLock::new(Some(owner_id.clone()))),
|
owner: Arc::new(RwLock::new(Some(owner_id.clone()))),
|
||||||
guild_id,
|
guild_id,
|
||||||
channel_id,
|
channel_id,
|
||||||
|
text_channel_id,
|
||||||
|
http: ctx.http.clone(),
|
||||||
session_manager: session_manager.clone(),
|
session_manager: session_manager.clone(),
|
||||||
call: call.clone(),
|
call: call.clone(),
|
||||||
track: track_handle.clone(),
|
track: track_handle.clone(),
|
||||||
|
@ -336,6 +246,8 @@ impl SpoticordSession {
|
||||||
}
|
}
|
||||||
|
|
||||||
IpcPacket::Paused(track, position_ms, duration_ms) => {
|
IpcPacket::Paused(track, position_ms, duration_ms) => {
|
||||||
|
ipc_instance.start_disconnect_timer().await;
|
||||||
|
|
||||||
// Convert to SpotifyId
|
// Convert to SpotifyId
|
||||||
let track_id = SpotifyId::from_uri(&track).unwrap();
|
let track_id = SpotifyId::from_uri(&track).unwrap();
|
||||||
|
|
||||||
|
@ -547,6 +459,28 @@ impl SpoticordSession {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Disconnect from voice channel with a message
|
||||||
|
pub async fn disconnect_with_message(&self, content: &str) {
|
||||||
|
self.disconnect().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
|
||||||
|
.map(|_| ())
|
||||||
|
{
|
||||||
|
error!("Failed to send disconnect message: {:?}", why);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Update playback info (duration, position, playing state)
|
// Update playback info (duration, position, playing state)
|
||||||
async fn update_playback(&self, duration_ms: u32, position_ms: u32, playing: bool) -> bool {
|
async fn update_playback(&self, duration_ms: u32, position_ms: u32, playing: bool) -> bool {
|
||||||
let is_none = {
|
let is_none = {
|
||||||
|
@ -572,6 +506,44 @@ impl SpoticordSession {
|
||||||
is_none
|
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();
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
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
|
// Get the playback info for the current track
|
||||||
pub async fn get_playback_info(&self) -> Option<PlaybackInfo> {
|
pub async fn get_playback_info(&self) -> Option<PlaybackInfo> {
|
||||||
self.playback_info.read().await.clone()
|
self.playback_info.read().await.clone()
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,6 +1,5 @@
|
||||||
use serenity::builder::CreateEmbed;
|
use serenity::builder::CreateEmbed;
|
||||||
|
|
||||||
#[allow(dead_code)]
|
|
||||||
pub enum Status {
|
pub enum Status {
|
||||||
Info = 0x0773D6,
|
Info = 0x0773D6,
|
||||||
Success = 0x3BD65D,
|
Success = 0x3BD65D,
|
||||||
|
|
Loading…
Reference in New Issue