A Python UPnP Media Server
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

550 lines
13 KiB

  1. # Licensed under the MIT license
  2. # http://opensource.org/licenses/mit-license.php
  3. # Copyright 2005, Tim Potter <tpot@samba.org>
  4. # Copyright 2006-2009 John-Mark Gurney <jmg@funkthat.com>
  5. __version__ = '$Change$'
  6. # $Id$
  7. from elementtree.ElementTree import Element, SubElement, tostring, _ElementInterface
  8. class Resource(object):
  9. """An object representing a resource."""
  10. validattrs = {
  11. 'protocolinfo': 'protocolInfo',
  12. 'importuri': 'importUri',
  13. 'size': 'size',
  14. 'duration': 'duration',
  15. 'protection': 'protection',
  16. 'bitrate': 'bitrate',
  17. 'bitspersample': 'bitsPerSample',
  18. 'samplefrequency': 'sampleFrequence',
  19. 'nraudiochannels': 'nrAudioChannels',
  20. 'resolution': 'resolution',
  21. 'colordepth': 'colorDepth',
  22. 'tspec': 'tspec',
  23. 'alloweduse': 'allowedUse',
  24. 'validitystart': 'validityStart',
  25. 'validityend': 'validityEnd',
  26. 'remainingtime': 'remainingTime',
  27. 'usageinfo': 'usageInfo',
  28. 'rightsinfouri': 'rightsInfoURI',
  29. 'contentinfouri': 'contentInfoURI',
  30. 'recordquality': 'recordQuality',
  31. }
  32. def __init__(self, data, protocolInfo):
  33. object.__init__(self)
  34. # Use thses so setattr can be more simple
  35. object.__setattr__(self, 'data', data)
  36. object.__setattr__(self, 'attrs', {})
  37. self.protocolInfo = protocolInfo
  38. def __getattr__(self, key):
  39. try:
  40. return self.attrs[key.lower()]
  41. except KeyError:
  42. raise AttributeError, key
  43. def __setattr__(self, key, value):
  44. key = key.lower()
  45. assert key in self.validattrs
  46. self.attrs[key] = value
  47. def toElement(self):
  48. root = Element('res')
  49. root.text = self.data
  50. for i in self.attrs:
  51. attr = self.validattrs[i]
  52. value = self.attrs[i]
  53. funname = 'format_%s' % attr
  54. if hasattr(self, funname):
  55. value = getattr(self, funname)(value)
  56. else:
  57. value = str(value)
  58. assert isinstance(value, basestring), \
  59. 'value is not a string: %s' % `value`
  60. root.attrib[attr] = value
  61. return root
  62. @staticmethod
  63. def format_duration(s):
  64. if isinstance(s, basestring):
  65. return s
  66. # assume it is a number
  67. s = abs(s)
  68. secs = int(s)
  69. frac = s - secs
  70. minutes, secs = divmod(secs, 60)
  71. hours, minutes = divmod(minutes, 60)
  72. if frac:
  73. frac = ('%.2f' % frac)[1:]
  74. else:
  75. frac = ''
  76. return '%d:%02d:%02d%s' % (hours, minutes, secs, frac)
  77. class ResourceList(list):
  78. '''Special class to not overwrite mimetypes that already exist.'''
  79. def __init__(self, *args, **kwargs):
  80. self._mt = {}
  81. list.__init__(self, *args, **kwargs)
  82. def append(self, k):
  83. assert isinstance(k, Resource)
  84. mt = k.protocolInfo.split(':')[2]
  85. if self._mt.has_key(mt):
  86. return
  87. list.append(self, k)
  88. self._mt[mt] = k
  89. class Object(object):
  90. """The root class of the entire content directory class heirachy."""
  91. klass = 'object'
  92. _optionattrs = {
  93. 'creator': 'dc',
  94. 'writeStatus': 'upnp',
  95. 'artist': 'upnp',
  96. 'actor': 'upnp',
  97. 'author': 'upnp',
  98. 'producer': 'upnp',
  99. 'director': 'upnp',
  100. 'publisher': 'dc',
  101. 'contributor': 'dc',
  102. 'genre': 'upnp',
  103. 'album': 'upnp',
  104. 'playlist': 'upnp',
  105. 'albumArtURI': 'upnp',
  106. 'artistDiscographyURI': 'upnp',
  107. 'lyricsURI': 'upnp',
  108. 'relation': 'dc',
  109. 'storageMedium': 'upnp',
  110. 'description': 'dc',
  111. 'longDescription': 'upnp',
  112. 'icon': 'upnp',
  113. 'region': 'upnp',
  114. 'rights': 'dc',
  115. 'date': 'dc',
  116. 'language': 'dc',
  117. 'playbackCount': 'upnp',
  118. 'lastPlaybackTime': 'upnp',
  119. 'lastPlaybackPosition': 'upnp',
  120. 'recordedStartDateTime': 'upnp',
  121. 'recordedDuration': 'upnp',
  122. 'recordedDayOfWeek': 'upnp',
  123. 'srsRecordScheduleID': 'upnp',
  124. 'srsRecordTaskID': 'upnp',
  125. 'recordable': 'upnp',
  126. 'programTitle': 'upnp',
  127. 'seriesTitle': 'upnp',
  128. 'programID': 'upnp',
  129. 'seriesID': 'upnp',
  130. 'channelID': 'upnp',
  131. 'episodeCount': 'upnp',
  132. 'episodeNumber': 'upnp',
  133. 'programCode': 'upnp',
  134. 'rating': 'upnp',
  135. 'channelGroupName': 'upnp',
  136. 'callSign': 'upnp',
  137. 'networkAffiliation': 'upnp',
  138. 'serviceProvider': 'upnp',
  139. 'price': 'upnp',
  140. 'payPerView': 'upnp',
  141. 'epgProviderName': 'upnp',
  142. 'dateTimeRange': 'upnp',
  143. 'radioCallSign': 'upnp',
  144. 'radioStationID': 'upnp',
  145. 'radioBand': 'upnp',
  146. 'channelNr': 'upnp',
  147. 'channelName': 'upnp',
  148. 'scheduledStartTime': 'upnp',
  149. 'scheduledEndTime': 'upnp',
  150. 'signalStrength': 'upnp',
  151. 'signalLocked': 'upnp',
  152. 'tuned': 'upnp',
  153. 'neverPlayable': 'upnp',
  154. 'bookmarkID': 'upnp',
  155. 'bookmarkedObjectID': 'upnp',
  156. 'deviceUDN': 'upnp',
  157. 'stateVariableCollection': 'upnp',
  158. 'DVDRegionCode': 'upnp',
  159. 'originalTrackNumber': 'upnp',
  160. 'toc': 'upnp',
  161. 'userAnnoation': 'upnp',
  162. }
  163. res = None
  164. content = property(lambda x: x._content)
  165. needupdate = None # do we update before sending? (for res)
  166. def __init__(self, cd, id, parentID, title, restricted=False,
  167. creator=None, **kwargs):
  168. self.cd = cd
  169. self.id = id
  170. self.parentID = parentID
  171. self.title = title
  172. self.creator = creator
  173. if restricted:
  174. self.restricted = '1'
  175. else:
  176. self.restricted = '0'
  177. if kwargs.has_key('content'):
  178. self._content = kwargs.pop('content')
  179. for i in kwargs:
  180. if i not in self._optionattrs:
  181. raise TypeError('invalid keyword arg: %s' % `i`)
  182. setattr(self, i, kwargs[i])
  183. def __cmp__(self, other):
  184. if not isinstance(other, self.__class__):
  185. return cmp(self.__class__.__name__,
  186. other.__class__.__name__)
  187. return cmp(self.id, other.id)
  188. def __repr__(self):
  189. cls = self.__class__
  190. return '<%s.%s: id: %s, parent: %s, title: %s>' % \
  191. (cls.__module__, cls.__name__, self.id, self.parentID,
  192. self.title)
  193. def checkUpdate(self):
  194. # It's tempting to call doUpdate here, but each object has to
  195. # decide that.
  196. pass
  197. def toElement(self):
  198. root = Element(self.elementName)
  199. root.attrib['id'] = self.id
  200. root.attrib['parentID'] = self.parentID
  201. SubElement(root, 'dc:title').text = self.title
  202. SubElement(root, 'upnp:class').text = self.klass
  203. root.attrib['restricted'] = self.restricted
  204. for i in (x for x in self.__dict__ if x in self._optionattrs):
  205. obj = getattr(self, i)
  206. if obj is None:
  207. continue
  208. SubElement(root, '%s:%s' % (self._optionattrs[i],
  209. i)).text = unicode(getattr(self, i))
  210. if self.res is not None:
  211. try:
  212. resiter = iter(self.res)
  213. except TypeError, x:
  214. resiter = [ self.res ]
  215. for res in resiter:
  216. root.append(res.toElement())
  217. return root
  218. def toString(self):
  219. return tostring(self.toElement())
  220. class Item(Object):
  221. """A class used to represent atomic (non-container) content
  222. objects."""
  223. klass = Object.klass + '.item'
  224. elementName = 'item'
  225. refID = None
  226. needupdate = True
  227. def doUpdate(self, child=False):
  228. # do NOT update parent container per 2.2.9 for
  229. # ContainerUpdateID changes
  230. # must be Container.didUpdate, otherwise we could update the
  231. # parent when we really just want to update the ID
  232. Container.didUpdate(self.cd[self.parentID])
  233. def toElement(self):
  234. root = Object.toElement(self)
  235. if self.refID is not None:
  236. SubElement(root, 'refID').text = self.refID
  237. return root
  238. class ImageItem(Item):
  239. klass = Item.klass + '.imageItem'
  240. class Photo(ImageItem):
  241. klass = ImageItem.klass + '.photo'
  242. class AudioItem(Item):
  243. """A piece of content that when rendered generates some audio."""
  244. klass = Item.klass + '.audioItem'
  245. class MusicTrack(AudioItem):
  246. """A discrete piece of audio that should be interpreted as music."""
  247. klass = AudioItem.klass + '.musicTrack'
  248. class AudioBroadcast(AudioItem):
  249. klass = AudioItem.klass + '.audioBroadcast'
  250. class AudioBook(AudioItem):
  251. klass = AudioItem.klass + '.audioBook'
  252. class VideoItem(Item):
  253. klass = Item.klass + '.videoItem'
  254. class Movie(VideoItem):
  255. klass = VideoItem.klass + '.movie'
  256. class VideoBroadcast(VideoItem):
  257. klass = VideoItem.klass + '.videoBroadcast'
  258. class MusicVideoClip(VideoItem):
  259. klass = VideoItem.klass + '.musicVideoClip'
  260. class PlaylistItem(Item):
  261. klass = Item.klass + '.playlistItem'
  262. class TextItem(Item):
  263. klass = Item.klass + '.textItem'
  264. class Container(Object, list):
  265. """An object that can contain other objects."""
  266. klass = Object.klass + '.container'
  267. elementName = 'container'
  268. childCount = property(lambda x: len(x))
  269. searchable = None
  270. updateID = 0
  271. needcontupdate = False
  272. _optionattrs = Object._optionattrs.copy()
  273. _optionattrs.update({
  274. 'searchClass': 'upnp',
  275. 'createClass': 'upnp',
  276. 'storageTotal': 'upnp',
  277. 'storageUsed': 'upnp',
  278. 'storageFree': 'upnp',
  279. 'storageMaxPartition': 'upnp',
  280. })
  281. def __init__(self, cd, id, parentID, title, **kwargs):
  282. Object.__init__(self, cd, id, parentID, title, **kwargs)
  283. list.__init__(self)
  284. self.doingUpdate = False
  285. self.needcontupdate = False
  286. self.oldchildren = {}
  287. def genCurrent(self):
  288. '''This function returns a tuple/list/generator that returns
  289. tuples of (id, name). name must match what is returned by
  290. genChildren. If name is used for the title directly, no
  291. override is necessary.'''
  292. return ((x.id, x.title) for x in self)
  293. def genChildren(self):
  294. '''This function returns a list or dict of names for new
  295. children.'''
  296. raise NotImplementedError
  297. def createObject(self, i, arg=None):
  298. '''This function returns the (class, name, *args, **kwargs)
  299. that will be passed to the addItem method of the
  300. ContentDirectory. arg will be passed the value of the dict
  301. keyed by i if genChildren is a dict.'''
  302. raise NotImplementedError
  303. def sort(self, fun=lambda x, y: cmp(x.title, y.title)):
  304. return list.sort(self, fun)
  305. def doUpdate(self):
  306. if self.doingUpdate:
  307. return
  308. self.doingUpdate = True
  309. self.needcontupdate = False
  310. children = self.genChildren()
  311. if isinstance(children, dict):
  312. oldchildren = self.oldchildren
  313. self.oldchildren = children
  314. isdict = True
  315. else:
  316. children = set(children)
  317. isdict = False
  318. names = {}
  319. #print 'i:', `self`, `self.genCurrent`, `self.__class__`
  320. for id, i in tuple(self.genCurrent()):
  321. if i not in children:
  322. didupdate = True
  323. # delete
  324. self.cd.delItem(id)
  325. else:
  326. names[i] = id
  327. for i in children:
  328. if i in names:
  329. if isdict:
  330. if oldchildren[i] == children[i]:
  331. continue
  332. self.cd.delItem(names[i])
  333. else:
  334. # XXX - some sort of comparision?
  335. continue
  336. # new object
  337. if isdict:
  338. args = (children[i], )
  339. else:
  340. args = ()
  341. try:
  342. #print 'i:', `i`, `isdict`, `args`, `self`
  343. pass
  344. except UnicodeEncodeError:
  345. print 'i decode error'
  346. klass, name, args, kwargs = self.createObject(i, *args)
  347. if klass is not None:
  348. self.cd.addItem(self.id, klass, name, *args,
  349. **kwargs)
  350. self.needcontupdate = True
  351. # sort our children
  352. self.sort()
  353. self.doingUpdate = False
  354. if self.needcontupdate:
  355. self.didUpdate()
  356. def didUpdate(self):
  357. if self.doingUpdate:
  358. self.needcontupdate = True
  359. return
  360. if self.id == '0':
  361. self.updateID = (self.updateID + 1)
  362. else:
  363. self.updateID = (self.updateID + 1) % (1l << 32)
  364. Container.didUpdate(self.cd['0'])
  365. def _addSet(self, e, items):
  366. if items is not None:
  367. if not isinstance(items, (list, tuple)):
  368. items = [ items ]
  369. for i in items:
  370. el = SubElement(root, e)
  371. el.text = i
  372. # XXX - how to specify?
  373. el.attrib['includeDerived'] = '1'
  374. def toElement(self):
  375. root = Object.toElement(self)
  376. # only include if we have children, it's possible we don't
  377. # have our children yet, and childCount is optional.
  378. if self.childCount:
  379. root.attrib['childCount'] = str(self.childCount)
  380. if self.searchable is not None:
  381. root.attrib['searchable'] = str(self.searchable)
  382. return root
  383. def __repr__(self):
  384. cls = self.__class__
  385. return '<%s.%s: id: %s, parent: %s, title: %s, cnt: %d>' % \
  386. (cls.__module__, cls.__name__, self.id, self.parentID,
  387. self.title, len(self))
  388. class Person(Container):
  389. klass = Container.klass + '.person'
  390. class MusicArtist(Person):
  391. klass = Person.klass + '.musicArtist'
  392. class PlaylistContainer(Container):
  393. klass = Container.klass + '.playlistContainer'
  394. class Album(Container):
  395. klass = Container.klass + '.album'
  396. class MusicAlbum(Album):
  397. klass = Album.klass + '.musicAlbum'
  398. class PhotoAlbum(Album):
  399. klass = Album.klass + '.photoAlbum'
  400. class Genre(Container):
  401. klass = Container.klass + '.genre'
  402. class MusicGenre(Genre):
  403. klass = Genre.klass + '.musicGenre'
  404. class MovieGenre(Genre):
  405. klass = Genre.klass + '.movieGenre'
  406. class StorageSystem(Container):
  407. klass = Container.klass + '.storageSystem'
  408. storageTotal = -1
  409. storageUsed = -1
  410. storageFree = -1
  411. storageMaxParition = -1
  412. storageMedium = 'UNKNOWN'
  413. class StorageVolume(Container):
  414. klass = Container.klass + '.storageVolume'
  415. storageTotal = -1
  416. storageUsed = -1
  417. storageFree = -1
  418. storageMedium = 'UNKNOWN'
  419. class StorageFolder(Container):
  420. klass = Container.klass + '.storageFolder'
  421. storageUsed = -1
  422. class DIDLElement(_ElementInterface):
  423. def __init__(self):
  424. _ElementInterface.__init__(self, 'DIDL-Lite', {})
  425. self.attrib['xmlns'] = 'urn:schemas-upnp-org:metadata-1-0/DIDL-Lite'
  426. self.attrib['xmlns:dc'] = 'http://purl.org/dc/elements/1.1/'
  427. self.attrib['xmlns:upnp'] = 'urn:schemas-upnp-org:metadata-1-0/upnp'
  428. def addItem(self, item):
  429. self.append(item.toElement())
  430. def numItems(self):
  431. return len(self)
  432. def toString(self):
  433. return tostring(self)
  434. if __name__ == '__main__':
  435. root = DIDLElement()
  436. root.addItem(Container(None, '0\Movie\\', '0\\', 'Movie'))
  437. root.addItem(Container(None, '0\Music\\', '0\\', 'Music'))
  438. root.addItem(Container(None, '0\Photo\\', '0\\', 'Photo'))
  439. root.addItem(Container(None, '0\OnlineMedia\\', '0\\', 'OnlineMedia'))
  440. print tostring(root)