mirror of
https://github.com/HKUDS/CLI-Anything.git
synced 2026-09-21 04:45:54 +08:00
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:
@@ -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*
|
||||
|
||||
@@ -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
|
||||
|
@@ -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}"
|
||||
@@ -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,
|
||||
)
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user