use std::collections::HashMap;
use crispy_iptv_types::PlaylistEntry;
use serde::{Deserialize, Serialize};
use crate::error::ToolsError;
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct UnifyConfig {
#[serde(default)]
pub id_unifiers: HashMap<String, String>,
#[serde(default)]
pub title_unifiers: HashMap<String, String>,
}
pub fn load_unify_config(json: &str) -> Result<UnifyConfig, ToolsError> {
serde_json::from_str(json).map_err(|e| ToolsError::InvalidConfig(e.to_string()))
}
pub fn unify_entries(entries: &[PlaylistEntry], config: &UnifyConfig) -> Vec<PlaylistEntry> {
let mut title_keys: Vec<&String> = config.title_unifiers.keys().collect();
title_keys.sort();
let mut id_keys: Vec<&String> = config.id_unifiers.keys().collect();
id_keys.sort();
entries
.iter()
.filter_map(|entry| {
let mut entry = entry.clone();
if let Some(ref title) = entry.name {
let mut new_title = title.clone();
for key in &title_keys {
if new_title.contains(key.as_str()) {
let replacement = &config.title_unifiers[key.as_str()];
new_title = new_title.replace(key.as_str(), replacement);
}
}
if new_title.is_empty() && !title.is_empty() {
return None;
}
entry.name = Some(new_title);
}
let working_id = entry
.tvg_name
.clone()
.or_else(|| entry.name.clone())
.unwrap_or_default();
let mut new_id = working_id;
for key in &id_keys {
if new_id.contains(key.as_str()) {
let replacement = &config.id_unifiers[key.as_str()];
new_id = new_id.replace(key.as_str(), replacement);
}
}
if new_id.is_empty() && entry.tvg_id.is_some() {
return None;
}
if !new_id.is_empty() {
entry.tvg_id = Some(new_id);
}
Some(entry)
})
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
fn make_entry(name: &str, tvg_id: &str, tvg_name: Option<&str>) -> PlaylistEntry {
PlaylistEntry {
name: Some(name.to_string()),
tvg_id: if tvg_id.is_empty() {
None
} else {
Some(tvg_id.to_string())
},
tvg_name: tvg_name.map(|s| s.to_string()),
..Default::default()
}
}
#[test]
fn load_config_from_json() {
let json = r#"{
"id_unifiers": {"old_id": "new_id"},
"title_unifiers": {"Old Title": "New Title"}
}"#;
let config = load_unify_config(json).unwrap();
assert_eq!(
config.id_unifiers.get("old_id"),
Some(&"new_id".to_string())
);
assert_eq!(
config.title_unifiers.get("Old Title"),
Some(&"New Title".to_string())
);
}
#[test]
fn load_config_invalid_json_errors() {
assert!(load_unify_config("not json").is_err());
}
#[test]
fn unify_renames_ids() {
let entries = vec![make_entry("BBC One", "bbc_old", None)];
let config = UnifyConfig {
id_unifiers: HashMap::from([("bbc_old".to_string(), "bbc_one".to_string())]),
..Default::default()
};
let result = unify_entries(&entries, &config);
assert_eq!(result.len(), 1);
}
#[test]
fn unify_renames_ids_via_tvg_name() {
let entries = vec![make_entry("BBC One", "", Some("bbc_old"))];
let config = UnifyConfig {
id_unifiers: HashMap::from([("bbc_old".to_string(), "bbc_one".to_string())]),
..Default::default()
};
let result = unify_entries(&entries, &config);
assert_eq!(result.len(), 1);
assert_eq!(result[0].tvg_id.as_deref(), Some("bbc_one"));
}
#[test]
fn unify_renames_titles() {
let entries = vec![make_entry("BBC World News", "", None)];
let config = UnifyConfig {
title_unifiers: HashMap::from([("World News".to_string(), "Global".to_string())]),
..Default::default()
};
let result = unify_entries(&entries, &config);
assert_eq!(result.len(), 1);
assert_eq!(result[0].name.as_deref(), Some("BBC Global"));
}
#[test]
fn unify_deletes_entries_mapped_to_empty_title() {
let entries = vec![
make_entry("Remove Me", "", None),
make_entry("Keep Me", "", None),
];
let config = UnifyConfig {
title_unifiers: HashMap::from([("Remove Me".to_string(), String::new())]),
..Default::default()
};
let result = unify_entries(&entries, &config);
assert_eq!(result.len(), 1);
assert_eq!(result[0].name.as_deref(), Some("Keep Me"));
}
#[test]
fn unify_empty_config_is_identity() {
let entries = vec![make_entry("BBC One", "bbc.uk", None)];
let config = UnifyConfig::default();
let result = unify_entries(&entries, &config);
assert_eq!(result.len(), 1);
assert_eq!(result[0].name.as_deref(), Some("BBC One"));
}
}