Coverage for coherence/backends/lolcats_storage.py : 0%

Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
# -*- coding: utf-8 -*-
# Licensed under the MIT license # http://opensource.org/licenses/mit-license.php
# Copyright 2008, Benjamin Kampmann <ben.kampmann@googlemail.com>
""" This is a Media Backend that allows you to access the cool and cute pictures from lolcats.com. This is mainly meant as a Sample Media Backend to learn how to write a Media Backend.
So. You are still reading which allows me to assume that you want to learn how to write a Media Backend for Coherence. NICE :) .
Once again: This is a SIMPLE Media Backend. It does not contain any big requests, searches or even transcoding. The only thing we want to do in this simple example, is to fetch a rss link on startup, parse it, save it and restart the process one hour later again. Well, on top of this, we also want to provide these informations as a Media Server in the UPnP/DLNA Network of course ;) .
Wow. You are still reading. You must be really interested. Then let's go. """
# NOTE: # Please don't complain about the coding style of this document - I know. # It is just this way to make it easier to document and to understand.
# THE IMPORTS # And to parse the RSS-Data (which is XML), we use lxml.etree.fromstring from lxml.etree import fromstring
# And we also import the reactor, # that allows us to specify an action to happen later from twisted.internet import reactor
# The data itself is stored in BackendItems. # They are also the first things we are going to create. from coherence.backend import BackendItem
# The entry point for each kind of Backend is a 'BackendStore'. # The BackendStore is the instance that does everything Usually. # In this Example it can be understood as the 'Server', the object retrieving # and serving the data. from coherence.backend import BackendStore
# To make the data 'renderable' we need to define the DIDLite-Class # of the Media we are providing. For that we have a bunch of helpers that we # also want to import from coherence.upnp.core import DIDLLite
# Coherence relies on the Twisted backend. I hope you are familiar with the # concept of deferreds. If not please read: # http://twistedmatrix.com/projects/core/documentation/howto/async.html # # It is a basic concept that you need to understand the following # code. But why am I talking about it? Oh, right, because we use a http-client # based on the twisted.web.client module to do our requests. from coherence.upnp.core.utils import getPage
# THE MODELS # After the download and parsing of the data is done, we want to save it. In # this case, we want to fetch the images and store their URL and the title of # the image. That is the LolcatsImage class:
class LolcatsImage(BackendItem): # We inherit from BackendItem as it already contains a lot of # helper methods and implementations. For this simple example, # we only have to fill the item with data.
def __init__(self, parent_id, id, title, url): BackendItem.__init__(self) self.parentid = parent_id # used to be able to 'go back'
self.update_id = 0
self.id = id # each item has its own and unique id
self.location = url # the url of the picture
self.name = title # the title of the picture. Inside # coherence this is called 'name'
# Item.item is a special thing. This is used to explain the client what # kind of data this is. For e.g. A VideoItem or a MusicTrack. In our # case, we have an image. self.item = DIDLLite.ImageItem(id, parent_id, self.name)
# each Item.item has to have one or more Resource objects these hold # detailed information about the media data and can represent variants # of it (different sizes, transcoded formats) res = DIDLLite.Resource(self.location, 'http-get:*:image/jpeg:*') res.size = None # FIXME: we should have a size here # and a resolution entry would be nice too self.item.res.append(res)
class LolcatsContainer(BackendItem): # The LolcatsContainer will hold the reference to all our LolcatsImages. # This kind of BackenedItem is a bit different from the normal BackendItem, # because it has 'children' (the lolcatsimages). Because of that we have # some more stuff to do in here.
def __init__(self, parent_id, id): BackendItem.__init__(self) # the ids as above self.parent_id = parent_id self.id = id
# we never have a different name anyway self.name = 'LOLCats'
# but we need to set it to a certain mimetype to explain it, that we # contain 'children'. self.mimetype = 'directory'
# As we are updating our data periodically, we increase this value so # that our clients can check easier if something has changed # since their last request. self.update_id = 0
# that is where we hold the children self.children = []
# and we need to give a DIDLLite again. This time we want to be # understood as 'Container'. self.item = DIDLLite.Container(id, parent_id, self.name)
self.item.childCount = None # will be set as soon as we have images
def get_children(self, start=0, end=0): # This is the only important implementation thing: # we have to return our list of children if end != 0: return self.children[start:end] return self.children[start:]
# there is nothing special in here # FIXME: move it to a base BackendContainer class def get_child_count(self): return len(self.children)
def get_item(self): return self.item
def get_name(self): return self.name
def get_id(self): return self.id
# THE SERVER # As already said before the implementation of the server is done in an # inheritance of a BackendStore. This is where the real code happens (usually). # In our case this would be: downloading the page, parsing the content, saving # it in the models and returning them on request.
class LolcatsStore(BackendStore): # this *must* be set. Because the (most used) MediaServer Coherence also # allows other kind of Backends (like remote lights). implements = ['MediaServer']
# This is only for this implementation: the http link to the lolcats rss # feed that we want to read and parse: rss_url = "http://feeds.feedburner.com/ICanHasCheezburger?format=xml"
# As we are going to build a (very small) tree with the items, we need to # define the first (the root) item: ROOT_ID = 0
def __init__(self, server, *args, **kwargs): # First we initialize our heritage BackendStore.__init__(self, server, **kwargs)
# When a Backend is initialized, the configuration is given as keyword # arguments to the initialization. We receive it here as a dictionary # and allow some values to be set: # the name of the MediaServer as it appears in the network self.name = kwargs.get('name', 'Lolcats')
# timeout between updates in hours: self.refresh = int(kwargs.get('refresh', 1)) * (60 * 60)
# the UPnP device that's hosting that backend, that's already done # in the BackendStore.__init__, just left here the sake of completeness self.server = server
# internally used to have a new id for each item self.next_id = 1000
# we store the last update from the rss feed so that we know # if we have to parse again, or not: self.last_updated = None
# initialize our lolcats container (no parent, this is the root) self.container = LolcatsContainer(None, self.ROOT_ID)
# but as we also have to return them on 'get_by_id', we have our local # store of images per id: self.images = {}
# we tell that if an XBox sends a request for images we'll # map the WMC id of that request to our local one self.wmc_mapping = {'16': 0}
# and trigger an update of the data dfr = self.update_data()
# So, even though the initialize is kind of done, # Coherence does not yet announce our Media Server. # Coherence does wait for signal send by us that we are ready now. # And we don't want that to happen as long as we don't have succeeded # in fetching some first data, so we delay this signaling after # the update is done: dfr.addCallback(self.init_completed) dfr.addCallback(self.queue_update)
def get_by_id(self, id): print("asked for", id, type(id)) # what ever we are asked for, # we want to return the container only if isinstance(id, str): id = id.split('@', 1)[0] elif isinstance(id, bytes): id = id.decode('utf-8').split('@', 1)[0] if int(id) == self.ROOT_ID: return self.container return self.images.get(int(id), None)
def upnp_init(self): # After the signal was triggered, # this method is called by coherence and # from now on self.server is existing and we can do the # necessary setup here that allows us to specify our server # options in more detail.
# Here we define what kind of media content we do provide # mostly needed to make some naughty DLNA devices behave # will probably move into Coherence internals one day self.server.connection_manager_server.set_variable( 0, 'SourceProtocolInfo', ['http-get:*:image/jpeg:DLNA.ORG_PN=JPEG_TN;' 'DLNA.ORG_OP=01;DLNA.ORG_FLAGS=00f00000000000000000000000000000', 'http-get:*:image/jpeg:DLNA.ORG_PN=JPEG_SM;' 'DLNA.ORG_OP=01;DLNA.ORG_FLAGS=00f00000000000000000000000000000', 'http-get:*:image/jpeg:DLNA.ORG_PN=JPEG_MED;' 'DLNA.ORG_OP=01;DLNA.ORG_FLAGS=00f00000000000000000000000000000', 'http-get:*:image/jpeg:DLNA.ORG_PN=JPEG_LRG;' 'DLNA.ORG_OP=01;DLNA.ORG_FLAGS=00f00000000000000000000000000000', 'http-get:*:image/jpeg:*'])
# and as it was done after we fetched the data the first time # we want to take care about the server wide updates as well self._update_container()
def _update_container(self, result=None): # we need to inform Coherence about these changes # again this is something that will probably move # into Coherence internals one day if self.server: self.server.content_directory_server.set_variable( 0, 'SystemUpdateID', self.update_id) value = (self.ROOT_ID, self.container.update_id) self.server.content_directory_server.set_variable( 0, 'ContainerUpdateIDs', value) return result
def update_loop(self): # in the loop we want to call update_data dfr = self.update_data() # aftert it was done we want to take care about updating # the container dfr.addCallback(self._update_container) # in ANY case queue an update of the data dfr.addBoth(self.queue_update)
def update_data(self): # trigger an update of the data
# fetch the rss dfr = getPage(self.rss_url)
# push it through our xml parser dfr.addCallback(fromstring)
# then parse the data into our models dfr.addCallback(self.parse_data)
return dfr
def parse_data(self, root): # from there, we look for the newest update and compare it with the one # we have saved. If they are the same, we don't need to go on: pub_date = root.find('./channel/lastBuildDate').text
if pub_date == self.last_updated: return
# not the case, set this as the last update and continue self.last_updated = pub_date
# and reset the children list of the container and the local storage self.container.children = [] self.images = {}
# Attention, as this is an example, this code is meant to be as simple # as possible and not as efficient as possible. IMHO the following code # pretty much sucks, because it is totally blocking # (even though we have 'only' 20 elements)
# we go through our entries and do something specific to the # lolcats-rss-feed to fetch the data out of it url_item = './{http://search.yahoo.com/mrss/}content' for item in root.findall('./channel/item'): title = item.find('./title').text try: url = item.findall(url_item)[1].get('url', None) except IndexError: continue
if url is None: continue
image = LolcatsImage(self.ROOT_ID, self.next_id, title, url) self.container.children.append(image) self.images[self.next_id] = image
# increase the next_id entry every time self.next_id += 1
# and increase the container update id and the system update id # so that the clients can refresh with the new data self.container.update_id += 1 self.update_id += 1
def queue_update(self, error_or_failure): # We use the reactor to queue another updating of our data print(error_or_failure) reactor.callLater(self.refresh, self.update_loop) |