From 62f7a574d8abbdb32670562267015c01708e8a16 Mon Sep 17 00:00:00 2001 From: euzu Date: Mon, 6 Oct 2025 21:32:02 +0200 Subject: [PATCH] Handling edge cases for series info parsing --- backend/src/model/xtream.rs | 63 +++++++++++++++++++++---- backend/src/processing/parser/xtream.rs | 7 ++- 2 files changed, 59 insertions(+), 11 deletions(-) diff --git a/backend/src/model/xtream.rs b/backend/src/model/xtream.rs index 93e9ad777..6e0cd4be2 100644 --- a/backend/src/model/xtream.rs +++ b/backend/src/model/xtream.rs @@ -1,13 +1,12 @@ use crate::model::{AppConfig, ProxyUserCredentials}; use crate::model::{ConfigTarget, XtreamTargetOutput}; -use shared::model::{xtream_const, PlaylistItem,XtreamPlaylistItem}; -use shared::utils::{deserialize_as_option_string, deserialize_as_string, deserialize_as_string_array, deserialize_number_from_string, - opt_string_or_number_u32, string_default_on_null, string_or_number_f64, string_or_number_u32, get_non_empty_str}; -use serde::{Deserialize, Serialize}; +use serde::{Deserialize, Deserializer, Serialize}; use serde_json::{Map, Value}; -use std::collections::HashMap; -use std::iter::FromIterator; +use shared::model::{xtream_const, PlaylistItem, XtreamPlaylistItem}; use shared::model::{ClusterFlags, PlaylistEntry, XtreamCluster}; +use shared::utils::{deserialize_as_option_string, deserialize_as_string, deserialize_as_string_array, deserialize_number_from_string, + get_non_empty_str, opt_string_or_number_u32, string_default_on_null, string_or_number_f64, string_or_number_u32}; +use std::iter::FromIterator; #[derive(Deserialize, Default)] pub struct XtreamCategory { @@ -341,14 +340,60 @@ impl XtreamSeriesEpisode { } } +fn deserialize_episodes<'de, D>(deserializer: D) -> Result>, D::Error> +where + D: Deserializer<'de>, +{ + // read as generic value + let value = Value::deserialize(deserializer)?; + match value { + Value::Null => Ok(None), + Value::Array(array) => { + if array.is_empty() { + Ok(None) + } else { + let mut result = Vec::new(); + for inner in array { + if let Some(inner_array) = inner.as_array() { + for item in inner_array { + let ep: XtreamSeriesInfoEpisode = serde_json::from_value(item.clone()) + .map_err(serde::de::Error::custom)?; + result.push(ep); + } + } + } + Ok(Some(result)) + } + } + Value::Object(object) => { + if object.is_empty() { + Ok(None) + } else { + let mut result = Vec::new(); + for (_key, val) in object { + if let Some(inner_array) = val.as_array() { + for item in inner_array { + let ep: XtreamSeriesInfoEpisode = serde_json::from_value(item.clone()) + .map_err(serde::de::Error::custom)?; + result.push(ep); + } + } + } + Ok(Some(result)) + } + } + _ => Err(serde::de::Error::custom("Invalid format for episodes")), + } +} + #[derive(Debug, Clone, Serialize, Deserialize)] pub struct XtreamSeriesInfo { #[serde(default)] pub seasons: Option>, #[serde(default)] pub info: Option, - #[serde(default)] - pub episodes: Option>>, + #[serde(default, deserialize_with = "deserialize_episodes")] + pub episodes: Option>, } impl XtreamSeriesInfoEpisode { @@ -408,7 +453,7 @@ pub fn normalize_release_date(document: &mut serde_json::Map) { let date_value = document.get("release_date") .or_else(|| document.get("releaseDate")) .or_else(|| document.get("releasedate")) - .filter(|v| v.as_str().is_some_and(|s| !s.is_empty())) + .filter(|v| v.as_str().is_some_and(|s| !s.is_empty())) .cloned(); // Remove unused keys (optional) diff --git a/backend/src/processing/parser/xtream.rs b/backend/src/processing/parser/xtream.rs index b2902e87f..b3c6506b8 100644 --- a/backend/src/processing/parser/xtream.rs +++ b/backend/src/processing/parser/xtream.rs @@ -44,7 +44,7 @@ pub fn parse_xtream_series_info(info: &Value, group_title: &str, series_name: &s match serde_json::from_value::(info.to_owned()) { Ok(series_info) => { if let Some(episodes) = &series_info.episodes { - let result: Vec<(XtreamSeriesInfoEpisode, PlaylistItem)> = episodes.values().flatten().map(|episode| { + let result: Vec<(XtreamSeriesInfoEpisode, PlaylistItem)> = episodes.iter().map(|episode| { let episode_url = create_xtream_series_episode_url(url, username, password, episode); let mut new_episode = episode.clone(); @@ -210,7 +210,10 @@ mod tests { fn test_read_json_file_into_struct() { let file_content = fs::read_to_string("/tmp/series-info.json").expect("Unable to read file"); match serde_json::from_str::(&file_content) { - Ok(series_info) => {}, + Ok(series_info) => { + println!("{:#?}", series_info); + assert!(true); + }, Err(err) => { assert!(false, "Failed to parse json file: {err}"); }