├── assets ├── a.ape ├── a.m4a ├── a.mp3 ├── a.ogg ├── a.wav └── a.flac ├── src ├── components.rs ├── config.rs ├── error.rs ├── anytag.rs ├── types.rs ├── traits.rs ├── lib.rs └── components │ ├── flac_tag.rs │ ├── id3_tag.rs │ └── mp4_tag.rs ├── audiotags-macro ├── .gitignore ├── Cargo.toml └── src │ └── lib.rs ├── .gitignore ├── Cargo.toml ├── LICENSE ├── .github └── workflows │ └── rust.yml ├── CHANGELOG.md ├── tests ├── inner.rs └── io.rs └── README.md /assets/a.ape: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TianyiShi2001/audiotags/HEAD/assets/a.ape -------------------------------------------------------------------------------- /assets/a.m4a: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TianyiShi2001/audiotags/HEAD/assets/a.m4a -------------------------------------------------------------------------------- /assets/a.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TianyiShi2001/audiotags/HEAD/assets/a.mp3 -------------------------------------------------------------------------------- /assets/a.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TianyiShi2001/audiotags/HEAD/assets/a.ogg -------------------------------------------------------------------------------- /assets/a.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TianyiShi2001/audiotags/HEAD/assets/a.wav -------------------------------------------------------------------------------- /assets/a.flac: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TianyiShi2001/audiotags/HEAD/assets/a.flac -------------------------------------------------------------------------------- /src/components.rs: -------------------------------------------------------------------------------- 1 | mod id3_tag; 2 | pub use id3_tag::Id3v2Tag; 3 | mod flac_tag; 4 | mod mp4_tag; 5 | pub use flac_tag::FlacTag; 6 | pub use mp4_tag::Mp4Tag; 7 | -------------------------------------------------------------------------------- /audiotags-macro/.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | /target/ 4 | 5 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries 6 | # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html 7 | Cargo.lock 8 | 9 | # These are backup files generated by rustfmt 10 | **/*.rs.bk 11 | 12 | -------------------------------------------------------------------------------- /audiotags-macro/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "audiotags-macro" 3 | version = "0.2.0" 4 | authors = ["Tianyi "] 5 | edition = "2021" 6 | description = "macros used during the development of audiotags" 7 | license = "MIT" 8 | repository = "https://github.com/TianyiShi2001/audiotags" 9 | 10 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 11 | 12 | [dependencies] 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | /target/ 4 | 5 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries 6 | # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html 7 | Cargo.lock 8 | 9 | # These are backup files generated by rustfmt 10 | **/*.rs.bk 11 | 12 | ./*.mp3 13 | ./*.m4a 14 | ./*.flac 15 | src/main.rs 16 | .lib1.rs 17 | 18 | 19 | rtfm/.Rproj.user 20 | rtfm/.Rhistory 21 | rtfm/.RData 22 | rtfm/.Ruserdata 23 | .Rproj.user 24 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "audiotags" 3 | version = "0.5.0" 4 | authors = ["Tianyi ", "Pierre de la Martinière "] 5 | edition = "2021" 6 | description = "Unified IO for different types of audio metadata" 7 | license = "MIT" 8 | repository = "https://github.com/TianyiShi2001/audiotags" 9 | keywords = ["id3", "tag", "tags", "audio", "audiotags"] 10 | 11 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 12 | 13 | [dependencies] 14 | id3 = "1.10.0" 15 | mp4ameta = "0.11.0" 16 | metaflac = "0.2.5" 17 | thiserror = "1.0.50" 18 | audiotags-macro = { version = "0.2", path = "./audiotags-macro" } 19 | 20 | [dev-dependencies] 21 | tempfile = "3.8.1" 22 | 23 | [build-dependencies] 24 | readme-rustdocifier = "0.1.1" 25 | 26 | [features] 27 | default = ['from'] 28 | from = [] 29 | -------------------------------------------------------------------------------- /src/config.rs: -------------------------------------------------------------------------------- 1 | #[derive(Clone, Copy)] 2 | pub struct Config { 3 | /// The separator used when parsing and formatting multiple artists in metadata formats that does not explicitly support 4 | /// multiple artists (i.e. artist is a single string separated by the separator) 5 | pub sep_artist: &'static str, 6 | /// Parse multiple artists from a single string using the separator specified above 7 | pub parse_multiple_artists: bool, 8 | } 9 | 10 | impl Default for Config { 11 | fn default() -> Self { 12 | Self { 13 | sep_artist: ";", 14 | parse_multiple_artists: true, 15 | } 16 | } 17 | } 18 | impl Config { 19 | pub fn sep_artist(mut self, sep: &'static str) -> Self { 20 | self.sep_artist = sep; 21 | self 22 | } 23 | pub fn parse_multiple_artists(mut self, parse_multiple_artists: bool) -> Self { 24 | self.parse_multiple_artists = parse_multiple_artists; 25 | self 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/error.rs: -------------------------------------------------------------------------------- 1 | /// Error that could occur in this library. 2 | #[derive(thiserror::Error, Debug)] 3 | pub enum Error { 4 | /// Fail to guess the metadata format based on the file extension. 5 | #[error("Fail to guess the metadata format based on the file extension.")] 6 | UnknownFileExtension(String), 7 | 8 | /// Represents a failure to read from input. 9 | #[error("Read error")] 10 | ReadError { source: std::io::Error }, 11 | 12 | /// Represents all other cases of `std::io::Error`. 13 | #[error(transparent)] 14 | IOError(#[from] std::io::Error), 15 | 16 | #[error("Unsupported format: {0}")] 17 | UnsupportedFormat(String), 18 | #[error("Unsupported mime type: {0}")] 19 | UnsupportedMimeType(String), 20 | #[error("")] 21 | NotAPicture, 22 | 23 | #[error(transparent)] 24 | FlacTagError(#[from] metaflac::Error), 25 | 26 | #[error(transparent)] 27 | Mp4TagError(#[from] mp4ameta::Error), 28 | 29 | #[error(transparent)] 30 | Id3TagError(#[from] id3::Error), 31 | } 32 | 33 | pub type Result = std::result::Result; 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Tianyi Shi 4 | Copyright (c) 2022 Pierre de la Martinière 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | on: [push, pull_request] 2 | 3 | name: Rust 4 | 5 | jobs: 6 | check: 7 | name: Check 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v2 11 | - uses: actions-rs/toolchain@v1 12 | with: 13 | profile: minimal 14 | toolchain: stable 15 | override: true 16 | - uses: actions-rs/cargo@v1 17 | with: 18 | command: check 19 | 20 | test: 21 | name: Test Suite 22 | runs-on: ubuntu-latest 23 | steps: 24 | - uses: actions/checkout@v2 25 | - uses: actions-rs/toolchain@v1 26 | with: 27 | profile: minimal 28 | toolchain: stable 29 | override: true 30 | - uses: actions-rs/cargo@v1 31 | with: 32 | command: test 33 | 34 | fmt: 35 | name: Rustfmt 36 | runs-on: ubuntu-latest 37 | steps: 38 | - uses: actions/checkout@v2 39 | - uses: actions-rs/toolchain@v1 40 | with: 41 | profile: minimal 42 | toolchain: stable 43 | override: true 44 | - run: rustup component add rustfmt 45 | - uses: actions-rs/cargo@v1 46 | with: 47 | command: fmt 48 | args: --all -- --check 49 | 50 | clippy: 51 | name: Clippy 52 | runs-on: ubuntu-latest 53 | steps: 54 | - uses: actions/checkout@v2 55 | - uses: actions-rs/toolchain@v1 56 | with: 57 | profile: minimal 58 | toolchain: stable 59 | override: true 60 | - run: rustup component add clippy 61 | - uses: actions-rs/cargo@v1 62 | with: 63 | command: clippy 64 | args: -- -D warnings 65 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## [0.5.0] 2023-11-22 2 | 3 | - Added support for getting/setting comment - PR: #27 4 | - Added full date capabilities - PR: #34 5 | - Fixed `Id3v2Tag::{*year}` methods - PR: #21 fix #9 6 | - Fixed incorrect access to track fields on disc getters - PR: #26 7 | - Fixed album artist getting copied to artist field - PR: #29 8 | - Fixed incorrect option handling for conversion of `ID3v2Tag` to `AnyTag` - PR: #37 fix #36 9 | - Changed `Tag::read_from_path` return type to `Result>` - PR: #21 fix #8 10 | - Removed `unwrap` in `Tag::read_from_path` - PR: #21 fix #7 11 | - Removed needless borrowed reference when getting `Picture` - PR: #28 12 | 13 | * Thanks to @Serial-ATA, @cdown, @microtonez, @aybruh00, and @BSteffaniak 14 | 15 | ## [0.4.1] 2022-08-02 16 | 17 | - Add AudioTagEdit::{set_,remove_}composer - PR: #19 fix #4 18 | 19 | * Thanks to @Serial-ATA and @ChousX 20 | 21 | ## [0.4.0] 2022-08-01 22 | 23 | - Merged audiotags2 to audiotags - Party! - PR: #18 24 | 25 | * Thanks to @martpie 26 | 27 | ## [0.3.1] 2022-05-25 28 | 29 | - Upgraded `id3` from 1.0.3 to 1.1.0 30 | 31 | ## [0.3.0] 2022-05-25 32 | 33 | - Added support for `duration` 34 | - Added support for `genre` 35 | - Upgraded `id3` from 0.5.1 to 1.0.3 36 | - Upgrade `mp4ameta` from 0.6 to 0.11 37 | - Execute tests from tmp directory to avoid repo corruption 38 | 39 | ## [0.2.7182] 2020-10-29 40 | 41 | - Improve docs 42 | - Ergonomic conversions 43 | 44 | ## [0.2.718] 2020-10-27 45 | 46 | - downcasting 47 | 48 | ## [0.2.71] 2020-10-27 49 | 50 | - Remove use of `Cow` 51 | 52 | ## [0.2.5] 2020-10-27 53 | 54 | - Naive implementation of config 55 | 56 | ## [0.2.3] 2020-10-27 57 | 58 | - multiple artists 59 | 60 | ## [0.2.2] 2020-10-27 61 | 62 | - Conversion between tag types without macro; removed the macro introduced in v0.2.0 63 | 64 | ## [0.2.1] 2020-10-27 65 | 66 | - Improved error handling 67 | 68 | ## [0.2.0] 2020-10-26 69 | 70 | - conversion between tag types (naive and unstable implementation) 71 | -------------------------------------------------------------------------------- /tests/inner.rs: -------------------------------------------------------------------------------- 1 | use audiotags::*; 2 | use id3::TagLike; 3 | use std::fs; 4 | use tempfile::Builder; 5 | 6 | #[test] 7 | fn test_inner() { 8 | let tmp = Builder::new().suffix(".mp3").tempfile().unwrap(); 9 | fs::copy("assets/a.mp3", &tmp).unwrap(); 10 | 11 | let tmp_path = tmp.path(); 12 | 13 | let mut innertag = metaflac::Tag::default(); 14 | let title = "title from metaflac::Tag"; 15 | let artist = "Billy Foo"; 16 | let album_artist = "Billy Foo & The Bars"; 17 | innertag.vorbis_comments_mut().set_title(vec![title]); 18 | innertag.vorbis_comments_mut().set_artist(vec![artist]); 19 | innertag 20 | .vorbis_comments_mut() 21 | .set_album_artist(vec![album_artist]); 22 | 23 | let tag: FlacTag = innertag.into(); 24 | let mut id3tag = tag.to_dyn_tag(TagType::Id3v2); 25 | 26 | id3tag 27 | .write_to_path(tmp_path.to_str().unwrap()) 28 | .expect("Fail to write!"); 29 | 30 | let id3tag_reload = Tag::default() 31 | .read_from_path(tmp_path) 32 | .expect("Fail to read!"); 33 | 34 | assert_eq!(id3tag_reload.title(), Some(title)); 35 | assert_eq!(id3tag_reload.artist(), Some(artist)); 36 | assert_eq!(id3tag_reload.album_artist(), Some(album_artist)); 37 | 38 | // let id3tag: Id3v2Tag = id3tag_reload.into(); 39 | let mut id3tag_inner: id3::Tag = id3tag_reload.into(); 40 | let timestamp = id3::Timestamp { 41 | year: 2013, 42 | month: Some(2u8), 43 | day: Some(5u8), 44 | hour: Some(6u8), 45 | minute: None, 46 | second: None, 47 | }; 48 | 49 | id3tag_inner.set_date_recorded(timestamp); 50 | id3tag_inner 51 | .write_to_path(tmp_path, id3::Version::Id3v24) 52 | .expect("Fail to write!"); 53 | 54 | let id3tag_reload = id3::Tag::read_from_path(tmp_path).expect("Fail to read!"); 55 | assert_eq!(id3tag_reload.date_recorded(), Some(timestamp)); 56 | assert_eq!(id3tag_reload.artist(), Some(artist)); 57 | assert_eq!(id3tag_reload.album_artist(), Some(album_artist)); 58 | } 59 | -------------------------------------------------------------------------------- /src/anytag.rs: -------------------------------------------------------------------------------- 1 | use crate::*; 2 | use id3::Timestamp; 3 | 4 | #[derive(Default)] 5 | pub struct AnyTag<'a> { 6 | pub config: Config, 7 | pub title: Option<&'a str>, 8 | pub artists: Option>, 9 | pub date: Option, 10 | pub year: Option, 11 | pub duration: Option, 12 | pub album_title: Option<&'a str>, 13 | pub album_artists: Option>, 14 | pub album_cover: Option>, 15 | pub track_number: Option, 16 | pub total_tracks: Option, 17 | pub disc_number: Option, 18 | pub total_discs: Option, 19 | pub genre: Option<&'a str>, 20 | pub composer: Option<&'a str>, 21 | pub comment: Option<&'a str>, 22 | } 23 | 24 | impl AudioTagConfig for AnyTag<'_> { 25 | fn config(&self) -> &Config { 26 | &self.config 27 | } 28 | fn set_config(&mut self, config: Config) { 29 | self.config = config; 30 | } 31 | } 32 | 33 | impl<'a> AnyTag<'a> { 34 | pub fn title(&self) -> Option<&str> { 35 | self.title 36 | } 37 | pub fn set_title(&mut self, title: &'a str) { 38 | self.title = Some(title); 39 | } 40 | pub fn artists(&self) -> Option<&[&str]> { 41 | self.artists.as_deref() 42 | } 43 | // set_artists; add_artist 44 | pub fn date(&self) -> Option { 45 | self.date 46 | } 47 | pub fn set_date(&mut self, date: Timestamp) { 48 | self.date = Some(date); 49 | } 50 | pub fn year(&self) -> Option { 51 | self.year 52 | } 53 | pub fn set_year(&mut self, year: i32) { 54 | self.year = Some(year); 55 | } 56 | pub fn duration(&self) -> Option { 57 | self.duration 58 | } 59 | pub fn album_title(&self) -> Option<&str> { 60 | self.album_title 61 | } 62 | pub fn album_artists(&self) -> Option<&[&str]> { 63 | self.album_artists.as_deref() 64 | } 65 | pub fn track_number(&self) -> Option { 66 | self.track_number 67 | } 68 | pub fn total_tracks(&self) -> Option { 69 | self.total_tracks 70 | } 71 | pub fn disc_number(&self) -> Option { 72 | self.disc_number 73 | } 74 | pub fn total_discs(&self) -> Option { 75 | self.total_discs 76 | } 77 | pub fn genre(&self) -> Option<&str> { 78 | self.genre 79 | } 80 | pub fn composer(&self) -> Option<&str> { 81 | self.composer 82 | } 83 | pub fn comment(&self) -> Option<&str> { 84 | self.comment 85 | } 86 | } 87 | 88 | impl AnyTag<'_> { 89 | pub fn artists_as_string(&self) -> Option { 90 | self.artists() 91 | .map(|artists| artists.join(self.config.sep_artist)) 92 | } 93 | pub fn album_artists_as_string(&self) -> Option { 94 | self.album_artists() 95 | .map(|artists| artists.join(self.config.sep_artist)) 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/types.rs: -------------------------------------------------------------------------------- 1 | pub use super::*; 2 | 3 | #[derive(Debug, Clone, Copy, Eq, PartialEq)] 4 | pub enum MimeType { 5 | Png, 6 | Jpeg, 7 | Tiff, 8 | Bmp, 9 | Gif, 10 | } 11 | 12 | impl TryFrom<&str> for MimeType { 13 | type Error = crate::Error; 14 | fn try_from(inp: &str) -> crate::Result { 15 | Ok(match inp { 16 | "image/jpeg" => MimeType::Jpeg, 17 | "image/png" => MimeType::Png, 18 | "image/tiff" => MimeType::Tiff, 19 | "image/bmp" => MimeType::Bmp, 20 | "image/gif" => MimeType::Gif, 21 | _ => return Err(crate::Error::UnsupportedMimeType(inp.to_owned())), 22 | }) 23 | } 24 | } 25 | 26 | impl From for &'static str { 27 | fn from(mt: MimeType) -> Self { 28 | match mt { 29 | MimeType::Jpeg => "image/jpeg", 30 | MimeType::Png => "image/png", 31 | MimeType::Tiff => "image/tiff", 32 | MimeType::Bmp => "image/bmp", 33 | MimeType::Gif => "image/gif", 34 | } 35 | } 36 | } 37 | 38 | impl From for String { 39 | fn from(mt: MimeType) -> Self { 40 | >::into(mt).to_owned() 41 | } 42 | } 43 | 44 | #[derive(Debug, Clone, Eq, PartialEq)] 45 | pub struct Picture<'a> { 46 | pub data: &'a [u8], 47 | pub mime_type: MimeType, 48 | } 49 | 50 | impl<'a> Picture<'a> { 51 | pub fn new(data: &'a [u8], mime_type: MimeType) -> Self { 52 | Self { data, mime_type } 53 | } 54 | } 55 | 56 | /// A struct for representing an album for convenience. 57 | #[derive(Debug)] 58 | pub struct Album<'a> { 59 | pub title: &'a str, 60 | pub artist: Option<&'a str>, 61 | pub cover: Option>, 62 | } 63 | 64 | impl<'a> Album<'a> { 65 | pub fn with_title(title: &'a str) -> Self { 66 | Self { 67 | title, 68 | artist: None, 69 | cover: None, 70 | } 71 | } 72 | pub fn and_artist(mut self, artist: &'a str) -> Self { 73 | self.artist = Some(artist); 74 | self 75 | } 76 | pub fn and_cover(mut self, cover: Picture<'a>) -> Self { 77 | self.cover = Some(cover); 78 | self 79 | } 80 | pub fn with_all(title: &'a str, artist: &'a str, cover: Picture<'a>) -> Self { 81 | Self { 82 | title, 83 | artist: Some(artist), 84 | cover: Some(cover), 85 | } 86 | } 87 | } 88 | 89 | // #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] 90 | // pub enum PictureType { 91 | // Other, 92 | // Icon, 93 | // OtherIcon, 94 | // CoverFront, 95 | // CoverBack, 96 | // Leaflet, 97 | // Media, 98 | // LeadArtist, 99 | // Artist, 100 | // Conductor, 101 | // Band, 102 | // Composer, 103 | // Lyricist, 104 | // RecordingLocation, 105 | // DuringRecording, 106 | // DuringPerformance, 107 | // ScreenCapture, 108 | // BrightFish, 109 | // Illustration, 110 | // BandLogo, 111 | // PublisherLogo, 112 | // Undefined(u8), 113 | // } 114 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # audiotags 2 | 3 | [![Crate](https://img.shields.io/crates/v/audiotags.svg)](https://crates.io/crates/audiotags) 4 | [![Crate](https://img.shields.io/crates/d/audiotags.svg)](https://crates.io/crates/audiotags) 5 | [![Crate](https://img.shields.io/crates/l/audiotags.svg)](https://crates.io/crates/audiotags) 6 | [![Documentation](https://docs.rs/audiotags/badge.svg)](https://docs.rs/audiotags/) 7 | 8 | This crate makes it easier to parse, convert and write metadata (a.k.a. tag) in audio files of different file types. 9 | 10 | This crate aims to provide a unified trait for parsers and writers of different audio file formats. 11 | This means that you can parse tags in mp3, flac, and m4a files with a single function: `Tag::default(). 12 | read_from_path()` and get fields by directly calling `.album()`, `.artist()` on its result. Without this 13 | crate, you would otherwise need to learn different APIs in **id3**, **mp4ameta** etc. in order to parse 14 | metadata in different file formats. 15 | 16 | ### Performance 17 | 18 | Using **audiotags** incurs a little overhead due to vtables if you want to guess the metadata format 19 | (from file extension). Apart from this the performance is almost the same as directly calling function 20 | provided by those 'specialized' crates. 21 | 22 | No copies will be made if you only need to read and write metadata of one format. If you want to convert 23 | between tags, copying is unavoidable no matter if you use **audiotags** or use getters and setters provided 24 | by specialized libraries. **audiotags** is not making additional unnecessary copies. 25 | 26 | ### Supported Formats 27 | 28 | | File Format | Metadata Format | backend | 29 | |---------------|-----------------------|-------------------------------------------------------------| 30 | | `mp3` | id3v2.4 | [**id3**](https://github.com/polyfloyd/rust-id3) | 31 | | `m4a/mp4/...` | MPEG-4 audio metadata | [**mp4ameta**](https://github.com/Saecki/rust-mp4ameta) | 32 | | `flac` | Vorbis comment | [**metaflac**](https://github.com/jameshurst/rust-metaflac) | 33 | 34 | ### Examples 35 | 36 | Read the [manual](https://docs.rs/audiotags) for some examples, but here's a quick-one: 37 | 38 | ```rust 39 | fn main() { 40 | // using `default()` or `new()` alone so that the metadata format is 41 | // guessed (from the file extension) (in this case, Id3v2 tag is read) 42 | let mut tag = Tag::new().read_from_path(MP3_FILE).unwrap(); 43 | 44 | tag.set_title("foo title"); 45 | assert_eq!(tag.title(), Some("foo title")); 46 | tag.remove_title(); 47 | assert!(tag.title().is_none()); 48 | tag.remove_title(); 49 | // trying to remove a field that's already empty won't hurt 50 | 51 | let cover = Picture { 52 | mime_type: MimeType::Jpeg, 53 | data: &vec![0u8; 10], 54 | }; 55 | 56 | tag.set_album_cover(cover.clone()); 57 | assert_eq!(tag.album_cover(), Some(cover)); 58 | tag.remove_album_cover(); 59 | assert!(tag.album_cover().is_none()); 60 | tag.remove_album_cover(); 61 | 62 | tag.write_to_path(MP3_FILE).expect("Fail to save"); 63 | } 64 | ``` 65 | 66 | License: MIT 67 | -------------------------------------------------------------------------------- /audiotags-macro/src/lib.rs: -------------------------------------------------------------------------------- 1 | #[macro_export] 2 | macro_rules! impl_audiotag_config { 3 | ($tag:ident) => { 4 | impl AudioTagConfig for $tag { 5 | fn config(&self) -> &Config { 6 | &self.config 7 | } 8 | fn set_config(&mut self, config: Config) { 9 | self.config = config.clone(); 10 | } 11 | } 12 | }; 13 | } 14 | 15 | #[macro_export] 16 | macro_rules! impl_tag { 17 | ($tag:ident , $inner:ident, $tag_type:expr) => { 18 | #[derive(Default)] 19 | pub struct $tag { 20 | inner: $inner, 21 | config: Config, 22 | } 23 | impl $tag { 24 | pub fn new() -> Self { 25 | Self::default() 26 | } 27 | pub fn read_from_path(path: impl AsRef) -> crate::Result { 28 | Ok(Self { 29 | inner: $inner::read_from_path(path)?, 30 | config: Config::default(), 31 | }) 32 | } 33 | } 34 | impl_audiotag_config!($tag); 35 | 36 | use std::any::Any; 37 | 38 | impl ToAnyTag for $tag { 39 | fn to_anytag(&self) -> AnyTag<'_> { 40 | self.into() 41 | } 42 | } 43 | 44 | impl ToAny for $tag { 45 | fn to_any(&self) -> &dyn Any { 46 | self 47 | } 48 | fn to_any_mut(&mut self) -> &mut dyn Any { 49 | self 50 | } 51 | } 52 | 53 | impl AudioTag for $tag {} 54 | 55 | // From wrapper to inner (same type) 56 | impl From<$tag> for $inner { 57 | fn from(inp: $tag) -> Self { 58 | inp.inner 59 | } 60 | } 61 | 62 | // From inner to wrapper (same type) 63 | impl From<$inner> for $tag { 64 | fn from(inp: $inner) -> Self { 65 | Self { 66 | inner: inp, 67 | config: Config::default(), 68 | } 69 | } 70 | } 71 | 72 | // From dyn AudioTag to wrapper (any type) 73 | impl From> for $tag { 74 | fn from(inp: Box) -> Self { 75 | let mut inp = inp; 76 | if let Some(t_refmut) = inp.to_any_mut().downcast_mut::<$tag>() { 77 | let t = std::mem::replace(t_refmut, $tag::new()); // TODO: can we avoid creating the dummy tag? 78 | t 79 | } else { 80 | let mut t = inp.to_dyn_tag($tag_type); 81 | let t_refmut = t.to_any_mut().downcast_mut::<$tag>().unwrap(); 82 | let t = std::mem::replace(t_refmut, $tag::new()); 83 | t 84 | } 85 | } 86 | } 87 | // From dyn AudioTag to inner (any type) 88 | impl std::convert::From> for $inner { 89 | fn from(inp: Box) -> Self { 90 | let t: $tag = inp.into(); 91 | t.into() 92 | } 93 | } 94 | }; 95 | } 96 | -------------------------------------------------------------------------------- /tests/io.rs: -------------------------------------------------------------------------------- 1 | use audiotags::{MimeType, Picture, Tag}; 2 | use id3::Timestamp; 3 | use std::ffi::OsString; 4 | use std::fs; 5 | use std::path::Path; 6 | use std::str::FromStr; 7 | use tempfile::Builder; 8 | 9 | macro_rules! test_file { 10 | ( $function:ident, $file:expr ) => { 11 | #[test] 12 | fn $function() { 13 | let path = Path::new($file); 14 | let mut suffix = OsString::from("."); 15 | suffix.push(path.extension().unwrap()); 16 | let tmp = Builder::new().suffix(&suffix).tempfile().unwrap(); 17 | fs::copy($file, &tmp).unwrap(); 18 | let tmp_path = tmp.path(); 19 | 20 | let mut tags = Tag::default().read_from_path(tmp_path).unwrap(); 21 | tags.set_title("foo title"); 22 | assert_eq!(tags.title(), Some("foo title")); 23 | tags.remove_title(); 24 | assert!(tags.title().is_none()); 25 | tags.remove_title(); // should not panic 26 | 27 | tags.set_artist("foo artist"); 28 | assert_eq!(tags.artist(), Some("foo artist")); 29 | tags.remove_artist(); 30 | assert!(tags.artist().is_none()); 31 | tags.remove_artist(); 32 | 33 | tags.set_date(Timestamp::from_str("2020-05-22").unwrap()); 34 | assert_eq!( 35 | tags.date(), 36 | Some(Timestamp::from_str("2020-05-22").unwrap()) 37 | ); 38 | tags.remove_date(); 39 | assert!(tags.date().is_none()); 40 | tags.remove_date(); 41 | 42 | tags.set_year(2020); 43 | assert_eq!(tags.year(), Some(2020)); 44 | tags.remove_year(); 45 | assert!(tags.year().is_none()); 46 | tags.remove_year(); 47 | 48 | tags.set_album_title("foo album title"); 49 | assert_eq!(tags.album_title(), Some("foo album title")); 50 | tags.remove_album_title(); 51 | assert!(tags.album_title().is_none()); 52 | tags.remove_album_title(); 53 | 54 | tags.set_album_artist("foo album artist"); 55 | assert_eq!(tags.album_artist(), Some("foo album artist")); 56 | tags.remove_album_artist(); 57 | assert!(tags.album_artist().is_none()); 58 | tags.remove_album_artist(); 59 | 60 | let cover = Picture { 61 | mime_type: MimeType::Jpeg, 62 | data: &[0u8; 10], 63 | }; 64 | 65 | tags.set_album_cover(cover.clone()); 66 | assert_eq!(tags.album_cover(), Some(cover)); 67 | tags.remove_album_cover(); 68 | assert!(tags.album_cover().is_none()); 69 | tags.remove_album_cover(); 70 | 71 | tags.set_genre("foo song genre"); 72 | assert_eq!(tags.genre(), Some("foo song genre")); 73 | tags.remove_genre(); 74 | assert!(tags.genre().is_none()); 75 | tags.remove_genre(); 76 | 77 | tags.set_comment("foo song comment".to_string()); 78 | assert_eq!(tags.comment(), Some("foo song comment")); 79 | tags.remove_comment(); 80 | assert!(tags.comment().is_none()); 81 | tags.remove_comment(); 82 | } 83 | }; 84 | } 85 | 86 | test_file!(test_mp3, "assets/a.mp3"); 87 | test_file!(test_m4a, "assets/a.m4a"); 88 | test_file!(test_flac, "assets/a.flac"); 89 | -------------------------------------------------------------------------------- /src/traits.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | use id3::Timestamp; 3 | 4 | pub trait AudioTag: AudioTagEdit + AudioTagWrite + ToAnyTag {} 5 | 6 | // pub trait TagIo { 7 | // fn read_from_path(path: &str) -> crate::Result; 8 | // fn write_to_path(path: &str) -> crate::Result<()>; 9 | // } 10 | 11 | /// Implementors of this trait are able to read and write audio metadata. 12 | /// 13 | /// Constructor methods e.g. `from_file` should be implemented separately. 14 | pub trait AudioTagEdit: AudioTagConfig { 15 | fn title(&self) -> Option<&str>; 16 | fn set_title(&mut self, title: &str); 17 | fn remove_title(&mut self); 18 | 19 | fn artist(&self) -> Option<&str>; 20 | fn set_artist(&mut self, artist: &str); 21 | fn remove_artist(&mut self); 22 | 23 | fn artists(&self) -> Option> { 24 | if self.config().parse_multiple_artists { 25 | self.artist() 26 | .map(|a| a.split(self.config().sep_artist).collect::>()) 27 | } else { 28 | self.artist().map(|v| vec![v]) 29 | } 30 | } 31 | fn add_artist(&mut self, artist: &str) { 32 | self.set_artist(artist); 33 | } 34 | 35 | fn date(&self) -> Option; 36 | fn set_date(&mut self, date: Timestamp); 37 | fn remove_date(&mut self); 38 | 39 | fn year(&self) -> Option; 40 | fn set_year(&mut self, year: i32); 41 | fn remove_year(&mut self); 42 | 43 | fn duration(&self) -> Option; 44 | 45 | fn album(&self) -> Option> { 46 | self.album_title().map(|title| Album { 47 | title, 48 | artist: self.album_artist(), 49 | cover: self.album_cover(), 50 | }) 51 | } 52 | fn set_album(&mut self, album: Album) { 53 | self.set_album_title(album.title); 54 | if let Some(artist) = album.artist { 55 | self.set_album_artist(artist) 56 | } else { 57 | self.remove_album_artist() 58 | } 59 | if let Some(pic) = album.cover { 60 | self.set_album_cover(pic) 61 | } else { 62 | self.remove_album_cover() 63 | } 64 | } 65 | fn remove_album(&mut self) { 66 | self.remove_album_title(); 67 | self.remove_album_artist(); 68 | self.remove_album_cover(); 69 | } 70 | 71 | fn album_title(&self) -> Option<&str>; 72 | fn set_album_title(&mut self, v: &str); 73 | fn remove_album_title(&mut self); 74 | 75 | fn album_artist(&self) -> Option<&str>; 76 | fn set_album_artist(&mut self, v: &str); 77 | fn remove_album_artist(&mut self); 78 | 79 | fn album_artists(&self) -> Option> { 80 | if self.config().parse_multiple_artists { 81 | self.album_artist() 82 | .map(|a| a.split(self.config().sep_artist).collect::>()) 83 | } else { 84 | self.album_artist().map(|v| vec![v]) 85 | } 86 | } 87 | fn add_album_artist(&mut self, artist: &str) { 88 | self.set_album_artist(artist); 89 | } 90 | 91 | fn album_cover(&self) -> Option; 92 | fn set_album_cover(&mut self, cover: Picture); 93 | fn remove_album_cover(&mut self); 94 | 95 | fn composer(&self) -> Option<&str>; 96 | fn set_composer(&mut self, composer: String); 97 | fn remove_composer(&mut self); 98 | 99 | fn track(&self) -> (Option, Option) { 100 | (self.track_number(), self.total_tracks()) 101 | } 102 | fn set_track(&mut self, track: (u16, u16)) { 103 | self.set_track_number(track.0); 104 | self.set_total_tracks(track.1); 105 | } 106 | fn remove_track(&mut self) { 107 | self.remove_track_number(); 108 | self.remove_total_tracks(); 109 | } 110 | 111 | fn track_number(&self) -> Option; 112 | fn set_track_number(&mut self, track_number: u16); 113 | fn remove_track_number(&mut self); 114 | 115 | fn total_tracks(&self) -> Option; 116 | fn set_total_tracks(&mut self, total_track: u16); 117 | fn remove_total_tracks(&mut self); 118 | 119 | fn disc(&self) -> (Option, Option) { 120 | (self.disc_number(), self.total_discs()) 121 | } 122 | fn set_disc(&mut self, disc: (u16, u16)) { 123 | self.set_disc_number(disc.0); 124 | self.set_total_discs(disc.1); 125 | } 126 | fn remove_disc(&mut self) { 127 | self.remove_disc_number(); 128 | self.remove_total_discs(); 129 | } 130 | 131 | fn disc_number(&self) -> Option; 132 | fn set_disc_number(&mut self, disc_number: u16); 133 | fn remove_disc_number(&mut self); 134 | 135 | fn total_discs(&self) -> Option; 136 | fn set_total_discs(&mut self, total_discs: u16); 137 | fn remove_total_discs(&mut self); 138 | 139 | fn genre(&self) -> Option<&str>; 140 | fn set_genre(&mut self, genre: &str); 141 | fn remove_genre(&mut self); 142 | 143 | fn comment(&self) -> Option<&str>; 144 | fn set_comment(&mut self, genre: String); 145 | fn remove_comment(&mut self); 146 | } 147 | 148 | pub trait AudioTagWrite { 149 | fn write_to(&mut self, file: &mut File) -> crate::Result<()>; 150 | // cannot use impl AsRef 151 | fn write_to_path(&mut self, path: &str) -> crate::Result<()>; 152 | } 153 | 154 | pub trait AudioTagConfig { 155 | fn config(&self) -> &Config; 156 | fn set_config(&mut self, config: Config); 157 | } 158 | 159 | pub trait ToAnyTag: ToAny { 160 | fn to_anytag(&self) -> AnyTag<'_>; 161 | 162 | /// Convert the tag type, which can be lossy. 163 | fn to_dyn_tag(&self, tag_type: TagType) -> Box { 164 | // TODO: write a macro or something that implement this method for every tag type so that if the 165 | // TODO: target type is the same, just return self 166 | match tag_type { 167 | TagType::Id3v2 => Box::new(Id3v2Tag::from(self.to_anytag())), 168 | TagType::Mp4 => Box::new(Mp4Tag::from(self.to_anytag())), 169 | TagType::Flac => Box::new(FlacTag::from(self.to_anytag())), 170 | } 171 | } 172 | } 173 | 174 | pub trait ToAny { 175 | fn to_any(&self) -> &dyn std::any::Any; 176 | fn to_any_mut(&mut self) -> &mut dyn std::any::Any; 177 | } 178 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! [![Crate](https://img.shields.io/crates/v/audiotags.svg)](https://crates.io/crates/audiotags) 2 | //! [![Crate](https://img.shields.io/crates/d/audiotags.svg)](https://crates.io/crates/audiotags) 3 | //! [![Crate](https://img.shields.io/crates/l/audiotags.svg)](https://crates.io/crates/audiotags) 4 | //! [![Documentation](https://docs.rs/audiotags/badge.svg)](https://docs.rs/audiotags/) 5 | //! 6 | //! This crate makes it easier to parse, convert and write metadata (a.k.a tag) in audio files of different file types. 7 | //! 8 | //! This crate aims to provide a unified trait for parsers and writers of different audio file formats. 9 | //! This means that you can parse tags in mp3, flac, and m4a files with a single function: `Tag::default(). 10 | //! read_from_path()` and get fields by directly calling `.album()`, `.artist()` on its result. Without this 11 | //! crate, you would otherwise need to learn different APIs in **id3**, **mp4ameta** etc. in order to parse 12 | //! metadata in different file formats. 13 | //! 14 | //! ## Performance 15 | //! 16 | //! Using **audiotags** incurs a little overhead due to vtables if you want to guess the metadata format 17 | //! (from file extension). Apart from this the performance is almost the same as directly calling function 18 | //! provided by those 'specialized' crates. 19 | //! 20 | //! No copies will be made if you only need to read and write metadata of one format. If you want to convert 21 | //! between tags, copying is unavoidable no matter if you use **audiotags** or use getters and setters provided 22 | //! by specialized libraries. **audiotags** is not making additional unnecessary copies. 23 | //! 24 | //! ## Supported Formats 25 | //! 26 | //! | File Format | Metadata Format | backend | 27 | //! |---------------|-----------------------|-------------------------------------------------------------| 28 | //! | `mp3` | id3v2.4 | [**id3**](https://github.com/polyfloyd/rust-id3) | 29 | //! | `m4a/mp4/...` | MPEG-4 audio metadata | [**mp4ameta**](https://github.com/Saecki/rust-mp4ameta) | 30 | //! | `flac` | Vorbis comment | [**metaflac**](https://github.com/jameshurst/rust-metaflac) | 31 | //! 32 | //! ## Examples 33 | //! 34 | //! Read the [manual](https://docs.rs/audiotags) for some examples, but here's a quick-one: 35 | //! 36 | //! ```rust,no_run 37 | //! use audiotags::{Tag, Picture, MimeType}; 38 | //! 39 | //! // using `default()` or `new()` alone so that the metadata format is 40 | //! // guessed (from the file extension) (in this case, Id3v2 tag is read) 41 | //! let mut tag = Tag::new().read_from_path("test.mp3").unwrap(); 42 | //! 43 | //! tag.set_title("foo title"); 44 | //! assert_eq!(tag.title(), Some("foo title")); 45 | //! tag.remove_title(); 46 | //! assert!(tag.title().is_none()); 47 | //! tag.remove_title(); 48 | //! // trying to remove a field that's already empty won't hurt 49 | //! 50 | //! let cover = Picture { 51 | //! mime_type: MimeType::Jpeg, 52 | //! data: &vec![0u8; 10], 53 | //! }; 54 | //! 55 | //! tag.set_album_cover(cover.clone()); 56 | //! assert_eq!(tag.album_cover(), Some(cover)); 57 | //! tag.remove_album_cover(); 58 | //! assert!(tag.album_cover().is_none()); 59 | //! tag.remove_album_cover(); 60 | //! 61 | //! tag.write_to_path("test.mp3").expect("Fail to save"); 62 | //! ``` 63 | 64 | pub(crate) use audiotags_macro::*; 65 | 66 | pub mod anytag; 67 | pub use anytag::*; 68 | 69 | pub mod components; 70 | pub use components::*; 71 | 72 | pub mod error; 73 | pub use error::{Error, Result}; 74 | 75 | pub mod traits; 76 | pub use traits::*; 77 | 78 | pub mod types; 79 | pub use types::*; 80 | 81 | pub mod config; 82 | pub use config::Config; 83 | 84 | use std::convert::From; 85 | use std::fs::File; 86 | use std::path::Path; 87 | 88 | pub use std::convert::{TryFrom, TryInto}; 89 | 90 | /// A builder for `Box`. If you do not want a trait object, you can use individual types. 91 | /// 92 | /// # Examples 93 | /// 94 | /// ```no_run 95 | /// use audiotags::{Tag, TagType}; 96 | /// 97 | /// # fn main() -> audiotags::Result<()> { 98 | /// // Guess the format by default 99 | /// let mut tag = Tag::new().read_from_path("assets/a.mp3").unwrap(); 100 | /// tag.set_title("Foo"); 101 | /// 102 | /// // you can convert the tag type and save the metadata to another file. 103 | /// tag.to_dyn_tag(TagType::Mp4).write_to_path("assets/a.m4a")?; 104 | /// 105 | /// // you can specify the tag type (but when you want to do this, also consider directly using the concrete type) 106 | /// let tag = Tag::new().with_tag_type(TagType::Mp4).read_from_path("assets/a.m4a").unwrap(); 107 | /// assert_eq!(tag.title(), Some("Foo")); 108 | /// # Ok(()) } 109 | /// ``` 110 | #[derive(Default)] 111 | pub struct Tag { 112 | /// The tag type which can be specified with `.with_tag_type()` before parsing. 113 | tag_type: Option, 114 | /// The config which can be specified with `.with_config()` before parsing. 115 | config: Config, 116 | } 117 | 118 | impl Tag { 119 | /// Initiate a new Tag (a builder for `Box`) with default configurations. 120 | /// You can then optionally chain `with_tag_type` and/or `with_config`. 121 | /// Finally, you `read_from_path` 122 | pub fn new() -> Self { 123 | Self::default() 124 | } 125 | /// Specify the tag type 126 | pub fn with_tag_type(self, tag_type: TagType) -> Self { 127 | Self { 128 | tag_type: Some(tag_type), 129 | config: self.config, 130 | } 131 | } 132 | /// Specify configuration, if you do not want to use the default 133 | pub fn with_config(self, config: Config) -> Self { 134 | Self { 135 | tag_type: self.tag_type, 136 | config, 137 | } 138 | } 139 | pub fn read_from_path( 140 | &self, 141 | path: impl AsRef, 142 | ) -> crate::Result> { 143 | match self.tag_type.unwrap_or(TagType::try_from_ext( 144 | path.as_ref() 145 | .extension() 146 | .ok_or(Error::UnknownFileExtension(String::new()))? 147 | .to_string_lossy() 148 | .to_string() 149 | .to_lowercase() 150 | .as_str(), 151 | )?) { 152 | TagType::Id3v2 => Ok(Box::new({ 153 | let mut t = Id3v2Tag::read_from_path(path)?; 154 | t.set_config(self.config); 155 | t 156 | })), 157 | TagType::Mp4 => Ok(Box::new({ 158 | let mut t = Mp4Tag::read_from_path(path)?; 159 | t.set_config(self.config); 160 | t 161 | })), 162 | TagType::Flac => Ok(Box::new({ 163 | let mut t = FlacTag::read_from_path(path)?; 164 | t.set_config(self.config); 165 | t 166 | })), 167 | } 168 | } 169 | } 170 | 171 | #[derive(Clone, Copy, Debug)] 172 | pub enum TagType { 173 | /// ## Common file extensions 174 | /// 175 | /// `.mp3` 176 | /// 177 | /// ## References 178 | /// 179 | /// - 180 | Id3v2, 181 | Flac, 182 | /// ## Common file extensions 183 | /// 184 | /// `.mp4, .m4a, .m4p, .m4b, .m4r and .m4v` 185 | /// 186 | /// ## References 187 | /// 188 | /// - 189 | Mp4, 190 | } 191 | 192 | #[rustfmt::skip] 193 | impl TagType { 194 | fn try_from_ext(ext: &str) -> crate::Result { 195 | match ext { 196 | "mp3" => Ok(Self::Id3v2), 197 | "m4a" | "m4b" | "m4p" | "m4v" | "isom" | "mp4" => Ok(Self::Mp4), 198 | "flac" => Ok(Self::Flac), 199 | p => Err(crate::Error::UnsupportedFormat(p.to_owned())), 200 | } 201 | } 202 | } 203 | 204 | /// Convert a concrete tag type into another 205 | #[macro_export] 206 | macro_rules! convert { 207 | ($inp:expr, $target_type:ty) => { 208 | $target_type::from(inp.to_anytag()) 209 | }; 210 | } 211 | -------------------------------------------------------------------------------- /src/components/flac_tag.rs: -------------------------------------------------------------------------------- 1 | use crate::*; 2 | use id3::Timestamp; 3 | use metaflac; 4 | use std::str::FromStr; 5 | 6 | pub use metaflac::Tag as FlacInnerTag; 7 | 8 | impl_tag!(FlacTag, FlacInnerTag, TagType::Flac); 9 | 10 | impl<'a> From> for FlacTag { 11 | fn from(inp: AnyTag<'a>) -> Self { 12 | let mut t = FlacTag::default(); 13 | if let Some(v) = inp.title() { 14 | t.set_title(v) 15 | } 16 | if let Some(v) = inp.artists_as_string() { 17 | t.set_artist(&v) 18 | } 19 | if let Some(v) = inp.date { 20 | t.set_date(v) 21 | } 22 | if let Some(v) = inp.year { 23 | t.set_year(v) 24 | } 25 | if let Some(v) = inp.album_title() { 26 | t.set_album_title(v) 27 | } 28 | if let Some(v) = inp.album_artists_as_string() { 29 | t.set_album_artist(&v) 30 | } 31 | if let Some(v) = inp.track_number() { 32 | t.set_track_number(v) 33 | } 34 | if let Some(v) = inp.total_tracks() { 35 | t.set_total_tracks(v) 36 | } 37 | if let Some(v) = inp.disc_number() { 38 | t.set_disc_number(v) 39 | } 40 | if let Some(v) = inp.total_discs() { 41 | t.set_total_discs(v) 42 | } 43 | t 44 | } 45 | } 46 | 47 | impl<'a> From<&'a FlacTag> for AnyTag<'a> { 48 | fn from(inp: &'a FlacTag) -> Self { 49 | let tag = Self { 50 | title: inp.title(), 51 | artists: inp.artists(), 52 | date: inp.date(), 53 | year: inp.year(), 54 | duration: inp.duration(), 55 | album_title: inp.album_title(), 56 | album_artists: inp.album_artists(), 57 | album_cover: inp.album_cover(), 58 | track_number: inp.track_number(), 59 | total_tracks: inp.total_tracks(), 60 | disc_number: inp.disc_number(), 61 | total_discs: inp.total_discs(), 62 | genre: inp.genre(), 63 | composer: inp.composer(), 64 | comment: inp.comment(), 65 | ..Self::default() 66 | }; 67 | 68 | tag 69 | } 70 | } 71 | 72 | impl FlacTag { 73 | pub fn get_first(&self, key: &str) -> Option<&str> { 74 | if let Some(Some(v)) = self.inner.vorbis_comments().map(|c| c.get(key)) { 75 | if !v.is_empty() { 76 | Some(v[0].as_str()) 77 | } else { 78 | None 79 | } 80 | } else { 81 | None 82 | } 83 | } 84 | pub fn set_first(&mut self, key: &str, val: &str) { 85 | self.inner.vorbis_comments_mut().set(key, vec![val]); 86 | } 87 | pub fn remove(&mut self, k: &str) { 88 | self.inner.vorbis_comments_mut().comments.remove(k); 89 | } 90 | } 91 | 92 | impl AudioTagEdit for FlacTag { 93 | fn title(&self) -> Option<&str> { 94 | self.get_first("TITLE") 95 | } 96 | fn set_title(&mut self, title: &str) { 97 | self.set_first("TITLE", title); 98 | } 99 | fn remove_title(&mut self) { 100 | self.remove("TITLE"); 101 | } 102 | 103 | fn artist(&self) -> Option<&str> { 104 | self.get_first("ARTIST") 105 | } 106 | fn set_artist(&mut self, artist: &str) { 107 | self.set_first("ARTIST", artist) 108 | } 109 | fn remove_artist(&mut self) { 110 | self.remove("ARTIST"); 111 | } 112 | 113 | fn date(&self) -> Option { 114 | if let Some(Ok(timestamp)) = self.get_first("DATE").map(Timestamp::from_str) { 115 | Some(timestamp) 116 | } else { 117 | None 118 | } 119 | } 120 | fn set_date(&mut self, date: Timestamp) { 121 | self.set_first("DATE", &date.to_string()); 122 | } 123 | fn remove_date(&mut self) { 124 | self.remove("DATE"); 125 | } 126 | 127 | fn year(&self) -> Option { 128 | if let Some(Ok(y)) = self.get_first("YEAR").map(|s| s.parse::()) { 129 | Some(y) 130 | } else if let Some(Ok(y)) = self 131 | .get_first("DATE") 132 | .map(|s| s.chars().take(4).collect::().parse::()) 133 | { 134 | Some(y) 135 | } else { 136 | None 137 | } 138 | } 139 | fn set_year(&mut self, year: i32) { 140 | self.set_first("YEAR", &year.to_string()); 141 | } 142 | fn remove_year(&mut self) { 143 | self.remove("YEAR"); 144 | self.remove("DATE"); 145 | } 146 | 147 | fn duration(&self) -> Option { 148 | self.inner 149 | .get_streaminfo() 150 | .map(|s| s.total_samples as f64 / f64::from(s.sample_rate)) 151 | } 152 | 153 | fn album_title(&self) -> Option<&str> { 154 | self.get_first("ALBUM") 155 | } 156 | fn set_album_title(&mut self, title: &str) { 157 | self.set_first("ALBUM", title) 158 | } 159 | fn remove_album_title(&mut self) { 160 | self.remove("ALBUM"); 161 | } 162 | 163 | fn album_artist(&self) -> Option<&str> { 164 | self.get_first("ALBUMARTIST") 165 | } 166 | fn set_album_artist(&mut self, v: &str) { 167 | self.set_first("ALBUMARTIST", v) 168 | } 169 | fn remove_album_artist(&mut self) { 170 | self.remove("ALBUMARTIST"); 171 | } 172 | 173 | fn album_cover(&self) -> Option { 174 | self.inner 175 | .pictures() 176 | .find(|&pic| matches!(pic.picture_type, metaflac::block::PictureType::CoverFront)) 177 | .and_then(|pic| { 178 | Some(Picture { 179 | data: &pic.data, 180 | mime_type: (pic.mime_type.as_str()).try_into().ok()?, 181 | }) 182 | }) 183 | } 184 | fn set_album_cover(&mut self, cover: Picture) { 185 | self.remove_album_cover(); 186 | let mime = String::from(cover.mime_type); 187 | let picture_type = metaflac::block::PictureType::CoverFront; 188 | self.inner 189 | .add_picture(mime, picture_type, (cover.data).to_owned()); 190 | } 191 | fn remove_album_cover(&mut self) { 192 | self.inner 193 | .remove_picture_type(metaflac::block::PictureType::CoverFront) 194 | } 195 | 196 | fn composer(&self) -> Option<&str> { 197 | self.get_first("COMPOSER") 198 | } 199 | fn set_composer(&mut self, composer: String) { 200 | self.set_first("COMPOSER", &composer); 201 | } 202 | fn remove_composer(&mut self) { 203 | self.remove("COMPOSER") 204 | } 205 | 206 | fn track_number(&self) -> Option { 207 | if let Some(Ok(n)) = self.get_first("TRACKNUMBER").map(|x| x.parse::()) { 208 | Some(n) 209 | } else { 210 | None 211 | } 212 | } 213 | fn set_track_number(&mut self, v: u16) { 214 | self.set_first("TRACKNUMBER", &v.to_string()) 215 | } 216 | fn remove_track_number(&mut self) { 217 | self.remove("TRACKNUMBER"); 218 | } 219 | 220 | // ! not standard 221 | fn total_tracks(&self) -> Option { 222 | if let Some(Ok(n)) = self.get_first("TOTALTRACKS").map(|x| x.parse::()) { 223 | Some(n) 224 | } else { 225 | None 226 | } 227 | } 228 | fn set_total_tracks(&mut self, v: u16) { 229 | self.set_first("TOTALTRACKS", &v.to_string()) 230 | } 231 | fn remove_total_tracks(&mut self) { 232 | self.remove("TOTALTRACKS"); 233 | } 234 | 235 | fn disc_number(&self) -> Option { 236 | if let Some(Ok(n)) = self.get_first("DISCNUMBER").map(|x| x.parse::()) { 237 | Some(n) 238 | } else { 239 | None 240 | } 241 | } 242 | fn set_disc_number(&mut self, v: u16) { 243 | self.set_first("DISCNUMBER", &v.to_string()) 244 | } 245 | fn remove_disc_number(&mut self) { 246 | self.remove("DISCNUMBER"); 247 | } 248 | 249 | // ! not standard 250 | fn total_discs(&self) -> Option { 251 | if let Some(Ok(n)) = self.get_first("TOTALDISCS").map(|x| x.parse::()) { 252 | Some(n) 253 | } else { 254 | None 255 | } 256 | } 257 | fn set_total_discs(&mut self, v: u16) { 258 | self.set_first("TOTALDISCS", &v.to_string()) 259 | } 260 | fn remove_total_discs(&mut self) { 261 | self.remove("TOTALDISCS"); 262 | } 263 | 264 | fn genre(&self) -> Option<&str> { 265 | self.get_first("GENRE") 266 | } 267 | fn set_genre(&mut self, v: &str) { 268 | self.set_first("GENRE", v); 269 | } 270 | fn remove_genre(&mut self) { 271 | self.remove("GENRE"); 272 | } 273 | 274 | fn comment(&self) -> Option<&str> { 275 | self.get_first("COMMENT") 276 | } 277 | fn set_comment(&mut self, v: String) { 278 | self.set_first("COMMENT", &v); 279 | } 280 | fn remove_comment(&mut self) { 281 | self.remove("COMMENT"); 282 | } 283 | } 284 | 285 | impl AudioTagWrite for FlacTag { 286 | fn write_to(&mut self, file: &mut File) -> crate::Result<()> { 287 | self.inner.write_to(file)?; 288 | Ok(()) 289 | } 290 | fn write_to_path(&mut self, path: &str) -> crate::Result<()> { 291 | self.inner.write_to_path(path)?; 292 | Ok(()) 293 | } 294 | } 295 | -------------------------------------------------------------------------------- /src/components/id3_tag.rs: -------------------------------------------------------------------------------- 1 | use crate::*; 2 | use id3::{self, Content, Frame, TagLike, Timestamp}; 3 | 4 | pub use id3::Tag as Id3v2InnerTag; 5 | 6 | impl_tag!(Id3v2Tag, Id3v2InnerTag, TagType::Id3v2); 7 | 8 | impl<'a> From<&'a Id3v2Tag> for AnyTag<'a> { 9 | fn from(inp: &'a Id3v2Tag) -> Self { 10 | Self { 11 | config: inp.config, 12 | 13 | title: inp.title(), 14 | artists: inp.artists(), 15 | date: inp.date(), 16 | year: inp.year(), 17 | duration: inp.inner.duration().map(f64::from), 18 | album_title: inp.album_title(), 19 | album_artists: inp.album_artists(), 20 | album_cover: inp.album_cover(), 21 | track_number: inp.track_number(), 22 | total_tracks: inp.total_tracks(), 23 | disc_number: inp.disc_number(), 24 | total_discs: inp.total_discs(), 25 | genre: inp.genre(), 26 | composer: inp.composer(), 27 | comment: inp.comment(), 28 | } 29 | } 30 | } 31 | 32 | impl<'a> From> for Id3v2Tag { 33 | fn from(inp: AnyTag<'a>) -> Self { 34 | Self { 35 | config: inp.config, 36 | inner: { 37 | let mut t = id3::Tag::new(); 38 | if let Some(v) = inp.title() { 39 | t.set_title(v) 40 | } 41 | if let Some(v) = inp.artists_as_string() { 42 | t.set_artist(&v) 43 | } 44 | if let Some(v) = inp.date() { 45 | t.set_date_recorded(v) 46 | } 47 | if let Some(v) = inp.year { 48 | t.set_year(v) 49 | } 50 | if let Some(v) = inp.album_title() { 51 | t.set_album(v) 52 | } 53 | if let Some(v) = inp.album_artists_as_string() { 54 | t.set_album_artist(&v) 55 | } 56 | if let Some(v) = inp.track_number() { 57 | t.set_track(v as u32) 58 | } 59 | if let Some(v) = inp.total_tracks() { 60 | t.set_total_tracks(v as u32) 61 | } 62 | if let Some(v) = inp.disc_number() { 63 | t.set_disc(v as u32) 64 | } 65 | if let Some(v) = inp.total_discs() { 66 | t.set_total_discs(v as u32) 67 | } 68 | if let Some(v) = inp.genre() { 69 | t.set_genre(v) 70 | } 71 | t 72 | }, 73 | } 74 | } 75 | } 76 | 77 | impl<'a> std::convert::TryFrom<&'a id3::frame::Picture> for Picture<'a> { 78 | type Error = crate::Error; 79 | fn try_from(inp: &'a id3::frame::Picture) -> crate::Result { 80 | let id3::frame::Picture { 81 | mime_type, data, .. 82 | } = inp; 83 | let mime_type: MimeType = mime_type.as_str().try_into()?; 84 | Ok(Self { data, mime_type }) 85 | } 86 | } 87 | 88 | impl AudioTagEdit for Id3v2Tag { 89 | fn title(&self) -> Option<&str> { 90 | self.inner.title() 91 | } 92 | fn set_title(&mut self, title: &str) { 93 | self.inner.set_title(title) 94 | } 95 | fn remove_title(&mut self) { 96 | self.inner.remove_title(); 97 | } 98 | 99 | fn artist(&self) -> Option<&str> { 100 | self.inner.artist() 101 | } 102 | fn set_artist(&mut self, artist: &str) { 103 | self.inner.set_artist(artist) 104 | } 105 | fn remove_artist(&mut self) { 106 | self.inner.remove_artist(); 107 | } 108 | 109 | fn date(&self) -> Option { 110 | self.inner.date_recorded() 111 | } 112 | fn set_date(&mut self, timestamp: Timestamp) { 113 | self.inner.set_date_recorded(timestamp) 114 | } 115 | fn remove_date(&mut self) { 116 | self.inner.remove_date_recorded() 117 | } 118 | 119 | fn year(&self) -> Option { 120 | self.inner.year() 121 | } 122 | fn set_year(&mut self, year: i32) { 123 | self.inner.set_year(year); 124 | } 125 | fn remove_year(&mut self) { 126 | self.inner.remove_date_recorded(); 127 | self.inner.remove_year(); 128 | } 129 | fn duration(&self) -> Option { 130 | self.inner.duration().map(f64::from) 131 | } 132 | 133 | fn album_title(&self) -> Option<&str> { 134 | self.inner.album() 135 | } 136 | fn set_album_title(&mut self, v: &str) { 137 | self.inner.set_album(v) 138 | } 139 | fn remove_album_title(&mut self) { 140 | self.inner.remove_album(); 141 | } 142 | 143 | fn album_artist(&self) -> Option<&str> { 144 | self.inner.album_artist() 145 | } 146 | fn set_album_artist(&mut self, v: &str) { 147 | self.inner.set_album_artist(v) 148 | } 149 | fn remove_album_artist(&mut self) { 150 | self.inner.remove_album_artist(); 151 | } 152 | 153 | fn album_cover(&self) -> Option { 154 | self.inner 155 | .pictures() 156 | .find(|&pic| matches!(pic.picture_type, id3::frame::PictureType::CoverFront)) 157 | .and_then(|pic| { 158 | Some(Picture { 159 | data: &pic.data, 160 | mime_type: (pic.mime_type.as_str()).try_into().ok()?, 161 | }) 162 | }) 163 | } 164 | fn set_album_cover(&mut self, cover: Picture) { 165 | self.remove_album_cover(); 166 | self.inner.add_frame(id3::frame::Picture { 167 | mime_type: String::from(cover.mime_type), 168 | picture_type: id3::frame::PictureType::CoverFront, 169 | description: "".to_owned(), 170 | data: cover.data.to_owned(), 171 | }); 172 | } 173 | fn remove_album_cover(&mut self) { 174 | self.inner 175 | .remove_picture_by_type(id3::frame::PictureType::CoverFront); 176 | } 177 | 178 | fn composer(&self) -> Option<&str> { 179 | if let Some(Content::Text(text)) = self.inner.get("TCOM").map(Frame::content) { 180 | return Some(text); 181 | } 182 | 183 | None 184 | } 185 | fn set_composer(&mut self, composer: String) { 186 | self.inner.add_frame(Frame::text("TCOM", composer)); 187 | } 188 | fn remove_composer(&mut self) { 189 | self.inner.remove("TCOM"); 190 | } 191 | 192 | fn track_number(&self) -> Option { 193 | self.inner.track().map(|x| x as u16) 194 | } 195 | fn set_track_number(&mut self, track: u16) { 196 | self.inner.set_track(track as u32); 197 | } 198 | fn remove_track_number(&mut self) { 199 | self.inner.remove_track(); 200 | } 201 | 202 | fn total_tracks(&self) -> Option { 203 | self.inner.total_tracks().map(|x| x as u16) 204 | } 205 | fn set_total_tracks(&mut self, total_track: u16) { 206 | self.inner.set_total_tracks(total_track as u32); 207 | } 208 | fn remove_total_tracks(&mut self) { 209 | self.inner.remove_total_tracks(); 210 | } 211 | 212 | fn disc_number(&self) -> Option { 213 | self.inner.disc().map(|x| x as u16) 214 | } 215 | fn set_disc_number(&mut self, disc_number: u16) { 216 | self.inner.set_disc(disc_number as u32) 217 | } 218 | fn remove_disc_number(&mut self) { 219 | self.inner.remove_disc(); 220 | } 221 | 222 | fn total_discs(&self) -> Option { 223 | self.inner.total_discs().map(|x| x as u16) 224 | } 225 | fn set_total_discs(&mut self, total_discs: u16) { 226 | self.inner.set_total_discs(total_discs as u32) 227 | } 228 | fn remove_total_discs(&mut self) { 229 | self.inner.remove_total_discs(); 230 | } 231 | 232 | fn genre(&self) -> Option<&str> { 233 | self.inner.genre() 234 | } 235 | fn set_genre(&mut self, v: &str) { 236 | self.inner.set_genre(v); 237 | } 238 | fn remove_genre(&mut self) { 239 | self.inner.remove_genre(); 240 | } 241 | 242 | fn comment(&self) -> Option<&str> { 243 | for comment in self.inner.comments() { 244 | if comment.description.is_empty() { 245 | return Some(comment.text.as_str()); 246 | } 247 | } 248 | None 249 | } 250 | fn set_comment(&mut self, comment: String) { 251 | self.inner.add_frame(id3::frame::Comment { 252 | lang: "XXX".to_string(), 253 | description: "".to_string(), 254 | text: comment, 255 | }); 256 | } 257 | fn remove_comment(&mut self) { 258 | self.inner.remove("COMM"); 259 | } 260 | } 261 | 262 | impl AudioTagWrite for Id3v2Tag { 263 | fn write_to(&mut self, file: &mut File) -> crate::Result<()> { 264 | self.inner.write_to(file, id3::Version::Id3v24)?; 265 | Ok(()) 266 | } 267 | fn write_to_path(&mut self, path: &str) -> crate::Result<()> { 268 | self.inner.write_to_path(path, id3::Version::Id3v24)?; 269 | Ok(()) 270 | } 271 | } 272 | 273 | // impl<'a> From> for Id3Tag { 274 | // fn from(anytag: AnyTag) -> Self { 275 | // Self { 276 | // inner: anytag.into(), 277 | // } 278 | // } 279 | // } 280 | 281 | // impl From for id3::Tag { 282 | // fn from(anytag: AnyTag) -> Self { 283 | // let mut id3tag = Self::default(); 284 | // anytag 285 | // .artists_as_string(SEP_ARTIST) 286 | // .map(|v| id3tag.set_artist(v)); 287 | // anytag.year().map(|v| id3tag.set_year(v)); 288 | // anytag.album_title().map(|v| id3tag.set_album(v)); 289 | // anytag 290 | // .album_artists_as_string(SEP_ARTIST) 291 | // .map(|v| id3tag.set_album_artist(v)); 292 | // anytag.track_number().map(|v| id3tag.set_track(v as u32)); 293 | // anytag 294 | // .total_tracks() 295 | // .map(|v| id3tag.set_total_tracks(v as u32)); 296 | // anytag.disc_number().map(|v| id3tag.set_disc(v as u32)); 297 | // anytag 298 | // .total_discs() 299 | // .map(|v| id3tag.set_total_discs(v as u32)); 300 | // id3tag 301 | // } 302 | // } 303 | -------------------------------------------------------------------------------- /src/components/mp4_tag.rs: -------------------------------------------------------------------------------- 1 | use crate::*; 2 | use id3::Timestamp; 3 | use mp4ameta::{self, ImgFmt}; 4 | use std::str::FromStr; 5 | 6 | pub use mp4ameta::Tag as Mp4InnerTag; 7 | 8 | impl_tag!(Mp4Tag, Mp4InnerTag, TagType::Mp4); 9 | 10 | impl<'a> From<&'a Mp4Tag> for AnyTag<'a> { 11 | fn from(inp: &'a Mp4Tag) -> Self { 12 | let title = inp.title(); 13 | let artists = inp.artists().map(|i| i.into_iter().collect::>()); 14 | let date = inp.date(); 15 | let year = inp.year(); 16 | let duration = inp.duration(); 17 | let album_title = inp.album_title(); 18 | let album_artists = inp 19 | .album_artists() 20 | .map(|i| i.into_iter().collect::>()); 21 | let album_cover = inp.album_cover(); 22 | let (a, b) = inp.track(); 23 | let track_number = a; 24 | let total_tracks = b; 25 | let (a, b) = inp.disc(); 26 | let disc_number = a; 27 | let total_discs = b; 28 | let genre = inp.genre(); 29 | let composer = inp.composer(); 30 | let comment = inp.comment(); 31 | Self { 32 | config: inp.config, 33 | title, 34 | artists, 35 | date, 36 | year, 37 | duration, 38 | album_title, 39 | album_cover, 40 | album_artists, 41 | track_number, 42 | total_tracks, 43 | disc_number, 44 | total_discs, 45 | genre, 46 | composer, 47 | comment, 48 | } 49 | } 50 | } 51 | 52 | impl<'a> From> for Mp4Tag { 53 | fn from(inp: AnyTag<'a>) -> Self { 54 | Self { 55 | config: inp.config, 56 | inner: { 57 | let mut t = mp4ameta::Tag::default(); 58 | if let Some(v) = inp.title() { 59 | t.set_title(v) 60 | } 61 | if let Some(i) = inp.artists() { 62 | i.iter().for_each(|&a| t.add_artist(a)) 63 | } 64 | if let Some(v) = inp.year { 65 | t.set_year(v.to_string()) 66 | } 67 | if let Some(v) = inp.album_title() { 68 | t.set_album(v) 69 | } 70 | if let Some(i) = inp.album_artists() { 71 | i.iter().for_each(|&a| t.add_album_artist(a)) 72 | } 73 | if let Some(v) = inp.track_number() { 74 | t.set_track_number(v) 75 | } 76 | if let Some(v) = inp.total_tracks() { 77 | t.set_total_tracks(v) 78 | } 79 | if let Some(v) = inp.disc_number() { 80 | t.set_disc_number(v) 81 | } 82 | if let Some(v) = inp.total_discs() { 83 | t.set_total_discs(v) 84 | } 85 | t 86 | }, 87 | } 88 | } 89 | } 90 | 91 | impl<'a> std::convert::TryFrom<&'a mp4ameta::Data> for Picture<'a> { 92 | type Error = crate::Error; 93 | fn try_from(inp: &'a mp4ameta::Data) -> crate::Result { 94 | Ok(match *inp { 95 | mp4ameta::Data::Png(ref data) => Self { 96 | data, 97 | mime_type: MimeType::Png, 98 | }, 99 | mp4ameta::Data::Jpeg(ref data) => Self { 100 | data, 101 | mime_type: MimeType::Jpeg, 102 | }, 103 | _ => return Err(crate::Error::NotAPicture), 104 | }) 105 | } 106 | } 107 | 108 | impl AudioTagEdit for Mp4Tag { 109 | fn title(&self) -> Option<&str> { 110 | self.inner.title() 111 | } 112 | fn set_title(&mut self, title: &str) { 113 | self.inner.set_title(title) 114 | } 115 | fn remove_title(&mut self) { 116 | self.inner.remove_title(); 117 | } 118 | 119 | fn artist(&self) -> Option<&str> { 120 | self.inner.artist() 121 | } 122 | fn set_artist(&mut self, artist: &str) { 123 | self.inner.set_artist(artist) 124 | } 125 | fn remove_artist(&mut self) { 126 | self.inner.remove_artists(); 127 | } 128 | 129 | fn artists(&self) -> Option> { 130 | let v = self.inner.artists().fold(Vec::new(), |mut v, a| { 131 | v.push(a); 132 | v 133 | }); 134 | if !v.is_empty() { 135 | Some(v) 136 | } else { 137 | None 138 | } 139 | } 140 | fn add_artist(&mut self, v: &str) { 141 | self.inner.add_artist(v); 142 | } 143 | 144 | fn date(&self) -> Option { 145 | if let Some(Ok(date)) = self.inner.year().map(Timestamp::from_str) { 146 | Some(date) 147 | } else { 148 | None 149 | } 150 | } 151 | fn set_date(&mut self, date: Timestamp) { 152 | self.inner.set_year(date.to_string()) 153 | } 154 | fn remove_date(&mut self) { 155 | self.inner.remove_year() 156 | } 157 | 158 | fn year(&self) -> Option { 159 | self.inner.year().and_then(|x| str::parse(x).ok()) 160 | } 161 | fn set_year(&mut self, year: i32) { 162 | self.inner.set_year(year.to_string()) 163 | } 164 | fn remove_year(&mut self) { 165 | self.inner.remove_year(); 166 | } 167 | 168 | // Return Option with duration in second 169 | fn duration(&self) -> Option { 170 | self.inner.duration().map(|d| d.as_secs_f64()) 171 | } 172 | 173 | fn album_title(&self) -> Option<&str> { 174 | self.inner.album() 175 | } 176 | fn set_album_title(&mut self, v: &str) { 177 | self.inner.set_album(v) 178 | } 179 | fn remove_album_title(&mut self) { 180 | self.inner.remove_album(); 181 | } 182 | 183 | fn album_artist(&self) -> Option<&str> { 184 | self.inner.album_artist() 185 | } 186 | fn set_album_artist(&mut self, v: &str) { 187 | self.inner.set_album_artist(v) 188 | } 189 | fn remove_album_artist(&mut self) { 190 | self.inner.remove_album_artists(); 191 | } 192 | 193 | fn album_artists(&self) -> Option> { 194 | let v = self.inner.album_artists().fold(Vec::new(), |mut v, a| { 195 | v.push(a); 196 | v 197 | }); 198 | if !v.is_empty() { 199 | Some(v) 200 | } else { 201 | None 202 | } 203 | } 204 | fn add_album_artist(&mut self, v: &str) { 205 | self.inner.add_album_artist(v); 206 | } 207 | 208 | fn album_cover(&self) -> Option { 209 | self.inner.artwork().and_then(|data| match data.fmt { 210 | ImgFmt::Jpeg => Some(Picture { 211 | data: data.data, 212 | mime_type: MimeType::Jpeg, 213 | }), 214 | ImgFmt::Png => Some(Picture { 215 | data: data.data, 216 | mime_type: MimeType::Png, 217 | }), 218 | _ => None, 219 | }) 220 | } 221 | fn set_album_cover(&mut self, cover: Picture) { 222 | self.remove_album_cover(); 223 | self.inner.add_artwork(match cover.mime_type { 224 | MimeType::Png => mp4ameta::Img { 225 | fmt: ImgFmt::Png, 226 | data: cover.data.to_owned(), 227 | }, 228 | MimeType::Jpeg => mp4ameta::Img { 229 | fmt: ImgFmt::Jpeg, 230 | data: cover.data.to_owned(), 231 | }, 232 | _ => panic!("Only png and jpeg are supported in m4a"), 233 | }); 234 | } 235 | fn remove_album_cover(&mut self) { 236 | self.inner.remove_artworks(); 237 | } 238 | 239 | fn remove_track(&mut self) { 240 | self.inner.remove_track(); // faster than removing separately 241 | } 242 | 243 | fn composer(&self) -> Option<&str> { 244 | self.inner.composer() 245 | } 246 | fn set_composer(&mut self, composer: String) { 247 | self.inner.set_composer(composer); 248 | } 249 | fn remove_composer(&mut self) { 250 | self.inner.remove_composers(); 251 | } 252 | 253 | fn track_number(&self) -> Option { 254 | self.inner.track_number() 255 | } 256 | fn set_track_number(&mut self, track: u16) { 257 | self.inner.set_track_number(track); 258 | } 259 | fn remove_track_number(&mut self) { 260 | self.inner.remove_track_number(); 261 | } 262 | 263 | fn total_tracks(&self) -> Option { 264 | self.inner.total_tracks() 265 | } 266 | fn set_total_tracks(&mut self, total_track: u16) { 267 | self.inner.set_total_tracks(total_track); 268 | } 269 | fn remove_total_tracks(&mut self) { 270 | self.inner.remove_total_tracks(); 271 | } 272 | 273 | fn remove_disc(&mut self) { 274 | self.inner.remove_disc(); 275 | } 276 | 277 | fn disc_number(&self) -> Option { 278 | self.inner.disc_number() 279 | } 280 | fn set_disc_number(&mut self, disc_number: u16) { 281 | self.inner.set_disc_number(disc_number) 282 | } 283 | fn remove_disc_number(&mut self) { 284 | self.inner.remove_disc_number(); 285 | } 286 | 287 | fn total_discs(&self) -> Option { 288 | self.inner.total_discs() 289 | } 290 | fn set_total_discs(&mut self, total_discs: u16) { 291 | self.inner.set_total_discs(total_discs) 292 | } 293 | fn remove_total_discs(&mut self) { 294 | self.inner.remove_total_discs(); 295 | } 296 | 297 | fn genre(&self) -> Option<&str> { 298 | self.inner.genre() 299 | } 300 | fn set_genre(&mut self, genre: &str) { 301 | self.inner.set_genre(genre); 302 | } 303 | fn remove_genre(&mut self) { 304 | self.inner.remove_genres(); 305 | } 306 | 307 | fn comment(&self) -> Option<&str> { 308 | self.inner.comment() 309 | } 310 | fn set_comment(&mut self, comment: String) { 311 | self.inner.set_comment(comment); 312 | } 313 | fn remove_comment(&mut self) { 314 | self.inner.remove_comments(); 315 | } 316 | } 317 | 318 | impl AudioTagWrite for Mp4Tag { 319 | fn write_to(&mut self, file: &mut File) -> crate::Result<()> { 320 | self.inner.write_to(file)?; 321 | Ok(()) 322 | } 323 | fn write_to_path(&mut self, path: &str) -> crate::Result<()> { 324 | self.inner.write_to_path(path)?; 325 | Ok(()) 326 | } 327 | } 328 | --------------------------------------------------------------------------------