I have a problem. Well. I have many problems. But I’m specifically referring to this one: I have too many TTRPG PDFs. Not “too many” in the “I wish I owned fewer” sense. I came by these honestly. Through backing so many Kickstarters that they sent me both the T-shirt and the coffee mug.

What I mean is “too many” in the “how am I ever going to find the one I’m looking for” sense.

To make the problem worse, they’re spread across a few different distribution platforms. Because crowdfunding.

My solution, for the last decade or so, has been to download everything to a local copy, in one big undifferentiated folder. Some might call this “not having a solution.” Those people are nasty hobbitses who need to give me back my ring.

But also, they’re right.

It occurred to me recently, in the way that sometimes blindingly obvious things take a minute to become blindingly obvious, that probably I could Build A Tool About This. Lots of what I do these days is build tools. Claude (or Codex, or Gemini, or …) makes this trivial to do, for those who have the eyes to see.

The data analytics people, who are both much smarter and much handsomer than I, often talk about data processing pipelines as something sort of like a ladder:

  1. You start with “bronze,” the messy undifferentiated data that’s not entirely unlike seawater. Filled with microplastics, sure, but somewhere in the briny depths also a vast amount of delicious krill.

  2. You do some filtering, you do some scrubbing, you do some dark rituals to ineffable beings from beyond comprehension, and you get “silver.” The silver tables are basically raw data if raw data made sense or could be used for anything. It’s the post-baleen krill story.

  3. You take those silver tables, create some standard ways to slice, dice, and even julienne-fry, and you get “gold.” The version of the data that’s actually useful to someone for something.

But I promise, dear reader (who, like the data analytics people, is both much smarter and handsomer than I), this is not secretly a lesson on data analytics, a topic about which I know approximately nothing. No. This is about me absconding wholesale with someone else’s good idea to do my nasty work.

So I thought to myself, “self,” I thought. “What if I did something like what I just said but for PDFs of elf games on my sand-thinker what drives the information superhighway.” And then, “yes, good.” And then I thought the version of me in my internal monologue was both smarter and handsomer than I.

What followed is a tool I call athanor which sounds like it should be a character from The Lord of the Rings but is actually an alchemical furnace. Only in this case, it’s an ETL (extract, transform, load) pipeline for the contents of my RPG library.

“Bronze” is one or more folders containing TTRPGs.

[[sources]]
name = "DTRPG"
path = "~/Library/DTRPG"

[[sources]]
name = "Crowdfunding"
path = "~/Library/Crowdfunding"

Then a series of rules selects across the pool of sources, starting with collection-wide rules that get progressively narrower to match specific systems or books as needed.

# ============================================================================================
# Library-wide heuristics
# ============================================================================================

# --- Never shelved -------------------------------------------------------------------------

[[rules]]
match = "**/*.epub"
skip = "epub copy; the PDF is shelved"

[[rules]]
match = ["**/*pread*", "**/*preads/**"]
skip = "spreads layout"

[[rules]]
match = ["**/*3PP*", "**/*3PP*/**", "**/*LICENSE*"]
skip = "license notice"

[…]

# --- Exalted Funeral ---

[[rules]]
in = "Crowdfunding/Exalted Funeral"
files = [
  { match = "Abyss of Hallucinations*/**", game = "Abyss of Hallucinations" },
  {
    match = "Abyss of Hallucinations*/*/*/Abyss of Hallucinations_Collected Edition_Pages_*.pdf",
    title = "Abyss of Hallucinations (Collected Edition)"
  },
  {
    match = "Abyss of Hallucinations*/Abyss_of_Hallucinations_Players_Guide_*.pdf",
    title = "Abyss of Hallucinations Player's Guide"
  },
  {
    match = "Abyss of Hallucinations*/Rebis_of_Baphomet_*.pdf",
    title = "Rebis of Baphomet - Field of Night"
  },
  { match = "Abyss of Hallucinations*/Wound_of_Amfortas_*.pdf", title = "Wound of Amfortas" },
  { match = "Dolmenwood/**", game = "Dolmenwood" },
  { match = "Dolmenwood/Dolmenwood_Player_s_Book.pdf", title = "Dolmenwood Player's Book" },
  { match = "Dolmenwood/Winter's Daughter/*.pdf", title = "Winter's Daughter" },
  {
    match = "His Majesty the Worm/*.pdf",
    game = "His Majesty the Worm",
    title = "His Majesty the Worm"
  },
  { match = "Mothership/*.pdf", game = "Mothership", title = "Mothership Zine Rulebook" },
  {
    match = "The Castle Automatic/*.pdf",
    game = "The Castle Automatic",
    title = "The Castle Automatic"
  },
]

“Silver” is a .plan.json file which tracks every single file in the inputs and where they’re going to end up in the outputs. It’s not meant to be read by humans, but for example’s sake:

    {
      "item": "Crowdfunding/Exalted Funeral/His Majesty the Worm/His_Majesty_the_Worm_digital_edition_6-23-24.pdf",
      "state": "placed",
      "file": "[...]/Library/Crowdfunding/Exalted Funeral/His Majesty the Worm/His_Majesty_the_Worm_digital_edition_6-23-24.pdf",
      "member": null,
      "game": "His Majesty the Worm",
      "title": "His Majesty the Worm",
      "kind": null,
      "skip": null,
      "conflict": false,
      "sha256": "95d75268cae90b9968fa527026bb2b6329ef74d9239fe81c1a0c78c706c8b4fc",
      "size": 345202449,
      "destination": "His Majesty the Worm/His Majesty the Worm.pdf"
    },

And all of this work gets us to “gold.” The clean, sorted, organized shelf of TTRPGs ready for me to push to my iPad or print, or read at my leisure. Organized by game system instead of three levels deep in DriveThruRPG’s preferred structure.

Now I can get back to the very important work, and grand tradition, of all TTRPG nerds: doing anything other than actually running a campaign.