omni_dev/cli/transcript/youtube/
info.rs1use anyhow::{Context, Result};
4use clap::{Parser, ValueEnum};
5
6use crate::transcript::source::{MediaInfo, TrackKind, TranscriptSource};
7use crate::transcript::sources::youtube::Youtube;
8
9#[derive(Parser)]
11pub struct InfoCommand {
12 pub url: String,
14
15 #[arg(long, value_enum, default_value_t = InfoOutput::Table)]
17 pub output: InfoOutput,
18}
19
20#[derive(Clone, Copy, Debug, PartialEq, Eq, ValueEnum)]
22#[value(rename_all = "lowercase")]
23pub enum InfoOutput {
24 Table,
26 Json,
28}
29
30impl InfoCommand {
31 pub async fn execute(self) -> Result<()> {
33 let yt = Youtube::new()?;
34 let info = yt.info(&self.url).await?;
35 match self.output {
36 InfoOutput::Table => print_table(&info),
37 InfoOutput::Json => print_json(&info)?,
38 }
39 Ok(())
40 }
41}
42
43fn print_table(info: &MediaInfo) {
44 println!("Source: {}", info.source);
45 println!("ID: {}", info.locator_id);
46 println!("Title: {}", info.title);
47 if let Some(author) = &info.author {
48 println!("Channel: {author}");
49 }
50 if let Some(duration_ms) = info.duration_ms {
51 println!("Duration: {}", format_duration(duration_ms));
52 }
53 if info.languages.is_empty() {
54 println!("Languages: (none)");
55 } else {
56 println!("Languages:");
57 for lang in &info.languages {
58 println!(" - {} [{}] {}", lang.code, kind_str(lang.kind), lang.name);
59 }
60 }
61}
62
63fn print_json(info: &MediaInfo) -> Result<()> {
64 let json =
65 serde_json::to_string_pretty(info).context("Failed to serialize MediaInfo as JSON")?;
66 println!("{json}");
67 Ok(())
68}
69
70fn kind_str(kind: TrackKind) -> &'static str {
71 match kind {
72 TrackKind::Manual => "manual",
73 TrackKind::Auto => "auto",
74 TrackKind::Translated => "translated",
75 }
76}
77
78fn format_duration(ms: u64) -> String {
79 let total_secs = ms / 1_000;
80 let hours = total_secs / 3_600;
81 let minutes = (total_secs % 3_600) / 60;
82 let seconds = total_secs % 60;
83 if hours > 0 {
84 format!("{hours:02}:{minutes:02}:{seconds:02}")
85 } else {
86 format!("{minutes:02}:{seconds:02}")
87 }
88}
89
90#[cfg(test)]
91#[allow(clippy::unwrap_used, clippy::expect_used)]
92mod tests {
93 use super::*;
94 use crate::transcript::source::LanguageInfo;
95 use clap::{CommandFactory, FromArgMatches};
96
97 fn parse(args: &[&str]) -> InfoCommand {
98 let cmd = InfoCommand::command().no_binary_name(true);
99 let matches = cmd.try_get_matches_from(args).unwrap();
100 InfoCommand::from_arg_matches(&matches).unwrap()
101 }
102
103 #[test]
104 fn info_command_defaults() {
105 let cmd = parse(&["abc"]);
106 assert_eq!(cmd.url, "abc");
107 assert_eq!(cmd.output, InfoOutput::Table);
108 }
109
110 #[test]
111 fn info_command_json_output() {
112 let cmd = parse(&["abc", "--output", "json"]);
113 assert_eq!(cmd.output, InfoOutput::Json);
114 }
115
116 #[test]
117 fn format_duration_under_an_hour() {
118 assert_eq!(format_duration(0), "00:00");
119 assert_eq!(format_duration(59_000), "00:59");
120 assert_eq!(format_duration(212_000), "03:32");
121 }
122
123 #[test]
124 fn format_duration_over_an_hour() {
125 assert_eq!(format_duration(3_600_000), "01:00:00");
126 assert_eq!(format_duration(3_661_000), "01:01:01");
127 }
128
129 #[test]
130 fn print_table_handles_full_info() {
131 let info = MediaInfo {
132 source: "youtube".into(),
133 locator_id: "abc".into(),
134 title: "Title".into(),
135 author: Some("Channel".into()),
136 duration_ms: Some(60_000),
137 languages: vec![LanguageInfo {
138 code: "en".into(),
139 name: "English".into(),
140 kind: TrackKind::Manual,
141 }],
142 };
143 print_table(&info);
144 }
145
146 #[test]
147 fn print_table_handles_minimal_info() {
148 let info = MediaInfo {
149 source: "youtube".into(),
150 locator_id: "abc".into(),
151 title: "Title".into(),
152 author: None,
153 duration_ms: None,
154 languages: vec![],
155 };
156 print_table(&info);
157 }
158
159 #[test]
160 fn print_json_round_trips() {
161 let info = MediaInfo {
162 source: "youtube".into(),
163 locator_id: "abc".into(),
164 title: "Title".into(),
165 author: None,
166 duration_ms: None,
167 languages: vec![],
168 };
169 print_json(&info).unwrap();
170 }
171}