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.
- pyrekordbox/__init__.py +8 -8
- pyrekordbox/__main__.py +3 -2
- pyrekordbox/_version.py +2 -2
- pyrekordbox/anlz/__init__.py +3 -2
- pyrekordbox/anlz/file.py +4 -2
- pyrekordbox/anlz/tags.py +3 -1
- pyrekordbox/config.py +79 -23
- pyrekordbox/db6/__init__.py +2 -2
- pyrekordbox/db6/aux_files.py +3 -2
- pyrekordbox/db6/database.py +227 -143
- pyrekordbox/db6/registry.py +1 -0
- pyrekordbox/db6/smartlist.py +375 -0
- pyrekordbox/db6/tables.py +81 -20
- pyrekordbox/logger.py +0 -1
- pyrekordbox/mysettings/__init__.py +5 -4
- pyrekordbox/mysettings/file.py +3 -1
- pyrekordbox/rbxml.py +5 -3
- pyrekordbox/utils.py +4 -3
- {pyrekordbox-0.3.1.dist-info → pyrekordbox-0.4.0.dist-info}/LICENSE +1 -1
- {pyrekordbox-0.3.1.dist-info → pyrekordbox-0.4.0.dist-info}/METADATA +26 -42
- pyrekordbox-0.4.0.dist-info/RECORD +25 -0
- {pyrekordbox-0.3.1.dist-info → pyrekordbox-0.4.0.dist-info}/WHEEL +1 -1
- {pyrekordbox-0.3.1.dist-info → pyrekordbox-0.4.0.dist-info}/top_level.txt +0 -2
- docs/Makefile +0 -20
- docs/make.bat +0 -35
- docs/source/_static/images/anlz_beat.svg +0 -53
- docs/source/_static/images/anlz_file.svg +0 -204
- docs/source/_static/images/anlz_pco2.svg +0 -138
- docs/source/_static/images/anlz_pcob.svg +0 -148
- docs/source/_static/images/anlz_pcp2.svg +0 -398
- docs/source/_static/images/anlz_pcpt.svg +0 -263
- docs/source/_static/images/anlz_ppth.svg +0 -123
- docs/source/_static/images/anlz_pqt2.svg +0 -324
- docs/source/_static/images/anlz_pqt2_2.svg +0 -253
- docs/source/_static/images/anlz_pqtz.svg +0 -140
- docs/source/_static/images/anlz_pssi.svg +0 -192
- docs/source/_static/images/anlz_pssi_entry.svg +0 -191
- docs/source/_static/images/anlz_pvbr.svg +0 -125
- docs/source/_static/images/anlz_pwav.svg +0 -130
- docs/source/_static/images/anlz_pwv3.svg +0 -139
- docs/source/_static/images/anlz_pwv4.svg +0 -139
- docs/source/_static/images/anlz_pwv5.svg +0 -139
- docs/source/_static/images/anlz_pwv5_entry.svg +0 -100
- docs/source/_static/images/anlz_pwv6.svg +0 -130
- docs/source/_static/images/anlz_pwv7.svg +0 -139
- docs/source/_static/images/anlz_pwvc.svg +0 -125
- docs/source/_static/images/anlz_tag.svg +0 -110
- docs/source/_static/images/x64dbg_rb_key.png +0 -0
- docs/source/_static/logos/dark/logo_primary.svg +0 -75
- docs/source/_static/logos/light/logo_primary.svg +0 -75
- docs/source/_static/logos/mid/logo_primary.svg +0 -75
- docs/source/_templates/apidoc/module.rst_t +0 -8
- docs/source/_templates/apidoc/package.rst_t +0 -57
- docs/source/_templates/apidoc/toc.rst_t +0 -7
- docs/source/_templates/autosummary/class.rst +0 -32
- docs/source/_templates/autosummary/module.rst +0 -55
- docs/source/api.md +0 -18
- docs/source/conf.py +0 -178
- docs/source/development/changes.md +0 -3
- docs/source/development/contributing.md +0 -3
- docs/source/formats/anlz.md +0 -634
- docs/source/formats/db6.md +0 -1233
- docs/source/formats/mysetting.md +0 -392
- docs/source/formats/xml.md +0 -376
- docs/source/index.md +0 -103
- docs/source/installation.md +0 -271
- docs/source/key.md +0 -103
- docs/source/quickstart.md +0 -185
- docs/source/requirements.txt +0 -7
- docs/source/tutorial/anlz.md +0 -7
- docs/source/tutorial/configuration.md +0 -66
- docs/source/tutorial/db6.md +0 -178
- docs/source/tutorial/index.md +0 -20
- docs/source/tutorial/mysetting.md +0 -124
- docs/source/tutorial/xml.md +0 -140
- pyrekordbox/db6/smart_playlist.py +0 -333
- pyrekordbox/xml.py +0 -8
- pyrekordbox-0.3.1.dist-info/RECORD +0 -84
- tests/__init__.py +0 -3
- tests/test_anlz.py +0 -206
- tests/test_config.py +0 -175
- tests/test_db6.py +0 -1115
- tests/test_mysetting.py +0 -203
- tests/test_xml.py +0 -629
pyrekordbox/db6/database.py
CHANGED
@@ -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
|
12
|
-
|
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
|
-
|
16
|
+
|
17
|
+
from ..anlz import AnlzFile, get_anlz_paths, read_anlz_files
|
16
18
|
from ..config import get_config
|
17
|
-
from ..
|
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
|
-
|
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 =
|
114
|
+
path = rb_config.get("db_path", "")
|
204
115
|
if not path:
|
205
116
|
pdir = get_config("pioneer", "install_dir")
|
206
|
-
raise FileNotFoundError(
|
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 =
|
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.
|
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
|
-
|
728
|
-
|
729
|
-
|
730
|
-
filter_clause =
|
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(
|
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
|
-
|
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",
|
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=
|
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(
|
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(
|
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=
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
):
|