Fixes#5788
This PR fixes a regression that occurred when the MusicBrainz autotagger
was moved from core functionality into a standalone plugin. During this
transition, the handling of user-configured `extra_tags` was broken,
causing `beets` to die with `AttributeError` when this option was set.
Key changes in `musicbrainz` plugin
- Properly process values from `extra_tags` using the same logic as
before the migration.
- Centralize common bits from `candidates` and `item_candidates`
implementations under `_search_api` to move this class closer towards
`MetadataSourcePlugin` definition.
- Instead of checking for empty `artist` query, use `va_likely`
parameter to determine whether we should query for Various Artists or
not.
- `album` / `title` is always a truthy string - no need to handle empty
criteria case
- `tracks` list always has at least one track - no need to check for
`len(items)`
Refactor: Centralize release ID extraction
This change introduces a new utility function `extract_release_id` in
`beets.util.id_extractors` to handle the parsing of release IDs (or URLs
containing IDs) for various metadata sources (Spotify, Deezer, Beatport,
Discogs, MusicBrainz, Bandcamp, Tidal).
Key changes:
- Added `extract_release_id` function and `PATTERN_BY_SOURCE` regex
dictionary.
- Converted `MetadataSourcePlugin._get_id` static method to an instance
method which uses the `data_source` property to pick the correct id
extractor.
- Removed the `id_regex` property and updated `_get_id` calls in all
modules.
- Replaced old tests related to ID parsing in individual plugin test
files with `test/util/test_id_extractors.py` that tests the
`extract_release_id` function.
This refactoring simplifies the codebase, reduces redundancy, and makes
it easier to manage and extend ID extraction logic for different
sources.
## Description
Hello y'all, when working on the importer.py file in a previous
[PR](#5611) I noticed that this file grew quite large and badly needs a
restructuring. Restructuring should improve our ability to apply changes
to it in the future and isolate sub-functionalities within the importer.
### Overview
For now I only changed the structure keeping the code (mostly)
unchanged.
I split the functions and classes in the importer.py into the following
responsibilities:
- `importer/session.py` : Includes the `ImportSession` class.
- `importer/stages.py` : Includes all stage functions, I prefixed the
helper functions with a `_` to allow distinguishing between stages and
helper functions more easily.
- `importer/state.py` : Includes the logic for the `ImportState`
handling i.e. the resume feat.
- `importer/tasks.py` : Includes the `ImportTask` class and all derived
classes. Also includes the `Action` enum which I have renamed from
`action`.
- `importer/__init__.py` : Identified all public facing classes and
functions and added them to `__all__`
### Potential future changes
I don't want to add this to this PR but there are some places here where
I see possible improvements for our code:
- There are quite some config parsing related functions in the
ImportSession which could be isolated (see e.g. set_config,
want_resume). Maybe a mixin class which handles the config operations
could be useful?
- The ImportSession should be abstract if it is not used directly (I
think it shouldn't). The function definitions which raise NotImplemented
errors are quite weird imo and could be avoided by making the class
abstract.
- For me it was difficult to understand the flow of the importer as
stages call session function and it is not clear which function is
called by which stage and when. Maybe a naming convention for the stage
functions in conjunction with the session methods could help here. Not
sure how this will look in practice but right now it is quite hard to
follow imo. Alternatively splitting the session into a outfacing session
and a session context which is passed to the stages could help.
- The use of the stage decorator is highly inconsistent. Maybe a better
way to handle the stages could be found. This is more of a pipeline
related issue and not directly related to the restructuring but I think
it is worth mentioning.
- Similar to the ImportSession, I think the ImportTask should be
abstract as well, maybe we can put a bit more thought into the task
hierarchy. This might also automatically improve the flow of the
importer pipeline.
Am happy to tackle some of these issues in future PRs if you also think
they are worth it.
Best,
Sebastian
Note: This PR is based on #5611 and can only be merged once the typing
additions are accepted.
# Move MusicBrainz autotagger from core to plugin
Fixes#2686Fixes#4605
This PR relocates the MusicBrainz autotagger functionality from the
beets core into a dedicated plugin. This promotes better code
organization and follows beets' modular design approach.
Key changes:
- Move code from `beets/autotag/mb.py` to new `beetsplug/musicbrainz.py`
- Update default config to include 'musicbrainz' in the plugins list
- Refactor related plugin APIs for better integration
- Update documentation with new musicbrainz plugin page
Impact for users: If you've customized your `plugins` list in
configuration, you'll need to explicitly add `musicbrainz` to continue
using MusicBrainz for autotagging.
Add link to community plugin
[`beets-filetote`](https://github.com/gtronset/beets-filetote).
This plugin is the spiritual successor to
[beets-copyartifacts](https://github.com/adammillerio/beets-copyartifacts)
(`beets-copyartifacts3` was last updated 3 years ago) and
[beets-extrafiles](https://github.com/Holzhaus/beets-extrafiles) (last
updated 5 years ago).
Given the updates and changes in beets and how outdated those plugins
are, does it make sense to keep `beets-copyartifacts` in the community
plugins list?
---------
Co-authored-by: Sebastian Mohr <39738318+semohr@users.noreply.github.com>