feat(kiosk): bump v0.1.5 with Yatse artwork proxy, smart track selection, and HA buttons
This commit is contained in:
+1
-1
@@ -7,4 +7,4 @@ dist/
|
||||
build/
|
||||
*.egg-info/
|
||||
.pytest_cache/
|
||||
jellyfin-kiosk-rs/target
|
||||
jellyfin-kiosk-rs/target*.log
|
||||
|
||||
Generated
+1
-1
@@ -1460,7 +1460,7 @@ checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
|
||||
|
||||
[[package]]
|
||||
name = "jellyfin-kiosk-rs"
|
||||
version = "0.1.4"
|
||||
version = "0.1.5"
|
||||
dependencies = [
|
||||
"axum",
|
||||
"eframe",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "jellyfin-kiosk-rs"
|
||||
version = "0.1.4"
|
||||
version = "0.1.5"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
|
||||
Binary file not shown.
@@ -1,12 +0,0 @@
|
||||
[2m2026-11-05T21:13:59.466559Z[0m [32m INFO[0m [2mjellyfin_kiosk_rs[0m[2m:[0m Starting Jellyfin HTPC Kiosk (Rust)...
|
||||
[2m2026-11-05T21:13:59.467206Z[0m [32m INFO[0m [2mjellyfin_kiosk_rs::config[0m[2m:[0m Loaded config from ../config.yaml
|
||||
[2m2026-11-05T21:13:59.474938Z[0m [32m INFO[0m [2mjellyfin_kiosk_rs::player[0m[2m:[0m libmpv initialized successfully (vo=libmpv, hwdec=auto)
|
||||
[2m2026-11-05T21:13:59.549439Z[0m [32m INFO[0m [2mjellyfin_kiosk_rs::yatse[0m[2m:[0m Yatse Kodi TCP JSON-RPC socket server listening on 0.0.0.0:9090
|
||||
[2m2026-11-05T21:13:59.549444Z[0m [32m INFO[0m [2mjellyfin_kiosk_rs[0m[2m:[0m Yatse Kodi JSON-RPC server listening on http://0.0.0.0:8080
|
||||
[2m2026-11-05T21:13:59.549923Z[0m [32m INFO[0m [2mjellyfin_kiosk_rs::eventserver[0m[2m:[0m Kodi EventServer listening on UDP 0.0.0.0:9777
|
||||
[2m2026-11-05T21:13:59.551471Z[0m [32m INFO[0m [2mjellyfin_kiosk_rs[0m[2m:[0m Published Zeroconf services for Yatse discovery: 'Kodi (bart-laptop)'
|
||||
[2m2026-11-05T21:13:59.691500Z[0m [32m INFO[0m [2mwinit::platform_impl::linux::x11::window[0m[2m:[0m Guessed window scale factor: 1
|
||||
[2m2026-11-05T21:13:59.729454Z[0m [32m INFO[0m [2mjellyfin_kiosk_rs::youtube[0m[2m:[0m Loaded cached YouTube token from youtube_token.json
|
||||
Cannot load libcuda.so.1
|
||||
[2m2026-11-05T21:13:59.854273Z[0m [32m INFO[0m [2mjellyfin_kiosk_rs::player[0m[2m:[0m libmpv OpenGL render context created successfully!
|
||||
[2m2026-11-05T21:13:59.916581Z[0m [32m INFO[0m [2mjellyfin_kiosk_rs::jellyfin[0m[2m:[0m Authenticated successfully with Jellyfin as bart
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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":{}}
|
||||
@@ -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/
|
||||
BIN
Binary file not shown.
+1
@@ -0,0 +1 @@
|
||||
This file has an mtime of when this was started.
|
||||
@@ -0,0 +1 @@
|
||||
3ac0e9726997097b
|
||||
+1
@@ -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}
|
||||
BIN
Binary file not shown.
+1
@@ -0,0 +1 @@
|
||||
This file has an mtime of when this was started.
|
||||
@@ -0,0 +1 @@
|
||||
5d1ec60264864a72
|
||||
+1
@@ -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}
|
||||
BIN
Binary file not shown.
+1
@@ -0,0 +1 @@
|
||||
This file has an mtime of when this was started.
|
||||
+1
@@ -0,0 +1 @@
|
||||
4ce51615b0d94a25
|
||||
+1
@@ -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}
|
||||
BIN
Binary file not shown.
+1
@@ -0,0 +1 @@
|
||||
This file has an mtime of when this was started.
|
||||
+1
@@ -0,0 +1 @@
|
||||
1cf94d54a53304d9
|
||||
+1
@@ -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}
|
||||
Binary file not shown.
@@ -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}
|
||||
Binary file not shown.
@@ -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}
|
||||
Binary file not shown.
@@ -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}
|
||||
Binary file not shown.
@@ -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}
|
||||
+1
@@ -0,0 +1 @@
|
||||
1c1b8857b419ba7f
|
||||
+1
@@ -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}
|
||||
+1
@@ -0,0 +1 @@
|
||||
5f7e4ce2e2fc51f8
|
||||
+1
@@ -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}
|
||||
BIN
Binary file not shown.
@@ -0,0 +1 @@
|
||||
This file has an mtime of when this was started.
|
||||
Binary file not shown.
@@ -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}
|
||||
Binary file not shown.
@@ -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}
|
||||
BIN
Binary file not shown.
+1
@@ -0,0 +1 @@
|
||||
This file has an mtime of when this was started.
|
||||
+1
@@ -0,0 +1 @@
|
||||
a7db3cf8b6df24b9
|
||||
+1
@@ -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}
|
||||
BIN
Binary file not shown.
+1
@@ -0,0 +1 @@
|
||||
This file has an mtime of when this was started.
|
||||
+1
@@ -0,0 +1 @@
|
||||
b625b33410081032
|
||||
+1
@@ -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}
|
||||
BIN
Binary file not shown.
+1
@@ -0,0 +1 @@
|
||||
This file has an mtime of when this was started.
|
||||
+1
@@ -0,0 +1 @@
|
||||
f90dc56d80769edd
|
||||
+1
@@ -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}
|
||||
BIN
Binary file not shown.
+1
@@ -0,0 +1 @@
|
||||
This file has an mtime of when this was started.
|
||||
+1
@@ -0,0 +1 @@
|
||||
91e2751f87e8a81a
|
||||
+1
@@ -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}
|
||||
BIN
Binary file not shown.
+1
@@ -0,0 +1 @@
|
||||
This file has an mtime of when this was started.
|
||||
+1
@@ -0,0 +1 @@
|
||||
3de3dc6a31a73318
|
||||
+1
@@ -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}
|
||||
Binary file not shown.
@@ -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}
|
||||
Binary file not shown.
@@ -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}
|
||||
BIN
Binary file not shown.
+1
@@ -0,0 +1 @@
|
||||
This file has an mtime of when this was started.
|
||||
@@ -0,0 +1 @@
|
||||
a0c307ffee1dce9e
|
||||
+1
@@ -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}
|
||||
BIN
Binary file not shown.
+1
@@ -0,0 +1 @@
|
||||
This file has an mtime of when this was started.
|
||||
@@ -0,0 +1 @@
|
||||
c7942b652db1b76f
|
||||
+1
@@ -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}
|
||||
Binary file not shown.
@@ -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}
|
||||
Binary file not shown.
@@ -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
Reference in New Issue
Block a user