From 07549ed896d9649562d40b75cd30702e6fa6e975 Mon Sep 17 00:00:00 2001 From: Sebastian Mohr Date: Sun, 24 Aug 2025 11:12:56 +0200 Subject: [PATCH 01/26] Moved plugin docs into folder. --- docs/dev/plugins/commands.rst | 50 ++++ docs/dev/plugins/events.rst | 142 +++++++++++ docs/dev/plugins/index.rst | 72 ++++++ docs/dev/{plugins.rst => plugins/other.rst} | 263 -------------------- 4 files changed, 264 insertions(+), 263 deletions(-) create mode 100644 docs/dev/plugins/commands.rst create mode 100644 docs/dev/plugins/events.rst create mode 100644 docs/dev/plugins/index.rst rename docs/dev/{plugins.rst => plugins/other.rst} (56%) diff --git a/docs/dev/plugins/commands.rst b/docs/dev/plugins/commands.rst new file mode 100644 index 000000000..6a9727859 --- /dev/null +++ b/docs/dev/plugins/commands.rst @@ -0,0 +1,50 @@ +.. _add_subcommands: + +Add Commands to the CLI +~~~~~~~~~~~~~~~~~~~~~~~ + +Plugins can add new subcommands to the ``beet`` command-line interface. Define +the plugin class' ``commands()`` method to return a list of ``Subcommand`` +objects. (The ``Subcommand`` class is defined in the ``beets.ui`` module.) +Here's an example plugin that adds a simple command: + +:: + + from beets.plugins import BeetsPlugin + from beets.ui import Subcommand + + my_super_command = Subcommand('super', help='do something super') + def say_hi(lib, opts, args): + print("Hello everybody! I'm a plugin!") + my_super_command.func = say_hi + + class SuperPlug(BeetsPlugin): + def commands(self): + return [my_super_command] + +To make a subcommand, invoke the constructor like so: ``Subcommand(name, parser, +help, aliases)``. The ``name`` parameter is the only required one and should +just be the name of your command. ``parser`` can be an `OptionParser instance`_, +but it defaults to an empty parser (you can extend it later). ``help`` is a +description of your command, and ``aliases`` is a list of shorthand versions of +your command name. + +.. _optionparser instance: https://docs.python.org/library/optparse.html + +You'll need to add a function to your command by saying ``mycommand.func = +myfunction``. This function should take the following parameters: ``lib`` (a +beets ``Library`` object) and ``opts`` and ``args`` (command-line options and +arguments as returned by OptionParser.parse_args_). + +.. _optionparser.parse_args: https://docs.python.org/library/optparse.html#parsing-arguments + +The function should use any of the utility functions defined in ``beets.ui``. +Try running ``pydoc beets.ui`` to see what's available. + +You can add command-line options to your new command using the ``parser`` member +of the ``Subcommand`` class, which is a ``CommonOptionsParser`` instance. Just +use it like you would a normal ``OptionParser`` in an independent script. Note +that it offers several methods to add common options: ``--album``, ``--path`` +and ``--format``. This feature is versatile and extensively documented, try +``pydoc beets.ui.CommonOptionsParser`` for more information. + diff --git a/docs/dev/plugins/events.rst b/docs/dev/plugins/events.rst new file mode 100644 index 000000000..704d4c794 --- /dev/null +++ b/docs/dev/plugins/events.rst @@ -0,0 +1,142 @@ +.. _plugin_events: + +Listen for Events +~~~~~~~~~~~~~~~~~ + +Event handlers allow plugins to run code whenever something happens in beets' +operation. For instance, a plugin could write a log message every time an album +is successfully autotagged or update MPD's index whenever the database is +changed. + +You can "listen" for events using ``BeetsPlugin.register_listener``. Here's an +example: + +:: + + from beets.plugins import BeetsPlugin + + def loaded(): + print 'Plugin loaded!' + + class SomePlugin(BeetsPlugin): + def __init__(self): + super().__init__() + self.register_listener('pluginload', loaded) + +Note that if you want to access an attribute of your plugin (e.g. ``config`` or +``log``) you'll have to define a method and not a function. Here is the usual +registration process in this case: + +:: + + from beets.plugins import BeetsPlugin + + class SomePlugin(BeetsPlugin): + def __init__(self): + super().__init__() + self.register_listener('pluginload', self.loaded) + + def loaded(self): + self._log.info('Plugin loaded!') + +The events currently available are: + +- ``pluginload``: called after all the plugins have been loaded after the + ``beet`` command starts +- ``import``: called after a ``beet import`` command finishes (the ``lib`` + keyword argument is a Library object; ``paths`` is a list of paths (strings) + that were imported) +- ``album_imported``: called with an ``Album`` object every time the ``import`` + command finishes adding an album to the library. Parameters: ``lib``, + ``album`` +- ``album_removed``: called with an ``Album`` object every time an album is + removed from the library (even when its file is not deleted from disk). +- ``item_copied``: called with an ``Item`` object whenever its file is copied. + Parameters: ``item``, ``source`` path, ``destination`` path +- ``item_imported``: called with an ``Item`` object every time the importer adds + a singleton to the library (not called for full-album imports). Parameters: + ``lib``, ``item`` +- ``before_item_moved``: called with an ``Item`` object immediately before its + file is moved. Parameters: ``item``, ``source`` path, ``destination`` path +- ``item_moved``: called with an ``Item`` object whenever its file is moved. + Parameters: ``item``, ``source`` path, ``destination`` path +- ``item_linked``: called with an ``Item`` object whenever a symlink is created + for a file. Parameters: ``item``, ``source`` path, ``destination`` path +- ``item_hardlinked``: called with an ``Item`` object whenever a hardlink is + created for a file. Parameters: ``item``, ``source`` path, ``destination`` + path +- ``item_reflinked``: called with an ``Item`` object whenever a reflink is + created for a file. Parameters: ``item``, ``source`` path, ``destination`` + path +- ``item_removed``: called with an ``Item`` object every time an item (singleton + or album's part) is removed from the library (even when its file is not + deleted from disk). +- ``write``: called with an ``Item`` object, a ``path``, and a ``tags`` + dictionary just before a file's metadata is written to disk (i.e., just before + the file on disk is opened). Event handlers may change the ``tags`` dictionary + to customize the tags that are written to the media file. Event handlers may + also raise a ``library.FileOperationError`` exception to abort the write + operation. Beets will catch that exception, print an error message and + continue. +- ``after_write``: called with an ``Item`` object after a file's metadata is + written to disk (i.e., just after the file on disk is closed). +- ``import_task_created``: called immediately after an import task is + initialized. Plugins can use this to, for example, change imported files of a + task before anything else happens. It's also possible to replace the task with + another task by returning a list of tasks. This list can contain zero or more + ``ImportTask``. Returning an empty list will stop the task. Parameters: + ``task`` (an ``ImportTask``) and ``session`` (an ``ImportSession``). +- ``import_task_start``: called when before an import task begins processing. + Parameters: ``task`` and ``session``. +- ``import_task_apply``: called after metadata changes have been applied in an + import task. This is called on the same thread as the UI, so use this + sparingly and only for tasks that can be done quickly. For most plugins, an + import pipeline stage is a better choice (see :ref:`plugin-stage`). + Parameters: ``task`` and ``session``. +- ``import_task_before_choice``: called after candidate search for an import + task before any decision is made about how/if to import or tag. Can be used to + present information about the task or initiate interaction with the user + before importing occurs. Return an importer action to take a specific action. + Only one handler may return a non-None result. Parameters: ``task`` and + ``session`` +- ``import_task_choice``: called after a decision has been made about an import + task. This event can be used to initiate further interaction with the user. + Use ``task.choice_flag`` to determine or change the action to be taken. + Parameters: ``task`` and ``session``. +- ``import_task_files``: called after an import task finishes manipulating the + filesystem (copying and moving files, writing metadata tags). Parameters: + ``task`` and ``session``. +- ``library_opened``: called after beets starts up and initializes the main + Library object. Parameter: ``lib``. +- ``database_change``: a modification has been made to the library database. The + change might not be committed yet. Parameters: ``lib`` and ``model``. +- ``cli_exit``: called just before the ``beet`` command-line program exits. + Parameter: ``lib``. +- ``import_begin``: called just before a ``beet import`` session starts up. + Parameter: ``session``. +- ``trackinfo_received``: called after metadata for a track item has been + fetched from a data source, such as MusicBrainz. You can modify the tags that + the rest of the pipeline sees on a ``beet import`` operation or during later + adjustments, such as ``mbsync``. Slow handlers of the event can impact the + operation, since the event is fired for any fetched possible match ``before`` + the user (or the autotagger machinery) gets to see the match. Parameter: + ``info``. +- ``albuminfo_received``: like ``trackinfo_received``, the event indicates new + metadata for album items. The parameter is an ``AlbumInfo`` object instead of + a ``TrackInfo``. Parameter: ``info``. +- ``before_choose_candidate``: called before the user is prompted for a decision + during a ``beet import`` interactive session. Plugins can use this event for + :ref:`appending choices to the prompt ` by returning a + list of ``PromptChoices``. Parameters: ``task`` and ``session``. +- ``mb_track_extract``: called after the metadata is obtained from MusicBrainz. + The parameter is a ``dict`` containing the tags retrieved from MusicBrainz for + a track. Plugins must return a new (potentially empty) ``dict`` with + additional ``field: value`` pairs, which the autotagger will apply to the + item, as flexible attributes if ``field`` is not a hardcoded field. Fields + already present on the track are overwritten. Parameter: ``data`` +- ``mb_album_extract``: Like ``mb_track_extract``, but for album tags. + Overwrites tags set at the track level, if they have the same ``field``. + Parameter: ``data`` + +The included ``mpdupdate`` plugin provides an example use case for event +listeners. \ No newline at end of file diff --git a/docs/dev/plugins/index.rst b/docs/dev/plugins/index.rst new file mode 100644 index 000000000..6c3578e4a --- /dev/null +++ b/docs/dev/plugins/index.rst @@ -0,0 +1,72 @@ +Plugin Development Guide +======================== + +Beets plugins are Python modules or packages that extend the core functionality +of beets. The plugin system is designed to be flexible, allowing developers to +add virtually any type of features. + +.. _writing-plugins: + +Writing Plugins +--------------- + +A beets plugin is just a Python module or package inside the ``beetsplug`` +namespace package. (Check out `this article`_ and `this Stack Overflow +question`_ if you haven't heard about namespace packages.) So, to make one, +create a directory called ``beetsplug`` and add either your plugin module: + +:: + + beetsplug/ + myawesomeplugin.py + +or your plugin subpackage: + +:: + + beetsplug/ + myawesomeplugin/ + __init__.py + myawesomeplugin.py + +.. attention:: + + You do not anymore need to add a ``__init__.py`` file to the ``beetsplug`` + directory. Python treats your plugin as a namespace package automatically, + thus we do not depend on ``pkgutil``-based setup in the ``__init__.py`` file + anymore. + +.. _this article: https://realpython.com/python-namespace-package/#setting-up-some-namespace-packages + +.. _this stack overflow question: https://stackoverflow.com/a/27586272/9582674 + +The meat of your plugin goes in ``myawesomeplugin.py``. There, you'll have to +import ``BeetsPlugin`` from ``beets.plugins`` and subclass it, for example + +.. code-block:: python + + from beets.plugins import BeetsPlugin + + + class MyAwesomePlugin(BeetsPlugin): + pass + +Once you have your ``BeetsPlugin`` subclass, there's a variety of things your +plugin can do. (Read on!) + +To use your new plugin, package your plugin (see how to do this with poetry_ or +setuptools_, for example) and install it into your ``beets`` virtual +environment. Then, add your plugin to beets configuration + +.. _poetry: https://python-poetry.org/docs/pyproject/#packages + +.. _setuptools: https://setuptools.pypa.io/en/latest/userguide/package_discovery.html#finding-simple-packages + +.. code-block:: yaml + + # config.yaml + plugins: + - myawesomeplugin + +and you're good to go! + diff --git a/docs/dev/plugins.rst b/docs/dev/plugins/other.rst similarity index 56% rename from docs/dev/plugins.rst rename to docs/dev/plugins/other.rst index 5ee07347f..9e4589ce7 100644 --- a/docs/dev/plugins.rst +++ b/docs/dev/plugins/other.rst @@ -1,267 +1,4 @@ -Plugin Development Guide -======================== -Beets plugins are Python modules or packages that extend the core functionality -of beets. The plugin system is designed to be flexible, allowing developers to -add virtually any type of features. - -.. _writing-plugins: - -Writing Plugins ---------------- - -A beets plugin is just a Python module or package inside the ``beetsplug`` -namespace package. (Check out `this article`_ and `this Stack Overflow -question`_ if you haven't heard about namespace packages.) So, to make one, -create a directory called ``beetsplug`` and add either your plugin module: - -:: - - beetsplug/ - myawesomeplugin.py - -or your plugin subpackage: - -:: - - beetsplug/ - myawesomeplugin/ - __init__.py - myawesomeplugin.py - -.. attention:: - - You do not anymore need to add a ``__init__.py`` file to the ``beetsplug`` - directory. Python treats your plugin as a namespace package automatically, - thus we do not depend on ``pkgutil``-based setup in the ``__init__.py`` file - anymore. - -.. _this article: https://realpython.com/python-namespace-package/#setting-up-some-namespace-packages - -.. _this stack overflow question: https://stackoverflow.com/a/27586272/9582674 - -The meat of your plugin goes in ``myawesomeplugin.py``. There, you'll have to -import ``BeetsPlugin`` from ``beets.plugins`` and subclass it, for example - -.. code-block:: python - - from beets.plugins import BeetsPlugin - - - class MyAwesomePlugin(BeetsPlugin): - pass - -Once you have your ``BeetsPlugin`` subclass, there's a variety of things your -plugin can do. (Read on!) - -To use your new plugin, package your plugin (see how to do this with poetry_ or -setuptools_, for example) and install it into your ``beets`` virtual -environment. Then, add your plugin to beets configuration - -.. _poetry: https://python-poetry.org/docs/pyproject/#packages - -.. _setuptools: https://setuptools.pypa.io/en/latest/userguide/package_discovery.html#finding-simple-packages - -.. code-block:: yaml - - # config.yaml - plugins: - - myawesomeplugin - -and you're good to go! - -.. _add_subcommands: - -Add Commands to the CLI -~~~~~~~~~~~~~~~~~~~~~~~ - -Plugins can add new subcommands to the ``beet`` command-line interface. Define -the plugin class' ``commands()`` method to return a list of ``Subcommand`` -objects. (The ``Subcommand`` class is defined in the ``beets.ui`` module.) -Here's an example plugin that adds a simple command: - -:: - - from beets.plugins import BeetsPlugin - from beets.ui import Subcommand - - my_super_command = Subcommand('super', help='do something super') - def say_hi(lib, opts, args): - print("Hello everybody! I'm a plugin!") - my_super_command.func = say_hi - - class SuperPlug(BeetsPlugin): - def commands(self): - return [my_super_command] - -To make a subcommand, invoke the constructor like so: ``Subcommand(name, parser, -help, aliases)``. The ``name`` parameter is the only required one and should -just be the name of your command. ``parser`` can be an `OptionParser instance`_, -but it defaults to an empty parser (you can extend it later). ``help`` is a -description of your command, and ``aliases`` is a list of shorthand versions of -your command name. - -.. _optionparser instance: https://docs.python.org/library/optparse.html - -You'll need to add a function to your command by saying ``mycommand.func = -myfunction``. This function should take the following parameters: ``lib`` (a -beets ``Library`` object) and ``opts`` and ``args`` (command-line options and -arguments as returned by OptionParser.parse_args_). - -.. _optionparser.parse_args: https://docs.python.org/library/optparse.html#parsing-arguments - -The function should use any of the utility functions defined in ``beets.ui``. -Try running ``pydoc beets.ui`` to see what's available. - -You can add command-line options to your new command using the ``parser`` member -of the ``Subcommand`` class, which is a ``CommonOptionsParser`` instance. Just -use it like you would a normal ``OptionParser`` in an independent script. Note -that it offers several methods to add common options: ``--album``, ``--path`` -and ``--format``. This feature is versatile and extensively documented, try -``pydoc beets.ui.CommonOptionsParser`` for more information. - -.. _plugin_events: - -Listen for Events -~~~~~~~~~~~~~~~~~ - -Event handlers allow plugins to run code whenever something happens in beets' -operation. For instance, a plugin could write a log message every time an album -is successfully autotagged or update MPD's index whenever the database is -changed. - -You can "listen" for events using ``BeetsPlugin.register_listener``. Here's an -example: - -:: - - from beets.plugins import BeetsPlugin - - def loaded(): - print 'Plugin loaded!' - - class SomePlugin(BeetsPlugin): - def __init__(self): - super().__init__() - self.register_listener('pluginload', loaded) - -Note that if you want to access an attribute of your plugin (e.g. ``config`` or -``log``) you'll have to define a method and not a function. Here is the usual -registration process in this case: - -:: - - from beets.plugins import BeetsPlugin - - class SomePlugin(BeetsPlugin): - def __init__(self): - super().__init__() - self.register_listener('pluginload', self.loaded) - - def loaded(self): - self._log.info('Plugin loaded!') - -The events currently available are: - -- ``pluginload``: called after all the plugins have been loaded after the - ``beet`` command starts -- ``import``: called after a ``beet import`` command finishes (the ``lib`` - keyword argument is a Library object; ``paths`` is a list of paths (strings) - that were imported) -- ``album_imported``: called with an ``Album`` object every time the ``import`` - command finishes adding an album to the library. Parameters: ``lib``, - ``album`` -- ``album_removed``: called with an ``Album`` object every time an album is - removed from the library (even when its file is not deleted from disk). -- ``item_copied``: called with an ``Item`` object whenever its file is copied. - Parameters: ``item``, ``source`` path, ``destination`` path -- ``item_imported``: called with an ``Item`` object every time the importer adds - a singleton to the library (not called for full-album imports). Parameters: - ``lib``, ``item`` -- ``before_item_moved``: called with an ``Item`` object immediately before its - file is moved. Parameters: ``item``, ``source`` path, ``destination`` path -- ``item_moved``: called with an ``Item`` object whenever its file is moved. - Parameters: ``item``, ``source`` path, ``destination`` path -- ``item_linked``: called with an ``Item`` object whenever a symlink is created - for a file. Parameters: ``item``, ``source`` path, ``destination`` path -- ``item_hardlinked``: called with an ``Item`` object whenever a hardlink is - created for a file. Parameters: ``item``, ``source`` path, ``destination`` - path -- ``item_reflinked``: called with an ``Item`` object whenever a reflink is - created for a file. Parameters: ``item``, ``source`` path, ``destination`` - path -- ``item_removed``: called with an ``Item`` object every time an item (singleton - or album's part) is removed from the library (even when its file is not - deleted from disk). -- ``write``: called with an ``Item`` object, a ``path``, and a ``tags`` - dictionary just before a file's metadata is written to disk (i.e., just before - the file on disk is opened). Event handlers may change the ``tags`` dictionary - to customize the tags that are written to the media file. Event handlers may - also raise a ``library.FileOperationError`` exception to abort the write - operation. Beets will catch that exception, print an error message and - continue. -- ``after_write``: called with an ``Item`` object after a file's metadata is - written to disk (i.e., just after the file on disk is closed). -- ``import_task_created``: called immediately after an import task is - initialized. Plugins can use this to, for example, change imported files of a - task before anything else happens. It's also possible to replace the task with - another task by returning a list of tasks. This list can contain zero or more - ``ImportTask``. Returning an empty list will stop the task. Parameters: - ``task`` (an ``ImportTask``) and ``session`` (an ``ImportSession``). -- ``import_task_start``: called when before an import task begins processing. - Parameters: ``task`` and ``session``. -- ``import_task_apply``: called after metadata changes have been applied in an - import task. This is called on the same thread as the UI, so use this - sparingly and only for tasks that can be done quickly. For most plugins, an - import pipeline stage is a better choice (see :ref:`plugin-stage`). - Parameters: ``task`` and ``session``. -- ``import_task_before_choice``: called after candidate search for an import - task before any decision is made about how/if to import or tag. Can be used to - present information about the task or initiate interaction with the user - before importing occurs. Return an importer action to take a specific action. - Only one handler may return a non-None result. Parameters: ``task`` and - ``session`` -- ``import_task_choice``: called after a decision has been made about an import - task. This event can be used to initiate further interaction with the user. - Use ``task.choice_flag`` to determine or change the action to be taken. - Parameters: ``task`` and ``session``. -- ``import_task_files``: called after an import task finishes manipulating the - filesystem (copying and moving files, writing metadata tags). Parameters: - ``task`` and ``session``. -- ``library_opened``: called after beets starts up and initializes the main - Library object. Parameter: ``lib``. -- ``database_change``: a modification has been made to the library database. The - change might not be committed yet. Parameters: ``lib`` and ``model``. -- ``cli_exit``: called just before the ``beet`` command-line program exits. - Parameter: ``lib``. -- ``import_begin``: called just before a ``beet import`` session starts up. - Parameter: ``session``. -- ``trackinfo_received``: called after metadata for a track item has been - fetched from a data source, such as MusicBrainz. You can modify the tags that - the rest of the pipeline sees on a ``beet import`` operation or during later - adjustments, such as ``mbsync``. Slow handlers of the event can impact the - operation, since the event is fired for any fetched possible match ``before`` - the user (or the autotagger machinery) gets to see the match. Parameter: - ``info``. -- ``albuminfo_received``: like ``trackinfo_received``, the event indicates new - metadata for album items. The parameter is an ``AlbumInfo`` object instead of - a ``TrackInfo``. Parameter: ``info``. -- ``before_choose_candidate``: called before the user is prompted for a decision - during a ``beet import`` interactive session. Plugins can use this event for - :ref:`appending choices to the prompt ` by returning a - list of ``PromptChoices``. Parameters: ``task`` and ``session``. -- ``mb_track_extract``: called after the metadata is obtained from MusicBrainz. - The parameter is a ``dict`` containing the tags retrieved from MusicBrainz for - a track. Plugins must return a new (potentially empty) ``dict`` with - additional ``field: value`` pairs, which the autotagger will apply to the - item, as flexible attributes if ``field`` is not a hardcoded field. Fields - already present on the track are overwritten. Parameter: ``data`` -- ``mb_album_extract``: Like ``mb_track_extract``, but for album tags. - Overwrites tags set at the track level, if they have the same ``field``. - Parameter: ``data`` - -The included ``mpdupdate`` plugin provides an example use case for event -listeners. Extend the Autotagger ~~~~~~~~~~~~~~~~~~~~~ From 69b47b3071ba71946943a394e2a4a45288549c05 Mon Sep 17 00:00:00 2001 From: Sebastian Mohr Date: Sun, 24 Aug 2025 11:13:47 +0200 Subject: [PATCH 02/26] A number of smaller additions. --- docs/_templates/autosummary/class.rst | 2 +- docs/api/plugins.rst | 8 ++++++++ docs/dev/index.rst | 10 +++++++--- docs/plugins/index.rst | 2 +- 4 files changed, 17 insertions(+), 5 deletions(-) diff --git a/docs/_templates/autosummary/class.rst b/docs/_templates/autosummary/class.rst index fdf251b15..586b207b7 100644 --- a/docs/_templates/autosummary/class.rst +++ b/docs/_templates/autosummary/class.rst @@ -1,4 +1,4 @@ -{{ fullname | escape | underline}} +{{ name | escape | underline}} .. currentmodule:: {{ module }} diff --git a/docs/api/plugins.rst b/docs/api/plugins.rst index 9320425db..2ce8dbed6 100644 --- a/docs/api/plugins.rst +++ b/docs/api/plugins.rst @@ -7,3 +7,11 @@ Plugins :toctree: generated/ BeetsPlugin + +.. currentmodule:: beets.metadata_plugins + +.. autosummary:: + :toctree: generated/ + + MetadataSourcePlugin + SearchApiMetadataSourcePlugin diff --git a/docs/dev/index.rst b/docs/dev/index.rst index 7f8af5276..8d9200f67 100644 --- a/docs/dev/index.rst +++ b/docs/dev/index.rst @@ -4,15 +4,19 @@ For Developers This section contains information for developers. Read on if you're interested in hacking beets itself or creating plugins for it. -See also the documentation for MediaFile_, the library used by beets to read and -write metadata tags in media files. +See also the documentation for the MediaFile_ and Confuse_ libraries. These are +maintained by the beets team and used to read and write metadata tags and manage +configuration files, respectively. + +.. _confuse: https://confuse.readthedocs.io/en/latest/ .. _mediafile: https://mediafile.readthedocs.io/en/latest/ .. toctree:: :maxdepth: 1 + :titlesonly: - plugins + plugins/index library importer cli diff --git a/docs/plugins/index.rst b/docs/plugins/index.rst index 1dfa3aae2..960ecfbef 100644 --- a/docs/plugins/index.rst +++ b/docs/plugins/index.rst @@ -5,7 +5,7 @@ Plugins extend beets' core functionality. They add new commands, fetch additional data during import, provide new metadata sources, and much more. If beets by itself doesn't do what you want it to, you may just need to enable a plugin---or, if you want to do something new, :doc:`writing a plugin -` is easy if you know a little Python. +` is easy if you know a little Python. .. _using-plugins: From 35ea9a7011db46f4f95cb02a89211257b4430a1b Mon Sep 17 00:00:00 2001 From: Sebastian Mohr Date: Sun, 24 Aug 2025 11:18:34 +0200 Subject: [PATCH 03/26] Enhanced index, changed wording slightly --- docs/dev/plugins/index.rst | 94 +++++++++++++++++++++++++------------- 1 file changed, 61 insertions(+), 33 deletions(-) diff --git a/docs/dev/plugins/index.rst b/docs/dev/plugins/index.rst index 6c3578e4a..491e48e0e 100644 --- a/docs/dev/plugins/index.rst +++ b/docs/dev/plugins/index.rst @@ -1,66 +1,58 @@ -Plugin Development Guide -======================== +Plugin Development +================== Beets plugins are Python modules or packages that extend the core functionality of beets. The plugin system is designed to be flexible, allowing developers to -add virtually any type of features. +add virtually any type of features to beets. -.. _writing-plugins: +For instance you can create plugins that add new commands to the command-line +interface, listen for events in the beets lifecycle or extend the autotagger +with new metadata sources. .. _writing-plugins: -Writing Plugins ---------------- +Basic Plugin Setup +------------------ A beets plugin is just a Python module or package inside the ``beetsplug`` -namespace package. (Check out `this article`_ and `this Stack Overflow -question`_ if you haven't heard about namespace packages.) So, to make one, -create a directory called ``beetsplug`` and add either your plugin module: +namespace [namespace]_ package. To create the basic plugin layout, create a +directory called ``beetsplug`` and add either your plugin module: -:: +.. code-block:: shell beetsplug/ - myawesomeplugin.py + └── myawesomeplugin.py -or your plugin subpackage: +or your plugin subpackage -:: +.. code-block:: shell beetsplug/ - myawesomeplugin/ - __init__.py - myawesomeplugin.py + └── myawesomeplugin/ + ├── __init__.py + └── myawesomeplugin.py .. attention:: - You do not anymore need to add a ``__init__.py`` file to the ``beetsplug`` + You do not need to add an ``__init__.py`` file to the ``beetsplug`` directory. Python treats your plugin as a namespace package automatically, thus we do not depend on ``pkgutil``-based setup in the ``__init__.py`` file anymore. -.. _this article: https://realpython.com/python-namespace-package/#setting-up-some-namespace-packages - -.. _this stack overflow question: https://stackoverflow.com/a/27586272/9582674 - -The meat of your plugin goes in ``myawesomeplugin.py``. There, you'll have to -import ``BeetsPlugin`` from ``beets.plugins`` and subclass it, for example +The meat of your plugin goes in ``myawesomeplugin.py``. Every plugin has to +extend the :class:`beets.plugins.BeetsPlugin` abstract base class [baseclass]_ . +For instance, a minimal plugin without any functionality would look like this: .. code-block:: python + # beetsplug/myawesomeplugin.py from beets.plugins import BeetsPlugin class MyAwesomePlugin(BeetsPlugin): pass -Once you have your ``BeetsPlugin`` subclass, there's a variety of things your -plugin can do. (Read on!) - -To use your new plugin, package your plugin (see how to do this with poetry_ or -setuptools_, for example) and install it into your ``beets`` virtual -environment. Then, add your plugin to beets configuration - -.. _poetry: https://python-poetry.org/docs/pyproject/#packages - -.. _setuptools: https://setuptools.pypa.io/en/latest/userguide/package_discovery.html#finding-simple-packages +To use your new plugin, you need to package [packaging]_ your plugin and install +it into your ``beets`` (virtual) environment. To enable your plugin, add it it +to the beets configuration .. code-block:: yaml @@ -70,3 +62,39 @@ environment. Then, add your plugin to beets configuration and you're good to go! +.. [namespace] Check out `this article`_ and `this Stack Overflow question`_ if + you haven't heard about namespace packages. + +.. [baseclass] Abstract base classes allow us to define a contract which any + plugin must follow. This is a common paradigm in object-oriented + programming, and it helps to ensure that plugins are implemented in a + consistent way. For more information, see for example pep-3119_. + +.. [packaging] There are a variety of packaging tools available for python, for + example you can use poetry_, setuptools_ or hatchling_. + +.. _hatchling: https://hatch.pypa.io/latest/config/build/#build-system + +.. _pep-3119: https://peps.python.org/pep-3119/#rationale + +.. _poetry: https://python-poetry.org/docs/pyproject/#packages + +.. _setuptools: https://setuptools.pypa.io/en/latest/userguide/package_discovery.html#finding-simple-packages + +.. _this article: https://realpython.com/python-namespace-package/#setting-up-some-namespace-packages + +.. _this stack overflow question: https://stackoverflow.com/a/27586272/9582674 + +More information +---------------- + +For more information on writing plugins, feel free to check out the following +resources: + +.. toctree:: + :maxdepth: 2 + :includehidden: + + commands + events + other From 6627a0740c950cdf4e5c7ada3f1645ae1c85d552 Mon Sep 17 00:00:00 2001 From: Sebastian Mohr Date: Sun, 24 Aug 2025 11:20:08 +0200 Subject: [PATCH 04/26] Changed events doc list to table. Added references to api. --- docs/dev/plugins/events.rst | 292 ++++++++++++++++++++++-------------- 1 file changed, 178 insertions(+), 114 deletions(-) diff --git a/docs/dev/plugins/events.rst b/docs/dev/plugins/events.rst index 704d4c794..3895d35aa 100644 --- a/docs/dev/plugins/events.rst +++ b/docs/dev/plugins/events.rst @@ -1,142 +1,206 @@ .. _plugin_events: Listen for Events -~~~~~~~~~~~~~~~~~ +================= -Event handlers allow plugins to run code whenever something happens in beets' -operation. For instance, a plugin could write a log message every time an album +.. currentmodule:: beets.plugins + +Event handlers allow plugins to hook into whenever something happens in beets' +operations. For instance, a plugin could write a log message every time an album is successfully autotagged or update MPD's index whenever the database is changed. -You can "listen" for events using ``BeetsPlugin.register_listener``. Here's an -example: +You can "listen" for events using :py:meth:`BeetsPlugin.register_listener`. +Here's an example: -:: +.. code-block:: python from beets.plugins import BeetsPlugin + def loaded(): - print 'Plugin loaded!' + print("Plugin loaded!") + class SomePlugin(BeetsPlugin): - def __init__(self): - super().__init__() - self.register_listener('pluginload', loaded) + def __init__(self): + super().__init__() + self.register_listener("pluginload", loaded) Note that if you want to access an attribute of your plugin (e.g. ``config`` or ``log``) you'll have to define a method and not a function. Here is the usual registration process in this case: -:: +.. code-block:: python from beets.plugins import BeetsPlugin + class SomePlugin(BeetsPlugin): - def __init__(self): - super().__init__() - self.register_listener('pluginload', self.loaded) + def __init__(self): + super().__init__() + self.register_listener("pluginload", self.loaded) - def loaded(self): - self._log.info('Plugin loaded!') + def loaded(self): + self._log.info("Plugin loaded!") -The events currently available are: +.. list-table:: Plugin Events + :widths: 15 25 60 + :header-rows: 1 -- ``pluginload``: called after all the plugins have been loaded after the - ``beet`` command starts -- ``import``: called after a ``beet import`` command finishes (the ``lib`` - keyword argument is a Library object; ``paths`` is a list of paths (strings) - that were imported) -- ``album_imported``: called with an ``Album`` object every time the ``import`` - command finishes adding an album to the library. Parameters: ``lib``, - ``album`` -- ``album_removed``: called with an ``Album`` object every time an album is - removed from the library (even when its file is not deleted from disk). -- ``item_copied``: called with an ``Item`` object whenever its file is copied. - Parameters: ``item``, ``source`` path, ``destination`` path -- ``item_imported``: called with an ``Item`` object every time the importer adds - a singleton to the library (not called for full-album imports). Parameters: - ``lib``, ``item`` -- ``before_item_moved``: called with an ``Item`` object immediately before its - file is moved. Parameters: ``item``, ``source`` path, ``destination`` path -- ``item_moved``: called with an ``Item`` object whenever its file is moved. - Parameters: ``item``, ``source`` path, ``destination`` path -- ``item_linked``: called with an ``Item`` object whenever a symlink is created - for a file. Parameters: ``item``, ``source`` path, ``destination`` path -- ``item_hardlinked``: called with an ``Item`` object whenever a hardlink is - created for a file. Parameters: ``item``, ``source`` path, ``destination`` - path -- ``item_reflinked``: called with an ``Item`` object whenever a reflink is - created for a file. Parameters: ``item``, ``source`` path, ``destination`` - path -- ``item_removed``: called with an ``Item`` object every time an item (singleton - or album's part) is removed from the library (even when its file is not - deleted from disk). -- ``write``: called with an ``Item`` object, a ``path``, and a ``tags`` - dictionary just before a file's metadata is written to disk (i.e., just before - the file on disk is opened). Event handlers may change the ``tags`` dictionary - to customize the tags that are written to the media file. Event handlers may - also raise a ``library.FileOperationError`` exception to abort the write - operation. Beets will catch that exception, print an error message and - continue. -- ``after_write``: called with an ``Item`` object after a file's metadata is - written to disk (i.e., just after the file on disk is closed). -- ``import_task_created``: called immediately after an import task is - initialized. Plugins can use this to, for example, change imported files of a - task before anything else happens. It's also possible to replace the task with - another task by returning a list of tasks. This list can contain zero or more - ``ImportTask``. Returning an empty list will stop the task. Parameters: - ``task`` (an ``ImportTask``) and ``session`` (an ``ImportSession``). -- ``import_task_start``: called when before an import task begins processing. - Parameters: ``task`` and ``session``. -- ``import_task_apply``: called after metadata changes have been applied in an - import task. This is called on the same thread as the UI, so use this - sparingly and only for tasks that can be done quickly. For most plugins, an - import pipeline stage is a better choice (see :ref:`plugin-stage`). - Parameters: ``task`` and ``session``. -- ``import_task_before_choice``: called after candidate search for an import - task before any decision is made about how/if to import or tag. Can be used to - present information about the task or initiate interaction with the user - before importing occurs. Return an importer action to take a specific action. - Only one handler may return a non-None result. Parameters: ``task`` and - ``session`` -- ``import_task_choice``: called after a decision has been made about an import - task. This event can be used to initiate further interaction with the user. - Use ``task.choice_flag`` to determine or change the action to be taken. - Parameters: ``task`` and ``session``. -- ``import_task_files``: called after an import task finishes manipulating the - filesystem (copying and moving files, writing metadata tags). Parameters: - ``task`` and ``session``. -- ``library_opened``: called after beets starts up and initializes the main - Library object. Parameter: ``lib``. -- ``database_change``: a modification has been made to the library database. The - change might not be committed yet. Parameters: ``lib`` and ``model``. -- ``cli_exit``: called just before the ``beet`` command-line program exits. - Parameter: ``lib``. -- ``import_begin``: called just before a ``beet import`` session starts up. - Parameter: ``session``. -- ``trackinfo_received``: called after metadata for a track item has been - fetched from a data source, such as MusicBrainz. You can modify the tags that - the rest of the pipeline sees on a ``beet import`` operation or during later - adjustments, such as ``mbsync``. Slow handlers of the event can impact the - operation, since the event is fired for any fetched possible match ``before`` - the user (or the autotagger machinery) gets to see the match. Parameter: - ``info``. -- ``albuminfo_received``: like ``trackinfo_received``, the event indicates new - metadata for album items. The parameter is an ``AlbumInfo`` object instead of - a ``TrackInfo``. Parameter: ``info``. -- ``before_choose_candidate``: called before the user is prompted for a decision - during a ``beet import`` interactive session. Plugins can use this event for - :ref:`appending choices to the prompt ` by returning a - list of ``PromptChoices``. Parameters: ``task`` and ``session``. -- ``mb_track_extract``: called after the metadata is obtained from MusicBrainz. - The parameter is a ``dict`` containing the tags retrieved from MusicBrainz for - a track. Plugins must return a new (potentially empty) ``dict`` with - additional ``field: value`` pairs, which the autotagger will apply to the - item, as flexible attributes if ``field`` is not a hardcoded field. Fields - already present on the track are overwritten. Parameter: ``data`` -- ``mb_album_extract``: Like ``mb_track_extract``, but for album tags. - Overwrites tags set at the track level, if they have the same ``field``. - Parameter: ``data`` + - - Event + - Parameters + - Description + - - `pluginload` + - + - called after all the plugins have been loaded after the ``beet`` command + starts + - - `import` + - :py:class:`lib `, ``paths`` is a list of paths + (strings) + - called after the ``import`` command finishes. + - - `album_imported` + - :py:class:`lib `, :py:class:`album + ` + - called every time the ``import`` command finishes adding an album to the + library + - - `album_removed` + - :py:class:`lib `, :py:class:`album + ` + - called every time an album is removed from the library (even when its + file is not deleted from disk) + - - `item_copied` + - :py:class:`item `, ``source`` path, ``destination`` + path + - called whenever an item file is copied + - - `item_imported` + - :py:class:`lib `, :py:class:`item + ` + - called every time the importer adds a singleton to the library (not + called for full-album imports) + - - `before_item_imported` + - :py:class:`item `, ``source`` path, ``destination`` + path + - called with an item object immediately before it is imported + - - `before_item_moved` + - :py:class:`item `, ``source`` path, ``destination`` + path + - called with an ``Item`` object immediately before its file is moved + - - `item_moved` + - :py:class:`item `, ``source`` path, ``destination`` + path + - called with an ``Item`` object whenever its file is moved + - - `item_linked` + - :py:class:`item `, ``source`` path, ``destination`` + path + - called with an ``Item`` object whenever a symlink is created for a file + - - `item_hardlinked` + - :py:class:`item `, ``source`` path, ``destination`` + path + - called with an ``Item`` object whenever a hardlink is created for a file + - - `item_reflinked` + - :py:class:`item `, ``source`` path, ``destination`` + path + - called with an ``Item`` object whenever a reflink is created for a file + - - `item_removed` + - :py:class:`item ` + - called with an ``Item`` object every time an item (singleton or album's + part) is removed from the library (even when its file is not deleted + from disk). + - - `write` + - :py:class:`item `, ``path``, ``tags`` dictionary + - called with an ``Item`` object, a ``path``, and a ``tags`` dictionary + just before a file's metadata is written to disk (i.e., just before the + file on disk is opened). Event handlers may change the ``tags`` + dictionary to customize the tags that are written to the media file. + Event handlers may also raise a ``library.FileOperationError`` exception + to abort the write operation. Beets will catch that exception, print an + error message, and continue. + - - `after_write` + - :py:class:`item ` + - called with an ``Item`` object after a file's metadata is written to + disk (i.e., just after the file on disk is closed). + - - `import_task_created` + - :py:class:`task `, :py:class:`session + ` + - called immediately after an import task is initialized. Plugins can use + this to, for example, change imported files of a task before anything + else happens. It's also possible to replace the task with another task + by returning a list of tasks. This list can contain zero or more + ImportTasks. Returning an empty list will stop the task. + - - `import_task_start` + - :py:class:`task `, :py:class:`session + ` + - called when before an import task begins processing. + - - `import_task_apply` + - :py:class:`task `, :py:class:`session + ` + - called after metadata changes have been applied in an import task. This + is called on the same thread as the UI, so use this sparingly and only + for tasks that can be done quickly. For most plugins, an import pipeline + stage is a better choice (see :ref:`plugin-stage`). + - - `import_task_before_choice` + - :py:class:`task `, :py:class:`session + ` + - called after candidate search for an import task before any decision is + made about how/if to import or tag. Can be used to present information + about the task or initiate interaction with the user before importing + occurs. Return an importer action to take a specific action. Only one + handler may return a non-None result. + - - `import_task_choice` + - :py:class:`task `, :py:class:`session + ` + - called after a decision has been made about an import task. This event + can be used to initiate further interaction with the user. Use + ``task.choice_flag`` to determine or change the action to be taken. + - - `import_task_files` + - :py:class:`task `, :py:class:`session + ` + - called after an import task finishes manipulating the filesystem + (copying and moving files, writing metadata tags). + - - `library_opened` + - :py:class:`lib ` + - called after beets starts up and initializes the main Library object. + - - `database_change` + - :py:class:`lib `, :py:class:`model + ` + - a modification has been made to the library database. The change might + not be committed yet. + - - `cli_exit` + - :py:class:`lib ` + - called just before the ``beet`` command-line program exits. + - - `import_begin` + - :py:class:`session ` + - called just before a ``beet import`` session starts up. + - - `trackinfo_received` + - :py:class:`info ` + - called after metadata for a track item has been fetched from a data + source, such as MusicBrainz. You can modify the tags that the rest of + the pipeline sees on a ``beet import`` operation or during later + adjustments, such as ``mbsync``. + - - `albuminfo_received` + - :py:class:`info ` + - like `trackinfo_received`, the event indicates new metadata for album + items. + - - `before_choose_candidate` + - :py:class:`task `, :py:class:`session + ` + - called before the user is prompted for a decision during a ``beet + import`` interactive session. Plugins can use this event for + :ref:`appending choices to the prompt ` by + returning a list of ``PromptChoices``. + - - `mb_track_extract` + - :py:class:`data ` + - called after the metadata is obtained from MusicBrainz. The parameter is + a ``dict`` containing the tags retrieved from MusicBrainz for a track. + Plugins must return a new (potentially empty) ``dict`` with additional + ``field: value`` pairs, which the autotagger will apply to the item, as + flexible attributes if ``field`` is not a hardcoded field. Fields + already present on the track are overwritten. + - - `mb_album_extract` + - :py:class:`data ` + - Like `mb_track_extract`, but for album tags. Overwrites tags set at the + track level, if they have the same ``field``. The included ``mpdupdate`` plugin provides an example use case for event -listeners. \ No newline at end of file +listeners. From 1dfd232270e61c23fe9ce385ad0c651b504ca75d Mon Sep 17 00:00:00 2001 From: Sebastian Mohr Date: Sun, 24 Aug 2025 11:21:17 +0200 Subject: [PATCH 05/26] further reading formatting and changed extending the autotagger to conform to new metadatasource plugin. --- docs/dev/plugins/other.rst | 169 +++++++++++++++++++------------------ 1 file changed, 85 insertions(+), 84 deletions(-) diff --git a/docs/dev/plugins/other.rst b/docs/dev/plugins/other.rst index 9e4589ce7..20441c0e9 100644 --- a/docs/dev/plugins/other.rst +++ b/docs/dev/plugins/other.rst @@ -1,52 +1,41 @@ +Further Reading +=============== +.. contents:: Table of Contents + :local: + :depth: 2 -Extend the Autotagger -~~~~~~~~~~~~~~~~~~~~~ +Extending the Autotagger +------------------------ -Plugins can also enhance the functionality of the autotagger. For a -comprehensive example, try looking at the ``chroma`` plugin, which is included -with beets. +.. currentmodule:: beets.metadata_plugins -A plugin can extend three parts of the autotagger's process: the track distance -function, the album distance function, and the initial MusicBrainz search. The -distance functions determine how "good" a match is at the track and album -levels; the initial search controls which candidates are presented to the -matching algorithm. Plugins implement these extensions by implementing four -methods on the plugin class: +Plugins can also be used to extend the autotagger functions i.e. the metadata +lookup from external sources. For this your plugin has to extend the +:py:class:`MetadataSourcePlugin` base class and implement all abstract methods. -- ``track_distance(self, item, info)``: adds a component to the distance - function (i.e., the similarity metric) for individual tracks. ``item`` is the - track to be matched (an Item object) and ``info`` is the TrackInfo object that - is proposed as a match. Should return a ``(dist, dist_max)`` pair of floats - indicating the distance. -- ``album_distance(self, items, album_info, mapping)``: like the above, but - compares a list of items (representing an album) to an album-level MusicBrainz - entry. ``items`` is a list of Item objects; ``album_info`` is an AlbumInfo - object; and ``mapping`` is a dictionary that maps Items to their corresponding - TrackInfo objects. -- ``candidates(self, items, artist, album, va_likely)``: given a list of items - comprised by an album to be matched, return a list of ``AlbumInfo`` objects - for candidate albums to be compared and matched. -- ``item_candidates(self, item, artist, album)``: given a *singleton* item, - return a list of ``TrackInfo`` objects for candidate tracks to be compared and - matched. -- ``album_for_id(self, album_id)``: given an ID from user input or an album's - tags, return a candidate AlbumInfo object (or None). -- ``track_for_id(self, track_id)``: given an ID from user input or a file's - tags, return a candidate TrackInfo object (or None). +On metadata lookup, the autotagger will try to find matching candidates from all +enabled metadata source plugins. To do this, we will call the +:py:meth:`MetadataSourcePlugin.candidates` (or +:py:meth:`MetadataSourcePlugin.item_candidates`) with all available (local) +metadata. The list of retrieved candidates will be ranked by their +:py:meth:`MetadataSourcePlugin.album_distance` (or +:py:meth:`MetadataSourcePlugin.track_distance`) and be presented to the user for +selection (or automatically selected if the threshold is met). -When implementing these functions, you may want to use the functions from the -``beets.autotag`` and ``beets.autotag.mb`` modules, both of which have somewhat -helpful docstrings. +Please have a look at the ``beets.autotag`` and especially the +``beets.metadata_plugin`` modules for more information. Additionally, for a +comprehensive example, see the ``musicbrainz`` or ``chroma`` plugins, which are +included with beets. Read Configuration Options -~~~~~~~~~~~~~~~~~~~~~~~~~~ +-------------------------- Plugins can configure themselves using the ``config.yaml`` file. You can read -configuration values in two ways. The first is to use ``self.config`` within -your plugin class. This gives you a view onto the configuration values in a -section with the same name as your plugin's module. For example, if your plugin -is in ``greatplugin.py``, then ``self.config`` will refer to options under the +configuration values in two ways. The first is to use `self.config` within your +plugin class. This gives you a view onto the configuration values in a section +with the same name as your plugin's module. For example, if your plugin is in +``greatplugin.py``, then `self.config` will refer to options under the ``greatplugin:`` section of the config file. For example, if you have a configuration value called "foo", then users can put @@ -58,26 +47,26 @@ this in their ``config.yaml``: foo: bar To access this value, say ``self.config['foo'].get()`` at any point in your -plugin's code. The ``self.config`` object is a *view* as defined by the Confuse_ +plugin's code. The `self.config` object is a *view* as defined by the Confuse_ library. .. _confuse: https://confuse.readthedocs.io/en/latest/ If you want to access configuration values *outside* of your plugin's section, -import the ``config`` object from the ``beets`` module. That is, just put ``from +import the `config` object from the `beets` module. That is, just put ``from beets import config`` at the top of your plugin and access values from there. If your plugin provides configuration values for sensitive data (e.g., passwords, API keys, ...), you should add these to the config so they can be redacted automatically when users dump their config. This can be done by setting -each value's ``redact`` flag, like so: +each value's `redact` flag, like so: :: self.config['password'].redact = True Add Path Format Functions and Fields -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +------------------------------------ Beets supports *function calls* in its path format syntax (see :doc:`/reference/pathformat`). Beets includes a few built-in functions, but @@ -86,18 +75,19 @@ dictionary. Here's an example: -:: +.. code-block:: python class MyPlugin(BeetsPlugin): def __init__(self): super().__init__() - self.template_funcs['initial'] = _tmpl_initial + self.template_funcs["initial"] = _tmpl_initial + def _tmpl_initial(text: str) -> str: if text: return text[0].upper() else: - return u'' + return "" This plugin provides a function ``%initial`` to path templates where ``%initial{$artist}`` expands to the artist's initial (its capitalized first @@ -108,12 +98,13 @@ Plugins can also add template *fields*, which are computed values referenced as ``Item`` object to the ``template_fields`` dictionary on the plugin object. Here's an example that adds a ``$disc_and_track`` field: -:: +.. code-block:: python class MyPlugin(BeetsPlugin): def __init__(self): super().__init__() - self.template_fields['disc_and_track'] = _tmpl_disc_and_track + self.template_fields["disc_and_track"] = _tmpl_disc_and_track + def _tmpl_disc_and_track(item: Item) -> str: """Expand to the disc number and track number if this is a @@ -133,7 +124,7 @@ template fields by adding a function accepting an ``Album`` argument to the ``album_template_fields`` dict. Extend MediaFile -~~~~~~~~~~~~~~~~ +---------------- MediaFile_ is the file tag abstraction layer that beets uses to make cross-format metadata manipulation simple. Plugins can add fields to MediaFile @@ -141,34 +132,34 @@ to extend the kinds of metadata that they can easily manage. The ``MediaFile`` class uses ``MediaField`` descriptors to provide access to file tags. If you have created a descriptor you can add it through your plugins -:py:meth:`beets.plugins.BeetsPlugin.add_media_field()` method. +:py:meth:`beets.plugins.BeetsPlugin.add_media_field()`` method. .. _mediafile: https://mediafile.readthedocs.io/en/latest/ Here's an example plugin that provides a meaningless new field "foo": -:: +.. code-block:: python class FooPlugin(BeetsPlugin): def __init__(self): field = mediafile.MediaField( - mediafile.MP3DescStorageStyle(u'foo'), - mediafile.StorageStyle(u'foo') + mediafile.MP3DescStorageStyle("foo"), mediafile.StorageStyle("foo") ) - self.add_media_field('foo', field) + self.add_media_field("foo", field) + FooPlugin() - item = Item.from_path('/path/to/foo/tag.mp3') - assert item['foo'] == 'spam' + item = Item.from_path("/path/to/foo/tag.mp3") + assert item["foo"] == "spam" - item['foo'] == 'ham' + item["foo"] == "ham" item.write() # The "foo" tag of the file is now "ham" .. _plugin-stage: Add Import Pipeline Stages -~~~~~~~~~~~~~~~~~~~~~~~~~~ +-------------------------- Many plugins need to add high-latency operations to the import workflow. For example, a plugin that fetches lyrics from the Web would, ideally, not block the @@ -186,20 +177,25 @@ Plugins provide stages as functions that take two arguments: ``config`` and in ``beets.importer``). Add such a function to the plugin's ``import_stages`` field to register it: -:: +.. code-block:: python from beets.plugins import BeetsPlugin + from beets.importer import ImportSession, ImportTask + + class ExamplePlugin(BeetsPlugin): + def __init__(self): super().__init__() self.import_stages = [self.stage] - def stage(self, session, task): - print('Importing something!') + + def stage(self, session: ImportSession, task: ImportTask): + print("Importing something!") It is also possible to request your function to run early in the pipeline by adding the function to the plugin's ``early_import_stages`` field instead: -:: +.. code-block:: python self.early_import_stages = [self.stage] @@ -233,46 +229,47 @@ from that class and override the ``value_match`` class method. (Remember the the ``@`` prefix to delimit exact string matches. The plugin will be used if we issue a command like ``beet ls @something`` or ``beet ls artist:@something``: -:: +.. code-block:: python from beets.plugins import BeetsPlugin from beets.dbcore import FieldQuery + class ExactMatchQuery(FieldQuery): @classmethod def value_match(self, pattern, val): return pattern == val + class ExactMatchPlugin(BeetsPlugin): def queries(self): - return { - '@': ExactMatchQuery - } + return {"@": ExactMatchQuery} Flexible Field Types -~~~~~~~~~~~~~~~~~~~~ +-------------------- If your plugin uses flexible fields to store numbers or other non-string values, you can specify the types of those fields. A rating plugin, for example, might want to declare that the ``rating`` field should have an integer type: -:: +.. code-block:: python from beets.plugins import BeetsPlugin from beets.dbcore import types + class RatingPlugin(BeetsPlugin): - item_types = {'rating': types.INTEGER} + item_types = {"rating": types.INTEGER} @property def album_types(self): - return {'rating': types.INTEGER} + return {"rating": types.INTEGER} -A plugin may define two attributes: ``item_types`` and ``album_types``. Each of +A plugin may define two attributes: `item_types` and `album_types`. Each of those attributes is a dictionary mapping a flexible field name to a type -instance. You can find the built-in types in the ``beets.dbcore.types`` and -``beets.library`` modules or implement your own type by inheriting from the -``Type`` class. +instance. You can find the built-in types in the `beets.dbcore.types` and +`beets.library` modules or implement your own type by inheriting from the `Type` +class. Specifying types has several advantages: @@ -287,7 +284,7 @@ Specifying types has several advantages: .. _plugin-logging: Logging -~~~~~~~ +------- Each plugin object has a ``_log`` attribute, which is a ``Logger`` from the `standard Python logging module`_. The logger is set up to `PEP 3101`_, @@ -295,7 +292,7 @@ str.format-style string formatting. So you can write logging calls like this: :: - self._log.debug(u'Processing {0.title} by {0.artist}', item) + self._log.debug('Processing {0.title} by {0.artist}', item) .. _pep 3101: https://www.python.org/dev/peps/pep-3101/ @@ -326,7 +323,7 @@ the importer interface when running automatically.) .. _append_prompt_choices: Append Prompt Choices -~~~~~~~~~~~~~~~~~~~~~ +--------------------- Plugins can also append choices to the prompt presented to the user during an import session. @@ -335,20 +332,24 @@ To do so, add a listener for the ``before_choose_candidate`` event, and return a list of ``PromptChoices`` that represent the additional choices that your plugin shall expose to the user: -:: +.. code-block:: python from beets.plugins import BeetsPlugin from beets.ui.commands import PromptChoice + class ExamplePlugin(BeetsPlugin): def __init__(self): super().__init__() - self.register_listener('before_choose_candidate', - self.before_choose_candidate_event) + self.register_listener( + "before_choose_candidate", self.before_choose_candidate_event + ) def before_choose_candidate_event(self, session, task): - return [PromptChoice('p', 'Print foo', self.foo), - PromptChoice('d', 'Do bar', self.bar)] + return [ + PromptChoice("p", "Print foo", self.foo), + PromptChoice("d", "Do bar", self.bar), + ] def foo(self, session, task): print('User has chosen "Print foo"!') @@ -358,14 +359,14 @@ shall expose to the user: The previous example modifies the standard prompt: -:: +.. code-block:: shell # selection (default 1), Skip, Use as-is, as Tracks, Group albums, Enter search, enter Id, aBort? by appending two additional options (``Print foo`` and ``Do bar``): -:: +.. code-block:: shell # selection (default 1), Skip, Use as-is, as Tracks, Group albums, Enter search, enter Id, aBort, Print foo, Do bar? From d6e3548d4a4ca02d2e306c20d8c77881439641dc Mon Sep 17 00:00:00 2001 From: Sebastian Mohr Date: Sun, 24 Aug 2025 11:21:47 +0200 Subject: [PATCH 06/26] Formatting commands --- docs/dev/plugins/commands.rst | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/docs/dev/plugins/commands.rst b/docs/dev/plugins/commands.rst index 6a9727859..f39578f11 100644 --- a/docs/dev/plugins/commands.rst +++ b/docs/dev/plugins/commands.rst @@ -1,23 +1,28 @@ .. _add_subcommands: Add Commands to the CLI -~~~~~~~~~~~~~~~~~~~~~~~ +======================= Plugins can add new subcommands to the ``beet`` command-line interface. Define the plugin class' ``commands()`` method to return a list of ``Subcommand`` objects. (The ``Subcommand`` class is defined in the ``beets.ui`` module.) Here's an example plugin that adds a simple command: -:: +.. code-block:: python from beets.plugins import BeetsPlugin from beets.ui import Subcommand - my_super_command = Subcommand('super', help='do something super') + my_super_command = Subcommand("super", help="do something super") + + def say_hi(lib, opts, args): print("Hello everybody! I'm a plugin!") + + my_super_command.func = say_hi + class SuperPlug(BeetsPlugin): def commands(self): return [my_super_command] @@ -47,4 +52,3 @@ use it like you would a normal ``OptionParser`` in an independent script. Note that it offers several methods to add common options: ``--album``, ``--path`` and ``--format``. This feature is versatile and extensively documented, try ``pydoc beets.ui.CommonOptionsParser`` for more information. - From 83eda270516f6a9e87ad461cdfe93e6d093def78 Mon Sep 17 00:00:00 2001 From: Sebastian Mohr Date: Sun, 24 Aug 2025 11:21:59 +0200 Subject: [PATCH 07/26] Update docstrfmt --- docs/code_of_conduct.rst | 3 +-- docs/contributing.rst | 3 +-- poetry.lock | 28 ++++++++++++++-------------- pyproject.toml | 4 ++-- 4 files changed, 18 insertions(+), 20 deletions(-) diff --git a/docs/code_of_conduct.rst b/docs/code_of_conduct.rst index 772800d44..76e57d0e6 100644 --- a/docs/code_of_conduct.rst +++ b/docs/code_of_conduct.rst @@ -1,4 +1,3 @@ -.. - code_of_conduct: +.. code_of_conduct: .. include:: ../CODE_OF_CONDUCT.rst diff --git a/docs/contributing.rst b/docs/contributing.rst index 6c71b2ce0..6af7deaef 100644 --- a/docs/contributing.rst +++ b/docs/contributing.rst @@ -1,4 +1,3 @@ -.. - contributing: +.. contributing: .. include:: ../CONTRIBUTING.rst diff --git a/poetry.lock b/poetry.lock index 25d9448ba..3383129ff 100644 --- a/poetry.lock +++ b/poetry.lock @@ -696,28 +696,28 @@ files = [ [[package]] name = "docstrfmt" -version = "1.10.0" +version = "1.11.0" description = "docstrfmt: A formatter for Sphinx flavored reStructuredText." optional = false -python-versions = "<4,>=3.8" +python-versions = ">=3.9" files = [ - {file = "docstrfmt-1.10.0-py3-none-any.whl", hash = "sha256:a34ef6f3d8ab3233a7d0b3d1c2f3c66f8acbb3917df5ed2f3e34c1629ac29cef"}, - {file = "docstrfmt-1.10.0.tar.gz", hash = "sha256:9da96e71552937f4b49ae2d6ab1c118ffa8ad6968082e6b8fd978b01d1bc0066"}, + {file = "docstrfmt-1.11.0-py3-none-any.whl", hash = "sha256:3d56bdd6e083091a8c5d7db098684f281de84667f4b7d4cc806092a63efc4844"}, + {file = "docstrfmt-1.11.0.tar.gz", hash = "sha256:37500c8086770294f265187c375c5c35a91a334d2c0b4f764aeace069d4ed501"}, ] [package.dependencies] -black = "==24.*" -click = "==8.*" -docutils = "==0.20.*" -libcst = "==1.*" -platformdirs = "==4.*" -sphinx = ">=7,<9" -tabulate = "==0.9.*" -toml = "==0.10.*" +black = ">=24" +click = ">=8" +docutils = ">=0.20" +libcst = ">=1" +platformdirs = ">=4" +sphinx = ">=7" +tabulate = ">=0.9" +toml = {version = ">=0.10", markers = "python_version < \"3.11\""} [package.extras] ci = ["coveralls"] -d = ["aiohttp (==3.*)"] +d = ["aiohttp (>=3)"] dev = ["docstrfmt[lint]", "docstrfmt[test]", "packaging"] lint = ["pre-commit", "ruff (>=0.0.292)"] test = ["pytest", "pytest-aiohttp"] @@ -3617,4 +3617,4 @@ web = ["flask", "flask-cors"] [metadata] lock-version = "2.0" python-versions = ">=3.9,<4" -content-hash = "daa6c3c2b5bee3180f74f4186bb29ee1ad825870b5b9f6c2b743fcaa61b34c8c" +content-hash = "b68f663437ec41a92319456277243438a5c5f5c5f3dca113df1d7cac1b78703a" diff --git a/pyproject.toml b/pyproject.toml index 3ba7b8b6a..2377f52f2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -100,7 +100,7 @@ requests_oauthlib = "*" responses = ">=0.3.0" [tool.poetry.group.lint.dependencies] -docstrfmt = ">=1.10.0" +docstrfmt = ">=1.11.0" ruff = ">=0.6.4" sphinx-lint = ">=1.0.0" @@ -212,7 +212,7 @@ cmd = "ruff check" [tool.poe.tasks.lint-docs] help = "Lint the documentation" -shell = "sphinx-lint --enable all $(git ls-files '*.rst')" +shell = "sphinx-lint --enable all --disable default-role $(git ls-files '*.rst')" [tool.poe.tasks.update-dependencies] help = "Update dependencies to their latest versions." From ea80ecab366431e795420e1567833a354ae0eaa3 Mon Sep 17 00:00:00 2001 From: Sebastian Mohr Date: Sun, 24 Aug 2025 11:22:50 +0200 Subject: [PATCH 08/26] Git blame for first commit --- .git-blame-ignore-revs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs index b20434e23..75e49f8cc 100644 --- a/.git-blame-ignore-revs +++ b/.git-blame-ignore-revs @@ -57,6 +57,8 @@ c490ac5810b70f3cf5fd8649669838e8fdb19f4d 769dcdc88a1263638ae25944ba6b2be3e8933666 # Reformat all docs using docstrfmt ab5acaabb3cd24c482adb7fa4800c89fd6a2f08d +# Moved dev docs +2504595532abd7584143007ede087ee4abc00916 # Replace format calls with f-strings 4a361bd501e85de12c91c2474c423559ca672852 # Replace percent formatting From 0dcd7caa9d512a8e536f4522eefe51c13a73d7b3 Mon Sep 17 00:00:00 2001 From: Sebastian Mohr Date: Sun, 24 Aug 2025 11:33:03 +0200 Subject: [PATCH 09/26] writing plugins label --- docs/dev/plugins/index.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/dev/plugins/index.rst b/docs/dev/plugins/index.rst index 491e48e0e..98a37aa87 100644 --- a/docs/dev/plugins/index.rst +++ b/docs/dev/plugins/index.rst @@ -9,6 +9,8 @@ For instance you can create plugins that add new commands to the command-line interface, listen for events in the beets lifecycle or extend the autotagger with new metadata sources. .. _writing-plugins: +.. _writing-plugins: + Basic Plugin Setup ------------------ From 676dc9c9537c8c9b49eeef13e25d68b3fda44aa7 Mon Sep 17 00:00:00 2001 From: Sebastian Mohr Date: Tue, 26 Aug 2025 11:23:12 +0200 Subject: [PATCH 10/26] Replaced writing-plugins with basic-plugin-setup. --- docs/changelog.rst | 6 +++--- docs/dev/plugins/index.rst | 4 ++-- docs/reference/pathformat.rst | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index d27596b64..95c22115b 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -4178,7 +4178,7 @@ fetching cover art for your music, enable this plugin after upgrading to beets "database is locked"). This release synchronizes access to the database to avoid internal SQLite contention, which should avoid this error. - Plugins can now add parallel stages to the import pipeline. See - :ref:`writing-plugins`. + :ref:`basic-plugin-setup`. - Beets now prints out an error when you use an unrecognized field name in a query: for example, when running ``beet ls -a artist:foo`` (because ``artist`` is an item-level field). @@ -4361,7 +4361,7 @@ to come in the next couple of releases. addition to replacing them) if the special string ```` is specified as the replacement. - New plugin API: plugins can now add fields to the MediaFile tag abstraction - layer. See :ref:`writing-plugins`. + layer. See :ref:`basic-plugin-setup`. - A reasonable error message is now shown when the import log file cannot be opened. - The import log file is now flushed and closed properly so that it can be used @@ -4405,7 +4405,7 @@ filenames that would otherwise conflict. Three new plugins (``inline``, naming rules: for example, ``%upper{%left{$artist,1}}`` will insert the capitalized first letter of the track's artist. For more details, see :doc:`/reference/pathformat`. If you're interested in adding your own template - functions via a plugin, see :ref:`writing-plugins`. + functions via a plugin, see :ref:`basic-plugin-setup`. - Plugins can also now define new path *fields* in addition to functions. - The new :doc:`/plugins/inline` lets you **use Python expressions to customize path formats** by defining new fields in the config file. diff --git a/docs/dev/plugins/index.rst b/docs/dev/plugins/index.rst index 98a37aa87..ff5a03652 100644 --- a/docs/dev/plugins/index.rst +++ b/docs/dev/plugins/index.rst @@ -7,9 +7,9 @@ add virtually any type of features to beets. For instance you can create plugins that add new commands to the command-line interface, listen for events in the beets lifecycle or extend the autotagger -with new metadata sources. .. _writing-plugins: +with new metadata sources. -.. _writing-plugins: +.. _basic-plugin-setup: Basic Plugin Setup ------------------ diff --git a/docs/reference/pathformat.rst b/docs/reference/pathformat.rst index 1fc204b62..30871cf55 100644 --- a/docs/reference/pathformat.rst +++ b/docs/reference/pathformat.rst @@ -289,4 +289,4 @@ constructs include: The :doc:`/plugins/inline` lets you define template fields in your beets configuration file using Python snippets. And for more advanced processing, you can go all-in and write a dedicated plugin to register your own fields and -functions (see :ref:`writing-plugins`). +functions (see :ref:`basic-plugin-setup`). From 037e59fe8f528a6ccded8848303c07ee45e1a261 Mon Sep 17 00:00:00 2001 From: Sebastian Mohr Date: Tue, 26 Aug 2025 12:15:25 +0200 Subject: [PATCH 11/26] Created autotagger file and enhanced the docs significantly. --- docs/dev/plugins/autotagger.rst | 103 ++++++++++++++++++++++++++++++++ docs/dev/plugins/index.rst | 1 + docs/dev/plugins/other.rst | 23 ------- 3 files changed, 104 insertions(+), 23 deletions(-) create mode 100644 docs/dev/plugins/autotagger.rst diff --git a/docs/dev/plugins/autotagger.rst b/docs/dev/plugins/autotagger.rst new file mode 100644 index 000000000..2d59ecdcf --- /dev/null +++ b/docs/dev/plugins/autotagger.rst @@ -0,0 +1,103 @@ +Extending the Autotagger +======================== + +.. currentmodule:: beets.metadata_plugins + +Beets supports **metadata source plugins**, which allow it to fetch and match +metadata from external services (such as Spotify, Discogs, or Deezer). This +guide explains how to build your own metadata source plugin by extending the +:py:class:`MetadataSourcePlugin`. + +These plugins integrate directly with the autotagger, providing candidate +metadata during lookups. To implement one, you must subclass +:py:class:`MetadataSourcePlugin` and implement its abstract methods. + +Overview +-------- + +Creating a metadata source plugin is very similar to writing a standard plugin +(see :ref:`basic-plugin-setup`). The main difference is that your plugin must: + +1. Subclass :py:class:`MetadataSourcePlugin`. +2. Implement all required abstract methods. + +Here`s a minimal example: + +.. code-block:: python + + # beetsplug/myawesomeplugin.py + from typing import Sequence + from beets.autotag.hooks import Item + from beets.metadata_plugin import MetadataSourcePlugin + + + class MyAwesomePlugin(MetadataSourcePlugin): + + def candidates( + self, + items: Sequence[Item], + artist: str, + album: str, + va_likely: bool, + ): ... + + def item_candidates(self, item: Item, artist: str, title: str): ... + + def track_for_id(self, track_id: str): ... + + def album_for_id(self, album_id: str): ... + +How Metadata Lookup Works +------------------------- + +When beets runs the autotagger, it queries **all enabled metadata source +plugins** for potential matches: + +- For **albums**, it calls :py:meth:`~MetadataSourcePlugin.candidates`. +- For **individual items**, it calls + :py:meth:`~MetadataSourcePlugin.item_candidates`. + +The results are combined and scored. By default, candidate ranking is handled +automatically by the beets core, but you can customize weighting by overriding: + +- :py:meth:`~MetadataSourcePlugin.album_distance` +- :py:meth:`~MetadataSourcePlugin.track_distance` + +This is optional, if not overridden, both methods return a constant distance of +`0.5`. + +Implementing ID-based Lookups +----------------------------- + +Your plugin must also define: + +- :py:meth:`~MetadataSourcePlugin.album_for_id` — fetch album metadata by ID. +- :py:meth:`~MetadataSourcePlugin.track_for_id` — fetch track metadata by ID. + +These methods should return `None` if your source doesn`t support ID lookups. +IDs are expected to be strings. If your source uses specific formats, consider +contributing an extractor regex to the core module: +:py:mod:`beets.util.id_extractors`. + +Best Practices +-------------- + +Beets already ships with several metadata source plugins. Studying these +implementations can help you follow conventions and avoid pitfalls. Good +starting points include: + +- `spotify` +- `deezer` +- `discogs` + +Migration Guidance +------------------ + +Older metadata plugins that extend :py:class:`beets.plugins.BeetsPlugin` should +be migrated to :py:class:`MetadataSourcePlugin`. Legacy support will be removed +in **beets v3.0.0**. + +.. seealso:: + + - :py:mod:`beets.autotag` + - :py:mod:`beets.metadata_plugins` diff --git a/docs/dev/plugins/index.rst b/docs/dev/plugins/index.rst index ff5a03652..1321c61dc 100644 --- a/docs/dev/plugins/index.rst +++ b/docs/dev/plugins/index.rst @@ -99,4 +99,5 @@ resources: commands events + autotagger other diff --git a/docs/dev/plugins/other.rst b/docs/dev/plugins/other.rst index 20441c0e9..164bac13c 100644 --- a/docs/dev/plugins/other.rst +++ b/docs/dev/plugins/other.rst @@ -5,29 +5,6 @@ Further Reading :local: :depth: 2 -Extending the Autotagger ------------------------- - -.. currentmodule:: beets.metadata_plugins - -Plugins can also be used to extend the autotagger functions i.e. the metadata -lookup from external sources. For this your plugin has to extend the -:py:class:`MetadataSourcePlugin` base class and implement all abstract methods. - -On metadata lookup, the autotagger will try to find matching candidates from all -enabled metadata source plugins. To do this, we will call the -:py:meth:`MetadataSourcePlugin.candidates` (or -:py:meth:`MetadataSourcePlugin.item_candidates`) with all available (local) -metadata. The list of retrieved candidates will be ranked by their -:py:meth:`MetadataSourcePlugin.album_distance` (or -:py:meth:`MetadataSourcePlugin.track_distance`) and be presented to the user for -selection (or automatically selected if the threshold is met). - -Please have a look at the ``beets.autotag`` and especially the -``beets.metadata_plugin`` modules for more information. Additionally, for a -comprehensive example, see the ``musicbrainz`` or ``chroma`` plugins, which are -included with beets. - Read Configuration Options -------------------------- From dee906e1ae1771fcb399feb782fe86be6114dd64 Mon Sep 17 00:00:00 2001 From: Sebastian Mohr Date: Tue, 26 Aug 2025 12:20:09 +0200 Subject: [PATCH 12/26] Minor formatting issues. --- docs/dev/plugins/other.rst | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/docs/dev/plugins/other.rst b/docs/dev/plugins/other.rst index 164bac13c..42fa9e768 100644 --- a/docs/dev/plugins/other.rst +++ b/docs/dev/plugins/other.rst @@ -109,7 +109,7 @@ to extend the kinds of metadata that they can easily manage. The ``MediaFile`` class uses ``MediaField`` descriptors to provide access to file tags. If you have created a descriptor you can add it through your plugins -:py:meth:`beets.plugins.BeetsPlugin.add_media_field()`` method. +:py:meth:`beets.plugins.BeetsPlugin.add_media_field` method. .. _mediafile: https://mediafile.readthedocs.io/en/latest/ @@ -156,8 +156,8 @@ field to register it: .. code-block:: python - from beets.plugins import BeetsPlugin from beets.importer import ImportSession, ImportTask + from beets.plugins import BeetsPlugin class ExamplePlugin(BeetsPlugin): @@ -242,11 +242,11 @@ want to declare that the ``rating`` field should have an integer type: def album_types(self): return {"rating": types.INTEGER} -A plugin may define two attributes: `item_types` and `album_types`. Each of +A plugin may define two attributes: ``item_types`` and ``album_types``. Each of those attributes is a dictionary mapping a flexible field name to a type -instance. You can find the built-in types in the `beets.dbcore.types` and -`beets.library` modules or implement your own type by inheriting from the `Type` -class. +instance. You can find the built-in types in the ``beets.dbcore.types`` and +``beets.library`` modules or implement your own type by inheriting from the +``Type`` class. Specifying types has several advantages: @@ -267,9 +267,9 @@ Each plugin object has a ``_log`` attribute, which is a ``Logger`` from the `standard Python logging module`_. The logger is set up to `PEP 3101`_, str.format-style string formatting. So you can write logging calls like this: -:: +.. code-block:: python - self._log.debug('Processing {0.title} by {0.artist}', item) + self._log.debug("Processing {0.title} by {0.artist}", item) .. _pep 3101: https://www.python.org/dev/peps/pep-3101/ From 4a8cabdaeb84ccd2de946cf6eabbb7fac598ebca Mon Sep 17 00:00:00 2001 From: Sebastian Mohr Date: Tue, 26 Aug 2025 12:31:29 +0200 Subject: [PATCH 13/26] Use rubric instead of list table. --- docs/dev/plugins/events.rst | 317 +++++++++++++++++++----------------- 1 file changed, 165 insertions(+), 152 deletions(-) diff --git a/docs/dev/plugins/events.rst b/docs/dev/plugins/events.rst index 3895d35aa..325b01b33 100644 --- a/docs/dev/plugins/events.rst +++ b/docs/dev/plugins/events.rst @@ -44,163 +44,176 @@ registration process in this case: def loaded(self): self._log.info("Plugin loaded!") -.. list-table:: Plugin Events - :widths: 15 25 60 - :header-rows: 1 +.. rubric:: Plugin Events - - - Event - - Parameters - - Description - - - `pluginload` - - - - called after all the plugins have been loaded after the ``beet`` command - starts - - - `import` - - :py:class:`lib `, ``paths`` is a list of paths - (strings) - - called after the ``import`` command finishes. - - - `album_imported` - - :py:class:`lib `, :py:class:`album +``pluginload`` + :Parameters: (none) + :Description: Called after all plugins have been loaded after the ``beet`` + command starts. + +``import`` + :Parameters: :py:class:`lib `, ``paths`` (list of + path strings) + :Description: Called after the ``import`` command finishes. + +``album_imported`` + :Parameters: :py:class:`lib `, :py:class:`album ` - - called every time the ``import`` command finishes adding an album to the - library - - - `album_removed` - - :py:class:`lib `, :py:class:`album + :Description: Called every time the importer finishes adding an album to the + library. + +``album_removed`` + :Parameters: :py:class:`lib `, :py:class:`album ` - - called every time an album is removed from the library (even when its - file is not deleted from disk) - - - `item_copied` - - :py:class:`item `, ``source`` path, ``destination`` - path - - called whenever an item file is copied - - - `item_imported` - - :py:class:`lib `, :py:class:`item + :Description: Called every time an album is removed from the library (even + when its files are not deleted from disk). + +``item_copied`` + :Parameters: :py:class:`item `, ``source`` (path), + ``destination`` (path) + :Description: Called whenever an item file is copied. + +``item_imported`` + :Parameters: :py:class:`lib `, :py:class:`item ` - - called every time the importer adds a singleton to the library (not - called for full-album imports) - - - `before_item_imported` - - :py:class:`item `, ``source`` path, ``destination`` - path - - called with an item object immediately before it is imported - - - `before_item_moved` - - :py:class:`item `, ``source`` path, ``destination`` - path - - called with an ``Item`` object immediately before its file is moved - - - `item_moved` - - :py:class:`item `, ``source`` path, ``destination`` - path - - called with an ``Item`` object whenever its file is moved - - - `item_linked` - - :py:class:`item `, ``source`` path, ``destination`` - path - - called with an ``Item`` object whenever a symlink is created for a file - - - `item_hardlinked` - - :py:class:`item `, ``source`` path, ``destination`` - path - - called with an ``Item`` object whenever a hardlink is created for a file - - - `item_reflinked` - - :py:class:`item `, ``source`` path, ``destination`` - path - - called with an ``Item`` object whenever a reflink is created for a file - - - `item_removed` - - :py:class:`item ` - - called with an ``Item`` object every time an item (singleton or album's - part) is removed from the library (even when its file is not deleted - from disk). - - - `write` - - :py:class:`item `, ``path``, ``tags`` dictionary - - called with an ``Item`` object, a ``path``, and a ``tags`` dictionary - just before a file's metadata is written to disk (i.e., just before the - file on disk is opened). Event handlers may change the ``tags`` - dictionary to customize the tags that are written to the media file. - Event handlers may also raise a ``library.FileOperationError`` exception - to abort the write operation. Beets will catch that exception, print an - error message, and continue. - - - `after_write` - - :py:class:`item ` - - called with an ``Item`` object after a file's metadata is written to - disk (i.e., just after the file on disk is closed). - - - `import_task_created` - - :py:class:`task `, :py:class:`session - ` - - called immediately after an import task is initialized. Plugins can use - this to, for example, change imported files of a task before anything - else happens. It's also possible to replace the task with another task - by returning a list of tasks. This list can contain zero or more - ImportTasks. Returning an empty list will stop the task. - - - `import_task_start` - - :py:class:`task `, :py:class:`session - ` - - called when before an import task begins processing. - - - `import_task_apply` - - :py:class:`task `, :py:class:`session - ` - - called after metadata changes have been applied in an import task. This - is called on the same thread as the UI, so use this sparingly and only - for tasks that can be done quickly. For most plugins, an import pipeline - stage is a better choice (see :ref:`plugin-stage`). - - - `import_task_before_choice` - - :py:class:`task `, :py:class:`session - ` - - called after candidate search for an import task before any decision is - made about how/if to import or tag. Can be used to present information - about the task or initiate interaction with the user before importing - occurs. Return an importer action to take a specific action. Only one - handler may return a non-None result. - - - `import_task_choice` - - :py:class:`task `, :py:class:`session - ` - - called after a decision has been made about an import task. This event - can be used to initiate further interaction with the user. Use - ``task.choice_flag`` to determine or change the action to be taken. - - - `import_task_files` - - :py:class:`task `, :py:class:`session - ` - - called after an import task finishes manipulating the filesystem - (copying and moving files, writing metadata tags). - - - `library_opened` - - :py:class:`lib ` - - called after beets starts up and initializes the main Library object. - - - `database_change` - - :py:class:`lib `, :py:class:`model + :Description: Called every time the importer adds a singleton to the library + (not called for full-album imports). + +``before_item_imported`` + :Parameters: :py:class:`item `, ``source`` (path), + ``destination`` (path) + :Description: Called with an ``Item`` object immediately before it is + imported. + +``before_item_moved`` + :Parameters: :py:class:`item `, ``source`` (path), + ``destination`` (path) + :Description: Called with an ``Item`` object immediately before its file is + moved. + +``item_moved`` + :Parameters: :py:class:`item `, ``source`` (path), + ``destination`` (path) + :Description: Called with an ``Item`` object whenever its file is moved. + +``item_linked`` + :Parameters: :py:class:`item `, ``source`` (path), + ``destination`` (path) + :Description: Called with an ``Item`` object whenever a symlink is created + for a file. + +``item_hardlinked`` + :Parameters: :py:class:`item `, ``source`` (path), + ``destination`` (path) + :Description: Called with an ``Item`` object whenever a hardlink is created + for a file. + +``item_reflinked`` + :Parameters: :py:class:`item `, ``source`` (path), + ``destination`` (path) + :Description: Called with an ``Item`` object whenever a reflink is created + for a file. + +``item_removed`` + :Parameters: :py:class:`item ` + :Description: Called with an ``Item`` object every time an item (singleton + or part of an album) is removed from the library (even when its file is + not deleted from disk). + +``write`` + :Parameters: :py:class:`item `, ``path`` (path), + ``tags`` (dict) + :Description: Called just before a file's metadata is written to disk. + Handlers may modify ``tags`` or raise ``library.FileOperationError`` to + abort. + +``after_write`` + :Parameters: :py:class:`item ` + :Description: Called after a file's metadata is written to disk. + +``import_task_created`` + :Parameters: :py:class:`task `, + :py:class:`session ` + :Description: Called immediately after an import task is initialized. May + return a list (possibly empty) of replacement tasks. + +``import_task_start`` + :Parameters: :py:class:`task `, + :py:class:`session ` + :Description: Called before an import task begins processing. + +``import_task_apply`` + :Parameters: :py:class:`task `, + :py:class:`session ` + :Description: Called after metadata changes have been applied in an import + task (on the UI thread; keep fast). Prefer a pipeline stage otherwise + (see :ref:`plugin-stage`). + +``import_task_before_choice`` + :Parameters: :py:class:`task `, + :py:class:`session ` + :Description: Called after candidate search and before deciding how to + import. May return an importer action (only one handler may return + non-None). + +``import_task_choice`` + :Parameters: :py:class:`task `, + :py:class:`session ` + :Description: Called after a decision has been made about an import task. + Use ``task.choice_flag`` to inspect or change the action. + +``import_task_files`` + :Parameters: :py:class:`task `, + :py:class:`session ` + :Description: Called after filesystem manipulation (copy/move/write) for an + import task. + +``library_opened`` + :Parameters: :py:class:`lib ` + :Description: Called after beets starts and initializes the main Library + object. + +``database_change`` + :Parameters: :py:class:`lib `, :py:class:`model ` - - a modification has been made to the library database. The change might - not be committed yet. - - - `cli_exit` - - :py:class:`lib ` - - called just before the ``beet`` command-line program exits. - - - `import_begin` - - :py:class:`session ` - - called just before a ``beet import`` session starts up. - - - `trackinfo_received` - - :py:class:`info ` - - called after metadata for a track item has been fetched from a data - source, such as MusicBrainz. You can modify the tags that the rest of - the pipeline sees on a ``beet import`` operation or during later - adjustments, such as ``mbsync``. - - - `albuminfo_received` - - :py:class:`info ` - - like `trackinfo_received`, the event indicates new metadata for album - items. - - - `before_choose_candidate` - - :py:class:`task `, :py:class:`session - ` - - called before the user is prompted for a decision during a ``beet - import`` interactive session. Plugins can use this event for - :ref:`appending choices to the prompt ` by - returning a list of ``PromptChoices``. - - - `mb_track_extract` - - :py:class:`data ` - - called after the metadata is obtained from MusicBrainz. The parameter is - a ``dict`` containing the tags retrieved from MusicBrainz for a track. - Plugins must return a new (potentially empty) ``dict`` with additional - ``field: value`` pairs, which the autotagger will apply to the item, as - flexible attributes if ``field`` is not a hardcoded field. Fields - already present on the track are overwritten. - - - `mb_album_extract` - - :py:class:`data ` - - Like `mb_track_extract`, but for album tags. Overwrites tags set at the - track level, if they have the same ``field``. + :Description: A modification has been made to the library database (may not + yet be committed). + +``cli_exit`` + :Parameters: :py:class:`lib ` + :Description: Called just before the ``beet`` command-line program exits. + +``import_begin`` + :Parameters: :py:class:`session ` + :Description: Called just before a ``beet import`` session starts. + +``trackinfo_received`` + :Parameters: :py:class:`info ` + :Description: Called after metadata for a track is fetched (e.g., from + MusicBrainz). Handlers can modify the tags seen by later pipeline stages + or adjustments (e.g., ``mbsync``). + +``albuminfo_received`` + :Parameters: :py:class:`info ` + :Description: Like ``trackinfo_received`` but for album-level metadata. + +``before_choose_candidate`` + :Parameters: :py:class:`task `, + :py:class:`session ` + :Description: Called before prompting the user during interactive import. + May return a list of ``PromptChoices`` to append to the prompt (see + :ref:`append_prompt_choices`). + +``mb_track_extract`` + :Parameters: :py:class:`data ` + :Description: Called after metadata is obtained from MusicBrainz for a + track. Must return a (possibly empty) dict of additional ``field: + value`` pairs to apply (overwriting existing fields). + +``mb_album_extract`` + :Parameters: :py:class:`data ` + :Description: Like ``mb_track_extract`` but for album tags. Overwrites tags + set at the track level with the same field. The included ``mpdupdate`` plugin provides an example use case for event listeners. From db9a587492a9e8feef6cd74ed918315bf0bdd5b5 Mon Sep 17 00:00:00 2001 From: Sebastian Mohr Date: Tue, 26 Aug 2025 13:02:23 +0200 Subject: [PATCH 14/26] Replaced named citations with number, seems to work for some reason. --- docs/dev/plugins/index.rst | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/docs/dev/plugins/index.rst b/docs/dev/plugins/index.rst index 1321c61dc..6f284b90b 100644 --- a/docs/dev/plugins/index.rst +++ b/docs/dev/plugins/index.rst @@ -15,8 +15,8 @@ Basic Plugin Setup ------------------ A beets plugin is just a Python module or package inside the ``beetsplug`` -namespace [namespace]_ package. To create the basic plugin layout, create a -directory called ``beetsplug`` and add either your plugin module: +namespace [1]_ package. To create the basic plugin layout, create a directory +called ``beetsplug`` and add either your plugin module: .. code-block:: shell @@ -40,8 +40,8 @@ or your plugin subpackage anymore. The meat of your plugin goes in ``myawesomeplugin.py``. Every plugin has to -extend the :class:`beets.plugins.BeetsPlugin` abstract base class [baseclass]_ . -For instance, a minimal plugin without any functionality would look like this: +extend the :class:`beets.plugins.BeetsPlugin` abstract base class [2]_ . For +instance, a minimal plugin without any functionality would look like this: .. code-block:: python @@ -52,9 +52,9 @@ For instance, a minimal plugin without any functionality would look like this: class MyAwesomePlugin(BeetsPlugin): pass -To use your new plugin, you need to package [packaging]_ your plugin and install -it into your ``beets`` (virtual) environment. To enable your plugin, add it it -to the beets configuration +To use your new plugin, you need to package [3]_ your plugin and install it into +your ``beets`` (virtual) environment. To enable your plugin, add it it to the +beets configuration .. code-block:: yaml @@ -64,16 +64,16 @@ to the beets configuration and you're good to go! -.. [namespace] Check out `this article`_ and `this Stack Overflow question`_ if - you haven't heard about namespace packages. +.. [1] Check out `this article`_ and `this Stack Overflow question`_ if you + haven't heard about namespace packages. -.. [baseclass] Abstract base classes allow us to define a contract which any - plugin must follow. This is a common paradigm in object-oriented - programming, and it helps to ensure that plugins are implemented in a - consistent way. For more information, see for example pep-3119_. +.. [2] Abstract base classes allow us to define a contract which any plugin must + follow. This is a common paradigm in object-oriented programming, and it + helps to ensure that plugins are implemented in a consistent way. For more + information, see for example pep-3119_. -.. [packaging] There are a variety of packaging tools available for python, for - example you can use poetry_, setuptools_ or hatchling_. +.. [3] There are a variety of packaging tools available for python, for example + you can use poetry_, setuptools_ or hatchling_. .. _hatchling: https://hatch.pypa.io/latest/config/build/#build-system From 1c6921758cd54a07092b1fff02dae8c1aeca8009 Mon Sep 17 00:00:00 2001 From: Sebastian Mohr Date: Wed, 27 Aug 2025 11:40:54 +0200 Subject: [PATCH 15/26] Capitalization and some more minor adjustments. Updated docstrfmt. --- docs/dev/index.rst | 2 +- docs/dev/plugins/autotagger.rst | 22 +++++++++++++--------- docs/dev/plugins/index.rst | 2 +- docs/guides/tagger.rst | 2 ++ poetry.lock | 20 ++++++++++++++++---- pyproject.toml | 2 +- 6 files changed, 34 insertions(+), 16 deletions(-) diff --git a/docs/dev/index.rst b/docs/dev/index.rst index 8d9200f67..633d50cd1 100644 --- a/docs/dev/index.rst +++ b/docs/dev/index.rst @@ -13,7 +13,7 @@ configuration files, respectively. .. _mediafile: https://mediafile.readthedocs.io/en/latest/ .. toctree:: - :maxdepth: 1 + :maxdepth: 2 :titlesonly: plugins/index diff --git a/docs/dev/plugins/autotagger.rst b/docs/dev/plugins/autotagger.rst index 2d59ecdcf..1a4fa6dd6 100644 --- a/docs/dev/plugins/autotagger.rst +++ b/docs/dev/plugins/autotagger.rst @@ -47,15 +47,18 @@ Here`s a minimal example: def album_for_id(self, album_id: str): ... -How Metadata Lookup Works -------------------------- +Each metadata source plugin automatically gets a unique identifier. You can +access this identifier using the :py:meth:`~MetadataSourcePlugin.data_source` +class property to tell plugins apart. + +Metadata lookup +--------------- When beets runs the autotagger, it queries **all enabled metadata source plugins** for potential matches: - For **albums**, it calls :py:meth:`~MetadataSourcePlugin.candidates`. -- For **individual items**, it calls - :py:meth:`~MetadataSourcePlugin.item_candidates`. +- For **singletons**, it calls :py:meth:`~MetadataSourcePlugin.item_candidates`. The results are combined and scored. By default, candidate ranking is handled automatically by the beets core, but you can customize weighting by overriding: @@ -66,20 +69,19 @@ automatically by the beets core, but you can customize weighting by overriding: This is optional, if not overridden, both methods return a constant distance of `0.5`. -Implementing ID-based Lookups ------------------------------ +ID-based lookups +---------------- Your plugin must also define: - :py:meth:`~MetadataSourcePlugin.album_for_id` — fetch album metadata by ID. - :py:meth:`~MetadataSourcePlugin.track_for_id` — fetch track metadata by ID. -These methods should return `None` if your source doesn`t support ID lookups. IDs are expected to be strings. If your source uses specific formats, consider contributing an extractor regex to the core module: :py:mod:`beets.util.id_extractors`. -Best Practices +Best practices -------------- Beets already ships with several metadata source plugins. Studying these @@ -90,7 +92,7 @@ starting points include: - `deezer` - `discogs` -Migration Guidance +Migration guidance ------------------ Older metadata plugins that extend :py:class:`beets.plugins.BeetsPlugin` should @@ -101,3 +103,5 @@ in **beets v3.0.0**. - :py:mod:`beets.autotag` - :py:mod:`beets.metadata_plugins` + - :ref:`autotagger_extensions` + - :ref:`using-the-auto-tagger` diff --git a/docs/dev/plugins/index.rst b/docs/dev/plugins/index.rst index 6f284b90b..018dce7e4 100644 --- a/docs/dev/plugins/index.rst +++ b/docs/dev/plugins/index.rst @@ -94,7 +94,7 @@ For more information on writing plugins, feel free to check out the following resources: .. toctree:: - :maxdepth: 2 + :maxdepth: 3 :includehidden: commands diff --git a/docs/guides/tagger.rst b/docs/guides/tagger.rst index dea1713f3..3ad85ec85 100644 --- a/docs/guides/tagger.rst +++ b/docs/guides/tagger.rst @@ -1,3 +1,5 @@ +.. _using-the-auto-tagger: + Using the Auto-Tagger ===================== diff --git a/poetry.lock b/poetry.lock index 3383129ff..8c109f930 100644 --- a/poetry.lock +++ b/poetry.lock @@ -696,13 +696,13 @@ files = [ [[package]] name = "docstrfmt" -version = "1.11.0" +version = "1.11.1" description = "docstrfmt: A formatter for Sphinx flavored reStructuredText." optional = false python-versions = ">=3.9" files = [ - {file = "docstrfmt-1.11.0-py3-none-any.whl", hash = "sha256:3d56bdd6e083091a8c5d7db098684f281de84667f4b7d4cc806092a63efc4844"}, - {file = "docstrfmt-1.11.0.tar.gz", hash = "sha256:37500c8086770294f265187c375c5c35a91a334d2c0b4f764aeace069d4ed501"}, + {file = "docstrfmt-1.11.1-py3-none-any.whl", hash = "sha256:6782d8663321c3a7c40be08a36fbcb1ea9e46d1efba85411ba807d97f384871a"}, + {file = "docstrfmt-1.11.1.tar.gz", hash = "sha256:d41e19d6c5d524cc7f8ff6cbfecb8762d77e696b9fe4f5057269051fb966fc80"}, ] [package.dependencies] @@ -711,6 +711,7 @@ click = ">=8" docutils = ">=0.20" libcst = ">=1" platformdirs = ">=4" +roman = "*" sphinx = ">=7" tabulate = ">=0.9" toml = {version = ">=0.10", markers = "python_version < \"3.11\""} @@ -2921,6 +2922,17 @@ urllib3 = ">=1.25.10,<3.0" [package.extras] tests = ["coverage (>=6.0.0)", "flake8", "mypy", "pytest (>=7.0.0)", "pytest-asyncio", "pytest-cov", "pytest-httpserver", "tomli", "tomli-w", "types-PyYAML", "types-requests"] +[[package]] +name = "roman" +version = "5.1" +description = "Integer to Roman numerals converter" +optional = false +python-versions = ">=3.9" +files = [ + {file = "roman-5.1-py3-none-any.whl", hash = "sha256:bf595d8a9bc4a8e8b1dfa23e1d4def0251b03b494786df6b8c3d3f1635ce285a"}, + {file = "roman-5.1.tar.gz", hash = "sha256:3a86572e9bc9183e771769601189e5fa32f1620ffeceebb9eca836affb409986"}, +] + [[package]] name = "ruff" version = "0.12.3" @@ -3617,4 +3629,4 @@ web = ["flask", "flask-cors"] [metadata] lock-version = "2.0" python-versions = ">=3.9,<4" -content-hash = "b68f663437ec41a92319456277243438a5c5f5c5f3dca113df1d7cac1b78703a" +content-hash = "faea27878ce1ca3f1335fd83e027b289351c51c73550bda72bf501a9c82166f7" diff --git a/pyproject.toml b/pyproject.toml index 2377f52f2..3cf3b9b47 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -100,7 +100,7 @@ requests_oauthlib = "*" responses = ">=0.3.0" [tool.poetry.group.lint.dependencies] -docstrfmt = ">=1.11.0" +docstrfmt = ">=1.11.1" ruff = ">=0.6.4" sphinx-lint = ">=1.0.0" From 33f1a5d0bef8ca08be79ee7a0d02a018d502680d Mon Sep 17 00:00:00 2001 From: J0J0 Todos Date: Sun, 31 Aug 2025 07:50:02 +0200 Subject: [PATCH 16/26] docs: Split Further Reading chapter to files to make it viewable in the primary sidebar instead of the secondary. --- docs/dev/plugins/index.rst | 2 +- docs/dev/plugins/other.rst | 368 --------------------------- docs/dev/plugins/other/config.rst | 36 +++ docs/dev/plugins/other/fields.rst | 35 +++ docs/dev/plugins/other/import.rst | 88 +++++++ docs/dev/plugins/other/index.rst | 16 ++ docs/dev/plugins/other/logging.rst | 38 +++ docs/dev/plugins/other/mediafile.rst | 32 +++ docs/dev/plugins/other/prompts.rst | 69 +++++ docs/dev/plugins/other/templates.rst | 57 +++++ 10 files changed, 372 insertions(+), 369 deletions(-) create mode 100644 docs/dev/plugins/other/config.rst create mode 100644 docs/dev/plugins/other/fields.rst create mode 100644 docs/dev/plugins/other/import.rst create mode 100644 docs/dev/plugins/other/index.rst create mode 100644 docs/dev/plugins/other/logging.rst create mode 100644 docs/dev/plugins/other/mediafile.rst create mode 100644 docs/dev/plugins/other/prompts.rst create mode 100644 docs/dev/plugins/other/templates.rst diff --git a/docs/dev/plugins/index.rst b/docs/dev/plugins/index.rst index 018dce7e4..d258e7df6 100644 --- a/docs/dev/plugins/index.rst +++ b/docs/dev/plugins/index.rst @@ -100,4 +100,4 @@ resources: commands events autotagger - other + other/index diff --git a/docs/dev/plugins/other.rst b/docs/dev/plugins/other.rst index 42fa9e768..e69de29bb 100644 --- a/docs/dev/plugins/other.rst +++ b/docs/dev/plugins/other.rst @@ -1,368 +0,0 @@ -Further Reading -=============== - -.. contents:: Table of Contents - :local: - :depth: 2 - -Read Configuration Options --------------------------- - -Plugins can configure themselves using the ``config.yaml`` file. You can read -configuration values in two ways. The first is to use `self.config` within your -plugin class. This gives you a view onto the configuration values in a section -with the same name as your plugin's module. For example, if your plugin is in -``greatplugin.py``, then `self.config` will refer to options under the -``greatplugin:`` section of the config file. - -For example, if you have a configuration value called "foo", then users can put -this in their ``config.yaml``: - -:: - - greatplugin: - foo: bar - -To access this value, say ``self.config['foo'].get()`` at any point in your -plugin's code. The `self.config` object is a *view* as defined by the Confuse_ -library. - -.. _confuse: https://confuse.readthedocs.io/en/latest/ - -If you want to access configuration values *outside* of your plugin's section, -import the `config` object from the `beets` module. That is, just put ``from -beets import config`` at the top of your plugin and access values from there. - -If your plugin provides configuration values for sensitive data (e.g., -passwords, API keys, ...), you should add these to the config so they can be -redacted automatically when users dump their config. This can be done by setting -each value's `redact` flag, like so: - -:: - - self.config['password'].redact = True - -Add Path Format Functions and Fields ------------------------------------- - -Beets supports *function calls* in its path format syntax (see -:doc:`/reference/pathformat`). Beets includes a few built-in functions, but -plugins can register new functions by adding them to the ``template_funcs`` -dictionary. - -Here's an example: - -.. code-block:: python - - class MyPlugin(BeetsPlugin): - def __init__(self): - super().__init__() - self.template_funcs["initial"] = _tmpl_initial - - - def _tmpl_initial(text: str) -> str: - if text: - return text[0].upper() - else: - return "" - -This plugin provides a function ``%initial`` to path templates where -``%initial{$artist}`` expands to the artist's initial (its capitalized first -character). - -Plugins can also add template *fields*, which are computed values referenced as -``$name`` in templates. To add a new field, add a function that takes an -``Item`` object to the ``template_fields`` dictionary on the plugin object. -Here's an example that adds a ``$disc_and_track`` field: - -.. code-block:: python - - class MyPlugin(BeetsPlugin): - def __init__(self): - super().__init__() - self.template_fields["disc_and_track"] = _tmpl_disc_and_track - - - def _tmpl_disc_and_track(item: Item) -> str: - """Expand to the disc number and track number if this is a - multi-disc release. Otherwise, just expands to the track - number. - """ - if item.disctotal > 1: - return f"{item.disc:02d}.{item.track:02d}" - else: - return f"{item.track:02d}" - -With this plugin enabled, templates can reference ``$disc_and_track`` as they -can any standard metadata field. - -This field works for *item* templates. Similarly, you can register *album* -template fields by adding a function accepting an ``Album`` argument to the -``album_template_fields`` dict. - -Extend MediaFile ----------------- - -MediaFile_ is the file tag abstraction layer that beets uses to make -cross-format metadata manipulation simple. Plugins can add fields to MediaFile -to extend the kinds of metadata that they can easily manage. - -The ``MediaFile`` class uses ``MediaField`` descriptors to provide access to -file tags. If you have created a descriptor you can add it through your plugins -:py:meth:`beets.plugins.BeetsPlugin.add_media_field` method. - -.. _mediafile: https://mediafile.readthedocs.io/en/latest/ - -Here's an example plugin that provides a meaningless new field "foo": - -.. code-block:: python - - class FooPlugin(BeetsPlugin): - def __init__(self): - field = mediafile.MediaField( - mediafile.MP3DescStorageStyle("foo"), mediafile.StorageStyle("foo") - ) - self.add_media_field("foo", field) - - - FooPlugin() - item = Item.from_path("/path/to/foo/tag.mp3") - assert item["foo"] == "spam" - - item["foo"] == "ham" - item.write() - # The "foo" tag of the file is now "ham" - -.. _plugin-stage: - -Add Import Pipeline Stages --------------------------- - -Many plugins need to add high-latency operations to the import workflow. For -example, a plugin that fetches lyrics from the Web would, ideally, not block the -progress of the rest of the importer. Beets allows plugins to add stages to the -parallel import pipeline. - -Each stage is run in its own thread. Plugin stages run after metadata changes -have been applied to a unit of music (album or track) and before file -manipulation has occurred (copying and moving files, writing tags to disk). -Multiple stages run in parallel but each stage processes only one task at a time -and each task is processed by only one stage at a time. - -Plugins provide stages as functions that take two arguments: ``config`` and -``task``, which are ``ImportSession`` and ``ImportTask`` objects (both defined -in ``beets.importer``). Add such a function to the plugin's ``import_stages`` -field to register it: - -.. code-block:: python - - from beets.importer import ImportSession, ImportTask - from beets.plugins import BeetsPlugin - - - class ExamplePlugin(BeetsPlugin): - - def __init__(self): - super().__init__() - self.import_stages = [self.stage] - - def stage(self, session: ImportSession, task: ImportTask): - print("Importing something!") - -It is also possible to request your function to run early in the pipeline by -adding the function to the plugin's ``early_import_stages`` field instead: - -.. code-block:: python - - self.early_import_stages = [self.stage] - -.. _extend-query: - -Extend the Query Syntax -~~~~~~~~~~~~~~~~~~~~~~~ - -You can add new kinds of queries to beets' :doc:`query syntax -`. There are two ways to add custom queries: using a prefix -and using a name. Prefix-based query extension can apply to *any* field, while -named queries are not associated with any field. For example, beets already -supports regular expression queries, which are indicated by a colon -prefix---plugins can do the same. - -For either kind of query extension, define a subclass of the ``Query`` type from -the ``beets.dbcore.query`` module. Then: - -- To define a prefix-based query, define a ``queries`` method in your plugin - class. Return from this method a dictionary mapping prefix strings to query - classes. -- To define a named query, defined dictionaries named either ``item_queries`` or - ``album_queries``. These should map names to query types. So if you use ``{ - "foo": FooQuery }``, then the query ``foo:bar`` will construct a query like - ``FooQuery("bar")``. - -For prefix-based queries, you will want to extend ``FieldQuery``, which -implements string comparisons on fields. To use it, create a subclass inheriting -from that class and override the ``value_match`` class method. (Remember the -``@classmethod`` decorator!) The following example plugin declares a query using -the ``@`` prefix to delimit exact string matches. The plugin will be used if we -issue a command like ``beet ls @something`` or ``beet ls artist:@something``: - -.. code-block:: python - - from beets.plugins import BeetsPlugin - from beets.dbcore import FieldQuery - - - class ExactMatchQuery(FieldQuery): - @classmethod - def value_match(self, pattern, val): - return pattern == val - - - class ExactMatchPlugin(BeetsPlugin): - def queries(self): - return {"@": ExactMatchQuery} - -Flexible Field Types --------------------- - -If your plugin uses flexible fields to store numbers or other non-string values, -you can specify the types of those fields. A rating plugin, for example, might -want to declare that the ``rating`` field should have an integer type: - -.. code-block:: python - - from beets.plugins import BeetsPlugin - from beets.dbcore import types - - - class RatingPlugin(BeetsPlugin): - item_types = {"rating": types.INTEGER} - - @property - def album_types(self): - return {"rating": types.INTEGER} - -A plugin may define two attributes: ``item_types`` and ``album_types``. Each of -those attributes is a dictionary mapping a flexible field name to a type -instance. You can find the built-in types in the ``beets.dbcore.types`` and -``beets.library`` modules or implement your own type by inheriting from the -``Type`` class. - -Specifying types has several advantages: - -- Code that accesses the field like ``item['my_field']`` gets the right type - (instead of just a string). -- You can use advanced queries (like :ref:`ranges `) from the - command line. -- User input for flexible fields may be validated and converted. -- Items missing the given field can use an appropriate null value for querying - and sorting purposes. - -.. _plugin-logging: - -Logging -------- - -Each plugin object has a ``_log`` attribute, which is a ``Logger`` from the -`standard Python logging module`_. The logger is set up to `PEP 3101`_, -str.format-style string formatting. So you can write logging calls like this: - -.. code-block:: python - - self._log.debug("Processing {0.title} by {0.artist}", item) - -.. _pep 3101: https://www.python.org/dev/peps/pep-3101/ - -.. _standard python logging module: https://docs.python.org/2/library/logging.html - -When beets is in verbose mode, plugin messages are prefixed with the plugin name -to make them easier to see. - -Which messages will be logged depends on the logging level and the action -performed: - -- Inside import stages and event handlers, the default is ``WARNING`` messages - and above. -- Everywhere else, the default is ``INFO`` or above. - -The verbosity can be increased with ``--verbose`` (``-v``) flags: each flags -lowers the level by a notch. That means that, with a single ``-v`` flag, event -handlers won't have their ``DEBUG`` messages displayed, but command functions -(for example) will. With ``-vv`` on the command line, ``DEBUG`` messages will be -displayed everywhere. - -This addresses a common pattern where plugins need to use the same code for a -command and an import stage, but the command needs to print more messages than -the import stage. (For example, you'll want to log "found lyrics for this song" -when you're run explicitly as a command, but you don't want to noisily interrupt -the importer interface when running automatically.) - -.. _append_prompt_choices: - -Append Prompt Choices ---------------------- - -Plugins can also append choices to the prompt presented to the user during an -import session. - -To do so, add a listener for the ``before_choose_candidate`` event, and return a -list of ``PromptChoices`` that represent the additional choices that your plugin -shall expose to the user: - -.. code-block:: python - - from beets.plugins import BeetsPlugin - from beets.ui.commands import PromptChoice - - - class ExamplePlugin(BeetsPlugin): - def __init__(self): - super().__init__() - self.register_listener( - "before_choose_candidate", self.before_choose_candidate_event - ) - - def before_choose_candidate_event(self, session, task): - return [ - PromptChoice("p", "Print foo", self.foo), - PromptChoice("d", "Do bar", self.bar), - ] - - def foo(self, session, task): - print('User has chosen "Print foo"!') - - def bar(self, session, task): - print('User has chosen "Do bar"!') - -The previous example modifies the standard prompt: - -.. code-block:: shell - - # selection (default 1), Skip, Use as-is, as Tracks, Group albums, - Enter search, enter Id, aBort? - -by appending two additional options (``Print foo`` and ``Do bar``): - -.. code-block:: shell - - # selection (default 1), Skip, Use as-is, as Tracks, Group albums, - Enter search, enter Id, aBort, Print foo, Do bar? - -If the user selects a choice, the ``callback`` attribute of the corresponding -``PromptChoice`` will be called. It is the responsibility of the plugin to check -for the status of the import session and decide the choices to be appended: for -example, if a particular choice should only be presented if the album has no -candidates, the relevant checks against ``task.candidates`` should be performed -inside the plugin's ``before_choose_candidate_event`` accordingly. - -Please make sure that the short letter for each of the choices provided by the -plugin is not already in use: the importer will emit a warning and discard all -but one of the choices using the same letter, giving priority to the core -importer prompt choices. As a reference, the following characters are used by -the choices on the core importer prompt, and hence should not be used: ``a``, -``s``, ``u``, ``t``, ``g``, ``e``, ``i``, ``b``. - -Additionally, the callback function can optionally specify the next action to be -performed by returning a ``importer.Action`` value. It may also return a -``autotag.Proposal`` value to update the set of current proposals to be -considered. diff --git a/docs/dev/plugins/other/config.rst b/docs/dev/plugins/other/config.rst new file mode 100644 index 000000000..043d4b28a --- /dev/null +++ b/docs/dev/plugins/other/config.rst @@ -0,0 +1,36 @@ +Read Configuration Options +-------------------------- + +Plugins can configure themselves using the ``config.yaml`` file. You can read +configuration values in two ways. The first is to use `self.config` within your +plugin class. This gives you a view onto the configuration values in a section +with the same name as your plugin's module. For example, if your plugin is in +``greatplugin.py``, then `self.config` will refer to options under the +``greatplugin:`` section of the config file. + +For example, if you have a configuration value called "foo", then users can put +this in their ``config.yaml``: + +:: + + greatplugin: + foo: bar + +To access this value, say ``self.config['foo'].get()`` at any point in your +plugin's code. The `self.config` object is a *view* as defined by the Confuse_ +library. + +.. _confuse: https://confuse.readthedocs.io/en/latest/ + +If you want to access configuration values *outside* of your plugin's section, +import the `config` object from the `beets` module. That is, just put ``from +beets import config`` at the top of your plugin and access values from there. + +If your plugin provides configuration values for sensitive data (e.g., +passwords, API keys, ...), you should add these to the config so they can be +redacted automatically when users dump their config. This can be done by setting +each value's `redact` flag, like so: + +:: + + self.config['password'].redact = True diff --git a/docs/dev/plugins/other/fields.rst b/docs/dev/plugins/other/fields.rst new file mode 100644 index 000000000..429b726dc --- /dev/null +++ b/docs/dev/plugins/other/fields.rst @@ -0,0 +1,35 @@ +Flexible Field Types +-------------------- + +If your plugin uses flexible fields to store numbers or other non-string values, +you can specify the types of those fields. A rating plugin, for example, might +want to declare that the ``rating`` field should have an integer type: + +.. code-block:: python + + from beets.plugins import BeetsPlugin + from beets.dbcore import types + + + class RatingPlugin(BeetsPlugin): + item_types = {"rating": types.INTEGER} + + @property + def album_types(self): + return {"rating": types.INTEGER} + +A plugin may define two attributes: ``item_types`` and ``album_types``. Each of +those attributes is a dictionary mapping a flexible field name to a type +instance. You can find the built-in types in the ``beets.dbcore.types`` and +``beets.library`` modules or implement your own type by inheriting from the +``Type`` class. + +Specifying types has several advantages: + +- Code that accesses the field like ``item['my_field']`` gets the right type + (instead of just a string). +- You can use advanced queries (like :ref:`ranges `) from the + command line. +- User input for flexible fields may be validated and converted. +- Items missing the given field can use an appropriate null value for querying + and sorting purposes. diff --git a/docs/dev/plugins/other/import.rst b/docs/dev/plugins/other/import.rst new file mode 100644 index 000000000..77d961522 --- /dev/null +++ b/docs/dev/plugins/other/import.rst @@ -0,0 +1,88 @@ +.. _plugin-stage: + +Add Import Pipeline Stages +-------------------------- + +Many plugins need to add high-latency operations to the import workflow. For +example, a plugin that fetches lyrics from the Web would, ideally, not block the +progress of the rest of the importer. Beets allows plugins to add stages to the +parallel import pipeline. + +Each stage is run in its own thread. Plugin stages run after metadata changes +have been applied to a unit of music (album or track) and before file +manipulation has occurred (copying and moving files, writing tags to disk). +Multiple stages run in parallel but each stage processes only one task at a time +and each task is processed by only one stage at a time. + +Plugins provide stages as functions that take two arguments: ``config`` and +``task``, which are ``ImportSession`` and ``ImportTask`` objects (both defined +in ``beets.importer``). Add such a function to the plugin's ``import_stages`` +field to register it: + +.. code-block:: python + + from beets.importer import ImportSession, ImportTask + from beets.plugins import BeetsPlugin + + + class ExamplePlugin(BeetsPlugin): + + def __init__(self): + super().__init__() + self.import_stages = [self.stage] + + def stage(self, session: ImportSession, task: ImportTask): + print("Importing something!") + +It is also possible to request your function to run early in the pipeline by +adding the function to the plugin's ``early_import_stages`` field instead: + +.. code-block:: python + + self.early_import_stages = [self.stage] + +.. _extend-query: + +Extend the Query Syntax +~~~~~~~~~~~~~~~~~~~~~~~ + +You can add new kinds of queries to beets' :doc:`query syntax +`. There are two ways to add custom queries: using a prefix +and using a name. Prefix-based query extension can apply to *any* field, while +named queries are not associated with any field. For example, beets already +supports regular expression queries, which are indicated by a colon +prefix---plugins can do the same. + +For either kind of query extension, define a subclass of the ``Query`` type from +the ``beets.dbcore.query`` module. Then: + +- To define a prefix-based query, define a ``queries`` method in your plugin + class. Return from this method a dictionary mapping prefix strings to query + classes. +- To define a named query, defined dictionaries named either ``item_queries`` or + ``album_queries``. These should map names to query types. So if you use ``{ + "foo": FooQuery }``, then the query ``foo:bar`` will construct a query like + ``FooQuery("bar")``. + +For prefix-based queries, you will want to extend ``FieldQuery``, which +implements string comparisons on fields. To use it, create a subclass inheriting +from that class and override the ``value_match`` class method. (Remember the +``@classmethod`` decorator!) The following example plugin declares a query using +the ``@`` prefix to delimit exact string matches. The plugin will be used if we +issue a command like ``beet ls @something`` or ``beet ls artist:@something``: + +.. code-block:: python + + from beets.plugins import BeetsPlugin + from beets.dbcore import FieldQuery + + + class ExactMatchQuery(FieldQuery): + @classmethod + def value_match(self, pattern, val): + return pattern == val + + + class ExactMatchPlugin(BeetsPlugin): + def queries(self): + return {"@": ExactMatchQuery} diff --git a/docs/dev/plugins/other/index.rst b/docs/dev/plugins/other/index.rst new file mode 100644 index 000000000..595139042 --- /dev/null +++ b/docs/dev/plugins/other/index.rst @@ -0,0 +1,16 @@ +Further Reading +=============== + +For more information on writing plugins, feel free to check out the following +resources: + +.. toctree:: + :maxdepth: 2 + + config + templates + mediafile + import + fields + logging + prompts diff --git a/docs/dev/plugins/other/logging.rst b/docs/dev/plugins/other/logging.rst new file mode 100644 index 000000000..cae088f50 --- /dev/null +++ b/docs/dev/plugins/other/logging.rst @@ -0,0 +1,38 @@ +.. _plugin-logging: + +Logging +------- + +Each plugin object has a ``_log`` attribute, which is a ``Logger`` from the +`standard Python logging module`_. The logger is set up to `PEP 3101`_, +str.format-style string formatting. So you can write logging calls like this: + +.. code-block:: python + + self._log.debug("Processing {0.title} by {0.artist}", item) + +.. _pep 3101: https://www.python.org/dev/peps/pep-3101/ + +.. _standard python logging module: https://docs.python.org/2/library/logging.html + +When beets is in verbose mode, plugin messages are prefixed with the plugin name +to make them easier to see. + +Which messages will be logged depends on the logging level and the action +performed: + +- Inside import stages and event handlers, the default is ``WARNING`` messages + and above. +- Everywhere else, the default is ``INFO`` or above. + +The verbosity can be increased with ``--verbose`` (``-v``) flags: each flags +lowers the level by a notch. That means that, with a single ``-v`` flag, event +handlers won't have their ``DEBUG`` messages displayed, but command functions +(for example) will. With ``-vv`` on the command line, ``DEBUG`` messages will be +displayed everywhere. + +This addresses a common pattern where plugins need to use the same code for a +command and an import stage, but the command needs to print more messages than +the import stage. (For example, you'll want to log "found lyrics for this song" +when you're run explicitly as a command, but you don't want to noisily interrupt +the importer interface when running automatically.) diff --git a/docs/dev/plugins/other/mediafile.rst b/docs/dev/plugins/other/mediafile.rst new file mode 100644 index 000000000..467fd34ea --- /dev/null +++ b/docs/dev/plugins/other/mediafile.rst @@ -0,0 +1,32 @@ +Extend MediaFile +---------------- + +MediaFile_ is the file tag abstraction layer that beets uses to make +cross-format metadata manipulation simple. Plugins can add fields to MediaFile +to extend the kinds of metadata that they can easily manage. + +The ``MediaFile`` class uses ``MediaField`` descriptors to provide access to +file tags. If you have created a descriptor you can add it through your plugins +:py:meth:`beets.plugins.BeetsPlugin.add_media_field` method. + +.. _mediafile: https://mediafile.readthedocs.io/en/latest/ + +Here's an example plugin that provides a meaningless new field "foo": + +.. code-block:: python + + class FooPlugin(BeetsPlugin): + def __init__(self): + field = mediafile.MediaField( + mediafile.MP3DescStorageStyle("foo"), mediafile.StorageStyle("foo") + ) + self.add_media_field("foo", field) + + + FooPlugin() + item = Item.from_path("/path/to/foo/tag.mp3") + assert item["foo"] == "spam" + + item["foo"] == "ham" + item.write() + # The "foo" tag of the file is now "ham" diff --git a/docs/dev/plugins/other/prompts.rst b/docs/dev/plugins/other/prompts.rst new file mode 100644 index 000000000..8916a3a03 --- /dev/null +++ b/docs/dev/plugins/other/prompts.rst @@ -0,0 +1,69 @@ +.. _append_prompt_choices: + +Append Prompt Choices +--------------------- + +Plugins can also append choices to the prompt presented to the user during an +import session. + +To do so, add a listener for the ``before_choose_candidate`` event, and return a +list of ``PromptChoices`` that represent the additional choices that your plugin +shall expose to the user: + +.. code-block:: python + + from beets.plugins import BeetsPlugin + from beets.ui.commands import PromptChoice + + + class ExamplePlugin(BeetsPlugin): + def __init__(self): + super().__init__() + self.register_listener( + "before_choose_candidate", self.before_choose_candidate_event + ) + + def before_choose_candidate_event(self, session, task): + return [ + PromptChoice("p", "Print foo", self.foo), + PromptChoice("d", "Do bar", self.bar), + ] + + def foo(self, session, task): + print('User has chosen "Print foo"!') + + def bar(self, session, task): + print('User has chosen "Do bar"!') + +The previous example modifies the standard prompt: + +.. code-block:: shell + + # selection (default 1), Skip, Use as-is, as Tracks, Group albums, + Enter search, enter Id, aBort? + +by appending two additional options (``Print foo`` and ``Do bar``): + +.. code-block:: shell + + # selection (default 1), Skip, Use as-is, as Tracks, Group albums, + Enter search, enter Id, aBort, Print foo, Do bar? + +If the user selects a choice, the ``callback`` attribute of the corresponding +``PromptChoice`` will be called. It is the responsibility of the plugin to check +for the status of the import session and decide the choices to be appended: for +example, if a particular choice should only be presented if the album has no +candidates, the relevant checks against ``task.candidates`` should be performed +inside the plugin's ``before_choose_candidate_event`` accordingly. + +Please make sure that the short letter for each of the choices provided by the +plugin is not already in use: the importer will emit a warning and discard all +but one of the choices using the same letter, giving priority to the core +importer prompt choices. As a reference, the following characters are used by +the choices on the core importer prompt, and hence should not be used: ``a``, +``s``, ``u``, ``t``, ``g``, ``e``, ``i``, ``b``. + +Additionally, the callback function can optionally specify the next action to be +performed by returning a ``importer.Action`` value. It may also return a +``autotag.Proposal`` value to update the set of current proposals to be +considered. diff --git a/docs/dev/plugins/other/templates.rst b/docs/dev/plugins/other/templates.rst new file mode 100644 index 000000000..21a592603 --- /dev/null +++ b/docs/dev/plugins/other/templates.rst @@ -0,0 +1,57 @@ +Add Path Format Functions and Fields +------------------------------------ + +Beets supports *function calls* in its path format syntax (see +:doc:`/reference/pathformat`). Beets includes a few built-in functions, but +plugins can register new functions by adding them to the ``template_funcs`` +dictionary. + +Here's an example: + +.. code-block:: python + + class MyPlugin(BeetsPlugin): + def __init__(self): + super().__init__() + self.template_funcs["initial"] = _tmpl_initial + + + def _tmpl_initial(text: str) -> str: + if text: + return text[0].upper() + else: + return "" + +This plugin provides a function ``%initial`` to path templates where +``%initial{$artist}`` expands to the artist's initial (its capitalized first +character). + +Plugins can also add template *fields*, which are computed values referenced as +``$name`` in templates. To add a new field, add a function that takes an +``Item`` object to the ``template_fields`` dictionary on the plugin object. +Here's an example that adds a ``$disc_and_track`` field: + +.. code-block:: python + + class MyPlugin(BeetsPlugin): + def __init__(self): + super().__init__() + self.template_fields["disc_and_track"] = _tmpl_disc_and_track + + + def _tmpl_disc_and_track(item: Item) -> str: + """Expand to the disc number and track number if this is a + multi-disc release. Otherwise, just expands to the track + number. + """ + if item.disctotal > 1: + return "%02i.%02i" % (item.disc, item.track) + else: + return "%02i" % (item.track) + +With this plugin enabled, templates can reference ``$disc_and_track`` as they +can any standard metadata field. + +This field works for *item* templates. Similarly, you can register *album* +template fields by adding a function accepting an ``Album`` argument to the +``album_template_fields`` dict. From fddda507ea25dca2cecd6df23b78b7cf361942b4 Mon Sep 17 00:00:00 2001 From: J0J0 Todos Date: Sun, 31 Aug 2025 07:36:03 +0200 Subject: [PATCH 17/26] docs: Reveal 3 nav levels in primary sidebar and also allow a max level of 3 to expand. --- docs/conf.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/docs/conf.py b/docs/conf.py index d0f8cdffe..840c55a3b 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -81,7 +81,12 @@ man_pages = [ html_theme = "pydata_sphinx_theme" -html_theme_options = {"collapse_navigation": True, "logo": {"text": "beets"}} +html_theme_options = { + "collapse_navigation": False, + "logo": {"text": "beets"}, + "show_nav_level": 3, # How many levels in left sidebar to show automatically + "navigation_depth": 4, # How many levels of navigation to expand +} html_title = "beets" html_logo = "_static/beets_logo_nobg.png" html_static_path = ["_static"] From e0d16c20f426b67ff3b43c5a6fbf666149a17349 Mon Sep 17 00:00:00 2001 From: J0J0 Todos Date: Sun, 31 Aug 2025 17:02:38 +0200 Subject: [PATCH 18/26] Add another docs file move to git blame ignore revs --- .git-blame-ignore-revs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs index 75e49f8cc..ebe27088c 100644 --- a/.git-blame-ignore-revs +++ b/.git-blame-ignore-revs @@ -59,6 +59,8 @@ c490ac5810b70f3cf5fd8649669838e8fdb19f4d ab5acaabb3cd24c482adb7fa4800c89fd6a2f08d # Moved dev docs 2504595532abd7584143007ede087ee4abc00916 +# Moved plugin docs Further Reading chapter +c8cb3813e38fe1381509c39e415c3a5fe0deb808 # Replace format calls with f-strings 4a361bd501e85de12c91c2474c423559ca672852 # Replace percent formatting From 09da8a29914c4d56e3d3c1d627a92ccc6be5b98d Mon Sep 17 00:00:00 2001 From: Sebastian Mohr Date: Tue, 2 Sep 2025 09:24:17 +0200 Subject: [PATCH 19/26] - Updated git blame ignore with new hashes - run docstrfmt --- .git-blame-ignore-revs | 8 ++++---- docs/dev/plugins/other.rst | 0 docs/dev/plugins/other/config.rst | 2 +- docs/dev/plugins/other/fields.rst | 2 +- docs/dev/plugins/other/import.rst | 4 ++-- docs/dev/plugins/other/logging.rst | 2 +- docs/dev/plugins/other/mediafile.rst | 2 +- docs/dev/plugins/other/prompts.rst | 2 +- docs/dev/plugins/other/templates.rst | 2 +- 9 files changed, 12 insertions(+), 12 deletions(-) delete mode 100644 docs/dev/plugins/other.rst diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs index ebe27088c..54cb86242 100644 --- a/.git-blame-ignore-revs +++ b/.git-blame-ignore-revs @@ -57,10 +57,6 @@ c490ac5810b70f3cf5fd8649669838e8fdb19f4d 769dcdc88a1263638ae25944ba6b2be3e8933666 # Reformat all docs using docstrfmt ab5acaabb3cd24c482adb7fa4800c89fd6a2f08d -# Moved dev docs -2504595532abd7584143007ede087ee4abc00916 -# Moved plugin docs Further Reading chapter -c8cb3813e38fe1381509c39e415c3a5fe0deb808 # Replace format calls with f-strings 4a361bd501e85de12c91c2474c423559ca672852 # Replace percent formatting @@ -71,3 +67,7 @@ c8cb3813e38fe1381509c39e415c3a5fe0deb808 2fccf64efe82851861e195b521b14680b480a42a # Do not use explicit indices for logging args when not needed d93ddf8dd43e4f9ed072a03829e287c78d2570a2 +# Moved dev docs +1f94bdbe4963c693847c24af18b151e12c670995 +# Moved plugin docs Further Reading chapter +18088c654665e84afc0a67173aa8056ca6b57a58 \ No newline at end of file diff --git a/docs/dev/plugins/other.rst b/docs/dev/plugins/other.rst deleted file mode 100644 index e69de29bb..000000000 diff --git a/docs/dev/plugins/other/config.rst b/docs/dev/plugins/other/config.rst index 043d4b28a..e5581fe63 100644 --- a/docs/dev/plugins/other/config.rst +++ b/docs/dev/plugins/other/config.rst @@ -1,5 +1,5 @@ Read Configuration Options --------------------------- +========================== Plugins can configure themselves using the ``config.yaml`` file. You can read configuration values in two ways. The first is to use `self.config` within your diff --git a/docs/dev/plugins/other/fields.rst b/docs/dev/plugins/other/fields.rst index 429b726dc..6ee570043 100644 --- a/docs/dev/plugins/other/fields.rst +++ b/docs/dev/plugins/other/fields.rst @@ -1,5 +1,5 @@ Flexible Field Types --------------------- +==================== If your plugin uses flexible fields to store numbers or other non-string values, you can specify the types of those fields. A rating plugin, for example, might diff --git a/docs/dev/plugins/other/import.rst b/docs/dev/plugins/other/import.rst index 77d961522..706a520b7 100644 --- a/docs/dev/plugins/other/import.rst +++ b/docs/dev/plugins/other/import.rst @@ -1,7 +1,7 @@ .. _plugin-stage: Add Import Pipeline Stages --------------------------- +========================== Many plugins need to add high-latency operations to the import workflow. For example, a plugin that fetches lyrics from the Web would, ideally, not block the @@ -44,7 +44,7 @@ adding the function to the plugin's ``early_import_stages`` field instead: .. _extend-query: Extend the Query Syntax -~~~~~~~~~~~~~~~~~~~~~~~ +----------------------- You can add new kinds of queries to beets' :doc:`query syntax `. There are two ways to add custom queries: using a prefix diff --git a/docs/dev/plugins/other/logging.rst b/docs/dev/plugins/other/logging.rst index cae088f50..1c4ce4838 100644 --- a/docs/dev/plugins/other/logging.rst +++ b/docs/dev/plugins/other/logging.rst @@ -1,7 +1,7 @@ .. _plugin-logging: Logging -------- +======= Each plugin object has a ``_log`` attribute, which is a ``Logger`` from the `standard Python logging module`_. The logger is set up to `PEP 3101`_, diff --git a/docs/dev/plugins/other/mediafile.rst b/docs/dev/plugins/other/mediafile.rst index 467fd34ea..8fa22ceca 100644 --- a/docs/dev/plugins/other/mediafile.rst +++ b/docs/dev/plugins/other/mediafile.rst @@ -1,5 +1,5 @@ Extend MediaFile ----------------- +================ MediaFile_ is the file tag abstraction layer that beets uses to make cross-format metadata manipulation simple. Plugins can add fields to MediaFile diff --git a/docs/dev/plugins/other/prompts.rst b/docs/dev/plugins/other/prompts.rst index 8916a3a03..f734f0de3 100644 --- a/docs/dev/plugins/other/prompts.rst +++ b/docs/dev/plugins/other/prompts.rst @@ -1,7 +1,7 @@ .. _append_prompt_choices: Append Prompt Choices ---------------------- +===================== Plugins can also append choices to the prompt presented to the user during an import session. diff --git a/docs/dev/plugins/other/templates.rst b/docs/dev/plugins/other/templates.rst index 21a592603..89509dcb7 100644 --- a/docs/dev/plugins/other/templates.rst +++ b/docs/dev/plugins/other/templates.rst @@ -1,5 +1,5 @@ Add Path Format Functions and Fields ------------------------------------- +==================================== Beets supports *function calls* in its path format syntax (see :doc:`/reference/pathformat`). Beets includes a few built-in functions, but From 67dd35596814985965b5fc65c652d8956a51e091 Mon Sep 17 00:00:00 2001 From: J0J0 Todos Date: Wed, 3 Sep 2025 07:14:17 +0200 Subject: [PATCH 20/26] docs: Ensure cleanup in poe docs shortcut otherwise Sphinx output is unpredictable! --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 3cf3b9b47..184325599 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -196,7 +196,7 @@ cmd = "mypy" [tool.poe.tasks.docs] help = "Build documentation" -cmd = "make -C docs html" +cmd = "make -C docs clean html" [tool.poe.tasks.format] help = "Format the codebase" From 33feb0348d68a974f3c39c6013cd2cb1d17dcdc0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0ar=C5=ABnas=20Nejus?= Date: Thu, 4 Sep 2025 10:23:52 +0100 Subject: [PATCH 21/26] Define replacements for commonly used classes --- docs/conf.py | 11 ++++++ docs/dev/library.rst | 68 ++++++++++++++++---------------- docs/dev/plugins/events.rst | 78 ++++++++++++++----------------------- 3 files changed, 73 insertions(+), 84 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 840c55a3b..1e53fe427 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -75,6 +75,17 @@ man_pages = [ ), ] +# Global substitutions that can be used anywhere in the documentation. +rst_epilog = """ +.. |Album| replace:: :class:`~beets.library.models.Album` +.. |AlbumInfo| replace:: :class:`beets.autotag.hooks.AlbumInfo` +.. |ImportSession| replace:: :class:`~beets.importer.session.ImportSession` +.. |ImportTask| replace:: :class:`~beets.importer.tasks.ImportTask` +.. |Item| replace:: :class:`~beets.library.models.Item` +.. |Library| replace:: :class:`~beets.library.library.Library` +.. |Model| replace:: :class:`~beets.dbcore.db.Model` +.. |TrackInfo| replace:: :class:`beets.autotag.hooks.TrackInfo` +""" # -- Options for HTML output ------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output diff --git a/docs/dev/library.rst b/docs/dev/library.rst index 0f7554aac..8b854937d 100644 --- a/docs/dev/library.rst +++ b/docs/dev/library.rst @@ -7,18 +7,18 @@ This page describes the internal API of beets' core database features. It doesn't exhaustively document the API, but is aimed at giving an overview of the architecture to orient anyone who wants to dive into the code. -The :class:`Library` object is the central repository for data in beets. It -represents a database containing songs, which are :class:`Item` instances, and -groups of items, which are :class:`Album` instances. +The |Library| object is the central repository for data in beets. It represents +a database containing songs, which are |Item| instances, and groups of items, +which are |Album| instances. The Library Class ----------------- -The :class:`Library` is typically instantiated as a singleton. A single -invocation of beets usually has only one :class:`Library`. It's powered by -:class:`dbcore.Database` under the hood, which handles the SQLite_ abstraction, -something like a very minimal ORM_. The library is also responsible for handling -queries to retrieve stored objects. +The |Library| is typically instantiated as a singleton. A single invocation of +beets usually has only one |Library|. It's powered by :class:`dbcore.Database` +under the hood, which handles the SQLite_ abstraction, something like a very +minimal ORM_. The library is also responsible for handling queries to retrieve +stored objects. Overview ~~~~~~~~ @@ -40,10 +40,9 @@ which you can get using the :py:meth:`Library.transaction` context manager. Model Classes ------------- -The two model entities in beets libraries, :class:`Item` and :class:`Album`, -share a base class, :class:`LibModel`, that provides common functionality. That -class itself specialises :class:`beets.dbcore.Model` which provides an ORM-like -abstraction. +The two model entities in beets libraries, |Item| and |Album|, share a base +class, :class:`LibModel`, that provides common functionality. That class itself +specialises :class:`beets.dbcore.Model` which provides an ORM-like abstraction. To get or change the metadata of a model (an item or album), either access its attributes (e.g., ``print(album.year)`` or ``album.year = 2012``) or use the @@ -56,8 +55,7 @@ Models use dirty-flags to track when the object's metadata goes out of sync with the database. The dirty dictionary maps field names to booleans indicating whether the field has been written since the object was last synchronized (via load or store) with the database. This logic is implemented in the model base -class :class:`LibModel` and is inherited by both :class:`Item` and -:class:`Album`. +class :class:`LibModel` and is inherited by both |Item| and |Album|. We provide CRUD-like methods for interacting with the database: @@ -77,10 +75,10 @@ normal the normal mapping API is supported: Item ~~~~ -Each :class:`Item` object represents a song or track. (We use the more generic -term item because, one day, beets might support non-music media.) An item can -either be purely abstract, in which case it's just a bag of metadata fields, or -it can have an associated file (indicated by ``item.path``). +Each |Item| object represents a song or track. (We use the more generic term +item because, one day, beets might support non-music media.) An item can either +be purely abstract, in which case it's just a bag of metadata fields, or it can +have an associated file (indicated by ``item.path``). In terms of the underlying SQLite database, items are backed by a single table called items with one column per metadata fields. The metadata fields currently @@ -97,12 +95,12 @@ become out of sync with on-disk metadata, mainly to speed up the :ref:`update-cmd` (which needs to check whether the database is in sync with the filesystem). This feature turns out to be sort of complicated. -For any :class:`Item`, there are two mtimes: the on-disk mtime (maintained by -the OS) and the database mtime (maintained by beets). Correspondingly, there is -on-disk metadata (ID3 tags, for example) and DB metadata. The goal with the -mtime is to ensure that the on-disk and DB mtimes match when the on-disk and DB -metadata are in sync; this lets beets do a quick mtime check and avoid rereading -files in some circumstances. +For any |Item|, there are two mtimes: the on-disk mtime (maintained by the OS) +and the database mtime (maintained by beets). Correspondingly, there is on-disk +metadata (ID3 tags, for example) and DB metadata. The goal with the mtime is to +ensure that the on-disk and DB mtimes match when the on-disk and DB metadata are +in sync; this lets beets do a quick mtime check and avoid rereading files in +some circumstances. Specifically, beets attempts to maintain the following invariant: @@ -126,14 +124,14 @@ This leads to the following implementation policy: Album ~~~~~ -An :class:`Album` is a collection of Items in the database. Every item in the -database has either zero or one associated albums (accessible via -``item.album_id``). An item that has no associated album is called a singleton. -Changing fields on an album (e.g. ``album.year = 2012``) updates the album -itself and also changes the same field in all associated items. +An |Album| is a collection of Items in the database. Every item in the database +has either zero or one associated albums (accessible via ``item.album_id``). An +item that has no associated album is called a singleton. Changing fields on an +album (e.g. ``album.year = 2012``) updates the album itself and also changes the +same field in all associated items. -An :class:`Album` object keeps track of album-level metadata, which is (mostly) -a subset of the track-level metadata. The album-level metadata fields are listed +An |Album| object keeps track of album-level metadata, which is (mostly) a +subset of the track-level metadata. The album-level metadata fields are listed in ``Album._fields``. For those fields that are both item-level and album-level (e.g., ``year`` or ``albumartist``), every item in an album should share the same value. Albums use an SQLite table called ``albums``, in which each column @@ -147,7 +145,7 @@ is an album metadata field. Transactions ~~~~~~~~~~~~ -The :class:`Library` class provides the basic methods necessary to access and +The |Library| class provides the basic methods necessary to access and manipulate its contents. To perform more complicated operations atomically, or to interact directly with the underlying SQLite database, you must use a *transaction* (see this `blog post`_ for motivation). For example @@ -181,8 +179,8 @@ matching items/albums. The ``clause()`` method should return an SQLite ``WHERE`` clause that matches appropriate albums/items. This allows for efficient batch queries. -Correspondingly, the ``match(item)`` method should take an :class:`Item` object -and return a boolean, indicating whether or not a specific item matches the +Correspondingly, the ``match(item)`` method should take an |Item| object and +return a boolean, indicating whether or not a specific item matches the criterion. This alternate implementation allows clients to determine whether items that have already been fetched from the database match the query. @@ -194,4 +192,4 @@ together, matching only albums/items that match all constituent queries. Beets has a human-writable plain-text query syntax that can be parsed into :class:`Query` objects. Calling ``AndQuery.from_strings`` parses a list of query -parts into a query object that can then be used with :class:`Library` objects. +parts into a query object that can then be used with |Library| objects. diff --git a/docs/dev/plugins/events.rst b/docs/dev/plugins/events.rst index 325b01b33..68773db3b 100644 --- a/docs/dev/plugins/events.rst +++ b/docs/dev/plugins/events.rst @@ -52,166 +52,146 @@ registration process in this case: command starts. ``import`` - :Parameters: :py:class:`lib `, ``paths`` (list of - path strings) + :Parameters: ``lib`` (|Library|), ``paths`` (list of path strings) :Description: Called after the ``import`` command finishes. ``album_imported`` - :Parameters: :py:class:`lib `, :py:class:`album - ` + :Parameters: ``lib`` (|Library|), ``album`` (|Album|) :Description: Called every time the importer finishes adding an album to the library. ``album_removed`` - :Parameters: :py:class:`lib `, :py:class:`album - ` + :Parameters: ``lib`` (|Library|), ``album`` (|Album|) :Description: Called every time an album is removed from the library (even when its files are not deleted from disk). ``item_copied`` - :Parameters: :py:class:`item `, ``source`` (path), - ``destination`` (path) + :Parameters: ``item`` (|Item|), ``source`` (path), ``destination`` (path) :Description: Called whenever an item file is copied. ``item_imported`` - :Parameters: :py:class:`lib `, :py:class:`item - ` + :Parameters: ``lib`` (|Library|), ``item`` (|Item|) :Description: Called every time the importer adds a singleton to the library (not called for full-album imports). ``before_item_imported`` - :Parameters: :py:class:`item `, ``source`` (path), - ``destination`` (path) + :Parameters: ``item`` (|Item|), ``source`` (path), ``destination`` (path) :Description: Called with an ``Item`` object immediately before it is imported. ``before_item_moved`` - :Parameters: :py:class:`item `, ``source`` (path), - ``destination`` (path) + :Parameters: ``item`` (|Item|), ``source`` (path), ``destination`` (path) :Description: Called with an ``Item`` object immediately before its file is moved. ``item_moved`` - :Parameters: :py:class:`item `, ``source`` (path), - ``destination`` (path) + :Parameters: ``item`` (|Item|), ``source`` (path), ``destination`` (path) :Description: Called with an ``Item`` object whenever its file is moved. ``item_linked`` - :Parameters: :py:class:`item `, ``source`` (path), - ``destination`` (path) + :Parameters: ``item`` (|Item|), ``source`` (path), ``destination`` (path) :Description: Called with an ``Item`` object whenever a symlink is created for a file. ``item_hardlinked`` - :Parameters: :py:class:`item `, ``source`` (path), - ``destination`` (path) + :Parameters: ``item`` (|Item|), ``source`` (path), ``destination`` (path) :Description: Called with an ``Item`` object whenever a hardlink is created for a file. ``item_reflinked`` - :Parameters: :py:class:`item `, ``source`` (path), - ``destination`` (path) + :Parameters: ``item`` (|Item|), ``source`` (path), ``destination`` (path) :Description: Called with an ``Item`` object whenever a reflink is created for a file. ``item_removed`` - :Parameters: :py:class:`item ` + :Parameters: ``item`` (|Item|) :Description: Called with an ``Item`` object every time an item (singleton or part of an album) is removed from the library (even when its file is not deleted from disk). ``write`` - :Parameters: :py:class:`item `, ``path`` (path), - ``tags`` (dict) + :Parameters: ``item`` (|Item|), ``path`` (path), ``tags`` (dict) :Description: Called just before a file's metadata is written to disk. Handlers may modify ``tags`` or raise ``library.FileOperationError`` to abort. ``after_write`` - :Parameters: :py:class:`item ` + :Parameters: ``item`` (|Item|) :Description: Called after a file's metadata is written to disk. ``import_task_created`` - :Parameters: :py:class:`task `, - :py:class:`session ` + :Parameters: ``task`` (|ImportTask|), ``session`` (|ImportSession|) :Description: Called immediately after an import task is initialized. May return a list (possibly empty) of replacement tasks. ``import_task_start`` - :Parameters: :py:class:`task `, - :py:class:`session ` + :Parameters: ``task`` (|ImportTask|), ``session`` (|ImportSession|) :Description: Called before an import task begins processing. ``import_task_apply`` - :Parameters: :py:class:`task `, - :py:class:`session ` + :Parameters: ``task`` (|ImportTask|), ``session`` (|ImportSession|) :Description: Called after metadata changes have been applied in an import task (on the UI thread; keep fast). Prefer a pipeline stage otherwise (see :ref:`plugin-stage`). ``import_task_before_choice`` - :Parameters: :py:class:`task `, - :py:class:`session ` + :Parameters: ``task`` (|ImportTask|), ``session`` (|ImportSession|) :Description: Called after candidate search and before deciding how to import. May return an importer action (only one handler may return non-None). ``import_task_choice`` - :Parameters: :py:class:`task `, - :py:class:`session ` + :Parameters: ``task`` (|ImportTask|), ``session`` (|ImportSession|) :Description: Called after a decision has been made about an import task. Use ``task.choice_flag`` to inspect or change the action. ``import_task_files`` - :Parameters: :py:class:`task `, - :py:class:`session ` + :Parameters: ``task`` (|ImportTask|), ``session`` (|ImportSession|) :Description: Called after filesystem manipulation (copy/move/write) for an import task. ``library_opened`` - :Parameters: :py:class:`lib ` + :Parameters: ``lib`` (|Library|) :Description: Called after beets starts and initializes the main Library object. ``database_change`` - :Parameters: :py:class:`lib `, :py:class:`model - ` + :Parameters: ``lib`` (|Library|), ``model`` (|Model|) :Description: A modification has been made to the library database (may not yet be committed). ``cli_exit`` - :Parameters: :py:class:`lib ` + :Parameters: ``lib`` (|Library|) :Description: Called just before the ``beet`` command-line program exits. ``import_begin`` - :Parameters: :py:class:`session ` + :Parameters: ``session`` (|ImportSession|) :Description: Called just before a ``beet import`` session starts. ``trackinfo_received`` - :Parameters: :py:class:`info ` + :Parameters: ``info`` (|TrackInfo|) :Description: Called after metadata for a track is fetched (e.g., from MusicBrainz). Handlers can modify the tags seen by later pipeline stages or adjustments (e.g., ``mbsync``). ``albuminfo_received`` - :Parameters: :py:class:`info ` + :Parameters: ``info`` (|AlbumInfo|) :Description: Like ``trackinfo_received`` but for album-level metadata. ``before_choose_candidate`` - :Parameters: :py:class:`task `, - :py:class:`session ` + :Parameters: ``task`` (|ImportTask|), ``session`` (|ImportSession|) :Description: Called before prompting the user during interactive import. May return a list of ``PromptChoices`` to append to the prompt (see :ref:`append_prompt_choices`). ``mb_track_extract`` - :Parameters: :py:class:`data ` + :Parameters: ``data`` (dict) :Description: Called after metadata is obtained from MusicBrainz for a track. Must return a (possibly empty) dict of additional ``field: value`` pairs to apply (overwriting existing fields). ``mb_album_extract`` - :Parameters: :py:class:`data ` + :Parameters: ``data`` (dict) :Description: Like ``mb_track_extract`` but for album tags. Overwrites tags set at the track level with the same field. From 1736a5e735ff681d246ed784599c4d3efb534d77 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0ar=C5=ABnas=20Nejus?= Date: Thu, 4 Sep 2025 11:47:05 +0100 Subject: [PATCH 22/26] Define MetadataSourcePlugin methods on the subclass only --- beets/metadata_plugins.py | 11 ----------- beets/plugins.py | 15 +++++++++++++++ 2 files changed, 15 insertions(+), 11 deletions(-) diff --git a/beets/metadata_plugins.py b/beets/metadata_plugins.py index 429a6e716..381881b51 100644 --- a/beets/metadata_plugins.py +++ b/beets/metadata_plugins.py @@ -8,7 +8,6 @@ implemented as plugins. from __future__ import annotations import abc -import inspect import re import warnings from typing import TYPE_CHECKING, Generic, Literal, Sequence, TypedDict, TypeVar @@ -421,13 +420,3 @@ class SearchApiMetadataSourcePlugin( query = unidecode.unidecode(query) return query - - -# Dynamically copy methods to BeetsPlugin for legacy support -# TODO: Remove this in the future major release, v3.0.0 - -for name, method in inspect.getmembers( - MetadataSourcePlugin, predicate=inspect.isfunction -): - if not hasattr(BeetsPlugin, name): - setattr(BeetsPlugin, name, method) diff --git a/beets/plugins.py b/beets/plugins.py index d9df4323c..d8d465183 100644 --- a/beets/plugins.py +++ b/beets/plugins.py @@ -158,6 +158,21 @@ class BeetsPlugin(metaclass=abc.ABCMeta): early_import_stages: list[ImportStageFunc] import_stages: list[ImportStageFunc] + def __init_subclass__(cls) -> None: + # Dynamically copy methods to BeetsPlugin for legacy support + # TODO: Remove this in the future major release, v3.0.0 + if inspect.isabstract(cls): + return + + from beets.metadata_plugins import MetadataSourcePlugin + + abstractmethods = MetadataSourcePlugin.__abstractmethods__ + for name, method in inspect.getmembers( + MetadataSourcePlugin, predicate=inspect.isfunction + ): + if name not in abstractmethods and not hasattr(cls, name): + setattr(cls, name, method) + def __init__(self, name: str | None = None): """Perform one-time plugin setup.""" From f8a98ac518a11d71deeabfcf7b0cb842a05746a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0ar=C5=ABnas=20Nejus?= Date: Thu, 4 Sep 2025 11:47:34 +0100 Subject: [PATCH 23/26] Add index for API Reference --- docs/api/index.rst | 9 +++++++++ docs/conf.py | 2 +- docs/dev/index.rst | 10 ++-------- 3 files changed, 12 insertions(+), 9 deletions(-) create mode 100644 docs/api/index.rst diff --git a/docs/api/index.rst b/docs/api/index.rst new file mode 100644 index 000000000..edec5fe96 --- /dev/null +++ b/docs/api/index.rst @@ -0,0 +1,9 @@ +API Reference +============= + +.. toctree:: + :maxdepth: 2 + :titlesonly: + + plugins + database diff --git a/docs/conf.py b/docs/conf.py index 1e53fe427..838d82800 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -95,7 +95,7 @@ html_theme = "pydata_sphinx_theme" html_theme_options = { "collapse_navigation": False, "logo": {"text": "beets"}, - "show_nav_level": 3, # How many levels in left sidebar to show automatically + "show_nav_level": 2, # How many levels in left sidebar to show automatically "navigation_depth": 4, # How many levels of navigation to expand } html_title = "beets" diff --git a/docs/dev/index.rst b/docs/dev/index.rst index 633d50cd1..7bd0ba709 100644 --- a/docs/dev/index.rst +++ b/docs/dev/index.rst @@ -13,17 +13,11 @@ configuration files, respectively. .. _mediafile: https://mediafile.readthedocs.io/en/latest/ .. toctree:: - :maxdepth: 2 + :maxdepth: 3 :titlesonly: plugins/index library importer cli - -.. toctree:: - :maxdepth: 1 - :caption: API Reference - - ../api/plugins - ../api/database + ../api/index From 4cb667cbb3b11405852a6569fdb1dc0343feba3b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0ar=C5=ABnas=20Nejus?= Date: Thu, 4 Sep 2025 12:25:22 +0100 Subject: [PATCH 24/26] Fix formatting issues --- docs/dev/plugins/autotagger.rst | 6 +++--- docs/dev/plugins/other/config.rst | 14 +++++++------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/docs/dev/plugins/autotagger.rst b/docs/dev/plugins/autotagger.rst index 1a4fa6dd6..1cae5295e 100644 --- a/docs/dev/plugins/autotagger.rst +++ b/docs/dev/plugins/autotagger.rst @@ -88,9 +88,9 @@ Beets already ships with several metadata source plugins. Studying these implementations can help you follow conventions and avoid pitfalls. Good starting points include: -- `spotify` -- `deezer` -- `discogs` +- ``spotify`` +- ``deezer`` +- ``discogs`` Migration guidance ------------------ diff --git a/docs/dev/plugins/other/config.rst b/docs/dev/plugins/other/config.rst index e5581fe63..7c529af93 100644 --- a/docs/dev/plugins/other/config.rst +++ b/docs/dev/plugins/other/config.rst @@ -2,10 +2,10 @@ Read Configuration Options ========================== Plugins can configure themselves using the ``config.yaml`` file. You can read -configuration values in two ways. The first is to use `self.config` within your -plugin class. This gives you a view onto the configuration values in a section -with the same name as your plugin's module. For example, if your plugin is in -``greatplugin.py``, then `self.config` will refer to options under the +configuration values in two ways. The first is to use ``self.config`` within +your plugin class. This gives you a view onto the configuration values in a +section with the same name as your plugin's module. For example, if your plugin +is in ``greatplugin.py``, then ``self.config`` will refer to options under the ``greatplugin:`` section of the config file. For example, if you have a configuration value called "foo", then users can put @@ -17,19 +17,19 @@ this in their ``config.yaml``: foo: bar To access this value, say ``self.config['foo'].get()`` at any point in your -plugin's code. The `self.config` object is a *view* as defined by the Confuse_ +plugin's code. The ``self.config`` object is a *view* as defined by the Confuse_ library. .. _confuse: https://confuse.readthedocs.io/en/latest/ If you want to access configuration values *outside* of your plugin's section, -import the `config` object from the `beets` module. That is, just put ``from +import the ``config`` object from the ``beets`` module. That is, just put ``from beets import config`` at the top of your plugin and access values from there. If your plugin provides configuration values for sensitive data (e.g., passwords, API keys, ...), you should add these to the config so they can be redacted automatically when users dump their config. This can be done by setting -each value's `redact` flag, like so: +each value's ``redact`` flag, like so: :: From b7e53579144c66329ca8bf650f08fe7ffa219965 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0ar=C5=ABnas=20Nejus?= Date: Thu, 4 Sep 2025 12:52:37 +0100 Subject: [PATCH 25/26] Run only html by default but allow adjustments --- .github/workflows/lint.yml | 2 +- pyproject.toml | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 0048a8f6e..8fdfa94e5 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -143,4 +143,4 @@ jobs: run: poe lint-docs - name: Build docs - run: poe docs -e 'SPHINXOPTS=--fail-on-warning --keep-going' + run: poe docs -- -e 'SPHINXOPTS=--fail-on-warning --keep-going' diff --git a/pyproject.toml b/pyproject.toml index 184325599..63a22f3f1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -196,7 +196,8 @@ cmd = "mypy" [tool.poe.tasks.docs] help = "Build documentation" -cmd = "make -C docs clean html" +args = [{ name = "COMMANDS", positional = true, multiple = true, default = "html" }] +cmd = "make -C docs $COMMANDS" [tool.poe.tasks.format] help = "Format the codebase" From 123075d51133589ec5fadbf7802f04ad092ce3ef Mon Sep 17 00:00:00 2001 From: Sebastian Mohr Date: Thu, 4 Sep 2025 16:07:25 +0200 Subject: [PATCH 26/26] Updated git blame hashes yet again :) --- .git-blame-ignore-revs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs index 54cb86242..fbe32b497 100644 --- a/.git-blame-ignore-revs +++ b/.git-blame-ignore-revs @@ -68,6 +68,6 @@ ab5acaabb3cd24c482adb7fa4800c89fd6a2f08d # Do not use explicit indices for logging args when not needed d93ddf8dd43e4f9ed072a03829e287c78d2570a2 # Moved dev docs -1f94bdbe4963c693847c24af18b151e12c670995 +07549ed896d9649562d40b75cd30702e6fa6e975 # Moved plugin docs Further Reading chapter -18088c654665e84afc0a67173aa8056ca6b57a58 \ No newline at end of file +33f1a5d0bef8ca08be79ee7a0d02a018d502680d \ No newline at end of file