fanficfare-service
HTTP API wrapping FanFicFare: story URL -> EPUB / WorkBundle JSON. Used by Calaloud.
- Stars
- 0
- Language
- Python
- Created
- Aug 4, 2026
- Updated
- Aug 4, 2026
Introduction
FanFicFare-as-a-service
A small HTTP API that turns a fanfiction story URL into an EPUB (and a
normalized WorkBundle JSON) using the real
FanFicFare engine
(the same code that powers the Calibre plugin). This is the missing "worker"
that Calaloud expects at
POST /ff/add and POST /ff/work.
Calaloud is a thin client and intentionally does not bundle FanFicFare —
this service is the acquisition backend it points at (default http://host:8787).
Contract
| Endpoint | Method | Body | Response |
|---|---|---|---|
/health | GET | — | {"ok": true} |
/ff/add | POST | {"url": "<story url>"} | EPUB bytes (application/epub+zip) |
/ff/work | POST | {"url": "<story url>"} | WorkBundle JSON ({work, chapters[]}) |
Optional Bearer auth: set FFF_API_KEY; then every request must send
Authorization: Bearer <key>.
CORS is wide open (*) so the Android WebView / browser can call it.
Request body (POST /ff/add, /ff/work)
{
"url": "https://archiveofourown.org/works/12345",
"siteCredentials": {
"archiveofourown": { "username": "user", "password": "pass", "adult": true }
},
"adultContentDefault": false
}
siteCredentials: optional per-site login. Key is the FFF site section (e.g.archiveofourown,fanfictionnet,fanfiction_net,spacebattles). The service maps the URL to its primary site section automatically.adultContentDefault: iftrue, request adult content for all sites unless overridden by per-siteadultinsiteCredentials.
Calaloud's Settings screen provides a UI to manage these; they are sent with every fetch request.
WorkBundle shape
Mirrors Calaloud's types.ts:
{
"work": {
"id": "fanficfare:<hash>",
"url": "<story url>",
"title": "...",
"author": "...",
"series": "...",
"tags": ["..."],
"summary": "...",
"wordCount": 0,
"chapterCount": 3,
"complete": true,
"updatedAt": "ISO8601",
"source": "fanficfare"
},
"chapters": [
{ "workId": "...", "index": 0, "title": "Chapter 1", "html": "...", "text": "..." }
]
}
wordCount/complete/updatedAt come from FanFicFare metadata; chapters are
parsed from the generated EPUB's OPF spine (same logic Calaloud uses in
src/lib/epubImport.ts).
Run locally
uv sync
FFF_PORT=8787 uv run uvicorn app:app --host 0.0.0.0 --port 8787
Run in Docker
docker build -t fanficfare-service .
docker run --rm -p 8787:8787 -e FFF_API_KEY=changeme fanficfare-service
Environment variables
| Variable | Default | Description |
|---|---|---|
FFF_HOST | 0.0.0.0 | Bind address |
FFF_PORT | 8787 | Port |
FFF_API_KEY | `` | Optional bearer token for all /ff/* endpoints |
FFF_CONFIG_DIR | /config | Directory for personal.ini / defaults.ini (bind-mount) |
FFF_USE_FLARESOLVERR | false | Enable FlareSolverr proxy (requires FFF_FLARESOLVERR_URL) |
FFF_FLARESOLVERR_URL | http://flaresolverr:8191 | FlareSolverr API URL |
FFF_USE_CLOUDSCRAPER | false | Use cloudscraper fetcher (basic Cloudflare bypass) |
FFF_PROXY_HTTP | `` | HTTP proxy (e.g. socks5://tor:9050) for RequestsFetcher |
FFF_PROXY_HTTPS | `` | HTTPS proxy (e.g. socks5://tor:9050) for RequestsFetcher |
Proxy / fetcher priority: FFF_USE_FLARESOLVERR > FFF_USE_CLOUDSCRAPER > FFF_PROXY_HTTP/HTTPS > none.
Deployment with Tor / FlareSolverr (recommended)
AO3, FFN, and other sites block datacenter IPs. The service integrates with the user's Docker stack:
# docker-compose.services.yml (Calaloud reference)
services:
fanficfare:
image: fanficfare-service:latest
environment:
- FFF_API_KEY=changeme
- FFF_PROXY_HTTP=socks5://tor:9050
- FFF_PROXY_HTTPS=socks5://tor:9050
depends_on:
- tor
tor:
image: peterdavehello/tor-socks:latest
ports: ["9050:9050", "9051:9051"]
Or use FlareSolverr instead of Tor:
fanficfare:
environment:
- FFF_USE_FLARESOLVERR=true
- FFF_FLARESOLVERR_URL=http://flaresolverr:8191
depends_on:
- flaresolverr
flaresolverr:
image: ghcr.io/flaresolverr/flaresolverr:latest
ports: ["8191:8191"]
Site credentials (two options)
- Per-request via API (Calaloud UI): send
siteCredentialsin each/ff/addor/ff/workcall. No server-side persistence. - Mounted
personal.ini: bind-mount a directory withpersonal.iniat/configand setFFF_CONFIG_DIR=/config. FFF reads credentials from there automatically (same as Calibre plugin).
Test
# health
curl localhost:8787/health
# add -> save epub
curl -X POST localhost:8787/ff/add -H 'Content-Type: application/json' \
-d '{"url":"https://archiveofourown.org/works/12345"}' -o story.epub \
-H 'Authorization: Bearer changeme'
# work -> normalized bundle
curl -X POST localhost:8787/ff/work -H 'Content-Type: application/json' \
-d '{"url":"https://archiveofourown.org/works/12345"}' \
-H 'Authorization: Bearer changeme'
# with credentials + adult
curl -X POST localhost:8787/ff/work -H 'Content-Type: application/json' \
-d '{"url":"https://archiveofourown.org/works/12345","siteCredentials":{"archiveofourown":{"username":"u","password":"p","adult":true}}}' \
-H 'Authorization: Bearer changeme'