omni_dev/cli/transcript/youtube/
fetch.rs1use std::fs;
5
6use anyhow::{Context, Result};
7use clap::Parser;
8
9use crate::cli::transcript::format::CliFormat;
10use crate::transcript::format::Format;
11use crate::transcript::source::{FetchOpts, TranscriptSource};
12use crate::transcript::sources::youtube::Youtube;
13
14#[derive(Parser)]
16pub struct FetchCommand {
17 pub url: String,
19
20 #[arg(long, default_value = "en")]
23 pub lang: String,
24
25 #[arg(short = 'o', long, value_enum, default_value_t = CliFormat::Srt)]
27 pub output: CliFormat,
28
29 #[arg(long = "format", hide = true)]
31 pub format: Option<CliFormat>,
32
33 #[arg(long)]
36 pub auto: bool,
37
38 #[arg(long, value_name = "LANG")]
41 pub translate: Option<String>,
42
43 #[arg(long = "out-file", value_name = "PATH")]
45 pub out_file: Option<String>,
46}
47
48impl FetchCommand {
49 pub async fn execute(mut self) -> Result<()> {
51 if let Some(format) = self.format.take() {
52 eprintln!("warning: --format is deprecated; use -o/--output instead");
53 self.output = format;
54 }
55
56 let yt = Youtube::new()?;
57 let opts = FetchOpts {
58 language: self.lang,
59 allow_auto: self.auto,
60 translate_to: self.translate,
61 };
62 let transcript = yt.fetch(&self.url, &opts).await?;
63 let rendered = Format::from(self.output).render(&transcript)?;
64 write_output(&rendered, self.out_file.as_deref())
65 }
66}
67
68fn write_output(text: &str, file: Option<&str>) -> Result<()> {
69 if let Some(path) = file {
70 fs::write(path, text).with_context(|| format!("Failed to write to {path}"))
71 } else {
72 print!("{text}");
73 Ok(())
74 }
75}
76
77#[cfg(test)]
78#[allow(clippy::unwrap_used, clippy::expect_used)]
79mod tests {
80 use super::*;
81 use clap::CommandFactory;
82
83 fn parse(args: &[&str]) -> FetchCommand {
86 let cmd = FetchCommand::command().no_binary_name(true);
87 let matches = cmd.try_get_matches_from(args).unwrap();
88 FetchCommand::from_arg_matches(&matches).unwrap()
89 }
90
91 use clap::FromArgMatches;
92
93 #[test]
94 fn fetch_command_defaults() {
95 let cmd = parse(&["https://youtu.be/dQw4w9WgXcQ"]);
96 assert_eq!(cmd.url, "https://youtu.be/dQw4w9WgXcQ");
97 assert_eq!(cmd.lang, "en");
98 assert_eq!(cmd.output, CliFormat::Srt);
99 assert_eq!(cmd.format, None);
100 assert!(!cmd.auto);
101 assert_eq!(cmd.translate, None);
102 assert_eq!(cmd.out_file, None);
103 }
104
105 #[test]
106 fn fetch_command_all_flags() {
107 let cmd = parse(&[
108 "abc",
109 "--lang",
110 "fr",
111 "-o",
112 "vtt",
113 "--auto",
114 "--translate",
115 "en",
116 "--out-file",
117 "out.vtt",
118 ]);
119 assert_eq!(cmd.url, "abc");
120 assert_eq!(cmd.lang, "fr");
121 assert_eq!(cmd.output, CliFormat::Vtt);
122 assert!(cmd.auto);
123 assert_eq!(cmd.translate.as_deref(), Some("en"));
124 assert_eq!(cmd.out_file.as_deref(), Some("out.vtt"));
125 }
126
127 #[test]
128 fn fetch_command_out_file_flag() {
129 let cmd = parse(&["abc", "--out-file", "out.srt"]);
130 assert_eq!(cmd.out_file.as_deref(), Some("out.srt"));
131 }
132
133 #[test]
134 fn fetch_command_output_accepts_each_variant() {
135 for (arg, want) in [
136 ("srt", CliFormat::Srt),
137 ("vtt", CliFormat::Vtt),
138 ("txt", CliFormat::Txt),
139 ("json", CliFormat::Json),
140 ] {
141 let cmd = parse(&["abc", "-o", arg]);
142 assert_eq!(cmd.output, want);
143 }
144 }
145
146 #[test]
147 fn fetch_command_deprecated_format_alias_still_parses() {
148 let cmd = parse(&["abc", "--format", "vtt"]);
151 assert_eq!(cmd.format, Some(CliFormat::Vtt));
152 assert_eq!(cmd.output, CliFormat::Srt);
153 }
154
155 #[tokio::test]
156 async fn fetch_command_execute_folds_deprecated_format_flag() {
157 let cmd = FetchCommand {
161 url: "abc".to_string(),
162 lang: "en".to_string(),
163 output: CliFormat::Srt,
164 format: Some(CliFormat::Vtt),
165 auto: false,
166 translate: None,
167 out_file: None,
168 };
169 assert!(cmd.execute().await.is_err());
170 }
171
172 #[test]
173 fn write_output_to_file_writes_bytes() {
174 let dir = tempfile::tempdir().unwrap();
175 let path = dir.path().join("out.txt");
176 write_output("hello\n", Some(path.to_str().unwrap())).unwrap();
177 let read = std::fs::read_to_string(&path).unwrap();
178 assert_eq!(read, "hello\n");
179 }
180
181 #[test]
182 fn write_output_to_stdout_returns_ok() {
183 write_output("noop", None).unwrap();
185 }
186
187 #[test]
188 fn write_output_invalid_path_errors() {
189 let err = write_output("x", Some("/nonexistent_dir_for_test/out.txt")).unwrap_err();
190 assert!(err.to_string().contains("Failed to write"));
191 }
192}