pyrekordbox 0.3.2__py3-none-any.whl → 0.4.1__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 (83) hide show
  1. pyrekordbox/__init__.py +8 -8
  2. pyrekordbox/__main__.py +3 -2
  3. pyrekordbox/_version.py +9 -4
  4. pyrekordbox/anlz/__init__.py +3 -2
  5. pyrekordbox/anlz/file.py +4 -2
  6. pyrekordbox/anlz/tags.py +8 -16
  7. pyrekordbox/config.py +116 -47
  8. pyrekordbox/db6/__init__.py +2 -2
  9. pyrekordbox/db6/aux_files.py +3 -2
  10. pyrekordbox/db6/database.py +130 -177
  11. pyrekordbox/db6/registry.py +1 -0
  12. pyrekordbox/db6/smartlist.py +9 -11
  13. pyrekordbox/db6/tables.py +112 -132
  14. pyrekordbox/logger.py +0 -1
  15. pyrekordbox/mysettings/__init__.py +5 -4
  16. pyrekordbox/mysettings/file.py +3 -1
  17. pyrekordbox/rbxml.py +8 -12
  18. pyrekordbox/utils.py +4 -3
  19. {pyrekordbox-0.3.2.dist-info → pyrekordbox-0.4.1.dist-info}/METADATA +35 -48
  20. pyrekordbox-0.4.1.dist-info/RECORD +25 -0
  21. {pyrekordbox-0.3.2.dist-info → pyrekordbox-0.4.1.dist-info}/WHEEL +1 -1
  22. {pyrekordbox-0.3.2.dist-info → pyrekordbox-0.4.1.dist-info}/top_level.txt +0 -2
  23. docs/Makefile +0 -20
  24. docs/make.bat +0 -35
  25. docs/source/_static/images/anlz_beat.svg +0 -53
  26. docs/source/_static/images/anlz_file.svg +0 -204
  27. docs/source/_static/images/anlz_pco2.svg +0 -138
  28. docs/source/_static/images/anlz_pcob.svg +0 -148
  29. docs/source/_static/images/anlz_pcp2.svg +0 -398
  30. docs/source/_static/images/anlz_pcpt.svg +0 -263
  31. docs/source/_static/images/anlz_ppth.svg +0 -123
  32. docs/source/_static/images/anlz_pqt2.svg +0 -324
  33. docs/source/_static/images/anlz_pqt2_2.svg +0 -253
  34. docs/source/_static/images/anlz_pqtz.svg +0 -140
  35. docs/source/_static/images/anlz_pssi.svg +0 -192
  36. docs/source/_static/images/anlz_pssi_entry.svg +0 -191
  37. docs/source/_static/images/anlz_pvbr.svg +0 -125
  38. docs/source/_static/images/anlz_pwav.svg +0 -130
  39. docs/source/_static/images/anlz_pwv3.svg +0 -139
  40. docs/source/_static/images/anlz_pwv4.svg +0 -139
  41. docs/source/_static/images/anlz_pwv5.svg +0 -139
  42. docs/source/_static/images/anlz_pwv5_entry.svg +0 -100
  43. docs/source/_static/images/anlz_pwv6.svg +0 -130
  44. docs/source/_static/images/anlz_pwv7.svg +0 -139
  45. docs/source/_static/images/anlz_pwvc.svg +0 -125
  46. docs/source/_static/images/anlz_tag.svg +0 -110
  47. docs/source/_static/images/x64dbg_rb_key.png +0 -0
  48. docs/source/_static/logos/dark/logo_primary.svg +0 -75
  49. docs/source/_static/logos/light/logo_primary.svg +0 -75
  50. docs/source/_static/logos/mid/logo_primary.svg +0 -75
  51. docs/source/_templates/apidoc/module.rst_t +0 -8
  52. docs/source/_templates/apidoc/package.rst_t +0 -57
  53. docs/source/_templates/apidoc/toc.rst_t +0 -7
  54. docs/source/_templates/autosummary/class.rst +0 -32
  55. docs/source/_templates/autosummary/module.rst +0 -55
  56. docs/source/api.md +0 -18
  57. docs/source/conf.py +0 -178
  58. docs/source/development/changes.md +0 -3
  59. docs/source/development/contributing.md +0 -3
  60. docs/source/formats/anlz.md +0 -634
  61. docs/source/formats/db6.md +0 -1233
  62. docs/source/formats/mysetting.md +0 -392
  63. docs/source/formats/xml.md +0 -376
  64. docs/source/index.md +0 -103
  65. docs/source/installation.md +0 -271
  66. docs/source/key.md +0 -103
  67. docs/source/quickstart.md +0 -189
  68. docs/source/requirements.txt +0 -7
  69. docs/source/tutorial/anlz.md +0 -7
  70. docs/source/tutorial/configuration.md +0 -66
  71. docs/source/tutorial/db6.md +0 -178
  72. docs/source/tutorial/index.md +0 -20
  73. docs/source/tutorial/mysetting.md +0 -124
  74. docs/source/tutorial/xml.md +0 -140
  75. pyrekordbox/xml.py +0 -8
  76. pyrekordbox-0.3.2.dist-info/RECORD +0 -84
  77. tests/__init__.py +0 -3
  78. tests/test_anlz.py +0 -206
  79. tests/test_config.py +0 -175
  80. tests/test_db6.py +0 -1193
  81. tests/test_mysetting.py +0 -203
  82. tests/test_xml.py +0 -629
  83. {pyrekordbox-0.3.2.dist-info → pyrekordbox-0.4.1.dist-info/licenses}/LICENSE +0 -0
