A Python UPnP Media Server

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