Writing a provider plugin
A provider plugin is a WebAssembly component: search terms in, episodes and stream URLs out.
anistream loads *.wasm from the plugin directory (see
where things live, or run
anistream --plugins) and treats it like a native source — same ranking, health tracking and
failover. Any WASI 0.2 language works; reference implementations in Rust and JavaScript live in
plugins/.
The interface
Section titled “The interface”wit/anistream-provider.wit is the contract — package anistream:[email protected]:
interface host { // what you may call fetch: func(req: http-request) -> result<http-response, host-error>; log: func(level: string, msg: string); aes-decrypt: func(key: list<u8>, iv: list<u8>, data: list<u8>) -> result<list<u8>, string>; regex-captures: func(pattern: string, haystack: string) -> list<list<string>>; fetch-many: func(reqs: list<http-request>) -> list<result<http-response, host-error>>; config-get: func(key: string) -> option<string>;}
interface provider { // what you must export describe: func() -> manifest; search: func(query: string, translation: string) -> result<list<search-hit>, provider-error>; list-episodes: func(id: string, translation: string) -> result<list<episode>, provider-error>; resolve: func(id: string, episode: string, translation: string) -> result<list<media-stream>, provider-error>; sources: func(id: string, episode: string, translation: string) -> result<list<source-candidate>, provider-error>; resolve-source: func(id: string, episode: string, translation: string, source-id: string) -> result<list<media-stream>, provider-error>;}fetch-many— several requests concurrently, results in request order. Same allowlist and fetch budget, per request. Return all your streams fromresolve: playback falls over to the next stream when one produces no frames, so ordering is a hint, not a verdict.sources— the selectable releases for the Sources overlay, best-first. An empty list means “nothing to choose between”: an answer, not a failure.resolve-source— one candidate’s id back into streams. Never fall back to the automatic pick.aes-decrypt— AES-CBC; the key length selects 128, 192 or 256.config-get— reads[providers.plugins.settings.<your-id>]from the user’s config.toml. Every key is optional; work with all of them unset.
Record fields are documented in the WIT. Easy to miss: synonyms/format on a search hit (title
matching), description/thumbnail/air-date/filler on an episode, format on a subtitle
whose URL has no extension, download-source on a stream.
Errors map one-for-one onto the registry’s failover rules:
| Return | Meaning | Failover? |
|---|---|---|
not-found |
Works, no such title or episode | No — this is an answer |
blocked(msg) |
Bot protection, rate limit, geo-block, ban | Yes |
parse(msg) |
Response arrived, did not look like expected | Yes |
other(msg) |
Anything else | Yes |
Do not flatten not-found into other — that makes every missing episode walk the whole chain.
cargo build --release --target wasm32-wasip2 \ --manifest-path plugins/example-rust/Cargo.toml# then copy the .wasm into the plugin directory — `anistream --plugins` prints itStart from plugins/example-rust/src/lib.rs — it exercises the whole ABI against a stable
endpoint.
JavaScript
Section titled “JavaScript”cd plugins/example-ts && npm install && npm run build# then copy anistream-example-plugin-ts.wasm into the plugin directoryFive differences from Rust:
- Imports carry the package version:
anistream:provider/[email protected]. - Names are lowerCamelCase:
list-episodes→listEpisodes. - Errors are thrown, not returned:
throw { tag: 'not-found' }.fetchneedstry/catch. - Exports run without a receiver —
this.resolve(…)traps; share a plain function. --disable allstrips WASI.
Measured with cargo run -p anistream-plugin --example plugin_bench --release:
| size | compile (once) | per call | |
|---|---|---|---|
| Rust | 0.1 MB | 19 ms | 38 µs |
| JavaScript | 12.2 MB | 1.06 s | 923 µs |
Both run under the same default limits; both are negligible beside one HTTP request. Every call gets a fresh store, so no state survives between calls.
Publishing
Section titled “Publishing”A plugin is a .wasm file someone drops in a directory, so: declare the narrowest
allowed-hosts that works (anistream --plugins shows it to users), name yourself clearly in
display_name, and version describe().