Dieses Dokument gibt es nur auf Englisch.

Usage

ddb — a CLI for the Deutsche Digitale Bibliothek (DDB) v2 API. This is the use-case-driven cookbook; for the option reference see the README, and for domain terms the Glossary.

ddb [global options] <command> [command options]

The v2 read routes are public — no API key. Just run the commands.

Global options

Option Description
--base-url <url> API base URL (default …/2; only http:/https: accepted; plain http: to a non-loopback host logs one WARN record of ddb.http, … unencrypted …, on stderr)
--timeout <ms> per-request timeout in ms (0 = no timeout)
--user-agent <ua> User-Agent header value
--max-retries <n> retries for transient 429/503 responses and reset connections (0..10; each waits the server’s Retry-After, up to 30 s; each retry logs one WARN record of ddb.http before it waits)
--max-response-bytes <n> cap the response body size in bytes (0 = unlimited; default 100 MiB)
--compact print JSON on a single line (for piping to jq)
--log-format <format> How errors, warnings and notes are written to stderr: text (default; log4j style, 2026-10-09T14:03:12.481Z WARN [ddb.http] …) or jsonl (one JSON object per line: ts, level, topic, msg). stdout is not affected
-o, --output <file> write output to a file instead of stdout (- = stdout; refuses to overwrite an existing file unless --force, and a directory always — checked before any request, exit 1)
--force overwrite the --output file if it already exists (needs --output)
-V, --version / -h, --help version / help

An option that takes one value may be given once: a second --rows, --sort, --base-url … is a usage error (exit 2) instead of silently replacing the first. --filter and --facet are the exceptions: they collect every value.

A --base-url on plain http: to a host other than the loopback interface gets one warning on stderr before the first request, a WARN record of ddb.http, e.g. … WARN [ddb.http] requests to mirror.example are sent unencrypted (http:, not https:), or … WARN [ddb.http] the base URL's credentials are sent unencrypted to mirror.example (http:, not https:) when it carries a user:password@ (never printed). stdout and the exit code are unchanged; --help, --version and usage errors never warn.

search — find objects

ddb search <query> [options]

v2 search is a Solr passthrough: the query and --filter use Solr syntax, and the response is native Solr JSON. Some starting points:

ddb search Goethe                         # simple keyword
ddb search "Weimarer Republik"            # phrase (quote it in your shell)
ddb search 'Goethe AND Faust'             # boolean operators
ddb search '*:*'                          # match everything (browse mode)
ddb search 'title:Faust'                  # field-scoped query
Option Description
--rows <n> number of documents (Solr rows, default 10; at most 2147483647)
--offset <n> offset of the first document (Solr start, paging; at most 2147483647)
--sort <spec> Solr sort, e.g. "score desc" or "id asc"
--fields <list> fields to return (Solr fl), e.g. id,label,type
--filter <fq> Solr filter query (repeatable), e.g. type_fct:mediatype_002
--facet <field> return counts for this facet field (repeatable)
--facet-limit <n> cap the number of values per facet (needs --facet; Solr’s default is 100, -1 = all, still sorted by count)
--collection <name> Solr collection (default search)
--handler <name> Solr request handler (default select). Only handlers that return Solr’s standard response envelope (numFound, docs) are supported; any other answer fails (exit 1) with an error naming the handler

The result shape is native Solr:

{
  "responseHeader": { "status": 0, "params": { "q": "Goethe", "rows": "10" } },
  "response": {
    "numFound": 99866,
    "start": 0,
    "docs": [ { "id": "…", "label": "…", "type": ["mediatype_002"] } ]
  },
  "facet_counts": { "facet_fields": { "type_fct": ["mediatype_003", 50024] } }
}

Filtering with facets

--filter <fq> narrows the result set with a Solr filter query; --facet <field> asks Solr to return value counts for a facet (to build the next filter):

# Images (a media-type code) from Berlin
ddb search '*:*' --filter type_fct:mediatype_002 --filter 'place_fct:"Berlin"'

# Two places at once (OR inside one filter query)
ddb search Bauhaus --filter 'place_fct:("Berlin" OR "Dessau")'

# Ask for the object-type distribution of a query, top 5 values
ddb search Goethe --rows 0 --facet objecttype_fct --facet-limit 5 \
  | jq '.facet_counts.facet_fields.objecttype_fct'

Facet counts arrive as a flat array [value, count, value, count, …], which can include values with count 0. To turn one into {value, count} objects and drop the zeros:

ddb search Bauhaus --rows 0 --facet place_fct --facet-limit 10 \
  | jq '.facet_counts.facet_fields.place_fct
        | [range(0; length; 2) as $i | {value: .[$i], count: .[$i+1]}]
        | map(select(.count > 0))'

Paging

