pyrekordbox 0.4.2__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.
@@ -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,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
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
 
@@ -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
- def _parse_query_result(query, kwargs):
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
- query = query.one()
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__(self, path=None, db_dir="", key="", unlock=True):
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,10 +135,10 @@ 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
- path = Path(path)
138
+ db_path: Path = Path(path)
123
139
  # make sure file exists
124
- if not path.exists():
125
- raise FileNotFoundError(f"File '{path}' does not exist!")
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
144
  if not _sqlcipher_available: # pragma: no cover
@@ -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}@/{path}?"
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:///{path}")
167
+ engine = create_engine(f"sqlite:///{db_path}")
152
168
 
153
169
  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!")
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=db_dir)
182
+ self.playlist_xml = MasterPlaylistXml(db_dir=db_directory)
166
183
  except FileNotFoundError:
167
- logger.warning(f"No masterPlaylists6.xml found in {db_dir}")
184
+ logger.warning(f"No masterPlaylists6.xml found in {db_directory}")
168
185
  self.playlist_xml = None
169
186
 
170
- self._db_dir = db_dir
171
- self._share_dir = db_dir / "share"
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__(self, exc_type, exc_val, exc_tb):
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
- results = list(results)
518
- results.sort(key=lambda x: x.ID)
519
- return results
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) -> Query:
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
- playlist = self.get_playlist(ID=playlist)
630
- if playlist.is_folder:
631
- raise ValueError(f"Playlist {playlist} is a playlist folder.")
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 playlist.is_smart_playlist:
684
+ if plist.is_smart_playlist:
639
685
  smartlist = SmartList()
640
- smartlist.parse(playlist.SmartList)
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 == playlist.ID
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(self, table, is_28_bit: bool = True, id_field_name: str = "ID") -> int:
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(self, playlist, content, track_no=None):
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
- playlist = self.get_playlist(ID=playlist)
849
+ plist = self.get_playlist(ID=playlist)
850
+ else:
851
+ plist = playlist
852
+
798
853
  if isinstance(content, (int, str)):
799
- content = self.get_content(ID=content)
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 playlist.Attribute != 0:
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=playlist.ID).count()
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 = content.ID
820
- pid = playlist.ID
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 == playlist.ID,
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 song in query:
842
- song.TrackNo += 1
843
- song.updated_at = now
844
- moved.append(song)
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(self, playlist, song):
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
- playlist = self.get_playlist(ID=playlist)
951
+ plist = self.get_playlist(ID=playlist)
952
+ else:
953
+ plist = playlist
954
+
888
955
  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)
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 = song.TrackNo
894
- self.delete(song)
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 == playlist.ID,
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 song in query:
908
- song.TrackNo -= 1
909
- song.updated_at = now
910
- moved.append(song)
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(self, playlist, song, new_track_no):
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
- playlist = self.get_playlist(ID=playlist)
1040
+ plist = self.get_playlist(ID=playlist)
1041
+ else:
1042
+ plist = playlist
1043
+
959
1044
  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()
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
- song.ID,
969
- playlist.ID,
1056
+ plist_song.ID,
1057
+ plist.ID,
970
1058
  new_track_no,
971
1059
  )
972
1060
  now = datetime.datetime.now()
973
- old_track_no = song.TrackNo
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 == playlist.ID,
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 == playlist.ID,
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
- song.TrackNo = new_track_no
1005
- song.updated_at = now
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(self, name, seq, image_path, parent, smart_list=None, attribute=None):
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
- attribute = int(attribute)
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", attribute)
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=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, attribute, now, lib_type=0, check_type=0)
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(self, name, parent=None, seq=None, image_path=None):
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(self, name, parent=None, seq=None, image_path=None):
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, name, smart_list: SmartList, parent=None, seq=None, image_path=None
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
- playlist = self.get_playlist(ID=playlist)
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 playlist.Attribute == 1:
1258
- logger.info("Deleting playlist folder '%s' with ID=%s", playlist.Name, playlist.ID)
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", playlist.Name, playlist.ID)
1370
+ logger.info("Deleting playlist '%s' with ID=%s", plist.Name, plist.ID)
1261
1371
 
