From 3b3dc7d0139750f3dce80acd18d607a0ce29b541 Mon Sep 17 00:00:00 2001 From: euzu Date: Fri, 25 Apr 2025 18:08:40 +0200 Subject: [PATCH 1/3] sorting with regexp --- src/model/config.rs | 60 ++++++++++++++++++---------- src/processing/processor/playlist.rs | 2 +- 2 files changed, 39 insertions(+), 23 deletions(-) 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/processing/processor/playlist.rs b/src/processing/processor/playlist.rs index 9f60ed563..fbea41d93 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()) { From c8ff857f3076dce5f3e064328945890629abbb37 Mon Sep 17 00:00:00 2001 From: euzu Date: Mon, 28 Apr 2025 07:43:13 +0200 Subject: [PATCH 2/3] provider grace period disabled was ignored --- src/api/model/active_provider_manager.rs | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/api/model/active_provider_manager.rs b/src/api/model/active_provider_manager.rs index 8dc20c7f4..eb6a4a64d 100644 --- a/src/api/model/active_provider_manager.rs +++ b/src/api/model/active_provider_manager.rs @@ -83,10 +83,10 @@ impl ProviderLineup { } } - async fn acquire(&self, grace_period_timeout_secs: u64) -> ProviderAllocation { + async fn acquire(&self, with_grace: bool, grace_period_timeout_secs: u64) -> ProviderAllocation { match self { - ProviderLineup::Single(lineup) => lineup.acquire(grace_period_timeout_secs).await, - ProviderLineup::Multi(lineup) => lineup.acquire(grace_period_timeout_secs).await, + ProviderLineup::Single(lineup) => lineup.acquire(with_grace, grace_period_timeout_secs).await, + ProviderLineup::Multi(lineup) => lineup.acquire(with_grace, grace_period_timeout_secs).await, } } @@ -115,8 +115,8 @@ impl SingleProviderLineup { self.provider.get_next(false, grace_period_timeout_secs).await } - async fn acquire(&self, grace_period_timeout_secs: u64) -> ProviderAllocation { - self.provider.try_allocate(true, grace_period_timeout_secs).await + async fn acquire(&self, with_grace: bool, grace_period_timeout_secs: u64) -> ProviderAllocation { + self.provider.try_allocate(with_grace, grace_period_timeout_secs).await } async fn release(&self, provider_name: &str) { @@ -305,7 +305,7 @@ impl MultiProviderLineup { /// ProviderAllocation::GracePeriodprovider) => println!("Provider with grace period {}", provider.name), /// } /// ``` - async fn acquire(&self, grace_period_timeout_secs: u64) -> ProviderAllocation { + async fn acquire(&self, with_grace: bool, grace_period_timeout_secs: u64) -> ProviderAllocation { let main_idx = self.index.load(Ordering::SeqCst); let provider_count = self.providers.len(); @@ -313,7 +313,7 @@ impl MultiProviderLineup { let priority_group = &self.providers[index]; let allocation = { let without_grace_allocation = Self::acquire_next_provider_from_group(priority_group, false, grace_period_timeout_secs).await; - if matches!(without_grace_allocation, ProviderAllocation::Exhausted) { + if with_grace && matches!(without_grace_allocation, ProviderAllocation::Exhausted) { Self::acquire_next_provider_from_group(priority_group, true, grace_period_timeout_secs).await } else { without_grace_allocation @@ -477,7 +477,7 @@ impl ActiveProviderManager { let providers = self.providers.read().await; let allocation = match Self::get_provider_config(input_name, &providers) { None => ProviderAllocation::Exhausted, // No Name matched, we don't have this provider - Some((lineup, _config)) => lineup.acquire(self.grace_period_timeout_secs).await + Some((lineup, _config)) => lineup.acquire(self.grace_period_millis > 0, self.grace_period_timeout_secs).await }; if log_enabled!(log::Level::Debug) { @@ -580,7 +580,7 @@ mod tests { macro_rules! should_available { ($lineup:expr, $provider_id:expr, $grace_period_timeout_secs: expr) => { thread::sleep(std::time::Duration::from_millis(200)); - match $lineup.acquire($grace_period_timeout_secs).await { + match $lineup.acquire(true, $grace_period_timeout_secs).await { ProviderAllocation::Exhausted => assert!(false, "Should available and not exhausted"), ProviderAllocation::Available(provider) => assert_eq!(provider.id, $provider_id), ProviderAllocation::GracePeriod(provider) => assert!(false, "Should available and not grace period: {}", provider.id), @@ -590,7 +590,7 @@ mod tests { macro_rules! should_grace_period { ($lineup:expr, $provider_id:expr, $grace_period_timeout_secs: expr) => { thread::sleep(std::time::Duration::from_millis(200)); - match $lineup.acquire($grace_period_timeout_secs).await { + match $lineup.acquire(true, $grace_period_timeout_secs).await { ProviderAllocation::Exhausted => assert!(false, "Should grace period and not exhausted"), ProviderAllocation::Available(provider) => assert!(false, "Should grace period and not available: {}", provider.id), ProviderAllocation::GracePeriod(provider) => assert_eq!(provider.id, $provider_id), @@ -601,7 +601,7 @@ mod tests { macro_rules! should_exhausted { ($lineup:expr, $grace_period_timeout_secs: expr) => { thread::sleep(std::time::Duration::from_millis(200)); - match $lineup.acquire($grace_period_timeout_secs).await { + match $lineup.acquire(true, $grace_period_timeout_secs).await { ProviderAllocation::Exhausted => {}, ProviderAllocation::Available(provider) => assert!(false, "Should exhausted and not available: {}", provider.id), ProviderAllocation::GracePeriod(provider) => assert!(false, "Should exhausted and not grace period: {}", provider.id), @@ -849,7 +849,7 @@ mod tests { let exhausted = Arc::clone(&exhausted_count); let rt = tokio::runtime::Runtime::new().unwrap(); rt.block_on(async move { - match lineup_clone.acquire(5).await { + match lineup_clone.acquire(true,5).await { ProviderAllocation::Exhausted => exhausted.fetch_sub(1, Ordering::SeqCst), ProviderAllocation::Available(_) => available.fetch_sub(1, Ordering::SeqCst), ProviderAllocation::GracePeriod(_) => grace_period.fetch_sub(1, Ordering::SeqCst), From 6da5f1370f6df134afd04c081d1ac1c748a39a55 Mon Sep 17 00:00:00 2001 From: euzu Date: Mon, 28 Apr 2025 08:30:48 +0200 Subject: [PATCH 3/3] padding for counter --- CHANGELOG.md | 1 + src/model/mapping.rs | 4 ++++ src/processing/processor/playlist.rs | 14 +++++++++----- 3 files changed, 14 insertions(+), 5 deletions(-) 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/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 fbea41d93..66583f8af 100644 --- a/src/processing/processor/playlist.rs +++ b/src/processing/processor/playlist.rs @@ -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); } } }