How to create an Audio-CD Media Provider
========================================

#TODO: Add support for the get_direc_children_with_info and explain it here

In this example we will use the audiocd media provider to demonstrate how to
write a component to add support for another media type. The code of this  
component can be found in the Elisa codebase at this location:

    elisa/plugins/base/media_providers/audiocd_media.py

First, you need to carefully read the API and its documentation of the 
MediaProvider base component, which is defined in the following file: 

    elisa/base_components/media_provider.py

To create a new type of media provider, you need to inherit from this
MediaProvider class and implement the functions the API defines.
You do not have to implement all of them in your own class, but basically
the following ones are usually mandatory to create a working media provider:

def supported_uri_schemes__get(self)
def get_media_type(self, uri)
def is_directory(self, uri)
def has_children(self, uri, callback)
def get_direct_children(self, uri, list_of_children)

The AudioCD media provider uses the musicbrainz2 python module to read the disc and
uses the internet connection to retrieve album and track information. 

We will first look at the import section of this component :

.. code-block:: python

    from elisa.base_components.media_provider import MediaProvider, UriNotMonitorable

This import the MediaProvider class we are going to inherit from, as well as
the exception UriNotMonitorable

.. code-block:: python

    from elisa.core.bus.bus_message import ComponentsLoaded, DeviceAction

Those define bus messages components are sending through Elisa internal bus.
ComponentsLoaded notifies us when all components have been loaded into Elisa,
and DeviceAction notifies us of local devices modifications (such as a disc
inserted, a USB key connected, ...)

### ben: we need a document about the message bus and should link to it here

.. code-block:: python

    from elisa.core.media_uri import quote, MediaUri

MediaUri is the class used to represent URIs. You should always use it to 
manage files.

.. code-block:: python

    from elisa.core import common

Through the interface define in common, you can call the application
object, which permits you to access Elisa's Managers (MediaManager, 
ThumbnailManager...)

.. code-block:: python

    from twisted.internet import defer, threads

import twisted modules to create and handle Deferred objects


Let's now look at the AudiocdMedia class itself.
It is defines as: class AudiocdMedia(MediaProvider)

We explicitely inherit from the MediaProvider base component.

We need to define a unique name for our component. This is done in the class
initialization :

### ben: Do we?

.. code-block:: python

    name = "audiocd"

It is also important you always call the __init__ function of the base component
you inherit from:

.. code-block:: python

    def __init__(self):
        MediaProvider.__init__(self)

You should not at this point call managers or ask a model to another component.
The initialization sequence is not done at this time. For this, the initialize()
function is defined. It is called by the InterfaceController once all managers,
plugins and components have been loaded.

.. code-block:: python

    def initialize(self):
        common.application.bus.register(self._bus_message_received,
                                        ComponentsLoaded, DeviceAction)

At this point, we explicitely listen to the application bus, and tell it we
want to listen to ComponentsLoaded and DeviceAction events. Once such a message
is triggered by another component, the self._bus_message_received function will
be called with the event and sender as parameters.

.. code-block:: python

    def _bus_message_received(self, msg, sender):
        if isinstance(msg, DeviceAction) and msg.fstype == 'cdda':
            if msg.action == DeviceAction.ActionType.DEVICE_ADDED:
                self.debug("AudioCD found at %s" % msg.name)
            elif msg.action == DeviceAction.ActionType.DEVICE_REMOVED:
                self.debug("AudioCD removed")
                self._discid = ""
                self._tracks = {}
                
This above implementation permits us to know when an audio cd has been inserted or
ejected.

.. code-block:: python

        def scannable_uri_schemes__get(self):
            return {}

We explicitely tell our component does not handle scannable media.

.. code-block:: python

        def supported_uri_schemes__get(self):
            return { 'cdda': 0}
            
This tells the MediaManager that our component can handle cdda URI scheme
With the get_media_type() function we can provide more info to other 
components about the type of media a URI represent.

