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

Lastgenre: Fix track-level handling, multi-genre keep, force behaviour, logging #4982

Draft
wants to merge 15 commits into
base: master
Choose a base branch
from
Draft
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
168 changes: 136 additions & 32 deletions beetsplug/lastgenre/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,8 @@
"fallback": None,
"canonical": False,
"source": "album",
"force": True,
"force": False,
"keep_allowed": True,
"auto": True,
"separator": ", ",
"prefer_specific": False,
Expand Down Expand Up @@ -250,7 +251,7 @@
"""
if genre is None:
return False
if not self.whitelist or genre in self.whitelist:
if not self.whitelist or genre.lower() in self.whitelist:
return True
return False

Expand Down Expand Up @@ -302,42 +303,116 @@
"track", LASTFM.get_track, obj.artist, obj.title
)

def _get_existing_genres(self, obj, separator):
"""Return a list of genres for this Item or Album."""
if isinstance(obj, library.Item):
item_genre = obj.get("genre", with_album=False).split(separator)
else:
item_genre = obj.get("genre").split(separator)

if any(item_genre):
return item_genre
return []

def _dedup_genres(self, genres, whitelist_only=True):
"""Return a list of deduplicated genres. Depending on the
whitelist_only option, gives filtered or unfiltered results."""
if whitelist_only:
return deduplicate([g for g in genres if self._is_allowed(g)])
else:
return deduplicate([g for g in genres])

def _combine_and_label_genres(
self, new_genres: str, keep_genres: list, separator: str, log_label: str

Check failure on line 326 in beetsplug/lastgenre/__init__.py

View workflow job for this annotation

GitHub Actions / Check types with mypy

Implicit generic "Any". Use "typing.List" and specify generic parameters
) -> tuple:

Check failure on line 327 in beetsplug/lastgenre/__init__.py

View workflow job for this annotation

GitHub Actions / Check types with mypy

Implicit generic "Any". Use "typing.Tuple" and specify generic parameters
"""Combines genres and returns them with a logging label.

Parameters:
new_genres (str): The new genre string result to process.
keep_genres (list): Existing genres to combine with new ones
separator (str): Separator used to split and join genre strings.
log_label (str): A label (like "track", "album") we possibly
combine with a prefix. For example resulting in something like
"keep + track" or just "track".

Returns:
tuple: A tuple containing the combined genre string and the "logging
label".
"""
if new_genres and keep_genres:
split_genres = new_genres.split(separator)
combined_genres = deduplicate(keep_genres + split_genres)
return separator.join(combined_genres), f"keep + {log_label}"
elif new_genres:
return new_genres, log_label
return "", log_label

def _get_genre(self, obj):
"""Get the genre string for an Album or Item object based on
self.sources. Return a `(genre, source)` pair. The
prioritization order is:
"""Get the final genre string for an Album or Item object

`self.sources` specifies allowed genre sources, prioritized as follows:
- track (for Items only)
- album
- artist
- original
- fallback
- None

Parameters:
obj: Either an Album or Item object.

Returns:
tuple: A `(genre, label)` pair, where `label` is a string used for
logging that describes the result. For example, "keep + artist"
indicates that existing genres were combined with new last.fm
genres, while "artist" means only new last.fm genres are
included.
"""

# Shortcut to existing genre if not forcing.
if not self.config["force"] and self._is_allowed(obj.genre):
return obj.genre, "keep"
separator = self.config["separator"].get()
keep_genres = []

if self.config["force"]:
genres = self._get_existing_genres(obj, separator)
# Case 3 - Keep WHITELISTED. Combine with new.
# Case 1 - Keep None. Overwrite all.
if self.config["keep_allowed"]:
keep_genres = self._dedup_genres(genres, whitelist_only=True)
else:
keep_genres = None
else:
genres = self._get_existing_genres(obj, separator)
# Case 4 - Keep WHITELISTED. Handle empty.
# Case 2 - Keep ANY. Handle empty.
if genres and self.config["keep_allowed"]:
keep_genres = self._dedup_genres(genres, whitelist_only=True)
return separator.join(keep_genres), "keep allowed"
elif genres and not self.config["keep_allowed"]:
keep_genres = self._dedup_genres(genres)
return separator.join(keep_genres), "keep any"
# else: Move on, genre tag is empty.

# Track genre (for Items only).
if isinstance(obj, library.Item):
if "track" in self.sources:
result = self.fetch_track_genre(obj)
if result:
return result, "track"
if isinstance(obj, library.Item) and "track" in self.sources:
new_genres = self.fetch_track_genre(obj)
return self._combine_and_label_genres(
new_genres, keep_genres, separator, "track"
)

# Album genre.
if "album" in self.sources:
result = self.fetch_album_genre(obj)
if result:
return result, "album"
new_genres = self.fetch_album_genre(obj)
return self._combine_and_label_genres(
new_genres, keep_genres, separator, "album"
)

