Merge branch 'master' into ftintitle-continue-even-if-albumartist-and-artist-is-the-same

This commit is contained in:
Ember Light 2025-10-20 15:24:43 +02:00 committed by GitHub
commit 00e3da1a92
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
28 changed files with 627 additions and 358 deletions

View file

@ -10,7 +10,7 @@ jobs:
check_changes: check_changes:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v5
- name: Get all updated Python files - name: Get all updated Python files
id: changed-python-files id: changed-python-files

View file

@ -25,12 +25,12 @@ jobs:
env: env:
IS_MAIN_PYTHON: ${{ matrix.python-version == '3.9' && matrix.platform == 'ubuntu-latest' }} IS_MAIN_PYTHON: ${{ matrix.python-version == '3.9' && matrix.platform == 'ubuntu-latest' }}
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v5
- name: Install Python tools - name: Install Python tools
uses: BrandonLWhite/pipx-install-action@v1.0.3 uses: BrandonLWhite/pipx-install-action@v1.0.3
- name: Setup Python with poetry caching - name: Setup Python with poetry caching
# poetry cache requires poetry to already be installed, weirdly # poetry cache requires poetry to already be installed, weirdly
uses: actions/setup-python@v5 uses: actions/setup-python@v6
with: with:
python-version: ${{ matrix.python-version }} python-version: ${{ matrix.python-version }}
cache: poetry cache: poetry
@ -90,10 +90,10 @@ jobs:
permissions: permissions:
id-token: write id-token: write
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v5
- name: Get the coverage report - name: Get the coverage report
uses: actions/download-artifact@v4 uses: actions/download-artifact@v5
with: with:
name: coverage-report name: coverage-report

View file

@ -7,10 +7,10 @@ jobs:
test_integration: test_integration:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v5
- name: Install Python tools - name: Install Python tools
uses: BrandonLWhite/pipx-install-action@v1.0.3 uses: BrandonLWhite/pipx-install-action@v1.0.3
- uses: actions/setup-python@v5 - uses: actions/setup-python@v6
with: with:
python-version: 3.9 python-version: 3.9
cache: poetry cache: poetry

View file

@ -24,7 +24,7 @@ jobs:
changed_doc_files: ${{ steps.changed-doc-files.outputs.all_changed_files }} changed_doc_files: ${{ steps.changed-doc-files.outputs.all_changed_files }}
changed_python_files: ${{ steps.changed-python-files.outputs.all_changed_files }} changed_python_files: ${{ steps.changed-python-files.outputs.all_changed_files }}
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v5
- name: Get changed docs files - name: Get changed docs files
id: changed-doc-files id: changed-doc-files
uses: tj-actions/changed-files@v46 uses: tj-actions/changed-files@v46
@ -56,10 +56,10 @@ jobs:
name: Check formatting name: Check formatting
needs: changed-files needs: changed-files
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v5
- name: Install Python tools - name: Install Python tools
uses: BrandonLWhite/pipx-install-action@v1.0.3 uses: BrandonLWhite/pipx-install-action@v1.0.3
- uses: actions/setup-python@v5 - uses: actions/setup-python@v6
with: with:
python-version: ${{ env.PYTHON_VERSION }} python-version: ${{ env.PYTHON_VERSION }}
cache: poetry cache: poetry
@ -77,10 +77,10 @@ jobs:
name: Check linting name: Check linting
needs: changed-files needs: changed-files
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v5
- name: Install Python tools - name: Install Python tools
uses: BrandonLWhite/pipx-install-action@v1.0.3 uses: BrandonLWhite/pipx-install-action@v1.0.3
- uses: actions/setup-python@v5 - uses: actions/setup-python@v6
with: with:
python-version: ${{ env.PYTHON_VERSION }} python-version: ${{ env.PYTHON_VERSION }}
cache: poetry cache: poetry
@ -97,10 +97,10 @@ jobs:
name: Check types with mypy name: Check types with mypy
needs: changed-files needs: changed-files
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v5
- name: Install Python tools - name: Install Python tools
uses: BrandonLWhite/pipx-install-action@v1.0.3 uses: BrandonLWhite/pipx-install-action@v1.0.3
- uses: actions/setup-python@v5 - uses: actions/setup-python@v6
with: with:
python-version: ${{ env.PYTHON_VERSION }} python-version: ${{ env.PYTHON_VERSION }}
cache: poetry cache: poetry
@ -120,10 +120,10 @@ jobs:
name: Check docs name: Check docs
needs: changed-files needs: changed-files
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v5
- name: Install Python tools - name: Install Python tools
uses: BrandonLWhite/pipx-install-action@v1.0.3 uses: BrandonLWhite/pipx-install-action@v1.0.3
- uses: actions/setup-python@v5 - uses: actions/setup-python@v6
with: with:
python-version: ${{ env.PYTHON_VERSION }} python-version: ${{ env.PYTHON_VERSION }}
cache: poetry cache: poetry

View file

@ -17,10 +17,10 @@ jobs:
name: Bump version, commit and create tag name: Bump version, commit and create tag
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v5
- name: Install Python tools - name: Install Python tools
uses: BrandonLWhite/pipx-install-action@v1.0.3 uses: BrandonLWhite/pipx-install-action@v1.0.3
- uses: actions/setup-python@v5 - uses: actions/setup-python@v6
with: with:
python-version: ${{ env.PYTHON_VERSION }} python-version: ${{ env.PYTHON_VERSION }}
cache: poetry cache: poetry
@ -45,13 +45,13 @@ jobs:
outputs: outputs:
changelog: ${{ steps.generate_changelog.outputs.changelog }} changelog: ${{ steps.generate_changelog.outputs.changelog }}
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v5
with: with:
ref: ${{ env.NEW_TAG }} ref: ${{ env.NEW_TAG }}
- name: Install Python tools - name: Install Python tools
uses: BrandonLWhite/pipx-install-action@v1.0.3 uses: BrandonLWhite/pipx-install-action@v1.0.3
- uses: actions/setup-python@v5 - uses: actions/setup-python@v6
with: with:
python-version: ${{ env.PYTHON_VERSION }} python-version: ${{ env.PYTHON_VERSION }}
cache: poetry cache: poetry
@ -92,7 +92,7 @@ jobs:
id-token: write id-token: write
steps: steps:
- name: Download all the dists - name: Download all the dists
uses: actions/download-artifact@v4 uses: actions/download-artifact@v5
with: with:
name: python-package-distributions name: python-package-distributions
path: dist/ path: dist/
@ -107,7 +107,7 @@ jobs:
CHANGELOG: ${{ needs.build.outputs.changelog }} CHANGELOG: ${{ needs.build.outputs.changelog }}
steps: steps:
- name: Download all the dists - name: Download all the dists
uses: actions/download-artifact@v4 uses: actions/download-artifact@v5
with: with:
name: python-package-distributions name: python-package-distributions
path: dist/ path: dist/

3
.gitignore vendored
View file

@ -94,3 +94,6 @@ ENV/
# pyright # pyright
pyrightconfig.json pyrightconfig.json
# Pyrefly
pyrefly.toml

View file

@ -940,10 +940,10 @@ class Transaction:
def __exit__( def __exit__(
self, self,
exc_type: type[Exception], exc_type: type[BaseException] | None,
exc_value: Exception, exc_value: BaseException | None,
traceback: TracebackType, traceback: TracebackType | None,
): ) -> bool | None:
"""Complete a transaction. This must be the most recently """Complete a transaction. This must be the most recently
entered but not yet exited transaction. If it is the last active entered but not yet exited transaction. If it is the last active
transaction, the database updates are committed. transaction, the database updates are committed.
@ -965,6 +965,8 @@ class Transaction:
): ):
raise DBCustomFunctionError() raise DBCustomFunctionError()
return None
def query( def query(
self, statement: str, subvals: Sequence[SQLiteType] = () self, statement: str, subvals: Sequence[SQLiteType] = ()
) -> list[sqlite3.Row]: ) -> list[sqlite3.Row]:

