From 0929e903afd6bdc1f1bed8f57be265c68b31ac26 Mon Sep 17 00:00:00 2001 From: "coderabbitai[bot]" <136622811+coderabbitai[bot]@users.noreply.github.com> Date: Fri, 25 Apr 2025 14:19:28 +0000 Subject: [PATCH] =?UTF-8?q?=F0=9F=93=9D=20Add=20docstrings=20to=20`feature?= =?UTF-8?q?/fuzzy=5Fepg=5Fmatching`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Docstrings generation was requested by @euzu. * https://github.com/euzu/m3u-filter/pull/223#issuecomment-2830556827 The following files were modified: * `src/model/config.rs` * `src/processing/parser/xmltv.rs` * `src/processing/processor/epg.rs` --- src/model/config.rs | 19 ++++++++- src/processing/parser/xmltv.rs | 72 +++++++++++++++++++++++++++++++++ src/processing/processor/epg.rs | 54 +++++++++++++++++++++++++ 3 files changed, 143 insertions(+), 2 deletions(-) diff --git a/src/model/config.rs b/src/model/config.rs index 1754aa46f..1e27d2f00 100644 --- a/src/model/config.rs +++ b/src/model/config.rs @@ -1004,14 +1004,29 @@ pub struct EpgSmartMatchConfig { impl EpgSmartMatchConfig { + /// Creates a new enabled `EpgSmartMatchConfig` with default settings and prepares it. + /// + /// Returns an error if preparation fails. + /// + /// # Examples + /// + /// ``` + /// let config = EpgSmartMatchConfig::new().unwrap(); + /// assert!(config.enabled); + /// ``` pub fn new() -> Result { let mut this = Self { enabled: true, ..Self::default() }; this.prepare()?; Ok(this) } - /// # Panics - pub fn prepare(&mut self) -> Result<(), M3uFilterError> { + /// Prepares the EPG smart match configuration by validating thresholds, compiling normalization regex, and setting default values as needed. + /// + /// Adjusts match thresholds to valid ranges, compiles the normalization regex, and sets default strip values and name prefix separators if not provided. Returns an error if the normalization regex is invalid. + /// + /// # Returns + /// + /// `Ok(())` if preparation succeeds, or an `M3uFilterError` if regex compilation fails. pub fn prepare(&mut self) -> Result<(), M3uFilterError> { if !self.enabled { return Ok(()); } diff --git a/src/processing/parser/xmltv.rs b/src/processing/parser/xmltv.rs index eeb41ac6a..35c546031 100644 --- a/src/processing/parser/xmltv.rs +++ b/src/processing/parser/xmltv.rs @@ -15,6 +15,22 @@ use std::path::Path; use std::sync::atomic::{AtomicBool, AtomicU16, Ordering}; use std::sync::{Arc, Mutex, RwLock}; +/// 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"); +/// ``` fn split_by_first_match<'a>(input: &'a str, delimiters: &[char]) -> (Option<&'a str>, &'a str) { for delim in delimiters { if let Some(index) = input.find(*delim) { @@ -124,6 +140,24 @@ impl TVGuide { matched } + /// 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); + /// } + /// ``` fn find_best_fuzzy_match(id_cache: &mut EpgIdCache, tag: &XmlTag) -> (bool, Option) { let early_exit_flag = Arc::new(AtomicBool::new(false)); // Flag für den frühen Abbruch let data: Mutex>> = Mutex::new(None); @@ -163,6 +197,19 @@ impl TVGuide { (false, None) } + /// 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`. + /// + /// # Examples + /// + /// ``` + /// let mut id_cache = EpgIdCache::default(); + /// let epg_file = Path::new("guide.xml.gz"); + /// if let Some(epg) = process_epg_file(&mut id_cache, epg_file) { + /// assert!(!epg.children.is_empty()); + /// } + /// ``` fn process_epg_file(id_cache: &mut EpgIdCache, epg_file: &Path) -> Option { match CompressedFileReader::new(epg_file) { Ok(mut reader) => { @@ -398,6 +445,13 @@ mod tests { use crate::processing::parser::xmltv::normalize_channel_name; #[test] + /// Tests normalization of a channel name using the default smart match configuration. + /// + /// # Examples + /// + /// ``` + /// parse_normalize().unwrap(); + /// ``` fn parse_normalize() -> Result<(), M3uFilterError> { let epg_normalize = EpgSmartMatchConfig::new()?; let normalized = normalize_channel_name("Love Nature", &epg_normalize); @@ -426,6 +480,14 @@ mod tests { // } #[test] + /// 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. + /// ``` fn normalize() { let mut epg_smart_cfg = EpgSmartMatchConfig::default(); epg_smart_cfg.enabled = true; @@ -444,6 +506,16 @@ mod tests { use rphonetic::{Encoder, Metaphone}; #[test] + /// 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. + /// ``` fn test_metaphone() { let metaphone = Metaphone::default(); let mut epg_smart_cfg = EpgSmartMatchConfig::default(); diff --git a/src/processing/processor/epg.rs b/src/processing/processor/epg.rs index 2e95eb8af..3187969e0 100644 --- a/src/processing/processor/epg.rs +++ b/src/processing/processor/epg.rs @@ -18,6 +18,16 @@ pub struct EpgIdCache<'a> { } impl EpgIdCache<'_> { + /// Creates a new `EpgIdCache` with configuration for smart and fuzzy matching. + /// + /// Initializes all internal caches and sets matching options based on the provided EPG configuration. If no configuration is given, defaults are used. + /// + /// # Examples + /// + /// ``` + /// let cache = EpgIdCache::new(None); + /// assert!(cache.is_empty()); + /// ``` pub fn new(epg_config: Option<&EpgConfig>) -> Self { let normalize_config = epg_config.map_or_else(EpgSmartMatchConfig::default, |epg_config| epg_config.t_smart_match.clone()); EpgIdCache { @@ -37,6 +47,18 @@ impl EpgIdCache<'_> { self.channel_epg_id.is_empty() && self.normalized.is_empty() } + /// Normalizes a channel name, computes its phonetic encoding, and stores both in the cache for later EPG matching. + /// + /// The normalized name is mapped to the provided EPG ID (if any), and the phonetic encoding is added to the phonetics map. + /// This facilitates efficient lookup and fuzzy matching of channel names during EPG assignment. + /// + /// # Examples + /// + /// ``` + /// let mut cache = EpgIdCache::new(None); + /// cache.normalize_and_store("Discovery Channel", Some(&"discovery.epg".to_string())); + /// assert!(cache.normalized.contains_key(&cache.normalize("Discovery Channel"))); + /// ``` fn normalize_and_store(&mut self, name: &str, epg_id: Option<&String>) { let normalized_name = self.normalize(name); let phonetic = self.phonetic(&normalized_name); @@ -44,6 +66,15 @@ impl EpgIdCache<'_> { self.phonetics.entry(phonetic.to_string()).or_default().insert(phonetic); } + /// Returns the normalized form of a channel name using the configured smart match settings. + /// + /// # Examples + /// + /// ``` + /// let cache = EpgIdCache::new(None); + /// let normalized = cache.normalize("HBO HD"); + /// assert!(!normalized.is_empty()); + /// ``` fn normalize(&self, name: &str) -> String { normalize_channel_name(name, &self.smart_match_config) } @@ -89,6 +120,18 @@ impl EpgIdCache<'_> { } } +/// Assigns EPG IDs and logos to live playlist channels by matching them with EPG data. +/// +/// For each live channel in the playlist missing an EPG ID, attempts to assign one using normalized name matching if smart matching is enabled. If a channel has an EPG ID but lacks logos, assigns logos from the corresponding EPG icon tags. Adds the matched EPG data to the provided vector. +/// +/// # Examples +/// +/// ``` +/// let mut new_epg = Vec::new(); +/// let mut playlist = FetchedPlaylist::default(); +/// let mut id_cache = EpgIdCache::new(None); +/// assign_channel_epg(&mut new_epg, &mut playlist, &mut id_cache); +/// ``` fn assign_channel_epg(new_epg: &mut Vec, fp: &mut FetchedPlaylist, id_cache: &mut EpgIdCache) { if let Some(tv_guide) = &fp.epg { if let Some(epg) = tv_guide.filter(id_cache) { @@ -139,6 +182,17 @@ fn assign_channel_epg(new_epg: &mut Vec, fp: &mut FetchedPlaylist, id_cache } } +/// Processes a fetched playlist and assigns EPG data to its channels. +/// +/// Collects EPG channel IDs from the playlist, initializes an EPG ID cache, and assigns EPG data to channels using normalization and smart matching if enabled. Logs a debug message if no EPG IDs are found and smart matching is disabled. +/// +/// # Examples +/// +/// ``` +/// let mut playlist = FetchedPlaylist::default(); +/// let mut epg_data = Vec::new(); +/// process_playlist_epg(&mut playlist, &mut epg_data); +/// ``` pub fn process_playlist_epg(fp: &mut FetchedPlaylist, epg: &mut Vec) { // collect all epg_channel ids let mut id_cache = EpgIdCache::new(fp.input.epg.as_ref());