A personal page to receive song gifts, integrated with ListenBrainz and MusicBrainz. Think “buy me a coffee”, but for music.
Find a file
💻 Eher 4879a366d4
gifts: adopt explicit events and lock post-create flow
Switch JSONL writes to explicit gift events with startup auto-compaction, keep legacy read compatibility, and lock frontend edits after gift creation. Validate sender identity earlier and document the updated flow/events in the README.
2026-03-18 23:04:46 +01:00
.githooks chore: tweak pre-commit script 2026-03-14 12:50:37 +01:00
models gifts: adopt explicit events and lock post-create flow 2026-03-18 23:04:46 +01:00
static gifts: adopt explicit events and lock post-create flow 2026-03-18 23:04:46 +01:00
store gifts: adopt explicit events and lock post-create flow 2026-03-18 23:04:46 +01:00
tools blocking: harden album lock checks and add stale MB fallback 2026-03-18 08:31:16 +01:00
.dockerignore Initial commit 2026-02-02 16:04:37 +01:00
.drone.yml style: gofmt CI 2026-03-14 10:14:19 +01:00
.gitignore test: add comprehensive migration tests for gift data format upgrade 2026-03-13 18:29:00 +01:00
coverage.out style: run gofmt 2026-03-14 09:49:06 +01:00
DEVELOPMENT.md style: gofmt CI 2026-03-14 10:14:19 +01:00
docker-compose.yml Fix the container build 2026-02-03 23:17:42 +01:00
Dockerfile docker: copy full module in builder stage 2026-03-14 13:01:45 +01:00
go.mod style: gofmt CI 2026-03-14 10:14:19 +01:00
go.sum style: gofmt CI 2026-03-14 10:14:19 +01:00
main.go gifts: adopt explicit events and lock post-create flow 2026-03-18 23:04:46 +01:00
package.json frontend: simplify selection and move album details to confirmation 2026-03-17 22:34:24 +01:00
README.md gifts: adopt explicit events and lock post-create flow 2026-03-18 23:04:46 +01:00
server.log style: run gofmt 2026-03-14 09:49:06 +01:00

Music Gift

A personal page to receive music gifts, integrated with ListenBrainz and MusicBrainz.

What this is

  • SPA (HTML/CSS/JS) served by a small Go server
  • No login, no SQL database
  • Storage in append-only JSONL event log
  • Manual payment confirmation flow

User flow

  1. Visitor picks one item (track or album).
  2. Visitor fills sender name or marks anonymous.
  3. Visitor chooses payment method.
  4. Visitor clicks Confirm gift (this creates the gift + reservation).
  5. Visitor sees payment data and can click I've paid.
  6. Admin confirms or rejects payment.

Important behavior:

  • Name/anonymous is validated before Continue and again before Confirm gift.
  • After Confirm gift, selection and payment method are locked.
  • I've paid only sends a notification event; it does not change payment method.

Storage model (JSONL)

The system uses event sourcing with one JSON object per line.

Current event types:

  • gift_created
  • payment_notified
  • gift_confirmed
  • gift_rejected
  • gift_snapshot (written by compaction)

Event fields are flat (no nested gift object).

Example:

{"event":"gift_created","id":24,"at":"2026-03-18T10:00:00Z","type":"recording","track":"Bohemian Rhapsody","artist":"Queen","mbid":"c3bb4e0a-2c8b-401e-91e1-17e0d70fe4d9","mbid_release":"","from":"Alice","anon":false,"message":"for you","method":"pix","created_at":"2026-03-18T10:00:00Z","expires_at":"2026-03-18T11:00:00Z"}
{"event":"payment_notified","id":24,"at":"2026-03-18T10:02:00Z","method":"pix","notified_at":"2026-03-18T10:02:00Z"}
{"event":"gift_confirmed","id":24,"at":"2026-03-18T10:15:00Z","confirmed_at":"2026-03-18T10:15:00Z"}

Compaction writes one gift_snapshot per gift (final state).

Auto compaction

At startup, the store auto-compacts when:

  • line_count > 4 * gift_count

There is also a manual admin compact endpoint.

Legacy compatibility

The loader still accepts old formats (type: gift/patch/confirm/reject, nested gift, old order* events, and legacy mbid_rec). New writes always use the current flat event format.

Configuration

Copy .env.dist to .env and set values.

cp .env.dist .env

Key variables:

  • PORT (default 8080)
  • GIFTS_FILE (default ./data/gifts.jsonl)
  • LB_USER
  • LB_USER_AGENT
  • MB_USER_AGENT
  • RESERVE_TTL_HOURS (default 1)
  • ADMIN_TOKEN
  • PAY_* (payment labels/payloads)

API

Public:

  • GET /api/gifts list gifts (newest first)
  • POST /api/gifts create gift
  • PATCH /api/gifts/:id notify payment ({"notify": true})
  • GET /api/search?q= MusicBrainz search proxy
  • GET /api/recording_releases?mbid= release options for a recording
  • GET /api/release_preview?mbid= album preview (tracks)
  • GET /api/pay payment configuration
  • GET /api/lb/top
  • GET /api/lb/recent

Admin (X-Admin-Token required):

  • PATCH /api/admin/gifts/:id/confirm
  • PATCH /api/admin/gifts/:id/reject
  • POST /api/admin/compact

Examples:

curl -X PATCH "http://localhost:8080/api/gifts/24" \
  -H "Content-Type: application/json" \
  -d '{"notify":true}'

curl -X PATCH "http://localhost:8080/api/admin/gifts/24/confirm" \
  -H "X-Admin-Token: $ADMIN_TOKEN"

curl -X POST "http://localhost:8080/api/admin/compact" \
  -H "X-Admin-Token: $ADMIN_TOKEN"

Running

Local

go run main.go

Docker Compose

docker compose up -d --build
docker compose logs -f

Backup

cp data/gifts.jsonl data/gifts.jsonl.bak

With container volume:

docker compose exec music-gift sh -c "cp /data/gifts.jsonl /data/gifts.jsonl.bak"