A personal page to receive song gifts, integrated with ListenBrainz and MusicBrainz. Think “buy me a coffee”, but for music.
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. |
||
|---|---|---|
| .githooks | ||
| models | ||
| static | ||
| store | ||
| tools | ||
| .dockerignore | ||
| .drone.yml | ||
| .gitignore | ||
| coverage.out | ||
| DEVELOPMENT.md | ||
| docker-compose.yml | ||
| Dockerfile | ||
| go.mod | ||
| go.sum | ||
| main.go | ||
| package.json | ||
| README.md | ||
| server.log | ||
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
- Visitor picks one item (track or album).
- Visitor fills sender name or marks anonymous.
- Visitor chooses payment method.
- Visitor clicks
Confirm gift(this creates the gift + reservation). - Visitor sees payment data and can click
I've paid. - Admin confirms or rejects payment.
Important behavior:
- Name/anonymous is validated before
Continueand again beforeConfirm gift. - After
Confirm gift, selection and payment method are locked. I've paidonly 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_createdpayment_notifiedgift_confirmedgift_rejectedgift_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(default8080)GIFTS_FILE(default./data/gifts.jsonl)LB_USERLB_USER_AGENTMB_USER_AGENTRESERVE_TTL_HOURS(default1)ADMIN_TOKENPAY_*(payment labels/payloads)
API
Public:
GET /api/giftslist gifts (newest first)POST /api/giftscreate giftPATCH /api/gifts/:idnotify payment ({"notify": true})GET /api/search?q=MusicBrainz search proxyGET /api/recording_releases?mbid=release options for a recordingGET /api/release_preview?mbid=album preview (tracks)GET /api/paypayment configurationGET /api/lb/topGET /api/lb/recent
Admin (X-Admin-Token required):
PATCH /api/admin/gifts/:id/confirmPATCH /api/admin/gifts/:id/rejectPOST /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"