Skip to content

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 a .js file in the extension directory whose name matches your package name (e.g. my.extension.js).

// V2: declare these as top-level functions; no class is needed
var 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 go through the global Miru object:

// request: appends url to the extension's @webSite (relative), and auto JSON.parse's the response
const json = await Miru.request("/api/list?page=1");
// rawRequest: uses the full URL, no @webSite prefixing
const html = await Miru.rawRequest("https://example.com/page");
// persist state across calls (the goja VM is destroyed after every execution); values are stored as strings
Miru.saveCache("token", "abc123");
const token = Miru.getCache("token");
// Miru also exposes the extension's own metadata
Miru.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
});

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 parsing
const 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).

// ==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: {} });

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.