1use crate::{
2 backup::Backup,
3 error::{Error, FileReadFailure, TagReadFailure, TagWriteFailure},
4};
5use id3::{self, Tag};
6use mediatype::{
7 names::{BMP, GIF, IMAGE, JPEG, PNG, SVG, WEBP},
8 MediaType,
9};
10use pipe_trait::Pipe;
11use sha2::{Digest, Sha256};
12use std::{fmt::Debug, fs::read as read_file, path::Path};
13use typed_builder::TypedBuilder;
14
15pub(crate) fn no_tag_to_empty_tag(error: id3::Error) -> id3::Result<Tag> {
18 if matches!(error.kind, id3::ErrorKind::NoTag) {
19 Ok(Tag::new())
20 } else {
21 Err(error)
22 }
23}
24
25pub fn read_tag_from_path(path: impl AsRef<Path>) -> Result<Tag, Error> {
28 path.pipe_as_ref(read_file)
29 .map_err(|error| FileReadFailure {
30 file: path.as_ref().to_path_buf(),
31 error,
32 })?
33 .pipe(read_tag_from_data)
34 .map_err(TagReadFailure::from)
35 .map_err(Error::from)
36}
37
38pub(crate) fn read_tag_from_data(data: impl AsRef<[u8]>) -> id3::Result<Tag> {
41 data.pipe_as_ref(Tag::read_from)
42 .or_else(no_tag_to_empty_tag)
43}
44
45pub(crate) fn get_image_extension(mime: MediaType) -> Option<&str> {
47 if mime.ty != IMAGE {
48 return None;
49 }
50
51 macro_rules! map_subty_ext {
52 ($subty:ident -> $ext:literal) => {
53 if mime.subty.as_str() == $subty.as_str() {
54 return Some($ext);
55 }
56 };
57 }
58
59 map_subty_ext!(BMP -> "bmp");
60 map_subty_ext!(GIF -> "gif");
61 map_subty_ext!(JPEG -> "jpg");
62 map_subty_ext!(PNG -> "png");
63 map_subty_ext!(SVG -> "svg");
64 map_subty_ext!(WEBP -> "webp");
65
66 None
67}
68
69pub fn sha256_data(data: impl AsRef<[u8]>) -> String {
71 let mut hasher = Sha256::new();
72 hasher.update(data);
73 format!("{:x}", hasher.finalize())
74}
75
76#[derive(Debug, Clone, Copy, TypedBuilder)]
78pub(crate) struct ModifyTags<'a> {
79 pub no_backup: bool,
81 pub target_audio: &'a Path,
83}
84
85impl<'a> ModifyTags<'a> {
86 pub fn run<Callback, Value>(self, callback: Callback) -> Result<Value, Error>
88 where
89 Callback: FnOnce(&mut Tag) -> Value,
90 {
91 let ModifyTags {
92 no_backup,
93 target_audio,
94 } = self;
95 let audio_content = read_file(&target_audio).map_err(|error| FileReadFailure {
96 file: target_audio.to_path_buf(),
97 error,
98 })?;
99 if !no_backup {
100 Backup::builder()
101 .source_file_path(target_audio)
102 .source_file_hash(&sha256_data(&audio_content))
103 .build()
104 .backup()?;
105 }
106 let mut tag = read_tag_from_data(&audio_content).map_err(TagReadFailure::from)?;
107 let version = tag.version();
108
109 let value = callback(&mut tag);
110
111 tag.write_to_path(target_audio, version)
112 .map_err(TagWriteFailure::from)?;
113
114 Ok(value)
115 }
116}