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
16pub 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#[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#[derive(Debug, Args)]
56#[clap(about = "")]
57pub struct SetText {
58 #[clap(long)]
60 pub no_backup: bool,
61 pub target_audio: PathBuf,
63 pub value: String,
65}
66
67#[derive(Debug, Subcommand)]
69pub enum SetGenre {
70 #[clap(name = "genre-code")]
71 Code(SetText),
72}
73
74#[derive(Debug, Args)]
76#[clap(about = "")]
77pub struct SetComment {
78 #[clap(long)]
80 pub no_backup: bool,
81 #[clap(long)]
83 pub language: Option<String>,
84 #[clap(long)]
86 pub description: Option<String>,
87 #[clap(long, value_enum)]
89 pub format: Option<TextFormat>,
90 pub target_audio: PathBuf,
92 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#[derive(Debug, Args)]
141#[clap(about = "")]
142pub struct SetPicture {
143 #[clap(long)]
145 pub no_backup: bool,
146 #[clap(long)]
148 pub mime_type: Option<String>,
149 #[clap(long)]
151 pub description: Option<String>,
152 pub target_audio: PathBuf,
154 pub target_picture: PathBuf,
156 #[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}