Formulario de ajustes (apiVersion 6)¶
Los ajustes de siempre guardan valores que tu código lee con kino.config.
Desde "apiVersion": 6 (Kino 0.9.50) el formulario también puede mostrar cosas y hacer cosas,
y cada plugin instalado tiene su propia pestaña en Ajustes. Esta página tiene todos los tipos de
campo, los tres tipos sin valor, las funciones que los llenan y los revisan, y
un ejemplo completo.
Su propia pestaña en Ajustes¶
Cada plugin instalado y encendido tiene su propia pestaña en Ajustes (celular y TV), con el nombre
del plugin y su interruptor de Modo debug; cuando tiene settings, la pestaña
también muestra su formulario, y Plugins ▸ Configurar abre el mismo formulario. Por eso, desde Kino 0.9.50,
el error auth_required dice "Configura {plugin} en Ajustes ▸ {plugin}" cuando tu plugin declara
ajustes.
Todos los tipos de campo¶
El formulario muestra tus ajustes en el orden del manifiesto, cada uno con su label y, debajo, su
hint. Nueve tipos:
type |
Lo que ve la persona | Lo que devuelve kino.config.get(key) |
Desde |
|---|---|---|---|
text |
un campo de texto (máximo 500 caracteres) | el texto, o undefined |
apiVersion 1 |
password |
un campo de texto oculto (máximo 500), guardado cifrado en el aparato | el texto, o undefined |
apiVersion 1 |
url |
un campo de dirección (máximo 2.048): http/https; ese servidor pasa a ser un host al que tu plugin puede llegar (Los servidores de la persona) |
la dirección tal como se escribió, o undefined |
apiVersion 1 |
toggle |
un interruptor | true / false (false si no hay default) |
apiVersion 1 |
select |
una elección entre options |
el value escogido (la primera opción si no hay default) |
apiVersion 1 |
list |
una lista que la persona arma con "Agregar", cada entrada un diálogo con los fields de la lista |
un arreglo de { [clave del campo]: texto }, o undefined si está vacía |
apiVersion 4 |
section |
un título, con hint como explicación |
nada | apiVersion 6 |
status |
una línea de solo lectura que llena tu settingsStatus() |
nada | apiVersion 6 |
action |
un botón que corre tu action(key) |
nada | apiVersion 6 |
Lo que puede llevar cada entrada:
| Atributo | Tipos | Regla |
|---|---|---|
key |
todos | obligatorio, ^[a-z][a-zA-Z0-9_]{0,31}$, único en la lista |
label |
todos | obligatorio, de 1 a 40 caracteres |
type |
todos | obligatorio, uno de los nueve de arriba |
hint |
todos | opcional, máximo 80 caracteres: el ejemplo o la explicación debajo del campo. En una section, hasta 300 y se parte en varias líneas (Kino 0.9.51; antes de 0.9.51 se rechaza una de más de 80) |
required |
text, password, url, list |
opcional true/false. Un ajuste obligatorio sin valor detiene toda llamada ("Falta configurar") |
default |
text, password, toggle, select |
opcional. Nunca en url ni en list ("… no puede tener valor por defecto: usa "hint"") ni en los tres tipos sin valor; tiene que servir para su tipo (el default de un select es uno de sus value) |
options |
solo select |
obligatorio: de 1 a 20 { "value", "label" }, value de 1 a 40 caracteres y sin repetir, label de 1 a 40 |
fields |
solo list |
obligatorio: de 1 a 4, cada uno { key, label, type, hint?, required? } con type "text" o "url", sin default ("Solo un ajuste de tipo list tiene "fields"") |
max |
solo list |
opcional, número entero de 1 a 50 (20 por defecto): cuántas entradas |
confirm |
solo action, apiVersion 6 |
opcional, de 1 a 120 caracteres: se pregunta antes de correr la acción, con Cancelar enfocado ("Solo un ajuste de tipo action tiene "confirm"") |
Cualquier otra clave de una entrada se ignora. Un manifiesto que incumple una regla se rechaza al
instalar con un mensaje que nombra el ajuste (El ajuste "quality" necesita opciones); una list por
debajo de apiVersion 4 o un section/status/action por debajo de apiVersion 6 también se rechazan.
Límites: 12 ajustes con valor (text, password, url, toggle, select, list) y, desde
apiVersion 6, 16 sin valor.
No hay campos condicionales: todo ajuste se muestra siempre, tenga lo que tenga otro. Usa un
section con hint para decir qué campos van juntos ("Opcional: sin cuenta ves el catálogo gratis"),
déjalos opcionales, y deja que validateSettings rechace una combinación que no tenga
sentido.
¿password o secrets sellados? Un ajuste password es la credencial de la persona: ella la
escribe, tu código la lee y viaja sellada de punta a punta a sus otros aparatos. Una clave que es
tuya, de quien hizo el plugin (una clave fija del reproductor del sitio), va en los
secrets sellados del manifiesto, y tu código solo tiene un marcador.
Tres tipos que no guardan valor¶
Desde apiVersion 6 hay tres tipos que no guardan ningún valor (nunca están en kino.config, nunca son
required, sin default), máximo 16 de ellos además de los 12 ajustes con valor:
"settings": [
{ "key": "account", "label": "Tu cuenta", "type": "section", "hint": "Opcional: sin cuenta usas la sesión anónima" },
{ "key": "email", "label": "Correo", "type": "text" },
{ "key": "password", "label": "Contraseña", "type": "password" },
{ "key": "linked", "label": "Estado", "type": "status" },
{ "key": "logout", "label": "Cerrar sesión", "type": "action", "confirm": "¿Cerrar la sesión de esta cuenta?" }
]
section: un título;hintes su explicación.status: una línea de solo lectura. Kino llama tusettingsStatus()cuando se abre el formulario (10 s) y muestra el texto que devuelves bajo esa clave:{ linked: "Vinculada como ana@…" }(máximo 200 caracteres). Mientras no responde la línea dice "Cargando…"; una clave que falta o un valor que no es texto muestran "Sin información"; un tiempo agotado o un error muestran "No se pudo consultar". El resto del formulario sigue funcionando en todos los casos. Export obligatorio cuando existe un ajustestatus.action: un botón. Kino llama tuaction(key)(30 s), una acción a la vez (los otros botones y Guardar esperan), y muestra elmessageque devuelves (máximo 300 caracteres) o "Listo"; si lanza un error o se pasa del tiempo, la persona ve el texto del error. Kino vuelve a pedirsettingsStatus()después de cada acción (y al abrir el formulario), así que las líneas de estado describen lo que acaba de hacer;refresh: truese sigue aceptando y no cambia nada.confirm(de 1 a 120 caracteres) pregunta antes, con Cancelar enfocado. Export obligatorio cuando existe un ajusteaction.
Olvidar ajustes desde una acción (clearSettings)¶
Una acción también puede olvidar ajustes propios con clearSettings: hasta 12 claves de tus ajustes
con valor declarados (text, url, password, toggle, select, list). Cuando la acción responde,
Kino los vacía exactamente como si la persona hubiera vaciado los campos y oprimido Guardar (una
password también sale del Keystore), cierra tu sandbox y olvida las cookies y las filas de Inicio
guardadas como con cualquier cambio guardado (kino.storage sobrevive), vuelve a cargar el formulario y
vuelve a pedir settingsStatus(); tu message igual se muestra. Así un botón "Cerrar sesión" evita que
el siguiente token vencido vuelva a iniciar sesión con la cuenta guardada.
export async function action(key) {
if (key === "logout") {
await api.logout(); // avisa primero al servidor: un error aquí conserva la cuenta guardada
return { message: "Sesión cerrada", clearSettings: ["email", "password"] };
}
}
(Aquí email y password tienen que ser ajustes opcionales; marca la cuenta required solo si el
plugin no puede funcionar sin sesión.)
Límites:
- Solo se toca el plugin de la propia acción, nunca otro.
- Una entrada que no es texto, que no existe, que nombra un
section/status/action, que nombra un ajusterequired(vaciarlo haría fallar todas las llamadas siguientes), que se repite o que viene después de la duodécima se descarta con una línea en el log (run.mjsmuestra[dropped by Kino]); un valor que no es un arreglo se ignora. - Una acción que lanza un error o se pasa del tiempo no vacía nada.
Validar antes de guardar (validateSettings)¶
validateSettings(values) (opcional, 20 s) corre antes de que Kino guarde. values trae solo los
ajustes con valor: textos sin espacios en los extremos, toggles como booleanos, listas como arreglos de
objetos.
- Devuelve
nullpara aceptar. { password: "La contraseña no es correcta" }para rechazar con ese texto debajo del campo (máximo 200 caracteres), o un texto para rechazar con un mensaje general. Una clave que no es uno de tus ajustes con valor también rechaza, y su texto se muestra como mensaje general.- Con un rechazo no se guarda nada y la persona conserva lo que escribió.
- Si lanza un error, se pasa del tiempo o responde algo que Kino no puede leer (un arreglo, un número…), tampoco se guarda nada, y el formulario ofrece "Guardar sin comprobar"; ese botón solo aparece entonces, nunca después de un rechazo con mensajes.
Reglas comunes¶
- Los textos que devuelves se muestran después de que Kino les quita cualquiera de tus secretos guardados.
- Estos tres exports corren aunque un ajuste
requiredsiga vacío, así una línea de estado puede decir qué falta. - Guardar cualquier ajuste cierra tu sandbox, como siempre;
kino.storagesobrevive. - Si declaras
telemetry, una falla desettingsStatus,actionovalidateSettingsse reporta como la de cualquier otra función (Registro y telemetría).
Pruébalos con el kit (imprime lo que la app conservaría): node sdk/run.mjs . settingsStatus,
node sdk/run.mjs . action logout, node sdk/run.mjs . validateSettings '{"email":"ana@x.co"}' (--raw
imprime tu respuesta sin tocar). El kit no aplica el tapado de secretos de la app, y validateSettings
recibe exactamente el JSON que escribes: la app solo envía los ajustes con valor, sin espacios en los
extremos. Un ejemplo que funciona es plugins/sdk/test/settings-demo en el repositorio de Kino.
Un ejemplo completo¶
Una cuenta opcional, una línea de estado, un botón "Cerrar sesión", una elección de calidad, un
interruptor y una lista de espejos propios de la persona, todo leído como lo declara
kino.d.ts:
{
"id": "mi-cuenta", "name": "Mi cuenta", "version": "1.0.0", "apiVersion": 6, "entry": "plugin.js",
"hosts": ["api.example.com"],
"capabilities": ["search", "resolve"],
"settings": [
{ "key": "account", "label": "Tu cuenta", "type": "section", "hint": "Opcional: sin cuenta ves el catálogo gratis" },
{ "key": "email", "label": "Correo", "type": "text", "hint": "ana@correo.com" },
{ "key": "password", "label": "Contraseña", "type": "password" },
{ "key": "linked", "label": "Estado", "type": "status" },
{ "key": "logout", "label": "Cerrar sesión", "type": "action", "confirm": "¿Cerrar la sesión de esta cuenta?" },
{ "key": "playback", "label": "Reproducción", "type": "section" },
{ "key": "quality", "label": "Calidad", "type": "select", "default": "auto",
"options": [{ "value": "auto", "label": "Automática" }, { "value": "hd", "label": "Alta" }, { "value": "sd", "label": "Ahorro de datos" }] },
{ "key": "spanishSubs", "label": "Solo subtítulos en español", "type": "toggle", "default": true },
{ "key": "mirrors", "label": "Espejos propios", "type": "list", "max": 5,
"fields": [{ "key": "url", "label": "Dirección", "type": "url", "required": true, "hint": "https://espejo.example.org" },
{ "key": "name", "label": "Nombre", "type": "text", "hint": "Espejo de la casa" }] }
]
}
const API = "https://api.example.com";
// Keyed by the account: settings can change under the plugin (another device, clearSettings).
const sessionKey = () => `session:${kino.config.get("email") ?? ""}`;
async function login(email, password) {
const r = await kino.fetch(`${API}/login`, { method: "POST", body: { json: { email, password } } });
if (r.status === 401) throw kino.error("auth_required", "login 401");
if (!r.ok) throw kino.error("unavailable", `login ${r.status}`);
return r.json().token;
}
async function session() {
await null; // the first await before anything can throw
const email = kino.config.get("email"); // undefined: signed out, free catalog
if (!email) return null;
const saved = kino.storage.get(sessionKey());
if (saved) return saved;
const token = await login(email, kino.config.get("password") ?? "");
kino.storage.set(sessionKey(), token, { ttlMs: 12 * 60 * 60 * 1000 });
return token;
}
export async function settingsStatus() { // one text per "status" key
const email = kino.config.get("email");
if (!email) return { linked: "Sin cuenta: ves el catálogo gratis" };
try {
await session();
return { linked: "Cuenta vinculada" };
} catch (e) {
return { linked: e.code === "auth_required" ? "El correo o la contraseña no coinciden" : "No se pudo revisar la cuenta ahora" };
}
}
export async function action(key) { // one call per "action" button
if (key !== "logout") return null;
const token = kino.storage.get(sessionKey());
if (token) await kino.fetch(`${API}/logout`, { method: "POST", headers: { Authorization: `Bearer ${token}` } });
kino.storage.remove(sessionKey());
return { message: "Sesión cerrada", clearSettings: ["email", "password"] };
}
export async function validateSettings(values) { // before saving: null accepts
await null;
if (values.email && !values.password) return { password: "Escribe la contraseña de esa cuenta" };
if (values.password && !values.email) return { email: "Escribe el correo de la cuenta" };
if (values.email) {
try {
await login(values.email, values.password);
} catch (e) {
if (e.code === "auth_required") return { password: "El correo o la contraseña no coinciden" };
throw e; // the person may "Guardar sin comprobar"
}
}
return null;
}
export async function search(query) {
const token = await session();
const r = await kino.fetch(`${API}/search?q=${encodeURIComponent(query.q)}`,
{ headers: token ? { Authorization: `Bearer ${token}` } : {} });
if (!r.ok) throw kino.error("unavailable", `search ${r.status}`);
return r.json().results.map((x) => ({ id: String(x.id), ref: String(x.id), title: x.title, kind: "movie" }));
}
export async function resolve(ref) {
const token = await session();
const quality = kino.config.get("quality"); // "auto", "hd" or "sd": a select always has a value
const r = await kino.fetch(`${API}/play/${encodeURIComponent(ref)}?q=${quality}`,
{ headers: token ? { Authorization: `Bearer ${token}` } : {} });
if (!r.ok) throw kino.error("not_found", `play ${r.status}`);
const s = r.json(); // { url, path, subtitles: [{ lang, url }] }
const mirrors = kino.config.get("mirrors") ?? []; // [{ url, name? }]: each url is a server the person typed
return {
url: s.url,
label: "Principal",
subtitles: kino.config.get("spanishSubs") ? s.subtitles.filter((t) => t.lang === "es") : s.subtitles,
alternatives: mirrors.slice(0, 8).map((m) => ({ url: m.url.replace(/\/+$/, "") + s.path, label: m.name || "Espejo" })),
};
}
Lo que recibe la persona: una pestaña "Mi cuenta" en Ajustes con dos títulos; "Estado" dice "Sin cuenta: ves el catálogo gratis" hasta que escriba una cuenta; Guardar revisa la cuenta antes de guardarla; "Cerrar sesión" pregunta primero y después vacía el correo y la contraseña en todos sus aparatos; cada espejo aparece como una copia más en el menú Servidor del reproductor.
En los otros aparatos de la persona¶
Cuando la persona empareja dos de sus aparatos (celular y TV), Kino mantiene sus plugins al día entre ellos. Lo que pasa y lo que no:
- Pasa: la instalación, las actualizaciones, encendido/apagado, la desinstalación y las aprobaciones de hosts. Cada aparato descarga tu código él mismo; solo viajan la dirección y la aprobación. El otro aparato instala en silencio cuando lo que descargó no pide nada más de lo que la persona aprobó en el primero; si ahora pides más (un host, permiso o capacidad nueva), espera en "Plugins de tus otros aparatos" con la hoja de consentimiento normal.
- Pasa: los valores de los ajustes
text,select,toggle,urlylist(unalistque tiene un campopasswordno viaja), y los ajustespassword, sellados de punta a punta con la llave que los aparatos acordaron al emparejarse, nunca legibles en el camino. - Pasa: los vaciados. Un campo que la persona vacía, o el
clearSettingsde una acción, también se vacía en sus otros aparatos (contraseñas incluidas). Un ajusterequirednunca se vacía a distancia. Un aparato con una versión de Kino más vieja ignora los vaciados y conserva su valor. - Nunca pasa:
kino.storage, las cookies, las filas de Inicio guardadas.
Un valor que llega así se aplica sin tu validateSettings y, como cualquier cambio guardado, cierra
tu sandbox. Así que escribe el plugin como si un ajuste pudiera cambiar en cualquier momento: ponle a
cualquier sesión que guardes en kino.storage como clave la cuenta a la que pertenece, y nunca pongas
una identidad del aparato en un ajuste, porque se copiaría al otro aparato.
const sessionKey = `session:${kino.config.get("email") ?? ""}`; // no solo "session"
Un plugin firmado instalado desde dos repositorios distintos en dos aparatos cuenta como el
mismo plugin cuando tiene el mismo id y la misma llave de autor: mira
El mismo plugin en dos direcciones.