diff --git a/CHANGELOG.md b/CHANGELOG.md index 86ad94e17..081c84705 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ - "time_shift" - "rec" - "source" +* Added static suffix and prefix at inpupt source level # v0.9.7(2023-02-15) * Breaking changes, mappings.yml refactored diff --git a/Cargo.lock b/Cargo.lock index cccc1c9f4..b37dd7e07 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1066,7 +1066,7 @@ dependencies = [ [[package]] name = "m3u-filter" -version = "0.9.7" +version = "0.9.8" dependencies = [ "actix-cors", "actix-files", diff --git a/Cargo.toml b/Cargo.toml index a12123f5f..5be2968b4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "m3u-filter" -version = "0.9.7" +version = "0.9.8" edition = "2018" [profile.release] diff --git a/README.md b/README.md index 720222a70..9a264d15c 100644 --- a/README.md +++ b/README.md @@ -66,12 +66,19 @@ This will replace all occurrences of `!delimiter!` and `!quality!` in the regexp * `targets` ### 1.5.1 `input` -Has two entries, `persist` and `url`. +Has for entries, `persist`, `url`, `prefix`, `suffix`. -`input: { persist: ./playlist_{}.m3u, url: http://myserver.net/playlist.m3u }` +`input: { persist: ./playlist_{}.m3u, url: http://myserver.net/playlist.m3u, prefix: {field: title, value: '#!# ' }, suffix: {field: title, value: ' +-+' } }` - `persist` is optional, you can skip or leave it blank to avoid persisting the input file. The `{}` in the filename is filled with the current timestamp. - `url` is the download url or a local filename of the input-source. + - `prefix` is optional, it is applied to the given field with the given value + - `suffix` is optional, it is applied to the given field with the given value + +`prefix` and `suffix` is appended after all processing is done, but before sort. +They have 2 fields: + - `field` can be `name` , `group`, `title` + - `value` a static text ### 1.5.2 `targets` Has the following top level entries: diff --git a/bin/release.sh b/bin/release.sh index 05a0010ed..9ee1c304a 100755 --- a/bin/release.sh +++ b/bin/release.sh @@ -12,12 +12,14 @@ fi #fi cd ./frontend || (echo "cant find frontend directory" && exit) -NEW_VERSION=$(yarn version --patch | grep "New version" | grep -Po "(\d+\.)+\d+") +NEW_VERSION=$(yarn version --no-git-tag-version --patch | grep "New version" | grep -Po "(\d+\.)+\d+") cd .. -cargo-set-version set-version ./Cargo.toml "$NEW_VERSION" - +cargo set-version "$NEW_VERSION" VERSION=v$NEW_VERSION + +echo "building version $NEW_VERSION" + LIN_DIR=m3u-filter_${VERSION}_linux_x86_64 WIN_DIR=m3u-filter_${VERSION}_windows_x86_64 DARWIN_DIR=m3u-filter_${VERSION}_darwin_x86_64 diff --git a/src/config.rs b/src/config.rs index f25f97b69..0ac734a1c 100644 --- a/src/config.rs +++ b/src/config.rs @@ -3,13 +3,10 @@ use path_absolutize::*; use crate::filter::{Filter, get_filter, MockValueProcessor, PatternTemplate, prepare_templates, ValueProvider}; use crate::mapping::Mappings; use crate::mapping::Mapping; -use crate::model::{ItemField, ProcessingOrder, SortOrder, TargetType}; +use crate::model::{ItemField, ProcessingOrder, SortOrder, TargetType, default_as_zero, default_as_empty_str, default_as_false}; use crate::utils; use crate::utils::get_working_path; -fn default_as_zero() -> u8 { 0 } -fn default_as_false() -> bool { false } -fn default_as_empty_str() -> String { String::from("") } fn default_as_frm() -> ProcessingOrder { ProcessingOrder::FRM } #[derive(Debug, serde::Serialize, serde::Deserialize)] @@ -95,11 +92,19 @@ impl ConfigSources { } } +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct InputAffix { + pub field: String, + pub value: String +} + #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct ConfigInput { pub url: String, #[serde(default = "default_as_empty_str")] pub persist: String, + pub prefix: Option, + pub suffix: Option, } impl ConfigInput { diff --git a/src/m3u_processing.rs b/src/m3u_processing.rs index b0d717501..0cf8ea351 100644 --- a/src/m3u_processing.rs +++ b/src/m3u_processing.rs @@ -6,12 +6,13 @@ use std::thread; use config::ConfigTarget; use chrono::Datelike; use unidecode::unidecode; -use crate::{config, Config, get_playlist, utils}; +use crate::{config, Config, get_playlist, utils, valid_property}; +use crate::config::{ConfigInput, InputAffix}; use crate::model::SortOrder::{Asc, Desc}; use crate::filter::{ValueProvider}; -use crate::m3u::{PlaylistGroup, PlaylistItem}; +use crate::m3u::{FieldAccessor, PlaylistGroup, PlaylistItem, PlaylistItemHeader}; use crate::mapping::{Mapping, MappingValueProcessor}; -use crate::model::{ItemField, ProcessingOrder, TargetType}; +use crate::model::{ItemField, AFFIX_FIELDS, ProcessingOrder, TargetType}; macro_rules! open_file { ($path:expr) => {{ @@ -61,7 +62,54 @@ fn filter_playlist(playlist: &Vec, target: &ConfigTarget, verbose Some(new_playlist) } +fn apply_affixes(playlist: &mut Vec, input: &ConfigInput, verbose: bool) { + if input.suffix.is_some() || input.prefix.is_some() { + let validate_affix = |a: &Option| match a { + Some(affix) => { + valid_property!(&affix.field.as_str(), AFFIX_FIELDS) && affix.value.len() > 0 + } + _ => false + }; + + let apply_prefix = validate_affix(&input.prefix); + let apply_suffix = validate_affix(&input.suffix); + + if apply_prefix || apply_suffix { + let get_affix_applied_value = |header: &mut PlaylistItemHeader, affix: &InputAffix, prefix: bool| { + if let Some(field_value) = header.get_field(&affix.field.as_str()) { + return if prefix { + format!("{}{}", &affix.value, field_value.as_str()) + } else { + format!("{}{}", field_value.as_str(), &affix.value) + } + } + return String::from(&affix.value) + }; + + for group in playlist { + for channel in &mut group.channels { + if apply_suffix { + if let Some(suffix) = &input.suffix { + let value = get_affix_applied_value(&mut channel.header, suffix, false); + if verbose { println!("Applying input suffix: {}={}", &suffix.field, &value)} + channel.header.set_field(&suffix.field, &value.as_str()); + } + } + if apply_prefix { + if let Some(prefix) = &input.prefix { + let value = get_affix_applied_value(&mut channel.header, prefix, true); + if verbose { println!("Applying input prefix: {}={}", &prefix.field, &value)} + channel.header.set_field(&prefix.field, &value.as_str()); + } + } + } + } + } + } +} + pub(crate) fn write_m3u(playlist: &Vec, + input: &ConfigInput, target: &ConfigTarget, cfg: &Config, verbose: bool) -> Result<(), std::io::Error> { let pipe: Vec, target: &ConfigTarget, verbose: bool) -> Option>> = @@ -84,6 +132,8 @@ pub(crate) fn write_m3u(playlist: &Vec, } } + apply_affixes(&mut new_playlist, input, verbose); + sort_playlist(target, &mut new_playlist); match &target.output { Some(output_type) => { @@ -319,7 +369,7 @@ fn map_channel(channel: &mut PlaylistItem, mapping: &Mapping, verbose: bool) -> } fn map_playlist(playlist: &Vec, target: &ConfigTarget, verbose: bool) -> Option> { - if verbose { println!("Mapping")} + if verbose { println!("Mapping") } if target._mapping.is_some() { let mut new_playlist: Vec = Vec::new(); for playlist_group in playlist { @@ -360,7 +410,8 @@ fn set_field_value(pli: &mut PlaylistItem, field: &ItemField, value: String) -> fn process_source(cfg: Arc, source_idx: usize, verbose: bool) { let source = cfg.sources.get(source_idx).unwrap(); - let url_str = source.input.url.as_str(); + let input = &source.input; + let url_str = input.url.as_str(); let persist_file: Option = if source.input.persist.is_empty() { None } else { utils::prepare_persist_path(source.input.persist.as_str()) }; let file_path = utils::get_file_path(&cfg.working_dir, persist_file); @@ -378,7 +429,7 @@ fn process_source(cfg: Arc, source_idx: usize, verbose: bool) { } else { if verbose { println!("Input file has {} groups", playlist.len()) } for target in source.targets.iter() { - match write_m3u(playlist, target, &cfg, verbose) { + match write_m3u(playlist, input, target, &cfg, verbose) { Ok(_) => (), Err(e) => println!("Failed to write file: {}", e) } @@ -390,10 +441,10 @@ fn process_source(cfg: Arc, source_idx: usize, verbose: bool) { } pub fn process_targets(cfg: Arc, verbose: bool) { - let mut handle_list = vec![]; + let mut handle_list = vec![]; let thread_num = cfg.threads; let process_parallel = thread_num > 1 && cfg.sources.len() > 1; - if verbose && process_parallel { println!("Using {} threads", thread_num)} + if verbose && process_parallel { println!("Using {} threads", thread_num) } for (index, _) in cfg.sources.iter().enumerate() { let config = cfg.clone(); @@ -410,6 +461,6 @@ pub fn process_targets(cfg: Arc, verbose: bool) { } } for handle in handle_list { - let _= handle.join(); + let _ = handle.join(); } } diff --git a/src/mapping.rs b/src/mapping.rs index 259d654fb..ab1943bee 100644 --- a/src/mapping.rs +++ b/src/mapping.rs @@ -2,13 +2,9 @@ use std::collections::HashMap; use regex::Regex; use crate::filter::{Filter, get_filter, PatternTemplate, prepare_templates, RegexWithCaptures, ValueProcessor}; use crate::m3u::{FieldAccessor, PlaylistItem}; -use crate::model::{ItemField, MAPPER_ATTRIBUTE_FIELDS, MAPPER_PREFIX_SUFFIX_FIELDS}; - -fn default_as_false() -> bool { false } - -fn default_as_empty_str() -> String { String::from("") } - -fn default_as_empty_map() -> HashMap { HashMap::new() } +use crate::model::{ItemField, MAPPER_ATTRIBUTE_FIELDS, AFFIX_FIELDS, + default_as_empty_str, default_as_false, default_as_empty_map, }; +use crate::valid_property; #[derive(Debug, serde::Serialize, serde::Deserialize)] pub struct MappingTag { @@ -79,12 +75,6 @@ impl Clone for Mapper { } } -macro_rules! valid_property { - ($key:expr, $array:expr) => {{ - $array.contains(&$key) - }}; -} - pub struct MappingValueProcessor<'a> { pub(crate) pli: &'a mut PlaylistItem, pub(crate) mapper: &'a Mapper, @@ -154,7 +144,7 @@ impl MappingValueProcessor<'_> { fn apply_suffix(&mut self, captures: &HashMap<&String, &str>, verbose: bool) { for (key, value) in &self.mapper.suffix { - if valid_property!(key.as_str(), MAPPER_PREFIX_SUFFIX_FIELDS) { + if valid_property!(key.as_str(), AFFIX_FIELDS) { match self.apply_tags(value, captures, verbose) { Some(suffix) => { match self.get_property(key) { @@ -173,7 +163,7 @@ impl MappingValueProcessor<'_> { fn apply_prefix(&mut self, captures: &HashMap<&String, &str>, verbose: bool) { for (key, value) in &self.mapper.prefix { - if valid_property!(key.as_str(), MAPPER_PREFIX_SUFFIX_FIELDS) { + if valid_property!(key.as_str(), AFFIX_FIELDS) { match self.apply_tags(value, captures, verbose) { Some(prefix) => { match self.get_property(key) { diff --git a/src/model.rs b/src/model.rs index 92b3da8e8..8e1c1f1d7 100644 --- a/src/model.rs +++ b/src/model.rs @@ -1,3 +1,4 @@ +use std::collections::HashMap; use enum_iterator::Sequence; pub const MAPPER_ATTRIBUTE_FIELDS: &[&str] = &[ @@ -9,7 +10,24 @@ pub const MAPPER_ATTRIBUTE_FIELDS: &[&str] = &[ "rec", "source", ]; -pub const MAPPER_PREFIX_SUFFIX_FIELDS: &[&str] = &["name", "title", "group"]; +pub const AFFIX_FIELDS: &[&str] = &["name", "title", "group"]; + +#[macro_export] +macro_rules! valid_property { + ($key:expr, $array:expr) => {{ + $array.contains(&$key) + }}; +} + + +pub fn default_as_false() -> bool { false } + +pub fn default_as_empty_str() -> String { String::from("") } + +pub fn default_as_empty_map() -> HashMap { HashMap::new() } + +pub fn default_as_zero() -> u8 { 0 } + #[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Sequence)] pub enum TargetType {