From cf0c928730912aba56e76f4780dcbafa4f14c013 Mon Sep 17 00:00:00 2001 From: euzu Date: Fri, 26 Dec 2025 16:03:03 +0100 Subject: [PATCH] stream properties refactored --- Cargo.lock | 17 +- backend/Cargo.toml | 1 - backend/src/api/api_utils.rs | 16 +- .../src/api/endpoints/api_playlist_utils.rs | 4 +- backend/src/api/endpoints/hdhomerun_api.rs | 2 +- backend/src/api/endpoints/v1_api.rs | 2 +- backend/src/api/endpoints/v1_api_playlist.rs | 12 +- backend/src/api/endpoints/xmltv_api.rs | 4 +- backend/src/library/metadata_resolver.rs | 7 +- backend/src/library/tmdb_client.rs | 6 +- backend/src/main.rs | 2 +- backend/src/model/media_properties.rs | 6 +- backend/src/model/xtream.rs | 2 +- backend/src/processing/parser/xtream.rs | 31 +-- backend/src/processing/processor/epg.rs | 4 +- backend/src/processing/processor/playlist.rs | 4 +- .../src/processing/processor/xtream_series.rs | 96 --------- .../src/processing/processor/xtream_vod.rs | 14 +- backend/src/ptt/parser.rs | 4 +- backend/src/ptt/transformers.rs | 14 -- backend/src/repository/bplustree.rs | 12 +- backend/src/repository/playlist_scratch.rs | 3 +- backend/src/repository/strm_repository.rs | 10 +- .../repository/xtream_playlist_iterator.rs | 1 + backend/src/repository/xtream_repository.rs | 11 +- backend/src/tools/lru_cache.rs | 17 +- backend/src/utils/logging.rs | 2 +- backend/src/utils/network/xtream.rs | 15 +- shared/src/model/config/cache.rs | 2 +- shared/src/model/config/input.rs | 2 +- shared/src/model/config/ipcheck.rs | 10 +- shared/src/model/config/log.rs | 4 +- shared/src/model/config/macros.rs | 4 +- shared/src/model/config/messaging.rs | 2 +- shared/src/model/config/proxy.rs | 8 +- shared/src/model/config/video_download.rs | 6 +- shared/src/model/config/web_auth.rs | 2 +- shared/src/model/config/web_ui.rs | 8 +- shared/src/model/info_doc_utils.rs | 24 +-- shared/src/model/playlist.rs | 112 +++++------ shared/src/model/stream_properties.rs | 183 +++++++++--------- shared/src/model/xtream.rs | 14 +- shared/src/utils/request.rs | 7 +- shared/src/utils/serde_utils.rs | 28 +-- shared/src/utils/string_utils.rs | 6 +- 45 files changed, 295 insertions(+), 446 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e633f44cd..90f55fac6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -350,12 +350,6 @@ version = "3.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" -[[package]] -name = "byteorder" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" - [[package]] name = "bytes" version = "1.11.0" @@ -3433,22 +3427,19 @@ dependencies = [ [[package]] name = "rmp" -version = "0.8.14" +version = "0.8.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "228ed7c16fa39782c3b3468e974aec2795e9089153cd08ee2e9aefb3613334c4" +checksum = "4ba8be72d372b2c9b35542551678538b562e7cf86c3315773cae48dfbfe7790c" dependencies = [ - "byteorder", "num-traits", - "paste", ] [[package]] name = "rmp-serde" -version = "1.3.0" +version = "1.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52e599a477cf9840e92f2cde9a7189e67b42c57532749bf90aea6ec10facd4db" +checksum = "72f81bee8c8ef9b577d1681a70ebbc962c232461e397b22c208c43c04b67a155" dependencies = [ - "byteorder", "rmp", "serde", ] diff --git a/backend/Cargo.toml b/backend/Cargo.toml index 7f8bf4582..1bd25e2bb 100644 --- a/backend/Cargo.toml +++ b/backend/Cargo.toml @@ -35,7 +35,6 @@ deunicode = "1.6.2" mime = "0.3" log = "0.4" env_logger = "0.11" -#bincode = { version = "2.0", features = ["std", "serde"] } rmp-serde = "1" rand = "0.9" fastrand = "2" diff --git a/backend/src/api/api_utils.rs b/backend/src/api/api_utils.rs index 83f9984a3..395dea33e 100644 --- a/backend/src/api/api_utils.rs +++ b/backend/src/api/api_utils.rs @@ -615,7 +615,7 @@ where P: PlaylistEntry, { let item_type = params.item.get_item_type(); - let provider_url = ¶ms.item.get_provider_url(); + let provider_url = params.item.get_provider_url(); let redirect_request = params.user.proxy.is_redirect(item_type) || params.target.is_force_redirect(item_type); @@ -627,17 +627,17 @@ where if params.target_type == TargetType::M3u { if redirect_request || is_dash_request { let redirect_url = if is_hls_request { - &replace_url_extension(provider_url, HLS_EXT) + Cow::Owned(replace_url_extension(&provider_url, HLS_EXT)) } else { provider_url }; let redirect_url = if is_dash_request { - &replace_url_extension(redirect_url, DASH_EXT) + Cow::Owned(replace_url_extension(&redirect_url, DASH_EXT)) } else { redirect_url }; let redirect_url = - get_redirect_alternative_url(app_state, redirect_url, params.input).await; + get_redirect_alternative_url(app_state, &redirect_url, params.input).await; debug_if_enabled!( "Redirecting stream request to {}", sanitize_sensitive_info(&redirect_url) @@ -670,8 +670,8 @@ where let stream_url = match get_xtream_player_api_stream_url( params.input, params.req_context, - ¶ms.get_query_path(provider_id, provider_url), - provider_url, + ¶ms.get_query_path(provider_id, &provider_url), + &provider_url, ) { None => { error!("Cant find stream url for target {target_name}, context {}, stream_id {virtual_id}", params.req_context); @@ -1234,7 +1234,7 @@ fn get_mime_type(headers: &axum::http::HeaderMap, resource_url: &str) -> Option< .and_then(|v| v.to_str().ok()) // Option<&str> .map(ToString::to_string) // Option .or_else(|| { - // fallback auf guess + // fallback to guess mime_guess::from_path(resource_url) .first_raw() .map(ToString::to_string) @@ -1529,7 +1529,7 @@ pub fn json_response(data: &T) -> impl IntoResponse + Send { (axum::http::StatusCode::OK, axum::Json(data)).into_response() } -pub fn json_or_bin_response(accept: Option<&String>, data: &T) -> impl IntoResponse + Send { +pub fn json_or_bin_response(accept: Option<&str>, data: &T) -> impl IntoResponse + Send { if accept.is_some_and(|a| a.contains(CONTENT_TYPE_BIN)) { return bin_response(data).into_response(); } diff --git a/backend/src/api/endpoints/api_playlist_utils.rs b/backend/src/api/endpoints/api_playlist_utils.rs index 15300021a..5185a69c0 100644 --- a/backend/src/api/endpoints/api_playlist_utils.rs +++ b/backend/src/api/endpoints/api_playlist_utils.rs @@ -115,7 +115,7 @@ async fn grouped_channels( )) } -pub(in crate::api::endpoints) async fn get_playlist_for_target(cfg_target: Option<&ConfigTarget>, cfg: &AppConfig, accept: Option<&String>) -> impl axum::response::IntoResponse + Send { +pub(in crate::api::endpoints) async fn get_playlist_for_target(cfg_target: Option<&ConfigTarget>, cfg: &AppConfig, accept: Option<&str>) -> impl axum::response::IntoResponse + Send { if let Some(target) = cfg_target { if target.has_output(TargetType::Xtream) { let live_channels = grouped_channels(cfg, target, XtreamCluster::Live).await; @@ -144,7 +144,7 @@ pub(in crate::api::endpoints) async fn get_playlist_for_target(cfg_target: Optio (axum::http::StatusCode::BAD_REQUEST, axum::Json(json!({"error": "Invalid Arguments"}))).into_response() } -pub(in crate::api::endpoints) async fn get_playlist(client: &reqwest::Client, cfg_input: Option<&Arc>, cfg: &Arc, accept: Option<&String>) -> impl IntoResponse + Send { +pub(in crate::api::endpoints) async fn get_playlist(client: &reqwest::Client, cfg_input: Option<&Arc>, cfg: &Arc, accept: Option<&str>) -> impl IntoResponse + Send { match cfg_input { Some(input) => { let (result, errors) = diff --git a/backend/src/api/endpoints/hdhomerun_api.rs b/backend/src/api/endpoints/hdhomerun_api.rs index 694fd715e..441e4b6df 100644 --- a/backend/src/api/endpoints/hdhomerun_api.rs +++ b/backend/src/api/endpoints/hdhomerun_api.rs @@ -121,7 +121,7 @@ where &credentials.username, &credentials.password, item.virtual_id, - container_extension.as_ref(), + container_extension.as_deref(), live_stream_use_prefix, live_stream_without_extension, ), diff --git a/backend/src/api/endpoints/v1_api.rs b/backend/src/api/endpoints/v1_api.rs index b476f0d2e..37889172f 100644 --- a/backend/src/api/endpoints/v1_api.rs +++ b/backend/src/api/endpoints/v1_api.rs @@ -71,7 +71,7 @@ async fn status(axum::extract::State(app_state): axum::extract::State>) -> axum::response::Response { let streams = app_state.active_users.active_streams().await; - json_or_bin_response(accept.as_ref(), &streams).into_response() + json_or_bin_response(accept.as_deref(), &streams).into_response() } async fn geoip_update(axum::extract::State(app_state): axum::extract::State>) -> axum::response::Response { diff --git a/backend/src/api/endpoints/v1_api_playlist.rs b/backend/src/api/endpoints/v1_api_playlist.rs index 9f6d34443..50503dd76 100644 --- a/backend/src/api/endpoints/v1_api_playlist.rs +++ b/backend/src/api/endpoints/v1_api_playlist.rs @@ -89,16 +89,16 @@ async fn playlist_content( let client = app_state.http_client.load(); match playlist_req { PlaylistRequest::Target(target_id) => { - get_playlist_for_target(app_state.app_config.get_target_by_id(target_id).as_deref(), &app_state.app_config, accept.as_ref()).await.into_response() + get_playlist_for_target(app_state.app_config.get_target_by_id(target_id).as_deref(), &app_state.app_config, accept.as_deref()).await.into_response() } PlaylistRequest::Input(input_id) => { - get_playlist(client.as_ref(), app_state.app_config.get_input_by_id(input_id).as_ref(), &config, accept.as_ref()).await.into_response() + get_playlist(client.as_ref(), app_state.app_config.get_input_by_id(input_id).as_ref(), &config, accept.as_deref()).await.into_response() } PlaylistRequest::CustomXtream(xtream) => { match Url::parse(&xtream.url) { Ok(parsed) if parsed.scheme() == "http" || parsed.scheme() == "https" => { let input = Arc::new(create_config_input_for_xtream(&xtream.username, &xtream.password, &xtream.url)); - get_playlist(client.as_ref(), Some(&input), &config, accept.as_ref()).await.into_response() + get_playlist(client.as_ref(), Some(&input), &config, accept.as_deref()).await.into_response() } _ => { (axum::http::StatusCode::BAD_REQUEST, axum::Json(json!({"error": "Invalid url scheme; only http/https are allowed"}))).into_response() @@ -109,7 +109,7 @@ async fn playlist_content( match Url::parse(&m3u.url) { Ok(parsed) if parsed.scheme() == "http" || parsed.scheme() == "https" => { let input = Arc::new(create_config_input_for_m3u(&m3u.url)); - get_playlist(client.as_ref(), Some(&input), &config, accept.as_ref()).await.into_response() + get_playlist(client.as_ref(), Some(&input), &config, accept.as_deref()).await.into_response() } _ => { (axum::http::StatusCode::BAD_REQUEST, axum::Json(json!({"error": "Invalid url scheme; only http/https are allowed"}))).into_response() @@ -142,7 +142,7 @@ async fn playlist_epg( let config = &app_state.app_config.config.load(); if let Some(epg_path) = crate::api::endpoints::xmltv_api::get_epg_path_for_target(config, &target) { if let Ok(epg) = parse_xmltv_for_web_ui_from_file(&epg_path).await { - return json_or_bin_response(accept.as_ref(), &epg).into_response(); + return json_or_bin_response(accept.as_deref(), &epg).into_response(); } } } @@ -160,7 +160,7 @@ async fn playlist_epg( } PlaylistEpgRequest::Custom(url) => { if let Ok(epg) = parse_xmltv_for_web_ui_from_url(&app_state, &url).await { - return json_or_bin_response(accept.as_ref(), &epg).into_response(); + return json_or_bin_response(accept.as_deref(), &epg).into_response(); } } } diff --git a/backend/src/api/endpoints/xmltv_api.rs b/backend/src/api/endpoints/xmltv_api.rs index 6701d47d2..b05be20d5 100644 --- a/backend/src/api/endpoints/xmltv_api.rs +++ b/backend/src/api/endpoints/xmltv_api.rs @@ -121,7 +121,7 @@ pub(in crate::api) fn get_epg_path_for_target(config: &Config, target: &ConfigTa /// or a timezone name (e.g. "`Europe/Berlin`", "`UTC`", "`America/New_York`"). /// /// Returns the total offset in minutes (i32). -fn parse_timeshift(time_shift: Option<&String>) -> Option { +fn parse_timeshift(time_shift: Option<&str>) -> Option { time_shift.and_then(|offset| { // Try to parse as timezone name first if let Ok(tz) = offset.parse::() { @@ -164,7 +164,7 @@ pub async fn serve_epg( let rewrite_urls = !redirect && rewrite_resources; // Use 0 for timeshift if None - let timeshift = parse_timeshift(user.epg_timeshift.as_ref()).unwrap_or(0); + let timeshift = parse_timeshift(user.epg_timeshift.as_deref()).unwrap_or(0); return if timeshift != 0 || rewrite_urls || filter.is_some() { let server_info = app_state.app_config.get_user_server_info(user); diff --git a/backend/src/library/metadata_resolver.rs b/backend/src/library/metadata_resolver.rs index d6651f525..ddad1c00b 100644 --- a/backend/src/library/metadata_resolver.rs +++ b/backend/src/library/metadata_resolver.rs @@ -78,8 +78,7 @@ impl MetadataResolver { // Attempts to resolve metadata from TMDB async fn resolve_from_tmdb(&self, movie: bool, file: &ScannedMediaFile, metadata: &PttMetadata, tmdb: &TmdbClient) -> Result, String> { if movie { - let tmdb_id = metadata.tmdb.as_ref(); - tmdb.search_movie(tmdb_id, metadata.title.as_str(), metadata.year.as_ref()).await + tmdb.search_movie(metadata.tmdb, metadata.title.as_str(), metadata.year).await } else { let (series_year, tmdb_id) = if metadata.year.is_some() { (metadata.year, metadata.tmdb) } else { // Try to extract year from parent directory if available @@ -189,7 +188,7 @@ mod tests { MediaClassification::Movie { metadata, .. } => metadata, MediaClassification::Series { metadata, .. } => metadata, }; - let group = MediaGroup::Movie { file, metadata }; + let group = MediaGroup::Movie { file, metadata: Box::new(metadata) }; let metadata = resolver.resolve(&group).await; assert!(metadata.is_some()); @@ -218,7 +217,7 @@ mod tests { MediaClassification::Movie { metadata, .. } => metadata, MediaClassification::Series { metadata, .. } => metadata, }; - let group = MediaGroup::Movie { file, metadata }; + let group = MediaGroup::Movie { file, metadata: Box::new(metadata) }; let metadata = resolver.resolve(&group).await; assert!(metadata.is_none()); diff --git a/backend/src/library/tmdb_client.rs b/backend/src/library/tmdb_client.rs index 5cc264dd5..b4e2bfaab 100644 --- a/backend/src/library/tmdb_client.rs +++ b/backend/src/library/tmdb_client.rs @@ -38,11 +38,11 @@ impl TmdbClient { } // Searches for a movie by title and optional year - pub async fn search_movie(&self, tmdb_id: Option<&u32>, title: &str, year: Option<&u32>) -> Result, String> { + pub async fn search_movie(&self, tmdb_id: Option, title: &str, year: Option) -> Result, String> { debug!("TMDB search movie: {title}"); if let Some(movie_id) = tmdb_id { - return self.fetch_movie_details(*movie_id).await; + return self.fetch_movie_details(movie_id).await; } sleep(Duration::from_millis(self.rate_limit_ms)).await; @@ -62,7 +62,7 @@ impl TmdbClient { } } - fn build_movie_search_url(&self, title: &str, year: Option<&u32>) -> Result { + fn build_movie_search_url(&self, title: &str, year: Option) -> Result { let mut url = Url::parse(&format!("{TMDB_API_BASE_URL}/search/movie")).map_err(|e| format!("Failed to parse URL for TMDB movie search: {e}"))?; { let mut q = url.query_pairs_mut(); diff --git a/backend/src/main.rs b/backend/src/main.rs index 170c036c7..7b1d0d5a3 100644 --- a/backend/src/main.rs +++ b/backend/src/main.rs @@ -113,7 +113,7 @@ async fn main() { let mut config_paths = get_file_paths(&args); init_logger( - args.log_level.as_ref(), + args.log_level.as_deref(), config_paths.config_file_path.as_str(), ); diff --git a/backend/src/model/media_properties.rs b/backend/src/model/media_properties.rs index 5d0fe08b4..c5e5b7158 100644 --- a/backend/src/model/media_properties.rs +++ b/backend/src/model/media_properties.rs @@ -159,7 +159,7 @@ impl MediaQuality { parts.join(separator) } - fn from_ffprobe_info_audio(audio: Option<&String>) -> Option<(AudioCodec, AudioChannels)> { + fn from_ffprobe_info_audio(audio: Option<&str>) -> Option<(AudioCodec, AudioChannels)> { // Assuming the first audio stream is the primary one. let audio_info = audio.and_then(|v| serde_json::from_str::>(v).ok())?; @@ -190,7 +190,7 @@ impl MediaQuality { Some((audio_codec, audio_channels)) } - fn from_ffprobe_info_video(video: Option<&String>) -> Option<(VideoResolution, VideoCodec, VideoDynamicRange)> { + fn from_ffprobe_info_video(video: Option<&str>) -> Option<(VideoResolution, VideoCodec, VideoDynamicRange)> { let video_info = video.and_then(|v| serde_json::from_str::>(v).ok())?; // 1. Classify video resolution from width @@ -238,7 +238,7 @@ impl MediaQuality { /// Extracts media quality information from an `ffprobe` info block. /// The `info_block` is expected to be a `serde_json::Value` object. - pub fn from_ffprobe_info(audio: Option<&String>, video: Option<&String>) -> Option { + pub fn from_ffprobe_info(audio: Option<&str>, video: Option<&str>) -> Option { match (Self::from_ffprobe_info_video(video), Self::from_ffprobe_info_audio(audio)) { (Some((resolution, video_codec, dynamic_range)), Some((audio_codec, audio_channels))) => { diff --git a/backend/src/model/xtream.rs b/backend/src/model/xtream.rs index 58b38522b..ef45b4bdd 100644 --- a/backend/src/model/xtream.rs +++ b/backend/src/model/xtream.rs @@ -52,7 +52,7 @@ pub fn xtream_mapping_option_from_target_options(target: &ConfigTarget, target_o reverse_item_types, username: user.username.clone(), password: user.password.clone(), - base_url: resource_url.map(ToString::to_string).clone(), + base_url: resource_url.map(ToString::to_string), } } diff --git a/backend/src/processing/parser/xtream.rs b/backend/src/processing/parser/xtream.rs index 8ce42f3f1..854490bc5 100644 --- a/backend/src/processing/parser/xtream.rs +++ b/backend/src/processing/parser/xtream.rs @@ -55,7 +55,7 @@ fn create_xtream_series_episode_url<'a>(url: &'a str, username: &'a str, passwor } } -pub fn parse_xtream_series_info(prent_uuid: &UUIDType, series_info: &SeriesStreamProperties, group_title: &str, series_name: &str, input: &ConfigInput) -> Option> { +pub fn parse_xtream_series_info(parent_uuid: &UUIDType, series_info: &SeriesStreamProperties, group_title: &str, series_name: &str, input: &ConfigInput) -> Option> { let url = input.url.as_str(); let username = input.username.as_ref().map_or("", |v| v); let password = input.password.as_ref().map_or("", |v| v); @@ -69,7 +69,7 @@ pub fn parse_xtream_series_info(prent_uuid: &UUIDType, series_info: &SeriesStrea id: episode.id.to_string(), uuid: generate_playlist_uuid(&input.name, &episode.id.to_string(), PlaylistItemType::Series, &episode_url), // we use parent_code to track the parent series - parent_code: prent_uuid.to_string(), + parent_code: parent_uuid.to_string(), name: series_name.to_string(), logo: episode.movie_image.clone(), group: group_title.to_string(), @@ -92,7 +92,7 @@ pub fn parse_xtream_series_info(prent_uuid: &UUIDType, series_info: &SeriesStrea #[allow(clippy::too_many_arguments)] pub fn get_xtream_url(xtream_cluster: XtreamCluster, url: &str, username: &str, password: &str, - stream_id: u32, container_extension: Option<&String>, + stream_id: u32, container_extension: Option<&str>, live_stream_use_prefix: bool, live_stream_without_extension: bool) -> String { let url = trim_last_slash(url); let stream_base_url = match xtream_cluster { @@ -102,8 +102,11 @@ pub fn get_xtream_url(xtream_cluster: XtreamCluster, url: &str, format!("{url}/{ctx_path}{username}/{password}/{stream_id}{suffix}") } XtreamCluster::Video => { - let ext = container_extension.as_ref().map_or("mp4", |e| e.as_str()); - format!("{url}/movie/{username}/{password}/{stream_id}.{ext}") + if let Some(extension) = container_extension { + format!("{url}/movie/{username}/{password}/{stream_id}.{extension}") + } else { + format!("{url}/movie/{username}/{password}/{stream_id}") + } } XtreamCluster::Series => format!("{}&action={}&series_id={stream_id}", get_xtream_stream_url_base(url.as_ref(), username, password), crate::model::XC_ACTION_GET_SERIES_INFO) @@ -111,15 +114,13 @@ pub fn get_xtream_url(xtream_cluster: XtreamCluster, url: &str, stream_base_url } -pub fn create_xtream_url(xtream_cluster: XtreamCluster, url: &str, username: &str, password: &str, - stream: &StreamProperties, live_stream_use_prefix: bool, live_stream_without_extension: bool) -> String { - if let Some(direct_source) = stream.get_direct_source() { - direct_source.clone() - } else { - get_xtream_url(xtream_cluster, url, username, password, stream.get_stream_id(), - stream.get_container_extension().as_ref().map(std::string::ToString::to_string).as_ref(), - live_stream_use_prefix, live_stream_without_extension) - } +pub fn create_xtream_url<'a>(xtream_cluster: XtreamCluster, url: &'a str, username: &'a str, password: &'a str, + stream: &'a StreamProperties, live_stream_use_prefix: bool, live_stream_without_extension: bool) -> Cow<'a, str> { + stream.get_direct_source().unwrap_or_else(|| + Cow::Owned(get_xtream_url(xtream_cluster, url, username, password, stream.get_stream_id(), + stream.get_container_extension().as_deref(), + live_stream_use_prefix, live_stream_without_extension)) + ) } pub async fn parse_xtream(input: &ConfigInput, @@ -159,7 +160,7 @@ pub async fn parse_xtream(input: &ConfigInput, logo: stream.get_stream_icon().to_string(), group: category_name.clone(), title: stream.get_name().to_string(), - url: stream_url.clone(), + url: stream_url.to_string(), epg_channel_id: stream.get_epg_channel_id(), item_type, xtream_cluster, diff --git a/backend/src/processing/processor/epg.rs b/backend/src/processing/processor/epg.rs index 0ed45b56d..8da10e7da 100644 --- a/backend/src/processing/processor/epg.rs +++ b/backend/src/processing/processor/epg.rs @@ -64,7 +64,7 @@ impl EpgIdCache<'_> { /// cache.normalize_and_store("Discovery Channel", Some(&"discovery.epg".to_string())); /// assert!(cache.normalized.contains_key(&cache.normalize("Discovery Channel"))); /// ``` - fn normalize_and_store(&mut self, name: &str, epg_id: Option<&String>) { + fn normalize_and_store(&mut self, name: &str, epg_id: Option<&str>) { self.insert_normalized(name); if let Some(chan_epg_id) = epg_id { @@ -124,7 +124,7 @@ impl EpgIdCache<'_> { if needs_normalization { let name = &channel.header.name; - self.normalize_and_store(name, channel.header.epg_channel_id.as_ref()); + self.normalize_and_store(name, channel.header.epg_channel_id.as_deref()); } } } diff --git a/backend/src/processing/processor/playlist.rs b/backend/src/processing/processor/playlist.rs index c6a19641d..4eecd1745 100644 --- a/backend/src/processing/processor/playlist.rs +++ b/backend/src/processing/processor/playlist.rs @@ -110,7 +110,7 @@ fn filter_playlist(playlist: &mut [PlaylistGroup], target: &ConfigTarget) -> Opt fn assign_channel_no_playlist(new_playlist: &mut [PlaylistGroup]) { let assigned_chnos: HashSet = new_playlist.iter().flat_map(|g| &g.channels) - .filter(|c| !c.header.chno == 0) + .filter(|c| c.header.chno != 0) .map(|c| c.header.chno) .collect(); let mut chno = 1; @@ -335,8 +335,8 @@ async fn process_source(client: &reqwest::Client, app_config: Arc, so let (playlistgroups, mut error_list) = { let (downloaded_playlist, mut download_err) = playlist_download_from_input(client, &app_config, input).await; let (playlist, error) = persist_input_playlist(&app_config, input, downloaded_playlist).await; - error!("Failed to persist input playlist {}", input.name); if let Some(err) = error { + error!("Failed to persist input playlist {}", input.name); download_err.push(err); } (playlist, download_err) diff --git a/backend/src/processing/processor/xtream_series.rs b/backend/src/processing/processor/xtream_series.rs index edf04768a..39933bfd8 100644 --- a/backend/src/processing/processor/xtream_series.rs +++ b/backend/src/processing/processor/xtream_series.rs @@ -89,102 +89,6 @@ async fn playlist_resolve_series_info(app_config: &Arc, client: &reqw info!("resolved {processed_series_info_count}/{series_info_count} series info"); result } -// -// async fn process_series_info( -// app_config: &AppConfig, -// fpl: &mut FetchedPlaylist<'_>, -// errors: &mut Vec, -// ) -> Vec { -// let mut result: Vec = vec![]; -// let input = fpl.input; -// let config = app_config.config.load(); -// let Ok(Some((info_path, idx_path))) = get_input_storage_path(&input.name, &config.working_dir) -// .map(|storage_path| xtream_get_info_file_paths(&storage_path, XtreamCluster::Series)) -// else { -// errors.push(notify_err!("Failed to open input info file for series".to_string())); -// return result; -// }; -// -// let mut write_counter = 0usize; -// -// let _file_lock = app_config.file_locks.read_lock(&info_path).await; -// -// // Contains the Series Info with episode listing -// let Ok(mut info_reader) = IndexedDocumentReader::::new(&info_path, &idx_path) else { return result; }; -// -// let Some((mut wal_file, wal_path)) = create_resolve_episode_wal_files(&config, input).await else { -// errors.push(notify_err!("Could not create wal file for series episodes record".to_string())); -// return result; -// }; -// let mut wal_writer = utils::async_file_writer(&mut wal_file); -// -// for plg in fpl -// .playlistgroups -// .iter_mut() -// .filter(|plg| plg.xtream_cluster == XtreamCluster::Series) -// { -// let mut group_series = vec![]; -// -// // Resolve does not handle LocalSeriesInfo -// for pli in plg -// .channels -// .iter_mut() -// .filter(|pli| pli.header.item_type == PlaylistItemType::SeriesInfo) -// { -// 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) = { -// let header = &pli.header; -// (header.group.clone(), if header.name.is_empty() { header.title.clone() } else { header.name.clone() }) -// }; -// if let Some(mut series) = parse_xtream_series_info(&series_content, &group, &series_name, input) { -// for pli_episode in &mut series { -// let Some(provider_id) = &pli_episode.header.get_provider_id() else { continue; }; -// match write_series_episode_record_to_wal_file(&mut wal_writer, *provider_id, pli.header.additional_properties.as_ref()).await { -// Ok(written_bytes) => { -// write_counter += written_bytes; -// // periodic flush to bound BufWriter memory -// if write_counter >= IO_BUFFER_SIZE { -// write_counter = 0; -// if let Err(err) = wal_writer.flush().await { -// errors.push(notify_err!(format!("Failed periodic flush of wal content writer {err}"))); -// } -// } -// } -// Err(err) => { errors.push(info_err!(format!("Failed to write to series episode wal file: {err}"))) } -// } -// } -// group_series.extend(series.into_iter()); -// } -// } -// Err(err) => errors.push(info_err!(format!("Failed to parse JSON: {err}"))), -// } -// } -// if !group_series.is_empty() { -// result.push(PlaylistGroup { -// id: plg.id, -// title: plg.title.clone(), -// channels: group_series, -// xtream_cluster: XtreamCluster::Series, -// }); -// } -// } -// -// handle_error!(wal_writer.flush().await, |err| errors.push(notify_err!(format!("Failed to resolve series episodes, could not write to wal file {err}")))); -// handle_error!(wal_writer.get_ref().sync_all().await, |err| errors.push(notify_err!(format!("Failed to sync series info to wal file {err}")))); -// -// drop(wal_writer); -// drop(wal_file); -// handle_error!(xtream_update_input_series_episodes_record_from_wal_file(app_config, input, &wal_path).await, -// |err| errors.push(err)); -// result -// } pub async fn playlist_resolve_series(cfg: &Arc, client: &reqwest::Client, diff --git a/backend/src/processing/processor/xtream_vod.rs b/backend/src/processing/processor/xtream_vod.rs index 4d4c3fa64..7b987a0ef 100644 --- a/backend/src/processing/processor/xtream_vod.rs +++ b/backend/src/processing/processor/xtream_vod.rs @@ -1,15 +1,15 @@ -use std::sync::Arc; use crate::model::FetchedPlaylist; use crate::model::{AppConfig, ConfigTarget}; -use crate::processing::processor::xtream::{playlist_resolve_download_playlist_item}; -use crate::processing::processor::{create_resolve_options_function_for_xtream_target}; -use crate::repository::xtream_repository::{persists_input_vod_info}; +use crate::processing::processor::xtream::playlist_resolve_download_playlist_item; +use crate::processing::processor::create_resolve_options_function_for_xtream_target; +use crate::repository::storage::get_input_storage_path; +use crate::repository::xtream_repository::persists_input_vod_info; use log::{error, info, log_enabled, Level}; use shared::error::TuliproxError; use shared::model::{InputType, PlaylistEntry, StreamProperties, VideoStreamProperties, XtreamVideoInfo}; use shared::model::{PlaylistItemType, XtreamCluster}; +use std::sync::Arc; use std::time::Instant; -use crate::repository::storage::get_input_storage_path; create_resolve_options_function_for_xtream_target!(vod); @@ -52,7 +52,9 @@ pub async fn playlist_resolve_vod(app_config: &Arc, client: &reqwest: if let Some(content) = playlist_resolve_download_playlist_item(client, pli, fpl.input, errors, resolve_delay, XtreamCluster::Video).await { if let Ok(info) = serde_json::from_str::(&content) { let video_stream_props = VideoStreamProperties::from_info(&info, pli); - let _ = persists_input_vod_info(app_config, &storage_path, pli.header.xtream_cluster, &input.name, provider_id, &video_stream_props).await; + if let Err(err) = persists_input_vod_info(app_config, &storage_path, pli.header.xtream_cluster, &input.name, provider_id, &video_stream_props).await { + error!("Failed to persist VOD info for provider_id {provider_id}: {err}"); + } // This makes the data available for subsequent processing steps like STRM export. pli.header.additional_properties = Some(StreamProperties::Video(Box::new(video_stream_props))); } diff --git a/backend/src/ptt/parser.rs b/backend/src/ptt/parser.rs index b425a9e13..961b70c1c 100644 --- a/backend/src/ptt/parser.rs +++ b/backend/src/ptt/parser.rs @@ -1,3 +1,4 @@ +use std::cmp::min; use std::collections::HashMap; use fancy_regex::{Regex as FancyRegex, Match}; use crate::ptt::constants::{BRACKETS, PTT_CONSTANTS}; @@ -150,7 +151,7 @@ impl PttParser { && match_index < end_of_title && end_of_title >= raw_len { - end_of_title -= raw_len; + end_of_title -= min(raw_len, end_of_title - match_index); } } } @@ -161,6 +162,7 @@ impl PttParser { result.seasons.dedup(); result.episodes.sort_unstable(); result.episodes.dedup(); + result.languages.sort_unstable(); result.languages.dedup(); let final_title = if end_of_title <= context.title.len() { diff --git a/backend/src/ptt/transformers.rs b/backend/src/ptt/transformers.rs index 99a84a46e..ddb6cee27 100644 --- a/backend/src/ptt/transformers.rs +++ b/backend/src/ptt/transformers.rs @@ -79,20 +79,6 @@ macro_rules! range_func { range_func!(range_i32, i32); range_func!(range_u32, u32); - -// -// pub fn range_x_of_y_func(input: &str) -> Option> { -// let numbers: Vec = PTT_CONSTANTS.integer -// .find_iter(input) -// .filter_map(|mat| mat.as_str().parse().ok()) -// .collect(); -// -// if numbers.len() != 1 { -// return None; -// } -// Some((1..=numbers[0]).collect()) -// } - pub fn transform_resolution(input: &str) -> String { let lower = input.to_lowercase(); if lower.contains("2160") || lower.contains("4k") { diff --git a/backend/src/repository/bplustree.rs b/backend/src/repository/bplustree.rs index 0756a90cd..738aecf06 100644 --- a/backend/src/repository/bplustree.rs +++ b/backend/src/repository/bplustree.rs @@ -13,7 +13,7 @@ use tempfile::NamedTempFile; use crate::utils::{binary_deserialize, binary_serialize}; const BLOCK_SIZE: usize = 4096; -const BINCODE_OVERHEAD: usize = 8; +const ENCODE_OVERHEAD: usize = 10*8; // for bincode we used 8 const LEN_SIZE: usize = 4; const FLAG_SIZE: usize = 1; @@ -464,14 +464,6 @@ fn decode_content(content_bytes: &[u8]) -> Option> { return Some(result); } } - - // TODO remove at next deployment, this is only fallback for older compressed files - let mut decoder = flate2::write::ZlibDecoder::new(Vec::with_capacity(content_bytes.len())); - if let Ok(()) = decoder.write_all(content_bytes) { - if let Ok(decoded) = decoder.finish() { - return Some(decoded); - } - } None } @@ -484,7 +476,7 @@ pub struct BPlusTree { } const fn calc_order() -> (usize, usize) { - let overhead_size = BINCODE_OVERHEAD + LEN_SIZE + FLAG_SIZE; + let overhead_size = ENCODE_OVERHEAD + LEN_SIZE + FLAG_SIZE; let key_size = size_of::() + overhead_size; let value_size = key_size + size_of::() + overhead_size; let inner_order = BLOCK_SIZE / key_size; diff --git a/backend/src/repository/playlist_scratch.rs b/backend/src/repository/playlist_scratch.rs index 3e6386cd6..c401a905c 100644 --- a/backend/src/repository/playlist_scratch.rs +++ b/backend/src/repository/playlist_scratch.rs @@ -48,7 +48,9 @@ impl PlaylistScratch { series: C::with_capacity(capacity), } } +} +impl PlaylistScratch { pub fn get_mut(&mut self, cluster: XtreamCluster) -> &mut C { match cluster { XtreamCluster::Live => &mut self.live, @@ -85,7 +87,6 @@ impl PlaylistScratch { } impl PlaylistScratch { - pub fn is_empty(&self, cluster: XtreamCluster) -> bool { match cluster { XtreamCluster::Live => self.live.is_empty(), diff --git a/backend/src/repository/strm_repository.rs b/backend/src/repository/strm_repository.rs index 933ad3af7..529a7eff3 100644 --- a/backend/src/repository/strm_repository.rs +++ b/backend/src/repository/strm_repository.rs @@ -199,7 +199,7 @@ fn extract_item_info(pli: &mut PlaylistItem) -> StrmItemInfo { input_name, url, series_name, - release_date, + release_date: release_date.as_deref().map(ToString::to_string), season, episode, added: added.as_ref().map_or_else(|| Some(0), |a| a.parse::().ok()), @@ -673,9 +673,9 @@ fn get_quality(strm_target_output: &StrmTargetOutput, pli: &PlaylistItem, separa StreamProperties::Live(_) | StreamProperties::Series(_) => (None, None), StreamProperties::Video(video) => - video.details.as_ref().map_or_else(|| (None, None), |d| (d.audio.as_ref(), d.video.as_ref())), + video.details.as_ref().map_or_else(|| (None, None), |d| (d.audio.as_deref(), d.video.as_deref())), StreamProperties::Episode(episode) => - (episode.audio.as_ref(), episode.video.as_ref()) + (episode.audio.as_deref(), episode.video.as_deref()) } } }; @@ -711,7 +711,7 @@ pub async fn write_strm_playlist( ))); }; - let user_and_server_info = get_credentials_and_server_info(app_config, target_output.username.as_ref()); + let user_and_server_info = get_credentials_and_server_info(app_config, target_output.username.as_deref()); let normalized_dir = normalize_string_path(&target_output.directory); let strm_file_prefix = hash_string_as_hex(&normalized_dir); let strm_index_path = @@ -895,7 +895,7 @@ async fn has_strm_file_same_hash(file_path: &PathBuf, content_hash: UUIDType) -> fn get_credentials_and_server_info( cfg: &AppConfig, - username: Option<&String>, + username: Option<&str>, ) -> Option<(ProxyUserCredentials, ApiProxyServerInfo)> { let username = username?; let credentials = cfg.get_user_credentials(username)?; diff --git a/backend/src/repository/xtream_playlist_iterator.rs b/backend/src/repository/xtream_playlist_iterator.rs index ea806fb51..f90165cd0 100644 --- a/backend/src/repository/xtream_playlist_iterator.rs +++ b/backend/src/repository/xtream_playlist_iterator.rs @@ -71,6 +71,7 @@ impl XtreamPlaylistIterator { } fn matches_filters(cluster: XtreamCluster, filter_ids: Option<&HashSet>, item: &XtreamPlaylistItem) -> bool { + // We can't serve episodes within series if cluster == XtreamCluster::Series && !matches!(item.item_type, PlaylistItemType::SeriesInfo | PlaylistItemType::LocalSeriesInfo) { return false; diff --git a/backend/src/repository/xtream_repository.rs b/backend/src/repository/xtream_repository.rs index 9b7c122ee..4ace3122b 100644 --- a/backend/src/repository/xtream_repository.rs +++ b/backend/src/repository/xtream_repository.rs @@ -50,13 +50,6 @@ macro_rules! try_option_ok { } }; } -// -// #[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Default)] -// pub struct InputVodInfoRecord { -// pub(crate) tmdb_id: u32, -// pub(crate) ts: u64, -// pub(crate) release_date: Option, -// } #[inline] fn get_collection_path(path: &Path, collection: &str) -> PathBuf { @@ -190,7 +183,7 @@ pub async fn write_playlist_item_to_file( let (xtream_path, idx_path) = xtream_get_file_paths(&storage_path, pli.xtream_cluster); { let _file_lock = app_config.file_locks.write_lock(&xtream_path).await; - match IndexedDocumentWriter::new(xtream_path.clone(), idx_path) { + match IndexedDocumentWriter::new_append(xtream_path.clone(), idx_path) { Ok(mut writer) => { match writer.write_doc(pli.virtual_id, pli) { Ok(()) => {} @@ -1008,7 +1001,7 @@ where { let _file_lock = app_config.file_locks.write_lock(&xtream_path).await; let mut writer = IndexedDocumentWriter::new_append(xtream_path.clone(), idx_path)?; - writer.write_doc(provider_id, &props).map_err(|_| str_to_io_error(&format!("failed to write vod info for input {input_name}")))?; + writer.write_doc(provider_id, &props).map_err(|_| str_to_io_error(&format!("failed to write {cluster} info for input {input_name}")))?; writer.store()?; } } diff --git a/backend/src/tools/lru_cache.rs b/backend/src/tools/lru_cache.rs index 14f2dfaca..62c30f114 100644 --- a/backend/src/tools/lru_cache.rs +++ b/backend/src/tools/lru_cache.rs @@ -38,11 +38,13 @@ impl LRUResourceCache { /// - `cache_dir`: The directory path where cached files are stored. /// pub fn new(capacity: usize, cache_dir: &str) -> Self { + // Estimate: assume average file size of 256KB + let estimated_entries = (capacity / (256 * 1024)).clamp(64, 16384); Self { capacity, cache_dir: PathBuf::from(cache_dir), current_size: 0, - cache: HashMap::, usize)>::with_capacity(4096), + cache: HashMap::, usize)>::with_capacity(estimated_entries), usage_order: VecDeque::new(), } } @@ -97,11 +99,6 @@ impl LRUResourceCache { /// - The `PathBuf` where the file is stored. pub fn add_content(&mut self, url: &str, mime_type: Option, file_size: usize) -> std::io::Result { let key = encode_cache_key(url); - // let (key, mime_type) = if let Some((part1, part2)) = file_name.split_once(".") { - // (part1.to_string(), String::from_utf8(decode_base64_string(part2)).ok()) - // } else { - // (key, None) - // }; let path = self.insert_to_cache(key, mime_type, file_size); if self.current_size > self.capacity { self.evict_if_needed(); @@ -147,7 +144,9 @@ impl LRUResourceCache { if path.exists() { trace_if_enabled!("Responding resource from cache with key: {key} for url: {}", sanitize_sensitive_info(url)); // Move to the end of the queue - self.usage_order.retain(|k| k != &key); // remove from queue + if let Some(pos) = self.usage_order.iter().position(|k| k == &key) { + self.usage_order.remove(pos); // remove from queue + } self.usage_order.push_back(key); // add to the to end return Some((path.clone(), mime_type.clone())); } @@ -156,7 +155,9 @@ impl LRUResourceCache { // this should not happen, someone deleted the file manually and the cache is not in sync self.current_size -= size; self.cache.remove(&key); - self.usage_order.retain(|k| k != &key); + if let Some(pos) = self.usage_order.iter().position(|k| k == &key) { + self.usage_order.remove(pos); + } } } } diff --git a/backend/src/utils/logging.rs b/backend/src/utils/logging.rs index f9237a5f9..70eadf8ce 100644 --- a/backend/src/utils/logging.rs +++ b/backend/src/utils/logging.rs @@ -23,7 +23,7 @@ fn get_log_level(log_level: &str) -> LevelFilter { } } -pub fn init_logger(user_log_level: Option<&String>, config_file: &str) { +pub fn init_logger(user_log_level: Option<&str>, config_file: &str) { // tracing_subscriber::registry() diff --git a/backend/src/utils/network/xtream.rs b/backend/src/utils/network/xtream.rs index 2649bb31f..aa7c87605 100644 --- a/backend/src/utils/network/xtream.rs +++ b/backend/src/utils/network/xtream.rs @@ -13,7 +13,7 @@ use crate::utils::request; use chrono::{DateTime, Utc}; use log::{error, info, warn}; use shared::error::{str_to_io_error, to_io_error, TuliproxError}; -use shared::model::{MsgKind, PlaylistEntry, PlaylistGroup, ProxyUserStatus, SeriesStreamProperties, VideoStreamProperties, XtreamCluster, XtreamPlaylistItem, XtreamSeriesInfo, XtreamVideoInfo}; +use shared::model::{MsgKind, PlaylistEntry, PlaylistGroup, ProxyUserStatus, SeriesStreamProperties, StreamProperties, VideoStreamProperties, XtreamCluster, XtreamPlaylistItem, XtreamSeriesInfo, XtreamVideoInfo}; use shared::utils::{extract_extension_from_url, get_i64_from_serde_value, get_string_from_serde_value, sanitize_sensitive_info}; use std::io::Error; use std::str::FromStr; @@ -57,7 +57,7 @@ pub async fn get_xtream_stream_info_content(client: &reqwest::Client, input: &In } } -#[allow(clippy::too_many_arguments)] +#[allow(clippy::too_many_arguments, clippy::too_many_lines)] pub async fn get_xtream_stream_info(client: &reqwest::Client, app_state: &Arc, user: &ProxyUserCredentials, @@ -90,12 +90,14 @@ pub async fn get_xtream_stream_info(client: &reqwest::Client, error!("Failed to persist video stream for input {}: {err}", &input.name); } - if let Err(err) = write_playlist_item_to_file(app_config, &target.name, pli).await { + let mut vod_pli = pli.clone(); + vod_pli.additional_properties = Some(StreamProperties::Video(Box::new(video_stream_props))); + if let Err(err) = write_playlist_item_to_file(app_config, &target.name, &vod_pli).await { error!("Failed to persist video stream: {err}"); } if target.use_memory_cache { - app_state.playlists.update_playlist_items(target, vec![pli]).await; + app_state.playlists.update_playlist_items(target, vec![&vod_pli]).await; } } } @@ -108,7 +110,9 @@ pub async fn get_xtream_stream_info(client: &reqwest::Client, if let Ok(storage_path) = get_input_storage_path(&input.name, working_dir) { if let Ok(info) = serde_json::from_str::(&content) { let series_stream_props = SeriesStreamProperties::from_info(&info, pli); - let _ = persists_input_series_info(app_config, &storage_path, cluster, &input.name, provider_id, &series_stream_props).await; + if let Err(err) = persists_input_series_info(app_config, &storage_path, cluster, &input.name, provider_id, &series_stream_props).await { + error!("Failed to persist series info for input {}: {err}", &input.name); + } if let Some(mut episodes) = parse_xtream_series_info(&pli.get_uuid(), &series_stream_props, &group, &series_name, input) { let config = &app_state.app_config.config.load(); match get_target_storage_path(config, target.name.as_str()) { @@ -148,6 +152,7 @@ pub async fn get_xtream_stream_info(client: &reqwest::Client, if !provider_series.is_empty() { let mut series_pli = pli.clone(); + series_pli.additional_properties = Some(StreamProperties::Series(Box::new(series_stream_props))); rewrite_provider_series_info_episode_virtual_id(&mut series_pli, &provider_series); if let Err(err) = write_playlist_item_to_file(app_config, &target.name, &series_pli).await { error!("Failed to persist series stream: {err}"); diff --git a/shared/src/model/config/cache.rs b/shared/src/model/config/cache.rs index 113ec2600..c27da6ec7 100644 --- a/shared/src/model/config/cache.rs +++ b/shared/src/model/config/cache.rs @@ -18,7 +18,7 @@ pub struct CacheConfigDto { impl CacheConfigDto { pub fn is_empty(&self) -> bool { - !self.enabled && is_blank_optional_string(self.size.as_ref()) && is_blank_optional_string(self.dir.as_ref()) + !self.enabled && is_blank_optional_string(self.size.as_deref()) && is_blank_optional_string(self.dir.as_deref()) } pub(crate) fn prepare(&mut self, working_dir: &str) -> Result<(), TuliproxError> { diff --git a/shared/src/model/config/input.rs b/shared/src/model/config/input.rs index bac23fa4d..59c9ddc1d 100644 --- a/shared/src/model/config/input.rs +++ b/shared/src/model/config/input.rs @@ -355,7 +355,7 @@ impl ConfigInputDto { } } - self.persist = get_trimmed_string(self.persist.as_ref()); + self.persist = get_trimmed_string(self.persist.as_deref()); let mut current_index = index + 1; self.id = current_index; diff --git a/shared/src/model/config/ipcheck.rs b/shared/src/model/config/ipcheck.rs index 35d1a2abc..837c74615 100644 --- a/shared/src/model/config/ipcheck.rs +++ b/shared/src/model/config/ipcheck.rs @@ -28,11 +28,11 @@ pub struct IpCheckConfigDto { impl IpCheckConfigDto { pub fn is_empty(&self) -> bool { - is_blank_optional_string(self.url.as_ref()) - && is_blank_optional_string(self.url_ipv4.as_ref()) - && is_blank_optional_string(self.url_ipv6.as_ref()) - && is_blank_optional_string(self.pattern_ipv4.as_ref()) - && is_blank_optional_string(self.pattern_ipv6.as_ref()) + is_blank_optional_string(self.url.as_deref()) + && is_blank_optional_string(self.url_ipv4.as_deref()) + && is_blank_optional_string(self.url_ipv6.as_deref()) + && is_blank_optional_string(self.pattern_ipv4.as_deref()) + && is_blank_optional_string(self.pattern_ipv6.as_deref()) } pub fn clean(&mut self) { diff --git a/shared/src/model/config/log.rs b/shared/src/model/config/log.rs index 4a06e43af..45902110f 100644 --- a/shared/src/model/config/log.rs +++ b/shared/src/model/config/log.rs @@ -23,11 +23,11 @@ impl Default for LogConfigDto { impl LogConfigDto { pub fn is_empty(&self) -> bool { - self.sanitize_sensitive_info && !self.log_active_user && is_blank_optional_string(self.log_level.as_ref()) + self.sanitize_sensitive_info && !self.log_active_user && is_blank_optional_string(self.log_level.as_deref()) } pub fn clean(&mut self) { - if is_blank_optional_string(self.log_level.as_ref()) { + if is_blank_optional_string(self.log_level.as_deref()) { self.log_level = None; } } diff --git a/shared/src/model/config/macros.rs b/shared/src/model/config/macros.rs index c31f7a4fe..aa7d35bbf 100644 --- a/shared/src/model/config/macros.rs +++ b/shared/src/model/config/macros.rs @@ -8,8 +8,8 @@ macro_rules! check_input_credentials { return Err(info_err!("url for input is mandatory".to_string())); } - $this.username = $crate::utils::get_trimmed_string($this.username.as_ref()); - $this.password = $crate::utils::get_trimmed_string($this.password.as_ref()); + $this.username = $crate::utils::get_trimmed_string($this.username.as_deref()); + $this.password = $crate::utils::get_trimmed_string($this.password.as_deref()); } match $input_type { InputType::M3u => { diff --git a/shared/src/model/config/messaging.rs b/shared/src/model/config/messaging.rs index af401bda4..5f62221e9 100644 --- a/shared/src/model/config/messaging.rs +++ b/shared/src/model/config/messaging.rs @@ -38,7 +38,7 @@ pub struct PushoverMessagingConfigDto { impl PushoverMessagingConfigDto { pub fn is_empty(&self) -> bool { - is_blank_optional_string(self.url.as_ref()) + is_blank_optional_string(self.url.as_deref()) && self.token.trim().is_empty() && self.user.trim().is_empty() } diff --git a/shared/src/model/config/proxy.rs b/shared/src/model/config/proxy.rs index 2e1c37e69..d49b91280 100644 --- a/shared/src/model/config/proxy.rs +++ b/shared/src/model/config/proxy.rs @@ -11,16 +11,16 @@ pub struct ProxyConfigDto { impl ProxyConfigDto { pub fn is_empty(&self) -> bool { - is_blank_optional_string(self.username.as_ref()) - && is_blank_optional_string(self.password.as_ref()) + is_blank_optional_string(self.username.as_deref()) + && is_blank_optional_string(self.password.as_deref()) && self.url.trim().is_empty() } pub fn clean(&mut self) { - if is_blank_optional_string(self.username.as_ref()) { + if is_blank_optional_string(self.username.as_deref()) { self.username = None; } - if is_blank_optional_string(self.password.as_ref()) { + if is_blank_optional_string(self.password.as_deref()) { self.password = None; } } diff --git a/shared/src/model/config/video_download.rs b/shared/src/model/config/video_download.rs index a15fbee6b..fbdd87fad 100644 --- a/shared/src/model/config/video_download.rs +++ b/shared/src/model/config/video_download.rs @@ -24,8 +24,8 @@ impl VideoDownloadConfigDto { pub fn is_empty(&self) -> bool { !self.organize_into_directories && self.headers.is_empty() - && is_blank_optional_string(self.directory.as_ref()) - && is_blank_optional_string(self.episode_pattern.as_ref()) + && is_blank_optional_string(self.directory.as_deref()) + && is_blank_optional_string(self.episode_pattern.as_deref()) } } @@ -43,7 +43,7 @@ pub struct VideoConfigDto { impl VideoConfigDto { pub fn is_empty(&self) -> bool { - self.extensions.is_empty() && is_blank_optional_string(self.web_search.as_ref()) + self.extensions.is_empty() && is_blank_optional_string(self.web_search.as_deref()) && (self.download.is_none() || self.download.as_ref().is_some_and(|d| d.is_empty())) } diff --git a/shared/src/model/config/web_auth.rs b/shared/src/model/config/web_auth.rs index 6b7e0d8ae..49bdb8188 100644 --- a/shared/src/model/config/web_auth.rs +++ b/shared/src/model/config/web_auth.rs @@ -36,6 +36,6 @@ impl WebAuthConfigDto { && self.token_ttl_mins == empty.token_ttl_mins && self.issuer.trim().is_empty() && self.secret.trim().is_empty() - && is_blank_optional_string(self.userfile.as_ref()) + && is_blank_optional_string(self.userfile.as_deref()) } } diff --git a/shared/src/model/config/web_ui.rs b/shared/src/model/config/web_ui.rs index b92d11d3d..3cd9a8a0a 100644 --- a/shared/src/model/config/web_ui.rs +++ b/shared/src/model/config/web_ui.rs @@ -105,8 +105,8 @@ impl WebUiConfigDto { let empty = WebUiConfigDto::default(); self.enabled == empty.enabled && self.user_ui_enabled == empty.user_ui_enabled - && is_blank_optional_string(self.path.as_ref()) - && is_blank_optional_string(self.player_server.as_ref()) + && is_blank_optional_string(self.path.as_deref()) + && is_blank_optional_string(self.player_server.as_deref()) && self.kick_secs == default_kick_secs() && (self.content_security_policy.is_none() || self @@ -128,10 +128,10 @@ impl WebUiConfigDto { self.auth = None; } - if is_blank_optional_string(self.path.as_ref()) { + if is_blank_optional_string(self.path.as_deref()) { self.path = None; } - if is_blank_optional_string(self.player_server.as_ref()) { + if is_blank_optional_string(self.player_server.as_deref()) { self.player_server = None; } self.kick_secs = default_kick_secs(); diff --git a/shared/src/model/info_doc_utils.rs b/shared/src/model/info_doc_utils.rs index b66dd3f36..448360339 100644 --- a/shared/src/model/info_doc_utils.rs +++ b/shared/src/model/info_doc_utils.rs @@ -12,7 +12,7 @@ impl InfoDocUtils { } } - pub fn make_bdpath_resource_url(resource_url: Option<&String>, bd_path: &str, index: usize, field_prefix: &str) -> String { + pub fn make_bdpath_resource_url(resource_url: Option<&str>, bd_path: &str, index: usize, field_prefix: &str) -> String { if let Some(url) = resource_url { if bd_path.starts_with("http") { return format!("{url}/{field_prefix}{}_{index}", xtream_const::XC_PROP_BACKDROP_PATH); @@ -22,27 +22,23 @@ impl InfoDocUtils { } pub fn limited(n: f64) -> String { - if n > 0.01 { - let s = n.to_string(); - if s.len() > 4 { - s[..4].to_string() - } else { - s - } - } else { + if n < 0.01 { "0".to_string() + } else { + let s = format!("{:.2}", n); + s.strip_suffix(".00").unwrap_or(&s).to_string() } } - pub fn as_string(value: Option<&String>) -> Value { - Value::String(value.map_or_else(String::new, |v| v.clone())) + pub fn build_string(value: Option<&str>) -> Value { + Value::String(value.map_or_else(String::new, String::from)) } pub fn empty_string() -> Value { Value::String(String::new()) } - pub fn as_value(value: Option<&String>) -> Value { + pub fn build_value(value: Option<&str>) -> Value { if let Some(text) = value { if let Ok(result) = serde_json::from_str(text) { return result; @@ -51,11 +47,11 @@ impl InfoDocUtils { Value::Array(Vec::new()) } - pub fn as_u32(value: u32) -> Value { + pub fn build_u32(value: u32) -> Value { Value::Number(serde_json::Number::from(value)) } - pub fn make_resource_url(resource_url: Option<&String>, value: &str, field: &str) -> String { + pub fn make_resource_url(resource_url: Option<&str>, value: &str, field: &str) -> String { if let Some(url) = resource_url { if value.starts_with("http") { return format!("{url}/{field}"); diff --git a/shared/src/model/playlist.rs b/shared/src/model/playlist.rs index b870496df..dd1b034c4 100644 --- a/shared/src/model/playlist.rs +++ b/shared/src/model/playlist.rs @@ -72,7 +72,7 @@ impl UUIDType { bytes[16..].copy_from_slice(&hash.as_bytes()[..16]); bytes } else { - // falback + // fallback *blake3::hash(uuid.as_bytes()).as_bytes() }; @@ -281,7 +281,7 @@ pub trait PlaylistEntry: Send + Sync { fn get_virtual_id(&self) -> VirtualId; fn get_provider_id(&self) -> Option; fn get_category_id(&self) -> Option; - fn get_provider_url(&self) -> String; + fn get_provider_url(&self) -> Cow<'_, str>; fn get_uuid(&self) -> UUIDType; fn get_item_type(&self) -> PlaylistItemType; fn get_group(&self) -> Cow<'_, str>; @@ -336,7 +336,7 @@ impl PlaylistItemHeader { } } - pub fn get_container_extension(&self) -> Option { + pub fn get_container_extension(&self) -> Option> { self.additional_properties.as_ref().and_then(|a| a.get_container_extension()) } } @@ -511,8 +511,8 @@ impl PlaylistEntry for M3uPlaylistItem { None } #[inline] - fn get_provider_url(&self) -> String { - self.url.to_string() + fn get_provider_url(&self) -> Cow<'_, str> { + Cow::Borrowed(self.url.as_str()) } fn get_uuid(&self) -> UUIDType { @@ -532,9 +532,9 @@ impl PlaylistEntry for M3uPlaylistItem { #[inline] fn get_name(&self) -> Cow<'_, str> { if self.title.is_empty() { - Cow::Borrowed(self.title.as_str()) - } else { Cow::Borrowed(self.name.as_str()) + } else { + Cow::Borrowed(self.title.as_str()) } } @@ -659,19 +659,20 @@ impl XtreamPlaylistItem { } } - pub fn get_container_extension(&self) -> Option { + pub fn get_container_extension(&self) -> Option> { match self.additional_properties { None => None, Some(ref props) => { match props { - StreamProperties::Live(_) => Some("ts".to_string()), - StreamProperties::Video(video) => Some(video.container_extension.clone()), + StreamProperties::Live(_) => Some(Cow::Borrowed("ts")), + StreamProperties::Video(video) => Some(Cow::Borrowed(video.container_extension.as_str())), StreamProperties::Series(_) => None, - StreamProperties::Episode(episode) => Some(episode.container_extension.clone()), + StreamProperties::Episode(episode) => Some(Cow::Borrowed(episode.container_extension.as_str())), } } } } + #[inline] pub fn has_details(&self) -> bool { self.additional_properties.as_ref().is_some_and(|p| p.has_details()) @@ -692,33 +693,33 @@ impl XtreamPlaylistItem { match self.xtream_cluster { XtreamCluster::Live => { let stream_icon = if !self.logo.is_empty() { - InfoDocUtils::make_resource_url(resource_url.as_ref(), &self.logo, "logo") + InfoDocUtils::make_resource_url(resource_url.as_deref(), &self.logo, "logo") } else if !self.logo_small.is_empty() { - InfoDocUtils::make_resource_url(resource_url.as_ref(), &self.logo_small, "logo_small") + InfoDocUtils::make_resource_url(resource_url.as_deref(), &self.logo_small, "logo_small") } else { String::new() }; Value::Object(serde_json::Map::from_iter([ - ("num".to_string(), InfoDocUtils::as_u32(self.channel_no)), + ("num".to_string(), InfoDocUtils::build_u32(self.channel_no)), ("name".to_string(), Value::String(self.title.clone())), // name or title ? - ("stream_id".to_string(), InfoDocUtils::as_u32(self.virtual_id)), + ("stream_id".to_string(), InfoDocUtils::build_u32(self.virtual_id)), ("stream_icon".to_string(), Value::String(stream_icon)), - ("epg_channel_id".to_string(), InfoDocUtils::as_string(self.epg_channel_id.as_ref())), + ("epg_channel_id".to_string(), InfoDocUtils::build_string(self.epg_channel_id.as_deref())), ("added".to_string(), InfoDocUtils::empty_string()), - ("is_adult".to_string(), InfoDocUtils::as_u32(0u32)), + ("is_adult".to_string(), InfoDocUtils::build_u32(0u32)), ("category_id".to_string(), Value::String(self.category_id.to_string())), ("category_ids".to_string(), Value::Array(Vec::from([Value::Number(serde_json::Number::from(self.category_id))]))), ("custom_sid".to_string(), Value::Null), ("direct_source".to_string(), InfoDocUtils::empty_string()), - ("tv_archive".to_string(), InfoDocUtils::as_u32(0u32)), - ("tv_archive_duration".to_string(), InfoDocUtils::as_u32(0u32)), + ("tv_archive".to_string(), InfoDocUtils::build_u32(0u32)), + ("tv_archive_duration".to_string(), InfoDocUtils::build_u32(0u32)), ])) } XtreamCluster::Video => { let stream_icon = if !self.logo.is_empty() { - InfoDocUtils::make_resource_url(resource_url.as_ref(), &self.logo, "logo") + InfoDocUtils::make_resource_url(resource_url.as_deref(), &self.logo, "logo") } else if !self.logo_small.is_empty() { - InfoDocUtils::make_resource_url(resource_url.as_ref(), &self.logo_small, "logo_small") + InfoDocUtils::make_resource_url(resource_url.as_deref(), &self.logo_small, "logo_small") } else { String::new() }; @@ -743,9 +744,9 @@ impl XtreamPlaylistItem { } XtreamCluster::Series => { let stream_icon = if !self.logo.is_empty() { - InfoDocUtils::make_resource_url(resource_url.as_ref(), &self.logo, "logo") + InfoDocUtils::make_resource_url(resource_url.as_deref(), &self.logo, "logo") } else if !self.logo_small.is_empty() { - InfoDocUtils::make_resource_url(resource_url.as_ref(), &self.logo_small, "logo_small") + InfoDocUtils::make_resource_url(resource_url.as_deref(), &self.logo_small, "logo_small") } else { String::new() }; @@ -802,26 +803,26 @@ impl XtreamPlaylistItem { ("num".to_string(), Value::Number(serde_json::Number::from(self.channel_no))), ("name".to_string(), Value::String(self.title.clone())), ("series_id".to_string(), Value::Number(serde_json::Number::from(self.virtual_id))), - ("cover".to_string(), Value::String(InfoDocUtils::make_resource_url(resource_url.as_ref(), &series.cover, "cover"))), - ("plot".to_string(), InfoDocUtils::as_string(series.plot.as_ref())), + ("cover".to_string(), Value::String(InfoDocUtils::make_resource_url(resource_url.as_deref(), &series.cover, "cover"))), + ("plot".to_string(), InfoDocUtils::build_string(series.plot.as_deref())), ("cast".to_string(), Value::String(series.cast.clone())), ("director".to_string(), Value::String(series.director.clone())), - ("genre".to_string(), InfoDocUtils::as_string(series.genre.as_ref())), - ("release_date".to_string(), InfoDocUtils::as_string(series.release_date.as_ref())), - ("releaseDate".to_string(), InfoDocUtils::as_string(series.release_date.as_ref())), - ("last_modified".to_string(), InfoDocUtils::as_string(series.last_modified.as_ref())), + ("genre".to_string(), InfoDocUtils::build_string(series.genre.as_deref())), + ("release_date".to_string(), InfoDocUtils::build_string(series.release_date.as_deref())), + ("releaseDate".to_string(), InfoDocUtils::build_string(series.release_date.as_deref())), + ("last_modified".to_string(), InfoDocUtils::build_string(series.last_modified.as_deref())), ("rating".to_string(), Value::String(InfoDocUtils::limited(series.rating))), ("rating_5based".to_string(), Value::String(InfoDocUtils::limited(series.rating_5based))), ("backdrop_path".to_string(), Value::Array( series.backdrop_path.as_ref().map_or_else(Vec::new, |b| b.iter().enumerate().map(|(idx, p)| Value::String( - InfoDocUtils::make_bdpath_resource_url(resource_url.as_ref(), p, idx, "") + InfoDocUtils::make_bdpath_resource_url(resource_url.as_deref(), p, idx, "") ) ).collect()) )), ("youtube_trailer".to_string(), Value::String(series.youtube_trailer.clone())), ("tmdb".to_string(), Value::String(series.tmdb.as_ref().map_or_else(String::new, ToString::to_string))), - ("episode_runtime".to_string(), InfoDocUtils::as_string(series.episode_run_time.as_ref())), + ("episode_runtime".to_string(), InfoDocUtils::build_string(series.episode_run_time.as_deref())), ("category_id".to_string(), Value::String(self.category_id.to_string())), ("category_ids".to_string(), Value::Array(Vec::from([Value::Number(serde_json::Number::from(self.category_id))]))), ])) @@ -830,9 +831,9 @@ impl XtreamPlaylistItem { fn video_to_document(&self, options: &XtreamMappingOptions, video: &VideoStreamProperties) -> Value { let resource_url = options.get_resource_url(self.xtream_cluster, self.item_type, self.virtual_id); let stream_icon = if !self.logo.is_empty() { - InfoDocUtils::make_resource_url(resource_url.as_ref(), &self.logo, "logo") + InfoDocUtils::make_resource_url(resource_url.as_deref(), &self.logo, "logo") } else if !self.logo_small.is_empty() { - InfoDocUtils::make_resource_url(resource_url.as_ref(), &self.logo_small, "logo_small") + InfoDocUtils::make_resource_url(resource_url.as_deref(), &self.logo_small, "logo_small") } else { String::new() }; @@ -845,7 +846,7 @@ impl XtreamPlaylistItem { ("rating".to_string(), Value::String(InfoDocUtils::limited(video.rating.unwrap_or_default()))), ("rating_5based".to_string(), Value::Number(serde_json::Number::from_f64(video.rating_5based.unwrap_or_default()).unwrap_or_else(|| serde_json::Number::from(0)))), ("tmdb".to_string(), Value::String(video.tmdb.as_ref().map_or_else(String::new, ToString::to_string))), - ("trailer".to_string(), InfoDocUtils::as_string(video.trailer.as_ref())), + ("trailer".to_string(), InfoDocUtils::build_string(video.trailer.as_deref())), ("added".to_string(), Value::String(video.added.clone())), ("is_adult".to_string(), Value::Number(serde_json::Number::from(video.is_adult))), ("category_id".to_string(), Value::String(self.category_id.to_string())), @@ -859,20 +860,20 @@ impl XtreamPlaylistItem { fn live_to_document(&self, options: &XtreamMappingOptions, live: &LiveStreamProperties) -> Value { let resource_url = options.get_resource_url(self.xtream_cluster, self.item_type, self.virtual_id); let stream_icon = if !self.logo.is_empty() { - InfoDocUtils::make_resource_url(resource_url.as_ref(), &self.logo, "logo") + InfoDocUtils::make_resource_url(resource_url.as_deref(), &self.logo, "logo") } else if !self.logo_small.is_empty() { - InfoDocUtils::make_resource_url(resource_url.as_ref(), &self.logo_small, "logo_small") + InfoDocUtils::make_resource_url(resource_url.as_deref(), &self.logo_small, "logo_small") } else { String::new() }; Value::Object(serde_json::Map::from_iter([ ("num".to_string(), Value::Number(serde_json::Number::from(self.channel_no))), ("name".to_string(), Value::String(self.title.clone())), // name or title ? - ("added".to_string(), InfoDocUtils::as_string(live.added.as_ref())), + ("added".to_string(), InfoDocUtils::build_string(live.added.as_deref())), ("stream_id".to_string(), Value::Number(serde_json::Number::from(self.virtual_id))), ("stream_icon".to_string(), Value::String(stream_icon)), ("stream_type".to_string(), Value::String(live.stream_type.as_ref().map_or_else(|| "live".to_string(), Clone::clone))), - ("epg_channel_id".to_string(), InfoDocUtils::as_string(self.epg_channel_id.as_ref())), + ("epg_channel_id".to_string(), InfoDocUtils::build_string(self.epg_channel_id.as_deref())), ("added".to_string(), InfoDocUtils::empty_string()), ("is_adult".to_string(), Value::Number(serde_json::Number::from(live.is_adult))), ("category_id".to_string(), Value::String(self.category_id.to_string())), @@ -888,9 +889,9 @@ impl XtreamPlaylistItem { match self.xtream_cluster { XtreamCluster::Live => { let stream_icon = if !self.logo.is_empty() { - InfoDocUtils::make_resource_url(resource_url.as_ref(), &self.logo, "logo") + InfoDocUtils::make_resource_url(resource_url.as_deref(), &self.logo, "logo") } else if !self.logo_small.is_empty() { - InfoDocUtils::make_resource_url(resource_url.as_ref(), &self.logo_small, "logo_small") + InfoDocUtils::make_resource_url(resource_url.as_deref(), &self.logo_small, "logo_small") } else { String::new() }; @@ -899,7 +900,7 @@ impl XtreamPlaylistItem { ("name".to_string(), Value::String(self.title.clone())), // name or title ? ("stream_id".to_string(), Value::Number(serde_json::Number::from(self.virtual_id))), ("stream_icon".to_string(), Value::String(stream_icon)), - ("epg_channel_id".to_string(), InfoDocUtils::as_string(self.epg_channel_id.as_ref())), + ("epg_channel_id".to_string(), InfoDocUtils::build_string(self.epg_channel_id.as_deref())), ("added".to_string(), InfoDocUtils::empty_string()), ("is_adult".to_string(), Value::Number(serde_json::Number::from(0u32))), ("category_id".to_string(), Value::String(self.category_id.to_string())), @@ -912,9 +913,9 @@ impl XtreamPlaylistItem { } XtreamCluster::Video => { let stream_icon = if !self.logo.is_empty() { - InfoDocUtils::make_resource_url(resource_url.as_ref(), &self.logo, "logo") + InfoDocUtils::make_resource_url(resource_url.as_deref(), &self.logo, "logo") } else if !self.logo_small.is_empty() { - InfoDocUtils::make_resource_url(resource_url.as_ref(), &self.logo_small, "logo_small") + InfoDocUtils::make_resource_url(resource_url.as_deref(), &self.logo_small, "logo_small") } else { String::new() }; @@ -939,9 +940,9 @@ impl XtreamPlaylistItem { } XtreamCluster::Series => { let stream_icon = if !self.logo.is_empty() { - InfoDocUtils::make_resource_url(resource_url.as_ref(), &self.logo, "logo") + InfoDocUtils::make_resource_url(resource_url.as_deref(), &self.logo, "logo") } else if !self.logo_small.is_empty() { - InfoDocUtils::make_resource_url(resource_url.as_ref(), &self.logo_small, "logo_small") + InfoDocUtils::make_resource_url(resource_url.as_deref(), &self.logo_small, "logo_small") } else { String::new() }; @@ -987,11 +988,11 @@ impl PlaylistEntry for XtreamPlaylistItem { } #[inline] fn get_category_id(&self) -> Option { - None + Some(self.category_id) } #[inline] - fn get_provider_url(&self) -> String { - self.url.to_string() + fn get_provider_url(&self) -> Cow<'_, str> { + Cow::Borrowed(self.url.as_str()) } #[inline] @@ -1009,9 +1010,9 @@ impl PlaylistEntry for XtreamPlaylistItem { #[inline] fn get_name(&self) -> Cow<'_, str> { if self.title.is_empty() { - Cow::Borrowed(self.title.as_str()) - } else { Cow::Borrowed(self.name.as_str()) + } else { + Cow::Borrowed(self.title.as_str()) } } @@ -1313,13 +1314,14 @@ impl PlaylistEntry for PlaylistItem { #[inline] fn get_category_id(&self) -> Option { - None + Some(self.header.category_id) } #[inline] - fn get_provider_url(&self) -> String { - self.header.url.to_string() + fn get_provider_url(&self) -> Cow<'_, str> { + Cow::Borrowed(self.header.url.as_str()) } + #[inline] fn get_uuid(&self) -> UUIDType { let header = &self.header; @@ -1339,9 +1341,9 @@ impl PlaylistEntry for PlaylistItem { #[inline] fn get_name(&self) -> Cow<'_, str> { if self.header.title.is_empty() { - Cow::Borrowed(self.header.title.as_str()) - } else { Cow::Borrowed(self.header.name.as_str()) + } else { + Cow::Borrowed(self.header.title.as_str()) } } diff --git a/shared/src/model/stream_properties.rs b/shared/src/model/stream_properties.rs index 6eda479fe..fe1691535 100644 --- a/shared/src/model/stream_properties.rs +++ b/shared/src/model/stream_properties.rs @@ -1,13 +1,13 @@ -use crate::model::{ PlaylistEntry, PlaylistItemType, VirtualId, XtreamCluster, XtreamMappingOptions, XtreamSeriesInfo, XtreamVideoInfo}; -use crate::utils::{deserialize_as_option_string, deserialize_number_from_string, - deserialize_number_from_string_or_zero, string_default_on_null, - deserialize_as_string, deserialize_as_string_array, deserialize_json_as_string, +use crate::model::info_doc_utils::InfoDocUtils; +use crate::model::{PlaylistEntry, PlaylistItemType, VirtualId, XtreamCluster, XtreamMappingOptions, XtreamSeriesInfo, XtreamVideoInfo}; +use crate::utils::{deserialize_as_option_string, deserialize_as_string, + deserialize_as_string_array, deserialize_json_as_opt_string, + deserialize_number_from_string, deserialize_number_from_string_or_zero, string_default_on_null, string_or_number_u32}; use serde::{Deserialize, Serialize}; +use serde_json::Value; use std::borrow::Cow; use std::collections::HashMap; -use serde_json::Value; -use crate::model::info_doc_utils::InfoDocUtils; #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] pub struct LiveStreamProperties { @@ -62,9 +62,9 @@ pub struct VideoStreamDetailProperties { pub backdrop_path: Option>, pub duration_secs: Option, pub duration: Option, - #[serde(default, deserialize_with = "deserialize_json_as_string")] + #[serde(default, deserialize_with = "deserialize_json_as_opt_string")] pub video: Option, - #[serde(default, deserialize_with = "deserialize_json_as_string")] + #[serde(default, deserialize_with = "deserialize_json_as_opt_string")] pub audio: Option, #[serde(default)] pub bitrate: u32, @@ -140,9 +140,9 @@ pub struct SeriesStreamDetailEpisodeProperties { pub bitrate: u32, #[serde(default, deserialize_with = "deserialize_number_from_string")] pub rating: Option, - #[serde(default, deserialize_with = "deserialize_json_as_string")] + #[serde(default, deserialize_with = "deserialize_json_as_opt_string")] pub video: Option, - #[serde(default, deserialize_with = "deserialize_json_as_string")] + #[serde(default, deserialize_with = "deserialize_json_as_opt_string")] pub audio: Option, } @@ -205,13 +205,12 @@ pub struct EpisodeStreamProperties { pub movie_image: String, #[serde(default, deserialize_with = "string_default_on_null")] pub container_extension: String, - #[serde(default, deserialize_with = "deserialize_json_as_string")] + #[serde(default, deserialize_with = "deserialize_json_as_opt_string")] pub video: Option, - #[serde(default, deserialize_with = "deserialize_json_as_string")] + #[serde(default, deserialize_with = "deserialize_json_as_opt_string")] pub audio: Option, } - #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] pub enum StreamProperties { Live(LiveStreamProperties), @@ -298,48 +297,44 @@ impl StreamProperties { } } - pub fn get_release_date(&self) -> Option { + pub fn get_release_date(&self) -> Option> { match self { StreamProperties::Live(_) => None, - StreamProperties::Video(video) => video.details.as_ref().and_then(|d| d.release_date.clone()), - StreamProperties::Series(series) => series.release_date.clone(), - StreamProperties::Episode(episode) => episode.release_date.clone(), + StreamProperties::Video(video) => video.details.as_ref().and_then(|d| d.release_date.as_deref().map(Cow::Borrowed)), + StreamProperties::Series(series) => series.release_date.as_deref().map(Cow::Borrowed), + StreamProperties::Episode(episode) => episode.release_date.as_deref().map(Cow::Borrowed), } } - pub fn get_added(&self) -> Option { + pub fn get_added(&self) -> Option> { match self { StreamProperties::Live(_) => None, - StreamProperties::Video(video) => Some(video.added.clone()), + StreamProperties::Video(video) => non_empty_string(video.added.as_str()), StreamProperties::Series(series) => series.details.as_ref() .and_then(|d| d.episodes.as_ref()) - .and_then(|e| e.first().map(|e| e.added.clone())), - StreamProperties::Episode(episode) => episode.added.clone(), + .and_then(|e| e.first().and_then(|e| non_empty_string(e.added.as_str()))), + StreamProperties::Episode(episode) => episode.added.as_deref().map(Cow::Borrowed), } } - pub fn get_container_extension(&self) -> Option { + pub fn get_container_extension(&self) -> Option> { match self { StreamProperties::Live(_) => None, - StreamProperties::Video(video) => if video.container_extension.is_empty() { None } else { Some(video.container_extension.clone()) }, + StreamProperties::Video(video) => non_empty_string(&video.container_extension), StreamProperties::Series(series) => series.details.as_ref() .and_then(|d| d.episodes.as_ref()) - .and_then(|e| e.first().and_then(|e| { - if e.container_extension.is_empty() { None } else { Some(e.container_extension.clone()) } - })), - StreamProperties::Episode(episode) => if episode.container_extension.is_empty() { None } else { Some(episode.container_extension.clone()) }, + .and_then(|e| e.first().and_then(|e| non_empty_string(&e.container_extension))), + StreamProperties::Episode(episode) => non_empty_string(&episode.container_extension), } } - pub fn get_direct_source(&self) -> Option { + pub fn get_direct_source(&self) -> Option> { match self { StreamProperties::Live(_) => None, - StreamProperties::Video(video) => if video.direct_source.is_empty() { None } else { Some(video.direct_source.clone()) }, + StreamProperties::Video(video) => non_empty_string(&video.direct_source), StreamProperties::Series(series) => series.details.as_ref() .and_then(|d| d.episodes.as_ref()) - .and_then(|e| e.first().and_then(|e| { - if e.direct_source.is_empty() { None } else { Some(e.direct_source.clone()) } - })), + .and_then(|e| e.first().and_then(|e| non_empty_string(&e.direct_source))), StreamProperties::Episode(_episode) => None, } } @@ -414,11 +409,11 @@ impl StreamProperties { StreamProperties::Episode(episode) => { Some(Cow::Borrowed(episode.movie_image.as_str())) } - } + }; } if field == "movie_image" { if let StreamProperties::Episode(episode) = self { - return Some(Cow::Borrowed(episode.movie_image.as_str())) + return Some(Cow::Borrowed(episode.movie_image.as_str())); } return None; } @@ -465,6 +460,7 @@ impl StreamProperties { virtual_id: VirtualId, category_id: u32) -> Value { match self { StreamProperties::Live(_live) => { + // Live streams don't expose info documents through the Xtream API. Value::Object(serde_json::Map::new()) } StreamProperties::Video(video) => { @@ -474,6 +470,7 @@ impl StreamProperties { self.series_to_info_document(options, series, item_type, virtual_id, category_id) } StreamProperties::Episode(_episode) => { + // Epsiode streams don't expose info documents through the Xtream API. Value::Object(serde_json::Map::new()) } } @@ -486,31 +483,31 @@ impl StreamProperties { ("seasons".to_string(), Value::Array(Vec::new())), ("info".to_string(), Value::Object(serde_json::Map::from_iter([ ("name".to_string(), Value::String(series.name.clone())), - ("cover".to_string(), Value::String(InfoDocUtils::make_resource_url(resource_url.as_ref(), series.cover.as_ref(), "cover"))), - ("plot".to_string(), InfoDocUtils::as_string(series.plot.as_ref())), + ("cover".to_string(), Value::String(InfoDocUtils::make_resource_url(resource_url.as_deref(), series.cover.as_ref(), "cover"))), + ("plot".to_string(), InfoDocUtils::build_string(series.plot.as_deref())), ("cast".to_string(), Value::String(series.cast.clone())), ("director".to_string(), Value::String(series.director.clone())), - ("genre".to_string(), InfoDocUtils::as_string(series.genre.as_ref())), - ("release_date".to_string(), InfoDocUtils::as_string(series.release_date.as_ref())), - ("releaseDate".to_string(), InfoDocUtils::as_string(series.release_date.as_ref())), - ("last_modified".to_string(), InfoDocUtils::as_string(series.last_modified.as_ref())), + ("genre".to_string(), InfoDocUtils::build_string(series.genre.as_deref())), + ("release_date".to_string(), InfoDocUtils::build_string(series.release_date.as_deref())), + ("releaseDate".to_string(), InfoDocUtils::build_string(series.release_date.as_deref())), + ("last_modified".to_string(), InfoDocUtils::build_string(series.last_modified.as_deref())), ("rating".to_string(), Value::String(InfoDocUtils::limited(series.rating))), ("rating_5based".to_string(), Value::String(InfoDocUtils::limited(series.rating_5based))), ("backdrop_path".to_string(), Value::Array( series.backdrop_path.as_ref().map_or_else(Vec::new, |b| b.iter().enumerate().map(|(idx, p)| Value::String( - InfoDocUtils::make_bdpath_resource_url(resource_url.as_ref(), p, idx, "") + InfoDocUtils::make_bdpath_resource_url(resource_url.as_deref(), p, idx, "") ) ).collect()) )), ("tmdb".to_string(), Value::String(series.tmdb.unwrap_or_default().to_string())), ("youtube_trailer".to_string(), Value::String(series.youtube_trailer.clone())), - ("episode_run_time".to_string(), InfoDocUtils::as_string(series.episode_run_time.as_ref())), + ("episode_run_time".to_string(), InfoDocUtils::build_string(series.episode_run_time.as_deref())), ("category_id".to_string(), Value::String(category_id.to_string())), - ("category_ids".to_string(), Value::Array(Vec::from([InfoDocUtils::as_u32(category_id)]))), + ("category_ids".to_string(), Value::Array(Vec::from([InfoDocUtils::build_u32(category_id)]))), ]))), if let Some(episodes) = series.details.as_ref().and_then(|d| d.episodes.as_ref()) { - ("episodes".to_string(), self.series_episodes_to_info_document(options, resource_url.as_ref(), episodes)) + ("episodes".to_string(), self.series_episodes_to_info_document(options, resource_url.as_deref(), episodes)) } else { ("episodes".to_string(), Value::Object(serde_json::Map::new())) } @@ -520,56 +517,56 @@ impl StreamProperties { fn video_to_info_document(&self, options: &XtreamMappingOptions, video: &VideoStreamProperties, item_type: PlaylistItemType, virtual_id: VirtualId, category_id: u32) -> Value { let resource_url = options.get_resource_url(XtreamCluster::Video, item_type, virtual_id); - let stream_icon = InfoDocUtils::make_resource_url(resource_url.as_ref(), &self.get_stream_icon(), "logo"); + let stream_icon = InfoDocUtils::make_resource_url(resource_url.as_deref(), &self.get_stream_icon(), "logo"); Value::Object(serde_json::Map::from_iter([ if let Some(details) = video.details.as_ref() { ("info".to_string(), Value::Object(serde_json::Map::from_iter([ - ("actors".to_string(), InfoDocUtils::as_string(details.actors.as_ref())), - ("age".to_string(), InfoDocUtils::as_string(details.age.as_ref())), - ("audio".to_string(), InfoDocUtils::as_value(details.audio.as_ref())), - ("video".to_string(), InfoDocUtils::as_value(details.video.as_ref())), + ("actors".to_string(), InfoDocUtils::build_string(details.actors.as_deref())), + ("age".to_string(), InfoDocUtils::build_string(details.age.as_deref())), + ("audio".to_string(), InfoDocUtils::build_value(details.audio.as_deref())), + ("video".to_string(), InfoDocUtils::build_value(details.video.as_deref())), ("backdrop_path".to_string(), Value::Array( - details.backdrop_path.as_ref().map_or_else(Vec::new, |b| b.iter().enumerate().map(|(idx, p)| + details.backdrop_path.as_deref().map_or_else(Vec::new, |b| b.iter().enumerate().map(|(idx, p)| Value::String( - InfoDocUtils::make_bdpath_resource_url(resource_url.as_ref(), p, idx, "nfo_") + InfoDocUtils::make_bdpath_resource_url(resource_url.as_deref(), p, idx, "nfo_") ) ).collect()) )), - ("bitrate".to_string(), InfoDocUtils::as_u32(details.bitrate)), - ("cast".to_string(), InfoDocUtils::as_string(details.cast.as_ref())), - ("country".to_string(), InfoDocUtils::as_string(details.country.as_ref())), - ("cover_big".to_string(), Value::String(InfoDocUtils::make_resource_url(resource_url.as_ref(), &details.cover_big.as_ref().map_or_else(String::new, Clone::clone), "nfo_cover_big"))), - ("description".to_string(), InfoDocUtils::as_string(details.description.as_ref())), - ("director".to_string(), InfoDocUtils::as_string(details.director.as_ref())), - ("duration".to_string(), InfoDocUtils::as_string(details.duration.as_ref())), - ("duration_secs".to_string(), InfoDocUtils::as_string(details.duration_secs.as_ref())), - ("episode_run_time".to_string(), InfoDocUtils::as_u32(details.episode_run_time.unwrap_or_default())), - ("genre".to_string(), InfoDocUtils::as_string(details.genre.as_ref())), - ("kinopoisk_url".to_string(), InfoDocUtils::as_string(details.kinopoisk_url.as_ref())), - ("movie_image".to_string(), Value::String(InfoDocUtils::make_resource_url(resource_url.as_ref(), &details.cover_big.as_ref().map_or_else(String::new, Clone::clone), "nfo_movie_image"))), - ("mpaa_rating".to_string(), InfoDocUtils::as_string(details.mpaa_rating.as_ref())), + ("bitrate".to_string(), InfoDocUtils::build_u32(details.bitrate)), + ("cast".to_string(), InfoDocUtils::build_string(details.cast.as_deref())), + ("country".to_string(), InfoDocUtils::build_string(details.country.as_deref())), + ("cover_big".to_string(), Value::String(InfoDocUtils::make_resource_url(resource_url.as_deref(), &details.cover_big.as_ref().map_or_else(String::new, Clone::clone), "nfo_cover_big"))), + ("description".to_string(), InfoDocUtils::build_string(details.description.as_deref())), + ("director".to_string(), InfoDocUtils::build_string(details.director.as_deref())), + ("duration".to_string(), InfoDocUtils::build_string(details.duration.as_deref())), + ("duration_secs".to_string(), InfoDocUtils::build_string(details.duration_secs.as_deref())), + ("episode_run_time".to_string(), InfoDocUtils::build_u32(details.episode_run_time.unwrap_or_default())), + ("genre".to_string(), InfoDocUtils::build_string(details.genre.as_deref())), + ("kinopoisk_url".to_string(), InfoDocUtils::build_string(details.kinopoisk_url.as_deref())), + ("movie_image".to_string(), Value::String(InfoDocUtils::make_resource_url(resource_url.as_deref(), &details.cover_big.as_ref().map_or_else(String::new, Clone::clone), "nfo_movie_image"))), + ("mpaa_rating".to_string(), InfoDocUtils::build_string(details.mpaa_rating.as_deref())), ("name".to_string(), Value::String(video.name.clone())), - ("o_name".to_string(), InfoDocUtils::as_string(details.o_name.as_ref())), - ("plot".to_string(), InfoDocUtils::as_string(details.plot.as_ref())), + ("o_name".to_string(), InfoDocUtils::build_string(details.o_name.as_deref())), + ("plot".to_string(), InfoDocUtils::build_string(details.plot.as_deref())), ("rating".to_string(), Value::String(InfoDocUtils::limited(video.rating.unwrap_or_default()))), - ("rating_count_kinopoisk".to_string(), InfoDocUtils::as_u32(details.rating_count_kinopoisk)), - ("release_date".to_string(), InfoDocUtils::as_string(details.release_date.as_ref())), - ("runtime".to_string(), InfoDocUtils::as_string(details.runtime.as_ref())), - ("status".to_string(), InfoDocUtils::as_string(details.status.as_ref())), + ("rating_count_kinopoisk".to_string(), InfoDocUtils::build_u32(details.rating_count_kinopoisk)), + ("release_date".to_string(), InfoDocUtils::build_string(details.release_date.as_deref())), + ("runtime".to_string(), InfoDocUtils::build_string(details.runtime.as_deref())), + ("status".to_string(), InfoDocUtils::build_string(details.status.as_deref())), ("tmdb_id".to_string(), Value::String(video.tmdb.unwrap_or_default().to_string())), - ("youtube_trailer".to_string(), InfoDocUtils::as_string(details.youtube_trailer.as_ref())), + ("youtube_trailer".to_string(), InfoDocUtils::build_string(details.youtube_trailer.as_deref())), ]))) } else { ("info".to_string(), Value::Object(serde_json::Map::from_iter([ ("actors".to_string(), InfoDocUtils::empty_string()), ("age".to_string(), InfoDocUtils::empty_string()), - ("audio".to_string(), InfoDocUtils::as_value(None)), - ("video".to_string(), InfoDocUtils::as_value(None)), + ("audio".to_string(), InfoDocUtils::build_value(None)), + ("video".to_string(), InfoDocUtils::build_value(None)), ("backdrop_path".to_string(), Value::Array(Vec::from_iter([ Value::String(stream_icon.clone()), ]))), - ("bitrate".to_string(), InfoDocUtils::as_u32(0)), + ("bitrate".to_string(), InfoDocUtils::build_u32(0)), ("cast".to_string(), InfoDocUtils::empty_string()), ("country".to_string(), InfoDocUtils::empty_string()), ("cover_big".to_string(), Value::String(stream_icon.clone())), @@ -577,7 +574,7 @@ impl StreamProperties { ("director".to_string(), InfoDocUtils::empty_string()), ("duration".to_string(), Value::String("0".to_string())), ("duration_secs".to_string(), Value::String("0".to_string())), - ("episode_run_time".to_string(), InfoDocUtils::as_u32(0)), + ("episode_run_time".to_string(), InfoDocUtils::build_u32(0)), ("genre".to_string(), InfoDocUtils::empty_string()), ("kinopoisk_url".to_string(), InfoDocUtils::empty_string()), ("movie_image".to_string(), Value::String(stream_icon.clone())), @@ -586,7 +583,7 @@ impl StreamProperties { ("o_name".to_string(), Value::String(video.name.clone())), ("plot".to_string(), InfoDocUtils::empty_string()), ("rating".to_string(), Value::String(InfoDocUtils::limited(video.rating.unwrap_or_default()))), - ("rating_count_kinopoisk".to_string(), InfoDocUtils::as_u32(0)), + ("rating_count_kinopoisk".to_string(), InfoDocUtils::build_u32(0)), ("release_date".to_string(), InfoDocUtils::empty_string()), ("runtime".to_string(), Value::String("0".to_string())), ("status".to_string(), Value::String("Released".to_string())), @@ -597,7 +594,7 @@ impl StreamProperties { ("movie_data".to_string(), Value::Object(serde_json::Map::from_iter([ ("added".to_string(), Value::String(video.added.clone())), ("category_id".to_string(), Value::String(category_id.to_string())), - ("category_ids".to_string(), Value::Array(Vec::from([InfoDocUtils::as_u32(category_id)]))), + ("category_ids".to_string(), Value::Array(Vec::from([InfoDocUtils::build_u32(category_id)]))), ("container_extension".to_string(), Value::String(video.container_extension.clone())), ("custom_sid".to_string(), video.custom_sid.as_ref().map_or(Value::Null, |s| Value::String(s.clone()))), ("direct_source".to_string(), if options.skip_video_direct_source { InfoDocUtils::empty_string() } else { Value::String(video.direct_source.clone()) }), @@ -608,29 +605,29 @@ impl StreamProperties { } fn series_episodes_to_info_document(&self, options: &XtreamMappingOptions, - resource_url: Option<&String>, + resource_url: Option<&str>, episodes: &Vec) -> Value { let mut map: HashMap> = HashMap::new(); for ep in episodes { let value = Value::Object(serde_json::Map::from_iter([ - ("id".to_string(), InfoDocUtils::as_u32(ep.id)), - ("episode_num".to_string(), InfoDocUtils::as_u32(ep.episode_num)), + ("id".to_string(), InfoDocUtils::build_u32(ep.id)), + ("episode_num".to_string(), InfoDocUtils::build_u32(ep.episode_num)), ("title".to_string(), Value::String(ep.title.clone())), ("container_extension".to_string(), Value::String(ep.container_extension.clone())), ("info".to_string(), Value::Object(serde_json::Map::from_iter([ - ("id".to_string(), InfoDocUtils::as_u32(ep.tmdb.unwrap_or_default())), + ("id".to_string(), InfoDocUtils::build_u32(ep.tmdb.unwrap_or_default())), ("air_date".to_string(), Value::String(ep.release_date.clone())), ("rating".to_string(), Value::Number(serde_json::Number::from_f64(ep.rating.unwrap_or_default()).unwrap_or_else(|| serde_json::Number::from(0)))), - ("movie_image".to_string(), Value::String(InfoDocUtils::make_resource_url(resource_url, &ep.movie_image, "nfo_movie_image"))), + ("movie_image".to_string(), Value::String(InfoDocUtils::make_resource_url(resource_url, &ep.movie_image, &build_season_episode_field(ep.season, ep.episode_num, "movie_image")))), ("duration".to_string(), Value::String(ep.duration.clone())), - ("duration_secs".to_string(), InfoDocUtils::as_u32(ep.duration_secs)), - ("video".to_string(), InfoDocUtils::as_value(ep.video.as_ref())), - ("audio".to_string(), InfoDocUtils::as_value(ep.audio.as_ref())), - ("bitrate".to_string(), InfoDocUtils::as_u32(ep.bitrate)), + ("duration_secs".to_string(), InfoDocUtils::build_u32(ep.duration_secs)), + ("video".to_string(), InfoDocUtils::build_value(ep.video.as_deref())), + ("audio".to_string(), InfoDocUtils::build_value(ep.audio.as_deref())), + ("bitrate".to_string(), InfoDocUtils::build_u32(ep.bitrate)), ]))), ("custom_sid".to_string(), ep.custom_sid.as_ref().map_or(Value::Null, |s| Value::String(s.clone()))), ("added".to_string(), Value::String(ep.added.clone())), - ("season".to_string(), InfoDocUtils::as_u32(ep.season)), + ("season".to_string(), InfoDocUtils::build_u32(ep.season)), ("direct_source".to_string(), if options.skip_series_direct_source { InfoDocUtils::empty_string() } else { Value::String(ep.direct_source.clone()) }), ])); map.entry(ep.season).or_default().push(value); @@ -647,8 +644,11 @@ impl StreamProperties { .collect::>() ) } +} - +#[inline] +fn build_season_episode_field(season: u32, episode: u32, field: &str) -> String { + format!("nfo_ep_{season}_{episode}_{field}") } fn parse_season_episode_field(s: &str) -> Option<(u32, u32, String)> { @@ -785,14 +785,13 @@ impl SeriesStreamProperties { video: e.info.as_ref().map(|i| i.video.clone()).unwrap_or_default(), audio: e.info.as_ref().map(|i| i.audio.clone()).unwrap_or_default(), } - }).collect()) + }).collect()), }), } } } impl EpisodeStreamProperties { - pub fn from_series(series: &SeriesStreamProperties, episode: &SeriesStreamDetailEpisodeProperties) -> EpisodeStreamProperties { EpisodeStreamProperties { episode_id: episode.id, @@ -808,3 +807,7 @@ impl EpisodeStreamProperties { } } } + +fn non_empty_string(s: &str) -> Option> { + if s.is_empty() { None } else { Some(Cow::Borrowed(s)) } +} \ No newline at end of file diff --git a/shared/src/model/xtream.rs b/shared/src/model/xtream.rs index 16d270bfb..b221cfb37 100644 --- a/shared/src/model/xtream.rs +++ b/shared/src/model/xtream.rs @@ -1,6 +1,6 @@ use crate::utils::{deserialize_as_option_string, deserialize_as_string_array, - deserialize_json_as_string, deserialize_number_from_string, - deserialize_number_from_string_or_zero, deserialize_release_date, + deserialize_json_as_opt_string, deserialize_number_from_string, + deserialize_number_from_string_or_zero, string_default_on_null, string_or_number_u32}; use serde::ser::SerializeMap; use serde::{Deserialize, Deserializer, Serialize, Serializer}; @@ -53,9 +53,9 @@ pub struct XtreamVideoInfoInfo { pub backdrop_path: Option>, pub duration_secs: Option, pub duration: Option, - #[serde(default, deserialize_with = "deserialize_json_as_string")] + #[serde(default, deserialize_with = "deserialize_json_as_opt_string")] pub video: Option, - #[serde(default, deserialize_with = "deserialize_json_as_string")] + #[serde(default, deserialize_with = "deserialize_json_as_opt_string")] pub audio: Option, #[serde(default)] pub bitrate: u32, @@ -107,7 +107,7 @@ pub struct XtreamSeriesInfoInfo { pub director: String, #[serde(default, deserialize_with = "string_default_on_null")] pub genre: String, - #[serde(default, deserialize_with = "deserialize_release_date")] + #[serde(default, alias = "releaseDate", alias = "releasedate", deserialize_with = "string_default_on_null")] pub release_date: String, #[serde(default, deserialize_with = "string_default_on_null")] pub last_modified: String, @@ -144,9 +144,9 @@ pub struct XtreamSeriesInfoEpisodeInfo { pub duration: String, #[serde(default, deserialize_with = "string_default_on_null")] pub movie_image: String, - #[serde(default, deserialize_with = "deserialize_json_as_string")] + #[serde(default, deserialize_with = "deserialize_json_as_opt_string")] pub video: Option, - #[serde(default, deserialize_with = "deserialize_json_as_string")] + #[serde(default, deserialize_with = "deserialize_json_as_opt_string")] pub audio: Option, #[serde(default, deserialize_with = "string_or_number_u32")] pub bitrate: u32, diff --git a/shared/src/utils/request.rs b/shared/src/utils/request.rs index bdf381f26..1b0b2bea0 100644 --- a/shared/src/utils/request.rs +++ b/shared/src/utils/request.rs @@ -79,17 +79,14 @@ pub fn replace_url_extension(url: &str, new_ext: &str) -> String { // Find the last dot in the file name to replace the extension if let Some(dot_pos) = file_name_with_extension.rfind('.') { return format!( - "{}{}.{}{}", - path_part, + "{path_part}{}.{ext}{suffix}", &file_name_with_extension[..dot_pos], // Keep the name part before the dot - ext, // Add the new extension - suffix // Add the query or fragment if any ); } } // If no extension is found, add the new extension to the base URL - format!("{}{}.{}{}", base_url, "", ext, suffix) + format!("{base_url}.{ext}{suffix}") } pub fn get_credentials_from_url(url: &Url) -> (Option, Option) { diff --git a/shared/src/utils/serde_utils.rs b/shared/src/utils/serde_utils.rs index dca47afdc..4307296aa 100644 --- a/shared/src/utils/serde_utils.rs +++ b/shared/src/utils/serde_utils.rs @@ -261,36 +261,10 @@ pub fn parse_timestamp(value: &str) -> Result, ParseError> { Ok(Some(timestamp)) } -pub fn deserialize_json_as_string<'de, D>(deserializer: D) -> Result, D::Error> +pub fn deserialize_json_as_opt_string<'de, D>(deserializer: D) -> Result, D::Error> where D: Deserializer<'de>, { let val: Value = Deserialize::deserialize(deserializer)?; Ok(Some(val.to_string())) } - - -const RELEASE_DATES: [&str; 3] = [ - "release_date", - "releaseDate", - "releasedata", -]; - -pub fn deserialize_release_date<'de, D>(deserializer: D) -> Result -where - D: Deserializer<'de>, -{ - let value = Value::deserialize(deserializer)?; - - for key in RELEASE_DATES { - if let Some(v) = value.get(key) { - if let Some(s) = v.as_str() { - if !s.trim().is_empty() { - return Ok(s.to_string()); - } - } - } - } - - Ok(String::new()) -} \ No newline at end of file diff --git a/shared/src/utils/string_utils.rs b/shared/src/utils/string_utils.rs index 74807bbee..29ce29119 100644 --- a/shared/src/utils/string_utils.rs +++ b/shared/src/utils/string_utils.rs @@ -18,7 +18,7 @@ impl> Capitalize for T { } } -pub fn get_trimmed_string(value: Option<&String>) -> Option { +pub fn get_trimmed_string(value: Option<&str>) -> Option { if let Some(v) = value { let trimmed = v.trim(); if !trimmed.is_empty() { @@ -58,8 +58,8 @@ pub fn get_non_empty_str<'a>(first: &'a str, second: &'a str, third: &'a str) -> } } -pub fn is_blank_optional_string(s: Option<&String>) -> bool { - s.is_none() || s.as_ref().is_some_and(|s| s.trim().is_empty()) +pub fn is_blank_optional_string(s: Option<&str>) -> bool { + s.is_none() || s.is_some_and(|s| s.trim().is_empty()) } pub fn trim_slash(s: &str) -> Cow<'_, str> {