diff options
author | Jesse Bannon <jbann1994@gmail.com> | 2023-09-09 00:46:26 -0700 |
---|---|---|
committer | GitHub <noreply@github.com> | 2023-09-09 09:46:26 +0200 |
commit | f72261e44f5501a870c43721883f1e4980003324 (patch) | |
tree | 820c0175701799bb8285799a32446bc6b4c663a2 | |
parent | b31ddf4f03a8bc04f26c73f7061b812bdc880c0b (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__.py | 33 | ||||
-rw-r--r-- | beets/autotag/hooks.py | 18 | ||||
-rw-r--r-- | beets/autotag/mb.py | 77 | ||||
-rw-r--r-- | beets/dbcore/types.py | 12 | ||||
-rw-r--r-- | beets/importer.py | 4 | ||||
-rw-r--r-- | beets/library.py | 15 | ||||
-rw-r--r-- | docs/changelog.rst | 2 | ||||
-rw-r--r-- | docs/reference/query.rst | 4 | ||||
-rwxr-xr-x | setup.py | 2 | ||||
-rw-r--r-- | test/test_autotag.py | 64 | ||||
-rw-r--r-- | test/test_edit.py | 4 | ||||
-rw-r--r-- | test/test_importer.py | 49 | ||||
-rw-r--r-- | test/test_info.py | 28 | ||||
-rw-r--r-- | test/test_library.py | 21 | ||||
-rw-r--r-- | test/test_mb.py | 168 | ||||
-rw-r--r-- | test/test_plugin_mediafield.py | 23 | ||||
-rw-r--r-- | test/test_query.py | 13 |
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 ------- @@ -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) |