Less Spotify API calls, rearranged some player stuff
This commit is contained in:
		
							parent
							
								
									fca55d5644
								
							
						
					
					
						commit
						2cebeb41ab
					
				
							
								
								
									
										9
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										9
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							| @ -94,6 +94,12 @@ dependencies = [ | |||||||
|  "libc", |  "libc", | ||||||
| ] | ] | ||||||
| 
 | 
 | ||||||
|  | [[package]] | ||||||
|  | name = "anyhow" | ||||||
|  | version = "1.0.75" | ||||||
|  | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
|  | checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6" | ||||||
|  | 
 | ||||||
| [[package]] | [[package]] | ||||||
| name = "arrayvec" | name = "arrayvec" | ||||||
| version = "0.7.4" | version = "0.7.4" | ||||||
| @ -2394,10 +2400,13 @@ dependencies = [ | |||||||
| name = "spoticord" | name = "spoticord" | ||||||
| version = "2.1.0" | version = "2.1.0" | ||||||
| dependencies = [ | dependencies = [ | ||||||
|  |  "anyhow", | ||||||
|  "dotenv", |  "dotenv", | ||||||
|  "env_logger 0.10.0", |  "env_logger 0.10.0", | ||||||
|  |  "hex", | ||||||
|  "librespot", |  "librespot", | ||||||
|  "log", |  "log", | ||||||
|  |  "protobuf", | ||||||
|  "redis", |  "redis", | ||||||
|  "reqwest", |  "reqwest", | ||||||
|  "samplerate", |  "samplerate", | ||||||
|  | |||||||
| @ -12,10 +12,13 @@ path = "src/main.rs" | |||||||
| stats = ["redis"] | stats = ["redis"] | ||||||
| 
 | 
 | ||||||
| [dependencies] | [dependencies] | ||||||
|  | anyhow = "1.0.75" | ||||||
| dotenv = "0.15.0" | dotenv = "0.15.0" | ||||||
| env_logger = "0.10.0" | env_logger = "0.10.0" | ||||||
|  | hex = "0.4.3" | ||||||
| librespot = { version = "0.4.2", default-features = false } | librespot = { version = "0.4.2", default-features = false } | ||||||
| log = "0.4.20" | log = "0.4.20" | ||||||
|  | protobuf = "2.28.0" | ||||||
| redis = { version = "0.23.3", optional = true } | redis = { version = "0.23.3", optional = true } | ||||||
| reqwest = "0.11.20" | reqwest = "0.11.20" | ||||||
| samplerate = "0.2.4" | samplerate = "0.2.4" | ||||||
|  | |||||||
| @ -1,6 +1,6 @@ | |||||||
| use std::time::Duration; | use std::time::Duration; | ||||||
| 
 | 
 | ||||||
| use librespot::core::spotify_id::{SpotifyAudioType, SpotifyId}; | use librespot::core::spotify_id::SpotifyId; | ||||||
| use log::error; | use log::error; | ||||||
| use serenity::{ | use serenity::{ | ||||||
|   builder::{CreateApplicationCommand, CreateButton, CreateComponents, CreateEmbed}, |   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
 |     // Get owner of session
 | ||||||
|     let owner = match utils::discord::get_user(&ctx, owner).await { |     let owner = match utils::discord::get_user(&ctx, owner).await { | ||||||
|       Some(user) => user, |       Some(user) => user, | ||||||
| @ -119,7 +110,7 @@ pub fn command(ctx: Context, command: ApplicationCommandInteraction) -> CommandO | |||||||
|     }; |     }; | ||||||
| 
 | 
 | ||||||
|     // Get metadata
 |     // 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 |     if let Err(why) = command | ||||||
|       .create_interaction_response(&ctx.http, |response| { |       .create_interaction_response(&ctx.http, |response| { | ||||||
| @ -129,8 +120,8 @@ pub fn command(ctx: Context, command: ApplicationCommandInteraction) -> CommandO | |||||||
|             message |             message | ||||||
|               .set_embed(build_playing_embed( |               .set_embed(build_playing_embed( | ||||||
|                 title, |                 title, | ||||||
|                 audio_type, |                 pbi.get_type(), | ||||||
|                 spotify_id, |                 pbi.spotify_id, | ||||||
|                 description, |                 description, | ||||||
|                 owner, |                 owner, | ||||||
|                 thumbnail, |                 thumbnail, | ||||||
| @ -409,20 +400,7 @@ async fn update_embed(interaction: &mut MessageComponentInteraction, ctx: &Conte | |||||||
|     } |     } | ||||||
|   }; |   }; | ||||||
| 
 | 
 | ||||||
|   let spotify_id = match pbi.spotify_id { |   let (title, description, thumbnail) = get_metadata(&pbi); | ||||||
|     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); |  | ||||||
| 
 | 
 | ||||||
|   if let Err(why) = interaction |   if let Err(why) = interaction | ||||||
|     .message |     .message | ||||||
| @ -430,8 +408,8 @@ async fn update_embed(interaction: &mut MessageComponentInteraction, ctx: &Conte | |||||||
|       message |       message | ||||||
|         .set_embed(build_playing_embed( |         .set_embed(build_playing_embed( | ||||||
|           title, |           title, | ||||||
|           audio_type, |           pbi.get_type(), | ||||||
|           spotify_id, |           pbi.spotify_id, | ||||||
|           description, |           description, | ||||||
|           owner, |           owner, | ||||||
|           thumbnail, |           thumbnail, | ||||||
| @ -477,20 +455,9 @@ fn build_playing_embed( | |||||||
|   embed |   embed | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| fn get_metadata(spotify_id: SpotifyId, pbi: &PlaybackInfo) -> (String, String, String, String) { | fn get_metadata(pbi: &PlaybackInfo) -> (String, String, String) { | ||||||
|   // Get audio type
 |  | ||||||
|   let audio_type = if spotify_id.audio_type == SpotifyAudioType::Track { |  | ||||||
|     "track" |  | ||||||
|   } else { |  | ||||||
|     "episode" |  | ||||||
|   }; |  | ||||||
| 
 |  | ||||||
|   // Create title
 |   // Create title
 | ||||||
|   let title = format!( |   let title = format!("{} - {}", pbi.get_artists(), pbi.get_name()); | ||||||
|     "{} - {}", |  | ||||||
|     pbi.get_artists().as_deref().unwrap_or("ID"), |  | ||||||
|     pbi.get_name().as_deref().unwrap_or("ID") |  | ||||||
|   ); |  | ||||||
| 
 | 
 | ||||||
|   // Create description
 |   // Create description
 | ||||||
|   let mut description = String::new(); |   let mut description = String::new(); | ||||||
| @ -518,5 +485,5 @@ fn get_metadata(spotify_id: SpotifyId, pbi: &PlaybackInfo) -> (String, String, S | |||||||
|   // Get the thumbnail image
 |   // Get the thumbnail image
 | ||||||
|   let thumbnail = pbi.get_thumbnail_url().expect("to contain a value"); |   let thumbnail = pbi.get_thumbnail_url().expect("to contain a value"); | ||||||
| 
 | 
 | ||||||
|   (title, description, audio_type.to_string(), thumbnail) |   (title, description, thumbnail) | ||||||
| } | } | ||||||
|  | |||||||
| @ -1,6 +1,10 @@ | |||||||
|  | #[cfg(not(debug_assertions))] | ||||||
| pub const VERSION: &str = env!("CARGO_PKG_VERSION"); | 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"; | pub const MOTD: &str = "some good 'ol music"; | ||||||
| 
 | 
 | ||||||
| /// The time it takes for Spoticord to disconnect when no music is being played
 | /// The time it takes for Spoticord to disconnect when no music is being played
 | ||||||
| pub const DISCONNECT_TIME: u64 = 5 * 60; | pub const DISCONNECT_TIME: u64 = 5 * 60; | ||||||
| 
 |  | ||||||
|  | |||||||
| @ -1,8 +1,12 @@ | |||||||
|  | use std::{io::Write, sync::Arc}; | ||||||
|  | 
 | ||||||
|  | use anyhow::{anyhow, Result}; | ||||||
| use librespot::{ | use librespot::{ | ||||||
|   connect::spirc::Spirc, |   connect::spirc::Spirc, | ||||||
|   core::{ |   core::{ | ||||||
|     config::{ConnectConfig, SessionConfig}, |     config::{ConnectConfig, SessionConfig}, | ||||||
|     session::Session, |     session::Session, | ||||||
|  |     spotify_id::{SpotifyAudioType, SpotifyId}, | ||||||
|   }, |   }, | ||||||
|   discovery::Credentials, |   discovery::Credentials, | ||||||
|   playback::{ |   playback::{ | ||||||
| @ -10,39 +14,52 @@ use librespot::{ | |||||||
|     mixer::{self, MixerConfig}, |     mixer::{self, MixerConfig}, | ||||||
|     player::{Player as SpotifyPlayer, PlayerEvent}, |     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::{ | use crate::{ | ||||||
|   audio::{stream::Stream, SinkEvent, StreamSink}, |   audio::{stream::Stream, SinkEvent, StreamSink}, | ||||||
|   librespot_ext::discovery::CredentialsExt, |   librespot_ext::discovery::CredentialsExt, | ||||||
|  |   session::pbi::{CurrentTrack, PlaybackInfo}, | ||||||
|   utils, |   utils, | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
|  | enum Event { | ||||||
|  |   Player(PlayerEvent), | ||||||
|  |   Sink(SinkEvent), | ||||||
|  |   Command(PlayerCommand), | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | #[derive(Clone)] | ||||||
|  | enum PlayerCommand { | ||||||
|  |   Next, | ||||||
|  |   Previous, | ||||||
|  |   Pause, | ||||||
|  |   Play, | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | #[derive(Clone)] | ||||||
| pub struct Player { | pub struct Player { | ||||||
|   stream: Stream, |   tx: Sender<PlayerCommand>, | ||||||
|   session: Option<Session>, | 
 | ||||||
|  |   pbi: Arc<Mutex<Option<PlaybackInfo>>>, | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| impl Player { | impl Player { | ||||||
|   pub fn create() -> Self { |   pub async fn create( | ||||||
|     Self { |     stream: Stream, | ||||||
|       stream: Stream::new(), |  | ||||||
|       session: None, |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   pub async fn start( |  | ||||||
|     &mut self, |  | ||||||
|     token: &str, |     token: &str, | ||||||
|     device_name: &str, |     device_name: &str, | ||||||
|   ) -> Result< |     track: TrackHandle, | ||||||
|     ( |   ) -> Result<Self> { | ||||||
|       Spirc, |  | ||||||
|       (UnboundedReceiver<PlayerEvent>, UnboundedReceiver<SinkEvent>), |  | ||||||
|     ), |  | ||||||
|     Box<dyn std::error::Error>, |  | ||||||
|   > { |  | ||||||
|     let username = utils::spotify::get_username(token).await?; |     let username = utils::spotify::get_username(token).await?; | ||||||
| 
 | 
 | ||||||
|     let player_config = PlayerConfig { |     let player_config = PlayerConfig { | ||||||
| @ -52,12 +69,6 @@ impl Player { | |||||||
| 
 | 
 | ||||||
|     let credentials = Credentials::with_token(username, token); |     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( |     let (session, _) = Session::connect( | ||||||
|       SessionConfig { |       SessionConfig { | ||||||
|         ap_port: Some(9999), // Force the use of ap.spotify.com, which has the lowest latency
 |         ap_port: Some(9999), // Force the use of ap.spotify.com, which has the lowest latency
 | ||||||
| @ -68,27 +79,26 @@ impl Player { | |||||||
|       false, |       false, | ||||||
|     ) |     ) | ||||||
|     .await?; |     .await?; | ||||||
|     self.session = Some(session.clone()); |  | ||||||
| 
 | 
 | ||||||
|     let mixer = (mixer::find(Some("softvol")).expect("to exist"))(MixerConfig { |     let mixer = (mixer::find(Some("softvol")).expect("to exist"))(MixerConfig { | ||||||
|       volume_ctrl: VolumeCtrl::Linear, |       volume_ctrl: VolumeCtrl::Linear, | ||||||
|       ..Default::default() |       ..Default::default() | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|     let stream = self.get_stream(); |     let (tx, rx_sink) = tokio::sync::mpsc::unbounded_channel(); | ||||||
|     let (tx, rx) = tokio::sync::mpsc::unbounded_channel(); |     let (player, rx_player) = | ||||||
|     let (player, receiver) = SpotifyPlayer::new( |       SpotifyPlayer::new(player_config, session.clone(), mixer.get_soft_volume(), { | ||||||
|       player_config, |         let stream = stream.clone(); | ||||||
|       session.clone(), |         move || Box::new(StreamSink::new(stream, tx)) | ||||||
|       mixer.get_soft_volume(), |       }); | ||||||
|       move || Box::new(StreamSink::new(stream, tx)), |  | ||||||
|     ); |  | ||||||
| 
 | 
 | ||||||
|     let (spirc, spirc_task) = Spirc::new( |     let (spirc, spirc_task) = Spirc::new( | ||||||
|       ConnectConfig { |       ConnectConfig { | ||||||
|         name: device_name.into(), |         name: device_name.into(), | ||||||
|         // 50%
 |         // 50%
 | ||||||
|         initial_volume: Some(65535 / 2), |         initial_volume: Some(65535 / 2), | ||||||
|  |         // Default Spotify behaviour
 | ||||||
|  |         autoplay: true, | ||||||
|         ..Default::default() |         ..Default::default() | ||||||
|       }, |       }, | ||||||
|       session.clone(), |       session.clone(), | ||||||
| @ -96,12 +106,275 @@ impl Player { | |||||||
|       mixer, |       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 { |   pub fn next(&self) { | ||||||
|     self.stream.clone() |     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<PlaybackInfo> { | ||||||
|  |     self.pbi.lock().await.as_ref().cloned() | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | struct PlayerTask { | ||||||
|  |   stream: Stream, | ||||||
|  |   session: Session, | ||||||
|  |   spirc: Spirc, | ||||||
|  |   track: TrackHandle, | ||||||
|  | 
 | ||||||
|  |   rx_player: UnboundedReceiver<PlayerEvent>, | ||||||
|  |   rx_sink: UnboundedReceiver<SinkEvent>, | ||||||
|  |   rx: Receiver<PlayerCommand>, | ||||||
|  | 
 | ||||||
|  |   pbi: Arc<Mutex<Option<PlaybackInfo>>>, | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | 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<Event> { | ||||||
|  |     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<CurrentTrack> { | ||||||
|  |     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<CurrentTrack> { | ||||||
|  |     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<CurrentTrack> { | ||||||
|  |     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(); | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  | |||||||
| @ -6,16 +6,11 @@ use self::{ | |||||||
|   pbi::PlaybackInfo, |   pbi::PlaybackInfo, | ||||||
| }; | }; | ||||||
| use crate::{ | use crate::{ | ||||||
|   audio::SinkEvent, |   audio::stream::Stream, | ||||||
|   consts::DISCONNECT_TIME, |   consts::DISCONNECT_TIME, | ||||||
|   database::{Database, DatabaseError}, |   database::{Database, DatabaseError}, | ||||||
|   player::Player, |   player::Player, | ||||||
|   utils::{embed::Status, spotify}, |   utils::embed::Status, | ||||||
| }; |  | ||||||
| use librespot::{ |  | ||||||
|   connect::spirc::Spirc, |  | ||||||
|   core::spotify_id::{SpotifyAudioType, SpotifyId}, |  | ||||||
|   playback::player::PlayerEvent, |  | ||||||
| }; | }; | ||||||
| use log::*; | use log::*; | ||||||
| use serenity::{ | use serenity::{ | ||||||
| @ -31,7 +26,6 @@ use songbird::{ | |||||||
|   Call, Event, EventContext, EventHandler, |   Call, Event, EventContext, EventHandler, | ||||||
| }; | }; | ||||||
| use std::{ | use std::{ | ||||||
|   io::Write, |  | ||||||
|   ops::{Deref, DerefMut}, |   ops::{Deref, DerefMut}, | ||||||
|   sync::Arc, |   sync::Arc, | ||||||
|   time::Duration, |   time::Duration, | ||||||
| @ -53,15 +47,10 @@ struct InnerSpoticordSession { | |||||||
| 
 | 
 | ||||||
|   call: Arc<Mutex<Call>>, |   call: Arc<Mutex<Call>>, | ||||||
|   track: Option<TrackHandle>, |   track: Option<TrackHandle>, | ||||||
| 
 |   player: Option<Player>, | ||||||
|   playback_info: Option<PlaybackInfo>, |  | ||||||
| 
 | 
 | ||||||
|   disconnect_handle: Option<tokio::task::JoinHandle<()>>, |   disconnect_handle: Option<tokio::task::JoinHandle<()>>, | ||||||
| 
 | 
 | ||||||
|   spirc: Option<Spirc>, |  | ||||||
| 
 |  | ||||||
|   player: Option<Player>, |  | ||||||
| 
 |  | ||||||
|   /// Whether the session has been disconnected
 |   /// Whether the session has been disconnected
 | ||||||
|   /// If this is true then this instance should no longer be used and dropped
 |   /// If this is true then this instance should no longer be used and dropped
 | ||||||
|   disconnected: bool, |   disconnected: bool, | ||||||
| @ -101,10 +90,8 @@ impl SpoticordSession { | |||||||
|       session_manager: session_manager.clone(), |       session_manager: session_manager.clone(), | ||||||
|       call: call.clone(), |       call: call.clone(), | ||||||
|       track: None, |       track: None, | ||||||
|       playback_info: None, |  | ||||||
|       disconnect_handle: None, |  | ||||||
|       spirc: None, |  | ||||||
|       player: None, |       player: None, | ||||||
|  |       disconnect_handle: None, | ||||||
|       disconnected: false, |       disconnected: false, | ||||||
|     }; |     }; | ||||||
| 
 | 
 | ||||||
| @ -157,29 +144,29 @@ impl SpoticordSession { | |||||||
| 
 | 
 | ||||||
|   /// Advance to the next track
 |   /// Advance to the next track
 | ||||||
|   pub async fn next(&mut self) { |   pub async fn next(&mut self) { | ||||||
|     if let Some(ref spirc) = self.acquire_read().await.spirc { |     if let Some(ref player) = self.acquire_read().await.player { | ||||||
|       spirc.next(); |       player.next(); | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   /// Rewind to the previous track
 |   /// Rewind to the previous track
 | ||||||
|   pub async fn previous(&mut self) { |   pub async fn previous(&mut self) { | ||||||
|     if let Some(ref spirc) = self.acquire_read().await.spirc { |     if let Some(ref player) = self.acquire_read().await.player { | ||||||
|       spirc.prev(); |       player.prev(); | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   /// Pause the current track
 |   /// Pause the current track
 | ||||||
|   pub async fn pause(&mut self) { |   pub async fn pause(&mut self) { | ||||||
|     if let Some(ref spirc) = self.acquire_read().await.spirc { |     if let Some(ref player) = self.acquire_read().await.player { | ||||||
|       spirc.pause(); |       player.pause(); | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   /// Resume the current track
 |   /// Resume the current track
 | ||||||
|   pub async fn resume(&mut self) { |   pub async fn resume(&mut self) { | ||||||
|     if let Some(ref spirc) = self.acquire_read().await.spirc { |     if let Some(ref player) = self.acquire_read().await.player { | ||||||
|       spirc.play(); |       player.play(); | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
| @ -215,13 +202,13 @@ impl SpoticordSession { | |||||||
|       } |       } | ||||||
|     }; |     }; | ||||||
| 
 | 
 | ||||||
|     // Create player
 |     // Create stream
 | ||||||
|     let mut player = Player::create(); |     let stream = Stream::new(); | ||||||
| 
 | 
 | ||||||
|     // Create track (paused, fixes audio glitches)
 |     // Create track (paused, fixes audio glitches)
 | ||||||
|     let (mut track, track_handle) = create_player(Input::new( |     let (mut track, track_handle) = create_player(Input::new( | ||||||
|       true, |       true, | ||||||
|       Reader::Extension(Box::new(player.get_stream())), |       Reader::Extension(Box::new(stream.clone())), | ||||||
|       Codec::Pcm, |       Codec::Pcm, | ||||||
|       Container::Raw, |       Container::Raw, | ||||||
|       None, |       None, | ||||||
| @ -234,7 +221,7 @@ impl SpoticordSession { | |||||||
|     // Set call audio to track
 |     // Set call audio to track
 | ||||||
|     call.play_only(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, |       Ok(v) => v, | ||||||
|       Err(why) => { |       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
 |     // Update inner client and track
 | ||||||
|     let mut inner = self.acquire_write().await; |     let mut inner = self.acquire_write().await; | ||||||
|     inner.track = Some(track_handle); |     inner.track = Some(track_handle); | ||||||
|     inner.spirc = Some(spirc); |  | ||||||
|     inner.player = Some(player); |     inner.player = Some(player); | ||||||
| 
 | 
 | ||||||
|     Ok(()) |     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::<Database>().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<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); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     // 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
 |   /// Called when the player must stop, but not leave the call
 | ||||||
|   async fn player_stopped(&self) { |   async fn player_stopped(&self) { | ||||||
|     let mut inner = self.acquire_write().await; |     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 Some(track) = inner.track.take() { | ||||||
|       if let Err(why) = track.stop() { |       if let Err(why) = track.stop() { | ||||||
|         error!("Failed to stop track: {:?}", why); |         error!("Failed to stop track: {:?}", why); | ||||||
| @ -491,9 +254,6 @@ impl SpoticordSession { | |||||||
|       inner.session_manager.remove_owner(owner_id).await; |       inner.session_manager.remove_owner(owner_id).await; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     // Clear playback info
 |  | ||||||
|     inner.playback_info = None; |  | ||||||
| 
 |  | ||||||
|     // Unlock to prevent deadlock in start_disconnect_timer
 |     // Unlock to prevent deadlock in start_disconnect_timer
 | ||||||
|     drop(inner); |     drop(inner); | ||||||
| 
 | 
 | ||||||
| @ -521,42 +281,11 @@ impl SpoticordSession { | |||||||
|     self.stop_disconnect_timer().await; |     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
 |   /// Start the disconnect timer, which will disconnect the bot from the voice channel after a
 | ||||||
|   /// certain amount of time
 |   /// certain amount of time
 | ||||||
|   async fn start_disconnect_timer(&self) { |   async fn start_disconnect_timer(&self) { | ||||||
|     self.stop_disconnect_timer().await; |     self.stop_disconnect_timer().await; | ||||||
| 
 | 
 | ||||||
|     let arc_handle = self.0.clone(); |  | ||||||
|     let mut inner = self.acquire_write().await; |     let mut inner = self.acquire_write().await; | ||||||
| 
 | 
 | ||||||
|     // Check if we are already disconnected
 |     // Check if we are already disconnected
 | ||||||
| @ -565,8 +294,7 @@ impl SpoticordSession { | |||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     inner.disconnect_handle = Some(tokio::spawn({ |     inner.disconnect_handle = Some(tokio::spawn({ | ||||||
|       let inner = arc_handle.clone(); |       let session = self.clone(); | ||||||
|       let instance = self.clone(); |  | ||||||
| 
 | 
 | ||||||
|       async move { |       async move { | ||||||
|         let mut timer = tokio::time::interval(Duration::from_secs(DISCONNECT_TIME)); |         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.
 |         // Make sure this task has not been aborted, if it has this will automatically stop execution.
 | ||||||
|         tokio::task::yield_now().await; |         tokio::task::yield_now().await; | ||||||
| 
 | 
 | ||||||
|         let is_playing = { |         let is_playing = session | ||||||
|           let inner = inner.read().await; |           .playback_info() | ||||||
| 
 |           .await | ||||||
|           if let Some(ref pbi) = inner.playback_info { |           .map(|pbi| pbi.is_playing) | ||||||
|             pbi.is_playing |           .unwrap_or(false); | ||||||
|           } else { |  | ||||||
|             false |  | ||||||
|           } |  | ||||||
|         }; |  | ||||||
| 
 | 
 | ||||||
|         if !is_playing { |         if !is_playing { | ||||||
|           info!("Player is not playing, disconnecting"); |           info!("Player is not playing, disconnecting"); | ||||||
|           instance |           session | ||||||
|             .disconnect_with_message( |             .disconnect_with_message( | ||||||
|               "The player has been inactive for too long, and has been disconnected.", |               "The player has been inactive for too long, and has been disconnected.", | ||||||
|             ) |             ) | ||||||
| @ -668,7 +392,10 @@ impl SpoticordSession { | |||||||
| 
 | 
 | ||||||
|   /// Get the playback info
 |   /// Get the playback info
 | ||||||
|   pub async fn playback_info(&self) -> Option<PlaybackInfo> { |   pub async fn playback_info(&self) -> Option<PlaybackInfo> { | ||||||
|     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<Mutex<Call>> { |   pub async fn call(&self) -> Arc<Mutex<Call>> { | ||||||
| @ -728,11 +455,6 @@ impl<'a> DerefMut for WriteLock<'a> { | |||||||
| impl InnerSpoticordSession { | impl InnerSpoticordSession { | ||||||
|   /// Internal version of disconnect, which does not abort the disconnect timer
 |   /// Internal version of disconnect, which does not abort the disconnect timer
 | ||||||
|   async fn disconnect_no_abort(&mut self) { |   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.disconnected = true; | ||||||
|     self |     self | ||||||
|       .session_manager |       .session_manager | ||||||
| @ -741,10 +463,6 @@ impl InnerSpoticordSession { | |||||||
| 
 | 
 | ||||||
|     let mut call = self.call.lock().await; |     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 Some(track) = self.track.take() { | ||||||
|       if let Err(why) = track.stop() { |       if let Err(why) = track.stop() { | ||||||
|         error!("Failed to stop track: {:?}", why); |         error!("Failed to stop track: {:?}", why); | ||||||
|  | |||||||
| @ -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)] | #[derive(Clone)] | ||||||
| pub struct PlaybackInfo { | pub struct PlaybackInfo { | ||||||
|   last_updated: u128, |   last_updated: u128, | ||||||
|   position_ms: u32, |   position_ms: u32, | ||||||
| 
 | 
 | ||||||
|   pub track: Option<spotify::Track>, |   pub track: CurrentTrack, | ||||||
|   pub episode: Option<spotify::Episode>, |   pub spotify_id: SpotifyId, | ||||||
|   pub spotify_id: Option<SpotifyId>, |  | ||||||
| 
 | 
 | ||||||
|   pub duration_ms: u32, |   pub duration_ms: u32, | ||||||
|   pub is_playing: bool, |   pub is_playing: bool, | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | #[derive(Clone)] | ||||||
|  | pub enum CurrentTrack { | ||||||
|  |   Track(Track), | ||||||
|  |   Episode(Episode), | ||||||
|  | } | ||||||
|  | 
 | ||||||
| impl PlaybackInfo { | impl PlaybackInfo { | ||||||
|   /// Create a new instance of 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 { |     Self { | ||||||
|       last_updated: utils::get_time_ms(), |       last_updated: utils::get_time_ms(), | ||||||
|       track: None, |       track, | ||||||
|       episode: None, |       spotify_id, | ||||||
|       spotify_id: None, |  | ||||||
|       duration_ms, |       duration_ms, | ||||||
|       position_ms, |       position_ms, | ||||||
|       is_playing, |       is_playing, | ||||||
| @ -39,15 +52,8 @@ impl PlaybackInfo { | |||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   /// Update spotify id, track and episode
 |   /// Update spotify id, track and episode
 | ||||||
|   pub fn update_track_episode( |   pub fn update_track(&mut self, track: CurrentTrack) { | ||||||
|     &mut self, |  | ||||||
|     spotify_id: SpotifyId, |  | ||||||
|     track: Option<spotify::Track>, |  | ||||||
|     episode: Option<spotify::Episode>, |  | ||||||
|   ) { |  | ||||||
|     self.spotify_id = Some(spotify_id); |  | ||||||
|     self.track = track; |     self.track = track; | ||||||
|     self.episode = episode; |  | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   /// Get the current playback position
 |   /// Get the current playback position
 | ||||||
| @ -63,71 +69,73 @@ impl PlaybackInfo { | |||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   /// Get the name of the track or episode
 |   /// Get the name of the track or episode
 | ||||||
|   pub fn get_name(&self) -> Option<String> { |   pub fn get_name(&self) -> String { | ||||||
|     if let Some(track) = &self.track { |     match &self.track { | ||||||
|       Some(track.name.clone()) |       CurrentTrack::Track(track) => track.get_name().to_string(), | ||||||
|     } else { |       CurrentTrack::Episode(episode) => episode.get_name().to_string(), | ||||||
|       self.episode.as_ref().map(|episode| episode.name.clone()) |  | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   /// Get the artist(s) or show name of the current track
 |   /// Get the artist(s) or show name of the current track
 | ||||||
|   pub fn get_artists(&self) -> Option<String> { |   pub fn get_artists(&self) -> String { | ||||||
|     if let Some(track) = &self.track { |     match &self.track { | ||||||
|       Some( |       CurrentTrack::Track(track) => track | ||||||
|         track |         .get_artist() | ||||||
|           .artists |  | ||||||
|         .iter() |         .iter() | ||||||
|           .map(|a| a.name.clone()) |         .map(|a| a.get_name().to_string()) | ||||||
|           .collect::<Vec<String>>() |         .collect::<Vec<_>>() | ||||||
|         .join(", "), |         .join(", "), | ||||||
|       ) |       CurrentTrack::Episode(episode) => episode.get_show().get_name().to_string(), | ||||||
|     } else { |  | ||||||
|       self |  | ||||||
|         .episode |  | ||||||
|         .as_ref() |  | ||||||
|         .map(|episode| episode.show.name.clone()) |  | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   /// Get the album art url
 |   /// Get the album art url
 | ||||||
|   pub fn get_thumbnail_url(&self) -> Option<String> { |   pub fn get_thumbnail_url(&self) -> Option<String> { | ||||||
|     if let Some(track) = &self.track { |     let file_id = match &self.track { | ||||||
|       let mut images = track.album.images.clone(); |       CurrentTrack::Track(track) => { | ||||||
|       images.sort_by(|a, b| b.width.cmp(&a.width)); |         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()) |         images | ||||||
|     } else if let Some(episode) = &self.episode { |           .get(0) | ||||||
|       let mut images = episode.show.images.clone(); |           .as_ref() | ||||||
|       images.sort_by(|a, b| b.width.cmp(&a.width)); |           .map(|image| image.get_file_id()) | ||||||
| 
 |           .map(hex::encode) | ||||||
|       images.get(0).as_ref().map(|image| image.url.clone()) |  | ||||||
|     } else { |  | ||||||
|       None |  | ||||||
|       } |       } | ||||||
|  |       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.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)
 |   /// Get the type of audio (track or episode)
 | ||||||
|   #[allow(dead_code)] |   #[allow(dead_code)] | ||||||
|   pub fn get_type(&self) -> Option<String> { |   pub fn get_type(&self) -> String { | ||||||
|     if self.track.is_some() { |     match &self.track { | ||||||
|       Some("track".into()) |       CurrentTrack::Track(_) => "track".to_string(), | ||||||
|     } else if self.episode.is_some() { |       CurrentTrack::Episode(_) => "episode".to_string(), | ||||||
|       Some("episode".into()) |  | ||||||
|     } else { |  | ||||||
|       None |  | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   /// Get the public facing url of the track or episode
 |   /// Get the public facing url of the track or episode
 | ||||||
|   #[allow(dead_code)] |   #[allow(dead_code)] | ||||||
|   pub fn get_url(&self) -> Option<&str> { |   pub fn get_url(&self) -> Option<&str> { | ||||||
|     if let Some(ref track) = self.track { |     match &self.track { | ||||||
|       Some(track.external_urls.spotify.as_str()) |       CurrentTrack::Track(track) => track | ||||||
|     } else if let Some(ref episode) = self.episode { |         .get_external_id() | ||||||
|       Some(episode.external_urls.spotify.as_str()) |         .iter() | ||||||
|     } else { |         .find(|id| id.get_typ() == "spotify") | ||||||
|       None |         .map(|v| v.get_id()), | ||||||
|  |       CurrentTrack::Episode(episode) => Some(episode.get_external_url()), | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  | |||||||
| @ -1,55 +1,8 @@ | |||||||
| use std::error::Error; | use anyhow::{anyhow, Result}; | ||||||
| 
 |  | ||||||
| use librespot::core::spotify_id::SpotifyId; |  | ||||||
| use log::{error, trace}; | use log::{error, trace}; | ||||||
| use serde::Deserialize; |  | ||||||
| use serde_json::Value; | use serde_json::Value; | ||||||
| 
 | 
 | ||||||
| #[derive(Debug, Clone, Deserialize)] | pub async fn get_username(token: impl Into<String>) -> Result<String> { | ||||||
| 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<Image>, |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| #[derive(Debug, Clone, Deserialize)] |  | ||||||
| pub struct ExternalUrls { |  | ||||||
|   pub spotify: String, |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| #[derive(Debug, Clone, Deserialize)] |  | ||||||
| pub struct Track { |  | ||||||
|   pub name: String, |  | ||||||
|   pub artists: Vec<Artist>, |  | ||||||
|   pub album: Album, |  | ||||||
|   pub external_urls: ExternalUrls, |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| #[derive(Debug, Clone, Deserialize)] |  | ||||||
| pub struct Show { |  | ||||||
|   pub name: String, |  | ||||||
|   pub images: Vec<Image>, |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| #[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<String>) -> Result<String, String> { |  | ||||||
|   let token = token.into(); |   let token = token.into(); | ||||||
|   let client = reqwest::Client::new(); |   let client = reqwest::Client::new(); | ||||||
| 
 | 
 | ||||||
| @ -65,7 +18,7 @@ pub async fn get_username(token: impl Into<String>) -> Result<String, String> { | |||||||
|       Ok(response) => response, |       Ok(response) => response, | ||||||
|       Err(why) => { |       Err(why) => { | ||||||
|         error!("Failed to get username: {}", 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<String>) -> Result<String, String> { | |||||||
| 
 | 
 | ||||||
|     if response.status() != 200 { |     if response.status() != 200 { | ||||||
|       error!("Failed to get username: {}", response.status()); |       error!("Failed to get username: {}", response.status()); | ||||||
|       return Err(format!( |       return Err(anyhow!( | ||||||
|         "Failed to get track info: Invalid status code: {}", |         "Failed to get track info: Invalid status code: {}", | ||||||
|         response.status() |         response.status() | ||||||
|       )); |       )); | ||||||
| @ -86,7 +39,7 @@ pub async fn get_username(token: impl Into<String>) -> Result<String, String> { | |||||||
|       Ok(body) => body, |       Ok(body) => body, | ||||||
|       Err(why) => { |       Err(why) => { | ||||||
|         error!("Failed to parse body: {}", 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<String>) -> Result<String, String> { | |||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     error!("Missing 'id' field in body: {:#?}", body); |     error!("Missing 'id' field in body: {:#?}", body); | ||||||
|     return Err("Failed to parse body: Invalid body received".to_string()); |     return Err(anyhow!("Failed to parse body: Invalid body received")); | ||||||
|   } |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| pub async fn get_track_info( |  | ||||||
|   token: impl Into<String>, |  | ||||||
|   track: SpotifyId, |  | ||||||
| ) -> Result<Track, Box<dyn Error>> { |  | ||||||
|   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<String>, |  | ||||||
|   episode: SpotifyId, |  | ||||||
| ) -> Result<Episode, Box<dyn Error>> { |  | ||||||
|   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?); |  | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user
	 DaXcess
						DaXcess