Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Latest commit

 

History

History
History
334 lines (255 loc) · 11.9 KB

File metadata and controls

334 lines (255 loc) · 11.9 KB
Copy raw file
Download raw file
Outline
Edit and raw actions

插件开发指南

本文面向想为 SPlayer-Next 编写插件的开发者。插件是一段运行在隔离沙箱里的 JavaScript,通过宿主注入的 splayer 全局对象对外提供能力。

最终用户如何安装使用见 plugins-usage.md

快速开始

一个最小插件就是一个 .js 文件。头部需要 JSDoc 声明元数据:

/**
 * @name Example
 * @version 1.0.0
 * @description 示例插件
 * @author you
 * @homepage https://example.com
 * @platform splayer
 * @apiLevel 1
 */

splayer.register({
  sources: {
    demo: {
      name: "Demo 源",
      actions: ["musicUrl", "lyric"],
      qualities: ["lq", "hq", "lossless"],
    },
  },
});

splayer.on("musicUrl", async (req) => {
  const { musicInfo, quality } = req;
  const resp = await splayer.request(
    `https://api.example.com/url?id=${musicInfo.songmid}&q=${quality}`,
    { responseType: "json" },
  );
  return { url: resp.body.url, quality, expire: resp.body.expire };
});

splayer.on("lyric", async (req) => {
  const { musicInfo } = req;
  const resp = await splayer.request(`https://api.example.com/lyric?id=${musicInfo.songmid}`, {
    responseType: "json",
  });
  return { lyric: resp.body.lrc, tlyric: resp.body.tlrc ?? null };
});

保存为 example.js,在 SPlayer 设置里「本地导入」即可。

脚本头部(JSDoc Manifest)

字段 必填 上限 说明
@name 24 字符 插件展示名
@version 36 字符 语义化版本号
@description 256 字符 简介
@author 56 字符 作者
@homepage 1024 字符 主页 URL
@platform splayerlx,不写默认 splayergz_ 压缩脚本默认为 lx
@apiLevel 声明兼容的 Host API 级别,当前宿主 = 1,超过会拒绝加载

缺少 @name@version → 安装失败。

插件 id 由宿主自动生成:slugify(name) + "-" + sha1(source).slice(0,8)。你不需要自己指定。

沙箱环境