# Artist (or album artist) genre.
if "artist" in self.sources:
result = None
new_genres = None
if isinstance(obj, library.Item):
result = self.fetch_artist_genre(obj)
new_genres = self.fetch_artist_genre(obj)
elif obj.albumartist != config["va_name"].as_str():
result = self.fetch_album_artist_genre(obj)
new_genres = self.fetch_album_artist_genre(obj)
else:
# For "Various Artists", pick the most popular track genre.
item_genres = []
Expand All @@ -350,10 +425,11 @@
if item_genre:
item_genres.append(item_genre)
if item_genres:
result, _ = plurality(item_genres)
new_genres, _ = plurality(item_genres)

if result:
return result, "artist"
return self._combine_and_label_genres(
new_genres, keep_genres, separator, "artist"
)

# Filter the existing genre.
if obj.genre:
Expand All @@ -377,6 +453,20 @@
action="store_true",
help="re-download genre when already present",
)
lastgenre_cmd.parser.add_option(
"-k",
"--keep-allowed",
dest="keep_allowed",
action="store_true",
help="keep already present genres when whitelisted",
)
lastgenre_cmd.parser.add_option(
"-K",
"--keep-any",
dest="keep_allowed",
action="store_false",
help="keep any already present genres",
)
lastgenre_cmd.parser.add_option(
"-s",
"--source",
Expand Down Expand Up @@ -409,9 +499,14 @@
for album in lib.albums(ui.decargs(args)):
album.genre, src = self._get_genre(album)
self._log.info(
"genre for album {0} ({1}): {0.genre}", album, src
'genre for album "{0.album}" ({1}): {0.genre}',
album,
src,
)
album.store()
if "track" in self.sources:
album.store(inherit=False)
else:
album.store()

for item in album.items():
# If we're using track-level sources, also look up each
Expand All @@ -420,7 +515,7 @@
item.genre, src = self._get_genre(item)
item.store()
self._log.info(
"genre for track {0} ({1}): {0.genre}",
'genre for track "{0.title}" ({1}): {0.genre}',
item,
src,
)
Expand All @@ -432,10 +527,10 @@
# an album
for item in lib.items(ui.decargs(args)):
item.genre, src = self._get_genre(item)
self._log.debug(
"added last.fm item genre ({0}): {1}", src, item.genre
)
item.store()
self._log.info(
"genre for track {0.title} ({1}): {0.genre}", item, src
)

lastgenre_cmd.func = lastgenre_func
return [lastgenre_cmd]
Expand All @@ -446,23 +541,32 @@
album = task.album
album.genre, src = self._get_genre(album)
self._log.debug(
"added last.fm album genre ({0}): {1}", src, album.genre
'genre for album "{0.album}" ({1}): {0.genre}', album, src
)
album.store()

# If we're using track-level sources, store the album genre only,
# then also look up individual track genres.
if "track" in self.sources:
album.store(inherit=False)
for item in album.items():
item.genre, src = self._get_genre(item)
self._log.debug(
"added last.fm item genre ({0}): {1}", src, item.genre
'genre for track "{0.title}" ({1}): {0.genre}',
item,
src,
)
item.store()
# Store the album genre and inherit to tracks.
else:
album.store()

else:
item = task.item
item.genre, src = self._get_genre(item)
self._log.debug(
"added last.fm item genre ({0}): {1}", src, item.genre
'genre for track "{0.title}" ({1}): {0.genre}',
item,
src,
)
item.store()

Expand Down
27 changes: 24 additions & 3 deletions docs/plugins/lastgenre.rst
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,22 @@ Last.fm returns both of those tags, lastgenre is going to use the most
popular, which is often the most generic (in this case ``folk``). By setting
``prefer_specific`` to true, lastgenre would use ``americana`` instead.

Handling pre-populated tags
^^^^^^^^^^^^^^^^^^^^^^^^^^^
The ``force`` and ``keep_allowed`` options control how pre-existing genres are
handled. By default, the plugin fetches new genres for empty tags only
(``force: no``), and keeps whitelisted genres in pre-populated tags
(``keep_allowed: yes``).

To write new genres to empty tags and keep *any* pre-existing tag content
as-is, set ``force: no`` and ``keep_allowed: no``.

To *overwrite* any content of pre-populated tags, set ``force: yes`` and
``keep_allowed: no``.

To *combine* newly fetched last.fm genres with whitelisted pre-existing genres,
set ``force: yes`` and ``keep_allowed: yes``.

Configuration
-------------

Expand All @@ -128,9 +144,14 @@ configuration file. The available options are:
- **fallback**: A string if to use a fallback genre when no genre is found.
You can use the empty string ``''`` to reset the genre.
Default: None.
- **force**: By default, beets will always fetch new genres, even if the files
already have one. To instead leave genres in place in when they pass the
whitelist, set the ``force`` option to ``no``.
- **force**: By default, lastgenre will fetch new genres for empty tags and
leave pre-existing content in place. When changing this option to ``yes``
also adjust the ``keep_allowed`` option to your preference (see `Handling
pre-populated tags`_).
Default: ``no``.
- **keep_allowed**: By default, whitelisted genres remain in pre-populated
tags. When changing this optio to ``yes``, also ajdust the ``force``
option to your preference (see `Handling pre-populated tags`_).
Default: ``yes``.
- **min_weight**: Minimum popularity factor below which genres are discarded.
Default: 10.
Expand Down
Loading