diff --git a/src/api/api_utils.rs b/src/api/api_utils.rs index ee9d8abca..bb624a11a 100644 --- a/src/api/api_utils.rs +++ b/src/api/api_utils.rs @@ -13,18 +13,18 @@ use crate::api::model::streams::provider_stream_factory::BufferStreamOptions; use crate::api::model::streams::shared_stream_manager::SharedStreamManager; use crate::api::model::streams::throttled_stream::ThrottledStream; use crate::auth::authenticator::Claims; -use crate::model::api_proxy::{ProxyUserCredentials, UserConnectionPermission}; -use crate::model::config::{ConfigInput, ConfigTarget, InputFetchMethod, TargetType}; -use crate::model::playlist::{PlaylistEntry, PlaylistItemType, XtreamCluster}; +use crate::model::{ProxyUserCredentials, UserConnectionPermission}; +use crate::model::{ConfigInput, ConfigTarget, InputFetchMethod, TargetType}; +use crate::model::{PlaylistEntry, PlaylistItemType, XtreamCluster}; use crate::tools::atomic_once_flag::AtomicOnceFlag; use crate::tools::lru_cache::LRUResourceCache; -use crate::utils::constants::{DASH_EXT, HLS_EXT}; -use crate::utils::default_utils::default_grace_period_millis; -use crate::utils::file::file_utils::create_new_file_for_write; -use crate::utils::network::request; -use crate::utils::network::request::{extract_extension_from_url, replace_url_extension, sanitize_sensitive_info}; -use crate::utils::size_utils::human_readable_byte_size; -use crate::utils::{debug_if_enabled, hash_utils, sys_utils, trace_if_enabled}; +use crate::utils::{DASH_EXT, HLS_EXT}; +use crate::utils::default_grace_period_millis; +use crate::utils::file_utils::create_new_file_for_write; +use crate::utils::request; +use crate::utils::request::{extract_extension_from_url, replace_url_extension, sanitize_sensitive_info}; +use crate::utils::human_readable_byte_size; +use crate::utils::{debug_if_enabled, trace_if_enabled}; use crate::BUILD_TIMESTAMP; use axum::body::Body; use axum::http::HeaderMap; @@ -84,7 +84,7 @@ pub use try_option_bad_request; pub use try_result_bad_request; use crate::api::model::active_user_manager::UserSession; use crate::api::model::provider_config::ProviderConfig; -use crate::utils::hash_utils::base64_to_u32; +use crate::utils::base64_to_u32; pub fn get_server_time() -> String { chrono::offset::Local::now().with_timezone(&chrono::Local).format("%Y-%m-%d %H:%M:%S %Z").to_string() @@ -95,7 +95,7 @@ pub fn get_build_time() -> Option { } pub fn get_memory_usage() -> String { - sys_utils::get_memory_usage().map_or(String::from("?"), human_readable_byte_size) + crate::utils::get_memory_usage().map_or(String::from("?"), human_readable_byte_size) } @@ -479,7 +479,7 @@ const SESSION_COOKIE_NAME: &str = "m3uflt_session="; // } pub fn create_session_cookie(token: u32) -> String { - let cookie = hash_utils::u32_to_base64(token); + let cookie = crate::utils::u32_to_base64(token); format!("{SESSION_COOKIE_NAME}{cookie}; Path=/; HttpOnly; SameSite=Lax") } diff --git a/src/api/endpoints/api_playlist_utils.rs b/src/api/endpoints/api_playlist_utils.rs index 1620e88c1..9f6710733 100644 --- a/src/api/endpoints/api_playlist_utils.rs +++ b/src/api/endpoints/api_playlist_utils.rs @@ -1,8 +1,8 @@ -use crate::model::config::{Config, ConfigInput, ConfigTarget, InputType, TargetType}; -use crate::model::playlist::{M3uPlaylistItem, PlaylistGroup, PlaylistItemType, XtreamCluster}; +use crate::model::{Config, ConfigInput, ConfigTarget, InputType, TargetType}; +use crate::model::{M3uPlaylistItem, PlaylistGroup, PlaylistItemType, XtreamCluster}; use crate::repository::{m3u_repository, xtream_repository}; -use crate::utils::file::file_lock_manager::FileReadGuard; -use crate::utils::network::{m3u, xtream}; +use crate::utils::file_lock_manager::FileReadGuard; +use crate::utils::{m3u, xtream}; use axum::response::IntoResponse; use serde::Serialize; use serde_json::{json, Value}; diff --git a/src/api/endpoints/download_api.rs b/src/api/endpoints/download_api.rs index e724aa576..4feff0ef2 100644 --- a/src/api/endpoints/download_api.rs +++ b/src/api/endpoints/download_api.rs @@ -1,7 +1,7 @@ use crate::api::model::app_state::AppState; use crate::api::model::download::{DownloadQueue, FileDownload, FileDownloadRequest}; -use crate::model::config::{ConfigProxy, VideoDownloadConfig}; -use crate::utils::network::request; +use crate::model::{ConfigProxy, VideoDownloadConfig}; +use crate::utils::request; use tokio::sync::RwLock; use futures::stream::TryStreamExt; use log::info; @@ -13,7 +13,7 @@ use std::sync::Arc; use std::{fs}; use axum::response::IntoResponse; use crate::m3u_filter_error::to_io_error; -use crate::utils::network::request::create_client; +use crate::utils::request::create_client; async fn download_file(active: Arc>>, client: &reqwest::Client) -> Result<(), String> { let file_download = { active.read().await.as_ref().unwrap().clone() }; diff --git a/src/api/endpoints/hdhomerun_api.rs b/src/api/endpoints/hdhomerun_api.rs index 9b3eabd4a..ae415a75c 100644 --- a/src/api/endpoints/hdhomerun_api.rs +++ b/src/api/endpoints/hdhomerun_api.rs @@ -1,12 +1,12 @@ use crate::api::model::app_state::HdHomerunAppState; use crate::auth::auth_basic::AuthBasic; -use crate::model::api_proxy::ProxyUserCredentials; -use crate::model::config::{Config, ConfigTarget, TargetType}; -use crate::model::playlist::{M3uPlaylistItem, PlaylistItemType, XtreamCluster, XtreamPlaylistItem}; +use crate::model::ProxyUserCredentials; +use crate::model::{Config, ConfigTarget, TargetType}; +use crate::model::{M3uPlaylistItem, PlaylistItemType, XtreamCluster, XtreamPlaylistItem}; use crate::processing::parser::xtream::get_xtream_url; use crate::repository::m3u_playlist_iterator::M3uPlaylistIterator; use crate::repository::xtream_playlist_iterator::XtreamPlaylistIterator; -use crate::utils::json_utils::get_string_from_serde_value; +use crate::utils::get_string_from_serde_value; use axum::response::IntoResponse; use bytes::Bytes; use futures::{stream, Stream, StreamExt}; diff --git a/src/api/endpoints/hls_api.rs b/src/api/endpoints/hls_api.rs index 42740a85d..cc29e5af2 100644 --- a/src/api/endpoints/hls_api.rs +++ b/src/api/endpoints/hls_api.rs @@ -2,13 +2,13 @@ use crate::api::api_utils::{create_session_cookie, force_provider_stream_respons use crate::api::api_utils::{try_option_bad_request}; use crate::api::model::app_state::AppState; use crate::api::model::streams::provider_stream::{create_custom_video_stream_response, CustomVideoStreamType}; -use crate::model::api_proxy::{ProxyUserCredentials, UserConnectionPermission}; -use crate::model::config::ConfigInput; -use crate::model::playlist::{PlaylistItemType, XtreamCluster}; +use crate::model::{ProxyUserCredentials, UserConnectionPermission}; +use crate::model::ConfigInput; +use crate::model::{PlaylistItemType, XtreamCluster}; use crate::processing::parser::hls::{get_hls_session_token_and_url_from_token, rewrite_hls, RewriteHlsProps}; -use crate::utils::constants::HLS_EXT; -use crate::utils::network::request; -use crate::utils::network::request::{is_hls_url, replace_url_extension, sanitize_sensitive_info}; +use crate::utils::HLS_EXT; +use crate::utils::request; +use crate::utils::request::{is_hls_url, replace_url_extension, sanitize_sensitive_info}; use axum::response::IntoResponse; use log::{debug, error}; use serde::Deserialize; diff --git a/src/api/endpoints/m3u_api.rs b/src/api/endpoints/m3u_api.rs index 8dcc2131e..c1eade96c 100644 --- a/src/api/endpoints/m3u_api.rs +++ b/src/api/endpoints/m3u_api.rs @@ -2,11 +2,11 @@ use crate::api::api_utils::{get_user_target, get_user_target_by_credentials, is_ use crate::api::endpoints::hls_api::handle_hls_stream_request; use crate::api::model::app_state::AppState; use crate::api::model::request::UserApiRequest; -use crate::model::api_proxy::{UserConnectionPermission}; -use crate::model::config::{TargetType}; -use crate::model::playlist::{FieldGetAccessor, PlaylistEntry, PlaylistItemType, XtreamCluster}; +use crate::model::{UserConnectionPermission}; +use crate::model::{TargetType}; +use crate::model::{FieldGetAccessor, PlaylistEntry, PlaylistItemType, XtreamCluster}; use crate::repository::m3u_repository::{m3u_get_item_for_stream_id, m3u_load_rewrite_playlist}; -use crate::utils::network::request::{extract_extension_from_url, sanitize_sensitive_info}; +use crate::utils::request::{extract_extension_from_url, sanitize_sensitive_info}; use axum::response::IntoResponse; use bytes::Bytes; use futures::stream; @@ -16,7 +16,7 @@ use axum::http::StatusCode; use crate::api::endpoints::xtream_api::XtreamApiStreamContext; use crate::api::model::streams::provider_stream::{create_custom_video_stream_response, CustomVideoStreamType}; use crate::repository::storage_const; -use crate::utils::constants::{HLS_EXT}; +use crate::utils::{HLS_EXT}; async fn m3u_api( api_req: &UserApiRequest, diff --git a/src/api/endpoints/user_api.rs b/src/api/endpoints/user_api.rs index 9f4151ec2..646165e5f 100644 --- a/src/api/endpoints/user_api.rs +++ b/src/api/endpoints/user_api.rs @@ -1,10 +1,10 @@ use crate::api::api_utils::{get_user_target_by_username, get_username_from_auth_header}; use crate::api::model::app_state::AppState; use crate::auth::authenticator::validator_user; -use crate::model::config::{Config, ConfigTarget, TargetType}; -use crate::model::playlist::XtreamCluster; -use crate::model::playlist_categories::PlaylistBouquetDto; -use crate::model::xtream::PlaylistXtreamCategory; +use crate::model::{Config, ConfigTarget, TargetType}; +use crate::model::XtreamCluster; +use crate::model::PlaylistBouquetDto; +use crate::model::PlaylistXtreamCategory; use crate::repository::user_repository::{load_user_bouquet_as_json, save_user_bouquet}; use crate::repository::xtream_repository::xtream_get_playlist_categories; use crate::repository::m3u_repository; diff --git a/src/api/endpoints/v1_api.rs b/src/api/endpoints/v1_api.rs index 960c7af24..50c9dadeb 100644 --- a/src/api/endpoints/v1_api.rs +++ b/src/api/endpoints/v1_api.rs @@ -1,3 +1,4 @@ +use crate::api::endpoints::api_playlist_utils::{get_playlist, get_playlist_for_target}; use crate::api::endpoints::download_api; use crate::api::endpoints::user_api::user_api_register; use crate::api::model::app_state::AppState; @@ -6,21 +7,21 @@ use crate::api::model::request::{PlaylistRequest, PlaylistRequestType}; use crate::auth::access_token::create_access_token; use crate::auth::authenticator::validator_admin; use crate::m3u_filter_error::M3uFilterError; -use crate::model::api_proxy::{ApiProxyConfig, ApiProxyServerInfo, ProxyUserCredentials, TargetUser}; -use crate::model::config::{validate_targets, Config, ConfigDto, ConfigInput, ConfigInputOptions, ConfigSource, ConfigTarget, InputType}; -use crate::model::playlist::{XtreamPlaylistItem}; +use crate::model::StatusCheck; +use crate::model::XtreamPlaylistItem; +use crate::model::{validate_targets, Config, ConfigDto, ConfigInput, ConfigInputOptions, ConfigSource, ConfigTarget, InputType}; +use crate::model::{ApiProxyConfig, ApiProxyServerInfo, ProxyUserCredentials, TargetUser}; use crate::processing::processor::playlist; use crate::repository::user_repository::store_api_user; -use crate::utils::file::config_reader; -use crate::utils::network::request::sanitize_sensitive_info; +use crate::utils::config_reader; +use crate::utils::ip_checker::get_ips; +use crate::utils::request::sanitize_sensitive_info; +use crate::VERSION; use axum::response::IntoResponse; use log::error; -use serde_json::{json}; +use serde_json::json; use std::collections::{BTreeMap, HashSet}; use std::sync::Arc; -use crate::api::endpoints::api_playlist_utils::{get_playlist, get_playlist_for_target}; -use crate::model::healthcheck::StatusCheck; -use crate::VERSION; fn intern_save_config_api_proxy(backup_dir: &str, api_proxy: &ApiProxyConfig, file_path: &str) -> Option { match config_reader::save_api_proxy(file_path, backup_dir, api_proxy) { @@ -299,6 +300,23 @@ async fn config( } +#[derive(Debug, serde::Serialize, serde::Deserialize, Default)] +struct IpCheck { + #[serde(skip_serializing_if = "Option::is_none")] + ipv4: Option, + #[serde(skip_serializing_if = "Option::is_none")] + ipv6: Option, +} + +async fn create_ipinfo_check(app_state: &Arc) -> Option<(Option, Option)> { + if let Some(ipcheck) = app_state.config.ipcheck.as_ref() { + if let Ok(check) = get_ips(&app_state.http_client, ipcheck).await { + return Some(check); + } + } + None +} + async fn create_status_check(app_state: &Arc) -> StatusCheck { let cache = match app_state.cache.as_ref().as_ref() { None => None, @@ -334,6 +352,22 @@ async fn status(axum::extract::State(app_state): axum::extract::State>) -> axum::response::Response { + if let Some((ipv4, ipv6)) = create_ipinfo_check(&app_state).await { + let ipcheck = IpCheck { + ipv4, + ipv6, + }; + return match serde_json::to_string(&ipcheck) { + Ok(json) => axum::response::Response::builder().status(axum::http::StatusCode::OK) + .header(axum::http::header::CONTENT_TYPE, mime::APPLICATION_JSON.to_string()).body(json).unwrap().into_response(), + Err(_) => axum::Json(ipcheck).into_response(), + } + } + axum::http::StatusCode::BAD_REQUEST.into_response() +} + + pub fn v1_api_register(web_auth_enabled: bool, app_state: Arc, web_ui_path: &str) -> axum::Router> { let mut router = axum::Router::new(); router = router @@ -347,6 +381,9 @@ pub fn v1_api_register(web_auth_enabled: bool, app_state: Arc, web_ui_ .route("/playlist", axum::routing::post(playlist_content)) .route("/file/download", axum::routing::post(download_api::queue_download_file)) .route("/file/download/info", axum::routing::get(download_api::download_file_info)); + if app_state.config.ipcheck.is_some() { + router = router.route("/ipinfo", axum::routing::get(ipinfo)); + } if web_auth_enabled { router = router.route_layer(axum::middleware::from_fn_with_state(Arc::clone(&app_state), validator_admin)); } diff --git a/src/api/endpoints/web_index.rs b/src/api/endpoints/web_index.rs index bd0e54026..006efbd2a 100644 --- a/src/api/endpoints/web_index.rs +++ b/src/api/endpoints/web_index.rs @@ -11,7 +11,7 @@ use std::collections::HashMap; use std::path::{Path, PathBuf}; use std::sync::{Arc}; use tower::Service; -use crate::utils::constants::CONSTANTS; +use crate::utils::CONSTANTS; fn no_web_auth_token() -> impl axum::response::IntoResponse + Send { axum::Json(HashMap::from([("token", "authorized")])).into_response() diff --git a/src/api/endpoints/xmltv_api.rs b/src/api/endpoints/xmltv_api.rs index 66b762714..726aa9540 100644 --- a/src/api/endpoints/xmltv_api.rs +++ b/src/api/endpoints/xmltv_api.rs @@ -12,13 +12,13 @@ use std::sync::Arc; use crate::api::api_utils::{get_user_target, serve_file}; use crate::api::model::app_state::AppState; use crate::api::model::request::UserApiRequest; -use crate::model::api_proxy::ProxyUserCredentials; -use crate::model::config::{Config, ConfigTarget, TargetOutput}; +use crate::model::ProxyUserCredentials; +use crate::model::{Config, ConfigTarget, TargetOutput}; use crate::repository::m3u_repository::m3u_get_epg_file_path; use crate::repository::storage::get_target_storage_path; use crate::repository::xtream_repository::{xtream_get_epg_file_path, xtream_get_storage_path}; -use crate::utils::file::file_utils; -use crate::utils::file::file_utils::file_reader; +use crate::utils::file_utils; +use crate::utils::file_utils::file_reader; pub fn get_empty_epg_response() -> impl axum::response::IntoResponse + Send { axum::response::Response::builder() diff --git a/src/api/endpoints/xtream_api.rs b/src/api/endpoints/xtream_api.rs index 1142302bf..441af63df 100644 --- a/src/api/endpoints/xtream_api.rs +++ b/src/api/endpoints/xtream_api.rs @@ -13,21 +13,19 @@ use crate::api::model::xtream::XtreamAuthorizationResponse; use crate::m3u_filter_error::create_m3u_filter_error_result; use crate::m3u_filter_error::info_err; use crate::m3u_filter_error::{str_to_io_error, M3uFilterError, M3uFilterErrorKind}; -use crate::model::api_proxy::{ProxyType, ProxyUserCredentials, UserConnectionPermission}; -use crate::model::config::TargetType; -use crate::model::config::{Config, ConfigInput, ConfigTarget}; -use crate::model::playlist::{get_backdrop_path_value, FieldGetAccessor, PlaylistEntry, PlaylistItemType, XtreamCluster, XtreamPlaylistItem}; -use crate::model::xtream_const; +use crate::model::{ProxyType, ProxyUserCredentials, UserConnectionPermission}; +use crate::model::TargetType; +use crate::model::{Config, ConfigInput, ConfigTarget}; +use crate::model::{get_backdrop_path_value, FieldGetAccessor, PlaylistEntry, PlaylistItemType, XtreamCluster, XtreamPlaylistItem}; use crate::repository::playlist_repository::get_target_id_mapping; use crate::repository::storage::{get_target_storage_path, hex_encode}; use crate::repository::{storage_const, user_repository, xtream_repository}; -use crate::utils::constants::HLS_EXT; -use crate::utils::hash_utils::generate_playlist_uuid; -use crate::utils::json_utils; -use crate::utils::json_utils::get_u32_from_serde_value; -use crate::utils::network::request::{extract_extension_from_url, sanitize_sensitive_info}; -use crate::utils::network::xtream::create_vod_info_from_item; -use crate::utils::network::{request, xtream}; +use crate::utils::HLS_EXT; +use crate::utils::generate_playlist_uuid; +use crate::utils::get_u32_from_serde_value; +use crate::utils::request::{extract_extension_from_url, sanitize_sensitive_info}; +use crate::utils::xtream::create_vod_info_from_item; +use crate::utils::{request, xtream}; use crate::utils::trace_if_enabled; use axum::http::{HeaderMap, StatusCode}; use axum::response::IntoResponse; @@ -136,7 +134,7 @@ impl<'a> XtreamApiStreamRequest<'a> { } pub fn serve_query(file_path: &Path, filter: &HashMap<&str, HashSet>) -> impl IntoResponse + Send { - let filtered = json_utils::json_filter_file(file_path, filter); + let filtered = crate::utils::json_filter_file(file_path, filter); axum::Json(filtered) } @@ -339,8 +337,8 @@ fn get_doc_id_and_field_name(input: &str) -> Option<(u32, &str)> { fn get_doc_resource_field_value(field: &str, doc: Option<&Value>) -> Option { if let Some(Value::Object(info_data)) = doc { - if field.starts_with(xtream_const::XC_PROP_BACKDROP_PATH) { - return get_backdrop_path_value(field, info_data.get(xtream_const::XC_PROP_BACKDROP_PATH)); + if field.starts_with(crate::model::XC_PROP_BACKDROP_PATH) { + return get_backdrop_path_value(field, info_data.get(crate::model::XC_PROP_BACKDROP_PATH)); } else if let Some(Value::String(url)) = info_data.get(field) { return Some(url.to_string()); } @@ -360,21 +358,21 @@ fn xtream_get_info_resource_url(config: &Config, pli: &XtreamPlaylistItem, targe }; if let Some(content) = info_content { let doc: Map = serde_json::from_str(&content)?; - let (field, possible_episode_id) = if let Some(field_name_with_episode_id) = resource.strip_prefix(xtream_const::XC_INFO_RESOURCE_PREFIX_EPISODE) { + let (field, possible_episode_id) = if let Some(field_name_with_episode_id) = resource.strip_prefix(crate::model::XC_INFO_RESOURCE_PREFIX_EPISODE) { if let Some((episode_id, field_name)) = get_doc_id_and_field_name(field_name_with_episode_id) { (field_name, Some(episode_id)) } else { return Ok(None); } } else { - (&resource[xtream_const::XC_INFO_RESOURCE_PREFIX.len()..], None) + (&resource[crate::model::XC_INFO_RESOURCE_PREFIX.len()..], None) }; let info_doc = match pli.xtream_cluster { XtreamCluster::Video | XtreamCluster::Series => { if let Some(episode_id) = possible_episode_id { get_episode_info_doc(&doc, episode_id) } else { - doc.get(xtream_const::XC_TAG_INFO_DATA) + doc.get(crate::model::XC_TAG_INFO_DATA) } } XtreamCluster::Live => None, @@ -388,15 +386,15 @@ fn xtream_get_info_resource_url(config: &Config, pli: &XtreamPlaylistItem, targe } fn get_episode_info_doc(doc: &Map, episode_id: u32) -> Option<&Value> { - let episodes = doc.get(xtream_const::XC_TAG_EPISODES)?.as_object()?; + let episodes = doc.get(crate::model::XC_TAG_EPISODES)?.as_object()?; for season_episodes in episodes.values() { if let Value::Array(episode_list) = season_episodes { for episode in episode_list { if let Value::Object(episode_doc) = episode { - if let Some(episode_id_value) = episode_doc.get(xtream_const::XC_TAG_ID) { + if let Some(episode_id_value) = episode_doc.get(crate::model::XC_TAG_ID) { if let Some(doc_episode_id) = get_u32_from_serde_value(episode_id_value) { if doc_episode_id == episode_id { - return episode_doc.get(xtream_const::XC_TAG_INFO_DATA); + return episode_doc.get(crate::model::XC_TAG_INFO_DATA); } } } @@ -410,7 +408,7 @@ fn get_episode_info_doc(doc: &Map, episode_id: u32) -> Option<&Va fn get_season_info_doc(doc: &Vec, season_id: u32) -> Option<&Value> { for season in doc { if let Value::Object(season_doc) = season { - if let Some(season_id_value) = season_doc.get(xtream_const::XC_TAG_ID) { + if let Some(season_id_value) = season_doc.get(crate::model::XC_TAG_ID) { if let Some(doc_season_id) = get_u32_from_serde_value(season_id_value) { if doc_season_id == season_id { return Some(season); @@ -433,10 +431,10 @@ fn xtream_get_season_resource_url(config: &Config, pli: &XtreamPlaylistItem, tar if let Some(content) = info_content { let doc: Map = serde_json::from_str(&content)?; - if let Some(field_name_with_season_id) = resource.strip_prefix(xtream_const::XC_SEASON_RESOURCE_PREFIX) { + if let Some(field_name_with_season_id) = resource.strip_prefix(crate::model::XC_SEASON_RESOURCE_PREFIX) { if let Some((season_id, field)) = get_doc_id_and_field_name(field_name_with_season_id) { let seasons_doc = match pli.xtream_cluster { - XtreamCluster::Series => doc.get(xtream_const::XC_TAG_SEASONS_DATA), + XtreamCluster::Series => doc.get(crate::model::XC_TAG_SEASONS_DATA), XtreamCluster::Video | XtreamCluster::Live => None, }; @@ -469,9 +467,9 @@ async fn xtream_player_api_resource( let virtual_id: u32 = try_result_bad_request!(resource_req.stream_id.trim().parse()); let resource = resource_req.action_path.trim(); let (pli, _) = try_result_bad_request!(xtream_repository::xtream_get_item_for_stream_id(virtual_id, &app_state.config, target, None), true, format!("Failed to read xtream item for stream id {}", virtual_id)); - let stream_url = if resource.starts_with(xtream_const::XC_INFO_RESOURCE_PREFIX) { + let stream_url = if resource.starts_with(crate::model::XC_INFO_RESOURCE_PREFIX) { try_result_bad_request!(xtream_get_info_resource_url(&app_state.config, &pli, target, resource)) - } else if resource.starts_with(xtream_const::XC_SEASON_RESOURCE_PREFIX) { + } else if resource.starts_with(crate::model::XC_SEASON_RESOURCE_PREFIX) { try_result_bad_request!(xtream_get_season_resource_url(&app_state.config, &pli, target, resource)) } else { pli.get_field(resource) @@ -627,8 +625,8 @@ async fn xtream_get_short_epg(app_state: &AppState, user: &ProxyUserCredentials, if pli.provider_id > 0 { let input_name = &pli.input_name; if let Some(input) = app_state.config.get_input_by_name(input_name.as_str()) { - if let Some(action_url) = xtream::get_xtream_player_api_action_url(input, xtream_const::XC_ACTION_GET_SHORT_EPG) { - let mut info_url = format!("{action_url}&{}={}", xtream_const::XC_TAG_STREAM_ID, pli.provider_id); + if let Some(action_url) = xtream::get_xtream_player_api_action_url(input, crate::model::XC_ACTION_GET_SHORT_EPG) { + let mut info_url = format!("{action_url}&{}={}", crate::model::XC_TAG_STREAM_ID, pli.provider_id); if !(limit.is_empty() || limit.eq("0")) { info_url = format!("{info_url}&limit={limit}"); } @@ -655,21 +653,21 @@ async fn xtream_get_short_epg(app_state: &AppState, user: &ProxyUserCredentials, async fn xtream_player_api_handle_content_action(config: &Config, target_name: &str, action: &str, category_id: Option, user: &ProxyUserCredentials) -> Option { if let Ok((path, content)) = match action { - xtream_const::XC_ACTION_GET_LIVE_CATEGORIES => xtream_repository::xtream_get_collection_path(config, target_name, storage_const::COL_CAT_LIVE), - xtream_const::XC_ACTION_GET_VOD_CATEGORIES => xtream_repository::xtream_get_collection_path(config, target_name, storage_const::COL_CAT_VOD), - xtream_const::XC_ACTION_GET_SERIES_CATEGORIES => xtream_repository::xtream_get_collection_path(config, target_name, storage_const::COL_CAT_SERIES), + crate::model::XC_ACTION_GET_LIVE_CATEGORIES => xtream_repository::xtream_get_collection_path(config, target_name, storage_const::COL_CAT_LIVE), + crate::model::XC_ACTION_GET_VOD_CATEGORIES => xtream_repository::xtream_get_collection_path(config, target_name, storage_const::COL_CAT_VOD), + crate::model::XC_ACTION_GET_SERIES_CATEGORIES => xtream_repository::xtream_get_collection_path(config, target_name, storage_const::COL_CAT_SERIES), _ => Err(str_to_io_error("")) } { if let Some(file_path) = path { // load user bouquet let filter = match action { - xtream_const::XC_ACTION_GET_LIVE_CATEGORIES => user_repository::user_get_bouquet_filter(config, &user.username, category_id, TargetType::Xtream, XtreamCluster::Live).await, - xtream_const::XC_ACTION_GET_VOD_CATEGORIES => user_repository::user_get_bouquet_filter(config, &user.username, category_id, TargetType::Xtream, XtreamCluster::Video).await, - xtream_const::XC_ACTION_GET_SERIES_CATEGORIES => user_repository::user_get_bouquet_filter(config, &user.username, category_id, TargetType::Xtream, XtreamCluster::Series).await, + crate::model::XC_ACTION_GET_LIVE_CATEGORIES => user_repository::user_get_bouquet_filter(config, &user.username, category_id, TargetType::Xtream, XtreamCluster::Live).await, + crate::model::XC_ACTION_GET_VOD_CATEGORIES => user_repository::user_get_bouquet_filter(config, &user.username, category_id, TargetType::Xtream, XtreamCluster::Video).await, + crate::model::XC_ACTION_GET_SERIES_CATEGORIES => user_repository::user_get_bouquet_filter(config, &user.username, category_id, TargetType::Xtream, XtreamCluster::Series).await, _ => None }; if let Some(flt) = filter { - return Some(serve_query(&file_path, &HashMap::from([(xtream_const::XC_TAG_CATEGORY_ID, flt)])).into_response()); + return Some(serve_query(&file_path, &HashMap::from([(crate::model::XC_TAG_CATEGORY_ID, flt)])).into_response()); } return Some(serve_file(&file_path, mime::APPLICATION_JSON).await.into_response()); } else if let Some(payload) = content { @@ -686,19 +684,19 @@ async fn xtream_get_catchup_response(app_state: &AppState, target: &ConfigTarget let virtual_id: u32 = try_result_bad_request!(FromStr::from_str(stream_id)); let (pli, _) = try_result_bad_request!(xtream_repository::xtream_get_item_for_stream_id(virtual_id, &app_state.config, target, Some(XtreamCluster::Live))); let input = try_option_bad_request!(app_state.config.get_input_by_name(pli.input_name.as_str())); - let info_url = try_option_bad_request!(xtream::get_xtream_player_api_action_url(input, xtream_const::XC_ACTION_GET_CATCHUP_TABLE) - .map(|action_url| format!("{action_url}&{}={}&start={start}&end={end}", xtream_const::XC_TAG_STREAM_ID, pli.provider_id))); + let info_url = try_option_bad_request!(xtream::get_xtream_player_api_action_url(input, crate::model::XC_ACTION_GET_CATCHUP_TABLE) + .map(|action_url| format!("{action_url}&{}={}&start={start}&end={end}", crate::model::XC_TAG_STREAM_ID, pli.provider_id))); let content = try_result_bad_request!(xtream::get_xtream_stream_info_content(Arc::clone(&app_state.http_client), info_url.as_str(), input).await); let mut doc: Map = try_result_bad_request!(serde_json::from_str(&content)); - let epg_listings = try_option_bad_request!(doc.get_mut(xtream_const::XC_TAG_EPG_LISTINGS).and_then(Value::as_array_mut)); + let epg_listings = try_option_bad_request!(doc.get_mut(crate::model::XC_TAG_EPG_LISTINGS).and_then(Value::as_array_mut)); let target_path = try_option_bad_request!(get_target_storage_path(&app_state.config, target.name.as_str())); let (mut target_id_mapping, file_lock) = get_target_id_mapping(&app_state.config, &target_path).await; for epg_list_item in epg_listings.iter_mut().filter_map(Value::as_object_mut) { // TODO epg_id - if let Some(catchup_provider_id) = epg_list_item.get(xtream_const::XC_TAG_ID).and_then(Value::as_str).and_then(|id| id.parse::().ok()) { + if let Some(catchup_provider_id) = epg_list_item.get(crate::model::XC_TAG_ID).and_then(Value::as_str).and_then(|id| id.parse::().ok()) { let uuid = generate_playlist_uuid(&hex_encode(&pli.get_uuid()), &catchup_provider_id.to_string(), pli.item_type, &pli.url); let virtual_id = target_id_mapping.get_and_update_virtual_id(&uuid, catchup_provider_id, PlaylistItemType::Catchup, pli.provider_id); - epg_list_item.insert(xtream_const::XC_TAG_ID.to_string(), Value::String(virtual_id.to_string())); + epg_list_item.insert(crate::model::XC_TAG_ID.to_string(), Value::String(virtual_id.to_string())); } } if let Err(err) = target_id_mapping.persist() { @@ -764,21 +762,21 @@ async fn xtream_player_api( }; match action { - xtream_const::XC_ACTION_GET_ACCOUNT_INFO => { + crate::model::XC_ACTION_GET_ACCOUNT_INFO => { return axum::response::Json(get_user_info(&user, app_state).await).into_response(); } - xtream_const::XC_ACTION_GET_SERIES_INFO => { + crate::model::XC_ACTION_GET_SERIES_INFO => { skip_json_response_if_flag_set!(skip_series, xtream_get_stream_info_response(app_state, &user, target, api_req.series_id.trim(), XtreamCluster::Series).await); } - xtream_const::XC_ACTION_GET_VOD_INFO => { + crate::model::XC_ACTION_GET_VOD_INFO => { skip_json_response_if_flag_set!(skip_vod, xtream_get_stream_info_response(app_state, &user, target, api_req.vod_id.trim(), XtreamCluster::Video).await); } - xtream_const::XC_ACTION_GET_EPG | xtream_const::XC_ACTION_GET_SHORT_EPG => { + crate::model::XC_ACTION_GET_EPG | crate::model::XC_ACTION_GET_SHORT_EPG => { return xtream_get_short_epg( app_state, &user, target, api_req.stream_id.trim(), api_req.limit.trim(), ).await.into_response(); } - xtream_const::XC_ACTION_GET_CATCHUP_TABLE => { + crate::model::XC_ACTION_GET_CATCHUP_TABLE => { skip_json_response_if_flag_set!(skip_live, xtream_get_catchup_response(app_state, target, api_req.stream_id.trim(), api_req.start.trim(), api_req.end.trim()).await); } _ => {} @@ -793,11 +791,11 @@ async fn xtream_player_api( } let result = match action { - xtream_const::XC_ACTION_GET_LIVE_STREAMS => + crate::model::XC_ACTION_GET_LIVE_STREAMS => skip_flag_optional!(skip_live, xtream_repository::xtream_load_rewrite_playlist(XtreamCluster::Live, &app_state.config, target, category_id, &user).await), - xtream_const::XC_ACTION_GET_VOD_STREAMS => + crate::model::XC_ACTION_GET_VOD_STREAMS => skip_flag_optional!(skip_vod, xtream_repository::xtream_load_rewrite_playlist(XtreamCluster::Video, &app_state.config, target, category_id, &user).await), - xtream_const::XC_ACTION_GET_SERIES => + crate::model::XC_ACTION_GET_SERIES => skip_flag_optional!(skip_series, xtream_repository::xtream_load_rewrite_playlist(XtreamCluster::Series, &app_state.config, target, category_id, &user).await), _ => Some(Err(info_err!(format!("Cant find action: {action} for target: {}", &target.name)) )), diff --git a/src/api/main_api.rs b/src/api/main_api.rs index a5316935d..97035ae87 100644 --- a/src/api/main_api.rs +++ b/src/api/main_api.rs @@ -11,8 +11,8 @@ use crate::api::model::app_state::{AppState, HdHomerunAppState}; use crate::api::model::download::DownloadQueue; use crate::api::model::streams::shared_stream_manager::SharedStreamManager; use crate::api::scheduler::start_scheduler; -use crate::model::config::{validate_targets, Config, ProcessTargets, RateLimitConfig, ScheduleConfig}; -use crate::model::healthcheck::{Healthcheck}; +use crate::model::{validate_targets, Config, ProcessTargets, RateLimitConfig, ScheduleConfig}; +use crate::model::{Healthcheck}; use crate::processing::processor::playlist; use crate::tools::lru_cache::LRUResourceCache; use log::{error, info}; @@ -27,7 +27,7 @@ use axum::Router; use tokio::sync::Mutex; use tower_governor::key_extractor::SmartIpKeyExtractor; use crate::api::api_utils::{get_build_time, get_server_time}; -use crate::utils::network::request::create_client; +use crate::utils::request::create_client; use crate::VERSION; fn get_web_dir_path(web_ui_enabled: bool, web_root: &str) -> Result { diff --git a/src/api/model/active_provider_manager.rs b/src/api/model/active_provider_manager.rs index f8dc61213..9bacbbca9 100644 --- a/src/api/model/active_provider_manager.rs +++ b/src/api/model/active_provider_manager.rs @@ -1,4 +1,4 @@ -use crate::model::config::{Config, ConfigInput}; +use crate::model::{Config, ConfigInput}; use log::{debug, log_enabled}; use std::collections::HashMap; use std::ops::Deref; @@ -6,7 +6,7 @@ use std::sync::atomic::{AtomicUsize, Ordering}; use std::sync::Arc; use tokio::sync::{Mutex, RwLock}; use crate::api::model::provider_config::{ProviderConfig, ProviderConfigWrapper}; -use crate::utils::default_utils::{default_grace_period_millis, default_grace_period_timeout_secs}; +use crate::utils::{default_grace_period_millis, default_grace_period_timeout_secs}; pub struct ProviderConnectionGuard { manager: Arc, @@ -574,7 +574,7 @@ impl ActiveProviderManager { mod tests { use std::sync::atomic::AtomicU16; use super::*; - use crate::model::config::{ConfigInputAlias, InputFetchMethod, InputType}; + use crate::model::{ConfigInputAlias, InputFetchMethod, InputType}; use crate::Arc; use std::thread; diff --git a/src/api/model/active_user_manager.rs b/src/api/model/active_user_manager.rs index 6630befb6..87fd6f451 100644 --- a/src/api/model/active_user_manager.rs +++ b/src/api/model/active_user_manager.rs @@ -1,7 +1,6 @@ -use crate::model::api_proxy::{ProxyUserCredentials, UserConnectionPermission}; -use crate::model::config::Config; -use crate::utils::default_utils::{default_grace_period_millis, default_grace_period_timeout_secs}; -use crate::utils::time_utils::current_time_secs; +use crate::model::{ProxyUserCredentials, UserConnectionPermission}; +use crate::model::Config; +use crate::utils::{current_time_secs, default_grace_period_millis, default_grace_period_timeout_secs}; use jsonwebtoken::get_current_timestamp; use log::{debug, info}; use rand::RngCore; diff --git a/src/api/model/app_state.rs b/src/api/model/app_state.rs index 9eefff0a0..e9b5ec31f 100644 --- a/src/api/model/app_state.rs +++ b/src/api/model/app_state.rs @@ -4,9 +4,7 @@ use crate::api::model::active_provider_manager::ActiveProviderManager; use crate::api::model::active_user_manager::ActiveUserManager; use crate::api::model::download::DownloadQueue; use crate::api::model::streams::shared_stream_manager::SharedStreamManager; -use crate::model::api_proxy::UserConnectionPermission; -use crate::model::config::{Config}; -use crate::model::hdhomerun_config::HdHomeRunDeviceConfig; +use crate::model::{Config, UserConnectionPermission, HdHomeRunDeviceConfig}; use crate::tools::lru_cache::LRUResourceCache; #[derive(Clone)] diff --git a/src/api/model/config.rs b/src/api/model/config.rs index 216d2a118..3798033bd 100644 --- a/src/api/model/config.rs +++ b/src/api/model/config.rs @@ -1,8 +1,6 @@ use serde::{Deserialize, Serialize}; -use crate::model::api_proxy::ApiProxyConfig; -use crate::model::config::{LogConfig, ProcessingOrder, ReverseProxyConfig, ScheduleConfig, WebUiConfig}; -use crate::model::config::{ConfigApi, ConfigRename, ConfigSort, ConfigTargetOptions, InputType, MessagingConfig, TargetOutput, VideoConfig}; +use crate::model::{ApiProxyConfig, ConfigApi, InputType, LogConfig, MessagingConfig, ProcessingOrder, ReverseProxyConfig, ScheduleConfig, VideoConfig, ConfigRename, ConfigSort, ConfigTargetOptions, TargetOutput, WebUiConfig}; #[derive(Deserialize, Serialize, Debug, Clone)] pub struct ServerInputConfig { diff --git a/src/api/model/download.rs b/src/api/model/download.rs index 808884f64..0ed865cfa 100644 --- a/src/api/model/download.rs +++ b/src/api/model/download.rs @@ -6,9 +6,9 @@ use std::sync::Arc; use deunicode::deunicode; use serde::{Deserialize, Serialize}; -use crate::model::config::VideoDownloadConfig; +use crate::model::{VideoDownloadConfig}; use crate::repository::storage::hash_string_as_hex; -use crate::utils::constants::{CONSTANTS, FILENAME_TRIM_PATTERNS}; +use crate::utils::{CONSTANTS, FILENAME_TRIM_PATTERNS}; /// File-Download information. #[derive(Clone)] diff --git a/src/api/model/model_utils.rs b/src/api/model/model_utils.rs index 4b765e757..fdf246681 100644 --- a/src/api/model/model_utils.rs +++ b/src/api/model/model_utils.rs @@ -2,7 +2,7 @@ use reqwest::{StatusCode}; use std::collections::{HashSet}; use std::str::FromStr; use reqwest::header::HeaderMap; -use crate::utils::constants::{MEDIA_STREAM_HEADERS}; +use crate::utils::{MEDIA_STREAM_HEADERS}; pub fn get_response_headers(headers: &HeaderMap) -> Vec<(String, String)> { let response_headers: Vec<(String, String)> = headers.iter() diff --git a/src/api/model/provider_config.rs b/src/api/model/provider_config.rs index fce730e62..f97d5bbff 100644 --- a/src/api/model/provider_config.rs +++ b/src/api/model/provider_config.rs @@ -1,5 +1,5 @@ use crate::api::model::active_provider_manager::ProviderAllocation; -use crate::model::config::{ConfigInput, ConfigInputAlias, InputType, InputUserInfo}; +use crate::model::{ConfigInput, ConfigInputAlias, InputType, InputUserInfo}; use jsonwebtoken::get_current_timestamp; use log::debug; use std::ops::Deref; diff --git a/src/api/model/streams/active_client_stream.rs b/src/api/model/streams/active_client_stream.rs index a283c5a1b..ba040ecf5 100644 --- a/src/api/model/streams/active_client_stream.rs +++ b/src/api/model/streams/active_client_stream.rs @@ -6,7 +6,7 @@ use crate::api::model::app_state::AppState; use crate::api::model::stream::BoxedProviderStream; use crate::api::model::stream_error::StreamError; use crate::api::model::streams::chunked_buffer::ChunkedBuffer; -use crate::model::api_proxy::{ProxyUserCredentials, UserConnectionPermission}; +use crate::model::{ProxyUserCredentials, UserConnectionPermission}; use bytes::Bytes; use futures::Stream; use log::{error, info}; diff --git a/src/api/model/streams/client_stream.rs b/src/api/model/streams/client_stream.rs index 33bf67c1b..c6fc52dca 100644 --- a/src/api/model/streams/client_stream.rs +++ b/src/api/model/streams/client_stream.rs @@ -9,7 +9,7 @@ use crate::api::model::stream::BoxedProviderStream; use crate::api::model::stream_error::StreamError; use crate::utils::trace_if_enabled; use crate::tools::atomic_once_flag::AtomicOnceFlag; -use crate::utils::network::request::sanitize_sensitive_info; +use crate::utils::request::sanitize_sensitive_info; /// This stream counts the send bytes for reconnecting to the actual position and /// sets the `close_signal` if the client drops the connection. diff --git a/src/api/model/streams/provider_stream.rs b/src/api/model/streams/provider_stream.rs index 5287f35c6..ff6d7cce0 100644 --- a/src/api/model/streams/provider_stream.rs +++ b/src/api/model/streams/provider_stream.rs @@ -4,10 +4,10 @@ use crate::api::model::model_utils::get_response_headers; use crate::api::model::stream_error::StreamError; use crate::api::model::streams::custom_video_stream::CustomVideoStream; use crate::api::model::streams::provider_stream_factory::{create_provider_stream, BufferStreamOptions}; -use crate::model::config::{Config}; -use crate::model::playlist::PlaylistItemType; +use crate::model::{Config}; +use crate::model::PlaylistItemType; use crate::utils::debug_if_enabled; -use crate::utils::network::request::{get_request_headers, sanitize_sensitive_info}; +use crate::utils::request::{get_request_headers, sanitize_sensitive_info}; use futures::TryStreamExt; use log::{error, trace}; use reqwest::StatusCode; diff --git a/src/api/model/streams/provider_stream_factory.rs b/src/api/model/streams/provider_stream_factory.rs index e3eecc5d4..6ebd80191 100644 --- a/src/api/model/streams/provider_stream_factory.rs +++ b/src/api/model/streams/provider_stream_factory.rs @@ -5,11 +5,11 @@ use crate::api::model::streams::buffered_stream::BufferedStream; use crate::api::model::streams::client_stream::ClientStream; use crate::api::model::streams::provider_stream::{create_channel_unavailable_stream, get_header_filter_for_item_type}; use crate::api::model::streams::timed_client_stream::{TimeoutClientStream}; -use crate::model::config::{Config}; -use crate::model::playlist::PlaylistItemType; +use crate::model::{Config}; +use crate::model::PlaylistItemType; use crate::tools::atomic_once_flag::AtomicOnceFlag; use crate::utils::debug_if_enabled; -use crate::utils::network::request::{classify_content_type, get_request_headers, sanitize_sensitive_info, MimeCategory}; +use crate::utils::request::{classify_content_type, get_request_headers, sanitize_sensitive_info, MimeCategory}; use futures::stream::{self}; use futures::{StreamExt, TryStreamExt}; use log::{debug, warn}; @@ -420,7 +420,7 @@ pub async fn create_provider_stream(cfg: &Config, // use actix_web::{HttpRequest, HttpResponse}; // use futures::StreamExt; // use std::sync::Arc; -// use crate::model::config::Config; +// use crate::model::Config; // // #[tokio::test] // async fn test_stream() { diff --git a/src/api/model/streams/shared_stream_manager.rs b/src/api/model/streams/shared_stream_manager.rs index b9231c5aa..ba3df51f4 100644 --- a/src/api/model/streams/shared_stream_manager.rs +++ b/src/api/model/streams/shared_stream_manager.rs @@ -2,7 +2,7 @@ use crate::api::model::app_state::AppState; use crate::api::model::streams::provider_stream_factory::STREAM_QUEUE_SIZE; use crate::api::model::stream_error::StreamError; use crate::utils::debug_if_enabled; -use crate::utils::network::request::sanitize_sensitive_info; +use crate::utils::request::sanitize_sensitive_info; use bytes::Bytes; use futures::stream::BoxStream; use futures::{Stream, StreamExt}; diff --git a/src/api/model/xtream.rs b/src/api/model/xtream.rs index b166f88de..2bda9f843 100644 --- a/src/api/model/xtream.rs +++ b/src/api/model/xtream.rs @@ -1,7 +1,7 @@ -use crate::model::api_proxy::{ApiProxyServerInfo, ProxyUserCredentials, ProxyUserStatus}; +use crate::model::{ApiProxyServerInfo, ProxyUserCredentials, ProxyUserStatus}; use chrono::{Duration, Local}; use serde::{Deserialize, Serialize}; -use crate::utils::constants::CONSTANTS; +use crate::utils::CONSTANTS; #[derive(Serialize, Deserialize, Clone)] pub struct XtreamUserInfoResponse { @@ -93,8 +93,8 @@ impl XtreamAuthorizationResponse { }, server_info: XtreamServerInfoResponse { url: server_info.host.clone(), - port: if server_info.protocol == "http" { server_info.port.clone() } else { String::from("80") }, - https_port: if server_info.protocol == "https" { server_info.port.clone() } else { String::from("443") }, + port: if server_info.protocol == "http" { server_info.port.as_ref().map_or("80", |v| v.as_str()).to_string() } else { String::from("80") }, + https_port: if server_info.protocol == "https" { server_info.port.as_ref().map_or("443", |v| v.as_str()).to_string() } else { String::from("443") }, server_protocol: server_info.protocol.clone(), rtmp_port: String::new(), timezone: server_info.timezone.to_string(), diff --git a/src/api/scheduler.rs b/src/api/scheduler.rs index e869ced2e..38ce178b9 100644 --- a/src/api/scheduler.rs +++ b/src/api/scheduler.rs @@ -3,9 +3,9 @@ use std::str::FromStr; use std::time::{Duration, Instant, SystemTime}; use chrono::{DateTime, FixedOffset, Local}; use cron::Schedule; -use log::error; -use crate::utils::sys_utils::exit; -use crate::model::config::{Config, ProcessTargets}; +use crate::utils::{exit}; +use log::{error}; +use crate::model::{Config, ProcessTargets}; use crate::processing::processor::playlist::exec_processing; pub fn datetime_to_instant(datetime: DateTime) -> Instant { diff --git a/src/auth/authenticator.rs b/src/auth/authenticator.rs index b8b50e1c0..feb321ff5 100644 --- a/src/auth/authenticator.rs +++ b/src/auth/authenticator.rs @@ -2,7 +2,7 @@ use std::sync::Arc; use chrono::{Local, Duration}; use jsonwebtoken::{Algorithm, DecodingKey, encode, decode, EncodingKey, Header, Validation, TokenData}; use crate::api::api_utils::get_username_from_auth_header; -use crate::model::config::WebAuthConfig; +use crate::model::WebAuthConfig; use crate::api::model::app_state::AppState; use crate::auth::auth_bearer::AuthBearer; use crate::m3u_filter_error::to_io_error; diff --git a/src/foundation/filter.rs b/src/foundation/filter.rs index 082a18e21..990cb035d 100644 --- a/src/foundation/filter.rs +++ b/src/foundation/filter.rs @@ -7,12 +7,12 @@ use pest::iterators::Pair; use pest::Parser; use crate::m3u_filter_error::{M3uFilterError, M3uFilterErrorKind}; -use crate::model::config::ItemField; -use crate::model::playlist::{PlaylistItem, PlaylistItemType}; +use crate::model::ItemField; +use crate::model::{PlaylistItem, PlaylistItemType}; use crate::tools::directed_graph::DirectedGraph; use crate::m3u_filter_error::{create_m3u_filter_error_result, info_err}; -use crate::utils::constants::CONSTANTS; -use crate::utils::sys_utils::exit; +use crate::utils::CONSTANTS; +use crate::utils::exit; pub fn get_field_value(pli: &PlaylistItem, field: &ItemField) -> String { let header = &pli.header; @@ -564,8 +564,8 @@ pub fn apply_templates_to_pattern(pattern: &str, templates: &Vec PlaylistItem { PlaylistItem { diff --git a/src/main.rs b/src/main.rs index 8d8b7d7ba..4ab8e4a99 100644 --- a/src/main.rs +++ b/src/main.rs @@ -10,12 +10,12 @@ mod modules; include_modules!(); use crate::auth::password::generate_password; -use crate::model::config::{validate_targets, Config, HealthcheckConfig, LogLevelConfig, ProcessTargets}; -use crate::model::healthcheck::Healthcheck; +use crate::model::{validate_targets, Config, HealthcheckConfig, Healthcheck, ProcessTargets, LogLevelConfig}; use crate::processing::processor::playlist; -use crate::utils::file::config_reader::config_file_reader; -use crate::utils::file::file_utils; -use crate::utils::network::request::{create_client, set_sanitize_sensitive_info}; +use crate::utils::config_reader::config_file_reader; +use utils::config_reader; +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; @@ -23,7 +23,6 @@ use log::{error, info, LevelFilter}; use std::fs::File; use std::path::{Path, PathBuf}; use std::sync::Arc; -use utils::file::config_reader; const LOG_ERROR_LEVEL_MOD: &[&str] = &[ "reqwest::async_impl::client", diff --git a/src/messaging.rs b/src/messaging.rs index d6bb05448..d60da306c 100644 --- a/src/messaging.rs +++ b/src/messaging.rs @@ -1,5 +1,5 @@ use std::sync::Arc; -use crate::model::config::{MessagingConfig}; +use crate::model::{MessagingConfig}; use log::{debug, error}; use reqwest::{header}; diff --git a/src/model/api_proxy.rs b/src/model/api_proxy.rs index 573beec2e..c798f5dd3 100644 --- a/src/model/api_proxy.rs +++ b/src/model/api_proxy.rs @@ -1,9 +1,9 @@ use crate::api::model::app_state::AppState; use crate::m3u_filter_error::{create_m3u_filter_error_result, info_err, M3uFilterError, M3uFilterErrorKind}; -use crate::model::config::{Config, ClusterFlags}; +use crate::model::{Config, ClusterFlags}; use crate::repository::user_repository::{backup_api_user_db_file, get_api_user_db_path, load_api_user, merge_api_user}; -use crate::utils::default_utils::default_as_true; -use crate::utils::file::config_reader; +use crate::utils::default_as_true; +use crate::utils::config_reader; use chrono::Local; use enum_iterator::Sequence; use log::debug; @@ -13,7 +13,7 @@ use std::fmt::Display; use std::fs; use std::str::FromStr; use serde::{Deserialize, Deserializer, Serialize, Serializer}; -use crate::model::playlist::PlaylistItemType; +use crate::model::PlaylistItemType; #[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq)] pub enum UserConnectionPermission { @@ -311,7 +311,8 @@ pub struct ApiProxyServerInfo { pub name: String, pub protocol: String, pub host: String, - pub port: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub port: Option, pub timezone: String, pub message: String, #[serde(default, skip_serializing_if = "Option::is_none")] @@ -333,15 +334,15 @@ impl ApiProxyServerInfo { if self.host.is_empty() { return Err(info_err!("host cant be empty for api server config".to_string())); } - self.port = self.port.trim().to_string(); - if self.port.is_empty() { - if self.protocol == "http" { - self.port = "80".to_string(); + if let Some(port)= self.port.as_ref() { + let port = port.trim().to_string(); + if port.is_empty() { + self.port = None; + } else if port.parse::().is_err() { + return Err(info_err!("invalid port for api server config".to_string())); } else { - self.port = "443".to_string(); + self.port = Some(port); } - } else if self.port.parse::().is_err() { - return Err(info_err!("invalid port for api server config".to_string())); } self.timezone = self.timezone.trim().to_string(); @@ -373,10 +374,10 @@ impl ApiProxyServerInfo { } pub fn get_base_url(&self) -> String { - let base_url = if self.port.is_empty() { - format!("{}://{}", self.protocol, self.host) + let base_url = if let Some(port) = self.port.as_ref() { + format!("{}://{}:{port}", self.protocol, self.host) } else { - format!("{}://{}:{}", self.protocol, self.host, self.port) + format!("{}://{}", self.protocol, self.host) }; match &self.path { diff --git a/src/model/cluster_flags.rs b/src/model/cluster_flags.rs new file mode 100644 index 000000000..2e8089342 --- /dev/null +++ b/src/model/cluster_flags.rs @@ -0,0 +1,128 @@ +use std::fmt; +use bitflags::bitflags; +use serde::{de, Deserialize, Deserializer, Serialize, Serializer}; +use serde::de::{Error, SeqAccess, Visitor}; +use crate::model::{PlaylistItemType, XtreamCluster}; + +bitflags! { + #[derive(Debug, Clone, PartialEq, Eq)] + pub struct ClusterFlags: u16 { + const Live = 1; // 0b0000_0001 + const Vod = 1 << 1; // 0b0000_0010 + const Series = 1 << 2; // 0b0000_0100 + } +} + +impl ClusterFlags { + pub fn has_cluster(&self, item_type: PlaylistItemType) -> bool { + XtreamCluster::try_from(item_type).ok().is_some_and(|cluster| match cluster { + XtreamCluster::Live => self.contains(ClusterFlags::Live), + XtreamCluster::Video => self.contains(ClusterFlags::Vod), + XtreamCluster::Series => self.contains(ClusterFlags::Series), + }) + } + + pub fn has_full_flags(&self) -> bool { + self.is_all() + } + + fn from_items(items: I) -> Result + where + I: IntoIterator, + S: AsRef, + { + let mut result = ClusterFlags::empty(); + + for item in items { + match item.as_ref().trim() { + "live" => result.set(ClusterFlags::Live, true), + "vod" => result.set(ClusterFlags::Vod, true), + "series" => result.set(ClusterFlags::Series, true), + _ => return Err("Invalid flag {item}, allowed are live, vod, series"), + } + } + + Ok(result) + } +} + +impl fmt::Display for ClusterFlags { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let mut flag_strings = Vec::new(); + if self.contains(ClusterFlags::Live) { + flag_strings.push("live"); + } + if self.contains(ClusterFlags::Vod) { + flag_strings.push("vod"); + } + if self.contains(ClusterFlags::Series) { + flag_strings.push("series"); + } + + write!(f, "[{}]", flag_strings.join(",")) + } +} + +impl TryFrom<&str> for ClusterFlags { + type Error = &'static str; + + fn try_from(value: &str) -> Result { + let input = value.trim().trim_matches(['[', ']'].as_ref()); + let items = input.split(',').map(str::trim); + ClusterFlags::from_items(items) + } +} + +impl TryFrom> for ClusterFlags { + type Error = &'static str; + + fn try_from(value: Vec) -> Result { + ClusterFlags::from_items(value) + } +} + +impl Serialize for ClusterFlags { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + serializer.serialize_some(&self.to_string()) + } +} + +impl<'de> Deserialize<'de> for ClusterFlags { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + struct ClusterFlagsVisitor; + + impl<'de> Visitor<'de> for ClusterFlagsVisitor { + type Value = ClusterFlags; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("a string or a map entry like : [vod, live, series]") + } + + fn visit_str(self, v: &str) -> Result + where + E: de::Error, + { + ClusterFlags::try_from(v).map_err(E::custom) + } + + fn visit_seq(self, mut seq: A) -> Result + where + A: SeqAccess<'de>, + { + let mut values = Vec::new(); + while let Some(val) = seq.next_element::()? { + let entry = val.trim().to_lowercase(); + values.push(entry); + } + ClusterFlags::try_from(values).map_err(A::Error::custom) + } + } + deserializer.deserialize_any(ClusterFlagsVisitor) + } +} diff --git a/src/model/config.rs b/src/model/config.rs index 929c3a729..3705492ab 100644 --- a/src/model/config.rs +++ b/src/model/config.rs @@ -1,58 +1,28 @@ #![allow(clippy::struct_excessive_bools)] -use bitflags::bitflags; use enum_iterator::Sequence; -use std::borrow::BorrowMut; use std::collections::{HashMap, HashSet}; -use std::fmt; use std::fmt::Display; -use std::fs::File; -use std::io::BufRead; use std::path::PathBuf; -use std::str::FromStr; use std::sync::Arc; use tokio::sync::RwLock; -use crate::auth::user::UserCredential; use log::{debug, error, warn}; use path_clean::PathClean; use rand::Rng; -use regex::Regex; -use serde::de::{self, Error, SeqAccess, Visitor}; -use serde::{Deserialize, Deserializer, Serialize, Serializer}; -use url::Url; -use crate::foundation::filter::{get_filter, prepare_templates, Filter, MockValueProcessor, PatternTemplate, ValueProvider}; -use crate::m3u_filter_error::info_err; +use crate::foundation::filter::{prepare_templates, PatternTemplate}; use crate::m3u_filter_error::{M3uFilterError, M3uFilterErrorKind}; -use crate::messaging::MsgKind; -use crate::model::api_proxy::{ApiProxyConfig, ApiProxyServerInfo, ProxyUserCredentials}; -use crate::model::mapping::Mapping; -use crate::model::mapping::Mappings; -use crate::utils::default_utils::{default_as_default, default_as_true, default_connect_timeout_secs, default_grace_period_millis, default_grace_period_timeout_secs, default_resolve_delay_secs}; -use crate::utils::file::file_lock_manager::FileLockManager; -use crate::utils::file::file_utils; -use crate::utils::file::file_utils::file_reader; -use crate::utils::size_utils::{parse_size_base_2, parse_to_kbps}; -use crate::utils::sys_utils::exit; - -const DEFAULT_USER_AGENT: &str = "Mozilla/5.0 (AppleTV; U; CPU OS 14_2 like Mac OS X; en-us) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.0.1 Safari/605.1.15"; - -pub const MAPPER_ATTRIBUTE_FIELDS: &[&str] = &[ - "name", "title", "caption", "group", "id", "chno", "logo", - "logo_small", "parent_code", "audio_track", - "time_shift", "rec", "url", "epg_channel_id", "epg_id" -]; - -pub const AFFIX_FIELDS: &[&str] = &["name", "title", "caption", "group"]; -pub const COUNTER_FIELDS: &[&str] = &["name", "title", "caption", "chno"]; +use crate::model::{ApiProxyConfig, ApiProxyServerInfo, ProxyUserCredentials, Mappings}; +use crate::utils::{default_as_default, default_connect_timeout_secs, default_grace_period_millis, default_grace_period_timeout_secs}; +use crate::utils::file_lock_manager::FileLockManager; +use crate::utils::file_utils; +use crate::utils::{parse_size_base_2, parse_to_kbps}; +use crate::utils::exit; +use crate::m3u_filter_error::{ create_m3u_filter_error_result}; +use crate::model::{ConfigInput, ConfigInputOptions, ConfigIpCheck, ConfigProxy, ConfigSource, ConfigTarget, HdHomeRunConfig, LogConfig, MessagingConfig, ProcessTargets, TargetOutput, VideoConfig, WebUiConfig}; const STREAM_QUEUE_SIZE: usize = 1024; // mpsc channel holding messages. with 8192byte chunks and 2Mbit/s approx 8MB -const RESERVED_PATHS: &[&str] = &[ - "live", "movie", "series", "m3u-stream", "healthcheck", "status", - "player_api.php", "panel_api.php", "xtream", "timeshift", "timeshift.php", "streaming", - "get.php", "apiget", "m3u", "resource" -]; fn generate_secret() -> [u8; 32] { let mut rng = rand::rng(); @@ -68,102 +38,6 @@ macro_rules! valid_property { }}; } pub use valid_property; -use crate::m3u_filter_error::{create_m3u_filter_error, create_m3u_filter_error_result, handle_m3u_filter_error_result, handle_m3u_filter_error_result_list}; -use crate::model::hdhomerun_config::HdHomeRunConfig; -use crate::model::playlist::{PlaylistItemType, XtreamCluster}; -use crate::utils::constants::CONSTANTS; -use crate::utils::file::config_reader::csv_read_inputs; -use crate::utils::network::request::{get_base_url_from_str, get_credentials_from_url, get_credentials_from_url_str}; -use crate::utils::string_utils::get_trimmed_string; - -#[derive(Debug, Copy, Clone, serde::Serialize, serde::Deserialize, Sequence, PartialEq, Eq, Hash)] -pub enum TargetType { - #[serde(rename = "m3u")] - M3u, - #[serde(rename = "xtream")] - Xtream, - #[serde(rename = "strm")] - Strm, - #[serde(rename = "hdhomerun")] - HdHomeRun, -} - -impl TargetType { - const M3U: &'static str = "M3u"; - const XTREAM: &'static str = "Xtream"; - const STRM: &'static str = "Strm"; - const HDHOMERUN: &'static str = "HdHomeRun"; -} - -impl Display for TargetType { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - write!(f, "{}", match *self { - Self::M3u => Self::M3U, - Self::Xtream => Self::XTREAM, - Self::Strm => Self::STRM, - Self::HdHomeRun => Self::HDHOMERUN, - }) - } -} - -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Sequence, PartialEq, Eq, Hash)] -enum HdHomeRunUseTargetType { - #[serde(rename = "m3u")] - M3u, - #[serde(rename = "xtream")] - Xtream, -} - -impl TryFrom for HdHomeRunUseTargetType { - type Error = &'static str; - - fn try_from(value: TargetType) -> Result { - match value { - TargetType::Xtream => Ok(Self::Xtream), - TargetType::M3u => Ok(Self::M3u), - _ => Err("Not allowed!"), - } - } -} - -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Sequence, PartialEq, Eq, Default)] -pub enum ProcessingOrder { - #[serde(rename = "frm")] - #[default] - Frm, - #[serde(rename = "fmr")] - Fmr, - #[serde(rename = "rfm")] - Rfm, - #[serde(rename = "rmf")] - Rmf, - #[serde(rename = "mfr")] - Mfr, - #[serde(rename = "mrf")] - Mrf, -} - -impl ProcessingOrder { - const FRM: &'static str = "filter, rename, map"; - const FMR: &'static str = "filter, map, rename"; - const RFM: &'static str = "rename, filter, map"; - const RMF: &'static str = "rename, map, filter"; - const MFR: &'static str = "map, filter, rename"; - const MRF: &'static str = "map, rename, filter"; -} - -impl Display for ProcessingOrder { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - write!(f, "{}", match *self { - Self::Frm => Self::FRM, - Self::Fmr => Self::FMR, - Self::Rfm => Self::RFM, - Self::Rmf => Self::RMF, - Self::Mfr => Self::MFR, - Self::Mrf => Self::MRF, - }) - } -} #[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Sequence, Eq, PartialEq)] pub enum ItemField { @@ -215,1119 +89,6 @@ pub enum FilterMode { Include, } -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] -pub enum SortOrder { - #[serde(rename = "asc")] - Asc, - #[serde(rename = "desc")] - Desc, -} - -#[derive(Clone, Debug)] -pub struct ProcessTargets { - pub enabled: bool, - pub inputs: Vec, - pub targets: Vec, -} - -impl ProcessTargets { - pub fn has_target(&self, tid: u16) -> bool { - matches!(self.targets.iter().position(|&x| x == tid), Some(_pos)) - } - - pub fn has_input(&self, tid: u16) -> bool { - matches!(self.inputs.iter().position(|&x| x == tid), Some(_pos)) - } -} - -fn compile_regex_vec(patterns: Option<&Vec>) -> Result>, M3uFilterError> { - patterns.as_ref() - .map(|seq| { - seq.iter() - .map(|s| Regex::new(s).map_err(|err| { - create_m3u_filter_error!(M3uFilterErrorKind::Info, "cant parse regex: {} {err}", s) - })) - .collect::, _>>() - }) - .transpose() // convert Option> to Result> -} - -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] -#[serde(deny_unknown_fields)] -pub struct ConfigSortGroup { - pub order: SortOrder, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub sequence: Option>, - #[serde(default, skip)] - pub t_sequence: Option>, -} - - -impl ConfigSortGroup { - pub fn prepare(&mut self) -> Result<(), M3uFilterError> { - // Compile sequence patterns, if any - self.t_sequence = compile_regex_vec(self.sequence.as_ref())?; - Ok(()) - } -} - -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] -#[serde(deny_unknown_fields)] -pub struct ConfigSortChannel { - // channel field - pub field: ItemField, - // match against group title - pub group_pattern: String, - pub order: SortOrder, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub sequence: Option>, - #[serde(default, skip)] - pub t_sequence: Option>, - #[serde(skip)] - pub t_re_group_pattern: Option, -} - -impl ConfigSortChannel { - pub fn prepare(&mut self) -> Result<(), M3uFilterError> { - // Compile group_pattern - self.t_re_group_pattern = Some( - Regex::new(&self.group_pattern).map_err(|err| { - create_m3u_filter_error!(M3uFilterErrorKind::Info, "cant parse regex: {} {err}", &self.group_pattern) - })? - ); - - // Compile sequence patterns, if any - self.t_sequence = compile_regex_vec(self.sequence.as_ref())?; - Ok(()) - } -} - -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Default)] -#[serde(deny_unknown_fields)] -pub struct ConfigSort { - #[serde(default)] - pub match_as_ascii: bool, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub groups: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub channels: Option>, -} - -impl ConfigSort { - pub fn prepare(&mut self) -> Result<(), M3uFilterError> { - if let Some(group) = self.groups.as_mut() { - group.prepare()?; - } - if let Some(channels) = self.channels.as_mut() { - handle_m3u_filter_error_result_list!(M3uFilterErrorKind::Info, channels.iter_mut().map(ConfigSortChannel::prepare)); - } - Ok(()) - } -} - -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] -#[serde(deny_unknown_fields)] -pub struct ConfigRename { - pub field: ItemField, - pub pattern: String, - pub new_name: String, - #[serde(skip_serializing, skip_deserializing)] - pub re: Option, -} - -impl ConfigRename { - pub fn prepare(&mut self) -> Result<(), M3uFilterError> { - match regex::Regex::new(&self.pattern) { - Ok(pattern) => { - self.re = Some(pattern); - Ok(()) - } - Err(err) => create_m3u_filter_error_result!(M3uFilterErrorKind::Info, "cant parse regex: {} {err}", &self.pattern), - } - } -} - -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Default)] -#[serde(deny_unknown_fields)] -pub struct ConfigTargetOptions { - #[serde(default)] - pub ignore_logo: bool, - #[serde(default)] - pub share_live_streams: bool, - #[serde(default)] - pub remove_duplicates: bool, - #[serde(skip_serializing_if = "Option::is_none")] - pub force_redirect: Option, -} - -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] -#[serde(deny_unknown_fields)] -pub struct XtreamTargetOutput { - #[serde(default = "default_as_true")] - pub skip_live_direct_source: bool, - #[serde(default = "default_as_true")] - pub skip_video_direct_source: bool, - #[serde(default = "default_as_true")] - pub skip_series_direct_source: bool, - #[serde(default)] - pub resolve_series: bool, - #[serde(default = "default_resolve_delay_secs")] - pub resolve_series_delay: u16, - #[serde(default)] - pub resolve_vod: bool, - #[serde(default = "default_resolve_delay_secs")] - pub resolve_vod_delay: u16, -} - - -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] -#[serde(deny_unknown_fields)] -pub struct M3uTargetOutput { - #[serde(skip_serializing_if = "Option::is_none")] - pub filename: Option, - #[serde(default)] - pub include_type_in_url: bool, - #[serde(default)] - pub mask_redirect_url: bool, -} - -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] -#[serde(deny_unknown_fields)] -pub struct StrmTargetOutput { - pub directory: String, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub username: Option, - #[serde(default)] - pub underscore_whitespace: bool, - #[serde(default)] - pub cleanup: bool, - #[serde(default)] - pub kodi_style: bool, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub strm_props: Option>, -} - -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] -#[serde(deny_unknown_fields)] -pub struct HdHomeRunTargetOutput { - pub device: String, - pub username: String, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub use_output: Option, -} - -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] -#[serde(deny_unknown_fields, tag = "type", rename_all = "lowercase")] -pub enum TargetOutput { - Xtream(XtreamTargetOutput), - M3u(M3uTargetOutput), - Strm(StrmTargetOutput), - HdHomeRun(HdHomeRunTargetOutput), -} - -bitflags! { - #[derive(Debug, Clone, PartialEq, Eq)] - pub struct ClusterFlags: u16 { - const Live = 1; // 0b0000_0001 - const Vod = 1 << 1; // 0b0000_0010 - const Series = 1 << 2; // 0b0000_0100 - } -} - -impl ClusterFlags { - pub fn has_cluster(&self, item_type: PlaylistItemType) -> bool { - XtreamCluster::try_from(item_type).ok().is_some_and(|cluster| match cluster { - XtreamCluster::Live => self.contains(ClusterFlags::Live), - XtreamCluster::Video => self.contains(ClusterFlags::Vod), - XtreamCluster::Series => self.contains(ClusterFlags::Series), - }) - } - - pub fn has_full_flags(&self) -> bool { - self.is_all() - } - - fn from_items(items: I) -> Result - where - I: IntoIterator, - S: AsRef, - { - let mut result = ClusterFlags::empty(); - - for item in items { - match item.as_ref().trim() { - "live" => result.set(ClusterFlags::Live, true), - "vod" => result.set(ClusterFlags::Vod, true), - "series" => result.set(ClusterFlags::Series, true), - _ => return Err("Invalid flag {item}, allowed are live, vod, series"), - } - } - - Ok(result) - } -} - -impl fmt::Display for ClusterFlags { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let mut flag_strings = Vec::new(); - if self.contains(ClusterFlags::Live) { - flag_strings.push("live"); - } - if self.contains(ClusterFlags::Vod) { - flag_strings.push("vod"); - } - if self.contains(ClusterFlags::Series) { - flag_strings.push("series"); - } - - write!(f, "[{}]", flag_strings.join(",")) - } -} - -impl TryFrom<&str> for ClusterFlags { - type Error = &'static str; - - fn try_from(value: &str) -> Result { - let input = value.trim().trim_matches(['[', ']'].as_ref()); - let items = input.split(',').map(str::trim); - ClusterFlags::from_items(items) - } -} - -impl TryFrom> for ClusterFlags { - type Error = &'static str; - - fn try_from(value: Vec) -> Result { - ClusterFlags::from_items(value) - } -} - -impl Serialize for ClusterFlags { - fn serialize(&self, serializer: S) -> Result - where - S: Serializer, - { - serializer.serialize_some(&self.to_string()) - } -} - -impl<'de> Deserialize<'de> for ClusterFlags { - fn deserialize(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - struct ClusterFlagsVisitor; - - impl<'de> Visitor<'de> for ClusterFlagsVisitor { - type Value = ClusterFlags; - - fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { - formatter.write_str("a string or a map entry like : [vod, live, series]") - } - - fn visit_str(self, v: &str) -> Result - where - E: de::Error, - { - ClusterFlags::try_from(v).map_err(E::custom) - } - - fn visit_seq(self, mut seq: A) -> Result - where - A: SeqAccess<'de>, - { - let mut values = Vec::new(); - while let Some(val) = seq.next_element::()? { - let entry = val.trim().to_lowercase(); - values.push(entry); - } - ClusterFlags::try_from(values).map_err(A::Error::custom) - } - } - deserializer.deserialize_any(ClusterFlagsVisitor) - } -} - -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Default)] -#[serde(deny_unknown_fields)] -pub struct ConfigTarget { - #[serde(skip)] - pub id: u16, - #[serde(default = "default_as_true")] - pub enabled: bool, - #[serde(default = "default_as_default")] - pub name: String, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub options: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub sort: Option, - pub filter: String, - #[serde(default)] - pub output: Vec, - #[serde(skip_serializing_if = "Option::is_none")] - pub rename: Option>, - #[serde(skip_serializing_if = "Option::is_none")] - pub mapping: Option>, - #[serde(default)] - pub processing_order: ProcessingOrder, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub watch: Option>, - #[serde(default, skip_serializing, skip_deserializing)] - pub t_watch_re: Option>, - #[serde(default, skip_serializing, skip_deserializing)] - pub t_filter: Option, - #[serde(default, skip_serializing, skip_deserializing)] - pub t_mapping: Option>, -} - -impl ConfigTarget { - pub fn prepare(&mut self, id: u16, templates: Option<&Vec>) -> Result<(), M3uFilterError> { - self.id = id; - if self.output.is_empty() { - return Err(info_err!(format!("Missing output format for {}", self.name))); - } - let mut m3u_cnt = 0; - let mut strm_cnt = 0; - let mut xtream_cnt = 0; - let mut strm_needs_xtream = false; - let mut hdhr_cnt = 0; - let mut hdhomerun_needs_m3u = false; - let mut hdhomerun_needs_xtream = false; - - for target_output in &mut self.output { - match target_output { - TargetOutput::Xtream(_) => { - xtream_cnt += 1; - if default_as_default().eq_ignore_ascii_case(&self.name) { - return create_m3u_filter_error_result!(M3uFilterErrorKind::Info, "unique target name is required for xtream type output: {}", self.name); - } - } - TargetOutput::M3u(m3u_output) => { - m3u_cnt += 1; - m3u_output.filename = m3u_output.filename.as_ref().map(|s| s.trim().to_string()); - } - TargetOutput::Strm(strm_output) => { - strm_cnt += 1; - strm_output.directory = strm_output.directory.trim().to_string(); - if strm_output.directory.trim().is_empty() { - return create_m3u_filter_error_result!(M3uFilterErrorKind::Info, "directory is required for strm type: {}", self.name); - } - let has_username = if let Some(username) = &strm_output.username { !username.trim().is_empty() } else { false }; - if has_username { - strm_needs_xtream = true; - } - } - TargetOutput::HdHomeRun(hdhomerun_output) => { - hdhr_cnt += 1; - hdhomerun_output.username = hdhomerun_output.username.trim().to_string(); - if hdhomerun_output.username.is_empty() { - return create_m3u_filter_error_result!(M3uFilterErrorKind::Info, "Username is required for HdHomeRun type: {}", self.name); - } - - hdhomerun_output.device = hdhomerun_output.device.trim().to_string(); - if hdhomerun_output.device.is_empty() { - return create_m3u_filter_error_result!(M3uFilterErrorKind::Info, "Device is required for HdHomeRun type: {}", self.name); - } - - if let Some(use_output) = hdhomerun_output.use_output.as_ref() { - match &use_output { - TargetType::M3u => { hdhomerun_needs_m3u = true; } - TargetType::Xtream => { hdhomerun_needs_xtream = true; } - _ => return create_m3u_filter_error_result!(M3uFilterErrorKind::Info, "HdHomeRun output option `use_output` only accepts `m3u` or `xtream` for target: {}", self.name), - } - } - } - } - } - - if m3u_cnt > 1 || strm_cnt > 1 || xtream_cnt > 1 || hdhr_cnt > 1 { - return create_m3u_filter_error_result!(M3uFilterErrorKind::Info, "Multiple output formats with same type : {}", self.name); - } - - if strm_cnt > 0 && strm_needs_xtream && xtream_cnt == 0 { - return create_m3u_filter_error_result!(M3uFilterErrorKind::Info, "strm output with a username is only permitted when used in combination with xtream output: {}", self.name); - } - - if hdhr_cnt > 0 { - if xtream_cnt == 0 && m3u_cnt == 0 { - return create_m3u_filter_error_result!(M3uFilterErrorKind::Info, "HdHomeRun output is only permitted when used in combination with xtream or m3u output: {}", self.name); - } - if hdhomerun_needs_m3u && m3u_cnt == 0 { - return create_m3u_filter_error_result!(M3uFilterErrorKind::Info, "HdHomeRun output has `use_output=m3u` but no `m3u` output defined: {}", self.name); - } - if hdhomerun_needs_xtream && xtream_cnt == 0 { - return create_m3u_filter_error_result!(M3uFilterErrorKind::Info, "HdHomeRun output has `use_output=xtream` but no `xtream` output defined: {}", self.name); - } - } - - if let Some(watch) = &self.watch { - let regexps: Result, _> = watch.iter().map(|s| regex::Regex::new(s)).collect(); - match regexps { - Ok(watch_re) => self.t_watch_re = Some(watch_re), - Err(err) => { - return create_m3u_filter_error_result!(M3uFilterErrorKind::Info, "Invalid watch regular expression: {}", err); - } - } - } - - match get_filter(&self.filter, templates) { - Ok(fltr) => { - // debug!("Filter: {}", fltr); - self.t_filter = Some(fltr); - if let Some(renames) = self.rename.as_mut() { - handle_m3u_filter_error_result_list!(M3uFilterErrorKind::Info, renames.iter_mut().map(ConfigRename::prepare)); - } - if let Some(sort) = self.sort.as_mut() { - handle_m3u_filter_error_result!(M3uFilterErrorKind::Info, sort.prepare()); - } - Ok(()) - } - Err(err) => Err(err), - } - } - - pub fn filter(&self, provider: &ValueProvider) -> bool { - let mut processor = MockValueProcessor {}; - if let Some(filter) = self.t_filter.as_ref() { - return filter.filter(provider, &mut processor); - } - true - } - - pub(crate) fn get_xtream_output(&self) -> Option<&XtreamTargetOutput> { - if let Some(TargetOutput::Xtream(output)) = self.output.iter().find(|o| matches!(o, TargetOutput::Xtream(_))) { - Some(output) - } else { - None - } - } - - pub(crate) fn get_m3u_output(&self) -> Option<&M3uTargetOutput> { - if let Some(TargetOutput::M3u(output)) = self.output.iter().find(|o| matches!(o, TargetOutput::M3u(_))) { - Some(output) - } else { - None - } - } - - // pub(crate) fn get_strm_output(&self) -> Option<&StrmTargetOutput> { - // if let Some(TargetOutput::Strm(output)) = self.output.iter().find(|o| matches!(o, TargetOutput::Strm(_))) { - // Some(output) - // } else { - // None - // } - // } - - pub(crate) fn get_hdhomerun_output(&self) -> Option<&HdHomeRunTargetOutput> { - if let Some(TargetOutput::HdHomeRun(output)) = self.output.iter().find(|o| matches!(o, TargetOutput::HdHomeRun(_))) { - Some(output) - } else { - None - } - } - - pub fn has_output(&self, tt: &TargetType) -> bool { - for target_output in &self.output { - match target_output { - TargetOutput::Xtream(_) => { if tt == &TargetType::Xtream { return true; } } - TargetOutput::M3u(_) => { if tt == &TargetType::M3u { return true; } } - TargetOutput::Strm(_) => { if tt == &TargetType::Strm { return true; } } - TargetOutput::HdHomeRun(_) => { if tt == &TargetType::HdHomeRun { return true; } } - } - } - false - } - - pub fn is_force_redirect(&self, item_type: PlaylistItemType) -> bool { - self.options - .as_ref() - .and_then(|options| options.force_redirect.as_ref()) - .is_some_and(|flags| flags.has_cluster(item_type)) - } -} - -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] -#[serde(deny_unknown_fields)] -pub struct ConfigSource { - pub inputs: Vec, - pub targets: Vec, -} - -impl ConfigSource { - #[allow(clippy::cast_possible_truncation)] - pub fn prepare(&mut self, index: u16, include_computed: bool) -> Result { - handle_m3u_filter_error_result_list!(M3uFilterErrorKind::Info, self.inputs.iter_mut().enumerate().map(|(idx, i)| i.prepare(index+(idx as u16), include_computed))); - Ok(index + (self.inputs.len() as u16)) - } - - pub fn get_inputs_for_target(&self, target_name: &str) -> Option> { - for target in &self.targets { - if target.name.eq(target_name) { - let inputs = self.inputs.iter().filter(|&i| i.enabled).collect::>(); - if !inputs.is_empty() { - return Some(inputs); - } - } - } - None - } -} - -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] -#[serde(deny_unknown_fields)] -pub struct InputAffix { - pub field: String, - pub value: String, -} - -#[derive( - Debug, - Copy, - Clone, - serde::Serialize, - serde::Deserialize, - Sequence, - PartialEq, - Eq, - Default -)] -pub enum InputType { - #[serde(rename = "m3u")] - #[default] - M3u, - #[serde(rename = "xtream")] - Xtream, - #[serde(rename = "m3u_batch")] - M3uBatch, - #[serde(rename = "xtream_batch")] - XtreamBatch, -} - -impl InputType { - const M3U: &'static str = "m3u"; - const XTREAM: &'static str = "xtream"; - const M3U_BATCH: &'static str = "m3u_batch"; - const XTREAM_BATCH: &'static str = "xtream_batch"; -} - -impl Display for InputType { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", match self { - Self::M3u => Self::M3U, - Self::Xtream => Self::XTREAM, - Self::M3uBatch => Self::M3U_BATCH, - Self::XtreamBatch => Self::XTREAM_BATCH, - }) - } -} - -impl FromStr for InputType { - type Err = M3uFilterError; - - fn from_str(s: &str) -> Result { - if s.eq(Self::M3U) { - Ok(Self::M3u) - } else if s.eq(Self::XTREAM) { - Ok(Self::Xtream) - } else { - create_m3u_filter_error_result!(M3uFilterErrorKind::Info, "Unknown InputType: {}", s) - } - } -} - -#[derive( - Debug, - Copy, - Clone, - serde::Serialize, - serde::Deserialize, - Sequence, - PartialEq, - Eq, - Default -)] -pub enum InputFetchMethod { - #[default] - GET, - POST, -} - -impl InputFetchMethod { - const GET_METHOD: &'static str = "GET"; - const POST_METHOD: &'static str = "POST"; -} - -impl Display for InputFetchMethod { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", match self { - Self::GET => Self::GET_METHOD, - Self::POST => Self::POST_METHOD, - }) - } -} - -impl FromStr for InputFetchMethod { - type Err = M3uFilterError; - - fn from_str(s: &str) -> Result { - if s.eq(Self::GET_METHOD) { - Ok(Self::GET) - } else if s.eq(Self::POST_METHOD) { - Ok(Self::POST) - } else { - create_m3u_filter_error_result!(M3uFilterErrorKind::Info, "Unknown Fetch Method: {}", s) - } - } -} - -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Default)] -#[serde(deny_unknown_fields)] -pub struct ConfigInputOptions { - #[serde(default)] - pub xtream_skip_live: bool, - #[serde(default)] - pub xtream_skip_vod: bool, - #[serde(default)] - pub xtream_skip_series: bool, - #[serde(default = "default_as_true")] - pub xtream_live_stream_use_prefix: bool, - #[serde(default)] - pub xtream_live_stream_without_extension: bool, -} - -pub struct InputUserInfo { - pub base_url: String, - pub username: String, - pub password: String, -} - -impl InputUserInfo { - pub fn new(input_type: InputType, username: Option<&str>, password: Option<&str>, input_url: &str) -> Option { - if input_type == InputType::Xtream { - if let (Some(username), Some(password)) = (username, password) { - return Some(Self { - base_url: input_url.to_string(), - username: username.to_owned(), - password: password.to_owned(), - }); - } - } else if let Ok(url) = Url::parse(input_url) { - let base_url = url.origin().ascii_serialization(); - let (username, password) = get_credentials_from_url(&url); - if username.is_some() || password.is_some() { - if let (Some(username), Some(password)) = (username.as_ref(), password.as_ref()) { - return Some(Self { - base_url, - username: username.to_owned(), - password: password.to_owned(), - }); - } - } - } - None - } -} - -macro_rules! check_input_credentials { - ($this:ident, $input_type:expr) => { - match $input_type { - InputType::M3u | InputType::M3uBatch => { - if $this.username.is_some() || $this.password.is_some() { - debug!("for input type m3u: username and password are ignored"); - } - if $this.username.is_none() && $this.password.is_none() { - let (username, password) = get_credentials_from_url_str(&$this.url); - $this.username = username; - $this.password = password; - } - } - InputType::Xtream | InputType::XtreamBatch => { - if $this.username.is_none() || $this.password.is_none() { - return Err(info_err!("for input type xtream: username and password are mandatory".to_string())); - } - } - } - }; -} - -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Default)] -#[serde(deny_unknown_fields)] -pub struct ConfigInputAlias { - #[serde(skip)] - pub id: u16, - pub name: String, - pub url: String, - pub username: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub password: Option, - #[serde(default)] - pub priority: i16, - #[serde(default)] - pub max_connections: u16, - #[serde(skip)] - pub t_base_url: String, -} - - -impl ConfigInputAlias { - pub fn prepare(&mut self, index: u16, input_type: &InputType) -> Result<(), M3uFilterError> { - self.id = index; - self.name = self.name.trim().to_string(); - if self.name.is_empty() { - return Err(info_err!("name for input is mandatory".to_string())); - } - self.url = self.url.trim().to_string(); - if self.url.is_empty() { - return Err(info_err!("url for input is mandatory".to_string())); - } - if let Some(base_url) = get_base_url_from_str(&self.url) { - self.t_base_url = base_url; - } - self.username = get_trimmed_string(&self.username); - self.password = get_trimmed_string(&self.password); - check_input_credentials!(self, input_type); - - Ok(()) - } -} - -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] -#[serde(untagged)] -pub enum EpgUrl { - Single(String), - Multi(Vec), -} - -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq, Default)] -#[serde(rename_all = "lowercase")] -pub enum EpgNamePrefix { - #[default] - Ignore, - Suffix(String), - Prefix(String), -} - -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] -#[serde(deny_unknown_fields)] -pub struct EpgSmartMatchConfig { - #[serde(default)] - pub enabled: bool, - pub normalize_regex: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub strip: Option>, - #[serde(default)] - pub name_prefix: EpgNamePrefix, - #[serde(skip)] - pub name_prefix_separator: Option>, - #[serde(default)] - pub fuzzy_matching: bool, - #[serde(default)] - pub match_threshold: u16, - #[serde(default)] - pub best_match_threshold: u16, - #[serde(skip)] - pub t_strip: Vec, - #[serde(skip)] - pub t_normalize_regex: Option, - #[serde(skip)] - pub t_name_prefix_separator: Vec, - -} - -impl EpgSmartMatchConfig { - /// Creates a new enabled `EpgSmartMatchConfig` with default settings and prepares it. - /// - /// Returns an error if preparation fails. - /// - /// # Examples - /// - /// ``` - /// let config = EpgSmartMatchConfig::new().unwrap(); - /// assert!(config.enabled); - /// ``` - pub fn new() -> Result { - let mut this = Self { enabled: true, ..Self::default() }; - this.prepare()?; - Ok(this) - } - - /// # Panics - /// - /// Prepares the EPG smart match configuration by validating thresholds, compiling normalization regex, and setting default values as needed. - /// - /// Adjusts match thresholds to valid ranges, compiles the normalization regex, and sets default strip values and name prefix separators if not provided. Returns an error if the normalization regex is invalid. - /// - /// # Returns - /// - /// `Ok(())` if preparation succeeds, or an `M3uFilterError` if regex compilation fails. - pub fn prepare(&mut self) -> Result<(), M3uFilterError> { - if !self.enabled { - return Ok(()); - } - - self.t_name_prefix_separator = match &self.name_prefix_separator { - None => vec![':', '|', '-'], - Some(list) => list.clone(), - }; - - if self.match_threshold < 10 { - warn!("match_threshold is less than 10%, setting to 10%"); - self.match_threshold = 10; - } else if self.match_threshold > 100 { - warn!("match_threshold is more than 100%, setting to 80%"); - self.match_threshold = 100; - } - - if self.best_match_threshold == 0 || self.best_match_threshold > 100 || self.best_match_threshold < self.match_threshold { - self.best_match_threshold = 99; - } - - self.t_normalize_regex = match self.normalize_regex.as_ref() { - None => Some(CONSTANTS.re_epg_normalize.clone()), - Some(regstr) => { - let re = regex::Regex::new(regstr.as_str()); - if re.is_err() { - return create_m3u_filter_error_result!(M3uFilterErrorKind::Info, "cant parse regex: {}", regstr); - } - Some(re.unwrap()) - } - }; - - if self.strip.is_none() { - self.t_strip = ["3840p", "uhd", "fhd", "hd", "sd", "4k", "plus", "raw"].iter().map(std::string::ToString::to_string).collect(); - } - Ok(()) - } -} - -impl Default for EpgSmartMatchConfig { - fn default() -> Self { - let mut instance = EpgSmartMatchConfig { - enabled: false, - normalize_regex: None, - strip: None, - name_prefix: EpgNamePrefix::default(), - name_prefix_separator: None, - fuzzy_matching: false, - match_threshold: 0, - best_match_threshold: 0, - t_strip: Vec::default(), - t_normalize_regex: None, - t_name_prefix_separator: Vec::default(), - }; - let _ = instance.prepare(); - instance - } -} - -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] -#[serde(deny_unknown_fields)] -pub struct EpgConfig { - #[serde(default)] - pub auto_epg: bool, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub url: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub smart_match: Option, - #[serde(skip)] - pub t_urls: Vec, - #[serde(skip)] - pub t_smart_match: EpgSmartMatchConfig, -} - -impl EpgConfig { - pub fn prepare(&mut self, include_computed: bool) -> Result<(), M3uFilterError> { - if include_computed { - self.t_urls = self.url.take().map_or_else(Vec::new, |epg_url| { - match epg_url { - EpgUrl::Single(url) => if url.trim().is_empty() { - vec![] - } else { - vec![url.trim().to_string()] - }, - EpgUrl::Multi(urls) => - urls.into_iter() - .map(|url| url.trim().to_string()) - .filter(|s| !s.is_empty()) - .collect() - } - }); - - self.t_smart_match = match self.smart_match.as_mut() { - None => { - let mut normalize: EpgSmartMatchConfig = EpgSmartMatchConfig::default(); - normalize.prepare()?; - normalize - } - Some(normalize_cfg) => { - let mut normalize: EpgSmartMatchConfig = normalize_cfg.clone(); - normalize.prepare()?; - normalize - } - }; - } - Ok(()) - } -} - -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Default)] -#[serde(deny_unknown_fields)] -pub struct ConfigInput { - #[serde(skip)] - pub id: u16, - pub name: String, - #[serde(default, rename = "type")] - pub input_type: InputType, - #[serde(default)] - pub headers: HashMap, - pub url: String, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub epg: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub username: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub password: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub persist: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub prefix: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub suffix: Option, - #[serde(default = "default_as_true")] - pub enabled: bool, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub options: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub aliases: Option>, - #[serde(default)] - pub priority: i16, - #[serde(default)] - pub max_connections: u16, - #[serde(default)] - pub method: InputFetchMethod, - #[serde(skip)] - pub t_base_url: String, -} - -impl ConfigInput { - #[allow(clippy::cast_possible_truncation)] - pub fn prepare(&mut self, index: u16, include_computed: bool) -> Result { - self.id = index; - self.check_url()?; - self.prepare_batch()?; - - self.name = self.name.trim().to_string(); - if self.name.is_empty() { - return Err(info_err!("name for input is mandatory".to_string())); - } - - self.username = get_trimmed_string(&self.username); - self.password = get_trimmed_string(&self.password); - check_input_credentials!(self, self.input_type); - self.persist = get_trimmed_string(&self.persist); - if let Some(base_url) = get_base_url_from_str(&self.url) { - self.t_base_url = base_url; - } - - if let Some(epg) = self.epg.as_mut() { - let _ = epg.prepare(include_computed); - if include_computed && epg.auto_epg { - let (username, password) = if self.username.is_none() || self.password.is_none() { - get_credentials_from_url_str(&self.url) - } else { - (self.username.clone(), self.password.clone()) - }; - - if username.is_none() || password.is_none() { - warn!("auto_epg is enabled for input {}, but no credentials could be extracted", self.name); - } else if !self.t_base_url.is_empty() { - let provider_epg_url = format!("{}/xmltv.php?username={}&password={}", self.t_base_url, username.unwrap_or_default(), password.unwrap_or_default()); - if !epg.t_urls.contains(&provider_epg_url) { - debug!("Added provider epg url {provider_epg_url} for input {}", self.name); - epg.t_urls.push(provider_epg_url); - } - } else { - warn!("auto_epg is enabled for input {}, but url could not be parsed {}", self.name, self.url); - } - } - } - - if let Some(aliases) = self.aliases.as_mut() { - let input_type = &self.input_type; - handle_m3u_filter_error_result_list!(M3uFilterErrorKind::Info, aliases.iter_mut().enumerate().map(|(idx, i)| i.prepare(index+1+(idx as u16), input_type))); - } - Ok(index + self.aliases.as_ref().map_or(0, std::vec::Vec::len) as u16) - } - - fn check_url(&mut self) -> Result<(), M3uFilterError> { - self.url = self.url.trim().to_string(); - if self.url.is_empty() { - return Err(info_err!("url for input is mandatory".to_string())); - } - Ok(()) - } - - fn prepare_batch(&mut self) -> Result<(), M3uFilterError> { - if self.input_type == InputType::M3uBatch || self.input_type == InputType::XtreamBatch { - let input_type = if self.input_type == InputType::M3uBatch { - InputType::M3u - } else { - InputType::Xtream - }; - - match csv_read_inputs(self) { - Ok(mut batch_aliases) => { - if !batch_aliases.is_empty() { - batch_aliases.reverse(); - if let Some(mut first) = batch_aliases.pop() { - self.username = first.username.take(); - self.password = first.password.take(); - self.url = first.url.trim().to_string(); - self.max_connections = first.max_connections; - self.priority = first.priority; - if self.name.is_empty() { - self.name = first.name.to_string(); - } - } - if !batch_aliases.is_empty() { - batch_aliases.reverse(); - if let Some(aliases) = self.aliases.as_mut() { - aliases.extend(batch_aliases); - } else { - self.aliases = Some(batch_aliases); - } - } - } - } - Err(err) => { - return Err(M3uFilterError::new(M3uFilterErrorKind::Info, err.to_string())); - } - } - self.input_type = input_type; - } - Ok(()) - } - - pub fn get_user_info(&self) -> Option { - InputUserInfo::new(self.input_type, self.username.as_deref(), self.password.as_deref(), &self.url) - } - - pub fn get_matched_config_by_url<'a>(&'a self, url: &str) -> Option<(&'a str, Option<&'a String>, Option<&'a String>)> { - if url.contains(&self.t_base_url) { - return Some((&self.t_base_url, self.username.as_ref(), self.password.as_ref())); - } - - if let Some(aliases) = &self.aliases { - for alias in aliases { - if url.contains(&alias.t_base_url) { - return Some((&alias.t_base_url, alias.username.as_ref(), alias.password.as_ref())); - } - } - } - None - } -} - #[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Default)] #[serde(deny_unknown_fields)] pub struct ConfigApi { @@ -1345,116 +106,6 @@ impl ConfigApi { } } -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] -#[serde(deny_unknown_fields)] -pub struct TelegramMessagingConfig { - pub bot_token: String, - pub chat_ids: Vec, -} - -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] -#[serde(deny_unknown_fields)] -pub struct RestMessagingConfig { - pub url: String, -} - -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] -#[serde(deny_unknown_fields)] -pub struct PushoverMessagingConfig { - pub(crate) url: Option, - pub(crate) token: String, - pub(crate) user: String, -} - -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Default)] -#[serde(deny_unknown_fields)] -pub struct MessagingConfig { - #[serde(default)] - pub notify_on: Vec, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub telegram: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub rest: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub pushover: Option, - -} - -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Default)] -#[serde(deny_unknown_fields)] -pub struct LogConfig { - #[serde(default = "default_as_true")] - pub sanitize_sensitive_info: bool, - #[serde(default)] - pub log_active_user: bool, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub log_level: Option, -} - -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Default)] -pub struct LogLevelConfig { - #[serde(default, skip_serializing_if = "Option::is_none")] - pub log: Option, -} - -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Default)] -#[serde(deny_unknown_fields)] -pub struct VideoDownloadConfig { - #[serde(default)] - pub headers: HashMap, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub directory: Option, - #[serde(default)] - pub organize_into_directories: bool, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub episode_pattern: Option, - #[serde(default, skip_serializing, skip_deserializing)] - pub t_re_episode_pattern: Option, -} - -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Default)] -#[serde(deny_unknown_fields)] -pub struct VideoConfig { - #[serde(default)] - pub extensions: Vec, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub download: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub web_search: Option, -} - -impl VideoConfig { - /// # Panics - /// - /// Will panic if default `RegEx` gets invalid - pub fn prepare(&mut self) -> Result<(), M3uFilterError> { - self.extensions = ["mkv", "avi", "mp4", "mpeg", "divx", "mov"].iter().map(|&arg| arg.to_string()).collect(); - match &mut self.download { - None => {} - Some(downl) => { - if downl.headers.is_empty() { - downl.headers.borrow_mut().insert("Accept".to_string(), "video/*".to_string()); - downl.headers.borrow_mut().insert("User-Agent".to_string(), DEFAULT_USER_AGENT.to_string()); - } - - if let Some(episode_pattern) = &downl.episode_pattern { - if !episode_pattern.is_empty() { - match regex::Regex::new(episode_pattern) { - Ok(pattern) => { - downl.t_re_episode_pattern = Some(pattern); - } - Err(err) => { - return create_m3u_filter_error_result!(M3uFilterErrorKind::Info, "cant parse regex: {episode_pattern} {err}"); - } - } - } - } - } - } - Ok(()) - } -} - #[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Default)] #[serde(deny_unknown_fields)] pub struct ConfigDto { @@ -1505,64 +156,6 @@ impl ConfigDto { } } -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] -#[serde(deny_unknown_fields)] -pub struct WebAuthConfig { - #[serde(default = "default_as_true")] - pub enabled: bool, - pub issuer: String, - pub secret: String, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub userfile: Option, - #[serde(skip_serializing, skip_deserializing)] - pub t_users: Option>, -} - -impl WebAuthConfig { - pub fn prepare(&mut self, config_path: &str) -> Result<(), M3uFilterError> { - let userfile_name = self.userfile.as_ref().map_or_else(|| file_utils::get_default_user_file_path(config_path), std::borrow::ToOwned::to_owned); - self.userfile = Some(userfile_name.clone()); - - let mut userfile_path = PathBuf::from(&userfile_name); - if !file_utils::path_exists(&userfile_path) { - userfile_path = PathBuf::from(config_path).join(&userfile_name); - if !file_utils::path_exists(&userfile_path) { - return create_m3u_filter_error_result!(M3uFilterErrorKind::Info, "Could not find userfile {}", &userfile_name); - } - } - - if let Ok(file) = File::open(&userfile_path) { - let mut users = vec![]; - let reader = file_reader(file); - for credentials in reader.lines().map_while(Result::ok) { - let mut parts = credentials.split(':'); - if let (Some(username), Some(password)) = (parts.next(), parts.next()) { - users.push(UserCredential { - username: username.trim().to_string(), - password: password.trim().to_string(), - }); - // debug!("Read ui user {}", username); - } - } - - self.t_users = Some(users); - } else { - return create_m3u_filter_error_result!(M3uFilterErrorKind::Info, "Could not read userfile {:?}", &userfile_path); - } - Ok(()) - } - - pub fn get_user_password(&self, username: &str) -> Option<&str> { - if let Some(users) = &self.t_users { - for credential in users { - if credential.username.eq_ignore_ascii_case(username) { - return Some(credential.password.as_str()); - } - } - } - None - } -} #[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Default)] #[serde(deny_unknown_fields)] @@ -1735,82 +328,6 @@ pub struct CustomStreamResponseConfig { pub provider_connections_exhausted: Option, // provider limit reached, has no more connections } -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Default)] -#[serde(deny_unknown_fields)] -pub struct WebUiConfig { - #[serde(default = "default_as_true")] - pub enabled: bool, - #[serde(default = "default_as_true")] - pub user_ui_enabled: bool, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub path: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub auth: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub player_server: Option, -} - -impl WebUiConfig { - pub fn prepare(&mut self, config_path: &str) -> Result<(), M3uFilterError> { - if !self.enabled { - self.auth = None; - } - - if let Some(web_ui_path) = self.path.as_ref() { - let web_path = web_ui_path.trim(); - if web_path.is_empty() { - self.path = None; - } else { - let web_path = web_path.trim().trim_start_matches('/').trim_end_matches('/').to_string(); - if RESERVED_PATHS.contains(&web_path.to_lowercase().as_str()) { - return Err(M3uFilterError::new(M3uFilterErrorKind::Info, format!("web ui path is a reserved path. Do not use {RESERVED_PATHS:?}"))); - } - self.path = Some(web_path); - } - } - - if let Some(web_auth) = &mut self.auth { - if web_auth.enabled { - web_auth.prepare(config_path)?; - } else { - self.auth = None; - } - } - Ok(()) - } -} - -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Default)] -#[serde(deny_unknown_fields)] -pub struct ConfigProxy { - pub url: String, - pub username: Option, - pub password: Option, -} - -impl ConfigProxy { - fn prepare(&mut self) -> Result<(), M3uFilterError> { - if self.username.is_some() || self.password.is_some() { - if let (Some(username), Some(password)) = (self.username.as_ref(), self.password.as_ref()) { - let uname = username.trim(); - let pwd = password.trim(); - if uname.is_empty() || pwd.is_empty() { - return Err(M3uFilterError::new(M3uFilterErrorKind::Info, "Proxy credentials missing".to_string())); - } - self.username = Some(uname.to_string()); - self.password = Some(pwd.to_string()); - } else { - return Err(M3uFilterError::new(M3uFilterErrorKind::Info, "Proxy credentials missing".to_string())); - } - } - - self.url = self.url.trim().to_string(); - if self.url.is_empty() { - return Err(M3uFilterError::new(M3uFilterErrorKind::Info, "Proxy url missing".to_string())); - } - Ok(()) - } -} #[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Default)] #[serde(deny_unknown_fields)] @@ -1850,6 +367,8 @@ pub struct Config { 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_api_proxy: Arc>>, #[serde(skip)] @@ -2126,6 +645,9 @@ impl Config { if let Some(proxy) = &mut self.proxy { proxy.prepare()?; } + if let Some(ipcheck) = self.ipcheck.as_mut() { + ipcheck.prepare()?; + } self.prepare_hdhomerun()?; self.api.prepare(); self.prepare_api_web_root(); @@ -2308,10 +830,3 @@ pub fn validate_targets(target_args: Option<&Vec>, sources: &Vec, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub directory: Option, + #[serde(default)] + pub organize_into_directories: bool, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub episode_pattern: Option, + #[serde(default, skip_serializing, skip_deserializing)] + pub t_re_episode_pattern: Option, +} + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Default)] +#[serde(deny_unknown_fields)] +pub struct VideoConfig { + #[serde(default)] + pub extensions: Vec, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub download: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub web_search: Option, +} + +impl VideoConfig { + /// # Panics + /// + /// Will panic if default `RegEx` gets invalid + pub fn prepare(&mut self) -> Result<(), M3uFilterError> { + self.extensions = ["mkv", "avi", "mp4", "mpeg", "divx", "mov"].iter().map(|&arg| arg.to_string()).collect(); + match &mut self.download { + None => {} + Some(downl) => { + if downl.headers.is_empty() { + downl.headers.borrow_mut().insert("Accept".to_string(), "video/*".to_string()); + downl.headers.borrow_mut().insert("User-Agent".to_string(), DEFAULT_USER_AGENT.to_string()); + } + + if let Some(episode_pattern) = &downl.episode_pattern { + if !episode_pattern.is_empty() { + match regex::Regex::new(episode_pattern) { + Ok(pattern) => { + downl.t_re_episode_pattern = Some(pattern); + } + Err(err) => { + return create_m3u_filter_error_result!(M3uFilterErrorKind::Info, "cant parse regex: {episode_pattern} {err}"); + } + } + } + } + } + } + Ok(()) + } +} \ No newline at end of file diff --git a/src/model/config_epg.rs b/src/model/config_epg.rs new file mode 100644 index 000000000..ee9aa7d04 --- /dev/null +++ b/src/model/config_epg.rs @@ -0,0 +1,183 @@ +use log::warn; +use regex::Regex; +use crate::m3u_filter_error::{M3uFilterError, M3uFilterErrorKind, create_m3u_filter_error_result}; +use crate::utils::CONSTANTS; + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +#[serde(untagged)] +pub enum EpgUrl { + Single(String), + Multi(Vec), +} + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq, Default)] +#[serde(rename_all = "lowercase")] +pub enum EpgNamePrefix { + #[default] + Ignore, + Suffix(String), + Prefix(String), +} + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +#[serde(deny_unknown_fields)] +pub struct EpgSmartMatchConfig { + #[serde(default)] + pub enabled: bool, + pub normalize_regex: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub strip: Option>, + #[serde(default)] + pub name_prefix: EpgNamePrefix, + #[serde(skip)] + pub name_prefix_separator: Option>, + #[serde(default)] + pub fuzzy_matching: bool, + #[serde(default)] + pub match_threshold: u16, + #[serde(default)] + pub best_match_threshold: u16, + #[serde(skip)] + pub t_strip: Vec, + #[serde(skip)] + pub t_normalize_regex: Option, + #[serde(skip)] + pub t_name_prefix_separator: Vec, + +} + +impl EpgSmartMatchConfig { + /// Creates a new enabled `EpgSmartMatchConfig` with default settings and prepares it. + /// + /// Returns an error if preparation fails. + /// + /// # Examples + /// + /// ``` + /// let config = EpgSmartMatchConfig::new().unwrap(); + /// assert!(config.enabled); + /// ``` + pub fn new() -> Result { + let mut this = Self { enabled: true, ..Self::default() }; + this.prepare()?; + Ok(this) + } + + /// # Panics + /// + /// Prepares the EPG smart match configuration by validating thresholds, compiling normalization regex, and setting default values as needed. + /// + /// Adjusts match thresholds to valid ranges, compiles the normalization regex, and sets default strip values and name prefix separators if not provided. Returns an error if the normalization regex is invalid. + /// + /// # Returns + /// + /// `Ok(())` if preparation succeeds, or an `M3uFilterError` if regex compilation fails. + pub fn prepare(&mut self) -> Result<(), M3uFilterError> { + if !self.enabled { + return Ok(()); + } + + self.t_name_prefix_separator = match &self.name_prefix_separator { + None => vec![':', '|', '-'], + Some(list) => list.clone(), + }; + + if self.match_threshold < 10 { + warn!("match_threshold is less than 10%, setting to 10%"); + self.match_threshold = 10; + } else if self.match_threshold > 100 { + warn!("match_threshold is more than 100%, setting to 80%"); + self.match_threshold = 100; + } + + if self.best_match_threshold == 0 || self.best_match_threshold > 100 || self.best_match_threshold < self.match_threshold { + self.best_match_threshold = 99; + } + + self.t_normalize_regex = match self.normalize_regex.as_ref() { + None => Some(CONSTANTS.re_epg_normalize.clone()), + Some(regstr) => { + let re = regex::Regex::new(regstr.as_str()); + if re.is_err() { + return create_m3u_filter_error_result!(M3uFilterErrorKind::Info, "cant parse regex: {}", regstr); + } + Some(re.unwrap()) + } + }; + + if self.strip.is_none() { + self.t_strip = ["3840p", "uhd", "fhd", "hd", "sd", "4k", "plus", "raw"].iter().map(std::string::ToString::to_string).collect(); + } + Ok(()) + } +} + +impl Default for EpgSmartMatchConfig { + fn default() -> Self { + let mut instance = EpgSmartMatchConfig { + enabled: false, + normalize_regex: None, + strip: None, + name_prefix: EpgNamePrefix::default(), + name_prefix_separator: None, + fuzzy_matching: false, + match_threshold: 0, + best_match_threshold: 0, + t_strip: Vec::default(), + t_normalize_regex: None, + t_name_prefix_separator: Vec::default(), + }; + let _ = instance.prepare(); + instance + } +} + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +#[serde(deny_unknown_fields)] +pub struct EpgConfig { + #[serde(default)] + pub auto_epg: bool, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub url: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub smart_match: Option, + #[serde(skip)] + pub t_urls: Vec, + #[serde(skip)] + pub t_smart_match: EpgSmartMatchConfig, +} + +impl EpgConfig { + pub fn prepare(&mut self, include_computed: bool) -> Result<(), M3uFilterError> { + if include_computed { + self.t_urls = self.url.take().map_or_else(Vec::new, |epg_url| { + match epg_url { + EpgUrl::Single(url) => if url.trim().is_empty() { + vec![] + } else { + vec![url.trim().to_string()] + }, + EpgUrl::Multi(urls) => + urls.into_iter() + .map(|url| url.trim().to_string()) + .filter(|s| !s.is_empty()) + .collect() + } + }); + + self.t_smart_match = match self.smart_match.as_mut() { + None => { + let mut normalize: EpgSmartMatchConfig = EpgSmartMatchConfig::default(); + normalize.prepare()?; + normalize + } + Some(normalize_cfg) => { + let mut normalize: EpgSmartMatchConfig = normalize_cfg.clone(); + normalize.prepare()?; + normalize + } + }; + } + Ok(()) + } +} \ No newline at end of file diff --git a/src/model/config_healthcheck.rs b/src/model/config_healthcheck.rs new file mode 100644 index 000000000..f8a3194e9 --- /dev/null +++ b/src/model/config_healthcheck.rs @@ -0,0 +1,7 @@ +use crate::model::ConfigApi; + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +#[serde(deny_unknown_fields)] +pub struct HealthcheckConfig { + pub api: ConfigApi, +} diff --git a/src/model/config_input.rs b/src/model/config_input.rs new file mode 100644 index 000000000..7bcb973ec --- /dev/null +++ b/src/model/config_input.rs @@ -0,0 +1,389 @@ +use crate::utils::default_as_true; +use std::collections::HashMap; +use std::fmt::Display; +use std::str::FromStr; +use enum_iterator::Sequence; +use log::{debug, warn}; +use url::Url; +use crate::m3u_filter_error::{M3uFilterError, M3uFilterErrorKind, create_m3u_filter_error_result, handle_m3u_filter_error_result_list, info_err}; +use crate::model::{EpgConfig}; +use crate::utils::config_reader::csv_read_inputs; +use crate::utils::request::{get_base_url_from_str, get_credentials_from_url, get_credentials_from_url_str}; +use crate::utils::get_trimmed_string; + +macro_rules! check_input_credentials { + ($this:ident, $input_type:expr) => { + match $input_type { + InputType::M3u | InputType::M3uBatch => { + if $this.username.is_some() || $this.password.is_some() { + debug!("for input type m3u: username and password are ignored"); + } + if $this.username.is_none() && $this.password.is_none() { + let (username, password) = get_credentials_from_url_str(&$this.url); + $this.username = username; + $this.password = password; + } + } + InputType::Xtream | InputType::XtreamBatch => { + if $this.username.is_none() || $this.password.is_none() { + return Err(info_err!("for input type xtream: username and password are mandatory".to_string())); + } + } + } + }; +} + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +#[serde(deny_unknown_fields)] +pub struct InputAffix { + pub field: String, + pub value: String, +} + +#[derive( + Debug, + Copy, + Clone, + serde::Serialize, + serde::Deserialize, + Sequence, + PartialEq, + Eq, + Default +)] +pub enum InputType { + #[serde(rename = "m3u")] + #[default] + M3u, + #[serde(rename = "xtream")] + Xtream, + #[serde(rename = "m3u_batch")] + M3uBatch, + #[serde(rename = "xtream_batch")] + XtreamBatch, +} + +impl InputType { + const M3U: &'static str = "m3u"; + const XTREAM: &'static str = "xtream"; + const M3U_BATCH: &'static str = "m3u_batch"; + const XTREAM_BATCH: &'static str = "xtream_batch"; +} + +impl Display for InputType { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", match self { + Self::M3u => Self::M3U, + Self::Xtream => Self::XTREAM, + Self::M3uBatch => Self::M3U_BATCH, + Self::XtreamBatch => Self::XTREAM_BATCH, + }) + } +} + +impl FromStr for InputType { + type Err = M3uFilterError; + + fn from_str(s: &str) -> Result { + if s.eq(Self::M3U) { + Ok(Self::M3u) + } else if s.eq(Self::XTREAM) { + Ok(Self::Xtream) + } else { + create_m3u_filter_error_result!(M3uFilterErrorKind::Info, "Unknown InputType: {}", s) + } + } +} + +#[derive( + Debug, + Copy, + Clone, + serde::Serialize, + serde::Deserialize, + Sequence, + PartialEq, + Eq, + Default +)] +pub enum InputFetchMethod { + #[default] + GET, + POST, +} + +impl InputFetchMethod { + const GET_METHOD: &'static str = "GET"; + const POST_METHOD: &'static str = "POST"; +} + +impl Display for InputFetchMethod { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", match self { + Self::GET => Self::GET_METHOD, + Self::POST => Self::POST_METHOD, + }) + } +} + +impl FromStr for InputFetchMethod { + type Err = M3uFilterError; + + fn from_str(s: &str) -> Result { + if s.eq(Self::GET_METHOD) { + Ok(Self::GET) + } else if s.eq(Self::POST_METHOD) { + Ok(Self::POST) + } else { + create_m3u_filter_error_result!(M3uFilterErrorKind::Info, "Unknown Fetch Method: {}", s) + } + } +} + +#[allow(clippy::struct_excessive_bools)] +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Default)] +#[serde(deny_unknown_fields)] +pub struct ConfigInputOptions { + #[serde(default)] + pub xtream_skip_live: bool, + #[serde(default)] + pub xtream_skip_vod: bool, + #[serde(default)] + pub xtream_skip_series: bool, + #[serde(default = "default_as_true")] + pub xtream_live_stream_use_prefix: bool, + #[serde(default)] + pub xtream_live_stream_without_extension: bool, +} + +pub struct InputUserInfo { + pub base_url: String, + pub username: String, + pub password: String, +} + +impl InputUserInfo { + pub fn new(input_type: InputType, username: Option<&str>, password: Option<&str>, input_url: &str) -> Option { + if input_type == InputType::Xtream { + if let (Some(username), Some(password)) = (username, password) { + return Some(Self { + base_url: input_url.to_string(), + username: username.to_owned(), + password: password.to_owned(), + }); + } + } else if let Ok(url) = Url::parse(input_url) { + let base_url = url.origin().ascii_serialization(); + let (username, password) = get_credentials_from_url(&url); + if username.is_some() || password.is_some() { + if let (Some(username), Some(password)) = (username.as_ref(), password.as_ref()) { + return Some(Self { + base_url, + username: username.to_owned(), + password: password.to_owned(), + }); + } + } + } + None + } +} + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Default)] +#[serde(deny_unknown_fields)] +pub struct ConfigInputAlias { + #[serde(skip)] + pub id: u16, + pub name: String, + pub url: String, + pub username: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub password: Option, + #[serde(default)] + pub priority: i16, + #[serde(default)] + pub max_connections: u16, + #[serde(skip)] + pub t_base_url: String, +} + + +impl ConfigInputAlias { + pub fn prepare(&mut self, index: u16, input_type: &InputType) -> Result<(), M3uFilterError> { + self.id = index; + self.name = self.name.trim().to_string(); + if self.name.is_empty() { + return Err(info_err!("name for input is mandatory".to_string())); + } + self.url = self.url.trim().to_string(); + if self.url.is_empty() { + return Err(info_err!("url for input is mandatory".to_string())); + } + if let Some(base_url) = get_base_url_from_str(&self.url) { + self.t_base_url = base_url; + } + self.username = get_trimmed_string(&self.username); + self.password = get_trimmed_string(&self.password); + check_input_credentials!(self, input_type); + + Ok(()) + } +} + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Default)] +#[serde(deny_unknown_fields)] +pub struct ConfigInput { + #[serde(skip)] + pub id: u16, + pub name: String, + #[serde(default, rename = "type")] + pub input_type: InputType, + #[serde(default)] + pub headers: HashMap, + pub url: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub epg: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub username: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub password: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub persist: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub prefix: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub suffix: Option, + #[serde(default = "default_as_true")] + pub enabled: bool, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub options: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub aliases: Option>, + #[serde(default)] + pub priority: i16, + #[serde(default)] + pub max_connections: u16, + #[serde(default)] + pub method: InputFetchMethod, + #[serde(skip)] + pub t_base_url: String, +} + +impl ConfigInput { + #[allow(clippy::cast_possible_truncation)] + pub fn prepare(&mut self, index: u16, include_computed: bool) -> Result { + self.id = index; + self.check_url()?; + self.prepare_batch()?; + + self.name = self.name.trim().to_string(); + if self.name.is_empty() { + return Err(info_err!("name for input is mandatory".to_string())); + } + + self.username = get_trimmed_string(&self.username); + self.password = get_trimmed_string(&self.password); + check_input_credentials!(self, self.input_type); + self.persist = get_trimmed_string(&self.persist); + if let Some(base_url) = get_base_url_from_str(&self.url) { + self.t_base_url = base_url; + } + + if let Some(epg) = self.epg.as_mut() { + let _ = epg.prepare(include_computed); + if include_computed && epg.auto_epg { + let (username, password) = if self.username.is_none() || self.password.is_none() { + get_credentials_from_url_str(&self.url) + } else { + (self.username.clone(), self.password.clone()) + }; + + if username.is_none() || password.is_none() { + warn!("auto_epg is enabled for input {}, but no credentials could be extracted", self.name); + } else if !self.t_base_url.is_empty() { + let provider_epg_url = format!("{}/xmltv.php?username={}&password={}", self.t_base_url, username.unwrap_or_default(), password.unwrap_or_default()); + if !epg.t_urls.contains(&provider_epg_url) { + debug!("Added provider epg url {provider_epg_url} for input {}", self.name); + epg.t_urls.push(provider_epg_url); + } + } else { + warn!("auto_epg is enabled for input {}, but url could not be parsed {}", self.name, self.url); + } + } + } + + if let Some(aliases) = self.aliases.as_mut() { + let input_type = &self.input_type; + handle_m3u_filter_error_result_list!(M3uFilterErrorKind::Info, aliases.iter_mut().enumerate().map(|(idx, i)| i.prepare(index+1+(idx as u16), input_type))); + } + Ok(index + self.aliases.as_ref().map_or(0, std::vec::Vec::len) as u16) + } + + fn check_url(&mut self) -> Result<(), M3uFilterError> { + self.url = self.url.trim().to_string(); + if self.url.is_empty() { + return Err(info_err!("url for input is mandatory".to_string())); + } + Ok(()) + } + + fn prepare_batch(&mut self) -> Result<(), M3uFilterError> { + if self.input_type == InputType::M3uBatch || self.input_type == InputType::XtreamBatch { + let input_type = if self.input_type == InputType::M3uBatch { + InputType::M3u + } else { + InputType::Xtream + }; + + match csv_read_inputs(self) { + Ok(mut batch_aliases) => { + if !batch_aliases.is_empty() { + batch_aliases.reverse(); + if let Some(mut first) = batch_aliases.pop() { + self.username = first.username.take(); + self.password = first.password.take(); + self.url = first.url.trim().to_string(); + self.max_connections = first.max_connections; + self.priority = first.priority; + if self.name.is_empty() { + self.name = first.name.to_string(); + } + } + if !batch_aliases.is_empty() { + batch_aliases.reverse(); + if let Some(aliases) = self.aliases.as_mut() { + aliases.extend(batch_aliases); + } else { + self.aliases = Some(batch_aliases); + } + } + } + } + Err(err) => { + return Err(M3uFilterError::new(M3uFilterErrorKind::Info, err.to_string())); + } + } + self.input_type = input_type; + } + Ok(()) + } + + pub fn get_user_info(&self) -> Option { + InputUserInfo::new(self.input_type, self.username.as_deref(), self.password.as_deref(), &self.url) + } + + pub fn get_matched_config_by_url<'a>(&'a self, url: &str) -> Option<(&'a str, Option<&'a String>, Option<&'a String>)> { + if url.contains(&self.t_base_url) { + return Some((&self.t_base_url, self.username.as_ref(), self.password.as_ref())); + } + + if let Some(aliases) = &self.aliases { + for alias in aliases { + if url.contains(&alias.t_base_url) { + return Some((&alias.t_base_url, alias.username.as_ref(), alias.password.as_ref())); + } + } + } + None + } +} diff --git a/src/model/config_ip_check.rs b/src/model/config_ip_check.rs new file mode 100644 index 000000000..ffdeb5ed4 --- /dev/null +++ b/src/model/config_ip_check.rs @@ -0,0 +1,80 @@ +use regex::Regex; +use crate::m3u_filter_error::{M3uFilterError, M3uFilterErrorKind}; + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Default)] +#[serde(deny_unknown_fields)] +pub struct ConfigIpCheck { + /// URL that may return both IPv4 and IPv6 in one response + #[serde(default, skip_serializing_if = "Option::is_none")] + pub url: Option, + + /// Dedicated URL to fetch only IPv4 + #[serde(default, skip_serializing_if = "Option::is_none")] + pub url_ipv4: Option, + + /// Dedicated URL to fetch only IPv6 + #[serde(default, skip_serializing_if = "Option::is_none")] + pub url_ipv6: Option, + + /// Optional regex pattern to extract IPv4 + #[serde(default, skip_serializing_if = "Option::is_none")] + pub pattern_ipv4: Option, + + /// Optional regex pattern to extract IPv6 + #[serde(default, skip_serializing_if = "Option::is_none")] + pub pattern_ipv6: Option, + + #[serde(skip)] + pub t_pattern_ipv4: Option, + #[serde(skip)] + pub t_pattern_ipv6: Option, +} + +impl ConfigIpCheck { + pub fn prepare(&mut self) -> Result<(), M3uFilterError> { + if let Some(url) = &self.url { + let url = url.trim(); + if url.is_empty() { + self.url = None; + } else { + self.url = Some(url.to_owned()); + } + } + if let Some(url) = &self.url_ipv4 { + let url = url.trim(); + if url.is_empty() { + self.url_ipv4 = None; + } else { + self.url_ipv4 = Some(url.to_owned()); + } + } + if let Some(url) = &self.url_ipv6 { + let url = url.trim(); + if url.is_empty() { + self.url_ipv6 = None; + } else { + self.url_ipv6 = Some(url.to_owned()); + } + } + + if self.url.is_none() && self.url_ipv4.is_none() && self.url_ipv6.is_none() { + return Err(M3uFilterError::new(M3uFilterErrorKind::Info, "No url provided!".to_owned())); + } + + if self.url.is_some() && (self.url_ipv4.is_some() || self.url_ipv6.is_some()) { + return Err(M3uFilterError::new(M3uFilterErrorKind::Info, "url in combination with ipv4 and/or ipv6 url not allowed!".to_owned())); + } + + if let Some(p4) = &self.pattern_ipv4 { + self.t_pattern_ipv4 = Some( + Regex::new(p4).map_err(|err| M3uFilterError::new(M3uFilterErrorKind::Info, format!("Invalid IPv4 regex: {p4} {err}")))?, + ); + } + if let Some(p6) = &self.pattern_ipv6 { + self.t_pattern_ipv6 = Some( + Regex::new(p6).map_err(|err| M3uFilterError::new(M3uFilterErrorKind::Info, format!("Invalid IPv6 regex: {p6} {err}")))?, + ); + } + Ok(()) + } +} diff --git a/src/model/config_log.rs b/src/model/config_log.rs new file mode 100644 index 000000000..8860363e3 --- /dev/null +++ b/src/model/config_log.rs @@ -0,0 +1,19 @@ + +use crate::utils::default_as_true; + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Default)] +#[serde(deny_unknown_fields)] +pub struct LogConfig { + #[serde(default = "default_as_true")] + pub sanitize_sensitive_info: bool, + #[serde(default)] + pub log_active_user: bool, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub log_level: Option, +} + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Default)] +pub struct LogLevelConfig { + #[serde(default, skip_serializing_if = "Option::is_none")] + pub log: Option, +} diff --git a/src/model/config_messaging.rs b/src/model/config_messaging.rs new file mode 100644 index 000000000..6f57c7c52 --- /dev/null +++ b/src/model/config_messaging.rs @@ -0,0 +1,36 @@ +use crate::messaging::MsgKind; + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +#[serde(deny_unknown_fields)] +pub struct TelegramMessagingConfig { + pub bot_token: String, + pub chat_ids: Vec, +} + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +#[serde(deny_unknown_fields)] +pub struct RestMessagingConfig { + pub url: String, +} + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +#[serde(deny_unknown_fields)] +pub struct PushoverMessagingConfig { + pub(crate) url: Option, + pub(crate) token: String, + pub(crate) user: String, +} + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Default)] +#[serde(deny_unknown_fields)] +pub struct MessagingConfig { + #[serde(default)] + pub notify_on: Vec, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub telegram: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub rest: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub pushover: Option, + +} \ No newline at end of file diff --git a/src/model/config_proxy.rs b/src/model/config_proxy.rs new file mode 100644 index 000000000..36ea24491 --- /dev/null +++ b/src/model/config_proxy.rs @@ -0,0 +1,33 @@ +use crate::m3u_filter_error::{M3uFilterError, M3uFilterErrorKind}; + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Default)] +#[serde(deny_unknown_fields)] +pub struct ConfigProxy { + pub url: String, + pub username: Option, + pub password: Option, +} + +impl ConfigProxy { + pub(crate) fn prepare(&mut self) -> Result<(), M3uFilterError> { + if self.username.is_some() || self.password.is_some() { + if let (Some(username), Some(password)) = (self.username.as_ref(), self.password.as_ref()) { + let uname = username.trim(); + let pwd = password.trim(); + if uname.is_empty() || pwd.is_empty() { + return Err(M3uFilterError::new(M3uFilterErrorKind::Info, "Proxy credentials missing".to_string())); + } + self.username = Some(uname.to_string()); + self.password = Some(pwd.to_string()); + } else { + return Err(M3uFilterError::new(M3uFilterErrorKind::Info, "Proxy credentials missing".to_string())); + } + } + + self.url = self.url.trim().to_string(); + if self.url.is_empty() { + return Err(M3uFilterError::new(M3uFilterErrorKind::Info, "Proxy url missing".to_string())); + } + Ok(()) + } +} \ No newline at end of file diff --git a/src/model/config_rename.rs b/src/model/config_rename.rs new file mode 100644 index 000000000..b2bbf39f0 --- /dev/null +++ b/src/model/config_rename.rs @@ -0,0 +1,24 @@ +use crate::m3u_filter_error::{M3uFilterError, M3uFilterErrorKind, create_m3u_filter_error_result}; +use crate::model::ItemField; + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +#[serde(deny_unknown_fields)] +pub struct ConfigRename { + pub field: ItemField, + pub pattern: String, + pub new_name: String, + #[serde(skip_serializing, skip_deserializing)] + pub re: Option, +} + +impl ConfigRename { + pub fn prepare(&mut self) -> Result<(), M3uFilterError> { + match regex::Regex::new(&self.pattern) { + Ok(pattern) => { + self.re = Some(pattern); + Ok(()) + } + Err(err) => create_m3u_filter_error_result!(M3uFilterErrorKind::Info, "cant parse regex: {} {err}", &self.pattern), + } + } +} \ No newline at end of file diff --git a/src/model/config_sort.rs b/src/model/config_sort.rs new file mode 100644 index 000000000..206561344 --- /dev/null +++ b/src/model/config_sort.rs @@ -0,0 +1,96 @@ +use regex::Regex; +use crate::m3u_filter_error::{M3uFilterError, M3uFilterErrorKind, create_m3u_filter_error, handle_m3u_filter_error_result_list}; +use crate::model::{ItemField}; + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub enum SortOrder { + #[serde(rename = "asc")] + Asc, + #[serde(rename = "desc")] + Desc, +} + +fn compile_regex_vec(patterns: Option<&Vec>) -> Result>, M3uFilterError> { + patterns.as_ref() + .map(|seq| { + seq.iter() + .map(|s| Regex::new(s).map_err(|err| { + create_m3u_filter_error!(M3uFilterErrorKind::Info, "cant parse regex: {} {err}", s) + })) + .collect::, _>>() + }) + .transpose() // convert Option> to Result> +} + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +#[serde(deny_unknown_fields)] +pub struct ConfigSortGroup { + pub order: SortOrder, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub sequence: Option>, + #[serde(default, skip)] + pub t_sequence: Option>, +} + + +impl ConfigSortGroup { + pub fn prepare(&mut self) -> Result<(), M3uFilterError> { + // Compile sequence patterns, if any + self.t_sequence = compile_regex_vec(self.sequence.as_ref())?; + Ok(()) + } +} + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +#[serde(deny_unknown_fields)] +pub struct ConfigSortChannel { + // channel field + pub field: ItemField, + // match against group title + pub group_pattern: String, + pub order: SortOrder, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub sequence: Option>, + #[serde(default, skip)] + pub t_sequence: Option>, + #[serde(skip)] + pub t_re_group_pattern: Option, +} + +impl ConfigSortChannel { + pub fn prepare(&mut self) -> Result<(), M3uFilterError> { + // Compile group_pattern + self.t_re_group_pattern = Some( + Regex::new(&self.group_pattern).map_err(|err| { + create_m3u_filter_error!(M3uFilterErrorKind::Info, "cant parse regex: {} {err}", &self.group_pattern) + })? + ); + + // Compile sequence patterns, if any + self.t_sequence = compile_regex_vec(self.sequence.as_ref())?; + Ok(()) + } +} + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Default)] +#[serde(deny_unknown_fields)] +pub struct ConfigSort { + #[serde(default)] + pub match_as_ascii: bool, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub groups: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub channels: Option>, +} + +impl ConfigSort { + pub fn prepare(&mut self) -> Result<(), M3uFilterError> { + if let Some(group) = self.groups.as_mut() { + group.prepare()?; + } + if let Some(channels) = self.channels.as_mut() { + handle_m3u_filter_error_result_list!(M3uFilterErrorKind::Info, channels.iter_mut().map(ConfigSortChannel::prepare)); + } + Ok(()) + } +} \ No newline at end of file diff --git a/src/model/config_source.rs b/src/model/config_source.rs new file mode 100644 index 000000000..6d065f350 --- /dev/null +++ b/src/model/config_source.rs @@ -0,0 +1,30 @@ +use crate::m3u_filter_error::{M3uFilterError, M3uFilterErrorKind, handle_m3u_filter_error_result_list}; +use crate::model::ConfigInput; +use crate::model::config_target::ConfigTarget; + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +#[serde(deny_unknown_fields)] +pub struct ConfigSource { + pub inputs: Vec, + pub targets: Vec, +} + +impl ConfigSource { + #[allow(clippy::cast_possible_truncation)] + pub fn prepare(&mut self, index: u16, include_computed: bool) -> Result { + handle_m3u_filter_error_result_list!(M3uFilterErrorKind::Info, self.inputs.iter_mut().enumerate().map(|(idx, i)| i.prepare(index+(idx as u16), include_computed))); + Ok(index + (self.inputs.len() as u16)) + } + + pub fn get_inputs_for_target(&self, target_name: &str) -> Option> { + for target in &self.targets { + if target.name.eq(target_name) { + let inputs = self.inputs.iter().filter(|&i| i.enabled).collect::>(); + if !inputs.is_empty() { + return Some(inputs); + } + } + } + None + } +} \ No newline at end of file diff --git a/src/model/config_target.rs b/src/model/config_target.rs new file mode 100644 index 000000000..56cd5432c --- /dev/null +++ b/src/model/config_target.rs @@ -0,0 +1,356 @@ +use std::fmt::Display; +use enum_iterator::Sequence; +use crate::foundation::filter::{get_filter, Filter, MockValueProcessor, PatternTemplate, ValueProvider}; +use crate::m3u_filter_error::{M3uFilterError, M3uFilterErrorKind, create_m3u_filter_error_result, handle_m3u_filter_error_result, handle_m3u_filter_error_result_list, info_err}; +use crate::model::cluster_flags::ClusterFlags; +use crate::model::config_rename::ConfigRename; +use crate::model::config_sort::ConfigSort; +use crate::model::mapping::Mapping; +use crate::model::PlaylistItemType; +use crate::model::processing_order::ProcessingOrder; +use crate::utils::{default_as_default, default_as_true, default_resolve_delay_secs}; + + +#[derive(Debug, Copy, Clone, serde::Serialize, serde::Deserialize, Sequence, PartialEq, Eq, Hash)] +pub enum TargetType { + #[serde(rename = "m3u")] + M3u, + #[serde(rename = "xtream")] + Xtream, + #[serde(rename = "strm")] + Strm, + #[serde(rename = "hdhomerun")] + HdHomeRun, +} + +impl TargetType { + const M3U: &'static str = "M3u"; + const XTREAM: &'static str = "Xtream"; + const STRM: &'static str = "Strm"; + const HDHOMERUN: &'static str = "HdHomeRun"; +} + +impl Display for TargetType { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(f, "{}", match *self { + Self::M3u => Self::M3U, + Self::Xtream => Self::XTREAM, + Self::Strm => Self::STRM, + Self::HdHomeRun => Self::HDHOMERUN, + }) + } +} + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Sequence, PartialEq, Eq, Hash)] +enum HdHomeRunUseTargetType { + #[serde(rename = "m3u")] + M3u, + #[serde(rename = "xtream")] + Xtream, +} + +impl TryFrom for HdHomeRunUseTargetType { + type Error = &'static str; + + fn try_from(value: TargetType) -> Result { + match value { + TargetType::Xtream => Ok(Self::Xtream), + TargetType::M3u => Ok(Self::M3u), + _ => Err("Not allowed!"), + } + } +} + +#[derive(Clone, Debug)] +pub struct ProcessTargets { + pub enabled: bool, + pub inputs: Vec, + pub targets: Vec, +} + +impl ProcessTargets { + pub fn has_target(&self, tid: u16) -> bool { + matches!(self.targets.iter().position(|&x| x == tid), Some(_pos)) + } + + pub fn has_input(&self, tid: u16) -> bool { + matches!(self.inputs.iter().position(|&x| x == tid), Some(_pos)) + } +} + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Default)] +#[serde(deny_unknown_fields)] +pub struct ConfigTargetOptions { + #[serde(default)] + pub ignore_logo: bool, + #[serde(default)] + pub share_live_streams: bool, + #[serde(default)] + pub remove_duplicates: bool, + #[serde(skip_serializing_if = "Option::is_none")] + pub force_redirect: Option, +} + +#[allow(clippy::struct_excessive_bools)] +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +#[serde(deny_unknown_fields)] +pub struct XtreamTargetOutput { + #[serde(default = "default_as_true")] + pub skip_live_direct_source: bool, + #[serde(default = "default_as_true")] + pub skip_video_direct_source: bool, + #[serde(default = "default_as_true")] + pub skip_series_direct_source: bool, + #[serde(default)] + pub resolve_series: bool, + #[serde(default = "default_resolve_delay_secs")] + pub resolve_series_delay: u16, + #[serde(default)] + pub resolve_vod: bool, + #[serde(default = "default_resolve_delay_secs")] + pub resolve_vod_delay: u16, +} + + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +#[serde(deny_unknown_fields)] +pub struct M3uTargetOutput { + #[serde(skip_serializing_if = "Option::is_none")] + pub filename: Option, + #[serde(default)] + pub include_type_in_url: bool, + #[serde(default)] + pub mask_redirect_url: bool, +} + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +#[serde(deny_unknown_fields)] +pub struct StrmTargetOutput { + pub directory: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub username: Option, + #[serde(default)] + pub underscore_whitespace: bool, + #[serde(default)] + pub cleanup: bool, + #[serde(default)] + pub kodi_style: bool, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub strm_props: Option>, +} + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +#[serde(deny_unknown_fields)] +pub struct HdHomeRunTargetOutput { + pub device: String, + pub username: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub use_output: Option, +} + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +#[serde(deny_unknown_fields, tag = "type", rename_all = "lowercase")] +pub enum TargetOutput { + Xtream(XtreamTargetOutput), + M3u(M3uTargetOutput), + Strm(StrmTargetOutput), + HdHomeRun(HdHomeRunTargetOutput), +} + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Default)] +#[serde(deny_unknown_fields)] +pub struct ConfigTarget { + #[serde(skip)] + pub id: u16, + #[serde(default = "default_as_true")] + pub enabled: bool, + #[serde(default = "default_as_default")] + pub name: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub options: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub sort: Option, + pub filter: String, + #[serde(default)] + pub output: Vec, + #[serde(skip_serializing_if = "Option::is_none")] + pub rename: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub mapping: Option>, + #[serde(default)] + pub processing_order: ProcessingOrder, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub watch: Option>, + #[serde(default, skip_serializing, skip_deserializing)] + pub t_watch_re: Option>, + #[serde(default, skip_serializing, skip_deserializing)] + pub t_filter: Option, + #[serde(default, skip_serializing, skip_deserializing)] + pub t_mapping: Option>, +} + +impl ConfigTarget { + pub fn prepare(&mut self, id: u16, templates: Option<&Vec>) -> Result<(), M3uFilterError> { + self.id = id; + if self.output.is_empty() { + return Err(info_err!(format!("Missing output format for {}", self.name))); + } + let mut m3u_cnt = 0; + let mut strm_cnt = 0; + let mut xtream_cnt = 0; + let mut strm_needs_xtream = false; + let mut hdhr_cnt = 0; + let mut hdhomerun_needs_m3u = false; + let mut hdhomerun_needs_xtream = false; + + for target_output in &mut self.output { + match target_output { + TargetOutput::Xtream(_) => { + xtream_cnt += 1; + if default_as_default().eq_ignore_ascii_case(&self.name) { + return create_m3u_filter_error_result!(M3uFilterErrorKind::Info, "unique target name is required for xtream type output: {}", self.name); + } + } + TargetOutput::M3u(m3u_output) => { + m3u_cnt += 1; + m3u_output.filename = m3u_output.filename.as_ref().map(|s| s.trim().to_string()); + } + TargetOutput::Strm(strm_output) => { + strm_cnt += 1; + strm_output.directory = strm_output.directory.trim().to_string(); + if strm_output.directory.trim().is_empty() { + return create_m3u_filter_error_result!(M3uFilterErrorKind::Info, "directory is required for strm type: {}", self.name); + } + let has_username = if let Some(username) = &strm_output.username { !username.trim().is_empty() } else { false }; + if has_username { + strm_needs_xtream = true; + } + } + TargetOutput::HdHomeRun(hdhomerun_output) => { + hdhr_cnt += 1; + hdhomerun_output.username = hdhomerun_output.username.trim().to_string(); + if hdhomerun_output.username.is_empty() { + return create_m3u_filter_error_result!(M3uFilterErrorKind::Info, "Username is required for HdHomeRun type: {}", self.name); + } + + hdhomerun_output.device = hdhomerun_output.device.trim().to_string(); + if hdhomerun_output.device.is_empty() { + return create_m3u_filter_error_result!(M3uFilterErrorKind::Info, "Device is required for HdHomeRun type: {}", self.name); + } + + if let Some(use_output) = hdhomerun_output.use_output.as_ref() { + match &use_output { + TargetType::M3u => { hdhomerun_needs_m3u = true; } + TargetType::Xtream => { hdhomerun_needs_xtream = true; } + _ => return create_m3u_filter_error_result!(M3uFilterErrorKind::Info, "HdHomeRun output option `use_output` only accepts `m3u` or `xtream` for target: {}", self.name), + } + } + } + } + } + + if m3u_cnt > 1 || strm_cnt > 1 || xtream_cnt > 1 || hdhr_cnt > 1 { + return create_m3u_filter_error_result!(M3uFilterErrorKind::Info, "Multiple output formats with same type : {}", self.name); + } + + if strm_cnt > 0 && strm_needs_xtream && xtream_cnt == 0 { + return create_m3u_filter_error_result!(M3uFilterErrorKind::Info, "strm output with a username is only permitted when used in combination with xtream output: {}", self.name); + } + + if hdhr_cnt > 0 { + if xtream_cnt == 0 && m3u_cnt == 0 { + return create_m3u_filter_error_result!(M3uFilterErrorKind::Info, "HdHomeRun output is only permitted when used in combination with xtream or m3u output: {}", self.name); + } + if hdhomerun_needs_m3u && m3u_cnt == 0 { + return create_m3u_filter_error_result!(M3uFilterErrorKind::Info, "HdHomeRun output has `use_output=m3u` but no `m3u` output defined: {}", self.name); + } + if hdhomerun_needs_xtream && xtream_cnt == 0 { + return create_m3u_filter_error_result!(M3uFilterErrorKind::Info, "HdHomeRun output has `use_output=xtream` but no `xtream` output defined: {}", self.name); + } + } + + if let Some(watch) = &self.watch { + let regexps: Result, _> = watch.iter().map(|s| regex::Regex::new(s)).collect(); + match regexps { + Ok(watch_re) => self.t_watch_re = Some(watch_re), + Err(err) => { + return create_m3u_filter_error_result!(M3uFilterErrorKind::Info, "Invalid watch regular expression: {}", err); + } + } + } + + match get_filter(&self.filter, templates) { + Ok(fltr) => { + // debug!("Filter: {}", fltr); + self.t_filter = Some(fltr); + if let Some(renames) = self.rename.as_mut() { + handle_m3u_filter_error_result_list!(M3uFilterErrorKind::Info, renames.iter_mut().map(ConfigRename::prepare)); + } + if let Some(sort) = self.sort.as_mut() { + handle_m3u_filter_error_result!(M3uFilterErrorKind::Info, sort.prepare()); + } + Ok(()) + } + Err(err) => Err(err), + } + } + + pub fn filter(&self, provider: &ValueProvider) -> bool { + let mut processor = MockValueProcessor {}; + if let Some(filter) = self.t_filter.as_ref() { + return filter.filter(provider, &mut processor); + } + true + } + + pub(crate) fn get_xtream_output(&self) -> Option<&XtreamTargetOutput> { + if let Some(TargetOutput::Xtream(output)) = self.output.iter().find(|o| matches!(o, TargetOutput::Xtream(_))) { + Some(output) + } else { + None + } + } + + pub(crate) fn get_m3u_output(&self) -> Option<&M3uTargetOutput> { + if let Some(TargetOutput::M3u(output)) = self.output.iter().find(|o| matches!(o, TargetOutput::M3u(_))) { + Some(output) + } else { + None + } + } + + // pub(crate) fn get_strm_output(&self) -> Option<&StrmTargetOutput> { + // if let Some(TargetOutput::Strm(output)) = self.output.iter().find(|o| matches!(o, TargetOutput::Strm(_))) { + // Some(output) + // } else { + // None + // } + // } + + pub(crate) fn get_hdhomerun_output(&self) -> Option<&HdHomeRunTargetOutput> { + if let Some(TargetOutput::HdHomeRun(output)) = self.output.iter().find(|o| matches!(o, TargetOutput::HdHomeRun(_))) { + Some(output) + } else { + None + } + } + + pub fn has_output(&self, tt: &TargetType) -> bool { + for target_output in &self.output { + match target_output { + TargetOutput::Xtream(_) => { if tt == &TargetType::Xtream { return true; } } + TargetOutput::M3u(_) => { if tt == &TargetType::M3u { return true; } } + TargetOutput::Strm(_) => { if tt == &TargetType::Strm { return true; } } + TargetOutput::HdHomeRun(_) => { if tt == &TargetType::HdHomeRun { return true; } } + } + } + false + } + + pub fn is_force_redirect(&self, item_type: PlaylistItemType) -> bool { + self.options + .as_ref() + .and_then(|options| options.force_redirect.as_ref()) + .is_some_and(|flags| flags.has_cluster(item_type)) + } +} \ No newline at end of file diff --git a/src/model/config_web_auth.rs b/src/model/config_web_auth.rs new file mode 100644 index 000000000..fa2d800d4 --- /dev/null +++ b/src/model/config_web_auth.rs @@ -0,0 +1,67 @@ +use crate::utils::default_as_true; +use std::fs::File; +use std::io::BufRead; +use std::path::PathBuf; +use crate::auth::user::UserCredential; +use crate::m3u_filter_error::{M3uFilterError, M3uFilterErrorKind, create_m3u_filter_error_result}; +use crate::utils::file_utils; +use crate::utils::file_utils::file_reader; + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +#[serde(deny_unknown_fields)] +pub struct WebAuthConfig { + #[serde(default = "default_as_true")] + pub enabled: bool, + pub issuer: String, + pub secret: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub userfile: Option, + #[serde(skip_serializing, skip_deserializing)] + pub t_users: Option>, +} + +impl WebAuthConfig { + pub fn prepare(&mut self, config_path: &str) -> Result<(), M3uFilterError> { + let userfile_name = self.userfile.as_ref().map_or_else(|| file_utils::get_default_user_file_path(config_path), std::borrow::ToOwned::to_owned); + self.userfile = Some(userfile_name.clone()); + + let mut userfile_path = PathBuf::from(&userfile_name); + if !file_utils::path_exists(&userfile_path) { + userfile_path = PathBuf::from(config_path).join(&userfile_name); + if !file_utils::path_exists(&userfile_path) { + return create_m3u_filter_error_result!(M3uFilterErrorKind::Info, "Could not find userfile {}", &userfile_name); + } + } + + if let Ok(file) = File::open(&userfile_path) { + let mut users = vec![]; + let reader = file_reader(file); + for credentials in reader.lines().map_while(Result::ok) { + let mut parts = credentials.split(':'); + if let (Some(username), Some(password)) = (parts.next(), parts.next()) { + users.push(UserCredential { + username: username.trim().to_string(), + password: password.trim().to_string(), + }); + // debug!("Read ui user {}", username); + } + } + + self.t_users = Some(users); + } else { + return create_m3u_filter_error_result!(M3uFilterErrorKind::Info, "Could not read userfile {:?}", &userfile_path); + } + Ok(()) + } + + pub fn get_user_password(&self, username: &str) -> Option<&str> { + if let Some(users) = &self.t_users { + for credential in users { + if credential.username.eq_ignore_ascii_case(username) { + return Some(credential.password.as_str()); + } + } + } + None + } +} \ No newline at end of file diff --git a/src/model/config_webui.rs b/src/model/config_webui.rs new file mode 100644 index 000000000..dd76549f2 --- /dev/null +++ b/src/model/config_webui.rs @@ -0,0 +1,54 @@ +use crate::utils::default_as_true; +use crate::m3u_filter_error::{M3uFilterError, M3uFilterErrorKind}; +use crate::model::WebAuthConfig; + +const RESERVED_PATHS: &[&str] = &[ + "live", "movie", "series", "m3u-stream", "healthcheck", "status", + "player_api.php", "panel_api.php", "xtream", "timeshift", "timeshift.php", "streaming", + "get.php", "apiget", "m3u", "resource" +]; + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Default)] +#[serde(deny_unknown_fields)] +pub struct WebUiConfig { + #[serde(default = "default_as_true")] + pub enabled: bool, + #[serde(default = "default_as_true")] + pub user_ui_enabled: bool, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub path: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub auth: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub player_server: Option, +} + +impl WebUiConfig { + pub fn prepare(&mut self, config_path: &str) -> Result<(), M3uFilterError> { + if !self.enabled { + self.auth = None; + } + + if let Some(web_ui_path) = self.path.as_ref() { + let web_path = web_ui_path.trim(); + if web_path.is_empty() { + self.path = None; + } else { + let web_path = web_path.trim().trim_start_matches('/').trim_end_matches('/').to_string(); + if RESERVED_PATHS.contains(&web_path.to_lowercase().as_str()) { + return Err(M3uFilterError::new(M3uFilterErrorKind::Info, format!("web ui path is a reserved path. Do not use {RESERVED_PATHS:?}"))); + } + self.path = Some(web_path); + } + } + + if let Some(web_auth) = &mut self.auth { + if web_auth.enabled { + web_auth.prepare(config_path)?; + } else { + self.auth = None; + } + } + Ok(()) + } +} \ No newline at end of file diff --git a/src/model/mapping.rs b/src/model/mapping.rs index b3c619e3b..5916953ad 100644 --- a/src/model/mapping.rs +++ b/src/model/mapping.rs @@ -11,11 +11,22 @@ use std::sync::Arc; use crate::foundation::filter::{apply_templates_to_pattern, get_filter, prepare_templates, Filter, PatternTemplate, RegexWithCaptures, ValueProcessor}; use crate::m3u_filter_error::{create_m3u_filter_error_result, handle_m3u_filter_error_result, info_err}; use crate::m3u_filter_error::{M3uFilterError, M3uFilterErrorKind}; -use crate::model::config::valid_property; -use crate::model::config::{ItemField, AFFIX_FIELDS, COUNTER_FIELDS, MAPPER_ATTRIBUTE_FIELDS}; -use crate::model::playlist::{FieldGetAccessor, FieldSetAccessor, PlaylistItem}; -use crate::utils::constants::CONSTANTS; -use crate::utils::string_utils::Capitalize; +use crate::model::valid_property; +use crate::model::{ItemField}; +use crate::model::{FieldGetAccessor, FieldSetAccessor, PlaylistItem}; +use crate::utils::CONSTANTS; +use crate::utils::Capitalize; + +pub const COUNTER_FIELDS: &[&str] = &["name", "title", "caption", "chno"]; + +pub const AFFIX_FIELDS: &[&str] = &["name", "title", "caption", "group"]; + +pub const MAPPER_ATTRIBUTE_FIELDS: &[&str] = &[ + "name", "title", "caption", "group", "id", "chno", "logo", + "logo_small", "parent_code", "audio_track", + "time_shift", "rec", "url", "epg_channel_id", "epg_id" +]; + #[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Default)] pub struct MappingTag { diff --git a/src/model/mod.rs b/src/model/mod.rs index a65f9db05..e6859dd6f 100644 --- a/src/model/mod.rs +++ b/src/model/mod.rs @@ -1,12 +1,57 @@ -pub mod config; -pub mod playlist; -pub mod mapping; -pub mod api_proxy; -pub mod stats; -pub mod xmltv; -pub mod xtream; -pub mod healthcheck; -pub mod playlist_categories; -pub mod hdhomerun_config; -pub mod xtream_const; -mod serde_utils; \ No newline at end of file +mod config; +mod playlist; +mod mapping; +mod api_proxy; +mod stats; +mod xmltv; +mod xtream; +mod healthcheck; +mod playlist_categories; +mod xtream_const; +mod serde_utils; +mod hdhomerun_config; +mod config_ip_check; +mod config_sort; +mod config_rename; +mod processing_order; +mod cluster_flags; +mod config_target; +mod config_source; +mod config_proxy; +mod config_webui; +mod config_input; +mod config_epg; +mod config_messaging; +mod config_download; +mod config_web_auth; +mod config_log; +mod config_healthcheck; + +pub use self::config::*; +pub use self::playlist::*; +pub use self::mapping::*; +pub use self::api_proxy::*; +pub use self::stats::*; +pub use self::xmltv::*; +pub use self::xtream::*; +pub use self::healthcheck::*; +pub use self::playlist_categories::*; +pub use self::xtream_const::*; +pub use self::config_webui::*; +pub use self::config_ip_check::*; +pub use self::config_sort::*; +pub use self::config_rename::*; +pub use self::processing_order::*; +pub use self::cluster_flags::*; +pub use self::config_target::*; +pub use self::config_source::*; +pub use self::config_proxy::*; +// pub use self::config_webui::*; +pub use self::config_input::*; +pub use self::config_epg::*; +pub use self::hdhomerun_config::*; +pub use self::config_messaging::*; +pub use self::config_download::*; +pub use self::config_web_auth::*; +pub use self::config_log::*; +pub use self::config_healthcheck::*; diff --git a/src/model/playlist.rs b/src/model/playlist.rs index b2c51d8ea..ddd772e24 100644 --- a/src/model/playlist.rs +++ b/src/model/playlist.rs @@ -1,16 +1,16 @@ use std::cmp::PartialEq; use std::fmt::{Display, Formatter}; use std::str::FromStr; -use crate::model::api_proxy::ProxyUserCredentials; -use crate::model::config::{ConfigInput, ConfigTargetOptions}; -use crate::model::xmltv::TVGuide; +use crate::model::ProxyUserCredentials; +use crate::model::{ConfigInput, ConfigTargetOptions}; +use crate::model::TVGuide; use crate::model::xtream_const; use crate::model::xtream::{xtream_playlistitem_to_document, XtreamMappingOptions}; -use crate::utils::json_utils::{get_string_from_serde_value, get_u64_from_serde_value}; +use crate::utils::{get_string_from_serde_value, get_u64_from_serde_value}; use serde::{Deserialize, Serialize}; use serde_json::{Map, Value}; -use crate::utils::hash_utils::{generate_playlist_uuid, get_provider_id}; -use crate::utils::network::request::extract_extension_from_url; +use crate::utils::{generate_playlist_uuid, get_provider_id}; +use crate::utils::request::extract_extension_from_url; // https://de.wikipedia.org/wiki/M3U // https://siptv.eu/howto/playlist.html diff --git a/src/model/processing_order.rs b/src/model/processing_order.rs new file mode 100644 index 000000000..ae66ab98d --- /dev/null +++ b/src/model/processing_order.rs @@ -0,0 +1,41 @@ +use std::fmt::Display; +use enum_iterator::Sequence; + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Sequence, PartialEq, Eq, Default)] +pub enum ProcessingOrder { + #[serde(rename = "frm")] + #[default] + Frm, + #[serde(rename = "fmr")] + Fmr, + #[serde(rename = "rfm")] + Rfm, + #[serde(rename = "rmf")] + Rmf, + #[serde(rename = "mfr")] + Mfr, + #[serde(rename = "mrf")] + Mrf, +} + +impl ProcessingOrder { + const FRM: &'static str = "filter, rename, map"; + const FMR: &'static str = "filter, map, rename"; + const RFM: &'static str = "rename, filter, map"; + const RMF: &'static str = "rename, map, filter"; + const MFR: &'static str = "map, filter, rename"; + const MRF: &'static str = "map, rename, filter"; +} + +impl Display for ProcessingOrder { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(f, "{}", match *self { + Self::Frm => Self::FRM, + Self::Fmr => Self::FMR, + Self::Rfm => Self::RFM, + Self::Rmf => Self::RMF, + Self::Mfr => Self::MFR, + Self::Mrf => Self::MRF, + }) + } +} \ No newline at end of file diff --git a/src/model/stats.rs b/src/model/stats.rs index 07504a2b3..e91a69134 100644 --- a/src/model/stats.rs +++ b/src/model/stats.rs @@ -1,6 +1,6 @@ use std::fmt::{Display}; use serde::{Serialize, Serializer}; -use crate::model::config::InputType; +use crate::model::InputType; pub fn format_elapsed_time(seconds: u64) -> String { if seconds < 60 { diff --git a/src/model/xtream.rs b/src/model/xtream.rs index 35b0cd3e1..c8734c5b5 100644 --- a/src/model/xtream.rs +++ b/src/model/xtream.rs @@ -1,9 +1,9 @@ -use crate::model::api_proxy::{ProxyUserCredentials}; -use crate::model::config::{Config, ConfigTarget, ClusterFlags, XtreamTargetOutput}; -use crate::model::playlist::{PlaylistEntry, PlaylistItem, XtreamCluster, XtreamPlaylistItem}; +use crate::model::{ProxyUserCredentials}; +use crate::model::{Config, ConfigTarget, ClusterFlags, XtreamTargetOutput}; +use crate::model::{PlaylistEntry, PlaylistItem, XtreamCluster, XtreamPlaylistItem}; use crate::model::serde_utils::{deserialize_as_option_rc_string, deserialize_as_rc_string, deserialize_as_string_array, deserialize_number_from_string}; use crate::model::xtream_const; -use crate::utils::json_utils::{opt_string_or_number_u32, string_default_on_null, string_or_number_f64, string_or_number_u32}; +use crate::utils::{opt_string_or_number_u32, string_default_on_null, string_or_number_f64, string_or_number_u32}; use serde::{Deserialize, Serialize}; use serde_json::{Map, Value}; use std::collections::HashMap; diff --git a/src/processing/parser/hls.rs b/src/processing/parser/hls.rs index 626bc48e6..314ebabad 100644 --- a/src/processing/parser/hls.rs +++ b/src/processing/parser/hls.rs @@ -1,11 +1,10 @@ -use crate::model::api_proxy::ProxyUserCredentials; -use crate::utils::constants::{CONSTANTS, HLS_PREFIX}; -use crate::utils::crypto_utils::{deobfuscate_text, obfuscate_text}; -use crate::utils::hash_utils; +use crate::model::ProxyUserCredentials; +use crate::utils::{CONSTANTS, HLS_PREFIX}; +use crate::utils::{deobfuscate_text, obfuscate_text, u32_to_base64, base64_to_u32}; use std::str; fn create_hls_session_token_and_url(secret: &[u8], session_token: u32, stream_url: &str) -> Option { - let token = hash_utils::u32_to_base64(session_token); + let token = u32_to_base64(session_token); if let Ok(cookie_value) = obfuscate_text(secret, &format!("{token}{stream_url}")) { return Some(cookie_value); } @@ -17,7 +16,7 @@ pub fn get_hls_session_token_and_url_from_token(secret: &[u8], token: &str) -> O if let Ok(decrypted) = deobfuscate_text(secret, token) { let session_token: String = decrypted.chars().take(TOKEN_LEN).collect(); let stream_url: String = decrypted.chars().skip(TOKEN_LEN).collect(); - return Some((hash_utils::base64_to_u32(&session_token), stream_url)); + return Some((base64_to_u32(&session_token), stream_url)); } None } @@ -102,13 +101,13 @@ pub fn rewrite_hls(user: &ProxyUserCredentials, props: &RewriteHlsProps) -> Stri #[cfg(test)] mod test { use rand::RngCore; - use crate::utils::hash_utils; + use crate::utils::u32_to_base64; #[test] fn test_token_size() { for _i in 0..10_000 { let session_token = rand::rng().next_u32(); - assert_eq!(hash_utils::u32_to_base64(session_token).len(), 6); + assert_eq!(u32_to_base64(session_token).len(), 6); } } diff --git a/src/processing/parser/m3u.rs b/src/processing/parser/m3u.rs index 1e5fd401c..992dbd69e 100644 --- a/src/processing/parser/m3u.rs +++ b/src/processing/parser/m3u.rs @@ -1,8 +1,7 @@ use std::borrow::BorrowMut; -use crate::model::config::{Config, ConfigInput}; -use crate::model::playlist::{PlaylistGroup, PlaylistItem, PlaylistItemHeader, PlaylistItemType, XtreamCluster}; -use crate::utils::hash_utils::extract_id_from_url; -use crate::utils::string_utils; +use crate::model::{Config, ConfigInput}; +use crate::model::{PlaylistGroup, PlaylistItem, PlaylistItemHeader, PlaylistItemType, XtreamCluster}; +use crate::utils::{extract_id_from_url, get_title_group}; #[inline] fn token_value(stack: &mut String, it: &mut std::str::Chars) -> String { @@ -190,7 +189,7 @@ where header.group = group_value; } else { let current_title = header.title.clone(); - header.group = string_utils::get_title_group(current_title.as_str()); + header.group = get_title_group(current_title.as_str()); } } visit(item); diff --git a/src/processing/parser/xmltv.rs b/src/processing/parser/xmltv.rs index c3c472986..d90c7373c 100644 --- a/src/processing/parser/xmltv.rs +++ b/src/processing/parser/xmltv.rs @@ -1,8 +1,8 @@ -use crate::model::config::{EpgNamePrefix, EpgSmartMatchConfig}; -use crate::model::xmltv::{Epg, TVGuide, XmlTag, EPG_ATTRIB_CHANNEL, EPG_ATTRIB_ID, EPG_TAG_CHANNEL, EPG_TAG_DISPLAY_NAME, EPG_TAG_ICON, EPG_TAG_PROGRAMME, EPG_TAG_TV}; +use crate::model::{EpgNamePrefix, EpgSmartMatchConfig}; +use crate::model::{Epg, TVGuide, XmlTag, EPG_ATTRIB_CHANNEL, EPG_ATTRIB_ID, EPG_TAG_CHANNEL, EPG_TAG_DISPLAY_NAME, EPG_TAG_ICON, EPG_TAG_PROGRAMME, EPG_TAG_TV}; use crate::processing::processor::epg::EpgIdCache; -use crate::utils::compression::compressed_file_reader::CompressedFileReader; -use crate::utils::constants::CONSTANTS; +use crate::utils::compressed_file_reader::CompressedFileReader; +use crate::utils::CONSTANTS; use deunicode::deunicode; use quick_xml::events::{BytesStart, BytesText, Event}; use quick_xml::Reader; @@ -439,7 +439,7 @@ pub fn flatten_tvguide(tv_guides: &[Epg]) -> Option { #[cfg(test)] mod tests { - use crate::model::config::{EpgNamePrefix, EpgSmartMatchConfig}; + use crate::model::{EpgNamePrefix, EpgSmartMatchConfig}; use crate::processing::parser::xmltv::normalize_channel_name; #[test] diff --git a/src/processing/parser/xtream.rs b/src/processing/parser/xtream.rs index 61512f88c..bd3bf9451 100644 --- a/src/processing/parser/xtream.rs +++ b/src/processing/parser/xtream.rs @@ -1,13 +1,12 @@ use crate::m3u_filter_error::{create_m3u_filter_error_result, M3uFilterError, M3uFilterErrorKind}; -use crate::model::config::ConfigInput; -use crate::model::playlist::{PlaylistGroup, PlaylistItem, PlaylistItemHeader, PlaylistItemType, XtreamCluster}; -use crate::model::xtream::{XtreamCategory, XtreamSeriesInfo, XtreamSeriesInfoEpisode, XtreamStream}; -use crate::utils::hash_utils::generate_playlist_uuid; -use crate::utils::network::xtream::{get_xtream_stream_url_base}; +use crate::model::ConfigInput; +use crate::model::{PlaylistGroup, PlaylistItem, PlaylistItemHeader, PlaylistItemType, XtreamCluster}; +use crate::model::{XtreamCategory, XtreamSeriesInfo, XtreamSeriesInfoEpisode, XtreamStream}; +use crate::utils::generate_playlist_uuid; +use crate::utils::xtream::{get_xtream_stream_url_base}; use serde_json::Value; use std::collections::HashMap; use std::sync::Arc; -use crate::model::xtream_const; fn map_to_xtream_category(categories: &Value) -> Result, M3uFilterError> { match serde_json::from_value::>(categories.to_owned()) { @@ -92,7 +91,7 @@ pub fn get_xtream_url(xtream_cluster: XtreamCluster, url: &str, format!("{url}/movie/{username}/{password}/{stream_id}.{ext}") } XtreamCluster::Series => - format!("{}&action={}&series_id={stream_id}", get_xtream_stream_url_base(url, username, password), xtream_const::XC_ACTION_GET_SERIES_INFO) + format!("{}&action={}&series_id={stream_id}", get_xtream_stream_url_base(url, username, password), crate::model::XC_ACTION_GET_SERIES_INFO) }; stream_base_url } diff --git a/src/processing/playlist_watch.rs b/src/processing/playlist_watch.rs index 5f0c30bac..741a6e44e 100644 --- a/src/processing/playlist_watch.rs +++ b/src/processing/playlist_watch.rs @@ -3,11 +3,11 @@ use std::path::{Path}; use std::sync::Arc; use log::{error, info}; use crate::messaging::{MsgKind, send_message}; -use crate::model::config::Config; -use crate::model::playlist::PlaylistGroup; -use crate::utils::bincode_utils::{bincode_deserialize, bincode_serialize}; -use crate::utils::file::file_utils; -use crate::utils::file::file_utils::sanitize_filename; +use crate::model::Config; +use crate::model::PlaylistGroup; +use crate::utils::{bincode_deserialize, bincode_serialize}; +use crate::utils::file_utils; +use crate::utils::file_utils::sanitize_filename; pub fn process_group_watch(client: &Arc, cfg: &Config, target_name: &str, pl: &PlaylistGroup) { let mut new_tree = BTreeSet::new(); diff --git a/src/processing/processor/affix.rs b/src/processing/processor/affix.rs index 867718863..64f3661ef 100644 --- a/src/processing/processor/affix.rs +++ b/src/processing/processor/affix.rs @@ -1,5 +1,5 @@ -use crate::model::config::{ConfigInput, InputAffix, AFFIX_FIELDS, valid_property}; -use crate::model::playlist::{FetchedPlaylist, FieldGetAccessor, FieldSetAccessor, PlaylistItem}; +use crate::model::{ConfigInput, InputAffix, AFFIX_FIELDS, valid_property}; +use crate::model::{FetchedPlaylist, FieldGetAccessor, FieldSetAccessor, PlaylistItem}; use crate::utils::{debug_if_enabled}; type AffixProcessor<'a> = Box; diff --git a/src/processing/processor/epg.rs b/src/processing/processor/epg.rs index 7c4625270..77bb6728d 100644 --- a/src/processing/processor/epg.rs +++ b/src/processing/processor/epg.rs @@ -1,6 +1,6 @@ -use crate::model::config::{EpgConfig, EpgSmartMatchConfig}; -use crate::model::playlist::{FetchedPlaylist, PlaylistItem, XtreamCluster}; -use crate::model::xmltv::{Epg, XmlTag, EPG_ATTRIB_ID}; +use crate::model::{EpgConfig, EpgSmartMatchConfig}; +use crate::model::{FetchedPlaylist, PlaylistItem, XtreamCluster}; +use crate::model::{Epg, XmlTag, EPG_ATTRIB_ID}; use crate::processing::parser::xmltv::normalize_channel_name; use log::debug; use rphonetic::{DoubleMetaphone, Encoder}; diff --git a/src/processing/processor/mod.rs b/src/processing/processor/mod.rs index ff4018e7b..37f41c85b 100644 --- a/src/processing/processor/mod.rs +++ b/src/processing/processor/mod.rs @@ -4,6 +4,7 @@ mod affix; mod xtream_vod; mod xtream_series; pub mod epg; +mod sort; #[macro_export] macro_rules! handle_error { diff --git a/src/processing/processor/playlist.rs b/src/processing/processor/playlist.rs index 841ad0c1f..bf34cd814 100644 --- a/src/processing/processor/playlist.rs +++ b/src/processing/processor/playlist.rs @@ -1,9 +1,8 @@ -use crate::model::config::{ConfigInput, ConfigRename, SortOrder}; -use crate::utils::network::epg; -use crate::utils::network::m3u; -use crate::utils::network::xtream; +use crate::model::{ConfigInput, ConfigRename}; +use crate::utils::epg; +use crate::utils::m3u; +use crate::utils::xtream; use crate::Config; -use core::cmp::Ordering; use std::collections::{HashMap, HashSet}; use std::path::PathBuf; use std::sync::Arc; @@ -13,26 +12,26 @@ use tokio::sync::Mutex; use crate::foundation::filter::{get_field_value, set_field_value, MockValueProcessor, ValueProvider}; use crate::m3u_filter_error::{get_errors_notify_message, notify_err, M3uFilterError, M3uFilterErrorKind}; use crate::messaging::{send_message, MsgKind}; -use crate::model::config::{ConfigSortChannel, ConfigSortGroup, ConfigTarget, InputType, - ItemField, ProcessTargets, ProcessingOrder}; -use crate::model::mapping::{CounterModifier, Mapping, MappingValueProcessor}; -use crate::model::playlist::{FetchedPlaylist, FieldGetAccessor, FieldSetAccessor, PlaylistEntry, PlaylistGroup, PlaylistItem, UUIDType, XtreamCluster}; -use crate::model::stats::{InputStats, PlaylistStats, SourceStats, TargetStats}; +use crate::model::{ConfigTarget, InputType, ItemField, ProcessTargets, ProcessingOrder}; +use crate::model::{CounterModifier, Mapping, MappingValueProcessor}; +use crate::model::{FetchedPlaylist, FieldGetAccessor, FieldSetAccessor, PlaylistEntry, PlaylistGroup, PlaylistItem, UUIDType, XtreamCluster}; +use crate::model::{InputStats, PlaylistStats, SourceStats, TargetStats}; use crate::processing::playlist_watch::process_group_watch; use crate::processing::processor::affix::apply_affixes; use crate::processing::processor::xtream_series::playlist_resolve_series; use crate::processing::processor::xtream_vod::playlist_resolve_vod; use crate::repository::playlist_repository::persist_playlist; -use crate::utils::default_utils::default_as_default; +use crate::utils::default_as_default; use crate::utils::debug_if_enabled; use deunicode::deunicode; use log::{debug, error, info, log_enabled, trace, warn, Level}; use std::time::Instant; -use crate::model::xmltv::Epg; +use crate::model::Epg; use crate::processing::parser::xmltv::flatten_tvguide; use crate::processing::processor::epg::process_playlist_epg; -use crate::utils::step_measure::StepMeasure; +use crate::processing::processor::sort::sort_playlist; +use crate::utils::StepMeasure; fn is_valid(pli: &PlaylistItem, target: &ConfigTarget) -> bool { let provider = ValueProvider { pli }; @@ -59,138 +58,8 @@ fn filter_playlist(playlist: &mut [PlaylistGroup], target: &ConfigTarget) -> Opt Some(new_playlist) } -fn playlist_comparator( - sequence: Option<&Vec>, - order: &SortOrder, - value_a: &str, - value_b: &str, -) -> Ordering { - if let Some(regex_list) = sequence { - let mut match_a = None; - let mut match_b = None; - for (i, regex) in regex_list.iter().enumerate() { - if match_a.is_none() { - if let Some(caps) = regex.captures(value_a) { - match_a = Some((i, caps)); - } - } - if match_b.is_none() { - if let Some(caps) = regex.captures(value_b) { - match_b = Some((i, caps)); - } - } - - // If both matches found → break - if match_a.is_some() && match_b.is_some() { - break; - } - } - - match (match_a, match_b) { - (Some((idx_a, caps_a)), Some((idx_b, caps_b))) => { - // Different regex indices → sort by their sequence order. - if idx_a != idx_b { - return match order { - SortOrder::Asc => idx_a.cmp(&idx_b), - SortOrder::Desc => idx_b.cmp(&idx_a), - }; - } - - // Same regex → sort by captures (c1, c2, …) - let mut named: Vec<_> = regex_list[idx_a] - .capture_names() - .flatten() - .filter(|name| name.starts_with('c')) - .collect(); - - named.sort_by_key(|name| name[1..].parse::().unwrap_or(0)); - - for name in named { - let va = caps_a.name(name).map(|m| m.as_str()); - let vb = caps_b.name(name).map(|m| m.as_str()); - if let (Some(va), Some(vb)) = (va, vb) { - let o = va.cmp(vb); - if o != Ordering::Equal { - return match order { - SortOrder::Asc => o, - SortOrder::Desc => o.reverse(), - }; - } - } - } - - Ordering::Equal - } - (Some(_), None) => match order { - SortOrder::Asc => Ordering::Less, - SortOrder::Desc => Ordering::Greater, - }, - (None, Some(_)) => match order { - SortOrder::Asc => Ordering::Greater, - SortOrder::Desc => Ordering::Less, - }, - (None, None) => { - // NP match → fallback - let o = value_a.cmp(value_b); - match order { - SortOrder::Asc => o, - SortOrder::Desc => o.reverse(), - } - } - } - } else { - // No Regex-Sequence defined → fallback - let o = value_a.cmp(value_b); - match order { - SortOrder::Asc => o, - SortOrder::Desc => o.reverse(), - } - } -} - -fn playlistgroup_comparator(a: &PlaylistGroup, b: &PlaylistGroup, group_sort: &ConfigSortGroup, match_as_ascii: bool) -> Ordering { - let value_a = if match_as_ascii { deunicode(&a.title) } else { a.title.to_string() }; - let value_b = if match_as_ascii { deunicode(&b.title) } else { b.title.to_string() }; - - playlist_comparator(group_sort.t_sequence.as_ref(), &group_sort.order, &value_a, &value_b) -} - -fn playlistitem_comparator( - a: &PlaylistItem, - b: &PlaylistItem, - channel_sort: &ConfigSortChannel, - match_as_ascii: bool, -) -> Ordering { - let raw_value_a = get_field_value(a, &channel_sort.field); - let raw_value_b = get_field_value(b, &channel_sort.field); - let value_a = if match_as_ascii { deunicode(&raw_value_a) } else { raw_value_a }; - let value_b = if match_as_ascii { deunicode(&raw_value_b) } else { raw_value_b }; - - playlist_comparator(channel_sort.t_sequence.as_ref(), &channel_sort.order, &value_a, &value_b) -} - -fn sort_playlist(target: &ConfigTarget, new_playlist: &mut [PlaylistGroup]) { - if let Some(sort) = &target.sort { - let match_as_ascii = sort.match_as_ascii; - if let Some(group_sort) = &sort.groups { - new_playlist.sort_by(|a, b| playlistgroup_comparator(a, b, group_sort, match_as_ascii)); - } - if let Some(channel_sorts) = &sort.channels { - for channel_sort in channel_sorts { - let regexp = channel_sort.t_re_group_pattern.as_ref().unwrap(); - for group in new_playlist.iter_mut() { - let group_title = if match_as_ascii { deunicode(&group.title) } else { group.title.to_string() }; - if regexp.is_match(group_title.as_str()) { - group.channels.sort_by(|chan1, chan2| playlistitem_comparator(chan1, chan2, channel_sort, match_as_ascii)); - } - } - } - } - } -} - -fn channel_no_playlist(new_playlist: &mut [PlaylistGroup]) { +fn assign_channel_no_playlist(new_playlist: &mut [PlaylistGroup]) { let assigned_chnos: HashSet = new_playlist.iter().flat_map(|g| &g.channels) .filter(|c| !c.header.chno.is_empty()) .map(|c| c.header.chno.as_str()) @@ -618,7 +487,7 @@ async fn process_playlist_for_target(client: Arc, step.tick("Sorted playlists"); sort_playlist(target, &mut flat_new_playlist); step.tick("Assigned channel number"); - channel_no_playlist(&mut flat_new_playlist); + assign_channel_no_playlist(&mut flat_new_playlist); step.tick("Assigned channel counter"); map_playlist_counter(target, &mut flat_new_playlist); step.tick("Processed group watches"); @@ -696,34 +565,4 @@ mod tests { // // println!("sorensen dice {:?}", strsim::sorensen_dice(data.0, data.1)); // } - use crate::model::config::{ConfigSortChannel, ItemField, SortOrder}; - use crate::model::playlist::{PlaylistItem, PlaylistItemHeader}; - use crate::processing::processor::playlist::playlistitem_comparator; - use regex::Regex; - - #[test] - fn test_sort() { - let mut channels: Vec = vec![ - ("D", "HD"), ("A", "FHD"), ("Z", "HD"), ("K", "HD"), ("B", "HD"), ("A", "HD"), - ("K", "UHD"), ("C", "HD"), ("L", "FHD"), ("R", "UHD"), ("T", "SD"), ("A", "FHD"), - ].into_iter().map(|(name, quality)| PlaylistItem { header: PlaylistItemHeader { title: format!("Chanel {name} [{quality}]"), ..Default::default() } }).collect::>().into(); - - let channel_sort = ConfigSortChannel { - field: ItemField::Caption, - group_pattern: ".*".to_string(), - order: SortOrder::Asc, - sequence: None, - t_sequence: Some(vec![ - Regex::new(r"(?P.*?)\bUHD\b").unwrap(), - Regex::new(r"(?P.*?)\bFHD\b").unwrap(), - Regex::new(r"(?P.*?)\bHD\b").unwrap(), - ]), - t_re_group_pattern: Some(Regex::new(".*").unwrap()), - }; - - channels.sort_by(|chan1, chan2| playlistitem_comparator(chan1, chan2, &channel_sort, true)); - let expected = vec!["Chanel K [UHD]", "Chanel R [UHD]", "Chanel A [FHD]", "Chanel A [FHD]", "Chanel L [FHD]", "Chanel A [HD]", "Chanel B [HD]", "Chanel C [HD]", "Chanel D [HD]", "Chanel K [HD]", "Chanel Z [HD]", "Chanel T [SD]"]; - let sorted = channels.into_iter().map(|pli| pli.header.title.clone()).collect::>(); - assert_eq!(expected, sorted); - } } \ No newline at end of file diff --git a/src/processing/processor/sort.rs b/src/processing/processor/sort.rs new file mode 100644 index 000000000..deb5ba01b --- /dev/null +++ b/src/processing/processor/sort.rs @@ -0,0 +1,171 @@ +use std::cmp::Ordering; +use deunicode::deunicode; +use crate::foundation::filter::get_field_value; +use crate::model::{ConfigSortChannel, ConfigSortGroup, ConfigTarget, SortOrder}; +use crate::model::{PlaylistGroup, PlaylistItem}; + +fn playlist_comparator( + sequence: Option<&Vec>, + order: &SortOrder, + value_a: &str, + value_b: &str, +) -> Ordering { + if let Some(regex_list) = sequence { + let mut match_a = None; + let mut match_b = None; + + for (i, regex) in regex_list.iter().enumerate() { + if match_a.is_none() { + if let Some(caps) = regex.captures(value_a) { + match_a = Some((i, caps)); + } + } + if match_b.is_none() { + if let Some(caps) = regex.captures(value_b) { + match_b = Some((i, caps)); + } + } + + // If both matches found → break + if match_a.is_some() && match_b.is_some() { + break; + } + } + + match (match_a, match_b) { + (Some((idx_a, caps_a)), Some((idx_b, caps_b))) => { + // Different regex indices → sort by their sequence order. + if idx_a != idx_b { + return match order { + SortOrder::Asc => idx_a.cmp(&idx_b), + SortOrder::Desc => idx_b.cmp(&idx_a), + }; + } + + // Same regex → sort by captures (c1, c2, …) + let mut named: Vec<_> = regex_list[idx_a] + .capture_names() + .flatten() + .filter(|name| name.starts_with('c')) + .collect(); + + named.sort_by_key(|name| name[1..].parse::().unwrap_or(0)); + + for name in named { + let va = caps_a.name(name).map(|m| m.as_str()); + let vb = caps_b.name(name).map(|m| m.as_str()); + if let (Some(va), Some(vb)) = (va, vb) { + let o = va.cmp(vb); + if o != Ordering::Equal { + return match order { + SortOrder::Asc => o, + SortOrder::Desc => o.reverse(), + }; + } + } + } + + Ordering::Equal + } + (Some(_), None) => match order { + SortOrder::Asc => Ordering::Less, + SortOrder::Desc => Ordering::Greater, + }, + (None, Some(_)) => match order { + SortOrder::Asc => Ordering::Greater, + SortOrder::Desc => Ordering::Less, + }, + (None, None) => { + // NP match → fallback + let o = value_a.cmp(value_b); + match order { + SortOrder::Asc => o, + SortOrder::Desc => o.reverse(), + } + } + } + } else { + // No Regex-Sequence defined → fallback + let o = value_a.cmp(value_b); + match order { + SortOrder::Asc => o, + SortOrder::Desc => o.reverse(), + } + } +} + +fn playlistgroup_comparator(a: &PlaylistGroup, b: &PlaylistGroup, group_sort: &ConfigSortGroup, match_as_ascii: bool) -> Ordering { + let value_a = if match_as_ascii { deunicode(&a.title) } else { a.title.to_string() }; + let value_b = if match_as_ascii { deunicode(&b.title) } else { b.title.to_string() }; + + playlist_comparator(group_sort.t_sequence.as_ref(), &group_sort.order, &value_a, &value_b) +} + +fn playlistitem_comparator( + a: &PlaylistItem, + b: &PlaylistItem, + channel_sort: &ConfigSortChannel, + match_as_ascii: bool, +) -> Ordering { + let raw_value_a = get_field_value(a, &channel_sort.field); + let raw_value_b = get_field_value(b, &channel_sort.field); + let value_a = if match_as_ascii { deunicode(&raw_value_a) } else { raw_value_a }; + let value_b = if match_as_ascii { deunicode(&raw_value_b) } else { raw_value_b }; + + playlist_comparator(channel_sort.t_sequence.as_ref(), &channel_sort.order, &value_a, &value_b) +} + +pub(in crate::processing::processor) fn sort_playlist(target: &ConfigTarget, new_playlist: &mut [PlaylistGroup]) { + if let Some(sort) = &target.sort { + let match_as_ascii = sort.match_as_ascii; + if let Some(group_sort) = &sort.groups { + new_playlist.sort_by(|a, b| playlistgroup_comparator(a, b, group_sort, match_as_ascii)); + } + if let Some(channel_sorts) = &sort.channels { + for channel_sort in channel_sorts { + let regexp = channel_sort.t_re_group_pattern.as_ref().unwrap(); + for group in new_playlist.iter_mut() { + let group_title = if match_as_ascii { deunicode(&group.title) } else { group.title.to_string() }; + if regexp.is_match(group_title.as_str()) { + group.channels.sort_by(|chan1, chan2| playlistitem_comparator(chan1, chan2, channel_sort, match_as_ascii)); + } + } + } + } + } +} + +#[cfg(test)] +mod tests { + + use crate::model::{ConfigSortChannel, ItemField, SortOrder}; + use crate::model::{PlaylistItem, PlaylistItemHeader}; + use regex::Regex; + use crate::processing::processor::sort::playlistitem_comparator; + + #[test] + fn test_sort() { + let mut channels: Vec = vec![ + ("D", "HD"), ("A", "FHD"), ("Z", "HD"), ("K", "HD"), ("B", "HD"), ("A", "HD"), + ("K", "UHD"), ("C", "HD"), ("L", "FHD"), ("R", "UHD"), ("T", "SD"), ("A", "FHD"), + ].into_iter().map(|(name, quality)| PlaylistItem { header: PlaylistItemHeader { title: format!("Chanel {name} [{quality}]"), ..Default::default() } }).collect::>().into(); + + let channel_sort = ConfigSortChannel { + field: ItemField::Caption, + group_pattern: ".*".to_string(), + order: SortOrder::Asc, + sequence: None, + t_sequence: Some(vec![ + Regex::new(r"(?P.*?)\bUHD\b").unwrap(), + Regex::new(r"(?P.*?)\bFHD\b").unwrap(), + Regex::new(r"(?P.*?)\bHD\b").unwrap(), + ]), + t_re_group_pattern: Some(Regex::new(".*").unwrap()), + }; + + channels.sort_by(|chan1, chan2| playlistitem_comparator(chan1, chan2, &channel_sort, true)); + let expected = vec!["Chanel K [UHD]", "Chanel R [UHD]", "Chanel A [FHD]", "Chanel A [FHD]", "Chanel L [FHD]", "Chanel A [HD]", "Chanel B [HD]", "Chanel C [HD]", "Chanel D [HD]", "Chanel K [HD]", "Chanel Z [HD]", "Chanel T [SD]"]; + let sorted = channels.into_iter().map(|pli| pli.header.title.clone()).collect::>(); + assert_eq!(expected, sorted); + } +} \ No newline at end of file diff --git a/src/processing/processor/xtream.rs b/src/processing/processor/xtream.rs index c078c07b1..be680a9cb 100644 --- a/src/processing/processor/xtream.rs +++ b/src/processing/processor/xtream.rs @@ -1,7 +1,7 @@ use crate::m3u_filter_error::{info_err, notify_err}; use crate::m3u_filter_error::{str_to_io_error, to_io_error, M3uFilterError, M3uFilterErrorKind}; -use crate::model::config::{Config, ConfigInput}; -use crate::model::playlist::{FetchedPlaylist, PlaylistEntry, PlaylistItem, PlaylistItemType, XtreamCluster}; +use crate::model::{Config, ConfigInput}; +use crate::model::{FetchedPlaylist, PlaylistEntry, PlaylistItem, PlaylistItemType, XtreamCluster}; use crate::repository::storage::get_input_storage_path; use serde::{Deserialize, Serialize}; use std::collections::HashMap; @@ -9,12 +9,11 @@ use std::fs::File; use std::io::{BufWriter, Write}; use std::path::PathBuf; use std::sync::Arc; -use crate::model::xtream_const; use crate::repository::bplustree::BPlusTree; use crate::repository::storage_const; use crate::repository::xtream_repository::xtream_get_record_file_path; -use crate::utils::file::file_utils::append_or_crate_file; -use crate::utils::network::xtream; +use crate::utils::file_utils::append_or_crate_file; +use crate::utils::xtream; pub(in crate::processing) async fn playlist_resolve_download_playlist_item(client: Arc, pli: &PlaylistItem, input: &ConfigInput, errors: &mut Vec, resolve_delay: u16, cluster: XtreamCluster) -> Option { let mut result = None; @@ -47,7 +46,7 @@ pub(in crate::processing) fn write_info_content_to_wal_file(writer: &mut BufWrit pub(in crate::processing) fn create_resolve_episode_wal_files(cfg: &Config, input: &ConfigInput) -> Option<(File, PathBuf)> { match get_input_storage_path(&input.name, &cfg.working_dir) { Ok(storage_path) => { - let info_path = storage_path.join(format!("{}.{}", xtream_const::XC_FILE_SERIES_EPISODE_RECORD, storage_const::FILE_SUFFIX_WAL)); + let info_path = storage_path.join(format!("{}.{}", crate::model::XC_FILE_SERIES_EPISODE_RECORD, storage_const::FILE_SUFFIX_WAL)); let info_file = append_or_crate_file(&info_path).ok()?; Some((info_file, info_path)) } @@ -60,8 +59,8 @@ pub(in crate::processing) fn create_resolve_info_wal_files(cfg: &Config, input: Ok(storage_path) => { if let Some(file_prefix) = match cluster { XtreamCluster::Live => None, - XtreamCluster::Video => Some(xtream_const::XC_FILE_VOD_INFO), - XtreamCluster::Series => Some(xtream_const::XC_FILE_SERIES_INFO) + XtreamCluster::Video => Some(crate::model::XC_FILE_VOD_INFO), + XtreamCluster::Series => Some(crate::model::XC_FILE_SERIES_INFO) } { let content_path = storage_path.join(format!("{file_prefix}_content.{}", storage_const::FILE_SUFFIX_WAL)); let info_path = storage_path.join(format!("{file_prefix}_record.{}", storage_const::FILE_SUFFIX_WAL)); diff --git a/src/processing/processor/xtream_series.rs b/src/processing/processor/xtream_series.rs index e2c5441e5..c2c6f0cdb 100644 --- a/src/processing/processor/xtream_series.rs +++ b/src/processing/processor/xtream_series.rs @@ -1,6 +1,6 @@ use crate::m3u_filter_error::{M3uFilterError, M3uFilterErrorKind}; -use crate::model::config::{Config, ConfigTarget, InputType}; -use crate::model::playlist::{FetchedPlaylist, PlaylistGroup, PlaylistItem, PlaylistItemType, XtreamCluster}; +use crate::model::{Config, ConfigTarget, InputType}; +use crate::model::{FetchedPlaylist, PlaylistGroup, PlaylistItem, PlaylistItemType, XtreamCluster}; use crate::processing::processor::playlist::ProcessingPipe; use crate::processing::parser::xtream::parse_xtream_series_info; use crate::processing::processor::xtream::{create_resolve_episode_wal_files, create_resolve_info_wal_files, playlist_resolve_download_playlist_item, read_processed_info_ids, should_update_info, write_info_content_to_wal_file}; @@ -15,10 +15,9 @@ use std::io::{BufWriter, Write}; use std::sync::Arc; use std::time::Instant; use log::{info, log_enabled, Level}; -use crate::model::xtream::{XtreamSeriesEpisode, XtreamSeriesInfoEpisode}; -use crate::model::xtream_const; -use crate::utils::bincode_utils::bincode_serialize; -use crate::utils::file::file_utils::file_writer; +use crate::model::{XtreamSeriesEpisode, XtreamSeriesInfoEpisode}; +use crate::utils::bincode_serialize; +use crate::utils::file_utils::file_writer; create_resolve_options_function_for_xtream_target!(series); @@ -52,7 +51,7 @@ fn write_series_episode_record_to_wal_file( } fn should_update_series_info(pli: &mut PlaylistItem, processed_provider_ids: &HashMap) -> (bool, u32, u64) { - should_update_info(pli, processed_provider_ids, xtream_const::XC_TAG_SERIES_INFO_LAST_MODIFIED) + should_update_info(pli, processed_provider_ids, crate::model::XC_TAG_SERIES_INFO_LAST_MODIFIED) } async fn playlist_resolve_series_info(client: Arc, cfg: &Config, errors: &mut Vec, diff --git a/src/processing/processor/xtream_vod.rs b/src/processing/processor/xtream_vod.rs index 04fd16cf5..5ebc7daba 100644 --- a/src/processing/processor/xtream_vod.rs +++ b/src/processing/processor/xtream_vod.rs @@ -1,11 +1,11 @@ use crate::m3u_filter_error::{M3uFilterError, M3uFilterErrorKind}; -use crate::model::config::{Config, ConfigTarget, InputType}; -use crate::model::playlist::{FetchedPlaylist, PlaylistItem, PlaylistItemType, XtreamCluster}; +use crate::model::{Config, ConfigTarget, InputType}; +use crate::model::{FetchedPlaylist, PlaylistItem, PlaylistItemType, XtreamCluster}; use crate::processing::processor::xtream::{create_resolve_info_wal_files, playlist_resolve_download_playlist_item, read_processed_info_ids, should_update_info, write_info_content_to_wal_file}; use crate::repository::xtream_repository::{xtream_update_input_info_file, xtream_update_input_vod_record_from_wal_file, InputVodInfoRecord}; use crate::m3u_filter_error::{notify_err}; use crate::processing::processor::{handle_error, handle_error_and_return, create_resolve_options_function_for_xtream_target}; -use crate::utils::json_utils::{get_u32_from_serde_value, get_u64_from_serde_value}; +use crate::utils::{get_u32_from_serde_value, get_u64_from_serde_value}; use serde_json::{Map, Value}; use std::collections::HashMap; use std::fs::File; @@ -13,8 +13,7 @@ use std::io::{BufWriter, Write}; use std::sync::Arc; use std::time::Instant; use log::{info, log_enabled, Level}; -use crate::model::xtream_const; -use crate::utils::file::file_utils::file_writer; +use crate::utils::file_utils::file_writer; create_resolve_options_function_for_xtream_target!(vod); @@ -25,18 +24,18 @@ async fn read_processed_vod_info_ids(cfg: &Config, errors: &mut Vec Option<(u32, InputVodInfoRecord)> { let doc = serde_json::from_str::>(content).ok()?; - let movie_data = doc.get(xtream_const::XC_TAG_VOD_INFO_MOVIE_DATA)?.as_object()?; + let movie_data = doc.get(crate::model::XC_TAG_VOD_INFO_MOVIE_DATA)?.as_object()?; let provider_id = get_u32_from_serde_value( - movie_data.get(xtream_const::XC_TAG_VOD_INFO_STREAM_ID)?, + movie_data.get(crate::model::XC_TAG_VOD_INFO_STREAM_ID)?, )?; let added = movie_data - .get(xtream_const::XC_TAG_VOD_INFO_ADDED) + .get(crate::model::XC_TAG_VOD_INFO_ADDED) .and_then(get_u64_from_serde_value) .unwrap_or(0); - let tmdb_id = doc.get(xtream_const::XC_TAG_VOD_INFO_INFO)?.as_object() - .and_then(|info| info.get(xtream_const::XC_TAG_VOD_INFO_TMDB_ID)) + let tmdb_id = doc.get(crate::model::XC_TAG_VOD_INFO_INFO)?.as_object() + .and_then(|info| info.get(crate::model::XC_TAG_VOD_INFO_TMDB_ID)) .and_then(get_u32_from_serde_value) .unwrap_or(0); @@ -58,7 +57,7 @@ fn write_vod_info_record_to_wal_file( } fn should_update_vod_info(pli: &mut PlaylistItem, processed_provider_ids: &HashMap) -> (bool, u32, u64) { - should_update_info(pli, processed_provider_ids, xtream_const::XC_TAG_VOD_INFO_ADDED) + should_update_info(pli, processed_provider_ids, crate::model::XC_TAG_VOD_INFO_ADDED) } pub async fn playlist_resolve_vod(client: Arc, cfg: &Config, target: &ConfigTarget, errors: &mut Vec, fpl: &mut FetchedPlaylist<'_>) { diff --git a/src/repository/bplustree.rs b/src/repository/bplustree.rs index 469c43c06..92e00de98 100644 --- a/src/repository/bplustree.rs +++ b/src/repository/bplustree.rs @@ -4,13 +4,13 @@ use std::marker::PhantomData; use std::mem::size_of; use std::path::Path; use crate::m3u_filter_error::{str_to_io_error, to_io_error}; -use crate::utils::file::file_utils::{file_reader, file_writer, open_read_write_file, rename_or_copy}; +use crate::utils::file_utils::{file_reader, file_writer, open_read_write_file, rename_or_copy}; use log::error; use ruzstd::decoding::StreamingDecoder; use ruzstd::encoding::{compress_to_vec, CompressionLevel}; use serde::{Deserialize, Serialize}; use tempfile::NamedTempFile; -use crate::utils::bincode_utils::{bincode_deserialize, bincode_serialize}; +use crate::utils::{bincode_deserialize, bincode_serialize}; const BLOCK_SIZE: usize = 4096; const BINCODE_OVERHEAD: usize = 8; @@ -784,7 +784,7 @@ mod tests { #[test] fn insert_test() -> io::Result<()> { let test_size = 500; - let content = crate::utils::string_utils::generate_random_string(1024); + let content = crate::utils::generate_random_string(1024); let mut tree = BPlusTree::::new(); for i in 0u32..=test_size { tree.insert(i, Record { diff --git a/src/repository/epg_repository.rs b/src/repository/epg_repository.rs index 91cfe6877..05ee983c6 100644 --- a/src/repository/epg_repository.rs +++ b/src/repository/epg_repository.rs @@ -1,6 +1,6 @@ use crate::m3u_filter_error::{notify_err, M3uFilterError, M3uFilterErrorKind}; -use crate::model::config::{Config, ConfigTarget, TargetOutput}; -use crate::model::xmltv::Epg; +use crate::model::{Config, ConfigTarget, TargetOutput}; +use crate::model::Epg; use crate::repository::m3u_repository::m3u_get_epg_file_path; use crate::repository::xtream_repository::{xtream_get_epg_file_path, xtream_get_storage_path}; use crate::utils::debug_if_enabled; diff --git a/src/repository/indexed_document.rs b/src/repository/indexed_document.rs index 1d62b98c2..212602f65 100644 --- a/src/repository/indexed_document.rs +++ b/src/repository/indexed_document.rs @@ -5,13 +5,13 @@ use std::marker::PhantomData; use std::path::{Path, PathBuf}; use crate::repository::bplustree::{BPlusTree, BPlusTreeQuery}; -use crate::utils::file::file_utils; +use crate::utils::file_utils; use log::error; use serde::{Deserialize, Serialize}; use tempfile::NamedTempFile; use crate::m3u_filter_error::{str_to_io_error, to_io_error}; -use crate::utils::bincode_utils::{bincode_deserialize, bincode_serialize}; -use crate::utils::file::file_utils::{create_new_file_for_read_write, file_reader, file_writer, open_read_write_file, open_readonly_file, rename_or_copy}; +use crate::utils::{bincode_deserialize, bincode_serialize}; +use crate::utils::file_utils::{create_new_file_for_read_write, file_reader, file_writer, open_read_write_file, open_readonly_file, rename_or_copy}; const BLOCK_SIZE: usize = 4096; const LEN_SIZE: usize = 4; @@ -512,10 +512,10 @@ mod tests { use std::path::PathBuf; use serde::{Deserialize, Serialize}; - use crate::model::playlist::XtreamPlaylistItem; - // use crate::model::playlist::XtreamPlaylistItem; + use crate::model::XtreamPlaylistItem; + // use crate::model::XtreamPlaylistItem; use crate::repository::indexed_document::{IndexedDocumentGarbageCollector, IndexedDocumentIterator, IndexedDocumentWriter}; - use crate::utils::file::config_reader::resolve_env_var; + use crate::utils::config_reader::resolve_env_var; // Example usage with a simple struct #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] diff --git a/src/repository/kodi_repository.rs b/src/repository/kodi_repository.rs index f59e970c5..903d5c4dd 100644 --- a/src/repository/kodi_repository.rs +++ b/src/repository/kodi_repository.rs @@ -1,19 +1,19 @@ use crate::m3u_filter_error::{create_m3u_filter_error_result, info_err}; use crate::m3u_filter_error::{M3uFilterError, M3uFilterErrorKind}; -use crate::model::api_proxy::{ApiProxyServerInfo, ProxyUserCredentials}; -use crate::model::config::{ClusterFlags, Config, ConfigTarget, StrmTargetOutput}; -use crate::model::playlist::{ +use crate::model::{ApiProxyServerInfo, ProxyUserCredentials}; +use crate::model::{ClusterFlags, Config, ConfigTarget, StrmTargetOutput}; +use crate::model::{ FieldGetAccessor, PlaylistGroup, PlaylistItem, PlaylistItemType, UUIDType, }; -use crate::model::xtream::XtreamSeriesEpisode; +use crate::model::XtreamSeriesEpisode; use crate::repository::bplustree::BPlusTree; use crate::repository::storage::{ensure_target_storage_path, get_input_storage_path, hash_bytes}; use crate::repository::storage_const; use crate::repository::xtream_repository::{xtream_get_record_file_path, InputVodInfoRecord}; -use crate::utils::constants::{KodiStyle, CONSTANTS}; -use crate::utils::file::file_lock_manager::FileReadGuard; -use crate::utils::file::file_utils; -use crate::utils::network::request::extract_extension_from_url; +use crate::utils::{KodiStyle, CONSTANTS}; +use crate::utils::file_lock_manager::FileReadGuard; +use crate::utils::file_utils; +use crate::utils::request::extract_extension_from_url; use chrono::Datelike; use filetime::{set_file_times, FileTime}; use log::{error, trace}; diff --git a/src/repository/m3u_playlist_iterator.rs b/src/repository/m3u_playlist_iterator.rs index f6dbaa5fe..5d1bca96c 100644 --- a/src/repository/m3u_playlist_iterator.rs +++ b/src/repository/m3u_playlist_iterator.rs @@ -1,14 +1,14 @@ use crate::m3u_filter_error::info_err; use crate::m3u_filter_error::{M3uFilterError, M3uFilterErrorKind}; -use crate::model::api_proxy::{ProxyType, ProxyUserCredentials}; -use crate::model::config::{Config, ConfigTarget, ConfigTargetOptions, TargetType}; -use crate::model::playlist::{M3uPlaylistItem, PlaylistItemType, XtreamCluster}; +use crate::model::{ProxyType, ProxyUserCredentials}; +use crate::model::{Config, ConfigTarget, ConfigTargetOptions, TargetType}; +use crate::model::{M3uPlaylistItem, PlaylistItemType, XtreamCluster}; use crate::repository::indexed_document::IndexedDocumentIterator; use crate::repository::m3u_repository::m3u_get_file_paths; use crate::repository::storage::ensure_target_storage_path; use crate::repository::storage_const; use crate::repository::user_repository::user_get_bouquet_filter; -use crate::utils::file::file_lock_manager::FileReadGuard; +use crate::utils::file_lock_manager::FileReadGuard; use std::collections::HashSet; #[allow(clippy::struct_excessive_bools)] diff --git a/src/repository/m3u_repository.rs b/src/repository/m3u_repository.rs index dd3c78714..00a4d50e6 100644 --- a/src/repository/m3u_repository.rs +++ b/src/repository/m3u_repository.rs @@ -1,14 +1,14 @@ use crate::m3u_filter_error::{create_m3u_filter_error,info_err}; use crate::m3u_filter_error::{str_to_io_error, M3uFilterError, M3uFilterErrorKind}; -use crate::model::api_proxy::ProxyUserCredentials; -use crate::model::config::{Config, ConfigTarget, M3uTargetOutput}; -use crate::model::playlist::{M3uPlaylistItem, PlaylistGroup, PlaylistItem, PlaylistItemType}; +use crate::model::ProxyUserCredentials; +use crate::model::{Config, ConfigTarget, M3uTargetOutput}; +use crate::model::{M3uPlaylistItem, PlaylistGroup, PlaylistItem, PlaylistItemType}; use crate::repository::indexed_document::{IndexedDocumentDirectAccess, IndexedDocumentIterator, IndexedDocumentWriter}; use crate::repository::m3u_playlist_iterator::{M3uPlaylistM3uTextIterator}; use crate::repository::storage::{get_target_storage_path}; -use crate::utils::file::file_lock_manager::FileReadGuard; -use crate::utils::file::file_utils; -use crate::utils::file::file_utils::file_writer; +use crate::utils::file_lock_manager::FileReadGuard; +use crate::utils::file_utils; +use crate::utils::file_utils::file_writer; use log::error; use std::fs::File; use std::io::{Error, Write}; diff --git a/src/repository/playlist_repository.rs b/src/repository/playlist_repository.rs index cc0fa6900..8af0dd412 100644 --- a/src/repository/playlist_repository.rs +++ b/src/repository/playlist_repository.rs @@ -1,16 +1,16 @@ use crate::m3u_filter_error::info_err; use crate::m3u_filter_error::{M3uFilterError, M3uFilterErrorKind}; -use crate::model::config::{Config, ConfigTarget, TargetOutput}; -use crate::model::playlist::{PlaylistGroup, PlaylistItemType}; -use crate::model::xmltv::Epg; +use crate::model::{Config, ConfigTarget, TargetOutput}; +use crate::model::{PlaylistGroup, PlaylistItemType}; +use crate::model::Epg; use crate::repository::epg_repository::epg_write; use crate::repository::kodi_repository::kodi_write_strm_playlist; use crate::repository::m3u_repository::m3u_write_playlist; use crate::repository::storage::{ensure_target_storage_path, get_target_id_mapping_file}; use crate::repository::target_id_mapping::TargetIdMapping; use crate::repository::xtream_repository::xtream_write_playlist; -use crate::utils::file::file_lock_manager::FileWriteGuard; -use crate::utils::network::request::{is_dash_url, is_hls_url}; +use crate::utils::file_lock_manager::FileWriteGuard; +use crate::utils::request::{is_dash_url, is_hls_url}; use std::path::Path; pub async fn persist_playlist(playlist: &mut [PlaylistGroup], epg: Option<&Epg>, diff --git a/src/repository/storage.rs b/src/repository/storage.rs index fe56ea41d..75626031c 100644 --- a/src/repository/storage.rs +++ b/src/repository/storage.rs @@ -1,11 +1,11 @@ use std::path::{Path, PathBuf}; use std::fmt::Write; use crate::m3u_filter_error::{M3uFilterError, M3uFilterErrorKind}; -use crate::model::config::{Config}; -use crate::model::playlist::UUIDType; +use crate::model::{Config}; +use crate::model::UUIDType; use crate::m3u_filter_error::{notify_err}; use crate::repository::storage_const; -use crate::utils::file::file_utils; +use crate::utils::file_utils; #[inline] pub fn hash_bytes(bytes: &[u8]) -> UUIDType { diff --git a/src/repository/target_id_mapping.rs b/src/repository/target_id_mapping.rs index 7f2511ec7..a89f64a84 100644 --- a/src/repository/target_id_mapping.rs +++ b/src/repository/target_id_mapping.rs @@ -7,7 +7,7 @@ use chrono::Local; use log::error; use serde::{Deserialize, Serialize}; -use crate::model::playlist::{PlaylistItemType, UUIDType}; +use crate::model::{PlaylistItemType, UUIDType}; use crate::repository::bplustree::BPlusTree; // TODO make configurable diff --git a/src/repository/user_repository.rs b/src/repository/user_repository.rs index 93270dac3..ea8e2a55b 100644 --- a/src/repository/user_repository.rs +++ b/src/repository/user_repository.rs @@ -1,13 +1,13 @@ -use crate::model::api_proxy::{ProxyType, ProxyUserCredentials, ProxyUserStatus, TargetUser}; -use crate::model::config::{Config, TargetType}; -use crate::model::playlist::XtreamCluster; -use crate::model::playlist_categories::{PlaylistBouquetDto, TargetBouquetDto}; -use crate::model::xtream::PlaylistXtreamCategory; +use crate::model::{ProxyType, ProxyUserCredentials, ProxyUserStatus, TargetUser}; +use crate::model::{Config, TargetType}; +use crate::model::XtreamCluster; +use crate::model::{PlaylistBouquetDto, TargetBouquetDto}; +use crate::model::PlaylistXtreamCategory; use crate::repository::bplustree::BPlusTree; use crate::repository::storage_const; use crate::repository::xtream_repository::xtream_get_playlist_categories; -use crate::utils::file::file_utils; -use crate::utils::json_utils::json_write_documents_to_file; +use crate::utils::file_utils; +use crate::utils::json_write_documents_to_file; use chrono::Local; use log::error; use std::collections::{HashMap, HashSet}; @@ -332,7 +332,7 @@ pub async fn user_get_bouquet_filter(config: &Config, username: &str, category_i #[cfg(test)] mod tests { use super::*; - use crate::model::api_proxy::{ProxyType, ProxyUserStatus}; + use crate::model::{ProxyType, ProxyUserStatus}; use std::env::temp_dir; diff --git a/src/repository/xtream_playlist_iterator.rs b/src/repository/xtream_playlist_iterator.rs index 2f7028248..48a363459 100644 --- a/src/repository/xtream_playlist_iterator.rs +++ b/src/repository/xtream_playlist_iterator.rs @@ -2,14 +2,14 @@ use std::collections::HashSet; use log::error; use crate::m3u_filter_error::info_err; use crate::m3u_filter_error::{M3uFilterError, M3uFilterErrorKind}; -use crate::model::api_proxy::{ProxyUserCredentials}; -use crate::model::config::{Config, ConfigTarget, TargetType}; -use crate::model::playlist::{XtreamCluster, XtreamPlaylistItem}; -use crate::model::xtream::XtreamMappingOptions; +use crate::model::{ProxyUserCredentials}; +use crate::model::{Config, ConfigTarget, TargetType}; +use crate::model::{XtreamCluster, XtreamPlaylistItem}; +use crate::model::XtreamMappingOptions; use crate::repository::indexed_document::{IndexedDocumentIterator}; use crate::repository::user_repository::user_get_bouquet_filter; use crate::repository::xtream_repository::{xtream_get_file_paths, xtream_get_storage_path}; -use crate::utils::file::file_lock_manager::FileReadGuard; +use crate::utils::file_lock_manager::FileReadGuard; pub struct XtreamPlaylistIterator { reader: IndexedDocumentIterator, diff --git a/src/repository/xtream_repository.rs b/src/repository/xtream_repository.rs index c9f98c229..559c9a401 100644 --- a/src/repository/xtream_repository.rs +++ b/src/repository/xtream_repository.rs @@ -1,9 +1,8 @@ use crate::m3u_filter_error::{create_m3u_filter_error, create_m3u_filter_error_result, info_err, notify_err, str_to_io_error, M3uFilterError, M3uFilterErrorKind}; -use crate::model::api_proxy::{ProxyUserCredentials}; -use crate::model::config::{Config, ConfigInput, ConfigTarget, XtreamTargetOutput}; -use crate::model::playlist::{PlaylistEntry, PlaylistGroup, PlaylistItem, PlaylistItemType, XtreamCluster, XtreamPlaylistItem}; -use crate::model::xtream::{rewrite_doc_urls, PlaylistXtreamCategory, XtreamMappingOptions, XtreamSeriesEpisode}; -use crate::model::xtream_const; +use crate::model::{ProxyUserCredentials}; +use crate::model::{Config, ConfigInput, ConfigTarget, XtreamTargetOutput}; +use crate::model::{PlaylistEntry, PlaylistGroup, PlaylistItem, PlaylistItemType, XtreamCluster, XtreamPlaylistItem}; +use crate::model::{rewrite_doc_urls, PlaylistXtreamCategory, XtreamMappingOptions, XtreamSeriesEpisode}; use crate::repository::bplustree::{BPlusTree, BPlusTreeQuery, BPlusTreeUpdate}; use crate::repository::indexed_document::{IndexedDocumentDirectAccess, IndexedDocumentGarbageCollector, IndexedDocumentIterator, IndexedDocumentWriter}; use crate::repository::playlist_repository::get_target_id_mapping; @@ -12,12 +11,12 @@ use crate::repository::storage::{get_input_storage_path, get_target_id_mapping_f use crate::repository::storage_const; use crate::repository::target_id_mapping::VirtualIdRecord; use crate::repository::xtream_playlist_iterator::XtreamPlaylistJsonIterator; -use crate::utils::bincode_utils::bincode_deserialize; -use crate::utils::file::file_lock_manager::FileReadGuard; -use crate::utils::file::file_utils::file_reader; -use crate::utils::file::file_utils::open_readonly_file; -use crate::utils::hash_utils::generate_playlist_uuid; -use crate::utils::json_utils::{get_u32_from_serde_value, json_iter_array, json_write_documents_to_file}; +use crate::utils::bincode_deserialize; +use crate::utils::file_lock_manager::FileReadGuard; +use crate::utils::file_utils::file_reader; +use crate::utils::file_utils::open_readonly_file; +use crate::utils::generate_playlist_uuid; +use crate::utils::{get_u32_from_serde_value, json_iter_array, json_write_documents_to_file}; use bytes::Bytes; use futures::{stream, Stream, StreamExt}; use log::error; @@ -146,9 +145,9 @@ fn load_old_category_ids(path: &Path) -> (u32, HashMap) { if let Ok(file) = File::open(col_path) { let reader = file_reader(file); for entry in json_iter_array::>(reader).flatten() { - if let Some(category_id) = entry.get(xtream_const::XC_TAG_CATEGORY_ID).and_then(get_u32_from_serde_value) { + if let Some(category_id) = entry.get(crate::model::XC_TAG_CATEGORY_ID).and_then(get_u32_from_serde_value) { if let Value::Object(item) = entry { - if let Some(category_name) = get_map_item_as_str(&item, xtream_const::XC_TAG_CATEGORY_NAME) { + if let Some(category_name) = get_map_item_as_str(&item, crate::model::XC_TAG_CATEGORY_NAME) { result.insert(format!("{cluster}{category_name}"), category_id); max_id = max_id.max(category_id); } @@ -228,9 +227,9 @@ pub async fn xtream_write_playlist( XtreamCluster::Series => &mut cat_series_col, XtreamCluster::Video => &mut cat_vod_col, }.push(json!({ - xtream_const::XC_TAG_CATEGORY_ID: format!("{}", &cat_id), - xtream_const::XC_TAG_CATEGORY_NAME: plg.title.clone(), - xtream_const::XC_TAG_PARENT_ID: 0 + crate::model::XC_TAG_CATEGORY_ID: format!("{}", &cat_id), + crate::model::XC_TAG_CATEGORY_NAME: plg.title.clone(), + crate::model::XC_TAG_PARENT_ID: 0 })); for pli in &mut plg.channels { @@ -529,31 +528,31 @@ async fn rewrite_xtream_vod_info

( { // we need to update the info data. if config.is_reverse_proxy_resource_rewrite_enabled() { - if let Some(Value::Object(info_data)) = doc.get_mut(xtream_const::XC_TAG_INFO_DATA) { + 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 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, xtream_const::XC_INFO_RESOURCE_PREFIX); + rewrite_doc_urls(resource_url.as_ref(), info_data, storage_const::INFO_REWRITE_FIELDS, crate::model::XC_INFO_RESOURCE_PREFIX); // doc.insert(TAG_INFO_DATA, Value::Object(info_data)); } } } // we need to update the movie data with virtual ids. - if let Some(Value::Object(movie_data)) = doc.get_mut(xtream_const::XC_TAG_MOVIE_DATA) { + if let Some(Value::Object(movie_data)) = doc.get_mut(crate::model::XC_TAG_MOVIE_DATA) { let stream_id = pli.get_virtual_id(); let category_id = pli.get_category_id().unwrap_or(0); - movie_data.insert(xtream_const::XC_TAG_STREAM_ID.to_string(), Value::Number(serde_json::value::Number::from(stream_id))); - movie_data.insert(xtream_const::XC_TAG_CATEGORY_ID.to_string(), Value::Number(serde_json::value::Number::from(category_id))); - movie_data.insert(xtream_const::XC_TAG_CATEGORY_IDS.to_string(), Value::Array(vec![Value::Number(serde_json::value::Number::from(category_id))])); + movie_data.insert(crate::model::XC_TAG_STREAM_ID.to_string(), Value::Number(serde_json::value::Number::from(stream_id))); + movie_data.insert(crate::model::XC_TAG_CATEGORY_ID.to_string(), Value::Number(serde_json::value::Number::from(category_id))); + movie_data.insert(crate::model::XC_TAG_CATEGORY_IDS.to_string(), Value::Array(vec![Value::Number(serde_json::value::Number::from(category_id))])); let options = XtreamMappingOptions::from_target_options(target, xtream_output, config); if options.skip_video_direct_source { - movie_data.insert(xtream_const::XC_TAG_DIRECT_SOURCE.to_string(), Value::String(String::new())); + movie_data.insert(crate::model::XC_TAG_DIRECT_SOURCE.to_string(), Value::String(String::new())); } else { movie_data.insert( - xtream_const::XC_TAG_DIRECT_SOURCE.to_string(), + crate::model::XC_TAG_DIRECT_SOURCE.to_string(), Value::String(pli.get_provider_url().to_string()), ); } @@ -618,14 +617,14 @@ async fn rewrite_xtream_series_info

( }; if resource_url.is_some() { // we need to update the info data. - if let Some(Value::Object(info_data)) = doc.get_mut(xtream_const::XC_TAG_INFO_DATA) { - rewrite_doc_urls(resource_url.as_ref(), info_data, storage_const::INFO_REWRITE_FIELDS, xtream_const::XC_INFO_RESOURCE_PREFIX); + if let Some(Value::Object(info_data)) = doc.get_mut(crate::model::XC_TAG_INFO_DATA) { + rewrite_doc_urls(resource_url.as_ref(), info_data, storage_const::INFO_REWRITE_FIELDS, crate::model::XC_INFO_RESOURCE_PREFIX); } - if let Some(Value::Array(seasons_data)) = doc.get_mut(xtream_const::XC_TAG_SEASONS_DATA) { + if let Some(Value::Array(seasons_data)) = doc.get_mut(crate::model::XC_TAG_SEASONS_DATA) { for season_value in seasons_data { if let Value::Object(season_doc) = season_value { - if let Some(season_provider_id) = season_doc.get(xtream_const::XC_TAG_ID).and_then(get_u32_from_serde_value) { - let field_prefix = format!("{}{season_provider_id}_", xtream_const::XC_SEASON_RESOURCE_PREFIX); + if let Some(season_provider_id) = season_doc.get(crate::model::XC_TAG_ID).and_then(get_u32_from_serde_value) { + let field_prefix = format!("{}{season_provider_id}_", crate::model::XC_SEASON_RESOURCE_PREFIX); rewrite_doc_urls(resource_url.as_ref(), season_doc, storage_const::INFO_REWRITE_FIELDS, &field_prefix); } } @@ -633,7 +632,7 @@ async fn rewrite_xtream_series_info

( } } - let episodes = doc.get_mut(xtream_const::XC_TAG_EPISODES).and_then(Value::as_object_mut).ok_or_else(|| str_to_io_error("No episodes found in content"))?; + let episodes = doc.get_mut(crate::model::XC_TAG_EPISODES).and_then(Value::as_object_mut).ok_or_else(|| str_to_io_error("No episodes found in content"))?; let virtual_id = pli.get_virtual_id(); { @@ -643,7 +642,7 @@ async fn rewrite_xtream_series_info

( let provider_url = pli.get_provider_url(); for episode_list in episodes.values_mut().filter_map(Value::as_array_mut) { for episode in episode_list.iter_mut().filter_map(Value::as_object_mut) { - if let Some(episode_provider_id) = episode.get(xtream_const::XC_TAG_ID).and_then(get_u32_from_serde_value) + if let Some(episode_provider_id) = episode.get(crate::model::XC_TAG_ID).and_then(get_u32_from_serde_value) { let uuid = generate_playlist_uuid(&hex_encode(&pli.get_uuid()), &episode_provider_id.to_string(), PlaylistItemType::Series, &provider_url); let episode_virtual_id = target_id_mapping.get_and_update_virtual_id( @@ -652,17 +651,17 @@ async fn rewrite_xtream_series_info

( PlaylistItemType::Series, virtual_id, ); - episode.insert(xtream_const::XC_TAG_ID.to_string(), Value::String(episode_virtual_id.to_string())); + episode.insert(crate::model::XC_TAG_ID.to_string(), Value::String(episode_virtual_id.to_string())); if resource_url.is_some() { // we need to update the info data. - if let Some(Value::Object(info_data)) = episode.get_mut(xtream_const::XC_TAG_INFO_DATA) { - let field_prefix = format!("{}{episode_provider_id}_", xtream_const::XC_INFO_RESOURCE_PREFIX_EPISODE); + if let Some(Value::Object(info_data)) = episode.get_mut(crate::model::XC_TAG_INFO_DATA) { + let field_prefix = format!("{}{episode_provider_id}_", crate::model::XC_INFO_RESOURCE_PREFIX_EPISODE); rewrite_doc_urls(resource_url.as_ref(), info_data, storage_const::INFO_REWRITE_FIELDS, &field_prefix); } } } if options.skip_series_direct_source { - episode.insert(xtream_const::XC_TAG_DIRECT_SOURCE.to_string(), Value::String(String::new())); + episode.insert(crate::model::XC_TAG_DIRECT_SOURCE.to_string(), Value::String(String::new())); } } } diff --git a/src/tools/lru_cache.rs b/src/tools/lru_cache.rs index 84d9f541c..34d1eaa2f 100644 --- a/src/tools/lru_cache.rs +++ b/src/tools/lru_cache.rs @@ -1,6 +1,6 @@ use crate::repository::storage::hash_string_as_hex; -use crate::utils::file::file_utils::traverse_dir; -use crate::utils::size_utils::human_readable_byte_size; +use crate::utils::file_utils::traverse_dir; +use crate::utils::human_readable_byte_size; use log::{debug, error, info, trace}; use std::collections::{HashMap, VecDeque}; use std::fs; diff --git a/src/utils/compression/compressed_file_reader.rs b/src/utils/compression/compressed_file_reader.rs index 30f0207f2..50335a4a2 100644 --- a/src/utils/compression/compressed_file_reader.rs +++ b/src/utils/compression/compressed_file_reader.rs @@ -2,7 +2,7 @@ use std::io::{BufRead, BufReader, Read, Seek, SeekFrom}; use std::path::Path; use flate2::bufread::{GzDecoder, ZlibDecoder}; use crate::utils::compression::compression_utils::{is_deflate, is_gzip}; -use crate::utils::file::file_utils::{file_reader, open_readonly_file}; +use crate::utils::file_utils::{file_reader, open_readonly_file}; pub struct CompressedFileReader { reader: BufReader>, diff --git a/src/utils/file/config_reader.rs b/src/utils/file/config_reader.rs index 7d7fc5b20..f43fa2dea 100644 --- a/src/utils/file/config_reader.rs +++ b/src/utils/file/config_reader.rs @@ -1,12 +1,12 @@ use crate::m3u_filter_error::{create_m3u_filter_error, create_m3u_filter_error_result, handle_m3u_filter_error_result, info_err, str_to_io_error, to_io_error, M3uFilterError, M3uFilterErrorKind}; -use crate::model::api_proxy::ApiProxyConfig; -use crate::model::config::{Config, ConfigDto, ConfigInput, ConfigInputAlias, InputType}; -use crate::model::mapping::Mappings; -use crate::utils::constants::CONSTANTS; -use crate::utils::file::env_resolving_reader::EnvResolvingReader; -use crate::utils::file::file_utils::{file_reader, resolve_relative_path}; -use crate::utils::file::{file_utils, multi_file_reader}; -use crate::utils::network::request::{get_credentials_from_url, get_local_file_content}; +use crate::model::ApiProxyConfig; +use crate::model::{Config, ConfigDto, ConfigInput, ConfigInputAlias, InputType}; +use crate::model::Mappings; +use crate::utils::CONSTANTS; +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 chrono::Local; use log::{debug, error, info, warn}; @@ -353,8 +353,8 @@ fn get_csv_file_path(file_uri: &String) -> Result { #[cfg(test)] mod tests { - use crate::model::config::InputType; - use crate::utils::file::config_reader::{csv_read_inputs_from_reader, resolve_env_var}; + use crate::model::InputType; + use crate::utils::config_reader::{csv_read_inputs_from_reader, resolve_env_var}; use std::io::{BufReader, Cursor}; const M3U_BATCH: &str = r#" #url;name;max_connections;priority diff --git a/src/utils/file/env_resolving_reader.rs b/src/utils/file/env_resolving_reader.rs index 2242647e1..7345646eb 100644 --- a/src/utils/file/env_resolving_reader.rs +++ b/src/utils/file/env_resolving_reader.rs @@ -1,5 +1,5 @@ use std::io::{self, BufRead, BufReader, Read, Cursor}; -use crate::utils::file::config_reader::resolve_env_var; +use crate::utils::config_reader::resolve_env_var; pub struct EnvResolvingReader { inner: BufReader, diff --git a/src/utils/file/file_utils.rs b/src/utils/file/file_utils.rs index 1ca4baac1..7fdf74abb 100644 --- a/src/utils/file/file_utils.rs +++ b/src/utils/file/file_utils.rs @@ -8,7 +8,7 @@ use crate::m3u_filter_error::str_to_io_error; use crate::utils::debug_if_enabled; use log::{debug, error}; use path_clean::PathClean; -use crate::utils::constants::{API_PROXY_FILE, CONFIG_FILE, CONFIG_PATH, MAPPING_FILE, SOURCE_FILE, USER_FILE}; +use crate::utils::{API_PROXY_FILE, CONFIG_FILE, CONFIG_PATH, MAPPING_FILE, SOURCE_FILE, USER_FILE}; pub fn file_writer(w: W) -> BufWriter where @@ -89,7 +89,7 @@ pub fn get_default_api_proxy_config_path(config_path: &str) -> String { } pub fn get_working_path(wd: &str) -> String { - let current_dir = std::env::current_dir().unwrap(); + let current_dir = std::env::current_dir().unwrap_or_default(); if wd.is_empty() { String::from(current_dir.to_str().unwrap_or(".")) } else { diff --git a/src/utils/file/multi_file_reader.rs b/src/utils/file/multi_file_reader.rs index 705f5407e..a412cfd07 100644 --- a/src/utils/file/multi_file_reader.rs +++ b/src/utils/file/multi_file_reader.rs @@ -1,8 +1,8 @@ use std::fs::File; use std::io::{self, ErrorKind, Read}; use std::path::{PathBuf}; -use crate::utils::file::env_resolving_reader::EnvResolvingReader; -use crate::utils::file::file_utils::file_reader; +use crate::utils::env_resolving_reader::EnvResolvingReader; +use crate::utils::file_utils::file_reader; pub struct MultiFileReader { files: Vec, diff --git a/src/utils/hash_utils.rs b/src/utils/hash_utils.rs index d30ea1dfb..513c46510 100644 --- a/src/utils/hash_utils.rs +++ b/src/utils/hash_utils.rs @@ -1,6 +1,6 @@ use base64::Engine; use base64::engine::general_purpose; -use crate::model::playlist::{PlaylistItemType, UUIDType}; +use crate::model::{PlaylistItemType, UUIDType}; use crate::repository::storage::hash_string; pub fn extract_id_from_url(url: &str) -> Option { diff --git a/src/utils/json_utils.rs b/src/utils/json_utils.rs index 386582ef2..647322f30 100644 --- a/src/utils/json_utils.rs +++ b/src/utils/json_utils.rs @@ -6,7 +6,7 @@ use std::path::Path; use serde::de::DeserializeOwned; use serde::{Deserialize, Serialize}; use serde_json::{self, Deserializer, Value}; -use crate::utils::file::file_utils::{file_reader, file_writer}; +use crate::utils::file_utils::{file_reader, file_writer}; fn read_skipping_ws(mut reader: impl Read) -> io::Result { loop { diff --git a/src/utils/mod.rs b/src/utils/mod.rs index 7a2e22510..3ca57cb43 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -1,17 +1,17 @@ -pub mod string_utils; -pub mod json_utils; -pub mod default_utils; -pub mod size_utils; -pub mod sys_utils; -pub mod hash_utils; -pub mod compression; -pub(crate) mod file; -pub(crate) mod network; -pub mod bincode_utils; -pub mod time_utils; -pub mod crypto_utils; -pub mod constants; -pub mod step_measure; +mod string_utils; +mod json_utils; +mod default_utils; +mod size_utils; +mod sys_utils; +mod hash_utils; +mod compression; +mod file; +mod network; +mod bincode_utils; +mod time_utils; +mod crypto_utils; +mod constants; +mod step_measure; #[macro_export] macro_rules! debug_if_enabled { @@ -45,3 +45,18 @@ macro_rules! trace_if_enabled { pub use debug_if_enabled; pub use trace_if_enabled; + +pub use self::default_utils::*; +pub use self::string_utils::*; +pub use self::json_utils::*; +pub use self::size_utils::*; +pub use self::sys_utils::*; +pub use self::hash_utils::*; +pub use self::compression::*; +pub use self::file::*; +pub use self::network::*; +pub use self::bincode_utils::*; +pub use self::time_utils::*; +pub use self::crypto_utils::*; +pub use self::constants::*; +pub use self::step_measure::*; diff --git a/src/utils/network/epg.rs b/src/utils/network/epg.rs index d4d784b04..64508dcde 100644 --- a/src/utils/network/epg.rs +++ b/src/utils/network/epg.rs @@ -1,10 +1,10 @@ use crate::m3u_filter_error::M3uFilterError; -use crate::model::config::{Config, ConfigInput}; -use crate::model::xmltv::TVGuide; +use crate::model::{Config, ConfigInput}; +use crate::model::TVGuide; use crate::repository::storage::short_hash; -use crate::utils::file::file_utils; -use crate::utils::file::file_utils::prepare_file_path; -use crate::utils::network::request; +use crate::utils::file_utils; +use crate::utils::file_utils::prepare_file_path; +use crate::utils::request; use log::debug; use std::path::PathBuf; use std::sync::Arc; diff --git a/src/utils/network/ip_checker.rs b/src/utils/network/ip_checker.rs new file mode 100644 index 000000000..70f9059c5 --- /dev/null +++ b/src/utils/network/ip_checker.rs @@ -0,0 +1,88 @@ +use crate::m3u_filter_error::{M3uFilterError, M3uFilterErrorKind}; +use crate::model::ConfigIpCheck; +use regex::Regex; +use reqwest::Client; +use std::sync::Arc; + +async fn fetch_ip(client: &Arc, url: &str, regex: Option<&Regex>) -> Result { + let response = client.get(url).send().await + .map_err(|e| M3uFilterError::new(M3uFilterErrorKind::Info, format!("Failed to request {url}: {e}")))?; + + let text = response.text().await + .map_err(|e| M3uFilterError::new(M3uFilterErrorKind::Info, format!("Failed to read response: {e}")))?; + + if let Some(re) = regex { + return if let Some(caps) = re.captures(&text) { + if let Some(m) = caps.get(1) { + Ok(m.as_str().to_string()) + } else { + Err(M3uFilterError::new(M3uFilterErrorKind::Info, "Regex matched but no group found".to_string())) + } + } else { + Err(M3uFilterError::new(M3uFilterErrorKind::Info, "Regex did not match".to_string())) + }; + } + + Ok(text.trim().to_string()) +} + +/// Fetch both IPs from a shared URL (if both regex patterns are available) +async fn fetch_combined_ips(client: &Arc, config: &ConfigIpCheck, url: &str) -> (Option, Option) { + let response = client.get(url).send().await.ok(); + let text = match response { + Some(r) => r.text().await.ok(), + None => None, + }; + + if let Some(body) = text { + let ipv4 = config + .t_pattern_ipv4 + .as_ref() + .and_then(|re| re.captures(&body)) + .and_then(|caps| caps.get(1).map(|m| m.as_str().to_string())); + + let ipv6 = config + .t_pattern_ipv6 + .as_ref() + .and_then(|re| re.captures(&body)) + .and_then(|caps| caps.get(1).map(|m| m.as_str().to_string())); + + (ipv4, ipv6) + } else { + (None, None) + } +} + + +/// Fetch both IPv4 and IPv6 addresses, using separate or combined URL(s) +pub async fn get_ips(client: &Arc, config: &ConfigIpCheck) -> Result<(Option, Option), M3uFilterError> { + match (&config.url_ipv4, &config.url_ipv6, &config.url) { + // Both dedicated URLs provided + (Some(url_v4), Some(url_v6), _) => { + let (ipv4, ipv6) = tokio::join!( + fetch_ip(client, url_v4, config.t_pattern_ipv4.as_ref()), + fetch_ip(client, url_v6, config.t_pattern_ipv6.as_ref()) + ); + Ok((ipv4.ok(), ipv6.ok())) + } + + // Only one combined URL provided + (_, _, Some(shared_url)) => { + let result = fetch_combined_ips(client, config, shared_url).await; + Ok(result) + } + + // Only one dedicated URL + (Some(url_v4), None, _) => { + let ipv4 = fetch_ip(client, url_v4, config.t_pattern_ipv4.as_ref()).await.ok(); + Ok((ipv4, None)) + } + (None, Some(url_v6), _) => { + let ipv6 = fetch_ip(client, url_v6, config.t_pattern_ipv6.as_ref()).await.ok(); + Ok((None, ipv6)) + } + + // No URLs given + _ => Err(M3uFilterError::new(M3uFilterErrorKind::Info, "No valid IP-check URLs provided".to_owned())), + } +} \ No newline at end of file diff --git a/src/utils/network/m3u.rs b/src/utils/network/m3u.rs index 06fe9cd0d..c0a710823 100644 --- a/src/utils/network/m3u.rs +++ b/src/utils/network/m3u.rs @@ -1,10 +1,10 @@ use std::sync::Arc; use crate::m3u_filter_error::M3uFilterError; -use crate::model::config::{Config, ConfigInput}; -use crate::model::playlist::PlaylistGroup; +use crate::model::{Config, ConfigInput}; +use crate::model::PlaylistGroup; use crate::processing::parser::m3u; -use crate::utils::file::file_utils::prepare_file_path; -use crate::utils::network::request; +use crate::utils::file_utils::prepare_file_path; +use crate::utils::request; pub async fn get_m3u_playlist(client: Arc, cfg: &Config, input: &ConfigInput, working_dir: &str) -> (Vec, Vec) { let url = input.url.clone(); diff --git a/src/utils/network/mod.rs b/src/utils/network/mod.rs index ce36c706e..ad3fefb8d 100644 --- a/src/utils/network/mod.rs +++ b/src/utils/network/mod.rs @@ -1,4 +1,5 @@ pub mod request; pub mod xtream; pub mod m3u; -pub mod epg; \ No newline at end of file +pub mod epg; +pub mod ip_checker; \ No newline at end of file diff --git a/src/utils/network/request.rs b/src/utils/network/request.rs index d590c892e..2cbf99d23 100644 --- a/src/utils/network/request.rs +++ b/src/utils/network/request.rs @@ -3,7 +3,7 @@ use std::fs; use std::fs::File; use std::io::{BufWriter, Error, ErrorKind, Read, Write}; use std::path::{Path, PathBuf}; -use std::sync::atomic::{Ordering}; +use std::sync::atomic::Ordering; use std::sync::Arc; use std::time::Instant; @@ -16,14 +16,14 @@ use url::Url; use crate::m3u_filter_error::create_m3u_filter_error_result; use crate::m3u_filter_error::{str_to_io_error, M3uFilterError, M3uFilterErrorKind}; -use crate::model::config::{ConfigInput, ConfigProxy, InputFetchMethod}; -use crate::model::stats::format_elapsed_time; +use crate::model::format_elapsed_time; +use crate::model::{ConfigInput, ConfigProxy, InputFetchMethod}; use crate::repository::storage::{get_input_storage_path, short_hash}; use crate::repository::storage_const; use crate::utils::compression::compression_utils::{is_deflate, is_gzip}; -use crate::utils::constants::{CONSTANTS, DASH_EXT, DASH_EXT_FRAGMENT, DASH_EXT_QUERY, ENCODING_DEFLATE, ENCODING_GZIP, HLS_EXT, HLS_EXT_FRAGMENT, HLS_EXT_QUERY}; use crate::utils::debug_if_enabled; -use crate::utils::file::file_utils::{get_file_path, persist_file}; +use crate::utils::file_utils::{get_file_path, persist_file}; +use crate::utils::{CONSTANTS, DASH_EXT, DASH_EXT_FRAGMENT, DASH_EXT_QUERY, ENCODING_DEFLATE, ENCODING_GZIP, HLS_EXT, HLS_EXT_FRAGMENT, HLS_EXT_QUERY}; pub const fn bytes_to_megabytes(bytes: u64) -> u64 { bytes / 1_048_576 @@ -121,34 +121,34 @@ pub async fn get_input_text_content(client: Arc, input: &Config } } -pub fn get_client_request(client: &Arc, - method: InputFetchMethod, - headers: Option<&HashMap>, - url: &Url, - custom_headers: Option<&HashMap>>) -> reqwest::RequestBuilder { +pub fn get_client_request(client: &Arc, + method: InputFetchMethod, + headers: Option<&HashMap>, + url: &Url, + custom_headers: Option<&HashMap, S>>) -> reqwest::RequestBuilder { let request = match method { InputFetchMethod::GET => client.get(url.clone()), InputFetchMethod::POST => { // let base_url = url[..url::Position::BeforePath].to_string() + url.path(); - let mut params = HashMap::new(); + let mut params: HashMap = HashMap::default(); for (key, value) in url.query_pairs() { params.insert(key.to_string(), value.to_string()); } // we could cut the params but we leave them as query and add them as form. client.post(url.clone()).form(¶ms) - }, + } }; let headers = get_request_headers(headers, custom_headers); request.headers(headers) } -pub fn get_request_headers(defined_headers: Option<&HashMap>, custom_headers: Option<&HashMap>>) -> HeaderMap { - let mut headers = HeaderMap::new(); +pub fn get_request_headers(defined_headers: Option<&HashMap>, custom_headers: Option<&HashMap, S>>) -> HeaderMap { + let mut headers = HeaderMap::default(); if let Some(def_headers) = defined_headers { for (key, value) in def_headers { - headers.insert( - HeaderName::from_bytes(key.as_bytes()).unwrap(), - HeaderValue::from_bytes(value.as_bytes()).unwrap()); + if let (Ok(key), Ok(value)) = (HeaderName::from_bytes(key.as_bytes()), HeaderValue::from_bytes(value.as_bytes())) { + headers.insert(key, value); + } } } if let Some(custom) = custom_headers { @@ -157,10 +157,8 @@ pub fn get_request_headers(defined_headers: Option<&HashMap>, cu let key_lc = key.to_lowercase(); if "host" == key_lc || header_keys.contains(key_lc.as_str()) { // debug_if_enabled!("Ignoring request header '{}={}'", key_lc, String::from_utf8_lossy(value)); - } else { - headers.insert( - HeaderName::from_bytes(key.as_bytes()).unwrap(), - HeaderValue::from_bytes(value).unwrap()); + } else if let (Ok(key), Ok(value)) = (HeaderName::from_bytes(key.as_bytes()), HeaderValue::from_bytes(value)) { + headers.insert(key, value); } } } @@ -185,7 +183,7 @@ pub fn get_local_file_content(file_path: &PathBuf) -> Result { Err(err) => Err(str_to_io_error(&format!("failed to decode gzip content {err}"))) }; } - return Ok(String::from_utf8_lossy(&content).parse().unwrap()); + return Ok(String::from_utf8_lossy(&content).parse().unwrap_or_default()); } } let file_str = file_path.to_str().unwrap_or("?"); @@ -326,7 +324,7 @@ pub async fn download_text_content(client: Arc, input: &ConfigI if let Ok(url) = url_str.parse::() { let result = if url.scheme() == "file" { url.to_file_path().map_or_else(|()| Err(str_to_io_error(&format!("Unknown file {}", sanitize_sensitive_info(url_str)))), |file_path| - get_local_file_content(&file_path).map(|c| (c, url.to_string())) + get_local_file_content(&file_path).map(|c| (c, url.to_string())), ) } else { get_remote_content(client, input, &url).await @@ -523,7 +521,7 @@ pub fn create_client(proxy_config: Option<&ConfigProxy>) -> reqwest::ClientBuild #[cfg(test)] mod tests { - use crate::utils::network::request::{get_base_url_from_str, replace_url_extension, sanitize_sensitive_info}; + use crate::utils::request::{get_base_url_from_str, replace_url_extension, sanitize_sensitive_info}; #[test] fn test_url_mask() { diff --git a/src/utils/network/xtream.rs b/src/utils/network/xtream.rs index 4612750dc..f341b50b5 100644 --- a/src/utils/network/xtream.rs +++ b/src/utils/network/xtream.rs @@ -1,18 +1,17 @@ use std::sync::Arc; use crate::m3u_filter_error::{str_to_io_error, M3uFilterError}; -use crate::model::config::{Config, ConfigInput, ConfigTarget}; -use crate::model::playlist::{PlaylistEntry, PlaylistGroup, XtreamCluster, XtreamPlaylistItem}; +use crate::model::{Config, ConfigInput, ConfigTarget}; +use crate::model::{PlaylistEntry, PlaylistGroup, XtreamCluster, XtreamPlaylistItem}; use crate::processing::parser::xtream; use crate::repository::xtream_repository::{rewrite_xtream_series_info_content, rewrite_xtream_vod_info_content, xtream_get_input_info}; use crate::repository::xtream_repository; use log::{info, warn}; use std::cmp::Ordering; use std::io::Error; -use crate::model::api_proxy::{ProxyUserCredentials}; -use crate::model::xtream_const; -use crate::utils::json_utils::get_string_from_serde_value; -use crate::utils::network::request; -use crate::utils::network::request::extract_extension_from_url; +use crate::model::{ProxyUserCredentials}; +use crate::utils::get_string_from_serde_value; +use crate::utils::request; +use crate::utils::request::extract_extension_from_url; #[inline] @@ -36,9 +35,9 @@ pub fn get_xtream_player_api_action_url(input: &ConfigInput, action: &str) -> Op pub fn get_xtream_player_api_info_url(input: &ConfigInput, cluster: XtreamCluster, stream_id: u32) -> Option { let (action, stream_id_field) = match cluster { - XtreamCluster::Live => (xtream_const::XC_ACTION_GET_LIVE_INFO, xtream_const::XC_LIVE_ID), - XtreamCluster::Video => (xtream_const::XC_ACTION_GET_VOD_INFO, xtream_const::XC_VOO_ID), - XtreamCluster::Series => (xtream_const::XC_ACTION_GET_SERIES_INFO, xtream_const::XC_SERIES_ID), + XtreamCluster::Live => (crate::model::XC_ACTION_GET_LIVE_INFO, crate::model::XC_LIVE_ID), + XtreamCluster::Video => (crate::model::XC_ACTION_GET_VOD_INFO, crate::model::XC_VOO_ID), + XtreamCluster::Series => (crate::model::XC_ACTION_GET_SERIES_INFO, crate::model::XC_SERIES_ID), }; get_xtream_player_api_action_url(input, action).map(|action_url| format!("{action_url}&{stream_id_field}={stream_id}")) } @@ -126,9 +125,9 @@ fn get_skip_cluster(input: &ConfigInput) -> Vec { } const ACTIONS: [(XtreamCluster, &str, &str); 3] = [ - (XtreamCluster::Live, xtream_const::XC_ACTION_GET_LIVE_CATEGORIES, xtream_const::XC_ACTION_GET_LIVE_STREAMS), - (XtreamCluster::Video, xtream_const::XC_ACTION_GET_VOD_CATEGORIES, xtream_const::XC_ACTION_GET_VOD_STREAMS), - (XtreamCluster::Series, xtream_const::XC_ACTION_GET_SERIES_CATEGORIES, xtream_const::XC_ACTION_GET_SERIES)]; + (XtreamCluster::Live, crate::model::XC_ACTION_GET_LIVE_CATEGORIES, crate::model::XC_ACTION_GET_LIVE_STREAMS), + (XtreamCluster::Video, crate::model::XC_ACTION_GET_VOD_CATEGORIES, crate::model::XC_ACTION_GET_VOD_STREAMS), + (XtreamCluster::Series, crate::model::XC_ACTION_GET_SERIES_CATEGORIES, crate::model::XC_ACTION_GET_SERIES)]; pub async fn get_xtream_playlist(client: Arc, input: &ConfigInput, working_dir: &str) -> (Vec, Vec) { diff --git a/src/utils/size_utils.rs b/src/utils/size_utils.rs index ca85621ef..3b4e3efb9 100644 --- a/src/utils/size_utils.rs +++ b/src/utils/size_utils.rs @@ -96,7 +96,7 @@ pub fn parse_to_kbps(input: &str) -> Result { #[cfg(test)] mod tests { - use crate::utils::size_utils::parse_to_kbps; + use crate::utils::parse_to_kbps; #[test] fn test_parse_kpbs() { diff --git a/src/utils/string_utils.rs b/src/utils/string_utils.rs index 8f4be50e2..0a3c69b9b 100644 --- a/src/utils/string_utils.rs +++ b/src/utils/string_utils.rs @@ -61,7 +61,7 @@ pub fn generate_random_string(length: usize) -> String { #[cfg(test)] mod test { use std::collections::HashSet; - use crate::utils::string_utils::generate_random_string; + use crate::utils::generate_random_string; #[test] fn test_generate_random_string() { diff --git a/src/utils/sys_utils.rs b/src/utils/sys_utils.rs index e42d4d173..a7a9972e9 100644 --- a/src/utils/sys_utils.rs +++ b/src/utils/sys_utils.rs @@ -6,7 +6,7 @@ macro_rules! exit { }}; } pub use exit; -use crate::utils::constants::CONSTANTS; +use crate::utils::CONSTANTS; #[cfg(target_os = "linux")] fn get_memory_usage_linux() -> std::io::Result {