diff --git a/backend/src/model/xtream.rs b/backend/src/model/xtream.rs index ad311d538..93e9ad777 100644 --- a/backend/src/model/xtream.rs +++ b/backend/src/model/xtream.rs @@ -1,7 +1,7 @@ use crate::model::{AppConfig, ProxyUserCredentials}; use crate::model::{ConfigTarget, XtreamTargetOutput}; use shared::model::{xtream_const, PlaylistItem,XtreamPlaylistItem}; -use shared::utils::{deserialize_as_option_rc_string, deserialize_as_rc_string, deserialize_as_string_array, deserialize_number_from_string, +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_json::{Map, Value}; @@ -11,9 +11,9 @@ use shared::model::{ClusterFlags, PlaylistEntry, XtreamCluster}; #[derive(Deserialize, Default)] pub struct XtreamCategory { - #[serde(deserialize_with = "deserialize_as_rc_string")] + #[serde(deserialize_with = "deserialize_as_string")] pub category_id: String, - #[serde(deserialize_with = "deserialize_as_rc_string")] + #[serde(deserialize_with = "deserialize_as_string")] pub category_name: String, //pub parent_id: i32, #[serde(default)] @@ -28,66 +28,68 @@ impl XtreamCategory { #[derive(Serialize, Deserialize)] pub struct XtreamStream { - #[serde(default, deserialize_with = "deserialize_as_rc_string")] + #[serde(default, deserialize_with = "deserialize_as_string")] pub name: String, - #[serde(default, deserialize_with = "deserialize_as_rc_string")] + #[serde(default, deserialize_with = "deserialize_as_string")] pub category_id: String, #[serde(default, deserialize_with = "deserialize_number_from_string")] pub stream_id: Option, #[serde(default, deserialize_with = "deserialize_number_from_string")] pub series_id: Option, - #[serde(default, deserialize_with = "deserialize_as_rc_string")] + #[serde(default, deserialize_with = "deserialize_as_string")] pub stream_icon: String, - #[serde(default, deserialize_with = "deserialize_as_rc_string")] + #[serde(default, deserialize_with = "deserialize_as_string")] pub direct_source: String, // optional attributes - #[serde(default, deserialize_with = "deserialize_as_option_rc_string")] + #[serde(default, deserialize_with = "deserialize_as_option_string")] pub custom_sid: Option, #[serde(default, deserialize_with = "deserialize_as_string_array")] pub backdrop_path: Option>, - #[serde(default, deserialize_with = "deserialize_as_option_rc_string")] + #[serde(default, deserialize_with = "deserialize_as_option_string")] pub added: Option, - #[serde(default, deserialize_with = "deserialize_as_option_rc_string")] + #[serde(default, deserialize_with = "deserialize_as_option_string")] pub cast: Option, - #[serde(default, deserialize_with = "deserialize_as_option_rc_string")] + #[serde(default, deserialize_with = "deserialize_as_option_string")] pub container_extension: Option, - #[serde(default, deserialize_with = "deserialize_as_option_rc_string")] + #[serde(default, deserialize_with = "deserialize_as_option_string")] pub cover: Option, - #[serde(default, deserialize_with = "deserialize_as_option_rc_string")] + #[serde(default, deserialize_with = "deserialize_as_option_string")] pub director: Option, - #[serde(default, deserialize_with = "deserialize_as_option_rc_string")] + #[serde(default, deserialize_with = "deserialize_as_option_string")] pub episode_run_time: Option, - #[serde(default, deserialize_with = "deserialize_as_option_rc_string")] + #[serde(default, deserialize_with = "deserialize_as_option_string")] pub genre: Option, - #[serde(default, deserialize_with = "deserialize_as_option_rc_string")] + #[serde(default, deserialize_with = "deserialize_as_option_string")] pub last_modified: Option, - #[serde(default, deserialize_with = "deserialize_as_option_rc_string")] + #[serde(default, deserialize_with = "deserialize_as_option_string")] pub plot: Option, #[serde(default, deserialize_with = "deserialize_number_from_string")] pub rating: Option, #[serde(default, deserialize_with = "deserialize_number_from_string")] pub rating_5based: Option, - #[serde(default, deserialize_with = "deserialize_as_option_rc_string")] + #[serde(default, deserialize_with = "deserialize_as_option_string")] pub release_date: Option, - #[serde(default, deserialize_with = "deserialize_as_option_rc_string")] + #[serde(default, deserialize_with = "deserialize_as_option_string")] pub stream_type: Option, - #[serde(default, deserialize_with = "deserialize_as_option_rc_string")] + #[serde(default, deserialize_with = "deserialize_as_option_string")] pub title: Option, - #[serde(default, deserialize_with = "deserialize_as_option_rc_string")] + #[serde(default, deserialize_with = "deserialize_as_option_string")] pub year: Option, - #[serde(default, deserialize_with = "deserialize_as_option_rc_string")] + #[serde(default, deserialize_with = "deserialize_as_option_string")] pub trailer: Option, - #[serde(default, deserialize_with = "deserialize_as_option_rc_string")] + #[serde(default, deserialize_with = "deserialize_as_option_string")] pub youtube_trailer: Option, - #[serde(default, deserialize_with = "deserialize_as_option_rc_string")] + #[serde(default, deserialize_with = "deserialize_as_option_string")] pub epg_channel_id: Option, #[serde(default, deserialize_with = "deserialize_number_from_string")] pub tv_archive: Option, #[serde(default, deserialize_with = "deserialize_number_from_string")] pub tv_archive_duration: Option, - #[serde(default, deserialize_with = "deserialize_as_option_rc_string")] - pub tmdb: Option, + #[serde(default, deserialize_with = "deserialize_number_from_string")] + pub tmdb: Option, + #[serde(default, deserialize_with = "deserialize_number_from_string")] + pub tmdb_id: Option, #[serde(default, deserialize_with = "deserialize_number_from_string")] pub is_adult: Option, @@ -153,7 +155,8 @@ impl XtreamStream { result.insert(String::from(xtream_const::XC_PROP_BACKDROP_PATH), Value::Array(Vec::from([Value::String(String::from(bdpath.first()?))]))); } } - add_rc_str_property_if_exists!(result, self.tmdb, "tmdb"); + + add_rc_str_property_if_exists!(result, self.tmdb_id.or(self.tmdb), "tmdb"); add_rc_str_property_if_exists!(result, self.added, "added"); add_rc_str_property_if_exists!(result, self.cast, "cast"); add_rc_str_property_if_exists!(result, self.container_extension, "container_extension"); @@ -332,7 +335,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.or(info.tmdb_id)).unwrap_or(0), + tmdb_id: info_episode.info.as_ref().and_then(|info| info.tmdb_id.or(info.tmdb)).unwrap_or(0), direct_source: info_episode.direct_source.clone(), } } diff --git a/backend/src/repository/strm_repository.rs b/backend/src/repository/strm_repository.rs index 426ae3130..aff0e7cb4 100644 --- a/backend/src/repository/strm_repository.rs +++ b/backend/src/repository/strm_repository.rs @@ -7,8 +7,8 @@ use crate::repository::bplustree::BPlusTree; use crate::repository::storage::{ensure_target_storage_path, get_input_storage_path}; use crate::repository::storage_const; use crate::repository::xtream_repository::{xtream_get_record_file_path, InputVodInfoRecord}; -use shared::utils::{extract_extension_from_url, hash_bytes, ExportStyleConfig, CONSTANTS}; -use crate::utils::FileReadGuard; +use shared::utils::{extract_extension_from_url, hash_bytes, truncate_string, ExportStyleConfig, CONSTANTS}; +use crate::utils::{truncate_filename, FileReadGuard}; use chrono::Datelike; use filetime::{set_file_times, FileTime}; use log::{error, trace}; @@ -852,10 +852,10 @@ pub async fn write_strm_playlist( ).await; for strm_file in strm_files { // file paths - let output_path = root_path.join(&strm_file.dir_path); - let file_path = output_path.join(format!("{}.strm", strm_file.file_name)); + let output_path = truncate_filename(&root_path.join(&strm_file.dir_path), 255); + let file_path = &output_path.join(format!("{}.strm", truncate_string(&strm_file.file_name, 252))); let file_exists = file_path.exists(); - let relative_file_path = get_relative_path_str(&file_path, &root_path); + let relative_file_path = get_relative_path_str(file_path, &root_path); // create content let url = get_strm_url(target_force_redirect, user_and_server_info.as_ref(), &strm_file.strm_info); @@ -866,7 +866,7 @@ pub async fn write_strm_playlist( let content_hash = hash_bytes(content_as_bytes); // check if file exists and has same hash - if file_exists && has_strm_file_same_hash(&file_path, content_hash).await { + if file_exists && has_strm_file_same_hash(file_path, content_hash).await { processed_strm.insert(relative_file_path); continue; // skip creation } @@ -877,7 +877,7 @@ pub async fn write_strm_playlist( } match write_strm_file( - &file_path, + file_path, content_as_bytes, strm_file.strm_info.get_file_ts(), ).await diff --git a/backend/src/utils/file/file_utils.rs b/backend/src/utils/file/file_utils.rs index a8a44aaae..386ad2fab 100644 --- a/backend/src/utils/file/file_utils.rs +++ b/backend/src/utils/file/file_utils.rs @@ -332,3 +332,29 @@ pub fn cleanup_unlisted_files_with_suffix( Ok(()) } + +pub fn truncate_filename(path: &Path, max_len: usize) -> PathBuf { + let file_name = path.file_name() + .and_then(|s| s.to_str()) + .unwrap_or_default(); + + let truncated_name = if file_name.len() > max_len { + // If a filename extension exists, keep it + if let Some(ext) = path.extension().and_then(|e| e.to_str()) { + let ext_len = ext.len() + 1; // +1 for the dot + if max_len > ext_len { + let name_len = max_len - ext_len; + format!("{}.{ext}", &file_name[..name_len]) + } else { + // it is not enoguh for the extension, so just truncate the filename + file_name[..max_len].to_string() + } + } else { + file_name[..max_len].to_string() + } + } else { + file_name.to_string() + }; + + path.with_file_name(truncated_name) +} \ No newline at end of file diff --git a/shared/src/utils/serde_utils.rs b/shared/src/utils/serde_utils.rs index 1ed566015..d527ee56e 100644 --- a/shared/src/utils/serde_utils.rs +++ b/shared/src/utils/serde_utils.rs @@ -15,7 +15,7 @@ fn value_to_string(v: &Value) -> Option { } } -pub fn deserialize_as_option_rc_string<'de, D>(deserializer: D) -> Result, D::Error> +pub fn deserialize_as_option_string<'de, D>(deserializer: D) -> Result, D::Error> where D: Deserializer<'de>, { @@ -28,7 +28,7 @@ where } } -pub fn deserialize_as_rc_string<'de, D>(deserializer: D) -> Result +pub fn deserialize_as_string<'de, D>(deserializer: D) -> Result where D: Deserializer<'de>, { @@ -52,8 +52,7 @@ where }) } - -pub fn deserialize_number_from_string<'de, D, T: DeserializeOwned>( +pub fn deserialize_number_from_string<'de, D, T: DeserializeOwned + std::str::FromStr>( deserializer: D, ) -> Result, D::Error> where @@ -78,8 +77,17 @@ where MaybeNumber::Value(value) => Ok(value), // (if it is any other string) - MaybeNumber::NumberString(string) => { - serde_json::from_str::(string.as_str()).map_or_else(|_| Ok(None), |val| Ok(Some(val))) + MaybeNumber::NumberString(s) => { + let s = s.trim(); + if s.is_empty() { + return Ok(None); + } + // parse string to number, if fails return None + if let Ok(num) = s.parse::() { + return Ok(Some(num)); + } + + serde_json::from_str::(s).map_or_else(|_| Ok(None), |val| Ok(Some(val))) } } } diff --git a/shared/src/utils/string_utils.rs b/shared/src/utils/string_utils.rs index 34a706c48..1fd1dcb96 100644 --- a/shared/src/utils/string_utils.rs +++ b/shared/src/utils/string_utils.rs @@ -91,6 +91,14 @@ impl Substring for String { } +pub fn truncate_string(s: &str, max_len: usize) -> String { + if s.chars().count() <= max_len { + s.to_string() + } else { + s.chars().take(max_len).collect() + } +} + #[cfg(test)] mod test { use std::collections::HashSet; @@ -111,4 +119,4 @@ mod test { assert_eq!("hELLO".capitalize(), "Hello"); } -} \ No newline at end of file +}