mirror of
https://github.com/beetbox/beets.git
synced 2025-12-09 18:12:19 +01:00
391 lines
14 KiB
ReStructuredText
391 lines
14 KiB
ReStructuredText
Further Reading
|
|
===============
|
|
|
|
.. contents:: Table of Contents
|
|
: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
|
|
--------------------------
|
|
|
|
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.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: 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
|
|
</reference/query>`. 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 <numericquery>`) 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:
|
|
|
|
::
|
|
|
|
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.
|