feat(kiosk): bump v0.1.5 with Yatse artwork proxy, smart track selection, and HA buttons

This commit is contained in:
bart
2026-11-06 18:20:20 +01:00
parent 423bc75c94
commit a795c3d024
6301 changed files with 19481 additions and 111 deletions
+1 -1
View File
@@ -7,4 +7,4 @@ dist/
build/
*.egg-info/
.pytest_cache/
jellyfin-kiosk-rs/target
jellyfin-kiosk-rs/target*.log
+1 -1
View File
@@ -1460,7 +1460,7 @@ checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
[[package]]
name = "jellyfin-kiosk-rs"
version = "0.1.4"
version = "0.1.5"
dependencies = [
"axum",
"eframe",
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "jellyfin-kiosk-rs"
version = "0.1.4"
version = "0.1.5"
edition = "2021"
[dependencies]
Binary file not shown.
-12
View File
@@ -1,12 +0,0 @@
2026-11-05T21:13:59.466559Z  INFO jellyfin_kiosk_rs: Starting Jellyfin HTPC Kiosk (Rust)...
2026-11-05T21:13:59.467206Z  INFO jellyfin_kiosk_rs::config: Loaded config from ../config.yaml
2026-11-05T21:13:59.474938Z  INFO jellyfin_kiosk_rs::player: libmpv initialized successfully (vo=libmpv, hwdec=auto)
2026-11-05T21:13:59.549439Z  INFO jellyfin_kiosk_rs::yatse: Yatse Kodi TCP JSON-RPC socket server listening on 0.0.0.0:9090
2026-11-05T21:13:59.549444Z  INFO jellyfin_kiosk_rs: Yatse Kodi JSON-RPC server listening on http://0.0.0.0:8080
2026-11-05T21:13:59.549923Z  INFO jellyfin_kiosk_rs::eventserver: Kodi EventServer listening on UDP 0.0.0.0:9777
2026-11-05T21:13:59.551471Z  INFO jellyfin_kiosk_rs: Published Zeroconf services for Yatse discovery: 'Kodi (bart-laptop)'
2026-11-05T21:13:59.691500Z  INFO winit::platform_impl::linux::x11::window: Guessed window scale factor: 1
2026-11-05T21:13:59.729454Z  INFO jellyfin_kiosk_rs::youtube: Loaded cached YouTube token from youtube_token.json
Cannot load libcuda.so.1
2026-11-05T21:13:59.854273Z  INFO jellyfin_kiosk_rs::player: libmpv OpenGL render context created successfully!
2026-11-05T21:13:59.916581Z  INFO jellyfin_kiosk_rs::jellyfin: Authenticated successfully with Jellyfin as bart
+13 -13
View File
@@ -431,8 +431,8 @@ impl KioskApp {
}
if item.item_type == "YouTubeVideo" || item.id.starts_with("http") {
self.view_mode = ViewMode::Player;
self.playback_osd_open = true;
self.playback_osd_idx = 2;
self.playback_osd_open = false;
self.playback_osd_until = None;
let player = self.player.clone();
let url = item.id.clone();
let name = item.name.clone();
@@ -1239,8 +1239,8 @@ impl KioskApp {
// Play first track or all tracks
if let Some(first_track) = self.folder_items.first() {
self.view_mode = ViewMode::Player;
self.playback_osd_open = true;
self.playback_osd_idx = 2;
self.playback_osd_open = false;
self.playback_osd_until = None;
let client = self.client.clone();
let player = self.player.clone();
let item = first_track.clone();
@@ -1254,8 +1254,8 @@ impl KioskApp {
let track_idx = self.selected_grid_idx - 1;
if let Some(track) = self.folder_items.get(track_idx) {
self.view_mode = ViewMode::Player;
self.playback_osd_open = true;
self.playback_osd_idx = 2;
self.playback_osd_open = false;
self.playback_osd_until = None;
let client = self.client.clone();
let player = self.player.clone();
let item = track.clone();
@@ -1659,11 +1659,11 @@ impl eframe::App for KioskApp {
let is_new_item = !current_title.is_empty() && current_title != self.last_playing_title;
if is_playing && (!self.was_playing || is_new_item) {
self.view_mode = ViewMode::Player;
self.playback_osd_open = true;
self.playback_osd_idx = 2; // Pause/Play
self.playback_osd_until = Some(Instant::now() + Duration::from_secs(4));
self.playback_osd_open = false; // Als een item begint met spelen moet het afspeelmenu niet worden getoond
self.playback_osd_until = None;
self.last_playing_title = current_title;
} else if !is_playing && self.was_playing {
} else if !is_playing && (self.was_playing || self.view_mode == ViewMode::Player) {
// Als een item klaar is met afspelen moet hij terug naar het menu
self.view_mode = ViewMode::Browser;
self.playback_osd_open = false;
self.playback_osd_until = None;
@@ -1681,7 +1681,7 @@ impl eframe::App for KioskApp {
}
}
if is_playing || self.volume_hud_until.is_some() || self.playback_osd_until.is_some() || self.osd_notice.is_some() || self.folder_loading {
if is_playing || self.view_mode == ViewMode::Player || self.volume_hud_until.is_some() || self.playback_osd_until.is_some() || self.osd_notice.is_some() || self.folder_loading {
ctx.request_repaint_after(Duration::from_millis(100));
}
@@ -3153,8 +3153,8 @@ impl eframe::App for KioskApp {
if let Some(t_idx) = clicked_track_idx {
self.selected_grid_idx = t_idx + 1;
self.view_mode = ViewMode::Player;
self.playback_osd_open = true;
self.playback_osd_idx = 2;
self.playback_osd_open = false;
self.playback_osd_until = None;
if let Some(track) = self.folder_items.get(t_idx) {
let client = self.client.clone();
let player = self.player.clone();
+62 -11
View File
@@ -94,7 +94,7 @@ async fn run_mqtt_loop(config: AppConfig, player: Arc<PlayerController>) {
let mut last_published: Option<MqttPlaybackState> = None;
loop {
let (client, mut eventloop) = AsyncClient::new(mqtt_opts.clone(), 30);
let (client, mut eventloop) = AsyncClient::new(mqtt_opts.clone(), 100);
let mut is_connected = false;
loop {
@@ -160,6 +160,7 @@ pub async fn publish_homeassistant_discovery(
) {
let state_topic = format!("{}/{}/state", mqtt_cfg.topic_prefix, device_id);
let avail_topic = format!("{}/{}/availability", mqtt_cfg.topic_prefix, device_id);
let command_topic = format!("{}/{}/command", mqtt_cfg.topic_prefix, device_id);
let device_obj = json!({
"identifiers": [device_id],
@@ -169,12 +170,6 @@ pub async fn publish_homeassistant_discovery(
"sw_version": env!("CARGO_PKG_VERSION")
});
let availability_obj = json!([{
"topic": avail_topic,
"payload_available": "online",
"payload_not_available": "offline"
}]);
struct SensorConfig<'a> {
object_id: &'a str,
name: &'a str,
@@ -227,20 +222,76 @@ pub async fn publish_homeassistant_discovery(
mqtt_cfg.discovery_prefix, device_id, s.object_id
);
let payload = json!({
"name": format!("{} {}", device_name, s.name),
"name": s.name,
"has_entity_name": true,
"unique_id": format!("{}_{}", device_id, s.object_id),
"state_topic": state_topic,
"value_template": s.value_template,
"json_attributes_topic": state_topic,
"icon": s.icon,
"device": device_obj,
"availability": availability_obj
"availability_topic": avail_topic,
"payload_available": "online",
"payload_not_available": "offline"
});
if let Err(e) = client.publish(&disc_topic, QoS::AtLeastOnce, true, payload.to_string()).await {
tracing::warn!("Failed to publish Home Assistant discovery for {}: {}", s.object_id, e);
tracing::warn!("Failed to publish Home Assistant discovery for sensor {}: {}", s.object_id, e);
} else {
tracing::debug!("Published HA discovery: {}", disc_topic);
tracing::info!("Published HA sensor discovery: {}", disc_topic);
}
}
struct ButtonConfig<'a> {
object_id: &'a str,
name: &'a str,
payload_press: &'a str,
icon: &'a str,
}
let buttons = [
ButtonConfig {
object_id: "play_pause",
name: "Play / Pause",
payload_press: "play_pause",
icon: "mdi:play-pause",
},
ButtonConfig {
object_id: "stop",
name: "Stop",
payload_press: "stop",
icon: "mdi:stop",
},
ButtonConfig {
object_id: "mute",
name: "Mute",
payload_press: "toggle_mute",
icon: "mdi:volume-mute",
},
];
for b in &buttons {
let disc_topic = format!(
"{}/button/{}/{}/config",
mqtt_cfg.discovery_prefix, device_id, b.object_id
);
let payload = json!({
"name": b.name,
"has_entity_name": true,
"unique_id": format!("{}_{}", device_id, b.object_id),
"command_topic": command_topic,
"payload_press": b.payload_press,
"icon": b.icon,
"device": device_obj,
"availability_topic": avail_topic,
"payload_available": "online",
"payload_not_available": "offline"
});
if let Err(e) = client.publish(&disc_topic, QoS::AtLeastOnce, true, payload.to_string()).await {
tracing::warn!("Failed to publish Home Assistant discovery for button {}: {}", b.object_id, e);
} else {
tracing::info!("Published HA button discovery: {}", disc_topic);
}
}
+345 -2
View File
@@ -46,6 +46,36 @@ impl Default for PlayerStatus {
}
}
#[derive(Debug, Clone)]
pub struct TrackInfo {
pub id: String,
pub track_type: String, // "audio", "sub", "video"
pub lang: String,
pub title: String,
pub is_default: bool,
pub is_forced: bool,
#[allow(dead_code)]
pub is_selected: bool,
}
pub fn is_english_lang(lang: &str, title: &str) -> bool {
let l = lang.trim().to_lowercase();
if l == "en" || l == "eng" || l.starts_with("en-") || l.starts_with("en_") {
return true;
}
let t = title.to_lowercase();
t.contains("english") || t.contains("engels")
}
pub fn is_dutch_lang(lang: &str, title: &str) -> bool {
let l = lang.trim().to_lowercase();
if l == "nl" || l == "nld" || l == "dut" || l.starts_with("nl-") || l.starts_with("nl_") {
return true;
}
let t = title.to_lowercase();
t.contains("dutch") || t.contains("nederlands")
}
#[derive(Clone, Copy)]
pub struct MpvHandle(pub *mut libmpv_sys::mpv_handle);
unsafe impl Send for MpvHandle {}
@@ -74,6 +104,123 @@ impl MpvHandle {
}
}
}
pub fn get_property_string(&self, name: &str) -> Option<String> {
if self.0.is_null() {
return None;
}
let c_name = CString::new(name).ok()?;
unsafe {
let ptr = libmpv_sys::mpv_get_property_string(self.0, c_name.as_ptr());
if ptr.is_null() {
None
} else {
let s = CStr::from_ptr(ptr).to_string_lossy().into_owned();
libmpv_sys::mpv_free(ptr as *mut _);
Some(s)
}
}
}
pub fn get_tracks(&self) -> Vec<TrackInfo> {
let count_str = self.get_property_string("track-list/count").unwrap_or_default();
let count: usize = count_str.parse().unwrap_or(0);
let mut tracks = Vec::with_capacity(count);
for i in 0..count {
let track_type = self.get_property_string(&format!("track-list/{}/type", i)).unwrap_or_default();
let id = self.get_property_string(&format!("track-list/{}/id", i)).unwrap_or_default();
let lang = self.get_property_string(&format!("track-list/{}/lang", i)).unwrap_or_default().to_lowercase();
let title = self.get_property_string(&format!("track-list/{}/title", i)).unwrap_or_default();
let is_default = self.get_property_string(&format!("track-list/{}/default", i)).as_deref() == Some("yes");
let is_forced = self.get_property_string(&format!("track-list/{}/forced", i)).as_deref() == Some("yes");
let is_selected = self.get_property_string(&format!("track-list/{}/selected", i)).as_deref() == Some("yes");
tracks.push(TrackInfo {
id,
track_type,
lang,
title,
is_default,
is_forced,
is_selected,
});
}
tracks
}
pub fn auto_select_tracks(&self) {
if self.0.is_null() {
return;
}
let tracks = self.get_tracks();
if tracks.is_empty() {
return;
}
if let Some(aid) = select_audio_track(&tracks) {
tracing::info!("Audio stream selected: aid={}", aid);
self.cmd(&["set", "aid", aid]);
}
if let Some(sid) = select_subtitle_track(&tracks) {
tracing::info!("Subtitle stream selected: sid={}", sid);
self.cmd(&["set", "sid", sid]);
self.cmd(&["set", "sub-visibility", "yes"]);
} else {
self.cmd(&["set", "sid", "no"]);
}
}
}
pub fn select_audio_track<'a>(tracks: &'a [TrackInfo]) -> Option<&'a str> {
let audio_tracks: Vec<&TrackInfo> = tracks.iter().filter(|t| t.track_type == "audio").collect();
if audio_tracks.is_empty() {
return None;
}
// 1. If default track can be determined, use it
if let Some(dt) = audio_tracks.iter().find(|t| t.is_default) {
return Some(&dt.id);
}
// 2. Default cannot be determined: try English first
if let Some(en) = audio_tracks.iter().find(|t| is_english_lang(&t.lang, &t.title)) {
return Some(&en.id);
}
// 3. Then Dutch
if let Some(nl) = audio_tracks.iter().find(|t| is_dutch_lang(&t.lang, &t.title)) {
return Some(&nl.id);
}
// 4. Fallback to first available audio track
Some(&audio_tracks[0].id)
}
pub fn select_subtitle_track<'a>(tracks: &'a [TrackInfo]) -> Option<&'a str> {
let sub_tracks: Vec<&TrackInfo> = tracks.iter().filter(|t| t.track_type == "sub").collect();
if sub_tracks.is_empty() {
return None;
}
// Prefer Dutch subtitles first (non-forced if available)
let dutch_sub = sub_tracks.iter().find(|t| is_dutch_lang(&t.lang, &t.title) && !t.is_forced)
.or_else(|| sub_tracks.iter().find(|t| is_dutch_lang(&t.lang, &t.title)));
if let Some(nl) = dutch_sub {
return Some(&nl.id);
}
// Otherwise English subtitles (non-forced if available)
let english_sub = sub_tracks.iter().find(|t| is_english_lang(&t.lang, &t.title) && !t.is_forced)
.or_else(|| sub_tracks.iter().find(|t| is_english_lang(&t.lang, &t.title)));
if let Some(en) = english_sub {
return Some(&en.id);
}
None
}
#[derive(Clone, Copy)]
@@ -185,10 +332,13 @@ impl PlayerController {
mpv.set_option_string("vo", "libmpv");
mpv.set_option_string("hwdec", &config.hwdec);
mpv.set_option_string("idle", "yes");
mpv.set_option_string("keep-open", "yes");
mpv.set_option_string("keep-open", "no");
mpv.set_option_string("osd-bar", "no");
mpv.set_option_string("osd-level", "0");
mpv.set_option_string("ytdl", "yes");
mpv.set_option_string("alang", "en,eng,nl,nld,dut");
mpv.set_option_string("slang", "nl,nld,dut,en,eng");
mpv.set_option_string("subs-with-matching-audio", "yes");
let ytdlp_path = if std::path::Path::new("/usr/local/bin/yt-dlp").exists() {
"/usr/local/bin/yt-dlp"
@@ -218,6 +368,7 @@ impl PlayerController {
libmpv_sys::mpv_observe_property(raw_mpv, 6, b"icy-title\0".as_ptr() as *const _, libmpv_sys::mpv_format_MPV_FORMAT_STRING);
libmpv_sys::mpv_observe_property(raw_mpv, 7, b"mute\0".as_ptr() as *const _, libmpv_sys::mpv_format_MPV_FORMAT_FLAG);
libmpv_sys::mpv_observe_property(raw_mpv, 8, b"metadata\0".as_ptr() as *const _, libmpv_sys::mpv_format_MPV_FORMAT_NODE);
libmpv_sys::mpv_observe_property(raw_mpv, 9, b"eof-reached\0".as_ptr() as *const _, libmpv_sys::mpv_format_MPV_FORMAT_FLAG);
}
let status_arc = Arc::new(Mutex::new(initial_status));
@@ -324,8 +475,10 @@ impl PlayerController {
st.error_message = Some(err_msg);
notify = true;
} else if reason == libmpv_sys::mpv_end_file_reason_MPV_END_FILE_REASON_EOF as i32 {
tracing::info!("MPV playback finished (EOF reached) -> returning to stopped state");
st.state = PlaybackState::Stopped;
st.time_pos = 0.0;
st.duration = 0.0;
st.media_type.clear();
st.title.clear();
st.artist.clear();
@@ -474,9 +627,10 @@ impl PlayerController {
url.to_string()
};
// Always reset video and audio track selection to auto so subsequent content plays normally
// Always reset video, audio, and subtitle track selection so subsequent content plays cleanly
self.mpv.cmd(&["set", "vid", "auto"]);
self.mpv.cmd(&["set", "aid", "auto"]);
self.mpv.cmd(&["set", "sid", "auto"]);
self.mpv.cmd(&["loadfile", &play_url, "replace"]);
Ok(())
}
@@ -637,6 +791,7 @@ fn run_mpv_event_loop(
}
if event_id == libmpv_sys::mpv_event_id_MPV_EVENT_FILE_LOADED {
mpv.auto_select_tracks();
let _ = event_tx.send(InternalMpvEvent::FileLoaded);
continue;
}
@@ -755,8 +910,196 @@ fn run_mpv_event_loop(
}
}
}
"eof-reached" => {
if unsafe { (*prop).format == libmpv_sys::mpv_format_MPV_FORMAT_FLAG } && !unsafe { (*prop).data.is_null() } {
let eof = unsafe { *((*prop).data as *const std::ffi::c_int) != 0 };
if eof {
let _ = event_tx.send(InternalMpvEvent::EndFile {
reason: libmpv_sys::mpv_end_file_reason_MPV_END_FILE_REASON_EOF as i32,
error: 0,
});
}
}
}
_ => {}
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_is_english_and_dutch_lang() {
assert!(is_english_lang("en", ""));
assert!(is_english_lang("eng", ""));
assert!(is_english_lang("en-US", ""));
assert!(is_english_lang("", "English (Stereo)"));
assert!(is_english_lang("", "Engels 5.1"));
assert!(!is_english_lang("nl", ""));
assert!(is_dutch_lang("nl", ""));
assert!(is_dutch_lang("nld", ""));
assert!(is_dutch_lang("dut", ""));
assert!(is_dutch_lang("", "Nederlands"));
assert!(is_dutch_lang("", "Dutch Surround"));
assert!(!is_dutch_lang("en", ""));
}
#[test]
fn test_select_audio_track_with_default() {
let tracks = vec![
TrackInfo {
id: "1".into(),
track_type: "audio".into(),
lang: "fra".into(),
title: "French".into(),
is_default: true,
is_forced: false,
is_selected: false,
},
TrackInfo {
id: "2".into(),
track_type: "audio".into(),
lang: "eng".into(),
title: "English".into(),
is_default: false,
is_forced: false,
is_selected: false,
},
];
// When default is determined, it picks the default track (id "1")
assert_eq!(select_audio_track(&tracks), Some("1"));
}
#[test]
fn test_select_audio_track_no_default_english_preferred() {
let tracks = vec![
TrackInfo {
id: "1".into(),
track_type: "audio".into(),
lang: "jpn".into(),
title: "Japanese".into(),
is_default: false,
is_forced: false,
is_selected: false,
},
TrackInfo {
id: "2".into(),
track_type: "audio".into(),
lang: "nld".into(),
title: "Nederlands".into(),
is_default: false,
is_forced: false,
is_selected: false,
},
TrackInfo {
id: "3".into(),
track_type: "audio".into(),
lang: "eng".into(),
title: "English".into(),
is_default: false,
is_forced: false,
is_selected: false,
},
];
// No default: English is preferred before Dutch
assert_eq!(select_audio_track(&tracks), Some("3"));
}
#[test]
fn test_select_audio_track_no_default_dutch_when_no_english() {
let tracks = vec![
TrackInfo {
id: "1".into(),
track_type: "audio".into(),
lang: "jpn".into(),
title: "Japanese".into(),
is_default: false,
is_forced: false,
is_selected: false,
},
TrackInfo {
id: "2".into(),
track_type: "audio".into(),
lang: "nld".into(),
title: "Nederlands".into(),
is_default: false,
is_forced: false,
is_selected: false,
},
];
// No default, no English: Dutch is chosen
assert_eq!(select_audio_track(&tracks), Some("2"));
}
#[test]
fn test_select_subtitle_track_dutch_preferred_over_english() {
let tracks = vec![
TrackInfo {
id: "10".into(),
track_type: "sub".into(),
lang: "eng".into(),
title: "English".into(),
is_default: true,
is_forced: false,
is_selected: false,
},
TrackInfo {
id: "11".into(),
track_type: "sub".into(),
lang: "nld".into(),
title: "Nederlands".into(),
is_default: false,
is_forced: false,
is_selected: false,
},
];
// Dutch subtitles preferred over English
assert_eq!(select_subtitle_track(&tracks), Some("11"));
}
#[test]
fn test_select_subtitle_track_english_fallback_when_no_dutch() {
let tracks = vec![
TrackInfo {
id: "10".into(),
track_type: "sub".into(),
lang: "fra".into(),
title: "French".into(),
is_default: false,
is_forced: false,
is_selected: false,
},
TrackInfo {
id: "11".into(),
track_type: "sub".into(),
lang: "eng".into(),
title: "English".into(),
is_default: false,
is_forced: false,
is_selected: false,
},
];
// No Dutch subtitles: English is selected
assert_eq!(select_subtitle_track(&tracks), Some("11"));
}
#[test]
fn test_select_subtitle_track_none_when_neither_dutch_nor_english() {
let tracks = vec![
TrackInfo {
id: "10".into(),
track_type: "sub".into(),
lang: "fra".into(),
title: "French".into(),
is_default: false,
is_forced: false,
is_selected: false,
},
];
assert_eq!(select_subtitle_track(&tracks), None);
}
}
+246 -70
View File
@@ -177,6 +177,13 @@ pub fn create_yatse_router(state: YatseServerState) -> Router {
Router::new()
.route("/", get(handle_root))
.route("/jsonrpc", post(handle_jsonrpc).get(handle_jsonrpc_get))
.route("/image", get(handle_image))
.route("/image/", get(handle_image))
.route("/image/*path", get(handle_image))
.route("/vfs", get(handle_image))
.route("/vfs/", get(handle_image))
.route("/vfs/*path", get(handle_image))
.fallback(handle_fallback)
.with_state(state)
}
@@ -184,7 +191,120 @@ async fn handle_root() -> impl IntoResponse {
"Jellyfin HTPC Kiosk (Rust) - Kodi JSON-RPC Server for Yatse"
}
fn percent_decode(input: &str) -> String {
pub fn percent_encode(s: &str) -> String {
let mut out = String::new();
for b in s.as_bytes() {
if b.is_ascii_alphanumeric() || *b == b'-' || *b == b'_' || *b == b'.' || *b == b'~' {
out.push(*b as char);
} else {
out.push_str(&format!("%{:02x}", b));
}
}
out
}
pub fn to_kodi_art(url: &str) -> String {
if url.is_empty() {
return String::new();
}
if url.starts_with("image://") {
return url.to_string();
}
format!("image://{}/", percent_encode(url))
}
pub fn extract_image_url(uri_path: &str, query: Option<&str>) -> Option<String> {
let mut s = uri_path;
for pfx in ["/image/", "/image", "/vfs/", "/vfs"] {
if s.starts_with(pfx) {
s = &s[pfx.len()..];
break;
}
}
let mut cur = s.to_string();
for _ in 0..3 {
let decoded = percent_decode(&cur);
let trimmed = decoded.trim().trim_end_matches('/');
if let Some(inner) = trimmed.strip_prefix("image://") {
cur = inner.trim_end_matches('/').to_string();
continue;
}
if trimmed.starts_with("http://") || trimmed.starts_with("https://") {
cur = trimmed.to_string();
break;
}
cur = decoded;
}
if cur.starts_with("http://") || cur.starts_with("https://") {
if let Some(q) = query {
if !cur.contains('?') {
cur = format!("{}?{}", cur, q);
}
}
if cur.contains('?') {
cur = cur.trim_end_matches('/').to_string();
}
return Some(cur);
}
None
}
async fn handle_image(
State(state): State<YatseServerState>,
uri: axum::http::Uri,
) -> impl IntoResponse {
let path = uri.path();
let query = uri.query();
if let Some(target_url) = extract_image_url(path, query) {
tracing::info!("Proxying Kodi image: {} -> {}", path, target_url);
let client = reqwest::Client::new();
let mut req = client.get(&target_url);
if let Some(token) = state.client.get_token().await {
req = req.header("X-Emby-Token", token);
}
match req.send().await {
Ok(resp) => {
let status = resp.status();
if status.is_success() {
let content_type = resp.headers()
.get(reqwest::header::CONTENT_TYPE)
.and_then(|v| v.to_str().ok())
.unwrap_or("image/jpeg")
.to_string();
if let Ok(bytes) = resp.bytes().await {
return (
axum::http::StatusCode::OK,
[
(axum::http::header::CONTENT_TYPE, content_type),
(axum::http::header::CACHE_CONTROL, "public, max-age=31536000".to_string()),
],
bytes,
).into_response();
}
} else {
tracing::warn!("Failed to fetch image from Jellyfin: status={} url={}", status, target_url);
}
}
Err(e) => {
tracing::warn!("Error requesting image from Jellyfin: {} url={}", e, target_url);
}
}
} else {
tracing::warn!("Could not extract image URL from uri: path={} query={:?}", path, query);
}
(axum::http::StatusCode::NOT_FOUND, "Image not found").into_response()
}
async fn handle_fallback(
State(state): State<YatseServerState>,
uri: axum::http::Uri,
) -> impl IntoResponse {
handle_image(State(state), uri).await
}
pub fn percent_decode(input: &str) -> String {
let mut bytes = Vec::new();
let mut chars = input.bytes();
while let Some(b) = chars.next() {
@@ -1208,6 +1328,38 @@ async fn dispatch_rpc(state: &YatseServerState, req: JsonRpcRequest) -> Value {
"id": id
})
}
"Files.PrepareDownload" => {
let path_opt = req.params.as_ref()
.and_then(|p| p.get("path").or_else(|| p.as_array().and_then(|a| a.first())))
.and_then(|s| s.as_str());
if let Some(path) = path_opt {
let encoded = percent_encode(path);
json!({
"jsonrpc": "2.0",
"result": {
"details": {
"path": format!("image/{}", encoded)
},
"mode": "redirect",
"protocol": "http"
},
"id": id
})
} else {
json!({
"jsonrpc": "2.0",
"result": {
"details": {
"path": ""
},
"mode": "redirect",
"protocol": "http"
},
"id": id
})
}
}
"VideoLibrary.GetMovies" => {
let lib = state.client.get_library().await;
let base_url = &state.client.config.server_url;
@@ -1230,12 +1382,12 @@ async fn dispatch_rpc(state: &YatseServerState, req: JsonRpcRequest) -> Value {
"playcount": playcount,
"genre": genres,
"file": format!("jellyfin://movie/{}", m.id),
"thumbnail": m.thumb_url(base_url),
"fanart": m.backdrop_url(base_url),
"thumbnail": to_kodi_art(&m.thumb_url(base_url)),
"fanart": to_kodi_art(&m.backdrop_url(base_url)),
"art": {
"poster": m.poster_url(base_url),
"fanart": m.backdrop_url(base_url),
"thumb": m.thumb_url(base_url),
"poster": to_kodi_art(&m.poster_url(base_url)),
"fanart": to_kodi_art(&m.backdrop_url(base_url)),
"thumb": to_kodi_art(&m.thumb_url(base_url)),
},
"resume": {
"position": 0.0,
@@ -1281,12 +1433,12 @@ async fn dispatch_rpc(state: &YatseServerState, req: JsonRpcRequest) -> Value {
"playcount": playcount,
"genre": genres,
"file": format!("jellyfin://movie/{}", m.id),
"thumbnail": m.thumb_url(base_url),
"fanart": m.backdrop_url(base_url),
"thumbnail": to_kodi_art(&m.thumb_url(base_url)),
"fanart": to_kodi_art(&m.backdrop_url(base_url)),
"art": {
"poster": m.poster_url(base_url),
"fanart": m.backdrop_url(base_url),
"thumb": m.thumb_url(base_url),
"poster": to_kodi_art(&m.poster_url(base_url)),
"fanart": to_kodi_art(&m.backdrop_url(base_url)),
"thumb": to_kodi_art(&m.thumb_url(base_url)),
},
"resume": {
"position": 0.0,
@@ -1326,12 +1478,12 @@ async fn dispatch_rpc(state: &YatseServerState, req: JsonRpcRequest) -> Value {
"playcount": playcount,
"genre": genres,
"file": format!("jellyfin://movie/{}", m.id),
"thumbnail": m.thumb_url(base_url),
"fanart": m.backdrop_url(base_url),
"thumbnail": to_kodi_art(&m.thumb_url(base_url)),
"fanart": to_kodi_art(&m.backdrop_url(base_url)),
"art": {
"poster": m.poster_url(base_url),
"fanart": m.backdrop_url(base_url),
"thumb": m.thumb_url(base_url),
"poster": to_kodi_art(&m.poster_url(base_url)),
"fanart": to_kodi_art(&m.backdrop_url(base_url)),
"thumb": to_kodi_art(&m.thumb_url(base_url)),
}
}));
}
@@ -1368,12 +1520,12 @@ async fn dispatch_rpc(state: &YatseServerState, req: JsonRpcRequest) -> Value {
"season": s.child_count.unwrap_or(1),
"watchedepisodes": 0,
"file": format!("jellyfin://show/{}", s.id),
"thumbnail": s.thumb_url(base_url),
"fanart": s.backdrop_url(base_url),
"thumbnail": to_kodi_art(&s.thumb_url(base_url)),
"fanart": to_kodi_art(&s.backdrop_url(base_url)),
"art": {
"poster": s.poster_url(base_url),
"fanart": s.backdrop_url(base_url),
"thumb": s.thumb_url(base_url),
"poster": to_kodi_art(&s.poster_url(base_url)),
"fanart": to_kodi_art(&s.backdrop_url(base_url)),
"thumb": to_kodi_art(&s.thumb_url(base_url)),
}
}));
}
@@ -1414,12 +1566,12 @@ async fn dispatch_rpc(state: &YatseServerState, req: JsonRpcRequest) -> Value {
"season": s.child_count.unwrap_or(1),
"watchedepisodes": 0,
"file": format!("jellyfin://show/{}", s.id),
"thumbnail": s.thumb_url(base_url),
"fanart": s.backdrop_url(base_url),
"thumbnail": to_kodi_art(&s.thumb_url(base_url)),
"fanart": to_kodi_art(&s.backdrop_url(base_url)),
"art": {
"poster": s.poster_url(base_url),
"fanart": s.backdrop_url(base_url),
"thumb": s.thumb_url(base_url),
"poster": to_kodi_art(&s.poster_url(base_url)),
"fanart": to_kodi_art(&s.backdrop_url(base_url)),
"thumb": to_kodi_art(&s.thumb_url(base_url)),
}
});
}
@@ -1475,12 +1627,12 @@ async fn dispatch_rpc(state: &YatseServerState, req: JsonRpcRequest) -> Value {
"label": format!("Seizoen {}", s_num),
"episode": ep_count,
"watchedepisodes": 0,
"thumbnail": poster,
"fanart": backdrop,
"thumbnail": to_kodi_art(&poster),
"fanart": to_kodi_art(&backdrop),
"art": {
"poster": poster,
"fanart": backdrop,
"thumb": poster
"poster": to_kodi_art(&poster),
"fanart": to_kodi_art(&backdrop),
"thumb": to_kodi_art(&poster)
}
}));
}
@@ -1557,12 +1709,12 @@ async fn dispatch_rpc(state: &YatseServerState, req: JsonRpcRequest) -> Value {
"rating": ep.rating.unwrap_or(0.0),
"playcount": playcount,
"file": format!("jellyfin://episode/{}", ep.id),
"thumbnail": ep.thumb_url(base_url),
"fanart": ep.backdrop_url(base_url),
"thumbnail": to_kodi_art(&ep.thumb_url(base_url)),
"fanart": to_kodi_art(&ep.backdrop_url(base_url)),
"art": {
"thumb": ep.thumb_url(base_url),
"poster": ep.poster_url(base_url),
"fanart": ep.backdrop_url(base_url),
"thumb": to_kodi_art(&ep.thumb_url(base_url)),
"poster": to_kodi_art(&ep.poster_url(base_url)),
"fanart": to_kodi_art(&ep.backdrop_url(base_url)),
}
}));
}
@@ -1606,12 +1758,12 @@ async fn dispatch_rpc(state: &YatseServerState, req: JsonRpcRequest) -> Value {
"rating": ep.rating.unwrap_or(0.0),
"playcount": playcount,
"file": format!("jellyfin://episode/{}", ep.id),
"thumbnail": ep.thumb_url(base_url),
"fanart": ep.backdrop_url(base_url),
"thumbnail": to_kodi_art(&ep.thumb_url(base_url)),
"fanart": to_kodi_art(&ep.backdrop_url(base_url)),
"art": {
"thumb": ep.thumb_url(base_url),
"poster": ep.poster_url(base_url),
"fanart": ep.backdrop_url(base_url),
"thumb": to_kodi_art(&ep.thumb_url(base_url)),
"poster": to_kodi_art(&ep.poster_url(base_url)),
"fanart": to_kodi_art(&ep.backdrop_url(base_url)),
}
}));
}
@@ -1653,12 +1805,12 @@ async fn dispatch_rpc(state: &YatseServerState, req: JsonRpcRequest) -> Value {
"rating": ep.rating.unwrap_or(0.0),
"playcount": playcount,
"file": format!("jellyfin://episode/{}", ep.id),
"thumbnail": ep.thumb_url(base_url),
"fanart": ep.backdrop_url(base_url),
"thumbnail": to_kodi_art(&ep.thumb_url(base_url)),
"fanart": to_kodi_art(&ep.backdrop_url(base_url)),
"art": {
"thumb": ep.thumb_url(base_url),
"poster": ep.poster_url(base_url),
"fanart": ep.backdrop_url(base_url),
"thumb": to_kodi_art(&ep.thumb_url(base_url)),
"poster": to_kodi_art(&ep.poster_url(base_url)),
"fanart": to_kodi_art(&ep.backdrop_url(base_url)),
}
});
}
@@ -1702,11 +1854,11 @@ async fn dispatch_rpc(state: &YatseServerState, req: JsonRpcRequest) -> Value {
"artistid": aid,
"artist": a.name,
"label": a.name,
"thumbnail": a.square_url(base_url),
"fanart": a.backdrop_url(base_url),
"thumbnail": to_kodi_art(&a.square_url(base_url)),
"fanart": to_kodi_art(&a.backdrop_url(base_url)),
"art": {
"thumb": a.square_url(base_url),
"fanart": a.backdrop_url(base_url),
"thumb": to_kodi_art(&a.square_url(base_url)),
"fanart": to_kodi_art(&a.backdrop_url(base_url)),
}
}));
}
@@ -1734,11 +1886,11 @@ async fn dispatch_rpc(state: &YatseServerState, req: JsonRpcRequest) -> Value {
"label": al.name,
"artist": al.artist_name.as_deref().unwrap_or(""),
"year": al.year.unwrap_or(0),
"thumbnail": al.square_url(base_url),
"fanart": al.backdrop_url(base_url),
"thumbnail": to_kodi_art(&al.square_url(base_url)),
"fanart": to_kodi_art(&al.backdrop_url(base_url)),
"art": {
"thumb": al.square_url(base_url),
"fanart": al.backdrop_url(base_url),
"thumb": to_kodi_art(&al.square_url(base_url)),
"fanart": to_kodi_art(&al.backdrop_url(base_url)),
}
}));
}
@@ -1766,11 +1918,11 @@ async fn dispatch_rpc(state: &YatseServerState, req: JsonRpcRequest) -> Value {
"label": al.name,
"artist": al.artist_name.as_deref().unwrap_or(""),
"year": al.year.unwrap_or(0),
"thumbnail": al.square_url(base_url),
"fanart": al.backdrop_url(base_url),
"thumbnail": to_kodi_art(&al.square_url(base_url)),
"fanart": to_kodi_art(&al.backdrop_url(base_url)),
"art": {
"thumb": al.square_url(base_url),
"fanart": al.backdrop_url(base_url),
"thumb": to_kodi_art(&al.square_url(base_url)),
"fanart": to_kodi_art(&al.backdrop_url(base_url)),
}
}));
}
@@ -1831,11 +1983,11 @@ async fn dispatch_rpc(state: &YatseServerState, req: JsonRpcRequest) -> Value {
"disc": disc,
"year": s.year.unwrap_or(0),
"file": file,
"thumbnail": s.square_url(base_url),
"fanart": s.backdrop_url(base_url),
"thumbnail": to_kodi_art(&s.square_url(base_url)),
"fanart": to_kodi_art(&s.backdrop_url(base_url)),
"art": {
"thumb": s.square_url(base_url),
"fanart": s.backdrop_url(base_url)
"thumb": to_kodi_art(&s.square_url(base_url)),
"fanart": to_kodi_art(&s.backdrop_url(base_url))
}
}));
}
@@ -1889,11 +2041,11 @@ async fn dispatch_rpc(state: &YatseServerState, req: JsonRpcRequest) -> Value {
"label": al.name,
"artist": al.artist_name.as_deref().unwrap_or(""),
"year": al.year.unwrap_or(0),
"thumbnail": al.square_url(base_url),
"fanart": al.backdrop_url(base_url),
"thumbnail": to_kodi_art(&al.square_url(base_url)),
"fanart": to_kodi_art(&al.backdrop_url(base_url)),
"art": {
"thumb": al.square_url(base_url),
"fanart": al.backdrop_url(base_url),
"thumb": to_kodi_art(&al.square_url(base_url)),
"fanart": to_kodi_art(&al.backdrop_url(base_url)),
}
});
}
@@ -1922,11 +2074,11 @@ async fn dispatch_rpc(state: &YatseServerState, req: JsonRpcRequest) -> Value {
"artistid": aid,
"artist": ar.name,
"label": ar.name,
"thumbnail": ar.square_url(base_url),
"fanart": ar.backdrop_url(base_url),
"thumbnail": to_kodi_art(&ar.square_url(base_url)),
"fanart": to_kodi_art(&ar.backdrop_url(base_url)),
"art": {
"thumb": ar.square_url(base_url),
"fanart": ar.backdrop_url(base_url),
"thumb": to_kodi_art(&ar.square_url(base_url)),
"fanart": to_kodi_art(&ar.backdrop_url(base_url)),
}
});
}
@@ -2222,4 +2374,28 @@ mod tests {
"https://youtube.com/watch?v=eMEIt6_mGSI&is=hEdy88z6Nhf7gRSE"
);
}
#[test]
fn test_extract_image_url_variations() {
let original = "http://10.2.0.4:8096/jellyfin/Items/125c/Images/Primary?fillWidth=320&fillHeight=480&quality=85";
let kodi_art = to_kodi_art(original);
assert!(kodi_art.starts_with("image://"));
assert!(kodi_art.ends_with('/'));
// Case 1: Yatse calls /image/<encoded>
let path1 = format!("/image/{}", percent_encode(&kodi_art));
let res1 = extract_image_url(&path1, None).expect("extracted 1");
assert_eq!(res1, original);
// Case 2: Yatse calls /image/<single-encoded-http-with-trailing-slash>
let path2 = format!("/image/{}/", percent_encode(original));
let res2 = extract_image_url(&path2, None).expect("extracted 2");
assert_eq!(res2, original);
// Case 3: Yatse calls /vfs/<kodi-art>
let path3 = format!("/vfs/{}", percent_encode(&kodi_art));
let res3 = extract_image_url(&path3, None).expect("extracted 3");
assert_eq!(res3, original);
}
}
@@ -0,0 +1 @@
{"rustc_fingerprint":14753936209423482788,"outputs":{"7971740275564407648":{"success":true,"status":"","code":0,"stdout":"___\nlib___.rlib\nlib___.so\nlib___.so\nlib___.a\nlib___.so\n/home/bart/.rustup/toolchains/stable-x86_64-unknown-linux-gnu\noff\npacked\nunpacked\n___\ndebug_assertions\npanic=\"unwind\"\nproc_macro\ntarget_abi=\"\"\ntarget_arch=\"x86_64\"\ntarget_endian=\"little\"\ntarget_env=\"gnu\"\ntarget_family=\"unix\"\ntarget_feature=\"fxsr\"\ntarget_feature=\"sse\"\ntarget_feature=\"sse2\"\ntarget_has_atomic=\"16\"\ntarget_has_atomic=\"32\"\ntarget_has_atomic=\"64\"\ntarget_has_atomic=\"8\"\ntarget_has_atomic=\"ptr\"\ntarget_os=\"linux\"\ntarget_pointer_width=\"64\"\ntarget_vendor=\"unknown\"\nunix\n","stderr":""},"17747080675513052775":{"success":true,"status":"","code":0,"stdout":"rustc 1.95.0 (59807616e 2026-04-14)\nbinary: rustc\ncommit-hash: 59807616e1fa2540724bfbac14d7976d7e4a3860\ncommit-date: 2026-04-14\nhost: x86_64-unknown-linux-gnu\nrelease: 1.95.0\nLLVM version: 22.1.2\n","stderr":""}},"successes":{}}
+3
View File
@@ -0,0 +1,3 @@
Signature: 8a477f597d28d172789f06886806bc55
# This file is a cache directory tag created by cargo.
# For information about cache directory tags see https://bford.info/cachedir/
@@ -0,0 +1 @@
This file has an mtime of when this was started.
@@ -0,0 +1 @@
3ac0e9726997097b
@@ -0,0 +1 @@
{"rustc":7458672600737419911,"features":"[\"default\", \"gvar-alloc\", \"std\", \"variable-fonts\"]","declared_features":"[\"default\", \"gvar-alloc\", \"libm\", \"std\", \"variable-fonts\"]","target":11794240345726188307,"profile":2241668132362809309,"path":5649524171242691192,"deps":[[4945662571602681759,"ab_glyph_rasterizer",false,2687199477971215692],[5327495677235252177,"owned_ttf_parser",false,13558008051659722080]],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/ab_glyph-0217f9255a6590dd/dep-lib-ab_glyph","checksum":false}}],"rustflags":[],"config":8247474407144887393,"compile_kind":0}
@@ -0,0 +1 @@
This file has an mtime of when this was started.
@@ -0,0 +1 @@
5d1ec60264864a72
@@ -0,0 +1 @@
{"rustc":7458672600737419911,"features":"[\"default\", \"gvar-alloc\", \"std\", \"variable-fonts\"]","declared_features":"[\"default\", \"gvar-alloc\", \"libm\", \"std\", \"variable-fonts\"]","target":11794240345726188307,"profile":15657897354478470176,"path":5649524171242691192,"deps":[[4945662571602681759,"ab_glyph_rasterizer",false,15637680591314221340],[5327495677235252177,"owned_ttf_parser",false,9976818268436013839]],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/ab_glyph-351e6496e093a624/dep-lib-ab_glyph","checksum":false}}],"rustflags":[],"config":8247474407144887393,"compile_kind":0}
@@ -0,0 +1 @@
This file has an mtime of when this was started.
@@ -0,0 +1 @@
{"rustc":7458672600737419911,"features":"[\"default\", \"std\"]","declared_features":"[\"default\", \"libm\", \"std\"]","target":4335109392423587462,"profile":2241668132362809309,"path":13818387167479311948,"deps":[],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/ab_glyph_rasterizer-34531215e91a4783/dep-lib-ab_glyph_rasterizer","checksum":false}}],"rustflags":[],"config":8247474407144887393,"compile_kind":0}
@@ -0,0 +1 @@
This file has an mtime of when this was started.
@@ -0,0 +1 @@
{"rustc":7458672600737419911,"features":"[\"default\", \"std\"]","declared_features":"[\"default\", \"libm\", \"std\"]","target":4335109392423587462,"profile":15657897354478470176,"path":13818387167479311948,"deps":[],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/ab_glyph_rasterizer-e4817a80f37d7712/dep-lib-ab_glyph_rasterizer","checksum":false}}],"rustflags":[],"config":8247474407144887393,"compile_kind":0}
@@ -0,0 +1 @@
This file has an mtime of when this was started.
@@ -0,0 +1 @@
699c6ac28aa5af89
@@ -0,0 +1 @@
{"rustc":7458672600737419911,"features":"[]","declared_features":"[\"core\", \"default\", \"rustc-dep-of-std\", \"std\"]","target":6569825234462323107,"profile":15657897354478470176,"path":4529919677372112567,"deps":[],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/adler2-5305f511e1c31af3/dep-lib-adler2","checksum":false}}],"rustflags":[],"config":8247474407144887393,"compile_kind":0}
@@ -0,0 +1 @@
This file has an mtime of when this was started.
@@ -0,0 +1 @@
892547091c8cf972
@@ -0,0 +1 @@
{"rustc":7458672600737419911,"features":"[]","declared_features":"[\"core\", \"default\", \"rustc-dep-of-std\", \"std\"]","target":6569825234462323107,"profile":2241668132362809309,"path":4529919677372112567,"deps":[],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/adler2-b5185ec3be97cc68/dep-lib-adler2","checksum":false}}],"rustflags":[],"config":8247474407144887393,"compile_kind":0}
@@ -0,0 +1 @@
This file has an mtime of when this was started.
@@ -0,0 +1 @@
5068cea52b295da9
@@ -0,0 +1 @@
{"rustc":7458672600737419911,"features":"[\"default\", \"getrandom\", \"no-rng\", \"runtime-rng\", \"std\"]","declared_features":"[\"atomic-polyfill\", \"compile-time-rng\", \"const-random\", \"default\", \"getrandom\", \"nightly-arm-aes\", \"no-rng\", \"runtime-rng\", \"serde\", \"std\"]","target":8470944000320059508,"profile":15657897354478470176,"path":3288015079357928609,"deps":[[966925859616469517,"build_script_build",false,9203697050838375196],[4321869508056025743,"zerocopy",false,2467696241657189483],[5855319743879205494,"once_cell",false,12280998255132305995],[15482175856213997617,"cfg_if",false,14176503484701607164],[18408407127522236545,"getrandom",false,2733066888145013121]],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/ahash-2b16cca8b306abc9/dep-lib-ahash","checksum":false}}],"rustflags":[],"config":8247474407144887393,"compile_kind":0}
@@ -0,0 +1 @@
This file has an mtime of when this was started.
@@ -0,0 +1 @@
842172d1ed01f74c
@@ -0,0 +1 @@
{"rustc":7458672600737419911,"features":"[\"default\", \"getrandom\", \"no-rng\", \"runtime-rng\", \"std\"]","declared_features":"[\"atomic-polyfill\", \"compile-time-rng\", \"const-random\", \"default\", \"getrandom\", \"nightly-arm-aes\", \"no-rng\", \"runtime-rng\", \"serde\", \"std\"]","target":8470944000320059508,"profile":2241668132362809309,"path":3288015079357928609,"deps":[[966925859616469517,"build_script_build",false,9203697050838375196],[4321869508056025743,"zerocopy",false,15765362044039701013],[5855319743879205494,"once_cell",false,11850536697227471877],[15482175856213997617,"cfg_if",false,14735624068320645977],[18408407127522236545,"getrandom",false,15986369605517680036]],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/ahash-a3e6e2b710a4e0bc/dep-lib-ahash","checksum":false}}],"rustflags":[],"config":8247474407144887393,"compile_kind":0}
@@ -0,0 +1 @@
{"rustc":7458672600737419911,"features":"","declared_features":"","target":0,"profile":0,"path":0,"deps":[[966925859616469517,"build_script_build",false,17893360845909163615]],"local":[{"RerunIfChanged":{"output":"debug/build/ahash-a57948a68d40e0a7/output","paths":["build.rs"]}}],"rustflags":[],"config":0,"compile_kind":0}
@@ -0,0 +1 @@
{"rustc":7458672600737419911,"features":"[\"default\", \"getrandom\", \"no-rng\", \"runtime-rng\", \"std\"]","declared_features":"[\"atomic-polyfill\", \"compile-time-rng\", \"const-random\", \"default\", \"getrandom\", \"nightly-arm-aes\", \"no-rng\", \"runtime-rng\", \"serde\", \"std\"]","target":17883862002600103897,"profile":2225463790103693989,"path":4299570802035626402,"deps":[[5398981501050481332,"version_check",false,14943717254255103075]],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/ahash-d6dd36af1ebc73ff/dep-build-script-build-script-build","checksum":false}}],"rustflags":[],"config":8247474407144887393,"compile_kind":0}
@@ -0,0 +1 @@
This file has an mtime of when this was started.
@@ -0,0 +1 @@
This file has an mtime of when this was started.
@@ -0,0 +1 @@
181ee43004d5c945
@@ -0,0 +1 @@
{"rustc":7458672600737419911,"features":"[]","declared_features":"[\"core-graphics\", \"default\", \"image\", \"image-data\", \"wayland-data-control\", \"windows-sys\", \"wl-clipboard-rs\"]","target":1337616771932055151,"profile":2241668132362809309,"path":17166859248196286572,"deps":[[6803352382179706244,"percent_encoding",false,7822495553327927090],[7264839661287455353,"log",false,12557639338793902813],[12459942763388630573,"parking_lot",false,17310600078098908667],[15803581142294733505,"x11rb",false,15864593136328662872]],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/arboard-4d99d111aeeb9d85/dep-lib-arboard","checksum":false}}],"rustflags":[],"config":8247474407144887393,"compile_kind":0}
@@ -0,0 +1 @@
This file has an mtime of when this was started.
@@ -0,0 +1 @@
941081f2db9ef8de
@@ -0,0 +1 @@
{"rustc":7458672600737419911,"features":"[]","declared_features":"[\"core-graphics\", \"default\", \"image\", \"image-data\", \"wayland-data-control\", \"windows-sys\", \"wl-clipboard-rs\"]","target":1337616771932055151,"profile":15657897354478470176,"path":17166859248196286572,"deps":[[6803352382179706244,"percent_encoding",false,10204264024909671241],[7264839661287455353,"log",false,10968825162537122904],[12459942763388630573,"parking_lot",false,7493661607571755906],[15803581142294733505,"x11rb",false,16408250448247954789]],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/arboard-941d3c3f22ae1264/dep-lib-arboard","checksum":false}}],"rustflags":[],"config":8247474407144887393,"compile_kind":0}
@@ -0,0 +1 @@
This file has an mtime of when this was started.
@@ -0,0 +1 @@
{"rustc":7458672600737419911,"features":"[\"alloc\", \"default\"]","declared_features":"[\"alloc\", \"default\"]","target":8427501830925002634,"profile":2241668132362809309,"path":11315664722530468864,"deps":[],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/as-raw-xcb-connection-1b429b60928621fa/dep-lib-as_raw_xcb_connection","checksum":false}}],"rustflags":[],"config":8247474407144887393,"compile_kind":0}
@@ -0,0 +1 @@
This file has an mtime of when this was started.
@@ -0,0 +1 @@
{"rustc":7458672600737419911,"features":"[\"alloc\", \"default\"]","declared_features":"[\"alloc\", \"default\"]","target":8427501830925002634,"profile":15657897354478470176,"path":11315664722530468864,"deps":[],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/as-raw-xcb-connection-9d9025af637da936/dep-lib-as_raw_xcb_connection","checksum":false}}],"rustflags":[],"config":8247474407144887393,"compile_kind":0}
@@ -0,0 +1 @@
This file has an mtime of when this was started.
@@ -0,0 +1 @@
{"rustc":7458672600737419911,"features":"[]","declared_features":"[]","target":5116616278641129243,"profile":2225463790103693989,"path":8469429350005984183,"deps":[[8711674966389384079,"syn",false,12098035866327759596],[8949245912927223590,"quote",false,6701396820762924173],[16346726298725429545,"proc_macro2",false,12397529350165103128]],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/async-trait-b09e65b0c30ab584/dep-lib-async_trait","checksum":false}}],"rustflags":[],"config":8247474407144887393,"compile_kind":0}
@@ -0,0 +1 @@
This file has an mtime of when this was started.
@@ -0,0 +1 @@
{"rustc":7458672600737419911,"features":"[]","declared_features":"[\"portable-atomic\"]","target":14411119108718288063,"profile":15657897354478470176,"path":551700150563500949,"deps":[],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/atomic-waker-199214763a0024c7/dep-lib-atomic_waker","checksum":false}}],"rustflags":[],"config":8247474407144887393,"compile_kind":0}
@@ -0,0 +1 @@
This file has an mtime of when this was started.
@@ -0,0 +1 @@
{"rustc":7458672600737419911,"features":"[]","declared_features":"[\"portable-atomic\"]","target":14411119108718288063,"profile":2241668132362809309,"path":551700150563500949,"deps":[],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/atomic-waker-96e688c59e310096/dep-lib-atomic_waker","checksum":false}}],"rustflags":[],"config":8247474407144887393,"compile_kind":0}
@@ -0,0 +1 @@
This file has an mtime of when this was started.
@@ -0,0 +1 @@
1fd5c4355ae3a430
@@ -0,0 +1 @@
{"rustc":7458672600737419911,"features":"[]","declared_features":"[]","target":6962977057026645649,"profile":2225463790103693989,"path":6403542389053020766,"deps":[],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/autocfg-374b6208e55aaac6/dep-lib-autocfg","checksum":false}}],"rustflags":[],"config":8247474407144887393,"compile_kind":0}
@@ -0,0 +1 @@
This file has an mtime of when this was started.
@@ -0,0 +1 @@
ef0aa26d005fff9d
@@ -0,0 +1 @@
{"rustc":7458672600737419911,"features":"[\"http1\", \"json\", \"tokio\"]","declared_features":"[\"__private_docs\", \"default\", \"form\", \"http1\", \"http2\", \"json\", \"macros\", \"matched-path\", \"multipart\", \"original-uri\", \"query\", \"tokio\", \"tower-log\", \"tracing\", \"ws\"]","target":13920321295547257648,"profile":15657897354478470176,"path":5480532092245258048,"deps":[[784494742817713399,"tower_service",false,7096277064673234537],[927329442006724342,"http_body_util",false,10169796446186527904],[2251399859588827949,"pin_project_lite",false,16496052122936131536],[2517136641825875337,"sync_wrapper",false,5993311283381840508],[3632162862999675140,"tower",false,5005464751011667245],[4359148418957042248,"axum_core",false,8050097667477050567],[5532778797167691009,"itoa",false,12625258790664933256],[6444209561448300374,"futures_util",false,2711510235064628060],[6557439603276904804,"serde",false,10117669846973571935],[6803352382179706244,"percent_encoding",false,10204264024909671241],[7712452662827335977,"tower_layer",false,1094941957052667426],[8160210889872729633,"serde_json",false,1366118035012243637],[9678799920983747518,"matchit",false,466840185268934315],[10229185211513642314,"mime",false,3488359487242993514],[10260941683582100114,"async_trait",false,15969331622671879673],[11926622812581095017,"bytes",false,14261233726640694094],[12328341851100645683,"http",false,3354019498251889263],[12613788554453945248,"memchr",false,14549996354126691281],[14092367075979712649,"hyper",false,11409751076288742659],[14814583949208169760,"serde_path_to_error",false,3598317744880925089],[15618961772992676818,"hyper_util",false,5909608442022453208],[16991438365634268121,"rustversion",false,3477745542916629179],[17866805608252134857,"tokio",false,10735189983294870288],[17905774625381964326,"http_body",false,8880207948320647891]],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/axum-05a9f8609164393f/dep-lib-axum","checksum":false}}],"rustflags":[],"config":8247474407144887393,"compile_kind":0}
@@ -0,0 +1 @@
This file has an mtime of when this was started.
@@ -0,0 +1 @@
a0c307ffee1dce9e
@@ -0,0 +1 @@
{"rustc":7458672600737419911,"features":"[]","declared_features":"[\"__private_docs\", \"tracing\"]","target":2565713999752801252,"profile":2241668132362809309,"path":4251269609799930624,"deps":[[784494742817713399,"tower_service",false,8641590773704259889],[927329442006724342,"http_body_util",false,17754582866654415635],[2251399859588827949,"pin_project_lite",false,6123724137355080315],[2517136641825875337,"sync_wrapper",false,6298301960919763486],[6444209561448300374,"futures_util",false,9044899930474534103],[7712452662827335977,"tower_layer",false,11366169840293730998],[10229185211513642314,"mime",false,1190444657937747181],[10260941683582100114,"async_trait",false,15969331622671879673],[11926622812581095017,"bytes",false,10182236420260745664],[12328341851100645683,"http",false,147347367879140718],[16991438365634268121,"rustversion",false,3477745542916629179],[17905774625381964326,"http_body",false,8442883690053730452]],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/axum-core-64fa7952c1b326c0/dep-lib-axum_core","checksum":false}}],"rustflags":[],"config":8247474407144887393,"compile_kind":0}
@@ -0,0 +1 @@
This file has an mtime of when this was started.
@@ -0,0 +1 @@
c7942b652db1b76f
@@ -0,0 +1 @@
{"rustc":7458672600737419911,"features":"[]","declared_features":"[\"__private_docs\", \"tracing\"]","target":2565713999752801252,"profile":15657897354478470176,"path":4251269609799930624,"deps":[[784494742817713399,"tower_service",false,7096277064673234537],[927329442006724342,"http_body_util",false,10169796446186527904],[2251399859588827949,"pin_project_lite",false,16496052122936131536],[2517136641825875337,"sync_wrapper",false,5993311283381840508],[6444209561448300374,"futures_util",false,2711510235064628060],[7712452662827335977,"tower_layer",false,1094941957052667426],[10229185211513642314,"mime",false,3488359487242993514],[10260941683582100114,"async_trait",false,15969331622671879673],[11926622812581095017,"bytes",false,14261233726640694094],[12328341851100645683,"http",false,3354019498251889263],[16991438365634268121,"rustversion",false,3477745542916629179],[17905774625381964326,"http_body",false,8880207948320647891]],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/axum-core-ef7cc4f7dfec6902/dep-lib-axum_core","checksum":false}}],"rustflags":[],"config":8247474407144887393,"compile_kind":0}
@@ -0,0 +1 @@
This file has an mtime of when this was started.
@@ -0,0 +1 @@
6e25403a230c2947
@@ -0,0 +1 @@
{"rustc":7458672600737419911,"features":"[\"http1\", \"json\", \"tokio\"]","declared_features":"[\"__private_docs\", \"default\", \"form\", \"http1\", \"http2\", \"json\", \"macros\", \"matched-path\", \"multipart\", \"original-uri\", \"query\", \"tokio\", \"tower-log\", \"tracing\", \"ws\"]","target":13920321295547257648,"profile":2241668132362809309,"path":5480532092245258048,"deps":[[784494742817713399,"tower_service",false,8641590773704259889],[927329442006724342,"http_body_util",false,17754582866654415635],[2251399859588827949,"pin_project_lite",false,6123724137355080315],[2517136641825875337,"sync_wrapper",false,6298301960919763486],[3632162862999675140,"tower",false,6705119147173761980],[4359148418957042248,"axum_core",false,11443116615513129888],[5532778797167691009,"itoa",false,13304641298247861803],[6444209561448300374,"futures_util",false,9044899930474534103],[6557439603276904804,"serde",false,7841406684811627843],[6803352382179706244,"percent_encoding",false,7822495553327927090],[7712452662827335977,"tower_layer",false,11366169840293730998],[8160210889872729633,"serde_json",false,2833392396230018759],[9678799920983747518,"matchit",false,15382285630579014378],[10229185211513642314,"mime",false,1190444657937747181],[10260941683582100114,"async_trait",false,15969331622671879673],[11926622812581095017,"bytes",false,10182236420260745664],[12328341851100645683,"http",false,147347367879140718],[12613788554453945248,"memchr",false,6094110374646518287],[14092367075979712649,"hyper",false,2757493669660138066],[14814583949208169760,"serde_path_to_error",false,2322671378224961024],[15618961772992676818,"hyper_util",false,16482928109375956042],[16991438365634268121,"rustversion",false,3477745542916629179],[17866805608252134857,"tokio",false,12111369162276798285],[17905774625381964326,"http_body",false,8442883690053730452]],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/axum-d810a780cd9dc547/dep-lib-axum","checksum":false}}],"rustflags":[],"config":8247474407144887393,"compile_kind":0}
@@ -0,0 +1 @@
This file has an mtime of when this was started.

Some files were not shown because too many files have changed in this diff Show More