Files
tuliprox/src/m3u_processing.rs
T

465 lines
18 KiB
Rust
Raw Normal View History

2023-01-13 18:38:13 +01:00
extern crate unidecode;
2022-03-24 14:08:25 +01:00
use std::io::Write;
use std::sync::Arc;
use std::thread;
2022-04-04 17:26:25 +02:00
use config::ConfigTarget;
2022-04-21 17:17:09 +02:00
use chrono::Datelike;
2023-01-13 18:38:13 +01:00
use unidecode::unidecode;
2023-03-19 17:16:05 +01:00
use crate::{config, Config, utils, valid_property};
use crate::config::{ConfigInput, InputAffix, InputType};
2022-04-04 18:36:21 +02:00
use crate::model::SortOrder::{Asc, Desc};
use crate::filter::{ValueProvider};
use crate::m3u::{FieldAccessor, PlaylistGroup, PlaylistItem, PlaylistItemHeader};
use crate::mapping::{Mapping, MappingValueProcessor};
use crate::model::{ItemField, AFFIX_FIELDS, ProcessingOrder, TargetType};
2023-03-19 17:16:05 +01:00
use crate::service::{get_m3u_playlist, get_xtream_playlist};
2021-09-22 10:55:12 +02:00
macro_rules! open_file {
($path:expr) => {{
match std::fs::File::create($path) {
Ok(file) => file,
Err(e) => {
println!("cant create file: {:?}", $path);
return Err(e);
}
}
}};
}
struct KodiStyle {
year: regex::Regex,
season: regex::Regex,
episode: regex::Regex,
whitespace: regex::Regex,
}
2021-10-15 16:59:20 +02:00
fn check_write(res: std::io::Result<usize>) -> Result<(), std::io::Error> {
match res {
Ok(_) => Ok(()),
Err(_) => return Err(std::io::Error::new(std::io::ErrorKind::Other, "Unable to write file")),
}
}
2023-01-14 12:46:19 +01:00
fn filter_playlist(playlist: &Vec<PlaylistGroup>, target: &ConfigTarget, verbose: bool) -> Option<Vec<PlaylistGroup>> {
if verbose { println!("Filtering {} groups", playlist.len()) }
2023-01-13 15:56:03 +01:00
let mut new_playlist = Vec::new();
for pg in playlist {
2023-01-14 12:46:19 +01:00
if verbose { println!("Filtering group {} with {} items", pg.title, pg.channels.len()) }
2023-01-13 15:56:03 +01:00
let mut channels = Vec::new();
for pli in &pg.channels {
if is_valid(&pli, &target, verbose) {
2023-01-13 15:56:03 +01:00
channels.push(pli.clone());
}
}
2023-01-14 12:46:19 +01:00
if verbose { println!("Filtered group {} has now {} items", pg.title, channels.len()) }
2023-01-13 15:56:03 +01:00
if channels.len() > 0 {
new_playlist.push(PlaylistGroup {
title: pg.title.clone(),
channels,
});
}
}
Some(new_playlist)
}
fn apply_affixes(playlist: &mut Vec<PlaylistGroup>, input: &ConfigInput, verbose: bool) {
if input.suffix.is_some() || input.prefix.is_some() {
let validate_affix = |a: &Option<InputAffix>| match a {
Some(affix) => {
valid_property!(&affix.field.as_str(), AFFIX_FIELDS) && affix.value.len() > 0
}
_ => false
};
let apply_prefix = validate_affix(&input.prefix);
let apply_suffix = validate_affix(&input.suffix);
if apply_prefix || apply_suffix {
let get_affix_applied_value = |header: &mut PlaylistItemHeader, affix: &InputAffix, prefix: bool| {
if let Some(field_value) = header.get_field(&affix.field.as_str()) {
return if prefix {
format!("{}{}", &affix.value, field_value.as_str())
} else {
format!("{}{}", field_value.as_str(), &affix.value)
}
}
return String::from(&affix.value)
};
for group in playlist {
for channel in &mut group.channels {
if apply_suffix {
if let Some(suffix) = &input.suffix {
let value = get_affix_applied_value(&mut channel.header, suffix, false);
if verbose { println!("Applying input suffix: {}={}", &suffix.field, &value)}
channel.header.set_field(&suffix.field, &value.as_str());
}
}
if apply_prefix {
if let Some(prefix) = &input.prefix {
let value = get_affix_applied_value(&mut channel.header, prefix, true);
if verbose { println!("Applying input prefix: {}={}", &prefix.field, &value)}
channel.header.set_field(&prefix.field, &value.as_str());
}
}
}
}
}
}
}
pub(crate) fn write_m3u(playlist: &Vec<PlaylistGroup>,
input: &ConfigInput,
target: &ConfigTarget, cfg: &Config,
2023-01-14 12:46:19 +01:00
verbose: bool) -> Result<(), std::io::Error> {
let pipe: Vec<fn(playlist: &Vec<PlaylistGroup>, target: &ConfigTarget, verbose: bool) -> Option<Vec<PlaylistGroup>>> =
match &target.processing_order {
ProcessingOrder::FRM => vec![filter_playlist, rename_playlist, map_playlist],
ProcessingOrder::FMR => vec![filter_playlist, map_playlist, rename_playlist],
ProcessingOrder::RFM => vec![rename_playlist, filter_playlist, map_playlist],
ProcessingOrder::RMF => vec![rename_playlist, map_playlist, filter_playlist],
ProcessingOrder::MFR => vec![map_playlist, filter_playlist, rename_playlist],
ProcessingOrder::MRF => vec![map_playlist, rename_playlist, filter_playlist]
};
if verbose { println!("Processing order is {}", &target.processing_order) }
2023-01-13 15:56:03 +01:00
let mut new_playlist = playlist.clone();
2023-01-13 15:56:03 +01:00
for f in pipe {
let r = f(&new_playlist, &target, verbose);
2023-01-13 15:56:03 +01:00
if r.is_some() {
new_playlist = r.unwrap();
}
}
apply_affixes(&mut new_playlist, input, verbose);
2022-04-05 12:23:30 +02:00
sort_playlist(target, &mut new_playlist);
match &target.output {
Some(output_type) => {
match output_type {
TargetType::Strm => return write_strm_playlist(&target, &cfg, &mut new_playlist),
_ => {}
2021-12-29 16:37:04 +01:00
}
}
2022-04-05 12:23:30 +02:00
_ => {}
2022-03-24 14:08:25 +01:00
}
2022-04-05 12:23:30 +02:00
return write_m3u_playlist(&target, &cfg, &mut new_playlist);
}
2021-12-29 16:37:04 +01:00
2022-04-05 12:23:30 +02:00
fn write_m3u_playlist(target: &ConfigTarget, cfg: &Config, new_playlist: &mut Vec<PlaylistGroup>) -> Result<(), std::io::Error> {
2022-03-24 14:08:25 +01:00
match utils::get_file_path(&cfg.working_dir, Some(std::path::PathBuf::from(&target.filename))) {
Some(path) => {
let mut m3u_file = open_file!(&path);
2022-04-05 12:23:30 +02:00
match check_write(m3u_file.write(b"#EXTM3U\n")) {
2022-03-24 14:08:25 +01:00
Ok(_) => (),
Err(e) => return Err(e),
}
2022-04-05 12:23:30 +02:00
for pg in new_playlist {
2022-03-24 14:08:25 +01:00
for pli in &pg.channels {
2023-01-13 15:56:03 +01:00
let content = pli.to_m3u(&target.options);
match check_write(m3u_file.write(content.as_bytes())) {
Ok(_) => (),
Err(e) => return Err(e),
}
match check_write(m3u_file.write(b"\n")) {
Ok(_) => (),
Err(e) => return Err(e),
2021-10-15 16:59:20 +02:00
}
2021-09-22 10:55:12 +02:00
}
2022-03-24 14:08:25 +01:00
}
2021-09-22 10:55:12 +02:00
}
2022-03-24 14:08:25 +01:00
None => (),
2021-09-22 10:55:12 +02:00
}
2021-10-15 16:59:20 +02:00
Ok(())
2021-09-22 10:55:12 +02:00
}
2022-04-05 12:23:30 +02:00
fn sanitize_for_filename(text: &String, underscore_whitespace: bool) -> String {
return text.chars().filter(|c| c.is_alphanumeric() || c.is_whitespace())
.map(|c| if underscore_whitespace { if c.is_whitespace() { '_' } else { c } } else { c })
.collect::<String>();
}
fn write_strm_playlist(target: &ConfigTarget, cfg: &Config, new_playlist: &mut Vec<PlaylistGroup>) -> Result<(), std::io::Error> {
let underscore_whitespace = target.options.as_ref().map_or(false, |o| o.underscore_whitespace);
2022-04-05 13:09:15 +02:00
let cleanup = target.options.as_ref().map_or(false, |o| o.cleanup);
2022-04-21 17:17:09 +02:00
let kodi_style = target.options.as_ref().map_or(false, |o| o.kodi_style);
2022-04-05 12:23:30 +02:00
match utils::get_file_path(&cfg.working_dir, Some(std::path::PathBuf::from(&target.filename))) {
Some(path) => {
2022-04-05 13:09:15 +02:00
if cleanup {
let _ = std::fs::remove_dir_all(&path);
}
2022-04-05 12:23:30 +02:00
match std::fs::create_dir_all(&path) {
Err(e) => {
println!("cant create directory: {:?}", &path);
return Err(e);
}
_ => {}
};
for pg in new_playlist {
for pli in &pg.channels {
2023-01-13 15:56:03 +01:00
let dir_path = path.join(sanitize_for_filename(&pli.header.group, underscore_whitespace));
match std::fs::create_dir_all(&dir_path) {
Err(e) => {
println!("cant create directory: {:?}", &path);
return Err(e);
}
_ => {}
};
let mut file_name = sanitize_for_filename(&pli.header.title, underscore_whitespace);
if kodi_style {
let style = KodiStyle {
season: regex::Regex::new(r"[Ss]\d\d").unwrap(),
episode: regex::Regex::new(r"[Ee]\d\d").unwrap(),
year: regex::Regex::new(r"\d\d\d\d").unwrap(),
whitespace: regex::Regex::new(r"\s+").unwrap(),
};
file_name = kodi_style_rename(&file_name, &style);
}
let file_path = dir_path.join(format!("{}.strm", file_name));
let mut strm_file = open_file!(&file_path);
2023-01-13 15:56:03 +01:00
match check_write(strm_file.write(pli.url.as_bytes())) {
Ok(_) => (),
Err(e) => return Err(e),
2022-04-05 12:23:30 +02:00
}
}
}
}
None => (),
}
Ok(())
}
2022-04-21 17:17:09 +02:00
fn kodi_style_rename_year(name: &String, style: &KodiStyle) -> (String, Option<String>) {
let current_date = chrono::Utc::now();
let cur_year = current_date.year();
match style.year.find(&name) {
Some(m) => {
let s_year = &name[m.start()..m.end()];
let t_year: i32 = s_year.parse().unwrap();
if t_year > 1900 && t_year <= cur_year {
let new_name = format!("{}{}", &name[0..m.start()], &name[m.end()..]);
return (new_name, Some(String::from(s_year)));
}
return (String::from(name), Some(cur_year.to_string()));
}
_ => (String::from(name), Some(cur_year.to_string())),
}
}
fn kodi_style_rename_season(name: &String, style: &KodiStyle) -> (String, Option<String>) {
match style.season.find(&name) {
Some(m) => {
let s_season = &name[m.start()..m.end()];
let season = Some(String::from(&s_season[1..]));
let new_name = format!("{}{}", &name[0..m.start()], &name[m.end()..]);
return (new_name, season);
}
_ => (String::from(name), Some(String::from("01"))),
}
}
fn kodi_style_rename_episode(name: &String, style: &KodiStyle) -> (String, Option<String>) {
match style.episode.find(&name) {
Some(m) => {
let s_episode = &name[m.start()..m.end()];
let episode = Some(String::from(&s_episode[1..]));
let new_name = format!("{}{}", &name[0..m.start()], &name[m.end()..]);
return (new_name, episode);
}
_ => (String::from(name), None),
}
}
fn kodi_style_rename(name: &String, style: &KodiStyle) -> String {
let (work_name_1, year) = kodi_style_rename_year(name, style);
let (work_name_2, season) = kodi_style_rename_season(&work_name_1, style);
let (work_name_3, episode) = kodi_style_rename_episode(&work_name_2, style);
if year.is_some() && season.is_some() && episode.is_some() {
let formatted = format!("{} ({}) S{}E{}", work_name_3, year.unwrap(), season.unwrap(), episode.unwrap());
return String::from(style.whitespace.replace_all(formatted.as_str(), " ").as_ref());
}
return String::from(name);
}
2022-04-05 12:23:30 +02:00
fn sort_playlist(target: &ConfigTarget, new_playlist: &mut Vec<PlaylistGroup>) {
if let Some(sort) = &target.sort {
new_playlist.sort_by(|a, b| {
let ordering = a.title.partial_cmp(&b.title).unwrap();
match sort.order {
Asc => ordering,
Desc => ordering.reverse()
}
});
}
}
2023-01-13 15:56:03 +01:00
fn is_valid(pli: &PlaylistItem, target: &ConfigTarget, verbose: bool) -> bool {
2023-01-13 15:56:03 +01:00
let provider = ValueProvider { pli };
return target.filter(&provider, verbose);
2023-01-13 15:56:03 +01:00
}
fn exec_rename(pli: &mut PlaylistItem, rename: &Option<Vec<config::ConfigRename>>, verbose: bool) {
2023-01-13 15:56:03 +01:00
match rename {
Some(renames) => {
if renames.len() > 0 {
let mut result = pli;
for r in renames {
let value = get_field_value(&result, &r.field);
let cap = r.re.as_ref().unwrap().replace_all(value, &r.new_name);
if verbose { println!("Renamed {}={} to {}", &r.field, value, cap) }
2023-01-13 15:56:03 +01:00
let value = cap.into_owned();
set_field_value(&mut result, &r.field, value);
}
}
}
_ => {}
}
}
2023-01-14 12:46:19 +01:00
fn rename_playlist(playlist: &Vec<PlaylistGroup>, target: &ConfigTarget, verbose: bool) -> Option<Vec<PlaylistGroup>> {
2023-01-13 15:56:03 +01:00
match &target.rename {
Some(renames) => {
if renames.len() > 0 {
let mut new_playlist: Vec<PlaylistGroup> = Vec::new();
2023-01-13 15:56:03 +01:00
for g in playlist {
let mut grp = g.clone();
2023-01-12 15:59:40 +01:00
for r in renames {
match r.field {
ItemField::Group => {
let cap = r.re.as_ref().unwrap().replace_all(&grp.title, &r.new_name);
2023-01-14 12:46:19 +01:00
if verbose { println!("Renamed group {} to {}", &grp.title, cap); }
2023-01-12 15:59:40 +01:00
grp.title = cap.into_owned();
}
_ => {}
}
}
2023-01-13 15:56:03 +01:00
for pli in &mut grp.channels {
2023-01-14 12:46:19 +01:00
exec_rename(pli, &target.rename, verbose)
2023-01-13 15:56:03 +01:00
}
new_playlist.push(grp);
2023-01-12 15:59:40 +01:00
}
return Some(new_playlist);
2023-01-12 15:59:40 +01:00
}
2023-01-13 15:56:03 +01:00
None
2023-01-12 15:59:40 +01:00
}
2023-01-13 15:56:03 +01:00
_ => None
2023-01-12 15:59:40 +01:00
}
}
fn map_channel(channel: &mut PlaylistItem, mapping: &Mapping, verbose: bool) -> PlaylistItem {
2023-01-14 12:46:19 +01:00
if mapping.mapper.len() > 0 {
let channel_name = if mapping.match_as_ascii { unidecode(&channel.header.name) } else { String::from(&channel.header.name) };
if verbose && mapping.match_as_ascii { println!("Decoded {} for matching to {}", &channel.header.name, &channel_name) };
let provider = ValueProvider { pli: &channel.clone() };
2023-01-12 15:59:40 +01:00
for m in &mapping.mapper {
let mut processor = MappingValueProcessor { pli: channel, mapper: m };
match &m._filter {
Some(filter) => {
filter.filter(&provider, &mut processor, verbose);
2022-04-05 12:23:30 +02:00
}
2023-01-12 15:59:40 +01:00
_ => {}
}
}
}
return channel.clone();
}
2023-01-14 12:46:19 +01:00
fn map_playlist(playlist: &Vec<PlaylistGroup>, target: &ConfigTarget, verbose: bool) -> Option<Vec<PlaylistGroup>> {
if verbose { println!("Mapping") }
2023-01-12 17:15:50 +01:00
if target._mapping.is_some() {
let mut new_playlist: Vec<PlaylistGroup> = Vec::new();
for playlist_group in playlist {
let mut grp = playlist_group.clone();
let mappings = target._mapping.as_ref().unwrap();
for mapping in mappings {
if mapping.mapper.len() > 0 {
grp.channels = grp.channels.iter_mut().map(|chan| map_channel(chan, &mapping, verbose)).collect();
}
2022-04-05 12:23:30 +02:00
}
2023-01-12 17:15:50 +01:00
new_playlist.push(grp);
2022-04-05 12:23:30 +02:00
}
2023-01-12 17:15:50 +01:00
Some(new_playlist)
} else {
None
2022-04-05 12:23:30 +02:00
}
}
fn get_field_value<'a>(pli: &'a PlaylistItem, field: &ItemField) -> &'a str {
2021-09-22 10:55:12 +02:00
let value = match field {
2022-04-04 18:36:21 +02:00
ItemField::Group => pli.header.group.as_str(),
ItemField::Name => pli.header.name.as_str(),
ItemField::Title => pli.header.title.as_str(),
ItemField::Url => pli.url.as_str(),
2021-09-22 10:55:12 +02:00
};
value
}
fn set_field_value(pli: &mut PlaylistItem, field: &ItemField, value: String) -> () {
2021-09-22 10:55:12 +02:00
let header = &mut pli.header;
match field {
2022-04-04 18:36:21 +02:00
ItemField::Group => header.group = value,
ItemField::Name => header.name = value,
ItemField::Title => header.title = value,
2022-04-05 12:23:30 +02:00
ItemField::Url => {}
2021-09-22 10:55:12 +02:00
};
}
fn process_source(cfg: Arc<Config>, source_idx: usize, verbose: bool) {
let source = cfg.sources.get(source_idx).unwrap();
let input = &source.input;
2023-03-03 09:00:56 +01:00
if input.enabled {
2023-03-19 17:16:05 +01:00
let result = match input.input_type {
InputType::M3u => get_m3u_playlist(input,&cfg.working_dir, verbose),
InputType::Xtream => get_xtream_playlist(input, &cfg.working_dir, verbose),
};
2023-03-03 09:00:56 +01:00
match &result {
Some(playlist) => {
if playlist.is_empty() {
if verbose { println!("Input file is empty") }
} else {
if verbose { println!("Input file has {} groups", playlist.len()) }
for target in source.targets.iter() {
if target.enabled {
match write_m3u(playlist, input, target, &cfg, verbose) {
Ok(_) => (),
Err(e) => println!("Failed to write file: {}", e)
}
}
2022-03-24 14:08:25 +01:00
}
}
}
2023-03-03 09:00:56 +01:00
None => ()
2022-03-24 14:08:25 +01:00
}
}
}
2023-03-03 09:00:56 +01:00
pub fn process_sources(cfg: Arc<Config>, verbose: bool) {
let mut handle_list = vec![];
let thread_num = cfg.threads;
let process_parallel = thread_num > 1 && cfg.sources.len() > 1;
if verbose && process_parallel { println!("Using {} threads", thread_num) }
for (index, _) in cfg.sources.iter().enumerate() {
let config = cfg.clone();
if process_parallel {
let handles = &mut handle_list;
handles.push(thread::spawn(move || process_source(config, index, verbose)));
if handles.len() as u8 >= thread_num {
while let Some(handle) = handles.pop() {
let _ = handle.join();
}
}
} else {
process_source(config, index, verbose);
}
}
for handle in handle_list {
let _ = handle.join();
2022-03-24 14:08:25 +01:00
}
}