pyrekordbox 0.4.2__py3-none-any.whl → 0.4.3__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.
- pyrekordbox/__init__.py +1 -0
- pyrekordbox/_version.py +2 -2
- pyrekordbox/anlz/__init__.py +6 -6
- pyrekordbox/anlz/file.py +40 -32
- pyrekordbox/anlz/tags.py +108 -70
- pyrekordbox/config.py +30 -24
- pyrekordbox/db6/aux_files.py +40 -14
- pyrekordbox/db6/database.py +379 -217
- pyrekordbox/db6/registry.py +48 -34
- pyrekordbox/db6/smartlist.py +12 -12
- pyrekordbox/db6/tables.py +51 -52
- pyrekordbox/mysettings/__init__.py +3 -2
- pyrekordbox/mysettings/file.py +27 -24
- pyrekordbox/rbxml.py +321 -142
- pyrekordbox/utils.py +7 -6
- {pyrekordbox-0.4.2.dist-info → pyrekordbox-0.4.3.dist-info}/METADATA +1 -1
- pyrekordbox-0.4.3.dist-info/RECORD +25 -0
- {pyrekordbox-0.4.2.dist-info → pyrekordbox-0.4.3.dist-info}/WHEEL +1 -1
- pyrekordbox-0.4.2.dist-info/RECORD +0 -25
- {pyrekordbox-0.4.2.dist-info → pyrekordbox-0.4.3.dist-info}/licenses/LICENSE +0 -0
- {pyrekordbox-0.4.2.dist-info → pyrekordbox-0.4.3.dist-info}/top_level.txt +0 -0
pyrekordbox/rbxml.py
CHANGED
@@ -10,6 +10,8 @@ import urllib.parse
|
|
10
10
|
import xml.etree.cElementTree as xml
|
11
11
|
from abc import abstractmethod
|
12
12
|
from collections import abc
|
13
|
+
from pathlib import Path
|
14
|
+
from typing import Any, Callable, Dict, Iterator, List, Set, Union
|
13
15
|
|
14
16
|
import bidict
|
15
17
|
|
@@ -17,6 +19,7 @@ from .utils import pretty_xml
|
|
17
19
|
|
18
20
|
logger = logging.getLogger(__name__)
|
19
21
|
|
22
|
+
|
20
23
|
URL_PREFIX = "file://localhost/"
|
21
24
|
POSMARK_TYPE_MAPPING = bidict.bidict(
|
22
25
|
{
|
@@ -31,21 +34,35 @@ RATING_MAPPING = bidict.bidict({"0": 0, "51": 1, "102": 2, "153": 3, "204": 4, "
|
|
31
34
|
NODE_KEYTYPE_MAPPING = bidict.bidict({"0": "TrackID", "1": "Location"})
|
32
35
|
|
33
36
|
|
37
|
+
class XmlElementNotInitializedError(Exception):
|
38
|
+
"""Raised when an XML element is not initialized."""
|
39
|
+
|
40
|
+
def __init__(self, name: str) -> None:
|
41
|
+
super().__init__(f"XML element {name} is not initialized!")
|
42
|
+
|
43
|
+
|
44
|
+
class RootNodeNotInitializedError(Exception):
|
45
|
+
"""Raised when the root paylist node is not initialized."""
|
46
|
+
|
47
|
+
def __init__(self) -> None:
|
48
|
+
super().__init__("Playlist root node is not initialized!")
|
49
|
+
|
50
|
+
|
34
51
|
class XmlDuplicateError(Exception):
|
35
52
|
"""Raised when a track already exists in the XML database."""
|
36
53
|
|
37
|
-
def __init__(self, key_type, key):
|
54
|
+
def __init__(self, key_type: str, key: str) -> None:
|
38
55
|
super().__init__(f"XML database already contains a track with {key_type}={key}")
|
39
56
|
|
40
57
|
|
41
58
|
class XmlAttributeKeyError(Exception):
|
42
|
-
def __init__(self, cls, key, attributes):
|
59
|
+
def __init__(self, cls: Any, key: str, attributes: List[str]) -> None:
|
43
60
|
super().__init__(
|
44
61
|
f"{key} is not a valid key for {cls.__name__}! Valid attribs:\n{attributes}"
|
45
62
|
)
|
46
63
|
|
47
64
|
|
48
|
-
def encode_path(path):
|
65
|
+
def encode_path(path: Union[str, Path]) -> str:
|
49
66
|
r"""Encodes a file path as URI string.
|
50
67
|
|
51
68
|
Parameters
|
@@ -70,7 +87,7 @@ def encode_path(path):
|
|
70
87
|
return url
|
71
88
|
|
72
89
|
|
73
|
-
def decode_path(url):
|
90
|
+
def decode_path(url: str) -> str:
|
74
91
|
r"""Decodes an as URI string encoded file path.
|
75
92
|
|
76
93
|
Parameters
|
@@ -95,7 +112,7 @@ def decode_path(url):
|
|
95
112
|
return os.path.normpath(path)
|
96
113
|
|
97
114
|
|
98
|
-
class AbstractElement(abc.Mapping):
|
115
|
+
class AbstractElement(abc.Mapping): # type: ignore[type-arg]
|
99
116
|
"""Abstract base class for Rekordbox XML elements.
|
100
117
|
|
101
118
|
Implements attribute getters and setters for an XML element
|
@@ -104,17 +121,17 @@ class AbstractElement(abc.Mapping):
|
|
104
121
|
TAG: str
|
105
122
|
"""str: Name of the XML element"""
|
106
123
|
|
107
|
-
ATTRIBS:
|
124
|
+
ATTRIBS: List[str]
|
108
125
|
"""list[str]: List of all attribute keys of the XML element"""
|
109
126
|
|
110
|
-
GETTERS = dict()
|
127
|
+
GETTERS: Dict[str, Callable[[Any], Any]] = dict()
|
111
128
|
"""dict[str, Callable]: Dictionary of attribute getter conversion methods.
|
112
129
|
|
113
130
|
See Also
|
114
131
|
--------
|
115
132
|
AbstractElement.get
|
116
133
|
"""
|
117
|
-
SETTERS = dict()
|
134
|
+
SETTERS: Dict[str, Callable[[Any], Any]] = dict()
|
118
135
|
"""dict[str, Callable]: Dictionary of attribute setter conversion methods.
|
119
136
|
|
120
137
|
See Also
|
@@ -122,23 +139,23 @@ class AbstractElement(abc.Mapping):
|
|
122
139
|
AbstractElement.set
|
123
140
|
"""
|
124
141
|
|
125
|
-
def __init__(self, element=None, *args, **kwargs):
|
126
|
-
self._element = element
|
142
|
+
def __init__(self, element: xml.Element = None, *args: Any, **kwargs: Any):
|
143
|
+
self._element: Union[xml.Element, None] = element
|
127
144
|
if element is None:
|
128
145
|
self._init(*args, **kwargs)
|
129
146
|
else:
|
130
147
|
self._load_subelements()
|
131
148
|
|
132
149
|
@abstractmethod
|
133
|
-
def _init(self, *args, **kwargs):
|
150
|
+
def _init(self, *args: Any, **kwargs: Any) -> None:
|
134
151
|
"""Initializes a new XML element."""
|
135
152
|
pass
|
136
153
|
|
137
|
-
def _load_subelements(self):
|
154
|
+
def _load_subelements(self) -> None:
|
138
155
|
"""Loads the sub-elements of an existing XML element."""
|
139
156
|
pass
|
140
157
|
|
141
|
-
def get(self, key, default=None):
|
158
|
+
def get(self, key: str, default: Any = None) -> Any:
|
142
159
|
"""Returns the value of an attribute of the XML element.
|
143
160
|
|
144
161
|
The type of the attribute value is converted if a conversion method is specified
|
@@ -165,6 +182,9 @@ class AbstractElement(abc.Mapping):
|
|
165
182
|
"""
|
166
183
|
if key not in self.ATTRIBS:
|
167
184
|
raise XmlAttributeKeyError(self.__class__, key, self.ATTRIBS)
|
185
|
+
if self._element is None:
|
186
|
+
raise XmlElementNotInitializedError("_element")
|
187
|
+
|
168
188
|
value = self._element.attrib.get(key, default)
|
169
189
|
if value == default:
|
170
190
|
return default
|
@@ -175,7 +195,7 @@ class AbstractElement(abc.Mapping):
|
|
175
195
|
pass
|
176
196
|
return value
|
177
197
|
|
178
|
-
def set(self, key, value):
|
198
|
+
def set(self, key: str, value: Any) -> None:
|
179
199
|
"""Sets the value of an attribute of the XML element.
|
180
200
|
|
181
201
|
The type of the given value is converted before updating the attribute if a
|
@@ -197,6 +217,8 @@ class AbstractElement(abc.Mapping):
|
|
197
217
|
"""
|
198
218
|
if key not in self.ATTRIBS:
|
199
219
|
raise XmlAttributeKeyError(self.__class__, key, self.ATTRIBS)
|
220
|
+
if self._element is None:
|
221
|
+
raise XmlElementNotInitializedError("_element")
|
200
222
|
try:
|
201
223
|
# Apply callback
|
202
224
|
value = self.SETTERS[key](value)
|
@@ -205,15 +227,19 @@ class AbstractElement(abc.Mapping):
|
|
205
227
|
value = str(value)
|
206
228
|
self._element.attrib[key] = value
|
207
229
|
|
208
|
-
def __len__(self):
|
230
|
+
def __len__(self) -> int:
|
209
231
|
"""int: The number of attributes of the XML element."""
|
232
|
+
if self._element is None:
|
233
|
+
raise XmlElementNotInitializedError("_element")
|
210
234
|
return len(self._element.attrib)
|
211
235
|
|
212
|
-
def __iter__(self):
|
236
|
+
def __iter__(self) -> Iterator[str]:
|
213
237
|
"""Iterable: An iterator of the attribute keys of the XML element."""
|
238
|
+
if self._element is None:
|
239
|
+
raise XmlElementNotInitializedError("_element")
|
214
240
|
return iter(self._element.attrib.keys())
|
215
241
|
|
216
|
-
def __getitem__(self, key):
|
242
|
+
def __getitem__(self, key: str) -> Any:
|
217
243
|
"""Returns the raw value of an attribute of the XML element.
|
218
244
|
|
219
245
|
Parameters
|
@@ -228,7 +254,7 @@ class AbstractElement(abc.Mapping):
|
|
228
254
|
"""
|
229
255
|
return self.get(key)
|
230
256
|
|
231
|
-
def __setitem__(self, key, value):
|
257
|
+
def __setitem__(self, key: str, value: Any) -> None:
|
232
258
|
"""Sets the raw value of an attribute of the XML element.
|
233
259
|
|
234
260
|
Parameters
|
@@ -240,11 +266,11 @@ class AbstractElement(abc.Mapping):
|
|
240
266
|
"""
|
241
267
|
self.set(key, value)
|
242
268
|
|
243
|
-
def __getattr__(self, key):
|
269
|
+
def __getattr__(self, key: str) -> Any:
|
244
270
|
"""Returns the raw value of an attribute of the XML element (same as `get`)."""
|
245
271
|
return self.get(key)
|
246
272
|
|
247
|
-
def __repr__(self):
|
273
|
+
def __repr__(self) -> str:
|
248
274
|
return f"<{self.__class__.__name__}()>"
|
249
275
|
|
250
276
|
|
@@ -271,10 +297,20 @@ class Tempo(AbstractElement):
|
|
271
297
|
ATTRIBS = ["Inizio", "Bpm", "Metro", "Battito"]
|
272
298
|
GETTERS = {"Inizio": float, "Bpm": float, "Battito": int}
|
273
299
|
|
274
|
-
def __init__(
|
300
|
+
def __init__(
|
301
|
+
self,
|
302
|
+
parent: xml.Element = None,
|
303
|
+
Inizio: float = 0.0,
|
304
|
+
Bpm: float = 0.0,
|
305
|
+
Metro: str = "4/4",
|
306
|
+
Battito: int = 1,
|
307
|
+
element: xml.Element = None,
|
308
|
+
):
|
275
309
|
super().__init__(element, parent, Inizio, Bpm, Metro, Battito)
|
276
310
|
|
277
|
-
def _init(
|
311
|
+
def _init(
|
312
|
+
self, parent: xml.Element, inizio: float, bpm: float, metro: str, battito: int
|
313
|
+
) -> None:
|
278
314
|
attrib = {
|
279
315
|
"Inizio": str(inizio),
|
280
316
|
"Bpm": str(bpm),
|
@@ -283,7 +319,7 @@ class Tempo(AbstractElement):
|
|
283
319
|
}
|
284
320
|
self._element = xml.SubElement(parent, self.TAG, attrib=attrib)
|
285
321
|
|
286
|
-
def __repr__(self):
|
322
|
+
def __repr__(self) -> str:
|
287
323
|
args = ", ".join(
|
288
324
|
[
|
289
325
|
f"Inizio={self.Inizio}",
|
@@ -327,20 +363,24 @@ class PositionMark(AbstractElement):
|
|
327
363
|
|
328
364
|
def __init__(
|
329
365
|
self,
|
330
|
-
parent=None,
|
331
|
-
Name="",
|
332
|
-
Type="cue",
|
333
|
-
Start=0.0,
|
334
|
-
End=None,
|
335
|
-
Num
|
336
|
-
element=None,
|
366
|
+
parent: xml.Element = None,
|
367
|
+
Name: str = "",
|
368
|
+
Type: str = "cue",
|
369
|
+
Start: float = 0.0,
|
370
|
+
End: float = None,
|
371
|
+
Num: int = -1,
|
372
|
+
element: xml.Element = None,
|
337
373
|
):
|
338
374
|
super().__init__(element, parent, Name, Type, Start, End, Num)
|
339
375
|
|
340
|
-
def _init(
|
376
|
+
def _init(
|
377
|
+
self, parent: xml.Element, name: str, type_: str, start: float, end: float, num: int
|
378
|
+
) -> None:
|
379
|
+
if type_ not in POSMARK_TYPE_MAPPING.inv:
|
380
|
+
raise ValueError(f"Type '{type_}' is not supported!")
|
341
381
|
attrib = {
|
342
382
|
"Name": name,
|
343
|
-
"Type": POSMARK_TYPE_MAPPING.inv.get(type_),
|
383
|
+
"Type": POSMARK_TYPE_MAPPING.inv.get(type_, "cue"),
|
344
384
|
"Start": str(start),
|
345
385
|
"Num": str(num),
|
346
386
|
}
|
@@ -348,7 +388,7 @@ class PositionMark(AbstractElement):
|
|
348
388
|
attrib["End"] = str(end)
|
349
389
|
self._element = xml.SubElement(parent, self.TAG, attrib=attrib)
|
350
390
|
|
351
|
-
def __repr__(self):
|
391
|
+
def __repr__(self) -> str:
|
352
392
|
args = ", ".join(
|
353
393
|
[
|
354
394
|
f"Name={self.Name}",
|
@@ -484,20 +524,31 @@ class Track(AbstractElement):
|
|
484
524
|
|
485
525
|
SETTERS = {"Rating": RATING_MAPPING.inv.get, "Location": encode_path} # noqa
|
486
526
|
|
487
|
-
def __init__(
|
488
|
-
self
|
489
|
-
|
527
|
+
def __init__(
|
528
|
+
self,
|
529
|
+
parent: xml.Element = None,
|
530
|
+
Location: Union[str, Path] = "",
|
531
|
+
element: xml.Element = None,
|
532
|
+
**kwargs: Any,
|
533
|
+
):
|
534
|
+
self.tempos: List[Tempo] = list()
|
535
|
+
self.marks: List[PositionMark] = list()
|
490
536
|
super().__init__(element, parent, Location, **kwargs)
|
491
537
|
|
492
|
-
def _init(self, parent, Location, **kwargs):
|
538
|
+
def _init(self, parent: xml.Element, Location: Union[str, Path] = "", **kwargs: Any) -> None:
|
493
539
|
attrib = {"Location": encode_path(Location)}
|
494
540
|
for key, val in kwargs.items():
|
495
541
|
if key not in self.ATTRIBS:
|
496
542
|
raise XmlAttributeKeyError(self.__class__, key, self.ATTRIBS)
|
497
543
|
attrib[key] = str(val)
|
498
|
-
|
544
|
+
element = xml.SubElement(parent, self.TAG, attrib=attrib)
|
545
|
+
if element is None:
|
546
|
+
raise RuntimeError("XML element is not initialized!")
|
547
|
+
self._element = element
|
499
548
|
|
500
|
-
def _load_subelements(self):
|
549
|
+
def _load_subelements(self) -> None:
|
550
|
+
if self._element is None:
|
551
|
+
raise XmlElementNotInitializedError("_element")
|
501
552
|
tempo_elements = self._element.findall(f"{Tempo.TAG}")
|
502
553
|
if tempo_elements is not None:
|
503
554
|
self.tempos = [Tempo(element=el) for el in tempo_elements]
|
@@ -505,7 +556,7 @@ class Track(AbstractElement):
|
|
505
556
|
if mark_elements is not None:
|
506
557
|
self.marks = [PositionMark(element=el) for el in mark_elements]
|
507
558
|
|
508
|
-
def add_tempo(self, Inizio, Bpm, Metro, Battito):
|
559
|
+
def add_tempo(self, Inizio: float, Bpm: float, Metro: str, Battito: int) -> Tempo:
|
509
560
|
"""Adds a new ``Tempo`` XML element to the track element.
|
510
561
|
|
511
562
|
Parameters
|
@@ -533,7 +584,14 @@ class Track(AbstractElement):
|
|
533
584
|
self.tempos.append(tempo)
|
534
585
|
return tempo
|
535
586
|
|
536
|
-
def add_mark(
|
587
|
+
def add_mark(
|
588
|
+
self,
|
589
|
+
Name: str = "",
|
590
|
+
Type: str = "cue",
|
591
|
+
Start: float = 0.0,
|
592
|
+
End: float = None,
|
593
|
+
Num: int = -1,
|
594
|
+
) -> PositionMark:
|
537
595
|
"""Adds a new ``PositionMark`` XML element to the track element.
|
538
596
|
|
539
597
|
Parameters
|
@@ -564,7 +622,7 @@ class Track(AbstractElement):
|
|
564
622
|
self.marks.append(mark)
|
565
623
|
return mark
|
566
624
|
|
567
|
-
def __repr__(self):
|
625
|
+
def __repr__(self) -> str:
|
568
626
|
return f"<{self.__class__.__name__}(Location={self.Location})>"
|
569
627
|
|
570
628
|
|
@@ -587,28 +645,30 @@ class Node:
|
|
587
645
|
FOLDER = 0
|
588
646
|
PLAYLIST = 1
|
589
647
|
|
590
|
-
def __init__(self, parent=None, element=None, **attribs):
|
648
|
+
def __init__(self, parent: xml.Element = None, element: xml.Element = None, **attribs: Any):
|
649
|
+
if element is None:
|
650
|
+
if parent is None:
|
651
|
+
raise ValueError("Either parent or element must be given!")
|
652
|
+
element = xml.SubElement(parent, self.TAG, attrib=attribs)
|
591
653
|
self._parent = parent
|
592
654
|
self._element = element
|
593
|
-
if element is None:
|
594
|
-
self._element = xml.SubElement(parent, self.TAG, attrib=attribs)
|
595
655
|
|
596
656
|
@classmethod
|
597
|
-
def folder(cls, parent, name):
|
657
|
+
def folder(cls, parent: xml.Element, name: str) -> "Node":
|
598
658
|
"""Initializes a playlist folder node XML element.
|
599
659
|
|
600
660
|
Parameters
|
601
661
|
----------
|
602
|
-
parent :
|
662
|
+
parent : Node
|
603
663
|
The parent node XML element of the new playlist folder node.
|
604
664
|
name : str
|
605
665
|
The name of the playlist folder node.
|
606
666
|
"""
|
607
667
|
attrib = {"Name": name, "Type": str(cls.FOLDER), "Count": "0"}
|
608
|
-
return cls(parent, **attrib)
|
668
|
+
return cls(parent, None, **attrib)
|
609
669
|
|
610
670
|
@classmethod
|
611
|
-
def playlist(cls, parent, name, keytype="TrackID"):
|
671
|
+
def playlist(cls, parent: xml.Element, name: str, keytype: str = "TrackID") -> "Node":
|
612
672
|
"""Initializes a playlist node XML element.
|
613
673
|
|
614
674
|
Parameters
|
@@ -621,61 +681,70 @@ class Node:
|
|
621
681
|
The key type used by the playlist node. Can be 'TrackID' or 'Location'
|
622
682
|
(file path of the track).
|
623
683
|
"""
|
684
|
+
if keytype not in NODE_KEYTYPE_MAPPING.inv:
|
685
|
+
raise ValueError(f"Key type '{keytype}' is not supported!")
|
624
686
|
attrib = {
|
625
687
|
"Name": name,
|
626
688
|
"Type": str(cls.PLAYLIST),
|
627
|
-
"KeyType": NODE_KEYTYPE_MAPPING.inv
|
689
|
+
"KeyType": NODE_KEYTYPE_MAPPING.inv.get(keytype, "TrackID"),
|
628
690
|
"Entries": "0",
|
629
691
|
}
|
630
|
-
return cls(parent, **attrib)
|
692
|
+
return cls(parent, None, **attrib)
|
631
693
|
|
632
694
|
@property
|
633
|
-
def parent(self):
|
634
|
-
"""
|
695
|
+
def parent(self) -> Union[xml.Element, None]:
|
696
|
+
"""xml.Element: The parent of the node."""
|
635
697
|
return self._parent
|
636
698
|
|
637
699
|
@property
|
638
|
-
def name(self):
|
639
|
-
"""str: The name of node."""
|
640
|
-
|
700
|
+
def name(self) -> str:
|
701
|
+
"""str: The name of the node."""
|
702
|
+
value = self._element.attrib.get("Name")
|
703
|
+
if value is None:
|
704
|
+
raise ValueError("Name element has no value")
|
705
|
+
return value
|
641
706
|
|
642
707
|
@property
|
643
|
-
def type(self):
|
708
|
+
def type(self) -> int:
|
644
709
|
"""int: The type of the node (0=folder or 1=playlist)."""
|
645
|
-
|
710
|
+
type_ = self._element.attrib.get("Type")
|
711
|
+
if type_ is None:
|
712
|
+
raise ValueError("Type element has no value")
|
713
|
+
return int(type_)
|
646
714
|
|
647
715
|
@property
|
648
|
-
def count(self):
|
716
|
+
def count(self) -> int:
|
649
717
|
"""int: The number of attributes of the XML element."""
|
650
718
|
return int(self._element.attrib.get("Count", 0))
|
651
719
|
|
652
720
|
@property
|
653
|
-
def entries(self):
|
721
|
+
def entries(self) -> int:
|
654
722
|
"""int: The number of entries of the node."""
|
655
723
|
return int(self._element.attrib.get("Entries", 0))
|
656
724
|
|
657
725
|
@property
|
658
|
-
def key_type(self):
|
726
|
+
def key_type(self) -> str:
|
659
727
|
"""str: The type of key used by the playlist node."""
|
660
|
-
|
728
|
+
keytype: str = NODE_KEYTYPE_MAPPING[self._element.attrib.get("KeyType", "0")]
|
729
|
+
return keytype
|
661
730
|
|
662
731
|
@property
|
663
|
-
def is_folder(self):
|
732
|
+
def is_folder(self) -> bool:
|
664
733
|
"""bool: True if the node is a playlist folder, false if otherwise."""
|
665
734
|
return self.type == self.FOLDER
|
666
735
|
|
667
736
|
@property
|
668
|
-
def is_playlist(self):
|
737
|
+
def is_playlist(self) -> bool:
|
669
738
|
"""bool: True if the node is a playlist, false if otherwise."""
|
670
739
|
return self.type == self.PLAYLIST
|
671
740
|
|
672
|
-
def _update_count(self):
|
741
|
+
def _update_count(self) -> None:
|
673
742
|
self._element.attrib["Count"] = str(len(self._element))
|
674
743
|
|
675
|
-
def _update_entries(self):
|
744
|
+
def _update_entries(self) -> None:
|
676
745
|
self._element.attrib["Entries"] = str(len(self._element))
|
677
746
|
|
678
|
-
def get_node(self, i):
|
747
|
+
def get_node(self, i: int) -> "Node":
|
679
748
|
"""Returns the i-th sub-Node of the current node.
|
680
749
|
|
681
750
|
Parameters
|
@@ -687,9 +756,9 @@ class Node:
|
|
687
756
|
-------
|
688
757
|
subnode : Node
|
689
758
|
"""
|
690
|
-
return Node(self, element=self._element.
|
759
|
+
return Node(self._element, element=self._element.find(f"{self.TAG}[{i + 1}]"))
|
691
760
|
|
692
|
-
def get_playlist(self, name):
|
761
|
+
def get_playlist(self, name: str) -> "Node":
|
693
762
|
"""Returns the sub-Node with the given name.
|
694
763
|
|
695
764
|
Parameters
|
@@ -701,9 +770,9 @@ class Node:
|
|
701
770
|
-------
|
702
771
|
subnode : Node
|
703
772
|
"""
|
704
|
-
return Node(self, element=self._element.find(f'.//{self.TAG}[@Name="{name}"]'))
|
773
|
+
return Node(self._element, element=self._element.find(f'.//{self.TAG}[@Name="{name}"]'))
|
705
774
|
|
706
|
-
def get_playlists(self):
|
775
|
+
def get_playlists(self) -> List["Node"]:
|
707
776
|
"""Returns all sub-nodes that are playlists.
|
708
777
|
|
709
778
|
Returns
|
@@ -711,9 +780,9 @@ class Node:
|
|
711
780
|
playlists : list[Node]
|
712
781
|
The playlist nodes in the current node.
|
713
782
|
"""
|
714
|
-
return [Node(self, element=el) for el in self._element]
|
783
|
+
return [Node(self._element, element=el) for el in self._element]
|
715
784
|
|
716
|
-
def add_playlist_folder(self, name):
|
785
|
+
def add_playlist_folder(self, name: str) -> "Node":
|
717
786
|
"""Add a new playlist folder as child to this node.
|
718
787
|
|
719
788
|
Parameters
|
@@ -738,7 +807,7 @@ class Node:
|
|
738
807
|
self._update_count()
|
739
808
|
return node
|
740
809
|
|
741
|
-
def add_playlist(self, name, keytype="TrackID"):
|
810
|
+
def add_playlist(self, name: str, keytype: str = "TrackID") -> "Node":
|
742
811
|
"""Add a new playlist as child to this node.
|
743
812
|
|
744
813
|
Parameters
|
@@ -766,7 +835,7 @@ class Node:
|
|
766
835
|
self._update_count()
|
767
836
|
return node
|
768
837
|
|
769
|
-
def remove_playlist(self, name):
|
838
|
+
def remove_playlist(self, name: str) -> None:
|
770
839
|
"""Removes a playlist from the playlist folder node.
|
771
840
|
|
772
841
|
Parameters
|
@@ -779,7 +848,7 @@ class Node:
|
|
779
848
|
self._update_count()
|
780
849
|
self._update_entries()
|
781
850
|
|
782
|
-
def add_track(self, key):
|
851
|
+
def add_track(self, key: Union[int, str]) -> xml.Element:
|
783
852
|
"""Adds a new track to the playlist node.
|
784
853
|
|
785
854
|
Parameters
|
@@ -793,10 +862,12 @@ class Node:
|
|
793
862
|
The newly created playlist track element.
|
794
863
|
"""
|
795
864
|
el = xml.SubElement(self._element, Track.TAG, attrib={"Key": str(key)})
|
865
|
+
if el is None:
|
866
|
+
raise RuntimeError("XML element is not initialized!")
|
796
867
|
self._update_entries()
|
797
868
|
return el
|
798
869
|
|
799
|
-
def remove_track(self, key):
|
870
|
+
def remove_track(self, key: Union[int, str]) -> xml.Element:
|
800
871
|
"""Removes a track from the playlist node.
|
801
872
|
|
802
873
|
Parameters
|
@@ -806,11 +877,13 @@ class Node:
|
|
806
877
|
playlist node.
|
807
878
|
"""
|
808
879
|
el = self._element.find(f'{Track.TAG}[@Key="{key}"]')
|
880
|
+
if el is None:
|
881
|
+
raise ValueError(f"Track key {key} not found.")
|
809
882
|
self._element.remove(el)
|
810
883
|
self._update_entries()
|
811
884
|
return el
|
812
885
|
|
813
|
-
def get_tracks(self):
|
886
|
+
def get_tracks(self) -> List[Union[int, str]]:
|
814
887
|
"""Returns the keys of all tracks contained in the playlist node.
|
815
888
|
|
816
889
|
Returns
|
@@ -824,21 +897,23 @@ class Node:
|
|
824
897
|
elements = self._element.findall(f".//{Track.TAG}")
|
825
898
|
items = list()
|
826
899
|
for el in elements:
|
827
|
-
val = el.attrib["Key"]
|
900
|
+
val: Union[int, str] = el.attrib["Key"]
|
828
901
|
if self.key_type == "TrackID":
|
829
902
|
val = int(val)
|
830
903
|
items.append(val)
|
831
904
|
return items
|
832
905
|
|
833
|
-
def get_track(self, key):
|
906
|
+
def get_track(self, key: str) -> Union[int, str]:
|
834
907
|
"""Returns the formatted key of the track."""
|
835
908
|
el = self._element.find(f'{Track.TAG}[@Key="{key}"]')
|
836
|
-
|
909
|
+
if el is None:
|
910
|
+
raise ValueError(f"Track key {key} not found.")
|
911
|
+
val: Union[int, str] = el.attrib["Key"]
|
837
912
|
if self.key_type == "TrackID":
|
838
913
|
val = int(val)
|
839
914
|
return val
|
840
915
|
|
841
|
-
def treestr(self, indent=4, lvl=0):
|
916
|
+
def treestr(self, indent: int = 4, lvl: int = 0) -> str:
|
842
917
|
"""returns a formatted string of the node tree strucutre.
|
843
918
|
|
844
919
|
Parameters
|
@@ -863,10 +938,12 @@ class Node:
|
|
863
938
|
string += node.treestr(indent, lvl + 1)
|
864
939
|
return string
|
865
940
|
|
866
|
-
def __eq__(self, other):
|
941
|
+
def __eq__(self, other: Any) -> bool:
|
942
|
+
if not isinstance(other, Node):
|
943
|
+
raise NotImplementedError()
|
867
944
|
return self.parent == other.parent and self.name == other.name
|
868
945
|
|
869
|
-
def __repr__(self):
|
946
|
+
def __repr__(self) -> str:
|
870
947
|
return f"<{self.__class__.__name__}({self.name})>"
|
871
948
|
|
872
949
|
|
@@ -908,17 +985,23 @@ class RekordboxXml:
|
|
908
985
|
PLST_TAG = "PLAYLISTS"
|
909
986
|
COLL_TAG = "COLLECTION"
|
910
987
|
|
911
|
-
def __init__(
|
912
|
-
self
|
913
|
-
|
914
|
-
|
915
|
-
|
916
|
-
|
988
|
+
def __init__(
|
989
|
+
self,
|
990
|
+
path: Union[str, Path] = None,
|
991
|
+
name: str = None,
|
992
|
+
version: str = None,
|
993
|
+
company: str = None,
|
994
|
+
):
|
995
|
+
self._root: Union[xml.Element, None] = None
|
996
|
+
self._product: Union[xml.Element, None] = None
|
997
|
+
self._collection: Union[xml.Element, None] = None
|
998
|
+
self._playlists: Union[xml.Element, None] = None
|
999
|
+
self._root_node: Union[Node, None] = None
|
917
1000
|
|
918
1001
|
self._last_id = 0
|
919
1002
|
# Used for fast duplicate check
|
920
|
-
self._locations = set()
|
921
|
-
self._ids = set()
|
1003
|
+
self._locations: Set[str] = set()
|
1004
|
+
self._ids: Set[int] = set()
|
922
1005
|
|
923
1006
|
if path is not None:
|
924
1007
|
self._parse(path)
|
@@ -926,36 +1009,48 @@ class RekordboxXml:
|
|
926
1009
|
self._init(name, version, company)
|
927
1010
|
|
928
1011
|
@property
|
929
|
-
def frmt_version(self):
|
1012
|
+
def frmt_version(self) -> str:
|
930
1013
|
"""str : The version of the Rekordbox XML format."""
|
931
|
-
|
1014
|
+
if self._root is None:
|
1015
|
+
raise XmlElementNotInitializedError("_root")
|
1016
|
+
return self._root.attrib.get("Version", "")
|
932
1017
|
|
933
1018
|
@property
|
934
|
-
def product_name(self):
|
1019
|
+
def product_name(self) -> str:
|
935
1020
|
"""str : The product name that will be displayed in the software."""
|
936
|
-
|
1021
|
+
if self._product is None:
|
1022
|
+
raise XmlElementNotInitializedError("_product")
|
1023
|
+
return self._product.attrib.get("Name", "")
|
937
1024
|
|
938
1025
|
@property
|
939
|
-
def product_version(self):
|
1026
|
+
def product_version(self) -> str:
|
940
1027
|
"""str : The product version."""
|
941
|
-
|
1028
|
+
if self._product is None:
|
1029
|
+
raise XmlElementNotInitializedError("_product")
|
1030
|
+
return self._product.attrib.get("Version", "")
|
942
1031
|
|
943
1032
|
@property
|
944
|
-
def product_company(self):
|
1033
|
+
def product_company(self) -> str:
|
945
1034
|
"""str : The company name."""
|
946
|
-
|
1035
|
+
if self._product is None:
|
1036
|
+
raise XmlElementNotInitializedError("_product")
|
1037
|
+
return self._product.attrib.get("Company", "")
|
947
1038
|
|
948
1039
|
@property
|
949
|
-
def num_tracks(self):
|
950
|
-
"""
|
951
|
-
|
1040
|
+
def num_tracks(self) -> int:
|
1041
|
+
"""int : The number of tracks in the collection."""
|
1042
|
+
if self._collection is None:
|
1043
|
+
raise XmlElementNotInitializedError("_collection")
|
1044
|
+
return int(self._collection.attrib.get("Entries", "0"))
|
952
1045
|
|
953
1046
|
@property
|
954
|
-
def root_playlist_folder(self):
|
1047
|
+
def root_playlist_folder(self) -> Node:
|
955
1048
|
"""Node: The node of the root playlist folder containing all other nodes."""
|
1049
|
+
if self._root_node is None:
|
1050
|
+
raise RootNodeNotInitializedError()
|
956
1051
|
return self._root_node
|
957
1052
|
|
958
|
-
def _parse(self, path):
|
1053
|
+
def _parse(self, path: Union[str, Path]) -> None:
|
959
1054
|
"""Parse an existing XML file.
|
960
1055
|
|
961
1056
|
Parameters
|
@@ -965,13 +1060,26 @@ class RekordboxXml:
|
|
965
1060
|
"""
|
966
1061
|
tree = xml.parse(str(path))
|
967
1062
|
self._root = tree.getroot()
|
968
|
-
|
969
|
-
|
970
|
-
|
1063
|
+
|
1064
|
+
product = self._root.find(self.PRDT_TAG)
|
1065
|
+
collection = self._root.find(self.COLL_TAG)
|
1066
|
+
playlists = self._root.find(self.PLST_TAG)
|
1067
|
+
if product is None:
|
1068
|
+
raise RuntimeError(f"No product found in {path}")
|
1069
|
+
if collection is None:
|
1070
|
+
raise RuntimeError(f"No collection found in {path}")
|
1071
|
+
if playlists is None:
|
1072
|
+
raise RuntimeError(f"No playlists found in {path}")
|
1073
|
+
|
1074
|
+
self._product = product
|
1075
|
+
self._collection = collection
|
1076
|
+
self._playlists = playlists
|
971
1077
|
self._root_node = Node(element=self._playlists.find(Node.TAG))
|
972
1078
|
self._update_cache()
|
973
1079
|
|
974
|
-
def _init(
|
1080
|
+
def _init(
|
1081
|
+
self, name: str = None, version: str = None, company: str = None, frmt_version: str = None
|
1082
|
+
) -> None:
|
975
1083
|
"""Initialize a new XML file."""
|
976
1084
|
frmt_version = frmt_version or "1.0.0"
|
977
1085
|
name = name or "pyrekordbox"
|
@@ -994,7 +1102,7 @@ class RekordboxXml:
|
|
994
1102
|
if track_ids:
|
995
1103
|
self._last_id = max(track_ids)
|
996
1104
|
|
997
|
-
def get_tracks(self):
|
1105
|
+
def get_tracks(self) -> List[Track]:
|
998
1106
|
"""Returns the tracks in the collection of the XML file.
|
999
1107
|
|
1000
1108
|
Returns
|
@@ -1002,10 +1110,14 @@ class RekordboxXml:
|
|
1002
1110
|
tracks : list of Track
|
1003
1111
|
A list of the track objects in the collection.
|
1004
1112
|
"""
|
1113
|
+
if self._collection is None:
|
1114
|
+
raise XmlElementNotInitializedError("_collection")
|
1005
1115
|
elements = self._collection.findall(f".//{Track.TAG}")
|
1006
1116
|
return [Track(element=el) for el in elements]
|
1007
1117
|
|
1008
|
-
def get_track(
|
1118
|
+
def get_track(
|
1119
|
+
self, index: int = None, TrackID: Union[int, str] = None, Location: str = None
|
1120
|
+
) -> Track:
|
1009
1121
|
"""Get a track in the collection of the XML file.
|
1010
1122
|
|
1011
1123
|
Parameters
|
@@ -1013,7 +1125,7 @@ class RekordboxXml:
|
|
1013
1125
|
index : int, optional
|
1014
1126
|
If `index` is given, the track with this index in the collection is
|
1015
1127
|
returned.
|
1016
|
-
TrackID : int, optional
|
1128
|
+
TrackID : int or str, optional
|
1017
1129
|
If `TrackID` is given, the track with this ID in the collection is
|
1018
1130
|
returned.
|
1019
1131
|
Location : str, optional
|
@@ -1042,19 +1154,20 @@ class RekordboxXml:
|
|
1042
1154
|
>>> track = file.get_track(TrackID=1)
|
1043
1155
|
|
1044
1156
|
"""
|
1045
|
-
if
|
1046
|
-
raise
|
1047
|
-
|
1157
|
+
if self._collection is None:
|
1158
|
+
raise XmlElementNotInitializedError("_collection")
|
1048
1159
|
if TrackID is not None:
|
1049
1160
|
el = self._collection.find(f'.//{Track.TAG}[@TrackID="{TrackID}"]')
|
1050
1161
|
elif Location is not None:
|
1051
1162
|
encoded = encode_path(Location)
|
1052
1163
|
el = self._collection.find(f'.//{Track.TAG}[@Location="{encoded}"]')
|
1053
|
-
|
1164
|
+
elif index is not None:
|
1054
1165
|
el = self._collection.find(f".//{Track.TAG}[{index + 1}]")
|
1166
|
+
else:
|
1167
|
+
raise ValueError("Either index, TrackID or Location has to be specified!")
|
1055
1168
|
return Track(element=el)
|
1056
1169
|
|
1057
|
-
def get_track_ids(self):
|
1170
|
+
def get_track_ids(self) -> List[int]:
|
1058
1171
|
"""Returns the `TrackID` of all tracks in the collection of the XML file.
|
1059
1172
|
|
1060
1173
|
Returns
|
@@ -1062,10 +1175,12 @@ class RekordboxXml:
|
|
1062
1175
|
ids : list of int
|
1063
1176
|
The ID's of all tracks.
|
1064
1177
|
"""
|
1178
|
+
if self._collection is None:
|
1179
|
+
raise XmlElementNotInitializedError("_collection")
|
1065
1180
|
elements = self._collection.findall(f".//{Track.TAG}")
|
1066
1181
|
return [int(el.attrib["TrackID"]) for el in elements]
|
1067
1182
|
|
1068
|
-
def get_playlist(self, *names):
|
1183
|
+
def get_playlist(self, *names: str) -> Node:
|
1069
1184
|
"""Returns a playlist or playlist folder with the given path.
|
1070
1185
|
|
1071
1186
|
Parameters
|
@@ -1085,6 +1200,8 @@ class RekordboxXml:
|
|
1085
1200
|
>>> playlist = file.get_playlist("Folder", "Sub Playlist")
|
1086
1201
|
|
1087
1202
|
"""
|
1203
|
+
if self._root_node is None:
|
1204
|
+
raise RootNodeNotInitializedError()
|
1088
1205
|
node = self._root_node
|
1089
1206
|
if not names:
|
1090
1207
|
return node
|
@@ -1097,34 +1214,38 @@ class RekordboxXml:
|
|
1097
1214
|
# num_tracks = len(self._collection.findall(f".//{Track.TAG}"))
|
1098
1215
|
# self._collection.attrib["Entries"] = str(num_tracks)
|
1099
1216
|
|
1100
|
-
def _increment_track_count(self):
|
1217
|
+
def _increment_track_count(self) -> None:
|
1101
1218
|
"""Increment the track count element."""
|
1219
|
+
if self._collection is None:
|
1220
|
+
raise XmlElementNotInitializedError("_collection")
|
1102
1221
|
old = int(self._collection.attrib["Entries"])
|
1103
1222
|
self._collection.attrib["Entries"] = str(old + 1)
|
1104
1223
|
|
1105
|
-
def _decrement_track_count(self):
|
1224
|
+
def _decrement_track_count(self) -> None:
|
1106
1225
|
"""Decrement the track count element."""
|
1226
|
+
if self._collection is None:
|
1227
|
+
raise XmlElementNotInitializedError("_collection")
|
1107
1228
|
old = int(self._collection.attrib["Entries"])
|
1108
1229
|
self._collection.attrib["Entries"] = str(old - 1)
|
1109
1230
|
|
1110
|
-
def _add_cache(self, track):
|
1231
|
+
def _add_cache(self, track: Track) -> None:
|
1111
1232
|
"""Add the TrackID and Location to the cache."""
|
1112
1233
|
self._locations.add(track.Location)
|
1113
1234
|
self._ids.add(track.TrackID)
|
1114
1235
|
|
1115
|
-
def _remove_cache(self, track):
|
1236
|
+
def _remove_cache(self, track: Track) -> None:
|
1116
1237
|
"""Remove the TrackID and Location from the cache."""
|
1117
1238
|
self._locations.remove(track.Location)
|
1118
1239
|
self._ids.remove(track.TrackID)
|
1119
1240
|
|
1120
|
-
def _update_cache(self):
|
1241
|
+
def _update_cache(self) -> None:
|
1121
1242
|
"""Update the cache with the current tracks in the collection."""
|
1122
1243
|
self._locations.clear()
|
1123
1244
|
self._ids.clear()
|
1124
1245
|
for track in self.get_tracks():
|
1125
1246
|
self._add_cache(track)
|
1126
1247
|
|
1127
|
-
def add_track(self, location, **kwargs):
|
1248
|
+
def add_track(self, location: Union[str, Path], **kwargs: Any) -> Track:
|
1128
1249
|
"""Add a new track element to the Rekordbox XML collection.
|
1129
1250
|
|
1130
1251
|
Parameters
|
@@ -1157,7 +1278,7 @@ class RekordboxXml:
|
|
1157
1278
|
# Check that Location and TrackID are unique
|
1158
1279
|
track_id = kwargs["TrackID"]
|
1159
1280
|
if os.path.normpath(location) in self._locations:
|
1160
|
-
raise XmlDuplicateError("Location", location)
|
1281
|
+
raise XmlDuplicateError("Location", str(location))
|
1161
1282
|
if track_id in self._ids:
|
1162
1283
|
raise XmlDuplicateError("TrackID", track_id)
|
1163
1284
|
|
@@ -1168,7 +1289,7 @@ class RekordboxXml:
|
|
1168
1289
|
self._add_cache(track)
|
1169
1290
|
return track
|
1170
1291
|
|
1171
|
-
def remove_track(self, track):
|
1292
|
+
def remove_track(self, track: Track) -> None:
|
1172
1293
|
"""Remove a track element from the Rekordbox XML collection.
|
1173
1294
|
|
1174
1295
|
Parameters
|
@@ -1183,11 +1304,15 @@ class RekordboxXml:
|
|
1183
1304
|
>>> file.remove_track(t)
|
1184
1305
|
|
1185
1306
|
"""
|
1307
|
+
if self._collection is None:
|
1308
|
+
raise XmlElementNotInitializedError("_collection")
|
1309
|
+
if track._element is None: # noqa
|
1310
|
+
raise XmlElementNotInitializedError("track._element")
|
1186
1311
|
self._collection.remove(track._element) # noqa
|
1187
1312
|
self._decrement_track_count()
|
1188
1313
|
self._remove_cache(track)
|
1189
1314
|
|
1190
|
-
def add_playlist_folder(self, name):
|
1315
|
+
def add_playlist_folder(self, name: str) -> Node:
|
1191
1316
|
"""Add a new top-level playlist folder to the XML collection.
|
1192
1317
|
|
1193
1318
|
Parameters
|
@@ -1209,10 +1334,12 @@ class RekordboxXml:
|
|
1209
1334
|
>>> file = RekordboxXml("database.xml")
|
1210
1335
|
>>> file.add_playlist_folder("New Folder")
|
1211
1336
|
"""
|
1337
|
+
if self._root_node is None:
|
1338
|
+
raise RootNodeNotInitializedError()
|
1212
1339
|
return self._root_node.add_playlist_folder(name)
|
1213
1340
|
|
1214
|
-
def add_playlist(self, name, keytype="TrackID"):
|
1215
|
-
"""
|
1341
|
+
def add_playlist(self, name: str, keytype: str = "TrackID") -> Node:
|
1342
|
+
"""Adds a new top-level playlist to the XML collection.
|
1216
1343
|
|
1217
1344
|
Parameters
|
1218
1345
|
----------
|
@@ -1242,45 +1369,97 @@ class RekordboxXml:
|
|
1242
1369
|
|
1243
1370
|
>>> file.add_playlist("New Playlist 2", keytype="Location")
|
1244
1371
|
"""
|
1372
|
+
if self._root_node is None:
|
1373
|
+
raise RootNodeNotInitializedError()
|
1245
1374
|
return self._root_node.add_playlist(name, keytype)
|
1246
1375
|
|
1247
|
-
def tostring(self, indent=None):
|
1248
|
-
"""Returns the contents of the XML file as a string.
|
1376
|
+
def tostring(self, indent: str = None, encoding: str = "utf-8") -> str:
|
1377
|
+
r"""Returns the contents of the XML file as a string.
|
1249
1378
|
|
1250
1379
|
Parameters
|
1251
1380
|
----------
|
1252
1381
|
indent : str, optional
|
1253
|
-
The indentation used for formatting the XML file. The default is
|
1382
|
+
The indentation used for formatting the XML file. The default is '\t'.
|
1383
|
+
encoding : str, optional
|
1384
|
+
The encoding used for the XML file. The default is 'utf-8'.
|
1254
1385
|
|
1255
1386
|
Returns
|
1256
1387
|
-------
|
1257
1388
|
s : str
|
1258
1389
|
The contents of the XML file
|
1259
1390
|
"""
|
1391
|
+
if self._collection is None:
|
1392
|
+
raise XmlElementNotInitializedError("_collection")
|
1393
|
+
if self._root is None:
|
1394
|
+
raise XmlElementNotInitializedError("_root")
|
1395
|
+
|
1260
1396
|
# Check track count is valid
|
1261
1397
|
num_tracks = len(self._collection.findall(f".//{Track.TAG}"))
|
1262
1398
|
n = int(self._collection.attrib["Entries"])
|
1263
1399
|
if n != num_tracks:
|
1264
1400
|
raise ValueError(f"Track count {num_tracks} does not match number of elements {n}")
|
1265
|
-
# Generate XML string
|
1266
|
-
return pretty_xml(self._root, indent, encoding="utf-8")
|
1267
1401
|
|
1268
|
-
|
1269
|
-
|
1402
|
+
space = "\t" if indent is None else indent
|
1403
|
+
text: str
|
1404
|
+
data: bytes
|
1405
|
+
try:
|
1406
|
+
tree = xml.ElementTree(self._root)
|
1407
|
+
xml.indent(tree, space=space, level=0)
|
1408
|
+
data = xml.tostring(self._root, encoding=encoding, xml_declaration=True)
|
1409
|
+
text = data.decode(encoding)
|
1410
|
+
except AttributeError:
|
1411
|
+
# For Python < 3.9
|
1412
|
+
try:
|
1413
|
+
text = pretty_xml(self._root, space, encoding=encoding)
|
1414
|
+
except Exception: # noqa
|
1415
|
+
# If the pretty_xml function fails, use unformatted XML
|
1416
|
+
data = xml.tostring(self._root, encoding=encoding, xml_declaration=True)
|
1417
|
+
text = data.decode(encoding)
|
1418
|
+
return text
|
1419
|
+
|
1420
|
+
def save(
|
1421
|
+
self, path: Union[str, Path] = "", indent: str = None, encoding: str = "utf-8"
|
1422
|
+
) -> None:
|
1423
|
+
r"""Saves the contents to an XML file.
|
1270
1424
|
|
1271
1425
|
Parameters
|
1272
1426
|
----------
|
1273
1427
|
path : str or Path, optional
|
1274
1428
|
The path for saving the XML file. The default is the original file.
|
1275
1429
|
indent : str, optional
|
1276
|
-
The indentation used for formatting the XML
|
1277
|
-
|
1430
|
+
The indentation used for formatting the XML file. The default is '\t'.
|
1431
|
+
encoding : str, optional
|
1432
|
+
The encoding used for the XML file. The default is 'utf-8'.
|
1278
1433
|
"""
|
1279
|
-
|
1280
|
-
|
1281
|
-
|
1434
|
+
if self._collection is None:
|
1435
|
+
raise XmlElementNotInitializedError("_collection")
|
1436
|
+
if self._root is None:
|
1437
|
+
raise XmlElementNotInitializedError("_root")
|
1438
|
+
|
1439
|
+
# Check track count is valid
|
1440
|
+
num_tracks = len(self._collection.findall(f".//{Track.TAG}"))
|
1441
|
+
n = int(self._collection.attrib["Entries"])
|
1442
|
+
if n != num_tracks:
|
1443
|
+
raise ValueError(f"Track count {num_tracks} does not match number of elements {n}")
|
1282
1444
|
|
1283
|
-
|
1445
|
+
space = "\t" if indent is None else indent
|
1446
|
+
try:
|
1447
|
+
tree = xml.ElementTree(self._root)
|
1448
|
+
xml.indent(tree, space=space, level=0)
|
1449
|
+
tree.write(path, encoding=encoding, xml_declaration=True)
|
1450
|
+
except AttributeError:
|
1451
|
+
# For Python < 3.9
|
1452
|
+
try:
|
1453
|
+
data: str = pretty_xml(self._root, space, encoding=encoding)
|
1454
|
+
with open(path, "w", encoding=encoding) as fh:
|
1455
|
+
fh.write(data)
|
1456
|
+
except Exception: # noqa
|
1457
|
+
# If the pretty_xml function fails, write the XML unformatted
|
1458
|
+
text: bytes = xml.tostring(self._root, encoding=encoding, xml_declaration=True)
|
1459
|
+
with open(path, "wb") as fh:
|
1460
|
+
fh.write(text)
|
1461
|
+
|
1462
|
+
def __repr__(self) -> str:
|
1284
1463
|
name = self.product_name
|
1285
1464
|
v = self.product_version
|
1286
1465
|
company = self.product_company
|