From 1dd4b4e40b387add8504156c7843c5a41ae7e69b Mon Sep 17 00:00:00 2001 From: Moonwolf711 <123460854+Moonwolf711@users.noreply.github.com> Date: Sat, 25 Apr 2026 22:11:35 -0600 Subject: [PATCH] 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. --- .gitignore | 4 + rekordbox/agent-harness/REKORDBOX.md | 143 ++++++ .../agent-harness/cli_anything/__init__.py | 0 .../cli_anything/rekordbox/README.md | 34 ++ .../cli_anything/rekordbox/__init__.py | 2 + .../cli_anything/rekordbox/__main__.py | 3 + .../cli_anything/rekordbox/core/__init__.py | 0 .../rekordbox/data/Bunker.midi.csv | 72 +++ .../cli_anything/rekordbox/eval/__init__.py | 0 .../rekordbox/eval/eval_pipeline.md | 17 + .../cli_anything/rekordbox/rekordbox_cli.py | 460 ++++++++++++++++++ .../cli_anything/rekordbox/skills/SKILL.md | 111 +++++ .../cli_anything/rekordbox/tests/__init__.py | 0 .../rekordbox/tests/test_smoke.py | 31 ++ .../cli_anything/rekordbox/utils/__init__.py | 0 rekordbox/agent-harness/setup.py | 57 +++ skills/cli-anything-rekordbox/SKILL.md | 111 +++++ 17 files changed, 1045 insertions(+) create mode 100644 rekordbox/agent-harness/REKORDBOX.md create mode 100644 rekordbox/agent-harness/cli_anything/__init__.py create mode 100644 rekordbox/agent-harness/cli_anything/rekordbox/README.md create mode 100644 rekordbox/agent-harness/cli_anything/rekordbox/__init__.py create mode 100644 rekordbox/agent-harness/cli_anything/rekordbox/__main__.py create mode 100644 rekordbox/agent-harness/cli_anything/rekordbox/core/__init__.py create mode 100644 rekordbox/agent-harness/cli_anything/rekordbox/data/Bunker.midi.csv create mode 100644 rekordbox/agent-harness/cli_anything/rekordbox/eval/__init__.py create mode 100644 rekordbox/agent-harness/cli_anything/rekordbox/eval/eval_pipeline.md create mode 100644 rekordbox/agent-harness/cli_anything/rekordbox/rekordbox_cli.py create mode 100644 rekordbox/agent-harness/cli_anything/rekordbox/skills/SKILL.md create mode 100644 rekordbox/agent-harness/cli_anything/rekordbox/tests/__init__.py create mode 100644 rekordbox/agent-harness/cli_anything/rekordbox/tests/test_smoke.py create mode 100644 rekordbox/agent-harness/cli_anything/rekordbox/utils/__init__.py create mode 100644 rekordbox/agent-harness/setup.py create mode 100644 skills/cli-anything-rekordbox/SKILL.md diff --git a/.gitignore b/.gitignore index cef559d80..73b94cbd9 100644 --- a/.gitignore +++ b/.gitignore @@ -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* diff --git a/rekordbox/agent-harness/REKORDBOX.md b/rekordbox/agent-harness/REKORDBOX.md new file mode 100644 index 000000000..ed9af0575 --- /dev/null +++ b/rekordbox/agent-harness/REKORDBOX.md @@ -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. diff --git a/rekordbox/agent-harness/cli_anything/__init__.py b/rekordbox/agent-harness/cli_anything/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/rekordbox/agent-harness/cli_anything/rekordbox/README.md b/rekordbox/agent-harness/cli_anything/rekordbox/README.md new file mode 100644 index 000000000..de750ef6f --- /dev/null +++ b/rekordbox/agent-harness/cli_anything/rekordbox/README.md @@ -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 diff --git a/rekordbox/agent-harness/cli_anything/rekordbox/__init__.py b/rekordbox/agent-harness/cli_anything/rekordbox/__init__.py new file mode 100644 index 000000000..7ddfac7e7 --- /dev/null +++ b/rekordbox/agent-harness/cli_anything/rekordbox/__init__.py @@ -0,0 +1,2 @@ +"""cli-anything-rekordbox: Pioneer Rekordbox CLI harness.""" +__version__ = "0.1.0" diff --git a/rekordbox/agent-harness/cli_anything/rekordbox/__main__.py b/rekordbox/agent-harness/cli_anything/rekordbox/__main__.py new file mode 100644 index 000000000..d7a8a3ac0 --- /dev/null +++ b/rekordbox/agent-harness/cli_anything/rekordbox/__main__.py @@ -0,0 +1,3 @@ +from cli_anything.rekordbox.rekordbox_cli import main +if __name__ == "__main__": + main() diff --git a/rekordbox/agent-harness/cli_anything/rekordbox/core/__init__.py b/rekordbox/agent-harness/cli_anything/rekordbox/core/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/rekordbox/agent-harness/cli_anything/rekordbox/data/Bunker.midi.csv b/rekordbox/agent-harness/cli_anything/rekordbox/data/Bunker.midi.csv new file mode 100644 index 000000000..55b70b400 --- /dev/null +++ b/rekordbox/agent-harness/cli_anything/rekordbox/data/Bunker.midi.csv @@ -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 diff --git a/rekordbox/agent-harness/cli_anything/rekordbox/eval/__init__.py b/rekordbox/agent-harness/cli_anything/rekordbox/eval/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/rekordbox/agent-harness/cli_anything/rekordbox/eval/eval_pipeline.md b/rekordbox/agent-harness/cli_anything/rekordbox/eval/eval_pipeline.md new file mode 100644 index 000000000..674e60b01 --- /dev/null +++ b/rekordbox/agent-harness/cli_anything/rekordbox/eval/eval_pipeline.md @@ -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 diff --git a/rekordbox/agent-harness/cli_anything/rekordbox/rekordbox_cli.py b/rekordbox/agent-harness/cli_anything/rekordbox/rekordbox_cli.py new file mode 100644 index 000000000..c8f1d6cbe --- /dev/null +++ b/rekordbox/agent-harness/cli_anything/rekordbox/rekordbox_cli.py @@ -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() diff --git a/rekordbox/agent-harness/cli_anything/rekordbox/skills/SKILL.md b/rekordbox/agent-harness/cli_anything/rekordbox/skills/SKILL.md new file mode 100644 index 000000000..49a1acca1 --- /dev/null +++ b/rekordbox/agent-harness/cli_anything/rekordbox/skills/SKILL.md @@ -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 diff --git a/rekordbox/agent-harness/cli_anything/rekordbox/tests/__init__.py b/rekordbox/agent-harness/cli_anything/rekordbox/tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/rekordbox/agent-harness/cli_anything/rekordbox/tests/test_smoke.py b/rekordbox/agent-harness/cli_anything/rekordbox/tests/test_smoke.py new file mode 100644 index 000000000..f682df33b --- /dev/null +++ b/rekordbox/agent-harness/cli_anything/rekordbox/tests/test_smoke.py @@ -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}" diff --git a/rekordbox/agent-harness/cli_anything/rekordbox/utils/__init__.py b/rekordbox/agent-harness/cli_anything/rekordbox/utils/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/rekordbox/agent-harness/setup.py b/rekordbox/agent-harness/setup.py new file mode 100644 index 000000000..56ad75b05 --- /dev/null +++ b/rekordbox/agent-harness/setup.py @@ -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, +) diff --git a/skills/cli-anything-rekordbox/SKILL.md b/skills/cli-anything-rekordbox/SKILL.md new file mode 100644 index 000000000..49a1acca1 --- /dev/null +++ b/skills/cli-anything-rekordbox/SKILL.md @@ -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