crispy_catchup/
provider.rs1use regex::Regex;
7use std::sync::LazyLock;
8
9use crate::error::CatchupError;
10
11static FLUSSONIC_REGEX: LazyLock<Regex> = LazyLock::new(|| {
22 Regex::new(r"^(https?://[^/]+)/(.*)/([^/]*)(mpegts|\.m3u8)(\?.+=.+)?$")
23 .expect("flussonic regex")
24});
25
26static FLUSSONIC_GENERIC_REGEX: LazyLock<Regex> = LazyLock::new(|| {
30 Regex::new(r"^(https?://[^/]+)/(.*)/([^\?]*)(\?.+=.+)?$").expect("flussonic generic regex")
31});
32
33pub fn generate_flussonic_source(
55 url: &str,
56 is_ts_hint: bool,
57) -> Result<(String, bool), CatchupError> {
58 if let Some(caps) = FLUSSONIC_REGEX.captures(url) {
60 let host = caps.get(1).map_or("", |m| m.as_str());
61 let channel_id = caps.get(2).map_or("", |m| m.as_str());
62 let list_type = caps.get(3).map_or("", |m| m.as_str());
63 let stream_type = caps.get(4).map_or("", |m| m.as_str());
64 let url_append = caps.get(5).map_or("", |m| m.as_str());
65
66 let is_ts = stream_type == "mpegts";
67 if is_ts {
68 let source = format!("{host}/{channel_id}/timeshift_abs-${{start}}.ts{url_append}");
69 return Ok((source, true));
70 }
71
72 let source = if list_type == "index" {
73 format!("{host}/{channel_id}/timeshift_rel-{{offset:1}}.m3u8{url_append}")
74 } else {
75 format!("{host}/{channel_id}/{list_type}-timeshift_rel-{{offset:1}}.m3u8{url_append}")
76 };
77 return Ok((source, false));
78 }
79
80 if let Some(caps) = FLUSSONIC_GENERIC_REGEX.captures(url) {
82 let host = caps.get(1).map_or("", |m| m.as_str());
83 let channel_id = caps.get(2).map_or("", |m| m.as_str());
84 let url_append = caps.get(4).map_or("", |m| m.as_str());
85
86 if is_ts_hint {
87 let source = format!("{host}/{channel_id}/timeshift_abs-${{start}}.ts{url_append}");
88 return Ok((source, true));
89 }
90
91 let source = format!("{host}/{channel_id}/timeshift_rel-{{offset:1}}.m3u8{url_append}");
92 return Ok((source, false));
93 }
94
95 Err(CatchupError::UrlParseFailed {
96 provider: "Flussonic".to_string(),
97 url: url.to_string(),
98 })
99}
100
101static XTREAM_CODES_REGEX: LazyLock<Regex> = LazyLock::new(|| {
111 Regex::new(r"^(https?://[^/]+)/(?:live/)?([^/]+)/([^/]+)/([^/\.]+)(\.m3u[8]?)?$")
112 .expect("xtream codes regex")
113});
114
115pub fn generate_xtream_codes_source(url: &str) -> Result<(String, bool), CatchupError> {
131 if let Some(caps) = XTREAM_CODES_REGEX.captures(url) {
132 let host = caps.get(1).map_or("", |m| m.as_str());
133 let username = caps.get(2).map_or("", |m| m.as_str());
134 let password = caps.get(3).map_or("", |m| m.as_str());
135 let channel_id = caps.get(4).map_or("", |m| m.as_str());
136 let extension = caps.get(5).map_or("", |m| m.as_str());
137
138 let (ext, is_ts) = if extension.is_empty() {
139 (".ts", true)
140 } else {
141 (extension, false)
142 };
143
144 let source = format!(
145 "{host}/timeshift/{username}/{password}/{{duration:60}}/{{Y}}-{{m}}-{{d}}:{{H}}-{{M}}/{channel_id}{ext}"
146 );
147 return Ok((source, is_ts));
148 }
149
150 Err(CatchupError::UrlParseFailed {
151 provider: "Xtream Codes".to_string(),
152 url: url.to_string(),
153 })
154}
155
156#[cfg(test)]
157mod tests {
158 use super::*;
159
160 #[test]
165 fn flussonic_mpegts_stream() {
166 let (source, is_ts) =
167 generate_flussonic_source("http://ch01.spr24.net/151/mpegts?token=my_token", false)
168 .unwrap();
169 assert!(is_ts);
170 assert_eq!(
171 source,
172 "http://ch01.spr24.net/151/timeshift_abs-${start}.ts?token=my_token"
173 );
174 }
175
176 #[test]
177 fn flussonic_index_m3u8() {
178 let (source, is_ts) =
179 generate_flussonic_source("http://list.tv:8888/325/index.m3u8?token=secret", false)
180 .unwrap();
181 assert!(!is_ts);
182 assert_eq!(
183 source,
184 "http://list.tv:8888/325/timeshift_rel-{offset:1}.m3u8?token=secret"
185 );
186 }
187
188 #[test]
189 fn flussonic_named_m3u8() {
190 let (source, is_ts) =
191 generate_flussonic_source("http://list.tv:8888/325/mono.m3u8?token=secret", false)
192 .unwrap();
193 assert!(!is_ts);
194 assert_eq!(
195 source,
196 "http://list.tv:8888/325/mono-timeshift_rel-{offset:1}.m3u8?token=secret"
197 );
198 }
199
200 #[test]
201 fn flussonic_generic_hls() {
202 let (source, is_ts) =
203 generate_flussonic_source("http://list.tv:8888/325/live?token=my_token", false)
204 .unwrap();
205 assert!(!is_ts);
206 assert_eq!(
207 source,
208 "http://list.tv:8888/325/timeshift_rel-{offset:1}.m3u8?token=my_token"
209 );
210 }
211
212 #[test]
213 fn flussonic_generic_ts_hint() {
214 let (source, is_ts) =
215 generate_flussonic_source("http://list.tv:8888/325/live?token=my_token", true).unwrap();
216 assert!(is_ts);
217 assert_eq!(
218 source,
219 "http://list.tv:8888/325/timeshift_abs-${start}.ts?token=my_token"
220 );
221 }
222
223 #[test]
224 fn flussonic_invalid_url() {
225 let result = generate_flussonic_source("not-a-url", false);
226 assert!(result.is_err());
227 }
228
229 #[test]
234 fn xtream_codes_no_extension() {
235 let (source, is_ts) =
236 generate_xtream_codes_source("http://list.tv:8080/my@account.xc/my_password/1477")
237 .unwrap();
238 assert!(is_ts);
239 assert_eq!(
240 source,
241 "http://list.tv:8080/timeshift/my@account.xc/my_password/{duration:60}/{Y}-{m}-{d}:{H}-{M}/1477.ts"
242 );
243 }
244
245 #[test]
246 fn xtream_codes_m3u8_extension() {
247 let (source, is_ts) = generate_xtream_codes_source(
248 "http://list.tv:8080/live/my@account.xc/my_password/1477.m3u8",
249 )
250 .unwrap();
251 assert!(!is_ts);
252 assert_eq!(
253 source,
254 "http://list.tv:8080/timeshift/my@account.xc/my_password/{duration:60}/{Y}-{m}-{d}:{H}-{M}/1477.m3u8"
255 );
256 }
257
258 #[test]
259 fn xtream_codes_with_live_prefix() {
260 let (source, _) =
261 generate_xtream_codes_source("http://list.tv:8080/live/user/pass/1477").unwrap();
262 assert!(source.contains("/timeshift/user/pass/"));
263 }
264
265 #[test]
266 fn xtream_codes_invalid_url() {
267 let result = generate_xtream_codes_source("not-a-url");
268 assert!(result.is_err());
269 }
270}