.. code-block:: python

    def get_media_type(self, uri):
        file_type = 'audio'
        mime_type = ''
        if self.is_directory(uri):
            file_type = 'directory'

        return { 'file_type' : file_type,
                 'mime_type' : mime_type }

Same goes to the is_directory() function. This is the kind of functions
a MenuActivity will use a lot to create a media tree.

.. code-block:: python

    def is_directory(self, uri):
        return uri.host == '' and uri.parent.path == '/'

Now comes the actual media listing with the get_direct_children(). As said in
the MediaProvider documentation, it should be non-blocking and return a 
Deferred object:

.. code-block:: python

    def get_direct_children(self, uri, l):
        d = threads.deferToThread(self._retrieve_children, uri, l)
        return d

This permits us to do the actual media retrieving in another thread, thus not
blocking the caller's context.

.. code-block:: python

    def _retrieve_children(self, uri, list_of_children):
        path = uri
        if self.is_directory(uri):
            try:
                disc = mbdisc.readDisc()
            except mbdisc.DiscError, e:
                return list_of_children
            if self._discid == disc.id: ## we gave the children already!
                return list_of_children

            self._discid = disc.id ## set the discid
            self.debug("the disc-id is: %s " % disc.id)
            self._tracks = {} ## refresh the tracks-cache

            try:
                service = mbws.WebService()
                query = mbws.Query(service)
                filter = mbws.ReleaseFilter(discId=disc.getId())
                results = query.getReleases(filter)
                if len(results) == 0:
                    raise NoCDFoundError()

                selectedRelease = results[0].release
                inc = mbws.ReleaseIncludes(artist=True, tracks=True, releaseEvents=True)
                release = query.getReleaseById(selectedRelease.getId(), inc)
                self.info("Found CD in Musicbrainz: %s - %s" % (release.artist.getUniqueName(), release.title))
                isSingleArtist = release.isSingleArtistRelease()

                i = 1
                for track in release.tracks:
                    if isSingleArtist:
                        title = release.artist.getUniqueName() + ' - ' +  track.title
                    else:
                        title = track.artist.name + ' - ' +  track.title
                    self.debug( "Processing the Track: %s" % track)
                    (minutes, seconds) = track.getDurationSplit()
                    i+=1
                    self._tracks[i] = MediaUri("cdda://%2d?label=%s (%d:%02d)" % (i, quote(unicode(title)), minutes, seconds))

            except (mbws.WebServiceError, NoCDFoundError):
                #FIXME: raise exception ?
                self.warning("Could not look up the CD, reading it without meta informations")
                index = 1
                for (offset, length) in disc.tracks:
                    track = unicode("cdda://%s?label=Track%d" % (index,index))
                    child_uri = MediaUri(track)
                    self._tracks[index] = child_uri
                    index += 1

            for track in self._tracks.itervalues():
                list_of_children.append(track)

        return list_of_children

Do the URI has children ?

.. code-block:: python

   def has_children(self, uri, callback):
        return self.is_directory(uri)

The following next_location() and previous_location() are called to get the
respective "neighbour" of a given URI. For us it returns respectively the next
and previous audio track on the CD.

.. code-block:: python

   def next_location(self, uri, root=None, at_user_request=False):
        next_uri = None
        device = uri.parent[10:-1]
        track = int(uri.host)
        next_track = track +1

        if self._tracks.has_key(next_track):
            next_uri = self._tracks[next_track]

        return next_uri

    def previous_location(self, uri):
        if self.is_directory(uri): return None

        prev_uri = None
        device = uri.parent[10:-1]
        track = int(uri.host)
        prev_track = track - 1

        if self._tracks.has_key(prev_track):
            prev_uri = self._tracks[prev_track]
        return prev_uri

And finally, we do not support URI monitoring for an Audio CD.

.. code-block:: python

    def uri_is_monitorable(self, uri):
        return False


As you can see, we do not implement the open(), read() and seek() functions on
this component. Those are handled directly by the GStreamer framework. For an
example of a Media Provider implementing the media access function, please refer
to the implementation of local filesystem support, in :

    elisa/plugins/base/media_providers/local_media.py
