pyrekordbox 0.2.1__py3-none-any.whl → 0.2.2__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 (71) hide show
  1. docs/source/formats/anlz.md +178 -7
  2. docs/source/formats/db6.md +1 -1
  3. docs/source/index.md +2 -6
  4. docs/source/quickstart.md +68 -45
  5. docs/source/tutorial/index.md +1 -1
  6. pyrekordbox/__init__.py +1 -1
  7. pyrekordbox/_version.py +2 -2
  8. pyrekordbox/anlz/file.py +39 -0
  9. pyrekordbox/anlz/structs.py +3 -5
  10. pyrekordbox/config.py +71 -27
  11. pyrekordbox/db6/database.py +260 -33
  12. pyrekordbox/db6/registry.py +22 -0
  13. pyrekordbox/db6/tables.py +3 -4
  14. {pyrekordbox-0.2.1.dist-info → pyrekordbox-0.2.2.dist-info}/METADATA +12 -11
  15. pyrekordbox-0.2.2.dist-info/RECORD +80 -0
  16. {pyrekordbox-0.2.1.dist-info → pyrekordbox-0.2.2.dist-info}/top_level.txt +0 -2
  17. tests/test_config.py +175 -0
  18. tests/test_db6.py +78 -0
  19. build/lib/build/lib/docs/source/conf.py +0 -178
  20. build/lib/build/lib/pyrekordbox/__init__.py +0 -22
  21. build/lib/build/lib/pyrekordbox/__main__.py +0 -204
  22. build/lib/build/lib/pyrekordbox/_version.py +0 -16
  23. build/lib/build/lib/pyrekordbox/anlz/__init__.py +0 -127
  24. build/lib/build/lib/pyrekordbox/anlz/file.py +0 -186
  25. build/lib/build/lib/pyrekordbox/anlz/structs.py +0 -299
  26. build/lib/build/lib/pyrekordbox/anlz/tags.py +0 -508
  27. build/lib/build/lib/pyrekordbox/config.py +0 -596
  28. build/lib/build/lib/pyrekordbox/db6/__init__.py +0 -45
  29. build/lib/build/lib/pyrekordbox/db6/aux_files.py +0 -213
  30. build/lib/build/lib/pyrekordbox/db6/database.py +0 -1808
  31. build/lib/build/lib/pyrekordbox/db6/registry.py +0 -304
  32. build/lib/build/lib/pyrekordbox/db6/tables.py +0 -1618
  33. build/lib/build/lib/pyrekordbox/logger.py +0 -23
  34. build/lib/build/lib/pyrekordbox/mysettings/__init__.py +0 -32
  35. build/lib/build/lib/pyrekordbox/mysettings/file.py +0 -369
  36. build/lib/build/lib/pyrekordbox/mysettings/structs.py +0 -282
  37. build/lib/build/lib/pyrekordbox/utils.py +0 -162
  38. build/lib/build/lib/pyrekordbox/xml.py +0 -1294
  39. build/lib/build/lib/tests/__init__.py +0 -3
  40. build/lib/build/lib/tests/test_anlz.py +0 -206
  41. build/lib/build/lib/tests/test_db6.py +0 -1039
  42. build/lib/build/lib/tests/test_mysetting.py +0 -203
  43. build/lib/build/lib/tests/test_xml.py +0 -629
  44. build/lib/docs/source/conf.py +0 -178
  45. build/lib/pyrekordbox/__init__.py +0 -22
  46. build/lib/pyrekordbox/__main__.py +0 -204
  47. build/lib/pyrekordbox/_version.py +0 -16
  48. build/lib/pyrekordbox/anlz/__init__.py +0 -127
  49. build/lib/pyrekordbox/anlz/file.py +0 -186
  50. build/lib/pyrekordbox/anlz/structs.py +0 -299
  51. build/lib/pyrekordbox/anlz/tags.py +0 -508
  52. build/lib/pyrekordbox/config.py +0 -596
  53. build/lib/pyrekordbox/db6/__init__.py +0 -45
  54. build/lib/pyrekordbox/db6/aux_files.py +0 -213
  55. build/lib/pyrekordbox/db6/database.py +0 -1808
  56. build/lib/pyrekordbox/db6/registry.py +0 -304
  57. build/lib/pyrekordbox/db6/tables.py +0 -1618
  58. build/lib/pyrekordbox/logger.py +0 -23
  59. build/lib/pyrekordbox/mysettings/__init__.py +0 -32
  60. build/lib/pyrekordbox/mysettings/file.py +0 -369
  61. build/lib/pyrekordbox/mysettings/structs.py +0 -282
  62. build/lib/pyrekordbox/utils.py +0 -162
  63. build/lib/pyrekordbox/xml.py +0 -1294
  64. build/lib/tests/__init__.py +0 -3
  65. build/lib/tests/test_anlz.py +0 -206
  66. build/lib/tests/test_db6.py +0 -1039
  67. build/lib/tests/test_mysetting.py +0 -203
  68. build/lib/tests/test_xml.py +0 -629
  69. pyrekordbox-0.2.1.dist-info/RECORD +0 -129
  70. {pyrekordbox-0.2.1.dist-info → pyrekordbox-0.2.2.dist-info}/LICENSE +0 -0
  71. {pyrekordbox-0.2.1.dist-info → pyrekordbox-0.2.2.dist-info}/WHEEL +0 -0
