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.
@@ -6,7 +6,8 @@ import datetime
6
6
  import logging
7
7
  import secrets
8
8
  from pathlib import Path
9
- from typing import Optional
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
- logger = logging.getLogger(__name__)
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
- class NoCachedKey(Exception):
45
- pass
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
- query = query.one()
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__(self, path=None, db_dir="", key="", unlock=True):
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
- path = Path(path)
133
+ db_path: Path = Path(path)
123
134
  # make sure file exists
124
- if not path.exists():
125
- raise FileNotFoundError(f"File '{path}' does not exist!")
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
- try:
132
- key = rb_config["dp"]
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
- if not key.startswith("402fd"):
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
- url = f"sqlite+pysqlcipher://:{key}@/{path}?"
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:///{path}")
153
+ engine = create_engine(f"sqlite:///{db_path}")
152
154
 
153
155
  if not db_dir:
154
- db_dir = path.parent
155
- db_dir = Path(db_dir)
156
- if not db_dir.exists():
157
- raise FileNotFoundError(f"Database directory '{db_dir}' does not exist!")
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=db_dir)
168
+ self.playlist_xml = MasterPlaylistXml(db_dir=db_directory)
166
169
  except FileNotFoundError:
167
- logger.warning(f"No masterPlaylists6.xml found in {db_dir}")
170
+ logger.warning(f"No masterPlaylists6.xml found in {db_directory}")
168
171
  self.playlist_xml = None
169
172
 
170
- self._db_dir = db_dir
171
- self._share_dir = db_dir / "share"
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__(self, exc_type, exc_val, exc_tb):
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
- results = list(results)
518
- results.sort(key=lambda x: x.ID)
519
- return results
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) -> Query:
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
- playlist = self.get_playlist(ID=playlist)
630
- if playlist.is_folder:
631
- raise ValueError(f"Playlist {playlist} is a playlist folder.")
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 playlist.is_smart_playlist:
670
+ if plist.is_smart_playlist:
639
671
  smartlist = SmartList()
640
- smartlist.parse(playlist.SmartList)
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 == playlist.ID
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(self, table, is_28_bit: bool = True, id_field_name: str = "ID") -> int:
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(self, playlist, content, track_no=None):
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
- playlist = self.get_playlist(ID=playlist)
835
+ plist = self.get_playlist(ID=playlist)
836
+ else:
837
+ plist = playlist
838
+
798
839
  if isinstance(content, (int, str)):
799
- content = self.get_content(ID=content)
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 playlist.Attribute != 0:
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=playlist.ID).count()
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 = content.ID
820
- pid = playlist.ID
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 == playlist.ID,
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 song in query:
842
- song.TrackNo += 1
843
- song.updated_at = now
844
- moved.append(song)
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(self, playlist, song):
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
- playlist = self.get_playlist(ID=playlist)
937
+ plist = self.get_playlist(ID=playlist)
938
+ else:
939
+ plist = playlist
940
+
888
941
  if isinstance(song, (int, str)):
889
- song = self.query(tables.DjmdSongPlaylist).filter_by(ID=song).one()
890
- logger.info("Removing song with ID=%s from playlist with ID=%s", song.ID, playlist.ID)
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 = song.TrackNo
894
- self.delete(song)
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 == playlist.ID,
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 song in query:
908
- song.TrackNo -= 1
909
- song.updated_at = now
910
- moved.append(song)
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(self, playlist, song, new_track_no):
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
- playlist = self.get_playlist(ID=playlist)
1026
+ plist = self.get_playlist(ID=playlist)
1027
+ else:
1028
+ plist = playlist
1029
+
959
1030
  if isinstance(song, (int, str)):
960
- song = self.query(tables.DjmdSongPlaylist).filter_by(ID=song).one()
961
- nsongs = self.query(tables.DjmdSongPlaylist).filter_by(PlaylistID=playlist.ID).count()
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
- song.ID,
969
- playlist.ID,
1042
+ plist_song.ID,
1043
+ plist.ID,
970
1044
  new_track_no,
971
1045
  )
972
1046
  now = datetime.datetime.now()
