
==========================
Elisa Plugin Documentation
==========================
:Author: Philippe Normand
:Contact: philippe [at] fluendo [dot] com
:Revision: $Revision: 1469 $
:Date: $Date: 2007-02-07 18:33:09 +0100 (mer, 07 fév 2007) $

.. sectnum::

.. contents::

Philosophy
==========

Elisa plugin framework relies on Setuptools_ which provides
interesting features like:

- eased packaging using Python Eggs
- dependency management
- release versions control
- easy package resources access
- and much more...

This document details step by step how to develop an Elisa plugin.

.. _Setuptools: http://peak.telecommunity.com/DevCenter/setuptools

Plugin types
============

Each plugin has to:

- implement at least the `core.plugin.IPlugin`
  interface
- inherit from `core.plugin.Plugin` class

For developer convenience there are 4 basic plugin types:

- Single inheritance from Plugin:

  * DataAccessPlugin : provides support for accessing data by URIs
  * QueuePlugin : provides playlist features support to the player

- Multiple inheritance from Plugin:

  * TreePlugin inherits from `core.menu.MenuItem` and
    `core.plugin.Plugin`. This kind of plugin is used to provide plugins
    with menus.
  * PlayerPlugin is just a TreePlugin including player controls

It's not required to use these Plugin classes, a trivial plugin would
only inherit from Plugin and that would be perfectly fine. Along with
these Plugin types, we provide some interfaces:

- IDataAccess: implemented by DataAccessPlugin subclasses
- IMediaPlayer: implemented by plugins willing to add support for
  multimedia playback management
- IQueue: implemented by QueuePlugins
- IUserInputPlugin: implemented by plugins adding support for input devices

Discovery
=========

Elisa exports a set of entry points where additional components can
plug into. Thus, Elisa is able to dynamically detect plugins, wherever
they are installed by easy_install. Current entry points groups are:

- elisa.plugins: TreePlugins willing to appear on Elisa's root menu
  should plug into this entry point
- elisa.plugins.data: used by DataAccess plugins
- elisa.plugins.queue: used by QueuePlugins
- elisa.plugins.services: show up in the Services menu
- elisa.skins: used by Skins
- elisa.plugins.misc: used by all other plugins not fitting in
  previous entry points

Plugins can even rely on other plugins. For instance it's fairly
common in Elisa that TreePlugins rely on plugins implementing
IDataAccess to actually build menus given some URIs. Such kind of
mechanism is implemented using the ExtensionPoint class which will be
detailled later on this document.

Structure
=========

Here's a typical Plugin example:

.. code-block:: python

  from core.plugin import Plugin
  from core.interfaces import IUserInputPlugin

  class LIRCPlugin(Plugin):

      __implements__ = IUserInputPlugin

      config_doc = {'lirc_rc': 'filename of the LIRC config map to use'}

      default_config = {'lirc_rc': 'elisa.streamzap',
                        }
      name = "lirc"

      provides = ['input',]
      singleton = True

      def get_input_events(self):
          # access to the plugin's config:
          lirc_rc = self.get_config().get('lirc_rc')
          return []

The plugin above implements the IUserInputPlugin interface, it has one
option (stored in elisa's config file), one unique name and an
optional list of features. This plugin is a singleton, it's
instantiated only once by the plugin manager. Each plugin option
should be documented in the `config_doc` instance attribute.


Plugin initialization
=====================

Some plugins may rely either on internal Elisa components (like the
cache manager) or on external Python packages. If one of these is not
active or installed in the end-user's system, the plugin developer may
detect it, report the error and unload the plugin like this:

.. code-block:: python

  class LIRCPlugin(Plugin):
      # ...

      def post_init(self):
          if something.is_wrong():
              self.logger.info("Something got wrong, i can't run!")
              self.unload()

      # ...

The post_init() method is called by the plugin manager, just after
plugin instantiation and just before the plugin starts to be used by
Elisa. 

Plugging to other plugins
=========================

Some plugins may require to use features provided by other
plugins. This is quite easy to do, all you have to know is the
interface implemented by the plugin and the feature you want to
use. The following code snippet shows how to proceed:

.. code-block:: python

  class MyPlugin(plugin.Plugin):

      # plug to all DataAccess plugins known and loaded by the plugin
      # manager
      data_access = plugin.ExtensionPoint(interfaces.IDataAccess)

      def get_data_plugin(self, location):
          """ This method returns the data access plugin to use to
          read data at given location
	  """
          uri = media_uri.Uri(location)

	  # extract the type or uri (file:// .. smb:// ....)
	  scheme = uri.scheme

          # look for the plugin supporting this type of uri
          data_access_plugin = self.data_access.get("uri:%s" % scheme)

          return data_access_plugin


ExtensionPoint instances are used as dictionnaries mapping plugin
features with actual plugin instances. So if you have a plugin which
**provides** the feature "uri:file", another plugin will be able to
read uris starting with file:// using an ExtensionPoint like in
following code snippet:

.. code-block:: python

   # ...
   location = "file:///path/to/foo.bar"
   data_fs = self.get_data_plugin(location)
   is_dir = data_fs.is_dir(location)
   # ...

Hooking to the database
=======================

Some plugins may require to use the Elisa's SQLite database. To do so,
the plugin declares the tables and their structure using the
`db_tables` instance attribute like in the following example:

.. code-block:: python

  class MyPlugin(plugin.Plugin):

      db_tables = {'table_name': """
                            id INTEGER PRIMARY KEY AUTOINCREMENT,
                            genre TEXT NOT NULL,
                            rank INT NOT NULL"""}

When the plugin initializes it will take care of creating the SQLite
tables if they don't exist already. One warning though, table names
should be unique and it's currently the responsability of the plugin
developer to check his SQLite table names won't clash with existing
ones. This statement is subject to change very soon :-)

