{"id":47750915,"url":"https://github.com/kondanta/kansou","last_synced_at":"2026-04-05T21:17:27.818Z","repository":{"id":348829563,"uuid":"1199998026","full_name":"kondanta/kansou","owner":"kondanta","description":"Weighted anime \u0026 manga scoring CLI with AniList integration. Opinionated by default, configurable without recompiling.","archived":false,"fork":false,"pushed_at":"2026-04-03T02:18:44.000Z","size":122,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-04-03T11:17:40.544Z","etag":null,"topics":["anilist","anime","cli","go","manga","scoring"],"latest_commit_sha":null,"homepage":"","language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/kondanta.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-04-02T23:42:14.000Z","updated_at":"2026-04-03T02:18:48.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/kondanta/kansou","commit_stats":null,"previous_names":["kondanta/kansou"],"tags_count":4,"template":false,"template_full_name":null,"purl":"pkg:github/kondanta/kansou","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kondanta%2Fkansou","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kondanta%2Fkansou/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kondanta%2Fkansou/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kondanta%2Fkansou/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/kondanta","download_url":"https://codeload.github.com/kondanta/kansou/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kondanta%2Fkansou/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":31450287,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-05T15:22:31.103Z","status":"ssl_error","status_checked_at":"2026-04-05T15:22:00.205Z","response_time":75,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.5:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["anilist","anime","cli","go","manga","scoring"],"created_at":"2026-04-03T03:05:59.977Z","updated_at":"2026-04-05T21:17:27.813Z","avatar_url":"https://github.com/kondanta.png","language":"Go","funding_links":[],"categories":[],"sub_categories":[],"readme":"# kansou (感想)\n\nA personal anime and manga scoring CLI and REST server.\n\nFetches media metadata from [AniList](https://anilist.co), 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.\n\n---\n\n## Features\n\n- **Configurable dimensions** — define any scoring dimensions you want in `config.toml`; the engine has no hardcoded list\n- **Genre-aware weights** — multipliers shift dimension weights based on the media's AniList genres; averaged across all matched genres\n- **Bias-resistant dimensions** — mark dimensions like Enjoyment as immune to genre adjustment\n- **Per-session overrides** — `--weight pacing=0.05` to nudge a specific show without touching your config\n- **Skippable dimensions** — enter `s` at any prompt to mark a dimension as N/A\n- **Full provenance** — every result carries a per-dimension audit trail and a config hash\n- **REST server** — same logic over HTTP for a web frontend, with Swagger UI included\n\n---\n\n## How It Works\n\nkansou 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.\n\n### Step 1 — Effective weight\n\nEach 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**.\n\nLet $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):\n\n$$\\bar{m}_i = \\begin{cases} \\dfrac{1}{|G_i|} \\displaystyle\\sum_{g \\in G_i} m_{g,i} \u0026 \\text{if } |G_i| \u003e 0 \\\\ 1.0 \u0026 \\text{if } |G_i| = 0 \\end{cases}$$\n\nThe effective (pre-normalisation) weight is:\n\n$$w_{\\text{eff},i} = w_{\\text{base},i} \\times \\bar{m}_i$$\n\nDimensions marked `bias_resistant` always use $\\bar{m}_i = 1.0$ — genre rules never touch them.\n\n**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$):\n\n$$\\bar{m}_i = \\beta \\cdot m_{\\text{primary},i} + (1-\\beta) \\cdot \\bar{m}^{\\text{secondary}}_i$$\n\nwhere $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.\n\n### Step 2 — Renormalization\n\nSkipped dimensions are removed from the pool entirely. The remaining effective weights are rescaled to sum exactly to $1.0$:\n\n$$w'_i = \\frac{w_{\\text{eff},i}}{\\displaystyle\\sum_{j \\in \\text{active}} w_{\\text{eff},j}}$$\n\nThis 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.\n\n### Step 3 — Per-session overrides (optional)\n\n`--weight pacing=0.05` pins a dimension to an explicit value. Overridden dimensions are fixed; the remaining budget is distributed proportionally among the rest:\n\n$$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}$$\n\n### Step 4 — Final score\n\n$$\\text{score} = \\sum_i s_i \\times w''_i \\qquad s_i \\in [1, 10]$$\n\nEach dimension's score is multiplied by its final renormalized weight. The result is a single number on the $[1, 10]$ scale.\n\n### Example\n\nConfig 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*.\n\n| Dimension   | Base W | $G_i$ | Multiplier | Effective W | Renormalized W |\n|-------------|--------|--------|-----------|-------------|----------------|\n| Story       | 0.20   | 1      | ×1.50     | 0.300       | **0.294**      |\n| Characters  | 0.20   | 0      | ×1.00 †   | 0.200       | **0.196**      |\n| Pacing      | 0.20   | 1      | ×0.80     | 0.160       | **0.157**      |\n| Enjoyment   | 0.20   | —  *   | ×1.00     | 0.200       | **0.196**      |\n| Production  | —      | —      | skipped   | —           | —              |\n| World Build | 0.20   | 0      | ×1.00 †   | 0.200       | **0.196**      |\n| **Total**   |        |        |           | **1.020** ✗ | **1.000** ✓    |\n\n\\* Enjoyment is `bias_resistant` — genre rules never apply.  \n† 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.\n\nWithout 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.\n\n---\n\n## Installation\n\n```bash\ngit clone https://github.com/kondanta/kansou\ncd kansou\njust build\n```\n\nOr with version stamping:\n\n```bash\njust build-release\n```\n\nRequires Go 1.26+.\n\n---\n\n## Configuration\n\nCopy the example config and edit to taste:\n\n```bash\ncp config.example.toml ~/.config/kansou/config.toml\n```\n\nThe tool runs with built-in defaults if no config file is found. See [`docs/CONFIG.md`](docs/CONFIG.md) for the full schema.\n\n---\n\n## AniList Token\n\nWrite operations (publish prompt in `score add`, `POST /score/publish`) require an AniList user token:\n\n```bash\nexport ANILIST_TOKEN=your_token_here\n```\n\nTo obtain a token:\n1. Go to https://anilist.co/settings/developer\n2. Create a client (redirect URI not needed for personal use)\n3. Authorise via: `https://anilist.co/api/v2/oauth/authorize?client_id={id}\u0026response_type=token`\n4. Copy the token from the redirect URL\n\nRead operations (search, fetch) do not require a token.\n\n\u003e **What publish does and does not do:**\n\u003e Publishing writes only the final numeric score to your AniList list entry.\n\u003e It does **not** change the entry's status (watching, completed, dropped, etc.).\n\u003e If the entry does not yet exist in your list, it is created with the score but no status set.\n\u003e Your watch/read status is always left as-is.\n\n---\n\n## CLI Usage\n\n```\nkansou [command]\n\nCommands:\n  media find \u003cquery\u003e    Search AniList and display media info\n  score add \u003cquery\u003e     Start an interactive scoring session (includes publish prompt)\n  serve                 Start the REST server\n\nGlobal flags:\n  --config \u003cpath\u003e       Config file path (default: ~/.config/kansou/config.toml)\n  --version             Print version and exit\n```\n\n### Score a show\n\n```bash\nkansou score add \"Frieren: Beyond Journey's End\"\nkansou score add \"frieren\" --breakdown\nkansou score add --url https://anilist.co/anime/154587 --breakdown\n```\n\nAfter scoring, you'll be prompted:\n\n```\nPublish to AniList? [y/N]:\n```\n\nWith per-session weight overrides:\n\n```bash\nkansou score add \"Mushishi\" --weight pacing=0.05,world_building=0.20\n```\n\n### Look up media without scoring\n\n```bash\nkansou media find \"Mushishi\"\nkansou media find --url https://anilist.co/anime/457\n```\n\n---\n\n## REST Server\n\n```bash\nkansou serve\nkansou serve --port 3000\n```\n\n| Method | Path | Description |\n|--------|------|-------------|\n| `GET` | `/health` | Liveness check |\n| `GET` | `/dimensions` | List configured scoring dimensions |\n| `GET` | `/genres` | List configured genre multiplier blocks |\n| `GET` | `/media/search?q={query}` | Search AniList by name |\n| `GET` | `/media/{id}` | Fetch media by AniList ID |\n| `POST` | `/score` | Calculate a weighted score |\n| `POST` | `/score/publish` | Publish a score to AniList |\n\nSwagger UI: `http://localhost:8080/swagger/index.html`\n\nAll errors return `{ \"error\": \"description\" }`.\n\n---\n\n## Environment Variables\n\n| Variable | Required | Description |\n|----------|----------|-------------|\n| `ANILIST_TOKEN` | For write ops | AniList user token |\n| `LOG_LEVEL` | No | `debug`, `info`, `warn`, `error` (default: `info`) |\n| `NO_COLOR` | No | Set to disable coloured CLI log output |\n\n---\n\n## Development\n\n```\njust build          # build binary\njust build-release  # build with git version stamp\njust test           # run tests\njust test-race      # run tests with race detector\njust vet            # go vet\njust check          # build + test + vet (full definition-of-done gate)\njust swagger        # regenerate Swagger docs after handler changes\njust run -- \u003cargs\u003e  # run via go run\njust serve          # start server via go run\njust clean          # remove built binary\n```\n\n---\n\n## Docs\n\n| Document | Contents |\n|----------|----------|\n| [`docs/REQUIREMENTS.md`](docs/REQUIREMENTS.md) | Functional and non-functional requirements |\n| [`docs/ADR.md`](docs/ADR.md) | Architecture decision records |\n| [`docs/CONFIG.md`](docs/CONFIG.md) | Full config schema reference |\n| [`docs/CLI.md`](docs/CLI.md) | CLI command reference |\n| [`docs/ANILIST_INTEGRATION.md`](docs/ANILIST_INTEGRATION.md) | AniList GraphQL integration details |\n| [`ARCHITECTURE.md`](ARCHITECTURE.md) | Package structure and data flow |\n\n---\n\n## License\n\nLicensed under either of:\n\n- [MIT License](LICENSE-MIT)\n- [Apache License, Version 2.0](LICENSE-APACHE)\n\nat your option.\n\nCopyright (c) 2026 Taylan Dogan\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fkondanta%2Fkansou","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fkondanta%2Fkansou","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fkondanta%2Fkansou/lists"}