id3_cli/
utils.rs

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
15/// Return an empty tag if the reason for the error was "no tag".
16/// Otherwise, return the error.
17pub(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
25/// Read tag from a path.
26/// Return an empty tag if tag does not exist.
27pub 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
38/// Read tag from a binary blob.
39/// Return an empty tag if tag does not exist.
40pub(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
45/// Get extension from some well known image media types.
46pub(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
69/// Create sha256 hash of a binary blob.
70pub 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/// Modify id3 tags of an audio file.
77#[derive(Debug, Clone, Copy, TypedBuilder)]
78pub(crate) struct ModifyTags<'a> {
79    /// Whether `--no-backup` was specified.
80    pub no_backup: bool,
81    /// Provided `<TARGET_AUDIO>` argument.
82    pub target_audio: &'a Path,
83}
84
85impl<'a> ModifyTags<'a> {
86    /// Run a callback that modify the tags of an audio file.
87    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}