To be able to play with the plugin's db tables, there's a method
called `sql_execute` which can be used from within the plugin:

.. code-block:: python

  class MyPlugin(plugin.Plugin):

     # ....

     def do_something(self):
        self.sql_execute("insert into table_name(id, genre, rank) values(1, 'foo', 2)")
        rows = self.sql_execute("select * from table_name")
        for row in rows:
           print row.genre, row.rank

This is pretty straightforward; the developer only needs to know the
SQL language. Simple immutable objects are returned from SQL select
queries. The interested user will find a real-life example of how to
use the Elisa SQL features in `core.plugins.web_radio` plugin.

Internationalization
====================


You can add multiple language support to your plugins using gettext.
Elisa provides an easy way to use gettext for translation your plugins
without breaking Elisa's main application translation.  You need first
to define in your plugin's class the variable ``translation_path``.
This variable must hold the path of your plugin's localization
directory.  When you plugin is created, this path will be parsed for .po
files, which will be then be automatically compiled. The .po files
must have the same name as the one you defined in the "name" property
of the plugin (i.e if your plugin's name property is "test", your .po
files should be named "test.po").

To use localization in your plugin, you will need to define at the top
level of your plugins files the local _() function, which will bind to
your plugin's text domain automatically. Here is an example below :

.. code-block:: python

    gettext.bindtextdomain("test", "i18n_test")
    _ = lambda s: gettext.dgettext("test", s);
    
    class TestPlug(Plugin):
        translation_path = "i18n_test"
        name = "test"
        __implements__ = IPlugin
        
        def __init__(self):
            Plugin.__init__(self)
            
        def say_yes(self):
            print _("Yes this is a test")
                
    t = TestPlug()
    t.say_yes()


Using resource/data files in your plugins
=========================================

If you need to access extra files from your plugin (such as pictures), you should not use
pkg_resources to retrieve a path relative to your module.
Please use get_resource_path() from elisa/core/utils/misc.py instead. This function
enables us to retrieve a path to a resource also if Elisa's script are self contained
into an executable (frozen application).


Packaging
=========


So, once the plugin is developed, it's time to package, either for
test purpose or for deployment. To make our plugin usable in Elisa,
the developer has to:

1. package the plugin using a setup.py
2. reference his plugin class in one of Elisa's entry points groups.
3. build an Egg file using bdist_egg setup.py command

Here is an example of a setup.py:

.. code-block:: python

  from setuptools import setup, find_packages

  setup(name="elisa-lirc",
        version="0.1",
        description="""LIRC plugin""",
        author="The Elisa Team",
        author_email="foo@bar.com",
        packages=find_packages(),
        entry_points="""
        [elisa.plugins.misc]
        lirc = mylircplugin.package.lirc:LIRCPlugin
        """)

In above example, the lirc plugin is registered in elisa.plugins.misc
group. In development process context, it's overkill to rebuild
the Egg at each iteration; the developer would use the "develop"
command:

::

  $ python setup.py develop

In deployment context, the developer would package the plugin with:

::

  $ python setup.py bdist_egg

And an .egg file would appear in a dist/ directory from where the
setup.py was called.

The end-user can then either install the egg using easy_install or put
the .egg file in $HOME/.elisa/plugins/ directory. Finally it's just a
matter of adding plugin's name to elisa configuration file to tell
Elisa to load and use the new plugin.

