mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-10-21 15:02:21 +02:00
refactor(core): move the lenient Go-style JSON decoder to core::json
The case-folding, null-tolerant struct decoder (`go_deserialize!` with `field_name`, `KeySeed`, `Update` and `Field`) lived in `core::lyrics::json` although downloads, metadata and resolver payloads depend on it. Move it unchanged to a neutral `core::json` module and name the macro after its behavior, `lenient_deserialize!`. All users are inside spotiflac-core, so no compatibility re-export is kept. The lyrics module keeps only its surrogate-normalizing `decode` helper. `field_name` is now public so the extension registry decoder reuses it instead of its own copy of the same folding. Its matcher folds the key once and compares ASCII case-insensitively against the wire name, which a new test checks against the previous matcher on edge inputs (empty, long s, Kelvin sign, combining marks, non-ASCII letters, BOM, NUL). The visitor's `expecting` text is unchanged so decode errors stay byte-identical.
This commit is contained in:
1 parent
ff3f752014
commit
088bd875dd
16 files changed
+340
-245
No files matched your search
@@ -65,7 +65,7 @@ pub struct DownloadRequest {
|
||||
pub network_concurrency_limit: i64,
|
||||
}
|
||||
|
||||
crate::lyrics::json::go_deserialize!(DownloadRequest {
|
||||
crate::json::lenient_deserialize!(DownloadRequest {
|
||||
"contract_version" => contract_version, "isrc" => isrc, "service" => service,
|
||||
"download_provider" => download_provider, "provider_track_id" => provider_track_id,
|
||||
"spotify_id" => spotify_id, "track_name" => track_name, "artist_name" => artist_name,
|
||||
|
||||
@@ -0,0 +1,160 @@
|
||||
//! Lenient typed JSON decoding with Go's `encoding/json` struct rules: object
|
||||
//! keys match fields case-insensitively (with Go's `ſ` and Kelvin sign folding),
|
||||
//! a later duplicate key wins, and a null scalar keeps the earlier value.
|
||||
|
||||
use serde::de::{DeserializeSeed, Visitor};
|
||||
use serde::{Deserialize, Deserializer};
|
||||
use std::borrow::Cow;
|
||||
|
||||
/// Folds a JSON object key the way Go matches it against struct field names.
|
||||
pub fn field_name(name: &str) -> String {
|
||||
name.chars()
|
||||
.map(|ch| match ch {
|
||||
'ſ' => 's',
|
||||
'\u{212A}' => 'k', // KELVIN SIGN
|
||||
_ => ch.to_ascii_lowercase(),
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
struct KeyVisitor;
|
||||
|
||||
impl<'de> Visitor<'de> for KeyVisitor {
|
||||
type Value = Cow<'de, str>;
|
||||
|
||||
fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
formatter.write_str("a string key")
|
||||
}
|
||||
|
||||
fn visit_borrowed_str<E: serde::de::Error>(self, value: &'de str) -> Result<Self::Value, E> {
|
||||
Ok(Cow::Borrowed(value))
|
||||
}
|
||||
|
||||
fn visit_str<E: serde::de::Error>(self, value: &str) -> Result<Self::Value, E> {
|
||||
Ok(Cow::Owned(value.into()))
|
||||
}
|
||||
|
||||
fn visit_string<E: serde::de::Error>(self, value: String) -> Result<Self::Value, E> {
|
||||
Ok(Cow::Owned(value))
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct KeySeed;
|
||||
|
||||
impl<'de> DeserializeSeed<'de> for KeySeed {
|
||||
type Value = Cow<'de, str>;
|
||||
|
||||
fn deserialize<D: Deserializer<'de>>(self, decoder: D) -> Result<Self::Value, D::Error> {
|
||||
decoder.deserialize_str(KeyVisitor)
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) trait Update<'de> {
|
||||
fn update<D: Deserializer<'de>>(&mut self, decoder: D) -> Result<(), D::Error>;
|
||||
}
|
||||
|
||||
macro_rules! scalar {
|
||||
($($type:ty),*) => {$(
|
||||
impl<'de> Update<'de> for $type {
|
||||
fn update<D: Deserializer<'de>>(&mut self, decoder: D) -> Result<(), D::Error> {
|
||||
// A later null does not overwrite an already decoded scalar.
|
||||
if let Some(value) = Option::<Self>::deserialize(decoder)? { *self = value; }
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
)*};
|
||||
}
|
||||
scalar!(String, bool, i64, isize, f64);
|
||||
|
||||
impl<'de, T: Deserialize<'de>> Update<'de> for Option<T> {
|
||||
fn update<D: Deserializer<'de>>(&mut self, decoder: D) -> Result<(), D::Error> {
|
||||
*self = Self::deserialize(decoder)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct Field<'a, T>(pub &'a mut T);
|
||||
|
||||
impl<'de, T: Update<'de>> DeserializeSeed<'de> for Field<'_, T> {
|
||||
type Value = ();
|
||||
fn deserialize<D: Deserializer<'de>>(self, decoder: D) -> Result<(), D::Error> {
|
||||
self.0.update(decoder)
|
||||
}
|
||||
}
|
||||
|
||||
// The `expecting` text predates this module's move out of the lyrics code and
|
||||
// is kept so decode error messages stay byte-identical.
|
||||
macro_rules! lenient_deserialize {
|
||||
($name:ident { $($key:literal => $field:ident,)* }) => {
|
||||
impl<'de> serde::Deserialize<'de> for $name {
|
||||
fn deserialize<D: serde::Deserializer<'de>>(decoder: D) -> Result<Self, D::Error> {
|
||||
let mut result = Self::default();
|
||||
$crate::json::Update::update(&mut result, decoder)?;
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
impl<'de> $crate::json::Update<'de> for $name {
|
||||
fn update<D: serde::Deserializer<'de>>(&mut self, decoder: D) -> Result<(), D::Error> {
|
||||
struct Visitor<'a>(&'a mut $name);
|
||||
impl<'de> serde::de::Visitor<'de> for Visitor<'_> {
|
||||
type Value = ();
|
||||
fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
formatter.write_str("a lyrics object or null")
|
||||
}
|
||||
fn visit_unit<E: serde::de::Error>(self) -> Result<(), E> {
|
||||
Ok(())
|
||||
}
|
||||
fn visit_map<M: serde::de::MapAccess<'de>>(self, mut map: M) -> Result<(), M::Error> {
|
||||
while let Some(key) = map.next_key_seed($crate::json::KeySeed)? {
|
||||
let raw_key = key.as_ref();
|
||||
let folded;
|
||||
let lookup = match raw_key {
|
||||
$($key => raw_key,)*
|
||||
_ => {
|
||||
folded = $crate::json::field_name(raw_key);
|
||||
folded.as_str()
|
||||
}
|
||||
};
|
||||
match lookup {
|
||||
$($key => map.next_value_seed($crate::json::Field(&mut self.0.$field))?,)*
|
||||
_ => { map.next_value::<serde::de::IgnoredAny>()?; }
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
decoder.deserialize_any(Visitor(self))
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
pub(crate) use lenient_deserialize;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn key_seed_borrows_canonical_keys_and_owns_escaped_keys() {
|
||||
let mut decoder = serde_json::Deserializer::from_str(r#""source""#);
|
||||
assert!(matches!(
|
||||
KeySeed.deserialize(&mut decoder).unwrap(),
|
||||
Cow::Borrowed("source")
|
||||
));
|
||||
|
||||
// `"so` + backslash + `u0075rce"`: the key spells `u` as a JSON escape.
|
||||
let escaped = concat!(r#""so\"#, r#"u0075rce""#);
|
||||
let mut decoder = serde_json::Deserializer::from_str(escaped);
|
||||
assert!(matches!(
|
||||
KeySeed.deserialize(&mut decoder).unwrap(),
|
||||
Cow::Owned(value) if value == "source"
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn field_name_folds_go_special_letters() {
|
||||
assert_eq!(field_name("ſOURCE"), "source");
|
||||
assert_eq!(field_name("\u{212A}"), "k");
|
||||
assert_eq!(field_name("K"), "k");
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,7 @@ pub mod cue;
|
||||
pub mod downloads;
|
||||
pub mod filename;
|
||||
pub mod isrc;
|
||||
pub mod json;
|
||||
pub mod lyrics;
|
||||
pub mod matching;
|
||||
pub mod media;
|
||||
|
||||
@@ -10,6 +10,7 @@ pub mod matching;
|
||||
pub mod models;
|
||||
pub mod payloads;
|
||||
|
||||
use crate::json::lenient_deserialize;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// HTTP providers use Go's streaming JSON decoder: decode the first value,
|
||||
@@ -49,7 +50,7 @@ pub struct LyricsWord {
|
||||
pub end_time_ms: i64,
|
||||
}
|
||||
|
||||
json::go_deserialize!(LyricsWord {
|
||||
lenient_deserialize!(LyricsWord {
|
||||
"text" => text,
|
||||
"starttimems" => start_time_ms,
|
||||
"endtimems" => end_time_ms,
|
||||
@@ -69,7 +70,7 @@ pub struct LyricsLine {
|
||||
pub translation: Option<String>,
|
||||
}
|
||||
|
||||
json::go_deserialize!(LyricsLine {
|
||||
lenient_deserialize!(LyricsLine {
|
||||
"starttimems" => start_time_ms,
|
||||
"words" => words,
|
||||
"endtimems" => end_time_ms,
|
||||
@@ -90,7 +91,7 @@ pub struct LyricsResponse {
|
||||
pub source: String,
|
||||
}
|
||||
|
||||
json::go_deserialize!(LyricsResponse {
|
||||
lenient_deserialize!(LyricsResponse {
|
||||
"lines" => lines,
|
||||
"synctype" => sync_type,
|
||||
"instrumental" => instrumental,
|
||||
|
||||
@@ -36,7 +36,7 @@ pub struct FetchOptions {
|
||||
pub musixmatch_language: String,
|
||||
}
|
||||
|
||||
super::json::go_deserialize!(FetchOptions {
|
||||
crate::json::lenient_deserialize!(FetchOptions {
|
||||
"include_translation_netease" => include_translation_netease,
|
||||
"include_romanization_netease" => include_romanization_netease,
|
||||
"multi_person_word_by_word" => multi_person_word_by_word,
|
||||
@@ -60,7 +60,7 @@ impl FetchOptions {
|
||||
/// Go unmarshals partial settings into the current options. Commit only a
|
||||
/// fully valid document, including duplicate fields and null scalar values.
|
||||
pub fn update_json(&mut self, raw: &str) -> Result<(), serde_json::Error> {
|
||||
use super::json::Update;
|
||||
use crate::json::Update;
|
||||
if raw.trim().is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
@@ -1,158 +1,16 @@
|
||||
//! Go's typed provider JSON accepts case-insensitive fields and null scalars.
|
||||
//! The lenient struct decoder lives in [`crate::json`].
|
||||
|
||||
use serde::de::{DeserializeOwned, DeserializeSeed, Visitor};
|
||||
use serde::{Deserialize, Deserializer};
|
||||
use std::borrow::Cow;
|
||||
use serde::de::DeserializeOwned;
|
||||
|
||||
pub(super) fn decode<T: DeserializeOwned>(raw: &str) -> Result<T, serde_json::Error> {
|
||||
serde_json::from_str(&crate::text::json_surrogates(raw))
|
||||
}
|
||||
|
||||
pub(crate) fn field_name(name: &str) -> String {
|
||||
name.chars()
|
||||
.map(|ch| match ch {
|
||||
'ſ' => 's',
|
||||
'K' => 'k',
|
||||
_ => ch.to_ascii_lowercase(),
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
struct KeyVisitor;
|
||||
|
||||
impl<'de> Visitor<'de> for KeyVisitor {
|
||||
type Value = Cow<'de, str>;
|
||||
|
||||
fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
formatter.write_str("a string key")
|
||||
}
|
||||
|
||||
fn visit_borrowed_str<E: serde::de::Error>(self, value: &'de str) -> Result<Self::Value, E> {
|
||||
Ok(Cow::Borrowed(value))
|
||||
}
|
||||
|
||||
fn visit_str<E: serde::de::Error>(self, value: &str) -> Result<Self::Value, E> {
|
||||
Ok(Cow::Owned(value.into()))
|
||||
}
|
||||
|
||||
fn visit_string<E: serde::de::Error>(self, value: String) -> Result<Self::Value, E> {
|
||||
Ok(Cow::Owned(value))
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct KeySeed;
|
||||
|
||||
impl<'de> DeserializeSeed<'de> for KeySeed {
|
||||
type Value = Cow<'de, str>;
|
||||
|
||||
fn deserialize<D: Deserializer<'de>>(self, decoder: D) -> Result<Self::Value, D::Error> {
|
||||
decoder.deserialize_str(KeyVisitor)
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) trait Update<'de> {
|
||||
fn update<D: Deserializer<'de>>(&mut self, decoder: D) -> Result<(), D::Error>;
|
||||
}
|
||||
|
||||
macro_rules! scalar {
|
||||
($($type:ty),*) => {$(
|
||||
impl<'de> Update<'de> for $type {
|
||||
fn update<D: Deserializer<'de>>(&mut self, decoder: D) -> Result<(), D::Error> {
|
||||
// A later null does not overwrite an already decoded scalar.
|
||||
if let Some(value) = Option::<Self>::deserialize(decoder)? { *self = value; }
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
)*};
|
||||
}
|
||||
scalar!(String, bool, i64, isize, f64);
|
||||
|
||||
impl<'de, T: Deserialize<'de>> Update<'de> for Option<T> {
|
||||
fn update<D: Deserializer<'de>>(&mut self, decoder: D) -> Result<(), D::Error> {
|
||||
*self = Self::deserialize(decoder)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct Field<'a, T>(pub &'a mut T);
|
||||
|
||||
impl<'de, T: Update<'de>> DeserializeSeed<'de> for Field<'_, T> {
|
||||
type Value = ();
|
||||
fn deserialize<D: Deserializer<'de>>(self, decoder: D) -> Result<(), D::Error> {
|
||||
self.0.update(decoder)
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! go_deserialize {
|
||||
($name:ident { $($key:literal => $field:ident,)* }) => {
|
||||
impl<'de> serde::Deserialize<'de> for $name {
|
||||
fn deserialize<D: serde::Deserializer<'de>>(decoder: D) -> Result<Self, D::Error> {
|
||||
let mut result = Self::default();
|
||||
$crate::lyrics::json::Update::update(&mut result, decoder)?;
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
impl<'de> $crate::lyrics::json::Update<'de> for $name {
|
||||
fn update<D: serde::Deserializer<'de>>(&mut self, decoder: D) -> Result<(), D::Error> {
|
||||
struct Visitor<'a>(&'a mut $name);
|
||||
impl<'de> serde::de::Visitor<'de> for Visitor<'_> {
|
||||
type Value = ();
|
||||
fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
formatter.write_str("a lyrics object or null")
|
||||
}
|
||||
fn visit_unit<E: serde::de::Error>(self) -> Result<(), E> {
|
||||
Ok(())
|
||||
}
|
||||
fn visit_map<M: serde::de::MapAccess<'de>>(self, mut map: M) -> Result<(), M::Error> {
|
||||
while let Some(key) = map.next_key_seed($crate::lyrics::json::KeySeed)? {
|
||||
let raw_key = key.as_ref();
|
||||
let folded;
|
||||
let lookup = match raw_key {
|
||||
$($key => raw_key,)*
|
||||
_ => {
|
||||
folded = $crate::lyrics::json::field_name(raw_key);
|
||||
folded.as_str()
|
||||
}
|
||||
};
|
||||
match lookup {
|
||||
$($key => map.next_value_seed($crate::lyrics::json::Field(&mut self.0.$field))?,)*
|
||||
_ => { map.next_value::<serde::de::IgnoredAny>()?; }
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
decoder.deserialize_any(Visitor(self))
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
pub(crate) use go_deserialize;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn key_seed_borrows_canonical_keys_and_owns_escaped_keys() {
|
||||
let mut decoder = serde_json::Deserializer::from_str(r#""source""#);
|
||||
assert!(matches!(
|
||||
KeySeed.deserialize(&mut decoder).unwrap(),
|
||||
Cow::Borrowed("source")
|
||||
));
|
||||
|
||||
let mut decoder = serde_json::Deserializer::from_str(r#""so\u0075rce""#);
|
||||
assert!(matches!(
|
||||
KeySeed.deserialize(&mut decoder).unwrap(),
|
||||
Cow::Owned(value) if value == "source"
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn field_folding_preserves_special_letters_and_null_duplicates() {
|
||||
assert_eq!(field_name("ſOURCE"), "source");
|
||||
assert_eq!(field_name("K"), "k");
|
||||
|
||||
let response: crate::lyrics::LyricsResponse = serde_json::from_str(
|
||||
r#"{"plainlyrics":"kept","PLAINLYRICS":null,"ſource":"old","SOURCE":"new"}"#,
|
||||
)
|
||||
|
||||
@@ -15,7 +15,7 @@ pub struct LrcLibResponse {
|
||||
pub synced_lyrics: String,
|
||||
}
|
||||
|
||||
super::json::go_deserialize!(LrcLibResponse {
|
||||
crate::json::lenient_deserialize!(LrcLibResponse {
|
||||
"id" => id, "name" => name, "trackname" => track_name,
|
||||
"artistname" => artist_name, "albumname" => album_name, "duration" => duration,
|
||||
"instrumental" => instrumental, "plainlyrics" => plain_lyrics,
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
//! Typed built-in search schemas, retaining Go field folding and null behavior.
|
||||
|
||||
use super::json::go_deserialize;
|
||||
use crate::json::lenient_deserialize;
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct Artist {
|
||||
pub name: String,
|
||||
}
|
||||
go_deserialize!(Artist { "name" => name, });
|
||||
lenient_deserialize!(Artist { "name" => name, });
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct SpotifySong {
|
||||
@@ -16,7 +16,7 @@ pub struct SpotifySong {
|
||||
pub artist_name: String,
|
||||
pub duration: String,
|
||||
}
|
||||
go_deserialize!(SpotifySong { "trackid" => track_id, "name" => name, "artistname" => artist_name, "duration" => duration, });
|
||||
lenient_deserialize!(SpotifySong { "trackid" => track_id, "name" => name, "artistname" => artist_name, "duration" => duration, });
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct YouTubeSong {
|
||||
@@ -25,7 +25,7 @@ pub struct YouTubeSong {
|
||||
pub author: String,
|
||||
pub duration: String,
|
||||
}
|
||||
go_deserialize!(YouTubeSong { "videoid" => video_id, "title" => title, "author" => author, "duration" => duration, });
|
||||
lenient_deserialize!(YouTubeSong { "videoid" => video_id, "title" => title, "author" => author, "duration" => duration, });
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct NeteaseSong {
|
||||
@@ -33,13 +33,13 @@ pub struct NeteaseSong {
|
||||
pub id: i64,
|
||||
pub artists: Option<Vec<Artist>>,
|
||||
}
|
||||
go_deserialize!(NeteaseSong { "name" => name, "id" => id, "artists" => artists, });
|
||||
lenient_deserialize!(NeteaseSong { "name" => name, "id" => id, "artists" => artists, });
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct NeteaseResults {
|
||||
pub songs: Option<Vec<NeteaseSong>>,
|
||||
pub song_count: isize,
|
||||
}
|
||||
go_deserialize!(NeteaseResults { "songs" => songs, "songcount" => song_count, });
|
||||
lenient_deserialize!(NeteaseResults { "songs" => songs, "songcount" => song_count, });
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct NeteaseSearch {
|
||||
pub result: NeteaseResults,
|
||||
@@ -47,12 +47,12 @@ pub struct NeteaseSearch {
|
||||
pub message: String,
|
||||
pub msg: String,
|
||||
}
|
||||
go_deserialize!(NeteaseSearch { "result" => result, "code" => code, "message" => message, "msg" => msg, });
|
||||
lenient_deserialize!(NeteaseSearch { "result" => result, "code" => code, "message" => message, "msg" => msg, });
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct LyricField {
|
||||
pub lyric: String,
|
||||
}
|
||||
go_deserialize!(LyricField { "lyric" => lyric, });
|
||||
lenient_deserialize!(LyricField { "lyric" => lyric, });
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct NeteaseLyrics {
|
||||
pub lrc: Option<LyricField>,
|
||||
@@ -60,23 +60,23 @@ pub struct NeteaseLyrics {
|
||||
pub romalrc: Option<LyricField>,
|
||||
pub code: isize,
|
||||
}
|
||||
go_deserialize!(NeteaseLyrics { "lrc" => lrc, "tlyric" => tlyric, "romalrc" => romalrc, "code" => code, });
|
||||
lenient_deserialize!(NeteaseLyrics { "lrc" => lrc, "tlyric" => tlyric, "romalrc" => romalrc, "code" => code, });
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct AppleSongId {
|
||||
pub id: String,
|
||||
}
|
||||
go_deserialize!(AppleSongId { "id" => id, });
|
||||
lenient_deserialize!(AppleSongId { "id" => id, });
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct AppleSongs {
|
||||
pub data: Option<Vec<AppleSongId>>,
|
||||
}
|
||||
go_deserialize!(AppleSongs { "data" => data, });
|
||||
lenient_deserialize!(AppleSongs { "data" => data, });
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct AppleResults {
|
||||
pub songs: Option<AppleSongs>,
|
||||
}
|
||||
go_deserialize!(AppleResults { "songs" => songs, });
|
||||
lenient_deserialize!(AppleResults { "songs" => songs, });
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct AppleAttributes {
|
||||
pub name: String,
|
||||
@@ -84,23 +84,23 @@ pub struct AppleAttributes {
|
||||
pub album_name: String,
|
||||
pub duration_in_millis: isize,
|
||||
}
|
||||
go_deserialize!(AppleAttributes { "name" => name, "artistname" => artist_name, "albumname" => album_name, "durationinmillis" => duration_in_millis, });
|
||||
lenient_deserialize!(AppleAttributes { "name" => name, "artistname" => artist_name, "albumname" => album_name, "durationinmillis" => duration_in_millis, });
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct AppleSong {
|
||||
pub attributes: AppleAttributes,
|
||||
}
|
||||
go_deserialize!(AppleSong { "attributes" => attributes, });
|
||||
lenient_deserialize!(AppleSong { "attributes" => attributes, });
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct AppleResources {
|
||||
pub songs: Option<BTreeMap<String, AppleSong>>,
|
||||
}
|
||||
go_deserialize!(AppleResources { "songs" => songs, });
|
||||
lenient_deserialize!(AppleResources { "songs" => songs, });
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct AppleSearch {
|
||||
pub results: AppleResults,
|
||||
pub resources: Option<AppleResources>,
|
||||
}
|
||||
go_deserialize!(AppleSearch { "results" => results, "resources" => resources, });
|
||||
lenient_deserialize!(AppleSearch { "results" => results, "resources" => resources, });
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct QqSong {
|
||||
@@ -110,13 +110,13 @@ pub struct QqSong {
|
||||
pub interval: isize,
|
||||
pub singer: Option<Vec<Artist>>,
|
||||
}
|
||||
go_deserialize!(QqSong { "mid" => mid, "id" => id, "name" => name, "interval" => interval, "singer" => singer, });
|
||||
lenient_deserialize!(QqSong { "mid" => mid, "id" => id, "name" => name, "interval" => interval, "singer" => singer, });
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct QqSongDetail {
|
||||
pub code: isize,
|
||||
pub data: Option<Vec<QqSong>>,
|
||||
}
|
||||
go_deserialize!(QqSongDetail { "code" => code, "data" => data, });
|
||||
lenient_deserialize!(QqSongDetail { "code" => code, "data" => data, });
|
||||
/// Search suggestions carry IDs as strings and singers joined by `/`.
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct QqSuggestion {
|
||||
@@ -125,30 +125,30 @@ pub struct QqSuggestion {
|
||||
pub name: String,
|
||||
pub singer: String,
|
||||
}
|
||||
go_deserialize!(QqSuggestion { "id" => id, "mid" => mid, "name" => name, "singer" => singer, });
|
||||
lenient_deserialize!(QqSuggestion { "id" => id, "mid" => mid, "name" => name, "singer" => singer, });
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct QqSuggestedSongs {
|
||||
pub itemlist: Option<Vec<QqSuggestion>>,
|
||||
}
|
||||
go_deserialize!(QqSuggestedSongs { "itemlist" => itemlist, });
|
||||
lenient_deserialize!(QqSuggestedSongs { "itemlist" => itemlist, });
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct QqSuggestionData {
|
||||
pub song: QqSuggestedSongs,
|
||||
}
|
||||
go_deserialize!(QqSuggestionData { "song" => song, });
|
||||
lenient_deserialize!(QqSuggestionData { "song" => song, });
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct QqSearch {
|
||||
pub code: isize,
|
||||
pub data: QqSuggestionData,
|
||||
}
|
||||
go_deserialize!(QqSearch { "code" => code, "data" => data, });
|
||||
lenient_deserialize!(QqSearch { "code" => code, "data" => data, });
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct QqLyrics {
|
||||
pub retcode: isize,
|
||||
pub code: isize,
|
||||
pub lyric: String,
|
||||
}
|
||||
go_deserialize!(QqLyrics { "retcode" => retcode, "code" => code, "lyric" => lyric, });
|
||||
lenient_deserialize!(QqLyrics { "retcode" => retcode, "code" => code, "lyric" => lyric, });
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct KugouSong {
|
||||
@@ -158,7 +158,7 @@ pub struct KugouSong {
|
||||
pub singer: String,
|
||||
pub duration: f64,
|
||||
}
|
||||
go_deserialize!(KugouSong { "id" => id, "accesskey" => accesskey, "song" => song, "singer" => singer, "duration" => duration, });
|
||||
lenient_deserialize!(KugouSong { "id" => id, "accesskey" => accesskey, "song" => song, "singer" => singer, "duration" => duration, });
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct KugouSearch {
|
||||
pub status: isize,
|
||||
@@ -166,7 +166,7 @@ pub struct KugouSearch {
|
||||
pub errmsg: String,
|
||||
pub candidates: Option<Vec<KugouSong>>,
|
||||
}
|
||||
go_deserialize!(KugouSearch { "status" => status, "errcode" => errcode, "errmsg" => errmsg, "candidates" => candidates, });
|
||||
lenient_deserialize!(KugouSearch { "status" => status, "errcode" => errcode, "errmsg" => errmsg, "candidates" => candidates, });
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct KugouLyrics {
|
||||
pub status: isize,
|
||||
@@ -174,7 +174,7 @@ pub struct KugouLyrics {
|
||||
pub info: String,
|
||||
pub content: String,
|
||||
}
|
||||
go_deserialize!(KugouLyrics { "status" => status, "error_code" => error_code, "info" => info, "content" => content, });
|
||||
lenient_deserialize!(KugouLyrics { "status" => status, "error_code" => error_code, "info" => info, "content" => content, });
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct GeniusSong {
|
||||
@@ -183,25 +183,25 @@ pub struct GeniusSong {
|
||||
pub primary_artist_names: String,
|
||||
pub url: String,
|
||||
}
|
||||
go_deserialize!(GeniusSong { "title" => title, "artist_names" => artist_names, "primary_artist_names" => primary_artist_names, "url" => url, });
|
||||
lenient_deserialize!(GeniusSong { "title" => title, "artist_names" => artist_names, "primary_artist_names" => primary_artist_names, "url" => url, });
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct GeniusHit {
|
||||
pub kind: String,
|
||||
pub result: GeniusSong,
|
||||
}
|
||||
go_deserialize!(GeniusHit { "type" => kind, "result" => result, });
|
||||
lenient_deserialize!(GeniusHit { "type" => kind, "result" => result, });
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct GeniusSection {
|
||||
pub hits: Option<Vec<GeniusHit>>,
|
||||
}
|
||||
go_deserialize!(GeniusSection { "hits" => hits, });
|
||||
lenient_deserialize!(GeniusSection { "hits" => hits, });
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct GeniusResults {
|
||||
pub sections: Option<Vec<GeniusSection>>,
|
||||
}
|
||||
go_deserialize!(GeniusResults { "sections" => sections, });
|
||||
lenient_deserialize!(GeniusResults { "sections" => sections, });
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct GeniusSearch {
|
||||
pub response: GeniusResults,
|
||||
}
|
||||
go_deserialize!(GeniusSearch { "response" => response, });
|
||||
lenient_deserialize!(GeniusSearch { "response" => response, });
|
||||
@@ -1,4 +1,5 @@
|
||||
use super::{LyricsResponse, LyricsWord, json, lrc};
|
||||
use crate::json::{field_name, lenient_deserialize};
|
||||
use regex::Regex;
|
||||
use serde::Serialize;
|
||||
use serde_json::value::RawValue;
|
||||
@@ -15,7 +16,7 @@ pub struct PaxDetail {
|
||||
pub timestamp: Option<isize>,
|
||||
pub endtime: Option<isize>,
|
||||
}
|
||||
json::go_deserialize!(PaxDetail {
|
||||
lenient_deserialize!(PaxDetail {
|
||||
"text" => text, "part" => part, "timestamp" => timestamp, "endtime" => endtime,
|
||||
});
|
||||
|
||||
@@ -33,7 +34,7 @@ pub struct PaxLine {
|
||||
#[serde(skip_serializing_if = "String::is_empty")]
|
||||
pub agent: String,
|
||||
}
|
||||
json::go_deserialize!(PaxLine {
|
||||
lenient_deserialize!(PaxLine {
|
||||
"text" => text, "timestamp" => timestamp, "oppositeturn" => opposite_turn,
|
||||
"background" => background, "backgroundtext" => background_text, "endtime" => endtime,
|
||||
"backgroundlines" => background_lines,
|
||||
@@ -45,13 +46,13 @@ struct AppleAgent {
|
||||
id: String,
|
||||
kind: String,
|
||||
}
|
||||
json::go_deserialize!(AppleAgent { "id" => id, "type" => kind, });
|
||||
lenient_deserialize!(AppleAgent { "id" => id, "type" => kind, });
|
||||
|
||||
#[derive(Default)]
|
||||
struct AppleMetadata {
|
||||
agents: Option<Vec<AppleAgent>>,
|
||||
}
|
||||
json::go_deserialize!(AppleMetadata { "agents" => agents, });
|
||||
lenient_deserialize!(AppleMetadata { "agents" => agents, });
|
||||
|
||||
#[derive(Default)]
|
||||
struct ApplePayload {
|
||||
@@ -63,7 +64,7 @@ struct ApplePayload {
|
||||
ttml_content: String,
|
||||
metadata: Option<AppleMetadata>,
|
||||
}
|
||||
json::go_deserialize!(ApplePayload {
|
||||
lenient_deserialize!(ApplePayload {
|
||||
"type" => kind, "content" => content, "elrc" => elrc,
|
||||
"elrcmultiperson" => elrc_multi_person, "plain" => plain, "ttmlcontent" => ttml_content,
|
||||
"metadata" => metadata,
|
||||
@@ -168,7 +169,7 @@ struct ProxyPayload {
|
||||
lyrics_text: String,
|
||||
plain_lyrics: String,
|
||||
}
|
||||
json::go_deserialize!(ProxyPayload {
|
||||
lenient_deserialize!(ProxyPayload {
|
||||
"type" => kind, "content" => content, "lyrics" => lyrics,
|
||||
"lyrics_text" => lyrics_text, "plain_lyrics" => plain_lyrics,
|
||||
});
|
||||
@@ -204,7 +205,7 @@ pub fn format_pax_content(
|
||||
if index > 0 {
|
||||
output.push('\n');
|
||||
}
|
||||
if json::field_name(kind) == "syllable" {
|
||||
if field_name(kind) == "syllable" {
|
||||
output.push_str(&lrc::timestamp(line.timestamp as i64));
|
||||
if multi_person {
|
||||
output.push_str(if line.opposite_turn { "v2:" } else { "v1:" });
|
||||
@@ -259,7 +260,7 @@ fn format_apple_payload(
|
||||
multi_person: bool,
|
||||
word_timing: bool,
|
||||
) -> Result<String, String> {
|
||||
let kind = json::field_name(&payload.kind);
|
||||
let kind = field_name(&payload.kind);
|
||||
if kind == "unsynced" && !payload.plain.trim().is_empty() {
|
||||
return Ok(payload.plain.trim().into());
|
||||
}
|
||||
@@ -380,7 +381,7 @@ pub fn apple_supplements(raw: &str, lyrics: &mut LyricsResponse) {
|
||||
struct Payload {
|
||||
metadata: Option<Box<RawValue>>,
|
||||
}
|
||||
json::go_deserialize!(Payload { "metadata" => metadata, });
|
||||
lenient_deserialize!(Payload { "metadata" => metadata, });
|
||||
|
||||
let metadata = json::decode::<Payload>(raw)
|
||||
.ok()
|
||||
@@ -589,7 +590,7 @@ pub struct KpoeSyllable {
|
||||
pub duration: f64,
|
||||
pub is_background: bool,
|
||||
}
|
||||
json::go_deserialize!(KpoeSyllable {
|
||||
lenient_deserialize!(KpoeSyllable {
|
||||
"text" => text, "time" => time, "duration" => duration, "isbackground" => is_background,
|
||||
});
|
||||
|
||||
@@ -600,7 +601,7 @@ pub struct KpoeLine {
|
||||
pub text: String,
|
||||
pub syllabus: Option<Vec<KpoeSyllable>>,
|
||||
}
|
||||
json::go_deserialize!(KpoeLine {
|
||||
lenient_deserialize!(KpoeLine {
|
||||
"time" => time, "duration" => duration, "text" => text, "syllabus" => syllabus,
|
||||
});
|
||||
|
||||
@@ -610,13 +611,10 @@ pub struct KpoeResponse {
|
||||
pub kind: String,
|
||||
pub lyrics: Option<Vec<KpoeLine>>,
|
||||
}
|
||||
json::go_deserialize!(KpoeResponse { "type" => kind, "lyrics" => lyrics, });
|
||||
lenient_deserialize!(KpoeResponse { "type" => kind, "lyrics" => lyrics, });
|
||||
|
||||
pub fn format_kpoe(response: &KpoeResponse, multi_person: bool, word_timing: bool) -> String {
|
||||
let words = matches!(
|
||||
json::field_name(&response.kind).as_str(),
|
||||
"word" | "syllable"
|
||||
);
|
||||
let words = matches!(field_name(&response.kind).as_str(), "word" | "syllable");
|
||||
let mut lines = Vec::new();
|
||||
let append = |details: &[&KpoeSyllable]| {
|
||||
let mut result = String::new();
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use super::TrackMetadata;
|
||||
use crate::lyrics::json::go_deserialize;
|
||||
use crate::json::lenient_deserialize;
|
||||
|
||||
mod catalog;
|
||||
pub use catalog::*;
|
||||
@@ -20,7 +20,7 @@ pub struct Track {
|
||||
pub album: Album,
|
||||
pub contributors: Option<Vec<Artist>>,
|
||||
}
|
||||
go_deserialize!(Track {
|
||||
lenient_deserialize!(Track {
|
||||
"id" => id, "title" => title, "duration" => duration, "track_position" => track_position,
|
||||
"disk_number" => disk_number, "isrc" => isrc, "link" => link, "release_date" => release_date,
|
||||
"explicit_lyrics" => explicit_lyrics, "explicit_content_lyrics" => explicit_content_lyrics,
|
||||
@@ -37,7 +37,7 @@ pub struct Artist {
|
||||
pub picture_xl: String,
|
||||
pub nb_fan: isize,
|
||||
}
|
||||
go_deserialize!(Artist {
|
||||
lenient_deserialize!(Artist {
|
||||
"id" => id, "name" => name, "picture" => picture, "picture_medium" => picture_medium,
|
||||
"picture_big" => picture_big, "picture_xl" => picture_xl, "nb_fan" => nb_fan,
|
||||
});
|
||||
@@ -53,7 +53,7 @@ pub struct Album {
|
||||
pub release_date: String,
|
||||
pub record_type: String,
|
||||
}
|
||||
go_deserialize!(Album {
|
||||
lenient_deserialize!(Album {
|
||||
"id" => id, "title" => title, "cover" => cover, "cover_medium" => cover_medium,
|
||||
"cover_big" => cover_big, "cover_xl" => cover_xl, "release_date" => release_date, "record_type" => record_type,
|
||||
});
|
||||
@@ -62,7 +62,7 @@ go_deserialize!(Album {
|
||||
pub struct Tracks {
|
||||
pub data: Option<Vec<Track>>,
|
||||
}
|
||||
go_deserialize!(Tracks { "data" => data, });
|
||||
lenient_deserialize!(Tracks { "data" => data, });
|
||||
|
||||
impl Track {
|
||||
pub fn artist_display(&self) -> String {
|
||||
|
||||
@@ -1,25 +1,25 @@
|
||||
use super::{Artist, Track, Tracks, best_image};
|
||||
use crate::lyrics::json::go_deserialize;
|
||||
use crate::json::lenient_deserialize;
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct TrackPage {
|
||||
pub data: Option<Vec<Track>>,
|
||||
pub next: String,
|
||||
}
|
||||
go_deserialize!(TrackPage { "data" => data, "next" => next, });
|
||||
lenient_deserialize!(TrackPage { "data" => data, "next" => next, });
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct Genre {
|
||||
pub id: isize,
|
||||
pub name: String,
|
||||
}
|
||||
go_deserialize!(Genre { "id" => id, "name" => name, });
|
||||
lenient_deserialize!(Genre { "id" => id, "name" => name, });
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct Genres {
|
||||
pub data: Option<Vec<Genre>>,
|
||||
}
|
||||
go_deserialize!(Genres { "data" => data, });
|
||||
lenient_deserialize!(Genres { "data" => data, });
|
||||
|
||||
impl Genres {
|
||||
pub fn display(&self) -> String {
|
||||
@@ -51,7 +51,7 @@ pub struct FullAlbum {
|
||||
pub contributors: Option<Vec<Artist>>,
|
||||
pub tracks: Tracks,
|
||||
}
|
||||
go_deserialize!(FullAlbum {
|
||||
lenient_deserialize!(FullAlbum {
|
||||
"id" => id, "title" => title, "cover" => cover, "cover_medium" => cover_medium,
|
||||
"cover_big" => cover_big, "cover_xl" => cover_xl, "release_date" => release_date,
|
||||
"nb_tracks" => nb_tracks, "record_type" => record_type, "label" => label,
|
||||
@@ -70,7 +70,7 @@ pub struct FullArtist {
|
||||
pub nb_fan: isize,
|
||||
pub nb_album: isize,
|
||||
}
|
||||
go_deserialize!(FullArtist {
|
||||
lenient_deserialize!(FullArtist {
|
||||
"id" => id, "name" => name, "picture" => picture, "picture_medium" => picture_medium,
|
||||
"picture_big" => picture_big, "picture_xl" => picture_xl, "nb_fan" => nb_fan, "nb_album" => nb_album,
|
||||
});
|
||||
@@ -79,7 +79,7 @@ go_deserialize!(FullArtist {
|
||||
pub struct Name {
|
||||
pub name: String,
|
||||
}
|
||||
go_deserialize!(Name { "name" => name, });
|
||||
lenient_deserialize!(Name { "name" => name, });
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct FullPlaylist {
|
||||
@@ -93,7 +93,7 @@ pub struct FullPlaylist {
|
||||
pub creator: Name,
|
||||
pub tracks: Tracks,
|
||||
}
|
||||
go_deserialize!(FullPlaylist {
|
||||
lenient_deserialize!(FullPlaylist {
|
||||
"id" => id, "title" => title, "picture" => picture, "picture_medium" => picture_medium,
|
||||
"picture_big" => picture_big, "picture_xl" => picture_xl, "nb_tracks" => nb_tracks,
|
||||
"creator" => creator, "tracks" => tracks,
|
||||
@@ -111,7 +111,7 @@ pub struct ArtistAlbum {
|
||||
pub cover_xl: String,
|
||||
pub record_type: String,
|
||||
}
|
||||
go_deserialize!(ArtistAlbum {
|
||||
lenient_deserialize!(ArtistAlbum {
|
||||
"id" => id, "title" => title, "release_date" => release_date, "nb_tracks" => nb_tracks,
|
||||
"cover" => cover, "cover_medium" => cover_medium, "cover_big" => cover_big,
|
||||
"cover_xl" => cover_xl, "record_type" => record_type,
|
||||
@@ -121,13 +121,13 @@ go_deserialize!(ArtistAlbum {
|
||||
pub struct ArtistAlbums {
|
||||
pub data: Option<Vec<ArtistAlbum>>,
|
||||
}
|
||||
go_deserialize!(ArtistAlbums { "data" => data, });
|
||||
lenient_deserialize!(ArtistAlbums { "data" => data, });
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct AlbumTrackCount {
|
||||
pub nb_tracks: isize,
|
||||
}
|
||||
go_deserialize!(AlbumTrackCount { "nb_tracks" => nb_tracks, });
|
||||
lenient_deserialize!(AlbumTrackCount { "nb_tracks" => nb_tracks, });
|
||||
|
||||
macro_rules! cover_image {
|
||||
($($name:ty),*) => {$(
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
mod case_data;
|
||||
mod casing;
|
||||
|
||||
use crate::lyrics::json::go_deserialize;
|
||||
use crate::json::lenient_deserialize;
|
||||
use crate::matching::lowercase;
|
||||
use std::collections::BTreeSet;
|
||||
|
||||
@@ -12,34 +12,34 @@ pub struct Tag {
|
||||
pub count: isize,
|
||||
pub name: String,
|
||||
}
|
||||
go_deserialize!(Tag { "count" => count, "name" => name, });
|
||||
lenient_deserialize!(Tag { "count" => count, "name" => name, });
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct ArtistCredit {
|
||||
pub name: String,
|
||||
pub joinphrase: String,
|
||||
}
|
||||
go_deserialize!(ArtistCredit { "name" => name, "joinphrase" => joinphrase, });
|
||||
lenient_deserialize!(ArtistCredit { "name" => name, "joinphrase" => joinphrase, });
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct Release {
|
||||
pub title: String,
|
||||
pub artist_credit: Option<Vec<ArtistCredit>>,
|
||||
}
|
||||
go_deserialize!(Release { "title" => title, "artist-credit" => artist_credit, });
|
||||
lenient_deserialize!(Release { "title" => title, "artist-credit" => artist_credit, });
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct Recording {
|
||||
pub tags: Option<Vec<Tag>>,
|
||||
pub releases: Option<Vec<Release>>,
|
||||
}
|
||||
go_deserialize!(Recording { "tags" => tags, "releases" => releases, });
|
||||
lenient_deserialize!(Recording { "tags" => tags, "releases" => releases, });
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct Response {
|
||||
pub recordings: Option<Vec<Recording>>,
|
||||
}
|
||||
go_deserialize!(Response { "recordings" => recordings, });
|
||||
lenient_deserialize!(Response { "recordings" => recordings, });
|
||||
|
||||
pub fn genre(tags: &[Tag]) -> String {
|
||||
let mut seen = BTreeSet::new();
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
//! Re-enrichment input, candidate policy and preview metadata. No file access.
|
||||
|
||||
use crate::lyrics::json::go_deserialize;
|
||||
use crate::json::lenient_deserialize;
|
||||
use crate::matching::lowercase;
|
||||
use crate::resolver::{artists_match, titles_match, track_identity_title, track_titles_match};
|
||||
use serde::Serialize;
|
||||
@@ -36,7 +36,7 @@ pub struct Request {
|
||||
pub preview_only: bool,
|
||||
pub replace_release_metadata: bool,
|
||||
}
|
||||
go_deserialize!(Request {
|
||||
lenient_deserialize!(Request {
|
||||
"file_path" => file_path, "cover_url" => cover_url,
|
||||
"cover_max_dimension" => cover_max_dimension, "embed_lyrics" => embed_lyrics,
|
||||
"lyrics_mode" => lyrics_mode, "artist_tag_mode" => artist_tag_mode,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
//! Collection matching and share links from already-decoded extension tracks.
|
||||
|
||||
use crate::lyrics::json::go_deserialize;
|
||||
use crate::json::lenient_deserialize;
|
||||
use crate::lyrics::matching::{normalize_loose_artist, normalize_title};
|
||||
use crate::matching::lowercase;
|
||||
use serde::Serialize;
|
||||
@@ -13,7 +13,7 @@ pub struct Request {
|
||||
pub kind: String,
|
||||
pub source_extension_id: String,
|
||||
}
|
||||
go_deserialize!(Request {
|
||||
lenient_deserialize!(Request {
|
||||
"name" => name, "artists" => artists, "type" => kind,
|
||||
"source_extension_id" => source_extension_id,
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
//! Typed platform-resolver payloads and metadata matching shared with Go.
|
||||
|
||||
use crate::lyrics::json::{Update, go_deserialize};
|
||||
use crate::json::{Update, lenient_deserialize};
|
||||
use crate::lyrics::matching::normalize_loose_artist;
|
||||
use serde::{Deserialize, Deserializer, Serialize};
|
||||
use std::collections::BTreeMap;
|
||||
@@ -36,14 +36,14 @@ impl<'de, T: Deserialize<'de>> Update<'de> for Map<T> {
|
||||
pub struct Link {
|
||||
pub url: String,
|
||||
}
|
||||
go_deserialize!(Link { "url" => url, });
|
||||
lenient_deserialize!(Link { "url" => url, });
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct Entity {
|
||||
pub title: String,
|
||||
pub artist: String,
|
||||
}
|
||||
go_deserialize!(Entity { "title" => title, "artistname" => artist, });
|
||||
lenient_deserialize!(Entity { "title" => title, "artistname" => artist, });
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct UniTune {
|
||||
@@ -51,7 +51,7 @@ pub struct UniTune {
|
||||
pub links: Map<Link>,
|
||||
pub entities: Map<Entity>,
|
||||
}
|
||||
go_deserialize!(UniTune {
|
||||
lenient_deserialize!(UniTune {
|
||||
"entityuniqueid" => entity_id,
|
||||
"linksbyplatform" => links,
|
||||
"entitiesbyuniqueid" => entities,
|
||||
@@ -61,7 +61,7 @@ go_deserialize!(UniTune {
|
||||
pub struct Artist {
|
||||
pub name: String,
|
||||
}
|
||||
go_deserialize!(Artist { "name" => name, });
|
||||
lenient_deserialize!(Artist { "name" => name, });
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct Recording {
|
||||
@@ -70,7 +70,7 @@ pub struct Recording {
|
||||
pub title: String,
|
||||
pub artists: Option<Vec<Artist>>,
|
||||
}
|
||||
go_deserialize!(Recording {
|
||||
lenient_deserialize!(Recording {
|
||||
"id" => id, "score" => score, "title" => title, "artist-credit" => artists,
|
||||
});
|
||||
|
||||
@@ -78,25 +78,25 @@ go_deserialize!(Recording {
|
||||
pub struct Recordings {
|
||||
pub recordings: Option<Vec<Recording>>,
|
||||
}
|
||||
go_deserialize!(Recordings { "recordings" => recordings, });
|
||||
lenient_deserialize!(Recordings { "recordings" => recordings, });
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct Resource {
|
||||
pub resource: String,
|
||||
}
|
||||
go_deserialize!(Resource { "resource" => resource, });
|
||||
lenient_deserialize!(Resource { "resource" => resource, });
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct Relation {
|
||||
pub url: Resource,
|
||||
}
|
||||
go_deserialize!(Relation { "url" => url, });
|
||||
lenient_deserialize!(Relation { "url" => url, });
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct Relations {
|
||||
pub relations: Option<Vec<Relation>>,
|
||||
}
|
||||
go_deserialize!(Relations { "relations" => relations, });
|
||||
lenient_deserialize!(Relations { "relations" => relations, });
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct Created {
|
||||
@@ -104,7 +104,7 @@ pub struct Created {
|
||||
pub title: String,
|
||||
pub artist: String,
|
||||
}
|
||||
go_deserialize!(Created { "full_url" => full_url, "title" => title, "artist" => artist, });
|
||||
lenient_deserialize!(Created { "full_url" => full_url, "title" => title, "artist" => artist, });
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct PageData {
|
||||
@@ -112,13 +112,13 @@ pub struct PageData {
|
||||
pub artist: String,
|
||||
pub services: Map<Option<Link>>,
|
||||
}
|
||||
go_deserialize!(PageData { "title" => title, "artist" => artist, "services" => services, });
|
||||
lenient_deserialize!(PageData { "title" => title, "artist" => artist, "services" => services, });
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct Page {
|
||||
pub data: PageData,
|
||||
}
|
||||
go_deserialize!(Page { "data" => data, });
|
||||
lenient_deserialize!(Page { "data" => data, });
|
||||
|
||||
/// The platform resolver uses Go's broader metadata artist matcher, rather
|
||||
/// than the stricter primary-artist equality required for lyrics searches.
|
||||
|
||||
@@ -2,6 +2,7 @@ use super::{RepositoryError, error};
|
||||
use crate::environment::compare_versions;
|
||||
use serde::{Deserialize, Deserializer, Serialize};
|
||||
use serde_json::{Value, json};
|
||||
use spotiflac_core::json::field_name;
|
||||
use spotiflac_core::matching::lowercase;
|
||||
use std::cmp::Ordering;
|
||||
use std::collections::BTreeMap;
|
||||
@@ -47,14 +48,10 @@ impl NullField for Tags {
|
||||
}
|
||||
}
|
||||
|
||||
fn field_matches(key: &str, wire: &str) -> bool {
|
||||
key.chars()
|
||||
.map(|value| match value {
|
||||
'ſ' => 's',
|
||||
'K' => 'k',
|
||||
value => value.to_ascii_lowercase(),
|
||||
})
|
||||
.eq(wire.chars().map(|value| value.to_ascii_lowercase()))
|
||||
// `folded_key` comes from `field_name`, which leaves no ASCII uppercase, so an
|
||||
// ASCII case-insensitive comparison equals comparing it with the lowercased wire.
|
||||
fn field_matches(folded_key: &str, wire: &str) -> bool {
|
||||
folded_key.eq_ignore_ascii_case(wire)
|
||||
}
|
||||
|
||||
macro_rules! wire_struct {
|
||||
@@ -77,6 +74,7 @@ macro_rules! wire_struct {
|
||||
fn visit_map<A: serde::de::MapAccess<'de>>(self, mut map: A) -> Result<Self::Value, A::Error> {
|
||||
let mut result = $name::default();
|
||||
while let Some(key) = map.next_key::<String>()? {
|
||||
let key = field_name(&key);
|
||||
$(if field_matches(&key, $wire) {
|
||||
match map.next_value::<Option<$kind>>()? {
|
||||
Some(value) => result.$field = value,
|
||||
@@ -224,3 +222,82 @@ impl Registry {
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
// The matcher this module used before it shared core's `field_name`.
|
||||
fn previous_field_matches(key: &str, wire: &str) -> bool {
|
||||
key.chars()
|
||||
.map(|value| match value {
|
||||
'ſ' => 's',
|
||||
'\u{212A}' => 'k',
|
||||
value => value.to_ascii_lowercase(),
|
||||
})
|
||||
.eq(wire.chars().map(|value| value.to_ascii_lowercase()))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn field_matching_with_core_folding_equals_previous_matcher() {
|
||||
let keys = [
|
||||
"",
|
||||
"id",
|
||||
"ID",
|
||||
"Id",
|
||||
"displayName",
|
||||
"DISPLAYNAME",
|
||||
"display_name",
|
||||
"DISPLAY_NAME",
|
||||
"checkſum_sha256",
|
||||
"CHECKSUMSHA256",
|
||||
"\u{212A}ey",
|
||||
"Key",
|
||||
"sha256",
|
||||
"SHA256",
|
||||
"s\u{0301}ha256",
|
||||
"\u{017F}",
|
||||
"\u{212A}",
|
||||
"\u{00C9}tag",
|
||||
"\u{00E9}tag",
|
||||
"tags ",
|
||||
"\u{FEFF}tags",
|
||||
"updated_at\u{0000}",
|
||||
];
|
||||
let wires = [
|
||||
"",
|
||||
"id",
|
||||
"display_name",
|
||||
"displayName",
|
||||
"checksum_sha256",
|
||||
"checksumSha256",
|
||||
"key",
|
||||
"sha256",
|
||||
"s",
|
||||
"k",
|
||||
"\u{00E9}tag",
|
||||
"tags",
|
||||
"updated_at",
|
||||
];
|
||||
for key in keys {
|
||||
let folded = field_name(key);
|
||||
for wire in wires {
|
||||
assert_eq!(
|
||||
field_matches(&folded, wire),
|
||||
previous_field_matches(key, wire),
|
||||
"key {key:?} wire {wire:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let registry: Registry = serde_json::from_str(
|
||||
r#"{"VERSION":2,"updated_at":"old","UPDATED_AT":null,"Extensions":[{"ID":"a","DisplayName":"A","checkſum_sha256":"x"}]}"#,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(registry.version, 2);
|
||||
assert_eq!(registry.updated_at, "old");
|
||||
assert_eq!(registry.extensions[0].id, "a");
|
||||
assert_eq!(registry.extensions[0].display_name_alt, "A");
|
||||
assert_eq!(registry.extensions[0].checksum_sha256, "x");
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user