diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 7ae98f3..aa61a0f 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -1,22 +1,17 @@ name: Rust -on: - push: - branches: [ main ] - pull_request: - branches: [ main ] +on: [push, pull_request] env: CARGO_TERM_COLOR: always jobs: build: - runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - - name: Build - run: cargo build --verbose - - name: Run tests - run: cargo test --verbose + - uses: actions/checkout@v2 + - name: Build + run: cargo build --verbose + - name: Run tests + run: cargo test --verbose diff --git a/Cargo.toml b/Cargo.toml index fef331a..5ed552d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,12 +10,15 @@ repository = "https://github.com/TianyiShi2001/audiotags" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -id3 = "0.5.1" -mp4ameta = "0.6" +id3 = "1.0.3" +mp4ameta = "0.11.0" metaflac = "0.2" thiserror = "1.0.21" audiotags-dev-macro = {path = "./audiotags-dev-macro", version = "0.1.4"} +[dev-dependencies] +tempfile = "3.3.0" + [features] -defualt = ['from'] -from = [] \ No newline at end of file +default = ['from'] +from = [] diff --git a/src/anytag.rs b/src/anytag.rs index 61248ca..c02646e 100644 --- a/src/anytag.rs +++ b/src/anytag.rs @@ -6,6 +6,7 @@ pub struct AnyTag<'a> { pub title: Option<&'a str>, pub artists: Option>, pub year: Option, + pub duration: Option, pub album_title: Option<&'a str>, pub album_artists: Option>, pub album_cover: Option>, @@ -13,6 +14,7 @@ pub struct AnyTag<'a> { pub total_tracks: Option, pub disc_number: Option, pub total_discs: Option, + pub genre:Option<&'a str>, } impl AudioTagConfig for AnyTag<'_> { @@ -41,6 +43,12 @@ impl<'a> AnyTag<'a> { pub fn set_year(&mut self, year: i32) { self.year = Some(year); } + pub fn duration(&self) -> Option { + self.duration + } + pub fn set_duration(&mut self, duration: f64) { + self.duration = Some(duration); + } pub fn album_title(&self) -> Option<&str> { self.album_title.as_deref() } @@ -59,6 +67,9 @@ impl<'a> AnyTag<'a> { pub fn total_discs(&self) -> Option { self.total_tracks } + pub fn genre(&self) -> Option<&str> { + self.genre.as_deref() + } } impl AnyTag<'_> { diff --git a/src/components/flac_tag.rs b/src/components/flac_tag.rs index 2320d60..07ef6ed 100644 --- a/src/components/flac_tag.rs +++ b/src/components/flac_tag.rs @@ -27,6 +27,7 @@ impl<'a> From<&'a FlacTag> for AnyTag<'a> { t.title = inp.title(); t.artists = inp.artists(); t.year = inp.year(); + t.duration = inp.duration(); t.album_title = inp.album_title(); t.album_artists = inp.album_artists(); t.album_cover = inp.album_cover(); @@ -90,6 +91,12 @@ impl AudioTagEdit for FlacTag { self.set_first("YEAR", &year.to_string()); } + fn duration(&self) -> Option { + self.inner + .get_streaminfo() + .map(|s| s.total_samples as f64 / f64::from(s.sample_rate)) + } + fn album_title(&self) -> Option<&str> { self.get_first("ALBUM") } @@ -170,6 +177,13 @@ impl AudioTagEdit for FlacTag { self.set_first("TOTALDISCS", &v.to_string()) } + fn genre(&self) -> Option<&str> { + self.get_first("GENRE") + } + fn set_genre(&mut self, v: &str) { + self.set_first("GENRE", v); + } + fn remove_title(&mut self) { self.remove("TITLE"); } @@ -202,6 +216,9 @@ impl AudioTagEdit for FlacTag { fn remove_total_discs(&mut self) { self.remove("TOTALDISCS"); } + fn remove_genre(&mut self) { + self.remove("GENRE"); + } } impl AudioTagWrite for FlacTag { diff --git a/src/components/id3_tag.rs b/src/components/id3_tag.rs index d965c2d..35a7731 100644 --- a/src/components/id3_tag.rs +++ b/src/components/id3_tag.rs @@ -1,5 +1,5 @@ use crate::*; -use id3; +use id3::{self, TagLike}; pub use id3::Tag as Id3v2InnerTag; @@ -13,6 +13,7 @@ impl<'a> From<&'a Id3v2Tag> for AnyTag<'a> { title: inp.title(), artists: inp.artists(), year: inp.year(), + duration: Some(inp.inner.duration().unwrap() as f64), album_title: inp.album_title(), album_artists: inp.album_artists(), album_cover: inp.album_cover(), @@ -20,6 +21,7 @@ impl<'a> From<&'a Id3v2Tag> for AnyTag<'a> { total_tracks: inp.total_tracks(), disc_number: inp.disc_number(), total_discs: inp.total_discs(), + genre: inp.genre(), } } } @@ -39,6 +41,7 @@ impl<'a> From> for Id3v2Tag { inp.total_tracks().map(|v| t.set_total_tracks(v as u32)); inp.disc_number().map(|v| t.set_disc(v as u32)); inp.total_discs().map(|v| t.set_total_discs(v as u32)); + inp.genre().map(|v| t.set_genre(v)); t }, } @@ -89,8 +92,10 @@ impl AudioTagEdit for Id3v2Tag { self.inner.set_year(year) } fn remove_year(&mut self) { - self.inner.remove("TYER") - // self.inner.remove_year(); // TODO + self.inner.remove_year(); + } + fn duration(&self) -> Option { + self.inner.duration().map(|d| f64::from(d)) } fn album_title(&self) -> Option<&str> { @@ -127,7 +132,7 @@ impl AudioTagEdit for Id3v2Tag { } fn set_album_cover(&mut self, cover: Picture) { self.remove_album_cover(); - self.inner.add_picture(id3::frame::Picture { + self.inner.add_frame(id3::frame::Picture { mime_type: String::from(cover.mime_type), picture_type: id3::frame::PictureType::CoverFront, description: "".to_owned(), @@ -178,6 +183,16 @@ impl AudioTagEdit for Id3v2Tag { fn remove_total_discs(&mut self) { self.inner.remove_total_discs(); } + + fn genre(&self) -> Option<&str> { + self.inner.genre() + } + fn set_genre(&mut self, v: &str) { + self.inner.set_genre(v); + } + fn remove_genre(&mut self) { + self.inner.remove_genre(); + } } impl AudioTagWrite for Id3v2Tag { diff --git a/src/components/mp4_tag.rs b/src/components/mp4_tag.rs index 5b12669..e251779 100644 --- a/src/components/mp4_tag.rs +++ b/src/components/mp4_tag.rs @@ -1,5 +1,5 @@ use crate::*; -use mp4ameta; +use mp4ameta::{self, ImgFmt}; pub use mp4ameta::Tag as Mp4InnerTag; @@ -10,6 +10,7 @@ impl<'a> From<&'a Mp4Tag> for AnyTag<'a> { let title = inp.title(); let artists = inp.artists().map(|i| i.into_iter().collect::>()); let year = inp.year(); + let duration = inp.duration(); let album_title = inp.album_title(); let album_artists = inp .album_artists() @@ -21,11 +22,13 @@ impl<'a> From<&'a Mp4Tag> for AnyTag<'a> { let (a, b) = inp.disc(); let disc_number = a; let total_discs = b; + let genre = inp.genre(); Self { config: inp.config.clone(), title, artists, year, + duration, album_title, album_cover, album_artists, @@ -33,6 +36,7 @@ impl<'a> From<&'a Mp4Tag> for AnyTag<'a> { total_tracks, disc_number, total_discs, + genre, } } } @@ -113,6 +117,11 @@ impl AudioTagEdit for Mp4Tag { self.inner.set_year(year.to_string()) } + // Return Option with duration in second + fn duration(&self) -> Option { + self.inner.duration().map(|d| d.as_secs_f64()) + } + fn album_title(&self) -> Option<&str> { self.inner.album() } @@ -143,14 +152,13 @@ impl AudioTagEdit for Mp4Tag { } fn album_cover(&self) -> Option { - use mp4ameta::Data::*; - self.inner.artwork().and_then(|data| match data { - Jpeg(d) => Some(Picture { - data: d, + self.inner.artwork().and_then(|data| match data.fmt { + ImgFmt::Jpeg => Some(Picture { + data: data.data, mime_type: MimeType::Jpeg, }), - Png(d) => Some(Picture { - data: d, + ImgFmt::Png => Some(Picture { + data: data.data, mime_type: MimeType::Png, }), _ => None, @@ -159,8 +167,14 @@ impl AudioTagEdit for Mp4Tag { fn set_album_cover(&mut self, cover: Picture) { self.remove_album_cover(); self.inner.add_artwork(match cover.mime_type { - MimeType::Png => mp4ameta::Data::Png(cover.data.to_owned()), - MimeType::Jpeg => mp4ameta::Data::Jpeg(cover.data.to_owned()), + MimeType::Png => mp4ameta::Img { + fmt: ImgFmt::Png, + data: cover.data.to_owned(), + }, + MimeType::Jpeg => mp4ameta::Img { + fmt: ImgFmt::Jpeg, + data: cover.data.to_owned(), + }, _ => panic!("Only png and jpeg are supported in m4a"), }); } @@ -191,6 +205,13 @@ impl AudioTagEdit for Mp4Tag { self.inner.set_total_discs(total_discs) } + fn genre(&self) -> Option<&str> { + self.inner.genre() + } + fn set_genre(&mut self, genre: &str) { + self.inner.set_genre(genre); + } + fn remove_title(&mut self) { self.inner.remove_title(); } @@ -204,11 +225,10 @@ impl AudioTagEdit for Mp4Tag { self.inner.remove_album(); } fn remove_album_artist(&mut self) { - self.inner.remove_data(mp4ameta::atom::ALBUM_ARTIST); self.inner.remove_album_artists(); } fn remove_album_cover(&mut self) { - self.inner.remove_artwork(); + self.inner.remove_artworks(); } fn remove_track(&mut self) { self.inner.remove_track(); // faster than removing separately @@ -228,6 +248,9 @@ impl AudioTagEdit for Mp4Tag { fn remove_total_discs(&mut self) { self.inner.remove_total_discs(); } + fn remove_genre(&mut self) { + self.inner.remove_genres(); + } } impl AudioTagWrite for Mp4Tag { diff --git a/src/lib.rs b/src/lib.rs index 6cb50e0..46df249 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -59,7 +59,7 @@ pub use std::convert::{TryFrom, TryInto}; /// /// # Examples /// -/// ``` +/// ```no_run /// use audiotags::{Tag, TagType}; /// // Guess the format by default /// let mut tag = Tag::new().read_from_path("assets/a.mp3").unwrap(); diff --git a/src/traits.rs b/src/traits.rs index 71ed58c..464d913 100644 --- a/src/traits.rs +++ b/src/traits.rs @@ -35,6 +35,8 @@ pub trait AudioTagEdit: AudioTagConfig { fn set_year(&mut self, year: i32); fn remove_year(&mut self); + fn duration(&self) -> Option; + fn album(&self) -> Option> { self.album_title().map(|title| Album { title, @@ -124,6 +126,10 @@ pub trait AudioTagEdit: AudioTagConfig { fn total_discs(&self) -> Option; fn set_total_discs(&mut self, total_discs: u16); fn remove_total_discs(&mut self); + + fn genre(&self) -> Option<&str>; + fn set_genre(&mut self, genre:&str); + fn remove_genre(&mut self); } pub trait AudioTagWrite { diff --git a/tests/inner.rs b/tests/inner.rs index 25d9921..966de6c 100644 --- a/tests/inner.rs +++ b/tests/inner.rs @@ -1,20 +1,31 @@ use audiotags::*; +use id3::TagLike; +use std::fs; +use tempfile::Builder; #[test] fn test_inner() { + let tmp = Builder::new().suffix(".mp3").tempfile().unwrap(); + fs::copy("assets/a.mp3", &tmp).unwrap(); + + let tmp_path = tmp.path(); + let mut innertag = metaflac::Tag::default(); innertag .vorbis_comments_mut() .set_title(vec!["title from metaflac::Tag"]); + let tag: FlacTag = innertag.into(); let mut id3tag = tag.to_dyn_tag(TagType::Id3v2); + id3tag - .write_to_path("assets/a.mp3") + .write_to_path(tmp_path.to_str().unwrap()) .expect("Fail to write!"); let id3tag_reload = Tag::default() - .read_from_path("assets/a.mp3") + .read_from_path(tmp_path) .expect("Fail to read!"); + assert_eq!(id3tag_reload.title(), Some("title from metaflac::Tag")); // let id3tag: Id3v2Tag = id3tag_reload.into(); @@ -27,11 +38,12 @@ fn test_inner() { minute: None, second: None, }; + id3tag_inner.set_date_recorded(timestamp.clone()); id3tag_inner - .write_to_path("assets/a.mp3", id3::Version::Id3v24) + .write_to_path(tmp_path, id3::Version::Id3v24) .expect("Fail to write!"); - let id3tag_reload = id3::Tag::read_from_path("assets/a.mp3").expect("Fail to read!"); + let id3tag_reload = id3::Tag::read_from_path(tmp_path).expect("Fail to read!"); assert_eq!(id3tag_reload.date_recorded(), Some(timestamp)); } diff --git a/tests/io.rs b/tests/io.rs index 744ab99..a55d6e3 100644 --- a/tests/io.rs +++ b/tests/io.rs @@ -1,10 +1,21 @@ use audiotags::{MimeType, Picture, Tag}; +use std::ffi::OsString; +use std::fs; +use std::path::Path; +use tempfile::Builder; macro_rules! test_file { ( $function:ident, $file:expr ) => { #[test] fn $function() { - let mut tags = Tag::default().read_from_path($file).unwrap(); + let path = Path::new($file); + let mut suffix = OsString::from("."); + suffix.push(path.extension().unwrap()); + let tmp = Builder::new().suffix(&suffix).tempfile().unwrap(); + fs::copy($file, &tmp).unwrap(); + let tmp_path = tmp.path(); + + let mut tags = Tag::default().read_from_path(tmp_path).unwrap(); tags.set_title("foo title"); assert_eq!(tags.title(), Some("foo title")); tags.remove_title(); @@ -45,6 +56,12 @@ macro_rules! test_file { tags.remove_album_cover(); assert!(tags.album_cover().is_none()); tags.remove_album_cover(); + + tags.set_genre("foo song genre"); + assert_eq!(tags.genre(), Some("foo song genre")); + tags.remove_genre(); + assert!(tags.genre().is_none()); + tags.remove_genre(); } }; }