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