Tutorial de scripts adaptadores de iPlay
La extensión de navegador de iPlay puede cargar scripts adaptadores externos que convierten entradas multimedia de un sitio web en URL directas para iPlay Direct. El adaptador se ejecuta en la página objetivo. Detecta páginas compatibles, agrega botones de iPlay, resuelve las URL multimedia y pide a iPlay que las abra.
Descargar la plantilla del adaptador
En Chrome y Edge, si cargas un adaptador desde un archivo local o una URL externa, abre la página de detalles de la extensión iPlay en el administrador de extensiones del navegador y activa el permiso "User scripts" o "scripts de usuario". Sin este permiso, es posible que la extensión no pueda inyectar scripts adaptadores externos en las páginas web.
Por qué no usar userscripts
iPlay al principio también probó userscripts para adaptar sitios web, pero el uso real mostró varias limitaciones. Por eso iPlay ofrece una extensión de navegador dedicada y un modelo de scripts adaptadores.
Primero, los userscripts no encajan bien con cierta lógica de parseo que necesita capacidades de sandbox. Algunos sitios ocultan la URL real de reproducción detrás de scripts restringidos, código dinámico o lógica ofuscada. Un adaptador a veces necesita ejecutar new Function, correr código aislado o llamar capacidades de la extensión para sortear restricciones de la página. Con un userscript normal es difícil hacerlo de forma estable.
Segundo, las herramientas de userscripts pueden tener una barrera de pago en macOS, mientras que la mayoría de funciones de iPlay se ofrecen gratis. Depender por completo de userscripts obligaría a algunos usuarios a comprar una herramienta de terceros solo para usar adaptadores web, algo que no encaja con el objetivo de iPlay de mantener el acceso sencillo.
Tercero, iPlay planea integrar la detección de recursos y capacidades relacionadas en la aplicación principal y en su ecosistema multiplataforma. El modelo de adaptadores de la extensión es más fácil de alinear con la app principal, iOS, Android y futuras plataformas. Un diseño basado solo en userscripts haría más difícil la migración multiplataforma y la gestión unificada de capacidades.
Estructura de archivos
iplay-adapter-example/
adapter.json Metadatos: id, nombre visible y reglas de coincidencia.
src/adapter.ts Entrada del adaptador: exporta createAdapters(api).
src/helper.ts Utilidades de ejemplo incluidas en el JS final.
src/iplay.ts Tipos mínimos de la API de adaptadores de iPlay.
vite.config.ts Construye un único archivo JS con metadatos.Inicio rápido
npm install
npm run typecheck
npm run buildEl resultado de la compilación es:
dist/example.jsCarga este archivo desde el popup de iPlay Direct con Browse JS, o alójalo y agrega su URL HTTP(S).
Flujo de desarrollo
- Edita
adapter.json.iddebe ser estable y único. Se usa para marcar botones, crear el nombre global del módulo y generar el nombre del archivo final.namees el nombre visible para los usuarios.matchescontrola en qué páginas se inyecta el adaptador, por ejemplo*://example.com/*.
- Edita
src/adapter.ts.createAdapters(api)crea y devuelve uno o varios adaptadores de sitio.canHandleLocation()comprueba si la pagina actual pertenece a este adaptador.scan()escanea la página y agrega botones de iPlay a tarjetas, enlaces o entradas de reproducción.getBestResult()resuelve la URL multimedia final usando el contexto recogido cuando el usuario hizo clic en el botón.
- Mantén las dependencias dentro del proyecto.
- Vite empaqueta los imports locales dentro del JS final.
- Las funciones pasadas a
api.sandbox.runFunction()se serializan y se ejecutan por separado, así que no pueden acceder directamente a variables de módulo, objetos DOM niapi.
Función de entrada
Todo script adaptador debe exportar createAdapters(api):
export function createAdapters(api: ExternalAdapterApi) {
return createExampleAdapter(api);
}Esta función inyecta en tu adaptador las capacidades que ofrece la extensión. No necesitas acceder a objetos internos de la extensión. Usa api para crear botones, enviar peticiones, leer ajustes y abrir medios.
Para soportar varios sitios desde un solo script, devuelve un arreglo:
export function createAdapters(api: ExternalAdapterApi) {
return [
createMovieSiteAdapter(api),
createTvSiteAdapter(api)
];
}Interfaz SiteAdapter
El tipo central de la plantilla es:
export type SiteAdapter<MediaRef = unknown> = {
id: string;
name: string;
refererUrl: string;
canHandleLocation(href?: string): boolean;
getBestResult(mediaRef: MediaRef): Promise<MediaResult>;
getTitle?(mediaRef: MediaRef): string | null;
scan(root?: ParentNode): void;
reset?(): void;
};id es el identificador programatico del adaptador. Mantenlo alineado con adapter.json para evitar conflictos entre adaptadores.
name es el nombre visible para el usuario. Puedes usarlo en registros, titulos de botones o mensajes de error.
refererUrl es la pagina de origen predeterminada. iPlay puede usarla como Referer de respaldo cuando la peticion multimedia lo requiere.
canHandleLocation(href = location.href) decide si este adaptador debe manejar la URL actual. Aunque adapter.json.matches ya filtra paginas, esta funcion sigue siendo util cuando un script se inyecta en varias paginas relacionadas.
scan(root = document) encuentra entradas multimedia y enlaza botones. Puede ejecutarse al cargar la pagina, durante actualizaciones parciales o despues de cambios de ruta en una SPA, asi que evita enlazar dos veces. La plantilla marca elementos procesados con element.dataset.iplayBound = "true".
getBestResult(mediaRef) realiza la resolucion real del medio. Despues del clic del usuario, api.openMedia(adapter, mediaRef) llama a esta funcion. Usala para consultar APIs del sitio, parsear JSON incrustado, elegir calidad o manejar flujos de audio y video separados, y devuelve un MediaResult.
getTitle(mediaRef) es opcional y devuelve un titulo de reproduccion a partir del contexto. Tambien puedes devolver title directamente en getBestResult().
reset() es opcional. Usalo para limpiar caches, quitar botones obsoletos o reiniciar estado despues de navegacion en una SPA.
Genéricos y contexto
SiteAdapter<MediaRef> usa MediaRef como tipo del contexto multimedia. Transporta los datos encontrados por scan() hacia getBestResult().
La plantilla define:
type ExampleMediaRef = {
apiUrl: string;
title?: string | null;
};
function createExampleAdapter(api: ExternalAdapterApi): SiteAdapter<ExampleMediaRef> {
// ...
}En scan(), el adaptador recoge una URL de API y un título desde el enlace:
api.openMedia(this, {
apiUrl,
title: element.textContent?.trim() || document.title
});Después, getBestResult(ref) puede usar con seguridad ref.apiUrl y ref.title:
async getBestResult(ref) {
const response = await api.fetch(ref.apiUrl, { credentials: "include" });
return normalizeMediaResult(await response.json(), this.getTitle?.(ref) || "Example");
}Diseña MediaRef como un objeto simple parecido a JSON, con strings, números, booleanos, arreglos y objetos simples. Evita nodos DOM, funciones, objetos Response, Map e instancias de clases. Así el adaptador es más fácil de depurar y queda preparado para mover trabajo al sandbox o a capacidades de fondo.
Capacidades clave de ExternalAdapterApi
api.createButton(size?, adapterId?, title?)Crea el boton flotante de iPlay. Usalo en scan() y agregalo a una tarjeta o enlace multimedia.
api.ensureRelativePosition(element)Garantiza que el elemento objetivo pueda contener un boton con posicion absoluta.
api.absoluteUrl(urlText, base?)Convierte URL relativas en URL absolutas. Usalo para valores de atributos data-*, href y src.
api.openMedia(adapter, mediaRef, buttonTitle?)Inicia la resolucion y reproduccion del medio. Llama a adapter.getBestResult(mediaRef).
api.fetch(input, init)
api.fetchText(url, init)
api.fetchJson<T>(url, init)
api.fetchRaw(url, init)Usa helpers de petición proporcionados por la extensión para acceder a páginas o APIs del sitio. Si la petición necesita la sesión del usuario, normalmente configura credentials: "include".
api.config.getAll()
api.config.get(key, fallback)
api.config.set(key, value)
api.getSettings()Lee o guarda ajustes de la extensión, como el núcleo del reproductor, idioma, idioma de audio de YouTube y preferencia de flujo. getSettings() es una forma cómoda de leer todos los ajustes.
api.sandbox.runFunction(fn, ...args)
api.sandbox.runCode(code, input)Ejecuta código en la página sandbox. Las funciones sandbox se serializan antes de ejecutarse, así que no pueden usar directamente variables externas, helpers importados, nodos DOM ni api. Pasa cada valor necesario como argumento.
api.requestCapability({ action, payload })Solicita capacidades de la extensión como fetchRaw, fetchText, fetchJson, getCookies, sha1Hex, openUrl y getUrl.
api.openUrl(url)Abre una URL, útil para flujos de inicio de sesión, autorización o depuración.
api.registerAdapter(adapter)Registra adaptadores manualmente. En la mayoria de proyectos basados en la plantilla basta con devolver adaptadores desde createAdapters().
api.log(site, ...args)
api.warn(site, ...args)Escribe registros de depuración con prefijo de sitio. Durante el desarrollo conviene registrar las decisiones importantes de parseo.
Forma de MediaResult
getBestResult() debe devolver:
{
videoUrl: "https://cdn.example.com/video.mp4",
audioUrl: null,
title: "Video title",
refererUrl: "https://example.com"
}Campos comunes:
videoUrl: obligatorio, flujo de video o URL principal de reproducción.audioUrl: opcional, URL de audio separado.title: opcional, título de la ventana del reproductor.refererUrl: opcional,Refererpara peticiones de video.subtitleUrl,subtitleRefererUrl,subtitleTitle: URL, origen y etiqueta de subtitulos.commentUrl,commentRefererUrl,commentTitle: URL, origen y etiqueta de comentarios o danmaku.prepared,picked: campos opcionales para datos de depuración o resultados elegidos aguas arriba.
Parámetros de URL de iPlay
iPlay Direct puede abrir URL multimedia con parametros adicionales:
iplay.window.title: título de la ventana del reproductor.iplay.window.width,iplay.window.height: tamaño preferido de la ventana.iplay.http.*: cabeceras de la petición multimedia. Por ejemplo,iplay.http.refererdefineReferer.iplay.kernel.type: sobrescribe el núcleo del reproductor. Valores comunes:mpv,vlc,exo.iplay.audio.url: URL del audio separado.iplay.comment.url: URL del flujo de comentarios o danmaku.iplay.subtitle.url: URL de subtitulos.iplay.video.url: URL principal de video.
iPlay Direct reconoce URL multimedia https:// y http://. Para agregar parámetros de iPlay sin cambiar el esquema original, los adaptadores también pueden usar:
iplays:// -> https://
iplay:// -> http://Ejemplo:
iplays://cdn.example.com/video.mp4?iplay.window.title=Example&iplay.http.referer=https%3A%2F%2Fexample.comiPlay Direct lo reproduce como https://cdn.example.com/video.mp4 y aplica el título y la cabecera Referer.
Ejemplo de Agent.md
Si quieres ayuda de IA para crear un adaptador para un sitio, coloca un archivo Agent.md en la raíz del proyecto del adaptador. Puedes empezar con esto:
# iPlay Adapter Agent Guide
You are helping build an iPlay Direct browser adapter script.
## Goal
Create or update a TypeScript adapter that detects playable media entries on the target website, adds iPlay buttons, and returns direct media URLs through the iPlay adapter API.
## Project Rules
- Use the existing template structure.
- Keep the adapter id stable and aligned between `adapter.json` and `SiteAdapter.id`.
- Implement `createAdapters(api)` as the public entry.
- Use `SiteAdapter<MediaRef>` with a typed plain-object `MediaRef`.
- Collect page context in `scan()` and pass it to `api.openMedia(adapter, mediaRef)`.
- Resolve final playback data in `getBestResult(mediaRef)`.
- Avoid duplicate buttons by marking processed elements with `data-iplay-bound="true"`.
- Use `api.absoluteUrl()` for relative URLs.
- Use `api.fetch()`, `api.fetchJson()`, or `api.requestCapability()` instead of raw global assumptions when extension capabilities are needed.
- Do not put DOM nodes, functions, Response objects, Map, or class instances inside `MediaRef`.
- Keep sandbox functions self-contained. Pass every value they need as an argument.
- Run `npm run typecheck` and `npm run build` before finishing.
## Target Site Notes
- Domain:
- Example media page URL:
- List/card selector:
- Title selector:
- API endpoint or embedded JSON source:
- Required cookies or headers:
- Stream selection rule:
- Subtitle/comment source:
## Expected Result
`getBestResult()` should return a `MediaResult` with at least `videoUrl`, and include `title`, `refererUrl`, `audioUrl`, `subtitleUrl`, or `commentUrl` when available.Consejos de depuración
- Empieza con
canHandleLocation()coincidiendo solo con la página que estás probando. - Registra la cantidad de elementos encontrados en
scan()para confirmar tus selectores. - Marca elementos procesados con
data-iplay-boundpara evitar botones duplicados en paginas SPA. - Devuelve primero una URL fija de prueba para verificar el flujo de
api.openMedia()y luego agrega el parseo real. - Si fallan las peticiones, revisa si el sitio requiere cookies,
Referer, cabeceras especiales o parseo en sandbox. - Si los scripts no se inyectan en Chrome o Edge, revisa primero que el permiso User scripts esté activado en la página de detalles de la extensión.