插件运行在独立 utilityProcess 子进程 + vm.createContext 双层隔离中:

  • 没有 Node 内置模块(fs / net / child_process 等),没有 require / import
  • 没有 DOM / Electron API(window 仅在 lx 兼容模式下作为 { lx } 垫片)
  • 以下全局:splayerBufferURL / URLSearchParamsTextEncoder / TextDecoderPromisesetTimeout / setInterval / clearTimeout / clearInterval / setImmediate / clearImmediate / queueMicrotaskconsole(重定向到 splayer.log

硬性约束:

  • 脚本同步部分执行时间 ≤ 5 秒(V8 超时后中止并上报脚本错误)
  • 从 fork 到脚本 ready ≤ 10 秒,超时即判定加载失败
  • 每插件并发请求 ≤ 4,全局并发 ≤ 16
  • 心跳间隔 10 秒,连续 3 次未回 pong 视为卡死,自动杀掉重启
  • 崩溃后重启节奏 2s / 8s / 30s,累计 3 次失败进入 error 状态

splayer API

注入到沙箱全局的对象。以下是完整表面,基于 Host API level 1。

只读字段

splayer.pluginId: string       // 宿主分配的插件 ID
splayer.apiLevel: number        // 宿主 Host API level(= 1)
splayer.locale: string          // 当前界面语言(如 "zh-CN")
splayer.appVersion: string      // SPlayer 应用版本

注册能力

splayer.register({
  sources: {
    [sourceKey: string]: {
      name: string;                    // 展示名
      actions: ("musicUrl" | "lyric" | "pic")[];
      qualities?: ("lq" | "sq" | "hq" | "lossless" | "hi-res")[];
    };
  };
});

音质等级对齐宿主的 QualityLevel(见 src/utils/quality.ts):

含义
hi-res 高解析度无损(采样率 ≥ 96kHz + 位深 ≥ 24bit)
lossless 无损(flac / ape / wav 等)
hq 有损 ≥ 320kbps
sq 有损 ≥ 192kbps
lq 有损 < 192kbps

lx 脚本声明的 128k/192k/320k/flac/flac24bit/ape/wav 会被垫片自动映射到上面的等级;handler 收到的 info.type 也会反向映射为 lx 原生值,老脚本无需改动。

必须在脚本同步部分调用。注册完后宿主才知道这个插件能做什么、支持哪些源。

注册动作处理器

splayer.on("musicUrl", async (req) => res);
splayer.on("lyric", async (req) => res);
splayer.on("pic", async (req) => res);

每个 action 最多一个 handler,重复注册后者覆盖前者。目前仅这三个动作,搜索和元数据由宿主自身负责。

请求/响应形状

Action 请求 响应 默认超时
musicUrl { source, quality, musicInfo } { url, quality?, expire? } 20 s
lyric { source, musicInfo } { lyric, tlyric?, rlyric?, lxlyric? } 15 s
pic { source, musicInfo } { url } 15 s

musicInfo:宿主会传至少 { songmid },可能还带 name / singer 等用于辅助识别。

handler 抛出的异常会被宿主捕获,错误码透传到上层。超时未返回 → 被主进程 cancel。

网络请求

splayer.request(url: string, opts?: HostRequestOptions): Promise<HostRequestResult>

interface HostRequestOptions {
  method?: "GET" | "POST";
  headers?: Record<string, string>;
  body?: string | ArrayBuffer | Uint8Array;
  timeout?: number;                            // 毫秒,默认 15000,最大 60000
  responseType?: "text" | "json" | "arraybuffer"; // 默认 "text"
}

interface HostRequestResult {
  status: number;
  headers: Record<string, string>;
  body: unknown; // text → string, json → 已 parse 对象,arraybuffer → Uint8Array
}

仅允许 http://https://。请求由主进程的 net.fetch 发出,遵循系统代理设置。

日志

splayer.log.debug(...args);
splayer.log.info(...args);
splayer.log.warn(...args);
splayer.log.error(...args);

转发到宿主主日志系统,并落盘到 {userData}/plugins/logs/{id}.logconsole.* 也会自动转发到同样的通道。

私有 KV 存储

splayer.storage.get<T>(key: string): Promise<T | null>;
splayer.storage.set(key: string, value: unknown): Promise<void>;
splayer.storage.remove(key: string): Promise<void>;
splayer.storage.keys(): Promise<string[]>;

每个插件一个独立命名空间,落盘到 {userData}/plugins/data/{id}.json。卸载插件会自动清除。

用户设置

splayer.getSetting<T>(key: string): T | undefined;

同步读取用户在设置界面给此插件配置的值。设置 schema 通过 register() 扩展的能力将在后续 API level 加入;当前 level 1 下返回 undefined

工具(splayer.utils

常用 Node 原语的安全封装,不需要自己调用 Node 内置模块:

splayer.utils.crypto.md5(data)
splayer.utils.crypto.sha1(data)
splayer.utils.crypto.sha256(data)
splayer.utils.crypto.hmac(algo, key, data)
splayer.utils.crypto.randomBytes(size)
splayer.utils.crypto.aesEncrypt(data, key, mode, iv?)
splayer.utils.crypto.aesDecrypt(data, key, mode, iv?)
splayer.utils.crypto.rsaEncrypt(data, publicKey)

splayer.utils.buffer.from(data, encoding?)
splayer.utils.buffer.bufToString(buf, encoding?)
splayer.utils.buffer.concat(list)

splayer.utils.base64.encode(data)
splayer.utils.base64.decode(data)

splayer.utils.zlib.inflate(data) / deflate(data)
splayer.utils.zlib.gunzip(data) / gzip(data)

错误码

handler 抛异常时可通过 err.code 带上错误码;不带的话宿主默认 PLUGIN_HANDLER_ERROR

Code 含义
PLUGIN_NOT_FOUND 找不到指定插件
PLUGIN_DISABLED 插件已禁用
PLUGIN_NOT_READY 插件未就绪 / 沙箱未启动
PLUGIN_ACTION_UNSUPPORTED 插件没注册该动作
PLUGIN_LOAD_TIMEOUT 加载超 10 秒
PLUGIN_SCRIPT_ERROR 脚本语法或运行错误
PLUGIN_INVALID_MANIFEST 头部字段缺失或不合法
PLUGIN_API_LEVEL_MISMATCH 声明 apiLevel 高于宿主
PLUGIN_REQUEST_TIMEOUT 动作或 request 超时
PLUGIN_CANCELLED 被上层取消
PLUGIN_NETWORK_ERROR 网络错误
PLUGIN_URL_NOT_ALLOWED URL 协议不在白名单
PLUGIN_HANDLER_ERROR handler 默认错误码
PLUGIN_WORKER_CRASHED 子进程崩溃

完整示例结构

/**
 * @name My Plugin
 * @version 1.0.0
 * @description 一个多源聚合示例
 * @author you
 * @apiLevel 1
 */

const SOURCES = ["sa", "sb"]; // 你内部的源标识

splayer.register({
  sources: {
    sa: { name: "SA 音源", actions: ["musicUrl", "lyric"], qualities: ["lq", "hq"] },
    sb: { name: "SB 音源", actions: ["musicUrl", "pic"], qualities: ["lq", "hq", "lossless"] },
  },
});

const apis = {
  sa: {
    musicUrl: async ({ musicInfo, quality }) => {
      /* ... */
    },
    lyric: async ({ musicInfo }) => {
      /* ... */
    },
  },
  sb: {
    musicUrl: async ({ musicInfo, quality }) => {
      /* ... */
    },
    pic: async ({ musicInfo }) => {
      /* ... */
    },
  },
};

const dispatch = (action) => async (req) => {
  const fn = apis[req.source]?.[action];
  if (!fn)
    throw Object.assign(new Error("source not supported"), { code: "PLUGIN_ACTION_UNSUPPORTED" });
  return fn(req);
};

splayer.on("musicUrl", dispatch("musicUrl"));
splayer.on("lyric", dispatch("lyric"));
splayer.on("pic", dispatch("pic"));

调试

  1. 控制台里(DevTools)直接调:

    await window.api.plugins.list();
    await window.api.plugins.resolveUrl({
      pluginId: "my-plugin-xxxxxxxx",
      source: "sa",
      quality: "hq",
      musicInfo: { songmid: "123" },
    });
  2. 查看 {userData}/plugins/logs/{id}.log 拿插件的运行日志

  3. 修改脚本 → 重新导入一次(id 会因为源码 sha1 变化而变化,旧版本会自动被替换)

发布

脚本可以:

  • 直接作为 .js 分发(推荐)
  • gz_ 前缀 + zlib + base64 压缩成单行文本(兼容 lx 生态)

用户通过「本地导入」或「在线导入(粘贴 raw URL)」装上即可。

兼容 lx 插件

SPlayer 提供了 window.lx / globalThis.lx 垫片,覆盖 lx-music-desktop 的 user_api 常用 API:

  • lx.request(url, opts, callback) — 回调风格 HTTP 请求
  • lx.on("request", handler) — 注册统一 handler,宿主根据 action 分派
  • lx.send("inited", { sources }) — 异步上报能力
  • lx.utils.crypto / lx.utils.buffer / lx.utils.zlib / lx.utils.base64 — 与 splayer.utils 等价

绝大多数 lx 公开脚本无需修改即可运行。头部写 @platform lx 或脚本整体以 gz_ 压缩会自动启用垫片。

注意:若你在写新插件,请直接用 splayer.* API,lx 垫片仅用于跑存量 lx 脚本。

Morty Proxy This is a proxified and sanitized view of the page, visit original site.