wudict (1)

Download OpenAPI specification:

URL: https://github.com/wuweidict/wudict License: GPL-3.0-or-later

A local dictionary server for MDX/MDD, StarDict, Slob, DSL and BGL files.

WuWeiDict serves your own dictionary files over HTTP, on your own machine.

Streaming. /api/dicts, /api/search and /api/rescan answer with NDJSON: one JSON object per line, flushed the moment it is ready. Read them line by line; do not wait for the body to end. That is the whole point - the first dictionary's results arrive while the slowest one is still opening. Every stream opens with a begin line and closes with an end line.

/api/ingest streams Server-Sent Events instead, because it reports progress rather than results.

Two surfaces. Three read-only endpoints answer a browser extension cross-origin: /api/dicts, /api/search and /res/. Everything else is same-origin, is marked internal below, and may change between releases.

An access key, when the server is reachable. On the default 127.0.0.1 bind there is nothing to authenticate and no key is asked for. On any other address - and Go's wildcard listen is dual-stack, so 0.0.0.0 includes every globally routable IPv6 address the machine holds - AUTH defaults to requiring one on every endpoint EXCEPT the three read-only ones above, which stay open to the extensions already granted them. Send it as Authorization: Bearer <key>, or open the link wudict token prints once and let the browser keep the cookie it sets. Without it: 401, with {"auth":"required"}. AUTH=off serves everything to anyone who can reach the port.

Address it by address. Every request must carry a Host of an IP literal or localhost; any other name is answered 421 Misdirected Request unless TRUSTED_HOSTS lists it. This is what stops a web page from re-pointing its own domain at your machine (DNS rebinding) and reading the API as same-origin, which no CORS rule can prevent.

And from this origin. A browser states who started a request in Sec-Fetch-Site. Anything it marks cross-site is answered 403 unless it addresses one of the three read-only endpoints above (or their preflight), or is a top-level navigation to a page rather than to /api/. This closes the state-changing GET: GET /api/rescan and GET /api/setup?save=1 are otherwise reachable from an <img> tag on any origin, carrying the cookie, and CORS never blocks a request - only the reading of its response. Clients that send no fetch metadata (curl, the Android shell, anything that is not a browser) are unaffected.

search

The client API. Read-only, and reachable from a browser extension.

Search every dictionary, streaming

Queries the selected dictionaries concurrently and streams one line per dictionary as it finishes. Lines arrive in completion order, never in slot order: each hit carries i, its index in the begin line's slots array.

A dictionary that fails, that cannot serve the requested mode, or that was declined by the memory cap reports that on its own line. None of those is a request failure, and the other dictionaries still answer.

A search is cancelled after 30 seconds.

query Parameters
q
required
string
Example: q=flight

The search text. Blank is a 400.

mode
string
Default: "prefix"
Enum: "exact" "prefix" "contains" "fts" "fuzzy"

exact · prefix · contains (trigram substring over folded headwords) · fts (full text). fuzzy is accepted as a legacy alias and now means prefix.

contains and fts need the dictionary to carry that index; ask /api/dicts for caps first, and expect skipped if you do not.

A multi-word fts query is a PHRASE first. It is retried as a proximity and then as separate words only when the stricter reading returns nothing, and the hit says which reading answered (rung). Quotes and the operators AND, OR, NOT and NEAR(a b, n) are parsed when present, and an explicit query like "faux ami" NOT linguistics is answered exactly as written, without relaxation. Anything unparseable is read as plain words rather than rejected.

hl
boolean
Default: false

Mark the query's words inside each article body, wrapped in <mark class="wu-hl">. Read ONLY in mode=fts, where the reason a result matched is invisible otherwise; ignored in every other mode, where the match is the headword itself.

Off unless you ask, so an article you fetch is the dictionary's own bytes by default. wudict's own UI sends hl=1 from a remembered preference, which is why the default here is off and the default there is on. Marking is prefix-based and whole-token, matching what the full-text index matched, and is capped at 500 marks per article

  • past the cap the article is still returned complete, just no longer marked. format=text drops the marks with all other markup.
