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:
vicrodh committed 2026-09-13 23:31:59 -06:00
1 parent 7f609a55e5
commit e546e6614e
5 files changed
+112 -10

No files matched your search

+2
View File
@@ -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
+37 -9
View File
@@ -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() {
+6 -1
View File
@@ -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
View File
@@ -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;
+66
View File
@@ -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:?}");
}
}
}