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.
- pyrekordbox/__init__.py +8 -8
- pyrekordbox/__main__.py +3 -2
- pyrekordbox/_version.py +9 -4
- pyrekordbox/anlz/__init__.py +3 -2
- pyrekordbox/anlz/file.py +4 -2
- pyrekordbox/anlz/tags.py +8 -16
- pyrekordbox/config.py +116 -47
- pyrekordbox/db6/__init__.py +2 -2
- pyrekordbox/db6/aux_files.py +3 -2
- pyrekordbox/db6/database.py +130 -177
- pyrekordbox/db6/registry.py +1 -0
- pyrekordbox/db6/smartlist.py +9 -11
- pyrekordbox/db6/tables.py +112 -132
- pyrekordbox/logger.py +0 -1
- pyrekordbox/mysettings/__init__.py +5 -4
- pyrekordbox/mysettings/file.py +3 -1
- pyrekordbox/rbxml.py +8 -12
- pyrekordbox/utils.py +4 -3
- {pyrekordbox-0.3.2.dist-info → pyrekordbox-0.4.1.dist-info}/METADATA +35 -48
- pyrekordbox-0.4.1.dist-info/RECORD +25 -0
- {pyrekordbox-0.3.2.dist-info → pyrekordbox-0.4.1.dist-info}/WHEEL +1 -1
- {pyrekordbox-0.3.2.dist-info → pyrekordbox-0.4.1.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.1.dist-info/licenses}/LICENSE +0 -0
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
|
@@ -31,6 +33,10 @@ except ImportError:
|
|
31
33
|
_sqlcipher_available = False
|
32
34
|
|
33
35
|
MAX_VERSION = "6.6.5"
|
36
|
+
SPECIAL_PLAYLIST_IDS = [
|
37
|
+
"100000", # Cloud Library Sync
|
38
|
+
"200000", # CUE Analysis Playlist
|
39
|
+
]
|
34
40
|
|
35
41
|
logger = logging.getLogger(__name__)
|
36
42
|
|
@@ -39,101 +45,6 @@ class NoCachedKey(Exception):
|
|
39
45
|
pass
|
40
46
|
|
41
47
|
|
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
48
|
def _parse_query_result(query, kwargs):
|
138
49
|
if "ID" in kwargs or "registry_id" in kwargs:
|
139
50
|
try:
|
@@ -193,17 +104,21 @@ class Rekordbox6Database:
|
|
193
104
|
"""
|
194
105
|
|
195
106
|
def __init__(self, path=None, db_dir="", key="", unlock=True):
|
196
|
-
|
107
|
+
# get config of latest supported version
|
108
|
+
rb_config = get_config("rekordbox7")
|
109
|
+
if not rb_config:
|
110
|
+
rb_config = get_config("rekordbox6")
|
111
|
+
|
197
112
|
pid = get_rekordbox_pid()
|
198
113
|
if pid:
|
199
114
|
logger.warning("Rekordbox is running!")
|
200
115
|
|
201
116
|
if not path:
|
202
117
|
# Get path from the RB config
|
203
|
-
path =
|
118
|
+
path = rb_config.get("db_path", "")
|
204
119
|
if not path:
|
205
120
|
pdir = get_config("pioneer", "install_dir")
|
206
|
-
raise FileNotFoundError(f"No Rekordbox v6 directory found in '{pdir}'")
|
121
|
+
raise FileNotFoundError(f"No Rekordbox v6/v7 directory found in '{pdir}'")
|
207
122
|
path = Path(path)
|
208
123
|
# make sure file exists
|
209
124
|
if not path.exists():
|
@@ -211,12 +126,10 @@ class Rekordbox6Database:
|
|
211
126
|
# Open database
|
212
127
|
if unlock:
|
213
128
|
if not _sqlcipher_available:
|
214
|
-
raise ImportError(
|
215
|
-
"Could not unlock database: 'sqlcipher3' package not found"
|
216
|
-
)
|
129
|
+
raise ImportError("Could not unlock database: 'sqlcipher3' package not found")
|
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)
|
@@ -491,17 +404,20 @@ class Rekordbox6Database:
|
|
491
404
|
# Sync the updated_at values of the playlists in the DB and the XML file
|
492
405
|
for pl in self.get_playlist():
|
493
406
|
plxml = self.playlist_xml.get(pl.ID)
|
494
|
-
if plxml is None:
|
495
|
-
|
496
|
-
|
497
|
-
|
498
|
-
"
|
499
|
-
|
500
|
-
|
501
|
-
|
502
|
-
|
503
|
-
|
504
|
-
|
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
|
+
)
|
505
421
|
|
506
422
|
# Save the XML file if it was modified
|
507
423
|
if self.playlist_xml.modified:
|
@@ -813,7 +729,7 @@ class Rekordbox6Database:
|
|
813
729
|
|
814
730
|
# -- Database updates --------------------------------------------------------------
|
815
731
|
|
816
|
-
def generate_unused_id(self, table, is_28_bit: bool = True) -> int:
|
732
|
+
def generate_unused_id(self, table, is_28_bit: bool = True, id_field_name: str = "ID") -> int:
|
817
733
|
"""Generates an unused ID for the given table."""
|
818
734
|
max_tries = 1000000
|
819
735
|
for _ in range(max_tries):
|
@@ -825,7 +741,8 @@ class Rekordbox6Database:
|
|
825
741
|
if id_ < 100:
|
826
742
|
continue
|
827
743
|
# Check if ID is already used
|
828
|
-
|
744
|
+
id_field = getattr(table, id_field_name)
|
745
|
+
query = self.query(id_field).filter(id_field == id_)
|
829
746
|
used = self.query(query.exists()).scalar()
|
830
747
|
if not used:
|
831
748
|
return id_
|
@@ -887,20 +804,14 @@ class Rekordbox6Database:
|
|
887
804
|
uuid = str(uuid4())
|
888
805
|
id_ = str(uuid4())
|
889
806
|
now = datetime.datetime.now()
|
890
|
-
nsongs = (
|
891
|
-
self.query(tables.DjmdSongPlaylist)
|
892
|
-
.filter_by(PlaylistID=playlist.ID)
|
893
|
-
.count()
|
894
|
-
)
|
807
|
+
nsongs = self.query(tables.DjmdSongPlaylist).filter_by(PlaylistID=playlist.ID).count()
|
895
808
|
if track_no is not None:
|
896
809
|
insert_at_end = False
|
897
810
|
track_no = int(track_no)
|
898
811
|
if track_no < 1:
|
899
812
|
raise ValueError("Track number must be greater than 0")
|
900
813
|
if track_no > nsongs + 1:
|
901
|
-
raise ValueError(
|
902
|
-
f"Track number too high, parent contains {nsongs} items"
|
903
|
-
)
|
814
|
+
raise ValueError(f"Track number too high, parent contains {nsongs} items")
|
904
815
|
else:
|
905
816
|
insert_at_end = True
|
906
817
|
track_no = nsongs + 1
|
@@ -976,9 +887,7 @@ class Rekordbox6Database:
|
|
976
887
|
playlist = self.get_playlist(ID=playlist)
|
977
888
|
if isinstance(song, (int, str)):
|
978
889
|
song = self.query(tables.DjmdSongPlaylist).filter_by(ID=song).one()
|
979
|
-
logger.info(
|
980
|
-
"Removing song with ID=%s from playlist with ID=%s", song.ID, playlist.ID
|
981
|
-
)
|
890
|
+
logger.info("Removing song with ID=%s from playlist with ID=%s", song.ID, playlist.ID)
|
982
891
|
now = datetime.datetime.now()
|
983
892
|
# Remove track from playlist
|
984
893
|
track_no = song.TrackNo
|
@@ -1049,11 +958,7 @@ class Rekordbox6Database:
|
|
1049
958
|
playlist = self.get_playlist(ID=playlist)
|
1050
959
|
if isinstance(song, (int, str)):
|
1051
960
|
song = self.query(tables.DjmdSongPlaylist).filter_by(ID=song).one()
|
1052
|
-
nsongs = (
|
1053
|
-
self.query(tables.DjmdSongPlaylist)
|
1054
|
-
.filter_by(PlaylistID=playlist.ID)
|
1055
|
-
.count()
|
1056
|
-
)
|
961
|
+
nsongs = self.query(tables.DjmdSongPlaylist).filter_by(PlaylistID=playlist.ID).count()
|
1057
962
|
if new_track_no < 1:
|
1058
963
|
raise ValueError("Track number must be greater than 0")
|
1059
964
|
if new_track_no > nsongs + 1:
|
@@ -1103,9 +1008,7 @@ class Rekordbox6Database:
|
|
1103
1008
|
self.registry.enable_tracking()
|
1104
1009
|
self.registry.on_move(moved)
|
1105
1010
|
|
1106
|
-
def _create_playlist(
|
1107
|
-
self, name, seq, image_path, parent, smart_list=None, attribute=None
|
1108
|
-
):
|
1011
|
+
def _create_playlist(self, name, seq, image_path, parent, smart_list=None, attribute=None):
|
1109
1012
|
"""Creates a new playlist object."""
|
1110
1013
|
table = tables.DjmdPlaylist
|
1111
1014
|
id_ = str(self.generate_unused_id(table, is_28_bit=True))
|
@@ -1130,9 +1033,7 @@ class Rekordbox6Database:
|
|
1130
1033
|
else:
|
1131
1034
|
# Check if parent exists and is a folder
|
1132
1035
|
parent_id = parent
|
1133
|
-
query = self.query(table.ID).filter(
|
1134
|
-
table.ID == parent_id, table.Attribute == 1
|
1135
|
-
)
|
1036
|
+
query = self.query(table.ID).filter(table.ID == parent_id, table.Attribute == 1)
|
1136
1037
|
if not self.query(query.exists()).scalar():
|
1137
1038
|
raise ValueError("Parent does not exist or is not a folder")
|
1138
1039
|
|
@@ -1191,9 +1092,7 @@ class Rekordbox6Database:
|
|
1191
1092
|
|
1192
1093
|
# Update masterPlaylists6.xml
|
1193
1094
|
if self.playlist_xml is not None:
|
1194
|
-
self.playlist_xml.add(
|
1195
|
-
id_, parent_id, attribute, now, lib_type=0, check_type=0
|
1196
|
-
)
|
1095
|
+
self.playlist_xml.add(id_, parent_id, attribute, now, lib_type=0, check_type=0)
|
1197
1096
|
|
1198
1097
|
return playlist
|
1199
1098
|
|
@@ -1241,9 +1140,7 @@ class Rekordbox6Database:
|
|
1241
1140
|
'123456'
|
1242
1141
|
"""
|
1243
1142
|
logger.info("Creating playlist %s", name)
|
1244
|
-
return self._create_playlist(
|
1245
|
-
name, seq, image_path, parent, attribute=PlaylistType.PLAYLIST
|
1246
|
-
)
|
1143
|
+
return self._create_playlist(name, seq, image_path, parent, attribute=PlaylistType.PLAYLIST)
|
1247
1144
|
|
1248
1145
|
def create_playlist_folder(self, name, parent=None, seq=None, image_path=None):
|
1249
1146
|
"""Creates a new playlist folder in the database.
|
@@ -1283,9 +1180,7 @@ class Rekordbox6Database:
|
|
1283
1180
|
'123456'
|
1284
1181
|
"""
|
1285
1182
|
logger.info("Creating playlist folder %s", name)
|
1286
|
-
return self._create_playlist(
|
1287
|
-
name, seq, image_path, parent, attribute=PlaylistType.FOLDER
|
1288
|
-
)
|
1183
|
+
return self._create_playlist(name, seq, image_path, parent, attribute=PlaylistType.FOLDER)
|
1289
1184
|
|
1290
1185
|
def create_smart_playlist(
|
1291
1186
|
self, name, smart_list: SmartList, parent=None, seq=None, image_path=None
|
@@ -1360,9 +1255,7 @@ class Rekordbox6Database:
|
|
1360
1255
|
playlist = self.get_playlist(ID=playlist)
|
1361
1256
|
|
1362
1257
|
if playlist.Attribute == 1:
|
1363
|
-
logger.info(
|
1364
|
-
"Deleting playlist folder '%s' with ID=%s", playlist.Name, playlist.ID
|
1365
|
-
)
|
1258
|
+
logger.info("Deleting playlist folder '%s' with ID=%s", playlist.Name, playlist.ID)
|
1366
1259
|
else:
|
1367
1260
|
logger.info("Deleting playlist '%s' with ID=%s", playlist.Name, playlist.ID)
|
1368
1261
|
|
@@ -1475,9 +1368,7 @@ class Rekordbox6Database:
|
|
1475
1368
|
else:
|
1476
1369
|
# Check if parent exists and is a folder
|
1477
1370
|
parent_id = str(parent)
|
1478
|
-
query = self.query(table.ID).filter(
|
1479
|
-
table.ID == parent_id, table.Attribute == 1
|
1480
|
-
)
|
1371
|
+
query = self.query(table.ID).filter(table.ID == parent_id, table.Attribute == 1)
|
1481
1372
|
if not self.query(query.exists()).scalar():
|
1482
1373
|
raise ValueError("Parent does not exist or is not a folder")
|
1483
1374
|
|
@@ -1498,9 +1389,7 @@ class Rekordbox6Database:
|
|
1498
1389
|
if seq < 1:
|
1499
1390
|
raise ValueError("Sequence number must be greater than 0")
|
1500
1391
|
elif seq > n + 1:
|
1501
|
-
raise ValueError(
|
1502
|
-
f"Sequence number too high, parent contains {n} items"
|
1503
|
-
)
|
1392
|
+
raise ValueError(f"Sequence number too high, parent contains {n} items")
|
1504
1393
|
|
1505
1394
|
if not insert_at_end:
|
1506
1395
|
# Get all playlists with seq between old_seq and seq
|
@@ -1634,9 +1523,7 @@ class Rekordbox6Database:
|
|
1634
1523
|
with self.registry.disabled():
|
1635
1524
|
playlist.updated_at = now
|
1636
1525
|
|
1637
|
-
def add_album(
|
1638
|
-
self, name, artist=None, image_path=None, compilation=None, search_str=None
|
1639
|
-
):
|
1526
|
+
def add_album(self, name, artist=None, image_path=None, compilation=None, search_str=None):
|
1640
1527
|
"""Adds a new album to the database.
|
1641
1528
|
|
1642
1529
|
Parameters
|
@@ -1768,9 +1655,7 @@ class Rekordbox6Database:
|
|
1768
1655
|
|
1769
1656
|
id_ = self.generate_unused_id(tables.DjmdArtist)
|
1770
1657
|
uuid = str(uuid4())
|
1771
|
-
artist = tables.DjmdArtist.create(
|
1772
|
-
ID=id_, Name=name, SearchStr=search_str, UUID=uuid
|
1773
|
-
)
|
1658
|
+
artist = tables.DjmdArtist.create(ID=id_, Name=name, SearchStr=search_str, UUID=uuid)
|
1774
1659
|
self.add(artist)
|
1775
1660
|
self.flush()
|
1776
1661
|
return artist
|
@@ -1875,6 +1760,78 @@ class Rekordbox6Database:
|
|
1875
1760
|
self.flush()
|
1876
1761
|
return label
|
1877
1762
|
|
1763
|
+
def add_content(self, path, **kwargs):
|
1764
|
+
"""Adds a new track to the database.
|
1765
|
+
|
1766
|
+
Parameters
|
1767
|
+
----------
|
1768
|
+
path : str
|
1769
|
+
Absolute path to the music file to be added.
|
1770
|
+
|
1771
|
+
**kwargs:
|
1772
|
+
Keyword arguments passed to DjmdContent on creation. These arguments
|
1773
|
+
should be a valid DjmdContent field.
|
1774
|
+
|
1775
|
+
Returns
|
1776
|
+
-------
|
1777
|
+
content : DjmdContent
|
1778
|
+
The newly created track.
|
1779
|
+
|
1780
|
+
Raises
|
1781
|
+
------
|
1782
|
+
ValueError : If a track with the same path already exists in the database.
|
1783
|
+
ValueError : If the file type is invalid.
|
1784
|
+
|
1785
|
+
Examples
|
1786
|
+
--------
|
1787
|
+
Add a new track to the database:
|
1788
|
+
|
1789
|
+
>>> db = Rekordbox6Database()
|
1790
|
+
>>> db.add_content("/Users/foo/Downloads/banger.mp3", Title="Banger")
|
1791
|
+
<DjmdContent(123456789 Title=Banger)>
|
1792
|
+
"""
|
1793
|
+
path = Path(path)
|
1794
|
+
path_string = str(path)
|
1795
|
+
query = self.query(tables.DjmdContent).filter_by(FolderPath=path_string)
|
1796
|
+
if query.count() > 0:
|
1797
|
+
raise ValueError(f"Track with path '{path}' already exists in database")
|
1798
|
+
|
1799
|
+
id_ = self.generate_unused_id(tables.DjmdContent)
|
1800
|
+
file_id = self.generate_unused_id(tables.DjmdContent, id_field_name="rb_file_id")
|
1801
|
+
uuid = str(uuid4())
|
1802
|
+
content_link = self.get_menu_items(Name="TRACK").one()
|
1803
|
+
date_created = datetime.date.today()
|
1804
|
+
device = self.get_device().first()
|
1805
|
+
file_name_l = path.name
|
1806
|
+
file_size = path.stat().st_size
|
1807
|
+
|
1808
|
+
file_type_string = path.suffix.lstrip(".").upper()
|
1809
|
+
try:
|
1810
|
+
file_type = getattr(FileType, file_type_string)
|
1811
|
+
except ValueError:
|
1812
|
+
raise ValueError(f"Invalid file type: {path.suffix}")
|
1813
|
+
|
1814
|
+
content = tables.DjmdContent.create(
|
1815
|
+
ID=id_,
|
1816
|
+
UUID=uuid,
|
1817
|
+
ContentLink=content_link.rb_local_usn,
|
1818
|
+
DateCreated=date_created,
|
1819
|
+
DeviceID=device.ID,
|
1820
|
+
FileNameL=file_name_l,
|
1821
|
+
FileSize=file_size,
|
1822
|
+
FileType=file_type.value,
|
1823
|
+
FolderPath=path_string,
|
1824
|
+
HotCueAutoLoad="on",
|
1825
|
+
MasterDBID=device.MasterDBID,
|
1826
|
+
MasterSongID=id_,
|
1827
|
+
StockDate=date_created,
|
1828
|
+
rb_file_id=file_id,
|
1829
|
+
**kwargs,
|
1830
|
+
)
|
1831
|
+
self.add(content)
|
1832
|
+
self.flush()
|
1833
|
+
return content
|
1834
|
+
|
1878
1835
|
# ----------------------------------------------------------------------------------
|
1879
1836
|
|
1880
1837
|
def get_mysetting_paths(self):
|
@@ -1997,9 +1954,7 @@ class Rekordbox6Database:
|
|
1997
1954
|
return AnlzFile.parse_file(path)
|
1998
1955
|
return None
|
1999
1956
|
|
2000
|
-
def update_content_path(
|
2001
|
-
self, content, path, save=True, check_path=True, commit=True
|
2002
|
-
):
|
1957
|
+
def update_content_path(self, content, path, save=True, check_path=True, commit=True):
|
2003
1958
|
"""Update the file path of a track in the Rekordbox v6 database.
|
2004
1959
|
|
2005
1960
|
This changes the `FolderPath` entry in the ``DjmdContent`` table and the
|
@@ -2090,9 +2045,7 @@ class Rekordbox6Database:
|
|
2090
2045
|
logger.debug("Committing changes to the database")
|
2091
2046
|
self.commit()
|
2092
2047
|
|
2093
|
-
def update_content_filename(
|
2094
|
-
self, content, name, save=True, check_path=True, commit=True
|
2095
|
-
):
|
2048
|
+
def update_content_filename(self, content, name, save=True, check_path=True, commit=True):
|
2096
2049
|
"""Update the file name of a track in the Rekordbox v6 database.
|
2097
2050
|
|
2098
2051
|
This changes the `FolderPath` entry in the ``DjmdContent`` table and the
|
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
|
|
@@ -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
|
@@ -320,11 +317,12 @@ class SmartList:
|
|
320
317
|
comps = list()
|
321
318
|
for cond in self.conditions:
|
322
319
|
val_left, val_right = _get_condition_values(cond)
|
323
|
-
|
320
|
+
# val_left = str(-abs(int(val_left))) if val_left is not None else ""
|
324
321
|
if cond.property in PROPERTY_COLUMN_MAP:
|
325
322
|
colum_name = PROPERTY_COLUMN_MAP[cond.property]
|
326
323
|
if cond.property == Property.MYTAG:
|
327
|
-
|
324
|
+
if int(val_left) < 0:
|
325
|
+
val_left = str(right_bitshift(int(val_left)))
|
328
326
|
|
329
327
|
if cond.operator == Operator.EQUAL:
|
330
328
|
comp = getattr(DjmdContent, colum_name) == val_left
|