audiobook_forge/models/
match_models.rs1use super::AudibleMetadata;
4use serde::{Deserialize, Serialize};
5use std::collections::HashMap;
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
9pub enum MatchConfidence {
10 Strong,
12 Medium,
14 Low,
16 None,
18}
19
20#[derive(Debug, Clone, Serialize, Deserialize)]
22pub struct MetadataDistance {
23 penalties: HashMap<String, f64>,
25 total: f64,
27}
28
29impl MetadataDistance {
30 pub fn new() -> Self {
32 Self {
33 penalties: HashMap::new(),
34 total: 0.0,
35 }
36 }
37
38 pub fn add_penalty(&mut self, field: &str, distance: f64, weight: f64) {
40 let weighted = distance * weight;
41 self.penalties.insert(field.to_string(), distance);
42 self.total += weighted;
43 }
44
45 pub fn total_distance(&self) -> f64 {
47 self.total
48 }
49
50 pub fn get_penalty(&self, field: &str) -> Option<f64> {
52 self.penalties.get(field).copied()
53 }
54
55 pub fn penalties(&self) -> &HashMap<String, f64> {
57 &self.penalties
58 }
59}
60
61impl Default for MetadataDistance {
62 fn default() -> Self {
63 Self::new()
64 }
65}
66
67#[derive(Debug, Clone, Serialize, Deserialize)]
69pub struct MatchCandidate {
70 pub distance: MetadataDistance,
72 pub metadata: AudibleMetadata,
74 pub confidence: MatchConfidence,
76}
77
78#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
80pub enum MetadataSource {
81 Embedded,
83 Filename,
85 Manual,
87}
88
89#[derive(Debug, Clone, Serialize, Deserialize)]
91pub struct CurrentMetadata {
92 pub title: Option<String>,
94 pub author: Option<String>,
96 pub year: Option<u32>,
98 pub duration: Option<f64>,
100 pub source: MetadataSource,
102}
103
104impl CurrentMetadata {
105 pub fn is_sufficient(&self) -> bool {
107 self.title.is_some() || self.author.is_some()
108 }
109
110 pub fn merge_with(self, other: CurrentMetadata) -> CurrentMetadata {
112 CurrentMetadata {
113 title: self.title.or(other.title),
114 author: self.author.or(other.author),
115 year: self.year.or(other.year),
116 duration: self.duration.or(other.duration),
117 source: self.source, }
119 }
120}
121
122#[cfg(test)]
123mod tests {
124 use super::*;
125
126 #[test]
127 fn test_metadata_distance() {
128 let mut distance = MetadataDistance::new();
129 distance.add_penalty("title", 0.1, 0.4); distance.add_penalty("author", 0.2, 0.3); assert!((distance.total_distance() - 0.10).abs() < 0.001);
133 assert_eq!(distance.get_penalty("title"), Some(0.1));
134 assert_eq!(distance.get_penalty("author"), Some(0.2));
135 }
136
137 #[test]
138 fn test_current_metadata_is_sufficient() {
139 let metadata = CurrentMetadata {
140 title: Some("Test".to_string()),
141 author: None,
142 year: None,
143 duration: None,
144 source: MetadataSource::Embedded,
145 };
146 assert!(metadata.is_sufficient());
147
148 let empty = CurrentMetadata {
149 title: None,
150 author: None,
151 year: None,
152 duration: None,
153 source: MetadataSource::Embedded,
154 };
155 assert!(!empty.is_sufficient());
156 }
157
158 #[test]
159 fn test_current_metadata_merge() {
160 let embedded = CurrentMetadata {
161 title: Some("Title from tags".to_string()),
162 author: None,
163 year: Some(2020),
164 duration: None,
165 source: MetadataSource::Embedded,
166 };
167
168 let filename = CurrentMetadata {
169 title: Some("Title from filename".to_string()),
170 author: Some("Author from filename".to_string()),
171 year: None,
172 duration: None,
173 source: MetadataSource::Filename,
174 };
175
176 let merged = embedded.merge_with(filename);
177
178 assert_eq!(merged.title, Some("Title from tags".to_string()));
180 assert_eq!(merged.author, Some("Author from filename".to_string()));
182 assert_eq!(merged.year, Some(2020));
184 assert_eq!(merged.source, MetadataSource::Embedded);
186 }
187}