diff --git a/backend/src/api/api_utils.rs b/backend/src/api/api_utils.rs index a49d40e8f..983e4efbb 100644 --- a/backend/src/api/api_utils.rs +++ b/backend/src/api/api_utils.rs @@ -1,16 +1,14 @@ use crate::api::endpoints::xtream_api::{get_xtream_player_api_stream_url, ApiStreamContext}; -use crate::api::model::active_provider_manager::{ProviderAllocation, ProviderConnectionGuard}; -use crate::api::model::app_state::AppState; -use crate::api::model::model_utils::get_stream_response_with_headers; -use crate::api::model::request::UserApiRequest; -use crate::api::model::stream::{BoxedProviderStream, ProviderStreamInfo, ProviderStreamResponse}; -use crate::api::model::stream_error::StreamError; -use crate::api::model::streams::active_client_stream::ActiveClientStream; -use crate::api::model::streams::persist_pipe_stream::PersistPipeStream; -use crate::api::model::streams::provider_stream::{create_channel_unavailable_stream, create_custom_video_stream_response, create_provider_connections_exhausted_stream, CustomVideoStreamType}; -use crate::api::model::streams::provider_stream_factory::{create_provider_stream, ProviderStreamFactoryOptions}; -use crate::api::model::streams::shared_stream_manager::SharedStreamManager; -use crate::api::model::streams::throttled_stream::ThrottledStream; +use crate::api::model::ProviderConfig; +use crate::api::model::UserSession; +use crate::api::model::{ + create_channel_unavailable_stream, create_custom_video_stream_response, + create_provider_connections_exhausted_stream, create_provider_stream, + get_stream_response_with_headers, ActiveClientStream, AppState, BoxedProviderStream, + CustomVideoStreamType, PersistPipeStream, ProviderAllocation, ProviderConnectionGuard, + ProviderStreamFactoryOptions, ProviderStreamInfo, ProviderStreamResponse, SharedStreamManager, + StreamError, ThrottledStream, UserApiRequest, +}; use crate::auth::Claims; use crate::model::ConfigInput; use crate::model::{ConfigTarget, ProxyUserCredentials}; @@ -27,9 +25,14 @@ use chrono::{DateTime, Utc}; use futures::{StreamExt, TryStreamExt}; use jsonwebtoken::{decode, Algorithm, DecodingKey, Validation}; use log::{debug, error, log_enabled, trace}; -use shared::model::{InputFetchMethod, PlaylistEntry, PlaylistItemType, TargetType, UserConnectionPermission, XtreamCluster}; +use shared::model::{ + InputFetchMethod, PlaylistEntry, PlaylistItemType, TargetType, UserConnectionPermission, + XtreamCluster, +}; use shared::utils::{default_grace_period_millis, human_readable_byte_size, trim_slash}; -use shared::utils::{extract_extension_from_url, replace_url_extension, sanitize_sensitive_info, DASH_EXT, HLS_EXT}; +use shared::utils::{ + extract_extension_from_url, replace_url_extension, sanitize_sensitive_info, DASH_EXT, HLS_EXT, +}; use std::borrow::Cow; use std::collections::HashMap; use std::io::BufWriter; @@ -37,8 +40,6 @@ use std::path::Path; use std::sync::Arc; use tokio::sync::Mutex; use url::Url; -use crate::api::model::active_user_manager::UserSession; -use crate::api::model::provider_config::ProviderConfig; #[macro_export] macro_rules! try_option_bad_request { @@ -46,7 +47,11 @@ macro_rules! try_option_bad_request { match $option { Some(value) => value, None => { - if $msg_is_error {error!("{}", $msg);} else {debug!("{}", $msg);} + if $msg_is_error { + error!("{}", $msg); + } else { + debug!("{}", $msg); + } return axum::http::StatusCode::BAD_REQUEST.into_response(); } } @@ -65,7 +70,11 @@ macro_rules! try_result_bad_request { match $option { Ok(value) => value, Err(_) => { - if $msg_is_error {error!("{}", $msg);} else {debug!("{}", $msg);} + if $msg_is_error { + error!("{}", $msg); + } else { + debug!("{}", $msg); + } return axum::http::StatusCode::BAD_REQUEST.into_response(); } } @@ -82,9 +91,9 @@ macro_rules! try_result_bad_request { macro_rules! try_unwrap_body { ($body:expr) => { $body.map_or_else( - |_| axum::http::StatusCode::INTERNAL_SERVER_ERROR.into_response(), - |resp| resp.into_response(), - ) + |_| axum::http::StatusCode::INTERNAL_SERVER_ERROR.into_response(), + |resp| resp.into_response(), + ) }; } @@ -93,18 +102,24 @@ pub use try_result_bad_request; pub use try_unwrap_body; pub fn get_server_time() -> String { - chrono::offset::Local::now().with_timezone(&chrono::Local).format("%Y-%m-%d %H:%M:%S %Z").to_string() + chrono::offset::Local::now() + .with_timezone(&chrono::Local) + .format("%Y-%m-%d %H:%M:%S %Z") + .to_string() } pub fn get_build_time() -> Option { - BUILD_TIMESTAMP.to_string().parse::>().ok().map(|datetime| datetime.format("%Y-%m-%d %H:%M:%S %Z").to_string()) + BUILD_TIMESTAMP + .to_string() + .parse::>() + .ok() + .map(|datetime| datetime.format("%Y-%m-%d %H:%M:%S %Z").to_string()) } pub fn get_memory_usage() -> String { crate::utils::get_memory_usage().map_or(String::from("?"), human_readable_byte_size) } - #[allow(clippy::missing_panics_doc)] pub async fn serve_file(file_path: &Path, mime_type: mime::Mime) -> impl IntoResponse + Send { if file_path.exists() { @@ -117,7 +132,10 @@ pub async fn serve_file(file_path: &Path, mime_type: mime::Mime) -> impl IntoRes try_unwrap_body!(axum::response::Response::builder() .status(axum::http::StatusCode::OK) .header(axum::http::header::CONTENT_TYPE, mime_type.to_string()) - .header(axum::http::header::CACHE_CONTROL, axum::http::header::HeaderValue::from_static("no-cache")) + .header( + axum::http::header::CACHE_CONTROL, + axum::http::header::HeaderValue::from_static("no-cache") + ) .body(body)) } Err(_) => axum::http::StatusCode::INTERNAL_SERVER_ERROR.into_response(), @@ -126,15 +144,22 @@ pub async fn serve_file(file_path: &Path, mime_type: mime::Mime) -> impl IntoRes axum::http::StatusCode::NOT_FOUND.into_response() } -pub fn get_user_target_by_username(username: &str, app_state: &AppState) -> Option<(ProxyUserCredentials, Arc)> { +pub fn get_user_target_by_username( + username: &str, + app_state: &AppState, +) -> Option<(ProxyUserCredentials, Arc)> { if !username.is_empty() { return app_state.app_config.get_target_for_username(username); } None } -pub fn get_user_target_by_credentials<'a>(username: &str, password: &str, api_req: &'a UserApiRequest, - app_state: &'a AppState) -> Option<(ProxyUserCredentials, Arc)> { +pub fn get_user_target_by_credentials<'a>( + username: &str, + password: &str, + api_req: &'a UserApiRequest, + app_state: &'a AppState, +) -> Option<(ProxyUserCredentials, Arc)> { if !username.is_empty() && !password.is_empty() { app_state.app_config.get_target_for_user(username, password) } else { @@ -147,7 +172,10 @@ pub fn get_user_target_by_credentials<'a>(username: &str, password: &str, api_re } } -pub fn get_user_target<'a>(api_req: &'a UserApiRequest, app_state: &'a AppState) -> Option<(ProxyUserCredentials, Arc)> { +pub fn get_user_target<'a>( + api_req: &'a UserApiRequest, + app_state: &'a AppState, +) -> Option<(ProxyUserCredentials, Arc)> { let username = api_req.username.as_str().trim(); let password = api_req.password.as_str().trim(); get_user_target_by_credentials(username, password, api_req, app_state) @@ -182,7 +210,9 @@ pub struct StreamOptions { /// Returns a `StreamOptions` instance with the resolved configuration. fn get_stream_options(app_state: &AppState) -> StreamOptions { let (stream_retry, stream_force_retry_secs, buffer_enabled, buffer_size) = app_state - .app_config.config.load() + .app_config + .config + .load() .reverse_proxy .as_ref() .and_then(|reverse_proxy| reverse_proxy.stream.as_ref()) @@ -191,10 +221,21 @@ fn get_stream_options(app_state: &AppState) -> StreamOptions { .buffer .as_ref() .map_or((false, 0), |buffer| (buffer.enabled, buffer.size)); - (stream.retry, stream.forced_retry_interval_secs, buffer_enabled, buffer_size) + ( + stream.retry, + stream.forced_retry_interval_secs, + buffer_enabled, + buffer_size, + ) }); let pipe_provider_stream = !stream_retry && !buffer_enabled; - StreamOptions { stream_retry, stream_force_retry_secs, buffer_enabled, buffer_size, pipe_provider_stream } + StreamOptions { + stream_retry, + stream_force_retry_secs, + buffer_enabled, + buffer_size, + pipe_provider_stream, + } } // fn get_stream_content_length(provider_response: Option<&(Vec<(String, String)>, StatusCode)>) -> u64 { @@ -206,21 +247,40 @@ fn get_stream_options(app_state: &AppState) -> StreamOptions { // content_length // } -pub fn get_stream_alternative_url(stream_url: &str, input: &ConfigInput, alias_input: &Arc) -> String { - let Some(input_user_info) = input.get_user_info() else { return stream_url.to_owned() }; - let Some(alt_input_user_info) = alias_input.get_user_info() else { return stream_url.to_owned() }; +pub fn get_stream_alternative_url( + stream_url: &str, + input: &ConfigInput, + alias_input: &Arc, +) -> String { + let Some(input_user_info) = input.get_user_info() else { + return stream_url.to_owned(); + }; + let Some(alt_input_user_info) = alias_input.get_user_info() else { + return stream_url.to_owned(); + }; let modified = stream_url.replace(&input_user_info.base_url, &alt_input_user_info.base_url); let modified = modified.replace(&input_user_info.username, &alt_input_user_info.username); modified.replace(&input_user_info.password, &alt_input_user_info.password) } -async fn get_redirect_alternative_url<'a>(app_state: &AppState, redirect_url: &'a str, input: &ConfigInput) -> Cow<'a, str> { +async fn get_redirect_alternative_url<'a>( + app_state: &AppState, + redirect_url: &'a str, + input: &ConfigInput, +) -> Cow<'a, str> { if let Some((base_url, username, password)) = input.get_matched_config_by_url(redirect_url) { - if let Some(provider_cfg) = app_state.active_provider.get_next_provider(&input.name).await { + if let Some(provider_cfg) = app_state + .active_provider + .get_next_provider(&input.name) + .await + { let mut new_url = redirect_url.replacen(base_url, provider_cfg.url.as_str(), 1); if let (Some(old_username), Some(old_password)) = (username, password) { - if let (Some(new_username), Some(new_password)) = (provider_cfg.username.as_ref(), provider_cfg.password.as_ref()) { + if let (Some(new_username), Some(new_password)) = ( + provider_cfg.username.as_ref(), + provider_cfg.password.as_ref(), + ) { new_url = new_url.replacen(old_username, new_username, 1); new_url = new_url.replacen(old_password, new_password, 1); return Cow::Owned(new_url); @@ -296,12 +356,27 @@ struct StreamingStrategy { /// - and optional HTTP headers to include in the request. /// /// This logic helps abstract the decision-making behind provider selection and stream URL resolution. -async fn resolve_streaming_strategy(app_state: &AppState, stream_url: &str, addr: &str, input: &ConfigInput, force_provider: Option<&str>) - -> StreamingStrategy { +async fn resolve_streaming_strategy( + app_state: &AppState, + stream_url: &str, + addr: &str, + input: &ConfigInput, + force_provider: Option<&str>, +) -> StreamingStrategy { // allocate a provider connection let provider_connection_guard = match force_provider { - Some(provider) => app_state.active_provider.force_exact_acquire_connection(provider, addr).await, - None => app_state.active_provider.acquire_connection(&input.name, addr).await + Some(provider) => { + app_state + .active_provider + .force_exact_acquire_connection(provider, addr) + .await + } + None => { + app_state + .active_provider + .acquire_connection(&input.name, addr) + .await + } }; let stream_response_params = match &**provider_connection_guard { @@ -317,10 +392,16 @@ async fn resolve_streaming_strategy(app_state: &AppState, stream_url: &str, addr let (provider, url) = if force_provider.is_some() || provider.id == input.id { (input.name.to_string(), stream_url.to_string()) } else { - (provider.name.to_string(), get_stream_alternative_url(stream_url, input, provider)) + ( + provider.name.to_string(), + get_stream_alternative_url(stream_url, input, provider), + ) }; - if matches!(&**provider_connection_guard, ProviderAllocation::Available(_, _)) { + if matches!( + &**provider_connection_guard, + ProviderAllocation::Available(_, _) + ) { ProviderStreamState::Available(Some(provider), url) } else { ProviderStreamState::GracePeriod(Some(provider), url) @@ -334,30 +415,52 @@ async fn resolve_streaming_strategy(app_state: &AppState, stream_url: &str, addr } } - -fn get_grace_period_millis(connection_permission: UserConnectionPermission, stream_response_params: &ProviderStreamState, config_grace_period_millis: u64) -> u64 { - if config_grace_period_millis > 0 && - (matches!(stream_response_params, ProviderStreamState::GracePeriod(_, _)) // provider grace period - || connection_permission == UserConnectionPermission::GracePeriod // user grace period - ) { config_grace_period_millis } else { 0 } +fn get_grace_period_millis( + connection_permission: UserConnectionPermission, + stream_response_params: &ProviderStreamState, + config_grace_period_millis: u64, +) -> u64 { + if config_grace_period_millis > 0 + && ( + matches!(stream_response_params, ProviderStreamState::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, - addr: &str, - req_headers: &HeaderMap, - input: &ConfigInput, - item_type: PlaylistItemType, - share_stream: bool, - connection_permission: UserConnectionPermission, - force_provider: Option<&str>) -> StreamDetails { +async fn create_stream_response_details( + app_state: &AppState, + stream_options: &StreamOptions, + stream_url: &str, + addr: &str, + req_headers: &HeaderMap, + input: &ConfigInput, + item_type: PlaylistItemType, + share_stream: bool, + connection_permission: UserConnectionPermission, + force_provider: Option<&str>, +) -> StreamDetails { let mut streaming_strategy = resolve_streaming_strategy(app_state, stream_url, addr, input, force_provider).await; - let config_grace_period_millis = app_state.app_config.config.load().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 = get_grace_period_millis(connection_permission, &streaming_strategy.provider_stream_state, config_grace_period_millis); + let config_grace_period_millis = app_state + .app_config + .config + .load() + .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 = get_grace_period_millis( + connection_permission, + &streaming_strategy.provider_stream_state, + config_grace_period_millis, + ); match streaming_strategy.provider_stream_state { // custom stream means we display our own stream like connection exhausted, channel-unavailable... ProviderStreamState::Custom(provider_stream) => { @@ -371,17 +474,28 @@ async fn create_stream_response_details(app_state: &AppState, provider_connection_guard: streaming_strategy.provider_connection_guard.clone(), } } - ProviderStreamState::Available(provider_name, request_url) | - ProviderStreamState::GracePeriod(provider_name, request_url) => { + ProviderStreamState::Available(provider_name, request_url) + | ProviderStreamState::GracePeriod(provider_name, request_url) => { let parsed_url = Url::parse(&request_url); let ((stream, stream_info), reconnect_flag) = if let Ok(url) = parsed_url { - let provider_stream_factory_options = ProviderStreamFactoryOptions::new(item_type, share_stream, stream_options, &url, req_headers, streaming_strategy.input_headers.as_ref()); + let provider_stream_factory_options = ProviderStreamFactoryOptions::new( + item_type, + share_stream, + stream_options, + &url, + req_headers, + streaming_strategy.input_headers.as_ref(), + ); let reconnect_flag = provider_stream_factory_options.get_reconnect_flag_clone(); - let provider_stream = match create_provider_stream(Arc::clone(&app_state.app_config), Arc::clone(&app_state.http_client.load()), provider_stream_factory_options).await { + let provider_stream = match create_provider_stream( + Arc::clone(&app_state.app_config), + Arc::clone(&app_state.http_client.load()), + provider_stream_factory_options, + ) + .await + { None => (None, None), - Some((stream, info)) => { - (Some(stream), info) - } + Some((stream, info)) => (Some(stream), info), }; (provider_stream, Some(reconnect_flag)) } else { @@ -400,7 +514,9 @@ async fn create_stream_response_details(app_state: &AppState, if let Some((headers, status_code, response_url)) = stream_info.as_ref() { debug!( "Responding stream request {} with status {}, headers {:?}", - sanitize_sensitive_info(response_url.as_ref().map_or(stream_url, |s| s.as_str())), + sanitize_sensitive_info( + response_url.as_ref().map_or(stream_url, |s| s.as_str()) + ), status_code, headers ); @@ -441,8 +557,12 @@ where { pub fn get_query_path(&self, provider_id: u32, url: &str) -> String { let extension = self.stream_ext.map_or_else( - || extract_extension_from_url(url).map_or_else(String::new, std::string::ToString::to_string), - std::string::ToString::to_string); + || { + extract_extension_from_url(url) + .map_or_else(String::new, std::string::ToString::to_string) + }, + std::string::ToString::to_string, + ); // if there is an action_path (like for timeshift duration/start), it will be added in front of the stream_id if self.action_path.is_empty() { @@ -453,23 +573,41 @@ where } } -pub async fn redirect_response<'a, P>(app_state: &AppState, params: &'a RedirectParams<'a, P>) -> Option +pub async fn redirect_response<'a, P>( + app_state: &AppState, + params: &'a RedirectParams<'a, P>, +) -> Option where P: PlaylistEntry, { let item_type = params.item.get_item_type(); let provider_url = ¶ms.item.get_provider_url(); - let redirect_request = params.user.proxy.is_redirect(item_type) || params.target.is_force_redirect(item_type); - let is_hls_request = item_type == PlaylistItemType::LiveHls || params.stream_ext == Some(HLS_EXT); - let is_dash_request = !is_hls_request && item_type == PlaylistItemType::LiveDash || params.stream_ext == Some(DASH_EXT); + let redirect_request = + params.user.proxy.is_redirect(item_type) || params.target.is_force_redirect(item_type); + let is_hls_request = + item_type == PlaylistItemType::LiveHls || params.stream_ext == Some(HLS_EXT); + let is_dash_request = !is_hls_request && item_type == PlaylistItemType::LiveDash + || params.stream_ext == Some(DASH_EXT); if params.target_type == TargetType::M3u { if redirect_request || is_dash_request { - let redirect_url = if is_hls_request { &replace_url_extension(provider_url, HLS_EXT) } else { provider_url }; - let redirect_url = if is_dash_request { &replace_url_extension(redirect_url, DASH_EXT) } else { redirect_url }; - let redirect_url = get_redirect_alternative_url(app_state, redirect_url, params.input).await; - debug_if_enabled!("Redirecting stream request to {}", sanitize_sensitive_info(&redirect_url)); + let redirect_url = if is_hls_request { + &replace_url_extension(provider_url, HLS_EXT) + } else { + provider_url + }; + let redirect_url = if is_dash_request { + &replace_url_extension(redirect_url, DASH_EXT) + } else { + redirect_url + }; + let redirect_url = + get_redirect_alternative_url(app_state, redirect_url, params.input).await; + debug_if_enabled!( + "Redirecting stream request to {}", + sanitize_sensitive_info(&redirect_url) + ); return Some(redirect(&redirect_url).into_response()); } } else if params.target_type == TargetType::Xtream { @@ -478,7 +616,6 @@ where }; if redirect_request { - // handle redirect for series but why? if params.cluster == XtreamCluster::Series { let ext = params.stream_ext.unwrap_or_default(); @@ -487,20 +624,34 @@ where let password = params.input.password.as_ref().map_or("", |v| v); // TODO do i need action_path like for timeshift ? let stream_url = format!("{url}/series/{username}/{password}/{provider_id}{ext}"); - debug_if_enabled!("Redirecting stream request to {}", sanitize_sensitive_info(&stream_url)); + debug_if_enabled!( + "Redirecting stream request to {}", + sanitize_sensitive_info(&stream_url) + ); return Some(redirect(&stream_url).into_response()); } let target_name = params.target.name.as_str(); let virtual_id = params.item.get_virtual_id(); - let stream_url = match get_xtream_player_api_stream_url(params.input, params.req_context, ¶ms.get_query_path(provider_id, provider_url), provider_url) { + let stream_url = match get_xtream_player_api_stream_url( + params.input, + params.req_context, + ¶ms.get_query_path(provider_id, provider_url), + provider_url, + ) { None => { error!("Cant find stream url for target {target_name}, context {}, stream_id {virtual_id}", params.req_context); return Some(axum::http::StatusCode::BAD_REQUEST.into_response()); } Some(url) => { - match app_state.active_provider.get_next_provider(¶ms.input.name).await { - Some(provider_cfg) => get_stream_alternative_url(&url, params.input, &provider_cfg), + match app_state + .active_provider + .get_next_provider(¶ms.input.name) + .await + { + Some(provider_cfg) => { + get_stream_alternative_url(&url, params.input, &provider_cfg) + } None => url, } } @@ -508,12 +659,22 @@ where // hls or dash redirect if is_dash_request { - let redirect_url = if is_hls_request { &replace_url_extension(&stream_url, HLS_EXT) } else { &replace_url_extension(&stream_url, DASH_EXT) }; - debug_if_enabled!("Redirecting stream request to {}", sanitize_sensitive_info(redirect_url)); + let redirect_url = if is_hls_request { + &replace_url_extension(&stream_url, HLS_EXT) + } else { + &replace_url_extension(&stream_url, DASH_EXT) + }; + debug_if_enabled!( + "Redirecting stream request to {}", + sanitize_sensitive_info(redirect_url) + ); return Some(redirect(redirect_url).into_response()); } - debug_if_enabled!("Redirecting stream request to {}", sanitize_sensitive_info(&stream_url)); + debug_if_enabled!( + "Redirecting stream request to {}", + sanitize_sensitive_info(&stream_url) + ); return Some(redirect(&stream_url).into_response()); } } @@ -522,10 +683,21 @@ where } fn is_throttled_stream(item_type: PlaylistItemType, throttle_kbps: usize) -> bool { - throttle_kbps > 0 && matches!(item_type, PlaylistItemType::Video | PlaylistItemType::Series | PlaylistItemType::SeriesInfo | PlaylistItemType::Catchup) + throttle_kbps > 0 + && matches!( + item_type, + PlaylistItemType::Video + | PlaylistItemType::Series + | PlaylistItemType::SeriesInfo + | PlaylistItemType::Catchup + ) } -fn prepare_body_stream(app_state: &AppState, item_type: PlaylistItemType, stream: ActiveClientStream) -> axum::body::Body { +fn prepare_body_stream( + app_state: &AppState, + item_type: PlaylistItemType, + stream: ActiveClientStream, +) -> axum::body::Body { let throttle_kbps = usize::try_from(get_stream_throttle(app_state)).unwrap_or_default(); let body_stream = if is_throttled_stream(item_type, throttle_kbps) { axum::body::Body::from_stream(ThrottledStream::new(stream.boxed(), throttle_kbps)) @@ -536,85 +708,164 @@ fn prepare_body_stream(app_state: &AppState, item_type: PlaylistItemType, stream } /// # Panics -pub async fn force_provider_stream_response(addr: &str, - app_state: &AppState, - user_session: &UserSession, - item_type: PlaylistItemType, - req_headers: &HeaderMap, - input: &ConfigInput, - user: &ProxyUserCredentials) -> impl IntoResponse + Send { +pub async fn force_provider_stream_response( + addr: &str, + app_state: &AppState, + user_session: &UserSession, + item_type: PlaylistItemType, + req_headers: &HeaderMap, + input: &ConfigInput, + user: &ProxyUserCredentials, +) -> impl IntoResponse + Send { let stream_options = get_stream_options(app_state); let share_stream = false; let connection_permission = UserConnectionPermission::Allowed; - let mut stream_details = - create_stream_response_details(app_state, &stream_options, &user_session.stream_url, addr, req_headers, input, item_type, share_stream, connection_permission, Some(&user_session.provider)).await; + let mut stream_details = create_stream_response_details( + app_state, + &stream_options, + &user_session.stream_url, + addr, + req_headers, + input, + item_type, + share_stream, + connection_permission, + Some(&user_session.provider), + ) + .await; if stream_details.has_stream() { - let provider_response = stream_details.stream_info.as_ref().map(|(h, sc, url)| (h.clone(), *sc, url.clone())); - app_state.active_users.update_session_addr(&user.username, &user_session.token, addr); - let stream = ActiveClientStream::new(stream_details, app_state, user, connection_permission, addr); + let provider_response = stream_details + .stream_info + .as_ref() + .map(|(h, sc, url)| (h.clone(), *sc, url.clone())); + app_state + .active_users + .update_session_addr(&user.username, &user_session.token, addr); + let stream = + ActiveClientStream::new(stream_details, app_state, user, connection_permission, addr); - let (status_code, header_map) = get_stream_response_with_headers(provider_response.map(|(h, s, _)| (h, s))); + let (status_code, header_map) = + get_stream_response_with_headers(provider_response.map(|(h, s, _)| (h, s))); let mut response = axum::response::Response::builder().status(status_code); for (key, value) in &header_map { response = response.header(key, value); } let body_stream = prepare_body_stream(app_state, item_type, stream); - debug_if_enabled!("Streaming provider forced stream request from {}", sanitize_sensitive_info(&user_session.stream_url)); + debug_if_enabled!( + "Streaming provider forced stream request from {}", + sanitize_sensitive_info(&user_session.stream_url) + ); return try_unwrap_body!(response.body(body_stream)); } drop(stream_details.provider_connection_guard.take()); - if let (Some(stream), _stream_info) = - create_channel_unavailable_stream(&app_state.app_config, &[], axum::http::StatusCode::BAD_GATEWAY) - { + if let (Some(stream), _stream_info) = create_channel_unavailable_stream( + &app_state.app_config, + &[], + axum::http::StatusCode::BAD_GATEWAY, + ) { debug!("Streaming custom stream"); - try_unwrap_body!(axum::response::Response::builder().status(axum::http::StatusCode::OK).body(axum::body::Body::from_stream(stream))) + try_unwrap_body!(axum::response::Response::builder() + .status(axum::http::StatusCode::OK) + .body(axum::body::Body::from_stream(stream))) } else { axum::http::StatusCode::BAD_REQUEST.into_response() } } /// # Panics -#[allow(clippy::too_many_arguments)] -pub async fn stream_response(addr: &str, - app_state: &AppState, session_token: &str, virtual_id: u32, - item_type: PlaylistItemType, stream_url: &str, req_headers: &HeaderMap, - input: &ConfigInput, target: &ConfigTarget, user: &ProxyUserCredentials, - connection_permission: UserConnectionPermission) -> impl IntoResponse + Send { - if log_enabled!(log::Level::Trace) { trace!("Try to open stream {}", sanitize_sensitive_info(stream_url)); } +#[allow(clippy::too_many_arguments, clippy::too_many_lines)] +pub async fn stream_response( + addr: &str, + app_state: &AppState, + session_token: &str, + virtual_id: u32, + item_type: PlaylistItemType, + stream_url: &str, + req_headers: &HeaderMap, + input: &ConfigInput, + target: &ConfigTarget, + user: &ProxyUserCredentials, + connection_permission: UserConnectionPermission, +) -> impl IntoResponse + Send { + if log_enabled!(log::Level::Trace) { + trace!("Try to open stream {}", sanitize_sensitive_info(stream_url)); + } if connection_permission == UserConnectionPermission::Exhausted { - return create_custom_video_stream_response(&app_state.app_config, CustomVideoStreamType::UserConnectionsExhausted).into_response(); + return create_custom_video_stream_response( + &app_state.app_config, + CustomVideoStreamType::UserConnectionsExhausted, + ) + .into_response(); } let share_stream = is_stream_share_enabled(item_type, target); if share_stream { - if let Some(value) = shared_stream_response(app_state, stream_url, addr, user, connection_permission) { + if let Some(value) = + shared_stream_response(app_state, stream_url, addr, user, connection_permission) + { return value.into_response(); } } let stream_options = get_stream_options(app_state); - let mut stream_details = - create_stream_response_details(app_state, &stream_options, stream_url, addr, req_headers, input, item_type, share_stream, connection_permission, None).await; + let mut stream_details = create_stream_response_details( + app_state, + &stream_options, + stream_url, + addr, + req_headers, + input, + item_type, + share_stream, + connection_permission, + None, + ) + .await; if stream_details.has_stream() { // let content_length = get_stream_content_length(provider_response.as_ref()); - let provider_response = stream_details.stream_info.as_ref().map(|(h, sc, response_url)| (h.clone(), *sc, response_url.clone())); - let provider_name = stream_details.provider_connection_guard.as_ref().and_then(|guard| guard.get_provider_name()); + let provider_response = stream_details + .stream_info + .as_ref() + .map(|(h, sc, response_url)| (h.clone(), *sc, response_url.clone())); + let provider_name = stream_details + .provider_connection_guard + .as_ref() + .and_then(|guard| guard.get_provider_name()); - let provider_guard = if share_stream { stream_details.provider_connection_guard.take() } else { None }; - let stream = ActiveClientStream::new(stream_details, app_state, user, connection_permission, addr); + let provider_guard = if share_stream { + stream_details.provider_connection_guard.take() + } else { + None + }; + let stream = + ActiveClientStream::new(stream_details, app_state, user, connection_permission, addr); let stream_resp = if share_stream { - debug_if_enabled!("Streaming shared stream request from {}", sanitize_sensitive_info(stream_url)); + debug_if_enabled!( + "Streaming shared stream request from {}", + sanitize_sensitive_info(stream_url) + ); // Shared Stream response - let shared_headers = provider_response.as_ref().map_or_else(Vec::new, |(h, _, _)| h.clone()); - SharedStreamManager::subscribe(app_state, stream_url, stream, shared_headers, stream_options.buffer_size, provider_guard); - if let Some(broadcast_stream) = SharedStreamManager::subscribe_shared_stream(app_state, stream_url, Some(addr)) { - let (status_code, header_map) = get_stream_response_with_headers(provider_response.map(|(h, s, _)| (h, s))); - let mut response = axum::response::Response::builder() - .status(status_code); + let shared_headers = provider_response + .as_ref() + .map_or_else(Vec::new, |(h, _, _)| h.clone()); + SharedStreamManager::subscribe( + app_state, + stream_url, + stream, + shared_headers, + stream_options.buffer_size, + provider_guard, + ); + if let Some(broadcast_stream) = + SharedStreamManager::subscribe_shared_stream(app_state, stream_url, Some(addr)) + { + let (status_code, header_map) = + get_stream_response_with_headers(provider_response.map(|(h, s, _)| (h, s))); + let mut response = axum::response::Response::builder().status(status_code); for (key, value) in &header_map { response = response.header(key, value); } @@ -623,23 +874,52 @@ pub async fn stream_response(addr: &str, axum::http::StatusCode::BAD_REQUEST.into_response() } } else { - let session_url = provider_response.as_ref().and_then(|(_, _, u)| u.as_ref()).map_or_else(|| Cow::Borrowed(stream_url), |url| Cow::Owned(url.to_string())); + let session_url = provider_response + .as_ref() + .and_then(|(_, _, u)| u.as_ref()) + .map_or_else( + || Cow::Borrowed(stream_url), + |url| Cow::Owned(url.to_string()), + ); if log_enabled!(log::Level::Debug) { if session_url.eq(&stream_url) { - debug!("Streaming stream request from {}", sanitize_sensitive_info(stream_url)); + debug!( + "Streaming stream request from {}", + sanitize_sensitive_info(stream_url) + ); } else { - debug!("Streaming stream request for {} from {}", sanitize_sensitive_info(stream_url), sanitize_sensitive_info(&session_url)); + debug!( + "Streaming stream request for {} from {}", + sanitize_sensitive_info(stream_url), + sanitize_sensitive_info(&session_url) + ); } } - let (status_code, header_map) = get_stream_response_with_headers(provider_response.map(|(h, s, _)| (h, s))); + let (status_code, header_map) = + get_stream_response_with_headers(provider_response.map(|(h, s, _)| (h, s))); let mut response = axum::response::Response::builder().status(status_code); for (key, value) in &header_map { response = response.header(key, value); } if let Some(provider) = provider_name { - if matches!(item_type, PlaylistItemType::LiveHls | PlaylistItemType::LiveDash | PlaylistItemType::Video | PlaylistItemType::Series | PlaylistItemType::Catchup) { - let _ = app_state.active_users.create_user_session(user, session_token, virtual_id, &provider, &session_url, addr, connection_permission); + if matches!( + item_type, + PlaylistItemType::LiveHls + | PlaylistItemType::LiveDash + | PlaylistItemType::Video + | PlaylistItemType::Series + | PlaylistItemType::Catchup + ) { + let _ = app_state.active_users.create_user_session( + user, + session_token, + virtual_id, + &provider, + &session_url, + addr, + connection_permission, + ); } } @@ -654,22 +934,44 @@ pub async fn stream_response(addr: &str, } fn get_stream_throttle(app_state: &AppState) -> u64 { - app_state.app_config.config.load() + app_state + .app_config + .config + .load() .reverse_proxy .as_ref() .and_then(|reverse_proxy| reverse_proxy.stream.as_ref()) - .map(|stream| stream.throttle_kbps).unwrap_or_default() + .map(|stream| stream.throttle_kbps) + .unwrap_or_default() } -fn shared_stream_response(app_state: &AppState, stream_url: &str, addr: &str, user: &ProxyUserCredentials, connect_permission: UserConnectionPermission) -> Option { - if let Some(stream) = SharedStreamManager::subscribe_shared_stream(app_state, stream_url, Some(addr)) { - debug_if_enabled!("Using shared stream {}", sanitize_sensitive_info(stream_url)); - if let Some(headers) = app_state.shared_stream_manager.get_shared_state_headers(stream_url) { - let (status_code, header_map) = get_stream_response_with_headers(Some((headers.clone(), axum::http::StatusCode::OK))); +fn shared_stream_response( + app_state: &AppState, + stream_url: &str, + addr: &str, + user: &ProxyUserCredentials, + connect_permission: UserConnectionPermission, +) -> Option { + if let Some(stream) = + SharedStreamManager::subscribe_shared_stream(app_state, stream_url, Some(addr)) + { + debug_if_enabled!( + "Using shared stream {}", + sanitize_sensitive_info(stream_url) + ); + if let Some(headers) = app_state + .shared_stream_manager + .get_shared_state_headers(stream_url) + { + let (status_code, header_map) = get_stream_response_with_headers(Some(( + headers.clone(), + axum::http::StatusCode::OK, + ))); let stream_details = StreamDetails::from_stream(stream); - let stream = ActiveClientStream::new(stream_details, app_state, user, connect_permission, addr).boxed(); - let mut response = axum::response::Response::builder() - .status(status_code); + let stream = + ActiveClientStream::new(stream_details, app_state, user, connect_permission, addr) + .boxed(); + let mut response = axum::response::Response::builder().status(status_code); for (key, value) in &header_map { response = response.header(key, value); } @@ -680,22 +982,32 @@ fn shared_stream_response(app_state: &AppState, stream_url: &str, addr: &str, us } pub fn is_stream_share_enabled(item_type: PlaylistItemType, target: &ConfigTarget) -> bool { - (item_type == PlaylistItemType::Live /* || item_type == PlaylistItemType::LiveHls */) && target.options.as_ref().is_some_and(|opt| opt.share_live_streams) + (item_type == PlaylistItemType::Live/* || item_type == PlaylistItemType::LiveHls */) + && target + .options + .as_ref() + .is_some_and(|opt| opt.share_live_streams) } pub type HeaderFilter = Option bool + Send>>; -pub fn get_headers_from_request(req_headers: &HeaderMap, filter: &HeaderFilter) -> HashMap> { +pub fn get_headers_from_request( + req_headers: &HeaderMap, + filter: &HeaderFilter, +) -> HashMap> { req_headers .iter() .filter(|(k, _)| match &filter { None => true, - Some(predicate) => predicate(k.as_str()) + Some(predicate) => predicate(k.as_str()), }) .map(|(k, v)| (k.as_str().to_string(), v.as_bytes().to_vec())) .collect() } -fn get_add_cache_content(res_url: &str, cache: &Arc>>) -> Arc { +fn get_add_cache_content( + res_url: &str, + cache: &Arc>>, +) -> Arc { let resource_url = String::from(res_url); let cache = Arc::clone(cache); let add_cache_content: Arc = Arc::new(move |size| { @@ -711,33 +1023,56 @@ fn get_add_cache_content(res_url: &str, cache: &Arc) -> impl IntoResponse + Send { +pub async fn resource_response( + app_state: &AppState, + resource_url: &str, + req_headers: &HeaderMap, + input: Option<&ConfigInput>, +) -> impl IntoResponse + Send { if resource_url.is_empty() { return axum::http::StatusCode::NO_CONTENT.into_response(); } - let filter: HeaderFilter = Some(Box::new(|key| key != "if-none-match" && key != "if-modified-since")); + let filter: HeaderFilter = Some(Box::new(|key| { + key != "if-none-match" && key != "if-modified-since" + })); let req_headers = get_headers_from_request(req_headers, &filter); if let Some(cache) = app_state.cache.load().as_ref() { let mut guard = cache.lock().await; if let Some(resource_path) = guard.get_content(resource_url) { - trace_if_enabled!("Responding resource from cache {}", sanitize_sensitive_info(resource_url)); - return serve_file(&resource_path, mime::APPLICATION_OCTET_STREAM).await.into_response(); + trace_if_enabled!( + "Responding resource from cache {}", + sanitize_sensitive_info(resource_url) + ); + return serve_file(&resource_path, mime::APPLICATION_OCTET_STREAM) + .await + .into_response(); } } - trace_if_enabled!("Try to fetch resource {}", sanitize_sensitive_info(resource_url)); + trace_if_enabled!( + "Try to fetch resource {}", + sanitize_sensitive_info(resource_url) + ); if let Ok(url) = Url::parse(resource_url) { - let client = request::get_client_request(&app_state.http_client.load(), input.map_or(InputFetchMethod::GET, |i| i.method), input.map(|i| &i.headers), &url, Some(&req_headers)); + let client = request::get_client_request( + &app_state.http_client.load(), + input.map_or(InputFetchMethod::GET, |i| i.method), + input.map(|i| &i.headers), + &url, + Some(&req_headers), + ); match client.send().await { Ok(response) => { let status = response.status(); if status.is_success() { - let mut response_builder = axum::response::Response::builder() - .status(axum::http::StatusCode::OK); + let mut response_builder = + axum::response::Response::builder().status(axum::http::StatusCode::OK); for (key, value) in response.headers() { response_builder = response_builder.header(key, value); } - let byte_stream = response.bytes_stream().map_err(|err| StreamError::reqwest(&err)); + let byte_stream = response + .bytes_stream() + .map_err(|err| StreamError::reqwest(&err)); let cache_resource_path = { if let Some(cache) = app_state.cache.load().as_ref() { Some(cache.lock().await.store_path(resource_url)) @@ -748,17 +1083,31 @@ pub async fn resource_response(app_state: &AppState, resource_url: &str, req_hea if let Some(resource_path) = cache_resource_path { if let Ok(file) = create_new_file_for_write(&resource_path) { let writer = BufWriter::new(file); - let add_cache_content = get_add_cache_content(resource_url, &app_state.cache); - let stream = PersistPipeStream::new(byte_stream, writer, add_cache_content); - return try_unwrap_body!(response_builder.body(axum::body::Body::from_stream(stream))); + let add_cache_content = + get_add_cache_content(resource_url, &app_state.cache); + let stream = + PersistPipeStream::new(byte_stream, writer, add_cache_content); + return try_unwrap_body!( + response_builder.body(axum::body::Body::from_stream(stream)) + ); } } - return try_unwrap_body!(response_builder.body(axum::body::Body::from_stream(byte_stream))); + return try_unwrap_body!( + response_builder.body(axum::body::Body::from_stream(byte_stream)) + ); } - debug_if_enabled!("Failed to open resource got status {} for {}", status, sanitize_sensitive_info(resource_url)); + debug_if_enabled!( + "Failed to open resource got status {} for {}", + status, + sanitize_sensitive_info(resource_url) + ); } Err(err) => { - error!("Received failure from server {}: {}", sanitize_sensitive_info(resource_url), err); + error!( + "Received failure from server {}: {}", + sanitize_sensitive_info(resource_url), + err + ); } } } else { @@ -768,11 +1117,14 @@ pub async fn resource_response(app_state: &AppState, resource_url: &str, req_hea } pub fn separate_number_and_remainder(input: &str) -> (String, Option) { - input.rfind('.').map_or_else(|| (input.to_string(), None), |dot_index| { - let number_part = input[..dot_index].to_string(); - let rest = input[dot_index..].to_string(); - (number_part, if rest.len() < 2 { None } else { Some(rest) }) - }) + input.rfind('.').map_or_else( + || (input.to_string(), None), + |dot_index| { + let number_part = input[..dot_index].to_string(); + let rest = input[dot_index..].to_string(); + (number_part, if rest.len() < 2 { None } else { Some(rest) }) + }, + ) } /// # Panics @@ -783,11 +1135,15 @@ pub fn empty_json_list_response() -> impl IntoResponse + Send { .body("[]".to_string())) } -pub fn get_username_from_auth_header( - token: &str, - app_state: &Arc, -) -> Option { - if let Some(web_auth_config) = &app_state.app_config.config.load().web_ui.as_ref().and_then(|c| c.auth.as_ref()) { +pub fn get_username_from_auth_header(token: &str, app_state: &Arc) -> Option { + if let Some(web_auth_config) = &app_state + .app_config + .config + .load() + .web_ui + .as_ref() + .and_then(|c| c.auth.as_ref()) + { let secret_key: &[u8] = web_auth_config.secret.as_ref(); if let Ok(token_data) = decode::( token, @@ -808,10 +1164,7 @@ pub fn redirect(url: &str) -> impl IntoResponse { .body(axum::body::Body::empty())) } -pub async fn is_seek_request( - cluster: XtreamCluster, - req_headers: &HeaderMap, -) -> bool { +pub async fn is_seek_request(cluster: XtreamCluster, req_headers: &HeaderMap) -> bool { // seek only for non-live streams if cluster == XtreamCluster::Live { return false; diff --git a/backend/src/api/config_watch.rs b/backend/src/api/config_watch.rs index dcf6b7112..b99cc84c3 100644 --- a/backend/src/api/config_watch.rs +++ b/backend/src/api/config_watch.rs @@ -1,4 +1,4 @@ -use crate::api::model::app_state::{update_app_state_config, update_app_state_sources, AppState}; +use crate::api::model::{update_app_state_config, update_app_state_sources, AppState}; use crate::model::{Config, SourcesConfig}; use crate::utils; use crate::utils::{is_directory, read_config_file, read_sources_file}; diff --git a/backend/src/api/endpoints/download_api.rs b/backend/src/api/endpoints/download_api.rs index e04c146b5..a1eea319a 100644 --- a/backend/src/api/endpoints/download_api.rs +++ b/backend/src/api/endpoints/download_api.rs @@ -1,5 +1,5 @@ -use crate::api::model::app_state::AppState; -use crate::api::model::download::{DownloadQueue, FileDownload, FileDownloadRequest}; +use crate::api::model::AppState; +use crate::api::model::{DownloadQueue, FileDownload, FileDownloadRequest}; use crate::model::{AppConfig, Config, VideoDownloadConfig}; use crate::utils::request; use tokio::sync::RwLock; diff --git a/backend/src/api/endpoints/hdhomerun_api.rs b/backend/src/api/endpoints/hdhomerun_api.rs index 23aedb0fc..cd972574b 100644 --- a/backend/src/api/endpoints/hdhomerun_api.rs +++ b/backend/src/api/endpoints/hdhomerun_api.rs @@ -1,4 +1,4 @@ -use crate::api::model::app_state::HdHomerunAppState; +use crate::api::model::HdHomerunAppState; use crate::auth::AuthBasic; use crate::model::{AppConfig, ConfigTarget, ProxyUserCredentials}; use shared::model::{M3uPlaylistItem, PlaylistItemType, TargetType, XtreamCluster, XtreamPlaylistItem}; diff --git a/backend/src/api/endpoints/hls_api.rs b/backend/src/api/endpoints/hls_api.rs index 28c5eab53..8f0b9204e 100644 --- a/backend/src/api/endpoints/hls_api.rs +++ b/backend/src/api/endpoints/hls_api.rs @@ -1,13 +1,17 @@ -use crate::api::api_utils::{force_provider_stream_response, get_stream_alternative_url, is_seek_request}; use crate::api::api_utils::try_option_bad_request; -use crate::api::model::active_user_manager::UserSession; -use crate::api::model::app_state::AppState; -use crate::api::model::streams::provider_stream::{create_custom_video_stream_response, CustomVideoStreamType}; +use crate::api::api_utils::try_unwrap_body; +use crate::api::api_utils::{ + force_provider_stream_response, get_stream_alternative_url, is_seek_request, +}; +use crate::api::model::AppState; +use crate::api::model::UserSession; +use crate::api::model::{create_custom_video_stream_response, CustomVideoStreamType}; use crate::auth::Fingerprint; use crate::model::ConfigInput; use crate::model::ProxyUserCredentials; -use crate::processing::parser::hls::{get_hls_session_token_and_url_from_token, rewrite_hls, RewriteHlsProps}; -use crate::api::api_utils::try_unwrap_body; +use crate::processing::parser::hls::{ + get_hls_session_token_and_url_from_token, rewrite_hls, RewriteHlsProps, +}; use crate::utils::request; use axum::response::IntoResponse; use log::{debug, error}; @@ -34,20 +38,27 @@ fn hls_response(hls_content: String) -> impl IntoResponse + Send { #[allow(clippy::too_many_arguments)] pub(in crate::api) async fn handle_hls_stream_request( - fingerprint: &str, addr: &str, + fingerprint: &str, + addr: &str, app_state: &Arc, user: &ProxyUserCredentials, user_session: Option<&UserSession>, hls_url: &str, virtual_id: u32, input: &ConfigInput, - connection_permission: UserConnectionPermission) -> impl IntoResponse + Send { + connection_permission: UserConnectionPermission, +) -> impl IntoResponse + Send { let url = replace_url_extension(hls_url, HLS_EXT); let server_info = app_state.app_config.get_user_server_info(user); let (request_url, session_token) = match user_session { Some(session) => { - match app_state.active_provider.force_exact_acquire_connection(&session.provider, addr).await.get_provider_config() { + match app_state + .active_provider + .force_exact_acquire_connection(&session.provider, addr) + .await + .get_provider_config() + { Some(provider_cfg) => { let stream_url = get_stream_alternative_url(&url, input, &provider_cfg); (stream_url, Some(session.token.to_string())) @@ -56,11 +67,23 @@ pub(in crate::api) async fn handle_hls_stream_request( } } None => { - match app_state.active_provider.get_next_provider(&input.name).await { + match app_state + .active_provider + .get_next_provider(&input.name) + .await + { Some(provider_cfg) => { let stream_url = get_stream_alternative_url(&url, input, &provider_cfg); let user_session_token = format!("{fingerprint}{virtual_id}"); - let session_token = app_state.active_users.create_user_session(user, &user_session_token, virtual_id, &provider_cfg.name, &stream_url, addr, connection_permission); + let session_token = app_state.active_users.create_user_session( + user, + &user_session_token, + virtual_id, + &provider_cfg.name, + &stream_url, + addr, + connection_permission, + ); (stream_url, Some(session_token)) } None => (url, None), @@ -68,7 +91,14 @@ pub(in crate::api) async fn handle_hls_stream_request( } }; - match request::download_text_content(Arc::clone(&app_state.http_client.load()), input, &request_url, None).await { + match request::download_text_content( + Arc::clone(&app_state.http_client.load()), + input, + &request_url, + None, + ) + .await + { Ok((content, response_url)) => { let rewrite_hls_props = RewriteHlsProps { secret: &app_state.app_config.encrypt_secret, @@ -83,12 +113,20 @@ pub(in crate::api) async fn handle_hls_stream_request( hls_response(hls_content).into_response() } Err(err) => { - error!("Failed to download m3u8 {}", sanitize_sensitive_info(err.to_string().as_str())); - create_custom_video_stream_response(&app_state.app_config, CustomVideoStreamType::ChannelUnavailable).into_response() + error!( + "Failed to download m3u8 {}", + sanitize_sensitive_info(err.to_string().as_str()) + ); + create_custom_video_stream_response( + &app_state.app_config, + CustomVideoStreamType::ChannelUnavailable, + ) + .into_response() } } } +#[allow(clippy::too_many_lines)] async fn hls_api_stream( Fingerprint(fingerprint, addr): Fingerprint, req_headers: axum::http::HeaderMap, @@ -96,29 +134,61 @@ async fn hls_api_stream( axum::extract::State(app_state): axum::extract::State>, ) -> impl axum::response::IntoResponse + Send { let (user, target) = try_option_bad_request!( - app_state.app_config.get_target_for_user(¶ms.username, ¶ms.password), false, - format!("Could not find any user for hls stream {}", params.username)); + app_state + .app_config + .get_target_for_user(¶ms.username, ¶ms.password), + false, + format!("Could not find any user for hls stream {}", params.username) + ); if user.permission_denied(&app_state) { - return create_custom_video_stream_response(&app_state.app_config, CustomVideoStreamType::UserAccountExpired).into_response(); + return create_custom_video_stream_response( + &app_state.app_config, + CustomVideoStreamType::UserAccountExpired, + ) + .into_response(); } let target_name = &target.name; let virtual_id = params.stream_id; - let input = try_option_bad_request!(app_state.app_config.get_input_by_id(params.input_id), true, format!("Cant find input for target {target_name}, context {}, stream_id {virtual_id}", XtreamCluster::Live)); + let input = try_option_bad_request!( + app_state.app_config.get_input_by_id(params.input_id), + true, + format!( + "Cant find input for target {target_name}, context {}, stream_id {virtual_id}", + XtreamCluster::Live + ) + ); let user_session_token = format!("{fingerprint}{virtual_id}"); - let mut user_session = app_state.active_users.get_user_session(&user.username, &user_session_token); + let mut user_session = app_state + .active_users + .get_user_session(&user.username, &user_session_token); if let Some(session) = &mut user_session { if session.permission == UserConnectionPermission::Exhausted { - return create_custom_video_stream_response(&app_state.app_config, CustomVideoStreamType::UserConnectionsExhausted).into_response(); + return create_custom_video_stream_response( + &app_state.app_config, + CustomVideoStreamType::UserConnectionsExhausted, + ) + .into_response(); } - if app_state.active_provider.is_over_limit(&session.provider).await { - return create_custom_video_stream_response(&app_state.app_config, CustomVideoStreamType::ProviderConnectionsExhausted).into_response(); + if app_state + .active_provider + .is_over_limit(&session.provider) + .await + { + return create_custom_video_stream_response( + &app_state.app_config, + CustomVideoStreamType::ProviderConnectionsExhausted, + ) + .into_response(); } - let hls_url = match get_hls_session_token_and_url_from_token(&app_state.app_config.encrypt_secret, ¶ms.token) { + let hls_url = match get_hls_session_token_and_url_from_token( + &app_state.app_config.encrypt_secret, + ¶ms.token, + ) { Some((Some(session_token), hls_url)) if session.token.eq(&session_token) => hls_url, _ => return axum::http::StatusCode::BAD_REQUEST.into_response(), }; @@ -127,7 +197,17 @@ async fn hls_api_stream( if session.virtual_id == virtual_id { if is_seek_request(XtreamCluster::Live, &req_headers).await { // partial request means we are in reverse proxy mode, seek happened - return force_provider_stream_response(&addr, &app_state, session, PlaylistItemType::LiveHls, &req_headers, &input, &user).await.into_response(); + return force_provider_stream_response( + &addr, + &app_state, + session, + PlaylistItemType::LiveHls, + &req_headers, + &input, + &user, + ) + .await + .into_response(); } } else { return axum::http::StatusCode::BAD_REQUEST.into_response(); @@ -135,22 +215,50 @@ async fn hls_api_stream( let connection_permission = user.connection_permission(&app_state); if connection_permission == UserConnectionPermission::Exhausted { - return create_custom_video_stream_response(&app_state.app_config, CustomVideoStreamType::UserConnectionsExhausted).into_response(); + return create_custom_video_stream_response( + &app_state.app_config, + CustomVideoStreamType::UserConnectionsExhausted, + ) + .into_response(); } if is_hls_url(&session.stream_url) { - return handle_hls_stream_request(&fingerprint, &addr, &app_state, &user, Some(session), &session.stream_url, virtual_id, &input, connection_permission).await.into_response(); + return handle_hls_stream_request( + &fingerprint, + &addr, + &app_state, + &user, + Some(session), + &session.stream_url, + virtual_id, + &input, + connection_permission, + ) + .await + .into_response(); } - force_provider_stream_response(&addr, &app_state, session, PlaylistItemType::LiveHls, &req_headers, &input, &user).await.into_response() + force_provider_stream_response( + &addr, + &app_state, + session, + PlaylistItemType::LiveHls, + &req_headers, + &input, + &user, + ) + .await + .into_response() } else { axum::http::StatusCode::BAD_REQUEST.into_response() } } pub fn hls_api_register() -> axum::Router> { - axum::Router::new() - .route("/hls/{username}/{password}/{input_id}/{stream_id}/{token}", axum::routing::get(hls_api_stream)) + axum::Router::new().route( + "/hls/{username}/{password}/{input_id}/{stream_id}/{token}", + axum::routing::get(hls_api_stream), + ) //cfg.service(web::resource("/hls/{token}/{stream}").route(web::get().to(xtream_player_api_hls_stream))); //cfg.service(web::resource("/play/{token}/{type}").route(web::get().to(xtream_player_api_play_stream))); } diff --git a/backend/src/api/endpoints/m3u_api.rs b/backend/src/api/endpoints/m3u_api.rs index a176804a0..6e3a31a77 100644 --- a/backend/src/api/endpoints/m3u_api.rs +++ b/backend/src/api/endpoints/m3u_api.rs @@ -1,37 +1,51 @@ -use crate::api::api_utils::{force_provider_stream_response, get_user_target, get_user_target_by_credentials, is_seek_request, redirect, redirect_response, resource_response, separate_number_and_remainder, stream_response, try_option_bad_request, try_result_bad_request, RedirectParams}; +use crate::api::api_utils::try_unwrap_body; +use crate::api::api_utils::{ + force_provider_stream_response, get_user_target, get_user_target_by_credentials, + is_seek_request, redirect, redirect_response, resource_response, separate_number_and_remainder, + stream_response, try_option_bad_request, try_result_bad_request, RedirectParams, +}; use crate::api::endpoints::hls_api::handle_hls_stream_request; use crate::api::endpoints::xtream_api::{ApiStreamContext, ApiStreamRequest}; -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 shared::model::{FieldGetAccessor, PlaylistEntry, PlaylistItemType, TargetType, UserConnectionPermission, XtreamCluster}; +use crate::api::model::AppState; +use crate::api::model::UserApiRequest; +use crate::api::model::{create_custom_video_stream_response, CustomVideoStreamType}; +use crate::auth::Fingerprint; use crate::repository::m3u_repository::{m3u_get_item_for_stream_id, m3u_load_rewrite_playlist}; use crate::repository::storage_const; -use shared::utils::{extract_extension_from_url, sanitize_sensitive_info, HLS_EXT}; use axum::response::IntoResponse; use bytes::Bytes; use futures::stream; use log::{debug, error}; +use shared::model::{ + FieldGetAccessor, PlaylistEntry, PlaylistItemType, TargetType, UserConnectionPermission, + XtreamCluster, +}; +use shared::utils::{extract_extension_from_url, sanitize_sensitive_info, HLS_EXT}; use std::sync::Arc; -use crate::auth::Fingerprint; -use crate::api::api_utils::try_unwrap_body; -async fn m3u_api( - api_req: &UserApiRequest, - app_state: &AppState, -) -> impl IntoResponse + Send { +async fn m3u_api(api_req: &UserApiRequest, app_state: &AppState) -> impl IntoResponse + Send { match get_user_target(api_req, app_state) { Some((user, target)) => { match m3u_load_rewrite_playlist(&app_state.app_config, &target, &user).await { Ok(m3u_iter) => { // Convert the iterator into a stream of `Bytes` - let content_stream = stream::iter(m3u_iter.map(|line| Ok::(Bytes::from([line.to_string().as_bytes(), b"\n"].concat())))); + let content_stream = stream::iter(m3u_iter.map(|line| { + Ok::(Bytes::from( + [line.to_string().as_bytes(), b"\n"].concat(), + )) + })); let mut builder = axum::response::Response::builder() .status(axum::http::StatusCode::OK) - .header(axum::http::header::CONTENT_TYPE, mime::TEXT_PLAIN_UTF_8.to_string()); + .header( + axum::http::header::CONTENT_TYPE, + mime::TEXT_PLAIN_UTF_8.to_string(), + ); if api_req.content_type == "m3u_plus" { - builder = builder.header("Content-Disposition", "attachment; filename=\"playlist.m3u\""); + builder = builder.header( + "Content-Disposition", + "attachment; filename=\"playlist.m3u\"", + ); } try_unwrap_body!(builder.body(axum::body::Body::from_stream(content_stream))) } @@ -45,9 +59,9 @@ async fn m3u_api( } } - -async fn m3u_api_get(axum::extract::Query(api_req): axum::extract::Query, - axum::extract::State(app_state): axum::extract::State>, +async fn m3u_api_get( + axum::extract::Query(api_req): axum::extract::Query, + axum::extract::State(app_state): axum::extract::State>, ) -> impl IntoResponse + Send { m3u_api(&api_req, &app_state).await } @@ -59,6 +73,7 @@ async fn m3u_api_post( m3u_api(&api_req, &app_state).await.into_response() } +#[allow(clippy::too_many_lines)] async fn m3u_api_stream( fingerprint: &str, addr: &str, @@ -68,9 +83,25 @@ async fn m3u_api_stream( stream_req: ApiStreamRequest<'_>, // _addr: &std::net::SocketAddr, ) -> impl IntoResponse + Send { - let (user, target) = try_option_bad_request!(get_user_target_by_credentials(stream_req.username, stream_req.password, api_req, app_state), false, format!("Could not find any user for m3u stream {}", stream_req.username)); + let (user, target) = try_option_bad_request!( + get_user_target_by_credentials( + stream_req.username, + stream_req.password, + api_req, + app_state + ), + false, + format!( + "Could not find any user for m3u stream {}", + stream_req.username + ) + ); if user.permission_denied(app_state) { - return create_custom_video_stream_response(&app_state.app_config, CustomVideoStreamType::UserAccountExpired).into_response(); + return create_custom_video_stream_response( + &app_state.app_config, + CustomVideoStreamType::UserAccountExpired, + ) + .into_response(); } let target_name = &target.name; @@ -81,25 +112,58 @@ async fn m3u_api_stream( let (action_stream_id, stream_ext) = separate_number_and_remainder(stream_req.stream_id); let virtual_id: u32 = try_result_bad_request!(action_stream_id.trim().parse()); - let pli = try_result_bad_request!(m3u_get_item_for_stream_id(virtual_id, &app_state.app_config, &target).await, true, format!("Failed to read m3u item for stream id {}", virtual_id)); - let input = try_option_bad_request!(app_state.app_config.get_input_by_name(pli.input_name.as_str()), true, format!("Cant find input for target {target_name}, stream_id {virtual_id}")); + let pli = try_result_bad_request!( + m3u_get_item_for_stream_id(virtual_id, &app_state.app_config, &target).await, + true, + format!("Failed to read m3u item for stream id {}", virtual_id) + ); + let input = try_option_bad_request!( + app_state + .app_config + .get_input_by_name(pli.input_name.as_str()), + true, + format!("Cant find input for target {target_name}, stream_id {virtual_id}") + ); let cluster = XtreamCluster::try_from(pli.item_type).unwrap_or(XtreamCluster::Live); - let session_key = format!("{fingerprint}{virtual_id}"); - let user_session = app_state.active_users.get_user_session(&user.username, &session_key); + let user_session = app_state + .active_users + .get_user_session(&user.username, &session_key); let session_url = if let Some(session) = &user_session { if session.permission == UserConnectionPermission::Exhausted { - return create_custom_video_stream_response(&app_state.app_config, CustomVideoStreamType::UserConnectionsExhausted).into_response(); + return create_custom_video_stream_response( + &app_state.app_config, + CustomVideoStreamType::UserConnectionsExhausted, + ) + .into_response(); } - if app_state.active_provider.is_over_limit(&session.provider).await { - return create_custom_video_stream_response(&app_state.app_config, CustomVideoStreamType::ProviderConnectionsExhausted).into_response(); + if app_state + .active_provider + .is_over_limit(&session.provider) + .await + { + return create_custom_video_stream_response( + &app_state.app_config, + CustomVideoStreamType::ProviderConnectionsExhausted, + ) + .into_response(); } if session.virtual_id == virtual_id && is_seek_request(cluster, req_headers).await { // partial request means we are in reverse proxy mode, seek happened - return force_provider_stream_response(addr, app_state, session, pli.item_type, req_headers, &input, &user).await.into_response(); + return force_provider_stream_response( + addr, + app_state, + session, + pli.item_type, + req_headers, + &input, + &user, + ) + .await + .into_response(); } session.stream_url.as_str() } else { @@ -108,7 +172,11 @@ async fn m3u_api_stream( let connection_permission = user.connection_permission(app_state); if connection_permission == UserConnectionPermission::Exhausted { - return create_custom_video_stream_response(&app_state.app_config, CustomVideoStreamType::UserConnectionsExhausted).into_response(); + return create_custom_video_stream_response( + &app_state.app_config, + CustomVideoStreamType::UserConnectionsExhausted, + ) + .into_response(); } let context = ApiStreamContext::try_from(cluster).unwrap_or(ApiStreamContext::Live); @@ -130,27 +198,67 @@ async fn m3u_api_stream( return response.into_response(); } - let extension = stream_ext.unwrap_or_else( - || extract_extension_from_url(&pli.url).map_or_else(String::new, std::string::ToString::to_string)); + let extension = stream_ext.unwrap_or_else(|| { + extract_extension_from_url(&pli.url) + .map_or_else(String::new, std::string::ToString::to_string) + }); - let is_hls_request = pli.item_type == PlaylistItemType::LiveHls || pli.item_type == PlaylistItemType::LiveDash || extension == HLS_EXT; + let is_hls_request = pli.item_type == PlaylistItemType::LiveHls + || pli.item_type == PlaylistItemType::LiveDash + || extension == HLS_EXT; // Reverse proxy mode if is_hls_request { - return handle_hls_stream_request(fingerprint, addr, app_state, &user, user_session.as_ref(), &pli.url, pli.virtual_id, &input, connection_permission).await.into_response(); + return handle_hls_stream_request( + fingerprint, + addr, + app_state, + &user, + user_session.as_ref(), + &pli.url, + pli.virtual_id, + &input, + connection_permission, + ) + .await + .into_response(); } - stream_response(addr, app_state, &session_key, pli.virtual_id, pli.item_type, session_url, req_headers, &input, &target, &user, connection_permission).await.into_response() + stream_response( + addr, + app_state, + &session_key, + pli.virtual_id, + pli.item_type, + session_url, + req_headers, + &input, + &target, + &user, + connection_permission, + ) + .await + .into_response() } async fn m3u_api_resource( req_headers: axum::http::HeaderMap, axum::extract::Query(api_req): axum::extract::Query, - axum::extract::Path((username, password, stream_id, resource)): axum::extract::Path<(String, String, String, String)>, + axum::extract::Path((username, password, stream_id, resource)): axum::extract::Path<( + String, + String, + String, + String, + )>, axum::extract::State(app_state): axum::extract::State>, ) -> impl IntoResponse + Send { - let Ok(m3u_stream_id) = stream_id.parse::() else { return axum::http::StatusCode::BAD_REQUEST.into_response() }; - let Some((user, target)) = get_user_target_by_credentials(&username, &password, &api_req, &app_state) - else { return axum::http::StatusCode::BAD_REQUEST.into_response() }; + let Ok(m3u_stream_id) = stream_id.parse::() else { + return axum::http::StatusCode::BAD_REQUEST.into_response(); + }; + let Some((user, target)) = + get_user_target_by_credentials(&username, &password, &api_req, &app_state) + else { + return axum::http::StatusCode::BAD_REQUEST.into_response(); + }; if user.permission_denied(&app_state) { return axum::http::StatusCode::FORBIDDEN.into_response(); } @@ -160,23 +268,34 @@ async fn m3u_api_resource( debug!("Target has no m3u playlist {target_name}"); return axum::http::StatusCode::BAD_REQUEST.into_response(); } - let m3u_item = match m3u_get_item_for_stream_id(m3u_stream_id, &app_state.app_config, &target).await { - Ok(item) => item, - Err(err) => { - error!("Failed to get m3u url: {}", sanitize_sensitive_info(err.to_string().as_str())); - return axum::http::StatusCode::BAD_REQUEST.into_response(); - } - }; + let m3u_item = + match m3u_get_item_for_stream_id(m3u_stream_id, &app_state.app_config, &target).await { + Ok(item) => item, + Err(err) => { + error!( + "Failed to get m3u url: {}", + sanitize_sensitive_info(err.to_string().as_str()) + ); + return axum::http::StatusCode::BAD_REQUEST.into_response(); + } + }; let stream_url = m3u_item.get_field(resource.as_str()); match stream_url { None => axum::http::StatusCode::NOT_FOUND.into_response(), Some(url) => { - if user.proxy.is_redirect(m3u_item.item_type) || target.is_force_redirect(m3u_item.item_type) { - debug!("Redirecting stream request to {}", sanitize_sensitive_info(&url)); + if user.proxy.is_redirect(m3u_item.item_type) + || target.is_force_redirect(m3u_item.item_type) + { + debug!( + "Redirecting stream request to {}", + sanitize_sensitive_info(&url) + ); redirect(&url).into_response() } else { - resource_response(&app_state, &url, &req_headers, None).await.into_response() + resource_response(&app_state, &url, &req_headers, None) + .await + .into_response() } } } @@ -188,10 +307,14 @@ macro_rules! create_m3u_api_stream { Fingerprint(fingerprint, addr): Fingerprint, req_headers: axum::http::HeaderMap, axum::extract::Query(api_req): axum::extract::Query, - axum::extract::Path((username, password, stream_id)): axum::extract::Path<(String, String, String)>, + axum::extract::Path((username, password, stream_id)): axum::extract::Path<( + String, + String, + String, + )>, axum::extract::State(app_state): axum::extract::State>, // axum::extract::ConnectInfo(addr): axum::extract::ConnectInfo, - ) -> impl IntoResponse + Send { + ) -> impl IntoResponse + Send { m3u_api_stream( &fingerprint, &addr, @@ -199,9 +322,11 @@ macro_rules! create_m3u_api_stream { &app_state, &api_req, ApiStreamRequest::from($context, &username, &password, &stream_id, ""), - ).await.into_response() + ) + .await + .into_response() } - } + }; } create_m3u_api_stream!(m3u_api_live_stream_alt, ApiStreamContext::LiveAlt); @@ -209,7 +334,6 @@ create_m3u_api_stream!(m3u_api_live_stream, ApiStreamContext::Live); create_m3u_api_stream!(m3u_api_series_stream, ApiStreamContext::Series); create_m3u_api_stream!(m3u_api_movie_stream, ApiStreamContext::Movie); - macro_rules! register_m3u_api_stream { ($router:expr, [$(($path:expr, $fn_name:ident)),*]) => {{ $router @@ -234,12 +358,30 @@ macro_rules! register_m3u_api_routes { pub fn m3u_api_register() -> axum::Router> { let mut router = axum::Router::new(); router = register_m3u_api_routes!(router, ["get.php", "apiget", "m3u"]); - router = register_m3u_api_stream!(router, [ - (storage_const::M3U_STREAM_PATH, m3u_api_live_stream_alt), - (format!("{}/live", storage_const::M3U_STREAM_PATH), m3u_api_live_stream), - (format!("{}/movie", storage_const::M3U_STREAM_PATH), m3u_api_movie_stream), - (format!("{}/series", storage_const::M3U_STREAM_PATH), m3u_api_series_stream)]); + router = register_m3u_api_stream!( + router, + [ + (storage_const::M3U_STREAM_PATH, m3u_api_live_stream_alt), + ( + format!("{}/live", storage_const::M3U_STREAM_PATH), + m3u_api_live_stream + ), + ( + format!("{}/movie", storage_const::M3U_STREAM_PATH), + m3u_api_movie_stream + ), + ( + format!("{}/series", storage_const::M3U_STREAM_PATH), + m3u_api_series_stream + ) + ] + ); - router - .route(&format!("/{}/{{username}}/{{password}}/{{stream_id}}/{{resource}}", storage_const::M3U_RESOURCE_PATH), axum::routing::get(m3u_api_resource)) -} \ No newline at end of file + router.route( + &format!( + "/{}/{{username}}/{{password}}/{{stream_id}}/{{resource}}", + storage_const::M3U_RESOURCE_PATH + ), + axum::routing::get(m3u_api_resource), + ) +} diff --git a/backend/src/api/endpoints/user_api.rs b/backend/src/api/endpoints/user_api.rs index e21972604..de3b103b9 100644 --- a/backend/src/api/endpoints/user_api.rs +++ b/backend/src/api/endpoints/user_api.rs @@ -1,5 +1,5 @@ use crate::api::api_utils::{get_user_target_by_username, get_username_from_auth_header}; -use crate::api::model::app_state::AppState; +use crate::api::model::AppState; use crate::auth::validator_user; use crate::model::{AppConfig, ConfigTarget}; use shared::model::{TargetType, XtreamCluster}; diff --git a/backend/src/api/endpoints/v1_api.rs b/backend/src/api/endpoints/v1_api.rs index f73144d60..69ae32775 100644 --- a/backend/src/api/endpoints/v1_api.rs +++ b/backend/src/api/endpoints/v1_api.rs @@ -1,8 +1,8 @@ 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; -use crate::api::model::request::{PlaylistRequest, PlaylistRequestType}; +use crate::api::model::AppState; +use crate::api::model::{PlaylistRequest, PlaylistRequestType}; use crate::auth::create_access_token; use crate::auth::validator_admin; use crate::model::{get_batch_aliases, TargetUser}; diff --git a/backend/src/api/endpoints/web_index.rs b/backend/src/api/endpoints/web_index.rs index d38bb3ef0..ce2b5b735 100644 --- a/backend/src/api/endpoints/web_index.rs +++ b/backend/src/api/endpoints/web_index.rs @@ -1,6 +1,6 @@ use crate::api::api_utils::serve_file; use crate::api::api_utils::try_unwrap_body; -use crate::api::model::app_state::AppState; +use crate::api::model::AppState; use crate::auth::{create_jwt_admin, create_jwt_user, is_admin, verify_password, verify_token, AuthBearer}; use axum::response::IntoResponse; use log::error; diff --git a/backend/src/api/endpoints/websocket_api.rs b/backend/src/api/endpoints/websocket_api.rs index cf3ce9fa3..e68dac948 100644 --- a/backend/src/api/endpoints/websocket_api.rs +++ b/backend/src/api/endpoints/websocket_api.rs @@ -1,5 +1,6 @@ use crate::api::endpoints::v1_api::create_status_check; -use crate::api::model::app_state::AppState; +use crate::api::model::AppState; +use crate::api::model::EventMessage; use crate::auth::verify_token_admin; use axum::extract::ws::CloseFrame; use axum::{ @@ -9,12 +10,12 @@ use axum::{ use log::{error, info}; use shared::model::{ProtocolHandler, ProtocolMessage, WsCloseCode, PROTOCOL_VERSION}; use std::sync::Arc; -use crate::api::model::event_manager::EventMessage; // WebSocket upgrade handler async fn websocket_handler( axum::extract::State(app_state): axum::extract::State>, - ws: WebSocketUpgrade) -> impl IntoResponse { + ws: WebSocketUpgrade, +) -> impl IntoResponse { info!("Websocket connected"); ws.on_upgrade(move |socket| handle_socket(socket, app_state, false)) } @@ -22,16 +23,23 @@ async fn websocket_handler( // WebSocket upgrade handler async fn websocket_handler_auth( axum::extract::State(app_state): axum::extract::State>, - ws: WebSocketUpgrade) -> impl IntoResponse { + ws: WebSocketUpgrade, +) -> impl IntoResponse { info!("Websocket connected"); ws.on_upgrade(move |socket| handle_socket(socket, app_state, true)) } pub fn ws_api_register(web_auth_enabled: bool, web_ui_path: &str) -> axum::Router> { if web_auth_enabled { - axum::Router::new().route(&format!("{web_ui_path}/ws"), axum::routing::get(websocket_handler_auth)) + axum::Router::new().route( + &format!("{web_ui_path}/ws"), + axum::routing::get(websocket_handler_auth), + ) } else { - axum::Router::new().route(&format!("{web_ui_path}/ws"), axum::routing::get(websocket_handler)) + axum::Router::new().route( + &format!("{web_ui_path}/ws"), + axum::routing::get(websocket_handler), + ) } } @@ -39,7 +47,7 @@ pub fn ws_api_register(web_auth_enabled: bool, web_ui_path: &str) -> axum::Route fn verify_auth_admin_token(auth_token: &str, secret_key: Option<&Vec>) -> bool { match secret_key.as_ref() { None => false, - Some(key) => verify_token_admin(auth_token, key.as_slice()) + Some(key) => verify_token_admin(auth_token, key.as_slice()), } } @@ -48,8 +56,12 @@ fn get_secret_key(app_state: &AppState, auth: bool) -> Option> { return None; } - app_state.app_config.config.load() - .web_ui.as_ref() + app_state + .app_config + .config + .load() + .web_ui + .as_ref() .and_then(|c| c.auth.as_ref()) .map(|c| { let secret_key: &[u8] = c.secret.as_ref(); @@ -57,26 +69,27 @@ fn get_secret_key(app_state: &AppState, auth: bool) -> Option> { }) } -async fn handle_handshake( - msg: Message, - socket: &mut WebSocket, - version: u8, -) -> Result<(), String> { +async fn handle_handshake(msg: Message, socket: &mut WebSocket, version: u8) -> Result<(), String> { if let Message::Binary(bytes) = msg { if bytes.len() == 1 { let client_version = bytes[0]; if client_version == version { - socket.send(Message::binary(bytes)).await.map_err(|e| e.to_string())?; + socket + .send(Message::binary(bytes)) + .await + .map_err(|e| e.to_string())?; return Ok(()); } error!("Protokol Version mismatch: server={version}, client={client_version}"); } } - let _ = socket.send(Message::Close(Some(CloseFrame { - code: WsCloseCode::Protocol.code(), - reason: "Unsupported protocol".into(), - }))).await; + let _ = socket + .send(Message::Close(Some(CloseFrame { + code: WsCloseCode::Protocol.code(), + reason: "Unsupported protocol".into(), + }))) + .await; Err("Protocol version mismatch".into()) } @@ -91,8 +104,8 @@ async fn handle_protocol_message( match ProtocolMessage::from_bytes(bytes) { Ok(ProtocolMessage::StatusRequest(auth_token)) => { if !auth || verify_auth_admin_token(&auth_token, secret_key) { - let status = create_status_check(app_state).await; - Some(ProtocolMessage::StatusResponse(status)) + let status = create_status_check(app_state).await; + Some(ProtocolMessage::StatusResponse(status)) } else { Some(ProtocolMessage::Unauthorized) } @@ -103,7 +116,9 @@ async fn handle_protocol_message( } Err(e) => { error!("Invalid websocket message: {e}"); - Some(ProtocolMessage::Error(format!("Invalid websocket message: {e}"))) + Some(ProtocolMessage::Error(format!( + "Invalid websocket message: {e}" + ))) } } } else { @@ -126,33 +141,47 @@ async fn handle_incoming_message( handle_handshake(msg, socket, *version).await?; *handler = ProtocolHandler::Default; Ok(()) - }, + } ProtocolHandler::Default => { let msg = handle_protocol_message(msg, app_state, auth, secret_key).await; match msg { - None => {Ok(())}, + None => Ok(()), Some(protocol_msg) => { let bytes = match protocol_msg.to_bytes() { Ok(bytes) => bytes, - Err(err) => ProtocolMessage::Error(err.to_string()).to_bytes().map_err(|e| e.to_string())?, + Err(err) => ProtocolMessage::Error(err.to_string()) + .to_bytes() + .map_err(|e| e.to_string())?, }; - Ok(socket.send(Message::Binary(bytes)).await.map_err(|e| e.to_string())?) + Ok(socket + .send(Message::Binary(bytes)) + .await + .map_err(|e| e.to_string())?) } } - }, + } } } async fn handle_event_message(socket: &mut WebSocket, event: EventMessage) -> Result<(), String> { match event { EventMessage::ActiveUserChange(users, connections) => { - let msg = ProtocolMessage::ActiveUserResponse(users, connections).to_bytes().map_err(|e| e.to_string())?; - socket.send(Message::Binary(msg)).await.map_err(|e| format!("Active user connection change event: {e} ")) + let msg = ProtocolMessage::ActiveUserResponse(users, connections) + .to_bytes() + .map_err(|e| e.to_string())?; + socket + .send(Message::Binary(msg)) + .await + .map_err(|e| format!("Active user connection change event: {e} ")) } EventMessage::ActiveProviderChange(provider, connections) => { - let msg = ProtocolMessage::ActiveProviderResponse(provider, connections).to_bytes().map_err(|e| e.to_string())?; - socket.send(Message::Binary(msg)).await.map_err(|e| format!("Provider connection change event: {e} ")) - + let msg = ProtocolMessage::ActiveProviderResponse(provider, connections) + .to_bytes() + .map_err(|e| e.to_string())?; + socket + .send(Message::Binary(msg)) + .await + .map_err(|e| format!("Provider connection change event: {e} ")) } } } @@ -186,4 +215,4 @@ async fn handle_socket(mut socket: WebSocket, app_state: Arc, auth: bo } } } -} \ No newline at end of file +} diff --git a/backend/src/api/endpoints/xmltv_api.rs b/backend/src/api/endpoints/xmltv_api.rs index 6c86874dc..0b77bf8aa 100644 --- a/backend/src/api/endpoints/xmltv_api.rs +++ b/backend/src/api/endpoints/xmltv_api.rs @@ -9,16 +9,16 @@ use std::fs::File; use std::path::{Path, PathBuf}; use std::sync::Arc; +use crate::api::api_utils::try_unwrap_body; 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::api::model::AppState; +use crate::api::model::UserApiRequest; use crate::model::Config; use crate::model::{ConfigTarget, ProxyUserCredentials, 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}; -use crate::api::api_utils::try_unwrap_body; +use crate::utils; pub fn get_empty_epg_response() -> impl axum::response::IntoResponse + Send { try_unwrap_body!(axum::response::Response::builder() @@ -35,20 +35,26 @@ fn time_correct(date_time: &str, correction: &TimeDelta) -> String { } // Parse the datetime string - NaiveDateTime::parse_from_str(date_time_split[0], "%Y%m%d%H%M%S").map_or_else(|_| date_time.to_string(), |native_dt| { - let corrected_dt = native_dt + *correction; - // Format the corrected datetime back to string - let formatted_dt = corrected_dt.format("%Y%m%d%H%M%S").to_string(); - let result = format!("{} {}", formatted_dt, date_time_split[1]); - result - }) + NaiveDateTime::parse_from_str(date_time_split[0], "%Y%m%d%H%M%S").map_or_else( + |_| date_time.to_string(), + |native_dt| { + let corrected_dt = native_dt + *correction; + // Format the corrected datetime back to string + let formatted_dt = corrected_dt.format("%Y%m%d%H%M%S").to_string(); + let result = format!("{} {}", formatted_dt, date_time_split[1]); + result + }, + ) } fn get_epg_path_for_target_of_type(target_name: &str, epg_path: PathBuf) -> Option { if utils::path_exists(&epg_path) { return Some(epg_path); } - trace!("Cant find epg file for {target_name} target: {}", epg_path.to_str().unwrap_or("?")); + trace!( + "Cant find epg file for {target_name} target: {}", + epg_path.to_str().unwrap_or("?") + ); None } @@ -61,12 +67,18 @@ fn get_epg_path_for_target(config: &Config, target: &ConfigTarget) -> Option { if let Some(storage_path) = xtream_get_storage_path(config, &target.name) { - return get_epg_path_for_target_of_type(&target.name, xtream_get_epg_file_path(&storage_path)); + return get_epg_path_for_target_of_type( + &target.name, + xtream_get_epg_file_path(&storage_path), + ); } } TargetOutput::M3u(_) => { if let Some(target_path) = get_target_storage_path(config, &target.name) { - return get_epg_path_for_target_of_type(&target.name, m3u_get_epg_file_path(&target_path)); + return get_epg_path_for_target_of_type( + &target.name, + m3u_get_epg_file_path(&target_path), + ); } } TargetOutput::Strm(_) | TargetOutput::HdHomeRun(_) => {} @@ -90,23 +102,23 @@ fn parse_timeshift(time_shift: Option<&String>) -> Option { }) } -async fn serve_epg(epg_path: &Path, user: &ProxyUserCredentials) -> impl axum::response::IntoResponse + Send { +async fn serve_epg( + epg_path: &Path, + user: &ProxyUserCredentials, +) -> impl axum::response::IntoResponse + Send { match File::open(epg_path) { - Ok(epg_file) => { - match parse_timeshift(user.epg_timeshift.as_ref()) { - None => serve_file(epg_path, mime::TEXT_XML).await.into_response(), - Some(duration) => { - serve_epg_with_timeshift(epg_file, duration).into_response() - } - } - } - Err(_) => { - get_empty_epg_response().into_response() - } + Ok(epg_file) => match parse_timeshift(user.epg_timeshift.as_ref()) { + None => serve_file(epg_path, mime::TEXT_XML).await.into_response(), + Some(duration) => serve_epg_with_timeshift(epg_file, duration).into_response(), + }, + Err(_) => get_empty_epg_response().into_response(), } } -fn serve_epg_with_timeshift(epg_file: File, offset_minutes: i32) -> impl axum::response::IntoResponse + Send { +fn serve_epg_with_timeshift( + epg_file: File, + offset_minutes: i32, +) -> impl axum::response::IntoResponse + Send { let reader = utils::file_reader(epg_file); let encoder = GzEncoder::new(Vec::with_capacity(4096), Compression::default()); let mut xml_reader = Reader::from_reader(reader); @@ -122,16 +134,24 @@ fn serve_epg_with_timeshift(epg_file: File, offset_minutes: i32) -> impl axum::r for attr in e.attributes() { match attr { Ok(attr) if attr.key.as_ref() == b"start" => { - let start_value = attr.decode_and_unescape_value(xml_reader.decoder()) + let start_value = attr + .decode_and_unescape_value(xml_reader.decoder()) .expect("Failed to decode start attribute"); // Modify the start attribute value as needed - elem.push_attribute(("start", time_correct(&start_value, &duration).as_str())); + elem.push_attribute(( + "start", + time_correct(&start_value, &duration).as_str(), + )); } Ok(attr) if attr.key.as_ref() == b"stop" => { - let stop_value = attr.decode_and_unescape_value(xml_reader.decoder()) + let stop_value = attr + .decode_and_unescape_value(xml_reader.decoder()) .expect("Failed to decode stop attribute"); // Modify the stop attribute value as needed - elem.push_attribute(("stop", time_correct(&stop_value, &duration).as_str())); + elem.push_attribute(( + "stop", + time_correct(&stop_value, &duration).as_str(), + )); } Ok(attr) => { // Copy any other attributes as they are @@ -144,12 +164,16 @@ fn serve_epg_with_timeshift(epg_file: File, offset_minutes: i32) -> impl axum::r } // Write the modified start event - xml_writer.write_event(Event::Start(elem)).expect("Failed to write event"); + xml_writer + .write_event(Event::Start(elem)) + .expect("Failed to write event"); } Ok(Event::Eof) => break, // End of file Ok(event) => { // Write any other event as is - xml_writer.write_event(event).expect("Failed to write event"); + xml_writer + .write_event(event) + .expect("Failed to write event"); } Err(e) => { error!("Error: {e}"); @@ -160,12 +184,18 @@ fn serve_epg_with_timeshift(epg_file: File, offset_minutes: i32) -> impl axum::r buf.clear(); } match xml_writer.into_inner().finish() { - Ok(compressed_data) => - try_unwrap_body!(axum::response::Response::builder() - .header(axum::http::header::CONTENT_TYPE, mime::APPLICATION_OCTET_STREAM.to_string()) + Ok(compressed_data) => try_unwrap_body!(axum::response::Response::builder() + .header( + axum::http::header::CONTENT_TYPE, + mime::APPLICATION_OCTET_STREAM.to_string() + ) .header(axum::http::header::CONTENT_ENCODING, "gzip") // Set Content-Encoding header .body(axum::body::Body::from(compressed_data))), - Err(err) => (axum::http::StatusCode::INTERNAL_SERVER_ERROR, err.to_string()).into_response() + Err(err) => ( + axum::http::StatusCode::INTERNAL_SERVER_ERROR, + err.to_string(), + ) + .into_response(), } } @@ -241,4 +271,4 @@ mod tests { assert_eq!(parse_timeshift(Some(&String::new())), None); assert_eq!(parse_timeshift(None), None); } -} \ No newline at end of file +} diff --git a/backend/src/api/endpoints/xtream_api.rs b/backend/src/api/endpoints/xtream_api.rs index 8d24c1d3e..90cfd73e7 100644 --- a/backend/src/api/endpoints/xtream_api.rs +++ b/backend/src/api/endpoints/xtream_api.rs @@ -1,29 +1,30 @@ // https://github.com/tellytv/go.xtream-codes/blob/master/structs.go use crate::api::api_utils; -use crate::api::api_utils::{force_provider_stream_response, get_user_target, get_user_target_by_credentials, is_seek_request, redirect_response, resource_response, separate_number_and_remainder, serve_file, stream_response, RedirectParams}; +use crate::api::api_utils::try_unwrap_body; +use crate::api::api_utils::{ + force_provider_stream_response, get_user_target, get_user_target_by_credentials, + is_seek_request, redirect_response, resource_response, separate_number_and_remainder, + serve_file, stream_response, RedirectParams, +}; use crate::api::api_utils::{redirect, try_option_bad_request, try_result_bad_request}; use crate::api::endpoints::hls_api::handle_hls_stream_request; use crate::api::endpoints::xmltv_api::get_empty_epg_response; -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::api::model::AppState; +use crate::api::model::UserApiRequest; +use crate::api::model::XtreamAuthorizationResponse; +use crate::api::model::{create_custom_video_stream_response, CustomVideoStreamType}; +use crate::auth::Fingerprint; +use crate::model::ProxyUserCredentials; use crate::model::{AppConfig, ConfigTarget}; use crate::model::{Config, ConfigInput}; -use crate::model::{ProxyUserCredentials}; use crate::repository::playlist_repository::get_target_id_mapping; -use crate::repository::storage::{get_target_storage_path}; +use crate::repository::storage::get_target_storage_path; use crate::repository::{storage_const, user_repository, xtream_repository}; -use shared::error::create_tuliprox_error_result; -use shared::error::info_err; -use shared::error::{str_to_io_error, TuliproxError, TuliproxErrorKind}; use crate::utils::trace_if_enabled; use crate::utils::xtream::create_vod_info_from_item; -use shared::utils::{extract_extension_from_url, generate_playlist_uuid, get_u32_from_serde_value, hex_encode, sanitize_sensitive_info, trim_slash, HLS_EXT}; use crate::utils::{request, xtream}; -use crate::auth::Fingerprint; -use axum::http::{HeaderMap}; +use axum::http::HeaderMap; use axum::response::IntoResponse; use bytes::Bytes; use futures::stream::{self, StreamExt}; @@ -31,14 +32,23 @@ use futures::Stream; use log::{debug, error, warn}; use serde::{Deserialize, Serialize}; use serde_json::{Map, Value}; +use shared::error::create_tuliprox_error_result; +use shared::error::info_err; +use shared::error::{str_to_io_error, TuliproxError, TuliproxErrorKind}; +use shared::model::{ + get_backdrop_path_value, FieldGetAccessor, PlaylistEntry, PlaylistItemType, ProxyType, + TargetType, UserConnectionPermission, XtreamCluster, XtreamPlaylistItem, +}; +use shared::utils::{ + extract_extension_from_url, generate_playlist_uuid, get_u32_from_serde_value, hex_encode, + sanitize_sensitive_info, trim_slash, HLS_EXT, +}; use std::borrow::Cow; use std::collections::{HashMap, HashSet}; use std::fmt::{Display, Formatter}; use std::path::Path; use std::str::FromStr; use std::sync::Arc; -use shared::model::{PlaylistItemType, XtreamCluster, FieldGetAccessor, PlaylistEntry, TargetType, UserConnectionPermission, ProxyType, get_backdrop_path_value, XtreamPlaylistItem}; -use crate::api::api_utils::try_unwrap_body; #[derive(Serialize, Deserialize, Debug, Copy, Clone, Eq, PartialEq)] pub enum ApiStreamContext { @@ -58,12 +68,16 @@ impl ApiStreamContext { impl Display for ApiStreamContext { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", match self { - Self::Live | Self::LiveAlt => Self::LIVE, - Self::Movie => Self::MOVIE, - Self::Series => Self::SERIES, - Self::Timeshift => Self::TIMESHIFT, - }) + write!( + f, + "{}", + match self { + Self::Live | Self::LiveAlt => Self::LIVE, + Self::Movie => Self::MOVIE, + Self::Series => Self::SERIES, + Self::Timeshift => Self::TIMESHIFT, + } + ) } } @@ -87,7 +101,11 @@ impl FromStr for ApiStreamContext { Self::MOVIE => Ok(Self::Movie), Self::SERIES => Ok(Self::Series), Self::TIMESHIFT => Ok(Self::Timeshift), - _ => create_tuliprox_error_result!(TuliproxErrorKind::Info, "Unknown CounterModifier: {}", s) + _ => create_tuliprox_error_result!( + TuliproxErrorKind::Info, + "Unknown CounterModifier: {}", + s + ), } } } @@ -103,11 +121,13 @@ pub struct ApiStreamRequest<'a> { } impl<'a> ApiStreamRequest<'a> { - pub const fn from(context: ApiStreamContext, - username: &'a str, - password: &'a str, - stream_id: &'a str, - action_path: &'a str) -> Self { + pub const fn from( + context: ApiStreamContext, + username: &'a str, + password: &'a str, + stream_id: &'a str, + action_path: &'a str, + ) -> Self { Self { context, access_token: false, @@ -117,10 +137,12 @@ impl<'a> ApiStreamRequest<'a> { action_path, } } - pub const fn from_access_token(context: ApiStreamContext, - password: &'a str, - stream_id: &'a str, - action_path: &'a str) -> Self { + pub const fn from_access_token( + context: ApiStreamContext, + password: &'a str, + stream_id: &'a str, + action_path: &'a str, + ) -> Self { Self { context, access_token: false, @@ -132,24 +154,32 @@ impl<'a> ApiStreamRequest<'a> { } } -pub fn serve_query(file_path: &Path, filter: &HashMap<&str, HashSet>) -> impl IntoResponse + Send { +pub fn serve_query( + file_path: &Path, + filter: &HashMap<&str, HashSet>, +) -> impl IntoResponse + Send { let filtered = crate::utils::json_filter_file(file_path, filter); axum::Json(filtered) } pub(in crate::api) fn get_xtream_player_api_stream_url( - input: &ConfigInput, context: ApiStreamContext, action_path: &str, fallback_url: &str, + input: &ConfigInput, + context: ApiStreamContext, + action_path: &str, + fallback_url: &str, ) -> Option { if let Some(input_user_info) = input.get_user_info() { let ctx = match context { - ApiStreamContext::LiveAlt | - ApiStreamContext::Live => { - let use_prefix = input.options.as_ref().is_none_or(|o| o.xtream_live_stream_use_prefix); + ApiStreamContext::LiveAlt | ApiStreamContext::Live => { + let use_prefix = input + .options + .as_ref() + .is_none_or(|o| o.xtream_live_stream_use_prefix); String::from(if use_prefix { "live" } else { "" }) } - ApiStreamContext::Movie - | ApiStreamContext::Series - | ApiStreamContext::Timeshift => context.to_string() + ApiStreamContext::Movie | ApiStreamContext::Series | ApiStreamContext::Timeshift => { + context.to_string() + } }; let mut parts = vec![ trim_slash(&input_user_info.base_url), @@ -160,7 +190,6 @@ pub(in crate::api) fn get_xtream_player_api_stream_url( ]; parts.retain(|s| !s.is_empty()); Some(parts.join("/")) - } else if !fallback_url.is_empty() { Some(String::from(fallback_url)) } else { @@ -171,9 +200,15 @@ pub(in crate::api) fn get_xtream_player_api_stream_url( fn get_user_info(user: &ProxyUserCredentials, app_state: &AppState) -> XtreamAuthorizationResponse { let server_info = app_state.app_config.get_user_server_info(user); let active_connections = app_state.get_active_connections_for_user(&user.username); - XtreamAuthorizationResponse::new(&server_info, user, active_connections, app_state.app_config.config.load().user_access_control) + XtreamAuthorizationResponse::new( + &server_info, + user, + active_connections, + app_state.app_config.config.load().user_access_control, + ) } +#[allow(clippy::too_many_lines)] async fn xtream_player_api_stream( fingerprint: &str, addr: &str, @@ -182,9 +217,25 @@ async fn xtream_player_api_stream( api_req: &UserApiRequest, stream_req: ApiStreamRequest<'_>, ) -> impl IntoResponse + Send { - let (user, target) = try_option_bad_request!(get_user_target_by_credentials(stream_req.username, stream_req.password, api_req, app_state), false, format!("Could not find any user for xc stream {}", stream_req.username)); + let (user, target) = try_option_bad_request!( + get_user_target_by_credentials( + stream_req.username, + stream_req.password, + api_req, + app_state + ), + false, + format!( + "Could not find any user for xc stream {}", + stream_req.username + ) + ); if user.permission_denied(app_state) { - return create_custom_video_stream_response(&app_state.app_config, CustomVideoStreamType::UserAccountExpired).into_response(); + return create_custom_video_stream_response( + &app_state.app_config, + CustomVideoStreamType::UserAccountExpired, + ) + .into_response(); } let target_name = &target.name; @@ -195,27 +246,73 @@ async fn xtream_player_api_stream( let (action_stream_id, stream_ext) = separate_number_and_remainder(stream_req.stream_id); let virtual_id: u32 = try_result_bad_request!(action_stream_id.trim().parse()); - let (pli, mapping) = try_result_bad_request!(xtream_repository::xtream_get_item_for_stream_id(virtual_id, &app_state.app_config, &target, None), true, format!("Failed to read xtream item for stream id {}", virtual_id)); - let input = try_option_bad_request!(app_state.app_config.get_input_by_name(pli.input_name.as_str()), true, format!("Cant find input for target {target_name}, context {}, stream_id {virtual_id}", stream_req.context)); + let (pli, mapping) = try_result_bad_request!( + xtream_repository::xtream_get_item_for_stream_id( + virtual_id, + &app_state.app_config, + &target, + None + ), + true, + format!("Failed to read xtream item for stream id {}", virtual_id) + ); + let input = try_option_bad_request!( + app_state + .app_config + .get_input_by_name(pli.input_name.as_str()), + true, + format!( + "Cant find input for target {target_name}, context {}, stream_id {virtual_id}", + stream_req.context + ) + ); let cluster = pli.xtream_cluster; - let item_type = if stream_req.context == ApiStreamContext::Timeshift { PlaylistItemType::Catchup } else { pli.item_type }; + let item_type = if stream_req.context == ApiStreamContext::Timeshift { + PlaylistItemType::Catchup + } else { + pli.item_type + }; let session_key = format!("{fingerprint}{virtual_id}"); - let user_session = app_state.active_users.get_user_session(&user.username, &session_key); + let user_session = app_state + .active_users + .get_user_session(&user.username, &session_key); let session_url = if let Some(session) = &user_session { if session.permission == UserConnectionPermission::Exhausted { - return create_custom_video_stream_response(&app_state.app_config, CustomVideoStreamType::UserConnectionsExhausted).into_response(); + return create_custom_video_stream_response( + &app_state.app_config, + CustomVideoStreamType::UserConnectionsExhausted, + ) + .into_response(); } - if app_state.active_provider.is_over_limit(&session.provider).await { - return create_custom_video_stream_response(&app_state.app_config, CustomVideoStreamType::ProviderConnectionsExhausted).into_response(); + if app_state + .active_provider + .is_over_limit(&session.provider) + .await + { + return create_custom_video_stream_response( + &app_state.app_config, + CustomVideoStreamType::ProviderConnectionsExhausted, + ) + .into_response(); } if session.virtual_id == virtual_id && is_seek_request(cluster, req_headers).await { // partial request means we are in reverse proxy mode, seek happened - return force_provider_stream_response(addr, app_state, session, item_type, req_headers, &input, &user).await.into_response(); + return force_provider_stream_response( + addr, + app_state, + session, + item_type, + req_headers, + &input, + &user, + ) + .await + .into_response(); } session.stream_url.as_str() @@ -225,7 +322,11 @@ async fn xtream_player_api_stream( let connection_permission = user.connection_permission(app_state); if connection_permission == UserConnectionPermission::Exhausted { - return create_custom_video_stream_response(&app_state.app_config, CustomVideoStreamType::UserConnectionsExhausted).into_response(); + return create_custom_video_stream_response( + &app_state.app_config, + CustomVideoStreamType::UserConnectionsExhausted, + ) + .into_response(); } let context = stream_req.context; @@ -246,8 +347,10 @@ async fn xtream_player_api_stream( return response.into_response(); } - let extension = stream_ext.unwrap_or_else( - || extract_extension_from_url(&pli.url).map_or_else(String::new, std::string::ToString::to_string)); + let extension = stream_ext.unwrap_or_else(|| { + extract_extension_from_url(&pli.url) + .map_or_else(String::new, std::string::ToString::to_string) + }); let query_path = if stream_req.action_path.is_empty() { format!("{}{extension}", pli.provider_id) @@ -255,18 +358,53 @@ async fn xtream_player_api_stream( format!("{}/{}{extension}", stream_req.action_path, pli.provider_id) }; - let stream_url = try_option_bad_request!(get_xtream_player_api_stream_url(&input, stream_req.context, &query_path, session_url), - true, format!("Cant find stream url for target {target_name}, context {}, stream_id {virtual_id}", stream_req.context)); + let stream_url = try_option_bad_request!( + get_xtream_player_api_stream_url(&input, stream_req.context, &query_path, session_url), + true, + format!( + "Cant find stream url for target {target_name}, context {}, stream_id {virtual_id}", + stream_req.context + ) + ); - let is_hls_request = item_type == PlaylistItemType::LiveHls || item_type == PlaylistItemType::LiveDash || extension == HLS_EXT; + let is_hls_request = item_type == PlaylistItemType::LiveHls + || item_type == PlaylistItemType::LiveDash + || extension == HLS_EXT; // Reverse proxy mode if is_hls_request { - return handle_hls_stream_request(fingerprint, addr, app_state, &user, user_session.as_ref(), &stream_url, pli.virtual_id, &input, connection_permission).await.into_response(); + return handle_hls_stream_request( + fingerprint, + addr, + app_state, + &user, + user_session.as_ref(), + &stream_url, + pli.virtual_id, + &input, + connection_permission, + ) + .await + .into_response(); } - stream_response(addr, app_state, session_key.as_str(), pli.virtual_id, item_type, &stream_url, req_headers, &input, &target, &user, connection_permission).await.into_response() + stream_response( + addr, + app_state, + session_key.as_str(), + pli.virtual_id, + item_type, + &stream_url, + req_headers, + &input, + &target, + &user, + connection_permission, + ) + .await + .into_response() } +#[allow(clippy::too_many_lines)] // Used by webui async fn xtream_player_api_stream_with_token( fingerprint: &str, @@ -284,15 +422,38 @@ async fn xtream_player_api_stream_with_token( } let (action_stream_id, stream_ext) = separate_number_and_remainder(stream_req.stream_id); let virtual_id: u32 = try_result_bad_request!(action_stream_id.trim().parse()); - let (pli, _mapping) = try_result_bad_request!(xtream_repository::xtream_get_item_for_stream_id(virtual_id, &app_state.app_config, &target, None), true, format!("Failed to read xtream item for stream id {}", virtual_id)); - let input = try_option_bad_request!(app_state.app_config.get_input_by_name(pli.input_name.as_str()), true, format!("Cant find input for target {target_name}, context {}, stream_id {virtual_id}", stream_req.context)); + let (pli, _mapping) = try_result_bad_request!( + xtream_repository::xtream_get_item_for_stream_id( + virtual_id, + &app_state.app_config, + &target, + None + ), + true, + format!("Failed to read xtream item for stream id {}", virtual_id) + ); + let input = try_option_bad_request!( + app_state + .app_config + .get_input_by_name(pli.input_name.as_str()), + true, + format!( + "Cant find input for target {target_name}, context {}, stream_id {virtual_id}", + stream_req.context + ) + ); let session_key = format!("{fingerprint}{virtual_id}"); - let is_hls_request = pli.item_type == PlaylistItemType::LiveHls || stream_ext.as_deref() == Some(HLS_EXT); + let is_hls_request = + pli.item_type == PlaylistItemType::LiveHls || stream_ext.as_deref() == Some(HLS_EXT); let config = app_state.app_config.config.load(); - let server = config.web_ui.as_ref().and_then(|web_ui| web_ui.player_server.as_ref()).map_or("default", |server_name| server_name.as_str()); + let server = config + .web_ui + .as_ref() + .and_then(|web_ui| web_ui.player_server.as_ref()) + .map_or("default", |server_name| server_name.as_str()); let user = ProxyUserCredentials { username: "api_user".to_string(), @@ -313,11 +474,25 @@ async fn xtream_player_api_stream_with_token( // Reverse proxy mode if is_hls_request { - return handle_hls_stream_request(fingerprint, addr, app_state, &user, None, &pli.url, pli.virtual_id, &input, UserConnectionPermission::Allowed).await.into_response(); + return handle_hls_stream_request( + fingerprint, + addr, + app_state, + &user, + None, + &pli.url, + pli.virtual_id, + &input, + UserConnectionPermission::Allowed, + ) + .await + .into_response(); } - let extension = stream_ext.unwrap_or_else( - || extract_extension_from_url(&pli.url).map_or_else(String::new, std::string::ToString::to_string)); + let extension = stream_ext.unwrap_or_else(|| { + extract_extension_from_url(&pli.url) + .map_or_else(String::new, std::string::ToString::to_string) + }); let query_path = if stream_req.action_path.is_empty() { format!("{}{extension}", pli.provider_id) @@ -325,19 +500,44 @@ async fn xtream_player_api_stream_with_token( format!("{}/{}{extension}", stream_req.action_path, pli.provider_id) }; - let stream_url = try_option_bad_request!(get_xtream_player_api_stream_url(&input, - stream_req.context, &query_path, pli.url.as_str()), - true, format!("Cant find stream url for target {target_name}, context {}, stream_id {virtual_id}", - stream_req.context)); + let stream_url = try_option_bad_request!( + get_xtream_player_api_stream_url( + &input, + stream_req.context, + &query_path, + pli.url.as_str() + ), + true, + format!( + "Cant find stream url for target {target_name}, context {}, stream_id {virtual_id}", + stream_req.context + ) + ); - trace_if_enabled!("Streaming stream request from {}", sanitize_sensitive_info(&stream_url)); - stream_response(addr, app_state, session_key.as_str(), pli.virtual_id, pli.item_type, &stream_url, req_headers, &input, &target, &user, UserConnectionPermission::Allowed).await.into_response() + trace_if_enabled!( + "Streaming stream request from {}", + sanitize_sensitive_info(&stream_url) + ); + stream_response( + addr, + app_state, + session_key.as_str(), + pli.virtual_id, + pli.item_type, + &stream_url, + req_headers, + &input, + &target, + &user, + UserConnectionPermission::Allowed, + ) + .await + .into_response() } else { axum::http::StatusCode::BAD_REQUEST.into_response() } } - fn get_doc_id_and_field_name(input: &str) -> Option<(u32, &str)> { if let Some(pos) = input.find('_') { let (number_part, rest) = input.split_at(pos); @@ -349,10 +549,16 @@ fn get_doc_id_and_field_name(input: &str) -> Option<(u32, &str)> { None } -fn get_doc_resource_field_value<'a>(field: &'a str, doc: Option<&'a Value>) -> Option> { +fn get_doc_resource_field_value<'a>( + field: &'a str, + doc: Option<&'a Value>, +) -> Option> { if let Some(Value::Object(info_data)) = doc { 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)); + 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(Cow::Borrowed(url)); } @@ -360,26 +566,42 @@ fn get_doc_resource_field_value<'a>(field: &'a str, doc: Option<&'a Value>) -> O None } -fn xtream_get_info_resource_url<'a>(config: &'a AppConfig, pli: &'a XtreamPlaylistItem, target: &'a ConfigTarget, resource: &'a str) -> Result>, serde_json::Error> { +fn xtream_get_info_resource_url<'a>( + config: &'a AppConfig, + pli: &'a XtreamPlaylistItem, + target: &'a ConfigTarget, + resource: &'a str, +) -> Result>, serde_json::Error> { let info_content = match pli.xtream_cluster { - XtreamCluster::Video => { - xtream_repository::xtream_load_vod_info(config, target.name.as_str(), pli.get_virtual_id()) - } - XtreamCluster::Series => { - xtream_repository::xtream_load_series_info(config, target.name.as_str(), pli.get_virtual_id()) - } + XtreamCluster::Video => xtream_repository::xtream_load_vod_info( + config, + target.name.as_str(), + pli.get_virtual_id(), + ), + XtreamCluster::Series => xtream_repository::xtream_load_series_info( + config, + target.name.as_str(), + pli.get_virtual_id(), + ), XtreamCluster::Live => None, }; 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(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) { + 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[crate::model::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 => { @@ -434,18 +656,26 @@ fn get_season_info_doc(doc: &Vec, season_id: u32) -> Option<&Value> { None } - -fn xtream_get_season_resource_url<'a>(config: &'a AppConfig, pli: &'a XtreamPlaylistItem, target: &'a ConfigTarget, resource: &'a str) -> Result>, serde_json::Error> { +fn xtream_get_season_resource_url<'a>( + config: &'a AppConfig, + pli: &'a XtreamPlaylistItem, + target: &'a ConfigTarget, + resource: &'a str, +) -> Result>, serde_json::Error> { let info_content = match pli.xtream_cluster { - XtreamCluster::Series => { - xtream_repository::xtream_load_series_info(config, target.name.as_str(), pli.get_virtual_id()) - } + XtreamCluster::Series => xtream_repository::xtream_load_series_info( + config, + target.name.as_str(), + pli.get_virtual_id(), + ), XtreamCluster::Video | XtreamCluster::Live => None, }; 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(crate::model::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(crate::model::XC_TAG_SEASONS_DATA), @@ -453,7 +683,9 @@ fn xtream_get_season_resource_url<'a>(config: &'a AppConfig, pli: &'a XtreamPlay }; if let Some(Value::Array(seasons)) = seasons_doc { - if let Some(value) = get_doc_resource_field_value(field, get_season_info_doc(seasons, season_id)) { + if let Some(value) = + get_doc_resource_field_value(field, get_season_info_doc(seasons, season_id)) + { return Ok(Some(Cow::Owned(value.into_owned()))); } } @@ -469,7 +701,19 @@ async fn xtream_player_api_resource( app_state: &Arc, resource_req: ApiStreamRequest<'_>, ) -> impl IntoResponse { - let (user, target) = try_option_bad_request!(get_user_target_by_credentials(resource_req.username, resource_req.password, api_req, app_state), false, format!("Could not find any user xc resource {}", resource_req.username)); + let (user, target) = try_option_bad_request!( + get_user_target_by_credentials( + resource_req.username, + resource_req.password, + api_req, + app_state + ), + false, + format!( + "Could not find any user xc resource {}", + resource_req.username + ) + ); if user.permission_denied(app_state) { return axum::http::StatusCode::FORBIDDEN.into_response(); } @@ -480,11 +724,30 @@ 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.app_config, &target, None), true, format!("Failed to read xtream item for stream id {}", virtual_id)); + let (pli, _) = try_result_bad_request!( + xtream_repository::xtream_get_item_for_stream_id( + virtual_id, + &app_state.app_config, + &target, + None + ), + true, + format!("Failed to read xtream item for stream id {}", virtual_id) + ); let stream_url = if resource.starts_with(crate::model::XC_INFO_RESOURCE_PREFIX) { - try_result_bad_request!(xtream_get_info_resource_url(&app_state.app_config, &pli, &target, resource)) + try_result_bad_request!(xtream_get_info_resource_url( + &app_state.app_config, + &pli, + &target, + resource + )) } else if resource.starts_with(crate::model::XC_SEASON_RESOURCE_PREFIX) { - try_result_bad_request!(xtream_get_season_resource_url(&app_state.app_config, &pli, &target, resource)) + try_result_bad_request!(xtream_get_season_resource_url( + &app_state.app_config, + &pli, + &target, + resource + )) } else { pli.get_field(resource) }; @@ -493,11 +756,16 @@ async fn xtream_player_api_resource( None => axum::http::StatusCode::NOT_FOUND.into_response(), Some(url) => { if user.proxy.is_redirect(pli.item_type) || target.is_force_redirect(pli.item_type) { - trace_if_enabled!("Redirecting resource request to {}", sanitize_sensitive_info(&url)); + trace_if_enabled!( + "Redirecting resource request to {}", + sanitize_sensitive_info(&url) + ); redirect(&url).into_response() } else { trace_if_enabled!("Resource request to {}", sanitize_sensitive_info(&url)); - resource_response(app_state, &url, req_headers, None).await.into_response() + resource_response(app_state, &url, req_headers, None) + .await + .into_response() } } } @@ -508,10 +776,14 @@ macro_rules! create_xtream_player_api_stream { async fn $fn_name( Fingerprint(fingerprint, addr): Fingerprint, req_headers: HeaderMap, - axum::extract::Path((username, password, stream_id)): axum::extract::Path<(String, String, String)>, + axum::extract::Path((username, password, stream_id)): axum::extract::Path<( + String, + String, + String, + )>, axum::extract::State(app_state): axum::extract::State>, axum::extract::Query(api_req): axum::extract::Query, - ) -> impl IntoResponse + Send { + ) -> impl IntoResponse + Send { xtream_player_api_stream( &fingerprint, &addr, @@ -519,22 +791,36 @@ macro_rules! create_xtream_player_api_stream { &app_state, &api_req, ApiStreamRequest::from($context, &username, &password, &stream_id, ""), - ).await.into_response() + ) + .await + .into_response() } - } + }; } macro_rules! create_xtream_player_api_resource { ($fn_name:ident, $context:expr) => { async fn $fn_name( - axum::extract::Path((username, password, stream_id, resource)): axum::extract::Path<(String, String, String, String)>, + axum::extract::Path((username, password, stream_id, resource)): axum::extract::Path<( + String, + String, + String, + String, + )>, axum::extract::State(app_state): axum::extract::State>, axum::extract::Query(api_req): axum::extract::Query, req_headers: HeaderMap, - ) -> impl IntoResponse { - xtream_player_api_resource(&req_headers, &api_req, &app_state, ApiStreamRequest::from($context, &username, &password, &stream_id, &resource)).await.into_response() + ) -> impl IntoResponse { + xtream_player_api_resource( + &req_headers, + &api_req, + &app_state, + ApiStreamRequest::from($context, &username, &password, &stream_id, &resource), + ) + .await + .into_response() } - } + }; } create_xtream_player_api_stream!(xtream_player_api_live_stream, ApiStreamContext::Live); @@ -573,18 +859,56 @@ async fn xtream_player_api_timeshift_stream( axum::extract::State(app_state): axum::extract::State>, axum::extract::Form(api_form_req): axum::extract::Form, ) -> impl IntoResponse + Send { - let username = get_non_empty(×hift_request.username, &api_form_req.username, &api_req.username).to_string(); - let password = get_non_empty(×hift_request.password, &api_form_req.password, &api_req.password).to_string(); - let stream_id = get_non_empty(×hift_request.stream_id, &api_req.stream_id, &api_form_req.stream_id).to_string(); - let duration = get_non_empty(×hift_request.duration, ×hift_request.duration, &api_form_req.duration); - let start = get_non_empty(×hift_request.start, ×hift_request.start, &api_form_req.start); + let username = get_non_empty( + ×hift_request.username, + &api_form_req.username, + &api_req.username, + ) + .to_string(); + let password = get_non_empty( + ×hift_request.password, + &api_form_req.password, + &api_req.password, + ) + .to_string(); + let stream_id = get_non_empty( + ×hift_request.stream_id, + &api_req.stream_id, + &api_form_req.stream_id, + ) + .to_string(); + let duration = get_non_empty( + ×hift_request.duration, + ×hift_request.duration, + &api_form_req.duration, + ); + let start = get_non_empty( + ×hift_request.start, + ×hift_request.start, + &api_form_req.start, + ); let action_path = format!("{duration}/{start}"); api_req.username = username.to_string(); api_req.password = password.to_string(); api_req.stream_id = stream_id.to_string(); - xtream_player_api_stream(&fingerprint, &addr, &req_headers, &app_state, &api_req, ApiStreamRequest::from(ApiStreamContext::Timeshift, &username, &password, &stream_id, &action_path), /*&addr*/).await.into_response() + xtream_player_api_stream( + &fingerprint, + &addr, + &req_headers, + &app_state, + &api_req, + ApiStreamRequest::from( + ApiStreamContext::Timeshift, + &username, + &password, + &stream_id, + &action_path, + ), /*&addr*/ + ) + .await + .into_response() } async fn xtream_player_api_timeshift_query_stream( @@ -600,37 +924,81 @@ async fn xtream_player_api_timeshift_query_stream( let duration = get_non_empty(&api_query_req.duration, &api_form_req.duration, ""); let start = get_non_empty(&api_query_req.start, &api_form_req.start, ""); let action_path = format!("{duration}/{start}"); - if username.is_empty() || password.is_empty() || stream_id.is_empty() || duration.is_empty() || start.is_empty() { + if username.is_empty() + || password.is_empty() + || stream_id.is_empty() + || duration.is_empty() + || start.is_empty() + { // if token.is_empty() { return axum::http::StatusCode::BAD_REQUEST.into_response(); // } // xtream_player_api_stream(&req_headers, &api_query_req, &app_state, ApiStreamRequest::from_access_token(ApiStreamContext::Timeshift, token, stream_id, &action_path)/*, &addr*/).await.into_response() } - xtream_player_api_stream(&fingerprint, &addr, &req_headers, &app_state, &api_query_req, ApiStreamRequest::from(ApiStreamContext::Timeshift, username, password, stream_id, &action_path)).await.into_response() + xtream_player_api_stream( + &fingerprint, + &addr, + &req_headers, + &app_state, + &api_query_req, + ApiStreamRequest::from( + ApiStreamContext::Timeshift, + username, + password, + stream_id, + &action_path, + ), + ) + .await + .into_response() } - -async fn xtream_get_stream_info_response(app_state: &AppState, user: &ProxyUserCredentials, - target: &ConfigTarget, stream_id: &str, - cluster: XtreamCluster) -> impl IntoResponse + Send { +async fn xtream_get_stream_info_response( + app_state: &AppState, + user: &ProxyUserCredentials, + target: &ConfigTarget, + stream_id: &str, + cluster: XtreamCluster, +) -> impl IntoResponse + Send { let virtual_id: u32 = match FromStr::from_str(stream_id) { Ok(id) => id, - Err(_) => return axum::http::StatusCode::BAD_REQUEST.into_response() + Err(_) => return axum::http::StatusCode::BAD_REQUEST.into_response(), }; - if let Ok((pli, virtual_record)) = xtream_repository::xtream_get_item_for_stream_id(virtual_id, &app_state.app_config, target, Some(cluster)) { + if let Ok((pli, virtual_record)) = xtream_repository::xtream_get_item_for_stream_id( + virtual_id, + &app_state.app_config, + target, + Some(cluster), + ) { if pli.provider_id > 0 { let input_name = &pli.input_name; if let Some(input) = app_state.app_config.get_input_by_name(input_name.as_str()) { - if let Some(info_url) = xtream::get_xtream_player_api_info_url(&input, cluster, pli.provider_id) { + if let Some(info_url) = + xtream::get_xtream_player_api_info_url(&input, cluster, pli.provider_id) + { // Redirect is only possible for live streams, vod and series info needs to be modified if user.proxy == ProxyType::Redirect && cluster == XtreamCluster::Live { return redirect(&info_url).into_response(); - } else if let Ok(content) = xtream::get_xtream_stream_info(Arc::clone(&app_state.http_client.load()), &app_state.app_config, user, &input, target, &pli, info_url.as_str(), cluster).await { + } else if let Ok(content) = xtream::get_xtream_stream_info( + Arc::clone(&app_state.http_client.load()), + &app_state.app_config, + user, + &input, + target, + &pli, + info_url.as_str(), + cluster, + ) + .await + { return try_unwrap_body!(axum::response::Response::builder() .status(axum::http::StatusCode::OK) - .header(axum::http::header::CONTENT_TYPE, mime::APPLICATION_JSON.to_string()) - .body(axum::body::Body::from(content))) + .header( + axum::http::header::CONTENT_TYPE, + mime::APPLICATION_JSON.to_string() + ) + .body(axum::body::Body::from(content))); } } } @@ -638,50 +1006,100 @@ async fn xtream_get_stream_info_response(app_state: &AppState, user: &ProxyUserC return match cluster { XtreamCluster::Video => { - let content = create_vod_info_from_item(target, user, &pli, virtual_record.last_updated); + let content = + create_vod_info_from_item(target, user, &pli, virtual_record.last_updated); try_unwrap_body!(axum::response::Response::builder() .status(axum::http::StatusCode::OK) - .header(axum::http::header::CONTENT_TYPE, mime::APPLICATION_JSON.to_string()) + .header( + axum::http::header::CONTENT_TYPE, + mime::APPLICATION_JSON.to_string() + ) .body(axum::body::Body::from(content))) } - XtreamCluster::Live | XtreamCluster::Series => try_unwrap_body!(axum::response::Response::builder() - .status(axum::http::StatusCode::OK) - .header(axum::http::header::CONTENT_TYPE, mime::APPLICATION_JSON.to_string()) - .body(axum::body::Body::from("{}".as_bytes()))), + XtreamCluster::Live | XtreamCluster::Series => { + try_unwrap_body!(axum::response::Response::builder() + .status(axum::http::StatusCode::OK) + .header( + axum::http::header::CONTENT_TYPE, + mime::APPLICATION_JSON.to_string() + ) + .body(axum::body::Body::from("{}".as_bytes()))) + } }; } try_unwrap_body!(axum::response::Response::builder() .status(axum::http::StatusCode::OK) - .header(axum::http::header::CONTENT_TYPE, mime::APPLICATION_JSON.to_string()) + .header( + axum::http::header::CONTENT_TYPE, + mime::APPLICATION_JSON.to_string() + ) .body(axum::body::Body::from("{}".as_bytes()))) } -async fn xtream_get_short_epg(app_state: &AppState, user: &ProxyUserCredentials, target: &ConfigTarget, stream_id: &str, limit: &str) -> impl IntoResponse + Send { +async fn xtream_get_short_epg( + app_state: &AppState, + user: &ProxyUserCredentials, + target: &ConfigTarget, + stream_id: &str, + limit: &str, +) -> impl IntoResponse + Send { let target_name = &target.name; if target.has_output(&TargetType::Xtream) { let virtual_id: u32 = match FromStr::from_str(stream_id.trim()) { Ok(id) => id, - Err(_) => return axum::http::StatusCode::BAD_REQUEST.into_response() + Err(_) => return axum::http::StatusCode::BAD_REQUEST.into_response(), }; - if let Ok((pli, _)) = xtream_repository::xtream_get_item_for_stream_id(virtual_id, &app_state.app_config, target, None) { + if let Ok((pli, _)) = xtream_repository::xtream_get_item_for_stream_id( + virtual_id, + &app_state.app_config, + target, + None, + ) { if pli.provider_id > 0 { let input_name = &pli.input_name; if let Some(input) = app_state.app_config.get_input_by_name(input_name.as_str()) { - 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 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}"); } - if user.proxy.is_redirect(pli.item_type) || target.is_force_redirect(pli.item_type) { + if user.proxy.is_redirect(pli.item_type) + || target.is_force_redirect(pli.item_type) + { return redirect(&info_url).into_response(); } // TODO serve epg from own db - return match request::download_text_content(Arc::clone(&app_state.http_client.load()), &input, info_url.as_str(), None).await { - Ok((content, _)) => (axum::http::StatusCode::OK, [(axum::http::header::CONTENT_TYPE.to_string(), mime::APPLICATION_JSON.to_string())], content).into_response(), + return match request::download_text_content( + Arc::clone(&app_state.http_client.load()), + &input, + info_url.as_str(), + None, + ) + .await + { + Ok((content, _)) => ( + axum::http::StatusCode::OK, + [( + axum::http::header::CONTENT_TYPE.to_string(), + mime::APPLICATION_JSON.to_string(), + )], + content, + ) + .into_response(), Err(err) => { - error!("Failed to download epg {}", sanitize_sensitive_info(err.to_string().as_str())); + error!( + "Failed to download epg {}", + sanitize_sensitive_info(err.to_string().as_str()) + ); get_empty_epg_response().into_response() } }; @@ -694,25 +1112,86 @@ async fn xtream_get_short_epg(app_state: &AppState, user: &ProxyUserCredentials, get_empty_epg_response().into_response() } -async fn xtream_player_api_handle_content_action(config: &Config, target_name: &str, action: &str, category_id: Option, user: &ProxyUserCredentials) -> Option { +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 { - 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("")) + 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 { - 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 + 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([(crate::model::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()); + return Some( + serve_file(&file_path, mime::APPLICATION_JSON) + .await + .into_response(), + ); } else if let Some(payload) = content { return Some(try_unwrap_body!(axum::response::Response::builder() .status(axum::http::StatusCode::OK) @@ -723,24 +1202,72 @@ async fn xtream_player_api_handle_content_action(config: &Config, target_name: & None } -async fn xtream_get_catchup_response(app_state: &AppState, target: &ConfigTarget, stream_id: &str, start: &str, end: &str) -> impl IntoResponse + Send { +async fn xtream_get_catchup_response( + app_state: &AppState, + target: &ConfigTarget, + stream_id: &str, + start: &str, + end: &str, +) -> impl IntoResponse + Send { 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.app_config, target, Some(XtreamCluster::Live))); - let input = try_option_bad_request!(app_state.app_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, 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.load()), info_url.as_str(), &input).await); + let (pli, _) = try_result_bad_request!(xtream_repository::xtream_get_item_for_stream_id( + virtual_id, + &app_state.app_config, + target, + Some(XtreamCluster::Live) + )); + let input = try_option_bad_request!(app_state + .app_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, + 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.load()), + 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(crate::model::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 config = &app_state.app_config.config.load(); - let target_path = try_option_bad_request!(get_target_storage_path(config, target.name.as_str())); - let (mut target_id_mapping, file_lock) = get_target_id_mapping(&app_state.app_config, &target_path).await; + let target_path = + try_option_bad_request!(get_target_storage_path(config, target.name.as_str())); + let (mut target_id_mapping, file_lock) = + get_target_id_mapping(&app_state.app_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(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(crate::model::XC_TAG_ID.to_string(), Value::String(virtual_id.to_string())); + 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( + crate::model::XC_TAG_ID.to_string(), + Value::String(virtual_id.to_string()), + ); } } if let Err(err) = target_id_mapping.persist() { @@ -748,13 +1275,18 @@ async fn xtream_get_catchup_response(app_state: &AppState, target: &ConfigTarget return axum::http::StatusCode::BAD_REQUEST.into_response(); } drop(file_lock); - serde_json::to_string(&doc) - .map_or_else( - |_| axum::http::StatusCode::BAD_REQUEST.into_response(), - |result| try_unwrap_body!(axum::response::Response::builder() + serde_json::to_string(&doc).map_or_else( + |_| axum::http::StatusCode::BAD_REQUEST.into_response(), + |result| { + try_unwrap_body!(axum::response::Response::builder() .status(axum::http::StatusCode::OK) - .header(axum::http::header::CONTENT_TYPE, mime::APPLICATION_JSON.to_string()) - .body(result))) + .header( + axum::http::header::CONTENT_TYPE, + mime::APPLICATION_JSON.to_string() + ) + .body(result)) + }, + ) } macro_rules! skip_json_response_if_flag_set { @@ -776,6 +1308,7 @@ macro_rules! skip_flag_optional { }; } +#[allow(clippy::too_many_lines)] async fn xtream_player_api( api_req: UserApiRequest, app_state: &Arc, @@ -796,32 +1329,75 @@ async fn xtream_player_api( } // Process specific playlist actions - let (skip_live, skip_vod, skip_series) = if let Some(inputs) = app_state.app_config.get_inputs_for_target(&target.name) { - inputs.iter().fold((true, true, true), |acc, i| { - let (l, v, s) = acc; - i.options.as_ref().map_or((false, false, false), |o| (l && o.xtream_skip_live, v && o.xtream_skip_vod, s && o.xtream_skip_series)) - }) - } else { - (false, false, false) - }; + let (skip_live, skip_vod, skip_series) = + if let Some(inputs) = app_state.app_config.get_inputs_for_target(&target.name) { + inputs.iter().fold((true, true, true), |acc, i| { + let (l, v, s) = acc; + i.options.as_ref().map_or((false, false, false), |o| { + ( + l && o.xtream_skip_live, + v && o.xtream_skip_vod, + s && o.xtream_skip_series, + ) + }) + }) + } else { + (false, false, false) + }; match action { crate::model::XC_ACTION_GET_ACCOUNT_INFO => { return axum::response::Json(get_user_info(&user, app_state)).into_response(); } 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); + 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 + ); } 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); + 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 + ); } 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(); + app_state, + &user, + &target, + api_req.stream_id.trim(), + api_req.limit.trim(), + ) + .await + .into_response(); } 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); + 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 + ); } _ => {} } @@ -829,19 +1405,55 @@ async fn xtream_player_api( let category_id = api_req.category_id.trim().parse::().ok(); // Handle general content actions if let Some(response) = xtream_player_api_handle_content_action( - &app_state.app_config.config.load(), &target.name, action, category_id, &user, - ).await { + &app_state.app_config.config.load(), + &target.name, + action, + category_id, + &user, + ) + .await + { return response.into_response(); } let result = match action { - crate::model::XC_ACTION_GET_LIVE_STREAMS => - skip_flag_optional!(skip_live, xtream_repository::xtream_load_rewrite_playlist(XtreamCluster::Live, &app_state.app_config, &target, category_id, &user).await), - crate::model::XC_ACTION_GET_VOD_STREAMS => - skip_flag_optional!(skip_vod, xtream_repository::xtream_load_rewrite_playlist(XtreamCluster::Video, &app_state.app_config, &target, category_id, &user).await), - crate::model::XC_ACTION_GET_SERIES => - skip_flag_optional!(skip_series, xtream_repository::xtream_load_rewrite_playlist(XtreamCluster::Series, &app_state.app_config, &target, category_id, &user).await), - _ => Some(Err(info_err!(format!("Cant find content: {action} for target: {}", &target.name)))), + crate::model::XC_ACTION_GET_LIVE_STREAMS => skip_flag_optional!( + skip_live, + xtream_repository::xtream_load_rewrite_playlist( + XtreamCluster::Live, + &app_state.app_config, + &target, + category_id, + &user + ) + .await + ), + crate::model::XC_ACTION_GET_VOD_STREAMS => skip_flag_optional!( + skip_vod, + xtream_repository::xtream_load_rewrite_playlist( + XtreamCluster::Video, + &app_state.app_config, + &target, + category_id, + &user + ) + .await + ), + crate::model::XC_ACTION_GET_SERIES => skip_flag_optional!( + skip_series, + xtream_repository::xtream_load_rewrite_playlist( + XtreamCluster::Series, + &app_state.app_config, + &target, + category_id, + &user + ) + .await + ), + _ => Some(Err(info_err!(format!( + "Cant find content: {action} for target: {}", + &target.name + )))), }; match result { @@ -852,11 +1464,17 @@ async fn xtream_player_api( let content_stream = xtream_create_content_stream(xtream_iter); try_unwrap_body!(axum::response::Response::builder() .status(axum::http::StatusCode::OK) - .header(axum::http::header::CONTENT_TYPE, mime::APPLICATION_JSON.to_string()) + .header( + axum::http::header::CONTENT_TYPE, + mime::APPLICATION_JSON.to_string() + ) .body(axum::body::Body::from_stream(content_stream))) } Err(err) => { - error!("Failed response for xtream target: {} action: {} error: {}", &target.name, action, err); + error!( + "Failed response for xtream target: {} action: {} error: {}", + &target.name, action, err + ); // Some players fail on NoContent, so we return an empty array api_utils::empty_json_list_response().into_response() } @@ -877,7 +1495,9 @@ async fn xtream_player_api( } } -fn xtream_create_content_stream(xtream_iter: impl Iterator) -> impl Stream> { +fn xtream_create_content_stream( + xtream_iter: impl Iterator, +) -> impl Stream> { stream::once(async { Ok::(Bytes::from("[")) }).chain( stream::iter(xtream_iter.map(move |(line, has_next)| { Ok::(Bytes::from(if has_next { @@ -885,7 +1505,11 @@ fn xtream_create_content_stream(xtream_iter: impl Iterator) } else { line.to_string() })) - })).chain(stream::once(async { Ok::(Bytes::from("]")) }))) + })) + .chain(stream::once(async { + Ok::(Bytes::from("]")) + })), + ) } async fn xtream_player_api_get( @@ -895,7 +1519,6 @@ async fn xtream_player_api_get( xtream_player_api(api_req, &app_state).await } - async fn xtream_player_api_post( axum::extract::State(app_state): axum::extract::State>, axum::extract::Form(api_req): axum::extract::Form, @@ -947,29 +1570,55 @@ macro_rules! register_xtream_api_timeshift { async fn xtream_player_token_stream( Fingerprint(fingerprint, addr): Fingerprint, - axum::extract::Path((token, target_id, cluster, stream_id)): axum::extract::Path<(String, u16, String, String)>, + axum::extract::Path((token, target_id, cluster, stream_id)): axum::extract::Path<( + String, + u16, + String, + String, + )>, axum::extract::State(app_state): axum::extract::State>, req_headers: HeaderMap, ) -> impl IntoResponse + Send { let ctxt = try_result_bad_request!(ApiStreamContext::from_str(cluster.as_str())); - xtream_player_api_stream_with_token(&fingerprint, &addr, &req_headers, &app_state, target_id, ApiStreamRequest::from_access_token(ctxt, &token, &stream_id, "")).await.into_response() + xtream_player_api_stream_with_token( + &fingerprint, + &addr, + &req_headers, + &app_state, + target_id, + ApiStreamRequest::from_access_token(ctxt, &token, &stream_id, ""), + ) + .await + .into_response() } pub fn xtream_api_register() -> axum::Router> { let router = axum::Router::new(); let mut router = register_xtream_api!(router, ["/player_api.php", "/panel_api.php", "/xtream"]); - router = router.route("/token/{token}/{target_id}/{cluster}/{stream_id}", axum::routing::get(xtream_player_token_stream)); - router = register_xtream_api_stream!(router, [ - ("", xtream_player_api_live_stream_alt), - ("/live", xtream_player_api_live_stream), - ("/movie", xtream_player_api_movie_stream), - ("/series", xtream_player_api_series_stream)]); - router = router.route("/timeshift/{username}/{password}/{duration}/{start}/{stream_id}", axum::routing::get(xtream_player_api_timeshift_stream)); - router = register_xtream_api_timeshift!(router, [ - "/timeshift.php", - "/streaming/timeshift.php"]); - register_xtream_api_resource!(router, [ - ("live", xtream_player_api_live_resource), - ("movie", xtream_player_api_movie_resource), - ("series", xtream_player_api_series_resource)]) -} \ No newline at end of file + router = router.route( + "/token/{token}/{target_id}/{cluster}/{stream_id}", + axum::routing::get(xtream_player_token_stream), + ); + router = register_xtream_api_stream!( + router, + [ + ("", xtream_player_api_live_stream_alt), + ("/live", xtream_player_api_live_stream), + ("/movie", xtream_player_api_movie_stream), + ("/series", xtream_player_api_series_stream) + ] + ); + router = router.route( + "/timeshift/{username}/{password}/{duration}/{start}/{stream_id}", + axum::routing::get(xtream_player_api_timeshift_stream), + ); + router = register_xtream_api_timeshift!(router, ["/timeshift.php", "/streaming/timeshift.php"]); + register_xtream_api_resource!( + router, + [ + ("live", xtream_player_api_live_resource), + ("movie", xtream_player_api_movie_resource), + ("series", xtream_player_api_series_resource) + ] + ) +} diff --git a/backend/src/api/main_api.rs b/backend/src/api/main_api.rs index 72500b10d..34b332eae 100644 --- a/backend/src/api/main_api.rs +++ b/backend/src/api/main_api.rs @@ -8,15 +8,18 @@ use crate::api::endpoints::web_index::{index_register_with_path, index_register_ use crate::api::endpoints::websocket_api::ws_api_register; use crate::api::endpoints::xmltv_api::xmltv_api_register; use crate::api::endpoints::xtream_api::xtream_api_register; -use crate::api::model::active_provider_manager::ActiveProviderManager; -use crate::api::model::active_user_manager::ActiveUserManager; -use crate::api::model::app_state::{create_cache, create_http_client, AppState, CancelTokens, HdHomerunAppState}; -use crate::api::model::download::DownloadQueue; -use crate::api::model::streams::shared_stream_manager::SharedStreamManager; +use crate::api::model::ActiveProviderManager; +use crate::api::model::ActiveUserManager; +use crate::api::model::DownloadQueue; +use crate::api::model::EventManager; +use crate::api::model::SharedStreamManager; +use crate::api::model::{ + create_cache, create_http_client, AppState, CancelTokens, HdHomerunAppState, +}; use crate::api::scheduler::exec_scheduler; use crate::api::serve::serve; -use crate::model::{AppConfig, Config, ProcessTargets, RateLimitConfig}; use crate::model::Healthcheck; +use crate::model::{AppConfig, Config, ProcessTargets, RateLimitConfig}; use crate::processing::processor::playlist; use crate::VERSION; use arc_swap::{ArcSwap, ArcSwapOption}; @@ -24,22 +27,25 @@ use axum::Router; use log::{error, info}; use std::io::ErrorKind; use std::path::PathBuf; -use std::sync::{Arc}; +use std::sync::Arc; use tokio_util::sync::CancellationToken; use tower_governor::key_extractor::SmartIpKeyExtractor; -use crate::api::model::event_manager::EventManager; fn get_web_dir_path(web_ui_enabled: bool, web_root: &str) -> Result { let web_dir = web_root.to_string(); let web_dir_path = PathBuf::from(&web_dir); if web_ui_enabled && (!&web_dir_path.exists() || !&web_dir_path.is_dir()) { - return Err(std::io::Error::new(ErrorKind::NotFound, - format!("web_root does not exists or is not an directory: {}", web_dir_path.display()))); + return Err(std::io::Error::new( + ErrorKind::NotFound, + format!( + "web_root does not exists or is not an directory: {}", + web_dir_path.display() + ), + )); } Ok(web_dir_path) } - fn create_healthcheck() -> Healthcheck { Healthcheck { status: "ok".to_string(), @@ -53,14 +59,22 @@ async fn healthcheck() -> impl axum::response::IntoResponse { axum::Json(create_healthcheck()) } -fn create_shared_data(app_config: &Arc, forced_targets: &Arc) -> AppState { +fn create_shared_data( + app_config: &Arc, + forced_targets: &Arc, +) -> AppState { let config = app_config.config.load(); let cache = create_cache(&config); let shared_stream_manager = Arc::new(SharedStreamManager::new()); let (provider_change_tx, provider_change_rx) = tokio::sync::mpsc::channel(10); let active_provider = Arc::new(ActiveProviderManager::new(app_config, provider_change_tx)); let (active_user_change_tx, active_user_change_rx) = tokio::sync::mpsc::channel(10); - let active_users = Arc::new(ActiveUserManager::new(&config, &shared_stream_manager, &active_provider, active_user_change_tx)); + let active_users = Arc::new(ActiveUserManager::new( + &config, + &shared_stream_manager, + &active_provider, + active_user_change_tx, + )); let event_manager = Arc::new(EventManager::new(active_user_change_rx, provider_change_rx)); let client = create_http_client(app_config); @@ -78,18 +92,21 @@ fn create_shared_data(app_config: &Arc, forced_targets: &Arc, cfg: &Arc, targets: &Arc) { +fn exec_update_on_boot( + client: Arc, + cfg: &Arc, + targets: &Arc, +) { let config = cfg.config.load(); if config.update_on_boot { let cfg_clone = Arc::clone(cfg); let targets_clone = Arc::clone(targets); tokio::spawn( - async move { playlist::exec_processing(client, cfg_clone, targets_clone).await } + async move { playlist::exec_processing(client, cfg_clone, targets_clone).await }, ); } } - fn is_web_auth_enabled(cfg: &Arc, web_ui_enabled: bool) -> bool { if web_ui_enabled { if let Some(web_auth) = &cfg.web_ui.as_ref().and_then(|c| c.auth.as_ref()) { @@ -103,7 +120,12 @@ fn create_cors_layer() -> tower_http::cors::CorsLayer { tower_http::cors::CorsLayer::new() // .allow_credentials(true) .allow_origin(tower_http::cors::Any) - .allow_methods([axum::http::Method::GET, axum::http::Method::POST, axum::http::Method::OPTIONS, axum::http::Method::HEAD]) + .allow_methods([ + axum::http::Method::GET, + axum::http::Method::POST, + axum::http::Method::OPTIONS, + axum::http::Method::HEAD, + ]) .allow_headers(tower_http::cors::Any) .max_age(std::time::Duration::from_secs(3600)) } @@ -115,8 +137,12 @@ fn create_compression_layer() -> tower_http::compression::CompressionLayer { .zstd(true) } -pub(in crate::api) fn start_hdhomerun(app_config: &Arc, app_state: &Arc, infos: &mut Vec, - cancel_token: &CancellationToken) { +pub(in crate::api) fn start_hdhomerun( + app_config: &Arc, + app_state: &Arc, + infos: &mut Vec, + cancel_token: &CancellationToken, +) { let config = app_config.config.load(); let host = config.api.host.to_string(); let guard = app_config.hdhomerun.load(); @@ -129,7 +155,10 @@ pub(in crate::api) fn start_hdhomerun(app_config: &Arc, app_state: &A let port = device.port; let device_clone = Arc::new(device.clone()); let basic_auth = hdhomerun.auth; - infos.push(format!("HdHomeRun Server '{}' running: http://{host}:{port}", device.name)); + infos.push(format!( + "HdHomeRun Server '{}' running: http://{host}:{port}", + device.name + )); let c_token = cancel_token.clone(); let active_user_manager = Arc::clone(&app_data.active_users); tokio::spawn(async move { @@ -139,12 +168,19 @@ pub(in crate::api) fn start_hdhomerun(app_config: &Arc, app_state: &A //.layer(tower_http::trace::TraceLayer::new_for_http()) // `Logger::default()` .merge(hdhr_api_register(basic_auth)); - let router: axum::Router<()> = router.with_state(Arc::new(HdHomerunAppState { - app_state: Arc::clone(&app_data), - device: Arc::clone(&device_clone), - })); + let router: axum::Router<()> = + router.with_state(Arc::new(HdHomerunAppState { + app_state: Arc::clone(&app_data), + device: Arc::clone(&device_clone), + })); - match tokio::net::TcpListener::bind(format!("{}:{}", app_host.clone(), port)).await { + match tokio::net::TcpListener::bind(format!( + "{}:{}", + app_host.clone(), + port + )) + .await + { Ok(listener) => { serve(listener, router, Some(c_token), active_user_manager).await; // if let Err(err) = axum::serve(listener, router.into_make_service_with_connect_info::()).into_future().await { @@ -165,7 +201,11 @@ pub(in crate::api) fn start_hdhomerun(app_config: &Arc, app_state: &A // next.run(request).await // } -pub async fn start_server(app_config: Arc, targets: Arc) -> futures::io::Result<()> { +#[allow(clippy::too_many_lines)] +pub async fn start_server( + app_config: Arc, + targets: Arc, +) -> futures::io::Result<()> { let mut infos = Vec::new(); let cfg = app_config.config.load(); let host = cfg.api.host.to_string(); @@ -173,7 +213,7 @@ pub async fn start_server(app_config: Arc, targets: Arc result, - Err(err) => return Err(err) + Err(err) => return Err(err), }; if web_ui_enabled { infos.push(format!("Web root: {}", web_dir_path.display())); @@ -184,11 +224,24 @@ pub async fn start_server(app_config: Arc, targets: Arc, targets: Arc, targets: Arc, targets: Arc, targets: Arc()).into_future().await } - -fn add_rate_limiter(router: Router>, rate_limit_cfg: &RateLimitConfig) -> Router> { +fn add_rate_limiter( + router: Router>, + rate_limit_cfg: &RateLimitConfig, +) -> Router> { if rate_limit_cfg.enabled { let governor_conf = tower_governor::governor::GovernorConfigBuilder::default() .key_extractor(SmartIpKeyExtractor) .per_millisecond(rate_limit_cfg.period_millis) .burst_size(rate_limit_cfg.burst_size) .finish(); - if let Some(config) = governor_conf { router.layer(tower_governor::GovernorLayer { config: Arc::new(config) }) } else { + if let Some(config) = governor_conf { + router.layer(tower_governor::GovernorLayer { + config: Arc::new(config), + }) + } else { error!("Failed to initialize rate limiter"); router } diff --git a/backend/src/api/model/active_provider_manager.rs b/backend/src/api/model/active_provider_manager.rs index 28d2e7154..f1240188b 100644 --- a/backend/src/api/model/active_provider_manager.rs +++ b/backend/src/api/model/active_provider_manager.rs @@ -1,4 +1,4 @@ -use crate::api::model::provider_config::{ProviderConnectionChangeSender, ProviderConfig, ProviderConfigConnection, ProviderConfigWrapper}; +use crate::api::model::{ProviderConnectionChangeSender, ProviderConfig, ProviderConfigConnection, ProviderConfigWrapper}; use crate::model::{AppConfig, ConfigInput}; use arc_swap::ArcSwap; use dashmap::DashMap; @@ -814,8 +814,8 @@ mod tests { thread::sleep(std::time::Duration::from_millis(200)); match $lineup.acquire(true, $grace_period_timeout_secs).await { ProviderAllocation::Exhausted => assert!(false, "Should available and not exhausted"), - ProviderAllocation::Available(provider) => assert_eq!(provider.id, $provider_id), - ProviderAllocation::GracePeriod(provider) => assert!(false, "Should available and not grace period: {}", provider.id), + ProviderAllocation::Available(_, provider) => assert_eq!(provider.id, $provider_id), + ProviderAllocation::GracePeriod(_, provider) => assert!(false, "Should available and not grace period: {}", provider.id), } }; } @@ -824,8 +824,8 @@ mod tests { thread::sleep(std::time::Duration::from_millis(200)); match $lineup.acquire(true, $grace_period_timeout_secs).await { ProviderAllocation::Exhausted => assert!(false, "Should grace period and not exhausted"), - ProviderAllocation::Available(provider) => assert!(false, "Should grace period and not available: {}", provider.id), - ProviderAllocation::GracePeriod(provider) => assert_eq!(provider.id, $provider_id), + ProviderAllocation::Available(_, provider) => assert!(false, "Should grace period and not available: {}", provider.id), + ProviderAllocation::GracePeriod(_, provider) => assert_eq!(provider.id, $provider_id), } }; } @@ -835,8 +835,8 @@ mod tests { thread::sleep(std::time::Duration::from_millis(200)); match $lineup.acquire(true, $grace_period_timeout_secs).await { ProviderAllocation::Exhausted => {}, - ProviderAllocation::Available(provider) => assert!(false, "Should exhausted and not available: {}", provider.id), - ProviderAllocation::GracePeriod(provider) => assert!(false, "Should exhausted and not grace period: {}", provider.id), + ProviderAllocation::Available(_, provider) => assert!(false, "Should exhausted and not available: {}", provider.id), + ProviderAllocation::GracePeriod(_, provider) => assert!(false, "Should exhausted and not grace period: {}", provider.id), } }; } @@ -884,8 +884,9 @@ mod tests { // Adding alias to the provider input.aliases = Some(vec![alias]); + let (change_tx, _) = tokio::sync::mpsc::channel::<(String, usize)>(1); // Create MultiProviderLineup with the provider and alias - let lineup = MultiProviderLineup::new(&input, None); + let lineup = MultiProviderLineup::new(&input, None, /* &tokio::sync::mpsc::Sender<(std::string::String, usize)> */ &change_tx); let rt = tokio::runtime::Runtime::new().unwrap(); rt.block_on(async move { // Test that the alias provider is available @@ -907,7 +908,8 @@ mod tests { let alias = create_config_input_alias(2, "http://alias.com", 0, 2); // Adding alias with different priority input.aliases = Some(vec![alias]); - let lineup = MultiProviderLineup::new(&input, None); + let (change_tx, _) = tokio::sync::mpsc::channel::<(String, usize)>(1); + let lineup = MultiProviderLineup::new(&input, None, &change_tx); // The alias has a higher priority, so the alias should be acquired first let rt = tokio::runtime::Runtime::new().unwrap(); rt.block_on(async move { @@ -927,8 +929,8 @@ mod tests { // Adding multiple aliases input.aliases = Some(vec![alias1, alias2]); - - let lineup = MultiProviderLineup::new(&input, None); + let (change_tx, _) = tokio::sync::mpsc::channel::<(String, usize)>(1); + let lineup = MultiProviderLineup::new(&input, None, &change_tx); let rt = tokio::runtime::Runtime::new().unwrap(); rt.block_on(async move { // The alias with priority 0 should be acquired first (higher priority) @@ -957,8 +959,8 @@ mod tests { // Adding alias input.aliases = Some(vec![alias1, alias2]); - - let lineup = MultiProviderLineup::new(&input, None); + let (change_tx, _) = tokio::sync::mpsc::channel::<(String, usize)>(1); + let lineup = MultiProviderLineup::new(&input, None, &change_tx); let rt = tokio::runtime::Runtime::new().unwrap(); rt.block_on(async move { // Acquire connection from alias2 @@ -984,7 +986,8 @@ mod tests { #[test] fn test_acquire_when_capacity_available() { let cfg = create_config_input(1, "provider5_1", 1, 2); - let lineup = SingleProviderLineup::new(&cfg, None); + let (change_tx, _) = tokio::sync::mpsc::channel::<(String, usize)>(1); + let lineup = SingleProviderLineup::new(&cfg, None, change_tx); let rt = tokio::runtime::Runtime::new().unwrap(); rt.block_on(async move { // First acquire attempt should succeed @@ -1062,7 +1065,8 @@ mod tests { #[test] fn test_concurrent_acquire() { let cfg = create_config_input(1, "provider9_1", 1, 2); - let lineup = Arc::new(SingleProviderLineup::new(&cfg, None)); + let (change_tx, _) = tokio::sync::mpsc::channel::<(String, usize)>(1); + let lineup = Arc::new(SingleProviderLineup::new(&cfg, None, change_tx)); let available_count = Arc::new(AtomicU16::new(2)); let grace_period_count = Arc::new(AtomicU16::new(1)); diff --git a/backend/src/api/model/active_user_manager.rs b/backend/src/api/model/active_user_manager.rs index 43bf90191..61b92807f 100644 --- a/backend/src/api/model/active_user_manager.rs +++ b/backend/src/api/model/active_user_manager.rs @@ -1,4 +1,3 @@ -use crate::api::model::streams::shared_stream_manager::SharedStreamManager; use crate::model::Config; use crate::model::ProxyUserCredentials; use dashmap::DashMap; @@ -8,7 +7,8 @@ use shared::model::UserConnectionPermission; use shared::utils::{current_time_secs, default_grace_period_millis, default_grace_period_timeout_secs, sanitize_sensitive_info}; use std::sync::atomic::{AtomicBool, AtomicU64, Ordering}; use std::sync::Arc; -use crate::api::model::active_provider_manager::ActiveProviderManager; +use crate::api::model::ActiveProviderManager; +use crate::api::model::SharedStreamManager; type ActiveUserConnectionChangeSender = tokio::sync::mpsc::Sender<(usize, usize)>; pub type ActiveUserConnectionChangeReceiver = tokio::sync::mpsc::Receiver<(usize, usize)>; diff --git a/backend/src/api/model/app_state.rs b/backend/src/api/model/app_state.rs index 62a5907d1..b4b3b5961 100644 --- a/backend/src/api/model/app_state.rs +++ b/backend/src/api/model/app_state.rs @@ -1,7 +1,6 @@ -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::api::model::ActiveProviderManager; +use crate::api::model::ActiveUserManager; +use crate::api::model::DownloadQueue; use crate::api::scheduler::exec_scheduler; use crate::model::{AppConfig, Config, HdHomeRunConfig, HdHomeRunDeviceConfig, ProcessTargets, ScheduleConfig, SourcesConfig}; use crate::tools::lru_cache::LRUResourceCache; @@ -17,7 +16,8 @@ use std::time::Duration; use tokio::sync::Mutex; use tokio_util::sync::CancellationToken; use crate::api::config_watch::exec_config_watch; -use crate::api::model::event_manager::EventManager; +use crate::api::model::EventManager; +use crate::api::model::SharedStreamManager; macro_rules! cancel_service { ($field: ident, $changes:expr, $cancel_tokens:expr) => { diff --git a/backend/src/api/model/event_manager.rs b/backend/src/api/model/event_manager.rs index 0df0d5f64..e75c5cbad 100644 --- a/backend/src/api/model/event_manager.rs +++ b/backend/src/api/model/event_manager.rs @@ -1,7 +1,7 @@ use log::error; use tokio::task; -use crate::api::model::active_user_manager::{ActiveUserConnectionChangeReceiver}; -use crate::api::model::provider_config::{ProviderConnectionChangeReceiver}; +use crate::api::model::{ActiveUserConnectionChangeReceiver}; +use crate::api::model::{ProviderConnectionChangeReceiver}; #[derive(Clone, PartialEq)] pub enum EventMessage { diff --git a/backend/src/api/model/mod.rs b/backend/src/api/model/mod.rs index 1aebde331..2e8eb2a55 100644 --- a/backend/src/api/model/mod.rs +++ b/backend/src/api/model/mod.rs @@ -1,13 +1,25 @@ -pub mod app_state; -pub(in crate::api) mod request; -pub(in crate::api) mod config; -pub(in crate::api) mod download; -pub(in crate::api) mod xtream; -pub(in crate::api) mod model_utils; -pub(in crate::api) mod stream_error; -pub(crate) mod streams; -pub(in crate::api) mod active_user_manager; -pub(in crate::api) mod active_provider_manager; -pub(in crate::api) mod stream; -pub(in crate::api) mod provider_config; -pub(in crate::api) mod event_manager; \ No newline at end of file +mod app_state; +mod request; +mod download; +mod xtream; +mod model_utils; +mod stream_error; +mod streams; +mod active_user_manager; +mod active_provider_manager; +mod stream; +mod provider_config; +mod event_manager; + +pub use self::app_state::*; +pub(in crate::api) use self::request::*; +pub(in crate::api) use self::download::*; +pub(in crate::api) use self::xtream::*; +pub(in crate::api) use self::model_utils::*; +pub(in crate::api) use self::stream_error::*; +pub(crate) use self::streams::*; +pub(in crate::api) use self::active_user_manager::*; +pub(in crate::api) use self::active_provider_manager::*; +pub(in crate::api) use self::stream::*; +pub(in crate::api) use self::provider_config::*; +pub(in crate::api) use self::event_manager::*; \ No newline at end of file diff --git a/backend/src/api/model/provider_config.rs b/backend/src/api/model/provider_config.rs index 3791cf868..5e36bd660 100644 --- a/backend/src/api/model/provider_config.rs +++ b/backend/src/api/model/provider_config.rs @@ -1,4 +1,4 @@ -use crate::api::model::active_provider_manager::ProviderAllocation; +use crate::api::model::ProviderAllocation; use crate::model::{ConfigInput, ConfigInputAlias, InputUserInfo}; use jsonwebtoken::get_current_timestamp; use log::{debug}; diff --git a/backend/src/api/model/stream.rs b/backend/src/api/model/stream.rs index ee1918521..2c8d693fb 100644 --- a/backend/src/api/model/stream.rs +++ b/backend/src/api/model/stream.rs @@ -1,4 +1,4 @@ -use crate::api::model::stream_error::StreamError; +use crate::api::model::StreamError; use axum::http::StatusCode; use bytes::Bytes; use futures::stream::BoxStream; diff --git a/backend/src/api/model/streams/active_client_stream.rs b/backend/src/api/model/streams/active_client_stream.rs index 5545f2cf8..aa27578dc 100644 --- a/backend/src/api/model/streams/active_client_stream.rs +++ b/backend/src/api/model/streams/active_client_stream.rs @@ -1,11 +1,11 @@ use crate::api::api_utils::StreamDetails; -use crate::api::model::active_provider_manager::{ActiveProviderManager, ProviderConnectionGuard}; -use crate::api::model::active_user_manager::{ActiveUserManager, UserConnectionGuard}; -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::timed_client_stream::TimedClientStream; -use crate::api::model::streams::transport_stream_buffer::TransportStreamBuffer; +use crate::api::model::{ActiveProviderManager, ProviderConnectionGuard}; +use crate::api::model::{ActiveUserManager, UserConnectionGuard}; +use crate::api::model::AppState; +use crate::api::model::BoxedProviderStream; +use crate::api::model::StreamError; +use crate::api::model::TimedClientStream; +use crate::api::model::TransportStreamBuffer; use crate::model::ProxyUserCredentials; use bytes::Bytes; use futures::Stream; diff --git a/backend/src/api/model/streams/buffered_stream.rs b/backend/src/api/model/streams/buffered_stream.rs index c6b701ff6..ae9880ec5 100644 --- a/backend/src/api/model/streams/buffered_stream.rs +++ b/backend/src/api/model/streams/buffered_stream.rs @@ -6,8 +6,8 @@ use std::{ use std::cmp::min; use tokio::sync::mpsc::{channel, Sender}; use tokio_stream::wrappers::ReceiverStream; -use crate::api::model::stream::BoxedProviderStream; -use crate::api::model::stream_error::StreamError; +use crate::api::model::BoxedProviderStream; +use crate::api::model::StreamError; use crate::tools::atomic_once_flag::AtomicOnceFlag; pub(in crate::api::model) struct BufferedStream { diff --git a/backend/src/api/model/streams/client_stream.rs b/backend/src/api/model/streams/client_stream.rs index f0458d38f..cbaf037d2 100644 --- a/backend/src/api/model/streams/client_stream.rs +++ b/backend/src/api/model/streams/client_stream.rs @@ -6,8 +6,8 @@ use std::task::{Poll}; use futures::{Stream}; use log::trace; use shared::utils::sanitize_sensitive_info; -use crate::api::model::stream::BoxedProviderStream; -use crate::api::model::stream_error::StreamError; +use crate::api::model::BoxedProviderStream; +use crate::api::model::StreamError; use crate::utils::trace_if_enabled; use crate::tools::atomic_once_flag::AtomicOnceFlag; diff --git a/backend/src/api/model/streams/custom_video_stream.rs b/backend/src/api/model/streams/custom_video_stream.rs index 2b0e657a6..c8b84dc7b 100644 --- a/backend/src/api/model/streams/custom_video_stream.rs +++ b/backend/src/api/model/streams/custom_video_stream.rs @@ -1,5 +1,5 @@ -use crate::api::model::stream_error::StreamError; -use crate::api::model::streams::transport_stream_buffer::TransportStreamBuffer; +use crate::api::model::StreamError; +use crate::api::model::TransportStreamBuffer; use bytes::Bytes; use futures::Stream; use std::pin::Pin; diff --git a/backend/src/api/model/streams/mod.rs b/backend/src/api/model/streams/mod.rs index 9be4f8bc5..208a2a351 100644 --- a/backend/src/api/model/streams/mod.rs +++ b/backend/src/api/model/streams/mod.rs @@ -1,12 +1,22 @@ -pub(in crate::api) mod provider_stream; -pub(in crate::api) mod persist_pipe_stream; -pub(in crate::api) mod provider_stream_factory; -pub(in crate::api) mod shared_stream_manager; -pub(in crate::api) mod active_client_stream; -pub(in crate::api) mod throttled_stream; mod timed_client_stream; mod buffered_stream; mod client_stream; mod custom_video_stream; -pub(in crate) mod transport_stream_buffer; +mod transport_stream_buffer; // mod chunked_buffer; +mod provider_stream; +mod persist_pipe_stream; +mod provider_stream_factory; +mod shared_stream_manager; +mod active_client_stream; +mod throttled_stream; + +pub(in crate) use self::transport_stream_buffer::*; +pub(in crate::api) use self::provider_stream::*; +pub(in crate::api) use self::persist_pipe_stream::*; +pub(in crate::api) use self::provider_stream_factory::*; +pub(in crate::api) use self::shared_stream_manager::*; +pub(in crate::api) use self::active_client_stream::*; +pub(in crate::api) use self::throttled_stream::*; +pub(in crate::api) use self::timed_client_stream::*; +pub(in crate::api) use self::custom_video_stream::*; diff --git a/backend/src/api/model/streams/persist_pipe_stream.rs b/backend/src/api/model/streams/persist_pipe_stream.rs index f518b4f2d..5864958e9 100644 --- a/backend/src/api/model/streams/persist_pipe_stream.rs +++ b/backend/src/api/model/streams/persist_pipe_stream.rs @@ -6,7 +6,7 @@ use std::task::{Context, Poll}; use bytes::Bytes; use log::error; use tokio_stream::Stream; -use crate::api::model::stream_error::StreamError; +use crate::api::model::StreamError; /// `PersistPipeStream` /// diff --git a/backend/src/api/model/streams/provider_stream.rs b/backend/src/api/model/streams/provider_stream.rs index b164c609f..447d8020f 100644 --- a/backend/src/api/model/streams/provider_stream.rs +++ b/backend/src/api/model/streams/provider_stream.rs @@ -1,12 +1,12 @@ use crate::api::api_utils::{HeaderFilter}; -use crate::api::model::streams::custom_video_stream::CustomVideoStream; +use crate::api::model::CustomVideoStream; use crate::model::{AppConfig}; use shared::model::PlaylistItemType; use log::{trace}; use reqwest::StatusCode; use axum::response::IntoResponse; use crate::api::model::stream::ProviderStreamResponse; -use crate::api::model::streams::transport_stream_buffer::TransportStreamBuffer; +use crate::api::model::TransportStreamBuffer; use crate::api::api_utils::try_unwrap_body; #[derive(Debug, Copy, Clone)] diff --git a/backend/src/api/model/streams/provider_stream_factory.rs b/backend/src/api/model/streams/provider_stream_factory.rs index 020ebfdf4..48045d821 100644 --- a/backend/src/api/model/streams/provider_stream_factory.rs +++ b/backend/src/api/model/streams/provider_stream_factory.rs @@ -1,27 +1,27 @@ use crate::api::api_utils::{get_headers_from_request, StreamOptions}; -use crate::api::model::model_utils::get_response_headers; -use crate::api::model::stream::{BoxedProviderStream, ProviderStreamFactoryResponse}; -use crate::api::model::stream_error::StreamError; -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::TimedClientStream; -use shared::model::{PlaylistItemType, DEFAULT_USER_AGENT}; -use crate::model::{AppConfig}; +use crate::api::model::get_response_headers; +use crate::api::model::StreamError; +use crate::api::model::TimedClientStream; +use crate::api::model::{create_channel_unavailable_stream, get_header_filter_for_item_type}; +use crate::api::model::{BoxedProviderStream, ProviderStreamFactoryResponse}; +use crate::model::AppConfig; use crate::tools::atomic_once_flag::AtomicOnceFlag; +use crate::utils::debug_if_enabled; use crate::utils::request::{classify_content_type, get_request_headers, MimeCategory}; -use crate::utils::{debug_if_enabled}; -use shared::utils::{filter_request_header, sanitize_sensitive_info}; use futures::stream::{self}; use futures::{StreamExt, TryStreamExt}; use log::{debug, log_enabled, warn}; use reqwest::header::{HeaderMap, RANGE}; use reqwest::StatusCode; +use shared::model::{PlaylistItemType, DEFAULT_USER_AGENT}; +use shared::utils::{filter_request_header, sanitize_sensitive_info}; use std::collections::HashMap; use std::sync::atomic::{AtomicUsize, Ordering}; use std::sync::Arc; use std::time::{Duration, Instant}; use url::Url; +use crate::api::model::streams::buffered_stream::BufferedStream; +use crate::api::model::streams::client_stream::ClientStream; // TODO make this configurable pub const STREAM_QUEUE_SIZE: usize = 4096; // mpsc channel holding messages. with possible 8192byte chunks @@ -53,7 +53,11 @@ impl ProviderStreamFactoryOptions { req_headers: &HeaderMap, input_headers: Option<&HashMap>, ) -> Self { - let buffer_size = if stream_options.buffer_enabled { stream_options.buffer_size } else { STREAM_QUEUE_SIZE }; + let buffer_size = if stream_options.buffer_enabled { + stream_options.buffer_size + } else { + STREAM_QUEUE_SIZE + }; let filter_header = get_header_filter_for_item_type(item_type); let mut req_headers = get_headers_from_request(req_headers, &filter_header); // we need the range bytes from client request for seek ing to the right position @@ -133,7 +137,10 @@ impl ProviderStreamFactoryOptions { #[inline] pub fn get_total_bytes_send(&self) -> Option { - self.range_bytes.as_ref().as_ref().map(|atomic| atomic.load(Ordering::SeqCst)) + self.range_bytes + .as_ref() + .as_ref() + .map(|atomic| atomic.load(Ordering::SeqCst)) } // pub fn get_range_bytes(&self) -> &Arc> { @@ -181,8 +188,10 @@ fn get_request_range_start_bytes(req_headers: &HashMap>) -> Opti // } // } - -fn prepare_client(request_client: &Arc, stream_options: &ProviderStreamFactoryOptions) -> (reqwest::RequestBuilder, bool) { +fn prepare_client( + request_client: &Arc, + stream_options: &ProviderStreamFactoryOptions, +) -> (reqwest::RequestBuilder, bool) { let url = stream_options.get_url(); let range_start = stream_options.get_total_bytes_send(); let original_headers = stream_options.get_headers(); @@ -209,11 +218,17 @@ fn prepare_client(request_client: &Arc, stream_options: &Provid // } if !headers.contains_key(axum::http::header::CONNECTION) { - headers.insert(axum::http::header::CONNECTION, axum::http::header::HeaderValue::from_static("keep-alive")); + headers.insert( + axum::http::header::CONNECTION, + axum::http::header::HeaderValue::from_static("keep-alive"), + ); } if !headers.contains_key(axum::http::header::USER_AGENT) { - headers.insert(axum::http::header::USER_AGENT, axum::http::header::HeaderValue::from_static(DEFAULT_USER_AGENT)); + headers.insert( + axum::http::header::USER_AGENT, + axum::http::header::HeaderValue::from_static(DEFAULT_USER_AGENT), + ); } let partial = if let Some(range) = range_start { @@ -227,7 +242,13 @@ fn prepare_client(request_client: &Arc, stream_options: &Provid }; if log_enabled!(log::Level::Debug) { - let message = format!("Stream requested with headers: {:?}", headers.iter().map(|header| (header.0, String::from_utf8_lossy(header.1.as_ref()))).collect::>()); + let message = format!( + "Stream requested with headers: {:?}", + headers + .iter() + .map(|header| (header.0, String::from_utf8_lossy(header.1.as_ref()))) + .collect::>() + ); debug!("{}", sanitize_sensitive_info(&message)); } @@ -236,7 +257,11 @@ fn prepare_client(request_client: &Arc, stream_options: &Provid (request_builder, partial) } -async fn provider_stream_request(cfg: &AppConfig, request_client: Arc, stream_options: &ProviderStreamFactoryOptions) -> Result, StatusCode> { +async fn provider_stream_request( + cfg: &AppConfig, + request_client: Arc, + stream_options: &ProviderStreamFactoryOptions, +) -> Result, StatusCode> { let (client, _partial_content) = prepare_client(&request_client, stream_options); match client.send().await { Ok(mut response) => { @@ -246,22 +271,38 @@ async fn provider_stream_request(cfg: &AppConfig, request_client: Arc = get_response_headers(response.headers()); + let response_headers: Vec<(String, String)> = + get_response_headers(response.headers()); //let url = stream_options.get_url(); // debug!("First headers {headers:?} {} {}", sanitize_sensitive_info(url.as_str())); - Some((response_headers, response.status(), Some(response.url().clone()))) + Some(( + response_headers, + response.status(), + Some(response.url().clone()), + )) }; - let provider_stream = response.bytes_stream().map_err(|err| { - // error!("Stream error {err}"); - StreamError::reqwest(&err) - }).boxed(); + let provider_stream = response + .bytes_stream() + .map_err(|err| { + // error!("Stream error {err}"); + StreamError::reqwest(&err) + }) + .boxed(); let boxed_provider_stream = if stream_options.get_reconnect_force_secs() > 0 { - TimedClientStream::new(provider_stream, stream_options.get_reconnect_force_secs()).boxed() + TimedClientStream::new( + provider_stream, + stream_options.get_reconnect_force_secs(), + ) + .boxed() } else { provider_stream }; @@ -277,40 +318,50 @@ async fn provider_stream_request(cfg: &AppConfig, request_client: Arc { if let (Some(boxed_provider_stream), response_info) = - create_channel_unavailable_stream(cfg, &get_response_headers(stream_options.get_headers()), StatusCode::BAD_GATEWAY) + create_channel_unavailable_stream( + cfg, + &get_response_headers(stream_options.get_headers()), + StatusCode::BAD_GATEWAY, + ) { Ok(Some((boxed_provider_stream, response_info))) } else { Err(StatusCode::SERVICE_UNAVAILABLE) } } - _ => Err(status) + _ => Err(status), }; } if status.is_server_error() { debug!("Server error status response : {status}"); return match status { - StatusCode::INTERNAL_SERVER_ERROR | - StatusCode::BAD_GATEWAY | - StatusCode::SERVICE_UNAVAILABLE | - StatusCode::GATEWAY_TIMEOUT => { + StatusCode::INTERNAL_SERVER_ERROR + | StatusCode::BAD_GATEWAY + | StatusCode::SERVICE_UNAVAILABLE + | StatusCode::GATEWAY_TIMEOUT => { if let (Some(boxed_provider_stream), response_info) = - create_channel_unavailable_stream(cfg, &get_response_headers(stream_options.get_headers()), StatusCode::BAD_GATEWAY) + create_channel_unavailable_stream( + cfg, + &get_response_headers(stream_options.get_headers()), + StatusCode::BAD_GATEWAY, + ) { Ok(Some((boxed_provider_stream, response_info))) } else { Err(StatusCode::SERVICE_UNAVAILABLE) } } - _ => Err(status) - } + _ => Err(status), + }; } Err(status) } Err(_err) => { - if let (Some(boxed_provider_stream), response_info) = - create_channel_unavailable_stream(cfg, &get_response_headers(stream_options.get_headers()), StatusCode::BAD_GATEWAY) - { + if let (Some(boxed_provider_stream), response_info) = create_channel_unavailable_stream( + cfg, + &get_response_headers(stream_options.get_headers()), + StatusCode::BAD_GATEWAY, + ) { Ok(Some((boxed_provider_stream, response_info))) } else { Err(StatusCode::SERVICE_UNAVAILABLE) @@ -319,7 +370,11 @@ async fn provider_stream_request(cfg: &AppConfig, request_client: Arc, stream_options: &ProviderStreamFactoryOptions) -> Result, StatusCode> { +async fn get_provider_stream( + cfg: &AppConfig, + client: Arc, + stream_options: &ProviderStreamFactoryOptions, +) -> Result, StatusCode> { let url = stream_options.get_url(); debug_if_enabled!("stream provider {}", sanitize_sensitive_info(url.as_str())); let start = Instant::now(); @@ -332,18 +387,30 @@ async fn get_provider_stream(cfg: &AppConfig, client: Arc, stre } Ok(None) => { if connect_err > ERR_MAX_RETRY_COUNT { - warn!("The stream could be unavailable. {}", sanitize_sensitive_info(stream_options.get_url().as_str())); + warn!( + "The stream could be unavailable. {}", + sanitize_sensitive_info(stream_options.get_url().as_str()) + ); } } Err(status) => { debug!("Provider stream response error status response : {status}"); - if status == StatusCode::FORBIDDEN || status == StatusCode::SERVICE_UNAVAILABLE || status == StatusCode::UNAUTHORIZED { - warn!("The stream could be unavailable. ({status}) {}", sanitize_sensitive_info(stream_options.get_url().as_str())); + if status == StatusCode::FORBIDDEN + || status == StatusCode::SERVICE_UNAVAILABLE + || status == StatusCode::UNAUTHORIZED + { + warn!( + "The stream could be unavailable. ({status}) {}", + sanitize_sensitive_info(stream_options.get_url().as_str()) + ); stream_options.cancel_reconnect(); return Err(status); } if connect_err > ERR_MAX_RETRY_COUNT { - warn!("The stream could be unavailable. ({status}) {}", sanitize_sensitive_info(stream_options.get_url().as_str())); + warn!( + "The stream could be unavailable. ({status}) {}", + sanitize_sensitive_info(stream_options.get_url().as_str()) + ); } } } @@ -354,29 +421,55 @@ async fn get_provider_stream(cfg: &AppConfig, client: Arc, stre break; } if start.elapsed().as_secs() > RETRY_SECONDS { - warn!("The stream could be unavailable. Giving up after {RETRY_SECONDS} seconds. {}", sanitize_sensitive_info(stream_options.get_url().as_str())); + warn!( + "The stream could be unavailable. Giving up after {RETRY_SECONDS} seconds. {}", + sanitize_sensitive_info(stream_options.get_url().as_str()) + ); break; } connect_err += 1; tokio::time::sleep(Duration::from_millis(50)).await; - debug_if_enabled!("Reconnecting stream {}", sanitize_sensitive_info(url.as_str())); + debug_if_enabled!( + "Reconnecting stream {}", + sanitize_sensitive_info(url.as_str()) + ); } - debug_if_enabled!("Stopped reconnecting stream {}", sanitize_sensitive_info(url.as_str())); + debug_if_enabled!( + "Stopped reconnecting stream {}", + sanitize_sensitive_info(url.as_str()) + ); stream_options.cancel_reconnect(); Err(StatusCode::SERVICE_UNAVAILABLE) } - -pub async fn create_provider_stream(cfg: Arc, - client: Arc, - stream_options: ProviderStreamFactoryOptions) -> Option { +#[allow(clippy::too_many_lines)] +pub async fn create_provider_stream( + cfg: Arc, + client: Arc, + stream_options: ProviderStreamFactoryOptions, +) -> Option { let client_stream_factory = |stream, reconnect_flag, range_cnt| { - let stream = if !stream_options.is_piped() && stream_options.is_buffer_enabled() && !stream_options.is_shared_stream() { - BufferedStream::new(stream, stream_options.get_buffer_size(), stream_options.get_reconnect_flag_clone(), stream_options.get_url_as_str()).boxed() + let stream = if !stream_options.is_piped() + && stream_options.is_buffer_enabled() + && !stream_options.is_shared_stream() + { + BufferedStream::new( + stream, + stream_options.get_buffer_size(), + stream_options.get_reconnect_flag_clone(), + stream_options.get_url_as_str(), + ) + .boxed() } else { stream }; - ClientStream::new(stream, reconnect_flag, range_cnt, stream_options.get_url_as_str()).boxed() + ClientStream::new( + stream, + reconnect_flag, + range_cnt, + stream_options.get_url_as_str(), + ) + .boxed() }; match get_provider_stream(&cfg, Arc::clone(&client), &stream_options).await { @@ -406,7 +499,11 @@ pub async fn create_provider_stream(cfg: Arc, Ok(None) => None, Err(status) => { if let (Some(boxed_provider_stream), _response_info) = - create_channel_unavailable_stream(&config_clone, &get_response_headers(stream_opts.get_headers()), status) + create_channel_unavailable_stream( + &config_clone, + &get_response_headers(stream_opts.get_headers()), + status, + ) { return Some((boxed_provider_stream, ())); } @@ -417,19 +514,37 @@ pub async fn create_provider_stream(cfg: Arc, None } } - }).flatten().boxed(); - Some((client_stream_factory(init_stream.chain(unfold).boxed(), Arc::clone(&continue_client_signal), stream_options.get_range_bytes_clone()).boxed(), info)) + }) + .flatten() + .boxed(); + Some(( + client_stream_factory( + init_stream.chain(unfold).boxed(), + Arc::clone(&continue_client_signal), + stream_options.get_range_bytes_clone(), + ) + .boxed(), + info, + )) } else { - Some((client_stream_factory(init_stream.boxed(), Arc::clone(&continue_signal), stream_options.get_range_bytes_clone()).boxed(), info)) + Some(( + client_stream_factory( + init_stream.boxed(), + Arc::clone(&continue_signal), + stream_options.get_range_bytes_clone(), + ) + .boxed(), + info, + )) } } - Ok(None) => { - None - } + Ok(None) => None, Err(status) => { - if let (Some(boxed_provider_stream), response_info) = - create_channel_unavailable_stream(&cfg, &get_response_headers(stream_options.get_headers()), status) - { + if let (Some(boxed_provider_stream), response_info) = create_channel_unavailable_stream( + &cfg, + &get_response_headers(stream_options.get_headers()), + status, + ) { return Some((boxed_provider_stream, response_info)); } None diff --git a/backend/src/api/model/streams/shared_stream_manager.rs b/backend/src/api/model/streams/shared_stream_manager.rs index 375858664..cfb2c724d 100644 --- a/backend/src/api/model/streams/shared_stream_manager.rs +++ b/backend/src/api/model/streams/shared_stream_manager.rs @@ -1,14 +1,14 @@ -use crate::api::model::app_state::AppState; -use crate::api::model::stream_error::StreamError; -use crate::api::model::streams::provider_stream_factory::STREAM_QUEUE_SIZE; +use crate::api::model::AppState; +use crate::api::model::StreamError; +use crate::api::model::STREAM_QUEUE_SIZE; use crate::utils::debug_if_enabled; use bytes::Bytes; use futures::stream::BoxStream; use futures::{Stream, StreamExt}; use std::sync::Arc; -use crate::api::model::active_provider_manager::ProviderConnectionGuard; -use crate::api::model::stream::BoxedProviderStream; +use crate::api::model::ProviderConnectionGuard; +use crate::api::model::BoxedProviderStream; use dashmap::DashMap; use log::{debug, trace}; use shared::utils::sanitize_sensitive_info; diff --git a/backend/src/api/model/streams/throttled_stream.rs b/backend/src/api/model/streams/throttled_stream.rs index dbf0c944f..39a905fde 100644 --- a/backend/src/api/model/streams/throttled_stream.rs +++ b/backend/src/api/model/streams/throttled_stream.rs @@ -1,4 +1,4 @@ -use crate::api::model::stream_error::StreamError; +use crate::api::model::StreamError; use bytes::Bytes; use futures::Stream; use std::future::Future; diff --git a/backend/src/api/model/streams/timed_client_stream.rs b/backend/src/api/model/streams/timed_client_stream.rs index f76d8b84a..51702e34d 100644 --- a/backend/src/api/model/streams/timed_client_stream.rs +++ b/backend/src/api/model/streams/timed_client_stream.rs @@ -4,7 +4,7 @@ use futures::Stream; use std::pin::Pin; use std::task::Poll; use std::time::{Duration, Instant}; -use crate::api::model::stream::BoxedProviderStream; +use crate::api::model::BoxedProviderStream; pub struct TimedClientStream { inner: BoxedProviderStream, diff --git a/backend/src/api/serve.rs b/backend/src/api/serve.rs index 61b44dbfa..f319f9b9c 100644 --- a/backend/src/api/serve.rs +++ b/backend/src/api/serve.rs @@ -17,7 +17,7 @@ use std::time::Duration; use tokio::sync::watch; use tokio_util::sync::CancellationToken; use tower::{Service, ServiceExt}; -use crate::api::model::active_user_manager::ActiveUserManager; +use crate::api::model::ActiveUserManager; #[derive(Debug)] struct IncomingStream diff --git a/backend/src/auth/access_token.rs b/backend/src/auth/access_token.rs index e5bb99bff..af241a7a4 100644 --- a/backend/src/auth/access_token.rs +++ b/backend/src/auth/access_token.rs @@ -1,17 +1,16 @@ use shared::utils::{hex_decode, hex_encode}; use chrono::Utc; -use serde::{Deserialize, Serialize}; fn constant_time_eq(a: &[u8], b: &[u8]) -> bool { a.len() == b.len() && a.iter().zip(b.iter()).fold(0u8, |acc, (x, y)| acc | (x ^ y)) == 0 } -#[derive(Serialize, Deserialize, Debug)] -struct AccessToken { - ts: i64, - ttl: i64, - sig: String, -} +// #[derive(Serialize, Deserialize, Debug)] +// struct AccessToken { +// ts: i64, +// ttl: i64, +// sig: String, +// } pub fn create_access_token(secret: &[u8; 32], ttl_secs: u16) -> String { let timestamp = Utc::now().timestamp(); diff --git a/backend/src/auth/authenticator.rs b/backend/src/auth/authenticator.rs index 3ba48575d..79312f528 100644 --- a/backend/src/auth/authenticator.rs +++ b/backend/src/auth/authenticator.rs @@ -3,7 +3,7 @@ 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::WebAuthConfig; -use crate::api::model::app_state::AppState; +use crate::api::model::AppState; use crate::auth::AuthBearer; use shared::error::to_io_error; diff --git a/backend/src/model/config/api_user.rs b/backend/src/model/config/api_user.rs index 65fb79204..7982e5be9 100644 --- a/backend/src/model/config/api_user.rs +++ b/backend/src/model/config/api_user.rs @@ -4,7 +4,7 @@ use arc_swap::ArcSwap; use chrono::Local; use log::debug; use shared::model::{ProxyType, ProxyUserCredentialsDto, ProxyUserStatus, TargetUserDto, UserConnectionPermission}; -use crate::api::model::app_state::AppState; +use crate::api::model::AppState; use crate::model::{macros, Config}; #[derive(Debug, Clone)] diff --git a/backend/src/model/config/app.rs b/backend/src/model/config/app.rs index 2a3de600b..833a1d43f 100644 --- a/backend/src/model/config/app.rs +++ b/backend/src/model/config/app.rs @@ -11,11 +11,10 @@ use rand::Rng; use shared::create_tuliprox_error_result; use shared::error::{TuliproxError, TuliproxErrorKind}; use shared::model::ConfigPaths; -use crate::api::model::streams::transport_stream_buffer::TransportStreamBuffer; +use crate::api::model::TransportStreamBuffer; use crate::model::{ApiProxyConfig, ApiProxyServerInfo, Config, ConfigInput, ConfigInputOptions, ConfigTarget, CustomStreamResponse, HdHomeRunConfig, Mappings, ProxyUserCredentials, SourcesConfig, TargetOutput}; use crate::utils; - const CHANNEL_UNAVAILABLE: &str = "channel_unavailable.ts"; const USER_CONNECTIONS_EXHAUSTED: &str = "user_connections_exhausted.ts"; const PROVIDER_CONNECTIONS_EXHAUSTED: &str = "provider_connections_exhausted.ts"; diff --git a/backend/src/model/config/base.rs b/backend/src/model/config/base.rs index 7a14e5573..273cd9d81 100644 --- a/backend/src/model/config/base.rs +++ b/backend/src/model/config/base.rs @@ -91,7 +91,7 @@ impl Config { set_directory(&mut self.user_config_dir, "user_config", &self.working_dir); } - pub fn get_backup_dir(&self) -> Cow { + pub fn get_backup_dir(&self) -> Cow<'_, str> { self.backup_dir.as_ref().map_or_else(|| Cow::Borrowed(DEFAULT_BACKUP_DIR), |v| Cow::Borrowed(v)) } diff --git a/backend/src/model/config/source.rs b/backend/src/model/config/source.rs index a9533193f..266f886dc 100644 --- a/backend/src/model/config/source.rs +++ b/backend/src/model/config/source.rs @@ -137,7 +137,7 @@ impl SourcesConfig { }) } - pub fn get_unique_target_names(&self) -> HashSet> { + pub fn get_unique_target_names(&self) -> HashSet> { let mut seen_names = HashSet::new(); for source in &self.sources { for target in &source.targets { diff --git a/backend/src/model/config/stream.rs b/backend/src/model/config/stream.rs index ef1536dc9..57f2ad0a2 100644 --- a/backend/src/model/config/stream.rs +++ b/backend/src/model/config/stream.rs @@ -1,6 +1,6 @@ use shared::model::{StreamBufferConfigDto, StreamConfigDto}; use shared::utils::parse_to_kbps; -use crate::api::model::streams::transport_stream_buffer::TransportStreamBuffer; +use crate::api::model::TransportStreamBuffer; use crate::model::macros; #[derive(Debug, Clone)] diff --git a/backend/src/model/xmltv.rs b/backend/src/model/xmltv.rs index 8dac73391..f3c133bc8 100644 --- a/backend/src/model/xmltv.rs +++ b/backend/src/model/xmltv.rs @@ -58,7 +58,7 @@ impl XmlTag { } if let Some(attribs) = self.attributes.as_ref() { - attribs.iter().for_each(|(k, v)| elem.push_attribute((k.as_str(), v.as_str()))); + for (k, v) in attribs { elem.push_attribute((k.as_str(), v.as_str())); } } writer.write_event(Event::Start(elem))?; self.value.as_ref().map(|text| writer.write_event(Event::Text(BytesText::new(text.as_str())))); @@ -84,7 +84,7 @@ impl Epg { pub fn write_to(&self, writer: &mut Writer) -> Result<(), quick_xml::Error> { let mut elem = BytesStart::new("tv"); if let Some(attribs) = self.attributes.as_ref() { - attribs.iter().for_each(|(k, v)| elem.push_attribute((k.as_str(), v.as_str()))); + for (k, v) in attribs { elem.push_attribute((k.as_str(), v.as_str())); } } writer.write_event(Event::Start(elem))?; for child in &self.children { diff --git a/backend/src/processing/processor/xtream_series.rs b/backend/src/processing/processor/xtream_series.rs index 41402efc1..c94f19fe3 100644 --- a/backend/src/processing/processor/xtream_series.rs +++ b/backend/src/processing/processor/xtream_series.rs @@ -93,7 +93,7 @@ async fn playlist_resolve_series_info(cfg: &AppConfig, client: Arc 0 && ((processed_series_info_count - last_processed_series_info_count) > 50) && (elapsed % 30 == 0) { + if elapsed > 0 && ((processed_series_info_count - last_processed_series_info_count) > 50) && elapsed.is_multiple_of(30) { info!("resolved {processed_series_info_count}/{series_info_count} series info"); last_processed_series_info_count = processed_series_info_count; } diff --git a/backend/src/processing/processor/xtream_vod.rs b/backend/src/processing/processor/xtream_vod.rs index 5f87d63f6..4ffaabdf2 100644 --- a/backend/src/processing/processor/xtream_vod.rs +++ b/backend/src/processing/processor/xtream_vod.rs @@ -106,7 +106,7 @@ pub async fn playlist_resolve_vod(app_config: &AppConfig, client: Arc 0 && ((processed_vod_info_count - last_processed_vod_info_count) > 50) && (elapsed % 30 == 0) { + if elapsed > 0 && ((processed_vod_info_count - last_processed_vod_info_count) > 50) && elapsed.is_multiple_of(30) { info!("resolved {processed_vod_info_count}/{vod_info_count} vod info"); last_processed_vod_info_count = processed_vod_info_count; } diff --git a/backend/src/repository/bplustree.rs b/backend/src/repository/bplustree.rs index b8e27cc0d..06a845e1e 100644 --- a/backend/src/repository/bplustree.rs +++ b/backend/src/repository/bplustree.rs @@ -19,7 +19,7 @@ const FLAG_SIZE: usize = 1; fn is_multiple_of_block_size(file: &File) -> io::Result { let file_size = file.metadata()?.len(); // Get the file size in bytes - Ok(file_size % (BLOCK_SIZE as u64) == 0) // Check if file size is a multiple of BLOCK_SIZE + Ok(file_size.is_multiple_of(BLOCK_SIZE as u64)) // Check if file size is a multiple of BLOCK_SIZE } fn is_file_valid(file: File) -> io::Result { @@ -258,7 +258,7 @@ where if let Some(mut left_over) = remaining { // we calculate the needed blocks let left_over_len = left_over.len(); - if left_over_len % BLOCK_SIZE != 0 { + if !left_over_len.is_multiple_of(BLOCK_SIZE) { let padding = BLOCK_SIZE - (left_over_len % BLOCK_SIZE); left_over.extend(vec![0u8; padding]); } diff --git a/shared/src/foundation/filter.rs b/shared/src/foundation/filter.rs index 6f79d2039..69cc2f210 100644 --- a/shared/src/foundation/filter.rs +++ b/shared/src/foundation/filter.rs @@ -50,7 +50,7 @@ pub struct ValueProvider<'a> { } impl ValueProvider<'_> { - pub fn get(&self, field: &str) -> Option> { + pub fn get(&self, field: &str) -> Option> { self.pli.header.get_field(field) } } @@ -60,7 +60,7 @@ pub struct ValueAccessor<'a> { } impl ValueAccessor<'_> { - pub fn get(&self, field: &str) -> Option> { + pub fn get(&self, field: &str) -> Option> { self.pli.header.get_field(field) } diff --git a/shared/src/foundation/mapper.rs b/shared/src/foundation/mapper.rs index 920b45df8..054c10475 100644 --- a/shared/src/foundation/mapper.rs +++ b/shared/src/foundation/mapper.rs @@ -873,7 +873,7 @@ impl EvalResult { } } -fn concat_args(args: &Vec) -> Vec> { +fn concat_args(args: &Vec) -> Vec> { let mut result = vec![]; for arg in args { diff --git a/shared/src/model/config/hdhomerun.rs b/shared/src/model/config/hdhomerun.rs index e8260b11a..b983e99c8 100644 --- a/shared/src/model/config/hdhomerun.rs +++ b/shared/src/model/config/hdhomerun.rs @@ -1,9 +1,7 @@ use std::collections::HashSet; -use enum_iterator::Sequence; use log::warn; use crate::create_tuliprox_error_result; use crate::error::{TuliproxError, TuliproxErrorKind}; -use crate::model::TargetType; fn default_friendly_name() -> String { String::from("TuliproxTV") } fn default_manufacturer() -> String { String::from("Silicondust") } @@ -14,25 +12,25 @@ fn default_device_type() -> String { String::from("urn:schemas-upnp-org:device:M fn default_device_udn() -> String { String::from("uuid:12345678-90ab-cdef-1234-567890abcdef::urn:dial-multicast:com.silicondust.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, 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, Default, PartialEq)] #[serde(deny_unknown_fields)] diff --git a/shared/src/model/playlist.rs b/shared/src/model/playlist.rs index 96314bcd7..87ff4ebdd 100644 --- a/shared/src/model/playlist.rs +++ b/shared/src/model/playlist.rs @@ -118,7 +118,7 @@ impl Display for PlaylistItemType { } pub trait FieldGetAccessor { - fn get_field(&self, field: &str) -> Option>; + fn get_field(&self, field: &str) -> Option>; } pub trait FieldSetAccessor { fn set_field(&mut self, field: &str, value: &str) -> bool; @@ -229,7 +229,7 @@ macro_rules! to_m3u_resource_non_empty_fields { macro_rules! generate_field_accessor_impl_for_playlist_item_header { ($($prop:ident),*;) => { impl crate::model::FieldGetAccessor for crate::model::PlaylistItemHeader { - fn get_field(&self, field: &str) -> Option> { + fn get_field(&self, field: &str) -> Option> { let field = field.to_lowercase(); match field.as_str() { $( @@ -356,7 +356,7 @@ impl PlaylistEntry for M3uPlaylistItem { macro_rules! generate_field_accessor_impl_for_m3u_playlist_item { ($($prop:ident),*;) => { impl crate::model::FieldGetAccessor for M3uPlaylistItem { - fn get_field(&self, field: &str) -> Option> { + fn get_field(&self, field: &str) -> Option> { let field = field.to_lowercase(); match field.as_str() { $( @@ -464,7 +464,7 @@ pub fn get_backdrop_path_value<'a>(field: &'a str, value: Option<&'a Value>) -> macro_rules! generate_field_accessor_impl_for_xtream_playlist_item { ($($prop:ident),*;) => { impl crate::model::FieldGetAccessor for crate::model::XtreamPlaylistItem { - fn get_field(&self, field: &str) -> Option> { + fn get_field(&self, field: &str) -> Option> { let field = field.to_lowercase(); match field.as_str() { $( diff --git a/shared/src/utils/hash_utils.rs b/shared/src/utils/hash_utils.rs index 0720557df..e1daa2732 100644 --- a/shared/src/utils/hash_utils.rs +++ b/shared/src/utils/hash_utils.rs @@ -27,9 +27,8 @@ pub fn hex_encode(bytes: &[u8]) -> String { output }) } - pub fn hex_decode(hex: &str) -> Result, String> { - if hex.len() % 2 != 0 { + if !hex.len().is_multiple_of(2) { return Err("hex string must have even length".to_string()); } diff --git a/shared/src/utils/request.rs b/shared/src/utils/request.rs index 6be7d5fd5..95db9c8e1 100644 --- a/shared/src/utils/request.rs +++ b/shared/src/utils/request.rs @@ -7,7 +7,7 @@ use crate::utils::{CONSTANTS, DASH_EXT, DASH_EXT_FRAGMENT, DASH_EXT_QUERY, HLS_E pub fn set_sanitize_sensitive_info(value: bool) { CONSTANTS.sanitize.store(value, Ordering::SeqCst); } -pub fn sanitize_sensitive_info(query: &str) -> Cow { +pub fn sanitize_sensitive_info(query: &str) -> Cow<'_, str> { if !CONSTANTS.sanitize.load(Ordering::SeqCst) { return Cow::Borrowed(query); } diff --git a/shared/src/utils/string_utils.rs b/shared/src/utils/string_utils.rs index 4a366252f..e21624318 100644 --- a/shared/src/utils/string_utils.rs +++ b/shared/src/utils/string_utils.rs @@ -65,7 +65,7 @@ pub fn get_non_empty_str<'a>(first: &'a str, second: &'a str, third: &'a str) -> } } -pub fn trim_slash(s: &str) -> Cow { +pub fn trim_slash(s: &str) -> Cow<'_, str> { let trimmed = s.trim_matches('/'); if trimmed.len() == s.len() { Cow::Borrowed(s) // Keine Änderung → kein Clone @@ -74,7 +74,7 @@ pub fn trim_slash(s: &str) -> Cow { } } -pub fn trim_last_slash(s: &str) -> Cow { +pub fn trim_last_slash(s: &str) -> Cow<'_, str> { if s.ends_with('/') { if let Some(stripped) = s.strip_suffix('/') { return Cow::Owned(stripped.to_string())