Merge pull request #1084 from gdesmott/lyrics

[dev] core: spclient: parse lyrics
This commit is contained in:
Roderick van Domburg 2023-01-04 20:25:26 +01:00 committed by GitHub
commit c14c22a5ef
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 94 additions and 16 deletions

View file

@ -94,6 +94,7 @@ https://github.com/librespot-org/librespot
Connect should use the 'filter-explicit-content' user attribute in the session. Connect should use the 'filter-explicit-content' user attribute in the session.
- [playback] Add metadata support via a `TrackChanged` event - [playback] Add metadata support via a `TrackChanged` event
- [connect] Add `activate` and `load` functions to `Spirc`, allowing control over local connect sessions - [connect] Add `activate` and `load` functions to `Spirc`, allowing control over local connect sessions
- [metadata] Add `Lyrics`
### Fixed ### Fixed

2
Cargo.lock generated
View file

@ -1500,6 +1500,8 @@ dependencies = [
"librespot-protocol", "librespot-protocol",
"log", "log",
"protobuf", "protobuf",
"serde",
"serde_json",
"thiserror", "thiserror",
"uuid", "uuid",
] ]

View file

@ -1138,7 +1138,7 @@ impl SpircTask {
self.state.set_status(PlayStatus::kPlayStatusPlay); self.state.set_status(PlayStatus::kPlayStatusPlay);
self.update_state_position(position_ms); self.update_state_position(position_ms);
self.play_status = SpircPlayStatus::Playing { self.play_status = SpircPlayStatus::Playing {
nominal_start_time: self.now_ms() as i64 - position_ms as i64, nominal_start_time: self.now_ms() - position_ms as i64,
preloading_of_next_track_triggered, preloading_of_next_track_triggered,
}; };
} }

View file

@ -173,5 +173,5 @@ where
D: serde::Deserializer<'de>, D: serde::Deserializer<'de>,
{ {
let v: String = serde::Deserialize::deserialize(de)?; let v: String = serde::Deserialize::deserialize(de)?;
base64::decode(&v).map_err(|e| serde::de::Error::custom(e.to_string())) base64::decode(v).map_err(|e| serde::de::Error::custom(e.to_string()))
} }

View file

