diff --git a/src/download.rs b/src/download.rs index a336262bb..6c584432b 100644 --- a/src/download.rs +++ b/src/download.rs @@ -90,7 +90,7 @@ pub(crate) fn get_xmltv(_cfg: &Config, input: &ConfigInput, working_dir: &String let persist_file_path = prepare_file_path(input, working_dir, "").map(|path| add_prefix_to_filename(&path, "epg_", Some("xml"))); match utils::get_input_text_content(input, working_dir, url, persist_file_path) { Ok(xml_content) => { - xmltv_parser::parse_tvguide(&xml_content) + (xmltv_parser::parse_tvguide(xml_content.as_str()), vec![]) } Err(err) => (None, vec![err]) } diff --git a/src/filter.rs b/src/filter.rs index e9a482f8f..64617c6b2 100644 --- a/src/filter.rs +++ b/src/filter.rs @@ -110,7 +110,7 @@ impl Filter { let value = provider.call(field); let is_match = rewc.re.is_match(value.as_str()); if is_match { - //debug!("Match found: {:?} {} => {}={}", &rewc, &rewc.restr, &field, &value); + debug!("Match found: {:?} {} => {}={}", &rewc, &rewc.restr, &field, &value); processor.process(field, &value, rewc); } is_match diff --git a/src/main.rs b/src/main.rs index fa76810e1..d9746149e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -7,7 +7,7 @@ use std::sync::Arc; use clap::Parser; use env_logger::Builder; -use log::{ error, info, LevelFilter}; +use log::{error, info, LevelFilter}; use crate::config_reader::{read_api_proxy_config, read_config, read_mappings}; use crate::model::config::{Config, ProcessTargets, validate_targets}; @@ -107,5 +107,3 @@ fn init_logger(log_level: &str) { }; log_builder.init(); } - - diff --git a/src/model/xmltv.rs b/src/model/xmltv.rs index c73eca2f9..3e3f96cb2 100644 --- a/src/model/xmltv.rs +++ b/src/model/xmltv.rs @@ -1,73 +1,102 @@ -use std::collections::HashSet; -use serde::{Deserialize, Serialize}; -use crate::model::model_config::default_as_empty_str; +use std::collections::{HashMap, HashSet}; +use std::rc::Rc; +use quick_xml::events::{BytesEnd, BytesStart, BytesText, Event}; +use quick_xml::{Error, Writer}; -#[derive(Debug, Clone, Deserialize, Serialize)] -pub(crate) struct Text { - #[serde(rename = "$value")] - #[serde(skip_serializing_if = "Option::is_none")] - value: Option, - #[serde(skip_serializing_if = "Option::is_none")] - lang: Option, +// https://github.com/XMLTV/xmltv/blob/master/xmltv.dtd + + +#[derive(Debug, Clone)] +pub(crate) struct XmlTag { + pub name: String, + pub value: Option, + pub attributes: Option>>, + pub children: Option>>, } -#[derive(Debug, Clone, Deserialize, Serialize)] -pub(crate) struct Channel { - #[serde(rename = "@id", default="default_as_empty_str")] - id: String, - #[serde(rename = "display-name")] - display_name: String, - #[serde(skip_serializing_if = "Option::is_none")] - icon: Option, +impl XmlTag { + pub(crate) fn get_attribute_value(&self, attr_name: &str) -> Option<&String> { + match &self.attributes { + None => None, + Some(attr) => { + attr.get(attr_name) + } + } + } + + fn write_to(&self, writer: &mut Writer) -> Result<(), Error> { + let mut elem = BytesStart::new(self.name.as_str()); + if let Some(attribs) = self.attributes.as_ref() { + attribs.iter().for_each(|(k, v)| elem.push_attribute((k.as_str(), v.as_str()))) + } + writer.write_event(Event::Start(elem))?; + self.value.as_ref().map(|text| writer.write_event(Event::Text(BytesText::new(text.as_str())))); + if let Some(children) = &self.children { + for child in children { + child.write_to(writer)? + } + } + writer.write_event(Event::End(BytesEnd::new(self.name.as_str()))) + } } -#[derive(Debug, Clone, Deserialize, Serialize)] -pub(crate) struct Icon { - #[serde(rename = "@src", default="default_as_empty_str")] - src: String, + +#[derive(Debug, Clone)] +pub(crate) struct Epg { + pub attributes: Option>>, + pub children: Vec>, } -#[derive(Debug, Clone, Deserialize, Serialize)] -pub(crate) struct Programme { - #[serde(rename = "@start", default="default_as_empty_str")] - start: String, - #[serde(rename = "@stop", default="default_as_empty_str")] - stop: String, - #[serde(rename = "@channel", default="default_as_empty_str")] - channel: String, - title: Text, - desc: Text, - #[serde(skip_serializing_if = "Option::is_none")] - category: Option, - #[serde(skip_serializing_if = "Option::is_none", rename="sub-title")] - sub_title: Option, +impl Epg { + pub(crate) fn write_to(&self, writer: &mut Writer) -> Result<(), quick_xml::Error> { + let mut elem = BytesStart::new("tv"); + if let Some(attribs) = self.attributes.as_ref() { + attribs.iter().for_each(|(k, v)| elem.push_attribute((k.as_str(), v.as_str()))) + } + writer.write_event(Event::Start(elem))?; + for child in &self.children { + child.write_to(writer)? + } + writer.write_event(Event::End(BytesEnd::new("tv"))) + } } -#[derive(Debug, Clone, Deserialize, Serialize)] -#[serde(rename = "tv")] +#[derive(Debug, Clone)] pub(crate) struct TVGuide { - #[serde(rename = "channel")] - pub channels: Vec, - #[serde(rename = "programme")] - pub programs: Vec, - #[serde(skip_serializing_if = "Option::is_none", rename="@date")] - pub date: Option, - #[serde(skip_serializing_if = "Option::is_none", alias="generator-info-url", rename="@source-info-url")] - pub source_info_url: Option, - #[serde(skip_serializing_if = "Option::is_none", alias="generator-info-name", rename="@source-info-name")] - pub source_info_name: Option, + pub epg: XmlTag, } impl TVGuide { - pub(crate) fn clear(&mut self, channel_ids: &HashSet) { + pub(crate) fn filter(&self, channel_ids: &HashSet) -> Option { if !channel_ids.is_empty() { - self.channels = self.channels.drain(..).filter(|chan| channel_ids.contains(&chan.id)).collect(); + let children: Vec> = self.epg.children.as_ref().unwrap().iter().filter(|c| { + match c.name.as_str() { + "channel" => { + match c.get_attribute_value("id") { + None => false, + Some(val) => channel_ids.contains(val) + } + } + "programme" => { + match c.get_attribute_value("channel") { + None => false, + Some(val) => channel_ids.contains(val) + } + } + _ => false, + } + }).cloned().collect(); + if children.is_empty() { + None + } else { + Some(Epg { + attributes: self.epg.attributes.clone(), + children, + }) + } + } else { + None } } } -impl TVGuide { - pub fn prepare(&mut self) { - self.channels = self.channels.drain(..).filter(|chan| !chan.id.trim().is_empty()).collect(); - } -} diff --git a/src/processing/playlist_processor.rs b/src/processing/playlist_processor.rs index de66d4ca1..62b8441b1 100644 --- a/src/processing/playlist_processor.rs +++ b/src/processing/playlist_processor.rs @@ -18,7 +18,7 @@ use crate::model::mapping::{Mapping, MappingValueProcessor}; use crate::model::model_config::{AFFIX_FIELDS, ItemField, ProcessingOrder, SortOrder::{Asc, Desc}, TargetType}; use crate::model::model_m3u::{FetchedPlaylist, FieldAccessor, PlaylistGroup, PlaylistItem, PlaylistItemHeader}; use crate::model::stats::{InputStats, PlaylistStats}; -use crate::model::xmltv::TVGuide; +use crate::model::xmltv::{Epg}; use crate::processing::playlist_watch::process_group_watch; use crate::processing::xmltv_parser::flatten_tvguide; use crate::repository::epg_repository::write_epg; @@ -26,17 +26,17 @@ use crate::repository::m3u_repository::{write_m3u_playlist, write_strm_playlist} use crate::repository::xtream_repository::write_xtream_playlist; fn filter_playlist(playlist: &mut [PlaylistGroup], target: &ConfigTarget) -> Option> { - //debug!("Filtering {} groups", playlist.len()); + debug!("Filtering {} groups", playlist.len()); let mut new_playlist = Vec::new(); playlist.iter_mut().for_each(|pg| { - //debug!("Filtering group {} with {} items", pg.title, pg.channels.len()); + debug!("Filtering group {} with {} items", pg.title, pg.channels.len()); let mut channels = Vec::new(); pg.channels.iter_mut().for_each(|pli| { if is_valid(pli, target) { channels.push(pli.clone()); } }); - // debug!("Filtered group {} has now {} items", pg.title, channels.len()); + debug!("Filtered group {} has now {} items", pg.title, channels.len()); if !channels.is_empty() { new_playlist.push(PlaylistGroup { id: pg.id, @@ -150,7 +150,7 @@ fn exec_rename(pli: &mut PlaylistItem, rename: &Option for r in renames { let value = get_field_value(result, &r.field); let cap = r.re.as_ref().unwrap().replace_all(value.as_str(), &r.new_name); - //debug!("Renamed {}={} to {}", &r.field, value, cap); + debug!("Renamed {}={} to {}", &r.field, value, cap); let value = cap.into_owned(); set_field_value(result, &r.field, value); } @@ -168,7 +168,7 @@ fn rename_playlist(playlist: &mut [PlaylistGroup], target: &ConfigTarget) -> Opt for r in renames { if let ItemField::Group = r.field { let cap = r.re.as_ref().unwrap().replace_all(&grp.title, &r.new_name); - //debug!("Renamed group {} to {} for {}", &grp.title, cap, target.name); + debug!("Renamed group {} to {} for {}", &grp.title, cap, target.name); grp.title = cap.into_owned(); } } @@ -428,19 +428,20 @@ pub(crate) fn process_playlist(playlists: &mut [FetchedPlaylist], apply_affixes(&mut new_fetched_playlists); let mut new_playlist = vec![]; let mut new_epg = vec![]; + let mut tv_guides = vec![]; new_fetched_playlists.drain(..).for_each(|mut fp| { fp.playlist.drain(..).for_each(|group| new_playlist.push(group)); - match fp.epg { - None => {} - Some(mut epg) => { - debug!("found epg information for {}", &target.name); - let channel_ids: HashSet<_> = new_playlist.iter().flat_map(|g| &g.channels).map(|c| c.header.borrow().id.to_owned()).collect(); - if !channel_ids.is_empty() { - epg.clear(&channel_ids); + if let Some(tv_guide) = fp.epg { + tv_guides.push(tv_guide); + let guide = tv_guides.last().unwrap(); + debug!("found epg information for {}", &target.name); + let channel_ids: HashSet<_> = new_playlist.iter().flat_map(|g| &g.channels).map(|c| c.header.borrow().id.to_owned()).collect(); + if !channel_ids.is_empty() { + if let Some(epg) = guide.filter(&channel_ids) { new_epg.push(epg); - } else { - debug!("channel ids are empty"); } + } else { + debug!("channel ids are empty"); } } }); @@ -461,14 +462,14 @@ pub(crate) fn process_playlist(playlists: &mut [FetchedPlaylist], } } - persist_playlist(&new_playlist, flatten_tvguide(&mut new_epg), target, cfg) + persist_playlist(&new_playlist, flatten_tvguide(&new_epg), target, cfg) } else { info!("Playlist is empty: {}", &target.name); Ok(()) } } -fn persist_playlist(playlist: &[PlaylistGroup], epg: Option, +fn persist_playlist(playlist: &[PlaylistGroup], epg: Option, target: &ConfigTarget, cfg: &Config) -> Result<(), Vec> { let mut errors = vec![]; for output in &target.output { diff --git a/src/processing/xmltv_parser.rs b/src/processing/xmltv_parser.rs index be77532e5..a43c406b2 100644 --- a/src/processing/xmltv_parser.rs +++ b/src/processing/xmltv_parser.rs @@ -1,46 +1,123 @@ -use log::debug; -use crate::m3u_filter_error::{M3uFilterError, M3uFilterErrorKind}; -use crate::model::xmltv::TVGuide; +use std::collections::HashMap; +use std::rc::Rc; +use quick_xml::events::Event; +use quick_xml::Reader; +use crate::model::xmltv::{Epg, TVGuide, XmlTag}; -pub(crate) fn parse_tvguide(content: &str) -> (Option, Vec) { - match serde_xml_rs::from_str::(content) { - Ok(mut tv_guide) => { - tv_guide.prepare(); - (Some(tv_guide), vec![]) +pub(crate) fn parse_tvguide(content: &str) -> Option { + let mut stack: Vec = vec![]; + let mut reader = Reader::from_str(content); + reader.trim_text(true); + let mut buf = Vec::::new(); + loop { + match reader.read_event_into(&mut buf) { + Ok(Event::Eof) => break, + Ok(Event::Start(e)) => { + let name = String::from_utf8_lossy(e.name().as_ref()).to_string(); + let attributes = e.attributes().filter_map(|a| a.ok()) + .filter_map(|a| { + let key = String::from_utf8_lossy(a.key.as_ref()).to_string(); + let value = String::from(a.unescape_value().unwrap().as_ref()).to_string(); + if !value.is_empty() { + Some((key, value)) + } else { + None + } + }).collect::>(); + let tag = XmlTag { + name, + value: None, + attributes: if attributes.is_empty() { None } else { Some(Rc::new(attributes)) }, + children: None, + }; + + stack.push(tag); + } + Ok(Event::End(_e)) => { + if stack.len() > 1 { + if let Some(tag) = stack.pop() { + if let Some(old_tag) = stack.pop().map(|mut r| { + let rc_tag = Rc::new(tag); + r.children = Some( + r.children.map_or(vec![rc_tag.clone()], + |mut c| { + c.push(rc_tag.clone()); + c + })); + r + }) { + stack.push(old_tag); + } + } + } + } + Ok(Event::Text(e)) => { + let text = e.unescape().unwrap().into_owned(); + if !text.is_empty() { + stack.last_mut().unwrap().value = Some(text); + } + } + _ => {} } - Err(err) => (None, vec![M3uFilterError::new(M3uFilterErrorKind::Notify, format!("Failed to download epg: {}", err))]) } + stack.pop().map(|epg| TVGuide { + epg, + }) } -pub(crate) fn flatten_tvguide(tvguides: &mut Vec) -> Option { - if tvguides.is_empty() { - debug!("tvguides are empty"); +pub(crate) fn flatten_tvguide(tv_guides: &[Epg]) -> Option { + if tv_guides.is_empty() { None - } else if tvguides.len() == 1 { - Some(tvguides.remove(0)) } else { - let mut guide = TVGuide { - channels: vec![], - programs: vec![], - date: None, - source_info_url: None, - source_info_name: None, + let mut epg = Epg { + attributes: None, + children: vec![], }; - tvguides.drain(..).for_each(|mut g| { - if guide.date.is_none() { - guide.date = g.date.to_owned(); - } - if guide.source_info_url.is_none() { - guide.source_info_url = g.source_info_url.to_owned(); - } - if guide.source_info_name.is_none() { - guide.source_info_name = g.source_info_name.to_owned(); - } - g.channels.drain(..).for_each(|c| guide.channels.push(c)); - g.programs.drain(..).for_each(|c| guide.programs.push(c)); - }); + let mut channel_ids: Vec<&String> = vec![]; + let mut program_ids: Vec<&String> = vec![]; - Some(guide) + tv_guides.iter().for_each(|guide| { + if epg.attributes.is_none() { + epg.attributes = guide.attributes.clone(); + } + guide.children.iter().for_each(|c| { + let add_child = match c.name.as_str() { + "channel" => { + let chan_id = c.get_attribute_value("id"); + match chan_id { + Some(id) => { + if channel_ids.contains(&id) { + false + } else { + channel_ids.push(id); + true + } + }, + None => false, + } + }, + "programme" => { + let chan_id = c.get_attribute_value("channel"); + match chan_id { + Some(id) => { + if program_ids.contains(&id) { + false + } else { + program_ids.push(id); + true + } + }, + None => false, + } + } + _ => false, + }; + if add_child { + epg.children.push(c.clone()) + } + }); + }); + Some(epg) } } \ No newline at end of file diff --git a/src/repository/epg_repository.rs b/src/repository/epg_repository.rs index 641600c33..151c5841f 100644 --- a/src/repository/epg_repository.rs +++ b/src/repository/epg_repository.rs @@ -1,24 +1,34 @@ use std::fs::File; -use std::io::Write; +use std::io::{Cursor, Write}; use std::path::{Path}; use log::debug; +use quick_xml::{Writer}; use crate::m3u_filter_error::{M3uFilterError, M3uFilterErrorKind}; use crate::model::config::{Config, ConfigTarget, TargetOutput}; use crate::model::model_config::TargetType; -use crate::model::xmltv::TVGuide; +use crate::model::xmltv::{Epg}; use crate::repository::m3u_repository::{get_m3u_epg_file_path}; use crate::repository::xtream_repository::{get_xtream_epg_file_path, get_xtream_storage_path}; -fn write_epg_file(target: &ConfigTarget, xml_content: &String, path: &Path) -> Result<(), M3uFilterError> { - match File::create(path) { - Ok(mut epg_file) => { - match epg_file.write_all("".as_bytes()) { - Ok(_) => {} - Err(_) => {} - } - match epg_file.write_all(xml_content.as_bytes()) { - Ok(_) => { - debug!("Epg for target {} written to {}", target.name, path.to_str().unwrap_or("?")) +fn write_epg_file(target: &ConfigTarget, epg: &Epg, path: &Path) -> Result<(), M3uFilterError> { + let mut writer = Writer::new(Cursor::new(vec![])); + match epg.write_to(&mut writer) { + Ok(_) => { + let result = writer.into_inner().into_inner(); + match File::create(path) { + Ok(mut epg_file) => { + match epg_file.write_all("".as_bytes()) { + Ok(_) => {} + Err(err) => return Err(M3uFilterError::new( + M3uFilterErrorKind::Notify, format!("failed to write epg: {} - {}", path.to_str().unwrap_or("?"), err))), + } + match epg_file.write_all(&result) { + Ok(_) => { + debug!("Epg for target {} written to {}", target.name, path.to_str().unwrap_or("?")) + } + Err(err) => return Err(M3uFilterError::new( + M3uFilterErrorKind::Notify, format!("failed to write epg: {} - {}", path.to_str().unwrap_or("?"), err))), + } } Err(err) => return Err(M3uFilterError::new( M3uFilterErrorKind::Notify, format!("failed to write epg: {} - {}", path.to_str().unwrap_or("?"), err))), @@ -30,40 +40,33 @@ fn write_epg_file(target: &ConfigTarget, xml_content: &String, path: &Path) -> R Ok(()) } -pub(crate) fn write_epg(target: &ConfigTarget, cfg: &Config, tv_guide: &Option, output: &TargetOutput) -> Result<(), M3uFilterError> { - if let Some(epg_data) = tv_guide { - match quick_xml::se::to_string(&epg_data) { - Ok(xml_content) => { - debug!("serde to xml ok "); - match &output.target { - TargetType::M3u => { - if output.filename.is_none() { - return Err(M3uFilterError::new( - M3uFilterErrorKind::Notify, - format!("write epg for target {} failed: No filename set", target.name))); - } - if let Some(path) = get_m3u_epg_file_path(cfg, &output.filename) { - debug!("writing m3u epg to {}", path.to_str().unwrap_or("?")); - write_epg_file(target, &xml_content, &path)? - } - } - TargetType::Xtream => { - match get_xtream_storage_path(cfg, &target.name) { - Some(path) => { - let epg_path = get_xtream_epg_file_path(&path); - debug!("writing xtream epg to {}", epg_path.to_str().unwrap_or("?")); - write_epg_file(target, &xml_content, &epg_path)? - } - None => return Err(M3uFilterError::new( - M3uFilterErrorKind::Notify, - format!("failed to serialize epg for target: {}, storage path not found", target.name))), - } - } - TargetType::Strm => {} +pub(crate) fn write_epg(target: &ConfigTarget, cfg: &Config, epg: &Option, output: &TargetOutput) -> Result<(), M3uFilterError> { + if let Some(epg_data) = epg { + match &output.target { + TargetType::M3u => { + if output.filename.is_none() { + return Err(M3uFilterError::new( + M3uFilterErrorKind::Notify, + format!("write epg for target {} failed: No filename set", target.name))); } - }, - Err(err) => return Err(M3uFilterError::new( - M3uFilterErrorKind::Notify, format!("failed to serialize epg for target: {} - {}", target.name, err))), + if let Some(path) = get_m3u_epg_file_path(cfg, &output.filename) { + debug!("writing m3u epg to {}", path.to_str().unwrap_or("?")); + write_epg_file(target, epg_data, &path)? + } + } + TargetType::Xtream => { + match get_xtream_storage_path(cfg, &target.name) { + Some(path) => { + let epg_path = get_xtream_epg_file_path(&path); + debug!("writing xtream epg to {}", epg_path.to_str().unwrap_or("?")); + write_epg_file(target, epg_data, &epg_path)? + } + None => return Err(M3uFilterError::new( + M3uFilterErrorKind::Notify, + format!("failed to serialize epg for target: {}, storage path not found", target.name))), + } + } + TargetType::Strm => {} } } Ok(())