From 7136eb1ab6e22a43fd60c23bd92b4bafcced16a5 Mon Sep 17 00:00:00 2001 From: euzu Date: Thu, 15 May 2025 11:15:18 +0200 Subject: [PATCH] - Added list template for sequences. List templates can only be used for sequences. ```yaml templates: - name: CHAN_SEQ value: - '(?i)\bUHD\b' - '(?i)\bFHD\b' ``` The template can now be used for sequence ```yaml sort: groups: order: asc channels: - field: caption group_pattern: "!US_TNT_ENTERTAIN!" order: asc sequence: - "!CHAN_SEQ!" - '(?i)\bHD\b' - '(?i)\bSD\b' ``` --- CHANGELOG.md | 24 ++++++ Cargo.lock | 28 +++---- src/api/config_watch.rs | 2 +- src/foundation/filter.rs | 155 ++++++++++++++++++++++++++++++++----- src/model/config_rename.rs | 4 +- src/model/config_sort.rs | 29 +++++-- src/model/mapping.rs | 14 ++-- src/utils/string_utils.rs | 8 +- 8 files changed, 212 insertions(+), 52 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 351b85b10..a96e7efbc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -36,6 +36,30 @@ epg: **Note:** This is a lexicographic sort — so `m_10.yml` comes before `m_2.yml` unless you name files carefully (e.g., `m_01.yml`, `m_02.yml`, ..., `m_10.yml`). - Added `mapping_path` to `config.yml`. +- Added list template for sequences. List templates can only be used for sequences. +```yaml +templates: + - name: CHAN_SEQ + value: + - '(?i)\bUHD\b' + - '(?i)\bFHD\b' +``` + +The template can now be used for sequence +```yaml + sort: + groups: + order: asc + channels: + - field: caption + group_pattern: "!US_TNT_ENTERTAIN!" + order: asc + sequence: + - "!CHAN_SEQ!" + - '(?i)\bHD\b' + - '(?i)\bSD\b' +``` + # 3.0.0 (2025-05-12) - !BREAKING_CHANGE! user has now the attribute `ui_enabled` to disable/enable web_ui for user. You need to migrate the user db if you have used `use_user_db:true`. diff --git a/Cargo.lock b/Cargo.lock index a348f8d8b..f9a2448ff 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -290,9 +290,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.9.0" +version = "2.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd" +checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" [[package]] name = "blake2b_simd" @@ -1370,7 +1370,7 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f37dccff2791ab604f9babef0ba14fbe0be30bd368dc541e2b08d07c8aa908f3" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.9.1", "inotify-sys", "libc", ] @@ -1547,7 +1547,7 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.9.1", "libc", "redox_syscall", ] @@ -1703,7 +1703,7 @@ version = "8.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2fee8403b3d66ac7b26aee6e40a897d85dc5ce26f44da36b8b73e987cc52e943" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.9.1", "filetime", "fsevent-sys", "inotify", @@ -1786,7 +1786,7 @@ version = "0.10.72" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fedfea7d58a1f73118430a55da6a286e7b044961736ce96a16a17068ea25e5da" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.9.1", "cfg-if", "foreign-types", "libc", @@ -2167,7 +2167,7 @@ version = "11.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c6df7ab838ed27997ba19a4664507e6f82b41fe6e20be42929332156e5e85146" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.9.1", ] [[package]] @@ -2196,7 +2196,7 @@ version = "0.5.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "928fca9cf2aa042393a8325b9ead81d2f0df4cb12e1e24cef072922ccd99c5af" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.9.1", ] [[package]] @@ -2372,7 +2372,7 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c71e83d6afe7ff64890ec6b71d6a69bb8a610ab78ce364b3352876bb4c801266" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.9.1", "errno", "libc", "linux-raw-sys", @@ -2474,7 +2474,7 @@ version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.9.1", "core-foundation", "core-foundation-sys", "libc", @@ -2687,7 +2687,7 @@ version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.9.1", "core-foundation", "system-configuration-sys", ] @@ -2910,7 +2910,7 @@ checksum = "0fdb0c213ca27a9f57ab69ddb290fd80d970922355b83ae380b395d3986b8a2e" dependencies = [ "async-compression", "base64 0.22.1", - "bitflags 2.9.0", + "bitflags 2.9.1", "bytes", "futures-core", "futures-util", @@ -3014,7 +3014,7 @@ dependencies = [ "axum", "base64 0.22.1", "bincode", - "bitflags 2.9.0", + "bitflags 2.9.1", "blake3", "bytes", "chrono", @@ -3676,7 +3676,7 @@ version = "0.39.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.9.1", ] [[package]] diff --git a/src/api/config_watch.rs b/src/api/config_watch.rs index fe3dc34f1..7067787c1 100644 --- a/src/api/config_watch.rs +++ b/src/api/config_watch.rs @@ -48,7 +48,7 @@ impl ConfigFile { Ok(()) } pub(crate) fn reload(&self, file_path: &PathBuf, app_state: &Arc) -> Result<(), TuliproxError> { - debug!("File changed {file_path:?}"); + debug!("File change detected {file_path:?}"); match self { ConfigFile::ApiProxy => ConfigFile::load_api_proxy(app_state), ConfigFile::Mapping => ConfigFile::load_mappping(app_state), diff --git a/src/foundation/filter.rs b/src/foundation/filter.rs index ec3c88bd2..702f3c9a3 100644 --- a/src/foundation/filter.rs +++ b/src/foundation/filter.rs @@ -1,7 +1,9 @@ #![allow(clippy::empty_docs)] +use std::cmp::Ordering; use std::collections::HashMap; use enum_iterator::all; +use indexmap::IndexSet; use log::{debug, error, log_enabled, trace, Level}; use pest::iterators::Pair; use pest::Parser; @@ -66,10 +68,17 @@ impl ValueProcessor for MockValueProcessor { } } +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +#[serde(untagged)] +pub enum TemplateValue { + Single(String), + Multi(Vec), +} + #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct PatternTemplate { pub name: String, - pub value: String, + pub value: TemplateValue, } #[derive(Debug, Clone)] @@ -254,7 +263,7 @@ fn get_parser_regexp( let mut parsed_text = String::from(expr.as_str()); parsed_text.pop(); parsed_text.remove(0); - let regstr = apply_templates_to_pattern(&parsed_text, templates); + let regstr = apply_templates_to_pattern_single(&parsed_text, templates)?; let re = regex::Regex::new(regstr.as_str()); if re.is_err() { return create_tuliprox_error_result!(TuliproxErrorKind::Info, "cant parse regex: {}", regstr); @@ -414,7 +423,7 @@ pub fn get_filter( ) -> Result { let empty_list = Vec::with_capacity(0); let template_list: &Vec = templates.unwrap_or(&empty_list); - let source = apply_templates_to_pattern(filter_text, template_list); + let source = apply_templates_to_pattern_single(filter_text, template_list)?; match FilterParser::parse(Rule::main, &source) { Ok(pairs) => { @@ -490,15 +499,21 @@ fn build_dependency_graph( let mut graph = DirectedGraph::::new(); for template in templates { graph.add_node(&template.name); - CONSTANTS.re_template_var - .captures_iter(&template.value) - .filter(|caps| caps.len() > 1) - .filter_map(|caps| caps.get(1)) - .map(|caps| String::from(caps.as_str())) - .for_each(|e| { - graph.add_node(&e); - graph.add_edge(&template.name, &e); - }); + let mut handle_template_value = |value| { + CONSTANTS.re_template_var + .captures_iter(value) + .filter(|caps| caps.len() > 1) + .filter_map(|caps| caps.get(1)) + .map(|caps| String::from(caps.as_str())) + .for_each(|e| { + graph.add_node(&e); + graph.add_edge(&template.name, &e); + }); + }; + match &template.value { + TemplateValue::Single(value) => handle_template_value(value), + TemplateValue::Multi(values) => values.iter().for_each(|value| handle_template_value(value)), + } } let cycles = graph.find_cycles(); for cyclic in &cycles { @@ -520,7 +535,7 @@ pub fn prepare_templates( templates: &Vec, ) -> Result, TuliproxError> { let graph = build_dependency_graph(templates)?; - let mut template_values = HashMap::::new(); + let mut template_values = HashMap::::new(); let mut template_map: HashMap = templates .iter() .map(|item| { @@ -533,11 +548,43 @@ pub fn prepare_templates( if let Some(sorted) = graph.topological_sort() { for template_name in sorted { if let Some(depends_on) = dependencies.get(&template_name) { - let mut templ_value = template_values.get(&template_name).unwrap().to_string(); + let mut templ_value = template_values.get(&template_name).unwrap().clone(); for dep_templ_name in depends_on { - let dep_value = template_values.get(dep_templ_name).ok_or_else(|| TuliproxError::new(TuliproxErrorKind::Info, format!("Failed to load template {dep_templ_name}")))?; - templ_value = - templ_value.replace(format!("!{dep_templ_name}!").as_str(), dep_value); + let dep_value = template_values.get(dep_templ_name).ok_or_else(|| info_err!(format!("Failed to load template {dep_templ_name}")))?; + templ_value = match dep_value { + TemplateValue::Single(dep_val) => { + match templ_value { + TemplateValue::Single(templ_val) => TemplateValue::Single(templ_val.replace(format!("!{dep_templ_name}!").as_str(), dep_val)), + TemplateValue::Multi(templ_vals) => { + let mut new_values = vec![]; + for val in templ_vals { + new_values.push(val.replace(format!("!{dep_templ_name}!").as_str(), dep_val)); + } + TemplateValue::Multi(new_values) + } + } + } + TemplateValue::Multi(dep_vals) => { + match templ_value { + TemplateValue::Single(templ_val) => { + let mut new_values = vec![]; + for dep_val in dep_vals { + new_values.push(templ_val.replace(format!("!{dep_templ_name}!").as_str(), dep_val)); + } + TemplateValue::Multi(new_values) + } + TemplateValue::Multi(templ_vals) => { + let mut new_values = vec![]; + for dep_val in dep_vals { + for templ_val in &templ_vals { + new_values.push(templ_val.replace(format!("!{dep_templ_name}!").as_str(), dep_val)); + } + } + TemplateValue::Multi(new_values) + } + } + } + }; } template_values.insert(template_name.clone(), templ_value); } @@ -552,12 +599,78 @@ pub fn prepare_templates( Ok(template_map.into_values().collect()) } -pub fn apply_templates_to_pattern(pattern: &str, templates: &Vec) -> String { - let mut new_pattern = pattern.to_string(); +pub fn apply_templates_to_pattern( + pattern: &str, + templates: &Vec, + allow_multi: bool, +) -> Result { + let mut new_pattern = TemplateValue::Single(pattern.to_string()); + for template in templates { - new_pattern = new_pattern.replace(format!("!{}!", &template.name).as_str(), &template.value); + let placeholder = format!("!{}!", &template.name); + + match &template.value { + TemplateValue::Single(val) => { + match new_pattern { + TemplateValue::Single(ref mut pat) => { + *pat = pat.replace(&placeholder, val); + } + TemplateValue::Multi(ref mut pats) => { + for pat in pats.iter_mut() { + *pat = pat.replace(&placeholder, val); + } + } + } + } + + TemplateValue::Multi(ref multi_vals) => { + new_pattern = match &new_pattern { + TemplateValue::Single(pat) => { + let mut new_values = IndexSet::new(); + for val in multi_vals { + new_values.insert(pat.replace(&placeholder, val)); + } + TemplateValue::Multi(new_values.into_iter().collect()) + } + TemplateValue::Multi(pats) => { + let mut new_values = IndexSet::new(); + for val in multi_vals { + for pat in pats { + new_values.insert(pat.replace(&placeholder, val)); + } + } + TemplateValue::Multi(new_values.into_iter().collect()) + } + }; + } + } + } + + if !allow_multi { + match &new_pattern { + TemplateValue::Single(_) => {} + TemplateValue::Multi(multi_vals) => { + match multi_vals.len().cmp(&1) { + Ordering::Less => {} + Ordering::Equal => { + new_pattern = TemplateValue::Single(multi_vals.first().unwrap().to_owned()); + } + Ordering::Greater => { + return create_tuliprox_error_result!(TuliproxErrorKind::Info, "Multi value templates are not supported for pattern! {pattern}"); + } + } + } + } + } + + Ok(new_pattern) +} + +pub fn apply_templates_to_pattern_single(pattern: &str, templates: &Vec) -> Result { + match apply_templates_to_pattern(pattern, templates, false)? { + TemplateValue::Single(value) => Ok(value), + TemplateValue::Multi(_) => create_tuliprox_error_result!(TuliproxErrorKind::Info, "Multi value templates are not supported for pattern!"), } - new_pattern } #[cfg(test)] diff --git a/src/model/config_rename.rs b/src/model/config_rename.rs index 2f281d104..08fd97ce4 100644 --- a/src/model/config_rename.rs +++ b/src/model/config_rename.rs @@ -1,4 +1,4 @@ -use crate::foundation::filter::{apply_templates_to_pattern, PatternTemplate}; +use crate::foundation::filter::{apply_templates_to_pattern_single, PatternTemplate}; use crate::tuliprox_error::{TuliproxError, TuliproxErrorKind, create_tuliprox_error_result}; use crate::model::ItemField; @@ -15,7 +15,7 @@ pub struct ConfigRename { impl ConfigRename { pub fn prepare(&mut self, templates: Option<&Vec>) -> Result<(), TuliproxError> { if let Some(templ) = templates { - self.pattern = apply_templates_to_pattern(&self.pattern, templ); + self.pattern = apply_templates_to_pattern_single(&self.pattern, templ)?; } match regex::Regex::new(&self.pattern) { Ok(pattern) => { diff --git a/src/model/config_sort.rs b/src/model/config_sort.rs index 0510a25d4..e7915bc6f 100644 --- a/src/model/config_sort.rs +++ b/src/model/config_sort.rs @@ -1,5 +1,5 @@ use regex::Regex; -use crate::foundation::filter::{apply_templates_to_pattern, PatternTemplate}; +use crate::foundation::filter::{apply_templates_to_pattern, apply_templates_to_pattern_single, PatternTemplate, TemplateValue}; use crate::tuliprox_error::{TuliproxError, TuliproxErrorKind, create_tuliprox_error, handle_tuliprox_error_result_list}; use crate::model::{ItemField}; @@ -35,15 +35,23 @@ pub struct ConfigSortGroup { impl ConfigSortGroup { + pub fn prepare(&mut self, templates: Option<&Vec>) -> Result<(), TuliproxError> { - // Transform sequence with templates if provided, otherwise use raw sequence let processed_sequence = match (&self.sequence, templates) { - (Some(seqs), Some(tmpls)) => Some(seqs.iter().map(|s| apply_templates_to_pattern(s, tmpls)).collect()), + (Some(seqs), Some(tmpls)) => { + let mut result = Vec::new(); + for s in seqs { + match apply_templates_to_pattern(s, tmpls, true)? { + TemplateValue::Single(val) => result.push(val), + TemplateValue::Multi(vals) => result.extend(vals), + } + } + Some(result) + }, (Some(seqs), None) => Some(seqs.clone()), (None, _) => None, }; - // Compile regex patterns self.t_re_sequence = compile_regex_vec(processed_sequence.as_ref())?; Ok(()) } @@ -68,7 +76,7 @@ pub struct ConfigSortChannel { impl ConfigSortChannel { pub fn prepare(&mut self, templates: Option<&Vec>) -> Result<(), TuliproxError> { if let Some(templ) = templates { - self.group_pattern = apply_templates_to_pattern(&self.group_pattern, templ); + self.group_pattern = apply_templates_to_pattern_single(&self.group_pattern, templ)?; } // Compile group_pattern self.t_re_group_pattern = Some( @@ -79,7 +87,16 @@ impl ConfigSortChannel { // Transform sequence with templates if provided, otherwise use raw sequence let processed_sequence = match (&self.sequence, templates) { - (Some(seqs), Some(tmpls)) => Some(seqs.iter().map(|s| apply_templates_to_pattern(s, tmpls)).collect()), + (Some(seqs), Some(tmpls)) => { + let mut result = Vec::new(); + for s in seqs { + match apply_templates_to_pattern(s, tmpls, true)? { + TemplateValue::Single(val) => result.push(val), + TemplateValue::Multi(vals) => result.extend(vals), + } + } + Some(result) + }, (Some(seqs), None) => Some(seqs.clone()), (None, _) => None, }; diff --git a/src/model/mapping.rs b/src/model/mapping.rs index a17250491..713db292d 100644 --- a/src/model/mapping.rs +++ b/src/model/mapping.rs @@ -8,14 +8,14 @@ use std::str::FromStr; use std::sync::atomic::AtomicU32; use std::sync::Arc; -use crate::foundation::filter::{apply_templates_to_pattern, get_filter, prepare_templates, Filter, PatternTemplate, RegexWithCaptures, ValueProcessor}; +use crate::foundation::filter::{apply_templates_to_pattern_single, get_filter, prepare_templates, Filter, PatternTemplate, RegexWithCaptures, ValueProcessor}; +use crate::model::valid_property; +use crate::model::{FieldGetAccessor, FieldSetAccessor, PlaylistItem}; +use crate::model::ItemField; use crate::tuliprox_error::{create_tuliprox_error_result, handle_tuliprox_error_result, info_err}; use crate::tuliprox_error::{TuliproxError, TuliproxErrorKind}; -use crate::model::valid_property; -use crate::model::{ItemField}; -use crate::model::{FieldGetAccessor, FieldSetAccessor, PlaylistItem}; -use crate::utils::CONSTANTS; use crate::utils::Capitalize; +use crate::utils::CONSTANTS; pub const COUNTER_FIELDS: &[&str] = &["name", "title", "caption", "chno"]; @@ -179,7 +179,7 @@ impl MapperTransform { match templates { None => {} Some(template_list) => { - new_pattern = apply_templates_to_pattern(pattern, template_list); + new_pattern = apply_templates_to_pattern_single(pattern, template_list)?; } } match Regex::new(&new_pattern) { @@ -225,7 +225,7 @@ impl Mapper { } if let Some(template_list) = templates { - let new_value = apply_templates_to_pattern(value, template_list); + let new_value = apply_templates_to_pattern_single(value, template_list)?; if new_value != *value { self.attributes.insert(key.clone(), new_value); } diff --git a/src/utils/string_utils.rs b/src/utils/string_utils.rs index 2cc49226a..776da5a59 100644 --- a/src/utils/string_utils.rs +++ b/src/utils/string_utils.rs @@ -65,7 +65,7 @@ pub fn generate_random_string(length: usize) -> String { #[cfg(test)] mod test { use std::collections::HashSet; - use crate::utils::generate_random_string; + use crate::utils::{generate_random_string, Capitalize}; #[test] fn test_generate_random_string() { @@ -75,4 +75,10 @@ mod test { } assert_eq!(strings.len(), 100); } + + #[test] + fn test_capitalize() { + assert_eq!("hELLO".capitalize(), "Hello"); + } + } \ No newline at end of file