Cleanup: validate request bodies for remaining JSON-mutating routes
Second of six cleanup MRs from the codebase review. Extends the handwritten-validator pattern from `api/preview/+server.ts` to every other route that calls `request.json()`.
What changes
- New `src/lib/server/validation/primitives.ts` with `requireObject`, `requireString`/`optionalString`, `requireInt`/`optionalInt`, `requireNumber`/`optionalNumber`, `requireBoolean`/`optionalBoolean`, `requireEnum`, `requireArray`, and a `ValidationError` class. Validators throw `ValidationError`; routes catch and return 400. No external deps — no zod/valibot.
- Filesystem-touching routes now validate:
- `api/sort/execute`: `albumIds` is a non-empty array of positive integers; `overwriteExisting` is a boolean if present.
- `api/scan`: `action` is one of `startWatcher`/`stopWatcher`/`scan` (missing/null still defaults to `scan`).
- `api/beets`: `action` is one of `ingest`/`sync`/`markInitialDone`; `autotag` is a boolean if present.
- DB-only routes now validate:
- `api/scrobble/submit`, `api/scrobble/now-playing`: `artist`/`track` non-empty strings, optional `album`/`duration`, plus required `timestamp` for `submit`.
- `api/albums` POST: `albumId` positive integer, `action` enum.
- `api/albums/[id]` POST: `action` enum, optional `useCache`/`artist`/`album`/`storefront`.
- `api/settings/autoplay`: `enabled` required boolean (was previously a loose truthiness check).
Behavior
Malformed bodies that previously fell through silently (or coerced via `!!`/`=== true`) now return 400 with a specific error message. Existing in-app callers already pass well-formed bodies, so no UI changes required. `pnpm run build` passes.
Why
The review flagged that several routes did `await request.json()` and cast the result without validation. After MR 1 centralized the category check (`parseCategory`), this MR extends the same approach to the remaining routes so attacker-controlled bodies cannot reach the filesystem or DB layer untyped.
Builds on MR !47 (merged).