Sign inSign up

dyphire/komf-rs

By dyphire

•Updated 1 day ago

Komga, Kavita and Stump metadata fetcher

Image
0

3.9K

dyphire/komf-rs repository overview

⁠Komga, Kavita and Stump Metadata Fetcher (Rust)

English | 简体中文⁠

This is the Rust implementation of komf⁠, a tool that fetches metadata and thumbnails for your digital comic book library. It automatically picks up added series in Komga, Kavita and Stump and updates their metadata, thumbnails and book-level data. You can also manually search, identify and match series — per series, per library, or for the whole library.

⁠Status

  • Komga: REST API + SSE event listener (auto-update, notifications)
  • Kavita: REST API + SignalR event listener (JWT auth, auto-refresh)
  • Stump (Rust-only): GraphQL API + GraphQL WebSocket event listener (auto-update with job-based event batching, API-key or JWT auth)
  • 12 metadata providers implemented, configurable per provider and per library
  • Discord webhook + Apprise notifications with Velocity templates
  • ComicInfo reading/writing, book ordering, score tags, reading direction override
  • Config hot-reload (PATCH /api/config), job tracking, metadata search/identify/match/reset endpoints
  • userscript compatible configuration UI
  • Built-in WebUI workbench (Rust-only): 12-provider matrix, per-library overrides, notification template editor, jobs with live SSE progress, search trial with one-click identify, tracker page (AniList / MAL / Bangumi reading-status sync), offline DB download, light/dark theme
  • OAuth login for MAL / AniList / MangaBaka / Bangumi (Rust-only) : server-side OAuth2 with a shared client + official relay page — no per-instance callback registration needed; login/logout/status in the Providers page, token auto-refresh and SQLite persistence
  • Reading-list tracker sync (Rust-only) : AniList / MyAnimeList / MangaBaka / Bangumi reading-status sync with a WebUI page and /api/tracker/* endpoints — search by title or platform link, tracked marking, state read and status/score/progress push

⁠Metadata providers

ProviderSearchSeries metadataBook metadata
MangaUpdates✅✅—
MyAnimeList✅✅✅
AniList✅✅—
MangaDex✅✅✅
BookWalker✅✅✅
Bangumi (bgm.tv)✅✅✅
ComicVine✅✅✅
YenPress✅✅✅
Viz✅✅✅
Webtoons✅✅✅
MangaBaka✅✅—
eHentai (Rust-only)✅✅—
Kodanshaplaceholder (unsupported in Kotlin too)
Nautiljonplaceholder (unsupported in Kotlin too)
Hentagplaceholder (unsupported in Kotlin too)

Providers can be configured globally (metadataProviders.defaultProviders) or per library (metadataProviders.libraryProviders). Each provider supports priority, enable/disable, media type filtering (MANGA/NOVEL/COMIC/WEBTOON), author/artist role mapping, per-field series/book metadata toggles, and provider-specific options (e.g. coverLanguages, tagsScoreThreshold, preferredLanguages). Kodansha, Nautiljon and Hentag are recognized in the configuration for compatibility, but the Kotlin version maps them to error("Unsupported") and the Rust version simply does not register them — enabling them has no effect.

⁠Building

Requirements: Rust⁠ (stable toolchain).

cargo build --release # builds the release binary at target/release/komf-app
cargo test --workspace # run unit tests

OAuth client_secret values are build-time injected (not runtime env vars): set KOMF_OAUTH_ANILIST_CLIENT_SECRET / KOMF_OAUTH_MAL_CLIENT_SECRET / KOMF_OAUTH_BANGUMI_CLIENT_SECRET when building and they are compiled into the binary via option_env!. Released binaries/images already carry them; see docs/oauth-relay/README.md⁠.

⁠Running

The repository does not ship application.yml (to avoid committing real credentials); use the template instead:

cp examples/application.example.yml application.yml # Linux/macOS
copy examples/application.example.yml application.yml # Windows
# edit application.yml: Komga/Kavita/Stump credentials, providers, metadata update, ...
./komf-app [path to config] # path to application.yml or its directory

The template (application.example.yml) contains every option with inline comments; sensitive fields (Komga user/password/API key, e-hentai/exhentai cookies) default to empty/placeholder values.

If no path is given, the KOMF_CONFIG_DIR environment variable is used. If neither is set, the service starts with built-in defaults (HTTP on 8085 only, no media-server credentials); configuration changed via PATCH /api/config is written back to ./application.yml (created if missing), and the database defaults to ./database.sqlite.

Environment variables (same as the Kotlin version):

VariableDescription
KOMF_KOMGA_BASE_URIKomga base URL
KOMF_KOMGA_USER / KOMF_KOMGA_PASSWORDKomga basic auth
KOMF_KOMGA_API_KEYKomga API key (X-API-Key auth, takes precedence when set)
KOMF_KAVITA_BASE_URI / KOMF_KAVITA_API_KEYKavita base URL + API key
KOMF_STUMP_BASE_URI / KOMF_STUMP_API_KEYStump base URL + API key (stump_ prefix, takes precedence when set)
KOMF_STUMP_USER / KOMF_STUMP_PASSWORDStump account password (only used to exchange a JWT when no API key is set)
KOMF_SERVER_PORTHTTP port (default 8085, restart required)
KOMF_SERVER_BINDHTTP bind address (default 0.0.0.0; use 127.0.0.1 for local-only, restart required)
KOMF_LOG_LEVELLog level (default INFO)
KOMF_DISCORD_WEBHOOKSComma-separated Discord webhook URLs
KOMF_APPRISE_URLSComma-separated Apprise URLs
KOMF_METADATA_PROVIDERS_MAL_CLIENT_IDRequired for MAL provider
KOMF_METADATA_PROVIDERS_COMIC_VINE_API_KEYRequired for ComicVine provider
KOMF_METADATA_PROVIDERS_BANGUMI_TOKENBangumi token (shows NSFW items)
KOMF_AUTH_KEYAccess key for sensitive operations (optional, strongly recommended for public exposure): when set, sensitive requests from outside the local network must present it (local/LAN bypass); GET /version and GET /api/health stay public; legacy KOMF_WEBUI_KEY still works as fallback
KOMF_WEB_DIRWebUI static directory override (web/dist -> ui lookup order)
KOMF_AUTH_FORCE_REMOTEDebug only: 1 treats every client as remote to force the key path
KOMF_TAG_TRANSLATIONEnv-only switch (no config option): built-in tag translation (EN→ZH tags mapping when seriesTitleLanguage is Chinese; not the ehentai-specific translator) is enabled by default; set to 0 or false (case-insensitive) to disable
⁠Docker
docker run -d --name komf ghcr.io/dyphire/komf-rs:latest \
 -p 8085:8085 \
 -v /path/to/config:/config \ # 存放 application.yml 的目录

The image exposes /config as a volume (KOMF_CONFIG_DIR=/config), so place your application.yml there. The image is published to ghcr.io/dyphire/komf-rs (latest + version tags); to build locally instead, run docker build -f docker/Dockerfile . -t komf-rs.

⁠Configuration

The repository does not include an application.yml; all options are documented in the template application.example.yml⁠ (every field with an inline comment and its code default value; sensitive fields — Komga user/password/API key, e-hentai/exhentai cookies — are empty placeholders).

To use it:

  1. Copy the template to application.yml (see Running⁠ for the exact command).
  2. Edit application.yml: fill in your Komga/Kavita/Stump credentials and enable the providers you want; each option is explained inline.
  3. Start the service with the config file (path argument or KOMF_CONFIG_DIR); without a config file it runs on built-in defaults.

⁠Security

The service ships with an optional key-based access gate — setting KOMF_AUTH_KEY is strongly recommended when exposed to the public internet. Set KOMF_AUTH_KEY (legacy KOMF_WEBUI_KEY still works as fallback) and sensitive requests from outside the local network must present the key — local/loopback and LAN (RFC1918 / link-local / IPv6 ULA) clients are always allowed through without one. Non-sensitive GET /version and GET /api/health stay public for version/health checks. Unauthorized sensitive /api/* requests get 401; page/asset requests get an inline login page (key input, styled to match the WebUI). A successful POST /api/auth/login sets an HttpOnly session cookie (komf_auth); POST /api/auth/logout clears it. A request passes with either credential: the cookie (derivation uses SHA-1 over the key with a fixed salt, compared in constant time) or an Authorization: Bearer <base64(key)> header (the key is base64-encoded for transport, decoded and constant-time compared server-side, keeping the raw key out of request headers/access logs) for scripts/API clients. KOMF_AUTH_FORCE_REMOTE=1 (debug only) forces the key path for every client.

Without the key gate the service has no built-in authentication: anyone who can reach the HTTP port can read the (credential-masked) configuration and change it via PATCH /api/config, and use the metadata endpoints. Treat it like a database admin panel:

  • Local-only (recommended for home servers): bind to loopback so only the machine itself can connect:

    server:
      bind: 127.0.0.1
      port: 8085
    

    or KOMF_SERVER_BIND=127.0.0.1 (restart required). Access the WebUI via SSH tunnel if needed.

  • LAN/VPS exposure: put a reverse proxy with authentication in front and firewall the raw port. Examples:

    # nginx: basic auth
    server {
      listen 80; server_name komf.example.com;
      location / {
        auth_basic "komf"; auth_basic_user_file /etc/nginx/.htpasswd;
        proxy_pass http://127.0.0.1:8085;
      }
    }
    
    # Caddy: basic auth (one line)
    komf.example.com {
      basicauth { admin $2a$14$... }
      reverse_proxy 127.0.0.1:8085
    }
    

⁠Per-library configuration

Any metadata update option or provider can be scoped to a specific library by its id (Komga, Kavita or Stump library id) via metadataUpdate.library.<libraryId> and metadataProviders.libraryProviders.<libraryId> — see the commented placeholders in the template (application.example.yml).

⁠Metadata aggregation

By default, metadata is fetched from the first positive match in configured providers, in priority order. With aggregate: true, metadata from all providers is aggregated: a field is only taken from another provider if the previous one did not provide it. With mergeGenres: true / mergeTags: true (aggregate mode only), the series'/books' existing genres/tags on the media server are also merged into the aggregated result, so provider data does not wipe them.

⁠Notifications

If any webhook URLs are configured, webhooks are called after books are added. Message formats are customizable with Velocity templates placed in templatesDirectory/discord or templatesDirectory/apprise:

  • Discord: title.vm, title_url.vm, description.vm, footer.vm, field_<index>_name<_inline>.vm, field_<index>_value.vm
  • Apprise: apprise_title.vm, apprise_body.vm

For Docker deployments, templates go in the mounted /config/discord or /config/apprise directory.

⁠HTTP Endpoints

⁠Configuration
  • GET /api/config, PATCH /api/config — read / update configuration (hot-reload)
⁠Authentication (active when KOMF_AUTH_KEY is set)
  • POST /api/auth/login — {"key":"..."}: 204 + komf_auth cookie on success, 401 on failure
  • POST /api/auth/logout — clears the auth cookie
  • Protected requests present a credential (either): Cookie: komf_auth=<from login>, or Authorization: Bearer <base64(KOMF_AUTH_KEY)> (decoded and verified server-side)
  • Public without auth (remote): GET /version, GET /api/health; all other sensitive requests require auth
  • Unauthorized sensitive /api/* requests return 401; other paths return the built-in login page; local/LAN clients bypass the gate
⁠Offline database download
  • POST /api/update-manga-baka-db, POST /api/update-book-walker-db — trigger an offline DB download; streams NDJSON progress events (ProgressEvent / FinishedEvent / ErrorEvent) until the stream closes. Manual trigger; additionally, updateIntervalHours (per provider, default 24, 0 = manual only) enables a background scheduled update: MangaBaka checks the official sha1 checksum (a checksum-identical local DB is skipped), BookWalker does a HEAD request comparing Last-Modified. Downloads are atomic (temp file + rename) — on failure the old database is kept and the check retries after 15 minutes.
⁠Jobs
  • GET /api/jobs, GET /api/jobs/all (DELETE), GET /api/jobs/{jobId}/events (SSE, per-job)
  • GET /api/jobs/events (SSE, global firehose, optional ?ids=a,b,c filter) — one connection observes all job activity; frames reuse the per-job event names plus JobCreatedEvent / JobFinishedEvent lifecycle frames, each data flattened with jobId + seriesId; replays current RUNNING jobs on connect, never closes on single-job completion, slow clients drop frames and catch up
⁠Metadata ({media-server} = komga, kavita or stump)
  • GET /api/{media-server}/metadata/providers — enabled providers (optional libraryId)
  • GET /api/{media-server}/metadata/search?name=... — search (optional libraryId)
  • GET /api/{media-server}/metadata/series-cover?providerSeriesId=...
  • POST /api/{media-server}/metadata/identify — set metadata from a provider:
{
 "libraryId": "09TDSWK3Q0XRA",
 "seriesId": "07XF6HKAWHHV4",
 "provider": "MANGA_UPDATES",
 "providerSeriesId": "1"
}
  • POST /api/{media-server}/metadata/match/library/{libraryId} — match all series in a library
  • POST /api/{media-server}/metadata/match/library/{libraryId}/series/{seriesId} — match one series
  • POST /api/{media-server}/metadata/reset/library/{libraryId} — reset all series metadata
  • POST /api/{media-server}/metadata/reset/library/{libraryId}/series/{seriesId} — reset one series
⁠Media server
  • GET /api/{media-server}/media-server/connected, GET /api/{media-server}/media-server/libraries
⁠Notifications
  • GET|POST /api/notifications/{discord,apprise}/{templates,send,render}
⁠Cover redirect
  • GET /api/cover/redirect?url=<encoded> — 302 + Referrer-Policy: no-referrer to the target cover URL. The WebUI uses it to render search-result covers: provider cover CDNs (MangaDex and others) return a placeholder banner for any Referer outside their allowlist, but serve the real cover when no Referer is sent — the redirect lets the browser drop the Referer and load the actual cover. The target host is validated against a provider-cover-domain allowlist (open-redirect / SSRF protection); other hosts get 400. The search API's imageUrl stays a direct link, so third-party server-side consumers are unaffected and may optionally use this endpoint.
⁠OAuth login ({provider} = anilist, mal, bangumi or mangabaka)

Server-side OAuth2 login for metadata providers, using a shared client + official relay page (no per-instance callback registration). See docs/oauth-relay/README.md⁠ for the mechanism, client-secret injection and deployment notes.

  • GET /api/oauth/{provider}/start — 302 redirect to the provider's authorization page (state carries the instance callback URL; PKCE for anilist/mal/mangabaka). Optional ?redirect_path_prefix=/prefix prefixes the instance callback path, for reverse proxies that mount the callback inside their own namespace (e.g. kmrs under /api/v1/komf) or sub-path deployments
  • GET /api/oauth/{provider}/callback — OAuth callback (via the relay page): exchanges the code, stores the token in <configDir>/oauth.sqlite, then 302 to /?oauth=success (or /?oauth=error&message=...)
  • GET /api/oauth/{provider}/status — 200 JSON {"logged_in":bool,"username":string|null}
  • POST /api/oauth/{provider}/logout — 204, clears the stored token

Once logged in, the provider requests are authenticated with the OAuth bearer token (takes precedence over the manual bangumiToken / KOMF_METADATA_PROVIDERS_MAL_CLIENT_ID options); expired tokens are auto-refreshed when a refresh token exists, otherwise the login is cleared and the provider falls back to anonymous. The tracker endpoints return 401 whenever the login is lost at request time (expired and unrefreshable, no secret, refresh rejected, or the provider itself rejects the token with 401 — which also clears the stored login), so reading-status sync never silently operates without the user's account. The WebUI Providers page shows login status and offers login/logout per provider.

⁠Tracker ({provider} = anilist, mal, bangumi or mangabaka; requires OAuth login)

Reading-list sync for the four platforms, backed by the OAuth login above.

  • GET /api/tracker/{provider}/search?name=...&nsfw=... — search the platform; each item carries tracked (whether it is already in the user's list). nsfw defaults to true and is filtered only when false. name may also be a platform entry link — anilist.co/manga/{id}, myanimelist.net/manga/{id}, bgm.tv/bangumi.tv/subject/{id}, mangabaka.org/{id} — with or without a scheme; the backend resolves it directly to the single item.
  • GET /api/tracker/{provider}/state?trackId=... — the current list entry (status, score, chapters/volumes read, start/finish dates, totals); not in the list returns an empty state (Bangumi maps the "not collected" 404 to an empty state).
  • POST /api/tracker/{provider}/update — push a state update:
{ "trackId": "70345", "score": 8, "status": "reading", "lastReadChapter": 12, "lastReadVolume": 1, "startReadDate": "2026-09-01", "finishReadDate": null }

status is one of reading, planning, completed, paused, dropped, rereading; omit the field to keep the current value.

Notes: tracked is user-scoped — AniList via mediaListEntry, MAL via my_list_status (details fetched concurrently), Bangumi via the user's collection list, MangaBaka via the user's library (batched GET /v1/my/library/batch). Bangumi search falls back to the legacy GET /search/subject/{q}?type=1 when the v0 API fails (the metadata matching provider has the same fallback). AniList scores follow the account's mediaListOptions.scoreFormat (POINT_10 accounts read/write 0–10; other formats 0–100). MangaBaka ratings are 0–100 and its plan_to_read/considering map to planning; updates create the library entry with POST when it does not exist yet, PATCH otherwise.

  • `GET /a

Tag summary

Content type

Image

Digest

sha256:d3866a2f9…

Size

43.6 MB

Last updated

1 day ago

docker pull dyphire/komf-rs