Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
c445889
dbserver: harden the message decoder against malformed input
vynulldev Oct 10, 2026
310e7e0
dbserver: setup reply follows request arity (legacy vs extended)
vynulldev Oct 10, 2026
a621538
conformance: add cmd/conformance-server for the link-spec harness
vynulldev Oct 10, 2026
bac78d7
conformance: report the discovery port as query_port in readiness
vynulldev Oct 10, 2026
4d710eb
tools: rekordbox_dump.py accepts the key via VYNULL_REKORDBOX_KEY env
vynulldev Oct 10, 2026
99eb0db
dbserver: emit the extended 16-argument row on the extended setup
vynulldev Oct 10, 2026
39fbe59
dbserver/import: preserve rekordbox content IDs + drop deleted tracks
vynulldev Oct 10, 2026
1b98881
dbserver: serve rekordbox's real category IDs, not hashed names
vynulldev Oct 10, 2026
a69038a
readme: credit link-spec in acknowledgements
vynulldev Oct 10, 2026
67160d7
dbserver: match rekordbox's track-row wire fields and track ordering
vynulldev Oct 10, 2026
b3da2b2
dbserver: match rekordbox's root menu and category drill-down hierarchy
vynulldev Oct 10, 2026
2bf28a6
dbserver: serve the REMIXER and ORIGINAL ARTIST category drill-downs
vynulldev Oct 10, 2026
20ae075
dbserver: match rekordbox's PLAYLIST menu ordering and row fields
vynulldev Oct 10, 2026
eee6337
dbserver: match rekordbox's numeric category lists (BPM/rating/year/t…
vynulldev Oct 10, 2026
e78bac9
dbserver: fix rating scale, COLOR list, FILE NAME rows, BPM-distance …
vynulldev Oct 10, 2026
10f4134
dbserver: serve rekordbox's full 24-key KEY menu
vynulldev Oct 10, 2026
41d7bc4
dbserver: match rekordbox's KEY distance rings and track view
vynulldev Oct 10, 2026
7344fc6
dbserver: drop now-dead resolveKeyName/countTracksForKeys helpers
vynulldev Oct 10, 2026
084e1ad
dbserver: serve the MATCHING browse from related-track recommendations
vynulldev Oct 10, 2026
0907f25
dbserver: match rekordbox's SEARCH matching and result rows
vynulldev Oct 10, 2026
dcef176
dbserver: reply to the HOT CUE BANK root with a menu-unavailable error
vynulldev Oct 10, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -803,6 +803,7 @@ This wouldn't exist without the people who reverse-engineered Pioneer's Pro DJ L
- **[rekordcrate](https://github.com/Holzhaus/rekordcrate)** (Jan Holthuis) — Rust parsers and reference exports I leaned on to get the `export.pdb` writer byte-for-byte right.
- **[python-prodj-link](https://github.com/flesniak/python-prodj-link)** (Florian Sniak) — a working Python implementation that helped me understand the dbserver menu and metadata flow.
- **[prolink-connect](https://github.com/EvanPurkhiser/prolink-connect)** and **[prolink-tools](https://github.com/EvanPurkhiser/prolink-tools)** (Evan Purkhiser) — a TypeScript Pro DJ Link stack and streaming overlay; alongside crate-digger they confirmed the route for reading metadata off tracks a deck plays from its own USB (download the player's `export.pdb` over NFS rather than asking its dbserver).
- **[link-spec](https://github.com/evanpurkhiser/link-spec)** (Evan Purkhiser) — a dbserver conformance suite that replays rekordbox's own menu responses as goldens. Running Vynull against it surfaced hundreds of wire-format divergences from rekordbox that would have been impractical to find by hand, and is driving the dbserver toward byte-for-byte parity across the menu queries.
- **[pyrekordbox](https://github.com/dylanljones/pyrekordbox)** (Dylan Jones) — format docs and the starting point for decrypting `master.db`.
- The Kaitai Struct community for the format definitions, and everyone who posted issues and helped along the way.
- **Audio-analysis algorithms** — the beat tracker is an independent implementation of D. Ellis's dynamic-programming method (*"Beat Tracking by Dynamic Programming"*, J. New Music Research, 2007); key detection uses the Krumhansl-Kessler probe-tone profiles (1982). These are published academic methods — the same DP beat-tracking approach is also implemented by [QM-DSP](https://github.com/c4dm/qm-dsp) and [Mixxx](https://github.com/mixxxdj/mixxx), which are worth a look, though the code here derives from the papers rather than those projects.
Expand Down
175 changes: 175 additions & 0 deletions cmd/conformance-server/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
// SPDX-License-Identifier: GPL-3.0-or-later

// Command conformance-server stands Vynull's dbserver up on loopback against a
// fixture library, for the evanpurkhiser/link-spec conformance harness.
//
// It implements the CLI + readiness contract the harness's replay_vynull.py
// adapter drives:
//
// conformance-server --fixture-json <dump> --data-dir <dir> --second-column <col>
//
// - --fixture-json is a tools/rekordbox_dump.py JSON dump (the same format our
// master.db importer consumes); we load it into a library and serve it.
// - --data-dir is a writable per-run scratch directory.
// - --second-column selects the track-list detail column.
//
// On startup it prints exactly one line of JSON to stdout —
// {"status":"ready","listen_address":"127.0.0.1","query_port":N} — then serves
// the dbserver on loopback until signalled. All logging goes to stderr so
// stdout carries only the readiness line. See conformance/BACKENDS.md in
// link-spec and docs/design/foh-manager.md's sibling note.
package main

import (
"context"
"encoding/json"
"flag"
"fmt"
"log"
"os"
"os/signal"
"path/filepath"
"sort"
"strconv"
"strings"
"syscall"

"github.com/vynulldev/vynull/analysis"
"github.com/vynulldev/vynull/dbserver"
"github.com/vynulldev/vynull/library"
)

// confMenu supplies the built-in root menu plus the harness-selected second
// (detail) column.
type confMenu struct{ second string }

func (m confMenu) RootMenu() []dbserver.RootMenuEntry { return dbserver.DefaultRootMenu() }
func (m confMenu) TrackDetail() string { return m.second }

// confPlaylists serves the fixture's playlist tree straight from the dump
// (which keeps rekordbox's playlist IDs, parent IDs, and membership order,
// in Seq order). A ParentID of "root" maps to folder ID 0, which is what the
// deck sends for the top level.
type confPlaylists struct{ nodes []library.MasterDBPlaylist }

func playlistID(s string) uint32 {
if s == "" || s == "root" {
return 0
}
n, _ := strconv.ParseUint(strings.TrimSpace(s), 10, 32)
return uint32(n)
}

func (p confPlaylists) Children(parentID uint32) []dbserver.PlaylistEntry {
var out []dbserver.PlaylistEntry
for _, n := range p.nodes {
if playlistID(n.ParentID) == parentID {
out = append(out, dbserver.PlaylistEntry{
ID: playlistID(n.ID), Name: n.Name, IsFolder: n.IsFolder, Seq: uint32(n.Seq),
})
}
}
// rekordbox lists folders before playlists, each group in Seq order (the
// stored Seq still rides along in arg 9).
sort.SliceStable(out, func(i, j int) bool {
if out[i].IsFolder != out[j].IsFolder {
return out[i].IsFolder
}
return out[i].Seq < out[j].Seq
})
return out
}

func (p confPlaylists) Tracks(playlistID_ uint32) []uint32 {
for _, n := range p.nodes {
if playlistID(n.ID) != playlistID_ {
continue
}
ids := make([]uint32, 0, len(n.TrackIDs))
for _, t := range n.TrackIDs {
ids = append(ids, playlistID(t))
}
return ids
}
return nil
}

func (confPlaylists) HistoryFolderID() uint32 { return 0 }

func main() {
// stdout is reserved for the single readiness line; everything else (ours
// and the dbserver's) goes to stderr.
log.SetOutput(os.Stderr)

fixtureJSON := flag.String("fixture-json", "", "path to a tools/rekordbox_dump.py JSON dump to serve")
dataDir := flag.String("data-dir", "", "writable directory for per-run state")
secondColumn := flag.String("second-column", "bpm", "track-list detail (second) column")
flag.Parse()

if *fixtureJSON == "" {
fatalf("--fixture-json is required")
}

data, err := os.ReadFile(*fixtureJSON)
if err != nil {
fatalf("read fixture: %v", err)
}
var dump library.MasterDBDump
if err := json.Unmarshal(data, &dump); err != nil {
fatalf("parse fixture JSON %s: %v", *fixtureJSON, err)
}
lib := library.New()
if _, err := library.ImportMasterDBDump(lib, &dump); err != nil {
fatalf("import fixture: %v", err)
}
log.Printf("conformance: loaded %d tracks from %s", len(dump.Tracks), *fixtureJSON)

analysisStore := analysis.NewStore()
if *dataDir != "" {
analysisStore = analysis.NewStoreWithCache(filepath.Join(*dataDir, "analysis"))
}

ctx, cancel := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
defer cancel()

db := &dbserver.Server{
Library: lib,
DeviceNumber: 17, // rekordbox device number, matching the oracle
Analysis: analysisStore,
Menu: confMenu{second: *secondColumn},
Playlists: confPlaylists{nodes: dump.Playlists},
BindHost: "127.0.0.1", // loopback only
Ready: make(chan struct{}),
// LinkedFn left nil so the server accepts connections (no UNLINK gate).
}

errCh := make(chan error, 1)
go func() { errCh <- db.Start(ctx) }()

select {
case <-db.Ready:
case err := <-errCh:
fatalf("dbserver failed to start: %v", err)
}

// The harness client feeds query_port into its port-query (RemoteDBServer
// discovery) to learn the dynamic port, so query_port is the discovery port
// (12523), not the dynamic one. dynamic_port is included for diagnostics.
readiness, _ := json.Marshal(map[string]any{
"status": "ready",
"listen_address": "127.0.0.1",
"query_port": 12523,
"dynamic_port": int(db.QueryPort()),
})
fmt.Println(string(readiness))
os.Stdout.Sync()

if err := <-errCh; err != nil {
fatalf("dbserver: %v", err)
}
}

func fatalf(format string, args ...any) {
log.Printf(format, args...)
os.Exit(1)
}
Loading
Loading