1use thiserror::Error;
10
11#[derive(Error, Debug)]
33pub enum SubXError {
34 #[error("I/O error: {0}")]
44 Io(#[from] std::io::Error),
45
46 #[error("Configuration error: {message}")]
50 Config {
51 message: String,
53 },
54
55 #[error("Subtitle format error [{format}]: {message}")]
59 SubtitleFormat {
60 format: String,
62 message: String,
64 },
65
66 #[error("AI service error: {0}")]
70 AiService(String),
71
72 #[error("Audio processing error: {message}")]
76 AudioProcessing {
77 message: String,
79 },
80
81 #[error("File matching error: {message}")]
85 FileMatching {
86 message: String,
88 },
89 #[error("File already exists: {0}")]
91 FileAlreadyExists(String),
92 #[error("File not found: {0}")]
94 FileNotFound(String),
95 #[error("Invalid file name: {0}")]
97 InvalidFileName(String),
98 #[error("File operation failed: {0}")]
100 FileOperationFailed(String),
101 #[error("{0}")]
103 CommandExecution(String),
104
105 #[error("Unknown error: {0}")]
107 Other(#[from] anyhow::Error),
108}
109
110#[cfg(test)]
112mod tests {
113 use super::*;
114 use std::io;
115
116 #[test]
117 fn test_config_error_creation() {
118 let error = SubXError::config("test config error");
119 assert!(matches!(error, SubXError::Config { .. }));
120 assert_eq!(error.to_string(), "Configuration error: test config error");
121 }
122
123 #[test]
124 fn test_subtitle_format_error_creation() {
125 let error = SubXError::subtitle_format("SRT", "invalid format");
126 assert!(matches!(error, SubXError::SubtitleFormat { .. }));
127 let msg = error.to_string();
128 assert!(msg.contains("SRT"));
129 assert!(msg.contains("invalid format"));
130 }
131
132 #[test]
133 fn test_audio_processing_error_creation() {
134 let error = SubXError::audio_processing("decode failed");
135 assert!(matches!(error, SubXError::AudioProcessing { .. }));
136 assert_eq!(error.to_string(), "Audio processing error: decode failed");
137 }
138
139 #[test]
140 fn test_file_matching_error_creation() {
141 let error = SubXError::file_matching("match failed");
142 assert!(matches!(error, SubXError::FileMatching { .. }));
143 assert_eq!(error.to_string(), "File matching error: match failed");
144 }
145
146 #[test]
147 fn test_io_error_conversion() {
148 let io_error = io::Error::new(io::ErrorKind::NotFound, "file not found");
149 let subx_error: SubXError = io_error.into();
150 assert!(matches!(subx_error, SubXError::Io(_)));
151 }
152
153 #[test]
154 fn test_exit_codes() {
155 assert_eq!(SubXError::config("test").exit_code(), 2);
156 assert_eq!(SubXError::subtitle_format("SRT", "test").exit_code(), 4);
157 assert_eq!(SubXError::audio_processing("test").exit_code(), 5);
158 assert_eq!(SubXError::file_matching("test").exit_code(), 6);
159 }
160
161 #[test]
162 fn test_user_friendly_messages() {
163 let config_error = SubXError::config("missing key");
164 let message = config_error.user_friendly_message();
165 assert!(message.contains("Configuration error:"));
166 assert!(message.contains("subx config --help"));
167
168 let ai_error = SubXError::ai_service("network failure".to_string());
169 let message = ai_error.user_friendly_message();
170 assert!(message.contains("AI service error:"));
171 assert!(message.contains("check network connection"));
172 }
173}
174
175impl From<reqwest::Error> for SubXError {
177 fn from(err: reqwest::Error) -> Self {
178 SubXError::AiService(err.to_string())
179 }
180}
181
182impl From<walkdir::Error> for SubXError {
184 fn from(err: walkdir::Error) -> Self {
185 SubXError::FileMatching {
186 message: err.to_string(),
187 }
188 }
189}
190impl From<symphonia::core::errors::Error> for SubXError {
192 fn from(err: symphonia::core::errors::Error) -> Self {
193 SubXError::audio_processing(err.to_string())
194 }
195}
196
197impl From<config::ConfigError> for SubXError {
199 fn from(err: config::ConfigError) -> Self {
200 SubXError::Config {
201 message: format!("Configuration loading error: {}", err),
202 }
203 }
204}
205
206pub type SubXResult<T> = Result<T, SubXError>;
208
209impl SubXError {
210 pub fn config<S: Into<String>>(message: S) -> Self {
220 SubXError::Config {
221 message: message.into(),
222 }
223 }
224
225 pub fn subtitle_format<S1, S2>(format: S1, message: S2) -> Self
235 where
236 S1: Into<String>,
237 S2: Into<String>,
238 {
239 SubXError::SubtitleFormat {
240 format: format.into(),
241 message: message.into(),
242 }
243 }
244
245 pub fn audio_processing<S: Into<String>>(message: S) -> Self {
255 SubXError::AudioProcessing {
256 message: message.into(),
257 }
258 }
259
260 pub fn ai_service<S: Into<String>>(message: S) -> Self {
270 SubXError::AiService(message.into())
271 }
272
273 pub fn file_matching<S: Into<String>>(message: S) -> Self {
283 SubXError::FileMatching {
284 message: message.into(),
285 }
286 }
287 pub fn parallel_processing(msg: String) -> Self {
289 SubXError::CommandExecution(format!("Parallel processing error: {}", msg))
290 }
291 pub fn task_execution_failed(task_id: String, reason: String) -> Self {
293 SubXError::CommandExecution(format!("Task {} execution failed: {}", task_id, reason))
294 }
295 pub fn worker_pool_exhausted() -> Self {
297 SubXError::CommandExecution("Worker pool exhausted".to_string())
298 }
299 pub fn task_timeout(task_id: String, duration: std::time::Duration) -> Self {
301 SubXError::CommandExecution(format!(
302 "Task {} timed out (limit: {:?})",
303 task_id, duration
304 ))
305 }
306 pub fn dialogue_detection_failed<S: Into<String>>(msg: S) -> Self {
308 SubXError::AudioProcessing {
309 message: format!("Dialogue detection failed: {}", msg.into()),
310 }
311 }
312 pub fn invalid_audio_format<S: Into<String>>(format: S) -> Self {
314 SubXError::AudioProcessing {
315 message: format!("Unsupported audio format: {}", format.into()),
316 }
317 }
318 pub fn dialogue_segment_invalid<S: Into<String>>(reason: S) -> Self {
320 SubXError::AudioProcessing {
321 message: format!("Invalid dialogue segment: {}", reason.into()),
322 }
323 }
324 pub fn exit_code(&self) -> i32 {
333 match self {
334 SubXError::Io(_) => 1,
335 SubXError::Config { .. } => 2,
336 SubXError::AiService(_) => 3,
337 SubXError::SubtitleFormat { .. } => 4,
338 SubXError::AudioProcessing { .. } => 5,
339 SubXError::FileMatching { .. } => 6,
340 _ => 1,
341 }
342 }
343
344 pub fn user_friendly_message(&self) -> String {
354 match self {
355 SubXError::Io(e) => format!("File operation error: {}", e),
356 SubXError::Config { message } => format!(
357 "Configuration error: {}\nHint: run 'subx config --help' for details",
358 message
359 ),
360 SubXError::AiService(msg) => format!(
361 "AI service error: {}\nHint: check network connection and API key settings",
362 msg
363 ),
364 SubXError::SubtitleFormat { message, .. } => format!(
365 "Subtitle processing error: {}\nHint: check file format and encoding",
366 message
367 ),
368 SubXError::AudioProcessing { message } => format!(
369 "Audio processing error: {}\nHint: ensure media file integrity and support",
370 message
371 ),
372 SubXError::FileMatching { message } => format!(
373 "File matching error: {}\nHint: verify file paths and patterns",
374 message
375 ),
376 SubXError::FileAlreadyExists(path) => format!("File already exists: {}", path),
377 SubXError::FileNotFound(path) => format!("File not found: {}", path),
378 SubXError::InvalidFileName(name) => format!("Invalid file name: {}", name),
379 SubXError::FileOperationFailed(msg) => format!("File operation failed: {}", msg),
380 SubXError::CommandExecution(msg) => msg.clone(),
381 SubXError::Other(err) => {
382 format!("Unknown error: {}\nHint: please report this issue", err)
383 }
384 }
385 }
386}
387
388impl From<aus::AudioError> for SubXError {
390 fn from(error: aus::AudioError) -> Self {
391 SubXError::audio_processing(format!("{:?}", error))
392 }
393}
394
395impl From<aus::spectrum::SpectrumError> for SubXError {
396 fn from(error: aus::spectrum::SpectrumError) -> Self {
397 SubXError::audio_processing(format!("Spectrum processing error: {:?}", error))
398 }
399}