@@ -247,7 +247,7 @@ Rekordbox Performance Mode, but starting with Rekordbox version 6 it also gets
247
247
  exported to external media so CDJ-3000 players can use it to control lighting looks.
248
248
 
249
249
  ```{note}
250
- The version that Rekordbox 6 exports is garbled with an XOR mask to make it
250
+ The version that Rekordbox 6 **exports** is garbled with an XOR mask to make it
251
251
  more difficult to access the data. All bytes after `len_e` are XOR-masked with a
252
252
  pattern that is generated by adding the value of `len_e` to each byte of the following
253
253
  base pattern:
@@ -270,14 +270,16 @@ entries are present in the tag. Each entry represents one recognized phrase.
270
270
 
271
271
  The value `mood` specifies the overall type of phrase structure that rekordbox chose to
272
272
  represent the song, based on its analysis of the audio.
273
- The value 1 is a “high” mood
274
- where the phrase types consist of “Intro”, “Up”, “Down”, “Chorus”, and “Outro”.
275
- Other values in each phrase entry cause the intro, chorus, and outro phrases to have
276
- their labels subdivided into styes “1” or “2” (for example, “Intro 1”), and “up” is
277
- subdivided into style “Up 1”, “Up 2”, or “Up 3”. See the table below for an expanded
278
- version of this description.
273
+
274
+ The value 1 is a “high” mood where the phrase types consist of “Intro”, “Up”, “Down”,
275
+ “Chorus”, and “Outro”. Other values in each phrase entry cause the intro, chorus, and
276
+ outro phrases to have their labels subdivided into styes “1” or “2”
277
+ (for example, “Intro 1”), and “up” is subdivided into style “Up 1”, “Up 2”, or “Up 3”.
278
+ See the table below for an expanded version of this description.
279
+
279
280
  The value 2 is a “mid” mood where the phrase types are labeled “Intro”, “Verse 1”
280
281
  through “Verse 6”, “Chorus”, “Bridge”, and “Outro”.
282
+
281
283
  And value 3 is a “low” mood where the phrase types are labeled “Intro”, “Verse 1”,
282
284
  “Verse 2”, “Chorus”, “Bridge”, and “Outro”. There are three different phrase type
283
285
  values for each of “Verse 1” and “Verse 2”, but rekordbox makes no distinction between
@@ -301,6 +303,175 @@ Each phrase entry has the structure shown below:
301
303
  Song structure entry.
302
304
  ```
303
305
 
