diff --git a/src/api/endpoints/hls_api.rs b/src/api/endpoints/hls_api.rs index b50056738..71f026970 100644 --- a/src/api/endpoints/hls_api.rs +++ b/src/api/endpoints/hls_api.rs @@ -31,12 +31,12 @@ pub(in crate::api) async fn handle_hls_stream_request(app_state: &Arc, let url = replace_url_extension(hls_url, HLS_EXT); let server_info = app_state.config.get_user_server_info(user).await; match request::download_text_content(Arc::clone(&app_state.http_client), input, &url, None).await { - Ok(content) => { + Ok((content, response_url)) => { let hls_token = app_state.hls_cache.new_token(); let rewrite_hls_props = RewriteHlsProps { base_url: &server_info.get_base_url(), content: &content, - hls_url, + hls_url: response_url, virtual_id, token: hls_token, target_type, diff --git a/src/model/hls.rs b/src/model/hls.rs index b020f8cd9..6d27c3744 100644 --- a/src/model/hls.rs +++ b/src/model/hls.rs @@ -2,7 +2,7 @@ use std::collections::HashMap; use tokio::time::Instant; use crate::model::config::TargetType; -#[derive(Clone)] +#[derive(Clone, Debug)] pub struct HlsEntry { pub ts: Instant, pub token: u32, diff --git a/src/processing/parser/hls.rs b/src/processing/parser/hls.rs index 8512892b8..6329fdda2 100644 --- a/src/processing/parser/hls.rs +++ b/src/processing/parser/hls.rs @@ -4,7 +4,6 @@ use crate::model::hls::HlsEntry; use std::collections::HashMap; use std::str; use tokio::time::Instant; -use crate::utils::string_utils::replace_after_last_slash; pub const HLS_PREFIX: &str = "hls"; @@ -12,13 +11,35 @@ pub const HLS_PREFIX: &str = "hls"; pub struct RewriteHlsProps<'a> { pub base_url: &'a str, pub content: &'a str, - pub hls_url: &'a str, + pub hls_url: String, pub virtual_id: u32, pub token: u32, pub target_type: TargetType, pub input_id: u16 } + +fn rewrite_hls_url(input: &str, replacement: &str) -> String { + if replacement.starts_with('/') { + let parts = input.splitn(4, '/').collect::>(); + if parts.len() < 4 { + return replacement.to_string(); // wenn weniger als 3 Slashes → alles ersetzen + } + format!("{}/{}/{}{}", parts[0], parts[1], parts[2], replacement) + } else { + match input.rsplitn(2, '/').collect::>().as_slice() { + [_after, before] => format!("{}/{}", before, replacement), + [_only] => replacement.to_string(), + _ => input.to_string(), + } + } + // match input.rsplitn(2, '/').collect::>().as_slice() { + // [_after, before] => format!("{before}/{replacement}"), + // [_only] => replacement.to_string(), // if there is no slash, replace complete + // _ => input.to_string(), // fallback, should never happen + // } +} + pub fn rewrite_hls(user: &ProxyUserCredentials, props: &RewriteHlsProps ) -> (HlsEntry, String) { let username = &user.username; let password = &user.password; @@ -32,7 +53,7 @@ pub fn rewrite_hls(user: &ProxyUserCredentials, props: &RewriteHlsProps ) -> (Hl let url = if line.starts_with("http") { line.to_string() } else { - replace_after_last_slash(props.hls_url, line) + rewrite_hls_url(&props.hls_url, line) }; chunks.insert(chunk, url); result.push(format!("{}/{HLS_PREFIX}/{}/{username}/{password}/{}/{chunk}", props.base_url, props.token, props.virtual_id)); diff --git a/src/utils/network/request.rs b/src/utils/network/request.rs index fab3f66dc..95179408e 100644 --- a/src/utils/network/request.rs +++ b/src/utils/network/request.rs @@ -84,7 +84,7 @@ pub async fn get_input_text_content(client: Arc, input: &Config if url_str.parse::().is_ok() { match download_text_content(client, input, url_str, persist_filepath).await { - Ok(content) => Ok(content), + Ok((content, _response_url)) => Ok(content), Err(e) => { error!("cant download input url: {} => {}", sanitize_sensitive_info(url_str), sanitize_sensitive_info(e.to_string().as_str())); create_m3u_filter_error_result!(M3uFilterErrorKind::Notify, "Failed to download") @@ -230,13 +230,14 @@ async fn get_remote_content_as_file(client: Arc, input: &Config } } -async fn get_remote_content(client: Arc, input: &ConfigInput, url: &Url) -> Result { +async fn get_remote_content(client: Arc, input: &ConfigInput, url: &Url) -> Result<(String, String), Error> { let start_time = Instant::now(); let request = get_client_request(&client, input.method, Some(&input.headers), url, None); match request.send().await { Ok(response) => { let is_success = response.status().is_success(); if is_success { + let response_url = response.url().to_string(); let header_value = response.headers().get(CONTENT_ENCODING); let mut encoding = header_value.and_then(|encoding_header| encoding_header.to_str().map_or(None, |value| Some(value.to_string()))); match response.bytes().await { @@ -275,7 +276,7 @@ async fn get_remote_content(client: Arc, input: &ConfigInput, u match String::from_utf8(content_bytes) { Ok(decoded_content) => { debug_if_enabled!("Request took:{} {}", format_elapsed_time(start_time.elapsed().as_secs()), sanitize_sensitive_info(url.as_str())); - Ok(decoded_content) + Ok((decoded_content, response_url)) } Err(err) => { println!("{err:?}"); @@ -284,7 +285,7 @@ async fn get_remote_content(client: Arc, input: &ConfigInput, u } } else { debug_if_enabled!("Request took:{}, {}", format_elapsed_time(start_time.elapsed().as_secs()), sanitize_sensitive_info(url.as_str())); - Ok(decode_buffer) + Ok((decode_buffer, response_url)) } } Err(err) => Err(str_to_io_error(&format!("failed to read response {} {err}", sanitize_sensitive_info(url.as_str())))) @@ -323,19 +324,21 @@ pub async fn download_text_content_as_file(client: Arc, input: } -pub async fn download_text_content(client: Arc, input: &ConfigInput, url_str: &str, persist_filepath: Option) -> Result { +pub async fn download_text_content(client: Arc, input: &ConfigInput, url_str: &str, persist_filepath: Option) -> Result<(String, String), Error> { if let Ok(url) = url_str.parse::() { let result = if url.scheme() == "file" { - url.to_file_path().map_or_else(|()| Err(str_to_io_error(&format!("Unknown file {}", sanitize_sensitive_info(url_str)))), |file_path| get_local_file_content(&file_path)) + url.to_file_path().map_or_else(|()| Err(str_to_io_error(&format!("Unknown file {}", sanitize_sensitive_info(url_str)))), |file_path| + get_local_file_content(&file_path).map(|c| (c, url.to_string())) + ) } else { get_remote_content(client, input, &url).await }; match result { - Ok(content) => { + Ok((content, response_url)) => { if persist_filepath.is_some() { persist_file(persist_filepath, &content); } - Ok(content) + Ok((content, response_url)) } Err(err) => Err(err) } @@ -347,7 +350,7 @@ pub async fn download_text_content(client: Arc, input: &ConfigI async fn download_json_content(client: Arc, input: &ConfigInput, url: &str, persist_filepath: Option) -> Result { debug_if_enabled!("downloading json content from {}", sanitize_sensitive_info(url)); match download_text_content(client, input, url, persist_filepath).await { - Ok(content) => { + Ok((content, _response_url)) => { match serde_json::from_str::(&content) { Ok(value) => Ok(value), Err(err) => Err(str_to_io_error(&format!("Failed to parse json {err}"))) diff --git a/src/utils/network/xtream.rs b/src/utils/network/xtream.rs index 3beeaa355..cc6902c32 100644 --- a/src/utils/network/xtream.rs +++ b/src/utils/network/xtream.rs @@ -53,7 +53,10 @@ pub fn get_xtream_player_api_info_url(input: &ConfigInput, cluster: XtreamCluste pub async fn get_xtream_stream_info_content(client: Arc, info_url: &str, input: &ConfigInput) -> Result { - request::download_text_content(client, input, info_url, None).await + match request::download_text_content(client, input, info_url, None).await { + Ok((content, _response_url)) => Ok(content), + Err(err) => Err(err) + } } #[allow(clippy::too_many_arguments)] diff --git a/src/utils/string_utils.rs b/src/utils/string_utils.rs index f6129faac..76e964e76 100644 --- a/src/utils/string_utils.rs +++ b/src/utils/string_utils.rs @@ -41,17 +41,3 @@ pub fn get_trimmed_string(value: &Option) -> Option { } None } - -pub fn replace_after_last_slash(input: &str, replacement: &str) -> String { - match input.rsplitn(2, '/').collect::>().as_slice() { - [_after, before] => { - if replacement.starts_with("/") { - format!("{before}{replacement}") - } else { - format!("{before}/{replacement}") - } - }, - [_only] => replacement.to_string(), // if there is no slash, replace complete - _ => input.to_string(), // fallback, should never happen - } -} \ No newline at end of file