1use crate::{
2 app::field::{ArgsTable, Field, Text},
3 error::{
4 AmbiguousCommentChoices, AmbiguousPictureChoices, CommentNotFound, Error,
5 OutputDirCreationFailure, PictureFileWriteFailure, PictureNotFound, PictureTypeNotFound,
6 },
7 run::Run,
8 text_data::{
9 comment::Comment,
10 picture::Picture,
11 picture_type::{PictureType, PictureTypeExtra},
12 },
13 text_format::TextFormat,
14 utils::{get_image_extension, read_tag_from_path},
15};
16use clap::{Args, Subcommand};
17use id3::{Tag, TagLike};
18use mediatype::MediaType;
19use pipe_trait::Pipe;
20use std::{borrow::Cow, fs, path::PathBuf};
21
22pub type Get = Field<GetArgsTable>;
24
25impl Run for Text<GetArgsTable> {
26 fn run(self) -> Result<(), Error> {
27 macro_rules! get_text {
28 ($args:expr, $get:expr) => {{
29 let GetText {
30 format,
31 input_audio,
32 } = $args;
33 let tag = read_tag_from_path(input_audio)?;
34 let value = $get(&tag);
35 match (format, value) {
36 (Some(format), value) => println!("{}", format.serialize(&value)?),
37 (None, Some(value)) => println!("{value}"),
38 (None, None) => {}
39 }
40 Ok(())
41 }};
42 }
43
44 match self {
45 Text::Title(args) => get_text!(args, Tag::title),
46 Text::Artist(args) => get_text!(args, Tag::artist),
47 Text::Album(args) => get_text!(args, Tag::album),
48 Text::AlbumArtist(args) => get_text!(args, Tag::album_artist),
49 Text::Genre(GetGenre::Name(args)) => get_text!(args, Tag::genre_parsed),
50 Text::Genre(GetGenre::Code(args)) => get_text!(args, Tag::genre),
51 }
52 }
53}
54
55#[derive(Debug)]
57pub struct GetArgsTable;
58impl ArgsTable for GetArgsTable {
59 type Text = GetText;
60 type Genre = GetGenre;
61 type Comment = GetComment;
62 type Picture = GetPicture;
63}
64
65#[derive(Debug, Args)]
67#[clap(about = "")]
68pub struct GetText {
69 #[clap(long, value_enum)]
71 pub format: Option<TextFormat>,
72 pub input_audio: PathBuf,
74}
75
76#[derive(Debug, Subcommand)]
78pub enum GetGenre {
79 #[clap(name = "genre-name")]
80 Name(GetText),
81 #[clap(name = "genre-code")]
82 Code(GetText),
83}
84
85#[derive(Debug, Args)]
87#[clap(about = "")]
88pub struct GetComment {
89 #[clap(long)]
91 pub language: Option<String>,
92 #[clap(long)]
94 pub description: Option<String>,
95 #[clap(long, value_enum)]
97 pub format: Option<TextFormat>,
98 pub input_audio: PathBuf,
100}
101
102impl Run for GetComment {
103 fn run(self) -> Result<(), Error> {
104 let GetComment {
105 language,
106 description,
107 format,
108 input_audio,
109 } = self;
110 let tag = read_tag_from_path(input_audio)?;
111 let comments = tag
112 .comments()
113 .filter(|comment| {
114 language
115 .as_ref()
116 .map_or(true, |language| &comment.lang == language)
117 })
118 .filter(|comment| {
119 description
120 .as_ref()
121 .map_or(true, |description| &comment.description == description)
122 });
123 let output_text: Cow<str> = if let Some(format) = format {
124 let comments: Vec<_> = comments.map(Comment::from).collect();
125 format.serialize(&comments)?.pipe(Cow::Owned)
126 } else {
127 let mut iter = comments;
128 let comment = iter.next().ok_or(CommentNotFound)?;
129 if iter.next().is_some() {
130 return AmbiguousCommentChoices.pipe(Error::from).pipe(Err);
131 }
132 Cow::Borrowed(&comment.text)
133 };
134 println!("{output_text}");
135 Ok(())
136 }
137}
138
139#[derive(Debug, Args)]
141#[clap(about = "")]
142pub struct GetPicture {
143 #[clap(subcommand)]
145 pub command: GetPictureCmd,
146}
147
148impl Run for GetPicture {
149 fn run(self) -> Result<(), Error> {
150 self.command.run()
151 }
152}
153
154#[derive(Debug, Subcommand)]
156#[clap(about = "")]
157pub enum GetPictureCmd {
158 List(GetPictureList),
160 File(GetPictureFile),
162 Dir(GetPictureDir),
164}
165
166impl Run for GetPictureCmd {
167 fn run(self) -> Result<(), Error> {
168 match self {
169 GetPictureCmd::List(proc) => proc.run(),
170 GetPictureCmd::File(proc) => proc.run(),
171 GetPictureCmd::Dir(proc) => proc.run(),
172 }
173 }
174}
175
176#[derive(Debug, Args)]
178pub struct GetPictureList {
179 #[clap(long, value_enum)]
181 pub format: TextFormat,
182 pub input_audio: PathBuf,
184}
185
186impl Run for GetPictureList {
187 fn run(self) -> Result<(), Error> {
188 let GetPictureList {
189 format,
190 input_audio,
191 } = self;
192 let tag = read_tag_from_path(input_audio)?;
193 let pictures: Vec<_> = tag.pictures().map(Picture::from_id3_ref).collect();
194 let serialized = format.serialize(&pictures)?;
195 println!("{serialized}");
196 Ok(())
197 }
198}
199
200#[derive(Debug, Args)]
202pub struct GetPictureFile {
203 pub input_audio: PathBuf,
205 pub output_picture: PathBuf,
207 #[clap(value_enum)]
209 pub picture_type: Option<PictureType>,
210}
211
212impl Run for GetPictureFile {
213 fn run(self) -> Result<(), Error> {
214 let GetPictureFile {
215 input_audio,
216 output_picture,
217 picture_type,
218 } = self;
219 let tag = read_tag_from_path(input_audio)?;
220 let data = if let Some(target_picture_type) = picture_type {
221 &tag.pictures()
222 .find(|picture| picture.picture_type.try_into() == Ok(target_picture_type))
223 .ok_or(PictureTypeNotFound)?
224 .data
225 } else {
226 let mut iter = tag.pictures().map(|picture| &picture.data);
227 let data = iter.next().ok_or(PictureNotFound)?;
228 if iter.next().is_some() {
229 return AmbiguousPictureChoices.pipe(Error::from).pipe(Err);
230 }
231 data
232 };
233 fs::write(output_picture, data)
234 .map_err(PictureFileWriteFailure::from)
235 .map_err(Error::from)
236 }
237}
238
239#[derive(Debug, Args)]
241pub struct GetPictureDir {
242 pub input_audio: PathBuf,
244 pub output_directory: PathBuf,
246}
247
248impl Run for GetPictureDir {
249 fn run(self) -> Result<(), Error> {
250 let GetPictureDir {
251 input_audio,
252 output_directory,
253 } = self;
254
255 let tag = read_tag_from_path(input_audio)?;
256 let pictures = tag.pictures().zip(0..);
257 for (picture, index) in pictures {
258 let id3::frame::Picture {
259 picture_type,
260 mime_type,
261 description,
262 data,
263 } = picture;
264 let picture_type: PictureTypeExtra = (*picture_type).into();
265 fs::create_dir_all(&output_directory).map_err(OutputDirCreationFailure::from)?;
266 eprintln!("{index}: {picture_type} {mime_type} {description}");
267 let ext = MediaType::parse(mime_type)
268 .ok()
269 .and_then(get_image_extension);
270 let output_file_name = match ext {
271 Some(ext) => format!("{index}-{picture_type}.{ext}"),
272 None => format!("{index}-{picture_type}"),
273 };
274 let output_file_path = output_directory.join(output_file_name);
275 fs::write(output_file_path, data).map_err(PictureFileWriteFailure::from)?;
276 }
277
278 Ok(())
279 }
280}