pyrekordbox 0.3.1__py3-none-any.whl → 0.4.0__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 (84) hide show
  1. pyrekordbox/__init__.py +8 -8
  2. pyrekordbox/__main__.py +3 -2
  3. pyrekordbox/_version.py +2 -2
  4. pyrekordbox/anlz/__init__.py +3 -2
  5. pyrekordbox/anlz/file.py +4 -2
  6. pyrekordbox/anlz/tags.py +3 -1
  7. pyrekordbox/config.py +79 -23
  8. pyrekordbox/db6/__init__.py +2 -2
  9. pyrekordbox/db6/aux_files.py +3 -2
  10. pyrekordbox/db6/database.py +227 -143
  11. pyrekordbox/db6/registry.py +1 -0
  12. pyrekordbox/db6/smartlist.py +375 -0
  13. pyrekordbox/db6/tables.py +81 -20
  14. pyrekordbox/logger.py +0 -1
  15. pyrekordbox/mysettings/__init__.py +5 -4
  16. pyrekordbox/mysettings/file.py +3 -1
  17. pyrekordbox/rbxml.py +5 -3
  18. pyrekordbox/utils.py +4 -3
  19. {pyrekordbox-0.3.1.dist-info → pyrekordbox-0.4.0.dist-info}/LICENSE +1 -1
  20. {pyrekordbox-0.3.1.dist-info → pyrekordbox-0.4.0.dist-info}/METADATA +26 -42
  21. pyrekordbox-0.4.0.dist-info/RECORD +25 -0
  22. {pyrekordbox-0.3.1.dist-info → pyrekordbox-0.4.0.dist-info}/WHEEL +1 -1
  23. {pyrekordbox-0.3.1.dist-info → pyrekordbox-0.4.0.dist-info}/top_level.txt +0 -2
  24. docs/Makefile +0 -20
  25. docs/make.bat +0 -35
  26. docs/source/_static/images/anlz_beat.svg +0 -53
  27. docs/source/_static/images/anlz_file.svg +0 -204
  28. docs/source/_static/images/anlz_pco2.svg +0 -138
  29. docs/source/_static/images/anlz_pcob.svg +0 -148
  30. docs/source/_static/images/anlz_pcp2.svg +0 -398
  31. docs/source/_static/images/anlz_pcpt.svg +0 -263
  32. docs/source/_static/images/anlz_ppth.svg +0 -123
  33. docs/source/_static/images/anlz_pqt2.svg +0 -324
  34. docs/source/_static/images/anlz_pqt2_2.svg +0 -253
  35. docs/source/_static/images/anlz_pqtz.svg +0 -140
  36. docs/source/_static/images/anlz_pssi.svg +0 -192
  37. docs/source/_static/images/anlz_pssi_entry.svg +0 -191
  38. docs/source/_static/images/anlz_pvbr.svg +0 -125
  39. docs/source/_static/images/anlz_pwav.svg +0 -130
  40. docs/source/_static/images/anlz_pwv3.svg +0 -139
  41. docs/source/_static/images/anlz_pwv4.svg +0 -139
  42. docs/source/_static/images/anlz_pwv5.svg +0 -139
  43. docs/source/_static/images/anlz_pwv5_entry.svg +0 -100
  44. docs/source/_static/images/anlz_pwv6.svg +0 -130
  45. docs/source/_static/images/anlz_pwv7.svg +0 -139
  46. docs/source/_static/images/anlz_pwvc.svg +0 -125
  47. docs/source/_static/images/anlz_tag.svg +0 -110
  48. docs/source/_static/images/x64dbg_rb_key.png +0 -0
  49. docs/source/_static/logos/dark/logo_primary.svg +0 -75
  50. docs/source/_static/logos/light/logo_primary.svg +0 -75
  51. docs/source/_static/logos/mid/logo_primary.svg +0 -75
  52. docs/source/_templates/apidoc/module.rst_t +0 -8
  53. docs/source/_templates/apidoc/package.rst_t +0 -57
  54. docs/source/_templates/apidoc/toc.rst_t +0 -7
  55. docs/source/_templates/autosummary/class.rst +0 -32
  56. docs/source/_templates/autosummary/module.rst +0 -55
  57. docs/source/api.md +0 -18
  58. docs/source/conf.py +0 -178
  59. docs/source/development/changes.md +0 -3
  60. docs/source/development/contributing.md +0 -3
  61. docs/source/formats/anlz.md +0 -634
  62. docs/source/formats/db6.md +0 -1233
  63. docs/source/formats/mysetting.md +0 -392
  64. docs/source/formats/xml.md +0 -376
  65. docs/source/index.md +0 -103
  66. docs/source/installation.md +0 -271
  67. docs/source/key.md +0 -103
  68. docs/source/quickstart.md +0 -185
  69. docs/source/requirements.txt +0 -7
  70. docs/source/tutorial/anlz.md +0 -7
  71. docs/source/tutorial/configuration.md +0 -66
  72. docs/source/tutorial/db6.md +0 -178
  73. docs/source/tutorial/index.md +0 -20
  74. docs/source/tutorial/mysetting.md +0 -124
  75. docs/source/tutorial/xml.md +0 -140
  76. pyrekordbox/db6/smart_playlist.py +0 -333
  77. pyrekordbox/xml.py +0 -8
  78. pyrekordbox-0.3.1.dist-info/RECORD +0 -84
  79. tests/__init__.py +0 -3
  80. tests/test_anlz.py +0 -206
  81. tests/test_config.py +0 -175
  82. tests/test_db6.py +0 -1115
  83. tests/test_mysetting.py +0 -203
  84. tests/test_xml.py +0 -629
