mirror of
https://github.com/Serial-ATA/lofty-rs
synced 2024-11-10 06:34:18 +00:00
Add simplified getters/setters and tag reader example
This commit is contained in:
parent
2a0da87074
commit
64362615cf
13 changed files with 378 additions and 27 deletions
10
README.md
10
README.md
|
@ -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
42
examples/tag_reader.rs
Normal 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);
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
///
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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()),
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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> {
|
||||
|
|
|
@ -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> {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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 {
|
||||
|
|
Loading…
Reference in a new issue