rekordbox-bass-notes

Distribution Plan — rekordbox bass notes for macOS

Turning the POC into a tool a handful of DJ friends can install and use on their own Macs, against their own Rekordbox libraries.

See PLAN.md for the app itself and README.md for dev setup. This document is only about packaging and shipping.


Phase 0 — parallel-safe prep — ✅ complete

Backend-only / additive hardening done without disturbing the npm run dev + uvicorn --reload loop, so UI tweaking could continue alongside. Decisions locked: app name rekordbox bass notes; soundfile-only decoding for now (M4A/AAC/ALAC → a clear “not supported yet” error, ffmpeg in M4); both backup endpoints added.

# Item Touches Status
0.1 Drop librosa → soundfile decode + scipy.signal.resample_poly to AUDIO_SR. Removed audio_res_type. Re-ran calibrate.py: 1 of ~117 tracks shifts one digit at a boundary → dbfs_scale unchanged, PRESET_LETTER=B. AudioDecodeError for unsupported/corrupt; M4A/AAC/ALAC → “not supported yet”. audioread DeprecationWarning noise gone. analysis.py, config.py, requirements.txt ✅
0.2 Runtime config — backend/runtime.py: RuntimeConfig {db_path, backup_dir} persisted to JSON (~/Library/Application Support/rekordbox bass notes/config.json frozen, ./.rkbx-config.json gitignored in dev). Precedence env → file → default; default db_path = “” (auto-locate the real library). /api/db/switch writes it (sticky across restart — verified). db.backup() / _prune_backups / restore.py read backup_dir from it. runtime.py (new), db.py, restore.py, main.py ✅
0.3 Human error messages — humanize() / _http() in main.py: library-not-found, Rekordbox open, audio moved, decode/format failure, unsupported-DB-version → plain sentences in detail, applied to the analyze / batch-stream / write / switch paths. No UI change. Verified. main.py ✅
0.4 Backup endpoints — GET /api/backups (listing + RekordboxDB.stats() for the live DB) and POST /api/backups/{name}/restore (409 if Rekordbox open → snapshot → restore.apply_restore → reopen, behind _swap_lock). restore.py refactored: resolve_backup() (raises, no SystemExit) + apply_restore() shared with the CLI. Full HTTP round-trip not run (Rekordbox was open — guard verified; mechanics covered by the CLI). Restore panel is UI work. main.py, restore.py, db.py ✅
0.5 Additive static mount — app.mount("/", StaticFiles(frontend/dist, html=True)) last, only when frontend/dist/index.html exists (sys._MEIPASS-aware). Verified: skipped in dev; serves the SPA at / with /api/* still winning when a build is present. npm run dev unaffected. main.py ✅
0.6 Doc sync — PLAN.md + README (librosa/ffmpeg, runtime.py, humanize, /api/backups, /api/db/switch persistence, config.json precedence). PLAN.md, README.md ✅

Also since done, outside the numbered list: the sample/master.db and every code reference to it were removed — the app now defaults to the real library.


Current state (dev)

Runs as two dev processes (uvicorn --reload + npm run dev). Already built toward distribution:

Known rough edges


Phase 1 — packaging build-out — 🟡 mostly done

Added since Phase 0 (see the per-item ✅s below for detail):

Still open: a GUI-window pass, .dmg polish, RB v5 path check, clean-machine field test, signing.


Goal

Target artifact

  v1 (first share) v2 (if it sticks)
Form .app inside a .dmg same
Signing unsigned (right-click → Open once) Developer ID signed + notarized
Arch arm64 (build a separate x86_64 only if a friend needs it) same, possibly universal2
Channel private GitHub Releases same
Update in-app “new version available” banner → link same, or Sparkle after signing

Non-goals


What’s left for a shippable v1

Each subsection is flagged: ✅ done · 🟡 partly done · ⬜ not started.

1. One process — ✅

2. Launcher — ✅

launcher.py (PyInstaller entry point):

  1. ✅ file logging via backend/logging_setup.py; single-instance lock (fcntl.flock on …/Application Support/rekordbox bass notes/app.lock).
  2. ✅ picks a free 127.0.0.1 port (pre-bind → read back → hand to uvicorn).
  3. ✅ uvicorn runs in a daemon thread; wait_until_ready() polls /api/health.
  4. ✅ pywebview window at http://127.0.0.1:<port>; webbrowser.open fallback if no GUI backend. RKBX_NO_WINDOW=1 runs it headless (smoke tests).
  5. ✅ window close → server.should_exit → join → exit; lock released on exit.

3. Dependency slimming — ✅

Runtime deps: numpy, scipy, soundfile, pyrekordbox (pinned ==0.4.4), sqlcipher3-wheels, fastapi, uvicorn, psutil, pywebview, imageio-ffmpeg.

4. Config & writable paths — ✅

5. First-run library setup — 🟡

6. Restore in the UI — ✅

7. Error surfacing — ✅

humanize() in main.py maps library-not-found, Rekordbox-open, audio-moved, decode/format failure, and unsupported-DB-version to plain sentences in the API detail; applied to analyze, batch-stream (per track), both write paths, the DB switch, and restore. Frontend renders detail as-is. (Raw detail → logs once §4 logging lands.)

8. Rekordbox-running UX — ✅

9. App polish — ✅ (icon is a placeholder)

10. In-app update check — ✅ (needs a repo configured)


Build pipeline

PyInstaller spec

rekordbox bass notes.spec is the real thing (checked in). It reads the version from backend/__init__.py, refuses to build if frontend/dist/index.html is missing, collect_alls pyrekordbox / soundfile / imageio_ffmpeg / scipy / numpy, collect_dynamic_libs for sqlcipher3, adds the uvicorn protocol/lifespan hidden imports, and emits a .app with the icon + an Info.plist (CFBundleName, version, LSMinimumSystemVersion 12.0, NSAllowsLocalNetworking). The Mach-O executable is named bass-notes (EXE_NAME), not rekordbox bass notes, so the running process can’t be confused with Rekordbox by a name match; CFBundleName and the window title are unchanged.

.venv/bin/pyinstaller "rekordbox bass notes.spec" --noconfirm

Status: a full --target-arch arm64 build succeeds and was smoke-tested headless (RKBX_NO_WINDOW=1): opens the real encrypted master.db (sqlcipher3 + pyrekordbox key blob resolve frozen), serves the SPA at /, and analyzes a real MP3 end-to-end (B:l3m9h7). Bundle ~213 MB. Not yet run through a GUI window, create-dmg, or signing.

Known gotchas for this stack:

Expected bundle

Reproducible build


Code signing & notarization (Phase 2)

Only when v1 has proven useful. One-time setup, then scripted.

  1. Apple Developer Program ($99/yr) → Developer ID Application certificate.
  2. codesign --deep --force --options runtime --timestamp --sign "Developer ID Application: <name> (<team>)" "rekordbox bass notes.app"
    • hardened runtime; entitlements file allowing the JIT-free basics. A non-sandboxed Developer ID app can read ~/Library/Pioneer/… without security-scoped bookmarks.
  3. Zip → `xcrun notarytool submit –wait –apple-id … –team-id … –password ` → `xcrun stapler staple "rekordbox bass notes.app"`.
  4. Sign the .dmg too, staple it.

Result: friends double-click, no Gatekeeper dialog. Enables Sparkle-based auto-update later.


Testing

Before every release

Dimension Cases
Arch arm64 (Apple Silicon); Intel if shipping that build
macOS oldest supported + latest
Rekordbox one 6.x, one 7.x; ideally a friend’s actual install
Library small (<100) and large (10k+) tracks; streaming-only entries present
Files local MP3, FLAC, AIFF, WAV; a relocated/offline file; M4A/AAC (once M1’s ffmpeg lands — until then expect the “not supported” message)
State Rekordbox running → refusal; Rekordbox closed → write + reopen shows the comment
Restore write, restore prior backup, confirm revert + quick_check ok
First run no config.json → detect → pick → analyze → save

Clean-machine test

Run the .dmg on a Mac (or fresh user account) that has never had Python / Homebrew / this repo. That’s the only real proof it’s self-contained. Ideally do this on one trusted friend’s machine before sending to the rest.


Release & updates


Risks & support plan

Risk Mitigation / message to friends
Rekordbox schema/key changes in an update Pin pyrekordbox; test against a friend’s install first; “don’t update Rekordbox until I confirm.” Ship a new build when pyrekordbox catches up.
pyrekordbox writes are “experimental” Backup before every write + one-click restore + post-write quick_check. Say so plainly in the first-run screen.
User writes while Rekordbox is open Two independent guards already; plus the banner.
Rekordbox cloud/library sync races the DB First-run note: “pause Rekordbox sync while using this.”
Backup folder fills the disk BACKUP_KEEP default 20; show total size in the Backups panel.
Unsigned-app confusion README with the exact right-click→Open / xattr steps and a screenshot.
A friend on Intel Ask first; ship an x86_64 build or tell them to wait.
Something breaks in the field Logs at ~/Library/Logs/rekordbox bass notes/; an in-app “Copy diagnostics” button; you fix and re-release.

Set expectations explicitly: this is a personal tool shared among friends, not a supported product; it can break when Rekordbox updates.


Milestones & effort (remaining)

Phase 0 already covered the librosa drop, runtime config, human errors, the backup endpoints, and the static mount.

# Milestone Status Left to do
M0 One process ✅ launcher.py — pywebview + webbrowser fallback, free 127.0.0.1 port, uvicorn thread, single-instance flock, graceful shutdown
M1 Deps ✅ ffmpeg fallback decode via imageio-ffmpeg; requirements.txt pinned
M2 Loose ends 🟡 ✅ file logging + /api/diagnostics + Copy-diagnostics button; ✅ native-dialog bridge (/api/pick-file, BrowseButton) — untested in a real window; ⬜ RB v5 path variant
M3 UI panels ✅ restore panel, Rekordbox banner + 5 s health polling, no-library screen, version footer, 70/30 layout, analysis cache, batch accordion, audio player + EQ
M4 Package 🟡 ✅ .icns (placeholder), ✅ rekordbox bass notes.spec — arm64 build succeeds, headless smoke test passes, ✅ scripts/build_app.sh; ⬜ run through a GUI window; ⬜ create-dmg polish; ⬜ clean-machine .dmg check
M5 Field test ⬜ clean-machine / friend’s-Mac run; fix what breaks; release to 1–2 people
M6 Ship 🟡 ✅ update-check endpoint + UpdateBanner + last_seen_version; ⬜ set UPDATE_REPO, cut the first tagged release
M7 (optional) Signing ⬜ Developer ID sign + notarize + staple (.app and .dmg)

Remaining to a shareable v1: a GUI-window pass on the build, .dmg packaging polish, the RB v5 path check, and a clean-machine field test (M5).


Execution notes (model & reasoning)

Phase Model Reasoning
M0, M1, M2, M3, M6 Sonnet 5 think
M4 (PyInstaller — trial and error, read tracebacks) Sonnet 5 think, iterate against real pyinstaller runs
M5 (field debugging) Sonnet 5 think
M7 (signing) Sonnet 5 think; follow Apple’s current docs, don’t rely on memorized commands

(M1’s DSP-correctness think hard item — the librosa→scipy resampler swap — is done, in Phase 0.1.)