LastGenre Plugin
================

The ``lastgenre`` plugin fetches *tags* from Last.fm_ and assigns them as genres
to your albums and items.

.. _last.fm: https://www.last.fm/

Installation
------------

To use the ``lastgenre`` plugin, first enable it in your configuration (see
:ref:`using-plugins`). Then, install ``beets`` with ``lastgenre`` extra

.. code-block:: bash

    pip install "beets[lastgenre]"

Usage
-----

The plugin chooses genres based on a *whitelist*, meaning that only certain tags
can be considered genres. This way, tags like "my favorite music" or "seen live"
won't be considered genres. The plugin ships with a fairly extensive `internal
whitelist`_, but you can set your own in the config file using the ``whitelist``
configuration value or forgo a whitelist altogether by setting the option to
``no``.

The genre list file should contain one genre per line. Blank lines are ignored.
For the curious, the default genre list is generated by a `script that scrapes
Wikipedia`_.

.. _internal whitelist: https://raw.githubusercontent.com/beetbox/beets/master/beetsplug/lastgenre/genres.txt

.. _script that scrapes wikipedia: https://gist.github.com/sampsyo/1241307

Canonicalization
~~~~~~~~~~~~~~~~

The plugin can also *canonicalize* genres, meaning that more obscure genres can
be turned into coarser-grained ones that are present in the whitelist. This
works using a `tree of nested genre names`_, represented using YAML_, where the
leaves of the tree represent the most specific genres.

The most common way to use this would be with a custom whitelist containing only
a desired subset of genres. Consider for a example this minimal whitelist:

::

    rock
    heavy metal
    pop

together with the default genre tree. Then an item that has its genre specified
as *viking metal* would actually be tagged as *heavy metal* because neither
*viking metal* nor its parent *black metal* are in the whitelist. It always
tries to use the most specific genre that's available in the whitelist.

The relevant subtree path in the default tree looks like this:

::

    - rock:
        - heavy metal:
            - black metal:
                - viking metal

Considering that, it's not very useful to use the default whitelist (which
contains about any genre contained in the tree) with canonicalization because
nothing would ever be matched to a more generic node since all the specific
subgenres are in the whitelist to begin with.

If you use canonicalization *without* a whitelist, the plugin will simply map
every genre to its top-most root category in the tree (e.g., ``Viking Metal`` →
``Rock``). This is a great way to keep your library broad without needing to
maintain a manual list of allowed genres.

.. _tree of nested genre names: https://raw.githubusercontent.com/beetbox/beets/master/beetsplug/lastgenre/genres-tree.yaml

.. _yaml: https://yaml.org/

Genre Source
~~~~~~~~~~~~

When looking up genres for albums or individual tracks, you can choose whether
to use Last.fm tags on the album, the artist, or the track. For example, you
might want all the albums for a certain artist to carry the same genre. The
default is "album". When set to "track", the plugin will fetch *both*
album-level and track-level genres for your music when importing albums.

Multiple Genres
~~~~~~~~~~~~~~~

By default, the plugin chooses the most popular tag on Last.fm as a genre. If
you prefer to use a *list* of popular genre tags, you can increase the number of
the ``count`` config option.

Lists of up to *count* genres will be stored in the ``genres`` field as a list
and written to your media files as separate genre tags.

Last.fm_ provides a popularity factor, a.k.a. *weight*, for each tag ranging
from 100 for the most popular tag down to 0 for the least popular. The plugin
uses this weight to discard unpopular tags. The default is to ignore tags with a
weight less then 10. You can change this by setting the ``min_weight`` config
option.

Specific vs. Popular Genres
~~~~~~~~~~~~~~~~~~~~~~~~~~~

By default, the plugin sorts genres by popularity. However, you can use the
``prefer_specific`` option to override this behavior and instead sort genres by
specificity, as determined by your whitelist and canonicalization tree.

For instance, say you have both ``folk`` and ``americana`` in your whitelist and
canonicalization tree and ``americana`` is a leaf within ``folk``. If 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``, ``keep_existing`` and ``whitelist`` options control how
pre-existing genres are handled.

As you would assume, setting ``force: no`` **won't touch pre-existing genre
tags** and will only **fetch new genres for empty tags**. When ``force`` is
``yes`` the setting of the ``whitelist`` option (as documented in Usage_)
applies to any existing or newly fetched genres.

The following configurations are possible:

**Setup 1** (default)

Add new last.fm genres when **empty**. Any present tags stay **untouched**.

.. code-block:: yaml

    force: no
    keep_existing: no

**Setup 2**

**Overwrite all**. Only fresh last.fm genres remain.

.. code-block:: yaml

    force: yes
    keep_existing: no

**Setup 3**

**Combine** genres in present tags with new ones (be aware of that with an
enabled ``whitelist`` setting, of course some genres might get cleaned up -
existing genres take precedence over new ones though. To make sure any existing
genres remain, set ``whitelist: no``).

.. code-block:: yaml

    force: yes
    keep_existing: yes

.. attention::

    If ``force`` is disabled the ``keep_existing`` option is simply ignored
    (since ``force: no`` means ``not touching`` existing tags anyway).

Genre Ignorelist
----------------

Last.fm tags are crowd-sourced, so they can be wrong — especially for artists
whose names are shared with or confused with others. For example, a "Drum And
Bass" artist named "Fracture" might incorrectly receive "Metal" tags. The
ignorelist lets you reject specific genres globally or per-artist.

Another example for the ignorelist is to exclude genres that are technically
correct but not useful to you. For example, you might want to exclude "Ska" for
"Bob Marley", even though it is a valid genre for his music.

Filtering is done in two places: when fetching genres from Last.fm and when
resolving to a final genre list (during canonicalization and whitelisting).

This means that existing genres are also filtered when ``force`` and
``keep_existing`` options are enabled (or ``cleanup_existing`` is enabled with
``force`` disabled).

To enable this feature, add an ``ignorelist`` section to your ``lastgenre``
configuration:

.. code-block:: yaml

    lastgenre:
        ignorelist:
            fracture:
                - ^(heavy|black|power|death)?\s?(metal|rock)$|\w+-metal\d*$
                - progressive metal
            bob marley:
                - ska
            '*':
                - electronic

A combination of regex patterns and plain genre names is possible. The ``'*'``
key applies globally to all artists — use it to block genres you never want,
regardless of artist. Patterns are matched against the full genre string, so a
plain ``metal`` will not match ``heavy metal`` unless you write a regex like
``.*metal``.

.. attention::

    - The global key ``'*'`` **must** be surrounded by single quotes so that
      YAML does not interpret it as an anchor.
    - Any regex pattern that starts with a special YAML character (``[``, ``*``,
      or ``:``) or ends with ``:`` **must** be surrounded by quotes.
    - Prefer **single quotes** (``'...'``) when quoting is necessary, as they
      treat backslashes literally (no double-escaping required).
    - Because the ignorelist uses plain YAML, you do **not** need to
      double-escape backslashes in unquoted or single-quoted strings (e.g., use
      ``\w``, not ``\\w``).

Genre Normalization (Aliases)
-----------------------------

Last.fm tags often contain variant spellings, abbreviations, or inconsistent
formatting (e.g., "hip-hop", "hiphop", and "hip hop"). The normalization feature
uses an ordered list of regular expression aliases to map these variants to a
single canonical name *before* any other filtering or canonicalization takes
place.

This feature is enabled by default and uses a built-in alias table that covers
many common cases, such as mapping "dnb" to "drum and bass" or "r&b" to "rhythm
and blues". Set ``aliases: no`` to turn it off entirely.

You can replace these aliases with your own by setting ``aliases`` to an inline
mapping in your configuration. The keys are the canonical genre names (which
support ``\1`` / ``\g<N>`` back-references to regex capture groups) and the
values are lists of regex patterns.

.. note::

    The same formatting and quoting rules regarding YAML special characters and
    backslashes mentioned in the **Attention** box in the **Genre Ignorelist**
    section apply here as well.

The full default mappings are shown below - copy and paste this into your config
below the ``lastgenre.aliases`` section to customize it:

.. literalinclude:: ../../beetsplug/lastgenre/aliases.yaml
    :language: yaml

Choosing the Right Tool
-----------------------

With multiple ways to filter and map genres, here is a quick guide on when to
use what:

- **Aliases**: Use these first to fix spelling variants and abbreviations (e.g.,
  ``dnb`` → ``drum and bass``).
- **Ignorelist**: Use this for error correction when Last.fm results are not
  accurate, or for precise per-artist or global exclusions (e.g., rejecting
  ``Metal`` for specific electronic artists).
- **Canonicalization**: Use this to automatically map specific sub-genres to
  broader categories (e.g., ``Grindcore`` → ``Metal``).
- **Whitelist**: Use this to finally limit your library to a predefined set of
  genres. When combined with canonicalization, the plugin will try to map a
  sub-genre to its closest whitelisted parent. Anything else is dropped.

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

To configure the plugin, make a ``lastgenre:`` section in your configuration
file. Default configuration:

.. code-block:: yaml

    lastgenre:
        auto: yes
        canonical: no
        cleanup_existing: no
        count: 1
        fallback: null
        force: no
        keep_existing: no
        min_weight: 10
        prefer_specific: no
        source: album
        whitelist: yes
        title_case: yes
        ignorelist: no
        aliases: yes

The available options are:

- **auto**: Fetch genres automatically during import. Default: ``yes``.
- **canonical**: Use a canonicalization tree. Setting this to ``yes`` will use a
  built-in tree. You can also set it to a path, like the ``whitelist`` config
  value, to use your own tree. Default: ``no`` (disabled).
- **cleanup_existing**: This option only takes effect with ``force: no``,
  Setting this to ``yes`` will result in cleanup of existing genres. That
  includes canonicalization and whitelisting, if enabled. If no matching genre
  can be determined, the ``fallback`` is used instead. Default: ``no``
  (disabled).
- **count**: Number of genres to fetch. Default: 1
- **fallback**: A string to use as a fallback genre when no genre is found
  ``or`` the original genre is not desired to be kept (``keep_existing: no``).
  You can use the empty string ``''`` to reset the genre. Default: None.
- **force**: By default, lastgenre will fetch new genres for empty tags only,
  enable this option to always try to fetch new last.fm genres. Enable the
  ``keep_existing`` option to combine existing and new genres. (see `Handling
  pre-populated tags`_). Default: ``no``.
- **keep_existing**: This option alters the ``force`` behavior. If both
  ``force`` and ``keep_existing`` are enabled, existing genres are combined with
  new ones. Depending on the ``whitelist`` setting, existing and new genres are
  filtered accordingly. To ensure only fresh last.fm genres, disable this
  option. (see `Handling pre-populated tags`_) Default: ``no``.
- **min_weight**: Minimum popularity factor below which genres are discarded.
  Default: 10.
- **prefer_specific**: Sort genres by the most to least specific, rather than
  most to least popular. Note that this option requires a ``canonical`` tree,
  and if not configured it will automatically enable and use the built-in tree.
  Default: ``no``.
- **source**: Which entity to look up in Last.fm. Can be either ``artist``,
  ``album`` or ``track``. Default: ``album``.
- **whitelist**: The filename of a custom genre list, ``yes`` to use the
  internal whitelist, or ``no`` to consider all genres valid. Default: ``yes``.
- **title_case**: Convert the new tags to TitleCase before saving. Default:
  ``yes``.
- **ignorelist**: A mapping of artist names (or the global ``'*'`` key) to lists
  of genres to exclude. See `Genre Ignorelist`_ for more details. Default:
  ``no``.
- **aliases**: Controls genre alias normalization. Set to ``yes`` (default) to
  use the built-in alias table, ``no`` to disable normalization entirely, or an
  inline mapping of canonical genre names to lists of regex patterns to replace
  the built-in table with your own. See `Genre Normalization (Aliases)`_ for
  details.

Running Manually
----------------

In addition to running automatically on import, the plugin can also be run
manually from the command line. Use the command ``beet lastgenre [QUERY]`` to
fetch genres for albums or items matching a certain query.

By default, ``beet lastgenre`` matches albums. To match individual tracks or
singletons, use the ``-A`` switch: ``beet lastgenre -A [QUERY]``.

To preview the changes that would be made without applying them, use the ``-p``
or ``--pretend`` flag. This shows which genres would be set but does not write
or store any changes.

To disable automatic genre fetching on import, set the ``auto`` config option to
false.

Tuning Logs
-----------

To enable tuning logs, run ``beet -vvv lastgenre ...`` or ``beet -vvv import
...``. This enables additional messages at the ``DEBUG`` log level, showing for
example what data was received from last.fm at each stage of genre fetching
(artist, album, and track levels) before any canonicalization or whitelist
filtering is applied. Tuning logs are useful for adjusting the plugin’s settings
and understanding its behavior, though they can be quite verbose.
