A Coral module for organizing, enriching, and maintaining self-hosted media libraries.
pnpm installpnpm devApp runs at http://localhost:3000.
If JELLYFIN_URL, JELLYFIN_API_KEY, and JELLYFIN_USER_ID are present in your environment, Librarian skips setup and connects immediately. Otherwise it will open /setup and store the connection details in local SQLite under ./data/librarian.sqlite.
Librarian is the Coral module focused on media hygiene and enrichment:
- Scan a library for missing or inconsistent metadata
- Flag duplicates, poster gaps, and low-quality assets
- Queue background jobs for renaming, tagging, and enrichment
- Prepare media for downstream Coral modules while keeping Jellyfin as the source of truth
The current repo contains the first product shell and landing experience for that workflow.
- Default database path:
./data/librarian.sqlite - Override with:
LIBRARIAN_DATA_DIR=/path/to/data - Main use today: persisted Jellyfin connection details when env vars are not provided
Librarian requires a Jellyfin sign-in by default. That is deliberate and differs from the read-only Coral modules: Aurora and Tide default open because the worst an open instance does is show someone a library they could already stream, whereas this one moves and deletes files.
Anything that touches the filesystem additionally requires the signed-in user to be a Jellyfin administrator, and that gate does not relax when sign-in is switched off. Turning sign-in off opens browsing, never file operations.
| Variable | Default | Effect |
|---|---|---|
LIBRARIAN_REQUIRE_LOGIN |
true |
Pins the setting and hides the UI toggle |
CORAL_SERVICE_TOKEN |
unset | Grants another module full access without issuing a token in the UI |
Librarian only touches directories you have enabled.
LIBRARIAN_DOWNLOADS_DIR and LIBRARIAN_MEDIA_DIR seed root records; they
do not grant permission. A seeded root arrives switched off and a human turns
it on, so a container that happens to have /media bind-mounted can do
nothing with it until somebody says so.
Mount media and downloads under one mount point. A hardlink cannot cross a mount, even when both sides are the same filesystem, and hardlinking is what makes an import cost zero bytes and lets a torrent keep seeding. Split them and every import silently becomes a full copy — visible as "Copy" rather than "Hardlink" in the import preview.
| Tool | Purpose |
|---|---|
| TanStack Start | Full-stack React framework |
| TanStack Router | Type-safe file-based routing |
| TanStack Query | Server state management |
| Tailwind v4 | Styling |
| Biome | Linting & formatting |
| @get-coral/jellyfin | Jellyfin API client |
| Vitest | Testing |
pnpm dev # Start dev server on :3000
pnpm build # Production build
pnpm start # Run production server
pnpm typecheck # TypeScript check
pnpm check # Biome lint + format check
pnpm lint # Biome lint with auto-fix
pnpm test # Run tests# Pull the published image
docker pull getcoral/librarian:latest
# Or build it yourself
docker build -t librarian .
# Run
docker run -p 3000:3000 \
-e JELLYFIN_URL=http://your-nas:8096 \
-e JELLYFIN_API_KEY=your-key \
-e JELLYFIN_USER_ID=your-user-id \
getcoral/librarian:latestPublished automatically on every release via GitHub Actions:
getcoral/librarianon Docker Hubghcr.io/get-coral/librarianon GHCR
| Workflow | Trigger | What it does |
|---|---|---|
ci.yml |
Every PR + push to main | Typecheck, lint, test, build, Docker build check |
docker-publish.yml |
Push to main + version tags | Publishes to GHCR |
release-please.yml |
Push to main | Opens release PR, publishes Docker on merge |
Releases are fully automated via Release Please. Use conventional commits:
| Commit prefix | Version bump |
|---|---|
feat: |
Minor |
fix: |
Patch |
feat!: / fix!: |
Major |
chore:, docs: |
No bump |
This module is part of the Coral ecosystem. See the contributing guide before opening PRs.