Merge pull request #361 from euzu/feature/series_info_parsing

Feature/series info parsing
This commit is contained in:
euzu
2025-10-06 21:45:29 +02:00
committed by GitHub
5 changed files with 88 additions and 28 deletions
+54 -9
View File
@@ -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<Option<Vec<XtreamSeriesInfoEpisode>>, 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<Vec<XtreamSeriesInfoSeason>>,
#[serde(default)]
pub info: Option<XtreamSeriesInfoInfo>,
#[serde(default)]
pub episodes: Option<HashMap<String, Vec<XtreamSeriesInfoEpisode>>>,
#[serde(default, deserialize_with = "deserialize_episodes")]
pub episodes: Option<Vec<XtreamSeriesInfoEpisode>>,
}
impl XtreamSeriesInfoEpisode {
@@ -408,7 +453,7 @@ pub fn normalize_release_date(document: &mut serde_json::Map<String, Value>) {
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)
+19 -14
View File
@@ -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(())
}
+11 -3
View File
@@ -44,7 +44,7 @@ pub fn parse_xtream_series_info(info: &Value, group_title: &str, series_name: &s
match serde_json::from_value::<XtreamSeriesInfo>(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::<XtreamSeriesInfo>(&file_content) {
Ok(series_info) => {
println!("{:#?}", series_info);
assert!(true);
},
Err(err) => {
assert!(false, "Failed to parse json file: {err}");
}
}
}
+2 -2
View File
@@ -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]
+2
View File
@@ -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")),