diff --git a/beetsplug/lastgenre/__init__.py b/beetsplug/lastgenre/__init__.py index ea0ab951a8..40019f5482 100644 --- a/beetsplug/lastgenre/__init__.py +++ b/beetsplug/lastgenre/__init__.py @@ -300,24 +300,20 @@ def _last_lookup(self, entity, method, *args): self._tunelog("last.fm (unfiltered) {} tags: {}", entity, genre) return genre - def fetch_album_genre(self, obj): - """Return raw album genres from Last.fm for this Item or Album.""" + def fetch_album_genre(self, albumartist, albumtitle): + """Return genres from Last.fm for the album by albumartist.""" return self._last_lookup( - "album", LASTFM.get_album, obj.albumartist, obj.album + "album", LASTFM.get_album, albumartist, albumtitle ) - def fetch_album_artist_genre(self, obj): - """Return raw album artist genres from Last.fm for this Item or Album.""" - return self._last_lookup("artist", LASTFM.get_artist, obj.albumartist) + def fetch_artist_genre(self, artist): + """Return genres from Last.fm for the artist.""" + return self._last_lookup("artist", LASTFM.get_artist, artist) - def fetch_artist_genre(self, item): - """Returns raw track artist genres from Last.fm for this Item.""" - return self._last_lookup("artist", LASTFM.get_artist, item.artist) - - def fetch_track_genre(self, obj): - """Returns raw track genres from Last.fm for this Item.""" + def fetch_track_genre(self, trackartist, tracktitle): + """Return genres from Last.fm for the track by artist.""" return self._last_lookup( - "track", LASTFM.get_track, obj.artist, obj.title + "track", LASTFM.get_track, trackartist, tracktitle ) # Main processing: _get_genre() and helpers. @@ -405,14 +401,14 @@ def _try_resolve_stage(stage_label: str, keep_genres, new_genres): # Run through stages: track, album, artist, # album artist, or most popular track genre. if isinstance(obj, library.Item) and "track" in self.sources: - if new_genres := self.fetch_track_genre(obj): + if new_genres := self.fetch_track_genre(obj.artist, obj.title): if result := _try_resolve_stage( "track", keep_genres, new_genres ): return result if "album" in self.sources: - if new_genres := self.fetch_album_genre(obj): + if new_genres := self.fetch_album_genre(obj.albumartist, obj.album): if result := _try_resolve_stage( "album", keep_genres, new_genres ): @@ -421,20 +417,36 @@ def _try_resolve_stage(stage_label: str, keep_genres, new_genres): if "artist" in self.sources: new_genres = [] if isinstance(obj, library.Item): - new_genres = self.fetch_artist_genre(obj) + new_genres = self.fetch_artist_genre(obj.artist) stage_label = "artist" elif obj.albumartist != config["va_name"].as_str(): - new_genres = self.fetch_album_artist_genre(obj) + new_genres = self.fetch_artist_genre(obj.albumartist) stage_label = "album artist" + if not new_genres: + self._tunelog( + 'No album artist genre found for "{}", ' + "trying multi-valued field...", + obj.albumartist, + ) + for albumartist in obj.albumartists: + self._tunelog( + 'Fetching artist genre for "{}"', albumartist + ) + new_genres += self.fetch_artist_genre(albumartist) + if new_genres: + stage_label = "multi-valued album artist" else: # For "Various Artists", pick the most popular track genre. item_genres = [] + assert isinstance(obj, Album) # Type narrowing for mypy for item in obj.items(): item_genre = None if "track" in self.sources: - item_genre = self.fetch_track_genre(item) + item_genre = self.fetch_track_genre( + item.artist, item.title + ) if not item_genre: - item_genre = self.fetch_artist_genre(item) + item_genre = self.fetch_artist_genre(item.artist) if item_genre: item_genres += item_genre if item_genres: diff --git a/docs/changelog.rst b/docs/changelog.rst index d95de38c52..fd5cd02645 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -48,6 +48,11 @@ Bug fixes: endpoints. Previously, due to single-quotes (ie. string literal) in the SQL query, the query eg. `GET /item/values/albumartist` would return the literal "albumartist" instead of a list of unique album artists. +- :doc:`plugins/lastgenre`: Fix the issue where last.fm does not give a result in + the artist genre stage because multi-artist "concatenation" words (like + "feat." "+", or "&" prevent exact matches. Using the albumartists list field + and fetching a genre for each artist separately massively improves the chance + to get a valid result in that stage. For plugin developers: diff --git a/test/plugins/test_lastgenre.py b/test/plugins/test_lastgenre.py index 12ff30f8ed..026001e387 100644 --- a/test/plugins/test_lastgenre.py +++ b/test/plugins/test_lastgenre.py @@ -546,13 +546,13 @@ def test_sort_by_depth(self): def test_get_genre(config_values, item_genre, mock_genres, expected_result): """Test _get_genre with various configurations.""" - def mock_fetch_track_genre(self, obj=None): + def mock_fetch_track_genre(self, trackartist, tracktitle): return mock_genres["track"] - def mock_fetch_album_genre(self, obj): + def mock_fetch_album_genre(self, albumartist, albumtitle): return mock_genres["album"] - def mock_fetch_artist_genre(self, obj): + def mock_fetch_artist_genre(self, artist): return mock_genres["artist"] # Mock the last.fm fetchers. When whitelist enabled, we can assume only