Initial v2.2.0 push

This commit is contained in:
DaXcess
2024-08-12 16:14:24 +02:00
parent d1c01e9274
commit 97c18385dd
97 changed files with 7252 additions and 5211 deletions
+18
View File
@@ -0,0 +1,18 @@
[package]
name = "spoticord_player"
version = "2.2.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
spoticord_audio = { path = "../spoticord_audio" }
spoticord_utils = { path = "../spoticord_utils" }
librespot = { git = "https://github.com/SpoticordMusic/librespot.git", version = "0.5.0-dev", default-features = false }
songbird = { version = "0.4.3", features = ["simd-json"] }
tokio = { version = "1.39.2", features = ["full"] }
anyhow = "1.0.86"
log = "0.4.22"
symphonia = { version = "0.5.4", default-features = false, features = ["pcm"] }
hex = "0.4.3"
+126
View File
@@ -0,0 +1,126 @@
use std::collections::HashSet;
use librespot::{
core::SpotifyId,
metadata::{
artist::ArtistsWithRole,
audio::{AudioItem, UniqueFields},
},
};
#[derive(Debug, Clone)]
pub struct PlaybackInfo {
audio_item: AudioItem,
updated_at: u128,
position: u32,
playing: bool,
}
impl PlaybackInfo {
pub fn new(audio_item: AudioItem, position: u32, playing: bool) -> Self {
Self {
audio_item,
updated_at: spoticord_utils::get_time(),
position,
playing,
}
}
pub fn track_id(&self) -> SpotifyId {
self.audio_item.track_id
}
pub fn track_id_string(&self) -> String {
self.audio_item
.track_id
.to_base62()
.expect("invalid spotify id")
}
pub fn name(&self) -> String {
self.audio_item.name.clone()
}
pub fn artists(&self) -> Option<ArtistsWithRole> {
let artists = match &self.audio_item.unique_fields {
UniqueFields::Track { artists, .. } => artists.clone().0,
UniqueFields::Episode { .. } => None?,
};
let mut seen = HashSet::new();
let artists = artists
.into_iter()
.filter(|item| seen.insert(item.id))
.collect();
Some(ArtistsWithRole(artists))
}
pub fn show_name(&self) -> Option<String> {
match &self.audio_item.unique_fields {
UniqueFields::Episode { show_name, .. } => Some(show_name.to_string()),
UniqueFields::Track { .. } => None,
}
}
pub fn thumbnail(&self) -> String {
self.audio_item
.covers
.first()
.expect("spotify track missing cover image")
.url
.to_string()
}
pub fn duration(&self) -> u32 {
self.audio_item.duration_ms
}
pub fn url(&self) -> String {
match &self.audio_item.unique_fields {
UniqueFields::Episode { .. } => format!(
"https://open.spotify.com/episode/{}",
self.track_id_string()
),
UniqueFields::Track { .. } => {
format!("https://open.spotify.com/track/{}", self.track_id_string())
}
}
}
/// Get the current playback position, which accounts for time that may have passed since this struct was last updated
pub fn current_position(&self) -> u32 {
if self.playing {
let now = spoticord_utils::get_time();
let diff = now - self.updated_at;
self.position + diff as u32
} else {
self.position
}
}
pub fn playing(&self) -> bool {
self.playing
}
pub fn update_playback(&mut self, position: u32, playing: bool) {
self.position = position;
self.playing = playing;
self.updated_at = spoticord_utils::get_time();
}
pub fn update_track(&mut self, audio_item: AudioItem) {
self.audio_item = audio_item;
}
pub fn is_episode(&self) -> bool {
matches!(self.audio_item.unique_fields, UniqueFields::Episode { .. })
}
pub fn is_track(&self) -> bool {
matches!(self.audio_item.unique_fields, UniqueFields::Track { .. })
}
}
+315
View File
@@ -0,0 +1,315 @@
pub mod info;
use anyhow::Result;
use info::PlaybackInfo;
use librespot::{
connect::{config::ConnectConfig, spirc::Spirc},
core::{http_client::HttpClientError, Session as SpotifySession, SessionConfig},
discovery::Credentials,
metadata::Lyrics,
playback::{
config::{Bitrate, PlayerConfig, VolumeCtrl},
mixer::{self, MixerConfig},
player::{Player as SpotifyPlayer, PlayerEvent as SpotifyPlayerEvent},
},
};
use log::error;
use songbird::{input::RawAdapter, tracks::TrackHandle, Call};
use spoticord_audio::{
sink::{SinkEvent, StreamSink},
stream::Stream,
};
use std::{io::Write, sync::Arc};
use tokio::sync::{mpsc, oneshot, Mutex};
#[derive(Debug)]
enum PlayerCommand {
NextTrack,
PreviousTrack,
Pause,
Play,
GetPlaybackInfo(oneshot::Sender<Option<PlaybackInfo>>),
GetLyrics(oneshot::Sender<Option<Lyrics>>),
Shutdown,
}
#[derive(Debug)]
pub enum PlayerEvent {
Pause,
Play,
Stopped,
TrackChanged(Box<PlaybackInfo>),
}
pub struct Player {
session: SpotifySession,
spirc: Spirc,
track: TrackHandle,
stream: Stream,
playback_info: Option<PlaybackInfo>,
// Communication
events: mpsc::Sender<PlayerEvent>,
commands: mpsc::Receiver<PlayerCommand>,
spotify_events: mpsc::UnboundedReceiver<SpotifyPlayerEvent>,
sink_events: mpsc::UnboundedReceiver<SinkEvent>,
}
impl Player {
pub async fn create(
credentials: Credentials,
call: Arc<Mutex<Call>>,
device_name: impl Into<String>,
) -> Result<(PlayerHandle, mpsc::Receiver<PlayerEvent>)> {
let (event_tx, event_rx) = mpsc::channel(16);
let mut call_lock = call.lock().await;
let stream = Stream::new();
// Create songbird audio track
let adapter = RawAdapter::new(stream.clone(), 44100, 2);
let track = call_lock.play_only_input(adapter.into());
track.pause()?;
// Free call lock before creating session
drop(call_lock);
// Create librespot audio streamer
let session = SpotifySession::new(SessionConfig::default(), None);
let mixer = (mixer::find(Some("softvol")).expect("missing softvol mixer"))(MixerConfig {
volume_ctrl: VolumeCtrl::Log(VolumeCtrl::DEFAULT_DB_RANGE),
..Default::default()
});
let (tx_sink, rx_sink) = mpsc::unbounded_channel();
let player = SpotifyPlayer::new(
PlayerConfig {
// 96kbps causes audio key errors, so enjoy the quality upgrade
bitrate: Bitrate::Bitrate160,
..Default::default()
},
session.clone(),
mixer.get_soft_volume(),
{
let stream = stream.clone();
move || Box::new(StreamSink::new(stream, tx_sink))
},
);
let rx_player = player.get_player_event_channel();
let (spirc, spirc_task) = Spirc::new(
ConnectConfig {
name: device_name.into(),
initial_volume: Some((0.75f32 * u16::MAX as f32) as u16),
..Default::default()
},
session.clone(),
credentials,
player,
mixer,
)
.await?;
let (tx, rx) = mpsc::channel(16);
let player = Self {
session,
spirc,
track,
stream,
playback_info: None,
events: event_tx,
commands: rx,
spotify_events: rx_player,
sink_events: rx_sink,
};
// Launch it all!
tokio::spawn(spirc_task);
tokio::spawn(player.run());
Ok((PlayerHandle { commands: tx }, event_rx))
}
async fn run(mut self) {
loop {
tokio::select! {
opt_command = self.commands.recv() => {
let command = match opt_command {
Some(command) => command,
None => break,
};
self.handle_command(command).await;
},
Some(event) = self.spotify_events.recv() => {
self.handle_spotify_event(event).await;
},
Some(event) = self.sink_events.recv() => {
self.handle_sink_event(event).await;
}
else => break,
}
}
}
async fn handle_command(&mut self, command: PlayerCommand) {
match command {
PlayerCommand::NextTrack => _ = self.spirc.next(),
PlayerCommand::PreviousTrack => _ = self.spirc.prev(),
PlayerCommand::Pause => _ = self.spirc.pause(),
PlayerCommand::Play => _ = self.spirc.play(),
PlayerCommand::GetPlaybackInfo(tx) => _ = tx.send(self.playback_info.clone()),
PlayerCommand::GetLyrics(tx) => self.get_lyrics(tx).await,
PlayerCommand::Shutdown => self.commands.close(),
};
}
async fn handle_spotify_event(&mut self, event: SpotifyPlayerEvent) {
match event {
SpotifyPlayerEvent::PositionCorrection { position_ms, .. }
| SpotifyPlayerEvent::Seeked { position_ms, .. } => {
if let Some(playback_info) = self.playback_info.as_mut() {
playback_info.update_playback(position_ms, true);
}
}
SpotifyPlayerEvent::Playing { position_ms, .. } => {
_ = self.events.send(PlayerEvent::Play).await;
if let Some(playback_info) = self.playback_info.as_mut() {
playback_info.update_playback(position_ms, true);
}
}
SpotifyPlayerEvent::Paused { position_ms, .. } => {
_ = self.events.send(PlayerEvent::Pause).await;
if let Some(playback_info) = self.playback_info.as_mut() {
playback_info.update_playback(position_ms, false);
}
}
SpotifyPlayerEvent::Stopped { .. } | SpotifyPlayerEvent::SessionDisconnected { .. } => {
if let Err(why) = self.track.pause() {
error!("Failed to pause songbird track: {why}");
}
_ = self.events.send(PlayerEvent::Pause).await;
self.playback_info = None;
}
SpotifyPlayerEvent::TrackChanged { audio_item } => {
if let Some(playback_info) = self.playback_info.as_mut() {
playback_info.update_track(*audio_item);
} else {
self.playback_info = Some(PlaybackInfo::new(*audio_item, 0, false));
}
_ = self
.events
.send(PlayerEvent::TrackChanged(Box::new(
self.playback_info.clone().expect("playback info is None"),
)))
.await;
}
_ => {}
}
}
async fn handle_sink_event(&self, event: SinkEvent) {
if let SinkEvent::Start = event {
if let Err(why) = self.track.play() {
error!("Failed to resume songbird track: {why}");
}
}
}
/// Grab the lyrics for the current active track from Spotify.
///
/// This might return None if nothing is being played, or the current song does not have any lyrics.
async fn get_lyrics(&self, tx: oneshot::Sender<Option<Lyrics>>) {
let Some(playback_info) = &self.playback_info else {
_ = tx.send(None);
return;
};
let lyrics = match Lyrics::get(&self.session, &playback_info.track_id()).await {
Ok(lyrics) => lyrics,
Err(why) => {
// Ignore 404 errors
match why.error.downcast_ref::<HttpClientError>() {
Some(HttpClientError::StatusCode(code)) if code.as_u16() == 404 => {}
_ => error!("Failed to get lyrics: {why}"),
}
_ = tx.send(None);
return;
}
};
_ = tx.send(Some(lyrics));
}
}
impl Drop for Player {
fn drop(&mut self) {
_ = self.spirc.shutdown();
_ = self.stream.flush();
}
}
#[derive(Clone, Debug)]
pub struct PlayerHandle {
commands: mpsc::Sender<PlayerCommand>,
}
impl PlayerHandle {
pub fn is_valid(&self) -> bool {
!self.commands.is_closed()
}
pub async fn next_track(&self) {
_ = self.commands.send(PlayerCommand::NextTrack).await;
}
pub async fn previous_track(&self) {
_ = self.commands.send(PlayerCommand::PreviousTrack).await;
}
pub async fn pause(&self) {
_ = self.commands.send(PlayerCommand::Pause).await;
}
pub async fn play(&self) {
_ = self.commands.send(PlayerCommand::Play).await;
}
pub async fn playback_info(&self) -> Result<Option<PlaybackInfo>> {
let (tx, rx) = oneshot::channel();
self.commands
.send(PlayerCommand::GetPlaybackInfo(tx))
.await?;
Ok(rx.await?)
}
pub async fn get_lyrics(&self) -> Result<Option<Lyrics>> {
let (tx, rx) = oneshot::channel();
self.commands.send(PlayerCommand::GetLyrics(tx)).await?;
Ok(rx.await?)
}
pub async fn shutdown(&self) {
_ = self.commands.send(PlayerCommand::Shutdown).await;
}
}