2025-06-09 10:30:52 +02:00
use crate ::model ::{ Epg , TVGuide , XmlTag , XmlTagIcon , EPG_ATTRIB_CHANNEL , EPG_ATTRIB_ID , EPG_TAG_CHANNEL , EPG_TAG_DISPLAY_NAME , EPG_TAG_ICON , EPG_TAG_PROGRAMME , EPG_TAG_TV };
2025-06-24 01:06:25 +02:00
use crate ::model ::{ EpgSmartMatchConfig , PersistedEpgSource };
2025-04-25 16:13:44 +02:00
use crate ::processing ::processor ::epg ::EpgIdCache ;
2026-01-04 16:36:35 +01:00
use crate ::utils ::async_file_reader ;
use crate ::utils ::compressed_file_reader_async ::CompressedFileReaderAsync ;
2025-07-22 11:32:46 +02:00
use dashmap ::DashMap ;
2025-04-11 20:25:09 +02:00
use deunicode ::deunicode ;
2025-05-16 15:27:04 +02:00
use quick_xml ::events ::{ BytesStart , BytesText , Event };
2025-04-12 14:14:06 +02:00
use rayon ::iter ::{ IntoParallelRefIterator , ParallelIterator };
2025-06-24 01:06:25 +02:00
use shared ::model ::EpgNamePrefix ;
use shared ::utils ::CONSTANTS ;
2025-04-11 20:25:09 +02:00
use std ::borrow ::Cow ;
2026-01-04 16:36:35 +01:00
use std ::cell ::RefCell ;
2025-04-12 14:14:06 +02:00
use std ::cmp ::min ;
2025-05-16 08:59:52 +02:00
use std ::collections ::HashMap ;
2025-04-12 14:14:06 +02:00
use std ::mem ;
2026-01-04 16:17:47 +01:00
use std ::sync ::{ Arc , Mutex };
2026-01-04 16:36:35 +01:00
use tokio ::io ::AsyncRead ;
2026-01-04 16:17:47 +01:00
struct EpgInterner {
2026-01-04 16:36:35 +01:00
map : RefCell < HashMap < String , Arc < str >>> ,
2026-01-04 16:17:47 +01:00
}
impl EpgInterner {
fn new () -> Self {
2026-01-04 16:36:35 +01:00
Self { map : RefCell ::new ( HashMap ::new ()) }
2026-01-04 16:17:47 +01:00
}
fn intern ( & self , s : & str ) -> Arc < str > {
2026-01-04 16:36:35 +01:00
let mut map = self . map . borrow_mut ();
if let Some ( arc ) = map . get ( s ) {
return arc . clone ();
}
let arc : Arc < str > = Arc ::from ( s );
map . insert ( s . to_string (), arc . clone ());
arc
2026-01-04 16:17:47 +01:00
}
}
2025-04-12 14:14:06 +02:00
2025-04-25 14:19:28 +00:00
/// Splits a string at the first delimiter if the prefix matches a known country code.
///
/// Returns a tuple containing the country code prefix (if found) and the remainder of the string, both trimmed. If no valid prefix is found, returns `None` and the original input.
///
/// # Examples
///
/// ```
/// let delimiters = vec!['.', '-', '_'];
/// let (prefix, rest) = split_by_first_match("US.HBO", &delimiters);
/// assert_eq!(prefix, Some("US"));
/// assert_eq!(rest, "HBO");
///
/// let (prefix, rest) = split_by_first_match("HBO", &delimiters);
/// assert_eq!(prefix, None);
/// assert_eq!(rest, "HBO");
/// ```
2025-04-12 14:14:06 +02:00
fn split_by_first_match < 'a > ( input : & 'a str , delimiters : & [ char ]) -> ( Option <& 'a str > , & 'a str ) {
2025-05-18 13:40:20 +02:00
let content = input . trim_start_matches ( | c : char | ! c . is_alphanumeric ());
2025-04-12 14:14:06 +02:00
for delim in delimiters {
2025-05-18 13:40:20 +02:00
if let Some ( index ) = content . find ( * delim ) {
let ( left , right ) = content . split_at ( index );
2025-04-12 14:14:06 +02:00
let right = & right [ delim . len_utf8 () .. ]. trim ();
if ! right . is_empty () {
2025-04-15 18:38:18 +02:00
let prefix = left . trim ();
2025-04-17 12:11:51 +02:00
if CONSTANTS . country_codes . contains ( & prefix ) {
2025-04-15 18:38:18 +02:00
return ( Some ( prefix ), right . trim ());
}
2025-04-12 14:14:06 +02:00
}
}
2025-04-11 20:25:09 +02:00
}
2025-04-12 14:14:06 +02:00
( None , input )
2025-04-11 20:25:09 +02:00
}
2025-04-06 16:50:43 +02:00
2025-05-18 13:40:20 +02:00
2025-04-12 14:14:06 +02:00
fn name_prefix < 'a > ( name : & 'a str , smart_config : & EpgSmartMatchConfig ) -> ( & 'a str , Option <& 'a str > ) {
if smart_config . name_prefix != EpgNamePrefix ::Ignore {
2025-06-24 01:06:25 +02:00
let ( prefix , suffix ) = split_by_first_match ( name , & smart_config . name_prefix_separator );
2025-04-11 20:25:09 +02:00
if prefix . is_some () {
2025-04-12 14:14:06 +02:00
return ( suffix , prefix );
2025-04-11 20:25:09 +02:00
}
}
( name , None )
}
2025-04-06 16:50:43 +02:00
2025-04-11 20:25:09 +02:00
fn combine ( join : & str , left : & str , right : & str ) -> String {
let mut combined = String ::with_capacity ( left . len () + join . len () + right . len ());
combined . push_str ( left );
2025-10-01 17:06:41 +02:00
combined . push_str ( join );
2025-04-11 20:25:09 +02:00
combined . push_str ( right );
combined
}
/// # Panics
2025-04-12 14:14:06 +02:00
pub fn normalize_channel_name ( name : & str , normalize_config : & EpgSmartMatchConfig ) -> String {
let normalized = deunicode ( name . trim ()). to_lowercase ();
let ( channel_name , suffix ) = name_prefix ( & normalized , normalize_config );
2025-04-11 20:25:09 +02:00
// Remove all non-alphanumeric characters (except dashes and underscores).
2025-06-24 01:06:25 +02:00
let cleaned_name = normalize_config . normalize_regex . replace_all ( channel_name , "" );
2025-04-06 16:50:43 +02:00
// Remove terms like resolution
2025-06-24 01:06:25 +02:00
let cleaned_name = normalize_config . strip . iter (). fold ( cleaned_name . to_string (), | acc , term | {
2025-04-11 20:25:09 +02:00
acc . replace ( term , "" )
2025-04-06 16:50:43 +02:00
});
2025-04-11 20:25:09 +02:00
match suffix {
None => cleaned_name ,
Some ( sfx ) => {
2025-04-12 14:14:06 +02:00
match & normalize_config . name_prefix {
EpgNamePrefix ::Ignore => cleaned_name ,
EpgNamePrefix ::Suffix ( sep ) => combine ( sep , & cleaned_name , sfx ),
EpgNamePrefix ::Prefix ( sep ) => combine ( sep , sfx , & cleaned_name ),
2025-04-11 20:25:09 +02:00
}
}
}
2025-04-06 16:50:43 +02:00
}
2024-11-02 16:44:38 +01:00
2025-06-09 10:30:52 +02:00
2024-11-02 16:44:38 +01:00
impl TVGuide {
2026-01-04 16:17:47 +01:00
pub fn merge ( epgs : Vec < Epg > ) -> Option < Epg > {
2026-01-04 16:24:08 +01:00
if let Some ( first_epg ) = epgs . first () {
2026-01-04 16:17:47 +01:00
let first_epg_attributes = first_epg . attributes . clone ();
let merged_children : Vec < Arc < XmlTag >> = epgs . into_iter (). flat_map ( | epg | epg . children ). collect ();
2025-05-16 08:59:52 +02:00
Some ( Epg {
logo_override : false ,
priority : 0 ,
attributes : first_epg_attributes ,
children : merged_children ,
})
} else {
None
2024-11-02 16:44:38 +01:00
}
2025-04-10 18:08:11 +02:00
}
2025-04-12 14:14:06 +02:00
fn prepare_tag ( id_cache : & mut EpgIdCache , tag : & mut XmlTag , smart_match : bool ) {
2025-08-18 20:21:58 +02:00
{
let maybe_epg_id = {
tag . get_attribute_value ( EPG_ATTRIB_ID ). cloned ()
};
if let Some ( epg_id ) = maybe_epg_id {
2026-01-04 16:36:35 +01:00
tag . normalized_epg_ids
2025-08-18 20:21:58 +02:00
. get_or_insert_with ( Vec ::new )
. push ( normalize_channel_name ( & epg_id , & id_cache . smart_match_config ));
}
}
2026-01-04 16:17:47 +01:00
if let Some ( children ) = & tag . children {
2025-04-12 14:14:06 +02:00
for child in children {
2026-01-04 16:17:47 +01:00
match child . name . as_ref () {
2025-04-12 14:14:06 +02:00
EPG_TAG_DISPLAY_NAME => {
if smart_match {
if let Some ( name ) = & child . value {
2025-06-05 20:06:04 +02:00
tag . normalized_epg_ids
. get_or_insert_with ( Vec ::new )
. push ( normalize_channel_name ( name , & id_cache . smart_match_config ));
2025-04-12 14:14:06 +02:00
}
}
}
EPG_TAG_ICON => {
if let Some ( src ) = child . get_attribute_value ( "src" ) {
2025-06-05 20:06:04 +02:00
if ! src . is_empty () {
2025-08-26 13:53:11 +02:00
tag . icon = XmlTagIcon ::Src ( src . clone ());
2026-01-04 16:17:47 +01:00
// We cannot easily modify the child icon since it's inside Arc,
// but we already set the tag.icon, which is what matters.
2025-06-05 20:06:04 +02:00
}
2025-04-12 14:14:06 +02:00
}
}
_ => {}
}
}
}
}
fn try_fuzzy_matching ( id_cache : & mut EpgIdCache , epg_id : & str , tag : & XmlTag , fuzzy_matching : bool ) -> bool {
2025-06-05 20:06:04 +02:00
let mut matched = tag
. normalized_epg_ids
. as_ref ()
2025-06-07 10:11:19 +02:00
. is_some_and ( | ids | id_cache . match_with_normalized ( epg_id , ids ));
2025-04-12 14:14:06 +02:00
if ! matched && fuzzy_matching {
2025-04-23 20:21:36 +02:00
let ( fuzzy_matched , matched_normalized_name ) = Self ::find_best_fuzzy_match ( id_cache , tag );
if fuzzy_matched {
2025-07-22 11:32:46 +02:00
if let Some ( key ) = matched_normalized_name {
let id = epg_id . to_string ();
id_cache . normalized . entry ( key ). and_modify ( | entry | {
entry . replace ( id . clone ());
id_cache . channel_epg_id . insert ( Cow ::Owned ( id ));
matched = true ;
});
}
2025-04-12 14:14:06 +02:00
}
}
matched
}
2025-04-25 14:19:28 +00:00
/// Finds the best fuzzy match for a channel's normalized EPG ID using phonetic encoding and Jaro-Winkler similarity.
///
/// Iterates over the tag's normalized EPG IDs, computes their phonetic codes, and searches for candidates in the phonetics map.
/// For each candidate, calculates the Jaro-Winkler similarity score and tracks the best match above the configured threshold.
/// Returns a tuple indicating whether a suitable match was found and the matched normalized EPG ID if available.
///
/// # Returns
///
/// A tuple where the first element is `true` if a match above the threshold was found, and the second element is the matched normalized EPG ID.
///
/// # Examples
///
/// ```
/// let (found, matched) = find_best_fuzzy_match(&mut id_cache, &tag);
/// if found {
/// println!("Best match: {:?}", matched);
/// }
/// ```
2025-04-12 14:14:06 +02:00
fn find_best_fuzzy_match ( id_cache : & mut EpgIdCache , tag : & XmlTag ) -> ( bool , Option < String > ) {
let match_threshold = id_cache . smart_match_config . match_threshold ;
let best_match_threshold = id_cache . smart_match_config . best_match_threshold ;
2025-08-18 20:21:58 +02:00
let Some ( normalized_epg_ids ) = tag . normalized_epg_ids . as_ref () else {
return ( false , None );
};
// 1) Precalculation: (tag_normalized, tag_code)
let pre : Vec < ( & str , String ) > = normalized_epg_ids
. iter ()
. map ( | tn | ( tn . as_str (), id_cache . phonetic ( tn )))
. collect ();
// 2) Early exit if match >= best_match_threshold
for ( tag_normalized , tag_code ) in & pre {
if let Some ( candidates ) = id_cache . phonetics . get ( tag_code ) {
if let Some ( good_enough ) = candidates . par_iter (). find_any ( | norm_key | {
let jw = strsim ::jaro_winkler ( norm_key . as_str (), tag_normalized );
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
let score = min ( 100 , ( jw * 100.0 ). round () as u16 );
score >= best_match_threshold
}) {
2025-08-26 13:53:11 +02:00
return ( true , Some ( good_enough . clone ()));
2025-06-05 20:06:04 +02:00
}
2025-04-12 14:14:06 +02:00
}
2025-04-25 16:13:44 +02:00
}
2025-08-18 20:21:58 +02:00
// 3) No full match: find best match with match_threshold
let best = pre
. par_iter ()
. filter_map ( | ( tag_normalized , tag_code ) | {
id_cache . phonetics . get ( tag_code ). map ( | candidates | {
candidates
. par_iter ()
. map ( | norm_key | {
let jw = strsim ::jaro_winkler ( norm_key . as_str (), tag_normalized );
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
let score = min ( 100 , ( jw * 100.0 ). round () as u16 );
( score , norm_key . as_str ())
})
. reduce_with ( | a , b | if a . 0 >= b . 0 { a } else { b })
})
})
. flatten ()
. reduce_with ( | a , b | if a . 0 >= b . 0 { a } else { b });
if let Some (( score , best_key )) = best {
if score >= match_threshold {
return ( true , Some ( best_key . to_string ()));
2025-07-22 11:32:46 +02:00
}
2025-04-12 14:14:06 +02:00
}
2025-08-18 20:21:58 +02:00
2025-04-12 14:14:06 +02:00
( false , None )
}
2025-04-25 14:19:28 +00:00
/// Parses and filters a compressed EPG XML file, extracting relevant channel and program tags based on smart and fuzzy matching criteria.
///
/// Returns an `Epg` containing filtered tags and TV attributes if any matching channels are found; otherwise, returns `None`.
2025-05-14 15:54:54 +02:00
/// The returned `Epg` will include the priority from the source, which is used for merging multiple EPG sources.
2025-04-25 14:19:28 +00:00
///
/// # Examples
///
/// ```
/// let mut id_cache = EpgIdCache::default();
2025-05-14 15:42:07 +02:00
/// let epg_source = PersistedEpgSource { file_path: Path::new("guide.xml.gz"), priority: 0 };
/// if let Some(epg) = process_epg_file(&mut id_cache, &epg_source) {
2025-04-25 14:19:28 +00:00
/// assert!(!epg.children.is_empty());
/// }
/// ```
2025-10-01 14:40:44 +02:00
async fn process_epg_file ( id_cache : & mut EpgIdCache < '_ > , epg_source : & PersistedEpgSource ) -> Option < Epg > {
match CompressedFileReaderAsync ::new ( & epg_source . file_path ). await {
2024-11-02 16:44:38 +01:00
Ok ( mut reader ) => {
2026-01-04 16:17:47 +01:00
let mut children : Vec < Arc < XmlTag >> = vec! [];
2025-04-12 14:14:06 +02:00
let mut tv_attributes : Option < HashMap < String , String >> = None ;
let smart_match = id_cache . smart_match_config . enabled ;
let fuzzy_matching = smart_match && id_cache . smart_match_config . fuzzy_matching ;
let mut filter_tags = | mut tag : XmlTag | {
2026-01-04 16:17:47 +01:00
match tag . name . as_ref () {
2024-11-02 16:44:38 +01:00
EPG_TAG_CHANNEL => {
2025-08-18 20:21:58 +02:00
let tag_epg_id = tag . get_attribute_value ( EPG_ATTRIB_ID ). map_or_else ( String ::new , std ::string ::ToString ::to_string );
if ! tag_epg_id . is_empty () && ! id_cache . processed . contains ( & tag_epg_id ) {
2025-04-25 16:13:44 +02:00
Self ::prepare_tag ( id_cache , & mut tag , smart_match );
if smart_match {
2025-08-18 20:21:58 +02:00
if Self ::try_fuzzy_matching ( id_cache , & tag_epg_id , & tag , fuzzy_matching ) {
2026-01-04 16:17:47 +01:00
children . push ( Arc ::new ( tag ));
2025-08-18 20:21:58 +02:00
id_cache . processed . insert ( tag_epg_id );
2025-04-25 16:13:44 +02:00
}
} else {
2025-08-18 20:21:58 +02:00
let borrowed_tag_epg_id = Cow ::Borrowed ( tag_epg_id . as_str ());
if id_cache . channel_epg_id . contains ( & borrowed_tag_epg_id ) {
2026-01-04 16:17:47 +01:00
children . push ( Arc ::new ( tag ));
2025-08-18 20:21:58 +02:00
id_cache . processed . insert ( tag_epg_id );
2025-04-10 18:08:11 +02:00
}
2025-04-09 20:51:25 +02:00
}
}
2024-11-02 16:44:38 +01:00
}
EPG_TAG_PROGRAMME => {
2025-04-09 20:51:25 +02:00
if let Some ( epg_id ) = tag . get_attribute_value ( EPG_ATTRIB_CHANNEL ) {
2025-04-25 16:13:44 +02:00
if id_cache . processed . contains ( epg_id ) {
2025-04-10 18:08:11 +02:00
let borrowed_epg_id = Cow ::Borrowed ( epg_id . as_str ());
2025-04-23 20:21:36 +02:00
if id_cache . channel_epg_id . contains ( & borrowed_epg_id ) {
2026-01-04 16:17:47 +01:00
children . push ( Arc ::new ( tag ));
2025-04-10 18:08:11 +02:00
}
2025-04-09 20:51:25 +02:00
}
}
}
2024-11-02 16:44:38 +01:00
EPG_TAG_TV => {
2026-01-04 16:17:47 +01:00
tv_attributes = tag . attributes . as_ref (). map ( | attrs | {
attrs . iter (). map ( | ( k , v ) | ( k . to_string (), v . clone ())). collect ::< HashMap < String , String >> ()
});
2025-04-09 20:51:25 +02:00
}
_ => {}
2025-04-10 00:06:48 +02:00
}
2024-11-02 16:44:38 +01:00
};
2025-10-01 14:40:44 +02:00
parse_tvguide ( & mut reader , & mut filter_tags ). await ;
2024-11-02 16:44:38 +01:00
if children . is_empty () {
return None ;
}
2025-04-10 18:08:11 +02:00
2024-11-02 16:44:38 +01:00
Some ( Epg {
2025-05-15 16:40:52 +02:00
logo_override : epg_source . logo_override ,
2025-05-14 15:42:07 +02:00
priority : epg_source . priority ,
2024-11-02 16:44:38 +01:00
attributes : tv_attributes ,
children ,
})
}
2026-01-04 16:36:35 +01:00
Err ( e ) => {
log ::warn! ( "Failed to process EPG file {}: {e}" , epg_source . file_path . display ());
None
}
2024-11-02 16:44:38 +01:00
}
}
2025-04-10 18:08:11 +02:00
2025-10-01 14:40:44 +02:00
pub async fn filter ( & self , id_cache : & mut EpgIdCache < '_ > ) -> Option < Vec < Epg >> {
2025-04-23 20:21:36 +02:00
if id_cache . channel_epg_id . is_empty () && id_cache . normalized . is_empty () {
2025-04-10 18:08:11 +02:00
return None ;
}
2025-10-01 14:40:44 +02:00
let mut epg_sources : Vec < Epg > = vec! [];
for epg_source in self . get_epg_sources () {
if let Some ( epg ) = Self ::process_epg_file ( id_cache , epg_source ). await {
epg_sources . push ( epg );
}
}
2025-05-16 08:59:52 +02:00
epg_sources . sort_by ( | a , b | a . priority . cmp ( & b . priority ));
Some ( epg_sources )
2025-04-10 18:08:11 +02:00
}
2024-11-02 16:44:38 +01:00
}
2024-05-10 10:13:50 +02:00
2025-04-12 14:14:06 +02:00
2026-01-04 16:17:47 +01:00
fn handle_tag_start < F > ( callback : & mut F , stack : & mut Vec < XmlTag > , e : & BytesStart , interner : & EpgInterner )
2024-11-02 16:44:38 +01:00
where
F : FnMut ( XmlTag ),
{
2026-01-04 16:17:47 +01:00
let binding = e . name ();
let name_raw = String ::from_utf8_lossy ( binding . as_ref ());
let name = interner . intern ( name_raw . as_ref ());
2025-04-12 14:14:06 +02:00
let ( is_tv_tag , is_channel , is_program ) = get_tag_types ( & name );
2026-01-04 16:17:47 +01:00
let attributes = collect_tag_attributes ( e , is_channel , is_program , interner );
2025-04-12 14:14:06 +02:00
let attribs = if attributes . is_empty () { None } else { Some ( attributes ) };
let tag = XmlTag ::new ( name , attribs );
2023-10-28 22:20:57 +02:00
2025-04-12 14:14:06 +02:00
if is_tv_tag {
callback ( tag );
} else {
stack . push ( tag );
}
}
2025-04-06 16:50:43 +02:00
2025-04-12 14:14:06 +02:00
fn handle_tag_end < F > ( callback : & mut F , stack : & mut Vec < XmlTag > )
where
F : FnMut ( XmlTag ),
{
if ! stack . is_empty () {
if let Some ( tag ) = stack . pop () {
2026-01-04 16:17:47 +01:00
if tag . name . as_ref () == EPG_TAG_CHANNEL {
2025-04-12 14:14:06 +02:00
if let Some ( chan_id ) = tag . get_attribute_value ( EPG_ATTRIB_ID ) {
if ! chan_id . is_empty () {
callback ( tag );
2023-10-28 22:20:57 +02:00
}
}
2026-01-04 16:17:47 +01:00
} else if tag . name . as_ref () == EPG_TAG_PROGRAMME {
2025-04-12 14:14:06 +02:00
if let Some ( chan_id ) = tag . get_attribute_value ( EPG_ATTRIB_CHANNEL ) {
if ! chan_id . is_empty () {
callback ( tag );
2025-04-09 21:30:04 +02:00
}
2023-10-28 22:20:57 +02:00
}
2025-04-12 14:14:06 +02:00
} else if ! stack . is_empty () {
2026-01-04 16:17:47 +01:00
let tag_arc = Arc ::new ( tag );
if let Some ( mut parent ) = stack . pop () {
2026-01-04 16:27:59 +01:00
parent . children . get_or_insert_with ( Vec ::new ). push ( tag_arc );
2026-01-04 16:17:47 +01:00
stack . push ( parent );
2025-04-12 14:14:06 +02:00
}
2023-10-28 22:20:57 +02:00
}
2025-04-12 14:14:06 +02:00
}
}
}
fn handle_text_tag ( stack : & mut [ XmlTag ], e : & BytesText ) {
2025-10-01 14:40:44 +02:00
if let Some ( tag ) = stack . last_mut () {
2025-09-20 17:02:06 +02:00
if let Ok ( text ) = e . decode () {
2025-04-12 14:14:06 +02:00
let t = text . trim ();
if ! t . is_empty () {
2025-10-01 14:40:44 +02:00
let t_fixed : Cow < str > = if t . ends_with ( '\\' ) {
let mut owned = t . to_string ();
owned . pop ();
2025-10-24 18:36:13 +02:00
owned . push_str ( "' " );
2025-10-01 14:40:44 +02:00
Cow ::Owned ( owned )
} else {
Cow ::Borrowed ( t )
};
let old = tag . value . get_or_insert_with ( String ::new );
old . push_str ( & t_fixed );
2025-04-12 14:14:06 +02:00
}
}
}
}
2025-10-01 14:40:44 +02:00
pub async fn parse_tvguide < R , F > ( content : R , callback : & mut F )
2025-04-12 14:14:06 +02:00
where
2025-10-01 14:40:44 +02:00
R : AsyncRead + Unpin ,
2025-04-12 14:14:06 +02:00
F : FnMut ( XmlTag ),
{
let mut stack : Vec < XmlTag > = vec! [];
2025-12-11 15:27:48 +01:00
let mut xml_reader = quick_xml ::reader ::Reader ::from_reader ( async_file_reader ( content ));
2025-04-12 14:14:06 +02:00
let mut buf = Vec ::< u8 > ::new ();
2026-01-04 16:17:47 +01:00
let interner = EpgInterner ::new ();
2025-04-12 14:14:06 +02:00
loop {
2025-10-01 14:40:44 +02:00
match xml_reader . read_event_into_async ( & mut buf ). await {
2025-04-12 14:14:06 +02:00
Ok ( Event ::Eof ) => break ,
2026-01-04 16:17:47 +01:00
Ok ( Event ::Start ( e )) => handle_tag_start ( callback , & mut stack , & e , & interner ),
2025-05-16 15:20:57 +02:00
Ok ( Event ::Empty ( e )) => {
2026-01-04 16:17:47 +01:00
handle_tag_start ( callback , & mut stack , & e , & interner );
2025-05-16 15:20:57 +02:00
handle_tag_end ( callback , & mut stack );
2025-06-24 01:06:25 +02:00
}
2025-04-12 14:14:06 +02:00
Ok ( Event ::End ( _e )) => handle_tag_end ( callback , & mut stack ),
Ok ( Event ::Text ( e )) => handle_text_tag ( & mut stack , & e ),
2023-10-28 22:20:57 +02:00
_ => {}
2023-10-27 19:03:29 +02:00
}
}
}
2025-04-10 10:43:14 +02:00
fn get_tag_types ( name : & str ) -> ( bool , bool , bool ) {
let ( is_tv_tag , is_channel , is_program ) = match name {
EPG_TAG_TV => ( true , false , false ),
EPG_TAG_CHANNEL => ( false , true , false ),
EPG_TAG_PROGRAMME => ( false , false , true ),
_ => ( false , false , false )
};
( is_tv_tag , is_channel , is_program )
}
2026-01-04 16:17:47 +01:00
fn collect_tag_attributes ( e : & BytesStart , is_channel : bool , is_program : bool , interner : & EpgInterner ) -> HashMap < Arc < str > , String > {
2025-04-10 10:43:14 +02:00
let attributes = e . attributes (). filter_map ( Result ::ok )
. filter_map ( | a | {
2026-01-04 16:17:47 +01:00
let key_binding = a . key ;
let key_raw = String ::from_utf8_lossy ( key_binding . as_ref ());
let key = interner . intern ( key_raw . as_ref ());
2025-07-22 11:32:46 +02:00
if let Ok ( value ) = a . unescape_value (). as_ref () {
if value . is_empty () {
None
2026-01-04 16:17:47 +01:00
} else if ( is_channel && key . as_ref () == EPG_ATTRIB_ID ) || ( is_program && key . as_ref () == EPG_ATTRIB_CHANNEL ) {
2025-10-01 17:10:51 +02:00
Some (( key , value . to_lowercase ()))
2025-07-22 11:32:46 +02:00
} else {
Some (( key , value . to_string ()))
}
2025-04-10 10:43:14 +02:00
} else {
2025-07-22 11:32:46 +02:00
None
2025-04-10 10:43:14 +02:00
}
2026-01-04 16:17:47 +01:00
}). collect ::< HashMap < Arc < str > , String >> ();
2025-04-10 10:43:14 +02:00
attributes
}
2024-11-04 18:46:56 +01:00
pub fn flatten_tvguide ( tv_guides : & [ Epg ]) -> Option < Epg > {
2023-10-28 22:20:57 +02:00
if tv_guides . is_empty () {
2023-10-27 19:03:29 +02:00
None
} else {
2026-01-04 16:17:47 +01:00
let epg_children : Mutex < Vec < Arc < XmlTag >>> = Mutex ::new ( Vec ::new ());
2025-04-12 14:14:06 +02:00
let epg_attributes = tv_guides . first (). and_then ( | t | t . attributes . clone ());
2025-04-09 21:30:04 +02:00
let count = tv_guides . iter (). map ( | tvg | tvg . children . len ()). sum ();
2025-07-19 15:32:30 +02:00
let channel_mapping : DashMap < String , i16 > = DashMap ::with_capacity ( count );
2025-05-14 15:42:07 +02:00
let mut sorted_guides = tv_guides . to_vec ();
// sort by priority
sorted_guides . sort_by ( | a , b | a . priority . cmp ( & b . priority ));
2025-05-18 13:03:37 +02:00
// if executed parallel it does not matter how we sort.
2025-05-14 15:42:07 +02:00
sorted_guides . par_iter (). for_each ( | guide | {
2025-04-12 14:14:06 +02:00
let mut children = vec! [];
2023-12-08 08:53:38 +01:00
guide . children . iter (). for_each ( | c | {
2026-01-04 16:17:47 +01:00
if c . name . as_ref () == EPG_TAG_CHANNEL {
2024-11-02 16:44:38 +01:00
if let Some ( chan_id ) = c . get_attribute_value ( EPG_ATTRIB_ID ) {
2025-05-14 15:42:07 +02:00
let should_add = {
// if not stored
2025-07-19 15:32:30 +02:00
! channel_mapping . contains_key ( chan_id ) ||
2025-05-14 15:42:07 +02:00
// or if priority is higher (less means higher priority)
2025-07-19 15:32:30 +02:00
channel_mapping . get ( chan_id ). as_deref (). is_none_or ( |& priority | guide . priority < priority )
2025-05-14 15:42:07 +02:00
};
if should_add {
2025-07-19 15:32:30 +02:00
if let Some ( mut existing ) = channel_mapping . get_mut ( chan_id ) {
if guide . priority < * existing {
* existing = guide . priority ;
2025-05-14 15:49:47 +02:00
children . push ( c . clone ());
}
2025-07-19 15:32:30 +02:00
} else {
channel_mapping . insert ( chan_id . clone (), guide . priority );
children . push ( c . clone ());
2025-05-14 15:49:47 +02:00
}
2023-12-08 08:53:38 +01:00
}
}
}
});
guide . children . iter (). for_each ( | c | {
2026-01-04 16:17:47 +01:00
if c . name . as_ref () == EPG_TAG_PROGRAMME {
2025-05-14 15:49:47 +02:00
if let Some ( chan_id ) = c . get_attribute_value ( EPG_ATTRIB_CHANNEL ) {
2025-07-19 15:32:30 +02:00
if let Some ( stored_priority ) = channel_mapping . get ( chan_id ) {
if * stored_priority == guide . priority {
2025-05-14 15:42:07 +02:00
children . push ( c . clone ());
}
2023-12-08 08:53:38 +01:00
}
}
}
});
2025-04-12 14:14:06 +02:00
2025-07-22 11:32:46 +02:00
if let Ok ( mut guard ) = epg_children . lock () {
guard . extend ( children );
}
2025-04-12 14:14:06 +02:00
});
2025-07-22 11:32:46 +02:00
let children = if let Ok ( mut children ) = epg_children . lock () {
mem ::take ( & mut * children )
} else {
vec! []
};
2025-04-12 14:14:06 +02:00
let epg = Epg {
2025-05-15 16:40:52 +02:00
logo_override : false ,
2025-05-14 15:42:07 +02:00
priority : 0 ,
2025-04-12 14:14:06 +02:00
attributes : epg_attributes ,
2025-07-22 11:32:46 +02:00
children ,
2025-04-12 14:14:06 +02:00
};
2023-10-28 22:20:57 +02:00
Some ( epg )
2023-10-27 19:03:29 +02:00
}
2024-11-02 16:44:38 +01:00
}
#[cfg(test)]
mod tests {
2026-01-04 16:36:35 +01:00
use crate ::model ::{ EpgSmartMatchConfig , PersistedEpgSource , TVGuide };
use crate ::processing ::parser ::xmltv ::normalize_channel_name ;
2025-10-01 14:40:44 +02:00
use std ::borrow ::Cow ;
2026-01-04 16:36:35 +01:00
use std ::collections ::HashSet ;
2025-10-01 14:40:44 +02:00
use std ::io ;
use std ::path ::PathBuf ;
2024-11-02 16:44:38 +01:00
2025-04-25 16:13:44 +02:00
#[test]
2025-04-25 14:19:28 +00:00
/// Tests normalization of a channel name using the default smart match configuration.
///
/// # Examples
///
/// ```
/// parse_normalize().unwrap();
/// ```
2025-06-24 01:06:25 +02:00
fn parse_normalize () {
2025-07-22 11:32:46 +02:00
let epg_normalize_dto = EpgSmartMatchConfigDto { .. Default ::default () };
2025-06-24 01:06:25 +02:00
let epg_normalize = EpgSmartMatchConfig ::from ( epg_normalize_dto );
2025-04-25 16:13:44 +02:00
let normalized = normalize_channel_name ( "Love Nature" , & epg_normalize );
assert_eq! ( normalized , "lovenature" . to_string ());
}
2025-10-01 14:40:44 +02:00
#[test]
fn parse_test () -> io ::Result < () > {
2025-10-06 21:00:13 +02:00
let run_test = async move || {
//let file_path = PathBuf::from("/tmp/epg.xml.gz");
let file_path = PathBuf ::from ( "/tmp/invalid_epg.xml" );
if file_path . exists () {
let tv_guide = TVGuide ::new ( vec! [ PersistedEpgSource { file_path , priority : 0 , logo_override : false }]);
let mut id_cache = EpgIdCache ::new ( None );
id_cache . channel_epg_id . insert ( Cow ::Owned ( "342" . to_string ()));
//id_cache.collect_epg_id(fp);
let channel_ids = HashSet ::from ([ "342" . to_string ()]);
match tv_guide . filter ( & mut id_cache ). await {
None => assert! ( false , "No epg filtered" ),
Some ( epgs ) => {
for epg in epgs {
assert_eq! ( epg . children . len (), channel_ids . len () * 2 , "Epg size does not match" )
}
2025-10-01 14:40:44 +02:00
}
}
}
2025-10-06 21:00:13 +02:00
};
let _result = tokio ::runtime ::Runtime ::new ()
. unwrap ()
. block_on ( run_test ());
2025-10-01 14:40:44 +02:00
Ok (())
}
2025-04-09 20:51:25 +02:00
#[test]
2025-04-25 14:19:28 +00:00
/// Tests normalization of channel names with various prefixes, suffixes, and special characters using a configured `EpgSmartMatchConfig`.
///
/// # Examples
///
/// ```
/// normalize();
/// // This will assert that various channel names are normalized as expected.
/// ```
2025-04-09 20:51:25 +02:00
fn normalize () {
2025-06-24 01:06:25 +02:00
let mut epg_smart_cfg_dto = EpgSmartMatchConfigDto { enabled : true , name_prefix : EpgNamePrefix ::Suffix ( "." . to_string ()), .. Default ::default () };
let _ = epg_smart_cfg_dto . prepare ();
let epg_smart_cfg = EpgSmartMatchConfig ::from ( epg_smart_cfg_dto );
2025-05-16 14:11:33 +02:00
println! ( " {epg_smart_cfg:?} " );
2025-04-12 14:14:06 +02:00
assert_eq! ( "supersport6.ru" , normalize_channel_name ( "RU: SUPERSPORT 6 ᴿᴬᵂ" , & epg_smart_cfg ));
assert_eq! ( "odisea.sat" , normalize_channel_name ( "SAT: ODISEA ᴿᴬᵂ" , & epg_smart_cfg ));
assert_eq! ( "odisea.4k" , normalize_channel_name ( "4K: ODISEA ᵁᴴᴰ ³⁸⁴⁰ᴾ" , & epg_smart_cfg ));
assert_eq! ( "odisea" , normalize_channel_name ( "ODISEA ᵁᴴᴰ ³⁸⁴⁰ᴾ" , & epg_smart_cfg ));
assert_eq! ( "odisea.bu" , normalize_channel_name ( "BU | ODISEA ᵁᴴᴰ ³⁸⁴⁰ᴾ" , & epg_smart_cfg ));
assert_eq! ( "odisea.bg" , normalize_channel_name ( "BG | ODISEA ᵁᴴᴰ ³⁸⁴⁰ᴾ" , & epg_smart_cfg ));
2025-04-11 20:25:09 +02:00
}
2026-01-04 16:36:35 +01:00
use crate ::processing ::processor ::epg ::EpgIdCache ;
2025-04-11 20:25:09 +02:00
use rphonetic ::{ Encoder , Metaphone };
2025-06-24 01:06:25 +02:00
use shared ::model ::{ EpgNamePrefix , EpgSmartMatchConfigDto };
2025-04-25 16:13:44 +02:00
2025-04-11 20:25:09 +02:00
#[test]
2025-04-25 14:19:28 +00:00
/// Demonstrates phonetic encoding (Metaphone) of normalized channel names with various prefixes and suffixes.
///
/// This test prints the Metaphone-encoded representations of several normalized channel names using a configured `EpgSmartMatchConfig`.
///
/// # Examples
///
/// ```
/// test_metaphone();
/// // Output will show the Metaphone encodings for different channel name variants.
/// ```
2025-04-11 20:25:09 +02:00
fn test_metaphone () {
let metaphone = Metaphone ::default ();
2025-06-24 01:06:25 +02:00
let mut epg_smart_cfg_dto = EpgSmartMatchConfigDto { enabled : true , name_prefix : EpgNamePrefix ::Suffix ( "." . to_string ()), .. Default ::default () };
let _ = epg_smart_cfg_dto . prepare ();
let epg_smart_cfg = EpgSmartMatchConfig ::from ( epg_smart_cfg_dto );
2025-05-16 14:11:33 +02:00
println! ( " {epg_smart_cfg:?} " );
2025-04-11 20:25:09 +02:00
// assert_eq!("supersport6.ru", metaphone.encode(&normalize_channel_name("RU: SUPERSPORT 6 ᴿᴬᵂ", &epg_normalize_cfg)));
2025-04-12 14:14:06 +02:00
// assert_eq!("odisea.sat", metaphone.encode(&normalize_channel_name("SAT: ODISEA ᴿᴬᵂ", &epg_normalize_cfg)));
2025-04-11 20:25:09 +02:00
// assert_eq!("odisea", metaphone.encode(&normalize_channel_name("4K: ODISEA ᵁᴴᴰ ³⁸⁴⁰ᴾ", &epg_normalize_cfg)));
// assert_eq!("odisea", metaphone.encode(&normalize_channel_name("ODISEA ᵁᴴᴰ ³⁸⁴⁰ᴾ", &epg_normalize_cfg)));
2025-04-12 14:14:06 +02:00
// assert_eq!("odisea.bu", metaphone.encode(&normalize_channel_name("BU | ODISEA ᵁᴴᴰ ³⁸⁴⁰ᴾ", &epg_normalize_cfg)));
2025-04-11 20:25:09 +02:00
// assert_eq!("odisea.bg", metaphone.encode(&normalize_channel_name("BG | ODISEA ᵁᴴᴰ ³⁸⁴⁰ᴾ", &epg_normalize_cfg)));
2025-04-10 20:26:22 +02:00
2025-04-12 14:14:06 +02:00
println! ( " {} " , metaphone . encode ( & normalize_channel_name ( "RU: SUPERSPORT 6 ᴿᴬᵂ" , & epg_smart_cfg )));
println! ( " {} " , metaphone . encode ( & normalize_channel_name ( "SAT: ODISEA ᴿᴬᵂ" , & epg_smart_cfg )));
println! ( " {} " , metaphone . encode ( & normalize_channel_name ( "4K: ODISEA ᵁᴴᴰ ³⁸⁴⁰ᴾ" , & epg_smart_cfg )));
println! ( " {} " , metaphone . encode ( & normalize_channel_name ( "ODISEA ᵁᴴᴰ ³⁸⁴⁰ᴾ" , & epg_smart_cfg )));
println! ( " {} " , metaphone . encode ( & normalize_channel_name ( "BU | ODISEA ᵁᴴᴰ ³⁸⁴⁰ᴾ" , & epg_smart_cfg )));
println! ( " {} " , metaphone . encode ( & normalize_channel_name ( "BG | ODISEA ᵁᴴᴰ ³⁸⁴⁰ᴾ" , & epg_smart_cfg )));
2025-04-09 20:51:25 +02:00
}
2023-10-27 19:03:29 +02:00
}