Files
tuliprox/src/processing/parser/m3u.rs
T

272 lines
8.8 KiB
Rust
Raw Normal View History

2024-05-10 10:13:50 +02:00
use std::borrow::BorrowMut;
2024-05-05 19:15:54 +02:00
use crate::model::config::{Config, ConfigInput};
2024-05-10 10:13:50 +02:00
use crate::model::playlist::{PlaylistGroup, PlaylistItem, PlaylistItemHeader, PlaylistItemType, XtreamCluster};
use crate::utils::hash_utils::extract_id_from_url;
use crate::utils::string_utils;
2023-03-19 17:16:05 +01:00
2024-10-21 19:41:17 +02:00
#[inline]
fn token_value(stack: &mut String, it: &mut std::str::Chars) -> String {
2024-10-21 19:41:17 +02:00
// Use .find() to skip until the first double quote (") character.
if it.any(|ch| ch == '"') {
2024-10-21 19:41:17 +02:00
// If a quote is found, call get_value to extract the value.
return get_value(stack, it);
2023-03-19 17:16:05 +01:00
}
2024-10-21 19:41:17 +02:00
// If no double quote is found, return an empty string.
2024-05-10 12:01:48 +02:00
String::new()
2023-03-19 17:16:05 +01:00
}
fn get_value(stack: &mut String, it: &mut std::str::Chars) -> String {
for c in it.skip_while(|c| c.is_whitespace()) {
if c == '"' {
2023-03-19 17:16:05 +01:00
break;
}
stack.push(c);
2023-03-19 17:16:05 +01:00
}
let result = stack.to_string();
stack.clear();
2024-10-16 16:56:26 +02:00
result
2023-03-19 17:16:05 +01:00
}
fn token_till(stack: &mut String, it: &mut std::str::Chars, stop_char: char, start_with_alpha: bool) -> Option<String> {
let mut skip_non_alpha = start_with_alpha;
2024-10-16 16:56:26 +02:00
2023-05-03 17:46:39 +02:00
for ch in it.by_ref() {
if ch == stop_char {
break;
2024-10-21 19:41:17 +02:00
}
if stack.is_empty() && ch.is_whitespace() {
2023-05-03 17:46:39 +02:00
continue;
2023-03-19 17:16:05 +01:00
}
2024-10-16 16:56:26 +02:00
if skip_non_alpha {
if ch.is_alphabetic() {
skip_non_alpha = false;
} else {
continue;
}
}
stack.push(ch);
2023-03-19 17:16:05 +01:00
}
2024-10-16 16:56:26 +02:00
if stack.is_empty() {
2024-10-16 16:56:26 +02:00
None
} else {
let result = stack.to_string();
stack.clear();
2024-10-16 16:56:26 +02:00
Some(result)
}
2023-03-19 17:16:05 +01:00
}
2024-10-21 19:41:17 +02:00
#[inline]
2023-03-19 17:16:05 +01:00
fn skip_digit(it: &mut std::str::Chars) -> Option<char> {
loop {
match it.next() {
Some(c) => {
2023-05-03 17:46:39 +02:00
if !(c == '-' || c == '+' || c.is_ascii_digit()) {
2023-03-19 17:16:05 +01:00
return Some(c);
}
}
None => return None,
}
}
}
2025-01-27 15:58:57 +01:00
fn create_empty_playlistitem_header(input_name: &str, url: &str) -> PlaylistItemHeader {
2023-09-29 16:03:20 +02:00
PlaylistItemHeader {
2025-03-11 14:56:10 +01:00
url: url.to_owned(),
2024-05-05 19:15:54 +02:00
category_id: 0,
2025-03-11 14:56:10 +01:00
input_name: input_name.to_string(),
2024-10-25 00:39:22 +02:00
..Default::default()
2023-09-29 16:03:20 +02:00
}
2023-09-26 19:41:02 +02:00
}
2023-03-19 17:16:05 +01:00
macro_rules! process_header_fields {
($header:expr, $token:expr, $(($prop:ident, $field:expr)),*; $val:expr) => {
match $token {
$(
2025-03-11 14:56:10 +01:00
$field => $header.$prop = $val,
)*
_ => {}
}
};
}
fn process_header(input_name: &str, video_suffixes: &[&str], content: &str, url: &str) -> PlaylistItemHeader {
let mut plih = create_empty_playlistitem_header(input_name, url);
2023-03-19 17:16:05 +01:00
let mut it = content.chars();
let mut stack = String::with_capacity(64);
let line_token = token_till(&mut stack, &mut it, ':', false);
2024-10-16 16:56:26 +02:00
if line_token.as_deref() == Some("#EXTINF") {
2023-03-19 17:16:05 +01:00
let mut c = skip_digit(&mut it);
loop {
2023-05-03 17:46:39 +02:00
if c.is_none() {
2023-03-19 17:16:05 +01:00
break;
}
let chr = c.unwrap();
if chr.is_whitespace() {
// skip
} else if chr == ',' {
plih.title = get_value(&mut stack, &mut it);
2024-05-10 12:01:48 +02:00
} else {
stack.push(chr);
let token = token_till(&mut stack, &mut it, '=', true);
2024-05-10 12:01:48 +02:00
if let Some(t) = token {
let value = token_value(&mut stack, &mut it);
process_header_fields!(plih, t.to_lowercase().as_str(),
(id, "tvg-id"),
(group, "group-title"),
(name, "tvg-name"),
(chno, "tvg-chno"),
(parent_code, "parent-code"),
(audio_track, "audio-track"),
(logo, "tvg-logo"),
(logo_small, "tvg-logo-small"),
(time_shift, "timeshift"),
2024-05-10 12:01:48 +02:00
(rec, "tvg-rec"); value);
2023-03-19 17:16:05 +01:00
}
}
c = it.next();
}
if plih.id.is_empty() {
2025-04-09 20:51:25 +02:00
plih.epg_channel_id = None;
2024-05-10 12:01:48 +02:00
if let Some(chanid) = extract_id_from_url(url) {
2025-03-11 14:56:10 +01:00
plih.id = chanid;
}
} else {
2025-04-09 20:51:25 +02:00
plih.epg_channel_id = Some(plih.id.to_string());
}
2023-03-19 17:16:05 +01:00
}
2023-09-26 19:41:02 +02:00
2024-10-16 16:56:26 +02:00
if video_suffixes.iter().any(|suffix| url.ends_with(suffix)) {
// TODO find Series based on group or configured names
plih.xtream_cluster = XtreamCluster::Video;
2024-10-25 18:26:25 +02:00
plih.item_type = PlaylistItemType::Video;
2023-10-25 09:57:44 +02:00
}
2024-10-16 16:56:26 +02:00
{
2024-10-21 19:41:17 +02:00
let header = plih.borrow_mut();
2024-10-16 16:56:26 +02:00
if header.name.is_empty() {
if !header.title.is_empty() {
header.name = header.title.clone();
} else if !header.id.is_empty() {
header.name = header.id.clone();
header.title = header.id.clone();
}
2023-10-25 09:57:44 +02:00
}
2023-09-26 19:41:02 +02:00
}
2024-10-16 16:56:26 +02:00
2023-03-19 17:16:05 +01:00
plih
}
2023-09-26 19:41:02 +02:00
2024-11-04 18:46:56 +01:00
pub fn consume_m3u<'a, I, F: FnMut(PlaylistItem)>(cfg: &Config, input: &ConfigInput, lines: I, mut visit: F)
2024-10-25 00:39:22 +02:00
where
I: Iterator<Item=&'a str>,
{
2023-03-19 17:16:05 +01:00
let mut header: Option<String> = None;
let mut group: Option<String> = None;
let input_name = input.name.as_str();
2023-03-19 17:16:05 +01:00
2024-10-16 16:56:26 +02:00
let video_suffixes = cfg.video.as_ref().unwrap().extensions.iter().map(String::as_str).collect::<Vec<&str>>();
2023-03-19 17:16:05 +01:00
for line in lines {
if line.starts_with("#EXTINF") {
2024-10-25 00:39:22 +02:00
header = Some(String::from(line));
2023-03-19 17:16:05 +01:00
continue;
}
if line.starts_with("#EXTGRP") {
group = Some(String::from(&line[8..]));
continue;
}
2023-05-03 17:46:39 +02:00
if line.starts_with('#') {
2023-03-19 17:16:05 +01:00
continue;
}
2023-10-11 14:33:17 +02:00
if let Some(header_value) = header {
let mut item = PlaylistItem { header: process_header(input_name, &video_suffixes, &header_value, line) };
2025-03-11 14:56:10 +01:00
let header = &mut item.header;
2024-05-05 19:15:54 +02:00
if header.group.is_empty() {
2024-03-26 11:01:59 +01:00
if let Some(group_value) = group {
2025-03-11 14:56:10 +01:00
header.group = group_value;
2024-03-26 11:01:59 +01:00
} else {
2024-05-10 12:01:48 +02:00
let current_title = header.title.clone();
2025-03-11 14:56:10 +01:00
header.group = string_utils::get_title_group(current_title.as_str());
2023-10-11 14:33:17 +02:00
}
2023-03-19 17:16:05 +01:00
}
visit(item);
2023-03-19 17:16:05 +01:00
}
header = None;
group = None;
}
}
2023-03-19 17:16:05 +01:00
2024-11-04 18:46:56 +01:00
pub fn parse_m3u<'a, I>(cfg: &Config, input: &ConfigInput, lines: I) -> Vec<PlaylistGroup>
2024-10-25 00:39:22 +02:00
where
I: Iterator<Item=&'a str>,
{
2024-09-11 12:13:05 +02:00
let mut sort_order: Vec<Vec<PlaylistItem>> = vec![];
2024-10-25 00:39:22 +02:00
let mut sort_order_idx: usize = 0;
2025-01-31 23:08:23 +01:00
let mut group_map: std::collections::HashMap<String, usize> = std::collections::HashMap::new();
2024-10-25 00:39:22 +02:00
consume_m3u(cfg, input, lines, |item| {
// keep the original sort order for groups and group the playlist items
2025-01-31 23:08:23 +01:00
let key = {
2025-03-11 14:56:10 +01:00
let header = &item.header;
2025-01-31 23:08:23 +01:00
format!("{}{}", &header.xtream_cluster, &header.group)
};
2024-09-11 12:13:05 +02:00
match group_map.entry(key) {
std::collections::hash_map::Entry::Vacant(v) => {
2024-10-25 00:39:22 +02:00
v.insert(sort_order_idx);
2024-09-11 12:13:05 +02:00
sort_order.push(vec![item]);
2024-10-25 00:39:22 +02:00
sort_order_idx += 1;
2024-09-11 12:13:05 +02:00
}
std::collections::hash_map::Entry::Occupied(o) => {
sort_order.get_mut(*o.get()).unwrap().push(item);
}
}
});
2024-09-11 12:13:05 +02:00
let mut grp_id = 0;
2024-11-04 18:46:56 +01:00
let result: Vec<PlaylistGroup> = sort_order.into_iter().map(|channels| {
2024-10-25 00:39:22 +02:00
// create a group based on the first playlist item
2024-09-11 12:13:05 +02:00
let channel = channels.first();
2024-10-25 00:39:22 +02:00
let (cluster, group_title) = channel.map(|pli|
(pli.header.xtream_cluster, &pli.header.group)).unwrap();
2024-09-11 12:13:05 +02:00
grp_id += 1;
2025-03-11 14:56:10 +01:00
PlaylistGroup { id: grp_id, xtream_cluster: cluster, title: group_title.to_string(), channels }
2024-09-11 12:13:05 +02:00
}).collect();
2023-03-19 17:16:05 +01:00
result
}
#[cfg(test)]
mod test {
use crate::processing::parser::m3u::process_header;
#[test]
fn test_process_header_1() {
let input: &str = "hello";
let video_suffixes = Vec::new();
let url = "http://hello.de/hello.ts";
let line = r#"#EXTINF:-1 channel-id="abc-seven" tvg-id="abc-seven" tvg-logo="https://abc.nz/.images/seven.png" tvg-chno="7" group-title="Sydney" , Seven"#;
let pli = process_header(input, &video_suffixes, line, url);
assert_eq!(pli.title, "Seven");
assert_eq!(pli.id, "abc-seven");
assert_eq!(pli.logo, "https://abc.nz/.images/seven.png");
assert_eq!(pli.chno, "7");
assert_eq!(pli.group, "Sydney");
}
#[test]
fn test_process_header_2() {
let input: &str = "hello";
let video_suffixes = Vec::new();
let url = "http://hello.de/hello.ts";
let line = r#"#EXTINF:-1 channel-id="abc-seven" tvg-id="abc-seven" tvg-logo="https://abc.nz/.images/seven.png" tvg-chno="7" group-title="Sydney", Seven"#;
let pli = process_header(input, &video_suffixes, line, url);
assert_eq!(pli.title, "Seven");
assert_eq!(pli.id, "abc-seven");
assert_eq!(pli.logo, "https://abc.nz/.images/seven.png");
assert_eq!(pli.chno, "7");
assert_eq!(pli.group, "Sydney");
}
}