From 50bed33aaa34e68e3d5d88c18c6542f1df80b366 Mon Sep 17 00:00:00 2001 From: euzu Date: Thu, 30 Jan 2025 12:30:04 +0100 Subject: [PATCH] Fixed iptv player using live tv stream without `/live/` context. --- CHANGELOG.md | 3 ++- README.md | 2 +- bin/build_github_docker_beta.sh | 2 ++ config/api-proxy.yml | 4 +-- src/api/api_utils.rs | 4 +-- src/api/model/provider_stream_factory.rs | 11 ++++++--- src/api/model/xtream.rs | 2 +- src/api/xtream_api.rs | 6 +++-- src/utils/mod.rs | 15 ++++++++++++ src/utils/request_utils.rs | 31 +++++++++++++++++++++++- 10 files changed, 67 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9d2d307f7..116a87b79 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,9 +3,10 @@ - !BREAKING CHANGE! unique `input` `name` is now mandatory, because rearranging the `source.yml` could lead to wrong results without a playlist update. - !BREAKING_CHANGE! `log_sanitize_sensitive_info` is now under `log` section as `sanitize_sensitive_info` - !BREAKING_CHANGE! uuid generation for entries changed to `input.name` + `stream_id`. Virtual id mapping changed. The new Virtual id is not a sequence anymore. -- Added `log_level`to `log` config. Priority: CLI-Argument, Env-Var, Config, Default(`info`) - Added Active clients count (for reverse proxy mode users) which is now displayed in `/status` and can be logged with setting `active_clients: true` under `log`section in `config.yml` +- Fixed iptv player using live tv stream without `/live/` context. +- Added `log_level`to `log` config. Priority: CLI-Argument, Env-Var, Config, Default(`info`) ```yaml log: sanitize_sensitive_info: false diff --git a/README.md b/README.md index fc4c567f4..5d6492226 100644 --- a/README.md +++ b/README.md @@ -211,7 +211,7 @@ If you set it `true` `cache` is disabled! Because the cache cant work without re ```yaml reverse_proxy: - resource_rewrite_disabled: false, + resource_rewrite_disabled: false stream: retry: true buffer: diff --git a/bin/build_github_docker_beta.sh b/bin/build_github_docker_beta.sh index 4b73c8660..4373b094b 100755 --- a/bin/build_github_docker_beta.sh +++ b/bin/build_github_docker_beta.sh @@ -5,6 +5,8 @@ source "${HOME}/.ghcr.io" TARGET=x86_64-unknown-linux-musl +(bin/build_fe.sh && bin/build_lin_static.sh) || exit 1; + # Check if the binary exists if [ ! -f "./target/${TARGET}/release/m3u-filter" ]; then echo "Error: Static binary '../target/${TARGET}/release/m3u-filter' does not exist." diff --git a/config/api-proxy.yml b/config/api-proxy.yml index d9be74a23..1103ba05c 100644 --- a/config/api-proxy.yml +++ b/config/api-proxy.yml @@ -10,8 +10,8 @@ server: - name: extern protocol: https host: m3u-filter.mydns.com - http_port: 8900 - https_port: + http_port: + https_port: 8443 rtmp_port: timezone: Europe/Paris message: Welcome to m3u-filter diff --git a/src/api/api_utils.rs b/src/api/api_utils.rs index dae5bb51e..731afe457 100644 --- a/src/api/api_utils.rs +++ b/src/api/api_utils.rs @@ -7,7 +7,7 @@ use crate::api::model::provider_stream_factory::BufferStreamOptions; use crate::api::model::request::UserApiRequest; use crate::api::model::shared_stream::SharedStream; use crate::api::model::stream_error::StreamError; -use crate::debug_if_enabled; +use crate::{debug_if_enabled, trace_if_enabled}; use crate::model::api_proxy::ProxyUserCredentials; use crate::model::config::{ConfigInput, ConfigTarget}; use crate::model::playlist::PlaylistItemType; @@ -212,7 +212,7 @@ pub async fn resource_response(app_state: &AppState, resource_url: &str, req: &H } } } - debug_if_enabled!("Try to fetch resource {}", sanitize_sensitive_info(resource_url)); + trace_if_enabled!("Try to fetch resource {}", sanitize_sensitive_info(resource_url)); if let Ok(url) = Url::parse(resource_url) { let client = request_utils::get_client_request(&app_state.http_client, input.map(|i| &i.headers), &url, Some(&req_headers)); match client.send().await { diff --git a/src/api/model/provider_stream_factory.rs b/src/api/model/provider_stream_factory.rs index 00e4d6d2e..f3a3f0d67 100644 --- a/src/api/model/provider_stream_factory.rs +++ b/src/api/model/provider_stream_factory.rs @@ -7,7 +7,7 @@ use crate::debug_if_enabled; use crate::model::config::ConfigInput; use crate::model::playlist::PlaylistItemType; use crate::utils::atomic_once_flag::AtomicOnceFlag; -use crate::utils::request_utils::{get_request_headers, sanitize_sensitive_info}; +use crate::utils::request_utils::{classify_content_type, get_request_headers, sanitize_sensitive_info, MimeCategory}; use actix_web::HttpRequest; use bytes::Bytes; use futures::stream::{self, BoxStream}; @@ -307,7 +307,6 @@ fn create_provider_stream_options(stream_url: &Url, } } - pub async fn create_provider_stream(client: Arc, stream_url: &Url, req: &HttpRequest, @@ -326,8 +325,14 @@ pub async fn create_provider_stream(client: Arc, match get_initial_stream(Arc::clone(&client), &stream_options).await { Some((init_stream, info)) => { + let is_video_stream = if let Some((headers, _)) = &info { + classify_content_type(headers) == MimeCategory::Video + } else { + true // don't know what it is but lets assume it is + }; + let continue_signal = stream_options.get_continue_flag_clone(); - if stream_options.should_reconnect() { + if is_video_stream && stream_options.should_reconnect() { let client_signal = Arc::clone(&continue_signal); let stream_options_provider = stream_options.clone(); let unfold: ResponseStream = stream::unfold((), move |()| { diff --git a/src/api/model/xtream.rs b/src/api/model/xtream.rs index a8addd29d..b5bd03561 100644 --- a/src/api/model/xtream.rs +++ b/src/api/model/xtream.rs @@ -47,7 +47,7 @@ impl XtreamAuthorizationResponse { Self { user_info: XtreamUserInfo { active_cons: "0".to_string(), - allowed_output_formats: Vec::from(["ts".to_string(), "m3u8".to_string(), "rtmp".to_string()]), + allowed_output_formats: Vec::from(["ts".to_string()]), auth: 1, created_at: (now - Duration::days(365)).timestamp(), // fake exp_date: (now + Duration::days(365)).timestamp(), // fake diff --git a/src/api/xtream_api.rs b/src/api/xtream_api.rs index 9a1685afe..7e0653c05 100644 --- a/src/api/xtream_api.rs +++ b/src/api/xtream_api.rs @@ -82,6 +82,7 @@ macro_rules! try_result_bad_request { }; } +#[derive(Debug)] enum XtreamApiStreamContext { LiveAlt, Live, @@ -91,7 +92,6 @@ enum XtreamApiStreamContext { } impl XtreamApiStreamContext { - const LIVE_ALT: &'static str = ""; const LIVE: &'static str = "live"; const MOVIE: &'static str = "movie"; const SERIES: &'static str = "series"; @@ -101,7 +101,7 @@ impl XtreamApiStreamContext { impl Display for XtreamApiStreamContext { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { write!(f, "{}", match self { - Self::LiveAlt => Self::LIVE_ALT, + Self::LiveAlt => Self::LIVE, Self::Live => Self::LIVE, Self::Movie => Self::MOVIE, Self::Series => Self::SERIES, @@ -110,6 +110,7 @@ impl Display for XtreamApiStreamContext { } } +#[derive(Debug)] struct XtreamApiStreamRequest<'a> { context: XtreamApiStreamContext, username: &'a str, @@ -217,6 +218,7 @@ async fn xtream_player_api_stream( stream_req.context.to_string().as_str(), &query_path, pli.url.as_str()), true, format!("Cant find stream url for target {target_name}, context {}, stream_id {virtual_id}", stream_req.context)); + debug_if_enabled!("Streaming stream request from {}", sanitize_sensitive_info(&stream_url)); stream_response(app_state, &stream_url, req, Some(input), pli.item_type, target).await } diff --git a/src/utils/mod.rs b/src/utils/mod.rs index 496012403..b5e204c9d 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -33,3 +33,18 @@ macro_rules! debug_if_enabled { } }; } + +#[macro_export] +macro_rules! trace_if_enabled { + ($fmt:expr, $( $args:expr ),*) => { + if log::log_enabled!(log::Level::Trace) { + log::log!(log::Level::Trace, $fmt, $($args),*); + } + }; + + ($txt:expr) => { + if log::log_enabled!(log::Level::Trace) { + log::log!(Level::Trace, $txt); + } + }; +} diff --git a/src/utils/request_utils.rs b/src/utils/request_utils.rs index a6406bbf9..86c090b8c 100644 --- a/src/utils/request_utils.rs +++ b/src/utils/request_utils.rs @@ -397,6 +397,35 @@ pub fn extract_extension_from_url(url: &str) -> Option<&str> { None } +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum MimeCategory { + Unknown, + Video, + M3U8, + Image, + Json, + Xml, + Text, + Unclassified, +} + +pub fn classify_content_type(headers: &[(String, String)]) -> MimeCategory { + headers.iter() + .find_map(|(k, v)| { + (k == actix_web::http::header::CONTENT_TYPE.as_str()).then_some(v) + }) + .map(|v| match v.as_str() { + v if v.starts_with("video/") || v == "application/octet-stream" => MimeCategory::Video, + "application/vnd.apple.mpegurl" | "application/x-mpegURL" => MimeCategory::M3U8, + v if v.starts_with("image/") => MimeCategory::Image, + v if v.starts_with("application/json") || v.ends_with("+json") => MimeCategory::Json, + v if v.starts_with("application/xml") || v.ends_with("+xml") || v == "text/xml" => MimeCategory::Xml, + v if v.starts_with("text/") => MimeCategory::Text, + _ => MimeCategory::Unclassified, + }) + .unwrap_or(MimeCategory::Unknown) +} + #[cfg(test)] mod tests { use crate::utils::request_utils::{sanitize_sensitive_info}; @@ -409,4 +438,4 @@ mod tests { println!("{masked}") } -} \ No newline at end of file +}