Files
tuliprox/src/model/config.rs
T
2025-02-06 10:49:34 +01:00

1337 lines
46 KiB
Rust

#![allow(clippy::struct_excessive_bools)]
use enum_iterator::Sequence;
use std::borrow::BorrowMut;
use std::collections::{HashMap, HashSet};
use std::fmt::Display;
use std::fs::File;
use std::io::BufRead;
use std::path::PathBuf;
use std::str::FromStr;
use parking_lot::RwLock;
use std::sync::Arc;
use crate::auth::user::UserCredential;
use log::{debug, error, warn};
use path_clean::PathClean;
use url::Url;
use crate::foundation::filter::{get_filter, prepare_templates, Filter, MockValueProcessor, PatternTemplate, ValueProvider};
use crate::m3u_filter_error::info_err;
use crate::m3u_filter_error::{M3uFilterError, M3uFilterErrorKind};
use crate::messaging::MsgKind;
use crate::model::api_proxy::{ApiProxyConfig, ApiProxyServerInfo, ProxyUserCredentials};
use crate::model::mapping::Mapping;
use crate::model::mapping::Mappings;
use crate::utils::file::config_reader;
use crate::utils::default_utils::{default_as_default, default_as_true, default_as_two_u16};
use crate::utils::file::file_lock_manager::FileLockManager;
use crate::utils::file::file_utils;
use crate::utils::file::file_utils::file_reader;
use crate::utils::size_utils::parse_size_base_2;
use crate::utils::sys_utils::exit;
pub const MAPPER_ATTRIBUTE_FIELDS: &[&str] = &[
"name", "title", "group", "id", "chno", "logo",
"logo_small", "parent_code", "audio_track",
"time_shift", "rec", "url", "epg_channel_id", "epg_id"
];
pub const AFFIX_FIELDS: &[&str] = &["name", "title", "group"];
pub const COUNTER_FIELDS: &[&str] = &["name", "title", "chno"];
const STREAM_QUEUE_SIZE: usize = 1024; // mpsc channel holding messages. with 8092byte chunks and 2Mbit/s approx 8MB
#[macro_export]
macro_rules! valid_property {
($key:expr, $array:expr) => {{
$array.contains(&$key)
}};
}
pub use valid_property;
use crate::m3u_filter_error::{create_m3u_filter_error_result, handle_m3u_filter_error_result, handle_m3u_filter_error_result_list};
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Sequence, PartialEq, Eq, Hash)]
pub enum TargetType {
#[serde(rename = "m3u")]
M3u,
#[serde(rename = "xtream")]
Xtream,
#[serde(rename = "strm")]
Strm,
}
impl TargetType {
const M3U: &'static str = "M3u";
const XTREAM: &'static str = "Xtream";
const STRM: &'static str = "Strm";
}
impl Display for TargetType {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "{}", match *self {
Self::M3u => Self::M3U,
Self::Xtream => Self::XTREAM,
Self::Strm => Self::STRM,
})
}
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Sequence, PartialEq, Eq, Default)]
pub enum ProcessingOrder {
#[serde(rename = "frm")]
#[default]
Frm,
#[serde(rename = "fmr")]
Fmr,
#[serde(rename = "rfm")]
Rfm,
#[serde(rename = "rmf")]
Rmf,
#[serde(rename = "mfr")]
Mfr,
#[serde(rename = "mrf")]
Mrf,
}
impl ProcessingOrder {
const FRM: &'static str = "filter, rename, map";
const FMR: &'static str = "filter, map, rename";
const RFM: &'static str = "rename, filter, map";
const RMF: &'static str = "rename, map, filter";
const MFR: &'static str = "map, filter, rename";
const MRF: &'static str = "map, rename, filter";
}
impl Display for ProcessingOrder {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "{}", match *self {
Self::Frm => Self::FRM,
Self::Fmr => Self::FMR,
Self::Rfm => Self::RFM,
Self::Rmf => Self::RMF,
Self::Mfr => Self::MFR,
Self::Mrf => Self::MRF,
})
}
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Sequence)]
pub enum ItemField {
#[serde(rename = "group")]
Group,
#[serde(rename = "name")]
Name,
#[serde(rename = "title")]
Title,
#[serde(rename = "url")]
Url,
#[serde(rename = "type")]
Type,
}
impl ItemField {
const GROUP: &'static str = "Group";
const NAME: &'static str = "Name";
const TITLE: &'static str = "Title";
const URL: &'static str = "Url";
const TYPE: &'static str = "Type";
}
impl Display for ItemField {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "{}", match *self {
Self::Group => Self::GROUP,
Self::Name => Self::NAME,
Self::Title => Self::TITLE,
Self::Url => Self::URL,
Self::Type => Self::TYPE,
})
}
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub enum FilterMode {
#[serde(rename = "discard")]
Discard,
#[serde(rename = "include")]
Include,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub enum SortOrder {
#[serde(rename = "asc")]
Asc,
#[serde(rename = "desc")]
Desc,
}
#[derive(Clone, Debug)]
pub struct ProcessTargets {
pub enabled: bool,
pub inputs: Vec<u16>,
pub targets: Vec<u16>,
}
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)]
#[serde(deny_unknown_fields)]
pub struct ConfigSortGroup {
pub order: SortOrder,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ConfigSortChannel {
pub field: ItemField,
// channel field
pub group_pattern: String,
// match against group title
pub order: SortOrder,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub sequence: Option<Vec<String>>,
#[serde(skip_serializing, skip_deserializing)]
pub re: Option<regex::Regex>,
}
impl ConfigSortChannel {
pub fn prepare(&mut self) -> Result<(), M3uFilterError> {
match regex::Regex::new(&self.group_pattern) {
Ok(pattern) => {
self.re = Some(pattern);
Ok(())
}
Err(err) => create_m3u_filter_error_result!(M3uFilterErrorKind::Info, "cant parse regex: {} {err}", &self.group_pattern),
}
}
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Default)]
#[serde(deny_unknown_fields)]
pub struct ConfigSort {
#[serde(default)]
pub match_as_ascii: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub groups: Option<ConfigSortGroup>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub channels: Option<Vec<ConfigSortChannel>>,
}
impl ConfigSort {
pub 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(ConfigSortChannel::prepare));
}
Ok(())
}
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ConfigRename {
pub field: ItemField,
pub pattern: String,
pub new_name: String,
#[serde(skip_serializing, skip_deserializing)]
pub re: Option<regex::Regex>,
}
impl ConfigRename {
pub fn prepare(&mut self) -> Result<(), M3uFilterError> {
match regex::Regex::new(&self.pattern) {
Ok(pattern) => {
self.re = Some(pattern);
Ok(())
}
Err(err) => create_m3u_filter_error_result!(M3uFilterErrorKind::Info, "cant parse regex: {} {err}", &self.pattern),
}
}
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Default)]
#[serde(deny_unknown_fields)]
pub struct ConfigTargetOptions {
#[serde(default)]
pub ignore_logo: bool,
#[serde(default)]
pub underscore_whitespace: bool,
#[serde(default)]
pub cleanup: bool,
#[serde(default)]
pub kodi_style: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub strm_props: Option<Vec<String>>,
#[serde(default = "default_as_true")]
pub xtream_skip_live_direct_source: bool,
#[serde(default = "default_as_true")]
pub xtream_skip_video_direct_source: bool,
#[serde(default = "default_as_true")]
pub xtream_skip_series_direct_source: bool,
#[serde(default)]
pub xtream_resolve_series: bool,
#[serde(default = "default_as_two_u16")]
pub xtream_resolve_series_delay: u16,
#[serde(default)]
pub xtream_resolve_vod: bool,
#[serde(default = "default_as_two_u16")]
pub xtream_resolve_vod_delay: u16,
#[serde(default)]
pub m3u_include_type_in_url: bool,
#[serde(default)]
pub m3u_mask_redirect_url: bool,
#[serde(default)]
pub share_live_streams: bool,
#[serde(default)]
pub remove_duplicates: bool,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(deny_unknown_fields)]
pub struct TargetOutput {
#[serde(alias = "type")]
pub target: TargetType,
#[serde(skip_serializing_if = "Option::is_none")]
pub filename: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub username: Option<String>,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Default)]
#[serde(deny_unknown_fields)]
pub struct ConfigTarget {
#[serde(skip)]
pub id: u16,
#[serde(default = "default_as_true")]
pub enabled: bool,
#[serde(default = "default_as_default")]
pub name: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub options: Option<ConfigTargetOptions>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub sort: Option<ConfigSort>,
pub filter: String,
#[serde(default)]
pub output: Vec<TargetOutput>,
#[serde(skip_serializing_if = "Option::is_none")]
pub rename: Option<Vec<ConfigRename>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub mapping: Option<Vec<String>>,
#[serde(default)]
pub processing_order: ProcessingOrder,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub watch: Option<Vec<String>>,
#[serde(default, skip_serializing, skip_deserializing)]
pub t_watch_re: Option<Vec<regex::Regex>>,
#[serde(default, skip_serializing, skip_deserializing)]
pub t_filter: Option<Filter>,
#[serde(default, skip_serializing, skip_deserializing)]
pub t_mapping: Option<Vec<Mapping>>,
}
impl ConfigTarget {
pub fn prepare(&mut self, id: u16, templates: Option<&Vec<PatternTemplate>>) -> Result<(), M3uFilterError> {
self.id = id;
if self.output.is_empty() {
return Err(info_err!(format!("Missing output format for {}", self.name)));
}
let mut m3u_cnt = 0;
let mut strm_cnt = 0;
let mut xtream_cnt = 0;
let mut strm_needs_xtream = false;
for format in &self.output {
let has_username = if let Some(username) = &format.username { !username.trim().is_empty() } else { false };
let has_filename = if let Some(fname) = &format.filename { !fname.trim().is_empty() } else { false };
match format.target {
TargetType::M3u => {
m3u_cnt += 1;
if has_username {
warn!("Username for target output m3u is ignored: {}", self.name);
}
}
TargetType::Strm => {
strm_cnt += 1;
if !has_filename {
return create_m3u_filter_error_result!(M3uFilterErrorKind::Info, "filename is required for strm type: {}", self.name);
}
if has_username {
strm_needs_xtream = true;
}
}
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 has_username {
warn!("Username for target output xtream is ignored: {}", self.name);
}
if has_filename {
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);
}
if strm_cnt > 0 && strm_needs_xtream && xtream_cnt == 0 {
return create_m3u_filter_error_result!(M3uFilterErrorKind::Info, "strm output with a username is only permitted when used in combination with xtream output: {}", self.name);
}
if let Some(watch) = &self.watch {
let regexps: Result<Vec<regex::Regex>, _> = watch.iter().map(|s| regex::Regex::new(s)).collect();
match regexps {
Ok(watch_re) => self.t_watch_re = Some(watch_re),
Err(err) => {
return create_m3u_filter_error_result!(M3uFilterErrorKind::Info, "Invalid watch regular expression: {}", err);
}
}
}
match get_filter(&self.filter, templates) {
Ok(fltr) => {
// debug!("Filter: {}", fltr);
self.t_filter = Some(fltr);
if let Some(renames) = self.rename.as_mut() {
handle_m3u_filter_error_result_list!(M3uFilterErrorKind::Info, renames.iter_mut().map(ConfigRename::prepare));
}
if let Some(sort) = self.sort.as_mut() {
handle_m3u_filter_error_result!(M3uFilterErrorKind::Info, sort.prepare());
}
Ok(())
}
Err(err) => Err(err),
}
}
pub fn filter(&self, provider: &ValueProvider) -> bool {
let mut processor = MockValueProcessor {};
if let Some(filter) = self.t_filter.as_ref() {
return filter.filter(provider, &mut processor);
}
true
}
pub fn get_m3u_filename(&self) -> Option<&String> {
for format in &self.output {
if format.target == TargetType::M3u {
return format.filename.as_ref();
}
}
None
}
pub 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)]
#[serde(deny_unknown_fields)]
pub struct ConfigSource {
pub inputs: Vec<ConfigInput>,
pub targets: Vec<ConfigTarget>,
}
impl ConfigSource {
#[allow(clippy::cast_possible_truncation)]
pub fn prepare(&mut self, index: u16) -> Result<u16, M3uFilterError> {
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 fn get_inputs_for_target(&self, target_name: &str) -> Option<Vec<&ConfigInput>> {
for target in &self.targets {
if target.name.eq(target_name) {
let inputs = self.inputs.iter().filter(|&i| i.enabled).collect::<Vec<&ConfigInput>>();
if !inputs.is_empty() {
return Some(inputs);
}
}
}
None
}
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(deny_unknown_fields)]
pub struct InputAffix {
pub field: String,
pub value: String,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Sequence, PartialEq, Eq, Default)]
pub enum InputType {
#[serde(rename = "m3u")]
#[default]
M3u,
#[serde(rename = "xtream")]
Xtream,
}
impl InputType {
const M3U: &'static str = "m3u";
const XTREAM: &'static str = "xtream";
}
impl Display for InputType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", match self {
Self::M3u => Self::M3U,
Self::Xtream => Self::XTREAM,
})
}
}
impl FromStr for InputType {
type Err = M3uFilterError;
fn from_str(s: &str) -> Result<Self, M3uFilterError> {
if s.eq(Self::M3U) {
Ok(Self::M3u)
} else if s.eq(Self::XTREAM) {
Ok(Self::Xtream)
} else {
create_m3u_filter_error_result!(M3uFilterErrorKind::Info, "Unknown InputType: {}", s)
}
}
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Default)]
#[serde(deny_unknown_fields)]
pub struct ConfigInputOptions {
#[serde(default)]
pub xtream_skip_live: bool,
#[serde(default)]
pub xtream_skip_vod: bool,
#[serde(default)]
pub xtream_skip_series: bool,
#[serde(default = "default_as_true")]
pub xtream_live_stream_use_prefix: bool,
#[serde(default)]
pub xtream_live_stream_without_extension: bool,
}
pub struct InputUserInfo {
pub base_url: String,
pub username: String,
pub password: String,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Default)]
#[serde(deny_unknown_fields)]
pub struct ConfigInput {
#[serde(skip)]
pub id: u16,
pub name: String,
#[serde(default, rename = "type")]
pub input_type: InputType,
#[serde(default)]
pub headers: HashMap<String, String>,
pub url: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub epg_url: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub username: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub password: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub persist: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub prefix: Option<InputAffix>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub suffix: Option<InputAffix>,
#[serde(default = "default_as_true")]
pub enabled: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub options: Option<ConfigInputOptions>,
}
impl ConfigInput {
pub fn prepare(&mut self, id: u16) -> Result<(), M3uFilterError> {
self.id = id;
if self.name.trim().is_empty() {
return Err(info_err!("name for input is mandatory".to_string()));
}
if self.url.trim().is_empty() {
return Err(info_err!("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_some() || self.password.is_some() {
debug!("for input type m3u: username and password are ignored");
}
}
InputType::Xtream => {
if self.username.is_none() || self.password.is_none() {
return Err(info_err!("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(())
}
pub fn get_user_info(&self) -> Option<InputUserInfo> {
if self.input_type == InputType::Xtream {
if let (Some(username), Some(password)) = (self.username.as_ref(), self.password.as_ref()) {
return Some(InputUserInfo {
base_url: self.url.clone(),
username: username.to_owned(),
password: password.to_owned(),
});
}
} else if let Ok(url) = Url::parse(&self.url) {
let base_url = url.origin().ascii_serialization();
let mut username = None;
let mut password = None;
for (key, value) in url.query_pairs() {
if key.eq("username") {
username = Some(value.into_owned());
} else if key.eq("password") {
password = Some(value.into_owned());
}
}
if username.is_some() || password.is_some() {
if let (Some(username), Some(password)) = (username.as_ref(), password.as_ref()) {
return Some(InputUserInfo {
base_url,
username: username.to_owned(),
password: password.to_owned(),
});
}
}
}
None
}
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Default)]
#[serde(deny_unknown_fields)]
pub struct ConfigApi {
pub host: String,
pub port: u16,
#[serde(default)]
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)]
#[serde(deny_unknown_fields)]
pub struct TelegramMessagingConfig {
pub bot_token: String,
pub chat_ids: Vec<String>,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(deny_unknown_fields)]
pub struct RestMessagingConfig {
pub url: String,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(deny_unknown_fields)]
pub struct PushoverMessagingConfig {
pub(crate) url: Option<String>,
pub(crate) token: String,
pub(crate) user: String,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Default)]
#[serde(deny_unknown_fields)]
pub struct MessagingConfig {
#[serde(default)]
pub notify_on: Vec<MsgKind>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub telegram: Option<TelegramMessagingConfig>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub rest: Option<RestMessagingConfig>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub pushover: Option<PushoverMessagingConfig>,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Default)]
#[serde(deny_unknown_fields)]
pub struct LogConfig {
#[serde(default = "default_as_true")]
pub sanitize_sensitive_info: bool,
#[serde(default)]
pub active_clients: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub log_level: Option<String>,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Default)]
pub struct LogLevelConfig {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub log: Option<LogConfig>,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Default)]
#[serde(deny_unknown_fields)]
pub struct VideoDownloadConfig {
#[serde(default)]
pub headers: HashMap<String, String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub directory: Option<String>,
#[serde(default)]
pub organize_into_directories: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub episode_pattern: Option<String>,
#[serde(default, skip_serializing, skip_deserializing)]
pub t_re_episode_pattern: Option<regex::Regex>,
#[serde(default, skip_serializing, skip_deserializing)]
pub t_re_filename: Option<regex::Regex>,
#[serde(default, skip_serializing, skip_deserializing)]
pub t_re_remove_filename_ending: Option<regex::Regex>,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Default)]
#[serde(deny_unknown_fields)]
pub struct VideoConfig {
#[serde(default)]
pub extensions: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub download: Option<VideoDownloadConfig>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub web_search: Option<String>,
}
impl VideoConfig {
/// # Panics
///
/// Will panic if default `RegEx` gets invalid
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(), "Mozilla/5.0 (AppleTV; U; CPU OS 14_2 like Mac OS X; en-us) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.0.1 Safari/605.1.15
".to_string());
}
if let Some(episode_pattern) = &downl.episode_pattern {
if !episode_pattern.is_empty() {
match regex::Regex::new(episode_pattern) {
Ok(pattern) => {
downl.t_re_episode_pattern = Some(pattern);
}
Err(err) => {
return create_m3u_filter_error_result!(M3uFilterErrorKind::Info, "cant parse regex: {episode_pattern} {err}");
}
}
}
}
downl.t_re_filename = Some(regex::Regex::new(r"[^A-Za-z0-9_.-]").unwrap());
downl.t_re_remove_filename_ending = Some(regex::Regex::new(r"[_.\s-]$").unwrap());
}
}
Ok(())
}
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Default)]
#[serde(deny_unknown_fields)]
pub struct ConfigDto {
#[serde(default)]
pub threads: u8,
pub api: ConfigApi,
#[serde(default)]
pub working_dir: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub backup_dir: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub video: Option<VideoConfig>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub schedules: Option<Vec<ScheduleConfig>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub messaging: Option<MessagingConfig>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub log: Option<LogConfig>,
#[serde(default)]
pub update_on_boot: bool,
#[serde(default = "default_as_true")]
pub web_ui_enabled: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub web_auth: Option<WebAuthConfig>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub reverse_proxy: Option<ReverseProxyConfig>,
}
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)]
#[serde(deny_unknown_fields)]
pub struct WebAuthConfig {
#[serde(default = "default_as_true")]
pub enabled: bool,
pub issuer: String,
pub secret: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub userfile: Option<String>,
#[serde(skip_serializing, skip_deserializing)]
pub t_users: Option<Vec<UserCredential>>,
}
impl WebAuthConfig {
pub fn prepare(&mut self, config_path: &str, resolve_var: bool) -> Result<(), M3uFilterError> {
if resolve_var {
self.issuer = config_reader::resolve_env_var(&self.issuer);
self.secret = config_reader::resolve_env_var(&self.secret);
if let Some(file) = &self.userfile {
self.userfile = Some(config_reader::resolve_env_var(file));
}
}
let userfile_name = self.userfile.as_ref().map_or_else(|| file_utils::get_default_user_file_path(config_path), std::borrow::ToOwned::to_owned);
self.userfile = Some(userfile_name.clone());
let mut userfile_path = PathBuf::from(&userfile_name);
if !file_utils::path_exists(&userfile_path) {
userfile_path = PathBuf::from(config_path).join(&userfile_name);
if !file_utils::path_exists(&userfile_path) {
return create_m3u_filter_error_result!(M3uFilterErrorKind::Info, "Could not find userfile {}", &userfile_name);
}
}
if let Ok(file) = File::open(&userfile_path) {
let mut users = vec![];
let reader = file_reader(file);
for credentials in reader.lines().map_while(Result::ok) {
let mut parts = credentials.split(':');
if let (Some(username), Some(password)) = (parts.next(), parts.next()) {
users.push(UserCredential {
username: username.trim().to_string(),
password: password.trim().to_string(),
});
debug!("Read ui user {}", username);
}
}
self.t_users = Some(users);
} else {
return create_m3u_filter_error_result!(M3uFilterErrorKind::Info, "Could not read userfile {:?}", &userfile_path);
}
Ok(())
}
pub fn get_user_password(&self, username: &str) -> Option<&str> {
if let Some(users) = &self.t_users {
for credential in users {
if credential.username.eq_ignore_ascii_case(username) {
return Some(credential.password.as_str());
}
}
}
None
}
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Default)]
#[serde(deny_unknown_fields)]
pub struct ScheduleConfig {
#[serde(default)]
pub schedule: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub targets: Option<Vec<String>>,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Default)]
#[serde(deny_unknown_fields)]
pub struct CacheConfig {
#[serde(default)]
pub enabled: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub size: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub dir: Option<String>,
#[serde(skip)]
pub t_size: usize,
}
impl CacheConfig {
fn prepare(&mut self, working_dir: &str, resolve_var: bool) {
if self.enabled {
let work_path = PathBuf::from(working_dir);
if self.dir.is_none() {
self.dir = Some(work_path.join("cache").to_string_lossy().to_string());
} else {
let mut cache_dir = if resolve_var { config_reader::resolve_env_var(self.dir.as_ref().unwrap()) } else { self.dir.as_ref().unwrap().to_string() };
if PathBuf::from(&cache_dir).is_relative() {
cache_dir = work_path.join(&cache_dir).clean().to_string_lossy().to_string();
}
self.dir = Some(cache_dir.to_string());
}
match self.size.as_ref() {
None => self.t_size = 1024,
Some(val) => match parse_size_base_2(val) {
Ok(size) => self.t_size = usize::try_from(size).unwrap_or(0),
Err(err) => { exit!("{err}") }
}
}
}
}
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Default)]
#[serde(deny_unknown_fields)]
pub struct StreamBufferConfig {
#[serde(default)]
pub enabled: bool,
#[serde(default)]
pub size: usize,
}
impl StreamBufferConfig {
fn prepare(&mut self) {
if self.enabled && self.size == 0 {
self.size = STREAM_QUEUE_SIZE;
}
}
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Default)]
#[serde(deny_unknown_fields)]
pub struct StreamConfig {
#[serde(default)]
pub retry: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub buffer: Option<StreamBufferConfig>,
}
impl StreamConfig {
fn prepare(&mut self) {
if let Some(buffer) = self.buffer.as_mut() {
buffer.prepare();
}
}
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Default)]
#[serde(deny_unknown_fields)]
pub struct ReverseProxyConfig {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub stream: Option<StreamConfig>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub cache: Option<CacheConfig>,
#[serde(default)]
pub resource_rewrite_disabled: bool,
}
impl ReverseProxyConfig {
fn prepare(&mut self, working_dir: &str, resolve_var: bool) {
if let Some(stream) = self.stream.as_mut() {
stream.prepare();
}
if let Some(cache) = self.cache.as_mut() {
if cache.enabled && self.resource_rewrite_disabled {
warn!("The cache is disabled because resource rewrite is disabled");
cache.enabled = false;
}
cache.prepare(working_dir, resolve_var);
}
}
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Default)]
#[serde(deny_unknown_fields)]
pub struct Config {
#[serde(default)]
pub threads: u8,
pub api: ConfigApi,
pub sources: Vec<ConfigSource>,
pub working_dir: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub backup_dir: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub templates: Option<Vec<PatternTemplate>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub video: Option<VideoConfig>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub schedules: Option<Vec<ScheduleConfig>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub log: Option<LogConfig>,
#[serde(default)]
pub update_on_boot: bool,
#[serde(default = "default_as_true")]
pub web_ui_enabled: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub web_auth: Option<WebAuthConfig>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub messaging: Option<MessagingConfig>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub reverse_proxy: Option<ReverseProxyConfig>,
#[serde(skip)]
pub t_api_proxy: Arc<RwLock<Option<ApiProxyConfig>>>,
#[serde(skip)]
pub t_config_path: String,
#[serde(skip)]
pub t_config_file_path: String,
#[serde(skip)]
pub t_sources_file_path: String,
#[serde(skip)]
pub t_api_proxy_file_path: String,
#[serde(skip)]
pub file_locks: Arc<FileLockManager>,
}
impl Config {
pub fn set_api_proxy(&mut self, api_proxy: Option<ApiProxyConfig>) {
self.t_api_proxy = Arc::new(RwLock::new(api_proxy));
}
pub fn is_reverse_proxy_resource_rewrite_enabled(&self) -> bool {
self.reverse_proxy.as_ref().map_or(true, |r| !r.resource_rewrite_disabled)
}
fn intern_get_target_for_user(&self, user_target: Option<(ProxyUserCredentials, String)>) -> Option<(ProxyUserCredentials, &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 fn get_inputs_for_target(&self, target_name: &str) -> Option<Vec<&ConfigInput>> {
for source in &self.sources {
if let Some(cfg) = source.get_inputs_for_target(target_name) {
return Some(cfg);
}
}
None
}
pub fn get_target_for_user(&self, username: &str, password: &str) -> Option<(ProxyUserCredentials, &ConfigTarget)> {
self.t_api_proxy.read().as_ref().and_then(|api_proxy| self.intern_get_target_for_user(api_proxy.get_target_name(username, password)))
}
pub fn get_target_for_user_by_token(&self, token: &str) -> Option<(ProxyUserCredentials, &ConfigTarget)> {
self.t_api_proxy.read().as_ref().and_then(|api_proxy| self.intern_get_target_for_user(api_proxy.get_target_name_by_token(token)))
}
pub fn get_user_credentials(&self, username: &str) -> Option<ProxyUserCredentials> {
self.t_api_proxy.read().as_ref().and_then(|api_proxy| api_proxy.get_user_credentials(username))
}
pub fn get_input_by_name(&self, input_name: &str) -> Option<&ConfigInput> {
for source in &self.sources {
for input in &source.inputs {
if input.name == input_name {
return Some(input);
}
}
}
None
}
pub fn set_mappings(&mut self, mappings_cfg: &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::with_capacity(128);
for mapping_id in mapping_ids {
let mapping = mappings_cfg.get_mapping(mapping_id);
if let Some(mappings) = mapping {
target_mappings.push(mappings);
}
}
target.t_mapping = if target_mappings.is_empty() { None } else { Some(target_mappings) };
}
}
}
}
fn check_unique_input_names(&mut self) -> Result<(), M3uFilterError> {
let mut seen_names = HashSet::new();
for source in &mut self.sources {
for input in &source.inputs {
let input_name = input.name.trim().to_string();
if input_name.is_empty() {
return create_m3u_filter_error_result!(M3uFilterErrorKind::Info, "input name required");
}
if seen_names.contains(input_name.as_str()) {
return create_m3u_filter_error_result!(M3uFilterErrorKind::Info, "input names should be unique: {}", input_name);
}
seen_names.insert(input_name);
}
}
Ok(())
}
fn check_unique_target_names(&mut self) -> Result<HashSet<String>, M3uFilterError> {
let mut seen_names = HashSet::new();
let default_target_name = default_as_default();
for source in &self.sources {
for target in &source.targets {
// check target name is unique
let target_name = target.name.trim().to_string();
if target_name.is_empty() {
return create_m3u_filter_error_result!(M3uFilterErrorKind::Info, "target name required");
}
if !default_target_name.eq_ignore_ascii_case(target_name.as_str()) {
if seen_names.contains(target_name.as_str()) {
return create_m3u_filter_error_result!(M3uFilterErrorKind::Info, "target names should be unique: {}", target_name);
}
seen_names.insert(target_name);
}
}
}
Ok(seen_names)
}
fn check_scheduled_targets(&mut self, target_names: &HashSet<String>) -> Result<(), M3uFilterError> {
if let Some(schedules) = &self.schedules {
for schedule in schedules {
if let Some(targets) = &schedule.targets {
for target_name in targets {
if !target_names.contains(target_name) {
return create_m3u_filter_error_result!(M3uFilterErrorKind::Info, "Unknown target name in scheduler: {}", target_name);
}
}
}
}
}
Ok(())
}
pub fn prepare(&mut self, resolve_var: bool) -> Result<(), M3uFilterError> {
let work_dir = if resolve_var { &config_reader::resolve_env_var(&self.working_dir) } else { &self.working_dir };
self.working_dir = file_utils::get_working_path(work_dir);
if self.backup_dir.is_none() {
self.backup_dir = Some(PathBuf::from(&self.working_dir).join("backup").clean().to_string_lossy().to_string());
} else {
self.backup_dir = self.backup_dir.as_ref().map(|backup_dir| {
if resolve_var {
config_reader::resolve_env_var(backup_dir)
} else {
backup_dir.to_owned()
}
}).map(|dir| dir.to_string());
}
if let Some(reverse_proxy) = self.reverse_proxy.as_mut() {
reverse_proxy.prepare(&self.working_dir, resolve_var);
}
self.api.prepare();
self.prepare_api_web_root(resolve_var);
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 source_index: u16 = 1;
let mut target_index: u16 = 1;
self.check_unique_input_names()?;
let target_names = self.check_unique_target_names()?;
self.check_scheduled_targets(&target_names)?;
for source in &mut self.sources {
source_index = source.prepare(source_index)?;
for target in &mut source.targets {
// prepare templates
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)
}
}
};
if !self.web_ui_enabled {
self.web_auth = None;
}
if let Some(web_auth) = &mut self.web_auth {
if web_auth.enabled {
web_auth.prepare(&self.t_config_path, resolve_var)?;
} else {
self.web_auth = None;
}
}
Ok(())
}
fn prepare_api_web_root(&mut self, resolve_var: bool) {
if !self.api.web_root.is_empty() {
let web_root = if resolve_var { config_reader::resolve_env_var(&self.api.web_root) } else { self.api.web_root.clone() };
self.api.web_root = web_root.to_string();
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(&web_root);
if !wrpb2.exists() {
wrpb2 = file_utils::get_exe_path().join(&web_root);
}
if !wrpb2.exists() {
let cwd = std::env::current_dir();
if let Ok(cwd_path) = cwd {
wrpb2 = cwd_path.join(&web_root);
}
}
if wrpb2.exists() {
self.api.web_root = String::from(wrpb2.clean().to_str().unwrap_or_default());
}
}
}
}
/// # Panics
///
/// Will panic if default server invalid
pub fn get_user_server_info(&self, user: &ProxyUserCredentials) -> ApiProxyServerInfo {
let server_info_list = self.t_api_proxy.read().as_ref().unwrap().server.clone();
let server_info_name = user.server.as_ref().map_or("default", |server_name| server_name.as_str());
server_info_list.iter().find(|c| c.name.eq(server_info_name)).map_or_else(|| server_info_list.first().unwrap().clone(), Clone::clone)
}
}
/// 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 fn validate_targets(target_args: Option<&Vec<String>>, sources: &Vec<ConfigSource>) -> Result<ProcessTargets, M3uFilterError> {
let mut enabled = true;
let mut inputs: Vec<u16> = vec![];
let mut targets: Vec<u16> = vec![];
if let Some(user_targets) = target_args {
let mut check_targets: HashMap<String, u16> = 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<String> = 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<String> = check_targets.iter().filter(|&(_, v)| *v != 0).map(|(k, _)| k.to_string()).collect();
// info!("Processing targets {}", processing_targets.join(", "));
} else {
enabled = false;
}
Ok(ProcessTargets {
enabled,
inputs,
targets,
})
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(deny_unknown_fields)]
pub struct HealthcheckConfig {
pub api: ConfigApi,
}