View file

@ -47,6 +47,7 @@ from typing import (
NamedTuple, NamedTuple,
TypeVar, TypeVar,
Union, Union,
cast,
) )
from unidecode import unidecode from unidecode import unidecode
@ -1052,7 +1053,7 @@ def par_map(transform: Callable[[T], Any], items: Sequence[T]) -> None:
pool.join() pool.join()
class cached_classproperty: class cached_classproperty(Generic[T]):
"""Descriptor implementing cached class properties. """Descriptor implementing cached class properties.
Provides class-level dynamic property behavior where the getter function is Provides class-level dynamic property behavior where the getter function is
@ -1060,9 +1061,9 @@ class cached_classproperty:
instance properties, this operates on the class rather than instances. instance properties, this operates on the class rather than instances.
""" """
cache: ClassVar[dict[tuple[Any, str], Any]] = {} cache: ClassVar[dict[tuple[type[object], str], object]] = {}
name: str name: str = ""
# Ideally, we would like to use `Callable[[type[T]], Any]` here, # Ideally, we would like to use `Callable[[type[T]], Any]` here,
# however, `mypy` is unable to see this as a **class** property, and thinks # however, `mypy` is unable to see this as a **class** property, and thinks
@ -1078,21 +1079,21 @@ class cached_classproperty:
# "Callable[[Album], ...]"; expected "Callable[[type[Album]], ...]" # "Callable[[Album], ...]"; expected "Callable[[type[Album]], ...]"
# #
# Therefore, we just use `Any` here, which is not ideal, but works. # Therefore, we just use `Any` here, which is not ideal, but works.
def __init__(self, getter: Callable[[Any], Any]) -> None: def __init__(self, getter: Callable[..., T]) -> None:
"""Initialize the descriptor with the property getter function.""" """Initialize the descriptor with the property getter function."""
self.getter = getter self.getter: Callable[..., T] = getter
def __set_name__(self, owner: Any, name: str) -> None: def __set_name__(self, owner: object, name: str) -> None:
"""Capture the attribute name this descriptor is assigned to.""" """Capture the attribute name this descriptor is assigned to."""
self.name = name self.name = name
def __get__(self, instance: Any, owner: type[Any]) -> Any: def __get__(self, instance: object, owner: type[object]) -> T:
"""Compute and cache if needed, and return the property value.""" """Compute and cache if needed, and return the property value."""
key = owner, self.name key: tuple[type[object], str] = owner, self.name
if key not in self.cache: if key not in self.cache:
self.cache[key] = self.getter(owner) self.cache[key] = self.getter(owner)
return self.cache[key] return cast(T, self.cache[key])
class LazySharedInstance(Generic[T]): class LazySharedInstance(Generic[T]):

View file