format
string
Default: "raw"
Enum: "raw" "clean" "text"

raw is the dictionary's own HTML. clean keeps structure, emphasis and media and drops scripts, styles and presentation, at roughly half the bytes; it also rewrites root-absolute links such as /res/… into absolute URLs, so the payload works inside a page served from somewhere else. text is markup-free, roughly 0.4.

Ask for clean unless you render the article with the dictionary's own stylesheet.

dict
string
Default: "all"
Example: dict=oxford,webster

all, one dictionary id, or a comma-separated list of ids in the order you want them back. Ids come from /api/dicts. A list where no id resolves is a 404; unknown ids inside a resolving list are skipped. Naming exactly one dictionary bypasses the memory cap, because a search of one is a direct demand.

n
integer >= 1
Default: 20

Maximum results per dictionary.

Responses

Request samples

curl -sN 'http://127.0.0.1:6888/api/search?q=flight&mode=prefix&format=clean&n=5'

# -N is the whole point: without it curl buffers, and the lines that
# were sent one at a time arrive together at the end.

Response samples

Content type
application/x-ndjson
"{\"t\":\"begin\",\"i\":0,\"slots\":[{\"dict\":\"oxford\",\"name\":\"oxford\"},{\"dict\":\"webster\",\"name\":\"webster\"}]}\n{\"t\":\"hit\",\"i\":1,\"dict\":\"webster\",\"name\":\"Webster's Revised Unabridged\",\"results\":[{\"Headword\":\"flight\",\"Body\":\"<div>…</div>\"}]}\n{\"t\":\"hit\",\"i\":0,\"dict\":\"oxford\",\"name\":\"Oxford Advanced Learner's\",\"results\":[]}\n{\"t\":\"end\",\"i\":0}\n"

CORS preflight

Answers 204 with the extension grant. The extension's own requests are CORS-simple and never preflight; this exists so a future one does not meet a 405 that explains nothing.

Responses

List every dictionary, streaming

begin carries the total immediately, from registry ids alone, so a client can show "0 of 105" before any dictionary is opened. Rows follow in completion order.

Responses

Request samples

curl -sN 'http://127.0.0.1:6888/api/dicts'

# just the ids, which is what /api/search wants
curl -sN 'http://127.0.0.1:6888/api/dicts' | jq -r 'select(.t=="dict") | .dict.id'

Response samples

Content type
application/x-ndjson
"{\"t\":\"begin\",\"total\":2}\n{\"t\":\"dict\",\"dict\":{\"id\":\"oxford\",\"name\":\"Oxford Advanced Learner's\",\"format\":\"mdx\",\"path\":\"/Users/me/Dicts/oald.mdx\",\"entries\":184000,\"caps\":{\"Exact\":true,\"Prefix\":true,\"Contains\":false,\"FTS\":false}}}\n{\"t\":\"end\"}\n"

CORS preflight

Responses

One file from inside a dictionary

Images, stylesheets, scripts and audio, as the article references them.

path is the whole remainder of the URL and may contain slashes, which an OpenAPI path parameter cannot express: send it unencoded, exactly as the article asks for it.

Speex audio is transcoded to WAV and answered as audio/wav. A .spx that cannot be transcoded, or one supplied in a res/ override folder, is answered raw as audio/ogg. Files from a res/ folder are served uncached, so an edit takes effect on reload.

path Parameters
dict
required
string

Dictionary id, from /api/dicts.

path
required
string
Example: audio/flight__gb.mp3

The resource path inside the dictionary, subfolders included.

Responses

Request samples

# the path after the id is the article's own path, slashes and all
curl -s 'http://127.0.0.1:6888/res/oxford/audio/flight__gb.mp3' -o flight.mp3

Response samples

Content type
application/json
{
  • "error": "string"
}

CORS preflight

path Parameters
dict
required
string
path
required
string

Responses

This document

