diff --git a/backend/src/model/xtream.rs b/backend/src/model/xtream.rs index ef747262e..f162bfaa5 100644 --- a/backend/src/model/xtream.rs +++ b/backend/src/model/xtream.rs @@ -7,6 +7,7 @@ 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; +use serde_json::value::RawValue; #[derive(Debug, Default)] pub struct XtreamLoginInfo { @@ -153,7 +154,7 @@ impl XtreamStream { self.stream_id.unwrap_or_else(|| self.series_id.unwrap_or(0)) } - pub fn get_additional_properties(&self) -> Option { + pub fn get_additional_properties(&self) -> Option> { let mut result = Map::new(); if let Some(bdpath) = self.backdrop_path.as_ref() { if !bdpath.is_empty() { @@ -183,7 +184,12 @@ impl XtreamStream { add_opt_i64_property_if_exists!(result, self.tv_archive, "tv_archive"); add_opt_i64_property_if_exists!(result, self.tv_archive_duration, "tv_archive_duration"); add_opt_i64_property_if_exists!(result, self.is_adult, "is_adult"); - if result.is_empty() { None } else { Some(Value::Object(result)) } + if result.is_empty() { + None + } else { + let s = serde_json::to_string(&Value::Object(result)).ok()?; + RawValue::from_string(s).ok() + } } } @@ -403,7 +409,7 @@ pub struct XtreamSeriesInfo { } impl XtreamSeriesInfoEpisode { - pub fn get_additional_properties(&self, series_info: &XtreamSeriesInfo) -> Option { + pub fn get_additional_properties(&self, series_info: &XtreamSeriesInfo) -> Option> { let mut result = Map::new(); let info = series_info.info.as_ref(); let bdpath = info.and_then(|i| i.backdrop_path.as_ref()); @@ -437,7 +443,12 @@ impl XtreamSeriesInfoEpisode { } } - if result.is_empty() { None } else { Some(Value::Object(result)) } + if result.is_empty() { + None + } else { + let s = serde_json::to_string(&Value::Object(result)).ok()?; + RawValue::from_string(s).ok() + } } } diff --git a/backend/src/processing/processor/trakt.rs b/backend/src/processing/processor/trakt.rs index 539f236da..b2083a318 100644 --- a/backend/src/processing/processor/trakt.rs +++ b/backend/src/processing/processor/trakt.rs @@ -40,14 +40,13 @@ fn is_compatible_content_type(cluster: XtreamCluster, content_type: TraktContent /// Extract TMDB ID from playlist item fn extract_tmdb_id_from_playlist_item(item: &PlaylistItem) -> Option { if let Some(additional_props) = &item.header.additional_properties { - if let Some(props_str) = additional_props.as_str() { - if let Ok(props) = serde_json::from_str::>(props_str) { - return props - .get("tmdb_id") - .and_then(get_u32_from_serde_value) - .filter(|&id| id != 0) - .or_else(|| props.get("tmdb").and_then(get_u32_from_serde_value)); - } + let props_str = additional_props.get(); + if let Ok(props) = serde_json::from_str::>(props_str) { + return props + .get("tmdb_id") + .and_then(get_u32_from_serde_value) + .filter(|&id| id != 0) + .or_else(|| props.get("tmdb").and_then(get_u32_from_serde_value)); } } None diff --git a/backend/src/processing/processor/xtream_vod.rs b/backend/src/processing/processor/xtream_vod.rs index 26810540f..badfbc28f 100644 --- a/backend/src/processing/processor/xtream_vod.rs +++ b/backend/src/processing/processor/xtream_vod.rs @@ -8,11 +8,12 @@ use crate::repository::xtream_repository::{write_vod_info_to_wal_file, xtream_up use shared::error::{notify_err}; use crate::processing::processor::{handle_error, handle_error_and_return, create_resolve_options_function_for_xtream_target}; use shared::utils::{get_u32_from_serde_value, get_u64_from_serde_value, get_string_from_serde_value}; -use serde_json::{from_str, Map, Value}; +use serde_json::{Map, Value}; use std::collections::{HashMap, HashSet}; use std::io::{Write}; use std::time::Instant; use log::{info, log_enabled, Level}; +use serde_json::value::RawValue; use crate::utils; use crate::processing::processor::xtream::normalize_json_content; use crate::utils::IO_BUFFER_SIZE; @@ -123,13 +124,13 @@ pub async fn playlist_resolve_vod(app_config: &AppConfig, client: &reqwest::Clie // Update in-memory playlist items with the newly fetched vod info. // This makes the data available for subsequent processing steps like STRM export. - pli.header.additional_properties = from_str::>(normalized_str).ok().and_then(|info_doc| { - info_doc.get("info").cloned().map(|info_content| { - let mut wrapped_info = Map::new(); - wrapped_info.insert("info".to_string(), info_content); - Value::Object(wrapped_info) - }) - }); + if let Ok(value) = serde_json::from_str::(normalized_str) { + if let Some(info_content) = value.get("info") { + if let Ok(raw_info) = serde_json::to_string(info_content) { + pli.header.additional_properties = serde_json::from_str::>(&raw_info).ok(); + } + } + } } } } diff --git a/backend/src/repository/strm_repository.rs b/backend/src/repository/strm_repository.rs index 0cfdeb873..d1f63eb27 100644 --- a/backend/src/repository/strm_repository.rs +++ b/backend/src/repository/strm_repository.rs @@ -749,7 +749,10 @@ async fn prepare_strm_files( let quality_string = if strm_target_output.add_quality_to_filename { pli.header.additional_properties .as_ref() - .and_then(|props| props.get("info")) + .and_then(|raw| { + let v: serde_json::Value = serde_json::from_str(raw.get()).ok()?; + v.get("info").cloned() + }).as_ref() .and_then(MediaQuality::from_ffprobe_info) .map_or_else(String::new, |quality| { let formatted = quality.format_for_filename(separator); diff --git a/shared/src/model/playlist.rs b/shared/src/model/playlist.rs index 1d91befa5..099223bca 100644 --- a/shared/src/model/playlist.rs +++ b/shared/src/model/playlist.rs @@ -5,7 +5,7 @@ use serde_json::{Map, Value}; use std::borrow::Cow; use std::fmt::{Display, Formatter}; use std::str::FromStr; - +use serde_json::value::RawValue; // https://de.wikipedia.org/wiki/M3U // https://siptv.eu/howto/playlist.html @@ -166,7 +166,7 @@ pub struct PlaylistItemHeader { pub url: String, pub epg_channel_id: Option, pub xtream_cluster: XtreamCluster, - pub additional_properties: Option, + pub additional_properties: Option>, #[serde(default)] pub item_type: PlaylistItemType, #[serde(default)] @@ -192,32 +192,33 @@ impl PlaylistItemHeader { } } - pub fn get_additional_property(&self, field: &str) -> Option<&Value> { - self.additional_properties.as_ref().and_then(|v| match v { - Value::Object(map) => { - map.get(field) - } + pub fn get_additional_property(&self, field: &str) -> Option { + let raw = self.additional_properties.as_ref()?; + let value: Value = serde_json::from_str(raw.get()).ok()?; + + match value { + Value::Object(map) => map.get(field).cloned(), _ => None, - }) + } } pub fn get_additional_property_as_u32(&self, field: &str) -> Option { match self.get_additional_property(field) { - Some(value) => get_u32_from_serde_value(value), + Some(value) => get_u32_from_serde_value(&value), None => None } } pub fn get_additional_property_as_u64(&self, field: &str) -> Option { match self.get_additional_property(field) { - Some(value) => get_u64_from_serde_value(value), + Some(value) => get_u64_from_serde_value(&value), None => None } } pub fn get_additional_property_as_str(&self, field: &str) -> Option { match self.get_additional_property(field) { - Some(value) => get_string_from_serde_value(value), + Some(value) => get_string_from_serde_value(&value), None => None } } @@ -607,23 +608,22 @@ impl PlaylistItem { pub fn to_xtream(&self) -> XtreamPlaylistItem { let header = &self.header; let provider_id = header.id.parse::().unwrap_or_default(); - let mut additional_properties = None; + let mut additional_properties: Option = None; + if header.xtream_cluster != XtreamCluster::Live { let add_ext = match header.get_additional_property("container_extension") { None => true, - Some(ext) => ext.as_str().is_none_or(str::is_empty) + Some(ext) => ext.as_str().is_none_or(str::is_empty), }; + if add_ext { if let Some(cont_ext) = extract_extension_from_url(&header.url) { - let ext = if let Some(stripped) = cont_ext.strip_prefix('.') { stripped } else { cont_ext }; - let mut result = match header.additional_properties.as_ref() { + let ext = cont_ext.strip_prefix('.').unwrap_or(cont_ext); + // parse RawValue into Value on-demand + let mut result: Map = match header.additional_properties.as_ref() { None => Map::new(), Some(props) => { - if let Value::Object(map) = props { - map.clone() - } else { - Map::new() - } + serde_json::from_str(props.get()).unwrap_or_else(|_| Map::new()) } }; result.insert("container_extension".to_string(), Value::String(ext.to_string())); @@ -631,14 +631,12 @@ impl PlaylistItem { } } } + if additional_properties.is_none() { additional_properties = header.additional_properties.as_ref().and_then(|props| { - serde_json::to_string(props).ok() + serde_json::to_string(props.get()).ok() }); } - // let additional_properties = header.additional_properties.as_ref().and_then(|props| { - // serde_json::to_string(props).ok() - // }); XtreamPlaylistItem { virtual_id: header.virtual_id, @@ -663,33 +661,35 @@ impl PlaylistItem { pub fn to_common(&self) -> CommonPlaylistItem { let header = &self.header; - let mut additional_properties = None; + let mut additional_properties: Option = None; + if header.xtream_cluster != XtreamCluster::Live { let add_ext = match header.get_additional_property("container_extension") { None => true, - Some(ext) => ext.as_str().is_none_or(str::is_empty) + Some(ext) => ext.as_str().is_none_or(str::is_empty), }; + if add_ext { if let Some(cont_ext) = extract_extension_from_url(&header.url) { - let ext = if let Some(stripped) = cont_ext.strip_prefix('.') { stripped } else { cont_ext }; - let mut result = match header.additional_properties.as_ref() { + let ext = cont_ext.strip_prefix('.').unwrap_or(cont_ext); + + // Parse RawValue on-demand + let mut result: Map = match header.additional_properties.as_ref() { None => Map::new(), Some(props) => { - if let Value::Object(map) = props { - map.clone() - } else { - Map::new() - } + serde_json::from_str(props.get()).unwrap_or_else(|_| Map::new()) } }; + result.insert("container_extension".to_string(), Value::String(ext.to_string())); additional_properties = serde_json::to_string(&Value::Object(result)).ok(); } } } + if additional_properties.is_none() { additional_properties = header.additional_properties.as_ref().and_then(|props| { - serde_json::to_string(props).ok() + serde_json::to_string(props.get()).ok() }); }