added unit to throttle

This commit is contained in:
euzu
2025-03-28 17:31:24 +01:00
parent 5f9576e2a0
commit 5842e00ed4
9 changed files with 73 additions and 21 deletions
+3
View File
@@ -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.
+3 -2
View File
@@ -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"
}
}
}
@@ -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);
}
}
}
}
@@ -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,
+1 -1
View File
@@ -193,7 +193,7 @@ export interface StreamBufferConfig {
export interface StreamConfig {
retry: boolean,
buffer?: StreamBufferConfig,
throttle_kpbs?: number,
throttle?: number,
grace_period_millis?: number,
}
+4 -9
View File
@@ -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<impl IntoResponse> {
-1
View File
@@ -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 {
+10 -4
View File
@@ -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<StreamBufferConfig>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub throttle_kbps: Option<u32>,
pub throttle: Option<String>,
#[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 {
+44
View File
@@ -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<u64, String> {
// 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::<Vec<_>>().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);
}
}