- comicvine provider for comics. ComicVine has no keyless tier, so the provider is only registered when VELLUM_COMICVINE_API_KEY is set; without it the server logs a note and comic enrichment completes as a no-op rather than erroring. - BaseURL is injectable so the response parsing is covered by a test against a local server: candidate mapping, the required User-Agent, and the status_code=100 invalid-key case (ComicVine returns HTTP 200 for that). - descriptions are stripped of ComicVine's embedded HTML before storage. Verified against a fake ComicVine server, and live that a comics library enriches cleanly with no key configured.
144 lines
5.6 KiB
Markdown
144 lines
5.6 KiB
Markdown
# Vellum
|
|
|
|
One application, one process, one database for every media type — ebooks,
|
|
comics, audiobooks, music, photos, games, and video — behind a single account
|
|
system with real per-user progress and per-library permissions.
|
|
|
|
A modular monolith. One Go binary, one SQLite file, one web frontend.
|
|
|
|
The full plan lives in `~/.opencode/plan/vellum.md`.
|
|
|
|
## Status
|
|
|
|
| Phase | Scope | State |
|
|
| --- | --- | --- |
|
|
| **0** | Repo, config, migrations, health, Docker | **done** |
|
|
| **1** | Users, sessions, library grants | **done** |
|
|
| **2** | Scanner, items, assets | **done** |
|
|
| **2b** | FTS5 search | **done** |
|
|
| **3** | EPUB reader + progress | **done** |
|
|
| **4** | CBZ comic reader | **done** |
|
|
| **5** | Job queue, metadata (Open Library, Google Books) | **done** |
|
|
| **6a** | Audiobooks + music playback | **done** |
|
|
| **6c** | Photos + thumbnails | **done** |
|
|
| **UI** | Svelte app | **done** |
|
|
| 6b | Music metadata, smart playlists | not started |
|
|
| 7 | Games (EmulatorJS, IGDB) | not started |
|
|
| — | Video (stays on Jellyfin for now) | deferred |
|
|
|
|
What works today: an administrator adds libraries pointing at server folders,
|
|
scans them, and optionally enriches metadata from Open Library. Users sign in
|
|
and read EPUBs, read CBZ comics, listen to audiobooks track-by-track, and
|
|
browse photos — with position saved per account, per file. PDF, RAR comics,
|
|
games, and video are not implemented.
|
|
|
|
Five of the six services in the existing stack are covered: **Stump, Kavita,
|
|
Audiobookshelf, Navidrome (playback), and RomM** are the intended
|
|
replacements. RomM is not yet covered; video stays on Jellyfin.
|
|
|
|
## Quick start
|
|
|
|
```bash
|
|
sudo dnf install golang # once; Go is not in the base image here
|
|
make build
|
|
make run
|
|
curl localhost:7788/healthz # {"database":"ok","status":"ok",...}
|
|
```
|
|
|
|
Run with media roots configured:
|
|
|
|
```bash
|
|
VELLUM_MEDIA_DIRS=/mnt/storage/media/ebooks:/mnt/storage/media/comics make run
|
|
```
|
|
|
|
## Configuration
|
|
|
|
All configuration is environment variables. Every one has a default, so the
|
|
binary runs with no configuration at all.
|
|
|
|
| Variable | Default | Meaning |
|
|
| --- | --- | --- |
|
|
| `VELLUM_LISTEN` | `:7788` | Bind address |
|
|
| `VELLUM_DATA_DIR` | `./data` | SQLite database lives here |
|
|
| `VELLUM_CONFIG_DIR` | `./config` | Regenerable cache (covers, pages) |
|
|
| `VELLUM_MEDIA_DIRS` | *(none)* | Read-only library roots, colon-separated |
|
|
| `VELLUM_ADMIN_USER` | *(none)* | Creates the first admin, **only on an empty database** |
|
|
| `VELLUM_ADMIN_PASSWORD` | *(none)* | Password for that first admin (min 8 chars) |
|
|
| `VELLUM_COOKIE_SECURE` | `false` | Set `true` when serving over TLS |
|
|
| `VELLUM_COMICVINE_API_KEY` | *(none)* | Enables comic metadata via ComicVine |
|
|
| `VELLUM_LOG_LEVEL` | `info` | `debug`, `info`, `warn`, `error` |
|
|
| `VELLUM_LOG_FORMAT` | `text` | `text` or `json` |
|
|
| `VELLUM_SESSION_TTL` | `720h` | Login session lifetime |
|
|
| `VELLUM_SHUTDOWN_GRACE` | `15s` | Graceful shutdown timeout |
|
|
|
|
The first admin is created only when no users exist, so setting these on an
|
|
established install does nothing. If the database is empty and they are unset,
|
|
the server logs a warning and refuses to invent a default password.
|
|
|
|
## Layout
|
|
|
|
```
|
|
cmd/vellum/ main — wiring and lifecycle only
|
|
internal/
|
|
core/ config, logging, database, migration runner
|
|
httpx/ shared JSON helpers
|
|
httpapi/ router, middleware, static SPA serving
|
|
auth/ argon2id, sessions, grants, bootstrap
|
|
library/ library CRUD, media-type registry, scanner
|
|
media/ generic item/file/cover/progress endpoints
|
|
progress/ the polymorphic per-user progress store
|
|
stream/ range-capable file serving
|
|
natsort/ natural ordering for pages and tracks
|
|
jobs/ table-backed job queue
|
|
metadata/ provider interface, Open Library, Google Books, enricher
|
|
search/ FTS5 queries
|
|
modules/
|
|
ebooks/ EPUB parsing and cover extraction
|
|
comics/ CBZ page listing and extraction
|
|
photos/ thumbnail generation
|
|
web/ Svelte app; dist/ is embedded via web/embed.go
|
|
migrations/ embedded SQL, applied in order at boot
|
|
```
|
|
|
|
**Module rule:** the core never imports a feature module; modules import the
|
|
core and mount their own routes, tables, and scanner rules. Adding a media type
|
|
is a new package plus a migration, never a core edit.
|
|
|
|
**Schema rule:** media items are global; everything user-specific is a join.
|
|
Nothing may hold per-user state on a media row.
|
|
|
|
## Database
|
|
|
|
SQLite in WAL mode, opened through `modernc.org/sqlite` — a pure-Go driver, so
|
|
the binary builds with `CGO_ENABLED=0` and cross-compiles without a toolchain
|
|
per target.
|
|
|
|
Migrations are embedded and applied in filename order; applied files are
|
|
recorded only by number in `PRAGMA user_version`, so **never edit an applied
|
|
migration** — add a new one.
|
|
|
|
Back up by copying the database (or its directory); `VACUUM INTO 'backup.db'`
|
|
takes a consistent snapshot while running.
|
|
|
|
## Deploy
|
|
|
|
```bash
|
|
make docker # podman build
|
|
podman compose up -d # see compose.yml
|
|
```
|
|
|
|
The container mounts `/data` (database), `/config` (cache), and `/media`
|
|
(read-only). On SELinux hosts every bind mount needs `:z` — `compose.yml`
|
|
already has it.
|
|
|
|
The application never writes to `/media`.
|
|
|
|
## Development
|
|
|
|
```bash
|
|
make build # builds the frontend, then the Go binary with it embedded
|
|
make build-go # Go only, when the frontend is unchanged
|
|
make check # gofmt + vet + test
|
|
make run # run the server on :7788
|
|
cd web && pnpm dev # frontend dev server, proxies /api to :7788
|
|
```
|