id3_cli/app/
set.rs

1use crate::{
2    app::{
3        field::{ArgsTable, Field, Text},
4        Run,
5    },
6    error::{Error, FileReadFailure},
7    text_data::{comment::Comment, picture_type::PictureType},
8    text_format::TextFormat,
9    utils::ModifyTags,
10};
11use clap::{Args, Subcommand};
12use id3::{frame, Content, Tag, TagLike};
13use pipe_trait::Pipe;
14use std::{borrow::Cow, fs::read as read_file, path::PathBuf};
15
16/// Subcommand of the `set` subcommand.
17pub type Set = Field<SetArgsTable>;
18
19impl Run for Text<SetArgsTable> {
20    fn run(self) -> Result<(), Error> {
21        fn set_text(args: SetText, set: impl FnOnce(&mut Tag, String)) -> Result<(), Error> {
22            let SetText {
23                no_backup,
24                target_audio,
25                value,
26            } = args;
27            ModifyTags::builder()
28                .no_backup(no_backup)
29                .target_audio(&target_audio)
30                .build()
31                .run(move |tag| set(tag, value))
32        }
33
34        match self {
35            Text::Title(args) => set_text(args, Tag::set_title),
36            Text::Artist(args) => set_text(args, Tag::set_artist),
37            Text::Album(args) => set_text(args, Tag::set_album),
38            Text::AlbumArtist(args) => set_text(args, Tag::set_album_artist),
39            Text::Genre(SetGenre::Code(args)) => set_text(args, Tag::set_genre),
40        }
41    }
42}
43
44/// Table of [`Args`] types for [`Set`].
45#[derive(Debug)]
46pub struct SetArgsTable;
47impl ArgsTable for SetArgsTable {
48    type Text = SetText;
49    type Genre = SetGenre;
50    type Comment = SetComment;
51    type Picture = SetPicture;
52}
53
54/// CLI arguments of `set <text-field>`.
55#[derive(Debug, Args)]
56#[clap(about = "")]
57pub struct SetText {
58    /// Don't create backup for the target audio file.
59    #[clap(long)]
60    pub no_backup: bool,
61    /// Path to the target audio file.
62    pub target_audio: PathBuf,
63    /// New value to set.
64    pub value: String,
65}
66
67/// Genre fields.
68#[derive(Debug, Subcommand)]
69pub enum SetGenre {
70    #[clap(name = "genre-code")]
71    Code(SetText),
72}
73
74/// CLI arguments of `set comment`.
75#[derive(Debug, Args)]
76#[clap(about = "")]
77pub struct SetComment {
78    /// Don't create backup for the target audio file.
79    #[clap(long)]
80    pub no_backup: bool,
81    /// Comment language (ISO 639-2).
82    #[clap(long)]
83    pub language: Option<String>,
84    /// Comment description.
85    #[clap(long)]
86    pub description: Option<String>,
87    /// Format of the ejected comment (if any).
88    #[clap(long, value_enum)]
89    pub format: Option<TextFormat>,
90    /// Path to the target audio file.
91    pub target_audio: PathBuf,
92    /// Content of the comment.
93    pub content: String,
94}
95
96impl Run for SetComment {
97    fn run(self) -> Result<(), Error> {
98        let SetComment {
99            no_backup,
100            language,
101            description,
102            format,
103            ref target_audio,
104            content,
105        } = self;
106        let language = language.unwrap_or_else(|| "\0\0\0".to_string());
107        let description = description.unwrap_or_default();
108
109        let ejected_frame = ModifyTags {
110            no_backup,
111            target_audio,
112        }
113        .run(|tag| {
114            tag.add_frame(Comment {
115                language,
116                description,
117                content,
118            })
119        })?;
120
121        let ejected_comment = match ejected_frame.as_ref().map(id3::Frame::content) {
122            None => return Ok(()),
123            Some(Content::Comment(comment)) => comment,
124            Some(content) => panic!("Impossible! The ejected frame wasn't a comment: {content:?}"),
125        };
126
127        let output_text: Cow<str> = if let Some(format) = format {
128            let output_object = Comment::from(ejected_comment);
129            format.serialize(&output_object)?.pipe(Cow::Owned)
130        } else {
131            Cow::Borrowed(&ejected_comment.text)
132        };
133
134        println!("{output_text}");
135        Ok(())
136    }
137}
138
139/// CLI arguments of `set comment`.
140#[derive(Debug, Args)]
141#[clap(about = "")]
142pub struct SetPicture {
143    /// Don't create backup for the target audio file.
144    #[clap(long)]
145    pub no_backup: bool,
146    /// Mime type of the picture.
147    #[clap(long)]
148    pub mime_type: Option<String>,
149    /// Description of the picture.
150    #[clap(long)]
151    pub description: Option<String>,
152    /// Path to the target audio file.
153    pub target_audio: PathBuf,
154    /// Path to the input picture file.
155    pub target_picture: PathBuf,
156    /// Type of picture.
157    #[clap(value_enum)]
158    pub picture_type: PictureType,
159}
160
161impl Run for SetPicture {
162    fn run(self) -> Result<(), Error> {
163        let SetPicture {
164            no_backup,
165            mime_type,
166            description,
167            ref target_audio,
168            ref target_picture,
169            picture_type,
170        } = self;
171
172        let data = read_file(target_picture).map_err(|error| FileReadFailure {
173            file: target_picture.to_path_buf(),
174            error,
175        })?;
176
177        let mime_type = mime_type
178            .or_else(|| infer::get(&data)?.mime_type().to_string().pipe(Some))
179            .unwrap_or_default();
180
181        let description = description.unwrap_or_else(|| {
182            target_picture
183                .file_name()
184                .map(|file_name| file_name.to_string_lossy())
185                .map(|file_name| file_name.to_string())
186                .unwrap_or_default()
187        });
188
189        let picture_type: frame::PictureType = picture_type.into();
190
191        let frame = frame::Picture {
192            data,
193            description,
194            mime_type,
195            picture_type,
196        };
197
198        let ejected_picture = ModifyTags::builder()
199            .target_audio(target_audio)
200            .no_backup(no_backup)
201            .build()
202            .run(|tag| tag.add_frame(frame))?;
203
204        if ejected_picture.is_some() {
205            eprintln!("A picture had been replaced");
206        } else {
207            eprintln!("A picture had been added");
208        }
209
210        Ok(())
211    }
212}