Probar en local¶
El kit de Node es la carpeta sdk/ de los plugins de ejemplo (de dónde sacarla):
run.mjs (ejecuta una función), validate.mjs (revisa un plugin como lo hace Kino), init.mjs (crea
el esqueleto de uno nuevo), kino-shim.mjs (la API kino en Node), contract.mjs (las reglas,
leídas de contract.json), seal.mjs (sella un secreto para tu manifiesto, y con --keygen/--sign firma un plugin firmado) y
guide-tables.mjs (regenera las tablas de la guía). No hay nada que
instalar. Necesita Node 18 o más nuevo (probado en 18.20, 20.11 y 24.14);
node --test sdk/test/kit.test.mjs ejecuta sus propias pruebas.
node sdk/run.mjs ./plugin.js search "metropolis"
node sdk/run.mjs ./plugin.js home
node sdk/run.mjs ./plugin.js browse films 2
node sdk/run.mjs ./plugin.js episodes 'Dragnet1951'
node sdk/run.mjs ./plugin.js resolve 'Dragnet1951|Dragnet/Season 1/Dragnet (1951) - S01E01 - The Human Bomb.mp4'
El primer argumento es tu archivo de entrada (o la carpeta que tiene kino-plugin.json), luego la
función, luego su argumento: el texto a buscar, el ref para episodes y resolve, o el ref y un
cursor opcional para browse. El runner lee tu manifiesto, pone el global kino, llama esa única
función como lo hace Kino, revisa la respuesta con las reglas de la app e imprime en stdout, como
JSON, lo que Kino conservaría; cada entrada que Kino descartaría sale en stderr con el motivo (--raw
imprime tu respuesta sin tocar). Los logs, console.* y los errores van a stderr, así que puedes
encadenar el resultado (... | head -30, ... | jq). El código de salida es 0 si todo sale bien, 1
cuando tu código lanza un error y 2 cuando el comando está mal. El runner solo ejecuta funciones que
tu manifiesto declara.
--config key=value(repetible) fija un ajuste; el runner también leesdk/config.json({ "server": "http://192.168.1.10:8096", "user": "ana" }; déjalo por fuera de git). Un ajuste obligatorio sin valor frena la ejecución conauth_required, como en la app.--record fixtures.jsonguarda cada respuesta dekino.fetch;--replay fixtures.jsonresponde solo desde ese archivo, sin red. Graba una vez y tus pruebas corren sin conexión y siempre igual (eltest/plugin.test.mjsdel esqueleto hace exactamente eso).KINO_TYPE=movie|series|anyfija eltypede la búsqueda (por defectoany).- Para llenar los otros campos de la consulta, pasa la consulta completa como JSON:
node sdk/run.mjs ./plugin.js search '{"q":"dragnet","type":"series","year":1951}'(season,episode,tmdbIdyyearson0si no). - En el kit de Node,
kino.storagees un archivo llamado.kino-storage.jsony el tarro de cookies.kino-cookies.json, los dos al lado de tu manifiesto. Agrégalos a tu.gitignore. Bórralos para empezar de cero. Un plugin consecretstambién lee.kino-secrets.jsonde la misma carpeta (abajo).node sdk/init.mjsya pone los tres en el.gitignoredel esqueleto. node sdk/validate.mjs <folder>revisa el manifiesto con todas las reglas de el manifiesto (los mismos mensajes en español que muestra la app) y que cada capacidad declarada esté exportada, e imprime las líneas extra de la hoja de consentimiento tal como las leerá la persona (las rojas, un hostinsecureHttp,"liveStreamHosts": "any"o"streamHosts": "any", marcadas "(en rojo)";secretsagrega "Usa datos sellados por su autor", con una nota de que solo la app puede comprobar para qué repositorio se sellaron);--run <function> [argument]además la ejecuta y lista lo que Kino descartaría. Con--run liveCategories, cada lista declarada también se descarga y se analiza: una que no se puede descargar o que da 0 canales es un problema, y se listan sus entradas descartadas. El código de salida 0 significa que Kino la aceptaría.- La carpeta
sdk/no tiene que vivir en tu repositorio. Cópiala a cualquier parte y ejecutanode /ruta/a/sdk/run.mjs ./plugin.js .... - Un stack trace nombra un
plugin.mjstemporal: el runner carga una copia de tu archivo para que Node lo trate como módulo ES sin importar su versión ni lo que digapackage.json. Los números de línea son los de tuplugin.js.
Secretos sellados (apiVersion 4)¶
El kit nunca puede abrir un sello: no tiene la clave privada. Por eso lee los valores en claro
directamente de .kino-secrets.json al lado de tu manifiesto ({ "apiKey": "..." }; déjalo fuera de
git, como hace el .gitignore del esqueleto) y simula todas las reglas de
kino.secret: los marcadores, el cambio dentro de kino.fetch, la revisión de
hosts del manifiesto por https en cada salto, las restricciones de kino.crypto y el tapado de lo que
vuelve. --record tampoco escribe nunca el valor en claro en un archivo de fixtures: en su lugar va
un marcador fijo, así que una grabación que subes a git nunca lleva un secreto, se reproduzca como se
reproduzca después.
Para hacer el sello: node sdk/seal.mjs --repo owner/repo --name apiKey, y escribe el valor en el
prompt oculto (o pásalo por stdin). Sella para el repositorio desde el que la gente va a instalar, y
prueba la versión sellada en la app instalada desde su rama principal, sin @ref
(por qué).
Plugins firmados (apiVersion 5)¶
validate.mjs comprueba un plugin firmado como lo hace Kino: la forma del campo
signature, la firma misma contra tu archivo de entrada (--repo owner/repo[/carpeta], o el origin
de GitHub de la carpeta si lo omites), que ningún *.pem esté rastreado por git, e imprime la huella
de la clave del autor y la línea de consentimiento "Firmado por su autor". También rechaza
"entry": "./plugin.js" (Kino 0.9.45 y anteriores no lo instalan) y avisa cuando hosts tiene más de
20 entradas (Kino 0.9.44 y anteriores lo rechazan). Firma otra vez después de cada cambio en el archivo
de entrada o en la version.
Canales en vivo (apiVersion 3)¶
Los exports de channels se ejecutan con live, con la carpeta del plugin primero:
node sdk/run.mjs . live categories
node sdk/run.mjs . live channels noticias
node sdk/run.mjs . live channels noticias 2
node sdk/run.mjs . live guide canal1,canal2
node sdk/run.mjs . live search noticias
node sdk/run.mjs live playlist https://iptv-org.github.io/iptv/countries/co.m3u
node sdk/run.mjs live playlist ./lista.m3u --epg ./guia.xml.gz
live categoriesllamaliveCategories()e imprime lo que Kino conserva. Luego, por cada{ playlist }de la respuesta, descarga la lista como lo haría la app (tusheaders, solo tushostso el servidor de la persona, cada redirección también) e imprime, en stderr, el mismo resumen quelive playlisty los grupos de la lista como las categorías que verá la gente.live channels <categoryId> [cursor]llamaliveChannels({ categoryId, cursor })y luego reproduce, como lo haría Kino, el primer canal que tengarefy nostream: le manda eserefaresolve()y revisa la respuesta como la de un canal en vivo (así que aplica"liveStreamHosts": "any"). Convalidate.mjs --run liveChannels, una respuesta rechazada ahí es un problema.live search <consulta>llamaliveSearch({ query }), imprime los canales que Kino conserva (máximo 100) y reproduce el primero que tengaref, comolive channels.resolve <ref> --liverevisa una respuesta deresolve()como la de un canal en vivo. Sin--liveel kit no puede saber que elrefes de un canal y aplica la regla estricta; cuando solo eso frena la URL y tu manifiesto tiene"liveStreamHosts": "any", dice "si este ref es de un canal en vivo, prueba con --live".live guide <id,id>llamaguide()con esos ids y una ventana de 24 horas que empieza dos horas atrás.live playlist <url|file>no necesita plugin: lee cualquier lista M3U con las reglas de Kino e imprimeN canales en M categorías; K entradas descartadas; L ocultas (adultos), las categorías y los primeros 20 canales comogroup › name url. Con--epg <url|file>también muestra qué tiene al aire cada uno de esos 20, o "sin guía". Una guía que declara un DOCTYPE se rechaza, como en la app, y el comando lo dice: "La guía declara un DOCTYPE; Kino la rechaza por seguridad". Úsalo con una lista antes de escribir una sola línea de plugin.
El kit lee listas y guías con sdk/live-playlist.mjs, una copia de los lectores de la app amarrada a
los mismos archivos de prueba (docs/plugins/fixtures/live en el repositorio de Kino): lo que
conserva es lo que conserva Kino.
Lo nuevo de apiVersion 6¶
node sdk/run.mjs . section [tab] # necesita "section" en el manifiesto
node sdk/run.mjs . categories # necesita la capacidad browse
node sdk/run.mjs . theme # tus colores, sus contrastes y sus respaldos
node sdk/run.mjs . settingsStatus
node sdk/run.mjs . action logout
node sdk/run.mjs . validateSettings '{"email":"ana@x.co"}'
node sdk/run.mjs --within '<ref>' ./plugin.js search "texto" # scopedSearch
node sdk/run.mjs ./plugin.js sign '{"url":"https://cdn.example/seg.ts","kind":"segment","ref":"<ref>","context":"<signContext>"}'
node sdk/run.mjs --retry conflict:1 ./plugin.js resolve '<ref>' # o conflict:1:409
node sdk/validate.mjs . --run liveSearch noticias # marca +18 de los resultados de liveSearch
node sdk/run.mjs ./plugin.js migrate '{"kind":"title","ref":"<ref viejo>"}'
run.mjs muestra como [dropped by Kino] lo que Kino descartaría (clearSettings, alternateHosts),
imprime lo que leería la persona con un userMessage (o por qué no se
mostraría), y validate.mjs avisa de debug antes de publicar, de un scopedSearch que nunca lee
within y de un plugin que usa los pares de llaves de kino.crypto sin "apiVersion": 6. Las páginas:
Formulario de ajustes, Firma por petición,
Pasar lo guardado, Sección, categorías y colores,
Registro y telemetría.
kino.meta y kino.tmdb (Kino 0.9.53)¶
Las dos funcionan en cualquier función que corra el runner, con los sustitutos del kit (cualquier apiVersion):
KINO_META_FIXTURE=meta.json node sdk/run.mjs . home # kino.meta responde desde meta.json; sin él, null
KINO_TMDB_KEY=<tu llave de TMDB> node sdk/run.mjs . home # kino.tmdb le pregunta a TMDB con TU llave donde Kino usa la suya (o "tmdbKey" en sdk/config.json)
KINO_TMDB_FIXTURE=tmdb.json node sdk/run.mjs . search matrix # kino.tmdb responde sin red desde tmdb.json
meta.jsonasocia"<type>:<idKey>:<valor>"o"<idKey>:<valor>"("movie:imdb:tt0133093","tmdb:1399") a una respuesta; responde el primer id de la consulta que tenga entrada, onull(la consulta de TMDB y AniList de Kino y los demás plugins de la persona no existen en Node).tmdb.jsonasocia"<path>?<params ordenados por nombre, codificados>"o solo"<path>"al cuerpo de TMDB ("/trending/movie/week?language=es-MX"); el archivo hace las veces de TMDB y de una llave, y una ruta que no tenga respondenot_found.- Tu llave hace las veces de la de Kino, con el límite de la llave de Kino (20 llamadas cada 10 s); el kit no tiene una
llave de la persona a la que pasar, así que pasado ese límite lanza
rate_limited. - Sin llave ni archivo,
kino.tmdblanzano_tmdb_key, como hace una versión de Kino sin llave propia con una persona sin llave, y el runner muestra la frase que Kino mostraría. Tu llave nunca se imprime. - La validación, los límites, la caché y los códigos de error son los de la app (
kino.meta,kino.tmdb);node sdk/validate.mjsavisa si tu código llama cualquiera de las dos sintypeof kino.<nombre> === "function"(las versiones anteriores de Kino no tienen ninguna). Hay ejemplos de los dos archivos endocs/plugins/fixtures/kino-services/del repositorio de Kino.
Lo que el kit de Node no reproduce¶
Kino es la autoridad; el kit solo se le aproxima para que puedas iterar rápido. Antes de publicar, instala el plugin en la app y pruébalo ahí. Las diferencias:
kino.html.selectlanza un error (usa Jsoup, que solo existe en la app).- El lector XMLTV del kit es un recorrido tolerante con expresiones regulares, no el analizador XML de la app. Da la misma respuesta que la app en todas las guías de prueba compartidas, pero ante un XML mal formado a mitad de documento puede conservar más que la app (que se detiene en el primer error y conserva lo que leyó hasta ahí).
- La trampa del rechazo de Límites y trampas del motor: Node ataja lo que Kino 0.9.49 y anteriores no atajarían.
- Node tiene globales que a Kino le faltan (
setTimeout,fetch,Buffer, ...): el plugin puede pasar en Node y fallar en Kino. ElURLde Kino no tiene punycode. - Las reglas de hosts, de redirecciones y de cantidad de peticiones son las mismas, y las de cookies también hasta donde llega el análisis propio de Node, pero no se rechazan nombres que resuelven a direcciones privadas, los cuerpos siempre se leen como UTF-8, y el tiempo límite de 15 s cubre la espera de la respuesta pero no la descarga.
- El kit nunca pregunta por un host: uno no declarado falla como
host_not_allowedaunque sea duranteresolveoepisodes, donde la app podría preguntarle a la persona. Tampoco tiene el permiso amplio de video;"streamHosts": "any"sí lo aplica. - No se hacen cumplir los límites de tiempo por llamada, el límite de memoria ni los topes de tamaño de peticiones, respuestas y selectores.
- No hay comando
meta, yvalidate.mjsno revisa que un plugin que declarametalo exporte: pruebametaen la app.kino.browser.captureykino.browser.pagesiempre respondenbrowser_unavailable(Navegador oculto).