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.
Files changed (69) hide show
  1. docs/Makefile +20 -0
  2. docs/make.bat +35 -0
  3. docs/source/_static/images/anlz_beat.svg +53 -0
  4. docs/source/_static/images/anlz_file.svg +204 -0
  5. docs/source/_static/images/anlz_pco2.svg +138 -0
  6. docs/source/_static/images/anlz_pcob.svg +148 -0
  7. docs/source/_static/images/anlz_pcp2.svg +398 -0
  8. docs/source/_static/images/anlz_pcpt.svg +263 -0
  9. docs/source/_static/images/anlz_ppth.svg +123 -0
  10. docs/source/_static/images/anlz_pqt2.svg +324 -0
  11. docs/source/_static/images/anlz_pqt2_2.svg +253 -0
  12. docs/source/_static/images/anlz_pqtz.svg +140 -0
  13. docs/source/_static/images/anlz_pssi.svg +192 -0
  14. docs/source/_static/images/anlz_pssi_entry.svg +191 -0
  15. docs/source/_static/images/anlz_pvbr.svg +125 -0
  16. docs/source/_static/images/anlz_pwav.svg +130 -0
  17. docs/source/_static/images/anlz_pwv3.svg +139 -0
  18. docs/source/_static/images/anlz_pwv4.svg +139 -0
  19. docs/source/_static/images/anlz_pwv5.svg +139 -0
  20. docs/source/_static/images/anlz_pwv5_entry.svg +100 -0
  21. docs/source/_static/images/anlz_pwv6.svg +130 -0
  22. docs/source/_static/images/anlz_pwv7.svg +139 -0
  23. docs/source/_static/images/anlz_pwvc.svg +125 -0
  24. docs/source/_static/images/anlz_tag.svg +110 -0
  25. docs/source/_static/logos/dark/logo_primary.svg +75 -0
  26. docs/source/_static/logos/light/logo_primary.svg +75 -0
  27. docs/source/_static/logos/mid/logo_primary.svg +75 -0
  28. docs/source/_templates/apidoc/module.rst_t +8 -0
  29. docs/source/_templates/apidoc/package.rst_t +57 -0
  30. docs/source/_templates/apidoc/toc.rst_t +7 -0
  31. docs/source/_templates/autosummary/class.rst +32 -0
  32. docs/source/_templates/autosummary/module.rst +55 -0
  33. docs/source/api.md +18 -0
  34. docs/source/conf.py +178 -0
  35. docs/source/development/changes.md +3 -0
  36. docs/source/development/contributing.md +3 -0
  37. docs/source/formats/anlz.md +634 -0
  38. docs/source/formats/db6.md +1233 -0
  39. docs/source/formats/mysetting.md +392 -0
  40. docs/source/formats/xml.md +376 -0
  41. docs/source/index.md +105 -0
  42. docs/source/installation.md +3 -0
  43. docs/source/quickstart.md +185 -0
  44. docs/source/requirements.txt +7 -0
  45. docs/source/tutorial/anlz.md +7 -0
  46. docs/source/tutorial/configuration.md +66 -0
  47. docs/source/tutorial/db6.md +179 -0
  48. docs/source/tutorial/index.md +20 -0
  49. docs/source/tutorial/mysetting.md +124 -0
  50. docs/source/tutorial/xml.md +140 -0
  51. pyrekordbox/__init__.py +1 -1
  52. pyrekordbox/__main__.py +16 -37
  53. pyrekordbox/_version.py +2 -2
  54. pyrekordbox/anlz/file.py +39 -0
  55. pyrekordbox/anlz/structs.py +3 -5
  56. pyrekordbox/config.py +71 -27
  57. pyrekordbox/db6/database.py +290 -61
  58. pyrekordbox/db6/registry.py +24 -0
  59. pyrekordbox/db6/tables.py +501 -340
  60. pyrekordbox/mysettings/file.py +0 -25
  61. pyrekordbox/utils.py +1 -1
  62. {pyrekordbox-0.2.0.dist-info → pyrekordbox-0.2.2.dist-info}/METADATA +42 -20
  63. pyrekordbox-0.2.2.dist-info/RECORD +80 -0
  64. {pyrekordbox-0.2.0.dist-info → pyrekordbox-0.2.2.dist-info}/top_level.txt +1 -0
  65. tests/test_config.py +175 -0
  66. tests/test_db6.py +95 -0
  67. pyrekordbox-0.2.0.dist-info/RECORD +0 -29
  68. {pyrekordbox-0.2.0.dist-info → pyrekordbox-0.2.2.dist-info}/LICENSE +0 -0
  69. {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
 
@@ -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" / Bytes(2),
222
+ "mood" / Int16ub,
225
223
  "u1" / Bytes(6),
226
- "end_beat" / Bytes(2),
224
+ "end_beat" / Int16ub,
227
225
  "u2" / Bytes(2),
228
- "bank" / Bytes(1),
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
- # Get latest Rekordbox installation directory for major release `major_version`
268
-
269
- # Find all 'V.x.x' version strings in dir names
270
- versions = list()
271
- for p in pioneer_install_dir.iterdir():
272
- name = p.name
273
- if name.startswith("rekordbox"):
274
- ver_str = name.replace("rekordbox", "").strip()
275
- if ver_str.startswith(str(major_version)):
276
- versions.append(ver_str)
277
- # Get latest 'V.x.x' version string and assure there is one
278
- versions.sort(key=lambda s: list(map(int, s.split("."))))
279
- try:
280
- rb_version = versions[-1]
281
- except IndexError:
282
- raise FileNotFoundError(
283
- f"No Rekordbox {major_version} folder found in installation "
284
- f"directory '{pioneer_install_dir}'"
285
- )
286
- # Name of the Rekordbox application directory in `pioneer_install_dir`
287
- rb_prog_dir = pioneer_install_dir / f"rekordbox {rb_version}"
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(pioneer_prog_dir: Path, pioneer_app_dir: Path) -> dict:
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(pioneer_prog_dir: Path, pioneer_app_dir: Path) -> dict:
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(pioneer_install_dir, pioneer_app_dir)
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(pioneer_install_dir, pioneer_app_dir)
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)