Sync with FRID protocol v5. Improved direct content id search
This commit is contained in:
+128
-13
@@ -22,15 +22,30 @@ pub fn track_share_link(track: &TrackItem) -> Option<String> {
|
||||
Some(track_share_link_for_content_id(track, &content_id))
|
||||
}
|
||||
|
||||
pub fn parse_frid_content_id(value: &str) -> Option<String> {
|
||||
/// A parsed `frid://` share link.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct FridLink {
|
||||
/// Canonical content id (`b3:<64 hex>`).
|
||||
pub content_id: String,
|
||||
/// Human-readable "artists-title" label from the `t` query parameter.
|
||||
pub label: Option<String>,
|
||||
}
|
||||
|
||||
pub fn parse_frid_link(value: &str) -> Option<FridLink> {
|
||||
let value = value.trim();
|
||||
let rest = value.strip_prefix("frid://")?;
|
||||
let content_id = rest
|
||||
.split(['?', '#'])
|
||||
.next()
|
||||
.unwrap_or_default()
|
||||
.trim_end_matches('/');
|
||||
music_dht::normalize_content_id(content_id)
|
||||
let mut parts = rest.splitn(2, ['?', '#']);
|
||||
let content_id =
|
||||
music_dht::normalize_content_id(parts.next().unwrap_or_default().trim_end_matches('/'))?;
|
||||
let label = parts.next().and_then(|query| {
|
||||
query.split('&').find_map(|pair| {
|
||||
let value = pair.strip_prefix("t=")?;
|
||||
let decoded = percent_decode(value);
|
||||
let decoded = decoded.trim();
|
||||
(!decoded.is_empty()).then(|| decoded.to_string())
|
||||
})
|
||||
});
|
||||
Some(FridLink { content_id, label })
|
||||
}
|
||||
|
||||
pub fn cached_track_share_link(track: &TrackItem) -> Option<String> {
|
||||
@@ -71,6 +86,41 @@ fn percent_encode(value: &str) -> String {
|
||||
out
|
||||
}
|
||||
|
||||
fn percent_decode(value: &str) -> String {
|
||||
let bytes = value.as_bytes();
|
||||
let mut out = Vec::with_capacity(bytes.len());
|
||||
let mut index = 0;
|
||||
while index < bytes.len() {
|
||||
match bytes[index] {
|
||||
b'%' if index + 3 <= bytes.len() => {
|
||||
let hex = std::str::from_utf8(&bytes[index + 1..index + 3])
|
||||
.ok()
|
||||
.and_then(|hex| u8::from_str_radix(hex, 16).ok());
|
||||
match hex {
|
||||
Some(byte) => {
|
||||
out.push(byte);
|
||||
index += 3;
|
||||
}
|
||||
None => {
|
||||
out.push(b'%');
|
||||
index += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Liberal form-encoding acceptance; our encoder never emits '+'.
|
||||
b'+' => {
|
||||
out.push(b' ');
|
||||
index += 1;
|
||||
}
|
||||
byte => {
|
||||
out.push(byte);
|
||||
index += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
String::from_utf8_lossy(&out).into_owned()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -116,13 +166,78 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn parses_frid_content_link() {
|
||||
let link = parse_frid_link(
|
||||
"frid://b3:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef?t=Artist-Track"
|
||||
)
|
||||
.expect("valid link");
|
||||
assert_eq!(
|
||||
parse_frid_content_id(
|
||||
"frid://b3:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef?t=Artist-Track"
|
||||
)
|
||||
.as_deref(),
|
||||
Some("b3:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef")
|
||||
link.content_id,
|
||||
"b3:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
|
||||
);
|
||||
assert!(parse_frid_content_id("https://example.com").is_none());
|
||||
assert_eq!(link.label.as_deref(), Some("Artist-Track"));
|
||||
assert!(parse_frid_link("https://example.com").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_percent_encoded_label() {
|
||||
let link = parse_frid_link(
|
||||
"frid://B3:0123456789ABCDEF0123456789abcdef0123456789abcdef0123456789abcdef?t=Rammstein-Du%20Riechst%20So%20Gut"
|
||||
)
|
||||
.expect("valid link");
|
||||
assert_eq!(
|
||||
link.content_id,
|
||||
"b3:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
|
||||
);
|
||||
assert_eq!(link.label.as_deref(), Some("Rammstein-Du Riechst So Gut"));
|
||||
|
||||
// Cyrillic labels and a missing label both survive.
|
||||
let link = parse_frid_link(
|
||||
"frid://b3:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef?t=%D0%90%D1%80%D1%82%D0%B8%D1%81%D1%82"
|
||||
)
|
||||
.expect("valid link");
|
||||
assert_eq!(link.label.as_deref(), Some("Артист"));
|
||||
let link = parse_frid_link(
|
||||
"frid://b3:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
|
||||
)
|
||||
.expect("valid link");
|
||||
assert_eq!(link.label, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn share_link_round_trips_through_parse() {
|
||||
let track = TrackItem {
|
||||
id: 1,
|
||||
title: "Du Riechst So Gut".into(),
|
||||
track_number: None,
|
||||
disc_number: None,
|
||||
duration_seconds: 1.0,
|
||||
artists: vec![ArtistRef {
|
||||
id: 1,
|
||||
name: "Rammstein".into(),
|
||||
}],
|
||||
featured_artists: Vec::new(),
|
||||
release_id: 1,
|
||||
release_title: "Herzeleid".into(),
|
||||
release_year: None,
|
||||
file_path: String::new(),
|
||||
content_id: Some(
|
||||
"b3:5ebd9e61e1154a64470e6ee4dd1225550f380dd575d01fd4ffba6a5bd1b34104".into(),
|
||||
),
|
||||
cover_path: None,
|
||||
audio_format: None,
|
||||
audio_bitrate: None,
|
||||
audio_sample_rate: None,
|
||||
audio_bit_depth: None,
|
||||
file_size_bytes: None,
|
||||
play_count: 0,
|
||||
fed: None,
|
||||
};
|
||||
let link = track_share_link(&track).expect("link");
|
||||
let parsed = parse_frid_link(&link).expect("parses back");
|
||||
assert_eq!(
|
||||
parsed.content_id,
|
||||
"b3:5ebd9e61e1154a64470e6ee4dd1225550f380dd575d01fd4ffba6a5bd1b34104"
|
||||
);
|
||||
assert_eq!(parsed.label.as_deref(), Some("Rammstein-Du Riechst So Gut"));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user