@ -132,9 +132,9 @@ class DiscogsPlugin(MetadataSourcePlugin):
"user_token": "", "user_token": "",
"separator": ", ", "separator": ", ",
"index_tracks": False, "index_tracks": False,
"featured_string": "Feat.",
"append_style_genre": False, "append_style_genre": False,
"strip_disambiguation": True, "strip_disambiguation": True,
"featured_string": "Feat.",
"anv": { "anv": {
"artist_credit": True, "artist_credit": True,
"artist": False, "artist": False,

View file

@ -28,6 +28,11 @@ from beets.util import get_temp_filename
# If this is missing, they're placed at the end. # If this is missing, they're placed at the end.
ARGS_MARKER = "$args" ARGS_MARKER = "$args"
# Indicate where the playlist file (with absolute path) should be inserted into
# the command string. If this is missing, its placed at the end, but before
# arguments.
PLS_MARKER = "$playlist"
def play( def play(
command_str, command_str,
@ -132,8 +137,23 @@ class PlayPlugin(BeetsPlugin):
return return
open_args = self._playlist_or_paths(paths) open_args = self._playlist_or_paths(paths)
open_args_str = [
p.decode("utf-8") for p in self._playlist_or_paths(paths)
]
command_str = self._command_str(opts.args) command_str = self._command_str(opts.args)
if PLS_MARKER in command_str:
if not config["play"]["raw"]:
command_str = command_str.replace(
PLS_MARKER, "".join(open_args_str)
)
self._log.debug(
"command altered by PLS_MARKER to: {}", command_str
)
open_args = []
else:
command_str = command_str.replace(PLS_MARKER, " ")
# Check if the selection exceeds configured threshold. If True, # Check if the selection exceeds configured threshold. If True,
# cancel, otherwise proceed with play command. # cancel, otherwise proceed with play command.
if opts.yes or not self._exceeds_threshold( if opts.yes or not self._exceeds_threshold(
@ -162,6 +182,7 @@ class PlayPlugin(BeetsPlugin):
return paths return paths
else: else:
return [self._create_tmp_playlist(paths)] return [self._create_tmp_playlist(paths)]
return [shlex.quote(self._create_tmp_playlist(paths))]
def _exceeds_threshold( def _exceeds_threshold(
self, selection, command_str, open_args, item_type="track" self, selection, command_str, open_args, item_type="track"

View file

@ -241,6 +241,11 @@ var AppView = Backbone.View.extend({
'pause': _.bind(this.audioPause, this), 'pause': _.bind(this.audioPause, this),
'ended': _.bind(this.audioEnded, this) 'ended': _.bind(this.audioEnded, this)
}); });
if ("mediaSession" in navigator) {
navigator.mediaSession.setActionHandler("nexttrack", () => {
this.playNext();
});
}
}, },
showItems: function(items) { showItems: function(items) {
this.shownItems = items; this.shownItems = items;
@ -306,7 +311,9 @@ var AppView = Backbone.View.extend({
}, },
audioEnded: function() { audioEnded: function() {
this.playingItem.entryView.setPlaying(false); this.playingItem.entryView.setPlaying(false);
this.playNext();
},
playNext: function(){
// Try to play the next track. // Try to play the next track.
var idx = this.shownItems.indexOf(this.playingItem); var idx = this.shownItems.indexOf(this.playingItem);
if (idx == -1) { if (idx == -1) {

View file

@ -12,6 +12,8 @@ New features:
- :doc:`plugins/ftintitle`: Added argument for custom feat. words in ftintitle. - :doc:`plugins/ftintitle`: Added argument for custom feat. words in ftintitle.
- :doc:`plugins/ftintitle`: Added argument to skip the processing of artist and - :doc:`plugins/ftintitle`: Added argument to skip the processing of artist and
album artist are the same in ftintitle. album artist are the same in ftintitle.
- :doc:`plugins/play`: Added `$playlist` marker to precisely edit the playlist
filepath into the command calling the player program.
Bug fixes: Bug fixes:
@ -48,6 +50,10 @@ Other changes:
- :doc:`guides/main`: Modernized the *Getting Started* guide with tabbed - :doc:`guides/main`: Modernized the *Getting Started* guide with tabbed
sections and dropdown menus. Installation instructions have been streamlined, sections and dropdown menus. Installation instructions have been streamlined,
and a new subpage now provides additional setup details. and a new subpage now provides additional setup details.
- Documentation: introduced a new role ``conf`` for documenting configuration
options. This role provides consistent formatting and creates references
automatically. Applied it to :doc:`plugins/deezer`, :doc:`plugins/discogs`,
:doc:`plugins/musicbrainz` and :doc:`plugins/spotify` plugins documentation.
2.5.0 (October 11, 2025) 2.5.0 (October 11, 2025)
------------------------ ------------------------
@ -58,16 +64,18 @@ New features:
without storing or writing them. without storing or writing them.
- :doc:`plugins/convert`: Add a config option to disable writing metadata to - :doc:`plugins/convert`: Add a config option to disable writing metadata to
converted files. converted files.
- :doc:`plugins/discogs`: New config option `strip_disambiguation` to toggle - :doc:`plugins/discogs`: New config option
stripping discogs numeric disambiguation on artist and label fields. :conf:`plugins.discogs:strip_disambiguation` to toggle stripping discogs
numeric disambiguation on artist and label fields.
- :doc:`plugins/discogs` Added support for featured artists. :bug:`6038` - :doc:`plugins/discogs` Added support for featured artists. :bug:`6038`
- :doc:`plugins/discogs` New configuration option `featured_string` to change - :doc:`plugins/discogs` New configuration option
the default string used to join featured artists. The default string is :conf:`plugins.discogs:featured_string` to change the default string used to
`Feat.`. join featured artists. The default string is `Feat.`.
- :doc:`plugins/discogs` Support for `artist_credit` in Discogs tags. - :doc:`plugins/discogs` Support for `artist_credit` in Discogs tags.
:bug:`3354` :bug:`3354`
- :doc:`plugins/discogs` Support for name variations and config options to - :doc:`plugins/discogs` Support for name variations and config options to
specify where the variations are written. :bug:`3354` specify where the variations are written. :bug:`3354`
- :doc:`plugins/web` Support for `nexttrack` keyboard press
Bug fixes: Bug fixes:
@ -91,9 +99,10 @@ Bug fixes:
- :doc:`/plugins/fromfilename`: Fix :bug:`5218`, improve the code (refactor - :doc:`/plugins/fromfilename`: Fix :bug:`5218`, improve the code (refactor
regexps, allow for more cases, add some logging), add tests. regexps, allow for more cases, add some logging), add tests.
- Metadata source plugins: Fixed data source penalty calculation that was - Metadata source plugins: Fixed data source penalty calculation that was
incorrectly applied during import matching. The ``source_weight`` incorrectly applied during import matching. The
configuration option has been renamed to ``data_source_mismatch_penalty`` to :conf:`plugins.index:source_weight` configuration option has been renamed to
better reflect its purpose. :bug:`6066` :conf:`plugins.index:data_source_mismatch_penalty` to better reflect its
purpose. :bug:`6066`
Other changes: Other changes:
@ -139,12 +148,13 @@ New features:
separate plugin. The default :ref:`plugins-config` includes ``musicbrainz``, separate plugin. The default :ref:`plugins-config` includes ``musicbrainz``,
but if you've customized your ``plugins`` list in your configuration, you'll but if you've customized your ``plugins`` list in your configuration, you'll
need to explicitly add ``musicbrainz`` to continue using this functionality. need to explicitly add ``musicbrainz`` to continue using this functionality.
Configuration option ``musicbrainz.enabled`` has thus been deprecated. Configuration option :conf:`plugins.musicbrainz:enabled` has thus been
:bug:`2686` :bug:`4605` deprecated. :bug:`2686` :bug:`4605`
- :doc:`plugins/web`: Show notifications when a track plays. This uses the Media - :doc:`plugins/web`: Show notifications when a track plays. This uses the Media
Session API to customize media notifications. Session API to customize media notifications.
- :doc:`plugins/discogs`: Add configurable ``search_limit`` option to limit the - :doc:`plugins/discogs`: Add configurable :conf:`plugins.discogs:search_limit`
number of results returned by the Discogs metadata search queries. option to limit the number of results returned by the Discogs metadata search
queries.
- :doc:`plugins/discogs`: Implement ``track_for_id`` method to allow retrieving - :doc:`plugins/discogs`: Implement ``track_for_id`` method to allow retrieving
singletons by their Discogs ID. :bug:`4661` singletons by their Discogs ID. :bug:`4661`
- :doc:`plugins/replace`: Add new plugin. - :doc:`plugins/replace`: Add new plugin.
@ -159,12 +169,13 @@ New features:
be played for it to be counted as played instead of skipped. be played for it to be counted as played instead of skipped.
- :doc:`plugins/web`: Display artist and album as part of the search results. - :doc:`plugins/web`: Display artist and album as part of the search results.
- :doc:`plugins/spotify` :doc:`plugins/deezer`: Add new configuration option - :doc:`plugins/spotify` :doc:`plugins/deezer`: Add new configuration option
``search_limit`` to limit the number of results returned by search queries. :conf:`plugins.index:search_limit` to limit the number of results returned by
search queries.
Bug fixes: Bug fixes:
- :doc:`plugins/musicbrainz`: fix regression where user configured - :doc:`plugins/musicbrainz`: fix regression where user configured
``extra_tags`` have been read incorrectly. :bug:`5788` :conf:`plugins.musicbrainz:extra_tags` have been read incorrectly. :bug:`5788`
- tests: Fix library tests failing on Windows when run from outside ``D:/``. - tests: Fix library tests failing on Windows when run from outside ``D:/``.
:bug:`5802` :bug:`5802`
- Fix an issue where calling ``Library.add`` would cause the ``database_change`` - Fix an issue where calling ``Library.add`` would cause the ``database_change``
@ -196,9 +207,10 @@ Bug fixes:
For packagers: For packagers:
- Optional ``extra_tags`` parameter has been removed from - Optional :conf:`plugins.musicbrainz:extra_tags` parameter has been removed
``BeetsPlugin.candidates`` method signature since it is never passed in. If from ``BeetsPlugin.candidates`` method signature since it is never passed in.
you override this method in your plugin, feel free to remove this parameter. If you override this method in your plugin, feel free to remove this
parameter.
- Loosened ``typing_extensions`` dependency in pyproject.toml to apply to every - Loosened ``typing_extensions`` dependency in pyproject.toml to apply to every
python version. python version.
@ -554,8 +566,9 @@ New features:
:bug:`4348` :bug:`4348`
- Create the parental directories for database if they do not exist. :bug:`3808` - Create the parental directories for database if they do not exist. :bug:`3808`
:bug:`4327` :bug:`4327`
- :ref:`musicbrainz-config`: a new :ref:`musicbrainz.enabled` option allows - :ref:`musicbrainz-config`: a new :conf:`plugins.musicbrainz:enabled` option
disabling the MusicBrainz metadata source during the autotagging process allows disabling the MusicBrainz metadata source during the autotagging
process
- :doc:`/plugins/kodiupdate`: Now supports multiple kodi instances :bug:`4101` - :doc:`/plugins/kodiupdate`: Now supports multiple kodi instances :bug:`4101`
- Add the item fields ``bitrate_mode``, ``encoder_info`` and - Add the item fields ``bitrate_mode``, ``encoder_info`` and
``encoder_settings``. ``encoder_settings``.
@ -588,8 +601,8 @@ New features:
:bug:`4561` :bug:`4600` :bug:`4561` :bug:`4600`
- :ref:`musicbrainz-config`: MusicBrainz release pages often link to related - :ref:`musicbrainz-config`: MusicBrainz release pages often link to related
metadata sources like Discogs, Bandcamp, Spotify, Deezer and Beatport. When metadata sources like Discogs, Bandcamp, Spotify, Deezer and Beatport. When
enabled via the :ref:`musicbrainz.external_ids` options, release ID's will be enabled via the :conf:`plugins.musicbrainz:external_ids` options, release ID's
extracted from those URL's and imported to the library. :bug:`4220` will be extracted from those URL's and imported to the library. :bug:`4220`
- :doc:`/plugins/convert`: Add support for generating m3u8 playlists together - :doc:`/plugins/convert`: Add support for generating m3u8 playlists together
with converted media files. :bug:`4373` with converted media files. :bug:`4373`
- Fetch the ``release_group_title`` field from MusicBrainz. :bug:`4809` - Fetch the ``release_group_title`` field from MusicBrainz. :bug:`4809`
@ -943,8 +956,9 @@ Other new things:
- ``beet remove`` now also allows interactive selection of items from the query, - ``beet remove`` now also allows interactive selection of items from the query,
similar to ``beet modify``. similar to ``beet modify``.
- Enable HTTPS for MusicBrainz by default and add configuration option ``https`` - Enable HTTPS for MusicBrainz by default and add configuration option
for custom servers. See :ref:`musicbrainz-config` for more details. :conf:`plugins.musicbrainz:https` for custom servers. See
:ref:`musicbrainz-config` for more details.
- :doc:`/plugins/mpdstats`: Add a new ``strip_path`` option to help build the - :doc:`/plugins/mpdstats`: Add a new ``strip_path`` option to help build the
right local path from MPD information. right local path from MPD information.
- :doc:`/plugins/convert`: Conversion can now parallelize conversion jobs on - :doc:`/plugins/convert`: Conversion can now parallelize conversion jobs on
@ -964,8 +978,8 @@ Other new things:
server. server.
- :doc:`/plugins/subsonicupdate`: The plugin now automatically chooses between - :doc:`/plugins/subsonicupdate`: The plugin now automatically chooses between
token- and password-based authentication based on the server version. token- and password-based authentication based on the server version.
- A new :ref:`extra_tags` configuration option lets you use more metadata in - A new :conf:`plugins.musicbrainz:extra_tags` configuration option lets you use
MusicBrainz queries to further narrow the search. more metadata in MusicBrainz queries to further narrow the search.
- A new :doc:`/plugins/fish` adds `Fish shell`_ tab autocompletion to beets. - A new :doc:`/plugins/fish` adds `Fish shell`_ tab autocompletion to beets.
- :doc:`plugins/fetchart` and :doc:`plugins/embedart`: Added a new ``quality`` - :doc:`plugins/fetchart` and :doc:`plugins/embedart`: Added a new ``quality``
option that controls the quality of the image output when the image is option that controls the quality of the image output when the image is
@ -1019,9 +1033,9 @@ Other new things:
(and now deprecated) separate ``host``, ``port``, and ``contextpath`` config (and now deprecated) separate ``host``, ``port``, and ``contextpath`` config
options. As a consequence, the plugin can now talk to Subsonic over HTTPS. options. As a consequence, the plugin can now talk to Subsonic over HTTPS.
Thanks to :user:`jef`. :bug:`3449` Thanks to :user:`jef`. :bug:`3449`
- :doc:`/plugins/discogs`: The new ``index_tracks`` option enables incorporation - :doc:`/plugins/discogs`: The new :conf:`plugins.discogs:index_tracks` option
of work names and intra-work divisions into imported track titles. Thanks to enables incorporation of work names and intra-work divisions into imported
:user:`cole-miller`. :bug:`3459` track titles. Thanks to :user:`cole-miller`. :bug:`3459`
- :doc:`/plugins/web`: The query API now interprets backslashes as path - :doc:`/plugins/web`: The query API now interprets backslashes as path
separators to support path queries. Thanks to :user:`nmeum`. :bug:`3567` separators to support path queries. Thanks to :user:`nmeum`. :bug:`3567`
- ``beet import`` now handles tar archives with bzip2 or gzip compression. - ``beet import`` now handles tar archives with bzip2 or gzip compression.
@ -1035,9 +1049,9 @@ Other new things:
:user:`logan-arens`. :bug:`2947` :user:`logan-arens`. :bug:`2947`
- There is a new ``--plugins`` (or ``-p``) CLI flag to specify a list of plugins - There is a new ``--plugins`` (or ``-p``) CLI flag to specify a list of plugins
to load. to load.
- A new :ref:`genres` option fetches genre information from MusicBrainz. This - A new :conf:`plugins.musicbrainz:genres` option fetches genre information from
functionality depends on functionality that is currently unreleased in the MusicBrainz. This functionality depends on functionality that is currently
python-musicbrainzngs_ library: see PR `#266 unreleased in the python-musicbrainzngs_ library: see PR `#266
<https://github.com/alastair/python-musicbrainzngs/pull/266>`_. Thanks to <https://github.com/alastair/python-musicbrainzngs/pull/266>`_. Thanks to
:user:`aereaux`. :user:`aereaux`.
- :doc:`/plugins/replaygain`: Analysis now happens in parallel using the - :doc:`/plugins/replaygain`: Analysis now happens in parallel using the
@ -1077,9 +1091,10 @@ Fixes:
:bug:`3867` :bug:`3867`
- :doc:`/plugins/web`: Fixed a small bug that caused the album art path to be - :doc:`/plugins/web`: Fixed a small bug that caused the album art path to be
redacted even when ``include_paths`` option is set. :bug:`3866` redacted even when ``include_paths`` option is set. :bug:`3866`
- :doc:`/plugins/discogs`: Fixed a bug with the ``index_tracks`` option that - :doc:`/plugins/discogs`: Fixed a bug with the
sometimes caused the index to be discarded. Also, remove the extra semicolon :conf:`plugins.discogs:index_tracks` option that sometimes caused the index to
that was added when there is no index track. be discarded. Also, remove the extra semicolon that was added when there is no
index track.
- :doc:`/plugins/subsonicupdate`: The API client was using the ``POST`` method - :doc:`/plugins/subsonicupdate`: The API client was using the ``POST`` method
rather the ``GET`` method. Also includes better exception handling, response rather the ``GET`` method. Also includes better exception handling, response
parsing, and tests. parsing, and tests.
@ -2695,9 +2710,9 @@ Major new features and bigger changes:
analysis tool. Thanks to :user:`jmwatte`. :bug:`1343` analysis tool. Thanks to :user:`jmwatte`. :bug:`1343`
- A new ``filesize`` field on items indicates the number of bytes in the file. - A new ``filesize`` field on items indicates the number of bytes in the file.
:bug:`1291` :bug:`1291`
- A new :ref:`search_limit` configuration option allows you to specify how many - A new :conf:`plugins.index:search_limit` configuration option allows you to
search results you wish to see when looking up releases at MusicBrainz during specify how many search results you wish to see when looking up releases at
import. :bug:`1245` MusicBrainz during import. :bug:`1245`
- The importer now records the data source for a match in a new flexible - The importer now records the data source for a match in a new flexible
attribute ``data_source`` on items and albums. :bug:`1311` attribute ``data_source`` on items and albums. :bug:`1311`
- The colors used in the terminal interface are now configurable via the new - The colors used in the terminal interface are now configurable via the new

View file

@ -6,6 +6,11 @@
# -- Project information ----------------------------------------------------- # -- Project information -----------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information # https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information
import sys
from pathlib import Path
# Add custom extensions directory to path
sys.path.insert(0, str(Path(__file__).parent / "extensions"))
project = "beets" project = "beets"
AUTHOR = "Adrian Sampson" AUTHOR = "Adrian Sampson"
@ -26,6 +31,7 @@ extensions = [
"sphinx.ext.viewcode", "sphinx.ext.viewcode",
"sphinx_design", "sphinx_design",
"sphinx_copybutton", "sphinx_copybutton",
"conf",
] ]
autosummary_generate = True autosummary_generate = True

View file

@ -13,7 +13,7 @@ str.format-style string formatting. So you can write logging calls like this:
.. _pep 3101: https://www.python.org/dev/peps/pep-3101/ .. _pep 3101: https://www.python.org/dev/peps/pep-3101/
.. _standard python logging module: https://docs.python.org/2/library/logging.html .. _standard python logging module: https://docs.python.org/3/library/logging.html
When beets is in verbose mode, plugin messages are prefixed with the plugin name When beets is in verbose mode, plugin messages are prefixed with the plugin name
to make them easier to see. to make them easier to see.

142
docs/extensions/conf.py Normal file
View file

@ -0,0 +1,142 @@
"""Sphinx extension for simple configuration value documentation."""
from __future__ import annotations
from typing import TYPE_CHECKING, Any, ClassVar
from docutils import nodes
from docutils.parsers.rst import directives
from sphinx import addnodes
from sphinx.directives import ObjectDescription
from sphinx.domains import Domain, ObjType
from sphinx.roles import XRefRole
from sphinx.util.nodes import make_refnode
if TYPE_CHECKING:
from collections.abc import Iterable, Sequence
from docutils.nodes import Element
from docutils.parsers.rst.states import Inliner
from sphinx.addnodes import desc_signature, pending_xref
from sphinx.application import Sphinx
from sphinx.builders import Builder
from sphinx.environment import BuildEnvironment
from sphinx.util.typing import ExtensionMetadata, OptionSpec
class Conf(ObjectDescription[str]):
"""Directive for documenting a single configuration value."""
option_spec: ClassVar[OptionSpec] = {
"default": directives.unchanged,
}
def handle_signature(self, sig: str, signode: desc_signature) -> str:
"""Process the directive signature (the config name)."""
signode += addnodes.desc_name(sig, sig)
# Add default value if provided
if "default" in self.options:
signode += nodes.Text(" ")
default_container = nodes.inline("", "")
default_container += nodes.Text("(default: ")
default_container += nodes.literal("", self.options["default"])
default_container += nodes.Text(")")
signode += default_container
return sig
def add_target_and_index(
self, name: str, sig: str, signode: desc_signature
) -> None:
"""Add cross-reference target and index entry."""
target = f"conf-{name}"
if target not in self.state.document.ids:
signode["ids"].append(target)
self.state.document.note_explicit_target(signode)
# A unique full name which includes the document name
index_name = f"{self.env.docname.replace('/', '.')}:{name}"
# Register with the conf domain
domain = self.env.get_domain("conf")
domain.data["objects"][index_name] = (self.env.docname, target)
# Add to index
self.indexnode["entries"].append(
("single", f"{name} (configuration value)", target, "", None)
)
class ConfDomain(Domain):
"""Domain for simple configuration values."""
name = "conf"
label = "Simple Configuration"
object_types = {"conf": ObjType("conf", "conf")}
directives = {"conf": Conf}
roles = {"conf": XRefRole()}
initial_data: dict[str, Any] = {"objects": {}}
def get_objects(self) -> Iterable[tuple[str, str, str, str, str, int]]:
"""Return an iterable of object tuples for the inventory."""
for name, (docname, targetname) in self.data["objects"].items():
# Remove the document name prefix for display
display_name = name.split(":")[-1]
yield (name, display_name, "conf", docname, targetname, 1)
def resolve_xref(
self,
env: BuildEnvironment,
fromdocname: str,
builder: Builder,
typ: str,
target: str,
node: pending_xref,
contnode: Element,
) -> Element | None:
if entry := self.data["objects"].get(target):
docname, targetid = entry
return make_refnode(
builder, fromdocname, docname, targetid, contnode
)
return None
# sphinx.util.typing.RoleFunction
def conf_role(
name: str,
rawtext: str,
text: str,
lineno: int,
inliner: Inliner,
/,
options: dict[str, Any] | None = None,
content: Sequence[str] = (),
) -> tuple[list[nodes.Node], list[nodes.system_message]]:
"""Role for referencing configuration values."""
node = addnodes.pending_xref(
"",
refdomain="conf",
reftype="conf",
reftarget=text,
refwarn=True,
**(options or {}),
)
node += nodes.literal(text, text.split(":")[-1])
return [node], []
def setup(app: Sphinx) -> ExtensionMetadata:
app.add_domain(ConfDomain)
# register a top-level directive so users can use ".. conf:: ..."
app.add_directive("conf", Conf)
# Register role with short name
app.add_role("conf", conf_role)
return {
"version": "0.1",
"parallel_read_safe": True,
"parallel_write_safe": True,
}

View file

@ -35,15 +35,23 @@ Default
.. code-block:: yaml .. code-block:: yaml
deezer: deezer:
search_query_ascii: no
data_source_mismatch_penalty: 0.5 data_source_mismatch_penalty: 0.5
search_limit: 5 search_limit: 5
search_query_ascii: no
- **search_query_ascii**: If set to ``yes``, the search query will be converted .. conf:: search_query_ascii
to ASCII before being sent to Deezer. Converting searches to ASCII can enhance :default: no
search results in some cases, but in general, it is not recommended. For
instance ``artist:deadmau5 album:4×4`` will be converted to ``artist:deadmau5 If enabled, the search query will be converted to ASCII before being sent to
album:4x4`` (notice ``×!=x``). Default: ``no``. Deezer. Converting searches to ASCII can enhance search results in some cases,
but in general, it is not recommended. For instance, ``artist:deadmau5
album:4×4`` will be converted to ``artist:deadmau5 album:4x4`` (notice
``×!=x``).
.. include:: ./shared_metadata_source_config.rst
Commands
--------
The ``deezer`` plugin provides an additional command ``deezerupdate`` to update The ``deezer`` plugin provides an additional command ``deezerupdate`` to update
the ``rank`` information from Deezer. The ``rank`` (ranges from 0 to 1M) is a the ``rank`` information from Deezer. The ``rank`` (ranges from 0 to 1M) is a

View file

@ -71,67 +71,93 @@ Default
.. code-block:: yaml .. code-block:: yaml
discogs: discogs:
data_source_mismatch_penalty: 0.5
search_limit: 5
apikey: REDACTED apikey: REDACTED
apisecret: REDACTED apisecret: REDACTED
tokenfile: discogs_token.json tokenfile: discogs_token.json
user_token: REDACTED user_token:
index_tracks: no index_tracks: no
append_style_genre: no append_style_genre: no
separator: ', ' separator: ', '
strip_disambiguation: yes strip_disambiguation: yes
featured_string: Feat.
- **index_tracks**: Index tracks (see the `Discogs guidelines`_) along with
headers, mark divisions between distinct works on the same release or within
works. When enabled, beets will incorporate the names of the divisions
containing each track into the imported track's title. Default: ``no``.
For example, importing `divisions album`_ would result in track names like:
.. code-block:: text
Messiah, Part I: No.1: Sinfony
Messiah, Part II: No.22: Chorus- Behold The Lamb Of God
Athalia, Act I, Scene I: Sinfonia
whereas with ``index_tracks`` disabled you'd get:
.. code-block:: text
No.1: Sinfony
No.22: Chorus- Behold The Lamb Of God
Sinfonia
This option is useful when importing classical music.
- **append_style_genre**: Appends the Discogs style (if found) to the genre tag.
This can be useful if you want more granular genres to categorize your music.
For example, a release in Discogs might have a genre of "Electronic" and a
style of "Techno": enabling this setting would set the genre to be
"Electronic, Techno" (assuming default separator of ``", "``) instead of just
"Electronic". Default: ``False``
- **separator**: How to join multiple genre and style values from Discogs into a
string. Default: ``", "``
- **strip_disambiguation**: Discogs uses strings like ``"(4)"`` to mark distinct
artists and labels with the same name. If you'd like to use the discogs
disambiguation in your tags, you can disable it. Default: ``True``
- **featured_string**: Configure the string used for noting featured artists.
Useful if you prefer ``Featuring`` or ``ft.``. Default: ``Feat.``
- **anv**: These configuration option are dedicated to handling Artist Name
Variations (ANVs). Sometimes a release credits artists differently compared to
the majority of their work. For example, "Basement Jaxx" may be credited as
"Tha Jaxx" or "The Basement Jaxx".You can select any combination of these
config options to control where beets writes and stores the variation credit.
The default, shown below, writes variations to the artist_credit field.
.. code-block:: yaml
discogs:
anv: anv:
artist_credit: True artist_credit: yes
artist: False artist: no
album_artist: False album_artist: no
data_source_mismatch_penalty: 0.5
search_limit: 5
.. conf:: index_tracks
:default: no
Index tracks (see the `Discogs guidelines`_) along with headers, mark divisions
between distinct works on the same release or within works. When enabled,
beets will incorporate the names of the divisions containing each track into the
imported track's title.
For example, importing `divisions album`_ would result in track names like:
.. code-block:: text
Messiah, Part I: No.1: Sinfony
Messiah, Part II: No.22: Chorus- Behold The Lamb Of God
Athalia, Act I, Scene I: Sinfonia
whereas with ``index_tracks`` disabled you'd get:
.. code-block:: text
No.1: Sinfony
No.22: Chorus- Behold The Lamb Of God
Sinfonia
This option is useful when importing classical music.
.. conf:: append_style_genre
:default: no
Appends the Discogs style (if found) to the genre tag. This can be useful if
you want more granular genres to categorize your music. For example,
a release in Discogs might have a genre of "Electronic" and a style of
"Techno": enabling this setting would set the genre to be "Electronic,
Techno" (assuming default separator of ``", "``) instead of just
"Electronic".
.. conf:: separator
:default: ", "
How to join multiple genre and style values from Discogs into a string.
.. conf:: strip_disambiguation
:default: yes
Discogs uses strings like ``"(4)"`` to mark distinct artists and labels with
the same name. If you'd like to use the Discogs disambiguation in your tags,
you can disable this option.
.. conf:: featured_string
:default: Feat.
Configure the string used for noting featured artists. Useful if you prefer ``Featuring`` or ``ft.``.
.. conf:: anv
This configuration option is dedicated to handling Artist Name
Variations (ANVs). Sometimes a release credits artists differently compared to
the majority of their work. For example, "Basement Jaxx" may be credited as
"Tha Jaxx" or "The Basement Jaxx". You can select any combination of these
config options to control where beets writes and stores the variation credit.
The default, shown below, writes variations to the artist_credit field.
.. code-block:: yaml
discogs:
anv:
artist_credit: yes
artist: no
album_artist: no
.. include:: ./shared_metadata_source_config.rst
.. _discogs guidelines: https://support.discogs.com/hc/en-us/articles/360005055373-Database-Guidelines-12-Tracklisting#Index_Tracks_And_Headings .. _discogs guidelines: https://support.discogs.com/hc/en-us/articles/360005055373-Database-Guidelines-12-Tracklisting#Index_Tracks_And_Headings

View file

@ -70,7 +70,7 @@ These options match the options from the `Python csv module`_.
.. _python csv module: https://docs.python.org/3/library/csv.html#csv-fmt-params .. _python csv module: https://docs.python.org/3/library/csv.html#csv-fmt-params
.. _python json module: https://docs.python.org/2/library/json.html#basic-usage .. _python json module: https://docs.python.org/3/library/json.html#basic-usage
The default options look like this: The default options look like this:

View file

@ -50,65 +50,7 @@ Using Metadata Source Plugins
We provide several :ref:`autotagger_extensions` that fetch metadata from online We provide several :ref:`autotagger_extensions` that fetch metadata from online
databases. They share the following configuration options: databases. They share the following configuration options:
.. _data_source_mismatch_penalty: .. include:: ./shared_metadata_source_config.rst
- **data_source_mismatch_penalty**: Penalty applied when the data source of a
match candidate differs from the original source of your existing tracks. Any
decimal number between 0.0 and 1.0. Default: ``0.5``.
This setting controls how much to penalize matches from different metadata
sources during import. The penalty is applied when beets detects that a match
candidate comes from a different data source than what appears to be the
original source of your music collection.
**Example configurations:**
.. code-block:: yaml
# Prefer MusicBrainz over Discogs when sources don't match
plugins: musicbrainz discogs
musicbrainz:
data_source_mismatch_penalty: 0.3 # Lower penalty = preferred
discogs:
data_source_mismatch_penalty: 0.8 # Higher penalty = less preferred
.. code-block:: yaml
# Do not penalise candidates from Discogs at all
plugins: musicbrainz discogs
musicbrainz:
data_source_mismatch_penalty: 0.5
discogs:
data_source_mismatch_penalty: 0.0
.. code-block:: yaml
# Disable cross-source penalties entirely
plugins: musicbrainz discogs
musicbrainz:
data_source_mismatch_penalty: 0.0
discogs:
data_source_mismatch_penalty: 0.0
.. tip::
The last configuration is equivalent to setting:
.. code-block:: yaml
match:
distance_weights:
data_source: 0.0 # Disable data source matching
- **source_weight**
.. deprecated:: 2.5 Use `data_source_mismatch_penalty`_ instead.
- **search_limit**: Maximum number of search results to consider. Default:
``5``.
.. toctree:: .. toctree::
:hidden: :hidden:

View file

@ -26,8 +26,6 @@ Default
.. code-block:: yaml .. code-block:: yaml
musicbrainz: musicbrainz:
data_source_mismatch_penalty: 0.5
search_limit: 5
host: musicbrainz.org host: musicbrainz.org
https: no https: no
ratelimit: 1 ratelimit: 1
@ -41,122 +39,107 @@ Default
deezer: no deezer: no
beatport: no beatport: no
tidal: no tidal: no
data_source_mismatch_penalty: 0.5
search_limit: 5
You can instruct beets to use `your own MusicBrainz database .. conf:: host
<https://musicbrainz.org/doc/MusicBrainz_Server/Setup>`__ instead of the :default: musicbrainz.org
`main server`_. Use the ``host``, ``https`` and ``ratelimit`` options under a The Web server hostname (and port, optionally) that will be contacted by beets.
``musicbrainz:`` header, like so You can use this to configure beets to use `your own MusicBrainz database
<https://musicbrainz.org/doc/MusicBrainz_Server/Setup>`__ instead of the
`main server`_.
.. code-block:: yaml The server must have search indices enabled (see `Building search indexes`_).
musicbrainz: Example:
host: localhost:5000
https: no
ratelimit: 100
The ``host`` key, of course, controls the Web server hostname (and port, .. code-block:: yaml
optionally) that will be contacted by beets (default: musicbrainz.org). The
``https`` key makes the client use HTTPS instead of HTTP. This setting applies
only to custom servers. The official MusicBrainz server always uses HTTPS.
(Default: no.) The server must have search indices enabled (see `Building search
indexes`_).
The ``ratelimit`` option, an integer, controls the number of Web service musicbrainz:
requests per second (default: 1). **Do not change the rate limit setting** if host: localhost:5000
you're using the main MusicBrainz server---on this public server, you're
limited_ to one request per second. .. conf:: https
:default: no
Makes the client use HTTPS instead of HTTP. This setting applies only to custom
servers. The official MusicBrainz server always uses HTTPS.
.. conf:: ratelimit
:default: 1
Controls the number of Web service requests per second.
**Do not change the rate limit setting** if you're using the main MusicBrainz
server---on this public server, you're limited_ to one request per second.
.. conf:: ratelimit_interval
:default: 1.0
The time interval (in seconds) for the rate limit.
.. conf:: enabled
:default: yes
.. deprecated:: 2.4 Add ``musicbrainz`` to the ``plugins`` list instead.
.. conf:: extra_tags
:default: []
By default, beets will use only the artist, album, and track count to query
MusicBrainz. Additional tags to be queried can be supplied with the
``extra_tags`` setting.
This setting should improve the autotagger results if the metadata with the
given tags match the metadata returned by MusicBrainz.
Note that the only tags supported by this setting are: ``barcode``,
``catalognum``, ``country``, ``label``, ``media``, and ``year``.
Example:
.. code-block:: yaml
musicbrainz:
extra_tags: [barcode, catalognum, country, label, media, year]
.. conf:: genres
:default: no
Use MusicBrainz genre tags to populate (and replace if it's already set) the
``genre`` tag. This will make it a list of all the genres tagged for the release
and the release-group on MusicBrainz, separated by "; " and sorted by the total
number of votes.
.. conf:: external_ids
**Default**
.. code-block:: yaml
musicbrainz:
external_ids:
discogs: no
spotify: no
bandcamp: no
beatport: no
deezer: no
tidal: no
Set any of the ``external_ids`` options to ``yes`` to enable the MusicBrainz
importer to look for links to related metadata sources. If such a link is
available the release ID will be extracted from the URL provided and imported to
the beets library.
The library fields of the corresponding :ref:`autotagger_extensions` are used to
save the data as flexible attributes (``discogs_album_id``, ``bandcamp_album_id``, ``spotify_album_id``,
``beatport_album_id``, ``deezer_album_id``, ``tidal_album_id``). On re-imports
existing data will be overwritten.
.. include:: ./shared_metadata_source_config.rst
.. _building search indexes: https://musicbrainz.org/doc/Development/Search_server_setup .. _building search indexes: https://musicbrainz.org/doc/Development/Search_server_setup
.. _limited: https://musicbrainz.org/doc/XML_Web_Service/Rate_Limiting .. _limited: https://musicbrainz.org/doc/XML_Web_Service/Rate_Limiting
.. _main server: https://musicbrainz.org/ .. _main server: https://musicbrainz.org/
.. _musicbrainz.enabled:
enabled
+++++++
.. deprecated:: 2.4 Add ``musicbrainz`` to the ``plugins`` list instead.
This option allows you to disable using MusicBrainz as a metadata source. This
applies if you use plugins that fetch data from alternative sources and should
make the import process quicker.
Default: ``yes``.
.. _search_limit:
search_limit
++++++++++++
The number of matches returned when sending search queries to the MusicBrainz
server.
Default: ``5``.
searchlimit
+++++++++++
.. deprecated:: 2.4 Use `search_limit`_.
.. _extra_tags:
extra_tags
++++++++++
By default, beets will use only the artist, album, and track count to query
MusicBrainz. Additional tags to be queried can be supplied with the
``extra_tags`` setting. For example
.. code-block:: yaml
musicbrainz:
extra_tags: [barcode, catalognum, country, label, media, year]
This setting should improve the autotagger results if the metadata with the
given tags match the metadata returned by MusicBrainz.
Note that the only tags supported by this setting are the ones listed in the
above example.
Default: ``[]``
.. _genres:
genres
++++++
Use MusicBrainz genre tags to populate (and replace if it's already set) the
``genre`` tag. This will make it a list of all the genres tagged for the release
and the release-group on MusicBrainz, separated by "; " and sorted by the total
number of votes. Default: ``no``
.. _musicbrainz.external_ids:
external_ids
++++++++++++
Set any of the ``external_ids`` options to ``yes`` to enable the MusicBrainz
importer to look for links to related metadata sources. If such a link is
available the release ID will be extracted from the URL provided and imported to
the beets library
.. code-block:: yaml
musicbrainz:
external_ids:
discogs: yes
spotify: yes
bandcamp: yes
beatport: yes
deezer: yes
tidal: yes
The library fields of the corresponding :ref:`autotagger_extensions` are used to
save the data (``discogs_albumid``, ``bandcamp_album_id``, ``spotify_album_id``,
``beatport_album_id``, ``deezer_album_id``, ``tidal_album_id``). On re-imports
existing data will be overwritten.
The default of all options is ``no``.

View file

@ -107,6 +107,15 @@ string, use ``$args`` to indicate where to insert them. For example:
indicates that you need to insert extra arguments before specifying the indicates that you need to insert extra arguments before specifying the
playlist. playlist.
Some players require a different syntax. For example, with ``mpv`` the optional
``$playlist`` variable can be used to match the syntax of the ``--playlist``
option:
::
play:
command: mpv $args --playlist=$playlist
The ``--yes`` (or ``-y``) flag to the ``play`` command will skip the warning The ``--yes`` (or ``-y``) flag to the ``play`` command will skip the warning
message if you choose to play more items than the **warning_threshold** value message if you choose to play more items than the **warning_threshold** value
usually allows. usually allows.
@ -123,4 +132,4 @@ until they are externally wiped could be an issue for privacy or storage
reasons. If this is the case for you, you might want to use the ``raw`` config reasons. If this is the case for you, you might want to use the ``raw`` config
option described above. option described above.
.. _tempfile.tempdir: https://docs.python.org/2/library/tempfile.html#tempfile.tempdir .. _tempfile.tempdir: https://docs.python.org/3/library/tempfile.html#tempfile.tempdir

View file

@ -0,0 +1,65 @@
.. _data_source_mismatch_penalty:
.. conf:: data_source_mismatch_penalty
:default: 0.5
Penalty applied when the data source of a
match candidate differs from the original source of your existing tracks. Any
decimal number between 0.0 and 1.0
This setting controls how much to penalize matches from different metadata
sources during import. The penalty is applied when beets detects that a match
candidate comes from a different data source than what appears to be the
original source of your music collection.
**Example configurations:**
.. code-block:: yaml
# Prefer MusicBrainz over Discogs when sources don't match
plugins: musicbrainz discogs
musicbrainz:
data_source_mismatch_penalty: 0.3 # Lower penalty = preferred
discogs:
data_source_mismatch_penalty: 0.8 # Higher penalty = less preferred
.. code-block:: yaml
# Do not penalise candidates from Discogs at all
plugins: musicbrainz discogs
musicbrainz:
data_source_mismatch_penalty: 0.5
discogs:
data_source_mismatch_penalty: 0.0
.. code-block:: yaml
# Disable cross-source penalties entirely
plugins: musicbrainz discogs
musicbrainz:
data_source_mismatch_penalty: 0.0
discogs:
data_source_mismatch_penalty: 0.0
.. tip::
The last configuration is equivalent to setting:
.. code-block:: yaml
match:
distance_weights:
data_source: 0.0 # Disable data source matching
.. conf:: source_weight
:default: 0.5
.. deprecated:: 2.5 Use `data_source_mismatch_penalty`_ instead.
.. conf:: search_limit
:default: 5
Maximum number of search results to return.

View file

@ -73,8 +73,6 @@ Default
.. code-block:: yaml .. code-block:: yaml
spotify: spotify:
data_source_mismatch_penalty: 0.5
search_limit: 5
mode: list mode: list
region_filter: region_filter:
show_failures: no show_failures: no
@ -84,59 +82,67 @@ Default
client_id: REDACTED client_id: REDACTED
client_secret: REDACTED client_secret: REDACTED
tokenfile: spotify_token.json tokenfile: spotify_token.json
data_source_mismatch_penalty: 0.5
search_limit: 5
- **mode**: One of the following: .. conf:: mode
:default: list
- ``list``: Print out the playlist as a list of links. This list can then Controls how the playlist is output:
be pasted in to a new or existing Spotify playlist.
- ``open``: This mode actually sends a link to your default browser with
instructions to open Spotify with the playlist you created. Until this
has been tested on all platforms, it will remain optional.
Default: ``list``. - ``list``: Print out the playlist as a list of links. This list can then
be pasted in to a new or existing Spotify playlist.
- ``open``: This mode actually sends a link to your default browser with
instructions to open Spotify with the playlist you created. Until this
has been tested on all platforms, it will remain optional.
- **region_filter**: A two-character country abbreviation, to limit results to .. conf:: region_filter
that market. Default: None. :default:
- **show_failures**: List each lookup that does not return a Spotify ID (and
therefore cannot be added to a playlist). Default: ``no``.
- **tiebreak**: How to choose the track if there is more than one identical
result. For example, there might be multiple releases of the same album. The
options are ``popularity`` and ``first`` (to just choose the first match
returned). Default: ``popularity``.
- **regex**: An array of regex transformations to perform on the
track/album/artist fields before sending them to Spotify. Can be useful for
changing certain abbreviations, like ft. -> feat. See the examples below.
Default: None.
- **search_query_ascii**: If set to ``yes``, the search query will be converted
to ASCII before being sent to Spotify. Converting searches to ASCII can
enhance search results in some cases, but in general, it is not recommended.
For instance ``artist:deadmau5 album:4×4`` will be converted to
``artist:deadmau5 album:4x4`` (notice ``×!=x``). Default: ``no``.
Here's an example: A two-character country abbreviation, to limit results to that market.
:: .. conf:: show_failures
:default: no
spotify: List each lookup that does not return a Spotify ID (and therefore cannot be
data_source_mismatch_penalty: 0.7 added to a playlist).
mode: open
region_filter: US
show_failures: on
tiebreak: first
search_query_ascii: no
regex: [ .. conf:: tiebreak
{ :default: popularity
field: "albumartist", # Field in the item object to regex.
search: "Something", # String to look for. How to choose the candidate if there is more than one identical result. For
replace: "Replaced" # Replacement value. example, there might be multiple releases of the same album.
},
{ - ``popularity``: pick the more popular candidate
field: "title", - ``first``: pick the first candidate
search: "Something Else",
replace: "AlsoReplaced" .. conf:: regex
} :default: []
]
An array of regex transformations to perform on the track/album/artist fields
before sending them to Spotify. Can be useful for changing certain
abbreviations, like ft. -> feat. For example:
.. code-block:: yaml
regex:
- field: albumartist
search: Something
replace: Replaced
- field: title
search: Something Else
replace: AlsoReplaced
.. conf:: search_query_ascii
:default: no
If enabled, the search query will be converted to ASCII before being sent to
Spotify. Converting searches to ASCII can enhance search results in some
cases, but in general, it is not recommended. For instance,
``artist:deadmau5 album:4×4`` will be converted to ``artist:deadmau5
album:4x4`` (notice ``×!=x``).
.. include:: ./shared_metadata_source_config.rst
Obtaining Track Popularity and Audio Features from Spotify Obtaining Track Popularity and Audio Features from Spotify
---------------------------------------------------------- ----------------------------------------------------------

View file

@ -376,7 +376,7 @@ terminal_encoding
~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~
The text encoding, as `known to Python The text encoding, as `known to Python
<https://docs.python.org/2/library/codecs.html#standard-encodings>`__, to use <https://docs.python.org/3/library/codecs.html#standard-encodings>`__, to use
for messages printed to the standard output. It's also used to read messages for messages printed to the standard output. It's also used to read messages
from the standard input. By default, this is determined automatically from the from the standard input. By default, this is determined automatically from the
locale environment variables. locale environment variables.

View file

@ -120,7 +120,7 @@ def create_rst_replacements() -> list[Replacement]:
# Replace Sphinx directives by documentation URLs, e.g., # Replace Sphinx directives by documentation URLs, e.g.,
# :ref:`/plugins/autobpm` -> [AutoBPM Plugin](DOCS/plugins/autobpm.html) # :ref:`/plugins/autobpm` -> [AutoBPM Plugin](DOCS/plugins/autobpm.html)
( (
r":(?:ref|doc|class):`+(?:([^`<]+)<)?/?([\w./_-]+)>?`+", r":(?:ref|doc|class|conf):`+(?:([^`<]+)<)?/?([\w.:/_-]+)>?`+",
lambda m: make_ref_link(m[2], m[1]), lambda m: make_ref_link(m[2], m[1]),
), ),
# Convert command references to documentation URLs # Convert command references to documentation URLs

15
poetry.lock generated
View file

@ -3473,6 +3473,17 @@ files = [
[package.dependencies] [package.dependencies]
types-html5lib = "*" types-html5lib = "*"
[[package]]
name = "types-docutils"
version = "0.22.2.20251006"
description = "Typing stubs for docutils"
optional = false
python-versions = ">=3.9"
files = [
{file = "types_docutils-0.22.2.20251006-py3-none-any.whl", hash = "sha256:1e61afdeb4fab4ae802034deea3e853ced5c9b5e1d156179000cb68c85daf384"},
{file = "types_docutils-0.22.2.20251006.tar.gz", hash = "sha256:c36c0459106eda39e908e9147bcff9dbd88535975cde399433c428a517b9e3b2"},
]
[[package]] [[package]]
name = "types-flask-cors" name = "types-flask-cors"
version = "6.0.0.20250520" version = "6.0.0.20250520"
@ -3650,7 +3661,7 @@ beatport = ["requests-oauthlib"]
bpd = ["PyGObject"] bpd = ["PyGObject"]
chroma = ["pyacoustid"] chroma = ["pyacoustid"]
discogs = ["python3-discogs-client"] discogs = ["python3-discogs-client"]
docs = ["pydata-sphinx-theme", "sphinx", "sphinx-copybutton", "sphinx-design"] docs = ["docutils", "pydata-sphinx-theme", "sphinx", "sphinx-copybutton", "sphinx-design"]
embedart = ["Pillow"] embedart = ["Pillow"]
embyupdate = ["requests"] embyupdate = ["requests"]
fetchart = ["Pillow", "beautifulsoup4", "langdetect", "requests"] fetchart = ["Pillow", "beautifulsoup4", "langdetect", "requests"]
@ -3672,4 +3683,4 @@ web = ["flask", "flask-cors"]
[metadata] [metadata]
lock-version = "2.0" lock-version = "2.0"
python-versions = ">=3.9,<4" python-versions = ">=3.9,<4"
content-hash = "1db39186aca430ef6f1fd9e51b9dcc3ed91880a458bc21b22d950ed8589fdf5a" content-hash = "aedfeb1ac78ae0120855c6a7d6f35963c63cc50a8750142c95dd07ffd213683f"

View file

@ -77,10 +77,11 @@ resampy = { version = ">=0.4.3", optional = true }
requests-oauthlib = { version = ">=0.6.1", optional = true } requests-oauthlib = { version = ">=0.6.1", optional = true }
soco = { version = "*", optional = true } soco = { version = "*", optional = true }
docutils = { version = ">=0.20.1", optional = true }
pydata-sphinx-theme = { version = "*", optional = true } pydata-sphinx-theme = { version = "*", optional = true }
sphinx = { version = "*", optional = true } sphinx = { version = "*", optional = true }
sphinx-design = { version = "^0.6.1", optional = true } sphinx-design = { version = ">=0.6.1", optional = true }
sphinx-copybutton = { version = "^0.5.2", optional = true } sphinx-copybutton = { version = ">=0.5.2", optional = true }
[tool.poetry.group.test.dependencies] [tool.poetry.group.test.dependencies]
beautifulsoup4 = "*" beautifulsoup4 = "*"
@ -109,6 +110,7 @@ sphinx-lint = ">=1.0.0"
[tool.poetry.group.typing.dependencies] [tool.poetry.group.typing.dependencies]
mypy = "*" mypy = "*"
types-beautifulsoup4 = "*" types-beautifulsoup4 = "*"
types-docutils = ">=0.22.2.20251006"
types-mock = "*" types-mock = "*"
types-Flask-Cors = "*" types-Flask-Cors = "*"
types-Pillow = "*" types-Pillow = "*"
@ -131,7 +133,14 @@ beatport = ["requests-oauthlib"]
bpd = ["PyGObject"] # gobject-introspection, gstreamer1.0-plugins-base, python3-gst-1.0 bpd = ["PyGObject"] # gobject-introspection, gstreamer1.0-plugins-base, python3-gst-1.0
chroma = ["pyacoustid"] # chromaprint or fpcalc chroma = ["pyacoustid"] # chromaprint or fpcalc
# convert # ffmpeg # convert # ffmpeg
docs = ["pydata-sphinx-theme", "sphinx", "sphinx-lint", "sphinx-design", "sphinx-copybutton"] docs = [
"docutils",
"pydata-sphinx-theme",
"sphinx",
"sphinx-lint",
"sphinx-design",
"sphinx-copybutton",
]
discogs = ["python3-discogs-client"] discogs = ["python3-discogs-client"]
embedart = ["Pillow"] # ImageMagick embedart = ["Pillow"] # ImageMagick
embyupdate = ["requests"] embyupdate = ["requests"]

View file

@ -105,6 +105,19 @@ class PlayPluginTest(CleanupModulesMixin, PluginTestCase):
open_mock.assert_called_once_with([self.item.path], "echo") open_mock.assert_called_once_with([self.item.path], "echo")
def test_pls_marker(self, open_mock):
self.config["play"]["command"] = (
"echo --some params --playlist=$playlist --some-more params"
)
self.run_command("play", "nice")
open_mock.assert_called_once
commandstr = open_mock.call_args_list[0][0][1]
assert commandstr.startswith("echo --some params --playlist=")
assert commandstr.endswith(" --some-more params")
def test_not_found(self, open_mock): def test_not_found(self, open_mock):
self.run_command("play", "not found") self.run_command("play", "not found")