pub fn jellyfin_image_url(
server_url: &str,
item_id: &str,
image_tag: Option<&str>,
access_token: Option<&str>,
max_width: u32,
quality: u32,
) -> String {
if let Some(tag) = image_tag
&& let Some(url) = decode_embedded_cover_url(tag)
{
return url;
}
let mut params = Vec::new();
params.push(format!("maxWidth={}", max_width));
params.push(format!("quality={}", quality));
if let Some(tag) = image_tag {
params.push(format!("tag={}", tag));
}
if let Some(token) = access_token {
params.push(format!("api_key={}", token));
}
let mut url = format!("{}/Items/{}/Images/Primary", server_url, item_id);
if !params.is_empty() {
url.push('?');
url.push_str(¶ms.join("&"));
}
url
}
pub fn parse_jellyfin_path(path_str: &str) -> Option<(&str, Option<&str>)> {
let parts: Vec<&str> = path_str.split(':').collect();
if parts.len() >= 2 {
let id = parts[1];
let tag = if parts.len() >= 3 {
Some(parts[2])
} else {
None
};
Some((id, tag))
} else {
None
}
}
pub fn jellyfin_image_url_from_path(
path_str: &str,
server_url: &str,
access_token: Option<&str>,
max_width: u32,
quality: u32,
) -> Option<String> {
if let Some(url) = path_str.strip_prefix("directurl:") {
return Some(url.to_string());
}
let (id, tag) = parse_jellyfin_path(path_str)?;
if tag == Some("none") {
return None;
}
if let Some(tag) = tag
&& let Some(url) = decode_embedded_cover_url(tag)
{
return Some(url);
}
if server_url.is_empty() {
return None;
}
Some(jellyfin_image_url(
server_url,
id,
tag,
access_token,
max_width,
quality,
))
}
pub fn track_cover_url_with_album_fallback(
track_path_str: &str,
album_id_str: &str,
server_url: &str,
access_token: Option<&str>,
max_width: u32,
quality: u32,
) -> Option<String> {
let can_build_remote = !server_url.is_empty();
if let Some((id, Some(tag))) = parse_jellyfin_path(track_path_str) {
if tag == "none" {
return None;
}
if let Some(url) = decode_embedded_cover_url(tag) {
return Some(url);
}
if can_build_remote {
return Some(jellyfin_image_url(
server_url,
id,
Some(tag),
access_token,
max_width,
quality,
));
}
}
if !album_id_str.is_empty()
&& let Some((album_item_id, album_tag)) = parse_jellyfin_path(album_id_str)
{
if album_tag == Some("none") {
return None;
}
if let Some(tag) = album_tag
&& let Some(url) = decode_embedded_cover_url(tag)
{
return Some(url);
}
if can_build_remote {
return Some(jellyfin_image_url(
server_url,
album_item_id,
album_tag,
access_token,
max_width,
quality,
));
}
}
if let Some((id, _)) = parse_jellyfin_path(track_path_str) {
if !can_build_remote {
return None;
}
return Some(jellyfin_image_url(
server_url,
id,
None,
access_token,
max_width,
quality,
));
}
None
}
pub fn resolve_track_cover(
cover: Option<&str>,
item_id: &str,
album_id_str: &str,
server_url: &str,
access_token: Option<&str>,
max_width: u32,
quality: u32,
) -> Option<String> {
let can_build_remote = !server_url.is_empty();
match cover {
Some("none") => return None,
Some(c) => {
if let Some(direct) = c.strip_prefix("directurl:") {
return Some(direct.to_string());
}
if c.starts_with("http://") || c.starts_with("https://") {
return Some(c.to_string());
}
if let Some(url) = decode_embedded_cover_url(c) {
return Some(url);
}
if can_build_remote {
return Some(jellyfin_image_url(
server_url,
item_id,
Some(c),
access_token,
max_width,
quality,
));
}
}
None => {}
}
if !album_id_str.is_empty()
&& let Some((album_item_id, album_tag)) = parse_jellyfin_path(album_id_str)
{
if album_tag == Some("none") {
return None;
}
if let Some(tag) = album_tag
&& let Some(url) = decode_embedded_cover_url(tag)
{
return Some(url);
}
if can_build_remote {
return Some(jellyfin_image_url(
server_url,
album_item_id,
album_tag,
access_token,
max_width,
quality,
));
}
}
if can_build_remote {
return Some(jellyfin_image_url(
server_url,
item_id,
None,
access_token,
max_width,
quality,
));
}
None
}
pub fn track_cover_url_or_default(
track_path_str: &str,
album_id_str: &str,
server_url: &str,
access_token: Option<&str>,
max_width: u32,
quality: u32,
) -> String {
track_cover_url_with_album_fallback(
track_path_str,
album_id_str,
server_url,
access_token,
max_width,
quality,
)
.unwrap_or_else(|| crate::default_cover_url().as_ref().to_string())
}
pub fn encode_cover_url(url: &str) -> String {
format!("urlhex_{}", hex::encode(url.as_bytes()))
}
fn decode_embedded_cover_url(tag: &str) -> Option<String> {
let hex = tag.strip_prefix("urlhex_")?;
if hex.len() % 2 != 0 {
return None;
}
let mut bytes = Vec::with_capacity(hex.len() / 2);
let chars: Vec<char> = hex.chars().collect();
let mut i = 0;
while i < chars.len() {
let hi = chars[i].to_digit(16)?;
let lo = chars[i + 1].to_digit(16)?;
bytes.push(((hi << 4) | lo) as u8);
i += 2;
}
String::from_utf8(bytes).ok()
}