The contract of the running build, served by it.

Responses

Request samples

curl -s 'http://127.0.0.1:6888/api/openapi.yaml' -o openapi.yaml

internal

Same-origin endpoints that serve the web page and the platform shell. Documented for completeness; not a stable contract.

Re-scan the dictionary folders, then list

Reads the dictionary folders again, warms the registry, and then answers exactly as /api/dicts does. Use it after adding files on disk.

Responses

Request samples

# picks up files added to the dictionary folders, then lists
curl -sN 'http://127.0.0.1:6888/api/rescan' | head -1

Response samples

Content type
application/x-ndjson
Example
{
  • "t": "DictsBegin",
  • "total": 0
}

Prepare a dictionary, or add an index to it

Brings one dictionary to the feature state you ask for, streaming Server-Sent Events:

event: progress   data: {"done":N,"total":M}
event: done       data: {…a /api/dicts row…}
event: error      data: {"error":"…"}

Each switch is a state, not a verb: a parameter you leave out keeps whatever that feature is now, so toggling one thing cannot strip another. Progress is emitted at most every 200 ms.

query Parameters
dict
required
string

Dictionary id.

contains
string
Enum: "0" "1" "true" "false"

Trigram substring index over headwords. 0 or false turns it off.

fts
string
Enum: "0" "1" "true" "false"

Full-text index over article text - the largest one.

media
string
Enum: "0" "1" "true" "false"

Pack the dictionary's media into media.db.

full
string
Deprecated

Legacy. Any value means full text plus media.

level
string
Deprecated
Value: "headwords"

Legacy. headwords turns full text off.

Responses

Request samples

# add the substring index to one dictionary and watch it build
curl -sN 'http://127.0.0.1:6888/api/ingest?dict=oxford&contains=1'

# leave a switch out and it keeps its current state; this only packs media
curl -sN 'http://127.0.0.1:6888/api/ingest?dict=oxford&media=1'

Response samples

Content type
application/json
{
  • "error": "string"
}

List the prepared dictionaries on disk

Everything in the library folder, whether or not it is currently enrolled. Listing is not consent: reading this never adds anything to the registry.

Responses

Request samples

curl -s 'http://127.0.0.1:6888/api/library' | jq '.dir, .count'

Response samples

Content type
application/json
{
  • "dir": "string",
  • "count": 0,
  • "useCached": true,
  • "entries": [
    ]
}

Delete one dictionary

Removes the prepared folder, the original files, or both. Both default to on, which is "remove this dictionary".

Irreversible. Files are unlinked, not moved to a Trash or Recycle Bin, and nothing here can be undone.

From the machine running wudict this is always allowed - it is your library and your disk. From a browser on another machine it follows ALLOW_REMOTE_DELETE (default off); with that off, the remote caller gets a 403 and canDelete: false in GET /api/config.

query Parameters
dict
required
string

Dictionary id.

prepared
string
Default: "1"

Delete the prepared library folder. 0 keeps it.

source
string
Default: "1"

Delete the original dictionary files. 0 keeps them.

Responses

Request samples

# remove one dictionary: prepared folder and original files
curl -s -X DELETE 'http://127.0.0.1:6888/api/library?dict=oxford'

# free the indexes only, keeping the original file
curl -s -X DELETE 'http://127.0.0.1:6888/api/library?dict=oxford&source=0'

# There is no dry run here - `wudict rm` is the one that previews.

Response samples

Content type
application/json
{
  • "name": "string",
  • "folder": "string",
  • "sources": [
    ],
  • "freed": 0,
  • "gone": true,
  • "note": "string"
}

List installable and installed lemma data

One row per language, merging the published catalogue (LEMMA_URL) with what is present in LEMMA_DIR, plus the state of any download running right now. Always answers 200: what is installed is the half of the answer that needs no network, and an unreachable catalogue is reported in error rather than by failing.

query Parameters
refresh
string

