爬蟲介接指南

從 Java、Python、QuickJS 範例開始,了解載入、共用接口與回傳格式。

載入爬蟲

Site 最小配置

JSON / Sitesites[] 物件片段
{
  "key": "demo",
  "name": "Demo",
  "type": 3,
  "api": "./demo.js",
  "ext": {}
}
Java JARapi: "csp_Demo"

類別放在 com.github.catvod.spider.Demo,並繼承抽象 Spider;主配置必須提供 spider 或站點的 jar。使用 catvod-api.jar 作為 compileOnly 編譯依賴,由 TV 根任務 :apiJar 產生於 build/outputs/api/catvod-api.jar。

Pythonapi: "./demo.py"

相對路徑以配置檔所在位置為基準;輸出 Spider 類別並繼承 base.spider.Spider。

QuickJSapi: "./demo.js"

JavaScript 由 QuickJS 執行;相對路徑以配置檔所在位置為基準。 使用 ES module,預設匯出物件或建立物件的函式。

安裝執行模組

在 App 的模組頁安裝 Python、QuickJS 或 Node.js 的獨立 APK。 Thunder、mpv 與 BD-J 也在此管理,按需要安裝。

安裝與更新

選擇模組、下載並完成系統安裝。 版本需與 App 的模組接口及裝置 ABI 相容。

測試與重啟

安裝後執行模組測試,確認能初始化。 若提示重新啟動,重開 App 後再載入爬蟲。

QuickJS 使用 QuickJS-NG 引擎,不能直接執行 Node.js 套件; Node.js 請依 獨立載入方式 設定。 使用新接口時,請一併更新 App 與對應模組。

測試爬蟲

連線 App 的網頁,從「設定 → 爬蟲測試」開啟 /spider.html。 Python 與 QuickJS 需先安裝對應模組。

  1. 載入 Java JAR 並選擇類別,或載入 .py/.js;ext 為選填。
  2. 選擇方法,以 JSON 陣列填入參數,執行並查看結果與網路紀錄。
  3. 修改後儲存、重新載入並重測;可格式化來源或還原版本。
  4. play 回傳有效結果後,送到 App 確認播放。

App 不編譯 Java,修改 .java 後需重新產生並載入 JAR。 測試逾時預設 30 秒,可設為 1–300 秒;重新載入、取消或離開會清理測試工作。 來源若已被其他頁面修改,先重新讀取再儲存。

先確認回傳,再確認播放模組測試與正確的 URL 不代表播放成功,仍需在 App 確認畫面、聲音、字幕與彈幕。

三語言 Demo

範例流程為「分類 → 列表 → 詳情 → 播放」。請將 example.com 換成自己的來源, 並補上搜尋與錯誤處理。

回傳格式Java 用 Android 內建的 JSONObject/JSONArray 組裝 JSON 字串;Python 回傳 dict/list,QuickJS 回傳物件/陣列,由 App 序列化。 直播可回傳 Group 陣列或 TXT/M3U 原始文字。
com/github/catvod/spider/Demo.java
// 最小點播範例;example.com 為示意網址。
package com.github.catvod.spider;

import com.github.catvod.crawler.Spider;

import org.json.JSONArray;
import org.json.JSONObject;

import java.util.HashMap;
import java.util.List;

public class Demo extends Spider {

    // 回傳分類清單。
    @Override
    public String homeContent(boolean filter) throws Exception {
        JSONObject item = new JSONObject().put("type_id", "movie").put("type_name", "電影");
        return new JSONObject().put("class", new JSONArray().put(item)).toString();
    }

    // 範例只有一頁;第二頁起回傳空清單。
    @Override
    public String categoryContent(String tid, String pg, boolean filter,
            HashMap<String, String> extend) throws Exception {
        int page = Math.max(1, Integer.parseInt(pg));
        JSONArray items = new JSONArray();
        if (page == 1) items.put(card("demo-1", "範例項目"));
        return new JSONObject().put("list", items).put("page", page)
                .put("pagecount", 1).put("limit", 20).put("total", 1).toString();
    }

    // 集數先使用穩定 ID,播放時再取得網址。
    @Override
    public String detailContent(List<String> ids) throws Exception {
        JSONObject item = card(ids.get(0), "範例項目").put("vod_play_from", "Demo")
                .put("vod_play_url", "第 1 集$demo://episode/1");
        return new JSONObject().put("list", new JSONArray().put(item)).toString();
    }

    // 將播放 ID 換成實際網址。
    @Override
    public String playerContent(String flag, String id, List<String> vipFlags) throws Exception {
        String url = id.replace("demo://", "https://example.com/");
        return new JSONObject().put("parse", 0).put("url", url).toString();
    }

    private JSONObject card(String id, String name) throws Exception {
        return new JSONObject().put("vod_id", id).put("vod_name", name)
                .put("vod_pic", "https://example.com/poster.jpg");
    }
}

完整範例見 CatVodSpider 的 Java、Python 與 QuickJS, 涵蓋篩選、搜尋分頁、直播、快取與網頁操作。

方法與呼叫時機

本表適用 Java、Python 與 QuickJS;Node.js 使用 獨立 HTTP 端點。

