Skip to content

JS 插件示例

警告

纯纯看番插件格式与技术可能在不远的将来发生改变,这篇教程仅供参考

如果想直接看代码示例可直接跳转到 社区 JS 源仓库。

环境

  • 开发语言: JavaScript
  • 开发软件:我们推荐使用 VisualStudio Code

TIP

纯纯看番使用 Rhino 为 js 加载引擎。不支持许多 js 新特性,例如 let、function 类型、async 函数等
但好处是可以直接操作 java 对象。

新建一个 bangumi.js 文件

输入元数据

元数据位于脚本文件开头,以注释的形式引入,具体格式:

JavaScript
// @key heyanle.bangumi
// @label Bangumi
// @versionName 1.0
// @versionCode 1
// @libVersion 12
// @cover https://bgm.clbug.com/img/favicon.ico
名称含义
key番源唯一标识,一般为 作者.番源
label番源名称,支持中文
versionName版本名称,支持中文,只做展示用
versionCode版本号,数值类型,用于判断是否有新版本
libVersion库版本,与纯纯看番版本引擎对应,用于引导用户升级纯纯看番或升级番源
cover展示图标,一般为 url

实体

CartoonSummary

确定一部番剧的最小单位,一般作为获取番剧数据的参数

Kotlin
data class CartoonSummary(
    var id: String,              // 标识,由源自己支持,用于区分番剧
    var source: String,
)

CartoonCover

番剧封面,一般在首页和搜索中展示

Kotlin
interface CartoonCover : Serializable {
    var id: String              // 标识,由源自己支持,用于区分番剧
    var source: String          // 番剧 key,对应元数据中的 key
    var url: String             // 番剧网页
    var title: String           // 番剧名称
    var coverUrl: String?       // 番剧封面 url,在封面版中展示
    var intro: String?          // 番剧副标题,在文字版中作为副标题展示
}

在 JavaScript 插件中,可调用 makeCartoonCover 函数快速构建:

JavaScript
// 纯纯看番会自动获取 source 参数,不用额外传入
var cover = makeCartoonCover({
    id: id,
    title: title,
    url: url,
    intro: intro,
    cover: cover,
});

Cartoon

Kotlin
interface Cartoon : Serializable {

    var id: String              // 标识,由源自己支持,用于区分番剧
    var source: String
    var url: String
    var title: String
    var genre: String?          // 标签,为 "xx, xx"
    var coverUrl: String?
    var intro: String?
    var description: String?
    var updateStrategy: Int     
    var isUpdate: Boolean       // 是否更新,在追番页显示角标
    var status: Int             // 暂时没有效果

    companion object {
        const val STATUS_UNKNOWN = 0               // 未知
        const val STATUS_ONGOING = 1               // 连载中
        const val STATUS_COMPLETED = 2             // 已完结

        /**
         * 无论严格还是不严格都会更新
         */
        const val UPDATE_STRATEGY_ALWAYS = 0

        /**
         * 只有严格更新时才会更新,一般用于已完结
         */
        const val UPDATE_STRATEGY_ONLY_STRICT = 1

        /**
         * 不更新,一般用于剧场版或年代久远不可能更新的番剧
         */
        const val UPDATE_STRATEGY_NEVER = 2
    }

    fun getGenres(): List<String>? {
        if (genre.isNullOrBlank()) return null
        return genre?.split(",")?.map { it.trim() }?.filterNot { it.isBlank() }?.distinct()
    }

}

在 JavaScript 插件中,可调用 makeCartoon 函数快速构建:

JavaScript
// 纯纯看番会自动获取 source 参数,不用额外传入
var cartoon = makeCartoon({
    id: id,
    url: url,
    title: title,
    cover: cover,
    intro: "",
    description: desc,
    genre: ["xx", "xx"],
    updateStrategy: Cartoon.UPDATE_STRATEGY_ALWAYS,     // 可选 默认 ALWAYS
});

PlayLine

Kotlin
class PlayLine(
    val id: String,     // 源自己维护和判断
    val label: String,  // 播放源封面
    val episode: ArrayList<Episode>, 
)

Episode

Kotlin
open class Episode(
    val id: String, // 源自己维护和判断
    val label: String,
    val order: Int, // 第几集,用来排序,都一致就按照 PlayLine 中顺序
)

PlayerInfo

Kotlin
class PlayerInfo(
    val decodeType: Int = DECODE_TYPE_OTHER,
    val uri: String = "",

) {

    // 播放请求 header
    var header: Map<String, String>? = null
    
    companion object {
        // 这里跟 exoplayer 对应的类型需要对应

        const val DECODE_TYPE_DASH = 0
        const val DECODE_TYPE_HLS = 2
        const val DECODE_TYPE_OTHER = 4
    }
}

首页

纯纯看番的首页为二级结构,分为一级 Tab(MainTab)和二级 Tab(SubTab),同时支持封面版和文字版首页:

TIP

一级 Tab 不支持异步获取,需要快速返回。而二级 Tab 支持异步操作

TIP

以下代码只是作为演示,实际上在函数中需要使用纯纯看番提供的各种工具从网络获取数据。可参考社区 JS 仓库插件代码

JavaScript
/**
 * 获取一级 Tab,暂不支持异步操作,需要快速返回
 */
function PageComponent_getMainTabs() {
    var res = new ArrayList();
    res.add(new MainTab("带二级 Tab", MainTab.MAIN_TAB_GROUP, "ext 对象,会透传给后续函数对象"));
    res.add(new MainTab("封面版", MainTab.MAIN_TAB_WITH_COVER));
    res.add(new MainTab("文字版", MainTab.MAIN_TAB_WITHOUT_COVER));
    return res;
}

