fix: set input.options defaults if input.options is none and normalize missing /live prefix for hls streams (#602)

fix: set input.options defaults if input.options is none and normalize missing /live prefix for hls streams
fix: document defensive options guard in config input prepare
fix: group configinputoptions methods in one impl
fix: document fallback semantics for configinput _or flag helpers
fix: restore false fallback for missing options in flag checks
This commit is contained in:
knylbyte
2026-02-22 23:27:40 +01:00
committed by GitHub
parent 8dbcc57f35
commit dedf6bb63b
4 changed files with 74 additions and 17 deletions
+36 -1
View File
@@ -9,6 +9,7 @@ use crate::api::model::{ProviderAllocation, UserSession};
use crate::auth::Fingerprint;
use crate::model::{ConfigInput, InputSource};
use crate::model::{ConfigTarget, ProxyUserCredentials};
use crate::model::ConfigInputFlags;
use crate::processing::parser::hls::{
get_hls_session_token_and_url_from_token, rewrite_hls, RewriteHlsProps,
};
@@ -23,6 +24,7 @@ use shared::model::{PlaylistItemType, StreamChannel, TargetType, UserConnectionP
use shared::utils::{is_hls_url, replace_url_extension, sanitize_sensitive_info, Internable, CUSTOM_VIDEO_PREFIX, HLS_EXT};
use std::sync::Arc;
use crate::utils::request::is_file_url;
use url::Url;
const PLAYLIST_TEMPLATE: &str = r"#EXTM3U
#EXT-X-VERSION:3
@@ -49,6 +51,31 @@ fn hls_response(hls_content: String) -> impl IntoResponse + Send {
.body(hls_content))
}
fn normalize_xtream_live_hls_url(hls_url: &str, input: &ConfigInput) -> String {
if !input.input_type.is_xtream() || !input.has_flag(ConfigInputFlags::XtreamLiveStreamUsePrefix) {
return hls_url.to_string();
}
let (Some(username), Some(password)) = (input.username.as_deref(), input.password.as_deref()) else {
return hls_url.to_string();
};
let Ok(mut parsed) = Url::parse(hls_url) else {
return hls_url.to_string();
};
let Some(segments) = parsed.path_segments() else {
return hls_url.to_string();
};
let parts: Vec<&str> = segments.collect();
if parts.len() >= 3 && parts[0] == username && parts[1] == password {
parsed.set_path(&format!("/live/{}", parts.join("/")));
return parsed.to_string();
}
hls_url.to_string()
}
#[allow(clippy::too_many_arguments, clippy::too_many_lines)]
pub(in crate::api) async fn handle_hls_stream_request(
fingerprint: &Fingerprint,
@@ -65,7 +92,15 @@ pub(in crate::api) async fn handle_hls_stream_request(
return axum::http::StatusCode::BAD_REQUEST.into_response();
}
let url = replace_url_extension(hls_url, HLS_EXT);
let normalized_hls_url = normalize_xtream_live_hls_url(hls_url, input);
if normalized_hls_url != hls_url {
debug_if_enabled!(
"Normalized xtream hls url from {} to {}",
sanitize_sensitive_info(hls_url),
sanitize_sensitive_info(&normalized_hls_url)
);
}
let url = replace_url_extension(&normalized_hls_url, HLS_EXT);
let server_info = app_state.app_config.get_user_server_info(user);
let (request_url, session_token) = match user_session {
+35 -5
View File
@@ -12,7 +12,7 @@ use std::borrow::Cow;
use std::collections::HashMap;
use std::fmt;
use std::path::PathBuf;
use std::sync::Arc;
use std::sync::{Arc, LazyLock};
use url::Url;
create_bitset!(
@@ -87,6 +87,9 @@ impl ConfigInputOptions {
pub fn has_all_flags(&self, flags: ConfigInputFlagsSet) -> bool {
self.flags.contains_all(flags)
}
#[inline]
pub fn defaults() -> &'static Self { &DEFAULT_CONFIG_INPUT_OPTIONS }
}
impl From<&ConfigInputOptionsDto> for ConfigInputOptions {
fn from(dto: &ConfigInputOptionsDto) -> Self {
@@ -116,6 +119,9 @@ impl From<&ConfigInputOptionsDto> for ConfigInputOptions {
}
}
static DEFAULT_CONFIG_INPUT_OPTIONS: LazyLock<ConfigInputOptions> =
LazyLock::new(|| ConfigInputOptions::from(&ConfigInputOptionsDto::default()));
pub struct InputUserInfo {
pub base_url: String,
pub username: String,
@@ -236,35 +242,54 @@ pub struct ConfigInput {
impl ConfigInput {
#[inline]
pub fn has_flag(&self, flag: ConfigInputFlags) -> bool {
self.options.as_ref().is_some_and(|o| o.has_flag(flag))
self.has_flag_or(flag, false)
}
#[inline]
/// Returns `default` when `self.options` is `None`; unlike `has_flag`, which returns
/// `false` for missing options. For `ConfigInput::default()` without `prepare()`, use
/// this `_or` variant when an explicit fallback is required.
pub fn has_flag_or(&self, flag: ConfigInputFlags, default: bool) -> bool {
self.options.as_ref().map_or(default, |o| o.has_flag(flag))
}
#[inline]
pub fn has_any_flags(&self, flags: ConfigInputFlagsSet) -> bool {
self.options.as_ref().is_some_and(|o| o.has_any_flags(flags))
self.has_any_flags_or(flags, false)
}
#[inline]
/// Returns `default` when `self.options` is `None`; unlike `has_any_flags`, which returns
/// `false` for missing options. For `ConfigInput::default()` without `prepare()`, use
/// this `_or` variant when an explicit fallback is required.
pub fn has_any_flags_or(&self, flags: ConfigInputFlagsSet, default: bool) -> bool {
self.options.as_ref().map_or(default, |o| o.has_any_flags(flags))
}
#[inline]
pub fn has_all_flags(&self, flags: ConfigInputFlagsSet) -> bool {
self.options.as_ref().is_some_and(|o| o.has_all_flags(flags))
self.options
.as_ref()
.unwrap_or(ConfigInputOptions::defaults())
.has_all_flags(flags)
}
#[inline]
/// Returns `default` when `self.options` is `None`; unlike `has_all_flags`, which uses
/// `ConfigInputOptions::defaults()`. For `ConfigInput::default()` without `prepare()`,
/// prefer this `_or` variant when an explicit fallback is required.
pub fn has_all_flags_or(&self, flags: ConfigInputFlagsSet, default: bool) -> bool {
self.options.as_ref().map_or(default, |o| o.has_all_flags(flags))
}
pub fn prepare(&mut self, provider_configs: &[Arc<ConfigProvider>]) -> Result<Option<PathBuf>, TuliproxError> {
// Defensive fallback: From<&ConfigInputDto> for ConfigInput sets options, but ConfigInput can
// still be built via Default::default(), batch/internal/test paths, so prepare() normalizes
// missing options with ConfigInputOptions::defaults().
if self.options.is_none() {
self.options = Some(ConfigInputOptions::defaults().clone());
}
let mut used_provider_configs: Vec<Arc<ConfigProvider>> = vec![];
let batch_file_path = self.prepare_batch();
self.name = self.name.trim().intern();
@@ -487,6 +512,11 @@ impl ConfigInput {
macros::from_impl!(ConfigInput);
impl From<&ConfigInputDto> for ConfigInput {
fn from(dto: &ConfigInputDto) -> Self {
let options = dto
.options
.as_ref()
.map_or_else(|| ConfigInputOptions::defaults().clone(), ConfigInputOptions::from);
Self {
id: dto.id,
name: dto.name.clone(),
@@ -498,7 +528,7 @@ impl From<&ConfigInputDto> for ConfigInput {
password: dto.password.clone(),
persist: dto.persist.clone(),
enabled: dto.enabled,
options: dto.options.as_ref().map(ConfigInputOptions::from),
options: Some(options),
aliases: dto.aliases.as_ref().map(|list| list.iter().map(ConfigInputAlias::from).collect()),
priority: dto.priority,
max_connections: dto.max_connections,
+2 -9
View File
@@ -244,15 +244,8 @@ where
input.username.as_deref().unwrap_or("").to_string(),
input.password.as_deref().unwrap_or("").to_string(),
);
let (live_stream_use_prefix, live_stream_without_extension) = input.options.as_ref().map_or(
(false, false),
|o| {
(
o.has_flag(ConfigInputFlags::XtreamLiveStreamUsePrefix),
o.has_flag(ConfigInputFlags::XtreamLiveStreamWithoutExtension),
)
},
);
let live_stream_use_prefix = input.has_flag(ConfigInputFlags::XtreamLiveStreamUsePrefix);
let live_stream_without_extension = input.has_flag(ConfigInputFlags::XtreamLiveStreamWithoutExtension);
// Map categories for lookup
let group_map: IndexMap<u32, Arc<str>> = xtream_categories.iter().map(|c| (c.category_id, c.category_name.clone())).collect();
@@ -611,7 +611,7 @@ pub fn ParticleFlowBackground() -> Html {
}));
let theme_observer = win.document().and_then(|doc| {
MutationObserver::new(on_theme_change.as_ref().unchecked_ref()).ok().and_then(|observer| {
MutationObserver::new(on_theme_change.as_ref().unchecked_ref()).ok().inspect(|observer| {
let options = MutationObserverInit::new();
options.set_attributes(true);
let attribute_filter = Array::new();
@@ -625,7 +625,6 @@ pub fn ParticleFlowBackground() -> Html {
if let Some(body) = doc.body() {
let _ = observer.observe_with_options(&body, &options);
}
Some(observer)
})
});