pyrekordbox 0.4.2__py3-none-any.whl → 0.4.4__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 +1 -0
- pyrekordbox/__main__.py +1 -51
- pyrekordbox/_version.py +16 -3
- pyrekordbox/anlz/__init__.py +6 -6
- pyrekordbox/anlz/file.py +56 -43
- pyrekordbox/anlz/tags.py +108 -70
- pyrekordbox/config.py +18 -356
- pyrekordbox/db6/aux_files.py +40 -14
- pyrekordbox/db6/database.py +384 -236
- pyrekordbox/db6/registry.py +48 -34
- pyrekordbox/db6/smartlist.py +12 -12
- pyrekordbox/db6/tables.py +60 -58
- pyrekordbox/mysettings/__init__.py +3 -2
- pyrekordbox/mysettings/file.py +27 -24
- pyrekordbox/rbxml.py +321 -142
- pyrekordbox/utils.py +27 -6
- {pyrekordbox-0.4.2.dist-info → pyrekordbox-0.4.4.dist-info}/METADATA +13 -38
- pyrekordbox-0.4.4.dist-info/RECORD +25 -0
- {pyrekordbox-0.4.2.dist-info → pyrekordbox-0.4.4.dist-info}/WHEEL +1 -1
- {pyrekordbox-0.4.2.dist-info → pyrekordbox-0.4.4.dist-info}/licenses/LICENSE +1 -1
- pyrekordbox-0.4.2.dist-info/RECORD +0 -25
- {pyrekordbox-0.4.2.dist-info → pyrekordbox-0.4.4.dist-info}/top_level.txt +0 -0
pyrekordbox/db6/database.py
CHANGED
@@ -6,7 +6,8 @@ import datetime
|
|
6
6
|
import logging
|
7
7
|
import secrets
|
8
8
|
from pathlib import Path
|
9
|
-
from
|
9
|
+
from types import TracebackType
|
10
|
+
from typing import Any, Callable, Dict, List, Optional, Type, TypeVar, Union
|
10
11
|
from uuid import uuid4
|
11
12
|
|
12
13
|
from sqlalchemy import MetaData, create_engine, event, or_, select
|
@@ -14,46 +15,54 @@ from sqlalchemy.exc import NoResultFound
|
|
14
15
|
from sqlalchemy.orm import Query, Session
|
15
16
|
from sqlalchemy.sql.sqltypes import DateTime, String
|
16
17
|
|
17
|
-
from ..anlz import AnlzFile, get_anlz_paths, read_anlz_files
|
18
|
+
from ..anlz import AnlzFile, get_anlz_paths, read_anlz_files # type: ignore[attr-defined]
|
18
19
|
from ..config import get_config
|
19
|
-
from ..utils import get_rekordbox_pid
|
20
|
+
from ..utils import deobfuscate, get_rekordbox_pid
|
20
21
|
from . import tables
|
21
22
|
from .aux_files import MasterPlaylistXml
|
22
23
|
from .registry import RekordboxAgentRegistry
|
23
24
|
from .smartlist import SmartList
|
24
|
-
from .tables import DjmdContent, FileType, PlaylistType
|
25
|
+
from .tables import DjmdContent, DjmdPlaylist, DjmdSongPlaylist, FileType, PlaylistType
|
25
26
|
|
26
27
|
try:
|
27
28
|
from sqlcipher3 import dbapi2 as sqlite3 # noqa
|
28
29
|
|
29
30
|
_sqlcipher_available = True
|
30
31
|
except ImportError: # pragma: no cover
|
31
|
-
import sqlite3
|
32
|
+
import sqlite3 # type: ignore[no-redef]
|
32
33
|
|
33
34
|
_sqlcipher_available = False
|
34
35
|
|
35
|
-
MAX_VERSION = "6.6.5"
|
36
36
|
SPECIAL_PLAYLIST_IDS = [
|
37
37
|
"100000", # Cloud Library Sync
|
38
38
|
"200000", # CUE Analysis Playlist
|
39
39
|
]
|
40
40
|
|
41
|
-
|
41
|
+
BLOB = b"PN_Pq^*N>(JYe*u^8;Yg76HuZ<mR13S?=>)b9;DpoTXV(6ItkU`}8*m6tx_I{Solh_N#dfe{v="
|
42
42
|
|
43
|
+
logger = logging.getLogger(__name__)
|
43
44
|
|
44
|
-
|
45
|
-
|
45
|
+
PathLike = Union[str, Path]
|
46
|
+
ContentLike = Union[DjmdContent, int, str]
|
47
|
+
PlaylistLike = Union[DjmdPlaylist, int, str]
|
48
|
+
T = TypeVar("T", bound=tables.Base)
|
46
49
|
|
47
50
|
|
48
|
-
def _parse_query_result(query, kwargs):
|
51
|
+
def _parse_query_result(query: Query[T], kwargs: Dict[str, Any]) -> Any:
|
49
52
|
if "ID" in kwargs or "registry_id" in kwargs:
|
50
53
|
try:
|
51
|
-
|
54
|
+
result: T = query.one()
|
55
|
+
return result
|
52
56
|
except NoResultFound:
|
53
57
|
return None
|
54
58
|
return query
|
55
59
|
|
56
60
|
|
61
|
+
class SessionNotInitializedError(Exception):
|
62
|
+
def __init__(self) -> None:
|
63
|
+
super().__init__("Sqlite-session not intialized!")
|
64
|
+
|
65
|
+
|
57
66
|
class Rekordbox6Database:
|
58
67
|
"""Rekordbox v6 master.db database handler.
|
59
68
|
|
@@ -103,7 +112,9 @@ class Rekordbox6Database:
|
|
103
112
|
<DjmdContent(40110712 Title=NOISE)>
|
104
113
|
"""
|
105
114
|
|
106
|
-
def __init__(
|
115
|
+
def __init__(
|
116
|
+
self, path: PathLike = None, db_dir: PathLike = "", key: str = "", unlock: bool = True
|
117
|
+
):
|
107
118
|
# get config of latest supported version
|
108
119
|
rb_config = get_config("rekordbox7")
|
109
120
|
if not rb_config:
|
@@ -119,73 +130,67 @@ class Rekordbox6Database:
|
|
119
130
|
if not path:
|
120
131
|
pdir = get_config("pioneer", "install_dir")
|
121
132
|
raise FileNotFoundError(f"No Rekordbox v6/v7 directory found in '{pdir}'")
|
122
|
-
|
133
|
+
db_path: Path = Path(path)
|
123
134
|
# make sure file exists
|
124
|
-
if not
|
125
|
-
raise FileNotFoundError(f"File '{
|
135
|
+
if not db_path.exists():
|
136
|
+
raise FileNotFoundError(f"File '{db_path}' does not exist!")
|
126
137
|
# Open database
|
127
138
|
if unlock:
|
128
139
|
if not _sqlcipher_available: # pragma: no cover
|
129
140
|
raise ImportError("Could not unlock database: 'sqlcipher3' package not found")
|
141
|
+
|
130
142
|
if not key: # pragma: no cover
|
131
|
-
|
132
|
-
|
133
|
-
except KeyError:
|
134
|
-
raise NoCachedKey(
|
135
|
-
"Could not unlock database: No key found\n"
|
136
|
-
f"If you are using Rekordbox>{MAX_VERSION} the key cannot be "
|
137
|
-
f"extracted automatically!\n"
|
138
|
-
"Please use the CLI of pyrekordbox to download the key or "
|
139
|
-
"use the `key` parameter to manually provide it."
|
140
|
-
)
|
141
|
-
else:
|
143
|
+
key = deobfuscate(BLOB)
|
144
|
+
elif not key.startswith("402fd"):
|
142
145
|
# Check if key looks like a valid key
|
143
|
-
|
144
|
-
raise ValueError("The provided database key doesn't look valid!")
|
146
|
+
raise ValueError("The provided database key doesn't look valid!")
|
145
147
|
|
146
|
-
logger.debug("Key: %s", key)
|
147
148
|
# Unlock database and create engine
|
148
|
-
|
149
|
+
logger.debug("Key: %s", key)
|
150
|
+
url = f"sqlite+pysqlcipher://:{key}@/{db_path}?"
|
149
151
|
engine = create_engine(url, module=sqlite3)
|
150
152
|
else:
|
151
|
-
engine = create_engine(f"sqlite:///{
|
153
|
+
engine = create_engine(f"sqlite:///{db_path}")
|
152
154
|
|
153
155
|
if not db_dir:
|
154
|
-
db_dir =
|
155
|
-
|
156
|
-
if not
|
157
|
-
raise FileNotFoundError(f"Database directory '{
|
156
|
+
db_dir = db_path.parent
|
157
|
+
db_directory: Path = Path(db_dir)
|
158
|
+
if not db_directory.exists():
|
159
|
+
raise FileNotFoundError(f"Database directory '{db_directory}' does not exist!")
|
158
160
|
|
159
161
|
self.engine = engine
|
160
162
|
self.session: Optional[Session] = None
|
161
163
|
|
162
164
|
self.registry = RekordboxAgentRegistry(self)
|
163
|
-
self._events = dict()
|
165
|
+
self._events: Dict[str, Callable[[Any], None]] = dict()
|
166
|
+
self.playlist_xml: Optional[MasterPlaylistXml]
|
164
167
|
try:
|
165
|
-
self.playlist_xml = MasterPlaylistXml(db_dir=
|
168
|
+
self.playlist_xml = MasterPlaylistXml(db_dir=db_directory)
|
166
169
|
except FileNotFoundError:
|
167
|
-
logger.warning(f"No masterPlaylists6.xml found in {
|
170
|
+
logger.warning(f"No masterPlaylists6.xml found in {db_directory}")
|
168
171
|
self.playlist_xml = None
|
169
172
|
|
170
|
-
self._db_dir =
|
171
|
-
self._share_dir =
|
173
|
+
self._db_dir = db_directory
|
174
|
+
self._share_dir: Path = db_directory / "share"
|
172
175
|
|
173
176
|
self.open()
|
174
177
|
|
175
178
|
@property
|
176
|
-
def no_autoflush(self):
|
179
|
+
def no_autoflush(self) -> Any:
|
177
180
|
"""Creates a no-autoflush context."""
|
181
|
+
if self.session is None:
|
182
|
+
raise SessionNotInitializedError()
|
178
183
|
return self.session.no_autoflush
|
179
184
|
|
180
185
|
@property
|
181
|
-
def db_directory(self):
|
186
|
+
def db_directory(self) -> Path:
|
182
187
|
return self._db_dir
|
183
188
|
|
184
189
|
@property
|
185
|
-
def share_directory(self):
|
190
|
+
def share_directory(self) -> Path:
|
186
191
|
return self._share_dir
|
187
192
|
|
188
|
-
def open(self):
|
193
|
+
def open(self) -> None:
|
189
194
|
"""Open the database by instantiating a new session using the SQLAchemy engine.
|
190
195
|
|
191
196
|
A new session instance is only created if the session was closed previously.
|
@@ -200,21 +205,28 @@ class Rekordbox6Database:
|
|
200
205
|
self.session = Session(bind=self.engine)
|
201
206
|
self.registry.clear_buffer()
|
202
207
|
|
203
|
-
def close(self):
|
208
|
+
def close(self) -> None:
|
204
209
|
"""Close the currently active session."""
|
210
|
+
if self.session is None:
|
211
|
+
raise SessionNotInitializedError()
|
205
212
|
for key in self._events:
|
206
213
|
self.unregister_event(key)
|
207
214
|
self.registry.clear_buffer()
|
208
215
|
self.session.close()
|
209
216
|
self.session = None
|
210
217
|
|
211
|
-
def __enter__(self):
|
218
|
+
def __enter__(self) -> "Rekordbox6Database":
|
212
219
|
return self
|
213
220
|
|
214
|
-
def __exit__(
|
221
|
+
def __exit__(
|
222
|
+
self,
|
223
|
+
type_: Optional[Type[BaseException]],
|
224
|
+
value: Optional[BaseException],
|
225
|
+
traceback: Optional[TracebackType],
|
226
|
+
) -> None:
|
215
227
|
self.close()
|
216
228
|
|
217
|
-
def register_event(self, identifier, fn):
|
229
|
+
def register_event(self, identifier: str, fn: Callable[[Any], None]) -> None:
|
218
230
|
"""Registers a session event callback.
|
219
231
|
|
220
232
|
Parameters
|
@@ -225,10 +237,12 @@ class Rekordbox6Database:
|
|
225
237
|
fn : callable
|
226
238
|
The event callback method.
|
227
239
|
"""
|
240
|
+
if self.session is None:
|
241
|
+
raise SessionNotInitializedError()
|
228
242
|
event.listen(self.session, identifier, fn)
|
229
243
|
self._events[identifier] = fn
|
230
244
|
|
231
|
-
def unregister_event(self, identifier):
|
245
|
+
def unregister_event(self, identifier: str) -> None:
|
232
246
|
"""Removes an existing session event callback.
|
233
247
|
|
234
248
|
Parameters
|
@@ -236,10 +250,12 @@ class Rekordbox6Database:
|
|
236
250
|
identifier : str
|
237
251
|
The identifier of the event
|
238
252
|
"""
|
253
|
+
if self.session is None:
|
254
|
+
raise SessionNotInitializedError()
|
239
255
|
fn = self._events[identifier]
|
240
256
|
event.remove(self.session, identifier, fn)
|
241
257
|
|
242
|
-
def query(self, *entities, **kwargs):
|
258
|
+
def query(self, *entities: Any, **kwargs: Any) -> Any:
|
243
259
|
"""Creates a new SQL query for the given entities.
|
244
260
|
|
245
261
|
Parameters
|
@@ -266,9 +282,11 @@ class Rekordbox6Database:
|
|
266
282
|
>>> db = Rekordbox6Database()
|
267
283
|
>>> query = db.query(DjmdContent.Title)
|
268
284
|
"""
|
285
|
+
if self.session is None:
|
286
|
+
raise SessionNotInitializedError()
|
269
287
|
return self.session.query(*entities, **kwargs)
|
270
288
|
|
271
|
-
def add(self, instance):
|
289
|
+
def add(self, instance: tables.Base) -> None:
|
272
290
|
"""Add an element to the Rekordbox database.
|
273
291
|
|
274
292
|
Parameters
|
@@ -276,10 +294,12 @@ class Rekordbox6Database:
|
|
276
294
|
instance : tables.Base
|
277
295
|
The table entry to add.
|
278
296
|
"""
|
297
|
+
if self.session is None:
|
298
|
+
raise SessionNotInitializedError()
|
279
299
|
self.session.add(instance)
|
280
300
|
self.registry.on_create(instance)
|
281
301
|
|
282
|
-
def delete(self, instance):
|
302
|
+
def delete(self, instance: tables.Base) -> None:
|
283
303
|
"""Delete an element from the Rekordbox database.
|
284
304
|
|
285
305
|
Parameters
|
@@ -287,10 +307,12 @@ class Rekordbox6Database:
|
|
287
307
|
instance : tables.Base
|
288
308
|
The table entry to delte.
|
289
309
|
"""
|
310
|
+
if self.session is None:
|
311
|
+
raise SessionNotInitializedError()
|
290
312
|
self.session.delete(instance)
|
291
313
|
self.registry.on_delete(instance)
|
292
314
|
|
293
|
-
def get_local_usn(self):
|
315
|
+
def get_local_usn(self) -> int:
|
294
316
|
"""Returns the local sequence number (update count) of Rekordbox.
|
295
317
|
|
296
318
|
Any changes made to the `Djmd...` tables increments the local update count of
|
@@ -304,7 +326,7 @@ class Rekordbox6Database:
|
|
304
326
|
"""
|
305
327
|
return self.registry.get_local_update_count()
|
306
328
|
|
307
|
-
def set_local_usn(self, usn):
|
329
|
+
def set_local_usn(self, usn: int) -> None:
|
308
330
|
"""Sets the local sequence number (update count) of Rekordbox.
|
309
331
|
|
310
332
|
Parameters
|
@@ -314,7 +336,7 @@ class Rekordbox6Database:
|
|
314
336
|
"""
|
315
337
|
self.registry.set_local_update_count(usn)
|
316
338
|
|
317
|
-
def increment_local_usn(self, num=1):
|
339
|
+
def increment_local_usn(self, num: int = 1) -> int:
|
318
340
|
"""Increments the local update sequence number (update count) of Rekordbox.
|
319
341
|
|
320
342
|
Parameters
|
@@ -342,7 +364,7 @@ class Rekordbox6Database:
|
|
342
364
|
"""
|
343
365
|
return self.registry.increment_local_update_count(num)
|
344
366
|
|
345
|
-
def autoincrement_usn(self, set_row_usn=True):
|
367
|
+
def autoincrement_usn(self, set_row_usn: bool = True) -> int:
|
346
368
|
"""Auto-increments the local USN for all uncommited changes.
|
347
369
|
|
348
370
|
Parameters
|
@@ -372,11 +394,13 @@ class Rekordbox6Database:
|
|
372
394
|
"""
|
373
395
|
return self.registry.autoincrement_local_update_count(set_row_usn)
|
374
396
|
|
375
|
-
def flush(self):
|
397
|
+
def flush(self) -> None:
|
376
398
|
"""Flushes the buffer of the SQLAlchemy session instance."""
|
399
|
+
if self.session is None:
|
400
|
+
raise SessionNotInitializedError()
|
377
401
|
self.session.flush()
|
378
402
|
|
379
|
-
def commit(self, autoinc=True):
|
403
|
+
def commit(self, autoinc: bool = True) -> None:
|
380
404
|
"""Commit the changes made to the database.
|
381
405
|
|
382
406
|
Parameters
|
@@ -389,6 +413,8 @@ class Rekordbox6Database:
|
|
389
413
|
--------
|
390
414
|
autoincrement_usn : Auto-increments the local Rekordbox USN's.
|
391
415
|
"""
|
416
|
+
if self.session is None:
|
417
|
+
raise SessionNotInitializedError()
|
392
418
|
pid = get_rekordbox_pid()
|
393
419
|
if pid:
|
394
420
|
raise RuntimeError(
|
@@ -423,45 +449,47 @@ class Rekordbox6Database:
|
|
423
449
|
if self.playlist_xml.modified:
|
424
450
|
self.playlist_xml.save()
|
425
451
|
|
426
|
-
def rollback(self):
|
452
|
+
def rollback(self) -> None:
|
427
453
|
"""Rolls back the uncommited changes to the database."""
|
454
|
+
if self.session is None:
|
455
|
+
raise SessionNotInitializedError()
|
428
456
|
self.session.rollback()
|
429
457
|
self.registry.clear_buffer()
|
430
458
|
|
431
459
|
# -- Table queries -----------------------------------------------------------------
|
432
460
|
|
433
|
-
def get_active_censor(self, **kwargs):
|
461
|
+
def get_active_censor(self, **kwargs: Any) -> Any:
|
434
462
|
"""Creates a filtered query for the ``DjmdActiveCensor`` table."""
|
435
463
|
query = self.query(tables.DjmdActiveCensor).filter_by(**kwargs)
|
436
464
|
return _parse_query_result(query, kwargs)
|
437
465
|
|
438
|
-
def get_album(self, **kwargs):
|
466
|
+
def get_album(self, **kwargs: Any) -> Any:
|
439
467
|
"""Creates a filtered query for the ``DjmdAlbum`` table."""
|
440
468
|
query = self.query(tables.DjmdAlbum).filter_by(**kwargs)
|
441
469
|
return _parse_query_result(query, kwargs)
|
442
470
|
|
443
|
-
def get_artist(self, **kwargs):
|
471
|
+
def get_artist(self, **kwargs: Any) -> Any:
|
444
472
|
"""Creates a filtered query for the ``DjmdArtist`` table."""
|
445
473
|
query = self.query(tables.DjmdArtist).filter_by(**kwargs)
|
446
474
|
return _parse_query_result(query, kwargs)
|
447
475
|
|
448
|
-
def get_category(self, **kwargs):
|
476
|
+
def get_category(self, **kwargs: Any) -> Any:
|
449
477
|
"""Creates a filtered query for the ``DjmdCategory`` table."""
|
450
478
|
query = self.query(tables.DjmdCategory).filter_by(**kwargs)
|
451
479
|
return _parse_query_result(query, kwargs)
|
452
480
|
|
453
|
-
def get_color(self, **kwargs):
|
481
|
+
def get_color(self, **kwargs: Any) -> Any:
|
454
482
|
"""Creates a filtered query for the ``DjmdColor`` table."""
|
455
483
|
query = self.query(tables.DjmdColor).filter_by(**kwargs)
|
456
484
|
return _parse_query_result(query, kwargs)
|
457
485
|
|
458
|
-
def get_content(self, **kwargs):
|
486
|
+
def get_content(self, **kwargs: Any) -> Any:
|
459
487
|
"""Creates a filtered query for the ``DjmdContent`` table."""
|
460
488
|
query = self.query(tables.DjmdContent).filter_by(**kwargs)
|
461
489
|
return _parse_query_result(query, kwargs)
|
462
490
|
|
463
491
|
# noinspection PyUnresolvedReferences
|
464
|
-
def search_content(self, text):
|
492
|
+
def search_content(self, text: str) -> List[DjmdContent]:
|
465
493
|
"""Searches the contents of the ``DjmdContent`` table.
|
466
494
|
|
467
495
|
The search is case-insensitive and includes the following collumns of the
|
@@ -514,86 +542,86 @@ class Rekordbox6Database:
|
|
514
542
|
query = self.query(DjmdContent).join(DjmdContent.Key)
|
515
543
|
results.update(query.filter(tables.DjmdKey.ScaleName.contains(text)).all())
|
516
544
|
|
517
|
-
|
518
|
-
|
519
|
-
return
|
545
|
+
result_list: List[DjmdContent] = list(results)
|
546
|
+
result_list.sort(key=lambda x: x.ID)
|
547
|
+
return result_list
|
520
548
|
|
521
|
-
def get_cue(self, **kwargs):
|
549
|
+
def get_cue(self, **kwargs: Any) -> Any:
|
522
550
|
"""Creates a filtered query for the ``DjmdCue`` table."""
|
523
551
|
query = self.query(tables.DjmdCue).filter_by(**kwargs)
|
524
552
|
return _parse_query_result(query, kwargs)
|
525
553
|
|
526
|
-
def get_device(self, **kwargs):
|
554
|
+
def get_device(self, **kwargs: Any) -> Any:
|
527
555
|
"""Creates a filtered query for the ``DjmdDevice`` table."""
|
528
556
|
query = self.query(tables.DjmdDevice).filter_by(**kwargs)
|
529
557
|
return _parse_query_result(query, kwargs)
|
530
558
|
|
531
|
-
def get_genre(self, **kwargs):
|
559
|
+
def get_genre(self, **kwargs: Any) -> Any:
|
532
560
|
"""Creates a filtered query for the ``DjmdGenre`` table."""
|
533
561
|
query = self.query(tables.DjmdGenre).filter_by(**kwargs)
|
534
562
|
return _parse_query_result(query, kwargs)
|
535
563
|
|
536
|
-
def get_history(self, **kwargs):
|
564
|
+
def get_history(self, **kwargs: Any) -> Any:
|
537
565
|
"""Creates a filtered query for the ``DjmdHistory`` table."""
|
538
566
|
query = self.query(tables.DjmdHistory).filter_by(**kwargs)
|
539
567
|
return _parse_query_result(query, kwargs)
|
540
568
|
|
541
|
-
def get_history_songs(self, **kwargs):
|
569
|
+
def get_history_songs(self, **kwargs: Any) -> Any:
|
542
570
|
"""Creates a filtered query for the ``DjmdSongHistory`` table."""
|
543
571
|
query = self.query(tables.DjmdSongHistory).filter_by(**kwargs)
|
544
572
|
return _parse_query_result(query, kwargs)
|
545
573
|
|
546
|
-
def get_hot_cue_banklist(self, **kwargs):
|
574
|
+
def get_hot_cue_banklist(self, **kwargs: Any) -> Any:
|
547
575
|
"""Creates a filtered query for the ``DjmdHotCueBanklist`` table."""
|
548
576
|
query = self.query(tables.DjmdHotCueBanklist).filter_by(**kwargs)
|
549
577
|
return _parse_query_result(query, kwargs)
|
550
578
|
|
551
|
-
def get_hot_cue_banklist_songs(self, **kwargs):
|
579
|
+
def get_hot_cue_banklist_songs(self, **kwargs: Any) -> Any:
|
552
580
|
"""Creates a filtered query for the ``DjmdSongHotCueBanklist`` table."""
|
553
581
|
query = self.query(tables.DjmdSongHotCueBanklist).filter_by(**kwargs)
|
554
582
|
return _parse_query_result(query, kwargs)
|
555
583
|
|
556
|
-
def get_key(self, **kwargs):
|
584
|
+
def get_key(self, **kwargs: Any) -> Any:
|
557
585
|
"""Creates a filtered query for the ``DjmdKey`` table."""
|
558
586
|
query = self.query(tables.DjmdKey).filter_by(**kwargs)
|
559
587
|
return _parse_query_result(query, kwargs)
|
560
588
|
|
561
|
-
def get_label(self, **kwargs):
|
589
|
+
def get_label(self, **kwargs: Any) -> Any:
|
562
590
|
"""Creates a filtered query for the ``DjmdLabel`` table."""
|
563
591
|
query = self.query(tables.DjmdLabel).filter_by(**kwargs)
|
564
592
|
return _parse_query_result(query, kwargs)
|
565
593
|
|
566
|
-
def get_menu_items(self, **kwargs):
|
594
|
+
def get_menu_items(self, **kwargs: Any) -> Any:
|
567
595
|
"""Creates a filtered query for the ``DjmdMenuItems`` table."""
|
568
596
|
query = self.query(tables.DjmdMenuItems).filter_by(**kwargs)
|
569
597
|
return _parse_query_result(query, kwargs)
|
570
598
|
|
571
|
-
def get_mixer_param(self, **kwargs):
|
599
|
+
def get_mixer_param(self, **kwargs: Any) -> Any:
|
572
600
|
"""Creates a filtered query for the ``DjmdMixerParam`` table."""
|
573
601
|
query = self.query(tables.DjmdMixerParam).filter_by(**kwargs)
|
574
602
|
return _parse_query_result(query, kwargs)
|
575
603
|
|
576
|
-
def get_my_tag(self, **kwargs):
|
604
|
+
def get_my_tag(self, **kwargs: Any) -> Any:
|
577
605
|
"""Creates a filtered query for the ``DjmdMyTag`` table."""
|
578
606
|
query = self.query(tables.DjmdMyTag).filter_by(**kwargs)
|
579
607
|
return _parse_query_result(query, kwargs)
|
580
608
|
|
581
|
-
def get_my_tag_songs(self, **kwargs):
|
609
|
+
def get_my_tag_songs(self, **kwargs: Any) -> Any:
|
582
610
|
"""Creates a filtered query for the ``DjmdSongMyTag`` table."""
|
583
611
|
query = self.query(tables.DjmdSongMyTag).filter_by(**kwargs)
|
584
612
|
return _parse_query_result(query, kwargs)
|
585
613
|
|
586
|
-
def get_playlist(self, **kwargs):
|
614
|
+
def get_playlist(self, **kwargs: Any) -> Any:
|
587
615
|
"""Creates a filtered query for the ``DjmdPlaylist`` table."""
|
588
616
|
query = self.query(tables.DjmdPlaylist).filter_by(**kwargs)
|
589
617
|
return _parse_query_result(query, kwargs)
|
590
618
|
|
591
|
-
def get_playlist_songs(self, **kwargs):
|
619
|
+
def get_playlist_songs(self, **kwargs: Any) -> Any:
|
592
620
|
"""Creates a filtered query for the ``DjmdSongPlaylist`` table."""
|
593
621
|
query = self.query(tables.DjmdSongPlaylist).filter_by(**kwargs)
|
594
622
|
return _parse_query_result(query, kwargs)
|
595
623
|
|
596
|
-
def get_playlist_contents(self, playlist, *entities) ->
|
624
|
+
def get_playlist_contents(self, playlist: PlaylistLike, *entities: tables.Base) -> Any:
|
597
625
|
"""Return the contents of a regular or smart playlist.
|
598
626
|
|
599
627
|
Parameters
|
@@ -625,111 +653,117 @@ class Rekordbox6Database:
|
|
625
653
|
>>> db.get_playlist_contents(pl, DjmdContent.ID).all()
|
626
654
|
[('12345678',), ('23456789',)]
|
627
655
|
"""
|
656
|
+
plist: DjmdPlaylist
|
628
657
|
if isinstance(playlist, (int, str)):
|
629
|
-
|
630
|
-
|
631
|
-
|
658
|
+
plist = self.get_playlist(ID=playlist)
|
659
|
+
else:
|
660
|
+
plist = playlist
|
661
|
+
|
662
|
+
if plist.is_folder:
|
663
|
+
raise ValueError(f"Playlist {plist} is a playlist folder.")
|
632
664
|
|
633
665
|
if not entities:
|
634
666
|
entities = [
|
635
667
|
DjmdContent,
|
636
|
-
]
|
668
|
+
] # type: ignore[assignment]
|
637
669
|
|
638
|
-
if
|
670
|
+
if plist.is_smart_playlist:
|
639
671
|
smartlist = SmartList()
|
640
|
-
smartlist.parse(
|
672
|
+
smartlist.parse(plist.SmartList)
|
641
673
|
filter_clause = smartlist.filter_clause()
|
642
674
|
else:
|
643
675
|
sub_query = self.query(tables.DjmdSongPlaylist.ContentID).filter(
|
644
|
-
tables.DjmdSongPlaylist.PlaylistID ==
|
676
|
+
tables.DjmdSongPlaylist.PlaylistID == plist.ID
|
645
677
|
)
|
646
678
|
filter_clause = DjmdContent.ID.in_(select(sub_query.subquery()))
|
647
679
|
|
648
680
|
return self.query(*entities).filter(filter_clause)
|
649
681
|
|
650
|
-
def get_property(self, **kwargs):
|
682
|
+
def get_property(self, **kwargs: Any) -> Any:
|
651
683
|
"""Creates a filtered query for the ``DjmdProperty`` table."""
|
652
684
|
query = self.query(tables.DjmdProperty).filter_by(**kwargs)
|
653
685
|
return _parse_query_result(query, kwargs)
|
654
686
|
|
655
|
-
def get_related_tracks(self, **kwargs):
|
687
|
+
def get_related_tracks(self, **kwargs: Any) -> Any:
|
656
688
|
"""Creates a filtered query for the ``DjmdRelatedTracks`` table."""
|
657
689
|
query = self.query(tables.DjmdRelatedTracks).filter_by(**kwargs)
|
658
690
|
return _parse_query_result(query, kwargs)
|
659
691
|
|
660
|
-
def get_related_tracks_songs(self, **kwargs):
|
692
|
+
def get_related_tracks_songs(self, **kwargs: Any) -> Any:
|
661
693
|
"""Creates a filtered query for the ``DjmdSongRelatedTracks`` table."""
|
662
694
|
query = self.query(tables.DjmdSongRelatedTracks).filter_by(**kwargs)
|
663
695
|
return _parse_query_result(query, kwargs)
|
664
696
|
|
665
|
-
def get_sampler(self, **kwargs):
|
697
|
+
def get_sampler(self, **kwargs: Any) -> Any:
|
666
698
|
"""Creates a filtered query for the ``DjmdSampler`` table."""
|
667
699
|
query = self.query(tables.DjmdSampler).filter_by(**kwargs)
|
668
700
|
return _parse_query_result(query, kwargs)
|
669
701
|
|
670
|
-
def get_sampler_songs(self, **kwargs):
|
702
|
+
def get_sampler_songs(self, **kwargs: Any) -> Any:
|
671
703
|
"""Creates a filtered query for the ``DjmdSongSampler`` table."""
|
672
704
|
query = self.query(tables.DjmdSongSampler).filter_by(**kwargs)
|
673
705
|
return _parse_query_result(query, kwargs)
|
674
706
|
|
675
|
-
def get_tag_list_songs(self, **kwargs):
|
707
|
+
def get_tag_list_songs(self, **kwargs: Any) -> Any:
|
676
708
|
"""Creates a filtered query for the ``DjmdSongTagList`` table."""
|
677
709
|
query = self.query(tables.DjmdSongTagList).filter_by(**kwargs)
|
678
710
|
return _parse_query_result(query, kwargs)
|
679
711
|
|
680
|
-
def get_sort(self, **kwargs):
|
712
|
+
def get_sort(self, **kwargs: Any) -> Any:
|
681
713
|
"""Creates a filtered query for the ``DjmdSort`` table."""
|
682
714
|
query = self.query(tables.DjmdSort).filter_by(**kwargs)
|
683
715
|
return _parse_query_result(query, kwargs)
|
684
716
|
|
685
|
-
def get_agent_registry(self, **kwargs):
|
717
|
+
def get_agent_registry(self, **kwargs: Any) -> Any:
|
686
718
|
"""Creates a filtered query for the ``AgentRegistry`` table."""
|
687
719
|
query = self.query(tables.AgentRegistry).filter_by(**kwargs)
|
688
720
|
return _parse_query_result(query, kwargs)
|
689
721
|
|
690
|
-
def get_cloud_agent_registry(self, **kwargs):
|
722
|
+
def get_cloud_agent_registry(self, **kwargs: Any) -> Any:
|
691
723
|
"""Creates a filtered query for the ``CloudAgentRegistry`` table."""
|
692
724
|
query = self.query(tables.CloudAgentRegistry).filter_by(**kwargs)
|
693
725
|
return _parse_query_result(query, kwargs)
|
694
726
|
|
695
|
-
def get_content_active_censor(self, **kwargs):
|
727
|
+
def get_content_active_censor(self, **kwargs: Any) -> Any:
|
696
728
|
"""Creates a filtered query for the ``ContentActiveCensor`` table."""
|
697
729
|
query = self.query(tables.ContentActiveCensor).filter_by(**kwargs)
|
698
730
|
return _parse_query_result(query, kwargs)
|
699
731
|
|
700
|
-
def get_content_cue(self, **kwargs):
|
732
|
+
def get_content_cue(self, **kwargs: Any) -> Any:
|
701
733
|
"""Creates a filtered query for the ``ContentCue`` table."""
|
702
734
|
query = self.query(tables.ContentCue).filter_by(**kwargs)
|
703
735
|
return _parse_query_result(query, kwargs)
|
704
736
|
|
705
|
-
def get_content_file(self, **kwargs):
|
737
|
+
def get_content_file(self, **kwargs: Any) -> Any:
|
706
738
|
"""Creates a filtered query for the ``ContentFile`` table."""
|
707
739
|
query = self.query(tables.ContentFile).filter_by(**kwargs)
|
708
740
|
return _parse_query_result(query, kwargs)
|
709
741
|
|
710
|
-
def get_hot_cue_banklist_cue(self, **kwargs):
|
742
|
+
def get_hot_cue_banklist_cue(self, **kwargs: Any) -> Any:
|
711
743
|
"""Creates a filtered query for the ``HotCueBanklistCue`` table."""
|
712
744
|
query = self.query(tables.HotCueBanklistCue).filter_by(**kwargs)
|
713
745
|
return _parse_query_result(query, kwargs)
|
714
746
|
|
715
|
-
def get_image_file(self, **kwargs):
|
747
|
+
def get_image_file(self, **kwargs: Any) -> Any:
|
716
748
|
"""Creates a filtered query for the ``ImageFile`` table."""
|
717
749
|
query = self.query(tables.ImageFile).filter_by(**kwargs)
|
718
750
|
return _parse_query_result(query, kwargs)
|
719
751
|
|
720
|
-
def get_setting_file(self, **kwargs):
|
752
|
+
def get_setting_file(self, **kwargs: Any) -> Any:
|
721
753
|
"""Creates a filtered query for the ``SettingFile`` table."""
|
722
754
|
query = self.query(tables.SettingFile).filter_by(**kwargs)
|
723
755
|
return _parse_query_result(query, kwargs)
|
724
756
|
|
725
|
-
def get_uuid_map(self, **kwargs):
|
757
|
+
def get_uuid_map(self, **kwargs: Any) -> Any:
|
726
758
|
"""Creates a filtered query for the ``UuidIDMap`` table."""
|
727
759
|
query = self.query(tables.UuidIDMap).filter_by(**kwargs)
|
728
760
|
return _parse_query_result(query, kwargs)
|
729
761
|
|
730
762
|
# -- Database updates --------------------------------------------------------------
|
731
763
|
|
732
|
-
def generate_unused_id(
|
764
|
+
def generate_unused_id(
|
765
|
+
self, table: Type[tables.Base], is_28_bit: bool = True, id_field_name: str = "ID"
|
766
|
+
) -> int:
|
733
767
|
"""Generates an unused ID for the given table."""
|
734
768
|
max_tries = 1000000
|
735
769
|
for _ in range(max_tries):
|
@@ -749,7 +783,9 @@ class Rekordbox6Database:
|
|
749
783
|
|
750
784
|
raise ValueError("Could not generate unused ID")
|
751
785
|
|
752
|
-
def add_to_playlist(
|
786
|
+
def add_to_playlist(
|
787
|
+
self, playlist: PlaylistLike, content: ContentLike, track_no: int = None
|
788
|
+
) -> tables.DjmdSongPlaylist:
|
753
789
|
"""Adds a track to a playlist.
|
754
790
|
|
755
791
|
Creates a new :class:`DjmdSongPlaylist` object corresponding to the given
|
@@ -793,18 +829,26 @@ class Rekordbox6Database:
|
|
793
829
|
>>> new_song.TrackNo
|
794
830
|
1
|
795
831
|
"""
|
832
|
+
plist: DjmdPlaylist
|
833
|
+
cont: DjmdContent
|
796
834
|
if isinstance(playlist, (int, str)):
|
797
|
-
|
835
|
+
plist = self.get_playlist(ID=playlist)
|
836
|
+
else:
|
837
|
+
plist = playlist
|
838
|
+
|
798
839
|
if isinstance(content, (int, str)):
|
799
|
-
|
840
|
+
cont = self.get_content(ID=content)
|
841
|
+
else:
|
842
|
+
cont = content
|
843
|
+
|
800
844
|
# Check playlist attribute (can't be folder or smart playlist)
|
801
|
-
if
|
845
|
+
if plist.Attribute != 0:
|
802
846
|
raise ValueError("Playlist must be a normal playlist")
|
803
847
|
|
804
848
|
uuid = str(uuid4())
|
805
849
|
id_ = str(uuid4())
|
806
850
|
now = datetime.datetime.now()
|
807
|
-
nsongs = self.query(tables.DjmdSongPlaylist).filter_by(PlaylistID=
|
851
|
+
nsongs = self.query(tables.DjmdSongPlaylist).filter_by(PlaylistID=plist.ID).count()
|
808
852
|
if track_no is not None:
|
809
853
|
insert_at_end = False
|
810
854
|
track_no = int(track_no)
|
@@ -816,8 +860,8 @@ class Rekordbox6Database:
|
|
816
860
|
insert_at_end = True
|
817
861
|
track_no = nsongs + 1
|
818
862
|
|
819
|
-
cid =
|
820
|
-
pid =
|
863
|
+
cid = cont.ID
|
864
|
+
pid = plist.ID
|
821
865
|
|
822
866
|
logger.info("Adding content with ID=%s to playlist with ID=%s:", cid, pid)
|
823
867
|
logger.debug("Content ID: %s", cid)
|
@@ -833,19 +877,19 @@ class Rekordbox6Database:
|
|
833
877
|
query = (
|
834
878
|
self.query(tables.DjmdSongPlaylist)
|
835
879
|
.filter(
|
836
|
-
tables.DjmdSongPlaylist.PlaylistID ==
|
880
|
+
tables.DjmdSongPlaylist.PlaylistID == plist.ID,
|
837
881
|
tables.DjmdSongPlaylist.TrackNo >= track_no,
|
838
882
|
)
|
839
883
|
.order_by(tables.DjmdSongPlaylist.TrackNo)
|
840
884
|
)
|
841
|
-
for
|
842
|
-
|
843
|
-
|
844
|
-
moved.append(
|
885
|
+
for other_song in query:
|
886
|
+
other_song.TrackNo += 1
|
887
|
+
other_song.updated_at = now
|
888
|
+
moved.append(other_song)
|
845
889
|
self.registry.enable_tracking()
|
846
890
|
|
847
891
|
# Add song to playlist
|
848
|
-
song = tables.DjmdSongPlaylist.create(
|
892
|
+
song: tables.DjmdSongPlaylist = tables.DjmdSongPlaylist.create(
|
849
893
|
ID=id_,
|
850
894
|
PlaylistID=str(pid),
|
851
895
|
ContentID=str(cid),
|
@@ -861,7 +905,11 @@ class Rekordbox6Database:
|
|
861
905
|
|
862
906
|
return song
|
863
907
|
|
864
|
-
def remove_from_playlist(
|
908
|
+
def remove_from_playlist(
|
909
|
+
self,
|
910
|
+
playlist: PlaylistLike,
|
911
|
+
song: Union[tables.DjmdSongPlaylist, int, str],
|
912
|
+
) -> None:
|
865
913
|
"""Removes a track from a playlist.
|
866
914
|
|
867
915
|
Parameters
|
@@ -883,36 +931,54 @@ class Rekordbox6Database:
|
|
883
931
|
>>> song = pl.Songs[0]
|
884
932
|
>>> db.remove_from_playlist(pl, song)
|
885
933
|
"""
|
934
|
+
plist: DjmdPlaylist
|
935
|
+
plist_song: DjmdSongPlaylist
|
886
936
|
if isinstance(playlist, (int, str)):
|
887
|
-
|
937
|
+
plist = self.get_playlist(ID=playlist)
|
938
|
+
else:
|
939
|
+
plist = playlist
|
940
|
+
|
888
941
|
if isinstance(song, (int, str)):
|
889
|
-
|
890
|
-
|
942
|
+
plist_song = self.query(tables.DjmdSongPlaylist).filter_by(ID=song).one()
|
943
|
+
else:
|
944
|
+
plist_song = song
|
945
|
+
|
946
|
+
if not isinstance(plist_song, tables.DjmdSongPlaylist):
|
947
|
+
raise ValueError(
|
948
|
+
"Playlist must be a DjmdSongPlaylist or corresponding playlist song ID!"
|
949
|
+
)
|
950
|
+
|
951
|
+
logger.info("Removing song with ID=%s from playlist with ID=%s", plist_song.ID, plist.ID)
|
891
952
|
now = datetime.datetime.now()
|
892
953
|
# Remove track from playlist
|
893
|
-
track_no =
|
894
|
-
self.delete(
|
954
|
+
track_no = plist_song.TrackNo
|
955
|
+
self.delete(plist_song)
|
895
956
|
self.commit()
|
896
957
|
# Update track numbers higher than the removed track
|
897
958
|
query = (
|
898
959
|
self.query(tables.DjmdSongPlaylist)
|
899
960
|
.filter(
|
900
|
-
tables.DjmdSongPlaylist.PlaylistID ==
|
961
|
+
tables.DjmdSongPlaylist.PlaylistID == plist.ID,
|
901
962
|
tables.DjmdSongPlaylist.TrackNo > track_no,
|
902
963
|
)
|
903
964
|
.order_by(tables.DjmdSongPlaylist.TrackNo)
|
904
965
|
)
|
905
966
|
moved = list()
|
906
967
|
with self.registry.disabled():
|
907
|
-
for
|
908
|
-
|
909
|
-
|
910
|
-
moved.append(
|
968
|
+
for other_song in query:
|
969
|
+
other_song.TrackNo -= 1
|
970
|
+
other_song.updated_at = now
|
971
|
+
moved.append(other_song)
|
911
972
|
|
912
973
|
if moved:
|
913
974
|
self.registry.on_move(moved)
|
914
975
|
|
915
|
-
def move_song_in_playlist(
|
976
|
+
def move_song_in_playlist(
|
977
|
+
self,
|
978
|
+
playlist: PlaylistLike,
|
979
|
+
song: Union[tables.DjmdSongPlaylist, int, str],
|
980
|
+
new_track_no: int,
|
981
|
+
) -> None:
|
916
982
|
"""Sets a new track number of a song.
|
917
983
|
|
918
984
|
Also updates the track numbers of the other songs in the playlist.
|
@@ -954,23 +1020,31 @@ class Rekordbox6Database:
|
|
954
1020
|
>>> [s.Content.Title for s in sorted(pl.Songs, key=lambda x: x.TrackNo)] # noqa
|
955
1021
|
['Demo Track 1', 'HORN', 'NOISE', 'Demo Track 2']
|
956
1022
|
"""
|
1023
|
+
plist: DjmdPlaylist
|
1024
|
+
plist_song: DjmdSongPlaylist
|
957
1025
|
if isinstance(playlist, (int, str)):
|
958
|
-
|
1026
|
+
plist = self.get_playlist(ID=playlist)
|
1027
|
+
else:
|
1028
|
+
plist = playlist
|
1029
|
+
|
959
1030
|
if isinstance(song, (int, str)):
|
960
|
-
|
961
|
-
|
1031
|
+
plist_song = self.query(tables.DjmdSongPlaylist).filter_by(ID=song).one()
|
1032
|
+
else:
|
1033
|
+
plist_song = song
|
1034
|
+
|
1035
|
+
nsongs = self.query(tables.DjmdSongPlaylist).filter_by(PlaylistID=plist.ID).count()
|
962
1036
|
if new_track_no < 1:
|
963
1037
|
raise ValueError("Track number must be greater than 0")
|
964
1038
|
if new_track_no > nsongs + 1:
|
965
1039
|
raise ValueError(f"Track number too high, parent contains {nsongs} items")
|
966
1040
|
logger.info(
|
967
1041
|
"Moving song with ID=%s in playlist with ID=%s to %s",
|
968
|
-
|
969
|
-
|
1042
|
+
plist_song.ID,
|
1043
|
+
plist.ID,
|
970
1044
|
new_track_no,
|
971
1045
|
)
|
972
1046
|
now = datetime.datetime.now()
|
973
|
-
old_track_no =
|
1047
|
+
old_track_no = plist_song.TrackNo
|
974
1048
|
|
975
1049
|
self.registry.disable_tracking()
|
976
1050
|
moved = list()
|
@@ -978,7 +1052,7 @@ class Rekordbox6Database:
|
|
978
1052
|
query = (
|
979
1053
|
self.query(tables.DjmdSongPlaylist)
|
980
1054
|
.filter(
|
981
|
-
tables.DjmdSongPlaylist.PlaylistID ==
|
1055
|
+
tables.DjmdSongPlaylist.PlaylistID == plist.ID,
|
982
1056
|
old_track_no < tables.DjmdSongPlaylist.TrackNo,
|
983
1057
|
tables.DjmdSongPlaylist.TrackNo <= new_track_no,
|
984
1058
|
)
|
@@ -990,7 +1064,7 @@ class Rekordbox6Database:
|
|
990
1064
|
moved.append(other_song)
|
991
1065
|
elif new_track_no < old_track_no:
|
992
1066
|
query = self.query(tables.DjmdSongPlaylist).filter(
|
993
|
-
tables.DjmdSongPlaylist.PlaylistID ==
|
1067
|
+
tables.DjmdSongPlaylist.PlaylistID == plist.ID,
|
994
1068
|
new_track_no <= tables.DjmdSongPlaylist.TrackNo,
|
995
1069
|
tables.DjmdSongPlaylist.TrackNo < old_track_no,
|
996
1070
|
)
|
@@ -1001,19 +1075,27 @@ class Rekordbox6Database:
|
|
1001
1075
|
else:
|
1002
1076
|
return
|
1003
1077
|
|
1004
|
-
|
1005
|
-
|
1078
|
+
plist_song.TrackNo = new_track_no
|
1079
|
+
plist_song.updated_at = now
|
1006
1080
|
moved.append(song)
|
1007
1081
|
|
1008
1082
|
self.registry.enable_tracking()
|
1009
1083
|
self.registry.on_move(moved)
|
1010
1084
|
|
1011
|
-
def _create_playlist(
|
1085
|
+
def _create_playlist(
|
1086
|
+
self,
|
1087
|
+
name: str,
|
1088
|
+
seq: Optional[int],
|
1089
|
+
image_path: Optional[str],
|
1090
|
+
parent: Optional[PlaylistLike],
|
1091
|
+
smart_list: Optional[SmartList] = None,
|
1092
|
+
attribute: int = None,
|
1093
|
+
) -> DjmdPlaylist:
|
1012
1094
|
"""Creates a new playlist object."""
|
1013
1095
|
table = tables.DjmdPlaylist
|
1014
1096
|
id_ = str(self.generate_unused_id(table, is_28_bit=True))
|
1015
1097
|
uuid = str(uuid4())
|
1016
|
-
|
1098
|
+
attrib = int(attribute) if attribute is not None else 0
|
1017
1099
|
now = datetime.datetime.now()
|
1018
1100
|
if smart_list is not None:
|
1019
1101
|
# Set the playlist ID in the smart list and generate XML
|
@@ -1032,7 +1114,7 @@ class Rekordbox6Database:
|
|
1032
1114
|
raise ValueError("Parent is not a folder")
|
1033
1115
|
else:
|
1034
1116
|
# Check if parent exists and is a folder
|
1035
|
-
parent_id = parent
|
1117
|
+
parent_id = str(parent)
|
1036
1118
|
query = self.query(table.ID).filter(table.ID == parent_id, table.Attribute == 1)
|
1037
1119
|
if not self.query(query.exists()).scalar():
|
1038
1120
|
raise ValueError("Parent does not exist or is not a folder")
|
@@ -1057,7 +1139,7 @@ class Rekordbox6Database:
|
|
1057
1139
|
logger.debug("Name: %s", name)
|
1058
1140
|
logger.debug("Parent ID: %s", parent_id)
|
1059
1141
|
logger.debug("Seq: %s", seq)
|
1060
|
-
logger.debug("Attribute: %s",
|
1142
|
+
logger.debug("Attribute: %s", attrib)
|
1061
1143
|
logger.debug("Smart List: %s", smart_list_xml)
|
1062
1144
|
logger.debug("Image Path: %s", image_path)
|
1063
1145
|
|
@@ -1074,12 +1156,12 @@ class Rekordbox6Database:
|
|
1074
1156
|
|
1075
1157
|
# Add new playlist to database
|
1076
1158
|
# First create with name 'New playlist'
|
1077
|
-
playlist = table.create(
|
1159
|
+
playlist: DjmdPlaylist = table.create(
|
1078
1160
|
ID=id_,
|
1079
1161
|
Seq=seq,
|
1080
1162
|
Name="New playlist",
|
1081
1163
|
ImagePath=image_path,
|
1082
|
-
Attribute=
|
1164
|
+
Attribute=attrib,
|
1083
1165
|
ParentID=parent_id,
|
1084
1166
|
SmartList=smart_list_xml,
|
1085
1167
|
UUID=uuid,
|
@@ -1092,11 +1174,13 @@ class Rekordbox6Database:
|
|
1092
1174
|
|
1093
1175
|
# Update masterPlaylists6.xml
|
1094
1176
|
if self.playlist_xml is not None:
|
1095
|
-
self.playlist_xml.add(id_, parent_id,
|
1177
|
+
self.playlist_xml.add(id_, parent_id, attrib, now, lib_type=0, check_type=0)
|
1096
1178
|
|
1097
1179
|
return playlist
|
1098
1180
|
|
1099
|
-
def create_playlist(
|
1181
|
+
def create_playlist(
|
1182
|
+
self, name: str, parent: PlaylistLike = None, seq: int = None, image_path: str = None
|
1183
|
+
) -> DjmdPlaylist:
|
1100
1184
|
"""Creates a new playlist in the database.
|
1101
1185
|
|
1102
1186
|
Parameters
|
@@ -1142,7 +1226,9 @@ class Rekordbox6Database:
|
|
1142
1226
|
logger.info("Creating playlist %s", name)
|
1143
1227
|
return self._create_playlist(name, seq, image_path, parent, attribute=PlaylistType.PLAYLIST)
|
1144
1228
|
|
1145
|
-
def create_playlist_folder(
|
1229
|
+
def create_playlist_folder(
|
1230
|
+
self, name: str, parent: PlaylistLike = None, seq: int = None, image_path: str = None
|
1231
|
+
) -> DjmdPlaylist:
|
1146
1232
|
"""Creates a new playlist folder in the database.
|
1147
1233
|
|
1148
1234
|
Parameters
|
@@ -1183,8 +1269,13 @@ class Rekordbox6Database:
|
|
1183
1269
|
return self._create_playlist(name, seq, image_path, parent, attribute=PlaylistType.FOLDER)
|
1184
1270
|
|
1185
1271
|
def create_smart_playlist(
|
1186
|
-
self,
|
1187
|
-
|
1272
|
+
self,
|
1273
|
+
name: str,
|
1274
|
+
smart_list: SmartList,
|
1275
|
+
parent: PlaylistLike = None,
|
1276
|
+
seq: int = None,
|
1277
|
+
image_path: str = None,
|
1278
|
+
) -> DjmdPlaylist:
|
1188
1279
|
"""Creates a new smart playlist in the database.
|
1189
1280
|
|
1190
1281
|
Parameters
|
@@ -1229,7 +1320,7 @@ class Rekordbox6Database:
|
|
1229
1320
|
name, seq, image_path, parent, smart_list, PlaylistType.SMART_PLAYLIST
|
1230
1321
|
)
|
1231
1322
|
|
1232
|
-
def delete_playlist(self, playlist):
|
1323
|
+
def delete_playlist(self, playlist: PlaylistLike) -> None:
|
1233
1324
|
"""Deletes a playlist or playlist folder from the database.
|
1234
1325
|
|
1235
1326
|
Parameters
|
@@ -1251,17 +1342,22 @@ class Rekordbox6Database:
|
|
1251
1342
|
>>> folder = db.get_playlist(Name="My Folder").one()
|
1252
1343
|
>>> db.delete_playlist(folder)
|
1253
1344
|
"""
|
1345
|
+
plist: DjmdPlaylist
|
1254
1346
|
if isinstance(playlist, (int, str)):
|
1255
|
-
|
1347
|
+
plist = self.get_playlist(ID=playlist)
|
1348
|
+
else:
|
1349
|
+
plist = playlist
|
1350
|
+
if not isinstance(plist, DjmdPlaylist):
|
1351
|
+
raise ValueError("Playlist must be a DjmdPlaylist or corresponding playlist ID!")
|
1256
1352
|
|
1257
|
-
if
|
1258
|
-
logger.info("Deleting playlist folder '%s' with ID=%s",
|
1353
|
+
if plist.Attribute == 1:
|
1354
|
+
logger.info("Deleting playlist folder '%s' with ID=%s", plist.Name, plist.ID)
|
1259
1355
|
else:
|
1260
|
-
logger.info("Deleting playlist '%s' with ID=%s",
|
1356
|
+
logger.info("Deleting playlist '%s' with ID=%s", plist.Name, plist.ID)
|
1261
1357
|
|
1262
1358
|
now = datetime.datetime.now()
|
1263
|
-
seq =
|
1264
|
-
parent_id =
|
1359
|
+
seq = plist.Seq
|
1360
|
+
parent_id = plist.ParentID
|
1265
1361
|
|
1266
1362
|
self.registry.disable_tracking()
|
1267
1363
|
# Update seq numbers higher than the deleted seq number
|
@@ -1278,9 +1374,9 @@ class Rekordbox6Database:
|
|
1278
1374
|
pl.Seq -= 1
|
1279
1375
|
pl.updated_at = now
|
1280
1376
|
moved.append(pl)
|
1281
|
-
moved.append(
|
1377
|
+
moved.append(plist)
|
1282
1378
|
|
1283
|
-
children = [
|
1379
|
+
children = [plist]
|
1284
1380
|
# Get all child playlist IDs
|
1285
1381
|
child_ids = list()
|
1286
1382
|
while len(children):
|
@@ -1298,14 +1394,16 @@ class Rekordbox6Database:
|
|
1298
1394
|
self.playlist_xml.remove(pid)
|
1299
1395
|
|
1300
1396
|
# Remove playlist from database
|
1301
|
-
self.delete(
|
1397
|
+
self.delete(plist)
|
1302
1398
|
self.registry.enable_tracking()
|
1303
1399
|
if len(child_ids) > 1:
|
1304
|
-
# The playlist folder had children:
|
1400
|
+
# The playlist folder had children: one extra USN increment
|
1305
1401
|
self.registry.on_delete(child_ids[1:])
|
1306
1402
|
self.registry.on_delete(moved)
|
1307
1403
|
|
1308
|
-
def move_playlist(
|
1404
|
+
def move_playlist(
|
1405
|
+
self, playlist: PlaylistLike, parent: PlaylistLike = None, seq: int = None
|
1406
|
+
) -> None:
|
1309
1407
|
"""Moves a playlist (folder) in the current parent folder or to a new one.
|
1310
1408
|
|
1311
1409
|
Parameters
|
@@ -1351,15 +1449,19 @@ class Rekordbox6Database:
|
|
1351
1449
|
"""
|
1352
1450
|
if parent is None and seq is None:
|
1353
1451
|
raise ValueError("Either parent or seq must be given")
|
1354
|
-
|
1355
|
-
|
1452
|
+
plist: DjmdPlaylist
|
1453
|
+
seqence: int
|
1356
1454
|
|
1455
|
+
if isinstance(playlist, (int, str)):
|
1456
|
+
plist = self.get_playlist(ID=playlist)
|
1457
|
+
else:
|
1458
|
+
plist = playlist
|
1357
1459
|
now = datetime.datetime.now()
|
1358
1460
|
table = tables.DjmdPlaylist
|
1359
1461
|
|
1360
1462
|
if parent is None:
|
1361
1463
|
# If no parent is given, keep the current parent
|
1362
|
-
parent_id =
|
1464
|
+
parent_id = plist.ParentID
|
1363
1465
|
elif isinstance(parent, tables.DjmdPlaylist):
|
1364
1466
|
# Check if parent is a folder
|
1365
1467
|
parent_id = parent.ID
|
@@ -1373,22 +1475,23 @@ class Rekordbox6Database:
|
|
1373
1475
|
raise ValueError("Parent does not exist or is not a folder")
|
1374
1476
|
|
1375
1477
|
n = self.get_playlist(ParentID=parent_id).count()
|
1376
|
-
old_seq =
|
1478
|
+
old_seq = plist.Seq
|
1377
1479
|
|
1378
|
-
if parent_id !=
|
1480
|
+
if parent_id != plist.ParentID:
|
1379
1481
|
# Move to new parent
|
1380
1482
|
|
1381
|
-
old_parent_id =
|
1483
|
+
old_parent_id = plist.ParentID
|
1382
1484
|
if seq is None:
|
1383
1485
|
# New playlist is last in parents
|
1384
|
-
|
1486
|
+
seqence = n + 1
|
1385
1487
|
insert_at_end = True
|
1386
1488
|
else:
|
1489
|
+
seqence = seq
|
1387
1490
|
# Check if sequence number is valid
|
1388
1491
|
insert_at_end = False
|
1389
|
-
if
|
1492
|
+
if seqence < 1:
|
1390
1493
|
raise ValueError("Sequence number must be greater than 0")
|
1391
|
-
elif
|
1494
|
+
elif seqence > n + 1:
|
1392
1495
|
raise ValueError(f"Sequence number too high, parent contains {n} items")
|
1393
1496
|
|
1394
1497
|
if not insert_at_end:
|
@@ -1404,10 +1507,10 @@ class Rekordbox6Database:
|
|
1404
1507
|
other_playlists = query.all()
|
1405
1508
|
# Set seq number and update time *before* other playlists to ensure
|
1406
1509
|
# right USN increment order
|
1407
|
-
|
1510
|
+
plist.ParentID = parent_id
|
1408
1511
|
with self.registry.disabled():
|
1409
|
-
|
1410
|
-
|
1512
|
+
plist.Seq = seqence
|
1513
|
+
plist.updated_at = now
|
1411
1514
|
|
1412
1515
|
if not insert_at_end:
|
1413
1516
|
# Update seq numbers higher than the new seq number in *new* parent
|
@@ -1438,31 +1541,33 @@ class Rekordbox6Database:
|
|
1438
1541
|
|
1439
1542
|
else:
|
1440
1543
|
# Keep parent, only change seq number
|
1441
|
-
|
1442
|
-
|
1544
|
+
if seq is None:
|
1545
|
+
raise ValueError("Sequence number must be given")
|
1546
|
+
seqence = seq
|
1547
|
+
if seqence < 1:
|
1443
1548
|
raise ValueError("Sequence number must be greater than 0")
|
1444
|
-
elif
|
1549
|
+
elif seqence > n + 1:
|
1445
1550
|
raise ValueError(f"Sequence number too high, parent contains {n} items")
|
1446
1551
|
|
1447
|
-
if
|
1552
|
+
if seqence > old_seq:
|
1448
1553
|
# Get all playlists with seq between old_seq and seq
|
1449
1554
|
query = (
|
1450
1555
|
self.query(tables.DjmdPlaylist)
|
1451
1556
|
.filter(
|
1452
|
-
tables.DjmdPlaylist.ParentID ==
|
1557
|
+
tables.DjmdPlaylist.ParentID == plist.ParentID,
|
1453
1558
|
old_seq < tables.DjmdPlaylist.Seq,
|
1454
|
-
tables.DjmdPlaylist.Seq <=
|
1559
|
+
tables.DjmdPlaylist.Seq <= seqence,
|
1455
1560
|
)
|
1456
1561
|
.order_by(tables.DjmdPlaylist.Seq)
|
1457
1562
|
)
|
1458
1563
|
other_playlists = query.all()
|
1459
1564
|
delta_seq = -1
|
1460
|
-
elif
|
1565
|
+
elif seqence < old_seq:
|
1461
1566
|
query = (
|
1462
1567
|
self.query(tables.DjmdPlaylist)
|
1463
1568
|
.filter(
|
1464
|
-
tables.DjmdPlaylist.ParentID ==
|
1465
|
-
|
1569
|
+
tables.DjmdPlaylist.ParentID == plist.ParentID,
|
1570
|
+
seqence <= tables.DjmdPlaylist.Seq,
|
1466
1571
|
tables.DjmdPlaylist.Seq < old_seq,
|
1467
1572
|
)
|
1468
1573
|
.order_by(tables.DjmdPlaylist.Seq)
|
@@ -1474,10 +1579,10 @@ class Rekordbox6Database:
|
|
1474
1579
|
|
1475
1580
|
# Set seq number and update time *before* other playlists to ensure
|
1476
1581
|
# right USN increment order
|
1477
|
-
|
1582
|
+
plist.Seq = seqence
|
1478
1583
|
# Each move counts as one USN increment, so disable for update time
|
1479
1584
|
with self.registry.disabled():
|
1480
|
-
|
1585
|
+
plist.updated_at = now
|
1481
1586
|
|
1482
1587
|
# Set seq number and update time for playlists between old_seq and seq
|
1483
1588
|
for pl in other_playlists:
|
@@ -1486,7 +1591,7 @@ class Rekordbox6Database:
|
|
1486
1591
|
with self.registry.disabled():
|
1487
1592
|
pl.updated_at = now
|
1488
1593
|
|
1489
|
-
def rename_playlist(self, playlist, name):
|
1594
|
+
def rename_playlist(self, playlist: PlaylistLike, name: str) -> None:
|
1490
1595
|
"""Renames a playlist or playlist folder.
|
1491
1596
|
|
1492
1597
|
Parameters
|
@@ -1514,16 +1619,27 @@ class Rekordbox6Database:
|
|
1514
1619
|
>>> [pl.Name for pl in playlists] # noqa
|
1515
1620
|
['Playlist new', 'Playlist 2']
|
1516
1621
|
"""
|
1622
|
+
pl: DjmdPlaylist
|
1517
1623
|
if isinstance(playlist, (int, str)):
|
1518
|
-
|
1624
|
+
pl = self.get_playlist(ID=playlist)
|
1625
|
+
else:
|
1626
|
+
pl = playlist
|
1627
|
+
|
1519
1628
|
now = datetime.datetime.now()
|
1520
1629
|
# Update name of playlist
|
1521
|
-
|
1630
|
+
pl.Name = name
|
1522
1631
|
# Update update time: USN not incremented
|
1523
1632
|
with self.registry.disabled():
|
1524
|
-
|
1633
|
+
pl.updated_at = now
|
1525
1634
|
|
1526
|
-
def add_album(
|
1635
|
+
def add_album(
|
1636
|
+
self,
|
1637
|
+
name: str,
|
1638
|
+
artist: Union[tables.DjmdArtist, int, str] = None,
|
1639
|
+
image_path: PathLike = None,
|
1640
|
+
compilation: bool = None,
|
1641
|
+
search_str: str = None,
|
1642
|
+
) -> tables.DjmdAlbum:
|
1527
1643
|
"""Adds a new album to the database.
|
1528
1644
|
|
1529
1645
|
Parameters
|
@@ -1583,18 +1699,22 @@ class Rekordbox6Database:
|
|
1583
1699
|
raise ValueError(f"Album '{name}' already exists in database")
|
1584
1700
|
|
1585
1701
|
# Get artist ID
|
1702
|
+
artist_id: Optional[str] = None
|
1586
1703
|
if artist is not None:
|
1704
|
+
art: tables.DjmdArtist
|
1587
1705
|
if isinstance(artist, (int, str)):
|
1588
|
-
|
1589
|
-
|
1706
|
+
art = self.get_artist(ID=artist)
|
1707
|
+
else:
|
1708
|
+
art = artist
|
1709
|
+
artist_id = art.ID
|
1590
1710
|
|
1591
1711
|
id_ = self.generate_unused_id(tables.DjmdAlbum)
|
1592
1712
|
uuid = str(uuid4())
|
1593
|
-
album = tables.DjmdAlbum.create(
|
1713
|
+
album: tables.DjmdAlbum = tables.DjmdAlbum.create(
|
1594
1714
|
ID=id_,
|
1595
1715
|
Name=name,
|
1596
|
-
AlbumArtistID=
|
1597
|
-
ImagePath=image_path,
|
1716
|
+
AlbumArtistID=artist_id,
|
1717
|
+
ImagePath=str(image_path) if image_path is not None else None,
|
1598
1718
|
Compilation=compilation,
|
1599
1719
|
SearchStr=search_str,
|
1600
1720
|
UUID=str(uuid),
|
@@ -1603,7 +1723,7 @@ class Rekordbox6Database:
|
|
1603
1723
|
self.flush()
|
1604
1724
|
return album
|
1605
1725
|
|
1606
|
-
def add_artist(self, name, search_str=None):
|
1726
|
+
def add_artist(self, name: str, search_str: str = None) -> tables.DjmdArtist:
|
1607
1727
|
"""Adds a new artist to the database.
|
1608
1728
|
|
1609
1729
|
Parameters
|
@@ -1655,12 +1775,14 @@ class Rekordbox6Database:
|
|
1655
1775
|
|
1656
1776
|
id_ = self.generate_unused_id(tables.DjmdArtist)
|
1657
1777
|
uuid = str(uuid4())
|
1658
|
-
artist = tables.DjmdArtist.create(
|
1778
|
+
artist: tables.DjmdArtist = tables.DjmdArtist.create(
|
1779
|
+
ID=id_, Name=name, SearchStr=search_str, UUID=uuid
|
1780
|
+
)
|
1659
1781
|
self.add(artist)
|
1660
1782
|
self.flush()
|
1661
1783
|
return artist
|
1662
1784
|
|
1663
|
-
def add_genre(self, name):
|
1785
|
+
def add_genre(self, name: str) -> tables.DjmdGenre:
|
1664
1786
|
"""Adds a new genre to the database.
|
1665
1787
|
|
1666
1788
|
Parameters
|
@@ -1705,12 +1827,12 @@ class Rekordbox6Database:
|
|
1705
1827
|
|
1706
1828
|
id_ = self.generate_unused_id(tables.DjmdGenre)
|
1707
1829
|
uuid = str(uuid4())
|
1708
|
-
genre = tables.DjmdGenre.create(ID=id_, Name=name, UUID=uuid)
|
1830
|
+
genre: tables.DjmdGenre = tables.DjmdGenre.create(ID=id_, Name=name, UUID=uuid)
|
1709
1831
|
self.add(genre)
|
1710
1832
|
self.flush()
|
1711
1833
|
return genre
|
1712
1834
|
|
1713
|
-
def add_label(self, name):
|
1835
|
+
def add_label(self, name: str) -> tables.DjmdLabel:
|
1714
1836
|
"""Adds a new label to the database.
|
1715
1837
|
|
1716
1838
|
Parameters
|
@@ -1755,12 +1877,12 @@ class Rekordbox6Database:
|
|
1755
1877
|
|
1756
1878
|
id_ = self.generate_unused_id(tables.DjmdLabel)
|
1757
1879
|
uuid = str(uuid4())
|
1758
|
-
label = tables.DjmdLabel.create(ID=id_, Name=name, UUID=uuid)
|
1880
|
+
label: tables.DjmdLabel = tables.DjmdLabel.create(ID=id_, Name=name, UUID=uuid)
|
1759
1881
|
self.add(label)
|
1760
1882
|
self.flush()
|
1761
1883
|
return label
|
1762
1884
|
|
1763
|
-
def add_content(self, path, **kwargs):
|
1885
|
+
def add_content(self, path: PathLike, **kwargs: Any) -> DjmdContent:
|
1764
1886
|
"""Adds a new track to the database.
|
1765
1887
|
|
1766
1888
|
Parameters
|
@@ -1811,7 +1933,7 @@ class Rekordbox6Database:
|
|
1811
1933
|
except ValueError:
|
1812
1934
|
raise ValueError(f"Invalid file type: {path.suffix}")
|
1813
1935
|
|
1814
|
-
content = tables.DjmdContent.create(
|
1936
|
+
content: DjmdContent = tables.DjmdContent.create(
|
1815
1937
|
ID=id_,
|
1816
1938
|
UUID=uuid,
|
1817
1939
|
ContentLink=content_link.rb_local_usn,
|
@@ -1834,7 +1956,7 @@ class Rekordbox6Database:
|
|
1834
1956
|
|
1835
1957
|
# ----------------------------------------------------------------------------------
|
1836
1958
|
|
1837
|
-
def get_mysetting_paths(self):
|
1959
|
+
def get_mysetting_paths(self) -> List[Path]:
|
1838
1960
|
"""Returns the file paths of the local Rekordbox MySetting files.
|
1839
1961
|
|
1840
1962
|
Returns
|
@@ -1842,12 +1964,12 @@ class Rekordbox6Database:
|
|
1842
1964
|
paths : list[str]
|
1843
1965
|
the file paths of the local MySetting files.
|
1844
1966
|
"""
|
1845
|
-
paths = list()
|
1967
|
+
paths: List[Path] = list()
|
1846
1968
|
for item in self.get_setting_file():
|
1847
1969
|
paths.append(self._db_dir / item.Path.lstrip("/\\"))
|
1848
1970
|
return paths
|
1849
1971
|
|
1850
|
-
def get_anlz_dir(self, content):
|
1972
|
+
def get_anlz_dir(self, content: ContentLike) -> Path:
|
1851
1973
|
"""Returns the directory path containing the ANLZ analysis files of a track.
|
1852
1974
|
|
1853
1975
|
Parameters
|
@@ -1862,14 +1984,17 @@ class Rekordbox6Database:
|
|
1862
1984
|
anlz_dir : Path
|
1863
1985
|
The path of the directory containing the analysis files for the content.
|
1864
1986
|
"""
|
1987
|
+
cont: DjmdContent
|
1865
1988
|
if isinstance(content, (int, str)):
|
1866
|
-
|
1989
|
+
cont = self.get_content(ID=content)
|
1990
|
+
else:
|
1991
|
+
cont = content
|
1867
1992
|
|
1868
|
-
dat_path = Path(
|
1869
|
-
path = self._share_dir / dat_path.parent
|
1993
|
+
dat_path = Path(cont.AnalysisDataPath.strip("\\/"))
|
1994
|
+
path: Path = self._share_dir / dat_path.parent
|
1870
1995
|
return path
|
1871
1996
|
|
1872
|
-
def get_anlz_paths(self, content):
|
1997
|
+
def get_anlz_paths(self, content: ContentLike) -> Dict[str, Optional[Path]]:
|
1873
1998
|
"""Returns all existing ANLZ analysis file paths of a track.
|
1874
1999
|
|
1875
2000
|
Parameters
|
@@ -1888,7 +2013,7 @@ class Rekordbox6Database:
|
|
1888
2013
|
root = self.get_anlz_dir(content)
|
1889
2014
|
return get_anlz_paths(root)
|
1890
2015
|
|
1891
|
-
def read_anlz_files(self, content):
|
2016
|
+
def read_anlz_files(self, content: ContentLike) -> Dict[Path, AnlzFile]:
|
1892
2017
|
"""Reads all existing ANLZ analysis files of a track.
|
1893
2018
|
|
1894
2019
|
Parameters
|
@@ -1907,7 +2032,7 @@ class Rekordbox6Database:
|
|
1907
2032
|
root = self.get_anlz_dir(content)
|
1908
2033
|
return read_anlz_files(root)
|
1909
2034
|
|
1910
|
-
def get_anlz_path(self, content, type_):
|
2035
|
+
def get_anlz_path(self, content: ContentLike, type_: str) -> Optional[PathLike]:
|
1911
2036
|
"""Returns the file path of an ANLZ analysis file of a track.
|
1912
2037
|
|
1913
2038
|
Parameters
|
@@ -1930,7 +2055,7 @@ class Rekordbox6Database:
|
|
1930
2055
|
paths = get_anlz_paths(root)
|
1931
2056
|
return paths.get(type_.upper(), "")
|
1932
2057
|
|
1933
|
-
def read_anlz_file(self, content, type_):
|
2058
|
+
def read_anlz_file(self, content: ContentLike, type_: str) -> Optional[AnlzFile]:
|
1934
2059
|
"""Reads an ANLZ analysis file of a track.
|
1935
2060
|
|
1936
2061
|
Parameters
|
@@ -1954,7 +2079,14 @@ class Rekordbox6Database:
|
|
1954
2079
|
return AnlzFile.parse_file(path)
|
1955
2080
|
return None
|
1956
2081
|
|
1957
|
-
def update_content_path(
|
2082
|
+
def update_content_path(
|
2083
|
+
self,
|
2084
|
+
content: ContentLike,
|
2085
|
+
path: PathLike,
|
2086
|
+
save: bool = True,
|
2087
|
+
check_path: bool = True,
|
2088
|
+
commit: bool = True,
|
2089
|
+
) -> None:
|
1958
2090
|
"""Update the file path of a track in the Rekordbox v6 database.
|
1959
2091
|
|
1960
2092
|
This changes the `FolderPath` entry in the ``DjmdContent`` table and the
|
@@ -2001,16 +2133,20 @@ class Rekordbox6Database:
|
|
2001
2133
|
C:/Music/PioneerDJ/Sampler/PRESET ONESHOT/NOISE.wav
|
2002
2134
|
|
2003
2135
|
"""
|
2136
|
+
cont: DjmdContent
|
2004
2137
|
if isinstance(content, (int, str)):
|
2005
|
-
|
2006
|
-
|
2138
|
+
cont = self.get_content(ID=content)
|
2139
|
+
else:
|
2140
|
+
cont = content
|
2141
|
+
|
2142
|
+
cid = cont.ID
|
2007
2143
|
|
2008
2144
|
path = Path(path)
|
2009
2145
|
# Check and format path (the database and ANLZ files use "/" as path delimiter)
|
2010
2146
|
if check_path:
|
2011
2147
|
assert path.exists()
|
2012
2148
|
path = str(path).replace("\\", "/")
|
2013
|
-
old_path =
|
2149
|
+
old_path = cont.FolderPath
|
2014
2150
|
logger.info("Replacing '%s' with '%s' of content [%s]", old_path, path, cid)
|
2015
2151
|
|
2016
2152
|
# Update path in ANLZ files
|
@@ -2021,18 +2157,18 @@ class Rekordbox6Database:
|
|
2021
2157
|
|
2022
2158
|
# Update path in database (DjmdContent)
|
2023
2159
|
logger.debug("Updating database file path: %s", path)
|
2024
|
-
|
2160
|
+
cont.FolderPath = path
|
2025
2161
|
|
2026
2162
|
# Update the OrgFolderPath column with the new path
|
2027
2163
|
# if the column matches the old_path variable
|
2028
|
-
org_folder_path =
|
2164
|
+
org_folder_path = cont.OrgFolderPath
|
2029
2165
|
if org_folder_path == old_path:
|
2030
|
-
|
2166
|
+
cont.OrgFolderPath = path
|
2031
2167
|
|
2032
2168
|
# Update the FileNameL column with the new filename if it changed
|
2033
2169
|
new_name = path.split("/")[-1]
|
2034
|
-
if
|
2035
|
-
|
2170
|
+
if cont.FileNameL != new_name:
|
2171
|
+
cont.FileNameL = new_name
|
2036
2172
|
|
2037
2173
|
if save:
|
2038
2174
|
logger.debug("Saving ANLZ files")
|
@@ -2045,7 +2181,14 @@ class Rekordbox6Database:
|
|
2045
2181
|
logger.debug("Committing changes to the database")
|
2046
2182
|
self.commit()
|
2047
2183
|
|
2048
|
-
def update_content_filename(
|
2184
|
+
def update_content_filename(
|
2185
|
+
self,
|
2186
|
+
content: ContentLike,
|
2187
|
+
name: str,
|
2188
|
+
save: bool = True,
|
2189
|
+
check_path: bool = True,
|
2190
|
+
commit: bool = True,
|
2191
|
+
) -> None:
|
2049
2192
|
"""Update the file name of a track in the Rekordbox v6 database.
|
2050
2193
|
|
2051
2194
|
This changes the `FolderPath` entry in the ``DjmdContent`` table and the
|
@@ -2090,16 +2233,19 @@ class Rekordbox6Database:
|
|
2090
2233
|
>>> cont.FolderPath == file.get("path")
|
2091
2234
|
True
|
2092
2235
|
"""
|
2236
|
+
cont: DjmdContent
|
2093
2237
|
if isinstance(content, (int, str)):
|
2094
|
-
|
2238
|
+
cont = self.get_content(ID=content)
|
2239
|
+
else:
|
2240
|
+
cont = content
|
2095
2241
|
|
2096
|
-
old_path = Path(
|
2242
|
+
old_path = Path(cont.FolderPath)
|
2097
2243
|
ext = old_path.suffix
|
2098
2244
|
new_path = old_path.parent / name
|
2099
2245
|
new_path = new_path.with_suffix(ext)
|
2100
|
-
self.update_content_path(
|
2246
|
+
self.update_content_path(cont, new_path, save, check_path, commit=commit)
|
2101
2247
|
|
2102
|
-
def to_dict(self, verbose=False):
|
2248
|
+
def to_dict(self, verbose: bool = False) -> Dict[str, Any]:
|
2103
2249
|
"""Convert the database to a dictionary.
|
2104
2250
|
|
2105
2251
|
Parameters
|
@@ -2127,11 +2273,13 @@ class Rekordbox6Database:
|
|
2127
2273
|
data[table_name] = table_data
|
2128
2274
|
return data
|
2129
2275
|
|
2130
|
-
def to_json(
|
2276
|
+
def to_json(
|
2277
|
+
self, file: PathLike, indent: int = 4, sort_keys: bool = True, verbose: bool = False
|
2278
|
+
) -> None:
|
2131
2279
|
"""Convert the database to a JSON file."""
|
2132
2280
|
import json
|
2133
2281
|
|
2134
|
-
def json_serial(obj):
|
2282
|
+
def json_serial(obj: Any) -> Any:
|
2135
2283
|
if isinstance(obj, (datetime.datetime, datetime.date)):
|
2136
2284
|
return obj.isoformat()
|
2137
2285
|
raise TypeError(f"Type {type(obj)} not serializable")
|
@@ -2140,7 +2288,7 @@ class Rekordbox6Database:
|
|
2140
2288
|
with open(file, "w") as fp:
|
2141
2289
|
json.dump(data, fp, indent=indent, sort_keys=sort_keys, default=json_serial)
|
2142
2290
|
|
2143
|
-
def copy_unlocked(self, output_file):
|
2291
|
+
def copy_unlocked(self, output_file: PathLike) -> None:
|
2144
2292
|
src_engine = self.engine
|
2145
2293
|
src_metadata = MetaData()
|
2146
2294
|
exclude_tables = ("sqlite_master", "sqlite_sequence", "sqlite_temp_master")
|
@@ -2149,7 +2297,7 @@ class Rekordbox6Database:
|
|
2149
2297
|
dst_metadata = MetaData()
|
2150
2298
|
|
2151
2299
|
@event.listens_for(src_metadata, "column_reflect")
|
2152
|
-
def genericize_datatypes(inspector, tablename, column_dict):
|
2300
|
+
def genericize_datatypes(inspector, tablename, column_dict): # type: ignore # noqa: ANN202
|
2153
2301
|
type_ = column_dict["type"].as_generic(allow_nulltype=True)
|
2154
2302
|
if isinstance(type_, DateTime):
|
2155
2303
|
type_ = String
|