mirror of
https://github.com/euzu/tuliprox.git
synced 2026-10-28 01:32:27 +01: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:
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,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) {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user