omni_dev/cli/transcript/
fetch.rs1use std::fs;
9
10use anyhow::{Context, Result};
11use clap::Parser;
12
13use crate::cli::transcript::format::CliFormat;
14use crate::transcript::detect::detect;
15use crate::transcript::format::Format;
16use crate::transcript::source::{FetchOpts, Transcript};
17
18#[derive(Parser)]
20pub struct FetchCommand {
21 pub url: String,
24
25 #[arg(long, default_value = "en")]
28 pub lang: String,
29
30 #[arg(long, value_enum, default_value_t = CliFormat::Srt)]
32 pub format: CliFormat,
33
34 #[arg(long)]
37 pub auto: bool,
38
39 #[arg(long, value_name = "LANG")]
42 pub translate: Option<String>,
43
44 #[arg(short, long)]
46 pub output: Option<String>,
47}
48
49impl FetchCommand {
50 pub async fn execute(self) -> Result<()> {
52 let source = detect(&self.url)?;
53 let transcript = source.fetch(&self.url, &self.fetch_opts()).await?;
54 render_and_write(&transcript, self.format, self.output.as_deref())
55 }
56
57 fn fetch_opts(&self) -> FetchOpts {
59 FetchOpts {
60 language: self.lang.clone(),
61 allow_auto: self.auto,
62 translate_to: self.translate.clone(),
63 }
64 }
65}
66
67fn render_and_write(
72 transcript: &Transcript,
73 format: CliFormat,
74 output: Option<&str>,
75) -> Result<()> {
76 let rendered = Format::from(format).render(transcript)?;
77 write_output(&rendered, output)
78}
79
80fn write_output(text: &str, file: Option<&str>) -> Result<()> {
81 if let Some(path) = file {
82 fs::write(path, text).with_context(|| format!("Failed to write to {path}"))
83 } else {
84 print!("{text}");
85 Ok(())
86 }
87}
88
89#[cfg(test)]
90#[allow(clippy::unwrap_used, clippy::expect_used)]
91mod tests {
92 use super::*;
93 use clap::{CommandFactory, FromArgMatches};
94
95 fn parse(args: &[&str]) -> FetchCommand {
98 let cmd = FetchCommand::command().no_binary_name(true);
99 let matches = cmd.try_get_matches_from(args).unwrap();
100 FetchCommand::from_arg_matches(&matches).unwrap()
101 }
102
103 #[test]
104 fn fetch_command_defaults() {
105 let cmd = parse(&["https://youtu.be/dQw4w9WgXcQ"]);
106 assert_eq!(cmd.url, "https://youtu.be/dQw4w9WgXcQ");
107 assert_eq!(cmd.lang, "en");
108 assert_eq!(cmd.format, CliFormat::Srt);
109 assert!(!cmd.auto);
110 assert_eq!(cmd.translate, None);
111 assert_eq!(cmd.output, None);
112 }
113
114 #[test]
115 fn fetch_command_all_flags() {
116 let cmd = parse(&[
117 "abc",
118 "--lang",
119 "fr",
120 "--format",
121 "vtt",
122 "--auto",
123 "--translate",
124 "en",
125 "--output",
126 "out.vtt",
127 ]);
128 assert_eq!(cmd.url, "abc");
129 assert_eq!(cmd.lang, "fr");
130 assert_eq!(cmd.format, CliFormat::Vtt);
131 assert!(cmd.auto);
132 assert_eq!(cmd.translate.as_deref(), Some("en"));
133 assert_eq!(cmd.output.as_deref(), Some("out.vtt"));
134 }
135
136 #[test]
137 fn fetch_command_short_output_flag() {
138 let cmd = parse(&["abc", "-o", "out.srt"]);
139 assert_eq!(cmd.output.as_deref(), Some("out.srt"));
140 }
141
142 #[test]
143 fn write_output_to_file_writes_bytes() {
144 let dir = tempfile::tempdir().unwrap();
145 let path = dir.path().join("out.txt");
146 write_output("hello\n", Some(path.to_str().unwrap())).unwrap();
147 let read = std::fs::read_to_string(&path).unwrap();
148 assert_eq!(read, "hello\n");
149 }
150
151 #[test]
152 fn write_output_to_stdout_returns_ok() {
153 write_output("noop", None).unwrap();
155 }
156
157 #[test]
158 fn write_output_invalid_path_errors() {
159 let err = write_output("x", Some("/nonexistent_dir_for_test/out.txt")).unwrap_err();
160 assert!(err.to_string().contains("Failed to write"));
161 }
162
163 use crate::transcript::cue::Cue;
164 use crate::transcript::source::{TrackKind, Transcript};
165
166 #[test]
167 fn fetch_opts_maps_flags() {
168 let cmd = parse(&["abc", "--lang", "fr", "--auto", "--translate", "en"]);
169 let opts = cmd.fetch_opts();
170 assert_eq!(opts.language, "fr");
171 assert!(opts.allow_auto);
172 assert_eq!(opts.translate_to.as_deref(), Some("en"));
173 }
174
175 #[test]
176 fn render_and_write_renders_selected_format_to_file() {
177 let dir = tempfile::tempdir().unwrap();
178 let path = dir.path().join("out.txt");
179 let transcript = Transcript {
180 source: "stub".into(),
181 locator_id: "vid".into(),
182 language: "en".into(),
183 kind: TrackKind::Manual,
184 cues: vec![Cue::new(0, 1_000, "hello world")],
185 };
186 render_and_write(&transcript, CliFormat::Txt, Some(path.to_str().unwrap())).unwrap();
187 let out = std::fs::read_to_string(&path).unwrap();
188 assert!(out.contains("hello world"));
189 }
190}