Profile
Back to NewsBack
GitHub Trending 31 min
Reader Mode
hunter-read/grimoire: Self-hosted organizer for your TTRPG content and campaigns

hunter-read/grimoire: Self-hosted organizer for your TTRPG content and campaigns

5 hours ago

Grimoire

Grimoire - Self-Hosted TTRPG Library Manager

Discord</a> CI</a> Backend coverage</a> Frontend coverage</a> Python</a> React</a> License</a> Docker</a>

Website · Documentation · Live Demo · Join our Discord

A Docker-based web application for managing your tabletop RPG PDF collection. Browse, search, and read your entire library from any device with a clean, responsive UI.

Features

Library

  • Library browser — Organizes your collection by game system from the folder structure, with card, compact, and list layouts.
  • Full-text search — Every page of every PDF is indexed with SQLite FTS5. Also finds books by title, author, or publisher, and maps, tokens, audio, and models by filename, folder, or tag. Narrow with title:, author:, tag: and friends — see Searching your library.
  • Sort, filter, and saved presets — Filter on genre, system family, edition, dice/materials, tags, favourites, and explicit content. Tag filters are boolean groups (Building AND store OR shop), and named presets save to your account with one default per view.
  • Shared tags — One tag catalog across every resource type, with a Tags page to rename, merge, delete, and browse by tag, and per-tag downloads.
  • Metadata editor — Rich metadata for systems and books, drawn from curated lists you manage in Settings → Metadata. Parent systems group related lines ("Cyberpunk" + "Red" → "Cyberpunk Red").
  • Duplicate detection — An admin scan finds byte-identical files, near-identical titles, overlapping page text, and gridded/gridless map pairs. Nothing is ever deleted automatically; you review each pair side by side.

Reading

  • Page-by-page viewer — PDFs rendered server-side as images for fast mobile viewing, with pinch-to-zoom, swipe navigation, and spread mode.
  • Bookmarks — Per-user page and text-selection bookmarks with inline highlights.
  • Favorites — Star systems, books, maps, tokens, audio, and models from a card, a list row, or the item's own page.
  • OPDS catalog — A personal feed URL per user, to connect e-reader apps directly to your library.

Maps

  • Map gallery — Browse battlemaps by folder with tag filtering, grid metadata, and full-res download. Image, PDF, animated (.webm/.mp4), and Universal VTT maps all display in-app; large maps load through a downscaled preview while downloads stay untouched.
  • UVTT editor — Export any image map as a Universal VTT file, drawing the walls, doors, and lights a virtual tabletop uses for dynamic lighting. Opens existing .uvtt files on the geometry they already carry.

Tokens

  • Token browser — Browse and tag character tokens and portrait assets.
  • Token editor — Turn any picture into a VTT-ready token: position the art, pick a frame and output size, and export it. See Token editor.

Audio

  • Audio library — Ambient tracks, soundscapes, music, and effects (MP3, OGG, Opus, FLAC, WAV, M4A, AAC), reading embedded duration, tags, and album art.
  • Global player — A persistent pop-out player that keeps playing as you navigate, with a queue you can reorder and a repeat toggle.
  • Soundboard — A floating, draggable grid of one-tap pads that play over the player and each other, with per-pad loop toggles and a configurable grid up to 8×15.
  • Saved sets — Name and keep playlists and boards. They live on your account rather than one browser, so a board built at home is there at the table.

Campaigns

  • Campaigns — Track GM-run and personal campaigns with character art and sheets, linked resources, and scheduling.
  • Wiki — A markdown notes wiki with deep linking, Markdown/JSON/LegendKeeper import and export.
  • Wiki note templates — Start a page from a community template, write your own, or upload a Markdown file or .zip. Templates belong to the campaign, so downloaded ones are yours to edit. See docs/wiki-templates.md.

Administration

  • Book restrictions — Restrict a book, system, or category to GMs and admins or admins only. Restricted content is hidden outright — from library, search, downloads, favourites, and OPDS — since the title and cover are the spoiler. See Restricting books.
  • Community add-ons — Install community metadata scrapers to fill in system and book details from external sources, reviewing a field-by-field diff before anything is written. See docs/addons.md.
  • Themes and light mode — Light, dark, or system, plus installable colour themes (including a WCAG AAA High Contrast palette). Themes are per user, so no admin approval is involved. See docs/themes.md.
  • Docker ready — One command to run, mount your library directory, done. Works on desktop, tablet, and phone.

Screenshots

Library and browsing

| Systems | System detail | | ----------------------------------------------- | ------------------------------------------------------------- | | !Systems view | !System detail view |

