pyrekordbox 0.3.2__py3-none-any.whl → 0.4.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. pyrekordbox/__init__.py +8 -8
  2. pyrekordbox/__main__.py +3 -2
  3. pyrekordbox/_version.py +9 -4
  4. pyrekordbox/anlz/__init__.py +3 -2
  5. pyrekordbox/anlz/file.py +4 -2
  6. pyrekordbox/anlz/tags.py +8 -16
  7. pyrekordbox/config.py +116 -47
  8. pyrekordbox/db6/__init__.py +2 -2
  9. pyrekordbox/db6/aux_files.py +3 -2
  10. pyrekordbox/db6/database.py +130 -177
  11. pyrekordbox/db6/registry.py +1 -0
  12. pyrekordbox/db6/smartlist.py +9 -11
  13. pyrekordbox/db6/tables.py +112 -132
  14. pyrekordbox/logger.py +0 -1
  15. pyrekordbox/mysettings/__init__.py +5 -4
  16. pyrekordbox/mysettings/file.py +3 -1
  17. pyrekordbox/rbxml.py +8 -12
  18. pyrekordbox/utils.py +4 -3
  19. {pyrekordbox-0.3.2.dist-info → pyrekordbox-0.4.1.dist-info}/METADATA +35 -48
  20. pyrekordbox-0.4.1.dist-info/RECORD +25 -0
  21. {pyrekordbox-0.3.2.dist-info → pyrekordbox-0.4.1.dist-info}/WHEEL +1 -1
  22. {pyrekordbox-0.3.2.dist-info → pyrekordbox-0.4.1.dist-info}/top_level.txt +0 -2
  23. docs/Makefile +0 -20
  24. docs/make.bat +0 -35
  25. docs/source/_static/images/anlz_beat.svg +0 -53
  26. docs/source/_static/images/anlz_file.svg +0 -204
  27. docs/source/_static/images/anlz_pco2.svg +0 -138
  28. docs/source/_static/images/anlz_pcob.svg +0 -148
  29. docs/source/_static/images/anlz_pcp2.svg +0 -398
  30. docs/source/_static/images/anlz_pcpt.svg +0 -263
  31. docs/source/_static/images/anlz_ppth.svg +0 -123
  32. docs/source/_static/images/anlz_pqt2.svg +0 -324
  33. docs/source/_static/images/anlz_pqt2_2.svg +0 -253
  34. docs/source/_static/images/anlz_pqtz.svg +0 -140
  35. docs/source/_static/images/anlz_pssi.svg +0 -192
  36. docs/source/_static/images/anlz_pssi_entry.svg +0 -191
  37. docs/source/_static/images/anlz_pvbr.svg +0 -125
  38. docs/source/_static/images/anlz_pwav.svg +0 -130
  39. docs/source/_static/images/anlz_pwv3.svg +0 -139
  40. docs/source/_static/images/anlz_pwv4.svg +0 -139
  41. docs/source/_static/images/anlz_pwv5.svg +0 -139
  42. docs/source/_static/images/anlz_pwv5_entry.svg +0 -100
  43. docs/source/_static/images/anlz_pwv6.svg +0 -130
  44. docs/source/_static/images/anlz_pwv7.svg +0 -139
  45. docs/source/_static/images/anlz_pwvc.svg +0 -125
  46. docs/source/_static/images/anlz_tag.svg +0 -110
  47. docs/source/_static/images/x64dbg_rb_key.png +0 -0
  48. docs/source/_static/logos/dark/logo_primary.svg +0 -75
  49. docs/source/_static/logos/light/logo_primary.svg +0 -75
  50. docs/source/_static/logos/mid/logo_primary.svg +0 -75
  51. docs/source/_templates/apidoc/module.rst_t +0 -8
  52. docs/source/_templates/apidoc/package.rst_t +0 -57
  53. docs/source/_templates/apidoc/toc.rst_t +0 -7
  54. docs/source/_templates/autosummary/class.rst +0 -32
  55. docs/source/_templates/autosummary/module.rst +0 -55
  56. docs/source/api.md +0 -18
  57. docs/source/conf.py +0 -178
  58. docs/source/development/changes.md +0 -3
  59. docs/source/development/contributing.md +0 -3
  60. docs/source/formats/anlz.md +0 -634
  61. docs/source/formats/db6.md +0 -1233
  62. docs/source/formats/mysetting.md +0 -392
  63. docs/source/formats/xml.md +0 -376
  64. docs/source/index.md +0 -103
  65. docs/source/installation.md +0 -271
  66. docs/source/key.md +0 -103
  67. docs/source/quickstart.md +0 -189
  68. docs/source/requirements.txt +0 -7
  69. docs/source/tutorial/anlz.md +0 -7
  70. docs/source/tutorial/configuration.md +0 -66
  71. docs/source/tutorial/db6.md +0 -178
  72. docs/source/tutorial/index.md +0 -20
  73. docs/source/tutorial/mysetting.md +0 -124
  74. docs/source/tutorial/xml.md +0 -140
  75. pyrekordbox/xml.py +0 -8
  76. pyrekordbox-0.3.2.dist-info/RECORD +0 -84
  77. tests/__init__.py +0 -3
  78. tests/test_anlz.py +0 -206
  79. tests/test_config.py +0 -175
  80. tests/test_db6.py +0 -1193
  81. tests/test_mysetting.py +0 -203
  82. tests/test_xml.py +0 -629
  83. {pyrekordbox-0.3.2.dist-info → pyrekordbox-0.4.1.dist-info/licenses}/LICENSE +0 -0
