A self-hosted web tool that looks up manga metadata in real time from Bangumi (bgm.tv) and AniList simultaneously, normalises both into a single canonical schema, and exposes everything through a clean REST API plus a minimal React UI.
Built for the use case of "I have a Chinese manga title in front of me — give me the full bibliographic record from every source I care about, side by side, in under a second."
- Dual-source lookup — every query hits both Bangumi (rich Chinese metadata, infobox publisher / magazine / authors) and AniList (English titles, structured volume/chapter counts, high-res covers) in parallel.
- One canonical schema — all sources return the same 23-field detail object. Fields a source does not carry come back empty (
"",[],null) so client code never has to branch. - Cross-source ranking — multiple candidates per source are merged into a single array sorted by fuzzy-match score against the query, so the most-relevant hit floats to the top regardless of which database it came from.
- Robust matching — automatic ASCII case variants (
darkness↔DARKNESS), traditional → simplified Chinese fallback (viazhconv), Bangumi-to-AniList title translation bridge for Chinese-only inputs. - Settings persistence — change Bangumi URL / token, AniList endpoint, fuzz threshold through a built-in web UI; values are saved to a JSON file on a Docker volume and survive container restarts.
- OpenAPI 3.1 —
/docs(Swagger UI),/redoc, and/openapi.jsonare auto-generated for every endpoint. - Single-container deploy — multi-stage Dockerfile builds the React SPA, copies it into the FastAPI image, and serves both from one process on one port.
| Source | Strengths | Weaknesses | Auth |
|---|---|---|---|
| Bangumi (bgm.tv) | Chinese title (name_cn) and summary, full infobox (publisher, serialisation magazine, story/art credits, ISBN, volume count), community ratings (score / rank / votes), simplified-Chinese tags |
Mostly East-Asian audience; English readers are sparse; serialisation start is stored as magazine issue numbers (e.g. 2006年21·22号) rather than calendar dates |
Optional Bearer token from next.bgm.tv/demo/access-token for a higher rate limit |
| AniList (anilist.co) | Multi-lingual titles (romaji / english / native), structured volume & chapter counts, status enum (RELEASING / FINISHED / HIATUS / CANCELLED), staff with role labels, high-resolution coverImage.extraLarge, English description, broad genres + community tags |
No Chinese title or summary; no publisher / magazine / ISBN fields | None for read; ~30 req/min rate limit |
The two sources are complementary — Bangumi excels at the bibliographic / publishing side, AniList at the cataloguing / multilingual side. The lookup pipeline always queries Bangumi first (even when source=anilist) so a Chinese keyword can be translated into the Japanese title AniList actually indexes by.
The schema is source-agnostic on purpose. To wire in a third database:
- Drop a client into
backend/app/clients/<name>.pyexposing two coroutines:<name>_search(keyword)returningPreviewIteminstances and<name>_full(item_id)returning the canonical 23-field dict (start fromempty_detail("<name>")and fill in what the source carries — leave the rest empty). - Wire it into the parallel
asyncio.gather(...)inbackend/app/api/metadata.pyand the_FETCHERSmap. - Optionally add a colour for the new source's card accent in
frontend/src/pages/Lookup.tsx(key it bysource).
No frontend type changes, no schema migration.
┌────────────────────────────────────────────────────────────────┐
│ Frontend (React 18 + Vite + TypeScript, built as static) │
│ - Lookup page: search box, per-source cards, score badges │
│ - Settings page: edit & persist Bangumi/AniList URLs + token │
└──────────────────────┬─────────────────────────────────────────┘
│ /api/*
┌──────────────────────▼─────────────────────────────────────────┐
│ Backend (FastAPI + httpx + rapidfuzz, async, stateless API) │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ Clients/ │ │
│ │ bangumi.py — /v0/search/subjects + /v0/subjects/{id}│ │
│ │ anilist.py — GraphQL (Media query) │ │
│ │ Pipeline │ │
│ │ search → fan-out case/script variants → fetch full │ │
│ │ detail → normalise → cross-source merge → rank │ │
│ └────────────────────────────────────────────────────────┘ │
│ Persistent settings: /app/data/settings.json (Docker volume) │
└──────────┬───────────────────┬─────────────────────────────────┘
│ │
▼ ▼
┌──────────┐ ┌──────────┐
│ Bangumi │ │ AniList │
│ api.bgm │ │ GraphQL │
│ .tv │ │ │
└──────────┘ └──────────┘
No database. Every API call is a pass-through to upstreams — there is no caching layer to invalidate and no DB migrations to manage.
Requires Docker Desktop (or any Docker Engine 24+) with Compose v2.
git clone https://github.com/acer1204/comic-metadata-syncer.git
cd comic-metadata-syncer
docker compose up -d --buildThe first build takes ~2 minutes (npm install + pip install). Subsequent rebuilds are seconds thanks to layer caching.
docker compose logs -f # follow logs
docker compose restart # restart the running container
docker compose down # stop and remove container + network
docker compose up -d --build # rebuild image and recreate containerThe image and container are both named comic-metadata-syncer. The compose stack is named the same so it shows up tidily in Docker Desktop.
Three layers of configuration, each overriding the previous:
- Defaults baked into
backend/app/config.py. - Environment variables in
docker-compose.yml(or a sibling.envfile). These seed the initial defaults at container start. - Runtime overrides saved through the web UI (
/api/settingsPUT). Written to./data/settings.json, loaded on every container start, and live-applied to the in-processSettingsobject without a restart.
| Variable | Default | Description |
|---|---|---|
BANGUMI_URL |
https://api.bgm.tv |
Bangumi REST endpoint. Point this at your self-hosted bangumi/server (e.g. http://host.docker.internal:3000) to run fully offline. |
BANGUMI_TOKEN |
(empty) | Personal access token from next.bgm.tv/demo/access-token. Sent as Authorization: Bearer … when set. |
ANILIST_URL |
https://graphql.anilist.co |
AniList GraphQL endpoint. |
CORS_ORIGINS |
* |
Comma-separated origin list for CORS. |
FUZZ_THRESHOLD |
80 |
Informational; surfaced to clients as a suggested minimum match score. |
Open http://localhost:8102 and click the ⚙️ in the top-right. The form persists changes to a JSON file inside the ./data volume — no container restart needed. The Bangumi token is stored in clear text on disk; treat the data/ folder as sensitive.
Three families of endpoints. Every path is prefixed with /api.
GET /api/health → { "status": "ok" }
Use when you only need to render a list of choices (titles + cover + score). One outbound API call per source, no detail fetch.
Query string:
q— search keyword (Chinese / Japanese / English all accepted)source—all(default) /bangumi/anilist
Each item has 12 fields:
{
"source": "bangumi",
"id": "31469",
"title_cn": "出包王女",
"title_native": "To LOVEる -とらぶる-",
"title_romaji": null,
"title_english": null,
"date": "2006-11-02",
"url": "https://bgm.tv/subject/31469",
"cover": "https://lain.bgm.tv/pic/cover/l/...",
"score": 100.0,
"has_summary": true,
"tag_count": 30
}has_summary and tag_count give a hint of which candidate is data-rich before you pay for the full detail fetch.
Returns the top candidates per source, fetches each one's complete detail in parallel, and ranks them globally by fuzzy score.
Query string:
q— search keywordsource—all/bangumi/anilist. When narrowed, only that source contributes entries — but a Bangumi search is still issued internally to translate Chinese queries into Japanese for AniList.
Response:
{
"query": "出包王女",
"sources": [
{ "source": "bangumi", "id": "31469", "match_score": 100.0, ... 22 more },
{ "source": "anilist", "id": "30671", "match_score": 100.0, ... },
{ "source": "anilist", "id": "52519", "match_score": 95.0, ... },
...
]
}A source with zero hits still contributes one empty entry (id: "", match_score: 0) so the client can tell the source was tried but came up dry.
Once a candidate is picked from /api/preview, hit this to get the full canonical detail of just that one entry. No wrapper — the response is the bare 23-field object.
GET /api/metadata/bangumi/31469
GET /api/metadata/anilist/30671
| Response | Meaning |
|---|---|
200 |
Found. match_score is set to 100 because the caller has confirmed this is the right pick. |
400 |
Unknown source (only bangumi and anilist are valid). |
404 |
Id format invalid or no such subject. |
502 |
Upstream Bangumi / AniList returned an unexpected error. |
GET /api/settings → effective settings (token is never echoed)
PUT /api/settings → partial update; missing fields are left unchanged
PUT body:
{
"bangumi_url": "https://api.bgm.tv",
"bangumi_token": "your_personal_access_token",
"anilist_url": "https://graphql.anilist.co",
"fuzz_threshold": 80
}To clear the Bangumi token, send "bangumi_token": "". To leave it as-is, omit the field entirely.
Every SourceDetail — whether from Bangumi, AniList, or a future source — has these 23 fields:
| Field | Type | Notes |
|---|---|---|
source |
string | "bangumi", "anilist" |
id |
string | Source-specific id; empty string when the source had no hit |
url |
string | Canonical URL on the source's website |
match_score |
number | 0–100 fuzzy similarity to query (100 = exact / user-picked) |
title_cn |
string | Chinese title; empty when source does not provide one |
title_native |
string | Native (typically Japanese) title |
summary |
string | Plot summary / description, plain text |
serialization_start_date |
string | YYYY-MM-DD of first chapter publication |
first_volume_date |
string | YYYY-MM-DD of first tankōbon release |
cover |
string | URL to cover image (largest available) |
platform |
string | E.g. "漫画" / "MANGA" / format name |
authors |
string[] | Story credits |
artists |
string[] | Art credits |
publisher |
string | |
magazine |
string | Serialisation magazine |
isbn |
string | First volume ISBN if known |
alt_titles |
string[] | Other known titles (romaji, English, alternate Chinese, …) |
volumes |
string | Volume count (string because Bangumi sometimes returns ranges) |
chapters |
string | Chapter count |
status |
string | Serialisation period in human-readable form |
tags |
string[] | Source-specific tags, filtered + deduped |
rating |
object | { score, rank, total } — score on the source's native scale |
infobox |
object | Free-form bucket of source-specific extras (Bangumi raw infobox, AniList genres/popularity/staff/etc.) |
Backend (Python 3.12+):
cd backend
python -m venv .venv
.venv\Scripts\activate # Windows
# source .venv/bin/activate # macOS/Linux
pip install -r requirements.txt
uvicorn app.main:app --reload --port 8787Frontend (Node 24+):
cd frontend
npm install
npm run dev # Vite at http://localhost:5173,
# proxies /api → http://localhost:8787In dev mode the frontend talks to the backend through Vite's proxy; in production both are served from the same FastAPI process so there is no CORS hop.
- Backend: FastAPI · Pydantic v2 · httpx · rapidfuzz · zhconv · uvicorn
- Frontend: React 18 · Vite 5 · TypeScript 5 · axios
- Container: Multi-stage Dockerfile (node 24-alpine builder → python 3.12-slim runtime)
- Deploy: Docker Compose, single port (8102 → 8000 inside container)
- 雙資料源同時查詢 — 一次查詢同時打 Bangumi (中文 metadata 強、infobox 含出版社/連載雜誌/作者) 與 AniList (英文標題、結構化卷/話數、高解析度封面),並行送出。
- 統一 canonical schema — 所有來源回傳完全相同的 23 欄詳情物件。某來源沒有的欄位用空字串 / 空陣列 /
null補齊,客戶端不必判斷來源寫條件分支。 - 跨來源排序 — 每個來源回多筆候選後合併成單一陣列,按與查詢字串的 fuzzy match 分數由高至低排,最相關的命中浮在最上面,不分來源。
- 強韌的匹配機制 — 自動嘗試 ASCII 大小寫變體 (
darkness↔DARKNESS)、繁體 → 簡體自動 fallback (透過zhconv)、Bangumi 拿日文標題當作翻譯橋送進 AniList,解決「使用者只有中文書名」的情境。 - 設定持久化 — Bangumi URL / Token、AniList endpoint、fuzz 閾值都可從內建 UI 修改,寫入 Docker volume 內的 JSON 檔,容器重啟也不會掉。
- OpenAPI 3.1 —
/docs(Swagger UI)、/redoc、/openapi.json自動生成,每個 endpoint 都有完整型別。 - 單一容器部署 — Multi-stage Dockerfile 把 React SPA build 完丟進 FastAPI image,同一個程序、同一個 port 服務前後端。
| 來源 | 強項 | 弱項 | 認證 |
|---|---|---|---|
| Bangumi (bgm.tv) | 中文標題 (name_cn) 與簡介、完整 infobox (出版社、連載雜誌、作者、作畫、ISBN、卷數)、社群評分 (score / rank / 投票數)、簡體中文 tag |
受眾以中日為主,英文資料稀疏;連載開始日存的是雜誌期數字串 (例如「2006年21·22号」) 而非日曆日期 | 選用 — 從 next.bgm.tv/demo/access-token 申請個人 token 可提高 rate limit |
| AniList (anilist.co) | 多語標題 (romaji / english / native)、結構化卷/話數、狀態 enum (RELEASING / FINISHED / HIATUS / CANCELLED)、製作群附 role tag、coverImage.extraLarge 高解析封面、英文簡介、寬泛 genres + 社群 tags |
沒有中文標題或簡介;沒有出版社 / 雜誌 / ISBN 等欄位 | 讀取不需 token;rate limit 約 30 req/min |
兩個來源互補 — Bangumi 擅長書誌出版面,AniList 擅長目錄分類面與多語對應。Pipeline 永遠先打一次 Bangumi (即使 source=anilist 也一樣),用 Bangumi 拿到的日文標題餵給 AniList,讓中文輸入也能命中 AniList 上的條目。
Schema 設計成跟來源無關,要加第三個資料庫只需要:
- 在
backend/app/clients/<name>.py新增一個 client,提供兩個 coroutine —<name>_search(keyword)回傳PreviewItem列表、<name>_full(item_id)回傳 canonical 23 欄 dict (用empty_detail("<name>")起手,填入該來源有的欄位,沒有的保持空值)。 - 把它接進
backend/app/api/metadata.py的並行asyncio.gather(...)與_FETCHERS對照表。 - (選用) 在
frontend/src/pages/Lookup.tsx的ACCENT字典加一個顏色給新來源的卡片頭。
前端型別不用改,也沒有 schema migration 要跑。
(同上方英文版的 ASCII 圖)
無資料庫,所有 API 呼叫都是直通上游 — 沒有 cache 失效問題,也不用維護 DB migration。
需要 Docker Desktop (或任何 Docker Engine 24+) 加 Compose v2。
git clone https://github.com/acer1204/comic-metadata-syncer.git
cd comic-metadata-syncer
docker compose up -d --build首次 build 大約 2 分鐘 (npm install + pip install),後續因 layer cache 只要幾秒。
docker compose logs -f # 追 log
docker compose restart # 重啟容器
docker compose down # 停止並移除容器 + 網路
docker compose up -d --build # 改 code 後重 build 並重建容器Image 與 container 都叫 comic-metadata-syncer,compose stack 也是同名,Docker Desktop 的 Stacks 列表會直接顯示這個名字。
三層覆蓋順序 (後面的蓋掉前面的):
- 預設值 — 寫死在
backend/app/config.py - 環境變數 —
docker-compose.yml的environment:區塊或同目錄.env檔,容器啟動時載入 - 執行時覆蓋 — 透過網頁 UI (
PUT /api/settings) 寫到./data/settings.json,容器啟動時自動套用,不用重啟
| 變數 | 預設 | 說明 |
|---|---|---|
BANGUMI_URL |
https://api.bgm.tv |
Bangumi REST 端點。改成 http://host.docker.internal:3000 等位址可指向自架 bangumi/server |
BANGUMI_TOKEN |
(空) | 從 next.bgm.tv/demo/access-token 申請的個人 token。有值時自動帶 Authorization: Bearer … |
ANILIST_URL |
https://graphql.anilist.co |
AniList GraphQL 端點 |
CORS_ORIGINS |
* |
CORS 允許來源,逗號分隔 |
FUZZ_THRESHOLD |
80 |
給客戶端參考的最低匹配分數,不會強制套用 |
開 http://localhost:8102,右上角的 ⚙️ 進入設定頁。表單修改後寫入 ./data volume 內的 JSON 檔,不用重啟容器。Bangumi token 以明碼形式存盤,請把 data/ 資料夾視為敏感資料看待。
三大類端點,全部以 /api 為前綴。
GET /api/health → { "status": "ok" }
只需要「畫一個選單給使用者挑」的時候用 (標題 + 封面 + 分數)。每個來源一次 API 呼叫,不抓詳情。
Query 參數:
q— 搜尋關鍵字 (中/日/英都吃)source—all(預設) /bangumi/anilist
每筆 12 個欄位 (見英文版範例)。has_summary 和 tag_count 是用來在抓完整詳情前先看「哪個來源資料比較豐富」。
每個來源回前幾名候選,並行抓完整詳情,然後跨來源按 fuzzy 分數排序。
Query 參數:
q— 搜尋關鍵字source—all/bangumi/anilist。指定單一來源時,只回那個來源的資料 — 但內部仍會 fire 一次 Bangumi 搜尋作為中→日翻譯橋,讓中文輸入也能命中 AniList。
回傳 { query, sources: [...] }。沒命中的來源仍會留一筆 id 為空字串的占位,讓客戶端知道「我有試過」。
從 /api/preview 拿到候選後,使用者確認選哪一筆,呼叫這個端點取那一筆的完整 canonical detail。直接回 23 欄物件,沒有 wrapper。
GET /api/metadata/bangumi/31469
GET /api/metadata/anilist/30671
| HTTP | 含義 |
|---|---|
200 |
找到,match_score=100 (因為呼叫端已確認是這筆) |
400 |
不認識的 source (只接受 bangumi、anilist) |
404 |
ID 格式不對或上游沒有這筆 |
502 |
上游 Bangumi/AniList 回了意外的錯誤 |
GET /api/settings → 目前生效的設定 (token 永遠不會回傳)
PUT /api/settings → 部分更新; 沒帶到的欄位保持原樣
PUT body 範例見英文版。要清掉 token 送 "bangumi_token": "",要保持不變就不要帶這個欄位。
任何來源 (Bangumi、AniList,未來新增的也一樣) 都會回這 23 個欄位:
(完整對照表見上方英文版,各欄位語意一致)
後端 (Python 3.12+):
cd backend
python -m venv .venv
.venv\Scripts\activate # Windows
pip install -r requirements.txt
uvicorn app.main:app --reload --port 8787前端 (Node 24+):
cd frontend
npm install
npm run dev # Vite 跑在 http://localhost:5173,
# /api 自動 proxy 到 http://localhost:8787開發時前端透過 Vite proxy 跟後端通信;部署時兩者由同一個 FastAPI process 服務,沒有 CORS 跨域問題。
- 後端: FastAPI · Pydantic v2 · httpx · rapidfuzz · zhconv · uvicorn
- 前端: React 18 · Vite 5 · TypeScript 5 · axios
- 容器: Multi-stage Dockerfile (node 24-alpine → python 3.12-slim)
- 部署: Docker Compose,單一 port (8102 → 容器內 8000)
MIT — free for personal and commercial use, no warranty. The only obligation is keeping the copyright notice in copies or substantial portions of the software.
This project does not store, host, or redistribute any manga content. It only queries publicly available metadata APIs (Bangumi, AniList) under their respective terms of service. Users are responsible for complying with those terms.
本專案不儲存、不託管、不轉發任何漫畫內容,僅查詢公開的 metadata API (Bangumi、AniList),並遵守各服務的使用條款。使用者需自行確保符合對應服務的規範。