use std::borrow::BorrowMut; use std::collections::{HashMap, HashSet}; use std::path::PathBuf; use std::str::FromStr; use std::sync::{Arc, RwLock}; use enum_iterator::Sequence; use log::{debug, error, warn}; use path_absolutize::*; use crate::filter::{Filter, get_filter, MockValueProcessor, PatternTemplate, prepare_templates, ValueProvider}; use crate::m3u_filter_error::{M3uFilterError, M3uFilterErrorKind}; use crate::messaging::MsgKind; use crate::model::api_proxy::{ApiProxyConfig, UserCredentials}; use crate::model::mapping::Mapping; use crate::model::mapping::Mappings; use crate::model::model_config::{default_as_false, default_as_true, default_as_zero, ItemField, ProcessingOrder, SortOrder, TargetType}; use crate::utils; use crate::utils::get_working_path; fn default_as_frm() -> ProcessingOrder { ProcessingOrder::Frm } pub(crate) fn default_as_default() -> String { String::from("default") } fn default_as_empty_map() -> HashMap { HashMap::new() } fn default_as_empty_list() -> Vec { vec![] } #[macro_export] macro_rules! create_m3u_filter_error_result { ($kind: expr, $($arg:tt)*) => { Err(M3uFilterError::new($kind, format!($($arg)*))) } } #[macro_export] macro_rules! handle_m3u_filter_error_result_list { ($kind:expr, $result: expr) => { let errors = $result .filter_map(|result| { if let Err(err) = result { Some(err.to_string()) } else { None } }) .collect::>(); if !&errors.is_empty() { return Err(M3uFilterError::new($kind, errors.join("\n"))); } } } #[macro_export] macro_rules! handle_m3u_filter_error_result { ($kind:expr, $result: expr) => { if let Err(err) = $result { return Err(M3uFilterError::new($kind, err.to_string())); } } } #[derive(Clone)] pub(crate) struct ProcessTargets { pub enabled: bool, pub inputs: Vec, pub targets: Vec, } impl ProcessTargets { pub fn has_target(&self, tid: u16) -> bool { matches!(self.targets.iter().position(|&x| x == tid), Some(_pos)) } pub fn has_input(&self, tid: u16) -> bool { matches!(self.inputs.iter().position(|&x| x == tid), Some(_pos)) } } #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub(crate) struct ConfigSortGroup { pub order: SortOrder, } #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub(crate) struct ConfigSortChannel { pub field: ItemField, // channel field pub group_pattern: String, // match against group title pub order: SortOrder, #[serde(skip_serializing, skip_deserializing)] pub re: Option, } impl ConfigSortChannel { pub(crate) fn prepare(&mut self) -> Result<(), M3uFilterError> { let re = regex::Regex::new(&self.group_pattern); if re.is_err() { return create_m3u_filter_error_result!(M3uFilterErrorKind::Info, "cant parse regex: {}", &self.group_pattern); } self.re = Some(re.unwrap()); Ok(()) } } #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub(crate) struct ConfigSort { #[serde(default = "default_as_false")] pub match_as_ascii: bool, pub groups: Option, pub channels: Option>, } impl ConfigSort { pub(crate) fn prepare(&mut self) -> Result<(), M3uFilterError> { if let Some(channels) = self.channels.as_mut() { handle_m3u_filter_error_result_list!(M3uFilterErrorKind::Info, channels.iter_mut().map(|r| r.prepare())); } Ok(()) } } #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub(crate) struct ConfigRename { pub field: ItemField, pub pattern: String, pub new_name: String, #[serde(skip_serializing, skip_deserializing)] pub re: Option, } impl ConfigRename { pub fn prepare(&mut self) -> Result<(), M3uFilterError> { let re = regex::Regex::new(&self.pattern); if re.is_err() { return create_m3u_filter_error_result!(M3uFilterErrorKind::Info, "cant parse regex: {}", &self.pattern); } self.re = Some(re.unwrap()); Ok(()) } } fn default_as_two() -> u16 { 2 } #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub(crate) struct ConfigTargetOptions { #[serde(default = "default_as_false")] pub ignore_logo: bool, #[serde(default = "default_as_false")] pub underscore_whitespace: bool, #[serde(default = "default_as_false")] pub cleanup: bool, #[serde(default = "default_as_false")] pub kodi_style: bool, #[serde(default = "default_as_false")] pub xtream_skip_live_direct_source: bool, #[serde(default = "default_as_false")] pub xtream_skip_video_direct_source: bool, #[serde(default = "default_as_false")] pub xtream_resolve_series: bool, #[serde(default = "default_as_two")] pub xtream_resolve_series_delay: u16, } #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub(crate) struct TargetOutput { #[serde(alias = "type")] pub target: TargetType, #[serde(skip_serializing_if = "Option::is_none")] pub filename: Option, } #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub(crate) struct ConfigTarget { #[serde(skip)] pub id: u16, #[serde(default = "default_as_true")] pub enabled: bool, #[serde(default = "default_as_default")] pub name: String, #[serde(skip_serializing_if = "Option::is_none")] pub options: Option, #[serde(skip_serializing_if = "Option::is_none")] pub sort: Option, pub filter: String, #[serde(alias = "type", default = "default_as_empty_list")] pub output: Vec, #[serde(skip_serializing_if = "Option::is_none")] pub rename: Option>, #[serde(skip_serializing_if = "Option::is_none")] pub mapping: Option>, #[serde(default = "default_as_frm")] pub processing_order: ProcessingOrder, #[serde(skip_serializing_if = "Option::is_none")] pub watch: Option>, #[serde(skip_serializing, skip_deserializing)] pub _filter: Option, #[serde(skip_serializing, skip_deserializing)] pub _mapping: Option>, } impl ConfigTarget { pub(crate) fn prepare(&mut self, id: u16, templates: Option<&Vec>) -> Result<(), M3uFilterError> { self.id = id; if self.output.is_empty() { return Err(M3uFilterError::new(M3uFilterErrorKind::Info, format!("Missing output format for {}", self.name))); } let mut m3u_cnt = 0; let mut strm_cnt = 0; let mut xtream_cnt = 0; for format in &self.output { match format.target { TargetType::M3u => { m3u_cnt += 1; if format.filename.is_none() { return create_m3u_filter_error_result!(M3uFilterErrorKind::Info, "filename is required for m3u type: {}", self.name); } } TargetType::Strm => { strm_cnt += 1; if format.filename.is_none() { return create_m3u_filter_error_result!(M3uFilterErrorKind::Info, "filename is required for strm type: {}", self.name); } } TargetType::Xtream => { xtream_cnt += 1; if default_as_default().eq_ignore_ascii_case(&self.name) { return create_m3u_filter_error_result!(M3uFilterErrorKind::Info, "unique target name is required for xtream type: {}", self.name); } if let Some(fname) = &format.filename { if !fname.trim().is_empty() { warn!("Filename for target output xtream is ignored: {}", self.name) } } } } } if m3u_cnt > 1 || strm_cnt > 1 || xtream_cnt > 1 { return create_m3u_filter_error_result!(M3uFilterErrorKind::Info, "Multiple output formats with same type : {}", self.name); } match get_filter(&self.filter, templates) { Ok(fltr) => { debug!("Filter: {}", fltr); self._filter = Some(fltr); if let Some(renames) = self.rename.as_mut() { handle_m3u_filter_error_result_list!(M3uFilterErrorKind::Info, renames.iter_mut().map(|r| r.prepare())); } if let Some(sort) = self.sort.as_mut() { handle_m3u_filter_error_result!(M3uFilterErrorKind::Info, sort.prepare()); } Ok(()) } Err(err) => Err(err), } } pub(crate) fn filter(&self, provider: &ValueProvider) -> bool { let mut processor = MockValueProcessor {}; return self._filter.as_ref().unwrap().filter(provider, &mut processor); } pub(crate) fn get_m3u_filename(&self) -> Option { for format in &self.output { match format.target { TargetType::M3u => return format.filename.clone(), TargetType::Strm => {} TargetType::Xtream => {} } } None } pub(crate) fn has_output(&self, tt: &TargetType) -> bool { for format in &self.output { if tt.eq(&format.target) { return true; } } false } } #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub(crate) struct ConfigSource { pub inputs: Vec, pub targets: Vec, } impl ConfigSource { pub(crate) fn prepare(&mut self, index: u16) -> Result { handle_m3u_filter_error_result_list!(M3uFilterErrorKind::Info, self.inputs.iter_mut().enumerate().map(|(idx, i)| i.prepare(index+(idx as u16)))); Ok(index + (self.inputs.len() as u16)) } pub(crate)fn get_input_for_target(&self, target_name: &str, input_type: &InputType) -> Option<&ConfigInput> { for target in &self.targets { if target.name.eq(target_name) { for input in &self.inputs { if input.input_type.eq(input_type) { return Some(input) } } } } None } } #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub(crate) struct InputAffix { pub field: String, pub value: String, } #[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Sequence, PartialEq)] pub(crate) enum InputType { #[serde(rename = "m3u")] M3u, #[serde(rename = "xtream")] Xtream, } impl ToString for InputType { fn to_string(&self) -> String { match self { InputType::M3u => "m3u".to_string(), InputType::Xtream => "xtream".to_string() } } } impl FromStr for InputType { type Err = M3uFilterError; fn from_str(s: &str) -> Result { if s.eq("m3u") { Ok(InputType::M3u) } else if s.eq("xtream") { Ok(InputType::Xtream) } else { create_m3u_filter_error_result!(M3uFilterErrorKind::Info, "Unkown InputType: {}", s) } } } #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub(crate) struct ConfigInputOptions { #[serde(default = "default_as_false")] pub xtream_info_cache: bool, } fn default_as_type_m3u() -> InputType { InputType::M3u } #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub(crate) struct ConfigInput { #[serde(skip)] pub id: u16, #[serde(rename = "type", default = "default_as_type_m3u")] pub input_type: InputType, #[serde(default = "default_as_empty_map")] pub headers: HashMap, pub url: String, #[serde(skip_serializing_if = "Option::is_none")] pub epg_url: Option, #[serde(skip_serializing_if = "Option::is_none")] pub username: Option, #[serde(skip_serializing_if = "Option::is_none")] pub password: Option, #[serde(skip_serializing_if = "Option::is_none")] pub persist: Option, #[serde(skip_serializing_if = "Option::is_none")] pub prefix: Option, #[serde(skip_serializing_if = "Option::is_none")] pub suffix: Option, #[serde(skip_serializing_if = "Option::is_none")] pub name: Option, #[serde(default = "default_as_true")] pub enabled: bool, #[serde(skip_serializing_if = "Option::is_none")] pub options: Option, } impl ConfigInput { pub fn prepare(&mut self, id: u16) -> Result<(), M3uFilterError> { self.id = id; if self.url.trim().is_empty() { return Err(M3uFilterError::new(M3uFilterErrorKind::Info, "url for input is mandatory".to_string())); } if let Some(user_name) = &self.username { if user_name.trim().is_empty() { self.username = None; } } if let Some(password) = &self.password { if password.trim().is_empty() { self.password = None; } } match self.input_type { InputType::M3u => { if self.username.is_none() || self.password.is_none() { debug!("for input type m3u: username and password are ignored") } } InputType::Xtream => { if self.username.is_none() || self.password.is_none() { return Err(M3uFilterError::new(M3uFilterErrorKind::Info, "for input type xtream: username and password are mandatory".to_string())); } } } if let Some(persist_path) = &self.persist { if persist_path.trim().is_empty() { self.persist = None; } } Ok(()) } } #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub(crate) struct ConfigApi { pub host: String, pub port: u16, pub web_root: String, } impl ConfigApi { pub fn prepare(&mut self) { if self.web_root.is_empty() { self.web_root = String::from("./web"); } } } #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub(crate) struct TelegramMessagingConfig { pub bot_token: String, pub chat_ids: Vec, } #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub(crate) struct RestMessagingConfig { pub url: String, } #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub(crate) struct MessagingConfig { #[serde(default = "default_as_empty_list")] pub notify_on: Vec, pub telegram: Option, pub rest: Option, } #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub(crate) struct VideoDownloadConfig { #[serde(default = "default_as_empty_map")] pub headers: HashMap, pub directory: Option, #[serde(default = "default_as_false")] pub organize_into_directories: bool, pub episode_pattern: Option, #[serde(skip_serializing, skip_deserializing)] pub _re_episode_pattern: Option, #[serde(skip_serializing, skip_deserializing)] pub _re_filename: Option, #[serde(skip_serializing, skip_deserializing)] pub _re_remove_filename_ending: Option, } #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub(crate) struct VideoConfig { #[serde(default = "default_as_empty_list")] pub extensions: Vec, #[serde(skip_serializing_if = "Option::is_none")] pub download: Option, #[serde(skip_serializing_if = "Option::is_none")] pub web_search: Option } impl VideoConfig { pub fn prepare(&mut self) -> Result<(), M3uFilterError> { self.extensions = vec!["mkv".to_string(), "avi".to_string(), "mp4".to_string()]; match &mut self.download { None => {} Some(downl) => { if downl.headers.is_empty() { downl.headers.borrow_mut().insert("Accept".to_string(), "video/*".to_string()); downl.headers.borrow_mut().insert("User-Agent".to_string(), "AppleTV/tvOS/9.1.1.".to_string()); } if let Some(episode_pattern) = &downl.episode_pattern { if !episode_pattern.is_empty() { let re = regex::Regex::new(episode_pattern); if re.is_err() { return create_m3u_filter_error_result!(M3uFilterErrorKind::Info, "cant parse regex: {}", episode_pattern); } downl._re_episode_pattern = Some(re.unwrap()); } } downl._re_filename = Some(regex::Regex::new(r"[^A-Za-z0-9_.-]").unwrap()); downl._re_remove_filename_ending = Some(regex::Regex::new(r"[_.\s-]$").unwrap()); } } Ok(()) } } #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub(crate) struct ConfigDto { #[serde(default = "default_as_zero")] pub threads: u8, pub api: ConfigApi, pub working_dir: String, pub backup_dir: Option, pub video: Option, pub schedule: Option, pub messaging: Option, } impl ConfigDto { pub fn is_valid(&self) -> bool { if self.api.host.is_empty() { return false; } if let Some(video) = &self.video { if let Some(download) = &video.download { if let Some(episode_pattern) = &download.episode_pattern { if ! episode_pattern.is_empty() { let re = regex::Regex::new(episode_pattern); if re.is_err() { return false; } } } } } true } } #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub(crate) struct Config { #[serde(default = "default_as_zero")] pub threads: u8, pub api: ConfigApi, pub sources: Vec, pub working_dir: String, pub backup_dir: Option, pub templates: Option>, pub video: Option, pub schedule: Option, pub messaging: Option, #[serde(skip_serializing, skip_deserializing)] pub _api_proxy: Arc>>, #[serde(skip_serializing, skip_deserializing)] pub _config_file_path: String, #[serde(skip_serializing, skip_deserializing)] pub _sources_file_path: String, } impl Config { pub fn set_api_proxy(&mut self, api_proxy: Option) { self._api_proxy = Arc::new(RwLock::new(api_proxy)); } fn _get_target_for_user(&self, user_target: Option<(UserCredentials, String)>) -> Option<(UserCredentials, &ConfigTarget)> { match user_target { Some((user, target_name)) => { for source in &self.sources { for target in &source.targets { if target_name.eq_ignore_ascii_case(&target.name) { return Some((user, target)); } } } None } None => None } } pub(crate) fn get_input_for_target(&self, target_name: &str, input_type: &InputType) -> Option<&ConfigInput> { for source in &self.sources { if let Some(cfg) = source.get_input_for_target(target_name, input_type) { return Some(cfg) } } None } pub fn get_target_for_user(&self, username: &str, password: &str) -> Option<(UserCredentials, &ConfigTarget)> { match self._api_proxy.read().unwrap().as_ref() { Some(api_proxy) => { self._get_target_for_user(api_proxy.get_target_name(username, password)) } _ => None } } pub fn get_target_for_user_by_token(&self, token: &str) -> Option<(UserCredentials, &ConfigTarget)> { match self._api_proxy.read().unwrap().as_ref() { Some(api_proxy) => { self._get_target_for_user(api_proxy.get_target_name_by_token(token)) } _ => None } } pub fn get_input_by_id(&self, input_id: &u16) -> Option { for source in &self.sources { for input in &source.inputs { if input.id == *input_id { return Some(input.clone()); } } } None } pub fn set_mappings(&mut self, mappings: Option) -> Result<(), M3uFilterError> { if let Some(mapping_list) = mappings { for source in &mut self.sources { for target in &mut source.targets { if let Some(mapping_ids) = &target.mapping { let mut target_mappings = Vec::new(); for mapping_id in mapping_ids { let mapping = mapping_list.get_mapping(mapping_id); if let Some(mappings) = mapping { target_mappings.push(mappings); } } target._mapping = if !target_mappings.is_empty() { Some(target_mappings) } else { None }; } } } } Ok(()) } pub fn prepare(&mut self) -> Result<(), M3uFilterError> { self.working_dir = get_working_path(&self.working_dir); if self.backup_dir.is_none() { self.backup_dir = Some(PathBuf::from(&self.working_dir).join(".backup").into_os_string().to_string_lossy().to_string()); } let backupdir = PathBuf::from(self.backup_dir.as_ref().unwrap()); if ! backupdir.exists() { match std::fs::create_dir(backupdir) { Ok(_) => {} Err(err) => { error!("Could not create backup dir {} {}", self.backup_dir.as_ref().unwrap(), err)} } } self.api.prepare(); self.prepare_api_web_root(); if let Some(templates) = &mut self.templates { match prepare_templates(templates) { Ok(tmplts) => { self.templates = Some(tmplts); } Err(err) => { return Err(err); } } }; // prepare sources and set id's let mut target_names_check = HashSet::::new(); let default_target_name = default_as_default(); let mut source_index: u16 = 1; let mut target_index: u16 = 1; for source in &mut self.sources { source_index = source.prepare(source_index)?; for target in &mut source.targets { // check target name is unique let target_name = target.name.clone(); if !default_target_name.eq_ignore_ascii_case(target_name.as_str()) { if target_names_check.contains(target_name.as_str()) { return create_m3u_filter_error_result!(M3uFilterErrorKind::Info, "target names should be unique: {}", target_name, ); } else { target_names_check.insert(target_name); } } // prepare templaes let prepare_result = match &self.templates { Some(templ) => target.prepare(target_index, Some(templ)), _ => target.prepare(target_index, None) }; prepare_result?; target_index += 1; } } match &mut self.video { None => { self.video = Some(VideoConfig { extensions: vec!["mkv".to_string(), "avi".to_string(), "mp4".to_string()], download: None, web_search: None, }); } Some(video) => { match video.prepare() { Ok(_) => {} Err(err) => return Err(err) } }, }; Ok(()) } fn prepare_api_web_root(&mut self) { if !self.api.web_root.is_empty() { let wrpb = std::path::PathBuf::from(&self.api.web_root); if wrpb.is_relative() { let mut wrpb2 = std::path::PathBuf::from(&self.working_dir).join(&self.api.web_root); if !wrpb2.exists() { wrpb2 = utils::get_exe_path().join(&self.api.web_root); } if !wrpb2.exists() { let cwd = std::env::current_dir(); if let Ok(cwd_path) = cwd { wrpb2 = cwd_path.join(&self.api.web_root); } } if wrpb2.exists() { match wrpb2.absolutize() { Ok(os) => self.api.web_root = String::from(os.to_str().unwrap()), Err(e) => { error!("failed to absolutize web_root {:?}", e); } } // } else { // error!("web_root directory does not exists {:?}", wrpb2) } } } } } /// Returns the targets that were specified as parameters. /// If invalid targets are found, the program will be terminated. /// The return value has `enabled` set to true, if selective targets should be processed, otherwise false. /// /// * `target_args` the program parameters given with `-target` parameter. /// * `sources` configured sources in config file /// pub(crate) fn validate_targets(target_args: &Option>, sources: &Vec) -> Result { let mut enabled = true; let mut inputs: Vec = vec![]; let mut targets: Vec = vec![]; if let Some(user_targets) = target_args { let mut check_targets: HashMap = user_targets.iter().map(|t| (t.to_lowercase(), 0)).collect(); for source in sources { let mut target_added = false; for target in &source.targets { for user_target in user_targets { let key = user_target.to_lowercase(); if target.name.eq_ignore_ascii_case(key.as_str()) { targets.push(target.id); target_added = true; if let Some(value) = check_targets.get(key.as_str()) { check_targets.insert(key, value + 1); } } } } if target_added { source.inputs.iter().map(|i| i.id).for_each(|id| inputs.push(id)); } } let missing_targets: Vec = check_targets.iter().filter(|&(_, v)| *v == 0).map(|(k, _)| k.to_string()).collect(); if !missing_targets.is_empty() { return create_m3u_filter_error_result!(M3uFilterErrorKind::Info, "No target found for {}", missing_targets.join(", ")); } let processing_targets: Vec = check_targets.iter().filter(|&(_, v)| *v != 0).map(|(k, _)| k.to_string()).collect(); debug!("Processing targets {}", processing_targets.join(", ")); } else { enabled = false; } Ok(ProcessTargets { enabled, inputs, targets, }) }