.. _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 ``config.yaml`` to include ``plugins: myawesomeplugin`` (substituting the name of the Python module containing your plugin). .. _virtualenv: http://pypi.python.org/pypi/virtualenv .. _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: 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) and ``opts`` and ``args`` (command-line options and arguments as returned by `OptionParser.parse_args`_). .. _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. .. _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 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. 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(SomePlugin, self).__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`` * *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_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`s. 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. 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. Parameter: ``lib``. * *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``. 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 four 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 (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). 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. 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 `Confit`_ library. .. _Confit: http://confit.readthedocs.org/ 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. 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:: class MyPlugin(BeetsPlugin): def __init__(self): super(MyPlugin, self).__init__() self.template_funcs['initial'] = _tmpl_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, 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:: class MyPlugin(BeetsPlugin): def __init__(self): super(MyPlugin, self).__init__() self.template_fields['disc_and_track'] = _tmpl_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. 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 ^^^^^^^^^^^^^^^^ :ref:`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. Have a look at the ``beets.mediafile`` source code to learn how to use this descriptor class. If you have created a descriptor you can add it through your plugins ``add_media_field()`` method. .. automethod:: beets.plugins.BeetsPlugin.add_media_field Here's an example plugin that provides a meaningless new field "foo":: class fooplugin(beetsplugin): def __init__(self): field = mediafile.mediafield( mediafile.mp3descstoragestyle(u'foo') mediafile.storagestyle(u'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" 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 ``ImportConfig`` and ``ImportTask`` objects (both defined in ``beets.importer``). Add such a function to the plugin's ``import_stages`` field to register it:: from beets.plugins import BeetsPlugin class ExamplePlugin(BeetsPlugin): def __init__(self): super(ExamplePlugin, self).__init__() self.import_stages = [self.stage] def stage(self, config, task): print('Importing something!') .. _extend-query: Extend the Query Syntax ^^^^^^^^^^^^^^^^^^^^^^^ You can add new kinds of queries to beets' :doc:`query syntax ` indicated by a prefix. As an example, beets already supports regular expression queries, which are indicated by a colon prefix---plugins can do the same. To do so, define a subclass of the ``Query`` type from the ``beets.dbcore.query`` module. Then, in the ``queries`` method of your plugin class, return a dictionary mapping prefix strings to query classes. One simple kind of query you can extend is the ``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``:: 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:: 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. .. _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(u'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. What messages will be logged depends on the logging level and the action performed: * On import stages and event, the default is ``WARNING`` messages. * On direct actions, the default is ``INFO`` and ``WARNING`` message. The verbosity can be increased with ``--verbose`` flags: each flags lowers the level by a notch. 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.)