diff --git a/jellyfin-kiosk-rs/Cargo.lock b/jellyfin-kiosk-rs/Cargo.lock index 105e79e..3191d54 100644 --- a/jellyfin-kiosk-rs/Cargo.lock +++ b/jellyfin-kiosk-rs/Cargo.lock @@ -1460,7 +1460,7 @@ checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] name = "jellyfin-kiosk-rs" -version = "0.1.2" +version = "0.1.3" dependencies = [ "axum", "eframe", diff --git a/jellyfin-kiosk-rs/Cargo.toml b/jellyfin-kiosk-rs/Cargo.toml index 13cc2db..e64f904 100644 --- a/jellyfin-kiosk-rs/Cargo.toml +++ b/jellyfin-kiosk-rs/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "jellyfin-kiosk-rs" -version = "0.1.2" +version = "0.1.3" edition = "2021" [dependencies] diff --git a/jellyfin-kiosk-rs/jellyfin-kiosk b/jellyfin-kiosk-rs/jellyfin-kiosk index 5e0b8da..3d7a2ab 100755 Binary files a/jellyfin-kiosk-rs/jellyfin-kiosk and b/jellyfin-kiosk-rs/jellyfin-kiosk differ diff --git a/jellyfin-kiosk-rs/src/yatse.rs b/jellyfin-kiosk-rs/src/yatse.rs index ea09663..f105435 100644 --- a/jellyfin-kiosk-rs/src/yatse.rs +++ b/jellyfin-kiosk-rs/src/yatse.rs @@ -217,6 +217,58 @@ pub fn normalize_playback_url(file: &str) -> String { trimmed.to_string() } +pub fn extract_addon_share_url(params: &Value) -> Option { + // 1. Direct fields + for key in ["data", "url", "file"] { + if let Some(v) = params.get(key).and_then(|v| v.as_str()) { + return Some(v.to_string()); + } + } + + // 2. Nested under "params" + if let Some(inner) = params.get("params") { + if let Some(obj) = inner.as_object() { + for key in ["data", "url", "file"] { + if let Some(v) = obj.get(key).and_then(|v| v.as_str()) { + return Some(v.to_string()); + } + } + } else if let Some(s) = inner.as_str() { + let q = s.trim_start_matches('?'); + for pair in q.split('&') { + let mut kv = pair.splitn(2, '='); + if let (Some(k), Some(v)) = (kv.next(), kv.next()) { + if k == "data" || k == "url" || k == "file" { + return Some(v.to_string()); + } + } + } + if s.contains("http://") || s.contains("https://") || s.contains("plugin://") { + return Some(s.to_string()); + } + } else if let Some(arr) = inner.as_array() { + for item in arr { + if let Some(s) = item.as_str() { + let q = s.trim_start_matches('?'); + for pair in q.split('&') { + let mut kv = pair.splitn(2, '='); + if let (Some(k), Some(v)) = (kv.next(), kv.next()) { + if k == "data" || k == "url" || k == "file" { + return Some(v.to_string()); + } + } + } + if s.contains("http://") || s.contains("https://") || s.contains("plugin://") { + return Some(s.to_string()); + } + } + } + } + } + + None +} + async fn handle_jsonrpc_get( State(state): State, uri: axum::http::Uri, @@ -1009,8 +1061,49 @@ async fn dispatch_rpc(state: &YatseServerState, req: JsonRpcRequest) -> Value { }) } "Addons.ExecuteAddon" => { + if let Some(ref params) = req.params { + if let Some(raw_url) = extract_addon_share_url(params) { + let decoded_url = percent_decode(&raw_url); + let norm_url = normalize_playback_url(&decoded_url); + tracing::info!("Addons.ExecuteAddon share requested: raw='{}' -> norm='{}'", raw_url, norm_url); + let p = state.player.clone(); + tokio::spawn(async move { + let _ = p.play(&norm_url, "YouTube via Yatse").await; + }); + } else { + tracing::warn!("Addons.ExecuteAddon received without resolvable URL: {:?}", params); + } + } json!({"jsonrpc": "2.0", "result": "OK", "id": id}) } + "Playlist.Add" | "Playlist.Insert" => { + if let Some(ref params) = req.params { + if let Some(item) = params.get("item") { + if let Some(file) = item.get("file").and_then(|f| f.as_str()) { + let norm_url = normalize_playback_url(file); + tracing::info!("Playlist.Add requested file: '{}' -> '{}'", file, norm_url); + let p = state.player.clone(); + tokio::spawn(async move { + let _ = p.play(&norm_url, "Media from Yatse").await; + }); + } + } + } + json!({"jsonrpc": "2.0", "result": "OK", "id": id}) + } + "Playlist.Clear" => { + json!({"jsonrpc": "2.0", "result": "OK", "id": id}) + } + "Playlist.GetItems" => { + json!({ + "jsonrpc": "2.0", + "result": { + "items": [], + "limits": {"start": 0, "end": 0, "total": 0} + }, + "id": id + }) + } "Files.GetSources" => { let media = req.params.as_ref() .and_then(|p| p.get("media").or_else(|| p.as_array().and_then(|a| a.first()))) @@ -1854,4 +1947,22 @@ mod tests { assert_eq!(eps[1]["title"], "Cat's in the Bag..."); assert_eq!(eps[1]["episode"], 2); } + + #[test] + fn test_extract_addon_share_url() { + let payload = json!({ + "addonid": "script.yatse.kodi", + "params": { + "action": "share", + "data": "https%3A%2F%2Fyoutube.com%2Fwatch%3Fv%3DeMEIt6_mGSI%26is%3DhEdy88z6Nhf7gRSE", + "queue": "false", + "type": "unresolvedurl" + } + }); + let extracted = extract_addon_share_url(&payload).expect("extracted url"); + assert_eq!( + percent_decode(&extracted), + "https://youtube.com/watch?v=eMEIt6_mGSI&is=hEdy88z6Nhf7gRSE" + ); + } }