pyrekordbox 0.2.0__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.
- docs/Makefile +20 -0
- docs/make.bat +35 -0
- docs/source/_static/images/anlz_beat.svg +53 -0
- docs/source/_static/images/anlz_file.svg +204 -0
- docs/source/_static/images/anlz_pco2.svg +138 -0
- docs/source/_static/images/anlz_pcob.svg +148 -0
- docs/source/_static/images/anlz_pcp2.svg +398 -0
- docs/source/_static/images/anlz_pcpt.svg +263 -0
- docs/source/_static/images/anlz_ppth.svg +123 -0
- docs/source/_static/images/anlz_pqt2.svg +324 -0
- docs/source/_static/images/anlz_pqt2_2.svg +253 -0
- docs/source/_static/images/anlz_pqtz.svg +140 -0
- docs/source/_static/images/anlz_pssi.svg +192 -0
- docs/source/_static/images/anlz_pssi_entry.svg +191 -0
- docs/source/_static/images/anlz_pvbr.svg +125 -0
- docs/source/_static/images/anlz_pwav.svg +130 -0
- docs/source/_static/images/anlz_pwv3.svg +139 -0
- docs/source/_static/images/anlz_pwv4.svg +139 -0
- docs/source/_static/images/anlz_pwv5.svg +139 -0
- docs/source/_static/images/anlz_pwv5_entry.svg +100 -0
- docs/source/_static/images/anlz_pwv6.svg +130 -0
- docs/source/_static/images/anlz_pwv7.svg +139 -0
- docs/source/_static/images/anlz_pwvc.svg +125 -0
- docs/source/_static/images/anlz_tag.svg +110 -0
- docs/source/_static/logos/dark/logo_primary.svg +75 -0
- docs/source/_static/logos/light/logo_primary.svg +75 -0
- docs/source/_static/logos/mid/logo_primary.svg +75 -0
- docs/source/_templates/apidoc/module.rst_t +8 -0
- docs/source/_templates/apidoc/package.rst_t +57 -0
- docs/source/_templates/apidoc/toc.rst_t +7 -0
- docs/source/_templates/autosummary/class.rst +32 -0
- docs/source/_templates/autosummary/module.rst +55 -0
- docs/source/api.md +18 -0
- docs/source/conf.py +178 -0
- docs/source/development/changes.md +3 -0
- docs/source/development/contributing.md +3 -0
- docs/source/formats/anlz.md +634 -0
- docs/source/formats/db6.md +1233 -0
- docs/source/formats/mysetting.md +392 -0
- docs/source/formats/xml.md +376 -0
- docs/source/index.md +105 -0
- docs/source/installation.md +3 -0
- docs/source/quickstart.md +185 -0
- docs/source/requirements.txt +7 -0
- docs/source/tutorial/anlz.md +7 -0
- docs/source/tutorial/configuration.md +66 -0
- docs/source/tutorial/db6.md +179 -0
- docs/source/tutorial/index.md +20 -0
- docs/source/tutorial/mysetting.md +124 -0
- docs/source/tutorial/xml.md +140 -0
- pyrekordbox/__init__.py +1 -1
- pyrekordbox/__main__.py +16 -37
- pyrekordbox/_version.py +2 -2
- pyrekordbox/anlz/file.py +39 -0
- pyrekordbox/anlz/structs.py +3 -5
- pyrekordbox/config.py +71 -27
- pyrekordbox/db6/database.py +290 -61
- pyrekordbox/db6/registry.py +24 -0
- pyrekordbox/db6/tables.py +501 -340
- pyrekordbox/mysettings/file.py +0 -25
- pyrekordbox/utils.py +1 -1
- {pyrekordbox-0.2.0.dist-info → pyrekordbox-0.2.2.dist-info}/METADATA +42 -20
- pyrekordbox-0.2.2.dist-info/RECORD +80 -0
- {pyrekordbox-0.2.0.dist-info → pyrekordbox-0.2.2.dist-info}/top_level.txt +1 -0
- tests/test_config.py +175 -0
- tests/test_db6.py +95 -0
- pyrekordbox-0.2.0.dist-info/RECORD +0 -29
- {pyrekordbox-0.2.0.dist-info → pyrekordbox-0.2.2.dist-info}/LICENSE +0 -0
- {pyrekordbox-0.2.0.dist-info → pyrekordbox-0.2.2.dist-info}/WHEEL +0 -0
pyrekordbox/anlz/file.py
CHANGED
@@ -8,9 +8,12 @@ from pathlib import Path
|
|
8
8
|
from typing import Union
|
9
9
|
from .tags import TAGS
|
10
10
|
from . import structs
|
11
|
+
from construct import Int16ub
|
11
12
|
|
12
13
|
logger = logging.getLogger(__name__)
|
13
14
|
|
15
|
+
XOR_MASK = bytearray.fromhex("CB E1 EE FA E5 EE AD EE E9 D2 E9 EB E1 E9 F3 E8 E9 F4 E1")
|
16
|
+
|
14
17
|
|
15
18
|
class BuildFileLengthError(Exception):
|
16
19
|
def __init__(self, struct, len_data):
|
@@ -103,6 +106,31 @@ class AnlzFile(abc.Mapping):
|
|
103
106
|
# Get the four byte struct type
|
104
107
|
tag_type = tag_data[:4].decode("ascii")
|
105
108
|
|
109
|
+
if tag_type == "PSSI":
|
110
|
+
# The version that rekordbox 6 *exports* is garbled with an XOR mask.
|
111
|
+
# All bytes after byte 17 (len_e) are XOR-masked with a pattern that is
|
112
|
+
# generated by adding the value of len_e to each byte of XOR_MASK
|
113
|
+
|
114
|
+
# Check if the file is garbled (only on exported files)
|
115
|
+
# For this we check the validity of mood and bank
|
116
|
+
# Mood: High=1, Mid=2, Low=3
|
117
|
+
# Bank: 1-8
|
118
|
+
mood = Int16ub.parse(tag_data[18:20])
|
119
|
+
bank = Int16ub.parse(tag_data[28:30])
|
120
|
+
if 1 <= mood <= 3 and 1 <= bank <= 8:
|
121
|
+
logger.debug("PSSI is not garbled!")
|
122
|
+
else:
|
123
|
+
logger.debug("PSSI is garbled!")
|
124
|
+
# deobfuscate tag_data[18:] using xor with XOR_MASK+len_entries
|
125
|
+
len_entries = Int16ub.parse(tag_data[16:])
|
126
|
+
tag_data = bytearray(data[i : i + len(tag_data)])
|
127
|
+
for x in range(len(tag_data[18:])):
|
128
|
+
mask = XOR_MASK[x % len(XOR_MASK)] + len_entries
|
129
|
+
if mask > 255:
|
130
|
+
mask -= 256
|
131
|
+
tag_data[x + 18] ^= mask
|
132
|
+
tag_data = bytes(tag_data)
|
133
|
+
|
106
134
|
try:
|
107
135
|
# Parse the struct
|
108
136
|
tag = TAGS[tag_type](tag_data)
|
@@ -178,6 +206,17 @@ class AnlzFile(abc.Mapping):
|
|
178
206
|
else:
|
179
207
|
return [tag for tag in self.tags if tag.name == item]
|
180
208
|
|
209
|
+
def __contains__(self, item):
|
210
|
+
if item.isupper() and len(item) == 4:
|
211
|
+
for tag in self.tags:
|
212
|
+
if item == tag.type:
|
213
|
+
return True
|
214
|
+
else:
|
215
|
+
for tag in self.tags:
|
216
|
+
if item == tag.name:
|
217
|
+
return True
|
218
|
+
return False
|
219
|
+
|
181
220
|
def __repr__(self):
|
182
221
|
return f"{self.__class__.__name__}({self.tag_types})"
|
183
222
|
|
pyrekordbox/anlz/structs.py
CHANGED
@@ -195,8 +195,6 @@ PWV5 = Struct(
|
|
195
195
|
|
196
196
|
# -- Song Structure Tag (PSSI) ---------------------------------------------------------
|
197
197
|
|
198
|
-
# FixMe: Implement reverse XOR mask to descramble values
|
199
|
-
|
200
198
|
SongStructureEntry = Struct(
|
201
199
|
"index" / Int16ub,
|
202
200
|
"beat" / Int16ub,
|
@@ -221,11 +219,11 @@ SongStructureEntry = Struct(
|
|
221
219
|
PSSI = Struct(
|
222
220
|
"len_entry_bytes" / Const(24, Int32ub),
|
223
221
|
"len_entries" / Int16ub,
|
224
|
-
"mood" /
|
222
|
+
"mood" / Int16ub,
|
225
223
|
"u1" / Bytes(6),
|
226
|
-
"end_beat" /
|
224
|
+
"end_beat" / Int16ub,
|
227
225
|
"u2" / Bytes(2),
|
228
|
-
"bank" /
|
226
|
+
"bank" / Int8ub,
|
229
227
|
"u3" / Bytes(1),
|
230
228
|
"entries" / Array(this.len_entries, SongStructureEntry),
|
231
229
|
)
|
pyrekordbox/config.py
CHANGED
@@ -15,6 +15,7 @@ import logging
|
|
15
15
|
import base64
|
16
16
|
import blowfish
|
17
17
|
import json
|
18
|
+
import packaging.version
|
18
19
|
import xml.etree.cElementTree as xml
|
19
20
|
from pathlib import Path
|
20
21
|
from typing import Union
|
@@ -247,6 +248,7 @@ def _get_rb_config(
|
|
247
248
|
pioneer_install_dir: Path,
|
248
249
|
pioneer_app_dir: Path,
|
249
250
|
major_version: int,
|
251
|
+
application_dirname: str = "",
|
250
252
|
) -> dict:
|
251
253
|
"""Get the program configuration for a given Rekordbox major version.
|
252
254
|
|
@@ -258,33 +260,55 @@ def _get_rb_config(
|
|
258
260
|
The path of the Pioneer application data directory.
|
259
261
|
major_version : int
|
260
262
|
The major version of Rekordbox.
|
263
|
+
application_dirname : str, optional
|
264
|
+
The name of the Rekordbox application directory. If not given, the latest
|
265
|
+
Rekordbox installation directory for major version `major_version` is used.
|
261
266
|
|
262
267
|
Returns
|
263
268
|
-------
|
264
269
|
config : dict
|
265
270
|
The program configuration.
|
266
271
|
"""
|
267
|
-
|
268
|
-
|
269
|
-
|
270
|
-
|
271
|
-
|
272
|
-
|
273
|
-
|
274
|
-
|
275
|
-
|
276
|
-
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
|
284
|
-
|
285
|
-
|
286
|
-
|
287
|
-
|
272
|
+
if application_dirname:
|
273
|
+
# Applitcation dirname is given, only extract version from it
|
274
|
+
# `major_version` is compared to the version string
|
275
|
+
rb_version = application_dirname.replace("rekordbox", "").strip()
|
276
|
+
rb_version = rb_version.replace(".app", "")
|
277
|
+
if not rb_version.startswith(str(major_version)):
|
278
|
+
raise ValueError(
|
279
|
+
f"Major version is {major_version}, but the supplied application "
|
280
|
+
f"dirname is '{application_dirname}'"
|
281
|
+
)
|
282
|
+
rb_prog_dir = pioneer_install_dir / application_dirname
|
283
|
+
if not rb_prog_dir.exists():
|
284
|
+
raise InvalidApplicationDirname(
|
285
|
+
f"The supplied application dirname '{application_dirname}' does not "
|
286
|
+
f"exist in '{pioneer_install_dir}'"
|
287
|
+
)
|
288
|
+
else:
|
289
|
+
# Get latest Rekordbox installation directory for major release `major_version`
|
290
|
+
|
291
|
+
# Find all 'V.x.x' version strings in dir names
|
292
|
+
versions = list()
|
293
|
+
for p in pioneer_install_dir.iterdir():
|
294
|
+
name = p.name
|
295
|
+
if name.startswith("rekordbox"):
|
296
|
+
ver_str = name.replace("rekordbox", "").strip()
|
297
|
+
ver_str = ver_str.replace(".app", "")
|
298
|
+
if ver_str.startswith(str(major_version)):
|
299
|
+
v = packaging.version.parse(ver_str)
|
300
|
+
versions.append(v)
|
301
|
+
# Get latest 'V.x.x' version string and assure there is one
|
302
|
+
versions.sort() # key=lambda s: list(map(int, s.split("."))))
|
303
|
+
try:
|
304
|
+
rb_version = str(versions[-1])
|
305
|
+
except IndexError:
|
306
|
+
raise FileNotFoundError(
|
307
|
+
f"No Rekordbox {major_version} folder found in installation "
|
308
|
+
f"directory '{pioneer_install_dir}'"
|
309
|
+
)
|
310
|
+
# Name of the Rekordbox application directory in `pioneer_install_dir`
|
311
|
+
rb_prog_dir = pioneer_install_dir / f"rekordbox {rb_version}"
|
288
312
|
|
289
313
|
# Check installation directory
|
290
314
|
if not rb_prog_dir.exists():
|
@@ -318,10 +342,12 @@ def _get_rb_config(
|
|
318
342
|
return conf
|
319
343
|
|
320
344
|
|
321
|
-
def _get_rb5_config(
|
345
|
+
def _get_rb5_config(
|
346
|
+
pioneer_prog_dir: Path, pioneer_app_dir: Path, dirname: str = ""
|
347
|
+
) -> dict:
|
322
348
|
"""Get the program configuration for Rekordbox v5.x.x."""
|
323
349
|
major_version = 5
|
324
|
-
conf = _get_rb_config(pioneer_prog_dir, pioneer_app_dir, major_version)
|
350
|
+
conf = _get_rb_config(pioneer_prog_dir, pioneer_app_dir, major_version, dirname)
|
325
351
|
return conf
|
326
352
|
|
327
353
|
|
@@ -377,10 +403,12 @@ def write_db6_key_cache(key: str) -> None: # pragma: no cover
|
|
377
403
|
__config__["rekordbox6"]["dp"] = key
|
378
404
|
|
379
405
|
|
380
|
-
def _get_rb6_config(
|
406
|
+
def _get_rb6_config(
|
407
|
+
pioneer_prog_dir: Path, pioneer_app_dir: Path, dirname: str = ""
|
408
|
+
) -> dict:
|
381
409
|
"""Get the program configuration for Rekordbox v6.x.x."""
|
382
410
|
major_version = 6
|
383
|
-
conf = _get_rb_config(pioneer_prog_dir, pioneer_app_dir, major_version)
|
411
|
+
conf = _get_rb_config(pioneer_prog_dir, pioneer_app_dir, major_version, dirname)
|
384
412
|
|
385
413
|
# Read Rekordbox 6 'options.json' and check db_path
|
386
414
|
opts = read_rekordbox6_options(pioneer_app_dir)
|
@@ -488,6 +516,8 @@ def read_pyrekordbox_configuration():
|
|
488
516
|
def update_config(
|
489
517
|
pioneer_install_dir: Union[str, Path] = None,
|
490
518
|
pioneer_app_dir: Union[str, Path] = None,
|
519
|
+
rb5_install_dirname: str = "",
|
520
|
+
rb6_install_dirname: str = "",
|
491
521
|
):
|
492
522
|
"""Update the pyrekordbox configuration.
|
493
523
|
|
@@ -508,6 +538,12 @@ def update_config(
|
|
508
538
|
The path to the Pioneer application directory. This is where the application
|
509
539
|
user data of Pioneer programs is stored. By default, the normal location of
|
510
540
|
the Pioneer application data is used.
|
541
|
+
rb5_install_dirname : str, optional
|
542
|
+
The name of the Rekordbox 5 installation directory. By default, the normal
|
543
|
+
directory name is used (Windows: 'rekordbox 5.x.x', macOS: 'rekordbox 5.app').
|
544
|
+
rb6_install_dirname : str, optional
|
545
|
+
The name of the Rekordbox 6 installation directory. By default, the normal
|
546
|
+
directory name is used (Windows: 'rekordbox 6.x.x', macOS: 'rekordbox 6.app').
|
511
547
|
"""
|
512
548
|
# Read config file
|
513
549
|
conf = read_pyrekordbox_configuration()
|
@@ -515,6 +551,10 @@ def update_config(
|
|
515
551
|
pioneer_install_dir = conf["pioneer-install-dir"]
|
516
552
|
if pioneer_app_dir is None and "pioneer-app-dir" in conf:
|
517
553
|
pioneer_app_dir = conf["pioneer-app-dir"]
|
554
|
+
if not rb5_install_dirname and "rekordbox5-install-dirname" in conf:
|
555
|
+
rb5_install_dirname = conf["rekordbox5-install-dirname"]
|
556
|
+
if not rb6_install_dirname and "rekordbox6-install-dirname" in conf:
|
557
|
+
rb6_install_dirname = conf["rekordbox6-install-dirname"]
|
518
558
|
|
519
559
|
# Pioneer installation directory
|
520
560
|
try:
|
@@ -534,14 +574,18 @@ def update_config(
|
|
534
574
|
|
535
575
|
# Update Rekordbox 5 config
|
536
576
|
try:
|
537
|
-
conf = _get_rb5_config(
|
577
|
+
conf = _get_rb5_config(
|
578
|
+
pioneer_install_dir, pioneer_app_dir, rb5_install_dirname
|
579
|
+
)
|
538
580
|
__config__["rekordbox5"].update(conf)
|
539
581
|
except FileNotFoundError as e:
|
540
582
|
logger.info(e)
|
541
583
|
|
542
584
|
# Update Rekordbox 6 config
|
543
585
|
try:
|
544
|
-
conf = _get_rb6_config(
|
586
|
+
conf = _get_rb6_config(
|
587
|
+
pioneer_install_dir, pioneer_app_dir, rb6_install_dirname
|
588
|
+
)
|
545
589
|
__config__["rekordbox6"].update(conf)
|
546
590
|
except FileNotFoundError as e:
|
547
591
|
logger.info(e)
|