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/xmltv.rs b/backend/src/processing/parser/xmltv.rs index 3ca8097a9..dc0a26be7 100644 --- a/backend/src/processing/parser/xmltv.rs +++ b/backend/src/processing/parser/xmltv.rs @@ -554,26 +554,31 @@ mod tests { #[test] fn parse_test() -> io::Result<()> { - //let file_path = PathBuf::from("/tmp/epg.xml.gz"); - let file_path = PathBuf::from("/tmp/invalid_epg.xml"); + let run_test = async move || { + //let file_path = PathBuf::from("/tmp/epg.xml.gz"); + let file_path = PathBuf::from("/tmp/invalid_epg.xml"); - if file_path.exists() { - let tv_guide = TVGuide::new(vec![PersistedEpgSource { file_path, priority: 0, logo_override: false }]); + if file_path.exists() { + let tv_guide = TVGuide::new(vec![PersistedEpgSource { file_path, priority: 0, logo_override: false }]); - let mut id_cache = EpgIdCache::new(None); - id_cache.channel_epg_id.insert(Cow::Owned("342".to_string())); - //id_cache.collect_epg_id(fp); + let mut id_cache = EpgIdCache::new(None); + id_cache.channel_epg_id.insert(Cow::Owned("342".to_string())); + //id_cache.collect_epg_id(fp); - let channel_ids = HashSet::from(["342".to_string()]); - match tv_guide.filter(&mut id_cache) { - None => assert!(false, "No epg filtered"), - Some(epgs) => { - for epg in epgs { - assert_eq!(epg.children.len(), channel_ids.len() * 2, "Epg size does not match") + let channel_ids = HashSet::from(["342".to_string()]); + match tv_guide.filter(&mut id_cache).await { + None => assert!(false, "No epg filtered"), + Some(epgs) => { + for epg in epgs { + assert_eq!(epg.children.len(), channel_ids.len() * 2, "Epg size does not match") + } } } } - } + }; + let _result = tokio::runtime::Runtime::new() + .unwrap() + .block_on(run_test()); Ok(()) } diff --git a/backend/src/processing/parser/xtream.rs b/backend/src/processing/parser/xtream.rs index cc5c27fb4..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(); @@ -208,8 +208,16 @@ mod tests { #[test] fn test_read_json_file_into_struct() { - let file_content = fs::read_to_string("series-info.json").expect("Unable to read file"); - let _info: XtreamSeriesInfo = serde_json::from_str(&file_content).expect("JSON was not well-formatted"); + 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) => { + println!("{:#?}", series_info); + assert!(true); + }, + Err(err) => { + assert!(false, "Failed to parse json file: {err}"); + } + } } diff --git a/backend/src/utils/file/csv_input_reader.rs b/backend/src/utils/file/csv_input_reader.rs index e50b49979..622440654 100644 --- a/backend/src/utils/file/csv_input_reader.rs +++ b/backend/src/utils/file/csv_input_reader.rs @@ -194,8 +194,8 @@ http://hd.providerline.com/get.php?username=user4&password=user4&type=m3u_plus;i const XTREAM_BATCH: &str = r" #name;username;password;url;max_connections -input_1;desanocra;eyCG8SN523KQ;http://provider_1.tv:80;1 -input_2;desanocra;eyCG8SN523KQ;http://provider_2.tv:8080;1 +input_1;de566567;de2345f43g5;http://provider_1.tv:80;1 +input_2;de566567;de2345f43g5;http://provider_2.tv:8080;1 "; #[test] diff --git a/shared/src/utils/json_utils.rs b/shared/src/utils/json_utils.rs index dd641940f..1ca312863 100644 --- a/shared/src/utils/json_utils.rs +++ b/shared/src/utils/json_utils.rs @@ -67,6 +67,7 @@ where let value: Value = serde::Deserialize::deserialize(deserializer)?; match value { + Value::Null => Ok(0u32), Value::Number(num) => { if let Some(v) = num.as_u64() { u32::try_from(v) @@ -114,6 +115,7 @@ where let value: Value = serde::Deserialize::deserialize(deserializer)?; match value { + Value::Null => Ok(0f64), Value::Number(num) => num .as_f64() .ok_or_else(|| serde::de::Error::custom("Invalid number")),