mirror of
https://github.com/euzu/tuliprox.git
synced 2026-10-10 09:52:18 +02:00
Feature/staged input and path improvements (#632)
* No template file not found error message * Setup wizard improvmenets * path improvements * staged input fix * path improvements * Provider dns resolved ips now writing to a file instead of source.yml * Playlist update hard kill after 1h * matadata update tmdb fix * Web UI fixes * GeoIp update schedule * batch:// prefix for aliases
This commit is contained in:
@@ -1,8 +1,52 @@
|
||||
use crate::utils::{get_default_web_root, is_blank_or_default_web_root};
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Default, PartialEq)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct ConfigApiDto {
|
||||
pub host: String,
|
||||
pub port: u16,
|
||||
#[serde(default, skip_serializing_if = "String::is_empty")]
|
||||
#[serde(default, skip_serializing_if = "is_blank_or_default_web_root")]
|
||||
pub web_root: String,
|
||||
}
|
||||
|
||||
impl ConfigApiDto {
|
||||
pub fn default_web_root() -> String { get_default_web_root() }
|
||||
|
||||
pub fn prepare(&mut self) {
|
||||
if self.web_root.trim().is_empty() {
|
||||
self.web_root = Self::default_web_root();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::ConfigApiDto;
|
||||
|
||||
#[test]
|
||||
fn serialization_skips_default_web_root() {
|
||||
let dto = ConfigApiDto { host: "0.0.0.0".to_string(), port: 8901, web_root: "./web".to_string() };
|
||||
|
||||
let serialized = serde_json::to_string(&dto).expect("api dto serialization should succeed");
|
||||
assert!(!serialized.contains("\"web_root\""), "expected default web_root to be skipped, got: {serialized}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn serialization_keeps_non_default_web_root() {
|
||||
let dto = ConfigApiDto { host: "0.0.0.0".to_string(), port: 8901, web_root: "/srv/tuliprox/web".to_string() };
|
||||
|
||||
let serialized = serde_json::to_string(&dto).expect("api dto serialization should succeed");
|
||||
assert!(
|
||||
serialized.contains("\"web_root\""),
|
||||
"expected non-default web_root to be persisted, got: {serialized}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prepare_sets_default_web_root_when_empty() {
|
||||
let mut dto = ConfigApiDto { host: "0.0.0.0".to_string(), port: 8901, web_root: String::new() };
|
||||
let expected_web_root = ConfigApiDto::default_web_root();
|
||||
dto.prepare();
|
||||
assert_eq!(dto.web_root, expected_web_root);
|
||||
}
|
||||
}
|
||||
|
||||
+200
-69
@@ -6,55 +6,18 @@ use crate::{
|
||||
WebUiConfigDto,
|
||||
},
|
||||
utils::{
|
||||
default_connect_timeout_secs, default_supported_video_extensions, is_blank_optional_string,
|
||||
is_default_connect_timeout_secs, is_false, CONFIG_PATH, MAPPING_FILE, TEMPLATE_FILE,
|
||||
default_connect_timeout_secs, default_custom_stream_response_path, default_default_user_agent,
|
||||
default_main_backup_dir, default_main_mapping_path, default_main_storage_dir, default_main_template_path,
|
||||
default_main_user_config_dir, default_supported_video_extensions, is_blank_optional_string,
|
||||
is_blank_or_default_backup_dir, is_blank_or_default_custom_stream_response_path,
|
||||
is_blank_or_default_mapping_path, is_blank_or_default_storage_dir, is_blank_or_default_template_path,
|
||||
is_blank_or_default_user_config_dir, is_default_connect_timeout_secs, is_false,
|
||||
is_none_or_empty_metadata_update, is_none_or_empty_video, normalize_optional_config_file_path,
|
||||
normalize_optional_dir, DEFAULT_BACKUP_DIR, DEFAULT_CUSTOM_STREAM_RESPONSE_PATH, DEFAULT_STORAGE_DIR,
|
||||
DEFAULT_USER_CONFIG_DIR, MAPPING_FILE, TEMPLATE_FILE,
|
||||
},
|
||||
};
|
||||
|
||||
pub const DEFAULT_USER_AGENT: &str = "VLC/3.0.16 LibVLC/3.0.16";
|
||||
|
||||
fn default_default_user_agent() -> Option<String> { Some(DEFAULT_USER_AGENT.to_string()) }
|
||||
fn default_main_mapping_path() -> Option<String> { Some(format!("./{CONFIG_PATH}/{MAPPING_FILE}")) }
|
||||
fn default_main_template_path() -> Option<String> { Some(format!("./{CONFIG_PATH}/{TEMPLATE_FILE}")) }
|
||||
|
||||
fn is_default_config_file_path(value: &str, file_name: &str) -> bool {
|
||||
let normalized = value.trim().replace('\\', "/");
|
||||
let normalized = normalized.trim_start_matches("./");
|
||||
normalized == file_name
|
||||
|| normalized.rsplit_once('/').is_some_and(|(dir, file)| dir == CONFIG_PATH && file == file_name)
|
||||
}
|
||||
|
||||
fn is_blank_or_default_mapping_path(path: &Option<String>) -> bool {
|
||||
path.as_ref().is_none_or(|value| value.trim().is_empty() || is_default_config_file_path(value, MAPPING_FILE))
|
||||
}
|
||||
|
||||
fn is_blank_or_default_template_path(path: &Option<String>) -> bool {
|
||||
path.as_ref().is_none_or(|value| value.trim().is_empty() || is_default_config_file_path(value, TEMPLATE_FILE))
|
||||
}
|
||||
|
||||
fn effective_main_mapping_path(path: Option<String>) -> Option<String> {
|
||||
if is_blank_or_default_mapping_path(&path) {
|
||||
default_main_mapping_path()
|
||||
} else {
|
||||
path
|
||||
}
|
||||
}
|
||||
|
||||
fn effective_main_template_path(path: Option<String>) -> Option<String> {
|
||||
if is_blank_or_default_template_path(&path) {
|
||||
default_main_template_path()
|
||||
} else {
|
||||
path
|
||||
}
|
||||
}
|
||||
|
||||
fn is_none_or_empty_video(video: &Option<VideoConfigDto>) -> bool {
|
||||
video.as_ref().is_none_or(VideoConfigDto::is_empty)
|
||||
}
|
||||
fn is_none_or_empty_metadata_update(metadata_update: &Option<MetadataUpdateConfigDto>) -> bool {
|
||||
metadata_update.as_ref().is_none_or(MetadataUpdateConfigDto::is_empty)
|
||||
}
|
||||
|
||||
#[allow(clippy::struct_excessive_bools)]
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
@@ -62,18 +25,22 @@ pub struct ConfigDto {
|
||||
#[serde(default, skip_serializing_if = "is_false")]
|
||||
pub process_parallel: bool,
|
||||
pub api: ConfigApiDto,
|
||||
pub working_dir: String,
|
||||
#[serde(default, alias = "working_dir", skip_serializing_if = "is_blank_or_default_storage_dir")]
|
||||
pub storage_dir: Option<String>,
|
||||
#[serde(default = "default_default_user_agent", skip_serializing_if = "is_blank_optional_string")]
|
||||
pub default_user_agent: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "is_blank_optional_string")]
|
||||
#[serde(default, skip_serializing_if = "is_blank_or_default_backup_dir")]
|
||||
pub backup_dir: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "is_blank_optional_string")]
|
||||
#[serde(default, skip_serializing_if = "is_blank_or_default_user_config_dir")]
|
||||
pub user_config_dir: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "is_blank_or_default_mapping_path")]
|
||||
pub mapping_path: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "is_blank_or_default_template_path")]
|
||||
pub template_path: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "is_blank_optional_string")]
|
||||
#[serde(
|
||||
default = "default_custom_stream_response_path",
|
||||
skip_serializing_if = "is_blank_or_default_custom_stream_response_path"
|
||||
)]
|
||||
pub custom_stream_response_path: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "is_none_or_empty_video")]
|
||||
pub video: Option<VideoConfigDto>,
|
||||
@@ -118,7 +85,7 @@ impl Default for ConfigDto {
|
||||
Self {
|
||||
process_parallel: false,
|
||||
api: ConfigApiDto::default(),
|
||||
working_dir: String::new(),
|
||||
storage_dir: None,
|
||||
default_user_agent: default_default_user_agent(),
|
||||
backup_dir: None,
|
||||
user_config_dir: None,
|
||||
@@ -153,12 +120,13 @@ impl Default for ConfigDto {
|
||||
pub struct MainConfigDto {
|
||||
#[serde(default, skip_serializing_if = "is_false")]
|
||||
pub process_parallel: bool,
|
||||
pub working_dir: String,
|
||||
#[serde(default = "default_main_storage_dir", skip_serializing_if = "is_blank_or_default_storage_dir")]
|
||||
pub storage_dir: Option<String>,
|
||||
#[serde(default = "default_default_user_agent", skip_serializing_if = "is_blank_optional_string")]
|
||||
pub default_user_agent: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "is_blank_optional_string")]
|
||||
#[serde(default = "default_main_backup_dir", skip_serializing_if = "is_blank_or_default_backup_dir")]
|
||||
pub backup_dir: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "is_blank_optional_string")]
|
||||
#[serde(default = "default_main_user_config_dir", skip_serializing_if = "is_blank_or_default_user_config_dir")]
|
||||
pub user_config_dir: Option<String>,
|
||||
#[serde(default = "default_main_mapping_path", skip_serializing_if = "is_blank_or_default_mapping_path")]
|
||||
pub mapping_path: Option<String>,
|
||||
@@ -187,10 +155,10 @@ impl Default for MainConfigDto {
|
||||
MainConfigDto {
|
||||
process_parallel: false,
|
||||
disk_based_processing: false,
|
||||
working_dir: String::new(),
|
||||
storage_dir: default_main_storage_dir(),
|
||||
default_user_agent: default_default_user_agent(),
|
||||
backup_dir: None,
|
||||
user_config_dir: None,
|
||||
backup_dir: default_main_backup_dir(),
|
||||
user_config_dir: default_main_user_config_dir(),
|
||||
mapping_path: default_main_mapping_path(),
|
||||
template_path: default_main_template_path(),
|
||||
custom_stream_response_path: None,
|
||||
@@ -209,12 +177,12 @@ impl From<&ConfigDto> for MainConfigDto {
|
||||
Self {
|
||||
process_parallel: config.process_parallel,
|
||||
disk_based_processing: config.disk_based_processing,
|
||||
working_dir: config.working_dir.clone(),
|
||||
storage_dir: config.storage_dir.clone(),
|
||||
default_user_agent: config.default_user_agent.clone(),
|
||||
backup_dir: config.backup_dir.clone(),
|
||||
user_config_dir: config.user_config_dir.clone(),
|
||||
mapping_path: effective_main_mapping_path(config.mapping_path.clone()),
|
||||
template_path: effective_main_template_path(config.template_path.clone()),
|
||||
mapping_path: config.mapping_path.clone(),
|
||||
template_path: config.template_path.clone(),
|
||||
custom_stream_response_path: config.custom_stream_response_path.clone(),
|
||||
user_access_control: config.user_access_control,
|
||||
connect_timeout_secs: config.connect_timeout_secs,
|
||||
@@ -249,9 +217,26 @@ pub struct HdHomeRunDeviceOverview {
|
||||
|
||||
impl ConfigDto {
|
||||
pub fn prepare(&mut self, include_computed: bool) -> Result<(), TuliproxError> {
|
||||
self.api.prepare();
|
||||
|
||||
if is_blank_optional_string(&self.default_user_agent) {
|
||||
self.default_user_agent = default_default_user_agent();
|
||||
}
|
||||
if is_blank_or_default_storage_dir(&self.storage_dir) {
|
||||
self.storage_dir = default_main_storage_dir();
|
||||
}
|
||||
if is_blank_or_default_backup_dir(&self.backup_dir) {
|
||||
self.backup_dir = default_main_backup_dir();
|
||||
}
|
||||
if is_blank_or_default_user_config_dir(&self.user_config_dir) {
|
||||
self.user_config_dir = default_main_user_config_dir();
|
||||
}
|
||||
if is_blank_or_default_mapping_path(&self.mapping_path) {
|
||||
self.mapping_path = default_main_mapping_path();
|
||||
}
|
||||
if is_blank_or_default_template_path(&self.template_path) {
|
||||
self.template_path = default_main_template_path();
|
||||
}
|
||||
|
||||
if let Some(mins) = self.sleep_timer_mins {
|
||||
if mins == 0 {
|
||||
@@ -268,7 +253,7 @@ impl ConfigDto {
|
||||
self.prepare_metadata_update_config()?;
|
||||
|
||||
if let Some(reverse_proxy) = self.reverse_proxy.as_mut() {
|
||||
reverse_proxy.prepare(&self.working_dir)?;
|
||||
reverse_proxy.prepare(self.storage_dir.as_deref().unwrap_or_default())?;
|
||||
}
|
||||
if let Some(proxy) = &mut self.proxy {
|
||||
proxy.prepare()?;
|
||||
@@ -280,6 +265,9 @@ impl ConfigDto {
|
||||
if let Some(messaging) = &mut self.messaging {
|
||||
messaging.prepare(include_computed)?;
|
||||
}
|
||||
if let Some(library) = &mut self.library {
|
||||
library.playlist.prepare();
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -361,13 +349,14 @@ impl ConfigDto {
|
||||
pub fn update_from_main_config(&mut self, main_config: &MainConfigDto) {
|
||||
self.process_parallel = main_config.process_parallel;
|
||||
self.disk_based_processing = main_config.disk_based_processing;
|
||||
self.working_dir = main_config.working_dir.clone();
|
||||
self.storage_dir = normalize_optional_dir(&main_config.storage_dir, DEFAULT_STORAGE_DIR);
|
||||
self.default_user_agent = main_config.default_user_agent.clone();
|
||||
self.backup_dir = main_config.backup_dir.clone();
|
||||
self.user_config_dir = main_config.user_config_dir.clone();
|
||||
self.mapping_path = main_config.mapping_path.clone();
|
||||
self.template_path = main_config.template_path.clone();
|
||||
self.custom_stream_response_path = main_config.custom_stream_response_path.clone();
|
||||
self.backup_dir = normalize_optional_dir(&main_config.backup_dir, DEFAULT_BACKUP_DIR);
|
||||
self.user_config_dir = normalize_optional_dir(&main_config.user_config_dir, DEFAULT_USER_CONFIG_DIR);
|
||||
self.mapping_path = normalize_optional_config_file_path(&main_config.mapping_path, MAPPING_FILE);
|
||||
self.template_path = normalize_optional_config_file_path(&main_config.template_path, TEMPLATE_FILE);
|
||||
self.custom_stream_response_path =
|
||||
normalize_optional_dir(&main_config.custom_stream_response_path, DEFAULT_CUSTOM_STREAM_RESPONSE_PATH);
|
||||
self.user_access_control = main_config.user_access_control;
|
||||
self.connect_timeout_secs = main_config.connect_timeout_secs;
|
||||
self.sleep_timer_mins = main_config.sleep_timer_mins;
|
||||
@@ -386,7 +375,7 @@ impl ConfigDto {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::utils::default_supported_video_extensions;
|
||||
use crate::utils::{default_supported_video_extensions, CONFIG_PATH};
|
||||
use serde_json::json;
|
||||
|
||||
#[test]
|
||||
@@ -425,6 +414,104 @@ mod tests {
|
||||
assert!(serialized.contains("\"video\""), "expected video field, got: {serialized}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn serializing_skips_default_storage_backup_and_user_config_dirs() {
|
||||
let cfg = ConfigDto {
|
||||
storage_dir: Some(DEFAULT_STORAGE_DIR.to_string()),
|
||||
backup_dir: Some(DEFAULT_BACKUP_DIR.to_string()),
|
||||
user_config_dir: Some(DEFAULT_USER_CONFIG_DIR.to_string()),
|
||||
..ConfigDto::default()
|
||||
};
|
||||
|
||||
let serialized = serde_json::to_string(&cfg).expect("config serialization should succeed");
|
||||
assert!(
|
||||
!serialized.contains("\"storage_dir\""),
|
||||
"expected no storage_dir field for default value, got: {serialized}"
|
||||
);
|
||||
assert!(
|
||||
!serialized.contains("\"backup_dir\""),
|
||||
"expected no backup_dir field for default value, got: {serialized}"
|
||||
);
|
||||
assert!(
|
||||
!serialized.contains("\"user_config_dir\""),
|
||||
"expected no user_config_dir field for default value, got: {serialized}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn serializing_keeps_non_default_storage_and_backup_dirs() {
|
||||
let cfg = ConfigDto {
|
||||
storage_dir: Some("custom-storage".to_string()),
|
||||
backup_dir: Some("custom-backup".to_string()),
|
||||
user_config_dir: Some("custom-user-config".to_string()),
|
||||
..ConfigDto::default()
|
||||
};
|
||||
|
||||
let serialized = serde_json::to_string(&cfg).expect("config serialization should succeed");
|
||||
assert!(
|
||||
serialized.contains("\"storage_dir\""),
|
||||
"expected storage_dir field for non-default value, got: {serialized}"
|
||||
);
|
||||
assert!(
|
||||
serialized.contains("\"backup_dir\""),
|
||||
"expected backup_dir field for non-default value, got: {serialized}"
|
||||
);
|
||||
assert!(
|
||||
serialized.contains("\"user_config_dir\""),
|
||||
"expected user_config_dir field for non-default value, got: {serialized}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn main_config_from_applies_default_storage_backup_and_user_config_dirs() {
|
||||
let mut cfg = ConfigDto::default();
|
||||
cfg.prepare(false).expect("prepare should succeed");
|
||||
let main = MainConfigDto::from(&cfg);
|
||||
assert_eq!(main.storage_dir.as_deref(), Some(DEFAULT_STORAGE_DIR));
|
||||
assert_eq!(main.backup_dir.as_deref(), Some(DEFAULT_BACKUP_DIR));
|
||||
assert_eq!(main.user_config_dir.as_deref(), Some(DEFAULT_USER_CONFIG_DIR));
|
||||
assert_eq!(main.mapping_path.as_deref(), Some(format!("./{CONFIG_PATH}/{MAPPING_FILE}").as_str()));
|
||||
assert_eq!(main.template_path.as_deref(), Some(format!("./{CONFIG_PATH}/{TEMPLATE_FILE}").as_str()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn update_from_main_config_omits_default_optional_paths() {
|
||||
let mut cfg = ConfigDto::default();
|
||||
let main = MainConfigDto {
|
||||
storage_dir: Some(DEFAULT_STORAGE_DIR.to_string()),
|
||||
backup_dir: Some(DEFAULT_BACKUP_DIR.to_string()),
|
||||
user_config_dir: Some(DEFAULT_USER_CONFIG_DIR.to_string()),
|
||||
mapping_path: Some(format!("./{CONFIG_PATH}/{MAPPING_FILE}")),
|
||||
template_path: Some(format!("./{CONFIG_PATH}/{TEMPLATE_FILE}")),
|
||||
..MainConfigDto::default()
|
||||
};
|
||||
|
||||
cfg.update_from_main_config(&main);
|
||||
assert!(cfg.storage_dir.is_none());
|
||||
assert!(cfg.backup_dir.is_none());
|
||||
assert!(cfg.user_config_dir.is_none());
|
||||
assert!(cfg.mapping_path.is_none());
|
||||
assert!(cfg.template_path.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prepare_sets_default_optional_paths() {
|
||||
let mut cfg = ConfigDto {
|
||||
storage_dir: None,
|
||||
backup_dir: None,
|
||||
user_config_dir: None,
|
||||
mapping_path: None,
|
||||
template_path: None,
|
||||
..ConfigDto::default()
|
||||
};
|
||||
cfg.prepare(false).expect("prepare should succeed");
|
||||
assert_eq!(cfg.storage_dir.as_deref(), Some(DEFAULT_STORAGE_DIR));
|
||||
assert_eq!(cfg.backup_dir.as_deref(), Some(DEFAULT_BACKUP_DIR));
|
||||
assert_eq!(cfg.user_config_dir.as_deref(), Some(DEFAULT_USER_CONFIG_DIR));
|
||||
assert_eq!(cfg.mapping_path.as_deref(), Some(format!("./{CONFIG_PATH}/{MAPPING_FILE}").as_str()));
|
||||
assert_eq!(cfg.template_path.as_deref(), Some(format!("./{CONFIG_PATH}/{TEMPLATE_FILE}").as_str()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deserializing_rejects_legacy_video_ffprobe_fields() {
|
||||
let raw = json!({
|
||||
@@ -433,7 +520,7 @@ mod tests {
|
||||
"port": 8901,
|
||||
"web_root": "./web"
|
||||
},
|
||||
"working_dir": ".",
|
||||
"storage_dir": ".",
|
||||
"video": {
|
||||
"extensions": ["mp4"],
|
||||
"ffprobe_enabled": true
|
||||
@@ -445,4 +532,48 @@ mod tests {
|
||||
let err = result.unwrap_err().to_string();
|
||||
assert!(err.contains("ffprobe_enabled"), "unexpected error text: {err}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deserializing_rejects_legacy_data_dir_alias() {
|
||||
let raw = json!({
|
||||
"api": {
|
||||
"host": "127.0.0.1",
|
||||
"port": 8901,
|
||||
"web_root": "./web"
|
||||
},
|
||||
"data_dir": "."
|
||||
});
|
||||
|
||||
let result: Result<ConfigDto, _> = serde_json::from_value(raw);
|
||||
assert!(result.is_err(), "data_dir should not deserialize");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deserializing_accepts_legacy_working_dir_alias() {
|
||||
let raw = json!({
|
||||
"api": {
|
||||
"host": "127.0.0.1",
|
||||
"port": 8901,
|
||||
"web_root": "./web"
|
||||
},
|
||||
"working_dir": "."
|
||||
});
|
||||
|
||||
let cfg: ConfigDto = serde_json::from_value(raw).expect("working_dir should deserialize as legacy alias");
|
||||
assert_eq!(cfg.storage_dir.as_deref(), Some("."));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deserializing_accepts_missing_storage_dir() {
|
||||
let raw = json!({
|
||||
"api": {
|
||||
"host": "127.0.0.1",
|
||||
"port": 8901,
|
||||
"web_root": "./web"
|
||||
}
|
||||
});
|
||||
|
||||
let cfg: ConfigDto = serde_json::from_value(raw).expect("missing storage_dir should deserialize");
|
||||
assert!(cfg.storage_dir.is_none());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
use crate::{
|
||||
error::TuliproxError,
|
||||
info_err_res,
|
||||
utils::{is_blank_optional_str, is_blank_optional_string, parse_size_base_2},
|
||||
utils::{
|
||||
is_blank_optional_str, is_blank_optional_string, is_blank_or_default_cache_dir, parse_size_base_2,
|
||||
DEFAULT_CACHE_DIR,
|
||||
},
|
||||
};
|
||||
use path_clean::PathClean;
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Default, PartialEq)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
@@ -13,27 +14,21 @@ pub struct CacheConfigDto {
|
||||
pub enabled: bool,
|
||||
#[serde(default, skip_serializing_if = "is_blank_optional_string")]
|
||||
pub size: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "is_blank_optional_string")]
|
||||
pub dir: Option<String>,
|
||||
#[serde(default, alias = "dir", skip_serializing_if = "is_blank_or_default_cache_dir")]
|
||||
pub directory: Option<String>,
|
||||
}
|
||||
|
||||
impl CacheConfigDto {
|
||||
pub fn is_empty(&self) -> bool {
|
||||
!self.enabled && is_blank_optional_str(self.size.as_deref()) && is_blank_optional_str(self.dir.as_deref())
|
||||
!self.enabled && is_blank_optional_str(self.size.as_deref()) && is_blank_optional_str(self.directory.as_deref())
|
||||
}
|
||||
|
||||
pub(crate) fn prepare(&mut self, working_dir: &str) -> Result<(), TuliproxError> {
|
||||
pub(crate) fn prepare(&mut self, _storage_dir: &str) -> Result<(), TuliproxError> {
|
||||
if self.enabled {
|
||||
let work_path = PathBuf::from(working_dir);
|
||||
match self.dir.as_ref() {
|
||||
None => self.dir = Some(work_path.join("cache").to_string_lossy().to_string()),
|
||||
Some(work_dir) => {
|
||||
let mut cache_dir = work_dir.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());
|
||||
}
|
||||
if is_blank_or_default_cache_dir(&self.directory) {
|
||||
self.directory = Some(DEFAULT_CACHE_DIR.to_string());
|
||||
} else if let Some(dir) = self.directory.as_ref() {
|
||||
self.directory = Some(dir.trim().to_string());
|
||||
}
|
||||
|
||||
if let Some(val) = self.size.as_ref() {
|
||||
@@ -50,3 +45,36 @@ impl CacheConfigDto {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn prepare_sets_default_cache_dir_when_enabled_and_missing() {
|
||||
let mut cache = CacheConfigDto { enabled: true, directory: None, size: None };
|
||||
cache.prepare("storage").expect("prepare should succeed");
|
||||
assert_eq!(cache.directory.as_deref(), Some(DEFAULT_CACHE_DIR));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prepare_keeps_custom_cache_dir_when_enabled() {
|
||||
let mut cache = CacheConfigDto { enabled: true, directory: Some("custom-cache".to_string()), size: None };
|
||||
cache.prepare("storage").expect("prepare should succeed");
|
||||
assert_eq!(cache.directory.as_deref(), Some("custom-cache"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn serializing_skips_default_cache_dir() {
|
||||
let cache = CacheConfigDto { enabled: true, directory: Some(DEFAULT_CACHE_DIR.to_string()), size: None };
|
||||
let serialized = serde_json::to_string(&cache).expect("cache serialization should succeed");
|
||||
assert!(!serialized.contains("\"directory\""), "expected no dir field for default value, got: {serialized}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn serializing_keeps_non_default_cache_dir() {
|
||||
let cache = CacheConfigDto { enabled: true, directory: Some("custom-cache".to_string()), size: None };
|
||||
let serialized = serde_json::to_string(&cache).expect("cache serialization should succeed");
|
||||
assert!(serialized.contains("\"directory\""), "expected dir field for custom value, got: {serialized}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,11 +10,10 @@ use crate::{
|
||||
deserialize_timestamp, get_credentials_from_url_str, get_trimmed_string, is_blank_optional_string,
|
||||
is_default_probe_delay_secs, is_default_probe_live_interval, is_default_resolve_delay_secs, is_false, is_true,
|
||||
is_zero_i16, is_zero_u16, parse_duration_seconds, parse_provider_scheme_url_parts, sanitize_sensitive_info,
|
||||
serialize_option_vec_flow_map_items, trim_last_slash, Internable, PROVIDER_SCHEME_PREFIX,
|
||||
serialize_option_vec_flow_map_items, trim_last_slash, Internable, BATCH_SCHEME_PREFIX, PROVIDER_SCHEME_PREFIX,
|
||||
},
|
||||
};
|
||||
use enum_iterator::Sequence;
|
||||
use indexmap::IndexMap;
|
||||
use log::warn;
|
||||
use std::{
|
||||
collections::{HashMap, HashSet},
|
||||
@@ -511,7 +510,7 @@ impl ConfigInputDto {
|
||||
|
||||
self.persist = get_trimmed_string(self.persist.as_deref());
|
||||
|
||||
if self.url.starts_with("file://") {
|
||||
if self.url.starts_with(BATCH_SCHEME_PREFIX) {
|
||||
match self.input_type {
|
||||
InputType::M3u => {
|
||||
self.input_type = InputType::M3uBatch;
|
||||
@@ -524,7 +523,10 @@ impl ConfigInputDto {
|
||||
} else if self.url.starts_with(PROVIDER_SCHEME_PREFIX)
|
||||
&& matches!(self.input_type, InputType::M3uBatch | InputType::XtreamBatch)
|
||||
{
|
||||
return info_err_res!("input type {} does not support provider:// URLs for batch definitions; use a local CSV path or file:// URL", self.input_type);
|
||||
return info_err_res!(
|
||||
"input type {} does not support provider:// URLs for batch definitions; use batch:// URL",
|
||||
self.input_type
|
||||
);
|
||||
}
|
||||
check_provider_scheme_url!(self.url, provider_names);
|
||||
|
||||
@@ -654,7 +656,10 @@ impl ConfigInputDto {
|
||||
if self.url.starts_with(PROVIDER_SCHEME_PREFIX)
|
||||
&& matches!(self.input_type, InputType::M3uBatch | InputType::XtreamBatch)
|
||||
{
|
||||
return info_err_res!("input type {} does not support provider:// URLs for batch definitions; use a local CSV path or file:// URL", self.input_type);
|
||||
return info_err_res!(
|
||||
"input type {} does not support provider:// URLs for batch definitions; use batch:// URL",
|
||||
self.input_type
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -791,8 +796,6 @@ pub struct ProviderDnsDto {
|
||||
pub keep_vhost: bool,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub overrides: Option<HashMap<String, Vec<IpAddr>>>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub resolved: Option<IndexMap<String, Vec<IpAddr>>>,
|
||||
#[serde(default, skip_serializing_if = "is_default_on_resolve_error")]
|
||||
pub on_resolve_error: OnResolveErrorPolicy,
|
||||
#[serde(default, skip_serializing_if = "is_default_on_connect_error")]
|
||||
@@ -809,7 +812,6 @@ impl Default for ProviderDnsDto {
|
||||
schemes: None,
|
||||
keep_vhost: false,
|
||||
overrides: None,
|
||||
resolved: None,
|
||||
on_resolve_error: OnResolveErrorPolicy::default(),
|
||||
on_connect_error: OnConnectErrorPolicy::default(),
|
||||
}
|
||||
@@ -1010,29 +1012,12 @@ mod tests {
|
||||
assert_eq!(overrides["example.com"].len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_provider_dns_resolved_deserializes() {
|
||||
let json = r#"{
|
||||
"name":"p1",
|
||||
"urls":["http://example.com"],
|
||||
"dns":{
|
||||
"enabled":true,
|
||||
"resolved":{"example.com":["203.0.113.10"]}
|
||||
}
|
||||
}"#;
|
||||
|
||||
let dto: ConfigProviderDto = serde_json::from_str(json).expect("provider json should parse");
|
||||
let dns = dto.dns.expect("dns should be present");
|
||||
let resolved = dns.resolved.expect("resolved must be deserialized");
|
||||
assert_eq!(resolved.get("example.com"), Some(&vec!["203.0.113.10".parse::<IpAddr>().expect("valid ip")]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prepare_switches_xtream_to_xtream_batch_when_alias_exists() {
|
||||
let mut dto = ConfigInputDto {
|
||||
name: "input_alias".intern(),
|
||||
input_type: InputType::Xtream,
|
||||
url: "file:///tmp/input_alias.csv".to_string(),
|
||||
url: "batch:///tmp/input_alias.csv".to_string(),
|
||||
aliases: Some(vec![ConfigInputAliasDto {
|
||||
id: 1,
|
||||
name: "alias_1".intern(),
|
||||
@@ -1046,7 +1031,7 @@ mod tests {
|
||||
};
|
||||
|
||||
dto.prepare_type().expect("prepare type should succeed");
|
||||
dto.prepare(0, true, &HashSet::new()).expect("prepare should succeed and infer batch type");
|
||||
dto.prepare(0, true, &HashSet::new()).expect("prepare should succeed and infer batch type from batch:// URL");
|
||||
assert_eq!(dto.input_type, InputType::XtreamBatch);
|
||||
}
|
||||
|
||||
@@ -1069,6 +1054,6 @@ mod tests {
|
||||
};
|
||||
|
||||
let err = dto.prepare(0, true, &HashSet::new()).expect_err("prepare must reject provider:// for batch input");
|
||||
assert!(err.to_string().contains("does not support provider:// URLs"));
|
||||
assert!(err.to_string().contains("does not support provider:// URLs"), "Error: {err}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -106,7 +106,7 @@ pub enum LibraryMetadataFormat {
|
||||
Nfo,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct LibraryPlaylistConfigDto {
|
||||
#[serde(default = "default_movie_category")]
|
||||
@@ -119,7 +119,7 @@ impl LibraryPlaylistConfigDto {
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.movie_category == default_movie_category() && self.series_category == default_series_category()
|
||||
}
|
||||
pub fn clean(&mut self) {
|
||||
pub fn prepare(&mut self) {
|
||||
if self.movie_category.trim().is_empty() {
|
||||
self.movie_category = default_movie_category();
|
||||
}
|
||||
@@ -127,10 +127,19 @@ impl LibraryPlaylistConfigDto {
|
||||
self.series_category = default_series_category();
|
||||
}
|
||||
}
|
||||
pub fn clean(&mut self) { self.prepare(); }
|
||||
}
|
||||
|
||||
impl Default for LibraryPlaylistConfigDto {
|
||||
fn default() -> Self {
|
||||
Self { movie_category: default_movie_category(), series_category: default_series_category() }
|
||||
}
|
||||
}
|
||||
|
||||
impl LibraryConfigDto {
|
||||
pub fn prepare(&mut self) -> Result<(), TuliproxError> {
|
||||
self.playlist.prepare();
|
||||
|
||||
// Validate enabled state
|
||||
if self.enabled && self.scan_directories.is_empty() {
|
||||
return info_err_res!("Library enabled but no scan_directories configured");
|
||||
@@ -146,3 +155,24 @@ impl LibraryConfigDto {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{default_movie_category, default_series_category, LibraryPlaylistConfigDto};
|
||||
|
||||
#[test]
|
||||
fn playlist_default_uses_default_categories() {
|
||||
let playlist = LibraryPlaylistConfigDto::default();
|
||||
assert_eq!(playlist.movie_category, default_movie_category());
|
||||
assert_eq!(playlist.series_category, default_series_category());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn playlist_prepare_sets_default_categories_when_empty() {
|
||||
let mut playlist =
|
||||
LibraryPlaylistConfigDto { movie_category: String::new(), series_category: " ".to_string() };
|
||||
playlist.prepare();
|
||||
assert_eq!(playlist.movie_category, default_movie_category());
|
||||
assert_eq!(playlist.series_category, default_series_category());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,11 @@ use crate::utils::{is_blank_optional_string, is_empty_optional_vec};
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct ConfigPaths {
|
||||
#[serde(default, skip_serializing_if = "String::is_empty")]
|
||||
pub home_path: String,
|
||||
pub config_path: String,
|
||||
#[serde(default, skip_serializing_if = "String::is_empty")]
|
||||
pub storage_path: String,
|
||||
pub config_file_path: String,
|
||||
pub sources_file_path: String,
|
||||
#[serde(default, skip_serializing_if = "is_blank_optional_string")]
|
||||
|
||||
@@ -89,7 +89,7 @@ impl ReverseProxyConfigDto {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn prepare(&mut self, working_dir: &str) -> Result<(), TuliproxError> {
|
||||
pub(crate) fn prepare(&mut self, storage_dir: &str) -> Result<(), TuliproxError> {
|
||||
self.rewrite_secret = self.rewrite_secret.trim().to_string();
|
||||
if !self.resource_rewrite_disabled {
|
||||
if self.rewrite_secret.is_empty() {
|
||||
@@ -106,7 +106,7 @@ impl ReverseProxyConfigDto {
|
||||
warn!("The cache is disabled because resource rewrite is disabled");
|
||||
cache.enabled = false;
|
||||
}
|
||||
cache.prepare(working_dir)?;
|
||||
cache.prepare(storage_dir)?;
|
||||
}
|
||||
|
||||
if let Some(rate_limit) = self.rate_limit.as_mut() {
|
||||
|
||||
@@ -3,6 +3,51 @@ pub enum ScheduleTaskType {
|
||||
#[default]
|
||||
PlaylistUpdate,
|
||||
LibraryScan,
|
||||
GeoIpUpdate,
|
||||
}
|
||||
|
||||
const fn default_schedule_task_type() -> ScheduleTaskType { ScheduleTaskType::PlaylistUpdate }
|
||||
|
||||
fn parse_schedule_task_type(value: &str) -> Option<ScheduleTaskType> {
|
||||
match value.trim().to_ascii_lowercase().as_str() {
|
||||
"playlistupdate" | "playlist_update" | "playlist-update" | "update" => Some(ScheduleTaskType::PlaylistUpdate),
|
||||
"libraryscan" | "library_scan" | "library-scan" | "scan" => Some(ScheduleTaskType::LibraryScan),
|
||||
"geoipupdate" | "geo_ip_update" | "geoip_update" | "geoip-update" | "geoip" => {
|
||||
Some(ScheduleTaskType::GeoIpUpdate)
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn deserialize_schedule_task_type<'de, D>(deserializer: D) -> Result<ScheduleTaskType, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
use serde::Deserialize as _;
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
#[serde(untagged)]
|
||||
enum CompatTaskType {
|
||||
Enum(ScheduleTaskType),
|
||||
String(String),
|
||||
}
|
||||
|
||||
let value = Option::<CompatTaskType>::deserialize(deserializer)?;
|
||||
match value {
|
||||
None => Ok(default_schedule_task_type()),
|
||||
Some(CompatTaskType::Enum(task_type)) => Ok(task_type),
|
||||
Some(CompatTaskType::String(raw)) => {
|
||||
if raw.trim().is_empty() {
|
||||
Ok(default_schedule_task_type())
|
||||
} else if let Some(task_type) = parse_schedule_task_type(&raw) {
|
||||
Ok(task_type)
|
||||
} else {
|
||||
Err(serde::de::Error::custom(format!(
|
||||
"invalid schedule type '{raw}', expected one of: PlaylistUpdate, LibraryScan, GeoIpUpdate"
|
||||
)))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Default, PartialEq, Eq)]
|
||||
@@ -10,8 +55,50 @@ pub enum ScheduleTaskType {
|
||||
pub struct ScheduleConfigDto {
|
||||
#[serde(default)]
|
||||
pub schedule: String,
|
||||
#[serde(default, rename = "type")]
|
||||
#[serde(
|
||||
default = "default_schedule_task_type",
|
||||
rename = "type",
|
||||
alias = "task_type",
|
||||
deserialize_with = "deserialize_schedule_task_type"
|
||||
)]
|
||||
pub task_type: ScheduleTaskType,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub targets: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{ScheduleConfigDto, ScheduleTaskType};
|
||||
|
||||
#[test]
|
||||
fn defaults_to_playlist_update_when_type_is_missing() {
|
||||
let dto: ScheduleConfigDto = serde_saphyr::from_str("schedule: \"0 0 * * * * *\"\ntargets: [\"main\"]")
|
||||
.expect("schedule dto should parse");
|
||||
assert_eq!(dto.task_type, ScheduleTaskType::PlaylistUpdate);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn defaults_to_playlist_update_when_type_is_null_or_empty() {
|
||||
let null_type: ScheduleConfigDto =
|
||||
serde_saphyr::from_str("schedule: \"0 0 * * * * *\"\ntype: ~").expect("null type should parse");
|
||||
let empty_type: ScheduleConfigDto =
|
||||
serde_saphyr::from_str("schedule: \"0 0 * * * * *\"\ntype: \"\"").expect("empty type should parse");
|
||||
|
||||
assert_eq!(null_type.task_type, ScheduleTaskType::PlaylistUpdate);
|
||||
assert_eq!(empty_type.task_type, ScheduleTaskType::PlaylistUpdate);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn accepts_legacy_task_type_aliases() {
|
||||
let legacy: ScheduleConfigDto = serde_saphyr::from_str("schedule: \"0 0 * * * * *\"\ntask_type: scan")
|
||||
.expect("legacy task_type alias should parse");
|
||||
assert_eq!(legacy.task_type, ScheduleTaskType::LibraryScan);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn accepts_geoip_update_aliases() {
|
||||
let legacy: ScheduleConfigDto =
|
||||
serde_saphyr::from_str("schedule: \"0 0 * * * * *\"\ntype: geoip").expect("geoip alias should parse");
|
||||
assert_eq!(legacy.task_type, ScheduleTaskType::GeoIpUpdate);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::{
|
||||
model::DEFAULT_USER_AGENT,
|
||||
utils::{default_as_true, default_trakt_fuzzy_threshold, is_true, TRAKT_API_KEY, TRAKT_API_URL, TRAKT_API_VERSION},
|
||||
use crate::utils::{
|
||||
default_as_true, default_trakt_fuzzy_threshold, is_true, DEFAULT_USER_AGENT, TRAKT_API_KEY, TRAKT_API_URL,
|
||||
TRAKT_API_VERSION,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{fmt, str::FromStr};
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
use crate::{
|
||||
error::TuliproxError,
|
||||
info_err_res,
|
||||
model::DEFAULT_USER_AGENT,
|
||||
utils::{
|
||||
default_supported_video_extensions, is_blank_optional_str, is_blank_optional_string,
|
||||
is_default_supported_video_extensions, is_false,
|
||||
default_download_dir, default_episode_pattern, default_supported_video_extensions, is_blank_optional_str,
|
||||
is_blank_optional_string, is_blank_or_default_download_dir, is_blank_or_default_episode_pattern,
|
||||
is_default_supported_video_extensions, is_false, DEFAULT_USER_AGENT,
|
||||
},
|
||||
};
|
||||
use std::{borrow::BorrowMut, collections::HashMap};
|
||||
@@ -14,12 +14,12 @@ use std::{borrow::BorrowMut, collections::HashMap};
|
||||
pub struct VideoDownloadConfigDto {
|
||||
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
|
||||
pub headers: HashMap<String, String>,
|
||||
#[serde(default, skip_serializing_if = "is_blank_optional_string")]
|
||||
#[serde(default = "default_download_dir", skip_serializing_if = "is_blank_or_default_download_dir")]
|
||||
pub directory: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "is_false")]
|
||||
pub organize_into_directories: bool,
|
||||
// TODO use ptt
|
||||
#[serde(default, skip_serializing_if = "is_blank_optional_string")]
|
||||
#[serde(default = "default_episode_pattern", skip_serializing_if = "is_blank_or_default_episode_pattern")]
|
||||
pub episode_pattern: Option<String>,
|
||||
}
|
||||
|
||||
@@ -27,8 +27,8 @@ impl VideoDownloadConfigDto {
|
||||
pub fn is_empty(&self) -> bool {
|
||||
!self.organize_into_directories
|
||||
&& self.headers.is_empty()
|
||||
&& is_blank_optional_str(self.directory.as_deref())
|
||||
&& is_blank_optional_str(self.episode_pattern.as_deref())
|
||||
&& is_blank_or_default_download_dir(&self.directory)
|
||||
&& is_blank_or_default_episode_pattern(&self.episode_pattern)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,11 +69,23 @@ impl VideoConfigDto {
|
||||
match &mut self.download {
|
||||
None => {}
|
||||
Some(downl) => {
|
||||
if is_blank_or_default_download_dir(&downl.directory) {
|
||||
downl.directory = default_download_dir();
|
||||
} else if let Some(directory) = downl.directory.as_ref() {
|
||||
downl.directory = Some(directory.trim().to_string());
|
||||
}
|
||||
|
||||
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(), DEFAULT_USER_AGENT.to_string());
|
||||
}
|
||||
|
||||
if is_blank_or_default_episode_pattern(&downl.episode_pattern) {
|
||||
downl.episode_pattern = default_episode_pattern();
|
||||
} else if let Some(episode_pattern) = downl.episode_pattern.as_ref() {
|
||||
downl.episode_pattern = Some(episode_pattern.trim().to_string());
|
||||
}
|
||||
|
||||
if let Some(episode_pattern) = &downl.episode_pattern {
|
||||
if let Err(err) = crate::model::REGEX_CACHE.get_or_compile(episode_pattern) {
|
||||
return info_err_res!("can't parse regex: {episode_pattern} {err}");
|
||||
@@ -84,3 +96,102 @@ impl VideoConfigDto {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::utils::DEFAULT_DOWNLOAD_DIR;
|
||||
|
||||
#[test]
|
||||
fn prepare_sets_default_download_dir_when_missing() {
|
||||
let mut video = VideoConfigDto {
|
||||
extensions: Vec::new(),
|
||||
download: Some(VideoDownloadConfigDto {
|
||||
headers: HashMap::new(),
|
||||
directory: None,
|
||||
organize_into_directories: false,
|
||||
episode_pattern: None,
|
||||
}),
|
||||
web_search: None,
|
||||
};
|
||||
video.prepare().expect("prepare should succeed");
|
||||
let download = video.download.expect("download should exist");
|
||||
assert_eq!(download.directory.as_deref(), Some(DEFAULT_DOWNLOAD_DIR));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prepare_sets_default_episode_pattern_when_missing() {
|
||||
let mut video = VideoConfigDto {
|
||||
extensions: Vec::new(),
|
||||
download: Some(VideoDownloadConfigDto {
|
||||
headers: HashMap::new(),
|
||||
directory: None,
|
||||
organize_into_directories: false,
|
||||
episode_pattern: None,
|
||||
}),
|
||||
web_search: None,
|
||||
};
|
||||
video.prepare().expect("prepare should succeed");
|
||||
let download = video.download.expect("download should exist");
|
||||
assert!(download.episode_pattern.is_some(), "expected default episode pattern to be set");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prepare_keeps_custom_download_dir() {
|
||||
let mut video = VideoConfigDto {
|
||||
extensions: Vec::new(),
|
||||
download: Some(VideoDownloadConfigDto {
|
||||
headers: HashMap::new(),
|
||||
directory: Some("custom-downloads".to_string()),
|
||||
organize_into_directories: false,
|
||||
episode_pattern: None,
|
||||
}),
|
||||
web_search: None,
|
||||
};
|
||||
video.prepare().expect("prepare should succeed");
|
||||
let download = video.download.expect("download should exist");
|
||||
assert_eq!(download.directory.as_deref(), Some("custom-downloads"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn serializing_skips_default_download_dir() {
|
||||
let download = VideoDownloadConfigDto {
|
||||
headers: HashMap::new(),
|
||||
directory: Some(DEFAULT_DOWNLOAD_DIR.to_string()),
|
||||
organize_into_directories: false,
|
||||
episode_pattern: None,
|
||||
};
|
||||
let serialized = serde_json::to_string(&download).expect("download serialization should succeed");
|
||||
assert!(
|
||||
!serialized.contains("\"directory\""),
|
||||
"expected no directory field for default value, got: {serialized}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn serializing_keeps_custom_download_dir() {
|
||||
let download = VideoDownloadConfigDto {
|
||||
headers: HashMap::new(),
|
||||
directory: Some("custom-downloads".to_string()),
|
||||
organize_into_directories: false,
|
||||
episode_pattern: None,
|
||||
};
|
||||
let serialized = serde_json::to_string(&download).expect("download serialization should succeed");
|
||||
assert!(serialized.contains("\"directory\""), "expected directory field for custom value, got: {serialized}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn serializing_skips_default_episode_pattern() {
|
||||
let download = VideoDownloadConfigDto {
|
||||
headers: HashMap::new(),
|
||||
directory: None,
|
||||
organize_into_directories: false,
|
||||
episode_pattern: default_episode_pattern(),
|
||||
};
|
||||
let serialized = serde_json::to_string(&download).expect("download serialization should succeed");
|
||||
assert!(
|
||||
!serialized.contains("\"episode_pattern\""),
|
||||
"expected no episode_pattern field for default value, got: {serialized}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use crate::utils::{
|
||||
default_as_true, default_token_ttl_mins, is_blank_optional_str, is_blank_optional_string,
|
||||
default_as_true, default_token_ttl_mins, default_user_file_path, is_blank_or_default_user_file_path,
|
||||
is_default_token_ttl_mins, is_true,
|
||||
};
|
||||
|
||||
@@ -12,7 +12,7 @@ pub struct WebAuthConfigDto {
|
||||
pub secret: String,
|
||||
#[serde(default = "default_token_ttl_mins", skip_serializing_if = "is_default_token_ttl_mins")]
|
||||
pub token_ttl_mins: u32,
|
||||
#[serde(default, skip_serializing_if = "is_blank_optional_string")]
|
||||
#[serde(default = "default_user_file_path", skip_serializing_if = "is_blank_or_default_user_file_path")]
|
||||
pub userfile: Option<String>,
|
||||
}
|
||||
|
||||
@@ -35,6 +35,6 @@ impl WebAuthConfigDto {
|
||||
&& self.token_ttl_mins == empty.token_ttl_mins
|
||||
&& self.issuer.trim().is_empty()
|
||||
&& self.secret.trim().is_empty()
|
||||
&& is_blank_optional_str(self.userfile.as_deref())
|
||||
&& is_blank_or_default_user_file_path(&self.userfile)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user