Free Player 通用播放器 0.1.0 正式版

Free Player / 开发者 / LOCAL_SOURCE_JS.md

本地 JS 源包格式

一份 JS 文件,下载后在客户端 QuickJS 内执行;Dart 负责抓取与 HLS 代理。

文件 LOCAL_SOURCE_JS.md 下载文件
返回开发者页面 LOCAL_SOURCE_JS.md · 7.6 KB
# 本地 JS 源包格式

> 适用:Free Player Windows 客户端 0.2+。  
> 目标:源从云端下载后完全在客户端进程内运行,不依赖 PHP、Node、Python 或任何外部程序。

---

## 1. 它是什么

本地源不是服务器,也不是可执行文件。它是一份 **JavaScript 文件**,包含:

- 源的元数据(id、名称、版本、上游地址、请求头);
- UI manifest(首页标签和导航);
- URL 生成函数;
- HTML 解析函数。

客户端下载并校验 SHA-256 后,把 JS 放进内置 QuickJS 运行时。Dart 侧负责:

- 用源声明的请求头抓取上游 HTML;
- 调用 JS 解析函数;
- 校验 HLS 播放列表;
- 启动本地 HLS 代理,改写 m3u8、密钥和分片地址;
- 把结果转换成 SourceProtocol 的 `Item`、`Episode`、`MediaPlan`。

因此 JS 里**不要发网络请求**,也不要依赖 `fetch`、`require`、`process` 或文件系统。

---

## 2. 包的最小结构

```js
var __FP_SOURCE = {
  api: 1,
  id: 'example',
  name: '示例源',
  version: '1.0.0',
  description: '一句话介绍',
  upstream: 'https://example.com',
  headers: {
    'User-Agent': 'Mozilla/5.0 ...',
    'Referer': 'https://example.com/'
  },
  brand: {
    name: '示例源',
    theme: { seed: '#3B6CF6', mode: 'auto' }
  },
  capabilities: ['feed', 'search', 'detail', 'episodes', 'play', 'ui_manifest'],

  ui: {
    ui_version: 1,
    default_page: 'home.feed',
    theme: { seed: '#3B6CF6', mode: 'auto' },
    home_tabs: [
      { key: 'home', title: '推荐', channel: 'home' }
    ],
    navigation: [
      {
        title: '内容',
        icon: 'home',
        items: [
          { title: '首页', icon: 'home', page: 'home.feed' },
          { title: '搜索', icon: 'search', page: 'search' }
        ]
      }
    ],
    labels: {
      play: '播放',
      'detail.tab.episodes': '选集'
    }
  },

  urls: {
    feed: function (ctx) { return '/'; },
    search: function (ctx) { return '/search?q=' + encodeURIComponent(ctx.q); },
    detail: function (ctx) { return '/item/' + encodeURIComponent(ctx.id); },
    play: function (ctx) {
      return '/play/' + encodeURIComponent(ctx.item_id)
        + '-' + encodeURIComponent(ctx.sid)
        + '-' + encodeURIComponent(ctx.nid);
    }
  },

  parseList: function (html, ctx) {
    return { items: [], next: '', total: 0 };
  },

  parseDetail: function (html, ctx) {
    return {
      item: {
        id: ctx.id,
        title: '',
        cover: '',
        description: '',
        tags: [],
        badges: [],
        updated_at: '',
        extra: {}
      },
      lines: [
        { sid: '1', name: '线路 1', episodes: [{ nid: '1', title: '第 1 集' }] }
      ]
    };
  },

  parsePlay: function (html, ctx) {
    return { url: 'https://cdn.example.com/index.m3u8', name: ctx.line_name };
  }
};
```

---

## 3. 必需字段

| 字段 | 类型 | 说明 |
|---|---|---|
| `api` | number | 固定 `1` |
| `id` | string | 源 ID,必须和 `/api/sources` 的 `source_id` 一致 |
| `name` | string | 客户端显示的名称 |
| `version` | string | 源版本 |
| `upstream` | string | 抓取根地址,必须是 http/https |
| `headers` | object | 抓取上游时附带的请求头 |
| `capabilities` | array | 支持的能力;至少包含 `feed`、`ui_manifest` |
| `ui` | object | UI manifest,结构同 SourceProtocol |
| `urls` | object | 地址生成函数 |
| `parseList` | function | 列表/搜索/分类解析 |
| `parseDetail` | function | 详情和线路解析 |
| `parsePlay` | function | 播放页解析 |

可选:

| 字段 | 说明 |
|---|---|
| `parseRank(html, ctx)` | 榜单解析;返回和 `parseList` 相同的形状 |
| `parseCategoryMeta(html, ctx)` | 分类筛选面板解析 |
| `rankMeta()` | 榜单筛选面板,返回 `{groups, defaults}` |
| `brand` | 主题色与品牌 |
| `limits` | `max_page_size`、`search_timeout_ms` |

---

## 4. 客户端调用约定

所有解析函数都是同步的:

```js
parseList(html, ctx) -> { items, next, total }
parseDetail(html, ctx) -> { item, lines }
parsePlay(html, ctx) -> { url, name } | null
```

`ctx` 包含当前动作的参数:

| 动作 | ctx |
|---|---|
| `feed` | `channel`, `cursor`, `page`, `limit` |
| `search` | `q`, `page`, `limit` |
| `rank` | `type`, `time`, `page`, `limit` |
| `new` | `gender`, `page`, `limit` |
| `category` | `type`, `area`, `genre`, `year`, `lang`, `sort`, `page`, `limit` |
| `detail` | `id` |
| `play` | `item_id`, `sid`, `nid`, `line_name`, `quality` |

`urls` 函数只返回地址(http/https 或站内路径),不要返回值对象:

```js
urls.detail({ id: '123' }) // '/item/123'
```

Dart 会用 `upstream` 解析相对地址。

---

## 5. 数据结构

### 列表项

```js
{
  id: '123',
  item_id: '123',
  title: '标题',
  cover: 'https://...',
  type: 'series',
  description: '',
  tags: [],
  badges: ['更新至 10 集'],
  updated_at: '',
  extra: { note: '更新至 10 集', score: 8.2 }
}
```

### 详情

```js
{
  item: { id, title, cover, description, tags, badges, updated_at, extra },
  lines: [
    {
      sid: '1',
      name: '线路 1',
      episodes: [
        { nid: '1', title: '第 1 集' },
        { nid: '2', title: '第 2 集' }
      ]
    }
  ]
}
```

客户端把第一集最多的线路作为默认线路;集 ID 由客户端拼成 `item_id-sid-nid`。

### 播放

```js
{
  url: 'https://cdn.example.com/index.m3u8',
  name: '线路 1'
}
```

客户端会:

1. 用源请求头拉取该 m3u8;
2. 要求内容以 `#EXTM3U` 开头;
3. 启动本地代理,改写 m3u8 里的子列表、AES key 和分片 URI;
4. 把本地代理地址交给播放器。

如果解析失败或播放列表不可用,返回 `null`,客户端会尝试同集的其他线路。

---

## 6. 安装方式

用户有三种安装本地源的方法:

1. **推荐源列表**:源在 `/api/sources` 里标记为 `local-js`,客户端在「添加源 → 网络源 → 官方推荐源」里点“下载”自动安装,列表会标出本地源。
2. **输入或粘贴脚本**:在客户端「添加源 → 本地源」里填 JS 地址,或直接粘贴脚本内容。地址方式会在线读取,安装后保存到本机。
3. **手动目录**:下载 `.js` 文件,放进客户端应用数据目录下的 `local_sources/`,点“扫描目录”导入;下次启动也会自动扫描。更新时替换同名文件,或先删除旧源再扫描。

手动安装的脚本也会被读取和执行;只放你信任的文件。

## 7. 发布与更新

1. 把 JS 放到网站,例如 `public/sources/example/local.js`。
2. 在后台“源管理”新建源,类型选“本地 JS 源包”,填写:
   - 源包地址 `/sources/example/local.js`
   - 源包版本 `1.0.0`
   - 源 ID
3. 站内文件不需要手填 SHA-256 和大小;`/api/sources` 会按文件实时计算。
4. 修改 JS 后,接口哈希自动变化;客户端打开推荐源列表时会比较版本和哈希,显示“更新”,下载后替换本机脚本。建议同时递增源包版本,便于用户识别。

---

## 8. 限制与安全

- JS 在 QuickJS 沙箱内执行,不暴露 Dart 文件系统、进程或任意网络 API。
- 解析函数必须是同步的;不要使用 `async` / `await` / Promise。
- 客户端按 UTF-8 解码上游 HTML;非 UTF-8 页面需要先在源站转码。
- 不要内置账号、Cookie、密钥或上游签名;只放公开页面解析逻辑。
- 客户端会校验源包 SHA-256;源站被篡改时不会安装。
- 本地源只适合公开可抓取的页面。需要登录的上游请使用线上源或本地核心。

完整的参考实现见 [555 本地源](/sources/555/local.js)。