306
+ The first two bytes of each song structure entry hold `index`, which numbers each phrase,
307
+ starting at one and incrementing with each entry. That is followed by beat,
308
+ a two-byte value that specifies the beat at which this phrase begins in the track.
309
+ It continues until either the beat number of the next phrase, or the beat identified
310
+ by end in the tag header if this is the last entry.
311
+
312
+ `kind` specifies what kind of phrase rekordbox has identified here.
313
+ The interpretation depends on the value of mood in the tag header, as is detailed the
314
+ table below. In the case of the “high” mood, there are numbered variations for some
315
+ of the phrases displayed in rekordbox that are not reflected in kind, but depend on the
316
+ values of three flag bytes `k1` through `k3` in a complicated way shown in its own table.
317
+
318
+ We also noticed that when `mood`, `kind` and the `k` flags indicate a phrase of type
319
+ “Up 3”, additional beat numbers (which all fall within the phrase) are present in the
320
+ entry. These may indicate points within the phrase at which lighting changes would look good;
321
+ more investigation is required to make sense of them.
322
+ The number of beats that will be listed seems to depend on the value of the flag `b`:
323
+ if this has the value 0, there will be a single beat found in `beat2`, and if `b` has
324
+ the value 1 there will be three different beat numbers present, with increasing values,
325
+ in `beat2`, `beat3` and `beat4`.
326
+
327
+ `fill` is a flag that indicates whether there are fill (non-phrase) beats at the end of
328
+ the phrase. If it is non-zero, then ``beat fill`` holds the beat number at which the
329
+ fill begins. When fill-in is present, it is indicated in rekordbox by little dots on the
330
+ full waveform. The manual says:
331
+
332
+
333
+ [Fill in] is a section that provides improvisational changes at the end of phrase.
334
+ [Fill in] is detected at the end of Intro, Up, and Chorus (up to 4 beats).
335
+
336
+
337
+
338
+ ```{eval-rst}
339
+ .. list-table:: Phrase labels in each mood.
340
+ :header-rows: 1
341
+
342
+ * - Phrase ID
343
+ - Low Label
344
+ - Mid Label
345
+ - High Label
346
+ * - 1
347
+ - Intro
348
+ - Intro
349
+ - Intro n
350
+ * - 2
351
+ - Verse 1
352
+ - Verse 1
353
+ - Up n
354
+ * - 3
355
+ - Verse 1
356
+ - Verse 2
357
+ - Down
358
+ * - 4
359
+ - Verse 1
360
+ - Verse 3
361
+ -
362
+ * - 5
363
+ - Verse 2
364
+ - Verse 4
365
+ - Chorus n
366
+ * - 6
367
+ - Verse 2
368
+ - Verse 5
369
+ - Outro n
370
+ * - 7
371
+ - Verse 2
372
+ - Verse 6
373
+ -
374
+ * - 8
375
+ - Bridge
376
+ - Bridge
377
+ -
378
+ * - 9
379
+ - Chorus
380
+ - Chorus
381
+ -
382
+ * - 10
383
+ - Outro
384
+ - Outro
385
+ -
386
+ ```
387
+
388
+ ```{eval-rst}
389
+ .. list-table:: High mood phrase variants.
390
+ :header-rows: 1
391
+
392
+ * - Phrase ID
393
+ - k1
394
+ - k2
395
+ - k3
396
+ - Expanded Label
397
+ * - 1
398
+ - 1
399
+ -
400
+ -
401
+ - Intro 1
402
+ * - 1
403
+ - 0
404
+ -
405
+ -
406
+ - Intro 2
407
+ * - 2
408
+ -
409
+ - 0
410
+ - 0
411
+ - Up 1
412
+ * - 2
413
+ -
414
+ - 0
415
+ - 1
416
+ - Up 2
417
+ * - 2
418
+ -
419
+ - 1
420
+ - 0
421
+ - Up 3
422
+ * - 3
423
+ -
424
+ -
425
+ -
426
+ - Down 1
427
+ * - 5
428
+ - 1
429
+ -
430
+ -
431
+ - Chorus 2
432
+ * - 5
433
+ - 0
434
+ -
435
+ -
436
+ - Chorus 1
437
+ * - 6
438
+ - 1
439
+ -
440
+ -
441
+ - Outro 1
442
+ * - 6
443
+ - 0
444
+ -
445
+ -
446
+ - Outro 2
447
+ ```
448
+
449
+ ```{eval-rst}
450
+ .. list-table:: Track banks.
451
+ :header-rows: 1
452
+
453
+ * - Bank ID
454
+ - Label
455
+ * - 0
456
+ - Default (treated as Cool)
457
+ * - 1
458
+ - Cool
459
+ * - 2
460
+ - Natural
461
+ * - 3
462
+ - Hot
463
+ * - 4
464
+ - Subtle
465
+ * - 5
466
+ - Warm
467
+ * - 6
468
+ - Vivid
469
+ * - 7
470
+ - Club 1
471
+ * - 8
472
+ - Club 2
473
+ ```
474
+
304
475
  ### PWAV: Waveform Preview Tag
305
476
 
306
477
  Seen in `.DAT` analysis files. This kind of section holds a fixed-width monochrome
