diff --git a/Cargo.toml b/Cargo.toml index c64e1092..c3e98643 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,6 +32,7 @@ riff_info_list = [] [dev-dependencies] criterion = { version = "0.3.5", features = ["html_reports"] } +structopt = { version = "0.3.25", default-features = false } tempfile = "3.2.0" [[bench]] diff --git a/examples/tag_stripper.rs b/examples/tag_stripper.rs index 4afcf792..1bf5efa0 100644 --- a/examples/tag_stripper.rs +++ b/examples/tag_stripper.rs @@ -12,7 +12,7 @@ fn main() { let tags = tagged_file.tags(); if tags.is_empty() { - println!("No tags found, exiting."); + eprintln!("No tags found, exiting."); std::process::exit(0); } @@ -45,14 +45,14 @@ fn main() { } input.clear(); - println!("Bad input") + eprintln!("ERROR: Unexpected input") } let tag_remove = available_tag_types[to_remove.unwrap()]; if tag_remove.remove_from_path(path) { - println!("Removed tag: {:?}", tag_remove); + println!("INFO: Removed tag: `{:?}`", tag_remove); } else { - println!("Failed to remove the tag") + eprintln!("ERROR: Failed to remove the tag") } } diff --git a/examples/tag_writer.rs b/examples/tag_writer.rs new file mode 100644 index 00000000..acc50880 --- /dev/null +++ b/examples/tag_writer.rs @@ -0,0 +1,83 @@ +use lofty::{Accessor, Probe, Tag}; + +use structopt::StructOpt; + +#[derive(Debug, StructOpt)] +#[structopt(name = "tag_writer", about = "A simple tag writer example")] +struct Opt { + #[structopt(short, long)] + title: Option, + + #[structopt(short, long)] + artist: Option, + + #[structopt(short = "A", long)] + album: Option, + + #[structopt(short, long)] + genre: Option, + + #[structopt(short, long)] + path: String, +} + +fn main() { + let opt = Opt::from_args(); + + let mut tagged_file = Probe::open(opt.path.as_str()) + .expect("Error: Bad path provided!") + .read() + .expect("Error: Failed to read file!"); + + let tag = match tagged_file.primary_tag_mut() { + Some(primary_tag) => primary_tag, + None => { + if let Some(first_tag) = tagged_file.first_tag_mut() { + first_tag + } else { + let tag_type = tagged_file.primary_tag_type(); + + eprintln!( + "WARN: No tags found, creating a new tag of type `{:?}`", + tag_type + ); + tagged_file.insert_tag(Tag::new(tag_type)); + + tagged_file.primary_tag_mut().unwrap() + } + }, + }; + + if let Opt { + title: None, + artist: None, + album: None, + genre: None, + .. + } = opt + { + eprintln!("ERROR: No options provided!"); + std::process::exit(1); + } + + if let Some(title) = opt.title { + tag.set_title(title) + } + + if let Some(artist) = opt.artist { + tag.set_artist(artist) + } + + if let Some(album) = opt.album { + tag.set_album(album) + } + + if let Some(genre) = opt.genre { + tag.set_genre(genre) + } + + tag.save_to_path(opt.path) + .expect("ERROR: Failed to write the tag!"); + + println!("INFO: Tag successfully updated!"); +} diff --git a/src/types/file.rs b/src/types/file.rs index e84b7153..71b2598c 100644 --- a/src/types/file.rs +++ b/src/types/file.rs @@ -4,6 +4,7 @@ use crate::error::{LoftyError, Result}; use std::convert::TryInto; use std::ffi::OsStr; +use std::fs::{File, OpenOptions}; use std::io::{Read, Seek}; use std::path::Path; @@ -53,18 +54,21 @@ impl TaggedFile { /// | `FLAC`, `Opus`, `Vorbis` | `VorbisComments` | /// | `MP4` | `Mp4Ilst` | pub fn primary_tag(&self) -> Option<&Tag> { - self.tag(&Self::primary_tag_type(self.ty)) + self.tag(&self.primary_tag_type()) } /// Gets a mutable reference to the file's "Primary tag" /// /// See [`primary_tag`](Self::primary_tag) for an explanation pub fn primary_tag_mut(&mut self) -> Option<&mut Tag> { - self.tag_mut(&Self::primary_tag_type(self.ty)) + self.tag_mut(&self.primary_tag_type()) } - fn primary_tag_type(f_ty: FileType) -> TagType { - match f_ty { + /// Returns the file type's "primary" [`TagType`] + /// + /// See [`primary_tag`](Self::primary_tag) for an explanation + pub fn primary_tag_type(&self) -> TagType { + match self.ty { #[cfg(feature = "id3v2")] FileType::AIFF | FileType::MP3 | FileType::WAV => TagType::Id3v2, #[cfg(feature = "ape")] @@ -76,6 +80,11 @@ impl TaggedFile { } } + /// Determines whether the file supports the given [`TagType`] + pub fn supports_tag_type(&self, tag_type: TagType) -> bool { + self.ty.supports_tag_type(&tag_type) + } + /// Returns all tags pub fn tags(&self) -> &[Tag] { self.tags.as_slice() @@ -101,6 +110,32 @@ impl TaggedFile { self.tags.iter_mut().find(|i| i.tag_type() == tag_type) } + /// Inserts a [`Tag`] + /// + /// If a tag is replaced, it will be returned + pub fn insert_tag(&mut self, tag: Tag) -> Option { + let tag_type = *tag.tag_type(); + + if self.supports_tag_type(tag_type) { + let ret = self.remove_tag(tag_type); + self.tags.push(tag); + + return ret; + } + + None + } + + /// Removes a specific [`TagType`] + /// + /// This will return the tag if it is removed + pub fn remove_tag(&mut self, tag_type: TagType) -> Option { + self.tags + .iter() + .position(|t| t.tag_type() == &tag_type) + .map(|pos| self.tags.remove(pos)) + } + /// Returns the file's [`FileType`] pub fn file_type(&self) -> &FileType { &self.ty @@ -110,6 +145,28 @@ impl TaggedFile { pub fn properties(&self) -> &FileProperties { &self.properties } + + /// Attempts to write all tags to a path + /// + /// # Errors + /// + /// See [TaggedFile::save_to] + pub fn save_to_path(&self, path: impl AsRef) -> Result<()> { + self.save_to(&mut OpenOptions::new().read(true).write(true).open(path)?) + } + + /// Attempts to write all tags to a file + /// + /// # Errors + /// + /// See [`Tag::save_to`], however this is applicable to every tag in the `TaggedFile`. + pub fn save_to(&self, file: &mut File) -> Result<()> { + for tag in &self.tags { + tag.save_to(file)?; + } + + Ok(()) + } } #[derive(PartialEq, Copy, Clone, Debug)]