pyrekordbox 0.3.2__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 (83) 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 +101 -111
  11. pyrekordbox/db6/registry.py +1 -0
  12. pyrekordbox/db6/smartlist.py +7 -6
  13. pyrekordbox/db6/tables.py +44 -16
  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.2.dist-info → pyrekordbox-0.4.0.dist-info}/METADATA +21 -41
  20. pyrekordbox-0.4.0.dist-info/RECORD +25 -0
  21. {pyrekordbox-0.3.2.dist-info → pyrekordbox-0.4.0.dist-info}/WHEEL +1 -1
  22. {pyrekordbox-0.3.2.dist-info → pyrekordbox-0.4.0.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.0.dist-info}/LICENSE +0 -0
pyrekordbox/__init__.py CHANGED
@@ -2,19 +2,19 @@
2
2
  # Author: Dylan Jones
3
3
  # Date: 2022-04-10
4
4
 
5
+ from .anlz import AnlzFile, get_anlz_paths, read_anlz_files, walk_anlz_paths
6
+ from .config import get_config, show_config, update_config
7
+ from .db6 import Rekordbox6Database
5
8
  from .logger import logger
6
- from .config import show_config, get_config, update_config
7
- from .rbxml import RekordboxXml, XmlDuplicateError, XmlAttributeKeyError
8
- from .anlz import get_anlz_paths, walk_anlz_paths, read_anlz_files, AnlzFile
9
9
  from .mysettings import (
10
+ DevSettingFile,
11
+ DjmMySettingFile,
12
+ MySetting2File,
13
+ MySettingFile,
10
14
  get_mysetting_paths,
11
15
  read_mysetting_file,
12
- MySettingFile,
13
- MySetting2File,
14
- DjmMySettingFile,
15
- DevSettingFile,
16
16
  )
17
- from .db6 import Rekordbox6Database, open_rekordbox_database
17
+ from .rbxml import RekordboxXml, XmlAttributeKeyError, XmlDuplicateError
18
18
 
19
19
  try:
20
20
  from ._version import version as __version__
pyrekordbox/__main__.py CHANGED
@@ -4,11 +4,12 @@
4
4
 
5
5
  import os
6
6
  import re
7
- import sys
8
7
  import shutil
8
+ import sys
9
9
  import urllib.request
10
10
  from pathlib import Path
11
- from pyrekordbox.config import write_db6_key_cache, _cache_file
11
+
12
+ from pyrekordbox.config import _cache_file, write_db6_key_cache
12
13
 
