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.
- docs/source/formats/anlz.md +178 -7
- docs/source/formats/db6.md +1 -1
- docs/source/index.md +2 -6
- docs/source/quickstart.md +68 -45
- docs/source/tutorial/index.md +1 -1
- pyrekordbox/__init__.py +1 -1
- pyrekordbox/_version.py +2 -2
- pyrekordbox/anlz/file.py +39 -0
- pyrekordbox/anlz/structs.py +3 -5
- pyrekordbox/config.py +71 -27
- pyrekordbox/db6/database.py +260 -33
- pyrekordbox/db6/registry.py +22 -0
- pyrekordbox/db6/tables.py +3 -4
- {pyrekordbox-0.2.1.dist-info → pyrekordbox-0.2.2.dist-info}/METADATA +12 -11
- pyrekordbox-0.2.2.dist-info/RECORD +80 -0
- {pyrekordbox-0.2.1.dist-info → pyrekordbox-0.2.2.dist-info}/top_level.txt +0 -2
- tests/test_config.py +175 -0
- tests/test_db6.py +78 -0
- build/lib/build/lib/docs/source/conf.py +0 -178
- build/lib/build/lib/pyrekordbox/__init__.py +0 -22
- build/lib/build/lib/pyrekordbox/__main__.py +0 -204
- build/lib/build/lib/pyrekordbox/_version.py +0 -16
- build/lib/build/lib/pyrekordbox/anlz/__init__.py +0 -127
- build/lib/build/lib/pyrekordbox/anlz/file.py +0 -186
- build/lib/build/lib/pyrekordbox/anlz/structs.py +0 -299
- build/lib/build/lib/pyrekordbox/anlz/tags.py +0 -508
- build/lib/build/lib/pyrekordbox/config.py +0 -596
- build/lib/build/lib/pyrekordbox/db6/__init__.py +0 -45
- build/lib/build/lib/pyrekordbox/db6/aux_files.py +0 -213
- build/lib/build/lib/pyrekordbox/db6/database.py +0 -1808
- build/lib/build/lib/pyrekordbox/db6/registry.py +0 -304
- build/lib/build/lib/pyrekordbox/db6/tables.py +0 -1618
- build/lib/build/lib/pyrekordbox/logger.py +0 -23
- build/lib/build/lib/pyrekordbox/mysettings/__init__.py +0 -32
- build/lib/build/lib/pyrekordbox/mysettings/file.py +0 -369
- build/lib/build/lib/pyrekordbox/mysettings/structs.py +0 -282
- build/lib/build/lib/pyrekordbox/utils.py +0 -162
- build/lib/build/lib/pyrekordbox/xml.py +0 -1294
- build/lib/build/lib/tests/__init__.py +0 -3
- build/lib/build/lib/tests/test_anlz.py +0 -206
- build/lib/build/lib/tests/test_db6.py +0 -1039
- build/lib/build/lib/tests/test_mysetting.py +0 -203
- build/lib/build/lib/tests/test_xml.py +0 -629
- build/lib/docs/source/conf.py +0 -178
- build/lib/pyrekordbox/__init__.py +0 -22
- build/lib/pyrekordbox/__main__.py +0 -204
- build/lib/pyrekordbox/_version.py +0 -16
- build/lib/pyrekordbox/anlz/__init__.py +0 -127
- build/lib/pyrekordbox/anlz/file.py +0 -186
- build/lib/pyrekordbox/anlz/structs.py +0 -299
- build/lib/pyrekordbox/anlz/tags.py +0 -508
- build/lib/pyrekordbox/config.py +0 -596
- build/lib/pyrekordbox/db6/__init__.py +0 -45
- build/lib/pyrekordbox/db6/aux_files.py +0 -213
- build/lib/pyrekordbox/db6/database.py +0 -1808
- build/lib/pyrekordbox/db6/registry.py +0 -304
- build/lib/pyrekordbox/db6/tables.py +0 -1618
- build/lib/pyrekordbox/logger.py +0 -23
- build/lib/pyrekordbox/mysettings/__init__.py +0 -32
- build/lib/pyrekordbox/mysettings/file.py +0 -369
- build/lib/pyrekordbox/mysettings/structs.py +0 -282
- build/lib/pyrekordbox/utils.py +0 -162
- build/lib/pyrekordbox/xml.py +0 -1294
- build/lib/tests/__init__.py +0 -3
- build/lib/tests/test_anlz.py +0 -206
- build/lib/tests/test_db6.py +0 -1039
- build/lib/tests/test_mysetting.py +0 -203
- build/lib/tests/test_xml.py +0 -629
- pyrekordbox-0.2.1.dist-info/RECORD +0 -129
- {pyrekordbox-0.2.1.dist-info → pyrekordbox-0.2.2.dist-info}/LICENSE +0 -0
- {pyrekordbox-0.2.1.dist-info → pyrekordbox-0.2.2.dist-info}/WHEEL +0 -0
docs/source/formats/anlz.md
CHANGED
@@ -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
|
-
|
274
|
-
where the phrase types consist of “Intro”, “Up”, “Down”,
|
275
|
-
Other values in each phrase entry cause the intro, chorus, and
|
276
|
-
their labels subdivided into styes “1” or “2”
|
277
|
-
subdivided into style “Up 1”, “Up 2”, or “Up 3”.
|
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
|
docs/source/formats/db6.md
CHANGED
@@ -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= `
|
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
|
-
|
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/
|
docs/source/tutorial/index.md
CHANGED
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
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
|
|
pyrekordbox/anlz/structs.py
CHANGED
@@ -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" /
|
222
|
+
"mood" / Int16ub,
|
225
223
|
"u1" / Bytes(6),
|
226
|
-
"end_beat" /
|
224
|
+
"end_beat" / Int16ub,
|
227
225
|
"u2" / Bytes(2),
|
228
|
-
"bank" /
|
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
|
-
|
268
|
-
|
269
|
-
|
270
|
-
|
271
|
-
|
272
|
-
|
273
|
-
|
274
|
-
|
275
|
-
|
276
|
-
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
|
284
|
-
|
285
|
-
|
286
|
-
|
287
|
-
|
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(
|
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(
|
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(
|
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(
|
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)
|