在 destroy 釋放自建 Session、WebSocket 與背景工作。 QuickJS 最多等待 5 秒(含 Promise),逾時仍會繼續清理引擎。

方法何時呼叫責任主要回傳
init初始化爬蟲時讀取 ext、建立連線或快取無
homeContent / home進入首頁分類與選填 filtersResult.class
homeVideoContent / homeVod首頁分類完成首頁推薦卡片Result.list
categoryContent / category分類、換頁、變更篩選該頁卡片與總頁數list + pagecount
detailContent / detail點擊卡片完整資訊與播放分組list[0]
searchContent / search搜尋搜尋結果,可支援頁碼list + pagecount
playerContent / play選擇集數最終 URL 與播放參數Result
danmaku選用爬蟲即時彈幕來源彈幕或訂閱狀態物件Java AutoCloseable/Python、QuickJS 停止函式
liveContent / live載入直播配置TXT、M3U 或 Group JSON文字 / Group 陣列
proxy / localProxy本地代理收到請求依執行環境提供狀態、類型、內容與標頭陣列 / HTTP 回應
action自訂操作可由 Result 解析的結果Java JSON 字串/Python、QuickJS 物件
destroy重新載入或清除釋放執行緒與連線;QuickJS 清理 hook 最長 5 秒無

播放詳情更新

playerContent/play 可在播放結果的 list[0] 帶回 VOD 詳情。 App 採用結果時更新頁面;預載結果在切集時套用,取消或過期的結果會丟棄。

JSON / Result播放結果附帶詳情
{
  "parse": 0,
  "url": "https://example.com/video.mp4",
  "list": [
    {
      "vod_id": "demo-1",
      "vod_name": "範例項目",
      "vod_pic": "https://example.com/cover.jpg",
      "type_name": "動畫",
      "vod_year": "2026",
      "vod_area": "台灣",
      "vod_director": "示範導演",
      "vod_actor": "演員甲、演員乙",
      "vod_content": "完整簡介",
      "vod_remarks": "更新至第 2 集",
      "vod_play_from": "主要線",
      "vod_play_url": "第1集$episode-1#第2集$episode-2"
    }
  ]
}

Java 使用 Response.vod(vod),Python/QuickJS 使用 list: [vod]; Node.js 的 /play 與 type=4 API 使用相同的 JSON 格式。 範例見 CatVodSpider Demo。

同名播放分組合併 episode,相同名稱與播放值去重,其餘追加,保留目前選集位置。vod_id 可省略,提供時會連動歷史與收藏識別。

共用網路

App 在初始化前提供網路接口:Java 使用 net、Python 使用 self.net、QuickJS 使用 net。 時間單位為毫秒。完整參數與範例見 CatVodSpider 開發說明。

方法用途
req / http取得 code、headers、content 與 error;HTTP 非 2xx 需自行判斷。body 支援文字或位元組陣列。
json取得 JSON;連線失敗、HTTP 非 2xx 或無效 JSON 會拋出例外。
get / postJava 取得文字正文;HTTP 非 2xx 仍保留正文。
getUserAgentPython/QuickJS 取得 App 的 User-Agent,按需放入 headers。
getCookie / setCookie讀寫 WebView Cookie;Session 使用隔離 Cookie。
session建立隔離 Cookie 的請求流程,用完關閉。
batch批次 HTTP 請求,預設最多 4 筆並行,按輸入順序回傳。
map三語言逐項執行回呼,預設最多 4 項並行,保留輸入順序。
download直接下載至檔案,由共用下載器管理暫存、續傳與分段。
openStreamPython/QuickJS 分塊讀取,用完關閉 reader。
requestStreamAsyncJava 傳入 NetOptions,Future 回傳 Net.StreamResult。
streamPython/QuickJS 原樣轉送網址,或由產生器提供 proxy 正文。
ws一次 WebSocket 訊息交換,取得首個完整訊息後關閉。
connect持續 WebSocket 連線,可設定斷線恢復與接收逾時。
cached / peek / refreshTTL 快取、讀取已有值與背景更新。
cachedTextPython 取得快取的 JSON 原文。
clearCache清除一筆;省略 key 清除目前站點全部 TTL 快取並取消載入。
sleep可取消的等待,單位為毫秒。

HTTP

只取 JSON 用 net.json(url, options);連線失敗、HTTP 非 2xx 或無效 JSON 會拋出例外。 需要狀態碼、錯誤正文或 bytes 時用 net.req,QuickJS 用 await net.http。 Java 的 options 除特別標示外使用 JSON 字串,json/req 也回傳 JSON 字串。

QuickJS 的 req 同步執行;http/json 以 await 取得結果。Python/QuickJS 使用物件傳 options, 二進位回應用 buffer: 3,分別取得 bytes/ArrayBuffer。net.getUserAgent() 取得 App 的 UA,需要時自行放入 headers。

原始 POST 用 body,與 data 擇一並提供 Content-Type。 body 支援文字或 0~255 整數陣列,例如 body: [0, 127, 128, 255];buffer 只控制回應格式。

Cookie 與 Session

cookie: true 使用並更新 WebView Cookie;以 getCookie(url) 讀取、setCookie(url, cookie) 寫入單筆 Set-Cookie。net.session() 自動管理隔離 Cookie,不共用站點的 Cookie 或 TTL 快取,也不支援 persist。

