A personal anime and manga scoring CLI and REST server.
Fetches media metadata from AniList, walks you through a structured per-dimension scoring session, applies a weighted genre-adjusted formula, and publishes the final score back to your AniList account.
- Configurable dimensions — define any scoring dimensions you want in
config.toml; the engine has no hardcoded list - Genre-aware weights — multipliers shift dimension weights based on the media's AniList genres; averaged across all matched genres
- Bias-resistant dimensions — mark dimensions like Enjoyment as immune to genre adjustment
- Per-session overrides —
--weight pacing=0.05to nudge a specific show without touching your config - Skippable dimensions — enter
sat any prompt to mark a dimension as N/A - Full provenance — every result carries a per-dimension audit trail and a config hash
- REST server — same logic over HTTP for a web frontend, with Swagger UI included
- Optional scoring history — persist every session to SQLite or Postgres and unlock
history,stats,export, and DB-backed config editing (see Scoring History (optional) below)
kansou scores media through a four-step pipeline. The renormalization step is what makes it robust: skipping a dimension or applying a genre multiplier never silently distorts the other weights — the formula always rebalances.
Each active dimension starts from its configured base weight. If the media's genres match any configured genre rules, the dimension's weight is nudged by a genre multiplier.
Let
The effective (pre-normalisation) weight is:
Dimensions marked bias_resistant always use
Optional: primary genre blend. When --primary-genre is specified, one genre is designated as constitutive. Its multiplier is blended with the contributing-only average across the remaining genres at a configurable ratio primary_genre_weight, default
where
Skipped dimensions are removed from the pool entirely. The remaining effective weights are rescaled to sum exactly to
This is the core of the formula. Whether you skip two dimensions or five, whether genres push weights up or down, the active dimensions always share the full
--weight pacing=0.05 pins a dimension to an explicit value. Overridden dimensions are fixed; the remaining budget is distributed proportionally among the rest:
Each dimension's score is multiplied by its final renormalized weight. The result is a single number on the
Config has six dimensions with equal base weights of
| Dimension | Base W | Multiplier | Effective W | Renormalized W | |
|---|---|---|---|---|---|
| Story | 0.20 | 1 | ×1.50 | 0.300 | 0.294 |
| Characters | 0.20 | 0 | ×1.00 † | 0.200 | 0.196 |
| Pacing | 0.20 | 1 | ×0.80 | 0.160 | 0.157 |
| Enjoyment | 0.20 | — * | ×1.00 | 0.200 | 0.196 |
| Production | — | — | skipped | — | — |
| World Build | 0.20 | 0 | ×1.00 † | 0.200 | 0.196 |
| Total | 1.020 ✗ | 1.000 ✓ |
* Enjoyment is bias_resistant — genre rules never apply.
† No matched genre defined a multiplier for this dimension — contributing-only averaging returns
Without renormalization the weights would sum to
git clone https://github.com/kondanta/kansou
cd kansou
just buildOr with version stamping:
just build-releaseRequires Go 1.26+.
Copy the example config and edit to taste:
cp config.example.toml ~/.config/kansou/config.tomlThe tool runs with built-in defaults if no config file is found. See docs/CONFIG.md for the full schema.
Write operations (publish prompt in score add, POST /api/v1/score/publish) require an AniList user token:
export ANILIST_TOKEN=your_token_hereTo obtain a token:
- Go to https://anilist.co/settings/developer
- Create a client (redirect URI not needed for personal use)
- Authorise via:
https://anilist.co/api/v1/v2/oauth/authorize?client_id={id}&response_type=token - Copy the token from the redirect URL
Read operations (search, fetch) do not require a token.
What publish does and does not do: Publishing writes only the final numeric score to your AniList list entry. It does not change the entry's status (watching, completed, dropped, etc.). If the entry does not yet exist in your list, it is created with the score but no status set. Your watch/read status is always left as-is.
kansou [command]
Commands:
media find <query> Search AniList and display media info
score add <query> Start an interactive scoring session (includes publish prompt)
serve Start the REST server
history List/show/delete scoring history (requires KANSOU_DB_TYPE)
stats Scoring history statistics (requires KANSOU_DB_TYPE)
export Export history + stats to a self-contained HTML file (requires KANSOU_DB_TYPE)
db prune Hard-delete soft-deleted score records (requires KANSOU_DB_TYPE)
config View/edit scoring config (dimension/genre subcommands require KANSOU_DB_TYPE)
Global flags:
--config <path> Config file path (default: ~/.config/kansou/config.toml)
--version Print version and exit
kansou score add "Frieren: Beyond Journey's End"
kansou score add "frieren" --breakdown
kansou score add --url https://anilist.co/anime/154587 --breakdownAfter scoring, you'll be prompted:
Publish to AniList? [y/N]:
With per-session weight overrides:
kansou score add "Mushishi" --weight pacing=0.05,world_building=0.20kansou media find "Mushishi"
kansou media find --url https://anilist.co/anime/457Set KANSOU_DB_TYPE to sqlite or postgres to persist every scoring session
and unlock history, stats, export, and database-backed config editing. Unset
(the default), kansou is fully stateless — nothing in this section applies.
export KANSOU_DB_TYPE=sqlite # or postgres, with POSTGRES_HOST/USER/PASSWORD/DB set
kansou history # latest score per entry, newest first
kansou history show "frieren" # full breakdown + previous scores
kansou history delete "..." # remove the latest score from active tracking
kansou stats # one-line summary per category
kansou stats dimensions # variance, consistency, correlation, ...
kansou export # self-contained HTML export with charts
kansou db prune # permanently remove soft-deleted score records
kansou config show # works with or without a database
kansou config dimension add pacing --label Pacing --weight 0.1Rescoring an entry you've already scored pre-fills each dimension prompt with
its previous value. See docs/CLI.md for every subcommand and
docs/CONFIG.md for the full environment variable
reference and max_history retention semantics.
kansou serve
kansou serve --port 3000
kansou serve --live-config # enables GET /api/v1/config and POST /api/v1/config| Method | Path | Description |
|---|---|---|
GET |
/health |
Liveness check |
GET |
/api/v1/dimensions |
List configured scoring dimensions |
GET |
/api/v1/genres |
List configured genre multiplier blocks |
GET |
/api/v1/media/search?q={query} |
Search AniList by name |
GET |
/api/v1/media/{id} |
Fetch media by AniList ID |
POST |
/api/v1/score |
Calculate a weighted score |
POST |
/api/v1/score/publish |
Publish a score to AniList |
GET |
/api/v1/config † |
Return current mutable config as JSON (with config_hash) |
POST |
/api/v1/config † |
Replace mutable config, reload engine, persist to DB or disk |
GET |
/api/v1/db-info |
Always available — reports active DB backend or DBless status |
GET |
/api/v1/history ‡ |
Latest score per entry, newest first |
GET |
/api/v1/history/{anilist_id} ‡ |
All non-deleted scores for one entry, full breakdown |
DELETE |
/api/v1/history/{score_id} ‡ |
Soft-delete one score by its row ID |
GET |
/api/v1/stats ‡ |
One-line summary per category |
GET |
/api/v1/stats/genres ‡ |
Genre breakdown, score by genre, genre×dimension affinity |
GET |
/api/v1/stats/dimensions ‡ |
Variance, consistency, correlation, skip rate, weight overrides |
GET |
/api/v1/stats/history ‡ |
Most rescored, outliers, config impact |
† Only available when --live-config is set. Requires a writable config file path. See docs/CONFIG.md.
‡ Requires KANSOU_DB_TYPE to be set — returns HTTP 503 otherwise. See Scoring History (optional).
Swagger UI: http://localhost:8080/swagger/index.html
All errors return { "error": "description" }.
| Variable | Required | Description |
|---|---|---|
ANILIST_TOKEN |
For write ops | AniList user token |
LOG_LEVEL |
No | debug, info, warn, error (default: info) |
NO_COLOR |
No | Set to disable coloured CLI log output |
KANSOU_DB_TYPE |
No | sqlite or postgres — enables persistent history. Unset = fully stateless (DBless). |
KANSOU_DB_PATH |
No | SQLite file path (default: ~/.local/share/kansou/kansou.db). Only used when KANSOU_DB_TYPE=sqlite. |
POSTGRES_HOST/PORT/USER/PASSWORD/DB |
If postgres | Postgres connection parameters. Password is never logged. |
KANSOU_PORT |
No | REST server port (default: 8080). Overridden by --port. Replaces the deprecated [server] config section. |
KANSOU_CORS_ORIGINS |
No | Comma-separated CORS allowed origins. Replaces the deprecated [server] config section. |
Full reference, including max_history retention semantics: docs/CONFIG.md.
A chart is published at oci://ghcr.io/kondanta/charts/kansou:
helm install kansou oci://ghcr.io/kondanta/charts/kansou \
--version <chart-version> \
--set anilistToken=<token>Persistent scoring history is opt-in via db.type:
# SQLite, backed by a PVC mounted at /data
--set db.type=sqlite
# Postgres
--set db.type=postgres \
--set db.postgres.host=<host> \
--set db.postgres.user=<user> \
--set db.postgres.database=<db> \
--set db.postgres.password=<password>See charts/kansou/values.yaml for the full set of
values, and docs/CONFIG.md for how they
map to the environment variables above.
just build # build binary
just build-release # build with git version stamp
just test # run tests
just test-race # run tests with race detector
just vet # go vet
just lint # golangci-lint
just ci # build + test (race) + vet + lint — full definition-of-done gate
just ci-local # same as ci, but forces a fresh, uncached test run
just swagger # regenerate Swagger docs after handler changes
just run -- <args> # run via go run
just serve # start server via go run
just clean # remove built binary
internal/store/postgres tests use a real, ephemeral Postgres container via
testcontainers-go — they skip cleanly
(not fail) when Docker isn't running.
| Document | Contents |
|---|---|
docs/REQUIREMENTS.md |
Functional and non-functional requirements |
docs/ADR.md |
Architecture decision records |
docs/CONFIG.md |
Full config schema reference |
docs/CLI.md |
CLI command reference |
docs/ANILIST_INTEGRATION.md |
AniList GraphQL integration details |
ARCHITECTURE.md |
Package structure and data flow |
Licensed under either of:
at your option.
Copyright (c) 2026 Taylan Dogan