Package elisa :: Package plugins :: Package bad :: Package coherence_plugin :: Module upnp_media_server
[hide private]
[frames] | no frames]

Source Code for Module elisa.plugins.bad.coherence_plugin.upnp_media_server

  1  # -*- coding: utf-8 -*- 
  2  # Elisa - Home multimedia server 
  3  # Copyright (C) 2006-2008 Fluendo Embedded S.L. (www.fluendo.com). 
  4  # All rights reserved. 
  5  # 
  6  # This file is available under one of two license agreements. 
  7  # 
  8  # This file is licensed under the GPL version 3. 
  9  # See "LICENSE.GPL" in the root of this distribution including a special 
 10  # exception to use Elisa with Fluendo's plugins. 
 11  # 
 12  # The GPL part of Elisa is also available under a commercial licensing 
 13  # agreement from Fluendo. 
 14  # See "LICENSE.Elisa" in the root directory of this distribution package 
 15  # for details on that license. 
 16   
 17  """ 
 18  UPnP MediaServer 
 19  """ 
 20   
 21  __maintainer__ = "Philippe Normand <philippe@fluendo.com>" 
 22   
 23  from elisa.base_components import service_provider 
 24  from elisa.core import media_uri, log 
 25  from elisa.core import common, component 
 26  from elisa.core.observers.list import ListObserver 
 27  from elisa.core.utils import misc, classinit 
 28  from elisa.core.bus.bus_message import HttpResource, PBReferenceable, \ 
 29       ComponentsLoaded, LocationsList, CoherencePlugin 
 30   
 31  from twisted.internet import defer 
 32  from twisted.web import resource, static, server 
 33  from twisted.spread import pb 
 34  import mimetypes, threading 
 35  import urllib 
 36   
