fix(kiosk): auto-hide OSD, prioritize hardware-decoded formats, clean YouTube share URLs
This commit is contained in:
+280
-23
@@ -135,13 +135,35 @@ fn parse_limits(params: Option<&Value>, total: usize) -> (usize, usize) {
|
||||
let start = limits.get("start").and_then(|v| v.as_u64()).unwrap_or(0) as usize;
|
||||
let end_val = limits.get("end").and_then(|v| v.as_u64()).unwrap_or(0) as usize;
|
||||
let start = start.min(total);
|
||||
let end = if end_val == 0 { total } else { (start + end_val).min(total) };
|
||||
let end = if end_val == 0 || end_val <= start {
|
||||
total
|
||||
} else {
|
||||
end_val.min(total)
|
||||
};
|
||||
return (start, end);
|
||||
}
|
||||
}
|
||||
(0, total)
|
||||
}
|
||||
|
||||
fn parse_limits_raw(params: Option<&Value>, default_limit: usize) -> (usize, usize) {
|
||||
if let Some(p) = params {
|
||||
let limits_obj = p.get("limits")
|
||||
.or_else(|| p.as_array().and_then(|a| a.iter().find_map(|x| x.get("limits"))));
|
||||
if let Some(limits) = limits_obj {
|
||||
let start = limits.get("start").and_then(|v| v.as_u64()).unwrap_or(0) as usize;
|
||||
let end_val = limits.get("end").and_then(|v| v.as_u64()).unwrap_or(0) as usize;
|
||||
let limit = if end_val > start {
|
||||
end_val - start
|
||||
} else {
|
||||
default_limit
|
||||
};
|
||||
return (start, limit);
|
||||
}
|
||||
}
|
||||
(0, default_limit)
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct JsonRpcRequest {
|
||||
#[allow(dead_code)]
|
||||
@@ -207,10 +229,30 @@ pub fn normalize_playback_url(file: &str) -> String {
|
||||
if let Some(query) = trimmed.split('?').nth(1) {
|
||||
let decoded = percent_decode(query);
|
||||
if decoded.starts_with("http://") || decoded.starts_with("https://") {
|
||||
return decoded;
|
||||
return normalize_playback_url(&decoded);
|
||||
}
|
||||
if let Some(url_part) = decoded.split("url=").nth(1) {
|
||||
return percent_decode(url_part);
|
||||
return normalize_playback_url(&percent_decode(url_part));
|
||||
}
|
||||
}
|
||||
} else if trimmed.contains("youtube.com/watch") || trimmed.contains("youtu.be/") || trimmed.contains("youtube.com/shorts/") {
|
||||
if let Some(pos) = trimmed.find("v=") {
|
||||
let rest = &trimmed[pos + 2..];
|
||||
let vid = rest.split('&').next().unwrap_or(rest);
|
||||
if !vid.is_empty() {
|
||||
return format!("https://www.youtube.com/watch?v={}", vid);
|
||||
}
|
||||
} else if let Some(pos) = trimmed.find("youtu.be/") {
|
||||
let rest = &trimmed[pos + 9..];
|
||||
let vid = rest.split('?').next().unwrap_or(rest);
|
||||
if !vid.is_empty() {
|
||||
return format!("https://www.youtube.com/watch?v={}", vid);
|
||||
}
|
||||
} else if let Some(pos) = trimmed.find("youtube.com/shorts/") {
|
||||
let rest = &trimmed[pos + 19..];
|
||||
let vid = rest.split('?').next().unwrap_or(rest);
|
||||
if !vid.is_empty() {
|
||||
return format!("https://www.youtube.com/watch?v={}", vid);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -680,6 +722,7 @@ async fn dispatch_rpc(state: &YatseServerState, req: JsonRpcRequest) -> Value {
|
||||
if let Some(item) = item_opt {
|
||||
let movieid_opt = item.get("movieid").and_then(|v| v.as_i64().or_else(|| v.as_str().and_then(|s| s.parse().ok())));
|
||||
let episodeid_opt = item.get("episodeid").and_then(|v| v.as_i64().or_else(|| v.as_str().and_then(|s| s.parse().ok())));
|
||||
let songid_opt = item.get("songid").and_then(|v| v.as_i64().or_else(|| v.as_str().and_then(|s| s.parse().ok())));
|
||||
let file_opt = item.get("file").and_then(|f| f.as_str()).map(|s| s.to_string());
|
||||
|
||||
let p = state.player.clone();
|
||||
@@ -712,14 +755,28 @@ async fn dispatch_rpc(state: &YatseServerState, req: JsonRpcRequest) -> Value {
|
||||
let _ = p.play(&stream_url, &ep_title).await;
|
||||
return;
|
||||
}
|
||||
} else if let Some(sid) = songid_opt {
|
||||
if let Some(jellyfin_id) = id_map.get_str(sid).await {
|
||||
let stream_url = format!("{}/Audio/{}/stream?static=true&api_key={}", base_url, jellyfin_id, token);
|
||||
tracing::info!("Player.Open songid={} (jellyfin={}) -> {}", sid, jellyfin_id, stream_url);
|
||||
let _ = p.play(&stream_url, "Muziek via Yatse").await;
|
||||
return;
|
||||
}
|
||||
} else if let Some(file) = file_opt {
|
||||
if file.starts_with("jellyfin://movie/") || file.starts_with("jellyfin://episode/") || file.starts_with("jellyfin://video/") {
|
||||
let item_id = file.rsplit('/').next().unwrap_or("");
|
||||
let stream_url = format!("{}/Videos/{}/stream?static=true&api_key={}", base_url, item_id, token);
|
||||
tracing::info!("Player.Open file='{}' -> {}", file, stream_url);
|
||||
tracing::info!("Player.Open video file='{}' -> {}", file, stream_url);
|
||||
let _ = p.play(&stream_url, "Media from Yatse").await;
|
||||
return;
|
||||
}
|
||||
if file.starts_with("jellyfin://audio/") || file.starts_with("jellyfin://song/") {
|
||||
let item_id = file.rsplit('/').next().unwrap_or("");
|
||||
let stream_url = format!("{}/Audio/{}/stream?static=true&api_key={}", base_url, item_id, token);
|
||||
tracing::info!("Player.Open audio file='{}' -> {}", file, stream_url);
|
||||
let _ = p.play(&stream_url, "Muziek via Yatse").await;
|
||||
return;
|
||||
}
|
||||
|
||||
let norm_url = normalize_playback_url(&file);
|
||||
tracing::info!("Player.Open file: '{}' -> normalized: '{}'", file, norm_url);
|
||||
@@ -1307,7 +1364,8 @@ async fn dispatch_rpc(state: &YatseServerState, req: JsonRpcRequest) -> Value {
|
||||
"rating": s.rating.unwrap_or(0.0),
|
||||
"plot": s.overview.clone().unwrap_or_default(),
|
||||
"genre": genres,
|
||||
"episode": 0,
|
||||
"episode": s.recursive_item_count.unwrap_or(1),
|
||||
"season": s.child_count.unwrap_or(1),
|
||||
"watchedepisodes": 0,
|
||||
"file": format!("jellyfin://show/{}", s.id),
|
||||
"thumbnail": s.thumb_url(base_url),
|
||||
@@ -1352,7 +1410,8 @@ async fn dispatch_rpc(state: &YatseServerState, req: JsonRpcRequest) -> Value {
|
||||
"rating": s.rating.unwrap_or(0.0),
|
||||
"plot": s.overview.clone().unwrap_or_default(),
|
||||
"genre": genres,
|
||||
"episode": 0,
|
||||
"episode": s.recursive_item_count.unwrap_or(1),
|
||||
"season": s.child_count.unwrap_or(1),
|
||||
"watchedepisodes": 0,
|
||||
"file": format!("jellyfin://show/{}", s.id),
|
||||
"thumbnail": s.thumb_url(base_url),
|
||||
@@ -1457,8 +1516,9 @@ async fn dispatch_rpc(state: &YatseServerState, req: JsonRpcRequest) -> Value {
|
||||
p.get("season").and_then(|v| v.as_u64().map(|n| n as u32))
|
||||
});
|
||||
|
||||
let mut episodes_out = Vec::new();
|
||||
let base_url = &state.client.config.server_url;
|
||||
if let Some(tid) = tid_opt {
|
||||
let mut episodes_out = Vec::new();
|
||||
if let Some(jellyfin_id) = state.id_map.get_str(tid).await {
|
||||
let episodes = {
|
||||
let cache = state.episodes_cache.read().await;
|
||||
@@ -1473,7 +1533,6 @@ async fn dispatch_rpc(state: &YatseServerState, req: JsonRpcRequest) -> Value {
|
||||
}
|
||||
};
|
||||
|
||||
let base_url = &state.client.config.server_url;
|
||||
for ep in &eps {
|
||||
let s_num = ep.parent_index_number.unwrap_or(1);
|
||||
if let Some(sf) = season_filter {
|
||||
@@ -1508,18 +1567,64 @@ async fn dispatch_rpc(state: &YatseServerState, req: JsonRpcRequest) -> Value {
|
||||
}));
|
||||
}
|
||||
}
|
||||
let total = episodes_out.len();
|
||||
let (start, end) = parse_limits(req.params.as_ref(), total);
|
||||
let paged = episodes_out[start..end].to_vec();
|
||||
json!({
|
||||
"jsonrpc": "2.0",
|
||||
"result": {
|
||||
"limits": {"start": start, "end": end, "total": total},
|
||||
"episodes": paged
|
||||
},
|
||||
"id": id
|
||||
})
|
||||
} else {
|
||||
let (start, limit) = parse_limits_raw(req.params.as_ref(), 100);
|
||||
let (total, eps) = state.client.fetch_episodes_paged(None, None, start, limit).await;
|
||||
let mut episodes_out = Vec::new();
|
||||
for ep in eps {
|
||||
let s_num = ep.parent_index_number.unwrap_or(1);
|
||||
let ep_num = ep.index_number.unwrap_or(1);
|
||||
let epid = state.id_map.get_or_create(&ep.id).await;
|
||||
let show_tid = if let Some(ref sid) = ep.series_id {
|
||||
state.id_map.get_or_create(sid).await
|
||||
} else {
|
||||
0
|
||||
};
|
||||
let runtime = ep.runtime_ticks.map(|t| (t / 10_000_000) as i64).unwrap_or(0);
|
||||
let playcount = if ep.played_percentage.unwrap_or(0.0) >= 90.0 { 1 } else { 0 };
|
||||
|
||||
episodes_out.push(json!({
|
||||
"episodeid": epid,
|
||||
"tvshowid": show_tid,
|
||||
"season": s_num,
|
||||
"episode": ep_num,
|
||||
"label": format!("{}x{:02}. {}", s_num, ep_num, ep.name),
|
||||
"title": ep.name,
|
||||
"plot": ep.overview.clone().unwrap_or_default(),
|
||||
"runtime": runtime,
|
||||
"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),
|
||||
"art": {
|
||||
"thumb": ep.thumb_url(base_url),
|
||||
"poster": ep.poster_url(base_url),
|
||||
"fanart": ep.backdrop_url(base_url),
|
||||
}
|
||||
}));
|
||||
}
|
||||
let actual_end = start + episodes_out.len();
|
||||
json!({
|
||||
"jsonrpc": "2.0",
|
||||
"result": {
|
||||
"limits": {"start": start, "end": actual_end, "total": total},
|
||||
"episodes": episodes_out
|
||||
},
|
||||
"id": id
|
||||
})
|
||||
}
|
||||
let total = episodes_out.len();
|
||||
let (start, end) = parse_limits(req.params.as_ref(), total);
|
||||
let paged = episodes_out[start..end].to_vec();
|
||||
json!({
|
||||
"jsonrpc": "2.0",
|
||||
"result": {
|
||||
"limits": {"start": start, "end": end, "total": total},
|
||||
"episodes": paged
|
||||
},
|
||||
"id": id
|
||||
})
|
||||
}
|
||||
"VideoLibrary.GetEpisodeDetails" => {
|
||||
let eid_opt = req.params.as_ref().and_then(|p| {
|
||||
@@ -1682,11 +1787,155 @@ async fn dispatch_rpc(state: &YatseServerState, req: JsonRpcRequest) -> Value {
|
||||
})
|
||||
}
|
||||
"AudioLibrary.GetSongs" => {
|
||||
let al_opt = req.params.as_ref().and_then(|p| {
|
||||
p.get("albumid").and_then(|v| v.as_i64().or_else(|| v.as_str().and_then(|s| s.parse().ok())))
|
||||
.or_else(|| p.as_array().and_then(|a| a.first()).and_then(|v| v.as_i64().or_else(|| v.as_str().and_then(|s| s.parse().ok()))))
|
||||
});
|
||||
let ar_opt = req.params.as_ref().and_then(|p| {
|
||||
p.get("artistid").and_then(|v| v.as_i64().or_else(|| v.as_str().and_then(|s| s.parse().ok())))
|
||||
});
|
||||
|
||||
let album_jf = if let Some(aid) = al_opt {
|
||||
state.id_map.get_str(aid).await
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let artist_jf = if let Some(arid) = ar_opt {
|
||||
state.id_map.get_str(arid).await
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let (start, limit) = parse_limits_raw(req.params.as_ref(), 100);
|
||||
let (total, songs) = state.client.fetch_songs_paged(album_jf.as_deref(), artist_jf.as_deref(), start, limit).await;
|
||||
|
||||
let base_url = &state.client.config.server_url;
|
||||
let mut songs_out = Vec::new();
|
||||
for s in songs {
|
||||
let songid = state.id_map.get_or_create(&s.id).await;
|
||||
let duration = s.runtime_ticks.map(|t| (t / 10_000_000) as i64).unwrap_or(0);
|
||||
let track = s.index_number.unwrap_or(0);
|
||||
let disc = s.parent_index_number.unwrap_or(1);
|
||||
let artist = s.artist_name.as_deref().unwrap_or("Onbekend");
|
||||
let album = s.album.as_deref().unwrap_or("");
|
||||
let file = format!("jellyfin://audio/{}", s.id);
|
||||
|
||||
songs_out.push(json!({
|
||||
"songid": songid,
|
||||
"label": s.name,
|
||||
"title": s.name,
|
||||
"artist": [artist],
|
||||
"album": album,
|
||||
"duration": duration,
|
||||
"track": track,
|
||||
"disc": disc,
|
||||
"year": s.year.unwrap_or(0),
|
||||
"file": file,
|
||||
"thumbnail": s.square_url(base_url),
|
||||
"fanart": s.backdrop_url(base_url),
|
||||
"art": {
|
||||
"thumb": s.square_url(base_url),
|
||||
"fanart": s.backdrop_url(base_url)
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
let actual_end = start + songs_out.len();
|
||||
json!({
|
||||
"jsonrpc": "2.0",
|
||||
"result": {
|
||||
"limits": {"start": 0, "end": 0, "total": 0},
|
||||
"songs": []
|
||||
"limits": {"start": start, "end": actual_end, "total": total},
|
||||
"songs": songs_out
|
||||
},
|
||||
"id": id
|
||||
})
|
||||
}
|
||||
"AudioLibrary.GetSongDetails" => {
|
||||
let sid_opt = req.params.as_ref().and_then(|p| {
|
||||
p.get("songid").and_then(|v| v.as_i64().or_else(|| v.as_str().and_then(|s| s.parse().ok())))
|
||||
.or_else(|| p.as_array().and_then(|a| a.first()).and_then(|v| v.as_i64().or_else(|| v.as_str().and_then(|s| s.parse().ok()))))
|
||||
});
|
||||
let mut details = json!({});
|
||||
if let Some(sid) = sid_opt {
|
||||
if let Some(jellyfin_id) = state.id_map.get_str(sid).await {
|
||||
details = json!({
|
||||
"songid": sid,
|
||||
"file": format!("jellyfin://audio/{}", jellyfin_id)
|
||||
});
|
||||
}
|
||||
}
|
||||
json!({
|
||||
"jsonrpc": "2.0",
|
||||
"result": {
|
||||
"songdetails": details
|
||||
},
|
||||
"id": id
|
||||
})
|
||||
}
|
||||
"AudioLibrary.GetAlbumDetails" => {
|
||||
let aid_opt = req.params.as_ref().and_then(|p| {
|
||||
p.get("albumid").and_then(|v| v.as_i64().or_else(|| v.as_str().and_then(|s| s.parse().ok())))
|
||||
.or_else(|| p.as_array().and_then(|a| a.first()).and_then(|v| v.as_i64().or_else(|| v.as_str().and_then(|s| s.parse().ok()))))
|
||||
});
|
||||
let mut details = json!({});
|
||||
if let Some(aid) = aid_opt {
|
||||
if let Some(jellyfin_id) = state.id_map.get_str(aid).await {
|
||||
let lib = state.client.get_library().await;
|
||||
if let Some(al) = lib.music_albums.iter().find(|x| x.id == jellyfin_id) {
|
||||
let base_url = &state.client.config.server_url;
|
||||
details = json!({
|
||||
"albumid": aid,
|
||||
"title": al.name,
|
||||
"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),
|
||||
"art": {
|
||||
"thumb": al.square_url(base_url),
|
||||
"fanart": al.backdrop_url(base_url),
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
json!({
|
||||
"jsonrpc": "2.0",
|
||||
"result": {
|
||||
"albumdetails": details
|
||||
},
|
||||
"id": id
|
||||
})
|
||||
}
|
||||
"AudioLibrary.GetArtistDetails" => {
|
||||
let aid_opt = req.params.as_ref().and_then(|p| {
|
||||
p.get("artistid").and_then(|v| v.as_i64().or_else(|| v.as_str().and_then(|s| s.parse().ok())))
|
||||
.or_else(|| p.as_array().and_then(|a| a.first()).and_then(|v| v.as_i64().or_else(|| v.as_str().and_then(|s| s.parse().ok()))))
|
||||
});
|
||||
let mut details = json!({});
|
||||
if let Some(aid) = aid_opt {
|
||||
if let Some(jellyfin_id) = state.id_map.get_str(aid).await {
|
||||
let lib = state.client.get_library().await;
|
||||
if let Some(ar) = lib.music_artists.iter().find(|x| x.id == jellyfin_id) {
|
||||
let base_url = &state.client.config.server_url;
|
||||
details = json!({
|
||||
"artistid": aid,
|
||||
"artist": ar.name,
|
||||
"label": ar.name,
|
||||
"thumbnail": ar.square_url(base_url),
|
||||
"fanart": ar.backdrop_url(base_url),
|
||||
"art": {
|
||||
"thumb": ar.square_url(base_url),
|
||||
"fanart": ar.backdrop_url(base_url),
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
json!({
|
||||
"jsonrpc": "2.0",
|
||||
"result": {
|
||||
"artistdetails": details
|
||||
},
|
||||
"id": id
|
||||
})
|
||||
@@ -1731,11 +1980,15 @@ mod tests {
|
||||
);
|
||||
assert_eq!(
|
||||
normalize_playback_url("plugin://plugin.video.sendtokodi/?https%3A%2F%2Fyoutube.com%2Fwatch%3Fv%3D6G45Qk0HQNY"),
|
||||
"https://youtube.com/watch?v=6G45Qk0HQNY"
|
||||
"https://www.youtube.com/watch?v=6G45Qk0HQNY"
|
||||
);
|
||||
assert_eq!(
|
||||
normalize_playback_url("https://youtube.com/watch?v=6G45Qk0HQNY&is=6-OPTODo-ca9bhei"),
|
||||
"https://youtube.com/watch?v=6G45Qk0HQNY&is=6-OPTODo-ca9bhei"
|
||||
"https://www.youtube.com/watch?v=6G45Qk0HQNY"
|
||||
);
|
||||
assert_eq!(
|
||||
normalize_playback_url("https://youtu.be/6G45Qk0HQNY?si=something"),
|
||||
"https://www.youtube.com/watch?v=6G45Qk0HQNY"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1778,6 +2031,7 @@ mod tests {
|
||||
has_backdrop_image: true,
|
||||
parent_primary_image_item_id: None,
|
||||
parent_backdrop_item_id: None,
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1851,6 +2105,7 @@ mod tests {
|
||||
has_backdrop_image: true,
|
||||
parent_primary_image_item_id: None,
|
||||
parent_backdrop_item_id: None,
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1880,6 +2135,7 @@ mod tests {
|
||||
has_backdrop_image: false,
|
||||
parent_primary_image_item_id: None,
|
||||
parent_backdrop_item_id: None,
|
||||
..Default::default()
|
||||
},
|
||||
MediaItem {
|
||||
id: "ep-uuid-2".into(),
|
||||
@@ -1902,6 +2158,7 @@ mod tests {
|
||||
has_backdrop_image: false,
|
||||
parent_primary_image_item_id: None,
|
||||
parent_backdrop_item_id: None,
|
||||
..Default::default()
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user