package bex:plugin@1.0.0; interface common { record request-context { request-id: string, locale: option, region: option, safe-mode: bool, hints: list, } record attr { key: string, value: string } enum image-layout { portrait, landscape, square, banner, circular, unknown } record image { url: string, layout: image-layout, width: option, height: option, blurhash: option, } record image-set { low: option, medium: option, high: option, backdrop: option, logo: option, } record linked-id { source: string, id: string } enum media-kind { movie, series, anime, short, special, documentary, music, podcast, book, live, unknown } enum status { unknown, upcoming, ongoing, completed, cancelled, paused } enum card-layout { carousel, grid, vertical-list, banner, compact } record page-cursor { token: option, limit: option } record media-card { id: string, title: string, kind: option, images: option, original-title: option, tagline: option, year: option, score: option, genres: list, status: option, content-rating: option, url: option, ids: list, extra: list, } record category-link { id: string, title: string, subtitle: option, image: option } record home-section { id: string, title: string, subtitle: option, items: list, next-page: option, layout: card-layout, show-rank: bool, categories: list, extra: list, } record paged-result { items: list, categories: list, next-page: option, } record search-filters { kind: option, page: page-cursor, fast-match: bool } record episode { id: string, title: string, number: option, season: option, images: option, description: option, released: option, score: option, url: option, tags: list, extra: list, } record season { id: string, title: string, number: option, year: option, episodes: list } record person { id: string, name: string, image: option, role: option, url: option } record media-info { id: string, title: string, kind: media-kind, images: option, original-title: option, description: option, score: option, scored-by: option, year: option, release-date: option, genres: list, tags: list, status: option, content-rating: option, seasons: list, cast: list, crew: list, runtime-minutes: option, trailer-url: option, ids: list, studio: option, country: option, language: option, url: option, extra: list, } record video-resolution { width: u32, height: u32, hdr: bool, label: string } enum stream-format { hls, dash, progressive, unknown } record video-track { resolution: video-resolution, url: string, mime-type: option, bitrate: option, codecs: option, } record subtitle-track { label: string, url: string, language: option, format: option } record server { id: string, label: string, url: string, priority: u8, extra: list } record stream-source { id: string, label: string, format: stream-format, manifest-url: option, videos: list, subtitles: list, headers: list, extra: list, } record subtitle-query { title: option, year: option, season: option, episode: option, language: option, fps: option, file-hash: option, file-size: option, identifiers: list, } record subtitle-entry { id: string, title: string, language: string, format: string, url: option, release: option, fps: option, downloads: option, score: option, hearing-impaired: bool, machine-translated: bool, file-hash: option, extra: list, } record subtitle-file { format: string, content: list } record article { id: string, title: string, summary: option, url: string, published: option, author: option, thumbnail: option, tags: list, extra: list, } record article-section { id: string, title: string, items: list
, next-page: option } variant plugin-error { network(string), parse(string), not-found, unauthorized, forbidden, rate-limited(option), timeout, cancelled, unsupported, invalid-input(string), internal(string), } } interface http { use common.{attr, plugin-error}; enum method { get, post, put, delete, head, patch, options } enum cache-mode { normal, no-store, only-cache, force-refresh } record request { method: method, url: string, headers: list, body: option>, timeout-ms: option, follow-redirects: bool, cache-mode: cache-mode, max-bytes: option, } record response { status: u16, headers: list, body: list, cached: bool, final-url: string, } /// Send an HTTP request and return the response. /// This is a simple function-based API (no resource types) for simplicity. send-request: func(req: request) -> result; } interface kv { use common.{attr}; set: func(key: string, value: list, ttl-seconds: option) -> bool; get: func(key: string) -> option>; remove: func(key: string) -> bool; keys: func(prefix: string) -> list; } interface secrets { get: func(key: string) -> option; } interface log { use common.{attr}; enum level { trace, debug, info, warn, error } write: func(level: level, msg: string, fields: list); } interface clock { now-ms: func() -> u64; monotonic: func() -> u64; } interface rng { bytes: func(len: u32) -> list; } interface js { use common.{plugin-error}; /// Options for JS evaluation with fine-grained control. record js-opts { /// Name for the script (appears in JS error stack traces). filename: option, /// Timeout in milliseconds. 0 = use pool default (10s). timeout-ms: u32, } /// Evaluate JavaScript. `input` is injected as the global variable `input`. /// The last expression value is returned as a JSON string. /// Use `JSON.parse(input)` in JS if structured data is needed. eval-js: func(code: string, input: string) -> result; /// Evaluate JavaScript with options (timeout, filename for error traces). eval-js-opts: func( code: string, input: string, opts: js-opts, ) -> result; /// Register + call a named JS function. Faster than eval-js for repeated calls. /// /// `fn-name` : Function name as defined in `fn-source`. /// `fn-source` : JavaScript defining exactly one function named `fn-name`. /// Parsed and registered on first call; reused on subsequent calls. /// Auto-re-registers if `fn-source` content changes. /// `args-json` : Passed to the function as a single string argument. /// In JS: `function myFn(args) { const d = JSON.parse(args); ... }` call-js-fn: func( fn-name: string, fn-source: string, args-json: string, ) -> result; /// Remove a named JS function from this plugin's context. /// Use when a cipher function is rotated (player update). /// Returns 0 on success. clear-js-fn: func(fn-name: string) -> result; } world plugin { import http; import kv; import secrets; import log; import clock; import rng; import js; export api: interface { use common.{ request-context, page-cursor, paged-result, search-filters, home-section, media-info, server, stream-source, subtitle-query, subtitle-entry, subtitle-file, article-section, article, plugin-error }; get-home: func(ctx: request-context) -> result, plugin-error>; get-category: func(ctx: request-context, id: string, page: page-cursor) -> result; search: func(ctx: request-context, query: string, filters: search-filters) -> result; get-info: func(ctx: request-context, id: string) -> result; get-servers: func(ctx: request-context, id: string) -> result, plugin-error>; resolve-stream: func(ctx: request-context, server: server) -> result; search-subtitles: func(ctx: request-context, query: subtitle-query) -> result, plugin-error>; download-subtitle: func(ctx: request-context, id: string) -> result; get-articles: func(ctx: request-context) -> result, plugin-error>; search-articles: func(ctx: request-context, query: string) -> result, plugin-error>; } }