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 CHANGED
@@ -2,6 +2,7 @@
2
2
  # Author: Dylan Jones
3
3
  # Date: 2022-04-10
4
4
 
5
+ # mypy: disable-error-code="attr-defined"
5
6
  from .anlz import AnlzFile, get_anlz_paths, read_anlz_files, walk_anlz_paths
6
7
  from .config import get_config, show_config, update_config
7
8
  from .db6 import Rekordbox6Database
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 == "download-key":
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__ = ["__version__", "__version_tuple__", "version", "version_tuple"]
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.2'
21
- __version_tuple__ = version_tuple = (0, 4, 2)
31
+ __version__ = version = '0.4.4'
32
+ __version_tuple__ = version_tuple = (0, 4, 4)
33
+
34
+ __commit_id__ = commit_id = None
@@ -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]) -> dict:
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] = "") -> dict:
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
- bank = Int16ub.parse(tag_data[28:30])
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
- # deobfuscate tag_data[18:] using xor with XOR_MASK+len_entries
127
- len_entries = Int16ub.parse(tag_data[16:])
128
- tag_data = bytearray(data[i : i + len(tag_data)])
129
- for x in range(len(tag_data[18:])):
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
- tag_data[x + 18] ^= mask
134
- tag_data = bytes(tag_data)
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
- len_tag = tag.struct.len_tag
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
- tag = structs.AnlzTag.parse(tag_data)
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)