|
| 1 | +# 订阅源格式规范(Subscription Format Spec) |
| 2 | + |
| 3 | +本文件定义 LunaTV / Selene 的订阅源数据格式,供**订阅源作者**参考构建。订阅是一个可远程访问的 URL,返回 **Base58 编码后的 JSON**,其中聚合了若干「搜索源(点播)」与「直播源」。 |
| 4 | + |
| 5 | +--- |
| 6 | + |
| 7 | +## 1. 总览 |
| 8 | + |
| 9 | +一份订阅就是一个 HTTP(S) 端点,客户端对它 `GET` 后会: |
| 10 | + |
| 11 | +1. 取得响应正文(一段 Base58 字符串); |
| 12 | +2. Base58 解码为 UTF-8 文本; |
| 13 | +3. 按本规范解析 JSON,得到搜索源列表 + 直播源列表。 |
| 14 | + |
| 15 | +订阅本身**只承载源清单**,不包含任何用户数据(收藏 / 播放记录 / 搜索历史等均由客户端本地保存,与订阅无关)。作为作者,你要做的就是:按第 4 节组织 JSON → 按第 3 节 Base58 编码 → 让 URL 以 `200` 返回这段字符串。 |
| 16 | + |
| 17 | +--- |
| 18 | + |
| 19 | +## 2. 传输层要求 |
| 20 | + |
| 21 | +| 项目 | 要求 | |
| 22 | +|---|---| |
| 23 | +| 方法 | 响应 `GET` | |
| 24 | +| 协议 | 仅 `http` / `https` | |
| 25 | +| 重定向 | **不会被跟随**,请直接提供最终可访问的地址 | |
| 26 | +| 状态码 | 必须返回 `200` | |
| 27 | +| 响应时间 | 连接 / 读取需在 10s 内完成 | |
| 28 | +| 大小上限 | 正文 **≤ 1 MB** | |
| 29 | +| TLS | 允许自签证书(客户端不校验证书链与 hostname),但仍建议使用有效证书 | |
| 30 | +| 响应正文 | 一段 Base58 字符串(首尾空白会被忽略) | |
| 31 | +| `Content-Type` | 不限,任意值均可 | |
| 32 | + |
| 33 | +--- |
| 34 | + |
| 35 | +## 3. 编码:Base58(无 checksum) |
| 36 | + |
| 37 | +响应正文是对「UTF-8 JSON 字节」做的 **标准 Base58 编码**,**不带 checksum**。 |
| 38 | + |
| 39 | +- 字符集:`123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz`(去掉了易混淆的 `0 O I l`)。 |
| 40 | +- 前导 `1` 表示前导零字节。 |
| 41 | +- 多数语言可用现成库:JS 的 `bs58`、Python 的 `base58`、Go 的 `btcutil/base58` 等(取其**不含 checksum** 的基础 `base58` 接口,而非 `base58check`)。 |
| 42 | + |
| 43 | +> Base58 仅用于编码、便于与既有生成器互通,**并非加密**——它不提供任何保密性,不要把敏感信息放进订阅。 |
| 44 | +
|
| 45 | +**生成订阅(伪代码):** |
| 46 | + |
| 47 | +``` |
| 48 | +json_text = JSON.stringify(payload) # 见第 4 节 |
| 49 | +utf8_bytes = utf8_encode(json_text) |
| 50 | +body = base58_encode(utf8_bytes) # 无 checksum |
| 51 | +serve HTTP 200 with body |
| 52 | +``` |
| 53 | + |
| 54 | +JS 示例: |
| 55 | + |
| 56 | +```js |
| 57 | +import bs58 from 'bs58' |
| 58 | +const payload = { api_site: { /* ... */ }, lives: { /* ... */ } } |
| 59 | +const body = bs58.encode(Buffer.from(JSON.stringify(payload), 'utf8')) |
| 60 | +// 把 body 作为该 URL 的响应正文,状态码 200 |
| 61 | +``` |
| 62 | + |
| 63 | +--- |
| 64 | + |
| 65 | +## 4. JSON 结构(Base58 解码后) |
| 66 | + |
| 67 | +顶层是一个对象,包含两个可选键:`api_site`(搜索 / 点播源)与 `lives`(直播源)。**两者均为「map(对象)」**——键是源的唯一标识,值是源定义对象。未识别的键会被忽略。 |
| 68 | + |
| 69 | +```jsonc |
| 70 | +{ |
| 71 | + "api_site": { |
| 72 | + "<source_key>": { /* 搜索源,见 4.1 */ }, |
| 73 | + "...": { } |
| 74 | + }, |
| 75 | + "lives": { |
| 76 | + "<live_key>": { /* 直播源,见 4.2 */ }, |
| 77 | + "...": { } |
| 78 | + } |
| 79 | +} |
| 80 | +``` |
| 81 | + |
| 82 | +> 顶层用 **map 而非数组**:当条目自身缺省 `key` 字段时,map 的 key 会充当兜底标识。 |
| 83 | +
|
| 84 | +### 4.1 `api_site` 条目 —— 搜索 / 点播源 |
| 85 | + |
| 86 | +| 字段 | 类型 | 必填 | 默认 | 说明 | |
| 87 | +|---|---|---|---|---| |
| 88 | +| `key` | string | 否 | 取所在 map 的 key | 源唯一标识。缺省时回退为外层 map 的 entry key | |
| 89 | +| `name` | string | **是** | `""` | 展示名 | |
| 90 | +| `api` | string | **是** | `""` | 苹果 CMS(maccms)风格 API 基址,详见第 5 节契约 | |
| 91 | +| `detail` | string | 否 | `null` | 详情页站点根(HTML 抓取兜底,见 5.3)。CMS 标准 JSON 详情可用时无需提供 | |
| 92 | +| `from` | string | 否 | `null` | 来源标注 / 出处,仅元信息 | |
| 93 | + |
| 94 | +- `name` / `api` 缺省会得到空串;**空 `api` 的源不可用**,请务必提供。 |
| 95 | +- 单个条目若字段无法解析,会被**静默跳过**,不影响其余源。 |
| 96 | + |
| 97 | +### 4.2 `lives` 条目 —— 直播源 |
| 98 | + |
| 99 | +| 字段 | 类型 | 必填 | 默认 | 说明 | |
| 100 | +|---|---|---|---|---| |
| 101 | +| `key` | string | 否 | 取所在 map 的 key | 源唯一标识。缺省时回退为外层 map 的 entry key | |
| 102 | +| `name` | string | **是** | `""` | 展示名 | |
| 103 | +| `url` | string | **是** | `""` | 直播频道清单地址(通常是 m3u 列表,亦可为单条流地址) | |
| 104 | +| `ua` | string | 否 | `""` | 自定义 User-Agent(部分源需要) | |
| 105 | +| `epg` | string | 否 | `""` | 节目单(EPG)XML 地址 | |
| 106 | +| `from` | string | 否 | `""` | 来源标注 / 出处 | |
| 107 | + |
| 108 | +--- |
| 109 | + |
| 110 | +## 5. `api` 字段的 CMS API 契约 |
| 111 | + |
| 112 | +`api_site[*].api` 必须指向一个**苹果 CMS(maccms)风格**的 JSON 接口基址(不含 query)。客户端会直接拼接以下 query 调用,源站需按此响应: |
| 113 | + |
| 114 | +### 5.1 搜索(列表) |
| 115 | +``` |
| 116 | +GET {api}?ac=videolist&wd={关键词} |
| 117 | +GET {api}?ac=videolist&wd={关键词}&pg={页码} # 翻页 |
| 118 | +``` |
| 119 | +返回标准 maccms `videolist` JSON(含 `list[]`,每项有 `vod_id` / `vod_name` / `vod_pic` / `vod_play_url` 等)。 |
| 120 | + |
| 121 | +### 5.2 详情(首选) |
| 122 | +``` |
| 123 | +GET {api}?ac=videolist&ids={vod_id} |
| 124 | +``` |
| 125 | +绝大多数 CMS 源此接口可用,用于取单片详情与播放地址。 |
| 126 | + |
| 127 | +### 5.3 详情兜底(HTML) |
| 128 | +当 5.2 不可用且条目提供了 `detail` 时,客户端会回退抓取详情 HTML: |
| 129 | +``` |
| 130 | +GET {detail}/index.php/vod/detail/id/{vod_id}.html |
| 131 | +``` |
| 132 | +因此 `detail` 应填**站点根**(不含 `/index.php/...`),例如 `https://example.com`。 |
| 133 | + |
| 134 | +> 简言之:`api` 应支持 `ac=videolist` 的 `wd=` 搜索与 `ids=` 详情;`detail` 是可选的 HTML 兜底。 |
| 135 | +
|
| 136 | +--- |
| 137 | + |
| 138 | +## 6. 订阅必须满足的条件 |
| 139 | + |
| 140 | +| 条件 | 不满足的后果 | |
| 141 | +|---|---| |
| 142 | +| URL 合法且为 http(s) | 拒绝加载 | |
| 143 | +| 响应状态码为 `200` | 拒绝加载 | |
| 144 | +| 正文 ≤ 1 MB | 拒绝加载 | |
| 145 | +| 正文为合法 Base58 | 拒绝加载(解码失败) | |
| 146 | +| 解码后为合法 JSON | 拒绝加载(解析失败) | |
| 147 | +| `api_site` + `lives` 解析后**总条目数 ≥ 1** | 拒绝加载(无可用源) | |
| 148 | + |
| 149 | +注意:客户端只校验「至少有一个源」,**不会**校验各源 URL 是否真的能连通——源是否可用由你自行保证。 |
| 150 | + |
| 151 | +--- |
| 152 | + |
| 153 | +## 7. 完整示例(Base58 解码后的明文 JSON) |
| 154 | + |
| 155 | +```json |
| 156 | +{ |
| 157 | + "api_site": { |
| 158 | + "source_a": { |
| 159 | + "name": "示例点播源 A", |
| 160 | + "api": "https://api-a.example.com/api.php/provide/vod", |
| 161 | + "detail": "https://api-a.example.com" |
| 162 | + }, |
| 163 | + "source_b": { |
| 164 | + "key": "source_b", |
| 165 | + "name": "示例点播源 B", |
| 166 | + "api": "https://api-b.example.com/api.php/provide/vod", |
| 167 | + "detail": "https://www.example-b.com", |
| 168 | + "from": "demo" |
| 169 | + } |
| 170 | + }, |
| 171 | + "lives": { |
| 172 | + "iptv_main": { |
| 173 | + "name": "默认直播", |
| 174 | + "url": "https://live.example.com/playlist.m3u", |
| 175 | + "epg": "https://epg.example.com/guide.xml" |
| 176 | + }, |
| 177 | + "ua_required": { |
| 178 | + "name": "需要UA的源", |
| 179 | + "url": "https://live.example.com/special.m3u", |
| 180 | + "ua": "Mozilla/5.0 (Linux; Android 10) AppleWebKit/537.36" |
| 181 | + } |
| 182 | + } |
| 183 | +} |
| 184 | +``` |
| 185 | + |
| 186 | +将上面这段 JSON 用 Base58(无 checksum)编码后,由订阅 URL 以 `HTTP 200` 返回即可。 |
| 187 | + |
| 188 | +--- |
| 189 | + |
| 190 | +## 8. 最小可用订阅 |
| 191 | + |
| 192 | +只放搜索源、不放直播也合法(反之亦然),只要总数 ≥ 1: |
| 193 | + |
| 194 | +```json |
| 195 | +{ |
| 196 | + "api_site": { |
| 197 | + "demo": { |
| 198 | + "name": "示例源", |
| 199 | + "api": "https://example.com/api.php/provide/vod" |
| 200 | + } |
| 201 | + } |
| 202 | +} |
| 203 | +``` |
| 204 | + |
| 205 | +--- |
| 206 | + |
| 207 | +## 9. 给订阅作者的建议 |
| 208 | + |
| 209 | +- **`key` 保持稳定**:它用于客户端去重 / 标识;改 key 等于换了一个新源。 |
| 210 | +- **`name` 简短可读**:会直接显示在界面上。 |
| 211 | +- **优先保证 `api` 的 `ac=videolist` 搜索与 `ids=` 详情可用**,能省掉 `detail` HTML 兜底。 |
| 212 | +- **不要塞敏感信息**:Base58 不是加密,订阅正文可被任何拿到 URL 的人解码。 |
| 213 | +- **控制体积**:编码前后均建议远小于 1 MB 上限。 |
| 214 | +- **直播 `url`** 建议为标准 m3u 列表。 |
| 215 | +</content> |
| 216 | +</invoke> |
0 commit comments