@@ -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
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
18
- from .registry import RekordboxAgentRegistry
19
- from .aux_files import MasterPlaylistXml
20
- from .tables import DjmdContent
21
- from .smart_playlist import SmartList
19
+ from ..utils import get_rekordbox_pid
22
20
  from . import tables
21
+ from .aux_files import MasterPlaylistXml
22
+ from .registry import RekordboxAgentRegistry
23
+ from .smartlist import SmartList
24
+ from .tables import DjmdContent, FileType, PlaylistType
23
25
 
24
26
  try:
25
27
  from sqlcipher3 import dbapi2 as sqlite3 # noqa
@@ -39,101 +41,6 @@ class NoCachedKey(Exception):
39
41
  pass
40
42
 
41
43
 
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
44
  def _parse_query_result(query, kwargs):
138
45
  if "ID" in kwargs or "registry_id" in kwargs:
139
46
  try:
@@ -193,17 +100,23 @@ class Rekordbox6Database:
193
100
  """
194
101
 
195
102
  def __init__(self, path=None, db_dir="", key="", unlock=True):
196
- rb6_config = get_config("rekordbox6")
103
+ # get config of latest supported version
104
+ rb_config = get_config("rekordbox7")
105
+ if not rb_config:
106
+ rb_config = get_config("rekordbox6")
107
+
197
108
  pid = get_rekordbox_pid()
198
109
  if pid:
199
110
  logger.warning("Rekordbox is running!")
200
111
 
201
112
  if not path:
202
113
  # Get path from the RB config
203
- path = rb6_config.get("db_path", "")
114
+ path = rb_config.get("db_path", "")
204
115
  if not path:
205
116
  pdir = get_config("pioneer", "install_dir")
206
- raise FileNotFoundError(f"No Rekordbox v6 directory found in '{pdir}'")
117
+ raise FileNotFoundError(
118
+ f"No Rekordbox v6/v7 directory found in '{pdir}'"
119
+ )
207
120
  path = Path(path)
208
121
  # make sure file exists
209
122
  if not path.exists():
@@ -216,7 +129,7 @@ class Rekordbox6Database:
216
129
  )
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)
@@ -724,32 +637,13 @@ class Rekordbox6Database:
724
637
  smartlist.parse(playlist.SmartList)
725
638
  filter_clause = smartlist.filter_clause()
726
639
  else:
727
- items = list()
728
- for song in playlist.Songs:
729
- items.append(DjmdContent.ID == song.ContentID)
730
- filter_clause = or_(*items)
640
+ sub_query = self.query(tables.DjmdSongPlaylist.ContentID).filter(
641
+ tables.DjmdSongPlaylist.PlaylistID == playlist.ID
642
+ )
643
+ filter_clause = DjmdContent.ID.in_(select(sub_query.subquery()))
731
644
 
732
645
  return self.query(*entities).filter(filter_clause)
733
646
 
734
- def get_smartlist_content(self, playlist):
735
- """Creates a filtered query for the contents in smart palylists.
736
-
737
- Parameters
738
- ----------
739
- playlist : DjmdPlaylist or int or str
740
- The playlist instance. Can either be a :class:`DjmdPlaylist`
741
- object or a playlist ID.
742
- """
743
- if isinstance(playlist, (int, str)):
744
- playlist = self.get_playlist(ID=playlist)
745
- if not playlist.is_smart_playlist:
746
- raise ValueError(f"Playlist {playlist} is not a smart playlist.")
747
-
748
- smartlist = SmartList()
749
- smartlist.parse(playlist.SmartList)
750
- clause_list = smartlist.filter_clause()
751
- return self.query(DjmdContent).filter(clause_list)
752
-
753
647
  def get_property(self, **kwargs):
754
648
  """Creates a filtered query for the ``DjmdProperty`` table."""
755
649
  query = self.query(tables.DjmdProperty).filter_by(**kwargs)
@@ -832,7 +726,9 @@ class Rekordbox6Database:
832
726
 
833
727
  # -- Database updates --------------------------------------------------------------
834
728
 
835
- def generate_unused_id(self, table, is_28_bit: bool = True) -> int:
729
+ def generate_unused_id(
730
+ self, table, is_28_bit: bool = True, id_field_name: str = "ID"
731
+ ) -> int:
836
732
  """Generates an unused ID for the given table."""
837
733
  max_tries = 1000000
838
734
  for _ in range(max_tries):
@@ -844,7 +740,8 @@ class Rekordbox6Database:
844
740
  if id_ < 100:
845
741
  continue
846
742
  # Check if ID is already used
847
- query = self.query(table.ID).filter(table.ID == id_)
743
+ id_field = getattr(table, id_field_name)
744
+ query = self.query(id_field).filter(id_field == id_)
848
745
  used = self.query(query.exists()).scalar()
849
746
  if not used:
850
747
  return id_
@@ -1129,7 +1026,14 @@ class Rekordbox6Database:
1129
1026
  table = tables.DjmdPlaylist
1130
1027
  id_ = str(self.generate_unused_id(table, is_28_bit=True))
1131
1028
  uuid = str(uuid4())
1029
+ attribute = int(attribute)
1132
1030
  now = datetime.datetime.now()
1031
+ if smart_list is not None:
1032
+ # Set the playlist ID in the smart list and generate XML
1033
+ smart_list.playlist_id = id_
1034
+ smart_list_xml = smart_list.to_xml()
1035
+ else:
1036
+ smart_list_xml = None
1133
1037
 
1134
1038
  if parent is None:
1135
1039
  # If no parent is given, use root playlist
@@ -1169,7 +1073,7 @@ class Rekordbox6Database:
1169
1073
  logger.debug("Parent ID: %s", parent_id)
1170
1074
  logger.debug("Seq: %s", seq)
1171
1075
  logger.debug("Attribute: %s", attribute)
1172
- logger.debug("Smart List: %s", smart_list)
1076
+ logger.debug("Smart List: %s", smart_list_xml)
1173
1077
  logger.debug("Image Path: %s", image_path)
1174
1078
 
1175
1079
  # Update seq numbers higher than the new seq number
@@ -1192,7 +1096,7 @@ class Rekordbox6Database:
1192
1096
  ImagePath=image_path,
1193
1097
  Attribute=attribute,
1194
1098
  ParentID=parent_id,
1195
- SmartList=smart_list,
1099
+ SmartList=smart_list_xml,
1196
1100
  UUID=uuid,
1197
1101
  created_at=now,
1198
1102
  updated_at=now,
@@ -1253,7 +1157,9 @@ class Rekordbox6Database:
1253
1157
  '123456'
1254
1158
  """