| Search | Tag browser | | ---------------------------------------------- | --------------------------------------------- | | !Search your library | !Tag browser |

| Favourites | | | ---------------------------------------------------- | --- | | !Favourites page | |

Reading

| Book view with table of contents | In-book search | | --------------------------------------------------------------- | --------------------------------------------------------- | | !Book view with ToC | !Book view search |

Maps, tokens, and models

| Maps | Tokens | | ----------------------------------------- | ------------------------------------------- | | !Maps page | !Token page |

| Token editor | 3D models | | ----------------------------------------------- | --------------------------------------------- | | !Token editor | !Models page |

| Universal VTT map editor | Token vision preview | | ------------------------------------------------------- | ------------------------------------------------------------------------------------------------------- | | !UVTT map editor | !UVTT map editor token vision preview |

Audio

| Audio page with soundboard and playlist | | | ------------------------------------------------------------------------------------------------------------- | --- | | !Audio page with soundboard and playlist | |

Campaigns

| Campaign overview | Campaign notes wiki | | --------------------------------------------------------- | --------------------------------------------------------------- | | !Campaign overview | !Campaign notes page |


Quick Start

New to Docker? See the Docker Installation Guide for a step-by-step walkthrough for Windows, macOS, and Linux.

1. Organize your library

Create a library/ folder with this structure:

library/
├── books/
│   └── Dungeons and Dragons 5e/
│       ├── core/
│       │   ├── Players Handbook.pdf
│       │   ├── Dungeon Masters Guide.pdf
│       │   └── monsters/              ← subfolder within a category
│       │       ├── Monster Manual.pdf
│       │       └── Mordenkainen's Monsters.pdf
│       ├── supplements/
│       ├── adventures/
│       │   ├── Curse of Strahd/       ← adventure path subfolder
│       │   │   ├── Curse of Strahd.pdf
│       │   │   └── Strahd DM Screen.pdf
│       │   └── Lost Mine of Phandelver/
│       │       └── Lost Mine of Phandelver.pdf
│       ├── character-sheets/
│       ├── handouts/
│       └── homebrew/
├── maps/
│   └── Sunken Temple (22x22)/
│       ├── Sunken Temple Basement.png
│       └── The Sunken Temple.png
├── tokens/
│   └── Monsters/
│       └── goblin.png
├── audio/
│   └── Ambient/
│       ├── cover.jpg
│       └── tavern-night.mp3
└── models/
    └── Goblins/
        ├── Presupported/
        │   └── goblin-archer.stl
        └── Unsupported/
            └── goblin-archer.stl

See Library Structure for the full layout and category rules.

2. Run with Docker Compose

Copy the default compose file, set your volume paths, then start:

cp docs/docker/docker-compose.yml docker-compose.yml

Edit docker-compose.yml and set the volume paths

docker compose up -d open http://localhost:9481

On first launch you'll be prompted to create an admin account, or you can pre-seed users automatically (see Pre-seeding users).

3. Pull from DockerHub

docker pull hunterreadca/grimoire:latest

Or pin to a specific release:

docker pull hunterreadca/grimoire:1.5.0

Image variants: the default tags (latest, 1.5.0, …) include the Tesseract OCR engine so image-only PDFs are searchable (see OCR). If you don't need OCR and prefer a smaller image, use the matching -slim tag (e.g. hunterreadca/grimoire:latest's slim counterpart :slim, or a pinned :1.5.0-slim), which omits Tesseract.

4. Minimal docker-compose.yml

services:
  grimoire:
    image: hunterreadca/grimoire:latest
    ports:
 - "9481:9481"
    volumes:
 - /path/to/your/library:/app/library   # add ":ro" to keep it read-only (see Volumes)
 - /path/to/grimoire/data:/app/data

5. Example compose files

Ready-to-use compose files for common setups are in docs/docker/:

| File | What it runs | | ------------------------------------------------------------------------------------------ | -------------------------------------------------------------- | | docs/docker/docker-compose.yml | Grimoire (default, no extras) | | docs/docker/docker-compose.valkey.yml | Grimoire + Valkey page cache (recommended for large libraries) | | docs/docker/docker-compose.calibre.yml | Grimoire + Calibre full desktop (metadata editing, OPF export) | | docs/docker/docker-compose.calibre-web.yml | Grimoire + Calibre-Web (lightweight Calibre browser UI) |

Each file has inline comments explaining the options. Copy and edit the one that fits your setup:

cp docs/docker/docker-compose.valkey.yml docker-compose.yml

Edit the volume paths, then:

docker compose up -d

6. Container health

