1pub mod color;
5pub mod db_cache;
6pub mod hls_source;
7pub mod jellyfin_image;
8pub mod logs;
9pub mod lyrics;
10pub mod musicbrainz;
11pub mod range_source;
12pub mod stream_buffer;
13pub mod subsonic_image;
14pub mod themes;
15use std::path::Path;
16use std::sync::Arc;
17
18pub type CoverUrl = Arc<str>;
19
20pub fn cover_url_from_string(url: String) -> CoverUrl {
21 Arc::from(url)
22}
23
24pub fn map_cover_url(url: Option<String>) -> Option<CoverUrl> {
25 url.map(cover_url_from_string)
26}
27
28pub async fn sleep(duration: std::time::Duration) {
30 tokio::time::sleep(duration).await;
31}
32
33pub async fn offload<F>(fut: F) -> F::Output
48where
49 F: std::future::Future + Send + 'static,
50 F::Output: Send + 'static,
51{
52 struct AbortOnDrop(tokio::task::AbortHandle);
53 impl Drop for AbortOnDrop {
54 fn drop(&mut self) {
55 self.0.abort();
56 }
57 }
58
59 let handle = tokio::spawn(fut);
60 let _guard = AbortOnDrop(handle.abort_handle());
63 match handle.await {
64 Ok(out) => out,
65 Err(err) => match err.try_into_panic() {
66 Ok(panic) => std::panic::resume_unwind(panic),
67 Err(err) => panic!("offloaded task cancelled: {err}"),
71 },
72 }
73}
74
75fn format_artwork_url_impl(path: Option<&impl AsRef<Path>>, size: Option<u32>) -> Option<CoverUrl> {
76 let p = path?;
77 let p = p.as_ref();
78 let p_str = p.to_string_lossy();
79
80 let abs_path = if let Some(stripped) = p_str.strip_prefix("./") {
81 std::env::current_dir().unwrap_or_default().join(stripped)
82 } else {
83 p.to_path_buf()
84 };
85
86 let abs_str = abs_path.to_string_lossy();
87 let abs_str = if abs_str.starts_with('~') {
88 if let Ok(home) = std::env::var("HOME") {
89 std::borrow::Cow::Owned(abs_str.replacen('~', &home, 1))
90 } else {
91 abs_str
92 }
93 } else {
94 abs_str
95 };
96
97 #[cfg(target_os = "android")]
100 {
101 use base64::{Engine as _, engine::general_purpose};
102 return match std::fs::read(&*abs_str) {
103 Ok(bytes) => {
104 let mime = if abs_str.ends_with(".png") {
105 "image/png"
106 } else if abs_str.ends_with(".gif") {
107 "image/gif"
108 } else if abs_str.ends_with(".webp") {
109 "image/webp"
110 } else {
111 "image/jpeg"
112 };
113 let b64 = general_purpose::STANDARD.encode(&bytes);
114 Some(cover_url_from_string(format!("data:{mime};base64,{b64}")))
115 }
116 Err(_) => None,
117 };
118 }
119
120 const QUERY_VAL: &percent_encoding::AsciiSet = &percent_encoding::CONTROLS
121 .add(b' ')
122 .add(b'"')
123 .add(b'#')
124 .add(b'%')
125 .add(b'&')
126 .add(b'+')
127 .add(b'=')
128 .add(b'?')
129 .add(b'<')
130 .add(b'>')
131 .add(b'`')
132 .add(b'\\')
133 .add(b':');
134
135 if cfg!(target_os = "windows") {
136 let mut url = format!(
137 "http://artwork.dioxus.localhost/local?p={}",
138 percent_encoding::utf8_percent_encode(&abs_str, QUERY_VAL)
139 );
140 if let Some(size) = size {
141 url.push_str(&format!("&s={size}"));
142 }
143 Some(cover_url_from_string(url))
144 } else {
145 let mut url = format!(
146 "artwork://local?p={}",
147 percent_encoding::utf8_percent_encode(&abs_str, QUERY_VAL)
148 );
149 if let Some(size) = size {
150 url.push_str(&format!("&s={size}"));
151 }
152 Some(cover_url_from_string(url))
153 }
154}
155
156pub fn format_artwork_url(path: Option<&impl AsRef<Path>>) -> Option<CoverUrl> {
157 format_artwork_url_impl(path, None)
158}
159
160pub fn format_artwork_thumb_url(path: Option<&impl AsRef<Path>>, size: u32) -> Option<CoverUrl> {
161 format_artwork_url_impl(path, Some(size))
162}
163
164pub fn default_cover_url() -> CoverUrl {
165 cover_url_from_string(
166 "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()
167 )
168}
169
170#[cfg(test)]
171mod offload_tests {
172 use std::sync::Arc;
173 use std::sync::atomic::{AtomicBool, Ordering};
174
175 #[tokio::test]
178 async fn dropping_offload_aborts_the_task() {
179 struct SetOnDrop(Arc<AtomicBool>);
180 impl Drop for SetOnDrop {
181 fn drop(&mut self) {
182 self.0.store(true, Ordering::SeqCst);
183 }
184 }
185 let dropped = Arc::new(AtomicBool::new(false));
186 let guard = SetOnDrop(dropped.clone());
187 let fut = super::offload(async move {
188 let _guard = guard;
189 tokio::time::sleep(std::time::Duration::from_secs(300)).await;
190 });
191 tokio::select! {
194 _ = fut => panic!("offloaded sleep cannot have completed"),
195 _ = tokio::time::sleep(std::time::Duration::from_millis(50)) => {}
196 }
197 for _ in 0..100 {
198 if dropped.load(Ordering::SeqCst) {
199 return;
200 }
201 tokio::time::sleep(std::time::Duration::from_millis(10)).await;
202 }
203 panic!("offloaded task kept running after its caller was dropped");
204 }
205}