mirror of
https://github.com/librespot-org/librespot.git
synced 2024-12-18 17:11:53 +00:00
Merge pull request #1084 from gdesmott/lyrics
[dev] core: spclient: parse lyrics
This commit is contained in:
commit
c14c22a5ef
13 changed files with 94 additions and 16 deletions
|
@ -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
2
Cargo.lock
generated
|
@ -1500,6 +1500,8 @@ dependencies = [
|
||||||
"librespot-protocol",
|
"librespot-protocol",
|
||||||
"log",
|
"log",
|
||||||
"protobuf",
|
"protobuf",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
"uuid",
|
"uuid",
|
||||||
]
|
]
|
||||||
|
|
|
@ -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,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()))
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)?;
|
||||||
|
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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;
|
||||||
|
|
||||||
|
|
|
@ -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
77
metadata/src/lyrics.rs
Normal 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
|
||||||
|
}
|
|
@ -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> {
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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() {
|
||||||
|
|
Loading…
Reference in a new issue