The image ships a HEALTHCHECK that probes the unauthenticated GET /api/health endpoint. It verifies the app is serving on port 9481 and can reach the database (and Valkey, when configured), so docker ps shows (healthy) / (unhealthy) rather than just "running". Orchestrators can gate startup on it:

depends_on:
  grimoire:
    condition: service_healthy

Persistent data

The database, search index, and rendered thumbnails are all stored under DATA_PATH (the /app/data volume). Back this directory up to preserve your library metadata and user accounts.

Upgrading

Pull the new image and restart (docker compose pull && docker compose up -d). Database schema changes are applied automatically on startup via Alembic - no manual action is required when upgrading, including from versions that predate Alembic. On first run under the new system, an existing database is detected and stamped at the correct baseline, so only genuinely new migrations run thereafter. Back up DATA_PATH before upgrading, as always.

Running from source

Prefer to build the image yourself or run Grimoire directly on the host (Python 3.12+, Node 20+) without Docker? See docs/running-from-source.md.


Library Structure

Books - one folder per game system

Each top-level folder under books/ becomes a game system. Subfolders are auto-detected as categories based on their name.

Folder name matching is case-insensitive, and hyphens, underscores, and spaces are interchangeable - Character-Sheets, character_sheets, and Character Sheets all map to the same category.

| Category | Recognized folder names | What goes here | | ---------------- | ----------------------------------------------------------------------- | --------------------------------------------- | | Core Rulebooks | core, rulebooks, rules | Player handbooks, GM guides, base rules | | Starter Set | starter-set, starter kit, beginner box, boxed set, essentials | Starter/beginner boxes, introductory sets | | Supplements | supplements, sourcebooks, expansions | Sourcebooks, expansions, setting guides | | Adventures | adventures, modules, campaigns | Published modules, campaigns, one-shots | | Character Sheets | character-sheets, character sheets, charsheets | Fillable sheets, alternative layouts | | Handouts | handouts, reference, screen | Reference cards, DM screens, quick-ref sheets | | Homebrew | homebrew, custom, house-rules | Community/custom content, house rules |

Files placed directly in a system folder (not in a subfolder) default to the core category.
> Any subfolder name that doesn't match the recognized keywords becomes its own category, slugified from the folder name. For example, a folder named Bestiary becomes the bestiary category.
> Prefer to organize categories yourself? Turn folder-name inference off in Settings → Application → Folder Category Inference (or pin it with the DISABLE_FOLDER_CATEGORY_INFERENCE env var); books then fall back to the uncategorized category. To disable inference for a single system only, drop an empty .no-auto-category file at that system's folder root.
> After adding new files, use Rescan in the sidebar (or Settings → Maintenance) to pick up the changes. For large libraries you can also rescan a single corner: every system, category, subfolder, and map/token group has its own rescan button that re-scans just that folder.

Subfolders within a category

Any category folder can contain named subfolders to group related books together. Grimoire detects these automatically and displays them as collapsible folder groups within the category section - no configuration needed.

books/
└── Pathfinder 2e/
    ├── core/
    │   ├── Core Rulebook.pdf          ← ungrouped, shown at top of Core Rulebooks
    │   └── monsters/                  ← subfolder group "Monsters"
    │       ├── Bestiary.pdf
    │       ├── Bestiary 2.pdf
    │       └── Bestiary 3.pdf
    └── adventures/
        ├── Standalone Adventure.pdf   ← ungrouped
        ├── Abomination Vaults/        ← subfolder group "Abomination Vaults"
        │   ├── Ruins of Gauntlight.pdf
        │   ├── Hands of the Devil.pdf
        │   └── Eyes of Empty Death.pdf
        └── Outlaws of Alkenstar/
            └── ...

Books without a subfolder are shown ungrouped at the top of their category section, above any subfolder groups. Subfolder groups are collapsible and include a download button for the whole group.

Book formats

Grimoire indexes more than PDFs. Every format below appears in the library, gets a cover thumbnail where one can be produced, and has its text added to the full-text search index:

| Format | Extensions | Reader | Full-text search | Thumbnail | | ---------------- | ----------------------------------------------------------------- | ---------------------------- | ----------------------- | --------- | | PDF | .pdf | Rendered pages | Yes (text layer or OCR) | Yes | | E-book | .epub | Rendered pages | Yes | Yes | | Scanned document | .djvu | Rendered pages | Yes | Yes | | Comic archive | .cbz, .cbr, .cb7, .cbt | Page images from the archive | No (images only) | Yes | | Plain text | .txt, .md, .rtf | Formatted text | Yes | No | | Image | .png, .jpg, .jpeg, .gif, .webp, .bmp, .tiff, .svg | Single image | No | Yes |