Re-read the catalogue instead of using the cached copy. The catalogue is otherwise cached for ten minutes, so a page polling during a download does not ask its host once a second.

Responses

Request samples

curl -s 'http://127.0.0.1:6888/api/lemmas' |
  jq -r '.languages[] | select(.installed) | .code'

Response samples

Content type
application/json
{
  • "dir": "string",
  • "url": "string",
  • "enabled": true,
  • "cacheSize": 0,
  • "attribution": "string",
  • "error": "string",
  • "shadowed": [
    ],
  • "languages": [
    ]
}

Install one language's lemma data

Starts a download and returns at once; follow it with GET /api/lemmas. The download runs in the server, not in this request, so it survives the page being reloaded, closed, or rotated away on Android.

The catalogue is never taken from the caller: only the configured LEMMA_URL is fetched. A second call for a language already downloading joins the running download instead of starting another.

On success the language is searchable immediately - no restart, because the lemma folder is re-indexed in place.

query Parameters
code
required
string

Language code or English name ("pl", "polish").

Responses

Request samples

curl -s -X POST 'http://127.0.0.1:6888/api/lemmas?code=pl'

Response samples

Content type
application/json
{
  • "code": "string",
  • "state": "string"
}

Delete one language's lemma data

Removes every file in LEMMA_DIR supplying this language, not only the one in use, and re-indexes the folder. Deleting English falls back to the pack compiled into the binary rather than losing the language. Unlike dictionary removal this is offered on every platform: these are files wudict downloaded, not files the user brought.

query Parameters
code
required
string

Language code or English name.

Responses

Request samples

curl -s -X DELETE 'http://127.0.0.1:6888/api/lemmas?code=pl'

Response samples

Content type
application/json
{
  • "code": "string",
  • "builtin": true,
  • "removed": [
    ]
}

Validate a dictionary folder, and optionally save it

Drives the first-run page. Without save it only reports what the folder holds, which is what live typing needs. With save=1 it switches the registry over and writes DICT_DIR. Repeat path for several folders; found is then the deduplicated total across all of them, because overlapping folders must not be counted twice.

query Parameters
path
string

A dictionary folder. Repeatable. ~ is expanded.

save
string

Any value persists the folders to the config file.

useCached
string
Enum: "0" "1"

1 enrolls the previously prepared dictionaries and persists USE_CACHED; 0 turns that off; absent leaves it alone.

Responses

Request samples

# what this folder holds, without changing anything
curl -s 'http://127.0.0.1:6888/api/setup?path=~/Dictionaries'

# adopt two folders and write them to the config file
curl -s 'http://127.0.0.1:6888/api/setup?path=~/Dictionaries&path=/Volumes/Ext/Dicts&save=1'

Response samples

Content type
application/json
{
  • "path": "string",
  • "found": 0,
  • "dirs": [
    ],
  • "saved": true,
  • "useCached": true,
  • "error": "string"
}

The effective settings, and where each came from

What the panel's folders section shows, including which layer set DICT_DIR - so the page can say that saving will not take effect, rather than silently doing nothing.

Responses

Request samples

# where the settings came from, and which folders are scanned
curl -s 'http://127.0.0.1:6888/api/config' | jq '.configPath, .dictDirOrigin, .roots'

Response samples

Content type
application/json
{
  • "roots": [
    ],
  • "libDir": "string",
  • "prepared": 0,
  • "useCached": true,
  • "configPath": "string",
  • "total": 0,
  • "dictDirOrigin": "flag",
  • "dictDirEditable": true,
  • "revealLabel": "string",
  • "canReveal": true,
  • "canDelete": true,
  • "pathAliases": [
    ]
}

Dictionary order, on/off switches and article text size

Healed against the dictionaries that exist right now. exists is false on a first run, which is the client's cue to adopt what an older build left in localStorage, once, instead of starting the user over.

Responses

Request samples

curl -s 'http://127.0.0.1:6888/api/prefs' | jq '.dicts'

Response samples

Content type
application/json
{
  • "exists": true,
  • "dicts": [
    ],
  • "ui": {
    }
}