13
14
  KEY_SOURCES = [
14
15
  {
pyrekordbox/_version.py CHANGED
@@ -12,5 +12,5 @@ __version__: str
12
12
  __version_tuple__: VERSION_TUPLE
13
13
  version_tuple: VERSION_TUPLE
14
14
 
15
- __version__ = version = '0.3.2'
16
- __version_tuple__ = version_tuple = (0, 3, 2)
15
+ __version__ = version = '0.4.0'
16
+ __version_tuple__ = version_tuple = (0, 4, 0)
@@ -5,6 +5,7 @@
5
5
  import re
6
6
  from pathlib import Path
7
7
  from typing import Union
8
+
8
9
  from . import structs
9
10
  from .file import AnlzFile
10
11
 
@@ -77,7 +78,7 @@ def walk_anlz_dirs(root_dir: Union[str, Path]):
77
78
  The path of the root directory.
78
79
 
79
80
  Yields
80
- -------
81
+ ------
81
82
  anlz_dir : str
82
83
  The path of a directory containing ANLZ files
83
84
  """
@@ -96,7 +97,7 @@ def walk_anlz_paths(root_dir: Union[str, Path]):
96
97
  The path of the root directory.
97
98
 
98
99
  Yields
99
- -------
100
+ ------
100
101
  anlz_dir : str
101
102
  The path of a directory containing ANLZ files.
102
103
  anlz_files : Sequence of str
pyrekordbox/anlz/file.py CHANGED
@@ -6,10 +6,12 @@ import logging
6
6
  from collections import abc
7
7
  from pathlib import Path
8
8
  from typing import Union
9
- from .tags import TAGS
10
- from . import structs
9
+
11
10
  from construct import Int16ub
12
11
 
12
+ from . import structs
13
+ from .tags import TAGS
14
+
13
15
  logger = logging.getLogger(__name__)
14
16
 
15
17
  XOR_MASK = bytearray.fromhex("CB E1 EE FA E5 EE AD EE E9 D2 E9 EB E1 E9 F3 E8 E9 F4 E1")
pyrekordbox/anlz/tags.py CHANGED
@@ -4,7 +4,9 @@
4
4
 
5
5
  import logging
6
6
  from abc import ABC
7
+
7
8
  import numpy as np
9
+
8
10
  from . import structs
9
11
 
10
12
  logger = logging.getLogger(__name__)
@@ -439,7 +441,7 @@ class PWV5AnlzTag(AbstractAnlzTag):
439
441
  LEN_HEADER = 24
440
442
 
441
443
  def get(self):
442
- """Parse the Waveform Color Detail Tag (PWV5)
444
+ """Parse the Waveform Color Detail Tag (PWV5).
443
445
 
444
446
  The format of the entries is:
445
447
 
pyrekordbox/config.py CHANGED
@@ -40,6 +40,7 @@ __config__ = {
40
40
  },
41
41
  "rekordbox5": {},
42
42
  "rekordbox6": {},
43
+ "rekordbox7": {},
43
44
  }
44
45
 
45
46
 
@@ -50,7 +51,8 @@ class InvalidApplicationDirname(Exception):
50
51
  def get_pioneer_install_dir(path: Union[str, Path] = None) -> Path: # pragma: no cover
51
52
  """Returns the path of the Pioneer program installation directory.
52
53
 
53
- On Windows, the Pioneer program data is stored in `/ProgramFiles/Pioneer`
54
+ On Windows, the Pioneer program data is stored in `/ProgramFiles/Pioneer`.
55
+ For rekordbox version 7 this has changed to `/ProgramFiles/rekordbox`.
54
56
  On macOS the program data is somewhere in `/Applications/`.
55
57
 
56
58
  Parameters
@@ -68,7 +70,7 @@ def get_pioneer_install_dir(path: Union[str, Path] = None) -> Path: # pragma: n
68
70
  if sys.platform == "win32":
69
71
  # Windows: located in /ProgramFiles/Pioneer
70
72
  program_files = os.environ["ProgramFiles"].replace("(x86)", "").strip()
71
- path = Path(program_files) / "Pioneer"
73
+ path = Path(program_files)
72
74
  elif sys.platform == "darwin":
73
75
  # MacOS: located in /Applications/
74
76
  path = Path("/Applications")
@@ -284,6 +286,12 @@ def _get_rb_config(
284
286
  config : dict
285
287
  The program configuration.
286
288
  """
289
+ if sys.platform == "win32":
290
+ if major_version >= 7:
291
+ pioneer_install_dir = pioneer_install_dir / "rekordbox"
292
+ else:
293
+ pioneer_install_dir = pioneer_install_dir / "Pioneer"
294
+
287
295
  if application_dirname:
288
296
  # Applitcation dirname is given, only extract version from it
289
297
  # `major_version` is compared to the version string
@@ -328,7 +336,7 @@ def _get_rb_config(
328
336
  logger.debug("Found Rekordbox %s install-dir: '%s'", major_version, rb_prog_dir)
329
337
 
330
338
  # Get Rekordbox application directory path for major release `major_version`
331
- name = "rekordbox6" if major_version == 6 else "rekordbox"
339
+ name = "rekordbox6" if major_version >= 6 else "rekordbox"
332
340
  rb_app_dir = pioneer_app_dir / name
333
341
  if not rb_app_dir.exists():
334
342
  raise FileNotFoundError(f"The directory '{rb_app_dir}' doesn't exist!")
@@ -337,7 +345,7 @@ def _get_rb_config(
337
345
  # Get Rekordbox database locations for major release `major_version`
338
346
  settings = read_rekordbox_settings(rb_app_dir)
339
347
  db_dir = Path(settings["masterDbDirectory"])
340
- db_filename = "master.db" if major_version == 6 else "datafile.edb"
348
+ db_filename = "master.db" if major_version >= 6 else "datafile.edb"
341
349
  db_path = db_dir / db_filename
342
350
  if not db_path.exists():
343
351
  raise FileNotFoundError(f"The Rekordbox database '{db_path}' doesn't exist!")
@@ -363,7 +371,6 @@ def _get_rb5_config(
363
371
 
364
372
  def _extract_pw(pioneer_install_dir: Path) -> str: # pragma: no cover
365
373
  """Extract the password for decrypting the Rekordbox 6 database key."""
366
-
367
374
  asar_data = read_rekordbox6_asar(pioneer_install_dir)
368
375
  match_result = re.search('pass: ".(.*?)"', asar_data)
369
376
  if match_result is None:
@@ -457,23 +464,13 @@ def write_db6_key_cache(key: str) -> None: # pragma: no cover
457
464
  with open(_cache_file, "w") as fh:
458
465
  fh.write(text)
459
466
  # Set the config key to make sure the key is present after calling method
460
- __config__["rekordbox6"]["dp"] = key
467
+ if __config__["rekordbox6"]:
468
+ __config__["rekordbox6"]["dp"] = key
469
+ if __config__["rekordbox7"]:
470
+ __config__["rekordbox7"]["dp"] = key
461
471
 
462
472
 
463
- def _get_rb6_config(
464
- pioneer_prog_dir: Path, pioneer_app_dir: Path, dirname: str = ""
465
- ) -> dict:
466
- """Get the program configuration for Rekordbox v6.x.x."""
467
- major_version = 6
468
- conf = _get_rb_config(pioneer_prog_dir, pioneer_app_dir, major_version, dirname)
469
-
470
- # Read Rekordbox 6 'options.json' and check db_path
471
- opts = read_rekordbox6_options(pioneer_app_dir)
472
- db_path = Path(opts["db-path"])
473
- db_dir = db_path.parent
474
- assert str(conf["db_dir"]) == str(db_dir)
475
- assert str(conf["db_path"]) == str(db_path)
476
-
473
+ def _update_sqlite_key(opts, conf):
477
474
  cache_version = 0
478
475
  pw, dp = "", ""
479
476
  if _cache_file.exists(): # pragma: no cover
@@ -520,9 +517,10 @@ def _get_rb6_config(
520
517
  if sys.platform == "win32":
521
518
  executable = conf["install_dir"] / "rekordbox.exe"
522
519
  elif sys.platform == "darwin":
523
- executable = (
524
- conf["install_dir"] / "Contents" / "MacOS" / "rekordbox"
525
- )
520
+ install_dir = conf["install_dir"]
521
+ if not str(install_dir).endswith(".app"):
522
+ install_dir = install_dir / "rekordbox.app"
523
+ executable = install_dir / "Contents" / "MacOS" / "rekordbox"
526
524
  else:
527
525
  # Linux: not supported
528
526
  logger.warning(f"OS {sys.platform} not supported!")
@@ -552,6 +550,44 @@ def _get_rb6_config(
552
550
  if dp:
553
551
  conf["dp"] = dp
554
552
 
553
+
554
+ def _get_rb6_config(
555
+ pioneer_prog_dir: Path, pioneer_app_dir: Path, dirname: str = ""
556
+ ) -> dict:
557
+ """Get the program configuration for Rekordbox v6.x.x."""
558
+ major_version = 6
559
+ conf = _get_rb_config(pioneer_prog_dir, pioneer_app_dir, major_version, dirname)
560
+
561
+ # Read Rekordbox 6 'options.json' and check db_path
562
+ opts = read_rekordbox6_options(pioneer_app_dir)
563
+ db_path = Path(opts["db-path"])
564
+ db_dir = db_path.parent
565
+ assert str(conf["db_dir"]) == str(db_dir)
566
+ assert str(conf["db_path"]) == str(db_path)
567
+
568
+ # Update SQLite key
569
+ _update_sqlite_key(opts, conf)
570
+
571
+ return conf
572
+
573
+
574
+ def _get_rb7_config(
575
+ pioneer_prog_dir: Path, pioneer_app_dir: Path, dirname: str = ""
576
+ ) -> dict:
577
+ """Get the program configuration for Rekordbox v7.x.x."""
578
+ major_version = 7
579
+ conf = _get_rb_config(pioneer_prog_dir, pioneer_app_dir, major_version, dirname)
580
+
581
+ # Read Rekordbox 6 'options.json' and check db_path
582
+ opts = read_rekordbox6_options(pioneer_app_dir)
583
+ db_path = Path(opts["db-path"])
584
+ db_dir = db_path.parent
585
+ assert str(conf["db_dir"]) == str(db_dir)
586
+ assert str(conf["db_path"]) == str(db_path)
587
+
588
+ # Update SQLite key
589
+ _update_sqlite_key(opts, conf)
590
+
555
591
  return conf
556
592
 
557
593
 
@@ -617,6 +653,7 @@ def update_config(
617
653
  pioneer_app_dir: Union[str, Path] = None,
618
654
  rb5_install_dirname: str = "",
619
655
  rb6_install_dirname: str = "",
656
+ rb7_install_dirname: str = "",
620
657
  ):
621
658
  """Update the pyrekordbox configuration.
622
659
 
@@ -643,6 +680,9 @@ def update_config(
643
680
  rb6_install_dirname : str, optional
644
681
  The name of the Rekordbox 6 installation directory. By default, the normal
645
682
  directory name is used (Windows: 'rekordbox 6.x.x', macOS: 'rekordbox 6.app').
683
+ rb7_install_dirname : str, optional
684
+ The name of the Rekordbox 7 installation directory. By default, the normal
685
+ directory name is used (Windows: 'rekordbox 7.x.x', macOS: 'rekordbox 7.app').
646
686
  """
647
687
  # Read config file
648
688
  conf = read_pyrekordbox_configuration()
@@ -654,6 +694,8 @@ def update_config(
654
694
  rb5_install_dirname = conf["rekordbox5-install-dirname"]
655
695
  if not rb6_install_dirname and "rekordbox6-install-dirname" in conf:
656
696
  rb6_install_dirname = conf["rekordbox6-install-dirname"]
697
+ if not rb7_install_dirname and "rekordbox7-install-dirname" in conf:
698
+ rb7_install_dirname = conf["rekordbox7-install-dirname"]
657
699
 
658
700
  # Pioneer installation directory
659
701
  try:
@@ -689,6 +731,15 @@ def update_config(
689
731
  except FileNotFoundError as e:
690
732
  logger.info(e)
691
733
 
734
+ # Update Rekordbox 7 config
735
+ try:
736
+ conf = _get_rb7_config(
737
+ pioneer_install_dir, pioneer_app_dir, rb7_install_dirname
738
+ )
739
+ __config__["rekordbox7"].update(conf)
740
+ except FileNotFoundError as e:
741
+ logger.info(e)
742
+
692
743
 
693
744
  def get_config(section: str, key: str = None):
694
745
  """Gets a section or value of the pyrekordbox configuration.
@@ -721,6 +772,7 @@ def pformat_config(indent: str = " ", hw: int = 14, delim: str = " = ") -> str
721
772
  pioneer = get_config("pioneer")
722
773
  rb5 = get_config("rekordbox5")
723
774
  rb6 = get_config("rekordbox6")
775
+ rb7 = get_config("rekordbox7")
724
776
 
725
777
  lines = ["Pioneer:"]
726
778
  lines += [f"{indent}{k + delim:<{hw}} {pioneer[k]}" for k in sorted(pioneer.keys())]
@@ -731,6 +783,10 @@ def pformat_config(indent: str = " ", hw: int = 14, delim: str = " = ") -> str
731
783
  if rb6:
732
784
  rb6_keys = [k for k in rb6.keys() if k not in ("dp", "p")]
733
785
  lines += [f"{indent}{k + delim:<{hw}} {rb6[k]}" for k in sorted(rb6_keys)]
786
+ lines.append("Rekordbox 7:")
787
+ if rb7:
788
+ rb7_keys = [k for k in rb7.keys() if k not in ("dp", "p")]
789
+ lines += [f"{indent}{k + delim:<{hw}} {rb7[k]}" for k in sorted(rb7_keys)]
734
790
  return "\n".join(lines)
735
791
 
736
792
 
@@ -2,6 +2,8 @@
2
2
  # Author: Dylan Jones
3
3
  # Date: 2022-05-07
4
4
 
5
+ from .database import Rekordbox6Database
6
+ from .smartlist import SmartList
5
7
  from .tables import (
6
8
  AgentRegistry,
7
9
  CloudAgentRegistry,
@@ -41,5 +43,3 @@ from .tables import (
41
43
  SettingFile,
42
44
  UuidIDMap,
43
45
  )
44
- from .smartlist import SmartList
45
- from .database import Rekordbox6Database, open_rekordbox_database
@@ -2,9 +2,10 @@
2
2
  # Author: Dylan Jones
3
3
  # Date: 2023-09-10
4
4
 
5
- from pathlib import Path
6
- from datetime import datetime
7
5
  import xml.etree.cElementTree as xml
6
+ from datetime import datetime
7
+ from pathlib import Path
8
+
8
9
  from ..config import get_config
9
10
  from ..utils import pretty_xml
10
11
 
@@ -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
@@ -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)
@@ -813,7 +726,9 @@ class Rekordbox6Database:
813
726
 
814
727
  # -- Database updates --------------------------------------------------------------
815
728
 
816
- 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:
817
732
  """Generates an unused ID for the given table."""
818
733
  max_tries = 1000000
819
734
  for _ in range(max_tries):
@@ -825,7 +740,8 @@ class Rekordbox6Database:
825
740
  if id_ < 100:
826
741
  continue
827
742
  # Check if ID is already used
828
- 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_)
829
745
  used = self.query(query.exists()).scalar()
830
746
  if not used:
831
747
  return id_
@@ -1875,6 +1791,80 @@ class Rekordbox6Database:
1875
1791
  self.flush()
1876
1792
  return label
1877
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
+
1878
1868
  # ----------------------------------------------------------------------------------
1879
1869
 
1880
1870
  def get_mysetting_paths(self):
@@ -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
 
@@ -320,11 +320,12 @@ class SmartList:
320
320
  comps = list()
321
321
  for cond in self.conditions:
322
322
  val_left, val_right = _get_condition_values(cond)
323
-
323
+ # val_left = str(-abs(int(val_left))) if val_left is not None else ""
324
324
  if cond.property in PROPERTY_COLUMN_MAP:
325
325
  colum_name = PROPERTY_COLUMN_MAP[cond.property]
326
326
  if cond.property == Property.MYTAG:
327
- val_left = str(right_bitshift(int(val_left)))
327
+ if int(val_left) < 0:
328
+ val_left = str(right_bitshift(int(val_left)))
328
329
 
329
330
  if cond.operator == Operator.EQUAL:
330
331
  comp = getattr(DjmdContent, colum_name) == val_left