diff --git a/src/api/api_utils.rs b/src/api/api_utils.rs index 42faee34a..70f368381 100644 --- a/src/api/api_utils.rs +++ b/src/api/api_utils.rs @@ -242,6 +242,14 @@ async fn get_streaming_options(app_state: &AppState, stream_url: &str, input_opt } } + +fn get_grace_period_millis(connection_permission: &UserConnectionPermission, stream_response_params: &StreamingOption, config_grace_period_millis: u64) -> u64 { + if config_grace_period_millis > 0 && + (matches!(stream_response_params, StreamingOption::GracePeriod(_, _)) // provider grace period + || connection_permission == &UserConnectionPermission::GracePeriod // user grace period + ) { config_grace_period_millis } else { 0 } +} + #[allow(clippy::too_many_arguments)] async fn create_stream_response_details(app_state: &AppState, stream_options: &StreamOptions, stream_url: &str, req_headers: &HeaderMap, input_opt: Option<&ConfigInput>, @@ -249,10 +257,7 @@ async fn create_stream_response_details(app_state: &AppState, stream_options: &S connection_permission: UserConnectionPermission) -> StreamDetails { let (mut provider_connection_guard, stream_response_params, input_headers) = get_streaming_options(app_state, stream_url, input_opt).await; let config_grace_period_millis = app_state.config.reverse_proxy.as_ref().and_then(|r| r.stream.as_ref()).map_or_else(default_grace_period_millis, |s| s.grace_period_millis); - let grace_period_millis = if config_grace_period_millis > 0 && - (matches!(stream_response_params, StreamingOption::GracePeriod(_, _)) // provider grace period - || connection_permission == UserConnectionPermission::GracePeriod // user grace period - ) { config_grace_period_millis } else { 0 }; + let grace_period_millis = get_grace_period_millis(&connection_permission, &stream_response_params, config_grace_period_millis); match stream_response_params { StreamingOption::Custom(provider_stream) => { let (stream, stream_info) = provider_stream; @@ -265,8 +270,8 @@ async fn create_stream_response_details(app_state: &AppState, stream_options: &S provider_connection_guard, } } - StreamingOption::Available(provider_name, request_url) - | StreamingOption::GracePeriod(provider_name, request_url) => { + StreamingOption::Available(provider_name, request_url) | + StreamingOption::GracePeriod(provider_name, request_url) => { let parsed_url = Url::parse(&request_url); let ((stream, stream_info), reconnect_flag) = if let Ok(url) = parsed_url { if stream_options.pipe_provider_stream { diff --git a/src/api/endpoints/hls_api.rs b/src/api/endpoints/hls_api.rs index 581fe0792..9c3fe01c2 100644 --- a/src/api/endpoints/hls_api.rs +++ b/src/api/endpoints/hls_api.rs @@ -7,11 +7,12 @@ use crate::model::config::{ConfigInput}; use crate::model::playlist::{PlaylistItemType, XtreamCluster}; use crate::processing::parser::hls::{rewrite_hls, RewriteHlsProps}; use crate::utils::network::request; -use crate::utils::network::request::{is_hls_url, replace_url_extension, sanitize_sensitive_info, HLS_EXT}; +use crate::utils::network::request::{is_hls_url, replace_url_extension, sanitize_sensitive_info}; use axum::response::IntoResponse; use log::{debug, error}; use serde::Deserialize; use std::sync::Arc; +use crate::utils::constants::HLS_EXT; use crate::utils::crypto_utils; #[derive(Debug, Deserialize)] diff --git a/src/api/endpoints/m3u_api.rs b/src/api/endpoints/m3u_api.rs index 2180c881c..6c0a0aa7a 100644 --- a/src/api/endpoints/m3u_api.rs +++ b/src/api/endpoints/m3u_api.rs @@ -5,9 +5,8 @@ use crate::api::model::request::UserApiRequest; use crate::model::api_proxy::{ProxyType, UserConnectionPermission}; use crate::model::config::TargetType; use crate::model::playlist::{FieldGetAccessor, PlaylistItemType, XtreamCluster}; -use crate::repository::m3u_playlist_iterator::{M3U_RESOURCE_PATH, M3U_STREAM_PATH}; use crate::repository::m3u_repository::{m3u_get_item_for_stream_id, m3u_load_rewrite_playlist}; -use crate::utils::network::request::{replace_url_extension, sanitize_sensitive_info, DASH_EXT, HLS_EXT}; +use crate::utils::network::request::{replace_url_extension, sanitize_sensitive_info}; use crate::utils::debug_if_enabled; use axum::response::IntoResponse; use bytes::Bytes; @@ -15,6 +14,8 @@ use futures::stream; use log::{debug, error}; use std::sync::Arc; use crate::api::model::streams::provider_stream::{create_custom_video_stream_response, CustomVideoStreamType}; +use crate::repository::storage_const; +use crate::utils::constants::{DASH_EXT, HLS_EXT}; async fn m3u_api( api_req: &UserApiRequest, @@ -154,7 +155,7 @@ macro_rules! register_m3u_stream_routes { ($router:expr, [$($path:expr),*]) => {{ $router $( - .route(&format!("/{M3U_STREAM_PATH}/{}/{{username}}/{{password}}/{{stream_id}}", $path), axum::routing::get(m3u_api_stream)) + .route(&format!("/{}/{}/{{username}}/{{password}}/{{stream_id}}", $path, storage_const::M3U_STREAM_PATH), axum::routing::get(m3u_api_stream)) // $cfg.service(web::resource(format!("/{M3U_STREAM_PATH}/{}/{{username}}/{{password}}/{{stream_id}}", $path)).route(web::get().to(m3u_api_stream))); )* }}; @@ -175,6 +176,6 @@ pub fn m3u_api_register() -> axum::Router> { let mut router = axum::Router::new(); router = register_m3u_api_routes!(router, ["get.php", "apiget", "m3u"]); register_m3u_stream_routes!(router, ["live", "movie", "series"]) - .route(&format!("/{M3U_STREAM_PATH}/{{username}}/{{password}}/{{stream_id}}"), axum::routing::get(m3u_api_stream)) - .route(&format!("/{M3U_RESOURCE_PATH}/{{username}}/{{password}}/{{stream_id}}/{{resource}}"), axum::routing::get(m3u_api_resource)) + .route(&format!("/{}/{{username}}/{{password}}/{{stream_id}}", storage_const::M3U_STREAM_PATH), axum::routing::get(m3u_api_stream)) + .route(&format!("/{}/{{username}}/{{password}}/{{stream_id}}/{{resource}}", storage_const::M3U_RESOURCE_PATH), axum::routing::get(m3u_api_resource)) } \ No newline at end of file diff --git a/src/api/endpoints/web_index.rs b/src/api/endpoints/web_index.rs index 37c34dc5a..9a0186b6b 100644 --- a/src/api/endpoints/web_index.rs +++ b/src/api/endpoints/web_index.rs @@ -6,12 +6,12 @@ use crate::auth::password::verify_password; use crate::auth::user::UserCredential; use axum::response::IntoResponse; use log::error; -use regex::Regex; use serde_json::json; use std::collections::HashMap; use std::path::{Path, PathBuf}; -use std::sync::{Arc, LazyLock}; +use std::sync::{Arc}; use tower::Service; +use crate::utils::constants::CONSTANTS; fn no_web_auth_token() -> impl axum::response::IntoResponse + Send { axum::Json(HashMap::from([("token", "authorized")])).into_response() @@ -83,8 +83,6 @@ async fn token_refresh( } } -static BASE_HREF: LazyLock = LazyLock::new(|| Regex::new(r#"(href|src)="/([^"]*)""#).unwrap()); - async fn index( axum::extract::State(app_state): axum::extract::State>, ) -> impl axum::response::IntoResponse + Send { @@ -92,7 +90,7 @@ async fn index( if let Some(web_ui_path) = &app_state.config.web_ui.as_ref().and_then(|c| c.path.as_ref()) { match tokio::fs::read_to_string(&path).await { Ok(content) => { - let mut new_content = BASE_HREF.replace_all(&content, |caps: ®ex::Captures| { + let mut new_content = CONSTANTS.base_href.replace_all(&content, |caps: ®ex::Captures| { format!(r#"{}="/{web_ui_path}/{}""#, &caps[1], &caps[2]) }).to_string(); diff --git a/src/api/endpoints/xtream_api.rs b/src/api/endpoints/xtream_api.rs index 42841de6c..ebece5951 100644 --- a/src/api/endpoints/xtream_api.rs +++ b/src/api/endpoints/xtream_api.rs @@ -9,24 +9,23 @@ use crate::api::model::app_state::AppState; use crate::api::model::request::UserApiRequest; use crate::api::model::streams::provider_stream::{create_custom_video_stream_response, CustomVideoStreamType}; use crate::api::model::xtream::XtreamAuthorizationResponse; -use crate::m3u_filter_error::info_err; 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::{INFO_RESOURCE_PREFIX, INFO_RESOURCE_PREFIX_EPISODE, PROP_BACKDROP_PATH, SEASON_RESOURCE_PREFIX}; +use crate::model::xtream_const; use crate::repository::playlist_repository::get_target_id_mapping; use crate::repository::storage::{get_target_storage_path, hex_encode}; -use crate::repository::xtream_repository::{TAG_EPISODES, TAG_INFO_DATA, TAG_SEASONS_DATA}; -use crate::repository::{user_repository, xtream_repository}; +use crate::repository::{storage_const, user_repository, xtream_repository}; use crate::utils::debug_if_enabled; 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, replace_url_extension, sanitize_sensitive_info, DASH_EXT, HLS_EXT}; -use crate::utils::network::xtream::{create_vod_info_from_item, ACTION_GET_LIVE_CATEGORIES, ACTION_GET_LIVE_STREAMS, ACTION_GET_SERIES, ACTION_GET_SERIES_CATEGORIES, ACTION_GET_SERIES_INFO, ACTION_GET_VOD_CATEGORIES, ACTION_GET_VOD_INFO, ACTION_GET_VOD_STREAMS}; +use crate::utils::network::request::{extract_extension_from_url, replace_url_extension, sanitize_sensitive_info}; +use crate::utils::network::xtream::create_vod_info_from_item; use crate::utils::network::{request, xtream}; use crate::utils::trace_if_enabled; use axum::http::{HeaderMap, StatusCode}; @@ -42,15 +41,7 @@ use std::fmt::{Display, Formatter}; use std::path::Path; use std::str::FromStr; use std::sync::Arc; - -const ACTION_GET_ACCOUNT_INFO: &str = "get_account_info"; -const ACTION_GET_EPG: &str = "get_epg"; -const ACTION_GET_SHORT_EPG: &str = "get_short_epg"; -const ACTION_GET_CATCHUP_TABLE: &str = "get_simple_data_table"; -const TAG_ID: &str = "id"; -const TAG_CATEGORY_ID: &str = "category_id"; -const TAG_STREAM_ID: &str = "stream_id"; -const TAG_EPG_LISTINGS: &str = "epg_listings"; +use crate::utils::constants::{DASH_EXT, HLS_EXT}; #[derive(Serialize, Deserialize, Debug)] pub(in crate::api) enum XtreamApiStreamContext { @@ -219,8 +210,8 @@ async fn xtream_player_api_stream( return redirect(&stream_url).into_response(); } - if is_hls_request || pli.item_type == PlaylistItemType::LiveDash { - let redirect_url = if is_hls_request { &replace_url_extension(&pli.url, HLS_EXT) } else { &replace_url_extension(&pli.url, DASH_EXT) }; + if is_hls_request || pli.item_type == PlaylistItemType::LiveDash { + let redirect_url = if is_hls_request { &replace_url_extension(&pli.url, HLS_EXT) } else { &replace_url_extension(&pli.url, DASH_EXT) }; debug_if_enabled!("Redirecting stream request to {}", sanitize_sensitive_info(redirect_url)); return redirect(redirect_url).into_response(); } @@ -327,8 +318,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(PROP_BACKDROP_PATH) { - return get_backdrop_path_value(field, info_data.get(PROP_BACKDROP_PATH)); + 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)); } else if let Some(Value::String(url)) = info_data.get(field) { return Some(url.to_string()); } @@ -348,21 +339,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(INFO_RESOURCE_PREFIX_EPISODE) { + let (field, possible_episode_id) = if let Some(field_name_with_episode_id) = resource.strip_prefix(xtream_const::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[INFO_RESOURCE_PREFIX.len()..], None) + (&resource[xtream_const::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(TAG_INFO_DATA) + doc.get(xtream_const::XC_TAG_INFO_DATA) } } XtreamCluster::Live => None, @@ -376,15 +367,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(TAG_EPISODES)?.as_object()?; + let episodes = doc.get(xtream_const::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(TAG_ID) { + if let Some(episode_id_value) = episode_doc.get(xtream_const::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(TAG_INFO_DATA); + return episode_doc.get(xtream_const::XC_TAG_INFO_DATA); } } } @@ -398,7 +389,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(TAG_ID) { + if let Some(season_id_value) = season_doc.get(xtream_const::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); @@ -421,10 +412,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(SEASON_RESOURCE_PREFIX) { + if let Some(field_name_with_season_id) = resource.strip_prefix(xtream_const::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(TAG_SEASONS_DATA), + XtreamCluster::Series => doc.get(xtream_const::XC_TAG_SEASONS_DATA), XtreamCluster::Video | XtreamCluster::Live => None, }; @@ -457,9 +448,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(INFO_RESOURCE_PREFIX) { + let stream_url = if resource.starts_with(xtream_const::XC_INFO_RESOURCE_PREFIX) { try_result_bad_request!(xtream_get_info_resource_url(&app_state.config, &pli, target, resource)) - } else if resource.starts_with(SEASON_RESOURCE_PREFIX) { + } else if resource.starts_with(xtream_const::XC_SEASON_RESOURCE_PREFIX) { try_result_bad_request!(xtream_get_season_resource_url(&app_state.config, &pli, target, resource)) } else { pli.get_field(resource) @@ -606,8 +597,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, ACTION_GET_SHORT_EPG) { - let mut info_url = format!("{action_url}&{TAG_STREAM_ID}={}", pli.provider_id); + 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 !(limit.is_empty() || limit.eq("0")) { info_url = format!("{info_url}&limit={limit}"); } @@ -634,21 +625,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 { - ACTION_GET_LIVE_CATEGORIES => xtream_repository::xtream_get_collection_path(config, target_name, xtream_repository::COL_CAT_LIVE), - ACTION_GET_VOD_CATEGORIES => xtream_repository::xtream_get_collection_path(config, target_name, xtream_repository::COL_CAT_VOD), - ACTION_GET_SERIES_CATEGORIES => xtream_repository::xtream_get_collection_path(config, target_name, xtream_repository::COL_CAT_SERIES), + 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), _ => Err(str_to_io_error("")) } { if let Some(file_path) = path { // load user bouquet let filter = match action { - ACTION_GET_LIVE_CATEGORIES => user_repository::user_get_bouquet_filter(config, &user.username, category_id, TargetType::Xtream, XtreamCluster::Live).await, - ACTION_GET_VOD_CATEGORIES => user_repository::user_get_bouquet_filter(config, &user.username, category_id, TargetType::Xtream, XtreamCluster::Video).await, - ACTION_GET_SERIES_CATEGORIES => user_repository::user_get_bouquet_filter(config, &user.username, category_id, TargetType::Xtream, XtreamCluster::Series).await, + 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, _ => None }; if let Some(flt) = filter { - return Some(serve_query(&file_path, &HashMap::from([(TAG_CATEGORY_ID, flt)])).into_response()); + return Some(serve_query(&file_path, &HashMap::from([(xtream_const::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 { @@ -665,30 +656,31 @@ 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, ACTION_GET_CATCHUP_TABLE).map(|action_url| format!("{action_url}&{TAG_STREAM_ID}={}&start={start}&end={end}", pli.provider_id))); + 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 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(TAG_EPG_LISTINGS).and_then(Value::as_array_mut)); + let epg_listings = try_option_bad_request!(doc.get_mut(xtream_const::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(TAG_ID).and_then(Value::as_str).and_then(|id| id.parse::().ok()) { + 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()) { 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(TAG_ID.to_string(), Value::String(virtual_id.to_string())); + epg_list_item.insert(xtream_const::XC_TAG_ID.to_string(), Value::String(virtual_id.to_string())); } } if let Err(err) = target_id_mapping.persist() { error!("Failed to write catchup id mapping {err}"); - return axum::http::StatusCode::BAD_REQUEST.into_response(); + return StatusCode::BAD_REQUEST.into_response(); } drop(file_lock); serde_json::to_string(&doc) .map_or_else( - |_| axum::http::StatusCode::BAD_REQUEST.into_response(), + |_| StatusCode::BAD_REQUEST.into_response(), |result| axum::response::Response::builder() - .status(axum::http::StatusCode::OK) + .status(StatusCode::OK) .header(axum::http::header::CONTENT_TYPE, mime::APPLICATION_JSON.to_string()) .body(result).unwrap().into_response()) } @@ -728,7 +720,7 @@ async fn xtream_player_api( } if user.permission_denied(app_state) { - return axum::http::StatusCode::FORBIDDEN.into_response(); + return StatusCode::FORBIDDEN.into_response(); } // Process specific playlist actions @@ -742,21 +734,21 @@ async fn xtream_player_api( }; match action { - ACTION_GET_ACCOUNT_INFO => { + xtream_const::XC_ACTION_GET_ACCOUNT_INFO => { return axum::response::Json(get_user_info(&user, app_state).await).into_response(); } - ACTION_GET_SERIES_INFO => { + xtream_const::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); } - ACTION_GET_VOD_INFO => { + xtream_const::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); } - ACTION_GET_EPG | ACTION_GET_SHORT_EPG => { + xtream_const::XC_ACTION_GET_EPG | xtream_const::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(); } - ACTION_GET_CATCHUP_TABLE => { + xtream_const::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); } _ => {} @@ -771,11 +763,11 @@ async fn xtream_player_api( } let result = match action { - ACTION_GET_LIVE_STREAMS => + xtream_const::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), - ACTION_GET_VOD_STREAMS => + xtream_const::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), - ACTION_GET_SERIES => + xtream_const::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/model/download.rs b/src/api/model/download.rs index 1ce54dc14..df5c4f77b 100644 --- a/src/api/model/download.rs +++ b/src/api/model/download.rs @@ -8,6 +8,7 @@ use serde::{Deserialize, Serialize}; use crate::model::config::VideoDownloadConfig; use crate::repository::storage::hash_string_as_hex; +use crate::utils::constants::FILENAME_TRIM_PATTERNS; /// File-Download information. #[derive(Clone)] @@ -60,8 +61,6 @@ fn get_download_directory(download_cfg: &VideoDownloadConfig, filestem: &str) -> } } -const FILENAME_TRIM_PATTERNS: &[char] = &['.', '-', '_']; - impl FileDownload { // TODO read header size info and restart support diff --git a/src/api/model/model_utils.rs b/src/api/model/model_utils.rs index 6b0585fe3..4b765e757 100644 --- a/src/api/model/model_utils.rs +++ b/src/api/model/model_utils.rs @@ -2,8 +2,7 @@ use reqwest::{StatusCode}; use std::collections::{HashSet}; use std::str::FromStr; use reqwest::header::HeaderMap; - -const MEDIA_STREAM_HEADERS: &[&str] = &["accept", "content-type", "content-length", "connection", "accept-ranges", "content-range", "vary", "transfer-encoding", "access-control-allow-origin", "access-control-allow-credentials", "icy-metadata"]; +use crate::utils::constants::{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/model/mod.rs b/src/model/mod.rs index 2ce38cc60..db400d89c 100644 --- a/src/model/mod.rs +++ b/src/model/mod.rs @@ -7,4 +7,5 @@ pub mod xmltv; pub mod xtream; pub mod healthcheck; pub mod playlist_categories; -pub mod hdhomerun_config; \ No newline at end of file +pub mod hdhomerun_config; +pub mod xtream_const; \ No newline at end of file diff --git a/src/model/playlist.rs b/src/model/playlist.rs index 9efe23d88..4fd4588a0 100644 --- a/src/model/playlist.rs +++ b/src/model/playlist.rs @@ -4,7 +4,8 @@ use std::str::FromStr; use crate::model::api_proxy::ProxyUserCredentials; use crate::model::config::{ConfigInput, ConfigTargetOptions}; use crate::model::xmltv::TVGuide; -use crate::model::xtream::{xtream_playlistitem_to_document, XtreamMappingOptions, PROP_BACKDROP_PATH, PROP_COVER}; +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 serde::{Deserialize, Serialize}; use serde_json::{Map, Value}; @@ -475,14 +476,14 @@ macro_rules! generate_field_accessor_impl_for_xtream_playlist_item { )* "epg_channel_id" | "epg_id" => self.epg_channel_id.clone(), _ => { - if field.starts_with(PROP_BACKDROP_PATH) || field == PROP_COVER { + if field.starts_with(xtream_const::XC_PROP_BACKDROP_PATH) || field == xtream_const::XC_PROP_COVER { let props = self.additional_properties.as_ref().and_then(|add_props| serde_json::from_str::>(add_props).ok()); return match props { Some(doc) => { - return if field == PROP_COVER { + return if field == xtream_const::XC_PROP_COVER { doc.get(field).and_then(|value| value.as_str().map(|s| s.to_string())) } else { - get_backdrop_path_value(field, doc.get(PROP_BACKDROP_PATH)) + get_backdrop_path_value(field, doc.get(xtream_const::XC_PROP_BACKDROP_PATH)) } } _=> None, diff --git a/src/model/xtream.rs b/src/model/xtream.rs index 70b4e758f..3ae25cb2a 100644 --- a/src/model/xtream.rs +++ b/src/model/xtream.rs @@ -7,31 +7,7 @@ use crate::utils::json_utils::{opt_string_or_number_u32, string_default_on_null, use serde::de::DeserializeOwned; use serde::{Deserialize, Deserializer, Serialize}; use serde_json::{Map, Value}; - -pub const INFO_RESOURCE_PREFIX: &str = "nfo_"; -pub const INFO_RESOURCE_PREFIX_EPISODE: &str = "nfo_ep_"; -pub const SEASON_RESOURCE_PREFIX: &str = "ssn_"; - -pub const PROP_BACKDROP_PATH: &str = "backdrop_path"; -pub const PROP_COVER: &str = "cover"; - -const LIVE_STREAM_FIELDS: &[&str] = &[]; - -const VIDEO_STREAM_FIELDS: &[&str] = &[ - "release_date", "cast", - "director", "episode_run_time", "genre", - "stream_type", "title", "year", "youtube_trailer", "trailer", - "plot", "rating_5based", "stream_icon", "container_extension" -]; - -const SERIES_STREAM_FIELDS: &[&str] = &[ - PROP_BACKDROP_PATH, "cast", PROP_COVER, "director", "episode_run_time", "genre", - "last_modified", "name", "plot", "rating_5based", - "stream_type", "title", "year", "youtube_trailer", "trailer" -]; - -const XTREAM_VOD_REWRITE_URL_PROPS: &[&str] = &[PROP_COVER]; - +use crate::model::xtream_const; fn deserialize_number_from_string<'de, D, T: DeserializeOwned>( deserializer: D, @@ -256,7 +232,7 @@ impl XtreamStream { let mut result = Map::new(); if let Some(bdpath) = self.backdrop_path.as_ref() { if !bdpath.is_empty() { - result.insert(String::from(PROP_BACKDROP_PATH), Value::Array(Vec::from([Value::String(String::from(bdpath.first()?))]))); + result.insert(String::from(xtream_const::XC_PROP_BACKDROP_PATH), Value::Array(Vec::from([Value::String(String::from(bdpath.first()?))]))); } } add_rc_str_property_if_exists!(result, self.tmdb, "tmdb"); @@ -543,7 +519,7 @@ fn append_prepared_series_properties(add_props: Option<&Map>, doc fn make_bdpath_resource_url(resource_url: &str, bd_path: &str, index: usize, field_prefix: &str) -> String { if bd_path.starts_with("http") { - format!("{resource_url}/{field_prefix}{PROP_BACKDROP_PATH}_{index}") + format!("{resource_url}/{field_prefix}{}_{index}", xtream_const::XC_PROP_BACKDROP_PATH) } else { bd_path.to_string() } @@ -613,36 +589,36 @@ pub fn xtream_playlistitem_to_document(pli: &XtreamPlaylistItem, url: &str, opti match pli.xtream_cluster { XtreamCluster::Live => { - append_mandatory_fields(&mut document, LIVE_STREAM_FIELDS); + append_mandatory_fields(&mut document, xtream_const::LIVE_STREAM_FIELDS); add_to_doc_str_property_if_not_exists!(document, "stream_type", Value::String(String::from("live"))); add_to_doc_str_property_if_not_exists!(document, "added", Value::String(chrono::Utc::now().timestamp().to_string())); } XtreamCluster::Video => { - append_mandatory_fields(&mut document, VIDEO_STREAM_FIELDS); + append_mandatory_fields(&mut document, xtream_const::VIDEO_STREAM_FIELDS); add_to_doc_str_property_if_not_exists!(document, "stream_type", Value::String(String::from("movie"))); add_to_doc_str_property_if_not_exists!(document, "added", Value::String(chrono::Utc::now().timestamp().to_string())); } XtreamCluster::Series => { append_prepared_series_properties(props.as_ref(), &mut document); - append_mandatory_fields(&mut document, SERIES_STREAM_FIELDS); + append_mandatory_fields(&mut document, xtream_const::SERIES_STREAM_FIELDS); append_release_date(&mut document); } } - rewrite_doc_urls(resource_url.as_ref(), &mut document, XTREAM_VOD_REWRITE_URL_PROPS, ""); + rewrite_doc_urls(resource_url.as_ref(), &mut document, xtream_const::XTREAM_VOD_REWRITE_URL_PROPS, ""); Value::Object(document) } pub fn rewrite_doc_urls(resource_url: Option<&String>, document: &mut Map, fields: &[&str], field_prefix: &str) { if let Some(rewrite_url) = resource_url { - if let Some(bdpath) = document.get(PROP_BACKDROP_PATH) { + if let Some(bdpath) = document.get(xtream_const::XC_PROP_BACKDROP_PATH) { match bdpath { Value::String(bd_path) => { - document.insert(PROP_BACKDROP_PATH.to_string(), Value::Array(vec![Value::String(make_bdpath_resource_url(rewrite_url.as_str(), bd_path, 0, field_prefix))])); + document.insert(xtream_const::XC_PROP_BACKDROP_PATH.to_string(), Value::Array(vec![Value::String(make_bdpath_resource_url(rewrite_url.as_str(), bd_path, 0, field_prefix))])); } Value::Array(bd_path) => { - document.insert(PROP_BACKDROP_PATH.to_string(), Value::Array( + document.insert(xtream_const::XC_PROP_BACKDROP_PATH.to_string(), Value::Array( bd_path.iter() .filter_map(|val| val.as_str()) .enumerate() diff --git a/src/model/xtream_const.rs b/src/model/xtream_const.rs new file mode 100644 index 000000000..a60049fc4 --- /dev/null +++ b/src/model/xtream_const.rs @@ -0,0 +1,63 @@ +pub const XC_LIVE_ID: &str = "live_id"; +pub const XC_VOO_ID: &str = "vod_id"; +pub const XC_SERIES_ID: &str = "series_id"; + +pub const XC_ACTION_GET_SERIES_INFO: &str = "get_series_info"; +pub const XC_ACTION_GET_VOD_INFO: &str = "get_vod_info"; +pub const XC_ACTION_GET_LIVE_INFO: &str = "get_live_info"; +pub const XC_ACTION_GET_SERIES: &str = "get_series"; +pub const XC_ACTION_GET_LIVE_CATEGORIES: &str = "get_live_categories"; +pub const XC_ACTION_GET_VOD_CATEGORIES: &str = "get_vod_categories"; +pub const XC_ACTION_GET_SERIES_CATEGORIES: &str = "get_series_categories"; +pub const XC_ACTION_GET_LIVE_STREAMS: &str = "get_live_streams"; +pub const XC_ACTION_GET_VOD_STREAMS: &str = "get_vod_streams"; +pub const XC_ACTION_GET_ACCOUNT_INFO: &str = "get_account_info"; +pub const XC_ACTION_GET_EPG: &str = "get_epg"; +pub const XC_ACTION_GET_SHORT_EPG: &str = "get_short_epg"; +pub const XC_ACTION_GET_CATCHUP_TABLE: &str = "get_simple_data_table"; +pub const XC_TAG_ID: &str = "id"; +pub const XC_TAG_CATEGORY_ID: &str = "category_id"; +pub const XC_TAG_STREAM_ID: &str = "stream_id"; +pub const XC_TAG_EPG_LISTINGS: &str = "epg_listings"; +pub const XC_INFO_RESOURCE_PREFIX: &str = "nfo_"; +pub const XC_INFO_RESOURCE_PREFIX_EPISODE: &str = "nfo_ep_"; +pub const XC_SEASON_RESOURCE_PREFIX: &str = "ssn_"; +pub const XC_PROP_BACKDROP_PATH: &str = "backdrop_path"; +pub const XC_PROP_COVER: &str = "cover"; +pub const XC_TAG_CATEGORY_IDS: &str = "category_ids"; +pub const XC_TAG_CATEGORY_NAME: &str = "category_name"; +pub const XC_TAG_DIRECT_SOURCE: &str = "direct_source"; +pub const XC_TAG_PARENT_ID: &str = "parent_id"; +pub const XC_TAG_MOVIE_DATA: &str = "movie_data"; +pub const XC_TAG_INFO_DATA: &str = "info"; +pub const XC_TAG_SEASONS_DATA: &str = "seasons"; +pub const XC_TAG_EPISODES: &str = "episodes"; +pub const XC_TAG_VOD_INFO_INFO: &str = "info"; +pub const XC_TAG_VOD_INFO_MOVIE_DATA: &str = "movie_data"; +pub const XC_TAG_VOD_INFO_TMDB_ID: &str = "tmdb_id"; +pub const XC_TAG_VOD_INFO_STREAM_ID: &str = "stream_id"; +pub const XC_TAG_VOD_INFO_ADDED: &str = "added"; + +pub const XC_FILE_SERIES_INFO: &str = "xtream_series_info"; +pub const XC_FILE_VOD_INFO: &str = "xtream_vod_info"; +pub const XC_FILE_SERIES_EPISODE_RECORD: &str = "series_episode_record"; +pub const XC_TAG_SERIES_INFO_LAST_MODIFIED: &str = "last_modified"; + + +pub(in crate::model) const LIVE_STREAM_FIELDS: &[&str] = &[]; + +pub(in crate::model) const VIDEO_STREAM_FIELDS: &[&str] = &[ + "release_date", "cast", + "director", "episode_run_time", "genre", + "stream_type", "title", "year", "youtube_trailer", "trailer", + "plot", "rating_5based", "stream_icon", "container_extension" +]; + +pub(in crate::model) const SERIES_STREAM_FIELDS: &[&str] = &[ + XC_PROP_BACKDROP_PATH, "cast", XC_PROP_COVER, "director", "episode_run_time", "genre", + "last_modified", "name", "plot", "rating_5based", + "stream_type", "title", "year", "youtube_trailer", "trailer" +]; + +pub(in crate::model) const XTREAM_VOD_REWRITE_URL_PROPS: &[&str] = &[XC_PROP_COVER]; + diff --git a/src/processing/parser/hls.rs b/src/processing/parser/hls.rs index 71235e0d1..964547d60 100644 --- a/src/processing/parser/hls.rs +++ b/src/processing/parser/hls.rs @@ -39,6 +39,7 @@ pub fn rewrite_hls(user: &ProxyUserCredentials, props: &RewriteHlsProps) -> Stri let mut result = Vec::new(); for line in props.content.lines() { if line.starts_with('#') { + result.push(line.to_string()); } else if let Ok(token) = if line.starts_with("http") { encrypt_text(props.secret, line) diff --git a/src/processing/parser/xmltv.rs b/src/processing/parser/xmltv.rs index 1731b0680..5b8e93897 100644 --- a/src/processing/parser/xmltv.rs +++ b/src/processing/parser/xmltv.rs @@ -12,23 +12,8 @@ use std::collections::{HashMap, HashSet}; use std::mem; use std::path::Path; use std::sync::atomic::{AtomicBool, Ordering}; -use std::sync::{Arc, LazyLock, Mutex, RwLock}; - -static COUNTRY_CODE: LazyLock> = LazyLock::new(|| vec![ - "af", "al", "dz", "ad", "ao", "ag", "ar", "am", "au", "at", "az", "bs", "bh", "bd", "bb", "by", - "be", "bz", "bj", "bt", "bo", "ba", "bw", "br", "bn", "bg", "bf", "bi", "cv", "kh", "cm", "ca", - "cf", "td", "cl", "cn", "co", "km", "cg", "cr", "hr", "cu", "cy", "cz", "cd", "dk", "dj", "dm", - "do", "tl", "ec", "eg", "sv", "gq", "er", "ee", "sz", "et", "fj", "fi", "fr", "ga", "gm", "ge", - "de", "gh", "gr", "gd", "gt", "gn", "gw", "gy", "ht", "hn", "hu", "is", "in", "id", "ir", "iq", - "ie", "il", "it", "ci", "jm", "jp", "jo", "kz", "ke", "ki", "kp", "kr", "kw", "kg", "la", "lv", - "lb", "ls", "lr", "ly", "li", "lt", "lu", "mg", "mw", "my", "mv", "ml", "mt", "mh", "mr", "mu", - "mx", "fm", "md", "mc", "mn", "me", "ma", "mz", "mm", "na", "nr", "np", "nl", "nz", "ni", "ne", - "ng", "mk", "no", "om", "pk", "pw", "pa", "pg", "py", "pe", "ph", "pl", "pt", "qa", "ro", "ru", - "rw", "kn", "lc", "vc", "ws", "sm", "st", "sa", "sn", "rs", "sc", "sl", "sg", "sk", "si", "sb", - "so", "za", "ss", "es", "lk", "sd", "sr", "se", "ch", "sy", "tw", "tj", "tz", "th", "tg", "to", - "tt", "tn", "tr", "tm", "tv", "ug", "ua", "ae", "gb", "us", "uy", "uz", "vu", "va", "ve", "vn", - "ye", "zm", "zw", -].into_iter().collect::>()); +use std::sync::{Arc, Mutex, RwLock}; +use crate::utils::constants::CONSTANTS; fn split_by_first_match<'a>(input: &'a str, delimiters: &[char]) -> (Option<&'a str>, &'a str) { for delim in delimiters { @@ -38,7 +23,7 @@ fn split_by_first_match<'a>(input: &'a str, delimiters: &[char]) -> (Option<&'a if !right.is_empty() { let prefix = left.trim(); // when we used anything as prefix the result was bad - if COUNTRY_CODE.contains(&prefix) { + if CONSTANTS.country_codes.contains(&prefix) { return (Some(prefix), right.trim()); } } diff --git a/src/processing/parser/xtream.rs b/src/processing/parser/xtream.rs index b30af6e77..f540dfd42 100644 --- a/src/processing/parser/xtream.rs +++ b/src/processing/parser/xtream.rs @@ -3,10 +3,11 @@ 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, ACTION_GET_SERIES_INFO}; +use crate::utils::network::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()) { @@ -91,7 +92,7 @@ pub fn get_xtream_url(xtream_cluster: XtreamCluster, url: &str, format!("{url}/movie/{username}/{password}/{stream_id}.{ext}") } XtreamCluster::Series => - format!("{}&action={ACTION_GET_SERIES_INFO}&series_id={stream_id}", get_xtream_stream_url_base(url, username, password)) + format!("{}&action={}&series_id={stream_id}", get_xtream_stream_url_base(url, username, password), xtream_const::XC_ACTION_GET_SERIES_INFO) }; stream_base_url } diff --git a/src/processing/processor/xtream.rs b/src/processing/processor/xtream.rs index 7b2f6215d..c078c07b1 100644 --- a/src/processing/processor/xtream.rs +++ b/src/processing/processor/xtream.rs @@ -9,13 +9,9 @@ use std::fs::File; use std::io::{BufWriter, Write}; use std::path::PathBuf; use std::sync::Arc; - -const FILE_SERIES_INFO: &str = "xtream_series_info"; -const FILE_VOD_INFO: &str = "xtream_vod_info"; -const FILE_SUFFIX_WAL: &str = "wal"; -const FILE_SERIES_EPISODE_RECORD: &str = "series_episode_record"; - +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; @@ -51,7 +47,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!("{FILE_SERIES_EPISODE_RECORD}.{FILE_SUFFIX_WAL}")); + let info_path = storage_path.join(format!("{}.{}", xtream_const::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)) } @@ -64,11 +60,11 @@ 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(FILE_VOD_INFO), - XtreamCluster::Series => Some(FILE_SERIES_INFO) + XtreamCluster::Video => Some(xtream_const::XC_FILE_VOD_INFO), + XtreamCluster::Series => Some(xtream_const::XC_FILE_SERIES_INFO) } { - let content_path = storage_path.join(format!("{file_prefix}_content.{FILE_SUFFIX_WAL}")); - let info_path = storage_path.join(format!("{file_prefix}_record.{FILE_SUFFIX_WAL}")); + 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)); let content_file = append_or_crate_file(&content_path).ok()?; let info_file = append_or_crate_file(&info_path).ok()?; return Some((content_file, info_file, content_path, info_path)); diff --git a/src/processing/processor/xtream_series.rs b/src/processing/processor/xtream_series.rs index 0154d05db..e2c5441e5 100644 --- a/src/processing/processor/xtream_series.rs +++ b/src/processing/processor/xtream_series.rs @@ -16,11 +16,10 @@ 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; -const TAG_SERIES_INFO_LAST_MODIFIED: &str = "last_modified"; - create_resolve_options_function_for_xtream_target!(series); async fn read_processed_series_info_ids(cfg: &Config, errors: &mut Vec, fpl: &FetchedPlaylist<'_>) -> HashMap { @@ -53,7 +52,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, TAG_SERIES_INFO_LAST_MODIFIED) + should_update_info(pli, processed_provider_ids, xtream_const::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 7aade4229..04fd16cf5 100644 --- a/src/processing/processor/xtream_vod.rs +++ b/src/processing/processor/xtream_vod.rs @@ -13,14 +13,9 @@ 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; -const TAG_VOD_INFO_INFO: &str = "info"; -const TAG_VOD_INFO_MOVIE_DATA: &str = "movie_data"; -const TAG_VOD_INFO_TMDB_ID: &str = "tmdb_id"; -const TAG_VOD_INFO_STREAM_ID: &str = "stream_id"; -const TAG_VOD_INFO_ADDED: &str = "added"; - create_resolve_options_function_for_xtream_target!(vod); async fn read_processed_vod_info_ids(cfg: &Config, errors: &mut Vec, fpl: &FetchedPlaylist<'_>) -> HashMap { @@ -30,18 +25,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(TAG_VOD_INFO_MOVIE_DATA)?.as_object()?; + let movie_data = doc.get(xtream_const::XC_TAG_VOD_INFO_MOVIE_DATA)?.as_object()?; let provider_id = get_u32_from_serde_value( - movie_data.get(TAG_VOD_INFO_STREAM_ID)?, + movie_data.get(xtream_const::XC_TAG_VOD_INFO_STREAM_ID)?, )?; let added = movie_data - .get(TAG_VOD_INFO_ADDED) + .get(xtream_const::XC_TAG_VOD_INFO_ADDED) .and_then(get_u64_from_serde_value) .unwrap_or(0); - let tmdb_id = doc.get(TAG_VOD_INFO_INFO)?.as_object() - .and_then(|info| info.get(TAG_VOD_INFO_TMDB_ID)) + 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)) .and_then(get_u32_from_serde_value) .unwrap_or(0); @@ -63,7 +58,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, TAG_VOD_INFO_ADDED) + should_update_info(pli, processed_provider_ids, xtream_const::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/kodi_repository.rs b/src/repository/kodi_repository.rs index a405af5b6..6b249d300 100644 --- a/src/repository/kodi_repository.rs +++ b/src/repository/kodi_repository.rs @@ -7,9 +7,7 @@ use crate::model::playlist::{ }; use crate::model::xtream::XtreamSeriesEpisode; use crate::repository::bplustree::BPlusTree; -use crate::repository::storage::{ - ensure_target_storage_path, get_input_storage_path, hash_bytes, FILE_SUFFIX_DB, -}; +use crate::repository::storage::{ensure_target_storage_path, get_input_storage_path, hash_bytes}; use crate::repository::xtream_repository::{xtream_get_record_file_path, InputVodInfoRecord}; use crate::utils::file::file_lock_manager::FileReadGuard; use crate::utils::file::file_utils; @@ -21,27 +19,11 @@ use regex::Regex; use serde::Serialize; use std::collections::{HashMap, HashSet, VecDeque}; use std::path::{Path, PathBuf}; -use std::sync::{Arc, LazyLock}; +use std::sync::{Arc}; use tokio::fs::{create_dir_all, remove_dir, remove_file, File}; use tokio::io::{AsyncBufReadExt, AsyncReadExt, AsyncWriteExt, BufReader, BufWriter}; - -const FILE_STRM: &str = "strm"; - -struct KodiStyle { - year: Regex, - season: Regex, - episode: Regex, - whitespace: Regex, - alphanumeric: Regex, -} - -static KODI_STYLE: LazyLock = LazyLock::new(|| KodiStyle { - season: Regex::new(r"[Ss]\d{1,2}").unwrap(), - episode: Regex::new(r"[Ee]\d{1,2}").unwrap(), - year: Regex::new(r"\d{4}").unwrap(), - whitespace: Regex::new(r"\s+").unwrap(), - alphanumeric: Regex::new(r"[^\w\s]").unwrap(), -}); +use crate::repository::storage_const; +use crate::utils::constants::{KodiStyle, CONSTANTS}; fn sanitize_for_filename(text: &str, underscore_whitespace: bool) -> String { text.trim() @@ -350,7 +332,7 @@ async fn get_tmdb_value( } pub fn strm_get_file_paths(target_path: &Path) -> PathBuf { - target_path.join(PathBuf::from(format!("{FILE_STRM}.{FILE_SUFFIX_DB}"))) + target_path.join(PathBuf::from(format!("{}.{}", storage_const::FILE_STRM, storage_const::FILE_SUFFIX_DB))) } #[derive(Serialize)] @@ -547,7 +529,7 @@ async fn prepare_strm_files( kodi_style_rename( cfg, &strm_item_info, - &KODI_STYLE, + &CONSTANTS.kodi_style, &mut input_tmdb_indexes, underscore_whitespace, ).await diff --git a/src/repository/m3u_playlist_iterator.rs b/src/repository/m3u_playlist_iterator.rs index 3b2d31a50..1c01a6197 100644 --- a/src/repository/m3u_playlist_iterator.rs +++ b/src/repository/m3u_playlist_iterator.rs @@ -7,12 +7,10 @@ use crate::model::playlist::{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; -pub const M3U_STREAM_PATH: &str = "m3u-stream"; -pub const M3U_RESOURCE_PATH: &str = "resource/m3u"; - #[allow(clippy::struct_excessive_bools)] pub struct M3uPlaylistIterator { reader: IndexedDocumentIterator, @@ -89,10 +87,10 @@ impl M3uPlaylistIterator { } fn get_stream_url(&self, m3u_pli: &M3uPlaylistItem, typed: bool) -> String { - self.get_rewritten_url(m3u_pli, typed, M3U_STREAM_PATH) + self.get_rewritten_url(m3u_pli, typed, storage_const::M3U_STREAM_PATH) } fn get_resource_url(&self, m3u_pli: &M3uPlaylistItem) -> String { - self.get_rewritten_url(m3u_pli, false, M3U_RESOURCE_PATH) + self.get_rewritten_url(m3u_pli, false, storage_const::M3U_RESOURCE_PATH) } fn get_next(&mut self) -> Option<(M3uPlaylistItem, bool)> { diff --git a/src/repository/m3u_repository.rs b/src/repository/m3u_repository.rs index 8d68bc5bc..dd3c78714 100644 --- a/src/repository/m3u_repository.rs +++ b/src/repository/m3u_repository.rs @@ -5,7 +5,7 @@ use crate::model::config::{Config, ConfigTarget, M3uTargetOutput}; use crate::model::playlist::{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, FILE_SUFFIX_DB, FILE_SUFFIX_INDEX}; +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; @@ -14,8 +14,8 @@ use std::fs::File; use std::io::{Error, Write}; use std::path::{Path, PathBuf}; use std::sync::Arc; +use crate::repository::storage_const; -const FILE_M3U: &str = "m3u"; macro_rules! cant_write_result { ($path:expr, $err:expr) => { create_m3u_filter_error!(M3uFilterErrorKind::Notify, "failed to write m3u playlist: {} - {}", $path.to_str().unwrap() ,$err) @@ -23,13 +23,13 @@ macro_rules! cant_write_result { } pub fn m3u_get_file_paths(target_path: &Path) -> (PathBuf, PathBuf) { - let m3u_path = target_path.join(PathBuf::from(format!("{FILE_M3U}.{FILE_SUFFIX_DB}"))); - let index_path = target_path.join(PathBuf::from(format!("{FILE_M3U}.{FILE_SUFFIX_INDEX}"))); + let m3u_path = target_path.join(PathBuf::from(format!("{}.{}", storage_const::FILE_M3U, storage_const::FILE_SUFFIX_DB))); + let index_path = target_path.join(PathBuf::from(format!("{}.{}", storage_const::FILE_M3U, storage_const::FILE_SUFFIX_INDEX))); (m3u_path, index_path) } pub fn m3u_get_epg_file_path(target_path: &Path) -> PathBuf { - let path = target_path.join(PathBuf::from(format!("{FILE_M3U}.{FILE_SUFFIX_DB}"))); + let path = target_path.join(PathBuf::from(format!("{}.{}", storage_const::FILE_M3U, storage_const::FILE_SUFFIX_DB))); file_utils::add_prefix_to_filename(&path, "epg_", Some("xml")) } diff --git a/src/repository/mod.rs b/src/repository/mod.rs index fdf7304b2..5e217a718 100644 --- a/src/repository/mod.rs +++ b/src/repository/mod.rs @@ -3,7 +3,6 @@ pub mod target_id_mapping; pub mod bplustree; mod indexed_document; pub use indexed_document::IndexedDocumentReader; - pub mod playlist_repository; pub mod m3u_repository; pub mod xtream_repository; @@ -12,4 +11,5 @@ pub mod kodi_repository; pub mod m3u_playlist_iterator; pub mod xtream_playlist_iterator; pub mod user_repository; +pub mod storage_const; diff --git a/src/repository/storage.rs b/src/repository/storage.rs index 92a65df51..fe56ea41d 100644 --- a/src/repository/storage.rs +++ b/src/repository/storage.rs @@ -4,13 +4,9 @@ use crate::m3u_filter_error::{M3uFilterError, M3uFilterErrorKind}; use crate::model::config::{Config}; use crate::model::playlist::UUIDType; use crate::m3u_filter_error::{notify_err}; +use crate::repository::storage_const; use crate::utils::file::file_utils; -pub(in crate::repository) const FILE_SUFFIX_DB: &str = "db"; -pub(in crate::repository) const FILE_SUFFIX_INDEX: &str = "idx"; - -const FILE_ID_MAPPING: &str = "id_mapping.db"; - #[inline] pub fn hash_bytes(bytes: &[u8]) -> UUIDType { blake3::hash(bytes).into() @@ -55,7 +51,7 @@ pub fn hash_string_as_hex(url: &str) -> String { } pub(in crate::repository) fn get_target_id_mapping_file(target_path: &Path) -> PathBuf { - target_path.join(PathBuf::from(FILE_ID_MAPPING)) + target_path.join(PathBuf::from(storage_const::FILE_ID_MAPPING)) } pub fn ensure_target_storage_path(cfg: &Config, target_name: &str) -> Result { diff --git a/src/repository/storage_const.rs b/src/repository/storage_const.rs new file mode 100644 index 000000000..42d3ddaad --- /dev/null +++ b/src/repository/storage_const.rs @@ -0,0 +1,30 @@ +pub const FILE_EPG: &str = "epg.xml"; +pub(in crate::repository) const FILE_SUFFIX_DB: &str = "db"; +pub(in crate::repository) const FILE_SUFFIX_INDEX: &str = "idx"; +pub(in crate::repository) const FILE_ID_MAPPING: &str = "id_mapping.db"; +pub(in crate::repository) const FILE_STRM: &str = "strm"; +pub(in crate::repository) const FILE_M3U: &str = "m3u"; + +pub const FILE_SUFFIX_WAL: &str = "wal"; +pub const M3U_STREAM_PATH: &str = "m3u-stream"; +pub const M3U_RESOURCE_PATH: &str = "resource/m3u"; + +pub const COL_CAT_LIVE: &str = "cat_live"; +pub const COL_CAT_SERIES: &str = "cat_series"; +pub const COL_CAT_VOD: &str = "cat_vod"; + +pub(in crate::repository) const USER_LIVE_BOUQUET: &str = "live_bouquet.json"; +pub(in crate::repository) const USER_VOD_BOUQUET: &str = "vod_bouquet.json"; +pub(in crate::repository) const USER_SERIES_BOUQUET: &str = "series_bouquet.json"; +pub(in crate::repository) const API_USER_DB_FILE: &str = "api_user.db"; + + +pub(in crate::repository) const FILE_SERIES_INFO: &str = "series_info"; +pub(in crate::repository) const FILE_VOD_INFO: &str = "vod_info"; +pub(in crate::repository) const FILE_VOD_INFO_RECORD: &str = "vod_info_record"; +pub(in crate::repository) const FILE_SERIES_INFO_RECORD: &str = "series_info_record"; +pub(in crate::repository) const FILE_SERIES_EPISODE_RECORD: &str = "series_episode_record"; +pub(in crate::repository) const FILE_SERIES: &str = "series"; +pub(in crate::repository) const PATH_XTREAM: &str = "xtream"; +pub(in crate::repository) const INFO_REWRITE_FIELDS: &[&str] = &["cover_big", "cover", "cover_tmdb", "movie_image", "tmdb_url", "overview", "kinopoisk_url"]; + diff --git a/src/repository/user_repository.rs b/src/repository/user_repository.rs index 1489da887..1d4f10644 100644 --- a/src/repository/user_repository.rs +++ b/src/repository/user_repository.rs @@ -4,6 +4,7 @@ use crate::model::playlist::XtreamCluster; use crate::model::playlist_categories::{PlaylistBouquetDto, TargetBouquetDto}; use crate::model::xtream::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; @@ -13,11 +14,6 @@ use std::collections::{HashMap, HashSet}; use std::io::Error; use std::path::{Path, PathBuf}; -const USER_LIVE_BOUQUET: &str = "live_bouquet.json"; -const USER_VOD_BOUQUET: &str = "vod_bouquet.json"; -const USER_SERIES_BOUQUET: &str = "series_bouquet.json"; -const API_USER_DB_FILE: &str = "api_user.db"; - // This is a Helper class to store all user into one Database file. // For the Config files we keep the old structure where a user is assigned to a target. // But for storing inside one db file it is easier to store the target next to the user. @@ -52,7 +48,7 @@ impl StoredProxyUserCredentials { exp_date: proxy.exp_date, max_connections: if proxy.max_connections > 0 { Some(proxy.max_connections) } else { None }, status: proxy.status.clone(), - ui_enabled: proxy.ui_enabled + ui_enabled: proxy.ui_enabled, } } @@ -68,14 +64,14 @@ impl StoredProxyUserCredentials { exp_date: stored.exp_date, max_connections: stored.max_connections.unwrap_or_default(), status: stored.status.clone(), - ui_enabled: stored.ui_enabled + ui_enabled: stored.ui_enabled, } } } pub fn get_api_user_db_path(cfg: &Config) -> PathBuf { - PathBuf::from(&cfg.t_config_path).join(API_USER_DB_FILE) + PathBuf::from(&cfg.t_config_path).join(storage_const::API_USER_DB_FILE) } @@ -103,7 +99,7 @@ pub fn merge_api_user(cfg: &Config, target_users: &[TargetUser]) -> Result {} @@ -162,15 +158,15 @@ fn ensure_user_storage_path(cfg: &Config, username: &str) -> Option { } fn user_get_live_bouquet_path(user_storage_path: &Path, target: TargetType) -> PathBuf { - user_storage_path.join(PathBuf::from(format!("{}_{USER_LIVE_BOUQUET}", target.to_string().to_lowercase()))) + user_storage_path.join(PathBuf::from(format!("{}_{}", target.to_string().to_lowercase(), storage_const::USER_LIVE_BOUQUET))) } fn user_get_vod_bouquet_path(user_storage_path: &Path, target: TargetType) -> PathBuf { - user_storage_path.join(PathBuf::from(format!("{}_{USER_VOD_BOUQUET}", target.to_string().to_lowercase()))) + user_storage_path.join(PathBuf::from(format!("{}_{}", target.to_string().to_lowercase(), storage_const::USER_VOD_BOUQUET))) } fn user_get_series_bouquet_path(user_storage_path: &Path, target: TargetType) -> PathBuf { - user_storage_path.join(PathBuf::from(format!("{}_{USER_SERIES_BOUQUET}", target.to_string().to_lowercase()))) + user_storage_path.join(PathBuf::from(format!("{}_{}", target.to_string().to_lowercase(), storage_const::USER_SERIES_BOUQUET))) } async fn save_xtream_user_bouquet_for_target(config: &Config, target_name: &str, storage_path: &Path, cluster: XtreamCluster, bouquet: Option<&Vec>) -> Result<(), Error> { diff --git a/src/repository/xtream_repository.rs b/src/repository/xtream_repository.rs index c94139e2d..49a300b70 100644 --- a/src/repository/xtream_repository.rs +++ b/src/repository/xtream_repository.rs @@ -1,3 +1,4 @@ +use crate::model::xtream_const; use crate::utils::file::file_lock_manager::FileReadGuard; use log::error; use serde_json::{json, Map, Value}; @@ -16,11 +17,12 @@ use crate::m3u_filter_error::{M3uFilterError, M3uFilterErrorKind, str_to_io_erro use crate::model::api_proxy::{ProxyType, 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, INFO_RESOURCE_PREFIX, INFO_RESOURCE_PREFIX_EPISODE, SEASON_RESOURCE_PREFIX}; +use crate::model::xtream::{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; -use crate::repository::storage::{get_input_storage_path, get_target_id_mapping_file, get_target_storage_path, FILE_SUFFIX_DB, FILE_SUFFIX_INDEX}; +use crate::repository::storage::{get_input_storage_path, get_target_id_mapping_file, get_target_storage_path}; +use crate::repository::storage_const; use crate::repository::target_id_mapping::{VirtualIdRecord}; use crate::utils::file::file_utils::open_readonly_file; use crate::utils::hash_utils::generate_playlist_uuid; @@ -28,32 +30,6 @@ use crate::utils::json_utils::{get_u32_from_serde_value, json_iter_array, json_w use crate::utils::bincode_utils::{bincode_deserialize}; use crate::repository::xtream_playlist_iterator::XtreamPlaylistJsonIterator; -pub static COL_CAT_LIVE: &str = "cat_live"; -pub static COL_CAT_SERIES: &str = "cat_series"; -pub static COL_CAT_VOD: &str = "cat_vod"; -const FILE_SERIES_INFO: &str = "series_info"; -const FILE_VOD_INFO: &str = "vod_info"; -const FILE_VOD_INFO_RECORD: &str = "vod_info_record"; -const FILE_SERIES_INFO_RECORD: &str = "series_info_record"; -const FILE_SERIES_EPISODE_RECORD: &str = "series_episode_record"; -const FILE_SERIES: &str = "series"; -pub const FILE_EPG: &str = "epg.xml"; -const PATH_XTREAM: &str = "xtream"; -const TAG_CATEGORY_ID: &str = "category_id"; -const TAG_CATEGORY_IDS: &str = "category_ids"; -const TAG_CATEGORY_NAME: &str = "category_name"; -const TAG_DIRECT_SOURCE: &str = "direct_source"; -const TAG_PARENT_ID: &str = "parent_id"; -const TAG_MOVIE_DATA: &str = "movie_data"; -pub const TAG_INFO_DATA: &str = "info"; -pub const TAG_SEASONS_DATA: &str = "seasons"; -const TAG_STREAM_ID: &str = "stream_id"; -const TAG_ID: &str = "id"; -pub const TAG_EPISODES: &str = "episodes"; - -const INFO_REWRITE_FIELDS: &[&str] = &["cover_big", "cover", "cover_tmdb", "movie_image", "tmdb_url", "overview", "kinopoisk_url"]; - - macro_rules! cant_write_result { ($path:expr, $err:expr) => { create_m3u_filter_error!( @@ -106,12 +82,12 @@ pub fn xtream_get_info_file_paths( cluster: XtreamCluster, ) -> Option<(PathBuf, PathBuf)> { if cluster == XtreamCluster::Series { - let xtream_path = storage_path.join(format!("{FILE_SERIES_INFO}.{FILE_SUFFIX_DB}")); - let index_path = storage_path.join(format!("{FILE_SERIES_INFO}.{FILE_SUFFIX_INDEX}")); + let xtream_path = storage_path.join(format!("{}.{}", storage_const::FILE_SERIES_INFO, storage_const::FILE_SUFFIX_DB)); + let index_path = storage_path.join(format!("{}.{}", storage_const::FILE_SERIES_INFO, storage_const::FILE_SUFFIX_INDEX)); return Some((xtream_path, index_path)); } else if cluster == XtreamCluster::Video { - let xtream_path = storage_path.join(format!("{FILE_VOD_INFO}.{FILE_SUFFIX_DB}")); - let index_path = storage_path.join(format!("{FILE_VOD_INFO}.{FILE_SUFFIX_INDEX}")); + let xtream_path = storage_path.join(format!("{}.{}", storage_const::FILE_VOD_INFO, storage_const::FILE_SUFFIX_DB)); + let index_path = storage_path.join(format!("{}.{}", storage_const::FILE_VOD_INFO, storage_const::FILE_SUFFIX_INDEX)); return Some((xtream_path, index_path)); } None @@ -119,9 +95,9 @@ pub fn xtream_get_info_file_paths( pub fn xtream_get_record_file_path(storage_path: &Path, item_type: PlaylistItemType) -> Option { match item_type { - PlaylistItemType::Video => Some(storage_path.join(format!("{FILE_VOD_INFO_RECORD}.{FILE_SUFFIX_DB}"))), - PlaylistItemType::SeriesInfo => Some(storage_path.join(format!("{FILE_SERIES_INFO_RECORD}.{FILE_SUFFIX_DB}"))), - PlaylistItemType::Series => Some(storage_path.join(format!("{FILE_SERIES_EPISODE_RECORD}.{FILE_SUFFIX_DB}"))), + PlaylistItemType::Video => Some(storage_path.join(format!("{}.{}", storage_const::FILE_VOD_INFO_RECORD, storage_const::FILE_SUFFIX_DB))), + PlaylistItemType::SeriesInfo => Some(storage_path.join(format!("{}.{}", storage_const::FILE_SERIES_INFO_RECORD, storage_const::FILE_SUFFIX_DB))), + PlaylistItemType::Series => Some(storage_path.join(format!("{}.{}", storage_const::FILE_SERIES_EPISODE_RECORD, storage_const::FILE_SUFFIX_DB))), _ => None, } } @@ -164,15 +140,15 @@ fn get_map_item_as_str(map: &serde_json::Map, key: &str) -> Optio fn load_old_category_ids(path: &Path) -> (u32, HashMap) { let mut result: HashMap = HashMap::new(); let mut max_id: u32 = 0; - for (cluster, cat) in [(XtreamCluster::Live, COL_CAT_LIVE), (XtreamCluster::Video, COL_CAT_VOD), (XtreamCluster::Series, COL_CAT_SERIES)] { + for (cluster, cat) in [(XtreamCluster::Live, storage_const::COL_CAT_LIVE), (XtreamCluster::Video, storage_const::COL_CAT_VOD), (XtreamCluster::Series, storage_const::COL_CAT_SERIES)] { let col_path = get_collection_path(path, cat); if col_path.exists() { 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(TAG_CATEGORY_ID).and_then(get_u32_from_serde_value) { + if let Some(category_id) = entry.get(xtream_const::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, TAG_CATEGORY_NAME) { + if let Some(category_name) = get_map_item_as_str(&item, xtream_const::XC_TAG_CATEGORY_NAME) { result.insert(format!("{cluster}{category_name}"), category_id); max_id = max_id.max(category_id); } @@ -186,16 +162,16 @@ fn load_old_category_ids(path: &Path) -> (u32, HashMap) { } pub fn xtream_get_storage_path(cfg: &Config, target_name: &str) -> Option { - get_target_storage_path(cfg, target_name).map(|target_path| target_path.join(PathBuf::from(PATH_XTREAM))) + get_target_storage_path(cfg, target_name).map(|target_path| target_path.join(PathBuf::from(storage_const::PATH_XTREAM))) } pub fn xtream_get_epg_file_path(path: &Path) -> PathBuf { - path.join(FILE_EPG) + path.join(storage_const::FILE_EPG) } fn xtream_get_file_paths_for_name(storage_path: &Path, name: &str) -> (PathBuf, PathBuf) { - let xtream_path = storage_path.join(format!("{name}.{FILE_SUFFIX_DB}")); - let index_path = storage_path.join(format!("{name}.{FILE_SUFFIX_INDEX}")); + let xtream_path = storage_path.join(format!("{name}.{}", storage_const::FILE_SUFFIX_DB)); + let index_path = storage_path.join(format!("{name}.{}", storage_const::FILE_SUFFIX_INDEX)); (xtream_path, index_path) } @@ -204,7 +180,7 @@ pub fn xtream_get_file_paths(storage_path: &Path, cluster: XtreamCluster) -> (Pa } pub fn xtream_get_file_paths_for_series(storage_path: &Path) -> (PathBuf, PathBuf) { - xtream_get_file_paths_for_name(storage_path, FILE_SERIES) + xtream_get_file_paths_for_name(storage_path, storage_const::FILE_SERIES) } fn xtream_garbage_collect(config: &Config, target_name: &str) -> std::io::Result<()> { @@ -252,9 +228,9 @@ pub async fn xtream_write_playlist( XtreamCluster::Series => &mut cat_series_col, XtreamCluster::Video => &mut cat_vod_col, }.push(json!({ - TAG_CATEGORY_ID: format!("{}", &cat_id), - TAG_CATEGORY_NAME: plg.title.clone(), - TAG_PARENT_ID: 0 + 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 })); for pli in &mut plg.channels { @@ -271,9 +247,9 @@ pub async fn xtream_write_playlist( } for (col_path, data) in [ - (get_collection_path(&path, COL_CAT_LIVE), &cat_live_col), - (get_collection_path(&path, COL_CAT_VOD), &cat_vod_col), - (get_collection_path(&path, COL_CAT_SERIES), &cat_series_col), + (get_collection_path(&path, storage_const::COL_CAT_LIVE), &cat_live_col), + (get_collection_path(&path, storage_const::COL_CAT_VOD), &cat_vod_col), + (get_collection_path(&path, storage_const::COL_CAT_SERIES), &cat_series_col), ] { match json_write_documents_to_file(&col_path, data) { Ok(()) => {} @@ -552,13 +528,13 @@ 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(TAG_INFO_DATA) { + if let Some(Value::Object(info_data)) = doc.get_mut(xtream_const::XC_TAG_INFO_DATA) { match user.proxy { ProxyType::Reverse => { 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, INFO_REWRITE_FIELDS, INFO_RESOURCE_PREFIX); + rewrite_doc_urls(resource_url.as_ref(), info_data, storage_const::INFO_REWRITE_FIELDS, xtream_const::XC_INFO_RESOURCE_PREFIX); // doc.insert(TAG_INFO_DATA, Value::Object(info_data)); } ProxyType::Redirect => {} @@ -567,18 +543,18 @@ async fn rewrite_xtream_vod_info

( } // we need to update the movie data with virtual ids. - if let Some(Value::Object(movie_data)) = doc.get_mut(TAG_MOVIE_DATA) { + if let Some(Value::Object(movie_data)) = doc.get_mut(xtream_const::XC_TAG_MOVIE_DATA) { let stream_id = pli.get_virtual_id(); let category_id = pli.get_category_id().unwrap_or(0); - movie_data.insert(TAG_STREAM_ID.to_string(), Value::Number(serde_json::value::Number::from(stream_id))); - movie_data.insert(TAG_CATEGORY_ID.to_string(), Value::Number(serde_json::value::Number::from(category_id))); - movie_data.insert(TAG_CATEGORY_IDS.to_string(), Value::Array(vec![Value::Number(serde_json::value::Number::from(category_id))])); + 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))])); let options = XtreamMappingOptions::from_target_options(xtream_output, config); if options.skip_video_direct_source { - movie_data.insert(TAG_DIRECT_SOURCE.to_string(), Value::String(String::new())); + movie_data.insert(xtream_const::XC_TAG_DIRECT_SOURCE.to_string(), Value::String(String::new())); } else { movie_data.insert( - TAG_DIRECT_SOURCE.to_string(), + xtream_const::XC_TAG_DIRECT_SOURCE.to_string(), Value::String(pli.get_provider_url().to_string()), ); } @@ -642,22 +618,22 @@ 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(TAG_INFO_DATA) { - rewrite_doc_urls(resource_url.as_ref(), info_data, INFO_REWRITE_FIELDS, INFO_RESOURCE_PREFIX); + 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::Array(seasons_data)) = doc.get_mut(TAG_SEASONS_DATA) { + if let Some(Value::Array(seasons_data)) = doc.get_mut(xtream_const::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(TAG_ID).and_then(get_u32_from_serde_value) { - let field_prefix = format!("{SEASON_RESOURCE_PREFIX}{season_provider_id}_"); - rewrite_doc_urls(resource_url.as_ref(), season_doc, INFO_REWRITE_FIELDS, &field_prefix); + 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); + rewrite_doc_urls(resource_url.as_ref(), season_doc, storage_const::INFO_REWRITE_FIELDS, &field_prefix); } } } } } - let episodes = doc.get_mut(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(xtream_const::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(); { @@ -667,7 +643,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(TAG_ID).and_then(get_u32_from_serde_value) + if let Some(episode_provider_id) = episode.get(xtream_const::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( @@ -676,17 +652,17 @@ async fn rewrite_xtream_series_info

( PlaylistItemType::Series, virtual_id, ); - episode.insert(TAG_ID.to_string(), Value::String(episode_virtual_id.to_string())); + episode.insert(xtream_const::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(TAG_INFO_DATA) { - let field_prefix = format!("{INFO_RESOURCE_PREFIX_EPISODE}{episode_provider_id}_"); - rewrite_doc_urls(resource_url.as_ref(), info_data, INFO_REWRITE_FIELDS, &field_prefix); + 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); + rewrite_doc_urls(resource_url.as_ref(), info_data, storage_const::INFO_REWRITE_FIELDS, &field_prefix); } } } if options.skip_series_direct_source { - episode.insert(TAG_DIRECT_SOURCE.to_string(), Value::String(String::new())); + episode.insert(xtream_const::XC_TAG_DIRECT_SOURCE.to_string(), Value::String(String::new())); } } } @@ -964,9 +940,9 @@ where pub(crate) async fn xtream_get_playlist_categories(config: &Config, target_name: &str, cluster: XtreamCluster) -> Option> { let path = xtream_get_collection_path(config, target_name, match cluster { - XtreamCluster::Live => COL_CAT_LIVE, - XtreamCluster::Video => COL_CAT_VOD, - XtreamCluster::Series => COL_CAT_SERIES, + XtreamCluster::Live => storage_const::COL_CAT_LIVE, + XtreamCluster::Video => storage_const::COL_CAT_VOD, + XtreamCluster::Series => storage_const::COL_CAT_SERIES, }); if let Ok((Some(file_path), _content)) = path { if let Ok(content) = tokio::fs::read_to_string(&file_path).await { diff --git a/src/utils/compression/compression_utils.rs b/src/utils/compression/compression_utils.rs index 69452192d..f59420533 100644 --- a/src/utils/compression/compression_utils.rs +++ b/src/utils/compression/compression_utils.rs @@ -3,9 +3,6 @@ use flate2::Compression; use flate2::read::GzDecoder; use flate2::write::GzEncoder; -pub const ENCODING_GZIP: &str = "gzip"; -pub const ENCODING_DEFLATE: &str = "deflate"; - pub const fn is_gzip(bytes: &[u8]) -> bool { // Gzip files start with the bytes 0x1F 0x8B bytes.len() >= 2 && bytes[0] == 0x1F && bytes[1] == 0x8B diff --git a/src/utils/constants.rs b/src/utils/constants.rs new file mode 100644 index 000000000..75e2daeaf --- /dev/null +++ b/src/utils/constants.rs @@ -0,0 +1,87 @@ +use regex::Regex; +use std::collections::HashSet; +use std::sync::atomic::AtomicBool; +use std::sync::LazyLock; + + +pub const USER_FILE: &str = "user.txt"; +pub const CONFIG_PATH: &str = "config"; +pub const CONFIG_FILE: &str = "config.yml"; +pub const SOURCE_FILE: &str = "source.yml"; +pub const MAPPING_FILE: &str = "mapping.yml"; +pub const API_PROXY_FILE: &str = "api-proxy.yml"; + + +pub const ENCODING_GZIP: &str = "gzip"; +pub const ENCODING_DEFLATE: &str = "deflate"; + + +pub const HLS_EXT: &str = ".m3u8"; +pub const DASH_EXT: &str = ".mpd"; + +pub const HLS_EXT_QUERY: &str = ".m3u8?"; +pub const HLS_EXT_FRAGMENT: &str = ".m3u8#"; +pub const DASH_EXT_QUERY: &str = ".mpd?"; +pub const DASH_EXT_FRAGMENT: &str = ".mpd#"; + +pub const FILENAME_TRIM_PATTERNS: &[char] = &['.', '-', '_']; + +pub const MEDIA_STREAM_HEADERS: &[&str] = &["accept", "content-type", "content-length", "connection", + "accept-ranges", "content-range", "vary", "transfer-encoding", "access-control-allow-origin", + "access-control-allow-credentials", "icy-metadata"]; + +pub struct KodiStyle { + pub year: Regex, + pub season: Regex, + pub episode: Regex, + pub whitespace: Regex, + pub alphanumeric: Regex, +} + +pub struct Constants { + pub username: Regex, + pub password: Regex, + pub token: Regex, + pub stream_url: Regex, + pub url: Regex, + pub base_href: Regex, + pub env: Regex, + pub sanitize: AtomicBool, + pub kodi_style: KodiStyle, + pub country_codes: HashSet<&'static str>, +} + +pub static CONSTANTS: LazyLock = LazyLock::new(|| + Constants { + username: Regex::new(r"(username=)[^&]*").unwrap(), + password: Regex::new(r"(password=)[^&]*").unwrap(), + token: Regex::new(r"(token=)[^&]*").unwrap(), + stream_url: Regex::new(r"(.*://).*/(live|video|movie|series|m3u-stream|resource)/\w+/\w+").unwrap(), + url: Regex::new(r"(.*://).*?/(.*)").unwrap(), + base_href: Regex::new(r#"(href|src)="/([^"]*)""#).unwrap(), + env: Regex::new(r"\$\{env:(?P[a-zA-Z_][a-zA-Z0-9_]*)}").unwrap(), + sanitize: AtomicBool::new(true), + kodi_style: KodiStyle { + season: Regex::new(r"[Ss]\d{1,2}").unwrap(), + episode: Regex::new(r"[Ee]\d{1,2}").unwrap(), + year: Regex::new(r"\d{4}").unwrap(), + whitespace: Regex::new(r"\s+").unwrap(), + alphanumeric: Regex::new(r"[^\w\s]").unwrap(), + }, + country_codes: vec![ + "af", "al", "dz", "ad", "ao", "ag", "ar", "am", "au", "at", "az", "bs", "bh", "bd", "bb", "by", + "be", "bz", "bj", "bt", "bo", "ba", "bw", "br", "bn", "bg", "bf", "bi", "cv", "kh", "cm", "ca", + "cf", "td", "cl", "cn", "co", "km", "cg", "cr", "hr", "cu", "cy", "cz", "cd", "dk", "dj", "dm", + "do", "tl", "ec", "eg", "sv", "gq", "er", "ee", "sz", "et", "fj", "fi", "fr", "ga", "gm", "ge", + "de", "gh", "gr", "gd", "gt", "gn", "gw", "gy", "ht", "hn", "hu", "is", "in", "id", "ir", "iq", + "ie", "il", "it", "ci", "jm", "jp", "jo", "kz", "ke", "ki", "kp", "kr", "kw", "kg", "la", "lv", + "lb", "ls", "lr", "ly", "li", "lt", "lu", "mg", "mw", "my", "mv", "ml", "mt", "mh", "mr", "mu", + "mx", "fm", "md", "mc", "mn", "me", "ma", "mz", "mm", "na", "nr", "np", "nl", "nz", "ni", "ne", + "ng", "mk", "no", "om", "pk", "pw", "pa", "pg", "py", "pe", "ph", "pl", "pt", "qa", "ro", "ru", + "rw", "kn", "lc", "vc", "ws", "sm", "st", "sa", "sn", "rs", "sc", "sl", "sg", "sk", "si", "sb", + "so", "za", "ss", "es", "lk", "sd", "sr", "se", "ch", "sy", "tw", "tj", "tz", "th", "tg", "to", + "tt", "tn", "tr", "tm", "tv", "ug", "ua", "ae", "gb", "us", "uy", "uz", "vu", "va", "ve", "vn", + "ye", "zm", "zw", + ].into_iter().collect::>(), + } +); diff --git a/src/utils/file/config_reader.rs b/src/utils/file/config_reader.rs index 548fd9004..389e92cf2 100644 --- a/src/utils/file/config_reader.rs +++ b/src/utils/file/config_reader.rs @@ -9,14 +9,13 @@ use crate::utils::network::request::{get_credentials_from_url, get_local_file_co use crate::utils::sys_utils::exit; use chrono::Local; use log::{debug, error, info, warn}; -use regex::Regex; use serde::Serialize; use std::env; use std::fs::File; use std::io::{self, BufRead, BufReader, Cursor, Error, Read}; use std::path::PathBuf; -use std::sync::{LazyLock}; use url::Url; +use crate::utils::constants::CONSTANTS; enum EitherReader { Left(L), @@ -169,13 +168,11 @@ pub fn save_main_config(file_path: &str, backup_dir: &str, config: &ConfigDto) - write_config_file(file_path, backup_dir, config, "config.yml") } -static ENV_REGEX: LazyLock = LazyLock::new(|| Regex::new(r"\$\{env:(?P[a-zA-Z_][a-zA-Z0-9_]*)}").unwrap()); - pub fn resolve_env_var(value: &str) -> String { if value.is_empty() { return String::new(); } - ENV_REGEX.replace_all(value, |caps: ®ex::Captures| { + CONSTANTS.env.replace_all(value, |caps: ®ex::Captures| { let var_name = &caps["var"]; env::var(var_name).unwrap_or_else(|_| format!("${{env:{var_name}}}")) }).to_string() diff --git a/src/utils/file/file_utils.rs b/src/utils/file/file_utils.rs index 7260dd75d..1ca4baac1 100644 --- a/src/utils/file/file_utils.rs +++ b/src/utils/file/file_utils.rs @@ -8,13 +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; - -const USER_FILE: &str = "user.txt"; -const CONFIG_PATH: &str = "config"; -const CONFIG_FILE: &str = "config.yml"; -const SOURCE_FILE: &str = "source.yml"; -const MAPPING_FILE: &str = "mapping.yml"; -const API_PROXY_FILE: &str = "api-proxy.yml"; +use crate::utils::constants::{API_PROXY_FILE, CONFIG_FILE, CONFIG_PATH, MAPPING_FILE, SOURCE_FILE, USER_FILE}; pub fn file_writer(w: W) -> BufWriter where diff --git a/src/utils/mod.rs b/src/utils/mod.rs index c08d175d0..b160b213c 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -10,6 +10,7 @@ pub(crate) mod network; pub mod bincode_utils; pub mod time_utils; pub mod crypto_utils; +pub mod constants; #[macro_export] macro_rules! debug_if_enabled { diff --git a/src/utils/network/request.rs b/src/utils/network/request.rs index 2b7b67ad3..0c0954d9c 100644 --- a/src/utils/network/request.rs +++ b/src/utils/network/request.rs @@ -3,15 +3,13 @@ use std::fs; use std::fs::File; use std::io::{BufWriter, Error, ErrorKind, Read, Write}; use std::path::{Path, PathBuf}; -use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::atomic::{Ordering}; use std::sync::Arc; -use std::sync::LazyLock; use std::time::Instant; use flate2::read::{GzDecoder, ZlibDecoder}; use futures::StreamExt; use log::{debug, error, log_enabled, trace, Level}; -use regex::Regex; use reqwest::header::CONTENT_ENCODING; use reqwest::header::{HeaderMap, HeaderName, HeaderValue}; use url::Url; @@ -21,14 +19,12 @@ use crate::m3u_filter_error::{str_to_io_error, M3uFilterError, M3uFilterErrorKin use crate::model::config::{ConfigInput, InputFetchMethod}; use crate::model::stats::format_elapsed_time; use crate::repository::storage::{get_input_storage_path, short_hash}; -use crate::repository::xtream_repository::FILE_EPG; -use crate::utils::compression::compression_utils::{is_deflate, is_gzip, ENCODING_DEFLATE, ENCODING_GZIP}; +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}; -pub const HLS_EXT: &str = ".m3u8"; -pub const DASH_EXT: &str = ".mpd"; - pub const fn bytes_to_megabytes(bytes: u64) -> u64 { bytes / 1_048_576 } @@ -311,7 +307,7 @@ pub async fn download_text_content_as_file(client: Arc, input: } else { let file_path = persist_filepath.map_or_else(|| match get_input_storage_path(&input.name, working_dir) { Ok(download_path) => { - Ok(download_path.join(format!("{}_{FILE_EPG}", short_hash(url_str)))) + Ok(download_path.join(format!("{}_{}", short_hash(url_str), storage_const::FILE_EPG))) } Err(err) => Err(err) }, Ok); @@ -368,38 +364,18 @@ pub async fn get_input_json_content(client: Arc, input: &Config Err(e) => create_m3u_filter_error_result!(M3uFilterErrorKind::Notify, "cant download input url: {} => {}", sanitize_sensitive_info(url), sanitize_sensitive_info(e.to_string().as_str())) } } -// -// pub fn get_base_url(url: &str) -> Option { -// if let Some((scheme_end, rest)) = url.split_once("://") { -// let scheme = scheme_end; -// if let Some(authority_end) = rest.find('/') { -// let authority = &rest[..authority_end]; -// return Some(format!("{}://{}", scheme, authority)); -// } -// return Some(format!("{}://{}", scheme, rest)); -// } -// None -// } - -static USERNAME_REGEX: LazyLock = LazyLock::new(|| Regex::new(r"(username=)[^&]*").unwrap()); -static PASSWORD_REGEX: LazyLock = LazyLock::new(|| Regex::new(r"(password=)[^&]*").unwrap()); -static TOKEN_REGEX: LazyLock = LazyLock::new(|| Regex::new(r"(token=)[^&]*").unwrap()); -static STREAM_URL_REGEX: LazyLock = LazyLock::new(|| Regex::new(r"(.*://).*/(live|video|movie|series|m3u-stream|resource)/\w+/\w+").unwrap()); -static URL_REGEX: LazyLock = LazyLock::new(|| Regex::new(r"(.*://).*?/(.*)").unwrap()); - -static SANITIZE_SENSITIVE_INFO: LazyLock = LazyLock::new(|| AtomicBool::new(true)); pub fn set_sanitize_sensitive_info(value: bool) { - SANITIZE_SENSITIVE_INFO.store(value, Ordering::SeqCst); + CONSTANTS.sanitize.store(value, Ordering::SeqCst); } pub fn sanitize_sensitive_info(query: &str) -> String { - if SANITIZE_SENSITIVE_INFO.load(Ordering::SeqCst) { + if CONSTANTS.sanitize.load(Ordering::SeqCst) { // Replace with "***" - let masked_query = USERNAME_REGEX.replace_all(query, "$1***"); - let masked_query = PASSWORD_REGEX.replace_all(&masked_query, "$1***"); - let masked_query = TOKEN_REGEX.replace_all(&masked_query, "$1***"); - let masked_query = STREAM_URL_REGEX.replace_all(&masked_query, "$1***/$2/***"); - let masked_query = URL_REGEX.replace_all(&masked_query, "$1***/$2"); + let masked_query = CONSTANTS.username.replace_all(query, "$1***"); + let masked_query = CONSTANTS.password.replace_all(&masked_query, "$1***"); + let masked_query = CONSTANTS.token.replace_all(&masked_query, "$1***"); + let masked_query = CONSTANTS.stream_url.replace_all(&masked_query, "$1***/$2/***"); + let masked_query = CONSTANTS.url.replace_all(&masked_query, "$1***/$2"); masked_query.to_string() } else { query.to_string() @@ -450,12 +426,6 @@ pub fn classify_content_type(headers: &[(String, String)]) -> MimeCategory { }) } -const HLS_EXT_QUERY: &str = ".m3u8?"; -const HLS_EXT_FRAGMENT: &str = ".m3u8#"; -const DASH_EXT_QUERY: &str = ".mpd?"; -const DASH_EXT_FRAGMENT: &str = ".mpd#"; - - pub fn is_hls_url(url: &str) -> bool { let lc_url = url.to_lowercase(); lc_url.ends_with(HLS_EXT) || lc_url.contains(HLS_EXT_QUERY) || lc_url.contains(HLS_EXT_FRAGMENT) diff --git a/src/utils/network/xtream.rs b/src/utils/network/xtream.rs index cc6902c32..568165f3b 100644 --- a/src/utils/network/xtream.rs +++ b/src/utils/network/xtream.rs @@ -9,19 +9,11 @@ use log::{info, warn}; use std::cmp::Ordering; use std::io::Error; use crate::model::api_proxy::{ProxyType, 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; -pub const ACTION_GET_SERIES_INFO: &str = "get_series_info"; -pub const ACTION_GET_VOD_INFO: &str = "get_vod_info"; -pub const ACTION_GET_LIVE_INFO: &str = "get_live_info"; -pub const ACTION_GET_SERIES: &str = "get_series"; -pub const ACTION_GET_LIVE_CATEGORIES: &str = "get_live_categories"; -pub const ACTION_GET_VOD_CATEGORIES: &str = "get_vod_categories"; -pub const ACTION_GET_SERIES_CATEGORIES: &str = "get_series_categories"; -pub const ACTION_GET_LIVE_STREAMS: &str = "get_live_streams"; -pub const ACTION_GET_VOD_STREAMS: &str = "get_vod_streams"; #[inline] pub fn get_xtream_stream_url_base(url: &str, username: &str, password: &str) -> String { @@ -44,9 +36,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 => (ACTION_GET_LIVE_INFO, "live_id"), - XtreamCluster::Video => (ACTION_GET_VOD_INFO, "vod_id"), - XtreamCluster::Series => (ACTION_GET_SERIES_INFO, "series_id"), + 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), }; get_xtream_player_api_action_url(input, action).map(|action_url| format!("{action_url}&{stream_id_field}={stream_id}")) } @@ -134,9 +126,9 @@ fn get_skip_cluster(input: &ConfigInput) -> Vec { } const ACTIONS: [(XtreamCluster, &str, &str); 3] = [ - (XtreamCluster::Live, ACTION_GET_LIVE_CATEGORIES, ACTION_GET_LIVE_STREAMS), - (XtreamCluster::Video, ACTION_GET_VOD_CATEGORIES, ACTION_GET_VOD_STREAMS), - (XtreamCluster::Series, ACTION_GET_SERIES_CATEGORIES, ACTION_GET_SERIES)]; + (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)]; pub async fn get_xtream_playlist(client: Arc, input: &ConfigInput, working_dir: &str) -> (Vec, Vec) {