mirror of
https://github.com/euzu/tuliprox.git
synced 2026-10-06 16:02:36 +02:00
added transform to mapper
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
@@ -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: ®ex::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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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("?"));
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user