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.

543 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. Container.doUpdate(self.cd[self.parentID])
  229. # do NOT update parent container per 2.2.9 for
  230. # ContainerUpdateID changes
  231. def toElement(self):
  232. root = Object.toElement(self)
  233. if self.refID is not None:
  234. SubElement(root, 'refID').text = self.refID
  235. return root
  236. class ImageItem(Item):
  237. klass = Item.klass + '.imageItem'
  238. class Photo(ImageItem):
  239. klass = ImageItem.klass + '.photo'
  240. class AudioItem(Item):
  241. """A piece of content that when rendered generates some audio."""
  242. klass = Item.klass + '.audioItem'
  243. class MusicTrack(AudioItem):
  244. """A discrete piece of audio that should be interpreted as music."""
  245. klass = AudioItem.klass + '.musicTrack'
  246. class AudioBroadcast(AudioItem):
  247. klass = AudioItem.klass + '.audioBroadcast'
  248. class AudioBook(AudioItem):
  249. klass = AudioItem.klass + '.audioBook'
  250. class VideoItem(Item):
  251. klass = Item.klass + '.videoItem'
  252. class Movie(VideoItem):
  253. klass = VideoItem.klass + '.movie'
  254. class VideoBroadcast(VideoItem):
  255. klass = VideoItem.klass + '.videoBroadcast'
  256. class MusicVideoClip(VideoItem):
  257. klass = VideoItem.klass + '.musicVideoClip'
  258. class PlaylistItem(Item):
  259. klass = Item.klass + '.playlistItem'
  260. class TextItem(Item):
  261. klass = Item.klass + '.textItem'
  262. class Container(Object, list):
  263. """An object that can contain other objects."""
  264. klass = Object.klass + '.container'
  265. elementName = 'container'
  266. childCount = property(lambda x: len(x))
  267. searchable = None
  268. updateID = 0
  269. needupdate = False
  270. _optionattrs = Object._optionattrs.copy()
  271. _optionattrs.update({
  272. 'searchClass': 'upnp',
  273. 'createClass': 'upnp',
  274. 'storageTotal': 'upnp',
  275. 'storageUsed': 'upnp',
  276. 'storageFree': 'upnp',
  277. 'storageMaxPartition': 'upnp',
  278. })
  279. def __init__(self, cd, id, parentID, title, **kwargs):
  280. Object.__init__(self, cd, id, parentID, title, **kwargs)
  281. list.__init__(self)
  282. self.doingUpdate = False
  283. self.oldchildren = {}
  284. def genCurrent(self):
  285. '''This function returns a tuple/list/generator that returns
  286. tuples of (id, name). name must match what is returned by
  287. genChildren. If name is used for the title directly, no
  288. override is necessary.'''
  289. return ((x.id, x.title) for x in self)
  290. def genChildren(self):
  291. '''This function returns a list or dict of names for new
  292. children.'''
  293. raise NotImplementedError
  294. def createObject(self, i, arg=None):
  295. '''This function returns the (class, name, *args, **kwargs)
  296. that will be passed to the addItem method of the
  297. ContentDirectory. arg will be passed the value of the dict
  298. keyed by i if genChildren is a dict.'''
  299. raise NotImplementedError
  300. def sort(self, fun=lambda x, y: cmp(x.title, y.title)):
  301. return list.sort(self, fun)
  302. def doUpdate(self):
  303. if self.doingUpdate:
  304. return
  305. self.doingUpdate = True
  306. didupdate = False
  307. children = self.genChildren()
  308. if isinstance(children, dict):
  309. oldchildren = self.oldchildren
  310. self.oldchildren = children
  311. isdict = True
  312. else:
  313. children = set(children)
  314. isdict = False
  315. names = {}
  316. #print 'i:', `self`, `self.genCurrent`, `self.__class__`
  317. for id, i in tuple(self.genCurrent()):
  318. if i not in children:
  319. didupdate = True
  320. # delete
  321. self.cd.delItem(id)
  322. else:
  323. names[i] = id
  324. for i in children:
  325. if i in names:
  326. if isdict:
  327. if oldchildren[i] == children[i]:
  328. continue
  329. self.cd.delItem(names[i])
  330. else:
  331. # XXX - some sort of comparision?
  332. continue
  333. # new object
  334. if isdict:
  335. args = (children[i], )
  336. else:
  337. args = ()
  338. try:
  339. #print 'i:', `i`, `isdict`, `args`, `self`
  340. pass
  341. except UnicodeEncodeError:
  342. print 'i decode error'
  343. klass, name, args, kwargs = self.createObject(i, *args)
  344. if klass is not None:
  345. self.cd.addItem(self.id, klass, name, *args,
  346. **kwargs)
  347. didupdate = True
  348. # sort our children
  349. self.sort()
  350. if didupdate:
  351. self.didUpdate()
  352. self.doingUpdate = False
  353. def didUpdate(self):
  354. if self.id == '0':
  355. self.updateID = (self.updateID + 1)
  356. else:
  357. self.updateID = (self.updateID + 1) % (1l << 32)
  358. Container.didUpdate(self.cd['0'])
  359. def _addSet(self, e, items):
  360. if items is not None:
  361. if not isinstance(items, (list, tuple)):
  362. items = [ items ]
  363. for i in items:
  364. el = SubElement(root, e)
  365. el.text = i
  366. # XXX - how to specify?
  367. el.attrib['includeDerived'] = '1'
  368. def toElement(self):
  369. root = Object.toElement(self)
  370. # only include if we have children, it's possible we don't
  371. # have our children yet, and childCount is optional.
  372. if self.childCount:
  373. root.attrib['childCount'] = str(self.childCount)
  374. if self.searchable is not None:
  375. root.attrib['searchable'] = str(self.searchable)
  376. return root
  377. def __repr__(self):
  378. cls = self.__class__
  379. return '<%s.%s: id: %s, parent: %s, title: %s, cnt: %d>' % \
  380. (cls.__module__, cls.__name__, self.id, self.parentID,
  381. self.title, len(self))
  382. class Person(Container):
  383. klass = Container.klass + '.person'
  384. class MusicArtist(Person):
  385. klass = Person.klass + '.musicArtist'
  386. class PlaylistContainer(Container):
  387. klass = Container.klass + '.playlistContainer'
  388. class Album(Container):
  389. klass = Container.klass + '.album'
  390. class MusicAlbum(Album):
  391. klass = Album.klass + '.musicAlbum'
  392. class PhotoAlbum(Album):
  393. klass = Album.klass + '.photoAlbum'
  394. class Genre(Container):
  395. klass = Container.klass + '.genre'
  396. class MusicGenre(Genre):
  397. klass = Genre.klass + '.musicGenre'
  398. class MovieGenre(Genre):
  399. klass = Genre.klass + '.movieGenre'
  400. class StorageSystem(Container):
  401. klass = Container.klass + '.storageSystem'
  402. storageTotal = -1
  403. storageUsed = -1
  404. storageFree = -1
  405. storageMaxParition = -1
  406. storageMedium = 'UNKNOWN'
  407. class StorageVolume(Container):
  408. klass = Container.klass + '.storageVolume'
  409. storageTotal = -1
  410. storageUsed = -1
  411. storageFree = -1
  412. storageMedium = 'UNKNOWN'
  413. class StorageFolder(Container):
  414. klass = Container.klass + '.storageFolder'
  415. storageUsed = -1
  416. class DIDLElement(_ElementInterface):
  417. def __init__(self):
  418. _ElementInterface.__init__(self, 'DIDL-Lite', {})
  419. self.attrib['xmlns'] = 'urn:schemas-upnp-org:metadata-1-0/DIDL-Lite'
  420. self.attrib['xmlns:dc'] = 'http://purl.org/dc/elements/1.1/'
  421. self.attrib['xmlns:upnp'] = 'urn:schemas-upnp-org:metadata-1-0/upnp'
  422. def addItem(self, item):
  423. self.append(item.toElement())
  424. def numItems(self):
  425. return len(self)
  426. def toString(self):
  427. return tostring(self)
  428. if __name__ == '__main__':
  429. root = DIDLElement()
  430. root.addItem(Container(None, '0\Movie\\', '0\\', 'Movie'))
  431. root.addItem(Container(None, '0\Music\\', '0\\', 'Music'))
  432. root.addItem(Container(None, '0\Photo\\', '0\\', 'Photo'))
  433. root.addItem(Container(None, '0\OnlineMedia\\', '0\\', 'OnlineMedia'))
  434. print tostring(root)