id3_cli/app/
get.rs

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
22/// Subcommand of the `get` subcommand.
23pub 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/// Table of [`Args`] types for [`Get`].
56#[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/// CLI arguments of `get <text-field>`.
66#[derive(Debug, Args)]
67#[clap(about = "")]
68pub struct GetText {
69    /// Format the output text into JSON or YAML.
70    #[clap(long, value_enum)]
71    pub format: Option<TextFormat>,
72    /// Path to the input audio file.
73    pub input_audio: PathBuf,
74}
75
76/// Genre fields.
77#[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/// CLI arguments of `get comment`.
86#[derive(Debug, Args)]
87#[clap(about = "")]
88pub struct GetComment {
89    /// Filter language.
90    #[clap(long)]
91    pub language: Option<String>,
92    /// Filter description.
93    #[clap(long)]
94    pub description: Option<String>,
95    /// Format of the output text. Required if there are multiple comments.
96    #[clap(long, value_enum)]
97    pub format: Option<TextFormat>,
98    /// Path to the input audio file.
99    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/// CLI arguments of `get picture`.
140#[derive(Debug, Args)]
141#[clap(about = "")]
142pub struct GetPicture {
143    /// Subcommand to execute.
144    #[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/// Subcommand of `get picture`.
155#[derive(Debug, Subcommand)]
156#[clap(about = "")]
157pub enum GetPictureCmd {
158    /// List descriptions, mime types, picture types, and sizes of all pictures.
159    List(GetPictureList),
160    /// Export a single picture to a file.
161    File(GetPictureFile),
162    /// Export all single pictures to a directory.
163    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/// CLI arguments of `get picture file`.
177#[derive(Debug, Args)]
178pub struct GetPictureList {
179    /// Format of the output text.
180    #[clap(long, value_enum)]
181    pub format: TextFormat,
182    /// Path to the input audio file.
183    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/// CLI arguments of `get picture file`.
201#[derive(Debug, Args)]
202pub struct GetPictureFile {
203    /// Path to the input audio file.
204    pub input_audio: PathBuf,
205    /// Path to the output picture file.
206    pub output_picture: PathBuf,
207    /// Type of picture to export. Required if there are multiple pictures.
208    #[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/// CLI arguments of `get picture dir`.
240#[derive(Debug, Args)]
241pub struct GetPictureDir {
242    /// Path to the input audio file.
243    pub input_audio: PathBuf,
244    /// Path to the directory to contain the output pictures.
245    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}