@@ -489,7 +489,7 @@ around 1/75th of a second (13.333ms) per frame, i.e. about half the granularity
489
489
  - `0` if not a loop or VBR/ABR MPEG file
490
490
  * - `Kind`
491
491
  - Type of cue point
492
- - Cue= `0` , Fade-In= `0` , Fade-Out= `0` , Load= `3` , Loop= `4`
492
+ - Cue= `0` , Fade-In= `1` , Fade-Out= `2` , Load= `3` , Loop= `4`
493
493
  * - `Color`
494
494
  - The color ID of the cue point
495
495
  - `-1` if no color
docs/source/index.md CHANGED
@@ -28,11 +28,7 @@ Pioneer's Rekordbox DJ Software. It currently supports
28
28
  - Analysis files (ANLZ)
29
29
  - My-Settings files
30
30
 
31
- Tested Rekordbox versions: `5.8.6 | 6.5.3`
32
-
33
- Starting from version `6.6.5` Pioneer obfuscated the `app.asar` file contents, breaking
34
- the key extraction (see [this issue][issue] and the Rekordbox 6 database section for
35
- more details).
31
+ Tested Rekordbox versions: `5.8.6 | 6.5.3 | 6.7.7`
36
32
 
37
33
  ```{warning}
38
34
  This project is still under development and might contain bugs or
@@ -59,10 +55,10 @@ maxdepth: 2
59
55
  caption: File formats
60
56
  ---
61
57
 
58
+ formats/db6
62
59
  formats/xml
63
60
  formats/anlz
64
61
  formats/mysetting
65
- formats/db6
66
62
  ```
67
63
 
