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(long, value_enum, default_value_t = CliFormat::Srt)]
27 pub format: CliFormat,
28
29 #[arg(long)]
32 pub auto: bool,
33
34 #[arg(long, value_name = "LANG")]
37 pub translate: Option<String>,
38
39 #[arg(short, long)]
41 pub output: Option<String>,
42}
43
44impl FetchCommand {
45 pub async fn execute(self) -> Result<()> {
47 let yt = Youtube::new()?;
48 let opts = FetchOpts {
49 language: self.lang,
50 allow_auto: self.auto,
51 translate_to: self.translate,
52 };
53 let transcript = yt.fetch(&self.url, &opts).await?;
54 let rendered = Format::from(self.format).render(&transcript)?;
55 write_output(&rendered, self.output.as_deref())
56 }
57}
58
59fn write_output(text: &str, file: Option<&str>) -> Result<()> {
60 if let Some(path) = file {
61 fs::write(path, text).with_context(|| format!("Failed to write to {path}"))
62 } else {
63 print!("{text}");
64 Ok(())
65 }
66}
67
68#[cfg(test)]
69#[allow(clippy::unwrap_used, clippy::expect_used)]
70mod tests {
71 use super::*;
72 use clap::CommandFactory;
73
74 fn parse(args: &[&str]) -> FetchCommand {
77 let cmd = FetchCommand::command().no_binary_name(true);
78 let matches = cmd.try_get_matches_from(args).unwrap();
79 FetchCommand::from_arg_matches(&matches).unwrap()
80 }
81
82 use clap::FromArgMatches;
83
84 #[test]
85 fn fetch_command_defaults() {
86 let cmd = parse(&["https://youtu.be/dQw4w9WgXcQ"]);
87 assert_eq!(cmd.url, "https://youtu.be/dQw4w9WgXcQ");
88 assert_eq!(cmd.lang, "en");
89 assert_eq!(cmd.format, CliFormat::Srt);
90 assert!(!cmd.auto);
91 assert_eq!(cmd.translate, None);
92 assert_eq!(cmd.output, None);
93 }
94
95 #[test]
96 fn fetch_command_all_flags() {
97 let cmd = parse(&[
98 "abc",
99 "--lang",
100 "fr",
101 "--format",
102 "vtt",
103 "--auto",
104 "--translate",
105 "en",
106 "--output",
107 "out.vtt",
108 ]);
109 assert_eq!(cmd.url, "abc");
110 assert_eq!(cmd.lang, "fr");
111 assert_eq!(cmd.format, CliFormat::Vtt);
112 assert!(cmd.auto);
113 assert_eq!(cmd.translate.as_deref(), Some("en"));
114 assert_eq!(cmd.output.as_deref(), Some("out.vtt"));
115 }
116
117 #[test]
118 fn fetch_command_short_output_flag() {
119 let cmd = parse(&["abc", "-o", "out.srt"]);
120 assert_eq!(cmd.output.as_deref(), Some("out.srt"));
121 }
122
123 #[test]
124 fn fetch_command_format_accepts_each_variant() {
125 for (arg, want) in [
126 ("srt", CliFormat::Srt),
127 ("vtt", CliFormat::Vtt),
128 ("txt", CliFormat::Txt),
129 ("json", CliFormat::Json),
130 ] {
131 let cmd = parse(&["abc", "--format", arg]);
132 assert_eq!(cmd.format, want);
133 }
134 }
135
136 #[test]
137 fn write_output_to_file_writes_bytes() {
138 let dir = tempfile::tempdir().unwrap();
139 let path = dir.path().join("out.txt");
140 write_output("hello\n", Some(path.to_str().unwrap())).unwrap();
141 let read = std::fs::read_to_string(&path).unwrap();
142 assert_eq!(read, "hello\n");
143 }
144
145 #[test]
146 fn write_output_to_stdout_returns_ok() {
147 write_output("noop", None).unwrap();
149 }
150
151 #[test]
152 fn write_output_invalid_path_errors() {
153 let err = write_output("x", Some("/nonexistent_dir_for_test/out.txt")).unwrap_err();
154 assert!(err.to_string().contains("Failed to write"));
155 }
156}