Appearance
数据模型
展示型扩展(widget / search)生成 ListItem。服务型扩展基于 MediaRef 工作。页面之间仅传递 ref 和 Open 意图,不传递函数名。
MediaRef
ts
{
source: "tmdb" | "imdb" | "douban" | "url" | "custom";
id: string;
mediaType?: "movie" | "tv";
}| 字段 | 规则 |
|---|---|
source | 标识来源,用于详情和播放解析 |
id | 对应 source 下的稳定字符串。例如,tmdb ID 应为 "1399",而不是 "tv.1399" |
mediaType | tmdb / imdb / douban 必填 |
Open 意图
App 将点击操作转换为:
ts
{
kind: "detail" | "play";
ref: MediaRef;
episode?: { season: number; episode: number };
origin: { packageId: string; extensionId: string; instanceId?: string };
}图片
图片字段不提供别名:
ts
{
poster?: string;
backdrop?: string;
cover?: string;
detailPoster?: string;
stills?: string[];
}| 位置 | 回退链 |
|---|---|
| 横图 | backdrop → cover → poster |
| 竖图 | poster → backdrop → cover |
| 详情海报 | detailPoster → 竖图链 |
| 剧照 | 仅使用 stills |
| 预告封面 | trailers[].cover → cover → backdrop → poster |
对于使用内置详情的平台条目,图片可由平台数据决定。如果扩展仍返回 images,tmdb 的 poster / backdrop 应传递原始路径,例如 /abc.jpg。
ListItem
ts
{
ref: MediaRef;
title: string;
subtitle?: string;
description?: string;
rating?: number;
releaseDate?: string;
genre?: string;
images?: { poster?: string; backdrop?: string; cover?: string };
intent?: "detail" | "play";
episode?: { season: number; episode: number };
}省略 intent 时默认为 "detail"。episode 决定剧集能否从小组件直接播放,详见 widget。
DetailItem
包含 ListItem 的展示字段(不含 intent),并扩展:
ts
{
images?: {
poster?: string;
backdrop?: string;
cover?: string;
detailPoster?: string;
stills?: string[];
};
genres?: { id: string; title: string }[];
people?: { id: string; title: string; avatar?: string; role?: string }[];
trailers?: { title?: string; url: string; cover?: string }[];
episodes?: Episode[];
related?: ListItem[];
runtimeSeconds?: number;
}Episode:{ season, episode, title?, ref? }。ref 缺省沿用详情的 ref。
点击 related 条目时同样使用 Open 解析,origin 保持为打开当前详情时的来源。
当 genres / people 包含 id 时,可在生成条目的 widget 支持相应筛选后用于返回筛选结果;当前版本仅用于展示。
Page / WidgetContent
ts
type PageToken =
| { type: "page"; page: number }
| { type: "cursor"; cursor: string };
type Page<T> = {
items: T[];
next?: PageToken;
};widget 的 load 和 search 的 search 均返回 { items, next? },等同于 Page<ListItem>。省略 next 表示已到末页。空数组表示请求成功但没有数据,不表示失败。
widget 的 load 输入为 { instanceId?, params, family, page? }。instanceId 是该实例的稳定 ID,位于 input 顶层,不属于 params。详见 widget。
search 的输入为 { query, params, page?, instanceId? }。query 由 App 注入,不属于 params。详见 search。
PlaybackSource
ts
{
id: string;
name: string;
url: string;
description?: string;
headers?: Record<string, string>;
player?: "system" | "app";
skipRedirectProbe?: boolean;
}| 字段 | 必填 | 规则 |
|---|---|---|
id | 是 | 稳定标识,用于记录用户选中的线路 |
name | 是 | 线路名 |
url | 是 | 最终播放地址 |
description | 否 | 分辨率、编码等 |
headers | 否 | 播放请求头,不含内部控制键 |
player | 否 | system 或 app |
skipRedirectProbe | 否 | 为 true 时跳过播放前的 GET 探测,适用于直播或一次性 URL |
sources 的输入为 { ref, instanceId?, params?, season?, episode? }。从 widget 实例进入时,instanceId 和 params 对应该实例。空数组表示当前没有可用线路。完整 protocol 见 playback。
字幕
ts
type SubtitleHit = {
id: string;
title: string;
language: string;
};
type SubtitleFile = {
url: string;
fileName?: string;
};SubtitleHit
| 字段 | 必填 | 规则 |
|---|---|---|
id | 是 | 稳定标识。宿主会保存完整的 hit,并在后续调用中传回 file |
title | 是 | 在选择器中显示的名称 |
language | 是 | BCP 47 |
match 的输入为 { title?, season?, episode?, fileName?, fileSize?, fileHash?, duration?, ref? }。search 的输入为 { query, ref?, season?, episode? }。空数组表示当前没有可用字幕。
SubtitleFile
| 字段 | 必填 | 规则 |
|---|---|---|
url | 是 | http(s) 或 bundle://,指向单个字幕文件 |
fileName | 否 | 保存到本地时使用的文件名,包含扩展名 |
file 的输入为 { hit }。返回 null 表示无法获取该文件。完整 protocol 见 subtitle。
弹幕
ts
type DanmuMatch = {
id: string;
title: string;
kind?: "movie" | "series";
episode?: DanmuEpisode;
};
type DanmuEpisode = {
id: string;
title: string;
season?: number;
episode?: number;
};
type DanmuCue = {
time: number;
text: string;
mode?: "rtl" | "top" | "bottom";
color?: string;
id?: string;
};DanmuMatch
| 字段 | 必填 | 规则 |
|---|---|---|
id | 是 | 稳定标识,用于记录用户选中的匹配项 |
title | 是 | 在选择器中显示的名称 |
kind | 否 | movie 或 series |
episode | 否 | 自动匹配已定位到具体剧集时提供。宿主将直接获取弹幕,不再调用 episodes |
match 的输入为 { title?, season?, episode?, fileName?, fileSize?, fileHash?, duration?, ref? }。search 的输入为 { query, ref? }。空数组表示当前没有弹幕匹配项。
DanmuEpisode
| 字段 | 必填 | 规则 |
|---|---|---|
id | 是 | 稳定 |
title | 是 | 分集名称 |
season | 否 | 季 |
episode | 否 | 集 |
episodes 的输入为 { match }。电影类扩展可以不实现此方法。
DanmuCue
| 字段 | 必填 | 规则 |
|---|---|---|
time | 是 | 出现时间,单位为秒 |
text | 是 | 弹幕正文 |
mode | 否 | rtl / top / bottom |
color | 否 | 颜色信息,宿主可以忽略 |
id | 否 | 去重 |
comments 的输入为 { match, episode?, segment? }。segment 的单位为秒,范围采用 [start, end)。空数组表示该时间段没有弹幕。完整 protocol 见 danmu。
用户字段
包 settings 与 widget / search 的 params 使用相同的字段结构。type 仅支持以下四种类型,请勿使用 file、date、password 或其他值:
type | 控件 | 传到 handler 的值 | 备注 |
|---|---|---|---|
text | 单行文本 | string | options 用作建议值,不构成枚举约束 |
number | 数字 | number | |
boolean | 开关 | true / false | default 写 JSON 布尔,不要写成 "true" |
select | 单选 | string(option.value) | 必须有 options: { title, value }[] |
公共键:name、title、description?、default?、options?、required?、visibleWhen?: { field, in }。
省略 required 或将其设为 false 时,字段为选填项。设为 true 时,填写页会在控件中显示必填提示,包括标签和空文本框的 placeholder。当可见的必填字段没有有效值时,扩展列表会显示提醒。未设置 default、用户未填写或仅输入空白字符,均视为未填写。boolean / number 只要存在值(包括 false / 0)即视为已填写。该校验不会阻止 load / search 调用,空值仍可能进入 input.params。
visibleWhen.in 始终是字符串。boolean 字段用 "true" / "false" 比较。被 visibleWhen 隐藏的必填字段不显示提示,也不计入扩展列表提醒。
当前版本会将未知 type 按 text 渲染,但扩展不应依赖此回退行为。当前界面尚未渲染 text 的建议列表。包 settings 在包详情页填写,并通过 ctx.settings 读取;用法见 开发者指南 · 设置。扩展 params 的用法见 开发者指南 · 参数。