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.

536 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, **kwargs):
  220. Object.__init__(self, cd, id, parentID, title, **kwargs)
  221. list.__init__(self)
  222. self.doingUpdate = False
  223. self.oldchildren = {}
  224. def genCurrent(self):
  225. '''This function returns a tuple/list/generator that returns
  226. tuples of (id, name). name must match what is returned by
  227. genChildren. If name is used for the title directly, no
  228. override is necessary.'''
  229. return ((x.id, x.title) for x in self)
  230. def genChildren(self):
  231. '''This function returns a list or dict of names for new
  232. children.'''
  233. raise NotImplementedError
  234. def createObject(self, i, arg=None):
  235. '''This function returns the (class, name, *args, **kwargs)
  236. that will be passed to the addItem method of the
  237. ContentDirectory. arg will be passed the value of the dict
  238. keyed by i if genChildren is a dict.'''
  239. raise NotImplementedError
  240. def sort(self, fun=None):
  241. if fun is None:
  242. return list.sort(self, lambda x, y: cmp(x.title,
  243. y.title))
  244. return list.sort(self, fun)
  245. def doUpdate(self):
  246. if self.doingUpdate:
  247. return
  248. self.doingUpdate = True
  249. didupdate = False
  250. children = self.genChildren()
  251. if isinstance(children, dict):
  252. oldchildren = self.oldchildren
  253. self.oldchildren = children
  254. isdict = True
  255. else:
  256. children = set(children)
  257. isdict = False
  258. names = {}
  259. #print 'i:', `self`, `self.genCurrent`, `self.__class__`
  260. for id, i in tuple(self.genCurrent()):
  261. if i not in children:
  262. didupdate = True
  263. # delete
  264. self.cd.delItem(id)
  265. else:
  266. names[i] = id
  267. for i in children:
  268. if i in names:
  269. if isdict:
  270. if oldchildren[i] == children[i]:
  271. continue
  272. self.cd.delItem(names[i])
  273. else:
  274. # XXX - some sort of comparision?
  275. continue
  276. # new object
  277. if isdict:
  278. args = (children[i], )
  279. else:
  280. args = ()
  281. #print 'i:', `i`, `isdict`, `args`, `self`
  282. klass, name, args, kwargs = self.createObject(i, *args)
  283. if klass is not None:
  284. self.cd.addItem(self.id, klass, name, *args,
  285. **kwargs)
  286. didupdate = True
  287. # sort our children
  288. self.sort()
  289. if didupdate:
  290. self.didUpdate()
  291. self.doingUpdate = False
  292. def didUpdate(self):
  293. if self.id == '0':
  294. self.updateID = (self.updateID + 1)
  295. else:
  296. self.updateID = (self.updateID + 1) % (1l << 32)
  297. Container.didUpdate(self.cd['0'])
  298. def _addSet(self, e, items):
  299. if items is not None:
  300. if not isinstance(items, (list, tuple)):
  301. items = [ items ]
  302. for i in items:
  303. el = SubElement(root, e)
  304. el.text = i
  305. # XXX - how to specify?
  306. el.attrib['includeDerived'] = '1'
  307. def toElement(self):
  308. root = Object.toElement(self)
  309. # only include if we have children, it's possible we don't
  310. # have our children yet, and childCount is optional.
  311. if self.childCount:
  312. root.attrib['childCount'] = str(self.childCount)
  313. self._addSet('upnp:createclass', self.createClass)
  314. self._addSet('upnp:searchclass', self.searchClass)
  315. if self.searchable is not None:
  316. root.attrib['searchable'] = str(self.searchable)
  317. return root
  318. def __repr__(self):
  319. cls = self.__class__
  320. return '<%s.%s: id: %s, parent: %s, title: %s, cnt: %d>' % \
  321. (cls.__module__, cls.__name__, self.id, self.parentID,
  322. self.title, len(self))
  323. class Person(Container):
  324. klass = Container.klass + '.person'
  325. class MusicArtist(Person):
  326. klass = Person.klass + '.musicArtist'
  327. class PlaylistContainer(Container):
  328. klass = Container.klass + '.playlistContainer'
  329. class Album(Container):
  330. klass = Container.klass + '.album'
  331. class MusicAlbum(Album):
  332. klass = Album.klass + '.musicAlbum'
  333. class PhotoAlbum(Album):
  334. klass = Album.klass + '.photoAlbum'
  335. class Genre(Container):
  336. klass = Container.klass + '.genre'
  337. class MusicGenre(Genre):
  338. klass = Genre.klass + '.musicGenre'
  339. class MovieGenre(Genre):
  340. klass = Genre.klass + '.movieGenre'
  341. class StorageSystem(Container):
  342. klass = Container.klass + '.storageSystem'
  343. total = -1
  344. used = -1
  345. free = -1
  346. maxpartition = -1
  347. medium = 'UNKNOWN'
  348. def toElement(self):
  349. root = Container.toElement(self)
  350. SubElement(root, 'upnp:storageTotal').text = str(self.total)
  351. SubElement(root, 'upnp:storageUsed').text = str(self.used)
  352. SubElement(root, 'upnp:storageFree').text = str(self.free)
  353. SubElement(root, 'upnp:storageMaxPartition').text = str(self.maxpartition)
  354. SubElement(root, 'upnp:storageMedium').text = self.medium
  355. return root
  356. class StorageVolume(Container):
  357. klass = Container.klass + '.storageVolume'
  358. total = -1
  359. used = -1
  360. free = -1
  361. medium = 'UNKNOWN'
  362. def toElement(self):
  363. root = Container.toElement(self)
  364. SubElement(root, 'upnp:storageTotal').text = str(self.total)
  365. SubElement(root, 'upnp:storageUsed').text = str(self.used)
  366. SubElement(root, 'upnp:storageFree').text = str(self.free)
  367. SubElement(root, 'upnp:storageMedium').text = self.medium
  368. return root
  369. class StorageFolder(Container):
  370. klass = Container.klass + '.storageFolder'
  371. used = -1
  372. def toElement(self):
  373. root = Container.toElement(self)
  374. if self.used is not None:
  375. SubElement(root, 'upnp:storageUsed').text = str(self.used)
  376. return root
  377. class DIDLElement(_ElementInterface):
  378. def __init__(self):
  379. _ElementInterface.__init__(self, 'DIDL-Lite', {})
  380. self.attrib['xmlns'] = 'urn:schemas-upnp-org:metadata-1-0/DIDL-Lite'
  381. self.attrib['xmlns:dc'] = 'http://purl.org/dc/elements/1.1/'
  382. self.attrib['xmlns:upnp'] = 'urn:schemas-upnp-org:metadata-1-0/upnp'
  383. def addItem(self, item):
  384. self.append(item.toElement())
  385. def numItems(self):
  386. return len(self)
  387. def toString(self):
  388. return tostring(self)
  389. if __name__ == '__main__':
  390. root = DIDLElement()
  391. root.addItem(Container(None, '0\Movie\\', '0\\', 'Movie'))
  392. root.addItem(Container(None, '0\Music\\', '0\\', 'Music'))
  393. root.addItem(Container(None, '0\Photo\\', '0\\', 'Photo'))
  394. root.addItem(Container(None, '0\OnlineMedia\\', '0\\', 'OnlineMedia'))
  395. print tostring(root)