A Python UPnP Media Server

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