summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJesse Bannon <jbann1994@gmail.com>2023-09-09 00:46:26 -0700
committerGitHub <noreply@github.com>2023-09-09 09:46:26 +0200
commitf72261e44f5501a870c43721883f1e4980003324 (patch)
tree820c0175701799bb8285799a32446bc6b4c663a2
parentb31ddf4f03a8bc04f26c73f7061b812bdc880c0b (diff)
Add support for artists and albumartists multi-valued tags (#4743)
Adds the following fields with id3v2.4 multi-valued tag support to autotag: - artists, artists_sort, artists_credit - albumartists, albumartists_sort, albumartists_credit - mb_artistids, mb_albumartistids MusicBrainz support to populate + write the above multi-valued tags by default. Can be toggled to use id3v2.3 or id3v2.4 tags via the existing beets configuration option `id3v23`. Big thanks to @JOJ0, @OxygenCobalt, @arsaboo for testing + @sampsyo for the initial code review .
-rw-r--r--beets/autotag/__init__.py33
-rw-r--r--beets/autotag/hooks.py18
-rw-r--r--beets/autotag/mb.py77
-rw-r--r--beets/dbcore/types.py12
-rw-r--r--beets/importer.py4
-rw-r--r--beets/library.py15
-rw-r--r--docs/changelog.rst2
-rw-r--r--docs/reference/query.rst4
-rwxr-xr-xsetup.py2
-rw-r--r--test/test_autotag.py64
-rw-r--r--test/test_edit.py4
-rw-r--r--test/test_importer.py49
-rw-r--r--test/test_info.py28
-rw-r--r--test/test_library.py21
-rw-r--r--test/test_mb.py168
-rw-r--r--test/test_plugin_mediafield.py23
-rw-r--r--test/test_query.py13
17 files changed, 511 insertions, 26 deletions
diff --git a/beets/autotag/__init__.py b/beets/autotag/__init__.py
index 339e3826c..59b62385f 100644
--- a/beets/autotag/__init__.py
+++ b/beets/autotag/__init__.py
@@ -41,6 +41,7 @@ SPECIAL_FIELDS = {
'va',
'releasegroup_id',
'artist_id',
+ 'artists_ids',
'album_id',
'mediums',
'tracks',
@@ -48,21 +49,28 @@ SPECIAL_FIELDS = {
'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',
@@ -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.
@@ -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_':
@@ -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.
diff --git a/beets/autotag/hooks.py b/beets/autotag/hooks.py
index 96208dd0c..8d17e5729 100644
--- a/beets/autotag/hooks.py
+++ b/beets/autotag/hooks.py
@@ -74,8 +74,11 @@ class AlbumInfo(AttrDict):
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,
asin: Optional[str] = None,
albumtype: Optional[str] = None,
+ albumtypes: Optional[List[str]] = None,
va: bool = False,
year: Optional[int] = None,
month: Optional[int] = None,
@@ -83,6 +86,7 @@ class AlbumInfo(AttrDict):
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,
@@ -96,6 +100,7 @@ class AlbumInfo(AttrDict):
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,
@@ -110,9 +115,12 @@ class AlbumInfo(AttrDict):
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 []
self.va = va
self.year = year
self.month = month
@@ -120,6 +128,7 @@ class AlbumInfo(AttrDict):
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
@@ -133,6 +142,7 @@ class AlbumInfo(AttrDict):
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
@@ -190,14 +200,18 @@ class TrackInfo(AttrDict):
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,
@@ -220,6 +234,8 @@ class TrackInfo(AttrDict):
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
@@ -227,8 +243,10 @@ class TrackInfo(AttrDict):
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
diff --git a/beets/autotag/mb.py b/beets/autotag/mb.py
index 8fb8302e0..11a476e49 100644
--- a/beets/autotag/mb.py
+++ b/beets/autotag/mb.py
@@ -29,6 +29,7 @@ from beets import util
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
@@ -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 = []
@@ -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', ()))
@@ -206,12 +210,42 @@ def _flatten_artist_credit(credit: List[Dict]) -> Tuple[str, str, str]:
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]:
+ """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.
@@ -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(
@@ -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'
@@ -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']),
)
diff --git a/beets/dbcore/types.py b/beets/dbcore/types.py
index 7398731af..63829fcf6 100644
--- a/beets/dbcore/types.py
+++ b/beets/dbcore/types.py
@@ -14,7 +14,6 @@
"""Representation of type information for DBCore model fields.
"""
-
from abc import ABC
import sys
import typing
@@ -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)
@@ -326,3 +325,6 @@ NULL_FLOAT = NullFloat()
STRING = String()
BOOLEAN = Boolean()
SEMICOLON_SPACE_DSV = DelimitedString(delimiter='; ')
+
+# Will set the proper null char in mediafile
+MULTI_VALUE_DSV = DelimitedString(delimiter='\\␀')
diff --git a/beets/importer.py b/beets/importer.py
index 67f111163..b00919404 100644
--- a/beets/importer.py
+++ b/beets/importer.py
@@ -741,8 +741,12 @@ class ImportTask(BaseImportTask):
# 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:
diff --git a/beets/library.py b/beets/library.py
index 405b546e2..ccd431b85 100644
--- a/beets/library.py
+++ b/beets/library.py
@@ -483,13 +483,20 @@ class Item(LibModel):
'title': types.STRING,
'artist': types.STRING,
+ 'artists': types.MULTI_VALUE_DSV,
+ 'artists_ids': types.MULTI_VALUE_DSV,
'artist_sort': types.STRING,
+ 'artists_sort': types.MULTI_VALUE_DSV,
'artist_credit': types.STRING,
+ 'artists_credit': types.MULTI_VALUE_DSV,
'remixer': types.STRING,
'album': types.STRING,
'albumartist': types.STRING,
+ 'albumartists': types.MULTI_VALUE_DSV,
'albumartist_sort': types.STRING,
+ 'albumartists_sort': types.MULTI_VALUE_DSV,
'albumartist_credit': types.STRING,
+ 'albumartists_credit': types.MULTI_VALUE_DSV,
'genre': types.STRING,
'style': types.STRING,
'discogs_albumid': types.INTEGER,
@@ -517,7 +524,9 @@ class Item(LibModel):
'mb_trackid': types.STRING,
'mb_albumid': types.STRING,
'mb_artistid': types.STRING,
+ 'mb_artistids': types.MULTI_VALUE_DSV,
'mb_albumartistid': types.STRING,
+ 'mb_albumartistids': types.MULTI_VALUE_DSV,
'mb_releasetrackid': types.STRING,
'trackdisambig': types.STRING,
'albumtype': types.STRING,
@@ -1070,6 +1079,9 @@ class Album(LibModel):
'albumartist': types.STRING,
'albumartist_sort': types.STRING,
'albumartist_credit': types.STRING,
+ 'albumartists': types.MULTI_VALUE_DSV,
+ 'albumartists_sort': types.MULTI_VALUE_DSV,
+ 'albumartists_credit': types.MULTI_VALUE_DSV,
'album': types.STRING,
'genre': types.STRING,
'style': types.STRING,
@@ -1120,8 +1132,11 @@ class Album(LibModel):
item_keys = [
'added',
'albumartist',
+ 'albumartists',
'albumartist_sort',
+ 'albumartists_sort',
'albumartist_credit',
+ 'albumartists_credit',
'album',
'genre',
'style',
diff --git a/docs/changelog.rst b/docs/changelog.rst
index 4ceeb1874..1293e6de1 100644
--- a/docs/changelog.rst
+++ b/docs/changelog.rst
@@ -119,6 +119,8 @@ New features:
to the `rewrite` plugin. The main difference between them being that
`rewrite` modifies files' metadata and `substitute` does not.
:bug:`2786`
+* Add support for ``artists`` and ``albumartists`` multi-valued tags.
+ :bug:`505`
Bug fixes:
diff --git a/docs/reference/query.rst b/docs/reference/query.rst
index a3d7944bf..2bed2ed68 100644
--- a/docs/reference/query.rst
+++ b/docs/reference/query.rst
@@ -79,6 +79,10 @@ one that matches albums by year::
Recall that ``-a`` makes the ``list`` command show albums instead of individual
tracks, so this command shows me all the releases I have from this year.
+For multi-valued tags (such as ``artists`` or ``albumartists``), a regular
+expression search must be used to search for a single value within the
+multi-valued tag.
+
Phrases
-------
diff --git a/setup.py b/setup.py
index 729d5003f..26eb8048a 100755
--- a/setup.py
+++ b/setup.py
@@ -88,7 +88,7 @@ setup(
'unidecode>=1.3.6',
'musicbrainzngs>=0.4',
'pyyaml',
- 'mediafile>=0.9.0',
+ 'mediafile>=0.12.0',
'confuse>=1.5.0',
'munkres>=1.0.0',
'jellyfish',
diff --git a/test/test_autotag.py b/test/test_autotag.py
index 2314b42e0..ae607cb19 100644
--- a/test/test_autotag.py
+++ b/test/test_autotag.py
@@ -628,7 +628,9 @@ class ApplyTest(_common.TestCase, ApplyTestUtil):
medium_total=1,
index=1,
artist_credit='trackArtistCredit',
+ artists_credit=['trackArtistCredit'],
artist_sort='trackArtistSort',
+ artists_sort=['trackArtistSort'],
))
trackinfo.append(TrackInfo(
title='twoNew',
@@ -641,11 +643,18 @@ class ApplyTest(_common.TestCase, ApplyTestUtil):
self.info = AlbumInfo(
tracks=trackinfo,
artist='artistNew',
+ artists=['artistNew', 'artistNew2'],
album='albumNew',
album_id='7edb51cb-77d6-4416-a23c-3a8c2994a2c7',
artist_id='a6623d39-2d8e-4f70-8242-0a9553b91e50',
+ artists_ids=[
+ 'a6623d39-2d8e-4f70-8242-0a9553b91e50',
+ 'a6623d39-2d8e-4f70-8242-0a9553b91e51'
+ ],
artist_credit='albumArtistCredit',
+ artists_credit=['albumArtistCredit', 'albumArtistCredit2'],
artist_sort='albumArtistSort',
+ artists_sort=['albumArtistSort', 'albumArtistSort2'],
albumtype='album',
va=False,
mediums=2,
@@ -662,6 +671,14 @@ class ApplyTest(_common.TestCase, ApplyTestUtil):
self.assertEqual(self.items[1].album, 'albumNew')
self.assertEqual(self.items[0].artist, 'artistNew')
self.assertEqual(self.items[1].artist, 'artistNew')
+ self.assertEqual(self.items[0].artists, ['artistNew', 'artistNew2'])
+ self.assertEqual(self.items[1].artists, ['artistNew', 'artistNew2'])
+ self.assertEqual(
+ self.items[0].albumartists, ['artistNew', 'artistNew2']
+ )
+ self.assertEqual(
+ self.items[1].albumartists, ['artistNew', 'artistNew2']
+ )
def test_track_index_applied(self):
self._apply()
@@ -699,6 +716,14 @@ class ApplyTest(_common.TestCase, ApplyTestUtil):
self.assertEqual(self.items[1].artist, 'albumArtistCredit')
self.assertEqual(self.items[0].albumartist, 'albumArtistCredit')
self.assertEqual(self.items[1].albumartist, 'albumArtistCredit')
+ self.assertEqual(
+ self.items[0].albumartists,
+ ['albumArtistCredit', 'albumArtistCredit2']
+ )
+ self.assertEqual(
+ self.items[1].albumartists,
+ ['albumArtistCredit', 'albumArtistCredit2']
+ )
def test_artist_credit_prefers_artist_over_albumartist_credit(self):
self.info.tracks[0].artist = 'oldArtist'
@@ -725,6 +750,13 @@ class ApplyTest(_common.TestCase, ApplyTestUtil):
'7edb51cb-77d6-4416-a23c-3a8c2994a2c7')
self.assertEqual(item.mb_artistid,
'a6623d39-2d8e-4f70-8242-0a9553b91e50')
+ self.assertEqual(
+ item.mb_artistids,
+ [
+ 'a6623d39-2d8e-4f70-8242-0a9553b91e50',
+ 'a6623d39-2d8e-4f70-8242-0a9553b91e51',
+ ]
+ )
def test_albumtype_applied(self):
self._apply()
@@ -736,28 +768,60 @@ class ApplyTest(_common.TestCase, ApplyTestUtil):
self._apply(info=my_info)
self.assertEqual(self.items[0].artist, 'artistNew')
self.assertEqual(self.items[1].artist, 'artistNew')
+ self.assertEqual(self.items[0].artists, ['artistNew', 'artistNew2'])
+ self.assertEqual(self.items[1].artists, ['artistNew', 'artistNew2'])
def test_album_artist_overridden_by_nonempty_track_artist(self):
my_info = self.info.copy()
my_info.tracks[0].artist = 'artist1!'
my_info.tracks[1].artist = 'artist2!'
+ my_info.tracks[0].artists = ['artist1!', 'artist1!!']
+ my_info.tracks[1].artists = ['artist2!', 'artist2!!']
self._apply(info=my_info)
self.assertEqual(self.items[0].artist, 'artist1!')
self.assertEqual(self.items[1].artist, 'artist2!')
+ self.assertEqual(self.items[0].artists, ['artist1!', 'artist1!!'])
+ self.assertEqual(self.items[1].artists, ['artist2!', 'artist2!!'])
def test_artist_credit_applied(self):
self._apply()
self.assertEqual(self.items[0].albumartist_credit, 'albumArtistCredit')
+ self.assertEqual(
+ self.items[0].albumartists_credit,
+ ['albumArtistCredit', 'albumArtistCredit2']
+ )
self.assertEqual(self.items[0].artist_credit, 'trackArtistCredit')
+ self.assertEqual(self.items[0].artists_credit, ['trackArtistCredit'])
self.assertEqual(self.items[1].albumartist_credit, 'albumArtistCredit')
+ self.assertEqual(
+ self.items[1].albumartists_credit,
+ ['albumArtistCredit', 'albumArtistCredit2']
+ )
self.assertEqual(self.items[1].artist_credit, 'albumArtistCredit')
+ self.assertEqual(
+ self.items[1].artists_credit,
+ ['albumArtistCredit', 'albumArtistCredit2']
+ )
def test_artist_sort_applied(self):
self._apply()
self.assertEqual(self.items[0].albumartist_sort, 'albumArtistSort')
+ self.assertEqual(
+ self.items[0].albumartists_sort,
+ ['albumArtistSort', 'albumArtistSort2']
+ )
self.assertEqual(self.items[0].artist_sort, 'trackArtistSort')
+ self.assertEqual(self.items[0].artists_sort, ['trackArtistSort'])
self.assertEqual(self.items[1].albumartist_sort, 'albumArtistSort')
+ self.assertEqual(
+ self.items[1].albumartists_sort,
+ ['albumArtistSort', 'albumArtistSort2']
+ )
self.assertEqual(self.items[1].artist_sort, 'albumArtistSort')
+ self.assertEqual(
+ self.items[1].artists_sort,
+ ['albumArtistSort', 'albumArtistSort2']
+ )
def test_full_date_applied(self):
my_info = self.info.copy()
diff --git a/test/test_edit.py b/test/test_edit.py
index 3926b2ebc..ad43ca839 100644
--- a/test/test_edit.py
+++ b/test/test_edit.py
@@ -312,7 +312,9 @@ class EditDuringImporterTest(TerminalImportSessionSetup, unittest.TestCase,
self.assertItemFieldsModified(self.lib.items(), self.items_orig,
['title'],
self.IGNORED + ['albumartist',
- 'mb_albumartistid'])
+ 'mb_albumartistid',
+ 'mb_albumartistids',
+ ])
self.assertTrue(all('Edited Title' in i.title
for i in self.lib.items()))
diff --git a/test/test_importer.py b/test/test_importer.py
index 1dc6705b7..233f9bc90 100644
--- a/test/test_importer.py
+++ b/test/test_importer.py
@@ -868,7 +868,7 @@ class ImportCompilationTest(_common.TestCase, ImportHelper):
for item in self.lib.items():
self.assertEqual(item.albumartist, 'Various Artists')
- def test_asis_heterogenous_sets_sompilation(self):
+ def test_asis_heterogenous_sets_compilation(self):
self.import_media[0].artist = 'Other Artist'
self.import_media[0].save()
self.import_media[1].artist = 'Another Artist'
@@ -908,6 +908,53 @@ class ImportCompilationTest(_common.TestCase, ImportHelper):
self.assertEqual(item.albumartist, 'Album Artist')
self.assertEqual(item.mb_albumartistid, 'Album Artist ID')
+ def test_asis_albumartists_tag_sets_multi_albumartists(self):
+ self.import_media[0].artist = 'Other Artist'
+ self.import_media[0].artists = ['Other Artist', 'Other Artist 2']
+ self.import_media[1].artist = 'Another Artist'
+ self.import_media[1].artists = ['Another Artist', 'Another Artist 2']
+ for mediafile in self.import_media:
+ mediafile.albumartist = 'Album Artist'
+ mediafile.albumartists = ['Album Artist 1', 'Album Artist 2']
+ mediafile.mb_albumartistid = 'Album Artist ID'
+ mediafile.save()
+
+ self.importer.add_choice(importer.action.ASIS)
+ self.importer.run()
+ self.assertEqual(self.lib.albums().get().albumartist, 'Album Artist')
+ self.assertEqual(
+ self.lib.albums().get().albumartists,
+ ['Album Artist 1', 'Album Artist 2']
+ )
+ self.assertEqual(self.lib.albums().get().mb_albumartistid,
+ 'Album Artist ID')
+
+ # Make sure both custom media items get tested
+ asserted_multi_artists_0 = False
+ asserted_multi_artists_1 = False
+ for item in self.lib.items():
+ self.assertEqual(item.albumartist, 'Album Artist')
+ self.assertEqual(
+ item.albumartists,
+ ['Album Artist 1', 'Album Artist 2']
+ )
+ self.assertEqual(item.mb_albumartistid, 'Album Artist ID')
+
+ if item.artist == "Other Artist":
+ asserted_multi_artists_0 = True
+ self.assertEqual(
+ item.artists,
+ ['Other Artist', 'Other Artist 2']
+ )
+ if item.artist == "Another Artist":
+ asserted_multi_artists_1 = True
+ self.assertEqual(
+ item.artists,
+ ['Another Artist', 'Another Artist 2']
+ )
+
+ self.assertTrue(asserted_multi_artists_0 and asserted_multi_artists_1)
+
class ImportExistingTest(_common.TestCase, ImportHelper):
"""Test importing files that are already in the library directory.
diff --git a/test/test_info.py b/test/test_info.py
index 94923a37f..929a83d80 100644
--- a/test/test_info.py
+++ b/test/test_info.py
@@ -90,6 +90,34 @@ class InfoTest(unittest.TestCase, TestHelper):
self.assertIn('title: [various]', out)
self.remove_mediafile_fixtures()
+ def test_collect_item_and_path_with_multi_values(self):
+ path = self.create_mediafile_fixture()
+ mediafile = MediaFile(path)
+ item, = self.add_item_fixtures()
+
+ item.album = mediafile.album = 'AAA'
+ item.tracktotal = mediafile.tracktotal = 5
+ item.title = 'TTT'
+ mediafile.title = 'SSS'
+
+ item.albumartists = ['Artist A', 'Artist B']
+ mediafile.albumartists = ['Artist C', 'Artist D']
+
+ item.artists = ['Artist A', 'Artist Z']
+ mediafile.artists = ['Artist A', 'Artist Z']
+
+ item.write()
+ item.store()
+ mediafile.save()
+
+ out = self.run_with_output('info', '--summarize', 'album:AAA', path)
+ self.assertIn('album: AAA', out)