@@ -2,24 +2,26 @@
2
2
  # Author: Dylan Jones
3
3
  # Date: 2023-08-13
4
4
 
5
- import logging
6
5
  import datetime
6
+ import logging
7
7
  import secrets
8
- from uuid import uuid4
9
8
  from pathlib import Path
10
9
  from typing import Optional
11
- from sqlalchemy import create_engine, or_, event, MetaData, select
12
- from sqlalchemy.orm import Session, Query
10
+ from uuid import uuid4
11
+
12
+ from sqlalchemy import MetaData, create_engine, event, or_, select
13
13
  from sqlalchemy.exc import NoResultFound
14
+ from sqlalchemy.orm import Query, Session
14
15
  from sqlalchemy.sql.sqltypes import DateTime, String
15
- from ..utils import get_rekordbox_pid, warn_deprecated
16
+
17
+ from ..anlz import AnlzFile, get_anlz_paths, read_anlz_files
16
18
  from ..config import get_config
17
- from ..anlz import get_anlz_paths, read_anlz_files, AnlzFile
18
- from .registry import RekordboxAgentRegistry
19
+ from ..utils import get_rekordbox_pid
20
+ from . import tables
19
21
  from .aux_files import MasterPlaylistXml
20
- from .tables import DjmdContent, PlaylistType
22
+ from .registry import RekordboxAgentRegistry
21
23
  from .smartlist import SmartList
22
- from . import tables
24
+ from .tables import DjmdContent, FileType, PlaylistType
23
25
 
24
26
  try:
25
27
  from sqlcipher3 import dbapi2 as sqlite3 # noqa
@@ -31,6 +33,10 @@ except ImportError:
31
33
  _sqlcipher_available = False
32
34
 
33
35
  MAX_VERSION = "6.6.5"
36
+ SPECIAL_PLAYLIST_IDS = [
37
+ "100000", # Cloud Library Sync
38
+ "200000", # CUE Analysis Playlist
39
+ ]
34
40
 
35
41
  logger = logging.getLogger(__name__)
36
42
 
@@ -39,101 +45,6 @@ class NoCachedKey(Exception):
39
45
  pass
40
46
 
41
47
 