pyrekordbox/__init__.py CHANGED
@@ -2,19 +2,19 @@
2
2
  # Author: Dylan Jones
3
3
  # Date: 2022-04-10
4
4
 
5
+ from .anlz import AnlzFile, get_anlz_paths, read_anlz_files, walk_anlz_paths
6
+ from .config import get_config, show_config, update_config
7
+ from .db6 import Rekordbox6Database
5
8
  from .logger import logger
6
- from .config import show_config, get_config, update_config
7
- from .rbxml import RekordboxXml, XmlDuplicateError, XmlAttributeKeyError
8
- from .anlz import get_anlz_paths, walk_anlz_paths, read_anlz_files, AnlzFile
9
9
  from .mysettings import (
10
+ DevSettingFile,
11
+ DjmMySettingFile,
12
+ MySetting2File,
13
+ MySettingFile,
10
14
  get_mysetting_paths,
11
15
  read_mysetting_file,
12
- MySettingFile,
13
- MySetting2File,
14
- DjmMySettingFile,
15
- DevSettingFile,
16
16
  )
17
- from .db6 import Rekordbox6Database, open_rekordbox_database
17
+ from .rbxml import RekordboxXml, XmlAttributeKeyError, XmlDuplicateError
18
18
 
19
19
  try:
20
20
  from ._version import version as __version__
pyrekordbox/__main__.py CHANGED
@@ -4,11 +4,12 @@
4
4
 
5
5
  import os
6
6
  import re
7
- import sys
8
7
  import shutil
8
+ import sys
9
9
  import urllib.request
10
10
  from pathlib import Path
11
- from pyrekordbox.config import write_db6_key_cache, _cache_file
11
+
12
+ from pyrekordbox.config import _cache_file, write_db6_key_cache
12
13
 
