python-didl-lite 1.2.4__py3-none-any.whl → 1.4.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
didl_lite/didl_lite.py CHANGED
@@ -1,79 +1,132 @@
1
1
  # -*- coding: utf-8 -*-
2
2
  """DIDL-Lite (Digital Item Declaration Language) tools for Python."""
3
-
4
- # Useful links:
5
- # http://upnp.org/specs/av/UPnP-av-ContentDirectory-v2-Service.pdf
6
- # http://www.upnp.org/schemas/av/didl-lite-v2.xsd
7
- # http://xml.coverpages.org/mpeg21-didl.html
8
-
9
- from typing import Any, Dict # noqa: F401 pylint: disable=unused-import
10
- from typing import cast, List, Optional, Sequence, Type, TypeVar, Union
3
+ # pylint: disable=too-many-lines
4
+
5
+ from typing import (
6
+ Any,
7
+ Dict,
8
+ Iterable,
9
+ List,
10
+ Mapping,
11
+ Optional,
12
+ Sequence,
13
+ Tuple,
14
+ Type,
15
+ TypeVar,
16
+ Union,
17
+ )
11
18
  from xml.etree import ElementTree as ET
12
19
 
13
- import defusedxml.ElementTree # type: ignore
20
+ import defusedxml.ElementTree
21
+
22
+ from .utils import (
23
+ NAMESPACES,
24
+ didl_property_def_key,
25
+ didl_property_key,
26
+ expand_namespace_tag,
27
+ split_namespace_tag,
28
+ to_camel_case,
29
+ )
14
30
 
15
- from .utils import NAMESPACES
16
- from .utils import didl_property_def_key
17
- from .utils import namespace_tag
18
- from .utils import ns_tag
19
- from .utils import to_camel_case
31
+ TDO = TypeVar("TDO", bound="DidlObject") # pylint: disable=invalid-name
32
+ TC = TypeVar("TC", bound="Container") # pylint: disable=invalid-name
33
+ TD = TypeVar("TD", bound="Descriptor") # pylint: disable=invalid-name
34
+ TR = TypeVar("TR", bound="Resource") # pylint: disable=invalid-name
20
35
 
21
36
 
22
- TDO = TypeVar('TDO', bound='DidlObject')
23
- TC = TypeVar('TC', bound='Container')
24
- TD = TypeVar('TD', bound='Descriptor')
25
- TR = TypeVar('TR', bound='Resource')
37
+ class DidlLiteException(Exception):
38
+ """DIDL Lite Exception."""
26
39
 
27
40
 
28
41
  # region: DidlObjects
42
+
43
+ # upnp_class to python type mapping
44
+ _upnp_class_map: Dict[str, Type["DidlObject"]] = {}
45
+ _upnp_class_map_lowercase: Dict[str, Type["DidlObject"]] = {}
46
+
47
+
29
48
  class DidlObject:
