mirror of
https://github.com/Serial-ATA/lofty-rs
synced 2024-11-10 06:34:18 +00:00
Implement read/write for ape
Quite a few weird things had to be done due to the ape crate's choices, and ape's lack of a standardized set of keys, but it all seems to work. Signed-off-by: Serial <69764315+Serial-ATA@users.noreply.github.com>
This commit is contained in:
parent
04a9ab4634
commit
001c33bf86
10 changed files with 346 additions and 9 deletions
26
Cargo.toml
26
Cargo.toml
|
@ -12,15 +12,25 @@ categories = ["accessiblity", "multimedia::audio"]
|
|||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
opus_headers = {version = "0.1.2", optional = true}
|
||||
lewton = {version = "0.10.2", optional = true}
|
||||
mp4ameta = {version = "0.9.1", optional = true}
|
||||
metaflac = {version = "0.2.4", optional = true}
|
||||
ogg = {version = "0.8.0", optional = true}
|
||||
id3 = {version = "0.6.2", optional = true}
|
||||
mp3-duration = {version = "0.1.10", optional = true}
|
||||
# Ape
|
||||
ape = {version = "0.3.0", optional = true}
|
||||
# Wav
|
||||
hound = {version = "3.4.0", optional = true}
|
||||
# Mp3
|
||||
id3 = {version = "0.6.2", optional = true} # De/Encoding
|
||||
mp3-duration = {version = "0.1.10", optional = true} # Duration
|
||||
# Ogg
|
||||
lewton = {version = "0.10.2", optional = true} # Decoding
|
||||
ogg = {version = "0.8.0", optional = true} # Encoding
|
||||
# Mp4
|
||||
mp4ameta = {version = "0.9.1", optional = true}
|
||||
# Flac
|
||||
metaflac = {version = "0.2.4", optional = true}
|
||||
# Opus
|
||||
opus_headers = {version = "0.1.2", optional = true}
|
||||
# Errors
|
||||
thiserror = "1.0.24"
|
||||
filepath = "0.1.1"
|
||||
|
||||
[features]
|
||||
default = ["full"]
|
||||
|
@ -29,5 +39,5 @@ mp4 = ["mp4ameta"]
|
|||
mp3 = ["id3"]
|
||||
wav = ["hound"]
|
||||
vorbis = ["lewton", "metaflac", "opus_headers", "ogg"]
|
||||
all_tags = ["vorbis", "mp4", "mp3", "wav"]
|
||||
all_tags = ["vorbis", "mp4", "mp3", "wav", "ape"]
|
||||
duration = ["mp3-duration"]
|
313
src/components/tags/ape_tag.rs
Normal file
313
src/components/tags/ape_tag.rs
Normal file
|
@ -0,0 +1,313 @@
|
|||
#![cfg(feature = "ape")]
|
||||
|
||||
use crate::{
|
||||
impl_tag, traits::ReadPath, Album, AnyTag, AudioTag, AudioTagEdit, AudioTagWrite, Picture,
|
||||
Result, TagType, ToAny, ToAnyTag,
|
||||
};
|
||||
use std::{fs::File, path::Path};
|
||||
|
||||
pub use ape::Tag as ApeInnerTag;
|
||||
use filepath::FilePath;
|
||||
|
||||
impl ReadPath for ApeInnerTag {
|
||||
fn from_path<P>(path: P, _tag_type: Option<TagType>) -> Result<Self>
|
||||
where
|
||||
P: AsRef<std::path::Path>,
|
||||
Self: Sized,
|
||||
{
|
||||
Ok(ape::read(path)?)
|
||||
}
|
||||
}
|
||||
|
||||
impl_tag!(ApeTag, ApeInnerTag, TagType::Ape);
|
||||
|
||||
impl<'a> From<&'a ApeTag> for AnyTag<'a> {
|
||||
fn from(inp: &'a ApeTag) -> Self {
|
||||
Self {
|
||||
title: inp.title(),
|
||||
artists: inp.artists(),
|
||||
year: inp.year().map(|y| y as i32),
|
||||
album: Album::new(inp.album_title(), inp.album_artists(), inp.album_cover()),
|
||||
track_number: inp.track_number(),
|
||||
total_tracks: inp.total_tracks(),
|
||||
disc_number: inp.disc_number(),
|
||||
total_discs: inp.total_discs(),
|
||||
comments: None,
|
||||
date: None, // TODO
|
||||
duration_ms: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<AnyTag<'a>> for ApeTag {
|
||||
fn from(inp: AnyTag<'a>) -> Self {
|
||||
let mut tag = ApeTag::new();
|
||||
|
||||
if let Some(v) = inp.title() {
|
||||
tag.set_title(v)
|
||||
}
|
||||
if let Some(v) = inp.artists_as_string() {
|
||||
tag.set_artist(v.as_str())
|
||||
}
|
||||
if let Some(v) = inp.year {
|
||||
tag.set_year(v)
|
||||
}
|
||||
if let Some(v) = inp.album().title {
|
||||
tag.set_album_title(v)
|
||||
}
|
||||
if let Some(v) = inp.album().artists {
|
||||
tag.set_album_artists(v.join(", "))
|
||||
}
|
||||
if let Some(v) = inp.track_number() {
|
||||
tag.set_track(v)
|
||||
}
|
||||
if let Some(v) = inp.total_tracks() {
|
||||
tag.set_total_tracks(v)
|
||||
}
|
||||
if let Some(v) = inp.disc_number() {
|
||||
tag.set_disc(v)
|
||||
}
|
||||
if let Some(v) = inp.total_discs() {
|
||||
tag.set_total_discs(v)
|
||||
}
|
||||
|
||||
tag
|
||||
}
|
||||
}
|
||||
|
||||
impl ApeTag {
|
||||
fn get_value(&self, key: &str) -> Option<&str> {
|
||||
if let Some(item) = self.0.item(key) {
|
||||
if let ape::ItemValue::Text(val) = &item.value {
|
||||
return Some(&*val);
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
fn set_value<V>(&mut self, key: &str, val: V)
|
||||
where
|
||||
V: Into<String>,
|
||||
{
|
||||
let item = ape::Item {
|
||||
key: key.to_string(),
|
||||
value: ape::ItemValue::Text(val.into()),
|
||||
};
|
||||
|
||||
self.0.set_item(item)
|
||||
}
|
||||
|
||||
fn remove_key(&mut self, key: &str) {
|
||||
let _ = self.0.remove_item(key);
|
||||
}
|
||||
}
|
||||
|
||||
impl AudioTagEdit for ApeTag {
|
||||
fn title(&self) -> Option<&str> {
|
||||
self.get_value("Title")
|
||||
}
|
||||
fn set_title(&mut self, title: &str) {
|
||||
self.set_value("Title", title)
|
||||
}
|
||||
fn remove_title(&mut self) {
|
||||
self.remove_key("Title")
|
||||
}
|
||||
|
||||
fn artist(&self) -> Option<&str> {
|
||||
self.get_value("Artist")
|
||||
}
|
||||
|
||||
fn set_artist(&mut self, artist: &str) {
|
||||
self.set_value("Artist", artist)
|
||||
}
|
||||
|
||||
fn add_artist(&mut self, artist: &str) {
|
||||
let artist = self.artist().as_ref().map_or_else(
|
||||
|| String::from(artist),
|
||||
|artist| {
|
||||
let mut artists: Vec<&str> = artist.split(", ").collect();
|
||||
artists.push(artist);
|
||||
artists.join(", ")
|
||||
},
|
||||
);
|
||||
|
||||
self.set_artist(artist.as_str())
|
||||
}
|
||||
|
||||
fn artists(&self) -> Option<Vec<&str>> {
|
||||
self.artist().map(|a| a.split(", ").collect())
|
||||
}
|
||||
|
||||
fn remove_artist(&mut self) {
|
||||
self.remove_key("Artist")
|
||||
}
|
||||
|
||||
fn year(&self) -> Option<i32> {
|
||||
if let Some(Ok(y)) = self
|
||||
.get_value("Date")
|
||||
.map(|s| s.chars().take(4).collect::<String>().parse::<i32>())
|
||||
{
|
||||
Some(y)
|
||||
} else if let Some(Ok(y)) = self.get_value("Year").map(str::parse::<i32>) {
|
||||
Some(y)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
fn set_year(&mut self, year: i32) {
|
||||
self.set_value("Year", year.to_string())
|
||||
}
|
||||
fn remove_year(&mut self) {
|
||||
self.remove_key("Year")
|
||||
}
|
||||
|
||||
fn album_title(&self) -> Option<&str> {
|
||||
self.get_value("Album")
|
||||
}
|
||||
fn set_album_title(&mut self, v: &str) {
|
||||
self.set_value("Album", v)
|
||||
}
|
||||
fn remove_album_title(&mut self) {
|
||||
self.remove_key("Album")
|
||||
}
|
||||
|
||||
// Album artists aren't standard?
|
||||
fn album_artists(&self) -> Option<Vec<&str>> {
|
||||
self.get_value("Album artist")
|
||||
.map(|a| a.split(", ").collect())
|
||||
}
|
||||
|
||||
fn set_album_artists(&mut self, artists: String) {
|
||||
self.set_value("Album artist", artists)
|
||||
}
|
||||
|
||||
fn add_album_artist(&mut self, _artist: &str) {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn remove_album_artists(&mut self) {
|
||||
self.remove_key("Album artist")
|
||||
}
|
||||
|
||||
fn album_cover(&self) -> Option<Picture> {
|
||||
None // TODO
|
||||
}
|
||||
fn set_album_cover(&mut self, _cover: Picture) {
|
||||
// TODO
|
||||
}
|
||||
fn remove_album_cover(&mut self) {
|
||||
// TODO
|
||||
}
|
||||
|
||||
// Track number and total tracks are stored together as num/total?
|
||||
fn track_number(&self) -> Option<u32> {
|
||||
let numbers = self.get_value("Track");
|
||||
|
||||
if let Some(numbers) = numbers {
|
||||
let split: Vec<&str> = numbers.split('/').collect();
|
||||
let track_number = split[0];
|
||||
|
||||
if !track_number.is_empty() {
|
||||
if let Ok(num) = track_number.parse::<u32>() {
|
||||
return Some(num);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
fn set_track_number(&mut self, track: u32) {
|
||||
if let (_, Some(total)) = self.track() {
|
||||
let track_str = format!("{}/{}", track, total);
|
||||
self.set_value("Track", track_str)
|
||||
} else {
|
||||
self.set_value("Track", track.to_string())
|
||||
}
|
||||
}
|
||||
fn remove_track_number(&mut self) {
|
||||
self.remove_key("Track")
|
||||
}
|
||||
|
||||
fn total_tracks(&self) -> Option<u32> {
|
||||
let numbers = self.get_value("Track");
|
||||
|
||||
if let Some(numbers) = numbers {
|
||||
let split: Vec<&str> = numbers.split('/').collect();
|
||||
let track_number = split[1];
|
||||
|
||||
if !track_number.is_empty() {
|
||||
if let Ok(num) = track_number.parse::<u32>() {
|
||||
return Some(num);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
fn set_total_tracks(&mut self, total_track: u32) {
|
||||
if let (Some(track_number), _) = self.track() {
|
||||
let track_str = format!("{}/{}", track_number, total_track);
|
||||
self.set_value("Track", track_str)
|
||||
} else {
|
||||
self.set_value("Track", format!("0/{}", total_track))
|
||||
}
|
||||
}
|
||||
fn remove_total_tracks(&mut self) {
|
||||
if let (Some(track_number), _) = self.track() {
|
||||
self.set_value("Track", track_number.to_string())
|
||||
} else {
|
||||
self.remove_track_number()
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: unsure what to do with these, disc information isn't standard
|
||||
// Just using keys that would make sense, but it's a guess
|
||||
fn disc_number(&self) -> Option<u32> {
|
||||
if let Some(disc_num) = self.get_value("Disc") {
|
||||
if let Ok(num) = disc_num.parse::<u32>() {
|
||||
return Some(num);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(disc_num) = self.get_value("Disc") {
|
||||
if let Ok(num) = disc_num.parse::<u32>() {
|
||||
return Some(num);
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
fn set_disc_number(&mut self, disc_number: u32) {
|
||||
self.set_value("Disc", disc_number.to_string())
|
||||
}
|
||||
fn remove_disc_number(&mut self) {
|
||||
self.remove_key("Disc");
|
||||
}
|
||||
|
||||
fn total_discs(&self) -> Option<u32> {
|
||||
if let Some(Ok(num)) = self.get_value("Disc").map(str::parse::<u32>) {
|
||||
return Some(num);
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
fn set_total_discs(&mut self, total_discs: u32) {
|
||||
self.set_value("Disc", total_discs.to_string())
|
||||
}
|
||||
fn remove_total_discs(&mut self) {
|
||||
self.remove_key("Disc")
|
||||
}
|
||||
}
|
||||
|
||||
impl AudioTagWrite for ApeTag {
|
||||
fn write_to(&self, file: &mut File) -> Result<()> {
|
||||
// Write only uses paths, this is annoying
|
||||
ape::write(&self.0, file.path()?)?;
|
||||
Ok(())
|
||||
}
|
||||
fn write_to_path(&self, path: &str) -> Result<()> {
|
||||
ape::write(&self.0, path)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
|
@ -1,8 +1,11 @@
|
|||
pub(crate) mod ape_tag;
|
||||
pub(crate) mod id3_tag;
|
||||
pub(crate) mod mp4_tag;
|
||||
pub(crate) mod vorbis_tag;
|
||||
pub(crate) mod wav_tag;
|
||||
|
||||
#[cfg(feature = "ape")]
|
||||
pub use ape_tag::ApeTag;
|
||||
#[cfg(feature = "mp3")]
|
||||
pub use id3_tag::Id3v2Tag;
|
||||
#[cfg(feature = "mp4")]
|
||||
|
|
|
@ -11,6 +11,8 @@ pub enum Error {
|
|||
#[error("Unsupported mime type: {0}")]
|
||||
UnsupportedMimeType(String),
|
||||
|
||||
#[error(transparent)]
|
||||
ApeTag(#[from] ape::Error),
|
||||
#[error(transparent)]
|
||||
FlacTag(#[from] metaflac::Error),
|
||||
#[error(transparent)]
|
||||
|
|
|
@ -36,11 +36,13 @@ impl Tag {
|
|||
.as_ref()
|
||||
.unwrap_or(&TagType::try_from_ext(extension_str)?)
|
||||
{
|
||||
#[cfg(feature = "ape")]
|
||||
TagType::Ape => Ok(Box::new(ApeTag::read_from_path(path, None)?)),
|
||||
#[cfg(feature = "mp3")]
|
||||
TagType::Id3v2 => Ok(Box::new(Id3v2Tag::read_from_path(path, None)?)),
|
||||
#[cfg(feature = "mp4")]
|
||||
TagType::Mp4 => Ok(Box::new(Mp4Tag::read_from_path(path, None)?)),
|
||||
#[cfg(feature = "vorbis")]
|
||||
#[cfg(feature = "vorbis")] // TODO: this isn't ideal, make this better somehow
|
||||
id => Ok(Box::new(VorbisTag::read_from_path(path, Some(id.clone()))?)),
|
||||
}
|
||||
}
|
||||
|
@ -49,6 +51,9 @@ impl Tag {
|
|||
/// The tag type, based on the file extension.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub enum TagType {
|
||||
#[cfg(feature = "ape")]
|
||||
/// Common file extensions: `.ape`
|
||||
Ape,
|
||||
#[cfg(feature = "mp3")]
|
||||
/// Common file extensions: `.mp3`
|
||||
Id3v2,
|
||||
|
@ -69,6 +74,8 @@ pub enum TagType {
|
|||
impl TagType {
|
||||
fn try_from_ext(ext: &str) -> Result<Self> {
|
||||
match ext {
|
||||
#[cfg(feature = "ape")]
|
||||
"ape" => Ok(Self::Ape),
|
||||
#[cfg(feature = "mp3")]
|
||||
"mp3" => Ok(Self::Id3v2),
|
||||
#[cfg(feature = "vorbis")]
|
||||
|
|
|
@ -106,6 +106,8 @@ pub trait ToAnyTag: ToAny {
|
|||
// TODO: write a macro or something that implement this method for every tag type so that if the
|
||||
// TODO: target type is the same, just return self
|
||||
match tag_type {
|
||||
#[cfg(feature = "ape")]
|
||||
TagType::Ape => Box::new(ApeTag::from(self.to_anytag())),
|
||||
#[cfg(feature = "mp3")]
|
||||
TagType::Id3v2 => Box::new(Id3v2Tag::from(self.to_anytag())),
|
||||
#[cfg(feature = "vorbis")]
|
||||
|
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Loading…
Reference in a new issue