Skip to main content

utils/
lib.rs

1//! Shared utility crate for Kopuz: color helpers, image processing (artwork,
2//! thumbnails, subsonic images), lyrics fetching, and terminal logging.
3
4pub mod color;
5pub mod db_cache;
6#[cfg(not(target_arch = "wasm32"))]
7pub mod hls_source;
8pub mod jellyfin_image;
9#[cfg(not(target_arch = "wasm32"))]
10pub mod logs;
11pub mod lyrics;
12pub mod musicbrainz;
13#[cfg(not(target_arch = "wasm32"))]
14pub mod range_source;
15#[cfg(not(target_arch = "wasm32"))]
16pub mod stream_buffer;
17pub mod subsonic_image;
18pub mod themes;
19use std::path::Path;
20use std::sync::Arc;
21
22pub type CoverUrl = Arc<str>;
23
24pub fn cover_url_from_string(url: String) -> CoverUrl {
25    Arc::from(url)
26}
27
28pub fn map_cover_url(url: Option<String>) -> Option<CoverUrl> {
29    url.map(cover_url_from_string)
30}
31
32/// Cross-platform async sleep that works on both native (tokio) and WASM (gloo-timers).
33pub async fn sleep(duration: std::time::Duration) {
34    #[cfg(not(target_arch = "wasm32"))]
35    {
36        tokio::time::sleep(duration).await;
37    }
38    #[cfg(target_arch = "wasm32")]
39    {
40        gloo_timers::future::sleep(duration).await;
41    }
42}
43
44fn format_artwork_url_impl(path: Option<&impl AsRef<Path>>, size: Option<u32>) -> Option<CoverUrl> {
45    let p = path?;
46    let p = p.as_ref();
47    let p_str = p.to_string_lossy();
48
49    let abs_path = if let Some(stripped) = p_str.strip_prefix("./") {
50        std::env::current_dir().unwrap_or_default().join(stripped)
51    } else {
52        p.to_path_buf()
53    };
54
55    let abs_str = abs_path.to_string_lossy();
56    let abs_str = if abs_str.starts_with('~') {
57        if let Ok(home) = std::env::var("HOME") {
58            std::borrow::Cow::Owned(abs_str.replacen('~', &home, 1))
59        } else {
60            abs_str
61        }
62    } else {
63        abs_str
64    };
65
66    // Android WebView is unreliable with custom URL schemes (artwork://) and the
67    // http localhost shim, so inline the cover as a base64 data URL instead.
68    #[cfg(target_os = "android")]
69    {
70        use base64::{Engine as _, engine::general_purpose};
71        return match std::fs::read(&*abs_str) {
72            Ok(bytes) => {
73                let mime = if abs_str.ends_with(".png") {
74                    "image/png"
75                } else if abs_str.ends_with(".gif") {
76                    "image/gif"
77                } else if abs_str.ends_with(".webp") {
78                    "image/webp"
79                } else {
80                    "image/jpeg"
81                };
82                let b64 = general_purpose::STANDARD.encode(&bytes);
83                Some(cover_url_from_string(format!("data:{mime};base64,{b64}")))
84            }
85            Err(_) => None,
86        };
87    }
88
89    const QUERY_VAL: &percent_encoding::AsciiSet = &percent_encoding::CONTROLS
90        .add(b' ')
91        .add(b'"')
92        .add(b'#')
93        .add(b'%')
94        .add(b'&')
95        .add(b'+')
96        .add(b'=')
97        .add(b'?')
98        .add(b'<')
99        .add(b'>')
100        .add(b'`')
101        .add(b'\\')
102        .add(b':');
103
104    if cfg!(target_os = "windows") {
105        let mut url = format!(
106            "http://artwork.dioxus.localhost/local?p={}",
107            percent_encoding::utf8_percent_encode(&abs_str, QUERY_VAL)
108        );
109        if let Some(size) = size {
110            url.push_str(&format!("&s={size}"));
111        }
112        Some(cover_url_from_string(url))
113    } else {
114        let mut url = format!(
115            "artwork://local?p={}",
116            percent_encoding::utf8_percent_encode(&abs_str, QUERY_VAL)
117        );
118        if let Some(size) = size {
119            url.push_str(&format!("&s={size}"));
120        }
121        Some(cover_url_from_string(url))
122    }
123}
124
125pub fn format_artwork_url(path: Option<&impl AsRef<Path>>) -> Option<CoverUrl> {
126    format_artwork_url_impl(path, None)
127}
128
129pub fn format_artwork_thumb_url(path: Option<&impl AsRef<Path>>, size: u32) -> Option<CoverUrl> {
130    format_artwork_url_impl(path, Some(size))
131}
132
133pub fn default_cover_url() -> CoverUrl {
134    cover_url_from_string(
135        "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='400' height='400' viewBox='0 0 400 400'%3E%3Crect width='400' height='400' fill='%231e1b2e'/%3E%3Ccircle cx='200' cy='180' r='70' fill='none' stroke='%233d3466' stroke-width='6'/%3E%3Cpath d='M155 280 Q200 240 245 280' fill='none' stroke='%233d3466' stroke-width='6' stroke-linecap='round'/%3E%3C/svg%3E".to_string()
136    )
137}