Session 用完須關閉:Java 用 try-with-resources、Python 用 with、QuickJS 用 finally。 關閉會取消其請求、串流與連線,之後不可繼續使用。

批次與下載

net.batch(requests, limit) 每筆傳入 {url, options},預設最多 4 筆並行,按輸入順序回傳。 每項還需快取、簽名或解析時用 net.map(items, mapper, limit),回呼收 item/index,各項自行檢查 HTTP 狀態。 拋錯後停止派送,已開始的工作繼續完成;Java/Python 在背景執行緒回呼,QuickJS 可用 async。

Java 下載用 net.download(url, optionsJson, file),Python/QuickJS 用 net.download(url, path, options)。threads 為 0 自動、1 單連線、2–4 並行上限;size 指定完整 bytes,maxBytes 限制寫入大小。 續傳與分段依來源的 Range 與驗證資訊決定,cookie:true/Session 不續傳;完成後仍須確認檔案內容。

串流讀取與代理

Java 用 net.requestStreamAsync(url, NetOptions),Future 回傳繼承 InputStream 的 Net.StreamResult。 Python 用 self.net.openStream(url, options),QuickJS 用 await net.openStream(url, options)。 用完以 try-with-resources、with 或 finally 關閉。

原樣轉送

將下列片段放入對應的 proxy 方法。成功交付後由 App 關閉 stream,不交付時自行 close。 QuickJS handle 只供當次 proxy 使用一次,不存入 Local/快取;Java 需引入 Net 與 NetOptions。

Demo.java / proxy
NetOptions options = NetOptions.from("{\"timeout\":15000,\"callTimeout\":15000}");
Net.StreamResult response = net.requestStreamAsync("https://example.com/data.bin", options).get();
return new Object[]{response.code, response.contentType, response, response.headers};

自訂串流正文

Python 用 self.net.stream(generator()),yield bytes/bytearray/連續 memoryview; QuickJS 用 await net.stream(asyncGenerator()),yield ArrayBuffer/TypedArray。 將回應的 stream 放入 proxy 第三格;Java 直接回傳自訂 InputStream。

產生器在播放器讀取時才執行,不接受 HTTP options。reader 在產生器內開啟,以 with/finally 關閉; App 關閉 body 時取消其等待中的串流請求與讀取,再關閉產生器。

直播可用 callTimeout: 0 關閉整次 HTTP 期限,保留 timeout 防止讀取卡住;Demo 使用 15 秒期限。 讀取大小、EOF 與取消限制見 開發說明。

WebSocket

一次交換用 net.ws(url, options),取得首個完整訊息後關閉。 持續收訊用 net.connect(url, options, callback),回呼接收 (socket, event), 事件為 open/message/retry/error/close。

content 為文字,二進位則為 Java byte[]、Python bytes 或 QuickJS Uint8Array。 使用回呼傳入的 socket.send() 發送、socket.close() 關閉,需立即中斷時用 cancel。 站點或 Session 釋放時也會取消連線。

timeout 只限制建立連線;ping 是協定 Ping 的毫秒間隔。 idle 是未收到文字/二進位訊息的逾時毫秒數,預設 0 關閉;送出訊息與 Ping/Pong 不會重設期限。 平台訂閱與應用層心跳由爬蟲處理,只在來源保證持續回訊時設定 idle。

reconnect: true 啟用斷線恢復,預設關閉,等待從 1 秒倍增至 30 秒。 每次 open 重新訂閱,retry 時停止心跳並清除殘包。 主動關閉、釋放或認證/憑證/協定/回呼錯誤後不再重連。

快取與儲存

TTL 快取

net.cached(key, options, loader) 按站點快取 JSON,同 key 合併載入。 ttl 必填;stale 預設 true,過期先回舊值並背景更新;persist 預設 false,啟用時持久化公開資料。 播放網址使用短 TTL、stale: false,不持久化。

peek(key) 只讀已有值,包括過期值;refresh(key, options, loader) 立即返回,依 TTL 背景更新。clearCache(key) 清一筆,省略 key 清除目前站點全部 TTL 快取並取消載入。

Python 的 self.net.cachedText(key, options, loader) 取得 JSON 原文;cached 則解碼為 Python 值,兩者共用 TTL 與清除規則。 Java 的 cached 回傳 JSON 字串。

Local

小型設定用 Java 的 local、Python 的 self.local 或 QuickJS 的 local。 以 get(key)/set(key, value)/delete(key) 讀寫,App 按站點隔離;Java 使用 JSON 字串,Python/QuickJS 使用 JSON 值。 清 TTL 快取不會刪除 Local。

執行 JavaScript

用 js.eval(source, options) 在獨立環境執行短段 JavaScript。 Java 的 options 與回傳值使用 JSON 字串;Python 用 self.js.eval,QuickJS 用 await js.eval。

globals 傳 JSON 變數,timeout 為正整數毫秒,預設 5000;內建 md5X。 回傳最後一個運算式的 JSON 值,不接受 Promise。環境不提供網路、DOM 或模組匯入,完成、逾時或取消後即關閉。 三種語言都需要 QuickJS 模組。

