Less Spotify API calls, rearranged some player stuff
parent
fca55d5644
commit
2cebeb41ab
|
@ -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…
Reference in New Issue