← Back to home@Gishi1

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

EndpointMethodBodyResponse
/healthGET—{"ok": true}
/ff/addPOST{"url": "<story url>"}EPUB bytes (application/epub+zip)
/ff/workPOST{"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: if true, request adult content for all sites unless overridden by per-site adult in siteCredentials.

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

VariableDefaultDescription
FFF_HOST0.0.0.0Bind address
FFF_PORT8787Port
FFF_API_KEY``Optional bearer token for all /ff/* endpoints
FFF_CONFIG_DIR/configDirectory for personal.ini / defaults.ini (bind-mount)
FFF_USE_FLARESOLVERRfalseEnable FlareSolverr proxy (requires FFF_FLARESOLVERR_URL)
FFF_FLARESOLVERR_URLhttp://flaresolverr:8191FlareSolverr API URL
FFF_USE_CLOUDSCRAPERfalseUse 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)

  1. Per-request via API (Calaloud UI): send siteCredentials in each /ff/add or /ff/work call. No server-side persistence.
  2. Mounted personal.ini: bind-mount a directory with personal.ini at /config and set FFF_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'