42
- def open_rekordbox_database(path=None, key="", unlock=True, sql_driver=None):
43
- """Opens a connection to the Rekordbox v6 master.db SQLite3 database.
44
-
45
- Parameters
46
- ----------
47
- path : str or Path, optional
48
- The path of the Rekordbox v6 database file. By default, pyrekordbox
49
- automatically finds the Rekordbox v6 master.db database file.
50
- This parameter is only required for opening other databases or if the
51
- configuration fails.
52
- key : str, optional
53
- The database key. By default, pyrekordbox automatically reads the database
54
- key from the Rekordbox v6 configuration file. This parameter is only required
55
- if the key extraction fails.
56
- unlock: bool, optional
57
- Flag if the database needs to be decrypted. Set to False if you are opening
58
- an unencrypted test database.
59
- sql_driver : Callable, optional
60
- The SQLite driver to used for opening the database. The standard ``sqlite3``
61
- package is used as default driver.
62
-
63
- Returns
64
- -------
65
- con : sql_driver.Connection
66
- The opened Rekordbox v6 database connection.
67
-
68
- Examples
69
- --------
70
- Open the Rekordbox v6 master.db database:
71
-
72
- >>> db = open_rekordbox_database()
73
-
74
- Open a copy of the database:
75
-
76
- >>> db = open_rekordbox_database("path/to/master_copy.db")
77
-
78
- Open a decrypted copy of the database:
79
-
80
- >>> db = open_rekordbox_database("path/to/master_unlocked.db", unlock=False)
81
-
82
- To use the ``pysqlcipher3`` package as SQLite driver, either import it as
83
-
84
- >>> from sqlcipher3 import dbapi2 as sqlite3 # noqa
85
- >>> db = open_rekordbox_database("path/to/master_copy.db")
86
-
87
- or supply the package as driver:
88
-
89
- >>> from sqlcipher3 import dbapi2 # noqa
90
- >>> db = open_rekordbox_database("path/to/master_copy.db", sql_driver=dbapi2)
91
- """
92
- warn_deprecated("open_rekordbox_database", remove_in="0.4.0")
93
- rb6_config = get_config("rekordbox6")
94
-
95
- if not path:
96
- path = rb6_config["db_path"]
97
- path = Path(path)
98
- if not path.exists():
99
- raise FileNotFoundError(f"File '{path}' does not exist!")
100
- logger.info("Opening %s", path)
101
-
102
- # Open database
103
- if sql_driver is None:
104
- # Use default sqlite3 package
105
- # This requires that the 'sqlite3.dll' was replaced by the 'sqlcipher.dll'
106
- sql_driver = sqlite3
107
- con = sql_driver.connect(str(path))
108
-
109
- if unlock:
110
- if not key:
111
- try:
112
- key = rb6_config["dp"]
113
- except KeyError:
114
- raise NoCachedKey(
115
- "Could not unlock database: No key found\n"
116
- f"If you are using Rekordbox>{MAX_VERSION} the key can not be "
117
- f"extracted automatically!\n"
118
- "Please use the CLI of pyrekordbox to download the key or "
119
- "use the `key` parameter to manually provide the database key."
120
- )
121
- logger.info("Key: %s", key)
122
- # Unlock database
123
- con.execute(f"PRAGMA key='{key}'")
124
-
125
- # Check connection
126
- try:
127
- con.execute("SELECT name FROM sqlite_master WHERE type='table';")
128
- except sqlite3.DatabaseError as e:
129
- msg = f"Opening database failed: '{e}'. Check if the database key is correct!"
130
- raise sqlite3.DatabaseError(msg)
131
- else:
132
- logger.info("Database unlocked!")
133
-
134
- return con
135
-
136
-
137
48
  def _parse_query_result(query, kwargs):
138
49
  if "ID" in kwargs or "registry_id" in kwargs:
139
50
  try:
