Development¶
Requires Bun 1.4.
bun install
bun run dev # TanStack Start on http://localhost:8420 (NAMARR_DEMO=1 for the demo catalog)
bun run test # Vitest: core, providers, db, server (under the Bun runtime because of bun:sqlite)
bun run e2e # build + Playwright: workbench flows against the demo backend with fake files
bun run lint # Biome
bun run typecheck
bun run corpus # parser against the release name corpus, hit rate per category
bun run build && bun run start
Migrations: change the schema in packages/db/src/schema.ts, then run
bun run --cwd packages/db generate. They run automatically at startup.
Texts: all texts live in apps/server/messages/{de,en}.json
(Paraglide JS), keys follow
area_group_name (settings_title, rules_describe_pad). bun run i18n compiles them to
apps/server/src/paraglide (dev, build, typecheck and test do that on their own). Components call
them directly: m.settings_title(), m.workbench_run({ n, count, test }); a key chosen at runtime
goes through pickMsg(msgGroup.actions, action). Placeholders are {name}, a literal brace is \{.
Texts the server produces (match reasons, file errors, provider errors) use msg("key", inputs)
from @namarr/core/i18n: they travel as key and inputs and are rendered in the viewer's language by
the UI (useLocalize()), or by the server where it answers itself (localizeIn(), notifications
in the title language). Entries stored by older versions, with both texts side by side, still render.
The first visit follows the browser language (everything but German → English), later the switch in
the navigation or the settings; the choice is kept in the namarr_lang cookie, and every request is
rendered in it (lang.server.ts). apps/server/test/i18n.test.ts checks that both languages have
the same keys and placeholders, that every message is used and every used key exists, and that no
German is left in English.
Architecture¶
packages/
core/ domain logic without a framework: parser, matcher, formatter, rules, scanner, fileops, jobs
providers/ TMDB, TheTVDB, TVmaze, AniDB (cache, rate limit, episode orders), demo catalog
db/ Drizzle schema, migrations, repositories (bun:sqlite)
apps/
server/ TanStack Start: routes, server functions (thin adapters with Zod), server routes, worker
scripts/ corpus.ts, the parser benchmark
docker/ Dockerfile, compose example, Quadlet, Unraid template
One Bun process: apps/server/server.ts runs the migrations at startup and starts the job queue and
the watch folders exactly once (a singleton on globalThis, also in Vite dev mode). Server functions
only create jobs; scanning, matching and execution run in the queue, progress arrives over SSE. The
workbench renders on the client (ssr: false).
The build writes .br and .gz next to the client assets (apps/server/scripts/precompress.ts);
server.ts serves the variant the browser accepts, answers HEAD and 304 (ETag, Last-Modified) and
gzips the SSR HTML. Route search params are read without zod (lib/search.ts), so zod stays out of
the initial bundle.
Tests¶
| Area | What is tested |
|---|---|
| Parser | every rule on its own, folder context, IDs in folder names, snapshots, YAML corpus |
| Matcher | Jaro-Winkler, year, ambiguity (The Office US/UK), grouping (one search per series), overrides, absolute numbers, lookups by ID, one entry per season |
| Formatter / rules | template language incl. error positions, presets, snapshots; property tests (fast-check): sanitising is idempotent and never produces invalid paths, rules never change the extension |
| FileOps | in temporary folders: all actions, conflicts, quality comparison for keep better, backup on overwrite, undo refused for changed files, cancel in the middle of a job |
| Providers | TMDB, TheTVDB, TVmaze and AniDB against responses in their documented format (no live API), cache TTL, 429 retry, login, error messages, IDs from folder names |
| DB / server | migrations, paging, inbox, dashboard numbers; job pipeline end to end, watch folders with real files, download client hook, auth, SSE, notifications |
| E2E | Playwright: preview, match picker, approval, formats, undo, rule mode, history, hook, English and German UI |
Parser corpus¶
bun run corpus, currently:
| Category | Names | Correct | Hit rate |
|---|---|---|---|
| Anime | 7 | 7 | 100.0 % |
| German | 16 | 16 | 100.0 % |
| Movies | 29 | 29 | 100.0 % |
| Series | 25 | 25 | 100.0 % |
| Total | 77 | 77 | 100.0 % |
The corpus is a start (the goal is 500+ real names) and was written together with the parser, so the
100 % says little. Every reported miss becomes a new entry in
packages/core/test/corpus/releases.yaml.
Screenshots¶
The README pictures docs/screenshot-dashboard.png and docs/screenshot-workbench.png come from the
demo backend, in English: apps/server/scripts/screenshots.spec.ts sets up two libraries, two watch
folders and three download-client jobs (one renamed, two in the inbox) on the fake files of
scripts/screenshots-setup.ts, then takes the workbench with a season of Severance and the
dashboard. bun run docs:screenshots builds, takes them and renders docs/social-preview.png from
them (bun run docs:social-preview alone renders only that). The fake files go to /data, like in
the container, so the paths in the pictures read as in real use; SHOTS_DATA=/tmp/namarr-shots
puts them elsewhere.
After a change to the look, run Update screenshots (Actions → Run workflow,
.github/workflows/screenshots.yml) on the branch: it takes the pictures in the official
Playwright image and commits the ones that changed.
Releases¶
A version is a tag. Without a checkout: Actions → Bump version → Run workflow with patch, minor or
major (bump.yml, the shared
bump-version); it raises the version, commits it
as Release vX.Y.Z, tags it and starts the release. Or on an up-to-date main, with a clean working
tree:
This runs npm version, which writes the new number into package.json, commits it and creates the
tag vX.Y.Z from the same number; the postversion script then pushes commit and tag
(git push --follow-tags).
The tag push starts the Release workflow (CI here, the rest from the shared
docker-release.yml in firsttris/workflows):
- the tag must match
versionin the rootpackage.json, - the full CI (lint, typecheck, tests, parser benchmark, build, E2E),
- the image as
x.y.z,x.yandlateston Docker Hub and GHCR, plus the Docker Hub description, - only then the GitHub release with generated notes.
Started by hand on main (Actions → Release → Run workflow), the workflow runs the same checks and
pushes the image as edge; nothing is released.
CI runs lint, typecheck, tests, the parser benchmark, the build and the E2E tests on every push to
main and on every pull request.
Roadmap¶
- M0 foundation, M1 core, M2 web UI and Docker: done.
- M3 automation: watch folders, inbox, learned overrides, library refresh, notifications and the download client hook are done.
- M4 rule mode: rule engine, rule stack UI, media + rules and YAML export and import are done.
- M5 anime: absolute numbers, TMDB episode groups, TheTVDB and AniDB are done; mapping AniDB entries onto TVDB seasons (anime lists) is open.
- M6 libraries and formats: folders with movie and series libraries, naming formats instead of profiles and detecting the format of an existing library are done.
Still open: a license, name reservation, a desktop app.