pyrekordbox 0.2.1__py3-none-any.whl → 0.2.2__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.
- docs/source/formats/anlz.md +178 -7
- docs/source/formats/db6.md +1 -1
- docs/source/index.md +2 -6
- docs/source/quickstart.md +68 -45
- docs/source/tutorial/index.md +1 -1
- pyrekordbox/__init__.py +1 -1
- pyrekordbox/_version.py +2 -2
- pyrekordbox/anlz/file.py +39 -0
- pyrekordbox/anlz/structs.py +3 -5
- pyrekordbox/config.py +71 -27
- pyrekordbox/db6/database.py +260 -33
- pyrekordbox/db6/registry.py +22 -0
- pyrekordbox/db6/tables.py +3 -4
- {pyrekordbox-0.2.1.dist-info → pyrekordbox-0.2.2.dist-info}/METADATA +12 -11
- pyrekordbox-0.2.2.dist-info/RECORD +80 -0
- {pyrekordbox-0.2.1.dist-info → pyrekordbox-0.2.2.dist-info}/top_level.txt +0 -2
- tests/test_config.py +175 -0
- tests/test_db6.py +78 -0
- build/lib/build/lib/docs/source/conf.py +0 -178
- build/lib/build/lib/pyrekordbox/__init__.py +0 -22
- build/lib/build/lib/pyrekordbox/__main__.py +0 -204
- build/lib/build/lib/pyrekordbox/_version.py +0 -16
- build/lib/build/lib/pyrekordbox/anlz/__init__.py +0 -127
- build/lib/build/lib/pyrekordbox/anlz/file.py +0 -186
- build/lib/build/lib/pyrekordbox/anlz/structs.py +0 -299
- build/lib/build/lib/pyrekordbox/anlz/tags.py +0 -508
- build/lib/build/lib/pyrekordbox/config.py +0 -596
- build/lib/build/lib/pyrekordbox/db6/__init__.py +0 -45
- build/lib/build/lib/pyrekordbox/db6/aux_files.py +0 -213
- build/lib/build/lib/pyrekordbox/db6/database.py +0 -1808
- build/lib/build/lib/pyrekordbox/db6/registry.py +0 -304
- build/lib/build/lib/pyrekordbox/db6/tables.py +0 -1618
- build/lib/build/lib/pyrekordbox/logger.py +0 -23
- build/lib/build/lib/pyrekordbox/mysettings/__init__.py +0 -32
- build/lib/build/lib/pyrekordbox/mysettings/file.py +0 -369
- build/lib/build/lib/pyrekordbox/mysettings/structs.py +0 -282
- build/lib/build/lib/pyrekordbox/utils.py +0 -162
- build/lib/build/lib/pyrekordbox/xml.py +0 -1294
- build/lib/build/lib/tests/__init__.py +0 -3
- build/lib/build/lib/tests/test_anlz.py +0 -206
- build/lib/build/lib/tests/test_db6.py +0 -1039
- build/lib/build/lib/tests/test_mysetting.py +0 -203
- build/lib/build/lib/tests/test_xml.py +0 -629
- build/lib/docs/source/conf.py +0 -178
- build/lib/pyrekordbox/__init__.py +0 -22
- build/lib/pyrekordbox/__main__.py +0 -204
- build/lib/pyrekordbox/_version.py +0 -16
- build/lib/pyrekordbox/anlz/__init__.py +0 -127
- build/lib/pyrekordbox/anlz/file.py +0 -186
- build/lib/pyrekordbox/anlz/structs.py +0 -299
- build/lib/pyrekordbox/anlz/tags.py +0 -508
- build/lib/pyrekordbox/config.py +0 -596
- build/lib/pyrekordbox/db6/__init__.py +0 -45
- build/lib/pyrekordbox/db6/aux_files.py +0 -213
- build/lib/pyrekordbox/db6/database.py +0 -1808
- build/lib/pyrekordbox/db6/registry.py +0 -304
- build/lib/pyrekordbox/db6/tables.py +0 -1618
- build/lib/pyrekordbox/logger.py +0 -23
- build/lib/pyrekordbox/mysettings/__init__.py +0 -32
- build/lib/pyrekordbox/mysettings/file.py +0 -369
- build/lib/pyrekordbox/mysettings/structs.py +0 -282
- build/lib/pyrekordbox/utils.py +0 -162
- build/lib/pyrekordbox/xml.py +0 -1294
- build/lib/tests/__init__.py +0 -3
- build/lib/tests/test_anlz.py +0 -206
- build/lib/tests/test_db6.py +0 -1039
- build/lib/tests/test_mysetting.py +0 -203
- build/lib/tests/test_xml.py +0 -629
- pyrekordbox-0.2.1.dist-info/RECORD +0 -129
- {pyrekordbox-0.2.1.dist-info → pyrekordbox-0.2.2.dist-info}/LICENSE +0 -0
- {pyrekordbox-0.2.1.dist-info → pyrekordbox-0.2.2.dist-info}/WHEEL +0 -0
@@ -1,1294 +0,0 @@
|
|
1
|
-
# -*- coding: utf-8 -*-
|
2
|
-
# Author: Dylan Jones
|
3
|
-
# Date: 2022-04-10
|
4
|
-
|
5
|
-
r"""Rekordbox XML database file handler."""
|
6
|
-
|
7
|
-
import logging
|
8
|
-
import os.path
|
9
|
-
import urllib.parse
|
10
|
-
from abc import abstractmethod
|
11
|
-
from collections import abc
|
12
|
-
import xml.etree.cElementTree as xml
|
13
|
-
import bidict
|
14
|
-
from .utils import pretty_xml
|
15
|
-
|
16
|
-
logger = logging.getLogger(__name__)
|
17
|
-
|
18
|
-
URL_PREFIX = "file://localhost/"
|
19
|
-
POSMARK_TYPE_MAPPING = bidict.bidict(
|
20
|
-
{
|
21
|
-
"0": "cue",
|
22
|
-
"1": "fadein",
|
23
|
-
"2": "fadeout",
|
24
|
-
"3": "load",
|
25
|
-
"4": "loop",
|
26
|
-
}
|
27
|
-
)
|
28
|
-
RATING_MAPPING = bidict.bidict(
|
29
|
-
{"0": 0, "51": 1, "102": 2, "153": 3, "204": 4, "255": 5}
|
30
|
-
)
|
31
|
-
NODE_KEYTYPE_MAPPING = bidict.bidict({"0": "TrackID", "1": "Location"})
|
32
|
-
|
33
|
-
|
34
|
-
class XmlDuplicateError(Exception):
|
35
|
-
"""Raised when a track already exists in the XML database."""
|
36
|
-
|
37
|
-
def __init__(self, key_type, key):
|
38
|
-
super().__init__(f"XML database already contains a track with {key_type}={key}")
|
39
|
-
|
40
|
-
|
41
|
-
class XmlAttributeKeyError(Exception):
|
42
|
-
def __init__(self, cls, key, attributes):
|
43
|
-
super().__init__(
|
44
|
-
f"{key} is not a valid key for {cls.__name__}! Valid attribs:\n{attributes}"
|
45
|
-
)
|
46
|
-
|
47
|
-
|
48
|
-
def encode_path(path):
|
49
|
-
r"""Encodes a file path as URI string.
|
50
|
-
|
51
|
-
Parameters
|
52
|
-
----------
|
53
|
-
path : str or Path
|
54
|
-
The file path to encode.
|
55
|
-
|
56
|
-
Returns
|
57
|
-
-------
|
58
|
-
url : str
|
59
|
-
The encoded file path as URI string.
|
60
|
-
|
61
|
-
Examples
|
62
|
-
--------
|
63
|
-
>>> s = r"C:\Music\PioneerDJ\Demo Tracks\Demo Track 1.mp3" # noqa: W605
|
64
|
-
>>> encode_path(s)
|
65
|
-
file://localhost/C:/Music/PioneerDJ/Demo%20Tracks/Demo%20Track%201.mp3
|
66
|
-
|
67
|
-
"""
|
68
|
-
url_path = urllib.parse.quote(str(path), safe=":/\\")
|
69
|
-
url = URL_PREFIX + url_path.replace("\\", "/")
|
70
|
-
return url
|
71
|
-
|
72
|
-
|
73
|
-
def decode_path(url):
|
74
|
-
r"""Decodes an as URI string encoded file path.
|
75
|
-
|
76
|
-
Parameters
|
77
|
-
----------
|
78
|
-
url : str
|
79
|
-
The encoded file path to decode.
|
80
|
-
|
81
|
-
Returns
|
82
|
-
-------
|
83
|
-
path : str
|
84
|
-
The decoded file path.
|
85
|
-
|
86
|
-
Examples
|
87
|
-
--------
|
88
|
-
>>> s = r"file://localhost/C:/Music/PioneerDJ/Demo%20Tracks/Demo%20Track%201.mp3"
|
89
|
-
>>> decode_path(s)
|
90
|
-
C:\Music\PioneerDJ\Demo Tracks\Demo Track 1.mp3 # noqa: W605
|
91
|
-
|
92
|
-
"""
|
93
|
-
path = urllib.parse.unquote(url)
|
94
|
-
path = path.replace(URL_PREFIX, "")
|
95
|
-
return os.path.normpath(path)
|
96
|
-
|
97
|
-
|
98
|
-
class AbstractElement(abc.Mapping):
|
99
|
-
"""Abstract base class for Rekordbox XML elements.
|
100
|
-
|
101
|
-
Implements attribute getters and setters for an XML element
|
102
|
-
"""
|
103
|
-
|
104
|
-
TAG: str
|
105
|
-
"""str: Name of the XML element"""
|
106
|
-
|
107
|
-
ATTRIBS: list
|
108
|
-
"""list[str]: List of all attribute keys of the XML element"""
|
109
|
-
|
110
|
-
GETTERS = dict()
|
111
|
-
"""dict[str, Callable]: Dictionary of attribute getter conversion methods.
|
112
|
-
|
113
|
-
See Also
|
114
|
-
--------
|
115
|
-
AbstractElement.get
|
116
|
-
"""
|
117
|
-
SETTERS = dict()
|
118
|
-
"""dict[str, Callable]: Dictionary of attribute setter conversion methods.
|
119
|
-
|
120
|
-
See Also
|
121
|
-
--------
|
122
|
-
AbstractElement.set
|
123
|
-
"""
|
124
|
-
|
125
|
-
def __init__(self, element=None, *args, **kwargs):
|
126
|
-
self._element = element
|
127
|
-
if element is None:
|
128
|
-
self._init(*args, **kwargs)
|
129
|
-
else:
|
130
|
-
self._load_subelements()
|
131
|
-
|
132
|
-
@abstractmethod
|
133
|
-
def _init(self, *args, **kwargs):
|
134
|
-
"""Initializes a new XML element."""
|
135
|
-
pass
|
136
|
-
|
137
|
-
def _load_subelements(self):
|
138
|
-
"""Loads the sub-elements of an existing XML element."""
|
139
|
-
pass
|
140
|
-
|
141
|
-
def get(self, key, default=None):
|
142
|
-
"""Returns the value of an attribute of the XML element.
|
143
|
-
|
144
|
-
The type of the attribute value is converted if a conversion method is specified
|
145
|
-
in the ``GETTERS`` class attribute. If no conversion method is found the value
|
146
|
-
is returned unconverted as the default type ``str``.
|
147
|
-
|
148
|
-
Parameters
|
149
|
-
----------
|
150
|
-
key : str
|
151
|
-
The key of the attribute.
|
152
|
-
default : Any, optional
|
153
|
-
The default value returned if the attribute does not exist.
|
154
|
-
|
155
|
-
Returns
|
156
|
-
-------
|
157
|
-
value : Any
|
158
|
-
The value of the atttribute. The type of the attribute is converted
|
159
|
-
acccording to the data of the field.
|
160
|
-
|
161
|
-
Raises
|
162
|
-
------
|
163
|
-
XmlAttributeKeyError:
|
164
|
-
Raised if `key` is not a valid attribute key.
|
165
|
-
"""
|
166
|
-
if key not in self.ATTRIBS:
|
167
|
-
raise XmlAttributeKeyError(self.__class__, key, self.ATTRIBS)
|
168
|
-
value = self._element.attrib.get(key, default)
|
169
|
-
if value == default:
|
170
|
-
return default
|
171
|
-
try:
|
172
|
-
# Apply callback
|
173
|
-
value = self.GETTERS[key](value)
|
174
|
-
except KeyError:
|
175
|
-
pass
|
176
|
-
return value
|
177
|
-
|
178
|
-
def set(self, key, value):
|
179
|
-
"""Sets the value of an attribute of the XML element.
|
180
|
-
|
181
|
-
The type of the given value is converted before updating the attribute if a
|
182
|
-
conversion method is specified in the ``SETTERS`` class attribute.
|
183
|
-
If no conversion method is found the value updated set as given.
|
184
|
-
|
185
|
-
Parameters
|
186
|
-
----------
|
187
|
-
key : str
|
188
|
-
The key of the attribute.
|
189
|
-
value : Any
|
190
|
-
The value for updating the attribute. The type conversion is handled
|
191
|
-
automatically.
|
192
|
-
|
193
|
-
Raises
|
194
|
-
------
|
195
|
-
XmlAttributeKeyError:
|
196
|
-
Raised if `key` is not a valid attribute key.
|
197
|
-
"""
|
198
|
-
if key not in self.ATTRIBS:
|
199
|
-
raise XmlAttributeKeyError(self.__class__, key, self.ATTRIBS)
|
200
|
-
try:
|
201
|
-
# Apply callback
|
202
|
-
value = self.SETTERS[key](value)
|
203
|
-
except KeyError:
|
204
|
-
# Convert to str just in case
|
205
|
-
value = str(value)
|
206
|
-
self._element.attrib[key] = value
|
207
|
-
|
208
|
-
def __len__(self):
|
209
|
-
"""int: The number of attributes of the XML element."""
|
210
|
-
return len(self._element.attrib)
|
211
|
-
|
212
|
-
def __iter__(self):
|
213
|
-
"""Iterable: An iterator of the attribute keys of the XML element."""
|
214
|
-
return iter(self._element.attrib.keys())
|
215
|
-
|
216
|
-
def __getitem__(self, key):
|
217
|
-
"""Returns the raw value of an attribute of the XML element.
|
218
|
-
|
219
|
-
Parameters
|
220
|
-
----------
|
221
|
-
key : str
|
222
|
-
The key of the attribute.
|
223
|
-
|
224
|
-
Returns
|
225
|
-
-------
|
226
|
-
value : Any
|
227
|
-
The raw value of the attribute.
|
228
|
-
"""
|
229
|
-
return self.get(key)
|
230
|
-
|
231
|
-
def __setitem__(self, key, value):
|
232
|
-
"""Sets the raw value of an attribute of the XML element.
|
233
|
-
|
234
|
-
Parameters
|
235
|
-
----------
|
236
|
-
key : str
|
237
|
-
The key of the attribute.
|
238
|
-
value : Any
|
239
|
-
The raw value for updating the attribute.
|
240
|
-
"""
|
241
|
-
self.set(key, value)
|
242
|
-
|
243
|
-
def __getattr__(self, key):
|
244
|
-
"""Returns the raw value of an attribute of the XML element (same as `get`)."""
|
245
|
-
return self.get(key)
|
246
|
-
|
247
|
-
def __repr__(self):
|
248
|
-
return f"<{self.__class__.__name__}()>"
|
249
|
-
|
250
|
-
|
251
|
-
# -- Collection elements ---------------------------------------------------------------
|
252
|
-
|
253
|
-
|
254
|
-
# noinspection PyPep8Naming,PyUnresolvedReferences
|
255
|
-
class Tempo(AbstractElement):
|
256
|
-
"""Tempo element representing the beat grid of a track.
|
257
|
-
|
258
|
-
Attributes
|
259
|
-
----------
|
260
|
-
Inizio : float
|
261
|
-
The start position of the beat grid item.
|
262
|
-
Bpm : float
|
263
|
-
The BPM value of the beat grid item.
|
264
|
-
Metro : str
|
265
|
-
The kind of musical meter, for example '4/4'. The default is '4/4'.
|
266
|
-
Battito : int
|
267
|
-
The beat number in the bar. If `metro` is '4/4', the value can be 1, 2, 3 or 4.
|
268
|
-
"""
|
269
|
-
|
270
|
-
TAG = "TEMPO"
|
271
|
-
ATTRIBS = ["Inizio", "Bpm", "Metro", "Battito"]
|
272
|
-
GETTERS = {"Inizio": float, "Bpm": float, "Battito": int}
|
273
|
-
|
274
|
-
def __init__(
|
275
|
-
self, parent=None, Inizio=0.0, Bpm=0.0, Metro="4/4", Battito=1, element=None
|
276
|
-
):
|
277
|
-
super().__init__(element, parent, Inizio, Bpm, Metro, Battito)
|
278
|
-
|
279
|
-
def _init(self, parent, inizio, bpm, metro, battito):
|
280
|
-
attrib = {
|
281
|
-
"Inizio": str(inizio),
|
282
|
-
"Bpm": str(bpm),
|
283
|
-
"Metro": str(metro),
|
284
|
-
"Battito": str(battito),
|
285
|
-
}
|
286
|
-
self._element = xml.SubElement(parent, self.TAG, attrib=attrib)
|
287
|
-
|
288
|
-
def __repr__(self):
|
289
|
-
args = ", ".join(
|
290
|
-
[
|
291
|
-
f"Inizio={self.Inizio}",
|
292
|
-
f"Bpm={self.Bpm}",
|
293
|
-
f"Metro={self.Metro}",
|
294
|
-
f"Battito={self.Battito}",
|
295
|
-
]
|
296
|
-
)
|
297
|
-
return f"<{self.__class__.__name__}({args})>"
|
298
|
-
|
299
|
-
|
300
|
-
# noinspection PyPep8Naming,PyUnresolvedReferences
|
301
|
-
class PositionMark(AbstractElement):
|
302
|
-
"""Position element for storing position markers like cue points of a track.
|
303
|
-
|
304
|
-
Attributes
|
305
|
-
----------
|
306
|
-
Name : str
|
307
|
-
The name of the position mark.
|
308
|
-
Type : str
|
309
|
-
The type of position mark. Can be 'cue', 'fadein', 'fadeout', 'load' or 'loop'.
|
310
|
-
Start : float
|
311
|
-
Start position of the position mark in seconds.
|
312
|
-
End : float, optionl
|
313
|
-
End position of the position mark in seconds.
|
314
|
-
Num : int, optional
|
315
|
-
Charakter for identification of the position mark (for hot cues). For memory
|
316
|
-
cues this is always -1.
|
317
|
-
"""
|
318
|
-
|
319
|
-
TAG = "POSITION_MARK"
|
320
|
-
ATTRIBS = ["Name", "Type", "Start", "End", "Num"]
|
321
|
-
|
322
|
-
GETTERS = {
|
323
|
-
"Type": POSMARK_TYPE_MAPPING.get,
|
324
|
-
"Start": float,
|
325
|
-
"End": float,
|
326
|
-
"Num": int,
|
327
|
-
}
|
328
|
-
SETTERS = {"Type": POSMARK_TYPE_MAPPING.inv.get} # noqa
|
329
|
-
|
330
|
-
def __init__(
|
331
|
-
self,
|
332
|
-
parent=None,
|
333
|
-
Name="",
|
334
|
-
Type="cue",
|
335
|
-
Start=0.0,
|
336
|
-
End=None,
|
337
|
-
Num=-1,
|
338
|
-
element=None,
|
339
|
-
):
|
340
|
-
super().__init__(element, parent, Name, Type, Start, End, Num)
|
341
|
-
|
342
|
-
def _init(self, parent, name, type_, start, end, num):
|
343
|
-
attrib = {
|
344
|
-
"Name": name,
|
345
|
-
"Type": POSMARK_TYPE_MAPPING.inv.get(type_), # noqa
|
346
|
-
"Start": str(start),
|
347
|
-
"Num": str(num),
|
348
|
-
}
|
349
|
-
if end is not None:
|
350
|
-
attrib["End"] = str(end)
|
351
|
-
self._element = xml.SubElement(parent, self.TAG, attrib=attrib)
|
352
|
-
|
353
|
-
def __repr__(self):
|
354
|
-
args = ", ".join(
|
355
|
-
[
|
356
|
-
f"Name={self.Name}",
|
357
|
-
f"Type={self.Type}",
|
358
|
-
f"Start={self.Start}",
|
359
|
-
f"End={self.End}",
|
360
|
-
f"Num={self.Num}",
|
361
|
-
]
|
362
|
-
)
|
363
|
-
return f"<{self.__class__.__name__}({args})>"
|
364
|
-
|
365
|
-
|
366
|
-
# noinspection PyPep8Naming,PyUnresolvedReferences
|
367
|
-
class Track(AbstractElement):
|
368
|
-
"""Track element for storing the metadata of a track.
|
369
|
-
|
370
|
-
Attributes
|
371
|
-
----------
|
372
|
-
TrackID : int
|
373
|
-
Identification of the track.
|
374
|
-
Name: str
|
375
|
-
The name of the track.
|
376
|
-
Artist : str
|
377
|
-
The name of the artist.
|
378
|
-
Composer : str
|
379
|
-
The name of the composer (or producer).
|
380
|
-
Album : str
|
381
|
-
The name of the album.
|
382
|
-
Grouping : str
|
383
|
-
The name of the grouping.
|
384
|
-
Genre : str
|
385
|
-
The name of the genre.
|
386
|
-
Kind : str
|
387
|
-
The kind of the audio file, for example 'WAV File' or 'MP3 File'.
|
388
|
-
Size : int
|
389
|
-
The size of the audio file.
|
390
|
-
TotalTime : int
|
391
|
-
The duration of the track in seconds.
|
392
|
-
DiscNumber : int
|
393
|
-
The number of the disc of the album.
|
394
|
-
TrackNumber : int
|
395
|
-
The Number of the track of the album.
|
396
|
-
Year : int
|
397
|
-
The year of release.
|
398
|
-
AverageBpm : float
|
399
|
-
The average BPM of the track.
|
400
|
-
DateModified : str
|
401
|
-
The date of last modification in the format 'yyyy-mm-dd'.
|
402
|
-
DateAdded : str
|
403
|
-
The date of addition modification in the format 'yyyy-mm-dd'.
|
404
|
-
BitRate : int
|
405
|
-
The encoding bit rate.
|
406
|
-
SampleRate : float
|
407
|
-
The frequency of sampling.
|
408
|
-
Comments : str
|
409
|
-
The comments of the track.
|
410
|
-
PlayCount : int
|
411
|
-
The play count of the track.
|
412
|
-
LastPlayed : str
|
413
|
-
The date of last playing in the format 'yyyy-mm-dd'.
|
414
|
-
Rating : int
|
415
|
-
The rating of the track using the mapping 0=0, 1=51, 2=102, 3=153, 4=204, 5=255.
|
416
|
-
Location : str
|
417
|
-
The location of the file encoded as URI string. This value is essential for
|
418
|
-
each track.
|
419
|
-
Remixer : str
|
420
|
-
The name of the remixer.
|
421
|
-
Tonality : str
|
422
|
-
The tonality or kind of musical key.
|
423
|
-
Label : str
|
424
|
-
The name of the record label.
|
425
|
-
Mix : str
|
426
|
-
The name of the mix.
|
427
|
-
Colour : str
|
428
|
-
The color for track grouping in RGB format.
|
429
|
-
tempos : list
|
430
|
-
The `Tempo` elements of the track.
|
431
|
-
marks : list
|
432
|
-
The `PositionMark` elements of the track.
|
433
|
-
|
434
|
-
Raises
|
435
|
-
------
|
436
|
-
XmlAttributeKeyError:
|
437
|
-
Raised if initialized with invalid key in attributes.
|
438
|
-
"""
|
439
|
-
|
440
|
-
TAG = "TRACK"
|
441
|
-
ATTRIBS = [
|
442
|
-
"TrackID",
|
443
|
-
"Name",
|
444
|
-
"Artist",
|
445
|
-
"Composer",
|
446
|
-
"Album",
|
447
|
-
"Grouping",
|
448
|
-
"Genre",
|
449
|
-
"Kind",
|
450
|
-
"Size",
|
451
|
-
"TotalTime",
|
452
|
-
"DiscNumber",
|
453
|
-
"TrackNumber",
|
454
|
-
"Year",
|
455
|
-
"AverageBpm",
|
456
|
-
"DateModified",
|
457
|
-
"DateAdded",
|
458
|
-
"BitRate",
|
459
|
-
"SampleRate",
|
460
|
-
"Comments",
|
461
|
-
"PlayCount",
|
462
|
-
"LastPlayed",
|
463
|
-
"Rating",
|
464
|
-
"Location",
|
465
|
-
"Remixer",
|
466
|
-
"Tonality",
|
467
|
-
"Label",
|
468
|
-
"Mix",
|
469
|
-
"Colour",
|
470
|
-
]
|
471
|
-
|
472
|
-
GETTERS = {
|
473
|
-
"TrackID": int,
|
474
|
-
"Size": int,
|
475
|
-
"TotalTime": int,
|
476
|
-
"DiscNumber": int,
|
477
|
-
"TrackNumber": int,
|
478
|
-
"Year": int,
|
479
|
-
"AverageBpm": float,
|
480
|
-
"BitRate": int,
|
481
|
-
"SampleRate": float,
|
482
|
-
"PlayCount": int,
|
483
|
-
"Rating": RATING_MAPPING.get,
|
484
|
-
"Location": decode_path,
|
485
|
-
}
|
486
|
-
|
487
|
-
SETTERS = {"Rating": RATING_MAPPING.inv.get, "Location": encode_path} # noqa
|
488
|
-
|
489
|
-
def __init__(self, parent=None, Location="", element=None, **kwargs):
|
490
|
-
self.tempos = list()
|
491
|
-
self.marks = list()
|
492
|
-
super().__init__(element, parent, Location, **kwargs)
|
493
|
-
|
494
|
-
def _init(self, parent, Location, **kwargs):
|
495
|
-
attrib = {"Location": encode_path(Location)}
|
496
|
-
for key, val in kwargs.items():
|
497
|
-
if key not in self.ATTRIBS:
|
498
|
-
raise XmlAttributeKeyError(self.__class__, key, self.ATTRIBS)
|
499
|
-
attrib[key] = str(val)
|
500
|
-
self._element = xml.SubElement(parent, self.TAG, attrib=attrib)
|
501
|
-
|
502
|
-
def _load_subelements(self):
|
503
|
-
tempo_elements = self._element.findall(f"{Tempo.TAG}")
|
504
|
-
if tempo_elements is not None:
|
505
|
-
self.tempos = [Tempo(element=el) for el in tempo_elements]
|
506
|
-
mark_elements = self._element.findall(f".//{PositionMark.TAG}")
|
507
|
-
if mark_elements is not None:
|
508
|
-
self.marks = [PositionMark(element=el) for el in mark_elements]
|
509
|
-
|
510
|
-
def add_tempo(self, Inizio, Bpm, Metro, Battito):
|
511
|
-
"""Adds a new ``Tempo`` XML element to the track element.
|
512
|
-
|
513
|
-
Parameters
|
514
|
-
----------
|
515
|
-
Inizio : float
|
516
|
-
The start position of the beat grid item.
|
517
|
-
Bpm : float
|
518
|
-
The BPM value of the beat grid item.
|
519
|
-
Metro : str, optional
|
520
|
-
The kind of musical meter, for example '4/4'. The default is '4/4'.
|
521
|
-
Battito : int
|
522
|
-
The beat number in the bar. If `metro` is '4/4', the value can be 1, 2, 3
|
523
|
-
or 4.
|
524
|
-
|
525
|
-
Returns
|
526
|
-
-------
|
527
|
-
tempo : Tempo
|
528
|
-
The newly created tempo XML element.
|
529
|
-
|
530
|
-
See Also
|
531
|
-
--------
|
532
|
-
Tempo: Beat grid XML element handler
|
533
|
-
"""
|
534
|
-
tempo = Tempo(self._element, Inizio, Bpm, Metro, Battito)
|
535
|
-
self.tempos.append(tempo)
|
536
|
-
return tempo
|
537
|
-
|
538
|
-
def add_mark(self, Name="", Type="cue", Start=0.0, End=None, Num=-1):
|
539
|
-
"""Adds a new ``PositionMark`` XML element to the track element.
|
540
|
-
|
541
|
-
Parameters
|
542
|
-
----------
|
543
|
-
Name : str
|
544
|
-
The name of the position mark.
|
545
|
-
Type : str
|
546
|
-
The type of position mark. Can be 'cue', 'fadein', 'fadeout', 'load' or
|
547
|
-
'loop'.
|
548
|
-
Start : float
|
549
|
-
Start position of the position mark in seconds.
|
550
|
-
End : float or None, optionl
|
551
|
-
End position of the position mark in seconds.
|
552
|
-
Num : int, optional
|
553
|
-
Charakter for identification of the position mark (for hot cues). For memory
|
554
|
-
cues this is always -1.
|
555
|
-
|
556
|
-
Returns
|
557
|
-
-------
|
558
|
-
position_mark : PositionMark
|
559
|
-
The newly created position mark XML element.
|
560
|
-
|
561
|
-
See Also
|
562
|
-
--------
|
563
|
-
PositionMark: Position mark XML element handler
|
564
|
-
"""
|
565
|
-
mark = PositionMark(self._element, Name, Type, Start, End, Num)
|
566
|
-
self.marks.append(mark)
|
567
|
-
return mark
|
568
|
-
|
569
|
-
def __repr__(self):
|
570
|
-
return f"<{self.__class__.__name__}(Location={self.Location})>"
|
571
|
-
|
572
|
-
|
573
|
-
# -- Playlist elements -----------------------------------------------------------------
|
574
|
-
|
575
|
-
|
576
|
-
class Node:
|
577
|
-
"""Node element used for representing playlist folders and playlists.
|
578
|
-
|
579
|
-
A node configured as playlist folder can store other nodes as well as tracks, a node
|
580
|
-
configured as playlist can only store tracks. The tracks in playlists are stored via
|
581
|
-
a key depending on the key type of the playlist. The key type can either be the
|
582
|
-
ID of the track in the XML database ('TrackID') or the file path of the track
|
583
|
-
('Location').
|
584
|
-
"""
|
585
|
-
|
586
|
-
TAG = "NODE"
|
587
|
-
"""str: Name of the XML element"""
|
588
|
-
|
589
|
-
FOLDER = 0
|
590
|
-
PLAYLIST = 1
|
591
|
-
|
592
|
-
def __init__(self, parent=None, element=None, **attribs):
|
593
|
-
self._parent = parent
|
594
|
-
self._element = element
|
595
|
-
if element is None:
|
596
|
-
self._element = xml.SubElement(parent, self.TAG, attrib=attribs)
|
597
|
-
|
598
|
-
@classmethod
|
599
|
-
def folder(cls, parent, name):
|
600
|
-
"""Initializes a playlist folder node XML element.
|
601
|
-
|
602
|
-
Parameters
|
603
|
-
----------
|
604
|
-
parent : xml.Element
|
605
|
-
The parent node XML element of the new playlist folder node.
|
606
|
-
name : str
|
607
|
-
The name of the playlist folder node.
|
608
|
-
"""
|
609
|
-
attrib = {"Name": name, "Type": str(cls.FOLDER), "Count": "0"}
|
610
|
-
return cls(parent, **attrib)
|
611
|
-
|
612
|
-
@classmethod
|
613
|
-
def playlist(cls, parent, name, keytype="TrackID"):
|
614
|
-
"""Initializes a playlist node XML element.
|
615
|
-
|
616
|
-
Parameters
|
617
|
-
----------
|
618
|
-
parent : xml.Element
|
619
|
-
The parent node XML element of the new playlist node.
|
620
|
-
name : str
|
621
|
-
The name of the playlist node.
|
622
|
-
keytype : str, optional
|
623
|
-
The key type used by the playlist node. Can be 'TrackID' or 'Location'
|
624
|
-
(file path of the track).
|
625
|
-
"""
|
626
|
-
attrib = {
|
627
|
-
"Name": name,
|
628
|
-
"Type": str(cls.PLAYLIST),
|
629
|
-
"KeyType": NODE_KEYTYPE_MAPPING.inv[keytype], # noqa
|
630
|
-
"Entries": "0",
|
631
|
-
}
|
632
|
-
return cls(parent, **attrib)
|
633
|
-
|
634
|
-
@property
|
635
|
-
def parent(self):
|
636
|
-
"""Node: The parent of the node."""
|
637
|
-
return self._parent
|
638
|
-
|
639
|
-
@property
|
640
|
-
def name(self):
|
641
|
-
"""str: The name of node."""
|
642
|
-
return self._element.attrib.get("Name")
|
643
|
-
|
644
|
-
@property
|
645
|
-
def type(self):
|
646
|
-
"""int: The type of the node (0=folder or 1=playlist)."""
|
647
|
-
return int(self._element.attrib.get("Type"))
|
648
|
-
|
649
|
-
@property
|
650
|
-
def count(self):
|
651
|
-
"""int: The number of attributes of the XML element."""
|
652
|
-
return int(self._element.attrib.get("Count", 0))
|
653
|
-
|
654
|
-
@property
|
655
|
-
def entries(self):
|
656
|
-
"""int: The number of entries of the node."""
|
657
|
-
return int(self._element.attrib.get("Entries", 0))
|
658
|
-
|
659
|
-
@property
|
660
|
-
def key_type(self):
|
661
|
-
"""str: The type of key used by the playlist node"""
|
662
|
-
return NODE_KEYTYPE_MAPPING.get(self._element.attrib.get("KeyType"))
|
663
|
-
|
664
|
-
@property
|
665
|
-
def is_folder(self):
|
666
|
-
"""bool: True if the node is a playlist folder, false if otherwise."""
|
667
|
-
return self.type == self.FOLDER
|
668
|
-
|
669
|
-
@property
|
670
|
-
def is_playlist(self):
|
671
|
-
"""bool: True if the node is a playlist, false if otherwise."""
|
672
|
-
return self.type == self.PLAYLIST
|
673
|
-
|
674
|
-
def _update_count(self):
|
675
|
-
self._element.attrib["Count"] = str(len(self._element))
|
676
|
-
|
677
|
-
def _update_entries(self):
|
678
|
-
self._element.attrib["Entries"] = str(len(self._element))
|
679
|
-
|
680
|
-
def get_node(self, i):
|
681
|
-
"""Returns the i-th sub-Node of the current node.
|
682
|
-
|
683
|
-
Parameters
|
684
|
-
----------
|
685
|
-
i : int
|
686
|
-
Index of sub-Node
|
687
|
-
|
688
|
-
Returns
|
689
|
-
-------
|
690
|
-
subnode : Node
|
691
|
-
"""
|
692
|
-
return Node(self, element=self._element.findall(f"{self.TAG}[{i + 1}]"))
|
693
|
-
|
694
|
-
def get_playlist(self, name):
|
695
|
-
"""Returns the sub-Node with the given name.
|
696
|
-
|
697
|
-
Parameters
|
698
|
-
----------
|
699
|
-
name : str
|
700
|
-
Name of the sub-Node
|
701
|
-
|
702
|
-
Returns
|
703
|
-
-------
|
704
|
-
subnode : Node
|
705
|
-
"""
|
706
|
-
return Node(self, element=self._element.find(f'.//{self.TAG}[@Name="{name}"]'))
|
707
|
-
|
708
|
-
def get_playlists(self):
|
709
|
-
"""Returns all sub-nodes that are playlists.
|
710
|
-
|
711
|
-
Returns
|
712
|
-
-------
|
713
|
-
playlists : list[Node]
|
714
|
-
The playlist nodes in the current node.
|
715
|
-
"""
|
716
|
-
return [Node(self, element=el) for el in self._element]
|
717
|
-
|
718
|
-
def add_playlist_folder(self, name):
|
719
|
-
"""Add a new playlist folder as child to this node.
|
720
|
-
|
721
|
-
Parameters
|
722
|
-
----------
|
723
|
-
name : str
|
724
|
-
The name of the new playlist folder.
|
725
|
-
|
726
|
-
Returns
|
727
|
-
-------
|
728
|
-
folder_node : Node
|
729
|
-
The newly created playlist folder node.
|
730
|
-
|
731
|
-
Raises
|
732
|
-
------
|
733
|
-
ValueError:
|
734
|
-
Raised if called on a playlist node.
|
735
|
-
"""
|
736
|
-
if self.is_playlist:
|
737
|
-
raise ValueError("Sub-elements can only be added to a folder node!")
|
738
|
-
|
739
|
-
node = Node.folder(self._element, name)
|
740
|
-
self._update_count()
|
741
|
-
return node
|
742
|
-
|
743
|
-
def add_playlist(self, name, keytype="TrackID"):
|
744
|
-
"""Add a new playlist as child to this node.
|
745
|
-
|
746
|
-
Parameters
|
747
|
-
----------
|
748
|
-
name : str
|
749
|
-
The name of the new playlist.
|
750
|
-
keytype : {'TrackID', 'Location'} str
|
751
|
-
The type of key the playlist uses to store the tracks. Can either be
|
752
|
-
'TrackID' or 'Location'.
|
753
|
-
|
754
|
-
Returns
|
755
|
-
-------
|
756
|
-
playlist_node : Node
|
757
|
-
The newly created playlist node.
|
758
|
-
|
759
|
-
Raises
|
760
|
-
------
|
761
|
-
ValueError:
|
762
|
-
Raised if called on a playlist node.
|
763
|
-
"""
|
764
|
-
if self.is_playlist:
|
765
|
-
raise ValueError("Sub-elements can only be added to a folder node!")
|
766
|
-
|
767
|
-
node = Node.playlist(self._element, name, keytype)
|
768
|
-
self._update_count()
|
769
|
-
return node
|
770
|
-
|
771
|
-
def remove_playlist(self, name):
|
772
|
-
"""Removes a playlist from the playlist folder node.
|
773
|
-
|
774
|
-
Parameters
|
775
|
-
----------
|
776
|
-
name : str
|
777
|
-
The name of the playlist to remove.
|
778
|
-
"""
|
779
|
-
item = self.get_playlist(name)
|
780
|
-
self._element.remove(item._element) # noqa
|
781
|
-
self._update_count()
|
782
|
-
self._update_entries()
|
783
|
-
|
784
|
-
def add_track(self, key):
|
785
|
-
"""Adds a new track to the playlist node.
|
786
|
-
|
787
|
-
Parameters
|
788
|
-
----------
|
789
|
-
key : int or str
|
790
|
-
The key of the track to add, depending on the `type` of the playlist node.
|
791
|
-
|
792
|
-
Returns
|
793
|
-
-------
|
794
|
-
el : xml.SubElement
|
795
|
-
The newly created playlist track element.
|
796
|
-
"""
|
797
|
-
el = xml.SubElement(self._element, Track.TAG, attrib={"Key": str(key)})
|
798
|
-
self._update_entries()
|
799
|
-
return el
|
800
|
-
|
801
|
-
def remove_track(self, key):
|
802
|
-
"""Removes a track from the playlist node.
|
803
|
-
|
804
|
-
Parameters
|
805
|
-
----------
|
806
|
-
key : int or str
|
807
|
-
The key of the track to remove, depending on the `type` attribute of the
|
808
|
-
playlist node.
|
809
|
-
"""
|
810
|
-
el = self._element.find(f'{Track.TAG}[@Key="{key}"]')
|
811
|
-
self._element.remove(el)
|
812
|
-
self._update_entries()
|
813
|
-
return el
|
814
|
-
|
815
|
-
def get_tracks(self):
|
816
|
-
"""Returns the keys of all tracks contained in the playlist node.
|
817
|
-
|
818
|
-
Returns
|
819
|
-
-------
|
820
|
-
keys : list
|
821
|
-
The keys of the tracks in the playlist. The format depends on the `type`
|
822
|
-
attribute of the playlist node.
|
823
|
-
"""
|
824
|
-
if self.type == self.FOLDER:
|
825
|
-
return list()
|
826
|
-
elements = self._element.findall(f".//{Track.TAG}")
|
827
|
-
items = list()
|
828
|
-
for el in elements:
|
829
|
-
val = el.attrib["Key"]
|
830
|
-
if self.key_type == "TrackID":
|
831
|
-
val = int(val)
|
832
|
-
items.append(val)
|
833
|
-
return items
|
834
|
-
|
835
|
-
def get_track(self, key):
|
836
|
-
"""Returns the formatted key of the track."""
|
837
|
-
el = self._element.find(f'{Track.TAG}[@Key="{key}"]')
|
838
|
-
val = el.attrib["Key"]
|
839
|
-
if self.key_type == "TrackID":
|
840
|
-
val = int(val)
|
841
|
-
return val
|
842
|
-
|
843
|
-
def treestr(self, indent=4, lvl=0):
|
844
|
-
"""returns a formatted string of the node tree strucutre.
|
845
|
-
|
846
|
-
Parameters
|
847
|
-
----------
|
848
|
-
indent : int, optional
|
849
|
-
Number of spaces used for indenting.
|
850
|
-
lvl : int, optional
|
851
|
-
Internal parameter for recursion, don't use!
|
852
|
-
|
853
|
-
Returns
|
854
|
-
-------
|
855
|
-
s : str
|
856
|
-
The formatted tree string.
|
857
|
-
"""
|
858
|
-
space = indent * lvl * " "
|
859
|
-
string = ""
|
860
|
-
if self.type == self.PLAYLIST:
|
861
|
-
string += space + f"Playlist: {self.name} ({self.entries} Tracks)\n"
|
862
|
-
elif self.type == self.FOLDER:
|
863
|
-
string += space + f"Folder: {self.name}\n"
|
864
|
-
for node in self.get_playlists():
|
865
|
-
string += node.treestr(indent, lvl + 1)
|
866
|
-
return string
|
867
|
-
|
868
|
-
def __eq__(self, other):
|
869
|
-
return self.parent == other.parent and self.name == other.name
|
870
|
-
|
871
|
-
def __repr__(self):
|
872
|
-
return f"<{self.__class__.__name__}({self.name})>"
|
873
|
-
|
874
|
-
|
875
|
-
# -- Main XML object -------------------------------------------------------------------
|
876
|
-
|
877
|
-
|
878
|
-
# noinspection PyPep8Naming,PyUnresolvedReferences
|
879
|
-
class RekordboxXml:
|
880
|
-
"""Rekordbox XML database object.
|
881
|
-
|
882
|
-
The XML database contains the tracks and playlists in the Rekordbox collection. By
|
883
|
-
importing the database, new tracks and items can be added to the Rekordbox
|
884
|
-
collection.
|
885
|
-
|
886
|
-
If a file path is passed to the constructor of the ``RekordboxXml`` object, the file
|
887
|
-
is opened and parsed. Otherwise, an empty file is created with the given arguments.
|
888
|
-
Creating an importable XML file requires a product name, xml database version and
|
889
|
-
company name.
|
890
|
-
|
891
|
-
Attributes
|
892
|
-
----------
|
893
|
-
path : str, optional
|
894
|
-
The file path to
|
895
|
-
|
896
|
-
Examples
|
897
|
-
--------
|
898
|
-
Open Rekordbox XML file
|
899
|
-
|
900
|
-
>>> file = RekordboxXml(Path(".testdata", "rekordbox 6", "database.xml"))
|
901
|
-
|
902
|
-
Create new XML file
|
903
|
-
|
904
|
-
>>> file = RekordboxXml()
|
905
|
-
|
906
|
-
"""
|
907
|
-
|
908
|
-
ROOT_TAG = "DJ_PLAYLISTS"
|
909
|
-
PRDT_TAG = "PRODUCT"
|
910
|
-
PLST_TAG = "PLAYLISTS"
|
911
|
-
COLL_TAG = "COLLECTION"
|
912
|
-
|
913
|
-
def __init__(self, path=None, name=None, version=None, company=None):
|
914
|
-
self._root = None
|
915
|
-
self._product = None
|
916
|
-
self._collection = None
|
917
|
-
self._playlists = None
|
918
|
-
self._root_node = None
|
919
|
-
|
920
|
-
self._last_id = 0
|
921
|
-
# Used for fast duplicate check
|
922
|
-
self._locations = set()
|
923
|
-
self._ids = set()
|
924
|
-
|
925
|
-
if path is not None:
|
926
|
-
self._parse(path)
|
927
|
-
else:
|
928
|
-
self._init(name, version, company)
|
929
|
-
|
930
|
-
@property
|
931
|
-
def frmt_version(self):
|
932
|
-
"""str : The version of the Rekordbox XML format."""
|
933
|
-
return self._root.attrib["Version"]
|
934
|
-
|
935
|
-
@property
|
936
|
-
def product_name(self):
|
937
|
-
"""str : The product name that will be displayed in the software."""
|
938
|
-
return self._product.attrib.get("Name")
|
939
|
-
|
940
|
-
@property
|
941
|
-
def product_version(self):
|
942
|
-
"""str : The product version."""
|
943
|
-
return self._product.attrib.get("Version")
|
944
|
-
|
945
|
-
@property
|
946
|
-
def product_company(self):
|
947
|
-
"""str : The company name."""
|
948
|
-
return self._product.attrib.get("Company")
|
949
|
-
|
950
|
-
@property
|
951
|
-
def num_tracks(self):
|
952
|
-
"""str : The number of tracks in the collection."""
|
953
|
-
return int(self._collection.attrib.get("Entries"))
|
954
|
-
|
955
|
-
@property
|
956
|
-
def root_playlist_folder(self):
|
957
|
-
"""Node: The node of the root playlist folder containing all other nodes."""
|
958
|
-
return self._root_node
|
959
|
-
|
960
|
-
def _parse(self, path):
|
961
|
-
"""Parse an existing XML file.
|
962
|
-
|
963
|
-
Parameters
|
964
|
-
----------
|
965
|
-
path : str or Path
|
966
|
-
The path to the XML file to parse.
|
967
|
-
"""
|
968
|
-
tree = xml.parse(str(path))
|
969
|
-
self._root = tree.getroot()
|
970
|
-
self._product = self._root.find(self.PRDT_TAG)
|
971
|
-
self._collection = self._root.find(self.COLL_TAG)
|
972
|
-
self._playlists = self._root.find(self.PLST_TAG)
|
973
|
-
self._root_node = Node(element=self._playlists.find(Node.TAG))
|
974
|
-
self._update_cache()
|
975
|
-
|
976
|
-
def _init(self, name=None, version=None, company=None, frmt_version=None):
|
977
|
-
"""Initialize a new XML file."""
|
978
|
-
frmt_version = frmt_version or "1.0.0"
|
979
|
-
name = name or "pyrekordbox"
|
980
|
-
version = version or "0.0.1"
|
981
|
-
company = company or ""
|
982
|
-
|
983
|
-
# Initialize root element
|
984
|
-
self._root = xml.Element(self.ROOT_TAG, attrib={"Version": frmt_version})
|
985
|
-
# Initialize product element
|
986
|
-
attrib = {"Name": name, "Version": version, "Company": company}
|
987
|
-
self._product = xml.SubElement(self._root, self.PRDT_TAG, attrib=attrib)
|
988
|
-
# Initialize collection element
|
989
|
-
attrib = {"Entries": "0"}
|
990
|
-
self._collection = xml.SubElement(self._root, self.COLL_TAG, attrib=attrib)
|
991
|
-
# Initialize playlist element
|
992
|
-
self._playlists = xml.SubElement(self._root, self.PLST_TAG)
|
993
|
-
self._root_node = Node.folder(self._playlists, "ROOT")
|
994
|
-
|
995
|
-
track_ids = self.get_track_ids()
|
996
|
-
if track_ids:
|
997
|
-
self._last_id = max(track_ids)
|
998
|
-
|
999
|
-
def get_tracks(self):
|
1000
|
-
"""Returns the tracks in the collection of the XML file.
|
1001
|
-
|
1002
|
-
Returns
|
1003
|
-
-------
|
1004
|
-
tracks : list of Track
|
1005
|
-
A list of the track objects in the collection.
|
1006
|
-
"""
|
1007
|
-
elements = self._collection.findall(f".//{Track.TAG}")
|
1008
|
-
return [Track(element=el) for el in elements]
|
1009
|
-
|
1010
|
-
def get_track(self, index=None, TrackID=None, Location=None):
|
1011
|
-
"""Get a track in the collection of the XML file.
|
1012
|
-
|
1013
|
-
Parameters
|
1014
|
-
----------
|
1015
|
-
index : int, optional
|
1016
|
-
If `index` is given, the track with this index in the collection is
|
1017
|
-
returned.
|
1018
|
-
TrackID : int, optional
|
1019
|
-
If `TrackID` is given, the track with this ID in the collection is
|
1020
|
-
returned.
|
1021
|
-
Location : str, optional
|
1022
|
-
If `Location` is given, the track with this file path in the collection is
|
1023
|
-
returned.
|
1024
|
-
|
1025
|
-
Returns
|
1026
|
-
-------
|
1027
|
-
track : Track
|
1028
|
-
The XML track element.
|
1029
|
-
|
1030
|
-
Raises
|
1031
|
-
------
|
1032
|
-
ValueError:
|
1033
|
-
Raised if neither the index of the track id is specified.
|
1034
|
-
|
1035
|
-
Examples
|
1036
|
-
--------
|
1037
|
-
Get track by index
|
1038
|
-
|
1039
|
-
>>> file = RekordboxXml("database.xml")
|
1040
|
-
>>> track = file.get_track(0)
|
1041
|
-
|
1042
|
-
or by ``TrackID``
|
1043
|
-
|
1044
|
-
>>> track = file.get_track(TrackID=1)
|
1045
|
-
|
1046
|
-
"""
|
1047
|
-
if index is None and TrackID is None:
|
1048
|
-
raise ValueError("Either index or TrackID has to be specified!")
|
1049
|
-
|
1050
|
-
if TrackID is not None:
|
1051
|
-
el = self._collection.find(f'.//{Track.TAG}[@TrackID="{TrackID}"]')
|
1052
|
-
elif Location is not None:
|
1053
|
-
el = self._collection.find(f'.//{Track.TAG}[@Location="{Location}"]')
|
1054
|
-
else:
|
1055
|
-
el = self._collection.find(f".//{Track.TAG}[{index + 1}]")
|
1056
|
-
return Track(element=el)
|
1057
|
-
|
1058
|
-
def get_track_ids(self):
|
1059
|
-
"""Returns the `TrackID` of all tracks in the collection of the XML file.
|
1060
|
-
|
1061
|
-
Returns
|
1062
|
-
-------
|
1063
|
-
ids : list of int
|
1064
|
-
The ID's of all tracks.
|
1065
|
-
"""
|
1066
|
-
elements = self._collection.findall(f".//{Track.TAG}")
|
1067
|
-
return [int(el.attrib["TrackID"]) for el in elements]
|
1068
|
-
|
1069
|
-
def get_playlist(self, *names):
|
1070
|
-
"""Returns a playlist or playlist folder with the given path.
|
1071
|
-
|
1072
|
-
Parameters
|
1073
|
-
----------
|
1074
|
-
*names : str
|
1075
|
-
Names in the path. If no names are given the root playlist folder is
|
1076
|
-
returned.
|
1077
|
-
|
1078
|
-
Returns
|
1079
|
-
-------
|
1080
|
-
node : Node
|
1081
|
-
The playlist or playlist folder node.
|
1082
|
-
|
1083
|
-
Examples
|
1084
|
-
--------
|
1085
|
-
>>> file = RekordboxXml("database.xml")
|
1086
|
-
>>> playlist = file.get_playlist("Folder", "Sub Playlist")
|
1087
|
-
|
1088
|
-
"""
|
1089
|
-
node = self._root_node
|
1090
|
-
if not names:
|
1091
|
-
return node
|
1092
|
-
for name in names:
|
1093
|
-
node = node.get_playlist(name)
|
1094
|
-
return node
|
1095
|
-
|
1096
|
-
def _update_track_count(self):
|
1097
|
-
"""Updates the track count element."""
|
1098
|
-
num_tracks = len(self._collection.findall(f".//{Track.TAG}"))
|
1099
|
-
self._collection.attrib["Entries"] = str(num_tracks)
|
1100
|
-
|
1101
|
-
def _increment_track_count(self):
|
1102
|
-
"""Increment the track count element."""
|
1103
|
-
old = int(self._collection.attrib["Entries"])
|
1104
|
-
self._collection.attrib["Entries"] = str(old + 1)
|
1105
|
-
|
1106
|
-
def _decrement_track_count(self):
|
1107
|
-
"""Decrement the track count element."""
|
1108
|
-
old = int(self._collection.attrib["Entries"])
|
1109
|
-
self._collection.attrib["Entries"] = str(old - 1)
|
1110
|
-
|
1111
|
-
def _add_cache(self, track):
|
1112
|
-
"""Add the TrackID and Location to the cache."""
|
1113
|
-
self._locations.add(track.Location)
|
1114
|
-
self._ids.add(track.TrackID)
|
1115
|
-
|
1116
|
-
def _remove_cache(self, track):
|
1117
|
-
"""Remove the TrackID and Location from the cache."""
|
1118
|
-
self._locations.remove(track.Location)
|
1119
|
-
self._ids.remove(track.TrackID)
|
1120
|
-
|
1121
|
-
def _update_cache(self):
|
1122
|
-
"""Update the cache with the current tracks in the collection."""
|
1123
|
-
self._locations.clear()
|
1124
|
-
self._ids.clear()
|
1125
|
-
for track in self.get_tracks():
|
1126
|
-
self._add_cache(track)
|
1127
|
-
|
1128
|
-
def add_track(self, location, **kwargs):
|
1129
|
-
"""Add a new track element to the Rekordbox XML collection.
|
1130
|
-
|
1131
|
-
Parameters
|
1132
|
-
----------
|
1133
|
-
location : str or Path
|
1134
|
-
The file path of the track.
|
1135
|
-
kwargs :
|
1136
|
-
Keyword arguments which are used to fill the track attributes. If no
|
1137
|
-
argument for ``TrackID`` is given the ID is auto-incremented.
|
1138
|
-
|
1139
|
-
Returns
|
1140
|
-
-------
|
1141
|
-
track : Track
|
1142
|
-
The newly created XML track element.
|
1143
|
-
|
1144
|
-
Raises
|
1145
|
-
------
|
1146
|
-
ValueError:
|
1147
|
-
Raised if the database already contains a track with the track-id
|
1148
|
-
or file path.
|
1149
|
-
|
1150
|
-
Examples
|
1151
|
-
--------
|
1152
|
-
>>> file = RekordboxXml("database.xml")
|
1153
|
-
>>> _ = file.add_track("path/to/track.wav")
|
1154
|
-
"""
|
1155
|
-
if "TrackID" not in kwargs:
|
1156
|
-
kwargs["TrackID"] = self._last_id + 1
|
1157
|
-
|
1158
|
-
# Check that Location and TrackID are unique
|
1159
|
-
track_id = kwargs["TrackID"]
|
1160
|
-
if os.path.normpath(location) in self._locations:
|
1161
|
-
raise XmlDuplicateError("Location", location)
|
1162
|
-
if track_id in self._ids:
|
1163
|
-
raise XmlDuplicateError("TrackID", track_id)
|
1164
|
-
|
1165
|
-
# Create track and add it to the collection
|
1166
|
-
track = Track(self._collection, location, **kwargs)
|
1167
|
-
self._last_id = int(track["TrackID"])
|
1168
|
-
self._increment_track_count()
|
1169
|
-
self._add_cache(track)
|
1170
|
-
return track
|
1171
|
-
|
1172
|
-
def remove_track(self, track):
|
1173
|
-
"""Remove a track element from the Rekordbox XML collection.
|
1174
|
-
|
1175
|
-
Parameters
|
1176
|
-
----------
|
1177
|
-
track : Track
|
1178
|
-
The XML track element to remove.
|
1179
|
-
|
1180
|
-
Examples
|
1181
|
-
--------
|
1182
|
-
>>> file = RekordboxXml("database.xml")
|
1183
|
-
>>> t = file.get_track(0)
|
1184
|
-
>>> file.remove_track(t)
|
1185
|
-
|
1186
|
-
"""
|
1187
|
-
self._collection.remove(track._element) # noqa
|
1188
|
-
self._decrement_track_count()
|
1189
|
-
self._remove_cache(track)
|
1190
|
-
|
1191
|
-
def add_playlist_folder(self, name):
|
1192
|
-
"""Add a new top-level playlist folder to the XML collection.
|
1193
|
-
|
1194
|
-
Parameters
|
1195
|
-
----------
|
1196
|
-
name : str
|
1197
|
-
The name of the new playlist folder.
|
1198
|
-
|
1199
|
-
Returns
|
1200
|
-
-------
|
1201
|
-
folder_node : Node
|
1202
|
-
The newly created playlist folder node.
|
1203
|
-
|
1204
|
-
See Also
|
1205
|
-
--------
|
1206
|
-
Node.add_playlist_folder
|
1207
|
-
|
1208
|
-
Examples
|
1209
|
-
--------
|
1210
|
-
>>> file = RekordboxXml("database.xml")
|
1211
|
-
>>> file.add_playlist_folder("New Folder")
|
1212
|
-
"""
|
1213
|
-
return self._root_node.add_playlist_folder(name)
|
1214
|
-
|
1215
|
-
def add_playlist(self, name, keytype="TrackID"):
|
1216
|
-
"""Add a new top-level playlist to the XML collection.
|
1217
|
-
|
1218
|
-
Parameters
|
1219
|
-
----------
|
1220
|
-
name : str
|
1221
|
-
The name of the new playlist.
|
1222
|
-
keytype : {'TrackID', 'Location'} str
|
1223
|
-
The type of key the playlist uses to store the tracks. Can either be
|
1224
|
-
'TrackID' or 'Location'.
|
1225
|
-
|
1226
|
-
Returns
|
1227
|
-
-------
|
1228
|
-
playlist_node : Node
|
1229
|
-
The newly created playlist node.
|
1230
|
-
|
1231
|
-
See Also
|
1232
|
-
--------
|
1233
|
-
Node.add_playlist
|
1234
|
-
|
1235
|
-
Examples
|
1236
|
-
--------
|
1237
|
-
Create playlist using the track ID as keys
|
1238
|
-
|
1239
|
-
>>> file = RekordboxXml("database.xml")
|
1240
|
-
>>> file.add_playlist("New Playlist", keytype="TrackID")
|
1241
|
-
|
1242
|
-
Create playlist using the file paths as keys
|
1243
|
-
|
1244
|
-
>>> file.add_playlist("New Playlist 2", keytype="Location")
|
1245
|
-
"""
|
1246
|
-
return self._root_node.add_playlist(name, keytype)
|
1247
|
-
|
1248
|
-
def tostring(self, indent=None):
|
1249
|
-
"""Returns the contents of the XML file as a string.
|
1250
|
-
|
1251
|
-
Parameters
|
1252
|
-
----------
|
1253
|
-
indent : str, optional
|
1254
|
-
The indentation used for formatting the XML file. The default is 3 spaces.
|
1255
|
-
|
1256
|
-
Returns
|
1257
|
-
-------
|
1258
|
-
s : str
|
1259
|
-
The contents of the XML file
|
1260
|
-
"""
|
1261
|
-
# Check track count is valid
|
1262
|
-
num_tracks = len(self._collection.findall(f".//{Track.TAG}"))
|
1263
|
-
n = int(self._collection.attrib["Entries"])
|
1264
|
-
if n != num_tracks:
|
1265
|
-
raise ValueError(
|
1266
|
-
f"Track count {num_tracks} does not match number of elements {n}"
|
1267
|
-
)
|
1268
|
-
# Generate XML string
|
1269
|
-
return pretty_xml(self._root, indent, encoding="utf-8")
|
1270
|
-
|
1271
|
-
def save(self, path="", indent=None):
|
1272
|
-
"""Saves the contents to an XML file.
|
1273
|
-
|
1274
|
-
Parameters
|
1275
|
-
----------
|
1276
|
-
path : str or Path, optional
|
1277
|
-
The path for saving the XML file. The default is the original file.
|
1278
|
-
indent : str, optional
|
1279
|
-
The indentation used for formatting the XML element.
|
1280
|
-
The default is 3 spaces.
|
1281
|
-
"""
|
1282
|
-
string = self.tostring(indent)
|
1283
|
-
with open(path, "w") as fh:
|
1284
|
-
fh.write(string)
|
1285
|
-
|
1286
|
-
def __repr__(self):
|
1287
|
-
name = self.product_name
|
1288
|
-
v = self.product_version
|
1289
|
-
company = self.product_company
|
1290
|
-
tracks = self.num_tracks
|
1291
|
-
|
1292
|
-
cls = self.__class__.__name__
|
1293
|
-
s = f"{cls}(tracks={tracks}, info={name}, {company}, v{v})"
|
1294
|
-
return s
|