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..fa6e9a7be 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -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 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/ 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..afae6e906 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. @@ -965,6 +965,8 @@ class Transaction: ): raise DBCustomFunctionError() + return None + def query( self, statement: str, subvals: Sequence[SQLiteType] = () ) -> list[sqlite3.Row]: 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]): 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/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/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 c6fbf098b..cce30a284 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -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 `_. 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 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/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/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/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/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/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/play.rst b/docs/plugins/play.rst index 2bc825773..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. @@ -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 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 ---------------------------------------------------------- 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. 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 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"] 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")