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

Add support for artists and albumartists multi-valued tags #4743

Merged
merged 43 commits into from
Sep 9, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
4d92a53
Attempt at multi artist + album artist support
jmbannon Apr 4, 2023
5202734
multi start
jmbannon Apr 5, 2023
1d7c671
tests passing
jmbannon Apr 5, 2023
f856a20
broken now?
jmbannon Apr 5, 2023
3ad6031
missing s
jmbannon Apr 5, 2023
d133dbe
moar tests
jmbannon Apr 5, 2023
b3e2188
mediafile
jmbannon Apr 5, 2023
4454e53
simplify delimiter format
jmbannon Apr 5, 2023
faaeb02
fix import
jmbannon Apr 5, 2023
02e851c
super simplified
jmbannon Apr 5, 2023
0d35e0b
artists
jmbannon Apr 5, 2023
04b21c8
better type hinting
jmbannon Apr 5, 2023
60cb7b2
more tests
jmbannon Apr 5, 2023
9400885
write test
jmbannon Apr 6, 2023
bfa8e9b
id3v23 test
jmbannon Apr 6, 2023
7074848
changelog, docs
jmbannon Apr 6, 2023
ffc05a0
manual linting ugh
jmbannon Apr 6, 2023
457b0ec
Merge branch 'master' into j/multi
jmbannon Apr 8, 2023
e57798b
also write mb_artistids and mb_albumartistids
jmbannon Apr 8, 2023
974a022
Merge branch 'master' into j/multi
jmbannon Apr 11, 2023
ab3ca88
python <=3.8 null char incompatibility
jmbannon Apr 18, 2023
7051c80
Merge branch 'master' into j/multi
jmbannon Apr 22, 2023
32deac0
more lint
jmbannon Apr 25, 2023
5d4b248
Merge branch 'master' into j/multi
jmbannon Apr 25, 2023
145b5f5
Merge branch 'master' into j/multi
jmbannon May 5, 2023
04dda2a
Merge branch 'master' into j/multi
jmbannon May 15, 2023
d4e9ff2
apply albumartists to track
jmbannon May 17, 2023
eecdbb3
update autotag to use multis from albuminfo
jmbannon May 17, 2023
56d1258
Merge branch 'master' into j/multi
jmbannon May 23, 2023
d9cc1e5
missing s in artists_credits
jmbannon May 23, 2023
2dadf0d
Merge branch 'master' into j/multi
jmbannon May 27, 2023
e277bb5
Merge branch 'master' into j/multi
jmbannon May 31, 2023
18a48cc
album/artists_credits -> album/artists_credit
jmbannon Jun 27, 2023
e2bcf30
Merge branch 'master' into j/multi
jmbannon Jun 27, 2023
0d8d59b
lint
jmbannon Jul 3, 2023
d836f78
require higher mediafile for multi-tags
jmbannon Jul 5, 2023
f569c07
remove debug code
jmbannon Jul 5, 2023
b63e0a7
lint
jmbannon Jul 7, 2023
195f1fe
Merge branch 'master' into j/multi
jmbannon Jul 21, 2023
4f58b03
Merge branch 'master' into j/multi
jmbannon Aug 30, 2023
6ff82d0
Merge branch 'master' into j/multi
JOJ0 Sep 9, 2023
0242a06
Move #4743 changelog entry to bottom of list
JOJ0 Sep 9, 2023
bc8b467
Fix query docs wording for multi-valued tags searches
JOJ0 Sep 9, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions beets/autotag/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,28 +41,36 @@
'va',
'releasegroup_id',
'artist_id',
'artists_ids',
'album_id',
'mediums',
'tracks',
'year',
'month',
'day',
'artist',
'artists',
'artist_credit',
'artists_credit',
'artist_sort',
'artists_sort',
'data_url'
),
'track': (
'track_alt',
'artist_id',
'artists_ids',
'release_track_id',
'medium',
'index',
'medium_index',
'title',
'artist_credit',
'artists_credit',
'artist_sort',
'artists_sort',
'artist',
'artists',
'track_id',
'medium_total',
'data_url',
Expand All @@ -77,13 +85,18 @@ def apply_item_metadata(item: Item, track_info: TrackInfo):
"""Set an item's metadata from its matched TrackInfo object.
"""
item.artist = track_info.artist
item.artists = track_info.artists
item.artist_sort = track_info.artist_sort
item.artists_sort = track_info.artists_sort
item.artist_credit = track_info.artist_credit
item.artists_credit = track_info.artists_credit
item.title = track_info.title
item.mb_trackid = track_info.track_id
item.mb_releasetrackid = track_info.release_track_id
if track_info.artist_id:
item.mb_artistid = track_info.artist_id
if track_info.artists_ids:
item.mb_artistids = track_info.artists_ids

for field, value in track_info.items():
# We only overwrite fields that are not already hardcoded.
Expand All @@ -108,21 +121,34 @@ def apply_metadata(album_info: AlbumInfo, mapping: Mapping[Item, TrackInfo]):
track_info.artist or
album_info.artist_credit or
album_info.artist)
item.artists = (track_info.artists_credit or
track_info.artists or
album_info.artists_credit or
album_info.artists)
item.albumartist = (album_info.artist_credit or
album_info.artist)
item.albumartists = (album_info.artists_credit or
album_info.artists)
else:
item.artist = (track_info.artist or album_info.artist)
item.artists = (track_info.artists or album_info.artists)
item.albumartist = album_info.artist
item.albumartists = album_info.artists

