Skip to main content

oggmeta/
lib.rs

1//! `oggmeta` is a crate for reading and writing audio metadata for ogg vorbis files
2
3use base64::Engine;
4use image::{DynamicImage, GenericImageView, RgbImage};
5use std::collections::HashMap;
6use std::convert::AsRef;
7use std::fs::File;
8use std::io::{Cursor, Read, Seek, Write};
9use std::path::Path;
10use thiserror::Error;
11
12use crate::utils::read_picture_block;
13
14mod reading;
15mod utils;
16mod writing;
17
18const VORBIS_HEADER: [u8; 7] = [3, 118, 111, 114, 98, 105, 115];
19const THEORA_HEADER: [u8; 7] = [0x81, 0x74, 0x68, 0x65, 0x6F, 0x72, 0x61];
20
21/// Error type.
22///
23/// An enum that contains the possible errors this crate can throw.
24#[derive(Error, Debug)]
25#[non_exhaustive]
26pub enum Error {
27    /// no comment packet was found in the file. this suggests the ogg file is malformed or
28    /// uses a codec besides vorbis/theora.
29    #[error("No vorbis or theora comment packet found. oggmeta only supports vorbis and theora comments. your file may be malformed.")]
30    NoComments,
31    /// wrapper around [`std::io::Error`]. generally caused by problems reading the file.
32    #[error("{0}")]
33    IoError(#[from] std::io::Error),
34    /// wrapper around [`std::string::FromUtf8Error`]. this means that your message vector contains
35    /// malformed UTF-8
36    #[error("{0}")]
37    InvalidString(#[from] std::string::FromUtf8Error),
38    /// wrapper around [`std::num::TryFromIntError`]. This means that one of the string indexes in
39    /// the file are unvalid [`u32`]s
40    #[error("{0}")]
41    InvalidLength(#[from] std::num::TryFromIntError),
42    /// there was some error while reading the ogg file. this generally suggests
43    /// some kind of error with libogg, libtheora, or theorafile.
44    #[error("there was an error parsing the ogg file. your file is most likely malformed.")]
45    ParseError,
46    /// wrapper around [`std::ffi::NulError`]
47    /// this means something in theorafile returned null.
48    #[error("{0}")]
49    NullError(#[from] std::ffi::NulError),
50    /// wrapper around [`image::error::ImageError`]
51    #[error("{0}")]
52    ImageError(#[from] image::error::ImageError),
53    /// wrapper around [`std::str::Utf8Error`]
54    #[error("{0}")]
55    StrError(#[from] std::str::Utf8Error),
56    /// wrapper around [`ogg::OggReadError`]
57    #[error("{0}")]
58    OggError(#[from] ogg::OggReadError),
59    /// wrapper around [`base64::DecodeError`]
60    #[error("{0}")]
61    Base64Error(#[from] base64::DecodeError),
62}
63
64/// A struct that contains all the available metadata in the file.
65#[derive(Clone, Debug, Default)]
66pub struct Tag {
67    pub vendor: String,
68    /// A map of comments, where the key is the comment key (e.g., "ARTIST") and the value is a vector of values.
69    /// Note that the key will always be uppercase.
70    pub comments: HashMap<String, Vec<String>>,
71    pub pictures: Vec<Picture>,
72}
73
74/// Implementation of FLAC picture block (is also ogg's recommended way to store album art).
75/// For an encoded description of this struct, see [RFC9639](https://www.rfc-editor.org/rfc/rfc9639.html#name-picture)
76#[derive(Clone, Debug)]
77pub struct Picture {
78    /// the type of picture: see [RFC9639](https://www.rfc-editor.org/rfc/rfc9639.html#table13)
79    pub picture_type: PictureType,
80    /// the media type as specified by [RFC2046](https://www.rfc-editor.org/rfc/rfc9639.html#RFC2046)<br>
81    /// essentially the MIME
82    pub media_type: String,
83    /// description string (often this is "Cover (front)" or something similar)
84    pub description: String,
85    /// width of the picture
86    pub width: u32,
87    /// height of the picture
88    pub height: u32,
89    /// color depth of the picture in bits per pixel
90    pub color_depth: u32,
91    /// For indexed-color pictures (e.g., GIF), the number of colors used; 0 for non-indexed pictures
92    pub number_colors: u32,
93    /// the actual picture data
94    pub data: Vec<u8>,
95}
96
97/// The type of picture, as specified by [RFC9639](https://www.rfc-editor.org/rfc/rfc9639.html#table13)
98#[derive(Debug, Clone, Copy, PartialEq, Eq)]
99#[repr(u32)]
100pub enum PictureType {
101    Other = 0,
102    PngIcon = 1,
103    GeneralIcon = 2,
104    FrontCover = 3,
105    BackCover = 4,
106    LinerNotesPage = 5,
107    MediaLabel = 6,
108    LeadArtist = 7,
109    Artist = 8,
110    Conductor = 9,
111    Band = 10,
112    Composer = 11,
113    Lyricist = 12,
114    RecordingLocation = 13,
115    DuringRecording = 14,
116    DuringPerformance = 15,
117    MovieScreenCapture = 16,
118    BrightColoredFish = 17,
119    Illustration = 18,
120    BandLogo = 19,
121    PublisherLogo = 20,
122}
123
124impl TryFrom<u32> for PictureType {
125    type Error = ();
126
127    fn try_from(value: u32) -> Result<Self, Self::Error> {
128        match value {
129            0 => Ok(PictureType::Other),
130            1 => Ok(PictureType::PngIcon),
131            2 => Ok(PictureType::GeneralIcon),
132            3 => Ok(PictureType::FrontCover),
133            4 => Ok(PictureType::BackCover),
134            5 => Ok(PictureType::LinerNotesPage),
135            6 => Ok(PictureType::MediaLabel),
136            7 => Ok(PictureType::LeadArtist),
137            8 => Ok(PictureType::Artist),
138            9 => Ok(PictureType::Conductor),
139            10 => Ok(PictureType::Band),
140            11 => Ok(PictureType::Composer),
141            12 => Ok(PictureType::Lyricist),
142            13 => Ok(PictureType::RecordingLocation),
143            14 => Ok(PictureType::DuringRecording),
144            15 => Ok(PictureType::DuringPerformance),
145            16 => Ok(PictureType::MovieScreenCapture),
146            17 => Ok(PictureType::BrightColoredFish),
147            18 => Ok(PictureType::Illustration),
148            19 => Ok(PictureType::BandLogo),
149            20 => Ok(PictureType::PublisherLogo),
150            _ => Err(()),
151        }
152    }
153}
154
155impl Tag {
156    /// attempts to read metadata from a [`Read`], returning a [`Tag`]
157    ///
158    /// # Errors
159    /// This function will error if the ogg file is malformed, or if it does not contain a type 3
160    /// vorbis packet (the packet that contains the metadata.)
161    ///
162    /// This function could also error if the architecture of the target causes [`usize`] to be
163    /// unable to contain a [`u32`] (below 32-bit, very unlikely)
164    ///
165    /// Lastly, this function will error if a non-utf8 character is contained in the packet, which
166    /// goes against the vorbis specification.
167    pub fn read_from<R: Read + Seek>(read: &mut R) -> Result<Tag, Error> {
168        reading::parse_file(read)
169    }
170
171    /// This function does the same as [`read_from`](crate::Tag::read_from), but takes a path instead, opening a [`File`]
172    ///
173    /// # Errors
174    /// see [`read_from`](crate::Tag::read_from)
175    pub fn read_from_path<P: AsRef<Path>>(path: &P) -> Result<Tag, Error> {
176        let mut file = File::open(path)?;
177
178        reading::parse_file(&mut file)
179    }
180
181    /// writes tags to a writer, expects the writer
182    /// to already contain a valid ogg stream.
183    /// edits the vorbis comment header only.
184    pub fn write_to<W: Read + Write + Seek>(&mut self, mut f_in: W) -> Result<(), crate::Error> {
185        let mut buf = Vec::new();
186        crate::writing::insert_comments(&mut f_in, &mut buf, self)?;
187        f_in.rewind()?;
188        std::io::copy(&mut buf.as_slice(), &mut f_in)?;
189        Ok(())
190    }
191
192    /// does the same thing as [`Tag::write_to`], but takes a path instead of a writer.
193    /// opens the file in read+write mode, and expects it to already contain a valid ogg stream.
194    /// edits the vorbis comment header only.
195    pub fn write_to_path<P: AsRef<Path>>(&mut self, path: P) -> Result<(), crate::Error> {
196        let mut file = File::options()
197            .read(true)
198            .write(true)
199            .create(false)
200            .open(path)?;
201
202        self.write_to(&mut file)?;
203
204        Ok(())
205    }
206}
207
208impl Picture {
209    pub fn from_raw_block(data: &Vec<u8>) -> Result<Picture, crate::Error> {
210        let mut buf = base64::engine::general_purpose::STANDARD_NO_PAD.decode(data)?;
211
212        read_picture_block(&mut Cursor::new(&mut buf))
213    }
214}
215
216impl From<RgbImage> for Picture {
217    fn from(img: RgbImage) -> Self {
218        let mut img_buf = Cursor::new(vec![]);
219        let (width, height) = img.dimensions();
220        img.write_to(&mut img_buf, image::ImageFormat::Jpeg)
221            .unwrap();
222
223        Picture {
224            picture_type: PictureType::FrontCover,
225            media_type: "image/jpeg".to_string(),
226            description: "Cover (front)".to_string(),
227            width,
228            height,
229            color_depth: 24,
230            number_colors: 0,
231            data: img_buf.into_inner(),
232        }
233    }
234}
235
236impl From<DynamicImage> for Picture {
237    fn from(img: DynamicImage) -> Self {
238        let mut img_buf = Cursor::new(vec![]);
239        let (width, height) = img.dimensions();
240        img.write_to(&mut img_buf, image::ImageFormat::Jpeg)
241            .unwrap();
242
243        Picture {
244            picture_type: PictureType::FrontCover,
245            media_type: "image/jpeg".to_string(),
246            description: "Cover (front)".to_string(),
247            width,
248            height,
249            color_depth: 24,
250            number_colors: 0,
251            data: img_buf.into_inner(),
252        }
253    }
254}
255
256impl TryFrom<&[u8]> for Picture {
257    type Error = crate::Error;
258
259    fn try_from(value: &[u8]) -> Result<Self, Self::Error> {
260        let dyn_img = image::load_from_memory(value)?;
261        Ok(dyn_img.into())
262    }
263}