api: "csp_Demo"類別放在 com.github.catvod.spider.Demo,並繼承抽象 Spider;主配置必須提供 spider 或站點的 jar。
從 Java、Python、JavaScript 範例開始,了解載入方式、方法呼叫與回傳資料。三種語言目前涵蓋 JAR、QuickJS、Chaquopy 與 Node.js 四種執行環境。
sites[] 物件片段{
"key": "demo",
"name": "Demo",
"type": 3,
"api": "./demo.js",
"ext": {}
}api: "csp_Demo"類別放在 com.github.catvod.spider.Demo,並繼承抽象 Spider;主配置必須提供 spider 或站點的 jar。
api: "./demo.py"相對路徑以配置檔所在位置為基準;輸出 Spider 類別並繼承 base.spider.Spider。
api: "./demo.js"相對路徑以配置檔所在位置為基準;ES module 預設匯出物件或建立物件的函式。
三份範例示範「分類 → 列表 → 詳情 → 播放」資料流程。example.com 是結構示意,並不是可用來源。正式實作時須接上自己的資料取得、錯誤處理與搜尋。
package com.github.catvod.spider;
import android.content.Context;
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 {
private String ext;
@Override
public void init(Context context, String extend) {
this.ext = extend;
}
@Override
public String homeContent(boolean filter) throws Exception {
JSONArray classes = new JSONArray()
.put(new JSONObject().put("type_id", "movie")
.put("type_name", "電影"));
return new JSONObject().put("class", classes).toString();
}
@Override
public String categoryContent(String tid, String pg, boolean filter,
HashMap<String, String> extend) throws Exception {
JSONArray list = new JSONArray().put(card("demo-1", "範例項目"));
return new JSONObject().put("list", list)
.put("pagecount", 1).toString();
}
@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();
}
@Override
public String playerContent(String flag, String id,
List<String> vipFlags) throws Exception {
return new JSONObject().put("parse", 0)
.put("url", resolve(id)).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");
}
private String resolve(String id) {
return id.replace("demo://", "https://example.com/");
}
}# demo.py
from base.spider import Spider as BaseSpider
class Spider(BaseSpider):
def init(self, extend=""):
self.extend = extend
def homeContent(self, filter):
return {"class": [
{"type_id": "movie", "type_name": "電影"}
]}
def categoryContent(self, tid, pg, filter, extend):
return {
"list": [self.card("demo-1", "範例項目")],
"pagecount": 1
}
def detailContent(self, ids):
item = self.card(ids[0], "範例項目")
item.update({
"vod_play_from": "Demo",
"vod_play_url": "第 1 集$demo://episode/1"
})
return {"list": [item]}
def playerContent(self, flag, id, vipFlags):
return {"parse": 0, "url": self.resolve(id)}
def card(self, id, name):
return {"vod_id": id, "vod_name": name,
"vod_pic": "https://example.com/poster.jpg"}
def resolve(self, id):
return id.replace("demo://", "https://example.com/")// demo.js — QuickJS ES module
const card = (id, name) => ({
vod_id: id,
vod_name: name,
vod_pic: "https://example.com/poster.jpg"
});
const resolve = (id) => id.replace("demo://", "https://example.com/");
export default {
init(ext) { this.ext = ext; },
home(filter) {
return JSON.stringify({ class: [
{ type_id: "movie", type_name: "電影" }
]});
},
category(tid, pg, filter, extend) {
return JSON.stringify({
list: [card("demo-1", "範例項目")],
pagecount: 1
});
},
detail(id) {
return JSON.stringify({ list: [{
...card(id, "範例項目"),
vod_play_from: "Demo",
vod_play_url: "第 1 集$demo://episode/1"
}]});
},
play(flag, id, vipFlags) {
return JSON.stringify({ parse: 0, url: resolve(id) });
}
};一般 ./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。
下表的方法名稱以 Java/Python 與 QuickJS 為主;Node.js 的 HTTP 端點請依上方獨立段落,不是把所有方法名稱直接加在網址後。
| 方法 | 何時呼叫 | 責任 | 主要回傳 |
|---|---|---|---|
init | 建立實例時;Node 重啟可再次呼叫 | 讀取 ext、建立連線或快取 | 無 |
homeContent / home | 進入首頁 | 分類與選填 filters | Result.class |
homeVideoContent / homeVod | 首頁分類完成 | 首頁推薦卡片 | Result.list |
categoryContent / category | 分類、換頁、變更篩選 | 該頁卡片與總頁數 | list + pagecount |
detailContent / detail | 點擊卡片 | 完整資訊與播放分組 | list[0] |
searchContent / search | 搜尋 | 搜尋結果,可支援頁碼 | list + pagecount |
playerContent / play | 選擇集數 | 最終 URL 與播放參數 | Result.url |
liveContent / live | 載入直播配置 | TXT、M3U 或 Group JSON | 原始文字 |
proxy / localProxy | 本地代理收到請求 | 依執行環境提供狀態、類型、內容與標頭 | 陣列 / HTTP 回應 |
action | 自訂操作 | 可由 Result 解析的結果 | JSON 字串 / Python dict |
destroy | 重新載入或清除 | 釋放執行緒、連線、引擎資源 | 無 |
home · 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。
playerContent{
"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 或配置旗標比對仍可能觸發解析。
| 欄位 | 類型 | 用途 | 預設 |
|---|---|---|---|
class | Class[] | 首頁分類清單。 | [] |
list | Vod[] | 首頁、分類與搜尋卡片;詳情取 list[0]。 | [] |
filters | object | 以分類 ID 為鍵,值為 Filter 陣列或單一 Filter。 | {} |
url | string | string[] | Url | 播放 URL 或多畫質清單,三種寫法見下方範例。 | — |
header | object | string | 播放請求標頭;也接受 JSON 物件字串。整份為空時回退 Site.header,不逐鍵合併。 | {} |
msg | string | 顯示提示文字;只有 code 為 0 時顯示。 | 空字串 |
code | integer | 訊息控制值,不是 HTTP 狀態碼;非 0 時隱藏 msg。 | 0 |
danmaku | Danmaku[] | string | 彈幕清單、單一 URL 或 JSON 陣列字串;仍受 App 彈幕載入設定控制。 | [] |
subs | Sub[] | 外掛字幕清單。 | [] |
playUrl | string | 解析前綴;json: 後接 JSON 解析端點,parse: 後接具名解析器。 | 空字串 |
artwork | string | 更新播放頁顯示的圖片。 | 空字串 |
jxFrom | string | 擴展 JSON 解析結果回報的解析器名稱,用於成功提示;不是播放分組旗標。 | 空字串 |
flag | string | 播放分組與解析比對旗標;未填時 App 補入呼叫 playerContent 時的 flag。 | 由 App 補入 |
desc | string | 清理後更新播放頁描述。 | 空字串 |
format | string | 媒體 MIME type 提示,請填完整類型,例如 application/x-mpegURL。 | — |
click | string | WebView 載入後執行的 JavaScript;Site.click 非空時優先。 | 空字串 |
key | string | App 內部回填的 Site key;爬蟲不必提供,提供也會被覆寫。 | 由 App 補入 |
position | integer | 播放起點,單位毫秒;120000 為 2 分鐘。未填時不覆蓋既有播放進度。 | — |
pagecount | integer | 總頁數;0 或未填表示未知,仍可繼續載入更多。 | 0 |
parse | integer | 1 要求解析;0 仍可能因 jx=1 或配置旗標比對進入解析。 | 0 |
jx | integer | 1 要求進入解析,即使 parse=0。 | 0 |
drm | Drm | 播放 DRM 設定,與 Channel.drm 使用相同物件,見配置字典。 | — |
單一 URL 見上方播放範例。陣列必須以「名稱、URL」成對排列,不是單純的 URL 清單;也可改用 values 物件。頂層 position 是毫秒,Url.position 則是從 0 起算的畫質索引,實際選取仍會由 App 調整。
playerContent{
"parse": 0,
"url": [
"主畫質", "https://example.com/video/main.m3u8",
"低畫質", "https://example.com/video/low.m3u8"
]
}playerContent{
"parse": 0,
"url": {
"values": [
{ "n": "主畫質", "v": "https://example.com/video/main.m3u8" },
{ "n": "低畫質", "v": "https://example.com/video/low.m3u8" }
],
"position": 0
}
}drm 的子欄位請參考共用物件字典;詳情的播放分組見下方集數格式。
list[] 中的 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 / localProxy[
200,
"text/plain; charset=utf-8",
"demo response",
{ "Cache-Control": "no-cache" }
]上例是 QuickJS / Python 一般陣列模式:狀態碼、Content-Type、內容、選填標頭;第 5 格 1 表示 Base64 內容。Python 方法名稱是 localProxy,第三格也可直接回傳 bytes。
Java 的一般 Object[] 回傳格式中,第三格必須是 InputStream,第四格標頭選填,不能直接放字串或 bytes;也可只回傳一格 NanoHTTPD.Response。QuickJS 的 from=catvod 另有 JSON response 模式;Node Proxy 則直接轉接 HTTP 回應。
代理請求帶 siteKey 時會交給對應 Spider;JAR 模式沒有 siteKey 時,則找 com.github.catvod.spider.Proxy.proxy(Map) 靜態入口。