@@ -193,17 +104,21 @@ class Rekordbox6Database:
193
104
  """
194
105
 
195
106
  def __init__(self, path=None, db_dir="", key="", unlock=True):
196
- rb6_config = get_config("rekordbox6")
107
+ # get config of latest supported version
108
+ rb_config = get_config("rekordbox7")
109
+ if not rb_config:
110
+ rb_config = get_config("rekordbox6")
111
+
197
112
  pid = get_rekordbox_pid()
198
113
  if pid:
199
114
  logger.warning("Rekordbox is running!")
200
115
 
201
116
  if not path:
202
117
  # Get path from the RB config
203
- path = rb6_config.get("db_path", "")
118
+ path = rb_config.get("db_path", "")
204
119
  if not path:
205
120
  pdir = get_config("pioneer", "install_dir")
206
- raise FileNotFoundError(f"No Rekordbox v6 directory found in '{pdir}'")
121
+ raise FileNotFoundError(f"No Rekordbox v6/v7 directory found in '{pdir}'")
207
122
  path = Path(path)
208
123
  # make sure file exists
209
124
  if not path.exists():
@@ -211,12 +126,10 @@ class Rekordbox6Database:
211
126
  # Open database
212
127
  if unlock:
213
128
  if not _sqlcipher_available:
214
- raise ImportError(
215
- "Could not unlock database: 'sqlcipher3' package not found"
216
- )
129
+ raise ImportError("Could not unlock database: 'sqlcipher3' package not found")
217
130
  if not key:
218
131
  try:
219
- key = rb6_config["dp"]
132
+ key = rb_config["dp"]
220
133
  except KeyError:
221
134
  raise NoCachedKey(
222
135
  "Could not unlock database: No key found\n"
@@ -230,7 +143,7 @@ class Rekordbox6Database:
230
143
  if not key.startswith("402fd"):
231
144
  raise ValueError("The provided database key doesn't look valid!")
232
145
 
233
- logger.info("Key: %s", key)
146
+ logger.debug("Key: %s", key)
234
147
  # Unlock database and create engine
235
148
  url = f"sqlite+pysqlcipher://:{key}@/{path}?"
236
149
  engine = create_engine(url, module=sqlite3)
@@ -491,17 +404,20 @@ class Rekordbox6Database:
491
404
  # Sync the updated_at values of the playlists in the DB and the XML file
492
405
  for pl in self.get_playlist():
493
406
  plxml = self.playlist_xml.get(pl.ID)
494
- if plxml is None:
495
- raise ValueError(
496
- f"Playlist {pl.ID} not found in masterPlaylists6.xml! "
497
- "Did you add it manually? "
498
- "Use the create_playlist method instead."
499
- )
500
- ts = plxml["Timestamp"]
501
- diff = pl.updated_at - ts
502
- if abs(diff.total_seconds()) > 1:
503
- logger.debug("Updating updated_at of playlist %s in XML", pl.ID)
504
- self.playlist_xml.update(pl.ID, updated_at=pl.updated_at)
407
+ if plxml is not None:
408
+ ts = plxml["Timestamp"]
409
+ diff = pl.updated_at - ts
410
+ if abs(diff.total_seconds()) > 1:
411
+ logger.debug("Updating updated_at of playlist %s in XML", pl.ID)
412
+ self.playlist_xml.update(pl.ID, updated_at=pl.updated_at)
413
+ else:
414
+ # Dont warn for special playlists
415
+ if pl.ID not in SPECIAL_PLAYLIST_IDS:
416
+ logger.warning(
417
+ f"Playlist {pl.ID} not found in masterPlaylists6.xml! "
418
+ "Did you add it manually? "
419
+ "Use the create_playlist method instead."
420
+ )
505
421
 
506
422
  # Save the XML file if it was modified
507
423
  if self.playlist_xml.modified:
@@ -813,7 +729,7 @@ class Rekordbox6Database:
813
729
 
814
730
  # -- Database updates --------------------------------------------------------------
815
731
 
816
- def generate_unused_id(self, table, is_28_bit: bool = True) -> int:
732
+ def generate_unused_id(self, table, is_28_bit: bool = True, id_field_name: str = "ID") -> int:
817
733
  """Generates an unused ID for the given table."""
818
734
  max_tries = 1000000
819
735
  for _ in range(max_tries):
@@ -825,7 +741,8 @@ class Rekordbox6Database:
825
741
  if id_ < 100:
826
742
  continue
827
743
  # Check if ID is already used
828
- query = self.query(table.ID).filter(table.ID == id_)
744
+ id_field = getattr(table, id_field_name)
745
+ query = self.query(id_field).filter(id_field == id_)
829
746
  used = self.query(query.exists()).scalar()
830
747
  if not used:
831
748
  return id_
@@ -887,20 +804,14 @@ class Rekordbox6Database:
887
804
  uuid = str(uuid4())
888
805
  id_ = str(uuid4())
889
806
  now = datetime.datetime.now()
890
- nsongs = (
891
- self.query(tables.DjmdSongPlaylist)
892
- .filter_by(PlaylistID=playlist.ID)
893
- .count()
894
- )
807
+ nsongs = self.query(tables.DjmdSongPlaylist).filter_by(PlaylistID=playlist.ID).count()
895
808
  if track_no is not None:
896
809
  insert_at_end = False
897
810
  track_no = int(track_no)
898
811
  if track_no < 1:
899
812
  raise ValueError("Track number must be greater than 0")
900
813
  if track_no > nsongs + 1:
901
- raise ValueError(
902
- f"Track number too high, parent contains {nsongs} items"
903
- )
814
+ raise ValueError(f"Track number too high, parent contains {nsongs} items")
904
815
  else:
905
816
  insert_at_end = True
906
817
  track_no = nsongs + 1
@@ -976,9 +887,7 @@ class Rekordbox6Database:
976
887
  playlist = self.get_playlist(ID=playlist)
977
888
  if isinstance(song, (int, str)):
978
889
  song = self.query(tables.DjmdSongPlaylist).filter_by(ID=song).one()
979
- logger.info(
980
- "Removing song with ID=%s from playlist with ID=%s", song.ID, playlist.ID
981
- )
890
+ logger.info("Removing song with ID=%s from playlist with ID=%s", song.ID, playlist.ID)
982
891
  now = datetime.datetime.now()
983
892
  # Remove track from playlist
984
893
  track_no = song.TrackNo
@@ -1049,11 +958,7 @@ class Rekordbox6Database:
1049
958
  playlist = self.get_playlist(ID=playlist)
1050
959
  if isinstance(song, (int, str)):
1051
960
  song = self.query(tables.DjmdSongPlaylist).filter_by(ID=song).one()
1052
- nsongs = (
1053
- self.query(tables.DjmdSongPlaylist)
1054
- .filter_by(PlaylistID=playlist.ID)
1055
- .count()
1056
- )
961
+ nsongs = self.query(tables.DjmdSongPlaylist).filter_by(PlaylistID=playlist.ID).count()
1057
962
  if new_track_no < 1:
1058
963
  raise ValueError("Track number must be greater than 0")
1059
964
  if new_track_no > nsongs + 1:
@@ -1103,9 +1008,7 @@ class Rekordbox6Database:
1103
1008
  self.registry.enable_tracking()
1104
1009
  self.registry.on_move(moved)
1105
1010
 
1106
- def _create_playlist(
1107
- self, name, seq, image_path, parent, smart_list=None, attribute=None
1108
- ):
1011
+ def _create_playlist(self, name, seq, image_path, parent, smart_list=None, attribute=None):
1109
1012
  """Creates a new playlist object."""
1110
1013
  table = tables.DjmdPlaylist
1111
1014
  id_ = str(self.generate_unused_id(table, is_28_bit=True))
@@ -1130,9 +1033,7 @@ class Rekordbox6Database:
1130
1033
  else:
1131
1034
  # Check if parent exists and is a folder
1132
1035
  parent_id = parent
1133
- query = self.query(table.ID).filter(
1134
- table.ID == parent_id, table.Attribute == 1
1135
- )
1036
+ query = self.query(table.ID).filter(table.ID == parent_id, table.Attribute == 1)
1136
1037
  if not self.query(query.exists()).scalar():
1137
1038
  raise ValueError("Parent does not exist or is not a folder")
1138
1039
 
@@ -1191,9 +1092,7 @@ class Rekordbox6Database:
1191
1092
 
1192
1093
  # Update masterPlaylists6.xml
1193
1094
  if self.playlist_xml is not None:
1194
- self.playlist_xml.add(
1195
- id_, parent_id, attribute, now, lib_type=0, check_type=0
1196
- )
1095
+ self.playlist_xml.add(id_, parent_id, attribute, now, lib_type=0, check_type=0)
1197
1096
 
1198
1097
  return playlist
1199
1098
 
@@ -1241,9 +1140,7 @@ class Rekordbox6Database:
1241
1140
  '123456'
1242
1141
  """