Android UI

共用 SDK 的 com.github.catvod.Runtime 以 Runtime.context()取得 Application Context、Runtime.activity() 取得目前 Activity,Runtime.toast() 顯示文字提示。Java 需明確匯入這個類別;Python 使用 jclass 取得。這些接口由宿主提供,爬蟲 JAR 使用compileOnly 依賴 SDK。

繼承 Spider 的 Java/Python 爬蟲也可使用 getActivity()/self.getActivity()。Activity 可能為 null/None;自訂原生 UI 在主執行緒顯示時才取得 Activity,不保存頁面引用。Runtime.toast() 由 App 切到主執行緒顯示。 QuickJS 的文字提示可從 action 回傳 msg 欄位。

三語言共用 Dialog 參數與宿主介面,可顯示文字、QR Code、圖片與自訂表單。 Java 用 dialog.show(options),Python 用 self.dialog(options) 同步取得結果; QuickJS 用 await dialog(options),onAction 放在 options 內。

關窗時回傳 {button, values}:button 是按鈕 id,values 以欄位 name 對應字串,多選欄位使用字串陣列。 允許的返回或點外側操作、預設取消按鈕及頁面銷毀時回傳 null/None。 宿主、參數或回呼錯誤時,Java/Python 拋出例外,QuickJS Promise reject。 呼叫取消或爬蟲釋放會關窗並停止等待。

Java/Python 在爬蟲工作執行緒呼叫,回呼也在原工作執行緒執行;宿主負責顯示 UI。 Java 的 options/event/結果使用 JSONObject,Python 使用 dict,QuickJS 使用物件。 Java/Python 回呼同步回傳更新,QuickJS 回呼可回傳 Promise;皆可省略回呼。

Java 需更新編譯用 SDK 與 App,Python/QuickJS 需使用配套 App 與模組。

Spider.action
JSONObject options = new JSONObject("""
    {"title":"輸入內容","inputs":[{"name":"text","label":"內容","required":true}]}
    """);
JSONObject result = dialog.show(options);
if (result == null) return "{}";
String text = result.getJSONObject("values").getString("text");
欄位類型用途預設
title / messagestring選填標題與說明。—
imagestring選填 base64 圖片 data URI,如 data:image/png;base64,...;由爬蟲準備內容。onAction 可更新,空字串移除;解碼失敗時,尚未結束的 dialog 會回報錯誤。—
qrcodestring選填非空白字串,在本機產生 QR Code;內容不必是網址,無法編碼時回報錯誤。—
copyableboolean可選取 message,並提供複製全文按鈕;複製不關窗,也不呼叫 onAction。false
cancelableboolean允許返回鍵取消;false 時也阻止點外側取消。頁面銷毀及呼叫取消仍會清理視窗。true
inputsobject[]選填輸入欄位;name 必填且不可重複,label 預設 name;value 為字串,預設空字串;多選為字串陣列,預設空陣列。—
inputs[].helperTextstring欄位下方的說明文字,省略或空字串時隱藏。—
inputs[].typestringtext、phone、number、password 為單行,textarea 為多行,select 為單選,multiselect 為多選清單;格式與業務驗證由爬蟲負責。text
inputs[].optionsobject[]select/multiselect 必填非空陣列;value 為非空白且唯一的字串,label 預設 value 且不可空白。顯示 label,單選回傳字串,多選依選項順序回傳字串陣列;非空初始值須存在於選項,多選不可重複。—
inputs[].enabledboolean允許操作欄位;停用時略過內建驗證,values 仍包含其值。onAction 可回傳 inputs 陣列,依 name 更新 enabled。true
inputs[].requiredboolean不可為空或全空白,多選至少選一項。false
inputs[].minLength / maxLengthinteger字串欄位的非負整數限制,以 Unicode 字元計算;maxLength 不可小於 minLength。留空時略過 minLength。0 / 不限制
inputs[].minItems / maxItemsinteger多選欄位的非負整數限制,以已選數量計算;maxItems 不可小於 minItems。留空時略過 minItems。0 / 不限制
buttonsobject[]選填 1~3 個按鈕,依序對應 positive、negative、neutral;id 與 label 必填且不可為空,id 不可重複。省略時提供確定(ok)與取消。—
buttons[].closebooleanfalse 時保留視窗,必須提供 onAction。true
buttons[].enabledboolean允許操作按鈕;onAction 可回傳 buttons 陣列,依 id 更新 enabled。true
buttons[].validatestring[]驗證指定的欄位 name;空陣列略過驗證。全部欄位
onActionfunction驗證通過後接收 {button, values},button 為按鈕 id。Java/Python 以第二參數傳入同步回呼,QuickJS 放在 options 內,可回傳 Promise。回傳 {message, qrcode, image, values, errors, inputs, buttons, close} 更新內容或關窗設定。—

三語言 Demo 的 dialog 示範欄位更新與驗證,qrcode 示範 QR Code 更新與複製,dialog_steps 示範圖片更新與連續對話框。 以下以 QuickJS 展示共用參數;範例不記錄或保存輸入內容。

