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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
pyrekordbox/config.py CHANGED
@@ -18,7 +18,7 @@ import textwrap
18
18
  import time
19
19
  import xml.etree.cElementTree as xml
20
20
  from pathlib import Path
21
- from typing import Union
21
+ from typing import Any, Dict, List, Union
22
22
 
23
23
  import blowfish
24
24
  import frida
@@ -67,6 +67,10 @@ def get_appdata_dir() -> Path:
67
67
  return app_data
68
68
 
69
69
 
70
+ def get_cache_file() -> Path:
71
+ return get_appdata_dir() / "pyrekordbox" / _cache_file_name
72
+
73
+
70
74
  def get_pioneer_install_dir(path: Union[str, Path] = None) -> Path: # pragma: no cover
71
75
  """Returns the path of the Pioneer program installation directory.
72
76
 
@@ -147,7 +151,7 @@ def get_pioneer_app_dir(path: Union[str, Path] = None) -> Path: # pragma: no co
147
151
  return path
148
152
 
149
153
 
150
- def _convert_type(s):
154
+ def _convert_type(s: str) -> Union[str, int, float, List[int], List[float]]:
151
155
  # Try to parse as int, float, list of int, list of float
152
156
  types_ = int, float
153
157
  for type_ in types_:
@@ -163,7 +167,7 @@ def _convert_type(s):
163
167
  return s
164
168
 
165
169
 
166
- def read_rekordbox_settings(rekordbox_app_dir: Union[str, Path]) -> dict:
170
+ def read_rekordbox_settings(rekordbox_app_dir: Union[str, Path]) -> Dict[str, Any]:
167
171
  """Finds and parses the 'rekordbox3.settings' file in the Rekordbox 5 or 6 app-dir.
168
172
 
169
173
  The settings file usually is called 'rekordbox3.settings' and is
@@ -193,12 +197,12 @@ def read_rekordbox_settings(rekordbox_app_dir: Union[str, Path]) -> dict:
193
197
  val = _convert_type(element.attrib["val"])
194
198
  except KeyError:
195
199
  device_setup = element.find("DEVICESETUP")
196
- val = {k: _convert_type(v) for k, v in device_setup.attrib.items()}
200
+ val = {k: _convert_type(v) for k, v in device_setup.attrib.items()} # type: ignore
197
201
  settings[name] = val
198
202
  return settings
199
203
 
200
204
 
201
- def read_rekordbox6_options(pioneer_app_dir: Union[str, Path]) -> dict:
205
+ def read_rekordbox6_options(pioneer_app_dir: Union[str, Path]) -> Dict[str, Any]:
202
206
  """Finds and parses the Rekordbox 6 `options.json` file with additional settings.
203
207
 
204
208
  The options file contains additional settings used by Rekordbox 6, for example the
@@ -267,7 +271,7 @@ def read_rekordbox6_asar(rb6_install_dir: Union[str, Path]) -> str:
267
271
  return data
268
272
 
269
273
 
270
- def _extract_version(name, major_version):
274
+ def _extract_version(name: str, major_version: int) -> str:
271
275
  name = name.replace(".app", "") # Needed for MacOS
272
276
  ver_str = name.replace("rekordbox", "").strip()
273
277
  if not ver_str:
@@ -280,7 +284,7 @@ def _get_rb_config(
280
284
  pioneer_app_dir: Path,
281
285
  major_version: int,
282
286
  application_dirname: str = "",
283
- ) -> dict:
287
+ ) -> Dict[str, Any]:
284
288
  """Get the program configuration for a given Rekordbox major version.
285
289
 
286
290
  Parameters
