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.

469 lines
11 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-2008 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. return self
  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. def doUpdate(self):
  236. if self.id == '0':
  237. self.updateID = (self.updateID + 1)
  238. else:
  239. self.updateID = (self.updateID + 1) % (1l << 32)
  240. Container.doUpdate(self.cd['0'])
  241. def toElement(self):
  242. root = Object.toElement(self)
  243. # only include if we have children, it's possible we don't
  244. # have our children yet, and childCount is optional.
  245. if self.childCount:
  246. root.attrib['childCount'] = str(self.childCount)
  247. if self.createClass is not None:
  248. SubElement(root, 'upnp:createclass').text = self.createClass
  249. if self.searchClass is not None:
  250. if not isinstance(self.searchClass, (list, tuple)):
  251. self.searchClass = ['searchClass']
  252. for i in searchClass:
  253. SubElement(root, 'upnp:searchclass').text = i
  254. if self.searchable is not None:
  255. root.attrib['searchable'] = str(self.searchable)
  256. return root
  257. def __repr__(self):
  258. cls = self.__class__
  259. return '<%s.%s: id: %s, parent: %s, title: %s, cnt: %d>' % \
  260. (cls.__module__, cls.__name__, self.id, self.parentID,
  261. self.title, len(self))
  262. class Person(Container):
  263. klass = Container.klass + '.person'
  264. class MusicArtist(Person):
  265. klass = Person.klass + '.musicArtist'
  266. class PlaylistContainer(Container):
  267. klass = Container.klass + '.playlistContainer'
  268. class Album(Container):
  269. klass = Container.klass + '.album'
  270. class MusicAlbum(Album):
  271. klass = Album.klass + '.musicAlbum'
  272. class PhotoAlbum(Album):
  273. klass = Album.klass + '.photoAlbum'
  274. class Genre(Container):
  275. klass = Container.klass + '.genre'
  276. class MusicGenre(Genre):
  277. klass = Genre.klass + '.musicGenre'
  278. class MovieGenre(Genre):
  279. klass = Genre.klass + '.movieGenre'
  280. class StorageSystem(Container):
  281. klass = Container.klass + '.storageSystem'
  282. total = -1
  283. used = -1
  284. free = -1
  285. maxpartition = -1
  286. medium = 'UNKNOWN'
  287. def toElement(self):
  288. root = Container.toElement(self)
  289. SubElement(root, 'upnp:storageTotal').text = str(self.total)
  290. SubElement(root, 'upnp:storageUsed').text = str(self.used)
  291. SubElement(root, 'upnp:storageFree').text = str(self.free)
  292. SubElement(root, 'upnp:storageMaxPartition').text = str(self.maxpartition)
  293. SubElement(root, 'upnp:storageMedium').text = self.medium
  294. return root
  295. class StorageVolume(Container):
  296. klass = Container.klass + '.storageVolume'
  297. total = -1
  298. used = -1
  299. free = -1
  300. medium = 'UNKNOWN'
  301. def toElement(self):
  302. root = Container.toElement(self)
  303. SubElement(root, 'upnp:storageTotal').text = str(self.total)
  304. SubElement(root, 'upnp:storageUsed').text = str(self.used)
  305. SubElement(root, 'upnp:storageFree').text = str(self.free)
  306. SubElement(root, 'upnp:storageMedium').text = self.medium
  307. return root
  308. class StorageFolder(Container):
  309. klass = Container.klass + '.storageFolder'
  310. used = -1
  311. def toElement(self):
  312. root = Container.toElement(self)
  313. if self.used is not None:
  314. SubElement(root, 'upnp:storageUsed').text = str(self.used)
  315. return root
  316. class DIDLElement(_ElementInterface):
  317. def __init__(self):
  318. _ElementInterface.__init__(self, 'DIDL-Lite', {})
  319. self.attrib['xmlns'] = 'urn:schemas-upnp-org:metadata-1-0/DIDL-Lite'
  320. self.attrib['xmlns:dc'] = 'http://purl.org/dc/elements/1.1/'
  321. self.attrib['xmlns:upnp'] = 'urn:schemas-upnp-org:metadata-1-0/upnp'
  322. def addContainer(self, id, parentID, title, restricted = False):
  323. e = Container(id, parentID, title, restricted, creator = '')
  324. self.append(e.toElement())
  325. def addItem(self, item):
  326. self.append(item.toElement())
  327. def numItems(self):
  328. return len(self)
  329. def toString(self):
  330. return tostring(self)
  331. if __name__ == '__main__':
  332. root = DIDLElement()
  333. root.addContainer('0\Movie\\', '0\\', 'Movie')
  334. root.addContainer('0\Music\\', '0\\', 'Music')
  335. root.addContainer('0\Photo\\', '0\\', 'Photo')
  336. root.addContainer('0\OnlineMedia\\', '0\\', 'OnlineMedia')
  337. print tostring(root)