Plugin Development

browsepy is extensible via a powerful plugin API. A plugin can register its own Flask blueprints, file browser widgets (filtering by file), mimetype detection functions and even its own command line arguments.

A fully functional browsepy.plugin.player plugin module is provided as example.

Plugin Namespace

Plugins are regular python modules. They are loaded by –plugin console argument.

Aiming to make plugin names shorter, browsepy try to load plugins using namespaces and prefixes defined on configuration’s plugin_namespaces entry on browsepy.app.config. Its default value is browsepy’s built-in module namespace browsepy.plugins, browsepy_ prefix and an empty namespace (so any plugin could be used with its full module name).

Summarizing, with default configuration:

  • Any python module inside browsepy.plugin can be loaded as plugin by its relative module name, ie. player instead of browsepy.plugin.player.
  • Any python module prefixed by browsepy_ can be loaded as plugin by its unprefixed name, ie. myplugin instead of browsepy_myplugin.
  • Any python module can be loaded as plugin by its full module name.

Said that, you can name your own plugin so it could be loaded easily.

Examples

Your built-in plugin, placed under browsepy/plugins/ in your own browsepy fork:

browsepy --plugin=my_builtin_module

Your prefixed plugin, a regular python module in python’s library path, named browsepy_prefixed_plugin:

browsepy --plugin=prefixed_plugin

Your plugin, a regular python module in python’s library path, named my_plugin.

browsepy --plugin=my_plugin

Protocol

The plugin manager subsystem expects a register_plugin callable at module level (in your __init__ module globals) which will be called with the manager itself (type PluginManager) as first parameter.

Plugin manager exposes several methods to register widgets and mimetype detection functions.

A *sregister_plugin*s function looks like this (taken from player plugin):

def register_plugin(manager):
    '''
    Register blueprints and actions using given plugin manager.

    :param manager: plugin manager
    :type manager: browsepy.manager.PluginManager
    '''
    manager.register_blueprint(player)
    manager.register_mimetype_function(detect_playable_mimetype)

    # add style tag
    manager.register_widget(
        place='styles',
        type='stylesheet',
        endpoint='player.static',
        filename='css/browse.css'
    )

    # register link actions
    manager.register_widget(
        place='entry-link',
        type='link',
        endpoint='player.audio',
        filter=PlayableFile.detect
    )
    manager.register_widget(
        place='entry-link',
        icon='playlist',
        type='link',
        endpoint='player.playlist',
        filter=PlayListFile.detect
    )

    # register action buttons
    manager.register_widget(
        place='entry-actions',
        css='play',
        type='button',
        endpoint='player.audio',
        filter=PlayableFile.detect
    )
    manager.register_widget(
        place='entry-actions',
        css='play',
        type='button',
        endpoint='player.playlist',
        filter=PlayListFile.detect
    )

    # check argument (see `register_arguments`) before registering
    if manager.get_argument('player_directory_play'):
        # register header button
        manager.register_widget(
            place='header',
            type='button',
            endpoint='player.directory',
            text='Play directory',
            filter=PlayableDirectory.detect
            )

In case you need to add extra command-line-arguments to browsepy command, register_arguments() method can be also declared (like register_plugin, at your plugin’s module level). It will receive a ArgumentPluginManager instance, providing an argument-related subset of whole plugin manager’s functionality.

A simple register_arguments example (from player plugin):

def register_arguments(manager):
    '''
    Register arguments using given plugin manager.

    This method is called before `register_plugin`.

    :param manager: plugin manager
    :type manager: browsepy.manager.PluginManager
    '''

    # Arguments are forwarded to argparse:ArgumentParser.add_argument,
    # https://docs.python.org/3.7/library/argparse.html#the-add-argument-method
    manager.register_argument(
        '--player-directory-play', action='store_true',
        help='enable directories as playlist'
        )

Widgets

Widget registration is provided by PluginManager.register_widget().

You can alternatively pass a widget object, via widget keyword argument, or use a pure functional approach by passing place, type and the widget-specific properties as keyword arguments.

In addition to that, you can also define in which cases widget will be shown passing a callable to filter argument keyword, which will receive a browsepy.file.Node (commonly a browsepy.file.File or a browsepy.file.Directory) instance.

For those wanting the object-oriented approach, and for reference for those wanting to know widget properties for using the functional way, WidgetPluginManager.widget_types dictionary is available, containing widget namedtuples (see collections.namedtuple()) definitions.

Here is the “widget_types” for reference.

class WidgetPluginManager(RegistrablePluginManager):
    ''' ... '''
    widget_types = {
        'base': defaultsnamedtuple(
            'Widget',
            ('place', 'type')),
        'link': defaultsnamedtuple(
            'Link',
            ('place', 'type', 'css', 'icon', 'text', 'endpoint', 'href'),
            {
                'text': lambda f: f.name,
                'icon': lambda f: f.category
            }),
        'button': defaultsnamedtuple(
            'Button',
            ('place', 'type', 'css', 'text', 'endpoint', 'href')),
        'upload': defaultsnamedtuple(
            'Upload',
            ('place', 'type', 'css', 'text', 'endpoint', 'action')),
        'stylesheet': defaultsnamedtuple(
            'Stylesheet',
            ('place', 'type', 'endpoint', 'filename', 'href')),
        'script': defaultsnamedtuple(
            'Script',
            ('place', 'type', 'endpoint', 'filename', 'src')),
        'html': defaultsnamedtuple(
            'Html',
            ('place', 'type', 'html')),
    }

Function browsepy.file.defaultsnamedtuple() is a collections.namedtuple() which uses a third argument dictionary as default attribute values. None is assumed as implicit default.

All attribute values can be either str or a callable accepting a browsepy.file.Node instance as argument and returning said str, allowing dynamic widget content and behavior.

Please note place and type are always required (otherwise widget won’t be drawn), and this properties are mutually exclusive:

  • link: attribute href supersedes endpoint.
  • button: attribute href supersedes endpoint.
  • upload: attribute action supersedes endpoint.
  • stylesheet: attribute href supersedes endpoint and filename
  • script: attribute src supersedes endpoint and filename.

Endpoints are Flask endpoint names, and endpoint handler functions must receive a “filename” parameter for stylesheet and script widgets (allowing it to point using with Flask’s statics view) and a “path” argument for other use-cases. In the former case it is recommended to use browsepy.file.Node.from_urlpath() static method to create the appropriate file/directory object (see browsepy.file).

Considerations

Name your plugin wisely, look at pypi for conflicting module names.

Always choose the less intrusive approach on plugin development, so new browsepy versions will not likely break it. That’s why stuff like PluginManager.register_blueprint() is provided and its usage is preferred over directly registering blueprints via plugin manager’s app reference (or even module-level app reference).

A good way to keep your plugin working on future browsepy releases is upstreaming it onto browsepy itself.

Feel free to hack everything you want. Pull requests are definitely welcome.