kopuz-utils 0.8.2

A modern, lightweight music player built with Rust and Dioxus.
Documentation
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(&params.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
}

/// Resolve a track's cover to a final image URL from the typed [`reader::Track`]
/// fields (`cover` + bare `item_id`), with album-art fallback. `cover` may be a
/// raw remote URL, a `directurl:`/`urlhex_` embedded form, a Jellyfin image tag,
/// `"none"`, or absent. Replaces the legacy `service:id:tag` path round-trip so a
/// cover URL containing `:` (e.g. YouTube thumbnails) is never split apart.
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())
}

/// Encode a remote cover URL into the `urlhex_HEX` tag form that
/// `decode_embedded_cover_url` (and the cover-URL resolvers that call
/// it) understand. The result is a single tag string — wrap it in a
/// path / album_id / image_tag yourself per call-site convention.
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()
}