Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

MBS-7646: Show localized artist aliases in more places #3191

Merged
merged 5 commits into from Apr 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
14 changes: 5 additions & 9 deletions lib/MusicBrainz/Server/Controller/Artist.pm
Expand Up @@ -146,9 +146,9 @@ after 'load' => sub
}
}

$c->model('ArtistType')->load($artist, map { $_->target } @{ $artist->relationships_by_type('artist') });
$c->model('Gender')->load($artist);
$c->model('Area')->load($artist);
my $lang = $c->stash->{current_language} // 'en';
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm pretty clueless about how this c (context?) stash stuff works. I'm passing the language around since current_language didn't seem to be available in load_related_info and the indexed search code, but please let me know if there's an easier way to get at it there.

$c->model('Artist')->load_related_info([$artist], $lang);
$c->model('ArtistType')->load(map { $_->target } @{ $artist->relationships_by_type('artist') });
$c->model('Area')->load_containment($artist->area, $artist->begin_area, $artist->end_area);
};

Expand Down Expand Up @@ -286,26 +286,22 @@ sub show : PathPart('') Chained('load')
if (defined $base_name) {
$c->model('Relationship')->load_subset(['artist'], $base_name);
$c->stash( legal_name => $base_name );
my $aliases = $c->model('Artist')->alias->find_by_entity_id($base_name->id);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it safe to assume that load_related_info will have saved the aliases to the Artist object whenever this is called, or should I include fallback code to try to load aliases if there aren't any?

It seems to work to the extent that I still see legal names when I view artist pages, at least. (Those weren't being shown with this change until I added the $c->model('Artist')->alias_type->load(@aliases) call in load_related_info.)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it safe to assume that load_related_info will have saved the aliases to the Artist object whenever this is called, or should I include fallback code to try to load aliases if there aren't any?

It looks safe to me since load_related_info is called in after 'load', while show uses Chained('load').

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe this isn't the case. https://beta.musicbrainz.org/artist/869cf6cc-07bb-470a-aa30-7e7e6000ffcf currently fails to load:

Can't use an undefined value as an ARRAY reference at lib/MusicBrainz/Server/Controller/Artist.pm line 295. at lib/MusicBrainz/Server/Controller/Artist.pm line 295
Catalyst::dispatch(?) called at lib/MusicBrainz/Server.pm line 400

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It works locally running the beta code, which suggests to me that the product of a prod load without aliases is being cached and causing the beta crash. If that's the case, we could either amend prod to save the info to the artist even if it doesn't use it, or amend beta temporarily to just show nothing rather than ISE if the data is missing. My preference would be the first, if we think it's safe (we're supposed to hotfix prod soon anyway for unrelated reasons)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mwiencek created #3252 to fix this.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mwiencek and @reosarevok, just to make sure I understand this properly, will the caching also cause issues with the primary_alias attribute on Entity::Artist? The alias is selected based on the UI language. Is that language incorporated into the cache keys, or will there be issues if an entity is cached for a request from a French-using user and then later reused for a request from an English-using user?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That shouldn't be an issue as entities are cached immediately before any additional data is loaded onto them. So no primary_alias value should be set on the cached copies.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Whew, thanks!

$c->model('Artist')->alias_type->load(@$aliases);
my @aliases = uniq map { $_->name }
sort_by { $coll->getSortKey($_->name) }
# An alias equal to the artist name already shown isn't useful
grep { ($_->name) ne $base_name->name }
# A legal name alias marked ended isn't a current legal name
grep { !($_->ended) }
grep { ($_->type_name // '') eq 'Legal name' } @$aliases;
grep { ($_->type_name // '') eq 'Legal name' } @{ $base_name->aliases };
$c->stash( legal_name_artist_aliases => \@aliases );
$base_name_legal_name_aliases = \@aliases;
push(@identities, $base_name);
} else {
my $aliases = $c->model('Artist')->alias->find_by_entity_id($artist->id);
$c->model('Artist')->alias_type->load(@$aliases);
my @aliases = uniq map { $_->name }
sort_by { $coll->getSortKey($_->name) }
# A legal name alias marked ended isn't a current legal name
grep { !($_->ended) }
grep { ($_->type_name // '') eq 'Legal name' } @$aliases;
grep { ($_->type_name // '') eq 'Legal name' } @{ $artist->aliases };
$c->stash( legal_name_aliases => \@aliases );
$legal_name_aliases = \@aliases;
}
Expand Down
10 changes: 6 additions & 4 deletions lib/MusicBrainz/Server/Controller/Search.pm
Expand Up @@ -125,9 +125,8 @@ sub direct : Private
my @entities = map { $_->entity } @$results;

if ($type eq 'artist') {
$c->model('ArtistType')->load(@entities);
$c->model('Area')->load(@entities);
$c->model('Gender')->load(@entities);
my $lang = $c->stash->{current_language} // 'en';
$c->model('Artist')->load_related_info(\@entities, $lang);
}
elsif ($type eq 'editor') {
$c->model('Editor')->load_preferences(@entities);
Expand Down Expand Up @@ -245,12 +244,15 @@ sub do_external_search {
my $query = $opts{query};
my $type = $opts{type};

my $lang = $c->stash->{current_language} // 'en';

my $search = $c->model('Search');
my $ret = $search->external_search($type,
$query,
$limit,
$page,
$advanced);
$advanced,
$lang);

if (exists $ret->{error})
{
Expand Down
2 changes: 1 addition & 1 deletion lib/MusicBrainz/Server/Controller/Series.pm
Expand Up @@ -72,7 +72,7 @@ sub show : PathPart('') Chained('load') {
}

if ($series->type->item_entity_type eq 'artist') {
$c->model('Artist')->load_related_info(@entities);
$c->model('Artist')->load_related_info(\@entities);
$c->model('Artist')->load_meta(@entities);
$c->model('Artist')->rating->load_user_ratings($c->user->id, @entities) if $c->user_exists;
}
Expand Down
19 changes: 18 additions & 1 deletion lib/MusicBrainz/Server/Data/Artist.pm
Expand Up @@ -14,6 +14,7 @@ use MusicBrainz::Server::Data::Utils qw(
add_partial_date_to_row
conditional_merge_column_query
contains_string
find_best_primary_alias
hash_to_row
load_subobjects
merge_table_attributes
Expand Down Expand Up @@ -465,12 +466,28 @@ sub _hash_to_row
}

sub load_related_info {
my ($self, @artists) = @_;
my ($self, $artists_ref, $user_lang) = @_;

my $c = $self->c;
my @artists = @{$artists_ref};
$c->model('ArtistType')->load(@artists);
$c->model('Gender')->load(@artists);
$c->model('Area')->load(@artists);

# Load and save aliases so Controller::Artist::show can use them later.
my $artist_aliases = $c->model('Artist')->alias->find_by_entity_ids(
map { $_->id } @artists,
);
my @all_aliases = map { @$_ } values %$artist_aliases;
$c->model('Artist')->alias_type->load(@all_aliases);
for my $artist (@artists) {
my @aliases = @{ $artist_aliases->{$artist->id} };
$artist->aliases(\@aliases);
if (defined $user_lang) {
my $best_alias = find_best_primary_alias(\@aliases, $user_lang);
$artist->primary_alias($best_alias->name) if defined $best_alias;
}
}
}

sub load_meta
Expand Down
37 changes: 30 additions & 7 deletions lib/MusicBrainz/Server/Data/Search.pm
Expand Up @@ -11,6 +11,7 @@ use Data::Dumper;
use Data::Page;
use URI::Escape qw( uri_escape_utf8 );
use List::AllUtils qw( any partition_by );
use MusicBrainz::Server::Entity::Alias;
use MusicBrainz::Server::Entity::Annotation;
use MusicBrainz::Server::Entity::Area;
use MusicBrainz::Server::Entity::AreaType;
Expand Down Expand Up @@ -58,7 +59,11 @@ use MusicBrainz::Server::Data::Release;
use MusicBrainz::Server::Data::ReleaseGroup;
use MusicBrainz::Server::Data::Series;
use MusicBrainz::Server::Data::Tag;
use MusicBrainz::Server::Data::Utils qw( contains_string ref_to_type );
use MusicBrainz::Server::Data::Utils qw(
contains_string
find_best_primary_alias
ref_to_type
);
use MusicBrainz::Server::Data::Work;
use MusicBrainz::Server::Constants qw( entities_with $DARTIST_ID $DLABEL_ID );
use MusicBrainz::Server::Data::Utils qw( type_to_model );
Expand Down Expand Up @@ -419,7 +424,7 @@ sub schema_fixup_type {
# matches up with the data returned from the DB for easy object creation
sub schema_fixup
{
my ($self, $data, $type) = @_;
my ($self, $data, $type, $user_lang) = @_;

return unless (ref($data) eq 'HASH');

Expand Down Expand Up @@ -685,7 +690,7 @@ sub schema_fixup

my %entity = %{ $rel->{$entity_type} };

$self->schema_fixup(\%entity, $entity_type);
$self->schema_fixup(\%entity, $entity_type, $user_lang);

# The search server returns the MBID in the 'id' attribute, so we
# need to delete that. (`schema_fixup` copies it to gid.)
Expand Down Expand Up @@ -716,13 +721,13 @@ sub schema_fixup
next if $k eq '_extra';
if (ref($data->{$k}) eq 'HASH')
{
$self->schema_fixup($data->{$k}, $type);
$self->schema_fixup($data->{$k}, $type, $user_lang);
}
if (ref($data->{$k}) eq 'ARRAY')
{
foreach my $item (@{$data->{$k}})
{
$self->schema_fixup($item, $type);
$self->schema_fixup($item, $type, $user_lang);
}
}
}
Expand All @@ -740,6 +745,24 @@ sub schema_fixup
$data->{'artist_credit'} = MusicBrainz::Server::Entity::ArtistCredit->new( { names => \@credits } );
}

if (defined $data->{aliases}) {
my @aliases = map {
MusicBrainz::Server::Entity::Alias->new(
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems to work, but please let me know if I should be doing something else to create these objects.

name => $_->{name} // '',
sort_name => $_->{sort_name} // '',
locale => $_->{locale} // '',
primary_for_locale => $_->{primary},
)
} @{ $data->{aliases} };
my $best_alias = find_best_primary_alias(\@aliases, $user_lang);
$data->{primary_alias} = $best_alias->{name} if defined $best_alias;

# Save the new objects so validation will pass, but note that the search
# API doesn't include all attributes that are present in Entity::Alias,
# e.g. no IDs.
$data->{aliases} = \@aliases;
}

if ($type eq 'work') {
if (defined $data->{relationships}) {
my %relationship_map = partition_by { $_->entity1->gid }
Expand Down Expand Up @@ -822,7 +845,7 @@ sub escape_query

sub external_search
{
my ($self, $type, $query, $limit, $page, $adv) = @_;
my ($self, $type, $query, $limit, $page, $adv, $user_lang) = @_;

my $entity_model = $self->c->model( type_to_model($type) )->_entity_class;
load_class($entity_model);
Expand Down Expand Up @@ -906,7 +929,7 @@ sub external_search

foreach my $t (@{$data->{$xmltype}})
{
$self->schema_fixup($t, $type);
$self->schema_fixup($t, $type, $user_lang);
push @results, MusicBrainz::Server::Entity::SearchResult->new(
position => $pos++,
score => $t->{score},
Expand Down
30 changes: 30 additions & 0 deletions lib/MusicBrainz/Server/Data/Utils.pm
Expand Up @@ -44,6 +44,7 @@ our @EXPORT_OK = qw(
copy_escape
coordinates_to_hash
datetime_to_iso8601
find_best_primary_alias
generate_gid
generate_token
get_area_containment_join
Expand Down Expand Up @@ -770,6 +771,35 @@ sub contains_string {
return any { $_ eq $string } @$array_ref;
}

# Given an array of MusicBrainz::Server::Entity::Alias objects and the UI
# language, try to find a localized primary alias to display to the user.
sub find_best_primary_alias {
my ($aliases_ref, $lang) = @_;

my $short_lang = substr($lang, 0, 2);
my ($best, $fallback);
foreach my $alias (@$aliases_ref) {
next if !defined $alias->locale || !$alias->primary_for_locale;

# If we find an exact match for the user's language, use it.
return $alias if $alias->locale eq $lang;

# Otherwise, favor more-generic aliases (e.g. "en" over "en_US").
if (substr($alias->locale, 0, 2) eq $short_lang &&
(!defined $best || length($alias->locale) == 2)) {
$best = $alias;
}
# If we find an English alias, use it as a fallback. This is likely
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I figured it'd be worthwhile falling back to English here since it looks like all of the UI languages that are presently listed use the Latin script.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I mentioned it earlier, but a (future?) downside of this is that if e.g. a user selects Japanese as their UI language (I don't think they can at present), then they'll always see English aliases for Japanese artists, since I don't think there's any way to know that the artist entity's name is already in Japanese.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You could in theory check if there's a Japanese artist alias that exactly matches the artist name (but you're right, if that data doesn't exist).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, in practice I don't think that many editors add primary aliases that are identical to entity names. https://musicbrainz.org/artist/9ddd7abc-9e1b-471d-8031-583bc6bc8be9/aliases has a zillion aliases, but of the two matching the "Пётр Ильич Чайковский" entity name, one is a non-primary legal name for Russian and the other is a search hint. Oddly, there's a primary artist name Russian alias of "Пётр Чайковский".

# better than nothing in many cases, since English aliases often apply
# to all Latin-script languages.
if (substr($alias->locale, 0, 2) eq 'en' &&
(!defined $fallback || length($alias->locale) == 2)) {
$fallback = $alias;
}
}
return $best || $fallback;
}

1;

=head1 COPYRIGHT AND LICENSE
Expand Down
12 changes: 12 additions & 0 deletions lib/MusicBrainz/Server/Entity/Artist.pm
Expand Up @@ -25,6 +25,17 @@ has 'sort_name' => (
isa => 'Str',
);

has 'aliases' => (
is => 'rw',
isa => 'ArrayRef[MusicBrainz::Server::Entity::Alias]',
default => sub { [] },
);

has 'primary_alias' => (
is => 'rw',
isa => 'Str',
);

has 'gender_id' => (
is => 'rw',
isa => 'Int',
Expand Down Expand Up @@ -82,6 +93,7 @@ around TO_JSON => sub {
$self->begin_area ? (begin_area => $self->begin_area->TO_JSON) : (),
$self->end_area ? (end_area => $self->end_area->TO_JSON) : (),
$self->gender ? (gender => $self->gender->TO_JSON) : (),
$self->primary_alias ? (primaryAlias => $self->primary_alias) : (),
begin_area_id => $self->begin_area_id,
end_area_id => $self->end_area_id,
gender_id => $self->gender_id,
Expand Down
1 change: 1 addition & 0 deletions lib/MusicBrainz/Server/WebService/JSONSerializer.pm
Expand Up @@ -221,6 +221,7 @@ sub _with_primary_alias {

$renderer //= \&get_entity_json;

# TODO: Consider updating this function to call find_best_primary_alias().
my @output;
if (@$results) {
my $munge_lang = sub {
Expand Down
46 changes: 36 additions & 10 deletions root/static/scripts/common/components/EntityLink.js
Expand Up @@ -13,6 +13,7 @@ import * as React from 'react';

import type {ReleaseEditorTrackT} from '../../release-editor/types.js';
import isGreyedOut from '../../url/utility/isGreyedOut.js';
import commaOnlyList from '../i18n/commaOnlyList.js';
import localizeAreaName from '../i18n/localizeAreaName.js';
import localizeInstrumentName from '../i18n/localizeInstrumentName.js';
import bracketed, {bracketedText} from '../utility/bracketed.js';
Expand Down Expand Up @@ -74,16 +75,30 @@ const iconClassPicker = {
};

const Comment = ({
alias,
className,
comment,
}: {+className: string, +comment: string}) => (
<>
{' '}
<span className={className}>
{bracketed(<bdi key="comment">{comment}</bdi>)}
</span>
</>
);
}: {+alias?: string, +className: string, +comment: string}) => {
const aliasElement = nonEmpty(alias)
? <i title={l('Primary alias')}>{alias}</i>
: null;
return (
<>
{' '}
<span className={className}>
{bracketed(
<bdi key="comment">
{nonEmpty(aliasElement) ? (
nonEmpty(comment) ? (
commaOnlyList([aliasElement, comment])
) : aliasElement
) : comment}
</bdi>,
)}
</span>
</>
);
};

const EventDisambiguation = ({
event,
Expand Down Expand Up @@ -428,9 +443,20 @@ $ReadOnlyArray<Expand2ReactOutput> | Expand2ReactOutput | null => {
/>,
);
}
if (comment) {
const primaryAlias =
(entity.entityType !== 'track' &&
nonEmpty(entity.primaryAlias) &&
entity.primaryAlias !== entityName)
? entity.primaryAlias
: '';
if (nonEmpty(comment) || nonEmpty(primaryAlias)) {
parts.push(
<Comment className="comment" comment={comment} key="comment" />,
<Comment
alias={primaryAlias}
className="comment"
comment={comment}
key="comment"
/>,
);
}
if (entity.entityType === 'area') {
Expand Down
1 change: 1 addition & 0 deletions t/selenium.mjs
Expand Up @@ -650,6 +650,7 @@ const seleniumTests = [
{name: 'MBS-2604.json5', login: true},
{name: 'MBS-5387.json5', login: true},
{name: 'MBS-7456.json5', login: true},
{name: 'MBS-7646.json5'},
{name: 'MBS-8952.json5', login: true},
{name: 'MBS-9396.json5', login: true},
{name: 'MBS-9548.json5'},
Expand Down