diff --git a/CHANGELOG.md b/CHANGELOG.md index fef1c6bd7..55a853ac5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,9 @@ # Changelog +# 3.1.0 (2025-05-xx) +- Fixed mapper transform capitalize. +- Auto hot reload for `mapping.yml`and `api_proxy.yml` +To enable set `config_hot_reload: true` in `config.yml` + # 3.0.0 (2025-05-12) - !BREAKING_CHANGE! user has now the attribute `ui_enabled` to disable/enable web_ui for user. You need to migrate the user db if you have used `use_user_db:true`. diff --git a/src/api/api_utils.rs b/src/api/api_utils.rs index 00b715797..2d34c181b 100644 --- a/src/api/api_utils.rs +++ b/src/api/api_utils.rs @@ -121,31 +121,31 @@ pub async fn serve_file(file_path: &Path, mime_type: mime::Mime) -> impl axum::r axum::http::StatusCode::NOT_FOUND.into_response() } -pub async fn get_user_target_by_username<'a>(username: &str, app_state: &'a AppState) -> Option<(ProxyUserCredentials, &'a ConfigTarget)> { +pub fn get_user_target_by_username<'a>(username: &str, app_state: &'a AppState) -> Option<(ProxyUserCredentials, &'a ConfigTarget)> { if !username.is_empty() { - return app_state.config.get_target_for_username(username).await; + return app_state.config.get_target_for_username(username); } None } -pub async fn get_user_target_by_credentials<'a>(username: &str, password: &str, api_req: &'a UserApiRequest, +pub fn get_user_target_by_credentials<'a>(username: &str, password: &str, api_req: &'a UserApiRequest, app_state: &'a AppState) -> Option<(ProxyUserCredentials, &'a ConfigTarget)> { if !username.is_empty() && !password.is_empty() { - app_state.config.get_target_for_user(username, password).await + app_state.config.get_target_for_user(username, password) } else { let token = api_req.token.as_str().trim(); if token.is_empty() { None } else { - app_state.config.get_target_for_user_by_token(token).await + app_state.config.get_target_for_user_by_token(token) } } } -pub async fn get_user_target<'a>(api_req: &'a UserApiRequest, app_state: &'a AppState) -> Option<(ProxyUserCredentials, &'a ConfigTarget)> { +pub fn get_user_target<'a>(api_req: &'a UserApiRequest, app_state: &'a AppState) -> Option<(ProxyUserCredentials, &'a ConfigTarget)> { let username = api_req.username.as_str().trim(); let password = api_req.password.as_str().trim(); - get_user_target_by_credentials(username, password, api_req, app_state).await + get_user_target_by_credentials(username, password, api_req, app_state) } pub struct StreamOptions { diff --git a/src/api/config_watch.rs b/src/api/config_watch.rs index 67f63fa23..c836b8d21 100644 --- a/src/api/config_watch.rs +++ b/src/api/config_watch.rs @@ -16,7 +16,7 @@ enum ConfigFile { } impl ConfigFile { - async fn load_mappping(app_state: &Arc) -> Result<(), TuliproxError> { + fn load_mappping(app_state: &Arc) -> Result<(), TuliproxError> { match config_reader::read_mappings(app_state.config.t_mapping_file_path.as_str(), true) { Ok(Some(mappings_cfg)) => { app_state.config.set_mappings(&mappings_cfg); @@ -29,13 +29,13 @@ impl ConfigFile { error!("Failed to load mapping file {err}"); return Err(err); } - }; + } Ok(()) } - async fn load_api_proxy(app_state: &Arc) -> Result<(), TuliproxError> { - match config_reader::read_api_proxy_config(&app_state.config).await { + fn load_api_proxy(app_state: &Arc) -> Result<(), TuliproxError> { + match config_reader::read_api_proxy_config(&app_state.config) { Ok(()) => { info!("Api Proxy File: {:?}", &app_state.config.t_api_proxy_file_path); } @@ -46,13 +46,12 @@ impl ConfigFile { } Ok(()) } - pub(crate) async fn reload(&self, file_path: &PathBuf, app_state: &Arc) -> Result<(), TuliproxError> { + pub(crate) fn reload(&self, file_path: &PathBuf, app_state: &Arc) -> Result<(), TuliproxError> { debug!("File changed {file_path:?}"); match self { - ConfigFile::Config => { Ok(()) } - ConfigFile::ApiProxy => ConfigFile::load_api_proxy(app_state).await, - ConfigFile::Mapping => ConfigFile::load_mappping(app_state).await, - ConfigFile::Sources => { Ok(()) } + ConfigFile::ApiProxy => ConfigFile::load_api_proxy(app_state), + ConfigFile::Mapping => ConfigFile::load_mappping(app_state), + ConfigFile::Config | ConfigFile::Sources => { Ok(()) } } } } @@ -87,7 +86,7 @@ pub async fn exec_config_watch(app_state: &Arc) -> Result<(), Tuliprox if let EventKind::Access(AccessKind::Close(AccessMode::Write)) = event.kind { for path in event.paths { if let Some(config_file) = files.get(&path) { - if let Err(err) = config_file.reload(&path, &watcher_app_state).await { + if let Err(err) = config_file.reload(&path, &watcher_app_state) { error!("Failed to reload config file {path:?}: {err}"); } } diff --git a/src/api/endpoints/hdhomerun_api.rs b/src/api/endpoints/hdhomerun_api.rs index 96a4b1d10..89f1e2069 100644 --- a/src/api/endpoints/hdhomerun_api.rs +++ b/src/api/endpoints/hdhomerun_api.rs @@ -158,9 +158,9 @@ where } } -async fn create_device(app_state: &Arc) -> Option { +fn create_device(app_state: &Arc) -> Option { if let Some(credentials) = app_state.app_state.config.get_user_credentials(&app_state.device.t_username) { - let server_info = app_state.app_state.config.get_user_server_info(&credentials).await; + let server_info = app_state.app_state.config.get_user_server_info(&credentials); let device = &app_state.device; let device_url = format!("{}://{}:{}", server_info.protocol, server_info.host, device.port); Some(Device { @@ -185,7 +185,7 @@ async fn create_device(app_state: &Arc) -> Option { } async fn device_xml(axum::extract::State(app_state): axum::extract::State>) -> impl IntoResponse { - if let Some(device) = create_device(&app_state).await { + if let Some(device) = create_device(&app_state) { axum::response::Response::builder() .status(axum::http::StatusCode::OK) .header(axum::http::header::CONTENT_TYPE, "application/xml") @@ -198,7 +198,7 @@ async fn device_xml(axum::extract::State(app_state): axum::extract::State>) -> impl IntoResponse { - if let Some(device) = create_device(&app_state).await { + if let Some(device) = create_device(&app_state) { axum::Json(device).into_response() } else { axum::http::StatusCode::INTERNAL_SERVER_ERROR.into_response() @@ -206,7 +206,7 @@ async fn device_json(axum::extract::State(app_state): axum::extract::State>) -> impl IntoResponse { - if let Some(device) = create_device(&app_state).await { + if let Some(device) = create_device(&app_state) { axum::Json(device).into_response() } else { axum::http::StatusCode::INTERNAL_SERVER_ERROR.into_response() @@ -239,7 +239,7 @@ async fn lineup(app_state: &Arc, cfg: &Arc, credentia .body(axum::body::Body::from_stream(body_stream)) .unwrap().into_response(); } else if (use_all || use_xtream) && target.has_output(&TargetType::Xtream) { - let server_info = app_state.app_state.config.get_user_server_info(credentials).await; + let server_info = app_state.app_state.config.get_user_server_info(credentials); let base_url = server_info.get_base_url(); let base_url_live = if credentials.proxy.is_redirect(PlaylistItemType::Live) || target.is_force_redirect(PlaylistItemType::Live) { None } else { Some(base_url.clone()) }; @@ -276,7 +276,7 @@ async fn lineup(app_state: &Arc, cfg: &Arc, credentia async fn auth_lineup_json(AuthBasic((username, password)): AuthBasic, axum::extract::State(app_state): axum::extract::State>) -> impl IntoResponse { let cfg = Arc::clone(&app_state.app_state.config); - if let Some((credentials, target)) = cfg.get_target_for_username(&app_state.device.t_username).await { + if let Some((credentials, target)) = cfg.get_target_for_username(&app_state.device.t_username) { if !username.eq(&credentials.username) || !password.eq(&credentials.password) { return axum::http::StatusCode::UNAUTHORIZED.into_response(); } @@ -288,7 +288,7 @@ async fn auth_lineup_json(AuthBasic((username, password)): AuthBasic, axum::extr async fn lineup_json(axum::extract::State(app_state): axum::extract::State>) -> impl IntoResponse { let cfg = Arc::clone(&app_state.app_state.config); - if let Some((credentials, target)) = cfg.get_target_for_username(&app_state.device.t_username).await { + if let Some((credentials, target)) = cfg.get_target_for_username(&app_state.device.t_username) { let user_credentials = Arc::new(credentials); return lineup(&app_state, &cfg, &user_credentials, target).await.into_response(); } diff --git a/src/api/endpoints/hls_api.rs b/src/api/endpoints/hls_api.rs index cc29e5af2..d69d3123c 100644 --- a/src/api/endpoints/hls_api.rs +++ b/src/api/endpoints/hls_api.rs @@ -44,7 +44,7 @@ pub(in crate::api) async fn handle_hls_stream_request(app_state: &Arc, input: &ConfigInput, connection_permission: UserConnectionPermission) -> impl IntoResponse + Send { let url = replace_url_extension(hls_url, HLS_EXT); - let server_info = app_state.config.get_user_server_info(user).await; + let server_info = app_state.config.get_user_server_info(user); let (request_url, session_token) = match user_session { Some(session) => { @@ -95,7 +95,7 @@ async fn hls_api_stream( axum::extract::State(app_state): axum::extract::State>, ) -> impl axum::response::IntoResponse + Send { let (user, target) = try_option_bad_request!( - app_state.config.get_target_for_user(¶ms.username, ¶ms.password).await, false, + app_state.config.get_target_for_user(¶ms.username, ¶ms.password), false, format!("Could not find any user {}", params.username)); if user.permission_denied(&app_state) { return axum::http::StatusCode::FORBIDDEN.into_response(); diff --git a/src/api/endpoints/m3u_api.rs b/src/api/endpoints/m3u_api.rs index d1497d1b3..2a39fcc61 100644 --- a/src/api/endpoints/m3u_api.rs +++ b/src/api/endpoints/m3u_api.rs @@ -22,7 +22,7 @@ async fn m3u_api( api_req: &UserApiRequest, app_state: &AppState, ) -> impl axum::response::IntoResponse + Send { - match get_user_target(api_req, app_state).await { + match get_user_target(api_req, app_state) { Some((user, target)) => { match m3u_load_rewrite_playlist(&app_state.config, target, &user).await { Ok(m3u_iter) => { @@ -67,7 +67,7 @@ async fn m3u_api_stream( axum::extract::Path((username, password, stream_id)): axum::extract::Path<(String, String, String)>, axum::extract::State(app_state): axum::extract::State>, ) -> impl axum::response::IntoResponse + Send { - let (user, target) = try_option_bad_request!(get_user_target_by_credentials(&username, &password, &api_req, &app_state).await, false, format!("Could not find any user {username}")); + let (user, target) = try_option_bad_request!(get_user_target_by_credentials(&username, &password, &api_req, &app_state), false, format!("Could not find any user {username}")); if user.permission_denied(&app_state) { return StatusCode::FORBIDDEN.into_response(); } @@ -147,7 +147,7 @@ async fn m3u_api_resource( axum::extract::State(app_state): axum::extract::State>, ) -> impl axum::response::IntoResponse + Send { let Ok(m3u_stream_id) = stream_id.parse::() else { return axum::http::StatusCode::BAD_REQUEST.into_response() }; - let Some((user, target)) = get_user_target_by_credentials(&username, &password, &api_req, &app_state).await + let Some((user, target)) = get_user_target_by_credentials(&username, &password, &api_req, &app_state) else { return StatusCode::BAD_REQUEST.into_response() }; if user.permission_denied(&app_state) { return StatusCode::FORBIDDEN.into_response(); diff --git a/src/api/endpoints/user_api.rs b/src/api/endpoints/user_api.rs index 646165e5f..600bc416a 100644 --- a/src/api/endpoints/user_api.rs +++ b/src/api/endpoints/user_api.rs @@ -46,7 +46,7 @@ async fn playlist_categories( axum::extract::State(app_state): axum::extract::State>, ) -> impl axum::response::IntoResponse + Send { if let Some(username) = get_username_from_auth_header(&token, &app_state) { - if let Some((user, target)) = get_user_target_by_username(username.as_str(), &app_state).await { + if let Some((user, target)) = get_user_target_by_username(username.as_str(), &app_state) { if user.permission_denied(&app_state) { return axum::http::StatusCode::FORBIDDEN.into_response(); } @@ -104,7 +104,7 @@ async fn save_playlist_bouquet( axum::extract::Json(bouquet): axum::extract::Json, ) -> impl axum::response::IntoResponse + Send { if let Some(username) = get_username_from_auth_header(&token, &app_state) { - if let Some((user, target)) = get_user_target_by_username(username.as_str(), &app_state).await { + if let Some((user, target)) = get_user_target_by_username(username.as_str(), &app_state) { if user.permission_denied(&app_state) { return axum::http::StatusCode::FORBIDDEN.into_response(); } @@ -126,7 +126,7 @@ async fn playlist_bouquet( axum::extract::State(app_state): axum::extract::State>, ) -> impl axum::response::IntoResponse + Send { if let Some(username) = get_username_from_auth_header(&token, &app_state) { - if let Some((user, _target)) = get_user_target_by_username(username.as_str(), &app_state).await { + if let Some((user, _target)) = get_user_target_by_username(username.as_str(), &app_state) { if user.permission_denied(&app_state) { return axum::http::StatusCode::FORBIDDEN.into_response(); } diff --git a/src/api/endpoints/v1_api.rs b/src/api/endpoints/v1_api.rs index 689b5fa1b..a50b5d133 100644 --- a/src/api/endpoints/v1_api.rs +++ b/src/api/endpoints/v1_api.rs @@ -231,7 +231,7 @@ async fn playlist_webplayer( ) -> impl axum::response::IntoResponse + Send { let access_token = create_access_token(&app_state.config.t_access_token_secret, 5); let server_name = app_state.config.web_ui.as_ref().and_then(|web_ui| web_ui.player_server.as_ref()).map_or("default", |server_name| server_name.as_str()); - let server_info = app_state.config.get_server_info(server_name).await; + let server_info = app_state.config.get_server_info(server_name); let base_url = server_info.get_base_url(); format!("{base_url}/token/{access_token}/{target_id}/{}/{}", playlist_item.xtream_cluster.as_stream_type(), playlist_item.virtual_id).into_response() } @@ -285,7 +285,7 @@ async fn config( sources: config.sources.iter().map(map_source).collect(), proxy: config.proxy.clone(), ipcheck: config.ipcheck.clone(), - api_proxy: config_reader::read_api_proxy(app_state.config.t_api_proxy_file_path.as_str(), false), + api_proxy: config_reader::read_api_proxy(&app_state.config, false), }; let mut result = match config_reader::read_config(app_state.config.t_config_path.as_str(), diff --git a/src/api/endpoints/xmltv_api.rs b/src/api/endpoints/xmltv_api.rs index 726aa9540..44ae29249 100644 --- a/src/api/endpoints/xmltv_api.rs +++ b/src/api/endpoints/xmltv_api.rs @@ -185,7 +185,7 @@ async fn xmltv_api( axum::extract::Query(api_req): axum::extract::Query, axum::extract::State(app_state): axum::extract::State>, ) -> impl IntoResponse + Send { - let Some((user, target)) = get_user_target(&api_req, &app_state).await else { + let Some((user, target)) = get_user_target(&api_req, &app_state) else { return axum::http::StatusCode::FORBIDDEN.into_response(); }; diff --git a/src/api/endpoints/xtream_api.rs b/src/api/endpoints/xtream_api.rs index e7aea206d..a9ba60cd5 100644 --- a/src/api/endpoints/xtream_api.rs +++ b/src/api/endpoints/xtream_api.rs @@ -168,7 +168,7 @@ pub(in crate::api) fn get_xtream_player_api_stream_url( } async fn get_user_info(user: &ProxyUserCredentials, app_state: &AppState) -> XtreamAuthorizationResponse { - let server_info = app_state.config.get_user_server_info(user).await; + let server_info = app_state.config.get_user_server_info(user); let active_connections = app_state.get_active_connections_for_user(&user.username).await; XtreamAuthorizationResponse::new(&server_info, user, active_connections, app_state.config.user_access_control) } @@ -179,7 +179,7 @@ async fn xtream_player_api_stream( app_state: &Arc, stream_req: XtreamApiStreamRequest<'_>, ) -> impl IntoResponse + Send { - let (user, target) = try_option_bad_request!(get_user_target_by_credentials(stream_req.username, stream_req.password, api_req, app_state).await, false, format!("Could not find any user {}", stream_req.username)); + let (user, target) = try_option_bad_request!(get_user_target_by_credentials(stream_req.username, stream_req.password, api_req, app_state), false, format!("Could not find any user {}", stream_req.username)); if user.permission_denied(app_state) { return StatusCode::FORBIDDEN.into_response(); } @@ -456,7 +456,7 @@ async fn xtream_player_api_resource( app_state: &Arc, resource_req: XtreamApiStreamRequest<'_>, ) -> impl IntoResponse { - let (user, target) = try_option_bad_request!(get_user_target_by_credentials(resource_req.username, resource_req.password, api_req, app_state).await, false, format!("Could not find any user {}", resource_req.username)); + let (user, target) = try_option_bad_request!(get_user_target_by_credentials(resource_req.username, resource_req.password, api_req, app_state), false, format!("Could not find any user {}", resource_req.username)); if user.permission_denied(app_state) { return axum::http::StatusCode::FORBIDDEN.into_response(); } @@ -737,7 +737,7 @@ async fn xtream_player_api( api_req: UserApiRequest, app_state: &Arc, ) -> impl IntoResponse + Send { - let user_target = get_user_target(&api_req, app_state).await; + let user_target = get_user_target(&api_req, app_state); if let Some((user, target)) = user_target { if !target.has_output(&TargetType::Xtream) { return axum::response::Json(get_user_info(&user, app_state).await).into_response(); diff --git a/src/api/main_api.rs b/src/api/main_api.rs index d02c34228..9d3db8d32 100644 --- a/src/api/main_api.rs +++ b/src/api/main_api.rs @@ -171,7 +171,8 @@ fn create_compression_layer() -> tower_http::compression::CompressionLayer { fn start_hdhomerun(cfg: &Arc, app_state: &Arc, infos: &mut Vec) { let host = cfg.api.host.to_string(); - if let Some(hdhomerun) = &cfg.hdhomerun { + let guard = cfg.t_hdhomerun.load(); + if let Some(hdhomerun) = &*guard { if hdhomerun.enabled { for device in &hdhomerun.devices { if device.t_enabled { @@ -232,8 +233,10 @@ pub async fn start_server(cfg: Arc, targets: Arc) -> fut exec_scheduler(&Arc::clone(&shared_data.http_client), &cfg, &targets); exec_update_on_boot(Arc::clone(&shared_data.http_client), &cfg, &targets); - if let Err(err) = exec_config_watch(&app_state).await { - error!("Failed to start config watch: {err}"); + if cfg.config_hot_reload { + if let Err(err) = exec_config_watch(&app_state).await { + error!("Failed to start config watch: {err}"); + } } let web_auth_enabled = is_web_auth_enabled(&cfg, web_ui_enabled); diff --git a/src/main.rs b/src/main.rs index e480cff08..a7620a607 100644 --- a/src/main.rs +++ b/src/main.rs @@ -10,7 +10,7 @@ mod modules; include_modules!(); use crate::auth::password::generate_password; -use crate::model::{validate_targets, Config, HealthcheckConfig, Healthcheck, ProcessTargets, LogLevelConfig}; +use crate::model::{validate_targets, Config, HealthcheckConfig, Healthcheck, ProcessTargets}; use crate::processing::processor::playlist; use crate::utils::config_reader::config_file_reader; use utils::config_reader; @@ -18,18 +18,11 @@ use crate::utils::file_utils; use crate::utils::request::{create_client, set_sanitize_sensitive_info}; use chrono::{DateTime, Utc}; use clap::Parser; -use env_logger::Builder; -use log::{error, info, LevelFilter}; +use log::{error, info}; use std::fs::File; use std::path::{Path, PathBuf}; use std::sync::Arc; - -const LOG_ERROR_LEVEL_MOD: &[&str] = &[ - "reqwest::async_impl::client", - "reqwest::connect", - "hyper_util::client", -]; - +use crate::utils::logging::init_logger; #[derive(Parser)] #[command(name = "tuliprox")] @@ -105,8 +98,7 @@ fn main() { let api_proxy_file = args.api_proxy.unwrap_or_else(|| file_utils::get_default_api_proxy_config_path(config_path.as_str())); let mappings_file = args.mapping_file.unwrap_or_else(|| file_utils::get_default_mappings_path(config_path.as_str())); - let env_log_level = std::env::var("TULIPROX_LOG"); - init_logger(args.log_level.as_ref(), env_log_level.ok(), config_file.as_str()); + init_logger(args.log_level.as_ref(), config_file.as_str()); if args.healthcheck { healthcheck(config_file.as_str()); @@ -150,14 +142,10 @@ fn main() { } } - // if cfg.t_channel_unavailable_video.is_some() { - // info!("Channel unavailable video loaded from {:?}", cfg.channel_unavailable_file.as_ref().map_or("?", |v| v.as_str())); - // } - let rt = tokio::runtime::Runtime::new().unwrap(); let () = rt.block_on(async { if args.server { - match config_reader::read_api_proxy_config(&cfg).await { + match config_reader::read_api_proxy_config(&cfg) { Ok(()) => {} Err(err) => exit!("{err}"), } @@ -211,51 +199,6 @@ async fn start_in_server_mode(cfg: Arc, targets: Arc) { } } -fn get_log_level(log_level: &str) -> LevelFilter { - match log_level.to_lowercase().as_str() { - "trace" => LevelFilter::Trace, - "debug" => LevelFilter::Debug, - "warn" => LevelFilter::Warn, - "error" => LevelFilter::Error, - // "info" => LevelFilter::Info, - _ => LevelFilter::Info, - } -} - -fn init_logger(user_log_level: Option<&String>, env_log_level: Option, config_file: &str) { - let mut log_builder = Builder::from_default_env(); - - // priority CLI-Argument, Env-Var, Config, Default - let log_level = user_log_level - .map(std::string::ToString::to_string) // cli-argument - .or(env_log_level) // env - .or_else(|| { // config - File::open(config_file).ok() - .and_then(|file| serde_yaml::from_reader::<_, LogLevelConfig>(config_file_reader(file, true)) - .map_err(|e| error!("Failed to parse log config file: {e}")) - .ok()) - .and_then(|cfg| cfg.log.and_then(|l| l.log_level)) - }) - .unwrap_or_else(|| "info".to_string()); // Default - - if log_level.contains('=') { - for pair in log_level.split(',').filter(|s| s.contains('=')) { - let mut kv_iter = pair.split('=').map(str::trim); - if let (Some(module), Some(level)) = (kv_iter.next(), kv_iter.next()) { - log_builder.filter_module(module, get_log_level(level)); - } - } - } else { - // Set the log level based on the parsed value - log_builder.filter_level(get_log_level(&log_level)); - } - for module in LOG_ERROR_LEVEL_MOD { - log_builder.filter_module(module, LevelFilter::Error); - } - log_builder.init(); - info!("Log Level {}", get_log_level(&log_level)); -} - fn healthcheck(config_file: &str) { let path = std::path::PathBuf::from(config_file); let file = File::open(path).expect("Failed to open config file"); diff --git a/src/model/api_proxy.rs b/src/model/api_proxy.rs index 2b77e5f80..759a375f8 100644 --- a/src/model/api_proxy.rs +++ b/src/model/api_proxy.rs @@ -401,7 +401,8 @@ pub struct ApiProxyConfig { impl ApiProxyConfig { // we have the option to store user in the config file or in the user_db // When we switch from one to other we need to migrate the existing data. - fn migrate_api_user(&mut self, cfg: &Config, errors: &mut Vec) { + /// # Panics + pub fn migrate_api_user(&mut self, cfg: &Config, errors: &mut Vec) { if self.use_user_db { // we have user defined in config file. // we migrate them to the db and delete them from the config file diff --git a/src/model/config.rs b/src/model/config.rs index 49f47f506..8955166de 100644 --- a/src/model/config.rs +++ b/src/model/config.rs @@ -362,18 +362,22 @@ pub struct Config { #[serde(default)] pub update_on_boot: bool, #[serde(default)] + pub config_hot_reload: bool, + #[serde(default)] pub web_ui: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub messaging: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub reverse_proxy: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub hdhomerun: Arc>, + pub hdhomerun: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub proxy: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub ipcheck: Option, #[serde(skip)] + pub t_hdhomerun: Arc>, + #[serde(skip)] pub t_api_proxy: Arc>, #[serde(skip)] pub t_config_path: String, @@ -400,14 +404,14 @@ pub struct Config { } impl Config { - pub async fn set_api_proxy(&self, api_proxy: Option>) -> Result<(), TuliproxError> { + pub fn set_api_proxy(&self, api_proxy: Option>) -> Result<(), TuliproxError> { self.t_api_proxy.store(api_proxy); - self.check_target_user().await + self.check_target_user() } - async fn check_username(&self, output_username: Option<&str>, target_name: &str) -> Result<(), TuliproxError> { + fn check_username(&self, output_username: Option<&str>, target_name: &str) -> Result<(), TuliproxError> { if let Some(username) = output_username { - if let Some((_, config_target)) = self.get_target_for_username(username).await { + if let Some((_, config_target)) = self.get_target_for_username(username) { if config_target.name != target_name { return create_tuliprox_error_result!(TuliproxErrorKind::Info, "User:{username} does not belong to target: {}", target_name); } @@ -419,27 +423,29 @@ impl Config { Ok(()) } } - async fn check_target_user(&self) -> Result<(), TuliproxError> { - let check_homerun = self.hdhomerun.as_ref().is_some_and(|h| h.enabled); + fn check_target_user(&self) -> Result<(), TuliproxError> { + let check_homerun = self.t_hdhomerun.load().as_ref().is_some_and(|h| h.enabled); for source in &self.sources { for target in &source.targets { for output in &target.output { match output { TargetOutput::Xtream(_) | TargetOutput::M3u(_) => {} TargetOutput::Strm(strm_output) => { - self.check_username(strm_output.username.as_deref(), &target.name).await?; + self.check_username(strm_output.username.as_deref(), &target.name)?; } TargetOutput::HdHomeRun(hdhomerun_output) => { if check_homerun { let hdhr_name = &hdhomerun_output.device; - self.check_username(Some(&hdhomerun_output.username), &target.name).await?; - if let Some(homerun) = &mut self.hdhomerun { - for device in &mut homerun.devices { + self.check_username(Some(&hdhomerun_output.username), &target.name)?; + if let Some(old_hdhomerun) = self.t_hdhomerun.load().clone() { + let mut hdhomerun = (*old_hdhomerun).clone(); + for device in &mut hdhomerun.devices { if &device.name == hdhr_name { device.t_username.clone_from(&hdhomerun_output.username); device.t_enabled = true; } } + self.t_hdhomerun.store(Some(Arc::new(hdhomerun))); } } } @@ -448,7 +454,8 @@ impl Config { } } - if let Some(hdhomerun) = &self.hdhomerun { + let guard = self.t_hdhomerun.load(); + if let Some(hdhomerun) = &*guard { for device in &hdhomerun.devices { if !device.t_enabled { debug!("HdHomeRun device '{}' has no username and will be disabled", device.name); @@ -487,7 +494,7 @@ impl Config { None } - pub async fn get_target_for_username(&self, username: &str) -> Option<(ProxyUserCredentials, &ConfigTarget)> { + pub fn get_target_for_username(&self, username: &str) -> Option<(ProxyUserCredentials, &ConfigTarget)> { if let Some(credentials) = self.get_user_credentials(username) { return self.t_api_proxy.load().as_ref() .and_then(|api_proxy| self.intern_get_target_for_user(api_proxy.get_target_name(&credentials.username, &credentials.password))); @@ -495,11 +502,11 @@ impl Config { None } - pub async fn get_target_for_user(&self, username: &str, password: &str) -> Option<(ProxyUserCredentials, &ConfigTarget)> { + pub fn get_target_for_user(&self, username: &str, password: &str) -> Option<(ProxyUserCredentials, &ConfigTarget)> { self.t_api_proxy.load().as_ref().and_then(|api_proxy| self.intern_get_target_for_user(api_proxy.get_target_name(username, password))) } - pub async fn get_target_for_user_by_token(&self, token: &str) -> Option<(ProxyUserCredentials, &ConfigTarget)> { + pub fn get_target_for_user_by_token(&self, token: &str) -> Option<(ProxyUserCredentials, &ConfigTarget)> { self.t_api_proxy.load().as_ref().as_ref().and_then(|api_proxy| self.intern_get_target_for_user(api_proxy.get_target_name_by_token(token))) } @@ -681,10 +688,12 @@ impl Config { } fn prepare_hdhomerun(&mut self) -> Result<(), TuliproxError> { - if let Some(hdhomerun) = self.hdhomerun.as_mut() { + if let Some(old_hdhomerun) = &self.hdhomerun { + let mut hdhomerun = (*old_hdhomerun).clone(); if hdhomerun.enabled { hdhomerun.prepare(self.api.port)?; } + self.t_hdhomerun.store(Some(Arc::new(hdhomerun))); } Ok(()) } @@ -777,15 +786,15 @@ impl Config { /// # Panics /// /// Will panic if default server invalid - pub async fn get_server_info(&self, server_info_name: &str) -> ApiProxyServerInfo { + pub fn get_server_info(&self, server_info_name: &str) -> ApiProxyServerInfo { let guard = self.t_api_proxy.load(); let server_info_list = guard.as_ref().unwrap().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) } - pub async fn get_user_server_info(&self, user: &ProxyUserCredentials) -> ApiProxyServerInfo { + pub fn get_user_server_info(&self, user: &ProxyUserCredentials) -> ApiProxyServerInfo { let server_info_name = user.server.as_ref().map_or("default", |server_name| server_name.as_str()); - self.get_server_info(server_info_name).await + self.get_server_info(server_info_name) } } diff --git a/src/repository/kodi_repository.rs b/src/repository/kodi_repository.rs index 549b509ca..0f4faf36e 100644 --- a/src/repository/kodi_repository.rs +++ b/src/repository/kodi_repository.rs @@ -590,7 +590,7 @@ pub async fn kodi_write_strm_playlist( ))); }; - let user_and_server_info = get_credentials_and_server_info(cfg, target_output.username.as_ref()).await; + let user_and_server_info = get_credentials_and_server_info(cfg, target_output.username.as_ref()); let strm_index_path = strm_get_file_paths(&ensure_target_storage_path(cfg, target.name.as_str())?); let existing_strm = { @@ -764,13 +764,13 @@ async fn has_strm_file_same_hash(file_path: &PathBuf, content_hash: UUIDType) -> false } -async fn get_credentials_and_server_info( +fn get_credentials_and_server_info( cfg: &Config, username: Option<&String>, ) -> Option<(ProxyUserCredentials, ApiProxyServerInfo)> { let username = username?; let credentials = cfg.get_user_credentials(username)?; - let server_info = cfg.get_user_server_info(&credentials).await; + let server_info = cfg.get_user_server_info(&credentials); Some((credentials, server_info)) } diff --git a/src/repository/m3u_playlist_iterator.rs b/src/repository/m3u_playlist_iterator.rs index 6e11e8861..577ea5de4 100644 --- a/src/repository/m3u_playlist_iterator.rs +++ b/src/repository/m3u_playlist_iterator.rs @@ -44,7 +44,7 @@ impl M3uPlaylistIterator { let filter = user_get_bouquet_filter(cfg, &user.username, None, TargetType::M3u, XtreamCluster::Live).await; - let server_info = cfg.get_user_server_info(user).await; + let server_info = cfg.get_user_server_info(user); Ok(Self { reader, base_url: server_info.get_base_url(), diff --git a/src/repository/xtream_playlist_iterator.rs b/src/repository/xtream_playlist_iterator.rs index 795e565c5..4c1dbe2a3 100644 --- a/src/repository/xtream_playlist_iterator.rs +++ b/src/repository/xtream_playlist_iterator.rs @@ -40,7 +40,7 @@ impl XtreamPlaylistIterator { .map_err(|err| info_err!(format!("Could not deserialize file {xtream_path:?} - {err}")))?; let options = XtreamMappingOptions::from_target_options(target, xtream_output, config); - let server_info = config.get_user_server_info(user).await; + let server_info = config.get_user_server_info(user); let filter = user_get_bouquet_filter(config, &user.username, category_id, TargetType::Xtream, cluster).await; diff --git a/src/repository/xtream_repository.rs b/src/repository/xtream_repository.rs index 38abc52af..410fa8b78 100644 --- a/src/repository/xtream_repository.rs +++ b/src/repository/xtream_repository.rs @@ -516,7 +516,7 @@ pub fn xtream_load_vod_info( None } -async fn rewrite_xtream_vod_info

