the downfall has begun
This commit is contained in:
1 parent
f71a2a3512
commit
9ccc3cea95
25 files changed
+4975
-108
No files matched your search
@@ -0,0 +1,50 @@
|
|||||||
|
---
|
||||||
|
name: pdb-maple
|
||||||
|
description: Updates maple's pdb serializer (maple-py/src/maple/pdb/) to match decoded format rules from the kaitai schema + FORMAT_SPEC, then regenerates and diffs the AceMo pdb against rekordbox's. Use after pdb-re records a finding.
|
||||||
|
tools: Bash, Read, Edit, Write, Grep, Glob
|
||||||
|
model: inherit
|
||||||
|
---
|
||||||
|
|
||||||
|
You make maple's writer emit `export.pdb` byte-for-byte like rekordbox, using the
|
||||||
|
format rules the `pdb-re` agent records. You implement the writer; you do NOT
|
||||||
|
reverse-engineer the binary.
|
||||||
|
|
||||||
|
## Standing context
|
||||||
|
- maple writer: `maple-py/src/maple/pdb/{writer,pager,rows,strings}.py`, model in
|
||||||
|
`maple-py/src/maple/model.py`.
|
||||||
|
- Format truth (your inputs): `maple-py/src/kaitai/rekordbox_pdb.py` (layout/types)
|
||||||
|
and `maple-py/re/FORMAT_SPEC.md` (value-construction rules). Read both each run.
|
||||||
|
- Reference real export: `maple-py/data/acemo/PIONEER/rekordbox/export.pdb`.
|
||||||
|
- Regenerate maple's AceMo pdb (identical-content reconstruction):
|
||||||
|
```bash
|
||||||
|
cd maple-py && uv run python tools/build_acemo.py # -> captures/maple_acemo.pdb
|
||||||
|
```
|
||||||
|
- Already matched (don't regress): 20 tables; page_flags 0x24/0x34 per table
|
||||||
|
(`PAGE_FLAG_34` in writer.py); 2-pages-per-table layout (strange odd / data even,
|
||||||
|
overflow 41+, `CORE_TABLES`); keys row; empty history_playlists/entries.
|
||||||
|
|
||||||
|
## Your loop, per finding
|
||||||
|
1. Read the latest `FORMAT_SPEC.md` rule + any changed kaitai fields.
|
||||||
|
2. Implement it in the writer (`rows.py` for row fields, `pager.py`/`writer.py`
|
||||||
|
for page/layout). Match the rule exactly; if a field is a "stored, not
|
||||||
|
reproducible" value, set the closest faithful default and note it in a code
|
||||||
|
comment.
|
||||||
|
3. `uv run python tools/build_acemo.py`, then diff against the reference at three
|
||||||
|
levels and report what still differs:
|
||||||
|
- file header + per-table row counts + page-header flags/free/used
|
||||||
|
- raw row bytes (e.g. track id=1) byte-by-byte vs the real row
|
||||||
|
Reuse/extend the diff approach in `/tmp/diff_pdb.py` + `/tmp/rowdiff.py` (or
|
||||||
|
write equivalents). `uv run pytest -q` must still pass.
|
||||||
|
4. Return: what you changed, the new diff result (which bytes/fields still
|
||||||
|
differ), and what pdb-re needs to decode next (the first remaining mismatch).
|
||||||
|
|
||||||
|
Goal: ZERO diff between `captures/maple_acemo.pdb` and the reference. Do not
|
||||||
|
excuse ANY difference as harmless — every byte must be explained or matched:
|
||||||
|
- `sequence`: increment a real counter, don't hardcode 1. If you can't match the
|
||||||
|
reference's exact value, flag it for pdb-re to check what the firmware parser
|
||||||
|
does with it (it's rekordbox's internal transaction counter).
|
||||||
|
- page layout / `next_unused` / overflow-page placement: match it. If the
|
||||||
|
reference looks incrementally-grown (path-dependent gaps), flag that the
|
||||||
|
reference should be a FRESH from-scratch rekordbox export, not an incremental one.
|
||||||
|
- density / free / used: investigate, never assume harmless.
|
||||||
|
Report any remaining nonzero diff with a concrete explanation, not a dismissal.
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
---
|
||||||
|
name: pdb-re
|
||||||
|
description: Reverse-engineers rekordbox export.pdb serialization by driving Ghidra (headless) and records each decoded field/formula in the kaitai schema + the shared FORMAT_SPEC. Use to decode an unknown/wrong pdb field.
|
||||||
|
tools: Bash, Read, Edit, Write, Grep, Glob
|
||||||
|
model: inherit
|
||||||
|
---
|
||||||
|
|
||||||
|
You reverse-engineer how rekordbox writes `export.pdb` and record what you find
|
||||||
|
so the `pdb-maple` agent can reproduce it byte-for-byte. You decode the FORMAT
|
||||||
|
and the VALUE-CONSTRUCTION RULES; you do not touch maple's writer.
|
||||||
|
|
||||||
|
## Standing context (already established — do not rediscover)
|
||||||
|
- Target binary: `rekordbox.exe` (89MB, JUCE C++). `export.pdb` is written by its
|
||||||
|
bundled **DeviceSQL "Page Manager"** (`dsql*`/`edb_dyn_*`).
|
||||||
|
- Analyzed Ghidra project: `~/ghidra_proj` (project `rbx`, program `rekordbox.exe`),
|
||||||
|
already imported + analyzed. JDK at `~/jdk/jdk-21.0.11+10`. Ghidra at
|
||||||
|
`/mnt/c/Users/ntr/code/ghidra_12.1.2_PUBLIC_20260605/ghidra_12.1.2_PUBLIC`.
|
||||||
|
- Run a Ghidra script (no re-analysis):
|
||||||
|
```bash
|
||||||
|
export JAVA_HOME=~/jdk/jdk-21.0.11+10; export GHIDRA_HEADLESS_MAXMEM=10G
|
||||||
|
GH=/mnt/c/Users/ntr/code/ghidra_12.1.2_PUBLIC_20260605/ghidra_12.1.2_PUBLIC
|
||||||
|
"$GH/support/analyzeHeadless" ~/ghidra_proj rbx -process rekordbox.exe -noanalysis \
|
||||||
|
-scriptPath ~/ghidra_scripts -postScript DecPdb.java
|
||||||
|
```
|
||||||
|
Edit `~/ghidra_scripts/DecPdb.java`'s ADDRS to decompile any addresses; output
|
||||||
|
→ `~/ghidra_proj/decpdb.txt`. Other scripts: FindPdb, CallTree, FindHash. Copies
|
||||||
|
also live in `maple-py/re/scripts/`.
|
||||||
|
- Address rebasing: Ghidra static = `0x140000000 + module_offset`. Frida traces
|
||||||
|
print `rekordbox.exe+0xOFFSET` (the agent `maple-py/tools/rbx_pdb_capture.js`);
|
||||||
|
never hardcode the ASLR runtime base.
|
||||||
|
- The serializer call stack is in `~/ghidra_proj/decpdb.txt` / `re/findings/`.
|
||||||
|
- Reference real export: `maple-py/data/acemo/PIONEER/rekordbox/export.pdb` (6
|
||||||
|
tracks). Parse with the kaitai schema (`maple-py/src/kaitai/rekordbox_pdb.py`)
|
||||||
|
via `uv run` from `maple-py/`.
|
||||||
|
|
||||||
|
## Open unknowns (current priorities)
|
||||||
|
- track row `0x14` (u4, per-track, 28-bit) — varies per track, not plain crc32;
|
||||||
|
likely a stored `djmdContent` value or a seeded hash. `0x18`/`0x1a` = per-export
|
||||||
|
constant.
|
||||||
|
- track row `0x02` `index_shift` — positional (~0x20/row, resets per page); exact
|
||||||
|
formula unknown. maple writes 0.
|
||||||
|
|
||||||
|
## Your loop, per field
|
||||||
|
1. Pick the unknown/wrong field you were asked about.
|
||||||
|
2. Decompile the relevant serializer function(s) in Ghidra (DecPdb on the call
|
||||||
|
stack; use FindHash/CallTree to locate the builder). Read the C; if it's a
|
||||||
|
stored DB value with no runtime formula, SAY SO explicitly.
|
||||||
|
3. Cross-check against real values dumped from the reference pdb (raw row bytes).
|
||||||
|
4. Record the finding in BOTH:
|
||||||
|
- `maple-py/src/kaitai/rekordbox_pdb.py` — rename the field / fix its type if
|
||||||
|
the layout was wrong.
|
||||||
|
- `maple-py/re/FORMAT_SPEC.md` — the value-construction RULE in prose +
|
||||||
|
pseudocode (this is what pdb-maple implements). Create the file if missing.
|
||||||
|
5. Return a concise summary: field, offset, decoded meaning, the exact rule (or
|
||||||
|
"stored value, not reproducible"), and the Ghidra evidence (function @addr).
|
||||||
|
|
||||||
|
Be rigorous: prefer reading the decompiled code over guessing. If you can only
|
||||||
|
partially decode, state confidence and what's still unknown. Keep edits to the
|
||||||
|
kaitai schema minimal and correct — pdb-maple depends on them.
|
||||||
@@ -46,14 +46,6 @@ usb-reset dst="/Volumes/MAPLE":
|
|||||||
COPYFILE_DISABLE=1 rsync -rtvh --no-perms --no-owner --no-group --modify-window=2 maple-py/data/MAPLE/Contents/ {{dst}}/Contents/
|
COPYFILE_DISABLE=1 rsync -rtvh --no-perms --no-owner --no-group --modify-window=2 maple-py/data/MAPLE/Contents/ {{dst}}/Contents/
|
||||||
COPYFILE_DISABLE=1 rsync -rtvh --no-perms --no-owner --no-group --modify-window=2 maple-py/data/MAPLE/PIONEER/ {{dst}}/PIONEER/
|
COPYFILE_DISABLE=1 rsync -rtvh --no-perms --no-owner --no-group --modify-window=2 maple-py/data/MAPLE/PIONEER/ {{dst}}/PIONEER/
|
||||||
|
|
||||||
# Build the maple AceMo export.pdb and sync it onto the Windows USB (drive F: by
|
|
||||||
# default) over rekordbox's export.pdb, so it can be tested on the player against
|
|
||||||
# the audio/USBANLZ already on the stick. Runs from WSL via powershell.exe.
|
|
||||||
usb-win dst="/mnt/f":
|
|
||||||
cd maple-py && uv run python tools/build_acemo.py
|
|
||||||
@test -d {{dst}}/PIONEER/rekordbox || (echo "no {{dst}}/PIONEER/rekordbox - is the USB mounted at {{dst}}?"; exit 1)
|
|
||||||
rsync -tvh --no-perms --no-owner --no-group maple-py/captures/maple_acemo.pdb {{dst}}/PIONEER/rekordbox/export.pdb
|
|
||||||
|
|
||||||
maple-setup:
|
maple-setup:
|
||||||
bash ./etc/maple_setup.sh
|
bash ./etc/maple_setup.sh
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ dependencies = [
|
|||||||
"kaitaistruct>=0.11",
|
"kaitaistruct>=0.11",
|
||||||
"mutagen>=1.47.0",
|
"mutagen>=1.47.0",
|
||||||
"pillow>=12.2.0",
|
"pillow>=12.2.0",
|
||||||
|
"pyrekordbox>=0.4.4",
|
||||||
]
|
]
|
||||||
|
|
||||||
[project.scripts]
|
[project.scripts]
|
||||||
|
|||||||
@@ -0,0 +1,609 @@
|
|||||||
|
# export.pdb value-construction rules (RE findings)
|
||||||
|
|
||||||
|
This file records the exact rules rekordbox uses to populate `export.pdb`
|
||||||
|
fields that maple currently writes wrong or as zero. Each section is a
|
||||||
|
self-contained spec for the `maple` writer to implement. Evidence is from
|
||||||
|
reverse-engineering `rekordbox.exe` (DeviceSQL "Page Manager", `dsql*` /
|
||||||
|
`edb_dyn_*`) in Ghidra plus byte-level analysis of the reference export
|
||||||
|
`maple-py/data/acemo/PIONEER/rekordbox/export.pdb` (6 tracks, 1 album,
|
||||||
|
1 artist, 1 key, 6 artwork).
|
||||||
|
|
||||||
|
Reference-pdb facts used throughout:
|
||||||
|
- `len_page` = 4096.
|
||||||
|
- Row index grows backward from end of page; each group spans 36 (0x24) bytes:
|
||||||
|
`group_base = len_page - group_index*36`. Within a group, `present_flags`
|
||||||
|
(u2) live at `group_base-4`; the row offset for slot `i` (0..15) is the u2 at
|
||||||
|
`group_base-(6+2*i)`; offsets are relative to the heap start (0x28, i.e. end
|
||||||
|
of the 0x28-byte page header).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Track row `index_shift` (offset 0x02, u2) — SOLVED
|
||||||
|
|
||||||
|
**Field:** `TrackRow.index_shift` (also present on artist/album/genre/label/
|
||||||
|
key rows — every row type whose layout begins `subtype:u2, index_shift:u2`).
|
||||||
|
|
||||||
|
**Rule:**
|
||||||
|
|
||||||
|
```
|
||||||
|
index_shift = slot_index * 0x20 # i.e. slot_index << 5
|
||||||
|
```
|
||||||
|
|
||||||
|
where `slot_index` is the row's **0-based position in the page row index**
|
||||||
|
(`group_index*16 + slot_within_group`), NOT its ordinal among present rows and
|
||||||
|
NOT its position in the heap. It resets to 0 at the start of every data page.
|
||||||
|
|
||||||
|
Key consequences for the writer:
|
||||||
|
- The first row index slot (slot 0) gets `index_shift = 0`.
|
||||||
|
- Deleted/absent slots still consume a slot number, so a gap in
|
||||||
|
`present_flags` produces a gap in `index_shift`. (See evidence: the AceMo
|
||||||
|
page has slot 5 deleted, so the row in slot 6 has `index_shift = 0xc0`,
|
||||||
|
skipping `0xa0`.)
|
||||||
|
- `index_shift` is purely a function of the row's slot, so the value is fully
|
||||||
|
reproducible from the page layout maple already controls — there is no
|
||||||
|
external/runtime input. If maple writes each track to slot `i`, set
|
||||||
|
`index_shift = i*0x20`.
|
||||||
|
|
||||||
|
**Evidence:**
|
||||||
|
- Reference AceMo `tracks` page (page 2): present slots are 1,2,3,4,6 (slots 0
|
||||||
|
and 5 are deleted), with `index_shift` = 0x20,0x40,0x60,0x80,0xc0
|
||||||
|
respectively. Overflow page (page 48) has one row in slot 1 →
|
||||||
|
`index_shift = 0x20`. The 0x80→0xc0 jump across the deleted slot 5 proves the
|
||||||
|
value tracks `slot_index`, not present-count.
|
||||||
|
- Cross-checked against `artists` (page 5) and `albums` (page 7) rows: every
|
||||||
|
row satisfies `index_shift == slot_index*0x20` with **0 mismatches** over all
|
||||||
|
present rows in the reference file
|
||||||
|
(`maple-py/re/scripts`-style raw parse, 8 rows checked, see RE notes).
|
||||||
|
- Ghidra: the value is assigned inside the DeviceSQL Page Manager record-insert
|
||||||
|
path, which is reached only through vtable dispatch
|
||||||
|
(`(**(code**)(*obj + 0x4d0))(obj,...)` etc. in `FUN_141b8b4c0` /
|
||||||
|
`FUN_141cac390`), so there is no single statically-anchored
|
||||||
|
`index_shift = slot<<5` instruction to cite; a constant-scan for `shl ,5`
|
||||||
|
paired with the row-index masks (0x1fff/0x7ff) and stride 0x24 produced only
|
||||||
|
CRT/`printf("%a")` false positives (`FUN_14232a86c`). The empirical rule
|
||||||
|
above is exact and zero-mismatch, so confidence is **high** on the formula;
|
||||||
|
confidence on "no hidden runtime input" is high because the value is fully
|
||||||
|
explained by slot position alone.
|
||||||
|
|
||||||
|
**Confidence:** high.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Track row 0x14 / 0x18 / 0x1a (opaque id blob) — SOLVED (stored value, not a formula)
|
||||||
|
|
||||||
|
**Fields:** `TrackRow.opaque_track_id_lo` (u4 @0x14), `TrackRow.opaque_seed_lo`
|
||||||
|
(u2 @0x18), `TrackRow.opaque_seed_hi` (u2 @0x1a). Together these are one
|
||||||
|
8-byte little-endian value:
|
||||||
|
|
||||||
|
```
|
||||||
|
blob_u64 = opaque_track_id_lo | (opaque_seed << 32)
|
||||||
|
opaque_seed = opaque_seed_lo | (opaque_seed_hi << 16) # u4 at offset 0x18
|
||||||
|
```
|
||||||
|
|
||||||
|
(@flesniak's schema labels these "Some ID? unknown", "always 19048?",
|
||||||
|
"always 30967?". The "19048"/"30967" = 0x4A68/0x78F7 are only correct for some
|
||||||
|
older exports; they are NOT universal — see below.)
|
||||||
|
|
||||||
|
**What they are (proven empirically across 5 GENUINE rekordbox exports):**
|
||||||
|
|
||||||
|
> NOTE (2026-06-09 correction): a prior draft included a 5th row
|
||||||
|
> "usb (176), seq=1, seed 0x78f74a68, lo all 0" and concluded rekordbox writes
|
||||||
|
> lo=0 / seed 0x78f74a68 for unanalyzed tracks. **That file
|
||||||
|
> (`data/usb/.../export.pdb`) is maple's OWN output, not a rekordbox export**
|
||||||
|
> (it is the compare_pdb.py "ours" path). Reading it back was circular; that
|
||||||
|
> claim is WRONG and has been dropped. Provenance rule: maple output = seed
|
||||||
|
> `0x78f74a68` + ALL per-track lo == 0; genuine rekordbox = per-install nonzero
|
||||||
|
> seed + nonzero per-track lo.
|
||||||
|
|
||||||
|
| export (tracks) | header.seq | opaque_seed | opaque_track_id_lo |
|
||||||
|
|----------------------------|-----------:|---------------|-------------------------------|
|
||||||
|
| acemo (6) | 353 | `0x21b4a7ab` | 6/6 nonzero |
|
||||||
|
| acemo_fresh (7, same lib) | 35 | `0x21b4a7ab` | 7/7 nonzero |
|
||||||
|
| lib_multi (10, same lib) | 56 | `0x21b4a7ab` | 10/10 nonzero |
|
||||||
|
| MAPLE (176, install B) | 546 | `0xd6d3a567` | 176/176 nonzero |
|
||||||
|
| data/PIONEER (847, inst. B)| 4211 | `0xd6d3a567` | 847/847 nonzero |
|
||||||
|
|
||||||
|
Across **all 1046 real tracks in 5 genuine exports: 100% nonzero per-track lo,
|
||||||
|
exactly one nonzero per-install seed per file. There is NO evidence rekordbox
|
||||||
|
ever writes lo=0 or seed `0x78f74a68`.** The lo values span nearly the full
|
||||||
|
28-bit space (`0x0003edeb`..`0x0ffe7f4e`).
|
||||||
|
|
||||||
|
Findings:
|
||||||
|
1. **`opaque_seed` is constant within a single export** (exactly one value per
|
||||||
|
file) and is **per-rekordbox-library/install**: acemo/acemo_fresh/lib_multi
|
||||||
|
are three exports of the same library and all share `0x21b4a7ab`; the separate
|
||||||
|
install behind MAPLE + data/PIONEER uses `0xd6d3a567`. (The old maple hardcode
|
||||||
|
`0xa567`/`0xd6d3` = `0xd6d3a567` was correct only for install B.) The seed is
|
||||||
|
NOT a binary constant (proven: no `0xa7ab`/`0x21b4`/`0xd6d3`/`0xa567`
|
||||||
|
immediate or data-byte sequence in rekordbox.exe), so it is derived at runtime
|
||||||
|
from the per-library DB (most likely a hash/truncation of the master.db's
|
||||||
|
per-library UUID or creation marker). maple cannot reproduce a specific
|
||||||
|
install's seed (see recommendation).
|
||||||
|
2. **`opaque_track_id_lo` is a stable per-track value within a library.** Every
|
||||||
|
track that appears in both the acemo and acemo_fresh exports has an
|
||||||
|
**identical** lo (matched by title): "All My Life"→`0x07ff5a56`,
|
||||||
|
"Smooth Move"→`0x0a20de34`, "Jelly Fish Jam"→`0x0f8fd598`, etc. It is NOT
|
||||||
|
per-export-random.
|
||||||
|
3. **It is NOT derivable from the track's file metadata.** Tested and ruled out
|
||||||
|
(no match, 28-bit and full, utf-8/utf-16, several hash funcs): crc32 / adler32
|
||||||
|
/ md5 / sha1 / fnv1 / fnv1a / djb2 of the title, filename, full
|
||||||
|
`/Contents/...` path, and crc32 of the ANLZ `.DAT`/`.EXT` and the `.mp3`
|
||||||
|
bytes. Also not the USBANLZ folder id, not sequential, not evenly spaced.
|
||||||
|
4. **The values are runtime-derived, not binary constants.** A full
|
||||||
|
instruction-immediate scan of `rekordbox.exe` found NO occurrence of either
|
||||||
|
real seed (`0x21b4a7ab`, `0xd6d3a567`), the per-track lo values, or the
|
||||||
|
track-row `bitmask` constant `0xc0700`. So these come from rekordbox's
|
||||||
|
internal database (master.db / `djmdContent` + a per-library seed), copied
|
||||||
|
into the row by the DeviceSQL record builder (vtable-dispatched, not
|
||||||
|
statically anchorable). (`0x78f74a68` does not appear either — and per the
|
||||||
|
correction above it was never a real rekordbox value here, just maple's
|
||||||
|
placeholder.)
|
||||||
|
|
||||||
|
**Round-2 evidence (2026-06-09, after team-lead supplied acemo_fresh and asked
|
||||||
|
to chase an audio-content hash):**
|
||||||
|
5. **Audio-content-hash hypothesis: DISPROVEN.** Hashed the actual `.mp3`
|
||||||
|
bytes in 8 framings — full file, ID3v2-stripped (audio frames only),
|
||||||
|
first 1k/2k/4k/64k, last 64k, ID3-stripped-first-64k — each with crc32,
|
||||||
|
adler32, md5 (lo/hi, LE+BE), sha1 (LE+BE), sha256, fnv1a, both full-32 and
|
||||||
|
masked-to-28-bit. NONE matched any track's `opaque_track_id_lo`. So it is
|
||||||
|
not a hash of the audio file content (at least not of the file bytes; a hash
|
||||||
|
of decoded PCM cannot be ruled out but there is no evidence for it and it is
|
||||||
|
not reproducible anyway).
|
||||||
|
6. **Source DB confirmed, and it is ENCRYPTED.** Both `exportLibrary.db` (next
|
||||||
|
to the pdb) and the captured `master.db`
|
||||||
|
(`maple-py/captures/p17328_master.db`) fail to open as plain SQLite
|
||||||
|
("file is not a database") — rekordbox 6/7 encrypts them with SQLCipher.
|
||||||
|
The `opaque_track_id_lo` originates there; it cannot be read without the key.
|
||||||
|
7. **The value is a pure stored field, used nowhere else.** It appears in the
|
||||||
|
export.pdb ONLY at each track row's own offset 0x14 (present and stale/deleted
|
||||||
|
rows alike); it does NOT appear in exportExt.pdb (no tag_tracks/ContentID
|
||||||
|
link), nor in the ANLZ `.DAT`/`.EXT` (searched LE+BE), nor does it equal the
|
||||||
|
USBANLZ folder id. So it is not a foreign key the player resolves.
|
||||||
|
8. **Range note (suggestive, not conclusive):** across the 5 real exports the
|
||||||
|
lo values span nearly the full 28-bit space (`0x0003edeb`..`0x0ffe7f4e`),
|
||||||
|
the band of rekordbox-6/7 9-digit `djmdContent.ID` integers, so the most
|
||||||
|
likely identity is "the rekordbox internal Content ID" — a stored
|
||||||
|
random/sequential DB id, NOT a content-derived hash. Consistent with every
|
||||||
|
other finding and with it being unreadable (encrypted DB).
|
||||||
|
|
||||||
|
**Conclusion:** this 8-byte blob is a **stored rekordbox-internal id (most
|
||||||
|
likely the encrypted-DB Content ID), not a function of the track metadata or
|
||||||
|
audio bytes maple has. It cannot be reproduced byte-for-byte** for an arbitrary
|
||||||
|
track without rekordbox's own (encrypted) DB. The audio-content-hash idea is
|
||||||
|
ruled out. There is **no "unanalyzed/zero" form** in any real export — every
|
||||||
|
real track carries a nonzero seed and a nonzero lo.
|
||||||
|
|
||||||
|
**Recommendation for maple's writer:**
|
||||||
|
- Players/crate-digger treat this blob as opaque (never decoded) and play fine
|
||||||
|
regardless — so any value works for playback.
|
||||||
|
- For maximum faithfulness to genuine rekordbox output (which never has
|
||||||
|
lo=0/seed `0x78f74a68`), maple should write a **nonzero per-install seed and a
|
||||||
|
nonzero, stable per-track lo**:
|
||||||
|
- `opaque_seed`: use a real per-install seed. If maple has no install of its
|
||||||
|
own, `0x21b4a7ab` (this RE machine's install) is a known-real value;
|
||||||
|
keep it constant across all tracks in one export. Do NOT use `0x78f74a68`
|
||||||
|
(that was maple's own placeholder, not a rekordbox value).
|
||||||
|
- `opaque_track_id_lo`: a nonzero 28-bit value, stable per track. Since the
|
||||||
|
real value is an opaque DB id maple can't recover, a deterministic stand-in
|
||||||
|
(e.g. a 28-bit hash of a stable per-track key, or the track's own pdb id
|
||||||
|
offset into the ContentID band) is fine — the goal is "looks like a real
|
||||||
|
nonzero ContentID", not byte-parity.
|
||||||
|
- Either way these 8 bytes will NOT match a specific reference export
|
||||||
|
byte-for-byte (the real ids live in that install's encrypted DB) — record the
|
||||||
|
divergence in re/KNOWN_DIVERGENCE.md.
|
||||||
|
|
||||||
|
**Confidence:** high (5-export empirical proof: 1046/1046 real tracks nonzero;
|
||||||
|
exhaustive negative hash search incl. audio bytes; binary const-absence). The
|
||||||
|
earlier "write 0 / 0x78f74a68" advice was based on a maple-output file and is
|
||||||
|
retracted.
|
||||||
|
|
||||||
|
**Round-3 (task #10): can pyrekordbox read the real ids out of master.db?**
|
||||||
|
User insight: pyrekordbox holds the SQLCipher key, so the DBs are not truly
|
||||||
|
unreadable. Validated — partly true, but blocked on the data we have:
|
||||||
|
- pyrekordbox 0.4.4 + `sqlcipher3-wheels` installs and wires up correctly. Its
|
||||||
|
master.db key is the single **global** rekordbox-6 key
|
||||||
|
`402fd482c38817c35ffa8ffb8c7d93143b749e7d315df7a81732a1ff43608497`
|
||||||
|
(`deobfuscate(BLOB)`, must start with `402fd`) — same for every install, NOT
|
||||||
|
per-install. So a VALID master.db *would* open and expose `DjmdContent`.
|
||||||
|
- BUT the captured `maple-py/captures/p17328_master.db` is **corrupt/truncated**:
|
||||||
|
1,024,000 bytes but only 4.4% non-zero, and the first 32 KB (the mandatory
|
||||||
|
SQLCipher salt + page 1) is **all zeros**. No key can decrypt a DB with a
|
||||||
|
zeroed salt. `p17328_master.backup.db` is 0 bytes. The Frida NtWriteFile
|
||||||
|
capture only caught scattered later pages, never the header. → unusable.
|
||||||
|
- The genuine `exportLibrary.db` shipped next to export.pdb (valid 16-byte salt
|
||||||
|
`ed0d3a3c…`) does NOT open with the global master.db key (different key
|
||||||
|
scheme) and pyrekordbox has **no exportLibrary.db support**. → can't read it
|
||||||
|
here.
|
||||||
|
- Strong inference (untested for lack of a readable DB): the per-track
|
||||||
|
`opaque_track_id_lo` is almost certainly `DjmdContent.ID` (the 9-digit
|
||||||
|
rekordbox Content ID; our lo range 0x0003edeb..0x0ffe7f4e fits). If maple can
|
||||||
|
open the user's LIVE `%APPDATA%/Pioneer/rekordbox6/master.db` with pyrekordbox
|
||||||
|
(global key), `db.get_content()` would give `(ID, …)` per track, and the blob
|
||||||
|
would become EXACTLY reproducible: `opaque_track_id_lo = DjmdContent.ID & 0xFFFFFFFF`.
|
||||||
|
- The per-install `opaque_seed` (0x21b4a7ab here) is NOT a DjmdContent column;
|
||||||
|
it is one value for the whole export and most likely derived from a
|
||||||
|
master.db-level per-library marker (a USB/library UUID or an `agentRegistry`/
|
||||||
|
`local_usn`-style value). Identifying it needs a readable master.db to inspect
|
||||||
|
candidate columns; unconfirmed.
|
||||||
|
|
||||||
|
**pyrekordbox write / export capability (asked in #10):**
|
||||||
|
- It CAN write the desktop master.db: `add_content`/`add_album`/`add_artist`/
|
||||||
|
`create_playlist`/`commit`, plus `set_local_usn`, `update_content_path`. But
|
||||||
|
`add_content` lets rekordbox ASSIGN the Content ID; there is no API to force a
|
||||||
|
specific ID. So it is a source of real ids, not a way to dictate them.
|
||||||
|
- It does NOT drive a USB export — only `get_device` (read). The actual
|
||||||
|
`export.pdb`/`exportLibrary.db` writing is rekordbox.exe's DeviceSQL engine
|
||||||
|
(the thing maple reimplements). pyrekordbox manipulates only the desktop
|
||||||
|
library DB, not the USB export. So pyrekordbox cannot replace maple's writer;
|
||||||
|
its only use to maple is as a READ source of the real `DjmdContent.ID` (and
|
||||||
|
potentially the seed) from a valid master.db.
|
||||||
|
|
||||||
|
**Net for #10:** the approach is sound (pyrekordbox + global key reads a real
|
||||||
|
master.db → Content IDs), so `opaque_track_id_lo` is likely reproducible *if
|
||||||
|
maple reads the live master.db*. It is NOT reproducible from the corrupt
|
||||||
|
captured DB we have, and the seed source is still unidentified. pyrekordbox
|
||||||
|
cannot produce the USB pdb itself. Recommended next step for maple/RE: point
|
||||||
|
pyrekordbox at the user's LIVE rekordbox6 master.db
|
||||||
|
(`%APPDATA%/Pioneer/rekordbox6/master.db`), confirm
|
||||||
|
`DjmdContent.ID == opaque_track_id_lo` for the AceMo tracks, then hunt the seed
|
||||||
|
in that DB. Until then, the nonzero stand-in (above) stays.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. File-header `sequence` (and per-page `sequence`)
|
||||||
|
|
||||||
|
**Field:** `RekordboxPdb.sequence` (u4 @0x14 of the file header) and
|
||||||
|
`Page.sequence` (u4 in each page header).
|
||||||
|
|
||||||
|
**Rule / meaning (confirmed from schema docs + the 5 genuine exports above):**
|
||||||
|
- `sequence` is a monotonically increasing **edit counter** for the database.
|
||||||
|
The file-header value is the **next** page-sequence number; each page records
|
||||||
|
the counter value as of its last edit (so `header.sequence >=
|
||||||
|
max(page.sequence)`). Observed header values in genuine rekordbox exports:
|
||||||
|
35, 56, 353, 546, 4211 — scales with edit history, not track count. (No real
|
||||||
|
export with sequence==1 is available; `data/usb`'s seq==1 is maple's own
|
||||||
|
output, not a rekordbox sample — see §2 correction.)
|
||||||
|
- The firmware/players (and crate-digger) use page `sequence` only as
|
||||||
|
informational ordering metadata; **they do not validate it** for playback.
|
||||||
|
No special value is required beyond keeping `header.sequence` greater than any
|
||||||
|
page's `sequence`.
|
||||||
|
- For maple it is enough to (a) pick a header `sequence` larger than every page
|
||||||
|
`sequence` it writes, and (b) give each page a `sequence` <= header. A simple
|
||||||
|
scheme (all pages = N, header = N+1) is accepted. This is what task #3
|
||||||
|
(incrementing sequence) implements.
|
||||||
|
|
||||||
|
**Confidence:** high for semantics and "not validated by players"; the exact
|
||||||
|
increment rekordbox uses per edit is not needed for a valid export.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. File-header offset 0x10 (u4, schema `_unnamed4`) — HISTORY-DEPENDENT; not byte-reproducible
|
||||||
|
|
||||||
|
> CORRECTION (2026-06-09): a prior draft cited a "usb fresh, sequence==1,
|
||||||
|
> _unnamed4==0" row as proof that 0 is the fresh value. That file is maple's own
|
||||||
|
> output, not a rekordbox export, so it is removed below. The remaining proof
|
||||||
|
> (same-library divergence) does not depend on it.
|
||||||
|
|
||||||
|
**Field:** the unnamed u4 between `next_unused_page` (0x0c) and `sequence`
|
||||||
|
(0x14). maple writes 0; the AceMo ref has 4.
|
||||||
|
|
||||||
|
**Finding (5 genuine rekordbox exports):** an **edit/allocator accumulator**,
|
||||||
|
not a structural value derivable from the tables. It does NOT equal track count,
|
||||||
|
page count, or `next_unused_page - npages`:
|
||||||
|
|
||||||
|
| export | sequence | _unnamed4@0x10 | next_unused | npages |
|
||||||
|
|---------------|---------:|---------------:|------------:|-------:|
|
||||||
|
| acemo_fresh | 35 | 5 | 52 | 49 |
|
||||||
|
| lib_multi | 56 | 4 | 57 | 52 |
|
||||||
|
| acemo | 353 | 4 | 53 | 49 |
|
||||||
|
| MAPLE | 546 | 5 | — | — |
|
||||||
|
| data/PIONEER | 4211 | 1 | 174 | 174 |
|
||||||
|
|
||||||
|
**Decisive (same-library proof):** acemo and acemo_fresh are the SAME library
|
||||||
|
(identical track set + identical per-track opaque ids) yet have `_unnamed4` =
|
||||||
|
4 vs 5. A structural field would be identical; differing proves it is edit
|
||||||
|
state. (We have NO genuine rekordbox export with `sequence==1`, so the exact
|
||||||
|
never-edited value is unconfirmed — but it is clearly not derivable from the
|
||||||
|
file's structure.)
|
||||||
|
|
||||||
|
**Rule for maple:** there is no reproducible rule. Write **0** (maple already
|
||||||
|
does) as a **reasonable minimal default — NOT a confirmed match** (every genuine
|
||||||
|
export we have is nonzero here, and we have no never-edited rekordbox export to
|
||||||
|
check what fresh rekordbox writes). Record the divergence in
|
||||||
|
re/KNOWN_DIVERGENCE.md. It cannot be matched to the AceMo ref's `4` without that
|
||||||
|
DB's edit history.
|
||||||
|
|
||||||
|
**Confidence:** high that it is edit-history/allocator state and not
|
||||||
|
structurally reproducible; the specific fresh-write value is unconfirmed (no
|
||||||
|
seq==1 rekordbox sample available).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Page-header offset 0x26 (u2, "unknown7"/strange-page) — HISTORY-DEPENDENT; maple's 0 is a reasonable default
|
||||||
|
|
||||||
|
> CORRECTION (2026-06-09): do NOT cite `data/usb` (sequence==1) here — it is
|
||||||
|
> maple's OWN output, so "maple's 0 matches usb" would be circular. The
|
||||||
|
> observations below are from the five GENUINE rekordbox exports only.
|
||||||
|
|
||||||
|
**Field:** the second of the two trailing u2s in the page header (after
|
||||||
|
`transaction_row_index` @0x22 and the "1004 for strange blocks" u2 @0x24).
|
||||||
|
@flesniak: "always 0 except 1 for history pages, num entries for strange
|
||||||
|
pages?". maple writes 0; the AceMo ref has 0/1/2 on various tables' first
|
||||||
|
("strange") pages.
|
||||||
|
|
||||||
|
**Finding (5 genuine rekordbox exports — all incrementally edited):**
|
||||||
|
- On DATA pages it is **always 0** (all five exports). This is a solid
|
||||||
|
observation about genuine rekordbox output.
|
||||||
|
- It is nonzero only on a table's first/"strange" page (flags 0x64), and the
|
||||||
|
value does NOT correlate with that table's data-page count or row count
|
||||||
|
(acemo tracks: 2 data pages → 2; data/PIONEER tracks: 105 data pages → 23).
|
||||||
|
It is transaction/edit-tracking state.
|
||||||
|
- **Decisive (same-library proof):** acemo and acemo_fresh hold the identical
|
||||||
|
tracks table, yet its strange-page unknown7 = 2 (acemo) vs 1 (acemo_fresh).
|
||||||
|
Same data → different value ⇒ not structural, it is edit state. (lib_multi:
|
||||||
|
tracks strange unk7 = 2 with 3 data pages / 10 rows — again no count relation.)
|
||||||
|
|
||||||
|
**Rule for maple:** write **0** on every page (maple already does). The data
|
||||||
|
pages being 0 across all five genuine exports makes 0 a well-supported value
|
||||||
|
*for data pages*. For strange pages the real value is per-export edit state
|
||||||
|
(0/1/2…) that cannot be reproduced — maple's 0 there is a **reasonable minimal
|
||||||
|
default, NOT a confirmed match** (we have no never-edited rekordbox export to
|
||||||
|
check what fresh rekordbox stamps on a strange page). Record the strange-page
|
||||||
|
divergence in re/KNOWN_DIVERGENCE.md.
|
||||||
|
|
||||||
|
**Confidence:** high that it is history-dependent and that data-page 0 matches
|
||||||
|
real rekordbox; the fresh-write strange-page value is unconfirmed (no seq==1
|
||||||
|
rekordbox sample).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Row-group trailing u2 (`transaction_row_flags`) — write `present_flags` (NOT 16); residual mismatch is edit history
|
||||||
|
|
||||||
|
> CORRECTION (2026-06-09): an earlier draft of this section claimed "a fresh
|
||||||
|
> export writes 16 everywhere, so maple's 16 is correct." That was based on
|
||||||
|
> `data/usb/.../export.pdb`, which on re-check is **maple's OWN output**, not a
|
||||||
|
> rekordbox export (opaque seed `0x78f74a68` + all-zero per-track lo +
|
||||||
|
> `sequence`==low). Reading it back was circular. The analysis below uses ONLY
|
||||||
|
> the five genuine rekordbox exports (provenance verified by nonzero per-track
|
||||||
|
> opaque ids + per-install seed): acemo, acemo_fresh, lib_multi, MAPLE,
|
||||||
|
> data/PIONEER.
|
||||||
|
|
||||||
|
**Field:** the last u2 of each 36-byte row-group block in the page row index
|
||||||
|
(crate-digger `transaction_row_flags`, at `base`; in maple's `pager.py` it is
|
||||||
|
`pack("<H", 16) # unknown (rekordcrate default)`).
|
||||||
|
|
||||||
|
**Finding (5 genuine rekordbox exports):** this u2 is the
|
||||||
|
**"rows touched by the last transaction" bitmask** (same bit layout as
|
||||||
|
`row_present_flags`), and is therefore **edit-history state, not reproducible**:
|
||||||
|
- **No genuine rekordbox export writes 16 as the field's value** on settled
|
||||||
|
pages (count of groups == 16 across the five exports: 0, 0, 0, 1, 8 — and
|
||||||
|
those few are coincidental transaction bitmasks, not a default). So maple's
|
||||||
|
constant **16 does NOT match rekordbox** — it is a rekordcrate invention.
|
||||||
|
- The dominant value in heavily-edited tables is **0x0** (last transaction
|
||||||
|
touched no rows on that page), e.g. data/PIONEER 170/294 groups = 0x0.
|
||||||
|
- Three DIFFERENT patterns coexist in ONE file, proving it is not structural:
|
||||||
|
(a) some pages `trailing == present_flags` (columns/unk17/unk18); (b) artwork
|
||||||
|
pages consistently use `last group = 1<<(rows_in_last_group-1)`, earlier groups
|
||||||
|
= 0 (data/PIONEER p28 g6 10rows→0x200, p147 g2 13rows→0x1000; acemo_fresh
|
||||||
|
artwork 7rows→0x40); (c) churned tracks pages use an arbitrary single bit
|
||||||
|
(0x1/0x2/0x4/0x8/0x10/0x40…) = whatever row that page's last edit touched.
|
||||||
|
A candidate clean rule `[last grp=1<<(rows-1), others=0]` matches only
|
||||||
|
**181/257** fully-present pages in data/PIONEER — confirming NO single formula.
|
||||||
|
- **Same-library proof:** acemo and acemo_fresh hold identical data, yet the
|
||||||
|
tracks page-2 group differs (acemo present=0x5e trailing=0x20; acemo_fresh
|
||||||
|
present=0xbb trailing=0x4) — same rows, different value ⇒ not structural.
|
||||||
|
The previously-flagged artwork `0x40` is likewise transaction state
|
||||||
|
(acemo_fresh artwork=0x40 vs lib_multi artwork p49=0x4), not an open
|
||||||
|
reproducible byte.
|
||||||
|
|
||||||
|
**Rule for maple (DECISION): write `present_flags`, NOT the constant 16.**
|
||||||
|
maple's old constant 16 (rekordcrate's placeholder) matches **0/14** real groups;
|
||||||
|
`trailing = row_present_flags` matches **12/14** clean groups in the real
|
||||||
|
`data/acemo_fresh` export (8 rows→0xff, 16→0xffff, 11→0x7ff, 6→0x3f, 1→0x1). The
|
||||||
|
2 misses (page2→0x4, page28→0x40, page40→0xc0) are the last-transaction bitmask
|
||||||
|
on pages that were edited after creation — i.e. residual edit history, not a
|
||||||
|
different rule. So for a from-scratch single-transaction write the correct value
|
||||||
|
IS `present_flags` (all rows touched in the one creating transaction). maple now
|
||||||
|
ships `present_flags` (pager.py) — this is the best achievable; reverting to 16
|
||||||
|
would regress 12 groups. Remaining mismatch vs an EDITED reference (the per-page
|
||||||
|
last-tx bitmask) is unavoidable edit history → KNOWN_DIVERGENCE.
|
||||||
|
|
||||||
|
> NOTE: the earlier "keep 16 / fresh usb writes 0x10 everywhere" framing was
|
||||||
|
> WRONG — it read `data/usb`, which is MAPLE'S OWN OUTPUT (an old build that
|
||||||
|
> emitted the rekordcrate-default 16), not a real export. `data/usb` must NOT be
|
||||||
|
> used as a 'fresh rekordbox' baseline for ANY field (same contamination that
|
||||||
|
> produced the wrong opaque-blob default in §2). The fresh-write value is
|
||||||
|
> established from the REAL `data/acemo_fresh` as `present_flags`.
|
||||||
|
|
||||||
|
**Confidence:** high that it is history-dependent and that real rekordbox never
|
||||||
|
uses 16; the correct fresh-write value is an open question (no seq==1 sample).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Note on the three "reproducible header fields" (task #9)
|
||||||
|
|
||||||
|
After multi-export analysis across the FIVE genuine rekordbox exports (acemo,
|
||||||
|
acemo_fresh, lib_multi, MAPLE, data/PIONEER — provenance verified by nonzero
|
||||||
|
per-track opaque ids + per-install seed), all three fields are
|
||||||
|
**edit-history / transaction state, NOT structurally reproducible**:
|
||||||
|
- §4 file-header 0x10: allocator/edit accumulator. → write 0, DIVERGENCE.
|
||||||
|
- §5 page 0x26 unknown7: 0 on all data pages (exact); per-table 0/1/2 on strange
|
||||||
|
pages is edit state. → write 0, strange-page DIVERGENCE.
|
||||||
|
- §6 row-group trailing u2: "rows touched by last transaction" bitmask. Fresh
|
||||||
|
single-transaction value = `present_flags` (12/14 in real data/acemo_fresh;
|
||||||
|
constant 16 = 0/14). → maple writes `present_flags`; residual mismatch vs an
|
||||||
|
EDITED ref is the per-page last-tx bitmask = edit history, DIVERGENCE.
|
||||||
|
|
||||||
|
The decisive evidence is the **same-library pair** (acemo vs acemo_fresh:
|
||||||
|
identical track data, yet header 0x10 = 4 vs 5, tracks strange unk7 = 2 vs 1,
|
||||||
|
tracks-page trailing differs) — a structural field would be identical.
|
||||||
|
|
||||||
|
maple's values: §4 header0x10 = 0 and §5 unknown7 = 0 are reasonable minimal
|
||||||
|
defaults (confirmed = real exports on data pages; fresh strange-page value
|
||||||
|
unconfirmed). §6 trailing u2 = `present_flags` is the established fresh-write
|
||||||
|
value (NOT the old constant 16). No further writer change needed; remaining
|
||||||
|
mismatches vs edited references are unavoidable edit history.
|
||||||
|
|
||||||
|
IMPORTANT corrections to the earlier draft: (1) `data/usb/.../export.pdb` is
|
||||||
|
maple's OWN output (an old build that emitted the rekordcrate-default 16), not a
|
||||||
|
rekordbox export — so any "fresh usb" claim was circular; `data/usb` is NOT an
|
||||||
|
authority for ANY field (same contamination that produced the wrong opaque-blob
|
||||||
|
default in §2). (2) We have NO genuine rekordbox export with sequence==1, so the
|
||||||
|
never-edited header0x10 / strange-page-unknown7 values are unconfirmed. (3) The
|
||||||
|
row-group trailing u2 fresh value is `present_flags`, established from the REAL
|
||||||
|
data/acemo_fresh (12/14); the rekordcrate "Default=16" is a placeholder that
|
||||||
|
matches 0/14 real groups.
|
||||||
|
|
||||||
|
All three go to re/KNOWN_DIVERGENCE.md, not the writer.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Artwork + USBANLZ path schemes (task #11)
|
||||||
|
|
||||||
|
Decoded from genuine `data/MAPLE` (176 tracks/176 artwork) + acemo. The guiding
|
||||||
|
principle (per the task): for **player acceptance only PATH SELF-CONSISTENCY
|
||||||
|
matters** — every path stored in the pdb must resolve to a real on-disk file.
|
||||||
|
Matching rekordbox's exact id-derivation is NOT required; maple's own consistent
|
||||||
|
scheme is acceptable.
|
||||||
|
|
||||||
|
### 7a. Artwork — SOLVED, exactly reproducible (maple already matches)
|
||||||
|
- Path: `/PIONEER/Artwork/{folder:05d}/a{artwork_id}.jpg` (80×80) plus a
|
||||||
|
`a{artwork_id}_m.jpg` (240×240) variant in the same folder (rekordbox writes
|
||||||
|
both; verified on disk in data/MAPLE).
|
||||||
|
- **Folder bucket rule: `folder = artwork_id // 20 + 1`** (20 ids per bucket;
|
||||||
|
bucket 00001 = ids 1–19 because ids are 1-based, 00002 = 20–39, 00003 = 40–59,
|
||||||
|
…). Verified **176/176** in data/MAPLE.
|
||||||
|
- maple's `artwork._bucket()` is literally `f"{artwork_id // 20 + 1:05d}"` and
|
||||||
|
`extract_artwork` writes both jpg variants at that path → **byte-identical to
|
||||||
|
rekordbox AND self-consistent**. No change needed.
|
||||||
|
|
||||||
|
### 7b. USBANLZ — opaque id; maple's own scheme is self-consistent (DIVERGENCE from rekordbox)
|
||||||
|
- Path: `/PIONEER/USBANLZ/P{bucket}/{8-hex}/ANLZ0000.DAT` (+`.EXT`).
|
||||||
|
- rekordbox's `{8-hex}` is a per-track **opaque analysis id** (range seen
|
||||||
|
0x117..0x309fd over 176 tracks; NOT == track id, NOT == the row-0x14
|
||||||
|
`opaque_track_id_lo`, NOT a crc of path/title — tested) and the `P{bucket}`
|
||||||
|
(0–127, 94 distinct over 176) is a spreading bucket that is **NOT a derivable
|
||||||
|
function** of the 8-hex or any track field (tested: `&0x7f`, `>>k`, `%127/128`,
|
||||||
|
low-byte — all fail). Like the row-0x14 blob, these come from rekordbox's
|
||||||
|
internal DB and are **not reproducible** without it.
|
||||||
|
- maple (cli.py `build()`): generates its OWN deterministic scheme —
|
||||||
|
`bucket = P{(crc32(file_path) >> 20) & 0xFFF:03X}`,
|
||||||
|
`analyze_id = {crc32(file_path):08X}`, then `write_anlz()` writes the .DAT at
|
||||||
|
exactly that path. This is **self-consistent by construction** (the path it
|
||||||
|
records is the path it writes) and is the correct approach per the
|
||||||
|
self-consistency principle. It does NOT match rekordbox's id/bucket (expected
|
||||||
|
DIVERGENCE).
|
||||||
|
- Minor cosmetic note (not a correctness issue): maple's bucket is 3 hex
|
||||||
|
digits `P000..PFFF` whereas rekordbox uses `P00..P7F` (0–127). Harmless;
|
||||||
|
optionally narrow to `& 0x7F` (`P{..:02X}`) to look more rekordbox-like.
|
||||||
|
|
||||||
|
### 7c. Self-consistency verification
|
||||||
|
- **Genuine rekordbox (data/MAPLE): 176/176 ANLZ paths and 176/176 artwork
|
||||||
|
paths resolve to on-disk files.** (rekordbox is fully self-consistent — sanity
|
||||||
|
check that confirms the parser and the principle.)
|
||||||
|
- **maple production `build()` (cli.py): self-consistent BY CONSTRUCTION** —
|
||||||
|
`extract_artwork` (artwork.py:62-65) writes `a{id}.jpg`/`a{id}_m.jpg` at the
|
||||||
|
recorded path; `write_anlz` (cli.py:93) writes `ANLZ0000.DAT` at the recorded
|
||||||
|
`analyze_path`. Each pdb path is computed and the file is written there in the
|
||||||
|
same step, so every path resolves.
|
||||||
|
- CAVEAT — the byte-diff harness `tools/build_acemo.py` is NOT the production
|
||||||
|
path: it MIRRORS a reference rekordbox export (copies its `analyze_path`/
|
||||||
|
artwork `path` strings verbatim, writes no ANLZ/artwork files), so the
|
||||||
|
`data/usb` export it produces references the reference's USBANLZ files which
|
||||||
|
are not co-located → those paths don't resolve. This is expected for a
|
||||||
|
structural-diff fixture and says nothing about production self-consistency.
|
||||||
|
|
||||||
|
**Confidence:** high. Artwork rule is exact (176/176). USBANLZ rekordbox ids are
|
||||||
|
opaque/non-reproducible (consistent with the row-0x14 finding); maple's own
|
||||||
|
USBANLZ scheme is self-consistent, which is all the player requires.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Page-manager free-space: `empty_candidate` + free pool (task #13 — THE FREEZE)
|
||||||
|
|
||||||
|
DECK TEST: maple's pdb FROZE the player (parser hang, not reject). Decoded the
|
||||||
|
page-manager/free-space divergence against genuine rekordbox exports
|
||||||
|
(`data/working` 9trk seq44, `data/MAPLE` 176trk, `data/PIONEER` 847trk — all
|
||||||
|
load) vs maple output (`captures/maple_acemo.pdb`).
|
||||||
|
|
||||||
|
### Data-page `next_page` SELF-LOOP — ROOT CAUSE (confirmed divergence; high confidence)
|
||||||
|
- The player reads each table by following `first_page → next_page → …`. maple's
|
||||||
|
**last data page of each multi-page chain set `next_page == its own index`**
|
||||||
|
(page 2→2, 6→6, 8→8, 12, 14, 28, 34, 36, 38, 40 — 10 self-loops in the 9-track
|
||||||
|
build). A page whose `next_page` is itself is an **infinite loop → FREEZE**.
|
||||||
|
- **Genuine rekordbox has ZERO self-loops.** A terminal data page uses
|
||||||
|
`next_page = 0` (the "no next" sentinel; seen on pages 10/16/18/20/…/41-47 in
|
||||||
|
`data/working`) OR points FORWARD to a distinct free page (never to itself).
|
||||||
|
- FIX (maple, done in writer.py): point every chain's last page at a single
|
||||||
|
dedicated zero-filled free-pool page (`free_sentinel`, just past the overflow
|
||||||
|
region) instead of itself; materialise that page and set `next_unused` beyond
|
||||||
|
it. Verified on the rebuilt pdb: 10 self-loops → 0; all 20 chains terminate at
|
||||||
|
the blank sentinel; player tolerates last-page→free-page (genuine exports do
|
||||||
|
the same). This is the primary freeze fix.
|
||||||
|
|
||||||
|
### Table-dir `empty_candidate` (u4 after page_type) — SECONDARY divergence (also wrong; lower-confidence freeze contributor)
|
||||||
|
- Layout: each 16-byte table-dir entry is `type:u4, empty_candidate:u4,
|
||||||
|
first_page:u4, last_page:u4`.
|
||||||
|
- `empty_candidate` is the head of that table's **empty/free-page list** (the
|
||||||
|
page the Page Manager grabs when the table needs to grow).
|
||||||
|
- **In EVERY genuine rekordbox export all 20 `empty_candidate`s are NONZERO and
|
||||||
|
distinct**, and each points to an **all-zero "free" page** — either an
|
||||||
|
in-file allocated-but-empty page (e.g. `last_page+1`, or a pooled page like
|
||||||
|
41–47/49) or a **beyond-EOF sentinel** (index ≥ npages, e.g. 50/51/52 when
|
||||||
|
npages=49 — the firmware tolerates beyond-EOF here). It NEVER points to a
|
||||||
|
used data page and NEVER to 0.
|
||||||
|
- **maple writes `empty_candidate = 0` for all 20 tables.** Page 0 is the FILE
|
||||||
|
HEADER. The firmware Page Manager, walking a table's empty-page list, follows
|
||||||
|
`0` → reads the 4 KB file header AS a page → its "next_page" field (header
|
||||||
|
offset 0x0c = `next_unused_page`) and bogus type/flags send the walk into a
|
||||||
|
loop or out-of-bounds read → **hang/FREEZE**. This is the highest-confidence
|
||||||
|
cause: it is the only systematic, data-independent divergence, and `0` is
|
||||||
|
never valid (collides with the header).
|
||||||
|
|
||||||
|
### Free pool / page layout
|
||||||
|
- rekordbox keeps the data pages in chains `1..40,48,…` and pre-allocates a
|
||||||
|
pool of all-zero "free" pages past them; `next_unused_page` = one past the
|
||||||
|
highest allocated page (so `next_unused > npages` is normal, e.g. 53 > 49).
|
||||||
|
- maple **compacts**: 41 pages, every page used, `next_unused == npages == 41`,
|
||||||
|
no free pool. The compaction itself is structurally legal (chains are clean),
|
||||||
|
but it left `empty_candidate = 0` with no free page to point at.
|
||||||
|
- Beyond-EOF links (a page's `next_page` ≥ npages, or `empty_candidate` ≥
|
||||||
|
npages) are TOLERATED — the genuine `working` file has many (page 4→53,
|
||||||
|
46→52, empty_candidate 51/52/50/49 all ≥ 49). So beyond-EOF is a valid
|
||||||
|
"no-more-pages / no-free-page" sentinel; it is NOT the freeze.
|
||||||
|
|
||||||
|
### Strange-page free-map — NOT the cause (decoded, maple matches)
|
||||||
|
- Each table's first/"strange" page (flags 0x64) carries, after a small prefix
|
||||||
|
`[this_page, next_or_0x03ffffff, 0x03ffffff, 0, 0x1fff0000]`, a heap filled
|
||||||
|
with **1004 × `0x1ffffff8`** (`f8 ff ff 1f` LE) — the empty-heap free-map
|
||||||
|
(count == the `m1004`=1004 marker at header 0x24). `0x03ffffff` in the 2nd u4
|
||||||
|
is the legitimate **"this table has no data page"** sentinel (genuine
|
||||||
|
rekordbox writes it for every empty table; e.g. labels/pltree/plentries).
|
||||||
|
- maple reproduces this free-map correctly; its strange-page prefixes match
|
||||||
|
rekordbox's pattern byte-for-byte (data table → u1=first_data_page; empty
|
||||||
|
table → u1=0x03ffffff). **So the free-map is NOT the freeze.**
|
||||||
|
|
||||||
|
### FIX (for maple → deck test)
|
||||||
|
PRIMARY (high confidence, DONE): **eliminate the data-page `next_page` self-loop**
|
||||||
|
— last page of each chain must point to a blank free page (or use `next_page=0`),
|
||||||
|
never to itself. maple now points all last pages at one zero-filled `free_sentinel`
|
||||||
|
page. Verified: 0 self-loops in the rebuilt pdb.
|
||||||
|
|
||||||
|
SECONDARY (cheap, also a real divergence — fold into the same build): **stop
|
||||||
|
writing `empty_candidate = 0`.** Point it at the same `free_sentinel`
|
||||||
|
(`pack("<I", free_sentinel)` instead of 0). Genuine rekordbox NEVER writes 0
|
||||||
|
(0 = the header page); it always points each table's empty_candidate at a blank
|
||||||
|
free page. Lower-confidence as a freeze cause for a read-only player than the
|
||||||
|
self-loop, but wrong and trivially fixable with the page maple already
|
||||||
|
materialised.
|
||||||
|
|
||||||
|
If the deck STILL hangs after both, next suspects (lower confidence):
|
||||||
|
`next_unused_page == npages` (give a little slack), and whether the single shared
|
||||||
|
sentinel needs to be per-table-distinct (rekordbox uses distinct free pages).
|
||||||
|
|
||||||
|
**Confidence:** HIGH that the self-loop was the freeze (self-referential
|
||||||
|
next_page is an unconditional infinite loop; 0/all genuine exports self-loop).
|
||||||
|
empty_candidate=0 is independently wrong (0/all genuine exports use it; collides
|
||||||
|
with header). Exact firmware behaviour is inferred (no firmware to trace); the
|
||||||
|
deck test confirms.
|
||||||
|
is the confirmation.
|
||||||
@@ -0,0 +1,167 @@
|
|||||||
|
# KNOWN_DIVERGENCE.md — fields maple's writer intentionally does NOT match
|
||||||
|
|
||||||
|
This is the canonical list of bytes that differ between maple's
|
||||||
|
`captures/maple_acemo.pdb` and the AceMo reference
|
||||||
|
`data/acemo/PIONEER/rekordbox/export.pdb` **on purpose**, because they are
|
||||||
|
*history-dependent* artifacts of the particular reference file rather than rules
|
||||||
|
of the `.pdb` format.
|
||||||
|
|
||||||
|
## TL;DR
|
||||||
|
|
||||||
|
The AceMo reference `export.pdb` is **not a fresh from-scratch export — it is an
|
||||||
|
incrementally-edited rekordbox database.** rekordbox's DeviceSQL page manager
|
||||||
|
edits rows *in place*: when a row changes it appends a new copy, clears the old
|
||||||
|
slot's presence bit, and advances `used_size` past the dead bytes **without
|
||||||
|
compacting**. The reference therefore carries the residue of its entire edit
|
||||||
|
history.
|
||||||
|
|
||||||
|
maple writes a **fresh, fully-compacted** database (tightly 4-byte-aligned rows,
|
||||||
|
no dead rows, no leftover slots). That output is byte-correct for a *from-scratch*
|
||||||
|
rekordbox export, but it cannot — and should not — reproduce another file's edit
|
||||||
|
history. The fields below are the consequences of that, and the team has decided
|
||||||
|
**not to chase them**. They only become matchable against a fresh from-scratch
|
||||||
|
reference export (export to an empty USB, never edited afterward).
|
||||||
|
|
||||||
|
Everything NOT listed here is fair game and must match. The task-#7 diff should
|
||||||
|
filter out (and reference) this list, then drive the remaining bytes to zero.
|
||||||
|
|
||||||
|
## Evidence (tracks table, page 2 of the reference)
|
||||||
|
|
||||||
|
5 present rows, each with ~377–417 bytes of real content, yet:
|
||||||
|
|
||||||
|
| row id | ofs | real content len | next ofs | gap | gap bytes are… |
|
||||||
|
|--------|-------|------------------|----------|-------|---------------------------|
|
||||||
|
| 1 | 428 | 377 | 856 | 51 | zero |
|
||||||
|
| 5 | 856 | 377 | 1324 | 91 | partial stale string |
|
||||||
|
| 2 | 1324 | 417 | 1792 | 51 | zero |
|
||||||
|
| 3 | 1792 | 386 | 2656 | 478 | a whole dead track row |
|
||||||
|
| 4 | 2656 | 404 | 3996* | 936 | two whole dead track rows |
|
||||||
|
|
||||||
|
`used_size=3996`, `free_size=38`. The gap after id=3 holds a dead copy of a row
|
||||||
|
ending `"...Jelly Fish Jam.mp3"`; the gap after id=4 holds dead copies of
|
||||||
|
`"Myrtle Ave Party Track"` and `"Smooth Move"` rows. These are previous versions
|
||||||
|
of edited rows, left uncompacted. (Verified by ASCII-dumping the gap bytes.)
|
||||||
|
|
||||||
|
## The fields we do NOT match (and why)
|
||||||
|
|
||||||
|
1. **File / page count and `next_unused_page`.**
|
||||||
|
Reference: 49 pages, `next_unused=53`. maple: 41 pages, `next_unused=41`.
|
||||||
|
The reference is larger purely because dead-row bloat pushed live rows onto
|
||||||
|
extra pages. A compacted export needs fewer pages.
|
||||||
|
|
||||||
|
2. **Overflow / data-page placement (tracks `last_page`).**
|
||||||
|
Reference tracks table spans pages **1 → 48** (live track id=7 spilled onto
|
||||||
|
overflow page 48 because dead rows "filled" page 2). maple spans **1 → 2**
|
||||||
|
(all 6 tracks fit one compacted page). This is the *only* table-directory
|
||||||
|
entry that differs — the other 19 tables match first/last page exactly. The
|
||||||
|
divergence is entirely a function of the dead-row bloat above.
|
||||||
|
|
||||||
|
3. **File-header `sequence` value.**
|
||||||
|
Reference: `353`. maple: an incrementing counter (reads the destination
|
||||||
|
export's sequence + 1, default 1; see `writer._next_sequence`). rekordbox's
|
||||||
|
value comes from its internal `master.db` transaction counter and is not
|
||||||
|
reproducible from content. We match the *semantics* (monotonic increment),
|
||||||
|
not the absolute value.
|
||||||
|
|
||||||
|
4. **Per-page `sequence` (page-header offset 16).**
|
||||||
|
Reference pages carry assorted values (e.g. 26, 45, 320, 342, 347) — each a
|
||||||
|
snapshot of the global counter at the moment that page was last rewritten.
|
||||||
|
maple writes a clean per-build value. Same root cause as (3); not reproducible.
|
||||||
|
|
||||||
|
5. **`transaction_row_count` / `transaction_row_index` (page-header u2 pair).**
|
||||||
|
Reference: nonzero per edited page (e.g. `1/5`, `1/8`, `2/0`). maple: `0/0`.
|
||||||
|
These track the last in-place transaction on the page; meaningless for a
|
||||||
|
freshly written, never-edited page.
|
||||||
|
|
||||||
|
6. **Dead-row offset arrays + `num_row_offsets` > `num_rows`.**
|
||||||
|
Reference row groups list offsets for dead slots too: e.g. tracks page 2 has
|
||||||
|
`nro=9, nrows=5` with offsets `[0,428,856,1324,1792,2224,2656,3112,3568]`
|
||||||
|
(slots 0,5,7,8 dead); artists page 6 has `nro=9, nrows=1`. maple emits exactly
|
||||||
|
`nro == nrows` with no dead slots. Reproducing dead offsets would mean
|
||||||
|
reproducing the edit history.
|
||||||
|
|
||||||
|
7. **Live row in a non-zero slot.**
|
||||||
|
Reference artists page 6 keeps its single live row in **slot 8** (slots 0–7
|
||||||
|
are dead). maple places the single live row in **slot 0**. A from-scratch
|
||||||
|
export also writes slot 0.
|
||||||
|
|
||||||
|
8. **`free_size` / `used_size` totals on data pages.**
|
||||||
|
Reference `used+free` totals vary (4034, 4050, …) because `used_size` covers
|
||||||
|
the dead-row region and the strange-block markers differ. maple's pages total
|
||||||
|
a clean `4020` (= heap capacity for a single 36-byte row group). Direct
|
||||||
|
consequence of (6) — dead bytes are counted as "used".
|
||||||
|
|
||||||
|
9. **Track row opaque id blob — `0x14`/`0x18`/`0x1a` (8 bytes/track).**
|
||||||
|
One 8-byte LE value `opaque_track_id_lo | (opaque_seed << 32)`, both halves
|
||||||
|
stored in rekordbox's internal master.db (a per-install `opaque_seed` + a
|
||||||
|
per-track `opaque_track_id_lo`). Proven non-derivable from track metadata
|
||||||
|
across every real export + an exhaustive negative hash search
|
||||||
|
(FORMAT_SPEC.md s2). **Every real rekordbox export has BOTH halves non-zero**
|
||||||
|
(this install's seed = `0x21b4a7ab`; `data/MAPLE` = `0xd6d3a567`); the
|
||||||
|
"unanalyzed default" lo=0/seed=0x78f74a68 was a false lead — it came from
|
||||||
|
maple's own earlier output (`data/usb`), not a real export. maple emits a
|
||||||
|
plausible non-zero stand-in of the right shape: `opaque_seed = 0x21b4a7ab`
|
||||||
|
(module const `TRACK_OPAQUE_SEED`, overridable per install) and a stable
|
||||||
|
per-track `opaque_track_id_lo` mapped into the real Content-ID band
|
||||||
|
(`OPAQUE_LO_MIN..MAX` = 0x08000000..0x0F900000, where every observed AceMo lo
|
||||||
|
lands) via `crc32(id|file_path)`. This will NOT match rekordbox's master.db
|
||||||
|
id byte-for-byte — unreproducible without its DB — but it is non-zero,
|
||||||
|
in-range and deterministic, the safest shape for deck acceptance. Players
|
||||||
|
treat the blob as opaque.
|
||||||
|
|
||||||
|
10. **Strange-page free-map extension + per-page seq on tracks/history.**
|
||||||
|
On the tracks (type 0) and history (type 19) strange pages only, the
|
||||||
|
free-space map carries an extra accounting word (body offset 16: `01 00`,
|
||||||
|
offset 20: a u4 = the data page's used_size for history=320; tracks differs
|
||||||
|
because of its dead-row/overflow state). maple writes the plain free-map.
|
||||||
|
Tied to the same content/overflow state as items (1)/(2); not reproducible
|
||||||
|
for a compacted writer. (The rest of every strange page — header, free-map
|
||||||
|
head, 1004× `f8 ff ff 1f`, the data-page pointer word, and unknown7 — now
|
||||||
|
matches the fresh reference byte-for-byte.)
|
||||||
|
|
||||||
|
11. **Table-directory `empty_candidate` (u32, table_dir entry +4) + header
|
||||||
|
`next_unused_page`.** Reference: a nonzero per-table `empty_candidate`
|
||||||
|
(e.g. 0x33, 0x04, 0x2e…); maple: 0. rekordcrate documents this field as
|
||||||
|
"maybe links to a chain of empty pages if the database is ever garbage
|
||||||
|
collected", and its fixtures show non-sequential free-pool page indices
|
||||||
|
(47, 4, 49, 8, 50, 46, 42, 16…). It is free-list/GC allocator state, not
|
||||||
|
derivable from content. `next_unused_page` (rekordcrate: "points past end of
|
||||||
|
file", unused) is the same class — see also item (1).
|
||||||
|
|
||||||
|
> Canonical reference note: the diff target is now the FRESH export
|
||||||
|
> `data/acemo_fresh/PIONEER/rekordbox/export.pdb` (7 tracks). It is cleaner than
|
||||||
|
> the edited `data/acemo` (6 tracks) but still incrementally built per-file, so
|
||||||
|
> the divergences above still apply — just smaller (e.g. track 7's dead-row span
|
||||||
|
> is 464B not 3568B, but it still overflows to page 48).
|
||||||
|
|
||||||
|
## What IS reproducible (NOW matched against the fresh reference)
|
||||||
|
|
||||||
|
- The 20-table directory layout and page placement: an empty table is
|
||||||
|
strange-page-only; a table gets a data page IFF it has rows (`CORE_TABLES` is
|
||||||
|
now empty — the old genres data page was an edit-history artifact of the
|
||||||
|
edited ref).
|
||||||
|
- `page_flags`: data pages are `0x34` for **tracks and history only**, `0x24`
|
||||||
|
for every other table (the wider `{0,2,3,13,17,19}` was edited-ref noise);
|
||||||
|
empty data pages `0x44`; strange pages `0x64`.
|
||||||
|
- Strange-page body: the full DeviceSQL free-space map — `u4(page_index)`,
|
||||||
|
`u4(first_data_page or 0x03ffffff)`, the 12-byte const tail, 1004×
|
||||||
|
`f8 ff ff 1f`, 20 zero bytes — plus header `free=used=0`, `txc=txi=0x1fff`,
|
||||||
|
`next_page = page_index+1`, and `unknown7 = 1` for tracks/history else 0.
|
||||||
|
- Track row `index_shift` (`slot_index<<5`) and every other live-row content
|
||||||
|
field except the opaque blob in divergence item (9).
|
||||||
|
- Three header fields `re` decoded (FORMAT_SPEC §4/§5/§6) as edit-history state
|
||||||
|
whose FRESH-export value maple already writes — so they're matched on a fresh
|
||||||
|
export and only diverge against edited refs:
|
||||||
|
- file-header offset 16 (`_unnamed4`): fresh=`0` (maple writes 0); edited DBs
|
||||||
|
accumulate a small nonzero with no structural meaning.
|
||||||
|
- strange/data-page `unknown7` (0x26): fresh=`0` everywhere (maple writes 0);
|
||||||
|
nonzero only on edited DBs' strange pages.
|
||||||
|
- row-group trailing u2: it is the "rows-touched-in-last-transaction" bitmask.
|
||||||
|
On a fresh export it equals the row-present mask, so maple writes
|
||||||
|
`presence` (matches 12/14 groups in `data/acemo_fresh`; the 2 misses are
|
||||||
|
edited pages — divergence). NOTE: `re`'s "keep constant 16" advice came from
|
||||||
|
reading `data/usb`, which is maple's OWN output, not a real export — a real
|
||||||
|
fresh export uses the presence value, not 16.
|
||||||
|
|
||||||
|
See `re/FORMAT_SPEC.md` for the value-construction rules and `re/README.md` for
|
||||||
|
the open RE questions.
|
||||||
@@ -0,0 +1,470 @@
|
|||||||
|
# REFERENCE_COMPARISON.md — pyrekordbox & rekordcrate vs maple
|
||||||
|
|
||||||
|
What the two mature open-source rekordbox libraries can decode/encode, how
|
||||||
|
that lines up with maple's writer and our RE findings, and the concrete GAPS
|
||||||
|
maple must close for a full from-scratch USB export.
|
||||||
|
|
||||||
|
- **pyrekordbox** (Python, dylanljones) — https://pyrekordbox.readthedocs.io
|
||||||
|
- **rekordcrate** (Rust, Holzhaus) — https://holzhaus.github.io/rekordcrate/
|
||||||
|
local source at `/mnt/c/Users/ntr/code/rekordcrate/src`
|
||||||
|
- **maple** — `maple-py/src/maple/*`, this `re/` dir.
|
||||||
|
|
||||||
|
Scope note (coordination): the per-format **library-capability** comparison and
|
||||||
|
the from-scratch-export gaps are mine (task #12). The master.db SQLCipher key
|
||||||
|
mechanics and opaque Content ID identity are owned by `re` (#10); the exact
|
||||||
|
USBANLZ/artwork on-disk path schemes by `re` (#11). Where those are cited below
|
||||||
|
they are tagged `[see re #10]` / `[see re #11]` and should be treated as
|
||||||
|
`re`'s authority, not re-derived here.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 0. One-screen capability matrix
|
||||||
|
|
||||||
|
| Format | pyrekordbox | rekordcrate | maple |
|
||||||
|
|---|---|---|---|
|
||||||
|
| **export.pdb** | parse only (via the `pdb` parser, read) | **parse + WRITE** (`BinWrite` on rows/pages, full roundtrip) | **WRITE only** (from-scratch, the whole point) |
|
||||||
|
| **exportExt.pdb** | not supported | not supported | not written |
|
||||||
|
| **ANLZ (.DAT/.EXT/.2EX)** | **parse only** ("creating … is planned") | parse + *partial* write (binrw `BinWrite` derived on waveform/cue structs; not a whole-file builder) | **WRITE** (minimal: PMAI + PPTH only) |
|
||||||
|
| **artwork (.jpg thumbs)** | reads `ImagePath` from db only | n/a (just the pdb `Artwork` row path string) | **WRITE** (extracts embedded art) |
|
||||||
|
| **master.db (db6/7)** | **READ + WRITE** (SQLCipher unlock) | not supported | not used |
|
||||||
|
| **mysetting (\*SETTING.DAT)** | **READ + WRITE** (build from scratch) | parse only (`setting` module) | not written |
|
||||||
|
| **rekordbox XML** | **READ + WRITE** | parse only (`xml` module) | not used |
|
||||||
|
|
||||||
|
The headline: **no existing library writes a complete from-scratch USB export.**
|
||||||
|
rekordcrate writes `export.pdb` rows but not ANLZ/exportExt and has no
|
||||||
|
metadata source; pyrekordbox writes master.db + mysetting + XML but not the USB
|
||||||
|
`export.pdb`/ANLZ. maple is filling a real gap. The reference libs are most
|
||||||
|
valuable to maple as (a) a **read-back oracle** (parse maple's output and diff)
|
||||||
|
and (b) for pyrekordbox, a **source of the real opaque Content IDs** from
|
||||||
|
master.db.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. export.pdb
|
||||||
|
|
||||||
|
### What rekordcrate does (the closest reference writer)
|
||||||
|
`pdb/mod.rs` decodes **and writes** every row type maple emits: Track, Artist,
|
||||||
|
Album, Artwork, Color, Genre, Key, Label, PlaylistTreeNode, PlaylistEntry,
|
||||||
|
ColumnEntry, HistoryPlaylist, HistoryEntry (`pub enum Row`, lines ~1064-1103).
|
||||||
|
It has a genuine page serializer: `write_page_contents` (mod.rs:212),
|
||||||
|
`write_options_and_get_row_offset` (mod.rs:463), per-row-type alignment
|
||||||
|
(`align_by`, mod.rs:1121-1133), and a `Track` `BinWrite` impl (mod.rs:934).
|
||||||
|
|
||||||
|
### Where rekordcrate CONFIRMS maple's layout (high-value confirmations)
|
||||||
|
- **Track fixed prefix = 0x5e bytes, then a 21×u2 (0x2a) offset block.**
|
||||||
|
rekordcrate asserts `start_of_string_section - base_position == 0x5e` and
|
||||||
|
seeks `0x2a` for 21 string offsets (mod.rs ~986-996). maple's
|
||||||
|
`TRACK_FIXED_PREFIX = 0x5E`, `TRACK_HEADER_LEN = 0x5E + 21*2`
|
||||||
|
(`rows.py:8-10`) and its 21-string list match field-for-field, including the
|
||||||
|
exact string order (isrc, …, analyze_path, analyze_date, comment, title,
|
||||||
|
unknown_string8, filename, file_path). **Independent confirmation of maple's
|
||||||
|
track row.**
|
||||||
|
- **Field order of the 0x5e prefix** is identical between rekordcrate's
|
||||||
|
`Track::write_options` and maple's `serialize_track`: unknown1/index_shift/
|
||||||
|
bitmask/sample_rate/composer_id/file_size/`unknown2`(=opaque lo @0x14)/
|
||||||
|
`unknown3`+`unknown4`(=seed @0x18/0x1a)/artwork_id/key_id/orig_artist_id/
|
||||||
|
label_id/remixer_id/bitrate/track_number/tempo/genre_id/album_id/artist_id/
|
||||||
|
id/disc/play_count/year/sample_depth/duration/unknown5/color/rating/unknown6/
|
||||||
|
unknown7.
|
||||||
|
- **DeviceSQL string encoding.** rekordcrate `pdb/string.rs` roundtrips the
|
||||||
|
same three forms maple's `strings.py` writes: short-ASCII
|
||||||
|
(`(len<<1)|1`), long-ASCII (`0x40`), long-UTF16LE (`0x90`). Its tests encode
|
||||||
|
`"I ❤ Rust"` as 0x90 and a long string as 0x40 — exactly maple's selection
|
||||||
|
rule. The empty string is `0x03` in both. **Confirms strings.py.**
|
||||||
|
- **Row-group / page-index mechanics.** rekordcrate's `RowGroup` (mod.rs:451,
|
||||||
|
2102) uses the same 16-rows-per-group, backward-growing offset index, and
|
||||||
|
presence bitmask maple builds in `pager.py`. `Header` (mod.rs:162) carries
|
||||||
|
the same 13-bit num_row_offsets / 11-bit num_rows packing maple writes
|
||||||
|
(`pager.py:131`).
|
||||||
|
|
||||||
|
### Where rekordcrate caught a maple BUG — `next_page` chain termination (the FREEZE)
|
||||||
|
rekordcrate `pdb/mod.rs` documents `Page::next_page` (lines 262-265) precisely:
|
||||||
|
> *"Index of the next page with the same page type. If this page is the last one
|
||||||
|
> of that type, the page index stored in the field will point **past the end of
|
||||||
|
> the file**."*
|
||||||
|
|
||||||
|
maple's writer originally set the **last** page of each table chain to
|
||||||
|
`next_page = its own page_index` (a self-pointer). That is exactly what
|
||||||
|
rekordcrate says must NOT happen — the last page must point *forward, past the
|
||||||
|
allocated pages*. A self-referencing `next_page` makes a firmware page-walk that
|
||||||
|
terminates on "next_page past EOF" **loop forever on the last page → the deck
|
||||||
|
freezes.** This is the root cause being fixed in task #13: `writer.py` now points
|
||||||
|
every last page at a dedicated free-pool sentinel page just past the overflow
|
||||||
|
region (`free_sentinel`, materialised zero-filled) and sets `next_unused_page`
|
||||||
|
beyond it — which removes the self-loop.
|
||||||
|
(rekordcrate's own `read_pages`, mod.rs:185-196, terminates the walk on
|
||||||
|
`page_index == table.last_page` from the directory, so its *parser* tolerates a
|
||||||
|
self-pointer — which is why maple's read-back oracle never flagged it. The
|
||||||
|
*firmware* uses the chain-walk, so only a real deck exposed the freeze.
|
||||||
|
Lesson: a clean rekordcrate parse is NOT proof the chain is firmware-valid.)
|
||||||
|
|
||||||
|
> **Verified against a real export — the EXACT rule (not just "past EOF").**
|
||||||
|
> I decoded `data/PIONEER/rekordbox/export.pdb` (847 tracks, 174 pages,
|
||||||
|
> `next_unused=177`). rekordcrate's "points past the end of file" is a
|
||||||
|
> simplification; the real, consistent rule across **all 20 tables** is:
|
||||||
|
> **each table's last/only page sets `next_page == that table's
|
||||||
|
> `empty_candidate`**, where `empty_candidate` (table-dir entry +4) is a
|
||||||
|
> *distinct, nonzero* page index `< next_unused` — the head of that table's
|
||||||
|
> free-page chain. Evidence: every last page's `next_page` equalled its own
|
||||||
|
> dir `empty_candidate` (type0 173→174=ec; type1 4→53=ec; type19 40→41=ec; …),
|
||||||
|
> and those targets (174,53,158,163,54,49,42,46,170,20,22,175,176,165,30,32,
|
||||||
|
> 43,44,45,41) are all `< next_unused`. Strange-only tables already match
|
||||||
|
> (next_page = page_idx+1 = their ec; free-map `data_ptr=0x03ffffff`) — maple's
|
||||||
|
> `build_strange_page` does this correctly.
|
||||||
|
>
|
||||||
|
> So maple's current #13 fix (a **single shared sentinel** for every data
|
||||||
|
> table + `empty_candidate=0` in the directory) removes the freeze but still
|
||||||
|
> **diverges** from real rekordbox, which uses a **distinct per-table
|
||||||
|
> `empty_candidate`** that the directory entry also records (nonzero). If
|
||||||
|
> firmware ever cross-checks `next_page` against the directory's
|
||||||
|
> `empty_candidate`, the shared sentinel + zero ec could still misbehave.
|
||||||
|
> Recommended (reported to `maple`): give each data table its own free page,
|
||||||
|
> set `next_page = that page` AND write that index into the table-dir
|
||||||
|
> `empty_candidate`, keep them all `< next_unused`. This is straightforwardly
|
||||||
|
> reproducible (unlike the *value* of `empty_candidate` on an edited ref, which
|
||||||
|
> is GC state — here we only need a valid distinct free page, which a
|
||||||
|
> from-scratch writer fully controls).
|
||||||
|
|
||||||
|
### Where rekordcrate is WRONG and maple is RIGHT — page row-count header (>255 rows)
|
||||||
|
Audited because rekordcrate's `Page` header (mod.rs:277-377) reads the row count
|
||||||
|
as **`num_rows_small: u8` @0x18** plus **`num_rows_large: u16` @0x22** with a
|
||||||
|
`calculate_num_rows` rule (`large if large>small && large!=0x1fff else small`).
|
||||||
|
maple instead writes the **canonical crate-digger .ksy bitfield** at 0x18:
|
||||||
|
13-bit `num_row_offsets` + 11-bit `num_rows`, then `page_flags` @0x1b
|
||||||
|
(`pager.py:131`). These are two different readings of the same bytes — so I
|
||||||
|
**verified against a real export** (`data/PIONEER`, which has 284-row
|
||||||
|
playlist-entry pages):
|
||||||
|
- Real page 18 bytes @0x18 = `1c 81 23 24`. The 24-bit LE value `0x23811c`
|
||||||
|
decodes (kaitai/maple way) to `num_row_offsets=284, num_rows=284` — **correct**.
|
||||||
|
- maple's packer reproduces `1c 81 23` **exactly** for n=284 (and `a6 c0 14` for
|
||||||
|
166, `7e c0 0f` for 126 — all matching real pages byte-for-byte).
|
||||||
|
- rekordcrate's reading of the *same* bytes: `num_rows_small = 0x1c = 28` (just
|
||||||
|
the low byte) and `num_rows_large @0x22 = 283` (which is really the
|
||||||
|
`transaction_row_index` edit field), giving `calculate_num_rows = 283` — i.e.
|
||||||
|
rekordcrate would report **283 rows on a 284-row page, and 28 if the
|
||||||
|
transaction field were 0.** Its small/large model only coincidentally agrees
|
||||||
|
when `n <= 255`.
|
||||||
|
- **Conclusion:** maple's header encoding is correct and firmware-faithful for
|
||||||
|
pages up to 2047 rows (the 11-bit max); rekordcrate's `num_rows_small`/
|
||||||
|
`num_rows_large` is an older/divergent decode that breaks for `n > 255`. This
|
||||||
|
matters because maple *can* emit >255-row pages (playlist_entries fits ~284/
|
||||||
|
page, colors ~394). No maple change needed — this is a maple-is-right result.
|
||||||
|
|
||||||
|
### Where rekordcrate's row alignment diverges (maple matches real rekordbox)
|
||||||
|
rekordcrate aligns each row by its Rust struct's natural `align_of_val`
|
||||||
|
(mod.rs:1114-1136), which would 2-align u16-only row types. maple aligns **every
|
||||||
|
row to 4** (`pager._align4`). Checked real exports: **every row in
|
||||||
|
`data/PIONEER` (3567/3567) and `rb_ref` (101/101) is 4-aligned.** So maple's
|
||||||
|
universal 4-alignment matches rekordbox; rekordcrate's per-type scheme is the
|
||||||
|
divergent one (its own code comments admit "we should survey all the different
|
||||||
|
kind of rows and ensure they're aligned properly", mod.rs:1110). No maple change
|
||||||
|
needed.
|
||||||
|
|
||||||
|
### Where maple's RE goes BEYOND rekordcrate (we know more than the reference)
|
||||||
|
These are fields rekordcrate leaves as opaque caller-supplied "unknown"s but
|
||||||
|
maple's RE solved/characterised — important because it means **rekordcrate
|
||||||
|
cannot be used to validate these; maple's FORMAT_SPEC is the authority**:
|
||||||
|
|
||||||
|
- **`index_shift` (row +0x02).** rekordcrate calls it `index_shift` but writes
|
||||||
|
whatever the caller stored — it does **not** know the value (mod.rs:104-105
|
||||||
|
"Unknown field, called `index_shift` by flesniak"). maple **derives** it as
|
||||||
|
`slot_index << 5` and patches it at page-build time (`pager.py:142-145`,
|
||||||
|
FORMAT_SPEC.md §1). maple is correct where rekordcrate is agnostic.
|
||||||
|
- **The 8-byte opaque blob (0x14 lo / 0x18+0x1a seed).** rekordcrate:
|
||||||
|
`unknown2: u32`, `unknown3: u16` ("always 19048?"), `unknown4: u16`
|
||||||
|
("always 30967?") — three blind unknowns it round-trips verbatim
|
||||||
|
(mod.rs:115-119). maple's RE (FORMAT_SPEC.md §2) proved this is one LE u64 =
|
||||||
|
`opaque_track_id_lo | (opaque_seed<<32)`, a per-track Content ID + per-install
|
||||||
|
seed stored in master.db, **non-derivable** from metadata, and that the
|
||||||
|
"19048/30967" constants are NOT universal (they only hold for some old
|
||||||
|
exports). maple emits a non-zero stand-in; the real values live in master.db
|
||||||
|
`[see re #10]`. maple's understanding is strictly deeper than rekordcrate's.
|
||||||
|
- **`bitmask` (0x06).** rekordcrate: opaque "unknown" round-tripped. maple
|
||||||
|
writes a fixed `788224` (0xC0700) — itself not yet RE'd to a formula, but
|
||||||
|
maple at least pins a known-good value.
|
||||||
|
- **Strange/"first" pages.** rekordcrate leaves the strange-page heap **zero**
|
||||||
|
(it round-trips parsed input); maple **constructs** the full DeviceSQL
|
||||||
|
free-space map from scratch byte-for-byte (`pager.build_strange_page`,
|
||||||
|
verified vs `data/acemo_fresh` and the real strange pages in data/PIONEER),
|
||||||
|
including the `f8 ff ff 1f`×1004 map, the `0x03ffffff` no-data sentinel, and
|
||||||
|
`unknown7=1` for tracks/history. `re` confirms maple is **strictly more
|
||||||
|
faithful than rekordcrate on strange pages.** This is from-scratch generation
|
||||||
|
rekordcrate never had to do.
|
||||||
|
- **RowGroup trailing u2.** rekordcrate writes a constant `16` placeholder
|
||||||
|
(`Default::default`, mod.rs:454) and does **not** decode it; `re` confirms
|
||||||
|
genuine rekordbox **never** uses 16 there — it is the "rows touched by the
|
||||||
|
last transaction" bitmask (FORMAT_SPEC §6). maple writes `present_flags` (the
|
||||||
|
single-transaction value), closer to real output; either way it is
|
||||||
|
edit-history state (KNOWN_DIVERGENCE §6), not a rekordcrate-validatable field.
|
||||||
|
|
||||||
|
### Where rekordcrate (and our RE) AGREE the field is unreproducible — with a caveat
|
||||||
|
rekordcrate documents `empty_candidate` ("maybe links to a chain of empty
|
||||||
|
pages if the database is ever garbage collected") and `next_unused_page`
|
||||||
|
("points past end of file") as allocator/GC state — matching
|
||||||
|
KNOWN_DIVERGENCE.md items 11 and 1. The **historical VALUE** on an edited
|
||||||
|
reference is indeed unreproducible. **BUT** my data-page audit (see the FREEZE
|
||||||
|
section above) found `empty_candidate` is **load-bearing structure**, not just
|
||||||
|
GC trivia: in data/PIONEER every table's last page has
|
||||||
|
`next_page == that table's empty_candidate`, a distinct free page `< next_unused`.
|
||||||
|
So a from-scratch writer must still emit a *valid* distinct `empty_candidate`
|
||||||
|
per table (which it fully controls) even though it can't match an edited ref's
|
||||||
|
specific value. **Confirms** these are correctly on
|
||||||
|
maple's divergence list, not bugs.
|
||||||
|
|
||||||
|
### GAP — none structural for export.pdb
|
||||||
|
maple writes a complete, players-accepted export.pdb. Remaining export.pdb
|
||||||
|
divergences from a reference file are all **edit-history artifacts**
|
||||||
|
(KNOWN_DIVERGENCE.md) that neither library can reproduce from content either.
|
||||||
|
The one content-level gap is the opaque blob (see §4).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. exportExt.pdb — GAP (nobody writes it; maybe nobody needs to)
|
||||||
|
|
||||||
|
- **pyrekordbox:** no support.
|
||||||
|
- **rekordcrate:** no support (its `pdb` module is export.pdb only).
|
||||||
|
- **maple:** does not emit it.
|
||||||
|
|
||||||
|
Format facts (Deep Symmetry export analysis; reverse-engineered by Dominik
|
||||||
|
Stolz, crate-digger issue #11): `exportExt.pdb` is a *second, smaller*
|
||||||
|
DeviceSQL file beside `export.pdb` holding only two table types — **Tags
|
||||||
|
(type 03)** and **Tag Tracks (type 04)** — i.e. the "My Tag" categorisation
|
||||||
|
feature and its tag→track associations. It uses the same page/row container
|
||||||
|
format as export.pdb.
|
||||||
|
|
||||||
|
**Assessment / recommendation:** exportExt.pdb is **optional**. It only carries
|
||||||
|
the My-Tag feature; players that don't find it simply show no custom tags, and
|
||||||
|
tracks/playlists from export.pdb play normally. maple has no tag data to
|
||||||
|
populate anyway. **Do not block a v1 export on this.** If maple ever supports
|
||||||
|
tags, the existing `pager.py`/`PageBuilder` already produces the container
|
||||||
|
format — only two new row serialisers (tag row, tag-track row) and a second
|
||||||
|
`write_pdb`-style call writing the 2-table directory would be needed.
|
||||||
|
Confidence the file is skippable: high (it post-dates the core format and is
|
||||||
|
absent from older exports that still play).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. ANLZ (.DAT / .EXT / .2EX) — the biggest real GAP
|
||||||
|
|
||||||
|
### What the references decode
|
||||||
|
- **pyrekordbox** (`pyrekordbox.anlz`): full **parser** for PMAI + all section
|
||||||
|
tags — PPTH, PQTZ/PQT2 (beat grids), PCOB/PCO2 (cues/loops), PVBR, PSSI
|
||||||
|
(phrase/song-structure, XOR-masked), PWAV/PWV2 (previews), PWV3/4/5
|
||||||
|
(detailed + colour waveforms), PWV6/7/PWVC (.2EX). Writing is **"planned …
|
||||||
|
but the full structure has to be understood first"** — i.e. **read-only
|
||||||
|
today.**
|
||||||
|
- **rekordcrate** (`anlz.rs`): parses the same tag set; magic constants present
|
||||||
|
in source: PMAI, PPTH, PQTZ, PVBR, PWAV, PWV2, PCOB, PCO2, PCPT, PCP2, PSSI,
|
||||||
|
PWV3/4/5. It derives `BinWrite` on the waveform-column structs
|
||||||
|
(`WaveformPreviewColumn` etc., anlz.rs:368-428) and has a `write_encrypted`
|
||||||
|
for the XOR-masked PSSI (anlz.rs:902) — so it has *piece-level* write code,
|
||||||
|
but **no whole-file ANLZ builder / CLI to emit a valid .DAT/.EXT from track
|
||||||
|
audio.**
|
||||||
|
|
||||||
|
### What maple does
|
||||||
|
`anlz.py` writes a **minimal** `ANLZ0000.DAT`: a 28-byte PMAI header + a single
|
||||||
|
**PPTH** section (u4 len + UTF-16BE path). No beat grid, no waveform, no cues,
|
||||||
|
no `.EXT`, no `.2EX`. Rationale in the module docstring: firmware reads PPTH to
|
||||||
|
confirm which audio file the analysis belongs to, and a track whose
|
||||||
|
`analyze_path` points at *nothing* can be hidden from the library, so PPTH
|
||||||
|
alone makes the track "appear analysed enough to surface."
|
||||||
|
|
||||||
|
### CONFIRMATIONS
|
||||||
|
- maple's PMAI/PPTH layout (4-cc + u4 len_header + u4 len_tag; PPTH = u4
|
||||||
|
len_path + UTF-16**BE**, big-endian throughout) matches the format both
|
||||||
|
references parse. The big-endian-for-ANLZ-vs-little-endian-for-pdb split that
|
||||||
|
`anlz.py` calls out is corroborated by both libraries.
|
||||||
|
|
||||||
|
### GAPS maple must close for full faithfulness
|
||||||
|
1. **No `.EXT` file.** Modern CDJs/XDJs (NXS2+) read `ANLZ0000.EXT` for colour
|
||||||
|
waveforms (PWV4/5), extended cues (PCO2), and phrase data (PSSI). Without it
|
||||||
|
maple tracks show **no colour waveform and no phrase/structure** on newer
|
||||||
|
players. The `analyze_path` in the pdb points at the `.DAT`; the player
|
||||||
|
derives the `.EXT` sibling. maple writes neither beyond the `.DAT` PPTH stub.
|
||||||
|
2. **No beat grid (PQTZ).** Without a beat grid, beat-sync / quantize / loop
|
||||||
|
features have nothing to lock to; the track plays but degrades the CDJ
|
||||||
|
experience. This is the highest-value ANLZ section to add after PPTH.
|
||||||
|
3. **No waveform (PWAV/PWV3-5).** Cosmetic but conspicuous — tracks show a flat
|
||||||
|
or absent waveform. pyrekordbox can *parse* these to learn the exact byte
|
||||||
|
layout, and rekordcrate's `BinWrite` waveform-column structs are a concrete
|
||||||
|
reference for the encoding, but **maple must generate the waveform data
|
||||||
|
itself** (decode audio → downsample → colour-band), which neither library
|
||||||
|
provides.
|
||||||
|
4. **Generation, not just layout, is the hard part.** The byte format is fully
|
||||||
|
documented (both libs + Deep Symmetry); what's missing everywhere is the
|
||||||
|
DSP that *produces* beat positions and waveform columns from PCM. That DSP
|
||||||
|
is maple's to build — the references only validate the container.
|
||||||
|
|
||||||
|
**Recommendation:** keep the PPTH-only `.DAT` as the working baseline (it is
|
||||||
|
what gets tracks visible), and treat PQTZ beat grid + a `.EXT` with PWV5 colour
|
||||||
|
waveform as the next ANLZ milestones. Use pyrekordbox's parser as the
|
||||||
|
round-trip oracle (write a section, parse it back, compare). Confidence the
|
||||||
|
PPTH-only stub is sufficient for *visibility/playback*: medium-high (matches
|
||||||
|
maple's own deck testing rationale); confidence it's sufficient for a *complete*
|
||||||
|
experience: low — beat grid/waveform are expected by users.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. master.db (db6/7) + the opaque Content IDs — GAP maple can close via pyrekordbox
|
||||||
|
|
||||||
|
### What pyrekordbox does (this is its core strength) — VALIDATED by re #10
|
||||||
|
- Reads **and writes** the encrypted `master.db`. The db is **SQLCipher4**
|
||||||
|
encrypted with a **single GLOBAL key, identical on every install** (not
|
||||||
|
machine/license-derived). `re` validated the concrete key (FORMAT_SPEC §2
|
||||||
|
Round-3): `402fd482c38817c35ffa8ffb8c7d93143b749e7d315df7a81732a1ff43608497`.
|
||||||
|
pyrekordbox 0.4.4 + `sqlcipher3-wheels` installs and wires up correctly, so a
|
||||||
|
**valid** master.db opens and exposes `DjmdContent` `[see re #10]`.
|
||||||
|
- Exposes `DjmdContent` (one row per track) with: **`ID`** (the rekordbox
|
||||||
|
internal Content ID), `FolderPath`, `FileNameL`, **`AnalysisDataPath`** (the
|
||||||
|
`/PIONEER/USBANLZ/...` analysis path), **`ImagePath`** (artwork), and FK ids
|
||||||
|
to Artist/Album/Genre/Label/Key/Composer; plus `djmdCue`, `djmdPlaylist`/
|
||||||
|
`djmdSongPlaylist`, `djmdHistory`, `djmdMixerParam`, `djmdDevice`, etc.
|
||||||
|
|
||||||
|
> **Reality check from re #10 — currently BLOCKED on data, not on method:**
|
||||||
|
> - The captured `maple-py/captures/p17328_master.db` is **corrupt/truncated**
|
||||||
|
> (zeroed SQLCipher salt + page 1) — no key can open it. The genuine
|
||||||
|
> `exportLibrary.db` shipped beside export.pdb uses a *different* key scheme
|
||||||
|
> that the global master.db key does not open, and pyrekordbox has no
|
||||||
|
> exportLibrary.db support. So the IDs have **not yet been read** on this RE
|
||||||
|
> machine — the path works only against a **live**
|
||||||
|
> `%APPDATA%/Pioneer/rekordbox6/master.db`.
|
||||||
|
> - `opaque_track_id_lo == DjmdContent.ID & 0xFFFFFFFF` is a **strong inference,
|
||||||
|
> still UNTESTED** (the lo range fits the 9-digit Content-ID band), not a
|
||||||
|
> confirmed equality.
|
||||||
|
> - The per-install **seed** (`0x21b4a7ab`) is **NOT** a DjmdContent column and
|
||||||
|
> its source is **still unidentified** (likely a library/USB UUID or
|
||||||
|
> agentRegistry/local_usn-style marker; needs a readable master.db to find).
|
||||||
|
> - pyrekordbox `add_content` lets *rekordbox* assign the Content ID — there is
|
||||||
|
> **no API to force a specific ID** — and it does **not** drive a USB export
|
||||||
|
> (`get_device` is read-only). So pyrekordbox is a **read source of real IDs**,
|
||||||
|
> not a writer/exporter and not a way to dictate IDs.
|
||||||
|
|
||||||
|
### Why this matters for maple (ties to FORMAT_SPEC §2 / KNOWN_DIVERGENCE #9)
|
||||||
|
maple's RE proved the export.pdb track blob at 0x14/0x18 is the rekordbox
|
||||||
|
**Content ID** (per-track, 28-bit band) plus a **per-install seed**, both stored
|
||||||
|
in master.db and **not derivable** from the audio/metadata maple has. maple
|
||||||
|
currently emits a non-zero *stand-in* (`crc32(id|file_path)` lo +
|
||||||
|
`TRACK_OPAQUE_SEED = 0x21b4a7ab`). Therefore:
|
||||||
|
|
||||||
|
- **GAP (closable in principle, not yet proven): real Content IDs.** If maple
|
||||||
|
runs on a machine with rekordbox installed, it can use **pyrekordbox + the
|
||||||
|
global key to open the LIVE `%APPDATA%/Pioneer/rekordbox6/master.db` and read
|
||||||
|
`DjmdContent.ID`** per track, then write those into the pdb blob instead of
|
||||||
|
the stand-in. `re` validated the *mechanism* (global key, DjmdContent schema)
|
||||||
|
but could NOT confirm `ID == opaque_track_id_lo` here because the only
|
||||||
|
captured master.db is corrupt — so this is a **strong-but-untested** lead, not
|
||||||
|
a closed item. Next step (re #10): point pyrekordbox at a live master.db and
|
||||||
|
confirm the equality for the AceMo tracks. The per-install **seed**
|
||||||
|
`0x21b4a7ab` source is still unidentified, so maple keeps the configurable
|
||||||
|
`TRACK_OPAQUE_SEED` either way.
|
||||||
|
- **Caveats:** (a) works only *with a rekordbox install present and a readable
|
||||||
|
master.db* (not the corrupt capture), and only for tracks rekordbox already
|
||||||
|
knows; (b) pyrekordbox `add_content` cannot **force** a chosen ID (rekordbox
|
||||||
|
assigns it) and does **not** drive a USB export, so it can supply IDs but
|
||||||
|
cannot replace maple's writer. Pure from-scratch (no rekordbox install) stays
|
||||||
|
a documented divergence (KNOWN_DIVERGENCE #9), with maple's non-zero stand-in.
|
||||||
|
|
||||||
|
### rekordcrate
|
||||||
|
No master.db support at all — it is a *device-export* (USB) library, not a
|
||||||
|
*library-database* library. So it cannot help with Content IDs.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. mysettings (\*SETTING.DAT) — GAP (small, easy) maple doesn't write
|
||||||
|
|
||||||
|
- **pyrekordbox:** reads **and writes/builds** `DEVSETTING.DAT`,
|
||||||
|
`MYSETTING.DAT` (40B, CDJ play settings), `MYSETTING2.DAT` (40B), and
|
||||||
|
`DJMMYSETTING.DAT` (52B, DJM mixer) — dict-style get/set + save. (`DEVSETTING`
|
||||||
|
payload still partly ununderstood.)
|
||||||
|
- **rekordcrate:** `setting` module **parses** these (read-only).
|
||||||
|
- **maple:** does **not** write any of them.
|
||||||
|
|
||||||
|
These live in `PIONEER/` on the USB alongside `rekordbox/`. They are **optional
|
||||||
|
preference files** — absent, the player just uses its own defaults; tracks and
|
||||||
|
playlists are unaffected. **Recommendation:** low priority. If desired,
|
||||||
|
pyrekordbox can *build* default ones in a few lines (it's read/write), so maple
|
||||||
|
could shell out to / import pyrekordbox to drop standard `MYSETTING.DAT` +
|
||||||
|
`MYSETTING2.DAT` rather than re-implementing the 40-byte structs. Confidence
|
||||||
|
they're optional for playback: high.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Overall directory layout + path schemes (cross-check) `[see re #11 / FORMAT_SPEC §7]`
|
||||||
|
|
||||||
|
Both references and Deep Symmetry agree on the USB tree maple targets:
|
||||||
|
`/PIONEER/rekordbox/export.pdb` (+ optional `exportExt.pdb`),
|
||||||
|
`/PIONEER/USBANLZ/<...>/ANLZ0000.{DAT,EXT,2EX}`,
|
||||||
|
`/PIONEER/Artwork/<...>/a<id>.jpg`, plus optional `/PIONEER/*SETTING.DAT`.
|
||||||
|
maple writes `PIONEER/rekordbox/export.pdb` (`cli.py:156`) and
|
||||||
|
`PIONEER/USBANLZ/<bucket>/<id>/ANLZ0000.DAT` (`cli.py:92`).
|
||||||
|
|
||||||
|
`re` resolved both path schemes (#11, FORMAT_SPEC §7):
|
||||||
|
|
||||||
|
- **Artwork — EXACTLY reproducible, and maple already matches.** Real rekordbox
|
||||||
|
uses `/PIONEER/Artwork/{id//20+1:05d}/a{id}.jpg` (+ a `a{id}_m.jpg` 240px
|
||||||
|
thumb), 20 ids per bucket folder (bucket 1 = ids 1-19). `re` verified
|
||||||
|
176/176 in data/MAPLE, and maple's `artwork._bucket()` already implements this
|
||||||
|
rule. **Reproducible, confirmed — no gap.**
|
||||||
|
|
||||||
|
- **USBANLZ — NOT reproducible; maple's own scheme is correct-by-construction.**
|
||||||
|
Real rekordbox uses `/PIONEER/USBANLZ/P{bucket}/{8hex}/ANLZ0000.DAT` where the
|
||||||
|
8-hex is an **opaque per-track analysis id** (NOT the track id, NOT the row
|
||||||
|
0x14 lo, NOT any crc of the path — `re` ruled these out) and the P-bucket
|
||||||
|
(0-127) is **not a derivable function** of it. Like the 0x14 blob, it is
|
||||||
|
DB-sourced and non-reproducible. maple synthesises its own
|
||||||
|
`P{(crc32(path)>>20)&0xFFF:03X}/{crc32(path):08X}` and writes the `.DAT`
|
||||||
|
there, so the pdb's `analyze_path` and the on-disk file always agree. **This
|
||||||
|
is fine:** the player only requires every pdb path to resolve to a real
|
||||||
|
on-disk file, not to match rekordbox's id scheme.
|
||||||
|
|
||||||
|
> **Gotcha for the gaps below (from `re`):** `tools/build_acemo.py` MIRRORS a
|
||||||
|
> reference export's paths verbatim and writes **no** ANLZ/artwork files (it's a
|
||||||
|
> byte-diff fixture builder). The **production** `cli.py build()` is the one that
|
||||||
|
> actually writes each ANLZ/artwork file at the recorded path. Don't conflate
|
||||||
|
> the two when reasoning about what maple emits on disk.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Bottom line — prioritised GAP list for a full from-scratch export
|
||||||
|
|
||||||
|
1. **(high, lead — untested) Real opaque Content IDs via pyrekordbox/master.db**
|
||||||
|
— mechanism validated by `re` (global SQLCipher key `402fd482…` +
|
||||||
|
`Rekordbox6Database` reads `DjmdContent`), but `DjmdContent.ID ==
|
||||||
|
opaque_track_id_lo` is a **strong INFERENCE, NOT validated** (the only
|
||||||
|
captured master.db is corrupt — zeroed salt — so `re` never confirmed the
|
||||||
|
equality on real data). Would close KNOWN_DIVERGENCE #9 *if* confirmed against
|
||||||
|
a readable **live** `%APPDATA%/Pioneer/rekordbox6/master.db`.
|
||||||
|
`[see re #10 / FORMAT_SPEC §2 Round-3]`
|
||||||
|
2. **(OPEN — Ghidra trace pending) Per-install `opaque_seed` source.** The seed
|
||||||
|
(`0x21b4a7ab` / `0xd6d3a567`) is **NOT** a `DjmdContent` column and `re` has
|
||||||
|
**not located it** — likely a per-library UUID/agent/usn marker or a computed
|
||||||
|
value. It needs a readable master.db to inspect candidate rows, OR a Ghidra
|
||||||
|
trace of rekordbox.exe (which `re` can now attempt — the Ghidra MCP just came
|
||||||
|
online), so it is **not permanently blocked.** Until resolved, maple keeps the
|
||||||
|
configurable `TRACK_OPAQUE_SEED` stand-in. This is the one genuinely open
|
||||||
|
question touching the opaque blob. `[see re #10 — OPEN; FORMAT_SPEC §2 Round-3]`
|
||||||
|
3. **(high) ANLZ beat grid (PQTZ) + an `.EXT` colour waveform (PWV5)** — needs
|
||||||
|
DSP maple must build; references validate the byte format only. Biggest
|
||||||
|
functional gap on modern players.
|
||||||
|
4. **(structural — in progress) Per-table `empty_candidate` / last-page
|
||||||
|
`next_page`** — the FREEZE fix (#13). maple must point each table's last page
|
||||||
|
at a distinct free page recorded in the dir's `empty_candidate`, not a shared
|
||||||
|
sentinel with `empty_candidate=0`. See the FREEZE section in §1.
|
||||||
|
5. **(low) mysettings** — optional; pyrekordbox can build them.
|
||||||
|
6. **(low/skip) exportExt.pdb** — optional My-Tag data; only if maple adds tags.
|
||||||
|
7. **(reference, not a gap) Use rekordcrate as a read-back oracle** — parse
|
||||||
|
maple's `export.pdb` with rekordcrate's CLI to catch container regressions;
|
||||||
|
use pyrekordbox's anlz parser as the oracle for any new ANLZ sections.
|
||||||
|
**Caveat (proven this session):** rekordcrate's parser tolerates things the
|
||||||
|
firmware rejects (the `next_page` self-loop) and is itself wrong on some
|
||||||
|
fields (>255-row count, per-type alignment) — so it is a *regression* oracle,
|
||||||
|
NOT a correctness authority. Validate load-critical fields against REAL
|
||||||
|
export bytes (e.g. data/PIONEER), not against rekordcrate.
|
||||||
|
|
||||||
|
**Headline confirmation for the team:** rekordcrate's *writer* independently
|
||||||
|
validates maple's export.pdb track-row layout, DeviceSQL string encoding, and
|
||||||
|
page/row-group container down to the 0x5e/0x2a offsets — but it is *agnostic*
|
||||||
|
exactly where maple's RE added knowledge (`index_shift` formula, the opaque-id
|
||||||
|
semantics). So maple's FORMAT_SPEC is the authority for those fields, and the
|
||||||
|
remaining real work is **ANLZ generation** and **pulling the real Content IDs
|
||||||
|
from master.db** — neither of which any existing library does end-to-end.
|
||||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,595 @@
|
|||||||
|
// HIT 0x4a68 @141f0a08b in FUN_141f09e70@141f09e70 : LEA RCX,[RDI + 0x4a68]
|
||||||
|
// HIT 0x4a68 @141f0a08b in FUN_141f09e70@141f09e70 : LEA RCX,[RDI + 0x4a68]
|
||||||
|
// HIT 0x4a68 @141f15d17 in FUN_141f15bf0@141f15bf0 : LEA RCX,[RBX + 0x4a68]
|
||||||
|
// HIT 0x4a68 @141f15d17 in FUN_141f15bf0@141f15bf0 : LEA RCX,[RBX + 0x4a68]
|
||||||
|
// HIT 0x4a68 @141f637b7 in FUN_141f63210@141f63210 : MOV RAX,qword ptr [RBX + 0x4a68]
|
||||||
|
// HIT 0x4a68 @141f637b7 in FUN_141f63210@141f63210 : MOV RAX,qword ptr [RBX + 0x4a68]
|
||||||
|
// HIT 0x4a68 @141f637be in FUN_141f63210@141f63210 : LEA RCX,[RBX + 0x4a68]
|
||||||
|
// HIT 0x4a68 @141f637be in FUN_141f63210@141f63210 : LEA RCX,[RBX + 0x4a68]
|
||||||
|
// HIT 0x4a68 @141f637da in FUN_141f63210@141f63210 : LEA RCX,[RBX + 0x4a68]
|
||||||
|
// HIT 0x4a68 @141f637da in FUN_141f63210@141f63210 : LEA RCX,[RBX + 0x4a68]
|
||||||
|
// HIT 0x4a68 @141f637e1 in FUN_141f63210@141f63210 : MOV RAX,qword ptr [RBX + 0x4a68]
|
||||||
|
// HIT 0x4a68 @141f637e1 in FUN_141f63210@141f63210 : MOV RAX,qword ptr [RBX + 0x4a68]
|
||||||
|
// --- decompiling containing functions ---
|
||||||
|
|
||||||
|
//===== FUN_141f09e70 @ 141f09e70 =====
|
||||||
|
|
||||||
|
undefined8 *
|
||||||
|
FUN_141f09e70(undefined8 *param_1,undefined4 param_2,undefined8 param_3,longlong param_4)
|
||||||
|
|
||||||
|
{
|
||||||
|
undefined8 *puVar1;
|
||||||
|
char cVar2;
|
||||||
|
undefined8 uVar3;
|
||||||
|
undefined8 *puVar4;
|
||||||
|
undefined8 *puVar5;
|
||||||
|
longlong lVar6;
|
||||||
|
undefined8 *puVar7;
|
||||||
|
undefined8 *puVar8;
|
||||||
|
longlong local_res20;
|
||||||
|
undefined1 local_f8 [4];
|
||||||
|
undefined8 local_f4;
|
||||||
|
undefined1 local_ec;
|
||||||
|
undefined4 local_e8;
|
||||||
|
undefined1 local_e0 [8];
|
||||||
|
undefined1 local_d8 [8];
|
||||||
|
undefined1 local_d0 [8];
|
||||||
|
undefined1 local_c8 [8];
|
||||||
|
undefined2 local_c0;
|
||||||
|
undefined1 local_b8 [8];
|
||||||
|
undefined1 local_b0;
|
||||||
|
undefined1 local_a8 [8];
|
||||||
|
undefined4 local_a0;
|
||||||
|
undefined1 local_98 [8];
|
||||||
|
undefined1 local_90 [8];
|
||||||
|
undefined1 local_88 [8];
|
||||||
|
undefined1 local_80 [8];
|
||||||
|
char *local_78;
|
||||||
|
undefined1 local_70 [16];
|
||||||
|
undefined1 local_60 [16];
|
||||||
|
undefined1 local_50 [24];
|
||||||
|
|
||||||
|
puVar8 = (undefined8 *)0x0;
|
||||||
|
uVar3 = FUN_1423d5980(&local_res20,param_3);
|
||||||
|
FUN_141f09b00(param_1,0xffffffff,uVar3);
|
||||||
|
FUN_14246e8b0(param_1 + 0x134);
|
||||||
|
*param_1 = djplay::HidMapCDJ2000NXS2::vftable;
|
||||||
|
param_1[0x14] = djplay::HidMapCDJ2000NXS2::vftable;
|
||||||
|
puVar1 = param_1 + 0x15;
|
||||||
|
*puVar1 = djplay::HidMapCDJ2000NXS2::vftable;
|
||||||
|
param_1[0x134] = djplay::HidMapCDJ2000NXS2::vftable;
|
||||||
|
param_1[0x138] = djplay::HidMapCDJ2000NXS2::vftable;
|
||||||
|
param_1[0x139] = djplay::HidMapCDJ2000NXS2::vftable;
|
||||||
|
param_1[0x13a] = djplay::HidMapCDJ2000NXS2::vftable;
|
||||||
|
*(undefined4 *)(param_1 + 0x13b) = 0x25f8;
|
||||||
|
param_1[0x1a3] = 0;
|
||||||
|
param_1[0x1a4] = 0;
|
||||||
|
param_1[0x1bb] = 0;
|
||||||
|
param_1[0x1bd] = 0;
|
||||||
|
param_1[0x1be] = 0;
|
||||||
|
param_1[0x1bf] = 0;
|
||||||
|
param_1[0x1c0] = 0;
|
||||||
|
*(undefined4 *)(param_1 + 0x1c1) = 0;
|
||||||
|
*(undefined2 *)((longlong)param_1 + 0xe0c) = 0;
|
||||||
|
*(undefined1 *)((longlong)param_1 + 0xe0e) = 0;
|
||||||
|
*(undefined4 *)(param_1 + 0x1c2) = 3;
|
||||||
|
puVar7 = param_1 + 0x1c4;
|
||||||
|
*puVar7 = 0;
|
||||||
|
param_1[0x1c5] = 0;
|
||||||
|
param_1[0x1c6] = 0;
|
||||||
|
param_1[0x1c8] = 0;
|
||||||
|
FUN_141ee27c0(param_1 + 0x1c9);
|
||||||
|
_eh_vector_constructor_iterator_(param_1 + 0x2d5,0x470,4,FUN_141ee3f50,FUN_141ee3fe0);
|
||||||
|
FUN_141ee3f50(param_1 + 0x50d);
|
||||||
|
FUN_141ee4d90(param_1 + 0x59c);
|
||||||
|
FUN_141ee5080(param_1 + 0x62b);
|
||||||
|
FUN_141ee5400(param_1 + 0x6ba);
|
||||||
|
FUN_141ee5970(param_1 + 0x74a);
|
||||||
|
FUN_141ee5db0(param_1 + 0x7da);
|
||||||
|
FUN_141ee6210(param_1 + 0x86a);
|
||||||
|
FUN_141ee97f0(param_1 + 0x8fb);
|
||||||
|
FUN_141ee9a10(param_1 + 0x907);
|
||||||
|
FUN_141ee9c10(param_1 + 0x915);
|
||||||
|
FUN_141ee9c10(param_1 + 0x920);
|
||||||
|
FUN_141ee9c10(param_1 + 0x92b);
|
||||||
|
FUN_141ee9c10(param_1 + 0x936);
|
||||||
|
FUN_141ee9e30(param_1 + 0x941);
|
||||||
|
FUN_141eea060(param_1 + 0x94d);
|
||||||
|
FUN_141eea150(param_1 + 0x959);
|
||||||
|
FUN_141eea330(param_1 + 0x964);
|
||||||
|
*(undefined2 *)(param_1 + 0x96f) = 0;
|
||||||
|
*(undefined1 *)((longlong)param_1 + 0x4b7a) = 0;
|
||||||
|
*(undefined4 *)((longlong)param_1 + 0x4b7c) = 0;
|
||||||
|
*(undefined1 *)(param_1 + 0x970) = 1;
|
||||||
|
param_1[0x197] = 0;
|
||||||
|
puVar4 = (undefined8 *)FUN_1423ce840(local_98,"CDJ-TOUR1");
|
||||||
|
cVar2 = FUN_1423d2890(param_3,*puVar4);
|
||||||
|
if (cVar2 != '\0') {
|
||||||
|
*(undefined1 *)((longlong)param_1 + 0xe0b) = 1;
|
||||||
|
}
|
||||||
|
*(undefined4 *)((longlong)param_1 + 0xe1a) = 0;
|
||||||
|
*(undefined1 *)((longlong)param_1 + 0xe1e) = 0;
|
||||||
|
LOCK();
|
||||||
|
*(undefined4 *)(param_1 + 0x1c8) = 0;
|
||||||
|
UNLOCK();
|
||||||
|
LOCK();
|
||||||
|
*(undefined4 *)((longlong)param_1 + 0xe44) = 0;
|
||||||
|
UNLOCK();
|
||||||
|
puVar5 = (undefined8 *)FUN_14217c644(0x40);
|
||||||
|
puVar4 = puVar8;
|
||||||
|
if (puVar5 != (undefined8 *)0x0) {
|
||||||
|
FUN_1423aa4f0(puVar5);
|
||||||
|
puVar5[6] = 0;
|
||||||
|
*(undefined4 *)(puVar5 + 7) = 8;
|
||||||
|
puVar4 = puVar5;
|
||||||
|
}
|
||||||
|
param_1[0x1c7] = puVar4;
|
||||||
|
lVar6 = FUN_14217c644(0x478);
|
||||||
|
puVar4 = puVar8;
|
||||||
|
if (lVar6 != 0) {
|
||||||
|
puVar4 = (undefined8 *)FUN_141ee41c0(lVar6,8);
|
||||||
|
}
|
||||||
|
param_1[0x59b] = puVar4;
|
||||||
|
local_f8[0] = 0;
|
||||||
|
local_f4 = 0;
|
||||||
|
local_ec = 0;
|
||||||
|
local_e8 = 0;
|
||||||
|
FUN_1423d59e0(local_e0);
|
||||||
|
FUN_1423d59e0(local_d8);
|
||||||
|
FUN_1423d59e0(local_d0);
|
||||||
|
FUN_1423d59e0(local_c8);
|
||||||
|
local_c0 = 0;
|
||||||
|
FUN_1423d59e0(local_b8);
|
||||||
|
local_b0 = 0;
|
||||||
|
FUN_1423d59e0(local_a8);
|
||||||
|
local_a0 = 0;
|
||||||
|
lVar6 = param_1[0x1c5];
|
||||||
|
if (lVar6 == param_1[0x1c6]) {
|
||||||
|
FUN_141edc340(puVar7,lVar6,local_f8);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
FUN_141068df0(lVar6,local_f8);
|
||||||
|
param_1[0x1c5] = param_1[0x1c5] + 0x60;
|
||||||
|
}
|
||||||
|
lVar6 = param_1[0x1c5];
|
||||||
|
if (lVar6 == param_1[0x1c6]) {
|
||||||
|
FUN_141edc340(puVar7,lVar6,local_f8);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
FUN_141068df0(lVar6,local_f8);
|
||||||
|
param_1[0x1c5] = param_1[0x1c5] + 0x60;
|
||||||
|
}
|
||||||
|
lVar6 = param_1[0x1c5];
|
||||||
|
if (lVar6 == param_1[0x1c6]) {
|
||||||
|
FUN_141edc340(puVar7,lVar6,local_f8);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
FUN_141068df0(lVar6,local_f8);
|
||||||
|
param_1[0x1c5] = param_1[0x1c5] + 0x60;
|
||||||
|
}
|
||||||
|
lVar6 = param_1[0x1c5];
|
||||||
|
if (lVar6 == param_1[0x1c6]) {
|
||||||
|
FUN_141edc340(puVar7,lVar6,local_f8);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
FUN_141068df0(lVar6,local_f8);
|
||||||
|
param_1[0x1c5] = param_1[0x1c5] + 0x60;
|
||||||
|
}
|
||||||
|
lVar6 = param_1[0x1c5];
|
||||||
|
if (lVar6 == param_1[0x1c6]) {
|
||||||
|
FUN_141edc340(puVar7,lVar6,local_f8);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
FUN_141068df0(lVar6,local_f8);
|
||||||
|
param_1[0x1c5] = param_1[0x1c5] + 0x60;
|
||||||
|
}
|
||||||
|
lVar6 = param_1[0x1c5];
|
||||||
|
if (lVar6 == param_1[0x1c6]) {
|
||||||
|
FUN_141edc340(puVar7,lVar6,local_f8);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
FUN_141068df0(lVar6,local_f8);
|
||||||
|
param_1[0x1c5] = param_1[0x1c5] + 0x60;
|
||||||
|
}
|
||||||
|
lVar6 = param_1[0x1c5];
|
||||||
|
if (lVar6 == param_1[0x1c6]) {
|
||||||
|
FUN_141edc340(puVar7,lVar6,local_f8);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
FUN_141068df0(lVar6,local_f8);
|
||||||
|
param_1[0x1c5] = param_1[0x1c5] + 0x60;
|
||||||
|
}
|
||||||
|
lVar6 = param_1[0x1c5];
|
||||||
|
if (lVar6 == param_1[0x1c6]) {
|
||||||
|
FUN_141edc340(puVar7,lVar6,local_f8);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
FUN_141068df0(lVar6,local_f8);
|
||||||
|
param_1[0x1c5] = param_1[0x1c5] + 0x60;
|
||||||
|
}
|
||||||
|
if (param_4 == 0) {
|
||||||
|
uVar3 = FUN_1423e00f0(local_60,0x41,1);
|
||||||
|
local_78 = "@Activate";
|
||||||
|
FUN_1423dfec0(local_70,uVar3);
|
||||||
|
FUN_141f07e80(param_1 + 0x132,local_50,&local_78);
|
||||||
|
FUN_1423dffc0(local_70);
|
||||||
|
FUN_1423dffc0(local_60);
|
||||||
|
puVar4 = (undefined8 *)FUN_14217c644(0x30);
|
||||||
|
puVar7 = puVar8;
|
||||||
|
if (puVar4 != (undefined8 *)0x0) {
|
||||||
|
*puVar4 = djplay::HidExtentFormat::vftable;
|
||||||
|
*(undefined2 *)(puVar4 + 1) = 0xf0;
|
||||||
|
puVar4[2] = 0;
|
||||||
|
puVar4[3] = 0;
|
||||||
|
puVar4[4] = 0;
|
||||||
|
puVar4[5] = 0;
|
||||||
|
puVar7 = puVar4;
|
||||||
|
}
|
||||||
|
puVar4 = (undefined8 *)param_1[0x1be];
|
||||||
|
param_1[0x1be] = puVar7;
|
||||||
|
if (puVar4 != (undefined8 *)0x0) {
|
||||||
|
(**(code **)*puVar4)(puVar4,1);
|
||||||
|
}
|
||||||
|
uVar3 = FUN_1423d5720(&local_res20,"@AuthReq");
|
||||||
|
FUN_141f1be20(param_1,0,0x11,uVar3,puVar1);
|
||||||
|
uVar3 = FUN_1423d5720(local_90,"@AuthChallengeA");
|
||||||
|
FUN_141f1be20(param_1,0,0x12,uVar3,puVar1);
|
||||||
|
uVar3 = FUN_1423d5720(local_88,"@AuthResponseA");
|
||||||
|
FUN_141f1be20(param_1,0,0x13,uVar3,puVar1);
|
||||||
|
uVar3 = FUN_1423d5720(local_80,"@AuthResponseE");
|
||||||
|
FUN_141f1be20(param_1,0,0x14,uVar3,puVar1);
|
||||||
|
uVar3 = FUN_1423d5720(local_98,"@AuthEnd");
|
||||||
|
FUN_141f1be20(param_1,0,0x15,uVar3,puVar1);
|
||||||
|
local_res20 = FUN_14217c644(0x1d0);
|
||||||
|
if (local_res20 != 0) {
|
||||||
|
puVar8 = (undefined8 *)FUN_141f093a0(local_res20,puVar1,0);
|
||||||
|
}
|
||||||
|
param_1[0x1bd] = puVar8;
|
||||||
|
*(undefined4 *)(puVar8 + 0x2f) = 4;
|
||||||
|
if (param_1[0x1bd] != 0) {
|
||||||
|
FUN_141f69980(param_1[0x1bd],param_1 + 0x13a);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
param_1[0x1bd] = param_4;
|
||||||
|
*(undefined1 *)((longlong)param_1 + 0xe09) = 1;
|
||||||
|
*(undefined4 *)((longlong)param_1 + 0x4b7c) = param_2;
|
||||||
|
FUN_141f40750(param_1);
|
||||||
|
}
|
||||||
|
FUN_1423d59b0(local_a8);
|
||||||
|
FUN_1423d59b0(local_b8);
|
||||||
|
FUN_1423d59b0(local_c8);
|
||||||
|
FUN_1423d59b0(local_d0);
|
||||||
|
FUN_1423d59b0(local_d8);
|
||||||
|
FUN_1423d59b0(local_e0);
|
||||||
|
FUN_1423d59b0(param_3);
|
||||||
|
return param_1;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
//===== FUN_141f15bf0 @ 141f15bf0 =====
|
||||||
|
|
||||||
|
void FUN_141f15bf0(undefined8 *param_1)
|
||||||
|
|
||||||
|
{
|
||||||
|
undefined8 *puVar1;
|
||||||
|
LPCRITICAL_SECTION lpCriticalSection;
|
||||||
|
longlong *plVar2;
|
||||||
|
undefined8 uVar3;
|
||||||
|
int iVar4;
|
||||||
|
|
||||||
|
*param_1 = djplay::HidMapCDJ2000NXS2::vftable;
|
||||||
|
param_1[0x14] = djplay::HidMapCDJ2000NXS2::vftable;
|
||||||
|
param_1[0x15] = djplay::HidMapCDJ2000NXS2::vftable;
|
||||||
|
param_1[0x134] = djplay::HidMapCDJ2000NXS2::vftable;
|
||||||
|
param_1[0x138] = djplay::HidMapCDJ2000NXS2::vftable;
|
||||||
|
param_1[0x139] = djplay::HidMapCDJ2000NXS2::vftable;
|
||||||
|
param_1[0x13a] = djplay::HidMapCDJ2000NXS2::vftable;
|
||||||
|
if (*(char *)((longlong)param_1 + 0xe09) == '\0') {
|
||||||
|
if (param_1[0x1bd] != 0) {
|
||||||
|
FUN_141f72e10(param_1[0x1bd],param_1 + 0x13a);
|
||||||
|
}
|
||||||
|
puVar1 = (undefined8 *)param_1[0x1bd];
|
||||||
|
if (puVar1 != (undefined8 *)0x0) {
|
||||||
|
(**(code **)*puVar1)(puVar1,1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
uVar3 = FUN_14106db40();
|
||||||
|
FUN_141ed5ce0(uVar3,param_1 + 0x138);
|
||||||
|
uVar3 = FUN_1414518c0();
|
||||||
|
FUN_141ede760(uVar3,param_1 + 0x139);
|
||||||
|
lpCriticalSection = (LPCRITICAL_SECTION)param_1[0x1c7];
|
||||||
|
if (lpCriticalSection != (LPCRITICAL_SECTION)0x0) {
|
||||||
|
DeleteCriticalSection(lpCriticalSection);
|
||||||
|
thunk_FUN_14217cc5c(lpCriticalSection,0x40);
|
||||||
|
}
|
||||||
|
puVar1 = (undefined8 *)param_1[0x59b];
|
||||||
|
if (puVar1 != (undefined8 *)0x0) {
|
||||||
|
(**(code **)*puVar1)(puVar1,1);
|
||||||
|
}
|
||||||
|
FUN_141eea370(param_1 + 0x964);
|
||||||
|
FUN_141eea1a0(param_1 + 0x959);
|
||||||
|
FUN_141eea0a0(param_1 + 0x94d);
|
||||||
|
FUN_141ee9e80(param_1 + 0x941);
|
||||||
|
FUN_141ee9c60(param_1 + 0x936);
|
||||||
|
FUN_141ee9c60(param_1 + 0x92b);
|
||||||
|
FUN_141ee9c60(param_1 + 0x920);
|
||||||
|
FUN_141ee9c60(param_1 + 0x915);
|
||||||
|
FUN_141ee9a70(param_1 + 0x907);
|
||||||
|
FUN_141ee9860(param_1 + 0x8fb);
|
||||||
|
FUN_141ee6340(param_1 + 0x86a);
|
||||||
|
FUN_141ee5ee0(param_1 + 0x7da);
|
||||||
|
FUN_141ee5aa0(param_1 + 0x74a);
|
||||||
|
FUN_141ee54a0(param_1 + 0x6ba);
|
||||||
|
FUN_141ee5120(param_1 + 0x62b);
|
||||||
|
FUN_141ee4ea0(param_1 + 0x59c);
|
||||||
|
FUN_141ee3fe0(param_1 + 0x50d);
|
||||||
|
_eh_vector_destructor_iterator_(param_1 + 0x2d5,0x470,4,FUN_141ee3fe0);
|
||||||
|
FUN_141ee2880(param_1 + 0x1c9);
|
||||||
|
FUN_141edd2a0(param_1 + 0x1c4);
|
||||||
|
iVar4 = 0;
|
||||||
|
if (0 < *(int *)((longlong)param_1 + 0xe04)) {
|
||||||
|
do {
|
||||||
|
FUN_1423dffc0((longlong)iVar4 * 0x10 + param_1[0x1bf]);
|
||||||
|
iVar4 = iVar4 + 1;
|
||||||
|
} while (iVar4 < *(int *)((longlong)param_1 + 0xe04));
|
||||||
|
}
|
||||||
|
*(undefined4 *)((longlong)param_1 + 0xe04) = 0;
|
||||||
|
FUN_14230889c(param_1[0x1bf]);
|
||||||
|
puVar1 = (undefined8 *)param_1[0x1be];
|
||||||
|
if (puVar1 != (undefined8 *)0x0) {
|
||||||
|
(**(code **)*puVar1)(puVar1,1);
|
||||||
|
}
|
||||||
|
plVar2 = (longlong *)param_1[0x1bb];
|
||||||
|
if (plVar2 != (longlong *)0x0) {
|
||||||
|
(**(code **)(*plVar2 + 0x38))(plVar2,1);
|
||||||
|
}
|
||||||
|
puVar1 = (undefined8 *)param_1[0x1a4];
|
||||||
|
if (puVar1 != (undefined8 *)0x0) {
|
||||||
|
(**(code **)*puVar1)(puVar1,1);
|
||||||
|
}
|
||||||
|
puVar1 = (undefined8 *)param_1[0x1a3];
|
||||||
|
if (puVar1 != (undefined8 *)0x0) {
|
||||||
|
(**(code **)*puVar1)(puVar1,1);
|
||||||
|
}
|
||||||
|
param_1[0x13a] = djplay::DeviceAuth::Listener::vftable;
|
||||||
|
param_1[0x139] = djplay::TrackInformationManager::Listener::vftable;
|
||||||
|
param_1[0x138] = djplay::DisplayDataManager::Listener::vftable;
|
||||||
|
FUN_14246e800(param_1 + 0x134);
|
||||||
|
FUN_141f15b10(param_1);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
//===== FUN_141f63210 @ 141f63210 =====
|
||||||
|
|
||||||
|
void FUN_141f63210(longlong param_1,undefined8 *param_2,undefined4 *param_3,undefined8 param_4,
|
||||||
|
undefined1 param_5)
|
||||||
|
|
||||||
|
{
|
||||||
|
char cVar1;
|
||||||
|
undefined8 uVar2;
|
||||||
|
undefined8 local_18 [2];
|
||||||
|
|
||||||
|
switch(param_5) {
|
||||||
|
case 0x21:
|
||||||
|
cVar1 = (**(code **)(*(longlong *)(param_1 + 0xe48) + 0x10))(param_1 + 0xe48);
|
||||||
|
if (cVar1 != '\0') {
|
||||||
|
local_18[0] = 0;
|
||||||
|
uVar2 = (**(code **)(*(longlong *)(param_1 + 0xe48) + 8))(param_1 + 0xe48,local_18,param_4);
|
||||||
|
*param_2 = uVar2;
|
||||||
|
goto LAB_141f63871;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 0x22:
|
||||||
|
cVar1 = (**(code **)(*(longlong *)(param_1 + 0x16a8) + 0x10))(param_1 + 0x16a8);
|
||||||
|
if (cVar1 != '\0') {
|
||||||
|
local_18[0] = 0;
|
||||||
|
uVar2 = (**(code **)(*(longlong *)(param_1 + 0x16a8) + 8))(param_1 + 0x16a8,local_18,param_4);
|
||||||
|
*param_2 = uVar2;
|
||||||
|
goto LAB_141f63871;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 0x23:
|
||||||
|
cVar1 = (**(code **)(*(longlong *)(param_1 + 0x1b18) + 0x10))(param_1 + 0x1b18);
|
||||||
|
if (cVar1 != '\0') {
|
||||||
|
local_18[0] = 0;
|
||||||
|
uVar2 = (**(code **)(*(longlong *)(param_1 + 0x1b18) + 8))(param_1 + 0x1b18,local_18,param_4);
|
||||||
|
*param_2 = uVar2;
|
||||||
|
goto LAB_141f63871;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 0x24:
|
||||||
|
cVar1 = (**(code **)(*(longlong *)(param_1 + 0x1f88) + 0x10))(param_1 + 0x1f88);
|
||||||
|
if (cVar1 != '\0') {
|
||||||
|
local_18[0] = 0;
|
||||||
|
uVar2 = (**(code **)(*(longlong *)(param_1 + 0x1f88) + 8))(param_1 + 0x1f88,local_18,param_4);
|
||||||
|
*param_2 = uVar2;
|
||||||
|
goto LAB_141f63871;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 0x25:
|
||||||
|
cVar1 = (**(code **)(*(longlong *)(param_1 + 0x23f8) + 0x10))(param_1 + 0x23f8);
|
||||||
|
if (cVar1 != '\0') {
|
||||||
|
local_18[0] = 0;
|
||||||
|
uVar2 = (**(code **)(*(longlong *)(param_1 + 0x23f8) + 8))(param_1 + 0x23f8,local_18,param_4);
|
||||||
|
*param_2 = uVar2;
|
||||||
|
goto LAB_141f63871;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 0x26:
|
||||||
|
cVar1 = (**(code **)(*(longlong *)(param_1 + 0x2868) + 0x10))(param_1 + 0x2868);
|
||||||
|
if (cVar1 != '\0') {
|
||||||
|
local_18[0] = 0;
|
||||||
|
uVar2 = (**(code **)(*(longlong *)(param_1 + 0x2868) + 8))(param_1 + 0x2868,local_18,param_4);
|
||||||
|
*param_2 = uVar2;
|
||||||
|
goto LAB_141f63871;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 0x2a:
|
||||||
|
if ((*(longlong **)(param_1 + 0x2cd8) != (longlong *)0x0) &&
|
||||||
|
(cVar1 = (**(code **)(**(longlong **)(param_1 + 0x2cd8) + 0x10))(), cVar1 != '\0')) {
|
||||||
|
local_18[0] = 0;
|
||||||
|
uVar2 = (**(code **)(**(longlong **)(param_1 + 0x2cd8) + 8))
|
||||||
|
(*(longlong **)(param_1 + 0x2cd8),local_18,param_4);
|
||||||
|
*param_2 = uVar2;
|
||||||
|
goto LAB_141f63871;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 0x2b:
|
||||||
|
cVar1 = (**(code **)(*(longlong *)(param_1 + 0x2ce0) + 0x10))(param_1 + 0x2ce0);
|
||||||
|
if (cVar1 != '\0') {
|
||||||
|
local_18[0] = 0;
|
||||||
|
uVar2 = (**(code **)(*(longlong *)(param_1 + 0x2ce0) + 8))(param_1 + 0x2ce0,local_18,param_4);
|
||||||
|
*param_2 = uVar2;
|
||||||
|
goto LAB_141f63871;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 0x2c:
|
||||||
|
cVar1 = (**(code **)(*(longlong *)(param_1 + 0x3158) + 0x10))(param_1 + 0x3158);
|
||||||
|
if (cVar1 != '\0') {
|
||||||
|
local_18[0] = 0;
|
||||||
|
uVar2 = (**(code **)(*(longlong *)(param_1 + 0x3158) + 8))(param_1 + 0x3158,local_18,param_4);
|
||||||
|
*param_2 = uVar2;
|
||||||
|
goto LAB_141f63871;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 0x2d:
|
||||||
|
cVar1 = (**(code **)(*(longlong *)(param_1 + 0x35d0) + 0x10))(param_1 + 0x35d0);
|
||||||
|
if (cVar1 != '\0') {
|
||||||
|
local_18[0] = 0;
|
||||||
|
uVar2 = (**(code **)(*(longlong *)(param_1 + 0x35d0) + 8))(param_1 + 0x35d0,local_18,param_4);
|
||||||
|
*param_2 = uVar2;
|
||||||
|
goto LAB_141f63871;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 0x2e:
|
||||||
|
if ((*(char *)(param_1 + 0xe0c) == '\0') &&
|
||||||
|
(cVar1 = (**(code **)(*(longlong *)(param_1 + 0x3a50) + 0x10))(param_1 + 0x3a50),
|
||||||
|
cVar1 != '\0')) {
|
||||||
|
local_18[0] = 0;
|
||||||
|
uVar2 = (**(code **)(*(longlong *)(param_1 + 0x3a50) + 8))(param_1 + 0x3a50,local_18,param_4);
|
||||||
|
*param_2 = uVar2;
|
||||||
|
goto LAB_141f63871;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 0x2f:
|
||||||
|
if ((*(char *)(param_1 + 0xe0c) == '\0') &&
|
||||||
|
(cVar1 = (**(code **)(*(longlong *)(param_1 + 0x3ed0) + 0x10))(param_1 + 0x3ed0),
|
||||||
|
cVar1 != '\0')) {
|
||||||
|
local_18[0] = 0;
|
||||||
|
uVar2 = (**(code **)(*(longlong *)(param_1 + 0x3ed0) + 8))(param_1 + 0x3ed0,local_18,param_4);
|
||||||
|
*param_2 = uVar2;
|
||||||
|
goto LAB_141f63871;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 0x30:
|
||||||
|
if ((*(char *)(param_1 + 0xe0c) == '\0') &&
|
||||||
|
(cVar1 = (**(code **)(*(longlong *)(param_1 + 0x4350) + 0x10))(param_1 + 0x4350),
|
||||||
|
cVar1 != '\0')) {
|
||||||
|
local_18[0] = 0;
|
||||||
|
uVar2 = (**(code **)(*(longlong *)(param_1 + 0x4350) + 8))(param_1 + 0x4350,local_18,param_4);
|
||||||
|
*param_2 = uVar2;
|
||||||
|
goto LAB_141f63871;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 0x40:
|
||||||
|
cVar1 = (**(code **)(*(longlong *)(param_1 + 0x47d8) + 0x10))(param_1 + 0x47d8);
|
||||||
|
if (cVar1 != '\0') {
|
||||||
|
local_18[0] = 0;
|
||||||
|
uVar2 = (**(code **)(*(longlong *)(param_1 + 0x47d8) + 8))(param_1 + 0x47d8,local_18,param_4);
|
||||||
|
*param_2 = uVar2;
|
||||||
|
goto LAB_141f63871;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 0x41:
|
||||||
|
cVar1 = (**(code **)(*(longlong *)(param_1 + 0x4838) + 0x10))(param_1 + 0x4838);
|
||||||
|
if (cVar1 != '\0') {
|
||||||
|
local_18[0] = 0;
|
||||||
|
uVar2 = (**(code **)(*(longlong *)(param_1 + 0x4838) + 8))(param_1 + 0x4838,local_18,param_4);
|
||||||
|
*param_2 = uVar2;
|
||||||
|
goto LAB_141f63871;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 0x42:
|
||||||
|
cVar1 = (**(code **)(*(longlong *)(param_1 + 0x48a8) + 0x10))(param_1 + 0x48a8);
|
||||||
|
if (cVar1 != '\0') {
|
||||||
|
local_18[0] = 0;
|
||||||
|
uVar2 = (**(code **)(*(longlong *)(param_1 + 0x48a8) + 8))(param_1 + 0x48a8,local_18,param_4);
|
||||||
|
*param_2 = uVar2;
|
||||||
|
goto LAB_141f63871;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 0x43:
|
||||||
|
cVar1 = (**(code **)(*(longlong *)(param_1 + 0x4900) + 0x10))(param_1 + 0x4900);
|
||||||
|
if (cVar1 != '\0') {
|
||||||
|
local_18[0] = 0;
|
||||||
|
uVar2 = (**(code **)(*(longlong *)(param_1 + 0x4900) + 8))(param_1 + 0x4900,local_18,param_4);
|
||||||
|
*param_2 = uVar2;
|
||||||
|
goto LAB_141f63871;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 0x44:
|
||||||
|
cVar1 = (**(code **)(*(longlong *)(param_1 + 0x4958) + 0x10))(param_1 + 0x4958);
|
||||||
|
if (cVar1 != '\0') {
|
||||||
|
local_18[0] = 0;
|
||||||
|
uVar2 = (**(code **)(*(longlong *)(param_1 + 0x4958) + 8))(param_1 + 0x4958,local_18,param_4);
|
||||||
|
*param_2 = uVar2;
|
||||||
|
goto LAB_141f63871;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 0x45:
|
||||||
|
cVar1 = (**(code **)(*(longlong *)(param_1 + 0x49b0) + 0x10))(param_1 + 0x49b0);
|
||||||
|
if (cVar1 != '\0') {
|
||||||
|
local_18[0] = 0;
|
||||||
|
uVar2 = (**(code **)(*(longlong *)(param_1 + 0x49b0) + 8))(param_1 + 0x49b0,local_18,param_4);
|
||||||
|
*param_2 = uVar2;
|
||||||
|
goto LAB_141f63871;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 0x46:
|
||||||
|
cVar1 = (**(code **)(*(longlong *)(param_1 + 0x4a08) + 0x10))(param_1 + 0x4a08);
|
||||||
|
if (cVar1 != '\0') {
|
||||||
|
local_18[0] = 0;
|
||||||
|
uVar2 = (**(code **)(*(longlong *)(param_1 + 0x4a08) + 8))(param_1 + 0x4a08,local_18,param_4);
|
||||||
|
*param_2 = uVar2;
|
||||||
|
goto LAB_141f63871;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 0x47:
|
||||||
|
cVar1 = (**(code **)(*(longlong *)(param_1 + 0x4a68) + 0x10))(param_1 + 0x4a68);
|
||||||
|
if (cVar1 != '\0') {
|
||||||
|
local_18[0] = 0;
|
||||||
|
uVar2 = (**(code **)(*(longlong *)(param_1 + 0x4a68) + 8))(param_1 + 0x4a68,local_18,param_4);
|
||||||
|
*param_2 = uVar2;
|
||||||
|
goto LAB_141f63871;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 0x48:
|
||||||
|
cVar1 = (**(code **)(*(longlong *)(param_1 + 0x4ac8) + 0x10))(param_1 + 0x4ac8);
|
||||||
|
if (cVar1 != '\0') {
|
||||||
|
local_18[0] = 0;
|
||||||
|
uVar2 = (**(code **)(*(longlong *)(param_1 + 0x4ac8) + 8))(param_1 + 0x4ac8,local_18,param_4);
|
||||||
|
*param_2 = uVar2;
|
||||||
|
goto LAB_141f63871;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 0x4f:
|
||||||
|
cVar1 = (**(code **)(*(longlong *)(param_1 + 0x4b20) + 0x10))(param_1 + 0x4b20);
|
||||||
|
if (cVar1 != '\0') {
|
||||||
|
local_18[0] = 0;
|
||||||
|
uVar2 = (**(code **)(*(longlong *)(param_1 + 0x4b20) + 8))(param_1 + 0x4b20,local_18,param_4);
|
||||||
|
*param_2 = uVar2;
|
||||||
|
goto LAB_141f63871;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
local_18[0]._0_4_ = 0;
|
||||||
|
*param_2 = 0;
|
||||||
|
LAB_141f63871:
|
||||||
|
*param_3 = (undefined4)local_18[0];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
// Decompile the export root FUN_14119d2d0 and selected callees, to find the
|
||||||
|
// per-track row-insert that sets index_shift (0x02) and the 0x14/0x18/0x1a blob.
|
||||||
|
// Also dump callees of FUN_14117be20 (rekordbox/export.pdb string xref).
|
||||||
|
// Output -> ~/ghidra_proj/decroot.txt
|
||||||
|
import java.io.PrintWriter;
|
||||||
|
import java.util.*;
|
||||||
|
import ghidra.app.script.GhidraScript;
|
||||||
|
import ghidra.app.decompiler.*;
|
||||||
|
import ghidra.program.model.listing.*;
|
||||||
|
|
||||||
|
public class DecRoot extends GhidraScript {
|
||||||
|
PrintWriter out; DecompInterface di; FunctionManager fm;
|
||||||
|
Set<String> done = new HashSet<>();
|
||||||
|
void dec(String a, String why) {
|
||||||
|
Function f = fm.getFunctionContaining(currentProgram.getAddressFactory().getAddress(a));
|
||||||
|
if (f == null) { out.println("// no fn at " + a + " (" + why + ")"); return; }
|
||||||
|
if (!done.add(f.getEntryPoint().toString())) return;
|
||||||
|
out.println("\n//===== " + f.getName() + " @ " + f.getEntryPoint() + " [" + why + "] =====");
|
||||||
|
DecompileResults r = di.decompileFunction(f, 120, monitor);
|
||||||
|
out.println(r != null && r.decompileCompleted() ? r.getDecompiledFunction().getC() : "// decompile failed: "+(r!=null?r.getErrorMessage():"null"));
|
||||||
|
}
|
||||||
|
@Override public void run() throws Exception {
|
||||||
|
out = new PrintWriter("/home/ntr/ghidra_proj/decroot.txt");
|
||||||
|
di = new DecompInterface(); di.openProgram(currentProgram);
|
||||||
|
fm = currentProgram.getFunctionManager();
|
||||||
|
dec("0x14119d2d0", "export root (/.PIONEER/rekordbox/export.pdb)");
|
||||||
|
dec("0x14117be20", "rekordbox/export.pdb xref");
|
||||||
|
out.flush(); out.close();
|
||||||
|
println("wrote decroot.txt");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
// Decode the track-row builder: the function that maps a djmdContent record into
|
||||||
|
// the on-disk DeviceSQL row (offset 0x02 index_shift, 0x14 u4 hash, 0x18/0x1a u2).
|
||||||
|
// Decompiles FUN_141c04be0 (AnalysisDataPath/SearchStr/MasterContentID mapper) and
|
||||||
|
// FUN_141ae4860 (djmdContent), plus scans for code referencing the constant
|
||||||
|
// 0x21b4a7ab / 0xa7ab and for `<<5` row-index assignment.
|
||||||
|
// Output -> ~/ghidra_proj/decrow.txt
|
||||||
|
import java.io.PrintWriter;
|
||||||
|
import java.util.*;
|
||||||
|
import ghidra.app.script.GhidraScript;
|
||||||
|
import ghidra.app.decompiler.*;
|
||||||
|
import ghidra.program.model.address.*;
|
||||||
|
import ghidra.program.model.listing.*;
|
||||||
|
import ghidra.program.model.scalar.Scalar;
|
||||||
|
import ghidra.program.model.lang.OperandType;
|
||||||
|
|
||||||
|
public class DecRow extends GhidraScript {
|
||||||
|
PrintWriter out;
|
||||||
|
DecompInterface di;
|
||||||
|
Set<String> done = new HashSet<>();
|
||||||
|
FunctionManager fm;
|
||||||
|
|
||||||
|
void dec(String a, String why) {
|
||||||
|
Function f = fm.getFunctionContaining(currentProgram.getAddressFactory().getAddress(a));
|
||||||
|
if (f == null) { out.println("// no function at " + a + " (" + why + ")"); return; }
|
||||||
|
if (!done.add(f.getEntryPoint().toString())) return;
|
||||||
|
out.println("\n//===== " + f.getName() + " @ " + f.getEntryPoint() + " [" + why + "] =====");
|
||||||
|
DecompileResults r = di.decompileFunction(f, 90, monitor);
|
||||||
|
out.println(r != null && r.decompileCompleted() ? r.getDecompiledFunction().getC() : "// decompile failed");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override public void run() throws Exception {
|
||||||
|
out = new PrintWriter("/home/ntr/ghidra_proj/decrow.txt");
|
||||||
|
di = new DecompInterface(); di.openProgram(currentProgram);
|
||||||
|
fm = currentProgram.getFunctionManager();
|
||||||
|
|
||||||
|
dec("0x141c04be0", "AnalysisDataPath/SearchStr/MasterContentID mapper");
|
||||||
|
dec("0x141c07230", "SearchStr 2");
|
||||||
|
dec("0x141c076d0", "SearchStr 3");
|
||||||
|
dec("0x141ae4860", "djmdContent xrefs");
|
||||||
|
|
||||||
|
// Scan all instructions for the per-export constants used in 0x18/0x1a
|
||||||
|
// (0xa7ab=42923, 0x21b4=8628, combined u4 0x21b4a7ab=565353387) and for
|
||||||
|
// an immediate 0x20 / shift-by-5 pattern that would build index_shift.
|
||||||
|
long[] consts = {0xa7abL, 0x21b4L, 0x21b4a7abL, 0xa7ab21b4L};
|
||||||
|
Set<String> hitFns = new LinkedHashSet<>();
|
||||||
|
Listing lst = currentProgram.getListing();
|
||||||
|
InstructionIterator it = lst.getInstructions(true);
|
||||||
|
int scanned = 0;
|
||||||
|
while (it.hasNext() && !monitor.isCancelled()) {
|
||||||
|
Instruction ins = it.next();
|
||||||
|
scanned++;
|
||||||
|
for (int op = 0; op < ins.getNumOperands(); op++) {
|
||||||
|
Object[] objs = ins.getOpObjects(op);
|
||||||
|
for (Object o : objs) {
|
||||||
|
if (o instanceof Scalar) {
|
||||||
|
long v = ((Scalar) o).getUnsignedValue();
|
||||||
|
for (long c : consts) {
|
||||||
|
if (v == c) {
|
||||||
|
Function f = fm.getFunctionContaining(ins.getAddress());
|
||||||
|
String tag = "const 0x" + Long.toHexString(c) + " @" + ins.getAddress()
|
||||||
|
+ " in " + (f != null ? f.getName() + "@" + f.getEntryPoint() : "?")
|
||||||
|
+ " : " + ins.toString();
|
||||||
|
out.println("// SCAN " + tag);
|
||||||
|
if (f != null) hitFns.add(f.getEntryPoint().toString() + "|const0x" + Long.toHexString(c));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out.println("// scanned " + scanned + " instructions");
|
||||||
|
for (String h : hitFns) {
|
||||||
|
String[] parts = h.split("\\|");
|
||||||
|
dec("0x" + parts[0], parts[1]);
|
||||||
|
}
|
||||||
|
out.flush(); out.close();
|
||||||
|
println("wrote decrow.txt");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
// Decompile the prime index_shift candidate FUN_14232a86c (shl5 + mask 0x7ff) and
|
||||||
|
// a few neighboring DeviceSQL page-manager functions. Output -> ~/ghidra_proj/decshift.txt
|
||||||
|
import java.io.PrintWriter;
|
||||||
|
import java.util.*;
|
||||||
|
import ghidra.app.script.GhidraScript;
|
||||||
|
import ghidra.app.decompiler.*;
|
||||||
|
import ghidra.program.model.listing.*;
|
||||||
|
|
||||||
|
public class DecShift extends GhidraScript {
|
||||||
|
PrintWriter out; DecompInterface di; FunctionManager fm; Set<String> done=new HashSet<>();
|
||||||
|
void dec(String a, String why) {
|
||||||
|
Function f = fm.getFunctionContaining(currentProgram.getAddressFactory().getAddress(a));
|
||||||
|
if (f == null) { out.println("// no fn at " + a + " (" + why + ")"); return; }
|
||||||
|
if (!done.add(f.getEntryPoint().toString())) return;
|
||||||
|
out.println("\n//===== " + f.getName() + " @ " + f.getEntryPoint() + " [" + why + "] =====");
|
||||||
|
DecompileResults r = di.decompileFunction(f, 120, monitor);
|
||||||
|
out.println(r != null && r.decompileCompleted() ? r.getDecompiledFunction().getC() : "// decompile failed");
|
||||||
|
}
|
||||||
|
@Override public void run() throws Exception {
|
||||||
|
out = new PrintWriter("/home/ntr/ghidra_proj/decshift.txt");
|
||||||
|
di = new DecompInterface(); di.openProgram(currentProgram);
|
||||||
|
fm = currentProgram.getFunctionManager();
|
||||||
|
dec("0x14232a86c", "shl5+mask7ff PRIME index_shift candidate");
|
||||||
|
// neighbors in DeviceSQL page-manager range
|
||||||
|
dec("0x1423e1550", "page-mgr neighbor");
|
||||||
|
dec("0x1423e12a0", "page-mgr neighbor");
|
||||||
|
out.flush(); out.close();
|
||||||
|
println("wrote decshift.txt");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
// @flesniak documented track-row 0x18=u2 "always 19048" (0x4A68) and 0x1a=u2
|
||||||
|
// "always 30967" (0x78F7). Our AceMo export has 0xa7ab / 0x21b4 instead, and a
|
||||||
|
// const-scan found no 0xa7ab/0x21b4 immediates. So locate every reference to the
|
||||||
|
// documented defaults 0x4A68 and 0x78F7 (and combined 0x78F74A68 / 0x4A6878F7),
|
||||||
|
// decompile the containing functions, and report. This should reveal how 0x18/0x1a
|
||||||
|
// are produced (literal default vs computed). Output -> ~/ghidra_proj/find4a68.txt
|
||||||
|
import java.io.PrintWriter;
|
||||||
|
import java.util.*;
|
||||||
|
import ghidra.app.script.GhidraScript;
|
||||||
|
import ghidra.app.decompiler.*;
|
||||||
|
import ghidra.program.model.listing.*;
|
||||||
|
import ghidra.program.model.scalar.Scalar;
|
||||||
|
|
||||||
|
public class Find4A68 extends GhidraScript {
|
||||||
|
@Override public void run() throws Exception {
|
||||||
|
PrintWriter out = new PrintWriter("/home/ntr/ghidra_proj/find4a68.txt");
|
||||||
|
FunctionManager fm = currentProgram.getFunctionManager();
|
||||||
|
DecompInterface di = new DecompInterface(); di.openProgram(currentProgram);
|
||||||
|
long[] consts = {0x4A68L, 0x78F7L, 0x78F74A68L, 0x4A6878F7L, 19048L, 30967L};
|
||||||
|
Set<String> fns = new LinkedHashSet<>();
|
||||||
|
InstructionIterator it = currentProgram.getListing().getInstructions(true);
|
||||||
|
while (it.hasNext() && !monitor.isCancelled()) {
|
||||||
|
Instruction ins = it.next();
|
||||||
|
for (int op=0; op<ins.getNumOperands(); op++)
|
||||||
|
for (Object o : ins.getOpObjects(op))
|
||||||
|
if (o instanceof Scalar) {
|
||||||
|
long v=((Scalar)o).getUnsignedValue();
|
||||||
|
for (long c: consts) if (v==c) {
|
||||||
|
Function f=fm.getFunctionContaining(ins.getAddress());
|
||||||
|
out.println("// HIT 0x"+Long.toHexString(c)+" @"+ins.getAddress()
|
||||||
|
+" in "+(f!=null?f.getName()+"@"+f.getEntryPoint():"?")+" : "+ins.toString());
|
||||||
|
if (f!=null) fns.add(f.getEntryPoint().toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// also search defined data for the byte patterns
|
||||||
|
out.println("// --- decompiling containing functions ---");
|
||||||
|
for (String a: fns) {
|
||||||
|
Function f=fm.getFunctionContaining(currentProgram.getAddressFactory().getAddress(a));
|
||||||
|
out.println("\n//===== "+f.getName()+" @ "+f.getEntryPoint()+" =====");
|
||||||
|
DecompileResults r=di.decompileFunction(f,90,monitor);
|
||||||
|
out.println(r!=null&&r.decompileCompleted()?r.getDecompiledFunction().getC():"// decompile failed");
|
||||||
|
}
|
||||||
|
out.flush(); out.close();
|
||||||
|
println("wrote find4a68.txt");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
// Anchor the track-row builder via the track-row bitmask 0xc0700 (constant in
|
||||||
|
// every AceMo track row at offset 0x04) and the subtype/0x24 nearby. Find any
|
||||||
|
// instruction with immediate 0xc0700 and decompile the function. Output ->
|
||||||
|
// ~/ghidra_proj/findbitmask.txt
|
||||||
|
import java.io.PrintWriter;
|
||||||
|
import java.util.*;
|
||||||
|
import ghidra.app.script.GhidraScript;
|
||||||
|
import ghidra.app.decompiler.*;
|
||||||
|
import ghidra.program.model.listing.*;
|
||||||
|
import ghidra.program.model.scalar.Scalar;
|
||||||
|
|
||||||
|
public class FindBitmask extends GhidraScript {
|
||||||
|
@Override public void run() throws Exception {
|
||||||
|
PrintWriter out = new PrintWriter("/home/ntr/ghidra_proj/findbitmask.txt");
|
||||||
|
FunctionManager fm = currentProgram.getFunctionManager();
|
||||||
|
DecompInterface di = new DecompInterface(); di.openProgram(currentProgram);
|
||||||
|
long[] consts = {0xc0700L, 0x700L, 0xc07L};
|
||||||
|
Set<String> fns = new LinkedHashSet<>();
|
||||||
|
InstructionIterator it = currentProgram.getListing().getInstructions(true);
|
||||||
|
while (it.hasNext() && !monitor.isCancelled()) {
|
||||||
|
Instruction ins = it.next();
|
||||||
|
for (int op=0; op<ins.getNumOperands(); op++)
|
||||||
|
for (Object o : ins.getOpObjects(op))
|
||||||
|
if (o instanceof Scalar) {
|
||||||
|
long v=((Scalar)o).getUnsignedValue();
|
||||||
|
if (v==0xc0700L) {
|
||||||
|
Function f=fm.getFunctionContaining(ins.getAddress());
|
||||||
|
out.println("// HIT 0xc0700 @"+ins.getAddress()+" in "
|
||||||
|
+(f!=null?f.getName()+"@"+f.getEntryPoint():"?")+" : "+ins.toString());
|
||||||
|
if (f!=null) fns.add(f.getEntryPoint().toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out.println("// --- decompiling ---");
|
||||||
|
for (String a: fns) {
|
||||||
|
Function f=fm.getFunctionContaining(currentProgram.getAddressFactory().getAddress(a));
|
||||||
|
out.println("\n//===== "+f.getName()+" @ "+f.getEntryPoint()+" =====");
|
||||||
|
DecompileResults r=di.decompileFunction(f,120,monitor);
|
||||||
|
out.println(r!=null&&r.decompileCompleted()?r.getDecompiledFunction().getC():"// fail");
|
||||||
|
}
|
||||||
|
out.flush(); out.close();
|
||||||
|
println("wrote findbitmask.txt");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
// Confirm 0x78f74a68 / 0x4a68 / 0x78f7 is rekordbox's default opaque_seed by
|
||||||
|
// searching DEFINED DATA (not just instruction immediates) for the byte pattern
|
||||||
|
// 68 4a f7 78 (LE u4) and the two u2s, and report containing context.
|
||||||
|
// Output -> ~/ghidra_proj/finddefault.txt
|
||||||
|
import java.io.PrintWriter;
|
||||||
|
import ghidra.app.script.GhidraScript;
|
||||||
|
import ghidra.program.model.address.*;
|
||||||
|
import ghidra.program.model.mem.*;
|
||||||
|
|
||||||
|
public class FindDefault extends GhidraScript {
|
||||||
|
@Override public void run() throws Exception {
|
||||||
|
PrintWriter out = new PrintWriter("/home/ntr/ghidra_proj/finddefault.txt");
|
||||||
|
Memory mem = currentProgram.getMemory();
|
||||||
|
byte[] pat = { (byte)0x68,(byte)0x4a,(byte)0xf7,(byte)0x78 }; // 0x78f74a68 LE
|
||||||
|
Address a = mem.getMinAddress();
|
||||||
|
int found=0;
|
||||||
|
while (a != null && found < 50) {
|
||||||
|
Address hit = mem.findBytes(a, pat, null, true, monitor);
|
||||||
|
if (hit == null) break;
|
||||||
|
out.println("// 0x78f74a68 bytes @ " + hit);
|
||||||
|
found++;
|
||||||
|
a = hit.add(1);
|
||||||
|
}
|
||||||
|
out.println("// total occurrences of 0x78f74a68 LE in memory image: " + found);
|
||||||
|
out.flush(); out.close();
|
||||||
|
println("wrote finddefault.txt found="+found);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
// Find the DeviceSQL row-index code that computes index_shift = slot << 5.
|
||||||
|
// Heuristic: functions that contain BOTH a left-shift-by-5 (SHL r,5 / LEA r,[r*0x20])
|
||||||
|
// AND reference the row-index masks (0x1fff / 0x7ff) or stride 0x24, AND a u2 store.
|
||||||
|
// Output -> ~/ghidra_proj/findshift.txt
|
||||||
|
import java.io.PrintWriter;
|
||||||
|
import java.util.*;
|
||||||
|
import ghidra.app.script.GhidraScript;
|
||||||
|
import ghidra.program.model.listing.*;
|
||||||
|
import ghidra.program.model.scalar.Scalar;
|
||||||
|
|
||||||
|
public class FindShift extends GhidraScript {
|
||||||
|
@Override public void run() throws Exception {
|
||||||
|
PrintWriter out = new PrintWriter("/home/ntr/ghidra_proj/findshift.txt");
|
||||||
|
FunctionManager fm = currentProgram.getFunctionManager();
|
||||||
|
Listing lst = currentProgram.getListing();
|
||||||
|
// accumulate per-function evidence
|
||||||
|
Map<String,Set<String>> ev = new HashMap<>();
|
||||||
|
InstructionIterator it = lst.getInstructions(true);
|
||||||
|
while (it.hasNext() && !monitor.isCancelled()) {
|
||||||
|
Instruction ins = it.next();
|
||||||
|
Function f = fm.getFunctionContaining(ins.getAddress());
|
||||||
|
if (f == null) continue;
|
||||||
|
String key = f.getEntryPoint().toString();
|
||||||
|
String mn = ins.getMnemonicString();
|
||||||
|
String txt = ins.toString();
|
||||||
|
boolean shl5 = (mn.equalsIgnoreCase("SHL") && txt.endsWith(",0x5"))
|
||||||
|
|| (mn.equalsIgnoreCase("LEA") && txt.contains("*0x20"));
|
||||||
|
if (shl5) ev.computeIfAbsent(key,k->new HashSet<>()).add("shl5@"+ins.getAddress());
|
||||||
|
for (int op=0; op<ins.getNumOperands(); op++)
|
||||||
|
for (Object o : ins.getOpObjects(op))
|
||||||
|
if (o instanceof Scalar) {
|
||||||
|
long v = ((Scalar)o).getUnsignedValue();
|
||||||
|
if (v==0x1fff) ev.computeIfAbsent(key,k->new HashSet<>()).add("mask1fff@"+ins.getAddress());
|
||||||
|
if (v==0x7ff) ev.computeIfAbsent(key,k->new HashSet<>()).add("mask7ff@"+ins.getAddress());
|
||||||
|
if (v==0x24) ev.computeIfAbsent(key,k->new HashSet<>()).add("stride24@"+ins.getAddress());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// report functions that have shl5 AND (mask or stride)
|
||||||
|
for (Map.Entry<String,Set<String>> e : ev.entrySet()) {
|
||||||
|
Set<String> tags = e.getValue();
|
||||||
|
boolean hasShl = tags.stream().anyMatch(t->t.startsWith("shl5"));
|
||||||
|
boolean hasCtx = tags.stream().anyMatch(t->t.startsWith("mask1fff")||t.startsWith("mask7ff")||t.startsWith("stride24"));
|
||||||
|
if (hasShl && hasCtx) {
|
||||||
|
Function f = fm.getFunctionContaining(currentProgram.getAddressFactory().getAddress(e.getKey()));
|
||||||
|
out.println("CANDIDATE " + (f!=null?f.getName():"?") + " @" + e.getKey() + " : " + tags);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out.flush(); out.close();
|
||||||
|
println("wrote findshift.txt");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -652,15 +652,31 @@ types:
|
|||||||
type: u4
|
type: u4
|
||||||
doc: |
|
doc: |
|
||||||
The length of the audio file, in bytes.
|
The length of the audio file, in bytes.
|
||||||
- type: u4
|
- id: opaque_track_id_lo
|
||||||
|
type: u4
|
||||||
doc: |
|
doc: |
|
||||||
Some ID? Purpose as yet unknown.
|
RE (maple): per-track opaque value. Together with the two u2 fields
|
||||||
- type: u2
|
below this forms an 8-byte little-endian blob
|
||||||
|
(opaque_track_id_lo | (opaque_seed << 32)). The high 32 bits
|
||||||
|
(opaque_seed) are constant across every track in one export; the low
|
||||||
|
32 bits vary per track and are NOT a hash of the file path, title,
|
||||||
|
filename, ANLZ .DAT/.EXT, or audio bytes (all tested). Believed to be
|
||||||
|
a stored rekordbox-internal id (rb_data UUID/hash). Not reproducible
|
||||||
|
from track metadata. See re/FORMAT_SPEC.md section 2.
|
||||||
|
- id: opaque_seed_lo
|
||||||
|
type: u2
|
||||||
doc: |
|
doc: |
|
||||||
From @flesniak: "always 19048?"
|
RE (maple): low u2 of the 32-bit per-export "opaque_seed" (see
|
||||||
- type: u2
|
opaque_track_id_lo). @flesniak called it "always 19048?"; that value
|
||||||
|
(0x4A68) does NOT hold here (AceMo export = 0xa7ab) and there is no
|
||||||
|
0x4A68/0x78F7 immediate in the binary, so it is a per-export runtime
|
||||||
|
value, not a constant.
|
||||||
|
- id: opaque_seed_hi
|
||||||
|
type: u2
|
||||||
doc: |
|
doc: |
|
||||||
From @flesniak: "always 30967?"
|
RE (maple): high u2 of the per-export "opaque_seed"
|
||||||
|
(AceMo export = 0x21b4; @flesniak's "always 30967?" = 0x78F7 does not
|
||||||
|
hold). Constant across all tracks in one export.
|
||||||
- id: artwork_id
|
- id: artwork_id
|
||||||
type: u4
|
type: u4
|
||||||
doc: |
|
doc: |
|
||||||
|
|||||||
@@ -1182,14 +1182,22 @@ class RekordboxPdb(KaitaiStruct):
|
|||||||
|
|
||||||
def _read(self):
|
def _read(self):
|
||||||
self.subtype = self._io.read_u2le()
|
self.subtype = self._io.read_u2le()
|
||||||
|
# index_shift (0x02): RE rule = slot_index * 0x20 (slot_index << 5),
|
||||||
|
# where slot_index is the 0-based row-index slot on the page (counts
|
||||||
|
# deleted slots), resets per page. See re/FORMAT_SPEC.md section 1.
|
||||||
self.index_shift = self._io.read_u2le()
|
self.index_shift = self._io.read_u2le()
|
||||||
self.bitmask = self._io.read_u4le()
|
self.bitmask = self._io.read_u4le()
|
||||||
self.sample_rate = self._io.read_u4le()
|
self.sample_rate = self._io.read_u4le()
|
||||||
self.composer_id = self._io.read_u4le()
|
self.composer_id = self._io.read_u4le()
|
||||||
self.file_size = self._io.read_u4le()
|
self.file_size = self._io.read_u4le()
|
||||||
self._unnamed6 = self._io.read_u4le()
|
# 0x14..0x1b: 8-byte LE blob = opaque_track_id_lo | (opaque_seed<<32).
|
||||||
self._unnamed7 = self._io.read_u2le()
|
# opaque_seed (the two u2s below) is per-export constant; the lo u4 is
|
||||||
self._unnamed8 = self._io.read_u2le()
|
# per-track and is NOT a hash of path/title/filename/ANLZ/audio.
|
||||||
|
# Believed a stored rekordbox-internal id; not reproducible from
|
||||||
|
# track metadata. See re/FORMAT_SPEC.md section 2.
|
||||||
|
self.opaque_track_id_lo = self._io.read_u4le()
|
||||||
|
self.opaque_seed_lo = self._io.read_u2le()
|
||||||
|
self.opaque_seed_hi = self._io.read_u2le()
|
||||||
self.artwork_id = self._io.read_u4le()
|
self.artwork_id = self._io.read_u4le()
|
||||||
self.key_id = self._io.read_u4le()
|
self.key_id = self._io.read_u4le()
|
||||||
self.original_artist_id = self._io.read_u4le()
|
self.original_artist_id = self._io.read_u4le()
|
||||||
|
|||||||
@@ -87,7 +87,12 @@ def build(input_dir: Path, usb_root: Path, with_playlists: bool = True) -> Datab
|
|||||||
# path keyed off the track id even if we don't write the .DAT.
|
# path keyed off the track id even if we don't write the .DAT.
|
||||||
import zlib
|
import zlib
|
||||||
h = zlib.crc32(file_path.encode()) & 0xFFFFFFFF
|
h = zlib.crc32(file_path.encode()) & 0xFFFFFFFF
|
||||||
bucket = f"P{(h >> 20) & 0xFFF:03X}"
|
# rekordbox's USBANLZ P-bucket is 0..127 (P00..P7F) and the 8-hex id is an
|
||||||
|
# opaque master.db value -- not reproducible without its DB (FORMAT_SPEC
|
||||||
|
# §7), so maple derives both from a stable crc32(path). Mask the bucket to
|
||||||
|
# 0x7F so it has rekordbox's P00..P7F shape; the path used here is the same
|
||||||
|
# one written to the pdb row, so it's self-consistent by construction.
|
||||||
|
bucket = f"P{(h >> 20) & 0x7F:02X}"
|
||||||
analyze_id = f"{h & 0xFFFFFFFF:08X}"
|
analyze_id = f"{h & 0xFFFFFFFF:08X}"
|
||||||
analyze_path = f"/PIONEER/USBANLZ/{bucket}/{analyze_id}/ANLZ0000.DAT"
|
analyze_path = f"/PIONEER/USBANLZ/{bucket}/{analyze_id}/ANLZ0000.DAT"
|
||||||
write_anlz(file_path, usb_root / analyze_path.lstrip("/"))
|
write_anlz(file_path, usb_root / analyze_path.lstrip("/"))
|
||||||
|
|||||||
@@ -127,7 +127,7 @@ UNKNOWN_18_ROWS: list[tuple[int, int, int, int]] = [
|
|||||||
(1, 6, 1, 0), (21, 7, 1, 0), (14, 8, 1, 0), (8, 9, 1, 0),
|
(1, 6, 1, 0), (21, 7, 1, 0), (14, 8, 1, 0), (8, 9, 1, 0),
|
||||||
(9, 10, 1, 0), (10, 11, 1, 0), (15, 13, 1, 0), (13, 15, 1, 0),
|
(9, 10, 1, 0), (10, 11, 1, 0), (15, 13, 1, 0), (13, 15, 1, 0),
|
||||||
(23, 16, 1, 0), (22, 17, 1, 0), (25, 0, 256, 0), (26, 1, 512, 0),
|
(23, 16, 1, 0), (22, 17, 1, 0), (25, 0, 256, 0), (26, 1, 512, 0),
|
||||||
(2, 2, 768, 0), (3, 3, 1024, 0), (5, 4, 1282, 0), (6, 5, 1536, 0),
|
(2, 2, 768, 0), (3, 3, 1024, 0), (5, 4, 1280, 0), (6, 5, 1536, 0),
|
||||||
(11, 12, 1792, 0),
|
(11, 12, 1792, 0),
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -172,3 +172,40 @@ class Database:
|
|||||||
genres: list[Genre] = field(default_factory=list)
|
genres: list[Genre] = field(default_factory=list)
|
||||||
colors: list[Color] = field(default_factory=list)
|
colors: list[Color] = field(default_factory=list)
|
||||||
keys: list[Key] = field(default_factory=list)
|
keys: list[Key] = field(default_factory=list)
|
||||||
|
|
||||||
|
|
||||||
|
def validate_referential_integrity(db: "Database") -> list[str]:
|
||||||
|
"""Return a list of dangling-foreign-key errors. Every FK must be 0 ("none")
|
||||||
|
or resolve to a real row in its target table. A track that references a
|
||||||
|
genre/artist/album/artwork id with no matching row is freeze-class: the
|
||||||
|
player follows the FK into an empty/short table and hangs (this is exactly
|
||||||
|
how build_acemo.py's omitted-genres bug froze the deck). Empty string = OK.
|
||||||
|
"""
|
||||||
|
errors: list[str] = []
|
||||||
|
artist_ids = {a.id for a in db.artists}
|
||||||
|
album_ids = {a.id for a in db.albums}
|
||||||
|
artwork_ids = {a.id for a in db.artwork}
|
||||||
|
genre_ids = {g.id for g in db.genres}
|
||||||
|
key_ids = {k.id for k in db.keys}
|
||||||
|
track_ids = {t.id for t in db.tracks}
|
||||||
|
playlist_ids = {n.id for n in db.playlist_nodes}
|
||||||
|
|
||||||
|
def check(kind: str, owner_id: int, field: str, value: int, valid: set[int]) -> None:
|
||||||
|
# 0 means "unset / no reference" everywhere in the pdb schema.
|
||||||
|
if value and value not in valid:
|
||||||
|
errors.append(f"{kind} id={owner_id}: {field}={value} has no matching row")
|
||||||
|
|
||||||
|
for t in db.tracks:
|
||||||
|
check("track", t.id, "artist_id", t.artist_id, artist_ids)
|
||||||
|
check("track", t.id, "album_id", t.album_id, album_ids)
|
||||||
|
check("track", t.id, "artwork_id", t.artwork_id, artwork_ids)
|
||||||
|
check("track", t.id, "genre_id", t.genre_id, genre_ids)
|
||||||
|
for a in db.albums:
|
||||||
|
check("album", a.id, "artist_id", a.artist_id, artist_ids)
|
||||||
|
for e in db.playlist_entries:
|
||||||
|
check("playlist_entry", e.playlist_id, "track_id", e.track_id, track_ids)
|
||||||
|
check("playlist_entry", e.playlist_id, "playlist_id", e.playlist_id, playlist_ids)
|
||||||
|
# key_ids is currently unreferenced by Track in maple's model; included for
|
||||||
|
# completeness so adding a track.key_id later is covered.
|
||||||
|
_ = key_ids
|
||||||
|
return errors
|
||||||
@@ -15,11 +15,34 @@ def _num_row_groups(n: int) -> int:
|
|||||||
return (n + ROWS_PER_GROUP - 1) // ROWS_PER_GROUP
|
return (n + ROWS_PER_GROUP - 1) // ROWS_PER_GROUP
|
||||||
|
|
||||||
|
|
||||||
def build_strange_page(page_index: int, page_type: int, next_page: int) -> bytes:
|
# Strange-page heap is a DeviceSQL free-space map, byte-exact in a fresh export:
|
||||||
"""Empty placeholder page that the reference exports use as the first
|
# u4(page_index) + u4(data_page_ptr) + 12-byte const tail
|
||||||
page of every table chain. Reproduces the observed header flags and
|
# + (f8 ff ff 1f)*1004 + 20 trailing zeros.
|
||||||
marker bytes; the heap is left zero-filled (rekordbox fills it with a
|
# data_page_ptr is the table's first data-page index, or the sentinel
|
||||||
`f8 ff ff 1f` pattern, but players don't care because num_rows=0).
|
# 0x03ffffff when the table is strange-only (no data page). The 1004 repeat
|
||||||
|
# count is the same value stored in the header's unknown6 field (0x03ec).
|
||||||
|
# Verified byte-for-byte against data/acemo_fresh strange pages.
|
||||||
|
_STRANGE_HEAP_NO_DATA = 0x03FFFFFF
|
||||||
|
_STRANGE_HEAP_TAIL_CONST = bytes.fromhex("ffffff03000000000000ff1f")
|
||||||
|
_STRANGE_HEAP_PATTERN = bytes.fromhex("f8ffff1f")
|
||||||
|
_STRANGE_HEAP_REPEAT = 1004
|
||||||
|
_STRANGE_HEAP_TAIL = 20
|
||||||
|
|
||||||
|
# Strange pages on these table types carry unknown7=1; all others 0. In a fresh
|
||||||
|
# export this is set for tracks (first table) and history (last table). Other
|
||||||
|
# fields (free/used=0, txc/txi=0x1fff) match across all strange pages.
|
||||||
|
STRANGE_UNKNOWN7 = frozenset({0, 19})
|
||||||
|
|
||||||
|
|
||||||
|
def build_strange_page(page_index: int, page_type: int, next_page: int,
|
||||||
|
data_page: int | None = None) -> bytes:
|
||||||
|
"""Empty placeholder page that the reference exports use as the first page of
|
||||||
|
every table chain. Reproduces a FRESH rekordbox export byte-for-byte: header
|
||||||
|
flags/markers plus the DeviceSQL free-space heap map. `next_page` should be
|
||||||
|
the reserved data-page index (page_index+1) even for strange-only tables --
|
||||||
|
rekordbox always points the strange page at its (possibly-blank) data slot.
|
||||||
|
`data_page` is the table's first data-page index, or None when the table is
|
||||||
|
strange-only (the free-map then stores the 0x03ffffff sentinel there).
|
||||||
"""
|
"""
|
||||||
header = b""
|
header = b""
|
||||||
header += pack("<I", 0) # magic
|
header += pack("<I", 0) # magic
|
||||||
@@ -30,23 +53,36 @@ def build_strange_page(page_index: int, page_type: int, next_page: int) -> bytes
|
|||||||
header += pack("<I", 0) # reserved
|
header += pack("<I", 0) # reserved
|
||||||
# packed: num_row_offsets=0, num_rows=0, page_flags=0x64
|
# packed: num_row_offsets=0, num_rows=0, page_flags=0x64
|
||||||
header += bytes([0, 0, 0, 0x64])
|
header += bytes([0, 0, 0, 0x64])
|
||||||
header += pack("<H", 0x1fff) # free_size (strange marker)
|
header += pack("<H", 0) # free_size (fresh export = 0)
|
||||||
header += pack("<H", 0x1fff) # used_size (strange marker)
|
header += pack("<H", 0) # used_size (fresh export = 0)
|
||||||
header += pack("<H", 0x1fff) # transaction_row_count = failed
|
header += pack("<H", 0x1fff) # transaction_row_count = failed
|
||||||
header += pack("<H", 0x1fff) # transaction_row_index = failed
|
header += pack("<H", 0x1fff) # transaction_row_index = failed
|
||||||
header += pack("<H", 0x03ec) # "1004 for strange blocks"
|
header += pack("<H", 0x03ec) # 1004 (free-map repeat count)
|
||||||
header += pack("<H", 0) # unknown
|
header += pack("<H", 1 if page_type in STRANGE_UNKNOWN7 else 0) # unknown7
|
||||||
assert len(header) == HEADER_SIZE
|
assert len(header) == HEADER_SIZE
|
||||||
return header + b"\x00" * (PAGE_SIZE - HEADER_SIZE)
|
|
||||||
|
data_ptr = _STRANGE_HEAP_NO_DATA if data_page is None else data_page
|
||||||
|
heap = pack("<II", page_index, data_ptr) + _STRANGE_HEAP_TAIL_CONST
|
||||||
|
heap += _STRANGE_HEAP_PATTERN * _STRANGE_HEAP_REPEAT
|
||||||
|
heap += b"\x00" * _STRANGE_HEAP_TAIL
|
||||||
|
assert len(heap) == PAGE_SIZE - HEADER_SIZE
|
||||||
|
return header + heap
|
||||||
|
|
||||||
|
|
||||||
class PageBuilder:
|
class PageBuilder:
|
||||||
def __init__(self, page_index: int, page_type: int, max_rows: int = 0, data_flag: int = 0x24):
|
def __init__(self, page_index: int, page_type: int, max_rows: int = 0,
|
||||||
|
data_flag: int = 0x24, has_index_shift: bool = False):
|
||||||
self.page_index = page_index
|
self.page_index = page_index
|
||||||
self.page_type = page_type
|
self.page_type = page_type
|
||||||
self.next_page = page_index # caller patches before finalize
|
self.next_page = page_index # caller patches before finalize
|
||||||
self.max_rows = max_rows
|
self.max_rows = max_rows
|
||||||
self.data_flag = data_flag # page_flags for a populated data page
|
self.data_flag = data_flag # page_flags for a populated data page
|
||||||
|
# Row types whose layout begins `subtype:u2, index_shift:u2` (tracks,
|
||||||
|
# artists, albums) carry a slot-based index_shift at row offset 0x02.
|
||||||
|
# See FORMAT_SPEC.md s1: index_shift = slot_index << 5, where slot_index
|
||||||
|
# is the row's 0-based position in the page row index. maple never
|
||||||
|
# deletes rows, so slot_index == the row's ordinal on the page.
|
||||||
|
self.has_index_shift = has_index_shift
|
||||||
self.row_bytes: list[bytes] = []
|
self.row_bytes: list[bytes] = []
|
||||||
self.row_offsets: list[int] = [] # relative to heap_pos
|
self.row_offsets: list[int] = [] # relative to heap_pos
|
||||||
self.heap_cursor = 0 # position relative to heap_pos
|
self.heap_cursor = 0 # position relative to heap_pos
|
||||||
@@ -102,7 +138,11 @@ class PageBuilder:
|
|||||||
assert len(header) == HEADER_SIZE
|
assert len(header) == HEADER_SIZE
|
||||||
|
|
||||||
heap = bytearray(heap_padding)
|
heap = bytearray(heap_padding)
|
||||||
for ofs, row in zip(self.row_offsets, self.row_bytes):
|
for slot, (ofs, row) in enumerate(zip(self.row_offsets, self.row_bytes)):
|
||||||
|
if self.has_index_shift:
|
||||||
|
# Patch index_shift (u2 at row offset 0x02) from the placeholder
|
||||||
|
# to slot_index<<5 (FORMAT_SPEC.md s1).
|
||||||
|
row = row[:2] + pack("<H", (slot << 5) & 0xFFFF) + row[4:]
|
||||||
heap[ofs:ofs + len(row)] = row
|
heap[ofs:ofs + len(row)] = row
|
||||||
|
|
||||||
# Footers go after the heap in the file. Group 0 (rows 0-15) sits closest
|
# Footers go after the heap in the file. Group 0 (rows 0-15) sits closest
|
||||||
@@ -119,7 +159,12 @@ class PageBuilder:
|
|||||||
for ofs in reversed(padded):
|
for ofs in reversed(padded):
|
||||||
rg += pack("<H", ofs)
|
rg += pack("<H", ofs)
|
||||||
rg += pack("<H", presence)
|
rg += pack("<H", presence)
|
||||||
rg += pack("<H", 16) # unknown (rekordcrate default)
|
# Trailing u2: on a fresh, fully-packed page (no deleted slots) this
|
||||||
|
# equals the presence mask. Verified across every clean row group in
|
||||||
|
# data/acemo_fresh; pages where ref's value differs from presence are
|
||||||
|
# exactly the dead-slot/edit-history pages (see KNOWN_DIVERGENCE.md).
|
||||||
|
# Was a constant 16 (rekordcrate default), which never matched.
|
||||||
|
rg += pack("<H", presence)
|
||||||
assert len(rg) == ROW_GROUP_SIZE
|
assert len(rg) == ROW_GROUP_SIZE
|
||||||
footer += rg
|
footer += rg
|
||||||
assert len(footer) == footer_size
|
assert len(footer) == footer_size
|
||||||
|
|||||||
@@ -9,6 +9,24 @@ TRACK_FIXED_PREFIX = 0x5E
|
|||||||
TRACK_OFS_STRINGS_START = TRACK_FIXED_PREFIX
|
TRACK_OFS_STRINGS_START = TRACK_FIXED_PREFIX
|
||||||
TRACK_HEADER_LEN = TRACK_OFS_STRINGS_START + 21 * 2
|
TRACK_HEADER_LEN = TRACK_OFS_STRINGS_START + 21 * 2
|
||||||
|
|
||||||
|
# Per-rekordbox-install seed for the track opaque-id blob (row 0x18, u4). It is
|
||||||
|
# a master.db value, constant across a single install, not derivable from track
|
||||||
|
# metadata (FORMAT_SPEC.md s2). EVERY real rekordbox export we have carries a
|
||||||
|
# NON-ZERO seed (this install = 0x21b4a7ab; data/MAPLE = 0xd6d3a567); the
|
||||||
|
# 0x78f74a68 "unanalyzed default" was a false lead -- it came from maple's own
|
||||||
|
# earlier output (data/usb), not a real export. Default to this install's real
|
||||||
|
# value; override per-install if deck testing shows it matters. Players treat
|
||||||
|
# the whole blob as opaque.
|
||||||
|
TRACK_OPAQUE_SEED = 0x21B4A7AB
|
||||||
|
|
||||||
|
# Range maple maps its stand-in opaque_track_id_lo (row 0x14) into, so it has the
|
||||||
|
# shape of a real rekordbox Content ID. The real AceMo lo values all fall in this
|
||||||
|
# band (observed 0x02c0a654 .. 0x0f8fd598), i.e. ~134M-261M. We can't reproduce
|
||||||
|
# the true (encrypted-DB) Content ID, so we map a stable per-track hash into the
|
||||||
|
# band -- non-zero, in-range, deterministic. Documented divergence (FORMAT_SPEC s2).
|
||||||
|
OPAQUE_LO_MIN = 0x08000000
|
||||||
|
OPAQUE_LO_MAX = 0x0F900000
|
||||||
|
|
||||||
|
|
||||||
def _pad2(b: bytes) -> bytes:
|
def _pad2(b: bytes) -> bytes:
|
||||||
return b + b"\x00" if len(b) & 1 else b
|
return b + b"\x00" if len(b) & 1 else b
|
||||||
@@ -56,20 +74,24 @@ def serialize_track(t: Track) -> bytes:
|
|||||||
prefix += pack("<I", t.sample_rate) # sample_rate
|
prefix += pack("<I", t.sample_rate) # sample_rate
|
||||||
prefix += pack("<I", 0) # composer_id
|
prefix += pack("<I", 0) # composer_id
|
||||||
prefix += pack("<I", t.file_size) # file_size
|
prefix += pack("<I", t.file_size) # file_size
|
||||||
# Mystery u4: schema calls it "Some ID? Purpose as yet unknown."
|
# offsets 0x14/0x18/0x1a are ONE 8-byte LE opaque id:
|
||||||
# In the verified reference every track has a unique non-zero value
|
# blob_u64 = opaque_track_id_lo | (opaque_seed << 32)
|
||||||
# here. Player firmware may reject rows where this is 0. Use a
|
# Both halves are rekordbox-internal stored values (master.db / djmdContent
|
||||||
# stable derived hash from (id, file_path) so it's reproducible.
|
# + a per-install seed), NOT a function of the track metadata maple has --
|
||||||
unk_id = zlib.crc32(f"{t.id}|{t.file_path}".encode()) & 0xFFFFFFFF
|
# proven across the real exports + an exhaustive negative hash search
|
||||||
if unk_id == 0:
|
# (FORMAT_SPEC.md s2). They cannot be reproduced byte-for-byte without
|
||||||
unk_id = 1
|
# rekordbox's DB; that is a documented divergence (KNOWN_DIVERGENCE.md).
|
||||||
prefix += pack("<I", unk_id) # "Some ID"
|
# Every REAL export has BOTH halves non-zero, so emit a non-zero stand-in of
|
||||||
# The kaitai schema author guessed these were "always 19048 / 30967",
|
# the right SHAPE: the per-install seed, and a per-track lo mapped into the
|
||||||
# but the verified reference export uses 42343 / 54995 (constant across
|
# real Content-ID band (OPAQUE_LO_MIN..MAX) -- which is where every observed
|
||||||
# all 176 tracks). Likely a per-version marker; player firmware may
|
# AceMo lo lands. Stable hash -> band, so it's deterministic and in-range.
|
||||||
# validate it. Match the reference.
|
# (Was the 0xa567/0xd6d3 marker, which matched no real export; lo=0/
|
||||||
prefix += pack("<H", 42343) # 0xa567
|
# seed=0x78f74a68 was also wrong -- that came from maple's own output
|
||||||
prefix += pack("<H", 54995) # 0xd6d3
|
# mistaken for an "unanalyzed library", not a real rekordbox export.)
|
||||||
|
h = zlib.crc32(f"{t.id}|{t.file_path}".encode()) & 0xFFFFFFFF
|
||||||
|
opaque_lo = OPAQUE_LO_MIN + (h % (OPAQUE_LO_MAX - OPAQUE_LO_MIN))
|
||||||
|
prefix += pack("<I", opaque_lo) # opaque_track_id_lo (stand-in, per track)
|
||||||
|
prefix += pack("<I", TRACK_OPAQUE_SEED) # opaque_seed (u4 @0x18 = lo|hi<<16)
|
||||||
prefix += pack("<I", t.artwork_id) # artwork_id
|
prefix += pack("<I", t.artwork_id) # artwork_id
|
||||||
prefix += pack("<I", 0) # key_id
|
prefix += pack("<I", 0) # key_id
|
||||||
prefix += pack("<I", 0) # original_artist_id
|
prefix += pack("<I", 0) # original_artist_id
|
||||||
@@ -106,7 +128,9 @@ def serialize_artist(a: Artist) -> bytes:
|
|||||||
if near_ofs < far_threshold:
|
if near_ofs < far_threshold:
|
||||||
subtype = 0x60
|
subtype = 0x60
|
||||||
row = b""
|
row = b""
|
||||||
row += pack("<HH", subtype, a.id & 0xFFFF) # subtype, index_shift
|
# index_shift is slot-based (slot_index<<5); the pager patches it at
|
||||||
|
# page-build time -- see FORMAT_SPEC.md s1. Write 0 as a placeholder.
|
||||||
|
row += pack("<HH", subtype, 0) # subtype, index_shift
|
||||||
row += pack("<I", a.id) # id
|
row += pack("<I", a.id) # id
|
||||||
row += pack("<B", 0x03) # always 0x03
|
row += pack("<B", 0x03) # always 0x03
|
||||||
row += pack("<B", near_ofs) # ofs_name_near
|
row += pack("<B", near_ofs) # ofs_name_near
|
||||||
@@ -118,7 +142,8 @@ def serialize_artist(a: Artist) -> bytes:
|
|||||||
# Layout: subtype u2, index_shift u2, id u4, 0x03 u1, ofs_name_near u1, ofs_name_far u2 -> name follows at offset 0x0e
|
# Layout: subtype u2, index_shift u2, id u4, 0x03 u1, ofs_name_near u1, ofs_name_far u2 -> name follows at offset 0x0e
|
||||||
far = 0x0E
|
far = 0x0E
|
||||||
row = b""
|
row = b""
|
||||||
row += pack("<HH", subtype, a.id & 0xFFFF)
|
# index_shift patched by the pager (slot_index<<5); 0 placeholder.
|
||||||
|
row += pack("<HH", subtype, 0)
|
||||||
row += pack("<I", a.id)
|
row += pack("<I", a.id)
|
||||||
row += pack("<B", 0x03)
|
row += pack("<B", 0x03)
|
||||||
row += pack("<B", 0)
|
row += pack("<B", 0)
|
||||||
@@ -132,7 +157,8 @@ def serialize_album(a: Album) -> bytes:
|
|||||||
near_ofs = 0x16
|
near_ofs = 0x16
|
||||||
subtype = 0x80
|
subtype = 0x80
|
||||||
row = b""
|
row = b""
|
||||||
row += pack("<HH", subtype, a.id & 0xFFFF) # subtype, index_shift
|
# index_shift patched by the pager (slot_index<<5); 0 placeholder.
|
||||||
|
row += pack("<HH", subtype, 0) # subtype, index_shift
|
||||||
row += pack("<I", 0) # unknown
|
row += pack("<I", 0) # unknown
|
||||||
row += pack("<I", a.artist_id) # artist_id
|
row += pack("<I", a.artist_id) # artist_id
|
||||||
row += pack("<I", a.id) # id
|
row += pack("<I", a.id) # id
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
"""Top-level rekordbox .pdb file writer."""
|
"""Top-level rekordbox .pdb file writer."""
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from struct import pack
|
from struct import pack, unpack_from
|
||||||
|
|
||||||
from maple.model import (
|
from maple.model import (
|
||||||
DEFAULT_COLUMNS,
|
DEFAULT_COLUMNS,
|
||||||
@@ -8,6 +8,7 @@ from maple.model import (
|
|||||||
UNKNOWN_17_ROWS,
|
UNKNOWN_17_ROWS,
|
||||||
UNKNOWN_18_ROWS,
|
UNKNOWN_18_ROWS,
|
||||||
Database,
|
Database,
|
||||||
|
validate_referential_integrity,
|
||||||
)
|
)
|
||||||
from maple.pdb.pager import PAGE_SIZE, PageBuilder, build_strange_page
|
from maple.pdb.pager import PAGE_SIZE, PageBuilder, build_strange_page
|
||||||
from maple.pdb.rows import (
|
from maple.pdb.rows import (
|
||||||
@@ -52,15 +53,58 @@ TABLE_ENTRY_SIZE = 16 # page_type,empty_candidate,first_page,last_page = 4*u4
|
|||||||
|
|
||||||
|
|
||||||
# Page types whose populated data pages rekordbox tags 0x34 (vs the 0x24 the
|
# Page types whose populated data pages rekordbox tags 0x34 (vs the 0x24 the
|
||||||
# rest get). Matched byte-for-byte against a real rekordbox export.
|
# rest get). In a FRESH from-scratch export (data/acemo_fresh) only tracks and
|
||||||
PAGE_FLAG_34 = frozenset({0, 2, 3, 13, 17, 19}) # tracks, artists, albums, artwork, browse_filter_index, history
|
# history carry 0x34; every other table's data page is 0x24. (The wider set
|
||||||
|
# {0,2,3,13,17,19} was matched against the EDITED reference, whose extra 0x34
|
||||||
|
# pages were edit-history artifacts.) These are the same two tables that get
|
||||||
|
# strange-page unknown7=1 -- see pager.STRANGE_UNKNOWN7.
|
||||||
|
PAGE_FLAG_34 = frozenset({0, 19}) # tracks, history
|
||||||
|
|
||||||
# "Core" tables always get a real data page (even when empty). The rest stay
|
# Tables that get a real data page even when they have zero rows. A FRESH
|
||||||
# strange-only when empty. Matched against a real rekordbox export.
|
# from-scratch rekordbox export (data/acemo_fresh) gives a data page to a table
|
||||||
CORE_TABLES = frozenset({0, 1, 2, 3, 5, 6, 13, 16, 17, 18, 19})
|
# IFF it has rows -- every empty table is strange-page-only. The earlier
|
||||||
|
# non-empty CORE_TABLES set was matched against the EDITED reference
|
||||||
|
# (data/acemo), whose genres table (type 1) carried a leftover empty data page
|
||||||
|
# from a deleted genre row -- an edit-history artifact, not a format rule (see
|
||||||
|
# KNOWN_DIVERGENCE.md). Against the fresh reference the correct rule is
|
||||||
|
# "data page iff rows>0", i.e. no table is forced. Kept as an (empty) hook in
|
||||||
|
# case a future fresh export proves some table is always materialised.
|
||||||
|
CORE_TABLES: frozenset[int] = frozenset()
|
||||||
|
|
||||||
|
# Row types whose layout begins `subtype:u2, index_shift:u2`, so they carry a
|
||||||
|
# slot-based index_shift the pager must patch (FORMAT_SPEC.md s1): tracks,
|
||||||
|
# artists, albums.
|
||||||
|
INDEX_SHIFT_TABLES = frozenset({0, 2, 3})
|
||||||
|
|
||||||
|
|
||||||
|
SEQUENCE_OFFSET = 20 # u4 offset of `sequence` in the file header (page 0)
|
||||||
|
|
||||||
|
|
||||||
|
def _next_sequence(path: Path) -> int:
|
||||||
|
"""rekordbox's file-header `sequence` is an internal transaction counter from
|
||||||
|
its master.db -- we can't reproduce its exact value. The faithful behaviour is
|
||||||
|
to monotonically increment: read the sequence already in the destination
|
||||||
|
export (if any) and add 1, defaulting to 1 for a brand-new file. This keeps
|
||||||
|
each export's sequence strictly greater than the last, as rekordbox does.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
head = path.read_bytes()[:FILE_HEADER_SIZE]
|
||||||
|
except (FileNotFoundError, OSError):
|
||||||
|
return 1
|
||||||
|
if len(head) < FILE_HEADER_SIZE:
|
||||||
|
return 1
|
||||||
|
existing = unpack_from("<I", head, SEQUENCE_OFFSET)[0]
|
||||||
|
return existing + 1 if existing else 1
|
||||||
|
|
||||||
|
|
||||||
def write_pdb(db: Database, path: str | Path) -> None:
|
def write_pdb(db: Database, path: str | Path) -> None:
|
||||||
|
# Refuse to emit a pdb with dangling foreign keys -- a track pointing at a
|
||||||
|
# genre/artist/album/artwork row that doesn't exist makes the player follow
|
||||||
|
# the FK into an empty table and freeze (freeze-class, like the deck hang).
|
||||||
|
fk_errors = validate_referential_integrity(db)
|
||||||
|
if fk_errors:
|
||||||
|
raise ValueError("dangling foreign keys (would freeze the player):\n " + "\n ".join(fk_errors))
|
||||||
|
|
||||||
# Layout (matching rekordbox): page 0 is the file header. Pages 1..2*N are
|
# Layout (matching rekordbox): page 0 is the file header. Pages 1..2*N are
|
||||||
# reserved 2-per-table -- strange page at the odd index 1+2k, first data page
|
# reserved 2-per-table -- strange page at the odd index 1+2k, first data page
|
||||||
# at the even index 2+2k. Overflow data pages come from the pool at 2*N+1.
|
# at the even index 2+2k. Overflow data pages come from the pool at 2*N+1.
|
||||||
@@ -112,36 +156,82 @@ def write_pdb(db: Database, path: str | Path) -> None:
|
|||||||
overflow_next = 1 + 2 * num_tables # first overflow page (41)
|
overflow_next = 1 + 2 * num_tables # first overflow page (41)
|
||||||
pages_by_index: dict[int, bytes] = {} # page_index -> 4096-byte page
|
pages_by_index: dict[int, bytes] = {} # page_index -> 4096-byte page
|
||||||
table_dir: list[tuple[int, int, int]] = [] # (page_type, first_page, last_page)
|
table_dir: list[tuple[int, int, int]] = [] # (page_type, first_page, last_page)
|
||||||
|
last_builders: list[PageBuilder] = [] # last data page of each chain, patched below
|
||||||
|
|
||||||
for k, ptype in enumerate(ALL_PAGE_TYPES):
|
for k, ptype in enumerate(ALL_PAGE_TYPES):
|
||||||
strange_idx = 1 + 2 * k
|
strange_idx = 1 + 2 * k
|
||||||
data_idx = 2 + 2 * k
|
data_idx = 2 + 2 * k
|
||||||
rows = rows_by_type.get(ptype, [])
|
rows = rows_by_type.get(ptype, [])
|
||||||
data_flag = 0x34 if ptype in PAGE_FLAG_34 else 0x24
|
data_flag = 0x34 if ptype in PAGE_FLAG_34 else 0x24
|
||||||
|
has_index_shift = ptype in INDEX_SHIFT_TABLES
|
||||||
|
|
||||||
# Optional table with no rows: strange page only (its even page stays blank).
|
# Optional table with no rows: strange page only (its even page stays
|
||||||
|
# blank). rekordbox still points the strange page at the reserved data
|
||||||
|
# index (strange_idx+1 == data_idx), and the table dir's last_page is the
|
||||||
|
# strange page itself.
|
||||||
if not rows and ptype not in CORE_TABLES:
|
if not rows and ptype not in CORE_TABLES:
|
||||||
pages_by_index[strange_idx] = build_strange_page(strange_idx, ptype, next_page=strange_idx)
|
pages_by_index[strange_idx] = build_strange_page(
|
||||||
|
strange_idx, ptype, next_page=data_idx, data_page=None)
|
||||||
table_dir.append((ptype, strange_idx, strange_idx))
|
table_dir.append((ptype, strange_idx, strange_idx))
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# First data page at the reserved even index; overflow pages from the pool.
|
# First data page at the reserved even index; overflow pages from the pool.
|
||||||
builders: list[PageBuilder] = [PageBuilder(data_idx, ptype, data_flag=data_flag)]
|
builders: list[PageBuilder] = [
|
||||||
|
PageBuilder(data_idx, ptype, data_flag=data_flag, has_index_shift=has_index_shift)
|
||||||
|
]
|
||||||
for row in rows:
|
for row in rows:
|
||||||
if not builders[-1].try_add(row):
|
if not builders[-1].try_add(row):
|
||||||
nb = PageBuilder(overflow_next, ptype, data_flag=data_flag)
|
nb = PageBuilder(overflow_next, ptype, data_flag=data_flag,
|
||||||
|
has_index_shift=has_index_shift)
|
||||||
overflow_next += 1
|
overflow_next += 1
|
||||||
if not nb.try_add(row):
|
if not nb.try_add(row):
|
||||||
raise ValueError(f"row of size {len(row)} too large for an empty page")
|
raise ValueError(f"row of size {len(row)} too large for an empty page")
|
||||||
builders.append(nb)
|
builders.append(nb)
|
||||||
|
|
||||||
pages_by_index[strange_idx] = build_strange_page(strange_idx, ptype, next_page=data_idx)
|
pages_by_index[strange_idx] = build_strange_page(
|
||||||
|
strange_idx, ptype, next_page=data_idx, data_page=data_idx)
|
||||||
for i, b in enumerate(builders):
|
for i, b in enumerate(builders):
|
||||||
b.next_page = b.page_index if i == len(builders) - 1 else builders[i + 1].page_index
|
# Intermediate pages chain forward to the next data page and are
|
||||||
|
# finalised now. The LAST page's next_page is patched after the loop
|
||||||
|
# to a free-pool sentinel (FREEZE FIX) -- never at itself -- so it is
|
||||||
|
# finalised there, not here.
|
||||||
|
if i != len(builders) - 1:
|
||||||
|
b.next_page = builders[i + 1].page_index
|
||||||
pages_by_index[b.page_index] = b.finalize()
|
pages_by_index[b.page_index] = b.finalize()
|
||||||
|
last_builders.append(builders[-1])
|
||||||
table_dir.append((ptype, strange_idx, builders[-1].page_index))
|
table_dir.append((ptype, strange_idx, builders[-1].page_index))
|
||||||
|
|
||||||
next_unused_page = max(overflow_next, 1 + 2 * num_tables)
|
# FREEZE FIX (#13): no data page may self-loop its next_page. In every
|
||||||
|
# genuine rekordbox export (data/MAPLE, lib_multi) the last page of each
|
||||||
|
# chain points FORWARD to a free/unallocated page; the firmware Page Manager
|
||||||
|
# walks next_page and treats an index at/after next_unused (incl. past EOF)
|
||||||
|
# as "end of chain", but an index == the current page is an infinite loop ->
|
||||||
|
# the deck froze. maple used to set last.next_page = its own index. Point
|
||||||
|
# every last page at a single dedicated free-pool sentinel just past the
|
||||||
|
# overflow region, materialise that page (zero-filled, as real exports do),
|
||||||
|
# and set next_unused beyond it.
|
||||||
|
free_sentinel = overflow_next
|
||||||
|
for b in last_builders:
|
||||||
|
b.next_page = free_sentinel
|
||||||
|
for b in last_builders:
|
||||||
|
pages_by_index[b.page_index] = b.finalize()
|
||||||
|
|
||||||
|
# Materialise a small free pool of zero-filled pages and set next_unused PAST
|
||||||
|
# it, mirroring a real export (e.g. data/MAPLE: 71 pages, next_unused=74 ->
|
||||||
|
# ~3 free pages past the last live page). A genuine export always keeps
|
||||||
|
# allocated-but-empty pages after the data and points next_unused beyond the
|
||||||
|
# file; maple previously had next_unused == EOF with no free pool, which can
|
||||||
|
# starve the firmware's free-list/allocator walk of a valid sentinel page.
|
||||||
|
FREE_POOL_PAGES = 4
|
||||||
|
for j in range(FREE_POOL_PAGES):
|
||||||
|
pages_by_index[free_sentinel + j] = b"\x00" * PAGE_SIZE
|
||||||
|
# File ends after the materialised free pool; next_unused points a couple
|
||||||
|
# pages PAST EOF, exactly as real exports do (data/MAPLE: file=71 pages,
|
||||||
|
# next_unused=74). The firmware tolerates a past-EOF next_unused.
|
||||||
|
file_page_count = free_sentinel + FREE_POOL_PAGES # number of pages incl. page 0
|
||||||
|
next_unused_page = file_page_count + 2
|
||||||
|
|
||||||
|
sequence = _next_sequence(Path(path))
|
||||||
|
|
||||||
file_header = b""
|
file_header = b""
|
||||||
file_header += pack("<I", 0) # magic
|
file_header += pack("<I", 0) # magic
|
||||||
@@ -149,7 +239,7 @@ def write_pdb(db: Database, path: str | Path) -> None:
|
|||||||
file_header += pack("<I", num_tables)
|
file_header += pack("<I", num_tables)
|
||||||
file_header += pack("<I", next_unused_page)
|
file_header += pack("<I", next_unused_page)
|
||||||
file_header += pack("<I", 0) # unknown
|
file_header += pack("<I", 0) # unknown
|
||||||
file_header += pack("<I", 1) # sequence
|
file_header += pack("<I", sequence) # incrementing transaction counter
|
||||||
file_header += pack("<I", 0) # gap
|
file_header += pack("<I", 0) # gap
|
||||||
assert len(file_header) == FILE_HEADER_SIZE
|
assert len(file_header) == FILE_HEADER_SIZE
|
||||||
|
|
||||||
@@ -172,7 +262,9 @@ def write_pdb(db: Database, path: str | Path) -> None:
|
|||||||
out_path.parent.mkdir(parents=True, exist_ok=True)
|
out_path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
with out_path.open("wb") as f:
|
with out_path.open("wb") as f:
|
||||||
f.write(page0)
|
f.write(page0)
|
||||||
for idx in range(1, next_unused_page): # reserved-but-unused pages written blank
|
# Write pages 1..file_page_count-1 (reserved-but-unused written blank).
|
||||||
|
# next_unused_page intentionally points a couple pages PAST this EOF.
|
||||||
|
for idx in range(1, file_page_count):
|
||||||
page = pages_by_index.get(idx, blank)
|
page = pages_by_index.get(idx, blank)
|
||||||
assert len(page) == PAGE_SIZE
|
assert len(page) == PAGE_SIZE
|
||||||
f.write(page)
|
f.write(page)
|
||||||
Generated
+269
-42
@@ -1,31 +1,89 @@
|
|||||||
version = 1
|
version = 1
|
||||||
|
revision = 3
|
||||||
requires-python = ">=3.14"
|
requires-python = ">=3.14"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bidict"
|
||||||
|
version = "0.23.1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/9a/6e/026678aa5a830e07cd9498a05d3e7e650a4f56a42f267a53d22bcda1bdc9/bidict-0.23.1.tar.gz", hash = "sha256:03069d763bc387bbd20e7d49914e75fc4132a41937fa3405417e1a5a2d006d71", size = 29093, upload-time = "2024-02-18T19:09:05.748Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/99/37/e8730c3587a65eb5645d4aba2d27aae48e8003614d6aaf15dda67f702f1f/bidict-0.23.1-py3-none-any.whl", hash = "sha256:5dae8d4d79b552a71cbabc7deb25dfe8ce710b17ff41711e13010ead2abfc3e5", size = 32764, upload-time = "2024-02-18T19:09:04.156Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "colorama"
|
name = "colorama"
|
||||||
version = "0.4.6"
|
version = "0.4.6"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 }
|
sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 },
|
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "construct"
|
||||||
|
version = "2.10.70"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/02/77/8c84b98eca70d245a2a956452f21d57930d22ab88cbeed9290ca630cf03f/construct-2.10.70.tar.gz", hash = "sha256:4d2472f9684731e58cc9c56c463be63baa1447d674e0d66aeb5627b22f512c29", size = 86337, upload-time = "2023-11-29T08:44:49.545Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b2/fb/08b3f4bf05da99aba8ffea52a558758def16e8516bc75ca94ff73587e7d3/construct-2.10.70-py3-none-any.whl", hash = "sha256:c80be81ef595a1a821ec69dc16099550ed22197615f4320b57cc9ce2a672cb30", size = 63020, upload-time = "2023-11-29T08:44:46.876Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "greenlet"
|
||||||
|
version = "3.5.1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/6d/6e/802acd792aebb2256fbbee8cacf2727faaeb6f240ac11008f09eae4414bc/greenlet-3.5.1.tar.gz", hash = "sha256:5a56aeb7d5d9cc4b3a735efb5095bd4b4f6f0e4f93e5ca876d0e2315137b7829", size = 197356, upload-time = "2026-05-20T15:05:03.917Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8a/cb/c62454606daf5640369c94d8a9dd540599b1bfc090e2d2180cb77f4038d2/greenlet-3.5.1-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:d8ab31c9de8651a2facdd5c5bb0011f2380dd1a7af78ce2adf4b56095294fc07", size = 285579, upload-time = "2026-05-20T13:08:56.396Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ec/71/c4270398c2eba968a6071af1dfbdcaeee6ec1c24bc8b435b8cc452700da6/greenlet-3.5.1-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5e300185139abc337ade480c327183adf42a875ac7181bfe66d7d4efea31fbea", size = 651106, upload-time = "2026-05-20T14:00:09.448Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/1a/ab/71e34b78a44ec271fb5f550c17bc46d301ddc5953890d935f270b0dcdb5a/greenlet-3.5.1-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7ffdb990dcaa0234cf9845aead5df2e3c3a8b6507d409274dd87e0d5ab05ffc2", size = 663478, upload-time = "2026-05-20T14:05:45.88Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/77/96/4efd6fa5c62c85426a0c19077a586258ebc3a2a146ff2493e4312a697a22/greenlet-3.5.1-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2f82b3597e9d83b63408affed0b48fd0f54935edac4302237b9a837be0dae33c", size = 660800, upload-time = "2026-05-20T13:14:29.129Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7a/e0/6c71401a25cac7000261304e866a2f2cc04dc74810d40e2f118aa4799495/greenlet-3.5.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c0141e37414c10164e702b8fb1473304221ad98f71600850c6ef7ff4880feba0", size = 1617518, upload-time = "2026-05-20T14:02:28.662Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/41/26/c5c06643e8c0af9e7bf18e16cb51d0ab7625155f0392e1c9015d66d556cd/greenlet-3.5.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:50ae25a67bea74ea41fb14b960bc532df73eb713417b2d61892dced82fe8d3bc", size = 1681593, upload-time = "2026-05-20T13:14:39.417Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8a/bd/e11a108317485075e68af9d23039619b86b28130c3b50d227d42edece64b/greenlet-3.5.1-cp314-cp314-win_amd64.whl", hash = "sha256:8a17c42330e261299766b75ac1ea32caa437a9453c8f65d16a13140db378ecd3", size = 239800, upload-time = "2026-05-20T13:09:30.128Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/47/f8/8e8e8417b7bf28639a5a56356ef934d0375e1d0c70a57e04d7701e870ffe/greenlet-3.5.1-cp314-cp314-win_arm64.whl", hash = "sha256:7b5f5fae05b8ac6d176a61b60c394a8cbdc2b5b91b81793066e68745cf165e54", size = 236862, upload-time = "2026-05-20T13:09:10.498Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/90/12/41bf27fde4d3605d3773ae57751eda182b8be2f5398011c041173b1d9534/greenlet-3.5.1-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:ea8da1e900d758d078810d4255d8c6aa572181896a31ec79d779eb79c3adc9ad", size = 293637, upload-time = "2026-05-20T13:12:35.529Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/44/44/ba14b23e9757707050c2f397d305bbcae62e5d7cad122f8b6baec5ae4a1f/greenlet-3.5.1-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a19570c52a21420dcbc94e661994bc325c0b5b11304540fed514586da5dc8f2e", size = 650840, upload-time = "2026-05-20T14:00:11.079Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a8/37/5ddc2b686a6844f91abecef43411842426da2e1573f60b49ecf2547f4ae1/greenlet-3.5.1-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3d955c89b75eeca4723d7cc14135f393cd47c32e2a6cb4a8e4c6e760a26b0986", size = 656416, upload-time = "2026-05-20T14:05:47.118Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e1/f0/d17510297c35a2992712f0bf84de3779749999f7d3d63aa1f09db7c62dbe/greenlet-3.5.1-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:de2daaaebd1a5aa88c49045b6baf9310b3263796bd88db713edf37cf53e7bb4e", size = 654397, upload-time = "2026-05-20T13:14:30.696Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/37/eb/147387705bb89092645b012586e7273cb5ed3c90ef7eaf3a69173eaf0209/greenlet-3.5.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:3bfbd69cc349e43bf3a8ae1c85548ff0718efc887615c2db16c3833d7b0b072d", size = 1614469, upload-time = "2026-05-20T14:02:30.192Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a6/4e/37ee0da7732b7aa9896f17e15579a9df34b9fcb9dd494f0adfa749af6623/greenlet-3.5.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:4378720dd888136c27215a0214d32a4d37c3852765d45bc37aad0623423cfd78", size = 1675115, upload-time = "2026-05-20T13:14:40.972Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/57/f3/97dfcf4a6eb5077f8a672234216fb5923eb89f2cab7081cb10b2cf75b605/greenlet-3.5.1-cp314-cp314t-win_amd64.whl", hash = "sha256:45718441607f9325d948db98cbc691276059316d0358c188c246da4e1d4d23d2", size = 245246, upload-time = "2026-05-20T13:12:22.646Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/5d/73/d7f72e34b582f694f4a9b248162db7b09cc458a259ba8f0c0bfa1a34ea7d/greenlet-3.5.1-cp315-cp315-macosx_11_0_universal2.whl", hash = "sha256:2baee5ca02031757ffe8cc3d69f0cc0aec7065ce362622da74f32d3bcab1c541", size = 285575, upload-time = "2026-05-20T13:12:07.043Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/df/59/fa9c6e87dc8ad27a95dabe2f29f372b733d05a8a67470f6c901ed9975655/greenlet-3.5.1-cp315-cp315-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9b1ec3274918a81d3ea778b9e75b56b72b33f300edb6cf7f3a7fe1dae56683de", size = 656428, upload-time = "2026-05-20T14:00:12.556Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f6/f9/e753408871eaa61dfe35e619cfc67512b036fde99893685d50eea9e07146/greenlet-3.5.1-cp315-cp315-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:111e2390ffffc47d5840b01711dd7fac07d4c09283d0283e7f3264b14e284c64", size = 667064, upload-time = "2026-05-20T14:05:48.662Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/96/27/5565b5b40389f1c7753003a07e21892fda8660926787036d5bc0308b8113/greenlet-3.5.1-cp315-cp315-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e630136e905fe5ff43e86945ae41220b6d1470956a39220e708110ac48d01ea5", size = 665697, upload-time = "2026-05-20T13:14:32.943Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/cf/82/e7de4178c0c2d1c9a5a3be3cc0b33e46a85b3ee4a77c071bf7ad8600e079/greenlet-3.5.1-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:975eac34b44a7077ca4d421348455b94f0f518246a7f14bc6d2fdcfe5b584368", size = 1621256, upload-time = "2026-05-20T14:02:31.91Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/00/10/f2dddcf7dacac17dfc68691809589adad06135eb28930429cf58a6467a2f/greenlet-3.5.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:9ab3c3a0b2ae6198e67c898dad5215a49f9ae0d0081b3c3ec59f333e39eeca26", size = 1685956, upload-time = "2026-05-20T13:14:42.55Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/22/17/4a232b32133230ada52f70e9d7f5b65b0caef8772f01849bd8d149e7e4ca/greenlet-3.5.1-cp315-cp315-win_amd64.whl", hash = "sha256:cbfc69be86e10dcfef5b1e6269d1d6926552aa89ee39e1de3353360c1b6989ab", size = 239802, upload-time = "2026-05-20T13:13:15.481Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c2/ae/4e623a7e6d4d2a5f4cb8e4c82de4169fc637942caae68d6e676b8a128ac5/greenlet-3.5.1-cp315-cp315-win_arm64.whl", hash = "sha256:92fd6d44ac5e5a887c8a5dc4a8ba0ba908527c31c12f78c6bc7dcfe8aab279f6", size = 236853, upload-time = "2026-05-20T13:15:37.301Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7a/57/816d9cff29119da3505b3d6a5e14a8af89006ac36f47f891ff293ee05af1/greenlet-3.5.1-cp315-cp315t-macosx_11_0_universal2.whl", hash = "sha256:a6fdf2433a5441ef9a95464f7c3e674775da1c8c1177fff311cee1acad4626ed", size = 293877, upload-time = "2026-05-20T13:10:19.078Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/23/a1/59b0a7c7d140ff1a75626680b9a9899b79a9176cab298b394968fb023295/greenlet-3.5.1-cp315-cp315t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7546556f0d649f99f6a361098a55f761181bb2ea12ff150bb16d26092ad88244", size = 655333, upload-time = "2026-05-20T14:00:14.758Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/72/1b/5efe127597625042218939d01855109f352779050768b670b52edcc16a6c/greenlet-3.5.1-cp315-cp315t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d5ee3ea898009fa898f85f9982255d35278c477bebe185beca249cab42d4526c", size = 659443, upload-time = "2026-05-20T14:05:50.159Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6c/6d/c404246ea4d22d097a7426d0efb5b781bd7eb67715f09e79001bd552ab18/greenlet-3.5.1-cp315-cp315t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a5c81f74d204d3edd136ebfd50dce53acbb776995d721a0fe801626cfc93b8cd", size = 658356, upload-time = "2026-05-20T13:14:35.091Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/51/02/f8ee37fb6d2219329f350af241c27fcf12df57e723d11f6fc6d3bacdadaa/greenlet-3.5.1-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:2c18ef16bf6d4dd410e4dd52996888ea1497be26892fe5bbc73580aba4287b8e", size = 1619216, upload-time = "2026-05-20T14:02:33.403Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/93/c5/3dc9475ace2c7a3680da12372cddd7f1ac874eb410a1ac48d3e9dab83782/greenlet-3.5.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:17d86354f0ae6b61bf9be5148d0dd34e06c3cb7c602c671f79f29ac3b150e659", size = 1678427, upload-time = "2026-05-20T13:14:43.71Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/df/4e/750c15c317a41ffb36f0bf40b933e3d744a7dede61889f74443ea69690cf/greenlet-3.5.1-cp315-cp315t-win_amd64.whl", hash = "sha256:e7516cf6ae6b8a582c2770a0caed47b8a48373ed732c33d69a72913ae6ac923e", size = 245225, upload-time = "2026-05-20T13:13:59.366Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/4f/fd/d3baea2eeb7b617efd47e87ca06e2ec2c6118d303aa9e918e0ce16eadc10/greenlet-3.5.1-cp315-cp315t-win_arm64.whl", hash = "sha256:5028648bf2253ec4745add746129d3904121fa7fe871a76bed23c5720573ce0a", size = 239590, upload-time = "2026-05-20T13:13:37.382Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "iniconfig"
|
name = "iniconfig"
|
||||||
version = "2.3.0"
|
version = "2.3.0"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503 }
|
sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484 },
|
{ url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "kaitaistruct"
|
name = "kaitaistruct"
|
||||||
version = "0.11"
|
version = "0.11"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/27/b8/ca7319556912f68832daa4b81425314857ec08dfccd8dbc8c0f65c992108/kaitaistruct-0.11.tar.gz", hash = "sha256:053ee764288e78b8e53acf748e9733268acbd579b8d82a427b1805453625d74b", size = 11519 }
|
sdist = { url = "https://files.pythonhosted.org/packages/27/b8/ca7319556912f68832daa4b81425314857ec08dfccd8dbc8c0f65c992108/kaitaistruct-0.11.tar.gz", hash = "sha256:053ee764288e78b8e53acf748e9733268acbd579b8d82a427b1805453625d74b", size = 11519, upload-time = "2025-09-08T15:46:25.037Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/4a/4a/cf14bf3b1f5ffb13c69cf5f0ea78031247790558ee88984a8bdd22fae60d/kaitaistruct-0.11-py2.py3-none-any.whl", hash = "sha256:5c6ce79177b4e193a577ecd359e26516d1d6d000a0bffd6e1010f2a46a62a561", size = 11372 },
|
{ url = "https://files.pythonhosted.org/packages/4a/4a/cf14bf3b1f5ffb13c69cf5f0ea78031247790558ee88984a8bdd22fae60d/kaitaistruct-0.11-py2.py3-none-any.whl", hash = "sha256:5c6ce79177b4e193a577ecd359e26516d1d6d000a0bffd6e1010f2a46a62a561", size = 11372, upload-time = "2025-09-08T15:46:23.635Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -36,6 +94,7 @@ dependencies = [
|
|||||||
{ name = "kaitaistruct" },
|
{ name = "kaitaistruct" },
|
||||||
{ name = "mutagen" },
|
{ name = "mutagen" },
|
||||||
{ name = "pillow" },
|
{ name = "pillow" },
|
||||||
|
{ name = "pyrekordbox" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.dev-dependencies]
|
[package.dev-dependencies]
|
||||||
@@ -48,6 +107,7 @@ requires-dist = [
|
|||||||
{ name = "kaitaistruct", specifier = ">=0.11" },
|
{ name = "kaitaistruct", specifier = ">=0.11" },
|
||||||
{ name = "mutagen", specifier = ">=1.47.0" },
|
{ name = "mutagen", specifier = ">=1.47.0" },
|
||||||
{ name = "pillow", specifier = ">=12.2.0" },
|
{ name = "pillow", specifier = ">=12.2.0" },
|
||||||
|
{ name = "pyrekordbox", specifier = ">=0.4.4" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.metadata.requires-dev]
|
[package.metadata.requires-dev]
|
||||||
@@ -57,69 +117,139 @@ dev = [{ name = "pytest", specifier = ">=9.0.3" }]
|
|||||||
name = "mutagen"
|
name = "mutagen"
|
||||||
version = "1.47.0"
|
version = "1.47.0"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/81/e6/64bc71b74eef4b68e61eb921dcf72dabd9e4ec4af1e11891bbd312ccbb77/mutagen-1.47.0.tar.gz", hash = "sha256:719fadef0a978c31b4cf3c956261b3c58b6948b32023078a2117b1de09f0fc99", size = 1274186 }
|
sdist = { url = "https://files.pythonhosted.org/packages/81/e6/64bc71b74eef4b68e61eb921dcf72dabd9e4ec4af1e11891bbd312ccbb77/mutagen-1.47.0.tar.gz", hash = "sha256:719fadef0a978c31b4cf3c956261b3c58b6948b32023078a2117b1de09f0fc99", size = 1274186, upload-time = "2023-09-03T16:33:33.411Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/b0/7a/620f945b96be1f6ee357d211d5bf74ab1b7fe72a9f1525aafbfe3aee6875/mutagen-1.47.0-py3-none-any.whl", hash = "sha256:edd96f50c5907a9539d8e5bba7245f62c9f520aef333d13392a79a4f70aca719", size = 194391 },
|
{ url = "https://files.pythonhosted.org/packages/b0/7a/620f945b96be1f6ee357d211d5bf74ab1b7fe72a9f1525aafbfe3aee6875/mutagen-1.47.0-py3-none-any.whl", hash = "sha256:edd96f50c5907a9539d8e5bba7245f62c9f520aef333d13392a79a4f70aca719", size = 194391, upload-time = "2023-09-03T16:33:29.955Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "numpy"
|
||||||
|
version = "2.4.6"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/d0/ad/fed0499ce6a338d2a03ebae59cd15093910c8875328855781952abf6c2fe/numpy-2.4.6.tar.gz", hash = "sha256:f3a3570c4a2a16746ac2c31a7c7c7b0c186b95ce902e33db6f28094ed7387dda", size = 20735807, upload-time = "2026-05-18T23:37:14.07Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f8/91/3ab2044d05fd16d343c5ac2e69b127f1b2854040dd20b193257c78028bd3/numpy-2.4.6-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:06ca2f61ec4385a07a6977c55ba998a4466c123642b4a32694d3128fce18c079", size = 16683458, upload-time = "2026-05-18T23:35:38.353Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8e/62/764ce66fa4147ae6d73071a3abf804ffe606f174618697c571acdf26a7c9/numpy-2.4.6-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:38efbc8de75c7a0fc1ac190162d892787f3f47b57cc291231aafee36b80982b7", size = 14704559, upload-time = "2026-05-18T23:35:42.14Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/60/61/23f27c172f022e04025b7dc2367f4d63c1a398120607ec896228649a6f48/numpy-2.4.6-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:d581b735e177fdcdce6fed8e7e8880a3fb6ee4e3653a3ac6af01c6f4c03effc5", size = 5209716, upload-time = "2026-05-18T23:35:45.377Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/03/71/21cf70dc6ea3e3acb95fc53a265b2fc248b981f0194ceb5b475271b8809d/numpy-2.4.6-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:0a041d3d761dc3c35cc56ce0351506a02bcbc25f7b169f652435141a17db9096", size = 6543947, upload-time = "2026-05-18T23:35:47.926Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d5/91/64288395ee1799bd2e0b04a305dce9666da90c961e1f3fe982a05ee1c036/numpy-2.4.6-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:40fdc1ae7125e518ea98e53e69a4ebc27e1fd50510c47b7ea130cf21e5e1d42b", size = 15685197, upload-time = "2026-05-18T23:35:50.863Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f3/eb/ebffaa97dc55502df69584a8f0dcf07f69a3e0b3e2323670a2722db9aa39/numpy-2.4.6-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a2c306dea656c12c68f51f4cea133cbe78ca7435eb28c735eac1d3ebe73be6e8", size = 16638245, upload-time = "2026-05-18T23:35:54.752Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b8/0b/54f9da33128d7e350fab89c7455902eeae70349ee52bddb448dc4a576f45/numpy-2.4.6-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:33111801a01c12a8a1e3721f0a9232f8cfc8ae2c6b7098167e6f623c6073f402", size = 17036587, upload-time = "2026-05-18T23:35:58.355Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b6/f0/fdebc1052db1cc37c64beb22072d67cd6d1c71adca1299f53dec2b5e20d3/numpy-2.4.6-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ae506e6902902557576a26ff33eda8695e7ecb3cb36c3b573a0765dee114ebdb", size = 18363226, upload-time = "2026-05-18T23:36:02.845Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/aa/b4/298628d98c72b57e57f7165ae6a481a1deaf6f3c28262a6e4c739c275930/numpy-2.4.6-cp314-cp314-win32.whl", hash = "sha256:aaf159caa35993cb1f56fb9b8e4610d35758e7ca005412eb1daa856a78c9c4b1", size = 6010196, upload-time = "2026-05-18T23:36:05.92Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/df/ac/46de6dda46478f7942f839e094970be2d4a861e005c4b3bf07c92e291a09/numpy-2.4.6-cp314-cp314-win_amd64.whl", hash = "sha256:b507f5c4c1d508876d1819b6bf9a49d365b96320b5d4993426b33a23ca4b8261", size = 12450334, upload-time = "2026-05-18T23:36:09.107Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/78/92/b8b798ac784102c0da830d2257d59358e3d3d90d1e2b3f2575dad976c5cf/numpy-2.4.6-cp314-cp314-win_arm64.whl", hash = "sha256:6f41ae150c4e32db4f3310cdaf64b1593a03dbabe29eec77fc9b50fe64061df6", size = 10495678, upload-time = "2026-05-18T23:36:12.766Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/30/34/ec28d1aa8115971537c01469ab2011ee96827930f0a124de1000cc2a7ed7/numpy-2.4.6-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ece3d2cfe132e7d51f44a832b303895e6f2d499c5e74dfbdb06ee246147a304a", size = 14823672, upload-time = "2026-05-18T23:36:16.473Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/16/bd/f6d1fede4e54e8042a7ff97bb495510f3c220f94bcd9e8b228e87c92cc0d/numpy-2.4.6-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:e3e5193ef5a3dc73bceee50f7fdc2c90dbb76c42df8d8fae3d1067a583df579e", size = 5328731, upload-time = "2026-05-18T23:36:19.767Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f4/f0/e105b9e2fd728a9910103884decd6951d9dd73896b914a98d9a231de02ee/numpy-2.4.6-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:17f9ade344e7d9b464a084d69bcf18fc691cb1db67c62ed80820bf4926d78f0e", size = 6649805, upload-time = "2026-05-18T23:36:22.266Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/82/dd/1206a7ca6ab15e3f02069707ca96222e202af681bb73756da7527f3cb837/numpy-2.4.6-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9cd5ffd25db4e7ba6a375693b3fc0fc1791ec636c17db3720da19bde7180ec43", size = 15730496, upload-time = "2026-05-18T23:36:25.713Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/51/e7/38d3ea825dcab85a591734decb2f6c67caa7c8367d374df1a1c3842f9b07/numpy-2.4.6-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7d92c3819208a60205a12a245c91ad70cb0a85336659b19b834205573ac8456e", size = 16679616, upload-time = "2026-05-18T23:36:29.652Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/93/b7/caabfdf53edf663e0b4eb74d7d405d83baef09eb5e83bcd32d601d72b93e/numpy-2.4.6-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e85b752a1e912b70eaad4fafbd4d1238007ab221de2009b9a2f5ae7461239895", size = 17085145, upload-time = "2026-05-18T23:36:33.449Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f9/45/68d7c33a6bcf3e5aa3bdbd57a367e6f615286dfd6482f97e8ffeb734306e/numpy-2.4.6-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:29cb7f67d10b479ff07c17d33e39f78c07f71c40ef30d63c153d340e96cd3fb4", size = 18403813, upload-time = "2026-05-18T23:36:37.369Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9c/50/0753655aa844c99cd9e018aacf76f130f1bd81d881bb74bc0aef5d73a8ba/numpy-2.4.6-cp314-cp314t-win32.whl", hash = "sha256:260a5d70215b61ab4fadf5c7baacd64821842975eea312125ed3c39a6391b063", size = 6156982, upload-time = "2026-05-18T23:36:40.817Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b2/d4/7c67becf668f973cb490cec3e98dfd799d866f9c989a54d355672cfa0db6/numpy-2.4.6-cp314-cp314t-win_amd64.whl", hash = "sha256:81a1cca95ed5bb92aa8b10dd2cdc9a0d3853a50fad926c28b5d7e8ea54389627", size = 12638908, upload-time = "2026-05-18T23:36:43.996Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/43/bb/e1c71a4295b1b1d1393d50dbb4f2a36283c6859d9d3892e84f00ec5a91d5/numpy-2.4.6-cp314-cp314t-win_arm64.whl", hash = "sha256:0c9136e14ed34a9e343a31c533d78a9813a69a3148332bce5e9821cb2f996e66", size = 10565867, upload-time = "2026-05-18T23:36:47.114Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "packaging"
|
name = "packaging"
|
||||||
version = "26.2"
|
version = "26.2"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/d7/f1/e7a6dd94a8d4a5626c03e4e99c87f241ba9e350cd9e6d75123f992427270/packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661", size = 228134 }
|
sdist = { url = "https://files.pythonhosted.org/packages/d7/f1/e7a6dd94a8d4a5626c03e4e99c87f241ba9e350cd9e6d75123f992427270/packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661", size = 228134, upload-time = "2026-04-24T20:15:23.917Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195 },
|
{ url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pillow"
|
name = "pillow"
|
||||||
version = "12.2.0"
|
version = "12.2.0"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/8c/21/c2bcdd5906101a30244eaffc1b6e6ce71a31bd0742a01eb89e660ebfac2d/pillow-12.2.0.tar.gz", hash = "sha256:a830b1a40919539d07806aa58e1b114df53ddd43213d9c8b75847eee6c0182b5", size = 46987819 }
|
sdist = { url = "https://files.pythonhosted.org/packages/8c/21/c2bcdd5906101a30244eaffc1b6e6ce71a31bd0742a01eb89e660ebfac2d/pillow-12.2.0.tar.gz", hash = "sha256:a830b1a40919539d07806aa58e1b114df53ddd43213d9c8b75847eee6c0182b5", size = 46987819, upload-time = "2026-04-01T14:46:17.687Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/bf/98/4595daa2365416a86cb0d495248a393dfc84e96d62ad080c8546256cb9c0/pillow-12.2.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:3adc9215e8be0448ed6e814966ecf3d9952f0ea40eb14e89a102b87f450660d8", size = 4100848 },
|
{ url = "https://files.pythonhosted.org/packages/bf/98/4595daa2365416a86cb0d495248a393dfc84e96d62ad080c8546256cb9c0/pillow-12.2.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:3adc9215e8be0448ed6e814966ecf3d9952f0ea40eb14e89a102b87f450660d8", size = 4100848, upload-time = "2026-04-01T14:44:48.48Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/0b/79/40184d464cf89f6663e18dfcf7ca21aae2491fff1a16127681bf1fa9b8cf/pillow-12.2.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:6a9adfc6d24b10f89588096364cc726174118c62130c817c2837c60cf08a392b", size = 4176515 },
|
{ url = "https://files.pythonhosted.org/packages/0b/79/40184d464cf89f6663e18dfcf7ca21aae2491fff1a16127681bf1fa9b8cf/pillow-12.2.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:6a9adfc6d24b10f89588096364cc726174118c62130c817c2837c60cf08a392b", size = 4176515, upload-time = "2026-04-01T14:44:51.353Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/b0/63/703f86fd4c422a9cf722833670f4f71418fb116b2853ff7da722ea43f184/pillow-12.2.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:6a6e67ea2e6feda684ed370f9a1c52e7a243631c025ba42149a2cc5934dec295", size = 3640159 },
|
{ url = "https://files.pythonhosted.org/packages/b0/63/703f86fd4c422a9cf722833670f4f71418fb116b2853ff7da722ea43f184/pillow-12.2.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:6a6e67ea2e6feda684ed370f9a1c52e7a243631c025ba42149a2cc5934dec295", size = 3640159, upload-time = "2026-04-01T14:44:53.588Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/71/e0/fb22f797187d0be2270f83500aab851536101b254bfa1eae10795709d283/pillow-12.2.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:2bb4a8d594eacdfc59d9e5ad972aa8afdd48d584ffd5f13a937a664c3e7db0ed", size = 5312185 },
|
{ url = "https://files.pythonhosted.org/packages/71/e0/fb22f797187d0be2270f83500aab851536101b254bfa1eae10795709d283/pillow-12.2.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:2bb4a8d594eacdfc59d9e5ad972aa8afdd48d584ffd5f13a937a664c3e7db0ed", size = 5312185, upload-time = "2026-04-01T14:44:56.039Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/ba/8c/1a9e46228571de18f8e28f16fabdfc20212a5d019f3e3303452b3f0a580d/pillow-12.2.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:80b2da48193b2f33ed0c32c38140f9d3186583ce7d516526d462645fd98660ae", size = 4695386 },
|
{ url = "https://files.pythonhosted.org/packages/ba/8c/1a9e46228571de18f8e28f16fabdfc20212a5d019f3e3303452b3f0a580d/pillow-12.2.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:80b2da48193b2f33ed0c32c38140f9d3186583ce7d516526d462645fd98660ae", size = 4695386, upload-time = "2026-04-01T14:44:58.663Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/70/62/98f6b7f0c88b9addd0e87c217ded307b36be024d4ff8869a812b241d1345/pillow-12.2.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:22db17c68434de69d8ecfc2fe821569195c0c373b25cccb9cbdacf2c6e53c601", size = 6280384 },
|
{ url = "https://files.pythonhosted.org/packages/70/62/98f6b7f0c88b9addd0e87c217ded307b36be024d4ff8869a812b241d1345/pillow-12.2.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:22db17c68434de69d8ecfc2fe821569195c0c373b25cccb9cbdacf2c6e53c601", size = 6280384, upload-time = "2026-04-01T14:45:01.5Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/5e/03/688747d2e91cfbe0e64f316cd2e8005698f76ada3130d0194664174fa5de/pillow-12.2.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7b14cc0106cd9aecda615dd6903840a058b4700fcb817687d0ee4fc8b6e389be", size = 8091599 },
|
{ url = "https://files.pythonhosted.org/packages/5e/03/688747d2e91cfbe0e64f316cd2e8005698f76ada3130d0194664174fa5de/pillow-12.2.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7b14cc0106cd9aecda615dd6903840a058b4700fcb817687d0ee4fc8b6e389be", size = 8091599, upload-time = "2026-04-01T14:45:04.5Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/f6/35/577e22b936fcdd66537329b33af0b4ccfefaeabd8aec04b266528cddb33c/pillow-12.2.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8cbeb542b2ebc6fcdacabf8aca8c1a97c9b3ad3927d46b8723f9d4f033288a0f", size = 6396021 },
|
{ url = "https://files.pythonhosted.org/packages/f6/35/577e22b936fcdd66537329b33af0b4ccfefaeabd8aec04b266528cddb33c/pillow-12.2.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8cbeb542b2ebc6fcdacabf8aca8c1a97c9b3ad3927d46b8723f9d4f033288a0f", size = 6396021, upload-time = "2026-04-01T14:45:07.117Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/11/8d/d2532ad2a603ca2b93ad9f5135732124e57811d0168155852f37fbce2458/pillow-12.2.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4bfd07bc812fbd20395212969e41931001fd59eb55a60658b0e5710872e95286", size = 7083360 },
|
{ url = "https://files.pythonhosted.org/packages/11/8d/d2532ad2a603ca2b93ad9f5135732124e57811d0168155852f37fbce2458/pillow-12.2.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4bfd07bc812fbd20395212969e41931001fd59eb55a60658b0e5710872e95286", size = 7083360, upload-time = "2026-04-01T14:45:09.763Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/5e/26/d325f9f56c7e039034897e7380e9cc202b1e368bfd04d4cbe6a441f02885/pillow-12.2.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9aba9a17b623ef750a4d11b742cbafffeb48a869821252b30ee21b5e91392c50", size = 6507628 },
|
{ url = "https://files.pythonhosted.org/packages/5e/26/d325f9f56c7e039034897e7380e9cc202b1e368bfd04d4cbe6a441f02885/pillow-12.2.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9aba9a17b623ef750a4d11b742cbafffeb48a869821252b30ee21b5e91392c50", size = 6507628, upload-time = "2026-04-01T14:45:12.378Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/5f/f7/769d5632ffb0988f1c5e7660b3e731e30f7f8ec4318e94d0a5d674eb65a4/pillow-12.2.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:deede7c263feb25dba4e82ea23058a235dcc2fe1f6021025dc71f2b618e26104", size = 7209321 },
|
{ url = "https://files.pythonhosted.org/packages/5f/f7/769d5632ffb0988f1c5e7660b3e731e30f7f8ec4318e94d0a5d674eb65a4/pillow-12.2.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:deede7c263feb25dba4e82ea23058a235dcc2fe1f6021025dc71f2b618e26104", size = 7209321, upload-time = "2026-04-01T14:45:15.122Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/6a/7a/c253e3c645cd47f1aceea6a8bacdba9991bf45bb7dfe927f7c893e89c93c/pillow-12.2.0-cp314-cp314-win32.whl", hash = "sha256:632ff19b2778e43162304d50da0181ce24ac5bb8180122cbe1bf4673428328c7", size = 6479723 },
|
{ url = "https://files.pythonhosted.org/packages/6a/7a/c253e3c645cd47f1aceea6a8bacdba9991bf45bb7dfe927f7c893e89c93c/pillow-12.2.0-cp314-cp314-win32.whl", hash = "sha256:632ff19b2778e43162304d50da0181ce24ac5bb8180122cbe1bf4673428328c7", size = 6479723, upload-time = "2026-04-01T14:45:17.797Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/cd/8b/601e6566b957ca50e28725cb6c355c59c2c8609751efbecd980db44e0349/pillow-12.2.0-cp314-cp314-win_amd64.whl", hash = "sha256:4e6c62e9d237e9b65fac06857d511e90d8461a32adcc1b9065ea0c0fa3a28150", size = 7217400 },
|
{ url = "https://files.pythonhosted.org/packages/cd/8b/601e6566b957ca50e28725cb6c355c59c2c8609751efbecd980db44e0349/pillow-12.2.0-cp314-cp314-win_amd64.whl", hash = "sha256:4e6c62e9d237e9b65fac06857d511e90d8461a32adcc1b9065ea0c0fa3a28150", size = 7217400, upload-time = "2026-04-01T14:45:20.529Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/d6/94/220e46c73065c3e2951bb91c11a1fb636c8c9ad427ac3ce7d7f3359b9b2f/pillow-12.2.0-cp314-cp314-win_arm64.whl", hash = "sha256:b1c1fbd8a5a1af3412a0810d060a78b5136ec0836c8a4ef9aa11807f2a22f4e1", size = 2554835 },
|
{ url = "https://files.pythonhosted.org/packages/d6/94/220e46c73065c3e2951bb91c11a1fb636c8c9ad427ac3ce7d7f3359b9b2f/pillow-12.2.0-cp314-cp314-win_arm64.whl", hash = "sha256:b1c1fbd8a5a1af3412a0810d060a78b5136ec0836c8a4ef9aa11807f2a22f4e1", size = 2554835, upload-time = "2026-04-01T14:45:23.162Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/b6/ab/1b426a3974cb0e7da5c29ccff4807871d48110933a57207b5a676cccc155/pillow-12.2.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:57850958fe9c751670e49b2cecf6294acc99e562531f4bd317fa5ddee2068463", size = 5314225 },
|
{ url = "https://files.pythonhosted.org/packages/b6/ab/1b426a3974cb0e7da5c29ccff4807871d48110933a57207b5a676cccc155/pillow-12.2.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:57850958fe9c751670e49b2cecf6294acc99e562531f4bd317fa5ddee2068463", size = 5314225, upload-time = "2026-04-01T14:45:25.637Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/19/1e/dce46f371be2438eecfee2a1960ee2a243bbe5e961890146d2dee1ff0f12/pillow-12.2.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:d5d38f1411c0ed9f97bcb49b7bd59b6b7c314e0e27420e34d99d844b9ce3b6f3", size = 4698541 },
|
{ url = "https://files.pythonhosted.org/packages/19/1e/dce46f371be2438eecfee2a1960ee2a243bbe5e961890146d2dee1ff0f12/pillow-12.2.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:d5d38f1411c0ed9f97bcb49b7bd59b6b7c314e0e27420e34d99d844b9ce3b6f3", size = 4698541, upload-time = "2026-04-01T14:45:28.355Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/55/c3/7fbecf70adb3a0c33b77a300dc52e424dc22ad8cdc06557a2e49523b703d/pillow-12.2.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5c0a9f29ca8e79f09de89293f82fc9b0270bb4af1d58bc98f540cc4aedf03166", size = 6322251 },
|
{ url = "https://files.pythonhosted.org/packages/55/c3/7fbecf70adb3a0c33b77a300dc52e424dc22ad8cdc06557a2e49523b703d/pillow-12.2.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5c0a9f29ca8e79f09de89293f82fc9b0270bb4af1d58bc98f540cc4aedf03166", size = 6322251, upload-time = "2026-04-01T14:45:30.924Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/1c/3c/7fbc17cfb7e4fe0ef1642e0abc17fc6c94c9f7a16be41498e12e2ba60408/pillow-12.2.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1610dd6c61621ae1cf811bef44d77e149ce3f7b95afe66a4512f8c59f25d9ebe", size = 8127807 },
|
{ url = "https://files.pythonhosted.org/packages/1c/3c/7fbc17cfb7e4fe0ef1642e0abc17fc6c94c9f7a16be41498e12e2ba60408/pillow-12.2.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1610dd6c61621ae1cf811bef44d77e149ce3f7b95afe66a4512f8c59f25d9ebe", size = 8127807, upload-time = "2026-04-01T14:45:33.908Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/ff/c3/a8ae14d6defd2e448493ff512fae903b1e9bd40b72efb6ec55ce0048c8ce/pillow-12.2.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0a34329707af4f73cf1782a36cd2289c0368880654a2c11f027bcee9052d35dd", size = 6433935 },
|
{ url = "https://files.pythonhosted.org/packages/ff/c3/a8ae14d6defd2e448493ff512fae903b1e9bd40b72efb6ec55ce0048c8ce/pillow-12.2.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0a34329707af4f73cf1782a36cd2289c0368880654a2c11f027bcee9052d35dd", size = 6433935, upload-time = "2026-04-01T14:45:36.623Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/6e/32/2880fb3a074847ac159d8f902cb43278a61e85f681661e7419e6596803ed/pillow-12.2.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8e9c4f5b3c546fa3458a29ab22646c1c6c787ea8f5ef51300e5a60300736905e", size = 7116720 },
|
{ url = "https://files.pythonhosted.org/packages/6e/32/2880fb3a074847ac159d8f902cb43278a61e85f681661e7419e6596803ed/pillow-12.2.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8e9c4f5b3c546fa3458a29ab22646c1c6c787ea8f5ef51300e5a60300736905e", size = 7116720, upload-time = "2026-04-01T14:45:39.258Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/46/87/495cc9c30e0129501643f24d320076f4cc54f718341df18cc70ec94c44e1/pillow-12.2.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:fb043ee2f06b41473269765c2feae53fc2e2fbf96e5e22ca94fb5ad677856f06", size = 6540498 },
|
{ url = "https://files.pythonhosted.org/packages/46/87/495cc9c30e0129501643f24d320076f4cc54f718341df18cc70ec94c44e1/pillow-12.2.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:fb043ee2f06b41473269765c2feae53fc2e2fbf96e5e22ca94fb5ad677856f06", size = 6540498, upload-time = "2026-04-01T14:45:41.879Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/18/53/773f5edca692009d883a72211b60fdaf8871cbef075eaa9d577f0a2f989e/pillow-12.2.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f278f034eb75b4e8a13a54a876cc4a5ab39173d2cdd93a638e1b467fc545ac43", size = 7239413 },
|
{ url = "https://files.pythonhosted.org/packages/18/53/773f5edca692009d883a72211b60fdaf8871cbef075eaa9d577f0a2f989e/pillow-12.2.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f278f034eb75b4e8a13a54a876cc4a5ab39173d2cdd93a638e1b467fc545ac43", size = 7239413, upload-time = "2026-04-01T14:45:44.705Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/c9/e4/4b64a97d71b2a83158134abbb2f5bd3f8a2ea691361282f010998f339ec7/pillow-12.2.0-cp314-cp314t-win32.whl", hash = "sha256:6bb77b2dcb06b20f9f4b4a8454caa581cd4dd0643a08bacf821216a16d9c8354", size = 6482084 },
|
{ url = "https://files.pythonhosted.org/packages/c9/e4/4b64a97d71b2a83158134abbb2f5bd3f8a2ea691361282f010998f339ec7/pillow-12.2.0-cp314-cp314t-win32.whl", hash = "sha256:6bb77b2dcb06b20f9f4b4a8454caa581cd4dd0643a08bacf821216a16d9c8354", size = 6482084, upload-time = "2026-04-01T14:45:47.568Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/ba/13/306d275efd3a3453f72114b7431c877d10b1154014c1ebbedd067770d629/pillow-12.2.0-cp314-cp314t-win_amd64.whl", hash = "sha256:6562ace0d3fb5f20ed7290f1f929cae41b25ae29528f2af1722966a0a02e2aa1", size = 7225152 },
|
{ url = "https://files.pythonhosted.org/packages/ba/13/306d275efd3a3453f72114b7431c877d10b1154014c1ebbedd067770d629/pillow-12.2.0-cp314-cp314t-win_amd64.whl", hash = "sha256:6562ace0d3fb5f20ed7290f1f929cae41b25ae29528f2af1722966a0a02e2aa1", size = 7225152, upload-time = "2026-04-01T14:45:50.032Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/ff/6e/cf826fae916b8658848d7b9f38d88da6396895c676e8086fc0988073aaf8/pillow-12.2.0-cp314-cp314t-win_arm64.whl", hash = "sha256:aa88ccfe4e32d362816319ed727a004423aab09c5cea43c01a4b435643fa34eb", size = 2556579 },
|
{ url = "https://files.pythonhosted.org/packages/ff/6e/cf826fae916b8658848d7b9f38d88da6396895c676e8086fc0988073aaf8/pillow-12.2.0-cp314-cp314t-win_arm64.whl", hash = "sha256:aa88ccfe4e32d362816319ed727a004423aab09c5cea43c01a4b435643fa34eb", size = 2556579, upload-time = "2026-04-01T14:45:52.529Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pluggy"
|
name = "pluggy"
|
||||||
version = "1.6.0"
|
version = "1.6.0"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412 }
|
sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538 },
|
{ url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "psutil"
|
||||||
|
version = "7.2.2"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/aa/c6/d1ddf4abb55e93cebc4f2ed8b5d6dbad109ecb8d63748dd2b20ab5e57ebe/psutil-7.2.2.tar.gz", hash = "sha256:0746f5f8d406af344fd547f1c8daa5f5c33dbc293bb8d6a16d80b4bb88f59372", size = 493740, upload-time = "2026-01-28T18:14:54.428Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/81/69/ef179ab5ca24f32acc1dac0c247fd6a13b501fd5534dbae0e05a1c48b66d/psutil-7.2.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:eed63d3b4d62449571547b60578c5b2c4bcccc5387148db46e0c2313dad0ee00", size = 130664, upload-time = "2026-01-28T18:15:09.469Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7b/64/665248b557a236d3fa9efc378d60d95ef56dd0a490c2cd37dafc7660d4a9/psutil-7.2.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7b6d09433a10592ce39b13d7be5a54fbac1d1228ed29abc880fb23df7cb694c9", size = 131087, upload-time = "2026-01-28T18:15:11.724Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d5/2e/e6782744700d6759ebce3043dcfa661fb61e2fb752b91cdeae9af12c2178/psutil-7.2.2-cp314-cp314t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fa4ecf83bcdf6e6c8f4449aff98eefb5d0604bf88cb883d7da3d8d2d909546a", size = 182383, upload-time = "2026-01-28T18:15:13.445Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/57/49/0a41cefd10cb7505cdc04dab3eacf24c0c2cb158a998b8c7b1d27ee2c1f5/psutil-7.2.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e452c464a02e7dc7822a05d25db4cde564444a67e58539a00f929c51eddda0cf", size = 185210, upload-time = "2026-01-28T18:15:16.002Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/dd/2c/ff9bfb544f283ba5f83ba725a3c5fec6d6b10b8f27ac1dc641c473dc390d/psutil-7.2.2-cp314-cp314t-win_amd64.whl", hash = "sha256:c7663d4e37f13e884d13994247449e9f8f574bc4655d509c3b95e9ec9e2b9dc1", size = 141228, upload-time = "2026-01-28T18:15:18.385Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f2/fc/f8d9c31db14fcec13748d373e668bc3bed94d9077dbc17fb0eebc073233c/psutil-7.2.2-cp314-cp314t-win_arm64.whl", hash = "sha256:11fe5a4f613759764e79c65cf11ebdf26e33d6dd34336f8a337aa2996d71c841", size = 136284, upload-time = "2026-01-28T18:15:19.912Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e7/36/5ee6e05c9bd427237b11b3937ad82bb8ad2752d72c6969314590dd0c2f6e/psutil-7.2.2-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:ed0cace939114f62738d808fdcecd4c869222507e266e574799e9c0faa17d486", size = 129090, upload-time = "2026-01-28T18:15:22.168Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/80/c4/f5af4c1ca8c1eeb2e92ccca14ce8effdeec651d5ab6053c589b074eda6e1/psutil-7.2.2-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:1a7b04c10f32cc88ab39cbf606e117fd74721c831c98a27dc04578deb0c16979", size = 129859, upload-time = "2026-01-28T18:15:23.795Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b5/70/5d8df3b09e25bce090399cf48e452d25c935ab72dad19406c77f4e828045/psutil-7.2.2-cp36-abi3-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:076a2d2f923fd4821644f5ba89f059523da90dc9014e85f8e45a5774ca5bc6f9", size = 155560, upload-time = "2026-01-28T18:15:25.976Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/63/65/37648c0c158dc222aba51c089eb3bdfa238e621674dc42d48706e639204f/psutil-7.2.2-cp36-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b0726cecd84f9474419d67252add4ac0cd9811b04d61123054b9fb6f57df6e9e", size = 156997, upload-time = "2026-01-28T18:15:27.794Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8e/13/125093eadae863ce03c6ffdbae9929430d116a246ef69866dad94da3bfbc/psutil-7.2.2-cp36-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:fd04ef36b4a6d599bbdb225dd1d3f51e00105f6d48a28f006da7f9822f2606d8", size = 148972, upload-time = "2026-01-28T18:15:29.342Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/04/78/0acd37ca84ce3ddffaa92ef0f571e073faa6d8ff1f0559ab1272188ea2be/psutil-7.2.2-cp36-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b58fabe35e80b264a4e3bb23e6b96f9e45a3df7fb7eed419ac0e5947c61e47cc", size = 148266, upload-time = "2026-01-28T18:15:31.597Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b4/90/e2159492b5426be0c1fef7acba807a03511f97c5f86b3caeda6ad92351a7/psutil-7.2.2-cp37-abi3-win_amd64.whl", hash = "sha256:eb7e81434c8d223ec4a219b5fc1c47d0417b12be7ea866e24fb5ad6e84b3d988", size = 137737, upload-time = "2026-01-28T18:15:33.849Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8c/c7/7bb2e321574b10df20cbde462a94e2b71d05f9bbda251ef27d104668306a/psutil-7.2.2-cp37-abi3-win_arm64.whl", hash = "sha256:8c233660f575a5a89e6d4cb65d9f938126312bca76d8fe087b947b3a1aaac9ee", size = 134617, upload-time = "2026-01-28T18:15:36.514Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pygments"
|
name = "pygments"
|
||||||
version = "2.20.0"
|
version = "2.20.0"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991 }
|
sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151 },
|
{ url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pyrekordbox"
|
||||||
|
version = "0.4.4"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "bidict" },
|
||||||
|
{ name = "construct" },
|
||||||
|
{ name = "numpy" },
|
||||||
|
{ name = "packaging" },
|
||||||
|
{ name = "psutil" },
|
||||||
|
{ name = "python-dateutil" },
|
||||||
|
{ name = "sqlalchemy" },
|
||||||
|
{ name = "sqlcipher3-wheels" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/39/8b/8fb7f21f4eb676df35c53214129ad33624634e2b678069abbf6a88509c6b/pyrekordbox-0.4.4.tar.gz", hash = "sha256:8bdb59f7c45c743bd2021613a72604b7607092d386e25f20cce78adfe30bf9a4", size = 2310256, upload-time = "2025-08-17T20:09:18.773Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b1/a8/28d28f02683bee40e3cfa32caed7c46e47daffb51482798fea9af0ab23cb/pyrekordbox-0.4.4-py3-none-any.whl", hash = "sha256:2de933870126b9a0ef04bea372f1c5934efcfbb2501efb6b47d8ace0e17fb3d8", size = 76000, upload-time = "2025-08-17T20:09:16.985Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -133,7 +263,104 @@ dependencies = [
|
|||||||
{ name = "pluggy" },
|
{ name = "pluggy" },
|
||||||
{ name = "pygments" },
|
{ name = "pygments" },
|
||||||
]
|
]
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/7d/0d/549bd94f1a0a402dc8cf64563a117c0f3765662e2e668477624baeec44d5/pytest-9.0.3.tar.gz", hash = "sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c", size = 1572165 }
|
sdist = { url = "https://files.pythonhosted.org/packages/7d/0d/549bd94f1a0a402dc8cf64563a117c0f3765662e2e668477624baeec44d5/pytest-9.0.3.tar.gz", hash = "sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c", size = 1572165, upload-time = "2026-04-07T17:16:18.027Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9", size = 375249 },
|
{ url = "https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9", size = 375249, upload-time = "2026-04-07T17:16:16.13Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "python-dateutil"
|
||||||
|
version = "2.9.0.post0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "six" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "six"
|
||||||
|
version = "1.17.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "sqlalchemy"
|
||||||
|
version = "2.0.50"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "greenlet", marker = "platform_machine == 'AMD64' or platform_machine == 'WIN32' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'ppc64le' or platform_machine == 'win32' or platform_machine == 'x86_64'" },
|
||||||
|
{ name = "typing-extensions" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/57/da/6fbf010c8ebb347679d0d100b22fe9ba5e13fd04046c5df7280d2f0bf706/sqlalchemy-2.0.50.tar.gz", hash = "sha256:af5607d11ef90fd6a5c0549fe0045dce1663d427426bcfb506dcb5346a85a3b9", size = 9907424, upload-time = "2026-05-24T19:20:04.018Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/df/32/10ac51b4be7cdecd7e93d069251c86dfbf70b7adbd7c67b48ccea6c49e1c/sqlalchemy-2.0.50-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c966932507a4d7d0a37314927dbfcd89720e3f37d2a1e3352e7ae7939fa8e8a0", size = 2158519, upload-time = "2026-05-24T19:27:56.472Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/5a/76/e703d2f7681d7d66c4c891af3f07c7ccf4c76ad7f18351de035b5eda007a/sqlalchemy-2.0.50-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:faffef4bcc20a1892e65e155293d99d60855bbbc79250ab712819cfd56a8e6bb", size = 3282063, upload-time = "2026-05-24T20:09:38.57Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/31/26/ef168b184a25701f9995e8fb7e503fafd7a99c1c77cda1bc1a26ea2ed486/sqlalchemy-2.0.50-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6c206aec519a2e7bd08abbfb33436e325fd22c632d9c21a9047e376ce241646e", size = 3287069, upload-time = "2026-05-24T20:17:21.942Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c2/15/765acc2bc693bccc43ca4a95d5b69750da8aaf6db1b5c616536e087f8920/sqlalchemy-2.0.50-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:bef4ac756363227ef6402a75fee025a4bc690f92328e825868939b3b3a446a6d", size = 3230453, upload-time = "2026-05-24T20:09:40.398Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/63/61/08e03c3adbf5db0087a0b6816746fec8f3032fb2f7fc899a9bb9b2a48ce4/sqlalchemy-2.0.50-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:96fbee6b19c19cd1556c8bf9419447cf2ec149ffcab7ab64348c23e54ef8547f", size = 3252413, upload-time = "2026-05-24T20:17:24.067Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/03/0c/370a1f2db38436c615e10134c8a37de3688e74084792380695f3f5083860/sqlalchemy-2.0.50-cp314-cp314-win32.whl", hash = "sha256:8f00e3eb43ba30eb1b238ee03a8a62309486d1321eda3328bb611e0340033ad8", size = 2120063, upload-time = "2026-05-24T19:50:11.08Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7f/a0/fe92bb9817863bc13ba093bda931979a26cc2ca69f8e8f26d07add3d7c6f/sqlalchemy-2.0.50-cp314-cp314-win_amd64.whl", hash = "sha256:15708c613cd5005b7dffe1f66ee6a63ee8f5e46799f71c70ebad74178c676a39", size = 2145830, upload-time = "2026-05-24T19:50:12.452Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/cc/ff/e5640a98a0b2f491eb8fde10fb6c773621a2e44340de231fafcc9370f4a9/sqlalchemy-2.0.50-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:3699dac4be410e97049a1658e9480da9cde956594aa0f3aebc60b88f21c5ba70", size = 2178435, upload-time = "2026-05-24T19:42:58.889Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b7/85/337116e186f1236375b5fb70c21cfac98e8e8ab0d3a47be838dc47a59e08/sqlalchemy-2.0.50-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f96233858e3df43932ac11589e22520da6e8aeb624b03fedfeebb0e8ea213086", size = 3566059, upload-time = "2026-05-24T20:01:20.848Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/96/34/bb0e190e161c3c2c24314a65add57218be14a4a9486886b7f5047c1ff7c8/sqlalchemy-2.0.50-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c4e70c46fad30c3bcc6a4708bc0130a3173e11a5b25f0ea4a9d8911b450f1f52", size = 3535366, upload-time = "2026-05-24T20:03:56.768Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/df/5a/a7f759f97e4fd499c5d4e4488c760d5a7fbecf3028b465a04274fcd52384/sqlalchemy-2.0.50-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:1918a3cf564d16d95bca7301005f41ab2ad50b07cd3b9da50d3ed986db148d6a", size = 3474879, upload-time = "2026-05-24T20:01:23.058Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9d/d9/2907ea38eb60687d297bf9c39e5ee58053c87b57fe8a9cae97090cecbf10/sqlalchemy-2.0.50-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b00098cdbdbd38c7be3d568b0c9c3122b8c0ec62b911b57cd5e6e0254d60a76d", size = 3486117, upload-time = "2026-05-24T20:03:59.052Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f2/e3/5aa06f167559f8c0bdae487e297d23ba548150ab016a3418265d617a4985/sqlalchemy-2.0.50-cp314-cp314t-win32.whl", hash = "sha256:1fbd55a969d7ac44a98e3dec75016074f809fa08f871585ace58dde110d1bf3e", size = 2150823, upload-time = "2026-05-24T20:08:58.644Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/65/9b/112fb8f977582d7489d036e409e3723948bcf5320b3ac465f3c481bbe8f9/sqlalchemy-2.0.50-cp314-cp314t-win_amd64.whl", hash = "sha256:c5c3cdb753a9004183e1ccb634b41611654c989e61bc68617ce878e46d6f1e51", size = 2185794, upload-time = "2026-05-24T20:09:00.319Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d0/10/f7220e9b784d295d241c86ed99aeb537f92afcd469a64861f2717e9bb077/sqlalchemy-2.0.50-py3-none-any.whl", hash = "sha256:92064363517a3ff8212b5a93b8c62876579d8dfd1ca5b561335f30152d884fa9", size = 1943861, upload-time = "2026-05-24T19:59:01.119Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "sqlcipher3-wheels"
|
||||||
|
version = "0.5.7"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/61/ec/d79c2e5f5b9246471f217cdd5f703dc8d804bce557a047e1bfad4f33c376/sqlcipher3_wheels-0.5.7.tar.gz", hash = "sha256:5fd344abba62c487c4d857a6fb55cb8dff8a97d4c4fbb4f9394b473e8e315dee", size = 2669821, upload-time = "2026-01-07T09:53:25.48Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/1e/b5/8ca4fe7928d1e7151ecdf221ad757540e9a815ea10d854f047491859cd0c/sqlcipher3_wheels-0.5.7-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:6831fd121adf8374ecfb67b63d9bdfc3bc405e7103e35dd3152f7f2d2bbe87ae", size = 4946253, upload-time = "2026-01-07T09:51:46.013Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/cd/39/5eca1cced6b3a78cb20201232103b58ded2d57e87c413212bfd2a30c7209/sqlcipher3_wheels-0.5.7-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:9d03491d9d4023cca969a4c87f01ea845368defcc52ea7cd71ce93deac3df286", size = 2898557, upload-time = "2026-01-07T09:51:47.261Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/34/93/8c34dd286ff8bea8785896eee401a5e15ab37901687ba09d2bf0df584712/sqlcipher3_wheels-0.5.7-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1f3325fee4698fadbfa9b8245f213f7f3ecc3f0c86a761a2b26c787d54d3ecd1", size = 3163380, upload-time = "2026-01-07T09:51:48.384Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6d/2e/5b62f3c4181a64ab041589c6fdb509a70d69c2653f91d35b19c7550ad0d3/sqlcipher3_wheels-0.5.7-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:f80b8c72df20f8e2696e3b23577145e94b3f5a10217fef02d5c78d0ef2e578ba", size = 7259742, upload-time = "2026-01-07T09:51:49.698Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/09/18/d78100264c66c96aec0723b2ddaf2b4edc72374e60c25b5ed8f3afb5e4f0/sqlcipher3_wheels-0.5.7-cp314-cp314-manylinux_2_28_i686.whl", hash = "sha256:9feb22952dd44a6e86bbb1e95a0a603bd7cc61332d0b1adad2484d2ccdc61704", size = 6875763, upload-time = "2026-01-07T09:51:51.009Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8b/e0/0529fc2d3cb0d58950557e2ac229d338506e9522a7d3eb03540acd4a6ee7/sqlcipher3_wheels-0.5.7-cp314-cp314-manylinux_2_28_ppc64le.whl", hash = "sha256:79fd014b64fefd85086cff6af85286cfb3b5761473f0a541f973a02a3c0e184b", size = 7413959, upload-time = "2026-01-07T09:51:52.344Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/06/81/290107b3bd5a84edcc7a542ce1e7fe42e401cd94f4a8b6ab22e546ba034d/sqlcipher3_wheels-0.5.7-cp314-cp314-manylinux_2_28_s390x.whl", hash = "sha256:92462368d7afc2c396466eeb4cfed85dccc12bef06a5f3a9a43a6598890ddd15", size = 7389084, upload-time = "2026-01-07T09:51:53.786Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/54/f1/9237d5d14807d1a6c7900330b55cad40e192f54dd4d5f732239a2271922d/sqlcipher3_wheels-0.5.7-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:663f814ca785d064408ec96c7abdc6e940809efac34cdc1656fb3536cffb8ff5", size = 7039875, upload-time = "2026-01-07T09:51:55.236Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b9/46/ba150f6d1c38f0742055d5a693f5c3532a85c4aac3c8789d130ab8492dd9/sqlcipher3_wheels-0.5.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:47b5ae81361c0ca45fab27ce09f94ff795a7763767c9bcbbbce19923c82d2f65", size = 7252142, upload-time = "2026-01-07T09:51:56.581Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b0/d5/e493a4a19dedc2a9c25751bcc1a7eebc29a5cbc8859108b814ef20a8742e/sqlcipher3_wheels-0.5.7-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:869f77c3f8bfc94f05352b6ca42236adfb1b46f6c92082829a608cd561793a54", size = 6948368, upload-time = "2026-01-07T09:51:57.855Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6e/ac/e82f8e26d91859570c79e3c07d2de07c65001dde49b25cb60f058f694929/sqlcipher3_wheels-0.5.7-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:98f1b5f0f54316f105f553ec254961986c8dc617947f67daa11d49a19cbc9a58", size = 7385735, upload-time = "2026-01-07T09:51:59.874Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c2/2e/3152b89c2b5ce3f7c830b675451957349e4e0d2006d0d90354a50f5c8a01/sqlcipher3_wheels-0.5.7-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:e4ca2cd825be2c30e6530c20ba6112545aaa409ad77478fb66285f98f44aac78", size = 7027637, upload-time = "2026-01-07T09:52:03.298Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/76/8b/a1b3e8044128ab6a6343ef181a658298b598d0a5a2973d33e7f8fa5e45c0/sqlcipher3_wheels-0.5.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:a823f48efe2927a781d28ce31a11263be0e836016ea7ef26e53da18efba2569b", size = 7061293, upload-time = "2026-01-07T09:52:04.997Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/cb/2f/350f8ab4e061c588ad90e12dec59f609a7e4b51c7dc921b9c62eee8c020c/sqlcipher3_wheels-0.5.7-cp314-cp314-win32.whl", hash = "sha256:0f3a11b144c46a33cb34235b253a46b1b255968be34430f9bd7f9ed710283d41", size = 2033959, upload-time = "2026-01-07T09:52:06.307Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a0/f4/ecb143774ccf0c6c8b7c718b97ac5a69ef59aa57d89c2422aa564d4a9b2f/sqlcipher3_wheels-0.5.7-cp314-cp314-win_amd64.whl", hash = "sha256:f210fdbbb794cc1b3739c7bff879b8bcc5949567f5c222e251882ec076e69b72", size = 2560254, upload-time = "2026-01-07T09:52:07.614Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2e/bc/82cd7b7ecc0ad647d9641c9115a875ea6fe8bbc04b83bfd583ebf954260f/sqlcipher3_wheels-0.5.7-cp314-cp314-win_arm64.whl", hash = "sha256:30f825607048436ab6134450ddcc49ad05f17479640a830c0ea8d78f1700e6e0", size = 2727306, upload-time = "2026-01-07T09:52:08.83Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8e/06/dcad55d438eeb9b9b46349434a75ee98f2c42196077a63d7e263969195eb/sqlcipher3_wheels-0.5.7-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:b822211ea0d50500bc8dc82863fa570543e0f224359ccb46b7675a24c89983f5", size = 4948699, upload-time = "2026-01-07T09:52:10.513Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/31/c2/66fcd2a7c3200323517e69c2ff49d48bb6aad15bc47d6f7d75710531a5c2/sqlcipher3_wheels-0.5.7-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:670f493bf5a9e70e86b0ae680a8bbf96aeaaf526c5018262186e6d7c52f021b1", size = 2900350, upload-time = "2026-01-07T09:52:11.766Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7b/3a/94bebb8260624be45531af2692e9f5329ee1163237f027a3716ce3f2ae39/sqlcipher3_wheels-0.5.7-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:8c4a6dd38e9c4eff2304ea0751e8c5612394f277e63f007ae85118d9ce49b427", size = 3167268, upload-time = "2026-01-07T09:52:13.036Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a1/04/00d4d7e8905d9b53cc3cf490223b151df9a794e10138692fd4822962408b/sqlcipher3_wheels-0.5.7-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:7438567a502bfde64ca8732d1466ee6801deb97856315ac06d168a68456b2783", size = 7301506, upload-time = "2026-01-07T09:52:14.366Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/95/81/75e439c4f13cbf79b243de08394916450c812eb8e949b726a758089aaf3b/sqlcipher3_wheels-0.5.7-cp314-cp314t-manylinux_2_28_i686.whl", hash = "sha256:098112cdba65feefdbaba3eebbefe45b5481158882d21a7d133f475614a2fff0", size = 6915748, upload-time = "2026-01-07T09:52:16.166Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/48/3c/36e5aa4fbfe25884d7c930215cc2003b4ac7709866992086179982d04bc5/sqlcipher3_wheels-0.5.7-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:3f54e5284ebfeb2b3578ec35d3c4ad209ec1d4a3fd31b910608eadd95169cf83", size = 7453665, upload-time = "2026-01-07T09:52:18.415Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ec/62/9eddf7efdd523b3a109416f31185361608d1db60b550eb6e787a209765bc/sqlcipher3_wheels-0.5.7-cp314-cp314t-manylinux_2_28_s390x.whl", hash = "sha256:8d3bcfbb799abfa4e31e70e61c779e963b993748bf56bd0603007a47cb2a3c3f", size = 7431832, upload-time = "2026-01-07T09:52:22.041Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/61/86/3e1f57a9e65d42f67f6e2e07b6c1eee14ef12caa08eedcbeebdba4389ba4/sqlcipher3_wheels-0.5.7-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:97617b4c838ad82877a342df111aecbd8653dd6c4c7dc3dccd1ef6389247d614", size = 7077205, upload-time = "2026-01-07T09:52:23.85Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/98/0a/4c062770cb79860cf2cdced2b34887aa27e400fd261c553025b74ba0de74/sqlcipher3_wheels-0.5.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:b750c3d51134b82a4c82c9e5a6463de495ad75b7e073d03b1377083332b13ac8", size = 7296097, upload-time = "2026-01-07T09:52:25.622Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/58/c7/6a6cec55e666ddd998c2c15e37a40e4fcab3c0b67c9d03c3d12b5f30d56a/sqlcipher3_wheels-0.5.7-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:dbebc62280b9fd005511189d887c080c7595226bb7242c11f1298a0b25a4c123", size = 6988446, upload-time = "2026-01-07T09:52:26.983Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/fd/f4/ce2198d1296f63d0917fde374a20c09c5c5e454d65681bf8c2ee4b59591b/sqlcipher3_wheels-0.5.7-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:a806f240ede8da4547f3e50dc3ad88c0d244712270fe361a9b7e3f5d52b4bc39", size = 7425807, upload-time = "2026-01-07T09:52:28.416Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/af/4b/d8735ffcf0b463bd54e23be7df46d77d40d43a672bc01a7a8da3e8f6daf3/sqlcipher3_wheels-0.5.7-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:7198ee5b4c51aae6a7dfab93da35fe82a3c8ed37b4bf6cf55d6f1a0eea387d72", size = 7067479, upload-time = "2026-01-07T09:52:29.867Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/97/54/663f4c349ae59a1a9b2180f2b06c9579ddc611649b50e2ad88c2e910582c/sqlcipher3_wheels-0.5.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:af83b12791a1221f1799d7ad6f79edb74a3be642326a9d780dd9f86a290f9c37", size = 7097843, upload-time = "2026-01-07T09:52:31.172Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f3/8e/f8737c593394f050a73742a81e47be2b46b6413f24e96fa0b10d01dc2555/sqlcipher3_wheels-0.5.7-cp314-cp314t-win32.whl", hash = "sha256:90994590f474975d6b480af96ae5ca3887e17303b1144bfea80a38c2e9fdc157", size = 2036431, upload-time = "2026-01-07T09:52:32.444Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2a/05/573df32b1b1738779dc5326b71d0d611d28d386e7141526ed27043d476a1/sqlcipher3_wheels-0.5.7-cp314-cp314t-win_amd64.whl", hash = "sha256:85422846b4c86e63a2b495a2a60945f7a1a04641d1600859cb65fc68a1ce53ed", size = 2564275, upload-time = "2026-01-07T09:52:33.574Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/54/ee/40e79926f469d630bc1df03b131e8e84740b82dc7e202fe272d2da9110e3/sqlcipher3_wheels-0.5.7-cp314-cp314t-win_arm64.whl", hash = "sha256:aeeacd6da1fc293b97641a794d15d862d7928408ddad9ea5072fad99229970aa", size = 2728867, upload-time = "2026-01-07T09:52:34.718Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "typing-extensions"
|
||||||
|
version = "4.15.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" },
|
||||||
]
|
]
|
||||||
@@ -5,10 +5,13 @@
|
|||||||
"path": ".",
|
"path": ".",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "/home/ntr/code/rekordcrate",
|
"path": "h:ome\\ntr\\code\\rekordcrate",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "/Users/nathanrashleigh/code/code/rekordcrate"
|
"path": "U:sers\\nathanrashleigh\\code\\code\\rekordcrate",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "C:\\Users\\ntr\\code\\rekordcrate"
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
"settings":
|
"settings":
|
||||||
|
|||||||
Reference in new issue
Block a user