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 #[error("could not resolve a YouTube channel ID from `{input}`")]
74 ChannelNotFound {
75 input: String,
77 },
78}
79
80#[cfg(test)]
81mod tests {
82 use super::*;
83
84 #[test]
85 fn invalid_locator_display() {
86 let err = TranscriptError::InvalidLocator("not a youtube url".to_string());
87 let msg = err.to_string();
88 assert!(msg.contains("invalid transcript locator"));
89 assert!(msg.contains("not a youtube url"));
90 }
91
92 #[test]
93 fn parse_error_display() {
94 let err = TranscriptError::ParseError("missing field `videoId`".to_string());
95 assert!(err.to_string().contains("missing field"));
96 }
97
98 #[test]
99 fn language_not_found_with_available() {
100 let err = TranscriptError::LanguageNotFound {
101 requested: "fr".to_string(),
102 available: vec!["en".to_string(), "es".to_string()],
103 };
104 let msg = err.to_string();
105 assert!(msg.contains("`fr`"));
106 assert!(msg.contains("en, es"));
107 }
108
109 #[test]
110 fn language_not_found_with_empty_available() {
111 let err = TranscriptError::LanguageNotFound {
112 requested: "en".to_string(),
113 available: vec![],
114 };
115 let msg = err.to_string();
116 assert!(msg.contains("`en`"));
117 assert!(msg.contains("(none)"));
118 }
119
120 #[test]
121 fn auto_captions_require_opt_in_display() {
122 let err = TranscriptError::AutoCaptionsRequireOptIn("en".to_string());
123 let msg = err.to_string();
124 assert!(msg.contains("auto-generated"));
125 assert!(msg.contains("--auto"));
126 }
127
128 #[test]
129 fn playability_refused_with_reason() {
130 let err = TranscriptError::PlayabilityRefused {
131 status: "LOGIN_REQUIRED".to_string(),
132 reason: Some("Sign in to confirm your age".to_string()),
133 };
134 let msg = err.to_string();
135 assert!(msg.contains("LOGIN_REQUIRED"));
136 assert!(msg.contains("Sign in to confirm your age"));
137 }
138
139 #[test]
140 fn playability_refused_without_reason() {
141 let err = TranscriptError::PlayabilityRefused {
142 status: "UNPLAYABLE".to_string(),
143 reason: None,
144 };
145 let msg = err.to_string();
146 assert!(msg.contains("UNPLAYABLE"));
147 assert!(!msg.contains("()"));
148 }
149
150 #[test]
151 fn io_error_from_conversion() {
152 let io_err = std::io::Error::new(std::io::ErrorKind::PermissionDenied, "denied");
153 let err: TranscriptError = io_err.into();
154 assert!(matches!(err, TranscriptError::Io(_)));
155 assert!(err.to_string().contains("I/O error"));
156 }
157
158 #[test]
159 fn debug_impl_present() {
160 let err = TranscriptError::InvalidLocator("x".to_string());
161 let dbg = format!("{err:?}");
162 assert!(dbg.contains("InvalidLocator"));
163 }
164
165 #[test]
166 fn missing_visitor_data_display() {
167 let err = TranscriptError::MissingVisitorData {
168 url: "https://www.youtube.com/watch?v=dQw4w9WgXcQ".to_string(),
169 };
170 let msg = err.to_string();
171 assert!(msg.contains("watch page"));
172 assert!(msg.contains("https://www.youtube.com/watch?v=dQw4w9WgXcQ"));
173 }
174}