( +fn rewrite_xtream_vod_info

( config: &Config, target: &ConfigTarget, xtream_output: &XtreamTargetOutput, @@ -531,7 +531,7 @@ async fn rewrite_xtream_vod_info

( if let Some(Value::Object(info_data)) = doc.get_mut(crate::model::XC_TAG_INFO_DATA) { let item_type = pli.get_item_type(); if user.proxy.is_reverse(item_type) && !target.is_force_redirect(item_type) { - let server_info = config.get_user_server_info(user).await; + let server_info = config.get_user_server_info(user); let url = server_info.get_base_url(); let resource_url = Some(format!("{url}/resource/movie/{}/{}/{}", user.username, user.password, pli.get_virtual_id())); rewrite_doc_urls(resource_url.as_ref(), info_data, storage_const::INFO_REWRITE_FIELDS, crate::model::XC_INFO_RESOURCE_PREFIX); @@ -562,7 +562,7 @@ async fn rewrite_xtream_vod_info

( Ok(result) } -pub async fn rewrite_xtream_vod_info_content

( +pub fn rewrite_xtream_vod_info_content

( config: &Config, target: &ConfigTarget, xtream_output: &XtreamTargetOutput, @@ -573,7 +573,7 @@ pub async fn rewrite_xtream_vod_info_content

( P: PlaylistEntry, { let mut doc = serde_json::from_str::>(content).map_err(|_| str_to_io_error("Failed to parse JSON content"))?; - rewrite_xtream_vod_info(config, target, xtream_output, pli, user, &mut doc).await + rewrite_xtream_vod_info(config, target, xtream_output, pli, user, &mut doc) } pub async fn write_and_get_xtream_vod_info

( @@ -588,7 +588,7 @@ pub async fn write_and_get_xtream_vod_info

( { let mut doc = serde_json::from_str::>(content).map_err(|_| str_to_io_error("Failed to parse JSON content"))?; xtream_write_vod_info(config, target.name.as_str(), pli.get_virtual_id(), content).await.ok(); - rewrite_xtream_vod_info(config, target, xtream_output, pli, user, &mut doc).await + rewrite_xtream_vod_info(config, target, xtream_output, pli, user, &mut doc) } async fn rewrite_xtream_series_info

( @@ -606,7 +606,7 @@ async fn rewrite_xtream_series_info

( let resource_url = if config.is_reverse_proxy_resource_rewrite_enabled() { let item_type = pli.get_item_type(); if user.proxy.is_reverse(item_type) && !target.is_force_redirect(item_type) { - let server_info = config.get_user_server_info(user).await; + let server_info = config.get_user_server_info(user); let url = server_info.get_base_url(); Some(format!("{url}/resource/series/{}/{}/{}", user.username, user.password, pli.get_virtual_id())) } else { diff --git a/src/utils/file/config_reader.rs b/src/utils/file/config_reader.rs index bb7caeae4..cb8836f27 100644 --- a/src/utils/file/config_reader.rs +++ b/src/utils/file/config_reader.rs @@ -1,13 +1,13 @@ -use crate::tuliprox_error::{create_tuliprox_error, create_tuliprox_error_result, handle_tuliprox_error_result, info_err, str_to_io_error, to_io_error, TuliproxError, TuliproxErrorKind}; use crate::model::ApiProxyConfig; -use crate::model::{Config, ConfigDto, ConfigInput, ConfigInputAlias, InputType}; use crate::model::Mappings; -use crate::utils::CONSTANTS; +use crate::model::{Config, ConfigDto, ConfigInput, ConfigInputAlias, InputType}; +use crate::tuliprox_error::{create_tuliprox_error, create_tuliprox_error_result, handle_tuliprox_error_result, info_err, str_to_io_error, to_io_error, TuliproxError, TuliproxErrorKind}; use crate::utils::env_resolving_reader::EnvResolvingReader; use crate::utils::file_utils::{file_reader, resolve_relative_path}; -use crate::utils::{file_utils, multi_file_reader}; use crate::utils::request::{get_credentials_from_url, get_local_file_content}; use crate::utils::sys_utils::exit; +use crate::utils::CONSTANTS; +use crate::utils::{file_utils, multi_file_reader}; use chrono::Local; use log::{error, info, warn}; use serde::Serialize; @@ -54,15 +54,15 @@ pub fn read_mappings(mappings_file: &str, resolve_env: bool) -> Result Result<(), TuliproxError> { - let api_proxy_config = read_api_proxy(cfg.t_api_proxy_file_path.as_str(), true); +pub fn read_api_proxy_config(cfg: &Config) -> Result<(), TuliproxError> { + let api_proxy_config = read_api_proxy(cfg, true); match api_proxy_config { None => { warn!("cant read api_proxy_config file: {}", cfg.t_api_proxy_file_path.as_str()); Ok(()) } Some(config) => { - cfg.set_api_proxy(Some(Arc::new(config))).await?; + cfg.set_api_proxy(Some(Arc::new(config)))?; Ok(()) } } @@ -96,11 +96,11 @@ pub fn read_config(config_path: &str, config_file: &str, sources_file: &str, api pub fn read_mapping(mapping_file: &str, resolve_var: bool) -> Result, TuliproxError> { let mapping_file = std::path::PathBuf::from(mapping_file); if let Ok(file) = file_utils::open_file(&mapping_file) { - let mapping: Result = serde_yaml::from_reader(config_file_reader(file, resolve_var)); - return match mapping { - Ok(mut result) => { - handle_tuliprox_error_result!(TuliproxErrorKind::Info, result.prepare()); - Ok(Some(result)) + let maybe_mapping: Result = serde_yaml::from_reader(config_file_reader(file, resolve_var)); + return match maybe_mapping { + Ok(mut mapping) => { + handle_tuliprox_error_result!(TuliproxErrorKind::Info, mapping.prepare()); + Ok(Some(mapping)) } Err(err) => { Err(info_err!(err.to_string())) @@ -111,18 +111,23 @@ pub fn read_mapping(mapping_file: &str, resolve_var: bool) -> Result Option { +pub fn read_api_proxy(config: &Config, resolve_env: bool) -> Option { + let api_proxy_file = config.t_api_proxy_file_path.as_str(); file_utils::open_file(&std::path::PathBuf::from(api_proxy_file)).map_or(None, |file| { - let mapping: Result = serde_yaml::from_reader(config_file_reader(file, resolve_env)); - match mapping { - Ok(mut result) => { - match result.prepare() { - Err(err) => { - exit!("cant read api-proxy-config file: {err}"); - } - _ => { - Some(result) + let maybe_api_proxy: Result = serde_yaml::from_reader(config_file_reader(file, resolve_env)); + match maybe_api_proxy { + Ok(mut api_proxy) => { + if let Err(err) = api_proxy.prepare() { + exit!("cant read api-proxy-config file: {err}"); + } else { + let mut errors = vec![]; + api_proxy.migrate_api_user(config, &mut errors); + if !errors.is_empty() { + for error in errors { + error!("{error}"); + } } + Some(api_proxy) } } Err(err) => { @@ -131,10 +136,6 @@ pub fn read_api_proxy(api_proxy_file: &str, resolve_env: bool) -> Option(file_path: &str, backup_dir: &str, config: &T, default_name: &str) -> Result<(), TuliproxError> diff --git a/src/utils/logging.rs b/src/utils/logging.rs new file mode 100644 index 000000000..d9c9fb837 --- /dev/null +++ b/src/utils/logging.rs @@ -0,0 +1,59 @@ +use std::fs::File; +use env_logger::Builder; +use log::{error, info, LevelFilter}; +use crate::model::LogLevelConfig; +use crate::utils::config_reader::config_file_reader; + +const LOG_ERROR_LEVEL_MOD: &[&str] = &[ + "reqwest::async_impl::client", + "reqwest::connect", + "hyper_util::client", +]; + + +fn get_log_level(log_level: &str) -> LevelFilter { + match log_level.to_lowercase().as_str() { + "trace" => LevelFilter::Trace, + "debug" => LevelFilter::Debug, + "warn" => LevelFilter::Warn, + "error" => LevelFilter::Error, + // "info" => LevelFilter::Info, + _ => LevelFilter::Info, + } +} + +pub fn init_logger(user_log_level: Option<&String>, config_file: &str) { + let env_log_level = std::env::var("TULIPROX_LOG").ok(); + + let mut log_builder = Builder::from_default_env(); + + // priority CLI-Argument, Env-Var, Config, Default + let log_level = user_log_level + .map(std::string::ToString::to_string) // cli-argument + .or(env_log_level) // env + .or_else(|| { // config + File::open(config_file).ok() + .and_then(|file| serde_yaml::from_reader::<_, LogLevelConfig>(config_file_reader(file, true)) + .map_err(|e| error!("Failed to parse log config file: {e}")) + .ok()) + .and_then(|cfg| cfg.log.and_then(|l| l.log_level)) + }) + .unwrap_or_else(|| "info".to_string()); // Default + + if log_level.contains('=') { + for pair in log_level.split(',').filter(|s| s.contains('=')) { + let mut kv_iter = pair.split('=').map(str::trim); + if let (Some(module), Some(level)) = (kv_iter.next(), kv_iter.next()) { + log_builder.filter_module(module, get_log_level(level)); + } + } + } else { + // Set the log level based on the parsed value + log_builder.filter_level(get_log_level(&log_level)); + } + for module in LOG_ERROR_LEVEL_MOD { + log_builder.filter_module(module, LevelFilter::Error); + } + log_builder.init(); + info!("Log Level {}", get_log_level(&log_level)); +} diff --git a/src/utils/mod.rs b/src/utils/mod.rs index 3ca57cb43..978a743b3 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -12,6 +12,7 @@ mod time_utils; mod crypto_utils; mod constants; mod step_measure; +pub mod logging; #[macro_export] macro_rules! debug_if_enabled { diff --git a/src/utils/network/xtream.rs b/src/utils/network/xtream.rs index e9eb925ec..9ae9d89b6 100644 --- a/src/utils/network/xtream.rs +++ b/src/utils/network/xtream.rs @@ -81,7 +81,7 @@ where } else if cluster == XtreamCluster::Video { if let Some(content) = xtream_repository::xtream_load_vod_info(config, target.name.as_str(), pli.get_virtual_id()) { // Deliver existing target content - return rewrite_xtream_vod_info_content(config, target, xtream_output, pli, user, &content).await; + return rewrite_xtream_vod_info_content(config, target, xtream_output, pli, user, &content); } // Check if the content has been resolved if xtream_output.resolve_vod {