@@ -374,7 +378,9 @@ def _get_rb_config(
374
378
  return conf
375
379
 
376
380
 
377
- def _get_rb5_config(pioneer_prog_dir: Path, pioneer_app_dir: Path, dirname: str = "") -> dict:
381
+ def _get_rb5_config(
382
+ pioneer_prog_dir: Path, pioneer_app_dir: Path, dirname: str = ""
383
+ ) -> Dict[str, Any]:
378
384
  """Get the program configuration for Rekordbox v5.x.x."""
379
385
  major_version = 5
380
386
  conf = _get_rb_config(pioneer_prog_dir, pioneer_app_dir, major_version, dirname)
@@ -413,16 +419,16 @@ class KeyExtractor:
413
419
  """)
414
420
  # fmt: on
415
421
 
416
- def __init__(self, rekordbox_executable):
422
+ def __init__(self, rekordbox_executable: Union[str, Path]):
417
423
  self.executable = str(rekordbox_executable)
418
424
  self.key = ""
419
425
 
420
- def on_message(self, message, data):
426
+ def on_message(self, message: Dict[str, Any], data: Any) -> None:
421
427
  payload = message["payload"]
422
428
  if payload.startswith("sqlite3_key"):
423
429
  self.key = payload.split(": ")[1]
424
430
 
425
- def run(self):
431
+ def run(self) -> str:
426
432
  pid = get_rekordbox_pid()
427
433
  if pid:
428
434
  raise RuntimeError(
@@ -477,7 +483,7 @@ def write_db6_key_cache(key: str) -> None: # pragma: no cover
477
483
  lines.append("dp: " + key)
478
484
  text = "\n".join(lines)
479
485
 
480
- cache_file = get_appdata_dir() / "pyrekordbox" / _cache_file_name
486
+ cache_file = get_cache_file()
481
487
  if not cache_file.parent.exists():
482
488
  cache_file.parent.mkdir()
483
489
 
@@ -485,16 +491,16 @@ def write_db6_key_cache(key: str) -> None: # pragma: no cover
485
491
  fh.write(text)
486
492
  # Set the config key to make sure the key is present after calling method
487
493
  if __config__["rekordbox6"]:
488
- __config__["rekordbox6"]["dp"] = key
494
+ __config__["rekordbox6"]["dp"] = key # type: ignore
489
495
  if __config__["rekordbox7"]:
490
- __config__["rekordbox7"]["dp"] = key
496
+ __config__["rekordbox7"]["dp"] = key # type: ignore
491
497
 
492
498
 
493
- def _update_sqlite_key(opts, conf):
499
+ def _update_sqlite_key(opts: Dict[str, Any], conf: Dict[str, Any]) -> None:
494
500
  cache_version = 0
495
501
  pw, dp = "", ""
496
502
 
497
- cache_file = get_appdata_dir() / "pyrekordbox" / _cache_file_name
503
+ cache_file = get_cache_file()
498
504
 
499
505
  if cache_file.exists(): # pragma: no cover
500
506
  logger.debug("Found cache file %s", cache_file)
@@ -533,7 +539,7 @@ def _update_sqlite_key(opts, conf):
533
539
  if not dp:
534
540
  if pw:
535
541
  cipher = blowfish.Cipher(pw.encode())
536
- dp = base64.standard_b64decode(opts["dp"])
542
+ dp = base64.standard_b64decode(opts["dp"]) # type: ignore
537
543
  dp = b"".join(cipher.decrypt_ecb(dp)).decode()
538
544
  logger.debug("Unlocked dp from pw: %s", dp)
539
545
  else:
@@ -574,7 +580,9 @@ def _update_sqlite_key(opts, conf):
574
580
  conf["dp"] = dp
575
581
 
576
582
 
577
- def _get_rb6_config(pioneer_prog_dir: Path, pioneer_app_dir: Path, dirname: str = "") -> dict:
583
+ def _get_rb6_config(
584
+ pioneer_prog_dir: Path, pioneer_app_dir: Path, dirname: str = ""
585
+ ) -> Dict[str, Any]:
578
586
  """Get the program configuration for Rekordbox v6.x.x."""
579
587
  major_version = 6
580
588
  conf = _get_rb_config(pioneer_prog_dir, pioneer_app_dir, major_version, dirname)
@@ -592,7 +600,9 @@ def _get_rb6_config(pioneer_prog_dir: Path, pioneer_app_dir: Path, dirname: str
592
600
  return conf
593
601
 
594
602
 
595
- def _get_rb7_config(pioneer_prog_dir: Path, pioneer_app_dir: Path, dirname: str = "") -> dict:
603
+ def _get_rb7_config(
604
+ pioneer_prog_dir: Path, pioneer_app_dir: Path, dirname: str = ""
605
+ ) -> Dict[str, Any]:
596
606
  """Get the program configuration for Rekordbox v7.x.x."""
597
607
  major_version = 7
598
608
  conf = _get_rb_config(pioneer_prog_dir, pioneer_app_dir, major_version, dirname)
@@ -611,7 +621,7 @@ def _get_rb7_config(pioneer_prog_dir: Path, pioneer_app_dir: Path, dirname: str
611
621
 
612
622
 
613
623
  # noinspection PyPackageRequirements,PyUnresolvedReferences
614
- def _read_config_file(path: str) -> dict:
624
+ def _read_config_file(path: Union[str, Path]) -> Dict[str, Any]:
615
625
  path = Path(path)
616
626
  if not path.exists():
617
627
  raise FileNotFoundError(f"No such file or directory: '{path}'")
@@ -622,20 +632,20 @@ def _read_config_file(path: str) -> dict:
622
632
 
623
633
  parser = ConfigParser()
624
634
  parser.read(path)
625
- return parser
635
+ return {k: v for k, v in parser.items() if v is not None}
626
636
  elif ext == ".toml":
627
637
  import toml
628
638
 
629
- return toml.load(path)
639
+ return dict(toml.load(path))
630
640
  elif ext in (".yaml", ".yml"):
631
641
  import yaml
632
642
 
633
643
  with open(path, "r") as stream:
634
- return yaml.safe_load(stream)
644
+ return dict(yaml.safe_load(stream))
635
645
  return dict()
636
646
 
637
647
 
638
- def read_pyrekordbox_configuration():
648
+ def read_pyrekordbox_configuration() -> Dict[str, Any]:
639
649
  """Reads the pyrekordbox configuration.
640
650
 
641
651
  So far only the `pioneer-install-dir` and `pioneer-app-dir` fileds in the
@@ -673,7 +683,7 @@ def update_config(
673
683
  rb5_install_dirname: str = "",
674
684
  rb6_install_dirname: str = "",
675
685
  rb7_install_dirname: str = "",
676
- ):
686
+ ) -> None:
677
687
  """Update the pyrekordbox configuration.
678
688
 
679
689
  This method scans the system for the Rekordbox installation and application data
@@ -754,7 +764,7 @@ def update_config(
754
764
  logger.info(e)
755
765
 
756
766
 
757
- def get_config(section: str, key: str = None):
767
+ def get_config(section: str, key: str = None) -> Any:
758
768
  """Gets a section or value of the pyrekordbox configuration.
