Package elisa :: Package plugins :: Package bad :: Package daap_plugin :: Module daap_media
[hide private]
[frames] | no frames]

Source Code for Module elisa.plugins.bad.daap_plugin.daap_media

  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  DAAP MediaProvider component 
 19  """ 
 20   
 21   
 22  __maintainer__ = 'Benjamin Kampmann <benjamin@fluendo.com>' 
 23  __maintainer2__ = 'Philippe Normand <philippe@fluendo.com>' 
 24  __maintainer3__ = 'Alessandro Decina <alessandro@fluendo.com>' 
 25   
 26  from elisa.base_components.media_provider import MediaProvider 
 27  from elisa.core import media_uri 
 28  from elisa.core import common 
 29  from elisa.core.bus import bus_message 
 30  from elisa.core.component import InitializeFailure 
 31  from elisa.core.media_uri import MediaUri 
 32  from elisa.core.observers.dict import DictObservable 
 33  from elisa.extern.natural_sort import natcasecmp 
 34  from elisa.extern.odict import SequenceOrderedDict 
 35   
 36  import socket 
 37  import gst 
 38  import gobject 
 39   
 40  import daap 
 41   
 42  try: 
 43      import dbus 
 44      if getattr(dbus, 'version', (0,0,0)) >= (0,41,0): 
 45          import dbus.glib 
 46  except ImportError: 
 47      dbus = None 
 48   
 49  if dbus: 
 50      try: 
 51          import avahi 
 52      except ImportError: 
 53          avahi = None 
 54  else: 
 55      avahi = None 
 56   
 57  from twisted.internet import defer, threads 
 58   
 59  """ 
 60  TODO: 
 61   
 62  - add a "copy to local library" feature? 
 63  """ 
 64   
65 -class DaapSource(gst.BaseSrc):
66 __gsttemplates__ = ( 67 gst.PadTemplate("src", 68 gst.PAD_SRC, 69 gst.PAD_ALWAYS, 70 gst.caps_new_any()), 71 ) 72 73 __gstdetails__ = ("DAAP plugin", "Foo/Bar", "Read data on DAAP shares", 74 "Philippe Normand <philippe@fluendo.com>, " 75 "Alessandro Decina <alessandro@fluendo.com>") 76 77 blocksize = 4096 78
79 - def __init__(self):
80 super(DaapSource, self).__init__() 81 self.curoffset = 0 82 self._libs = {} 83 self.client = None 84 self.session = None 85 self.response = None 86 self.track = None 87 self.data = None
88 89 @classmethod
90 - def do_get_type(cls):
91 return gst.URI_SRC
92 93 @classmethod
94 - def do_get_protocols(cls):
95 return ["daap"]
96
97 - def do_set_uri(self, uri):
98 if not uri.startswith('daap://'): 99 return False 100 101 self.uri = uri 102 return True
103
104 - def do_get_uri(self):
105 return self.uri
106 107 # set_uri and get_uri are just syntactic sugar for do_set_uri and 108 # do_get_uri
109 - def set_uri(self, uri):
110 return self.do_set_uri(uri)
111
112 - def get_uri(self):
113 return self.do_get_uri()
114
115 - def do_start(self):
116 m_uri = media_uri.MediaUri(self.uri) 117 host = m_uri.host 118 port = int(m_uri.port or '3689') 119 120 self.client = daap.DAAPClient() 121 self.client.connect(host, port) 122 self.session = self.client.login() 123 library = self.session.library() 124 track_id = int(m_uri.get_param('id','1')) 125 self.track = None 126 for track in library.tracks(): 127 if track.id == track_id: 128 self.track = track 129 break 130 131 if track is None: 132 return False 133 134 return True
135
136 - def do_stop(self):
137 self.session.logout() 138 self.client.socket.close() 139 self.client = None 140 self.session = None 141 self.track = None 142 self.data = None 143 144 return True
145
146 - def do_check_get_range(self):
147 return False
148
149 - def do_create(self, offset, length):
150 if self.data is None: 151 self.data = self.track.request() 152 data = self.data.read(self.blocksize) 153 if data: 154 return gst.FLOW_OK, gst.Buffer(data) 155 else: 156 self.data = None 157 return gst.FLOW_UNEXPECTED, None
158 159 # define this here so we can use it in the unittest without having to create the 160 # media provider
161 -def register_daap_source (name):
162 if gst.gst_version < (0, 10, 13): 163 raise InitializeFailure(name, 164 "The installed gst version doesn't support python plugins, " 165 "daap support will not be available.") 166 167 gobject.type_register(DaapSource) 168 if gst.pygst_version <= (0, 10, 8): 169 # use our workaround 170 try: 171 import _daap_uri_interface 172 except ImportError: 173 raise InitializeFailure(name, 174 "The installed pygst version doesn't support the " 175 "GstURIHandler interface and the custom GstURIInterface " 176 "wrapper has not been built") 177 178 # only register the first time (ComponentTestCase creates the daap 179 # provider multiple times, so we have to check this) 180 if gst.URIHandler.__gtype__ not in DaapSource.__gtype__.interfaces: 181 _daap_uri_interface.bind_to(DaapSource) 182 183 gst.element_register(type=DaapSource, elementname="daapsrc", 184 rank=gst.RANK_PRIMARY)
185
186 -class AvahiMonitor:
187
188 - def __init__(self):
189 self._callbacks = {'new-service': [], 190 'remove-service': [] 191 } 192 self.bus = dbus.SystemBus() 193 avahi_bus = self.bus.get_object(avahi.DBUS_NAME, avahi.DBUS_PATH_SERVER) 194 self.server = dbus.Interface(avahi_bus, avahi.DBUS_INTERFACE_SERVER) 195 196 stype = '_daap._tcp' 197 domain = 'local' 198 self._plugged = {} 199 avahi_browser = self.server.ServiceBrowserNew(avahi.IF_UNSPEC, 200 avahi.PROTO_UNSPEC, 201 stype, domain, 202 dbus.UInt32(0)) 203 obj = self.bus.get_object(avahi.DBUS_NAME, avahi_browser) 204 self.browser = dbus.Interface(obj, avahi.DBUS_INTERFACE_SERVICE_BROWSER)
205
206 - def start(self):
207 self.browser.connect_to_signal('ItemNew', self.new_service) 208 self.browser.connect_to_signal('ItemRemove', self.remove_service)
209
210 - def stop(self):
211 self.bus.close()
212
213 - def new_service(self, interface, protocol, name, type, domain, flags):
214 service = self.server.ResolveService(interface, protocol, 215 name, type, domain, 216 avahi.PROTO_UNSPEC, dbus.UInt32(0)) 217 218 address, port = service[-4:-2] 219 name = unicode(service[2]) 220 for cb in self._callbacks['new-service']: 221 self._plugged[name] = (address,port) 222 cb(name, address, port)
223
224 - def remove_service(self, interface, protocol, name, type, domain,server):
225 address, port = self._plugged[name] 226 for cb in self._callbacks['remove-service']: 227 cb(name, address, port)
228 229
230 - def add_callback(self, sig_name, callback):
231 self._callbacks[sig_name].append(callback)
232
233 - def remove_callback(self, sig_name, callback):
234 self._callback[sig_name].remove(callback)
235
236 -class DaapArtist(SequenceOrderedDict):
237 - def __init__(self, name):
238 self.name = name 239 240 super(DaapArtist, self).__init__()
241
242 -class DaapAlbum(SequenceOrderedDict):
243 - def __init__(self, name):
244 self.name = name 245 246 super(DaapAlbum, self).__init__()
247
248 -class DaapTrack(object):
249 - def __init__(self, track_id, name):
250 self.id = track_id 251 self.name = name 252 253 super(DaapTrack, self).__init__()
254
255 -class DaapMedia(MediaProvider):
256
257 - def __init__(self):
258 MediaProvider.__init__(self) 259 self._libraries = {} 260 self._monitor = None
261
262 - def initialize(self):
265
266 - def _initialize_daap_src(self):
268
269 - def _initialize_avahi(self):
270 if not dbus: 271 if not avahi: 272 self.warning("Please install python-avahi to get Avahi support") 273 self.warning("python-dbus is missing...") 274 self.warning("Avahi support disabled. DAAP shares won't appear automagically") 275 else: 276 try: 277 self._monitor = AvahiMonitor() 278 self._monitor.add_callback('new-service', self._add_location) 279 self._monitor.add_callback('remove-service', 280 self._remove_location) 281 self._monitor.start() 282 except Exception, ex: 283 error_msg = "Couldn't initialize Avahi monitor: %s" % str(ex) 284 raise InitializeFailure(self.name, error_msg)
285
286 - def _add_location(self, name, address, port):
287 self.info("New DAAP share at %s:%s : %s" % (address, port, name)) 288 uri = 'daap://%s:%s/' % (address, port) 289 action_type = bus_message.LocalNetworkLocation.ActionType.LOCATION_ADDED 290 msg = bus_message.LocalNetworkLocation(action_type, name, 'daap', 291 uri, ['audio',], 292 theme_icon='network_share_icon') 293 common.application.bus.send_message(msg)
294
295 - def _remove_location(self, name, address, port):
296 self.info("DAAP share at %s:%s disappeared" % (address, port)) 297 uri = 'daap://%s:%s/' % (address, port) 298 action_type = bus_message.LocalNetworkLocation.ActionType.LOCATION_REMOVED 299 msg = bus_message.LocalNetworkLocation(action_type, name, 'daap', 300 uri, ['audio',]) 301 common.application.bus.send_message(msg)
302
303 - def _get_library(self, uri):
304 self.debug("Retrieving DAAP library at %r", uri) 305 306 host = uri.host 307 if uri.port: 308 port = int(uri.port) 309 else: 310 port = 3689 311 312 address = (host, port) 313 if address not in self._libraries: 314 connection = daap.DAAPClient() 315 try: 316 connection.connect(*address) 317 except Exception, exc: 318 # FIXME: prezise the Exception 319 self.info("Couldn't connect to %s : %s" % (address,str(exc))) 320 else: 321 try: 322 # auth isn't supported yet. Just log in 323 session = connection.login() 324 library = session.library() 325 326 self._libraries[address] = self._build_library(library.tracks()) 327 except (socket.timeout, socket.error): 328 pass 329 except daap.DAAPError, error: 330 msg = "Error login into %s: %s" % (address, str(error)) 331 self.warning(msg) 332 raise daap.DAAPError(msg) 333 334 return self._libraries.get(address)
335
336 - def _build_library(self, tracks):
337 lib = SequenceOrderedDict() 338 for track in tracks: 339 if None in (track.artist, track.album): 340 continue 341 if not lib.get(track.artist): 342 lib[track.artist] = DaapArtist(track.artist) 343 if not lib[track.artist].get(track.album): 344 lib[track.artist][track.album] = DaapAlbum(track.album) 345 if not lib[track.artist][track.album].get(track.id): 346 lib[track.artist][track.album][track.id] = \ 347 DaapTrack(track.id, track.name) 348 349 for artist_name, album in lib.iteritems(): 350 for album_name, tracks in album.iteritems(): 351 tracks.sort(natcasecmp, key=lambda key: tracks[key].name) 352 album.sort(natcasecmp) 353 lib.sort(natcasecmp) 354 355 return lib
356
358 return []
359
361 return {'daap' : 0}
362
363 - def _blocking_get_media_type(self, uri):
364 if not uri.get_param('id'): 365 media_type = {'file_type': 'directory', 'mime_type': ''} 366 else: 367 # DAAP only supports audio media, see DMAP for picture media 368 media_type = {'file_type': 'audio', 'mime_type': ''} 369 return media_type
370
371 - def _blocking_is_directory(self, uri):
372 return not uri.get_param('id')
373
374 - def get_direct_children(self, uri, l):
375 d = threads.deferToThread(self._blocking_get_direct_children, uri, l) 376 return d
377
378 - def _blocking_get_direct_children(self, uri, children):
379 self.debug("Retrieving children of: %s", uri) 380 lib = None 381 try: 382 lib = self._get_library(uri) 383 except daap.DAAPError, error: 384 self.warning(error) 385 386 if lib: 387 artist = uri.get_param('artist', None) 388 album = uri.get_param('album', None) 389 track_id = uri.get_param('track', None) 390 self.debug("URI artist and album are: %r - %r" % (artist,album)) 391 392 if track_id: 393 # no children 394 pass 395 elif artist and album: 396 # process songs of given album of the artist 397 for track_id, track in lib.get(artist).get(album).iteritems(): 398 track = track.name 399 metadata = DictObservable() 400 child_uri = media_uri.MediaUri(uri.parent) 401 child_dict = {'id': track_id, 'artist': artist, 402 'album': album, 'track': track} 403 child_uri.set_params(child_dict) 404 child_uri.label = track 405 ## FIXME: remove this ugly hack: 406 ## UGLY HACK, so that the albums are not shown for 407 ## track-items 408 metadata['song_artist'] = artist 409 metadata['song_album'] = album 410 metadata['song'] = track 411 self.debug("Appendind %r with metadata %s" % (child_uri, 412 metadata)) 413 children.append((child_uri, metadata)) 414 elif artist: 415 # list albums of given artist 416 for album_name, album in lib.get(artist,{}).iteritems(): 417 metadata = DictObservable() 418 child_uri = media_uri.MediaUri(uri.parent) 419 metadata['artist'] = artist 420 metadata['album'] = album_name 421 ## Don't use a DictObservable for params! 422 child_uri.set_params({'artist' : artist, 423 'album' : album_name}) 424 child_uri.label = album_name 425 self.debug("Appendind %r with metadata %s" % (child_uri, 426 metadata)) 427 428 children.append((child_uri, metadata)) 429 else: 430 # list artists 431 for artist in lib.iterkeys(): 432 metadata = DictObservable() 433 child_uri = media_uri.MediaUri(uri.parent) 434 child_uri.set_params({'artist': artist}) 435 child_uri.label = artist 436 metadata['artist'] = artist 437 self.debug("Appendind %r with metadata %s" % (child_uri, 438 metadata)) 439 children.append((child_uri,metadata)) 440 441 self.debug("Retrieved children: %r", children) 442 return children
443
444 - def next_location(self, uri, root=None):
445 d = threads.deferToThread(self._blocking_next_location, uri, root) 446 return d
447
448 - def _blocking_next_location(self, uri, root):
449 artist_name = uri.get_param('artist') 450 album_name = uri.get_param('album') 451 track_id = uri.get_param('id') 452 if track_id: 453 track_id = int(track_id) 454 455 artist = None 456 album = None 457 track = None 458 459 lib = self._get_library(uri) 460 461 if artist_name: 462 artist = lib[artist_name] 463 464 if album_name: 465 album = artist[album_name] 466 467 while True: 468 if artist is None: 469 # start from the first artist 470 artist = lib.values[0] 471 472 if album is None: 473 # start from the first album of the current artist 474 album = artist.values[0] 475 476 if not track_id: 477 # start from the first track of the selected album of the 478 # selected artist 479 track = album.values[0] 480 481 # we have the track 482 break 483 484 track_index = album.index(track_id) 485 if track_index == len(album) - 1: 486 # the current track is the last in the album, we must 487 # find the next album if the current album is not the root of 488 # our search 489 490 if album.name == root.get_param('album'): 491 break 492 493 # reset the track_id so in the next iteration the first track of 494 # the next album will be picked 495 track_id = None 496 497 album_index = artist.index(album.name) 498 if album_index == len(artist) - 1: 499 # last album, find the next artist if the current one is no 500 # the root of the search 501 502 if artist.name == root.get_param('artist'): 503 break 504 505 # reset the album so in the next iteration the first album 506 # of the next artist will be picked 507 album = None 508 509 artist_index = lib.index(artist.name) 510 if artist_index == len(lib) - 1: 511 # no more tracks 512 break 513 else: 514 artist = lib.values[artist_index + 1] 515 else: 516 album = artist.values[album_index + 1] 517 else: 518 track = album.values[track_index + 1] 519 break 520 521 if track is None: 522 return None 523 524 uri = MediaUri(uri) 525 child_dict = {'id': track.id, 'artist': artist.name, 526 'album': album.name, 'track': track.name} 527 uri.label = track.name 528 uri.set_params(child_dict) 529 return uri
530
531 - def _blocking_has_children_with_types(self, uri, media_types):
532 if uri.get_param('id'): 533 # a track 534 return False 535 536 return bool(set(['audio', 'directory']).intersection(media_types))
537