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:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
- name: Get all updated Python files
id: changed-python-files

View file

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

View file

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

View file

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

View file

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

3
.gitignore vendored
View file

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

View file

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

View file

@ -47,6 +47,7 @@ from typing import (
NamedTuple,
TypeVar,
Union,
cast,
)
from unidecode import unidecode
@ -1052,7 +1053,7 @@ def par_map(transform: Callable[[T], Any], items: Sequence[T]) -> None:
pool.join()
class cached_classproperty:
class cached_classproperty(Generic[T]):
"""Descriptor implementing cached class properties.
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.
"""
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,
# 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]], ...]"
#
# 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."""
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."""
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."""
key = owner, self.name
key: tuple[type[object], str] = owner, self.name
if key not in self.cache:
self.cache[key] = self.getter(owner)
return self.cache[key]
return cast(T, self.cache[key])
class LazySharedInstance(Generic[T]):

View file

@ -132,9 +132,9 @@ class DiscogsPlugin(MetadataSourcePlugin):
"user_token": "",
"separator": ", ",
"index_tracks": False,
"featured_string": "Feat.",
"append_style_genre": False,
"strip_disambiguation": True,
"featured_string": "Feat.",
"anv": {
"artist_credit": True,
"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.
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(
command_str,
@ -132,8 +137,23 @@ class PlayPlugin(BeetsPlugin):
return
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)
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,
# cancel, otherwise proceed with play command.
if opts.yes or not self._exceeds_threshold(
@ -162,6 +182,7 @@ class PlayPlugin(BeetsPlugin):
return paths
else:
return [self._create_tmp_playlist(paths)]
return [shlex.quote(self._create_tmp_playlist(paths))]
def _exceeds_threshold(
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),
'ended': _.bind(this.audioEnded, this)
});
if ("mediaSession" in navigator) {
navigator.mediaSession.setActionHandler("nexttrack", () => {
this.playNext();
});
}
},
showItems: function(items) {
this.shownItems = items;
@ -306,7 +311,9 @@ var AppView = Backbone.View.extend({
},
audioEnded: function() {
this.playingItem.entryView.setPlaying(false);
this.playNext();
},
playNext: function(){
// Try to play the next track.
var idx = this.shownItems.indexOf(this.playingItem);
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 to skip the processing of artist and
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:
@ -48,6 +50,10 @@ Other changes:
- :doc:`guides/main`: Modernized the *Getting Started* guide with tabbed
sections and dropdown menus. Installation instructions have been streamlined,
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)
------------------------
@ -58,16 +64,18 @@ New features:
without storing or writing them.
- :doc:`plugins/convert`: Add a config option to disable writing metadata to
converted files.
- :doc:`plugins/discogs`: New config option `strip_disambiguation` to toggle
stripping discogs numeric disambiguation on artist and label fields.
- :doc:`plugins/discogs`: New config option
: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` New configuration option `featured_string` to change
the default string used to join featured artists. The default string is
`Feat.`.
- :doc:`plugins/discogs` New configuration option
:conf:`plugins.discogs:featured_string` to change the default string used to
join featured artists. The default string is `Feat.`.
- :doc:`plugins/discogs` Support for `artist_credit` in Discogs tags.
:bug:`3354`
- :doc:`plugins/discogs` Support for name variations and config options to
specify where the variations are written. :bug:`3354`
- :doc:`plugins/web` Support for `nexttrack` keyboard press
Bug fixes:
@ -91,9 +99,10 @@ Bug fixes:
- :doc:`/plugins/fromfilename`: Fix :bug:`5218`, improve the code (refactor
regexps, allow for more cases, add some logging), add tests.
- Metadata source plugins: Fixed data source penalty calculation that was
incorrectly applied during import matching. The ``source_weight``
configuration option has been renamed to ``data_source_mismatch_penalty`` to
better reflect its purpose. :bug:`6066`
incorrectly applied during import matching. The
:conf:`plugins.index:source_weight` configuration option has been renamed to
:conf:`plugins.index:data_source_mismatch_penalty` to better reflect its
purpose. :bug:`6066`
Other changes:
@ -139,12 +148,13 @@ New features:
separate plugin. The default :ref:`plugins-config` includes ``musicbrainz``,
but if you've customized your ``plugins`` list in your configuration, you'll
need to explicitly add ``musicbrainz`` to continue using this functionality.
Configuration option ``musicbrainz.enabled`` has thus been deprecated.
:bug:`2686` :bug:`4605`
Configuration option :conf:`plugins.musicbrainz:enabled` has thus been
deprecated. :bug:`2686` :bug:`4605`
- :doc:`plugins/web`: Show notifications when a track plays. This uses the Media
Session API to customize media notifications.
- :doc:`plugins/discogs`: Add configurable ``search_limit`` option to limit the
number of results returned by the Discogs metadata search queries.
- :doc:`plugins/discogs`: Add configurable :conf:`plugins.discogs:search_limit`
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
singletons by their Discogs ID. :bug:`4661`
- :doc:`plugins/replace`: Add new plugin.
@ -159,12 +169,13 @@ New features:
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/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:
- :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:/``.
:bug:`5802`
- Fix an issue where calling ``Library.add`` would cause the ``database_change``
@ -196,9 +207,10 @@ Bug fixes:
For packagers:
- Optional ``extra_tags`` parameter has been removed from
``BeetsPlugin.candidates`` method signature since it is never passed in. If
you override this method in your plugin, feel free to remove this parameter.
- Optional :conf:`plugins.musicbrainz:extra_tags` parameter has been removed
from ``BeetsPlugin.candidates`` method signature since it is never passed in.
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
python version.
@ -554,8 +566,9 @@ New features:
:bug:`4348`
- Create the parental directories for database if they do not exist. :bug:`3808`
:bug:`4327`
- :ref:`musicbrainz-config`: a new :ref:`musicbrainz.enabled` option allows
disabling the MusicBrainz metadata source during the autotagging process
- :ref:`musicbrainz-config`: a new :conf:`plugins.musicbrainz:enabled` option
allows disabling the MusicBrainz metadata source during the autotagging
process
- :doc:`/plugins/kodiupdate`: Now supports multiple kodi instances :bug:`4101`
- Add the item fields ``bitrate_mode``, ``encoder_info`` and
``encoder_settings``.
@ -588,8 +601,8 @@ New features:
:bug:`4561` :bug:`4600`
- :ref:`musicbrainz-config`: MusicBrainz release pages often link to related
metadata sources like Discogs, Bandcamp, Spotify, Deezer and Beatport. When
enabled via the :ref:`musicbrainz.external_ids` options, release ID's will be
extracted from those URL's and imported to the library. :bug:`4220`
enabled via the :conf:`plugins.musicbrainz:external_ids` options, release ID's
will be extracted from those URL's and imported to the library. :bug:`4220`
- :doc:`/plugins/convert`: Add support for generating m3u8 playlists together
with converted media files. :bug:`4373`
- 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,
similar to ``beet modify``.
- Enable HTTPS for MusicBrainz by default and add configuration option ``https``
for custom servers. See :ref:`musicbrainz-config` for more details.
- Enable HTTPS for MusicBrainz by default and add configuration option
: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
right local path from MPD information.
- :doc:`/plugins/convert`: Conversion can now parallelize conversion jobs on
@ -964,8 +978,8 @@ Other new things:
server.
- :doc:`/plugins/subsonicupdate`: The plugin now automatically chooses between
token- and password-based authentication based on the server version.
- A new :ref:`extra_tags` configuration option lets you use more metadata in
MusicBrainz queries to further narrow the search.
- A new :conf:`plugins.musicbrainz:extra_tags` configuration option lets you use
more metadata in MusicBrainz queries to further narrow the search.
- A new :doc:`/plugins/fish` adds `Fish shell`_ tab autocompletion to beets.
- :doc:`plugins/fetchart` and :doc:`plugins/embedart`: Added a new ``quality``
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
options. As a consequence, the plugin can now talk to Subsonic over HTTPS.
Thanks to :user:`jef`. :bug:`3449`
- :doc:`/plugins/discogs`: The new ``index_tracks`` option enables incorporation
of work names and intra-work divisions into imported track titles. Thanks to
:user:`cole-miller`. :bug:`3459`
- :doc:`/plugins/discogs`: The new :conf:`plugins.discogs:index_tracks` option
enables incorporation of work names and intra-work divisions into imported
track titles. Thanks to :user:`cole-miller`. :bug:`3459`
- :doc:`/plugins/web`: The query API now interprets backslashes as path
separators to support path queries. Thanks to :user:`nmeum`. :bug:`3567`
- ``beet import`` now handles tar archives with bzip2 or gzip compression.
@ -1035,9 +1049,9 @@ Other new things:
:user:`logan-arens`. :bug:`2947`
- There is a new ``--plugins`` (or ``-p``) CLI flag to specify a list of plugins
to load.
- A new :ref:`genres` option fetches genre information from MusicBrainz. This
functionality depends on functionality that is currently unreleased in the
python-musicbrainzngs_ library: see PR `#266
- A new :conf:`plugins.musicbrainz:genres` option fetches genre information from
MusicBrainz. This functionality depends on functionality that is currently
unreleased in the python-musicbrainzngs_ library: see PR `#266
<https://github.com/alastair/python-musicbrainzngs/pull/266>`_. Thanks to
:user:`aereaux`.
- :doc:`/plugins/replaygain`: Analysis now happens in parallel using the
@ -1077,9 +1091,10 @@ Fixes:
:bug:`3867`
- :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`
- :doc:`/plugins/discogs`: Fixed a bug with the ``index_tracks`` option that
sometimes caused the index to be discarded. Also, remove the extra semicolon
that was added when there is no index track.
- :doc:`/plugins/discogs`: Fixed a bug with the
:conf:`plugins.discogs:index_tracks` option that sometimes caused the index to
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
rather the ``GET`` method. Also includes better exception handling, response
parsing, and tests.
@ -2695,9 +2710,9 @@ Major new features and bigger changes:
analysis tool. Thanks to :user:`jmwatte`. :bug:`1343`
- A new ``filesize`` field on items indicates the number of bytes in the file.
:bug:`1291`
- A new :ref:`search_limit` configuration option allows you to specify how many
search results you wish to see when looking up releases at MusicBrainz during
import. :bug:`1245`
- A new :conf:`plugins.index:search_limit` configuration option allows you to
specify how many search results you wish to see when looking up releases at
MusicBrainz during import. :bug:`1245`
- The importer now records the data source for a match in a new flexible
attribute ``data_source`` on items and albums. :bug:`1311`
- The colors used in the terminal interface are now configurable via the new

View file

@ -6,6 +6,11 @@
# -- 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"
AUTHOR = "Adrian Sampson"
@ -26,6 +31,7 @@ extensions = [
"sphinx.ext.viewcode",
"sphinx_design",
"sphinx_copybutton",
"conf",
]
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/
.. _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
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
deezer:
search_query_ascii: no
data_source_mismatch_penalty: 0.5
search_limit: 5
search_query_ascii: no
- **search_query_ascii**: If set to ``yes``, the search query will be converted
to ASCII before being sent to 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``). Default: ``no``.
.. conf:: search_query_ascii
:default: no
If enabled, the search query will be converted to ASCII before being sent to
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 ``rank`` information from Deezer. The ``rank`` (ranges from 0 to 1M) is a

View file

@ -71,21 +71,29 @@ Default
.. code-block:: yaml
discogs:
data_source_mismatch_penalty: 0.5
search_limit: 5
apikey: REDACTED
apisecret: REDACTED
tokenfile: discogs_token.json
user_token: REDACTED
user_token:
index_tracks: no
append_style_genre: no
separator: ', '
strip_disambiguation: yes
featured_string: Feat.
anv:
artist_credit: yes
artist: no
album_artist: no
data_source_mismatch_penalty: 0.5
search_limit: 5
- **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``.
.. 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:
@ -105,20 +113,36 @@ Default
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
.. 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
@ -129,9 +153,11 @@ Default
discogs:
anv:
artist_credit: True
artist: False
album_artist: False
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

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 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:

View file

@ -50,65 +50,7 @@ Using Metadata Source Plugins
We provide several :ref:`autotagger_extensions` that fetch metadata from online
databases. They share the following configuration options:
.. _data_source_mismatch_penalty:
- **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``.
.. include:: ./shared_metadata_source_config.rst
.. toctree::
:hidden:

View file

@ -26,8 +26,6 @@ Default
.. code-block:: yaml
musicbrainz:
data_source_mismatch_penalty: 0.5
search_limit: 5
host: musicbrainz.org
https: no
ratelimit: 1
@ -41,122 +39,107 @@ Default
deezer: no
beatport: no
tidal: no
data_source_mismatch_penalty: 0.5
search_limit: 5
You can instruct beets to use `your own MusicBrainz database
.. conf:: host
:default: musicbrainz.org
The Web server hostname (and port, optionally) that will be contacted by beets.
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`_.
`main server`_. Use the ``host``, ``https`` and ``ratelimit`` options under a
``musicbrainz:`` header, like so
The server must have search indices enabled (see `Building search indexes`_).
Example:
.. code-block:: yaml
musicbrainz:
host: localhost:5000
https: no
ratelimit: 100
The ``host`` key, of course, controls the Web server hostname (and port,
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`_).
.. conf:: https
:default: no
The ``ratelimit`` option, an integer, controls the number of Web service
requests per second (default: 1). **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.
Makes the client use HTTPS instead of HTTP. This setting applies only to custom
servers. The official MusicBrainz server always uses HTTPS.
.. _building search indexes: https://musicbrainz.org/doc/Development/Search_server_setup
.. conf:: ratelimit
:default: 1
.. _limited: https://musicbrainz.org/doc/XML_Web_Service/Rate_Limiting
Controls the number of Web service requests per second.
.. _main server: https://musicbrainz.org/
**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.
.. _musicbrainz.enabled:
.. conf:: ratelimit_interval
:default: 1.0
enabled
+++++++
The time interval (in seconds) for the rate limit.
.. conf:: enabled
:default: yes
.. 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
++++++++++
.. 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. For example
``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]
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
++++++
.. 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. Default: ``no``
number of votes.
.. _musicbrainz.external_ids:
.. conf:: 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
**Default**
.. code-block:: yaml
musicbrainz:
external_ids:
discogs: yes
spotify: yes
bandcamp: yes
beatport: yes
deezer: yes
tidal: yes
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 (``discogs_albumid``, ``bandcamp_album_id``, ``spotify_album_id``,
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.
The default of all options is ``no``.
.. include:: ./shared_metadata_source_config.rst
.. _building search indexes: https://musicbrainz.org/doc/Development/Search_server_setup
.. _limited: https://musicbrainz.org/doc/XML_Web_Service/Rate_Limiting
.. _main server: https://musicbrainz.org/

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
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
message if you choose to play more items than the **warning_threshold** value
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
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
spotify:
data_source_mismatch_penalty: 0.5
search_limit: 5
mode: list
region_filter:
show_failures: no
@ -84,8 +82,13 @@ Default
client_id: REDACTED
client_secret: REDACTED
tokenfile: spotify_token.json
data_source_mismatch_penalty: 0.5
search_limit: 5
- **mode**: One of the following:
.. conf:: mode
:default: list
Controls how the playlist is output:
- ``list``: Print out the playlist as a list of links. This list can then
be pasted in to a new or existing Spotify playlist.
@ -93,50 +96,53 @@ Default
instructions to open Spotify with the playlist you created. Until this
has been tested on all platforms, it will remain optional.
Default: ``list``.
.. conf:: region_filter
:default:
- **region_filter**: A two-character country abbreviation, to limit results to
that market. Default: None.
- **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``.
A two-character country abbreviation, to limit results to that market.
Here's an example:
.. conf:: show_failures
:default: no
::
List each lookup that does not return a Spotify ID (and therefore cannot be
added to a playlist).
spotify:
data_source_mismatch_penalty: 0.7
mode: open
region_filter: US
show_failures: on
tiebreak: first
search_query_ascii: no
.. conf:: tiebreak
:default: popularity
regex: [
{
field: "albumartist", # Field in the item object to regex.
search: "Something", # String to look for.
replace: "Replaced" # Replacement value.
},
{
field: "title",
search: "Something Else",
replace: "AlsoReplaced"
}
]
How to choose the candidate if there is more than one identical result. For
example, there might be multiple releases of the same album.
- ``popularity``: pick the more popular candidate
- ``first``: pick the first candidate
.. 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
----------------------------------------------------------

View file

@ -376,7 +376,7 @@ terminal_encoding
~~~~~~~~~~~~~~~~~
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
from the standard input. By default, this is determined automatically from the
locale environment variables.

View file

@ -120,7 +120,7 @@ def create_rst_replacements() -> list[Replacement]:
# Replace Sphinx directives by documentation URLs, e.g.,
# :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]),
),
# Convert command references to documentation URLs

