1pub fn parse_subsonic_path(path_str: &str) -> Option<(&str, Option<&str>)> {
2 let parts: Vec<&str> = path_str.split(':').collect();
3 if parts.len() >= 2 {
4 let id = parts[1].trim();
5 if id.is_empty() {
6 return None;
7 }
8 let tag = if parts.len() >= 3 {
9 Some(parts[2])
10 } else {
11 None
12 };
13 Some((id, tag))
14 } else {
15 None
16 }
17}
18
19pub fn subsonic_image_url_from_path(
20 path_str: &str,
21 server_url: &str,
22 access_token: Option<&str>,
23 max_width: u32,
24 quality: u32,
25) -> Option<String> {
26 let (id, tag) = parse_subsonic_path(path_str)?;
27 if tag == Some("none") {
28 return None;
29 }
30
31 if let Some(tag) = tag
32 && let Some(url) = decode_embedded_cover_url(tag)
33 {
34 return Some(url);
35 }
36
37 let mut url = reqwest::Url::parse(&format!(
38 "{}/rest/getCoverArt.view",
39 server_url.trim_end_matches('/')
40 ))
41 .unwrap_or_else(|_| reqwest::Url::parse("http://127.0.0.1/").unwrap());
42
43 {
44 let mut pairs = url.query_pairs_mut();
45 pairs.append_pair("id", id);
46 pairs.append_pair("size", &max_width.to_string());
47 pairs.append_pair("quality", &quality.to_string());
48 if let Some(token) = access_token {
49 pairs.append_pair("access_token", token);
50 }
51 }
52
53 Some(url.to_string())
54}
55
56fn decode_embedded_cover_url(tag: &str) -> Option<String> {
57 let hex = tag.strip_prefix("urlhex_")?;
58 if hex.len() % 2 != 0 {
59 return None;
60 }
61
62 let mut bytes = Vec::with_capacity(hex.len() / 2);
63 let chars: Vec<char> = hex.chars().collect();
64 let mut i = 0;
65 while i < chars.len() {
66 let hi = chars[i].to_digit(16)?;
67 let lo = chars[i + 1].to_digit(16)?;
68 bytes.push(((hi << 4) | lo) as u8);
69 i += 2;
70 }
71
72 String::from_utf8(bytes).ok()
73}