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", | ||||
| ] | ||||
| 
 | ||||
| [[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", | ||||
|  | ||||
| @ -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" | ||||
|  | ||||
| @ -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) | ||||
| } | ||||
|  | ||||
| @ -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; | ||||
| 
 | ||||
|  | ||||
| @ -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<Session>, | ||||
|   tx: Sender<PlayerCommand>, | ||||
| 
 | ||||
|   pbi: Arc<Mutex<Option<PlaybackInfo>>>, | ||||
| } | ||||
| 
 | ||||
| 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<PlayerEvent>, UnboundedReceiver<SinkEvent>), | ||||
|     ), | ||||
|     Box<dyn std::error::Error>, | ||||
|   > { | ||||
|     track: TrackHandle, | ||||
|   ) -> Result<Self> { | ||||
|     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<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, | ||||
| }; | ||||
| 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<Mutex<Call>>, | ||||
|   track: Option<TrackHandle>, | ||||
| 
 | ||||
|   playback_info: Option<PlaybackInfo>, | ||||
|   player: Option<Player>, | ||||
| 
 | ||||
|   disconnect_handle: Option<tokio::task::JoinHandle<()>>, | ||||
| 
 | ||||
|   spirc: Option<Spirc>, | ||||
| 
 | ||||
|   player: Option<Player>, | ||||
| 
 | ||||
|   /// 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::<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
 | ||||
|   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<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>> { | ||||
| @ -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); | ||||
|  | ||||
| @ -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<spotify::Track>, | ||||
|   pub episode: Option<spotify::Episode>, | ||||
|   pub spotify_id: Option<SpotifyId>, | ||||
|   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<spotify::Track>, | ||||
|     episode: Option<spotify::Episode>, | ||||
|   ) { | ||||
|     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<String> { | ||||
|     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<String> { | ||||
|     if let Some(track) = &self.track { | ||||
|       Some( | ||||
|         track | ||||
|           .artists | ||||
|           .iter() | ||||
|           .map(|a| a.name.clone()) | ||||
|           .collect::<Vec<String>>() | ||||
|           .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::<Vec<_>>() | ||||
|         .join(", "), | ||||
|       CurrentTrack::Episode(episode) => episode.get_show().get_name().to_string(), | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   /// Get the album art url
 | ||||
|   pub fn get_thumbnail_url(&self) -> Option<String> { | ||||
|     if let Some(track) = &self.track { | ||||
|       let mut images = track.album.images.clone(); | ||||
|       images.sort_by(|a, b| b.width.cmp(&a.width)); | ||||
|     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<String> { | ||||
|     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()), | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
| @ -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<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> { | ||||
| pub async fn get_username(token: impl Into<String>) -> Result<String> { | ||||
|   let token = token.into(); | ||||
|   let client = reqwest::Client::new(); | ||||
| 
 | ||||
| @ -65,7 +18,7 @@ pub async fn get_username(token: impl Into<String>) -> Result<String, String> { | ||||
|       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<String>) -> Result<String, String> { | ||||
| 
 | ||||
|     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<String>) -> Result<String, String> { | ||||
|       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<String>) -> Result<String, String> { | ||||
|     } | ||||
| 
 | ||||
|     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<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?); | ||||
|     return Err(anyhow!("Failed to parse body: Invalid body received")); | ||||
|   } | ||||
| } | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user
	 DaXcess
						DaXcess