# Melody Binary Serving (Plain HTTP) ## Why this exists ESP32 devices download `.bsm` melody files directly, using a URL supplied through the Android app. The download previously pointed at a Firebase Storage public URL (HTTPS only). ESP32's TLS client needs 40KB+ of RAM to open an HTTPS connection, which these devices can't reliably spare. To fix this, melody `.bsm` binaries are now stored and served by our own backend over plain HTTP, instead of Firebase Storage. This does **not** affect the audio preview file (`information.previewURL`) — that's only ever fetched by the browser admin UI, which is already HTTPS, so it stays on Firebase Storage unchanged. ## PID vs UID — why storage is keyed on pid, not uid - **`pid`** ("Playback ID") identifies the underlying **archetype binary** — the raw note sequence (A, B, C...) baked into a `.bsm` file. Multiple melodies can legitimately share one `pid`: each melody remaps those notes to actual bells via its own `noteAssignments`, speed, and duration settings. One binary can back many different "remix" melodies. **Duplicate PIDs across melodies are correct and expected, not a bug.** - **`uid`** is meant to be each melody's own unique database identity. It is *not* currently populated anywhere in `MelodyForm.jsx` — this is a pre-existing, separate bug, deferred as its own task. Because of this, local binary storage is keyed on `pid`, not `uid`. ## What changed - **Storage**: `.bsm` binaries are written to `./data/melody_binaries/{pid}.bsm` on the host (mounted into the backend container at `/app/storage/melody_binaries`, same pattern as `./data/firmware` and `./data/built_melodies`). Because storage is keyed on `pid`, multiple melodies sharing a `pid` share one file — saving from any of them overwrites the same file, which is expected. - **Upload path**: `SelectArchetypeModal` / `BuildOnTheFlyModal` still call `POST /api/melodies/{melodyId}/upload/binary` exactly as before. The backend (`backend/melodies/service.py::save_binary_for_melody`) now writes the bytes to local disk instead of uploading to Firebase Storage, and returns a URL like `http://melodies.bellsystems.net/download/{pid}`. The melody must have a `pid` set before uploading a binary (the endpoint returns 400 if not). - **Stored URL**: the melody's `url` field (Firestore + SQLite) now holds that plain-HTTP URL instead of a Firebase public URL. No schema change — `url` was already a plain string field. - **Download route**: `GET /api/melodies/download/{pid}` (`backend/melodies/router.py`) is unauthenticated (devices have no login token) and resolves `pid` directly to `{pid}.bsm` on disk, same pattern as the existing firmware download route (`backend/firmware/router.py::download_firmware`). - **Deletion is share-aware**: deleting a melody or its binary (`service.delete_file` / `service._delete_storage_files`) only removes the local `.bsm` file if no *other* melody still references the same `pid` (`service._pid_used_by_other_melody`). This prevents one melody's deletion from breaking playback for other melodies sharing its archetype binary. ## Migrating existing melodies Melodies created before this change still have a Firebase Storage URL in their `url` field — deploying this code does not touch existing data. They keep working exactly as before (still HTTPS to Firebase) until migrated. Two ways to move a melody to the new plain-HTTP URL: 1. **Per-melody, via the UI**: open the melody and re-run "Select Archetype" or "Build on the Fly" — this naturally re-uploads through the same endpoint, which now writes to local disk and updates `url`. 2. **Bulk, via script**: `backend/scripts/migrate_melody_binaries_to_local.py` downloads each melody's current Firebase binary and writes it to local disk under its `pid`, then updates `url` (SQLite + Firestore if published). Run with `--dry-run` first: ``` docker exec -it bellsystems-backend python scripts/migrate_melody_binaries_to_local.py --dry-run docker exec -it bellsystems-backend python scripts/migrate_melody_binaries_to_local.py ``` **Known caveat**: some existing melodies share a `pid` despite their Firebase URLs pointing at *different* source files (observed on real data — e.g. voice-count variant filenames like `1N_`/`2N_`/`3N_`/`4N_` all filed under one `pid`). The script downloads only the first melody's file for each `pid` and reuses it for every other melody sharing that `pid` — it does not attempt to detect which file is "correct". It prints a warning list of every `pid` where this happened; use each melody's playback button afterward to verify the right archetype plays, and manually re-assign the correct one via "Select Archetype" if it's wrong. ## Infrastructure (outside this repo) The plain-HTTP requirement is handled by keeping `melodies.bellsystems.net` on a **separate** hostname from `console.bellsystems.net`, so the console's NPM proxy host can stay HTTPS-only with no per-path exceptions. Required, one-time setup outside this repository: 1. **DNS**: add an A/CNAME record for `melodies.bellsystems.net` pointing at the same host as `console.bellsystems.net`. 2. **NPM (Nginx Proxy Manager) proxy host**: create a new proxy host for `melodies.bellsystems.net` forwarding to the `nginx` container's exposed port (`90` per `docker-compose.yml`). **Do not force SSL / do not enable the "Force SSL" redirect** on this proxy host — it must remain reachable over plain HTTP. The in-repo `nginx/nginx.conf` already has a dedicated `server_name melodies.bellsystems.net` block that maps `/download/{pid}` to the backend's `/api/melodies/download/{pid}` route, so no further nginx changes are needed once the NPM proxy host exists. ## Verification 1. Build/select an archetype for a melody with a `pid` set → confirm a file appears at `./data/melody_binaries/{pid}.bsm` and the melody's `url` becomes `http://melodies.bellsystems.net/download/{pid}`. 2. `curl http://localhost:8000/api/melodies/download/{pid}` (direct to backend, bypassing nginx) → returns the `.bsm` bytes, no auth header needed. 3. `curl http://localhost:90/api/melodies/download/{pid}` (through nginx on the console server block) → same result. 4. Once NPM is configured, `curl http://melodies.bellsystems.net/download/{pid}` → same result, over plain HTTP. 5. Delete a melody whose `pid` is *not* shared with any other melody → confirm the local `.bsm` file is removed. Delete one of two melodies sharing a `pid` → confirm the file survives until the last one is deleted. 6. Upload a preview audio file → confirm it still lands in Firebase Storage and `previewURL` still populates (regression check).