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-05-16 08:59:52 +02:00
use crate ::model ::{ EpgNamePrefix , EpgSmartMatchConfig , PersistedEpgSource };
2025-04-25 16:13:44 +02:00
use crate ::processing ::processor ::epg ::EpgIdCache ;
2025-05-04 10:59:08 +02:00
use crate ::utils ::compressed_file_reader ::CompressedFileReader ;
2025-06-19 13:22:43 +02:00
use shared ::utils ::CONSTANTS ;
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 };
2023-10-28 22:20:57 +02:00
use quick_xml ::Reader ;
2025-04-12 14:14:06 +02:00
use rayon ::iter ::{ IntoParallelRefIterator , ParallelIterator };
2025-04-11 20:25:09 +02:00
use std ::borrow ::Cow ;
2025-04-12 14:14:06 +02:00
use std ::cmp ::min ;
2025-05-14 15:49:47 +02:00
use std ::collections ::hash_map ::Entry ;
2025-05-16 08:59:52 +02:00
use std ::collections ::HashMap ;
2025-04-12 14:14:06 +02:00
use std ::mem ;
2025-04-25 16:34:58 +02:00
use std ::sync ::atomic ::{ AtomicBool , Ordering };
2025-04-17 12:11:51 +02:00
use std ::sync ::{ Arc , Mutex , RwLock };
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 {
let ( prefix , suffix ) = split_by_first_match ( name , & smart_config . t_name_prefix_separator . clone ());
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 );
combined . push ( '.' );
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-04-12 14:14:06 +02:00
let cleaned_name = normalize_config . t_normalize_regex . as_ref (). unwrap (). replace_all ( channel_name , "" );
2025-04-06 16:50:43 +02:00
// Remove terms like resolution
2025-04-11 20:25:09 +02:00
let cleaned_name = normalize_config . t_strip . iter (). fold ( cleaned_name . to_string (), | acc , term | {
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 {
2025-05-16 08:59:52 +02:00
pub fn merge ( mut epgs : Vec < Epg > ) -> Option < Epg > {
if let Some ( first_epg ) = epgs . get_mut ( 0 ) {
let first_epg_attributes = first_epg . attributes . take ();
let merged_children : Vec < XmlTag > = epgs . into_iter (). flat_map ( | epg | epg . children ). collect ();
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 ) {
if let Some ( children ) = & mut tag . children {
for child in children {
match child . name . as_str () {
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-06-09 10:30:52 +02:00
tag . icon = XmlTagIcon ::Src ( src . to_string ());
child . icon = XmlTagIcon ::Exists ;
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 {
let key = matched_normalized_name . unwrap ();
let id = epg_id . to_string ();
2025-04-12 14:14:06 +02:00
id_cache . normalized . entry ( key ). and_modify ( | entry | {
2025-04-23 20:21:36 +02:00
entry . replace ( id . clone ());
id_cache . channel_epg_id . insert ( Cow ::Owned ( id ));
2025-04-12 14:14:06 +02:00
matched = true ;
});
}
}
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 > ) {
2025-04-25 16:34:58 +02:00
let early_exit_flag = Arc ::new ( AtomicBool ::new ( false ));
let data : Mutex < ( u16 , Option < Cow < str >> ) > = Mutex ::new (( 0 , None ));
2025-04-12 14:14:06 +02:00
let match_threshold = id_cache . smart_match_config . match_threshold ;
let best_match_threshold = id_cache . smart_match_config . best_match_threshold ;
2025-06-05 20:06:04 +02:00
if let Some ( normalized_epg_ids ) = tag . normalized_epg_ids . as_ref () {
for tag_normalized in normalized_epg_ids {
let tag_code = id_cache . phonetic ( tag_normalized );
if let Some ( normalized ) = id_cache . phonetics . get ( & tag_code ) {
normalized . par_iter (). find_any ( | norm_key | {
let match_jw = strsim ::jaro_winkler ( norm_key , tag_normalized );
#[allow(clippy::cast_possible_truncation)]
#[allow(clippy::cast_sign_loss)]
let mjw = min ( 100 , ( match_jw * 100.0 ). round () as u16 );
if mjw >= match_threshold {
let mut lock = data . lock (). unwrap ();
if lock . 0 < mjw {
* lock = ( mjw , Some ( Cow ::Borrowed ( norm_key )));
}
if mjw > best_match_threshold {
return true ; // (true, matched_normalized_epg_id.map(|s| s.to_string()));
}
2025-04-12 14:14:06 +02:00
}
2025-06-05 20:06:04 +02:00
false
});
}
2025-04-12 14:14:06 +02:00
}
2025-04-25 16:13:44 +02:00
}
// is there an early exit strategy ???
2025-04-12 14:14:06 +02:00
if early_exit_flag . load ( Ordering ::SeqCst ) {
2025-04-25 16:34:58 +02:00
let result = data . lock (). unwrap (). 1. take ();
2025-04-12 14:14:06 +02:00
return ( true , result . as_ref (). map ( std ::string ::ToString ::to_string ));
}
( 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-05-14 12:40:51 +02:00
fn process_epg_file ( id_cache : & mut EpgIdCache , epg_source : & PersistedEpgSource ) -> Option < Epg > {
match CompressedFileReader ::new ( & epg_source . file_path ) {
2024-11-02 16:44:38 +01:00
Ok ( mut reader ) => {
let mut children : Vec < 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 | {
2025-04-09 20:51:25 +02:00
match tag . name . as_str () {
2024-11-02 16:44:38 +01:00
EPG_TAG_CHANNEL => {
2025-04-25 16:13:44 +02:00
let epg_id = tag . get_attribute_value ( EPG_ATTRIB_ID ). map_or_else ( String ::new , std ::string ::ToString ::to_string );
if ! epg_id . is_empty () && ! id_cache . processed . contains ( & epg_id ) {
Self ::prepare_tag ( id_cache , & mut tag , smart_match );
if smart_match {
if Self ::try_fuzzy_matching ( id_cache , & epg_id , & tag , fuzzy_matching ) {
children . push ( tag );
id_cache . processed . insert ( epg_id );
}
} else {
let borrowed_epg_id = Cow ::Borrowed ( epg_id . as_str ());
if id_cache . channel_epg_id . contains ( & borrowed_epg_id ) {
children . push ( tag );
id_cache . processed . insert ( 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 ) {
2025-04-10 18:08:11 +02:00
children . push ( tag );
}
2025-04-09 20:51:25 +02:00
}
}
}
2024-11-02 16:44:38 +01:00
EPG_TAG_TV => {
2025-04-12 14:14:06 +02:00
tv_attributes = tag . attributes . take ();
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-04-12 14:14:06 +02:00
parse_tvguide ( & mut reader , & mut filter_tags );
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 ,
})
}
Err ( _ ) => None
}
}
2025-04-10 18:08:11 +02:00
2025-05-16 08:59:52 +02:00
pub 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-05-16 08:59:52 +02:00
let mut epg_sources : Vec < Epg > = self . get_epg_sources (). iter ()
2025-05-14 15:42:07 +02:00
. filter_map ( | epg_source | Self ::process_epg_file ( id_cache , epg_source ))
2025-04-10 18:08:11 +02:00
. collect ();
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
fn handle_tag_start < F > ( callback : & mut F , stack : & mut Vec < XmlTag > , e : & BytesStart )
2024-11-02 16:44:38 +01:00
where
F : FnMut ( XmlTag ),
{
2025-04-12 14:14:06 +02:00
let name = String ::from_utf8_lossy ( e . name (). as_ref ()). as_ref (). to_owned ();
let ( is_tv_tag , is_channel , is_program ) = get_tag_types ( & name );
let attributes = collect_tag_attributes ( e , is_channel , is_program );
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 () {
if tag . name == EPG_TAG_CHANNEL {
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
}
}
2025-04-12 14:14:06 +02:00
} else if tag . name == EPG_TAG_PROGRAMME {
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 () {
if let Some ( old_tag ) = stack . pop (). map ( | mut r | {
r . children = Some ( match r . children . take () {
None => vec! [ tag ],
Some ( mut tags ) => {
tags . push ( tag );
tags
}
});
r
}) {
stack . push ( old_tag );
}
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 ) {
if ! stack . is_empty () {
if let Ok ( text ) = e . unescape () {
let t = text . trim ();
if ! t . is_empty () {
stack . last_mut (). unwrap (). value = Some ( t . to_string ());
}
}
}
}
pub fn parse_tvguide < R , F > ( content : R , callback : & mut F )
where
R : std ::io ::BufRead ,
F : FnMut ( XmlTag ),
{
let mut stack : Vec < XmlTag > = vec! [];
let mut reader = Reader ::from_reader ( content );
let mut buf = Vec ::< u8 > ::new ();
loop {
match reader . read_event_into ( & mut buf ) {
Ok ( Event ::Eof ) => break ,
Ok ( Event ::Start ( e )) => handle_tag_start ( callback , & mut stack , & e ),
2025-05-16 15:20:57 +02:00
Ok ( Event ::Empty ( e )) => {
handle_tag_start ( callback , & mut stack , & e );
handle_tag_end ( callback , & mut stack );
},
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 )
}
fn collect_tag_attributes ( e : & BytesStart , is_channel : bool , is_program : bool ) -> HashMap < String , String > {
let attributes = e . attributes (). filter_map ( Result ::ok )
. filter_map ( | a | {
let key = String ::from_utf8_lossy ( a . key . as_ref ()). to_string ();
let mut value = String ::from ( a . unescape_value (). unwrap (). as_ref ());
if ( is_channel && key == EPG_ATTRIB_ID ) || ( is_program && key == EPG_ATTRIB_CHANNEL ) {
value = value . to_lowercase (). to_string ();
}
if value . is_empty () {
None
} else {
Some (( key , value ))
}
}). collect ::< HashMap < String , String >> ();
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 {
2025-04-12 14:14:06 +02:00
let epg_children = Mutex ::new ( Vec ::new ());
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-05-14 15:42:07 +02:00
let channel_mapping : RwLock < HashMap < String , i16 >> = RwLock ::new ( HashMap ::with_capacity ( count ));
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 | {
2024-11-02 16:44:38 +01:00
if c . name . as_str () == EPG_TAG_CHANNEL {
if let Some ( chan_id ) = c . get_attribute_value ( EPG_ATTRIB_ID ) {
2025-05-14 15:42:07 +02:00
let should_add = {
let channel_map = channel_mapping . read (). unwrap ();
// if not stored
! channel_map . contains_key ( chan_id ) ||
// or if priority is higher (less means higher priority)
channel_map . get ( chan_id ). is_none_or ( |& priority | guide . priority < priority )
};
if should_add {
let mut channel_map = channel_mapping . write (). unwrap ();
2025-05-14 15:49:47 +02:00
match channel_map . entry ( chan_id . to_string ()) {
Entry ::Vacant ( e ) => {
e . insert ( guide . priority );
children . push ( c . clone ());
}
Entry ::Occupied ( mut e ) if guide . priority < * e . get () => {
e . insert ( guide . priority );
children . push ( c . clone ());
}
Entry ::Occupied ( _ ) => {}
}
2023-12-08 08:53:38 +01:00
}
}
}
});
guide . children . iter (). for_each ( | c | {
2024-11-02 16:44:38 +01:00
if c . name . as_str () == EPG_TAG_PROGRAMME {
2025-05-14 15:49:47 +02:00
if let Some ( chan_id ) = c . get_attribute_value ( EPG_ATTRIB_CHANNEL ) {
2025-05-14 15:42:07 +02:00
let channel_map = channel_mapping . read (). unwrap ();
if let Some ( & stored_priority ) = channel_map . get ( chan_id ) {
if stored_priority == guide . priority {
children . push ( c . clone ());
}
2023-12-08 08:53:38 +01:00
}
}
}
});
2025-04-12 14:14:06 +02:00
epg_children . lock (). unwrap (). extend ( children );
});
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 ,
children : mem ::take ( & mut * epg_children . lock (). unwrap ()),
};
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 {
2025-05-04 10:59:08 +02:00
use crate ::model ::{ EpgNamePrefix , EpgSmartMatchConfig };
2025-04-09 20:51:25 +02:00
use crate ::processing ::parser ::xmltv ::normalize_channel_name ;
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-05-13 21:50:08 +02:00
fn parse_normalize () -> Result < (), TuliproxError > {
2025-04-25 16:13:44 +02:00
let epg_normalize = EpgSmartMatchConfig ::new () ? ;
let normalized = normalize_channel_name ( "Love Nature" , & epg_normalize );
assert_eq! ( normalized , "lovenature" . to_string ());
Ok (())
}
2025-04-10 18:08:11 +02:00
// #[test]
// fn parse_test() -> io::Result<()> {
// let file_path = PathBuf::from("/tmp/epg.xml.gz");
//
// if file_path.exists() {
// let tv_guide = TVGuide { file: file_path };
//
// let mut channel_ids = HashSet::from(["channel.1".to_string(), "channel.2".to_string(), "channel.3".to_string()]);
// let mut nomalized = HashMap::new();
// match tv_guide.filter(&mut channel_ids, &mut nomalized) {
// None => assert!(false, "No epg filtered"),
// Some(epg) => {
// assert_eq!(epg.children.len(), channel_ids.len() * 2, "Epg size does not match")
// }
// }
// }
// 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-13 08:42:47 +02:00
let mut epg_smart_cfg = EpgSmartMatchConfig { enabled : true , name_prefix : EpgNamePrefix ::Suffix ( "." . to_string ()), .. Default ::default () };
let _ = epg_smart_cfg . prepare ();
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
}
2025-06-19 16:37:37 +02:00
use shared ::error ::TuliproxError ;
2025-04-11 20:25:09 +02:00
use rphonetic ::{ Encoder , Metaphone };
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-13 08:42:47 +02:00
let mut epg_smart_cfg = EpgSmartMatchConfig { enabled : true , name_prefix : EpgNamePrefix ::Suffix ( "." . to_string ()), .. Default ::default () };
2025-04-12 14:14:06 +02:00
let _ = epg_smart_cfg . prepare ();
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
}