API and development

last updated

UI password#

Off by default: on a trusted home network the web UI is open. Enable it from Settings -> Authentication, or by hashing a password on the command line and pasting the result into the config:

# on the host:
./monloader -hash-password 'your-password'
# or via Docker:
docker exec -it monloader monloader -hash-password 'your-password'

Put the output in auth.password_hash and set auth.enable_password = true. When enabled, the web UI is gated by a session cookie (lifetime auth.session_lifetime_days, default 7); removing the password clears every session.

API tokens and scopes#

monloader’s JSON API lives under /api/v1/ and requires a bearer token; the API is disabled until at least one token exists. Create tokens in Settings -> Authentication: each is named, individually revocable, and its secret is shown once at creation, never again.

Each token’s config dialog sets its scopes: read (list the queue, inspect jobs, list sites) and/or write (enqueue URLs, manage jobs). A call whose token lacks the needed scope gets 403 insufficient_scope. Tokens provisioned by pairing have fixed scopes and are removed by removing the pairing.

/health, /api/v1/openapi.json, and /api/v1/docs stay open without a token.

curl -H "Authorization: Bearer <token>" \
     -X POST http://localhost:8456/api/v1/queue \
     -d '{"url":"https://danbooru.donmai.us/posts/xxx"}'

API reference#

The API documents itself in-app:

  • HTML reference: /api/v1/docs (linked in the footer).
  • OpenAPI spec: /api/v1/openapi.json.

Error codes#

A failed queue item carries one of these stable codes; scripts and the monsender extension can branch on them:

error_code when
unsupported_url gallery-dl matched no extractor for the URL, and it is not itself a direct link to a media file monbooru can ingest
auth_required the site needs credentials (a 401/403 with a missing-auth message)
blocked a bot-protection wall (Cloudflare / captcha challenge), kept distinct from auth_required so its 403 is not read as a missing credential
rate_limited the site returned 429 / a rate-limit error
network_unreachable gallery-dl could not resolve or reach the host (DNS failure, network unreachable); a refused/dropped connection stays download_failed
download_failed any other non-zero gallery-dl exit (a reached host erroring, HTTP 4xx/5xx, a refused/dropped connection)
mapping_failed metadata present but no usable file or URL could be built
file_too_large monbooru rejected the upload for size
monbooru_unreachable the push got no HTTP response (connect / timeout)
monbooru_rejected monbooru returned a 4xx/5xx other than the duplicate 200
hash_mismatch a metadata refetch returned a file that does not match the one monbooru stored, so nothing was merged; on a replace job, the downloaded file did not match the md5 the site claims, so nothing was pushed
hash_not_found a hash lookup found no site (or the PTR index) holding the hash
ptr_unavailable a PTR lookup was requested while the PTR backend is off; normally refused as a 409 when the lookup is enqueued
ptr_syncing a PTR lookup was requested while the index is still building; normally refused as a 409 when the lookup is enqueued
ptr_account_required a PTR contribution ran without a personal account key, or with one the PTR does not accept for contributing
ptr_banned the PTR reports the configured account as banned
already_exists a replace job’s downloaded file already exists in the gallery as another image; monbooru refuses the swap and records the two as a potential-duplicate pair instead
wrong_type a replace job aimed at an image whose row is an archive or video, which cannot be swapped this way
canceled the job was canceled while the item was in flight

The job’s summary aggregates its items: { created, duplicate, enriched, matched, replaced, skipped, archived, failed, canceled, total }. archived is the part of skipped that gallery-dl’s download archive passed over, which is what the queue row’s force download re-fetches. The outcomes themselves are described in downloading.

Building from source#

monloader is pure Go with no CGO, so the binary is static and builds without system libraries:

make build      # go build with the version injected from VERSION.md
# or directly:
go build -o monloader ./cmd/monloader

./monloader -config /path/to/monloader.toml

make build injects the version and repository URL via -ldflags (read from VERSION.md and REPOSITORY.md), which feed the footer and /health; a plain go build reports dev.

CLI flags and subcommands:

  • -config - path to the TOML config file (default ./monloader.toml).
  • -hash-password '...' - print a bcrypt hash for the UI password and exit.
  • -desktop - the desktop profile: OS-native paths, a log file, a browser at startup, the first-run setup. The desktop downloads have it baked in; -desktop=false turns one back into a server build.
  • -no-browser - with -desktop, do not open a browser at startup.
  • -version - print the version and exit.
  • healthcheck - probe the local /health endpoint and exit non-zero if it is not healthy (accepts -config and -timeout).

The container image bundles Python and a pinned gallery-dl. Outside the container you supply it yourself; if it is not on PATH, set gallerydl.binary_path to its location. A missing binary is not fatal at startup - the UI and API still run, the bundled version shows as unavailable, and downloads fail until it is installed.

Adding a site profile#

Mappings are data, not code, so tracking gallery-dl’s growing site list is a data change. There are three levels:

  • No profile needed. Any gallery-dl site without a profile uses the generic fallback (sites), and the [[tag_overrides]] / [[rating_overrides]] tables in monloader.toml (mapping) can correct a generic site with no release.
  • Your own profile. The settings profile tab (mapping) writes /config/profiles/<category>.json, applied immediately and layered over the shipped set. You can also write the file by hand; a file that fails to parse or validate is skipped with a warning in the log.
  • A shipped profile. The built-ins are the same files, embedded from internal/mapping/profiles/<category>.json. Contributing one is a pull request that adds your /config/profiles/<category>.json.

A profile’s fields:

field purpose
family danbooru, e621, moebooru, gelbooru_v02, philomena, or generic - picks the rating semantics and the per-category tag regime
kind booru (default), manga - a manga gallery bundles its pages into one cbz - or other for sites that are neither
post_url_template the canonical post URL with {id} substituted (e.g. https://danbooru.donmai.us/posts/{id})
md5_search_template the site’s md5 search URL with {md5} substituted, a form gallery-dl’s tag-search extractor matches (e.g. https://danbooru.donmai.us/posts?tags=md5:{md5}); set it to make the site eligible for hash lookup
auth none, api_optional, api_required, username_password, cookies, or oauth - drives the settings login indicator and the credential fields the dialog shows
hosts extra hosts that belong to the site (file CDNs, mirror instances); a direct file link on one of them counts as the site, and monsender treats them as the site’s hosts too - a domain covers its subdomains (mapping)
example a representative URL for the per-site test probe
category_overrides per-suffix tag-category remaps baked into the profile
rating_overrides per-value rating remaps baked into the profile
default_rating rating used only when the source gives none
needs_tags a generic-family site that needs gallery-dl’s tags: true to emit per-category tags (one extra request per post)
has_notes a generic-family site whose extractor emits note boxes with gallery-dl’s notes: true; the note-carrying booru families get it by family
options gallery-dl options merged into the managed config for this site; shareable, so never secrets
tag_rules per-tag corrections keyed by normalized name: "" suppresses the tag, a bare name renames it, category:name retargets it