diff --git a/crates/metadata/src/lib.rs b/crates/metadata/src/lib.rs index 0fd88ca..7713b73 100644 --- a/crates/metadata/src/lib.rs +++ b/crates/metadata/src/lib.rs @@ -6025,8 +6025,22 @@ impl MetadataClient { let mut found: HashMap = HashMap::new(); for page in 0..MB_RECORDING_PAGES { let offset = page * MB_RECORDING_PAGE; + // Filtered to official ALBUM releases, which is both cheaper and + // more relevant: unfiltered, Radiohead has 1141 releases and the + // two-page window saw 18% of them, mostly singles and bootleg + // pressings. `type=album&status=official` leaves 274, so the same + // two pages now cover 73% of the releases that could actually own a + // popular song. + // + // It does not fix the tail — Queen still has 1119 official album + // pressings — and that is accepted rather than paged around. This + // map exists to place songs Deezer does not carry, and an artist + // with a thousand pressings is one Deezer carries exhaustively: + // measured, the relabel pass reports 0 for both Queen and + // Radiohead. Paging to reach them would cost 11 requests behind the + // 1 req/s gate to change nothing. let url = format!( - "https://musicbrainz.org/ws/2/release?artist={mbid}\ + "https://musicbrainz.org/ws/2/release?artist={mbid}&type=album&status=official\ &inc=recordings+release-groups&limit={MB_RECORDING_PAGE}&offset={offset}&fmt=json" ); let Some(body) = self.musicbrainz_json(&url).await else { @@ -8420,6 +8434,26 @@ fn apply_original_album_attribution( .unwrap_or("9999") .cmp(right.release_date.as_deref().unwrap_or("9999")) }) + // A title one candidate BUILDS ON is the original of it. + // + // Above the track-count rule below, and Radiohead is why. + // Deezer carries "OK Computer" (12 tracks) and "OK Computer + // OKNOTOK 1997 2017" (23) and dates BOTH 1997-05-21, so EP, + // edition-keyword and date all tie and the sampler rule + // then picked the 23-track reissue for "No Surprises", + // "Karma Police" and "Let Down". `is_edition_title` cannot + // catch it because "OKNOTOK 1997 2017" is a bespoke name + // with none of the usual keywords in it. + // + // Only ever fires when both records actually contain the + // song, which is what makes it safe: a sequel does not + // share its predecessor's tracklist, so "Greatest Hits II" + // is never in the running against "Greatest Hits" for the + // same track. The numeral guard is belt and braces. + // `false` sorts before `true`, and the winner is the + // SMALLEST, so the one that expands on the other loses. + .then_with(|| expands_on(&left.title, &right.title) + .cmp(&expands_on(&right.title, &left.title))) // An original album over a truncated sampler of it. .then_with(|| { right @@ -9150,6 +9184,29 @@ fn regex_lite_year_mix_tail(lower_title: &str) -> bool { /// ("OK Computer OKNOTOK 1997 2017") is built on an existing album ("OK /// Computer") despite carrying no edition keyword. Length-guarded so a very /// short album title cannot spuriously match. +/// Does `title` look like `base` plus something — a reissue, an expansion, a +/// bespoke anniversary name — rather than a different record? +/// +/// Returns `false` when the extra part is only a sequel number, because +/// "Greatest Hits II" builds on nothing: it is its own record that happens to +/// share a prefix. Same rule the frontend uses when it decides whether a +/// MusicBrainz row is already covered by a Deezer variant. +fn expands_on(title: &str, base: &str) -> bool { + let (title, base) = (normalize(title), normalize(base)); + if base.len() < 5 || title.len() <= base.len() || !title.starts_with(&base) { + return false; + } + let tail = &title[base.len()..]; + // A bare numeral or volume marker is a SEQUEL, not an edition. + !matches!( + tail, + "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" | "10" + | "ii" | "iii" | "iv" | "v" | "vi" | "vii" | "viii" | "ix" | "x" + ) && !tail.starts_with("vol") + && !tail.starts_with("part") + && !tail.starts_with("pt") +} + fn contains_other_album_title(title: &str, non_edition_titles: &[String]) -> bool { let n = normalize(title); non_edition_titles @@ -14965,6 +15022,48 @@ mod tests { assert_eq!(tracks[0].album, "Weezer (Blue Album)"); } + #[test] + fn attribution_prefers_the_record_a_reissue_was_built_from() { + // Radiohead, measured live: Deezer carries "OK Computer" (12 tracks) + // and "OK Computer OKNOTOK 1997 2017" (23) and dates BOTH 1997-05-21. + // EP, edition-keyword and date all tie — "OKNOTOK 1997 2017" carries + // none of the edition keywords — so the "original over a truncated + // sampler" rule handed No Surprises, Karma Police and Let Down to the + // 23-track reissue. + let reissue = catalog_album(40, "OK Computer OKNOTOK 1997 2017", Some("1997-05-21")); + let original = catalog_album(41, "OK Computer", Some("1997-05-21")); + let songs: &[&str] = &["No Surprises", "Karma Police", "Let Down"]; + let (albums, containment, details) = + attribution_fixture(&[(reissue, songs), (original, songs)]); + let mut tracks = vec![licensed_track(999, "No Surprises", "Some Compilation", 77)]; + let mb = HashMap::from([ + (release_date_key(Some("Album"), "OK Computer"), mb_album("1997-05-21")), + ( + release_date_key(Some("Album"), "OK Computer OKNOTOK 1997 2017"), + mb_album("1997-05-21"), + ), + ]); + + let moved = + apply_original_album_attribution(&mut tracks, &albums, &containment, &details, &mb); + assert_eq!(moved, 1); + assert_eq!(tracks[0].album, "OK Computer"); + } + + #[test] + fn a_sequel_is_not_an_expansion_of_the_record_it_follows() { + // The guard on the rule above. A sequel shares a prefix but not a + // tracklist, so it must never be read as an edition of its predecessor. + assert!(expands_on("OK Computer OKNOTOK 1997 2017", "OK Computer")); + assert!(expands_on("Rumours (Super Deluxe)", "Rumours")); + assert!(!expands_on("Greatest Hits II", "Greatest Hits")); + assert!(!expands_on("Trap House 3", "Trap House")); + assert!(!expands_on("Deep Cuts, Volume 2", "Deep Cuts")); + // Not a prefix at all, and never itself. + assert!(!expands_on("Kid A", "Amnesiac")); + assert!(!expands_on("OK Computer", "OK Computer")); + } + #[test] fn attribution_leaves_a_track_alone_when_nothing_contains_it() { // 3 of the 80 measured tracks match nothing — a remix, an "(Original)"