use regex::Regex;
use std::sync::LazyLock;
use crate::error::CatchupError;
static FLUSSONIC_REGEX: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(r"^(https?://[^/]+)/(.*)/([^/]*)(mpegts|\.m3u8)(\?.+=.+)?$")
.expect("flussonic regex")
});
static FLUSSONIC_GENERIC_REGEX: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(r"^(https?://[^/]+)/(.*)/([^\?]*)(\?.+=.+)?$").expect("flussonic generic regex")
});
pub fn generate_flussonic_source(
url: &str,
is_ts_hint: bool,
) -> Result<(String, bool), CatchupError> {
if let Some(caps) = FLUSSONIC_REGEX.captures(url) {
let host = caps.get(1).map_or("", |m| m.as_str());
let channel_id = caps.get(2).map_or("", |m| m.as_str());
let list_type = caps.get(3).map_or("", |m| m.as_str());
let stream_type = caps.get(4).map_or("", |m| m.as_str());
let url_append = caps.get(5).map_or("", |m| m.as_str());
let is_ts = stream_type == "mpegts";
if is_ts {
let source = format!("{host}/{channel_id}/timeshift_abs-${{start}}.ts{url_append}");
return Ok((source, true));
}
let source = if list_type == "index" {
format!("{host}/{channel_id}/timeshift_rel-{{offset:1}}.m3u8{url_append}")
} else {
format!("{host}/{channel_id}/{list_type}-timeshift_rel-{{offset:1}}.m3u8{url_append}")
};
return Ok((source, false));
}
if let Some(caps) = FLUSSONIC_GENERIC_REGEX.captures(url) {
let host = caps.get(1).map_or("", |m| m.as_str());
let channel_id = caps.get(2).map_or("", |m| m.as_str());
let url_append = caps.get(4).map_or("", |m| m.as_str());
if is_ts_hint {
let source = format!("{host}/{channel_id}/timeshift_abs-${{start}}.ts{url_append}");
return Ok((source, true));
}
let source = format!("{host}/{channel_id}/timeshift_rel-{{offset:1}}.m3u8{url_append}");
return Ok((source, false));
}
Err(CatchupError::UrlParseFailed {
provider: "Flussonic".to_string(),
url: url.to_string(),
})
}
static XTREAM_CODES_REGEX: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(r"^(https?://[^/]+)/(?:live/)?([^/]+)/([^/]+)/([^/\.]+)(\.m3u[8]?)?$")
.expect("xtream codes regex")
});
pub fn generate_xtream_codes_source(url: &str) -> Result<(String, bool), CatchupError> {
if let Some(caps) = XTREAM_CODES_REGEX.captures(url) {
let host = caps.get(1).map_or("", |m| m.as_str());
let username = caps.get(2).map_or("", |m| m.as_str());
let password = caps.get(3).map_or("", |m| m.as_str());
let channel_id = caps.get(4).map_or("", |m| m.as_str());
let extension = caps.get(5).map_or("", |m| m.as_str());
let (ext, is_ts) = if extension.is_empty() {
(".ts", true)
} else {
(extension, false)
};
let source = format!(
"{host}/timeshift/{username}/{password}/{{duration:60}}/{{Y}}-{{m}}-{{d}}:{{H}}-{{M}}/{channel_id}{ext}"
);
return Ok((source, is_ts));
}
Err(CatchupError::UrlParseFailed {
provider: "Xtream Codes".to_string(),
url: url.to_string(),
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn flussonic_mpegts_stream() {
let (source, is_ts) =
generate_flussonic_source("http://ch01.spr24.net/151/mpegts?token=my_token", false)
.unwrap();
assert!(is_ts);
assert_eq!(
source,
"http://ch01.spr24.net/151/timeshift_abs-${start}.ts?token=my_token"
);
}
#[test]
fn flussonic_index_m3u8() {
let (source, is_ts) =
generate_flussonic_source("http://list.tv:8888/325/index.m3u8?token=secret", false)
.unwrap();
assert!(!is_ts);
assert_eq!(
source,
"http://list.tv:8888/325/timeshift_rel-{offset:1}.m3u8?token=secret"
);
}
#[test]
fn flussonic_named_m3u8() {
let (source, is_ts) =
generate_flussonic_source("http://list.tv:8888/325/mono.m3u8?token=secret", false)
.unwrap();
assert!(!is_ts);
assert_eq!(
source,
"http://list.tv:8888/325/mono-timeshift_rel-{offset:1}.m3u8?token=secret"
);
}
#[test]
fn flussonic_generic_hls() {
let (source, is_ts) =
generate_flussonic_source("http://list.tv:8888/325/live?token=my_token", false)
.unwrap();
assert!(!is_ts);
assert_eq!(
source,
"http://list.tv:8888/325/timeshift_rel-{offset:1}.m3u8?token=my_token"
);
}
#[test]
fn flussonic_generic_ts_hint() {
let (source, is_ts) =
generate_flussonic_source("http://list.tv:8888/325/live?token=my_token", true).unwrap();
assert!(is_ts);
assert_eq!(
source,
"http://list.tv:8888/325/timeshift_abs-${start}.ts?token=my_token"
);
}
#[test]
fn flussonic_invalid_url() {
let result = generate_flussonic_source("not-a-url", false);
assert!(result.is_err());
}
#[test]
fn xtream_codes_no_extension() {
let (source, is_ts) =
generate_xtream_codes_source("http://list.tv:8080/my@account.xc/my_password/1477")
.unwrap();
assert!(is_ts);
assert_eq!(
source,
"http://list.tv:8080/timeshift/my@account.xc/my_password/{duration:60}/{Y}-{m}-{d}:{H}-{M}/1477.ts"
);
}
#[test]
fn xtream_codes_m3u8_extension() {
let (source, is_ts) = generate_xtream_codes_source(
"http://list.tv:8080/live/my@account.xc/my_password/1477.m3u8",
)
.unwrap();
assert!(!is_ts);
assert_eq!(
source,
"http://list.tv:8080/timeshift/my@account.xc/my_password/{duration:60}/{Y}-{m}-{d}:{H}-{M}/1477.m3u8"
);
}
#[test]
fn xtream_codes_with_live_prefix() {
let (source, _) =
generate_xtream_codes_source("http://list.tv:8080/live/user/pass/1477").unwrap();
assert!(source.contains("/timeshift/user/pass/"));
}
#[test]
fn xtream_codes_invalid_url() {
let result = generate_xtream_codes_source("not-a-url");
assert!(result.is_err());
}
}