added transform to mapper

This commit is contained in:
euzu
2024-09-18 19:50:38 +02:00
parent b929038411
commit e240259d55
8 changed files with 203 additions and 19 deletions
+1
View File
@@ -7,6 +7,7 @@
* timeshift.php api implementation
* New Filter `type` added can be uses as `Type = vod` or `Type = live` or `Type = series`
* Counter in `mapping.yml`. Each mapper can have counters to add counter to specific fields.
* Added new mapper feature `transform`. `uppercase`, `lowercase` and `capitalize` supported.
# 2.0.3(2024-07-11)
* added `source` - `input` - `name` attribute to README
+31
View File
@@ -569,6 +569,7 @@ Has the following top level entries:
- `suffix`
- `prefix`
- `assignments`
- `transform`
#### 2.3.3.1 `filter`
The filter is a string with a statement (@see filter statements).
@@ -692,6 +693,36 @@ assignments:
```
This configuration sets `title` property to the value of `name`.
#### 2.3.3.6 `transform`
`transform` is a list of transformations.
Each transformation can have the following attributes:
- `field` _mandatory_ the field where the transformation will be applied
- `modifier` _mandatory_, values are: `lowercase`, `uppercase` and `capitalize`
- `pattern` _optional_ is a regular expression (not filter!) with captures. Only needed when you want to transform parts of the property.
For example: first 3 chars of channel name to lowercase:
```yaml
mapper:
- pattern: 'Group ~ ".*"'
transform:
- field: name
pattern: "^(...)"
modifier: lowercase
```
channel name to uppercase:
```yaml
mapper:
- pattern: 'Group ~ ".*"'
transform:
- field: name
modifier: uppercase
```
### 2.3.4 counter
Each mapping can have a list of counter.
+10 -8
View File
@@ -222,10 +222,7 @@ fn get_parser_regexp(expr: &Pair<Rule>, templates: &Vec<PatternTemplate>) -> Res
let mut parsed_text = String::from(expr.as_str());
parsed_text.pop();
parsed_text.remove(0);
let mut regstr = String::from(parsed_text.as_str());
for t in templates {
regstr = regstr.replace(format!("!{}!", &t.name).as_str(), &t.value);
}
let regstr = apply_templates_to_pattern(&parsed_text, templates);
let re = regex::Regex::new(regstr.as_str());
if re.is_err() {
return create_m3u_filter_error_result!(M3uFilterErrorKind::Info, "cant parse regex: {}", regstr);
@@ -369,10 +366,7 @@ fn get_parser_binary_op(expr: &Pair<Rule>) -> Result<BinaryOperator, M3uFilterEr
pub(crate) fn get_filter(filter_text: &str, templates: Option<&Vec<PatternTemplate>>) -> Result<Filter, M3uFilterError> {
let empty_list = Vec::new();
let template_list: &Vec<PatternTemplate> = templates.unwrap_or(&empty_list);
let mut source = String::from(filter_text);
for t in template_list {
source = source.replace(format!("!{}!", &t.name).as_str(), &t.value);
}
let source = apply_templates_to_pattern(filter_text, template_list);
match FilterParser::parse(Rule::main, &source) {
Ok(pairs) => {
@@ -515,3 +509,11 @@ pub(crate) fn prepare_templates(templates: &Vec<PatternTemplate>) -> Result<Vec<
}
Ok(result)
}
pub(crate) fn apply_templates_to_pattern(pattern: &str, templates: &Vec<PatternTemplate>) -> String {
let mut new_pattern = pattern.to_string();
for template in templates {
new_pattern = new_pattern.replace(format!("!{}!", &template.name).as_str(), &template.value);
}
new_pattern
}
-1
View File
@@ -33,7 +33,6 @@ pub(crate) const MAPPER_ATTRIBUTE_FIELDS: &[&str] = &[
pub(crate) const AFFIX_FIELDS: &[&str] = &["name", "title", "group"];
pub(crate) const COUNTER_FIELDS: &[&str] = &["name", "title", "chno"];
#[macro_export]
+128 -5
View File
@@ -1,21 +1,23 @@
use std::borrow::Cow;
use std::cell::RefCell;
use std::collections::HashMap;
use std::fmt::Display;
use std::rc::Rc;
use std::str::FromStr;
use std::sync::{Arc, Mutex};
use enum_iterator::Sequence;
use enum_iterator::Sequence;
use log::{debug, error};
use regex::Regex;
use crate::{create_m3u_filter_error_result, handle_m3u_filter_error_result, valid_property};
use crate::filter::{Filter, get_filter, PatternTemplate, prepare_templates, RegexWithCaptures, ValueProcessor};
use crate::filter::{apply_templates_to_pattern, Filter, get_filter, PatternTemplate, prepare_templates, RegexWithCaptures, ValueProcessor};
use crate::m3u_filter_error::{M3uFilterError, M3uFilterErrorKind};
use crate::model::config::{AFFIX_FIELDS, COUNTER_FIELDS, ItemField, MAPPER_ATTRIBUTE_FIELDS};
use crate::model::playlist::{FieldAccessor, PlaylistItem};
use crate::utils::default_utils::{default_as_empty_map, default_as_empty_str,
default_as_false, default_as_zero_u32};
use crate::utils::string_utils::Capitalize;
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub(crate) struct MappingTag {
@@ -93,6 +95,80 @@ pub(crate) struct MappingCounter {
pub value: Arc<Mutex<u32>>,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Sequence, PartialEq)]
pub(crate) enum TransformModifier {
#[serde(rename = "lowercase")]
Lowercase,
#[serde(rename = "uppercase")]
Uppercase,
#[serde(rename = "capitalize")]
Capitalize,
}
impl Default for TransformModifier {
fn default() -> Self {
Self::Lowercase
}
}
impl Display for TransformModifier {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", match self {
Self::Lowercase => "lowercase",
Self::Uppercase => "uppercase",
Self::Capitalize => "capitalize"
})
}
}
impl FromStr for TransformModifier {
type Err = M3uFilterError;
fn from_str(s: &str) -> Result<Self, M3uFilterError> {
if s.eq("lowercase") {
Ok(Self::Lowercase)
} else if s.eq("uppercase") {
Ok(Self::Uppercase)
} else if s.eq("capitalize") {
Ok(Self::Capitalize)
} else {
create_m3u_filter_error_result!(M3uFilterErrorKind::Info, "Unknown TransformModifier: {}", s)
}
}
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub(crate) struct MapperTransform {
pub field: String,
pub modifier: TransformModifier,
pub pattern: Option<String>,
#[serde(skip_serializing, skip_deserializing)]
pub t_pattern: Option<Regex>,
}
impl MapperTransform {
pub fn prepare(&mut self, templates: Option<&Vec<PatternTemplate>>) -> Result<(), M3uFilterError> {
match &self.pattern {
None => self.t_pattern = None,
Some(pattern) => {
let mut new_pattern = pattern.to_string();
match templates {
None => {}
Some(template_list) => {
new_pattern = apply_templates_to_pattern(pattern, template_list);
}
}
let re = Regex::new(&*new_pattern);
if re.is_err() {
return create_m3u_filter_error_result!(M3uFilterErrorKind::Info, "cant parse regex: {}", new_pattern);
}
self.t_pattern = Some(re.unwrap());
}
}
Ok(())
}
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub(crate) struct Mapper {
pub filter: Option<String>,
@@ -105,16 +181,17 @@ pub(crate) struct Mapper {
prefix: HashMap<String, String>,
#[serde(default = "default_as_empty_map")]
assignments: HashMap<String, String>,
transform: Option<Vec<MapperTransform>>,
#[serde(skip_serializing, skip_deserializing)]
pub(crate) t_filter: Option<Filter>,
#[serde(skip_serializing, skip_deserializing)]
pub(crate) t_pattern: Option<Filter>,
#[serde(skip_serializing, skip_deserializing)]
pub t_tags: Vec<MappingTag>,
t_tags: Vec<MappingTag>,
#[serde(skip_serializing, skip_deserializing)]
pub t_tagre: Option<Regex>,
t_tagre: Option<Regex>,
#[serde(skip_serializing, skip_deserializing)]
pub t_attre: Option<Regex>,
t_attre: Option<Regex>,
}
impl Mapper {
@@ -142,6 +219,22 @@ impl Mapper {
return Err(M3uFilterError::new(M3uFilterErrorKind::Info, format!("Invalid mapper assignment field {value}")));
}
}
match &mut self.transform {
None => {}
Some(transforms) => {
for t in transforms {
let field = t.field.as_str();
if !valid_property!(field, AFFIX_FIELDS) {
return Err(M3uFilterError::new(M3uFilterErrorKind::Info, format!("Invalid mapper transform field {field}")));
}
if let Err(err) = t.prepare(templates) {
return Err(err);
}
}
}
}
match get_filter(&self.pattern, templates) {
Ok(pattern) => {
self.t_pattern = Some(pattern);
@@ -275,6 +368,35 @@ impl MappingValueProcessor<'_> {
}
}
}
fn apply_transform_modifier(modifier: &TransformModifier, value: &str) -> String {
match modifier {
TransformModifier::Uppercase => value.to_uppercase(),
TransformModifier::Lowercase => value.to_lowercase(),
TransformModifier::Capitalize => value.capitalize(),
}
}
fn apply_transform(&mut self) {
let mapper = self.mapper;
match &mapper.transform {
None => {}
Some(transform_list) => {
for transform in transform_list {
if let Some(prop_value) = self.get_property(&transform.field) {
let value = if let Some(regex) = &transform.t_pattern {
regex.replace_all(&prop_value, |caps: &regex::Captures| {
Self::apply_transform_modifier(&transform.modifier, &caps[0])
})
} else {
Cow::from(Self::apply_transform_modifier(&transform.modifier, prop_value.as_str()))
};
self.set_property(&transform.field, &value);
}
}
}
}
}
}
impl ValueProcessor for MappingValueProcessor<'_> {
@@ -300,6 +422,7 @@ impl ValueProcessor for MappingValueProcessor<'_> {
MappingValueProcessor::<'_>::apply_suffix(self, &captured_values);
MappingValueProcessor::<'_>::apply_prefix(self, &captured_values);
MappingValueProcessor::<'_>::apply_assignments(self);
MappingValueProcessor::<'_>::apply_transform(self);
true
}
}
+9 -3
View File
@@ -71,10 +71,16 @@ pub(crate) fn read_config(config_path: &str, config_file: &str, sources_file: &s
pub(crate) fn read_mapping(mapping_file: &str) -> Result<Option<Mappings>, M3uFilterError> {
let mapping_file = std::path::PathBuf::from(mapping_file);
if let Ok(file) = file_utils::open_file(&mapping_file) {
info!("Mapping file: {}", mapping_file.to_str().unwrap_or("?"));
let mapping: Result<Mappings, _> = serde_yaml::from_reader(file);
if let Ok(mut result) = mapping {
handle_m3u_filter_error_result!(M3uFilterErrorKind::Info, result.prepare());
return Ok(Some(result));
match mapping {
Ok(mut result) => {
handle_m3u_filter_error_result!(M3uFilterErrorKind::Info, result.prepare());
return Ok(Some(result));
},
Err(err) => {
return Err(M3uFilterError::new(M3uFilterErrorKind::Info, err.to_string()));
}
}
}
warn!("cant read mapping file: {}", mapping_file.to_str().unwrap_or("?"));
+22
View File
@@ -12,3 +12,25 @@ pub (crate) fn get_title_group(text: &str) -> String {
}
text.to_string()
}
pub(crate) trait Capitalize {
fn capitalize(&self) -> String;
}
// Implement the Capitalize trait for &str
impl Capitalize for &str {
fn capitalize(&self) -> String {
let mut chars = self.chars();
match chars.next() {
None => String::new(), // Handle empty string case
Some(first_char) => first_char.to_uppercase().collect::<String>() + chars.as_str(),
}
}
}
// Implement the trait for String as well
impl Capitalize for String {
fn capitalize(&self) -> String {
self.as_str().capitalize() // Reuse the &str implementation
}
}
+2 -2
View File
@@ -19,8 +19,8 @@ Content-Type: application/json
### xtream live_categories
GET http://localhost:8901/m3u?username=me&password=me
### xtream live_categories
GET http://localhost:8901/player_api.php?action=get_live_categories&username=xt&password=xt
### xtream live_streams
GET http://localhost:8901/player_api.php?action=get_live_streams&username=xt&password=xt&category_id=203
### xtream series_categories
GET http://localhost:8901/player_api.php?action=get_series_categories&username=xt&password=xt