From 2cebeb41ab3e825ab2fba2f58d0b24ca8910797f Mon Sep 17 00:00:00 2001 From: DaXcess Date: Tue, 19 Sep 2023 20:01:36 +0200 Subject: [PATCH] Less Spotify API calls, rearranged some player stuff --- Cargo.lock | 9 + Cargo.toml | 3 + src/bot/commands/music/playing.rs | 53 +---- src/consts.rs | 6 +- src/player/mod.rs | 349 ++++++++++++++++++++++++++---- src/session/mod.rs | 336 +++------------------------- src/session/pbi.rs | 130 +++++------ src/utils/spotify.rs | 135 +----------- 8 files changed, 440 insertions(+), 581 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b774a1e..7b2c40e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -94,6 +94,12 @@ dependencies = [ "libc", ] +[[package]] +name = "anyhow" +version = "1.0.75" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6" + [[package]] name = "arrayvec" version = "0.7.4" @@ -2394,10 +2400,13 @@ dependencies = [ name = "spoticord" version = "2.1.0" dependencies = [ + "anyhow", "dotenv", "env_logger 0.10.0", + "hex", "librespot", "log", + "protobuf", "redis", "reqwest", "samplerate", diff --git a/Cargo.toml b/Cargo.toml index 571af67..cced65f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,10 +12,13 @@ path = "src/main.rs" stats = ["redis"] [dependencies] +anyhow = "1.0.75" dotenv = "0.15.0" env_logger = "0.10.0" +hex = "0.4.3" 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" samplerate = "0.2.4" diff --git a/src/bot/commands/music/playing.rs b/src/bot/commands/music/playing.rs index f022575..9dbe5ad 100644 --- a/src/bot/commands/music/playing.rs +++ b/src/bot/commands/music/playing.rs @@ -1,6 +1,6 @@ use std::time::Duration; -use librespot::core::spotify_id::{SpotifyAudioType, SpotifyId}; +use librespot::core::spotify_id::SpotifyId; use log::error; use serenity::{ builder::{CreateApplicationCommand, CreateButton, CreateComponents, CreateEmbed}, @@ -82,15 +82,6 @@ pub fn command(ctx: Context, command: ApplicationCommandInteraction) -> CommandO } }; - let spotify_id = match pbi.spotify_id { - Some(spotify_id) => spotify_id, - None => { - not_playing.await; - - return; - } - }; - // Get owner of session let owner = match utils::discord::get_user(&ctx, owner).await { Some(user) => user, @@ -119,7 +110,7 @@ pub fn command(ctx: Context, command: ApplicationCommandInteraction) -> CommandO }; // Get metadata - let (title, description, audio_type, thumbnail) = get_metadata(spotify_id, &pbi); + let (title, description, thumbnail) = get_metadata(&pbi); if let Err(why) = command .create_interaction_response(&ctx.http, |response| { @@ -129,8 +120,8 @@ pub fn command(ctx: Context, command: ApplicationCommandInteraction) -> CommandO message .set_embed(build_playing_embed( title, - audio_type, - spotify_id, + pbi.get_type(), + pbi.spotify_id, description, owner, thumbnail, @@ -409,20 +400,7 @@ async fn update_embed(interaction: &mut MessageComponentInteraction, ctx: &Conte } }; - let spotify_id = match pbi.spotify_id { - Some(spotify_id) => spotify_id, - None => { - error_edit( - "Cannot change playback state", - "I'm currently not playing any music in this server", - ) - .await; - - return; - } - }; - - let (title, description, audio_type, thumbnail) = get_metadata(spotify_id, &pbi); + let (title, description, thumbnail) = get_metadata(&pbi); if let Err(why) = interaction .message @@ -430,8 +408,8 @@ async fn update_embed(interaction: &mut MessageComponentInteraction, ctx: &Conte message .set_embed(build_playing_embed( title, - audio_type, - spotify_id, + pbi.get_type(), + pbi.spotify_id, description, owner, thumbnail, @@ -477,20 +455,9 @@ fn build_playing_embed( embed } -fn get_metadata(spotify_id: SpotifyId, pbi: &PlaybackInfo) -> (String, String, String, String) { - // Get audio type - let audio_type = if spotify_id.audio_type == SpotifyAudioType::Track { - "track" - } else { - "episode" - }; - +fn get_metadata(pbi: &PlaybackInfo) -> (String, String, String) { // Create title - let title = format!( - "{} - {}", - pbi.get_artists().as_deref().unwrap_or("ID"), - pbi.get_name().as_deref().unwrap_or("ID") - ); + let title = format!("{} - {}", pbi.get_artists(), pbi.get_name()); // Create description let mut description = String::new(); @@ -518,5 +485,5 @@ fn get_metadata(spotify_id: SpotifyId, pbi: &PlaybackInfo) -> (String, String, S // Get the thumbnail image let thumbnail = pbi.get_thumbnail_url().expect("to contain a value"); - (title, description, audio_type.to_string(), thumbnail) + (title, description, thumbnail) } diff --git a/src/consts.rs b/src/consts.rs index de675d1..c050977 100644 --- a/src/consts.rs +++ b/src/consts.rs @@ -1,6 +1,10 @@ +#[cfg(not(debug_assertions))] pub const VERSION: &str = env!("CARGO_PKG_VERSION"); + +#[cfg(debug_assertions)] +pub const VERSION: &str = concat!(env!("CARGO_PKG_VERSION"), "-dev"); + 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; - diff --git a/src/player/mod.rs b/src/player/mod.rs index faa5539..220ea8e 100644 --- a/src/player/mod.rs +++ b/src/player/mod.rs @@ -1,8 +1,12 @@ +use std::{io::Write, sync::Arc}; + +use anyhow::{anyhow, Result}; use librespot::{ connect::spirc::Spirc, core::{ config::{ConnectConfig, SessionConfig}, session::Session, + spotify_id::{SpotifyAudioType, SpotifyId}, }, discovery::Credentials, playback::{ @@ -10,39 +14,52 @@ use librespot::{ mixer::{self, MixerConfig}, player::{Player as SpotifyPlayer, PlayerEvent}, }, + protocol::metadata::{Episode, Track}, +}; +use log::error; +use protobuf::Message; +use songbird::tracks::TrackHandle; +use tokio::sync::{ + broadcast::{Receiver, Sender}, + mpsc::UnboundedReceiver, + Mutex, }; -use tokio::sync::mpsc::UnboundedReceiver; use crate::{ audio::{stream::Stream, SinkEvent, StreamSink}, librespot_ext::discovery::CredentialsExt, + session::pbi::{CurrentTrack, PlaybackInfo}, utils, }; +enum Event { + Player(PlayerEvent), + Sink(SinkEvent), + Command(PlayerCommand), +} + +#[derive(Clone)] +enum PlayerCommand { + Next, + Previous, + Pause, + Play, +} + +#[derive(Clone)] pub struct Player { - stream: Stream, - session: Option, + tx: Sender, + + pbi: Arc>>, } impl Player { - pub fn create() -> Self { - Self { - stream: Stream::new(), - session: None, - } - } - - pub async fn start( - &mut self, + pub async fn create( + stream: Stream, token: &str, device_name: &str, - ) -> Result< - ( - Spirc, - (UnboundedReceiver, UnboundedReceiver), - ), - Box, - > { + track: TrackHandle, + ) -> Result { let username = utils::spotify::get_username(token).await?; let player_config = PlayerConfig { @@ -52,12 +69,6 @@ impl Player { let credentials = Credentials::with_token(username, token); - // Shutdown old session (cannot be done in the stop function) - if let Some(session) = self.session.take() { - session.shutdown() - } - - // Connect the session let (session, _) = Session::connect( SessionConfig { ap_port: Some(9999), // Force the use of ap.spotify.com, which has the lowest latency @@ -68,27 +79,26 @@ impl Player { false, ) .await?; - self.session = Some(session.clone()); let mixer = (mixer::find(Some("softvol")).expect("to exist"))(MixerConfig { volume_ctrl: VolumeCtrl::Linear, ..Default::default() }); - let stream = self.get_stream(); - let (tx, rx) = tokio::sync::mpsc::unbounded_channel(); - let (player, receiver) = SpotifyPlayer::new( - player_config, - session.clone(), - mixer.get_soft_volume(), - move || Box::new(StreamSink::new(stream, tx)), - ); + let (tx, rx_sink) = tokio::sync::mpsc::unbounded_channel(); + let (player, rx_player) = + SpotifyPlayer::new(player_config, session.clone(), mixer.get_soft_volume(), { + let stream = stream.clone(); + move || Box::new(StreamSink::new(stream, tx)) + }); let (spirc, spirc_task) = Spirc::new( ConnectConfig { name: device_name.into(), // 50% initial_volume: Some(65535 / 2), + // Default Spotify behaviour + autoplay: true, ..Default::default() }, session.clone(), @@ -96,12 +106,275 @@ impl Player { mixer, ); - tokio::spawn(spirc_task); + let (tx, rx) = tokio::sync::broadcast::channel(10); + let pbi = Arc::new(Mutex::new(None)); - Ok((spirc, (receiver, rx))) + let player_task = PlayerTask { + pbi: pbi.clone(), + session: session.clone(), + rx_player, + rx_sink, + rx, + spirc, + track, + stream, + }; + + tokio::spawn(spirc_task); + tokio::spawn(player_task.run()); + + Ok(Self { pbi, tx }) } - pub fn get_stream(&self) -> Stream { - self.stream.clone() + pub fn next(&self) { + self.tx.send(PlayerCommand::Next).ok(); + } + + pub fn prev(&self) { + self.tx.send(PlayerCommand::Previous).ok(); + } + + pub fn pause(&self) { + self.tx.send(PlayerCommand::Pause).ok(); + } + + pub fn play(&self) { + self.tx.send(PlayerCommand::Play).ok(); + } + + pub async fn pbi(&self) -> Option { + self.pbi.lock().await.as_ref().cloned() + } +} + +struct PlayerTask { + stream: Stream, + session: Session, + spirc: Spirc, + track: TrackHandle, + + rx_player: UnboundedReceiver, + rx_sink: UnboundedReceiver, + rx: Receiver, + + pbi: Arc>>, +} + +impl PlayerTask { + pub async fn run(mut self) { + let check_result = |result| { + if let Err(why) = result { + error!("Failed to issue track command: {:?}", why); + } + }; + + loop { + match self.next().await { + // Spotify player events + Some(Event::Player(event)) => match event { + PlayerEvent::Playing { + play_request_id: _, + track_id, + position_ms, + duration_ms, + } => { + self + .update_pbi(track_id, position_ms, duration_ms, true) + .await; + } + + PlayerEvent::Paused { + play_request_id: _, + track_id, + position_ms, + duration_ms, + } => { + self + .update_pbi(track_id, position_ms, duration_ms, false) + .await; + } + + PlayerEvent::Changed { + old_track_id: _, + new_track_id, + } => { + if let Ok(current) = self.resolve_audio_info(new_track_id).await { + let mut pbi = self.pbi.lock().await; + + if let Some(pbi) = pbi.as_mut() { + pbi.update_track(current); + } + } + } + + PlayerEvent::Stopped { + play_request_id: _, + track_id: _, + } => { + check_result(self.track.stop()); + } + + _ => {} + }, + + // Audio sink events + Some(Event::Sink(event)) => match event { + SinkEvent::Start => { + check_result(self.track.play()); + } + + SinkEvent::Stop => { + // EXPERIMENT: It may be beneficial to *NOT* pause songbird here + // We already have a fallback if no audio is present in the buffer (write all zeroes aka silence) + // So commenting this out may help prevent a substantial portion of jitter + // This comes at a cost of more bandwidth, though opus should compress it down to almost nothing + + // check_result(track.pause()); + } + }, + + // The `Player` has instructed us to do something + Some(Event::Command(command)) => match command { + PlayerCommand::Next => self.spirc.next(), + PlayerCommand::Previous => self.spirc.prev(), + PlayerCommand::Pause => self.spirc.pause(), + PlayerCommand::Play => self.spirc.play(), + }, + + None => { + // One of the channels died + log::debug!("Channel died"); + break; + } + } + } + } + + async fn next(&mut self) -> Option { + tokio::select! { + event = self.rx_player.recv() => { + event.map(Event::Player) + } + + event = self.rx_sink.recv() => { + event.map(Event::Sink) + } + + command = self.rx.recv() => { + command.ok().map(Event::Command) + } + } + } + + /// Update current playback info, or return early if not necessary + async fn update_pbi( + &self, + spotify_id: SpotifyId, + position_ms: u32, + duration_ms: u32, + playing: bool, + ) { + let mut pbi = self.pbi.lock().await; + + if let Some(pbi) = pbi.as_mut() { + pbi.update_pos_dur(position_ms, duration_ms, playing); + } + + if !pbi + .as_ref() + .map(|pbi| pbi.spotify_id == spotify_id) + .unwrap_or(true) + { + return; + } + + if let Ok(current) = self.resolve_audio_info(spotify_id).await { + match pbi.as_mut() { + Some(pbi) => { + pbi.update_track(current); + pbi.update_pos_dur(position_ms, duration_ms, true); + } + None => { + *pbi = Some(PlaybackInfo::new( + duration_ms, + position_ms, + true, + current, + spotify_id, + )); + } + } + } else { + log::error!("Failed to resolve audio info"); + } + } + + /// Retrieve the metadata for a `SpotifyId` + async fn resolve_audio_info(&self, spotify_id: SpotifyId) -> Result { + match spotify_id.audio_type { + SpotifyAudioType::Track => self.resolve_track_info(spotify_id).await, + SpotifyAudioType::Podcast => self.resolve_episode_info(spotify_id).await, + SpotifyAudioType::NonPlayable => Err(anyhow!("Cannot resolve non-playable audio type")), + } + } + + /// Retrieve the metadata for a Spotify Track + async fn resolve_track_info(&self, spotify_id: SpotifyId) -> Result { + let result = self + .session + .mercury() + .get(format!("hm://metadata/3/track/{}", spotify_id.to_base16()?)) + .await + .map_err(|_| anyhow!("Mercury metadata request failed"))?; + + if result.status_code != 200 { + return Err(anyhow!("Mercury metadata request invalid status code")); + } + + let message = match result.payload.get(0) { + Some(v) => v, + None => return Err(anyhow!("Mercury metadata request invalid payload")), + }; + + let proto_track = Track::parse_from_bytes(message)?; + + Ok(CurrentTrack::Track(proto_track)) + } + + /// Retrieve the metadata for a Spotify Podcast + async fn resolve_episode_info(&self, spotify_id: SpotifyId) -> Result { + let result = self + .session + .mercury() + .get(format!( + "hm://metadata/3/episode/{}", + spotify_id.to_base16()? + )) + .await + .map_err(|_| anyhow!("Mercury metadata request failed"))?; + + if result.status_code != 200 { + return Err(anyhow!("Mercury metadata request invalid status code")); + } + + let message = match result.payload.get(0) { + Some(v) => v, + None => return Err(anyhow!("Mercury metadata request invalid payload")), + }; + + let proto_episode = Episode::parse_from_bytes(message)?; + + Ok(CurrentTrack::Episode(proto_episode)) + } +} + +impl Drop for PlayerTask { + fn drop(&mut self) { + log::trace!("drop PlayerTask"); + + self.track.stop().ok(); + self.spirc.shutdown(); + self.session.shutdown(); + self.stream.flush().ok(); } } diff --git a/src/session/mod.rs b/src/session/mod.rs index 92a2b6b..1ff9e7c 100644 --- a/src/session/mod.rs +++ b/src/session/mod.rs @@ -6,16 +6,11 @@ use self::{ pbi::PlaybackInfo, }; use crate::{ - audio::SinkEvent, + audio::stream::Stream, consts::DISCONNECT_TIME, database::{Database, DatabaseError}, player::Player, - utils::{embed::Status, spotify}, -}; -use librespot::{ - connect::spirc::Spirc, - core::spotify_id::{SpotifyAudioType, SpotifyId}, - playback::player::PlayerEvent, + utils::embed::Status, }; use log::*; use serenity::{ @@ -31,7 +26,6 @@ use songbird::{ Call, Event, EventContext, EventHandler, }; use std::{ - io::Write, ops::{Deref, DerefMut}, sync::Arc, time::Duration, @@ -53,15 +47,10 @@ struct InnerSpoticordSession { call: Arc>, track: Option, - - playback_info: Option, + player: Option, disconnect_handle: Option>, - spirc: Option, - - player: Option, - /// Whether the session has been disconnected /// If this is true then this instance should no longer be used and dropped disconnected: bool, @@ -101,10 +90,8 @@ impl SpoticordSession { session_manager: session_manager.clone(), call: call.clone(), track: None, - playback_info: None, - disconnect_handle: None, - spirc: None, player: None, + disconnect_handle: None, disconnected: false, }; @@ -157,29 +144,29 @@ impl SpoticordSession { /// Advance to the next track pub async fn next(&mut self) { - if let Some(ref spirc) = self.acquire_read().await.spirc { - spirc.next(); + if let Some(ref player) = self.acquire_read().await.player { + player.next(); } } /// Rewind to the previous track pub async fn previous(&mut self) { - if let Some(ref spirc) = self.acquire_read().await.spirc { - spirc.prev(); + if let Some(ref player) = self.acquire_read().await.player { + player.prev(); } } /// Pause the current track pub async fn pause(&mut self) { - if let Some(ref spirc) = self.acquire_read().await.spirc { - spirc.pause(); + if let Some(ref player) = self.acquire_read().await.player { + player.pause(); } } /// Resume the current track pub async fn resume(&mut self) { - if let Some(ref spirc) = self.acquire_read().await.spirc { - spirc.play(); + if let Some(ref player) = self.acquire_read().await.player { + player.play(); } } @@ -215,13 +202,13 @@ impl SpoticordSession { } }; - // Create player - let mut player = Player::create(); + // Create stream + let stream = Stream::new(); // Create track (paused, fixes audio glitches) let (mut track, track_handle) = create_player(Input::new( true, - Reader::Extension(Box::new(player.get_stream())), + Reader::Extension(Box::new(stream.clone())), Codec::Pcm, Container::Raw, None, @@ -234,7 +221,7 @@ impl SpoticordSession { // Set call audio to track call.play_only(track); - let (spirc, (mut player_rx, mut sink_rx)) = match player.start(&token, &user.device_name).await + let player = match Player::create(stream, &token, &user.device_name, track_handle.clone()).await { Ok(v) => v, Err(why) => { @@ -244,242 +231,18 @@ impl SpoticordSession { } }; - // Handle events - tokio::spawn({ - let track = track_handle.clone(); - let ctx = ctx.clone(); - let instance = self.clone(); - let inner = self.0.clone(); - - 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; - - // Check if the session has been disconnected - let disconnected = { - let inner = inner.read().await; - inner.disconnected - }; - if disconnected { - break; - } - - tokio::select! { - event = player_rx.recv() => { - let Some(event) = event else { break; }; - - match event { - PlayerEvent::Playing { - play_request_id: _, - track_id, - position_ms, - duration_ms, - } => { - let was_none = instance - .update_playback(duration_ms, position_ms, true) - .await; - - if was_none { - // Stop player if update track fails - if let Err(why) = instance.update_track(&ctx, &owner_id, track_id).await { - error!("Failed to update track: {:?}", why); - - instance.player_stopped().await; - return; - } - } - } - - PlayerEvent::Paused { - play_request_id: _, - track_id, - position_ms, - duration_ms, - } => { - instance.start_disconnect_timer().await; - - let was_none = instance - .update_playback(duration_ms, position_ms, false) - .await; - - if was_none { - // Stop player if update track fails - - if let Err(why) = instance.update_track(&ctx, &owner_id, track_id).await { - error!("Failed to update track: {:?}", why); - - instance.player_stopped().await; - return; - } - } - } - - PlayerEvent::Changed { - old_track_id: _, - new_track_id, - } => { - let instance = instance.clone(); - let ctx = ctx.clone(); - - // Fetch track info - // This is done in a separate task to avoid blocking the IPC handler - tokio::spawn(async move { - if let Err(why) = instance.update_track(&ctx, &owner_id, new_track_id).await { - error!("Failed to update track: {:?}", why); - - instance.player_stopped().await; - } - }); - } - - PlayerEvent::Stopped { - play_request_id: _, - track_id: _, - } => { - check_result(track.pause()); - - { - let mut inner = inner.write().await; - inner.playback_info.take(); - } - - instance.start_disconnect_timer().await; - } - - _ => {} - }; - } - - event = sink_rx.recv() => { - let Some(event) = event else { break; }; - - let check_result = |result| { - if let Err(why) = result { - error!("Failed to issue track command: {:?}", why); - } - }; - - - match event { - SinkEvent::Start => { - check_result(track.play()); - } - - SinkEvent::Stop => { - // EXPERIMENT: It may be beneficial to *NOT* pause songbird here - // We already have a fallback if no audio is present in the buffer (write all zeroes aka silence) - // So commenting this out may help prevent a substantial portion of jitter - // This comes at a cost of more bandwidth, though opus should compress it down to almost nothing - - // check_result(track.pause()); - } - } - } - }; - } - - // Clean up session - if !inner.read().await.disconnected { - instance.player_stopped().await; - } - } - }); - // Update inner client and track let mut inner = self.acquire_write().await; inner.track = Some(track_handle); - inner.spirc = Some(spirc); inner.player = Some(player); 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().await; - - if let Some(pbi) = pbi { - pbi.spotify_id.is_none() || pbi.spotify_id != Some(spotify_id) - } else { - false - } - }; - - if !should_update { - return Ok(()); - } - - let data = ctx.data.read().await; - let database = data.get::().expect("to contain a value"); - - 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 = None; - let mut episode: Option = 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); - } - - // Update track/episode - let mut inner = self.acquire_write().await; - - if let Some(pbi) = inner.playback_info.as_mut() { - pbi.update_track_episode(spotify_id, track, episode); - } - - Ok(()) - } - /// Called when the player must stop, but not leave the call async fn player_stopped(&self) { let mut inner = self.acquire_write().await; - if let Some(spirc) = inner.spirc.take() { - spirc.shutdown(); - } - if let Some(track) = inner.track.take() { if let Err(why) = track.stop() { error!("Failed to stop track: {:?}", why); @@ -491,9 +254,6 @@ impl SpoticordSession { inner.session_manager.remove_owner(owner_id).await; } - // Clear playback info - inner.playback_info = None; - // Unlock to prevent deadlock in start_disconnect_timer drop(inner); @@ -521,42 +281,11 @@ impl SpoticordSession { self.stop_disconnect_timer().await; } - // 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().await; - - pbi.is_none() - }; - - { - let mut inner = self.acquire_write().await; - - if is_none { - inner.playback_info = Some(PlaybackInfo::new(duration_ms, position_ms, playing)); - } else { - // Update position, duration and playback state - inner - .playback_info - .as_mut() - .expect("to contain a value") - .update_pos_dur(position_ms, duration_ms, playing); - }; - }; - - if playing { - self.stop_disconnect_timer().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) { self.stop_disconnect_timer().await; - let arc_handle = self.0.clone(); let mut inner = self.acquire_write().await; // Check if we are already disconnected @@ -565,8 +294,7 @@ impl SpoticordSession { } inner.disconnect_handle = Some(tokio::spawn({ - let inner = arc_handle.clone(); - let instance = self.clone(); + let session = self.clone(); async move { let mut timer = tokio::time::interval(Duration::from_secs(DISCONNECT_TIME)); @@ -578,19 +306,15 @@ impl SpoticordSession { // 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 inner = inner.read().await; - - if let Some(ref pbi) = inner.playback_info { - pbi.is_playing - } else { - false - } - }; + let is_playing = session + .playback_info() + .await + .map(|pbi| pbi.is_playing) + .unwrap_or(false); if !is_playing { info!("Player is not playing, disconnecting"); - instance + session .disconnect_with_message( "The player has been inactive for too long, and has been disconnected.", ) @@ -668,7 +392,10 @@ impl SpoticordSession { /// Get the playback info pub async fn playback_info(&self) -> Option { - self.acquire_read().await.playback_info.clone() + let handle = self.acquire_read().await; + let player = handle.player.as_ref()?; + + player.pbi().await } pub async fn call(&self) -> Arc> { @@ -728,11 +455,6 @@ impl<'a> DerefMut for WriteLock<'a> { impl InnerSpoticordSession { /// Internal version of disconnect, which does not abort the disconnect timer async fn disconnect_no_abort(&mut self) { - // Flush stream so that it is not permanently blocking the thread - if let Some(player) = self.player.take() { - player.get_stream().flush().ok(); - } - self.disconnected = true; self .session_manager @@ -741,10 +463,6 @@ impl InnerSpoticordSession { let mut call = self.call.lock().await; - if let Some(spirc) = self.spirc.take() { - spirc.shutdown(); - } - if let Some(track) = self.track.take() { if let Err(why) = track.stop() { error!("Failed to stop track: {:?}", why); diff --git a/src/session/pbi.rs b/src/session/pbi.rs index 293805b..11629da 100644 --- a/src/session/pbi.rs +++ b/src/session/pbi.rs @@ -1,28 +1,41 @@ -use librespot::core::spotify_id::SpotifyId; +use librespot::{ + core::spotify_id::SpotifyId, + protocol::metadata::{Episode, Track}, +}; -use crate::utils::{self, spotify}; +use crate::utils; #[derive(Clone)] pub struct PlaybackInfo { last_updated: u128, position_ms: u32, - pub track: Option, - pub episode: Option, - pub spotify_id: Option, + pub track: CurrentTrack, + pub spotify_id: SpotifyId, pub duration_ms: u32, pub is_playing: bool, } +#[derive(Clone)] +pub enum CurrentTrack { + Track(Track), + Episode(Episode), +} + impl PlaybackInfo { /// Create a new instance of PlaybackInfo - pub fn new(duration_ms: u32, position_ms: u32, is_playing: bool) -> Self { + pub fn new( + duration_ms: u32, + position_ms: u32, + is_playing: bool, + track: CurrentTrack, + spotify_id: SpotifyId, + ) -> Self { Self { last_updated: utils::get_time_ms(), - track: None, - episode: None, - spotify_id: None, + track, + spotify_id, duration_ms, position_ms, is_playing, @@ -39,15 +52,8 @@ impl PlaybackInfo { } /// Update spotify id, track and episode - pub fn update_track_episode( - &mut self, - spotify_id: SpotifyId, - track: Option, - episode: Option, - ) { - self.spotify_id = Some(spotify_id); + pub fn update_track(&mut self, track: CurrentTrack) { self.track = track; - self.episode = episode; } /// Get the current playback position @@ -63,71 +69,73 @@ impl PlaybackInfo { } /// Get the name of the track or episode - pub fn get_name(&self) -> Option { - if let Some(track) = &self.track { - Some(track.name.clone()) - } else { - self.episode.as_ref().map(|episode| episode.name.clone()) + pub fn get_name(&self) -> String { + match &self.track { + CurrentTrack::Track(track) => track.get_name().to_string(), + CurrentTrack::Episode(episode) => episode.get_name().to_string(), } } /// Get the artist(s) or show name of the current track - pub fn get_artists(&self) -> Option { - if let Some(track) = &self.track { - Some( - track - .artists - .iter() - .map(|a| a.name.clone()) - .collect::>() - .join(", "), - ) - } else { - self - .episode - .as_ref() - .map(|episode| episode.show.name.clone()) + pub fn get_artists(&self) -> String { + match &self.track { + CurrentTrack::Track(track) => track + .get_artist() + .iter() + .map(|a| a.get_name().to_string()) + .collect::>() + .join(", "), + CurrentTrack::Episode(episode) => episode.get_show().get_name().to_string(), } } /// Get the album art url pub fn get_thumbnail_url(&self) -> Option { - if let Some(track) = &self.track { - let mut images = track.album.images.clone(); - images.sort_by(|a, b| b.width.cmp(&a.width)); + let file_id = match &self.track { + CurrentTrack::Track(track) => { + let mut images = track.get_album().get_cover_group().get_image().to_vec(); + images.sort_by_key(|b| std::cmp::Reverse(b.get_width())); - images.get(0).as_ref().map(|image| image.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)); + images + .get(0) + .as_ref() + .map(|image| image.get_file_id()) + .map(hex::encode) + } + CurrentTrack::Episode(episode) => { + let mut images = episode.get_covers().get_image().to_vec(); + images.sort_by_key(|b| std::cmp::Reverse(b.get_width())); - images.get(0).as_ref().map(|image| image.url.clone()) - } else { - None - } + images + .get(0) + .as_ref() + .map(|image| image.get_file_id()) + .map(hex::encode) + } + }; + + file_id.map(|id| format!("https://i.scdn.co/image/{id}")) } /// Get the type of audio (track or episode) #[allow(dead_code)] - pub fn get_type(&self) -> Option { - if self.track.is_some() { - Some("track".into()) - } else if self.episode.is_some() { - Some("episode".into()) - } else { - None + pub fn get_type(&self) -> String { + match &self.track { + CurrentTrack::Track(_) => "track".to_string(), + CurrentTrack::Episode(_) => "episode".to_string(), } } /// Get the public facing url of the track or episode #[allow(dead_code)] pub fn get_url(&self) -> Option<&str> { - if let Some(ref track) = self.track { - Some(track.external_urls.spotify.as_str()) - } else if let Some(ref episode) = self.episode { - Some(episode.external_urls.spotify.as_str()) - } else { - None + match &self.track { + CurrentTrack::Track(track) => track + .get_external_id() + .iter() + .find(|id| id.get_typ() == "spotify") + .map(|v| v.get_id()), + CurrentTrack::Episode(episode) => Some(episode.get_external_url()), } } } diff --git a/src/utils/spotify.rs b/src/utils/spotify.rs index 3a7dfe4..7468693 100644 --- a/src/utils/spotify.rs +++ b/src/utils/spotify.rs @@ -1,55 +1,8 @@ -use std::error::Error; - -use librespot::core::spotify_id::SpotifyId; +use anyhow::{anyhow, Result}; use log::{error, trace}; -use serde::Deserialize; use serde_json::Value; -#[derive(Debug, Clone, Deserialize)] -pub struct Artist { - pub name: String, -} - -#[derive(Debug, Clone, Deserialize)] -pub struct Image { - pub url: String, - pub height: u32, - pub width: u32, -} - -#[derive(Debug, Clone, Deserialize)] -pub struct Album { - pub name: String, - pub images: Vec, -} - -#[derive(Debug, Clone, Deserialize)] -pub struct ExternalUrls { - pub spotify: String, -} - -#[derive(Debug, Clone, Deserialize)] -pub struct Track { - pub name: String, - pub artists: Vec, - pub album: Album, - pub external_urls: ExternalUrls, -} - -#[derive(Debug, Clone, Deserialize)] -pub struct Show { - pub name: String, - pub images: Vec, -} - -#[derive(Debug, Clone, Deserialize)] -pub struct Episode { - pub name: String, - pub show: Show, - pub external_urls: ExternalUrls, -} - -pub async fn get_username(token: impl Into) -> Result { +pub async fn get_username(token: impl Into) -> Result { let token = token.into(); let client = reqwest::Client::new(); @@ -65,7 +18,7 @@ pub async fn get_username(token: impl Into) -> Result { Ok(response) => response, Err(why) => { error!("Failed to get username: {}", why); - return Err(format!("{}", why)); + return Err(why.into()); } }; @@ -76,7 +29,7 @@ pub async fn get_username(token: impl Into) -> Result { if response.status() != 200 { error!("Failed to get username: {}", response.status()); - return Err(format!( + return Err(anyhow!( "Failed to get track info: Invalid status code: {}", response.status() )); @@ -86,7 +39,7 @@ pub async fn get_username(token: impl Into) -> Result { Ok(body) => body, Err(why) => { error!("Failed to parse body: {}", why); - return Err(format!("{}", why)); + return Err(why.into()); } }; @@ -96,82 +49,6 @@ pub async fn get_username(token: impl Into) -> Result { } error!("Missing 'id' field in body: {:#?}", body); - return Err("Failed to parse body: Invalid body received".to_string()); - } -} - -pub async fn get_track_info( - token: impl Into, - track: SpotifyId, -) -> Result> { - let token = token.into(); - let client = reqwest::Client::new(); - - let mut retries = 3; - - loop { - let response = client - .get(format!( - "https://api.spotify.com/v1/tracks/{}", - track.to_base62()? - )) - .bearer_auth(&token) - .send() - .await?; - - if response.status().as_u16() >= 500 && retries > 0 { - retries -= 1; - continue; - } - - if response.status() != 200 { - return Err( - format!( - "Failed to get track info: Invalid status code: {}", - response.status() - ) - .into(), - ); - } - - return Ok(response.json().await?); - } -} - -pub async fn get_episode_info( - token: impl Into, - episode: SpotifyId, -) -> Result> { - let token = token.into(); - let client = reqwest::Client::new(); - - let mut retries = 3; - - loop { - let response = client - .get(format!( - "https://api.spotify.com/v1/episodes/{}", - episode.to_base62()? - )) - .bearer_auth(&token) - .send() - .await?; - - if response.status().as_u16() >= 500 && retries > 0 { - retries -= 1; - continue; - } - - if response.status() != 200 { - return Err( - format!( - "Failed to get episode info: Invalid status code: {}", - response.status() - ) - .into(), - ); - } - - return Ok(response.json().await?); + return Err(anyhow!("Failed to parse body: Invalid body received")); } }