Replace the order and the enabled set

Array order is the user's order. Entries record the path as well as the id, because ids are derived from the path: moving a dictionary folder changes every id at once, and the path is what re-attaches the state instead of resetting it. Body is capped at 1 MiB.

Request Body schema: application/json
required
Array of objects (DictPref)
object

Omit to leave the stored UI record untouched. Absent and empty are different: a client that only reorders dictionaries must not clear the reader's text size.

Responses

Request samples

Content type
application/json
{
  • "dicts": [
    ],
  • "ui": {
    }
}

Response samples

Content type
application/json
{
  • "exists": true,
  • "dicts": [
    ],
  • "ui": {
    }
}

The user's global stylesheets

Two CSS files the user owns, applied to everything wuDict renders. app styles the page itself - its colours, its own layout - and its :root custom properties reach articles too, because they inherit across the shadow boundary. article styles what dictionaries render, in both article flavours (shadow root and sandboxed iframe).

The split is what keeps body{}, p{} and a{} - written with articles in mind - from also restyling the chrome.

dir is where the two files live on disk, beside the wudict.toml in effect, so they can be edited with an ordinary text editor as well; writable is false when there is no config directory at all, and the editor says so instead of failing to save.

Responses

Request samples

curl -s 'http://127.0.0.1:6888/api/style' | jq -r .app

Response samples

Content type
application/json
{
  • "app": "string",
  • "article": "string",
  • "dir": "string",
  • "writable": true
}

Replace either stylesheet, or both

Omit a field to leave that file untouched: absent and empty are different, and the App tab and the Article tab are one editor sending different bodies. An empty string REMOVES the file, so "I cleared the box" and "there is a file here containing nothing" do not become two states that behave the same but read differently.

Each file is capped at 256 KiB. A stylesheet that hides the app can be stepped around with GET /?style=off, which omits it.

Request Body schema: application/json
required
app
string

The chrome stylesheet.

article
string

The article stylesheet.

Responses

Request samples

Content type
application/json
{
  • "app": "string",
  • "article": "string"
}

Response samples

Content type
application/json
{
  • "app": "string",
  • "article": "string",
  • "dir": "string",
  • "writable": true
}

The user's own file store

Files the user uploaded so that something they wrote can reference them by URL: an image for a background-image in their custom CSS, a font for an @font-face, an imported stylesheet, a script, a PDF a private dictionary links to. They live beside the two stylesheets, in an assets folder under the same directory GET /api/style reports, and are served from /files/<name>.

No type restriction: the store is "files the page can reach by URL", and an extension allowlist would only mean the feature is missing for whoever needed the entry it lacks. limits is reported rather than assumed, so a client's message about a refused upload cannot drift from the rule that refused it.

Responses

Request samples

curl -s 'http://127.0.0.1:6888/api/files' | jq -r '.files[].name'

Response samples

Content type
application/json
{
  • "files": [
    ],
  • "dir": "string",
  • "writable": true,
  • "total": 0,
  • "limits": {
    }
}

Add or replace one file

The request body is the file itself, raw - there is one file per request and no other field, so multipart would add a parser to carry nothing. The name comes from name, and is one path segment of A-Z a-z 0-9 . _ - starting with a letter or digit, at most 64 characters; it is an allowlist, not a cleaning pass, so nothing here can name a path.

Uploading onto an existing name is refused with 409 unless replace=1: a silent overwrite is the one mistake this endpoint can make that the user cannot undo, because their stylesheet already points at that name.

query Parameters
name
required
string

The file name, which is also its URL under /files/.

replace
string

Set to 1 to overwrite a file that is already there.

Request Body schema: application/octet-stream
required
string <binary>

Responses

Request samples

curl -s -X POST --data-binary @w1.jpg \
  'http://127.0.0.1:6888/api/files?name=w1.jpg'

Response samples

Content type
application/json
{
  • "files": [
    ],
  • "dir": "string",
  • "writable": true,
  • "total": 0,
  • "limits": {
    }
}

