pyrekordbox 0.4.0__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.
- pyrekordbox/_version.py +9 -4
- pyrekordbox/anlz/tags.py +5 -15
- pyrekordbox/config.py +46 -33
- pyrekordbox/db6/database.py +38 -75
- pyrekordbox/db6/smartlist.py +2 -5
- pyrekordbox/db6/tables.py +69 -117
- pyrekordbox/rbxml.py +3 -9
- {pyrekordbox-0.4.0.dist-info → pyrekordbox-0.4.1.dist-info}/METADATA +16 -9
- {pyrekordbox-0.4.0.dist-info → pyrekordbox-0.4.1.dist-info}/RECORD +12 -12
- {pyrekordbox-0.4.0.dist-info → pyrekordbox-0.4.1.dist-info}/WHEEL +1 -1
- {pyrekordbox-0.4.0.dist-info → pyrekordbox-0.4.1.dist-info/licenses}/LICENSE +0 -0
- {pyrekordbox-0.4.0.dist-info → pyrekordbox-0.4.1.dist-info}/top_level.txt +0 -0
pyrekordbox/_version.py
CHANGED
@@ -1,8 +1,13 @@
|
|
1
|
-
# file generated by
|
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
|
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.4.
|
16
|
-
__version_tuple__ = version_tuple = (0, 4,
|
20
|
+
__version__ = version = '0.4.1'
|
21
|
+
__version_tuple__ = version_tuple = (0, 4, 1)
|
pyrekordbox/anlz/tags.py
CHANGED
@@ -162,13 +162,9 @@ class PQTZAnlzTag(AbstractAnlzTag):
|
|
162
162
|
n_bpms = len(bpms)
|
163
163
|
n_times = len(times)
|
164
164
|
if n_bpms != n_beats:
|
165
|
-
raise ValueError(
|
166
|
-
f"Number of bpms not equal to number of beats: {n_bpms} != {n_beats}"
|
167
|
-
)
|
165
|
+
raise ValueError(f"Number of bpms not equal to number of beats: {n_bpms} != {n_beats}")
|
168
166
|
if n_times != n_beats:
|
169
|
-
raise ValueError(
|
170
|
-
f"Number of times not equal to number of beats: {n_bpms} != {n_times}"
|
171
|
-
)
|
167
|
+
raise ValueError(f"Number of times not equal to number of beats: {n_bpms} != {n_times}")
|
172
168
|
|
173
169
|
# For now only values of existing beats can be set
|
174
170
|
if n_beats != n:
|
@@ -184,9 +180,7 @@ class PQTZAnlzTag(AbstractAnlzTag):
|
|
184
180
|
n = len(self.content.entries)
|
185
181
|
n_new = len(beats)
|
186
182
|
if n_new != n:
|
187
|
-
raise ValueError(
|
188
|
-
f"Number of beats not equal to current content length: {n_new} != {n}"
|
189
|
-
)
|
183
|
+
raise ValueError(f"Number of beats not equal to current content length: {n_new} != {n}")
|
190
184
|
|
191
185
|
for i, beat in enumerate(beats):
|
192
186
|
self.content.entries[i].beat = beat
|
@@ -195,9 +189,7 @@ class PQTZAnlzTag(AbstractAnlzTag):
|
|
195
189
|
n = len(self.content.entries)
|
196
190
|
n_new = len(bpms)
|
197
191
|
if n_new != n:
|
198
|
-
raise ValueError(
|
199
|
-
f"Number of bpms not equal to current content length: {n_new} != {n}"
|
200
|
-
)
|
192
|
+
raise ValueError(f"Number of bpms not equal to current content length: {n_new} != {n}")
|
201
193
|
|
202
194
|
for i, bpm in enumerate(bpms):
|
203
195
|
self.content.entries[i].tempo = int(bpm * 100)
|
@@ -206,9 +198,7 @@ class PQTZAnlzTag(AbstractAnlzTag):
|
|
206
198
|
n = len(self.content.entries)
|
207
199
|
n_new = len(times)
|
208
200
|
if n_new != n:
|
209
|
-
raise ValueError(
|
210
|
-
f"Number of times not equal to current content length: {n_new} != {n}"
|
211
|
-
)
|
201
|
+
raise ValueError(f"Number of times not equal to current content length: {n_new} != {n}")
|
212
202
|
|
213
203
|
for i, t in enumerate(times):
|
214
204
|
self.content.entries[i].time = int(1000 * t)
|
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
|
-
|
33
|
+
_cache_file_name = "rb.cache"
|
34
34
|
|
35
35
|
# Define empty pyrekordbox configuration
|
36
36
|
__config__ = {
|
@@ -48,6 +48,25 @@ class InvalidApplicationDirname(Exception):
|
|
48
48
|
pass
|
49
49
|
|
50
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
|
+
|
51
70
|
def get_pioneer_install_dir(path: Union[str, Path] = None) -> Path: # pragma: no cover
|
52
71
|
"""Returns the path of the Pioneer program installation directory.
|
53
72
|
|
@@ -234,12 +253,7 @@ def read_rekordbox6_asar(rb6_install_dir: Union[str, Path]) -> str:
|
|
234
253
|
if not str(rb6_install_dir).endswith(".app"):
|
235
254
|
rb6_install_dir = rb6_install_dir / "rekordbox.app"
|
236
255
|
location = (
|
237
|
-
rb6_install_dir
|
238
|
-
/ "Contents"
|
239
|
-
/ "MacOS"
|
240
|
-
/ "rekordboxAgent.app"
|
241
|
-
/ "Contents"
|
242
|
-
/ "Resources"
|
256
|
+
rb6_install_dir / "Contents" / "MacOS" / "rekordboxAgent.app" / "Contents" / "Resources"
|
243
257
|
)
|
244
258
|
encoding = "cp437"
|
245
259
|
else:
|
@@ -249,7 +263,7 @@ def read_rekordbox6_asar(rb6_install_dir: Union[str, Path]) -> str:
|
|
249
263
|
# Read asar file
|
250
264
|
path = (location / "app.asar").absolute()
|
251
265
|
with open(path, "rb") as fh:
|
252
|
-
data = fh.read().decode(encoding)
|
266
|
+
data = fh.read().decode(encoding, errors="replace")
|
253
267
|
return data
|
254
268
|
|
255
269
|
|
@@ -360,9 +374,7 @@ def _get_rb_config(
|
|
360
374
|
return conf
|
361
375
|
|
362
376
|
|
363
|
-
def _get_rb5_config(
|
364
|
-
pioneer_prog_dir: Path, pioneer_app_dir: Path, dirname: str = ""
|
365
|
-
) -> dict:
|
377
|
+
def _get_rb5_config(pioneer_prog_dir: Path, pioneer_app_dir: Path, dirname: str = "") -> dict:
|
366
378
|
"""Get the program configuration for Rekordbox v5.x.x."""
|
367
379
|
major_version = 5
|
368
380
|
conf = _get_rb_config(pioneer_prog_dir, pioneer_app_dir, major_version, dirname)
|
@@ -414,8 +426,7 @@ class KeyExtractor:
|
|
414
426
|
pid = get_rekordbox_pid()
|
415
427
|
if pid:
|
416
428
|
raise RuntimeError(
|
417
|
-
"Rekordbox is running. "
|
418
|
-
"Please close Rekordbox before running the `KeyExtractor`."
|
429
|
+
"Rekordbox is running. Please close Rekordbox before running the `KeyExtractor`."
|
419
430
|
)
|
420
431
|
# Spawn Rekordbox process and attach to it
|
421
432
|
pid = frida.spawn(self.executable)
|
@@ -434,12 +445,16 @@ class KeyExtractor:
|
|
434
445
|
|
435
446
|
|
436
447
|
def write_db6_key_cache(key: str) -> None: # pragma: no cover
|
437
|
-
"""Writes the decrypted Rekordbox6 database key to the cache file.
|
448
|
+
r"""Writes the decrypted Rekordbox6 database key to the cache file.
|
438
449
|
|
439
450
|
This method can also be used to manually cache the database key, provided
|
440
451
|
the user has found the key somewhere else. The key can be, for example,
|
441
452
|
found in some other projects that hard-coded it.
|
442
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
|
+
|
443
458
|
Parameters
|
444
459
|
----------
|
445
460
|
key : str
|
@@ -461,7 +476,12 @@ def write_db6_key_cache(key: str) -> None: # pragma: no cover
|
|
461
476
|
lines.append(f"version: {_cache_file_version}")
|
462
477
|
lines.append("dp: " + key)
|
463
478
|
text = "\n".join(lines)
|
464
|
-
|
479
|
+
|
480
|
+
cache_file = get_appdata_dir() / "pyrekordbox" / _cache_file_name
|
481
|
+
if not cache_file.parent.exists():
|
482
|
+
cache_file.parent.mkdir()
|
483
|
+
|
484
|
+
with open(cache_file, "w") as fh:
|
465
485
|
fh.write(text)
|
466
486
|
# Set the config key to make sure the key is present after calling method
|
467
487
|
if __config__["rekordbox6"]:
|
@@ -473,10 +493,13 @@ def write_db6_key_cache(key: str) -> None: # pragma: no cover
|
|
473
493
|
def _update_sqlite_key(opts, conf):
|
474
494
|
cache_version = 0
|
475
495
|
pw, dp = "", ""
|
476
|
-
|
477
|
-
|
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)
|
478
501
|
# Read cache file
|
479
|
-
with open(
|
502
|
+
with open(cache_file, "r") as fh:
|
480
503
|
text = fh.read()
|
481
504
|
lines = text.splitlines()
|
482
505
|
if lines[0].startswith("version:"):
|
@@ -551,9 +574,7 @@ def _update_sqlite_key(opts, conf):
|
|
551
574
|
conf["dp"] = dp
|
552
575
|
|
553
576
|
|
554
|
-
def _get_rb6_config(
|
555
|
-
pioneer_prog_dir: Path, pioneer_app_dir: Path, dirname: str = ""
|
556
|
-
) -> dict:
|
577
|
+
def _get_rb6_config(pioneer_prog_dir: Path, pioneer_app_dir: Path, dirname: str = "") -> dict:
|
557
578
|
"""Get the program configuration for Rekordbox v6.x.x."""
|
558
579
|
major_version = 6
|
559
580
|
conf = _get_rb_config(pioneer_prog_dir, pioneer_app_dir, major_version, dirname)
|
@@ -571,9 +592,7 @@ def _get_rb6_config(
|
|
571
592
|
return conf
|
572
593
|
|
573
594
|
|
574
|
-
def _get_rb7_config(
|
575
|
-
pioneer_prog_dir: Path, pioneer_app_dir: Path, dirname: str = ""
|
576
|
-
) -> dict:
|
595
|
+
def _get_rb7_config(pioneer_prog_dir: Path, pioneer_app_dir: Path, dirname: str = "") -> dict:
|
577
596
|
"""Get the program configuration for Rekordbox v7.x.x."""
|
578
597
|
major_version = 7
|
579
598
|
conf = _get_rb_config(pioneer_prog_dir, pioneer_app_dir, major_version, dirname)
|
@@ -715,27 +734,21 @@ def update_config(
|
|
715
734
|
|
716
735
|
# Update Rekordbox 5 config
|
717
736
|
try:
|
718
|
-
conf = _get_rb5_config(
|
719
|
-
pioneer_install_dir, pioneer_app_dir, rb5_install_dirname
|
720
|
-
)
|
737
|
+
conf = _get_rb5_config(pioneer_install_dir, pioneer_app_dir, rb5_install_dirname)
|
721
738
|
__config__["rekordbox5"].update(conf)
|
722
739
|
except FileNotFoundError as e:
|
723
740
|
logger.info(e)
|
724
741
|
|
725
742
|
# Update Rekordbox 6 config
|
726
743
|
try:
|
727
|
-
conf = _get_rb6_config(
|
728
|
-
pioneer_install_dir, pioneer_app_dir, rb6_install_dirname
|
729
|
-
)
|
744
|
+
conf = _get_rb6_config(pioneer_install_dir, pioneer_app_dir, rb6_install_dirname)
|
730
745
|
__config__["rekordbox6"].update(conf)
|
731
746
|
except FileNotFoundError as e:
|
732
747
|
logger.info(e)
|
733
748
|
|
734
749
|
# Update Rekordbox 7 config
|
735
750
|
try:
|
736
|
-
conf = _get_rb7_config(
|
737
|
-
pioneer_install_dir, pioneer_app_dir, rb7_install_dirname
|
738
|
-
)
|
751
|
+
conf = _get_rb7_config(pioneer_install_dir, pioneer_app_dir, rb7_install_dirname)
|
739
752
|
__config__["rekordbox7"].update(conf)
|
740
753
|
except FileNotFoundError as e:
|
741
754
|
logger.info(e)
|
pyrekordbox/db6/database.py
CHANGED
@@ -33,6 +33,10 @@ except ImportError:
|
|
33
33
|
_sqlcipher_available = False
|
34
34
|
|
35
35
|
MAX_VERSION = "6.6.5"
|
36
|
+
SPECIAL_PLAYLIST_IDS = [
|
37
|
+
"100000", # Cloud Library Sync
|
38
|
+
"200000", # CUE Analysis Playlist
|
39
|
+
]
|
36
40
|
|
37
41
|
logger = logging.getLogger(__name__)
|
38
42
|
|
@@ -114,9 +118,7 @@ class Rekordbox6Database:
|
|
114
118
|
path = rb_config.get("db_path", "")
|
115
119
|
if not path:
|
116
120
|
pdir = get_config("pioneer", "install_dir")
|
117
|
-
raise FileNotFoundError(
|
118
|
-
f"No Rekordbox v6/v7 directory found in '{pdir}'"
|
119
|
-
)
|
121
|
+
raise FileNotFoundError(f"No Rekordbox v6/v7 directory found in '{pdir}'")
|
120
122
|
path = Path(path)
|
121
123
|
# make sure file exists
|
122
124
|
if not path.exists():
|
@@ -124,9 +126,7 @@ class Rekordbox6Database:
|
|
124
126
|
# Open database
|
125
127
|
if unlock:
|
126
128
|
if not _sqlcipher_available:
|
127
|
-
raise ImportError(
|
128
|
-
"Could not unlock database: 'sqlcipher3' package not found"
|
129
|
-
)
|
129
|
+
raise ImportError("Could not unlock database: 'sqlcipher3' package not found")
|
130
130
|
if not key:
|
131
131
|
try:
|
132
132
|
key = rb_config["dp"]
|
@@ -404,17 +404,20 @@ class Rekordbox6Database:
|
|
404
404
|
# Sync the updated_at values of the playlists in the DB and the XML file
|
405
405
|
for pl in self.get_playlist():
|
406
406
|
plxml = self.playlist_xml.get(pl.ID)
|
407
|
-
if plxml is None:
|
408
|
-
|
409
|
-
|
410
|
-
|
411
|
-
"
|
412
|
-
|
413
|
-
|
414
|
-
|
415
|
-
|
416
|
-
|
417
|
-
|
407
|
+
if plxml is not None:
|
408
|
+
ts = plxml["Timestamp"]
|
409
|
+
diff = pl.updated_at - ts
|
410
|
+
if abs(diff.total_seconds()) > 1:
|
411
|
+
logger.debug("Updating updated_at of playlist %s in XML", pl.ID)
|
412
|
+
self.playlist_xml.update(pl.ID, updated_at=pl.updated_at)
|
413
|
+
else:
|
414
|
+
# Dont warn for special playlists
|
415
|
+
if pl.ID not in SPECIAL_PLAYLIST_IDS:
|
416
|
+
logger.warning(
|
417
|
+
f"Playlist {pl.ID} not found in masterPlaylists6.xml! "
|
418
|
+
"Did you add it manually? "
|
419
|
+
"Use the create_playlist method instead."
|
420
|
+
)
|
418
421
|
|
419
422
|
# Save the XML file if it was modified
|
420
423
|
if self.playlist_xml.modified:
|
@@ -726,9 +729,7 @@ class Rekordbox6Database:
|
|
726
729
|
|
727
730
|
# -- Database updates --------------------------------------------------------------
|
728
731
|
|
729
|
-
def generate_unused_id(
|
730
|
-
self, table, is_28_bit: bool = True, id_field_name: str = "ID"
|
731
|
-
) -> int:
|
732
|
+
def generate_unused_id(self, table, is_28_bit: bool = True, id_field_name: str = "ID") -> int:
|
732
733
|
"""Generates an unused ID for the given table."""
|
733
734
|
max_tries = 1000000
|
734
735
|
for _ in range(max_tries):
|
@@ -803,20 +804,14 @@ class Rekordbox6Database:
|
|
803
804
|
uuid = str(uuid4())
|
804
805
|
id_ = str(uuid4())
|
805
806
|
now = datetime.datetime.now()
|
806
|
-
nsongs = (
|
807
|
-
self.query(tables.DjmdSongPlaylist)
|
808
|
-
.filter_by(PlaylistID=playlist.ID)
|
809
|
-
.count()
|
810
|
-
)
|
807
|
+
nsongs = self.query(tables.DjmdSongPlaylist).filter_by(PlaylistID=playlist.ID).count()
|
811
808
|
if track_no is not None:
|
812
809
|
insert_at_end = False
|
813
810
|
track_no = int(track_no)
|
814
811
|
if track_no < 1:
|
815
812
|
raise ValueError("Track number must be greater than 0")
|
816
813
|
if track_no > nsongs + 1:
|
817
|
-
raise ValueError(
|
818
|
-
f"Track number too high, parent contains {nsongs} items"
|
819
|
-
)
|
814
|
+
raise ValueError(f"Track number too high, parent contains {nsongs} items")
|
820
815
|
else:
|
821
816
|
insert_at_end = True
|
822
817
|
track_no = nsongs + 1
|
@@ -892,9 +887,7 @@ class Rekordbox6Database:
|
|
892
887
|
playlist = self.get_playlist(ID=playlist)
|
893
888
|
if isinstance(song, (int, str)):
|
894
889
|
song = self.query(tables.DjmdSongPlaylist).filter_by(ID=song).one()
|
895
|
-
logger.info(
|
896
|
-
"Removing song with ID=%s from playlist with ID=%s", song.ID, playlist.ID
|
897
|
-
)
|
890
|
+
logger.info("Removing song with ID=%s from playlist with ID=%s", song.ID, playlist.ID)
|
898
891
|
now = datetime.datetime.now()
|
899
892
|
# Remove track from playlist
|
900
893
|
track_no = song.TrackNo
|
@@ -965,11 +958,7 @@ class Rekordbox6Database:
|
|
965
958
|
playlist = self.get_playlist(ID=playlist)
|
966
959
|
if isinstance(song, (int, str)):
|
967
960
|
song = self.query(tables.DjmdSongPlaylist).filter_by(ID=song).one()
|
968
|
-
nsongs = (
|
969
|
-
self.query(tables.DjmdSongPlaylist)
|
970
|
-
.filter_by(PlaylistID=playlist.ID)
|
971
|
-
.count()
|
972
|
-
)
|
961
|
+
nsongs = self.query(tables.DjmdSongPlaylist).filter_by(PlaylistID=playlist.ID).count()
|
973
962
|
if new_track_no < 1:
|
974
963
|
raise ValueError("Track number must be greater than 0")
|
975
964
|
if new_track_no > nsongs + 1:
|
@@ -1019,9 +1008,7 @@ class Rekordbox6Database:
|
|
1019
1008
|
self.registry.enable_tracking()
|
1020
1009
|
self.registry.on_move(moved)
|
1021
1010
|
|
1022
|
-
def _create_playlist(
|
1023
|
-
self, name, seq, image_path, parent, smart_list=None, attribute=None
|
1024
|
-
):
|
1011
|
+
def _create_playlist(self, name, seq, image_path, parent, smart_list=None, attribute=None):
|
1025
1012
|
"""Creates a new playlist object."""
|
1026
1013
|
table = tables.DjmdPlaylist
|
1027
1014
|
id_ = str(self.generate_unused_id(table, is_28_bit=True))
|
@@ -1046,9 +1033,7 @@ class Rekordbox6Database:
|
|
1046
1033
|
else:
|
1047
1034
|
# Check if parent exists and is a folder
|
1048
1035
|
parent_id = parent
|
1049
|
-
query = self.query(table.ID).filter(
|
1050
|
-
table.ID == parent_id, table.Attribute == 1
|
1051
|
-
)
|
1036
|
+
query = self.query(table.ID).filter(table.ID == parent_id, table.Attribute == 1)
|
1052
1037
|
if not self.query(query.exists()).scalar():
|
1053
1038
|
raise ValueError("Parent does not exist or is not a folder")
|
1054
1039
|
|
@@ -1107,9 +1092,7 @@ class Rekordbox6Database:
|
|
1107
1092
|
|
1108
1093
|
# Update masterPlaylists6.xml
|
1109
1094
|
if self.playlist_xml is not None:
|
1110
|
-
self.playlist_xml.add(
|
1111
|
-
id_, parent_id, attribute, now, lib_type=0, check_type=0
|
1112
|
-
)
|
1095
|
+
self.playlist_xml.add(id_, parent_id, attribute, now, lib_type=0, check_type=0)
|
1113
1096
|
|
1114
1097
|
return playlist
|
1115
1098
|
|
@@ -1157,9 +1140,7 @@ class Rekordbox6Database:
|
|
1157
1140
|
'123456'
|
1158
1141
|
"""
|
1159
1142
|
logger.info("Creating playlist %s", name)
|
1160
|
-
return self._create_playlist(
|
1161
|
-
name, seq, image_path, parent, attribute=PlaylistType.PLAYLIST
|
1162
|
-
)
|
1143
|
+
return self._create_playlist(name, seq, image_path, parent, attribute=PlaylistType.PLAYLIST)
|
1163
1144
|
|
1164
1145
|
def create_playlist_folder(self, name, parent=None, seq=None, image_path=None):
|
1165
1146
|
"""Creates a new playlist folder in the database.
|
@@ -1199,9 +1180,7 @@ class Rekordbox6Database:
|
|
1199
1180
|
'123456'
|
1200
1181
|
"""
|
1201
1182
|
logger.info("Creating playlist folder %s", name)
|
1202
|
-
return self._create_playlist(
|
1203
|
-
name, seq, image_path, parent, attribute=PlaylistType.FOLDER
|
1204
|
-
)
|
1183
|
+
return self._create_playlist(name, seq, image_path, parent, attribute=PlaylistType.FOLDER)
|
1205
1184
|
|
1206
1185
|
def create_smart_playlist(
|
1207
1186
|
self, name, smart_list: SmartList, parent=None, seq=None, image_path=None
|
@@ -1276,9 +1255,7 @@ class Rekordbox6Database:
|
|
1276
1255
|
playlist = self.get_playlist(ID=playlist)
|
1277
1256
|
|
1278
1257
|
if playlist.Attribute == 1:
|
1279
|
-
logger.info(
|
1280
|
-
"Deleting playlist folder '%s' with ID=%s", playlist.Name, playlist.ID
|
1281
|
-
)
|
1258
|
+
logger.info("Deleting playlist folder '%s' with ID=%s", playlist.Name, playlist.ID)
|
1282
1259
|
else:
|
1283
1260
|
logger.info("Deleting playlist '%s' with ID=%s", playlist.Name, playlist.ID)
|
1284
1261
|
|
@@ -1391,9 +1368,7 @@ class Rekordbox6Database:
|
|
1391
1368
|
else:
|
1392
1369
|
# Check if parent exists and is a folder
|
1393
1370
|
parent_id = str(parent)
|
1394
|
-
query = self.query(table.ID).filter(
|
1395
|
-
table.ID == parent_id, table.Attribute == 1
|
1396
|
-
)
|
1371
|
+
query = self.query(table.ID).filter(table.ID == parent_id, table.Attribute == 1)
|
1397
1372
|
if not self.query(query.exists()).scalar():
|
1398
1373
|
raise ValueError("Parent does not exist or is not a folder")
|
1399
1374
|
|
@@ -1414,9 +1389,7 @@ class Rekordbox6Database:
|
|
1414
1389
|
if seq < 1:
|
1415
1390
|
raise ValueError("Sequence number must be greater than 0")
|
1416
1391
|
elif seq > n + 1:
|
1417
|
-
raise ValueError(
|
1418
|
-
f"Sequence number too high, parent contains {n} items"
|
1419
|
-
)
|
1392
|
+
raise ValueError(f"Sequence number too high, parent contains {n} items")
|
1420
1393
|
|
1421
1394
|
if not insert_at_end:
|
1422
1395
|
# Get all playlists with seq between old_seq and seq
|
@@ -1550,9 +1523,7 @@ class Rekordbox6Database:
|
|
1550
1523
|
with self.registry.disabled():
|
1551
1524
|
playlist.updated_at = now
|
1552
1525
|
|
1553
|
-
def add_album(
|
1554
|
-
self, name, artist=None, image_path=None, compilation=None, search_str=None
|
1555
|
-
):
|
1526
|
+
def add_album(self, name, artist=None, image_path=None, compilation=None, search_str=None):
|
1556
1527
|
"""Adds a new album to the database.
|
1557
1528
|
|
1558
1529
|
Parameters
|
@@ -1684,9 +1655,7 @@ class Rekordbox6Database:
|
|
1684
1655
|
|
1685
1656
|
id_ = self.generate_unused_id(tables.DjmdArtist)
|
1686
1657
|
uuid = str(uuid4())
|
1687
|
-
artist = tables.DjmdArtist.create(
|
1688
|
-
ID=id_, Name=name, SearchStr=search_str, UUID=uuid
|
1689
|
-
)
|
1658
|
+
artist = tables.DjmdArtist.create(ID=id_, Name=name, SearchStr=search_str, UUID=uuid)
|
1690
1659
|
self.add(artist)
|
1691
1660
|
self.flush()
|
1692
1661
|
return artist
|
@@ -1828,9 +1797,7 @@ class Rekordbox6Database:
|
|
1828
1797
|
raise ValueError(f"Track with path '{path}' already exists in database")
|
1829
1798
|
|
1830
1799
|
id_ = self.generate_unused_id(tables.DjmdContent)
|
1831
|
-
file_id = self.generate_unused_id(
|
1832
|
-
tables.DjmdContent, id_field_name="rb_file_id"
|
1833
|
-
)
|
1800
|
+
file_id = self.generate_unused_id(tables.DjmdContent, id_field_name="rb_file_id")
|
1834
1801
|
uuid = str(uuid4())
|
1835
1802
|
content_link = self.get_menu_items(Name="TRACK").one()
|
1836
1803
|
date_created = datetime.date.today()
|
@@ -1987,9 +1954,7 @@ class Rekordbox6Database:
|
|
1987
1954
|
return AnlzFile.parse_file(path)
|
1988
1955
|
return None
|
1989
1956
|
|
1990
|
-
def update_content_path(
|
1991
|
-
self, content, path, save=True, check_path=True, commit=True
|
1992
|
-
):
|
1957
|
+
def update_content_path(self, content, path, save=True, check_path=True, commit=True):
|
1993
1958
|
"""Update the file path of a track in the Rekordbox v6 database.
|
1994
1959
|
|
1995
1960
|
This changes the `FolderPath` entry in the ``DjmdContent`` table and the
|
@@ -2080,9 +2045,7 @@ class Rekordbox6Database:
|
|
2080
2045
|
logger.debug("Committing changes to the database")
|
2081
2046
|
self.commit()
|
2082
2047
|
|
2083
|
-
def update_content_filename(
|
2084
|
-
self, content, name, save=True, check_path=True, commit=True
|
2085
|
-
):
|
2048
|
+
def update_content_filename(self, content, name, save=True, check_path=True, commit=True):
|
2086
2049
|
"""Update the file name of a track in the Rekordbox v6 database.
|
2087
2050
|
|
2088
2051
|
This changes the `FolderPath` entry in the ``DjmdContent`` table and the
|
pyrekordbox/db6/smartlist.py
CHANGED
@@ -179,8 +179,7 @@ class Condition:
|
|
179
179
|
def __post_init__(self):
|
180
180
|
if self.property not in PROPERTIES:
|
181
181
|
raise ValueError(
|
182
|
-
f"Invalid property: '{self.property}'! "
|
183
|
-
f"Supported properties: {PROPERTIES}"
|
182
|
+
f"Invalid property: '{self.property}'! Supported properties: {PROPERTIES}"
|
184
183
|
)
|
185
184
|
|
186
185
|
valid_ops = VALID_OPS[self.property]
|
@@ -232,9 +231,7 @@ def _get_condition_values(cond):
|
|
232
231
|
class SmartList:
|
233
232
|
"""Rekordbox smart playlist XML handler."""
|
234
233
|
|
235
|
-
def __init__(
|
236
|
-
self, logical_operator: int = LogicalOperator.ALL, auto_update: int = 0
|
237
|
-
):
|
234
|
+
def __init__(self, logical_operator: int = LogicalOperator.ALL, auto_update: int = 0):
|
238
235
|
self.playlist_id: Union[int, str] = ""
|
239
236
|
self.logical_operator: int = int(logical_operator)
|
240
237
|
self.auto_update: int = auto_update
|
pyrekordbox/db6/tables.py
CHANGED
@@ -115,6 +115,37 @@ TABLES = [
|
|
115
115
|
]
|
116
116
|
|
117
117
|
|
118
|
+
def datetime_to_str(value: datetime) -> str:
|
119
|
+
s = value.isoformat().replace("T", " ")
|
120
|
+
if value.tzinfo is not None:
|
121
|
+
# Get the timezone info (last 6 characters of the string)
|
122
|
+
tzinfo = s[-6:]
|
123
|
+
s = s[:-9] + " " + tzinfo
|
124
|
+
else:
|
125
|
+
s = s[:-3] + " +00:00"
|
126
|
+
return s
|
127
|
+
|
128
|
+
|
129
|
+
def string_to_datetime(value: str) -> datetime:
|
130
|
+
try:
|
131
|
+
dt = datetime.fromisoformat(value)
|
132
|
+
except ValueError:
|
133
|
+
if len(value.strip()) > 23:
|
134
|
+
# Assume the format
|
135
|
+
# "2025-04-12 19:11:29.274 -05:00" or
|
136
|
+
# "2025-04-12 19:11:29.274 -05:00 (Central Daylight Time)"
|
137
|
+
datestr, tzinfo = value[:23], value[23:30]
|
138
|
+
datestr = datestr.strip()
|
139
|
+
tzinfo = tzinfo.strip()
|
140
|
+
assert re.match(r"^[+-]?\d{1,2}:\d{2}", tzinfo)
|
141
|
+
datestr = datestr.strip() + tzinfo
|
142
|
+
else:
|
143
|
+
datestr, tzinfo = value, ""
|
144
|
+
dt = datetime.fromisoformat(datestr)
|
145
|
+
# Convert to local timezone and return without timezone
|
146
|
+
return dt.astimezone().replace(tzinfo=None)
|
147
|
+
|
148
|
+
|
118
149
|
class DateTime(TypeDecorator):
|
119
150
|
"""Custom datetime column with timezone support.
|
120
151
|
|
@@ -125,23 +156,12 @@ class DateTime(TypeDecorator):
|
|
125
156
|
impl = Text
|
126
157
|
cache_ok = True
|
127
158
|
|
128
|
-
def process_bind_param(self, value, dialect):
|
129
|
-
return value
|
159
|
+
def process_bind_param(self, value: datetime, dialect) -> str:
|
160
|
+
return datetime_to_str(value)
|
130
161
|
|
131
|
-
def process_result_value(self, value, dialect):
|
162
|
+
def process_result_value(self, value: str, dialect):
|
132
163
|
if value:
|
133
|
-
|
134
|
-
dt = datetime.fromisoformat(value)
|
135
|
-
except ValueError:
|
136
|
-
if len(value.strip()) > 23:
|
137
|
-
datestr, tzinfo = value[:23], value[23:]
|
138
|
-
datestr = datestr.strip()
|
139
|
-
tzinfo = tzinfo.strip()
|
140
|
-
assert re.match(r"^\+\d{2}:\d{2}$", tzinfo)
|
141
|
-
else:
|
142
|
-
datestr, tzinfo = value, ""
|
143
|
-
dt = datetime.fromisoformat(datestr)
|
144
|
-
return dt
|
164
|
+
return string_to_datetime(value)
|
145
165
|
return None
|
146
166
|
|
147
167
|
|
@@ -240,9 +260,7 @@ class Base(DeclarativeBase):
|
|
240
260
|
class StatsTime:
|
241
261
|
"""Mixin class for tables that only use time statistics columns."""
|
242
262
|
|
243
|
-
created_at: Mapped[datetime] = mapped_column(
|
244
|
-
DateTime, nullable=False, default=datetime.now
|
245
|
-
)
|
263
|
+
created_at: Mapped[datetime] = mapped_column(DateTime, nullable=False, default=datetime.now)
|
246
264
|
"""The creation date of the table entry (from :class:`StatsTime`)."""
|
247
265
|
updated_at: Mapped[datetime] = mapped_column(
|
248
266
|
DateTime, nullable=False, default=datetime.now, onupdate=datetime.now
|
@@ -271,9 +289,7 @@ class StatsFull:
|
|
271
289
|
rb_local_usn: Mapped[int] = mapped_column(BigInteger, default=None)
|
272
290
|
"""The local USN (unique sequence number) of the table entry
|
273
291
|
(from :class:`StatsFull`)."""
|
274
|
-
created_at: Mapped[datetime] = mapped_column(
|
275
|
-
DateTime, nullable=False, default=datetime.now
|
276
|
-
)
|
292
|
+
created_at: Mapped[datetime] = mapped_column(DateTime, nullable=False, default=datetime.now)
|
277
293
|
"""The creation date of the table entry (from :class:`StatsFull`)."""
|
278
294
|
updated_at: Mapped[datetime] = mapped_column(
|
279
295
|
DateTime, nullable=False, default=datetime.now, onupdate=datetime.now
|
@@ -371,9 +387,7 @@ class ContentActiveCensor(Base, StatsFull):
|
|
371
387
|
|
372
388
|
ID: Mapped[str] = mapped_column(VARCHAR(255), primary_key=True)
|
373
389
|
"""The ID (primary key) of the table entry."""
|
374
|
-
ContentID: Mapped[str] = mapped_column(
|
375
|
-
VARCHAR(255), ForeignKey("djmdContent.ID"), default=None
|
376
|
-
)
|
390
|
+
ContentID: Mapped[str] = mapped_column(VARCHAR(255), ForeignKey("djmdContent.ID"), default=None)
|
377
391
|
"""The ID of the :class:`DjmdContent` entry this censor belongs to."""
|
378
392
|
ActiveCensors: Mapped[str] = mapped_column(Text, default=None)
|
379
393
|
"""The active censors of the table entry."""
|
@@ -396,9 +410,7 @@ class ContentCue(Base, StatsFull):
|
|
396
410
|
|
397
411
|
ID: Mapped[str] = mapped_column(VARCHAR(255), primary_key=True)
|
398
412
|
"""The ID (primary key) of the table entry."""
|
399
|
-
ContentID: Mapped[str] = mapped_column(
|
400
|
-
VARCHAR(255), ForeignKey("djmdContent.ID"), default=None
|
401
|
-
)
|
413
|
+
ContentID: Mapped[str] = mapped_column(VARCHAR(255), ForeignKey("djmdContent.ID"), default=None)
|
402
414
|
"""The ID of the :class:`DjmdContent` entry this cue belongs to."""
|
403
415
|
Cues: Mapped[str] = mapped_column(Text, default=None)
|
404
416
|
"""The cues of the table entry."""
|
@@ -421,9 +433,7 @@ class ContentFile(Base, StatsFull):
|
|
421
433
|
|
422
434
|
ID: Mapped[str] = mapped_column(VARCHAR(255), primary_key=True)
|
423
435
|
"""The ID (primary key) of the table entry."""
|
424
|
-
ContentID: Mapped[str] = mapped_column(
|
425
|
-
VARCHAR(255), ForeignKey("djmdContent.ID"), default=None
|
426
|
-
)
|
436
|
+
ContentID: Mapped[str] = mapped_column(VARCHAR(255), ForeignKey("djmdContent.ID"), default=None)
|
427
437
|
"""The ID of the :class:`DjmdContent` entry this file belongs to."""
|
428
438
|
Path: Mapped[str] = mapped_column(VARCHAR(255), default=None)
|
429
439
|
"""The path of the file."""
|
@@ -468,9 +478,7 @@ class DjmdActiveCensor(Base, StatsFull):
|
|
468
478
|
|
469
479
|
ID: Mapped[str] = mapped_column(VARCHAR(255), primary_key=True)
|
470
480
|
"""The ID (primary key) of the table entry."""
|
471
|
-
ContentID: Mapped[str] = mapped_column(
|
472
|
-
VARCHAR(255), ForeignKey("djmdContent.ID"), default=None
|
473
|
-
)
|
481
|
+
ContentID: Mapped[str] = mapped_column(VARCHAR(255), ForeignKey("djmdContent.ID"), default=None)
|
474
482
|
"""The ID of the :class:`DjmdContent` entry this censor belongs to."""
|
475
483
|
InMsec: Mapped[int] = mapped_column(Integer, default=None)
|
476
484
|
"""The in time of the censor (in milliseconds)."""
|
@@ -483,9 +491,7 @@ class DjmdActiveCensor(Base, StatsFull):
|
|
483
491
|
ContentUUID: Mapped[str] = mapped_column(VARCHAR(255), default=None)
|
484
492
|
"""The UUID of the :class:`DjmdContent` entry this censor belongs to."""
|
485
493
|
|
486
|
-
Content = relationship(
|
487
|
-
"DjmdContent", foreign_keys=ContentID, back_populates="ActiveCensors"
|
488
|
-
)
|
494
|
+
Content = relationship("DjmdContent", foreign_keys=ContentID, back_populates="ActiveCensors")
|
489
495
|
"""The content entry this censor belongs to (links to :class:`DjmdContent`)."""
|
490
496
|
|
491
497
|
|
@@ -616,17 +622,11 @@ class DjmdContent(Base, StatsFull):
|
|
616
622
|
"""The short file name of the file corresponding to the content entry."""
|
617
623
|
Title: Mapped[str] = mapped_column(VARCHAR(255), default=None)
|
618
624
|
"""The title of the track."""
|
619
|
-
ArtistID: Mapped[str] = mapped_column(
|
620
|
-
VARCHAR(255), ForeignKey("djmdArtist.ID"), default=None
|
621
|
-
)
|
625
|
+
ArtistID: Mapped[str] = mapped_column(VARCHAR(255), ForeignKey("djmdArtist.ID"), default=None)
|
622
626
|
"""The ID of the :class:`DjmdArtist` entry of the artist of this track."""
|
623
|
-
AlbumID: Mapped[str] = mapped_column(
|
624
|
-
VARCHAR(255), ForeignKey("djmdAlbum.ID"), default=None
|
625
|
-
)
|
627
|
+
AlbumID: Mapped[str] = mapped_column(VARCHAR(255), ForeignKey("djmdAlbum.ID"), default=None)
|
626
628
|
"""The ID of the :class:`DjmdAlbum` entry of the album of this track."""
|
627
|
-
GenreID: Mapped[str] = mapped_column(
|
628
|
-
VARCHAR(255), ForeignKey("djmdGenre.ID"), default=None
|
629
|
-
)
|
629
|
+
GenreID: Mapped[str] = mapped_column(VARCHAR(255), ForeignKey("djmdGenre.ID"), default=None)
|
630
630
|
"""The ID of the :class:`DjmdGenre` entry of the genre of this track."""
|
631
631
|
BPM: Mapped[int] = mapped_column(Integer, default=None)
|
632
632
|
"""The BPM (beats per minute) of the track."""
|
@@ -646,27 +646,19 @@ class DjmdContent(Base, StatsFull):
|
|
646
646
|
"""The rating of the track."""
|
647
647
|
ReleaseYear: Mapped[int] = mapped_column(Integer, default=None)
|
648
648
|
"""The release year of the track."""
|
649
|
-
RemixerID: Mapped[str] = mapped_column(
|
650
|
-
VARCHAR(255), ForeignKey("djmdArtist.ID"), default=None
|
651
|
-
)
|
649
|
+
RemixerID: Mapped[str] = mapped_column(VARCHAR(255), ForeignKey("djmdArtist.ID"), default=None)
|
652
650
|
"""The ID of the :class:`DjmdArtist` entry of the remixer of this track."""
|
653
|
-
LabelID: Mapped[str] = mapped_column(
|
654
|
-
VARCHAR(255), ForeignKey("djmdLabel.ID"), default=None
|
655
|
-
)
|
651
|
+
LabelID: Mapped[str] = mapped_column(VARCHAR(255), ForeignKey("djmdLabel.ID"), default=None)
|
656
652
|
"""The ID of the :class:`DjmdLabel` entry of the label of this track."""
|
657
653
|
OrgArtistID: Mapped[str] = mapped_column(
|
658
654
|
VARCHAR(255), ForeignKey("djmdArtist.ID"), default=None
|
659
655
|
)
|
660
656
|
"""The ID of the :class:`DjmdArtist` entry of the original artist of this track."""
|
661
|
-
KeyID: Mapped[str] = mapped_column(
|
662
|
-
VARCHAR(255), ForeignKey("djmdKey.ID"), default=None
|
663
|
-
)
|
657
|
+
KeyID: Mapped[str] = mapped_column(VARCHAR(255), ForeignKey("djmdKey.ID"), default=None)
|
664
658
|
"""The ID of the :class:`DjmdKey` entry of the key of this track."""
|
665
659
|
StockDate: Mapped[str] = mapped_column(VARCHAR(255), default=None)
|
666
660
|
"""The stock date of the track."""
|
667
|
-
ColorID: Mapped[str] = mapped_column(
|
668
|
-
VARCHAR(255), ForeignKey("djmdColor.ID"), default=None
|
669
|
-
)
|
661
|
+
ColorID: Mapped[str] = mapped_column(VARCHAR(255), ForeignKey("djmdColor.ID"), default=None)
|
670
662
|
"""The ID of the :class:`DjmdColor` entry of the color of this track."""
|
671
663
|
DJPlayCount: Mapped[str] = mapped_column(VARCHAR(255), default=None)
|
672
664
|
"""The play count of the track."""
|
@@ -684,9 +676,7 @@ class DjmdContent(Base, StatsFull):
|
|
684
676
|
"""The file size of the track."""
|
685
677
|
DiscNo: Mapped[int] = mapped_column(Integer, default=None)
|
686
678
|
"""The number of the disc of the album of the track."""
|
687
|
-
ComposerID: Mapped[str] = mapped_column(
|
688
|
-
VARCHAR(255), ForeignKey("djmdArtist.ID"), default=None
|
689
|
-
)
|
679
|
+
ComposerID: Mapped[str] = mapped_column(VARCHAR(255), ForeignKey("djmdArtist.ID"), default=None)
|
690
680
|
"""The ID of the :class:`DjmdArtist` entry of the composer of this track."""
|
691
681
|
Subtitle: Mapped[str] = mapped_column(VARCHAR(255), default=None)
|
692
682
|
"""The subtitle of the track."""
|
@@ -718,9 +708,7 @@ class DjmdContent(Base, StatsFull):
|
|
718
708
|
"""The analysis updated status of the track."""
|
719
709
|
TrackInfoUpdated: Mapped[str] = mapped_column(VARCHAR(255), default=None)
|
720
710
|
"""The track info updated status of the track."""
|
721
|
-
Lyricist: Mapped[str] = mapped_column(
|
722
|
-
VARCHAR(255), ForeignKey("djmdArtist.ID"), default=None
|
723
|
-
)
|
711
|
+
Lyricist: Mapped[str] = mapped_column(VARCHAR(255), ForeignKey("djmdArtist.ID"), default=None)
|
724
712
|
"""The ID of the :class:`DjmdArtist` entry of the lyricist of this track."""
|
725
713
|
ISRC: Mapped[str] = mapped_column(VARCHAR(255), default=None)
|
726
714
|
"""The ISRC of the track."""
|
@@ -787,9 +775,7 @@ class DjmdContent(Base, StatsFull):
|
|
787
775
|
"""The album artist entry of the track (links to :class:`DjmdArtist`)."""
|
788
776
|
MyTags = relationship("DjmdSongMyTag", back_populates="Content")
|
789
777
|
"""The my tags of the track (links to :class:`DjmdSongMyTag`)."""
|
790
|
-
Cues = relationship(
|
791
|
-
"DjmdCue", foreign_keys="DjmdCue.ContentID", back_populates="Content"
|
792
|
-
)
|
778
|
+
Cues = relationship("DjmdCue", foreign_keys="DjmdCue.ContentID", back_populates="Content")
|
793
779
|
"""The cues of the track (links to :class:`DjmdCue`)."""
|
794
780
|
ActiveCensors = relationship("DjmdActiveCensor", back_populates="Content")
|
795
781
|
"""The active censors of the track (links to :class:`DjmdActiveCensor`)."""
|
@@ -838,9 +824,7 @@ class DjmdCue(Base, StatsFull):
|
|
838
824
|
|
839
825
|
ID: Mapped[str] = mapped_column(VARCHAR(255), primary_key=True)
|
840
826
|
"""The ID (primary key) of the table entry."""
|
841
|
-
ContentID: Mapped[str] = mapped_column(
|
842
|
-
VARCHAR(255), ForeignKey("djmdContent.ID"), default=None
|
843
|
-
)
|
827
|
+
ContentID: Mapped[str] = mapped_column(VARCHAR(255), ForeignKey("djmdContent.ID"), default=None)
|
844
828
|
"""The ID of the content (:class:`DjmdContent`) containing the cue point."""
|
845
829
|
InMsec: Mapped[int] = mapped_column(Integer, default=None)
|
846
830
|
"""The in point of the cue point in milliseconds."""
|
@@ -943,9 +927,7 @@ class DjmdHistory(Base, StatsFull):
|
|
943
927
|
"""The name of the history playlist."""
|
944
928
|
Attribute: Mapped[int] = mapped_column(Integer, default=None)
|
945
929
|
"""The attributes of the history playlist"""
|
946
|
-
ParentID: Mapped[str] = mapped_column(
|
947
|
-
VARCHAR(255), ForeignKey("djmdHistory.ID"), default=None
|
948
|
-
)
|
930
|
+
ParentID: Mapped[str] = mapped_column(VARCHAR(255), ForeignKey("djmdHistory.ID"), default=None)
|
949
931
|
"""The ID of the parent history playlist (:class:`DjmdHistory`)."""
|
950
932
|
DateCreated: Mapped[str] = mapped_column(VARCHAR(255), default=None)
|
951
933
|
"""The date the history playlist was created."""
|
@@ -978,13 +960,9 @@ class DjmdSongHistory(Base, StatsFull):
|
|
978
960
|
|
979
961
|
ID: Mapped[str] = mapped_column(VARCHAR(255), primary_key=True)
|
980
962
|
|
981
|
-
HistoryID: Mapped[str] = mapped_column(
|
982
|
-
VARCHAR(255), ForeignKey("djmdHistory.ID"), default=None
|
983
|
-
)
|
963
|
+
HistoryID: Mapped[str] = mapped_column(VARCHAR(255), ForeignKey("djmdHistory.ID"), default=None)
|
984
964
|
"""The ID of the history playlist (:class:`DjmdHistory`)."""
|
985
|
-
ContentID: Mapped[str] = mapped_column(
|
986
|
-
VARCHAR(255), ForeignKey("djmdContent.ID"), default=None
|
987
|
-
)
|
965
|
+
ContentID: Mapped[str] = mapped_column(VARCHAR(255), ForeignKey("djmdContent.ID"), default=None)
|
988
966
|
"""The ID of the content (:class:`DjmdContent`)."""
|
989
967
|
TrackNo: Mapped[int] = mapped_column(Integer, default=None)
|
990
968
|
"""The track number of the song in the history playlist."""
|
@@ -1050,9 +1028,7 @@ class DjmdSongHotCueBanklist(Base, StatsFull):
|
|
1050
1028
|
VARCHAR(255), ForeignKey("djmdHotCueBanklist.ID"), default=None
|
1051
1029
|
)
|
1052
1030
|
"""The ID of the hot cue banklist (:class:`DjmdHotCueBanklist`)."""
|
1053
|
-
ContentID: Mapped[str] = mapped_column(
|
1054
|
-
VARCHAR(255), ForeignKey("djmdContent.ID"), default=None
|
1055
|
-
)
|
1031
|
+
ContentID: Mapped[str] = mapped_column(VARCHAR(255), ForeignKey("djmdContent.ID"), default=None)
|
1056
1032
|
"""The ID of the content (:class:`DjmdContent`)."""
|
1057
1033
|
TrackNo: Mapped[int] = mapped_column(Integer, default=None)
|
1058
1034
|
"""The track number of the hot-cue in the hot cue banklist."""
|
@@ -1160,9 +1136,7 @@ class DjmdMixerParam(Base, StatsFull):
|
|
1160
1136
|
|
1161
1137
|
ID: Mapped[str] = mapped_column(VARCHAR(255), primary_key=True)
|
1162
1138
|
"""The ID (primary key) of the table entry."""
|
1163
|
-
ContentID: Mapped[str] = mapped_column(
|
1164
|
-
VARCHAR(255), ForeignKey("djmdContent.ID"), default=None
|
1165
|
-
)
|
1139
|
+
ContentID: Mapped[str] = mapped_column(VARCHAR(255), ForeignKey("djmdContent.ID"), default=None)
|
1166
1140
|
"""The ID of the content (:class:`DjmdContent`)."""
|
1167
1141
|
GainHigh: Mapped[int] = mapped_column(Integer, default=None)
|
1168
1142
|
"""The high gain of the mixer parameter."""
|
@@ -1243,9 +1217,7 @@ class DjmdMyTag(Base, StatsFull):
|
|
1243
1217
|
"""The name of the My-Tag list."""
|
1244
1218
|
Attribute: Mapped[int] = mapped_column(Integer, default=None)
|
1245
1219
|
"""The attribute of the My-Tag list."""
|
1246
|
-
ParentID: Mapped[str] = mapped_column(
|
1247
|
-
VARCHAR(255), ForeignKey("djmdMyTag.ID"), default=None
|
1248
|
-
)
|
1220
|
+
ParentID: Mapped[str] = mapped_column(VARCHAR(255), ForeignKey("djmdMyTag.ID"), default=None)
|
1249
1221
|
"""The ID of the parent My-Tag list (:class:`DjmdMyTag`)."""
|
1250
1222
|
|
1251
1223
|
MyTags = relationship("DjmdSongMyTag", back_populates="MyTag")
|
@@ -1274,13 +1246,9 @@ class DjmdSongMyTag(Base, StatsFull):
|
|
1274
1246
|
|
1275
1247
|
ID: Mapped[str] = mapped_column(VARCHAR(255), primary_key=True)
|
1276
1248
|
"""The ID (primary key) of the table entry."""
|
1277
|
-
MyTagID: Mapped[str] = mapped_column(
|
1278
|
-
VARCHAR(255), ForeignKey("djmdMyTag.ID"), default=None
|
1279
|
-
)
|
1249
|
+
MyTagID: Mapped[str] = mapped_column(VARCHAR(255), ForeignKey("djmdMyTag.ID"), default=None)
|
1280
1250
|
"""The ID of the My-Tag list (links to :class:`DjmdMyTag`)."""
|
1281
|
-
ContentID: Mapped[str] = mapped_column(
|
1282
|
-
VARCHAR(255), ForeignKey("djmdContent.ID"), default=None
|
1283
|
-
)
|
1251
|
+
ContentID: Mapped[str] = mapped_column(VARCHAR(255), ForeignKey("djmdContent.ID"), default=None)
|
1284
1252
|
"""The ID of the content this item belongs to (:class:`DjmdContent`)."""
|
1285
1253
|
TrackNo: Mapped[int] = mapped_column(Integer, default=None)
|
1286
1254
|
"""The track number of the My-Tag item (for ordering)."""
|
@@ -1314,16 +1282,12 @@ class DjmdPlaylist(Base, StatsFull):
|
|
1314
1282
|
"""The path to the image of the playlist."""
|
1315
1283
|
Attribute: Mapped[int] = mapped_column(Integer, default=None)
|
1316
1284
|
"""The attribute of the playlist."""
|
1317
|
-
ParentID: Mapped[str] = mapped_column(
|
1318
|
-
VARCHAR(255), ForeignKey("djmdPlaylist.ID"), default=None
|
1319
|
-
)
|
1285
|
+
ParentID: Mapped[str] = mapped_column(VARCHAR(255), ForeignKey("djmdPlaylist.ID"), default=None)
|
1320
1286
|
"""The ID of the parent playlist (:class:`DjmdPlaylist`)."""
|
1321
1287
|
SmartList: Mapped[str] = mapped_column(Text, default=None)
|
1322
1288
|
"""The smart list settings of the playlist."""
|
1323
1289
|
|
1324
|
-
Songs = relationship(
|
1325
|
-
"DjmdSongPlaylist", back_populates="Playlist", cascade="all, delete"
|
1326
|
-
)
|
1290
|
+
Songs = relationship("DjmdSongPlaylist", back_populates="Playlist", cascade="all, delete")
|
1327
1291
|
"""The contents of the playlist (links to :class:`DjmdSongPlaylist`)."""
|
1328
1292
|
Children = relationship(
|
1329
1293
|
"DjmdPlaylist",
|
@@ -1368,9 +1332,7 @@ class DjmdSongPlaylist(Base, StatsFull):
|
|
1368
1332
|
VARCHAR(255), ForeignKey("djmdPlaylist.ID"), default=None
|
1369
1333
|
)
|
1370
1334
|
"""The ID of the playlist this item is in (:class:`DjmdPlaylist`)."""
|
1371
|
-
ContentID: Mapped[str] = mapped_column(
|
1372
|
-
VARCHAR(255), ForeignKey("djmdContent.ID"), default=None
|
1373
|
-
)
|
1335
|
+
ContentID: Mapped[str] = mapped_column(VARCHAR(255), ForeignKey("djmdContent.ID"), default=None)
|
1374
1336
|
"""The ID of the content this item belongs to (:class:`DjmdContent`)."""
|
1375
1337
|
TrackNo: Mapped[int] = mapped_column(Integer, default=None)
|
1376
1338
|
"""The track number of the playlist item (for ordering)."""
|
@@ -1441,9 +1403,7 @@ class DjmdSongRelatedTracks(Base, StatsFull):
|
|
1441
1403
|
)
|
1442
1404
|
"""The ID of the related tracks list this item is in
|
1443
1405
|
(:class:`DjmdRelatedTracks`)."""
|
1444
|
-
ContentID: Mapped[str] = mapped_column(
|
1445
|
-
VARCHAR(255), ForeignKey("djmdContent.ID"), default=None
|
1446
|
-
)
|
1406
|
+
ContentID: Mapped[str] = mapped_column(VARCHAR(255), ForeignKey("djmdContent.ID"), default=None)
|
1447
1407
|
"""The ID of the content this item belongs to (:class:`DjmdContent`)."""
|
1448
1408
|
TrackNo: Mapped[int] = mapped_column(Integer, default=None)
|
1449
1409
|
"""The track number of the related tracks list item (for ordering)."""
|
@@ -1472,9 +1432,7 @@ class DjmdSampler(Base, StatsFull):
|
|
1472
1432
|
"""The name of the sampler list."""
|
1473
1433
|
Attribute: Mapped[int] = mapped_column(Integer, default=None)
|
1474
1434
|
"""The attribute of the sampler list."""
|
1475
|
-
ParentID: Mapped[str] = mapped_column(
|
1476
|
-
VARCHAR(255), ForeignKey("djmdSampler.ID"), default=None
|
1477
|
-
)
|
1435
|
+
ParentID: Mapped[str] = mapped_column(VARCHAR(255), ForeignKey("djmdSampler.ID"), default=None)
|
1478
1436
|
"""The ID of the parent sampler list (:class:`DjmdSampler`)."""
|
1479
1437
|
|
1480
1438
|
Songs = relationship("DjmdSongSampler", back_populates="Sampler")
|
@@ -1505,13 +1463,9 @@ class DjmdSongSampler(Base, StatsFull):
|
|
1505
1463
|
|
1506
1464
|
ID: Mapped[str] = mapped_column(VARCHAR(255), primary_key=True)
|
1507
1465
|
"""The ID (primary key) of the table entry."""
|
1508
|
-
SamplerID: Mapped[str] = mapped_column(
|
1509
|
-
VARCHAR(255), ForeignKey("djmdSampler.ID"), default=None
|
1510
|
-
)
|
1466
|
+
SamplerID: Mapped[str] = mapped_column(VARCHAR(255), ForeignKey("djmdSampler.ID"), default=None)
|
1511
1467
|
"""The ID of the sampler list this item is in (:class:`DjmdSampler`)."""
|
1512
|
-
ContentID: Mapped[str] = mapped_column(
|
1513
|
-
VARCHAR(255), ForeignKey("djmdContent.ID"), default=None
|
1514
|
-
)
|
1468
|
+
ContentID: Mapped[str] = mapped_column(VARCHAR(255), ForeignKey("djmdContent.ID"), default=None)
|
1515
1469
|
"""The ID of the content this item belongs to (:class:`DjmdContent`)."""
|
1516
1470
|
TrackNo: Mapped[int] = mapped_column(Integer, default=None)
|
1517
1471
|
"""The track number of the sampler list item (for ordering)."""
|
@@ -1529,9 +1483,7 @@ class DjmdSongTagList(Base, StatsFull):
|
|
1529
1483
|
|
1530
1484
|
ID: Mapped[str] = mapped_column(VARCHAR(255), primary_key=True)
|
1531
1485
|
"""The ID (primary key) of the table entry."""
|
1532
|
-
ContentID: Mapped[str] = mapped_column(
|
1533
|
-
VARCHAR(255), ForeignKey("djmdContent.ID"), default=None
|
1534
|
-
)
|
1486
|
+
ContentID: Mapped[str] = mapped_column(VARCHAR(255), ForeignKey("djmdContent.ID"), default=None)
|
1535
1487
|
"""The ID of the content this item belongs to (:class:`DjmdContent`)."""
|
1536
1488
|
TrackNo: Mapped[int] = mapped_column(Integer, default=None)
|
1537
1489
|
"""The track number of the tag list item (for ordering)."""
|
pyrekordbox/rbxml.py
CHANGED
@@ -27,9 +27,7 @@ POSMARK_TYPE_MAPPING = bidict.bidict(
|
|
27
27
|
"4": "loop",
|
28
28
|
}
|
29
29
|
)
|
30
|
-
RATING_MAPPING = bidict.bidict(
|
31
|
-
{"0": 0, "51": 1, "102": 2, "153": 3, "204": 4, "255": 5}
|
32
|
-
)
|
30
|
+
RATING_MAPPING = bidict.bidict({"0": 0, "51": 1, "102": 2, "153": 3, "204": 4, "255": 5})
|
33
31
|
NODE_KEYTYPE_MAPPING = bidict.bidict({"0": "TrackID", "1": "Location"})
|
34
32
|
|
35
33
|
|
@@ -273,9 +271,7 @@ class Tempo(AbstractElement):
|
|
273
271
|
ATTRIBS = ["Inizio", "Bpm", "Metro", "Battito"]
|
274
272
|
GETTERS = {"Inizio": float, "Bpm": float, "Battito": int}
|
275
273
|
|
276
|
-
def __init__(
|
277
|
-
self, parent=None, Inizio=0.0, Bpm=0.0, Metro="4/4", Battito=1, element=None
|
278
|
-
):
|
274
|
+
def __init__(self, parent=None, Inizio=0.0, Bpm=0.0, Metro="4/4", Battito=1, element=None):
|
279
275
|
super().__init__(element, parent, Inizio, Bpm, Metro, Battito)
|
280
276
|
|
281
277
|
def _init(self, parent, inizio, bpm, metro, battito):
|
@@ -1264,9 +1260,7 @@ class RekordboxXml:
|
|
1264
1260
|
num_tracks = len(self._collection.findall(f".//{Track.TAG}"))
|
1265
1261
|
n = int(self._collection.attrib["Entries"])
|
1266
1262
|
if n != num_tracks:
|
1267
|
-
raise ValueError(
|
1268
|
-
f"Track count {num_tracks} does not match number of elements {n}"
|
1269
|
-
)
|
1263
|
+
raise ValueError(f"Track count {num_tracks} does not match number of elements {n}")
|
1270
1264
|
# Generate XML string
|
1271
1265
|
return pretty_xml(self._root, indent, encoding="utf-8")
|
1272
1266
|
|
@@ -1,6 +1,6 @@
|
|
1
|
-
Metadata-Version: 2.
|
1
|
+
Metadata-Version: 2.4
|
2
2
|
Name: pyrekordbox
|
3
|
-
Version: 0.4.
|
3
|
+
Version: 0.4.1
|
4
4
|
Summary: Inofficial Python package for interacting with the library of Pioneers Rekordbox DJ software.
|
5
5
|
Author-email: Dylan Jones <dylanljones94@gmail.com>
|
6
6
|
License: MIT License
|
@@ -61,6 +61,7 @@ Provides-Extra: test
|
|
61
61
|
Requires-Dist: hypothesis>=6.0.0; extra == "test"
|
62
62
|
Requires-Dist: pytest>=6.2.0; extra == "test"
|
63
63
|
Requires-Dist: pytest-cov; extra == "test"
|
64
|
+
Dynamic: license-file
|
64
65
|
|
65
66
|
|
66
67
|
<p align="center">
|
@@ -88,9 +89,9 @@ Pioneers Rekordbox DJ Software. It currently supports
|
|
88
89
|
|
89
90
|
Tested Rekordbox versions: ``5.8.6 | 6.7.7 | 7.0.9``
|
90
91
|
|
91
|
-
|
92
|
-
|
93
|
-
|
92
|
+
> [!WARNING]
|
93
|
+
> This project is still under development and might contain bugs or have breaking API changes in the future.
|
94
|
+
> Check the [changelog][CHANGELOG] for recent changes!
|
94
95
|
|
95
96
|
|
96
97
|
## 🔧 Installation
|
@@ -118,8 +119,10 @@ If this fails, it can be installed manually following the [installation guide][i
|
|
118
119
|
|
119
120
|
[Read the full documentation on ReadTheDocs!][documentation]
|
120
121
|
|
121
|
-
|
122
|
-
|
122
|
+
> [!CAUTION]
|
123
|
+
> Please make sure to back up your Rekordbox collection before making changes with pyrekordbox
|
124
|
+
> or developing/testing new features.
|
125
|
+
> The backup dialog can be found under "File" > "Library" > "Backup Library"
|
123
126
|
|
124
127
|
|
125
128
|
### Configuration
|
@@ -174,9 +177,13 @@ can be changed via the corresponding property of the object:
|
|
174
177
|
````python
|
175
178
|
content = db.get_content()[0]
|
176
179
|
content.Title = "New Title"
|
180
|
+
db.commit()
|
177
181
|
````
|
178
|
-
|
179
|
-
|
182
|
+
|
183
|
+
> [!NOTE]
|
184
|
+
> Some fields are stored as references to other tables, for example the artist of a track.
|
185
|
+
> Check the [documentation][db6-doc] of the corresponding object for more information.
|
186
|
+
|
180
187
|
So far only a few tables support adding or deleting entries:
|
181
188
|
- ``DjmdContent``: Tracks
|
182
189
|
- ``DjmdPlaylist``: Playlists/Playlist Folders
|
@@ -1,25 +1,25 @@
|
|
1
1
|
pyrekordbox/__init__.py,sha256=hvg2dB7qhgGB6SmcRg8AION6mTGdlsThdHhsDZ9WYfs,622
|
2
2
|
pyrekordbox/__main__.py,sha256=BtyDwVSGyoURY9Cy004vRL5Tgkwt4F7eXX_GD_vVYNQ,5976
|
3
|
-
pyrekordbox/_version.py,sha256=
|
4
|
-
pyrekordbox/config.py,sha256=
|
3
|
+
pyrekordbox/_version.py,sha256=yF2DwGUoQKNnLhAbpZX8kCQKjw77EZzhRk7_OTftets,511
|
4
|
+
pyrekordbox/config.py,sha256=TTrL2bOo9DDKSW2yys8axaLB2GHhhD8VL7vq4dJxi1A,28309
|
5
5
|
pyrekordbox/logger.py,sha256=dq1BtXBGavuAjuc45mvjF6mOWaeZqZFzo2aBOJdJ0Ik,483
|
6
|
-
pyrekordbox/rbxml.py,sha256=
|
6
|
+
pyrekordbox/rbxml.py,sha256=UqATygpuOVGlSYj0nzcHSmDjTeQ9EL5AVLhAxS523F4,38340
|
7
7
|
pyrekordbox/utils.py,sha256=hkYIgG5U4rzl2tjN9ESzLnf8OysEFybRQgmr6J7xq-k,4363
|
8
8
|
pyrekordbox/anlz/__init__.py,sha256=SEVY0oPX9ohCVViUbsoOLTrBrFewTh-61qJxwXgAJKg,3155
|
9
9
|
pyrekordbox/anlz/file.py,sha256=F6axHmprnp0j3pZkqmmp5iiJBUpqtWiAhSzlAJp2H6Y,6951
|
10
10
|
pyrekordbox/anlz/structs.py,sha256=Lt4fkb3SAE8w146eWeWGnpgRoP6jhLMWrSMoMwPjG04,7925
|
11
|
-
pyrekordbox/anlz/tags.py,sha256=
|
11
|
+
pyrekordbox/anlz/tags.py,sha256=nlPBKyRB8Z9J69bX2K8ZPQk_g1tMazKeVf2ViAqMX-c,13947
|
12
12
|
pyrekordbox/db6/__init__.py,sha256=TZX_BPGZIkc4zSTULIc8yd_bf91MAezGtZevKNh3kZ0,856
|
13
13
|
pyrekordbox/db6/aux_files.py,sha256=MehdQSc4iryiHvH8RfE9_9xMnD5qjRRDhTo0o0KRLw0,7592
|
14
|
-
pyrekordbox/db6/database.py,sha256=
|
14
|
+
pyrekordbox/db6/database.py,sha256=2FTwVZOx8pSLLRr9ZV_95TUUtCfPo3CSLKYyudBN8-o,80678
|
15
15
|
pyrekordbox/db6/registry.py,sha256=zYq_B7INM97de7vUTxqmA4N_P53loDkBkYdzdoeSnvY,9725
|
16
|
-
pyrekordbox/db6/smartlist.py,sha256=
|
17
|
-
pyrekordbox/db6/tables.py,sha256=
|
16
|
+
pyrekordbox/db6/smartlist.py,sha256=GG6jE0BHMtx0RHgiVRpWYuL-Ex5edTNSLSDQNiCGzK0,12170
|
17
|
+
pyrekordbox/db6/tables.py,sha256=EST6wvsJjlwmVB8JWBLMKbW3Mskk5qAo-xBqB09Q-Hg,68479
|
18
18
|
pyrekordbox/mysettings/__init__.py,sha256=6iLTQ1KIjuoq8Zt3thmkjqJSxrRVIi7BrQpxNcsQK04,706
|
19
19
|
pyrekordbox/mysettings/file.py,sha256=JBfVe3jsmah_mGJjyC20_EqJZyJ7ftcOcCkRDKcWgv0,12671
|
20
20
|
pyrekordbox/mysettings/structs.py,sha256=5Y1F3qTmsP1fRB39_BEHpQVxKx2DO9BytEuJUG_RNcY,8472
|
21
|
-
pyrekordbox-0.4.
|
22
|
-
pyrekordbox-0.4.
|
23
|
-
pyrekordbox-0.4.
|
24
|
-
pyrekordbox-0.4.
|
25
|
-
pyrekordbox-0.4.
|
21
|
+
pyrekordbox-0.4.1.dist-info/licenses/LICENSE,sha256=VwG9ZgC2UZnI0gTezGz1qkcAZ7sknBUQ1M62Z2nht54,1074
|
22
|
+
pyrekordbox-0.4.1.dist-info/METADATA,sha256=q8KLPyfFte6I8cpdZai6LN8Kck8LI1E4rNZs947nnSY,15480
|
23
|
+
pyrekordbox-0.4.1.dist-info/WHEEL,sha256=pxyMxgL8-pra_rKaQ4drOZAegBVuX-G_4nRHjjgWbmo,91
|
24
|
+
pyrekordbox-0.4.1.dist-info/top_level.txt,sha256=bUHkyxIHZDgSB6zhYnF1o4Yf1EQlTGGIkVRq9uEtsa4,12
|
25
|
+
pyrekordbox-0.4.1.dist-info/RECORD,,
|
File without changes
|
File without changes
|