diff --git a/CHANGELOG.md b/CHANGELOG.md index 2be455e..ef32baa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,8 @@ # Changelog ## bliss 0.9.0 +* Make album playlists take into account disk numbers (no more mixed disks!). +* Add a `disc_number` field in `Song`s. * Add a mechanism to do migrations for Libraries, to make sure we're ready for potential new features. * Make `track_number` an integer, and not a string. diff --git a/Cargo.lock b/Cargo.lock index e18a754..d2ed73f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -97,7 +97,7 @@ dependencies = [ "peeking_take_while", "proc-macro2", "quote", - "regex 1.10.5", + "regex 1.10.6", "rustc-hash", "shlex", "syn 1.0.109", @@ -118,10 +118,10 @@ dependencies = [ "lazycell", "proc-macro2", "quote", - "regex 1.10.5", + "regex 1.10.6", "rustc-hash", "shlex", - "syn 2.0.72", + "syn 2.0.73", ] [[package]] @@ -227,9 +227,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.1.7" +version = "1.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26a5c3fd7bfa1ce3897a3a3501d362b2d87b7f2583ebcb4a949ec25911025cbc" +checksum = "e9e8aabfac534be767c909e0690571677d49f41bd8465ae876fe043d52ba5292" dependencies = [ "jobserver", "libc", @@ -437,9 +437,9 @@ checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" [[package]] name = "ffmpeg-next" -version = "7.0.3" +version = "7.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bdbf855af38f590e03db263812926ca9a7695f1510e4d88b9c889df06728e396" +checksum = "19a340e3d664ad5f530147cd6d4a86ece739a829fe2d81c369389ef903bd96f6" dependencies = [ "bitflags 2.6.0", "ffmpeg-sys-next", @@ -486,9 +486,9 @@ dependencies = [ [[package]] name = "flate2" -version = "1.0.30" +version = "1.0.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f54427cfd1c7829e2a139fcefea601bf088ebca651d2bf53ebc600eac295dae" +checksum = "7f211bbe8e69bbd0cfdea405084f128ae8b4aaa6b0b522fc8f2b009084797920" dependencies = [ "crc32fast", "miniz_oxide", @@ -996,7 +996,7 @@ dependencies = [ "pest_meta", "proc-macro2", "quote", - "syn 2.0.72", + "syn 2.0.73", ] [[package]] @@ -1024,9 +1024,12 @@ checksum = "da544ee218f0d287a911e9c99a39a8c9bc8fcad3cb8db5959940044ecfc67265" [[package]] name = "ppv-lite86" -version = "0.2.17" +version = "0.2.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" +checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" +dependencies = [ + "zerocopy", +] [[package]] name = "pretty_assertions" @@ -1213,9 +1216,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.10.5" +version = "1.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b91213439dad192326a0d7c6ee3955910425f441d7038e0d6933b0aec5c4517f" +checksum = "4219d74c6b67a3654a9fbebc4b419e22126d13d2f3c4a07ee0cb61ff79a79619" dependencies = [ "aho-corasick 1.1.3", "memchr 2.7.4", @@ -1323,22 +1326,22 @@ checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" [[package]] name = "serde" -version = "1.0.204" +version = "1.0.206" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc76f558e0cbb2a839d37354c575f1dc3fdc6546b5be373ba43d95f231bf7c12" +checksum = "5b3e4cd94123dd520a128bcd11e34d9e9e423e7e3e50425cb1b4b1e3549d0284" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.204" +version = "1.0.206" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0cd7e117be63d3c3678776753929474f3b04a43a080c744d6b0ae2a8c28e222" +checksum = "fabfb6138d2383ea8208cf98ccf69cdfb1aff4088460681d84189aa259762f97" dependencies = [ "proc-macro2", "quote", - "syn 2.0.72", + "syn 2.0.73", ] [[package]] @@ -1354,9 +1357,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.121" +version = "1.0.122" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ab380d7d9f22ef3f21ad3e6c1ebe8e4fc7a2000ccba2e4d71fc96f15b2cb609" +checksum = "784b6203951c57ff748476b126ccb5e8e2959a5c19e5c617ab1956be3dbc68da" dependencies = [ "itoa", "memchr 2.7.4", @@ -1431,9 +1434,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.72" +version = "2.0.73" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc4b9b9bf2add8093d3f2c0204471e951b2285580335de42f9d2534f3ae7a8af" +checksum = "837a7e8026c6ce912ff01cefbe8cafc2f8010ac49682e2a3d9decc3bce1ecaaf" dependencies = [ "proc-macro2", "quote", @@ -1476,7 +1479,7 @@ checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261" dependencies = [ "proc-macro2", "quote", - "syn 2.0.72", + "syn 2.0.73", ] [[package]] @@ -1791,6 +1794,7 @@ version = "0.7.35" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" dependencies = [ + "byteorder", "zerocopy-derive", ] @@ -1802,7 +1806,7 @@ checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.72", + "syn 2.0.73", ] [[package]] diff --git a/data/special-tags.mp3 b/data/special-tags.mp3 new file mode 100644 index 0000000..46ba49b Binary files /dev/null and b/data/special-tags.mp3 differ diff --git a/data/unsupported-tags.mp3 b/data/unsupported-tags.mp3 new file mode 100644 index 0000000..bf2f938 Binary files /dev/null and b/data/unsupported-tags.mp3 differ diff --git a/data/white_noise.mp3 b/data/white_noise.mp3 index 3ac06fa..7836aa3 100644 Binary files a/data/white_noise.mp3 and b/data/white_noise.mp3 differ diff --git a/src/library.rs b/src/library.rs index c738892..663944d 100644 --- a/src/library.rs +++ b/src/library.rs @@ -1085,7 +1085,7 @@ impl Library { audio_file_path, id from song where album = ? and analyzed = true and version = ? order - by track_number; + by disc_number, track_number; "; // Get the song's analysis, and attach it to the existing song. @@ -1093,7 +1093,7 @@ impl Library { select feature, song.id from feature join song on song.id = feature.song_id where album=? and analyzed = true and version = ? - order by track_number; + order by disc_number, track_number; "; let songs = self._songs_from_statement(songs_statement, features_statement, params)?; if songs.is_empty() { @@ -1565,10 +1565,10 @@ mod test { path: "/path/to/song2201".into(), artist: Some("Artist2001".into()), title: Some("Title2001".into()), - album: Some("Remixes of Album2001".into()), + album: Some("An Album2001".into()), album_artist: Some("An Album Artist2001".into()), - track_number: Some(2), - disc_number: Some(1), + track_number: Some(1), + disc_number: Some(2), genre: Some("Electronica2001".into()), analysis: Analysis { internal_analysis: analysis_vector, @@ -1760,8 +1760,8 @@ mod test { \"/path/to/charlie2001\"}', null, null ), ( - 2201, '/path/to/song2201', 'Artist2001', 'Title2001', 'Remixes of Album2001', - 'An Album Artist2001', 2, 1, 'Electronica2001', 410, true, + 2201, '/path/to/song2201', 'Artist2001', 'Title2001', 'An Album2001', + 'An Album Artist2001', 1, 2, 'Electronica2001', 410, true, 1, '{\"ignore\": false, \"metadata_bliss_does_not_have\": \"/path/to/charlie2201\"}', null, null ), @@ -2287,10 +2287,10 @@ mod test { // First album. "/path/to/song5001".to_string(), "/path/to/song1001".to_string(), - // Second album, well ordered. + // Second album, well ordered, disc 1 "/path/to/song6001".to_string(), "/path/to/song2001".to_string(), - // Seecond album, remixes + // Second album disc 2 "/path/to/song2201".to_string(), // Third album. "/path/to/song7001".to_string(), @@ -2320,6 +2320,7 @@ mod test { // Second album, well ordered. "/path/to/song6001".to_string(), "/path/to/song2001".to_string(), + "/path/to/song2201".to_string(), ], album .into_iter() diff --git a/src/playlist.rs b/src/playlist.rs index ca660ff..ec1536e 100644 --- a/src/playlist.rs +++ b/src/playlist.rs @@ -324,12 +324,9 @@ pub fn closest_album_to_group + Clone>( al.sort_by(|s1, s2| { let track_number1 = s1.as_ref().track_number.to_owned(); let track_number2 = s2.as_ref().track_number.to_owned(); - if let Some(x) = track_number1 { - if let Some(y) = track_number2 { - return x.cmp(&y); - } - } - std::cmp::Ordering::Equal + let disc_number1 = s1.as_ref().disc_number.to_owned(); + let disc_number2 = s2.as_ref().disc_number.to_owned(); + (disc_number1, track_number1).cmp(&(disc_number2, track_number2)) }); playlist.extend(al); } @@ -813,36 +810,57 @@ mod test { album: Some(String::from("Album")), artist: Some(String::from("Artist")), track_number: Some(1), + disc_number: Some(1), ..Default::default() }; - let second_song = Song { - path: Path::new("path-to-second").to_path_buf(), - analysis: Analysis::new([0.1; 20]), - album: Some(String::from("Another Album")), - artist: Some(String::from("Artist")), - track_number: Some(10), - ..Default::default() - }; - - let third_song = Song { path: Path::new("path-to-third").to_path_buf(), analysis: Analysis::new([10.; 20]), album: Some(String::from("Album")), artist: Some(String::from("Another Artist")), track_number: Some(2), + disc_number: Some(1), ..Default::default() }; - let fourth_song = Song { + let first_song_other_album_disc_1 = Song { + path: Path::new("path-to-second-2").to_path_buf(), + analysis: Analysis::new([0.15; 20]), + album: Some(String::from("Another Album")), + artist: Some(String::from("Artist")), + track_number: Some(1), + disc_number: Some(1), + ..Default::default() + }; + let second_song_other_album_disc_1 = Song { + path: Path::new("path-to-second").to_path_buf(), + analysis: Analysis::new([0.1; 20]), + album: Some(String::from("Another Album")), + artist: Some(String::from("Artist")), + track_number: Some(2), + disc_number: Some(1), + ..Default::default() + }; + let first_song_other_album_disc_2 = Song { path: Path::new("path-to-fourth").to_path_buf(), analysis: Analysis::new([20.; 20]), album: Some(String::from("Another Album")), artist: Some(String::from("Another Artist")), track_number: Some(1), + disc_number: Some(2), ..Default::default() }; - let fifth_song = Song { + let second_song_other_album_disc_2 = Song { + path: Path::new("path-to-fourth").to_path_buf(), + analysis: Analysis::new([20.; 20]), + album: Some(String::from("Another Album")), + artist: Some(String::from("Another Artist")), + track_number: Some(4), + disc_number: Some(2), + ..Default::default() + }; + + let song_no_album = Song { path: Path::new("path-to-fifth").to_path_buf(), analysis: Analysis::new([40.; 20]), artist: Some(String::from("Third Artist")), @@ -852,18 +870,22 @@ mod test { let pool = vec![ first_song.to_owned(), - fourth_song.to_owned(), - third_song.to_owned(), + second_song_other_album_disc_1.to_owned(), + second_song_other_album_disc_2.to_owned(), second_song.to_owned(), - fifth_song.to_owned(), + first_song_other_album_disc_2.to_owned(), + first_song_other_album_disc_1.to_owned(), + song_no_album.to_owned(), ]; - let group = vec![first_song.to_owned(), third_song.to_owned()]; + let group = vec![first_song.to_owned(), second_song.to_owned()]; assert_eq!( vec![ first_song.to_owned(), - third_song.to_owned(), - fourth_song.to_owned(), - second_song.to_owned() + second_song.to_owned(), + first_song_other_album_disc_1.to_owned(), + second_song_other_album_disc_1.to_owned(), + first_song_other_album_disc_2.to_owned(), + second_song_other_album_disc_2.to_owned(), ], closest_album_to_group(group, pool.to_owned()).unwrap(), ); @@ -876,34 +898,46 @@ mod test { bliss_song: second_song, something: true, }; - let third_song = CustomSong { - bliss_song: third_song, + + let first_song_other_album_disc_1 = CustomSong { + bliss_song: first_song_other_album_disc_1, something: true, }; - let fourth_song = CustomSong { - bliss_song: fourth_song, + let second_song_other_album_disc_1 = CustomSong { + bliss_song: second_song_other_album_disc_1, something: true, }; - - let fifth_song = CustomSong { - bliss_song: fifth_song, + let first_song_other_album_disc_2 = CustomSong { + bliss_song: first_song_other_album_disc_2, + something: true, + }; + let second_song_other_album_disc_2 = CustomSong { + bliss_song: second_song_other_album_disc_2, + something: true, + }; + let song_no_album = CustomSong { + bliss_song: song_no_album, something: true, }; let pool = vec![ first_song.to_owned(), - fourth_song.to_owned(), - third_song.to_owned(), + second_song_other_album_disc_2.to_owned(), + second_song_other_album_disc_1.to_owned(), second_song.to_owned(), - fifth_song.to_owned(), + first_song_other_album_disc_2.to_owned(), + first_song_other_album_disc_1.to_owned(), + song_no_album.to_owned(), ]; - let group = vec![first_song.to_owned(), third_song.to_owned()]; + let group = vec![first_song.to_owned(), second_song.to_owned()]; assert_eq!( vec![ first_song.to_owned(), - third_song.to_owned(), - fourth_song.to_owned(), - second_song.to_owned() + second_song.to_owned(), + first_song_other_album_disc_1.to_owned(), + second_song_other_album_disc_1.to_owned(), + first_song_other_album_disc_2.to_owned(), + second_song_other_album_disc_2.to_owned(), ], closest_album_to_group(group, pool.to_owned()).unwrap(), ); diff --git a/src/song/decoder.rs b/src/song/decoder.rs index 1ab94c3..9d7d6b2 100644 --- a/src/song/decoder.rs +++ b/src/song/decoder.rs @@ -512,13 +512,19 @@ pub mod ffmpeg { if let Some(track_number) = ictx.metadata().get("track") { song.track_number = match track_number { "" => None, - t => t.parse::().ok(), + t => t + .parse::() + .ok() + .or_else(|| t.split_once('/').and_then(|(n, _)| n.parse::().ok())), }; }; if let Some(disc_number) = ictx.metadata().get("disc") { song.disc_number = match disc_number { "" => None, - t => t.parse::().ok(), + t => t + .parse::() + .ok() + .or_else(|| t.split_once('/').and_then(|(n, _)| n.parse::().ok())), }; }; if let Some(album_artist) = ictx.metadata().get("album_artist") { @@ -682,6 +688,21 @@ pub mod ffmpeg { assert!((song.duration.as_millis() as f32 - 11070.).abs() < 10.); } + #[test] + fn test_special_tags() { + // This file has tags like `DISC: 02/05` and `TRACK: 06/24`. + let song = Decoder::decode(Path::new("data/special-tags.mp3")).unwrap(); + assert_eq!(song.disc_number, Some(2)); + assert_eq!(song.track_number, Some(6)); + } + + #[test] + fn test_unsupported_tags_format() { + // This file has tags like `TRACK: 02test/05`. + let song = Decoder::decode(Path::new("data/unsupported-tags.mp3")).unwrap(); + assert_eq!(song.track_number, None); + } + #[test] fn test_empty_tags() { let song = Decoder::decode(Path::new("data/no_tags.flac")).unwrap();