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

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 the directory that contains 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).


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.

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).

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.


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(SomePlugin, self).__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(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"

* *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`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. 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 Add
  Import Pipeline Stages). 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 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
=====================

Plugins 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 Confuse library.

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 Path
Formats). 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 expands 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
================

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 "add_media_field()" method.

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 "ImportSession" 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, session, task):
           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:

   self.early_import_stages = [self.stage]


Extend the Query Syntax
=======================

You can add new kinds of queries to beets' 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":

   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 ranges) from the command line.

* User input for flexible fields may be validated and converted.


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)

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
=====================

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:

   from beets.plugins import BeetsPlugin
   from beets.ui.commands import PromptChoice

   class ExamplePlugin(BeetsPlugin):
       def __init__(self):
           super(ExamplePlugin, self).__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:

   # 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"):

   # 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.
