Skip to content

acer1204/comic-metadata-syncer

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

3 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Comic Metadata Syncer

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


Table of contents


English

Features

  • 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 (darknessDARKNESS), traditional → simplified Chinese fallback (via zhconv), 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.json are 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.

Supported sources

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.

Adding a new source

The schema is source-agnostic on purpose. To wire in a third database:

  1. Drop a client into backend/app/clients/<name>.py exposing two coroutines: <name>_search(keyword) returning PreviewItem instances and <name>_full(item_id) returning the canonical 23-field dict (start from empty_detail("<name>") and fill in what the source carries — leave the rest empty).
  2. Wire it into the parallel asyncio.gather(...) in backend/app/api/metadata.py and the _FETCHERS map.
  3. Optionally add a colour for the new source's card accent in frontend/src/pages/Lookup.tsx (key it by source).

No frontend type changes, no schema migration.

Architecture

┌────────────────────────────────────────────────────────────────┐
│  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.

Quick start

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 --build

Open http://localhost:8102

The first build takes ~2 minutes (npm install + pip install). Subsequent rebuilds are seconds thanks to layer caching.

Useful commands

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 container

The image and container are both named comic-metadata-syncer. The compose stack is named the same so it shows up tidily in Docker Desktop.

Configuration

Three layers of configuration, each overriding the previous:

  1. Defaults baked into backend/app/config.py.
  2. Environment variables in docker-compose.yml (or a sibling .env file). These seed the initial defaults at container start.
  3. Runtime overrides saved through the web UI (/api/settings PUT). Written to ./data/settings.json, loaded on every container start, and live-applied to the in-process Settings object 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.

Editing settings through the UI

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.

REST API reference

Three families of endpoints. Every path is prefixed with /api.

Health

GET /api/health  →  { "status": "ok" }

1. GET /api/preview — lightweight candidates

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)
  • sourceall (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.

2. GET /api/metadata — full canonical metadata by search

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 keyword
  • sourceall / 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.

3. GET /api/metadata/{source}/{id} — direct fetch by id

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.

4. Settings

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.

Canonical detail schema

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

Local development

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 8787

Frontend (Node 24+):

cd frontend
npm install
npm run dev                     # Vite at http://localhost:5173,
                                # proxies /api → http://localhost:8787

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

Tech stack

  • 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 大小寫變體 (darknessDARKNESS)、繁體 → 簡體自動 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 設計成跟來源無關,要加第三個資料庫只需要:

  1. backend/app/clients/<name>.py 新增一個 client,提供兩個 coroutine — <name>_search(keyword) 回傳 PreviewItem 列表、<name>_full(item_id) 回傳 canonical 23 欄 dict (用 empty_detail("<name>") 起手,填入該來源有的欄位,沒有的保持空值)。
  2. 把它接進 backend/app/api/metadata.py 的並行 asyncio.gather(...)_FETCHERS 對照表。
  3. (選用) 在 frontend/src/pages/Lookup.tsxACCENT 字典加一個顏色給新來源的卡片頭。

前端型別不用改,也沒有 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

打開 http://localhost:8102

首次 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 列表會直接顯示這個名字。

設定

三層覆蓋順序 (後面的蓋掉前面的):

  1. 預設值 — 寫死在 backend/app/config.py
  2. 環境變數docker-compose.ymlenvironment: 區塊或同目錄 .env 檔,容器啟動時載入
  3. 執行時覆蓋 — 透過網頁 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 給客戶端參考的最低匹配分數,不會強制套用

用 UI 改設定

http://localhost:8102,右上角的 ⚙️ 進入設定頁。表單修改後寫入 ./data volume 內的 JSON 檔,不用重啟容器。Bangumi token 以明碼形式存盤,請把 data/ 資料夾視為敏感資料看待。

REST API 參考

三大類端點,全部以 /api 為前綴。

Health

GET /api/health  →  { "status": "ok" }

1. GET /api/preview — 輕量候選清單

只需要「畫一個選單給使用者挑」的時候用 (標題 + 封面 + 分數)。每個來源一次 API 呼叫,不抓詳情。

Query 參數:

  • q — 搜尋關鍵字 (中/日/英都吃)
  • sourceall (預設) / bangumi / anilist

每筆 12 個欄位 (見英文版範例)。has_summarytag_count 是用來在抓完整詳情前先看「哪個來源資料比較豐富」。

2. GET /api/metadata — 完整 metadata 查詢

每個來源回前幾名候選,並行抓完整詳情,然後跨來源按 fuzzy 分數排序。

Query 參數:

  • q — 搜尋關鍵字
  • sourceall / bangumi / anilist。指定單一來源時,只回那個來源的資料 — 但內部仍會 fire 一次 Bangumi 搜尋作為中→日翻譯橋,讓中文輸入也能命中 AniList。

回傳 { query, sources: [...] }。沒命中的來源仍會留一筆 id 為空字串的占位,讓客戶端知道「我有試過」。

3. GET /api/metadata/{source}/{id} — 直接以 ID 取單筆完整資料

/api/preview 拿到候選後,使用者確認選哪一筆,呼叫這個端點取那一筆的完整 canonical detail。直接回 23 欄物件,沒有 wrapper

GET /api/metadata/bangumi/31469
GET /api/metadata/anilist/30671
HTTP 含義
200 找到,match_score=100 (因為呼叫端已確認是這筆)
400 不認識的 source (只接受 bangumianilist)
404 ID 格式不對或上游沒有這筆
502 上游 Bangumi/AniList 回了意外的錯誤

4. 設定

GET /api/settings           → 目前生效的設定 (token 永遠不會回傳)
PUT /api/settings           → 部分更新; 沒帶到的欄位保持原樣

PUT body 範例見英文版。要清掉 token"bangumi_token": "",要保持不變不要帶這個欄位

Canonical detail schema (23 欄)

任何來源 (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)

License

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.


Disclaimer / 免責聲明

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),並遵守各服務的使用條款。使用者需自行確保符合對應服務的規範。

About

Self-hosted manga metadata lookup — query Bangumi + AniList simultaneously and get a unified canonical schema. Docker-ready REST API + minimal React UI.

Topics

Resources

License

Stars

0 stars

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors