pyrekordbox 0.4.2__py3-none-any.whl → 0.4.4__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 +1 -51
- pyrekordbox/_version.py +16 -3
- pyrekordbox/anlz/__init__.py +6 -6
- pyrekordbox/anlz/file.py +56 -43
- pyrekordbox/anlz/tags.py +108 -70
- pyrekordbox/config.py +18 -356
- pyrekordbox/db6/aux_files.py +40 -14
- pyrekordbox/db6/database.py +384 -236
- pyrekordbox/db6/registry.py +48 -34
- pyrekordbox/db6/smartlist.py +12 -12
- pyrekordbox/db6/tables.py +60 -58
- pyrekordbox/mysettings/__init__.py +3 -2
- pyrekordbox/mysettings/file.py +27 -24
- pyrekordbox/rbxml.py +321 -142
- pyrekordbox/utils.py +27 -6
- {pyrekordbox-0.4.2.dist-info → pyrekordbox-0.4.4.dist-info}/METADATA +13 -38
- pyrekordbox-0.4.4.dist-info/RECORD +25 -0
- {pyrekordbox-0.4.2.dist-info → pyrekordbox-0.4.4.dist-info}/WHEEL +1 -1
- {pyrekordbox-0.4.2.dist-info → pyrekordbox-0.4.4.dist-info}/licenses/LICENSE +1 -1
- pyrekordbox-0.4.2.dist-info/RECORD +0 -25
- {pyrekordbox-0.4.2.dist-info → pyrekordbox-0.4.4.dist-info}/top_level.txt +0 -0
pyrekordbox/__init__.py
CHANGED
pyrekordbox/__main__.py
CHANGED
@@ -3,30 +3,10 @@
|
|
3
3
|
# Date: 2023-08-15
|
4
4
|
|
5
5
|
import os
|
6
|
-
import re
|
7
6
|
import shutil
|
8
7
|
import sys
|
9
|
-
import urllib.request
|
10
8
|
from pathlib import Path
|
11
9
|
|
12
|
-
from pyrekordbox.config import get_cache_file, write_db6_key_cache
|
13
|
-
|
14
|
-
KEY_SOURCES = [
|
15
|
-
{
|
16
|
-
"url": r"https://raw.githubusercontent.com/mganss/CueGen/19878e6eb3f586dee0eb3eb4f2ce3ef18309de9d/CueGen/Generator.cs", # noqa: E501
|
17
|
-
"regex": re.compile(
|
18
|
-
r'((.|\n)*)Config\.UseSqlCipher.*\?.*"(?P<dp>.*)".*:.*null',
|
19
|
-
flags=re.IGNORECASE | re.MULTILINE,
|
20
|
-
),
|
21
|
-
},
|
22
|
-
{
|
23
|
-
"url": r"https://raw.githubusercontent.com/dvcrn/go-rekordbox/8be6191ba198ed7abd4ad6406d177ed7b4f749b5/cmd/getencryptionkey/main.go", # noqa: E501
|
24
|
-
"regex": re.compile(
|
25
|
-
r'((.|\n)*)fmt\.Print\("(?P<dp>.*)"\)', flags=re.IGNORECASE | re.MULTILINE
|
26
|
-
),
|
27
|
-
},
|
28
|
-
]
|
29
|
-
|
30
10
|
|
31
11
|
class WorkingDir:
|
32
12
|
def __init__(self, path):
|
@@ -130,40 +110,12 @@ def install_pysqlcipher(
|
|
130
110
|
print(f"Could not remove temporary directory '{tmpdir}'!")
|
131
111
|
|
132
112
|
|
133
|
-
def download_db6_key():
|
134
|
-
dp = ""
|
135
|
-
for source in KEY_SOURCES:
|
136
|
-
url = source["url"]
|
137
|
-
regex = source["regex"]
|
138
|
-
print(f"Looking for key: {url}")
|
139
|
-
|
140
|
-
res = urllib.request.urlopen(url)
|
141
|
-
data = res.read().decode("utf-8")
|
142
|
-
match = regex.match(data)
|
143
|
-
if match:
|
144
|
-
dp = match.group("dp")
|
145
|
-
break
|
146
|
-
if dp:
|
147
|
-
cache_file = get_cache_file()
|
148
|
-
print(f"Found key, updating cache file {cache_file}")
|
149
|
-
write_db6_key_cache(dp)
|
150
|
-
else:
|
151
|
-
print("No key found in the online sources.")
|
152
|
-
|
153
|
-
|
154
113
|
def main():
|
155
114
|
from argparse import ArgumentParser
|
156
115
|
|
157
116
|
parser = ArgumentParser("pyrekordbox")
|
158
117
|
subparsers = parser.add_subparsers(dest="command")
|
159
118
|
|
160
|
-
# Download Rekordbx 6 database key command
|
161
|
-
subparsers.add_parser(
|
162
|
-
"download-key",
|
163
|
-
help="Download the Rekordbox 6 database key from the internet "
|
164
|
-
"and write it to the cache file.",
|
165
|
-
)
|
166
|
-
|
167
119
|
# Install pysqlcipher3 command (Windows only)
|
168
120
|
install_parser = subparsers.add_parser(
|
169
121
|
"install-sqlcipher",
|
@@ -192,9 +144,7 @@ def main():
|
|
192
144
|
|
193
145
|
# Parse args and handle command
|
194
146
|
args = parser.parse_args(sys.argv[1:])
|
195
|
-
if args.command == "
|
196
|
-
download_db6_key()
|
197
|
-
elif args.command == "install-sqlcipher":
|
147
|
+
if args.command == "install-sqlcipher":
|
198
148
|
install_pysqlcipher(args.tmpdir, args.cryptolib, install=not args.buildonly)
|
199
149
|
|
200
150
|
|
pyrekordbox/_version.py
CHANGED
@@ -1,7 +1,14 @@
|
|
1
1
|
# file generated by setuptools-scm
|
2
2
|
# don't change, don't track in version control
|
3
3
|
|
4
|
-
__all__ = [
|
4
|
+
__all__ = [
|
5
|
+
"__version__",
|
6
|
+
"__version_tuple__",
|
7
|
+
"version",
|
8
|
+
"version_tuple",
|
9
|
+
"__commit_id__",
|
10
|
+
"commit_id",
|
11
|
+
]
|
5
12
|
|
6
13
|
TYPE_CHECKING = False
|
7
14
|
if TYPE_CHECKING:
|
@@ -9,13 +16,19 @@ if TYPE_CHECKING:
|
|
9
16
|
from typing import Union
|
10
17
|
|
11
18
|
VERSION_TUPLE = Tuple[Union[int, str], ...]
|
19
|
+
COMMIT_ID = Union[str, None]
|
12
20
|
else:
|
13
21
|
VERSION_TUPLE = object
|
22
|
+
COMMIT_ID = object
|
14
23
|
|
15
24
|
version: str
|
16
25
|
__version__: str
|
17
26
|
__version_tuple__: VERSION_TUPLE
|
18
27
|
version_tuple: VERSION_TUPLE
|
28
|
+
commit_id: COMMIT_ID
|
29
|
+
__commit_id__: COMMIT_ID
|
19
30
|
|
20
|
-
__version__ = version = '0.4.
|
21
|
-
__version_tuple__ = version_tuple = (0, 4,
|
31
|
+
__version__ = version = '0.4.4'
|
32
|
+
__version_tuple__ = version_tuple = (0, 4, 4)
|
33
|
+
|
34
|
+
__commit_id__ = commit_id = None
|
pyrekordbox/anlz/__init__.py
CHANGED
@@ -4,7 +4,7 @@
|
|
4
4
|
|
5
5
|
import re
|
6
6
|
from pathlib import Path
|
7
|
-
from typing import Union
|
7
|
+
from typing import Dict, Iterator, Tuple, Union
|
8
8
|
|
9
9
|
from . import structs
|
10
10
|
from .file import AnlzFile
|
@@ -42,7 +42,7 @@ def is_anlz_file(path: Union[str, Path]) -> bool:
|
|
42
42
|
return bool(RE_ANLZ.match(path.name))
|
43
43
|
|
44
44
|
|
45
|
-
def get_anlz_paths(root: Union[str, Path]) ->
|
45
|
+
def get_anlz_paths(root: Union[str, Path]) -> Dict[str, Union[Path, None]]:
|
46
46
|
"""Returns the paths of all existing ANLZ files in a directory.
|
47
47
|
|
48
48
|
Parameters
|
@@ -62,14 +62,14 @@ def get_anlz_paths(root: Union[str, Path]) -> dict:
|
|
62
62
|
>>> p["DAT"]
|
63
63
|
directory/ANLZ0000.DAT
|
64
64
|
"""
|
65
|
-
paths = {"DAT": None, "EXT": None, "2EX": None}
|
65
|
+
paths: Dict[str, Union[Path, None]] = {"DAT": None, "EXT": None, "2EX": None}
|
66
66
|
for path in Path(root).iterdir():
|
67
67
|
if RE_ANLZ.match(path.name):
|
68
68
|
paths[path.suffix[1:].upper()] = path
|
69
69
|
return paths
|
70
70
|
|
71
71
|
|
72
|
-
def walk_anlz_dirs(root_dir: Union[str, Path]):
|
72
|
+
def walk_anlz_dirs(root_dir: Union[str, Path]) -> Iterator[Path]:
|
73
73
|
"""Finds all ANLZ directory paths recursively.
|
74
74
|
|
75
75
|
Parameters
|
@@ -88,7 +88,7 @@ def walk_anlz_dirs(root_dir: Union[str, Path]):
|
|
88
88
|
yield path
|
89
89
|
|
90
90
|
|
91
|
-
def walk_anlz_paths(root_dir: Union[str, Path]):
|
91
|
+
def walk_anlz_paths(root_dir: Union[str, Path]) -> Iterator[Tuple[Path, Dict[str, Path]]]:
|
92
92
|
"""Finds all ANLZ directory paths and the containing file paths recursively.
|
93
93
|
|
94
94
|
Parameters
|
@@ -110,7 +110,7 @@ def walk_anlz_paths(root_dir: Union[str, Path]):
|
|
110
110
|
yield anlz_dir, files
|
111
111
|
|
112
112
|
|
113
|
-
def read_anlz_files(root: Union[str, Path] = "") ->
|
113
|
+
def read_anlz_files(root: Union[str, Path] = "") -> Dict[Path, AnlzFile]:
|
114
114
|
"""Open all ANLZ files in the given root directory.
|
115
115
|
|
116
116
|
Parameters
|
pyrekordbox/anlz/file.py
CHANGED
@@ -5,12 +5,12 @@
|
|
5
5
|
import logging
|
6
6
|
from collections import abc
|
7
7
|
from pathlib import Path
|
8
|
-
from typing import Union
|
8
|
+
from typing import Any, Iterator, List, Union
|
9
9
|
|
10
|
-
from construct import Int16ub
|
10
|
+
from construct import Int16ub, Struct
|
11
11
|
|
12
12
|
from . import structs
|
13
|
-
from .tags import TAGS
|
13
|
+
from .tags import TAGS, AbstractAnlzTag, StructNotInitializedError
|
14
14
|
|
15
15
|
logger = logging.getLogger(__name__)
|
16
16
|
|
@@ -18,35 +18,35 @@ XOR_MASK = bytearray.fromhex("CB E1 EE FA E5 EE AD EE E9 D2 E9 EB E1 E9 F3 E8 E9
|
|
18
18
|
|
19
19
|
|
20
20
|
class BuildFileLengthError(Exception):
|
21
|
-
def __init__(self, struct, len_data):
|
21
|
+
def __init__(self, struct: Struct, len_data: int) -> None:
|
22
22
|
super().__init__(
|
23
23
|
f"`len_file` ({struct.len_file}) of '{struct.type}' does not "
|
24
24
|
f"match the data-length ({len_data})!"
|
25
25
|
)
|
26
26
|
|
27
27
|
|
28
|
-
class AnlzFile(abc.Mapping):
|
28
|
+
class AnlzFile(abc.Mapping): # type: ignore[type-arg]
|
29
29
|
"""Rekordbox `ANLZnnnn.xxx` binary file handler."""
|
30
30
|
|
31
|
-
def __init__(self):
|
32
|
-
self._path = ""
|
33
|
-
self.file_header = None
|
34
|
-
self.tags = list()
|
31
|
+
def __init__(self) -> None:
|
32
|
+
self._path: str = ""
|
33
|
+
self.file_header: Union[Struct, None] = None
|
34
|
+
self.tags: List[AbstractAnlzTag] = list()
|
35
35
|
|
36
36
|
@property
|
37
|
-
def num_tags(self):
|
37
|
+
def num_tags(self) -> int:
|
38
38
|
return len(self.tags)
|
39
39
|
|
40
40
|
@property
|
41
|
-
def tag_types(self):
|
41
|
+
def tag_types(self) -> List[str]:
|
42
42
|
return [tag.type for tag in self.tags]
|
43
43
|
|
44
44
|
@property
|
45
|
-
def path(self):
|
45
|
+
def path(self) -> str:
|
46
46
|
return self._path
|
47
47
|
|
48
48
|
@classmethod
|
49
|
-
def parse(cls, data: bytes):
|
49
|
+
def parse(cls, data: bytes) -> "AnlzFile":
|
50
50
|
"""Parses the in-memory data of a Rekordbox analysis binary file.
|
51
51
|
|
52
52
|
Parameters
|
@@ -64,7 +64,7 @@ class AnlzFile(abc.Mapping):
|
|
64
64
|
return self
|
65
65
|
|
66
66
|
@classmethod
|
67
|
-
def parse_file(cls, path: Union[str, Path]):
|
67
|
+
def parse_file(cls, path: Union[str, Path]) -> "AnlzFile":
|
68
68
|
"""Reads and parses a Rekordbox analysis binary file.
|
69
69
|
|
70
70
|
Parameters
|
@@ -92,10 +92,10 @@ class AnlzFile(abc.Mapping):
|
|
92
92
|
data = fh.read()
|
93
93
|
|
94
94
|
self = cls.parse(data)
|
95
|
-
self._path = path
|
95
|
+
self._path = str(path)
|
96
96
|
return self
|
97
97
|
|
98
|
-
def _parse(self, data: bytes):
|
98
|
+
def _parse(self, data: bytes) -> None:
|
99
99
|
file_header = structs.AnlzFileHeader.parse(data)
|
100
100
|
tag_type = file_header.type
|
101
101
|
assert tag_type == "PMAI"
|
@@ -108,37 +108,45 @@ class AnlzFile(abc.Mapping):
|
|
108
108
|
# Get the four byte struct type
|
109
109
|
tag_type = tag_data[:4].decode("ascii")
|
110
110
|
|
111
|
+
# Get tag length from generic tag header
|
112
|
+
tag_struct = structs.AnlzTag.parse(tag_data)
|
113
|
+
len_tag = tag_struct.len_tag
|
114
|
+
|
111
115
|
if tag_type == "PSSI":
|
112
116
|
# The version that rekordbox 6 *exports* is garbled with an XOR mask.
|
117
|
+
# Determine if the tag is garbled by checking the initial (masked) mood value.
|
113
118
|
# All bytes after byte 17 (len_e) are XOR-masked with a pattern that is
|
114
119
|
# generated by adding the value of len_e to each byte of XOR_MASK
|
115
120
|
|
116
121
|
# Check if the file is garbled (only on exported files)
|
117
122
|
# For this we check the validity of mood and bank
|
118
123
|
# Mood: High=1, Mid=2, Low=3
|
119
|
-
# Bank: 0-8
|
120
124
|
mood = Int16ub.parse(tag_data[18:20])
|
121
|
-
|
122
|
-
if 1 <= mood <= 3 and 0 <= bank <= 8:
|
125
|
+
if 1 <= mood <= 3:
|
123
126
|
logger.debug("PSSI is not garbled!")
|
124
127
|
else:
|
125
|
-
logger.debug("PSSI is garbled!")
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
128
|
+
logger.debug("PSSI is garbled! (raw_mood=%s)", mood)
|
129
|
+
len_entries = Int16ub.parse(tag_data[16:18])
|
130
|
+
|
131
|
+
# Copy only this tag's data so we don't mutate the remainder of file slice
|
132
|
+
mutable_tag_data = bytearray(tag_data[:len_tag])
|
133
|
+
|
134
|
+
for x in range(len(mutable_tag_data) - 18):
|
130
135
|
mask = XOR_MASK[x % len(XOR_MASK)] + len_entries
|
131
136
|
if mask > 255:
|
132
137
|
mask -= 256
|
133
|
-
|
134
|
-
|
138
|
+
mutable_tag_data[18 + x] ^= mask
|
139
|
+
|
140
|
+
tag_data = bytes(mutable_tag_data)
|
135
141
|
|
136
142
|
try:
|
137
143
|
# Parse the struct
|
138
144
|
tag = TAGS[tag_type](tag_data)
|
145
|
+
if tag.struct is None:
|
146
|
+
raise StructNotInitializedError()
|
139
147
|
tags.append(tag)
|
140
148
|
len_header = tag.struct.len_header
|
141
|
-
|
149
|
+
|
142
150
|
logger.debug(
|
143
151
|
"Parsed struct '%s' (len_header=%s, len_tag=%s)",
|
144
152
|
tag_type,
|
@@ -147,28 +155,33 @@ class AnlzFile(abc.Mapping):
|
|
147
155
|
)
|
148
156
|
except KeyError:
|
149
157
|
logger.warning("Tag '%s' not supported!", tag_type)
|
150
|
-
|
151
|
-
len_tag = tag.len_tag
|
158
|
+
|
152
159
|
i += len_tag
|
153
160
|
|
154
161
|
self.file_header = file_header
|
155
162
|
self.tags = tags
|
156
163
|
|
157
|
-
def update_len(self):
|
164
|
+
def update_len(self) -> None:
|
158
165
|
# Update struct lengths
|
166
|
+
if self.file_header is None:
|
167
|
+
raise StructNotInitializedError()
|
159
168
|
tags_len = 0
|
160
169
|
for tag in self.tags:
|
170
|
+
if tag.struct is None:
|
171
|
+
raise StructNotInitializedError()
|
161
172
|
tag.update_len()
|
162
173
|
tags_len += tag.struct.len_tag
|
163
174
|
# Update file length
|
164
175
|
len_file = self.file_header.len_header + tags_len
|
165
176
|
self.file_header.len_file = len_file
|
166
177
|
|
167
|
-
def build(self):
|
178
|
+
def build(self) -> bytes:
|
179
|
+
if self.file_header is None:
|
180
|
+
raise StructNotInitializedError()
|
168
181
|
self.update_len()
|
169
182
|
header_data = structs.AnlzFileHeader.build(self.file_header)
|
170
183
|
section_data = b"".join(tag.build() for tag in self.tags)
|
171
|
-
data = header_data + section_data
|
184
|
+
data: bytes = header_data + section_data
|
172
185
|
# Check `len_file`
|
173
186
|
len_file = self.file_header.len_file
|
174
187
|
len_data = len(data)
|
@@ -177,38 +190,38 @@ class AnlzFile(abc.Mapping):
|
|
177
190
|
|
178
191
|
return data
|
179
192
|
|
180
|
-
def save(self, path=""):
|
193
|
+
def save(self, path: Union[str, Path] = "") -> None:
|
181
194
|
path = path or self._path
|
182
195
|
|
183
196
|
data = self.build()
|
184
197
|
with open(path, "wb") as fh:
|
185
198
|
fh.write(data)
|
186
199
|
|
187
|
-
def get_tag(self, key):
|
200
|
+
def get_tag(self, key: str) -> AbstractAnlzTag:
|
188
201
|
return self.__getitem__(key)[0]
|
189
202
|
|
190
|
-
def getall_tags(self, key):
|
203
|
+
def getall_tags(self, key: str) -> List[AbstractAnlzTag]:
|
191
204
|
return self.__getitem__(key)
|
192
205
|
|
193
|
-
def get(self, key):
|
206
|
+
def get(self, key: str) -> Any: # type: ignore[override]
|
194
207
|
return self.__getitem__(key)[0].get()
|
195
208
|
|
196
|
-
def getall(self, key):
|
209
|
+
def getall(self, key: str) -> List[Any]:
|
197
210
|
return [tag.get() for tag in self.__getitem__(key)]
|
198
211
|
|
199
|
-
def __len__(self):
|
212
|
+
def __len__(self) -> int:
|
200
213
|
return len(self.keys())
|
201
214
|
|
202
|
-
def __iter__(self):
|
215
|
+
def __iter__(self) -> Iterator[str]:
|
203
216
|
return iter(set(tag.type for tag in self.tags))
|
204
217
|
|
205
|
-
def __getitem__(self, item):
|
218
|
+
def __getitem__(self, item: str) -> List[AbstractAnlzTag]:
|
206
219
|
if item.isupper() and len(item) == 4:
|
207
220
|
return [tag for tag in self.tags if tag.type == item]
|
208
221
|
else:
|
209
222
|
return [tag for tag in self.tags if tag.name == item]
|
210
223
|
|
211
|
-
def __contains__(self, item):
|
224
|
+
def __contains__(self, item: str) -> bool: # type: ignore[override]
|
212
225
|
if item.isupper() and len(item) == 4:
|
213
226
|
for tag in self.tags:
|
214
227
|
if item == tag.type:
|
@@ -219,9 +232,9 @@ class AnlzFile(abc.Mapping):
|
|
219
232
|
return True
|
220
233
|
return False
|
221
234
|
|
222
|
-
def __repr__(self):
|
235
|
+
def __repr__(self) -> str:
|
223
236
|
return f"{self.__class__.__name__}({self.tag_types})"
|
224
237
|
|
225
|
-
def set_path(self, path):
|
238
|
+
def set_path(self, path: Union[Path, str]) -> None:
|
226
239
|
tag = self.get_tag("PPTH")
|
227
240
|
tag.set(path)
|