759
769
 
760
770
  Parameters
@@ -5,10 +5,20 @@
5
5
  import xml.etree.cElementTree as xml
6
6
  from datetime import datetime
7
7
  from pathlib import Path
8
+ from typing import Any, Dict, List, Optional, Union
8
9
 
9
10
  from ..config import get_config
10
11
  from ..utils import pretty_xml
11
12
 
13
+ Attribs = Dict[str, Any]
14
+
15
+
16
+ class XmlElementNotInitializedError(Exception):
17
+ """Raised when an XML element is not initialized."""
18
+
19
+ def __init__(self, name: str) -> None:
20
+ super().__init__(f"XML element {name} is not initialized!")
21
+
12
22
 
13
23
  class MasterPlaylistXml:
14
24
  """Rekordbox v6 masterPlaylists6.xml file handler.
@@ -26,7 +36,7 @@ class MasterPlaylistXml:
26
36
 
27
37
  KEYS = ["Id", "ParentId", "Attributes", "Timestamp", "Lib_Type", "CheckType"]
28
38
 
29
- def __init__(self, path=None, db_dir=None):
39
+ def __init__(self, path: Union[str, Path] = None, db_dir: Union[str, Path] = None):
30
40
  if path is None:
31
41
  if db_dir is None:
32
42
  db_dir = get_config("rekordbox6", "db_dir")
@@ -40,29 +50,33 @@ class MasterPlaylistXml:
40
50
  self._changed = False
41
51
 
42
52
  @property
43
- def version(self):
53
+ def version(self) -> str:
44
54
  return self.root.attrib["Version"]
45
55
 
46
56
  @property
47
- def automatic_sync(self):
57
+ def automatic_sync(self) -> str:
48
58
  return self.root.attrib["AutomaticSync"]
49
59
 
50
60
  @property
51
- def rekordbox_version(self):
61
+ def rekordbox_version(self) -> str:
62
+ if self.product is None:
63
+ raise XmlElementNotInitializedError("product")
52
64
  return self.product.attrib["Version"]
53
65
 
54
66
  @property
55
- def modified(self):
67
+ def modified(self) -> bool:
56
68
  return self._changed
57
69
 
58
- def get_playlists(self):
70
+ def get_playlists(self) -> List[Dict[str, Any]]:
59
71
  """Returns a list of the attributes of all playlist elements."""
72
+ if self.playlists is None:
73
+ raise XmlElementNotInitializedError("playlists")
60
74
  items = list()
61
75
  for playlist in self.playlists:
62
76
  items.append(playlist.attrib)
63
77
  return items
64
78
 
65
- def get(self, playlist_id):
79
+ def get(self, playlist_id: Union[str, int]) -> Optional[Attribs]:
66
80
  """Returns element attribs with the PlaylistID used in the `master.db` database.
