omni_dev/cli/transcript/youtube/
list_langs.rs1use anyhow::{Context, Result};
5use clap::{Parser, ValueEnum};
6
7use crate::transcript::source::{LanguageInfo, TrackKind, TranscriptSource};
8use crate::transcript::sources::youtube::Youtube;
9
10#[derive(Parser)]
12pub struct ListLangsCommand {
13 pub url: String,
15
16 #[arg(long, value_enum, default_value_t = ListLangsOutput::Table)]
18 pub output: ListLangsOutput,
19}
20
21#[derive(Clone, Copy, Debug, PartialEq, Eq, ValueEnum)]
23#[value(rename_all = "lowercase")]
24pub enum ListLangsOutput {
25 Table,
27 Json,
29}
30
31impl ListLangsCommand {
32 pub async fn execute(self) -> Result<()> {
34 let yt = Youtube::new()?;
35 let langs = yt.list_languages(&self.url).await?;
36 match self.output {
37 ListLangsOutput::Table => print_table(&langs),
38 ListLangsOutput::Json => print_json(&langs)?,
39 }
40 Ok(())
41 }
42}
43
44fn print_table(langs: &[LanguageInfo]) {
45 if langs.is_empty() {
46 println!("(no caption tracks available)");
47 return;
48 }
49
50 let code_width = "code"
51 .len()
52 .max(langs.iter().map(|l| l.code.len()).max().unwrap_or(0));
53 let kind_width = "kind".len().max(
54 langs
55 .iter()
56 .map(|l| kind_str(l.kind).len())
57 .max()
58 .unwrap_or(0),
59 );
60
61 println!(
62 "{:<code_w$} {:<kind_w$} name",
63 "code",
64 "kind",
65 code_w = code_width,
66 kind_w = kind_width,
67 );
68 println!(
69 "{:-<code_w$} {:-<kind_w$} {:-<name_w$}",
70 "",
71 "",
72 "",
73 code_w = code_width,
74 kind_w = kind_width,
75 name_w = "name".len(),
76 );
77 for lang in langs {
78 println!(
79 "{:<code_w$} {:<kind_w$} {}",
80 lang.code,
81 kind_str(lang.kind),
82 lang.name,
83 code_w = code_width,
84 kind_w = kind_width,
85 );
86 }
87}
88
89fn print_json(langs: &[LanguageInfo]) -> Result<()> {
90 let json =
91 serde_json::to_string_pretty(langs).context("Failed to serialize languages as JSON")?;
92 println!("{json}");
93 Ok(())
94}
95
96fn kind_str(kind: TrackKind) -> &'static str {
97 match kind {
98 TrackKind::Manual => "manual",
99 TrackKind::Auto => "auto",
100 TrackKind::Translated => "translated",
101 }
102}
103
104#[cfg(test)]
105#[allow(clippy::unwrap_used, clippy::expect_used)]
106mod tests {
107 use super::*;
108 use clap::{CommandFactory, FromArgMatches};
109
110 fn parse(args: &[&str]) -> ListLangsCommand {
111 let cmd = ListLangsCommand::command().no_binary_name(true);
112 let matches = cmd.try_get_matches_from(args).unwrap();
113 ListLangsCommand::from_arg_matches(&matches).unwrap()
114 }
115
116 #[test]
117 fn list_langs_command_defaults() {
118 let cmd = parse(&["abc"]);
119 assert_eq!(cmd.url, "abc");
120 assert_eq!(cmd.output, ListLangsOutput::Table);
121 }
122
123 #[test]
124 fn list_langs_command_json_output() {
125 let cmd = parse(&["abc", "--output", "json"]);
126 assert_eq!(cmd.output, ListLangsOutput::Json);
127 }
128
129 #[test]
130 fn kind_str_lowercase() {
131 assert_eq!(kind_str(TrackKind::Manual), "manual");
132 assert_eq!(kind_str(TrackKind::Auto), "auto");
133 assert_eq!(kind_str(TrackKind::Translated), "translated");
134 }
135
136 #[test]
137 fn print_json_round_trips() {
138 let langs = vec![LanguageInfo {
140 code: "en".into(),
141 name: "English".into(),
142 kind: TrackKind::Manual,
143 }];
144 print_json(&langs).unwrap();
145 }
146
147 #[test]
148 fn print_table_handles_empty_input() {
149 print_table(&[]);
151 }
152
153 #[test]
154 fn print_table_handles_populated_input() {
155 let langs = vec![
156 LanguageInfo {
157 code: "en".into(),
158 name: "English".into(),
159 kind: TrackKind::Manual,
160 },
161 LanguageInfo {
162 code: "es-419".into(),
163 name: "Spanish (Latin America)".into(),
164 kind: TrackKind::Auto,
165 },
166 ];
167 print_table(&langs);
168 }
169}