Skip to content

kondanta/kansou

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

140 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

kansou (感想)

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.


Features

  • 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.05 to nudge a specific show without touching your config
  • Skippable dimensions — enter s at 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)

How It Works

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.

Step 1 — Effective weight

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 $G_i \subseteq G$ be the subset of matched genres that explicitly define a multiplier for dimension $i$. Genres that have no configured entry for dimension $i$ are excluded entirely — they do not contribute a diluting $1.0$ to the average (Option B, see ADR-021):

$$\bar{m}_i = \begin{cases} \dfrac{1}{|G_i|} \displaystyle\sum_{g \in G_i} m_{g,i} & \text{if } |G_i| > 0 \ 1.0 & \text{if } |G_i| = 0 \end{cases}$$

The effective (pre-normalisation) weight is:

$$w_{\text{eff},i} = w_{\text{base},i} \times \bar{m}_i$$

Dimensions marked bias_resistant always use $\bar{m}_i = 1.0$ — genre rules never touch them.

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 $\beta$ (primary_genre_weight, default $0.6$):

$$\bar{m}_i = \beta \cdot m_{\text{primary},i} + (1-\beta) \cdot \bar{m}^{\text{secondary}}_i$$

where $m_{\text{primary},i}$ is the primary genre's multiplier for dimension $i$ (or $1.0$ if it has no entry), and $\bar{m}^{\text{secondary}}_i$ is the contributing-only average over non-primary matched genres. Setting $\beta = 0$ disables the feature.

Step 2 — Renormalization

Skipped dimensions are removed from the pool entirely. The remaining effective weights are rescaled to sum exactly to $1.0$:

$$w'_i = \frac{w_{\text{eff},i}}{\displaystyle\sum_{j \in \text{active}} w_{\text{eff},j}}$$

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 $[0, 1]$ budget proportionally. Nothing leaks, nothing inflates.

Step 3 — Per-session overrides (optional)

--weight pacing=0.05 pins a dimension to an explicit value. Overridden dimensions are fixed; the remaining budget is distributed proportionally among the rest:

$$w''_k = \frac{w'_k}{\displaystyle\sum_{j \notin \text{pinned}} w'_j} \times \left(1 - \sum_{i \in \text{pinned}} w^*_i\right) \quad \text{for } k \notin \text{pinned}$$

Step 4 — Final score

$$\text{score} = \sum_i s_i \times w''_i \qquad s_i \in [1, 10]$$

Each dimension's score is multiplied by its final renormalized weight. The result is a single number on the $[1, 10]$ scale.

Example

Config has six dimensions with equal base weights of $0.20$. The media matches one genre that defines a $1.5\times$ multiplier on Story and a $0.8\times$ multiplier on Pacing — it has no entry for Characters or World Building. The user skips Production.

Dimension Base W $G_i$ 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 $1.0$ (neutral), not an average diluted by a phantom $1.0$ contribution.

Without renormalization the weights would sum to $1.02$ and the score would be silently inflated. With renormalization, the active pool is always $1.0$ and the result is honest.


Installation

git clone https://github.com/kondanta/kansou
cd kansou
just build

Or with version stamping:

just build-release

Requires Go 1.26+.


Configuration

Copy the example config and edit to taste:

cp config.example.toml ~/.config/kansou/config.toml

The tool runs with built-in defaults if no config file is found. See docs/CONFIG.md for the full schema.


AniList Token

Write operations (publish prompt in score add, POST /api/v1/score/publish) require an AniList user token:

export ANILIST_TOKEN=your_token_here

To obtain a token:

  1. Go to https://anilist.co/settings/developer
  2. Create a client (redirect URI not needed for personal use)
  3. Authorise via: https://anilist.co/api/v1/v2/oauth/authorize?client_id={id}&response_type=token
  4. 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.


CLI Usage

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

Score a show

kansou score add "Frieren: Beyond Journey's End"
kansou score add "frieren" --breakdown
kansou score add --url https://anilist.co/anime/154587 --breakdown

After 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.20

Look up media without scoring

kansou media find "Mushishi"
kansou media find --url https://anilist.co/anime/457

Scoring History (optional)

Set 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.1

Rescoring 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.


REST Server

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" }.


Environment Variables

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.


Helm Chart

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.


Development

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.


Docs

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

License

Licensed under either of:

at your option.

Copyright (c) 2026 Taylan Dogan

About

Weighted anime & manga scoring CLI with AniList integration. Opinionated by default, configurable without recompiling.

Topics

Resources

License

Unknown, MIT licenses found

Licenses found

Unknown
LICENSE-APACHE
MIT
LICENSE-MIT

Stars

0 stars

Watchers

0 watching

Forks

Packages

 
 
 

Contributors

Languages