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.
PATCH /api/config), job tracking, metadata search/identify/match/reset endpoints/api/tracker/* endpoints — search by title or platform link, tracked marking, state read and status/score/progress push| Provider | Search | Series metadata | Book metadata |
|---|---|---|---|
| MangaUpdates | ✅ | ✅ | — |
| MyAnimeList | ✅ | ✅ | ✅ |
| AniList | ✅ | ✅ | — |
| MangaDex | ✅ | ✅ | ✅ |
| BookWalker | ✅ | ✅ | ✅ |
| Bangumi (bgm.tv) | ✅ | ✅ | ✅ |
| ComicVine | ✅ | ✅ | ✅ |
| YenPress | ✅ | ✅ | ✅ |
| Viz | ✅ | ✅ | ✅ |
| Webtoons | ✅ | ✅ | ✅ |
| MangaBaka | ✅ | ✅ | — |
| eHentai (Rust-only) | ✅ | ✅ | — |
| placeholder (unsupported in Kotlin too) | |||
| placeholder (unsupported in Kotlin too) | |||
| placeholder (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.
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.
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):
| Variable | Description |
|---|---|
KOMF_KOMGA_BASE_URI | Komga base URL |
KOMF_KOMGA_USER / KOMF_KOMGA_PASSWORD | Komga basic auth |
KOMF_KOMGA_API_KEY | Komga API key (X-API-Key auth, takes precedence when set) |
KOMF_KAVITA_BASE_URI / KOMF_KAVITA_API_KEY | Kavita base URL + API key |
KOMF_STUMP_BASE_URI / KOMF_STUMP_API_KEY | Stump base URL + API key (stump_ prefix, takes precedence when set) |
KOMF_STUMP_USER / KOMF_STUMP_PASSWORD | Stump account password (only used to exchange a JWT when no API key is set) |
KOMF_SERVER_PORT | HTTP port (default 8085, restart required) |
KOMF_SERVER_BIND | HTTP bind address (default 0.0.0.0; use 127.0.0.1 for local-only, restart required) |
KOMF_LOG_LEVEL | Log level (default INFO) |
KOMF_DISCORD_WEBHOOKS | Comma-separated Discord webhook URLs |
KOMF_APPRISE_URLS | Comma-separated Apprise URLs |
KOMF_METADATA_PROVIDERS_MAL_CLIENT_ID | Required for MAL provider |
KOMF_METADATA_PROVIDERS_COMIC_VINE_API_KEY | Required for ComicVine provider |
KOMF_METADATA_PROVIDERS_BANGUMI_TOKEN | Bangumi token (shows NSFW items) |
KOMF_AUTH_KEY | Access 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_DIR | WebUI static directory override (web/dist -> ui lookup order) |
KOMF_AUTH_FORCE_REMOTE | Debug only: 1 treats every client as remote to force the key path |
KOMF_TAG_TRANSLATION | Env-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 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.
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:
application.yml (see Running for the exact command).application.yml: fill in your Komga/Kavita/Stump credentials and enable the providers you want; each option is explained inline.KOMF_CONFIG_DIR); without a config file it runs on built-in defaults.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
}
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).
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.
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:
title.vm, title_url.vm, description.vm, footer.vm, field_<index>_name<_inline>.vm, field_<index>_value.vmapprise_title.vm, apprise_body.vmFor Docker deployments, templates go in the mounted /config/discord or /config/apprise directory.
GET /api/config, PATCH /api/config — read / update configuration (hot-reload)KOMF_AUTH_KEY is set)POST /api/auth/login — {"key":"..."}: 204 + komf_auth cookie on success, 401 on failurePOST /api/auth/logout — clears the auth cookieCookie: komf_auth=<from login>, or Authorization: Bearer <base64(KOMF_AUTH_KEY)> (decoded and verified server-side)GET /version, GET /api/health; all other sensitive requests require auth/api/* requests return 401; other paths return the built-in login page; local/LAN clients bypass the gatePOST /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.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{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 libraryPOST /api/{media-server}/metadata/match/library/{libraryId}/series/{seriesId} — match one seriesPOST /api/{media-server}/metadata/reset/library/{libraryId} — reset all series metadataPOST /api/{media-server}/metadata/reset/library/{libraryId}/series/{seriesId} — reset one seriesGET /api/{media-server}/media-server/connected, GET /api/{media-server}/media-server/librariesGET|POST /api/notifications/{discord,apprise}/{templates,send,render}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.{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 deploymentsGET /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 tokenOnce 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.
{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.
Content type
Image
Digest
sha256:d3866a2f9…
Size
43.6 MB
Last updated
1 day ago
docker pull dyphire/komf-rs