JSON
let version = 1;
const initial = "https://example.com/?v=" + version;
await dialog({
  title: "QR Code 範例",
  message: initial,
  qrcode: initial,
  copyable: true,
  buttons: [{id: "refresh", label: "更新", close: false}, {id: "close", label: "關閉"}],
  async onAction({button}) {
    if (button !== "refresh") return;
    await net.sleep(250);
    const content = "https://example.com/?v=" + (++version);
    return {qrcode: content, message: content};
  }
});

QR Code 在本機產生,內容不必是網址。message、QR Code、圖片與輸入欄共用捲動區域。 copyable 為 true 且 message 非空時提供複製全文按鈕;複製不關窗,也不觸發 onAction。 onAction 更新 message 後複製的是最新全文,保留換行與前導零。 要複製 QR Code 原始內容,可將相同字串填入 message;省略 qrcode 即可顯示純文字。

JSON
const result = await dialog({
  title: "表單範例",
  cancelable: true,
  inputs: [
    {name: "text", label: "內容", helperText: "可輸入多行文字,換行會保留", type: "textarea", enabled: false, required: true, maxLength: 80},
    {name: "choice", label: "選項", type: "select", required: true,
      options: [{value: "first", label: "選項一"}, {value: "second", label: "選項二"}]},
    {name: "choices", label: "多選", type: "multiselect", required: true, maxItems: 2,
      options: [{value: "first", label: "選項一"}, {value: "second", label: "選項二"}, {value: "third", label: "選項三"}]}
  ],
  buttons: [
    {id: "fill", label: "帶入範例", close: false, validate: []},
    {id: "confirm", label: "確認", enabled: false},
    {id: "error", label: "錯誤提示", close: false, validate: []}
  ],
  async onAction({button}) {
    await net.sleep(250);
    if (button === "fill") return {
      values: {text: "範例內容", choice: "first", choices: ["first", "second"]},
      inputs: [{name: "text", enabled: true}],
      buttons: [{id: "confirm", enabled: true}]
    };
    if (button === "error") return {errors: {text: "這是欄位錯誤提示示範"}};
    return {close: true};
  }
});
if (result === null) return {};
const text = result.values.text;

宿主只負責顯示表單與回傳;按鈕用途、站點請求及業務驗證由爬蟲決定。 onAction 可回傳 message/qrcode/image 更新顯示內容、values 更新欄位值、errors 顯示欄位錯誤,close 覆蓋按鈕的關窗設定。 有非空欄位錯誤時保留視窗與輸入值;回傳 null/None 沿用按鈕設定,QuickJS 也可回傳 undefined。

回呼期間停用欄位與按鈕並顯示處理中,結束後恢復各自的 enabled 設定。 可修正的欄位錯誤用 errors 顯示;回呼拋出例外時會關窗並回報錯誤。

cancelable 預設 true,允許返回或點外側取消;false 時兩種操作都不取消。 預設取消按鈕、頁面銷毀及呼叫取消仍會清理視窗。

textarea 保留換行;select 顯示 label,回傳所選 value,長度驗證也依 value 計算。 multiselect 開啟原生勾選清單,確認後才套用,取消保留原值;回傳依 options 順序排列的字串陣列。

上下鍵移動焦點,多行輸入在首尾行才移出欄位;確認鍵開啟單選/多選清單,返回後回到原欄位。

values 只更新指定欄位,空字串可清空文字或取消單選,空陣列可清空多選。 整批 values 先核對欄位、型別與選項再套用,未知欄位、型別錯誤、不存在或重複的選項會關窗並回報錯誤。 更新不觸發 onAction,必填與長度檢查仍在按鈕操作時執行;帶入或重設按鈕可設 validate:[]。 更新欄位會清除其舊錯誤,同次回傳的 errors 隨後套用;關窗結果包含更新後的 values。

回傳 inputs 陣列以 name、buttons 陣列以 id 指定既有控制項,只更新 enabled;省略保持原值。 未知控制項、重複更新或無效布林值會關窗並回報錯誤;布林參數不接受字串或數字。 停用欄位略過內建驗證,values 仍包含其值。

回傳 qrcode 可更新或新增 QR Code;省略 qrcode 保留原 QR Code,提供時須為可編碼的非空白字串,否則關窗並回報錯誤。 更新 QR Code 保留輸入值;按鈕設定 close:false 或回傳 close:false 可繼續操作。

等待輸入暫停目前呼叫及 App 工作的期限,onAction 執行及 QuickJS 等待 Promise 時恢復計時;網路期限照常生效。

JSON
const first = await dialog({
  title: "第一步",
  image: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAAAwCAIAAABhdOiYAAAAbklEQVR4nO3QwQkAIAwEQUuzdDvTDgQJRtE59hsIU1qtmlSOf3B5gAABAvQlUF8ZIECAAAHKFUm7BQQIECBAgAABAgToKaDIAAECBAjQZqC0W0CAAAECBAgQIECAAAECBAgQIAECBAjQLQECFGsAofuPsjrQFugAAAAASUVORK5CYII=",
  inputs: [{name: "text", label: "內容", helperText: "確認後進入下一步", required: true}]
});
if (first === null) return {};
const second = await dialog({
  title: "第二步",
  message: first.values.text,
  copyable: true
});
if (second === null) return {};

連續對話框依序呼叫即可,QuickJS 使用 await;前一個視窗完成關閉後才回傳。 每一步都檢查 null/None,可在取消時停止後續步驟;步驟與資料傳遞由爬蟲控制。

image 接收爬蟲提供的 base64 圖片 data URI,使用 Android 支援的點陣圖片;圖片取得由爬蟲負責。 onAction 可回傳 image 更新圖片,省略時保留原圖,空字串移除;解碼失敗時,尚未結束的 dialog 會關窗並回報錯誤。 關窗會取消並釋放該圖片請求。

helperText 顯示欄位說明,錯誤清除後會恢復顯示。

手機驗證可把 phone 與 code 放在同一表單,發碼按鈕使用 close: false 與 validate: ["phone"]。 登入成功後關窗,確認 dialog 回傳後才保存憑據;取消時不套用登入結果。驗證碼保持字串,保留前導零。

JAR 自己的 com.github.catvod.spider.Init.init(Context)是可選的初始化入口,與 Runtime 分開;每個爬蟲仍由 App 呼叫 Spider.init(Context, ext)。

Result 回傳格式

探索類

JSON / Resulthome · category
{
  "class": [
    { "type_id": "movie", "type_name": "電影" }
  ],
  "filters": {
    "movie": [
      {
        "key": "area",
        "name": "地區",
        "init": "",
        "value": [
          { "n": "全部", "v": "" },
          { "n": "台灣", "v": "tw" }
        ]
      }
    ]
  },
  "list": [
    {
      "vod_id": "demo-1",
      "vod_name": "範例項目",
      "vod_pic": "https://example.com/poster.jpg",
      "vod_remarks": "示意"
    }
  ],
  "pagecount": 1,
  "msg": "選填提示"
}

homeContent 使用 class / filters;列表、搜尋、詳情使用 list。filters 第一層鍵必須等於分類的 type_id 值,本例是 movie。

播放類

JSON / ResultplayerContent
{
  "url": "https://example.com/video/demo-1.m3u8",
  "parse": 0,
  "header": {
    "User-Agent": "ExampleClient/1.0",
    "Referer": "https://example.com/"
  },
  "format": "application/x-mpegURL",
  "subs": [
    {
      "url": "https://example.com/subs/demo-1.vtt",
      "name": "繁體中文",
      "lang": "zh-TW",
      "format": "text/vtt",
      "flag": 0
    }
  ],
  "danmaku": [
    { "url": "https://example.com/danmaku/demo-1.xml", "name": "示意彈幕" }
  ],
  "artwork": "https://example.com/poster.jpg",
  "desc": "示意播放資訊",
  "position": 120000
}

url 是核心欄位;parse=0 表示此欄位未要求解析,但 jx=1 或配置旗標比對仍可能觸發解析。

完整 Result 欄位

欄位類型用途預設
web{url, mode?, cookie?: {url?, keys: string[]}[]}action 回應的開網頁指令,只執行一次;詳見開網頁章節。—
classClass[]首頁分類清單。[]
listVod[]首頁、分類與搜尋卡片;詳情取 list[0]。播放結果可用 list[0] 更新該片詳情,預載時延至採用結果才套用。[]
filtersobject以分類 ID 為鍵,值為 Filter 陣列或單一 Filter。{}
urlstring | string[] | Url播放 URL 或多畫質清單,三種寫法見下方範例。—
skipsSkipSegment[]每集片頭、片中、片尾的跳過區間,start / end 都用毫秒;格式錯誤的項目會忽略。[]
timeshiftobject直播回看範圍與取源網址 {url, start, window},格式見直播回看章節。—
headerobject | string播放請求標頭;也接受 JSON 物件字串。整份為空時回退 Site.header,不逐鍵合併。{}
msgstring顯示提示文字;只有 code 為 0 時顯示。空字串
codeinteger訊息控制值,不是 HTTP 狀態碼;非 0 時隱藏 msg。0
danmakuDanmaku[] | string彈幕清單、單一 URL 或 JSON 陣列字串,可使用檔案或爬蟲即時來源;受 App 彈幕載入設定控制。[]
subsSub[]外掛字幕清單。[]
playUrlstring解析前綴;json: 後接 JSON 解析端點,parse: 後接具名解析器。空字串
artworkstring更新播放頁顯示的圖片。空字串
jxFromstring擴展 JSON 解析結果回報的解析器名稱,用於成功提示;不是播放分組旗標。空字串
flagstring播放分組與解析比對旗標;未填時 App 補入呼叫 playerContent 時的 flag。由 App 補入
descstring清理後更新播放頁描述。空字串
formatstring媒體 MIME type 提示,請填完整類型,例如 application/x-mpegURL。—
clickstringWebView 載入後執行的 JavaScript;Site.click 非空時優先。空字串
keystringApp 內部回填的 Site key;爬蟲不必提供,提供也會被覆寫。由 App 補入
positioninteger播放起點,單位毫秒;120000 為 2 分鐘。未填時不覆蓋既有播放進度。—
pagecountinteger總頁數;0 或未填表示未知,仍可繼續載入更多。0
parseinteger1 要求解析;0 仍可能因 jx=1 或配置旗標比對進入解析。0
jxinteger1 要求進入解析,即使 parse=0。0
drmDrm播放 DRM 設定,與 Channel.drm 使用相同物件,見配置字典。—

url 的多畫質寫法

單一 URL 見上方播放範例。陣列必須以「名稱、URL」成對排列,不是單純的 URL 清單;也可改用 values 物件。頂層 position 是毫秒,Url.position 則是從 0 起算的畫質索引,實際選取仍會由 App 調整。

名稱與 URL 成對陣列

JSON / ResultplayerContent
{
  "parse": 0,
  "url": [
    "主畫質", "https://example.com/video/main.m3u8",
    "低畫質", "https://example.com/video/low.m3u8"
  ]
}

Url 物件

JSON / ResultplayerContent
{
  "parse": 0,
  "url": {
    "values": [
      { "n": "主畫質", "v": "https://example.com/video/main.m3u8" },
      { "n": "低畫質", "v": "https://example.com/video/low.m3u8" }
    ],
    "position": 0
  }
}

drm 的子欄位請參考共用物件字典;詳情的播放分組見 集數格式。

資料物件

演員或分類超連結使用 Java 的 Json.link(name, target)、 Python 的 self.link(name, target) 或 QuickJS 的 link(name, target)。 target 包含目標分類及篩選,可用 id、name、type_flag、filter。

Vod

vod_id
傳給 detailContent 的唯一值。
vod_name
顯示名稱。
vod_pic / vod_remarks
縮圖與角標。
type_name
分類文字。
vod_year / vod_area
年份與地區。
vod_director / vod_actor
人員資訊。
vod_content
詳細描述。
vod_play_from / vod_play_url
播放分組與集數,分隔方式見下節。
vod_tag / action
folder 表示資料夾;非空 action 在點擊時觸發操作。type 3 傳給爬蟲 action();type 4 填回傳 JSON 的接口網址。
cate
提供此物件時也視為資料夾,可含 land、circle、ratio。
land / circle / ratio / style
卡片外觀覆蓋。

Class

type_id / id
傳給 categoryContent 的分類 ID。
type_name / name
分類顯示名稱。
type_flag
1 代表資料夾分類。
land / circle / ratio
該分類預設卡片樣式。
filters
也可在分類物件內提供 Filter 陣列。

Filter

key / name
傳入鍵與顯示名稱。
init
預設選項值。
value
選項陣列,例如 [{"n":"全部","v":""}];n 是顯示名稱,v 是傳入值。

Sub

url
字幕檔位置。
name / lang
顯示名稱與語言代碼。
format
字幕 MIME type,可省略並由 App 推定。
flag
0 交由 App 依字幕數量與偏好語言選擇;非零使用 Media3 選擇旗標:1 預設、2 強制、4 自動選擇,可位元組合。

Danmaku

url
HTTP(S) 資料網址,或由 getDanmakuUrl(id) 產生的即時來源。
name
選填顯示名稱。

SkipSegment

type
opening 為片頭,middle 為片中,ending 為片尾。
start
開始跳過的時間(毫秒)。
end
要跳到的時間(毫秒);須晚於 start,僅片尾且 start > 0 時可省略。

播放分組與集數

$$$分隔播放分組
#分隔同組集數
$分隔集數名稱與 id
JSON / Vod fieldslist[] 中的 Vod 欄位片段
{
  "vod_play_from": "主線路$$$備用線路",
  "vod_play_url": "第 01 集$demo://ep/1#第 02 集$demo://ep/2$$$第 01 集$backup://ep/1"
}

使用者點擊某集後,$ 右側的 value 會成為 playerContent(flag, id, …) 的 id。

Proxy 回傳

JSON / Proxy 陣列proxy / localProxy
[
  200,
  "text/plain; charset=utf-8",
  "demo response",
  { "Cache-Control": "no-cache" }
]

上例是 Python/QuickJS 一般陣列模式:狀態碼、Content-Type、內容、選填標頭;第 5 格 1 表示 Base64 內容。Python 方法名稱是 localProxy,第三格也可直接回傳 bytes。

Java 的一般 Object[] 回傳格式中,第三格必須是 InputStream,第四格標頭選填,不能直接放字串或 bytes;也可只回傳一格 NanoHTTPD.Response。QuickJS 的 from=catvod 另有 JSON response 模式;Node Proxy 則直接轉接 HTTP 回應。

使用 getProxyUrl(params) 產生網址,App 自動加入 do、siteKey 並編碼參數。JAR 模式沒有 siteKey 時會找 com.github.catvod.spider.Proxy.proxy(Map) 靜態入口。

即時彈幕

Java/Python/QuickJS 的播放回應沿用 danmaku: [{name, url}]。 即時來源的 url 用 getDanmakuUrl(id) 產生,Python 使用 self.getDanmakuUrl(id);App 會呼叫該站點的 danmaku(id, emit)。 HTTP(S) 來源仍按彈幕檔案載入。

