Get Started (V2)
Miru Alpha’s JavaScript extensions run on the pure-Go goja engine; extension files use the .js suffix. This page covers the V2 (recommended) JS workflow; see Getting Started for the shared metadata and Data Formats for the return structures.
Create the Extension File
Section titled “Create the Extension File”Create a .js file in the extension directory whose name matches your package name (e.g. my.extension.js).
Entry Points
Section titled “Entry Points”// V2: declare these as top-level functions; no class is neededvar latest = (page) => { /* latest updates */ };var search = (kw, page, filter) => { /* search (filter is an optional filter string) */ };var detail = (url) => { /* detail */ };var watch = (url) => { /* returns a list of mirrors */ };var mirror = (url) => { /* resolves the final playback URL */ };var createFilter = (filter) => { /* returns filter descriptions (optional, not implemented by default) */ };var checkUpdate = () => { /* check for updates (optional) */ };async function load() { /* runs once when the extension loads */ }Requests and Caching
Section titled “Requests and Caching”Requests go through the global Miru object:
// request: appends url to the extension's @webSite (relative), and auto JSON.parse's the responseconst json = await Miru.request("/api/list?page=1");// rawRequest: uses the full URL, no @webSite prefixingconst html = await Miru.rawRequest("https://example.com/page");
// persist state across calls (the goja VM is destroyed after every execution); values are stored as stringsMiru.saveCache("token", "abc123");const token = Miru.getCache("token");
// Miru also exposes the extension's own metadataMiru.pkg; Miru.name; Miru.website;In V2, watch() returns a list of mirrors (not the final URL). After the user picks a mirror, the frontend calls mirror(url) to resolve the final playback link:
var watch = (url) => { const groups = { "Server 1": [ { name: "Mirror 1", url: "https://m1.example.com/a.m3u8", headers: {} }, { name: "Mirror 2", url: "https://m2.example.com/a.m3u8", headers: {} }, ], }; return { groups: Object.keys(groups).map((t) => ({ title: t, mirrors: groups[t] })) };};
var mirror = (url) => ({ type: "hls", // hls | mp4 | torrent | magnet (content type, NOT extension type) url, headers: { "User-Agent": "Mozilla/5.0" }, // tlsConfig: { profile: "chrome_133" }, // Go extensions only: backend auto-proxies with TLS fingerprint});Node Modules (require)
Section titled “Node Modules (require)”The goja runtime has a nodejs-style require built in, so extensions can load the host’s prebundled modules without shipping their own dependencies:
const { parseHTML } = require("linkedom"); // lightweight DOM parsingconst CryptoJS = require("crypto-js"); // CryptoJS.MD5 / AES / ...const md5 = require("md5"); // blueimp JavaScript-MD5: md5("str")const JSEncrypt = require("jsencrypt"); // RSA: new JSEncrypt()const zlib = require("zlib"); // compression (see table below)Available modules:
| Module | Purpose / exports |
|---|---|
linkedom |
parseHTML(html) returns a DOM (document / querySelector, etc.) for scraping pages. |
crypto-js |
CryptoJS (MD5, SHA, AES, DES, Base64, Hmac — same API as the browser crypto-js). |
md5 |
blueimp’s md5(string) function. |
jsencrypt |
JSEncrypt class (new JSEncrypt() + setPublicKey + encrypt). |
zlib |
Synchronous compress/decompress: gzipSync / gunzipSync / deflateSync / inflateSync / brotliCompressSync / brotliDecompressSync / zstdCompressSync / zstdDecompressSync, returning Uint8Array; bytesFromBase64(str) decodes base64 into a binary-safe Uint8Array. |
url |
nodejs url module (new URL(), parsing, etc.). |
console |
Debug output (console.log / warn / error), forwarded to Miru’s developer log. |
There is also a global crypto object (crypto.getRandomValues, max 65536 bytes per call).
Complete Example
Section titled “Complete Example”// ==MiruExtension==// @name Demo Bangumi// @package demo.bangumi// @author Demo// @license MIT// @lang zh-cn// @icon https://example.com/icon.png// @webSite https://example.com/// @type bangumi// @apiVersion 2// ==/MiruExtension==
var latest = async (page) => { const json = await Miru.request(`/api/latest?page=${page}`); return json.list.map((it) => ({ title: it.title, url: it.id, cover: it.cover, update: it.update }));};
var search = async (kw, page) => { const json = await Miru.request(`/api/search?kw=${kw}&page=${page}`); return json.list.map((it) => ({ title: it.title, url: it.id, cover: it.cover }));};
var detail = async (url) => { const json = await Miru.request(`/api/detail/${url}`); return { title: json.title, cover: json.cover, desc: json.desc, chapters: json.episodes.map((ep) => ({ title: ep.name, urls: ep.sources.map((s) => s.url) })), };};
var watch = (url) => ({ groups: [{ title: "Default", mirrors: [{ name: "Source", url, headers: {} }] }],});
var mirror = (url) => ({ type: "mp4", url, headers: {} });Submitting Extensions
Section titled “Submitting Extensions”Submit your extension to the Miru Extension Repository via a PR. The PR must contain the extension source file and does not need the index.json file.