mirror of
https://github.com/euzu/tuliprox.git
synced 2026-10-29 10:12:22 +01:00
M3u catchup fix (#794)
* **New Features** * Catch-up streams can now auto-detect whether to serve direct video or an HLS playlist. * Added archive-aware HLS session/identity handling and improved upstream user-agent propagation for manifests. * Enhanced M3U timeshift/archiving, including nested handling and native Flussonic archive support; Short EPG can advertise archive availability. * **Bug Fixes** * Provider session headers now persist across retries and deferred-open scenarios. * Legacy HLS routing is now cache-aware; seek detection improvements. * Stalker errors now better redact sensitive URL details. * Fix: Preserve VOD client range requests * **Tests** * Expanded coverage for probing, archive/timeshift resolution, header overlay rules, and related routing.
This commit is contained in:
@@ -2,7 +2,7 @@ use crate::model::ProxyUserCredentials;
|
||||
use shared::concat_string;
|
||||
use shared::{
|
||||
utils::{deobfuscate_text, extract_extension_from_url, obfuscate_text, CONSTANTS},
|
||||
defaults::{HLS_PREFIX}
|
||||
defaults::{HLS_EXT, HLS_PREFIX}
|
||||
};
|
||||
use std::borrow::Cow;
|
||||
use std::str;
|
||||
@@ -63,10 +63,71 @@ pub struct RewriteHlsProps<'a> {
|
||||
pub user_token: Option<&'a str>,
|
||||
}
|
||||
|
||||
fn is_direct_archive_start_query_key(key: &str) -> bool {
|
||||
key.eq_ignore_ascii_case("utc") || key.eq_ignore_ascii_case("utcstart")
|
||||
}
|
||||
|
||||
fn is_contextual_archive_start_query_key(key: &str) -> bool {
|
||||
key.eq_ignore_ascii_case("start") || key.eq_ignore_ascii_case("timestamp")
|
||||
}
|
||||
|
||||
fn is_archive_start_context_query_key(key: &str) -> bool {
|
||||
key.eq_ignore_ascii_case("end")
|
||||
|| key.eq_ignore_ascii_case("duration")
|
||||
|| key.eq_ignore_ascii_case("lutc")
|
||||
|| key.eq_ignore_ascii_case("offset")
|
||||
}
|
||||
|
||||
fn has_archive_start_query(url: &Url) -> bool {
|
||||
let has_context = url.query_pairs().any(|(key, _)| is_archive_start_context_query_key(&key));
|
||||
url.query_pairs().any(|(key, _)| {
|
||||
is_direct_archive_start_query_key(&key) || (has_context && is_contextual_archive_start_query_key(&key))
|
||||
})
|
||||
}
|
||||
|
||||
fn preserve_archive_start_query(base: &Url, mut target: Url) -> Url {
|
||||
let has_context = base.query_pairs().any(|(key, _)| is_archive_start_context_query_key(&key));
|
||||
if !has_archive_start_query(base) {
|
||||
return target;
|
||||
}
|
||||
|
||||
for (key, value) in base.query_pairs() {
|
||||
let is_start = is_direct_archive_start_query_key(&key)
|
||||
|| (has_context && is_contextual_archive_start_query_key(&key));
|
||||
let already_present = if is_start {
|
||||
has_archive_start_query(&target)
|
||||
} else {
|
||||
target.query_pairs().any(|(target_key, _)| target_key.eq_ignore_ascii_case(&key))
|
||||
};
|
||||
if (is_start || is_archive_start_context_query_key(&key)) && !already_present {
|
||||
target.query_pairs_mut().append_pair(&key, &value);
|
||||
}
|
||||
}
|
||||
target
|
||||
}
|
||||
|
||||
fn preserve_token_query(base: &Url, mut target: Url) -> Url {
|
||||
let target_has_token = target.query_pairs().any(|(key, _)| key.eq_ignore_ascii_case("token"));
|
||||
if target_has_token {
|
||||
return target;
|
||||
}
|
||||
|
||||
if let Some((key, value)) = base.query_pairs().find(|(key, _)| key.eq_ignore_ascii_case("token")) {
|
||||
target.query_pairs_mut().append_pair(&key, &value);
|
||||
}
|
||||
target
|
||||
}
|
||||
|
||||
fn has_same_origin(left: &Url, right: &Url) -> bool {
|
||||
matches!(left.scheme(), "ftp" | "http" | "https" | "ws" | "wss")
|
||||
&& left.scheme() == right.scheme()
|
||||
&& left.host() == right.host()
|
||||
&& left.port_or_known_default() == right.port_or_known_default()
|
||||
}
|
||||
|
||||
/// Rewrites an HLS URI relative to a base playlist URL.
|
||||
/// Absolute URIs are returned unchanged.
|
||||
pub fn rewrite_hls_url<'a>(base: &'a str, reference: &'a str) -> Cow<'a, str> {
|
||||
// absolute URI -> passthrough
|
||||
if Url::parse(reference).is_ok() {
|
||||
return Cow::Borrowed(reference);
|
||||
}
|
||||
@@ -75,7 +136,23 @@ pub fn rewrite_hls_url<'a>(base: &'a str, reference: &'a str) -> Cow<'a, str> {
|
||||
return Cow::Borrowed(reference);
|
||||
};
|
||||
|
||||
base_url.join(reference).map_or_else(|_| Cow::Borrowed(reference), |u| Cow::Owned(u.to_string()))
|
||||
base_url.join(reference).map_or_else(
|
||||
|_| Cow::Borrowed(reference),
|
||||
|mut target| {
|
||||
let is_same_origin = has_same_origin(&target, &base_url);
|
||||
if is_same_origin {
|
||||
target = preserve_token_query(&base_url, target);
|
||||
}
|
||||
let is_same_origin_child_playlist = is_same_origin
|
||||
&& extract_extension_from_url(target.as_str())
|
||||
.is_some_and(|extension| extension.eq_ignore_ascii_case(HLS_EXT));
|
||||
Cow::Owned(if is_same_origin_child_playlist {
|
||||
preserve_archive_start_query(&base_url, target).into()
|
||||
} else {
|
||||
target.into()
|
||||
})
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fn rewrite_uri_attrib<'a>(line: &'a str, props: &RewriteHlsProps, user: &ProxyUserCredentials) -> Cow<'a, str> {
|
||||
@@ -193,6 +270,113 @@ mod test {
|
||||
assert_eq!(out, "http://example.com/hls/seg001.ts");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rewrite_relative_hls_resources_inherit_token() {
|
||||
let base = "https://cdn.example/hls/channel/timeshift_abs-1785136500.m3u8?token=secret";
|
||||
|
||||
for (reference, expected) in [
|
||||
(
|
||||
"tracks-v1a1/timeshift_abs-1785136500.m3u8",
|
||||
"https://cdn.example/hls/channel/tracks-v1a1/timeshift_abs-1785136500.m3u8?token=secret",
|
||||
),
|
||||
(
|
||||
"tracks-v1a1/dvr-2026/07/27/12/00/00-06000.ts",
|
||||
"https://cdn.example/hls/channel/tracks-v1a1/dvr-2026/07/27/12/00/00-06000.ts?token=secret",
|
||||
),
|
||||
("key.bin?version=2", "https://cdn.example/hls/channel/key.bin?version=2&token=secret"),
|
||||
("init.mp4", "https://cdn.example/hls/channel/init.mp4?token=secret"),
|
||||
] {
|
||||
assert_eq!(rewrite_hls_url(base, reference), expected);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rewrite_relative_hls_resource_keeps_its_own_token() {
|
||||
let base = "https://cdn.example/hls/channel/index.m3u8?TOKEN=base";
|
||||
|
||||
assert_eq!(
|
||||
rewrite_hls_url(base, "child.m3u8?token=child"),
|
||||
"https://cdn.example/hls/channel/child.m3u8?token=child"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rewrite_does_not_leak_token_to_cross_origin_resource() {
|
||||
let base = "https://cdn.example/hls/channel/index.m3u8?token=secret";
|
||||
|
||||
assert_eq!(rewrite_hls_url(base, "https://media.example/child.m3u8"), "https://media.example/child.m3u8");
|
||||
assert_eq!(rewrite_hls_url(base, "//media.example/child.m3u8"), "https://media.example/child.m3u8");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rewrite_relative_variant_preserves_archive_start_query() {
|
||||
let base = "https://cdn.example/hls/channel/index.m3u8?offset=-10752&utcstart=1785072000&useseq=t";
|
||||
let uri = "variant/playlist.m3u8?offset=-10752&useseq=t";
|
||||
|
||||
let out = rewrite_hls_url(base, uri);
|
||||
assert_eq!(
|
||||
out,
|
||||
"https://cdn.example/hls/channel/variant/playlist.m3u8?offset=-10752&useseq=t&utcstart=1785072000"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rewrite_relative_variant_preserves_all_archive_context_queries() {
|
||||
let base = "https://cdn.example/hls/channel/index.m3u8?utcstart=1785072000&offset=-3600&end=1785075600&duration=3600";
|
||||
let uri = "variant/playlist.m3u8?offset=-1800";
|
||||
|
||||
let out = rewrite_hls_url(base, uri);
|
||||
assert_eq!(
|
||||
out,
|
||||
"https://cdn.example/hls/channel/variant/playlist.m3u8?offset=-1800&utcstart=1785072000&end=1785075600&duration=3600"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rewrite_keeps_child_archive_start_query() {
|
||||
let base = "https://cdn.example/hls/channel/index.m3u8?utcstart=1785072000";
|
||||
let uri = "variant/playlist.m3u8?utc=1785071000";
|
||||
|
||||
let out = rewrite_hls_url(base, uri);
|
||||
assert_eq!(out, "https://cdn.example/hls/channel/variant/playlist.m3u8?utc=1785071000");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rewrite_does_not_propagate_plain_start_query() {
|
||||
let base = "https://cdn.example/hls/channel/index.m3u8?start=1785072000";
|
||||
let uri = "variant/playlist.m3u8";
|
||||
|
||||
let out = rewrite_hls_url(base, uri);
|
||||
assert_eq!(out, "https://cdn.example/hls/channel/variant/playlist.m3u8");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rewrite_archive_playlist_does_not_modify_signed_media_urls() {
|
||||
let base = "https://cdn.example/hls/channel/index.m3u8?utcstart=1785072000&offset=-3600";
|
||||
|
||||
assert_eq!(
|
||||
rewrite_hls_url(base, "segment.ts?sig=abc"),
|
||||
"https://cdn.example/hls/channel/segment.ts?sig=abc"
|
||||
);
|
||||
assert_eq!(
|
||||
rewrite_hls_url(base, "key.bin?sig=def"),
|
||||
"https://cdn.example/hls/channel/key.bin?sig=def"
|
||||
);
|
||||
assert_eq!(
|
||||
rewrite_hls_url(base, "init.mp4?sig=ghi"),
|
||||
"https://cdn.example/hls/channel/init.mp4?sig=ghi"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rewrite_absolute_same_origin_url_remains_exact_passthrough() {
|
||||
let base = "https://cdn.example/hls/channel/index.m3u8?utcstart=1785072000&offset=-3600";
|
||||
let reference = "https://cdn.example/hls/channel/segment.ts?sig=a%2Fb&token=x+y";
|
||||
|
||||
assert!(matches!(rewrite_hls_url(base, reference), std::borrow::Cow::Borrowed(_)));
|
||||
assert_eq!(rewrite_hls_url(base, reference), reference);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rewrite_https_absolute_passthrough() {
|
||||
let base = "http://example.com/hls/playlist.m3u8";
|
||||
@@ -220,6 +404,13 @@ mod test {
|
||||
assert_eq!(out, "file:///mnt/media/hls/seg001.ts");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rewrite_file_child_playlist_does_not_inherit_archive_query() {
|
||||
let base = "file:///mnt/media/hls/playlist.m3u8?utc=1785072000";
|
||||
|
||||
assert_eq!(rewrite_hls_url(base, "child.m3u8"), "file:///mnt/media/hls/child.m3u8");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rewrite_file_absolute_passthrough() {
|
||||
let base = "file:///mnt/media/hls/playlist.m3u8";
|
||||
|
||||
@@ -462,9 +462,22 @@ fn parse_extm3u_catchup_correction(attributes: &str) -> Option<Arc<str>> {
|
||||
None
|
||||
}
|
||||
|
||||
fn parse_extvlcopt_user_agent(line: &str) -> Option<&str> {
|
||||
let (prefix, option) = line.trim().split_once(':')?;
|
||||
if !prefix.eq_ignore_ascii_case("#EXTVLCOPT") {
|
||||
return None;
|
||||
}
|
||||
let (name, value) = option.split_once('=')?;
|
||||
if !name.trim().eq_ignore_ascii_case("http-user-agent") {
|
||||
return None;
|
||||
}
|
||||
Some(value.trim())
|
||||
}
|
||||
|
||||
pub async fn consume_m3u<F: FnMut(PlaylistItem)>(cfg: &Config, input: &ConfigInput, lines: DynReader, mut visit: F) {
|
||||
let mut header: Option<String> = None;
|
||||
let mut group: Option<Arc<str>> = None;
|
||||
let mut upstream_user_agent: Option<Arc<str>> = None;
|
||||
let mut default_catchup_correction: Option<Arc<str>> = None;
|
||||
let input_name = &input.name;
|
||||
|
||||
@@ -481,6 +494,13 @@ pub async fn consume_m3u<F: FnMut(PlaylistItem)>(cfg: &Config, input: &ConfigInp
|
||||
if let Some(b'#') = bytes.first().copied() {
|
||||
if bytes.starts_with(b"#EXTINF") {
|
||||
header = Some(line);
|
||||
upstream_user_agent = None;
|
||||
continue;
|
||||
}
|
||||
if let Some(value) = parse_extvlcopt_user_agent(&line) {
|
||||
if header.is_some() {
|
||||
upstream_user_agent = (!value.is_empty()).then(|| value.intern());
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if let Some(rest) = line.strip_prefix("#EXTM3U") {
|
||||
@@ -505,6 +525,7 @@ pub async fn consume_m3u<F: FnMut(PlaylistItem)>(cfg: &Config, input: &ConfigInp
|
||||
),
|
||||
};
|
||||
let header = &mut item.header;
|
||||
header.upstream_user_agent = upstream_user_agent.take();
|
||||
header.source_ordinal = ord_counter;
|
||||
ord_counter += 1;
|
||||
if header.group.is_empty() {
|
||||
@@ -832,4 +853,28 @@ mod test {
|
||||
assert_eq!(items[0].header.group.as_ref(), "Sports");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn consume_m3u_scopes_extvlcopt_user_agent_to_one_item() {
|
||||
let content = concat!(
|
||||
"#EXTM3U\n",
|
||||
"#EXTINF:-1,Channel 1\n",
|
||||
"#extvlcopt:HTTP-USER-AGENT= Source UA/1.0 \n",
|
||||
"http://provider.example/live/user/pass/1.ts\n",
|
||||
"#EXTINF:-1,Channel 2\n",
|
||||
"#EXTVLCOPT:http-user-agent=\n",
|
||||
"http://provider.example/live/user/pass/2.ts\n",
|
||||
"#EXTVLCOPT:http-user-agent=must-not-leak\n",
|
||||
"#EXTINF:-1,Channel 3\n",
|
||||
"http://provider.example/live/user/pass/3.ts\n",
|
||||
);
|
||||
let mut items = Vec::new();
|
||||
|
||||
super::consume_m3u(&Config::default(), &test_input(), make_reader(content), |item| items.push(item)).await;
|
||||
|
||||
assert_eq!(items.len(), 3);
|
||||
assert_eq!(items[0].header.upstream_user_agent.as_deref(), Some("Source UA/1.0"));
|
||||
assert_eq!(items[1].header.upstream_user_agent, None);
|
||||
assert_eq!(items[2].header.upstream_user_agent, None);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ use shared::utils::Internable;
|
||||
use std::collections::HashSet;
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::utils::network::stalker::catalog::{
|
||||
use crate::iptv::stalker::catalog::{
|
||||
StalkerCategory, StalkerRawItem, StalkerRawItemInfo, StalkerRawSeriesDetails, StalkerRawSeriesEpisode,
|
||||
StalkerRawSeriesItem, StalkerRawSeriesSeason,
|
||||
};
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use crate::model::{ConfigInput, ConfigInputFlags};
|
||||
use crate::model::XtreamCategory;
|
||||
use crate::utils::request::DynReader;
|
||||
use crate::utils::xtream::get_xtream_stream_url_base;
|
||||
use crate::iptv::xtream::get_xtream_stream_url_base;
|
||||
use indexmap::IndexMap;
|
||||
use serde::Deserializer;
|
||||
use shared::error::TuliproxError;
|
||||
|
||||
@@ -6,6 +6,7 @@ use crate::{
|
||||
},
|
||||
sync_panel_api_exp_dates,
|
||||
},
|
||||
iptv::{m3u, xtream},
|
||||
messaging::send_message,
|
||||
media_server::{
|
||||
media_server_catalog_snapshot_to_playlist, refresh_media_server_catalog_complete_before_publish,
|
||||
@@ -32,7 +33,7 @@ use crate::{
|
||||
PlaylistSource,
|
||||
},
|
||||
utils::{
|
||||
debug_if_enabled, epg, log_memory_snapshot, m3u, trace_if_enabled, xtream,
|
||||
debug_if_enabled, epg, log_memory_snapshot, trace_if_enabled,
|
||||
StepMeasure, StepMeasureCallback,
|
||||
},
|
||||
};
|
||||
@@ -2077,18 +2078,29 @@ mod tests {
|
||||
use shared::utils::Internable;
|
||||
use crate::model::Config;
|
||||
|
||||
fn serialize_without_trailing_input_stream_id<T: serde::Serialize>(value: &T) -> Vec<u8> {
|
||||
fn serialize_without_trailing_fields<T: serde::Serialize>(value: &T, trailing_fields: &[u8]) -> Vec<u8> {
|
||||
let mut encoded = rmp_serde::to_vec(value).expect("playlist item should serialize");
|
||||
assert_eq!(encoded.pop(), Some(0xa0), "input_stream_id should be the trailing empty string");
|
||||
for expected in trailing_fields {
|
||||
assert_eq!(encoded.pop(), Some(*expected), "unexpected trailing MessagePack field");
|
||||
}
|
||||
let removed = trailing_fields.len();
|
||||
match encoded[0] {
|
||||
marker @ 0x91..=0x9f => encoded[0] = marker - 1,
|
||||
marker @ 0x92..=0x9f => {
|
||||
let len = usize::from(marker - 0x90);
|
||||
assert!(len >= removed, "trailing field count exceeds MessagePack sequence length");
|
||||
encoded[0] = 0x90 + u8::try_from(len - removed).unwrap_or_default();
|
||||
}
|
||||
0xdc => {
|
||||
let len = u16::from_be_bytes([encoded[1], encoded[2]]);
|
||||
encoded[1..3].copy_from_slice(&(len - 1).to_be_bytes());
|
||||
let removed = u16::try_from(removed).unwrap_or(u16::MAX);
|
||||
assert!(len >= removed, "trailing field count exceeds MessagePack sequence length");
|
||||
encoded[1..3].copy_from_slice(&(len - removed).to_be_bytes());
|
||||
}
|
||||
0xdd => {
|
||||
let len = u32::from_be_bytes([encoded[1], encoded[2], encoded[3], encoded[4]]);
|
||||
encoded[1..5].copy_from_slice(&(len - 1).to_be_bytes());
|
||||
let removed = u32::try_from(removed).unwrap_or(u32::MAX);
|
||||
assert!(len >= removed, "trailing field count exceeds MessagePack sequence length");
|
||||
encoded[1..5].copy_from_slice(&(len - removed).to_be_bytes());
|
||||
}
|
||||
marker => panic!("unexpected MessagePack sequence marker {marker:#x}"),
|
||||
}
|
||||
@@ -2231,11 +2243,12 @@ mod tests {
|
||||
},
|
||||
};
|
||||
|
||||
let header_bytes = serialize_without_trailing_input_stream_id(&source.header);
|
||||
let header_bytes = serialize_without_trailing_fields(&source.header, &[0xc0, 0xa0]);
|
||||
let decoded_header: PlaylistItemHeader =
|
||||
rmp_serde::from_slice(&header_bytes).expect("legacy header should deserialize");
|
||||
assert!(decoded_header.input_stream_id.is_empty());
|
||||
assert_eq!(decoded_header.get_input_stream_id(), None);
|
||||
assert_eq!(decoded_header.upstream_user_agent, None);
|
||||
let mut decoded_header = decoded_header;
|
||||
decoded_header.freeze_input_stream_id();
|
||||
assert_eq!(decoded_header.get_input_stream_id().as_deref(), Some("origin-alpha"));
|
||||
@@ -2243,19 +2256,73 @@ mod tests {
|
||||
source.header.freeze_input_stream_id();
|
||||
let mut m3u_item = M3uPlaylistItem::from(&source);
|
||||
m3u_item.input_stream_id = "".intern();
|
||||
let m3u_bytes = serialize_without_trailing_input_stream_id(&m3u_item);
|
||||
let m3u_bytes = serialize_without_trailing_fields(&m3u_item, &[0xc0, 0xa0]);
|
||||
let decoded_m3u: M3uPlaylistItem =
|
||||
rmp_serde::from_slice(&m3u_bytes).expect("legacy M3U item should deserialize");
|
||||
assert!(decoded_m3u.input_stream_id.is_empty());
|
||||
assert_eq!(decoded_m3u.get_input_stream_id().as_deref(), Some("origin-alpha"));
|
||||
assert_eq!(decoded_m3u.upstream_user_agent, None);
|
||||
|
||||
let mut xtream_item = XtreamPlaylistItem::from(&source);
|
||||
xtream_item.input_stream_id = "".intern();
|
||||
let xtream_bytes = serialize_without_trailing_input_stream_id(&xtream_item);
|
||||
let xtream_bytes = serialize_without_trailing_fields(&xtream_item, &[0xc0, 0xa0]);
|
||||
let decoded_xtream: XtreamPlaylistItem =
|
||||
rmp_serde::from_slice(&xtream_bytes).expect("legacy Xtream item should deserialize");
|
||||
assert!(decoded_xtream.input_stream_id.is_empty());
|
||||
assert_eq!(decoded_xtream.get_input_stream_id().as_deref(), Some("80510"));
|
||||
assert_eq!(decoded_xtream.upstream_user_agent, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn previous_messagepack_playlist_items_default_missing_upstream_user_agent() {
|
||||
let source = PlaylistItem {
|
||||
header: PlaylistItemHeader {
|
||||
id: "80510".intern(),
|
||||
input_stream_id: "origin-alpha".intern(),
|
||||
..Default::default()
|
||||
},
|
||||
};
|
||||
|
||||
let header: PlaylistItemHeader = rmp_serde::from_slice(&serialize_without_trailing_fields(&source.header, &[0xc0]))
|
||||
.expect("previous header should deserialize");
|
||||
let m3u: M3uPlaylistItem = rmp_serde::from_slice(&serialize_without_trailing_fields(
|
||||
&M3uPlaylistItem::from(&source),
|
||||
&[0xc0],
|
||||
))
|
||||
.expect("previous M3U item should deserialize");
|
||||
let xtream: XtreamPlaylistItem = rmp_serde::from_slice(&serialize_without_trailing_fields(
|
||||
&XtreamPlaylistItem::from(&source),
|
||||
&[0xc0],
|
||||
))
|
||||
.expect("previous Xtream item should deserialize");
|
||||
|
||||
assert_eq!(header.input_stream_id.as_ref(), "origin-alpha");
|
||||
assert_eq!(m3u.input_stream_id.as_ref(), "origin-alpha");
|
||||
assert_eq!(xtream.input_stream_id.as_ref(), "origin-alpha");
|
||||
assert_eq!(header.upstream_user_agent, None);
|
||||
assert_eq!(m3u.upstream_user_agent, None);
|
||||
assert_eq!(xtream.upstream_user_agent, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn messagepack_playlist_items_preserve_upstream_user_agent() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let source = PlaylistItem {
|
||||
header: PlaylistItemHeader {
|
||||
upstream_user_agent: Some("Provider-UA".intern()),
|
||||
..Default::default()
|
||||
},
|
||||
};
|
||||
|
||||
let header: PlaylistItemHeader = rmp_serde::from_slice(&rmp_serde::to_vec(&source.header)?)?;
|
||||
let m3u: M3uPlaylistItem =
|
||||
rmp_serde::from_slice(&rmp_serde::to_vec(&M3uPlaylistItem::from(&source))?)?;
|
||||
let xtream: XtreamPlaylistItem =
|
||||
rmp_serde::from_slice(&rmp_serde::to_vec(&XtreamPlaylistItem::from(&source))?)?;
|
||||
|
||||
assert_eq!(header.upstream_user_agent.as_deref(), Some("Provider-UA"));
|
||||
assert_eq!(m3u.upstream_user_agent.as_deref(), Some("Provider-UA"));
|
||||
assert_eq!(xtream.upstream_user_agent.as_deref(), Some("Provider-UA"));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
//!
|
||||
//! Reverse-proxy re-resolve (when a 4xx upstream error is observed) is implemented in the
|
||||
//! HLS/Xtream endpoints and reaches back into the API client via the helper
|
||||
//! [`crate::utils::network::stalker::client::StalkerApiClient::create_link`].
|
||||
//! [`crate::iptv::stalker::client::StalkerApiClient::create_link`].
|
||||
|
||||
#![allow(clippy::too_many_lines, clippy::needless_pass_by_value)]
|
||||
|
||||
@@ -33,8 +33,8 @@ use crate::repository::stalker_generation_repository::{load_active_manifest, loa
|
||||
use super::stalker_refresh::{
|
||||
advance_stalker_refresh, StalkerClusterSelection, StalkerRefreshMode, StalkerRefreshOutcome,
|
||||
};
|
||||
use crate::utils::network::stalker::client::StalkerApiClient;
|
||||
use crate::utils::network::stalker::error::StalkerError;
|
||||
use crate::iptv::stalker::client::StalkerApiClient;
|
||||
use crate::iptv::stalker::error::StalkerError;
|
||||
|
||||
|
||||
/// Cluster selector used by the Stalker processor. Mirrors the Xtream cluster split
|
||||
|
||||
@@ -18,10 +18,10 @@ use crate::repository::stalker_repository::{
|
||||
remove_stalker_file, snapshot_stalker_epg_at, snapshot_stalker_items_at,
|
||||
upsert_stalker_epg_at, upsert_stalker_items_at,
|
||||
};
|
||||
use crate::utils::network::stalker::catalog::{StalkerCategory, StalkerRawItem};
|
||||
use crate::utils::network::stalker::client::StalkerApiClient;
|
||||
use crate::utils::network::stalker::error::StalkerError;
|
||||
use crate::utils::network::stalker::profile::StalkerHandshake;
|
||||
use crate::iptv::stalker::catalog::{StalkerCategory, StalkerRawItem};
|
||||
use crate::iptv::stalker::client::StalkerApiClient;
|
||||
use crate::iptv::stalker::error::StalkerError;
|
||||
use crate::iptv::stalker::profile::StalkerHandshake;
|
||||
use super::stalker::StalkerCluster;
|
||||
|
||||
const MAX_RETRIES: u8 = 3;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use crate::api::model::UpdateTask;
|
||||
use crate::api::model::{ActiveProviderManager, ProviderHandle, ProviderIdType, ResolveReason, ResolveReasonSet};
|
||||
use crate::library::{MetadataResolver, MetadataStorage};
|
||||
use crate::iptv::xtream;
|
||||
use crate::media_enrichment::policy::MissingFactEnrichmentPolicy;
|
||||
use crate::media_enrichment::xtream::{
|
||||
apply_fact_patch_to_series, series_fact_patch_from_metadata, series_fact_patch_from_title_candidates,
|
||||
@@ -21,7 +22,7 @@ use crate::repository::persists_input_series_info;
|
||||
use crate::repository::{get_input_storage_path, persist_input_series_info_batch, MemoryPlaylistSource};
|
||||
use crate::repository::{xtream_get_file_path, BPlusTreeQuery};
|
||||
use crate::utils::ffmpeg::{is_supported_probe_url, FfmpegExecutor, ProbeFailureKind, ProbeUrlOutcome};
|
||||
use crate::utils::{debug_if_enabled, xtream};
|
||||
use crate::utils::debug_if_enabled;
|
||||
use log::{debug, error, info, log_enabled, trace, warn, Level};
|
||||
use parking_lot::Mutex;
|
||||
use serde_json::Value;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use crate::api::model::{ActiveProviderManager, ProviderHandle};
|
||||
use crate::api::model::{ProviderIdType, ResolveReason, ResolveReasonSet, UpdateTask};
|
||||
use crate::library::{MediaMetadata, MetadataResolver, MetadataStorage};
|
||||
use crate::iptv::xtream;
|
||||
use crate::media_enrichment::policy::MissingFactEnrichmentPolicy;
|
||||
use crate::media_enrichment::xtream::{
|
||||
apply_fact_patch_to_video, distinct_non_empty_title_candidates, video_fact_patch_from_metadata,
|
||||
@@ -22,7 +23,7 @@ use crate::repository::persist_input_vod_info;
|
||||
use crate::repository::persist_input_vod_info_batch;
|
||||
use crate::repository::{xtream_get_file_path, BPlusTreeQuery};
|
||||
use crate::utils::ffmpeg::{is_supported_probe_url, FfmpegExecutor, ProbeFailureKind, ProbeUrlOutcome};
|
||||
use crate::utils::{debug_if_enabled, trace_if_enabled, xtream};
|
||||
use crate::utils::{debug_if_enabled, trace_if_enabled};
|
||||
use log::{debug, error, info, log_enabled, trace, warn, Level};
|
||||
use parking_lot::Mutex;
|
||||
use serde_json::Value;
|
||||
|
||||
Reference in New Issue
Block a user