4.2.7r1
[enigma2.git] / usr / lib / enigma2 / python / Plugins / SystemPlugins / UPnP / UPnPCore.py
1 # -*- coding: UTF-8 -*-
2 from Components.ResourceManager import resourcemanager
3 from Components.PluginComponent import plugins
4
5 from coherence.base import Coherence
6 from coherence.upnp.core import DIDLLite
7 from coherence.upnp.devices.control_point import ControlPoint
8 from coherence.upnp.devices.media_renderer import MediaRenderer
9 from coherence.upnp.devices.media_server import MediaServer
10 from coherence.upnp.devices.media_server_client import MediaServerClient
11 from HTMLParser import HTMLParser
12 from Plugins.Plugin import PluginDescriptor
13 from Tools.Log import Log
14
15 class Statics:
16         CONTAINER_ID_ROOT = 0
17         CONTAINER_ID_SERVERLIST = -1
18
19         ITEM_TYPE_AUDIO = "audio"
20         ITEM_TYPE_CONTAINER = "container"
21         ITEM_TYPE_PICTURE = "picture"
22         ITEM_TYPE_SERVER = "server"
23         ITEM_TYPE_FILE = "file"
24         ITEM_TYPE_VIDEO = "video"
25
26         META_ALBUM = 'album'
27         META_ALBUM_ART_URI = 'album_art_uri'
28         META_ARTIST = 'artist'
29         META_BITRATE = 'bitrate'
30         META_CHILD_COUNT = 'child_count'
31         META_DATE = 'date'
32         META_DURATION = 'duration'
33         META_GENRE = 'genre'
34         META_METATYPE = 'metatype'
35         META_RESOLUTION = 'resolution'
36         META_SIZE = 'size'
37         META_TITLE = 'title'
38         META_TYPE = 'type'
39         META_URI = 'uri'
40         META_COVER_URI = 'cover_uri'
41
42         SORT_TITLE_ASC = "+dc:title"
43         SORT_TITLE_DSC = "+dc:title"
44
45
46 def hs(intval):
47         """
48         Convert an int into a 2-character hex string without the 0x prepended
49         e.g.: 1 -> 01
50         """
51         return "%0.2X" %(intval)
52
53 class DLNA(object):
54         """
55         DLNA.ORG_CI: conversion indicator parameter (boolean integer)
56         0 not transcoded
57         1 transcoded
58         """
59         DLNA_ORG_CONVERSION_KEY = "DLNA_ORG.CI"
60         DLNA_ORG_CONVERSION_NONE = 0,
61         DLNA_ORG_CONVERSION_TRANSCODED = 1,
62
63         """
64         DLNA.ORG_OP: operations parameter (string)
65         "00" (or "0") neither time seek range nor range supported
66         "01" range supported
67         "10" time seek range supported
68         "11" both time seek range and range supported
69         """
70         DLNA_ORG_OPERATION_KEY = "DLNA_ORG.OP"
71         DLNA_ORG_OPERATION_NONE         = 0x00,
72         DLNA_ORG_OPERATION_RANGE        = 0x01,
73         DLNA_ORG_OPERATION_TIMESEEK     = 0x10,
74
75         DLNA_ORG_PROFILE_NAME_KEY = "DLNA_ORG.PN"
76
77         LUT_MIME = {
78                         "mp3"  : "audio/mpeg",
79                         "aac"  : "audio/mp4",
80                         "flac" : "audio/ogg",
81                         "mkv"  : "video/x-matroska",
82                         "mp4"  : "video/mp4",
83                         "ts"   : "video/mpeg",
84                         "mpg"  : "video/mpeg",
85                 }
86
87         LUT_PARAMS = {
88                         "video/mpeg" : ";".join(["DLNA.ORG_PN=MPEG_TS_SD_EU"] + DIDLLite.simple_dlna_tags),
89                 }
90
91         @staticmethod
92         def getMimeType(codec, default="audio/mpeg"):
93                 return DLNA.LUT_MIME.get(codec, default)
94
95         @staticmethod
96         def getParams(mimetype, default="*"):
97                 return DLNA.LUT_PARAMS.get(mimetype, default)
98
99 '''
100 This is a "managed" UPnP A/V Controlpoint which eases the use of UPnP, for Browsing media or adding a Renderer
101 please see the helper classes (UPnPBrowser and AbstractUPnPRenderer) for more
102 '''
103 class ManagedControlPoint(object):
104         def __init__(self):
105                 self.coherence = None
106                 self._controlPoint = None
107                 self.__mediaServerClients = {}
108                 self.__mediaRendererClients = {}
109                 self.__mediaDevices = {}
110                 self.__devices = []
111
112                 self.onMediaServerDetected = []
113                 self.onMediaServerRemoved  = []
114                 self.onMediaRendererDetected = []
115                 self.onMediaRendererRemoved = []
116                 self.onMediaDeviceDectected = []
117                 self._session = None
118                 self.__deferredShutDown = None
119                 self._startPending = False
120
121         def _onShutdownFinished(self, *args, **kwargs):
122                 self.__deferredShutDown = None
123                 if self._startPending:
124                         self.start()
125
126         def start(self):
127                 def doStart(*args, **kwargs):
128                         if self._controlPoint:
129                                 Log.w("already running!")
130                                 return
131                         Log.w("starting now!")
132                         self._startPending = False
133                         self.coherence = Coherence({'logmode':'warning'})
134                         self._controlPoint = ControlPoint(self.coherence, auto_client=['MediaServer','MediaRenderer'])
135                         self.coherence.ctrl = self._controlPoint
136                         self.__mediaServerClients = {}
137                         self.__mediaRendererClients = {}
138                         self.__mediaDevices = {}
139                         self.__devices = []
140                         self._controlPoint.connect(self._onMediaServerDetected, 'Coherence.UPnP.ControlPoint.MediaServer.detected')
141                         self._controlPoint.connect(self._onMediaServerRemoved, 'Coherence.UPnP.ControlPoint.MediaServer.removed')
142                         self._controlPoint.connect(self._onMediaRendererDetected, 'Coherence.UPnP.ControlPoint.MediaRenderer.detected')
143                         self._controlPoint.connect(self._onMediaRendererRemoved, 'Coherence.UPnP.ControlPoint.MediaRenderer.removed')
144                         self._controlPoint.connect(self._onMediaDeviceDectected, 'Coherence.UPnP.Device.detection_completed')
145                         self._controlPoint.connect(self._onMediaDeviceRemoved, 'Coherence.UPnP.RootDevice.removed')
146                         self.__deferredShutDown = None
147                         if self._session:
148                                 self._callPlugins(reason=0)
149                 if self.__deferredShutDown:
150                         Log.w("deferring start until shutdown is finished")
151                         if not self._startPending:
152                                 self._startPending = True
153                 else:
154                         doStart()
155
156         def restart(self):
157                 Log.w()
158                 if not self.__deferredShutDown:
159                         self.shutdown()
160                 self.start()
161
162         def setSession(self, session):
163                 self._session = session
164                 if self.coherence:
165                         self._callPlugins(reason=0)
166
167         def _callPlugins(self, reason=0):
168                 for plugin in plugins.getPlugins(PluginDescriptor.WHERE_UPNP):
169                         plugin(reason, session=self._session)
170
171         def _onMediaServerDetected(self, client, udn):
172                 print "[DLNA] MediaServer Detected: %s (%s)" % (client.device.get_friendly_name(), client.device.get_friendly_device_type())
173                 self.__mediaServerClients[udn] = client
174                 for fnc in self.onMediaServerDetected:
175                         fnc(udn, client)
176
177         def _onMediaServerRemoved(self, udn):
178                 if self.__mediaServerClients.get(udn, None) != None:
179                         del self.__mediaServerClients[udn]
180                         for fnc in self.onMediaServerRemoved:
181                                 fnc(udn)
182
183         def _onMediaRendererDetected(self, client, udn):
184                 print "[DLNA] MediaRenderer detected: %s (%s, %s)" % (client.device.get_friendly_name(), client.device.get_friendly_device_type(), udn)
185                 self.__mediaRendererClients[udn] = client
186                 for fnc in self.onMediaRendererDetected:
187                         fnc(udn, client)
188
189         def _onMediaRendererRemoved(self, udn):
190                 print "[DLNA] MediaRenderer removed: %s" % (udn)
191                 if self.__mediaRendererClients.get(udn, None) != None:
192                         del self.__mediaRendererClients[udn]
193                         for fnc in self.onMediaRendererRemoved:
194                                 fnc(udn)
195
196         def _onMediaDeviceDectected(self, device):
197                 if not self.__mediaDevices.has_key(device.get_usn()):
198                         print "[DLNA] New device found: %s (%s)" % (device.get_friendly_name(), device.get_friendly_device_type())
199                 self.__mediaDevices[device.get_usn()] = device
200
201         def _onMediaDeviceRemoved(self, usn):
202                 if usn in self.__mediaDevices:
203                         print "[DLNA] Device removed: %s" % (usn)
204                         del self.__mediaDevices[usn]
205
206         def registerRenderer(self, classDef, **kwargs):
207                 renderer = MediaRenderer(self.coherence, classDef, no_thread_needed=True, **kwargs)
208                 self.__devices.append(renderer)
209                 return renderer
210
211         def registerServer(self, classDef, **kwargs):
212                 server = MediaServer(self.coherence, classDef, no_thread_needed=True, **kwargs)
213                 self.__devices.append(server)
214                 return server
215
216         def getServerList(self):
217                 return self.__mediaServerClients.values()
218
219         def getRenderingControlClientList(self):
220                 return self.__mediaRendererClients.values()
221
222         def getDeviceName(self, client):
223                 return Item.ue(client.device.get_friendly_name())
224
225         def getDevice(self, uuid):
226                 for device in self.__devices:
227                         if device.uuid == uuid:
228                                 return device
229                 return None
230
231         def removeDevice(self, uuid):
232                 device = self.getDevice(uuid)
233                 if device:
234                         device.unregister()
235                         self.__devices.remove(device)
236                         return True
237                 return False
238
239         def shutdown(self):
240                 Log.w("%s" %(self.coherence,))
241                 if True:
242                         Log.w("shutdown is broken... will continue running. please restart enigma2 instead!")
243                         return
244                 if self.coherence:
245                         self._callPlugins(reason=1)
246                         self.__mediaServerClients = {}
247                         self.__mediaRendererClients = {}
248                         self.__mediaDevices = {}
249                         self.__devices = []
250                         self.__deferredShutDown = self.coherence.shutdown(force=True)
251                         self.__deferredShutDown.addCallback(self._onShutdownFinished)
252                         self._controlPoint.disconnect(self._onMediaServerDetected, 'Coherence.UPnP.ControlPoint.MediaServer.detected')
253                         self._controlPoint.disconnect(self._onMediaServerRemoved, 'Coherence.UPnP.ControlPoint.MediaServer.removed')
254                         self._controlPoint.disconnect(self._onMediaRendererDetected, 'Coherence.UPnP.ControlPoint.MediaRenderer.detected')
255                         self._controlPoint.disconnect(self._onMediaRendererRemoved, 'Coherence.UPnP.ControlPoint.MediaRenderer.removed')
256                         self._controlPoint.disconnect(self._onMediaDeviceDectected, 'Coherence.UPnP.Device.detection_completed')
257                         self._controlPoint.disconnect(self._onMediaDeviceRemoved, 'Coherence.UPnP.RootDevice.removed')
258                         self.coherence = None
259                         self._controlPoint = None
260
261 class Item(object):
262         htmlparser = HTMLParser()
263
264         @staticmethod
265         def ue(val):
266                 return Item.htmlparser.unescape(val).encode("utf-8")
267
268         @staticmethod
269         def getItemType(item):
270                 if item != None:
271                         if item.__class__.__name__ == MediaServerClient.__name__:
272                                 return Statics.ITEM_TYPE_SERVER
273
274                         itemClass = Item.ue(item.upnp_class)
275                         if Item.isContainer(item):
276                                 return Statics.ITEM_TYPE_CONTAINER
277
278                         elif itemClass.startswith("object.item"):
279                                 type = item.upnp_class.split('.')[-1]
280                                 if type == "videoItem" or type == "movie":
281                                         return Statics.ITEM_TYPE_VIDEO
282                                 elif type == "musicTrack" or type == "audioItem":
283                                         return Statics.ITEM_TYPE_AUDIO
284                                 elif type == "photo":
285                                         return Statics.ITEM_TYPE_PICTURE
286
287                 return None
288
289         @staticmethod
290         def isServer(item):
291                 return Item.getItemType(item) == Statics.ITEM_TYPE_SERVER
292
293         @staticmethod
294         def getServerName(client):
295                 return Item.ue(client.device.get_friendly_name())
296
297         '''
298         Returns the title of the current item
299         '''
300         @staticmethod
301         def getItemTitle(item):
302                 if Item.isServer(item):
303                         return Item.getServerName(item)
304
305                 if item.title != None:
306                         return Item.ue(item.title)
307                 else:
308                         return "<missing title>"
309
310         '''
311         returns the number of children for container items
312         returns -1 for non-container items
313         '''
314         @staticmethod
315         def getItemChildCount(item):
316                 if Item.getItemType(item) != Statics.ITEM_TYPE_SERVER and Item.isContainer(item):
317                         return item.childCount
318
319                 return -1
320
321         '''
322         Currently always returns a dict with the first url and meta-type, which is usually the original/non-transcoded source
323         Raises an IllegalInstanceException if you pass in a container-item
324         '''
325         @staticmethod
326         def getItemUriMeta(item):
327                 assert not Item.isContainer(item)
328                 for res in item.res:
329                         uri = Item.ue(res.data)
330                         meta = Item.ue(res.protocolInfo.split(":")[2])
331                         print "URL: %s\nMeta:%s" %(uri, meta)
332                         if uri:
333                                 return {Statics.META_URI : uri, Statics.META_METATYPE : meta}
334
335         @staticmethod
336         def getItemId(item):
337                 if Item.isServer(item):
338                         return item.device.get_id()
339                 else:
340                         return item.id
341
342         @staticmethod
343         def getAttrOrDefault(instance, attr, default=None):
344                 val = getattr(instance, attr, default) or default
345                 try:
346                         return Item.ue(val)
347                 except:
348                         return val
349
350         @staticmethod
351         def getItemMetadata(item):
352                 type = Item.getItemType(item)
353                 meta = {}
354                 metaNA = _('n/a')
355                 cover_uri = None
356
357                 if type == Statics.ITEM_TYPE_SERVER:
358                         meta = {
359                                         Statics.META_TYPE : type,
360                                         Statics.META_TITLE : Item.getServerName(item),
361                                 }
362
363                 elif type == Statics.ITEM_TYPE_CONTAINER:
364                         meta = {
365                                         Statics.META_TYPE : type,
366                                         Statics.META_TITLE : Item.getAttrOrDefault(item, 'title', metaNA),
367                                         Statics.META_CHILD_COUNT : Item.getItemChildCount(item),
368                                 }
369                 elif type == Statics.ITEM_TYPE_PICTURE or type == Statics.ITEM_TYPE_VIDEO:
370                         for res in item.res:
371                                 content_format = Item.ue( res.protocolInfo.split(':')[2] )
372                                 if ( type == Statics.ITEM_TYPE_VIDEO and content_format.startswith("video") ) or type == Statics.ITEM_TYPE_PICTURE:
373                                         meta = {
374                                                         Statics.META_TYPE : type,
375                                                         Statics.META_METATYPE : content_format,
376                                                         Statics.META_TITLE : Item.getAttrOrDefault(item, 'title', metaNA),
377                                                         Statics.META_DATE : Item.getAttrOrDefault(item, 'date', metaNA),
378                                                         Statics.META_RESOLUTION : Item.getAttrOrDefault(item, 'resolution', metaNA),
379                                                         Statics.META_SIZE : Item.getAttrOrDefault(item, 'size', -1),
380                                                         Statics.META_URI : Item.getAttrOrDefault(res, 'data'),
381                                                 }
382                                 elif type == Statics.ITEM_TYPE_VIDEO and content_format.startswith("image"):
383                                         cover_uri = Item.getAttrOrDefault(res, 'data')
384
385                                 if type == Statics.ITEM_TYPE_PICTURE:
386                                         meta[Statics.META_ALBUM] = Item.getAttrOrDefault(item, 'album', metaNA)
387                                 elif type == Statics.ITEM_TYPE_VIDEO:
388                                         meta[Statics.META_ALBUM_ART_URI] = Item.getAttrOrDefault(item, 'albumArtURI')
389
390                 elif type == Statics.ITEM_TYPE_AUDIO:
391                         for res in item.res:
392                                 content_format = Item.ue( res.protocolInfo.split(':')[2] )
393                                 if content_format.startswith("audio"):
394                                         meta = {
395                                                         Statics.META_TYPE : type,
396                                                         Statics.META_METATYPE : content_format,
397                                                         Statics.META_TITLE : Item.getAttrOrDefault(item, 'title', metaNA),
398                                                         Statics.META_ALBUM : Item.getAttrOrDefault(item, 'album', metaNA),
399                                                         Statics.META_ARTIST : Item.getAttrOrDefault(item, 'artist', metaNA),
400                                                         Statics.META_GENRE : Item.getAttrOrDefault(item, 'genre', metaNA),
401                                                         Statics.META_DURATION : Item.getAttrOrDefault(item, 'duration', "0"),
402                                                         Statics.META_BITRATE : Item.getAttrOrDefault(item, 'bitrate', "0"),
403                                                         Statics.META_SIZE : Item.getAttrOrDefault(item, 'size', -1),
404                                                         Statics.META_ALBUM_ART_URI : Item.getAttrOrDefault(item, 'albumArtURI'),
405                                                         Statics.META_URI : Item.getAttrOrDefault(res, 'data'),
406                                                 }
407                                 elif content_format.startswith("image"):
408                                         cover_uri = Item.getAttrOrDefault(res, 'data')
409                 if cover_uri != None:
410                         meta[Statics.META_COVER_URI] = cover_uri
411
412                 return meta
413
414         @staticmethod
415         def isContainer(item):
416                 if item.__class__.__name__ == MediaServerClient.__name__:
417                         return True
418                 return item.upnp_class.startswith("object.container")
419
420 def removeUPnPDevice(uuid, cp=None):
421         if not cp:
422                 cp = resourcemanager.getResource("UPnPControlPoint")
423         if cp:
424                 return cp.removeDevice(uuid)
425         return False