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.

967 lines
28 KiB

4 years ago
  1. # -*- coding: utf-8 -*-
  2. """DIDL-Lite (Digital Item Declaration Language) tools for Python."""
  3. # Useful links:
  4. # http://upnp.org/specs/av/UPnP-av-ContentDirectory-v2-Service.pdf
  5. # http://www.upnp.org/schemas/av/didl-lite-v2.xsd
  6. # http://xml.coverpages.org/mpeg21-didl.html
  7. import re
  8. from typing import Any, Dict, List, Optional # noqa: F401 pylint: disable=unused-import
  9. from xml.etree import ElementTree as ET
  10. NAMESPACES = {
  11. 'didl_lite': 'urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/',
  12. 'dc': 'http://purl.org/dc/elements/1.1/',
  13. 'upnp': 'urn:schemas-upnp-org:metadata-1-0/upnp/',
  14. 'xsi': 'http://www.w3.org/2001/XMLSchema-instance',
  15. }
  16. def _ns_tag(tag: str) -> str:
  17. """
  18. Expand namespace-alias to url.
  19. E.g.,
  20. _ns_tag('didl_lite:item') -> '{urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/}item'
  21. """
  22. if ':' not in tag:
  23. return tag
  24. namespace, tag = tag.split(':')
  25. namespace_uri = NAMESPACES[namespace]
  26. return '{{{0}}}{1}'.format(namespace_uri, tag)
  27. def _namespace_tag(namespaced_tag: str) -> str:
  28. """
  29. Extract namespace and tag from namespaced-tag.
  30. E.g., _namespace_tag('{urn:schemas-upnp-org:metadata-1-0/upnp/}class') ->
  31. 'urn:schemas-upnp-org:metadata-1-0/upnp/', 'class'
  32. """
  33. if '}' not in namespaced_tag:
  34. return None, namespaced_tag
  35. idx = namespaced_tag.index('}')
  36. namespace = namespaced_tag[1:idx]
  37. tag = namespaced_tag[idx + 1:]
  38. return namespace, tag
  39. def _to_camel_case(name):
  40. sub1 = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', name)
  41. return re.sub('([a-z0-9])([A-Z])', r'\1_\2', sub1).lower()
  42. def _didl_property_def_key(didl_property_def):
  43. """Get Python property key for didl_property_def."""
  44. if didl_property_def[1].startswith('@'):
  45. return _to_camel_case(didl_property_def[1].replace('@', ''))
  46. return _to_camel_case(didl_property_def[1].replace('@', '_'))
  47. # region: DidlObjects
  48. class DidlObject:
  49. """DIDL Ojbect."""
  50. tag = None # type: Optional[str]
  51. upnp_class = 'object'
  52. didl_properties_defs = [
  53. ('didl_lite', '@id', 'R'),
  54. ('didl_lite', '@parentID', 'R'),
  55. ('didl_lite', '@restricted', 'R'),
  56. ('dc', 'title', 'R'),
  57. ('upnp', 'class', 'R'),
  58. ('dc', 'creator', 'O'),
  59. ('didl_lite', 'res', 'O'),
  60. ('upnp', 'writeStatus', 'O'),
  61. ]
  62. def __init__(self, id="", parent_id="", descriptors=None, **properties):
  63. """Initializer."""
  64. # pylint: disable=invalid-name,redefined-builtin
  65. properties['id'] = id
  66. properties['parent_id'] = parent_id
  67. properties['class'] = self.upnp_class
  68. self._ensure_required_properties(**properties)
  69. self._set_properties(**properties)
  70. self.resources = properties.get('resources') or []
  71. self.descriptors = descriptors if descriptors else []
  72. def _ensure_required_properties(self, **properties):
  73. """Check if all required properties are given."""
  74. for property_def in self.didl_properties_defs:
  75. key = _didl_property_def_key(property_def)
  76. if property_def[2] == 'R' and key not in properties:
  77. raise Exception(key + ' is mandatory')
  78. def _set_properties(self, **properties):
  79. """Set attributes from properties."""
  80. # ensure we have default/known slots
  81. for property_def in self.didl_properties_defs:
  82. key = _didl_property_def_key(property_def)
  83. setattr(self, key, None)
  84. for key, value in properties.items():
  85. setattr(self, key, value)
  86. @classmethod
  87. def from_xml(cls, xml_el: ET.Element):
  88. """
  89. Initialize from an XML node.
  90. I.e., parse XML and return instance.
  91. """
  92. # pylint: disable=too-many-locals
  93. properties = {} # type: Dict[str, Any]
  94. # attributes
  95. for attr_key, attr_value in xml_el.attrib.items():
  96. key = _to_camel_case(attr_key)
  97. properties[key] = attr_value
  98. # child-nodes
  99. for xml_child_node in xml_el:
  100. if xml_child_node.tag == _ns_tag('didl_lite:res'):
  101. continue
  102. _, tag = _namespace_tag(xml_child_node.tag)
  103. key = _to_camel_case(tag)
  104. value = xml_child_node.text
  105. properties[key] = value
  106. # attributes of child nodes
  107. parent_key = key
  108. for attr_key, attr_value in xml_child_node.attrib.items():
  109. key = parent_key + '_' + _to_camel_case(attr_key)
  110. properties[key] = attr_value
  111. # resources
  112. resources = []
  113. for res_el in xml_el.findall('./didl_lite:res', NAMESPACES):
  114. resource = Resource.from_xml(res_el)
  115. resources.append(resource)
  116. properties['resources'] = resources
  117. # descriptors
  118. descriptors = []
  119. for desc_el in xml_el.findall('./didl_lite:desc', NAMESPACES):
  120. descriptor = Descriptor.from_xml(desc_el)
  121. descriptors.append(descriptor)
  122. return cls(descriptors=descriptors, **properties)
  123. def to_xml(self) -> ET.Element:
  124. """Convert self to XML Element."""
  125. item_el = ET.Element(_ns_tag(self.tag))
  126. elements = {'': item_el}
  127. # properties
  128. for property_def in self.didl_properties_defs:
  129. if '@' in property_def[1]:
  130. continue
  131. key = _didl_property_def_key(property_def)
  132. if getattr(self, key) is None or \
  133. key == 'res': # no resources, handled later on
  134. continue
  135. tag = property_def[0] + ':' + property_def[1]
  136. property_el = ET.Element(_ns_tag(tag), {})
  137. property_el.text = getattr(self, key)
  138. item_el.append(property_el)
  139. elements[property_def[1]] = property_el
  140. # attributes and property@attributes
  141. for property_def in self.didl_properties_defs:
  142. if '@' not in property_def[1]:
  143. continue
  144. key = _didl_property_def_key(property_def)
  145. value = getattr(self, key)
  146. if value is None:
  147. continue
  148. el_name, attr_name = property_def[1].split('@')
  149. property_el = elements[el_name]
  150. property_el.attrib[attr_name] = value
  151. # resource
  152. for resource in self.resources:
  153. res_el = resource.to_xml()
  154. item_el.append(res_el)
  155. # descriptor
  156. for descriptor in self.descriptors:
  157. desc_el = descriptor.to_xml()
  158. item_el.append(desc_el)
  159. return item_el
  160. # region: items
  161. class Item(DidlObject):
  162. """DIDL Item."""
  163. # pylint: disable=too-few-public-methods
  164. tag = 'item'
  165. upnp_class = 'object.item'
  166. didl_properties_defs = DidlObject.didl_properties_defs + [
  167. ('didl_lite', '@refID', 'O'), # actually, R, but ignore for now
  168. ('upnp', 'bookmarkID', 'O'),
  169. ]
  170. class ImageItem(Item):
  171. """DIDL Image Item."""
  172. # pylint: disable=too-few-public-methods
  173. upnp_class = 'object.item.imageItem'
  174. didl_properties_defs = Item.didl_properties_defs + [
  175. ('upnp', 'longDescription', 'O'),
  176. ('upnp', 'storageMedium', 'O'),
  177. ('upnp', 'rating', 'O'),
  178. ('dc', 'description', 'O'),
  179. ('dc', 'publisher', 'O'),
  180. ('dc', 'date', 'O'),
  181. ('dc', 'rights', 'O'),
  182. ]
  183. class Photo(ImageItem):
  184. """DIDL Photo."""
  185. # pylint: disable=too-few-public-methods
  186. upnp_class = 'object.item.imageItem.photo'
  187. didl_properties_defs = ImageItem.didl_properties_defs + [
  188. ('upnp', 'album', 'O'),
  189. ]
  190. class AudioItem(Item):
  191. """DIDL Audio Item."""
  192. # pylint: disable=too-few-public-methods
  193. upnp_class = 'object.item.audioItem'
  194. didl_properties_defs = Item.didl_properties_defs + [
  195. ('upnp', 'genre', 'O'),
  196. ('dc', 'description', 'O'),
  197. ('upnp', 'longDescription', 'O'),
  198. ('dc', 'publisher', 'O'),
  199. ('dc', 'language', 'O'),
  200. ('dc', 'relation', 'O'),
  201. ('dc', 'rights', 'O'),
  202. ]
  203. class MusicTrack(AudioItem):
  204. """DIDL Music Track."""
  205. # pylint: disable=too-few-public-methods
  206. upnp_class = 'object.item.audioItem.musicTrack'
  207. didl_properties_defs = AudioItem.didl_properties_defs + [
  208. ('upnp', 'artist', 'O'),
  209. ('upnp', 'album', 'O'),
  210. ('upnp', 'originalTrackNumber', 'O'),
  211. ('upnp', 'playlist', 'O'),
  212. ('upnp', 'storageMedium', 'O'),
  213. ('dc', 'contributor', 'O'),
  214. ('dc', 'date', 'O'),
  215. ]
  216. class AudioBroadcast(AudioItem):
  217. """DIDL Audio Broadcast."""
  218. # pylint: disable=too-few-public-methods
  219. upnp_class = 'object.item.audioItem.audioBroadcast'
  220. didl_properties_defs = AudioItem.didl_properties_defs + [
  221. ('upnp', 'region', 'O'),
  222. ('upnp', 'radioCallSign', 'O'),
  223. ('upnp', 'radioStationID', 'O'),
  224. ('upnp', 'radioBand', 'O'),
  225. ('upnp', 'channelNr', 'O'),
  226. ('upnp', 'signalStrength', 'O'),
  227. ('upnp', 'signalLocked', 'O'),
  228. ('upnp', 'tuned', 'O'),
  229. ('upnp', 'recordable', 'O'),
  230. ]
  231. class AudioBook(AudioItem):
  232. """DIDL Audio Book."""
  233. # pylint: disable=too-few-public-methods
  234. upnp_class = 'object.item.audioItem.audioBook'
  235. didl_properties_defs = AudioItem.didl_properties_defs + [
  236. ('upnp', 'storageMedium', 'O'),
  237. ('upnp', 'producer', 'O'),
  238. ('dc', 'contributor', 'O'),
  239. ('dc', 'date', 'O'),
  240. ]
  241. class VideoItem(Item):
  242. """DIDL Video Item."""
  243. # pylint: disable=too-few-public-methods
  244. upnp_class = 'object.item.videoItem'
  245. didl_properties_defs = Item.didl_properties_defs + [
  246. ('upnp', 'genre', 'O'),
  247. ('upnp', 'genre@id', 'O'),
  248. ('upnp', 'genre@type', 'O'),
  249. ('upnp', 'longDescription', 'O'),
  250. ('upnp', 'producer', 'O'),
  251. ('upnp', 'rating', 'O'),
  252. ('upnp', 'actor', 'O'),
  253. ('upnp', 'director', 'O'),
  254. ('dc', 'description', 'O'),
  255. ('dc', 'publisher', 'O'),
  256. ('dc', 'language', 'O'),
  257. ('dc', 'relation', 'O'),
  258. ('upnp', 'playbackCount', 'O'),
  259. ('upnp', 'lastPlaybackTime', 'O'),
  260. ('upnp', 'lastPlaybackPosition', 'O'),
  261. ('upnp', 'recordedDayOfWeek', 'O'),
  262. ('upnp', 'srsRecordScheduleID', 'O'),
  263. ]
  264. class Movie(VideoItem):
  265. """DIDL Movie."""
  266. # pylint: disable=too-few-public-methods
  267. upnp_class = 'object.item.videoItem.movie'
  268. didl_properties_defs = VideoItem.didl_properties_defs + [
  269. ('upnp', 'storageMedium', 'O'),
  270. ('upnp', 'DVDRegionCode', 'O'),
  271. ('upnp', 'channelName', 'O'),
  272. ('upnp', 'scheduledStartTime', 'O'),
  273. ('upnp', 'scheduledEndTime', 'O'),
  274. ('upnp', 'programTitle', 'O'),
  275. ('upnp', 'seriesTitle', 'O'),
  276. ('upnp', 'episodeCount', 'O'),
  277. ('upnp', 'episodeNr', 'O'),
  278. ]
  279. class VideoBroadcast(VideoItem):
  280. """DIDL Video Broadcast."""
  281. # pylint: disable=too-few-public-methods
  282. upnp_class = 'object.item.videoItem.videoBroadcast'
  283. didl_properties_defs = VideoItem.didl_properties_defs + [
  284. ('upnp', 'icon', 'O'),
  285. ('upnp', 'region', 'O'),
  286. ('upnp', 'channelNr', 'O'),
  287. ('upnp', 'signalStrength', 'O'),
  288. ('upnp', 'signalLocked', 'O'),
  289. ('upnp', 'tuned', 'O'),
  290. ('upnp', 'recordable', 'O'),
  291. ('upnp', 'callSign', 'O'),
  292. ('upnp', 'price', 'O'),
  293. ('upnp', 'payPerView', 'O'),
  294. ]
  295. class MusicVideoClip(VideoItem):
  296. """DIDL Music Video Clip."""
  297. # pylint: disable=too-few-public-methods
  298. upnp_class = 'object.item.videoItem.musicVideoClip'
  299. didl_properties_defs = VideoItem.didl_properties_defs + [
  300. ('upnp', 'artist', 'O'),
  301. ('upnp', 'storageMedium', 'O'),
  302. ('upnp', 'album', 'O'),
  303. ('upnp', 'scheduledStartTime', 'O'),
  304. ('upnp', 'scheduledStopTime', 'O'),
  305. # ('upnp', 'director', 'O'), # duplicate in standard
  306. ('dc', 'contributor', 'O'),
  307. ('dc', 'date', 'O'),
  308. ]
  309. class PlaylistItem(Item):
  310. """DIDL Playlist Item."""
  311. # pylint: disable=too-few-public-methods
  312. upnp_class = 'object.item.playlistItem'
  313. didl_properties_defs = Item.didl_properties_defs + [
  314. ('upnp', 'artist', 'O'),
  315. ('upnp', 'genre', 'O'),
  316. ('upnp', 'longDescription', 'O'),
  317. ('upnp', 'storageMedium', 'O'),
  318. ('dc', 'description', 'O'),
  319. ('dc', 'date', 'O'),
  320. ('dc', 'language', 'O'),
  321. ]
  322. class TextItem(Item):
  323. """DIDL Text Item."""
  324. # pylint: disable=too-few-public-methods
  325. upnp_class = 'object.item.textItem'
  326. didl_properties_defs = Item.didl_properties_defs + [
  327. ('upnp', 'author', 'O'),
  328. ('upnp', 'res@protection', 'O'),
  329. ('upnp', 'longDescription', 'O'),
  330. ('upnp', 'storageMedium', 'O'),
  331. ('upnp', 'rating', 'O'),
  332. ('dc', 'description', 'O'),
  333. ('dc', 'publisher', 'O'),
  334. ('dc', 'contributor', 'O'),
  335. ('dc', 'date', 'O'),
  336. ('dc', 'relation', 'O'),
  337. ('dc', 'language', 'O'),
  338. ('dc', 'rights', 'O'),
  339. ]
  340. class BookmarkItem(Item):
  341. """DIDL Bookmark Item."""
  342. # pylint: disable=too-few-public-methods
  343. upnp_class = 'object.item.bookmarkItem'
  344. didl_properties_defs = Item.didl_properties_defs + [
  345. ('upnp', 'bookmarkedObjectID', 'R'),
  346. ('upnp', 'neverPlayable', 'O'),
  347. ('upnp', 'deviceUDN', 'R'),
  348. ('upnp', 'serviceType', 'R'),
  349. ('upnp', 'serviceId', 'R'),
  350. ('dc', 'date', 'O'),
  351. ('dc', 'stateVariableCollection', 'R'),
  352. ]
  353. class EpgItem(Item):
  354. """DIDL EPG Item."""
  355. # pylint: disable=too-few-public-methods
  356. upnp_class = 'object.item.epgItem'
  357. didl_properties_defs = Item.didl_properties_defs + [
  358. ('upnp', 'channelGroupName', 'O'),
  359. ('upnp', 'channelGroupName@id', 'O'),
  360. ('upnp', 'epgProviderName', 'O'),
  361. ('upnp', 'serviceProvider', 'O'),
  362. ('upnp', 'channelName', 'O'),
  363. ('upnp', 'channelNr', 'O'),
  364. ('upnp', 'programTitle', 'O'),
  365. ('upnp', 'seriesTitle', 'O'),
  366. ('upnp', 'programID', 'O'),
  367. ('upnp', 'programID@type', 'O'),
  368. ('upnp', 'seriesID', 'O'),
  369. ('upnp', 'seriesID@type', 'O'),
  370. ('upnp', 'channelID', 'O'),
  371. ('upnp', 'channelID@type', 'O'),
  372. ('upnp', 'episodeCount', 'O'),
  373. ('upnp', 'episodeNumber', 'O'),
  374. ('upnp', 'programCode', 'O'),
  375. ('upnp', 'programCode_type', 'O'),
  376. ('upnp', 'rating', 'O'),
  377. ('upnp', 'rating@type', 'O'),
  378. ('upnp', 'episodeType', 'O'),
  379. ('upnp', 'genre', 'O'),
  380. ('upnp', 'genre@id', 'O'),
  381. ('upnp', 'genre@extended', 'O'),
  382. ('upnp', 'artist', 'O'),
  383. ('upnp', 'artist@role', 'O'),
  384. ('upnp', 'actor', 'O'),
  385. ('upnp', 'actor@role', 'O'),
  386. ('upnp', 'author', 'O'),
  387. ('upnp', 'author@role', 'O'),
  388. ('upnp', 'producer', 'O'),
  389. ('upnp', 'director', 'O'),
  390. ('dc', 'publisher', 'O'),
  391. ('dc', 'contributor', 'O'),
  392. ('upnp', 'networkAffiliation', 'O'),
  393. # ('upnp', 'serviceProvider', 'O'), # duplicate in standard
  394. ('upnp', 'price', 'O'),
  395. ('upnp', 'price@currency', 'O'),
  396. ('upnp', 'payPerView', 'O'),
  397. # ('upnp', 'epgProviderName', 'O'), # duplicate in standard
  398. ('dc', 'description', 'O'),
  399. ('upnp', 'longDescription', 'O'),
  400. ('upnp', 'icon', 'O'),
  401. ('upnp', 'region', 'O'),
  402. ('dc', 'language', 'O'),
  403. ('dc', 'relation', 'O'),
  404. ('upnp', 'scheduledStartTime', 'O'),
  405. ('upnp', 'scheduledEndTime', 'O'),
  406. ('upnp', 'recordable', 'O'),
  407. ]
  408. class AudioProgram(EpgItem):
  409. """DIDL Audio Program."""
  410. # pylint: disable=too-few-public-methods
  411. upnp_class = 'object.item.epgItem.audioProgram'
  412. didl_properties_defs = Item.didl_properties_defs + [
  413. ('upnp', 'radioCallSign', 'O'),
  414. ('upnp', 'radioStationID', 'O'),
  415. ('upnp', 'radioBand', 'O'),
  416. ]
  417. class VideoProgram(EpgItem):
  418. """DIDL Video Program."""
  419. # pylint: disable=too-few-public-methods
  420. upnp_class = 'object.item.epgItem.videoProgram'
  421. didl_properties_defs = Item.didl_properties_defs + [
  422. ('upnp', 'price', 'O'),
  423. ('upnp', 'price@currency', 'O'),
  424. ('upnp', 'payPerView', 'O'),
  425. ]
  426. # endregion
  427. # region: containers
  428. class Container(DidlObject, list):
  429. """DIDL Container."""
  430. # pylint: disable=too-few-public-methods
  431. tag = 'container'
  432. upnp_class = 'object.container'
  433. didl_properties_defs = DidlObject.didl_properties_defs + [
  434. ('didl_lite', '@childCount', 'O'),
  435. ('upnp', 'createClass', 'O'),
  436. ('upnp', 'searchClass', 'O'),
  437. ('didl_lite', '@searchable', 'O'),
  438. ('didl_lite', '@neverPlayable', 'O'),
  439. ]
  440. @classmethod
  441. def from_xml(cls, xml_el: ET.Element):
  442. """
  443. Initialize from an XML node.
  444. I.e., parse XML and return instance.
  445. """
  446. instance = super().from_xml(xml_el)
  447. # add all children
  448. didl_objects = from_xml_el(xml_el)
  449. instance.extend(didl_objects) # pylint: disable=no-member
  450. return instance
  451. def to_xml(self) -> ET.Element:
  452. """Convert self to XML Element."""
  453. container_el = super().to_xml()
  454. for didl_object in self:
  455. didl_object_el = didl_object.to_xml()
  456. container_el.append(didl_object_el)
  457. return container_el
  458. class Person(Container):
  459. """DIDL Person."""
  460. # pylint: disable=too-few-public-methods
  461. upnp_class = 'object.container.person'
  462. didl_properties_defs = Container.didl_properties_defs + [
  463. ('dc', 'language', 'O'),
  464. ]
  465. class MusicArtist(Person):
  466. """DIDL Music Artist."""
  467. # pylint: disable=too-few-public-methods
  468. upnp_class = 'object.container.person.musicArtist'
  469. didl_properties_defs = Container.didl_properties_defs + [
  470. ('upnp', 'genre', 'O'),
  471. ('upnp', 'artistDiscographyURI', 'O'),
  472. ]
  473. class PlaylistContainer(Container):
  474. """DIDL Playlist Container."""
  475. # pylint: disable=too-few-public-methods
  476. upnp_class = 'object.container.playlistContainer'
  477. didl_properties_defs = Container.didl_properties_defs + [
  478. ('upnp', 'artist', 'O'),
  479. ('upnp', 'genre', 'O'),
  480. ('upnp', 'longDescription', 'O'),
  481. ('upnp', 'producer', 'O'),
  482. ('upnp', 'storageMedium', 'O'),
  483. ('dc', 'description', 'O'),
  484. ('dc', 'contributor', 'O'),
  485. ('dc', 'date', 'O'),
  486. ('dc', 'language', 'O'),
  487. ('dc', 'rights', 'O'),
  488. ]
  489. class Album(Container):
  490. """DIDL Album."""
  491. # pylint: disable=too-few-public-methods
  492. upnp_class = 'object.container.album'
  493. didl_properties_defs = Container.didl_properties_defs + [
  494. ('upnp', 'storageMedium', 'O'),
  495. ('dc', 'longDescription', 'O'),
  496. ('dc', 'description', 'O'),
  497. ('dc', 'publisher', 'O'),
  498. ('dc', 'contributor', 'O'),
  499. ('dc', 'date', 'O'),
  500. ('dc', 'relation', 'O'),
  501. ('dc', 'rights', 'O'),
  502. ]
  503. class MusicAlbum(Album):
  504. """DIDL Music Album."""
  505. # pylint: disable=too-few-public-methods
  506. upnp_class = 'object.container.album.musicAlbum'
  507. didl_properties_defs = Container.didl_properties_defs + [
  508. ('upnp', 'artist', 'O'),
  509. ('upnp', 'genre', 'O'),
  510. ('upnp', 'producer', 'O'),
  511. ('upnp', 'albumArtURI', 'O'),
  512. ('upnp', 'toc', 'O'),
  513. ]
  514. class PhotoAlbum(Album):
  515. """DIDL Photo Album."""
  516. # pylint: disable=too-few-public-methods
  517. upnp_class = 'object.container.album.photoAlbum'
  518. didl_properties_defs = Container.didl_properties_defs + [
  519. ]
  520. class Genre(Container):
  521. """DIDL Genre."""
  522. # pylint: disable=too-few-public-methods
  523. upnp_class = 'object.container.genre'
  524. didl_properties_defs = Container.didl_properties_defs + [
  525. ('upnp', 'genre', 'O'),
  526. ('upnp', 'longDescription', 'O'),
  527. ('dc', 'description', 'O'),
  528. ]
  529. class MusicGenre(Genre):
  530. """DIDL Music Genre."""
  531. # pylint: disable=too-few-public-methods
  532. upnp_class = 'object.container.genre.musicGenre'
  533. didl_properties_defs = Container.didl_properties_defs + [
  534. ]
  535. class MovieGenre(Genre):
  536. """DIDL Movie Genre."""
  537. # pylint: disable=too-few-public-methods
  538. upnp_class = 'object.container.genre.movieGenre'
  539. didl_properties_defs = Container.didl_properties_defs + [
  540. ]
  541. class ChannelGroup(Container):
  542. """DIDL Channel Group."""
  543. # pylint: disable=too-few-public-methods
  544. upnp_class = 'object.container.channelGroup'
  545. didl_properties_defs = Container.didl_properties_defs + [
  546. ('upnp', 'channelGroupName', 'O'),
  547. ('upnp', 'channelGroupName@id', 'O'),
  548. ('upnp', 'epgProviderName', 'O'),
  549. ('upnp', 'serviceProvider', 'O'),
  550. ('upnp', 'icon', 'O'),
  551. ('upnp', 'region', 'O'),
  552. ]
  553. class AudioChannelGroup(ChannelGroup):
  554. """DIDL Audio Channel Group."""
  555. # pylint: disable=too-few-public-methods
  556. upnp_class = 'object.container.channelGroup.audioChannelGroup'
  557. didl_properties_defs = Container.didl_properties_defs + [
  558. ]
  559. class VideoChannelGroup(ChannelGroup):
  560. """DIDL Video Channel Group."""
  561. # pylint: disable=too-few-public-methods
  562. upnp_class = 'object.container.channelGroup.videoChannelGroup'
  563. didl_properties_defs = Container.didl_properties_defs + [
  564. ]
  565. class EpgContainer(Container):
  566. """DIDL EPG Container."""
  567. # pylint: disable=too-few-public-methods
  568. upnp_class = 'object.container.epgContainer'
  569. didl_properties_defs = Container.didl_properties_defs + [
  570. ('upnp', 'channelGroupName', 'O'),
  571. ('upnp', 'channelGroupName@id', 'O'),
  572. ('upnp', 'epgProviderName', 'O'),
  573. ('upnp', 'serviceProvider', 'O'),
  574. ('upnp', 'channelName', 'O'),
  575. ('upnp', 'channelNr', 'O'),
  576. ('upnp', 'channelID', 'O'),
  577. ('upnp', 'channelID@type', 'O'),
  578. ('upnp', 'radioCallSign', 'O'),
  579. ('upnp', 'radioStationID', 'O'),
  580. ('upnp', 'radioBand', 'O'),
  581. ('upnp', 'callSign', 'O'),
  582. ('upnp', 'networkAffiliation', 'O'),
  583. # ('upnp', 'serviceProvider', 'O'), # duplicate in standard
  584. ('upnp', 'price', 'O'),
  585. ('upnp', 'price@currency', 'O'),
  586. ('upnp', 'payPerView', 'O'),
  587. # ('upnp', 'epgProviderName', 'O'), # duplicate in standard
  588. ('upnp', 'icon', 'O'),
  589. ('upnp', 'region', 'O'),
  590. ('dc', 'language', 'O'),
  591. ('dc', 'relation', 'O'),
  592. ('upnp', 'dateTimeRange', 'O'),
  593. ]
  594. class StorageSystem(Container):
  595. """DIDL Storage System."""
  596. # pylint: disable=too-few-public-methods
  597. upnp_class = 'object.container.storageSystem'
  598. didl_properties_defs = Container.didl_properties_defs + [
  599. ('upnp', 'storageTotal', 'R'),
  600. ('upnp', 'storageUsed', 'R'),
  601. ('upnp', 'storageFree', 'R'),
  602. ('upnp', 'storageMaxPartition', 'R'),
  603. ('upnp', 'storageMedium', 'R'),
  604. ]
  605. class StorageVolume(Container):
  606. """DIDL Storage Volume."""
  607. # pylint: disable=too-few-public-methods
  608. upnp_class = 'object.container.storageVolume'
  609. didl_properties_defs = Container.didl_properties_defs + [
  610. ('upnp', 'storageTotal', 'R'),
  611. ('upnp', 'storageUsed', 'R'),
  612. ('upnp', 'storageFree', 'R'),
  613. ('upnp', 'storageMedium', 'R'),
  614. ]
  615. class StorageFolder(Container):
  616. """DIDL Storage Folder."""
  617. # pylint: disable=too-few-public-methods
  618. upnp_class = 'object.container.storageFolder'
  619. didl_properties_defs = Container.didl_properties_defs + [
  620. ('upnp', 'storageUsed', 'R'),
  621. ]
  622. class BookmarkFolder(Container):
  623. """DIDL Bookmark Folder."""
  624. # pylint: disable=too-few-public-methods
  625. upnp_class = 'object.container.bookmarkFolder'
  626. didl_properties_defs = Container.didl_properties_defs + [
  627. ('upnp', 'genre', 'O'),
  628. ('upnp', 'longDescription', 'O'),
  629. ('dc', 'description', 'O'),
  630. ]
  631. # endregion
  632. class Resource:
  633. """DIDL Resource."""
  634. # pylint: disable=too-few-public-methods,too-many-instance-attributes
  635. def __init__(self, uri, protocol_info, import_uri=None, size=None, duration=None,
  636. bitrate=None, sample_frequency=None, bits_per_sample=None,
  637. nr_audio_channels=None, resolution=None, color_depth=None, protection=None):
  638. """Initializer."""
  639. # pylint: disable=too-many-arguments
  640. self.uri = uri
  641. self.protocol_info = protocol_info
  642. self.import_uri = import_uri
  643. self.size = size
  644. self.duration = duration
  645. self.bitrate = bitrate
  646. self.sample_frequency = sample_frequency
  647. self.bits_per_sample = bits_per_sample
  648. self.nr_audio_channels = nr_audio_channels
  649. self.resolution = resolution
  650. self.color_depth = color_depth
  651. self.protection = protection
  652. @classmethod
  653. def from_xml(cls, xml_node: ET.Element):
  654. """Initialize from an XML node."""
  655. uri = xml_node.text
  656. protocol_info = xml_node.attrib["protocolInfo"]
  657. import_uri = xml_node.attrib.get('importUri')
  658. size = xml_node.attrib.get('size')
  659. duration = xml_node.attrib.get('duration')
  660. bitrate = xml_node.attrib.get('bitrate')
  661. sample_frequency = xml_node.attrib.get('sampleFrequency')
  662. bits_per_sample = xml_node.attrib.get('bitsPerSample')
  663. nr_audio_channels = xml_node.attrib.get('nrAudioChannels')
  664. resolution = xml_node.attrib.get('resolution')
  665. color_depth = xml_node.attrib.get('colorDepth')
  666. protection = xml_node.attrib.get('protection')
  667. return cls(uri, protocol_info,
  668. import_uri=import_uri, size=size, duration=duration,
  669. bitrate=bitrate, sample_frequency=sample_frequency,
  670. bits_per_sample=bits_per_sample,
  671. nr_audio_channels=nr_audio_channels,
  672. resolution=resolution, color_depth=color_depth,
  673. protection=protection)
  674. def to_xml(self) -> ET.Element:
  675. """Convert self to XML."""
  676. attribs = {
  677. 'protocolInfo': self.protocol_info,
  678. }
  679. res_el = ET.Element(_ns_tag('res'), attribs)
  680. res_el.text = self.uri
  681. return res_el
  682. class Descriptor:
  683. """DIDL Descriptor."""
  684. def __init__(self, id, name_space, type=None, text=None):
  685. """Initializer."""
  686. # pylint: disable=invalid-name,redefined-builtin
  687. self.id = id
  688. self.name_space = name_space
  689. self.type = type
  690. self.text = text
  691. @classmethod
  692. def from_xml(cls, xml_node: ET.Element):
  693. """Initialize from an XML node."""
  694. id_ = xml_node.attrib['id']
  695. name_space = xml_node.attrib['nameSpace']
  696. type_ = xml_node.attrib.get('type')
  697. return cls(id_, name_space, type_, xml_node.text)
  698. def to_xml(self) -> ET.Element:
  699. """Convert self to XML."""
  700. attribs = {
  701. 'id': self.id,
  702. 'nameSpace': self.name_space,
  703. 'type': self.type,
  704. }
  705. desc_el = ET.Element(_ns_tag('desc'), attribs)
  706. desc_el.text = self.text
  707. return desc_el
  708. # endregion
  709. def to_xml_string(*objects) -> str:
  710. """Convert items to DIDL-Lite XML string."""
  711. root_el = ET.Element(_ns_tag('DIDL-Lite'), {})
  712. root_el.attrib['xmlns'] = NAMESPACES['didl_lite']
  713. for didl_object in objects:
  714. didl_object_el = didl_object.to_xml()
  715. root_el.append(didl_object_el)
  716. return ET.tostring(root_el)
  717. def from_xml_string(xml_string) -> List[DidlObject]:
  718. """Convert XML string to DIDL Objects."""
  719. xml_el = ET.fromstring(xml_string)
  720. return from_xml_el(xml_el)
  721. def from_xml_el(xml_el: ET.Element) -> List[DidlObject]:
  722. """Convert XML Element to DIDL Objects."""
  723. didl_objects = []
  724. # items and containers, in order
  725. for child_el in xml_el:
  726. if child_el.tag != _ns_tag('didl_lite:item') and \
  727. child_el.tag != _ns_tag('didl_lite:container'):
  728. continue
  729. # construct item
  730. upnp_class = child_el.find('./upnp:class', NAMESPACES)
  731. if upnp_class is None or not upnp_class.text:
  732. continue
  733. didl_object_type = type_by_upnp_class(upnp_class.text)
  734. didl_object = didl_object_type.from_xml(child_el)
  735. didl_objects.append(didl_object)
  736. # descriptors
  737. for desc_el in xml_el.findall('./didl_lite:desc', NAMESPACES):
  738. desc = Descriptor.from_xml(desc_el)
  739. didl_objects.append(desc)
  740. return didl_objects
  741. # upnp_class to python type mapping
  742. def type_by_upnp_class(upnp_class: str) -> type:
  743. """Get DidlObject-type by upnp_class."""
  744. queue = DidlObject.__subclasses__()
  745. while queue:
  746. type_ = queue.pop()
  747. queue.extend(type_.__subclasses__())
  748. if type_.upnp_class == upnp_class:
  749. return type_