spoticord/src/session/mod.rs

623 lines
17 KiB
Rust

use self::{
manager::{SessionCreateError, SessionManager},
pbi::PlaybackInfo,
};
use crate::{
consts::DISCONNECT_TIME,
database::{Database, DatabaseError},
ipc::{self, packet::IpcPacket, Client},
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},
tracks::TrackHandle,
Call, Event, EventContext, EventHandler,
};
use std::{
process::{Command, Stdio},
sync::Arc,
time::Duration,
};
use tokio::sync::Mutex;
pub mod manager;
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,
call: Arc<Mutex<Call>>,
track: TrackHandle,
playback_info: Arc<RwLock<Option<PlaybackInfo>>>,
disconnect_handle: Arc<Mutex<Option<tokio::task::JoinHandle<()>>>>,
client: Client,
}
impl SpoticordSession {
pub async fn new(
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
let data = ctx.data.read().await;
let database = data.get::<Database>().unwrap();
let session_manager = data.get::<SessionManager>().unwrap().clone();
let token = match database.get_access_token(owner_id.to_string()).await {
Ok(token) => token,
Err(why) => {
if let DatabaseError::InvalidStatusCode(code) = why {
if code == 404 {
return Err(SessionCreateError::NoSpotifyError);
}
}
return Err(SessionCreateError::DatabaseError);
}
};
let user = match database.get_user(owner_id.to_string()).await {
Ok(user) => user,
Err(why) => {
error!("Failed to get user: {:?}", why);
return Err(SessionCreateError::DatabaseError);
}
};
// Create IPC oneshot server
let (server, tx_name, rx_name) = match ipc::Server::create() {
Ok(server) => server,
Err(why) => {
error!("Failed to create IPC server: {:?}", why);
return Err(SessionCreateError::ForkError);
}
};
// Join the voice channel
let songbird = songbird::get(ctx).await.unwrap().clone();
let (call, result) = songbird.join(guild_id, channel_id).await;
if let Err(why) = result {
error!("Error joining voice channel: {:?}", why);
return Err(SessionCreateError::JoinError(channel_id, guild_id));
}
let mut call_mut = call.lock().await;
// Spawn player process
let child = match Command::new(std::env::current_exe().unwrap())
.args(["--player", &tx_name, &rx_name])
.stdout(Stdio::piped())
.stderr(Stdio::inherit())
.spawn()
{
Ok(child) => child,
Err(why) => {
error!("Failed to start player process: {:?}", why);
return Err(SessionCreateError::ForkError);
}
};
// Establish bi-directional IPC channel
let client = match server.accept() {
Ok(client) => client,
Err(why) => {
error!("Failed to accept IPC connection: {:?}", why);
return Err(SessionCreateError::ForkError);
}
};
// Pipe player audio to the voice channel
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));
track.pause();
// Set call audio to track
call_mut.play_only(track);
let instance = Self {
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(),
};
// Clone variables for use in the IPC handler
let ipc_track = track_handle.clone();
let ipc_client = client.clone();
let ipc_context = ctx.clone();
let mut ipc_instance = instance.clone();
// Handle IPC packets
// This will automatically quit once the IPC connection is closed
tokio::spawn(async move {
let check_result = |result| {
if let Err(why) = result {
error!("Failed to issue track command: {:?}", why);
}
};
loop {
// Required for IpcPacket::TrackChange to work
tokio::task::yield_now().await;
let msg = match ipc_client.try_recv() {
Ok(msg) => msg,
Err(why) => {
if let TryRecvError::Empty = why {
// No message, wait a bit and try again
tokio::time::sleep(Duration::from_millis(25)).await;
continue;
} else if let TryRecvError::IpcError(why) = &why {
if let IpcError::Disconnected = why {
break;
}
}
error!("Failed to receive IPC message: {:?}", why);
break;
}
};
trace!("Received IPC message: {:?}", msg);
match msg {
// Sink requests playback to start/resume
IpcPacket::StartPlayback => {
check_result(ipc_track.play());
}
// Sink requests playback to pause
IpcPacket::StopPlayback => {
check_result(ipc_track.pause());
}
// A new track has been set by the player
IpcPacket::TrackChange(track) => {
// Convert to SpotifyId
let track_id = SpotifyId::from_uri(&track).unwrap();
let mut instance = ipc_instance.clone();
let context = ipc_context.clone();
tokio::spawn(async move {
if let Err(why) = instance.update_track(&context, &owner_id, track_id).await {
error!("Failed to update track: {:?}", why);
instance.player_stopped().await;
}
});
}
// The player has started playing a track
IpcPacket::Playing(track, position_ms, duration_ms) => {
// Convert to SpotifyId
let track_id = SpotifyId::from_uri(&track).unwrap();
let was_none = ipc_instance
.update_playback(duration_ms, position_ms, true)
.await;
if was_none {
// Stop player if update track fails
if let Err(why) = ipc_instance
.update_track(&ipc_context, &owner_id, track_id)
.await
{
error!("Failed to update track: {:?}", why);
ipc_instance.player_stopped().await;
return;
}
}
}
IpcPacket::Paused(track, position_ms, duration_ms) => {
ipc_instance.start_disconnect_timer().await;
// Convert to SpotifyId
let track_id = SpotifyId::from_uri(&track).unwrap();
let was_none = ipc_instance
.update_playback(duration_ms, position_ms, false)
.await;
if was_none {
// Stop player if update track fails
if let Err(why) = ipc_instance
.update_track(&ipc_context, &owner_id, track_id)
.await
{
error!("Failed to update track: {:?}", why);
ipc_instance.player_stopped().await;
return;
}
}
}
IpcPacket::Stopped => {
ipc_instance.player_stopped().await;
}
// Ignore other packets
_ => {}
}
}
});
// Set up events
call_mut.add_global_event(
songbird::Event::Core(songbird::CoreEvent::DriverDisconnect),
instance.clone(),
);
call_mut.add_global_event(
songbird::Event::Core(songbird::CoreEvent::ClientDisconnect),
instance.clone(),
);
// Inform the player process to connect to Spotify
if let Err(why) = client.send(IpcPacket::Connect(token, user.device_name)) {
error!("Failed to send IpcPacket::Connect packet: {:?}", why);
}
Ok(instance)
}
pub async fn update_owner(
&self,
ctx: &Context,
owner_id: UserId,
) -> Result<(), SessionCreateError> {
// Get the Spotify token of the owner
let data = ctx.data.read().await;
let database = data.get::<Database>().unwrap();
let mut session_manager = data.get::<SessionManager>().unwrap().clone();
let token = match database.get_access_token(owner_id.to_string()).await {
Ok(token) => token,
Err(why) => {
if let DatabaseError::InvalidStatusCode(code) = why {
if code == 404 {
return Err(SessionCreateError::NoSpotifyError);
}
}
return Err(SessionCreateError::DatabaseError);
}
};
let user = match database.get_user(owner_id.to_string()).await {
Ok(user) => user,
Err(why) => {
error!("Failed to get user: {:?}", why);
return Err(SessionCreateError::DatabaseError);
}
};
{
let mut owner = self.owner.write().await;
*owner = Some(owner_id);
}
session_manager.set_owner(owner_id, self.guild_id).await;
// Inform the player process to connect to Spotify
if let Err(why) = self
.client
.send(IpcPacket::Connect(token, user.device_name))
{
error!("Failed to send IpcPacket::Connect packet: {:?}", why);
}
Ok(())
}
// Update current track
async fn update_track(
&self,
ctx: &Context,
owner_id: &UserId,
spotify_id: SpotifyId,
) -> Result<(), String> {
let should_update = {
let pbi = self.playback_info.read().await;
if let Some(pbi) = &*pbi {
pbi.spotify_id.is_none() || pbi.spotify_id.unwrap() != spotify_id
} else {
false
}
};
if !should_update {
return Ok(());
}
let data = ctx.data.read().await;
let database = data.get::<Database>().unwrap();
let token = match database.get_access_token(&owner_id.to_string()).await {
Ok(token) => token,
Err(why) => {
error!("Failed to get access token: {:?}", why);
return Err("Failed to get access token".to_string());
}
};
let mut track: Option<spotify::Track> = None;
let mut episode: Option<spotify::Episode> = None;
if spotify_id.audio_type == SpotifyAudioType::Track {
let track_info = match spotify::get_track_info(&token, spotify_id).await {
Ok(track) => track,
Err(why) => {
error!("Failed to get track info: {:?}", why);
return Err("Failed to get track info".to_string());
}
};
trace!("Received track info: {:?}", track_info);
track = Some(track_info);
} else if spotify_id.audio_type == SpotifyAudioType::Podcast {
let episode_info = match spotify::get_episode_info(&token, spotify_id).await {
Ok(episode) => episode,
Err(why) => {
error!("Failed to get episode info: {:?}", why);
return Err("Failed to get episode info".to_string());
}
};
trace!("Received episode info: {:?}", episode_info);
episode = Some(episode_info);
}
let mut pbi = self.playback_info.write().await;
if let Some(pbi) = &mut *pbi {
pbi.update_track_episode(spotify_id, track, episode);
}
Ok(())
}
/// Called when the player must stop, but not leave the call
async fn player_stopped(&mut self) {
if let Err(why) = self.track.pause() {
error!("Failed to pause track: {:?}", why);
}
// Disconnect from Spotify
if let Err(why) = self.client.send(IpcPacket::Disconnect) {
error!("Failed to send disconnect packet: {:?}", why);
}
// Clear owner
let mut owner = self.owner.write().await;
if let Some(owner_id) = owner.take() {
self.session_manager.remove_owner(owner_id).await;
}
// Clear playback info
let mut playback_info = self.playback_info.write().await;
*playback_info = None;
}
/// Internal version of disconnect, which does not abort the disconnect timer
async fn disconnect_no_abort(&self) {
self
.session_manager
.clone()
.remove_session(self.guild_id)
.await;
let mut call = self.call.lock().await;
self.track.stop().unwrap_or(());
call.remove_all_global_events();
if let Err(why) = call.leave().await {
error!("Failed to leave voice channel: {:?}", why);
}
}
// 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 = {
let pbi = self.playback_info.read().await;
pbi.is_none()
};
if is_none {
let mut pbi = self.playback_info.write().await;
*pbi = Some(PlaybackInfo::new(duration_ms, position_ms, playing));
} else {
let mut pbi = self.playback_info.write().await;
// Update position, duration and playback state
pbi
.as_mut()
.unwrap()
.update_pos_dur(position_ms, duration_ms, playing)
.await;
};
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;
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()
}
// Get the current owner of this session
pub async fn get_owner(&self) -> Option<UserId> {
let owner = self.owner.read().await;
*owner
}
// Get the server id this session is playing in
pub fn get_guild_id(&self) -> GuildId {
self.guild_id
}
// Get the channel id this session is playing in
pub fn get_channel_id(&self) -> ChannelId {
self.channel_id
}
}
#[async_trait]
impl EventHandler for SpoticordSession {
async fn act(&self, ctx: &EventContext<'_>) -> Option<Event> {
match ctx {
EventContext::DriverDisconnect(_) => {
debug!("Driver disconnected, leaving voice channel");
self.disconnect().await;
}
EventContext::ClientDisconnect(who) => {
trace!("Client disconnected, {}", who.user_id.to_string());
if let Some(session) = self.session_manager.find(UserId(who.user_id.0)).await {
if session.get_guild_id() == self.guild_id && session.get_channel_id() == self.channel_id
{
// Clone because haha immutable references
self.clone().player_stopped().await;
}
}
}
_ => {}
}
return None;
}
}