Delete one file

Removing what is not there is a success: the caller asked for it to be gone and it is. Nothing checks whether a stylesheet still references the name - that is the caller's warning to give, before it asks.

query Parameters
name
required
string

The file to remove.

Responses

Request samples

curl -s -X DELETE 'http://127.0.0.1:6888/api/files?name=w1.jpg'

Response samples

Content type
application/json
{
  • "files": [
    ],
  • "dir": "string",
  • "writable": true,
  • "total": 0,
  • "limits": {
    }
}

Prepare this dictionary now, because the user chose it

Selecting a dictionary is a statement that it is about to be searched, so its headword index is prepared at once rather than waiting for the first query to discover it is missing. On a large dictionary that first query would otherwise have to search the source format directly, which is what the search budget is too short for.

The request returns as soon as the work is queued; the ingest outlives it. Progress is not reported here - a client learns the dictionary is ready from /api/dicts, where dbSize becomes non-zero.

Idempotent: repeated selection of the same dictionary neither queues a second ingest nor restarts a running one, and a failed preparation is retried only after a cooldown. Does nothing when AUTO_INDEX is off, where preparation is the dictionary panel's job; the response then reports indexing: false.

Same-origin only. It is not part of the client API a browser extension reaches (D69): it starts work and writes to the user's library folder.

query Parameters
dict
required
string

Dictionary id, as returned by /api/dicts.

Responses

Request samples

curl -s -X POST 'http://127.0.0.1:6888/api/demand?dict=<id>'

Response samples

Content type
application/json
{
  • "indexing": true
}

What a dictionary says about itself

The dictionary's own annotation: the publisher's blurb, the edition, the copyright. Composed from the live sidecar beside the source file where the format ships one (Lingvo's <stem>.ann) and otherwise from the description its header declared, then normalised server-side - plain text escaped, markup reduced to prose elements - so a client renders html as-is and needs no sanitiser of its own.

Same-origin only. It is not part of the client API a browser extension reaches (D69): an annotation names a file on the user's disk. It is also deliberately absent from /api/dicts, which is built without opening any dictionary; this endpoint is fetched lazily, per dictionary.

query Parameters
dict
required
string

Dictionary id, as returned by /api/dicts.

Responses

Request samples

curl -s 'http://127.0.0.1:6888/api/about?dict=<id>'

Response samples

Content type
application/json
{
  • "id": "string",
  • "name": "string",
  • "format": "string",
  • "html": "string",
  • "source": "string",
  • "indexLang": "string",
  • "contentsLang": "string"
}

Show a file or folder in the desktop's file manager

Loopback only, and limited to the paths this app already displays: a remote browser must not open a window on someone else's desktop, and an arbitrary path must never reach the shell.

query Parameters
path
required
string

An absolute path the app is showing.

Responses

Request samples

# opens a file manager window on the machine running the server
curl -s 'http://127.0.0.1:6888/api/reveal?path=/Users/me/.wudict/db'

# Loopback only: the request must come from this machine.

Response samples

Content type
application/json
{
  • "revealed": "string"
}

Report the platform's power state

How the Android shell tells the server what the platform just told it - onStop, onTrimMemory, thermal throttling, battery saver - which an exec'd server has no other way of learning.

active is normal. background stops all indexing and closes every handle that can be reopened, while prepared databases stay open so returning is instant. restricted closes those too, leaving the process holding nothing but its listener.

Loopback only, and POST because it changes state: a GET here would be followed by every link prefetcher on the network.

query Parameters
state
required
string
Enum: "active" "background" "restricted"

Responses

Request samples

# what the Android shell sends when the app leaves the screen
curl -s -X POST 'http://127.0.0.1:6888/api/power?state=background'
curl -s -X POST 'http://127.0.0.1:6888/api/power?state=active'

# Loopback only, like /api/reveal.

Response samples

Content type
application/json
{
  • "state": "active"
}