1243
1142
  logger.info("Creating playlist %s", name)
1244
- return self._create_playlist(
1245
- name, seq, image_path, parent, attribute=PlaylistType.PLAYLIST
1246
- )
1143
+ return self._create_playlist(name, seq, image_path, parent, attribute=PlaylistType.PLAYLIST)
1247
1144
 
1248
1145
  def create_playlist_folder(self, name, parent=None, seq=None, image_path=None):
1249
1146
  """Creates a new playlist folder in the database.
@@ -1283,9 +1180,7 @@ class Rekordbox6Database:
1283
1180
  '123456'
1284
1181
  """
1285
1182
  logger.info("Creating playlist folder %s", name)
1286
- return self._create_playlist(
1287
- name, seq, image_path, parent, attribute=PlaylistType.FOLDER
1288
- )
1183
+ return self._create_playlist(name, seq, image_path, parent, attribute=PlaylistType.FOLDER)
1289
1184
 
1290
1185
  def create_smart_playlist(
1291
1186
  self, name, smart_list: SmartList, parent=None, seq=None, image_path=None
@@ -1360,9 +1255,7 @@ class Rekordbox6Database:
1360
1255
  playlist = self.get_playlist(ID=playlist)
1361
1256
 
1362
1257
  if playlist.Attribute == 1:
1363
- logger.info(
1364
- "Deleting playlist folder '%s' with ID=%s", playlist.Name, playlist.ID
1365
- )
1258
+ logger.info("Deleting playlist folder '%s' with ID=%s", playlist.Name, playlist.ID)
1366
1259
  else:
1367
1260
  logger.info("Deleting playlist '%s' with ID=%s", playlist.Name, playlist.ID)
1368
1261
 
@@ -1475,9 +1368,7 @@ class Rekordbox6Database:
1475
1368
  else:
1476
1369
  # Check if parent exists and is a folder
1477
1370
  parent_id = str(parent)
1478
- query = self.query(table.ID).filter(
1479
- table.ID == parent_id, table.Attribute == 1
1480
- )
1371
+ query = self.query(table.ID).filter(table.ID == parent_id, table.Attribute == 1)
1481
1372
  if not self.query(query.exists()).scalar():
1482
1373
  raise ValueError("Parent does not exist or is not a folder")
1483
1374
 
@@ -1498,9 +1389,7 @@ class Rekordbox6Database:
1498
1389
  if seq < 1:
1499
1390
  raise ValueError("Sequence number must be greater than 0")
1500
1391
  elif seq > n + 1:
1501
- raise ValueError(
1502
- f"Sequence number too high, parent contains {n} items"
1503
- )
1392
+ raise ValueError(f"Sequence number too high, parent contains {n} items")
1504
1393
 
1505
1394
  if not insert_at_end:
1506
1395
  # Get all playlists with seq between old_seq and seq
@@ -1634,9 +1523,7 @@ class Rekordbox6Database:
1634
1523
  with self.registry.disabled():
1635
1524
  playlist.updated_at = now
1636
1525
 
1637
- def add_album(
1638
- self, name, artist=None, image_path=None, compilation=None, search_str=None
1639
- ):
1526
+ def add_album(self, name, artist=None, image_path=None, compilation=None, search_str=None):
1640
1527
  """Adds a new album to the database.
1641
1528
 
1642
1529
  Parameters
@@ -1768,9 +1655,7 @@ class Rekordbox6Database:
1768
1655
 
1769
1656
  id_ = self.generate_unused_id(tables.DjmdArtist)
1770
1657
  uuid = str(uuid4())
1771
- artist = tables.DjmdArtist.create(
1772
- ID=id_, Name=name, SearchStr=search_str, UUID=uuid
1773
- )
1658
+ artist = tables.DjmdArtist.create(ID=id_, Name=name, SearchStr=search_str, UUID=uuid)
1774
1659
  self.add(artist)
1775
1660
  self.flush()
1776
1661
  return artist
@@ -1875,6 +1760,78 @@ class Rekordbox6Database:
1875
1760
  self.flush()
1876
1761
  return label
1877
1762
 
1763
+ def add_content(self, path, **kwargs):
1764
+ """Adds a new track to the database.
1765
+
1766
+ Parameters
1767
+ ----------
1768
+ path : str
1769
+ Absolute path to the music file to be added.
1770
+
1771
+ **kwargs:
1772
+ Keyword arguments passed to DjmdContent on creation. These arguments
1773
+ should be a valid DjmdContent field.
1774
+
1775
+ Returns
1776
+ -------
1777
+ content : DjmdContent
1778
+ The newly created track.
1779
+
1780
+ Raises
1781
+ ------
1782
+ ValueError : If a track with the same path already exists in the database.
1783
+ ValueError : If the file type is invalid.
1784
+
1785
+ Examples
1786
+ --------
1787
+ Add a new track to the database:
1788
+
1789
+ >>> db = Rekordbox6Database()
1790
+ >>> db.add_content("/Users/foo/Downloads/banger.mp3", Title="Banger")
1791
+ <DjmdContent(123456789 Title=Banger)>
1792
+ """
1793
+ path = Path(path)
1794
+ path_string = str(path)
1795
+ query = self.query(tables.DjmdContent).filter_by(FolderPath=path_string)
1796
+ if query.count() > 0:
1797
+ raise ValueError(f"Track with path '{path}' already exists in database")
1798
+
1799
+ id_ = self.generate_unused_id(tables.DjmdContent)
1800
+ file_id = self.generate_unused_id(tables.DjmdContent, id_field_name="rb_file_id")
1801
+ uuid = str(uuid4())
1802
+ content_link = self.get_menu_items(Name="TRACK").one()
1803
+ date_created = datetime.date.today()
1804
+ device = self.get_device().first()
1805
+ file_name_l = path.name
1806
+ file_size = path.stat().st_size
1807
+
1808
+ file_type_string = path.suffix.lstrip(".").upper()
1809
+ try:
1810
+ file_type = getattr(FileType, file_type_string)
1811
+ except ValueError:
1812
+ raise ValueError(f"Invalid file type: {path.suffix}")
1813
+
1814
+ content = tables.DjmdContent.create(
1815
+ ID=id_,
1816
+ UUID=uuid,
1817
+ ContentLink=content_link.rb_local_usn,
1818
+ DateCreated=date_created,
1819
+ DeviceID=device.ID,
1820
+ FileNameL=file_name_l,
1821
+ FileSize=file_size,
1822
+ FileType=file_type.value,
1823
+ FolderPath=path_string,
1824
+ HotCueAutoLoad="on",
1825
+ MasterDBID=device.MasterDBID,
1826
+ MasterSongID=id_,
1827
+ StockDate=date_created,
1828
+ rb_file_id=file_id,
1829
+ **kwargs,
1830
+ )
1831
+ self.add(content)
1832
+ self.flush()
1833
+ return content
1834
+
1878
1835
  # ----------------------------------------------------------------------------------
1879
1836
 
1880
1837
  def get_mysetting_paths(self):
@@ -1997,9 +1954,7 @@ class Rekordbox6Database:
1997
1954
  return AnlzFile.parse_file(path)
1998
1955
  return None
1999
1956
 
2000
- def update_content_path(
2001
- self, content, path, save=True, check_path=True, commit=True
2002
- ):
1957
+ def update_content_path(self, content, path, save=True, check_path=True, commit=True):
2003
1958
  """Update the file path of a track in the Rekordbox v6 database.
2004
1959
 
2005
1960
  This changes the `FolderPath` entry in the ``DjmdContent`` table and the
@@ -2090,9 +2045,7 @@ class Rekordbox6Database:
2090
2045
  logger.debug("Committing changes to the database")
2091
2046
  self.commit()
2092
2047
 
2093
- def update_content_filename(
2094
- self, content, name, save=True, check_path=True, commit=True
2095
- ):
2048
+ def update_content_filename(self, content, name, save=True, check_path=True, commit=True):
2096
2049
  """Update the file name of a track in the Rekordbox v6 database.
2097
2050
 
2098
2051
  This changes the `FolderPath` entry in the ``DjmdContent`` table and the
@@ -4,6 +4,7 @@
4
4
 
5
5
  import logging
6
6
  from contextlib import contextmanager
7
+
7
8
  from sqlalchemy.orm.exc import ObjectDeletedError
8
9
 
9
10
  logger = logging.getLogger(__name__)
@@ -4,14 +4,14 @@
4
4
 
5
5
  import logging
6
6
  import xml.etree.cElementTree as xml
7
+ from dataclasses import dataclass
8
+ from datetime import datetime
7
9
  from enum import Enum, IntEnum
8
10
  from typing import List, Union
9
- from datetime import datetime
10
- from dataclasses import dataclass
11
11
 
12
- from sqlalchemy import or_, and_, not_
13
- from sqlalchemy.sql.elements import BooleanClauseList
14
12
  from dateutil.relativedelta import relativedelta # noqa
13
+ from sqlalchemy import and_, not_, or_
14
+ from sqlalchemy.sql.elements import BooleanClauseList
15
15
 
16
16
  from .tables import DjmdContent
17
17
 
@@ -179,8 +179,7 @@ class Condition:
179
179
  def __post_init__(self):
180
180
  if self.property not in PROPERTIES:
181
181
  raise ValueError(
182
- f"Invalid property: '{self.property}'! "
183
- f"Supported properties: {PROPERTIES}"
182
+ f"Invalid property: '{self.property}'! Supported properties: {PROPERTIES}"
184
183
  )
185
184
 
186
185
  valid_ops = VALID_OPS[self.property]
@@ -232,9 +231,7 @@ def _get_condition_values(cond):
232
231
  class SmartList:
233
232
  """Rekordbox smart playlist XML handler."""
234
233
 
235
- def __init__(
236
- self, logical_operator: int = LogicalOperator.ALL, auto_update: int = 0
237
- ):
234
+ def __init__(self, logical_operator: int = LogicalOperator.ALL, auto_update: int = 0):
238
235
  self.playlist_id: Union[int, str] = ""
239
236
  self.logical_operator: int = int(logical_operator)
240
237
  self.auto_update: int = auto_update
@@ -320,11 +317,12 @@ class SmartList:
320
317
  comps = list()
321
318
  for cond in self.conditions:
322
319
  val_left, val_right = _get_condition_values(cond)
323
-
320
+ # val_left = str(-abs(int(val_left))) if val_left is not None else ""
324
321
  if cond.property in PROPERTY_COLUMN_MAP:
325
322
  colum_name = PROPERTY_COLUMN_MAP[cond.property]
326
323
  if cond.property == Property.MYTAG:
327
- val_left = str(right_bitshift(int(val_left)))
324
+ if int(val_left) < 0:
325
+ val_left = str(right_bitshift(int(val_left)))
328
326
 
329
327
  if cond.operator == Operator.EQUAL:
330
328
  comp = getattr(DjmdContent, colum_name) == val_left