# First page (1–10) then the next page (11–20)
ddb search Goethe --rows 10
ddb search Goethe --rows 10 --offset 10

When more documents match than were returned, ddb logs a note (an INFO record of ddb.api) like … INFO [ddb.api] 99866 documents match; 10 shown. to stderr (past the first page with the positions, e.g. 10 shown (11–20)) — page with --offset or narrow with --filter. Read response.numFound for the true total. A --rows 0 facet query gets no note.

Paging past the end — an --offset at or beyond the total — is not an error: Solr answers with an empty docs and ddb exits 0, but stderr says so, naming the total and the offset asked for:

ddb search 'Goethe Faust' --offset 1000000 --rows 2 --fields id
# stderr: … INFO  [ddb.api] 6584 documents match; --offset 1000000 is past the end, so none are shown.

A paging loop can stop when response.docs is empty or --offset reaches response.numFound.

item — object detail

ddb item <id> [--part <component>] [--lang <code>]

<id> is the exact 32-character id from a search result’s id (upper-case letters and digits; anything else is a usage error, exit 2). --part selects which component to fetch — most are JSON, a few are XML / a plain file and print raw (so > file.xml and piping keep them intact). With -o, a redirect or a pipe you get the upstream bytes exactly — any charset, CRs included, nothing appended; only on a terminal is the body decoded (by its declared charset, so a Latin-1 record shows its umlauts) and control characters removed first:

--part Returns Format
view (default) the data set a DDB frontend object page is built on JSON
aip the full Archive Information Package JSON
edm the Europeana Data Model record RDF/XML
binaries related binary files (thumbnails, media) and their URLs JSON
children child items (accepts --rows/--offset, the only part that does) JSON
parents the chain up the hierarchy: the object itself first, the institution last JSON
source the ingest source metadata JSON
source-description a description of the source record JSON
source-record the raw provider record (METS/MODS, LIDO, MARCXML); see below for sub-items XML
iiif the IIIF Presentation manifest (only where present → else 404) JSON
citation a newspaper-issue citation file (only where applicable) BIB file

A sub-item’s source-record may belong to an ancestor. Sections of a digitised book and units of an archive finding aid have no provider record of their own: the API answers 303 and points to an ancestor’s record (in a Location without the /2 prefix, which can’t be followed as sent). ddb fetches that ancestor’s record through the base URL — up to maxRedirects (5) levels — prints it, and notes on stderr whose it is (an INFO record of ddb.api): item JG3Y… has no source-record of its own; this is the source-record of its ancestor JY7H…. That file is the whole parent record (the volume’s METS), not one for the section. If no ancestor has one, the 404 (exit 4) names the chain: … item N4N7… has no source-record of its own; the API points to its ancestors (N4N7… → NSNK… → RQBX…), and the last has none either.

--lang <code> sets the preferred label language for view/aip/edm/binaries/source/source-description (with any other part it is a usage error, exit 2, as are --rows/--offset outside children). An object without a record in that language answers 404 (exit 4); --lang en did so for every object tried on 2026-09-15, so retry without --lang before treating the id as wrong.

ID=$(ddb search Goethe --fields id | jq -r '.response.docs[0].id')
ddb item "$ID"
ddb item "$ID" --part edm -o object.edm.xml     # RDF/XML → file
ddb item "$ID" --part binaries | jq '.'
ddb item "$ID" --part children --rows 20         # first 20 children

version — connectivity check

ddb version        # e.g. 7.5

Useful to confirm the API is reachable and the CLI is wired up. Anything but a short version token — an HTML page from a captive portal or proxy login, JSON, an empty body — fails with exit 1 (“expected a version string such as “7.5””), so a wrong --base-url that answers 200 doesn’t pass the check.

Scripting recipes

# Object ids + labels of the current page
ddb search Goethe | jq -r '.response.docs[] | "\(.id)\t\(.label)"'

# Total number of matches
ddb search Goethe | jq '.response.numFound'

# Top 10 providers for a query (flat facet array)
ddb search Goethe --rows 0 --facet provider_fct --facet-limit 10 \
  | jq '.facet_counts.facet_fields.provider_fct'

# Save a full result page to disk
ddb --output goethe.json search Goethe --rows 100

Exit codes

Code Meaning
0 success (help/version included); an empty result also exits 0
1 API/logical error, or a catch-all (includes an unexpected 403)
2 usage error (bad flags, unknown command, wrong-length item id, bad --base-url)
4 HTTP 404 (not found)
6 network / transport failure (DNS, connection, timeout, response size-cap)

A reader that stops early (ddb search … | head) ends the run quietly with exit 0. If stderr’s reader is gone (2>&1 | true), a failed run still exits with its own code.

Notes

Quelle auf GitHub ansehen →