diff --git a/Cargo.lock b/Cargo.lock index b446c456f..cd0ca4cb4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1262,6 +1262,7 @@ dependencies = [ "bytes", "chrono", "cron", + "enum-iterator", "fastrand", "futures", "futures-signals", @@ -4045,6 +4046,7 @@ dependencies = [ "serde-saphyr", "serde_json", "serde_tuple", + "thiserror 2.0.18", "url", "zeroize", ] diff --git a/Cargo.toml b/Cargo.toml index c5caac21a..16c1d3000 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -33,6 +33,7 @@ cron = "0.16.0" enum-iterator = "2.3.0" futures = "0.3.32" serde-saphyr = "0.0.22" +thiserror = "2.0.18" tokio = { version = "1.50.0" } [profile.release] diff --git a/backend/src/api/config_watch.rs b/backend/src/api/config_watch.rs index 12d02f9eb..a60da7fc5 100644 --- a/backend/src/api/config_watch.rs +++ b/backend/src/api/config_watch.rs @@ -14,7 +14,7 @@ use notify::{ recommended_watcher, EventKind, RecursiveMode, Watcher, }; use shared::{ - error::{TuliproxError, TuliproxErrorKind}, + error::TuliproxError, model::ConfigPaths, }; use std::{ @@ -57,10 +57,10 @@ fn start_config_watch(app_state: &Arc, cancel_token: &CancellationToke }); let mut watcher = recommended_watcher(std_tx).map_err(|err| { - TuliproxError::new(TuliproxErrorKind::Info, format!("Failed to init config file watcher {err}")) + TuliproxError::Io(format!("Failed to init config file watcher {err}")) })?; watcher.watch(path, recursive_mode).map_err(|err| { - TuliproxError::new(TuliproxErrorKind::Info, format!("Failed to start config file watcher {err}")) + TuliproxError::Io(format!("Failed to start config file watcher {err}")) })?; info!("Watching config file changes {}", path.display()); diff --git a/backend/src/api/endpoints/xtream_api.rs b/backend/src/api/endpoints/xtream_api.rs index bd8f41146..d37efe25f 100644 --- a/backend/src/api/endpoints/xtream_api.rs +++ b/backend/src/api/endpoints/xtream_api.rs @@ -46,7 +46,7 @@ use serde::{Deserialize, Serialize}; use serde_json::{json, Map, Value}; use shared::{ concat_string, - error::{info_err, info_err_res, TuliproxError}, + error::{TuliproxError}, model::{ create_stream_channel_with_type, PlaylistEntry, PlaylistItemType, ProxyType, ShortEpgResultDto, TargetType, UserConnectionPermission, XtreamCluster, XtreamPlaylistItem, @@ -113,7 +113,7 @@ impl FromStr for ApiStreamContext { Self::MOVIE => Ok(Self::Movie), Self::SERIES => Ok(Self::Series), Self::TIMESHIFT => Ok(Self::Timeshift), - _ => info_err_res!("Unknown ApiStreamContext: {}", s), + _ => Err(TuliproxError::ApiXtream(format!("Unknown ApiStreamContext: {s}"))), } } } @@ -866,7 +866,7 @@ pub async fn xtream_get_stream_info_response( if pli.item_type.is_local() { let Ok(xtream_output) = target .get_xtream_output() - .ok_or_else(|| info_err!("Unexpected: xtream output required for target {}", target.name)) + .ok_or_else(|| TuliproxError::ApiXtream(format!("Unexpected: xtream output required for target {}", target.name))) else { return try_unwrap_body!(empty_json_response_as_array()); }; @@ -1311,7 +1311,10 @@ async fn xtream_player_api(api_req: UserApiRequest, app_state: &Arc) - xtream_load_rewrite_playlist(XtreamCluster::Series, app_state, &target, category_id, &user) .await ), - _ => Some(info_err_res!("Unknown api call: {action} for target: {}", &target.name)), + _ => Some(Err(TuliproxError::ApiXtream(format!( + "Unknown api call: {action} for target: {}", + &target.name + )))), }; match result { diff --git a/backend/src/api/main_api.rs b/backend/src/api/main_api.rs index dd4f5d453..eb7d45737 100644 --- a/backend/src/api/main_api.rs +++ b/backend/src/api/main_api.rs @@ -43,7 +43,6 @@ use dashmap::DashSet; use log::{debug, error, info, warn}; use shared::{ error::TuliproxError, - info_err, info_err_res, utils::{concat_path_leading_slash, sanitize_sensitive_info}, }; use std::{ @@ -96,8 +95,7 @@ async fn recover_persisted_downloads_state(downloads: &DownloadQueue) -> Result< } Ok(()) } - Err(err) => Err(TuliproxError::new( - shared::error::TuliproxErrorKind::Info, + Err(err) => Err(TuliproxError::Io( format!("Failed to load persisted downloads: {err}"), )), } @@ -239,7 +237,7 @@ fn spawn_metadata_trigger_update( fn get_web_dir_path(web_ui_enabled: bool, web_root: &str) -> Result { let web_dir_path = if web_root.is_empty() { get_default_web_root_path() } else { PathBuf::from(web_root) }; if web_ui_enabled && (!&web_dir_path.exists() || !&web_dir_path.is_dir()) { - return info_err_res!("web_root does not exist or is not a directory: {}", web_dir_path.display()); + return Err(TuliproxError::Server(format!("web_root does not exist or is not a directory: {}", web_dir_path.display()))); } Ok(web_dir_path) } @@ -727,7 +725,7 @@ pub async fn start_server(app_config: Arc, targets: Arc = router.with_state(shared_data.clone()); let listener = tokio::net::TcpListener::bind(format!("{host}:{port}")) .await - .map_err(|err| info_err!("Failed to bind to {host}:{port}, {err}"))?; + .map_err(|err| TuliproxError::Server(format!("Failed to bind to {host}:{port}, {err}")))?; if let Some(download_cfg) = cfg.video.as_ref().and_then(|video| video.download.as_ref()) { resume_downloads_after_bind(&app_state, download_cfg).await; diff --git a/backend/src/api/model/app_state.rs b/backend/src/api/model/app_state.rs index 429ee7b5b..0995dd205 100644 --- a/backend/src/api/model/app_state.rs +++ b/backend/src/api/model/app_state.rs @@ -26,7 +26,7 @@ use arc_swap::{ArcSwap, ArcSwapOption}; use log::{error, info}; use reqwest::Client; use shared::{ - create_bitset, error::TuliproxError, info_err_res, model::{UserConnectionPermission, VideoDownloadConfigDto}, + create_bitset, error::TuliproxError, model::{UserConnectionPermission, VideoDownloadConfigDto}, utils::small_vecs_equal_unordered, }; use std::{ @@ -315,7 +315,7 @@ fn build_http_client_with_fallback( if proxy_configured { error!("{proxy_error_log}"); - return info_err_res!("{proxy_error_msg}"); + return Err(TuliproxError::Config(proxy_error_msg.to_string())); } error!("{fallback_log}"); diff --git a/backend/src/api/model/metadata_update_manager.rs b/backend/src/api/model/metadata_update_manager.rs index 2e7c54092..8353fd01b 100644 --- a/backend/src/api/model/metadata_update_manager.rs +++ b/backend/src/api/model/metadata_update_manager.rs @@ -1644,10 +1644,10 @@ impl InputWorker { apply_rate_limit = true; } Err(e) => { - if Self::is_permanent_not_found_error(&e.message) { + if Self::is_permanent_not_found_error(e.message()) { debug!( "[Task] Task failed with permanent not-found for input {}: {} (error={})", - input_name, task_for_execution, e.message + input_name, task_for_execution, e.message() ); let retry_domain = Self::retry_domain_for_task(&task_for_execution); self.scheduled_requeues.remove(¤t_key); @@ -1659,7 +1659,7 @@ impl InputWorker { state.attempts = runtime_settings.max_attempts_probe; state.next_allowed_at_ts = cooldown_until_ts; state.cooldown_until_ts = Some(cooldown_until_ts); - state.last_error = Some(e.message.clone()); + state.last_error = Some(e.message().to_string()); state_bundle.touch(now_ts); state_bundle.clone() }; @@ -1681,7 +1681,7 @@ impl InputWorker { state.attempts = runtime_settings.max_attempts_resolve; state.next_allowed_at_ts = cooldown_until_ts; state.cooldown_until_ts = Some(cooldown_until_ts); - state.last_error = Some(e.message.clone()); + state.last_error = Some(e.message().to_string()); state_bundle.touch(now_ts); state_bundle.clone() }; @@ -1695,11 +1695,11 @@ impl InputWorker { task_for_execution, cooldown_until_ts, runtime_settings.resolve_exhaustion_reset_gap_secs, - e.message + e.message() ); } - } else if Self::is_transient_worker_error(&e.message) { - if e.message == TASK_ERR_UPDATE_IN_PROGRESS { + } else if Self::is_transient_worker_error(e.message()) { + if e.message() == TASK_ERR_UPDATE_IN_PROGRESS { // Drop cached readers quickly so foreground writer can progress. self.release_db_handles(); } @@ -1713,7 +1713,7 @@ impl InputWorker { input_name, task_for_execution, retry_delay_secs, - e.message + e.message() ); } else { let retry_domain = Self::retry_domain_for_task(&task_for_execution); @@ -1728,7 +1728,7 @@ impl InputWorker { let state_after_update = { let state = state_bundle.get_mut_or_insert(retry_domain); state.attempts = state.attempts.saturating_add(1); - state.last_error = Some(e.message.clone()); + state.last_error = Some(e.message().to_string()); if state.attempts < max_attempts { let backoff_secs = if retry_domain == RetryDomain::Probe { @@ -1772,7 +1772,7 @@ impl InputWorker { state_after_update.attempts, max_attempts, cooldown_until, - e.message + e.message() ); } else { let cooldown_until_ts = @@ -1783,7 +1783,7 @@ impl InputWorker { state.attempts = max_attempts; state.next_allowed_at_ts = cooldown_until_ts; state.cooldown_until_ts = Some(cooldown_until_ts); - state.last_error = Some(e.message.clone()); + state.last_error = Some(e.message().to_string()); state_bundle.touch(now_ts); state_bundle.clone() }; @@ -1798,7 +1798,7 @@ impl InputWorker { attempts, max_attempts, runtime_settings.resolve_exhaustion_reset_gap_secs, - e.message + e.message() ); } } else { @@ -1818,7 +1818,7 @@ impl InputWorker { max_attempts, state_after_update.next_allowed_at_ts, state_after_update.next_allowed_at_ts.saturating_sub(now_ts), - e.message + e.message() ); } } @@ -3173,14 +3173,14 @@ impl InputWorker { failed_clusters: &mut HashSet, ) -> Result { let app_state = - app_state_weak.and_then(Weak::upgrade).ok_or_else(|| shared::error::info_err!("AppState not available"))?; + app_state_weak.and_then(Weak::upgrade).ok_or_else(|| shared::error::TuliproxError::Config("AppState not available".to_string()))?; let Some(input_base) = app_state.app_config.get_input_by_name(input_name) else { - return Err(shared::error::info_err!("Input {} not found", input_name)); + return Err(shared::error::TuliproxError::Config(format!("Input {input_name} not found"))); }; if !input_base.enabled { - return Err(shared::error::info_err!("Input {} is disabled", input_name)); + return Err(shared::error::TuliproxError::Config(format!("Input {input_name} is disabled"))); } // Background metadata/probe tasks are low-priority. @@ -3188,7 +3188,7 @@ impl InputWorker { if let Some(guard) = app_state.update_guard.try_playlist() { drop(guard); } else { - return Err(shared::error::info_err!("{}", TASK_ERR_UPDATE_IN_PROGRESS)); + return Err(shared::error::TuliproxError::Config(TASK_ERR_UPDATE_IN_PROGRESS.to_string())); } let needs_probe_connection = Self::task_needs_provider_connection(task, input_base.input_type); @@ -3205,7 +3205,7 @@ impl InputWorker { let provider_handle = if needs_probe_connection { let Some(handle) = app_state.active_provider.acquire_connection_for_probe(input_name, probe_priority).await else { debug_if_enabled!("No provider connection available for background task {}, skipping...", task); - return Err(shared::error::info_err!("{}", TASK_ERR_NO_CONNECTION)); + return Err(shared::error::TuliproxError::Config(TASK_ERR_NO_CONNECTION.to_string())); }; Some(handle) } else { @@ -3256,7 +3256,7 @@ impl InputWorker { () = token.cancelled() => { debug_if_enabled!("Metadata update task preempted by user request for input {}", input_name); - Err(shared::error::info_err!("{}", TASK_ERR_PREEMPTED)) + Err(shared::error::TuliproxError::Config(TASK_ERR_PREEMPTED.to_string())) } res = Self::execute_task_inner_static(&app_state, &client, &input_to_use, task, item_title.as_deref(), Some(handle), probe_priority, collector, db_handles, failed_clusters) => { @@ -3304,9 +3304,9 @@ impl InputWorker { "Metadata probe task timed out after {PROBE_TASK_TIMEOUT_SECS}s for input {input_name}: {task}; \ releasing provider handle and skipping task" ); - Err(shared::error::info_err!( + Err(shared::error::TuliproxError::Config(format!( "Task timed out after {PROBE_TASK_TIMEOUT_SECS}s for input {input_name}: {task}" - )) + ))) } } } else { @@ -3525,11 +3525,9 @@ impl InputWorker { match outcome { GenericProbeOutcome::Updated | GenericProbeOutcome::Noop => Ok((false, false)), - GenericProbeOutcome::ProbeFailed => Err(shared::error::info_err!( - "Probe stream task failed for key {:?} ({})", - task_key, - probe_identifier - )), + GenericProbeOutcome::ProbeFailed => Err(shared::error::TuliproxError::Config(format!( + "Probe stream task failed for key {task_key:?} ({probe_identifier})" + ))), } } } diff --git a/backend/src/api/panel_api.rs b/backend/src/api/panel_api.rs index 2ad726c0f..533346729 100644 --- a/backend/src/api/panel_api.rs +++ b/backend/src/api/panel_api.rs @@ -25,7 +25,7 @@ use serde::{Deserialize, Serialize}; use serde_json::Value; use shared::{ concat_string, create_bitset, - error::{info_err, info_err_res, TuliproxError}, + error::{TuliproxError}, model::{ ConfigInputAliasDto, InputType, PanelApiAliasPoolSizeValue, PanelApiProvisioningMethod, ProxyUserStatus, SourcesConfigDto, VirtualId, @@ -216,19 +216,19 @@ fn validate_type_is_m3u(params: &[PanelApiQueryParam]) -> Result<(), TuliproxErr if typ_str.trim().eq_ignore_ascii_case("m3u") { Ok(()) } else if typ_str.is_empty() { - info_err_res!("panel_api: missing required query param 'type=m3u'") + Err(TuliproxError::ConfigPanelApi("panel_api: missing required query param 'type=m3u'".to_string())) } else { - info_err_res!("panel_api: unsupported type={typ_str}, only m3u is supported") + Err(TuliproxError::ConfigPanelApi(format!("panel_api: unsupported type={typ_str}, only m3u is supported"))) } } fn require_api_key_param(params: &[PanelApiQueryParam], section: &str) -> Result<(), TuliproxError> { let api_key = params.iter().find(|p| p.key.trim().eq_ignore_ascii_case("api_key")); let Some(api_key) = api_key else { - return info_err_res!("panel_api: {section} must contain query param 'api_key' (use value 'auto')"); + return Err(TuliproxError::ConfigPanelApi(format!("panel_api: {section} must contain query param 'api_key' (use value 'auto')"))); }; if api_key.value.trim().is_empty() { - return info_err_res!("panel_api: {section} query param 'api_key' must not be empty (use value 'auto')"); + return Err(TuliproxError::ConfigPanelApi(format!("panel_api: {section} query param 'api_key' must not be empty (use value 'auto')"))); } Ok(()) } @@ -237,16 +237,16 @@ fn require_username_password_params_auto(params: &[PanelApiQueryParam], section: let username = params.iter().find(|p| p.key.trim().eq_ignore_ascii_case("username")); let password = params.iter().find(|p| p.key.trim().eq_ignore_ascii_case("password")); if username.is_none() || password.is_none() { - return info_err_res!( + return Err(TuliproxError::ConfigPanelApi(format!( "panel_api: {section} must contain query params 'username' and 'password' (use value 'auto')" - ); + ))); } if !username.is_some_and(|p| p.value.trim().eq_ignore_ascii_case("auto")) || !password.is_some_and(|p| p.value.trim().eq_ignore_ascii_case("auto")) { - return info_err_res!( + return Err(TuliproxError::ConfigPanelApi(format!( "panel_api: {section} requires 'username: auto' and 'password: auto' (credentials must not be hardcoded)" - ); + ))); } Ok(()) } @@ -255,7 +255,7 @@ fn validate_client_new_params(params: &[PanelApiQueryParam]) -> Result<(), Tulip require_api_key_param(params, "query_parameter.client_new")?; validate_type_is_m3u(params)?; if params.iter().any(|p| p.key.trim().eq_ignore_ascii_case("user")) { - return info_err_res!("panel_api: client_new must not contain query param 'user'"); + return Err(TuliproxError::ConfigPanelApi("panel_api: client_new must not contain query param 'user'".to_string())); } Ok(()) } @@ -341,20 +341,20 @@ fn parse_panel_api_provisioning_offset_secs(offset: &str) -> Result (&lower[..lower.len().saturating_sub(1)], 60_u64 * 60 * 24), b'0'..=b'9' => (lower.as_str(), 1_u64), _ => { - return info_err_res!( + return Err(TuliproxError::ConfigPanelApi(format!( "panel_api.provisioning.offset must be a number with optional suffix s/m/h/d (e.g. 30m, 12h), got '{raw}'" - ); + ))); } }; let num_part = num_part.trim(); if num_part.is_empty() { - return info_err_res!( + return Err(TuliproxError::ConfigPanelApi(format!( "panel_api.provisioning.offset must be a number with optional suffix s/m/h/d (e.g. 30m, 12h), got '{raw}'" - ); + ))); } let value: u64 = - num_part.parse().map_err(|_| info_err!("panel_api.provisioning.offset is not a valid number: '{raw}'"))?; - value.checked_mul(multiplier).ok_or_else(|| info_err!("panel_api.provisioning.offset is too large: '{raw}'")) + num_part.parse().map_err(|_| TuliproxError::ConfigPanelApi(format!("panel_api.provisioning.offset is not a valid number: '{raw}'")))?; + value.checked_mul(multiplier).ok_or_else(|| TuliproxError::ConfigPanelApi(format!("panel_api.provisioning.offset is too large: '{raw}'"))) } fn validate_panel_api_config(cfg: &PanelApiConfig) -> Result<(), TuliproxError> { @@ -362,13 +362,13 @@ fn validate_panel_api_config(cfg: &PanelApiConfig) -> Result<(), TuliproxError> return Ok(()); } if cfg.url.trim().is_empty() { - return info_err_res!("panel_api: url is missing"); + return Err(TuliproxError::ConfigPanelApi("panel_api: url is missing".to_string())); } if cfg.api_key.as_ref().is_none_or(|k| k.trim().is_empty()) { - return info_err_res!("panel_api: api_key is missing"); + return Err(TuliproxError::ConfigPanelApi("panel_api: api_key is missing".to_string())); } if cfg.query_parameter.client_info.is_empty() { - return info_err_res!("panel_api: query_parameter.client_info must be configured"); + return Err(TuliproxError::ConfigPanelApi("panel_api: query_parameter.client_info must be configured".to_string())); } validate_client_info_params(&cfg.query_parameter.client_info)?; if !cfg.query_parameter.client_new.is_empty() { @@ -386,23 +386,23 @@ fn validate_panel_api_config(cfg: &PanelApiConfig) -> Result<(), TuliproxError> let (min_val, max_val) = alias_pool_limit_values(cfg); if let Some(PanelApiAliasPoolSizeValue::Number(value)) = min_val { if *value == 0 { - return info_err_res!("panel_api.alias_pool.size.min must be greater than 0"); + return Err(TuliproxError::ConfigPanelApi("panel_api.alias_pool.size.min must be greater than 0".to_string())); } } if let Some(PanelApiAliasPoolSizeValue::Number(value)) = max_val { if *value == 0 { - return info_err_res!("panel_api.alias_pool.size.max must be greater than 0"); + return Err(TuliproxError::ConfigPanelApi("panel_api.alias_pool.size.max must be greater than 0".to_string())); } } let min = min_val.and_then(PanelApiAliasPoolSizeValue::as_number); let max = max_val.and_then(PanelApiAliasPoolSizeValue::as_number); if let (Some(min), Some(max)) = (min, max) { if min > max { - return info_err_res!("panel_api.alias_pool.size.min must be <= panel_api.alias_pool.size.max"); + return Err(TuliproxError::ConfigPanelApi("panel_api.alias_pool.size.min must be <= panel_api.alias_pool.size.max".to_string())); } } if cfg.provisioning.probe_interval_sec == 0 { - return info_err_res!("panel_api.provisioning.probe_interval_sec must be greater than 0"); + return Err(TuliproxError::ConfigPanelApi("panel_api.provisioning.probe_interval_sec must be greater than 0".to_string())); } if let Some(offset) = cfg.provisioning.offset.as_deref() { let _secs = parse_panel_api_provisioning_offset_secs(offset)?; @@ -425,21 +425,21 @@ fn resolve_query_params( if value.eq_ignore_ascii_case("auto") { if key.eq_ignore_ascii_case("api_key") { let Some(k) = api_key.filter(|s| !s.trim().is_empty()) else { - return info_err_res!("panel_api: query param {key} uses 'auto' but panel_api.api_key is missing"); + return Err(TuliproxError::ConfigPanelApi(format!("panel_api: query param {key} uses 'auto' but panel_api.api_key is missing"))); }; value = k.to_string(); } else if key.eq_ignore_ascii_case("username") { let Some((u, _)) = creds else { - return info_err_res!( + return Err(TuliproxError::ConfigPanelApi(format!( "panel_api: query param {key} uses 'auto' but no account username is available" - ); + ))); }; value = u.to_string(); } else if key.eq_ignore_ascii_case("password") { let Some((_, pw)) = creds else { - return info_err_res!( + return Err(TuliproxError::ConfigPanelApi(format!( "panel_api: query param {key} uses 'auto' but no account password is available" - ); + ))); }; value = pw.to_string(); } @@ -450,7 +450,7 @@ fn resolve_query_params( } fn build_panel_url(base_url: &str, query_params: &[(String, String)]) -> Result { - let mut url = Url::parse(base_url).map_err(|e| info_err!("panel_api: invalid url {base_url}: {e}"))?; + let mut url = Url::parse(base_url).map_err(|e| TuliproxError::ConfigPanelApi(format!("panel_api: invalid url {base_url}: {e}")))?; { let mut pairs = url.query_pairs_mut(); for (k, v) in query_params { @@ -504,11 +504,11 @@ async fn panel_get_json(app_state: &AppState, url: Url) -> Result Result Result<(), TuliproxError> { if let Some(parent) = cache_path.parent() { - tokio::fs::create_dir_all(parent).await.map_err(|e| info_err!("panel_api: failed to create cache dir: {e}"))?; + tokio::fs::create_dir_all(parent).await.map_err(|e| TuliproxError::ConfigPanelApi(format!("panel_api: failed to create cache dir: {e}")))?; } - let content = serde_json::to_string_pretty(cache).map_err(|e| info_err!("panel_api: {e}"))?; + let content = serde_json::to_string_pretty(cache).map_err(|e| TuliproxError::ConfigPanelApi(format!("panel_api: {e}")))?; let _lock = app_state.app_config.file_locks.write_lock(cache_path).await; tokio::fs::write(cache_path, content) .await - .map_err(|e| info_err!("panel_api: failed to persist time cache: {e}"))?; + .map_err(|e| TuliproxError::ConfigPanelApi(format!("panel_api: failed to persist time cache: {e}")))?; Ok(()) } @@ -772,14 +772,14 @@ async fn panel_account_info( let url = build_panel_url(cfg.url.as_ref(), ¶ms)?; let json = panel_get_json(app_state, url).await?; let Some(obj) = first_json_object(&json) else { - return info_err_res!("panel_api: account_info response is not a JSON object/array"); + return Err(TuliproxError::ConfigPanelApi("panel_api: account_info response is not a JSON object/array".to_string())); }; let status_ok = obj.get("status").is_some_and(parse_boolish); if !status_ok { - return info_err_res!("panel_api: account_info status=false"); + return Err(TuliproxError::ConfigPanelApi("panel_api: account_info status=false".to_string())); } let Some(credits) = obj.get("credits").and_then(extract_stringish) else { - return info_err_res!("panel_api: account_info response missing credits"); + return Err(TuliproxError::ConfigPanelApi("panel_api: account_info response missing credits".to_string())); }; Ok(Some(credits)) } @@ -797,11 +797,11 @@ async fn panel_client_adult_content( let url = build_panel_url(cfg.url.as_ref(), ¶ms)?; let json = panel_get_json(app_state, url).await?; let Some(obj) = first_json_object(&json) else { - return info_err_res!("panel_api: client_adult_content response is not a JSON object/array"); + return Err(TuliproxError::ConfigPanelApi("panel_api: client_adult_content response is not a JSON object/array".to_string())); }; let status_ok = obj.get("status").is_some_and(parse_boolish); if !status_ok { - return info_err_res!("panel_api: client_adult_content status=false"); + return Err(TuliproxError::ConfigPanelApi("panel_api: client_adult_content status=false".to_string())); } Ok(()) } @@ -926,7 +926,7 @@ fn resolve_alias_pool_limits( }; if let (Some(min), Some(max)) = (min, max) { if min > max { - return info_err_res!("panel_api.alias_pool.size.min must be <= panel_api.alias_pool.size.max"); + return Err(TuliproxError::ConfigPanelApi("panel_api.alias_pool.size.min must be <= panel_api.alias_pool.size.max".to_string())); } } Ok((min, max)) @@ -1123,17 +1123,17 @@ async fn patch_source_yml_add_alias( ) -> Result<(), TuliproxError> { let mut sources = match read_sources_file_from_path(source_file_path, false, false, None).await { Ok(sources) => sources, - Err(e) => return info_err_res!("panel_api: failed to read source file: {e}"), + Err(e) => return Err(TuliproxError::ConfigPanelApi(format!("panel_api: failed to read source file: {e}"))), }; let Some(input) = sources.inputs.iter_mut().find(|i| i.name == *input_name) else { - return info_err_res!("panel_api: could not find input '{input_name}' in source.yml"); + return Err(TuliproxError::ConfigPanelApi(format!("panel_api: could not find input '{input_name}' in source.yml"))); }; let aliases = input.aliases.get_or_insert_with(Vec::new); let next_index = aliases.iter().map(|a| a.id).max().unwrap_or(0); if next_index == u16::MAX { - return info_err_res!("panel_api: cannot add alias for '{input_name}': alias id overflow"); + return Err(TuliproxError::ConfigPanelApi(format!("panel_api: cannot add alias for '{input_name}': alias id overflow"))); } let alias = ConfigInputAliasDto { @@ -1286,11 +1286,11 @@ fn apply_sources_yml_patches(doc: &mut SourcesConfigDto, patches: &[SourcesYmlPa SourcesYmlPatch::UpdatePanelApiCredits { input_name, credits } => { let idx = *inputs_by_name .get(input_name.as_ref()) - .ok_or_else(|| info_err!("panel_api: could not find input '{input_name}' in source.yml"))?; + .ok_or_else(|| TuliproxError::ConfigPanelApi(format!("panel_api: could not find input '{input_name}' in source.yml")))?; let Some(panel_api) = doc.inputs[idx].panel_api.as_mut() else { - return Err(info_err!( + return Err(TuliproxError::ConfigPanelApi(format!( "panel_api: could not find panel_api for input '{input_name}' in source.yml" - )); + ))); }; if panel_api.credits.as_deref().map(str::trim) != Some(credits.trim()) { panel_api.credits = Some(credits.trim().to_string()); @@ -1300,7 +1300,7 @@ fn apply_sources_yml_patches(doc: &mut SourcesConfigDto, patches: &[SourcesYmlPa SourcesYmlPatch::SortAliases { input_name } => { let idx = *inputs_by_name .get(input_name.as_ref()) - .ok_or_else(|| info_err!("panel_api: could not find input '{input_name}' in source.yml"))?; + .ok_or_else(|| TuliproxError::ConfigPanelApi(format!("panel_api: could not find input '{input_name}' in source.yml")))?; let Some(aliases) = doc.inputs[idx].aliases.as_mut() else { continue; }; @@ -1313,7 +1313,7 @@ fn apply_sources_yml_patches(doc: &mut SourcesConfigDto, patches: &[SourcesYmlPa SourcesYmlPatch::UpdateExpDate { input_name, account_name, exp_date } => { let idx = *inputs_by_name .get(input_name.as_ref()) - .ok_or_else(|| info_err!("panel_api: could not find input '{input_name}' in source.yml"))?; + .ok_or_else(|| TuliproxError::ConfigPanelApi(format!("panel_api: could not find input '{input_name}' in source.yml")))?; if account_name == input_name { if doc.inputs[idx].exp_date != Some(*exp_date) { doc.inputs[idx].exp_date = Some(*exp_date); @@ -1324,14 +1324,14 @@ fn apply_sources_yml_patches(doc: &mut SourcesConfigDto, patches: &[SourcesYmlPa continue; } let Some(alias_idx) = alias_indices[idx].get(account_name).copied() else { - return Err(info_err!( + return Err(TuliproxError::ConfigPanelApi(format!( "panel_api: could not find alias '{account_name}' under input '{input_name}' in source.yml" - )); + ))); }; let aliases = doc.inputs[idx] .aliases .as_mut() - .ok_or_else(|| info_err!("panel_api: input '{input_name}' has no aliases"))?; + .ok_or_else(|| TuliproxError::ConfigPanelApi(format!("panel_api: input '{input_name}' has no aliases")))?; if aliases[alias_idx].exp_date != Some(*exp_date) { aliases[alias_idx].exp_date = Some(*exp_date); aliases[alias_idx].max_connections = 1; @@ -1341,7 +1341,7 @@ fn apply_sources_yml_patches(doc: &mut SourcesConfigDto, patches: &[SourcesYmlPa SourcesYmlPatch::UpdateRootCredentials { input_name, username, password, exp_date } => { let idx = *inputs_by_name .get(input_name.as_ref()) - .ok_or_else(|| info_err!("panel_api: could not find input '{input_name}' in source.yml"))?; + .ok_or_else(|| TuliproxError::ConfigPanelApi(format!("panel_api: could not find input '{input_name}' in source.yml")))?; let input = &mut doc.inputs[idx]; let exp_date_changed = exp_date.is_some() && input.exp_date != *exp_date; if input.username.as_deref() != Some(username.as_str()) @@ -1362,16 +1362,16 @@ fn apply_sources_yml_patches(doc: &mut SourcesConfigDto, patches: &[SourcesYmlPa SourcesYmlPatch::UpdateAliasCredentials { input_name, alias_name, username, password, exp_date } => { let idx = *inputs_by_name .get(input_name.as_ref()) - .ok_or_else(|| info_err!("panel_api: could not find input '{input_name}' in source.yml"))?; + .ok_or_else(|| TuliproxError::ConfigPanelApi(format!("panel_api: could not find input '{input_name}' in source.yml")))?; let Some(alias_idx) = alias_indices[idx].get(alias_name).copied() else { - return Err(info_err!( + return Err(TuliproxError::ConfigPanelApi(format!( "panel_api: could not find alias '{alias_name}' under input '{input_name}' in source.yml" - )); + ))); }; let aliases = doc.inputs[idx] .aliases .as_mut() - .ok_or_else(|| info_err!("panel_api: input '{input_name}' has no aliases"))?; + .ok_or_else(|| TuliproxError::ConfigPanelApi(format!("panel_api: input '{input_name}' has no aliases")))?; let alias = &mut aliases[alias_idx]; let exp_date_changed = exp_date.is_some() && alias.exp_date != *exp_date; if alias.username.as_deref() != Some(username.as_str()) @@ -1391,12 +1391,12 @@ fn apply_sources_yml_patches(doc: &mut SourcesConfigDto, patches: &[SourcesYmlPa SourcesYmlPatch::AddAlias { input_name, alias_name, base_url, username, password, exp_date } => { let idx = *inputs_by_name .get(input_name) - .ok_or_else(|| info_err!("panel_api: could not find input '{input_name}' in source.yml"))?; + .ok_or_else(|| TuliproxError::ConfigPanelApi(format!("panel_api: could not find input '{input_name}' in source.yml")))?; let input_type = doc.inputs[idx].input_type; let aliases = doc.inputs[idx].aliases.get_or_insert_with(Vec::new); let next_index = u16::try_from(aliases.len()) - .map_err(|_| info_err!("panel_api: cannot add alias for '{input_name}': alias id overflow"))?; + .map_err(|_| TuliproxError::ConfigPanelApi(format!("panel_api: cannot add alias for '{input_name}': alias id overflow")))?; let mut alias = ConfigInputAliasDto { id: 0, @@ -1418,7 +1418,7 @@ fn apply_sources_yml_patches(doc: &mut SourcesConfigDto, patches: &[SourcesYmlPa SourcesYmlPatch::RemoveExpiredAliases { input_name } => { let idx = *inputs_by_name .get(input_name) - .ok_or_else(|| info_err!("panel_api: could not find input '{input_name}' in source.yml"))?; + .ok_or_else(|| TuliproxError::ConfigPanelApi(format!("panel_api: could not find input '{input_name}' in source.yml")))?; let Some(aliases) = doc.inputs[idx].aliases.as_mut() else { continue; }; @@ -1446,7 +1446,7 @@ async fn persist_sources_yml_with_patches( } let mut sources = read_sources_file_from_path(sources_path, false, false, None) .await - .map_err(|e| info_err!("panel_api: failed to read source file: {e}"))?; + .map_err(|e| TuliproxError::ConfigPanelApi(format!("panel_api: failed to read source file: {e}")))?; let changed = apply_sources_yml_patches(&mut sources, patches)?; if !changed { @@ -1466,11 +1466,11 @@ async fn patch_source_yml_update_exp_date( ) -> Result<(), TuliproxError> { let mut sources = match read_sources_file_from_path(source_file_path, false, false, None).await { Ok(sources) => sources, - Err(e) => return info_err_res!("panel_api: failed to read source file: {e}"), + Err(e) => return Err(TuliproxError::ConfigPanelApi(format!("panel_api: failed to read source file: {e}"))), }; let Some(input) = sources.inputs.iter_mut().find(|i| i.name == *input_name) else { - return info_err_res!("panel_api: could not find input '{input_name}' in source.yml"); + return Err(TuliproxError::ConfigPanelApi(format!("panel_api: could not find input '{input_name}' in source.yml"))); }; if account_name == input_name { @@ -1479,16 +1479,16 @@ async fn patch_source_yml_update_exp_date( input.max_connections = 1; } else if let Some(aliases) = input.aliases.as_mut() { let Some(alias) = aliases.iter_mut().find(|a| &a.name == account_name) else { - return info_err_res!( + return Err(TuliproxError::ConfigPanelApi(format!( "panel_api: could not find alias '{account_name}' under input '{input_name}' in source.yml" - ); + ))); }; alias.exp_date = Some(exp_date); alias.max_connections = 1; } else { - return info_err_res!( + return Err(TuliproxError::ConfigPanelApi(format!( "panel_api: input '{input_name}' has no aliases; cannot update exp_date for '{account_name}'" - ); + ))); } persist_source_config(app_state, Some(source_file_path), sources).await?; diff --git a/backend/src/api/setup_api.rs b/backend/src/api/setup_api.rs index 90bcd48c3..f9d79480c 100644 --- a/backend/src/api/setup_api.rs +++ b/backend/src/api/setup_api.rs @@ -21,7 +21,6 @@ use serde_json::json; use shared::{ error::TuliproxError, foundation::prepare_templates, - info_err, model::{ ApiProxyConfigDto, ApiProxyServerInfoDto, AppConfigDto, ConfigApiDto, ConfigDto, ConfigPaths, PatternTemplate, SourcesConfigDto, TemplateDefinitionDto, TokenResponse, WebAuthConfigDto, WebUiConfigDto, TOKEN_NO_AUTH, @@ -898,7 +897,7 @@ pub async fn start_setup_server(paths: &ConfigPaths, missing_files: &[String]) - let draft = build_initial_draft(paths).await; let (host, port, web_root) = setup_bind_values(&draft, paths.home_path.as_str()); let web_dir = resolve_setup_web_dir(&web_root, paths.home_path.as_str()) - .ok_or_else(|| info_err!("Setup mode requires a web directory. Tried '{}'", web_root.display(),))?; + .ok_or_else(|| TuliproxError::Server(format!("Setup mode requires a web directory. Tried '{}'", web_root.display())))?; let (shutdown_tx, shutdown_rx) = oneshot::channel::<()>(); let state = Arc::new(SetupModeState { @@ -949,14 +948,14 @@ pub async fn start_setup_server(paths: &ConfigPaths, missing_files: &[String]) - let listener = tokio::net::TcpListener::bind(format!("{host}:{port}")) .await - .map_err(|err| info_err!("Failed to bind setup server to {host}:{port}: {err}"))?; + .map_err(|err| TuliproxError::Server(format!("Failed to bind setup server to {host}:{port}: {err}")))?; axum::serve(listener, router.into_make_service_with_connect_info::()) .with_graceful_shutdown(async move { let _ = shutdown_rx.await; }) .await - .map_err(|err| info_err!("Setup server error: {err}"))?; + .map_err(|err| TuliproxError::Server(format!("Setup server error: {err}")))?; Ok(()) } diff --git a/backend/src/model/config/app.rs b/backend/src/model/config/app.rs index f4b2e9498..756aa8b01 100644 --- a/backend/src/model/config/app.rs +++ b/backend/src/model/config/app.rs @@ -8,8 +8,7 @@ use crate::utils; use arc_swap::{ArcSwap, ArcSwapOption}; use log::{error, warn}; use rand::Rng; -use shared::error::{TuliproxError, TuliproxErrorKind}; -use shared::info_err_res; +use shared::error::TuliproxError; use shared::model::ConfigPaths; use shared::utils::{ CHANNEL_UNAVAILABLE, LOW_PRIORITY_PREEMPTED, PANEL_API_PROVISIONING, PROVIDER_CONNECTIONS_EXHAUSTED, @@ -84,10 +83,10 @@ impl AppConfig { if let Some(username) = output_username { if let Some((_, config_target)) = self.get_target_for_username(username) { if config_target.name != target_name { - return info_err_res!("User:{username} does not belong to target: {}", target_name); + return Err(TuliproxError::Config(format!("User:{username} does not belong to target: {target_name}"))); } } else { - return info_err_res!("User: {username} does not exist"); + return Err(TuliproxError::Config(format!("User: {username} does not exist"))); } Ok(()) } else { @@ -264,20 +263,20 @@ impl AppConfig { for input in &sources.inputs { let input_name = input.name.trim(); if input_name.is_empty() { - return info_err_res!("input name required"); + return Err(TuliproxError::Config("input name required".to_string())); } if seen_names.contains(input_name) { - return info_err_res!("input names should be unique: {}", input_name); + return Err(TuliproxError::Config(format!("input names should be unique: {input_name}"))); } seen_names.insert(input_name.to_string()); if let Some(aliases) = &input.aliases { for alias in aliases { let input_name = alias.name.trim().to_string(); if input_name.is_empty() { - return info_err_res!("input name required"); + return Err(TuliproxError::Config("input name required".to_string())); } if seen_names.contains(&input_name) { - return info_err_res!("input and alias names should be unique: {}", input_name); + return Err(TuliproxError::Config(format!("input and alias names should be unique: {input_name}"))); } seen_names.insert(input_name.clone()); } @@ -295,7 +294,7 @@ impl AppConfig { if let Some(targets) = &schedule.targets { for target_name in targets { if !target_names.contains(target_name.as_str()) { - return info_err_res!("Unknown target name in scheduler: {}", target_name); + return Err(TuliproxError::Config(format!("Unknown target name in scheduler: {target_name}"))); } } } @@ -310,7 +309,7 @@ impl AppConfig { pub fn prepare(&mut self, include_computed: bool) -> Result<(), TuliproxError> { if include_computed { self.access_token_secret = generate_secret(); - self.encrypt_secret = <&[u8] as TryInto<[u8; 16]>>::try_into(&generate_secret()[0..16]).map_err(|err| TuliproxError::new(TuliproxErrorKind::Info, err.to_string()))?; + self.encrypt_secret = <&[u8] as TryInto<[u8; 16]>>::try_into(&generate_secret()[0..16]).map_err(|err| TuliproxError::Crypto(err.to_string()))?; self.prepare_paths(); } else { self.prepare_mapping_path(); @@ -445,9 +444,7 @@ impl AppConfig { /// Will panic if default server invalid pub fn get_server_info(&self, server_info_name: &str) -> ApiProxyServerInfo { let guard = self.api_proxy.load(); - if let Ok(api_proxy) = guard.as_ref().ok_or_else(|| { - TuliproxError::new(TuliproxErrorKind::Info, "API proxy config not loaded".to_string()) - }) { + if let Some(api_proxy) = guard.as_ref() { let server_info_list = api_proxy.server.clone(); server_info_list.iter().find(|c| c.name.eq(server_info_name)) .map_or_else(|| server_info_list.first().unwrap().clone(), Clone::clone) diff --git a/backend/src/model/config/input.rs b/backend/src/model/config/input.rs index 20db5024a..7ecba100d 100644 --- a/backend/src/model/config/input.rs +++ b/backend/src/model/config/input.rs @@ -6,8 +6,8 @@ use shared::{apply_flags, create_bitset}; use shared::error::TuliproxError; use shared::model::{ClusterSource, ConfigInputAliasDto, ConfigInputDto, ConfigInputOptionsDto, InputFetchMethod, InputType, StagedInputDto, XtreamCluster}; use shared::utils::{get_credentials_from_url, parse_provider_scheme_url_parts, sanitize_sensitive_info, Internable, PROVIDER_SCHEME_PREFIX}; -use shared::{check_input_connections, info_err_res, write_if_some}; -use shared::{check_input_credentials, concat_string, info_err}; +use shared::{check_input_connections, write_if_some}; +use shared::{check_input_credentials, concat_string }; use std::borrow::Cow; use std::collections::HashMap; use std::fmt; @@ -264,18 +264,18 @@ pub struct ConfigInput { impl ConfigInput { fn resolve_provider_config(url: &str, provider_configs: &[Arc]) -> Result, TuliproxError> { let (host, _path) = parse_provider_scheme_url_parts(url).map_err(|err| { - info_err!( + TuliproxError::ConfigInput(format!( "Malformed provider URL {}: {}", sanitize_sensitive_info(url), sanitize_sensitive_info(err.to_string().as_str()) - ) + )) })?; provider_configs .iter() .find(|p| p.name.as_ref() == host) .cloned() - .ok_or_else(|| info_err!("Failed to resolve provider config for {}", sanitize_sensitive_info(url))) + .ok_or_else(|| TuliproxError::ConfigInput(format!("Failed to resolve provider config for {}", sanitize_sensitive_info(url)))) } fn prepare_staged_input( @@ -296,11 +296,11 @@ impl ConfigInput { check_input_credentials!(staged_input, staged_input.input_type, false, true); if !matches!(staged_input.input_type, InputType::M3u | InputType::Xtream) { - return info_err_res!( + return Err(TuliproxError::ConfigInput(format!( "Staged input can only be from type m3u or xtream (input: {}, staged: {})", self.name, staged_input.name - ); + ))); } if self.input_type.is_xtream() { @@ -309,19 +309,19 @@ impl ConfigInput { let series_uses_staged = matches!(staged_input.series_source, ClusterSource::Staged) && !skip_series; if !live_uses_staged && !vod_uses_staged && !series_uses_staged { - return info_err_res!( + return Err(TuliproxError::ConfigInput(format!( "Staged input is enabled but no cluster source uses 'staged'; set at least one of live_source/vod_source/series_source to 'staged' (input: {}, staged: {})", self.name, staged_input.name - ); + ))); } if staged_input.input_type.is_m3u() && (vod_uses_staged || series_uses_staged) { - return info_err_res!( + return Err(TuliproxError::ConfigInput(format!( "Staged M3U input cannot provide VOD or Series clusters; use 'input' or 'skip' (input: {}, staged: {})", self.name, staged_input.name - ); + ))); } } @@ -598,7 +598,7 @@ impl ConfigInput { let (_, resolved) = resolve_provider_scheme_url_with_provider(url, Some(provider))?; Ok(resolved) } else { - info_err_res!("Provider config for '{}' not found in input '{}'", host, self.name) + Err(TuliproxError::ConfigInput(format!("Provider config for '{}' not found in input '{}'", host, self.name))) } } @@ -702,7 +702,7 @@ pub fn resolve_provider_scheme_url_with_provider( let (_host, path_and_query) = parse_provider_scheme_url_parts(stream_url)?; let provider = provider_config.ok_or_else(|| { - info_err!("Provider config missing for resolution of: '{}'", sanitize_sensitive_info(stream_url)) + TuliproxError::ConfigInput(format!("Provider config missing for resolution of: '{}'", sanitize_sensitive_info(stream_url))) })?; let final_url = assemble_provider_url(&provider, path_and_query)?; @@ -712,7 +712,7 @@ pub fn resolve_provider_scheme_url_with_provider( /// Internal helper to build the final URL string fn assemble_provider_url(provider: &ConfigProvider, path_and_query: &str) -> Result { let base = provider.get_current_url() - .ok_or_else(|| info_err!("Provider '{}' has no URLs available", provider.name))?; + .ok_or_else(|| TuliproxError::ConfigInput(format!("Provider '{}' has no URLs available", provider.name)))?; // Add http:// scheme if no scheme is present let base_with_scheme = if base.contains("://") { diff --git a/backend/src/model/config/library.rs b/backend/src/model/config/library.rs index 46378e1db..63dc34dcc 100644 --- a/backend/src/model/config/library.rs +++ b/backend/src/model/config/library.rs @@ -1,5 +1,5 @@ use crate::model::macros; -use shared::error::{info_err_res, TuliproxError}; +use shared::error::TuliproxError; use shared::model::{ConfigDto, LibraryConfigDto, LibraryContentType, LibraryMetadataFormat}; use shared::utils::DEFAULT_STORAGE_DIR; use shared::utils::Internable; @@ -55,7 +55,7 @@ impl LibraryConfig { fn canonicalize_scan_directory_path(path: &str, storage_dir: &str) -> Result { let path = path.trim(); if path.is_empty() { - return info_err_res!("Library scan directory path cannot be empty"); + return Err(TuliproxError::Config("Library scan directory path cannot be empty".to_string())); } let scan_path = PathBuf::from(path); @@ -63,7 +63,7 @@ impl LibraryConfig { match scan_path.canonicalize() { Ok(path_buf) => Ok(path_buf.to_string_lossy().to_string()), - Err(err) => info_err_res!("Failed to canonicalize directory path {}: {err}", path), + Err(err) => Err(TuliproxError::Config(format!("Failed to canonicalize directory path {path}: {err}"))), } } diff --git a/backend/src/model/config/panel_api.rs b/backend/src/model/config/panel_api.rs index 6f24b6be9..f529bdeba 100644 --- a/backend/src/model/config/panel_api.rs +++ b/backend/src/model/config/panel_api.rs @@ -1,5 +1,5 @@ use crate::model::macros; -use shared::error::{TuliproxError, info_err_res}; +use shared::error::TuliproxError; use shared::model::{PanelApiAliasPoolDto, PanelApiAliasPoolSizeDto, PanelApiAliasPoolSizeValue, PanelApiConfigDto, PanelApiProvisioningDto, PanelApiQueryParamDto, PanelApiQueryParametersDto}; use std::sync::Arc; use shared::utils::Internable; @@ -73,18 +73,18 @@ impl PanelApiQueryParameters { .map(|p| p.value.trim().to_string()); match typ { Some(v) if v.eq_ignore_ascii_case("m3u") => Ok(()), - Some(v) => info_err_res!("panel_api: unsupported type={v}, only m3u is supported"), - None => info_err_res!("panel_api: missing required query param 'type=m3u'"), + Some(v) => Err(TuliproxError::Config(format!("panel_api: unsupported type={v}, only m3u is supported"))), + None => Err(TuliproxError::Config("panel_api: missing required query param 'type=m3u'".to_string())), } } fn require_api_key_param(params: &[PanelApiQueryParam], section: &str) -> Result<(), TuliproxError> { let api_key = params.iter().find(|p| p.key.trim().eq_ignore_ascii_case("api_key")); let Some(api_key) = api_key else { - return info_err_res!("panel_api: {section} must contain query param 'api_key' (use value 'auto')"); + return Err(TuliproxError::Config(format!("panel_api: {section} must contain query param 'api_key' (use value 'auto')"))); }; if api_key.value.trim().is_empty() { - return info_err_res!("panel_api: {section} query param 'api_key' must not be empty (use value 'auto')"); + return Err(TuliproxError::Config(format!("panel_api: {section} query param 'api_key' must not be empty (use value 'auto')"))); } Ok(()) } @@ -93,12 +93,12 @@ impl PanelApiQueryParameters { let username = params.iter().find(|p| p.key.trim().eq_ignore_ascii_case("username")); let password = params.iter().find(|p| p.key.trim().eq_ignore_ascii_case("password")); if username.is_none() || password.is_none() { - return info_err_res!("panel_api: {section} must contain query params 'username' and 'password' (use value 'auto')"); + return Err(TuliproxError::Config(format!("panel_api: {section} must contain query params 'username' and 'password' (use value 'auto')"))); } if !username.is_some_and(|p| p.value.trim().eq_ignore_ascii_case("auto")) || !password.is_some_and(|p| p.value.trim().eq_ignore_ascii_case("auto")) { - return info_err_res!("panel_api: {section} requires 'username: auto' and 'password: auto' (credentials must not be hardcoded)"); + return Err(TuliproxError::Config(format!("panel_api: {section} requires 'username: auto' and 'password: auto' (credentials must not be hardcoded)"))); } Ok(()) } @@ -113,7 +113,7 @@ impl PanelApiQueryParameters { Self::require_api_key_param(params, "query_parameter.client_new")?; Self::validate_type_is_m3u(params)?; if params.iter().any(|p| p.key.trim().eq_ignore_ascii_case("user")) { - return info_err_res!("panel_api: client_new must not contain query param 'user'"); + return Err(TuliproxError::Config("panel_api: client_new must not contain query param 'user'".to_string())); } Ok(()) } @@ -250,17 +250,19 @@ impl PanelApiConfig { return Ok(()); } if self.url.trim().is_empty() { - return info_err_res!("panel_api: url is missing"); + return Err(TuliproxError::Config("panel_api: url is missing".to_string())); } if self.api_key.as_ref().is_none_or(|k| k.trim().is_empty()) { - return info_err_res!("panel_api: api_key is missing"); + return Err(TuliproxError::Config("panel_api: api_key is missing".to_string())); } if self.query_parameter.client_info.is_empty() || self.query_parameter.client_new.is_empty() || self.query_parameter.client_renew.is_empty() { - return info_err_res!("panel_api: query_parameter.client_info/client_new/client_renew must be configured"); + return Err(TuliproxError::Config( + "panel_api: query_parameter.client_info/client_new/client_renew must be configured".to_string(), + )); } self.query_parameter.prepare() } -} \ No newline at end of file +} diff --git a/backend/src/model/config/source.rs b/backend/src/model/config/source.rs index 8911d6f14..f52810c86 100644 --- a/backend/src/model/config/source.rs +++ b/backend/src/model/config/source.rs @@ -1,7 +1,7 @@ use crate::model::{macros, ConfigInput, ConfigTarget, ProcessTargets}; use indexmap::IndexMap; use parking_lot::RwLock; -use shared::error::{info_err_res, TuliproxError}; +use shared::error::TuliproxError; use shared::model::{ ConfigProviderDto, ConfigSourceDto, DnsPrefer, DnsScheme, OnConnectErrorPolicy, OnResolveErrorPolicy, PatternTemplate, SourcesConfigDto, @@ -414,7 +414,7 @@ impl TryFrom<&SourcesConfigDto> for SourcesConfig { // Validate that all input references exist for input_name in &source_dto.inputs { if !input_names.contains(input_name) { - return info_err_res!("Source references unknown input: {input_name}"); + return Err(TuliproxError::ConfigSource(format!("Source references unknown input: {input_name}"))); } } sources.push(ConfigSource::from(source_dto)); @@ -488,7 +488,7 @@ impl SourcesConfig { let missing_targets: Vec = check_targets.iter().filter(|&(_, v)| *v == 0).map(|(k, _)| k.clone()).collect(); if !missing_targets.is_empty() { - return info_err_res!("No target found for {}", missing_targets.join(", ")); + return Err(TuliproxError::ConfigSource(format!("No target found for {}", missing_targets.join(", ")))); } // let processing_targets: Vec = check_targets.iter().filter(|&(_, v)| *v != 0).map(|(k, _)| k.to_string()).collect(); // info!("Processing targets {}", processing_targets.join(", ")); diff --git a/backend/src/model/config/web_auth.rs b/backend/src/model/config/web_auth.rs index 5ff9346d7..2ee9ec127 100644 --- a/backend/src/model/config/web_auth.rs +++ b/backend/src/model/config/web_auth.rs @@ -4,7 +4,7 @@ use std::io::{BufRead, ErrorKind}; use std::path::{Path, PathBuf}; use log::{debug, error, trace, warn}; -use shared::error::{info_err_res, TuliproxError}; +use shared::error::TuliproxError; use shared::model::permission::{permission_from_name, PermissionSet, PERM_ALL}; use shared::model::WebAuthConfigDto; @@ -99,11 +99,11 @@ impl WebAuthConfig { PathBuf::from(&userfile_name) }; if !utils::path_exists(&userfile_path) { - return info_err_res!("Could not find userfile {}", &userfile_name); + return Err(TuliproxError::Config(format!("Could not find userfile {}", &userfile_name))); } let Ok(file) = File::open(&userfile_path) else { - return info_err_res!("Could not read userfile {:?}", &userfile_path); + return Err(TuliproxError::Config(format!("Could not read userfile {}", userfile_path.display()))); }; let reader = utils::file_reader(file); let mut users = vec![]; diff --git a/backend/src/model/config/web_ui.rs b/backend/src/model/config/web_ui.rs index 4fa0671f9..c1b88e63f 100644 --- a/backend/src/model/config/web_ui.rs +++ b/backend/src/model/config/web_ui.rs @@ -1,3 +1,4 @@ +use shared::model::view_type::ViewType; use shared::error::TuliproxError; use shared::model::{ContentSecurityPolicyConfigDto, WebUiConfigDto}; use shared::utils::default_kick_secs; @@ -19,6 +20,7 @@ pub struct WebUiConfig { pub player_server: Option, pub kick_secs: u64, pub combine_views_stats_streams: bool, + pub landing_page: ViewType, } impl WebUiConfig { @@ -76,7 +78,8 @@ impl From<&WebUiConfigDto> for WebUiConfig { auth: dto.auth.as_ref().map(Into::into), player_server: dto.player_server.clone(), kick_secs: dto.kick_secs, - combine_views_stats_streams: dto.combine_views_stats_streams + combine_views_stats_streams: dto.combine_views_stats_streams, + landing_page: dto.landing_page, } } } @@ -90,7 +93,8 @@ impl From<&WebUiConfig> for WebUiConfigDto { auth: instance.auth.as_ref().map(Into::into), player_server: instance.player_server.clone(), kick_secs: instance.kick_secs, - combine_views_stats_streams: instance.combine_views_stats_streams + combine_views_stats_streams: instance.combine_views_stats_streams, + landing_page: instance.landing_page, } } } diff --git a/backend/src/model/xmltv.rs b/backend/src/model/xmltv.rs index 4dd121be9..6ac4df221 100644 --- a/backend/src/model/xmltv.rs +++ b/backend/src/model/xmltv.rs @@ -7,7 +7,7 @@ use chrono::{Datelike, TimeZone, Utc}; use futures::TryFutureExt; use quick_xml::events::{Event}; use shared::concat_string; -use shared::error::{TuliproxError, TuliproxErrorKind}; +use shared::error::TuliproxError; use shared::model::{EpgChannel, EpgProgramme, InputFetchMethod}; use shared::utils::{sanitize_sensitive_info, Internable}; use std::collections::HashMap; @@ -118,7 +118,7 @@ fn filter_channels_and_programmes( } pub async fn parse_xmltv_for_web_ui_from_file(path: &Path) -> Result, TuliproxError> { - let file = tokio::fs::File::open(path).map_err(|err| TuliproxError::new(TuliproxErrorKind::Info, err.to_string())).await?; + let file = tokio::fs::File::open(path).map_err(|err| TuliproxError::Io(err.to_string())).await?; parse_xmltv_for_web_ui(file).await } @@ -145,10 +145,10 @@ pub async fn parse_xmltv_for_web_ui_from_url(app_state: &Arc, url: &st Ok((stream, _url)) => { parse_xmltv_for_web_ui(stream).await } - Err(err) => Err(TuliproxError::new(TuliproxErrorKind::Info, format!("Failed to download: {} {err}", sanitize_sensitive_info(url)))) + Err(err) => Err(TuliproxError::Download(format!("Failed to download: {} {err}", sanitize_sensitive_info(url)))) } } else { - Err(TuliproxError::new(TuliproxErrorKind::Info, format!("Invalid url: {}", sanitize_sensitive_info(url)))) + Err(TuliproxError::UrlParse(format!("Invalid url: {}", sanitize_sensitive_info(url)))) } } @@ -288,7 +288,7 @@ async fn parse_xmltv_for_web_ui(reader: R) -> Resul current_tag.clear(); } Ok(Event::Eof) => break, - Err(err) => return Err(TuliproxError::new(TuliproxErrorKind::Info, err.to_string())), + Err(err) => return Err(TuliproxError::Parse(err.to_string())), _ => {} } diff --git a/backend/src/processing/parser/xtream.rs b/backend/src/processing/parser/xtream.rs index 91a0ed4dc..6312086f7 100644 --- a/backend/src/processing/parser/xtream.rs +++ b/backend/src/processing/parser/xtream.rs @@ -4,7 +4,7 @@ use crate::utils::request::DynReader; use crate::utils::xtream::get_xtream_stream_url_base; use indexmap::IndexMap; use serde::Deserializer; -use shared::error::{notify_err, notify_err_res, TuliproxError}; +use shared::error::TuliproxError; use shared::model::UUIDType; use shared::model::{EpisodeStreamProperties, LiveStreamProperties, PlaylistGroup, PlaylistItem, PlaylistItemHeader, PlaylistItemType, SeriesStreamDetailEpisodeProperties, @@ -41,10 +41,10 @@ async fn map_to_xtream_category(categories: DynReader, input_name: &Arc) -> match serde_json::from_reader::<_, Vec>(reader) { Ok(xtream_categories) => Ok(xtream_categories), Err(err) => { - notify_err_res!("Failed to process categories input {input_name_clone}: {err}") + Err(TuliproxError::RepositoryXtream(format!("Failed to process categories input {input_name_clone}: {err}"))) } } - }).await.map_err(|err| notify_err!("Mapping xtream categories failed for input {input_name}: {err}"))? + }).await.map_err(|err| TuliproxError::RepositoryXtream(format!("Mapping xtream categories failed for input {input_name}: {err}")))? } async fn map_to_xtream_streams(xtream_cluster: XtreamCluster, streams: DynReader, input_name: &Arc) -> Result, TuliproxError> { @@ -66,10 +66,10 @@ async fn map_to_xtream_streams(xtream_cluster: XtreamCluster, streams: DynReader Ok(stream_list) } Err(err) => { - notify_err_res!("Failed to map to xtream streams {xtream_cluster} for input {input_name_clone}: {err}") + Err(TuliproxError::RepositoryXtream(format!("Failed to map to xtream streams {xtream_cluster} for input {input_name_clone}: {err}"))) } } - }).await.map_err(|e| notify_err!("Mapping xtream streams failed for input {input_name}: {e}"))? + }).await.map_err(|e| TuliproxError::RepositoryXtream(format!("Mapping xtream streams failed for input {input_name}: {e}")))? } pub fn create_xtream_series_episode_url(url: &str, username: &str, password: &str, episode: &SeriesStreamDetailEpisodeProperties) -> Arc { @@ -318,7 +318,7 @@ where stream_prop, &mut on_item, live_stream_use_prefix, live_stream_without_extension, ordinal) }; let visitor = XtreamItemVisitor { on_item: &mut on_stream, _marker: std::marker::PhantomData }; - deserializer.deserialize_any(visitor).map_err(|e| notify_err!("JSON parse error: {e}"))?; + deserializer.deserialize_any(visitor).map_err(|e| TuliproxError::RepositoryXtream(format!("JSON parse error: {e}")))?; } XtreamCluster::Video => { let mut on_stream = |stream: VideoStreamProperties| { @@ -329,7 +329,7 @@ where stream_prop, &mut on_item, live_stream_use_prefix, live_stream_without_extension, ordinal) }; let visitor = XtreamItemVisitor { on_item: &mut on_stream, _marker: std::marker::PhantomData }; - deserializer.deserialize_any(visitor).map_err(|e| notify_err!("JSON parse error: {e}"))?; + deserializer.deserialize_any(visitor).map_err(|e| TuliproxError::RepositoryXtream(format!("JSON parse error: {e}")))?; } XtreamCluster::Series => { let mut on_stream = |stream: SeriesStreamProperties| { @@ -340,11 +340,11 @@ where stream_prop, &mut on_item, live_stream_use_prefix, live_stream_without_extension, ordinal) }; let visitor = XtreamItemVisitor { on_item: &mut on_stream, _marker: std::marker::PhantomData }; - deserializer.deserialize_any(visitor).map_err(|e| notify_err!("JSON parse error: {e}"))?; + deserializer.deserialize_any(visitor).map_err(|e| TuliproxError::RepositoryXtream(format!("JSON parse error: {e}")))?; } } Ok(()) - }).await.map_err(|e| notify_err!("Streaming parse failed: {e}"))??; + }).await.map_err(|e| TuliproxError::RepositoryXtream(format!("Streaming parse failed: {e}")))??; Ok(xtream_categories) } diff --git a/backend/src/processing/processor/playlist.rs b/backend/src/processing/processor/playlist.rs index 66c27eb8f..ec6497501 100644 --- a/backend/src/processing/processor/playlist.rs +++ b/backend/src/processing/processor/playlist.rs @@ -35,7 +35,7 @@ use indexmap::IndexMap; use log::{debug, error, info, log_enabled, warn, Level}; use shared::{ concat_string, - error::{get_errors_notify_message, notify_err, TuliproxError}, + error::{get_errors_notify_message, TuliproxError}, foundation::{get_field_value, set_field_value, Filter, ValueAccessor, ValueProvider}, model::{ ClusterSource, CounterModifier, FieldGetAccessor, FieldSetAccessor, InputStats, InputType, ItemField, @@ -596,7 +596,7 @@ async fn process_source( if playlist_groups.is_empty() { broadcast_step("Playlist download", &format!("Input '{}' playlist is empty", input.name)); info!("Source is empty {input_name}"); - errors.push(notify_err!("Source is empty {input_name}")); + errors.push(TuliproxError::RepositoryPlaylist(format!("Source is empty {input_name}"))); } else { source_playlists.push(FetchedPlaylist { input, source: playlist_groups, epg: tvguide }); log_memory_snapshot( @@ -621,10 +621,10 @@ async fn process_source( if source_downloaded { if source_playlists.is_empty() { debug!("Source at index {source_idx} is empty"); - errors.push(notify_err!( + errors.push(TuliproxError::RepositoryPlaylist(format!( "Source at index {source_idx} is empty: {}", source.inputs.iter().map(Clone::clone).collect::>>().join(", ") - )); + ))); } else { debug_if_enabled!( "Source has {} groups", @@ -1542,7 +1542,7 @@ pub async fn exec_processing( // log errors for err in &errors { - error!("{}", err.message); + error!("{}", err.message()); } if !stats.is_empty() { diff --git a/backend/src/processing/processor/stream_probe.rs b/backend/src/processing/processor/stream_probe.rs index f4b6c7e44..3bed794c2 100644 --- a/backend/src/processing/processor/stream_probe.rs +++ b/backend/src/processing/processor/stream_probe.rs @@ -52,7 +52,7 @@ pub async fn update_generic_stream_metadata( // Determine storage file path based on input type let storage_path = get_input_storage_path(&input.name, storage_dir).await - .map_err(|e| shared::error::info_err!("Storage path error: {e}"))?; + .map_err(|e| TuliproxError::Io(format!("Storage path error: {e}")))?; let (db_path, storage_kind) = match input.input_type { InputType::M3u | InputType::M3uBatch => ( @@ -82,7 +82,11 @@ pub async fn update_generic_stream_metadata( }; if !db_path.exists() { - return Err(shared::error::info_err!("Playlist DB file not found for input {}: {}", input.name, db_path.display())); + return Err(shared::error::TuliproxError::Config(format!( + "Playlist DB file not found for input {input_name}: {db_path_display}", + input_name = input.name, + db_path_display = db_path.display() + ))); } let needs_provider_connection = requires_provider_connection_for_generic_probe(input.input_type); @@ -98,7 +102,7 @@ pub async fn update_generic_stream_metadata( if needs_provider_connection && active_handle.is_none() && acquired_handle.is_none() { warn!("Skipping probe for generic stream {unique_id} due to connection limits"); - return Err(shared::error::info_err!("No connection available")); + return Err(TuliproxError::Probe(format!("Skipping probe for generic stream {unique_id} due to connection limits"))); } let probe_url = stream_url.to_string(); @@ -143,7 +147,7 @@ pub async fn update_generic_stream_metadata( ProbeUrlOutcome::Success(_quality, raw_video, raw_audio, stats) => (raw_video, raw_audio, stats), ProbeUrlOutcome::Failed(ProbeFailureKind::NotFound) => { warn!("Probe target not found (404) for generic stream: {unique_id}"); - return Err(shared::error::info_err!("Probe target returned 404 Not Found for stream {unique_id}")); + return Err(shared::error::TuliproxError::Probe(format!("Probe target returned 404 Not Found for stream {unique_id}"))); } ProbeUrlOutcome::Failed(ProbeFailureKind::Other) => { warn!("Probe failed or timed out for generic stream: {unique_id}"); @@ -246,8 +250,8 @@ pub async fn update_generic_stream_metadata( Ok(updated) }) .await - .map_err(|e| shared::error::info_err!("Failed to join generic probe DB update task: {e}"))? - .map_err(|e| shared::error::info_err!("{e}"))?; + .map_err(|e| shared::error::TuliproxError::Config(format!("Failed to join generic probe DB update task: {e}")))? + .map_err(shared::error::TuliproxError::Config)?; drop(file_lock); if updated { diff --git a/backend/src/processing/processor/trakt.rs b/backend/src/processing/processor/trakt.rs index 0bd52c271..a361932b6 100644 --- a/backend/src/processing/processor/trakt.rs +++ b/backend/src/processing/processor/trakt.rs @@ -250,7 +250,7 @@ impl TraktCategoriesProcessor { } } Err(err) => { - warn!("Failed to fetch Trakt list {cache_key}: {}", err.message); + warn!("Failed to fetch Trakt list {cache_key}: {}", err.message()); } } } diff --git a/backend/src/processing/processor/xtream.rs b/backend/src/processing/processor/xtream.rs index f21cf871d..8c156380c 100644 --- a/backend/src/processing/processor/xtream.rs +++ b/backend/src/processing/processor/xtream.rs @@ -23,7 +23,7 @@ pub async fn update_live_stream_metadata( ) -> Result, TuliproxError> { let storage_dir = &app_config.config.load().storage_dir; let storage_path = get_input_storage_path(&input.name, storage_dir).await - .map_err(|e| shared::error::info_err!("Storage path error: {e}"))?; + .map_err(|e| TuliproxError::Io(format!("Storage path error: {e}")))?; // Try to load existing info first to preserve data let mut props: Option = None; @@ -175,16 +175,16 @@ pub async fn update_live_stream_metadata( if let Some(stream_id) = stream_id_opt { persist_input_live_info(app_config, &storage_path, XtreamCluster::Live, &input.name, stream_id, &properties) .await - .map_err(|e| shared::error::info_err!("Persist error: {e}"))?; + .map_err(|e| shared::error::TuliproxError::Io(format!("Persist error: {e}")))?; } } if !success { if not_found { - return Err(shared::error::info_err!("Probe failed with 404 Not Found for stream {display_id}")); + return Err(shared::error::TuliproxError::Probe(format!("Probe failed with 404 Not Found for stream {display_id}"))); } // Return error to propagate failure up to task manager/logs - return Err(shared::error::info_err!("Probe failed for stream {display_id}")); + return Err(shared::error::TuliproxError::Probe(format!("Probe failed for stream {display_id}"))); } Ok(Some(properties)) diff --git a/backend/src/processing/processor/xtream_series.rs b/backend/src/processing/processor/xtream_series.rs index 620d6dcec..c8eef0817 100644 --- a/backend/src/processing/processor/xtream_series.rs +++ b/backend/src/processing/processor/xtream_series.rs @@ -649,7 +649,7 @@ pub async fn update_series_metadata( let storage_dir = &app_config.config.load().storage_dir; let storage_path = get_input_storage_path(&input.name, storage_dir) .await - .map_err(|e| shared::error::info_err!("Storage path error: {e}"))?; + .map_err(|e| TuliproxError::Io(format!("Storage path error: {e}")))?; if input.has_flag(ConfigInputFlags::XtreamSkipSeries) { return Ok(None); @@ -734,12 +734,12 @@ pub async fn update_series_metadata( if let Some(series_id) = series_id_opt { // Fetch Info from Provider let info_url = xtream::get_xtream_player_api_info_url(input, XtreamCluster::Series, series_id) - .ok_or_else(|| shared::error::info_err!("Failed to build info URL"))?; + .ok_or_else(|| shared::error::TuliproxError::Config("Failed to build info URL".to_string()))?; let input_source = InputSource::from(input).with_url(info_url); let content = xtream::get_xtream_stream_info_content(app_config, client, &input_source, true) .await - .map_err(|e| shared::error::info_err!("{e}"))?; + .map_err(|e| shared::error::TuliproxError::Config(format!("{e}")))?; if content.is_empty() { debug!("Series {display_id}: provider returned empty content for info fetch"); @@ -763,16 +763,16 @@ pub async fn update_series_metadata( properties_updated = true; } Err(e) => { - return Err(shared::error::info_err!( + return Err(shared::error::TuliproxError::Config(format!( "Series {display_id}: failed to parse XtreamSeriesInfo: {e}. Raw response: {content}" - )); + ))); } } } Err(e) => { - return Err(shared::error::info_err!( + return Err(shared::error::TuliproxError::Config(format!( "Series {display_id}: failed to parse provider response as JSON: {e}. Raw response: {content}" - )); + ))); } } } @@ -789,12 +789,12 @@ pub async fn update_series_metadata( }; props = Some(new_props); } else { - return Err(shared::error::info_err!("No Series properties available and no title found for {display_id}")); + return Err(shared::error::TuliproxError::Config(format!("No Series properties available and no title found for {display_id}"))); } } let mut properties = props.ok_or_else(|| { - shared::error::info_err!("No Series properties available after fallback creation for {display_id}") + shared::error::TuliproxError::Config(format!("No Series properties available after fallback creation for {display_id}")) })?; let resolve_tmdb_enabled = input.has_flag(ConfigInputFlags::ResolveTmdb); @@ -994,10 +994,10 @@ pub async fn update_series_metadata( if let Some(kind) = probe_failure { let err = match kind { ProbeFailureKind::NotFound => { - shared::error::info_err!("Probe failed with 404 Not Found for Series {display_id}") + shared::error::TuliproxError::Config(format!("Probe failed with 404 Not Found for Series {display_id}")) } - ProbeFailureKind::Other => shared::error::info_err!("Probe failed for Series {display_id}"), - ProbeFailureKind::Cancelled => shared::error::info_err!("Probe cancelled for Series {display_id}"), + ProbeFailureKind::Other => shared::error::TuliproxError::Config(format!("Probe failed for Series {display_id}")), + ProbeFailureKind::Cancelled => shared::error::TuliproxError::Config(format!("Probe cancelled for Series {display_id}")), }; return Err(err); } @@ -1016,7 +1016,7 @@ pub async fn update_series_metadata( &properties, ) .await - .map_err(|e| shared::error::info_err!("Persist error: {e}"))?; + .map_err(|e| shared::error::TuliproxError::Config(format!("Persist error: {e}")))?; debug_if_enabled!("Successfully updated Series metadata for ID {}", series_id); } diff --git a/backend/src/processing/processor/xtream_vod.rs b/backend/src/processing/processor/xtream_vod.rs index da0747d52..4f67c655a 100644 --- a/backend/src/processing/processor/xtream_vod.rs +++ b/backend/src/processing/processor/xtream_vod.rs @@ -625,7 +625,7 @@ pub async fn update_vod_metadata( if should_fetch_info { if let Some(stream_id) = stream_id_opt { let info_url = xtream::get_xtream_player_api_info_url(input, XtreamCluster::Video, stream_id) - .ok_or_else(|| shared::error::info_err!("Failed to build info URL"))?; + .ok_or_else(|| shared::error::TuliproxError::Config("Failed to build info URL".to_string()))?; let input_source = InputSource::from(input).with_url(info_url); @@ -670,12 +670,12 @@ pub async fn update_vod_metadata( props = Some(new_props); } else { // We can't proceed without at least a name - return Err(shared::error::info_err!("No VOD properties available and no title found for {display_id}")); + return Err(shared::error::TuliproxError::Config(format!("No VOD properties available and no title found for {display_id}"))); } } let Some(mut properties) = props else { - return Err(shared::error::info_err!("No VOD properties available after fallback creation for {display_id}")); + return Err(shared::error::TuliproxError::Config(format!("No VOD properties available after fallback creation for {display_id}"))); }; let resolve_tmdb_enabled = input.has_flag(ConfigInputFlags::ResolveTmdb); @@ -908,10 +908,10 @@ pub async fn update_vod_metadata( if let Some(kind) = probe_failure { let err = match kind { ProbeFailureKind::NotFound => { - shared::error::info_err!("Probe failed with 404 Not Found for VOD {display_id}") + shared::error::TuliproxError::Config(format!("Probe failed with 404 Not Found for VOD {display_id}")) } - ProbeFailureKind::Other => shared::error::info_err!("Probe failed for VOD {display_id}"), - ProbeFailureKind::Cancelled => shared::error::info_err!("Probe cancelled for VOD {display_id}"), + ProbeFailureKind::Other => shared::error::TuliproxError::Config(format!("Probe failed for VOD {display_id}")), + ProbeFailureKind::Cancelled => shared::error::TuliproxError::Config(format!("Probe cancelled for VOD {display_id}")), }; return Err(err); } @@ -930,7 +930,7 @@ pub async fn update_vod_metadata( &properties, ) .await - .map_err(|e| shared::error::info_err!("Persist error: {e}"))?; + .map_err(|e| shared::error::TuliproxError::Config(format!("Persist error: {e}")))?; } debug_if_enabled!("Successfully updated VOD metadata for '{}' (ID: {})", display_title, display_id); diff --git a/backend/src/repository/alias_repository.rs b/backend/src/repository/alias_repository.rs index d093b5cf1..6cb1c80d2 100644 --- a/backend/src/repository/alias_repository.rs +++ b/backend/src/repository/alias_repository.rs @@ -5,7 +5,6 @@ use crate::utils::{file_reader, resolve_relative_path}; use futures::TryFutureExt; use log::{error, warn}; use shared::error::{string_to_io_error, to_io_error, TuliproxError}; -use shared::info_err; use shared::model::{ConfigInputAliasDto, InputType}; use shared::utils::{get_credentials_from_url, get_credentials_from_url_str, parse_timestamp, sanitize_sensitive_info, Internable, BATCH_SCHEME_PREFIX, PROVIDER_SCHEME_PREFIX}; use std::io; @@ -361,14 +360,14 @@ pub async fn csv_patch_batch_append( // TODO check if alias name exists in any config ? let (file_path, mut aliases) = csv_read_inputs_from_path(input_type, csv_path) - .map_err(|err| info_err!("{err}")) + .map_err(|err| TuliproxError::ConfigInput(format!("{err}"))) .await?; let url = if input_type == InputType::M3uBatch { - let base = Url::parse(base_url).map_err(|e| info_err!("{e}"))?; + let base = Url::parse(base_url).map_err(|e| TuliproxError::Config(format!("{e}")))?; build_m3u_url(&base, Some(username), Some(password)) - .map_err(|e| info_err!("{e}"))? + .map_err(|e| TuliproxError::ConfigInput(format!("{e}")))? .to_string() } else { base_url.to_string() @@ -388,7 +387,7 @@ pub async fn csv_patch_batch_append( aliases.push(alias); csv_write_input_to_path(&file_path, &aliases) - .map_err(|err| info_err!("{err}")) + .map_err(|err| TuliproxError::ConfigInput(format!("{err}"))) .await?; Ok(()) } @@ -403,7 +402,7 @@ pub async fn csv_patch_batch_update_exp_date( ) -> Result<(), TuliproxError> { let mut matched = false; let (file_path, mut aliases) = csv_read_inputs_from_path(input_type, csv_path) - .map_err(|err| info_err!("{err}")) + .map_err(|err| TuliproxError::ConfigInput(format!("{err}"))) .await?; for alias in &mut aliases { if &alias.name == account_name @@ -424,7 +423,7 @@ pub async fn csv_patch_batch_update_exp_date( if matched { csv_write_input_to_path(&file_path, &aliases) - .map_err(|err| info_err!("{err}")) + .map_err(|err| TuliproxError::ConfigInput(format!("{err}"))) .await?; } else { warn!("panel_api: could not find batch csv row for account {account_name}"); @@ -445,7 +444,7 @@ pub async fn csv_patch_batch_update_credentials( ) -> Result<(), TuliproxError> { let mut matched = false; let (file_path, mut aliases) = csv_read_inputs_from_path(input_type, csv_path) - .map_err(|err| info_err!("{err}")) + .map_err(|err| TuliproxError::ConfigInput(format!("{err}"))) .await?; for alias in &mut aliases { @@ -518,7 +517,7 @@ pub async fn csv_patch_batch_update_credentials( if matched { csv_write_input_to_path(&file_path, &aliases) - .map_err(|err| info_err!("{err}")) + .map_err(|err| TuliproxError::ConfigInput(format!("{err}"))) .await?; } else { warn!("panel_api: could not find batch csv row to update credentials for account {account_name}"); @@ -531,14 +530,14 @@ pub async fn csv_patch_batch_remove_expired( csv_path: &Path, ) -> Result { let (file_path, mut aliases) = csv_read_inputs_from_path(input_type, csv_path) - .map_err(|err| info_err!("{err}")) + .map_err(|err| TuliproxError::ConfigInput(format!("{err}"))) .await?; let before_len = aliases.len(); aliases.retain(|alias| !is_input_expired(alias.exp_date)); let changed = before_len != aliases.len(); if changed { csv_write_input_to_path(&file_path, &aliases) - .map_err(|err| info_err!("{err}")) + .map_err(|err| TuliproxError::ConfigInput(format!("{err}"))) .await?; } Ok(changed) @@ -549,7 +548,7 @@ pub async fn csv_patch_batch_sort_by_exp_date( csv_path: &Path, ) -> Result { let (file_path, mut aliases) = csv_read_inputs_from_path(input_type, csv_path) - .map_err(|err| info_err!("{err}")) + .map_err(|err| TuliproxError::ConfigInput(format!("{err}"))) .await?; if aliases.len() < 2 { return Ok(false); @@ -565,7 +564,7 @@ pub async fn csv_patch_batch_sort_by_exp_date( } aliases = sorted; csv_write_input_to_path(&file_path, &aliases) - .map_err(|err| info_err!("{err}")) + .map_err(|err| TuliproxError::ConfigInput(format!("{err}"))) .await?; Ok(true) } diff --git a/backend/src/repository/epg_repository.rs b/backend/src/repository/epg_repository.rs index 370468f9d..891afb7ae 100644 --- a/backend/src/repository/epg_repository.rs +++ b/backend/src/repository/epg_repository.rs @@ -3,7 +3,7 @@ use crate::model::{Epg}; use crate::repository::{m3u_get_epg_file_path_for_target, BPlusTree}; use crate::repository::{xtream_get_epg_file_path_for_target, xtream_get_storage_path}; use crate::utils::{debug_if_enabled}; -use shared::error::{notify_err, TuliproxError}; +use shared::error::{ TuliproxError}; use shared::model::{EpgChannel, PlaylistGroup}; use std::collections::HashMap; use std::path::Path; @@ -18,11 +18,11 @@ pub const XML_PREAMBLE: &str = r#" // // // XML Header via events (DO NOT USE, kept for documentation): // writer.write_event_async(quick_xml::events::Event::Decl(quick_xml::events::BytesDecl::new("1.0", Some("utf-8"), None))) -// .await.map_err(|e| notify_err!("failed to write XML header: {}", e))?; +// .await.map_err(|e| TuliproxError::RepositoryEpg(format!("failed to write XML header: {}", e)))?; // // // DOCTYPE via events (DO NOT USE): // writer.write_event_async(quick_xml::events::Event::DocType(quick_xml::events::BytesText::new(r#"tv SYSTEM "xmltv.dtd""#))) -// .await.map_err(|e| notify_err!("failed to write doctype: {}", e))?; +// .await.map_err(|e| TuliproxError::RepositoryEpg(format!("failed to write doctype: {}", e)))?; pub fn epg_write_file( target_name: &str, epg: &Epg, @@ -45,7 +45,7 @@ pub fn epg_write_file( } } - tree.store(path).map_err(|err| notify_err!("Failed to write epg for target {}: {} - {err}", target_name, path.display()))?; + tree.store(path).map_err(|err| TuliproxError::RepositoryEpg(format!("Failed to write epg for target {}: {} - {err}", target_name, path.display())))?; debug_if_enabled!("Epg for target {} written to {}", target_name, path.display()); Ok(()) @@ -88,9 +88,16 @@ pub async fn epg_write_for_target(cfg: &Config, target: &ConfigTarget, target_pa epg_write_file(&target_name, &epg_data, &epg_path, &rename_map) }) .await - .map_err(|err| notify_err!("Failed to write epg for target {}: {err}", target_name_err))??; + .map_err(|err| TuliproxError::RepositoryEpg(format!( + "Failed to write epg for target {target_name_err}: {err}" + )))??; + } + None => { + return Err(TuliproxError::RepositoryEpg(format!( + "failed to write epg for target: {}, storage path not found", + target.name + ))) } - None => return Err(notify_err!("failed to write epg for target: {}, storage path not found", target.name)), } } TargetOutput::M3u(_) => { @@ -105,7 +112,9 @@ pub async fn epg_write_for_target(cfg: &Config, target: &ConfigTarget, target_pa epg_write_file(&target_name, &epg_data, &path, &rename_map) }) .await - .map_err(|err| notify_err!("Failed to write epg for target {}: {err}", target_name_err))??; + .map_err(|err| TuliproxError::RepositoryEpg(format!( + "Failed to write epg for target {target_name_err}: {err}" + )))??; } TargetOutput::Strm(_) | TargetOutput::HdHomeRun(_) => {} } diff --git a/backend/src/repository/library_repository.rs b/backend/src/repository/library_repository.rs index f779ab093..2d48ab157 100644 --- a/backend/src/repository/library_repository.rs +++ b/backend/src/repository/library_repository.rs @@ -1,6 +1,6 @@ use crate::model::AppConfig; use crate::repository::bplustree::{BPlusTree, BPlusTreeQuery}; -use shared::error::{notify_err, TuliproxError}; +use shared::error::{ TuliproxError}; use shared::model::{PlaylistGroup, PlaylistItem, StreamProperties, XtreamCluster, XtreamPlaylistItem}; use std::path::Path; use std::sync::Arc; @@ -53,10 +53,10 @@ pub async fn persist_input_library_playlist( tree .store(&library_path) .map(|_| ()) - .map_err(|err| notify_err!("failed to write local library playlist: {} - {err}", library_path.display())) + .map_err(|err| TuliproxError::RepositoryLibrary(format!("failed to write local library playlist: {} - {err}", library_path.display()))) }) .await - .map_err(|err| notify_err!("failed to write local library playlist: {} - {err}", library_path_err.display())); + .map_err(|err| TuliproxError::RepositoryLibrary(format!("failed to write local library playlist: {} - {err}", library_path_err.display()))); let playlist = match Arc::try_unwrap(playlist) { Ok(playlist) => playlist, @@ -141,7 +141,7 @@ pub async fn load_input_local_library_playlist(app_config: &Arc, lib_ Ok(groups.into_values().collect()) }) .await - .map_err(|err| notify_err!("failed to read local library playlist: {} - {err}", lib_path_err.display()))??; + .map_err(|err| TuliproxError::RepositoryLibrary(format!("failed to read local library playlist: {} - {err}", lib_path_err.display())))??; return Ok(groups); } diff --git a/backend/src/repository/m3u_playlist_iterator.rs b/backend/src/repository/m3u_playlist_iterator.rs index 4793b6cdf..e7629542a 100644 --- a/backend/src/repository/m3u_playlist_iterator.rs +++ b/backend/src/repository/m3u_playlist_iterator.rs @@ -1,4 +1,3 @@ -use shared::error::info_err; use shared::error::TuliproxError; use crate::model::{AppConfig, ProxyUserCredentials}; use crate::model::ConfigTarget; @@ -156,7 +155,7 @@ impl M3uPlaylistIterator { // TODO use playlist memory cache, but be aware of sorting ! - let m3u_output = target.get_m3u_output().ok_or_else(|| info_err!("Unexpected failure, missing m3u target output for target {}", target.name))?; + let m3u_output = target.get_m3u_output().ok_or_else(|| TuliproxError::Config(format!("Unexpected failure, missing m3u target output for target {}", target.name)))?; let config = cfg.config.load(); let target_path = ensure_target_storage_path(&config, target.name.as_str()).await?; let m3u_path = m3u_get_file_path_for_db(&target_path); diff --git a/backend/src/repository/m3u_repository.rs b/backend/src/repository/m3u_repository.rs index 3edce6933..c3611bd42 100644 --- a/backend/src/repository/m3u_repository.rs +++ b/backend/src/repository/m3u_repository.rs @@ -19,9 +19,8 @@ use log::error; use serde::{Deserialize, Serialize}; use shared::{ concat_string, - error::{notify_err, str_to_io_error, string_to_io_error, TuliproxError}, + error::{str_to_io_error, string_to_io_error, TuliproxError}, model::{M3uPlaylistItem, PlaylistGroup, PlaylistItem, PlaylistItemType, XtreamCluster}, - notify_err_res, utils::PROVIDER_SCHEME_PREFIX, }; use std::{ @@ -36,14 +35,14 @@ use tokio_stream::Stream; macro_rules! cant_write_result { ($path:expr, $err:expr) => { - notify_err!("failed to write m3u playlist: {} - {}", $path.display(), $err) + TuliproxError::RepositoryM3u(format!("failed to write m3u playlist: {} - {}", $path.display(), $err)) }; } macro_rules! await_playlist_write { ($expr:expr, $fmt:literal $(, $args:expr)* ) => {{ $expr.await.map_err(|err| { - notify_err!($fmt $(, $args)*, err) + TuliproxError::RepositoryM3u(format!($fmt $(, $args)*, err)) })? }}; } @@ -74,12 +73,12 @@ pub async fn ensure_m3u_storage_path(cfg: &Config, target_name: &str) -> Result< if let Some(path) = m3u_get_storage_path(cfg, target_name) { if tokio::fs::create_dir_all(&path).await.is_err() { let msg = format!("Failed to save m3u data, can't create directory {}", &path.display()); - return notify_err_res!("{msg}"); + return Err(TuliproxError::RepositoryM3u(msg)); } Ok(path) } else { let msg = format!("Failed to save m3u data, can't create directory for target {target_name}"); - notify_err_res!("{msg}") + Err(TuliproxError::RepositoryM3u(msg)) } } @@ -219,7 +218,7 @@ async fn persist_m3u_playlist_as_text( let input = provider_input_by_name .get(&m3u.input_name) - .ok_or_else(|| notify_err!("Input '{}' not found for provider URL resolution", m3u.input_name))?; + .ok_or_else(|| TuliproxError::RepositoryM3u(format!("Input '{}' not found for provider URL resolution", m3u.input_name)))?; let resolved = input.resolve_url(effective_url)?; if resolved.as_ref() == effective_url.as_ref() { @@ -307,7 +306,7 @@ pub async fn m3u_write_playlist( Ok(()) }) .await - .map_err(|err| notify_err!("failed to write m3u playlist: {} - {err}", m3u_path.display()))??; + .map_err(|err| TuliproxError::RepositoryM3u(format!("failed to write m3u playlist: {} - {err}", m3u_path.display())))??; Ok(()) } @@ -438,10 +437,10 @@ where tokio::spawn(async move { if let Err(err) = handle.await { error!("M3U playlist reader task failed for {}: {err}", m3u_path_for_log.display()); - let _ = join_err_tx.send(Err(notify_err!( + let _ = join_err_tx.send(Err(TuliproxError::RepositoryM3u(format!( "M3U playlist reader task failed for {}: {err}", m3u_path_for_log.display() - ))) + )))) .await; } }); @@ -472,7 +471,7 @@ pub async fn persist_input_m3u_playlist( Ok(()) }) .await - .map_err(|err| notify_err!("failed to write m3u playlist: {} - {err}", m3u_path.display()))??; + .map_err(|err| TuliproxError::RepositoryM3u(format!("failed to write m3u playlist: {} - {err}", m3u_path.display())))??; Ok(()) } @@ -515,7 +514,7 @@ pub async fn load_input_m3u_playlist( Ok(groups.into_values().collect()) }) .await - .map_err(|err| notify_err!("failed to read m3u playlist: {} - {err}", m3u_path_err.display()))??; + .map_err(|err| TuliproxError::RepositoryM3u(format!("failed to read m3u playlist: {} - {err}", m3u_path_err.display())))??; Ok(groups) } diff --git a/backend/src/repository/playlist_repository.rs b/backend/src/repository/playlist_repository.rs index 68d23da9a..c6f24cc91 100644 --- a/backend/src/repository/playlist_repository.rs +++ b/backend/src/repository/playlist_repository.rs @@ -14,7 +14,7 @@ use crate::repository::{LocalLibraryDiskPlaylistSource, M3uDiskPlaylistSource, M use crate::repository::{TargetIdMapping, VirtualIdRecord}; use crate::utils; use log::{info, warn}; -use shared::error::{info_err, TuliproxError}; +use shared::error::{ TuliproxError}; use shared::model::xtream_const::XTREAM_CLUSTER; use shared::model::{InputType, M3uPlaylistItem, PlaylistEntry, PlaylistGroup, PlaylistItem, PlaylistItemHeader, PlaylistItemType, StreamProperties, VirtualId, XtreamCluster, XtreamPlaylistItem}; use shared::utils::{is_dash_url, is_hls_url, Internable}; @@ -136,7 +136,7 @@ pub async fn persist_playlist(app_config: &Arc, playlist: &mut [Playl } if let Err(err) = target_id_mapping.persist() { - errors.push(info_err!("{err}")); + errors.push(TuliproxError::Config(format!("{err}"))); } // Keep lock until all outputs are persisted to prevent concurrent writers // from interleaving mapping and output state for the same target. @@ -315,7 +315,7 @@ pub async fn get_target_id_mapping(cfg: &AppConfig, target_path: &Path, use_memo TargetIdMapping::new(&mapping_path, use_memory_cache) }) .await - .map_err(|err| info_err!("spawn_blocking failed while creating TargetIdMapping: {err}"))??; + .map_err(|err| TuliproxError::Config(format!("spawn_blocking failed while creating TargetIdMapping: {err}")))??; Ok((mapping, file_lock)) } @@ -332,8 +332,8 @@ async fn load_target_id_mapping_as_tree(app_config: &AppConfig, target_path: &Pa BPlusTree::::load(&path_clone) }) .await - .map_err(|e| info_err!("Blocking task failed: {e}"))? - .map_err(|err| info_err!("Could not find path for target {} err:{err}", &target_name)) + .map_err(|e| TuliproxError::Config(format!("Blocking task failed: {e}")))? + .map_err(|err| TuliproxError::Config(format!("Could not find path for target {} err:{err}", &target_name))) } async fn load_xtream_playlist_as_tree(app_config: &AppConfig, storage_path: &Path, cluster: XtreamCluster) -> BPlusTree { @@ -353,7 +353,7 @@ async fn load_xtream_playlist_as_tree(app_config: &AppConfig, storage_path: &Pat async fn load_id_mapping_target_storage(app_config: &AppConfig, target: &ConfigTarget) -> Result, TuliproxError> { let config = app_config.config.load(); let target_path = get_target_storage_path(&config, target.name.as_str()).ok_or_else(|| - info_err!("Could not find path for target {}", &target.name))?; + TuliproxError::Config(format!("Could not find path for target {}", &target.name)))?; load_target_id_mapping_as_tree(app_config, &target_path, target).await } @@ -362,7 +362,7 @@ async fn load_xtream_target_storage(app_config: &AppConfig, target: &ConfigTarge let config = app_config.config.load(); let storage_path = xtream_get_storage_path(&config, target.name.as_str()).ok_or_else(|| - info_err!("Could not find path for target {} xtream output", &target.name))?; + TuliproxError::Config(format!("Could not find path for target {} xtream output", &target.name)))?; let live_storage = load_xtream_playlist_as_tree(app_config, &storage_path, XtreamCluster::Live).await; let vod_storage = load_xtream_playlist_as_tree(app_config, &storage_path, XtreamCluster::Video).await; @@ -378,7 +378,7 @@ async fn load_xtream_target_storage(app_config: &AppConfig, target: &ConfigTarge async fn load_m3u_target_storage(app_config: &AppConfig, target: &ConfigTarget) -> Result { let config = app_config.config.load(); let target_path = get_target_storage_path(&config, target.name.as_str()).ok_or_else(|| - info_err!("Could not find path for target {}", &target.name))?; + TuliproxError::Config(format!("Could not find path for target {}", &target.name)))?; let m3u_path = m3u_get_file_path_for_db(&target_path); let file_lock = app_config.file_locks.read_lock(&m3u_path).await; @@ -433,7 +433,7 @@ pub async fn persist_input_playlist(app_config: &Arc, input: &ConfigI let storage_path = match get_input_storage_path(&input.name, &cfg.storage_dir).await { Ok(storage_path) => storage_path, Err(err) => { - return (playlist, Some(info_err!("Error creating input storage directory for input '{}' failed: {err}", input.name))); + return (playlist, Some(TuliproxError::Config(format!("Error creating input storage directory for input '{}' failed: {err}", input.name)))); } }; @@ -466,7 +466,7 @@ pub async fn load_input_playlist(ctx: &PlaylistProcessingContext, input: &Config let app_config = &ctx.config; let cfg = app_config.config.load(); let storage_path = get_input_storage_path(&input.name, &cfg.storage_dir).await - .map_err(|e| info_err!("Error getting input path: {e}"))?; + .map_err(|e| TuliproxError::Config(format!("Error getting input path: {e}")))?; let disk_based_processing = cfg.disk_based_processing; match input.get_download_input_type() { diff --git a/backend/src/repository/playlist_stream.rs b/backend/src/repository/playlist_stream.rs index 0283e3528..040c975a5 100644 --- a/backend/src/repository/playlist_stream.rs +++ b/backend/src/repository/playlist_stream.rs @@ -2,7 +2,7 @@ use crate::utils::FileReadGuard; use futures::Stream; use log::error; use serde::{Deserialize, Serialize}; -use shared::error::{info_err, TuliproxError}; +use shared::error::{ TuliproxError}; use std::path::Path; use std::pin::Pin; use std::task::{Context, Poll}; @@ -48,7 +48,10 @@ where SortKey: for<'de> Deserialize<'de>, { let query = BPlusTreeQuery::::try_new(path) - .map_err(|err| info_err!("Could not open BPlusTreeQuery {path:?} - {err}"))?; + .map_err(|err| TuliproxError::Config(format!( + "Could not open BPlusTreeQuery {} - {err}", + path.display() + )))?; if index_path.exists() { match SortedIndexReader::::open(index_path) { diff --git a/backend/src/repository/storage.rs b/backend/src/repository/storage.rs index 1297d89be..aaee53da5 100644 --- a/backend/src/repository/storage.rs +++ b/backend/src/repository/storage.rs @@ -1,10 +1,9 @@ use crate::model::Config; use crate::repository::storage_const; use crate::utils; -use shared::error::notify_err; use shared::error::TuliproxError; use std::path::{Path, PathBuf}; -use shared::{concat_string, notify_err_res}; +use shared::concat_string; pub fn get_target_id_mapping_file(target_path: &Path) -> PathBuf { // Join directly with &str to avoid an intermediate PathBuf allocation @@ -13,14 +12,16 @@ pub fn get_target_id_mapping_file(target_path: &Path) -> PathBuf { pub async fn ensure_target_storage_path(cfg: &Config, target_name: &str) -> Result { if let Some(path) = get_target_storage_path(cfg, target_name) { - if tokio::fs::create_dir_all(&path).await.is_err() { - let msg = format!("Failed to save target data, can't create directory {}", path.display()); - return notify_err_res!("{msg}"); - } + tokio::fs::create_dir_all(&path).await.map_err(|err| { + TuliproxError::RepositoryStorage(format!( + "Failed to save target data, can't create directory {}: {err}", + path.display() + )) + })?; Ok(path) } else { let msg = format!("Failed to save target data, can't create directory for target {target_name}"); - notify_err_res!("{msg}") + Err(TuliproxError::RepositoryStorage(msg)) } } @@ -49,7 +50,7 @@ pub async fn get_input_storage_path(input_name: &str, storage_dir: &str) -> std: pub async fn ensure_input_storage_path(cfg: &Config, input_name: &str) -> Result { get_input_storage_path(input_name, &cfg.storage_dir).await .map_err(|err| { - notify_err!("Failed to save input data, can't create directory for input {input_name}: {err}") + TuliproxError::RepositoryStorage(format!("Failed to save input data, can't create directory for input {input_name}: {err}")) }) } @@ -59,4 +60,4 @@ pub fn get_geoip_path(storage_dir: &str) -> PathBuf { pub fn get_file_path_for_db_index(db_path: &Path) -> PathBuf { db_path.with_extension(storage_const::FILE_SUFFIX_INDEX) -} \ No newline at end of file +} diff --git a/backend/src/repository/strm_repository.rs b/backend/src/repository/strm_repository.rs index 4ce4174c0..7895ca531 100644 --- a/backend/src/repository/strm_repository.rs +++ b/backend/src/repository/strm_repository.rs @@ -8,7 +8,7 @@ use chrono::Datelike; use filetime::{set_file_times, FileTime}; use log::{error, trace}; use serde::Serialize; -use shared::error::{info_err_res, TuliproxError}; +use shared::error::TuliproxError; use shared::model::{ClusterFlags, MediaQuality, PlaylistGroup, PlaylistItem, PlaylistItemType, StreamProperties, StrmExportStyle}; use shared::utils::{arc_str_option_serde, arc_str_serde, clean_playlist_title, extract_extension_from_url, hash_bytes, hash_string_as_hex, is_blank_optional_arc_str, truncate_string, ExportStyleConfig, CONSTANTS}; @@ -260,7 +260,7 @@ async fn prepare_strm_output_directory(path: &Path) -> Result<(), TuliproxError> // Ensure the directory exists if let Err(e) = tokio::fs::create_dir_all(path).await { error!("Failed to create directory {}: {e}", path.display()); - return info_err_res!("Error creating STRM directory: {e}"); + return Err(TuliproxError::Config(format!("Error creating STRM directory: {e}"))); } Ok(()) } @@ -853,7 +853,7 @@ pub async fn write_strm_playlist( &config.storage_dir, Some(std::path::PathBuf::from(&target_output.directory)), ) else { - return info_err_res!("Failed to get file path for {}",target_output.directory); + return Err(TuliproxError::Config(format!("Failed to get file path for {}",target_output.directory))); }; let user_and_server_info = get_credentials_and_server_info(app_config, target_output.username.as_deref()); @@ -963,7 +963,7 @@ pub async fn write_strm_playlist( if failed.is_empty() { Ok(()) } else { - info_err_res!("{}", failed.join(", ")) + Err(TuliproxError::Config(failed.join(", "))) } } async fn write_strm_index_file( diff --git a/backend/src/repository/target_id_mapping.rs b/backend/src/repository/target_id_mapping.rs index 9bcaf4be3..7471ae102 100644 --- a/backend/src/repository/target_id_mapping.rs +++ b/backend/src/repository/target_id_mapping.rs @@ -3,7 +3,6 @@ use chrono::Local; use log::error; use serde::{Deserialize, Serialize}; use shared::error::TuliproxError; -use shared::info_err; use shared::model::{PlaylistItemType}; use shared::model::UUIDType; use std::cmp::max; @@ -78,9 +77,9 @@ impl TargetIdMapping { // Ensure both tree files exist ensure_tree_file::(path) - .map_err(|e| info_err!("Failed to create primary tree at {}: {e}", path.display()))?; + .map_err(|e| TuliproxError::Config(format!("Failed to create primary tree at {}: {e}", path.display())))?; ensure_tree_file::(&uuid_index_path) - .map_err(|e| info_err!("Failed to create UUID index at {}: {e}", uuid_index_path.display()))?; + .map_err(|e| TuliproxError::Config(format!("Failed to create UUID index at {}: {e}", uuid_index_path.display())))?; // Open disk-based update handles let mut disk_by_virtual_id = match BPlusTreeUpdate::::try_new_with_backoff(path) { @@ -89,7 +88,7 @@ impl TargetIdMapping { error!("Failed to open primary tree at {}: {e}", path.display()); // Create fresh and try again let _ = BPlusTree::::new().store(path); - BPlusTreeUpdate::try_new_with_backoff(path).map_err(|_| info_err!("Failed to create primary tree after retry"))? + BPlusTreeUpdate::try_new_with_backoff(path).map_err(|_| TuliproxError::Config("Failed to create primary tree after retry".to_string()))? } }; @@ -99,7 +98,7 @@ impl TargetIdMapping { error!("Failed to open UUID index at {}: {e}", uuid_index_path.display()); // Create fresh and try again let _ = BPlusTree::::new().store(&uuid_index_path); - BPlusTreeUpdate::try_new_with_backoff(&uuid_index_path).map_err(|_| info_err!("Failed to create UUID index after retry"))? + BPlusTreeUpdate::try_new_with_backoff(&uuid_index_path).map_err(|_| TuliproxError::Config("Failed to create UUID index after retry".to_string()))? } }; @@ -303,7 +302,7 @@ mod tests { #[test] fn test_disk_only_mode() -> Result<(), TuliproxError> { - let dir = tempdir().map_err(|_| info_err!("Failed to create temp dir"))?; + let dir = tempdir().map_err(|_| TuliproxError::Config("Failed to create temp dir".to_string()))?; let path = dir.path().join("id_mapping.db"); // Create mapping in disk-only mode @@ -312,7 +311,7 @@ mod tests { let mut mapping = TargetIdMapping::new(&path, false)?; let vid1 = mapping.get_and_update_virtual_id(&uuid1, 100, PlaylistItemType::Live, 0); assert_eq!(vid1, 1); - mapping.persist().map_err(|_| info_err!("Failed to persist mapping"))?; + mapping.persist().map_err(|_| TuliproxError::Config("Failed to persist mapping".to_string()))?; } // Reopen and verify persistence @@ -327,7 +326,7 @@ mod tests { #[test] fn test_memory_cache_mode() -> Result<(), TuliproxError> { - let dir = tempdir().map_err(|_| info_err!("Failed to create temp dir"))?; + let dir = tempdir().map_err(|_| TuliproxError::Config("Failed to create temp dir".to_string()))?; let path = dir.path().join("id_mapping_mem.db"); let uuid1 = UUIDType::default(); @@ -335,7 +334,7 @@ mod tests { let mut mapping = TargetIdMapping::new(&path, false)?; let vid1 = mapping.get_and_update_virtual_id(&uuid1, 100, PlaylistItemType::Video, 0); assert_eq!(vid1, 1); - mapping.persist().map_err(|err| info_err!("{err}"))?; + mapping.persist().map_err(|err| TuliproxError::Config(err.to_string()))?; } // Reopen with memory cache and verify diff --git a/backend/src/repository/xtream_playlist_iterator.rs b/backend/src/repository/xtream_playlist_iterator.rs index 1dcff034a..dcb5c1d95 100644 --- a/backend/src/repository/xtream_playlist_iterator.rs +++ b/backend/src/repository/xtream_playlist_iterator.rs @@ -5,7 +5,7 @@ use crate::repository::user_get_bouquet_filter; use crate::repository::{xtream_get_file_path, xtream_get_storage_path}; use futures::Stream; use log::error; -use shared::error::{TuliproxError, info_err, info_err_res}; +use shared::error::TuliproxError; use shared::model::{PlaylistItemType, TargetType, XtreamCluster, XtreamMappingOptions, XtreamPlaylistItem}; use std::collections::HashSet; use crate::repository::get_file_path_for_db_index; @@ -36,7 +36,7 @@ impl XtreamPlaylistIterator { if let Some(storage_path) = xtream_get_storage_path(&config, target.name.as_str()) { let xtream_path = xtream_get_file_path(&storage_path, cluster); if !xtream_path.exists() { - return info_err_res!("No {cluster} entries found for target {}", &target.name); + return Err(TuliproxError::Config(format!("No {cluster} entries found for target {}", &target.name))); } // Hold iter_lock for the stream lifetime (LockedReceiverStream), and bg_lock for the background reader. let iter_lock = app_config.file_locks.read_lock(&xtream_path).await; @@ -113,7 +113,7 @@ impl XtreamPlaylistIterator { inner: LockedReceiverStream::new(rx, iter_lock), }) } else { - info_err_res!("Failed to find xtream storage for target {}", &target.name) + Err(TuliproxError::Config(format!("Failed to find xtream storage for target {}", &target.name))) } } @@ -157,7 +157,7 @@ impl XtreamPlaylistJsonIterator { category_id: Option, user: &ProxyUserCredentials, ) -> Result { - let xtream_output = target.get_xtream_output().ok_or_else(|| info_err!("Unexpected: xtream output required for target {}", target.name))?; + let xtream_output = target.get_xtream_output().ok_or_else(|| TuliproxError::Config(format!("Unexpected: xtream output required for target {}", target.name)))?; let encrypt_secret = app_state.get_encrypt_secret(); let options = xtream_mapping_option_from_target_options( target, diff --git a/backend/src/repository/xtream_repository.rs b/backend/src/repository/xtream_repository.rs index 904456e49..b2b4d331e 100644 --- a/backend/src/repository/xtream_repository.rs +++ b/backend/src/repository/xtream_repository.rs @@ -22,11 +22,11 @@ use indexmap::IndexMap; use log::error; use serde::{Deserialize, Serialize}; use serde_json::{json, Value}; -use shared::error::{info_err_res, notify_err, string_to_io_error, TuliproxError}; +use shared::error::{string_to_io_error, TuliproxError}; use shared::model::xtream_const::XTREAM_CLUSTER; use shared::model::{LiveStreamProperties, PlaylistGroup, PlaylistItem, PlaylistItemType, SeriesStreamProperties, StreamProperties, VideoStreamProperties, XtreamCluster, XtreamPlaylistItem}; use shared::utils::{arc_str_serde, get_u32_from_serde_value, Internable}; -use shared::{concat_string, notify_err_res}; +use shared::concat_string; use std::collections::HashMap; use std::fs::File; use std::io::{Error, ErrorKind}; @@ -37,11 +37,11 @@ use tokio_stream::wrappers::ReceiverStream; macro_rules! cant_write_result { ($path:expr, $err:expr) => { - notify_err!( + TuliproxError::RepositoryXtream(format!( "failed to write xtream playlist: {} - {}", $path.display(), $err - ) + )) }; } @@ -73,12 +73,12 @@ pub async fn ensure_xtream_storage_path(cfg: &Config, target_name: &str) -> Resu "Failed to save xtream data, can't create directory {}", &path.display() ); - return notify_err_res!("{msg}"); + return Err(TuliproxError::RepositoryXtream(msg)); } Ok(path) } else { let msg = format!("Failed to save xtream data, can't create directory for target {target_name}"); - notify_err_res!("{msg}") + Err(TuliproxError::RepositoryXtream(msg)) } } @@ -124,7 +124,7 @@ async fn write_playlists_to_file( Ok(()) }) .await - .map_err(|e| notify_err!("Blocking task failed: {e}"))? + .map_err(|e| TuliproxError::RepositoryXtream(format!("Blocking task failed: {e}")))? .map_err(|err| cant_write_result!(&xtream_path, err))?; } Ok(()) @@ -142,12 +142,12 @@ pub async fn write_playlist_item_update( let xtream_path = xtream_get_file_path(&storage_path, pli.xtream_cluster); if !file_exists_async(&xtream_path).await { - return info_err_res!("BPlusTree file not found for update {}", xtream_path.display()); + return Err(TuliproxError::RepositoryXtream(format!("BPlusTree file not found for update {}", xtream_path.display()))); } // Prepare encoded payload before opening the writer lock. let prepared_items = BPlusTreeUpdate::::prepare_upsert_batch(&[(&pli.virtual_id, pli)]) - .map_err(|e| notify_err!("Failed to serialize value: {e}"))?; + .map_err(|e| TuliproxError::RepositoryXtream(format!("Failed to serialize value: {e}")))?; // Keep FileLockManager lock for cross-operation coordination (e.g. swap + update). let file_lock = app_config.file_locks.write_lock(&xtream_path).await; @@ -160,7 +160,7 @@ pub async fn write_playlist_item_update( Ok(()) }) .await - .map_err(|e| notify_err!("Blocking task failed: {e}"))? + .map_err(|e| TuliproxError::RepositoryXtream(format!("Blocking task failed: {e}")))? .map_err(|err| cant_write_result!(&xtream_path, err))?; Ok(()) @@ -183,7 +183,7 @@ pub async fn write_playlist_batch_item_upsert( let xtream_path = xtream_get_file_path(&storage_path, xtream_cluster); if !file_exists_async(&xtream_path).await { - return info_err_res!("BPlusTree file not found for upsert {}", xtream_path.display()); + return Err(TuliproxError::RepositoryXtream(format!("BPlusTree file not found for upsert {}", xtream_path.display()))); } // Prepare encoded payload before opening the writer lock. @@ -192,7 +192,7 @@ pub async fn write_playlist_batch_item_upsert( .map(|pli| (&pli.virtual_id, pli)) .collect(); let prepared_items = BPlusTreeUpdate::::prepare_upsert_batch(&batch_refs) - .map_err(|e| notify_err!("Failed to serialize value: {e}"))?; + .map_err(|e| TuliproxError::RepositoryXtream(format!("Failed to serialize value: {e}")))?; // Keep FileLockManager lock for cross-operation coordination (e.g. swap + update). let file_lock = app_config.file_locks.write_lock(&xtream_path).await; @@ -205,7 +205,7 @@ pub async fn write_playlist_batch_item_upsert( Ok(()) }) .await - .map_err(|e| notify_err!("Blocking task failed: {e}"))? + .map_err(|e| TuliproxError::RepositoryXtream(format!("Blocking task failed: {e}")))? .map_err(|err| cant_write_result!(&xtream_path, err))?; Ok(()) @@ -376,7 +376,7 @@ pub async fn xtream_write_playlist( } if !errors.is_empty() { - return info_err_res!("{}", errors.join("\n")); + return Err(TuliproxError::Config(errors.join("\n"))); } Ok(()) @@ -791,7 +791,7 @@ fn preserve_details_input_xtream_playlist_cluster_to_disk( updates.push((new_item.provider_id, new_item)); if updates.len() >= BATCH_SIZE { let refs: Vec<(&u32, &XtreamPlaylistItem)> = updates.iter().map(|(id, pli)| (id, pli)).collect(); - new_tree.update_batch(&refs).map_err(|e| notify_err!("Failed to update tmp tree during merge: {e}"))?; + new_tree.update_batch(&refs).map_err(|e| TuliproxError::RepositoryXtream(format!("Failed to update tmp tree during merge: {e}")))?; updates.clear(); } } @@ -805,11 +805,11 @@ fn preserve_details_input_xtream_playlist_cluster_to_disk( let refs: Vec<(&u32, &XtreamPlaylistItem)> = updates.iter().map(|(id, pli)| (id, pli)).collect(); new_tree.update_batch(&refs) - .map_err(|e| notify_err!("Failed to update tmp tree during merge: {e}"))?; + .map_err(|e| TuliproxError::RepositoryXtream(format!("Failed to update tmp tree during merge: {e}")))?; } new_tree.commit() - .map_err(|e| notify_err!("Failed to commit tmp tree merge: {e}"))?; + .map_err(|e| TuliproxError::RepositoryXtream(format!("Failed to commit tmp tree merge: {e}")))?; Ok(()) } @@ -847,7 +847,7 @@ pub async fn persist_input_xtream_playlist_cluster_to_disk( // This is safe here because it runs within its own tokio::spawn task. if let Err(e) = tx_for_closure.blocking_send(item) { error!("Channel closed while processing {cluster} for item {item_id}: {e}"); - return notify_err_res!("Channel closed while processing {cluster}"); + return Err(TuliproxError::RepositoryXtream(format!("Channel closed while processing {cluster}"))); } Ok(()) }, @@ -872,13 +872,13 @@ pub async fn persist_input_xtream_playlist_cluster_to_disk( "Failed to initialize ghost BPlusTree at {}: {e}", tmp_xtream_path.display() ); - notify_err!("Init tree error {e}") + TuliproxError::RepositoryXtream(format!("Init tree error {e}")) })?; let mut tree: BPlusTreeUpdate = BPlusTreeUpdate::try_new_with_backoff(&tmp_xtream_path).map_err(|e| { error!("Failed to open ghost tree at {}: {e}", tmp_xtream_path.display()); - notify_err!("Failed to open tree {e}") + TuliproxError::RepositoryXtream(format!("Failed to open tree {e}")) })?; tree.set_flush_policy(FlushPolicy::Batch); @@ -893,11 +893,11 @@ pub async fn persist_input_xtream_playlist_cluster_to_disk( let prepared = BPlusTreeUpdate::::prepare_upsert_batch(&batch) .map_err(|e| { error!("Batch prepare failed for cluster {cluster}: {e}"); - notify_err!("Prepare failed {e}") + TuliproxError::RepositoryXtream(format!("Prepare failed {e}")) })?; tree.upsert_batch_encoded(prepared).map_err(|e| { error!("Batch upsert failed for cluster {cluster}: {e}"); - notify_err!("Upsert failed {e}") + TuliproxError::RepositoryXtream(format!("Upsert failed {e}")) })?; buffer.clear(); } @@ -910,17 +910,17 @@ pub async fn persist_input_xtream_playlist_cluster_to_disk( let prepared = BPlusTreeUpdate::::prepare_upsert_batch(&batch) .map_err(|e| { error!("Final batch prepare failed for cluster {cluster}: {e}"); - notify_err!("Prepare failed {e}") + TuliproxError::RepositoryXtream(format!("Prepare failed {e}")) })?; tree.upsert_batch_encoded(prepared).map_err(|e| { error!("Final batch upsert failed for cluster {cluster}: {e}"); - notify_err!("Upsert failed {e}") + TuliproxError::RepositoryXtream(format!("Upsert failed {e}")) })?; } tree.commit().map_err(|e| { error!("Commit failed for cluster {cluster}: {e}"); - notify_err!("Commit failed {e}") + TuliproxError::RepositoryXtream(format!("Commit failed {e}")) })?; Ok::<(), TuliproxError>(()) }); @@ -928,7 +928,7 @@ pub async fn persist_input_xtream_playlist_cluster_to_disk( // 3. Robust Joining of both tasks // try_join! returns immediately if any task returns an error or panics. let (parse_res, consumer_res) = tokio::try_join!(parse_task, consumer_task) - .map_err(|e| notify_err!("Task join error during cluster {cluster} update: {e}"))?; + .map_err(|e| TuliproxError::RepositoryXtream(format!("Task join error during cluster {cluster} update: {e}")))?; // Handle internal errors from the tasks let parsed_categories = parse_res?; @@ -957,7 +957,7 @@ pub async fn persist_input_xtream_playlist_cluster_to_disk( let new_tmp_path = tmp_xtream_path.clone(); tokio::task::spawn_blocking(move || preserve_details_input_xtream_playlist_cluster_to_disk(&old_path, &new_tmp_path)) .await - .map_err(|e| notify_err!("Merge task join error during cluster {cluster} update: {e}"))??; + .map_err(|e| TuliproxError::RepositoryXtream(format!("Merge task join error during cluster {cluster} update: {e}")))??; } // Optional compaction to optimize the newly created database file. @@ -993,7 +993,7 @@ pub async fn persist_input_xtream_playlist_cluster_to_disk( tmp_xtream_path.display(), xtream_path.display() ); - return notify_err_res!("Failed to swap database: {e}"); + return Err(TuliproxError::RepositoryXtream(format!("Failed to swap database: {e}"))); } // Atomic Swap: Replace old categories with new ones @@ -1003,7 +1003,7 @@ pub async fn persist_input_xtream_playlist_cluster_to_disk( tmp_col_path.display(), col_path.display() ); - return notify_err_res!("Failed to swap categories: {e}"); + return Err(TuliproxError::RepositoryXtream(format!("Failed to swap categories: {e}"))); } // Cleanup: Remove temporary files if they still exist (defensive) @@ -1041,16 +1041,16 @@ async fn save_xtream_categories_to_file( } serde_json::to_writer(&file, &cat_entries).map_err(|e| { error!("Failed to write categories to file {}: {e}", col_path_buf.display()); - notify_err!("Write failed: {e}") + TuliproxError::RepositoryXtream(format!("Write failed: {e}")) })?; let _ = file.unlock(); } else { - return notify_err_res!("Failed to create category file {}", col_path_buf.display()); + return Err(TuliproxError::RepositoryXtream(format!("Failed to create category file {}", col_path_buf.display()))); } Ok(()) }) .await - .map_err(|e| notify_err!("Spawn error {e}"))? + .map_err(|e| TuliproxError::RepositoryXtream(format!("Spawn error {e}")))? } #[allow(clippy::too_many_lines)] @@ -1188,7 +1188,7 @@ pub async fn persist_input_xtream_playlist(app_config: &Arc, storage_ let err = if errors.is_empty() { None } else { - Some(notify_err!("{}", errors.join("\n"))) + Some(TuliproxError::RepositoryXtream(errors.join("\n"))) }; (result, err) @@ -1482,7 +1482,9 @@ pub async fn load_input_xtream_playlist(app_config: &Arc, storage_pat Ok(items) }) .await - .map_err(|err| notify_err!("failed to read xtream playlist: {} - {err}", xtream_display))??; + .map_err(|err| TuliproxError::RepositoryXtream(format!( + "failed to read xtream playlist: {xtream_display} - {err}" + )))??; for item in items { let cat_id = item.category_id; diff --git a/backend/src/utils/crypto_utils.rs b/backend/src/utils/crypto_utils.rs index eab5b723f..d36bb779c 100644 --- a/backend/src/utils/crypto_utils.rs +++ b/backend/src/utils/crypto_utils.rs @@ -1,7 +1,7 @@ use base64::{engine::general_purpose, Engine as _}; use openssl::symm::{Cipher, Crypter, Mode}; use rand::{RngCore, rngs::OsRng, TryRngCore}; -use shared::error::{TuliproxError, TuliproxErrorKind}; +use shared::error::TuliproxError; use shared::utils::encode_base64_string; pub fn encode_base64_hash(text: &str) -> String { @@ -17,10 +17,10 @@ pub fn obscure_text(secret: &[u8;16], url: &str) -> Result Result Result { - // Base64 decode - let data = general_purpose::URL_SAFE_NO_PAD.decode(encoded).map_err(|_err| TuliproxError::new(TuliproxErrorKind::Info, "Can't decode base64".to_string()))?; + let data = general_purpose::URL_SAFE_NO_PAD.decode(encoded).map_err(|_| TuliproxError::Crypto("Can't decode base64".to_string()))?; if data.len() < 16 { - return Err(TuliproxError::new( - TuliproxErrorKind::Info, - "Token too short to contain IV".to_string(), - )); + return Err(TuliproxError::Crypto("Token too short to contain IV".to_string())); } let (iv, ciphertext) = data.split_at(16); - // AES-CTR Decryption let cipher = Cipher::aes_128_ctr(); - let mut crypter = Crypter::new(cipher, Mode::Decrypt, secret, Some(iv)).map_err(|_err| TuliproxError::new(TuliproxErrorKind::Info, "Can't create decrypt cipher".to_string()))?; + let mut crypter = Crypter::new(cipher, Mode::Decrypt, secret, Some(iv)).map_err(|_| TuliproxError::Crypto("Can't create decrypt cipher".to_string()))?; let mut buf = vec![0u8; ciphertext.len() + cipher.block_size()]; - let mut count = crypter.update(ciphertext, &mut buf).map_err(|_errerr| TuliproxError::new(TuliproxErrorKind::Info, "Can't decrypt".to_string()))?; - count += crypter.finalize(&mut buf[count..]).map_err(|_err| TuliproxError::new(TuliproxErrorKind::Info, "Can't finalize decrypt".to_string()))?; + let mut count = crypter.update(ciphertext, &mut buf).map_err(|_| TuliproxError::Crypto("Can't decrypt".to_string()))?; + count += crypter.finalize(&mut buf[count..]).map_err(|_| TuliproxError::Crypto("Can't finalize decrypt".to_string()))?; buf.truncate(count); - String::from_utf8(buf).map_err(|_err| TuliproxError::new(TuliproxErrorKind::Info, "Can't create utf8 string from decrypted".to_string())) + String::from_utf8(buf).map_err(|_| TuliproxError::Crypto("Can't create utf8 string from decrypted".to_string())) } #[cfg(test)] @@ -69,4 +64,4 @@ mod tests { assert_eq!(decrypted, plain); } -} \ No newline at end of file +} diff --git a/backend/src/utils/file/config_reader.rs b/backend/src/utils/file/config_reader.rs index 0d53e38b1..545348361 100644 --- a/backend/src/utils/file/config_reader.rs +++ b/backend/src/utils/file/config_reader.rs @@ -15,7 +15,7 @@ use arc_swap::{ArcSwap, ArcSwapAny}; use chrono::Local; use log::{error, info, warn}; use serde::Serialize; -use shared::error::{info_err, info_err_res, TuliproxError}; +use shared::error::TuliproxError; use shared::foundation::prepare_templates; use shared::model::{ ApiProxyConfigDto, AppConfigDto, ConfigDto, ConfigInputAliasDto, ConfigPaths, HdHomeRunDeviceOverview, InputType, @@ -95,19 +95,19 @@ async fn parse_sources_file_from_path( serde_saphyr::from_reader(config_file_reader(file, resolve_env)); match maybe_sources { Ok(sources) => Ok(sources), - Err(err) => info_err_res!( + Err(err) => Err(TuliproxError::ConfigSource(format!( "Can't read the sources-config file: {}: {err}", sources_file.display() - ), + ))), } } - Err(err) => info_err_res!( + Err(err) => Err(TuliproxError::ConfigSource(format!( "Can't read the sources-config file: {}: {err}", sources_file.display() - ), + ))), }) .await - .map_err(|join_err| info_err!("Failed to read sources-config file: {join_err}"))? + .map_err(|join_err| TuliproxError::ConfigSource(format!("Failed to read sources-config file: {join_err}")))? } pub fn resolve_template_and_mapping_paths( @@ -136,10 +136,10 @@ pub async fn read_sources_file_from_path_with_templates( let mut sources = parse_sources_file_from_path(sources_file, resolve_env).await?; if resolve_env { if let Err(err) = sources.prepare(include_computed, hdhr_config, prepared_templates) { - return info_err_res!( + return Err(TuliproxError::Config(format!( "Can't read the sources-config file: {}: {err}", sources_file.display() - ); + ))); } } Ok(sources) @@ -180,10 +180,10 @@ pub fn read_config_file( } Ok(config) } - Err(err) => info_err_res!("Can't read the config file: {config_file}: {err}"), + Err(err) => Err(TuliproxError::Config(format!("Can't read the config file: {config_file}: {err}"))), } } - Err(err) => info_err_res!("Can't read the config file: {config_file}: {err}"), + Err(err) => Err(TuliproxError::Config(format!("Can't read the config file: {config_file}: {err}"))), } } @@ -281,10 +281,10 @@ pub(crate) fn read_templates( .collect::>(); if !duplicate_templates.is_empty() { duplicate_templates.sort(); - return info_err_res!( + return Err(TuliproxError::Config(format!( "Duplicate template names found across merged template sources: {}", duplicate_templates.join("; ") - ); + ))); } let files_used = if loaded_template_files.is_empty() { @@ -450,16 +450,16 @@ pub async fn get_batch_aliases( ) -> Result)>, TuliproxError> { if input_type == InputType::M3uBatch || input_type == InputType::XtreamBatch { if url.starts_with(PROVIDER_SCHEME_PREFIX) { - return info_err_res!( + return Err(TuliproxError::Config(format!( "Batch input type '{input_type}' does not support provider:// URLs. \ Use a batch:// URL or a local CSV path (absolute/relative)." - ); + ))); } return match csv_read_inputs(input_type, url).await { Ok((file_path, batch_aliases)) => Ok(Some((file_path, batch_aliases))), Err(err) => { - info_err_res!("{err}") + Err(TuliproxError::Config(format!("{err}"))) } }; } @@ -634,7 +634,7 @@ pub fn read_api_proxy_file( Ok(Some(api_proxy_dto)) } Err(err) => { - info_err_res!("can't read api-proxy-config file: {err}") + Err(TuliproxError::Config(format!("can't read api-proxy-config file: {err}"))) } } }) @@ -682,7 +682,7 @@ where ..Default::default() }; serde_saphyr::to_fmt_writer_with_options(&mut serialized, &config, options) - .map_err(|err| info_err!("Could not serialize config: {}", err))?; + .map_err(|err| TuliproxError::Config(format!("Could not serialize config: {err}")))?; if file_exists_async(&path).await { if let Ok(existing) = fs::read_to_string(&path).await { @@ -705,7 +705,7 @@ where info!("Saving file to {}", &path.to_str().unwrap_or("?")); } - let parent_dir = path.parent().ok_or_else(|| { info_err!("Could not write file {}: missing parent directory", &path.to_str().unwrap_or("?"))})?; + let parent_dir = path.parent().ok_or_else(|| { TuliproxError::Config(format!("Could not write file {}: missing parent directory", &path.to_str().unwrap_or("?")))})?; let dest_file_name = path .file_name() @@ -715,7 +715,7 @@ where let mut tmp_path = parent_dir.to_path_buf(); tmp_path.push(format!(".{dest_file_name}.tmp-{}-{}", std::process::id(), Local::now().timestamp_nanos_opt().unwrap_or_default())); - fs::write(&tmp_path, serialized).await.map_err(|err| { info_err!("Could not write temp file {}: {err}", &tmp_path.to_str().unwrap_or("?"))})?; + fs::write(&tmp_path, serialized).await.map_err(|err| { TuliproxError::Config(format!("Could not write temp file {}: {err}", &tmp_path.to_str().unwrap_or("?")))})?; match fs::rename(&tmp_path, &path).await { Ok(()) => Ok(()), @@ -732,11 +732,11 @@ where // Best-effort cleanup; if the temp file can't be removed, ignore it. let _ = fs::remove_file(&tmp_path).await; - Err(info_err!( + Err(TuliproxError::Config(format!( "Could not replace file {} with {}: {err}", &path.to_str().unwrap_or("?"), &tmp_path.to_str().unwrap_or("?") - )) + ))) } } } @@ -1008,7 +1008,7 @@ async fn persist_single_template(prefix: &str, kind: Option<&MsgKind>, template: if !file_exists_async(templates_dir).await { tokio::fs::create_dir_all(templates_dir) .await - .map_err(|e| info_err!("Messaging templates dir: failed to create dir: {} {e}", templates_dir.display()))?; + .map_err(|e| TuliproxError::Config(format!("Messaging templates dir: failed to create dir: {} {e}", templates_dir.display())))?; } let filename = if let Some(k) = kind { @@ -1018,11 +1018,11 @@ async fn persist_single_template(prefix: &str, kind: Option<&MsgKind>, template: }; let file_path = templates_dir.join(filename); - fs::write(&file_path, template).await.map_err(|e| info_err!("Failed to write template file: {e}"))?; + fs::write(&file_path, template).await.map_err(|e| TuliproxError::Config(format!("Failed to write template file: {e}")))?; Url::from_file_path(&file_path) .map(|u| u.to_string()) - .map_err(|()| info_err!("Failed to convert persisted path to file URL: {}", file_path.display())) + .map_err(|()| TuliproxError::Config(format!("Failed to convert persisted path to file URL: {}", file_path.display()))) } diff --git a/backend/src/utils/file/mapping_reader.rs b/backend/src/utils/file/mapping_reader.rs index 7679b78e7..f2889958e 100644 --- a/backend/src/utils/file/mapping_reader.rs +++ b/backend/src/utils/file/mapping_reader.rs @@ -1,11 +1,10 @@ use std::collections::HashMap; use crate::model::{Mappings}; -use shared::error::{info_err_res, TuliproxError}; +use shared::error::TuliproxError; use crate::utils::traverse_dir; use crate::utils::{config_file_reader, open_file}; use log::{warn}; use std::path::{Path, PathBuf}; -use shared::info_err; use shared::model::{MappingDefinitionDto, MappingDto, MappingsDto, PatternTemplate}; fn read_mapping(mapping_file: &Path, resolve_var: bool) -> Result, TuliproxError> { @@ -14,7 +13,7 @@ fn read_mapping(mapping_file: &Path, resolve_var: bool) -> Result Ok(Some(mapping)), Err(err) => { - info_err_res!("{err}") + Err(TuliproxError::Config(format!("{err}"))) } }; } @@ -85,7 +84,7 @@ fn read_mappings_from_directory(path: &Path, resolve_env: bool) -> Result