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("API error [{source:?}]: {message}")]
77 Api {
78 message: String,
80 source: ApiErrorSource,
82 },
83
84 #[error("Audio processing error: {message}")]
88 AudioProcessing {
89 message: String,
91 },
92
93 #[error("File matching error: {message}")]
97 FileMatching {
98 message: String,
100 },
101 #[error("File already exists: {0}")]
103 FileAlreadyExists(String),
104 #[error("File not found: {0}")]
106 FileNotFound(String),
107 #[error("Invalid file name: {0}")]
109 InvalidFileName(String),
110 #[error("File operation failed: {0}")]
112 FileOperationFailed(String),
113 #[error("{0}")]
115 CommandExecution(String),
116
117 #[error("No input path specified")]
119 NoInputSpecified,
120
121 #[error("Invalid path: {0}")]
123 InvalidPath(std::path::PathBuf),
124
125 #[error("Path not found: {0}")]
127 PathNotFound(std::path::PathBuf),
128
129 #[error("Unable to read directory: {path}")]
131 DirectoryReadError {
132 path: std::path::PathBuf,
134 #[source]
136 source: std::io::Error,
137 },
138
139 #[error(
141 "Invalid sync configuration: please specify video and subtitle files, or use -i parameter for batch processing"
142 )]
143 InvalidSyncConfiguration,
144
145 #[error("Unsupported file type: {0}")]
147 UnsupportedFileType(String),
148
149 #[error("Unknown error: {0}")]
151 Other(#[from] anyhow::Error),
152}
153
154#[cfg(test)]
156mod tests {
157 use super::*;
158 use std::io;
159
160 #[test]
161 fn test_config_error_creation() {
162 let error = SubXError::config("test config error");
163 assert!(matches!(error, SubXError::Config { .. }));
164 assert_eq!(error.to_string(), "Configuration error: test config error");
165 }
166
167 #[test]
168 fn test_subtitle_format_error_creation() {
169 let error = SubXError::subtitle_format("SRT", "invalid format");
170 assert!(matches!(error, SubXError::SubtitleFormat { .. }));
171 let msg = error.to_string();
172 assert!(msg.contains("SRT"));
173 assert!(msg.contains("invalid format"));
174 }
175
176 #[test]
177 fn test_audio_processing_error_creation() {
178 let error = SubXError::audio_processing("decode failed");
179 assert!(matches!(error, SubXError::AudioProcessing { .. }));
180 assert_eq!(error.to_string(), "Audio processing error: decode failed");
181 }
182
183 #[test]
184 fn test_file_matching_error_creation() {
185 let error = SubXError::file_matching("match failed");
186 assert!(matches!(error, SubXError::FileMatching { .. }));
187 assert_eq!(error.to_string(), "File matching error: match failed");
188 }
189
190 #[test]
191 fn test_io_error_conversion() {
192 let io_error = io::Error::new(io::ErrorKind::NotFound, "file not found");
193 let subx_error: SubXError = io_error.into();
194 assert!(matches!(subx_error, SubXError::Io(_)));
195 }
196
197 #[test]
198 fn test_exit_codes() {
199 assert_eq!(SubXError::config("test").exit_code(), 2);
200 assert_eq!(SubXError::subtitle_format("SRT", "test").exit_code(), 4);
201 assert_eq!(SubXError::audio_processing("test").exit_code(), 5);
202 assert_eq!(SubXError::file_matching("test").exit_code(), 6);
203 }
204
205 #[test]
206 fn test_user_friendly_messages() {
207 let config_error = SubXError::config("missing key");
208 let message = config_error.user_friendly_message();
209 assert!(message.contains("Configuration error:"));
210 assert!(message.contains("subx-cli config --help"));
211
212 let ai_error = SubXError::ai_service("network failure".to_string());
213 let message = ai_error.user_friendly_message();
214 assert!(message.contains("AI service error:"));
215 assert!(message.contains("check network connection"));
216 }
217}
218
219impl From<reqwest::Error> for SubXError {
221 fn from(err: reqwest::Error) -> Self {
222 SubXError::AiService(err.to_string())
223 }
224}
225
226impl From<walkdir::Error> for SubXError {
228 fn from(err: walkdir::Error) -> Self {
229 SubXError::FileMatching {
230 message: err.to_string(),
231 }
232 }
233}
234impl From<symphonia::core::errors::Error> for SubXError {
236 fn from(err: symphonia::core::errors::Error) -> Self {
237 SubXError::audio_processing(err.to_string())
238 }
239}
240
241impl From<config::ConfigError> for SubXError {
243 fn from(err: config::ConfigError) -> Self {
244 match err {
245 config::ConfigError::NotFound(path) => SubXError::Config {
246 message: format!("Configuration file not found: {}", path),
247 },
248 config::ConfigError::Message(msg) => SubXError::Config { message: msg },
249 _ => SubXError::Config {
250 message: format!("Configuration error: {}", err),
251 },
252 }
253 }
254}
255
256impl From<serde_json::Error> for SubXError {
257 fn from(err: serde_json::Error) -> Self {
258 SubXError::Config {
259 message: format!("JSON serialization/deserialization error: {}", err),
260 }
261 }
262}
263
264pub type SubXResult<T> = Result<T, SubXError>;
266
267impl SubXError {
268 pub fn config<S: Into<String>>(message: S) -> Self {
278 SubXError::Config {
279 message: message.into(),
280 }
281 }
282
283 pub fn subtitle_format<S1, S2>(format: S1, message: S2) -> Self
293 where
294 S1: Into<String>,
295 S2: Into<String>,
296 {
297 SubXError::SubtitleFormat {
298 format: format.into(),
299 message: message.into(),
300 }
301 }
302
303 pub fn audio_processing<S: Into<String>>(message: S) -> Self {
313 SubXError::AudioProcessing {
314 message: message.into(),
315 }
316 }
317
318 pub fn ai_service<S: Into<String>>(message: S) -> Self {
328 SubXError::AiService(message.into())
329 }
330
331 pub fn file_matching<S: Into<String>>(message: S) -> Self {
341 SubXError::FileMatching {
342 message: message.into(),
343 }
344 }
345 pub fn parallel_processing(msg: String) -> Self {
347 SubXError::CommandExecution(format!("Parallel processing error: {}", msg))
348 }
349 pub fn task_execution_failed(task_id: String, reason: String) -> Self {
351 SubXError::CommandExecution(format!("Task {} execution failed: {}", task_id, reason))
352 }
353 pub fn worker_pool_exhausted() -> Self {
355 SubXError::CommandExecution("Worker pool exhausted".to_string())
356 }
357 pub fn task_timeout(task_id: String, duration: std::time::Duration) -> Self {
359 SubXError::CommandExecution(format!(
360 "Task {} timed out (limit: {:?})",
361 task_id, duration
362 ))
363 }
364 pub fn dialogue_detection_failed<S: Into<String>>(msg: S) -> Self {
366 SubXError::AudioProcessing {
367 message: format!("Dialogue detection failed: {}", msg.into()),
368 }
369 }
370 pub fn invalid_audio_format<S: Into<String>>(format: S) -> Self {
372 SubXError::AudioProcessing {
373 message: format!("Unsupported audio format: {}", format.into()),
374 }
375 }
376 pub fn dialogue_segment_invalid<S: Into<String>>(reason: S) -> Self {
378 SubXError::AudioProcessing {
379 message: format!("Invalid dialogue segment: {}", reason.into()),
380 }
381 }
382 pub fn exit_code(&self) -> i32 {
391 match self {
392 SubXError::Io(_) => 1,
393 SubXError::Config { .. } => 2,
394 SubXError::Api { .. } => 3,
395 SubXError::AiService(_) => 3,
396 SubXError::SubtitleFormat { .. } => 4,
397 SubXError::AudioProcessing { .. } => 5,
398 SubXError::FileMatching { .. } => 6,
399 _ => 1,
400 }
401 }
402
403 pub fn user_friendly_message(&self) -> String {
413 match self {
414 SubXError::Io(e) => format!("File operation error: {}", e),
415 SubXError::Config { message } => format!(
416 "Configuration error: {}\nHint: run 'subx-cli config --help' for details",
417 message
418 ),
419 SubXError::Api { message, source } => format!(
420 "API error ({:?}): {}\nHint: check network connection and API key settings",
421 source, message
422 ),
423 SubXError::AiService(msg) => format!(
424 "AI service error: {}\nHint: check network connection and API key settings",
425 msg
426 ),
427 SubXError::SubtitleFormat { message, .. } => format!(
428 "Subtitle processing error: {}\nHint: check file format and encoding",
429 message
430 ),
431 SubXError::AudioProcessing { message } => format!(
432 "Audio processing error: {}\nHint: ensure media file integrity and support",
433 message
434 ),
435 SubXError::FileMatching { message } => format!(
436 "File matching error: {}\nHint: verify file paths and patterns",
437 message
438 ),
439 SubXError::FileAlreadyExists(path) => format!("File already exists: {}", path),
440 SubXError::FileNotFound(path) => format!("File not found: {}", path),
441 SubXError::InvalidFileName(name) => format!("Invalid file name: {}", name),
442 SubXError::FileOperationFailed(msg) => format!("File operation failed: {}", msg),
443 SubXError::CommandExecution(msg) => msg.clone(),
444 SubXError::Other(err) => {
445 format!("Unknown error: {}\nHint: please report this issue", err)
446 }
447 _ => format!("Error: {}", self),
448 }
449 }
450}
451
452impl SubXError {
454 pub fn whisper_api<T: Into<String>>(message: T) -> Self {
464 Self::Api {
465 message: message.into(),
466 source: ApiErrorSource::Whisper,
467 }
468 }
469
470 pub fn audio_extraction<T: Into<String>>(message: T) -> Self {
480 Self::AudioProcessing {
481 message: message.into(),
482 }
483 }
484}
485
486#[derive(Debug, thiserror::Error)]
491pub enum ApiErrorSource {
492 #[error("OpenAI")]
494 OpenAI,
495 #[error("Whisper")]
497 Whisper,
498}
499
500impl From<Box<dyn std::error::Error>> for SubXError {
502 fn from(err: Box<dyn std::error::Error>) -> Self {
503 SubXError::audio_processing(err.to_string())
504 }
505}