A personal page to receive song gifts, integrated with ListenBrainz and MusicBrainz. Think “buy me a coffee”, but for music.
Find a file
2026-02-03 23:17:42 +01:00
static Fix payments with SEPA 2026-02-03 22:40:08 +01:00
.dockerignore Initial commit 2026-02-02 16:04:37 +01:00
.gitignore Initial commit 2026-02-02 16:04:37 +01:00
docker-compose.yml Fix the container build 2026-02-03 23:17:42 +01:00
Dockerfile Fix the container build 2026-02-03 23:17:42 +01:00
go.mod Initial commit 2026-02-02 16:04:37 +01:00
go.sum Initial commit 2026-02-02 16:04:37 +01:00
main.go Improve order flow and admin endpoints 2026-02-03 01:26:44 +01:00
README.md Improve order flow and admin endpoints 2026-02-03 01:26:44 +01:00

🎵 Music Gift

A personal page to receive song gifts, integrated with ListenBrainz and MusicBrainz. Think “buy me a coffee”, but for music.

Each song costs R$ 10 / € 2 / ≈ € 2 in Bitcoin (configurable).


What this is

A SPA (HTML + CSS + JS) served by a minimal Go HTTP server.

  • No login
  • No database
  • No external Go dependencies
  • JSONL append-only storage
  • Works great on Raspberry Pi

🧠 How it works

  1. The visitor picks a song:
    • Top and Recent come from your ListenBrainz user.
    • Search uses MusicBrainz via the backend (cache + rate limit).
  2. They add a name/handle (or stay anonymous) and an optional message.
  3. They choose a payment method (Pix / SEPA / Bitcoin).
  4. They confirm the order → the songs are reserved for 1 hour.
  5. They click “Ive paid” → you get a notification, but it does not auto-confirm payment.
  6. The songs appear in the public list with status:
    • 🟡 reserved (awaiting payment)
    • pending confirmation (notified)
    • confirmed (manual admin action)

⚠️ There is no automatic payment validation. This is a trust model + manual confirmation. Messages are private and only visible to the gift recipient.


💾 Storage (no database)

Data is stored in a JSONL file (one event per line):

gifts.jsonl

Append-only event log:

  • order → order creation
  • order_patch → payment method + notified_at (order)
  • order_confirm → manual order confirmation
  • order_reject → manual order rejection
  • gift → gift creation (items)
  • patch → payment method + notified_at (gift)
  • confirm → manual gift confirmation
  • reject → manual gift rejection

Why this format

  • Safer on crashes (doesnt corrupt the whole file)
  • Updates without full rewrite
  • Easy to inspect/debug
  • Simple backups (copy one file)

⚙️ Configuration

Before running, copy .env.dist to .env and adjust values as needed:

cp .env.dist .env

1) Payment data (env)

Set the payment info via .env:

PAY_PIX_PRICE="R$ 10"
PAY_PIX_DETAILS="Pix: your.pix.key@example.com"
PAY_PIX_PAYLOAD="PIX_COPIA_E_COLA_AQUI"
PAY_SEPA_PRICE="€ 2"
PAY_SEPA_DETAILS="IBAN: DE00 0000 0000 0000 0000 00 · BIC: DEUTDEFF"
PAY_SEPA_PAYLOAD="BCD\\n001\\n1\\nSCT\\nDEUTDEFF\\nYOUR_NAME\\nDE00 0000 0000 0000 0000 00\\nEUR2\\n\\nMUSIC GIFT\\n"
PAY_BTC_PRICE="₿ 0.00003"
PAY_BTC_DETAILS="BTC: YOUR_BITCOIN_ADDRESS"
PAY_BTC_PAYLOAD="bitcoin:YOUR_BITCOIN_ADDRESS?amount=0.00003"

PAY_*_PAYLOAD is what gets encoded as a QR code and copied to clipboard. If you want privacy with Pix, you can leave PAY_PIX_PAYLOAD empty and use only the key in PAY_PIX_DETAILS.

2) Environment variables

Variable Default Description
PORT 8080 HTTP server port
GIFTS_FILE ./data/gifts.jsonl JSONL file path (overridden to /data/gifts.jsonl in Docker)
LB_USER eher ListenBrainz user
LB_USER_AGENT (empty) ListenBrainz User-Agent (include contact)
MB_USER_AGENT (empty) MusicBrainz User-Agent (include contact)
RESERVE_TTL_HOURS 1 Reservation TTL in hours
ADMIN_TOKEN (empty) Token for admin endpoints
PAY_* (empty) Payment data (see above)

Recommended example:

LB_USER=eher
MB_USER_AGENT="MusicGift/1.0 (contact: you@example.com)"
ADMIN_TOKEN="a_long_random_secret"

Expected structure

music-gift/
├── docker-compose.yml
├── Dockerfile
├── .env
├── .env.dist
├── main.go
├── go.mod
├── static/
│   └── index.html
└── data/
    └── gifts.jsonl

Start (Raspberry Pi)

docker compose up -d --build

View logs

docker compose logs -f

Update (pull + rebuild)

git pull
docker compose up -d --build

🗄️ Backup

Local file backup:

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

If you are using a Docker volume:

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

Change port or ListenBrainz user

Edit .env:

PORT=8080
LB_USER=eher

Then recreate the container:

docker compose up -d --build

🔐 API endpoints

Public

  • GET /api/gifts → list gifts (newest first)
  • POST /api/orders → create a new order with multiple songs
  • PATCH /api/orders/:id → set payment method and notify
  • GET /api/search?q= → MusicBrainz search (via backend)
  • GET /api/pay → payment config (public)

Admin (requires ADMIN_TOKEN)

Confirm order payment (id or ref):

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

Reject order payment (id or ref):

curl -X PATCH "http://localhost:8080/api/admin/orders/12/reject" \
  -H "X-Admin-Token: $ADMIN_TOKEN" \

List orders (with items):

curl "http://localhost:8080/api/admin/orders?status=pending" \
  -H "X-Admin-Token: $ADMIN_TOKEN"

Get a single order (id or ref):

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

Compact the JSONL file (optional):

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

🧪 Run without Docker (dev)

go run main.go
# http://localhost:8080

No external dependencies. Works on amd64 and arm64.


🚀 Deployment notes

  • Ideal for Raspberry Pi
  • Use Docker Compose
  • Optionally put Caddy / Nginx in front for HTTPS
  • The container runs as a non-root user
  • Persistent data is ensured via a volume