fix(qt): catalog placeholder names never render as artists or composers
Qobuz fills missing credits with strings such as "Not Documented", "Unknown Artist" or "N/A". `qbz_text_utils::names::is_placeholder_name` recognises them (case-insensitive, hyphen/underscore folded) and the album document, the track info modal and the album info modal drop those names from credits, composers, featured lists and the header's artist fallback, dropping a row entirely when nothing legible is left. cc
This commit is contained in:
1 parent
7f609a55e5
commit
e546e6614e
5 files changed
+112
-10
No files matched your search
@@ -121,6 +121,8 @@ fn map_track(index: usize, t: &Track, album_artist: &str) -> InfoTrack {
|
|||||||
let performers: Vec<PerformerRow> =
|
let performers: Vec<PerformerRow> =
|
||||||
parse_performers(t.performers.as_deref().unwrap_or_default())
|
parse_performers(t.performers.as_deref().unwrap_or_default())
|
||||||
.into_iter()
|
.into_iter()
|
||||||
|
// Catalog fillers ("Not Documented") are not people (2026-09-13).
|
||||||
|
.filter(|p| !qbz_text_utils::names::is_placeholder_name(&p.name))
|
||||||
.map(|p| PerformerRow {
|
.map(|p| PerformerRow {
|
||||||
roles: roles_suffix(&p.roles),
|
roles: roles_suffix(&p.roles),
|
||||||
primary_role: p
|
primary_role: p
|
||||||
|
|||||||
@@ -485,10 +485,14 @@ fn credit_role(roles: Option<&Vec<String>>) -> String {
|
|||||||
/// album.rs `build_credits` (releaseArtistsMapper parity, minus the
|
/// album.rs `build_credits` (releaseArtistsMapper parity, minus the
|
||||||
/// "VARIOUS"-composer drop it also applies).
|
/// "VARIOUS"-composer drop it also applies).
|
||||||
fn build_credits(album: &Album) -> Vec<(String, String, String)> {
|
fn build_credits(album: &Album) -> Vec<(String, String, String)> {
|
||||||
|
use qbz_text_utils::names::is_placeholder_name;
|
||||||
|
// Catalog fillers ("Not Documented") are not credits — dropped here so
|
||||||
|
// the header never prints them beside the real artist (2026-09-13).
|
||||||
let mut credits: Vec<(String, String, String)> =
|
let mut credits: Vec<(String, String, String)> =
|
||||||
match album.artists.as_ref().filter(|v| !v.is_empty()) {
|
match album.artists.as_ref().filter(|v| !v.is_empty()) {
|
||||||
Some(list) => list
|
Some(list) => list
|
||||||
.iter()
|
.iter()
|
||||||
|
.filter(|a| !is_placeholder_name(&a.name))
|
||||||
.map(|a| {
|
.map(|a| {
|
||||||
(
|
(
|
||||||
a.name.clone(),
|
a.name.clone(),
|
||||||
@@ -497,15 +501,22 @@ fn build_credits(album: &Album) -> Vec<(String, String, String)> {
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
.collect(),
|
.collect(),
|
||||||
None => vec![(
|
None => Vec::new(),
|
||||||
album.artist.name.clone(),
|
|
||||||
album.artist.id.to_string(),
|
|
||||||
String::new(),
|
|
||||||
)],
|
|
||||||
};
|
};
|
||||||
|
if credits.is_empty() && !is_placeholder_name(&album.artist.name) {
|
||||||
|
credits.push((
|
||||||
|
album.artist.name.clone(),
|
||||||
|
album.artist.id.to_string(),
|
||||||
|
String::new(),
|
||||||
|
));
|
||||||
|
}
|
||||||
// Album-level composer appended last, unless it's the localized
|
// Album-level composer appended last, unless it's the localized
|
||||||
// "Various Composers" placeholder (releaseArtistsMapper's VARIOUS drop).
|
// "Various Composers" placeholder (releaseArtistsMapper's VARIOUS drop).
|
||||||
if let Some(composer) = album.composer.as_ref().filter(|c| !c.name.is_empty()) {
|
if let Some(composer) = album
|
||||||
|
.composer
|
||||||
|
.as_ref()
|
||||||
|
.filter(|c| !c.name.is_empty() && !is_placeholder_name(&c.name))
|
||||||
|
{
|
||||||
if !composer.name.to_uppercase().contains("VARIOUS") {
|
if !composer.name.to_uppercase().contains("VARIOUS") {
|
||||||
credits.push((
|
credits.push((
|
||||||
composer.name.clone(),
|
composer.name.clone(),
|
||||||
@@ -542,6 +553,7 @@ pub(crate) fn featured_list(
|
|||||||
};
|
};
|
||||||
qbz_qobuz::performers::featured_artists(performers, artist, title)
|
qbz_qobuz::performers::featured_artists(performers, artist, title)
|
||||||
.into_iter()
|
.into_iter()
|
||||||
|
.filter(|name| !qbz_text_utils::names::is_placeholder_name(name))
|
||||||
.map(|name| {
|
.map(|name| {
|
||||||
let id = credits
|
let id = credits
|
||||||
.iter()
|
.iter()
|
||||||
@@ -636,8 +648,22 @@ pub async fn load_album(
|
|||||||
other => other.to_string(),
|
other => other.to_string(),
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
let artist = album.artist.name.clone();
|
// The header's artist: never a catalog filler — fall back to the first
|
||||||
let artist_id = album.artist.id.to_string();
|
// real name in `artists`, else nothing (the credits line still shows
|
||||||
|
// whatever real names exist).
|
||||||
|
let (artist, artist_id) = if qbz_text_utils::names::is_placeholder_name(&album.artist.name) {
|
||||||
|
album
|
||||||
|
.artists
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|list| {
|
||||||
|
list.iter()
|
||||||
|
.find(|a| !qbz_text_utils::names::is_placeholder_name(&a.name))
|
||||||
|
})
|
||||||
|
.map(|a| (a.name.clone(), a.id.to_string()))
|
||||||
|
.unwrap_or_default()
|
||||||
|
} else {
|
||||||
|
(album.artist.name.clone(), album.artist.id.to_string())
|
||||||
|
};
|
||||||
let credits = build_credits(&album);
|
let credits = build_credits(&album);
|
||||||
let date_display = format_release_date(
|
let date_display = format_release_date(
|
||||||
album
|
album
|
||||||
@@ -1168,7 +1194,9 @@ fn album_to_card(album: &Album) -> AlbumCardData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn card_artist(album: &Album) -> (String, String) {
|
fn card_artist(album: &Album) -> (String, String) {
|
||||||
if !album.artist.name.is_empty() {
|
if !album.artist.name.is_empty()
|
||||||
|
&& !qbz_text_utils::names::is_placeholder_name(&album.artist.name)
|
||||||
|
{
|
||||||
return (album.artist.name.clone(), album.artist.id.to_string());
|
return (album.artist.name.clone(), album.artist.id.to_string());
|
||||||
}
|
}
|
||||||
if let Some(list) = album.artists.as_ref() {
|
if let Some(list) = album.artists.as_ref() {
|
||||||
|
|||||||
@@ -65,8 +65,13 @@ fn map(track: Track) -> TrackInfoDoc {
|
|||||||
.map(|(role, names)| CreditRow {
|
.map(|(role, names)| CreditRow {
|
||||||
role: format_role_label(&role).to_uppercase(),
|
role: format_role_label(&role).to_uppercase(),
|
||||||
role_raw: role,
|
role_raw: role,
|
||||||
names,
|
// Catalog fillers ("Not Documented") are not people (2026-09-13).
|
||||||
|
names: names
|
||||||
|
.into_iter()
|
||||||
|
.filter(|name| !qbz_text_utils::names::is_placeholder_name(name))
|
||||||
|
.collect(),
|
||||||
})
|
})
|
||||||
|
.filter(|row| !row.names.is_empty())
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
TrackInfoDoc {
|
TrackInfoDoc {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
//! Frontend-agnostic text/date utilities extracted from the qbz binary.
|
//! Frontend-agnostic text/date utilities extracted from the qbz binary.
|
||||||
pub mod dates;
|
pub mod dates;
|
||||||
|
pub mod names;
|
||||||
pub mod sleep;
|
pub mod sleep;
|
||||||
pub mod strip_html;
|
pub mod strip_html;
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,66 @@
|
|||||||
|
//! Placeholder names the catalog uses where a real credit is missing.
|
||||||
|
//!
|
||||||
|
//! Qobuz ships literal fillers as artist / composer / performer names —
|
||||||
|
//! "Not Documented" is the one seen on album headers (2026-09-13), and the
|
||||||
|
//! usual "Unknown" / "N/A" family exists too. They are not people: a UI
|
||||||
|
//! that shows them prints junk beside the real artist and links to an
|
||||||
|
//! artist page that does not exist. Every credit list runs through
|
||||||
|
//! [`is_placeholder_name`] before it reaches a document.
|
||||||
|
|
||||||
|
/// True for a name the catalog uses as "no data": matched case-insensitively
|
||||||
|
/// on the trimmed text, punctuation-insensitive for the hyphenated forms.
|
||||||
|
pub fn is_placeholder_name(name: &str) -> bool {
|
||||||
|
let folded: String = name
|
||||||
|
.trim()
|
||||||
|
.chars()
|
||||||
|
.map(|c| if c == '-' || c == '_' { ' ' } else { c.to_ascii_lowercase() })
|
||||||
|
.collect::<String>()
|
||||||
|
.split_whitespace()
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join(" ");
|
||||||
|
matches!(
|
||||||
|
folded.as_str(),
|
||||||
|
"" | "not documented"
|
||||||
|
| "undocumented"
|
||||||
|
| "unknown"
|
||||||
|
| "unknown artist"
|
||||||
|
| "unknown composer"
|
||||||
|
| "n/a"
|
||||||
|
| "na"
|
||||||
|
| "none"
|
||||||
|
| "null"
|
||||||
|
| "no artist"
|
||||||
|
| "no name"
|
||||||
|
| "[unknown]"
|
||||||
|
| "(unknown)"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::is_placeholder_name;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn the_catalog_fillers_are_placeholders() {
|
||||||
|
for name in [
|
||||||
|
"Not Documented",
|
||||||
|
"not documented",
|
||||||
|
"NOT-DOCUMENTED",
|
||||||
|
" Undocumented ",
|
||||||
|
"Unknown",
|
||||||
|
"Unknown Artist",
|
||||||
|
"N/A",
|
||||||
|
"",
|
||||||
|
" ",
|
||||||
|
] {
|
||||||
|
assert!(is_placeholder_name(name), "{name:?}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn real_names_pass() {
|
||||||
|
for name in ["Stream Of Passion", "Various Artists", "Roy Orbison", "Non Documenté"] {
|
||||||
|
assert!(!is_placeholder_name(name), "{name:?}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user