pyrekordbox 0.3.2__py3-none-any.whl → 0.4.0__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 +8 -8
- pyrekordbox/__main__.py +3 -2
- pyrekordbox/_version.py +2 -2
- pyrekordbox/anlz/__init__.py +3 -2
- pyrekordbox/anlz/file.py +4 -2
- pyrekordbox/anlz/tags.py +3 -1
- pyrekordbox/config.py +79 -23
- pyrekordbox/db6/__init__.py +2 -2
- pyrekordbox/db6/aux_files.py +3 -2
- pyrekordbox/db6/database.py +101 -111
- pyrekordbox/db6/registry.py +1 -0
- pyrekordbox/db6/smartlist.py +7 -6
- pyrekordbox/db6/tables.py +44 -16
- pyrekordbox/logger.py +0 -1
- pyrekordbox/mysettings/__init__.py +5 -4
- pyrekordbox/mysettings/file.py +3 -1
- pyrekordbox/rbxml.py +5 -3
- pyrekordbox/utils.py +4 -3
- {pyrekordbox-0.3.2.dist-info → pyrekordbox-0.4.0.dist-info}/METADATA +21 -41
- pyrekordbox-0.4.0.dist-info/RECORD +25 -0
- {pyrekordbox-0.3.2.dist-info → pyrekordbox-0.4.0.dist-info}/WHEEL +1 -1
- {pyrekordbox-0.3.2.dist-info → pyrekordbox-0.4.0.dist-info}/top_level.txt +0 -2
- docs/Makefile +0 -20
- docs/make.bat +0 -35
- docs/source/_static/images/anlz_beat.svg +0 -53
- docs/source/_static/images/anlz_file.svg +0 -204
- docs/source/_static/images/anlz_pco2.svg +0 -138
- docs/source/_static/images/anlz_pcob.svg +0 -148
- docs/source/_static/images/anlz_pcp2.svg +0 -398
- docs/source/_static/images/anlz_pcpt.svg +0 -263
- docs/source/_static/images/anlz_ppth.svg +0 -123
- docs/source/_static/images/anlz_pqt2.svg +0 -324
- docs/source/_static/images/anlz_pqt2_2.svg +0 -253
- docs/source/_static/images/anlz_pqtz.svg +0 -140
- docs/source/_static/images/anlz_pssi.svg +0 -192
- docs/source/_static/images/anlz_pssi_entry.svg +0 -191
- docs/source/_static/images/anlz_pvbr.svg +0 -125
- docs/source/_static/images/anlz_pwav.svg +0 -130
- docs/source/_static/images/anlz_pwv3.svg +0 -139
- docs/source/_static/images/anlz_pwv4.svg +0 -139
- docs/source/_static/images/anlz_pwv5.svg +0 -139
- docs/source/_static/images/anlz_pwv5_entry.svg +0 -100
- docs/source/_static/images/anlz_pwv6.svg +0 -130
- docs/source/_static/images/anlz_pwv7.svg +0 -139
- docs/source/_static/images/anlz_pwvc.svg +0 -125
- docs/source/_static/images/anlz_tag.svg +0 -110
- docs/source/_static/images/x64dbg_rb_key.png +0 -0
- docs/source/_static/logos/dark/logo_primary.svg +0 -75
- docs/source/_static/logos/light/logo_primary.svg +0 -75
- docs/source/_static/logos/mid/logo_primary.svg +0 -75
- docs/source/_templates/apidoc/module.rst_t +0 -8
- docs/source/_templates/apidoc/package.rst_t +0 -57
- docs/source/_templates/apidoc/toc.rst_t +0 -7
- docs/source/_templates/autosummary/class.rst +0 -32
- docs/source/_templates/autosummary/module.rst +0 -55
- docs/source/api.md +0 -18
- docs/source/conf.py +0 -178
- docs/source/development/changes.md +0 -3
- docs/source/development/contributing.md +0 -3
- docs/source/formats/anlz.md +0 -634
- docs/source/formats/db6.md +0 -1233
- docs/source/formats/mysetting.md +0 -392
- docs/source/formats/xml.md +0 -376
- docs/source/index.md +0 -103
- docs/source/installation.md +0 -271
- docs/source/key.md +0 -103
- docs/source/quickstart.md +0 -189
- docs/source/requirements.txt +0 -7
- docs/source/tutorial/anlz.md +0 -7
- docs/source/tutorial/configuration.md +0 -66
- docs/source/tutorial/db6.md +0 -178
- docs/source/tutorial/index.md +0 -20
- docs/source/tutorial/mysetting.md +0 -124
- docs/source/tutorial/xml.md +0 -140
- pyrekordbox/xml.py +0 -8
- pyrekordbox-0.3.2.dist-info/RECORD +0 -84
- tests/__init__.py +0 -3
- tests/test_anlz.py +0 -206
- tests/test_config.py +0 -175
- tests/test_db6.py +0 -1193
- tests/test_mysetting.py +0 -203
- tests/test_xml.py +0 -629
- {pyrekordbox-0.3.2.dist-info → pyrekordbox-0.4.0.dist-info}/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 .
|
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
|
-
|
11
|
+
|
12
|
+
from pyrekordbox.config import _cache_file, write_db6_key_cache
|
12
13
|
|
13
14
|
KEY_SOURCES = [
|
14
15
|
{
|
pyrekordbox/_version.py
CHANGED
pyrekordbox/anlz/__init__.py
CHANGED
@@ -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
|
-
|
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__)
|
@@ -439,7 +441,7 @@ class PWV5AnlzTag(AbstractAnlzTag):
|
|
439
441
|
LEN_HEADER = 24
|
440
442
|
|
441
443
|
def get(self):
|
442
|
-
"""Parse the Waveform Color Detail Tag (PWV5)
|
444
|
+
"""Parse the Waveform Color Detail Tag (PWV5).
|
443
445
|
|
444
446
|
The format of the entries is:
|
445
447
|
|
pyrekordbox/config.py
CHANGED
@@ -40,6 +40,7 @@ __config__ = {
|
|
40
40
|
},
|
41
41
|
"rekordbox5": {},
|
42
42
|
"rekordbox6": {},
|
43
|
+
"rekordbox7": {},
|
43
44
|
}
|
44
45
|
|
45
46
|
|
@@ -50,7 +51,8 @@ class InvalidApplicationDirname(Exception):
|
|
50
51
|
def get_pioneer_install_dir(path: Union[str, Path] = None) -> Path: # pragma: no cover
|
51
52
|
"""Returns the path of the Pioneer program installation directory.
|
52
53
|
|
53
|
-
On Windows, the Pioneer program data is stored in `/ProgramFiles/Pioneer
|
54
|
+
On Windows, the Pioneer program data is stored in `/ProgramFiles/Pioneer`.
|
55
|
+
For rekordbox version 7 this has changed to `/ProgramFiles/rekordbox`.
|
54
56
|
On macOS the program data is somewhere in `/Applications/`.
|
55
57
|
|
56
58
|
Parameters
|
@@ -68,7 +70,7 @@ def get_pioneer_install_dir(path: Union[str, Path] = None) -> Path: # pragma: n
|
|
68
70
|
if sys.platform == "win32":
|
69
71
|
# Windows: located in /ProgramFiles/Pioneer
|
70
72
|
program_files = os.environ["ProgramFiles"].replace("(x86)", "").strip()
|
71
|
-
path = Path(program_files)
|
73
|
+
path = Path(program_files)
|
72
74
|
elif sys.platform == "darwin":
|
73
75
|
# MacOS: located in /Applications/
|
74
76
|
path = Path("/Applications")
|
@@ -284,6 +286,12 @@ def _get_rb_config(
|
|
284
286
|
config : dict
|
285
287
|
The program configuration.
|
286
288
|
"""
|
289
|
+
if sys.platform == "win32":
|
290
|
+
if major_version >= 7:
|
291
|
+
pioneer_install_dir = pioneer_install_dir / "rekordbox"
|
292
|
+
else:
|
293
|
+
pioneer_install_dir = pioneer_install_dir / "Pioneer"
|
294
|
+
|
287
295
|
if application_dirname:
|
288
296
|
# Applitcation dirname is given, only extract version from it
|
289
297
|
# `major_version` is compared to the version string
|
@@ -328,7 +336,7 @@ def _get_rb_config(
|
|
328
336
|
logger.debug("Found Rekordbox %s install-dir: '%s'", major_version, rb_prog_dir)
|
329
337
|
|
330
338
|
# Get Rekordbox application directory path for major release `major_version`
|
331
|
-
name = "rekordbox6" if major_version
|
339
|
+
name = "rekordbox6" if major_version >= 6 else "rekordbox"
|
332
340
|
rb_app_dir = pioneer_app_dir / name
|
333
341
|
if not rb_app_dir.exists():
|
334
342
|
raise FileNotFoundError(f"The directory '{rb_app_dir}' doesn't exist!")
|
@@ -337,7 +345,7 @@ def _get_rb_config(
|
|
337
345
|
# Get Rekordbox database locations for major release `major_version`
|
338
346
|
settings = read_rekordbox_settings(rb_app_dir)
|
339
347
|
db_dir = Path(settings["masterDbDirectory"])
|
340
|
-
db_filename = "master.db" if major_version
|
348
|
+
db_filename = "master.db" if major_version >= 6 else "datafile.edb"
|
341
349
|
db_path = db_dir / db_filename
|
342
350
|
if not db_path.exists():
|
343
351
|
raise FileNotFoundError(f"The Rekordbox database '{db_path}' doesn't exist!")
|
@@ -363,7 +371,6 @@ def _get_rb5_config(
|
|
363
371
|
|
364
372
|
def _extract_pw(pioneer_install_dir: Path) -> str: # pragma: no cover
|
365
373
|
"""Extract the password for decrypting the Rekordbox 6 database key."""
|
366
|
-
|
367
374
|
asar_data = read_rekordbox6_asar(pioneer_install_dir)
|
368
375
|
match_result = re.search('pass: ".(.*?)"', asar_data)
|
369
376
|
if match_result is None:
|
@@ -457,23 +464,13 @@ def write_db6_key_cache(key: str) -> None: # pragma: no cover
|
|
457
464
|
with open(_cache_file, "w") as fh:
|
458
465
|
fh.write(text)
|
459
466
|
# Set the config key to make sure the key is present after calling method
|
460
|
-
__config__["rekordbox6"]
|
467
|
+
if __config__["rekordbox6"]:
|
468
|
+
__config__["rekordbox6"]["dp"] = key
|
469
|
+
if __config__["rekordbox7"]:
|
470
|
+
__config__["rekordbox7"]["dp"] = key
|
461
471
|
|
462
472
|
|
463
|
-
def
|
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)
|
469
|
-
|
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
|
-
|
473
|
+
def _update_sqlite_key(opts, conf):
|
477
474
|
cache_version = 0
|
478
475
|
pw, dp = "", ""
|
479
476
|
if _cache_file.exists(): # pragma: no cover
|
@@ -520,9 +517,10 @@ def _get_rb6_config(
|
|
520
517
|
if sys.platform == "win32":
|
521
518
|
executable = conf["install_dir"] / "rekordbox.exe"
|
522
519
|
elif sys.platform == "darwin":
|
523
|
-
|
524
|
-
|
525
|
-
|
520
|
+
install_dir = conf["install_dir"]
|
521
|
+
if not str(install_dir).endswith(".app"):
|
522
|
+
install_dir = install_dir / "rekordbox.app"
|
523
|
+
executable = install_dir / "Contents" / "MacOS" / "rekordbox"
|
526
524
|
else:
|
527
525
|
# Linux: not supported
|
528
526
|
logger.warning(f"OS {sys.platform} not supported!")
|
@@ -552,6 +550,44 @@ def _get_rb6_config(
|
|
552
550
|
if dp:
|
553
551
|
conf["dp"] = dp
|
554
552
|
|
553
|
+
|
554
|
+
def _get_rb6_config(
|
555
|
+
pioneer_prog_dir: Path, pioneer_app_dir: Path, dirname: str = ""
|
556
|
+
) -> dict:
|
557
|
+
"""Get the program configuration for Rekordbox v6.x.x."""
|
558
|
+
major_version = 6
|
559
|
+
conf = _get_rb_config(pioneer_prog_dir, pioneer_app_dir, major_version, dirname)
|
560
|
+
|
561
|
+
# Read Rekordbox 6 'options.json' and check db_path
|
562
|
+
opts = read_rekordbox6_options(pioneer_app_dir)
|
563
|
+
db_path = Path(opts["db-path"])
|
564
|
+
db_dir = db_path.parent
|
565
|
+
assert str(conf["db_dir"]) == str(db_dir)
|
566
|
+
assert str(conf["db_path"]) == str(db_path)
|
567
|
+
|
568
|
+
# Update SQLite key
|
569
|
+
_update_sqlite_key(opts, conf)
|
570
|
+
|
571
|
+
return conf
|
572
|
+
|
573
|
+
|
574
|
+
def _get_rb7_config(
|
575
|
+
pioneer_prog_dir: Path, pioneer_app_dir: Path, dirname: str = ""
|
576
|
+
) -> dict:
|
577
|
+
"""Get the program configuration for Rekordbox v7.x.x."""
|
578
|
+
major_version = 7
|
579
|
+
conf = _get_rb_config(pioneer_prog_dir, pioneer_app_dir, major_version, dirname)
|
580
|
+
|
581
|
+
# Read Rekordbox 6 'options.json' and check db_path
|
582
|
+
opts = read_rekordbox6_options(pioneer_app_dir)
|
583
|
+
db_path = Path(opts["db-path"])
|
584
|
+
db_dir = db_path.parent
|
585
|
+
assert str(conf["db_dir"]) == str(db_dir)
|
586
|
+
assert str(conf["db_path"]) == str(db_path)
|
587
|
+
|
588
|
+
# Update SQLite key
|
589
|
+
_update_sqlite_key(opts, conf)
|
590
|
+
|
555
591
|
return conf
|
556
592
|
|
557
593
|
|
@@ -617,6 +653,7 @@ def update_config(
|
|
617
653
|
pioneer_app_dir: Union[str, Path] = None,
|
618
654
|
rb5_install_dirname: str = "",
|
619
655
|
rb6_install_dirname: str = "",
|
656
|
+
rb7_install_dirname: str = "",
|
620
657
|
):
|
621
658
|
"""Update the pyrekordbox configuration.
|
622
659
|
|
@@ -643,6 +680,9 @@ def update_config(
|
|
643
680
|
rb6_install_dirname : str, optional
|
644
681
|
The name of the Rekordbox 6 installation directory. By default, the normal
|
645
682
|
directory name is used (Windows: 'rekordbox 6.x.x', macOS: 'rekordbox 6.app').
|
683
|
+
rb7_install_dirname : str, optional
|
684
|
+
The name of the Rekordbox 7 installation directory. By default, the normal
|
685
|
+
directory name is used (Windows: 'rekordbox 7.x.x', macOS: 'rekordbox 7.app').
|
646
686
|
"""
|
647
687
|
# Read config file
|
648
688
|
conf = read_pyrekordbox_configuration()
|
@@ -654,6 +694,8 @@ def update_config(
|
|
654
694
|
rb5_install_dirname = conf["rekordbox5-install-dirname"]
|
655
695
|
if not rb6_install_dirname and "rekordbox6-install-dirname" in conf:
|
656
696
|
rb6_install_dirname = conf["rekordbox6-install-dirname"]
|
697
|
+
if not rb7_install_dirname and "rekordbox7-install-dirname" in conf:
|
698
|
+
rb7_install_dirname = conf["rekordbox7-install-dirname"]
|
657
699
|
|
658
700
|
# Pioneer installation directory
|
659
701
|
try:
|
@@ -689,6 +731,15 @@ def update_config(
|
|
689
731
|
except FileNotFoundError as e:
|
690
732
|
logger.info(e)
|
691
733
|
|
734
|
+
# Update Rekordbox 7 config
|
735
|
+
try:
|
736
|
+
conf = _get_rb7_config(
|
737
|
+
pioneer_install_dir, pioneer_app_dir, rb7_install_dirname
|
738
|
+
)
|
739
|
+
__config__["rekordbox7"].update(conf)
|
740
|
+
except FileNotFoundError as e:
|
741
|
+
logger.info(e)
|
742
|
+
|
692
743
|
|
693
744
|
def get_config(section: str, key: str = None):
|
694
745
|
"""Gets a section or value of the pyrekordbox configuration.
|
@@ -721,6 +772,7 @@ def pformat_config(indent: str = " ", hw: int = 14, delim: str = " = ") -> str
|
|
721
772
|
pioneer = get_config("pioneer")
|
722
773
|
rb5 = get_config("rekordbox5")
|
723
774
|
rb6 = get_config("rekordbox6")
|
775
|
+
rb7 = get_config("rekordbox7")
|
724
776
|
|
725
777
|
lines = ["Pioneer:"]
|
726
778
|
lines += [f"{indent}{k + delim:<{hw}} {pioneer[k]}" for k in sorted(pioneer.keys())]
|
@@ -731,6 +783,10 @@ def pformat_config(indent: str = " ", hw: int = 14, delim: str = " = ") -> str
|
|
731
783
|
if rb6:
|
732
784
|
rb6_keys = [k for k in rb6.keys() if k not in ("dp", "p")]
|
733
785
|
lines += [f"{indent}{k + delim:<{hw}} {rb6[k]}" for k in sorted(rb6_keys)]
|
786
|
+
lines.append("Rekordbox 7:")
|
787
|
+
if rb7:
|
788
|
+
rb7_keys = [k for k in rb7.keys() if k not in ("dp", "p")]
|
789
|
+
lines += [f"{indent}{k + delim:<{hw}} {rb7[k]}" for k in sorted(rb7_keys)]
|
734
790
|
return "\n".join(lines)
|
735
791
|
|
736
792
|
|
pyrekordbox/db6/__init__.py
CHANGED
@@ -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
|
pyrekordbox/db6/aux_files.py
CHANGED
@@ -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
|
|
pyrekordbox/db6/database.py
CHANGED
@@ -2,24 +2,26 @@
|
|
2
2
|
# Author: Dylan Jones
|
3
3
|
# Date: 2023-08-13
|
4
4
|
|
5
|
-
import logging
|
6
5
|
import datetime
|
6
|
+
import logging
|
7
7
|
import secrets
|
8
|
-
from uuid import uuid4
|
9
8
|
from pathlib import Path
|
10
9
|
from typing import Optional
|
11
|
-
from
|
12
|
-
|
10
|
+
from uuid import uuid4
|
11
|
+
|
12
|
+
from sqlalchemy import MetaData, create_engine, event, or_, select
|
13
13
|
from sqlalchemy.exc import NoResultFound
|
14
|
+
from sqlalchemy.orm import Query, Session
|
14
15
|
from sqlalchemy.sql.sqltypes import DateTime, String
|
15
|
-
|
16
|
+
|
17
|
+
from ..anlz import AnlzFile, get_anlz_paths, read_anlz_files
|
16
18
|
from ..config import get_config
|
17
|
-
from ..
|
18
|
-
from .
|
19
|
+
from ..utils import get_rekordbox_pid
|
20
|
+
from . import tables
|
19
21
|
from .aux_files import MasterPlaylistXml
|
20
|
-
from .
|
22
|
+
from .registry import RekordboxAgentRegistry
|
21
23
|
from .smartlist import SmartList
|
22
|
-
from . import
|
24
|
+
from .tables import DjmdContent, FileType, PlaylistType
|
23
25
|
|
24
26
|
try:
|
25
27
|
from sqlcipher3 import dbapi2 as sqlite3 # noqa
|
@@ -39,101 +41,6 @@ class NoCachedKey(Exception):
|
|
39
41
|
pass
|
40
42
|
|
41
43
|
|
42
|
-
def open_rekordbox_database(path=None, key="", unlock=True, sql_driver=None):
|
43
|
-
"""Opens a connection to the Rekordbox v6 master.db SQLite3 database.
|
44
|
-
|
45
|
-
Parameters
|
46
|
-
----------
|
47
|
-
path : str or Path, optional
|
48
|
-
The path of the Rekordbox v6 database file. By default, pyrekordbox
|
49
|
-
automatically finds the Rekordbox v6 master.db database file.
|
50
|
-
This parameter is only required for opening other databases or if the
|
51
|
-
configuration fails.
|
52
|
-
key : str, optional
|
53
|
-
The database key. By default, pyrekordbox automatically reads the database
|
54
|
-
key from the Rekordbox v6 configuration file. This parameter is only required
|
55
|
-
if the key extraction fails.
|
56
|
-
unlock: bool, optional
|
57
|
-
Flag if the database needs to be decrypted. Set to False if you are opening
|
58
|
-
an unencrypted test database.
|
59
|
-
sql_driver : Callable, optional
|
60
|
-
The SQLite driver to used for opening the database. The standard ``sqlite3``
|
61
|
-
package is used as default driver.
|
62
|
-
|
63
|
-
Returns
|
64
|
-
-------
|
65
|
-
con : sql_driver.Connection
|
66
|
-
The opened Rekordbox v6 database connection.
|
67
|
-
|
68
|
-
Examples
|
69
|
-
--------
|
70
|
-
Open the Rekordbox v6 master.db database:
|
71
|
-
|
72
|
-
>>> db = open_rekordbox_database()
|
73
|
-
|
74
|
-
Open a copy of the database:
|
75
|
-
|
76
|
-
>>> db = open_rekordbox_database("path/to/master_copy.db")
|
77
|
-
|
78
|
-
Open a decrypted copy of the database:
|
79
|
-
|
80
|
-
>>> db = open_rekordbox_database("path/to/master_unlocked.db", unlock=False)
|
81
|
-
|
82
|
-
To use the ``pysqlcipher3`` package as SQLite driver, either import it as
|
83
|
-
|
84
|
-
>>> from sqlcipher3 import dbapi2 as sqlite3 # noqa
|
85
|
-
>>> db = open_rekordbox_database("path/to/master_copy.db")
|
86
|
-
|
87
|
-
or supply the package as driver:
|
88
|
-
|
89
|
-
>>> from sqlcipher3 import dbapi2 # noqa
|
90
|
-
>>> db = open_rekordbox_database("path/to/master_copy.db", sql_driver=dbapi2)
|
91
|
-
"""
|
92
|
-
warn_deprecated("open_rekordbox_database", remove_in="0.4.0")
|
93
|
-
rb6_config = get_config("rekordbox6")
|
94
|
-
|
95
|
-
if not path:
|
96
|
-
path = rb6_config["db_path"]
|
97
|
-
path = Path(path)
|
98
|
-
if not path.exists():
|
99
|
-
raise FileNotFoundError(f"File '{path}' does not exist!")
|
100
|
-
logger.info("Opening %s", path)
|
101
|
-
|
102
|
-
# Open database
|
103
|
-
if sql_driver is None:
|
104
|
-
# Use default sqlite3 package
|
105
|
-
# This requires that the 'sqlite3.dll' was replaced by the 'sqlcipher.dll'
|
106
|
-
sql_driver = sqlite3
|
107
|
-
con = sql_driver.connect(str(path))
|
108
|
-
|
109
|
-
if unlock:
|
110
|
-
if not key:
|
111
|
-
try:
|
112
|
-
key = rb6_config["dp"]
|
113
|
-
except KeyError:
|
114
|
-
raise NoCachedKey(
|
115
|
-
"Could not unlock database: No key found\n"
|
116
|
-
f"If you are using Rekordbox>{MAX_VERSION} the key can not be "
|
117
|
-
f"extracted automatically!\n"
|
118
|
-
"Please use the CLI of pyrekordbox to download the key or "
|
119
|
-
"use the `key` parameter to manually provide the database key."
|
120
|
-
)
|
121
|
-
logger.info("Key: %s", key)
|
122
|
-
# Unlock database
|
123
|
-
con.execute(f"PRAGMA key='{key}'")
|
124
|
-
|
125
|
-
# Check connection
|
126
|
-
try:
|
127
|
-
con.execute("SELECT name FROM sqlite_master WHERE type='table';")
|
128
|
-
except sqlite3.DatabaseError as e:
|
129
|
-
msg = f"Opening database failed: '{e}'. Check if the database key is correct!"
|
130
|
-
raise sqlite3.DatabaseError(msg)
|
131
|
-
else:
|
132
|
-
logger.info("Database unlocked!")
|
133
|
-
|
134
|
-
return con
|
135
|
-
|
136
|
-
|
137
44
|
def _parse_query_result(query, kwargs):
|
138
45
|
if "ID" in kwargs or "registry_id" in kwargs:
|
139
46
|
try:
|
@@ -193,17 +100,23 @@ class Rekordbox6Database:
|
|
193
100
|
"""
|
194
101
|
|
195
102
|
def __init__(self, path=None, db_dir="", key="", unlock=True):
|
196
|
-
|
103
|
+
# get config of latest supported version
|
104
|
+
rb_config = get_config("rekordbox7")
|
105
|
+
if not rb_config:
|
106
|
+
rb_config = get_config("rekordbox6")
|
107
|
+
|
197
108
|
pid = get_rekordbox_pid()
|
198
109
|
if pid:
|
199
110
|
logger.warning("Rekordbox is running!")
|
200
111
|
|
201
112
|
if not path:
|
202
113
|
# Get path from the RB config
|
203
|
-
path =
|
114
|
+
path = rb_config.get("db_path", "")
|
204
115
|
if not path:
|
205
116
|
pdir = get_config("pioneer", "install_dir")
|
206
|
-
raise FileNotFoundError(
|
117
|
+
raise FileNotFoundError(
|
118
|
+
f"No Rekordbox v6/v7 directory found in '{pdir}'"
|
119
|
+
)
|
207
120
|
path = Path(path)
|
208
121
|
# make sure file exists
|
209
122
|
if not path.exists():
|
@@ -216,7 +129,7 @@ class Rekordbox6Database:
|
|
216
129
|
)
|
217
130
|
if not key:
|
218
131
|
try:
|
219
|
-
key =
|
132
|
+
key = rb_config["dp"]
|
220
133
|
except KeyError:
|
221
134
|
raise NoCachedKey(
|
222
135
|
"Could not unlock database: No key found\n"
|
@@ -230,7 +143,7 @@ class Rekordbox6Database:
|
|
230
143
|
if not key.startswith("402fd"):
|
231
144
|
raise ValueError("The provided database key doesn't look valid!")
|
232
145
|
|
233
|
-
logger.
|
146
|
+
logger.debug("Key: %s", key)
|
234
147
|
# Unlock database and create engine
|
235
148
|
url = f"sqlite+pysqlcipher://:{key}@/{path}?"
|
236
149
|
engine = create_engine(url, module=sqlite3)
|
@@ -813,7 +726,9 @@ class Rekordbox6Database:
|
|
813
726
|
|
814
727
|
# -- Database updates --------------------------------------------------------------
|
815
728
|
|
816
|
-
def generate_unused_id(
|
729
|
+
def generate_unused_id(
|
730
|
+
self, table, is_28_bit: bool = True, id_field_name: str = "ID"
|
731
|
+
) -> int:
|
817
732
|
"""Generates an unused ID for the given table."""
|
818
733
|
max_tries = 1000000
|
819
734
|
for _ in range(max_tries):
|
@@ -825,7 +740,8 @@ class Rekordbox6Database:
|
|
825
740
|
if id_ < 100:
|
826
741
|
continue
|
827
742
|
# Check if ID is already used
|
828
|
-
|
743
|
+
id_field = getattr(table, id_field_name)
|
744
|
+
query = self.query(id_field).filter(id_field == id_)
|
829
745
|
used = self.query(query.exists()).scalar()
|
830
746
|
if not used:
|
831
747
|
return id_
|
@@ -1875,6 +1791,80 @@ class Rekordbox6Database:
|
|
1875
1791
|
self.flush()
|
1876
1792
|
return label
|
1877
1793
|
|
1794
|
+
def add_content(self, path, **kwargs):
|
1795
|
+
"""Adds a new track to the database.
|
1796
|
+
|
1797
|
+
Parameters
|
1798
|
+
----------
|
1799
|
+
path : str
|
1800
|
+
Absolute path to the music file to be added.
|
1801
|
+
|
1802
|
+
**kwargs:
|
1803
|
+
Keyword arguments passed to DjmdContent on creation. These arguments
|
1804
|
+
should be a valid DjmdContent field.
|
1805
|
+
|
1806
|
+
Returns
|
1807
|
+
-------
|
1808
|
+
content : DjmdContent
|
1809
|
+
The newly created track.
|
1810
|
+
|
1811
|
+
Raises
|
1812
|
+
------
|
1813
|
+
ValueError : If a track with the same path already exists in the database.
|
1814
|
+
ValueError : If the file type is invalid.
|
1815
|
+
|
1816
|
+
Examples
|
1817
|
+
--------
|
1818
|
+
Add a new track to the database:
|
1819
|
+
|
1820
|
+
>>> db = Rekordbox6Database()
|
1821
|
+
>>> db.add_content("/Users/foo/Downloads/banger.mp3", Title="Banger")
|
1822
|
+
<DjmdContent(123456789 Title=Banger)>
|
1823
|
+
"""
|
1824
|
+
path = Path(path)
|
1825
|
+
path_string = str(path)
|
1826
|
+
query = self.query(tables.DjmdContent).filter_by(FolderPath=path_string)
|
1827
|
+
if query.count() > 0:
|
1828
|
+
raise ValueError(f"Track with path '{path}' already exists in database")
|
1829
|
+
|
1830
|
+
id_ = self.generate_unused_id(tables.DjmdContent)
|
1831
|
+
file_id = self.generate_unused_id(
|
1832
|
+
tables.DjmdContent, id_field_name="rb_file_id"
|
1833
|
+
)
|
1834
|
+
uuid = str(uuid4())
|
1835
|
+
content_link = self.get_menu_items(Name="TRACK").one()
|
1836
|
+
date_created = datetime.date.today()
|
1837
|
+
device = self.get_device().first()
|
1838
|
+
file_name_l = path.name
|
1839
|
+
file_size = path.stat().st_size
|
1840
|
+
|
1841
|
+
file_type_string = path.suffix.lstrip(".").upper()
|
1842
|
+
try:
|
1843
|
+
file_type = getattr(FileType, file_type_string)
|
1844
|
+
except ValueError:
|
1845
|
+
raise ValueError(f"Invalid file type: {path.suffix}")
|
1846
|
+
|
1847
|
+
content = tables.DjmdContent.create(
|
1848
|
+
ID=id_,
|
1849
|
+
UUID=uuid,
|
1850
|
+
ContentLink=content_link.rb_local_usn,
|
1851
|
+
DateCreated=date_created,
|
1852
|
+
DeviceID=device.ID,
|
1853
|
+
FileNameL=file_name_l,
|
1854
|
+
FileSize=file_size,
|
1855
|
+
FileType=file_type.value,
|
1856
|
+
FolderPath=path_string,
|
1857
|
+
HotCueAutoLoad="on",
|
1858
|
+
MasterDBID=device.MasterDBID,
|
1859
|
+
MasterSongID=id_,
|
1860
|
+
StockDate=date_created,
|
1861
|
+
rb_file_id=file_id,
|
1862
|
+
**kwargs,
|
1863
|
+
)
|
1864
|
+
self.add(content)
|
1865
|
+
self.flush()
|
1866
|
+
return content
|
1867
|
+
|
1878
1868
|
# ----------------------------------------------------------------------------------
|
1879
1869
|
|
1880
1870
|
def get_mysetting_paths(self):
|
pyrekordbox/db6/registry.py
CHANGED
pyrekordbox/db6/smartlist.py
CHANGED
@@ -4,14 +4,14 @@
|
|
4
4
|
|
5
5
|
import logging
|
6
6
|
import xml.etree.cElementTree as xml
|
7
|
+
from dataclasses import dataclass
|
8
|
+
from datetime import datetime
|
7
9
|
from enum import Enum, IntEnum
|
8
10
|
from typing import List, Union
|
9
|
-
from datetime import datetime
|
10
|
-
from dataclasses import dataclass
|
11
11
|
|
12
|
-
from sqlalchemy import or_, and_, not_
|
13
|
-
from sqlalchemy.sql.elements import BooleanClauseList
|
14
12
|
from dateutil.relativedelta import relativedelta # noqa
|
13
|
+
from sqlalchemy import and_, not_, or_
|
14
|
+
from sqlalchemy.sql.elements import BooleanClauseList
|
15
15
|
|
16
16
|
from .tables import DjmdContent
|
17
17
|
|
@@ -320,11 +320,12 @@ class SmartList:
|
|
320
320
|
comps = list()
|
321
321
|
for cond in self.conditions:
|
322
322
|
val_left, val_right = _get_condition_values(cond)
|
323
|
-
|
323
|
+
# val_left = str(-abs(int(val_left))) if val_left is not None else ""
|
324
324
|
if cond.property in PROPERTY_COLUMN_MAP:
|
325
325
|
colum_name = PROPERTY_COLUMN_MAP[cond.property]
|
326
326
|
if cond.property == Property.MYTAG:
|
327
|
-
|
327
|
+
if int(val_left) < 0:
|
328
|
+
val_left = str(right_bitshift(int(val_left)))
|
328
329
|
|
329
330
|
if cond.operator == Operator.EQUAL:
|
330
331
|
comp = getattr(DjmdContent, colum_name) == val_left
|