omni_dev/transcript/
error.rs1use thiserror::Error;
4
5pub type Result<T> = std::result::Result<T, TranscriptError>;
7
8#[derive(Error, Debug)]
10pub enum TranscriptError {
11 #[error("invalid transcript locator: {0}")]
13 InvalidLocator(String),
14
15 #[error("failed to parse response from media platform: {0}")]
17 ParseError(String),
18
19 #[error(
21 "no caption track for language `{requested}`; available: {}",
22 if available.is_empty() { "(none)".to_string() } else { available.join(", ") }
23 )]
24 LanguageNotFound {
25 requested: String,
27 available: Vec<String>,
29 },
30
31 #[error("only auto-generated captions are available for `{0}`; pass --auto to accept them")]
34 AutoCaptionsRequireOptIn(String),
35
36 #[error("media platform refused playback: status={status}{}", reason.as_deref().map(|r| format!(" ({r})")).unwrap_or_default())]
41 PlayabilityRefused {
42 status: String,
45 reason: Option<String>,
47 },
48
49 #[error("I/O error: {0}")]
51 Io(#[from] std::io::Error),
52
53 #[error("HTTP error: {0}")]
55 Http(#[from] reqwest::Error),
56
57 #[error("watch page at {url} did not contain the expected session token")]
63 MissingVisitorData {
64 url: String,
66 },
67}
68
69#[cfg(test)]
70mod tests {
71 use super::*;
72
73 #[test]
74 fn invalid_locator_display() {
75 let err = TranscriptError::InvalidLocator("not a youtube url".to_string());
76 let msg = err.to_string();
77 assert!(msg.contains("invalid transcript locator"));
78 assert!(msg.contains("not a youtube url"));
79 }
80
81 #[test]
82 fn parse_error_display() {
83 let err = TranscriptError::ParseError("missing field `videoId`".to_string());
84 assert!(err.to_string().contains("missing field"));
85 }
86
87 #[test]
88 fn language_not_found_with_available() {
89 let err = TranscriptError::LanguageNotFound {
90 requested: "fr".to_string(),
91 available: vec!["en".to_string(), "es".to_string()],
92 };
93 let msg = err.to_string();
94 assert!(msg.contains("`fr`"));
95 assert!(msg.contains("en, es"));
96 }
97
98 #[test]
99 fn language_not_found_with_empty_available() {
100 let err = TranscriptError::LanguageNotFound {
101 requested: "en".to_string(),
102 available: vec![],
103 };
104 let msg = err.to_string();
105 assert!(msg.contains("`en`"));
106 assert!(msg.contains("(none)"));
107 }
108
109 #[test]
110 fn auto_captions_require_opt_in_display() {
111 let err = TranscriptError::AutoCaptionsRequireOptIn("en".to_string());
112 let msg = err.to_string();
113 assert!(msg.contains("auto-generated"));
114 assert!(msg.contains("--auto"));
115 }
116
117 #[test]
118 fn playability_refused_with_reason() {
119 let err = TranscriptError::PlayabilityRefused {
120 status: "LOGIN_REQUIRED".to_string(),
121 reason: Some("Sign in to confirm your age".to_string()),
122 };
123 let msg = err.to_string();
124 assert!(msg.contains("LOGIN_REQUIRED"));
125 assert!(msg.contains("Sign in to confirm your age"));
126 }
127
128 #[test]
129 fn playability_refused_without_reason() {
130 let err = TranscriptError::PlayabilityRefused {
131 status: "UNPLAYABLE".to_string(),
132 reason: None,
133 };
134 let msg = err.to_string();
135 assert!(msg.contains("UNPLAYABLE"));
136 assert!(!msg.contains("()"));
137 }
138
139 #[test]
140 fn io_error_from_conversion() {
141 let io_err = std::io::Error::new(std::io::ErrorKind::PermissionDenied, "denied");
142 let err: TranscriptError = io_err.into();
143 assert!(matches!(err, TranscriptError::Io(_)));
144 assert!(err.to_string().contains("I/O error"));
145 }
146
147 #[test]
148 fn debug_impl_present() {
149 let err = TranscriptError::InvalidLocator("x".to_string());
150 let dbg = format!("{err:?}");
151 assert!(dbg.contains("InvalidLocator"));
152 }
153
154 #[test]
155 fn missing_visitor_data_display() {
156 let err = TranscriptError::MissingVisitorData {
157 url: "https://www.youtube.com/watch?v=dQw4w9WgXcQ".to_string(),
158 };
159 let msg = err.to_string();
160 assert!(msg.contains("watch page"));
161 assert!(msg.contains("https://www.youtube.com/watch?v=dQw4w9WgXcQ"));
162 }
163}