pyrekordbox 0.4.2__py3-none-any.whl → 0.4.4__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pyrekordbox/__init__.py +1 -0
- pyrekordbox/__main__.py +1 -51
- pyrekordbox/_version.py +16 -3
- pyrekordbox/anlz/__init__.py +6 -6
- pyrekordbox/anlz/file.py +56 -43
- pyrekordbox/anlz/tags.py +108 -70
- pyrekordbox/config.py +18 -356
- pyrekordbox/db6/aux_files.py +40 -14
- pyrekordbox/db6/database.py +384 -236
- pyrekordbox/db6/registry.py +48 -34
- pyrekordbox/db6/smartlist.py +12 -12
- pyrekordbox/db6/tables.py +60 -58
- pyrekordbox/mysettings/__init__.py +3 -2
- pyrekordbox/mysettings/file.py +27 -24
- pyrekordbox/rbxml.py +321 -142
- pyrekordbox/utils.py +27 -6
- {pyrekordbox-0.4.2.dist-info → pyrekordbox-0.4.4.dist-info}/METADATA +13 -38
- pyrekordbox-0.4.4.dist-info/RECORD +25 -0
- {pyrekordbox-0.4.2.dist-info → pyrekordbox-0.4.4.dist-info}/WHEEL +1 -1
- {pyrekordbox-0.4.2.dist-info → pyrekordbox-0.4.4.dist-info}/licenses/LICENSE +1 -1
- pyrekordbox-0.4.2.dist-info/RECORD +0 -25
- {pyrekordbox-0.4.2.dist-info → pyrekordbox-0.4.4.dist-info}/top_level.txt +0 -0
pyrekordbox/db6/tables.py
CHANGED
@@ -7,15 +7,15 @@
|
|
7
7
|
import math
|
8
8
|
import re
|
9
9
|
import struct
|
10
|
-
from datetime import datetime
|
10
|
+
from datetime import datetime, timezone
|
11
11
|
from enum import IntEnum
|
12
|
-
from typing import List
|
12
|
+
from typing import Any, Dict, Iterator, List, Optional, Tuple
|
13
13
|
|
14
14
|
import numpy as np
|
15
15
|
from sqlalchemy import (
|
16
16
|
VARCHAR,
|
17
17
|
BigInteger,
|
18
|
-
|
18
|
+
Dialect,
|
19
19
|
Float,
|
20
20
|
ForeignKey,
|
21
21
|
Integer,
|
@@ -71,6 +71,7 @@ __all__ = [
|
|
71
71
|
"ImageFile",
|
72
72
|
"SettingFile",
|
73
73
|
"UuidIDMap",
|
74
|
+
"FileType",
|
74
75
|
]
|
75
76
|
|
76
77
|
|
@@ -116,37 +117,38 @@ TABLES = [
|
|
116
117
|
|
117
118
|
|
118
119
|
def datetime_to_str(value: datetime) -> str:
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
else:
|
125
|
-
s = s[:-3] + " +00:00"
|
120
|
+
# Convert to UTC timezone string
|
121
|
+
s = value.astimezone(timezone.utc).isoformat().replace("T", " ")
|
122
|
+
# Get the timezone info (last 6 characters of the string)
|
123
|
+
tzinfo = s[-6:]
|
124
|
+
s = s[:-9] + " " + tzinfo
|
126
125
|
return s
|
127
126
|
|
128
127
|
|
129
128
|
def string_to_datetime(value: str) -> datetime:
|
130
129
|
try:
|
130
|
+
# Normalize 'Z' (Zulu/UTC) to '+00:00' for fromisoformat compatibility
|
131
|
+
if value.endswith("Z"):
|
132
|
+
value = value[:-1] + "+00:00"
|
131
133
|
dt = datetime.fromisoformat(value)
|
132
134
|
except ValueError:
|
133
135
|
if len(value.strip()) > 23:
|
134
|
-
#
|
135
|
-
# "2025-04-12 19:11:29.274 -05:00" or
|
136
|
+
# Handles formats like:
|
136
137
|
# "2025-04-12 19:11:29.274 -05:00 (Central Daylight Time)"
|
137
138
|
datestr, tzinfo = value[:23], value[23:30]
|
138
139
|
datestr = datestr.strip()
|
139
140
|
tzinfo = tzinfo.strip()
|
140
|
-
|
141
|
-
|
141
|
+
if tzinfo == "Z":
|
142
|
+
tzinfo = "+00:00"
|
143
|
+
assert re.match(r"^[+-]?\d{1,2}:\d{2}", tzinfo), f"Invalid tzinfo: {tzinfo}"
|
144
|
+
datestr = datestr + tzinfo
|
142
145
|
else:
|
143
|
-
datestr
|
146
|
+
datestr = value.strip()
|
144
147
|
dt = datetime.fromisoformat(datestr)
|
145
|
-
# Convert to local timezone and return without timezone
|
146
148
|
return dt.astimezone().replace(tzinfo=None)
|
147
149
|
|
148
150
|
|
149
|
-
class DateTime(TypeDecorator):
|
151
|
+
class DateTime(TypeDecorator): # type: ignore[type-arg]
|
150
152
|
"""Custom datetime column with timezone support.
|
151
153
|
|
152
154
|
The datetime format in the database is `YYYY-MM-DD HH:MM:SS.SSS +00:00`.
|
@@ -156,10 +158,10 @@ class DateTime(TypeDecorator):
|
|
156
158
|
impl = Text
|
157
159
|
cache_ok = True
|
158
160
|
|
159
|
-
def process_bind_param(self, value: datetime, dialect) -> str:
|
161
|
+
def process_bind_param(self, value: datetime, dialect: Dialect) -> str: # type: ignore[override]
|
160
162
|
return datetime_to_str(value)
|
161
163
|
|
162
|
-
def process_result_value(self, value: str, dialect):
|
164
|
+
def process_result_value(self, value: str, dialect: Dialect) -> Optional[datetime]: # type: ignore[override]
|
163
165
|
if value:
|
164
166
|
return string_to_datetime(value)
|
165
167
|
return None
|
@@ -190,65 +192,65 @@ class Base(DeclarativeBase):
|
|
190
192
|
__keys__: List[str] = []
|
191
193
|
|
192
194
|
@classmethod
|
193
|
-
def create(cls, **kwargs):
|
195
|
+
def create(cls, **kwargs: Any): # type: ignore # noqa: ANN206
|
194
196
|
with RekordboxAgentRegistry.disabled():
|
195
197
|
# noinspection PyArgumentList
|
196
198
|
self = cls(**kwargs)
|
197
199
|
return self
|
198
200
|
|
199
201
|
@classmethod
|
200
|
-
def columns(cls):
|
202
|
+
def columns(cls) -> List[str]:
|
201
203
|
"""Returns a list of all column names without the relationships."""
|
202
204
|
return [column.name for column in inspect(cls).c]
|
203
205
|
|
204
206
|
@classmethod
|
205
|
-
def relationships(cls):
|
207
|
+
def relationships(cls) -> List[str]:
|
206
208
|
"""Returns a list of all relationship names."""
|
207
209
|
return [column.key for column in inspect(cls).relationships] # noqa
|
208
210
|
|
209
211
|
@classmethod
|
210
|
-
def __get_keys__(cls): # pragma: no cover
|
212
|
+
def __get_keys__(cls) -> List[str]: # pragma: no cover
|
211
213
|
"""Get all attributes of the table."""
|
212
214
|
items = cls.__dict__.items()
|
213
215
|
keys = [k for k, v in items if not callable(v) and not k.startswith("_")]
|
214
216
|
return keys
|
215
217
|
|
216
218
|
@classmethod
|
217
|
-
def keys(cls): # pragma: no cover
|
219
|
+
def keys(cls) -> List[str]: # pragma: no cover
|
218
220
|
"""Returns a list of all column names including the relationships."""
|
219
221
|
if not cls.__keys__: # Cache the keys
|
220
222
|
cls.__keys__ = cls.__get_keys__()
|
221
223
|
return cls.__keys__
|
222
224
|
|
223
|
-
def __iter__(self):
|
225
|
+
def __iter__(self) -> Iterator[str]:
|
224
226
|
"""Iterates over all columns and relationship names."""
|
225
227
|
return iter(self.keys())
|
226
228
|
|
227
|
-
def __len__(self):
|
229
|
+
def __len__(self) -> int:
|
228
230
|
return sum(1 for _ in self.__iter__())
|
229
231
|
|
230
|
-
def __getitem__(self, item):
|
232
|
+
def __getitem__(self, item: str) -> Any:
|
231
233
|
return self.__getattribute__(item)
|
232
234
|
|
233
235
|
# noinspection PyUnresolvedReferences
|
234
|
-
def __setattr__(self, key, value):
|
236
|
+
def __setattr__(self, key: str, value: Any) -> None:
|
235
237
|
if not key.startswith("_"):
|
236
238
|
RekordboxAgentRegistry.on_update(self, key, value)
|
237
239
|
super().__setattr__(key, value)
|
238
240
|
|
239
|
-
def values(self):
|
241
|
+
def values(self) -> List[Any]:
|
240
242
|
"""Returns a list of all column values including the relationships."""
|
241
243
|
return [self.__getitem__(key) for key in self.keys()]
|
242
244
|
|
243
|
-
def items(self):
|
245
|
+
def items(self) -> Iterator[Tuple[str, Any]]:
|
244
246
|
for key in self.__iter__():
|
245
247
|
yield key, self.__getitem__(key)
|
246
248
|
|
247
|
-
def to_dict(self):
|
249
|
+
def to_dict(self) -> Dict[str, Any]:
|
248
250
|
"""Returns a dictionary of all column names and values."""
|
249
251
|
return {key: self.__getitem__(key) for key in self.columns()}
|
250
252
|
|
251
|
-
def pformat(self, indent=" "): # pragma: no cover
|
253
|
+
def pformat(self, indent: str = " ") -> str: # pragma: no cover
|
252
254
|
lines = [f"{self.__tablename__}"]
|
253
255
|
columns = self.columns()
|
254
256
|
w = max(len(col) for col in columns)
|
@@ -271,7 +273,7 @@ class StatsTime:
|
|
271
273
|
class StatsFull:
|
272
274
|
"""Mixin class for tables that use all statistics columns."""
|
273
275
|
|
274
|
-
ID:
|
276
|
+
ID: Mapped[str]
|
275
277
|
"""The ID (primary key) of the table entry."""
|
276
278
|
|
277
279
|
UUID: Mapped[str] = mapped_column(VARCHAR(255), default=None)
|
@@ -296,7 +298,7 @@ class StatsFull:
|
|
296
298
|
)
|
297
299
|
"""The last update date of the table entry (from :class:`StatsFull`)."""
|
298
300
|
|
299
|
-
def __repr__(self):
|
301
|
+
def __repr__(self) -> str:
|
300
302
|
return f"<{self.__class__.__name__}({self.ID})>"
|
301
303
|
|
302
304
|
|
@@ -525,7 +527,7 @@ class DjmdAlbum(Base, StatsFull):
|
|
525
527
|
AlbumArtistName = association_proxy("AlbumArtist", "Name")
|
526
528
|
"""The name of the album artist (:class:`DjmdArtist`) of the track."""
|
527
529
|
|
528
|
-
def __repr__(self):
|
530
|
+
def __repr__(self) -> str:
|
529
531
|
s = f"{self.ID: <10} Name={self.Name}"
|
530
532
|
return f"<{self.__class__.__name__}({s})>"
|
531
533
|
|
@@ -542,7 +544,7 @@ class DjmdArtist(Base, StatsFull):
|
|
542
544
|
SearchStr: Mapped[str] = mapped_column(VARCHAR(255), default=None)
|
543
545
|
"""The search string of the artist."""
|
544
546
|
|
545
|
-
def __repr__(self):
|
547
|
+
def __repr__(self) -> str:
|
546
548
|
s = f"{self.ID: <10} Name={self.Name}"
|
547
549
|
return f"<{self.__class__.__name__}({s})>"
|
548
550
|
|
@@ -588,7 +590,7 @@ class DjmdColor(Base, StatsFull):
|
|
588
590
|
Commnt: Mapped[str] = mapped_column(VARCHAR(255), default=None)
|
589
591
|
"""The comment (name) of the color."""
|
590
592
|
|
591
|
-
def __repr__(self):
|
593
|
+
def __repr__(self) -> str:
|
592
594
|
s = f"{self.ID: <2} Comment={self.Commnt}"
|
593
595
|
return f"<{self.__class__.__name__}({s})>"
|
594
596
|
|
@@ -807,7 +809,7 @@ class DjmdContent(Base, StatsFull):
|
|
807
809
|
MyTagIDs = association_proxy("MyTags", "MyTagID")
|
808
810
|
"""The IDs of the my tags (:class:`DjmdSongMyTag`) of the track."""
|
809
811
|
|
810
|
-
def __repr__(self):
|
812
|
+
def __repr__(self) -> str:
|
811
813
|
s = f"{self.ID: <10} Title={self.Title}"
|
812
814
|
return f"<{self.__class__.__name__}({s})>"
|
813
815
|
|
@@ -869,11 +871,11 @@ class DjmdCue(Base, StatsFull):
|
|
869
871
|
"""The content entry of the cue point (links to :class:`DjmdContent`)."""
|
870
872
|
|
871
873
|
@property
|
872
|
-
def is_memory_cue(self):
|
874
|
+
def is_memory_cue(self) -> bool:
|
873
875
|
return self.Kind == 0
|
874
876
|
|
875
877
|
@property
|
876
|
-
def is_hot_cue(self):
|
878
|
+
def is_hot_cue(self) -> bool:
|
877
879
|
return self.Kind > 0
|
878
880
|
|
879
881
|
|
@@ -889,7 +891,7 @@ class DjmdDevice(Base, StatsFull):
|
|
889
891
|
Name: Mapped[str] = mapped_column(VARCHAR(255), default=None)
|
890
892
|
"""The name of the device."""
|
891
893
|
|
892
|
-
def __repr__(self):
|
894
|
+
def __repr__(self) -> str:
|
893
895
|
s = f"{self.ID: <2} Name={self.Name}"
|
894
896
|
return f"<{self.__class__.__name__}({s})>"
|
895
897
|
|
@@ -904,7 +906,7 @@ class DjmdGenre(Base, StatsFull):
|
|
904
906
|
Name: Mapped[str] = mapped_column(VARCHAR(255), default=None)
|
905
907
|
"""The name of the genre."""
|
906
908
|
|
907
|
-
def __repr__(self):
|
909
|
+
def __repr__(self) -> str:
|
908
910
|
s = f"{self.ID: <2} Name={self.Name}"
|
909
911
|
return f"<{self.__class__.__name__}({s})>"
|
910
912
|
|
@@ -943,7 +945,7 @@ class DjmdHistory(Base, StatsFull):
|
|
943
945
|
Backrefs to the parent history playlist via :attr:`Parent`.
|
944
946
|
"""
|
945
947
|
|
946
|
-
def __repr__(self):
|
948
|
+
def __repr__(self) -> str:
|
947
949
|
s = f"{self.ID: <2} Name={self.Name}"
|
948
950
|
return f"<{self.__class__.__name__}({s})>"
|
949
951
|
|
@@ -1007,7 +1009,7 @@ class DjmdHotCueBanklist(Base, StatsFull):
|
|
1007
1009
|
Backrefs to the parent hot-cue banklist via :attr:`Parent`.
|
1008
1010
|
"""
|
1009
1011
|
|
1010
|
-
def __repr__(self):
|
1012
|
+
def __repr__(self) -> str:
|
1011
1013
|
s = f"{self.ID: <2} Name={self.Name}"
|
1012
1014
|
return f"<{self.__class__.__name__}({s})>"
|
1013
1015
|
|
@@ -1087,7 +1089,7 @@ class DjmdKey(Base, StatsFull):
|
|
1087
1089
|
Seq: Mapped[int] = mapped_column(Integer, default=None)
|
1088
1090
|
"""The sequence of the key (for ordering)."""
|
1089
1091
|
|
1090
|
-
def __repr__(self):
|
1092
|
+
def __repr__(self) -> str:
|
1091
1093
|
s = f"{self.ID: <2} Name={self.ScaleName}"
|
1092
1094
|
return f"<{self.__class__.__name__}({s})>"
|
1093
1095
|
|
@@ -1102,7 +1104,7 @@ class DjmdLabel(Base, StatsFull):
|
|
1102
1104
|
Name: Mapped[str] = mapped_column(VARCHAR(255), default=None)
|
1103
1105
|
"""The name of the label."""
|
1104
1106
|
|
1105
|
-
def __repr__(self):
|
1107
|
+
def __repr__(self) -> str:
|
1106
1108
|
s = f"{self.ID: <2} Name={self.Name}"
|
1107
1109
|
return f"<{self.__class__.__name__}({s})>"
|
1108
1110
|
|
@@ -1119,7 +1121,7 @@ class DjmdMenuItems(Base, StatsFull):
|
|
1119
1121
|
Name: Mapped[str] = mapped_column(VARCHAR(255), default=None)
|
1120
1122
|
"""The name of the menu item."""
|
1121
1123
|
|
1122
|
-
def __repr__(self):
|
1124
|
+
def __repr__(self) -> str:
|
1123
1125
|
s = f"{self.ID: <2} Name={self.Name}"
|
1124
1126
|
return f"<{self.__class__.__name__}({s})>"
|
1125
1127
|
|
@@ -1151,7 +1153,7 @@ class DjmdMixerParam(Base, StatsFull):
|
|
1151
1153
|
"""The content this mixer parameters belong to (links to :class:`DjmdContent`)."""
|
1152
1154
|
|
1153
1155
|
@staticmethod
|
1154
|
-
def _get_db(low, high):
|
1156
|
+
def _get_db(low: int, high: int) -> float:
|
1155
1157
|
integer = (high << 16) | low
|
1156
1158
|
byte_data = integer.to_bytes(4, byteorder="big")
|
1157
1159
|
factor = struct.unpack("!f", byte_data)[0]
|
@@ -1160,7 +1162,7 @@ class DjmdMixerParam(Base, StatsFull):
|
|
1160
1162
|
return 20 * math.log10(factor)
|
1161
1163
|
|
1162
1164
|
@staticmethod
|
1163
|
-
def _set_db(value):
|
1165
|
+
def _set_db(value: float) -> Tuple[int, int]:
|
1164
1166
|
if value == -np.inf:
|
1165
1167
|
return 0, 0
|
1166
1168
|
factor = 10 ** (value / 20)
|
@@ -1184,7 +1186,7 @@ class DjmdMixerParam(Base, StatsFull):
|
|
1184
1186
|
self.GainLow, self.GainHigh = self._set_db(value)
|
1185
1187
|
|
1186
1188
|
@property
|
1187
|
-
def Peak(self):
|
1189
|
+
def Peak(self) -> float:
|
1188
1190
|
"""The peak amplitude of a track in dB.
|
1189
1191
|
|
1190
1192
|
This value is computed from the low and high peak values.
|
@@ -1193,7 +1195,7 @@ class DjmdMixerParam(Base, StatsFull):
|
|
1193
1195
|
return self._get_db(self.PeakLow, self.PeakHigh)
|
1194
1196
|
|
1195
1197
|
@Peak.setter
|
1196
|
-
def Peak(self, value):
|
1198
|
+
def Peak(self, value: float) -> None:
|
1197
1199
|
self.PeakLow, self.PeakHigh = self._set_db(value)
|
1198
1200
|
|
1199
1201
|
|
@@ -1229,7 +1231,7 @@ class DjmdMyTag(Base, StatsFull):
|
|
1229
1231
|
Backrefs to the parent list via :attr:`Parent`.
|
1230
1232
|
"""
|
1231
1233
|
|
1232
|
-
def __repr__(self):
|
1234
|
+
def __repr__(self) -> str:
|
1233
1235
|
s = f"{self.ID: <2} Name={self.Name}"
|
1234
1236
|
return f"<{self.__class__.__name__}({s})>"
|
1235
1237
|
|
@@ -1300,18 +1302,18 @@ class DjmdPlaylist(Base, StatsFull):
|
|
1300
1302
|
"""
|
1301
1303
|
|
1302
1304
|
@property
|
1303
|
-
def is_playlist(self):
|
1305
|
+
def is_playlist(self) -> bool:
|
1304
1306
|
return self.Attribute == PlaylistType.PLAYLIST
|
1305
1307
|
|
1306
1308
|
@property
|
1307
|
-
def is_folder(self):
|
1309
|
+
def is_folder(self) -> bool:
|
1308
1310
|
return self.Attribute == PlaylistType.FOLDER
|
1309
1311
|
|
1310
1312
|
@property
|
1311
|
-
def is_smart_playlist(self):
|
1313
|
+
def is_smart_playlist(self) -> bool:
|
1312
1314
|
return self.Attribute == PlaylistType.SMART_PLAYLIST
|
1313
1315
|
|
1314
|
-
def __repr__(self):
|
1316
|
+
def __repr__(self) -> str:
|
1315
1317
|
s = f"{self.ID: <2} Name={self.Name}"
|
1316
1318
|
return f"<{self.__class__.__name__}({s})>"
|
1317
1319
|
|
@@ -1381,7 +1383,7 @@ class DjmdRelatedTracks(Base, StatsFull):
|
|
1381
1383
|
Backrefs to the parent related tracks list via :attr:`Parent`.
|
1382
1384
|
"""
|
1383
1385
|
|
1384
|
-
def __repr__(self):
|
1386
|
+
def __repr__(self) -> str:
|
1385
1387
|
s = f"{self.ID: <2} Name={self.Name}"
|
1386
1388
|
return f"<{self.__class__.__name__}({s})>"
|
1387
1389
|
|
@@ -1446,7 +1448,7 @@ class DjmdSampler(Base, StatsFull):
|
|
1446
1448
|
Backrefs to the parent sampler list via :attr:`Parent`.
|
1447
1449
|
"""
|
1448
1450
|
|
1449
|
-
def __repr__(self):
|
1451
|
+
def __repr__(self) -> str:
|
1450
1452
|
s = f"{self.ID: <2} Name={self.Name}"
|
1451
1453
|
return f"<{self.__class__.__name__}({s})>"
|
1452
1454
|
|
@@ -4,6 +4,7 @@
|
|
4
4
|
|
5
5
|
import re
|
6
6
|
from pathlib import Path
|
7
|
+
from typing import List, Union
|
7
8
|
|
8
9
|
from . import structs
|
9
10
|
from .file import (
|
@@ -18,7 +19,7 @@ from .file import (
|
|
18
19
|
RE_MYSETTING = re.compile(".*SETTING[0-9]?.DAT$")
|
19
20
|
|
20
21
|
|
21
|
-
def get_mysetting_paths(root, deep=False):
|
22
|
+
def get_mysetting_paths(root: Union[str, Path], deep: bool = False) -> List[Path]:
|
22
23
|
files = list()
|
23
24
|
root = Path(root)
|
24
25
|
iteator = root.rglob("*") if deep else root.iterdir()
|
@@ -28,6 +29,6 @@ def get_mysetting_paths(root, deep=False):
|
|
28
29
|
return files
|
29
30
|
|
30
31
|
|
31
|
-
def read_mysetting_file(path) -> SettingsFile:
|
32
|
+
def read_mysetting_file(path: Union[str, Path]) -> SettingsFile:
|
32
33
|
obj = FILES[str(Path(path).name)]
|
33
34
|
return obj.parse_file(path)
|
pyrekordbox/mysettings/file.py
CHANGED
@@ -6,6 +6,8 @@
|
|
6
6
|
|
7
7
|
import re
|
8
8
|
from collections.abc import MutableMapping
|
9
|
+
from pathlib import Path
|
10
|
+
from typing import Any, Dict, Iterator, Type, Union
|
9
11
|
|
10
12
|
from construct import Struct
|
11
13
|
|
@@ -51,7 +53,7 @@ CRC16_XMODEM_TABLE = [
|
|
51
53
|
RE_INVALID_KEY = re.compile("[_u][0-9]?", flags=re.IGNORECASE)
|
52
54
|
|
53
55
|
|
54
|
-
def compute_checksum(data, struct):
|
56
|
+
def compute_checksum(data: bytes, struct: Struct) -> int:
|
55
57
|
"""Computes the CRC16 XModem checksum for My-Setting files.
|
56
58
|
|
57
59
|
The checksum is calculated over the contents of the `data` field,
|
@@ -83,11 +85,11 @@ def compute_checksum(data, struct):
|
|
83
85
|
return crc
|
84
86
|
|
85
87
|
|
86
|
-
def _is_valid_key(k: str):
|
88
|
+
def _is_valid_key(k: str) -> bool:
|
87
89
|
return not RE_INVALID_KEY.match(k)
|
88
90
|
|
89
91
|
|
90
|
-
class SettingsFile(MutableMapping):
|
92
|
+
class SettingsFile(MutableMapping): # type: ignore[type-arg]
|
91
93
|
"""Base class for the Rekordbox My-Setting file handler.
|
92
94
|
|
93
95
|
The base class implements the getters and setter defined by the keys and
|
@@ -98,16 +100,16 @@ class SettingsFile(MutableMapping):
|
|
98
100
|
"""
|
99
101
|
|
100
102
|
struct: Struct
|
101
|
-
defaults:
|
103
|
+
defaults: Dict[str, str]
|
102
104
|
version: str = "" # only used by DEVSETTING
|
103
105
|
|
104
|
-
def __init__(self):
|
106
|
+
def __init__(self) -> None:
|
105
107
|
super().__init__()
|
106
108
|
self.parsed = None
|
107
|
-
self._items = dict()
|
109
|
+
self._items: Dict[str, str] = dict()
|
108
110
|
|
109
111
|
@classmethod
|
110
|
-
def parse(cls, data: bytes):
|
112
|
+
def parse(cls, data: bytes) -> "SettingsFile":
|
111
113
|
"""Parses the in-memory data of a Rekordbox settings binary file.
|
112
114
|
|
113
115
|
Parameters
|
@@ -125,18 +127,18 @@ class SettingsFile(MutableMapping):
|
|
125
127
|
return self
|
126
128
|
|
127
129
|
@classmethod
|
128
|
-
def parse_file(cls, path: str):
|
130
|
+
def parse_file(cls, path: Union[str, Path]) -> "SettingsFile":
|
129
131
|
"""Reads and parses a Rekordbox settings binary file.
|
130
132
|
|
131
133
|
Parameters
|
132
134
|
----------
|
133
|
-
path : str
|
135
|
+
path : str or Path
|
134
136
|
The path of a Rekordbox settings file which is used to read
|
135
137
|
the file contents before parsing the binary data.
|
136
138
|
|
137
139
|
Returns
|
138
140
|
-------
|
139
|
-
self :
|
141
|
+
self : SettingsFile
|
140
142
|
The new instance with the parsed file content.
|
141
143
|
|
142
144
|
See Also
|
@@ -147,7 +149,7 @@ class SettingsFile(MutableMapping):
|
|
147
149
|
data = fh.read()
|
148
150
|
return cls.parse(data)
|
149
151
|
|
150
|
-
def _parse(self, data):
|
152
|
+
def _parse(self, data: bytes) -> None:
|
151
153
|
parsed = self.struct.parse(data)
|
152
154
|
keys = filter(_is_valid_key, parsed.data.keys())
|
153
155
|
items = dict()
|
@@ -157,27 +159,27 @@ class SettingsFile(MutableMapping):
|
|
157
159
|
self.parsed = parsed
|
158
160
|
self._items.update(items)
|
159
161
|
|
160
|
-
def __len__(self):
|
162
|
+
def __len__(self) -> int:
|
161
163
|
return len(self.defaults.keys())
|
162
164
|
|
163
|
-
def __iter__(self):
|
165
|
+
def __iter__(self) -> Iterator[str]:
|
164
166
|
return iter(self.defaults)
|
165
167
|
|
166
|
-
def __getitem__(self, key):
|
168
|
+
def __getitem__(self, key: str) -> str:
|
167
169
|
try:
|
168
170
|
return self._items[key]
|
169
171
|
except KeyError:
|
170
172
|
return self.defaults[key]
|
171
173
|
|
172
|
-
def __setitem__(self, key, value):
|
174
|
+
def __setitem__(self, key: str, value: str) -> None:
|
173
175
|
if key not in self.defaults.keys():
|
174
176
|
raise KeyError(f"Key {key} not a valid field of {self.__class__.__name__}")
|
175
177
|
self._items[key] = value
|
176
178
|
|
177
|
-
def __delitem__(self, key):
|
179
|
+
def __delitem__(self, key: str) -> None:
|
178
180
|
del self._items[key]
|
179
181
|
|
180
|
-
def get(self, key, default=None):
|
182
|
+
def get(self, key: str, default: str = None) -> Union[str, None]: # type: ignore[override]
|
181
183
|
"""Returns the value of a setting of the My-Setting file.
|
182
184
|
|
183
185
|
If the key is not found in the My-Setting data, but it is present in the
|
@@ -202,7 +204,7 @@ class SettingsFile(MutableMapping):
|
|
202
204
|
except KeyError:
|
203
205
|
return default
|
204
206
|
|
205
|
-
def set(self, key, value):
|
207
|
+
def set(self, key: str, value: str) -> None:
|
206
208
|
"""Sets the value of a setting of the My-Setting file.
|
207
209
|
|
208
210
|
Parameters
|
@@ -214,7 +216,7 @@ class SettingsFile(MutableMapping):
|
|
214
216
|
"""
|
215
217
|
self.__setitem__(key, value)
|
216
218
|
|
217
|
-
def build(self):
|
219
|
+
def build(self) -> bytes:
|
218
220
|
"""Constructs the binary data for saving the My-Setting file.
|
219
221
|
|
220
222
|
Returns
|
@@ -227,7 +229,7 @@ class SettingsFile(MutableMapping):
|
|
227
229
|
items.update(self._items)
|
228
230
|
|
229
231
|
# Create file data
|
230
|
-
file_items = {"data": items, "checksum": 0}
|
232
|
+
file_items: Dict[str, Any] = {"data": items, "checksum": 0}
|
231
233
|
if self.version:
|
232
234
|
file_items["version"] = self.version
|
233
235
|
|
@@ -239,9 +241,10 @@ class SettingsFile(MutableMapping):
|
|
239
241
|
file_items["checksum"] = checksum
|
240
242
|
|
241
243
|
# Write data with updated checksum
|
242
|
-
|
244
|
+
bytedata: bytes = self.struct.build(file_items)
|
245
|
+
return bytedata
|
243
246
|
|
244
|
-
def save(self, path):
|
247
|
+
def save(self, path: Union[str, Path]) -> None:
|
245
248
|
"""Save the contents of the My-Setting file object.
|
246
249
|
|
247
250
|
Parameters
|
@@ -257,7 +260,7 @@ class SettingsFile(MutableMapping):
|
|
257
260
|
with open(path, "wb") as fh:
|
258
261
|
fh.write(data)
|
259
262
|
|
260
|
-
def __repr__(self):
|
263
|
+
def __repr__(self) -> str:
|
261
264
|
return f"{self.__class__.__name__}()"
|
262
265
|
|
263
266
|
|
@@ -363,7 +366,7 @@ class DevSettingFile(SettingsFile):
|
|
363
366
|
defaults = dict(entries="")
|
364
367
|
|
365
368
|
|
366
|
-
FILES = {
|
369
|
+
FILES: Dict[str, Type[SettingsFile]] = {
|
367
370
|
"DEVSETTING.DAT": DevSettingFile,
|
368
371
|
"DJMMYSETTING.DAT": DjmMySettingFile,
|
369
372
|
"MYSETTING.DAT": MySettingFile,
|