1255
1159
  logger.info("Creating playlist %s", name)
1256
- return self._create_playlist(name, seq, image_path, parent, attribute=0)
1160
+ return self._create_playlist(
1161
+ name, seq, image_path, parent, attribute=PlaylistType.PLAYLIST
1162
+ )
1257
1163
 
1258
1164
  def create_playlist_folder(self, name, parent=None, seq=None, image_path=None):
1259
1165
  """Creates a new playlist folder in the database.
@@ -1293,7 +1199,56 @@ class Rekordbox6Database:
1293
1199
  '123456'
1294
1200
  """
1295
1201
  logger.info("Creating playlist folder %s", name)
1296
- return self._create_playlist(name, seq, image_path, parent, attribute=1)
1202
+ return self._create_playlist(
1203
+ name, seq, image_path, parent, attribute=PlaylistType.FOLDER
1204
+ )
1205
+
1206
+ def create_smart_playlist(
1207
+ self, name, smart_list: SmartList, parent=None, seq=None, image_path=None
1208
+ ):
1209
+ """Creates a new smart playlist in the database.
1210
+
1211
+ Parameters
1212
+ ----------
1213
+ name : str
1214
+ The name of the new smart playlist.
1215
+ smart_list : SmartList
1216
+ The smart list conditions to use for the new playlist.
1217
+ parent : DjmdPlaylist or int or str, optional
1218
+ The parent playlist of the new playlist. If not given, the playlist will be
1219
+ added to the root playlist. Can either be a :class:`DjmdPlaylist` object or
1220
+ a playlist ID.
1221
+ seq : int, optional
1222
+ The sequence number of the new playlist. If not given, the playlist will be
1223
+ added at the end of the parent playlist.
1224
+ image_path : str, optional
1225
+ The path to the image file of the new playlist.
1226
+
1227
+ Returns
1228
+ -------
1229
+ playlist : DjmdPlaylist
1230
+ The newly created playlist.
1231
+
1232
+ Examples
1233
+ --------
1234
+ Create a new smart list which we will use for the new smart playlist:
1235
+
1236
+ >>> smart = SmartList(logical_operator=1) # ALL conditions must be met
1237
+ >>> smart.add_condition("genre", operator=1, value_left="House") # is House
1238
+
1239
+ Create a new smart playlist in the root playlist:
1240
+ >>> db = Rekordbox6Database()
1241
+ >>> pl = db.create_smart_playlist("My Smart Playlist", smart)
1242
+ >>> pl.ID
1243
+ '123456789'
1244
+
1245
+ >>> pl.SmartList[:72]
1246
+ '<NODE Id="123456789" LogicalOperator="1" AutomaticUpdate="1"><CONDITION '
1247
+ """
1248
+ logger.info("Creating smart playlist %s", name)
1249
+ return self._create_playlist(
1250
+ name, seq, image_path, parent, smart_list, PlaylistType.SMART_PLAYLIST
1251
+ )
1297
1252
 