37 -class LocationsObserver(ListObserver):
38
39 - def __init__(self, media_server):
40 ListObserver.__init__(self) 41 self._media_server = media_server
42
43 - def inserted(self, elements, position):
44 for location in elements: 45 self._media_server.add_media_location(location)
46
47 - def removed(self, elements, position):
48 for element in elements: 49 self._media_server.remove_media_location(location)
50
51 -class UPnPResource(object, resource.Resource):
52 """ 53 DOCME 54 """ 55 56 57 __metaclass__ = classinit.ClassInitMeta 58 __classinit__ = classinit.build_properties 59 60 addSlash = True 61
62 - def __init__(self, upnp_ms):
63 resource.Resource.__init__(self) 64 self._upnp_ms = upnp_ms
65
66 - def render(self, request):
67 return """ 68 <html> 69 <head> 70 <title>Elisa UPnP page</title> 71 </head> 72 <body> 73 <h1>UPnP page!</h1> 74 </body> 75 </html> 76 """
77
78 - def getChild(self, path, request):
79 child = self 80 if path != '': 81 media_manager = common.application.media_manager 82 try: 83 media_id = int(path) 84 except ValueError: 85 log.debug('upnp_resource', "wrong media ID: %r", path) 86 else: 87 cm = media_manager.media_db.get_media_with_id(int(path)) 88 uri = media_uri.MediaUri(cm.uri) 89 f = uri.host + uri.path 90 log.debug('upnp_resource', 'serving %r', f) 91 child = static.File(f) 92 return child
93
94 - def ip_port__set(self, value):
95 self._ip_port = value 96 self._upnp_ms.enable_coherence(*value)
97
98 - def ip_port__get(self):
99 return self._ip_port
100
101 -class DeferredResource(resource.Resource):
102
103 - def __init__(self, dfr):
104 resource.Resource.__init__(self) 105 self._dfr = dfr
106
107 - def render(self, request):
108 return server.NOT_DONE_YET
109
110 -class CoverResource(resource.Resource):
111 """ 112 DOCME 113 """ 114 115 addSlash = True 116
117 - def render(self, request):
118 return """ 119 <html> 120 <head> 121 <title>Elisa covers</title> 122 </head> 123 <body> 124 <h1>Get lost :)</h1> 125 </body> 126 </html> 127 """
128
129 - def getChild(self, path, request):
130 child = self 131 params = request.args 132 artist = params.get('artist',[]) 133 album = params.get('album',[]) 134 135 if artist: 136 artist = artist[0] 137 138 if album: 139 album = album[0] 140 141 def got_metadata(metadata): 142 data = "" 143 if 'cover' in metadata and metadata['cover'] is not None: 144 uri = metadata['cover'] 145 f = open(uri.host + uri.path) 146 data = f.read() 147 f.close() 148 149 request.write(data) 150 request.finish()
151 152 if artist and album: 153 metadata_manager = common.application.metadata_manager 154 dfr = metadata_manager.get_metadata({'artist': artist, 155 'album': album, 156 'cover': None}) 157 dfr.addCallback(got_metadata) 158 child = DeferredResource(dfr) 159 160 return child
161 162 163 ALL=0 164 FOLDER=1 165
166 -class UPnPMediaServer(pb.Referenceable, service_provider.ServiceProvider):
167 """ 168 Provides UPnP clients access to UPnP resources 169 """ 170 171 # TODO: should this be translated, too? 172 tree = { '0': ('Elisa', { 173 'A' : ('Music', 174 {'A': ('All', {'content': (ALL, 'audio', None)}), 175 'B': ('By artist', 176 {'content': media_uri.MediaUri('elisa://localhost/artists/')}), 177 'C': ('By album', 178 {'content': media_uri.MediaUri('elisa://localhost/albums/')}), 179 'D': ('By folder', {'content': (FOLDER, 'audio', None)}) 180 }), 181 'B': ('Videos', 182 {'A': ('All', {'content': (ALL, 'video', None)}), 183 'B': ('By folder', {'content': (FOLDER, 'video', None)}), 184 }), 185 'C': ('Pictures', 186 {'A': ('All', {'content': (ALL, 'image', None)}), 187 'B': ('By folder', {'content': (FOLDER, 'image', None)}), 188 }), 189 }) 190 } 191
192 - def __init__(self):
193 service_provider.ServiceProvider.__init__(self) 194 self.container_id = 1 195 self.http_upnp_path = "data/upnp" 196 self._upnp_resource = UPnPResource(self) 197 self._media_locations = {'audio':[], 'video':[], 'image':[]} 198 self._lock = threading.Lock() 199 self._locations_observable = None 200 self._locations_observer = LocationsObserver(self)
201
202 - def initialize(self):
205
206 - def _bus_message_received(self, msg, sender):
207 if isinstance(msg, ComponentsLoaded): 208 # register the cover HTTP resource to the HTTP server 209 msg = HttpResource("data/covers", CoverResource()) 210 common.application.bus.send_message(msg) 211 212 # register the UPnPResource to the HTTP server, via the Message bus 213 msg = HttpResource(self.http_upnp_path, self._upnp_resource) 214 common.application.bus.send_message(msg) 215 216 # register myself to the PB service 217 msg = PBReferenceable('get_cache_manager', self) 218 common.application.bus.send_message(msg) 219 else: 220 self._locations_observable = msg.locations 221 for location in self._locations_observable: 222 self.add_media_location(location) 223 self._locations_observable.add_observer(self._locations_observer)
224
225 - def clean(self):
226 if self._locations_observable: 227 self._locations_observable.remove_observer(self._locations_observer) 228 service_provider.ServiceProvider.clean(self)
229
230 - def add_media_location(self, location):
231 # TODO: add support in the http server to serve daap and other 232 # URI schemes we correctly handle elsewhere in Elisa 233 if location.uri.scheme not in ('elisa', 'file'): 234 self.debug("Unsupported URI scheme for %r media location", 235 location.uri) 236 else: 237 self.info("Adding media location: %r with media_types %r", 238 location.uri, location.media_types) 239 for media_type in location.media_types: 240 self._media_locations[media_type].append(location.uri)
241
242 - def remove_media_location(self, location):
243 self.info("Removing media location %r with media_types %r", 244 location.uri, location.media_types) 245 for media_type in location.media_types: 246 if location.uri in self._media_locations[media_type]: 247 self._media_locations[media_type].remove(location.uri)
248
249 - def enable_coherence(self, host, port):
250 # add a new plugin in Coherence 251 # 'version': 2 252 args = {'name': 'Elisa medias', 253 'host': host, } 254 msg = CoherencePlugin('ElisaMediaStore', args) 255 common.application.bus.send_message(msg)
256
257 - def _next_id(self):
258 try: 259 self._lock.acquire() 260 self.container_id += 1 261 finally: 262 self._lock.release() 263 return self.container_id
264 265 @defer.deferredGenerator
266 - def _got_children(self, children, parent):
267 self.debug("Got %r", children) 268 media_manager = common.application.media_manager 269 db = media_manager.media_db 270 nodes = [] 271 272 def got_result(is_directory, child_uri): 273 item = {} 274 if is_directory: 275 container_id = "%s-%s" % (parent, self._next_id()) 276 container_name = child_uri.label 277 size = 1 278 item = {'id': container_id, 279 'parent_id': parent, 280 'name': container_name, 281 'mimetype': 'directory', 282 'size': size, 283 'children': [], 284 'location': {}, 285 'content': child_uri 286 } 287 else: 288 node = db.get_media_information(child_uri, extended=True) 289 if node: 290 item = self._node_to_dict(node, parent) 291 item['content'] = child_uri 292 else: 293 self.debug("%r not found in DB. Can't serve it", child_uri) 294 return item
295 296 if db: 297 for child in children: 298 d = media_manager.is_directory(child[0]) 299 d.addCallback(got_result, child[0]) 300 wfd = defer.waitForDeferred(d) 301 yield wfd 302 node = wfd.getResult() 303 if node: 304 nodes.append(node) 305 306 yield nodes
307
308 - def _db_filter(self, kind, media_type, parent):
309 nodes = [] 310 if kind == ALL: 311 db = common.application.media_manager.media_db 312 if db: 313 for media_node in db.get_medias(media_type=media_type): 314 node_dict = self._node_to_dict(media_node, parent) 315 node_dict['content'] = media_uri.MediaUri(media_node.uri) 316 nodes.append(node_dict) 317 elif kind == FOLDER: 318 locations = self._media_locations[media_type] 319 for location in locations: 320 container_id = "%s-%s" % (parent, self._next_id()) 321 container_name = location.label 322 size = 1 323 item = {'id': container_id, 324 'parent_id': parent, 325 'name': container_name, 326 'mimetype': 'directory', 327 'size': size, 328 'children': [], 329 'location': {}, 330 'content': (FOLDER, media_type, location) 331 } 332 nodes.append(item) 333 return nodes
334
335 - def _get_containers(self, tree, parent=None):
336 media_manager = common.application.media_manager 337 338 def fill_cache(nodes): 339 for node in nodes: 340 uri = node['content'] 341 del node['content'] 342 path = node['id'].split('-') 343 tree[path[-1]] = (node['name'],{'content': uri}) 344 return nodes
345 346 if 'content' in tree: 347 348 if isinstance(tree['content'], media_uri.MediaUri): 349 uri = tree['content'] 350 children = [] 351 self.debug("Retrieving children of %r", uri) 352 dfr = media_manager.get_direct_children(uri, 353 children) 354 dfr.addCallback(self._got_children, parent) 355 dfr.addCallback(fill_cache) 356 else: 357 kind, media_type, uri = tree['content'] 358 if uri: 359 children = [] 360 dfr = media_manager.get_direct_children(uri, 361 children) 362 dfr.addCallback(self._got_children, parent) 363 dfr.addCallback(fill_cache) 364 else: 365 nodes = self._db_filter(kind, media_type, parent) 366 nodes = fill_cache(nodes) 367 dfr = defer.Deferred() 368 dfr.callback(nodes) 369 370 else: 371 containers = [] 372 for container_id, container in tree.iteritems(): 373 sub_tree = container[1] 374 container_name = container[0] 375 size = len(sub_tree.keys()) 376 container_id = "%s-%s" % (parent, container_id) 377 node_dict = {'id': container_id, 378 'parent_id': parent, 379 'name': container_name, 380 'mimetype': 'directory', 381 'size': size, 382 'children': [], 383 'location': {} 384 } 385 containers.append(node_dict) 386 387 dfr = defer.Deferred() 388 dfr.callback(containers) 389 390 return dfr 391
392 - def _node_to_dict(self, node, parent_id):
393 # try to guess the uri mime-type 394 node_dict = {} 395 if node: 396 mimetype, sub_type = mimetypes.guess_type(node.uri) 397 if not mimetype: 398 # fallback to incomplete mime-type based on node.format 399 mimetype = '%s/' % node.format 400 401 ip, port = self._upnp_resource.ip_port 402 external_location = 'http://%s:%s/%s/%s' % (ip, port, 403 self.http_upnp_path, 404 node.id) 405 self.debug(external_location) 406 407 cover_uri = None 408 if 'artist' in node.keys() and 'album' in node.keys(): 409 artist = media_uri.quote(node['artist']) 410 album = media_uri.quote(node['album']) 411 cover_uri = 'http://%s:%s/%s/cover.jpg?artist=%s&album=%s' 412 cover_uri = cover_uri % (ip, port, "data/covers", 413 artist, album) 414 self.debug(cover_uri) 415 416 internal_location = node.uri 417 node_dict = {'id': "%s-%s" % (parent_id,self._next_id()), 418 'parent_id': parent_id, 419 'name': node.short_name, 420 'mimetype': mimetype, 421 'size': 0, 422 'cover': cover_uri, 423 'children': [], 424 'location': {'internal': internal_location, 425 'external': external_location} 426 } 427 return node_dict
428
429 - def remote_get_media_root_id(self, media_type):
430 db = common.application.media_manager.media_db 431 if db: 432 medias = db.get_medias() 433 for media in medias: 434 if media.typ == media_type: 435 return media.source_id 436 return 0
437
438 - def remote_get_media_node_with_id(self, container_id):
439 """ 440 Returns a dict with following keys: 441 id = id in the media db 442 parent_id = parent_id in the media db 443 name = title, album name or basename 444 mimetype = 'directory' or real mimetype 445 size = in bytes (??) 446 children = list of objects for which this item is the parent 447 location = filesystem path if item is a file 448 """ 449 result = {} 450 451 path = container_id.split('-') 452 if len(path) > 1: 453 parent_id = '-'.join(path[:-1]) 454 else: 455 parent_id = '0' 456 457 self.debug("Looking for container %r with parent %r", container_id, 458 parent_id) 459 i = 0 460 c = None 461 tree = self.tree 462 container_name = '' 463 464 for c_id in path: 465 c = tree[c_id][1] 466 container_name = tree[c_id][0] 467 tree = c 468 469 self.debug("Looking in container: %r", tree) 470 471 def got_containers(child_list): 472 size = len(child_list) 473 container = {'id': container_id, 474 'parent_id': parent_id, 475 'name': container_name, 476 'mimetype': 'directory', 477 'size': size, 478 'children': child_list, 479 'location': {} 480 } 481 return container
482 483 if c: 484 result = self._get_containers(c, container_id) 485 result.addCallback(got_containers) 486 return result 487