diff --git a/CHANGELOG.md b/CHANGELOG.md index d5e639dd5..535402116 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -98,6 +98,7 @@ hdhomerun: - A new filter field `caption` has been added. This field is used to bypass the `title/name` issue. If `caption` is provided, its value is read from `title` if available, otherwise from `name`. When setting `caption`, both `title` and `name` are updated.” +- Counter has now an attribute padding. Which fills the number like 001. # 2.2.5 (2025-03-27) - fixed web ui playlist regexp search diff --git a/src/model/config.rs b/src/model/config.rs index 11008bd01..56d31c485 100644 --- a/src/model/config.rs +++ b/src/model/config.rs @@ -1,15 +1,15 @@ #![allow(clippy::struct_excessive_bools)] -use bitflags::{bitflags}; +use bitflags::bitflags; use enum_iterator::Sequence; use std::borrow::BorrowMut; use std::collections::{HashMap, HashSet}; -use std::{fmt}; use std::fmt::Display; use std::fs::File; use std::io::BufRead; use std::path::PathBuf; use std::str::FromStr; use std::sync::Arc; +use std::fmt; use tokio::sync::RwLock; use crate::auth::user::UserCredential; @@ -17,8 +17,8 @@ use log::{debug, error, warn}; use path_clean::PathClean; use rand::Rng; use regex::Regex; +use serde::de::{self, Error, SeqAccess, Visitor}; use serde::{Deserialize, Deserializer, Serialize, Serializer}; -use serde::de::{self, Visitor, SeqAccess, Error}; use url::Url; use crate::foundation::filter::{get_filter, prepare_templates, Filter, MockValueProcessor, PatternTemplate, ValueProvider}; @@ -28,7 +28,7 @@ use crate::messaging::MsgKind; use crate::model::api_proxy::{ApiProxyConfig, ApiProxyServerInfo, ProxyUserCredentials}; use crate::model::mapping::Mapping; use crate::model::mapping::Mappings; -use crate::utils::default_utils::{default_as_default, default_as_true, default_resolve_delay_secs, default_grace_period_millis, default_grace_period_timeout_secs, default_connect_timeout_secs}; +use crate::utils::default_utils::{default_as_default, default_as_true, default_connect_timeout_secs, default_grace_period_millis, default_grace_period_timeout_secs, default_resolve_delay_secs}; use crate::utils::file::file_lock_manager::FileLockManager; use crate::utils::file::file_utils; use crate::utils::file::file_utils::file_reader; @@ -68,7 +68,7 @@ macro_rules! valid_property { }}; } pub use valid_property; -use crate::m3u_filter_error::{create_m3u_filter_error_result, handle_m3u_filter_error_result, handle_m3u_filter_error_result_list}; +use crate::m3u_filter_error::{create_m3u_filter_error, create_m3u_filter_error_result, handle_m3u_filter_error_result, handle_m3u_filter_error_result_list}; use crate::model::hdhomerun_config::HdHomeRunConfig; use crate::model::playlist::{PlaylistItemType, XtreamCluster}; use crate::utils::constants::CONSTANTS; @@ -126,7 +126,6 @@ impl TryFrom for HdHomeRunUseTargetType { } } - #[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Sequence, PartialEq, Eq, Default)] pub enum ProcessingOrder { #[serde(rename = "frm")] @@ -245,31 +244,49 @@ impl ProcessTargets { #[serde(deny_unknown_fields)] pub struct ConfigSortGroup { pub order: SortOrder, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub sequence: Option>, + #[serde(default, skip)] + pub t_sequence: Option>, } #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] #[serde(deny_unknown_fields)] pub struct ConfigSortChannel { - pub field: ItemField, // channel field - pub group_pattern: String, + pub field: ItemField, // match against group title + pub group_pattern: String, pub order: SortOrder, #[serde(default, skip_serializing_if = "Option::is_none")] pub sequence: Option>, - #[serde(skip_serializing, skip_deserializing)] - pub re: Option, + #[serde(default, skip)] + pub t_sequence: Option>, + #[serde(skip)] + pub t_re_group_pattern: Option, } impl ConfigSortChannel { pub fn prepare(&mut self) -> Result<(), M3uFilterError> { - match regex::Regex::new(&self.group_pattern) { - Ok(pattern) => { - self.re = Some(pattern); - Ok(()) - } - Err(err) => create_m3u_filter_error_result!(M3uFilterErrorKind::Info, "cant parse regex: {} {err}", &self.group_pattern), - } + // Compile group_pattern + self.t_re_group_pattern = Some( + Regex::new(&self.group_pattern).map_err(|err| { + create_m3u_filter_error!(M3uFilterErrorKind::Info, "cant parse regex: {} {err}", &self.group_pattern) + })? + ); + + // Compile sequence patterns, if any + self.t_sequence = self.sequence.as_ref() + .map(|seq| { + seq.iter() + .map(|s| Regex::new(s).map_err(|err| { + create_m3u_filter_error!(M3uFilterErrorKind::Info, "cant parse regex: {} {err}", s) + })) + .collect::, _>>() + }) + .transpose()?; // convert Option> to Result> + + Ok(()) } } @@ -417,7 +434,7 @@ impl ClusterFlags { fn from_items(items: I) -> Result where - I: IntoIterator, + I: IntoIterator, S: AsRef, { let mut result = ClusterFlags::empty(); @@ -427,7 +444,7 @@ impl ClusterFlags { "live" => result.set(ClusterFlags::Live, true), "vod" => result.set(ClusterFlags::Vod, true), "series" => result.set(ClusterFlags::Series, true), - _ => return Err("Invalid flag {item}, allowed are live, vod, series"), + _ => return Err("Invalid flag {item}, allowed are live, vod, series"), } } @@ -1003,7 +1020,6 @@ pub struct EpgSmartMatchConfig { } impl EpgSmartMatchConfig { - /// Creates a new enabled `EpgSmartMatchConfig` with default settings and prepares it. /// /// Returns an error if preparation fails. @@ -1015,7 +1031,7 @@ impl EpgSmartMatchConfig { /// assert!(config.enabled); /// ``` pub fn new() -> Result { - let mut this = Self { enabled: true, ..Self::default() }; + let mut this = Self { enabled: true, ..Self::default() }; this.prepare()?; Ok(this) } @@ -1027,7 +1043,7 @@ impl EpgSmartMatchConfig { /// Adjusts match thresholds to valid ranges, compiles the normalization regex, and sets default strip values and name prefix separators if not provided. Returns an error if the normalization regex is invalid. /// /// # Returns - /// + /// /// `Ok(())` if preparation succeeds, or an `M3uFilterError` if regex compilation fails. pub fn prepare(&mut self) -> Result<(), M3uFilterError> { if !self.enabled { diff --git a/src/model/mapping.rs b/src/model/mapping.rs index 9dce5c502..b3c619e3b 100644 --- a/src/model/mapping.rs +++ b/src/model/mapping.rs @@ -88,6 +88,8 @@ pub struct MappingCounterDefinition { pub modifier: CounterModifier, #[serde(default)] pub value: u32, + #[serde(default)] + pub padding: u8, } #[derive(Debug, Clone)] @@ -97,6 +99,7 @@ pub struct MappingCounter { pub concat: String, pub modifier: CounterModifier, pub value: Arc, + pub padding: u8, } #[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Sequence, PartialEq, Eq)] @@ -458,6 +461,7 @@ impl Mapping { concat: def.concat.clone(), modifier: def.modifier.clone(), value: Arc::new(AtomicU32::new(def.value)), + padding: def.padding, }); } Err(e) => return Err(info_err!(e.to_string())) diff --git a/src/processing/processor/playlist.rs b/src/processing/processor/playlist.rs index 9f60ed563..66583f8af 100644 --- a/src/processing/processor/playlist.rs +++ b/src/processing/processor/playlist.rs @@ -118,7 +118,7 @@ fn sort_playlist(target: &ConfigTarget, new_playlist: &mut [PlaylistGroup]) { } if let Some(channel_sorts) = &sort.channels { for channel_sort in channel_sorts { - let regexp = channel_sort.re.as_ref().unwrap(); + let regexp = channel_sort.t_re_group_pattern.as_ref().unwrap(); for group in new_playlist.iter_mut() { let group_title = if match_as_ascii { deunicode(&group.title) } else { group.title.to_string() }; if regexp.is_match(group_title.as_str()) { @@ -275,19 +275,23 @@ fn map_playlist_counter(target: &ConfigTarget, playlist: &mut [PlaylistGroup]) { for channel in &mut plg.channels { let provider = ValueProvider { pli: channel }; if counter.filter.filter(&provider, &mut mock_processor) { - let cntval = counter.value.load(core::sync::atomic::Ordering::SeqCst); - let new_value = if counter.modifier == CounterModifier::Assign { + let cntval = counter.value.fetch_add(1, core::sync::atomic::Ordering::SeqCst); + let padded_cntval = if counter.padding > 0 { + format!("{:0width$}", cntval, width = counter.padding as usize) + } else { cntval.to_string() + }; + let new_value = if counter.modifier == CounterModifier::Assign { + padded_cntval } else { let value = channel.header.get_field(&counter.field).map_or_else(String::new, |field_value| field_value.to_string()); if counter.modifier == CounterModifier::Suffix { - format!("{value}{}{cntval}", counter.concat) + format!("{value}{}{padded_cntval}", counter.concat) } else { - format!("{cntval}{}{value}", counter.concat) + format!("{padded_cntval}{}{value}", counter.concat) } }; channel.header.set_field(&counter.field, new_value.as_str()); - counter.value.fetch_add(1, core::sync::atomic::Ordering::SeqCst); } } }