mirror of
https://github.com/euzu/tuliprox.git
synced 2026-10-03 14:32:08 +02:00
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:
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user