From 5842e00ed4ffa74935cd676f2cde7acf56d3a573 Mon Sep 17 00:00:00 2001 From: euzu Date: Fri, 28 Mar 2025 17:31:24 +0100 Subject: [PATCH] added unit to throttle --- CHANGELOG.md | 3 ++ frontend/public/assets/i18n/en_common.json | 5 ++- .../src/component/form-view/from-view.tsx | 6 ++- .../main-config-view/main-config-view.tsx | 6 +-- frontend/src/model/server-config.ts | 2 +- src/api/api_utils.rs | 13 ++---- src/model/api_proxy.rs | 1 - src/model/config.rs | 14 ++++-- src/utils/size_utils.rs | 44 +++++++++++++++++++ 9 files changed, 73 insertions(+), 21 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 15682a61f..c4b5c07bd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ # Changelog # 2.2.6 (2025-03-xx) +- !BREAKING CHANGE! bandwidth `throttle_kbps` attribute for `reverse_proxy.stream` in `config.yml` +is now `throttle` and supports units. Allowed units are `KB/s`,`MB/s`,`KiB/s`,`MiB/s`,`kbps`,`mbps`,`Mibps`. +Default unit is `kbps`. - Added rate limiting per IP. The burst_size defines the initial number of available connections, while period_millis specifies the interval at which one connection is replenished. If behind a proxy `x-forwarded-for`, `x-real-ip` or `forwarded` should be set as header. diff --git a/frontend/public/assets/i18n/en_common.json b/frontend/public/assets/i18n/en_common.json index bfc62ecc9..2cdd305f5 100644 --- a/frontend/public/assets/i18n/en_common.json +++ b/frontend/public/assets/i18n/en_common.json @@ -29,7 +29,7 @@ "ACTIVE_CLIENTS": "Active Clients", "RESOURCE_REWRITE_DISABLE": "Resource Rewrite disable", "RECONNECT": "Reconnect", - "THROTTLE_KBPS": "Throttle kbps", + "THROTTLE": "Throttle", "GRACE_PERIOD": "Grace period ms", "BUFFER_ENABLED": "Buffer Enabled", "BUFFER_SIZE": "Buffer Size", @@ -150,7 +150,8 @@ "ACTIVE_CLIENTS": "Activates active clients info log" }, "PROXY": { - "RESOURCE_REWRITE_DISABLE": "If activated the URL rewrite of resources in reverse proxy mode is disabled" + "RESOURCE_REWRITE_DISABLE": "If activated the URL rewrite of resources in reverse proxy mode is disabled", + "THROTTLE": "Supported units are KB/s,MB/s,KiB/s,MiB/s,kbps,mbps,Mibps" } } } diff --git a/frontend/src/component/form-view/from-view.tsx b/frontend/src/component/form-view/from-view.tsx index b0ad37ae3..0068d1c4d 100644 --- a/frontend/src/component/form-view/from-view.tsx +++ b/frontend/src/component/form-view/from-view.tsx @@ -68,7 +68,11 @@ export default function FormView(props: FormViewProps) { for (const fieldDef of fields) { if (fieldDef.name === field) { if (fieldDef.fieldType === FormFieldType.NUMBER) { - value = parseInt(value); + if (value.trim().isEmpty()) { + value = undefined; + } else { + value = parseInt(value); + } } } } diff --git a/frontend/src/component/main-config-view/main-config-view.tsx b/frontend/src/component/main-config-view/main-config-view.tsx index b945f2d3d..969957d74 100644 --- a/frontend/src/component/main-config-view/main-config-view.tsx +++ b/frontend/src/component/main-config-view/main-config-view.tsx @@ -60,8 +60,8 @@ const CONFIG_REVERSE_PROXY_FIELDS = [ const CONFIG_REVERSE_PROXY_STREAM_FIELDS = [ {name: 'retry', label: 'LABEL.RECONNECT', fieldType: FormFieldType.CHECK}, - {name: 'throttle_kbps', label: 'LABEL.THROTTLE_KBPS', fieldType: FormFieldType.TEXT}, - {name: 'grace_period_millis', label: 'LABEL.GRACE_PERIOD', fieldType: FormFieldType.TEXT}, + {name: 'throttle', label: 'LABEL.THROTTLE', hint: 'HINT.CONFIG.PROXY.THROTTLE', fieldType: FormFieldType.TEXT}, + {name: 'grace_period_millis', label: 'LABEL.GRACE_PERIOD', fieldType: FormFieldType.NUMBER}, ]; const CONFIG_REVERSE_PROXY_STREAM_BUFFER_FIELDS = [ @@ -195,7 +195,7 @@ export default function MainConfigView(props: MainConfigViewProps) { let cfgReverseProxyStreamBuffer: StreamConfig = { retry: reverseProxyStreamConfig.retry, - throttle_kpbs: reverseProxyStreamConfig.throttle_kpbs, + throttle: reverseProxyStreamConfig.throttle, grace_period_millis: reverseProxyStreamConfig.grace_period_millis, buffer: { enabled: reverseProxyStreamBufferConfig.enabled, diff --git a/frontend/src/model/server-config.ts b/frontend/src/model/server-config.ts index f686118f7..fe005f183 100644 --- a/frontend/src/model/server-config.ts +++ b/frontend/src/model/server-config.ts @@ -193,7 +193,7 @@ export interface StreamBufferConfig { export interface StreamConfig { retry: boolean, buffer?: StreamBufferConfig, - throttle_kpbs?: number, + throttle?: number, grace_period_millis?: number, } diff --git a/src/api/api_utils.rs b/src/api/api_utils.rs index a9393cf41..2eb0cdf9c 100644 --- a/src/api/api_utils.rs +++ b/src/api/api_utils.rs @@ -351,13 +351,8 @@ pub async fn stream_response(app_state: &AppState, let throttle_kbps = get_stream_throttle(app_state); - let body_stream = if throttle_kbps > 0 { - match item_type { - PlaylistItemType::Video | PlaylistItemType::Series | PlaylistItemType::SeriesInfo => { - axum::body::Body::from_stream(ThrottledStream::new(stream.boxed(), throttle_kbps as usize)) - } - _ => axum::body::Body::from_stream(stream) - } + let body_stream = if throttle_kbps > 0 && matches!(item_type, PlaylistItemType::Video | PlaylistItemType::Series | PlaylistItemType::SeriesInfo) { + axum::body::Body::from_stream(ThrottledStream::new(stream.boxed(), throttle_kbps as usize)) } else { axum::body::Body::from_stream(stream) }; @@ -372,12 +367,12 @@ pub async fn stream_response(app_state: &AppState, axum::http::StatusCode::BAD_REQUEST.into_response() } -fn get_stream_throttle(app_state: &AppState) -> u32 { +fn get_stream_throttle(app_state: &AppState) -> u64 { app_state.config .reverse_proxy .as_ref() .and_then(|reverse_proxy| reverse_proxy.stream.as_ref()) - .map(|stream| stream.throttle_kbps).map_or(0, |t| t.unwrap_or_default()) + .map(|stream| stream.throttle_kbps).unwrap_or_default() } async fn shared_stream_response(app_state: &AppState, stream_url: &str, user: &ProxyUserCredentials) -> Option { diff --git a/src/model/api_proxy.rs b/src/model/api_proxy.rs index b6d47f21a..67b170655 100644 --- a/src/model/api_proxy.rs +++ b/src/model/api_proxy.rs @@ -201,7 +201,6 @@ impl ProxyUserCredentials { pub async fn connection_permission(&self, app_state: &AppState) -> UserConnectionPermission { if app_state.config.user_access_control { - // we allow requests with max connection reached, but we should block streaming after grace period if let Some(&max_connections) = self.max_connections.as_ref() { if max_connections > 0 { diff --git a/src/model/config.rs b/src/model/config.rs index b9f00653a..02ca5c157 100644 --- a/src/model/config.rs +++ b/src/model/config.rs @@ -26,7 +26,7 @@ use crate::utils::default_utils::{default_as_default, default_as_true, default_a use crate::utils::file::file_lock_manager::FileLockManager; use crate::utils::file::file_utils; use crate::utils::file::file_utils::file_reader; -use crate::utils::size_utils::parse_size_base_2; +use crate::utils::size_utils::{parse_size_base_2, parse_to_kbps}; use crate::utils::sys_utils::exit; const DEFAULT_USER_AGENT: &str = "Mozilla/5.0 (AppleTV; U; CPU OS 14_2 like Mac OS X; en-us) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.0.1 Safari/605.1.15"; @@ -1175,18 +1175,24 @@ pub struct StreamConfig { #[serde(default, skip_serializing_if = "Option::is_none")] pub buffer: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub throttle_kbps: Option, + pub throttle: Option, #[serde(default = "default_grace_period_millis")] pub grace_period_millis: u64, #[serde(default)] pub forced_retry_interval_secs: u32, + #[serde(default, skip)] + pub throttle_kbps: u64, } impl StreamConfig { - fn prepare(&mut self) { + fn prepare(&mut self) -> Result<(), M3uFilterError> { if let Some(buffer) = self.buffer.as_mut() { buffer.prepare(); } + if let Some (throttle) = &self.throttle { + self.throttle_kbps = parse_to_kbps(throttle).map_err(|err| M3uFilterError::new(M3uFilterErrorKind::Info, err))?; + } + Ok(()) } } @@ -1227,7 +1233,7 @@ pub struct ReverseProxyConfig { impl ReverseProxyConfig { fn prepare(&mut self, working_dir: &str) -> Result<(), M3uFilterError> { if let Some(stream) = self.stream.as_mut() { - stream.prepare(); + stream.prepare()?; } if let Some(cache) = self.cache.as_mut() { if cache.enabled && self.resource_rewrite_disabled { diff --git a/src/utils/size_utils.rs b/src/utils/size_utils.rs index fff9826e9..fd67cfd9b 100644 --- a/src/utils/size_utils.rs +++ b/src/utils/size_utils.rs @@ -63,4 +63,48 @@ pub fn human_readable_byte_size(bytes: u64) -> String { } format!("{size:.2} {unit}") +} + +pub fn parse_to_kbps(input: &str) -> Result { + // Define unit conversion factors (in bits per second) + let units: &[(&str, u64)] = &[ + ("KB/s", 8), // Kilobytes per second to kbps + ("MB/s", 8000), // Megabytes per second to kbps + ("KiB/s", 8 * 1024 / 1000), // Kibibytes per second to kbps + ("MiB/s", 8 * 1024), // Mebibytes per second to kbps + ("kbps", 1), // Kilobits per second (already in kbps) + ("Kbps", 1), // Kilobits per second (already in kbps) + ("mbps", 1000), // Megabits per second to kbps + ("Mbps", 1000), // Megabits per second to kbps + ("Mibps", 1024), // Mebibits per second to kbps + ]; + + let speed_str = input.trim(); + for (unit, multiplier) in units { + if speed_str.ends_with(unit) { + let number_part = speed_str[..speed_str.len() - unit.len()].trim(); + let value = u64::from_str(number_part).map_err(|_| format!("Invalid speed: {number_part}"))?; + return value.checked_mul(*multiplier).ok_or_else(|| format!("Speed too large: {speed_str}")); + } + } + + u64::from_str(&speed_str).map_err(|_| format!("Invalid speed: {speed_str}, supported units are {}", units.iter().map(|p| p.0).collect::>().join(","))) +} + +#[cfg(test)] +mod tests { + use crate::utils::size_utils::parse_to_kbps; + + #[test] + fn test_parse_kpbs() { + assert_eq!(parse_to_kbps("1KB/s").unwrap(), 8); + assert_eq!(parse_to_kbps("1MB/s").unwrap(), 8000); + assert_eq!(parse_to_kbps("1KiB/s").unwrap(), 8 * 1024 / 1000); + assert_eq!(parse_to_kbps("1MiB/s").unwrap(), 8 * 1024); + assert_eq!(parse_to_kbps("1kbps").unwrap(), 1); + assert_eq!(parse_to_kbps("1mbps").unwrap(), 1000); + assert_eq!(parse_to_kbps("1Kbps").unwrap(), 1); + assert_eq!(parse_to_kbps("1Mbps").unwrap(), 1000); + assert_eq!(parse_to_kbps("1Mibps").unwrap(), 1024); + } } \ No newline at end of file