JavaScript Extensions V1 (legacy)
A V1 extension is an object that extends the host-injected Extension class. At load time the host rewrites class X extends Extension into globalThis.Ext = class extends Extension {...} and instantiates it. All request helpers live on this.
Entry Methods
Section titled “Entry Methods”class Demo extends Extension { // Recently updated (page starts at 1) async latest(page) { /* ... */ }
// Search (V1 also receives filter; may be ignored) async search(keyword, page, filter) { /* ... */ }
// Detail async detail(url) { /* ... */ }
// Watch: returns the final playback shape directly (no mirror step) async watch(url) { /* ... */ }
// Optional: popular / check update / load hook async popular(page) { /* ... */ } async checkUpdate(url) { /* ... */ } async load() { /* runs once when the extension loads */ }}Requests and DOM
Section titled “Requests and DOM”class Demo extends Extension { async request(url, options) { // relative to @webSite; auto JSON.parse's the response const json = await this.request("/api/list?page=1"); // absolute URL request const html = await this.rawRequest("https://example.com/page"); // parse HTML with linkedom (V1 also has require built in) const { parseHTML } = require("linkedom"); const { document } = parseHTML(html); const title = document.querySelector(".title")?.textContent; return { json, title }; }}this also exposes convenience methods querySelector(content, selector) / querySelectorAll(content, selector) / getAttributeText(content, selector, attr) (all backed by linkedom).
Complete Example (V1)
Section titled “Complete Example (V1)”// ==MiruExtension==// @name Demo Bangumi (V1)// @package demo.bangumi.v1// @author Demo// @license MIT// @lang zh-cn// @icon https://example.com/icon.png// @webSite https://example.com/// @type bangumi// ==/MiruExtension== (note: V1 omits @apiVersion, or sets a non-2 value)
class Demo extends Extension { async latest(page) { const json = await this.request(`/api/latest?page=${page}`); return json.list.map((it) => ({ title: it.title, url: it.id, cover: it.cover, update: it.update, })); }
async search(keyword, page) { const json = await this.request(`/api/search?kw=${keyword}&page=${page}`); return json.list.map((it) => ({ title: it.title, url: it.id, cover: it.cover, })); }
async detail(url) { const json = await this.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), })), }; }
// V1: watch returns the final playback shape directly (here an hls link) async watch(url) { const json = await this.request(`/api/stream/${url}`); return { type: "hls", // hls | mp4 | torrent | magnet url: json.playUrl, headers: { Referer: "https://example.com/" }, }; }}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.