1298
1253
  def delete_playlist(self, playlist):
1299
1254
  """Deletes a playlist or playlist folder from the database.
@@ -1662,13 +1617,16 @@ class Rekordbox6Database:
1662
1617
  artist = self.get_artist(ID=artist)
1663
1618
  artist = artist.ID
1664
1619
 
1620
+ id_ = self.generate_unused_id(tables.DjmdAlbum)
1621
+ uuid = str(uuid4())
1665
1622
  album = tables.DjmdAlbum.create(
1666
- ID=self.generate_unused_id(tables.DjmdAlbum),
1623
+ ID=id_,
1667
1624
  Name=name,
1668
1625
  AlbumArtistID=artist,
1669
1626
  ImagePath=image_path,
1670
1627
  Compilation=compilation,
1671
1628
  SearchStr=search_str,
1629
+ UUID=str(uuid),
1672
1630
  )
1673
1631
  self.add(album)
1674
1632
  self.flush()
@@ -1725,7 +1683,10 @@ class Rekordbox6Database:
1725
1683
  raise ValueError(f"Artist '{name}' already exists in database")
1726
1684
 
1727
1685
  id_ = self.generate_unused_id(tables.DjmdArtist)
1728
- artist = tables.DjmdArtist.create(ID=id_, Name=name, SearchStr=search_str)
1686
+ uuid = str(uuid4())
1687
+ artist = tables.DjmdArtist.create(
1688
+ ID=id_, Name=name, SearchStr=search_str, UUID=uuid
1689
+ )
1729
1690
  self.add(artist)
1730
1691
  self.flush()
1731
1692
  return artist
@@ -1774,7 +1735,8 @@ class Rekordbox6Database:
1774
1735
  raise ValueError(f"Genre '{name}' already exists in database")
1775
1736
 
1776
1737
  id_ = self.generate_unused_id(tables.DjmdGenre)
1777
- genre = tables.DjmdGenre.create(ID=id_, Name=name)
1738
+ uuid = str(uuid4())
1739
+ genre = tables.DjmdGenre.create(ID=id_, Name=name, UUID=uuid)
1778
1740
  self.add(genre)
1779
1741
  self.flush()
1780
1742
  return genre
@@ -1823,11 +1785,86 @@ class Rekordbox6Database:
1823
1785
  raise ValueError(f"Label '{name}' already exists in database")
1824
1786
 
1825
1787
  id_ = self.generate_unused_id(tables.DjmdLabel)
1826
- label = tables.DjmdLabel.create(ID=id_, Name=name)
1788
+ uuid = str(uuid4())
1789
+ label = tables.DjmdLabel.create(ID=id_, Name=name, UUID=uuid)
1827
1790
  self.add(label)
1828
1791
  self.flush()
1829
1792
  return label
1830
1793
 
1794
+ def add_content(self, path, **kwargs):
1795
+ """Adds a new track to the database.
1796
+
1797
+ Parameters
1798
+ ----------
1799
+ path : str
1800
+ Absolute path to the music file to be added.
1801
+
1802
+ **kwargs:
1803
+ Keyword arguments passed to DjmdContent on creation. These arguments
1804
+ should be a valid DjmdContent field.
1805
+
1806
+ Returns
1807
+ -------
1808
+ content : DjmdContent
1809
+ The newly created track.
1810
+
1811
+ Raises
1812
+ ------
1813
+ ValueError : If a track with the same path already exists in the database.
1814
+ ValueError : If the file type is invalid.
1815
+
1816
+ Examples
1817
+ --------
1818
+ Add a new track to the database:
1819
+
1820
+ >>> db = Rekordbox6Database()
1821
+ >>> db.add_content("/Users/foo/Downloads/banger.mp3", Title="Banger")
1822
+ <DjmdContent(123456789 Title=Banger)>
1823
+ """
1824
+ path = Path(path)
1825
+ path_string = str(path)
1826
+ query = self.query(tables.DjmdContent).filter_by(FolderPath=path_string)
1827
+ if query.count() > 0:
1828
+ raise ValueError(f"Track with path '{path}' already exists in database")
1829
+
1830
+ id_ = self.generate_unused_id(tables.DjmdContent)
1831
+ file_id = self.generate_unused_id(
1832
+ tables.DjmdContent, id_field_name="rb_file_id"
1833
+ )
1834
+ uuid = str(uuid4())
1835
+ content_link = self.get_menu_items(Name="TRACK").one()
1836
+ date_created = datetime.date.today()
1837
+ device = self.get_device().first()
1838
+ file_name_l = path.name
1839
+ file_size = path.stat().st_size
1840
+
1841
+ file_type_string = path.suffix.lstrip(".").upper()
1842
+ try:
1843
+ file_type = getattr(FileType, file_type_string)
1844
+ except ValueError:
1845
+ raise ValueError(f"Invalid file type: {path.suffix}")
1846
+
1847
+ content = tables.DjmdContent.create(
1848
+ ID=id_,
1849
+ UUID=uuid,
1850
+ ContentLink=content_link.rb_local_usn,
1851
+ DateCreated=date_created,
1852
+ DeviceID=device.ID,
1853
+ FileNameL=file_name_l,
1854
+ FileSize=file_size,
1855
+ FileType=file_type.value,
1856
+ FolderPath=path_string,
1857
+ HotCueAutoLoad="on",
1858
+ MasterDBID=device.MasterDBID,
1859
+ MasterSongID=id_,
1860
+ StockDate=date_created,
1861
+ rb_file_id=file_id,
1862
+ **kwargs,
1863
+ )
1864
+ self.add(content)
1865
+ self.flush()
1866
+ return content
1867
+
1831
1868
  # ----------------------------------------------------------------------------------
1832
1869
 
1833
1870
  def get_mysetting_paths(self):
@@ -1903,6 +1940,53 @@ class Rekordbox6Database:
1903
1940
  root = self.get_anlz_dir(content)
1904
1941
  return read_anlz_files(root)
1905
1942
 
1943
+ def get_anlz_path(self, content, type_):
1944
+ """Returns the file path of an ANLZ analysis file of a track.
1945
+
1946
+ Parameters
1947
+ ----------
1948
+ content : DjmdContent or int or str
1949
+ The content corresponding to a track in the Rekordbox v6 database.
1950
+ If an integer is passed the database is queried for the ``DjmdContent``
1951
+ entry.
1952
+ type_ : str, optional
1953
+ The type of the analysis file to return. Must be one of "DAT", "EXT" or
1954
+ "EX2". "DAT" by default.
1955
+
1956
+ Returns
1957
+ -------
1958
+ anlz_path : Path or None
1959
+ The file path of the analysis file for the content. If the file does not
1960
+ exist, None is returned.
1961
+ """
1962
+ root = self.get_anlz_dir(content)
1963
+ paths = get_anlz_paths(root)
1964
+ return paths.get(type_.upper(), "")
1965
+
1966
+ def read_anlz_file(self, content, type_):
1967
+ """Reads an ANLZ analysis file of a track.
1968
+
1969
+ Parameters
1970
+ ----------
1971
+ content : DjmdContent or int or str
1972
+ The content corresponding to a track in the Rekordbox v6 database.
1973
+ If an integer is passed the database is queried for the ``DjmdContent``
1974
+ entry.
1975
+ type_ : str, optional
1976
+ The type of the analysis file to return. Must be one of "DAT", "EXT" or
1977
+ "EX2". "DAT" by default.
1978
+
1979
+ Returns
1980
+ -------
1981
+ anlz_file : AnlzFile or None
1982
+ The analysis file for the content. If the file does not exist, None is
1983
+ returned.
1984
+ """
1985
+ path = self.get_anlz_path(content, type_)
1986
+ if path:
1987
+ return AnlzFile.parse_file(path)
1988
+ return None
1989
+
1906
1990
  def update_content_path(
1907
1991
  self, content, path, save=True, check_path=True, commit=True
1908
1992
  ):
@@ -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__)