diff --git a/CHANGELOG.md b/CHANGELOG.md index 587ab28e4..6a32dc8a9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,13 @@ - more logs sanitized - added session key for session management - added sleep timer `sleep_timer_mins` to config.yml +- added mapper script builtin function `template` to access template definitions. +``` + station_prefix = template(concat("US_", station, "_PREFIX")), +``` +If we assume the variable `station` contains the value `WINK`, +this script receives the template with the concatenated name `US_WINK_PREFIX` which should be defined in `templates` section, +and assigns it to the variable `station_prefix`. # 3.1.3 (2025-06-06) - Fixed xtream codes series info duplicate fields problem. diff --git a/src/foundation/filter.rs b/src/foundation/filter.rs index 4639f90fc..e0b4e410c 100644 --- a/src/foundation/filter.rs +++ b/src/foundation/filter.rs @@ -8,7 +8,6 @@ use pest::iterators::Pair; use pest::Parser; use std::cmp::Ordering; use std::collections::HashMap; - use crate::model::{FieldGetAccessor, FieldSetAccessor, ItemField}; use crate::model::{PlaylistItem, PlaylistItemType}; use crate::tools::directed_graph::DirectedGraph; @@ -286,7 +285,7 @@ fn get_parser_item_field(expr: &Pair) -> Result fn get_parser_regexp( expr: &Pair, - templates: &Vec, + templates: Option<&Vec>, ) -> Result { if expr.as_rule() == Rule::regexp { let mut parsed_text = String::from(expr.as_str()); @@ -311,7 +310,7 @@ fn get_parser_regexp( fn get_parser_field_comparison( expr: Pair, - templates: &Vec, + templates: Option<&Vec>, ) -> Result { let mut expr_inner = expr.into_inner(); match get_parser_item_field(&expr_inner.next().unwrap()) { @@ -372,7 +371,7 @@ macro_rules! handle_expr { fn get_parser_expression( expr: Pair, - templates: &Vec, + templates: Option<&Vec>, errors: &mut Vec, ) -> Result { let mut stmts = Vec::with_capacity(128); @@ -450,9 +449,7 @@ pub fn get_filter( filter_text: &str, templates: Option<&Vec>, ) -> Result { - let empty_list = Vec::with_capacity(0); - let template_list: &Vec = templates.unwrap_or(&empty_list); - let source = apply_templates_to_pattern_single(filter_text, template_list)?; + let source = apply_templates_to_pattern_single(filter_text, templates)?; match FilterParser::parse(Rule::main, &source) { Ok(pairs) => { @@ -465,7 +462,7 @@ pub fn get_filter( for expr in pair.into_inner() { match expr.as_rule() { Rule::expr => { - match get_parser_expression(expr, template_list, &mut errors) { + match get_parser_expression(expr, templates, &mut errors) { Ok(expr) => { match &op { Some(binop) => { @@ -652,59 +649,61 @@ pub fn prepare_templates(templates: &mut Vec) -> Result, + templates_list: Option<&Vec>, allow_multi: bool, ) -> Result { let mut new_pattern = TemplateValue::Single(pattern.to_string()); - for template in templates { - match &template.value { - TemplateValue::Single(val) => { - match new_pattern { - TemplateValue::Single(ref mut pat) => { - let replaced = pat.replace(&template.placeholder, val); - if replaced != *pat { - *pat = replaced; - } - } - TemplateValue::Multi(ref mut pats) => { - for pat in pats.iter_mut() { + if let Some(templates) = templates_list { + for template in templates { + match &template.value { + TemplateValue::Single(val) => { + match new_pattern { + TemplateValue::Single(ref mut pat) => { let replaced = pat.replace(&template.placeholder, val); if replaced != *pat { *pat = replaced; } } - } - } - } - - TemplateValue::Multi(ref multi_vals) => { - new_pattern = match &new_pattern { - TemplateValue::Single(pat) => { - let mut new_values = IndexSet::new(); - for val in multi_vals { - if pat.contains(&template.placeholder) { - new_values.insert(pat.replace(&template.placeholder, val)); - } else { - new_values.insert(pat.clone()); + TemplateValue::Multi(ref mut pats) => { + for pat in pats.iter_mut() { + let replaced = pat.replace(&template.placeholder, val); + if replaced != *pat { + *pat = replaced; + } } } - 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 { + } + + TemplateValue::Multi(ref multi_vals) => { + new_pattern = match &new_pattern { + TemplateValue::Single(pat) => { + let mut new_values = IndexSet::new(); + for val in multi_vals { if pat.contains(&template.placeholder) { new_values.insert(pat.replace(&template.placeholder, val)); } else { new_values.insert(pat.clone()); } } + TemplateValue::Multi(new_values.into_iter().collect()) } - 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 { + if pat.contains(&template.placeholder) { + new_values.insert(pat.replace(&template.placeholder, val)); + } else { + new_values.insert(pat.clone()); + } + } + } + TemplateValue::Multi(new_values.into_iter().collect()) + } + }; + } } } } @@ -731,7 +730,7 @@ pub fn apply_templates_to_pattern( Ok(new_pattern) } -pub fn apply_templates_to_pattern_single(pattern: &str, templates: &Vec) -> Result { +pub fn apply_templates_to_pattern_single(pattern: &str, templates: Option<&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!"), diff --git a/src/foundation/mapper.rs b/src/foundation/mapper.rs index 8c865c13a..1f0ee3a30 100644 --- a/src/foundation/mapper.rs +++ b/src/foundation/mapper.rs @@ -1,6 +1,6 @@ #![allow(clippy::empty_docs)] -use crate::foundation::filter::ValueAccessor; +use crate::foundation::filter::{PatternTemplate, TemplateValue, ValueAccessor}; use crate::foundation::mapper::EvalResult::{AnyValue, Failure, Named, Number, Undefined, Value}; use crate::tuliprox_error::{create_tuliprox_error_result, info_err, TuliproxError, TuliproxErrorKind}; use crate::utils::Capitalize; @@ -35,7 +35,7 @@ block_expr = { "{" ~ statements ~ "}" } condition = { function_call | var_access | field_access } assignment = { (field_access | identifier) ~ "=" ~ expression } expression = { assignment | map_block | match_block | function_call | regex_expr | string_literal | number | var_access | field_access | null | block_expr } -function_name = { "concat" | "uppercase" | "lowercase" | "capitalize" | "trim" | "print" | "number" | "first" } +function_name = { "concat" | "uppercase" | "lowercase" | "capitalize" | "trim" | "print" | "number" | "first" | "template" } function_call = { function_name ~ "(" ~ (expression ~ ("," ~ expression)*)? ~ ")" } any_match = { "_" } match_case_key = { any_match | identifier } @@ -104,6 +104,7 @@ enum BuiltInFunction { Print, ToNumber, First, + Template, } impl FromStr for BuiltInFunction { @@ -119,6 +120,7 @@ impl FromStr for BuiltInFunction { "print" => Ok(Self::Print), "number" => Ok(Self::ToNumber), "first" => Ok(Self::First), + "template" => Ok(Self::Template), _ => create_tuliprox_error_result!(TuliproxErrorKind::Info, "Unknown function {}", s), } } @@ -165,8 +167,8 @@ pub struct MapperScript { } impl MapperScript { - pub fn eval(&self, setter: &mut ValueAccessor) { - let ctx = &mut MapperContext::new(&self.expressions); + pub fn eval(&self, setter: &mut ValueAccessor, templates: Option<&Vec>) { + let ctx = &mut MapperContext::new(&self.expressions, templates); self.eval_with_context(ctx, setter); } @@ -201,8 +203,8 @@ impl Statement { } impl MapperScript { - fn validate(expressions: &Vec, statements: &Vec) -> Result<(), TuliproxError> { - let ctx = &mut MapperContext::new(expressions); + fn validate(expressions: &Vec, statements: &Vec, templates: Option<&Vec>) -> Result<(), TuliproxError> { + let ctx = &mut MapperContext::new(expressions, templates); let mut identifiers: HashSet = HashSet::new(); for stmt in statements { @@ -216,7 +218,7 @@ impl MapperScript { Ok(()) } - pub fn parse(input: &str) -> Result { + pub fn parse(input: &str, templates: Option<&Vec>) -> Result { let mut parsed = MapperParser::parse(Rule::main, input).map_err(|e| info_err!(e.to_string()))?; let program_pair = parsed.next().unwrap(); let mut statements = Vec::new(); @@ -227,7 +229,7 @@ impl MapperScript { } } - MapperScript::validate(&expressions, &statements)?; + MapperScript::validate(&expressions, &statements, templates)?; Ok(Self { expressions, statements }) } fn parse_statement(pair: Pair, expressions: &mut Vec) -> Result, TuliproxError> { @@ -536,13 +538,37 @@ impl MapperScript { pub struct MapperContext<'a> { expressions: &'a Vec, variables: HashMap, + templates: Option>, } impl<'a> MapperContext<'a> { - fn new(expressions: &'a Vec) -> Self { + fn new(expressions: &'a Vec, templates: Option<&'a Vec>) -> Self { Self { expressions, variables: HashMap::new(), + templates: templates.and_then(|vec_templates| { + if vec_templates.is_empty() { + None + } else { + let mut hash_map = HashMap::new(); + for template in vec_templates { + hash_map.insert(template.name.to_string(), template); + } + Some(hash_map) + } + }) + } + } + + fn get_template(&self, name: &str) -> Option<&str> { + match self.templates.as_ref() { + None => None, + Some(templates) => templates.get(name).and_then(|&template| { + match &template.value { + TemplateValue::Single(v) => Some(v.as_str()), + TemplateValue::Multi(_) => None, + } + }) } } @@ -601,6 +627,7 @@ impl<'a> MapperContext<'a> { } match name { BuiltInFunction::ToNumber + | BuiltInFunction::Template | BuiltInFunction::First => { if args.len() > 1 { return create_tuliprox_error_result!(TuliproxErrorKind::Info, "Function accepts only one argument {:?}, {} given", name, args.len()); @@ -995,6 +1022,29 @@ impl Expression { None => Undefined, } } + BuiltInFunction::Template => { + let evaluated_arg = &evaluated_args[0]; + let value = match evaluated_arg { + Value(value) => { + Some(value) + } + Named(values) => { + match values.first() { + None => None, + Some((_key, val)) => Some(val), + } + } + _ => None + }; + if let Some(val) = value { + match ctx.get_template(val) { + Some(v) => Value(v.to_string()), + None => Undefined, + } + } else { + Undefined + } + } } } } diff --git a/src/model/config_rename.rs b/src/model/config_rename.rs index 08fd97ce4..b8f44621c 100644 --- a/src/model/config_rename.rs +++ b/src/model/config_rename.rs @@ -14,9 +14,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_single(&self.pattern, templ)?; - } + self.pattern = apply_templates_to_pattern_single(&self.pattern, templates)?; match regex::Regex::new(&self.pattern) { Ok(pattern) => { self.re = Some(pattern); diff --git a/src/model/config_sort.rs b/src/model/config_sort.rs index 76d4d7c75..49326a1dd 100644 --- a/src/model/config_sort.rs +++ b/src/model/config_sort.rs @@ -41,7 +41,7 @@ impl ConfigSortGroup { (Some(seqs), Some(tmpls)) => { let mut result = Vec::new(); for s in seqs { - match apply_templates_to_pattern(s, tmpls, true)? { + match apply_templates_to_pattern(s, Some(tmpls), true)? { TemplateValue::Single(val) => result.push(val), TemplateValue::Multi(vals) => result.extend(vals), } @@ -75,9 +75,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_single(&self.group_pattern, templ)?; - } + self.group_pattern = apply_templates_to_pattern_single(&self.group_pattern, templates)?; // Compile group_pattern self.t_re_group_pattern = Some( Regex::new(&self.group_pattern).map_err(|err| { @@ -90,7 +88,7 @@ impl ConfigSortChannel { (Some(seqs), Some(tmpls)) => { let mut result = Vec::new(); for s in seqs { - match apply_templates_to_pattern(s, tmpls, true)? { + match apply_templates_to_pattern(s, Some(tmpls), true)? { TemplateValue::Single(val) => result.push(val), TemplateValue::Multi(vals) => result.extend(vals), } diff --git a/src/model/mapping.rs b/src/model/mapping.rs index 53f347898..3ee28d58d 100644 --- a/src/model/mapping.rs +++ b/src/model/mapping.rs @@ -129,8 +129,8 @@ impl MapperOperation { return Err(info_err!(format!("Invalid mapper attribute field {field}"))); } - if let Some(template_list) = templates { - *value = apply_templates_to_pattern_single(value, template_list)?; + if templates.is_some() { + *value = apply_templates_to_pattern_single(value, templates)?; } } } @@ -158,12 +158,13 @@ impl Mapper { Ok(filter) => self.t_filter = Some(filter), Err(err) => return Err(err), } - let script = match templates { - None => self.script.to_string(), - Some(tmpls) => apply_templates_to_pattern_single(&self.script, tmpls)?, + let script = if templates.is_some() { + apply_templates_to_pattern_single(&self.script, templates)? + } else { + self.script.to_string() }; trace!("Mapper script: {script}"); - self.t_script = Some(MapperScript::parse(&script)?); + self.t_script = Some(MapperScript::parse(&script, templates)?); Ok(()) } } @@ -178,10 +179,13 @@ pub struct Mapping { pub counter: Option>, #[serde(skip_serializing, skip_deserializing)] pub t_counter: Option>, + #[serde(skip_serializing, skip_deserializing)] + pub(crate) templates: Option> } impl Mapping { pub fn prepare(&mut self, templates: Option<&Vec>) -> Result<(), TuliproxError> { + self.templates = templates.map(|t| t.iter().map(PatternTemplate::clone).collect::>()); if let Some(mapper_list) = &mut self.mapper { for mapper in mapper_list { mapper.prepare(templates)?; diff --git a/src/processing/processor/playlist.rs b/src/processing/processor/playlist.rs index 52ed43d6d..3349e7766 100644 --- a/src/processing/processor/playlist.rs +++ b/src/processing/processor/playlist.rs @@ -126,13 +126,14 @@ fn map_channel(mut channel: PlaylistItem, mapping: &Mapping) -> PlaylistItem { let channel_name = if mapping.match_as_ascii { deunicode(&header.name) } else { header.name.to_string() }; if mapping.match_as_ascii && log_enabled!(Level::Trace) { trace!("Decoded {} for matching to {}", &header.name, &channel_name); } let ref_chan = &mut channel; + let templates = mapping.templates.as_ref(); for m in mapper { if let Some(script) = m.t_script.as_ref() { let provider = ValueProvider { pli: &ref_chan.clone() }; let mut accessor = ValueAccessor { pli: ref_chan }; if let Some(filter) = &m.t_filter { if filter.filter(&provider) { - script.eval(&mut accessor); + script.eval(&mut accessor, templates); } } }