diff --git a/docs/plugins/index.rst b/docs/plugins/index.rst index 117db3a0d..ecd5c5eff 100644 --- a/docs/plugins/index.rst +++ b/docs/plugins/index.rst @@ -70,270 +70,14 @@ Here are a few of the plugins written by the beets community: https://github.com/coolkehon/beets/blob/master/beetsplug/cmus.py .. _cmus: http://cmus.sourceforge.net/ -.. _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:: +If you know a little Python, you can write your own plugin to do almost anything +you can imagine with your music collection. See the :doc:`guide to writing beets +plugins `. - 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 - -* *save*: called whenever the library is changed and written to disk (the - ``lib`` keyword argument is the Library object that was written) - -* *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. - -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 +.. toctree:: + :hidden: + + writing diff --git a/docs/plugins/writing.rst b/docs/plugins/writing.rst new file mode 100644 index 000000000..b1790dcf9 --- /dev/null +++ b/docs/plugins/writing.rst @@ -0,0 +1,267 @@ +.. _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 + +* *save*: called whenever the library is changed and written to disk (the + ``lib`` keyword argument is the Library object that was written) + +* *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. + +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