30
- """DIDL Ojbect."""
31
-
32
- tag = None # type: Optional[str]
33
- upnp_class = 'object'
34
- didl_properties_defs = [
35
- ('didl_lite', '@id', 'R'),
36
- ('didl_lite', '@parentID', 'R'),
37
- ('didl_lite', '@restricted', 'R'),
38
- ('dc', 'title', 'R'),
39
- ('upnp', 'class', 'R'),
40
- ('dc', 'creator', 'O'),
41
- ('didl_lite', 'res', 'O'),
42
- ('upnp', 'writeStatus', 'O'),
49
+ """DIDL Object."""
50
+
51
+ tag: Optional[str] = None
52
+ upnp_class: str = "object"
53
+ didl_properties_defs: List[Tuple[str, str, str]] = [
54
+ ("didl_lite", "@id", "R"),
55
+ ("didl_lite", "@parentID", "R"),
56
+ ("didl_lite", "@restricted", "R"),
57
+ ("dc", "title", "R"),
58
+ ("upnp", "class", "R"),
59
+ ("dc", "creator", "O"),
60
+ ("didl_lite", "res", "O"),
61
+ ("upnp", "writeStatus", "O"),
43
62
  ]
44
63
 
45
- def __init__(self, id: str = "", parent_id: str = "",
46
- descriptors: Optional[Sequence['Descriptor']] = None, **properties: Any):
47
- """Initializer."""
48
- # pylint: disable=invalid-name,redefined-builtin
49
- properties['id'] = id
50
- properties['parent_id'] = parent_id
51
- properties['class'] = self.upnp_class
52
- self._ensure_required_properties(**properties)
53
- self._set_properties(**properties)
54
-
55
- self.resources = properties.get('resources') or []
64
+ id: str
65
+ parent_id: str
66
+ res: List["Resource"]
67
+ xml_el: Optional[ET.Element]
68
+ descriptors: Sequence["Descriptor"]
69
+
70
+ @classmethod
71
+ def __init_subclass__(cls: Type["DidlObject"], **kwargs: Any) -> None:
72
+ """Create mapping of upnp_class to Python type for fast lookup."""
73
+ super().__init_subclass__(**kwargs)
74
+ assert cls.upnp_class not in _upnp_class_map
75
+ assert cls.upnp_class.lower() not in _upnp_class_map_lowercase
76
+ _upnp_class_map[cls.upnp_class] = cls
77
+ _upnp_class_map_lowercase[cls.upnp_class.lower()] = cls
78
+
79
+ def __init__(
80
+ self,
81
+ id: str = "",
82
+ parent_id: str = "",
83
+ descriptors: Optional[Sequence["Descriptor"]] = None,
84
+ xml_el: Optional[ET.Element] = None,
85
+ strict: bool = True,
86
+ **properties: Any,
87
+ ) -> None:
88
+ """Initialize."""
89
+ # pylint: disable=invalid-name,redefined-builtin,too-many-arguments
90
+ properties["id"] = id
91
+ properties["parent_id"] = parent_id
92
+ properties["class"] = self.upnp_class
93
+ properties["res"] = properties.get("res") or properties.get("resources") or []
94
+ if "resources" in properties:
95
+ del properties["resources"]
96
+ self._ensure_required_properties(strict, properties)
97
+ self._set_property_defaults()
98
+ self._set_properties(properties)
99
+
100
+ self.xml_el = xml_el
56
101
  self.descriptors = descriptors if descriptors else []
57
102
 
58
- def _ensure_required_properties(self, **properties: Any) -> None:
103
+ def _ensure_required_properties(
104
+ self, strict: bool, properties: Mapping[str, Any]
105
+ ) -> None:
59
106
  """Check if all required properties are given."""
107
+ if not strict:
108
+ return
109
+
110
+ python_property_keys = {didl_property_key(key) for key in properties}
111
+
60
112
  for property_def in self.didl_properties_defs:
61
113
  key = didl_property_def_key(property_def)
62
- if property_def[2] == 'R' and key not in properties:
63
- raise Exception(key + ' is mandatory')
114
+ if property_def[2] == "R" and key not in python_property_keys:
115
+ raise DidlLiteException(key + " is mandatory")
64
116
 
65
- def _set_properties(self, **properties: Any) -> None:
66
- """Set attributes from properties."""
67
- # ensure we have default/known slots
117
+ def _set_property_defaults(self) -> None:
118
+ """Ensure we have default/known slots, and set them all to None."""
68
119
  for property_def in self.didl_properties_defs:
69
120
  key = didl_property_def_key(property_def)
70
121
  setattr(self, key, None)
71
122
 
123
+ def _set_properties(self, properties: Mapping[str, Any]) -> None:
124
+ """Set attributes from properties."""
72
125
  for key, value in properties.items():
73
126
  setattr(self, key, value)
74
127
 
75
128
  @classmethod
76
- def from_xml(cls: Type[TDO], xml_el: ET.Element) -> TDO:
129
+ def from_xml(cls: Type[TDO], xml_el: ET.Element, strict: bool = True) -> TDO:
77
130
  """
78
131
  Initialize from an XML node.
79
132
 
@@ -89,10 +142,10 @@ class DidlObject:
89
142
 
90
143
  # child-nodes
91
144
  for xml_child_node in xml_el:
92
- if xml_child_node.tag == ns_tag('didl_lite:res'):
145
+ if xml_child_node.tag == expand_namespace_tag("didl_lite:res"):
93
146
  continue
94
147
 
95
- _, tag = namespace_tag(xml_child_node.tag)
148
+ _, tag = split_namespace_tag(xml_child_node.tag)
96
149
  key = to_camel_case(tag)
97
150
  value = xml_child_node.text
98
151
  properties[key] = value
@@ -100,41 +153,42 @@ class DidlObject:
100
153
  # attributes of child nodes
101
154
  parent_key = key
102
155
  for attr_key, attr_value in xml_child_node.attrib.items():
103
- key = parent_key + '_' + to_camel_case(attr_key)
156
+ key = parent_key + "_" + to_camel_case(attr_key)
104
157
  properties[key] = attr_value
105
158
 
106
159
  # resources
107
160
  resources = []
108
- for res_el in xml_el.findall('./didl_lite:res', NAMESPACES):
161
+ for res_el in xml_el.findall("./didl_lite:res", NAMESPACES):
109
162
  resource = Resource.from_xml(res_el)
110
163
  resources.append(resource)
111
- properties['resources'] = resources
164
+ properties["res"] = properties["resources"] = resources
112
165
 
113
166
  # descriptors
114
167
  descriptors = []
115
- for desc_el in xml_el.findall('./didl_lite:desc', NAMESPACES):
168
+ for desc_el in xml_el.findall("./didl_lite:desc", NAMESPACES):
116
169
  descriptor = Descriptor.from_xml(desc_el)
117
170
  descriptors.append(descriptor)
118
171
 
119
- return cls(descriptors=descriptors, **properties)
172
+ return cls(xml_el=xml_el, descriptors=descriptors, strict=strict, **properties)
120
173
 
121
174
  def to_xml(self) -> ET.Element:
122
175
  """Convert self to XML Element."""
123
176
  assert self.tag is not None
124
177
  item_el = ET.Element(self.tag)
125
- elements = {'': item_el}
178
+ elements = {"": item_el}
126
179
 
127
180
  # properties
128
181
  for property_def in self.didl_properties_defs:
129
- if '@' in property_def[1]:
182
+ if "@" in property_def[1]:
130
183
  continue
131
184
  key = didl_property_def_key(property_def)
132
185
 
133
- if getattr(self, key) is None or \
134
- key == 'res': # no resources, handled later on
186
+ if (
187
+ getattr(self, key) is None or key == "res"
188
+ ): # no resources, handled later on
135
189
  continue
136
190
 
137
- tag = property_def[0] + ':' + property_def[1]
191
+ tag = property_def[0] + ":" + property_def[1]
138
192
  property_el = ET.Element(tag, {})
139
193
  property_el.text = getattr(self, key)
140
194
  item_el.append(property_el)
@@ -142,7 +196,7 @@ class DidlObject:
142
196
 
143
197
  # attributes and property@attributes
144
198
  for property_def in self.didl_properties_defs:
145
- if '@' not in property_def[1]:
199
+ if "@" not in property_def[1]:
146
200
  continue
147
201
 
148
202
  key = didl_property_def_key(property_def)
@@ -150,7 +204,7 @@ class DidlObject:
150
204
  if value is None:
151
205
  continue
152
206
 
153
- el_name, attr_name = property_def[1].split('@')
207
+ el_name, attr_name = property_def[1].split("@")
154
208
  property_el = elements[el_name]
155
209
  property_el.attrib[attr_name] = value
156
210
 
@@ -167,10 +221,35 @@ class DidlObject:
167
221
  return item_el
168
222
 
169
223
  def __getattr__(self, name: str) -> Any:
170
- """Get attribute."""
171
- if name not in self.__dict__:
224
+ """Get attribute, modifying case as needed."""
225
+ if name == "resources":
226
+ return getattr(self, "res")
227
+ if name in self.__dict__:
228
+ return self.__dict__[name]
229
+ cleaned_name = didl_property_key(name)
230
+ if cleaned_name not in self.__dict__:
172
231
  raise AttributeError(name)
173
- return self.__dict__[name]
232
+ return self.__dict__[cleaned_name]
233
+
234
+ def __setattr__(self, name: str, value: Any) -> None:
235
+ """Set attribute, modifying case as needed."""
236
+ if name not in self.__dict__:
237
+ # Redirect to the lower_camel_case version if it's already set,
238
+ # which is the case for all defined didl properties.
239
+ cleaned_name = didl_property_key(name)
240
+ if cleaned_name in self.__dict__:
241
+ name = cleaned_name
242
+ self.__dict__[name] = value
243
+
244
+ def __repr__(self) -> str:
245
+ """Evaluatable string representation of this object."""
246
+ class_name = type(self).__name__
247
+ attr = ", ".join(
248
+ f"{key}={val!r}"
249
+ for key, val in self.__dict__.items()
250
+ if key not in ("class", "xml_el")
251
+ )
252
+ return f"{class_name}({attr})"
174
253
 
175
254
 
176
255
  # region: items
@@ -179,11 +258,11 @@ class Item(DidlObject):
179
258
 
180
259
  # pylint: disable=too-few-public-methods
181
260
 
182
- tag = 'item'
183
- upnp_class = 'object.item'
261
+ tag = "item"
262
+ upnp_class = "object.item"
184
263
  didl_properties_defs = DidlObject.didl_properties_defs + [
185
- ('didl_lite', '@refID', 'O'), # actually, R, but ignore for now
186
- ('upnp', 'bookmarkID', 'O'),
264
+ ("didl_lite", "@refID", "O"), # actually, R, but ignore for now
265
+ ("upnp", "bookmarkID", "O"),
187
266
  ]
188
267
 
189
268
 
@@ -192,15 +271,15 @@ class ImageItem(Item):
192
271
 
193
272
  # pylint: disable=too-few-public-methods
194
273
 
195
- upnp_class = 'object.item.imageItem'
274
+ upnp_class = "object.item.imageItem"
196
275
  didl_properties_defs = Item.didl_properties_defs + [
197
- ('upnp', 'longDescription', 'O'),
198
- ('upnp', 'storageMedium', 'O'),
199
- ('upnp', 'rating', 'O'),
200
- ('dc', 'description', 'O'),
201
- ('dc', 'publisher', 'O'),
202
- ('dc', 'date', 'O'),
203
- ('dc', 'rights', 'O'),
276
+ ("upnp", "longDescription", "O"),
277
+ ("upnp", "storageMedium", "O"),
278
+ ("upnp", "rating", "O"),
279
+ ("dc", "description", "O"),
280
+ ("dc", "publisher", "O"),
281
+ ("dc", "date", "O"),
282
+ ("dc", "rights", "O"),
204
283
  ]
205
284
 
206
285
 
@@ -209,9 +288,9 @@ class Photo(ImageItem):
209
288
 
210
289
  # pylint: disable=too-few-public-methods
211
290
 
212
- upnp_class = 'object.item.imageItem.photo'
291
+ upnp_class = "object.item.imageItem.photo"
213
292
  didl_properties_defs = ImageItem.didl_properties_defs + [
214
- ('upnp', 'album', 'O'),
293
+ ("upnp", "album", "O"),
215
294
  ]
216
295
 
217
296
 
@@ -220,15 +299,15 @@ class AudioItem(Item):
220
299
 
221
300
  # pylint: disable=too-few-public-methods
222
301
 
223
- upnp_class = 'object.item.audioItem'
302
+ upnp_class = "object.item.audioItem"
224
303
  didl_properties_defs = Item.didl_properties_defs + [
225
- ('upnp', 'genre', 'O'),
226
- ('dc', 'description', 'O'),
227
- ('upnp', 'longDescription', 'O'),
228
- ('dc', 'publisher', 'O'),
229
- ('dc', 'language', 'O'),
230
- ('dc', 'relation', 'O'),
231
- ('dc', 'rights', 'O'),
304
+ ("upnp", "genre", "O"),
305
+ ("dc", "description", "O"),
306
+ ("upnp", "longDescription", "O"),
307
+ ("dc", "publisher", "O"),
308
+ ("dc", "language", "O"),
309
+ ("dc", "relation", "O"),
310
+ ("dc", "rights", "O"),
232
311
  ]
233
312
 
234
313
 
@@ -237,15 +316,15 @@ class MusicTrack(AudioItem):
237
316
 
238
317
  # pylint: disable=too-few-public-methods
239
318
 
240
- upnp_class = 'object.item.audioItem.musicTrack'
319
+ upnp_class = "object.item.audioItem.musicTrack"
241
320
  didl_properties_defs = AudioItem.didl_properties_defs + [
242
- ('upnp', 'artist', 'O'),
243
- ('upnp', 'album', 'O'),
244
- ('upnp', 'originalTrackNumber', 'O'),
245
- ('upnp', 'playlist', 'O'),
246
- ('upnp', 'storageMedium', 'O'),
247
- ('dc', 'contributor', 'O'),
248
- ('dc', 'date', 'O'),
321
+ ("upnp", "artist", "O"),
322
+ ("upnp", "album", "O"),
323
+ ("upnp", "originalTrackNumber", "O"),
324
+ ("upnp", "playlist", "O"),
325
+ ("upnp", "storageMedium", "O"),
326
+ ("dc", "contributor", "O"),
327
+ ("dc", "date", "O"),
249
328
  ]
250
329
 
251
330
 
@@ -254,17 +333,17 @@ class AudioBroadcast(AudioItem):
254
333
 
255
334
  # pylint: disable=too-few-public-methods
256
335
 
257
- upnp_class = 'object.item.audioItem.audioBroadcast'
336
+ upnp_class = "object.item.audioItem.audioBroadcast"
258
337
  didl_properties_defs = AudioItem.didl_properties_defs + [
259
- ('upnp', 'region', 'O'),
260
- ('upnp', 'radioCallSign', 'O'),
261
- ('upnp', 'radioStationID', 'O'),
262
- ('upnp', 'radioBand', 'O'),
263
- ('upnp', 'channelNr', 'O'),
264
- ('upnp', 'signalStrength', 'O'),
265
- ('upnp', 'signalLocked', 'O'),
266
- ('upnp', 'tuned', 'O'),
267
- ('upnp', 'recordable', 'O'),
338
+ ("upnp", "region", "O"),
339
+ ("upnp", "radioCallSign", "O"),
340
+ ("upnp", "radioStationID", "O"),
341
+ ("upnp", "radioBand", "O"),
342
+ ("upnp", "channelNr", "O"),
343
+ ("upnp", "signalStrength", "O"),
344
+ ("upnp", "signalLocked", "O"),
345
+ ("upnp", "tuned", "O"),
346
+ ("upnp", "recordable", "O"),
268
347
  ]
269
348
 
270
349
 
@@ -273,12 +352,12 @@ class AudioBook(AudioItem):
273
352
 
274
353
  # pylint: disable=too-few-public-methods
275
354
 
276
- upnp_class = 'object.item.audioItem.audioBook'
355
+ upnp_class = "object.item.audioItem.audioBook"
277
356
  didl_properties_defs = AudioItem.didl_properties_defs + [
278
- ('upnp', 'storageMedium', 'O'),
279
- ('upnp', 'producer', 'O'),
280
- ('dc', 'contributor', 'O'),
281
- ('dc', 'date', 'O'),
357
+ ("upnp", "storageMedium", "O"),
358
+ ("upnp", "producer", "O"),
359
+ ("dc", "contributor", "O"),
360
+ ("dc", "date", "O"),
282
361
  ]
283
362
 
284
363
 
@@ -287,25 +366,25 @@ class VideoItem(Item):
287
366
 
288
367
  # pylint: disable=too-few-public-methods
289
368
 
290
- upnp_class = 'object.item.videoItem'
369
+ upnp_class = "object.item.videoItem"
291
370
  didl_properties_defs = Item.didl_properties_defs + [
292
- ('upnp', 'genre', 'O'),
293
- ('upnp', 'genre@id', 'O'),
294
- ('upnp', 'genre@type', 'O'),
295
- ('upnp', 'longDescription', 'O'),
296
- ('upnp', 'producer', 'O'),
297
- ('upnp', 'rating', 'O'),
298
- ('upnp', 'actor', 'O'),
299
- ('upnp', 'director', 'O'),
300
- ('dc', 'description', 'O'),
301
- ('dc', 'publisher', 'O'),
302
- ('dc', 'language', 'O'),
303
- ('dc', 'relation', 'O'),
304
- ('upnp', 'playbackCount', 'O'),
305
- ('upnp', 'lastPlaybackTime', 'O'),
306
- ('upnp', 'lastPlaybackPosition', 'O'),
307
- ('upnp', 'recordedDayOfWeek', 'O'),
308
- ('upnp', 'srsRecordScheduleID', 'O'),
371
+ ("upnp", "genre", "O"),
372
+ ("upnp", "genre@id", "O"),
373
+ ("upnp", "genre@type", "O"),
374
+ ("upnp", "longDescription", "O"),
375
+ ("upnp", "producer", "O"),
376
+ ("upnp", "rating", "O"),
377
+ ("upnp", "actor", "O"),
378
+ ("upnp", "director", "O"),
379
+ ("dc", "description", "O"),
380
+ ("dc", "publisher", "O"),
381
+ ("dc", "language", "O"),
382
+ ("dc", "relation", "O"),
383
+ ("upnp", "playbackCount", "O"),
384
+ ("upnp", "lastPlaybackTime", "O"),
385
+ ("upnp", "lastPlaybackPosition", "O"),
386
+ ("upnp", "recordedDayOfWeek", "O"),
387
+ ("upnp", "srsRecordScheduleID", "O"),
309
388
  ]
310
389
 
311
390
 
@@ -314,17 +393,18 @@ class Movie(VideoItem):
314
393
 
315
394
  # pylint: disable=too-few-public-methods
316
395
 
317
- upnp_class = 'object.item.videoItem.movie'
396
+ upnp_class = "object.item.videoItem.movie"
318
397
  didl_properties_defs = VideoItem.didl_properties_defs + [
319
- ('upnp', 'storageMedium', 'O'),
320
- ('upnp', 'DVDRegionCode', 'O'),
321
- ('upnp', 'channelName', 'O'),
322
- ('upnp', 'scheduledStartTime', 'O'),
323
- ('upnp', 'scheduledEndTime', 'O'),
324
- ('upnp', 'programTitle', 'O'),
325
- ('upnp', 'seriesTitle', 'O'),
326
- ('upnp', 'episodeCount', 'O'),
327
- ('upnp', 'episodeNr', 'O'),
398
+ ("upnp", "storageMedium", "O"),
399
+ ("upnp", "DVDRegionCode", "O"),
400
+ ("upnp", "channelName", "O"),
401
+ ("upnp", "scheduledStartTime", "O"),
402
+ ("upnp", "scheduledEndTime", "O"),
403
+ ("upnp", "programTitle", "O"),
404
+ ("upnp", "seriesTitle", "O"),
405
+ ("upnp", "episodeCount", "O"),
406
+ ("upnp", "episodeNumber", "O"),
407
+ ("upnp", "episodeSeason", "O"),
328
408
  ]
329
409
 
330
410
 
@@ -333,18 +413,18 @@ class VideoBroadcast(VideoItem):
333
413
 
334
414
  # pylint: disable=too-few-public-methods
335
415
 
336
- upnp_class = 'object.item.videoItem.videoBroadcast'
416
+ upnp_class = "object.item.videoItem.videoBroadcast"
337
417
  didl_properties_defs = VideoItem.didl_properties_defs + [
338
- ('upnp', 'icon', 'O'),
339
- ('upnp', 'region', 'O'),
340
- ('upnp', 'channelNr', 'O'),
341
- ('upnp', 'signalStrength', 'O'),
342
- ('upnp', 'signalLocked', 'O'),
343
- ('upnp', 'tuned', 'O'),
344
- ('upnp', 'recordable', 'O'),
345
- ('upnp', 'callSign', 'O'),
346
- ('upnp', 'price', 'O'),
347
- ('upnp', 'payPerView', 'O'),
418
+ ("upnp", "icon", "O"),
419
+ ("upnp", "region", "O"),
420
+ ("upnp", "channelNr", "O"),
421
+ ("upnp", "signalStrength", "O"),
422
+ ("upnp", "signalLocked", "O"),
423
+ ("upnp", "tuned", "O"),
424
+ ("upnp", "recordable", "O"),
425
+ ("upnp", "callSign", "O"),
426
+ ("upnp", "price", "O"),
427
+ ("upnp", "payPerView", "O"),
348
428
  ]
349
429
 
350
430
 
@@ -353,16 +433,16 @@ class MusicVideoClip(VideoItem):
353
433
 
354
434
  # pylint: disable=too-few-public-methods
355
435
 
356
- upnp_class = 'object.item.videoItem.musicVideoClip'
436
+ upnp_class = "object.item.videoItem.musicVideoClip"
357
437
  didl_properties_defs = VideoItem.didl_properties_defs + [
358
- ('upnp', 'artist', 'O'),
359
- ('upnp', 'storageMedium', 'O'),
360
- ('upnp', 'album', 'O'),
361
- ('upnp', 'scheduledStartTime', 'O'),
362
- ('upnp', 'scheduledStopTime', 'O'),
438
+ ("upnp", "artist", "O"),
439
+ ("upnp", "storageMedium", "O"),
440
+ ("upnp", "album", "O"),
441
+ ("upnp", "scheduledStartTime", "O"),
442
+ ("upnp", "scheduledStopTime", "O"),
363
443
  # ('upnp', 'director', 'O'), # duplicate in standard
364
- ('dc', 'contributor', 'O'),
365
- ('dc', 'date', 'O'),
444
+ ("dc", "contributor", "O"),
445
+ ("dc", "date", "O"),
366
446
  ]
367
447
 
368
448
 
@@ -371,15 +451,15 @@ class PlaylistItem(Item):
371
451
 
372
452
  # pylint: disable=too-few-public-methods
373
453
 
374
- upnp_class = 'object.item.playlistItem'
454
+ upnp_class = "object.item.playlistItem"
375
455
  didl_properties_defs = Item.didl_properties_defs + [
376
- ('upnp', 'artist', 'O'),
377
- ('upnp', 'genre', 'O'),
378
- ('upnp', 'longDescription', 'O'),
379
- ('upnp', 'storageMedium', 'O'),
380
- ('dc', 'description', 'O'),
381
- ('dc', 'date', 'O'),
382
- ('dc', 'language', 'O'),
456
+ ("upnp", "artist", "O"),
457
+ ("upnp", "genre", "O"),
458
+ ("upnp", "longDescription", "O"),
459
+ ("upnp", "storageMedium", "O"),
460
+ ("dc", "description", "O"),
461
+ ("dc", "date", "O"),
462
+ ("dc", "language", "O"),
383
463
  ]
384
464
 
385
465
 
@@ -388,20 +468,20 @@ class TextItem(Item):
388
468
 
389
469
  # pylint: disable=too-few-public-methods
390
470
 
391
- upnp_class = 'object.item.textItem'
471
+ upnp_class = "object.item.textItem"
392
472
  didl_properties_defs = Item.didl_properties_defs + [
393
- ('upnp', 'author', 'O'),
394
- ('upnp', 'res@protection', 'O'),
395
- ('upnp', 'longDescription', 'O'),
396
- ('upnp', 'storageMedium', 'O'),
397
- ('upnp', 'rating', 'O'),
398
- ('dc', 'description', 'O'),
399
- ('dc', 'publisher', 'O'),
400
- ('dc', 'contributor', 'O'),
401
- ('dc', 'date', 'O'),
402
- ('dc', 'relation', 'O'),
403
- ('dc', 'language', 'O'),
404
- ('dc', 'rights', 'O'),
473
+ ("upnp", "author", "O"),
474
+ ("upnp", "res@protection", "O"),
475
+ ("upnp", "longDescription", "O"),
476
+ ("upnp", "storageMedium", "O"),
477
+ ("upnp", "rating", "O"),
478
+ ("dc", "description", "O"),
479
+ ("dc", "publisher", "O"),
480
+ ("dc", "contributor", "O"),
481
+ ("dc", "date", "O"),
482
+ ("dc", "relation", "O"),
483
+ ("dc", "language", "O"),
484
+ ("dc", "rights", "O"),
405
485
  ]
406
486
 
407
487
 
@@ -410,15 +490,15 @@ class BookmarkItem(Item):
410
490
 
411
491
  # pylint: disable=too-few-public-methods
412
492
 
413
- upnp_class = 'object.item.bookmarkItem'
493
+ upnp_class = "object.item.bookmarkItem"
414
494
  didl_properties_defs = Item.didl_properties_defs + [
415
- ('upnp', 'bookmarkedObjectID', 'R'),
416
- ('upnp', 'neverPlayable', 'O'),
417
- ('upnp', 'deviceUDN', 'R'),
418
- ('upnp', 'serviceType', 'R'),
419
- ('upnp', 'serviceId', 'R'),
420
- ('dc', 'date', 'O'),
421
- ('dc', 'stateVariableCollection', 'R'),
495
+ ("upnp", "bookmarkedObjectID", "R"),
496
+ ("upnp", "neverPlayable", "O"),
497
+ ("upnp", "deviceUDN", "R"),
498
+ ("upnp", "serviceType", "R"),
499
+ ("upnp", "serviceId", "R"),
500
+ ("dc", "date", "O"),
501
+ ("dc", "stateVariableCollection", "R"),
422
502
  ]
423
503
 
424
504
 
@@ -427,57 +507,67 @@ class EpgItem(Item):
427
507
 
428
508
  # pylint: disable=too-few-public-methods
429
509
 
430
- upnp_class = 'object.item.epgItem'
510
+ upnp_class = "object.item.epgItem"
431
511
  didl_properties_defs = Item.didl_properties_defs + [
432
- ('upnp', 'channelGroupName', 'O'),
433
- ('upnp', 'channelGroupName@id', 'O'),
434
- ('upnp', 'epgProviderName', 'O'),
435
- ('upnp', 'serviceProvider', 'O'),
436
- ('upnp', 'channelName', 'O'),
437
- ('upnp', 'channelNr', 'O'),
438
- ('upnp', 'programTitle', 'O'),
439
- ('upnp', 'seriesTitle', 'O'),
440
- ('upnp', 'programID', 'O'),
441
- ('upnp', 'programID@type', 'O'),
442
- ('upnp', 'seriesID', 'O'),
443
- ('upnp', 'seriesID@type', 'O'),
444
- ('upnp', 'channelID', 'O'),
445
- ('upnp', 'channelID@type', 'O'),
446
- ('upnp', 'episodeCount', 'O'),
447
- ('upnp', 'episodeNumber', 'O'),
448
- ('upnp', 'programCode', 'O'),
449
- ('upnp', 'programCode_type', 'O'),
450
- ('upnp', 'rating', 'O'),
451
- ('upnp', 'rating@type', 'O'),
452
- ('upnp', 'episodeType', 'O'),
453
- ('upnp', 'genre', 'O'),
454
- ('upnp', 'genre@id', 'O'),
455
- ('upnp', 'genre@extended', 'O'),
456
- ('upnp', 'artist', 'O'),
457
- ('upnp', 'artist@role', 'O'),
458
- ('upnp', 'actor', 'O'),
459
- ('upnp', 'actor@role', 'O'),
460
- ('upnp', 'author', 'O'),
461
- ('upnp', 'author@role', 'O'),
462
- ('upnp', 'producer', 'O'),
463
- ('upnp', 'director', 'O'),
464
- ('dc', 'publisher', 'O'),
465
- ('dc', 'contributor', 'O'),
466
- ('upnp', 'networkAffiliation', 'O'),
512
+ ("upnp", "channelGroupName", "O"),
513
+ ("upnp", "channelGroupName@id", "O"),
514
+ ("upnp", "epgProviderName", "O"),
515
+ ("upnp", "serviceProvider", "O"),
516
+ ("upnp", "channelName", "O"),
517
+ ("upnp", "channelNr", "O"),
518
+ ("upnp", "programTitle", "O"),
519
+ ("upnp", "seriesTitle", "O"),
520
+ ("upnp", "programID", "O"),
521
+ ("upnp", "programID@type", "O"),
522
+ ("upnp", "seriesID", "O"),
523
+ ("upnp", "seriesID@type", "O"),
524
+ ("upnp", "channelID", "O"),
525
+ ("upnp", "channelID@type", "O"),
526
+ ("upnp", "channelID@distriNetworkName", "O"),
527
+ ("upnp", "channelID@distriNetworkID", "O"),
528
+ ("upnp", "episodeType", "O"),
529
+ ("upnp", "episodeCount", "O"),
530
+ ("upnp", "episodeNumber", "O"),
531
+ ("upnp", "episodeSeason", "O"),
532
+ ("upnp", "programCode", "O"),
533
+ ("upnp", "programCode@type", "O"),
534
+ ("upnp", "rating", "O"),
535
+ ("upnp", "rating@type", "O"),
536
+ ("upnp", "rating@advice", "O"),
537
+ ("upnp", "rating@equivalentAge", "O"),
538
+ ("upnp", "recommendationID", "O"),
539
+ ("upnp", "recommendationID@type", "O"),
540
+ ("upnp", "genre", "O"),
541
+ ("upnp", "genre@id", "O"),
542
+ ("upnp", "genre@extended", "O"),
543
+ ("upnp", "artist", "O"),
544
+ ("upnp", "artist@role", "O"),
545
+ ("upnp", "actor", "O"),
546
+ ("upnp", "actor@role", "O"),
547
+ ("upnp", "author", "O"),
548
+ ("upnp", "author@role", "O"),
549
+ ("upnp", "producer", "O"),
550
+ ("upnp", "director", "O"),
551
+ ("dc", "publisher", "O"),
552
+ ("dc", "contributor", "O"),
553
+ ("upnp", "callSign", "O"),
554
+ ("upnp", "networkAffiliation", "O"),
467
555
  # ('upnp', 'serviceProvider', 'O'), # duplicate in standard
468
- ('upnp', 'price', 'O'),
469
- ('upnp', 'price@currency', 'O'),
470
- ('upnp', 'payPerView', 'O'),
556
+ ("upnp", "price", "O"),
557
+ ("upnp", "price@currency", "O"),
558
+ ("upnp", "payPerView", "O"),
471
559
  # ('upnp', 'epgProviderName', 'O'), # duplicate in standard
472
- ('dc', 'description', 'O'),
473
- ('upnp', 'longDescription', 'O'),
474
- ('upnp', 'icon', 'O'),
475
- ('upnp', 'region', 'O'),
476
- ('dc', 'language', 'O'),
477
- ('dc', 'relation', 'O'),
478
- ('upnp', 'scheduledStartTime', 'O'),
479
- ('upnp', 'scheduledEndTime', 'O'),
480
- ('upnp', 'recordable', 'O'),
560
+ ("dc", "description", "O"),
561
+ ("upnp", "longDescription", "O"),
562
+ ("upnp", "icon", "O"),
563
+ ("upnp", "region", "O"),
564
+ ("upnp", "rights", "O"),
565
+ ("dc", "language", "O"),
566
+ ("dc", "relation", "O"),
567
+ ("upnp", "scheduledStartTime", "O"),
568
+ ("upnp", "scheduledEndTime", "O"),
569
+ ("upnp", "recordable", "O"),
570
+ ("upnp", "foreignMetadata", "O"),
481
571
  ]
482
572
 
483
573
 
@@ -486,11 +576,11 @@ class AudioProgram(EpgItem):
486
576
 
487
577
  # pylint: disable=too-few-public-methods
488
578
 
489
- upnp_class = 'object.item.epgItem.audioProgram'
579
+ upnp_class = "object.item.epgItem.audioProgram"
490
580
  didl_properties_defs = Item.didl_properties_defs + [
491
- ('upnp', 'radioCallSign', 'O'),
492
- ('upnp', 'radioStationID', 'O'),
493
- ('upnp', 'radioBand', 'O'),
581
+ ("upnp", "radioCallSign", "O"),
582
+ ("upnp", "radioStationID", "O"),
583
+ ("upnp", "radioBand", "O"),
494
584
  ]
495
585
 
496
586
 
@@ -499,12 +589,14 @@ class VideoProgram(EpgItem):
499
589
 
500
590
  # pylint: disable=too-few-public-methods
501
591
 
502
- upnp_class = 'object.item.epgItem.videoProgram'
592
+ upnp_class = "object.item.epgItem.videoProgram"
503
593
  didl_properties_defs = Item.didl_properties_defs + [
504
- ('upnp', 'price', 'O'),
505
- ('upnp', 'price@currency', 'O'),
506
- ('upnp', 'payPerView', 'O'),
594
+ ("upnp", "price", "O"),
595
+ ("upnp", "price@currency", "O"),
596
+ ("upnp", "payPerView", "O"),
507
597
  ]
598
+
599
+
508
600
  # endregion
509
601
 
510
602
 
@@ -514,27 +606,42 @@ class Container(DidlObject, list):
514
606
 
515
607
  # pylint: disable=too-few-public-methods
516
608
 
517
- tag = 'container'
518
- upnp_class = 'object.container'
609
+ tag = "container"
610
+ upnp_class = "object.container"
519
611
  didl_properties_defs = DidlObject.didl_properties_defs + [
520
- ('didl_lite', '@childCount', 'O'),
521
- ('upnp', 'createClass', 'O'),
522
- ('upnp', 'searchClass', 'O'),
523
- ('didl_lite', '@searchable', 'O'),
524
- ('didl_lite', '@neverPlayable', 'O'),
612
+ ("didl_lite", "@childCount", "O"),
613
+ ("upnp", "createClass", "O"),
614
+ ("upnp", "searchClass", "O"),
615
+ ("didl_lite", "@searchable", "O"),
616
+ ("didl_lite", "@neverPlayable", "O"),
525
617
  ]
526
618
 
619
+ def __init__(
620
+ self,
621
+ id: str = "",
622
+ parent_id: str = "",
623
+ descriptors: Optional[Sequence["Descriptor"]] = None,
624
+ xml_el: Optional[ET.Element] = None,
625
+ strict: bool = True,
626
+ children: Iterable[DidlObject] = (),
627
+ **properties: Any,
628
+ ) -> None:
629
+ """Initialize."""
630
+ # pylint: disable=redefined-builtin,too-many-arguments
631
+ super().__init__(id, parent_id, descriptors, xml_el, strict, **properties)
632
+ self.extend(children)
633
+
527
634
  @classmethod
528
- def from_xml(cls: Type[TC], xml_el: ET.Element) -> TC:
635
+ def from_xml(cls: Type[TC], xml_el: ET.Element, strict: bool = True) -> TC:
529
636
  """
530
637
  Initialize from an XML node.
531
638
 
532
639
  I.e., parse XML and return instance.
533
640
  """
534
- instance = super().from_xml(xml_el)
641
+ instance = super().from_xml(xml_el, strict)
535
642
 
536
643
  # add all children
537
- didl_objects = from_xml_el(xml_el)
644
+ didl_objects = from_xml_el(xml_el, strict)
538
645
  instance.extend(didl_objects) # pylint: disable=no-member
539
646
 
540
647
  return instance
@@ -549,15 +656,26 @@ class Container(DidlObject, list):
549
656
 
550
657
  return container_el
551
658
 
659
+ def __repr__(self) -> str:
660
+ """Evaluatable string representation of this object."""
661
+ class_name = type(self).__name__
662
+ attr = ", ".join(
663
+ f"{key}={val!r}"
664
+ for key, val in self.__dict__.items()
665
+ if key not in ("class", "xml_el")
666
+ )
667
+ children_repr = ", ".join(repr(child) for child in self)
668
+ return f"{class_name}({attr}, children=[{children_repr}])"
669
+
552
670
 
553
671
  class Person(Container):
554
672
  """DIDL Person."""
555
673
 
556
674
  # pylint: disable=too-few-public-methods
557
675
 
558
- upnp_class = 'object.container.person'
676
+ upnp_class = "object.container.person"
559
677
  didl_properties_defs = Container.didl_properties_defs + [
560
- ('dc', 'language', 'O'),
678
+ ("dc", "language", "O"),
561
679
  ]
562
680
 
563
681
 
@@ -566,10 +684,10 @@ class MusicArtist(Person):
566
684
 
567
685
  # pylint: disable=too-few-public-methods
568
686
 
569
- upnp_class = 'object.container.person.musicArtist'
687
+ upnp_class = "object.container.person.musicArtist"
570
688
  didl_properties_defs = Container.didl_properties_defs + [
571
- ('upnp', 'genre', 'O'),
572
- ('upnp', 'artistDiscographyURI', 'O'),
689
+ ("upnp", "genre", "O"),
690
+ ("upnp", "artistDiscographyURI", "O"),
573
691
  ]
574
692
 
575
693
 
@@ -578,18 +696,18 @@ class PlaylistContainer(Container):
578
696
 
579
697
  # pylint: disable=too-few-public-methods
580
698
 
581
- upnp_class = 'object.container.playlistContainer'
699
+ upnp_class = "object.container.playlistContainer"
582
700
  didl_properties_defs = Container.didl_properties_defs + [
583
- ('upnp', 'artist', 'O'),
584
- ('upnp', 'genre', 'O'),
585
- ('upnp', 'longDescription', 'O'),
586
- ('upnp', 'producer', 'O'),
587
- ('upnp', 'storageMedium', 'O'),
588
- ('dc', 'description', 'O'),
589
- ('dc', 'contributor', 'O'),
590
- ('dc', 'date', 'O'),
591
- ('dc', 'language', 'O'),
592
- ('dc', 'rights', 'O'),
701
+ ("upnp", "artist", "O"),
702
+ ("upnp", "genre", "O"),
703
+ ("upnp", "longDescription", "O"),
704
+ ("upnp", "producer", "O"),
705
+ ("upnp", "storageMedium", "O"),
706
+ ("dc", "description", "O"),
707
+ ("dc", "contributor", "O"),
708
+ ("dc", "date", "O"),
709
+ ("dc", "language", "O"),
710
+ ("dc", "rights", "O"),
593
711
  ]
594
712
 
595
713
 
@@ -598,16 +716,16 @@ class Album(Container):
598
716
 
599
717
  # pylint: disable=too-few-public-methods
600
718
 
601
- upnp_class = 'object.container.album'
719
+ upnp_class = "object.container.album"
602
720
  didl_properties_defs = Container.didl_properties_defs + [
603
- ('upnp', 'storageMedium', 'O'),
604
- ('dc', 'longDescription', 'O'),
605
- ('dc', 'description', 'O'),
606
- ('dc', 'publisher', 'O'),
607
- ('dc', 'contributor', 'O'),
608
- ('dc', 'date', 'O'),
609
- ('dc', 'relation', 'O'),
610
- ('dc', 'rights', 'O'),
721
+ ("upnp", "storageMedium", "O"),
722
+ ("dc", "longDescription", "O"),
723
+ ("dc", "description", "O"),
724
+ ("dc", "publisher", "O"),
725
+ ("dc", "contributor", "O"),
726
+ ("dc", "date", "O"),
727
+ ("dc", "relation", "O"),
728
+ ("dc", "rights", "O"),
611
729
  ]
612
730
 
613
731
 
@@ -616,13 +734,13 @@ class MusicAlbum(Album):
616
734
 
617
735
  # pylint: disable=too-few-public-methods
618
736
 
619
- upnp_class = 'object.container.album.musicAlbum'
737
+ upnp_class = "object.container.album.musicAlbum"
620
738
  didl_properties_defs = Container.didl_properties_defs + [
621
- ('upnp', 'artist', 'O'),
622
- ('upnp', 'genre', 'O'),
623
- ('upnp', 'producer', 'O'),
624
- ('upnp', 'albumArtURI', 'O'),
625
- ('upnp', 'toc', 'O'),
739
+ ("upnp", "artist", "O"),
740
+ ("upnp", "genre", "O"),
741
+ ("upnp", "producer", "O"),
742
+ ("upnp", "albumArtURI", "O"),
743
+ ("upnp", "toc", "O"),
626
744
  ]
627
745
 
628
746
 
@@ -631,9 +749,8 @@ class PhotoAlbum(Album):
631
749
 
632
750
  # pylint: disable=too-few-public-methods
633
751
 
634
- upnp_class = 'object.container.album.photoAlbum'
635
- didl_properties_defs = Container.didl_properties_defs + [
636
- ]
752
+ upnp_class = "object.container.album.photoAlbum"
753
+ didl_properties_defs = Container.didl_properties_defs + []
637
754
 
638
755
 
639
756
  class Genre(Container):
@@ -641,11 +758,11 @@ class Genre(Container):
641
758
 
642
759
  # pylint: disable=too-few-public-methods
643
760
 
644
- upnp_class = 'object.container.genre'
761
+ upnp_class = "object.container.genre"
645
762
  didl_properties_defs = Container.didl_properties_defs + [
646
- ('upnp', 'genre', 'O'),
647
- ('upnp', 'longDescription', 'O'),
648
- ('dc', 'description', 'O'),
763
+ ("upnp", "genre", "O"),
764
+ ("upnp", "longDescription", "O"),
765
+ ("dc", "description", "O"),
649
766
  ]
650
767
 
651
768
 
@@ -654,9 +771,8 @@ class MusicGenre(Genre):
654
771
 
655
772
  # pylint: disable=too-few-public-methods
656
773
 
657
- upnp_class = 'object.container.genre.musicGenre'
658
- didl_properties_defs = Container.didl_properties_defs + [
659
- ]
774
+ upnp_class = "object.container.genre.musicGenre"
775
+ didl_properties_defs = Container.didl_properties_defs + []
660
776
 
661
777
 
662
778
  class MovieGenre(Genre):
@@ -664,9 +780,8 @@ class MovieGenre(Genre):
664
780
 
665
781
  # pylint: disable=too-few-public-methods
666
782
 
667
- upnp_class = 'object.container.genre.movieGenre'
668
- didl_properties_defs = Container.didl_properties_defs + [
669
- ]
783
+ upnp_class = "object.container.genre.movieGenre"
784
+ didl_properties_defs = Container.didl_properties_defs + []
670
785
 
671
786
 
672
787
  class ChannelGroup(Container):
@@ -674,14 +789,14 @@ class ChannelGroup(Container):
674
789
 
675
790
  # pylint: disable=too-few-public-methods
676
791
 
677
- upnp_class = 'object.container.channelGroup'
792
+ upnp_class = "object.container.channelGroup"
678
793
  didl_properties_defs = Container.didl_properties_defs + [
679
- ('upnp', 'channelGroupName', 'O'),
680
- ('upnp', 'channelGroupName@id', 'O'),
681
- ('upnp', 'epgProviderName', 'O'),
682
- ('upnp', 'serviceProvider', 'O'),
683
- ('upnp', 'icon', 'O'),
684
- ('upnp', 'region', 'O'),
794
+ ("upnp", "channelGroupName", "O"),
795
+ ("upnp", "channelGroupName@id", "O"),
796
+ ("upnp", "epgProviderName", "O"),
797
+ ("upnp", "serviceProvider", "O"),
798
+ ("upnp", "icon", "O"),
799
+ ("upnp", "region", "O"),
685
800
  ]
686
801
 
687
802
 
@@ -690,9 +805,8 @@ class AudioChannelGroup(ChannelGroup):
690
805
 
691
806
  # pylint: disable=too-few-public-methods
692
807
 
693
- upnp_class = 'object.container.channelGroup.audioChannelGroup'
694
- didl_properties_defs = Container.didl_properties_defs + [
695
- ]
808
+ upnp_class = "object.container.channelGroup.audioChannelGroup"
809
+ didl_properties_defs = Container.didl_properties_defs + []
696
810
 
697
811
 
698
812
  class VideoChannelGroup(ChannelGroup):
@@ -700,9 +814,8 @@ class VideoChannelGroup(ChannelGroup):
700
814
 
701
815
  # pylint: disable=too-few-public-methods
702
816
 
703
- upnp_class = 'object.container.channelGroup.videoChannelGroup'
704
- didl_properties_defs = Container.didl_properties_defs + [
705
- ]
817
+ upnp_class = "object.container.channelGroup.videoChannelGroup"
818
+ didl_properties_defs = Container.didl_properties_defs + []
706
819
 
707
820
 
708
821
  class EpgContainer(Container):
@@ -710,31 +823,31 @@ class EpgContainer(Container):
710
823
 
711
824
  # pylint: disable=too-few-public-methods
712
825
 
713
- upnp_class = 'object.container.epgContainer'
826
+ upnp_class = "object.container.epgContainer"
714
827
  didl_properties_defs = Container.didl_properties_defs + [
715
- ('upnp', 'channelGroupName', 'O'),
716
- ('upnp', 'channelGroupName@id', 'O'),
717
- ('upnp', 'epgProviderName', 'O'),
718
- ('upnp', 'serviceProvider', 'O'),
719
- ('upnp', 'channelName', 'O'),
720
- ('upnp', 'channelNr', 'O'),
721
- ('upnp', 'channelID', 'O'),
722
- ('upnp', 'channelID@type', 'O'),
723
- ('upnp', 'radioCallSign', 'O'),
724
- ('upnp', 'radioStationID', 'O'),
725
- ('upnp', 'radioBand', 'O'),
726
- ('upnp', 'callSign', 'O'),
727
- ('upnp', 'networkAffiliation', 'O'),
828
+ ("upnp", "channelGroupName", "O"),
829
+ ("upnp", "channelGroupName@id", "O"),
830
+ ("upnp", "epgProviderName", "O"),
831
+ ("upnp", "serviceProvider", "O"),
832
+ ("upnp", "channelName", "O"),
833
+ ("upnp", "channelNr", "O"),
834
+ ("upnp", "channelID", "O"),
835
+ ("upnp", "channelID@type", "O"),
836
+ ("upnp", "radioCallSign", "O"),
837
+ ("upnp", "radioStationID", "O"),
838
+ ("upnp", "radioBand", "O"),
839
+ ("upnp", "callSign", "O"),
840
+ ("upnp", "networkAffiliation", "O"),
728
841
  # ('upnp', 'serviceProvider', 'O'), # duplicate in standard
729
- ('upnp', 'price', 'O'),
730
- ('upnp', 'price@currency', 'O'),
731
- ('upnp', 'payPerView', 'O'),
842
+ ("upnp", "price", "O"),
843
+ ("upnp", "price@currency", "O"),
844
+ ("upnp", "payPerView", "O"),
732
845
  # ('upnp', 'epgProviderName', 'O'), # duplicate in standard
733
- ('upnp', 'icon', 'O'),
734
- ('upnp', 'region', 'O'),
735
- ('dc', 'language', 'O'),
736
- ('dc', 'relation', 'O'),
737
- ('upnp', 'dateTimeRange', 'O'),
846
+ ("upnp", "icon", "O"),
847
+ ("upnp", "region", "O"),
848
+ ("dc", "language", "O"),
849
+ ("dc", "relation", "O"),
850
+ ("upnp", "dateTimeRange", "O"),
738
851
  ]
739
852
 
740
853
 
@@ -743,13 +856,13 @@ class StorageSystem(Container):
743
856
 
744
857
  # pylint: disable=too-few-public-methods
745
858
 
746
- upnp_class = 'object.container.storageSystem'
859
+ upnp_class = "object.container.storageSystem"
747
860
  didl_properties_defs = Container.didl_properties_defs + [
748
- ('upnp', 'storageTotal', 'R'),
749
- ('upnp', 'storageUsed', 'R'),
750
- ('upnp', 'storageFree', 'R'),
751
- ('upnp', 'storageMaxPartition', 'R'),
752
- ('upnp', 'storageMedium', 'R'),
861
+ ("upnp", "storageTotal", "R"),
862
+ ("upnp", "storageUsed", "R"),
863
+ ("upnp", "storageFree", "R"),
864
+ ("upnp", "storageMaxPartition", "R"),
865
+ ("upnp", "storageMedium", "R"),
753
866
  ]
754
867
 
755
868
 
@@ -758,12 +871,12 @@ class StorageVolume(Container):
758
871
 
759
872
  # pylint: disable=too-few-public-methods
760
873
 
761
- upnp_class = 'object.container.storageVolume'
874
+ upnp_class = "object.container.storageVolume"
762
875
  didl_properties_defs = Container.didl_properties_defs + [
763
- ('upnp', 'storageTotal', 'R'),
764
- ('upnp', 'storageUsed', 'R'),
765
- ('upnp', 'storageFree', 'R'),
766
- ('upnp', 'storageMedium', 'R'),
876
+ ("upnp", "storageTotal", "R"),
877
+ ("upnp", "storageUsed", "R"),
878
+ ("upnp", "storageFree", "R"),
879
+ ("upnp", "storageMedium", "R"),
767
880
  ]
768
881
 
769
882
 
@@ -772,9 +885,9 @@ class StorageFolder(Container):
772
885
 
773
886
  # pylint: disable=too-few-public-methods
774
887
 
775
- upnp_class = 'object.container.storageFolder'
888
+ upnp_class = "object.container.storageFolder"
776
889
  didl_properties_defs = Container.didl_properties_defs + [
777
- ('upnp', 'storageUsed', 'R'),
890
+ ("upnp", "storageUsed", "R"),
778
891
  ]
779
892
 
780
893
 
@@ -783,12 +896,14 @@ class BookmarkFolder(Container):
783
896
 
784
897
  # pylint: disable=too-few-public-methods
785
898
 
786
- upnp_class = 'object.container.bookmarkFolder'
899
+ upnp_class = "object.container.bookmarkFolder"
787
900
  didl_properties_defs = Container.didl_properties_defs + [
788
- ('upnp', 'genre', 'O'),
789
- ('upnp', 'longDescription', 'O'),
790
- ('dc', 'description', 'O'),
901
+ ("upnp", "genre", "O"),
902
+ ("upnp", "longDescription", "O"),
903
+ ("dc", "description", "O"),
791
904
  ]
905
+
906
+
792
907
  # endregion
793
908
 
794
909
 
@@ -797,13 +912,23 @@ class Resource:
797
912
 
798
913
  # pylint: disable=too-few-public-methods,too-many-instance-attributes
799
914
 
800
- def __init__(self, uri: Optional[str], protocol_info: str, import_uri: Optional[str] = None,
801
- size: Optional[str] = None, duration: Optional[str] = None,
802
- bitrate: Optional[str] = None, sample_frequency: Optional[str] = None,
803
- bits_per_sample: Optional[str] = None, nr_audio_channels: Optional[str] = None,
804
- resolution: Optional[str] = None, color_depth: Optional[str] = None,
805
- protection: Optional[str] = None):
806
- """Initializer."""
915
+ def __init__(
916
+ self,
917
+ uri: Optional[str],
918
+ protocol_info: Optional[str],
919
+ import_uri: Optional[str] = None,
920
+ size: Optional[str] = None,
921
+ duration: Optional[str] = None,
922
+ bitrate: Optional[str] = None,
923
+ sample_frequency: Optional[str] = None,
924
+ bits_per_sample: Optional[str] = None,
925
+ nr_audio_channels: Optional[str] = None,
926
+ resolution: Optional[str] = None,
927
+ color_depth: Optional[str] = None,
928
+ protection: Optional[str] = None,
929
+ xml_el: Optional[ET.Element] = None,
930
+ ) -> None:
931
+ """Initialize."""
807
932
  # pylint: disable=too-many-arguments
808
933
  self.uri = uri
809
934
  self.protocol_info = protocol_info
@@ -817,69 +942,96 @@ class Resource:
817
942
  self.resolution = resolution
818
943
  self.color_depth = color_depth
819
944
  self.protection = protection
945
+ self.xml_el = xml_el
820
946
 
821
947
  @classmethod
822
- def from_xml(cls: Type[TR], xml_node: ET.Element) -> TR:
948
+ def from_xml(cls: Type[TR], xml_el: ET.Element) -> TR:
823
949
  """Initialize from an XML node."""
824
- uri = xml_node.text
825
- protocol_info = xml_node.attrib["protocolInfo"]
826
- import_uri = xml_node.attrib.get('importUri')
827
- size = xml_node.attrib.get('size')
828
- duration = xml_node.attrib.get('duration')
829
- bitrate = xml_node.attrib.get('bitrate')
830
- sample_frequency = xml_node.attrib.get('sampleFrequency')
831
- bits_per_sample = xml_node.attrib.get('bitsPerSample')
832
- nr_audio_channels = xml_node.attrib.get('nrAudioChannels')
833
- resolution = xml_node.attrib.get('resolution')
834
- color_depth = xml_node.attrib.get('colorDepth')
835
- protection = xml_node.attrib.get('protection')
836
- return cls(uri, protocol_info,
837
- import_uri=import_uri, size=size, duration=duration,
838
- bitrate=bitrate, sample_frequency=sample_frequency,
839
- bits_per_sample=bits_per_sample,
840
- nr_audio_channels=nr_audio_channels,
841
- resolution=resolution, color_depth=color_depth,
842
- protection=protection)
950
+ uri = xml_el.text
951
+ protocol_info = xml_el.attrib.get("protocolInfo")
952
+ import_uri = xml_el.attrib.get("importUri")
953
+ size = xml_el.attrib.get("size")
954
+ duration = xml_el.attrib.get("duration")
955
+ bitrate = xml_el.attrib.get("bitrate")
956
+ sample_frequency = xml_el.attrib.get("sampleFrequency")
957
+ bits_per_sample = xml_el.attrib.get("bitsPerSample")
958
+ nr_audio_channels = xml_el.attrib.get("nrAudioChannels")
959
+ resolution = xml_el.attrib.get("resolution")
960
+ color_depth = xml_el.attrib.get("colorDepth")
961
+ protection = xml_el.attrib.get("protection")
962
+ return cls(
963
+ uri,
964
+ protocol_info=protocol_info,
965
+ import_uri=import_uri,
966
+ size=size,
967
+ duration=duration,
968
+ bitrate=bitrate,
969
+ sample_frequency=sample_frequency,
970
+ bits_per_sample=bits_per_sample,
971
+ nr_audio_channels=nr_audio_channels,
972
+ resolution=resolution,
973
+ color_depth=color_depth,
974
+ protection=protection,
975
+ xml_el=xml_el,
976
+ )
843
977
 
844
978
  def to_xml(self) -> ET.Element:
845
979
  """Convert self to XML."""
846
980
  attribs = {
847
- 'protocolInfo': self.protocol_info,
981
+ "protocolInfo": self.protocol_info or "",
848
982
  }
849
- res_el = ET.Element('res', attribs)
983
+ res_el = ET.Element("res", attribs)
850
984
  res_el.text = self.uri
851
985
  return res_el
852
986
 
987
+ def __repr__(self) -> str:
988
+ """Evaluatable string representation of this object."""
989
+ class_name = type(self).__name__
990
+ attr = ", ".join(
991
+ f"{key}={val!r}"
992
+ for key, val in self.__dict__.items()
993
+ if val is not None and key != "xml_el"
994
+ )
995
+ return f"{class_name}({attr})"
996
+
853
997
 
854
998
  class Descriptor:
855
999
  """DIDL Descriptor."""
856
1000
 
857
- def __init__(self, id: str, name_space: str, type: Optional[str] = None,
858
- text: Optional[str] = None):
859
- """Initializer."""
860
- # pylint: disable=invalid-name,redefined-builtin
1001
+ def __init__(
1002
+ self,
1003
+ id: str,
1004
+ name_space: str,
1005
+ type: Optional[str] = None,
1006
+ text: Optional[str] = None,
1007
+ xml_el: Optional[ET.Element] = None,
1008
+ ) -> None:
1009
+ """Initialize."""
1010
+ # pylint: disable=invalid-name,redefined-builtin,too-many-arguments
861
1011
  self.id = id
862
1012
  self.name_space = name_space
863
1013
  self.type = type
864
1014
  self.text = text
1015
+ self.xml_el = xml_el
865
1016
 
866
1017
  @classmethod
867
- def from_xml(cls: Type[TD], xml_node: ET.Element) -> TD:
1018
+ def from_xml(cls: Type[TD], xml_el: ET.Element) -> TD:
868
1019
  """Initialize from an XML node."""
869
- id_ = xml_node.attrib['id']
870
- name_space = xml_node.attrib['nameSpace']
871
- type_ = xml_node.attrib.get('type')
872
- return cls(id_, name_space, type_, xml_node.text)
1020
+ id_ = xml_el.attrib["id"]
1021
+ name_space = xml_el.attrib["nameSpace"]
1022
+ type_ = xml_el.attrib.get("type")
1023
+ text = xml_el.text
1024
+ return cls(id_, name_space, type=type_, text=text, xml_el=xml_el)
873
1025
 
874
1026
  def to_xml(self) -> ET.Element:
875
1027
  """Convert self to XML."""
876
1028
  attribs = {
877
- 'id': self.id,
878
- 'nameSpace': self.name_space,
1029
+ "id": self.id,
1030
+ "nameSpace": self.name_space,
879
1031
  }
880
1032
  if self.type is not None:
881
- attribs['type'] = self.type
882
- desc_el = ET.Element('desc', attribs)
1033
+ attribs["type"] = self.type
1034
+ desc_el = ET.Element("desc", attribs)
883
1035
  desc_el.text = self.text
884
1036
  return desc_el
885
1037
 
@@ -888,52 +1040,77 @@ class Descriptor:
888
1040
  if name not in self.__dict__:
889
1041
  raise AttributeError(name)
890
1042
  return self.__dict__[name]
1043
+
1044
+ def __repr__(self) -> str:
1045
+ """Evaluatable string representation of this object."""
1046
+ class_name = type(self).__name__
1047
+ attr = ", ".join(
1048
+ f"{key}={val!r}"
1049
+ for key, val in self.__dict__.items()
1050
+ if val is not None and key != "xml_el"
1051
+ )
1052
+ return f"{class_name}({attr})"
1053
+
1054
+
891
1055
  # endregion
892
1056
 
893
1057
 
894
1058
  def to_xml_string(*objects: DidlObject) -> bytes:
895
1059
  """Convert items to DIDL-Lite XML string."""
896
- root_el = ET.Element('DIDL-Lite', {})
897
- root_el.attrib['xmlns'] = NAMESPACES['didl_lite']
898
- root_el.attrib['xmlns:dc'] = NAMESPACES['dc']
899
- root_el.attrib['xmlns:upnp'] = NAMESPACES['upnp']
900
- root_el.attrib['xmlns:sec'] = NAMESPACES['sec']
1060
+ root_el = ET.Element("DIDL-Lite", {})
1061
+ root_el.attrib["xmlns"] = NAMESPACES["didl_lite"]
1062
+ root_el.attrib["xmlns:dc"] = NAMESPACES["dc"]
1063
+ root_el.attrib["xmlns:upnp"] = NAMESPACES["upnp"]
1064
+ root_el.attrib["xmlns:sec"] = NAMESPACES["sec"]
901
1065
 
902
1066
  for didl_object in objects:
903
1067
  didl_object_el = didl_object.to_xml()
904
1068
  root_el.append(didl_object_el)
905
1069
 
906
- return cast(bytes, ET.tostring(root_el))
1070
+ return ET.tostring(root_el)
907
1071
 
908
1072
 
909
- def from_xml_string(xml_string: str) -> List[Union[DidlObject, Descriptor]]:
1073
+ def from_xml_string(
1074
+ xml_string: str, strict: bool = True
1075
+ ) -> List[Union[DidlObject, Descriptor]]:
910
1076
  """Convert XML string to DIDL Objects."""
911
1077
  xml_el = defusedxml.ElementTree.fromstring(xml_string)
912
- return from_xml_el(xml_el)
1078
+ return from_xml_el(xml_el, strict)
913
1079
 
914
1080
 
915
- def from_xml_el(xml_el: ET.Element) -> List[Union[DidlObject, Descriptor]]:
1081
+ def from_xml_el(
1082
+ xml_el: ET.Element, strict: bool = True
1083
+ ) -> List[Union[DidlObject, Descriptor]]:
916
1084
  """Convert XML Element to DIDL Objects."""
917
1085
  didl_objects = [] # type: List[Union[DidlObject, Descriptor]]
918
1086
 
919
1087
  # items and containers, in order
920
1088
  for child_el in xml_el:
921
- if child_el.tag != ns_tag('didl_lite:item') and \
922
- child_el.tag != ns_tag('didl_lite:container'):
1089
+ if child_el.tag != expand_namespace_tag(
1090
+ "didl_lite:item"
1091
+ ) and child_el.tag != expand_namespace_tag("didl_lite:container"):
923
1092
  continue
924
1093
 
925
1094
  # construct item
926
- upnp_class = child_el.find('./upnp:class', NAMESPACES)
1095
+ upnp_class = child_el.find("./upnp:class", NAMESPACES)
927
1096
  if upnp_class is None or not upnp_class.text:
928
- continue
929
- didl_object_type = type_by_upnp_class(upnp_class.text)
1097
+ if strict:
1098
+ continue
1099
+ # WiiM Pro and possibly other Linkplay devices emit
1100
+ # upnp_class above the item element instead of inside it
1101
+ upnp_class = xml_el.find("./upnp:class", NAMESPACES)
1102
+ if upnp_class is None or not upnp_class.text:
1103
+ continue
1104
+ didl_object_type = type_by_upnp_class(upnp_class.text, strict)
930
1105
  if didl_object_type is None:
1106
+ if strict:
1107
+ raise DidlLiteException(f"upnp:class {upnp_class.text} is unknown")
931
1108
  continue
932
- didl_object = didl_object_type.from_xml(child_el)
1109
+ didl_object = didl_object_type.from_xml(child_el, strict)
933
1110
  didl_objects.append(didl_object)
934
1111
 
935
1112
  # descriptors
936
- for desc_el in xml_el.findall('./didl_lite:desc', NAMESPACES):
1113
+ for desc_el in xml_el.findall("./didl_lite:desc", NAMESPACES):
937
1114
  desc = Descriptor.from_xml(desc_el)
938
1115
  didl_objects.append(desc)
939
1116
 
@@ -941,13 +1118,14 @@ def from_xml_el(xml_el: ET.Element) -> List[Union[DidlObject, Descriptor]]:
941
1118
 
942
1119
 
943
1120
  # upnp_class to python type mapping
944
- def type_by_upnp_class(upnp_class: str) -> Optional[Type[DidlObject]]:
945
- """Get DidlObject-type by upnp_class."""
946
- queue = DidlObject.__subclasses__()
947
- while queue:
948
- type_ = queue.pop()
949
- queue.extend(type_.__subclasses__())
950
-
951
- if type_.upnp_class == upnp_class:
952
- return type_
953
- return None
1121
+ def type_by_upnp_class(
1122
+ upnp_class: str, strict: bool = True
1123
+ ) -> Optional[Type[DidlObject]]:
1124
+ """Get DidlObject-type by upnp_class.
1125
+
1126
+ When strict is False, the upnp_class lookup will be done ignoring string
1127
+ case.
1128
+ """
1129
+ if strict:
1130
+ return _upnp_class_map.get(upnp_class)
1131
+ return _upnp_class_map_lowercase.get(upnp_class.lower())