13
14
  KEY_SOURCES = [
14
15
  {
pyrekordbox/_version.py CHANGED
@@ -1,8 +1,13 @@
1
- # file generated by setuptools_scm
1
+ # file generated by setuptools-scm
2
2
  # don't change, don't track in version control
3
+
4
+ __all__ = ["__version__", "__version_tuple__", "version", "version_tuple"]
5
+
3
6
  TYPE_CHECKING = False
4
7
  if TYPE_CHECKING:
5
- from typing import Tuple, Union
8
+ from typing import Tuple
9
+ from typing import Union
10
+
6
11
  VERSION_TUPLE = Tuple[Union[int, str], ...]
7
12
  else:
8
13
  VERSION_TUPLE = object
@@ -12,5 +17,5 @@ __version__: str
12
17
  __version_tuple__: VERSION_TUPLE
13
18
  version_tuple: VERSION_TUPLE
14
19
 
15
- __version__ = version = '0.3.2'
16
- __version_tuple__ = version_tuple = (0, 3, 2)
20
+ __version__ = version = '0.4.1'
21
+ __version_tuple__ = version_tuple = (0, 4, 1)
@@ -5,6 +5,7 @@
5
5
  import re
6
6
  from pathlib import Path
7
7
  from typing import Union
8
+
8
9
  from . import structs
9
10
  from .file import AnlzFile
10
11
 
@@ -77,7 +78,7 @@ def walk_anlz_dirs(root_dir: Union[str, Path]):
77
78
  The path of the root directory.
78
79
 
79
80
  Yields
80
- -------
81
+ ------
81
82
  anlz_dir : str
82
83
  The path of a directory containing ANLZ files
83
84
  """
@@ -96,7 +97,7 @@ def walk_anlz_paths(root_dir: Union[str, Path]):
96
97
  The path of the root directory.
97
98
 
98
99
  Yields
99
- -------
100
+ ------
100
101
  anlz_dir : str
101
102
  The path of a directory containing ANLZ files.
102
103
  anlz_files : Sequence of str
pyrekordbox/anlz/file.py CHANGED
@@ -6,10 +6,12 @@ import logging
6
6
  from collections import abc
7
7
  from pathlib import Path
8
8
  from typing import Union
9
- from .tags import TAGS
10
- from . import structs
9
+
11
10
  from construct import Int16ub
12
11
 
12
+ from . import structs
13
+ from .tags import TAGS
14
+
13
15
  logger = logging.getLogger(__name__)
14
16
 
15
17
  XOR_MASK = bytearray.fromhex("CB E1 EE FA E5 EE AD EE E9 D2 E9 EB E1 E9 F3 E8 E9 F4 E1")
pyrekordbox/anlz/tags.py CHANGED
@@ -4,7 +4,9 @@
4
4
 
5
5
  import logging
6
6
  from abc import ABC
7
+
7
8
  import numpy as np
9
+
8
10
  from . import structs
9
11
 
10
12
  logger = logging.getLogger(__name__)
@@ -160,13 +162,9 @@ class PQTZAnlzTag(AbstractAnlzTag):
160
162
  n_bpms = len(bpms)
161
163
  n_times = len(times)
162
164
  if n_bpms != n_beats:
163
- raise ValueError(
164
- f"Number of bpms not equal to number of beats: {n_bpms} != {n_beats}"
165
- )
165
+ raise ValueError(f"Number of bpms not equal to number of beats: {n_bpms} != {n_beats}")
166
166
  if n_times != n_beats:
167
- raise ValueError(
168
- f"Number of times not equal to number of beats: {n_bpms} != {n_times}"
169
- )
167
+ raise ValueError(f"Number of times not equal to number of beats: {n_bpms} != {n_times}")
170
168
 
171
169
  # For now only values of existing beats can be set
172
170
  if n_beats != n:
@@ -182,9 +180,7 @@ class PQTZAnlzTag(AbstractAnlzTag):
182
180
  n = len(self.content.entries)
183
181
  n_new = len(beats)
184
182
  if n_new != n:
185
- raise ValueError(
186
- f"Number of beats not equal to current content length: {n_new} != {n}"
187
- )
183
+ raise ValueError(f"Number of beats not equal to current content length: {n_new} != {n}")
188
184
 
189
185
  for i, beat in enumerate(beats):
190
186
  self.content.entries[i].beat = beat
@@ -193,9 +189,7 @@ class PQTZAnlzTag(AbstractAnlzTag):
193
189
  n = len(self.content.entries)
194
190
  n_new = len(bpms)
195
191
  if n_new != n:
196
- raise ValueError(
197
- f"Number of bpms not equal to current content length: {n_new} != {n}"
198
- )
192
+ raise ValueError(f"Number of bpms not equal to current content length: {n_new} != {n}")
199
193
 
200
194
  for i, bpm in enumerate(bpms):
201
195
  self.content.entries[i].tempo = int(bpm * 100)
@@ -204,9 +198,7 @@ class PQTZAnlzTag(AbstractAnlzTag):
204
198
  n = len(self.content.entries)
205
199
  n_new = len(times)
206
200
  if n_new != n:
207
- raise ValueError(
208
- f"Number of times not equal to current content length: {n_new} != {n}"
209
- )
201
+ raise ValueError(f"Number of times not equal to current content length: {n_new} != {n}")
210
202
 
211
203
  for i, t in enumerate(times):
212
204
  self.content.entries[i].time = int(1000 * t)
@@ -439,7 +431,7 @@ class PWV5AnlzTag(AbstractAnlzTag):
439
431
  LEN_HEADER = 24
440
432
 
441
433
  def get(self):
442
- """Parse the Waveform Color Detail Tag (PWV5)
434
+ """Parse the Waveform Color Detail Tag (PWV5).
443
435
 
444
436
  The format of the entries is:
445
437
 
pyrekordbox/config.py CHANGED
@@ -30,7 +30,7 @@ logger = logging.getLogger(__name__)
30
30
 
31
31
  # Cache file for pyrekordbox data
32
32
  _cache_file_version = 2
33
- _cache_file = Path(__file__).parent / "rb.cache"
33
+ _cache_file_name = "rb.cache"
34
34
 
35
35
  # Define empty pyrekordbox configuration
36
36
  __config__ = {
@@ -40,6 +40,7 @@ __config__ = {
40
40
  },
41
41
  "rekordbox5": {},
42
42
  "rekordbox6": {},
43
+ "rekordbox7": {},
43
44
  }
44
45
 
45
46
 
@@ -47,10 +48,30 @@ class InvalidApplicationDirname(Exception):
47
48
  pass
48
49
 
49
50
 
51
+ def get_appdata_dir() -> Path:
52
+ """Returns the path of the application data directory.
53
+
54
+ On Windows, the application data is stored in `/Users/user/AppData/Roaming`.
55
+ On macOS the application data is stored in `~/Libary/Application Support`.
56
+ """
57
+ if sys.platform == "win32":
58
+ # Windows: located in /Users/user/AppData/Roaming/
59
+ app_data = Path(os.environ["AppData"])
60
+ elif sys.platform == "darwin":
61
+ # MacOS: located in ~/Library/Application Support/
62
+ app_data = Path("~").expanduser() / "Library" / "Application Support"
63
+ else:
64
+ # Linux: not supported
65
+ logger.warning(f"OS {sys.platform} not supported!")
66
+ return Path("~").expanduser() / ".local" / "share"
67
+ return app_data
68
+
69
+
50
70
  def get_pioneer_install_dir(path: Union[str, Path] = None) -> Path: # pragma: no cover
51
71
  """Returns the path of the Pioneer program installation directory.
52
72
 
53
- On Windows, the Pioneer program data is stored in `/ProgramFiles/Pioneer`
73
+ On Windows, the Pioneer program data is stored in `/ProgramFiles/Pioneer`.
74
+ For rekordbox version 7 this has changed to `/ProgramFiles/rekordbox`.
54
75
  On macOS the program data is somewhere in `/Applications/`.
55
76
 
56
77
  Parameters
@@ -68,7 +89,7 @@ def get_pioneer_install_dir(path: Union[str, Path] = None) -> Path: # pragma: n
68
89
  if sys.platform == "win32":
69
90
  # Windows: located in /ProgramFiles/Pioneer
70
91
  program_files = os.environ["ProgramFiles"].replace("(x86)", "").strip()
71
- path = Path(program_files) / "Pioneer"
92
+ path = Path(program_files)
72
93
  elif sys.platform == "darwin":
73
94
  # MacOS: located in /Applications/
74
95
  path = Path("/Applications")
@@ -232,12 +253,7 @@ def read_rekordbox6_asar(rb6_install_dir: Union[str, Path]) -> str:
232
253
  if not str(rb6_install_dir).endswith(".app"):
233
254
  rb6_install_dir = rb6_install_dir / "rekordbox.app"
234
255
  location = (
235
- rb6_install_dir
236
- / "Contents"
237
- / "MacOS"
238
- / "rekordboxAgent.app"
239
- / "Contents"
240
- / "Resources"
256
+ rb6_install_dir / "Contents" / "MacOS" / "rekordboxAgent.app" / "Contents" / "Resources"
241
257
  )
242
258
  encoding = "cp437"
243
259
  else:
@@ -247,7 +263,7 @@ def read_rekordbox6_asar(rb6_install_dir: Union[str, Path]) -> str:
247
263
  # Read asar file
248
264
  path = (location / "app.asar").absolute()
249
265
  with open(path, "rb") as fh:
250
- data = fh.read().decode(encoding)
266
+ data = fh.read().decode(encoding, errors="replace")
251
267
  return data
252
268
 
253
269
 
@@ -284,6 +300,12 @@ def _get_rb_config(
284
300
  config : dict
285
301
  The program configuration.
286
302
  """
303
+ if sys.platform == "win32":
304
+ if major_version >= 7:
305
+ pioneer_install_dir = pioneer_install_dir / "rekordbox"
306
+ else:
307
+ pioneer_install_dir = pioneer_install_dir / "Pioneer"
308
+
287
309
  if application_dirname:
288
310
  # Applitcation dirname is given, only extract version from it
289
311
  # `major_version` is compared to the version string
@@ -328,7 +350,7 @@ def _get_rb_config(
328
350
  logger.debug("Found Rekordbox %s install-dir: '%s'", major_version, rb_prog_dir)
329
351
 
330
352
  # Get Rekordbox application directory path for major release `major_version`
331
- name = "rekordbox6" if major_version == 6 else "rekordbox"
353
+ name = "rekordbox6" if major_version >= 6 else "rekordbox"
332
354
  rb_app_dir = pioneer_app_dir / name
333
355
  if not rb_app_dir.exists():
334
356
  raise FileNotFoundError(f"The directory '{rb_app_dir}' doesn't exist!")
@@ -337,7 +359,7 @@ def _get_rb_config(
337
359
  # Get Rekordbox database locations for major release `major_version`
338
360
  settings = read_rekordbox_settings(rb_app_dir)
339
361
  db_dir = Path(settings["masterDbDirectory"])
340
- db_filename = "master.db" if major_version == 6 else "datafile.edb"
362
+ db_filename = "master.db" if major_version >= 6 else "datafile.edb"
341
363
  db_path = db_dir / db_filename
342
364
  if not db_path.exists():
343
365
  raise FileNotFoundError(f"The Rekordbox database '{db_path}' doesn't exist!")
@@ -352,9 +374,7 @@ def _get_rb_config(
352
374
  return conf
353
375
 
354
376
 
355
- def _get_rb5_config(
356
- pioneer_prog_dir: Path, pioneer_app_dir: Path, dirname: str = ""
357
- ) -> dict:
377
+ def _get_rb5_config(pioneer_prog_dir: Path, pioneer_app_dir: Path, dirname: str = "") -> dict:
358
378
  """Get the program configuration for Rekordbox v5.x.x."""
359
379
  major_version = 5
360
380
  conf = _get_rb_config(pioneer_prog_dir, pioneer_app_dir, major_version, dirname)
@@ -363,7 +383,6 @@ def _get_rb5_config(
363
383
 
364
384
  def _extract_pw(pioneer_install_dir: Path) -> str: # pragma: no cover
365
385
  """Extract the password for decrypting the Rekordbox 6 database key."""
366
-
367
386
  asar_data = read_rekordbox6_asar(pioneer_install_dir)
368
387
  match_result = re.search('pass: ".(.*?)"', asar_data)
369
388
  if match_result is None:
@@ -407,8 +426,7 @@ class KeyExtractor:
407
426
  pid = get_rekordbox_pid()
408
427
  if pid:
409
428
  raise RuntimeError(
410
- "Rekordbox is running. "
411
- "Please close Rekordbox before running the `KeyExtractor`."
429
+ "Rekordbox is running. Please close Rekordbox before running the `KeyExtractor`."
412
430
  )
413
431
  # Spawn Rekordbox process and attach to it
414
432
  pid = frida.spawn(self.executable)
@@ -427,12 +445,16 @@ class KeyExtractor:
427
445
 
428
446
 
429
447
  def write_db6_key_cache(key: str) -> None: # pragma: no cover
430
- """Writes the decrypted Rekordbox6 database key to the cache file.
448
+ r"""Writes the decrypted Rekordbox6 database key to the cache file.
431
449
 
432
450
  This method can also be used to manually cache the database key, provided
433
451
  the user has found the key somewhere else. The key can be, for example,
434
452
  found in some other projects that hard-coded it.
435
453
 
454
+ The cache file is stored in the application data directory of pyrekordbox:
455
+ Windows: `C:\Users\<user>\AppData\Roaming\pyrekordbox`
456
+ macOS: `~/Library/Application Support/pyrekordbox`
457
+
436
458
  Parameters
437
459
  ----------
438
460
  key : str
@@ -454,32 +476,30 @@ def write_db6_key_cache(key: str) -> None: # pragma: no cover
454
476
  lines.append(f"version: {_cache_file_version}")
455
477
  lines.append("dp: " + key)
456
478
  text = "\n".join(lines)
457
- with open(_cache_file, "w") as fh:
458
- fh.write(text)
459
- # Set the config key to make sure the key is present after calling method
460
- __config__["rekordbox6"]["dp"] = key
461
479
 
480
+ cache_file = get_appdata_dir() / "pyrekordbox" / _cache_file_name
481
+ if not cache_file.parent.exists():
482
+ cache_file.parent.mkdir()
462
483
 
463
- def _get_rb6_config(
464
- pioneer_prog_dir: Path, pioneer_app_dir: Path, dirname: str = ""
465
- ) -> dict:
466
- """Get the program configuration for Rekordbox v6.x.x."""
467
- major_version = 6
468
- conf = _get_rb_config(pioneer_prog_dir, pioneer_app_dir, major_version, dirname)
484
+ with open(cache_file, "w") as fh:
485
+ fh.write(text)
486
+ # Set the config key to make sure the key is present after calling method
487
+ if __config__["rekordbox6"]:
488
+ __config__["rekordbox6"]["dp"] = key
489
+ if __config__["rekordbox7"]:
490
+ __config__["rekordbox7"]["dp"] = key
469
491
 
470
- # Read Rekordbox 6 'options.json' and check db_path
471
- opts = read_rekordbox6_options(pioneer_app_dir)
472
- db_path = Path(opts["db-path"])
473
- db_dir = db_path.parent
474
- assert str(conf["db_dir"]) == str(db_dir)
475
- assert str(conf["db_path"]) == str(db_path)
476
492
 
493
+ def _update_sqlite_key(opts, conf):
477
494
  cache_version = 0
478
495
  pw, dp = "", ""
479
- if _cache_file.exists(): # pragma: no cover
480
- logger.debug("Found cache file %s", _cache_file)
496
+
497
+ cache_file = get_appdata_dir() / "pyrekordbox" / _cache_file_name
498
+
499
+ if cache_file.exists(): # pragma: no cover
500
+ logger.debug("Found cache file %s", cache_file)
481
501
  # Read cache file
482
- with open(_cache_file, "r") as fh:
502
+ with open(cache_file, "r") as fh:
483
503
  text = fh.read()
484
504
  lines = text.splitlines()
485
505
  if lines[0].startswith("version:"):
@@ -520,9 +540,10 @@ def _get_rb6_config(
520
540
  if sys.platform == "win32":
521
541
  executable = conf["install_dir"] / "rekordbox.exe"
522
542
  elif sys.platform == "darwin":
523
- executable = (
524
- conf["install_dir"] / "Contents" / "MacOS" / "rekordbox"
525
- )
543
+ install_dir = conf["install_dir"]
544
+ if not str(install_dir).endswith(".app"):
545
+ install_dir = install_dir / "rekordbox.app"
546
+ executable = install_dir / "Contents" / "MacOS" / "rekordbox"
526
547
  else:
527
548
  # Linux: not supported
528
549
  logger.warning(f"OS {sys.platform} not supported!")
@@ -552,6 +573,40 @@ def _get_rb6_config(
552
573
  if dp:
553
574
  conf["dp"] = dp
554
575
 
576
+
577
+ def _get_rb6_config(pioneer_prog_dir: Path, pioneer_app_dir: Path, dirname: str = "") -> dict:
578
+ """Get the program configuration for Rekordbox v6.x.x."""
579
+ major_version = 6
580
+ conf = _get_rb_config(pioneer_prog_dir, pioneer_app_dir, major_version, dirname)
581
+
582
+ # Read Rekordbox 6 'options.json' and check db_path
583
+ opts = read_rekordbox6_options(pioneer_app_dir)
584
+ db_path = Path(opts["db-path"])
585
+ db_dir = db_path.parent
586
+ assert str(conf["db_dir"]) == str(db_dir)
587
+ assert str(conf["db_path"]) == str(db_path)
588
+
589
+ # Update SQLite key
590
+ _update_sqlite_key(opts, conf)
591
+
592
+ return conf
593
+
594
+
595
+ def _get_rb7_config(pioneer_prog_dir: Path, pioneer_app_dir: Path, dirname: str = "") -> dict:
596
+ """Get the program configuration for Rekordbox v7.x.x."""
597
+ major_version = 7
598
+ conf = _get_rb_config(pioneer_prog_dir, pioneer_app_dir, major_version, dirname)
599
+
600
+ # Read Rekordbox 6 'options.json' and check db_path
601
+ opts = read_rekordbox6_options(pioneer_app_dir)
602
+ db_path = Path(opts["db-path"])
603
+ db_dir = db_path.parent
604
+ assert str(conf["db_dir"]) == str(db_dir)
605
+ assert str(conf["db_path"]) == str(db_path)
606
+
607
+ # Update SQLite key
608
+ _update_sqlite_key(opts, conf)
609
+
555
610
  return conf
556
611
 
557
612
 
@@ -617,6 +672,7 @@ def update_config(
617
672
  pioneer_app_dir: Union[str, Path] = None,
618
673
  rb5_install_dirname: str = "",
619
674
  rb6_install_dirname: str = "",
675
+ rb7_install_dirname: str = "",
620
676
  ):
621
677
  """Update the pyrekordbox configuration.
622
678
 
@@ -643,6 +699,9 @@ def update_config(
643
699
  rb6_install_dirname : str, optional
644
700
  The name of the Rekordbox 6 installation directory. By default, the normal
645
701
  directory name is used (Windows: 'rekordbox 6.x.x', macOS: 'rekordbox 6.app').
702
+ rb7_install_dirname : str, optional
703
+ The name of the Rekordbox 7 installation directory. By default, the normal
704
+ directory name is used (Windows: 'rekordbox 7.x.x', macOS: 'rekordbox 7.app').
646
705
  """
647
706
  # Read config file
648
707
  conf = read_pyrekordbox_configuration()
@@ -654,6 +713,8 @@ def update_config(
654
713
  rb5_install_dirname = conf["rekordbox5-install-dirname"]
655
714
  if not rb6_install_dirname and "rekordbox6-install-dirname" in conf:
656
715
  rb6_install_dirname = conf["rekordbox6-install-dirname"]
716
+ if not rb7_install_dirname and "rekordbox7-install-dirname" in conf:
717
+ rb7_install_dirname = conf["rekordbox7-install-dirname"]
657
718
 
658
719
  # Pioneer installation directory
659
720
  try:
@@ -673,22 +734,25 @@ def update_config(
673
734
 
674
735
  # Update Rekordbox 5 config
675
736
  try:
676
- conf = _get_rb5_config(
677
- pioneer_install_dir, pioneer_app_dir, rb5_install_dirname
678
- )
737
+ conf = _get_rb5_config(pioneer_install_dir, pioneer_app_dir, rb5_install_dirname)
679
738
  __config__["rekordbox5"].update(conf)
680
739
  except FileNotFoundError as e:
681
740
  logger.info(e)
682
741
 
683
742
  # Update Rekordbox 6 config
684
743
  try:
685
- conf = _get_rb6_config(
686
- pioneer_install_dir, pioneer_app_dir, rb6_install_dirname
687
- )
744
+ conf = _get_rb6_config(pioneer_install_dir, pioneer_app_dir, rb6_install_dirname)
688
745
  __config__["rekordbox6"].update(conf)
689
746
  except FileNotFoundError as e:
690
747
  logger.info(e)
691
748
 
749
+ # Update Rekordbox 7 config
750
+ try:
751
+ conf = _get_rb7_config(pioneer_install_dir, pioneer_app_dir, rb7_install_dirname)
752
+ __config__["rekordbox7"].update(conf)
753
+ except FileNotFoundError as e:
754
+ logger.info(e)
755
+
692
756
 
693
757
  def get_config(section: str, key: str = None):
694
758
  """Gets a section or value of the pyrekordbox configuration.
@@ -721,6 +785,7 @@ def pformat_config(indent: str = " ", hw: int = 14, delim: str = " = ") -> str
721
785
  pioneer = get_config("pioneer")
722
786
  rb5 = get_config("rekordbox5")
723
787
  rb6 = get_config("rekordbox6")
788
+ rb7 = get_config("rekordbox7")
724
789
 
725
790
  lines = ["Pioneer:"]
726
791
  lines += [f"{indent}{k + delim:<{hw}} {pioneer[k]}" for k in sorted(pioneer.keys())]
@@ -731,6 +796,10 @@ def pformat_config(indent: str = " ", hw: int = 14, delim: str = " = ") -> str
731
796
  if rb6:
732
797
  rb6_keys = [k for k in rb6.keys() if k not in ("dp", "p")]
733
798
  lines += [f"{indent}{k + delim:<{hw}} {rb6[k]}" for k in sorted(rb6_keys)]
799
+ lines.append("Rekordbox 7:")
800
+ if rb7:
801
+ rb7_keys = [k for k in rb7.keys() if k not in ("dp", "p")]
802
+ lines += [f"{indent}{k + delim:<{hw}} {rb7[k]}" for k in sorted(rb7_keys)]
734
803
  return "\n".join(lines)
735
804
 
736
805
 
@@ -2,6 +2,8 @@
2
2
  # Author: Dylan Jones
3
3
  # Date: 2022-05-07
4
4
 
5
+ from .database import Rekordbox6Database
6
+ from .smartlist import SmartList
5
7
  from .tables import (
6
8
  AgentRegistry,
7
9
  CloudAgentRegistry,
@@ -41,5 +43,3 @@ from .tables import (
41
43
  SettingFile,
42
44
  UuidIDMap,
43
45
  )
44
- from .smartlist import SmartList
45
- from .database import Rekordbox6Database, open_rekordbox_database
@@ -2,9 +2,10 @@
2
2
  # Author: Dylan Jones
3
3
  # Date: 2023-09-10
4
4
 
5
- from pathlib import Path
6
- from datetime import datetime
7
5
  import xml.etree.cElementTree as xml
6
+ from datetime import datetime
7
+ from pathlib import Path
8
+
8
9
  from ..config import get_config
9
10
  from ..utils import pretty_xml
10
11