Less Spotify API calls, rearranged some player stuff

main
DaXcess 2023-09-19 20:01:36 +02:00
parent fca55d5644
commit 2cebeb41ab
No known key found for this signature in database
GPG Key ID: CF78CC72F0FD5EAD
8 changed files with 440 additions and 581 deletions

9
Cargo.lock generated
View File

@ -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",

View File

@ -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"

View File

@ -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)
}

View File

@ -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;

View File

@ -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();
}
}

View File

@ -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);

View File

@ -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()),
}
}
}

View File

@ -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"));
}
}