67
81
 
68
82
  Parameters
@@ -75,11 +89,13 @@ class MasterPlaylistXml:
75
89
  -------
76
90
  playlist : dict
77
91
  """
92
+ if self.playlists is None:
93
+ raise XmlElementNotInitializedError("playlists")
78
94
  hex_id = f"{int(playlist_id):X}"
79
95
  element = self.playlists.find(f'.//NODE[@Id="{hex_id}"]')
80
96
  if element is None:
81
97
  return None
82
- attribs = dict(element.attrib)
98
+ attribs: Attribs = dict(element.attrib)
83
99
  attribs["Attribute"] = int(attribs["Attribute"])
84
100
  attribs["Timestamp"] = datetime.fromtimestamp(int(attribs["Timestamp"]) / 1000)
85
101
  attribs["Lib_Type"] = int(attribs["Lib_Type"])
@@ -94,7 +110,7 @@ class MasterPlaylistXml:
94
110
  updated_at: datetime,
95
111
  lib_type: int = 0,
96
112
  check_type: int = 0,
97
- ):
113
+ ) -> xml.Element:
98
114
  """Adds a new element with the PlaylistID used in the `master.db` database.
99
115
 
100
116
  Parameters
@@ -119,6 +135,9 @@ class MasterPlaylistXml:
119
135
  element : xml.Element
120
136
  The newly created element.
121
137
  """
138
+ if self.playlists is None:
139
+ raise XmlElementNotInitializedError("playlists")
140
+
122
141
  hex_id = f"{int(playlist_id):X}"
123
142
  parent_id = f"{int(parent_id):X}" if parent_id != "root" else "0"
124
143
  timestamp = int(updated_at.timestamp() * 1000)
@@ -136,7 +155,7 @@ class MasterPlaylistXml:
136
155
  self._changed = True
137
156
  return element
138
157
 
139
- def remove(self, playlist_id):
158
+ def remove(self, playlist_id: Union[str, int]) -> None:
140
159
  """Removes the element with the PlaylistID used in the `master.db` database.
141
160
 
142
161
  Parameters
@@ -145,6 +164,9 @@ class MasterPlaylistXml:
145
164
  The playlist ID used in the main `master.db` database. This id is converted
146
165
  to hexadecimal format before searching.
147
166
  """
167
+ if self.playlists is None:
168
+ raise XmlElementNotInitializedError("playlists")
169
+
148
170
  hex_id = f"{int(playlist_id):X}"
149
171
  element = self.playlists.find(f'.//NODE[@Id="{hex_id}"]')
150
172
  if element is None:
@@ -160,7 +182,7 @@ class MasterPlaylistXml:
160
182
  updated_at: datetime = None,
161
183
  lib_type: int = None,
162
184
  check_type: int = None,
163
- ):
185
+ ) -> None:
164
186
  """Updates the element with the PlaylistID used in the `master.db` database.
165
187
 
166
188
  Parameters
@@ -180,6 +202,9 @@ class MasterPlaylistXml:
180
202
  check_type : int, optional
181
203
  The check type. It seems to be always 0.
182
204
  """
205
+ if self.playlists is None:
206
+ raise XmlElementNotInitializedError("playlists")
207
+
183
208
  hex_id = f"{int(playlist_id):X}"
184
209
  element = self.playlists.find(f'.//NODE[@Id="{hex_id}"]')
185
210
  if element is None:
@@ -200,10 +225,11 @@ class MasterPlaylistXml:
200
225
  element.attrib.update(attribs)
201
226
  self._changed = True
202
227
 
203
- def to_string(self, indent=None):
204
- return pretty_xml(self.root, indent, encoding="utf-8")
228
+ def to_string(self, indent: str = None) -> str:
229
+ text: str = pretty_xml(self.root, indent, encoding="utf-8")
230
+ return text
205
231
 
206
- def save(self, path=None, indent=None):
232
+ def save(self, path: Union[str, Path] = None, indent: str = None) -> None:
207
233
  if path is None:
208
234
  path = self.path
209
235
  path = str(path)