From 545213421ba649e247b280f9b07ea0a9329bc8f8 Mon Sep 17 00:00:00 2001 From: Martin Atukunda Date: Thu, 9 Oct 2025 20:11:19 +0300 Subject: [PATCH 01/14] feat(plugin/web): support for nexttrack keypress --- beetsplug/web/static/beets.js | 9 ++++++++- docs/changelog.rst | 1 + 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/beetsplug/web/static/beets.js b/beetsplug/web/static/beets.js index eace4d27d..0600d09d0 100644 --- a/beetsplug/web/static/beets.js +++ b/beetsplug/web/static/beets.js @@ -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) { diff --git a/docs/changelog.rst b/docs/changelog.rst index b56413ee9..b5032a0b9 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -23,6 +23,7 @@ New features: :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: From a938449b2922acf2747e3587321bf1108fe00512 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0ar=C5=ABnas=20Nejus?= Date: Sun, 12 Oct 2025 00:17:19 +0100 Subject: [PATCH 02/14] Add Sphinx extension for configuration value documentation Create a custom Sphinx extension to document configuration values with a simplified syntax. It is based on the `confval` but takes less space when rendered. The extension provides: - A `conf` directive for documenting individual configuration values with optional type and default parameters - A `conf` role for cross-referencing configuration values - Automatic formatting of default values in the signature - A custom domain that handles indexing and cross-references For example, if we have .. conf:: search_limit :default: 5 We refer to this configuration option with :conf:`plugins.discogs:search_limit`. The extension is loaded by adding the docs/extensions directory to the Python path and registering it in the Sphinx extensions list. --- docs/conf.py | 6 ++ docs/extensions/conf.py | 142 ++++++++++++++++++++++++++++++++++++++++ poetry.lock | 15 ++++- pyproject.toml | 15 ++++- 4 files changed, 173 insertions(+), 5 deletions(-) create mode 100644 docs/extensions/conf.py diff --git a/docs/conf.py b/docs/conf.py index c2cecc510..8d2bae130 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -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 diff --git a/docs/extensions/conf.py b/docs/extensions/conf.py new file mode 100644 index 000000000..308d28be2 --- /dev/null +++ b/docs/extensions/conf.py @@ -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, + } diff --git a/poetry.lock b/poetry.lock index 6f0523a42..615598d67 100644 --- a/poetry.lock +++ b/poetry.lock @@ -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" diff --git a/pyproject.toml b/pyproject.toml index 0058c7f9b..b546b4dc2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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"] From 498b14ee1d50edfead49efe190335b0fc6ffd496 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0ar=C5=ABnas=20Nejus?= Date: Sun, 12 Oct 2025 00:19:08 +0100 Subject: [PATCH 03/14] Convert autotagger plugin docs to use conf role --- beetsplug/discogs.py | 2 +- docs/plugins/deezer.rst | 20 +- docs/plugins/discogs.rst | 132 +++++++----- docs/plugins/index.rst | 60 +----- docs/plugins/musicbrainz.rst | 201 ++++++++---------- .../plugins/shared_metadata_source_config.rst | 65 ++++++ docs/plugins/spotify.rst | 100 +++++---- 7 files changed, 305 insertions(+), 275 deletions(-) create mode 100644 docs/plugins/shared_metadata_source_config.rst diff --git a/beetsplug/discogs.py b/beetsplug/discogs.py index 874eab6ec..be1cf97fa 100644 --- a/beetsplug/discogs.py +++ b/beetsplug/discogs.py @@ -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, diff --git a/docs/plugins/deezer.rst b/docs/plugins/deezer.rst index 96ed34652..d44a565ce 100644 --- a/docs/plugins/deezer.rst +++ b/docs/plugins/deezer.rst @@ -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 diff --git a/docs/plugins/discogs.rst b/docs/plugins/discogs.rst index 64b68248d..780042026 100644 --- a/docs/plugins/discogs.rst +++ b/docs/plugins/discogs.rst @@ -71,67 +71,93 @@ 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 - -- **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: + featured_string: Feat. anv: - artist_credit: True - artist: False - album_artist: False + artist_credit: yes + artist: no + 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 diff --git a/docs/plugins/index.rst b/docs/plugins/index.rst index a877d2320..2c9d94dfd 100644 --- a/docs/plugins/index.rst +++ b/docs/plugins/index.rst @@ -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: diff --git a/docs/plugins/musicbrainz.rst b/docs/plugins/musicbrainz.rst index 5ac287368..00c553d8b 100644 --- a/docs/plugins/musicbrainz.rst +++ b/docs/plugins/musicbrainz.rst @@ -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 -`__ instead of the +.. conf:: host + :default: musicbrainz.org -`main server`_. Use the ``host``, ``https`` and ``ratelimit`` options under a -``musicbrainz:`` header, like so + 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 + `__ instead of the + `main server`_. -.. code-block:: yaml + The server must have search indices enabled (see `Building search indexes`_). - musicbrainz: - host: localhost:5000 - https: no - ratelimit: 100 + Example: -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`_). + .. code-block:: yaml -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. + musicbrainz: + host: localhost:5000 + +.. 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 .. _limited: https://musicbrainz.org/doc/XML_Web_Service/Rate_Limiting .. _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``. diff --git a/docs/plugins/shared_metadata_source_config.rst b/docs/plugins/shared_metadata_source_config.rst new file mode 100644 index 000000000..609c7afd2 --- /dev/null +++ b/docs/plugins/shared_metadata_source_config.rst @@ -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. diff --git a/docs/plugins/spotify.rst b/docs/plugins/spotify.rst index b72f22f20..f0d6ac2ef 100644 --- a/docs/plugins/spotify.rst +++ b/docs/plugins/spotify.rst @@ -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,59 +82,67 @@ 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 - - ``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. + Controls how the playlist is output: - 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 - 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``. +.. conf:: region_filter + :default: -Here's an example: + A two-character country abbreviation, to limit results to that market. -:: +.. conf:: show_failures + :default: no - spotify: - data_source_mismatch_penalty: 0.7 - mode: open - region_filter: US - show_failures: on - tiebreak: first - search_query_ascii: no + List each lookup that does not return a Spotify ID (and therefore cannot be + added to a playlist). - 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" - } - ] +.. conf:: tiebreak + :default: popularity + + 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 ---------------------------------------------------------- From e87235117037deb3d958172940f95d6e955d8f3c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0ar=C5=ABnas=20Nejus?= Date: Sun, 12 Oct 2025 00:20:46 +0100 Subject: [PATCH 04/14] Add references to configuration values in the changelog --- docs/changelog.rst | 82 +++++++++++++++++++++++++--------------------- 1 file changed, 45 insertions(+), 37 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 9a8fc539b..669f1eb50 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -56,12 +56,13 @@ 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 @@ -89,9 +90,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: @@ -137,12 +139,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. @@ -157,12 +160,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`` @@ -194,9 +198,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. @@ -552,8 +557,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``. @@ -586,8 +592,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` @@ -941,8 +947,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 @@ -962,8 +969,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 @@ -1017,9 +1024,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. @@ -1033,9 +1040,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 `_. Thanks to :user:`aereaux`. - :doc:`/plugins/replaygain`: Analysis now happens in parallel using the @@ -1075,9 +1082,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. @@ -2693,9 +2701,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 From 861504d5f6068896f0d9ef120619475334b8fa8f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0ar=C5=ABnas=20Nejus?= Date: Sun, 12 Oct 2025 00:28:44 +0100 Subject: [PATCH 05/14] Make sure conf references are converted properly in release notes --- extra/release.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extra/release.py b/extra/release.py index d4ebb950f..e16814960 100755 --- a/extra/release.py +++ b/extra/release.py @@ -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 From 9519d47d57e35291d2956e761441baaf49876fc4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0ar=C5=ABnas=20Nejus?= Date: Sun, 12 Oct 2025 18:39:37 +0100 Subject: [PATCH 06/14] Convert Python 2 URLs to Python 3 --- docs/dev/plugins/other/logging.rst | 2 +- docs/plugins/export.rst | 2 +- docs/plugins/play.rst | 2 +- docs/reference/config.rst | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/dev/plugins/other/logging.rst b/docs/dev/plugins/other/logging.rst index 1c4ce4838..a26f0c4c0 100644 --- a/docs/dev/plugins/other/logging.rst +++ b/docs/dev/plugins/other/logging.rst @@ -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. diff --git a/docs/plugins/export.rst b/docs/plugins/export.rst index a5fa78617..b8e14ef22 100644 --- a/docs/plugins/export.rst +++ b/docs/plugins/export.rst @@ -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: diff --git a/docs/plugins/play.rst b/docs/plugins/play.rst index 2bc825773..f4b07ac52 100644 --- a/docs/plugins/play.rst +++ b/docs/plugins/play.rst @@ -123,4 +123,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 diff --git a/docs/reference/config.rst b/docs/reference/config.rst index eae9deb21..b4874416c 100644 --- a/docs/reference/config.rst +++ b/docs/reference/config.rst @@ -376,7 +376,7 @@ terminal_encoding ~~~~~~~~~~~~~~~~~ The text encoding, as `known to Python -`__, to use +`__, 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. From d83402fc65e9eef8b6230fd08208a1e8d8dd36fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0ar=C5=ABnas=20Nejus?= Date: Sun, 19 Oct 2025 01:46:32 +0100 Subject: [PATCH 07/14] Add a changelog note --- docs/changelog.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/changelog.rst b/docs/changelog.rst index 669f1eb50..0fc0ee477 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -46,6 +46,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) ------------------------ From e61ecb449675c766f920d04a26a32af06e2e3fb1 Mon Sep 17 00:00:00 2001 From: Martin Atukunda Date: Fri, 10 Oct 2025 08:35:56 +0300 Subject: [PATCH 08/14] fix(github/workflows): update to checkout v5, and setup-python v6. * also run ci against python 3.13, which is default in debian trixie. --- .github/workflows/changelog_reminder.yaml | 2 +- .github/workflows/ci.yaml | 10 +++++----- .github/workflows/integration_test.yaml | 4 ++-- .github/workflows/lint.yml | 18 +++++++++--------- .github/workflows/make_release.yaml | 12 ++++++------ 5 files changed, 23 insertions(+), 23 deletions(-) diff --git a/.github/workflows/changelog_reminder.yaml b/.github/workflows/changelog_reminder.yaml index a9c26c1f5..380d89996 100644 --- a/.github/workflows/changelog_reminder.yaml +++ b/.github/workflows/changelog_reminder.yaml @@ -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 diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 80826f468..f1623e8a5 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -20,17 +20,17 @@ jobs: fail-fast: false matrix: platform: [ubuntu-latest, windows-latest] - python-version: ["3.9", "3.10", "3.11", "3.12"] + python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] runs-on: ${{ matrix.platform }} 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 diff --git a/.github/workflows/integration_test.yaml b/.github/workflows/integration_test.yaml index f88864c48..8c7e44d7a 100644 --- a/.github/workflows/integration_test.yaml +++ b/.github/workflows/integration_test.yaml @@ -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 diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 8fdfa94e5..dcc5d0f12 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -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 diff --git a/.github/workflows/make_release.yaml b/.github/workflows/make_release.yaml index b18dded8d..5a8abe5bb 100644 --- a/.github/workflows/make_release.yaml +++ b/.github/workflows/make_release.yaml @@ -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/ From 3ccc91d4d478d6c2625babdff9d3c5e11146a822 Mon Sep 17 00:00:00 2001 From: Martin Atukunda Date: Thu, 16 Oct 2025 09:47:59 +0300 Subject: [PATCH 09/14] Drop 3.13 from python-version for now. --- .github/workflows/ci.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index f1623e8a5..fa6e9a7be 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -20,7 +20,7 @@ jobs: fail-fast: false matrix: platform: [ubuntu-latest, windows-latest] - python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] + python-version: ["3.9", "3.10", "3.11", "3.12"] runs-on: ${{ matrix.platform }} env: IS_MAIN_PYTHON: ${{ matrix.python-version == '3.9' && matrix.platform == 'ubuntu-latest' }} From 1275ccf8c1e6fcd54217ee82059fb493ee8b9129 Mon Sep 17 00:00:00 2001 From: cvx35isl <127420554+cvx35isl@users.noreply.github.com> Date: Sun, 19 Oct 2025 08:38:20 +0200 Subject: [PATCH 10/14] =?UTF-8?q?play=20plugin:=20$playlist=20marker=20for?= =?UTF-8?q?=20precise=20control=20where=20the=20playlist=20=E2=80=A6=20(#4?= =?UTF-8?q?728)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit …file is placed in the command ## Description see included doc; placing the playlist filename at the end of command just isn't working for all players I have this in use with `mpv` Co-authored-by: cvx35isl Co-authored-by: J0J0 Todos <2733783+JOJ0@users.noreply.github.com> --- beetsplug/play.py | 21 +++++++++++++++++++++ docs/changelog.rst | 4 ++++ docs/plugins/play.rst | 9 +++++++++ test/plugins/test_play.py | 13 +++++++++++++ 4 files changed, 47 insertions(+) diff --git a/beetsplug/play.py b/beetsplug/play.py index 35b4b1f76..8fb146213 100644 --- a/beetsplug/play.py +++ b/beetsplug/play.py @@ -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" diff --git a/docs/changelog.rst b/docs/changelog.rst index 0fc0ee477..5c6224de9 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -10,6 +10,8 @@ Unreleased New features: - :doc:`plugins/ftintitle`: Added argument for custom feat. words in ftintitle. +- :doc: `/plugins/play`: Added `$playlist` marker to precisely edit the playlist + filepath into the command calling the player program. Bug fixes: @@ -71,6 +73,8 @@ New features: :bug:`3354` - :doc:`plugins/discogs` Support for name variations and config options to specify where the variations are written. :bug:`3354` +- :doc: `/plugins/play`: Added `$playlist` marker to precisely edit the playlist + filepath into the command calling the player program. Bug fixes: diff --git a/docs/plugins/play.rst b/docs/plugins/play.rst index f4b07ac52..f06eb4cb3 100644 --- a/docs/plugins/play.rst +++ b/docs/plugins/play.rst @@ -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. diff --git a/test/plugins/test_play.py b/test/plugins/test_play.py index 293a50a20..b184db63f 100644 --- a/test/plugins/test_play.py +++ b/test/plugins/test_play.py @@ -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") From 39aadf709932a2a5ad2e9f69378a09b50fe9b78c Mon Sep 17 00:00:00 2001 From: J0J0 Todos <2733783+JOJ0@users.noreply.github.com> Date: Sun, 19 Oct 2025 08:50:25 +0200 Subject: [PATCH 11/14] Remove duplicate changelog entry (play plugin) --- docs/changelog.rst | 2 -- 1 file changed, 2 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 5c6224de9..449ca6dd3 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -73,8 +73,6 @@ New features: :bug:`3354` - :doc:`plugins/discogs` Support for name variations and config options to specify where the variations are written. :bug:`3354` -- :doc: `/plugins/play`: Added `$playlist` marker to precisely edit the playlist - filepath into the command calling the player program. Bug fixes: From 8a24518c4c0bdbcde5d60e35599d742274752904 Mon Sep 17 00:00:00 2001 From: Konstantin <78656278+amogus07@users.noreply.github.com> Date: Sun, 19 Oct 2025 10:06:16 +0200 Subject: [PATCH 12/14] use `Generic` instead of `Any` for `cached_classproperty` --- beets/util/__init__.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/beets/util/__init__.py b/beets/util/__init__.py index 0f2ef5b97..fc05e4997 100644 --- a/beets/util/__init__.py +++ b/beets/util/__init__.py @@ -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]): From d7138062639ed237f0ce92dfcead9459f292efbb Mon Sep 17 00:00:00 2001 From: Konstantin <78656278+amogus07@users.noreply.github.com> Date: Sun, 19 Oct 2025 15:07:17 +0200 Subject: [PATCH 13/14] fix transaction context manager signature --- .gitignore | 3 +++ beets/dbcore/db.py | 8 ++++---- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/.gitignore b/.gitignore index 90ef7387d..138965b22 100644 --- a/.gitignore +++ b/.gitignore @@ -94,3 +94,6 @@ ENV/ # pyright pyrightconfig.json + +# Pyrefly +pyrefly.toml diff --git a/beets/dbcore/db.py b/beets/dbcore/db.py index 192cfac70..4bcc8e9c1 100755 --- a/beets/dbcore/db.py +++ b/beets/dbcore/db.py @@ -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. From 12f2a1f6943d65487c02bf29d0fb129456176bad Mon Sep 17 00:00:00 2001 From: Konstantin <78656278+amogus07@users.noreply.github.com> Date: Sun, 19 Oct 2025 15:12:27 +0200 Subject: [PATCH 14/14] fix mypy error --- beets/dbcore/db.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/beets/dbcore/db.py b/beets/dbcore/db.py index 4bcc8e9c1..afae6e906 100755 --- a/beets/dbcore/db.py +++ b/beets/dbcore/db.py @@ -965,6 +965,8 @@ class Transaction: ): raise DBCustomFunctionError() + return None + def query( self, statement: str, subvals: Sequence[SQLiteType] = () ) -> list[sqlite3.Row]: