execDownload's return shape — verified against every real legacy Download/*.pm's own
provide_url sub. The host performs the real byte-level HTTP transfer itself now (Rust-side
streaming download — see specs/005-download-plugin-progress/plan.md's Summary for why: this
is what makes real progress reporting, per-domain concurrency limiting, and rate limiting
possible at all), rather than treating a plugin's result as an opaque already-downloaded blob.
Exactly one of downloads, file_path, or error is present in
a given result; a downloads array, if present, has at least one element.
downloads: DownloadRequest[]
One element = a single-file download (e.g. Chaika/EHentai-style plugins, which resolve one
real download URL); more than one element = a multi-resource download (e.g. Pixiv's per-page
images), assembled per the plugin's PluginOptionsResult.bundle_as_archive
setting.
file_path: string
Pre-existing fallback escape hatch: a plugin that already downloaded/wrote a file itself and
hands back a local path. Mutually exclusive with downloads; does not receive
progress/concurrency/rate-limit treatment since the transfer already happened entirely inside
the plugin process by the time this returns.
version_history: VersionHistoryEntry[]
The full known version-history of this download's series, as this plugin's site exposes it — every source URL the plugin can enumerate, each with the timestamp the site reports for it.
"Every node" is a real requirement, not a best effort: the host can only detect a relationship
to a revision that appears in this list, so an omitted node means an older download silently
passes as unrelated. Mind the traversal when a site only exposes upward links. E-Hentai is the
worked example: a gallery names its parent_gid, never its children, and current_gid jumps
straight to the chain tip — so walking outward from the requested gallery (up its parents, then
one hop to the tip) skips every revision in between. Jumping to the tip first and then
following parent_gid down to the root is what actually visits all of them.
The plugin performs NO comparison against the library and reaches NO conclusion — it just
reports what the site says exists. The host walks this list against its own catalogue to decide
whether/what to overwrite (see PluginOptionsResult.relative_newer_policy/
PluginOptionsResult.relative_older_policy). Keeping the judgment host-side means
one tested implementation shared by every plugin, rather than each (especially AI-generated)
plugin reinventing a multi-hop reachability walk with its own edge-case bugs.
Every source MUST already be passed through this plugin's own canonicalizeSource
— the host compares these against stored source: tags by plain string equality, so an
un-normalized entry silently fails to match with no error surfaced anywhere. Include the
just-downloaded item itself as one of the entries; the host locates it by matching this
download's own source URL. Order does not matter — the host sorts by posted_at.
Absent when the plugin's site has no version-history concept, or this particular download has none (e.g. a first-ever upload with no parent and no later revision).
error: PluginError