Add simplified getters/setters and tag reader example

This commit is contained in:
Serial 2021-12-12 12:47:26 -05:00
parent 2a0da87074
commit 64362615cf
13 changed files with 378 additions and 27 deletions

View file

@ -19,6 +19,16 @@ Parse, convert, and write metadata to various audio formats.
| Ogg Vorbis | `ogg` |**X** |**X** |`Vorbis Comments` |
| WAV | `wav`, `wave` |**X** |**X** |`ID3v2`, `RIFF INFO` |
## Example
See the [tag reader example](examples/tag_reader.rs)
To try it out, run:
```bash
cargo run --example tag_reader /path/to/file
```
## Documentation
Available [here](https://docs.rs/lofty)

42
examples/tag_reader.rs Normal file
View file

@ -0,0 +1,42 @@
use lofty::{Accessor, Probe};
fn main() {
let path = std::env::args().nth(1).expect("Error: No path specified!");
let tagged_file = Probe::open(path)
.expect("Error: Bad path provided!")
.read()
.expect("Error: Failed to read file!");
let tag = match tagged_file.primary_tag() {
Some(primary_tag) => primary_tag,
None => tagged_file.first_tag().expect("Error: No tags found!"),
};
println!("--- Tag Information ---");
println!("Title: {}", tag.title().unwrap_or("None"));
println!("Artist: {}", tag.artist().unwrap_or("None"));
println!("Album: {}", tag.album().unwrap_or("None"));
println!("Album Artist: {}", tag.album_artist().unwrap_or("None"));
println!("Genre: {}", tag.genre().unwrap_or("None"));
let properties = tagged_file.properties();
let duration = properties.duration();
let seconds = duration.as_secs() % 60;
let duration_display = format!("{:02}:{:02}", (duration.as_secs() - seconds) / 60, seconds);
println!("--- Audio Properties ---");
println!(
"Bitrate (Audio): {}",
properties.audio_bitrate().unwrap_or(0)
);
println!(
"Bitrate (Overall): {}",
properties.overall_bitrate().unwrap_or(0)
);
println!("Sample Rate: {}", properties.sample_rate().unwrap_or(0));
println!("Channels: {}", properties.channels().unwrap_or(0));
println!("Duration: {}", duration_display);
}

View file

@ -163,7 +163,7 @@ pub use crate::types::{
file::{FileType, TaggedFile},
item::{ItemKey, ItemValue, TagItem},
properties::FileProperties,
tag::{Tag, TagType},
tag::{Accessor, Tag, TagType},
};
pub use crate::types::file::AudioFile;

View file

@ -5,11 +5,47 @@ pub(in crate::logic) mod write;
use crate::error::Result;
use crate::logic::ape::tag::item::{ApeItem, ApeItemRef};
use crate::types::item::{ItemKey, ItemValue, TagItem};
use crate::types::tag::{Tag, TagType};
use crate::types::tag::{Accessor, Tag, TagType};
use std::convert::TryInto;
use std::fs::File;
macro_rules! impl_accessor {
($($name:ident, $($key:literal)|+;)+) => {
paste::paste! {
impl Accessor for ApeTag {
$(
fn $name(&self) -> Option<&str> {
$(
if let Some(i) = self.get_key($key) {
if let ItemValue::Text(val) = i.value() {
return Some(val)
}
}
)+
None
}
fn [<set_ $name>](&mut self, value: String) {
self.insert(ApeItem {
read_only: false,
key: String::from(crate::types::item::first_key!($($key)|*)),
value: ItemValue::Text(value)
})
}
fn [<remove_ $name>](&mut self) {
$(
self.remove_key($key);
)+
}
)+
}
}
}
}
#[derive(Default, Debug, PartialEq)]
/// An `APE` tag
///
@ -39,6 +75,14 @@ pub struct ApeTag {
pub(super) items: Vec<ApeItem>,
}
impl_accessor!(
artist, "Artist";
title, "Title";
album, "Album";
album_artist, "Album Artist" | "ALBUMARTST";
genre, "GENRE";
);
impl ApeTag {
/// Get an [`ApeItem`] by key
///

View file

@ -1,10 +1,30 @@
use crate::error::Result;
use crate::logic::id3::v1::constants::GENRES;
use crate::types::item::{ItemKey, ItemValue, TagItem};
use crate::types::tag::{Tag, TagType};
use crate::types::tag::{Accessor, Tag, TagType};
use std::fs::File;
macro_rules! impl_accessor {
($($name:ident,)+) => {
paste::paste! {
$(
fn $name(&self) -> Option<&str> {
self.$name.as_deref()
}
fn [<set_ $name>](&mut self, value: String) {
self.$name = Some(value)
}
fn [<remove_ $name>](&mut self) {
self.$name = None
}
)+
}
}
}
#[derive(Default, Debug, PartialEq)]
/// An ID3v1 tag
///
@ -57,6 +77,26 @@ pub struct Id3v1Tag {
pub genre: Option<u8>,
}
impl Accessor for Id3v1Tag {
impl_accessor!(title, artist, album,);
fn genre(&self) -> Option<&str> {
if let Some(g) = self.genre {
let g = g as usize;
if g < GENRES.len() {
return Some(GENRES[g]);
}
}
None
}
fn remove_genre(&mut self) {
self.genre = None
}
}
impl Id3v1Tag {
/// Returns `true` if the tag contains no data
pub fn is_empty(&self) -> bool {

View file

@ -23,8 +23,9 @@ pub(crate) fn parse_content(
"TXXX" => parse_user_defined(content, false)?,
"WXXX" => parse_user_defined(content, true)?,
"COMM" | "USLT" => parse_text_language(content, id)?,
// Apple's WFED (Podcast URL) is a text frame
_ if id.starts_with('T') || id == "WFED" => parse_text(content)?,
// Apple proprietary frames
"WFED" | "GRP1" => parse_text(content)?,
_ if id.starts_with('W') => parse_link(content)?,
// SYLT, GEOB, and any unknown frames
_ => FrameValue::Binary(content.to_vec()),

View file

@ -9,13 +9,51 @@ use crate::logic::id3::v2::frame::FrameRef;
use crate::probe::Probe;
use crate::types::file::FileType;
use crate::types::item::{ItemKey, ItemValue, TagItem};
use crate::types::tag::{Tag, TagType};
use crate::types::tag::{Accessor, Tag, TagType};
use std::convert::TryInto;
use std::fs::File;
use byteorder::ByteOrder;
macro_rules! impl_accessor {
($($name:ident, $id:literal;)+) => {
paste::paste! {
impl Accessor for Id3v2Tag {
$(
fn $name(&self) -> Option<&str> {
if let Some(f) = self.get($id) {
if let FrameValue::Text {
ref value,
..
} = f.content() {
return Some(value)
}
}
None
}
fn [<set_ $name>](&mut self, value: String) {
self.insert(Frame {
id: FrameID::Valid(String::from($id)),
value: FrameValue::Text {
encoding: TextEncoding::UTF8,
value,
},
flags: FrameFlags::default()
});
}
fn [<remove_ $name>](&mut self) {
self.remove($id)
}
)+
}
}
}
}
#[derive(PartialEq, Debug)]
/// An `ID3v2` tag
///
@ -59,6 +97,14 @@ pub struct Id3v2Tag {
frames: Vec<Frame>,
}
// TODO: Genre
impl_accessor!(
title, "TIT2";
artist, "TPE1";
album, "TALB";
album_artist, "TPE2";
);
impl Default for Id3v2Tag {
fn default() -> Self {
Self {

View file

@ -1,7 +1,7 @@
use crate::error::Result;
use crate::logic::iff::chunk::Chunks;
use crate::types::item::{ItemKey, ItemValue, TagItem};
use crate::types::tag::{Tag, TagType};
use crate::types::tag::{Accessor, Tag, TagType};
use std::convert::TryFrom;
use std::fs::File;
@ -41,7 +41,44 @@ pub struct AiffTextChunks {
pub copyright: Option<String>,
}
impl Accessor for AiffTextChunks {
fn artist(&self) -> Option<&str> {
self.author.as_deref()
}
fn set_artist(&mut self, value: String) {
self.author = Some(value)
}
fn remove_artist(&mut self) {
self.author = None
}
fn title(&self) -> Option<&str> {
self.name.as_deref()
}
fn set_title(&mut self, value: String) {
self.name = Some(value)
}
fn remove_title(&mut self) {
self.name = None
}
}
impl AiffTextChunks {
/// Returns the copyright message
pub fn copyright(&self) -> Option<&str> {
self.copyright.as_deref()
}
/// Sets the copyright message
pub fn set_copyright(&mut self, value: String) {
self.copyright = Some(value)
}
/// Removes the copyright message
pub fn remove_copyright(&mut self) {
self.copyright = None
}
/// Writes the tag to a file
///
/// # Errors

View file

@ -3,10 +3,32 @@ pub(in crate::logic::iff::wav) mod write;
use crate::error::Result;
use crate::types::item::{ItemKey, ItemValue, TagItem};
use crate::types::tag::{Tag, TagType};
use crate::types::tag::{Accessor, Tag, TagType};
use std::fs::File;
macro_rules! impl_accessor {
($($name:ident, $key:literal;)+) => {
paste::paste! {
impl Accessor for RiffInfoList {
$(
fn $name(&self) -> Option<&str> {
self.get($key)
}
fn [<set_ $name>](&mut self, value: String) {
self.insert(String::from($key), value)
}
fn [<remove_ $name>](&mut self) {
self.remove($key)
}
)+
}
}
}
}
#[derive(Default, Debug, PartialEq)]
/// A RIFF INFO LIST
///
@ -25,6 +47,13 @@ pub struct RiffInfoList {
pub(crate) items: Vec<(String, String)>,
}
impl_accessor!(
artist, "IART";
title, "INAM";
album, "IPRD";
genre, "IGNR";
);
impl RiffInfoList {
/// Get an item by key
pub fn get(&self, key: &str) -> Option<&str> {

View file

@ -6,12 +6,49 @@ use super::AtomIdent;
use crate::error::Result;
use crate::types::item::{ItemKey, ItemValue, TagItem};
use crate::types::picture::{Picture, PictureType};
use crate::types::tag::{Tag, TagType};
use crate::types::tag::{Accessor, Tag, TagType};
use atom::{Atom, AtomData, AtomDataRef, AtomIdentRef, AtomRef};
use std::convert::TryInto;
use std::fs::File;
const ARTIST: AtomIdent = AtomIdent::Fourcc(*b"\xa9ART");
const TITLE: AtomIdent = AtomIdent::Fourcc(*b"\xa9nam");
const ALBUM: AtomIdent = AtomIdent::Fourcc(*b"\xa9alb");
const ALBUM_ARTIST: AtomIdent = AtomIdent::Fourcc(*b"aART");
const GENRE: AtomIdent = AtomIdent::Fourcc(*b"\xa9gen");
macro_rules! impl_accessor {
($($name:ident, $const:ident;)+) => {
paste::paste! {
impl Accessor for Ilst {
$(
fn $name(&self) -> Option<&str> {
if let Some(atom) = self.atom(&$const) {
if let AtomData::UTF8(val) | AtomData::UTF16(val) = atom.data() {
return Some(val)
}
}
None
}
fn [<set_ $name>](&mut self, value: String) {
self.replace_atom(Atom {
ident: $const,
data: AtomData::UTF8(value),
})
}
fn [<remove_ $name>](&mut self) {
self.remove_atom(&$const)
}
)+
}
}
}
}
#[derive(Default, PartialEq, Debug)]
/// An MP4 ilst atom
///
@ -41,6 +78,14 @@ pub struct Ilst {
pub(crate) atoms: Vec<Atom>,
}
impl_accessor!(
artist, ARTIST;
title, TITLE;
album, ALBUM;
album_artist, ALBUM_ARTIST;
genre, GENRE;
);
impl Ilst {
/// Get an item by its [`AtomIdent`]
pub fn atom(&self, ident: &AtomIdent) -> Option<&Atom> {

View file

@ -5,10 +5,32 @@ use crate::types::file::FileType;
use crate::types::item::{ItemKey, ItemValue, TagItem};
use crate::types::picture::Picture;
use crate::types::picture::PictureInformation;
use crate::types::tag::{Tag, TagType};
use crate::types::tag::{Accessor, Tag, TagType};
use std::fs::File;
macro_rules! impl_accessor {
($($name:ident, $key:literal;)+) => {
paste::paste! {
impl Accessor for VorbisComments {
$(
fn $name(&self) -> Option<&str> {
self.get_item($key)
}
fn [<set_ $name>](&mut self, value: String) {
self.insert_item(String::from($key), value, true)
}
fn [<remove_ $name>](&mut self) {
self.remove_key($key)
}
)+
}
}
}
}
#[derive(Default, PartialEq, Debug)]
/// Vorbis comments
pub struct VorbisComments {
@ -20,6 +42,14 @@ pub struct VorbisComments {
pub(crate) pictures: Vec<(Picture, PictureInformation)>,
}
impl_accessor!(
artist, "ARTIST";
title, "TITLE";
album, "ALBUM";
album_artist, "ALBUMARTST";
genre, "GENRE";
);
impl VorbisComments {
/// Returns the vendor string
pub fn vendor(&self) -> &str {

View file

@ -7,6 +7,8 @@ macro_rules! first_key {
};
}
pub(crate) use first_key;
// This is used to create the ItemKey enum and its to and from key conversions
//
// First comes the ItemKey variant as an ident (ex. Artist), then a collection of the appropriate mappings.
@ -133,7 +135,7 @@ item_keys!(
// People & Organizations
AlbumArtist => [
TagType::Id3v2 => "TPE2", TagType::Mp4Ilst => "aART",
TagType::VorbisComments | TagType::Ape => "ALBUMARTIST"
TagType::VorbisComments => "ALBUMARTIST", TagType::Ape => "Album Artist" | "ALBUMARTIST"
],
TrackArtist => [
TagType::Id3v2 => "TPE1", TagType::Mp4Ilst => "\u{a9}ART",

View file

@ -6,15 +6,36 @@ use crate::probe::Probe;
use std::fs::{File, OpenOptions};
use std::path::Path;
use paste::paste;
macro_rules! common_items {
($($item_key:ident => $name:tt),+) => {
paste! {
impl Tag {
macro_rules! accessor_trait {
($($name:ident),+) => {
/// Provides accessors for common items
pub trait Accessor {
paste::paste! {
$(
#[doc = "Gets the " $name]
pub fn $name(&self) -> Option<&str> {
fn $name(&self) -> Option<&str> { None }
#[doc = "Sets the " $name]
fn [<set_ $name>](&mut self, _value: String) {}
#[doc = "Removes the " $name]
fn [<remove_ $name>](&mut self) {}
)+
}
}
};
}
accessor_trait! {
artist, title,
album, album_artist,
genre
}
macro_rules! impl_accessor {
($($item_key:ident => $name:tt),+) => {
paste::paste! {
impl Accessor for Tag {
$(
fn $name(&self) -> Option<&str> {
if let Some(ItemValue::Text(txt)) = self.get_item_ref(&ItemKey::$item_key).map(TagItem::value) {
return Some(&*txt)
}
@ -22,14 +43,12 @@ macro_rules! common_items {
None
}
#[doc = "Removes the " $name]
pub fn [<remove_ $name>](&mut self) {
self.retain_items(|i| i.item_key != ItemKey::$item_key)
fn [<set_ $name>](&mut self, value: String) {
self.insert_item(TagItem::new(ItemKey::$item_key, ItemValue::Text(value)));
}
#[doc = "Sets the " $name]
pub fn [<set_ $name>](&mut self, value: String) {
self.insert_item(TagItem::new(ItemKey::$item_key, ItemValue::Text(value)));
fn [<remove_ $name>](&mut self) {
self.retain_items(|i| i.item_key != ItemKey::$item_key)
}
)+
}
@ -56,13 +75,13 @@ macro_rules! common_items {
/// Accessing common items
///
/// ```rust
/// # use lofty::{Tag, TagType};
/// # use lofty::{Tag, TagType, Accessor};
/// # let tag = Tag::new(TagType::Id3v2);
/// // There are multiple quick getter methods for common items
///
/// let title = tag.title();
/// let artist = tag.artist();
/// let album = tag.album_title();
/// let album = tag.album();
/// let album_artist = tag.album_artist();
/// ```
///
@ -107,6 +126,14 @@ impl IntoIterator for Tag {
}
}
impl_accessor!(
TrackArtist => artist,
TrackTitle => title,
AlbumTitle => album,
AlbumArtist => album_artist,
Genre => genre
);
impl Tag {
/// Initialize a new tag with a certain [`TagType`]
pub fn new(tag_type: TagType) -> Self {
@ -282,8 +309,6 @@ impl Tag {
}
}
common_items!(TrackArtist => artist, TrackTitle => title, AlbumTitle => album_title, AlbumArtist => album_artist);
/// The tag's format
#[derive(Clone, Debug, PartialEq)]
pub enum TagType {