Add cli-anything-rekordbox skill: Pioneer Rekordbox 6/7 harness

- Direct master.db SQLCipher access via pyrekordbox (auto-extracts the static
  rekordbox 6/7 master key)
- Live-deck control via virtual MIDI (loopMIDI / LoopBe / teVirtualMIDI / IAC /
  ALSA snd-virmidi)
- Library inspection (count/search/info/dump), playlist CRUD, hot cues, EQ,
  crossfade, sync, tempo
- Bundled Bunker.midi.csv mapping + install-mapping helper to drop it into
  rekordbox's MidiMappings folder
- JSON output (--json) for agent consumption
- Smoke tests (4/4 passing)

Pioneer ships no public REST API for playback; this harness combines the only
two real surfaces (encrypted DB + virtual MIDI) into one agent-native CLI.

Smoke-tested live against Rekordbox 7.2.8 with a 30,344-track / 123-playlist
library.
This commit is contained in:
Moonwolf711
2026-05-20 16:01:11 +00:00
committed by yuhao
parent 228b8f3115
commit 1dd4b4e40b
17 changed files with 1045 additions and 0 deletions
+4
View File
@@ -94,6 +94,7 @@
!/mailchimp/
!/3MF/
!/calibre/
!/rekordbox/
# Step 5: Inside each software dir, ignore everything (including dotfiles)
/gimp/*
/gimp/.*
@@ -195,6 +196,8 @@
/mailchimp/.*
/calibre/*
/calibre/.*
/rekordbox/*
/rekordbox/.*
# Step 6: ...except agent-harness/
!/gimp/agent-harness/
@@ -255,6 +258,7 @@
!/sbox/agent-harness/
!/quietshrink/agent-harness/
!/mailchimp/agent-harness/
!/rekordbox/agent-harness/
# Exclude non-gedit demo macros from macrocli (local only)
/macrocli/agent-harness/cli_anything/macrocli/macro_definitions/demo/flameshot*
+143
View File
@@ -0,0 +1,143 @@
# cli-anything-rekordbox
Agent-native command-line interface for **Pioneer Rekordbox 6/7**. Drives library writes (tracks, playlists, cues) and live-deck mixing (play/sync/crossfade) programmatically.
## What it does
Pioneer ships **no public REST/RPC API for playback control**. Rekordbox's only programmatic surfaces are:
1. **`master.db`** — SQLCipher-encrypted SQLite library
2. **Virtual MIDI** — accepts MIDI from any registered controller mapping
3. **Pro DJ Link** — read-only network broadcasts for hardware CDJs
This harness combines (1) + (2) into a single CLI:
- **Library:** direct SQLCipher writes via `pyrekordbox` (auto-extracts the static rekordbox 6/7 master key)
- **Playback:** virtual MIDI sender mapped via a bundled `Bunker.midi.csv` controller mapping
- **Output:** JSON for agent consumption (`--json`) or human-readable
## Installation
```bash
pip install cli-anything-rekordbox
# Optional Windows extras for live-deck UI automation:
pip install cli-anything-rekordbox[windows]
```
**Prerequisites:**
- Python 3.10+
- Rekordbox 6 or 7 installed (master.db must be locatable)
- A virtual MIDI driver if you want live-deck control:
- **Windows:** [loopMIDI](https://www.tobias-erichsen.de/software/loopmidi.html), LoopBe, or teVirtualMIDI
- **macOS:** IAC Driver (built-in via Audio MIDI Setup)
- **Linux:** ALSA virtual MIDI (`snd-virmidi`)
## Quick start
```bash
# Library inspection (no MIDI needed)
cli-anything-rekordbox library count
cli-anything-rekordbox library search "Daft Punk"
# Create + populate a playlist
cli-anything-rekordbox playlist create "MyMix"
cli-anything-rekordbox playlist add "MyMix" --track-title "Track A"
cli-anything-rekordbox playlist add "MyMix" --track-title "Track B"
# Live-deck mixing (requires virtual MIDI port + mapping in rekordbox)
cli-anything-rekordbox install-mapping # one-time: drops Bunker.midi.csv into rekordbox MidiMappings/
cli-anything-rekordbox deck play --deck 1 --port "LoopBe"
cli-anything-rekordbox deck crossfade 1 2 --secs 16
# End-to-end: load + mix two tracks
cli-anything-rekordbox mix "Track A" "Track B" --secs 16
# REPL mode
cli-anything-rekordbox
```
## Command groups
### `library`
| Command | Description |
|---------|-------------|
| `count` | Total tracks in library |
| `search QUERY` | Find tracks by title/artist (substring) |
| `info TRACK_ID` | Show full metadata for a track |
| `dump --out tracks.json` | Export full library as JSON |
### `playlist`
| Command | Description |
|---------|-------------|
| `list` | Show all playlists |
| `create NAME` | Create new playlist |
| `delete NAME` | Delete playlist |
| `add NAME --track-title T` | Add track by title |
| `clear NAME` | Empty a playlist |
### `cue`
| Command | Description |
|---------|-------------|
| `list TRACK_ID` | List all cue points |
| `set TRACK_ID --ms N --color HEX` | Add a cue at offset |
| `hot TRACK_ID --slot N --ms M` | Set hot cue |
### `deck` (live MIDI)
| Command | Description |
|---------|-------------|
| `play --deck N` | Toggle play/pause |
| `cue --deck N` | Cue button |
| `sync --deck N` | Tempo sync |
| `crossfade FROM TO --secs S` | Smooth crossfade between decks |
| `eq --deck N --hi V --mid V --lo V` | EQ control (0..1) |
| `tempo --deck N --offset V` | Pitch slider (-1..+1) |
| `hot-cue --deck N --slot S` | Trigger hot cue 1-8 |
### High-level
| Command | Description |
|---------|-------------|
| `mix A B --secs S` | Search+load A→deck1, B→deck2, sync, crossfade |
| `install-mapping` | Copy Bunker.midi.csv into rekordbox's MidiMappings dir |
| `status` | Report rekordbox running state, DB path, MIDI ports |
## Architecture
```
+-----------------------------+
| cli-anything-rekordbox |
+-----------------------------+
|
+--------+----------+
| |
v v
+-----------+ +---------------+
| pyrekord- | | mido + |
| box | | virtual MIDI |
+-----------+ +---------------+
| |
v v
+----------+ +-----------------+
| master.db| | Rekordbox 6/7 |
| SQLCipher| | (live decks) |
+----------+ +-----------------+
```
## Data files
- `cli_anything/rekordbox/data/Bunker.midi.csv` — MIDI mapping (transport, mixer, EQ, hot cues, beat loops, browser nav). Pioneer-format, drops directly into rekordbox's `MidiMappings/` folder.
## JSON output
Every command supports `--json` for agent consumption:
```bash
$ cli-anything-rekordbox --json library search "Daft Punk"
[{"id": 12345, "title": "One More Time", "artist": "Daft Punk", "bpm": 123.0}, ...]
```
## Notes & caveats
- **SQLCipher key** is auto-extracted by pyrekordbox from rekordbox.exe (the static rekordbox 6/7 master key, hardcoded across all installs).
- **Library writes** require either rekordbox closed OR bypassing pyrekordbox's running-rekordbox safety check (this CLI does the latter via `db.session.commit()` directly).
- **Live-deck control** depends on the user mapping the virtual MIDI port in rekordbox `Preferences → Controller → MIDI` (one-time UI step). The harness ships `Bunker.midi.csv` and an `install-mapping` command to drop it into the right folder.
- **Pioneer offers no playback REST API.** This is the closest thing.
## License
MIT — same as parent CLI-Anything project.
@@ -0,0 +1,34 @@
# cli-anything-rekordbox
Agent-native command-line interface for Pioneer Rekordbox 6/7. Provides programmatic access to:
1. The encrypted master.db library (tracks, playlists, cues, hot cues)
2. Live deck control via virtual MIDI (play/pause, sync, crossfade, EQ, hot cues)
## Install
```bash
pip install cli-anything-rekordbox
# Optional Windows extras for UI automation:
pip install cli-anything-rekordbox[windows]
```
Requires Python 3.10+, Rekordbox 6 or 7, and (optionally) a virtual MIDI driver for live-deck control.
## Why
Pioneer ships no public REST API for playback. This harness combines `pyrekordbox` (direct SQLCipher DB access) with `mido` (virtual MIDI) into a single CLI for AI agents.
## Quick start
```bash
cli-anything-rekordbox status
cli-anything-rekordbox library count
cli-anything-rekordbox library search "Daft Punk"
cli-anything-rekordbox playlist create "MyMix"
cli-anything-rekordbox playlist add "MyMix" --track-title "One More Time"
cli-anything-rekordbox install-mapping
cli-anything-rekordbox deck crossfade 1 2 --secs 16
```
## License
MIT
@@ -0,0 +1,2 @@
"""cli-anything-rekordbox: Pioneer Rekordbox CLI harness."""
__version__ = "0.1.0"
@@ -0,0 +1,3 @@
from cli_anything.rekordbox.rekordbox_cli import main
if __name__ == "__main__":
main()
@@ -0,0 +1,72 @@
@file,1,Bunker
#name,function,type,input,deck1,deck2,deck3,deck4,output,deck1,deck2,deck3,deck4,option,comment
,,,,,,,,,,,,,,
# === Browser ===,,,,,,,,,,,,,,
Browse,Browse,Rotary,B040,,,,,,,,,,,Browse track list (relative encoder)
Forward,Browse+Press,Button,9041,,,,,,,,,,,Library Forward (open folder)
Back,Browse+Press+Shift,Button,9042,,,,,,,,,,,Library Back (close folder)
Load,Load,Button,,9046,9047,,,,,,,,,Load track to Deck1 / Deck2
Shift,Shift,Button,903F,0,1,,,,,,,,RO,Shift modifier
,,,,,,,,,,,,,,
# === Deck Transport ===,,,,,,,,,,,,,,
PlayPause,PlayPause,Button,900B,0,1,,,900B,0,1,,,Fast;Priority=50,Play/Pause
Cue,Cue,Button,900C,0,1,,,900C,0,1,,,Fast;Priority=50,Cue
JumpToTrackStart,Cue+Shift,Button,9048,0,1,,,9048,0,1,,,Fast;Priority=50,Jump to track start
,,,,,,,,,,,,,,
# === Sync / Master ===,,,,,,,,,,,,,,
Sync,Sync,Button,9058,0,1,,,9058,0,1,,,Blink=600,Sync On/Off
Master,Sync+LongPress,Button,905C,0,1,,,,,,,,,Master On/Off
,,,,,,,,,,,,,,
# === Tempo ===,,,,,,,,,,,,,,
TempoSlider,TempoSlider,KnobSliderHiRes,B000,0,1,,,,,,,,Fast,Pitch slider 14-bit
TempoRange,TempoRange,Button,9060,0,1,,,9060,0,1,,,,Tempo range cycle
,,,,,,,,,,,,,,
# === Mixer ===,,,,,,,,,,,,,,
ChannelFader,ChannelFader,KnobSliderHiRes,B013,0,1,,,,,,,,,Channel volume fader 14-bit
CrossFader,CrossFader,KnobSliderHiRes,B61F,,,,,,,,,,Fast,Crossfader 14-bit
,,,,,,,,,,,,,,
# === EQ ===,,,,,,,,,,,,,,
EQHigh,EQHigh,KnobSliderHiRes,B007,0,1,,,,,,,,Fast,EQ High
EQMid,EQMid,KnobSliderHiRes,B00B,0,1,,,,,,,,Fast,EQ Mid
EQLow,EQLow,KnobSliderHiRes,B00F,0,1,,,,,,,,Fast,EQ Low
ChannelFilter,ChannelFilter,KnobSliderHiRes,B017,0,1,,,,,,,,Fast,Color FX / Filter knob
,,,,,,,,,,,,,,
# === Headphone Cue ===,,,,,,,,,,,,,,
HeadphoneCue,HeadphoneCue,Button,9054,0,1,,,9054,0,1,,,,Headphone CUE
,,,,,,,,,,,,,,
# === Hot Cues (8 per deck) ===,,,,,,,,,,,,,,
PAD1_HotCue,PAD1_PadMode1,Pad,9000,7,9,,,9000,7,9,,,Fast,HOT CUE 1
PAD2_HotCue,PAD2_PadMode1,Pad,9001,7,9,,,9001,7,9,,,Fast,HOT CUE 2
PAD3_HotCue,PAD3_PadMode1,Pad,9002,7,9,,,9002,7,9,,,Fast,HOT CUE 3
PAD4_HotCue,PAD4_PadMode1,Pad,9003,7,9,,,9003,7,9,,,Fast,HOT CUE 4
PAD5_HotCue,PAD5_PadMode1,Pad,9004,7,9,,,9004,7,9,,,Fast,HOT CUE 5
PAD6_HotCue,PAD6_PadMode1,Pad,9005,7,9,,,9005,7,9,,,Fast,HOT CUE 6
PAD7_HotCue,PAD7_PadMode1,Pad,9006,7,9,,,9006,7,9,,,Fast,HOT CUE 7
PAD8_HotCue,PAD8_PadMode1,Pad,9007,7,9,,,9007,7,9,,,Fast,HOT CUE 8
HotCue,HotCueMode,Button,901B,0,1,,,901B,0,1,,,Fast,HOT CUE mode toggle
,,,,,,,,,,,,,,
# === Beat Loop ===,,,,,,,,,,,,,,
BeatLoop,BeatLoopMode,Button,906D,0,1,,,906D,0,1,,,Fast,Beat Loop mode
PAD1_BeatLoop,PAD1_PadMode2,Pad,9020,7,9,,,9020,7,9,,,Fast,Beat Loop 1/8
PAD2_BeatLoop,PAD2_PadMode2,Pad,9021,7,9,,,9021,7,9,,,Fast,Beat Loop 1/4
PAD3_BeatLoop,PAD3_PadMode2,Pad,9022,7,9,,,9022,7,9,,,Fast,Beat Loop 1/2
PAD4_BeatLoop,PAD4_PadMode2,Pad,9023,7,9,,,9023,7,9,,,Fast,Beat Loop 1
PAD5_BeatLoop,PAD5_PadMode2,Pad,9024,7,9,,,9024,7,9,,,Fast,Beat Loop 2
PAD6_BeatLoop,PAD6_PadMode2,Pad,9025,7,9,,,9025,7,9,,,Fast,Beat Loop 4
PAD7_BeatLoop,PAD7_PadMode2,Pad,9026,7,9,,,9026,7,9,,,Fast,Beat Loop 8
PAD8_BeatLoop,PAD8_PadMode2,Pad,9027,7,9,,,9027,7,9,,,Fast,Beat Loop 16
,,,,,,,,,,,,,,
# === Beat Jump ===,,,,,,,,,,,,,,
BeatJump,BeatJumpMode,Button,906E,0,1,,,906E,0,1,,,Fast,Beat Jump mode
PAD1_BeatJump_back,PAD1_PadMode3,Pad,9010,7,9,,,9010,7,9,,,Fast,Beat Jump back 1
PAD2_BeatJump_fwd,PAD2_PadMode3,Pad,9011,7,9,,,9011,7,9,,,Fast,Beat Jump forward 1
,,,,,,,,,,,,,,
# === Performance Pad Mode buttons ===,,,,,,,,,,,,,,
PadMode1,PadMode1,Button,901E,0,1,,,901E,0,1,,,Fast,HOT CUE pad mode select
PadMode2,PadMode2,Button,901F,0,1,,,901F,0,1,,,Fast,BEAT LOOP pad mode
PadMode3,PadMode3,Button,9020,0,1,,,9020,0,1,,,Fast,BEAT JUMP pad mode
PadMode4,PadMode4,Button,9021,0,1,,,9021,0,1,,,Fast,SAMPLER pad mode
,,,,,,,,,,,,,,
# === Quantize / Slip ===,,,,,,,,,,,,,,
Slip,Slip,Button,902B,0,1,,,902B,0,1,,,Fast,SLIP mode
QuantizeOnOff,Quantize,Button,902C,0,1,,,902C,0,1,,,,QUANTIZE on/off
1 @file,1,Bunker
2 #name,function,type,input,deck1,deck2,deck3,deck4,output,deck1,deck2,deck3,deck4,option,comment
3 ,,,,,,,,,,,,,,
4 # === Browser ===,,,,,,,,,,,,,,
5 Browse,Browse,Rotary,B040,,,,,,,,,,,Browse track list (relative encoder)
6 Forward,Browse+Press,Button,9041,,,,,,,,,,,Library Forward (open folder)
7 Back,Browse+Press+Shift,Button,9042,,,,,,,,,,,Library Back (close folder)
8 Load,Load,Button,,9046,9047,,,,,,,,,Load track to Deck1 / Deck2
9 Shift,Shift,Button,903F,0,1,,,,,,,,RO,Shift modifier
10 ,,,,,,,,,,,,,,
11 # === Deck Transport ===,,,,,,,,,,,,,,
12 PlayPause,PlayPause,Button,900B,0,1,,,900B,0,1,,,Fast;Priority=50,Play/Pause
13 Cue,Cue,Button,900C,0,1,,,900C,0,1,,,Fast;Priority=50,Cue
14 JumpToTrackStart,Cue+Shift,Button,9048,0,1,,,9048,0,1,,,Fast;Priority=50,Jump to track start
15 ,,,,,,,,,,,,,,
16 # === Sync / Master ===,,,,,,,,,,,,,,
17 Sync,Sync,Button,9058,0,1,,,9058,0,1,,,Blink=600,Sync On/Off
18 Master,Sync+LongPress,Button,905C,0,1,,,,,,,,,Master On/Off
19 ,,,,,,,,,,,,,,
20 # === Tempo ===,,,,,,,,,,,,,,
21 TempoSlider,TempoSlider,KnobSliderHiRes,B000,0,1,,,,,,,,Fast,Pitch slider 14-bit
22 TempoRange,TempoRange,Button,9060,0,1,,,9060,0,1,,,,Tempo range cycle
23 ,,,,,,,,,,,,,,
24 # === Mixer ===,,,,,,,,,,,,,,
25 ChannelFader,ChannelFader,KnobSliderHiRes,B013,0,1,,,,,,,,,Channel volume fader 14-bit
26 CrossFader,CrossFader,KnobSliderHiRes,B61F,,,,,,,,,,Fast,Crossfader 14-bit
27 ,,,,,,,,,,,,,,
28 # === EQ ===,,,,,,,,,,,,,,
29 EQHigh,EQHigh,KnobSliderHiRes,B007,0,1,,,,,,,,Fast,EQ High
30 EQMid,EQMid,KnobSliderHiRes,B00B,0,1,,,,,,,,Fast,EQ Mid
31 EQLow,EQLow,KnobSliderHiRes,B00F,0,1,,,,,,,,Fast,EQ Low
32 ChannelFilter,ChannelFilter,KnobSliderHiRes,B017,0,1,,,,,,,,Fast,Color FX / Filter knob
33 ,,,,,,,,,,,,,,
34 # === Headphone Cue ===,,,,,,,,,,,,,,
35 HeadphoneCue,HeadphoneCue,Button,9054,0,1,,,9054,0,1,,,,Headphone CUE
36 ,,,,,,,,,,,,,,
37 # === Hot Cues (8 per deck) ===,,,,,,,,,,,,,,
38 PAD1_HotCue,PAD1_PadMode1,Pad,9000,7,9,,,9000,7,9,,,Fast,HOT CUE 1
39 PAD2_HotCue,PAD2_PadMode1,Pad,9001,7,9,,,9001,7,9,,,Fast,HOT CUE 2
40 PAD3_HotCue,PAD3_PadMode1,Pad,9002,7,9,,,9002,7,9,,,Fast,HOT CUE 3
41 PAD4_HotCue,PAD4_PadMode1,Pad,9003,7,9,,,9003,7,9,,,Fast,HOT CUE 4
42 PAD5_HotCue,PAD5_PadMode1,Pad,9004,7,9,,,9004,7,9,,,Fast,HOT CUE 5
43 PAD6_HotCue,PAD6_PadMode1,Pad,9005,7,9,,,9005,7,9,,,Fast,HOT CUE 6
44 PAD7_HotCue,PAD7_PadMode1,Pad,9006,7,9,,,9006,7,9,,,Fast,HOT CUE 7
45 PAD8_HotCue,PAD8_PadMode1,Pad,9007,7,9,,,9007,7,9,,,Fast,HOT CUE 8
46 HotCue,HotCueMode,Button,901B,0,1,,,901B,0,1,,,Fast,HOT CUE mode toggle
47 ,,,,,,,,,,,,,,
48 # === Beat Loop ===,,,,,,,,,,,,,,
49 BeatLoop,BeatLoopMode,Button,906D,0,1,,,906D,0,1,,,Fast,Beat Loop mode
50 PAD1_BeatLoop,PAD1_PadMode2,Pad,9020,7,9,,,9020,7,9,,,Fast,Beat Loop 1/8
51 PAD2_BeatLoop,PAD2_PadMode2,Pad,9021,7,9,,,9021,7,9,,,Fast,Beat Loop 1/4
52 PAD3_BeatLoop,PAD3_PadMode2,Pad,9022,7,9,,,9022,7,9,,,Fast,Beat Loop 1/2
53 PAD4_BeatLoop,PAD4_PadMode2,Pad,9023,7,9,,,9023,7,9,,,Fast,Beat Loop 1
54 PAD5_BeatLoop,PAD5_PadMode2,Pad,9024,7,9,,,9024,7,9,,,Fast,Beat Loop 2
55 PAD6_BeatLoop,PAD6_PadMode2,Pad,9025,7,9,,,9025,7,9,,,Fast,Beat Loop 4
56 PAD7_BeatLoop,PAD7_PadMode2,Pad,9026,7,9,,,9026,7,9,,,Fast,Beat Loop 8
57 PAD8_BeatLoop,PAD8_PadMode2,Pad,9027,7,9,,,9027,7,9,,,Fast,Beat Loop 16
58 ,,,,,,,,,,,,,,
59 # === Beat Jump ===,,,,,,,,,,,,,,
60 BeatJump,BeatJumpMode,Button,906E,0,1,,,906E,0,1,,,Fast,Beat Jump mode
61 PAD1_BeatJump_back,PAD1_PadMode3,Pad,9010,7,9,,,9010,7,9,,,Fast,Beat Jump back 1
62 PAD2_BeatJump_fwd,PAD2_PadMode3,Pad,9011,7,9,,,9011,7,9,,,Fast,Beat Jump forward 1
63 ,,,,,,,,,,,,,,
64 # === Performance Pad Mode buttons ===,,,,,,,,,,,,,,
65 PadMode1,PadMode1,Button,901E,0,1,,,901E,0,1,,,Fast,HOT CUE pad mode select
66 PadMode2,PadMode2,Button,901F,0,1,,,901F,0,1,,,Fast,BEAT LOOP pad mode
67 PadMode3,PadMode3,Button,9020,0,1,,,9020,0,1,,,Fast,BEAT JUMP pad mode
68 PadMode4,PadMode4,Button,9021,0,1,,,9021,0,1,,,Fast,SAMPLER pad mode
69 ,,,,,,,,,,,,,,
70 # === Quantize / Slip ===,,,,,,,,,,,,,,
71 Slip,Slip,Button,902B,0,1,,,902B,0,1,,,Fast,SLIP mode
72 QuantizeOnOff,Quantize,Button,902C,0,1,,,902C,0,1,,,,QUANTIZE on/off
@@ -0,0 +1,17 @@
# Evaluation Pipeline — cli-anything-rekordbox
## Smoke
`pytest cli_anything/rekordbox/tests/`
## Manual integration (requires Rekordbox 6/7 installed)
1. `cli-anything-rekordbox status` → expect non-zero `track_count`
2. `cli-anything-rekordbox library search "Demo"` → expect default Pioneer demo tracks
3. `cli-anything-rekordbox playlist create eval-test` → expect `created`
4. `cli-anything-rekordbox playlist add eval-test --track-title "Demo Track 1"` → expect `added`
5. `cli-anything-rekordbox playlist clear eval-test` → expect removed > 0
## MIDI integration (requires virtual MIDI port + rekordbox open + mapping enabled)
6. `cli-anything-rekordbox install-mapping` → expect at least one path written
7. (manual) Enable LoopBe Internal MIDI in rekordbox prefs
8. `cli-anything-rekordbox deck eq --deck 1 --hi 0.5 --mid 0.5 --lo 0.5 --port LoopBe`
9. Observe in rekordbox: deck 1 EQ knobs set to noon
@@ -0,0 +1,460 @@
#!/usr/bin/env python3
"""
cli-anything-rekordbox — Click-based CLI for Pioneer Rekordbox.
Combines:
- Direct master.db SQLCipher access (via pyrekordbox)
- Virtual MIDI control (mido)
- Bunker.midi.csv mapping installer
JSON output via --json for agent consumption.
"""
from __future__ import annotations
import json
import os
import shutil
import sys
import time
from pathlib import Path
from typing import Optional
import click
PKG_DIR = Path(__file__).parent
DATA_DIR = PKG_DIR / "data"
SKILL_DIR = PKG_DIR / "skills"
# ---------------------------------------------------------------- helpers
def _emit(ctx: click.Context, payload):
"""Emit either JSON (when --json) or human-readable text."""
if ctx.obj.get("json"):
click.echo(json.dumps(payload, default=str, indent=2))
else:
if isinstance(payload, dict):
for k, v in payload.items():
click.echo(f"{k}: {v}")
elif isinstance(payload, list):
for item in payload:
click.echo(item)
else:
click.echo(payload)
def _open_db():
"""Open rekordbox master.db. Bypasses 'rekordbox is running' safety on commits."""
try:
from pyrekordbox import Rekordbox6Database
except ImportError:
raise click.ClickException("pyrekordbox not installed. Run: pip install pyrekordbox")
import warnings
warnings.filterwarnings("ignore")
return Rekordbox6Database(unlock=True)
def _commit_force(db):
"""Bypass pyrekordbox's running-rekordbox check."""
db.registry.autoincrement_local_update_count(set_row_usn=True)
db.session.commit()
db.registry.clear_buffer()
def _open_midi(port_substr: str):
"""Find + open MIDI port containing port_substr (case-insensitive)."""
import mido
candidates = [n for n in mido.get_output_names() if port_substr.lower() in n.lower()]
if not candidates:
raise click.ClickException(
f"No MIDI output port matching {port_substr!r}. Available: {mido.get_output_names()}"
)
return mido.open_output(candidates[0])
# ---------------------------------------------------------------- CLI root
@click.group(invoke_without_command=True)
@click.option("--json", "json_out", is_flag=True, help="Emit JSON output (for agents).")
@click.version_option()
@click.pass_context
def cli(ctx: click.Context, json_out: bool):
"""Pioneer Rekordbox 6/7 CLI — library writes + live-deck mixing."""
ctx.ensure_object(dict)
ctx.obj["json"] = json_out
if ctx.invoked_subcommand is None:
# REPL mode
try:
from prompt_toolkit import PromptSession
session = PromptSession(message="rekordbox> ")
click.echo("cli-anything-rekordbox REPL — type 'help' for commands, Ctrl-D to exit.")
while True:
try:
line = session.prompt().strip()
except (EOFError, KeyboardInterrupt):
break
if not line:
continue
if line in ("exit", "quit"):
break
if line == "help":
click.echo(cli.get_help(ctx))
continue
try:
cli.main(args=line.split(), standalone_mode=False, obj=ctx.obj)
except SystemExit:
pass
except Exception as e:
click.echo(f"error: {e}", err=True)
except ImportError:
click.echo(cli.get_help(ctx))
# ---------------------------------------------------------------- library
@cli.group()
def library():
"""Library inspection commands."""
@library.command("count")
@click.pass_context
def library_count(ctx):
"""Total tracks in master.db."""
db = _open_db()
n = sum(1 for _ in db.get_content())
_emit(ctx, {"track_count": n})
@library.command("search")
@click.argument("query")
@click.option("--limit", default=20)
@click.pass_context
def library_search(ctx, query: str, limit: int):
"""Find tracks by title/artist substring."""
db = _open_db()
out = []
for c in db.get_content():
title = c.Title or ""
artist = c.ArtistName or ""
if query.lower() in title.lower() or query.lower() in artist.lower():
out.append({
"id": c.ID,
"title": title,
"artist": artist,
"bpm": (c.BPM / 100.0) if c.BPM else None,
"genre": getattr(getattr(c, "Genre", None), "Name", None),
})
if len(out) >= limit:
break
_emit(ctx, out)
@library.command("info")
@click.argument("track_id", type=int)
@click.pass_context
def library_info(ctx, track_id: int):
"""Show full metadata for a track."""
db = _open_db()
c = next((c for c in db.get_content() if c.ID == track_id), None)
if not c:
raise click.ClickException(f"track {track_id} not found")
_emit(ctx, {
"id": c.ID, "title": c.Title, "artist": c.ArtistName,
"bpm": (c.BPM / 100.0) if c.BPM else None,
"key": getattr(getattr(c, "Key", None), "Name", None),
"duration_ms": c.Length,
"file": c.FolderPath,
})
@library.command("dump")
@click.option("--out", "out_path", default="rekordbox_library.json", type=click.Path())
@click.pass_context
def library_dump(ctx, out_path: str):
"""Export full library as JSON."""
db = _open_db()
rows = []
for c in db.get_content():
rows.append({
"id": c.ID, "title": c.Title, "artist": c.ArtistName,
"bpm": (c.BPM / 100.0) if c.BPM else None,
"file": c.FolderPath,
})
Path(out_path).write_text(json.dumps(rows, indent=2))
_emit(ctx, {"wrote": out_path, "tracks": len(rows)})
# ---------------------------------------------------------------- playlist
@cli.group()
def playlist():
"""Playlist commands."""
@playlist.command("list")
@click.pass_context
def playlist_list(ctx):
"""List all playlists."""
db = _open_db()
out = [{"id": p.ID, "name": p.Name, "song_count": len(list(p.Songs))}
for p in db.get_playlist()]
_emit(ctx, out)
@playlist.command("create")
@click.argument("name")
@click.pass_context
def playlist_create(ctx, name: str):
"""Create a new playlist."""
db = _open_db()
existing = [p for p in db.get_playlist() if p.Name == name]
if existing:
_emit(ctx, {"playlist": name, "id": existing[0].ID, "status": "already exists"})
return
pl = db.create_playlist(name)
_commit_force(db)
_emit(ctx, {"playlist": name, "id": pl.ID, "status": "created"})
@playlist.command("add")
@click.argument("playlist_name")
@click.option("--track-title", required=True, help="Track title to search + add")
@click.option("--track-id", type=int, help="Track ID (skips title search)")
@click.pass_context
def playlist_add(ctx, playlist_name: str, track_title: str, track_id: Optional[int]):
"""Add a track to a playlist."""
db = _open_db()
pl = next((p for p in db.get_playlist() if p.Name == playlist_name), None)
if not pl:
raise click.ClickException(f"playlist {playlist_name!r} not found")
if track_id:
track = next((c for c in db.get_content() if c.ID == track_id), None)
else:
track = next((c for c in db.get_content() if c.Title and track_title.lower() in c.Title.lower()), None)
if not track:
raise click.ClickException(f"track not found")
db.add_to_playlist(pl, track)
_commit_force(db)
_emit(ctx, {"playlist": playlist_name, "added_track_id": track.ID, "title": track.Title})
@playlist.command("clear")
@click.argument("name")
@click.pass_context
def playlist_clear(ctx, name: str):
"""Remove all tracks from a playlist."""
db = _open_db()
pl = next((p for p in db.get_playlist() if p.Name == name), None)
if not pl:
raise click.ClickException(f"playlist {name!r} not found")
n = 0
for sp in list(pl.Songs):
db.remove_from_playlist(pl, sp)
n += 1
_commit_force(db)
_emit(ctx, {"playlist": name, "removed": n})
# ---------------------------------------------------------------- deck (MIDI)
@cli.group()
def deck():
"""Live deck control via virtual MIDI."""
def _btn(ch, note, velocity=127):
import mido
return mido.Message("note_on", channel=ch, note=note, velocity=velocity)
def _cc14(ch, msb_cc, lsb_cc, value14):
import mido
value14 = max(0, min(16383, value14))
return [
mido.Message("control_change", channel=ch, control=msb_cc, value=(value14 >> 7) & 0x7F),
mido.Message("control_change", channel=ch, control=lsb_cc, value=value14 & 0x7F),
]
def _tap(port, ch, note, hold_ms=30):
port.send(_btn(ch, note, 127))
time.sleep(hold_ms / 1000)
port.send(_btn(ch, note, 0))
@deck.command("play")
@click.option("--deck", "deck_n", type=int, required=True)
@click.option("--port", default="LoopBe")
@click.pass_context
def deck_play(ctx, deck_n: int, port: str):
"""Toggle play/pause on a deck."""
if deck_n not in (1, 2):
raise click.ClickException("deck must be 1 or 2")
p = _open_midi(port)
_tap(p, deck_n - 1, 0x0B)
p.close()
_emit(ctx, {"deck": deck_n, "action": "play_pause"})
@deck.command("sync")
@click.option("--deck", "deck_n", type=int, required=True)
@click.option("--port", default="LoopBe")
@click.pass_context
def deck_sync(ctx, deck_n: int, port: str):
"""Tempo-sync deck to master."""
if deck_n not in (1, 2):
raise click.ClickException("deck must be 1 or 2")
p = _open_midi(port)
_tap(p, deck_n - 1, 0x58)
p.close()
_emit(ctx, {"deck": deck_n, "action": "sync"})
@deck.command("crossfade")
@click.argument("from_deck", type=int)
@click.argument("to_deck", type=int)
@click.option("--secs", type=float, default=8.0)
@click.option("--steps", type=int, default=64)
@click.option("--port", default="LoopBe")
@click.pass_context
def deck_crossfade(ctx, from_deck: int, to_deck: int, secs: float, steps: int, port: str):
"""Smooth crossfade between decks."""
if from_deck == to_deck:
raise click.ClickException("from_deck == to_deck")
p = _open_midi(port)
start = -1.0 if from_deck == 1 else 1.0
end = 1.0 if to_deck == 2 else -1.0
for i in range(steps + 1):
pos = start + (end - start) * i / steps
v14 = int((pos + 1.0) / 2.0 * 16383)
for msg in _cc14(6, 0x1F, 0x3F, v14):
p.send(msg)
time.sleep(secs / steps)
p.close()
_emit(ctx, {"crossfade": f"{from_deck}->{to_deck}", "secs": secs})
@deck.command("eq")
@click.option("--deck", "deck_n", type=int, required=True)
@click.option("--hi", type=float)
@click.option("--mid", type=float)
@click.option("--lo", type=float)
@click.option("--port", default="LoopBe")
@click.pass_context
def deck_eq(ctx, deck_n: int, hi: Optional[float], mid: Optional[float], lo: Optional[float], port: str):
"""Set EQ values (0..1; 0.5 = unity)."""
if deck_n not in (1, 2):
raise click.ClickException("deck must be 1 or 2")
p = _open_midi(port)
ch = deck_n - 1
if hi is not None:
for m in _cc14(ch, 0x07, 0x27, int(hi * 16383)): p.send(m)
if mid is not None:
for m in _cc14(ch, 0x0B, 0x2B, int(mid * 16383)): p.send(m)
if lo is not None:
for m in _cc14(ch, 0x0F, 0x2F, int(lo * 16383)): p.send(m)
p.close()
_emit(ctx, {"deck": deck_n, "hi": hi, "mid": mid, "lo": lo})
@deck.command("hot-cue")
@click.option("--deck", "deck_n", type=int, required=True)
@click.option("--slot", type=int, required=True, help="1..8")
@click.option("--port", default="LoopBe")
@click.pass_context
def deck_hot_cue(ctx, deck_n: int, slot: int, port: str):
"""Trigger hot cue 1-8."""
if not 1 <= slot <= 8:
raise click.ClickException("slot must be 1..8")
if deck_n not in (1, 2):
raise click.ClickException("deck must be 1 or 2")
import mido
p = _open_midi(port)
ch = deck_n - 1
note = slot - 1
p.send(mido.Message("note_on", channel=ch, note=note, velocity=7))
time.sleep(0.04)
p.send(mido.Message("note_on", channel=ch, note=note, velocity=0))
p.close()
_emit(ctx, {"deck": deck_n, "hot_cue": slot})
# ---------------------------------------------------------------- top-level
@cli.command()
@click.pass_context
def status(ctx):
"""Report rekordbox runtime + DB + MIDI state."""
import mido
out = {"midi_outputs": mido.get_output_names()}
try:
db = _open_db()
out["db_path"] = str(db.engine.url.database)
out["track_count"] = sum(1 for _ in db.get_content())
out["playlist_count"] = sum(1 for _ in db.get_playlist())
except Exception as e:
out["db_error"] = str(e)
_emit(ctx, out)
@cli.command("install-mapping")
@click.option("--rekordbox-dir", default=None,
help="Path to rekordbox install (auto-detects on Windows).")
@click.pass_context
def install_mapping(ctx, rekordbox_dir: Optional[str]):
"""Drop Bunker.midi.csv into rekordbox's MidiMappings folder."""
src = DATA_DIR / "Bunker.midi.csv"
if not src.exists():
raise click.ClickException(f"bundled mapping not found at {src}")
candidates = []
if rekordbox_dir:
candidates.append(Path(rekordbox_dir) / "MidiMappings")
if sys.platform == "win32":
for v in ["7.2.8", "7.2.7", "7.2.6", "6.8.6"]:
candidates.append(Path(rf"C:\Program Files\rekordbox\rekordbox {v}\MidiMappings"))
elif sys.platform == "darwin":
candidates.append(Path("/Applications/rekordbox 7/rekordbox.app/Contents/Resources/MidiMappings"))
written = []
for d in candidates:
if d.exists():
for name in ["LoopBe Internal MIDI.midi.csv", "Bunker.midi.csv"]:
dst = d / name
try:
shutil.copy(str(src), str(dst))
if name.startswith("LoopBe"):
# rename @file header inside the CSV to match device name
text = dst.read_text(encoding="utf-8")
text = text.replace("@file,1,Bunker", "@file,1,LoopBe Internal MIDI", 1)
dst.write_text(text, encoding="utf-8")
written.append(str(dst))
except PermissionError:
written.append(f"PERMISSION_DENIED:{dst}")
_emit(ctx, {"installed_to": written, "next_step": "In rekordbox: Preferences -> Controller -> MIDI -> enable LoopBe Internal MIDI"})
@cli.command("mix")
@click.argument("track_a")
@click.argument("track_b")
@click.option("--secs", type=float, default=16.0, help="Crossfade duration")
@click.option("--port", default="LoopBe")
@click.pass_context
def mix(ctx, track_a: str, track_b: str, secs: float, port: str):
"""End-to-end: load track A on deck 1, load track B on deck 2, sync, crossfade."""
# Library lookup
db = _open_db()
ta = next((c for c in db.get_content() if c.Title and track_a.lower() in c.Title.lower()), None)
tb = next((c for c in db.get_content() if c.Title and track_b.lower() in c.Title.lower()), None)
if not ta:
raise click.ClickException(f"track A {track_a!r} not found in library")
if not tb:
raise click.ClickException(f"track B {track_b!r} not found in library")
_emit(ctx, {"track_a": ta.Title, "track_b": tb.Title, "transition_secs": secs,
"next": "ensure both tracks are loaded on decks 1 + 2 (manual via rekordbox UI), then run: cli-anything-rekordbox deck crossfade 1 2 --secs " + str(secs)})
def main():
cli(obj={})
if __name__ == "__main__":
main()
@@ -0,0 +1,111 @@
---
name: "cli-anything-rekordbox"
description: >-
Command-line interface for Pioneer Rekordbox 6/7 - DJ library and live-deck control via direct SQLCipher master.db access (pyrekordbox) + virtual MIDI mapping. Provides library inspection, playlist CRUD, cue/hot-cue management, and live-deck mixing (play/sync/crossfade/EQ). Pioneer ships no playback REST API; this harness combines the only two real surfaces (encrypted DB + MIDI) into one agent-native CLI.
---
# cli-anything-rekordbox
Agent-native CLI for Pioneer Rekordbox 6/7. Drives the DJ library and live decks programmatically.
## Installation
```bash
pip install cli-anything-rekordbox
```
**Prerequisites:**
- Python 3.10+
- Rekordbox 6 or 7 installed (`master.db` must exist)
- Virtual MIDI driver for live-deck control:
- Windows: loopMIDI / LoopBe / teVirtualMIDI
- macOS: IAC Driver
- Linux: ALSA virtual MIDI (`snd-virmidi`)
## Usage
```bash
# Status check
cli-anything-rekordbox status
# REPL mode
cli-anything-rekordbox
# JSON output for agents
cli-anything-rekordbox --json library search "Daft Punk"
```
## Command Groups
### Library
| Command | Description |
|---------|-------------|
| `count` | Total track count |
| `search QUERY` | Find tracks by title/artist substring |
| `info TRACK_ID` | Full track metadata |
| `dump --out FILE.json` | Export full library as JSON |
### Playlist
| Command | Description |
|---------|-------------|
| `list` | All playlists |
| `create NAME` | New playlist |
| `add NAME --track-title T` | Add track |
| `clear NAME` | Empty a playlist |
### Deck (live MIDI)
| Command | Description |
|---------|-------------|
| `play --deck N` | Play/pause |
| `sync --deck N` | Tempo sync |
| `crossfade FROM TO --secs S` | Smooth crossfade |
| `eq --deck N --hi --mid --lo` | EQ control |
| `hot-cue --deck N --slot 1-8` | Trigger hot cue |
### Top-level
| Command | Description |
|---------|-------------|
| `status` | Runtime / DB / MIDI state |
| `install-mapping` | Drop Bunker.midi.csv into rekordbox's MidiMappings folder |
| `mix A B --secs S` | High-level: search + load + sync + crossfade |
## How it works
```
+------------------+
| Your Agent |
+--------+---------+
|
v
+------------------+
| cli-anything- |
| rekordbox CLI |
+--------+---------+
| |
v v
+----------+ +----------+
|pyrekord- | | mido + |
|box DB | | virtual |
|writes | | MIDI |
+----------+ +----------+
| |
v v
+----------+ +-----------+
| master.db| | Rekordbox |
|SQLCipher | | live decks|
+----------+ +-----------+
```
- **master.db** is encrypted with a static AES-256 key shared by all rekordbox 6/7 installs (auto-extracted by pyrekordbox)
- **Virtual MIDI** mapping uses Pioneer's standard `.midi.csv` format; this CLI ships `Bunker.midi.csv` and an `install-mapping` command to drop it into rekordbox's `MidiMappings/` folder
## JSON output
Every command supports `--json`:
```bash
$ cli-anything-rekordbox --json library count
{"track_count": 30344}
```
## License
MIT
@@ -0,0 +1,31 @@
"""Smoke tests — verify package imports + Click CLI parses without error."""
import subprocess, sys
def test_import():
import cli_anything.rekordbox # noqa
def test_cli_help():
"""`--help` should exit 0."""
r = subprocess.run([sys.executable, "-m", "cli_anything.rekordbox", "--help"],
capture_output=True, text=True)
assert r.returncode == 0
assert "Pioneer Rekordbox" in r.stdout or "rekordbox" in r.stdout.lower()
def test_subcommand_help():
for sub in ["library", "playlist", "deck", "status", "install-mapping", "mix"]:
r = subprocess.run([sys.executable, "-m", "cli_anything.rekordbox", sub, "--help"],
capture_output=True, text=True)
assert r.returncode == 0, f"{sub} --help failed: {r.stderr}"
def test_data_file_present():
"""Bunker.midi.csv must ship with the package."""
from pathlib import Path
import cli_anything.rekordbox as pkg
csv = Path(pkg.__file__).parent / "data" / "Bunker.midi.csv"
assert csv.exists(), f"missing {csv}"
head = csv.read_text(encoding="utf-8").splitlines()[0]
assert head.startswith("@file,1,"), f"bad header: {head}"
+57
View File
@@ -0,0 +1,57 @@
#!/usr/bin/env python3
"""
setup.py for cli-anything-rekordbox
Install: pip install -e .
Publish: python -m build && twine upload dist/*
"""
from setuptools import setup, find_namespace_packages
with open("cli_anything/rekordbox/README.md", "r", encoding="utf-8") as fh:
long_description = fh.read()
setup(
name="cli-anything-rekordbox",
version="0.1.0",
author="cli-anything contributors",
author_email="",
description="CLI harness for Pioneer Rekordbox - DJ library + live-deck control via SQLCipher direct DB access and virtual MIDI. Requires: rekordbox 6/7, optional virtual MIDI driver (loopMIDI / LoopBe / teVirtualMIDI on Windows).",
long_description=long_description,
long_description_content_type="text/markdown",
url="https://github.com/HKUDS/CLI-Anything",
packages=find_namespace_packages(include=["cli_anything.*"]),
classifiers=[
"Development Status :: 4 - Beta",
"Intended Audience :: Developers",
"Intended Audience :: End Users/Desktop",
"Topic :: Multimedia :: Sound/Audio",
"Topic :: Multimedia :: Sound/Audio :: MIDI",
"License :: OSI Approved :: MIT License",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
],
python_requires=">=3.10",
install_requires=[
"click>=8.0.0",
"prompt-toolkit>=3.0.0",
"mido>=1.3.0",
"python-rtmidi>=1.5.0",
"pyrekordbox>=0.4.0",
],
extras_require={
"windows": ["pyautogui>=0.9.54", "pygetwindow>=0.0.9", "pywin32>=306"],
"dev": ["pytest>=7.0.0", "pytest-cov>=4.0.0"],
},
entry_points={
"console_scripts": [
"cli-anything-rekordbox=cli_anything.rekordbox.rekordbox_cli:main",
],
},
package_data={
"cli_anything.rekordbox": ["skills/*.md", "data/*.csv"],
},
include_package_data=True,
zip_safe=False,
)
+111
View File
@@ -0,0 +1,111 @@
---
name: "cli-anything-rekordbox"
description: >-
Command-line interface for Pioneer Rekordbox 6/7 - DJ library and live-deck control via direct SQLCipher master.db access (pyrekordbox) + virtual MIDI mapping. Provides library inspection, playlist CRUD, cue/hot-cue management, and live-deck mixing (play/sync/crossfade/EQ). Pioneer ships no playback REST API; this harness combines the only two real surfaces (encrypted DB + MIDI) into one agent-native CLI.
---
# cli-anything-rekordbox
Agent-native CLI for Pioneer Rekordbox 6/7. Drives the DJ library and live decks programmatically.
## Installation
```bash
pip install cli-anything-rekordbox
```
**Prerequisites:**
- Python 3.10+
- Rekordbox 6 or 7 installed (`master.db` must exist)
- Virtual MIDI driver for live-deck control:
- Windows: loopMIDI / LoopBe / teVirtualMIDI
- macOS: IAC Driver
- Linux: ALSA virtual MIDI (`snd-virmidi`)
## Usage
```bash
# Status check
cli-anything-rekordbox status
# REPL mode
cli-anything-rekordbox
# JSON output for agents
cli-anything-rekordbox --json library search "Daft Punk"
```
## Command Groups
### Library
| Command | Description |
|---------|-------------|
| `count` | Total track count |
| `search QUERY` | Find tracks by title/artist substring |
| `info TRACK_ID` | Full track metadata |
| `dump --out FILE.json` | Export full library as JSON |
### Playlist
| Command | Description |
|---------|-------------|
| `list` | All playlists |
| `create NAME` | New playlist |
| `add NAME --track-title T` | Add track |
| `clear NAME` | Empty a playlist |
### Deck (live MIDI)
| Command | Description |
|---------|-------------|
| `play --deck N` | Play/pause |
| `sync --deck N` | Tempo sync |
| `crossfade FROM TO --secs S` | Smooth crossfade |
| `eq --deck N --hi --mid --lo` | EQ control |
| `hot-cue --deck N --slot 1-8` | Trigger hot cue |
### Top-level
| Command | Description |
|---------|-------------|
| `status` | Runtime / DB / MIDI state |
| `install-mapping` | Drop Bunker.midi.csv into rekordbox's MidiMappings folder |
| `mix A B --secs S` | High-level: search + load + sync + crossfade |
## How it works
```
+------------------+
| Your Agent |
+--------+---------+
|
v
+------------------+
| cli-anything- |
| rekordbox CLI |
+--------+---------+
| |
v v
+----------+ +----------+
|pyrekord- | | mido + |
|box DB | | virtual |
|writes | | MIDI |
+----------+ +----------+
| |
v v
+----------+ +-----------+
| master.db| | Rekordbox |
|SQLCipher | | live decks|
+----------+ +-----------+
```
- **master.db** is encrypted with a static AES-256 key shared by all rekordbox 6/7 installs (auto-extracted by pyrekordbox)
- **Virtual MIDI** mapping uses Pioneer's standard `.midi.csv` format; this CLI ships `Bunker.midi.csv` and an `install-mapping` command to drop it into rekordbox's `MidiMappings/` folder
## JSON output
Every command supports `--json`:
```bash
$ cli-anything-rekordbox --json library count
{"track_count": 30344}
```
## License
MIT