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:
euzu
2026-03-06 17:29:42 +01:00
committed by GitHub
parent c5632d0c98
commit 737d04d08b
82 changed files with 2506 additions and 1433 deletions
+40 -35
View File
@@ -22,7 +22,6 @@ use shared::model::{
MsgKind, PatternTemplate, SourcesConfigDto, TargetUserDto, TemplateDefinitionDto,
};
use shared::utils::{generate_default_access_secret, generate_default_encrypt_secret, CONSTANTS, PROVIDER_SCHEME_PREFIX, TEMPLATE_FILE};
use std::borrow::Cow;
use std::collections::{HashMap, HashSet};
use std::env;
use std::fs::File;
@@ -112,14 +111,19 @@ async fn parse_sources_file_from_path(
.map_err(|join_err| info_err!("Failed to read sources-config file: {join_err}"))?
}
pub fn resolve_template_and_mapping_paths<'a>(paths: &'a ConfigPaths, template_path: Option<&'a str>, mapping_path: Option<&'a str>) -> (Cow<'a, str>, Cow<'a, str>) {
// Resolve effective paths for templates and mappings (with robust fallbacks)
let effective_template_path = paths.template_file_path.as_deref()
.or(template_path).map_or_else(|| Cow::Owned(utils::get_default_templates_path(&paths.config_path)), Cow::Borrowed);
let effective_mapping_path = paths.mapping_file_path.as_deref()
.or(mapping_path).map_or_else(|| Cow::Owned(utils::get_default_mappings_path(&paths.config_path)), Cow::Borrowed);
pub fn resolve_template_and_mapping_paths(
paths: &ConfigPaths,
template_path: Option<&str>,
mapping_path: Option<&str>,
) -> (String, String) {
let effective_template_path = utils::resolve_template_file_path(
paths.config_path.as_str(),
paths.template_file_path.as_deref().or(template_path),
);
let effective_mapping_path = utils::resolve_mapping_file_path(
paths.config_path.as_str(),
paths.mapping_file_path.as_deref().or(mapping_path),
);
(effective_template_path, effective_mapping_path)
}
@@ -322,14 +326,15 @@ pub async fn read_app_config_dto(
let config = read_config_file(config_file, resolve_env, include_computed)?;
// Resolve effective paths for templates and mappings (with robust fallbacks)
let (effective_template_path,effective_mapping_path) = resolve_template_and_mapping_paths(paths, config.template_path.as_deref(), config.mapping_path.as_deref());
let (effective_template_path, effective_mapping_path) =
resolve_template_and_mapping_paths(paths, config.template_path.as_deref(), config.mapping_path.as_deref());
let mut sources = parse_sources_file_from_path(&PathBuf::from(sources_file), resolve_env).await?;
let mut mappings = read_mappings_file_unprepared(effective_mapping_path.as_ref(), resolve_env)?
let mut mappings = read_mappings_file_unprepared(&effective_mapping_path, resolve_env)?
.map(|(_, mapping)| mapping);
let template_bundle = read_templates(
Some(effective_template_path.as_ref()),
Some(&effective_template_path),
resolve_env,
sources.templates.as_deref(),
mappings
@@ -447,7 +452,7 @@ pub async fn get_batch_aliases(
if url.starts_with(PROVIDER_SCHEME_PREFIX) {
return info_err_res!(
"Batch input type '{input_type}' does not support provider:// URLs. \
Use a local CSV path (absolute/relative) or file:// URL."
Use a batch:// URL or a local CSV path (absolute/relative)."
);
}
@@ -505,26 +510,25 @@ pub async fn read_initial_app_config(
let config_dto = read_config_file(config_file, resolve_env, include_computed)?;
if paths.mapping_file_path.is_none() {
let mut path = config_dto.mapping_path.as_ref().map_or_else(
|| utils::get_default_mappings_path(config_path),
ToString::to_string,
);
if resolve_env {
path = resolve_env_var(&path);
}
paths.mapping_file_path.replace(path);
}
if paths.template_file_path.is_none() {
let mut path = config_dto.template_path.as_ref().map_or_else(
|| utils::get_default_templates_path(config_path),
ToString::to_string,
);
if resolve_env {
path = resolve_env_var(&path);
}
paths.template_file_path.replace(path);
}
let configured_mapping_path = paths
.mapping_file_path
.as_deref()
.or(config_dto.mapping_path.as_deref())
.map(|path| if resolve_env { resolve_env_var(path) } else { path.to_string() });
let configured_template_path = paths
.template_file_path
.as_deref()
.or(config_dto.template_path.as_deref())
.map(|path| if resolve_env { resolve_env_var(path) } else { path.to_string() });
paths.mapping_file_path = Some(utils::resolve_mapping_file_path(
config_path,
configured_mapping_path.as_deref(),
));
paths.template_file_path = Some(utils::resolve_template_file_path(
config_path,
configured_template_path.as_deref(),
));
let mut sources_dto = parse_sources_file_from_path(&PathBuf::from(sources_file), resolve_env).await?;
@@ -539,7 +543,7 @@ pub async fn read_initial_app_config(
};
let template_bundle = read_templates(
paths.template_file_path.as_deref().or(config_dto.template_path.as_deref()),
paths.template_file_path.as_deref(),
resolve_env,
sources_dto.templates.as_deref(),
mappings_dto
@@ -562,7 +566,8 @@ pub async fn read_initial_app_config(
let sources: SourcesConfig = SourcesConfig::try_from(sources_dto)?;
let mut config: Config = Config::from(config_dto);
config.prepare(config_path)?;
config.prepare(config_path, paths.home_path.as_str())?;
paths.storage_path.clone_from(&config.storage_dir);
config.update_runtime();
let mut app_config = AppConfig {
+117 -25
View File
@@ -67,17 +67,27 @@ pub fn get_home_path() -> PathBuf {
}
pub fn get_default_web_root_path() -> PathBuf {
if let Some(web_root) = env::var(DEFAULT_WEB_ROOT_ENV_VAR)
.ok()
.filter(|p| !p.trim().is_empty()) {
return PathBuf::from(web_root)
get_default_web_root_path_for_home(get_home_path().as_path())
}
pub fn get_default_web_root_path_for_home(home_path: &Path) -> PathBuf {
if let Some(web_root) = env::var(DEFAULT_WEB_ROOT_ENV_VAR).ok().filter(|p| !p.trim().is_empty()) {
let configured = PathBuf::from(web_root);
return if configured.is_absolute() {
configured
} else {
home_path.join(configured)
};
}
get_exe_path().join(DEFAULT_WEB_DIR)
get_default_path_for_home(home_path, DEFAULT_WEB_DIR)
}
pub fn get_default_path(file: &str) -> PathBuf {
let home_path = get_home_path();
get_default_path_for_home(get_home_path().as_path(), file)
}
pub fn get_default_path_for_home(home_path: &Path, file: &str) -> PathBuf {
home_path.join(file)
}
@@ -116,11 +126,52 @@ pub fn get_default_templates_path(config_path: &str) -> String {
get_default_file_path(config_path, TEMPLATE_FILE)
}
fn resolve_config_scoped_path(config_path: &str, candidate: &str) -> String {
let candidate_path = PathBuf::from(candidate.trim()).clean();
if candidate_path.is_absolute() {
return candidate_path.to_string_lossy().to_string();
}
let config_dir = PathBuf::from(config_path).clean();
let relative = if let Some(config_dir_name) = config_dir.file_name() {
candidate_path
.strip_prefix(Path::new(config_dir_name))
.map_or_else(|_| candidate_path.clone(), Path::to_path_buf)
} else {
candidate_path.clone()
};
config_dir.join(relative).clean().to_string_lossy().to_string()
}
#[inline]
pub fn resolve_mapping_file_path(config_path: &str, mapping_path: Option<&str>) -> String {
let configured = mapping_path.map(str::trim).filter(|path| !path.is_empty()).map(ToString::to_string);
let candidate = if shared::utils::is_blank_or_default_mapping_path(&configured) {
get_default_mappings_path(config_path)
} else {
configured.unwrap_or_else(|| get_default_mappings_path(config_path))
};
resolve_config_scoped_path(config_path, &candidate)
}
#[inline]
pub fn resolve_template_file_path(config_path: &str, template_path: Option<&str>) -> String {
let configured = template_path.map(str::trim).filter(|path| !path.is_empty()).map(ToString::to_string);
let candidate = if shared::utils::is_blank_or_default_template_path(&configured) {
get_default_templates_path(config_path)
} else {
configured.unwrap_or_else(|| get_default_templates_path(config_path))
};
resolve_config_scoped_path(config_path, &candidate)
}
#[inline]
pub fn resolve_template_persist_file_path(template_path: Option<&str>, config_path: &str) -> String {
let candidate = template_path
let raw_candidate = template_path
.filter(|path| !path.trim().is_empty())
.map_or_else(|| get_default_templates_path(config_path), ToString::to_string);
let candidate = resolve_template_file_path(config_path, Some(raw_candidate.as_str()));
let path = PathBuf::from(&candidate);
if path.exists() {
@@ -130,8 +181,8 @@ pub fn resolve_template_persist_file_path(template_path: Option<&str>, config_pa
return path.to_string_lossy().to_string();
}
let looks_like_directory = candidate.ends_with('/')
|| candidate.ends_with('\\')
let looks_like_directory = raw_candidate.ends_with('/')
|| raw_candidate.ends_with('\\')
|| path.extension().is_none()
|| path.extension().is_some_and(|ext| ext == "d");
@@ -329,11 +380,11 @@ where
Ok(())
}
pub fn prepare_file_path(persist: Option<&str>, working_dir: &str, action: &str) -> Option<PathBuf> {
pub fn prepare_file_path(persist: Option<&str>, storage_dir: &str, action: &str) -> Option<PathBuf> {
let persist_file: Option<PathBuf> =
persist.map(|persist_path| prepare_persist_path(persist_path, action));
if persist_file.is_some() {
let file_path = get_file_path(working_dir, persist_file);
let file_path = get_file_path(storage_dir, persist_file);
debug_if_enabled!("persist to file: {}", file_path.as_ref().map_or(Cow::from("?"), |p| p.to_string_lossy()));
file_path
} else {
@@ -348,27 +399,28 @@ pub fn read_file_as_bytes(path: &Path) -> std::io::Result<Vec<u8>> {
Ok(buffer)
}
pub fn make_absolute_path(path: &str, working_dir: &str) -> String {
pub fn make_absolute_path(path: &str, storage_dir: &str) -> String {
let rpb = std::path::PathBuf::from(path);
let pathbuf = make_path_absolute(&rpb, working_dir);
let pathbuf = make_path_absolute(&rpb, storage_dir);
pathbuf.to_str().unwrap_or_default().to_string()
}
pub fn make_path_absolute(rpb: &Path, working_dir: &str) -> PathBuf {
pub fn make_path_absolute(rpb: &Path, storage_dir: &str) -> PathBuf {
if rpb.is_relative() {
let mut rpb2 = std::path::PathBuf::from(working_dir).join(rpb);
if !rpb2.exists() {
rpb2 = get_exe_path().join(rpb);
if !storage_dir.trim().is_empty() {
return std::path::PathBuf::from(storage_dir).join(rpb).clean();
}
if !rpb2.exists() {
let cwd = std::env::current_dir();
if let Ok(cwd_path) = cwd {
rpb2 = cwd_path.join(rpb);
}
let exe_based = get_exe_path().join(rpb).clean();
if exe_based.is_absolute() {
return exe_based;
}
if rpb2.exists() {
return rpb2.clean();
if let Ok(cwd_path) = std::env::current_dir() {
return cwd_path.join(rpb).clean();
}
return exe_based;
}
rpb.to_path_buf()
}
@@ -473,7 +525,11 @@ pub fn get_file_extension(path: &str) -> Option<String> {
#[cfg(test)]
mod tests {
use super::normalize_string_path;
use super::{
normalize_string_path, resolve_mapping_file_path, resolve_template_file_path,
resolve_template_persist_file_path,
};
use std::path::PathBuf;
#[test]
fn test_simple_relative_path() {
@@ -509,4 +565,40 @@ mod tests {
let normalized = normalize_string_path(input);
assert_eq!(normalized, "");
}
#[test]
fn test_resolve_template_default_path_uses_config_dir() {
let config_path = std::env::temp_dir().join("tuliprox").join("settings").join("config");
let resolved = resolve_template_file_path(
config_path.to_string_lossy().as_ref(),
Some("./config/template.yml"),
);
let expected = config_path.join("template.yml");
assert_eq!(PathBuf::from(resolved), expected);
}
#[test]
fn test_resolve_template_directory_path_uses_config_dir() {
let config_path = std::env::temp_dir().join("tuliprox").join("settings").join("config");
let resolved = resolve_template_file_path(config_path.to_string_lossy().as_ref(), Some("template.d"));
let expected = config_path.join("template.d");
assert_eq!(PathBuf::from(resolved), expected);
}
#[test]
fn test_resolve_mapping_directory_path_uses_config_dir() {
let config_path = std::env::temp_dir().join("tuliprox").join("settings").join("config");
let resolved = resolve_mapping_file_path(config_path.to_string_lossy().as_ref(), Some("mapping.d"));
let expected = config_path.join("mapping.d");
assert_eq!(PathBuf::from(resolved), expected);
}
#[test]
fn test_resolve_template_persist_file_for_directory_path() {
let config_path = std::env::temp_dir().join("tuliprox").join("settings").join("config");
let resolved =
resolve_template_persist_file_path(Some("template.d"), config_path.to_string_lossy().as_ref());
let expected = config_path.join("template.d").join("template.yml");
assert_eq!(PathBuf::from(resolved), expected);
}
}
+45 -18
View File
@@ -1,28 +1,47 @@
use crate::utils::traverse_dir;
use crate::utils::{config_file_reader, open_file};
use log::warn;
use log::{debug, warn};
use shared::error::{info_err_res, TuliproxError};
use shared::info_err;
use shared::model::TemplateDefinitionDto;
use std::io::ErrorKind;
use std::path::{Path, PathBuf};
fn read_template_definition(
template_file: &Path,
resolve_var: bool,
) -> Result<Option<TemplateDefinitionDto>, TuliproxError> {
if let Ok(file) = open_file(template_file) {
let maybe_definition: Result<TemplateDefinitionDto, _> =
serde_saphyr::from_reader(config_file_reader(file, resolve_var));
return match maybe_definition {
Ok(definition) => Ok(Some(definition)),
Err(err) => info_err_res!("{err}"),
};
match open_file(template_file) {
Ok(file) => {
let maybe_definition: Result<TemplateDefinitionDto, _> =
serde_saphyr::from_reader(config_file_reader(file, resolve_var));
match maybe_definition {
Ok(definition) => Ok(Some(definition)),
Err(err) => info_err_res!(
"Failed to parse template file {}: {err}",
template_file.to_string_lossy()
),
}
}
Err(err) => {
if err.kind() == ErrorKind::NotFound {
debug!(
"Optional template file not found: {}",
template_file.to_str().unwrap_or("?")
);
Ok(None)
} else {
warn!(
"Can't read template file {}: {err}",
template_file.to_str().unwrap_or("?")
);
info_err_res!(
"Can't read template file {}: {err}",
template_file.to_string_lossy()
)
}
}
}
warn!(
"Can't read template file: {}",
template_file.to_str().unwrap_or("?")
);
Ok(None)
}
fn merge_template_definitions(definitions: Vec<TemplateDefinitionDto>) -> Option<TemplateDefinitionDto> {
@@ -104,11 +123,19 @@ pub fn read_templates_file(
}
}
Err(err) => {
warn!(
"Can't read template path metadata for {}: {err}",
path.to_string_lossy()
);
Ok(None)
if err.kind() == ErrorKind::NotFound {
debug!("Optional template path not found: {}", path.to_string_lossy());
Ok(None)
} else {
warn!(
"Can't read template path metadata for {}: {err}",
path.to_string_lossy()
);
info_err_res!(
"Can't read template path metadata for {}: {err}",
path.to_string_lossy()
)
}
}
}
}
+8 -8
View File
@@ -11,28 +11,28 @@ use shared::error::{info_err, TuliproxError};
use shared::utils::{sanitize_sensitive_info, short_hash};
use std::path::PathBuf;
pub async fn get_input_raw_epg_file_path(url: &str, input: &ConfigInput, working_dir: &str) -> std::io::Result<PathBuf> {
pub async fn get_input_raw_epg_file_path(url: &str, input: &ConfigInput, storage_dir: &str) -> std::io::Result<PathBuf> {
let file_prefix = short_hash(url);
if let Some(persist_path) = input.persist.as_deref() {
if !persist_path.is_empty() {
if let Some(path) = prepare_file_path(input.persist.as_deref(), working_dir, "")
if let Some(path) = prepare_file_path(input.persist.as_deref(), storage_dir, "")
.map(|path| add_prefix_to_filename(&path, concat_string!(&file_prefix, "_epg_").as_str(), Some("xml"))) {
return Ok(path);
}
}
}
let download_path = get_input_storage_path(&input.name, working_dir).await?;
let download_path = get_input_storage_path(&input.name, storage_dir).await?;
Ok(download_path.join(format!("{}_{}", file_prefix, storage_const::FILE_EPG)))
}
async fn download_epg_file(url: &str, ctx: &PlaylistProcessingContext,
input: &ConfigInput,
headers: Option<&reqwest::header::HeaderMap>,
working_dir: &str) -> Result<PathBuf, TuliproxError> {
storage_dir: &str) -> Result<PathBuf, TuliproxError> {
debug!("Getting epg file path for url: {}", sanitize_sensitive_info(url));
let persist_file_path = get_input_raw_epg_file_path(url, input, working_dir).await.map_err(|e| info_err!("Could not access epg file download directory: {}", e))?;
let persist_file_path = get_input_raw_epg_file_path(url, input, storage_dir).await.map_err(|e| info_err!("Could not access epg file download directory: {}", e))?;
if input.cache_duration_seconds > 0 {
if let Ok(metadata) = tokio::fs::metadata(&persist_file_path).await {
@@ -55,7 +55,7 @@ async fn download_epg_file(url: &str, ctx: &PlaylistProcessingContext,
return Ok(persist_file_path);
}
debug!("Downloading epg for input '{}'", input.name);
match request::get_input_epg_content_as_file(&ctx.config, &ctx.client, input, headers, working_dir, url, &persist_file_path).await {
match request::get_input_epg_content_as_file(&ctx.config, &ctx.client, input, headers, storage_dir, url, &persist_file_path).await {
Ok(path) => {
ctx.mark_input_downloaded(lock_key.clone()).await;
Ok(path)
@@ -66,7 +66,7 @@ async fn download_epg_file(url: &str, ctx: &PlaylistProcessingContext,
pub async fn get_xmltv(ctx: &PlaylistProcessingContext, input: &ConfigInput,
headers: Option<&reqwest::header::HeaderMap>,
working_dir: &str) -> (Option<TVGuide>, Vec<TuliproxError>) {
storage_dir: &str) -> (Option<TVGuide>, Vec<TuliproxError>) {
match &input.epg {
None => (None, vec![]),
Some(epg_config) => {
@@ -75,7 +75,7 @@ pub async fn get_xmltv(ctx: &PlaylistProcessingContext, input: &ConfigInput,
let mut stored_file_paths = vec![];
for epg_source in &epg_config.sources {
match download_epg_file(&epg_source.url, ctx, input, headers, working_dir).await {
match download_epg_file(&epg_source.url, ctx, input, headers, storage_dir).await {
Ok(file_path) => {
stored_file_paths.push(file_path.clone());
file_paths.push(PersistedEpgSource { file_path, priority: epg_source.priority, logo_override: epg_source.logo_override });
+3 -3
View File
@@ -12,19 +12,19 @@ pub async fn download_m3u_playlist(
cfg: &Arc<Config>,
input: &ConfigInput,
) -> (Vec<PlaylistGroup>, Vec<TuliproxError>) {
let working_dir = &cfg.working_dir;
let storage_dir = &cfg.storage_dir;
let input_source: InputSource = {
match input.staged.as_ref() {
None => input.into(),
Some(staged) => if staged.enabled { staged.into() } else { input.into() },
}
};
let persist_file_path = prepare_file_path(input.persist.as_deref(), working_dir, "");
let persist_file_path = prepare_file_path(input.persist.as_deref(), storage_dir, "");
match request::get_input_text_content_as_stream(
app_config,
client,
&input_source,
working_dir,
storage_dir,
persist_file_path,
)
.await
+18 -15
View File
@@ -20,7 +20,7 @@ use reqwest::{
};
use shared::{
error::{notify_err_res, string_to_io_error, TuliproxError},
model::{format_elapsed_time, InputFetchMethod, OnConnectErrorPolicy, DEFAULT_USER_AGENT},
model::{format_elapsed_time, InputFetchMethod, OnConnectErrorPolicy},
utils::{
filter_request_header, human_readable_byte_size, sanitize_sensitive_info, CONTENT_TYPE_JSON, ENCODING_DEFLATE,
ENCODING_GZIP,
@@ -42,6 +42,7 @@ use tokio::{
};
use tokio_util::io::StreamReader;
use url::Url;
use shared::utils::DEFAULT_USER_AGENT;
static PROXY_DIAGNOSTICS_ONCE: Once = Once::new();
@@ -533,13 +534,13 @@ pub async fn get_input_epg_content_as_file(
client: &reqwest::Client,
input: &ConfigInput,
headers: Option<&HeaderMap>,
working_dir: &str,
storage_dir: &str,
url_str: &str,
persist_filepath: &Path,
) -> Result<PathBuf, TuliproxError> {
debug_if_enabled!(
"getting input epg content working_dir: {}, url: {}",
working_dir,
"getting input epg content storage_dir: {}, url: {}",
storage_dir,
sanitize_sensitive_info(url_str)
);
if url_str.parse::<url::Url>().is_ok() {
@@ -556,7 +557,7 @@ pub async fn get_input_epg_content_as_file(
}
}
} else {
let result = match get_file_path(working_dir, Some(PathBuf::from(url_str))) {
let result = match get_file_path(storage_dir, Some(PathBuf::from(url_str))) {
Some(filepath) => {
if filepath.exists() {
if let Err(e) = tokio::fs::copy(&filepath, persist_filepath).await {
@@ -590,12 +591,12 @@ pub async fn get_input_text_content(
app_state: &Arc<AppState>,
client: &reqwest::Client,
input: &InputSource,
working_dir: &str,
storage_dir: &str,
persist_filepath: Option<PathBuf>,
) -> Result<String, TuliproxError> {
debug_if_enabled!(
"getting input text content working_dir: {}, url: {}",
working_dir,
"getting input text content storage_dir: {}, url: {}",
storage_dir,
sanitize_sensitive_info(&input.url)
);
@@ -612,7 +613,7 @@ pub async fn get_input_text_content(
}
}
} else {
let result = match get_file_path(working_dir, Some(PathBuf::from(&input.url))) {
let result = match get_file_path(storage_dir, Some(PathBuf::from(&input.url))) {
Some(filepath) => {
if filepath.exists() {
if let Some(persist_file_value) = persist_filepath {
@@ -650,12 +651,12 @@ pub async fn get_input_text_content_as_stream(
app_config: &Arc<AppConfig>,
client: &reqwest::Client,
input: &InputSource,
working_dir: &str,
storage_dir: &str,
persist_filepath: Option<PathBuf>,
) -> Result<DynReader, TuliproxError> {
debug_if_enabled!(
"getting input text content working_dir: {}, url: {}",
working_dir,
"getting input text content storage_dir: {}, url: {}",
storage_dir,
sanitize_sensitive_info(&input.url)
);
@@ -672,7 +673,7 @@ pub async fn get_input_text_content_as_stream(
}
}
} else {
let result = match get_file_path(working_dir, Some(PathBuf::from(&input.url))) {
let result = match get_file_path(storage_dir, Some(PathBuf::from(&input.url))) {
Some(filepath) => {
if filepath.exists() {
match get_local_file_content_as_stream(&filepath).await {
@@ -1506,7 +1507,7 @@ mod tests {
};
use crate::{
model::{AppConfig, Config, ConfigProvider, ResourceRetryConfig, ReverseProxyConfig, SourcesConfig},
utils::FileLockManager,
utils::{FileLockManager, DEFAULT_USER_AGENT}
};
use arc_swap::{ArcSwap, ArcSwapOption};
use shared::model::{
@@ -1536,7 +1537,9 @@ mod tests {
api_proxy: Arc::new(ArcSwapOption::default()),
file_locks: Arc::new(FileLockManager::default()),
paths: Arc::new(ArcSwap::from_pointee(ConfigPaths {
home_path: String::new(),
config_path: String::new(),
storage_path: String::new(),
config_file_path: String::new(),
sources_file_path: String::new(),
mapping_file_path: None,
@@ -1631,7 +1634,7 @@ mod tests {
#[test]
fn test_get_request_headers_prioritization() {
use super::{get_request_headers, DEFAULT_USER_AGENT};
use super::{get_request_headers};
use axum::http::header::USER_AGENT;
// Case 1: No headers provided -> Default UA
+14 -10
View File
@@ -14,7 +14,7 @@ use chrono::{DateTime, Utc};
use log::{error, info, warn};
use shared::error::TuliproxError;
use shared::model::{PlaylistEntry, PlaylistGroup, ProxyUserStatus, SeriesStreamProperties,
StreamProperties, VideoStreamProperties, XtreamCluster, XtreamPlaylistItem,
StreamProperties, VideoStreamProperties, InputType, XtreamCluster, XtreamPlaylistItem,
XtreamSeriesInfo, XtreamVideoInfo, XtreamVideoInfoDoc};
use shared::utils::{extract_extension_from_url, get_i64_from_serde_value, get_string_from_serde_value, sanitize_sensitive_info, Internable, PROVIDER_SCHEME_PREFIX};
use std::collections::HashMap;
@@ -93,8 +93,8 @@ pub async fn get_xtream_stream_info(client: &reqwest::Client,
match cluster {
XtreamCluster::Live => {}
XtreamCluster::Video => {
let working_dir = &app_config.config.load().working_dir;
if let Ok(storage_path) = get_input_storage_path(&input.name, working_dir).await {
let storage_dir = &app_config.config.load().storage_dir;
if let Ok(storage_path) = get_input_storage_path(&input.name, storage_dir).await {
match serde_json::from_str::<XtreamVideoInfo>(&content) {
Ok(info) => {
// parse downloaded info into StreamProperties
@@ -126,7 +126,7 @@ pub async fn get_xtream_stream_info(client: &reqwest::Client,
}
}
XtreamCluster::Series => {
let working_dir = &app_config.config.load().working_dir;
let storage_dir = &app_config.config.load().storage_dir;
let group = pli.get_group();
let series_name = pli.get_name();
@@ -135,7 +135,7 @@ pub async fn get_xtream_stream_info(client: &reqwest::Client,
// parse series info
let series_stream_props = SeriesStreamProperties::from_info(&info, pli);
if let Ok(storage_path) = get_input_storage_path(&input.name, working_dir).await {
if let Ok(storage_path) = get_input_storage_path(&input.name, storage_dir).await {
// update input db
if let Err(err) = persists_input_series_info(app_config, &storage_path, cluster, &input.name, provider_id, &series_stream_props).await {
error!("Failed to persist series info for input {}: {err}", &input.name);
@@ -387,7 +387,11 @@ pub async fn download_xtream_playlist(app_config: &Arc<AppConfig>, client: &reqw
let skip_cluster = get_skip_cluster(input);
let cfg = app_config.config.load();
let working_dir = &cfg.working_dir;
let storage_dir = &cfg.storage_dir;
// Staged input type is the effective type during download.
let effective_type = input.get_download_input_type();
let use_disk_based_processing =
cfg.disk_based_processing && matches!(effective_type, InputType::Xtream | InputType::XtreamBatch);
let mut errors = vec![];
for (xtream_cluster, category, stream) in &ACTIONS {
@@ -396,16 +400,16 @@ pub async fn download_xtream_playlist(app_config: &Arc<AppConfig>, client: &reqw
let input_source_category = input_source.with_url(concat_string!(&base_url, "&action=", category));
let input_source_stream = input_source.with_url(concat_string!(&base_url, "&action=", stream));
let category_file_path = crate::utils::prepare_file_path(input.persist.as_deref(),
working_dir, concat_string!(category, "_").as_str());
storage_dir, concat_string!(category, "_").as_str());
let stream_file_path = crate::utils::prepare_file_path(input.persist.as_deref(),
working_dir, concat_string!(stream, "_").as_str());
storage_dir, concat_string!(stream, "_").as_str());
match futures::join!(
request::get_input_json_content_as_stream(app_config, client, &input_source_category, category_file_path),
request::get_input_json_content_as_stream(app_config, client, &input_source_stream, stream_file_path)
) {
(Ok(category_content), Ok(stream_content)) => {
if cfg.disk_based_processing {
if use_disk_based_processing {
// trace!("Using disk input playlist optimization for cluster {}", xtream_cluster);
if let Err(err) = persist_input_xtream_playlist_cluster_to_disk(app_config, input, *xtream_cluster, category_content, stream_content).await {
error!("persist_input_xtream_playlist_cluster_to_disk failed: {err}");
@@ -443,7 +447,7 @@ pub async fn download_xtream_playlist(app_config: &Arc<AppConfig>, client: &reqw
plg.id = grp_id;
}
(playlist_groups, errors, cfg.disk_based_processing)
(playlist_groups, errors, use_disk_based_processing)
}
async fn check_alias_user_state(app_config: &Arc<AppConfig>, client: &reqwest::Client, input: &ConfigInput) {
+1 -2
View File
@@ -3,8 +3,7 @@ use shared::error::TuliproxError;
use reqwest::header::{HeaderMap, HeaderValue};
use log::{debug, info};
use shared::error::{info_err};
use shared::model::DEFAULT_USER_AGENT;
use shared::utils::{trim_last_slash, TRAKT_API_KEY};
use shared::utils::{trim_last_slash, DEFAULT_USER_AGENT, TRAKT_API_KEY};
use super::errors::{handle_trakt_api_error};
pub struct TraktClient {