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

Implement multi-value genres tag #5426

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 4 additions & 0 deletions beets/autotag/hooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@
return id(self)


class AlbumInfo(AttrDict):

Check failure on line 59 in beets/autotag/hooks.py

View workflow job for this annotation

GitHub Actions / Check types with mypy

Missing type parameters for generic type "AttrDict"
"""Describes a canonical release that may be used to match a release
in the library. Consists of these data members:

Expand Down Expand Up @@ -100,6 +100,7 @@
country: str | None = None,
style: str | None = None,
genre: str | None = None,
genres: str | None = None,
albumstatus: str | None = None,
media: str | None = None,
albumdisambig: str | None = None,
Expand Down Expand Up @@ -143,6 +144,7 @@
self.country = country
self.style = style
self.genre = genre
self.genres = genres
self.albumstatus = albumstatus
self.media = media
self.albumdisambig = albumdisambig
Expand All @@ -166,7 +168,7 @@
return dupe


class TrackInfo(AttrDict):

Check failure on line 171 in beets/autotag/hooks.py

View workflow job for this annotation

GitHub Actions / Check types with mypy

Missing type parameters for generic type "AttrDict"
"""Describes a canonical track present on a release. Appears as part
of an AlbumInfo's ``tracks`` list. Consists of these data members:

Expand Down Expand Up @@ -212,6 +214,7 @@
bpm: str | None = None,
initial_key: str | None = None,
genre: str | None = None,
genres: str | None = None,
album: str | None = None,
**kwargs,
):
Expand Down Expand Up @@ -246,6 +249,7 @@
self.bpm = bpm
self.initial_key = initial_key
self.genre = genre
self.genres = genres
self.album = album
self.update(kwargs)

Expand Down Expand Up @@ -647,7 +651,7 @@
yield t


def invoke_mb(call_func: Callable, *args):

Check failure on line 654 in beets/autotag/hooks.py

View workflow job for this annotation

GitHub Actions / Check types with mypy

Missing type parameters for generic type "Callable"
try:
return call_func(*args)
except mb.MusicBrainzAPIError as exc:
Expand All @@ -661,8 +665,8 @@
artist: str,
album: str,
va_likely: bool,
extra_tags: dict,

Check failure on line 668 in beets/autotag/hooks.py

View workflow job for this annotation

GitHub Actions / Check types with mypy

Missing type parameters for generic type "dict"
) -> Iterable[tuple]:

Check failure on line 669 in beets/autotag/hooks.py

View workflow job for this annotation

GitHub Actions / Check types with mypy

Missing type parameters for generic type "tuple"
"""Search for album matches. ``items`` is a list of Item objects
that make up the album. ``artist`` and ``album`` are the respective
names (strings), which may be derived from the item list or may be
Expand Down Expand Up @@ -690,7 +694,7 @@


@plugins.notify_info_yielded("trackinfo_received")
def item_candidates(item: Item, artist: str, title: str) -> Iterable[tuple]:

Check failure on line 697 in beets/autotag/hooks.py

View workflow job for this annotation

GitHub Actions / Check types with mypy

Missing type parameters for generic type "tuple"
"""Search for item matches. ``item`` is the Item to be matched.
``artist`` and ``title`` are strings and either reflect the item or
are specified by the user.
Expand Down
3 changes: 3 additions & 0 deletions beets/library.py
Original file line number Diff line number Diff line change
Expand Up @@ -534,6 +534,7 @@ class Item(LibModel):
"albumartist_credit": types.STRING,
"albumartists_credit": types.MULTI_VALUE_DSV,
"genre": types.STRING,
"genres": types.MULTI_VALUE_DSV,
"style": types.STRING,
"discogs_albumid": types.INTEGER,
"discogs_artistid": types.INTEGER,
Expand Down Expand Up @@ -1182,6 +1183,7 @@ class Album(LibModel):
"albumartists_credit": types.MULTI_VALUE_DSV,
"album": types.STRING,
"genre": types.STRING,
"genres": types.MULTI_VALUE_DSV,
"style": types.STRING,
"discogs_albumid": types.INTEGER,
"discogs_artistid": types.INTEGER,
Expand Down Expand Up @@ -1238,6 +1240,7 @@ class Album(LibModel):
"albumartists_credit",
"album",
"genre",
"genres",
"style",
"discogs_albumid",
"discogs_artistid",
Expand Down
2 changes: 2 additions & 0 deletions docs/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,8 @@ New features:
* Beets now uses ``platformdirs`` to determine the default music directory.
This location varies between systems -- for example, users can configure it
on Unix systems via ``user-dirs.dirs(5)``.
* New multi-valued ``genres`` tag. This change brings up the ``genres`` tag to the same state as the ``*artists*`` multi-valued tags (see :bug:`4743` for details).
:bug:`5426`

Bug fixes:

Expand Down
30 changes: 26 additions & 4 deletions test/test_library.py
Original file line number Diff line number Diff line change
Expand Up @@ -710,13 +710,13 @@ def test_if_def_false_complete(self):
self._assert_dest(b"/base/not_played")

def test_first(self):
snejus marked this conversation as resolved.
Show resolved Hide resolved
self.i.genres = "Pop; Rock; Classical Crossover"
self._setf("%first{$genres}")
self.i.semicolon_delimited_field = "Pop; Rock; Classical Crossover"
self._setf("%first{$semicolon_delimited_field}")
self._assert_dest(b"/base/Pop")

def test_first_skip(self):
self.i.genres = "Pop; Rock; Classical Crossover"
self._setf("%first{$genres,1,2}")
self.i.semicolon_delimited_field = "Pop; Rock; Classical Crossover"
self._setf("%first{$semicolon_delimited_field,1,2}")
self._assert_dest(b"/base/Classical Crossover")

def test_first_different_sep(self):
Expand Down Expand Up @@ -1308,6 +1308,28 @@ def test_write_date_field(self):
item.write()
assert MediaFile(syspath(item.path)).year == clean_year

def test_write_multi_genres(self):
item = self.add_item_fixture(genre="old genre")
item.write(
tags={"genres": ["g1", "g2"]},
)

# Ensure it reads all genres
assert MediaFile(syspath(item.path)).genres == ["g1", "g2"]

# Ensure reading single genre outputs the first of the genres
assert MediaFile(syspath(item.path)).genre == "g1"

def test_write_multi_genres_both_single_and_multi(self):
item = self.add_item_fixture(genre="old genre 1")
item.write(
tags={"genre": "single genre", "genres": ["multi genre"]},
)

# Ensure the multi takes precedence
assert MediaFile(syspath(item.path)).genre == "multi genre"
assert MediaFile(syspath(item.path)).genres == ["multi genre"]


class ItemReadTest(unittest.TestCase):
def test_unreadable_raise_read_error(self):
Expand Down
Loading