JSON / 即時彈幕emit(item)
{
  "text": "即時彈幕",
  "color": 6737151,
  "size": 25,
  "type": 1,
  "colors": [6737151, 13408767]
}
欄位類型用途預設
text必要string解碼後的文字;不得全空白,最多 4096 字元。—
colorintegerRGB 顏色,範圍 0~16777215。16777215(白色)
sizenumber字級,單位 sp,範圍 0~96;0 跟隨使用者設定。0
typeinteger1 滾動、4 底部、5 頂部、6 反向。1
colorsinteger[]2~8 個 RGB 顏色組成漸層;省略時使用 color。—

狀態也使用 emit,不可同時包含 text。 state 為 ready(訂閱可用)、retrying(恢復中)或 error(終止失敗); msg 選填,最多 512 字元,不放 Cookie、token 或簽名網址。 ready 須確認平台訂閱可用,不能僅以 WebSocket open 判斷。

JSON / 訂閱狀態emit(item)
{
  "state": "retrying",
  "msg": "連線中斷,等待恢復"
}

App 自行設定 connecting、合併相同 state/msg;初始化例外轉成 error。 error 後停止訂閱並忽略後續回呼,主動停止則清除狀態,不需回報 closed。 狀態記錄在日誌,不會當成彈幕或反覆跳出提示。

emit 接收 Java JSONObject、Python dict 或 QuickJS 物件;Java 用 emit.accept(item) 並回傳 AutoCloseable,Python/QuickJS 回傳停止函式。 切台或關閉時,App 呼叫停止;爬蟲須取消自己的連線與心跳。

爬蟲負責協定、解碼與樣式轉換,App 負責顯示限制,超量或過期訊息可能丟棄。 完整範例見 CatVodSpider 的三語言 Demo。

直播回看

來源支援依時間取源時,在播放回應或直播列表的 Channel 加入 timeshift。 App 會顯示可拖曳的時間軸,最右側為直播。

JSON / ResultplayerContent
{
  "parse": 0,
  "url": "https://example.com/live.flv",
  "timeshift": {
    "url": "https://example.com/replay?time={time}",
    "start": 1792700000000,
    "window": 7200000
  }
}

start 是本場直播開始的 Unix 毫秒,window 是最多可回看的毫秒數。 時間軸從本場開始累加,最長不超過 window;可用範圍由來源決定。

App 將網址的 {time} 換成目標 Unix 毫秒後 GET;0 表示重新取得直播來源。 端點回傳直接播放的 url,並可附上 header 與 format。 使用本地代理時,以 getProxyUrl 產生網址。

連續拖曳或切台會取消舊請求;失敗時提示並保留目前播放。 回看期間暫停爬蟲即時彈幕,回到直播後恢復。 從節目表選取回看則使用 catchup,時間軸以該節目為範圍。

片段跳過

在每集的 playerContent 回應頂層加入 skips:

JSON / ResultplayerContent
{
  "parse": 0,
  "url": "https://example.com/video.m3u8",
  "skips": [
    {"type": "opening", "start": 0, "end": 90000},
    {"type": "middle", "start": 600000, "end": 630000},
    {"type": "ending", "start": 1200000}
  ]
}

opening 為片頭、middle 為片中、ending 為片尾。 時間單位是毫秒;start 是開始跳過的時間,end 是要跳到的時間。 上例片中會從 10:00 跳到 10:30。

片頭與片中必填 end,且須晚於 start;片中可提供多段。 片尾可在 start 大於 0 時省略 end,表示換下一集,沒有下一集則跳到片尾。

使用者手動設定的片頭或片尾會優先套用;片中跳過不受影響。

開網頁

卡片的 action 觸發後,在回應頂層提供 web。

一般網頁

JSON / Resultaction
{
  "web": {
    "url": "https://example.com/"
  }
}

獨立內容視窗

JSON / Resultaction
{
  "web": {
    "url": "https://example.com/game",
    "mode": "view"
  }
}

url 必填,只接受 HTTP 或 HTTPS。mode 省略或填 browser 為一般網頁;view 為獨立內容視窗, 不啟動外部 App,也不會自動全螢幕。兩者返回都先退出全螢幕, 再依網頁歷史回上一頁;沒有歷史才關閉。

登入後自動關閉

JSON / Resultaction
{
  "web": {
    "url": "https://example.com/login",
    "cookie": [
      {"keys": ["sid", "token"]},
      {"url": "https://login.example.com/", "keys": ["auth"]}
    ]
  }
}

cookie 為選填陣列,每組 url 省略時使用開啟網址。同組 keys 必須全部非空, 不同組任一組完成即可;至少一個指定 Cookie 比開啟前改變,且目前頁面與該組的 scheme、host、port 相同,才自動關閉。此條件不代表站點已驗證帳號授權。

Node.js 載入

一般 ./demo.js 仍由 QuickJS 執行。Node.js 必須以 index.js.md5 作為配置入口,同目錄提供 index.js、index.config.js 與 index.config.js.md5。

App 啟動 bundle 後,讀取 /config 回應的 video 或 data.video,把 video.sites 映射為 node: 站點路由。爬蟲以 HTTP JSON 處理接在各站點路由後的 /init、/home、/category、/detail、/search 與 /play。

/play 的詳情回傳格式見 播放詳情更新。

不要只替換 api 前綴Node.js 不是 QuickJS 的 export default 物件模式;單獨把普通配置的 api 改成 node:,不會完成 Node bundle 初始化。