1use std::fmt;
11use std::str::FromStr;
12
13use serde::{Deserialize, Serialize};
14
15use crate::error::{Error, Result};
16
17#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
19#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
20#[serde(rename_all = "lowercase")]
21pub enum SourceMode {
22 Mirror,
24 Copy,
26}
27
28#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
40pub enum ArtifactKind {
41 CoverJpg,
43 CoverWebp,
49 DetailsTxt,
51 LyricsTxt,
53 Lrc,
55 VideoMp4,
58 FolderJpg,
60 FolderWebp,
62 FolderMp4,
66 Playlist,
68}
69
70#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
72#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
73#[serde(rename_all = "lowercase")]
74pub enum AudioFormat {
75 Mp3,
76 #[default]
77 Flac,
78 Wav,
79 Alac,
80}
81
82impl AudioFormat {
83 pub fn ext(self) -> &'static str {
87 match self {
88 Self::Mp3 => "mp3",
89 Self::Flac => "flac",
90 Self::Wav => "wav",
91 Self::Alac => "m4a",
92 }
93 }
94
95 pub fn embeds_animated_cover(self) -> bool {
100 !matches!(self, Self::Alac)
101 }
102}
103
104impl FromStr for AudioFormat {
105 type Err = Error;
106
107 fn from_str(s: &str) -> Result<Self> {
111 match s {
112 "mp3" => Ok(Self::Mp3),
113 "flac" => Ok(Self::Flac),
114 "wav" => Ok(Self::Wav),
115 "alac" => Ok(Self::Alac),
116 other => Err(Error::Config(format!("unknown format '{other}'"))),
117 }
118 }
119}
120
121impl fmt::Display for AudioFormat {
122 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
123 match self {
124 Self::Mp3 => f.write_str("mp3"),
125 Self::Flac => f.write_str("flac"),
126 Self::Wav => f.write_str("wav"),
127 Self::Alac => f.write_str("alac"),
128 }
129 }
130}
131
132#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
140#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
141#[serde(rename_all = "lowercase")]
142pub enum StemFormat {
143 #[default]
145 Wav,
146 Mp3,
148}
149
150impl StemFormat {
151 pub fn ext(self) -> &'static str {
153 match self {
154 Self::Wav => "wav",
155 Self::Mp3 => "mp3",
156 }
157 }
158}
159
160impl FromStr for StemFormat {
161 type Err = Error;
162
163 fn from_str(s: &str) -> Result<Self> {
166 match s {
167 "wav" => Ok(Self::Wav),
168 "mp3" => Ok(Self::Mp3),
169 "flac" => Err(Error::Config(
170 "stems cannot be stored as FLAC; use 'wav' or 'mp3'".to_string(),
171 )),
172 other => Err(Error::Config(format!("unknown stem format '{other}'"))),
173 }
174 }
175}
176
177impl fmt::Display for StemFormat {
178 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
179 f.write_str(self.ext())
180 }
181}
182
183#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
185#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
186#[serde(rename_all = "lowercase")]
187pub enum VideoCoverRetention {
188 #[default]
189 Neither,
190 Webp,
191 Mp4,
192 Both,
193}
194
195impl VideoCoverRetention {
196 pub fn keeps_webp(self) -> bool {
197 matches!(self, Self::Webp | Self::Both)
198 }
199
200 pub fn keeps_mp4(self) -> bool {
201 matches!(self, Self::Mp4 | Self::Both)
202 }
203}
204
205impl FromStr for VideoCoverRetention {
206 type Err = Error;
207
208 fn from_str(s: &str) -> Result<Self> {
211 match s {
212 "neither" => Ok(Self::Neither),
213 "webp" => Ok(Self::Webp),
214 "mp4" => Ok(Self::Mp4),
215 "both" => Ok(Self::Both),
216 other => Err(Error::Config(format!(
217 "unknown video_cover_retention '{other}'"
218 ))),
219 }
220 }
221}
222
223impl fmt::Display for VideoCoverRetention {
224 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
225 match self {
226 Self::Neither => f.write_str("neither"),
227 Self::Webp => f.write_str("webp"),
228 Self::Mp4 => f.write_str("mp4"),
229 Self::Both => f.write_str("both"),
230 }
231 }
232}
233
234#[derive(Debug, Clone, Copy, PartialEq, Eq)]
245pub struct WebpEncodeSettings {
246 pub quality: u8,
249 pub max_fps: u32,
251 pub max_width: Option<u32>,
255 pub lossless: bool,
259 pub compression_level: u8,
262}
263
264impl Default for WebpEncodeSettings {
265 fn default() -> Self {
266 Self {
267 quality: 90,
268 max_fps: 24,
269 max_width: Some(640),
270 lossless: false,
271 compression_level: 4,
272 }
273 }
274}
275
276#[cfg(test)]
277mod tests {
278 use super::*;
279
280 #[test]
281 fn audio_format_parses_lowercase_only() {
282 assert_eq!("flac".parse::<AudioFormat>().unwrap(), AudioFormat::Flac);
283 assert_eq!("mp3".parse::<AudioFormat>().unwrap(), AudioFormat::Mp3);
284 assert_eq!("wav".parse::<AudioFormat>().unwrap(), AudioFormat::Wav);
285 assert_eq!("alac".parse::<AudioFormat>().unwrap(), AudioFormat::Alac);
286 assert!("FLAC".parse::<AudioFormat>().is_err());
288 assert!("Mp3".parse::<AudioFormat>().is_err());
289 }
290
291 #[test]
292 fn audio_format_rejects_unknown_without_panicking() {
293 assert!(matches!(
294 "ogg".parse::<AudioFormat>().unwrap_err(),
295 Error::Config(_)
296 ));
297 }
298
299 #[test]
300 fn audio_format_default_is_flac() {
301 assert_eq!(AudioFormat::default(), AudioFormat::Flac);
302 }
303
304 #[test]
305 fn audio_format_ext_differs_from_display_for_alac() {
306 assert_eq!(AudioFormat::Alac.ext(), "m4a");
307 assert_eq!(AudioFormat::Alac.to_string(), "alac");
308 }
309
310 #[test]
311 fn audio_format_display_round_trips_through_from_str() {
312 for f in [
313 AudioFormat::Mp3,
314 AudioFormat::Flac,
315 AudioFormat::Wav,
316 AudioFormat::Alac,
317 ] {
318 assert_eq!(f.to_string().parse::<AudioFormat>().unwrap(), f);
319 }
320 }
321
322 #[test]
323 fn audio_format_embeds_animated_cover_except_alac() {
324 assert!(AudioFormat::Flac.embeds_animated_cover());
325 assert!(AudioFormat::Mp3.embeds_animated_cover());
326 assert!(AudioFormat::Wav.embeds_animated_cover());
327 assert!(!AudioFormat::Alac.embeds_animated_cover());
328 }
329
330 #[test]
331 fn stem_format_parses_wav_and_mp3() {
332 assert_eq!("wav".parse::<StemFormat>().unwrap(), StemFormat::Wav);
333 assert_eq!("mp3".parse::<StemFormat>().unwrap(), StemFormat::Mp3);
334 assert!("WAV".parse::<StemFormat>().is_err());
336 }
337
338 #[test]
339 fn stem_format_rejects_flac_with_guidance() {
340 match "flac".parse::<StemFormat>().unwrap_err() {
341 Error::Config(msg) => assert!(msg.contains("FLAC")),
342 other => panic!("expected Config error, got {other:?}"),
343 }
344 }
345
346 #[test]
347 fn stem_format_rejects_unknown_without_panicking() {
348 assert!(matches!(
349 "ogg".parse::<StemFormat>().unwrap_err(),
350 Error::Config(_)
351 ));
352 }
353
354 #[test]
355 fn stem_format_default_is_wav_and_display_matches_ext() {
356 assert_eq!(StemFormat::default(), StemFormat::Wav);
357 assert_eq!(StemFormat::Mp3.to_string(), StemFormat::Mp3.ext());
358 }
359
360 #[test]
361 fn video_cover_retention_parses_all_variants() {
362 assert_eq!(
363 "neither".parse::<VideoCoverRetention>().unwrap(),
364 VideoCoverRetention::Neither
365 );
366 assert_eq!(
367 "webp".parse::<VideoCoverRetention>().unwrap(),
368 VideoCoverRetention::Webp
369 );
370 assert_eq!(
371 "mp4".parse::<VideoCoverRetention>().unwrap(),
372 VideoCoverRetention::Mp4
373 );
374 assert_eq!(
375 "both".parse::<VideoCoverRetention>().unwrap(),
376 VideoCoverRetention::Both
377 );
378 assert!("WEBP".parse::<VideoCoverRetention>().is_err());
380 }
381
382 #[test]
383 fn video_cover_retention_rejects_unknown_without_panicking() {
384 assert!(matches!(
385 "all".parse::<VideoCoverRetention>().unwrap_err(),
386 Error::Config(_)
387 ));
388 }
389
390 #[test]
391 fn video_cover_retention_keeps_matrix() {
392 assert!(!VideoCoverRetention::Neither.keeps_webp());
393 assert!(!VideoCoverRetention::Neither.keeps_mp4());
394 assert!(VideoCoverRetention::Webp.keeps_webp());
395 assert!(!VideoCoverRetention::Webp.keeps_mp4());
396 assert!(!VideoCoverRetention::Mp4.keeps_webp());
397 assert!(VideoCoverRetention::Mp4.keeps_mp4());
398 assert!(VideoCoverRetention::Both.keeps_webp());
399 assert!(VideoCoverRetention::Both.keeps_mp4());
400 }
401
402 #[test]
403 fn video_cover_retention_default_is_neither() {
404 assert_eq!(VideoCoverRetention::default(), VideoCoverRetention::Neither);
405 }
406
407 #[test]
408 fn webp_defaults_fit_the_flac_picture_ceiling() {
409 let d = WebpEncodeSettings::default();
410 assert_eq!(d.quality, 90);
411 assert_eq!(d.max_fps, 24);
412 assert_eq!(d.max_width, Some(640));
413 assert!(!d.lossless);
414 assert_eq!(d.compression_level, 4);
415 }
416}