973
- old_track_no = song.TrackNo
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 == playlist.ID,
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 == playlist.ID,
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
- song.TrackNo = new_track_no
1005
- song.updated_at = now
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(self, name, seq, image_path, parent, smart_list=None, attribute=None):
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
- attribute = int(attribute)
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", attribute)
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=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, attribute, now, lib_type=0, check_type=0)
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(self, name, parent=None, seq=None, image_path=None):
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(self, name, parent=None, seq=None, image_path=None):
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, name, smart_list: SmartList, parent=None, seq=None, image_path=None
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
- playlist = self.get_playlist(ID=playlist)
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 playlist.Attribute == 1:
1258
- logger.info("Deleting playlist folder '%s' with ID=%s", playlist.Name, playlist.ID)
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", playlist.Name, playlist.ID)
1356
+ logger.info("Deleting playlist '%s' with ID=%s", plist.Name, plist.ID)
1261
1357
 
1262
1358
  now = datetime.datetime.now()
1263
- seq = playlist.Seq
1264
- parent_id = playlist.ParentID
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(playlist)
1377
+ moved.append(plist)
1282
1378
 
1283
- children = [playlist]
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(playlist)
1397
+ self.delete(plist)
1302
1398
  self.registry.enable_tracking()
1303
1399
  if len(child_ids) > 1:
1304
- # The playlist folder had children: on extra USN increment
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(self, playlist, parent=None, seq=None):
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
- if isinstance(playlist, (int, str)):
1355
- playlist = self.get_playlist(ID=playlist)
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 = playlist.ParentID
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 = playlist.Seq
1478
+ old_seq = plist.Seq
1377
1479
 
1378
- if parent_id != playlist.ParentID:
1480
+ if parent_id != plist.ParentID:
1379
1481
  # Move to new parent
1380
1482
 
1381
- old_parent_id = playlist.ParentID
1483
+ old_parent_id = plist.ParentID
1382
1484
  if seq is None:
1383
1485
  # New playlist is last in parents
1384
- seq = n + 1
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 seq < 1:
1492
+ if seqence < 1:
1390
1493
  raise ValueError("Sequence number must be greater than 0")
1391
- elif seq > n + 1:
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
- playlist.ParentID = parent_id
1510
+ plist.ParentID = parent_id
1408
1511
  with self.registry.disabled():
1409
- playlist.Seq = seq
1410
- playlist.updated_at = now
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
- if seq < 1:
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 seq > n + 1:
1549
+ elif seqence > n + 1:
1445
1550
  raise ValueError(f"Sequence number too high, parent contains {n} items")
1446
1551
 
1447
- if seq > old_seq:
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 == playlist.ParentID,
1557
+ tables.DjmdPlaylist.ParentID == plist.ParentID,
1453
1558
  old_seq < tables.DjmdPlaylist.Seq,
1454
- tables.DjmdPlaylist.Seq <= 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 seq < old_seq:
1565
+ elif seqence < old_seq:
1461
1566
  query = (
1462
1567
  self.query(tables.DjmdPlaylist)
1463
1568
  .filter(
1464
- tables.DjmdPlaylist.ParentID == playlist.ParentID,
1465
- seq <= tables.DjmdPlaylist.Seq,
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
- playlist.Seq = seq
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
- playlist.updated_at = now
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
- playlist = self.get_playlist(ID=playlist)
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
- playlist.Name = name
1630
+ pl.Name = name
1522
1631
  # Update update time: USN not incremented
1523
1632
  with self.registry.disabled():
1524
- playlist.updated_at = now
1633
+ pl.updated_at = now
1525
1634
 
1526
- def add_album(self, name, artist=None, image_path=None, compilation=None, search_str=None):
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
- artist = self.get_artist(ID=artist)
1589
- artist = artist.ID
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=artist,
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(ID=id_, Name=name, SearchStr=search_str, UUID=uuid)
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
- content = self.get_content(ID=content)
1989
+ cont = self.get_content(ID=content)
1990
+ else:
1991
+ cont = content
1867
1992
 
1868
- dat_path = Path(content.AnalysisDataPath.strip("\\/"))
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(self, content, path, save=True, check_path=True, commit=True):
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
- content = self.get_content(ID=content)
2006
- cid = content.ID
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 = content.FolderPath
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
- content.FolderPath = path
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 = content.OrgFolderPath
2164
+ org_folder_path = cont.OrgFolderPath
2029
2165
  if org_folder_path == old_path:
2030
- content.OrgFolderPath = path
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 content.FileNameL != new_name:
2035
- content.FileNameL = new_name
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(self, content, name, save=True, check_path=True, commit=True):
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
- content = self.get_content(ID=content)
2238
+ cont = self.get_content(ID=content)
2239
+ else:
2240
+ cont = content
2095
2241
 
2096
- old_path = Path(content.FolderPath)
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(content, new_path, save, check_path, commit=commit)
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(self, file, indent=4, sort_keys=True, verbose=False):
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