pyrekordbox 0.4.2__py3-none-any.whl → 0.4.3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pyrekordbox/__init__.py +1 -0
- pyrekordbox/_version.py +2 -2
- pyrekordbox/anlz/__init__.py +6 -6
- pyrekordbox/anlz/file.py +40 -32
- pyrekordbox/anlz/tags.py +108 -70
- pyrekordbox/config.py +30 -24
- pyrekordbox/db6/aux_files.py +40 -14
- pyrekordbox/db6/database.py +379 -217
- pyrekordbox/db6/registry.py +48 -34
- pyrekordbox/db6/smartlist.py +12 -12
- pyrekordbox/db6/tables.py +51 -52
- pyrekordbox/mysettings/__init__.py +3 -2
- pyrekordbox/mysettings/file.py +27 -24
- pyrekordbox/rbxml.py +321 -142
- pyrekordbox/utils.py +7 -6
- {pyrekordbox-0.4.2.dist-info → pyrekordbox-0.4.3.dist-info}/METADATA +1 -1
- pyrekordbox-0.4.3.dist-info/RECORD +25 -0
- {pyrekordbox-0.4.2.dist-info → pyrekordbox-0.4.3.dist-info}/WHEEL +1 -1
- pyrekordbox-0.4.2.dist-info/RECORD +0 -25
- {pyrekordbox-0.4.2.dist-info → pyrekordbox-0.4.3.dist-info}/licenses/LICENSE +0 -0
- {pyrekordbox-0.4.2.dist-info → pyrekordbox-0.4.3.dist-info}/top_level.txt +0 -0
pyrekordbox/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
|
@@ -151,7 +151,7 @@ def get_pioneer_app_dir(path: Union[str, Path] = None) -> Path: # pragma: no co
|
|
151
151
|
return path
|
152
152
|
|
153
153
|
|
154
|
-
def _convert_type(s):
|
154
|
+
def _convert_type(s: str) -> Union[str, int, float, List[int], List[float]]:
|
155
155
|
# Try to parse as int, float, list of int, list of float
|
156
156
|
types_ = int, float
|
157
157
|
for type_ in types_:
|
@@ -167,7 +167,7 @@ def _convert_type(s):
|
|
167
167
|
return s
|
168
168
|
|
169
169
|
|
170
|
-
def read_rekordbox_settings(rekordbox_app_dir: Union[str, Path]) ->
|
170
|
+
def read_rekordbox_settings(rekordbox_app_dir: Union[str, Path]) -> Dict[str, Any]:
|
171
171
|
"""Finds and parses the 'rekordbox3.settings' file in the Rekordbox 5 or 6 app-dir.
|
172
172
|
|
173
173
|
The settings file usually is called 'rekordbox3.settings' and is
|
@@ -197,12 +197,12 @@ def read_rekordbox_settings(rekordbox_app_dir: Union[str, Path]) -> dict:
|
|
197
197
|
val = _convert_type(element.attrib["val"])
|
198
198
|
except KeyError:
|
199
199
|
device_setup = element.find("DEVICESETUP")
|
200
|
-
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
|
201
201
|
settings[name] = val
|
202
202
|
return settings
|
203
203
|
|
204
204
|
|
205
|
-
def read_rekordbox6_options(pioneer_app_dir: Union[str, Path]) ->
|
205
|
+
def read_rekordbox6_options(pioneer_app_dir: Union[str, Path]) -> Dict[str, Any]:
|
206
206
|
"""Finds and parses the Rekordbox 6 `options.json` file with additional settings.
|
207
207
|
|
208
208
|
The options file contains additional settings used by Rekordbox 6, for example the
|
@@ -271,7 +271,7 @@ def read_rekordbox6_asar(rb6_install_dir: Union[str, Path]) -> str:
|
|
271
271
|
return data
|
272
272
|
|
273
273
|
|
274
|
-
def _extract_version(name, major_version):
|
274
|
+
def _extract_version(name: str, major_version: int) -> str:
|
275
275
|
name = name.replace(".app", "") # Needed for MacOS
|
276
276
|
ver_str = name.replace("rekordbox", "").strip()
|
277
277
|
if not ver_str:
|
@@ -284,7 +284,7 @@ def _get_rb_config(
|
|
284
284
|
pioneer_app_dir: Path,
|
285
285
|
major_version: int,
|
286
286
|
application_dirname: str = "",
|
287
|
-
) ->
|
287
|
+
) -> Dict[str, Any]:
|
288
288
|
"""Get the program configuration for a given Rekordbox major version.
|
289
289
|
|
290
290
|
Parameters
|
@@ -378,7 +378,9 @@ def _get_rb_config(
|
|
378
378
|
return conf
|
379
379
|
|
380
380
|
|
381
|
-
def _get_rb5_config(
|
381
|
+
def _get_rb5_config(
|
382
|
+
pioneer_prog_dir: Path, pioneer_app_dir: Path, dirname: str = ""
|
383
|
+
) -> Dict[str, Any]:
|
382
384
|
"""Get the program configuration for Rekordbox v5.x.x."""
|
383
385
|
major_version = 5
|
384
386
|
conf = _get_rb_config(pioneer_prog_dir, pioneer_app_dir, major_version, dirname)
|
@@ -417,16 +419,16 @@ class KeyExtractor:
|
|
417
419
|
""")
|
418
420
|
# fmt: on
|
419
421
|
|
420
|
-
def __init__(self, rekordbox_executable):
|
422
|
+
def __init__(self, rekordbox_executable: Union[str, Path]):
|
421
423
|
self.executable = str(rekordbox_executable)
|
422
424
|
self.key = ""
|
423
425
|
|
424
|
-
def on_message(self, message, data):
|
426
|
+
def on_message(self, message: Dict[str, Any], data: Any) -> None:
|
425
427
|
payload = message["payload"]
|
426
428
|
if payload.startswith("sqlite3_key"):
|
427
429
|
self.key = payload.split(": ")[1]
|
428
430
|
|
429
|
-
def run(self):
|
431
|
+
def run(self) -> str:
|
430
432
|
pid = get_rekordbox_pid()
|
431
433
|
if pid:
|
432
434
|
raise RuntimeError(
|
@@ -489,12 +491,12 @@ def write_db6_key_cache(key: str) -> None: # pragma: no cover
|
|
489
491
|
fh.write(text)
|
490
492
|
# Set the config key to make sure the key is present after calling method
|
491
493
|
if __config__["rekordbox6"]:
|
492
|
-
__config__["rekordbox6"]["dp"] = key
|
494
|
+
__config__["rekordbox6"]["dp"] = key # type: ignore
|
493
495
|
if __config__["rekordbox7"]:
|
494
|
-
__config__["rekordbox7"]["dp"] = key
|
496
|
+
__config__["rekordbox7"]["dp"] = key # type: ignore
|
495
497
|
|
496
498
|
|
497
|
-
def _update_sqlite_key(opts, conf):
|
499
|
+
def _update_sqlite_key(opts: Dict[str, Any], conf: Dict[str, Any]) -> None:
|
498
500
|
cache_version = 0
|
499
501
|
pw, dp = "", ""
|
500
502
|
|
@@ -537,7 +539,7 @@ def _update_sqlite_key(opts, conf):
|
|
537
539
|
if not dp:
|
538
540
|
if pw:
|
539
541
|
cipher = blowfish.Cipher(pw.encode())
|
540
|
-
dp = base64.standard_b64decode(opts["dp"])
|
542
|
+
dp = base64.standard_b64decode(opts["dp"]) # type: ignore
|
541
543
|
dp = b"".join(cipher.decrypt_ecb(dp)).decode()
|
542
544
|
logger.debug("Unlocked dp from pw: %s", dp)
|
543
545
|
else:
|
@@ -578,7 +580,9 @@ def _update_sqlite_key(opts, conf):
|
|
578
580
|
conf["dp"] = dp
|
579
581
|
|
580
582
|
|
581
|
-
def _get_rb6_config(
|
583
|
+
def _get_rb6_config(
|
584
|
+
pioneer_prog_dir: Path, pioneer_app_dir: Path, dirname: str = ""
|
585
|
+
) -> Dict[str, Any]:
|
582
586
|
"""Get the program configuration for Rekordbox v6.x.x."""
|
583
587
|
major_version = 6
|
584
588
|
conf = _get_rb_config(pioneer_prog_dir, pioneer_app_dir, major_version, dirname)
|
@@ -596,7 +600,9 @@ def _get_rb6_config(pioneer_prog_dir: Path, pioneer_app_dir: Path, dirname: str
|
|
596
600
|
return conf
|
597
601
|
|
598
602
|
|
599
|
-
def _get_rb7_config(
|
603
|
+
def _get_rb7_config(
|
604
|
+
pioneer_prog_dir: Path, pioneer_app_dir: Path, dirname: str = ""
|
605
|
+
) -> Dict[str, Any]:
|
600
606
|
"""Get the program configuration for Rekordbox v7.x.x."""
|
601
607
|
major_version = 7
|
602
608
|
conf = _get_rb_config(pioneer_prog_dir, pioneer_app_dir, major_version, dirname)
|
@@ -615,7 +621,7 @@ def _get_rb7_config(pioneer_prog_dir: Path, pioneer_app_dir: Path, dirname: str
|
|
615
621
|
|
616
622
|
|
617
623
|
# noinspection PyPackageRequirements,PyUnresolvedReferences
|
618
|
-
def _read_config_file(path: str) ->
|
624
|
+
def _read_config_file(path: Union[str, Path]) -> Dict[str, Any]:
|
619
625
|
path = Path(path)
|
620
626
|
if not path.exists():
|
621
627
|
raise FileNotFoundError(f"No such file or directory: '{path}'")
|
@@ -626,20 +632,20 @@ def _read_config_file(path: str) -> dict:
|
|
626
632
|
|
627
633
|
parser = ConfigParser()
|
628
634
|
parser.read(path)
|
629
|
-
return parser
|
635
|
+
return {k: v for k, v in parser.items() if v is not None}
|
630
636
|
elif ext == ".toml":
|
631
637
|
import toml
|
632
638
|
|
633
|
-
return toml.load(path)
|
639
|
+
return dict(toml.load(path))
|
634
640
|
elif ext in (".yaml", ".yml"):
|
635
641
|
import yaml
|
636
642
|
|
637
643
|
with open(path, "r") as stream:
|
638
|
-
return yaml.safe_load(stream)
|
644
|
+
return dict(yaml.safe_load(stream))
|
639
645
|
return dict()
|
640
646
|
|
641
647
|
|
642
|
-
def read_pyrekordbox_configuration():
|
648
|
+
def read_pyrekordbox_configuration() -> Dict[str, Any]:
|
643
649
|
"""Reads the pyrekordbox configuration.
|
644
650
|
|
645
651
|
So far only the `pioneer-install-dir` and `pioneer-app-dir` fileds in the
|
@@ -677,7 +683,7 @@ def update_config(
|
|
677
683
|
rb5_install_dirname: str = "",
|
678
684
|
rb6_install_dirname: str = "",
|
679
685
|
rb7_install_dirname: str = "",
|
680
|
-
):
|
686
|
+
) -> None:
|
681
687
|
"""Update the pyrekordbox configuration.
|
682
688
|
|
683
689
|
This method scans the system for the Rekordbox installation and application data
|
@@ -758,7 +764,7 @@ def update_config(
|
|
758
764
|
logger.info(e)
|
759
765
|
|
760
766
|
|
761
|
-
def get_config(section: str, key: str = None):
|
767
|
+
def get_config(section: str, key: str = None) -> Any:
|
762
768
|
"""Gets a section or value of the pyrekordbox configuration.
|
763
769
|
|
764
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)
|