A few notes:

  • EPUB is reflowable, so it has no fixed page count of its own. Grimoire lays every EPUB out at one fixed page size, which keeps page numbers stable: a search result for page 12 always opens the same page 12 you would see while reading.
  • Text files are paginated into fixed-size pages, split at paragraph boundaries so a page break never lands mid-sentence. Markdown keeps its source markers (#, *) rather than being rendered as HTML, so searching for a heading finds what you typed.
  • .rtf files are unwrapped to plain text; formatting is not preserved. Older files saved in a legacy Windows encoding are decoded correctly.
  • Comic archives have no text layer, so they are readable but not searchable.
If you have EPUB or DjVu books that were added by an earlier version, a rescan backfills their thumbnails, page counts, and search index - you do not need to re-add them.

Archive files

Archive files placed anywhere under books/ are shown alongside your books in their category - handy for bundling a set of related files (a maps pack, a COMP/CON export, loose handouts) next to the book they belong to. Recognized extensions:

| Type | Extensions | | ----- | ------------------------------------------------------ | | Zip | .zip, .cbz | | RAR | .rar, .cbr | | 7-Zip | .7z, .cb7 | | Tar | .tar, .cbt, .tar.gz, .tgz, .tar.bz2, .tbz2 |

Ordinary archives are treated as opaque downloads - Grimoire does not extract or read their contents, so clicking one downloads the file rather than opening the reader. They're also included when you download a whole system, category, or subfolder as an archive.

Comic-book archives (.cbz, .cbr, .cb7, .cbt) are the exception: they open in the reader and page through the images inside them, and they get a cover thumbnail from the first page. Pages are ordered by filename, which is the convention comic archives are built on (page01.jpg, page02.jpg, ...); macOS resource-fork entries and hidden files are skipped. Only the page you are looking at is decompressed, so a large collection doesn't have to be unpacked to read one issue.

Archives are also recognized under maps/, tokens/, audio/, and models/, where they appear in the gallery next to your images and tracks marked with an Archive badge. Map packs and art collections are often distributed zipped alongside supplementary files (PSDs, STLs, source files), so bundling them keeps the extras with the maps they belong to without cluttering the gallery. Opening one offers a download instead of a preview - there is no thumbnail, no image viewer, and no audio player, since the contents are never extracted. The comic-book extensions (.cbz, .cbr, .cb7, .cbt) are books-only and are skipped in these collections.

Special collections (system-agnostic & one-page)

Some books don't belong to a single game system - reference material, zines, art books, or rulesets like Ironsworn or Mothership that span multiple systems. And some "systems" are really a bucket of many tiny games: one-page and small RPGs. Create a folder whose name is one of the recognized names below and Grimoire will display its contents in a separate Special Collections section on the library page, outside the normal game-system grid.

Recognized folder names (case-insensitive):

| Folder name | Collection | Example | | ------------------ | --------------------- | ------------------------- | | System Agnostic | System-agnostic | books/System Agnostic/ | | Generic | System-agnostic | books/Generic/ | | Any | System-agnostic | books/Any/ | | One-Page RPGs | One-page / small RPGs | books/One-Page RPGs/ | | Single-Page RPGs | One-page / small RPGs | books/Single-Page RPGs/ | | One-Shot RPGs | One-page / small RPGs | books/One-Shot RPGs/ | | Micro RPGs | One-page / small RPGs | books/Micro RPGs/ |

For the system-agnostic collections, subfolders directly under the root become custom category headings - whatever you name them is what appears in the UI. There is no keyword matching; the folder name is used as-is (slugified).

books/
└── System Agnostic/
    ├── Ironsworn/
    │   ├── Ironsworn.pdf
    │   └── Ironsworn Delve.pdf
    ├── OSR Zines/
    │   └── Knock Issue 1.pdf
    └── Art Books/
        └── MCDM Strongholds and Followers.pdf

Books placed directly in the root (without a subfolder) appear under an Uncategorized heading.

If you'd rather not use one of the recognized names, drop a .system-agnostic-container marker file in any folder and it becomes the system-agnostic collection instead. The marker only changes _which_ folder is the collection - the shape stays the same, so its subfolders are still category headings rather than systems, and the library counts the books inside it. There can only be one system-agnostic collection, so Grimoire refuses a second folder claiming it.

The one-page / small RPG collections behave differently: they are _system containers_, described next.

System containers (parent systems & sub-libraries)

Sometimes a folder isn't a game system - it's a shelf holding several. Grimoire supports five flavours, and in every case the folder's immediate children become game systems in their own right, each with its own metadata, tags, cover, and place in the system filters.

Parent systems group the editions of one game:

books/
└── Dungeons & Dragons/          ← a container, not a system
    ├── .parent-system-container ← marker file declaring it
    ├── 3e/
    │   └── core/
    │       └── Players Handbook.pdf
    └── 5e/
        └── core/
            └── Players Handbook.pdf

This yields two systems - "Dungeons & Dragons 3e" and "Dungeons & Dragons 5e" - each with Parent System set to "Dungeons & Dragons" and Edition set to the folder name, so you can filter the library by either. Category folders (core, adventure, …) work normally _inside_ each edition.

System families group related but _distinct_ systems that share a lineage - not editions of one game:

books/
└── d20 System/                    ← .system-family-container
    ├── Pathfinder/                ← .parent-system-container (nesting is fine)
    │   ├── 1e/
    │   └── 2e/
    ├── Mutants & Masterminds/
    └── d20 Modern/

Each child is an independent system, and the container's name fills in its System Family field - so the folder structure and the family filter finally line up. Children keep their own names (no {Parent} {Child} prefixing) and get no Edition/Parent System, because they aren't variants of anything. As shown above, a family can hold a multi-edition system: the nested .parent-system-container resolves its editions normally, and inherits the family name itself.

Publisher containers group the systems one company puts out:

books/
└── Paizo/                         ← .publisher-container
    ├── Pathfinder 2e/
    └── Starfinder/

Each child is an independent system with the container's name recorded as its Publisher.

Family and publisher containers only _fill in_ metadata a system doesn't already have. If a book's OPF sidecar, an add-on, or your own edit already set the family or publisher, a rescan leaves it alone.

Generic containers are the escape hatch. A bare .container marker says only "the folders in here are systems" and claims nothing about how they relate, so it propagates no metadata at all - use it when your shelf doesn't fit any of the named kinds:

books/
└── Kickstarter Hauls/             ← .container
    ├── Mörk Borg/
    └── Mothership/

One-page / micro-RPG collections are sub-libraries of many tiny games. Here, _both_ subfolders and loose files at the root become systems:

books/
└── One-Page RPGs/               ← a container (recognized by name)
    ├── honey-heist.pdf          → system "Honey Heist" (1 book)
    ├── lasers-and-feelings.pdf  → system "Lasers And Feelings" (1 book)
    └── cbr+pnk/                 → system "Cbr+pnk" (2 books)
        ├── core/
        │   └── core-rules.pdf
        └── character-sheets/
            └── character.pdf

A single-file game becomes a system holding that one book; a folder-backed game keeps its internal category structure. Either way each game gets full system-level metadata and tagging, while the collection itself stays as one tidy entry in the Special Collections strip instead of flooding the main grid.

Declaring a container. Any of these work, and they can be combined with (nsfw) and sort-order prefixes:

| Method | Example | Kind | | ------------------ | -------------------------------------------------- | ------------------- | | Marker file | books/D&D/.parent-system-container | Parent system | | Marker file | books/Itch Bundle/.one-page-container | One-page collection | | Marker file | books/d20 System/.system-family-container | System family | | Marker file | books/Paizo/.publisher-container | Publisher | | Marker file | books/Kickstarter Hauls/.container | Generic | | Folder-name suffix | books/Cyberpunk (parent-system)/ | Parent system | | Folder-name suffix | books/Jam Games (one-page)/ | One-page collection | | Folder-name suffix | books/Powered by the Apocalypse (system-family)/ | System family | | Folder-name suffix | books/Chaosium (publisher)/ | Publisher | | Folder-name suffix | books/My Shelf (container)/ | Generic | | Recognized name | books/One-Page RPGs/ | One-page collection |

The system-agnostic marker (.system-agnostic-container, or a (system-agnostic) suffix) is listed with these for consistency, but it is not a shelf of systems: it names the special collection described above, whose subfolders stay categories.

If a folder somehow carries more than one declaration, the most specific kind wins, in this order: parent system → one-page → system agnostic → system family → publisher → generic. Every recognized suffix is stripped from the stored name either way, so a stray (publisher) never shows up in the UI.

Naming. Child systems get a sensible default name - {Parent} {Edition} for parent systems, the prettified file/folder name for one-page games, and their own folder name for family, publisher, and generic children. Rename any of them in the UI and your name sticks: rescans never overwrite a system you've renamed, so "Dungeons & Dragons 2e" can become "Advanced Dungeons & Dragons".

Reorganizing an existing library. If you move a flat books/Dungeons & Dragons 5e/ into books/Dungeons & Dragons/5e/, the generated child name matches the system you already have - so Grimoire adopts that existing system rather than creating a duplicate. Its books, metadata, tags, and cover all follow it into the container.

Cover art. A container holds no books of its own, so there's no thumbnail to derive a cover from. Give it art either way:

books/
└── Dungeons & Dragons/
    ├── .parent-system-container
    ├── cover.jpg           ← folder artwork (cover. or folder.)
    └── 5e/

Or set one from the container's page (Cover image, GM/admin only) - upload a file, paste an image from your clipboard, or pick one Grimoire already has (see Setting images). A cover./folder. file in the library folder takes precedence over an upload, and both beat the book thumbnail an ordinary system falls back to. This works for any system, not just containers. A cover./folder. image at a system's folder root is artwork only - it is not also indexed as a book.

Grouping toggle. Containers are a way to organize the grid, not a cage. The Group collections switch beside the "Your Collection" heading (shown only when you actually have a container) flattens them: the container cards drop out and their child systems take their place, so you get a plain A-Z list of every real system with the usual sorting and filters applied. Switch it back on to return to the drill-down view. Your choice is remembered across sessions.

One-page collections are the deliberate exception - they stay grouped either way. Keeping a pile of tiny one-book games out of the main grid is the whole reason that collection exists, so flattening leaves its chip in the Special Collections strip and its games reachable by drilling in.

Note: systems nested inside a container count toward your library's game-system total. If you already used a One-Page RPGs folder, expect that number to rise after the first rescan as each game inside it becomes its own system.

Marking a system as explicit

Append (nsfw) to the folder name to mark all content in that system as explicit:

books/
└── Some Adult Game (nsfw)/
    └── core/
        └── rulebook.pdf

Users with explicit content disabled will not see this system or its books.

Alternatively, drop an empty .nsfw file at the system's root - useful when parenthesised folder names are awkward for your filesystem or sync tool:

books/
└── Some Adult Game/
    ├── .nsfw
    └── core/
        └── rulebook.pdf

Sort-order prefixes

To pull a system to the top of an alphabetically-sorted file browser, you can prefix its folder name with !, $, or %. Grimoire strips a leading run of those characters when deriving the system name (only the leading run - internal occurrences are kept):

books/
├── !!Dungeons & Dragons/   → "Dungeons & Dragons"
├── !system-agnostic/       → still the System-Agnostic collection
└── $%Pathfinder 2e/        → "Pathfinder 2e"

The prefix stacks with (nsfw), so !!Forbidden Lore (NSFW) becomes the explicit system "Forbidden Lore".

Book metadata from OPF files

Grimoire reads OPF sidecar files to populate book metadata automatically on first scan. OPF files are the format used by Calibre and many other library managers.

Supported fields

| OPF element | Book field | | ---------------------------------------- | ------------------------------------------------- | | dc:title | Title | | dc:creator (role=aut) | Authors | | dc:publisher | Publisher | | dc:date | Year (4-digit year extracted) | | dc:description | Description (HTML tags stripped) | | dc:subject | Tags (lowercased) | | dc:identifier with opf:scheme="ISBN" | ISBN (hyphens stripped, check digit validated) | | guide/reference[@type='cover'] | Cover image (file is excluded from the book list) |

dc:contributor entries (e.g. Calibre's own tool credit) are intentionally ignored, as are dc:identifier elements without opf:scheme="ISBN" — that filter is what keeps Calibre's internal UUIDs out of the ISBN field while letting a real ISBN (including one Grimoire's own sidecar export wrote) come back in. An ISBN whose check digit does not validate is dropped rather than stored. dc:language is parsed but not stored (no matching field).

OPF file discovery

The scanner checks two locations for each book file, in priority order:

  1. .opf - a sidecar file with the same stem as the PDF, in the same directory. Suits hand-crafted or single-file layouts.
  2. metadata.opf - a file named metadata.opf in the same directory. This is the format Calibre uses when it exports each book into its own subfolder.
A typical Calibre export looks like this and is fully supported:
books/
└── Dungeons & Dragons/
    └── core/
        ├── Players Handbook/
        │   ├── players_handbook.pdf
        │   ├── metadata.opf
        │   └── cover.jpg          ← skipped (referenced as cover in OPF)
        └── Dungeon Masters Guide/
            ├── dungeon_masters_guide.pdf
            ├── metadata.opf
            └── cover.jpg

OPF metadata is only applied when a book is first indexed, and ordinary rescans leave existing books alone, so edits made via the web UI are not overwritten. To pick up an OPF or tags.json you added or corrected after the initial scan, choose a metadata-refresh mode in the rescan dialog (available on the global Rescan button and every per-folder rescan button):

  • Find new files - the default: add new files, flag missing ones, leave existing records untouched.
  • Update missing metadata - additionally fill empty book fields from sidecar files, without touching anything you've already set (non-destructive).
  • Replace all metadata - overwrite fields with whatever the sidecar files provide (this discards UI edits the sidecar covers).

Writing metadata back out (sidecar export)

The reverse of the above: Grimoire can write its metadata _out_ as sidecar files next to your content, so the library folder describes itself. Copy the library to another machine, or rebuild the container with a fresh DATA_PATH, and the metadata travels with the files instead of living only in the app database. Other tools can read it too - Calibre, Jellyfin, Kodi, or a plain file manager.

This is off by default. Grimoire is otherwise a read-only viewer of your library, so writing into it is a deliberate opt-in.

Four formats, and you can enable any combination:

| Format | File written | Read by | | ------ | ---------------------- | ---------------------------------------------------------------------- | | OPF | .opf | Calibre - and Grimoire itself, so it round-trips | | NFO | .nfo | Jellyfin, Kodi, Emby | | JSON | .grimoire.json | Grimoire-native; lossless | | YAML | .grimoire.yaml | Grimoire-native; lossless, and the easiest to read or edit by hand |

OPF and NFO can only hold the fields their formats define, so anything without a slot is dropped. Enable JSON or YAML if you want a complete metadata backup rather than a feed for another app. Those two hold exactly the same fields and differ only in syntax, so there is little reason to enable both: pick YAML if you will read or edit the file yourself, JSON if a script will parse it. Optionally a cover image is written alongside as .cover.jpg.

Turn it on in Settings → Maintenance → Metadata Sidecars: tick the formats you want and save.

Three things trigger a write. The Export Metadata To Library button in that same section backfills the library, writing only the sidecars that are missing - so it is safe to re-run and will not overwrite one you have edited by hand. After that, any new book a library scan picks up gets its sidecars written automatically, and editing a book's metadata in the UI updates the sidecars it already has - creating none, so a library you have never backfilled never sprouts new files just because you renamed something.

Grimoire only overwrites files it wrote: every exported sidecar carries a marker, and a .opf you maintain in Calibre is left alone and reported as skipped unless you explicitly allow overwriting.

Sidecars are hidden in the File Manager - they describe your content rather than being content, and a book with four sidecars and a cover would otherwise show as six rows. They still travel with the file they belong to: move a book and its sidecars move too, rename it and they are renamed to match. A sidecar whose book no longer exists stays visible, so nothing vanishes with no way to reach it.

Sidecar export writes into your library folder, so it needs the library mounted writable - the default. If you have mounted it :ro, export reports that clearly and your metadata edits keep working; only the sidecar write is skipped. See Read-only or writable?.

Exported sidecars and covers are created with the container's UMASK applied, exactly like an uploaded file - with the default 022 that means rw-r--r--, readable by other users and other tools sharing the volume. Set UMASK on the container if your setup needs something different (Unraid users typically want 000 to get rw-rw-rw-).

See docs/sidecars.md for the full field mapping per format and how export precedence pairs with the refresh modes above.

Maps - organize by creator or collection

maps/
└── Creator Name/
    └── map-file.png

The folder name is shown as a group header in the map gallery. Both image maps and PDF maps (including multi-page PDFs) are supported and viewable in-app.

Animated maps (.webm, .mp4) and Universal VTT exports (.uvtt, .dd2vtt) sit alongside the stills they belong to - publishers commonly ship a looping video and a VTT data file next to each still variant - and both are viewable in-app. Animated maps play in the detail view on a muted loop with normal playback controls. Universal VTT files show the battlemap held inside them, and their detail panel lists the grid resolution plus the wall, door, and light counts the file carries for a virtual tabletop (see the format reference). Both get a gallery thumbnail like any other map: a Universal VTT because the image is right there in the file, and an animated map from a frame decoded a moment into the clip. Grimoire bundles a purpose-built, decode-only build of ffmpeg for this, trimmed to just the codecs a battlemap uses, so the feature costs about 5 MB of image size rather than the several hundred a stock ffmpeg would add. Existing libraries pick these up on the next rescan - any animated map still missing a thumbnail is retried, so you do not need to re-add anything.

Any image map can also be exported as a Universal VTT file, from the Download menu on its detail page. The .uvtt carries the map image and its grid, so it drops into Foundry, Roll20, or any VTT that reads the format with the grid already lined up, instead of you scaling the image by hand. Maps that already have a real .uvtt linked to them do not offer the option, since the file you already have carries walls and lighting of its own.

Drawing walls, doors, and lights

The export can carry more than the image and the grid. Edit VTT on a map's detail page opens a full-screen editor for the vision-blocking walls, doors, windows, and lights a virtual tabletop uses for dynamic lighting - the parts of the format that otherwise take a separate tool like Dungeondraft to produce.

Nothing you do in the editor touches your library. The walls and lights are saved against the map inside Grimoire, and the .uvtt is built fresh each time you export it: your original image is never modified, and no extra file appears next to it. That also means an edited map can be added to a campaign like any other, and exported from there whenever you need the file.

Editing a .uvtt you already have. A Universal VTT file is not just viewable - it opens in the editor too, on the walls, doors, and lights it already carries, so you can fix a wall that is in the wrong place rather than redrawing the map from scratch. The file on disk is never rewritten; your changes are saved against the map in Grimoire, and exporting gives you a new .uvtt carrying the original's own image alongside your edits. When a map image and a .uvtt are linked as versions of each other, Edit VTT asks which one you mean - drawing fresh geometry over the picture, or changing what the linked file already holds. Only PDFs and videos are left out, having no single image to draw on.

A map that is not in your library yet. The maps page has its own VTT Editor button, the same way the tokens page has a token editor. Drop in a map image or an existing .uvtt - from your desktop, a purchase you have not filed yet, anywhere - and edit it without adding anything to your library. When you are done, download the .uvtt, or send it straight into one of your campaigns as a linked resource, choosing the category it should be filed under. Nothing is written to your library either way, so this works exactly as well on a read-only mount.

Confirm the grid first. Everything you draw is measured in grid squares, so the editor opens on the grid rather than the drawing tools: your map appears with the grid Grimoire detected drawn over it, and you can see at a glance whether it lines up. Zoom in to check it closely - at fit-to-window on a large map, one screen pixel covers several of the image's. If it does not line up, the panel beside the map gives you two ways to fix it: say how many squares the map is across and down, which is how most people already know their grid and redraws the overlay as you type, or set the cell size and the grid's offset directly, with nudge buttons for walking an almost-right grid into place a pixel at a time.

Then draw:

  • Walls block line of sight. Click to place each corner and double-click (or press Enter) to finish the run; close a room by ending where you started. Snapping is a three-way choice - grid intersections, half squares for a diagonal or a split doorway, or free-form for an irregular cave wall.
  • Object walls are a separate layer for furniture, pillars, and other scatter. They are kept apart from ordinary walls because virtual tabletops treat them differently - Roll20, for instance, turns them into transparent barriers rather than solid walls.
  • Doors and windows have a button each, and are drawn as a two-click line across an opening. A door blocks sight until it is opened at the table; a window can be seen through but not walked through.
  • Lights are placed with a single click, and open for editing straight away - the panel at the top of the sidebar shows the new light's settings without you having to select it again. Start from a preset (candle, sconce, torch, brazier, campfire, moonlight, and a dozen more) picked from a menu or a row of colour swatches, then adjust the range in grid squares, the colour, the intensity, and whether it casts shadows. The preset values are taken from what Dungeondraft itself writes, so a torch starts out looking like one; tune any of them and the picker simply reads as Custom.
Whichever tool you pick, the panel on the right tells you how to drive it - how to finish a wall, how to abandon a half-drawn shape, which keys do what. Holding Alt places a single point off the grid without leaving your snap setting, for a room that is square apart from one canted corner.

The sidebar is a stack of collapsible sections. What you have selected sits at the top, since that is what you are looking at after a click; the tool help, layer counts and grid come next, and the player view and map-wide lighting settings anchor the bottom. Fold away anything you have stopped needing - the layer counts keep showing their total while collapsed - and selecting something on the map always reopens the selection panel.

Map-wide, you can set how bright an area with no light of its own looks, with a swatch showing the result, and mark a map whose lighting is already painted into the artwork. That last one matters: when it is set, a virtual tabletop may ignore or dim the lights you place, so Grimoire warns you if you have done both.

Checking it from a player's chair

Show player view drops a token on the map and darkens everything that token could not see - walls cast real shadows, a closed door hides the room behind it, and a window does not. It answers "can my players see around that corner?" in place, instead of by exporting, importing, and moving a token in another program to find out.

Drag the token, or walk it with the arrow keys (hold Shift for half a square). Its sight is described the way a virtual tabletop describes one, with three separate switches:

  • Vision is the master switch. Turn it off and the token sees nothing at all - which is what an object or scenery token is set to.
  • Night vision lets the token see without any light, out to a distance you set. This is darkvision, and it is the only one of the three that genuinely has a range. It is tinted a cool blue in the preview, so you can tell ground that is merely visible from ground that is actu
... (README truncated for length)
Chat with me