@ -61,7 +61,7 @@ impl RequestHandler {
} }
fn handle_get_info(&self) -> Response<hyper::Body> { fn handle_get_info(&self) -> Response<hyper::Body> {
let public_key = base64::encode(&self.keys.public_key()); let public_key = base64::encode(self.keys.public_key());
let device_type: &str = self.config.device_type.into(); let device_type: &str = self.config.device_type.into();
let mut active_user = String::new(); let mut active_user = String::new();
if let Some(username) = &self.username { if let Some(username) = &self.username {
@ -139,7 +139,7 @@ impl RequestHandler {
let encrypted = &encrypted_blob[16..encrypted_blob_len - 20]; let encrypted = &encrypted_blob[16..encrypted_blob_len - 20];
let cksum = &encrypted_blob[encrypted_blob_len - 20..encrypted_blob_len]; let cksum = &encrypted_blob[encrypted_blob_len - 20..encrypted_blob_len];
let base_key = Sha1::digest(&shared_key); let base_key = Sha1::digest(shared_key);
let base_key = &base_key[..16]; let base_key = &base_key[..16];
let checksum_key = { let checksum_key = {
@ -179,7 +179,7 @@ impl RequestHandler {
data data
}; };
let credentials = Credentials::with_blob(username, &decrypted, &self.config.device_id)?; let credentials = Credentials::with_blob(username, decrypted, &self.config.device_id)?;
self.tx.send(credentials)?; self.tx.send(credentials)?;

View file

@ -16,6 +16,8 @@ log = "0.4"
protobuf = "2" protobuf = "2"
thiserror = "1" thiserror = "1"
uuid = { version = "1", default-features = false } uuid = { version = "1", default-features = false }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
[dependencies.librespot-core] [dependencies.librespot-core]
path = "../core" path = "../core"

View file

@ -109,7 +109,7 @@ impl AudioItem {
) )
}; };
let popularity = track.popularity.max(0).min(100) as u8; let popularity = track.popularity.clamp(0, 100) as u8;
let number = track.number.max(0) as u32; let number = track.number.max(0) as u32;
let disc_number = track.disc_number.max(0) as u32; let disc_number = track.disc_number.max(0) as u32;

View file

@ -18,6 +18,7 @@ pub mod episode;
pub mod error; pub mod error;
pub mod external_id; pub mod external_id;
pub mod image; pub mod image;
pub mod lyrics;
pub mod playlist; pub mod playlist;
mod request; mod request;
pub mod restriction; pub mod restriction;
@ -33,6 +34,7 @@ use request::RequestResult;
pub use album::Album; pub use album::Album;
pub use artist::Artist; pub use artist::Artist;
pub use episode::Episode; pub use episode::Episode;
pub use lyrics::Lyrics;
pub use playlist::Playlist; pub use playlist::Playlist;
pub use show::Show; pub use show::Show;
pub use track::Track; pub use track::Track;

77
metadata/src/lyrics.rs Normal file
View file

@ -0,0 +1,77 @@
use bytes::Bytes;
use librespot_core::{Error, FileId, Session, SpotifyId};
impl Lyrics {
pub async fn get(session: &Session, id: &SpotifyId) -> Result<Self, Error> {
let spclient = session.spclient();
let lyrics = spclient.get_lyrics(id).await?;
Self::try_from(&lyrics)
}
pub async fn get_for_image(
session: &Session,
id: &SpotifyId,
image_id: &FileId,
) -> Result<Self, Error> {
let spclient = session.spclient();
let lyrics = spclient.get_lyrics_for_image(id, image_id).await?;
Self::try_from(&lyrics)
}
}
impl TryFrom<&Bytes> for Lyrics {
type Error = Error;
fn try_from(lyrics: &Bytes) -> Result<Self, Self::Error> {
serde_json::from_slice(lyrics).map_err(|err| err.into())
}
}
#[derive(Debug, Clone, PartialEq, Eq, serde::Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Lyrics {
pub colors: Colors,
pub has_vocal_removal: bool,
pub lyrics: LyricsInner,
}
#[derive(Debug, Clone, PartialEq, Eq, serde::Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Colors {
pub background: i32,
pub highlight_text: i32,
pub text: i32,
}
#[derive(Debug, Clone, PartialEq, Eq, serde::Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct LyricsInner {
// TODO: 'alternatives' field as an array but I don't know what it's meant for
pub fullscreen_action: String,
pub is_dense_typeface: bool,
pub is_rtl_language: bool,
pub language: String,
pub lines: Vec<Line>,
pub provider: String,
pub provider_display_name: String,
pub provider_lyrics_id: String,
pub sync_lyrics_uri: String,
pub sync_type: SyncType,
}
#[derive(Debug, Clone, PartialEq, Eq, serde::Deserialize)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum SyncType {
Unsynced,
LineSynced,
}
#[derive(Debug, Clone, PartialEq, Eq, serde::Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Line {
pub start_time_ms: String,
pub end_time_ms: String,
pub words: String,
// TODO: 'syllables' array
}

View file

@ -76,13 +76,7 @@ impl Converter {
let min = -factor; let min = -factor;
let max = factor - 1.0; let max = factor - 1.0;
if int_value < min { int_value.clamp(min, max)
min
} else if int_value > max {
max
} else {
int_value
}
} }
pub fn f64_to_f32(&mut self, samples: &[f64]) -> Vec<f32> { pub fn f64_to_f32(&mut self, samples: &[f64]) -> Vec<f32> {

View file

@ -15,6 +15,6 @@ pub mod player;
pub const SAMPLE_RATE: u32 = 44100; pub const SAMPLE_RATE: u32 = 44100;
pub const NUM_CHANNELS: u8 = 2; pub const NUM_CHANNELS: u8 = 2;
pub const SAMPLES_PER_SECOND: u32 = SAMPLE_RATE as u32 * NUM_CHANNELS as u32; pub const SAMPLES_PER_SECOND: u32 = SAMPLE_RATE * NUM_CHANNELS as u32;
pub const PAGES_PER_MS: f64 = SAMPLE_RATE as f64 / 1000.0; pub const PAGES_PER_MS: f64 = SAMPLE_RATE as f64 / 1000.0;
pub const MS_PER_PAGE: f64 = 1000.0 / SAMPLE_RATE as f64; pub const MS_PER_PAGE: f64 = 1000.0 / SAMPLE_RATE as f64;

View file

@ -1311,7 +1311,7 @@ impl Future for PlayerInternal {
self.send_event(PlayerEvent::PositionCorrection { self.send_event(PlayerEvent::PositionCorrection {
play_request_id, play_request_id,
track_id, track_id,
position_ms: new_stream_position_ms as u32, position_ms: new_stream_position_ms,
}); });
} }
} }

View file

@ -9,7 +9,7 @@ fn out_dir() -> PathBuf {
} }
fn cleanup() { fn cleanup() {
let _ = fs::remove_dir_all(&out_dir()); let _ = fs::remove_dir_all(out_dir());
} }
fn compile() { fn compile() {