Skip to content

Commit 8b98001

Browse files
senshinyaclaude
andcommitted
docs: 新增订阅源格式规范(Subscription Format Spec)
面向订阅源作者,说明 Base58 编码、JSON 结构(api_site/lives)、 CMS API 契约与构建约定。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 8948101 commit 8b98001

1 file changed

Lines changed: 216 additions & 0 deletions

File tree

docs/SUBSCRIPTION.md

Lines changed: 216 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,216 @@
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

Comments
 (0)