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.

567 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. creator = None
  93. res = None
  94. writeStatus = None
  95. content = property(lambda x: x._content)
  96. needupdate = None # do we update before sending? (for res)
  97. def __init__(self, cd, id, parentID, title, restricted=False,
  98. creator=None, **kwargs):
  99. self.cd = cd
  100. self.id = id
  101. self.parentID = parentID
  102. self.title = title
  103. self.creator = creator
  104. if restricted:
  105. self.restricted = '1'
  106. else:
  107. self.restricted = '0'
  108. if kwargs.has_key('content'):
  109. self._content = kwargs['content']
  110. def __cmp__(self, other):
  111. if not isinstance(other, self.__class__):
  112. return cmp(self.__class__.__name__,
  113. other.__class__.__name__)
  114. return cmp(self.id, other.id)
  115. def __repr__(self):
  116. cls = self.__class__
  117. return '<%s.%s: id: %s, parent: %s, title: %s>' % \
  118. (cls.__module__, cls.__name__, self.id, self.parentID,
  119. self.title)
  120. def checkUpdate(self):
  121. # It's tempting to call doUpdate here, but each object has to
  122. # decide that.
  123. pass
  124. def toElement(self):
  125. root = Element(self.elementName)
  126. root.attrib['id'] = self.id
  127. root.attrib['parentID'] = self.parentID
  128. SubElement(root, 'dc:title').text = self.title
  129. SubElement(root, 'upnp:class').text = self.klass
  130. root.attrib['restricted'] = self.restricted
  131. if self.creator is not None:
  132. SubElement(root, 'dc:creator').text = self.creator
  133. if self.res is not None:
  134. try:
  135. resiter = iter(self.res)
  136. except TypeError, x:
  137. resiter = [ self.res ]
  138. for res in resiter:
  139. root.append(res.toElement())
  140. if self.writeStatus is not None:
  141. SubElement(root, 'upnp:writeStatus').text = self.writeStatus
  142. return root
  143. def toString(self):
  144. return tostring(self.toElement())
  145. class Item(Object):
  146. """A class used to represent atomic (non-container) content
  147. objects."""
  148. klass = Object.klass + '.item'
  149. elementName = 'item'
  150. refID = None
  151. needupdate = True
  152. def doUpdate(self, child=False):
  153. Container.doUpdate(self.cd[self.parentID])
  154. # do NOT update parent container per 2.2.9 for
  155. # ContainerUpdateID changes
  156. def toElement(self):
  157. root = Object.toElement(self)
  158. if self.refID is not None:
  159. SubElement(root, 'refID').text = self.refID
  160. return root
  161. class ImageItem(Item):
  162. klass = Item.klass + '.imageItem'
  163. class Photo(ImageItem):
  164. klass = ImageItem.klass + '.photo'
  165. class AudioItem(Item):
  166. """A piece of content that when rendered generates some audio."""
  167. klass = Item.klass + '.audioItem'
  168. genre = None
  169. description = None
  170. longDescription = None
  171. publisher = None
  172. language = None
  173. relation = None
  174. rights = None
  175. def toElement(self):
  176. root = Item.toElement(self)
  177. if self.genre is not None:
  178. SubElement(root, 'upnp:genre').text = self.genre
  179. if self.description is not None:
  180. SubElement(root, 'dc:description').text = self.description
  181. if self.longDescription is not None:
  182. SubElement(root, 'upnp:longDescription').text = \
  183. self.longDescription
  184. if self.publisher is not None:
  185. SubElement(root, 'dc:publisher').text = self.publisher
  186. if self.language is not None:
  187. SubElement(root, 'dc:language').text = self.language
  188. if self.relation is not None:
  189. SubElement(root, 'dc:relation').text = self.relation
  190. if self.rights is not None:
  191. SubElement(root, 'dc:rights').text = self.rights
  192. return root
  193. class MusicTrack(AudioItem):
  194. """A discrete piece of audio that should be interpreted as music."""
  195. klass = AudioItem.klass + '.musicTrack'
  196. artist = None
  197. album = None
  198. originalTrackNumber = None
  199. playlist = None
  200. storageMedium = None
  201. contributor = None
  202. date = None
  203. def toElement(self):
  204. root = AudioItem.toElement(self)
  205. if self.artist is not None:
  206. SubElement(root, 'upnp:artist').text = self.artist
  207. if self.album is not None:
  208. SubElement(root, 'upnp:album').text = self.album
  209. if self.originalTrackNumber is not None:
  210. SubElement(root, 'upnp:originalTrackNumber').text = \
  211. self.originalTrackNumber
  212. if self.playlist is not None:
  213. SubElement(root, 'upnp:playlist').text = self.playlist
  214. if self.storageMedium is not None:
  215. SubElement(root, 'upnp:storageMedium').text = self.storageMedium
  216. if self.contributor is not None:
  217. SubElement(root, 'dc:contributor').text = self.contributor
  218. if self.date is not None:
  219. SubElement(root, 'dc:date').text = self.date
  220. return root
  221. class AudioBroadcast(AudioItem):
  222. klass = AudioItem.klass + '.audioBroadcast'
  223. class AudioBook(AudioItem):
  224. klass = AudioItem.klass + '.audioBook'
  225. class VideoItem(Item):
  226. klass = Item.klass + '.videoItem'
  227. class Movie(VideoItem):
  228. klass = VideoItem.klass + '.movie'
  229. class VideoBroadcast(VideoItem):
  230. klass = VideoItem.klass + '.videoBroadcast'
  231. class MusicVideoClip(VideoItem):
  232. klass = VideoItem.klass + '.musicVideoClip'
  233. class PlaylistItem(Item):
  234. klass = Item.klass + '.playlistItem'
  235. class TextItem(Item):
  236. klass = Item.klass + '.textItem'
  237. class Container(Object, list):
  238. """An object that can contain other objects."""
  239. klass = Object.klass + '.container'
  240. elementName = 'container'
  241. childCount = property(lambda x: len(x))
  242. createClass = None
  243. searchClass = None
  244. searchable = None
  245. updateID = 0
  246. needupdate = False
  247. def __init__(self, cd, id, parentID, title, **kwargs):
  248. Object.__init__(self, cd, id, parentID, title, **kwargs)
  249. list.__init__(self)
  250. self.doingUpdate = False
  251. self.oldchildren = {}
  252. def genCurrent(self):
  253. '''This function returns a tuple/list/generator that returns
  254. tuples of (id, name). name must match what is returned by
  255. genChildren. If name is used for the title directly, no
  256. override is necessary.'''
  257. return ((x.id, x.title) for x in self)
  258. def genChildren(self):
  259. '''This function returns a list or dict of names for new
  260. children.'''
  261. raise NotImplementedError
  262. def createObject(self, i, arg=None):
  263. '''This function returns the (class, name, *args, **kwargs)
  264. that will be passed to the addItem method of the
  265. ContentDirectory. arg will be passed the value of the dict
  266. keyed by i if genChildren is a dict.'''
  267. raise NotImplementedError
  268. def sort(self, fun=lambda x, y: cmp(x.title, y.title)):
  269. return list.sort(self, fun)
  270. def doUpdate(self):
  271. if self.doingUpdate:
  272. return
  273. self.doingUpdate = True
  274. didupdate = False
  275. children = self.genChildren()
  276. if isinstance(children, dict):
  277. oldchildren = self.oldchildren
  278. self.oldchildren = children
  279. isdict = True
  280. else:
  281. children = set(children)
  282. isdict = False
  283. names = {}
  284. #print 'i:', `self`, `self.genCurrent`, `self.__class__`
  285. for id, i in tuple(self.genCurrent()):
  286. if i not in children:
  287. didupdate = True
  288. # delete
  289. self.cd.delItem(id)
  290. else:
  291. names[i] = id
  292. for i in children:
  293. if i in names:
  294. if isdict:
  295. if oldchildren[i] == children[i]:
  296. continue
  297. self.cd.delItem(names[i])
  298. else:
  299. # XXX - some sort of comparision?
  300. continue
  301. # new object
  302. if isdict:
  303. args = (children[i], )
  304. else:
  305. args = ()
  306. try:
  307. #print 'i:', `i`, `isdict`, `args`, `self`
  308. pass
  309. except UnicodeEncodeError:
  310. print 'i decode error'
  311. klass, name, args, kwargs = self.createObject(i, *args)
  312. if klass is not None:
  313. self.cd.addItem(self.id, klass, name, *args,
  314. **kwargs)
  315. didupdate = True
  316. # sort our children
  317. self.sort()
  318. if didupdate:
  319. self.didUpdate()
  320. self.doingUpdate = False
  321. def didUpdate(self):
  322. if self.id == '0':
  323. self.updateID = (self.updateID + 1)
  324. else:
  325. self.updateID = (self.updateID + 1) % (1l << 32)
  326. Container.didUpdate(self.cd['0'])
  327. def _addSet(self, e, items):
  328. if items is not None:
  329. if not isinstance(items, (list, tuple)):
  330. items = [ items ]
  331. for i in items:
  332. el = SubElement(root, e)
  333. el.text = i
  334. # XXX - how to specify?
  335. el.attrib['includeDerived'] = '1'
  336. def toElement(self):
  337. root = Object.toElement(self)
  338. # only include if we have children, it's possible we don't
  339. # have our children yet, and childCount is optional.
  340. if self.childCount:
  341. root.attrib['childCount'] = str(self.childCount)
  342. self._addSet('upnp:createclass', self.createClass)
  343. self._addSet('upnp:searchclass', self.searchClass)
  344. if self.searchable is not None:
  345. root.attrib['searchable'] = str(self.searchable)
  346. return root
  347. def __repr__(self):
  348. cls = self.__class__
  349. return '<%s.%s: id: %s, parent: %s, title: %s, cnt: %d>' % \
  350. (cls.__module__, cls.__name__, self.id, self.parentID,
  351. self.title, len(self))
  352. class Person(Container):
  353. klass = Container.klass + '.person'
  354. class MusicArtist(Person):
  355. klass = Person.klass + '.musicArtist'
  356. class PlaylistContainer(Container):
  357. klass = Container.klass + '.playlistContainer'
  358. class Album(Container):
  359. klass = Container.klass + '.album'
  360. class MusicAlbum(Album):
  361. klass = Album.klass + '.musicAlbum'
  362. class PhotoAlbum(Album):
  363. klass = Album.klass + '.photoAlbum'
  364. class Genre(Container):
  365. klass = Container.klass + '.genre'
  366. class MusicGenre(Genre):
  367. klass = Genre.klass + '.musicGenre'
  368. class MovieGenre(Genre):
  369. klass = Genre.klass + '.movieGenre'
  370. class StorageSystem(Container):
  371. klass = Container.klass + '.storageSystem'
  372. total = -1
  373. used = -1
  374. free = -1
  375. maxpartition = -1
  376. medium = 'UNKNOWN'
  377. def toElement(self):
  378. root = Container.toElement(self)
  379. SubElement(root, 'upnp:storageTotal').text = str(self.total)
  380. SubElement(root, 'upnp:storageUsed').text = str(self.used)
  381. SubElement(root, 'upnp:storageFree').text = str(self.free)
  382. SubElement(root, 'upnp:storageMaxPartition').text = str(self.maxpartition)
  383. SubElement(root, 'upnp:storageMedium').text = self.medium
  384. return root
  385. class StorageVolume(Container):
  386. klass = Container.klass + '.storageVolume'
  387. total = -1
  388. used = -1
  389. free = -1
  390. medium = 'UNKNOWN'
  391. def toElement(self):
  392. root = Container.toElement(self)
  393. SubElement(root, 'upnp:storageTotal').text = str(self.total)
  394. SubElement(root, 'upnp:storageUsed').text = str(self.used)
  395. SubElement(root, 'upnp:storageFree').text = str(self.free)
  396. SubElement(root, 'upnp:storageMedium').text = self.medium
  397. return root
  398. class StorageFolder(Container):
  399. klass = Container.klass + '.storageFolder'
  400. used = -1
  401. def toElement(self):
  402. root = Container.toElement(self)
  403. if self.used is not None:
  404. SubElement(root, 'upnp:storageUsed').text = str(self.used)
  405. return root
  406. class DIDLElement(_ElementInterface):
  407. def __init__(self):
  408. _ElementInterface.__init__(self, 'DIDL-Lite', {})
  409. self.attrib['xmlns'] = 'urn:schemas-upnp-org:metadata-1-0/DIDL-Lite'
  410. self.attrib['xmlns:dc'] = 'http://purl.org/dc/elements/1.1/'
  411. self.attrib['xmlns:upnp'] = 'urn:schemas-upnp-org:metadata-1-0/upnp'
  412. def addItem(self, item):
  413. self.append(item.toElement())
  414. def numItems(self):
  415. return len(self)
  416. def toString(self):
  417. return tostring(self)
  418. if __name__ == '__main__':
  419. root = DIDLElement()
  420. root.addItem(Container(None, '0\Movie\\', '0\\', 'Movie'))
  421. root.addItem(Container(None, '0\Music\\', '0\\', 'Music'))
  422. root.addItem(Container(None, '0\Photo\\', '0\\', 'Photo'))
  423. root.addItem(Container(None, '0\OnlineMedia\\', '0\\', 'OnlineMedia'))
  424. print tostring(root)