# Album.
item.album = album_info.album

# Artist sort and credit names.
item.artist_sort = track_info.artist_sort or album_info.artist_sort
item.artists_sort = track_info.artists_sort or album_info.artists_sort
item.artist_credit = (track_info.artist_credit or
album_info.artist_credit)
item.artists_credit = (track_info.artists_credit or
album_info.artists_credit)
item.albumartist_sort = album_info.artist_sort
item.albumartists_sort = album_info.artists_sort
item.albumartist_credit = album_info.artist_credit
item.albumartists_credit = album_info.artists_credit

# Release date.
for prefix in '', 'original_':
Expand Down Expand Up @@ -174,7 +200,14 @@ def apply_metadata(album_info: AlbumInfo, mapping: Mapping[Item, TrackInfo]):
item.mb_artistid = track_info.artist_id
else:
item.mb_artistid = album_info.artist_id

if track_info.artists_ids:
item.mb_artistids = track_info.artists_ids
else:
item.mb_artistids = album_info.artists_ids

item.mb_albumartistid = album_info.artist_id
item.mb_albumartistids = album_info.artists_ids
item.mb_releasegroupid = album_info.releasegroup_id

# Compilation flag.
Expand Down
18 changes: 18 additions & 0 deletions beets/autotag/hooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,15 +74,19 @@ def __init__(
album_id: Optional[str] = None,
artist: Optional[str] = None,
artist_id: Optional[str] = None,
artists: Optional[List[str]] = None,
artists_ids: Optional[List[str]] = None,
Copy link
Member

Choose a reason for hiding this comment

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

FWIW, we've been gradually moving away from listing each field exhaustively in AlbumInfo/TrackInfo and instead just relying on **kwargs to convey all the information we need. But no big deal if it's easier to grok with these fields listed explicitly here.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

As boiler-plate as it is, I prefer it lol. For me, it helps to know which native field types are expected

asin: Optional[str] = None,
albumtype: Optional[str] = None,
albumtypes: Optional[List[str]] = None,
va: bool = False,
year: Optional[int] = None,
month: Optional[int] = None,
day: Optional[int] = None,
label: Optional[str] = None,
mediums: Optional[int] = None,
artist_sort: Optional[str] = None,
artists_sort: Optional[List[str]] = None,
releasegroup_id: Optional[str] = None,
release_group_title: Optional[str] = None,
catalognum: Optional[str] = None,
Expand All @@ -96,6 +100,7 @@ def __init__(
albumdisambig: Optional[str] = None,
releasegroupdisambig: Optional[str] = None,
artist_credit: Optional[str] = None,
artists_credit: Optional[List[str]] = None,
original_year: Optional[int] = None,
original_month: Optional[int] = None,
original_day: Optional[int] = None,
Expand All @@ -110,16 +115,20 @@ def __init__(
self.album_id = album_id
self.artist = artist
self.artist_id = artist_id
self.artists = artists or []
self.artists_ids = artists_ids or []
self.tracks = tracks
self.asin = asin
self.albumtype = albumtype
self.albumtypes = albumtypes or []
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 was missing from the other PR

self.va = va
self.year = year
self.month = month
self.day = day
self.label = label
self.mediums = mediums
self.artist_sort = artist_sort
self.artists_sort = artists_sort or []
self.releasegroup_id = releasegroup_id
self.release_group_title = release_group_title
self.catalognum = catalognum
Expand All @@ -133,6 +142,7 @@ def __init__(
self.albumdisambig = albumdisambig
self.releasegroupdisambig = releasegroupdisambig
self.artist_credit = artist_credit
self.artists_credit = artists_credit or []
self.original_year = original_year
self.original_month = original_month
self.original_day = original_day
Expand Down Expand Up @@ -190,14 +200,18 @@ def __init__(
release_track_id: Optional[str] = None,
artist: Optional[str] = None,
artist_id: Optional[str] = None,
artists: Optional[List[str]] = None,
artists_ids: Optional[List[str]] = None,
length: Optional[float] = None,
index: Optional[int] = None,
medium: Optional[int] = None,
medium_index: Optional[int] = None,
medium_total: Optional[int] = None,
artist_sort: Optional[str] = None,
artists_sort: Optional[List[str]] = None,
disctitle: Optional[str] = None,
artist_credit: Optional[str] = None,
artists_credit: Optional[List[str]] = None,
data_source: Optional[str] = None,
data_url: Optional[str] = None,
media: Optional[str] = None,
Expand All @@ -220,15 +234,19 @@ def __init__(
self.release_track_id = release_track_id
self.artist = artist
self.artist_id = artist_id
self.artists = artists or []
self.artists_ids = artists_ids or []
self.length = length
self.index = index
self.media = media
self.medium = medium
self.medium_index = medium_index
self.medium_total = medium_total
self.artist_sort = artist_sort
self.artists_sort = artists_sort or []
self.disctitle = disctitle
self.artist_credit = artist_credit
self.artists_credit = artists_credit or []
self.data_source = data_source
self.data_url = data_url
self.lyricist = lyricist
Expand Down
77 changes: 66 additions & 11 deletions beets/autotag/mb.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
from beets import config
from collections import Counter
from urllib.parse import urljoin

from beets.util.id_extractors import extract_discogs_id_regex, \
spotify_id_regex, deezer_id_regex, beatport_id_regex
from beets.plugins import MetadataSourcePlugin
Expand Down Expand Up @@ -166,9 +167,11 @@ def _preferred_release_event(release: Dict[str, Any]) -> Tuple[str, str]:
)


def _flatten_artist_credit(credit: List[Dict]) -> Tuple[str, str, str]:
"""Given a list representing an ``artist-credit`` block, flatten the
data into a triple of joined artist name strings: canonical, sort, and
def _multi_artist_credit(
credit: List[Dict], include_join_phrase: bool
) -> Tuple[List[str], List[str], List[str]]:
"""Given a list representing an ``artist-credit`` block, accumulate
data into a triple of joined artist name lists: canonical, sort, and
credit.
"""
artist_parts = []
Expand All @@ -177,9 +180,10 @@ def _flatten_artist_credit(credit: List[Dict]) -> Tuple[str, str, str]:
for el in credit:
if isinstance(el, str):
# Join phrase.
artist_parts.append(el)
artist_credit_parts.append(el)
artist_sort_parts.append(el)
if include_join_phrase:
artist_parts.append(el)
artist_credit_parts.append(el)
artist_sort_parts.append(el)

else:
alias = _preferred_alias(el['artist'].get('alias-list', ()))
Expand All @@ -205,13 +209,43 @@ def _flatten_artist_credit(credit: List[Dict]) -> Tuple[str, str, str]:
else:
artist_credit_parts.append(cur_artist_name)

return (
artist_parts,
artist_sort_parts,
artist_credit_parts,
)


def _flatten_artist_credit(credit: List[Dict]) -> Tuple[str, str, str]:
Copy link
Member

Choose a reason for hiding this comment

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

Really nice work on this refactoring. Clever and also simple!

"""Given a list representing an ``artist-credit`` block, flatten the
data into a triple of joined artist name strings: canonical, sort, and
credit.
"""
artist_parts, artist_sort_parts, artist_credit_parts = \
_multi_artist_credit(
credit,
include_join_phrase=True
)
return (
''.join(artist_parts),
''.join(artist_sort_parts),
''.join(artist_credit_parts),
)


def _artist_ids(credit: List[Dict]) -> List[str]:
"""
Given a list representing an ``artist-credit``,
return a list of artist IDs
"""
artist_ids: List[str] = []
for el in credit:
if isinstance(el, dict):
artist_ids.append(el['artist']['id'])

return artist_ids


def _get_related_artist_names(relations, relation_type):
"""Given a list representing the artist relationships extract the names of
the remixers and concatenate them.
Expand Down Expand Up @@ -255,9 +289,13 @@ def track_info(
info.artist, info.artist_sort, info.artist_credit = \
_flatten_artist_credit(recording['artist-credit'])

# Get the ID and sort name of the first artist.
artist = recording['artist-credit'][0]['artist']
info.artist_id = artist['id']
info.artists, info.artists_sort, info.artists_credit = \
_multi_artist_credit(
recording['artist-credit'], include_join_phrase=False
)

info.artists_ids = _artist_ids(recording['artist-credit'])
info.artist_id = info.artists_ids[0]

if recording.get('artist-relation-list'):
info.remixer = _get_related_artist_names(
Expand Down Expand Up @@ -350,6 +388,11 @@ def album_info(release: Dict) -> beets.autotag.hooks.AlbumInfo:
artist_name, artist_sort_name, artist_credit_name = \
_flatten_artist_credit(release['artist-credit'])

artists_names, artists_sort_names, artists_credit_names = \
_multi_artist_credit(
release['artist-credit'], include_join_phrase=False
)

ntracks = sum(len(m['track-list']) for m in release['medium-list'])

# The MusicBrainz API omits 'artist-relation-list' and 'work-relation-list'
Expand Down Expand Up @@ -421,21 +464,33 @@ def album_info(release: Dict) -> beets.autotag.hooks.AlbumInfo:
# Get the artist names.
ti.artist, ti.artist_sort, ti.artist_credit = \
_flatten_artist_credit(track['artist-credit'])
ti.artist_id = track['artist-credit'][0]['artist']['id']

ti.artists, ti.artists_sort, ti.artists_credit = \
_multi_artist_credit(
track['artist-credit'], include_join_phrase=False
)

ti.artists_ids = _artist_ids(track['artist-credit'])
ti.artist_id = ti.artists_ids[0]
if track.get('length'):
ti.length = int(track['length']) / (1000.0)

track_infos.append(ti)

album_artist_ids = _artist_ids(release['artist-credit'])
info = beets.autotag.hooks.AlbumInfo(
album=release['title'],
album_id=release['id'],
artist=artist_name,
artist_id=release['artist-credit'][0]['artist']['id'],
artist_id=album_artist_ids[0],
artists=artists_names,
artists_ids=album_artist_ids,
tracks=track_infos,
mediums=len(release['medium-list']),
artist_sort=artist_sort_name,
artists_sort=artists_sort_names,
artist_credit=artist_credit_name,
artists_credit=artists_credit_names,
data_source='MusicBrainz',
data_url=album_url(release['id']),
)
Expand Down
12 changes: 7 additions & 5 deletions beets/dbcore/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@

"""Representation of type information for DBCore model fields.
"""

from abc import ABC
import sys
import typing
Expand Down Expand Up @@ -287,18 +286,18 @@ class DelimitedString(BaseString[List[str], List[str]]):
"""
model_type = list

def __init__(self, delimiter):
def __init__(self, delimiter: str):
self.delimiter = delimiter

def format(self, value):
def format(self, value: List[str]):
return self.delimiter.join(value)

def parse(self, string):
def parse(self, string: str):
if not string:
return []
return string.split(self.delimiter)

def to_sql(self, model_value):
def to_sql(self, model_value: List[str]):
return self.delimiter.join(model_value)


Expand Down Expand Up @@ -326,3 +325,6 @@ def parse(self, string: str) -> bool:
STRING = String()
BOOLEAN = Boolean()
SEMICOLON_SPACE_DSV = DelimitedString(delimiter='; ')

# Will set the proper null char in mediafile
MULTI_VALUE_DSV = DelimitedString(delimiter='\\␀')
4 changes: 4 additions & 0 deletions beets/importer.py
Original file line number Diff line number Diff line change
Expand Up @@ -741,8 +741,12 @@ def align_album_level_fields(self):
# item.
if not self.items[0].albumartist:
changes['albumartist'] = self.items[0].artist
if not self.items[0].albumartists:
changes['albumartists'] = self.items[0].artists
if not self.items[0].mb_albumartistid:
changes['mb_albumartistid'] = self.items[0].mb_artistid
if not self.items[0].mb_albumartistids:
changes['mb_albumartistids'] = self.items[0].mb_artistids

# Apply new metadata.
for item in self.items:
Expand Down
Loading
Loading