Files
tuliprox/src/m3u.rs
T
2023-02-13 20:34:43 +01:00

259 lines
8.7 KiB
Rust

use serde::{Deserialize, Serialize};
use crate::config::ConfigOptions;
// https://de.wikipedia.org/wiki/M3U
// https://siptv.eu/howto/playlist.html
pub trait FieldAccessor {
fn get_field(&self, field: &str) -> Option<&String>;
fn set_field(&mut self, field: &str, value: &str) -> bool;
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PlaylistItemHeader {
pub id: String,
pub name: String,
pub logo: String,
pub logo_small: String,
pub group: String,
pub title: String,
pub parent_code: String,
pub audio_track: String,
pub time_shift: String,
pub rec: String,
pub source: String,
pub chno: String,
}
impl FieldAccessor for PlaylistItemHeader {
fn get_field(&self, field: &str) -> Option<&String> {
match field {
"id" => Some(&self.id),
"name" => Some(&self.name),
"logo" => Some(&self.logo),
"logo_small" => Some(&self.logo_small),
"group" => Some(&self.group),
"title" => Some(&self.title),
"parent_code" => Some(&self.parent_code),
"audio_track" => Some(&self.audio_track),
"time_shift" => Some(&self.time_shift),
"rec" => Some(&self.rec),
"source" => Some(&self.source),
"chno" => Some(&self.chno),
_ => None
}
}
fn set_field(&mut self, field: &str, value: &str) -> bool {
let val = String::from(value);
return match field {
"id" => { self.id = val; true},
"name" => { self.name = val; true }
"logo" => { self.logo = val; true }
"logo_small" => { self.logo_small = val; true }
"group" => { self.group = val; true }
"title" => { self.title = val; true }
"parent_code" => { self.parent_code = val; true }
"audio_track" => { self.audio_track = val; true }
"time_shift" => { self.time_shift = val; true }
"rec" => { self.rec = val; true }
"source" => { self.source = val; true }
"chno" => { self.chno = val; true }
_ => false
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PlaylistItem {
pub header: PlaylistItemHeader,
pub url: String,
}
impl PlaylistItem {
pub(crate) fn to_m3u(&self, options: &Option<ConfigOptions>) -> String {
let ignore_logo = options.as_ref().map_or(false, |o| o.ignore_logo);
let mut line = format!("#EXTINF:-1 tvg-id=\"{}\" tvg-name=\"{}\" group-title=\"{}\"", self.header.id, self.header.name, self.header.group);
if !self.header.chno.is_empty() {
line = format!("{} tvg-chno=\"{}\"", line, self.header.chno);
}
if !ignore_logo {
if !self.header.logo.is_empty() {
line = format!("{} tvg-logo=\"{}\"", line, self.header.logo);
}
if !self.header.logo_small.is_empty() {
line = format!("{} tvg-logo-small=\"{}\"", line, self.header.logo_small);
}
}
if !self.header.parent_code.is_empty() {
line = format!("{} parent-code=\"{}\"", line, self.header.parent_code);
}
if !self.header.audio_track.is_empty() {
line = format!("{} audio-track=\"{}\"", line, self.header.audio_track);
}
if !self.header.time_shift.is_empty() {
line = format!("{} timeschift=\"{}\"", line, self.header.time_shift);
}
if !self.header.rec.is_empty() {
line = format!("{} rec=\"{}\"", line, self.header.rec);
}
format!("{},{}\n{}", line, self.header.title, self.url)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PlaylistGroup {
pub title: String,
pub channels: Vec<PlaylistItem>,
}
fn token_value(it: &mut std::str::Chars) -> String {
if let Some(oc) = it.next() {
if oc == '"' {
return get_value(it);
}
}
String::from("")
}
fn get_value(it: &mut std::str::Chars) -> String {
let mut result: Vec<char> = vec![];
while let Some(oc) = it.next() {
if oc == '"' {
break;
}
result.push(oc);
}
String::from(result.iter().collect::<String>())
}
fn token_till(it: &mut std::str::Chars, stop_char: char) -> Option<String> {
let mut result: Vec<char> = vec![];
loop {
match it.next() {
Some(ch) => {
if ch == stop_char {
break;
} else if ch.is_whitespace() && result.is_empty() {
continue;
} else {
result.push(ch);
}
}
None => break,
}
}
if !result.is_empty() { Some(String::from(result.iter().collect::<String>())) } else { None }
}
fn skip_digit(it: &mut std::str::Chars) -> Option<char> {
loop {
match it.next() {
Some(c) => {
if !(c == '-' || c == '+' || c.is_digit(10)) {
return Some(c);
}
}
None => return None,
}
}
}
fn decode_header(content: &String) -> PlaylistItemHeader {
let mut plih = PlaylistItemHeader {
id: String::from(""),
name: String::from(""),
logo: String::from(""),
logo_small: String::from(""),
group: String::from("Unknown"),
title: String::from(""),
parent_code: String::from(""),
audio_track: String::from(""),
time_shift: String::from(""),
rec: String::from(""),
source: String::from(content),
chno: String::from(""),
};
let mut it = content.chars();
let line_token = token_till(&mut it, ':');
if line_token == Some(String::from("#EXTINF")) {
let mut c = skip_digit(&mut it);
loop {
if !c.is_some() {
break;
}
match c.unwrap() {
',' => plih.title = get_value(&mut it),
_ => {
let token = token_till(&mut it, '=');
if token != None {
let value = token_value(&mut it);
match token.unwrap().as_str() {
"tvg-id" => plih.id = value,
"tvg-name" => plih.name = value,
"group-title" => if !value.is_empty() { plih.group = value },
"parent-code" => plih.parent_code = value,
"audio-track" => plih.audio_track = value,
"tvg-logo" => plih.logo = value,
"tvg-logo-small" => plih.logo_small = value,
"timeshift" => plih.time_shift = value,
"tvg-rec" => plih.rec = value,
_ => {}
}
}
}
}
c = it.next();
}
}
plih
}
pub(crate) fn decode(lines: &Vec<String>) -> Vec<PlaylistGroup> {
let mut groups: std::collections::HashMap<String, Vec<PlaylistItem>> = std::collections::HashMap::new();
let mut sort_order: Vec<String> = vec![];
let mut header: Option<String> = None;
let mut group: Option<String> = None;
for line in lines {
if line.starts_with("#EXTINF") {
header = Some(String::from(line));
continue;
}
if line.starts_with("#EXTGRP") {
group = Some(String::from(&line[8..]));
continue;
}
if line.starts_with("#") {
continue;
}
if header.is_some() {
let mut item = PlaylistItem { header: decode_header(&header.unwrap()), url: String::from(line) };
if group.is_some() && item.header.group.is_empty() {
item.header.group = String::from(group.unwrap());
}
let key = String::from(&item.header.group);
// let key2 = String::from(&item.header.group);
match groups.entry(key.clone()) {
std::collections::hash_map::Entry::Vacant(e) => {
e.insert(vec![item]);
sort_order.push(key);
}
std::collections::hash_map::Entry::Occupied(mut e) => { e.get_mut().push(item); }
}
}
header = None;
group = None;
}
let mut result: Vec<PlaylistGroup> = vec![];
for (key, value) in groups {
result.push(PlaylistGroup { title: key, channels: value });
}
result.sort_by(|f, s| {
let i1 = sort_order.iter().position(|r| r == &f.title).unwrap();
let i2 = sort_order.iter().position(|r| r == &s.title).unwrap();
return i1.cmp(&i2);
});
result
}