15
poetry.lock generated
View file

@ -3473,6 +3473,17 @@ files = [
[package.dependencies]
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]]
name = "types-flask-cors"
version = "6.0.0.20250520"
@ -3650,7 +3661,7 @@ beatport = ["requests-oauthlib"]
bpd = ["PyGObject"]
chroma = ["pyacoustid"]
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"]
embyupdate = ["requests"]
fetchart = ["Pillow", "beautifulsoup4", "langdetect", "requests"]
@ -3672,4 +3683,4 @@ web = ["flask", "flask-cors"]
[metadata]
lock-version = "2.0"
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 }
soco = { version = "*", optional = true }
docutils = { version = ">=0.20.1", optional = true }
pydata-sphinx-theme = { version = "*", optional = true }
sphinx = { version = "*", optional = true }
sphinx-design = { version = "^0.6.1", optional = true }
sphinx-copybutton = { version = "^0.5.2", optional = true }
sphinx-design = { version = ">=0.6.1", optional = true }
sphinx-copybutton = { version = ">=0.5.2", optional = true }
[tool.poetry.group.test.dependencies]
beautifulsoup4 = "*"
@ -109,6 +110,7 @@ sphinx-lint = ">=1.0.0"
[tool.poetry.group.typing.dependencies]
mypy = "*"
types-beautifulsoup4 = "*"
types-docutils = ">=0.22.2.20251006"
types-mock = "*"
types-Flask-Cors = "*"
types-Pillow = "*"
@ -131,7 +133,14 @@ beatport = ["requests-oauthlib"]
bpd = ["PyGObject"] # gobject-introspection, gstreamer1.0-plugins-base, python3-gst-1.0
chroma = ["pyacoustid"] # chromaprint or fpcalc
# 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"]
embedart = ["Pillow"] # ImageMagick
embyupdate = ["requests"]

View file

@ -105,6 +105,19 @@ class PlayPluginTest(CleanupModulesMixin, PluginTestCase):
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):
self.run_command("play", "not found")