crispy_iptv_tools/
unify.rs1use std::collections::HashMap;
8
9use crispy_iptv_types::PlaylistEntry;
10use serde::{Deserialize, Serialize};
11
12use crate::error::ToolsError;
13
14#[derive(Debug, Clone, Default, Serialize, Deserialize)]
20pub struct UnifyConfig {
21 #[serde(default)]
23 pub id_unifiers: HashMap<String, String>,
24
25 #[serde(default)]
27 pub title_unifiers: HashMap<String, String>,
28}
29
30pub fn load_unify_config(json: &str) -> Result<UnifyConfig, ToolsError> {
39 serde_json::from_str(json).map_err(|e| ToolsError::InvalidConfig(e.to_string()))
40}
41
42pub fn unify_entries(entries: &[PlaylistEntry], config: &UnifyConfig) -> Vec<PlaylistEntry> {
53 let mut title_keys: Vec<&String> = config.title_unifiers.keys().collect();
54 title_keys.sort();
55
56 let mut id_keys: Vec<&String> = config.id_unifiers.keys().collect();
57 id_keys.sort();
58
59 entries
60 .iter()
61 .filter_map(|entry| {
62 let mut entry = entry.clone();
63
64 if let Some(ref title) = entry.name {
66 let mut new_title = title.clone();
67 for key in &title_keys {
68 if new_title.contains(key.as_str()) {
69 let replacement = &config.title_unifiers[key.as_str()];
70 new_title = new_title.replace(key.as_str(), replacement);
71 }
72 }
73 if new_title.is_empty() && !title.is_empty() {
75 return None;
76 }
77 entry.name = Some(new_title);
78 }
79
80 let working_id = entry
82 .tvg_name
83 .clone()
84 .or_else(|| entry.name.clone())
85 .unwrap_or_default();
86
87 let mut new_id = working_id;
89 for key in &id_keys {
90 if new_id.contains(key.as_str()) {
91 let replacement = &config.id_unifiers[key.as_str()];
92 new_id = new_id.replace(key.as_str(), replacement);
93 }
94 }
95
96 if new_id.is_empty() && entry.tvg_id.is_some() {
98 return None;
99 }
100
101 if !new_id.is_empty() {
103 entry.tvg_id = Some(new_id);
104 }
105
106 Some(entry)
107 })
108 .collect()
109}
110
111#[cfg(test)]
112mod tests {
113 use super::*;
114
115 fn make_entry(name: &str, tvg_id: &str, tvg_name: Option<&str>) -> PlaylistEntry {
116 PlaylistEntry {
117 name: Some(name.to_string()),
118 tvg_id: if tvg_id.is_empty() {
119 None
120 } else {
121 Some(tvg_id.to_string())
122 },
123 tvg_name: tvg_name.map(|s| s.to_string()),
124 ..Default::default()
125 }
126 }
127
128 #[test]
129 fn load_config_from_json() {
130 let json = r#"{
131 "id_unifiers": {"old_id": "new_id"},
132 "title_unifiers": {"Old Title": "New Title"}
133 }"#;
134 let config = load_unify_config(json).unwrap();
135 assert_eq!(
136 config.id_unifiers.get("old_id"),
137 Some(&"new_id".to_string())
138 );
139 assert_eq!(
140 config.title_unifiers.get("Old Title"),
141 Some(&"New Title".to_string())
142 );
143 }
144
145 #[test]
146 fn load_config_invalid_json_errors() {
147 assert!(load_unify_config("not json").is_err());
148 }
149
150 #[test]
151 fn unify_renames_ids() {
152 let entries = vec![make_entry("BBC One", "bbc_old", None)];
153 let config = UnifyConfig {
154 id_unifiers: HashMap::from([("bbc_old".to_string(), "bbc_one".to_string())]),
155 ..Default::default()
156 };
157 let result = unify_entries(&entries, &config);
158 assert_eq!(result.len(), 1);
159 }
164
165 #[test]
166 fn unify_renames_ids_via_tvg_name() {
167 let entries = vec![make_entry("BBC One", "", Some("bbc_old"))];
168 let config = UnifyConfig {
169 id_unifiers: HashMap::from([("bbc_old".to_string(), "bbc_one".to_string())]),
170 ..Default::default()
171 };
172 let result = unify_entries(&entries, &config);
173 assert_eq!(result.len(), 1);
174 assert_eq!(result[0].tvg_id.as_deref(), Some("bbc_one"));
175 }
176
177 #[test]
178 fn unify_renames_titles() {
179 let entries = vec![make_entry("BBC World News", "", None)];
180 let config = UnifyConfig {
181 title_unifiers: HashMap::from([("World News".to_string(), "Global".to_string())]),
182 ..Default::default()
183 };
184 let result = unify_entries(&entries, &config);
185 assert_eq!(result.len(), 1);
186 assert_eq!(result[0].name.as_deref(), Some("BBC Global"));
187 }
188
189 #[test]
190 fn unify_deletes_entries_mapped_to_empty_title() {
191 let entries = vec![
192 make_entry("Remove Me", "", None),
193 make_entry("Keep Me", "", None),
194 ];
195 let config = UnifyConfig {
196 title_unifiers: HashMap::from([("Remove Me".to_string(), String::new())]),
197 ..Default::default()
198 };
199 let result = unify_entries(&entries, &config);
200 assert_eq!(result.len(), 1);
201 assert_eq!(result[0].name.as_deref(), Some("Keep Me"));
202 }
203
204 #[test]
205 fn unify_empty_config_is_identity() {
206 let entries = vec![make_entry("BBC One", "bbc.uk", None)];
207 let config = UnifyConfig::default();
208 let result = unify_entries(&entries, &config);
209 assert_eq!(result.len(), 1);
210 assert_eq!(result[0].name.as_deref(), Some("BBC One"));
211 }
212}