mirror of
https://github.com/euzu/tuliprox.git
synced 2026-10-26 16:52:25 +01:00
- Added new mapper function `pad(text | number, number, char, optional position: "<" | ">" | "^")`
- Added new mapper function `format`, it is very simple and only supports in text replacement like `format("Hello {}! Hello {}!", "Bob", "World")`
371 lines
14 KiB
Rust
371 lines
14 KiB
Rust
use log::warn;
|
|
use crate::error::{TuliproxError, TuliproxErrorKind};
|
|
use crate::{create_tuliprox_error_result, handle_tuliprox_error_result_list, info_err};
|
|
use crate::foundation::filter::{get_filter, Filter};
|
|
use crate::model::{ClusterFlags, ConfigFavouritesDto, ConfigRenameDto, ConfigSortDto, HdHomeRunDeviceOverview, PatternTemplate, ProcessingOrder, StrmExportStyle, TargetType, TraktConfigDto};
|
|
use crate::utils::{default_as_true, default_resolve_delay_secs, default_as_default};
|
|
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Default, PartialEq)]
|
|
#[serde(deny_unknown_fields)]
|
|
pub struct ConfigTargetOptions {
|
|
#[serde(default)]
|
|
pub ignore_logo: bool,
|
|
#[serde(default)]
|
|
pub share_live_streams: bool,
|
|
#[serde(default)]
|
|
pub remove_duplicates: bool,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub force_redirect: Option<ClusterFlags>,
|
|
}
|
|
|
|
impl ConfigTargetOptions {
|
|
pub fn is_empty(&self) -> bool {
|
|
!self.ignore_logo
|
|
&& !self.share_live_streams
|
|
&& !self.remove_duplicates
|
|
&& (self.force_redirect.is_none() || self.force_redirect.is_some_and(|f| f.has_full_flags() || f.is_empty()))
|
|
}
|
|
}
|
|
|
|
#[allow(clippy::struct_excessive_bools)]
|
|
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq)]
|
|
#[serde(deny_unknown_fields)]
|
|
pub struct XtreamTargetOutputDto {
|
|
#[serde(default = "default_as_true")]
|
|
pub skip_live_direct_source: bool,
|
|
#[serde(default = "default_as_true")]
|
|
pub skip_video_direct_source: bool,
|
|
#[serde(default = "default_as_true")]
|
|
pub skip_series_direct_source: bool,
|
|
#[serde(default)]
|
|
pub resolve_series: bool,
|
|
#[serde(default = "default_resolve_delay_secs")]
|
|
pub resolve_series_delay: u16,
|
|
#[serde(default)]
|
|
pub resolve_vod: bool,
|
|
#[serde(default = "default_resolve_delay_secs")]
|
|
pub resolve_vod_delay: u16,
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub trakt: Option<TraktConfigDto>,
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub filter: Option<String>,
|
|
#[serde(skip)]
|
|
pub t_filter: Option<Filter>,
|
|
}
|
|
|
|
impl Default for XtreamTargetOutputDto {
|
|
fn default() -> Self {
|
|
XtreamTargetOutputDto {
|
|
skip_live_direct_source: default_as_true(),
|
|
skip_video_direct_source: default_as_true(),
|
|
skip_series_direct_source: default_as_true(),
|
|
resolve_series: false,
|
|
resolve_series_delay: default_resolve_delay_secs(),
|
|
resolve_vod: false,
|
|
resolve_vod_delay: default_resolve_delay_secs(),
|
|
trakt: None,
|
|
filter: None,
|
|
t_filter: None,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl XtreamTargetOutputDto {
|
|
pub fn prepare(&mut self) {
|
|
if let Some(trakt) = &mut self.trakt {
|
|
trakt.prepare();
|
|
}
|
|
}
|
|
|
|
pub fn has_any_option(&self) -> bool {
|
|
self.skip_live_direct_source
|
|
|| self.skip_video_direct_source
|
|
|| self.skip_series_direct_source
|
|
|| self.resolve_series
|
|
|| self.resolve_vod
|
|
|| self.trakt.is_some()
|
|
|| self.filter.is_some()
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Default, Clone, serde::Serialize, serde::Deserialize, PartialEq)]
|
|
#[serde(deny_unknown_fields)]
|
|
pub struct M3uTargetOutputDto {
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub filename: Option<String>,
|
|
#[serde(default)]
|
|
pub include_type_in_url: bool,
|
|
#[serde(default)]
|
|
pub mask_redirect_url: bool,
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub filter: Option<String>,
|
|
#[serde(skip)]
|
|
pub t_filter: Option<Filter>,
|
|
}
|
|
|
|
impl M3uTargetOutputDto {
|
|
pub fn has_any_option(&self) -> bool {
|
|
self.filename.is_some()
|
|
|| self.include_type_in_url
|
|
|| self.mask_redirect_url
|
|
|| self.filter.is_some()
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Default, Clone, serde::Serialize, serde::Deserialize, PartialEq)]
|
|
#[serde(deny_unknown_fields)]
|
|
pub struct StrmTargetOutputDto {
|
|
pub directory: String,
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub username: Option<String>,
|
|
#[serde(default)]
|
|
pub style: StrmExportStyle,
|
|
#[serde(default)]
|
|
pub flat: bool,
|
|
#[serde(default)]
|
|
pub underscore_whitespace: bool,
|
|
#[serde(default)]
|
|
pub cleanup: bool,
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub strm_props: Option<Vec<String>>,
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub filter: Option<String>,
|
|
#[serde(skip)]
|
|
pub t_filter: Option<Filter>,
|
|
#[serde(default)]
|
|
pub add_quality_to_filename: bool,
|
|
}
|
|
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq)]
|
|
#[serde(deny_unknown_fields)]
|
|
pub struct HdHomeRunTargetOutputDto {
|
|
pub device: String,
|
|
pub username: String,
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub use_output: Option<TargetType>,
|
|
}
|
|
|
|
impl Default for HdHomeRunTargetOutputDto {
|
|
fn default() -> Self {
|
|
Self {
|
|
device: String::new(),
|
|
username: String::new(),
|
|
use_output: Some(TargetType::M3u),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq)]
|
|
#[serde(deny_unknown_fields, tag = "type", rename_all = "lowercase")]
|
|
pub enum TargetOutputDto {
|
|
Xtream(XtreamTargetOutputDto),
|
|
M3u(M3uTargetOutputDto),
|
|
Strm(StrmTargetOutputDto),
|
|
HdHomeRun(HdHomeRunTargetOutputDto),
|
|
}
|
|
|
|
impl TargetOutputDto {
|
|
pub fn prepare(&mut self) {
|
|
match self {
|
|
TargetOutputDto::Xtream(output) => output.prepare(),
|
|
TargetOutputDto::M3u(_)
|
|
| TargetOutputDto::Strm(_)
|
|
| TargetOutputDto::HdHomeRun(_) => {}
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq)]
|
|
#[serde(deny_unknown_fields)]
|
|
pub struct ConfigTargetDto {
|
|
#[serde(default)]
|
|
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<ConfigSortDto>,
|
|
pub filter: String,
|
|
#[serde(default)]
|
|
pub output: Vec<TargetOutputDto>,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub rename: Option<Vec<ConfigRenameDto>>,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub mapping: Option<Vec<String>>,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub favourites: Option<Vec<ConfigFavouritesDto>>,
|
|
#[serde(default)]
|
|
pub processing_order: ProcessingOrder,
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub watch: Option<Vec<String>>,
|
|
#[serde(default)]
|
|
pub use_memory_cache: bool,
|
|
#[serde(skip)]
|
|
pub t_filter: Option<Filter>,
|
|
}
|
|
|
|
impl Default for ConfigTargetDto {
|
|
fn default() -> Self {
|
|
ConfigTargetDto {
|
|
id: 0,
|
|
enabled: default_as_true(),
|
|
name: default_as_default(),
|
|
options: None,
|
|
sort: None,
|
|
filter: String::new(),
|
|
output: Vec::new(),
|
|
rename: None,
|
|
mapping: None,
|
|
favourites: None,
|
|
processing_order: ProcessingOrder::default(),
|
|
watch: None,
|
|
use_memory_cache: false,
|
|
t_filter: None,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl ConfigTargetDto {
|
|
#[allow(clippy::too_many_lines)]
|
|
pub fn prepare(&mut self, id: u16, templates: Option<&Vec<PatternTemplate>>, hdhr_config: Option<&HdHomeRunDeviceOverview>) -> Result<(), TuliproxError> {
|
|
self.id = id;
|
|
if self.output.is_empty() {
|
|
return Err(info_err!(format!("Missing output format for {}", self.name)));
|
|
}
|
|
self.name = self.name.trim().to_string();
|
|
if self.name.is_empty() {
|
|
return create_tuliprox_error_result!(TuliproxErrorKind::Info, "target name required");
|
|
}
|
|
|
|
let mut m3u_cnt = 0;
|
|
let mut xtream_cnt = 0;
|
|
let mut strm_cnt = 0;
|
|
let mut strm_needs_xtream = false;
|
|
let mut hdhr_cnt = 0;
|
|
let mut hdhomerun_needs_m3u = false;
|
|
let mut hdhomerun_needs_xtream = false;
|
|
|
|
//let mut strm_export_styles = vec![];
|
|
let mut strm_directories: Vec<&str> = vec![];
|
|
|
|
for target_output in &mut self.output {
|
|
target_output.prepare();
|
|
match target_output {
|
|
TargetOutputDto::Xtream(_) => {
|
|
xtream_cnt += 1;
|
|
if default_as_default().eq_ignore_ascii_case(&self.name) {
|
|
return create_tuliprox_error_result!(TuliproxErrorKind::Info, "unique target name is required for xtream type output: {}", self.name);
|
|
}
|
|
}
|
|
TargetOutputDto::M3u(m3u_output) => {
|
|
m3u_cnt += 1;
|
|
m3u_output.filename = m3u_output.filename.as_ref().map(|s| s.trim().to_string());
|
|
}
|
|
TargetOutputDto::Strm(strm_output) => {
|
|
strm_cnt += 1;
|
|
strm_output.directory = strm_output.directory.trim().to_string();
|
|
if strm_output.directory.trim().is_empty() {
|
|
return create_tuliprox_error_result!(TuliproxErrorKind::Info, "directory is required for strm type: {}", self.name);
|
|
}
|
|
if let Some(username) = &mut strm_output.username {
|
|
*username = username.trim().to_string();
|
|
}
|
|
let has_username = strm_output.username.as_ref().is_some_and(|u| !u.is_empty());
|
|
|
|
if has_username {
|
|
strm_needs_xtream = true;
|
|
}
|
|
// if strm_export_styles.contains(&strm_output.style) {
|
|
// return create_tuliprox_error_result!(TuliproxErrorKind::Info, "strm outputs with same export style are not allowed: {}", self.name);
|
|
// }
|
|
// strm_export_styles.push(strm_output.style);
|
|
if strm_directories.contains(&strm_output.directory.as_str()) {
|
|
return create_tuliprox_error_result!(TuliproxErrorKind::Info, "strm outputs with same export directory are not allowed: {}", self.name);
|
|
}
|
|
strm_directories.push(strm_output.directory.as_str());
|
|
}
|
|
TargetOutputDto::HdHomeRun(hdhomerun_output) => {
|
|
hdhr_cnt += 1;
|
|
hdhomerun_output.username = hdhomerun_output.username.trim().to_string();
|
|
if hdhomerun_output.username.is_empty() {
|
|
return create_tuliprox_error_result!(TuliproxErrorKind::Info, "Username is required for HdHomeRun type: {}", self.name);
|
|
}
|
|
|
|
hdhomerun_output.device = hdhomerun_output.device.trim().to_string();
|
|
if hdhomerun_output.device.is_empty() {
|
|
return create_tuliprox_error_result!(TuliproxErrorKind::Info, "Device is required for HdHomeRun type: {}", self.name);
|
|
}
|
|
|
|
if let Some(use_output) = hdhomerun_output.use_output.as_ref() {
|
|
match &use_output {
|
|
TargetType::M3u => { hdhomerun_needs_m3u = true; }
|
|
TargetType::Xtream => { hdhomerun_needs_xtream = true; }
|
|
_ => return create_tuliprox_error_result!(TuliproxErrorKind::Info, "HdHomeRun output option `use_output` only accepts `m3u` or `xtream` for target: {}", self.name),
|
|
}
|
|
}
|
|
if let Some(hdhr_devices) = hdhr_config {
|
|
if !hdhr_devices.devices.contains(&hdhomerun_output.device) {
|
|
return create_tuliprox_error_result!(TuliproxErrorKind::Info, "HdHomeRun output device is not defined: {}", hdhomerun_output.device);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if m3u_cnt > 1 || xtream_cnt > 1 || hdhr_cnt > 1 {
|
|
return create_tuliprox_error_result!(TuliproxErrorKind::Info, "Multiple output formats with same type : {}", self.name);
|
|
}
|
|
|
|
if strm_cnt > 0 && strm_needs_xtream && xtream_cnt == 0 {
|
|
return create_tuliprox_error_result!(TuliproxErrorKind::Info, "strm output with a username is only permitted when used in combination with xtream output: {}", self.name);
|
|
}
|
|
|
|
if hdhr_cnt > 0 {
|
|
if xtream_cnt == 0 && m3u_cnt == 0 {
|
|
return create_tuliprox_error_result!(TuliproxErrorKind::Info, "HdHomeRun output is only permitted when used in combination with xtream or m3u output: {}", self.name);
|
|
}
|
|
if hdhomerun_needs_m3u && m3u_cnt == 0 {
|
|
return create_tuliprox_error_result!(TuliproxErrorKind::Info, "HdHomeRun output has `use_output=m3u` but no `m3u` output defined: {}", self.name);
|
|
}
|
|
if hdhomerun_needs_xtream && xtream_cnt == 0 {
|
|
return create_tuliprox_error_result!(TuliproxErrorKind::Info, "HdHomeRun output has `use_output=xtream` but no `xtream` output defined: {}", self.name);
|
|
}
|
|
|
|
if let Some(hdhr_devices) = hdhr_config {
|
|
if !hdhr_devices.enabled {
|
|
warn!("You have defined an HDHomeRun output, but HDHomeRun devices are disabled.");
|
|
}
|
|
}
|
|
}
|
|
|
|
if let Some(favourites) = self.favourites.as_mut() {
|
|
for favourite in favourites {
|
|
favourite.prepare(templates)?;
|
|
}
|
|
}
|
|
|
|
if let Some(watch) = &self.watch {
|
|
for pat in watch {
|
|
if let Err(err) = regex::Regex::new(pat) {
|
|
return create_tuliprox_error_result!(TuliproxErrorKind::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_tuliprox_error_result_list!(TuliproxErrorKind::Info, renames.iter_mut().map(|cr|cr.prepare(templates)));
|
|
}
|
|
if let Some(sort) = self.sort.as_mut() {
|
|
sort.prepare(templates)?;
|
|
}
|
|
Ok(())
|
|
}
|
|
Err(err) => Err(err),
|
|
}
|
|
}
|
|
} |