Plugin reference
The file
Section titled “The file”A plugin is one .js file.
Its manifest is a comment that opens the file, and is everything up to the first comment terminator.
Yonto reads the manifest as text and never runs the file to learn it, which is why a permission lives there and not in an export.
The file is an ES module and may await at the top level.
It must not read yonto while it is being evaluated, so read it inside a function.
The manifest
Section titled “The manifest”| Key | |
|---|---|
kind |
"content-source". |
id |
2 to 32 characters: lowercase letters, digits and hyphens, starting with a letter or digit. Installing a plugin with the id of a built-in one replaces it, and removing it brings the built-in back. |
name, version |
What the viewer sees. version is three numbers, 1.2.3, and a new build needs a new version. |
contractVersion |
The newest part of the plugin interface the plugin uses. lint works it out: declaring less is refused, and declaring more is warned about, since it would turn away apps that could run the plugin. |
provides |
"source" is a plugin that is one source, and installing it switches to it. "source-type" is a kind of source a viewer configures, and appears in Add source; it needs a configSchema. |
allowedHosts |
The hosts the plugin may reach, as bare hosts. A *. entry matches the subdomains. An entry that cannot be a host is refused by lint. |
configSchema |
The fields of the Add source form, below. |
description, probeQuery |
A sentence for the install dialog, and a word doctor searches for. |
A key lint does not know is refused, and the message names the nearest key it does know.
A television ignores a key it does not know, so a plugin written for a later interface still installs.
Settings
Section titled “Settings”Each entry of configSchema has an id, a label and a type, and may be required or have a default.
The types are text, secret, url, choice and bool.
- Every answer arrives in
yonto.configas a string, trimmed. - A field left empty is not there, so
yonto.config.x || FALLBACKis how to read one. - A
boolis the string'true'or'false', and'false'is truthy, so compare it:yonto.config.x === 'true'. - A
requiredfield left empty is refused before the first call, naming the field. - A
choicelists itsoptions. lintrefuses aprovides: "source"plugin with a required field and nodefault, since a source that asks nothing has no question.
The host of a url field the viewer fills in is allowed in addition to allowedHosts, which is how a plugin for a server the viewer owns can leave allowedHosts empty.
hostsFromConfig turns the list off entirely.
It is for plugins whose whole job is to read addresses the viewer supplies, such as an XPTV catalog, and a plugin that declares it asks the viewer to confirm first.
Do not use it to avoid writing a host down.
The functions
Section titled “The functions”Four are required, and lint refuses a plugin without them.
| Function | Called with | Answers |
|---|---|---|
getCategories() |
nothing | [{ id, name }], the tabs along Home. |
getMediaList(categoryId, { page, filters, cursor }) |
the category’s id, then one options object: page counts from 1, filters maps a filter id to the chosen option id, cursor is only there if you handed one out |
A list of summaries, or { items, nextCursor } when the source pages by cursor. An empty list ends the listing. |
getMediaDetail(id) |
an id you handed out |
A detail: the summary plus synopsis, genres and playbackOptions. |
search(query) |
the query, verbatim | A list of summaries. |
A summary is { id, title } and may have type, posterUrl, backdropUrl, year and rating, all strings.
type is a plain string, and an unrecognised one falls back to a safe default, so write "movie" and not "MOVIE".
A playback option is { label, stream: { url, mimeType, headers } }.
Instead of stream it may carry pan, a cloud-drive share that the app redeems, or track, a string you will be handed back in getStream.
These are optional.
doctor says which a plugin has.
| Function | What it is for |
|---|---|
getFilters(categoryId) |
Groups of filters for a category. Do not export one that returns []; a source with no filters just has none. |
getRecommendations() |
The featured titles on Home. |
checkHealth() |
A status line for Settings. Without it, a source is healthy if getCategories answers. |
getImageHeaders() |
Headers to send with poster requests. |
getSubSources() |
When one source is several libraries, a picker. |
getStream(token) |
Redeem a track. |
A source that cannot search still exports search, and throws yonto.error.unavailable(reason).
The yonto global
Section titled “The yonto global”A plugin reaches the host through yonto and declares contract 21: no app runs a plugin declaring less.
yonto.config |
The viewer’s settings, an object of strings. |
yonto.fetch(url, { method, headers, body, encoding, redirect }) |
Resolves with { status, url, headers, body, bodyBase64 } for every status. It rejects only when it has no answer, with a code: REQUEST_INVALID, HOST_NOT_ALLOWED, REDIRECT_REFUSED, TIMEOUT, RESPONSE_TOO_LARGE or REQUEST_FAILED. |
yonto.text.decode(…) |
Decode bytes that are not UTF-8, such as GBK, since there is no TextDecoder. |
yonto.html.load(markup), yonto.xml.load(markup) |
cheerio’s $ over a page or an XML document. |
yonto.store.get / set / remove / clear |
A cache kept for this source. set(key, value, ttlSeconds). |
yonto.now() |
The clock. Use it, and never Date.now(), for elapsed time. |
yonto.sleep(ms) |
There is no setTimeout. |
yonto.log(message) |
A log line. |
yonto.partial(reason) |
Says the answer you are about to return is incomplete, such as a search that reached three of four sites. The app shows the sentence under the result. |
yonto.installId(), yonto.subSource() |
Which install this is, and the library the viewer picked. |
yonto.crypto, yonto.encoding |
md5, sha1, sha256, hmacSha256, aesCbcDecrypt, and base64 and hex conversions, with strings in and strings out. |
yonto.cryptoJs(), yonto.jsEncrypt() |
The whole CryptoJS or JSEncrypt library, for a site that needs more. They are slow to start and not secure random, so never make a key with them. |
Requests follow the WHATWG Fetch rules.
A GET with a body, credentials in the URL, and headers the host owns, such as Host or Content-Length, are refused before anything is sent.
Cookie, Origin and Referer you may set.
Errors
Section titled “Errors”Throw one of these, and the app shows the right screen:
| Means | |
|---|---|
yonto.error.notFound(message) |
The title does not exist. |
yonto.error.unauthenticated(message) |
The server refused the credentials the viewer gave. |
yonto.error.misconfigured(reason) |
A field of your own form is blank or wrong, and no retry could help. |
yonto.error.unreachable(reason) |
The server did not answer. The app may rest it. |
yonto.error.unavailable(reason) |
Anything else that went wrong. |
reason and message are shown to the viewer verbatim, in the language of the site you read, since nothing translates them.
Name the part that failed and not the source, because the app’s headline already says Can't reach <source>.
Keep it to one sentence.
Put a status or a code in yonto.log, and never a URL, a key or what the viewer searched for.
An error thrown with an unknown code, or no code, is a METHOD_THREW, which shows as a plugin fault.
Limits
Section titled “Limits”- A call’s own JavaScript may run for 20 seconds in all; time spent waiting on
yonto.fetchoryonto.sleepdoes not count. Once 20 seconds have passed, no newyonto.fetch,yonto.sleeporyonto.storecall may start, and a call that never finishes is ended at 85 seconds. One source runs one call at a time, so a slow call makes everything behind it wait. Fetch in the call that needs the data, and cache what you can. - A response body is at most 16 MB, and less for a body that is not plain text.
Read
bodyBase64in the call that made the request. - The store holds 256 keys, each up to 1 MiB of JSON, per source.
A write past that rejects with
STORE_REFUSED, so catch it and carry on. - A plugin file is at most 1 MiB to install, and the download has 30 seconds.
- A plugin cannot read a file, open a socket, run a timer, or reach a host the viewer was not told about.
- The private-address floor refuses loopback,
10.0.0.0/8,172.16.0.0/12,192.168.0.0/16, link-local and*.localaddresses for every plugin, whatever its manifest says. The one exception is an address a person typed into aurlfield.
The exact definition
Section titled “The exact definition”This page is the short version.
The interface’s full text, with the reasons and every edge case, is contracts/content-source-http.md in kangzj/yonto-plugins, and its schemas are the files beside it.