diff --git a/backend/src/model/xtream.rs b/backend/src/model/xtream.rs index 5dcf707ee..ad311d538 100644 --- a/backend/src/model/xtream.rs +++ b/backend/src/model/xtream.rs @@ -141,7 +141,6 @@ macro_rules! add_to_doc_str_property_if_not_exists { } } - impl XtreamStream { pub fn get_stream_id(&self) -> u32 { self.stream_id.unwrap_or_else(|| self.series_id.unwrap_or(0)) @@ -241,6 +240,10 @@ pub struct XtreamSeriesInfoInfo { episode_run_time: String, #[serde(default, deserialize_with = "string_or_number_u32")] category_id: u32, + #[serde(default, deserialize_with = "opt_string_or_number_u32")] + pub tmdb_id: Option, + #[serde(default, deserialize_with = "opt_string_or_number_u32")] + pub tmdb: Option, } #[allow(non_snake_case)] @@ -248,6 +251,8 @@ pub struct XtreamSeriesInfoInfo { pub struct XtreamSeriesInfoEpisodeInfo { #[serde(default, deserialize_with = "opt_string_or_number_u32")] pub tmdb_id: Option, + #[serde(default, deserialize_with = "opt_string_or_number_u32")] + pub tmdb: Option, #[serde(default, deserialize_with = "string_default_on_null")] pub release_date: String, #[serde(default, deserialize_with = "string_default_on_null")] @@ -327,7 +332,7 @@ impl XtreamSeriesEpisode { custom_sid: info_episode.custom_sid.clone(), added: info_episode.added.clone(), season: info_episode.season, - tmdb_id: info_episode.info.as_ref().and_then(|info| info.tmdb_id).unwrap_or(0), + tmdb_id: info_episode.info.as_ref().and_then(|info| info.tmdb.or(info.tmdb_id)).unwrap_or(0), direct_source: info_episode.direct_source.clone(), } } @@ -368,7 +373,8 @@ impl XtreamSeriesInfoEpisode { add_str_property_if_exists!(result, self.title, "title"); add_i64_property_if_exists!(result, self.season, "season"); add_i64_property_if_exists!(result, self.episode_num, "episode"); - add_opt_i64_property_if_exists!(result, self.info.as_ref().and_then(|info| info.tmdb_id), "tmdb_id"); + let series_tmdb_id = info.and_then(|i| i.tmdb_id.or(i.tmdb)); + add_opt_i64_property_if_exists!(result, self.info.as_ref().and_then(|info| info.tmdb_id.or(info.tmdb.or(series_tmdb_id))), "tmdb_id"); if result.is_empty() { None } else { Some(Value::Object(result)) } } } diff --git a/backend/src/processing/parser/xtream.rs b/backend/src/processing/parser/xtream.rs index 7c4f035d4..cc5c27fb4 100644 --- a/backend/src/processing/parser/xtream.rs +++ b/backend/src/processing/parser/xtream.rs @@ -46,7 +46,18 @@ pub fn parse_xtream_series_info(info: &Value, group_title: &str, series_name: &s if let Some(episodes) = &series_info.episodes { let result: Vec<(XtreamSeriesInfoEpisode, PlaylistItem)> = episodes.values().flatten().map(|episode| { let episode_url = create_xtream_series_episode_url(url, username, password, episode); - (episode.clone(), + let mut new_episode = episode.clone(); + + // We need to set the tmdb_id and tmdb from the series info if it is not set. + if let Some(episode_info) = &mut new_episode.info { + if episode_info.tmdb_id.is_none_or(|id| id == 0) && episode_info.tmdb.is_none_or(|id| id == 0) { + let series_tmdb_id = series_info.info.as_ref().and_then(|i| i.tmdb_id.or(i.tmdb)); + episode_info.tmdb_id = series_tmdb_id; + episode_info.tmdb = series_tmdb_id; + } + } + + (new_episode, PlaylistItem { header: PlaylistItemHeader { id: episode.id.to_string(), diff --git a/backend/src/processing/processor/trakt.rs b/backend/src/processing/processor/trakt.rs index 16f694835..708d788bb 100644 --- a/backend/src/processing/processor/trakt.rs +++ b/backend/src/processing/processor/trakt.rs @@ -43,12 +43,11 @@ 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) { - if let Some(tmdb_value) = props.get("tmdb") { - return get_u32_from_serde_value(tmdb_value); - } - if let Some(tmdb_id_value) = props.get("tmdb_id") { - return get_u32_from_serde_value(tmdb_id_value); - } + 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)); } } } diff --git a/backend/src/processing/processor/xtream_series.rs b/backend/src/processing/processor/xtream_series.rs index b08772a62..752d2ec55 100644 --- a/backend/src/processing/processor/xtream_series.rs +++ b/backend/src/processing/processor/xtream_series.rs @@ -16,7 +16,7 @@ use std::fs::File; use std::io::{BufWriter, Write}; use std::sync::Arc; use std::time::Instant; -use log::{error, info, log_enabled, Level}; +use log::{error, info, log_enabled, warn, Level}; use crate::model::{XtreamSeriesEpisode, XtreamSeriesInfoEpisode}; use crate::utils; use crate::processing::processor::xtream::normalize_json_content; @@ -167,6 +167,10 @@ async fn process_series_info( { let Some(provider_id) = pli.header.get_provider_id() else { continue; }; let Ok(content) = info_reader.get(&provider_id) else { continue; }; + if content.is_empty() { + warn!("Series info content is empty, skipping series with provider id: {provider_id}"); + continue; + } match serde_json::from_str::(&content) { Ok(series_content) => { let (group, series_name) = { diff --git a/backend/src/processing/processor/xtream_vod.rs b/backend/src/processing/processor/xtream_vod.rs index 3cf8bcf22..c7010fc47 100644 --- a/backend/src/processing/processor/xtream_vod.rs +++ b/backend/src/processing/processor/xtream_vod.rs @@ -42,6 +42,12 @@ fn extract_info_record_from_vod_info(content: &str) -> Option<(u32, InputVodInfo let tmdb_id = info_section .get(crate::model::XC_TAG_VOD_INFO_TMDB_ID) .and_then(get_u32_from_serde_value) + .filter(|&id| id != 0) + .or_else(|| { + info_section + .get(crate::model::XC_TAG_VOD_INFO_TMDB) + .and_then(get_u32_from_serde_value) + }) .unwrap_or(0); let release_date = info_section diff --git a/backend/src/repository/strm_repository.rs b/backend/src/repository/strm_repository.rs index 3a7001468..426ae3130 100644 --- a/backend/src/repository/strm_repository.rs +++ b/backend/src/repository/strm_repository.rs @@ -58,7 +58,10 @@ fn sanitize_for_filename(text: &str, underscore_whitespace: bool) -> String { sanitized.remove(0); } - // 4. Final check: If sanitization resulted in an empty string, return a default. + // 4. Remove empty paaren + sanitized = CONSTANTS.export_style_config.paaren.replace_all(sanitized.as_str(), "").trim().to_string(); + + // 5. Final check: If sanitization resulted in an empty string, return a default. if sanitized.is_empty() { EMPTY_FILENAME_REPLACEMENT.to_string() } else { @@ -139,7 +142,6 @@ fn style_rename_year<'a>( } } new_name.push_str(&name[last_index..]); - let smallest_year = years.into_iter().min(); if smallest_year.is_none() { if let Some(rel_date) = release_date { @@ -257,6 +259,7 @@ struct StrmItemInfo { season: Option, episode: Option, added: Option, + tmdb_id: Option, } impl StrmItemInfo { @@ -274,7 +277,7 @@ fn extract_item_info(pli: &mut PlaylistItem) -> StrmItemInfo { let virtual_id = header.virtual_id; let input_name = header.input_name.clone(); let url = header.url.clone(); - let (series_name, release_date, added, season, episode) = match header.item_type { + let (series_name, release_date, added, season, episode, tmdb_id) = match header.item_type { PlaylistItemType::Series => { let series_name = match header.get_field("name") { Some(name) if !name.is_empty() => Some(name.to_string()), @@ -286,15 +289,23 @@ fn extract_item_info(pli: &mut PlaylistItem) -> StrmItemInfo { let season = header.get_additional_property_as_str("season"); let episode = header.get_additional_property_as_str("episode"); let added = header.get_additional_property_as_u64("added"); - (series_name, release_date, added, season, episode) + let tmdb_id = header + .get_additional_property_as_u32("tmdb_id") + .filter(|&id| id != 0) + .or_else(|| header.get_additional_property_as_u32("tmdb")); + (series_name, release_date, added, season, episode, tmdb_id) } PlaylistItemType::Video => { let name = header.get_field("name").map(|v| v.to_string()); let release_date = header.get_additional_property_as_str("release_date"); let added = header.get_additional_property_as_u64("added"); - (name, release_date, added, None, None) + let tmdb_id = header + .get_additional_property_as_u32("tmdb_id") + .filter(|&id| id != 0) + .or_else(|| header.get_additional_property_as_u32("tmdb")); + (name, release_date, added, None, None, tmdb_id) } - _ => (None, None, None, None, None), + _ => (None, None, None, None, None, None), }; StrmItemInfo { group, @@ -309,6 +320,7 @@ fn extract_item_info(pli: &mut PlaylistItem) -> StrmItemInfo { season, episode, added, + tmdb_id, } } @@ -685,7 +697,7 @@ async fn style_based_rename( strm_item_info.item_type, ).await; - let tmdb_id = tmdb_id_val.map_or(0, |v| match v { + let tmdb_id = tmdb_id_val.map_or_else(|| strm_item_info.tmdb_id.unwrap_or(0), |v| match v { InputTmdbIndexValue::Video(r) => r.tmdb_id, InputTmdbIndexValue::Series(e) => e.tmdb_id, }); diff --git a/backend/src/utils/file/config_reader.rs b/backend/src/utils/file/config_reader.rs index 4a73c066f..149f85a73 100644 --- a/backend/src/utils/file/config_reader.rs +++ b/backend/src/utils/file/config_reader.rs @@ -1,6 +1,6 @@ use crate::model::Config; use crate::model::{ApiProxyConfig, AppConfig, SourcesConfig}; -use crate::{print_info, utils}; +use crate::{utils}; use crate::utils::file_reader; use crate::utils::sys_utils::exit; use crate::utils::{open_file, read_mappings_file, EnvResolvingReader, FileLockManager}; @@ -239,7 +239,7 @@ pub fn read_initial_app_config(paths: &mut ConfigPaths, encrypt_secret: Default::default(), }; app_config.prepare(include_computed)?; - print_info(&app_config); + //print_info(&app_config); if let Some(mappings_file) = &paths.mapping_file_path { match utils::read_mappings(mappings_file.as_str(), resolve_env) { diff --git a/shared/src/model/playlist.rs b/shared/src/model/playlist.rs index cbca4f3ec..363c00e3d 100644 --- a/shared/src/model/playlist.rs +++ b/shared/src/model/playlist.rs @@ -4,7 +4,7 @@ use std::str::FromStr; use serde::{Deserialize, Serialize}; use serde_json::{Map, Value}; use crate::model::{ConfigTargetOptions, xtream_const, CommonPlaylistItem}; -use crate::utils::{extract_extension_from_url, generate_playlist_uuid, get_provider_id, get_string_from_serde_value, get_u64_from_serde_value}; +use crate::utils::{extract_extension_from_url, generate_playlist_uuid, get_provider_id, get_string_from_serde_value, get_u32_from_serde_value, get_u64_from_serde_value}; // https://de.wikipedia.org/wiki/M3U // https://siptv.eu/howto/playlist.html @@ -186,12 +186,14 @@ impl PlaylistItemHeader { _ => 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), - // None => 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), + 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), diff --git a/shared/src/model/xtream_const.rs b/shared/src/model/xtream_const.rs index 148478d78..461a828fd 100644 --- a/shared/src/model/xtream_const.rs +++ b/shared/src/model/xtream_const.rs @@ -35,6 +35,7 @@ pub const XC_TAG_EPISODES: &str = "episodes"; pub const XC_TAG_VOD_INFO_INFO: &str = "info"; pub const XC_TAG_VOD_INFO_MOVIE_DATA: &str = "movie_data"; pub const XC_TAG_VOD_INFO_TMDB_ID: &str = "tmdb_id"; +pub const XC_TAG_VOD_INFO_TMDB: &str = "tmdb"; pub const XC_TAG_VOD_INFO_STREAM_ID: &str = "stream_id"; pub const XC_TAG_VOD_INFO_ADDED: &str = "added"; pub const XC_TAG_VOD_INFO_RELEASEDATE: &str = "release_date"; diff --git a/shared/src/utils/constants.rs b/shared/src/utils/constants.rs index 99450410e..56c7fb9bb 100644 --- a/shared/src/utils/constants.rs +++ b/shared/src/utils/constants.rs @@ -67,6 +67,7 @@ pub struct ExportStyleConfig { pub episode: Regex, pub whitespace: Regex, pub alphanumeric: Regex, + pub paaren: Regex, } pub struct Constants { @@ -126,6 +127,7 @@ pub static CONSTANTS: LazyLock = LazyLock::new(|| year: Regex::new(r"(\d{4})").unwrap(), whitespace: Regex::new(r"\s+").unwrap(), alphanumeric: Regex::new(r"[^\w\s]").unwrap(), + paaren: Regex::new(r"(\(\)|\[\]|\{\})").unwrap(), }, allowed_output_formats: Vec::from(["m3u8".to_string(), "ts".to_string()]), country_codes: vec![