68
64
  ```{toctree}
docs/source/quickstart.md CHANGED
@@ -25,18 +25,6 @@ from pyrekordbox import show_config
25
25
 
26
26
  show_config()
27
27
  ````
28
-
29
- which, for example, will print
30
- ````
31
- Pioneer:
32
- app_dir = C:\Users\user\AppData\Roaming\Pioneer
33
- install_dir = C:\Program Files\Pioneer
34
- Rekordbox 5:
35
- app_dir = C:\Users\user\AppData\Roaming\Pioneer\rekordbox
36
- install_dir = C:\Program Files\Pioneer\rekordbox 5.8.6
37
- ...
38
- ````
39
-
40
28
  If for some reason the configuration fails the values can be updated by providing the
41
29
  paths to the directory where Pioneer applications are installed (`pioneer_install_dir`)
42
30
  and to the directory where Pioneer stores the application data (`pioneer_app_dir`)
@@ -45,12 +33,65 @@ from pyrekordbox.config import update_config
45
33
 
46
34
  update_config("<pioneer_install_dir>", "<pioneer_app_dir>")
47
35
  ````
48
-
49
36
  Alternatively the two paths can be specified in a configuration file under the section
50
37
  `rekordbox`. Supported configuration files are pyproject.toml, setup.cfg, pyrekordbox.toml,
51
38
  pyrekordbox.cfg and pyrekordbox.yaml.
52
39
 
53
40
 
41
+ ## Rekordbox 6 database
42
+
43
+ Rekordbox 6 now uses a SQLite database for storing the collection content.
44
+ Unfortunatly, the new `master.db` SQLite database is encrypted using
45
+ [SQLCipher][sqlcipher], which means it can't be used without the encryption key.
46
+ However, since your data is stored and used locally, the key must be present on the
47
+ machine running Rekordbox.
48
+
49
+ Pyrekordbox can unlock the new Rekordbox `master.db` SQLite database and provides
50
+ an easy interface for accessing the data stored in it:
51
+
52
+ ````python
53
+ from pyrekordbox import Rekordbox6Database
54
+
55
+ db = Rekordbox6Database()
56
+
57
+ for content in db.get_content():
58
+ print(content.Title, content.Artist.Name)
59
+
60
+ playlist = db.get_playlist()[0]
61
+ for song in playlist.Songs:
62
+ content = song.Content
63
+ print(content.Title, content.Artist.Name)
64
+ ````
65
+ Fields in the Rekordbox database that are stored without linking to other tables
66
+ can be changed via the corresponding property of the object:
67
+ ````python
68
+ content = db.get_content()[0]
69
+ content.Title = "New Title"
70
+ ````
71
+ Some fields are stored as references to other tables, for example the artist of a track.
72
+ Check the [documentation](#db6-format) of the corresponding object for more information.
73
+ So far only a few tables support adding or deleting entries:
74
+ - ``DjmdPlaylist``: Playlists/Playlist Folders
75
+ - ``DjmdSongPlaylist``: Songs in a playlist
76
+
77
+ ````{important}
78
+ Starting from Rekordbox version ``6.6.5`` Pioneer obfuscated the ``app.asar`` file
79
+ contents, breaking the key extraction (see [this discussion](https://github.com/dylanljones/pyrekordbox/discussions/97) for more details).
80
+ If you are using a later version of Rekorbox and have no cached key from a previous
81
+ version, the database can not be unlocked automatically.
82
+ The command line interface of ``pyrekordbox`` provides a command for downloading
83
+ the key from known sources and writing it to the cache file:
84
+ ```shell
85
+ python -m pyrekordbox download-key
86
+ ```
87
+ Once the key is cached the database can be opened without providing the key.
88
+ The key can also be provided manually:
89
+ ```python
90
+ db = Rekordbox6Database(key="<insert key here>")
91
+ ```
92
+ ````
93
+
94
+
54
95
  ## Rekordbox XML
55
96
 
56
97
  The Rekordbox XML database is used for importing (and exporting) Rekordbox collections
@@ -107,6 +148,17 @@ Changing and creating the Rekordbox analysis files is planned as well, but for t
107
148
  full structure of the analysis files has to be understood.
108
149
 
109
150
 
151
+ ```{note}
152
+ Some ANLZ tags are still unsupported:
153
+ - PCOB
154
+ - PCO2
155
+ - PSSI
156
+ - PWV6
157
+ - PWV7
158
+ - PWVC
159
+ ```
160
+
161
+
110
162
  ## Rekordbox My-Settings
111
163
 
112
164
  Rekordbox stores the user settings in `*SETTING.DAT` files, which get exported to USB
@@ -126,37 +178,8 @@ sync = mysett.get("sync")
126
178
  quant = mysett.get("quantize")
127
179
  ````
128
180
 
129
-
130
- ## Rekordbox 6 database
131
-
132
- Rekordbox 6 now uses a SQLite database for storing the collection content.
133
- Unfortunatly, the new `master.db` SQLite database is encrypted using
134
- [SQLCipher][sqlcipher], which means it can't be used without the encryption key.
135
- However, since your data is stored and used locally, the key must be present on the
136
- machine running Rekordbox.
137
-
138
- Pyrekordbox can unlock the new Rekordbox `master.db` SQLite database and provides
139
- an easy interface for accessing the data stored in it:
140
-
141
- ````python
142
- from pyrekordbox import Rekordbox6Database
143
-
144
- db = Rekordbox6Database()
145
-
146
- for content in db.get_content():
147
- print(content.Title, content.Artist.Name)
148
-
149
- playlist = db.get_playlist()[0]
150
- for song in playlist.Songs:
151
- content = song.Content
152
- print(content.Title, content.Artist.Name)
153
- ````
154
- Adding new rows to the tables of the database is not supported since it is not yet known
155
- how Rekordbox generates the UUID/ID's. Using wrong values for new database entries
156
- could corrupt the library. This feature will be added after some testing.
157
- Changing existing entries like the title, artist or file path of a track in the database
158
- should work as expected.
159
-
160
-
181
+ ```{note}
182
+ The `DEVSETTING.DAT` file is still not supported
183
+ ```
161
184
 
162
185
  [sqlcipher]: https://www.zetetic.net/sqlcipher/open-source/
@@ -13,8 +13,8 @@ maxdepth: 3
13
13
  ---
14
14
 
15
15
  configuration
16
+ db6
16
17
  xml
17
18
  anlz
18
19
  mysetting
19
- db6
20
20
  ````
pyrekordbox/__init__.py CHANGED
@@ -3,7 +3,7 @@
3
3
  # Date: 2022-04-10
4
4
 
5
5
  from .logger import logger
6
- from .config import show_config, get_config
6
+ from .config import show_config, get_config, update_config
7
7
  from .xml import RekordboxXml, XmlDuplicateError, XmlAttributeKeyError
8
8
  from .anlz import get_anlz_paths, walk_anlz_paths, read_anlz_files, AnlzFile
9
9
  from .mysettings import (
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.2.1'
16
- __version_tuple__ = version_tuple = (0, 2, 1)
15
+ __version__ = version = '0.2.2'
16
+ __version_tuple__ = version_tuple = (0, 2, 2)
pyrekordbox/anlz/file.py CHANGED
@@ -8,9 +8,12 @@ from pathlib import Path
8
8
  from typing import Union
9
9
  from .tags import TAGS
10
10
  from . import structs
11
+ from construct import Int16ub
11
12
 
12
13
  logger = logging.getLogger(__name__)
13
14
 
15
+ XOR_MASK = bytearray.fromhex("CB E1 EE FA E5 EE AD EE E9 D2 E9 EB E1 E9 F3 E8 E9 F4 E1")
16
+
14
17
 
15
18
  class BuildFileLengthError(Exception):
16
19
  def __init__(self, struct, len_data):
@@ -103,6 +106,31 @@ class AnlzFile(abc.Mapping):
103
106
  # Get the four byte struct type
104
107
  tag_type = tag_data[:4].decode("ascii")
105
108
 
109
+ if tag_type == "PSSI":
110
+ # The version that rekordbox 6 *exports* is garbled with an XOR mask.
111
+ # All bytes after byte 17 (len_e) are XOR-masked with a pattern that is
112
+ # generated by adding the value of len_e to each byte of XOR_MASK
113
+
114
+ # Check if the file is garbled (only on exported files)
115
+ # For this we check the validity of mood and bank
116
+ # Mood: High=1, Mid=2, Low=3
117
+ # Bank: 1-8
118
+ mood = Int16ub.parse(tag_data[18:20])
119
+ bank = Int16ub.parse(tag_data[28:30])
120
+ if 1 <= mood <= 3 and 1 <= bank <= 8:
121
+ logger.debug("PSSI is not garbled!")
122
+ else:
123
+ logger.debug("PSSI is garbled!")
124
+ # deobfuscate tag_data[18:] using xor with XOR_MASK+len_entries
125
+ len_entries = Int16ub.parse(tag_data[16:])
126
+ tag_data = bytearray(data[i : i + len(tag_data)])
127
+ for x in range(len(tag_data[18:])):
128
+ mask = XOR_MASK[x % len(XOR_MASK)] + len_entries
129
+ if mask > 255:
130
+ mask -= 256
131
+ tag_data[x + 18] ^= mask
132
+ tag_data = bytes(tag_data)
133
+
106
134
  try:
107
135
  # Parse the struct
108
136
  tag = TAGS[tag_type](tag_data)
@@ -178,6 +206,17 @@ class AnlzFile(abc.Mapping):
178
206
  else:
179
207
  return [tag for tag in self.tags if tag.name == item]
180
208
 
209
+ def __contains__(self, item):
210
+ if item.isupper() and len(item) == 4:
211
+ for tag in self.tags:
212
+ if item == tag.type:
213
+ return True
214
+ else:
215
+ for tag in self.tags:
216
+ if item == tag.name:
217
+ return True
218
+ return False
219
+
181
220
  def __repr__(self):
182
221
  return f"{self.__class__.__name__}({self.tag_types})"
183
222
 
@@ -195,8 +195,6 @@ PWV5 = Struct(
195
195
 
196
196
  # -- Song Structure Tag (PSSI) ---------------------------------------------------------
197
197
 
198
- # FixMe: Implement reverse XOR mask to descramble values
199
-
200
198
  SongStructureEntry = Struct(
201
199
  "index" / Int16ub,
202
200
  "beat" / Int16ub,
@@ -221,11 +219,11 @@ SongStructureEntry = Struct(
221
219
  PSSI = Struct(
222
220
  "len_entry_bytes" / Const(24, Int32ub),
223
221
  "len_entries" / Int16ub,
224
- "mood" / Bytes(2),
222
+ "mood" / Int16ub,
225
223
  "u1" / Bytes(6),
226
- "end_beat" / Bytes(2),
224
+ "end_beat" / Int16ub,
227
225
  "u2" / Bytes(2),
228
- "bank" / Bytes(1),
226
+ "bank" / Int8ub,
229
227
  "u3" / Bytes(1),
230
228
  "entries" / Array(this.len_entries, SongStructureEntry),
231
229
  )
pyrekordbox/config.py CHANGED
@@ -15,6 +15,7 @@ import logging
15
15
  import base64
16
16
  import blowfish
17
17
  import json
18
+ import packaging.version
18
19
  import xml.etree.cElementTree as xml
19
20
  from pathlib import Path
20
21
  from typing import Union
@@ -247,6 +248,7 @@ def _get_rb_config(
247
248
  pioneer_install_dir: Path,
248
249
  pioneer_app_dir: Path,
249
250
  major_version: int,
251
+ application_dirname: str = "",
250
252
  ) -> dict:
251
253
  """Get the program configuration for a given Rekordbox major version.
252
254
 
@@ -258,33 +260,55 @@ def _get_rb_config(
258
260
  The path of the Pioneer application data directory.
259
261
  major_version : int
260
262
  The major version of Rekordbox.
263
+ application_dirname : str, optional
264
+ The name of the Rekordbox application directory. If not given, the latest
265
+ Rekordbox installation directory for major version `major_version` is used.
261
266
 
262
267
  Returns
263
268
  -------
264
269
  config : dict
265
270
  The program configuration.
266
271
  """
267
- # Get latest Rekordbox installation directory for major release `major_version`
268
-
269
- # Find all 'V.x.x' version strings in dir names
270
- versions = list()
271
- for p in pioneer_install_dir.iterdir():
272
- name = p.name
273
- if name.startswith("rekordbox"):
274
- ver_str = name.replace("rekordbox", "").strip()
275
- if ver_str.startswith(str(major_version)):
276
- versions.append(ver_str)
277
- # Get latest 'V.x.x' version string and assure there is one
278
- versions.sort(key=lambda s: list(map(int, s.split("."))))
279
- try:
280
- rb_version = versions[-1]
281
- except IndexError:
282
- raise FileNotFoundError(
283
- f"No Rekordbox {major_version} folder found in installation "
284
- f"directory '{pioneer_install_dir}'"
285
- )
286
- # Name of the Rekordbox application directory in `pioneer_install_dir`
287
- rb_prog_dir = pioneer_install_dir / f"rekordbox {rb_version}"
272
+ if application_dirname:
273
+ # Applitcation dirname is given, only extract version from it
274
+ # `major_version` is compared to the version string
275
+ rb_version = application_dirname.replace("rekordbox", "").strip()
276
+ rb_version = rb_version.replace(".app", "")
277
+ if not rb_version.startswith(str(major_version)):
278
+ raise ValueError(
279
+ f"Major version is {major_version}, but the supplied application "
280
+ f"dirname is '{application_dirname}'"
281
+ )
282
+ rb_prog_dir = pioneer_install_dir / application_dirname
283
+ if not rb_prog_dir.exists():
284
+ raise InvalidApplicationDirname(
285
+ f"The supplied application dirname '{application_dirname}' does not "
286
+ f"exist in '{pioneer_install_dir}'"
287
+ )
288
+ else:
289
+ # Get latest Rekordbox installation directory for major release `major_version`
290
+
291
+ # Find all 'V.x.x' version strings in dir names
292
+ versions = list()
293
+ for p in pioneer_install_dir.iterdir():
294
+ name = p.name
295
+ if name.startswith("rekordbox"):
296
+ ver_str = name.replace("rekordbox", "").strip()
297
+ ver_str = ver_str.replace(".app", "")
298
+ if ver_str.startswith(str(major_version)):
299
+ v = packaging.version.parse(ver_str)
300
+ versions.append(v)
301
+ # Get latest 'V.x.x' version string and assure there is one
302
+ versions.sort() # key=lambda s: list(map(int, s.split("."))))
303
+ try:
304
+ rb_version = str(versions[-1])
305
+ except IndexError:
306
+ raise FileNotFoundError(
307
+ f"No Rekordbox {major_version} folder found in installation "
308
+ f"directory '{pioneer_install_dir}'"
309
+ )
310
+ # Name of the Rekordbox application directory in `pioneer_install_dir`
311
+ rb_prog_dir = pioneer_install_dir / f"rekordbox {rb_version}"
288
312
 
289
313
  # Check installation directory
290
314
  if not rb_prog_dir.exists():
@@ -318,10 +342,12 @@ def _get_rb_config(
318
342
  return conf
319
343
 
320
344
 
321
- def _get_rb5_config(pioneer_prog_dir: Path, pioneer_app_dir: Path) -> dict:
345
+ def _get_rb5_config(
346
+ pioneer_prog_dir: Path, pioneer_app_dir: Path, dirname: str = ""
347
+ ) -> dict:
322
348
  """Get the program configuration for Rekordbox v5.x.x."""
323
349
  major_version = 5
324
- conf = _get_rb_config(pioneer_prog_dir, pioneer_app_dir, major_version)
350
+ conf = _get_rb_config(pioneer_prog_dir, pioneer_app_dir, major_version, dirname)
325
351
  return conf
326
352
 
327
353
 
@@ -377,10 +403,12 @@ def write_db6_key_cache(key: str) -> None: # pragma: no cover
377
403
  __config__["rekordbox6"]["dp"] = key
378
404
 
379
405
 
380
- def _get_rb6_config(pioneer_prog_dir: Path, pioneer_app_dir: Path) -> dict:
406
+ def _get_rb6_config(
407
+ pioneer_prog_dir: Path, pioneer_app_dir: Path, dirname: str = ""
408
+ ) -> dict:
381
409
  """Get the program configuration for Rekordbox v6.x.x."""
382
410
  major_version = 6
383
- conf = _get_rb_config(pioneer_prog_dir, pioneer_app_dir, major_version)
411
+ conf = _get_rb_config(pioneer_prog_dir, pioneer_app_dir, major_version, dirname)
384
412
 
385
413
  # Read Rekordbox 6 'options.json' and check db_path
386
414
  opts = read_rekordbox6_options(pioneer_app_dir)
@@ -488,6 +516,8 @@ def read_pyrekordbox_configuration():
488
516
  def update_config(
489
517
  pioneer_install_dir: Union[str, Path] = None,
490
518
  pioneer_app_dir: Union[str, Path] = None,
519
+ rb5_install_dirname: str = "",
520
+ rb6_install_dirname: str = "",
491
521
  ):
492
522
  """Update the pyrekordbox configuration.
493
523
 
@@ -508,6 +538,12 @@ def update_config(
508
538
  The path to the Pioneer application directory. This is where the application
509
539
  user data of Pioneer programs is stored. By default, the normal location of
510
540
  the Pioneer application data is used.
541
+ rb5_install_dirname : str, optional
542
+ The name of the Rekordbox 5 installation directory. By default, the normal
543
+ directory name is used (Windows: 'rekordbox 5.x.x', macOS: 'rekordbox 5.app').
544
+ rb6_install_dirname : str, optional
545
+ The name of the Rekordbox 6 installation directory. By default, the normal
546
+ directory name is used (Windows: 'rekordbox 6.x.x', macOS: 'rekordbox 6.app').
511
547
  """
512
548
  # Read config file
513
549
  conf = read_pyrekordbox_configuration()
@@ -515,6 +551,10 @@ def update_config(
515
551
  pioneer_install_dir = conf["pioneer-install-dir"]
516
552
  if pioneer_app_dir is None and "pioneer-app-dir" in conf:
517
553
  pioneer_app_dir = conf["pioneer-app-dir"]
554
+ if not rb5_install_dirname and "rekordbox5-install-dirname" in conf:
555
+ rb5_install_dirname = conf["rekordbox5-install-dirname"]
556
+ if not rb6_install_dirname and "rekordbox6-install-dirname" in conf:
557
+ rb6_install_dirname = conf["rekordbox6-install-dirname"]
518
558
 
519
559
  # Pioneer installation directory
520
560
  try:
@@ -534,14 +574,18 @@ def update_config(
534
574
 
535
575
  # Update Rekordbox 5 config
536
576
  try:
537
- conf = _get_rb5_config(pioneer_install_dir, pioneer_app_dir)
577
+ conf = _get_rb5_config(
578
+ pioneer_install_dir, pioneer_app_dir, rb5_install_dirname
579
+ )
538
580
  __config__["rekordbox5"].update(conf)
539
581
  except FileNotFoundError as e:
540
582
  logger.info(e)
541
583
 
542
584
  # Update Rekordbox 6 config
543
585
  try:
544
- conf = _get_rb6_config(pioneer_install_dir, pioneer_app_dir)
586
+ conf = _get_rb6_config(
587
+ pioneer_install_dir, pioneer_app_dir, rb6_install_dirname
588
+ )
545
589
  __config__["rekordbox6"].update(conf)
546
590
  except FileNotFoundError as e:
547
591
  logger.info(e)