mirror of
https://github.com/beetbox/beets.git
synced 2025-12-07 09:04:33 +01:00
In an attempt to finally address the longstanding SQLite locking issues, I'm introducing a way to explicitly, lexically scope transactions. The Transaction class is a context manager that always fully fetches after SELECTs and automatically commits on exit. No direct access to the library is allowed, so all changes will eventually be committed and all queries will be completed. This will also provide a debugging mechanism to show where concurrent transactions are beginning and ending. To support composition (transaction reentrancy), an internal, per-Library stack of transactions is maintained. Commits only happen when the outermost transaction exits. This means that, while it's possible to introduce atomicity bugs by invoking Library methods outside of a transaction, you can conveniently call them *without* a currently-active transaction to get a single atomic action. Note that this "transaction stack" concepts assumes a single Library object per thread. Because we need to duplicate Library objects for concurrent access due to sqlite3 limitation already, this is fine for now. Later, the interface should provide one transaction stack per thread for shared Library objects.
270 lines
11 KiB
ReStructuredText
270 lines
11 KiB
ReStructuredText
.. _writing-plugins:
|
|
|
|
Writing Plugins
|
|
---------------
|
|
|
|
A beets plugin is just a Python module inside the ``beetsplug`` namespace
|
|
package. (Check out this `Stack Overflow question about namespace packages`_ if
|
|
you haven't heard of them.) So, to make one, create a directory called
|
|
``beetsplug`` and put two files in it: one called ``__init__.py`` and one called
|
|
``myawesomeplugin.py`` (but don't actually call it that). Your directory
|
|
structure should look like this::
|
|
|
|
beetsplug/
|
|
__init__.py
|
|
myawesomeplugin.py
|
|
|
|
.. _Stack Overflow question about namespace packages:
|
|
http://stackoverflow.com/questions/1675734/how-do-i-create-a-namespace-package-in-python/1676069#1676069
|
|
|
|
Then, you'll need to put this stuff in ``__init__.py`` to make ``beetsplug`` a
|
|
namespace package::
|
|
|
|
from pkgutil import extend_path
|
|
__path__ = extend_path(__path__, __name__)
|
|
|
|
That's all for ``__init__.py``; you can can leave it alone. The meat of your
|
|
plugin goes in ``myawesomeplugin.py``. There, you'll have to import the
|
|
``beets.plugins`` module and define a subclass of the ``BeetsPlugin`` class
|
|
found therein. Here's a skeleton of a plugin file::
|
|
|
|
from beets.plugins import BeetsPlugin
|
|
|
|
class MyPlugin(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, make sure your ``beetsplug`` directory is in the Python
|
|
path (using ``PYTHONPATH`` or by installing in a `virtualenv`_, for example).
|
|
Then, as described above, edit your ``.beetsconfig`` to include
|
|
``plugins=myawesomeplugin`` (substituting the name of the Python module
|
|
containing your plugin).
|
|
|
|
.. _virtualenv: http://pypi.python.org/pypi/virtualenv
|
|
|
|
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, config, 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: http://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), ``config`` (a `ConfigParser object`_ containing the
|
|
configuration values), and ``opts`` and ``args`` (command-line options and
|
|
arguments as returned by `OptionParser.parse_args`_).
|
|
|
|
.. _ConfigParser object: http://docs.python.org/library/configparser.html
|
|
.. _OptionParser.parse_args:
|
|
http://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 an ``OptionParser`` instance. Just use it
|
|
like you would a normal ``OptionParser`` in an independent script.
|
|
|
|
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 the ``BeetsPlugin.listen`` decorator. Here's
|
|
an example::
|
|
|
|
from beets.plugins import BeetsPlugin
|
|
|
|
class SomePlugin(BeetsPlugin):
|
|
pass
|
|
|
|
@SomePlugin.listen('pluginload')
|
|
def loaded():
|
|
print 'Plugin loaded!'
|
|
|
|
Pass the name of the event in question to the ``listen`` decorator. 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 fishes (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``, ``config``
|
|
|
|
* *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``, ``config``
|
|
|
|
* *write*: called with an ``Item`` and a ``MediaFile`` object just before a
|
|
file's metadata is written to disk.
|
|
|
|
* *import_task_start*: called when before an import task begins processing.
|
|
Parameters: ``task`` and ``config``.
|
|
|
|
* *import_task_apply*: called after metadata changes have been applied in an
|
|
import task. Parameters: ``task`` and ``config``.
|
|
|
|
The included ``mpdupdate`` plugin provides an example use case for event listeners.
|
|
|
|
Extend the Autotagger
|
|
^^^^^^^^^^^^^^^^^^^^^
|
|
|
|
Plugins in can also enhance the functionality of the autotagger. For a
|
|
comprehensive example, try looking at the ``chroma`` plugin, which is included
|
|
with beets.
|
|
|
|
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 three
|
|
methods on the plugin class:
|
|
|
|
* ``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 (and Item object) and ``info`` is the MusicBrainz track
|
|
entry that is proposed as a match. Should return a ``(dist, dist_max)`` pair
|
|
of floats indicating the distance.
|
|
|
|
* ``album_distance(self, items, info)``: like the above, but compares a list of
|
|
items (representing an album) to an album-level MusicBrainz entry. Should
|
|
only consider album-level metadata (e.g., the artist name and album title) and
|
|
should not duplicate the factors considered by ``track_distance``.
|
|
|
|
* ``candidates(self, items)``: 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)``: given a *singleton* item, return a list of
|
|
``TrackInfo`` objects for candidate tracks to be compared and matched.
|
|
|
|
When implementing these functions, it will probably be very necessary to use the
|
|
functions from the ``beets.autotag`` and ``beets.autotag.mb`` modules, both of
|
|
which have somewhat helpful docstrings.
|
|
|
|
Read Configuration Options
|
|
^^^^^^^^^^^^^^^^^^^^^^^^^^
|
|
|
|
Plugins can configure themselves using the ``.beetsconfig`` file. Define a
|
|
``configure`` method on your plugin that takes an ``OptionParser`` object as an
|
|
argument. Then use the ``beets.ui.config_val`` convenience function to access
|
|
values from the config file. Like so::
|
|
|
|
class MyPlugin(BeetsPlugin):
|
|
def configure(self, config):
|
|
number_of_goats = beets.ui.config_val(config, 'myplug', 'goats', '42')
|
|
|
|
Try looking at the ``mpdupdate`` plugin (included with beets) for an example of
|
|
real-world use of this API.
|
|
|
|
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 add new functions using the ``template_func`` decorator. To use it,
|
|
decorate a function with ``MyPlugin.template_func("name")`` where ``name`` is
|
|
the name of the function as it should appear in template strings.
|
|
|
|
Here's an example::
|
|
|
|
class MyPlugin(BeetsPlugin):
|
|
pass
|
|
@MyPlugin.template_func('initial')
|
|
def _tmpl_initial(text):
|
|
if text:
|
|
return text[0].upper()
|
|
else:
|
|
return u''
|
|
|
|
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, decorate a function taking a single
|
|
parameter, ``item``, with ``MyPlugin.template_field("name")``. Here's an example
|
|
that adds a ``$disc_and_track`` field::
|
|
|
|
@MyPlugin.template_field('disc_and_track')
|
|
def _tmpl_disc_and_track(item):
|
|
"""Expand to the disc number and track number if this is a
|
|
multi-disc release. Otherwise, just exapnds to the track
|
|
number.
|
|
"""
|
|
if item.disctotal > 1:
|
|
return u'%02i.%02i' % (item.disc, item.track)
|
|
else:
|
|
return u'%02i' % (item.track)
|
|
|
|
With this plugin enabled, templates can reference ``$disc_and_track`` as they
|
|
can any standard metadata field.
|
|
|
|
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 ``item_fields`` method on plugins should be overridden to return a
|
|
dictionary whose keys are field names and whose values are descriptor objects
|
|
that provide the field in question. The descriptors should probably be
|
|
``MediaField`` instances (defined in ``beets.mediafile``). Here's an example
|
|
plugin that provides a meaningless new field "foo"::
|
|
|
|
from beets import mediafile, plugins, ui
|
|
class FooPlugin(plugins.BeetsPlugin):
|
|
def item_fields(self):
|
|
return {
|
|
'foo': mediafile.MediaField(
|
|
mp3 = mediafile.StorageStyle(
|
|
'TXXX', id3_desc=u'Foo Field'),
|
|
mp4 = mediafile.StorageStyle(
|
|
'----:com.apple.iTunes:Foo Field'),
|
|
etc = mediafile.StorageStyle('FOO FIELD')
|
|
),
|
|
}
|
|
|
|
Later, the plugin can manipulate this new field by saying something like
|
|
``mf.foo = 'bar'`` where ``mf`` is a ``MediaFile`` instance.
|
|
|
|
Note that, currently, these additional fields are *only* applied to
|
|
``MediaFile`` itself. The beets library database schema and the ``Item`` class
|
|
are not extended, so the fields are second-class citizens. This may change
|
|
eventually.
|
|
|
|
.. _MediaFile: https://github.com/sampsyo/beets/wiki/MediaFile
|