/**
 * 获取二级 Tab,支持异步操作,可以发起网络请求
 * @param mainTab 当前的一级 Tab,只会传入 MainTab.MAIN_TAB_GROUP 的一级 Tab
 */
function PageComponent_getSubTabs(mainTab) {
    var res = new ArrayList();
    if (mainTab.label == "带二级 Tab") {
        // SubTab 第二个参数代表是否为封面版
        res.add(new SubTab("封面版", true, "ext 对象,会透传给后续函数对象"));
        res.add(new SubTab("文字版", false, "ext 对象,会透传给后续函数对象"));
    }
    return res;
}


/**
 * 获取首页番剧,分页加载,支持异步操作,可以发起网络请求
 * @param mainTab
 * @param subTab
 * @param key 页码,从 0 开始
 * @return Pair<Int?, List<CartoonCover>> 下一页页码(为空代表当前最后一页)
 */
function PageComponent_getContent(mainTab, subTab, key) {
    // 透传对象,为 getMainTab 函数返回对象第三个参赛,可能为空
    var mainExt = mainTab.ext;
    var subExt = subTab.ext;
    var cartoonList = new ArrayList();
    var nextKey = null;
    if (mainTab.label == "带二级 Tab") {
        if (subTab.label == "封面版") {
            cartoonList.add(makeCartoonCover({
                id: "番剧id",
                title: "番剧名称",
                url: "番剧网址",
                intro: "番剧简介",
                cover: "封面 url",
            }))
             // ...
        } else if (subTab.label == "文字版") {
            cartoonList.add(makeCartoonCover({
                id: "番剧id",
                title: "番剧名称",
                url: "番剧网址",
                intro: "番剧简介",
                cover: "封面 url",
            }))
        }
         // ...
    }
    return new Pair(nextKey, cartoonList);
}

TIP

特别的,如果需要隐藏一级 Tab 行,可在 getMainTabs 函数直接返回 NonLabelMainTab
如果 Type 为 MainTab.MAIN_TAB_GROUP 则会只有二级 Tab,适用于需要异步 Tab 的形态
次数因为没有 一级 Tab 区分,因此 getSubTabs 的 mainTab 参数将没有意义

JavaScript
function PageComponent_getMainTabs() {
    return new NonLabelMainTab(MainTab.MAIN_TAB_GROUP, "ext 透传")
}

function PageComponent_getSubTabs(mainTab) {
    var res = new ArrayList();
    res.add(new SubTab("封面版", true, "ext 对象,会透传给后续函数对象"));
    res.add(new SubTab("文字版", false, "ext 对象,会透传给后续函数对象"));
    return res;
}

搜索

JavaScript

/**
 * 根据关键字搜索番剧
 * @param page 页码,从 1 开始,后续为返回值的第一个分量
 * @param keyword 搜索关键字
 * @return Pair<Int?, List<CartoonCover>> 首个分量为空代表当前是最后一页
 */
function SearchComponent_search(page, keyword) {
    var res = new ArrayList();
    // ... 获取 ArrayList<CartoonCover>
    if (res.size() == 0) {
        return new Pair(null, new ArrayList());
    }
    return new Pair(page + 1, res);
}

详情和播放列表

JavaScript

/**
 * 获取播放线路和详细信息
 * @param summary 番剧摘要 CartoonSummary,id 为首页或搜索中返回的 CartoonCover 的数据
 * @return Pair<Cartoon, List<PlayLine>>
 */
function DetailedComponent_getDetailed(summary) {
    // 伪代码
    var cartoon = getDetailed(summary);
    var playLineList = getPlaylineList(summary);
    return new Pair(cartoon, playLineList);
}

获取播放地址

JavaScript
/**
 * 获取播放地址
 * @param summary 番剧摘要 CartoonSummary,id 为首页或搜索中返回的 CartoonCover 的数据
 * @param playLine 播放列表  getDetailed 返回
 * @param episode 当前集  getDetailed 返回
 * @return PlayerInfo 支持 m3u8 以及 普通 mp4,支持添加自定义请求 Header
 */
function PlayComponent_getPlayInfo(summary, playLine, episode) {
    return new PlayerInfo(
        type, res
    )
}

番源配置

纯纯看番支持番源自定义自己的配置页,支持输入配置,选择配置和开关配置

JavaScript
function PreferenceComponent_getPreference() {
    var res = new ArrayList();
    // label key default
    var host = new SourcePreference.Edit("网页", "Host", "https://www.xxx.tv");
    var playerUrl = new SourcePreference.Edit("播放器网页正则", "PlayerReg", "https://xxx?.*");
    var timeout = new SourcePreference.Edit("超时时间", "Timeout", "20000");
    // label key default
    // 会存入 "true" "false"
    var isCookie = new SourcePreference.Switch("是否保存 cookie", true);
    var proxyLine = new ArrayList();
    proxyLine.add("国内线路");
    proxyLine.add("日本线路")
    // label key default selection
    var proxy = new SourcePreference.Selection("选择线路", "urlLine", "国内线路", proxyLine);
    res.add(host);
    res.add(playerUrl);
    res.add(timeout);
    res.add(isCookie);
    res.add(proxy);
    return res;
}

可通过 PreferenceHelper 工具获取配置:

JavaScript
var preferenceHelper = Inject_PreferenceHelper;

// key default
// 只支持 String
var host = preferenceHelper.get("host", "https://www.xxx.tv");
var isCookie = preferenceHelper.get("cookie", "true") == "true";

Apache-2.0 Licensed.