1262
1372
  now = datetime.datetime.now()
1263
- seq = playlist.Seq
1264
- parent_id = playlist.ParentID
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(playlist)
1391
+ moved.append(plist)
1282
1392
 
1283
- children = [playlist]
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(playlist)
1411
+ self.delete(plist)
1302
1412
  self.registry.enable_tracking()
1303
1413
  if len(child_ids) > 1:
1304
- # The playlist folder had children: on extra USN increment
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(self, playlist, parent=None, seq=None):
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
- if isinstance(playlist, (int, str)):
1355
- playlist = self.get_playlist(ID=playlist)
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 = playlist.ParentID
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 = playlist.Seq
1492
+ old_seq = plist.Seq
1377
1493
 
1378
- if parent_id != playlist.ParentID:
1494
+ if parent_id != plist.ParentID:
1379
1495
  # Move to new parent
1380
1496
 
1381
- old_parent_id = playlist.ParentID
1497
+ old_parent_id = plist.ParentID
1382
1498
  if seq is None:
1383
1499
  # New playlist is last in parents
1384
- seq = n + 1
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 seq < 1:
1506
+ if seqence < 1:
1390
1507
  raise ValueError("Sequence number must be greater than 0")
1391
- elif seq > n + 1:
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
- playlist.ParentID = parent_id
1524
+ plist.ParentID = parent_id
1408
1525
  with self.registry.disabled():
1409
- playlist.Seq = seq
1410
- playlist.updated_at = now
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
- if seq < 1:
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 seq > n + 1:
1563
+ elif seqence > n + 1:
1445
1564
  raise ValueError(f"Sequence number too high, parent contains {n} items")
1446
1565
 
1447
- if seq > old_seq:
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 == playlist.ParentID,
1571
+ tables.DjmdPlaylist.ParentID == plist.ParentID,
1453
1572
  old_seq < tables.DjmdPlaylist.Seq,
1454
- tables.DjmdPlaylist.Seq <= 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 seq < old_seq:
1579
+ elif seqence < old_seq:
1461
1580
  query = (
1462
1581
  self.query(tables.DjmdPlaylist)
1463
1582
  .filter(
1464
- tables.DjmdPlaylist.ParentID == playlist.ParentID,
1465
- seq <= tables.DjmdPlaylist.Seq,
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
- playlist.Seq = seq
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
- playlist.updated_at = now
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
- playlist = self.get_playlist(ID=playlist)
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
- playlist.Name = name
1644
+ pl.Name = name
1522
1645
  # Update update time: USN not incremented
1523
1646
  with self.registry.disabled():
1524
- playlist.updated_at = now
1647
+ pl.updated_at = now
1525
1648
 
1526
- def add_album(self, name, artist=None, image_path=None, compilation=None, search_str=None):
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
- artist = self.get_artist(ID=artist)
1589
- artist = artist.ID
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=artist,
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(ID=id_, Name=name, SearchStr=search_str, UUID=uuid)
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
- content = self.get_content(ID=content)
2003
+ cont = self.get_content(ID=content)
2004
+ else:
2005
+ cont = content
1867
2006
 
1868
- dat_path = Path(content.AnalysisDataPath.strip("\\/"))
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(self, content, path, save=True, check_path=True, commit=True):
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
- content = self.get_content(ID=content)
2006
- cid = content.ID
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 = content.FolderPath
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
- content.FolderPath = path
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 = content.OrgFolderPath
2178
+ org_folder_path = cont.OrgFolderPath
2029
2179
  if org_folder_path == old_path:
2030
- content.OrgFolderPath = path
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 content.FileNameL != new_name:
2035
- content.FileNameL = new_name
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(self, content, name, save=True, check_path=True, commit=True):
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
- content = self.get_content(ID=content)
2252
+ cont = self.get_content(ID=content)
2253
+ else:
2254
+ cont = content
2095
2255
 
2096
- old_path = Path(content.FolderPath)
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(content, new_path, save, check_path, commit=commit)
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(self, file, indent=4, sort_keys=True, verbose=False):
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