Writing a Plugin

You have a great idea for extending Picard with a plugin but don’t know where to start. Unfortunately, this is a common problem and prevents far too many of those great ideas from ever seeing the light of day. Perhaps this tutorial will help get you started in turning your great idea a reality.

Picard plugins are written in Python, so that’s the programming language you’ll be using. Please check the INSTALL.md file in the Picard repository on GitHub to see the minimum version requirements. This is Python 3.6 as of the time this tutorial was written. Also refer to the Plugins API for additional information, including the parameters passed to each of the function types.

For the purpose of this tutorial, we’re going to develop a simple plugin to save the argument information provided by Picard to track and release processing plugins. This will demonstrate how the information is accessed, and will provide a utility that you might find useful when developing your own plugins.

The first thing that we’ll need to include is the header information that describes the plugin.

PLUGIN_NAME = "Example plugin"
PLUGIN_AUTHOR = "This authors name"
PLUGIN_DESCRIPTION = "This plugin is an example"
PLUGIN_VERSION = '0.1'
PLUGIN_API_VERSIONS = ['2.2']
PLUGIN_LICENSE = "GPL-2.0-or-later"
PLUGIN_LICENSE_URL = "https://www.gnu.org/licenses/gpl-2.0.html"

Next we list the modules that will be referenced in our code. In this case, we will be using the os module to build the output file path, and the json module to format the argument dictionary text for readability. We will be saving our output file to the base directory used for file naming so we import the config module from Picard, as well as the log module so that we can write debug or error messages to Picard’s log. Finally, we import the appropriate processing hooks and plugin priority settings.

import json
import os

from picard import config, log
from picard.metadata import (register_album_metadata_processor,
                           register_track_metadata_processor)
from picard.plugin import PluginPriority

Warning

To ensure maximum compatibility, you should only use standard Python modules, or third-party modules that are already included in Picard. If you use other modules, then the plugin will not function properly if used on a system that doesn’t have the proper version of the module installed or if someone is using an executable version of Picard.

Now we can start adding the code that we want Picard to execute. First we’ll identify the output file to store the parameter information provided by Picard. This is a file named data_dump.txt to be stored in the file naming output directory. We find the name of the configuration setting we need, move_files_to, by examining the Picard source code for the corresponding option setting screen. In this case it is a TextOption in the RenamingOptionsPage class found in the file picard/ui/options/renaming.py.

file_to_write = os.path.join(config.setting["move_files_to"], "data_dump.txt")

The next part is a function to write a Python object to our output file. To allow the same function to be used for different situations, we include parameters to identify the type of line (input type), the object to write, and options for writing to JSON format and appending or overwriting an existing output file. In our case, we want to overwrite the file each time a new release is processed, but always append the track information to the file.

We also include error checking to write an entry to the Picard log in the event of an exception.

def write_line(line_type, object_to_write, dump_json=False, append=True):
    file_mode = 'a' if append else 'w'
    try:
        with open(file_to_write, file_mode, encoding="UTF-8") as f:
            if dump_json:
                f.write('{0} JSON dump follows:\n'.format(line_type,))
                f.write('{0}\n\n'.format(json.dumps(object_to_write, indent=4)))
            else:
                f.write("{0:s}: {1:s}\n".format(line_type, str(object_to_write),))
    except Exception as ex:
        log.error("{0}: Error: {1}".format(PLUGIN_NAME, ex,))

Now we include the functions to be called when releases and tracks are retrieved by Picard. The release function hook provides three arguments, and the track function hook provides four arguments. The argument types are described in the Plugins API section. The first argument, album, is an object that holds information about the selected album. See the Album class in the picard/album.py file in Picard’s source code for more information.

The second argument, metadata, is an object that holds the tags and variables that Picard has assigned for the current release and track. This is where you can add or edit the tags and variables that Picard makes available to the user for scripts. See the Metadata class in the picard/metadata.py file in Picard’s source code for more information.

The track and release arguments are Python dictionaries containing the information provided in response to Picard’s calls to the MusicBrainz API. The information may differ, depending on the user’s Metadata Options settings for things like “Use release relationships” or “Use track relationships”.

def dump_release_info(album, metadata, release):
    write_line('Release Argument 1 (album)', album, append=False)
    write_line('Release Argument 3 (release)', release, dump_json=True)

def dump_track_info(album, metadata, track, release):
    write_line('Track Argument 1 (album)', album)
    write_line('Track Argument 3 (track)', track, dump_json=True)
    # write_line('Track Argument 4 (release)', release, dump_json=True)

Finally, we need to register our functions so that they are processed with the appropriate events. In our case, we set the priority to HIGH so that we output the parameter information as it is received by Picard before any other plugins have an opportunity to modify it.

# Register the plugin to run at a HIGH priority so that other plugins will
# not have an opportunity to modify the contents of the metadata provided.
register_album_metadata_processor(dump_release_info, priority=PluginPriority.HIGH)
register_track_metadata_processor(dump_track_info, priority=PluginPriority.HIGH)

The complete plugin code file looks something like:

PLUGIN_NAME = "Example plugin"
PLUGIN_AUTHOR = "This authors name"
PLUGIN_DESCRIPTION = "This plugin is an example"
PLUGIN_VERSION = '0.1'
PLUGIN_API_VERSIONS = ['2.2']
PLUGIN_LICENSE = "GPL-2.0-or-later"
PLUGIN_LICENSE_URL = "https://www.gnu.org/licenses/gpl-2.0.html"

import json
import os

from picard import config, log
from picard.metadata import (register_album_metadata_processor,
                           register_track_metadata_processor)
from picard.plugin import PluginPriority

file_to_write = os.path.join(config.setting["move_files_to"], "data_dump.txt")

def write_line(line_type, object_to_write, dump_json=False, append=True):
    file_mode = 'a' if append else 'w'
    try:
        with open(file_to_write, file_mode, encoding="UTF-8") as f:
            if dump_json:
                f.write('{0} JSON dump follows:\n'.format(line_type,))
                f.write('{0}\n\n'.format(json.dumps(object_to_write, indent=4)))
            else:
                f.write("{0:s}: {1:s}\n".format(line_type, str(object_to_write),))
    except Exception as ex:
        log.error("{0}: Error: {1}".format(PLUGIN_NAME, ex,))

def dump_release_info(album, metadata, release):
    write_line('Release Argument 1 (album)', album, append=False)
    write_line('Release Argument 3 (release)', release, dump_json=True)

def dump_track_info(album, metadata, track, release):
   write_line('Track Argument 1 (album)', album)
   write_line('Track Argument 3 (track)', track, dump_json=True)
   # write_line('Track Argument 4 (release)', release, dump_json=True)

# Register the plugin to run at a HIGH priority so that other plugins will
# not have an opportunity to modify the contents of the metadata provided.
register_album_metadata_processor(dump_release_info, priority=PluginPriority.HIGH)
register_track_metadata_processor(dump_track_info, priority=PluginPriority.HIGH)

That’s it for our plugin code. Now we need to package it so that we can install it into Picard. If we’re going to just use it locally for ourself, the easiest way is to just name the file something like my_plugin.py. If there are multiple files, such as plugins that include additional settings screens, then the files should be saved in a directory such as my_plugin with the main file named __init__.py. The directory is then archived into a my_plugin.zip file, with the file name the same as the included directory name. The contents of the archive would show as something like:

my_plugin/__init__.py
my_plugin/another_file.py
my_plugin/etc

If you’ve made it this far, congratulations! You’ve just created your first Picard plugin. Now you have a starting point for turning that great idea into reality.

See also

Relevant portions of Picard’s source code including: