pyrekordbox 0.4.1__py3-none-any.whl → 0.4.3__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 +3 -2
- pyrekordbox/_version.py +2 -2
- pyrekordbox/anlz/__init__.py +6 -6
- pyrekordbox/anlz/file.py +40 -32
- pyrekordbox/anlz/tags.py +108 -70
- pyrekordbox/config.py +36 -26
- pyrekordbox/db6/aux_files.py +40 -14
- pyrekordbox/db6/database.py +382 -220
- pyrekordbox/db6/registry.py +48 -34
- pyrekordbox/db6/smartlist.py +12 -12
- pyrekordbox/db6/tables.py +51 -52
- pyrekordbox/mysettings/__init__.py +3 -2
- pyrekordbox/mysettings/file.py +27 -24
- pyrekordbox/rbxml.py +327 -147
- pyrekordbox/utils.py +7 -6
- {pyrekordbox-0.4.1.dist-info → pyrekordbox-0.4.3.dist-info}/METADATA +1 -1
- pyrekordbox-0.4.3.dist-info/RECORD +25 -0
- {pyrekordbox-0.4.1.dist-info → pyrekordbox-0.4.3.dist-info}/WHEEL +1 -1
- pyrekordbox-0.4.1.dist-info/RECORD +0 -25
- {pyrekordbox-0.4.1.dist-info → pyrekordbox-0.4.3.dist-info}/licenses/LICENSE +0 -0
- {pyrekordbox-0.4.1.dist-info → pyrekordbox-0.4.3.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,21 +15,21 @@ 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
20
|
from ..utils import 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
|
-
except ImportError:
|
31
|
-
import sqlite3
|
31
|
+
except ImportError: # pragma: no cover
|
32
|
+
import sqlite3 # type: ignore[no-redef]
|
32
33
|
|
33
34
|
_sqlcipher_available = False
|
34
35
|
|
@@ -40,20 +41,33 @@ SPECIAL_PLAYLIST_IDS = [
|
|
40
41
|
|
41
42
|
logger = logging.getLogger(__name__)
|
42
43
|
|
44
|
+
PathLike = Union[str, Path]
|
45
|
+
ContentLike = Union[DjmdContent, int, str]
|
46
|
+
PlaylistLike = Union[DjmdPlaylist, int, str]
|
47
|
+
|
43
48
|
|
44
49
|
class NoCachedKey(Exception):
|
45
50
|
pass
|
46
51
|
|
47
52
|
|
48
|
-
|
53
|
+
T = TypeVar("T", bound=tables.Base)
|
54
|
+
|
55
|
+
|
56
|
+
def _parse_query_result(query: Query[T], kwargs: Dict[str, Any]) -> Any:
|
49
57
|
if "ID" in kwargs or "registry_id" in kwargs:
|
50
58
|
try:
|
51
|
-
|
59
|
+
result: T = query.one()
|
60
|
+
return result
|
52
61
|
except NoResultFound:
|
53
62
|
return None
|
54
63
|
return query
|
55
64
|
|
56
65
|
|
66
|
+
class SessionNotInitializedError(Exception):
|
67
|
+
def __init__(self) -> None:
|
68
|
+
super().__init__("Sqlite-session not intialized!")
|
69
|
+
|
70
|
+
|
57
71
|
class Rekordbox6Database:
|
58
72
|
"""Rekordbox v6 master.db database handler.
|
59
73
|
|
@@ -103,7 +117,9 @@ class Rekordbox6Database:
|
|
103
117
|
<DjmdContent(40110712 Title=NOISE)>
|
104
118
|
"""
|
105
119
|
|
106
|
-
def __init__(
|
120
|
+
def __init__(
|
121
|
+
self, path: PathLike = None, db_dir: PathLike = "", key: str = "", unlock: bool = True
|
122
|
+
):
|
107
123
|
# get config of latest supported version
|
108
124
|
rb_config = get_config("rekordbox7")
|
109
125
|
if not rb_config:
|
@@ -119,15 +135,15 @@ class Rekordbox6Database:
|
|
119
135
|
if not path:
|
120
136
|
pdir = get_config("pioneer", "install_dir")
|
121
137
|
raise FileNotFoundError(f"No Rekordbox v6/v7 directory found in '{pdir}'")
|
122
|
-
|
138
|
+
db_path: Path = Path(path)
|
123
139
|
# make sure file exists
|
124
|
-
if not
|
125
|
-
raise FileNotFoundError(f"File '{
|
140
|
+
if not db_path.exists():
|
141
|
+
raise FileNotFoundError(f"File '{db_path}' does not exist!")
|
126
142
|
# Open database
|
127
143
|
if unlock:
|
128
|
-
if not _sqlcipher_available:
|
144
|
+
if not _sqlcipher_available: # pragma: no cover
|
129
145
|
raise ImportError("Could not unlock database: 'sqlcipher3' package not found")
|
130
|
-
if not key:
|
146
|
+
if not key: # pragma: no cover
|
131
147
|
try:
|
132
148
|
key = rb_config["dp"]
|
133
149
|
except KeyError:
|
@@ -145,47 +161,50 @@ class Rekordbox6Database:
|
|
145
161
|
|
146
162
|
logger.debug("Key: %s", key)
|
147
163
|
# Unlock database and create engine
|
148
|
-
url = f"sqlite+pysqlcipher://:{key}@/{
|
164
|
+
url = f"sqlite+pysqlcipher://:{key}@/{db_path}?"
|
149
165
|
engine = create_engine(url, module=sqlite3)
|
150
166
|
else:
|
151
|
-
engine = create_engine(f"sqlite:///{
|
167
|
+
engine = create_engine(f"sqlite:///{db_path}")
|
152
168
|
|
153
169
|
if not db_dir:
|
154
|
-
db_dir =
|
155
|
-
|
156
|
-
if not
|
157
|
-
raise FileNotFoundError(f"Database directory '{
|
170
|
+
db_dir = db_path.parent
|
171
|
+
db_directory: Path = Path(db_dir)
|
172
|
+
if not db_directory.exists():
|
173
|
+
raise FileNotFoundError(f"Database directory '{db_directory}' does not exist!")
|
158
174
|
|
159
175
|
self.engine = engine
|
160
176
|
self.session: Optional[Session] = None
|
161
177
|
|
162
178
|
self.registry = RekordboxAgentRegistry(self)
|
163
|
-
self._events = dict()
|
179
|
+
self._events: Dict[str, Callable[[Any], None]] = dict()
|
180
|
+
self.playlist_xml: Optional[MasterPlaylistXml]
|
164
181
|
try:
|
165
|
-
self.playlist_xml = MasterPlaylistXml(db_dir=
|
182
|
+
self.playlist_xml = MasterPlaylistXml(db_dir=db_directory)
|
166
183
|
except FileNotFoundError:
|
167
|
-
logger.warning(f"No masterPlaylists6.xml found in {
|
184
|
+
logger.warning(f"No masterPlaylists6.xml found in {db_directory}")
|
168
185
|
self.playlist_xml = None
|
169
186
|
|
170
|
-
self._db_dir =
|
171
|
-
self._share_dir =
|
187
|
+
self._db_dir = db_directory
|
188
|
+
self._share_dir: Path = db_directory / "share"
|
172
189
|
|
173
190
|
self.open()
|
174
191
|
|
175
192
|
@property
|
176
|
-
def no_autoflush(self):
|
193
|
+
def no_autoflush(self) -> Any:
|
177
194
|
"""Creates a no-autoflush context."""
|
195
|
+
if self.session is None:
|
196
|
+
raise SessionNotInitializedError()
|
178
197
|
return self.session.no_autoflush
|
179
198
|
|
180
199
|
@property
|
181
|
-
def db_directory(self):
|
200
|
+
def db_directory(self) -> Path:
|
182
201
|
return self._db_dir
|
183
202
|
|
184
203
|
@property
|
185
|
-
def share_directory(self):
|
204
|
+
def share_directory(self) -> Path:
|
186
205
|
return self._share_dir
|
187
206
|
|
188
|
-
def open(self):
|
207
|
+
def open(self) -> None:
|
189
208
|
"""Open the database by instantiating a new session using the SQLAchemy engine.
|
190
209
|
|
191
210
|
A new session instance is only created if the session was closed previously.
|
@@ -200,21 +219,28 @@ class Rekordbox6Database:
|
|
200
219
|
self.session = Session(bind=self.engine)
|
201
220
|
self.registry.clear_buffer()
|
202
221
|
|
203
|
-
def close(self):
|
222
|
+
def close(self) -> None:
|
204
223
|
"""Close the currently active session."""
|
224
|
+
if self.session is None:
|
225
|
+
raise SessionNotInitializedError()
|
205
226
|
for key in self._events:
|
206
227
|
self.unregister_event(key)
|
207
228
|
self.registry.clear_buffer()
|
208
229
|
self.session.close()
|
209
230
|
self.session = None
|
210
231
|
|
211
|
-
def __enter__(self):
|
232
|
+
def __enter__(self) -> "Rekordbox6Database":
|
212
233
|
return self
|
213
234
|
|
214
|
-
def __exit__(
|
235
|
+
def __exit__(
|
236
|
+
self,
|
237
|
+
type_: Optional[Type[BaseException]],
|
238
|
+
value: Optional[BaseException],
|
239
|
+
traceback: Optional[TracebackType],
|
240
|
+
) -> None:
|
215
241
|
self.close()
|
216
242
|
|
217
|
-
def register_event(self, identifier, fn):
|
243
|
+
def register_event(self, identifier: str, fn: Callable[[Any], None]) -> None:
|
218
244
|
"""Registers a session event callback.
|
219
245
|
|
220
246
|
Parameters
|
@@ -225,10 +251,12 @@ class Rekordbox6Database:
|
|
225
251
|
fn : callable
|
226
252
|
The event callback method.
|
227
253
|
"""
|
254
|
+
if self.session is None:
|
255
|
+
raise SessionNotInitializedError()
|
228
256
|
event.listen(self.session, identifier, fn)
|
229
257
|
self._events[identifier] = fn
|
230
258
|
|
231
|
-
def unregister_event(self, identifier):
|
259
|
+
def unregister_event(self, identifier: str) -> None:
|
232
260
|
"""Removes an existing session event callback.
|
233
261
|
|
234
262
|
Parameters
|
@@ -236,10 +264,12 @@ class Rekordbox6Database:
|
|
236
264
|
identifier : str
|
237
265
|
The identifier of the event
|
238
266
|
"""
|
267
|
+
if self.session is None:
|
268
|
+
raise SessionNotInitializedError()
|
239
269
|
fn = self._events[identifier]
|
240
270
|
event.remove(self.session, identifier, fn)
|
241
271
|
|
242
|
-
def query(self, *entities, **kwargs):
|
272
|
+
def query(self, *entities: Any, **kwargs: Any) -> Any:
|
243
273
|
"""Creates a new SQL query for the given entities.
|
244
274
|
|
245
275
|
Parameters
|
@@ -266,9 +296,11 @@ class Rekordbox6Database:
|
|
266
296
|
>>> db = Rekordbox6Database()
|
267
297
|
>>> query = db.query(DjmdContent.Title)
|
268
298
|
"""
|
299
|
+
if self.session is None:
|
300
|
+
raise SessionNotInitializedError()
|
269
301
|
return self.session.query(*entities, **kwargs)
|
270
302
|
|
271
|
-
def add(self, instance):
|
303
|
+
def add(self, instance: tables.Base) -> None:
|
272
304
|
"""Add an element to the Rekordbox database.
|
273
305
|
|
274
306
|
Parameters
|
@@ -276,10 +308,12 @@ class Rekordbox6Database:
|
|
276
308
|
instance : tables.Base
|
277
309
|
The table entry to add.
|
278
310
|
"""
|
311
|
+
if self.session is None:
|
312
|
+
raise SessionNotInitializedError()
|
279
313
|
self.session.add(instance)
|
280
314
|
self.registry.on_create(instance)
|
281
315
|
|
282
|
-
def delete(self, instance):
|
316
|
+
def delete(self, instance: tables.Base) -> None:
|
283
317
|
"""Delete an element from the Rekordbox database.
|
284
318
|
|
285
319
|
Parameters
|
@@ -287,10 +321,12 @@ class Rekordbox6Database:
|
|
287
321
|
instance : tables.Base
|
288
322
|
The table entry to delte.
|
289
323
|
"""
|
324
|
+
if self.session is None:
|
325
|
+
raise SessionNotInitializedError()
|
290
326
|
self.session.delete(instance)
|
291
327
|
self.registry.on_delete(instance)
|
292
328
|
|
293
|
-
def get_local_usn(self):
|
329
|
+
def get_local_usn(self) -> int:
|
294
330
|
"""Returns the local sequence number (update count) of Rekordbox.
|
295
331
|
|
296
332
|
Any changes made to the `Djmd...` tables increments the local update count of
|
@@ -304,7 +340,7 @@ class Rekordbox6Database:
|
|
304
340
|
"""
|
305
341
|
return self.registry.get_local_update_count()
|
306
342
|
|
307
|
-
def set_local_usn(self, usn):
|
343
|
+
def set_local_usn(self, usn: int) -> None:
|
308
344
|
"""Sets the local sequence number (update count) of Rekordbox.
|
309
345
|
|
310
346
|
Parameters
|
@@ -314,7 +350,7 @@ class Rekordbox6Database:
|
|
314
350
|
"""
|
315
351
|
self.registry.set_local_update_count(usn)
|
316
352
|
|
317
|
-
def increment_local_usn(self, num=1):
|
353
|
+
def increment_local_usn(self, num: int = 1) -> int:
|
318
354
|
"""Increments the local update sequence number (update count) of Rekordbox.
|
319
355
|
|
320
356
|
Parameters
|
@@ -342,7 +378,7 @@ class Rekordbox6Database:
|
|
342
378
|
"""
|
343
379
|
return self.registry.increment_local_update_count(num)
|
344
380
|
|
345
|
-
def autoincrement_usn(self, set_row_usn=True):
|
381
|
+
def autoincrement_usn(self, set_row_usn: bool = True) -> int:
|
346
382
|
"""Auto-increments the local USN for all uncommited changes.
|
347
383
|
|
348
384
|
Parameters
|
@@ -372,11 +408,13 @@ class Rekordbox6Database:
|
|
372
408
|
"""
|
373
409
|
return self.registry.autoincrement_local_update_count(set_row_usn)
|
374
410
|
|
375
|
-
def flush(self):
|
411
|
+
def flush(self) -> None:
|
376
412
|
"""Flushes the buffer of the SQLAlchemy session instance."""
|
413
|
+
if self.session is None:
|
414
|
+
raise SessionNotInitializedError()
|
377
415
|
self.session.flush()
|
378
416
|
|
379
|
-
def commit(self, autoinc=True):
|
417
|
+
def commit(self, autoinc: bool = True) -> None:
|
380
418
|
"""Commit the changes made to the database.
|
381
419
|
|
382
420
|
Parameters
|
@@ -389,6 +427,8 @@ class Rekordbox6Database:
|
|
389
427
|
--------
|
390
428
|
autoincrement_usn : Auto-increments the local Rekordbox USN's.
|
391
429
|
"""
|
430
|
+
if self.session is None:
|
431
|
+
raise SessionNotInitializedError()
|
392
432
|
pid = get_rekordbox_pid()
|
393
433
|
if pid:
|
394
434
|
raise RuntimeError(
|
@@ -423,45 +463,47 @@ class Rekordbox6Database:
|
|
423
463
|
if self.playlist_xml.modified:
|
424
464
|
self.playlist_xml.save()
|
425
465
|
|
426
|
-
def rollback(self):
|
466
|
+
def rollback(self) -> None:
|
427
467
|
"""Rolls back the uncommited changes to the database."""
|
468
|
+
if self.session is None:
|
469
|
+
raise SessionNotInitializedError()
|
428
470
|
self.session.rollback()
|
429
471
|
self.registry.clear_buffer()
|
430
472
|
|
431
473
|
# -- Table queries -----------------------------------------------------------------
|
432
474
|
|
433
|
-
def get_active_censor(self, **kwargs):
|
475
|
+
def get_active_censor(self, **kwargs: Any) -> Any:
|
434
476
|
"""Creates a filtered query for the ``DjmdActiveCensor`` table."""
|
435
477
|
query = self.query(tables.DjmdActiveCensor).filter_by(**kwargs)
|
436
478
|
return _parse_query_result(query, kwargs)
|
437
479
|
|
438
|
-
def get_album(self, **kwargs):
|
480
|
+
def get_album(self, **kwargs: Any) -> Any:
|
439
481
|
"""Creates a filtered query for the ``DjmdAlbum`` table."""
|
440
482
|
query = self.query(tables.DjmdAlbum).filter_by(**kwargs)
|
441
483
|
return _parse_query_result(query, kwargs)
|
442
484
|
|
443
|
-
def get_artist(self, **kwargs):
|
485
|
+
def get_artist(self, **kwargs: Any) -> Any:
|
444
486
|
"""Creates a filtered query for the ``DjmdArtist`` table."""
|
445
487
|
query = self.query(tables.DjmdArtist).filter_by(**kwargs)
|
446
488
|
return _parse_query_result(query, kwargs)
|
447
489
|
|
448
|
-
def get_category(self, **kwargs):
|
490
|
+
def get_category(self, **kwargs: Any) -> Any:
|
449
491
|
"""Creates a filtered query for the ``DjmdCategory`` table."""
|
450
492
|
query = self.query(tables.DjmdCategory).filter_by(**kwargs)
|
451
493
|
return _parse_query_result(query, kwargs)
|
452
494
|
|
453
|
-
def get_color(self, **kwargs):
|
495
|
+
def get_color(self, **kwargs: Any) -> Any:
|
454
496
|
"""Creates a filtered query for the ``DjmdColor`` table."""
|
455
497
|
query = self.query(tables.DjmdColor).filter_by(**kwargs)
|
456
498
|
return _parse_query_result(query, kwargs)
|
457
499
|
|
458
|
-
def get_content(self, **kwargs):
|
500
|
+
def get_content(self, **kwargs: Any) -> Any:
|
459
501
|
"""Creates a filtered query for the ``DjmdContent`` table."""
|
460
502
|
query = self.query(tables.DjmdContent).filter_by(**kwargs)
|
461
503
|
return _parse_query_result(query, kwargs)
|
462
504
|
|
463
505
|
# noinspection PyUnresolvedReferences
|
464
|
-
def search_content(self, text):
|
506
|
+
def search_content(self, text: str) -> List[DjmdContent]:
|
465
507
|
"""Searches the contents of the ``DjmdContent`` table.
|
466
508
|
|
467
509
|
The search is case-insensitive and includes the following collumns of the
|
@@ -514,86 +556,86 @@ class Rekordbox6Database:
|
|
514
556
|
query = self.query(DjmdContent).join(DjmdContent.Key)
|
515
557
|
results.update(query.filter(tables.DjmdKey.ScaleName.contains(text)).all())
|
516
558
|
|
517
|
-
|
518
|
-
|
519
|
-
return
|
559
|
+
result_list: List[DjmdContent] = list(results)
|
560
|
+
result_list.sort(key=lambda x: x.ID)
|
561
|
+
return result_list
|
520
562
|
|
521
|
-
def get_cue(self, **kwargs):
|
563
|
+
def get_cue(self, **kwargs: Any) -> Any:
|
522
564
|
"""Creates a filtered query for the ``DjmdCue`` table."""
|
523
565
|
query = self.query(tables.DjmdCue).filter_by(**kwargs)
|
524
566
|
return _parse_query_result(query, kwargs)
|
525
567
|
|
526
|
-
def get_device(self, **kwargs):
|
568
|
+
def get_device(self, **kwargs: Any) -> Any:
|
527
569
|
"""Creates a filtered query for the ``DjmdDevice`` table."""
|
528
570
|
query = self.query(tables.DjmdDevice).filter_by(**kwargs)
|
529
571
|
return _parse_query_result(query, kwargs)
|
530
572
|
|
531
|
-
def get_genre(self, **kwargs):
|
573
|
+
def get_genre(self, **kwargs: Any) -> Any:
|
532
574
|
"""Creates a filtered query for the ``DjmdGenre`` table."""
|
533
575
|
query = self.query(tables.DjmdGenre).filter_by(**kwargs)
|
534
576
|
return _parse_query_result(query, kwargs)
|
535
577
|
|
536
|
-
def get_history(self, **kwargs):
|
578
|
+
def get_history(self, **kwargs: Any) -> Any:
|
537
579
|
"""Creates a filtered query for the ``DjmdHistory`` table."""
|
538
580
|
query = self.query(tables.DjmdHistory).filter_by(**kwargs)
|
539
581
|
return _parse_query_result(query, kwargs)
|
540
582
|
|
541
|
-
def get_history_songs(self, **kwargs):
|
583
|
+
def get_history_songs(self, **kwargs: Any) -> Any:
|
542
584
|
"""Creates a filtered query for the ``DjmdSongHistory`` table."""
|
543
585
|
query = self.query(tables.DjmdSongHistory).filter_by(**kwargs)
|
544
586
|
return _parse_query_result(query, kwargs)
|
545
587
|
|
546
|
-
def get_hot_cue_banklist(self, **kwargs):
|
588
|
+
def get_hot_cue_banklist(self, **kwargs: Any) -> Any:
|
547
589
|
"""Creates a filtered query for the ``DjmdHotCueBanklist`` table."""
|
548
590
|
query = self.query(tables.DjmdHotCueBanklist).filter_by(**kwargs)
|
549
591
|
return _parse_query_result(query, kwargs)
|
550
592
|
|
551
|
-
def get_hot_cue_banklist_songs(self, **kwargs):
|
593
|
+
def get_hot_cue_banklist_songs(self, **kwargs: Any) -> Any:
|
552
594
|
"""Creates a filtered query for the ``DjmdSongHotCueBanklist`` table."""
|
553
595
|
query = self.query(tables.DjmdSongHotCueBanklist).filter_by(**kwargs)
|
554
596
|
return _parse_query_result(query, kwargs)
|
555
597
|
|
556
|
-
def get_key(self, **kwargs):
|
598
|
+
def get_key(self, **kwargs: Any) -> Any:
|
557
599
|
"""Creates a filtered query for the ``DjmdKey`` table."""
|
558
600
|
query = self.query(tables.DjmdKey).filter_by(**kwargs)
|
559
601
|
return _parse_query_result(query, kwargs)
|
560
602
|
|
561
|
-
def get_label(self, **kwargs):
|
603
|
+
def get_label(self, **kwargs: Any) -> Any:
|
562
604
|
"""Creates a filtered query for the ``DjmdLabel`` table."""
|
563
605
|
query = self.query(tables.DjmdLabel).filter_by(**kwargs)
|
564
606
|
return _parse_query_result(query, kwargs)
|
565
607
|
|
566
|
-
def get_menu_items(self, **kwargs):
|
608
|
+
def get_menu_items(self, **kwargs: Any) -> Any:
|
567
609
|
"""Creates a filtered query for the ``DjmdMenuItems`` table."""
|
568
610
|
query = self.query(tables.DjmdMenuItems).filter_by(**kwargs)
|
569
611
|
return _parse_query_result(query, kwargs)
|
570
612
|
|
571
|
-
def get_mixer_param(self, **kwargs):
|
613
|
+
def get_mixer_param(self, **kwargs: Any) -> Any:
|
572
614
|
"""Creates a filtered query for the ``DjmdMixerParam`` table."""
|
573
615
|
query = self.query(tables.DjmdMixerParam).filter_by(**kwargs)
|
574
616
|
return _parse_query_result(query, kwargs)
|
575
617
|
|
576
|
-
def get_my_tag(self, **kwargs):
|
618
|
+
def get_my_tag(self, **kwargs: Any) -> Any:
|
577
619
|
"""Creates a filtered query for the ``DjmdMyTag`` table."""
|
578
620
|
query = self.query(tables.DjmdMyTag).filter_by(**kwargs)
|
579
621
|
return _parse_query_result(query, kwargs)
|
580
622
|
|
581
|
-
def get_my_tag_songs(self, **kwargs):
|
623
|
+
def get_my_tag_songs(self, **kwargs: Any) -> Any:
|
582
624
|
"""Creates a filtered query for the ``DjmdSongMyTag`` table."""
|
583
625
|
query = self.query(tables.DjmdSongMyTag).filter_by(**kwargs)
|
584
626
|
return _parse_query_result(query, kwargs)
|
585
627
|
|
586
|
-
def get_playlist(self, **kwargs):
|
628
|
+
def get_playlist(self, **kwargs: Any) -> Any:
|
587
629
|
"""Creates a filtered query for the ``DjmdPlaylist`` table."""
|
588
630
|
query = self.query(tables.DjmdPlaylist).filter_by(**kwargs)
|
589
631
|
return _parse_query_result(query, kwargs)
|
590
632
|
|
591
|
-
def get_playlist_songs(self, **kwargs):
|
633
|
+
def get_playlist_songs(self, **kwargs: Any) -> Any:
|
592
634
|
"""Creates a filtered query for the ``DjmdSongPlaylist`` table."""
|
593
635
|
query = self.query(tables.DjmdSongPlaylist).filter_by(**kwargs)
|
594
636
|
return _parse_query_result(query, kwargs)
|
595
637
|
|
596
|
-
def get_playlist_contents(self, playlist, *entities) ->
|
638
|
+
def get_playlist_contents(self, playlist: PlaylistLike, *entities: tables.Base) -> Any:
|
597
639
|
"""Return the contents of a regular or smart playlist.
|
598
640
|
|
599
641
|
Parameters
|
@@ -625,111 +667,117 @@ class Rekordbox6Database:
|
|
625
667
|
>>> db.get_playlist_contents(pl, DjmdContent.ID).all()
|
626
668
|
[('12345678',), ('23456789',)]
|
627
669
|
"""
|
670
|
+
plist: DjmdPlaylist
|
628
671
|
if isinstance(playlist, (int, str)):
|
629
|
-
|
630
|
-
|
631
|
-
|
672
|
+
plist = self.get_playlist(ID=playlist)
|
673
|
+
else:
|
674
|
+
plist = playlist
|
675
|
+
|
676
|
+
if plist.is_folder:
|
677
|
+
raise ValueError(f"Playlist {plist} is a playlist folder.")
|
632
678
|
|
633
679
|
if not entities:
|
634
680
|
entities = [
|
635
681
|
DjmdContent,
|
636
|
-
]
|
682
|
+
] # type: ignore[assignment]
|
637
683
|
|
638
|
-
if
|
684
|
+
if plist.is_smart_playlist:
|
639
685
|
smartlist = SmartList()
|
640
|
-
smartlist.parse(
|
686
|
+
smartlist.parse(plist.SmartList)
|
641
687
|
filter_clause = smartlist.filter_clause()
|
642
688
|
else:
|
643
689
|
sub_query = self.query(tables.DjmdSongPlaylist.ContentID).filter(
|
644
|
-
tables.DjmdSongPlaylist.PlaylistID ==
|
690
|
+
tables.DjmdSongPlaylist.PlaylistID == plist.ID
|
645
691
|
)
|
646
692
|
filter_clause = DjmdContent.ID.in_(select(sub_query.subquery()))
|
647
693
|
|
648
694
|
return self.query(*entities).filter(filter_clause)
|
649
695
|
|
650
|
-
def get_property(self, **kwargs):
|
696
|
+
def get_property(self, **kwargs: Any) -> Any:
|
651
697
|
"""Creates a filtered query for the ``DjmdProperty`` table."""
|
652
698
|
query = self.query(tables.DjmdProperty).filter_by(**kwargs)
|
653
699
|
return _parse_query_result(query, kwargs)
|
654
700
|
|
655
|
-
def get_related_tracks(self, **kwargs):
|
701
|
+
def get_related_tracks(self, **kwargs: Any) -> Any:
|
656
702
|
"""Creates a filtered query for the ``DjmdRelatedTracks`` table."""
|
657
703
|
query = self.query(tables.DjmdRelatedTracks).filter_by(**kwargs)
|
658
704
|
return _parse_query_result(query, kwargs)
|
659
705
|
|
660
|
-
def get_related_tracks_songs(self, **kwargs):
|
706
|
+
def get_related_tracks_songs(self, **kwargs: Any) -> Any:
|
661
707
|
"""Creates a filtered query for the ``DjmdSongRelatedTracks`` table."""
|
662
708
|
query = self.query(tables.DjmdSongRelatedTracks).filter_by(**kwargs)
|
663
709
|
return _parse_query_result(query, kwargs)
|
664
710
|
|
665
|
-
def get_sampler(self, **kwargs):
|
711
|
+
def get_sampler(self, **kwargs: Any) -> Any:
|
666
712
|
"""Creates a filtered query for the ``DjmdSampler`` table."""
|
667
713
|
query = self.query(tables.DjmdSampler).filter_by(**kwargs)
|
668
714
|
return _parse_query_result(query, kwargs)
|
669
715
|
|
670
|
-
def get_sampler_songs(self, **kwargs):
|
716
|
+
def get_sampler_songs(self, **kwargs: Any) -> Any:
|
671
717
|
"""Creates a filtered query for the ``DjmdSongSampler`` table."""
|
672
718
|
query = self.query(tables.DjmdSongSampler).filter_by(**kwargs)
|
673
719
|
return _parse_query_result(query, kwargs)
|
674
720
|
|
675
|
-
def get_tag_list_songs(self, **kwargs):
|
721
|
+
def get_tag_list_songs(self, **kwargs: Any) -> Any:
|
676
722
|
"""Creates a filtered query for the ``DjmdSongTagList`` table."""
|
677
723
|
query = self.query(tables.DjmdSongTagList).filter_by(**kwargs)
|
678
724
|
return _parse_query_result(query, kwargs)
|
679
725
|
|
680
|
-
def get_sort(self, **kwargs):
|
726
|
+
def get_sort(self, **kwargs: Any) -> Any:
|
681
727
|
"""Creates a filtered query for the ``DjmdSort`` table."""
|
682
728
|
query = self.query(tables.DjmdSort).filter_by(**kwargs)
|
683
729
|
return _parse_query_result(query, kwargs)
|
684
730
|
|
685
|
-
def get_agent_registry(self, **kwargs):
|
731
|
+
def get_agent_registry(self, **kwargs: Any) -> Any:
|
686
732
|
"""Creates a filtered query for the ``AgentRegistry`` table."""
|
687
733
|
query = self.query(tables.AgentRegistry).filter_by(**kwargs)
|
688
734
|
return _parse_query_result(query, kwargs)
|
689
735
|
|
690
|
-
def get_cloud_agent_registry(self, **kwargs):
|
736
|
+
def get_cloud_agent_registry(self, **kwargs: Any) -> Any:
|
691
737
|
"""Creates a filtered query for the ``CloudAgentRegistry`` table."""
|
692
738
|
query = self.query(tables.CloudAgentRegistry).filter_by(**kwargs)
|
693
739
|
return _parse_query_result(query, kwargs)
|
694
740
|
|
695
|
-
def get_content_active_censor(self, **kwargs):
|
741
|
+
def get_content_active_censor(self, **kwargs: Any) -> Any:
|
696
742
|
"""Creates a filtered query for the ``ContentActiveCensor`` table."""
|
697
743
|
query = self.query(tables.ContentActiveCensor).filter_by(**kwargs)
|
698
744
|
return _parse_query_result(query, kwargs)
|
699
745
|
|
700
|
-
def get_content_cue(self, **kwargs):
|
746
|
+
def get_content_cue(self, **kwargs: Any) -> Any:
|
701
747
|
"""Creates a filtered query for the ``ContentCue`` table."""
|
702
748
|
query = self.query(tables.ContentCue).filter_by(**kwargs)
|
703
749
|
return _parse_query_result(query, kwargs)
|
704
750
|
|
705
|
-
def get_content_file(self, **kwargs):
|
751
|
+
def get_content_file(self, **kwargs: Any) -> Any:
|
706
752
|
"""Creates a filtered query for the ``ContentFile`` table."""
|
707
753
|
query = self.query(tables.ContentFile).filter_by(**kwargs)
|
708
754
|
return _parse_query_result(query, kwargs)
|
709
755
|
|
710
|
-
def get_hot_cue_banklist_cue(self, **kwargs):
|
756
|
+
def get_hot_cue_banklist_cue(self, **kwargs: Any) -> Any:
|
711
757
|
"""Creates a filtered query for the ``HotCueBanklistCue`` table."""
|
712
758
|
query = self.query(tables.HotCueBanklistCue).filter_by(**kwargs)
|
713
759
|
return _parse_query_result(query, kwargs)
|
714
760
|
|
715
|
-
def get_image_file(self, **kwargs):
|
761
|
+
def get_image_file(self, **kwargs: Any) -> Any:
|
716
762
|
"""Creates a filtered query for the ``ImageFile`` table."""
|
717
763
|
query = self.query(tables.ImageFile).filter_by(**kwargs)
|
718
764
|
return _parse_query_result(query, kwargs)
|
719
765
|
|
720
|
-
def get_setting_file(self, **kwargs):
|
766
|
+
def get_setting_file(self, **kwargs: Any) -> Any:
|
721
767
|
"""Creates a filtered query for the ``SettingFile`` table."""
|
722
768
|
query = self.query(tables.SettingFile).filter_by(**kwargs)
|
723
769
|
return _parse_query_result(query, kwargs)
|
724
770
|
|
725
|
-
def get_uuid_map(self, **kwargs):
|
771
|
+
def get_uuid_map(self, **kwargs: Any) -> Any:
|
726
772
|
"""Creates a filtered query for the ``UuidIDMap`` table."""
|
727
773
|
query = self.query(tables.UuidIDMap).filter_by(**kwargs)
|
728
774
|
return _parse_query_result(query, kwargs)
|
729
775
|
|
730
776
|
# -- Database updates --------------------------------------------------------------
|
731
777
|
|
732
|
-
def generate_unused_id(
|
778
|
+
def generate_unused_id(
|
779
|
+
self, table: Type[tables.Base], is_28_bit: bool = True, id_field_name: str = "ID"
|
780
|
+
) -> int:
|
733
781
|
"""Generates an unused ID for the given table."""
|
734
782
|
max_tries = 1000000
|
735
783
|
for _ in range(max_tries):
|
@@ -749,7 +797,9 @@ class Rekordbox6Database:
|
|
749
797
|
|
750
798
|
raise ValueError("Could not generate unused ID")
|
751
799
|
|
752
|
-
def add_to_playlist(
|
800
|
+
def add_to_playlist(
|
801
|
+
self, playlist: PlaylistLike, content: ContentLike, track_no: int = None
|
802
|
+
) -> tables.DjmdSongPlaylist:
|
753
803
|
"""Adds a track to a playlist.
|
754
804
|
|
755
805
|
Creates a new :class:`DjmdSongPlaylist` object corresponding to the given
|
@@ -793,18 +843,26 @@ class Rekordbox6Database:
|
|
793
843
|
>>> new_song.TrackNo
|
794
844
|
1
|
795
845
|
"""
|
846
|
+
plist: DjmdPlaylist
|
847
|
+
cont: DjmdContent
|
796
848
|
if isinstance(playlist, (int, str)):
|
797
|
-
|
849
|
+
plist = self.get_playlist(ID=playlist)
|
850
|
+
else:
|
851
|
+
plist = playlist
|
852
|
+
|
798
853
|
if isinstance(content, (int, str)):
|
799
|
-
|
854
|
+
cont = self.get_content(ID=content)
|
855
|
+
else:
|
856
|
+
cont = content
|
857
|
+
|
800
858
|
# Check playlist attribute (can't be folder or smart playlist)
|
801
|
-
if
|
859
|
+
if plist.Attribute != 0:
|
802
860
|
raise ValueError("Playlist must be a normal playlist")
|
803
861
|
|
804
862
|
uuid = str(uuid4())
|
805
863
|
id_ = str(uuid4())
|
806
864
|
now = datetime.datetime.now()
|
807
|
-
nsongs = self.query(tables.DjmdSongPlaylist).filter_by(PlaylistID=
|
865
|
+
nsongs = self.query(tables.DjmdSongPlaylist).filter_by(PlaylistID=plist.ID).count()
|
808
866
|
if track_no is not None:
|
809
867
|
insert_at_end = False
|
810
868
|
track_no = int(track_no)
|
@@ -816,8 +874,8 @@ class Rekordbox6Database:
|
|
816
874
|
insert_at_end = True
|
817
875
|
track_no = nsongs + 1
|
818
876
|
|
819
|
-
cid =
|
820
|
-
pid =
|
877
|
+
cid = cont.ID
|
878
|
+
pid = plist.ID
|
821
879
|
|
822
880
|
logger.info("Adding content with ID=%s to playlist with ID=%s:", cid, pid)
|
823
881
|
logger.debug("Content ID: %s", cid)
|
@@ -833,19 +891,19 @@ class Rekordbox6Database:
|
|
833
891
|
query = (
|
834
892
|
self.query(tables.DjmdSongPlaylist)
|
835
893
|
.filter(
|
836
|
-
tables.DjmdSongPlaylist.PlaylistID ==
|
894
|
+
tables.DjmdSongPlaylist.PlaylistID == plist.ID,
|
837
895
|
tables.DjmdSongPlaylist.TrackNo >= track_no,
|
838
896
|
)
|
839
897
|
.order_by(tables.DjmdSongPlaylist.TrackNo)
|
840
898
|
)
|
841
|
-
for
|
842
|
-
|
843
|
-
|
844
|
-
moved.append(
|
899
|
+
for other_song in query:
|
900
|
+
other_song.TrackNo += 1
|
901
|
+
other_song.updated_at = now
|
902
|
+
moved.append(other_song)
|
845
903
|
self.registry.enable_tracking()
|
846
904
|
|
847
905
|
# Add song to playlist
|
848
|
-
song = tables.DjmdSongPlaylist.create(
|
906
|
+
song: tables.DjmdSongPlaylist = tables.DjmdSongPlaylist.create(
|
849
907
|
ID=id_,
|
850
908
|
PlaylistID=str(pid),
|
851
909
|
ContentID=str(cid),
|
@@ -861,7 +919,11 @@ class Rekordbox6Database:
|
|
861
919
|
|
862
920
|
return song
|
863
921
|
|
864
|
-
def remove_from_playlist(
|
922
|
+
def remove_from_playlist(
|
923
|
+
self,
|
924
|
+
playlist: PlaylistLike,
|
925
|
+
song: Union[tables.DjmdSongPlaylist, int, str],
|
926
|
+
) -> None:
|
865
927
|
"""Removes a track from a playlist.
|
866
928
|
|
867
929
|
Parameters
|
@@ -883,36 +945,54 @@ class Rekordbox6Database:
|
|
883
945
|
>>> song = pl.Songs[0]
|
884
946
|
>>> db.remove_from_playlist(pl, song)
|
885
947
|
"""
|
948
|
+
plist: DjmdPlaylist
|
949
|
+
plist_song: DjmdSongPlaylist
|
886
950
|
if isinstance(playlist, (int, str)):
|
887
|
-
|
951
|
+
plist = self.get_playlist(ID=playlist)
|
952
|
+
else:
|
953
|
+
plist = playlist
|
954
|
+
|
888
955
|
if isinstance(song, (int, str)):
|
889
|
-
|
890
|
-
|
956
|
+
plist_song = self.query(tables.DjmdSongPlaylist).filter_by(ID=song).one()
|
957
|
+
else:
|
958
|
+
plist_song = song
|
959
|
+
|
960
|
+
if not isinstance(plist_song, tables.DjmdSongPlaylist):
|
961
|
+
raise ValueError(
|
962
|
+
"Playlist must be a DjmdSongPlaylist or corresponding playlist song ID!"
|
963
|
+
)
|
964
|
+
|
965
|
+
logger.info("Removing song with ID=%s from playlist with ID=%s", plist_song.ID, plist.ID)
|
891
966
|
now = datetime.datetime.now()
|
892
967
|
# Remove track from playlist
|
893
|
-
track_no =
|
894
|
-
self.delete(
|
968
|
+
track_no = plist_song.TrackNo
|
969
|
+
self.delete(plist_song)
|
895
970
|
self.commit()
|
896
971
|
# Update track numbers higher than the removed track
|
897
972
|
query = (
|
898
973
|
self.query(tables.DjmdSongPlaylist)
|
899
974
|
.filter(
|
900
|
-
tables.DjmdSongPlaylist.PlaylistID ==
|
975
|
+
tables.DjmdSongPlaylist.PlaylistID == plist.ID,
|
901
976
|
tables.DjmdSongPlaylist.TrackNo > track_no,
|
902
977
|
)
|
903
978
|
.order_by(tables.DjmdSongPlaylist.TrackNo)
|
904
979
|
)
|
905
980
|
moved = list()
|
906
981
|
with self.registry.disabled():
|
907
|
-
for
|
908
|
-
|
909
|
-
|
910
|
-
moved.append(
|
982
|
+
for other_song in query:
|
983
|
+
other_song.TrackNo -= 1
|
984
|
+
other_song.updated_at = now
|
985
|
+
moved.append(other_song)
|
911
986
|
|
912
987
|
if moved:
|
913
988
|
self.registry.on_move(moved)
|
914
989
|
|
915
|
-
def move_song_in_playlist(
|
990
|
+
def move_song_in_playlist(
|
991
|
+
self,
|
992
|
+
playlist: PlaylistLike,
|
993
|
+
song: Union[tables.DjmdSongPlaylist, int, str],
|
994
|
+
new_track_no: int,
|
995
|
+
) -> None:
|
916
996
|
"""Sets a new track number of a song.
|
917
997
|
|
918
998
|
Also updates the track numbers of the other songs in the playlist.
|
@@ -954,23 +1034,31 @@ class Rekordbox6Database:
|
|
954
1034
|
>>> [s.Content.Title for s in sorted(pl.Songs, key=lambda x: x.TrackNo)] # noqa
|
955
1035
|
['Demo Track 1', 'HORN', 'NOISE', 'Demo Track 2']
|
956
1036
|
"""
|
1037
|
+
plist: DjmdPlaylist
|
1038
|
+
plist_song: DjmdSongPlaylist
|
957
1039
|
if isinstance(playlist, (int, str)):
|
958
|
-
|
1040
|
+
plist = self.get_playlist(ID=playlist)
|
1041
|
+
else:
|
1042
|
+
plist = playlist
|
1043
|
+
|
959
1044
|
if isinstance(song, (int, str)):
|
960
|
-
|
961
|
-
|
1045
|
+
plist_song = self.query(tables.DjmdSongPlaylist).filter_by(ID=song).one()
|
1046
|
+
else:
|
1047
|
+
plist_song = song
|
1048
|
+
|
1049
|
+
nsongs = self.query(tables.DjmdSongPlaylist).filter_by(PlaylistID=plist.ID).count()
|
962
1050
|
if new_track_no < 1:
|
963
1051
|
raise ValueError("Track number must be greater than 0")
|
964
1052
|
if new_track_no > nsongs + 1:
|
965
1053
|
raise ValueError(f"Track number too high, parent contains {nsongs} items")
|
966
1054
|
logger.info(
|
967
1055
|
"Moving song with ID=%s in playlist with ID=%s to %s",
|
968
|
-
|
969
|
-
|
1056
|
+
plist_song.ID,
|
1057
|
+
plist.ID,
|
970
1058
|
new_track_no,
|
971
1059
|
)
|
972
1060
|
now = datetime.datetime.now()
|
973
|
-
old_track_no =
|
1061
|
+
old_track_no = plist_song.TrackNo
|
974
1062
|
|
975
1063
|
self.registry.disable_tracking()
|
976
1064
|
moved = list()
|
@@ -978,7 +1066,7 @@ class Rekordbox6Database:
|
|
978
1066
|
query = (
|
979
1067
|
self.query(tables.DjmdSongPlaylist)
|
980
1068
|
.filter(
|
981
|
-
tables.DjmdSongPlaylist.PlaylistID ==
|
1069
|
+
tables.DjmdSongPlaylist.PlaylistID == plist.ID,
|
982
1070
|
old_track_no < tables.DjmdSongPlaylist.TrackNo,
|
983
1071
|
tables.DjmdSongPlaylist.TrackNo <= new_track_no,
|
984
1072
|
)
|
@@ -990,7 +1078,7 @@ class Rekordbox6Database:
|
|
990
1078
|
moved.append(other_song)
|
991
1079
|
elif new_track_no < old_track_no:
|
992
1080
|
query = self.query(tables.DjmdSongPlaylist).filter(
|
993
|
-
tables.DjmdSongPlaylist.PlaylistID ==
|
1081
|
+
tables.DjmdSongPlaylist.PlaylistID == plist.ID,
|
994
1082
|
new_track_no <= tables.DjmdSongPlaylist.TrackNo,
|
995
1083
|
tables.DjmdSongPlaylist.TrackNo < old_track_no,
|
996
1084
|
)
|
@@ -1001,19 +1089,27 @@ class Rekordbox6Database:
|
|
1001
1089
|
else:
|
1002
1090
|
return
|
1003
1091
|
|
1004
|
-
|
1005
|
-
|
1092
|
+
plist_song.TrackNo = new_track_no
|
1093
|
+
plist_song.updated_at = now
|
1006
1094
|
moved.append(song)
|
1007
1095
|
|
1008
1096
|
self.registry.enable_tracking()
|
1009
1097
|
self.registry.on_move(moved)
|
1010
1098
|
|
1011
|
-
def _create_playlist(
|
1099
|
+
def _create_playlist(
|
1100
|
+
self,
|
1101
|
+
name: str,
|
1102
|
+
seq: Optional[int],
|
1103
|
+
image_path: Optional[str],
|
1104
|
+
parent: Optional[PlaylistLike],
|
1105
|
+
smart_list: Optional[SmartList] = None,
|
1106
|
+
attribute: int = None,
|
1107
|
+
) -> DjmdPlaylist:
|
1012
1108
|
"""Creates a new playlist object."""
|
1013
1109
|
table = tables.DjmdPlaylist
|
1014
1110
|
id_ = str(self.generate_unused_id(table, is_28_bit=True))
|
1015
1111
|
uuid = str(uuid4())
|
1016
|
-
|
1112
|
+
attrib = int(attribute) if attribute is not None else 0
|
1017
1113
|
now = datetime.datetime.now()
|
1018
1114
|
if smart_list is not None:
|
1019
1115
|
# Set the playlist ID in the smart list and generate XML
|
@@ -1032,7 +1128,7 @@ class Rekordbox6Database:
|
|
1032
1128
|
raise ValueError("Parent is not a folder")
|
1033
1129
|
else:
|
1034
1130
|
# Check if parent exists and is a folder
|
1035
|
-
parent_id = parent
|
1131
|
+
parent_id = str(parent)
|
1036
1132
|
query = self.query(table.ID).filter(table.ID == parent_id, table.Attribute == 1)
|
1037
1133
|
if not self.query(query.exists()).scalar():
|
1038
1134
|
raise ValueError("Parent does not exist or is not a folder")
|
@@ -1057,7 +1153,7 @@ class Rekordbox6Database:
|
|
1057
1153
|
logger.debug("Name: %s", name)
|
1058
1154
|
logger.debug("Parent ID: %s", parent_id)
|
1059
1155
|
logger.debug("Seq: %s", seq)
|
1060
|
-
logger.debug("Attribute: %s",
|
1156
|
+
logger.debug("Attribute: %s", attrib)
|
1061
1157
|
logger.debug("Smart List: %s", smart_list_xml)
|
1062
1158
|
logger.debug("Image Path: %s", image_path)
|
1063
1159
|
|
@@ -1074,12 +1170,12 @@ class Rekordbox6Database:
|
|
1074
1170
|
|
1075
1171
|
# Add new playlist to database
|
1076
1172
|
# First create with name 'New playlist'
|
1077
|
-
playlist = table.create(
|
1173
|
+
playlist: DjmdPlaylist = table.create(
|
1078
1174
|
ID=id_,
|
1079
1175
|
Seq=seq,
|
1080
1176
|
Name="New playlist",
|
1081
1177
|
ImagePath=image_path,
|
1082
|
-
Attribute=
|
1178
|
+
Attribute=attrib,
|
1083
1179
|
ParentID=parent_id,
|
1084
1180
|
SmartList=smart_list_xml,
|
1085
1181
|
UUID=uuid,
|
@@ -1092,11 +1188,13 @@ class Rekordbox6Database:
|
|
1092
1188
|
|
1093
1189
|
# Update masterPlaylists6.xml
|
1094
1190
|
if self.playlist_xml is not None:
|
1095
|
-
self.playlist_xml.add(id_, parent_id,
|
1191
|
+
self.playlist_xml.add(id_, parent_id, attrib, now, lib_type=0, check_type=0)
|
1096
1192
|
|
1097
1193
|
return playlist
|
1098
1194
|
|
1099
|
-
def create_playlist(
|
1195
|
+
def create_playlist(
|
1196
|
+
self, name: str, parent: PlaylistLike = None, seq: int = None, image_path: str = None
|
1197
|
+
) -> DjmdPlaylist:
|
1100
1198
|
"""Creates a new playlist in the database.
|
1101
1199
|
|
1102
1200
|
Parameters
|
@@ -1142,7 +1240,9 @@ class Rekordbox6Database:
|
|
1142
1240
|
logger.info("Creating playlist %s", name)
|
1143
1241
|
return self._create_playlist(name, seq, image_path, parent, attribute=PlaylistType.PLAYLIST)
|
1144
1242
|
|
1145
|
-
def create_playlist_folder(
|
1243
|
+
def create_playlist_folder(
|
1244
|
+
self, name: str, parent: PlaylistLike = None, seq: int = None, image_path: str = None
|
1245
|
+
) -> DjmdPlaylist:
|
1146
1246
|
"""Creates a new playlist folder in the database.
|
1147
1247
|
|
1148
1248
|
Parameters
|
@@ -1183,8 +1283,13 @@ class Rekordbox6Database:
|
|
1183
1283
|
return self._create_playlist(name, seq, image_path, parent, attribute=PlaylistType.FOLDER)
|
1184
1284
|
|
1185
1285
|
def create_smart_playlist(
|
1186
|
-
self,
|
1187
|
-
|
1286
|
+
self,
|
1287
|
+
name: str,
|
1288
|
+
smart_list: SmartList,
|
1289
|
+
parent: PlaylistLike = None,
|
1290
|
+
seq: int = None,
|
1291
|
+
image_path: str = None,
|
1292
|
+
) -> DjmdPlaylist:
|
1188
1293
|
"""Creates a new smart playlist in the database.
|
1189
1294
|
|
1190
1295
|
Parameters
|
@@ -1229,7 +1334,7 @@ class Rekordbox6Database:
|
|
1229
1334
|
name, seq, image_path, parent, smart_list, PlaylistType.SMART_PLAYLIST
|
1230
1335
|
)
|
1231
1336
|
|
1232
|
-
def delete_playlist(self, playlist):
|
1337
|
+
def delete_playlist(self, playlist: PlaylistLike) -> None:
|
1233
1338
|
"""Deletes a playlist or playlist folder from the database.
|
1234
1339
|
|
1235
1340
|
Parameters
|
@@ -1251,17 +1356,22 @@ class Rekordbox6Database:
|
|
1251
1356
|
>>> folder = db.get_playlist(Name="My Folder").one()
|
1252
1357
|
>>> db.delete_playlist(folder)
|
1253
1358
|
"""
|
1359
|
+
plist: DjmdPlaylist
|
1254
1360
|
if isinstance(playlist, (int, str)):
|
1255
|
-
|
1361
|
+
plist = self.get_playlist(ID=playlist)
|
1362
|
+
else:
|
1363
|
+
plist = playlist
|
1364
|
+
if not isinstance(plist, DjmdPlaylist):
|
1365
|
+
raise ValueError("Playlist must be a DjmdPlaylist or corresponding playlist ID!")
|
1256
1366
|
|
1257
|
-
if
|
1258
|
-
logger.info("Deleting playlist folder '%s' with ID=%s",
|
1367
|
+
if plist.Attribute == 1:
|
1368
|
+
logger.info("Deleting playlist folder '%s' with ID=%s", plist.Name, plist.ID)
|
1259
1369
|
else:
|
1260
|
-
logger.info("Deleting playlist '%s' with ID=%s",
|
1370
|
+
logger.info("Deleting playlist '%s' with ID=%s", plist.Name, plist.ID)
|
1261
1371
|
|
1262
1372
|
now = datetime.datetime.now()
|
1263
|
-
seq =
|
1264
|
-
parent_id =
|
1373
|
+
seq = plist.Seq
|
1374
|
+
parent_id = plist.ParentID
|
1265
1375
|
|
1266
1376
|
self.registry.disable_tracking()
|
1267
1377
|
# Update seq numbers higher than the deleted seq number
|
@@ -1278,9 +1388,9 @@ class Rekordbox6Database:
|
|
1278
1388
|
pl.Seq -= 1
|
1279
1389
|
pl.updated_at = now
|
1280
1390
|
moved.append(pl)
|
1281
|
-
moved.append(
|
1391
|
+
moved.append(plist)
|
1282
1392
|
|
1283
|
-
children = [
|
1393
|
+
children = [plist]
|
1284
1394
|
# Get all child playlist IDs
|
1285
1395
|
child_ids = list()
|
1286
1396
|
while len(children):
|
@@ -1298,14 +1408,16 @@ class Rekordbox6Database:
|
|
1298
1408
|
self.playlist_xml.remove(pid)
|
1299
1409
|
|
1300
1410
|
# Remove playlist from database
|
1301
|
-
self.delete(
|
1411
|
+
self.delete(plist)
|
1302
1412
|
self.registry.enable_tracking()
|
1303
1413
|
if len(child_ids) > 1:
|
1304
|
-
# The playlist folder had children:
|
1414
|
+
# The playlist folder had children: one extra USN increment
|
1305
1415
|
self.registry.on_delete(child_ids[1:])
|
1306
1416
|
self.registry.on_delete(moved)
|
1307
1417
|
|
1308
|
-
def move_playlist(
|
1418
|
+
def move_playlist(
|
1419
|
+
self, playlist: PlaylistLike, parent: PlaylistLike = None, seq: int = None
|
1420
|
+
) -> None:
|
1309
1421
|
"""Moves a playlist (folder) in the current parent folder or to a new one.
|
1310
1422
|
|
1311
1423
|
Parameters
|
@@ -1351,15 +1463,19 @@ class Rekordbox6Database:
|
|
1351
1463
|
"""
|
1352
1464
|
if parent is None and seq is None:
|
1353
1465
|
raise ValueError("Either parent or seq must be given")
|
1354
|
-
|
1355
|
-
|
1466
|
+
plist: DjmdPlaylist
|
1467
|
+
seqence: int
|
1356
1468
|
|
1469
|
+
if isinstance(playlist, (int, str)):
|
1470
|
+
plist = self.get_playlist(ID=playlist)
|
1471
|
+
else:
|
1472
|
+
plist = playlist
|
1357
1473
|
now = datetime.datetime.now()
|
1358
1474
|
table = tables.DjmdPlaylist
|
1359
1475
|
|
1360
1476
|
if parent is None:
|
1361
1477
|
# If no parent is given, keep the current parent
|
1362
|
-
parent_id =
|
1478
|
+
parent_id = plist.ParentID
|
1363
1479
|
elif isinstance(parent, tables.DjmdPlaylist):
|
1364
1480
|
# Check if parent is a folder
|
1365
1481
|
parent_id = parent.ID
|
@@ -1373,22 +1489,23 @@ class Rekordbox6Database:
|
|
1373
1489
|
raise ValueError("Parent does not exist or is not a folder")
|
1374
1490
|
|
1375
1491
|
n = self.get_playlist(ParentID=parent_id).count()
|
1376
|
-
old_seq =
|
1492
|
+
old_seq = plist.Seq
|
1377
1493
|
|
1378
|
-
if parent_id !=
|
1494
|
+
if parent_id != plist.ParentID:
|
1379
1495
|
# Move to new parent
|
1380
1496
|
|
1381
|
-
old_parent_id =
|
1497
|
+
old_parent_id = plist.ParentID
|
1382
1498
|
if seq is None:
|
1383
1499
|
# New playlist is last in parents
|
1384
|
-
|
1500
|
+
seqence = n + 1
|
1385
1501
|
insert_at_end = True
|
1386
1502
|
else:
|
1503
|
+
seqence = seq
|
1387
1504
|
# Check if sequence number is valid
|
1388
1505
|
insert_at_end = False
|
1389
|
-
if
|
1506
|
+
if seqence < 1:
|
1390
1507
|
raise ValueError("Sequence number must be greater than 0")
|
1391
|
-
elif
|
1508
|
+
elif seqence > n + 1:
|
1392
1509
|
raise ValueError(f"Sequence number too high, parent contains {n} items")
|
1393
1510
|
|
1394
1511
|
if not insert_at_end:
|
@@ -1404,10 +1521,10 @@ class Rekordbox6Database:
|
|
1404
1521
|
other_playlists = query.all()
|
1405
1522
|
# Set seq number and update time *before* other playlists to ensure
|
1406
1523
|
# right USN increment order
|
1407
|
-
|
1524
|
+
plist.ParentID = parent_id
|
1408
1525
|
with self.registry.disabled():
|
1409
|
-
|
1410
|
-
|
1526
|
+
plist.Seq = seqence
|
1527
|
+
plist.updated_at = now
|
1411
1528
|
|
1412
1529
|
if not insert_at_end:
|
1413
1530
|
# Update seq numbers higher than the new seq number in *new* parent
|
@@ -1438,31 +1555,33 @@ class Rekordbox6Database:
|
|
1438
1555
|
|
1439
1556
|
else:
|
1440
1557
|
# Keep parent, only change seq number
|
1441
|
-
|
1442
|
-
|
1558
|
+
if seq is None:
|
1559
|
+
raise ValueError("Sequence number must be given")
|
1560
|
+
seqence = seq
|
1561
|
+
if seqence < 1:
|
1443
1562
|
raise ValueError("Sequence number must be greater than 0")
|
1444
|
-
elif
|
1563
|
+
elif seqence > n + 1:
|
1445
1564
|
raise ValueError(f"Sequence number too high, parent contains {n} items")
|
1446
1565
|
|
1447
|
-
if
|
1566
|
+
if seqence > old_seq:
|
1448
1567
|
# Get all playlists with seq between old_seq and seq
|
1449
1568
|
query = (
|
1450
1569
|
self.query(tables.DjmdPlaylist)
|
1451
1570
|
.filter(
|
1452
|
-
tables.DjmdPlaylist.ParentID ==
|
1571
|
+
tables.DjmdPlaylist.ParentID == plist.ParentID,
|
1453
1572
|
old_seq < tables.DjmdPlaylist.Seq,
|
1454
|
-
tables.DjmdPlaylist.Seq <=
|
1573
|
+
tables.DjmdPlaylist.Seq <= seqence,
|
1455
1574
|
)
|
1456
1575
|
.order_by(tables.DjmdPlaylist.Seq)
|
1457
1576
|
)
|
1458
1577
|
other_playlists = query.all()
|
1459
1578
|
delta_seq = -1
|
1460
|
-
elif
|
1579
|
+
elif seqence < old_seq:
|
1461
1580
|
query = (
|
1462
1581
|
self.query(tables.DjmdPlaylist)
|
1463
1582
|
.filter(
|
1464
|
-
tables.DjmdPlaylist.ParentID ==
|
1465
|
-
|
1583
|
+
tables.DjmdPlaylist.ParentID == plist.ParentID,
|
1584
|
+
seqence <= tables.DjmdPlaylist.Seq,
|
1466
1585
|
tables.DjmdPlaylist.Seq < old_seq,
|
1467
1586
|
)
|
1468
1587
|
.order_by(tables.DjmdPlaylist.Seq)
|
@@ -1474,10 +1593,10 @@ class Rekordbox6Database:
|
|
1474
1593
|
|
1475
1594
|
# Set seq number and update time *before* other playlists to ensure
|
1476
1595
|
# right USN increment order
|
1477
|
-
|
1596
|
+
plist.Seq = seqence
|
1478
1597
|
# Each move counts as one USN increment, so disable for update time
|
1479
1598
|
with self.registry.disabled():
|
1480
|
-
|
1599
|
+
plist.updated_at = now
|
1481
1600
|
|
1482
1601
|
# Set seq number and update time for playlists between old_seq and seq
|
1483
1602
|
for pl in other_playlists:
|
@@ -1486,7 +1605,7 @@ class Rekordbox6Database:
|
|
1486
1605
|
with self.registry.disabled():
|
1487
1606
|
pl.updated_at = now
|
1488
1607
|
|
1489
|
-
def rename_playlist(self, playlist, name):
|
1608
|
+
def rename_playlist(self, playlist: PlaylistLike, name: str) -> None:
|
1490
1609
|
"""Renames a playlist or playlist folder.
|
1491
1610
|
|
1492
1611
|
Parameters
|
@@ -1514,16 +1633,27 @@ class Rekordbox6Database:
|
|
1514
1633
|
>>> [pl.Name for pl in playlists] # noqa
|
1515
1634
|
['Playlist new', 'Playlist 2']
|
1516
1635
|
"""
|
1636
|
+
pl: DjmdPlaylist
|
1517
1637
|
if isinstance(playlist, (int, str)):
|
1518
|
-
|
1638
|
+
pl = self.get_playlist(ID=playlist)
|
1639
|
+
else:
|
1640
|
+
pl = playlist
|
1641
|
+
|
1519
1642
|
now = datetime.datetime.now()
|
1520
1643
|
# Update name of playlist
|
1521
|
-
|
1644
|
+
pl.Name = name
|
1522
1645
|
# Update update time: USN not incremented
|
1523
1646
|
with self.registry.disabled():
|
1524
|
-
|
1647
|
+
pl.updated_at = now
|
1525
1648
|
|
1526
|
-
def add_album(
|
1649
|
+
def add_album(
|
1650
|
+
self,
|
1651
|
+
name: str,
|
1652
|
+
artist: Union[tables.DjmdArtist, int, str] = None,
|
1653
|
+
image_path: PathLike = None,
|
1654
|
+
compilation: bool = None,
|
1655
|
+
search_str: str = None,
|
1656
|
+
) -> tables.DjmdAlbum:
|
1527
1657
|
"""Adds a new album to the database.
|
1528
1658
|
|
1529
1659
|
Parameters
|
@@ -1583,18 +1713,22 @@ class Rekordbox6Database:
|
|
1583
1713
|
raise ValueError(f"Album '{name}' already exists in database")
|
1584
1714
|
|
1585
1715
|
# Get artist ID
|
1716
|
+
artist_id: Optional[str] = None
|
1586
1717
|
if artist is not None:
|
1718
|
+
art: tables.DjmdArtist
|
1587
1719
|
if isinstance(artist, (int, str)):
|
1588
|
-
|
1589
|
-
|
1720
|
+
art = self.get_artist(ID=artist)
|
1721
|
+
else:
|
1722
|
+
art = artist
|
1723
|
+
artist_id = art.ID
|
1590
1724
|
|
1591
1725
|
id_ = self.generate_unused_id(tables.DjmdAlbum)
|
1592
1726
|
uuid = str(uuid4())
|
1593
|
-
album = tables.DjmdAlbum.create(
|
1727
|
+
album: tables.DjmdAlbum = tables.DjmdAlbum.create(
|
1594
1728
|
ID=id_,
|
1595
1729
|
Name=name,
|
1596
|
-
AlbumArtistID=
|
1597
|
-
ImagePath=image_path,
|
1730
|
+
AlbumArtistID=artist_id,
|
1731
|
+
ImagePath=str(image_path) if image_path is not None else None,
|
1598
1732
|
Compilation=compilation,
|
1599
1733
|
SearchStr=search_str,
|
1600
1734
|
UUID=str(uuid),
|
@@ -1603,7 +1737,7 @@ class Rekordbox6Database:
|
|
1603
1737
|
self.flush()
|
1604
1738
|
return album
|
1605
1739
|
|
1606
|
-
def add_artist(self, name, search_str=None):
|
1740
|
+
def add_artist(self, name: str, search_str: str = None) -> tables.DjmdArtist:
|
1607
1741
|
"""Adds a new artist to the database.
|
1608
1742
|
|
1609
1743
|
Parameters
|
@@ -1655,12 +1789,14 @@ class Rekordbox6Database:
|
|
1655
1789
|
|
1656
1790
|
id_ = self.generate_unused_id(tables.DjmdArtist)
|
1657
1791
|
uuid = str(uuid4())
|
1658
|
-
artist = tables.DjmdArtist.create(
|
1792
|
+
artist: tables.DjmdArtist = tables.DjmdArtist.create(
|
1793
|
+
ID=id_, Name=name, SearchStr=search_str, UUID=uuid
|
1794
|
+
)
|
1659
1795
|
self.add(artist)
|
1660
1796
|
self.flush()
|
1661
1797
|
return artist
|
1662
1798
|
|
1663
|
-
def add_genre(self, name):
|
1799
|
+
def add_genre(self, name: str) -> tables.DjmdGenre:
|
1664
1800
|
"""Adds a new genre to the database.
|
1665
1801
|
|
1666
1802
|
Parameters
|
@@ -1705,12 +1841,12 @@ class Rekordbox6Database:
|
|
1705
1841
|
|
1706
1842
|
id_ = self.generate_unused_id(tables.DjmdGenre)
|
1707
1843
|
uuid = str(uuid4())
|
1708
|
-
genre = tables.DjmdGenre.create(ID=id_, Name=name, UUID=uuid)
|
1844
|
+
genre: tables.DjmdGenre = tables.DjmdGenre.create(ID=id_, Name=name, UUID=uuid)
|
1709
1845
|
self.add(genre)
|
1710
1846
|
self.flush()
|
1711
1847
|
return genre
|
1712
1848
|
|
1713
|
-
def add_label(self, name):
|
1849
|
+
def add_label(self, name: str) -> tables.DjmdLabel:
|
1714
1850
|
"""Adds a new label to the database.
|
1715
1851
|
|
1716
1852
|
Parameters
|
@@ -1755,12 +1891,12 @@ class Rekordbox6Database:
|
|
1755
1891
|
|
1756
1892
|
id_ = self.generate_unused_id(tables.DjmdLabel)
|
1757
1893
|
uuid = str(uuid4())
|
1758
|
-
label = tables.DjmdLabel.create(ID=id_, Name=name, UUID=uuid)
|
1894
|
+
label: tables.DjmdLabel = tables.DjmdLabel.create(ID=id_, Name=name, UUID=uuid)
|
1759
1895
|
self.add(label)
|
1760
1896
|
self.flush()
|
1761
1897
|
return label
|
1762
1898
|
|
1763
|
-
def add_content(self, path, **kwargs):
|
1899
|
+
def add_content(self, path: PathLike, **kwargs: Any) -> DjmdContent:
|
1764
1900
|
"""Adds a new track to the database.
|
1765
1901
|
|
1766
1902
|
Parameters
|
@@ -1811,7 +1947,7 @@ class Rekordbox6Database:
|
|
1811
1947
|
except ValueError:
|
1812
1948
|
raise ValueError(f"Invalid file type: {path.suffix}")
|
1813
1949
|
|
1814
|
-
content = tables.DjmdContent.create(
|
1950
|
+
content: DjmdContent = tables.DjmdContent.create(
|
1815
1951
|
ID=id_,
|
1816
1952
|
UUID=uuid,
|
1817
1953
|
ContentLink=content_link.rb_local_usn,
|
@@ -1834,7 +1970,7 @@ class Rekordbox6Database:
|
|
1834
1970
|
|
1835
1971
|
# ----------------------------------------------------------------------------------
|
1836
1972
|
|
1837
|
-
def get_mysetting_paths(self):
|
1973
|
+
def get_mysetting_paths(self) -> List[Path]:
|
1838
1974
|
"""Returns the file paths of the local Rekordbox MySetting files.
|
1839
1975
|
|
1840
1976
|
Returns
|
@@ -1842,12 +1978,12 @@ class Rekordbox6Database:
|
|
1842
1978
|
paths : list[str]
|
1843
1979
|
the file paths of the local MySetting files.
|
1844
1980
|
"""
|
1845
|
-
paths = list()
|
1981
|
+
paths: List[Path] = list()
|
1846
1982
|
for item in self.get_setting_file():
|
1847
1983
|
paths.append(self._db_dir / item.Path.lstrip("/\\"))
|
1848
1984
|
return paths
|
1849
1985
|
|
1850
|
-
def get_anlz_dir(self, content):
|
1986
|
+
def get_anlz_dir(self, content: ContentLike) -> Path:
|
1851
1987
|
"""Returns the directory path containing the ANLZ analysis files of a track.
|
1852
1988
|
|
1853
1989
|
Parameters
|
@@ -1862,14 +1998,17 @@ class Rekordbox6Database:
|
|
1862
1998
|
anlz_dir : Path
|
1863
1999
|
The path of the directory containing the analysis files for the content.
|
1864
2000
|
"""
|
2001
|
+
cont: DjmdContent
|
1865
2002
|
if isinstance(content, (int, str)):
|
1866
|
-
|
2003
|
+
cont = self.get_content(ID=content)
|
2004
|
+
else:
|
2005
|
+
cont = content
|
1867
2006
|
|
1868
|
-
dat_path = Path(
|
1869
|
-
path = self._share_dir / dat_path.parent
|
2007
|
+
dat_path = Path(cont.AnalysisDataPath.strip("\\/"))
|
2008
|
+
path: Path = self._share_dir / dat_path.parent
|
1870
2009
|
return path
|
1871
2010
|
|
1872
|
-
def get_anlz_paths(self, content):
|
2011
|
+
def get_anlz_paths(self, content: ContentLike) -> Dict[str, Optional[Path]]:
|
1873
2012
|
"""Returns all existing ANLZ analysis file paths of a track.
|
1874
2013
|
|
1875
2014
|
Parameters
|
@@ -1888,7 +2027,7 @@ class Rekordbox6Database:
|
|
1888
2027
|
root = self.get_anlz_dir(content)
|
1889
2028
|
return get_anlz_paths(root)
|
1890
2029
|
|
1891
|
-
def read_anlz_files(self, content):
|
2030
|
+
def read_anlz_files(self, content: ContentLike) -> Dict[Path, AnlzFile]:
|
1892
2031
|
"""Reads all existing ANLZ analysis files of a track.
|
1893
2032
|
|
1894
2033
|
Parameters
|
@@ -1907,7 +2046,7 @@ class Rekordbox6Database:
|
|
1907
2046
|
root = self.get_anlz_dir(content)
|
1908
2047
|
return read_anlz_files(root)
|
1909
2048
|
|
1910
|
-
def get_anlz_path(self, content, type_):
|
2049
|
+
def get_anlz_path(self, content: ContentLike, type_: str) -> Optional[PathLike]:
|
1911
2050
|
"""Returns the file path of an ANLZ analysis file of a track.
|
1912
2051
|
|
1913
2052
|
Parameters
|
@@ -1930,7 +2069,7 @@ class Rekordbox6Database:
|
|
1930
2069
|
paths = get_anlz_paths(root)
|
1931
2070
|
return paths.get(type_.upper(), "")
|
1932
2071
|
|
1933
|
-
def read_anlz_file(self, content, type_):
|
2072
|
+
def read_anlz_file(self, content: ContentLike, type_: str) -> Optional[AnlzFile]:
|
1934
2073
|
"""Reads an ANLZ analysis file of a track.
|
1935
2074
|
|
1936
2075
|
Parameters
|
@@ -1954,7 +2093,14 @@ class Rekordbox6Database:
|
|
1954
2093
|
return AnlzFile.parse_file(path)
|
1955
2094
|
return None
|
1956
2095
|
|
1957
|
-
def update_content_path(
|
2096
|
+
def update_content_path(
|
2097
|
+
self,
|
2098
|
+
content: ContentLike,
|
2099
|
+
path: PathLike,
|
2100
|
+
save: bool = True,
|
2101
|
+
check_path: bool = True,
|
2102
|
+
commit: bool = True,
|
2103
|
+
) -> None:
|
1958
2104
|
"""Update the file path of a track in the Rekordbox v6 database.
|
1959
2105
|
|
1960
2106
|
This changes the `FolderPath` entry in the ``DjmdContent`` table and the
|
@@ -2001,16 +2147,20 @@ class Rekordbox6Database:
|
|
2001
2147
|
C:/Music/PioneerDJ/Sampler/PRESET ONESHOT/NOISE.wav
|
2002
2148
|
|
2003
2149
|
"""
|
2150
|
+
cont: DjmdContent
|
2004
2151
|
if isinstance(content, (int, str)):
|
2005
|
-
|
2006
|
-
|
2152
|
+
cont = self.get_content(ID=content)
|
2153
|
+
else:
|
2154
|
+
cont = content
|
2155
|
+
|
2156
|
+
cid = cont.ID
|
2007
2157
|
|
2008
2158
|
path = Path(path)
|
2009
2159
|
# Check and format path (the database and ANLZ files use "/" as path delimiter)
|
2010
2160
|
if check_path:
|
2011
2161
|
assert path.exists()
|
2012
2162
|
path = str(path).replace("\\", "/")
|
2013
|
-
old_path =
|
2163
|
+
old_path = cont.FolderPath
|
2014
2164
|
logger.info("Replacing '%s' with '%s' of content [%s]", old_path, path, cid)
|
2015
2165
|
|
2016
2166
|
# Update path in ANLZ files
|
@@ -2021,18 +2171,18 @@ class Rekordbox6Database:
|
|
2021
2171
|
|
2022
2172
|
# Update path in database (DjmdContent)
|
2023
2173
|
logger.debug("Updating database file path: %s", path)
|
2024
|
-
|
2174
|
+
cont.FolderPath = path
|
2025
2175
|
|
2026
2176
|
# Update the OrgFolderPath column with the new path
|
2027
2177
|
# if the column matches the old_path variable
|
2028
|
-
org_folder_path =
|
2178
|
+
org_folder_path = cont.OrgFolderPath
|
2029
2179
|
if org_folder_path == old_path:
|
2030
|
-
|
2180
|
+
cont.OrgFolderPath = path
|
2031
2181
|
|
2032
2182
|
# Update the FileNameL column with the new filename if it changed
|
2033
2183
|
new_name = path.split("/")[-1]
|
2034
|
-
if
|
2035
|
-
|
2184
|
+
if cont.FileNameL != new_name:
|
2185
|
+
cont.FileNameL = new_name
|
2036
2186
|
|
2037
2187
|
if save:
|
2038
2188
|
logger.debug("Saving ANLZ files")
|
@@ -2045,7 +2195,14 @@ class Rekordbox6Database:
|
|
2045
2195
|
logger.debug("Committing changes to the database")
|
2046
2196
|
self.commit()
|
2047
2197
|
|
2048
|
-
def update_content_filename(
|
2198
|
+
def update_content_filename(
|
2199
|
+
self,
|
2200
|
+
content: ContentLike,
|
2201
|
+
name: str,
|
2202
|
+
save: bool = True,
|
2203
|
+
check_path: bool = True,
|
2204
|
+
commit: bool = True,
|
2205
|
+
) -> None:
|
2049
2206
|
"""Update the file name of a track in the Rekordbox v6 database.
|
2050
2207
|
|
2051
2208
|
This changes the `FolderPath` entry in the ``DjmdContent`` table and the
|
@@ -2090,16 +2247,19 @@ class Rekordbox6Database:
|
|
2090
2247
|
>>> cont.FolderPath == file.get("path")
|
2091
2248
|
True
|
2092
2249
|
"""
|
2250
|
+
cont: DjmdContent
|
2093
2251
|
if isinstance(content, (int, str)):
|
2094
|
-
|
2252
|
+
cont = self.get_content(ID=content)
|
2253
|
+
else:
|
2254
|
+
cont = content
|
2095
2255
|
|
2096
|
-
old_path = Path(
|
2256
|
+
old_path = Path(cont.FolderPath)
|
2097
2257
|
ext = old_path.suffix
|
2098
2258
|
new_path = old_path.parent / name
|
2099
2259
|
new_path = new_path.with_suffix(ext)
|
2100
|
-
self.update_content_path(
|
2260
|
+
self.update_content_path(cont, new_path, save, check_path, commit=commit)
|
2101
2261
|
|
2102
|
-
def to_dict(self, verbose=False):
|
2262
|
+
def to_dict(self, verbose: bool = False) -> Dict[str, Any]:
|
2103
2263
|
"""Convert the database to a dictionary.
|
2104
2264
|
|
2105
2265
|
Parameters
|
@@ -2127,11 +2287,13 @@ class Rekordbox6Database:
|
|
2127
2287
|
data[table_name] = table_data
|
2128
2288
|
return data
|
2129
2289
|
|
2130
|
-
def to_json(
|
2290
|
+
def to_json(
|
2291
|
+
self, file: PathLike, indent: int = 4, sort_keys: bool = True, verbose: bool = False
|
2292
|
+
) -> None:
|
2131
2293
|
"""Convert the database to a JSON file."""
|
2132
2294
|
import json
|
2133
2295
|
|
2134
|
-
def json_serial(obj):
|
2296
|
+
def json_serial(obj: Any) -> Any:
|
2135
2297
|
if isinstance(obj, (datetime.datetime, datetime.date)):
|
2136
2298
|
return obj.isoformat()
|
2137
2299
|
raise TypeError(f"Type {type(obj)} not serializable")
|
@@ -2140,7 +2302,7 @@ class Rekordbox6Database:
|
|
2140
2302
|
with open(file, "w") as fp:
|
2141
2303
|
json.dump(data, fp, indent=indent, sort_keys=sort_keys, default=json_serial)
|
2142
2304
|
|
2143
|
-
def copy_unlocked(self, output_file):
|
2305
|
+
def copy_unlocked(self, output_file: PathLike) -> None:
|
2144
2306
|
src_engine = self.engine
|
2145
2307
|
src_metadata = MetaData()
|
2146
2308
|
exclude_tables = ("sqlite_master", "sqlite_sequence", "sqlite_temp_master")
|
@@ -2149,7 +2311,7 @@ class Rekordbox6Database:
|
|
2149
2311
|
dst_metadata = MetaData()
|
2150
2312
|
|
2151
2313
|
@event.listens_for(src_metadata, "column_reflect")
|
2152
|
-
def genericize_datatypes(inspector, tablename, column_dict):
|
2314
|
+
def genericize_datatypes(inspector, tablename, column_dict): # type: ignore # noqa: ANN202
|
2153
2315
|
type_ = column_dict["type"].as_generic(allow_nulltype=True)
|
2154
2316
|
if isinstance(type_, DateTime):
|
2155
2317
|
type_ = String
|