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.
Files changed (71) hide show
  1. docs/source/formats/anlz.md +178 -7
  2. docs/source/formats/db6.md +1 -1
  3. docs/source/index.md +2 -6
  4. docs/source/quickstart.md +68 -45
  5. docs/source/tutorial/index.md +1 -1
  6. pyrekordbox/__init__.py +1 -1
  7. pyrekordbox/_version.py +2 -2
  8. pyrekordbox/anlz/file.py +39 -0
  9. pyrekordbox/anlz/structs.py +3 -5
  10. pyrekordbox/config.py +71 -27
  11. pyrekordbox/db6/database.py +260 -33
  12. pyrekordbox/db6/registry.py +22 -0
  13. pyrekordbox/db6/tables.py +3 -4
  14. {pyrekordbox-0.2.1.dist-info → pyrekordbox-0.2.2.dist-info}/METADATA +12 -11
  15. pyrekordbox-0.2.2.dist-info/RECORD +80 -0
  16. {pyrekordbox-0.2.1.dist-info → pyrekordbox-0.2.2.dist-info}/top_level.txt +0 -2
  17. tests/test_config.py +175 -0
  18. tests/test_db6.py +78 -0
  19. build/lib/build/lib/docs/source/conf.py +0 -178
  20. build/lib/build/lib/pyrekordbox/__init__.py +0 -22
  21. build/lib/build/lib/pyrekordbox/__main__.py +0 -204
  22. build/lib/build/lib/pyrekordbox/_version.py +0 -16
  23. build/lib/build/lib/pyrekordbox/anlz/__init__.py +0 -127
  24. build/lib/build/lib/pyrekordbox/anlz/file.py +0 -186
  25. build/lib/build/lib/pyrekordbox/anlz/structs.py +0 -299
  26. build/lib/build/lib/pyrekordbox/anlz/tags.py +0 -508
  27. build/lib/build/lib/pyrekordbox/config.py +0 -596
  28. build/lib/build/lib/pyrekordbox/db6/__init__.py +0 -45
  29. build/lib/build/lib/pyrekordbox/db6/aux_files.py +0 -213
  30. build/lib/build/lib/pyrekordbox/db6/database.py +0 -1808
  31. build/lib/build/lib/pyrekordbox/db6/registry.py +0 -304
  32. build/lib/build/lib/pyrekordbox/db6/tables.py +0 -1618
  33. build/lib/build/lib/pyrekordbox/logger.py +0 -23
  34. build/lib/build/lib/pyrekordbox/mysettings/__init__.py +0 -32
  35. build/lib/build/lib/pyrekordbox/mysettings/file.py +0 -369
  36. build/lib/build/lib/pyrekordbox/mysettings/structs.py +0 -282
  37. build/lib/build/lib/pyrekordbox/utils.py +0 -162
  38. build/lib/build/lib/pyrekordbox/xml.py +0 -1294
  39. build/lib/build/lib/tests/__init__.py +0 -3
  40. build/lib/build/lib/tests/test_anlz.py +0 -206
  41. build/lib/build/lib/tests/test_db6.py +0 -1039
  42. build/lib/build/lib/tests/test_mysetting.py +0 -203
  43. build/lib/build/lib/tests/test_xml.py +0 -629
  44. build/lib/docs/source/conf.py +0 -178
  45. build/lib/pyrekordbox/__init__.py +0 -22
  46. build/lib/pyrekordbox/__main__.py +0 -204
  47. build/lib/pyrekordbox/_version.py +0 -16
  48. build/lib/pyrekordbox/anlz/__init__.py +0 -127
  49. build/lib/pyrekordbox/anlz/file.py +0 -186
  50. build/lib/pyrekordbox/anlz/structs.py +0 -299
  51. build/lib/pyrekordbox/anlz/tags.py +0 -508
  52. build/lib/pyrekordbox/config.py +0 -596
  53. build/lib/pyrekordbox/db6/__init__.py +0 -45
  54. build/lib/pyrekordbox/db6/aux_files.py +0 -213
  55. build/lib/pyrekordbox/db6/database.py +0 -1808
  56. build/lib/pyrekordbox/db6/registry.py +0 -304
  57. build/lib/pyrekordbox/db6/tables.py +0 -1618
  58. build/lib/pyrekordbox/logger.py +0 -23
  59. build/lib/pyrekordbox/mysettings/__init__.py +0 -32
  60. build/lib/pyrekordbox/mysettings/file.py +0 -369
  61. build/lib/pyrekordbox/mysettings/structs.py +0 -282
  62. build/lib/pyrekordbox/utils.py +0 -162
  63. build/lib/pyrekordbox/xml.py +0 -1294
  64. build/lib/tests/__init__.py +0 -3
  65. build/lib/tests/test_anlz.py +0 -206
  66. build/lib/tests/test_db6.py +0 -1039
  67. build/lib/tests/test_mysetting.py +0 -203
  68. build/lib/tests/test_xml.py +0 -629
  69. pyrekordbox-0.2.1.dist-info/RECORD +0 -129
  70. {pyrekordbox-0.2.1.dist-info → pyrekordbox-0.2.2.dist-info}/LICENSE +0 -0
  71. {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