···11-FROM oven/bun:1 AS base
22-WORKDIR /app
33-44-COPY package.json bun.lock ./
55-RUN bun install --frozen-lockfile --production
66-77-COPY src/ src/
88-99-EXPOSE 3000
1010-CMD ["bun", "run", "src/index.ts"]
+39-15
README.md
···11# api.matthew-hre.com
2233-Personal API powering [matthew-hre.com](https://matthew-hre.com). Built with [Hono](https://hono.dev) on [Bun](https://bun.sh), deployed via [Dokploy](https://dokploy.com).
33+Personal API powering [matthew-hre.com](https://matthew-hre.com). Built with
44+[Hono](https://hono.dev) on [Cloudflare Workers](https://workers.cloudflare.com),
55+backed by [D1](https://developers.cloudflare.com/d1/), and managed with
66+[Wrangler](https://developers.cloudflare.com/workers/wrangler/).
4758## Endpoints
69710### `GET /vinyl`
81199-Paginated vinyl collection, synced nightly from Discogs.
1212+Paginated vinyl collection, synced from Discogs every 6 hours via cron.
10131114| Param | Default | Options |
1215| ------- | --------- | ---------------------------- |
···16191720### `GET /activity/music`
18211919-Currently playing (or last played) track from Last.fm.
2222+Currently playing (or last played) track from Last.fm. Polled into D1 every
2323+minute by a cron trigger.
20242125### `GET /activity/music/stream`
22262323-SSE stream that pushes track changes in real-time. Polls Last.fm every ~10 seconds server-side and broadcasts to all connected clients.
2727+SSE stream that pushes track changes. Reads from the D1-cached value every
2828+10s and emits when it changes.
24292530## Local Development
26312732```bash
2828-cp .env.example .env # fill in your tokens
2929-docker compose up -d # start Postgres
3333+nix develop # gets you bun + wrangler
3434+3035bun install
3131-bun run sync # seed the database from Discogs
3232-bun run dev # start the API on :3000
3636+wrangler d1 create api # one-time; copy the id into wrangler.toml
3737+bun run migrate:local # applies migrations/ to local D1
3838+bun run dev # starts the worker on :8787
3339```
34403535-## Environment Variables
4141+Trigger the cron handlers locally with:
36423737-| Variable | Description |
3838-| -------------------------------- | ---------------------------- |
3939-| `DATABASE_URL` | Postgres connection string |
4040-| `DISCOGS_PERSONAL_ACCESS_TOKEN` | Discogs API token |
4141-| `LASTFM_API_KEY` | Last.fm API key |
4242-| `LASTFM_USERNAME` | Last.fm username |
4343+```bash
4444+# Last.fm poll
4545+curl 'http://localhost:8787/__scheduled?cron=*+*+*+*+*'
4646+# Discogs sync
4747+curl 'http://localhost:8787/__scheduled?cron=0+*/6+*+*+*'
4848+```
4949+5050+## Deployment
5151+5252+```bash
5353+bun run migrate:remote # apply migrations to remote D1
5454+bun run deploy # wrangler deploy
5555+```
5656+5757+## Secrets
5858+5959+Set with `wrangler secret put <NAME>`:
6060+6161+| Variable | Description |
6262+| -------------------------------- | --------------------- |
6363+| `DISCOGS_PERSONAL_ACCESS_TOKEN` | Discogs API token |
6464+| `LASTFM_API_KEY` | Last.fm API key |
6565+6666+`LASTFM_USERNAME` and `DISCOGS_USER` are plain `[vars]` in `wrangler.toml`.
···11+-- Vinyl collection synced from Discogs
22+CREATE TABLE IF NOT EXISTS releases (
33+ discogs_id INTEGER PRIMARY KEY,
44+ title TEXT NOT NULL,
55+ artist_name TEXT NOT NULL,
66+ cover_image TEXT NOT NULL,
77+ date_added TEXT NOT NULL
88+);
99+1010+CREATE INDEX IF NOT EXISTS releases_date_added_idx ON releases (date_added);
1111+CREATE INDEX IF NOT EXISTS releases_title_idx ON releases (title);
1212+CREATE INDEX IF NOT EXISTS releases_artist_idx ON releases (artist_name);
1313+1414+-- Single-row cache of the currently-playing Last.fm track
1515+CREATE TABLE IF NOT EXISTS now_playing (
1616+ id INTEGER PRIMARY KEY CHECK (id = 1),
1717+ payload TEXT, -- JSON-encoded Track or NULL
1818+ updated_at INTEGER NOT NULL -- epoch ms
1919+);
2020+2121+INSERT OR IGNORE INTO now_playing (id, payload, updated_at) VALUES (1, NULL, 0);