pyrekordbox 0.4.1__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/__main__.py +3 -2
- 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 +36 -26
- pyrekordbox/db6/aux_files.py +40 -14
- pyrekordbox/db6/database.py +382 -220
- 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 +327 -147
- pyrekordbox/utils.py +7 -6
- {pyrekordbox-0.4.1.dist-info → pyrekordbox-0.4.3.dist-info}/METADATA +1 -1
- pyrekordbox-0.4.3.dist-info/RECORD +25 -0
- {pyrekordbox-0.4.1.dist-info → pyrekordbox-0.4.3.dist-info}/WHEEL +1 -1
- pyrekordbox-0.4.1.dist-info/RECORD +0 -25
- {pyrekordbox-0.4.1.dist-info → pyrekordbox-0.4.3.dist-info}/licenses/LICENSE +0 -0
- {pyrekordbox-0.4.1.dist-info → pyrekordbox-0.4.3.dist-info}/top_level.txt +0 -0
pyrekordbox/config.py
CHANGED
@@ -18,7 +18,7 @@ import textwrap
|
|
18
18
|
import time
|
19
19
|
import xml.etree.cElementTree as xml
|
20
20
|
from pathlib import Path
|
21
|
-
from typing import Union
|
21
|
+
from typing import Any, Dict, List, Union
|
22
22
|
|
23
23
|
import blowfish
|
24
24
|
import frida
|
@@ -67,6 +67,10 @@ def get_appdata_dir() -> Path:
|
|
67
67
|
return app_data
|
68
68
|
|
69
69
|
|
70
|
+
def get_cache_file() -> Path:
|
71
|
+
return get_appdata_dir() / "pyrekordbox" / _cache_file_name
|
72
|
+
|
73
|
+
|
70
74
|
def get_pioneer_install_dir(path: Union[str, Path] = None) -> Path: # pragma: no cover
|
71
75
|
"""Returns the path of the Pioneer program installation directory.
|
72
76
|
|
@@ -147,7 +151,7 @@ def get_pioneer_app_dir(path: Union[str, Path] = None) -> Path: # pragma: no co
|
|
147
151
|
return path
|
148
152
|
|
149
153
|
|
150
|
-
def _convert_type(s):
|
154
|
+
def _convert_type(s: str) -> Union[str, int, float, List[int], List[float]]:
|
151
155
|
# Try to parse as int, float, list of int, list of float
|
152
156
|
types_ = int, float
|
153
157
|
for type_ in types_:
|
@@ -163,7 +167,7 @@ def _convert_type(s):
|
|
163
167
|
return s
|
164
168
|
|
165
169
|
|
166
|
-
def read_rekordbox_settings(rekordbox_app_dir: Union[str, Path]) ->
|
170
|
+
def read_rekordbox_settings(rekordbox_app_dir: Union[str, Path]) -> Dict[str, Any]:
|
167
171
|
"""Finds and parses the 'rekordbox3.settings' file in the Rekordbox 5 or 6 app-dir.
|
168
172
|
|
169
173
|
The settings file usually is called 'rekordbox3.settings' and is
|
@@ -193,12 +197,12 @@ def read_rekordbox_settings(rekordbox_app_dir: Union[str, Path]) -> dict:
|
|
193
197
|
val = _convert_type(element.attrib["val"])
|
194
198
|
except KeyError:
|
195
199
|
device_setup = element.find("DEVICESETUP")
|
196
|
-
val = {k: _convert_type(v) for k, v in device_setup.attrib.items()}
|
200
|
+
val = {k: _convert_type(v) for k, v in device_setup.attrib.items()} # type: ignore
|
197
201
|
settings[name] = val
|
198
202
|
return settings
|
199
203
|
|
200
204
|
|
201
|
-
def read_rekordbox6_options(pioneer_app_dir: Union[str, Path]) ->
|
205
|
+
def read_rekordbox6_options(pioneer_app_dir: Union[str, Path]) -> Dict[str, Any]:
|
202
206
|
"""Finds and parses the Rekordbox 6 `options.json` file with additional settings.
|
203
207
|
|
204
208
|
The options file contains additional settings used by Rekordbox 6, for example the
|
@@ -267,7 +271,7 @@ def read_rekordbox6_asar(rb6_install_dir: Union[str, Path]) -> str:
|
|
267
271
|
return data
|
268
272
|
|
269
273
|
|
270
|
-
def _extract_version(name, major_version):
|
274
|
+
def _extract_version(name: str, major_version: int) -> str:
|
271
275
|
name = name.replace(".app", "") # Needed for MacOS
|
272
276
|
ver_str = name.replace("rekordbox", "").strip()
|
273
277
|
if not ver_str:
|
@@ -280,7 +284,7 @@ def _get_rb_config(
|
|
280
284
|
pioneer_app_dir: Path,
|
281
285
|
major_version: int,
|
282
286
|
application_dirname: str = "",
|
283
|
-
) ->
|
287
|
+
) -> Dict[str, Any]:
|
284
288
|
"""Get the program configuration for a given Rekordbox major version.
|
285
289
|
|
286
290
|
Parameters
|
@@ -374,7 +378,9 @@ def _get_rb_config(
|
|
374
378
|
return conf
|
375
379
|
|
376
380
|
|
377
|
-
def _get_rb5_config(
|
381
|
+
def _get_rb5_config(
|
382
|
+
pioneer_prog_dir: Path, pioneer_app_dir: Path, dirname: str = ""
|
383
|
+
) -> Dict[str, Any]:
|
378
384
|
"""Get the program configuration for Rekordbox v5.x.x."""
|
379
385
|
major_version = 5
|
380
386
|
conf = _get_rb_config(pioneer_prog_dir, pioneer_app_dir, major_version, dirname)
|
@@ -413,16 +419,16 @@ class KeyExtractor:
|
|
413
419
|
""")
|
414
420
|
# fmt: on
|
415
421
|
|
416
|
-
def __init__(self, rekordbox_executable):
|
422
|
+
def __init__(self, rekordbox_executable: Union[str, Path]):
|
417
423
|
self.executable = str(rekordbox_executable)
|
418
424
|
self.key = ""
|
419
425
|
|
420
|
-
def on_message(self, message, data):
|
426
|
+
def on_message(self, message: Dict[str, Any], data: Any) -> None:
|
421
427
|
payload = message["payload"]
|
422
428
|
if payload.startswith("sqlite3_key"):
|
423
429
|
self.key = payload.split(": ")[1]
|
424
430
|
|
425
|
-
def run(self):
|
431
|
+
def run(self) -> str:
|
426
432
|
pid = get_rekordbox_pid()
|
427
433
|
if pid:
|
428
434
|
raise RuntimeError(
|
@@ -477,7 +483,7 @@ def write_db6_key_cache(key: str) -> None: # pragma: no cover
|
|
477
483
|
lines.append("dp: " + key)
|
478
484
|
text = "\n".join(lines)
|
479
485
|
|
480
|
-
cache_file =
|
486
|
+
cache_file = get_cache_file()
|
481
487
|
if not cache_file.parent.exists():
|
482
488
|
cache_file.parent.mkdir()
|
483
489
|
|
@@ -485,16 +491,16 @@ def write_db6_key_cache(key: str) -> None: # pragma: no cover
|
|
485
491
|
fh.write(text)
|
486
492
|
# Set the config key to make sure the key is present after calling method
|
487
493
|
if __config__["rekordbox6"]:
|
488
|
-
__config__["rekordbox6"]["dp"] = key
|
494
|
+
__config__["rekordbox6"]["dp"] = key # type: ignore
|
489
495
|
if __config__["rekordbox7"]:
|
490
|
-
__config__["rekordbox7"]["dp"] = key
|
496
|
+
__config__["rekordbox7"]["dp"] = key # type: ignore
|
491
497
|
|
492
498
|
|
493
|
-
def _update_sqlite_key(opts, conf):
|
499
|
+
def _update_sqlite_key(opts: Dict[str, Any], conf: Dict[str, Any]) -> None:
|
494
500
|
cache_version = 0
|
495
501
|
pw, dp = "", ""
|
496
502
|
|
497
|
-
cache_file =
|
503
|
+
cache_file = get_cache_file()
|
498
504
|
|
499
505
|
if cache_file.exists(): # pragma: no cover
|
500
506
|
logger.debug("Found cache file %s", cache_file)
|
@@ -533,7 +539,7 @@ def _update_sqlite_key(opts, conf):
|
|
533
539
|
if not dp:
|
534
540
|
if pw:
|
535
541
|
cipher = blowfish.Cipher(pw.encode())
|
536
|
-
dp = base64.standard_b64decode(opts["dp"])
|
542
|
+
dp = base64.standard_b64decode(opts["dp"]) # type: ignore
|
537
543
|
dp = b"".join(cipher.decrypt_ecb(dp)).decode()
|
538
544
|
logger.debug("Unlocked dp from pw: %s", dp)
|
539
545
|
else:
|
@@ -574,7 +580,9 @@ def _update_sqlite_key(opts, conf):
|
|
574
580
|
conf["dp"] = dp
|
575
581
|
|
576
582
|
|
577
|
-
def _get_rb6_config(
|
583
|
+
def _get_rb6_config(
|
584
|
+
pioneer_prog_dir: Path, pioneer_app_dir: Path, dirname: str = ""
|
585
|
+
) -> Dict[str, Any]:
|
578
586
|
"""Get the program configuration for Rekordbox v6.x.x."""
|
579
587
|
major_version = 6
|
580
588
|
conf = _get_rb_config(pioneer_prog_dir, pioneer_app_dir, major_version, dirname)
|
@@ -592,7 +600,9 @@ def _get_rb6_config(pioneer_prog_dir: Path, pioneer_app_dir: Path, dirname: str
|
|
592
600
|
return conf
|
593
601
|
|
594
602
|
|
595
|
-
def _get_rb7_config(
|
603
|
+
def _get_rb7_config(
|
604
|
+
pioneer_prog_dir: Path, pioneer_app_dir: Path, dirname: str = ""
|
605
|
+
) -> Dict[str, Any]:
|
596
606
|
"""Get the program configuration for Rekordbox v7.x.x."""
|
597
607
|
major_version = 7
|
598
608
|
conf = _get_rb_config(pioneer_prog_dir, pioneer_app_dir, major_version, dirname)
|
@@ -611,7 +621,7 @@ def _get_rb7_config(pioneer_prog_dir: Path, pioneer_app_dir: Path, dirname: str
|
|
611
621
|
|
612
622
|
|
613
623
|
# noinspection PyPackageRequirements,PyUnresolvedReferences
|
614
|
-
def _read_config_file(path: str) ->
|
624
|
+
def _read_config_file(path: Union[str, Path]) -> Dict[str, Any]:
|
615
625
|
path = Path(path)
|
616
626
|
if not path.exists():
|
617
627
|
raise FileNotFoundError(f"No such file or directory: '{path}'")
|
@@ -622,20 +632,20 @@ def _read_config_file(path: str) -> dict:
|
|
622
632
|
|
623
633
|
parser = ConfigParser()
|
624
634
|
parser.read(path)
|
625
|
-
return parser
|
635
|
+
return {k: v for k, v in parser.items() if v is not None}
|
626
636
|
elif ext == ".toml":
|
627
637
|
import toml
|
628
638
|
|
629
|
-
return toml.load(path)
|
639
|
+
return dict(toml.load(path))
|
630
640
|
elif ext in (".yaml", ".yml"):
|
631
641
|
import yaml
|
632
642
|
|
633
643
|
with open(path, "r") as stream:
|
634
|
-
return yaml.safe_load(stream)
|
644
|
+
return dict(yaml.safe_load(stream))
|
635
645
|
return dict()
|
636
646
|
|
637
647
|
|
638
|
-
def read_pyrekordbox_configuration():
|
648
|
+
def read_pyrekordbox_configuration() -> Dict[str, Any]:
|
639
649
|
"""Reads the pyrekordbox configuration.
|
640
650
|
|
641
651
|
So far only the `pioneer-install-dir` and `pioneer-app-dir` fileds in the
|
@@ -673,7 +683,7 @@ def update_config(
|
|
673
683
|
rb5_install_dirname: str = "",
|
674
684
|
rb6_install_dirname: str = "",
|
675
685
|
rb7_install_dirname: str = "",
|
676
|
-
):
|
686
|
+
) -> None:
|
677
687
|
"""Update the pyrekordbox configuration.
|
678
688
|
|
679
689
|
This method scans the system for the Rekordbox installation and application data
|
@@ -754,7 +764,7 @@ def update_config(
|
|
754
764
|
logger.info(e)
|
755
765
|
|
756
766
|
|
757
|
-
def get_config(section: str, key: str = None):
|
767
|
+
def get_config(section: str, key: str = None) -> Any:
|
758
768
|
"""Gets a section or value of the pyrekordbox configuration.
|
759
769
|
|
760
770
|
Parameters
|
pyrekordbox/db6/aux_files.py
CHANGED
@@ -5,10 +5,20 @@
|
|
5
5
|
import xml.etree.cElementTree as xml
|
6
6
|
from datetime import datetime
|
7
7
|
from pathlib import Path
|
8
|
+
from typing import Any, Dict, List, Optional, Union
|
8
9
|
|
9
10
|
from ..config import get_config
|
10
11
|
from ..utils import pretty_xml
|
11
12
|
|
13
|
+
Attribs = Dict[str, Any]
|
14
|
+
|
15
|
+
|
16
|
+
class XmlElementNotInitializedError(Exception):
|
17
|
+
"""Raised when an XML element is not initialized."""
|
18
|
+
|
19
|
+
def __init__(self, name: str) -> None:
|
20
|
+
super().__init__(f"XML element {name} is not initialized!")
|
21
|
+
|
12
22
|
|
13
23
|
class MasterPlaylistXml:
|
14
24
|
"""Rekordbox v6 masterPlaylists6.xml file handler.
|
@@ -26,7 +36,7 @@ class MasterPlaylistXml:
|
|
26
36
|
|
27
37
|
KEYS = ["Id", "ParentId", "Attributes", "Timestamp", "Lib_Type", "CheckType"]
|
28
38
|
|
29
|
-
def __init__(self, path=None, db_dir=None):
|
39
|
+
def __init__(self, path: Union[str, Path] = None, db_dir: Union[str, Path] = None):
|
30
40
|
if path is None:
|
31
41
|
if db_dir is None:
|
32
42
|
db_dir = get_config("rekordbox6", "db_dir")
|
@@ -40,29 +50,33 @@ class MasterPlaylistXml:
|
|
40
50
|
self._changed = False
|
41
51
|
|
42
52
|
@property
|
43
|
-
def version(self):
|
53
|
+
def version(self) -> str:
|
44
54
|
return self.root.attrib["Version"]
|
45
55
|
|
46
56
|
@property
|
47
|
-
def automatic_sync(self):
|
57
|
+
def automatic_sync(self) -> str:
|
48
58
|
return self.root.attrib["AutomaticSync"]
|
49
59
|
|
50
60
|
@property
|
51
|
-
def rekordbox_version(self):
|
61
|
+
def rekordbox_version(self) -> str:
|
62
|
+
if self.product is None:
|
63
|
+
raise XmlElementNotInitializedError("product")
|
52
64
|
return self.product.attrib["Version"]
|
53
65
|
|
54
66
|
@property
|
55
|
-
def modified(self):
|
67
|
+
def modified(self) -> bool:
|
56
68
|
return self._changed
|
57
69
|
|
58
|
-
def get_playlists(self):
|
70
|
+
def get_playlists(self) -> List[Dict[str, Any]]:
|
59
71
|
"""Returns a list of the attributes of all playlist elements."""
|
72
|
+
if self.playlists is None:
|
73
|
+
raise XmlElementNotInitializedError("playlists")
|
60
74
|
items = list()
|
61
75
|
for playlist in self.playlists:
|
62
76
|
items.append(playlist.attrib)
|
63
77
|
return items
|
64
78
|
|
65
|
-
def get(self, playlist_id):
|
79
|
+
def get(self, playlist_id: Union[str, int]) -> Optional[Attribs]:
|
66
80
|
"""Returns element attribs with the PlaylistID used in the `master.db` database.
|
67
81
|
|
68
82
|
Parameters
|
@@ -75,11 +89,13 @@ class MasterPlaylistXml:
|
|
75
89
|
-------
|
76
90
|
playlist : dict
|
77
91
|
"""
|
92
|
+
if self.playlists is None:
|
93
|
+
raise XmlElementNotInitializedError("playlists")
|
78
94
|
hex_id = f"{int(playlist_id):X}"
|
79
95
|
element = self.playlists.find(f'.//NODE[@Id="{hex_id}"]')
|
80
96
|
if element is None:
|
81
97
|
return None
|
82
|
-
attribs = dict(element.attrib)
|
98
|
+
attribs: Attribs = dict(element.attrib)
|
83
99
|
attribs["Attribute"] = int(attribs["Attribute"])
|
84
100
|
attribs["Timestamp"] = datetime.fromtimestamp(int(attribs["Timestamp"]) / 1000)
|
85
101
|
attribs["Lib_Type"] = int(attribs["Lib_Type"])
|
@@ -94,7 +110,7 @@ class MasterPlaylistXml:
|
|
94
110
|
updated_at: datetime,
|
95
111
|
lib_type: int = 0,
|
96
112
|
check_type: int = 0,
|
97
|
-
):
|
113
|
+
) -> xml.Element:
|
98
114
|
"""Adds a new element with the PlaylistID used in the `master.db` database.
|
99
115
|
|
100
116
|
Parameters
|
@@ -119,6 +135,9 @@ class MasterPlaylistXml:
|
|
119
135
|
element : xml.Element
|
120
136
|
The newly created element.
|
121
137
|
"""
|
138
|
+
if self.playlists is None:
|
139
|
+
raise XmlElementNotInitializedError("playlists")
|
140
|
+
|
122
141
|
hex_id = f"{int(playlist_id):X}"
|
123
142
|
parent_id = f"{int(parent_id):X}" if parent_id != "root" else "0"
|
124
143
|
timestamp = int(updated_at.timestamp() * 1000)
|
@@ -136,7 +155,7 @@ class MasterPlaylistXml:
|
|
136
155
|
self._changed = True
|
137
156
|
return element
|
138
157
|
|
139
|
-
def remove(self, playlist_id):
|
158
|
+
def remove(self, playlist_id: Union[str, int]) -> None:
|
140
159
|
"""Removes the element with the PlaylistID used in the `master.db` database.
|
141
160
|
|
142
161
|
Parameters
|
@@ -145,6 +164,9 @@ class MasterPlaylistXml:
|
|
145
164
|
The playlist ID used in the main `master.db` database. This id is converted
|
146
165
|
to hexadecimal format before searching.
|
147
166
|
"""
|
167
|
+
if self.playlists is None:
|
168
|
+
raise XmlElementNotInitializedError("playlists")
|
169
|
+
|
148
170
|
hex_id = f"{int(playlist_id):X}"
|
149
171
|
element = self.playlists.find(f'.//NODE[@Id="{hex_id}"]')
|
150
172
|
if element is None:
|
@@ -160,7 +182,7 @@ class MasterPlaylistXml:
|
|
160
182
|
updated_at: datetime = None,
|
161
183
|
lib_type: int = None,
|
162
184
|
check_type: int = None,
|
163
|
-
):
|
185
|
+
) -> None:
|
164
186
|
"""Updates the element with the PlaylistID used in the `master.db` database.
|
165
187
|
|
166
188
|
Parameters
|
@@ -180,6 +202,9 @@ class MasterPlaylistXml:
|
|
180
202
|
check_type : int, optional
|
181
203
|
The check type. It seems to be always 0.
|
182
204
|
"""
|
205
|
+
if self.playlists is None:
|
206
|
+
raise XmlElementNotInitializedError("playlists")
|
207
|
+
|
183
208
|
hex_id = f"{int(playlist_id):X}"
|
184
209
|
element = self.playlists.find(f'.//NODE[@Id="{hex_id}"]')
|
185
210
|
if element is None:
|
@@ -200,10 +225,11 @@ class MasterPlaylistXml:
|
|
200
225
|
element.attrib.update(attribs)
|
201
226
|
self._changed = True
|
202
227
|
|
203
|
-
def to_string(self, indent=None):
|
204
|
-
|
228
|
+
def to_string(self, indent: str = None) -> str:
|
229
|
+
text: str = pretty_xml(self.root, indent, encoding="utf-8")
|
230
|
+
return text
|
205
231
|
|
206
|
-
def save(self, path=None, indent=None):
|
232
|
+
def save(self, path: Union[str, Path] = None, indent: str = None) -> None:
|
207
233
|
if path is None:
|
208
234
|
path = self.path
|
209
235
|
path = str(path)
|