aede_core/artwork.rs
1//! The picture inside an audio file, pulled out of it.
2//!
3//! # Why this goes through `lofty` for every format
4//!
5//! This crate parses FLAC, MP3, MP4 and Ogg itself, and reads the rest through
6//! `lofty`. Those parsers exist because tag reading is the hot path — a scan
7//! opens every file in the library — and because the fields they take are few
8//! and stable.
9//!
10//! Extraction is neither of those things. It runs once per folder, on demand,
11//! and what it produces is written into somebody's music library, where being
12//! wrong is expensive and being slow is not. Four more hand-written extractors
13//! — a FLAC `PICTURE` block, an ID3 `APIC` frame, an MP4 `covr` atom, a
14//! base64-wrapped picture block inside an Ogg comment — would be four more
15//! chances to write a corrupt file, to buy speed nobody is waiting for.
16//!
17//! So one path, for every format, through a dependency this crate already
18//! carries. What it hands back is still decoded and validated before anything
19//! is written: see [`crate::coverart::write_image`].
20
21use std::path::Path;
22
23use crate::coverart::Kind;
24use crate::tags::TagError;
25
26/// Every picture a file carries, each with what it is of.
27///
28/// In the order the file holds them, which for a booklet is the order somebody
29/// scanned its pages in, and worth keeping for that reason alone.
30///
31/// The kinds are the tag formats' own: ID3, Vorbis and MP4 all classify a
32/// picture, out of a list far longer than this program has any use for. It is
33/// narrowed to [`Kind`] here — a conductor, a lyricist and a brightly coloured
34/// fish all being, for the purpose of putting a file in a music folder, simply
35/// another image.
36pub fn pictures(path: &Path) -> Result<Vec<(Kind, Vec<u8>)>, TagError> {
37 use lofty::file::TaggedFileExt;
38
39 let file = crate::tags::bytes::open_regular(path)?;
40 let mut probe = lofty::probe::Probe::new(std::io::BufReader::new(file));
41 // Keep read_from_path's extension-based dispatch on the checked descriptor.
42 if let Some(file_type) = lofty::file::FileType::from_path(path) {
43 probe = probe.set_file_type(file_type);
44 }
45 let file = probe.read().map_err(|_| TagError::UnrecognizedFormat)?;
46 Ok(file
47 .tags()
48 .iter()
49 .flat_map(|tag| tag.pictures())
50 .map(|picture| (kind_of(picture.pic_type()), picture.data().to_vec()))
51 .collect())
52}
53
54/// What a tag's own classification of a picture amounts to here.
55fn kind_of(what: lofty::picture::PictureType) -> Kind {
56 use lofty::picture::PictureType as P;
57 match what {
58 P::CoverFront => Kind::Front,
59 P::CoverBack => Kind::Back,
60 P::Leaflet => Kind::Booklet,
61 P::Media => Kind::Media,
62 // A photograph of whoever made the record, under whichever of the
63 // three headings the tagger happened to choose.
64 P::LeadArtist | P::Artist | P::Band => Kind::Artist,
65 _ => Kind::Other,
66 }
67}
68
69/// The front cover inside a file, if it has one.
70///
71/// **The front, not the first.** A file may carry several pictures — front,
72/// back, the artist, a scan of the booklet — and a folder image that turned out
73/// to be the back of the sleeve would be worse than none, because nothing later
74/// would look for it again.
75///
76/// Falls back to the first picture only when nothing is typed as a front cover:
77/// a great many files carry exactly one image and type it as nothing at all.
78pub fn embedded(path: &Path) -> Result<Option<Vec<u8>>, TagError> {
79 let pictures = pictures(path)?;
80 let chosen = pictures
81 .iter()
82 .find(|(kind, _)| *kind == Kind::Front)
83 .or_else(|| pictures.first());
84 Ok(chosen.map(|(_, bytes)| bytes.clone()))
85}
86
87/// Takes the picture out of one file and writes it into a folder.
88///
89/// The whole of what `aede artwork` does to one album, in one place, so that
90/// the end-to-end proof of it can live beside the extraction it depends on —
91/// with real containers rather than a fixture agreeing with itself.
92pub fn extract_into(source: &Path, folder: &Path) -> Result<std::path::PathBuf, String> {
93 let bytes = embedded(source)
94 .map_err(|why| why.to_string())?
95 .ok_or_else(|| "the file carries no picture".to_string())?;
96 crate::coverart::write_beside(folder, &bytes)
97}
98
99/// Writes out every picture a file carries that is **not** the front cover.
100///
101/// Into `folder/artwork/`, never beside the music: the reason is written on
102/// [`crate::coverart::EXTRAS`], and it is not a matter of tidiness — an image
103/// next to the tracks becomes the album's cover as far as the scanner is
104/// concerned, and a back sleeve promoted to cover is worse than no cover.
105///
106/// Answers one line per picture, said or refused, because a run over a library
107/// wants a count of both and the caller cannot tell them apart afterwards. A
108/// picture already written is a refusal like any other: nothing overwrites.
109pub fn extract_extras_into(source: &Path, folder: &Path) -> Result<Vec<Extracted>, TagError> {
110 let all = pictures(source)?;
111 let extras: Vec<(Kind, Vec<u8>)> = all
112 .into_iter()
113 .filter(|(kind, _)| *kind != Kind::Front)
114 .collect();
115 let kinds: Vec<Kind> = extras.iter().map(|(kind, _)| *kind).collect();
116 let places = crate::coverart::positions(&kinds);
117 let into = crate::coverart::extras_in(folder);
118 Ok(extras
119 .iter()
120 .zip(places)
121 .map(|((kind, bytes), where_)| Extracted {
122 kind: *kind,
123 wrote: crate::coverart::write_image(&into, *kind, where_, bytes),
124 })
125 .collect())
126}
127
128/// One picture out of a file, and where it went — or why it did not.
129#[derive(Debug)]
130pub struct Extracted {
131 /// What the picture is of.
132 pub kind: Kind,
133 /// The file written, one already there, or the reason for neither.
134 pub wrote: Result<crate::coverart::Written, String>,
135}
136
137#[cfg(test)]
138#[path = "artwork_tests.rs"]
139mod tests;