Enabling the plugin
===================

At current stage of development, there's no plugin browser in Elisa,
so users have to enable or disable plugins by editing the elisa
configuration file, usually elisa.conf. 

Each entry point group is mapped to a elisa configuration option (in
"general" section) allowing the user to select the plugins to use:

- elisa.plugins -> plugins (type: list of strings)
- elisa.plugins.data -> plugins.data (type: list of strings)
- elisa.plugins.misc -> plugins.misc (type: list of strings)
- elisa.plugins.queue -> active_queue (type: string)
- elisa.plugins.services -> plugins.services (type: list of strings)
- elisa.skins -> skin (type: string)

The plugin manager can load and enable multiple members of one group
(like elisa.plugins.data) whereas it's only possible to load one entry
point of other groups (like elisa.skins). This explains why the
configuration uses lists for some groups and single string for others.

If we take our LIRC plugin example, it's registered in
"elisa.plugins.misc" group  with the name of "lirc". So, to
enable the plugin, the user has to add the string 'lirc' to the
plugins.misc config option:

::

  [general]
  # ...
  plugins.misc = ['lirc', 'web_radio', 'hal']

Every time the configuration file is edited by the user, Elisa has to
be restarted.


Debugging
=========

This section provides some hints about log and debug Elisa
abilities. They are quite simple to use, Elisa has a global Logger
which can be accessed by every single component:

.. code-block:: python

   from core.log import Logger

   # access to the global Logger
   logger = Logger()

   logger.info("some message")
   logger.debug("some debug message...")
   logger.debug_detailled("some detailled debug message...")
   logger.debug_verbose("some verbose debug message...")
   
For developer convenience, the Plugin class includes the `logger` as an
instance attribute. Each method of the above example is mapped to a
log level which is set globally via the Elisa's configuration
file. Log levels are hierarchically structured:

::

      ----------------------  DEBUG_VERBOSE
          debug_verbose
       --------------------   DEUG_DETAILLED
         debug_detailled
        -----------------     DEBUG
               debug
         ---------------      INFO
               info

(Ok i'm not an ascii artist as you see :))

If log_level is set to INFO in the config, only info() will actually
be activated. If log_level is set to DEBUG_DETAILLED then info(),
debug() and debug_detailled() would be activated... and so on.

Sometimes it's also convenient to be able to log a method's arguments,
the `core.log` module provides a decorator for that purpose:

.. code-block:: python

   from core.log import Logger, debug
   from core import plugin

   class MyPlugin(plugin.Plugin):
      # ...

      @debug("sample message")
      def some_method(self, foo, bar='quoi')
          pass

When `some_method` is called and log_level is set to DEBUG in the
config file, the call would be logged like this:

::

   # in the code:
   plugin.some_method(2)

   # in the logs:
   10/08/2006 09:51:48.766  DEBUG     some_method(<MyPlugin instance...>, 2) bar='quoi' sample message


Miscellaneous notes
===================

Make your DataAccess plugin provide a customized GStreamer pipeline
-------------------------------------------------------------------

By default the player uses the Pigment Stream class which relies on
GStreamer playbin magic pipeline. Generally this works fine, playbin
builds an internal pipeline depending on the media to play. But
nothing is perfect in the world of pipelines plumbery and some medias
may not be played using playbin. When that happens you can provide a
pipeline yourself using the `get_gst_pipeline` method of
DataAccessPlugins.

Say you have a nice YoutubeFSPlugin providing youtube:// uris. Playbin
can't handle such kind of uri, let's provide our own pipeline:

::

    def get_gst_pipeline(self, uri):
        pipeline = gst.parse_launch("gnomevfssrc name=src ! typefind ! "
                                    "ffdemux_flv name=demuxer demuxer. ! "
                                    "queue ! ffdec_flv ! ffmpegcolorspace ! "
                                    "pgmrendersink name=sink demuxer. ! "
                                    "queue ! mad ! audioconvert ! "
                                    "audioresample ! volume name=vlm ! "
                                    "autoaudiosink")
        return pipeline

There are some implied conventions by the use of that method. Your
pipeline has to have the following elements:

- an src element (here: gnomevfssrc) named "src"
- pgmrendersink has to be used if the media contains video, the sink
  shall be named "sink"
- if the media contains audio there shall be the volume element
  somewhere, named "vlm"
- to be generic enough, use autoaudiosink instead of specific sinks
  like alsasink for instance

That pipeline will be passed to the Pigment Stream instance via the
Elisa player and your media should play fine. You can find a real life
example of `get_gst_pipeline` in the daap_fs plugin's code.
