# 应用完整开发文档 本文是当前项目的唯一完整说明文档,面向 App 使用者、配置维护者、WebHome 首页开发者、爬虫开发者和后续改造 AI。本文按当前代码行为整理,包含项目结构、配置、Spider、WebHome、本地 HTTP 服务、网盘检测、特殊协议、隐藏功能、打包和安全注意事项。 ## 1. 项目定位 这是一个基于 CatVod 生态的 Android 影音应用,包名为 `com.fongmi.android.tv`,同时支持手机端和电视端。 核心能力: - 点播:多站点分类、详情、搜索、换源、收藏、最近观看。 - 直播:M3U、TXT、JSON、EPG、时移、收藏、分组。 - 播放:ExoPlayer/Media3、FFmpeg、DRM、字幕、弹幕、倍速、缩放、画中画。 - 扩展:Java/JAR Spider、JavaScript Spider、Python Spider、HTTP API 站点。 - WebHome:CSP 自定义网页首页,可调用 App Native SDK。 - Pan:`pan.check` 提供网盘有效性检测,`pan.play` 提供网盘播放语义入口并复用 `push_agent/pvideo` 链路。 - 增强功能:集中放置网盘检测、壳代理、一键同步、调试日志等能力。 - 本地服务:局域网 HTTP 服务、文件管理、远程推送、远程控制、多设备同步。 - 投屏:手机端可投屏,电视端可作为 DLNA Renderer 接收投屏。 ## 2. 目录结构 ```text TV/ ├── app/ Android 主应用 ├── catvod/ CatVod 抽象层、Spider 接口、OkHttp、代理工具 ├── quickjs/ JavaScript Spider 运行时 ├── chaquo/ Python Spider 运行时 ├── other/ 其它构建或依赖模块 └── docs/应用完整开发文档.md ``` 主要源码分层: ```text app/src/main/ 手机端和电视端共用业务逻辑 app/src/mobile/ 手机端 UI app/src/leanback/ 电视端 UI app/src/main/assets 内置局域网页面和解析页 ``` ## 3. Flavor、包和打包 当前主要 flavor: | Flavor | 场景 | | --- | --- | | `mobile` | 手机/平板端 | | `leanback` | Android TV/电视盒子端 | 常用手机端 arm64 release 打包: ```bash bash gradlew assembleMobileArm64_v8aRelease ``` 当前项目常见 APK 输出路径: ```text app/build/outputs/apk/mobileArm64_v8a/release/mobile-arm64_v8a.apk Release/apk/mobile-arm64_v8a.apk ``` 实际以 Gradle 本次构建输出为准。 版本更新行为: - App 启动时不会自动弹出版本更新弹窗。 - 用户仍可在设置页手动点击版本检查。 ## 4. 配置总览 配置是 App 的主要开放入口。Vod 配置通常是 JSON。当前代码识别的顶层字段如下: ```json { "spider": "./spider.jar", "sites": [], "parses": [], "lives": [], "doh": [], "proxy": [], "hosts": [], "headers": [], "rules": [], "ads": [], "flags": [], "wallpaper": "", "logo": "", "notice": "", "home": "", "parse": "", "urls": [], "msg": "" } ``` 顶层字段: | 字段 | 类型 | 说明 | | --- | --- | --- | | `spider` | string | 全局 JAR Spider 地址。站点或直播源未单独指定 `jar` 时使用 | | `sites` | array | 点播站点列表,元素结构见“点播站点配置” | | `parses` | array | 点播解析器列表,元素结构见“解析器配置” | | `lives` | array | 直播配置列表。点播配置内带 `lives` 时,会同步生成直播配置 | | `doh` | array | DNS over HTTPS 配置,元素结构见下方 `doh` 表;配置值会追加到内置 DoH 列表后生效 | | `proxy` | array | HTTP/HTTPS/SOCKS 代理规则,元素结构见下方 `proxy` 表 | | `hosts` | array | host 覆盖规则,字符串数组,格式为 `匹配规则=目标host或IP` | | `headers` | array | 按 host 注入请求 header,元素结构见下方 `headers` 表 | | `rules` | array | 嗅探规则,元素结构见下方 `rules` 表;点播和直播规则会合并进入 `RuleConfig` | | `ads` | array | 广告域名或正则字符串数组;命中后解析 WebView 会拦截该 host | | `flags` | array | 播放 flag 字符串数组,用于识别需要解析或特殊处理的播放来源 | | `wallpaper` | string | 壁纸配置 URL 或路径;存在时会同步生成壁纸配置 | | `logo` | string | 配置图标,保存到当前 Config | | `notice` | string | 配置公告文本,保存到当前 Config | | `home` | string | 默认站点 key;App 会优先选择 `sites[].key` 等于该值的站点 | | `parse` | string | 默认解析器名称;App 会优先选择 `parses[].name` 等于该值的解析器 | | `urls` | array | 配置仓库/多配置入口,元素结构为 `{ "name": "显示名", "url": "配置URL" }`;存在该字段时 App 按 depot 处理,不再按普通点播配置解析 | | `msg` | string | 错误消息。存在该字段时加载配置会直接抛出该消息 | `doh` 元素字段: | 字段 | 类型 | 说明 | | --- | --- | --- | | `name` | string | DoH 显示名 | | `url` | string | DoH endpoint URL,例如 `https://dns.google/dns-query` | | `ips` | array | bootstrap DNS IP 字符串数组;为空时使用系统 DNS 解析 DoH host | `proxy` 元素字段: | 字段 | 类型 | 说明 | | --- | --- | --- | | `name` | string | 代理规则名称 | | `hosts` | array | host 匹配字符串数组;匹配方式是 `text.contains(rule)` 或 `text.matches(rule)` | | `urls` | array | 代理地址数组。支持 `http://host:port`、`https://host:port`、`socks://host:port`、`socks5://host:port`;带账号密码时使用 `scheme://user:pass@host:port` | `proxy.hosts` 示例: ```json { "proxy": [ { "name": "app", "hosts": ["example.com", ".*\\.example\\.com", "*"], "urls": ["socks5://127.0.0.1:7897"] } ] } ``` `proxy.hosts` 只写目标 host 匹配规则,不写 `=` 映射。`*` 表示匹配所有非本机目标 host。 顶层 `hosts` 字符串规则: ```json { "hosts": [ "example.com=1.2.3.4", ".*\\.example\\.com=cdn.example.com" ] } ``` 顶层 `hosts` 的左侧是精确 host、包含匹配片段或 Java 正则,右侧是要交给 DNS 继续解析的目标 host/IP。不要把 `example.com=1.2.3.4` 这种 DNS 写法放到 `proxy.hosts` 里。 `headers` 元素字段: | 字段 | 类型 | 说明 | | --- | --- | --- | | `host` | string | host 匹配规则;匹配方式是 `text.contains(host)` 或 `text.matches(host)` | | `header` | object | 命中后注入的 header key/value;会覆盖同名请求 header | `rules` 元素字段: | 字段 | 类型 | 说明 | | --- | --- | --- | | `name` | string | 规则名;相同名称视为同一规则 | | `hosts` | array | 适用 host 匹配规则;匹配当前 URL host 和 `url` 查询参数里的 host | | `regex` | array | 视频 URL 识别规则;URL 包含规则字符串或匹配 Java 正则时视为可播放地址 | | `exclude` | array | 排除规则;URL 包含规则字符串或匹配 Java 正则时不会被当作视频地址 | | `script` | array | 解析 WebView 页面加载后顺序执行的 JS 脚本 | 配置中的相对路径以配置文件 URL 为基准解析。`spider`、站点 `api/ext/homePage`、直播 `api/ext`、解析器 `url` 等路径会经过 `UrlUtil.convert()` 或相对 URL 解析。 ## 5. 点播站点配置 站点字段示例: ```json { "key": "site_key", "name": "站点名", "type": 3, "api": "csp_MySpider", "jar": "./spider.jar", "ext": "./ext.json", "click": "document.querySelector('video').click()", "playUrl": "", "homePage": "./nostr.html", "hide": 0, "indexs": 0, "timeout": 30, "searchable": 1, "changeable": 1, "quickSearch": 1, "categories": ["电影", "剧集"], "header": { "User-Agent": "Mozilla/5.0" }, "style": { "type": "rect", "ratio": 1.33 } } ``` 字段说明: | 字段 | 类型 | 说明 | | --- | --- | --- | | `key` | string | 站点唯一标识。原生详情、播放、收藏、历史都会使用该值定位站点 | | `name` | string | 展示名 | | `type` | number | 站点类型,完整取值见下方站点 `type` 表;为空时按 `0` 处理 | | `api` | string | API 地址或 Spider 类名。`type=3` 时为 `csp_Xxx`、JS 文件 URL 或 Python 文件 URL;`type=0/1/2/4` 时为 HTTP 地址 | | `jar` | string | 当前站点独立 JAR,覆盖全局 `spider` | | `ext` | string/object | 传给 Spider `init` 的扩展参数;配置解析阶段会把 URL 或相对路径转成绝对地址,Spider 初始化前如果是远程 URL 会拉取文本 | | `click` | string | WebView 解析点击脚本,解析页面加载后会优先执行 | | `playUrl` | string | 站点级播放前缀或解析辅助。播放结果中 `playUrl` 也可用 `json:`、`parse:` 前缀指定解析器行为 | | `homePage` | string | 自定义 WebHome 首页 | | `home_page` | string | `homePage` 别名 | | `webHome` | string | `homePage` 别名 | | `web_home` | string | `homePage` 别名 | | `hide` | number | `0` 显示,`1` 隐藏。为空时按 `0` 处理 | | `indexs` | number | `0` 普通站点,`1` 索引站点。索引站点条目点击进入聚合搜索 | | `timeout` | number | 播放超时,单位秒;为空时使用默认 15 秒,最小按 1 秒处理 | | `searchable` | number | `0` 永久禁用搜索,`1` 当前启用搜索,`2` 当前禁用搜索但允许 App 根据用户操作改回 `1`;为空时按 `1` 处理 | | `changeable` | number | `0` 永久禁用换源,`1` 当前允许换源,`2` 当前禁用换源但允许 App 根据用户操作改回 `1`;为空时按 `1` 处理 | | `quickSearch` | number | `0` 禁用快速搜索,`1` 启用快速搜索。为空时按 `1` 处理 | | `categories` | array | 字符串数组,限制分类列表;为空数组表示不限制 | | `header` | object | 当前站点请求 header,key/value 对象 | | `style` | object | 卡片样式,结构见“样式配置” | 站点 `type` 取值: | type | 名称 | 行为 | | --- | --- | --- | | `0` | XML API | 首页按 XML 解析;分类/详情请求带 `ac=videolist`;播放结果走站点 `playUrl` 或默认解析逻辑 | | `1` | JSON API | 首页按 JSON 解析;分类/详情请求带 `ac=detail`;分类筛选参数会以 `f={json}` 传给接口 | | `2` | JSON API 兼容类型 | 返回按 JSON 解析;分类/详情请求带 `ac=detail`;分类筛选不会自动追加 `f` 参数 | | `3` | Spider | 调用 Java/JAR、JS 或 Python Spider 标准方法 | | `4` | HTTP API + Base64 ext | 首页请求带 `filter=true`;分类请求带 `ext={base64(extend)}`;播放请求带 `play` 和 `flag` | `homePage` 配置后,切换到该站点主页时会加载 WebHome,而不是原生推荐页。 ## 6. 解析器配置 解析器字段示例: ```json { "name": "解析名", "type": 1, "url": "https://example.com/parse?url=", "ext": { "flag": ["qq", "iqiyi"], "header": { "User-Agent": "Mozilla/5.0" } }, "header": {}, "click": "" } ``` 字段说明: | 字段 | 说明 | | --- | --- | | `name` | 解析器名称,也是 UI 中展示和 `parse:` 前缀匹配的名称 | | `type` | 解析器类型。完整取值见下表;为空时按 `0` 处理 | | `url` | 解析地址。`type=0/1` 时通常是 HTTP 地址;`type=2/3` 时是 JAR parser key;`type=4` 由 App 内置生成 | | `ext.flag` | 字符串数组,适用播放 flag。为空数组表示不过滤 flag | | `ext.header` | 解析请求 header,key/value 对象 | | `header` | 简写 header,会合并到 `ext.header` | | `click` | WebView 点击脚本 | 解析器 `type` 取值: | type | 名称 | 行为 | | --- | --- | --- | | `0` | Web 解析 | 打开解析 WebView 嗅探真实播放地址 | | `1` | JSON 解析 | 请求 `url + webUrl`,读取返回 JSON 里的 `url` 或 `data.url` | | `2` | JAR Json 扩展解析 | 调用当前 JAR 中 `com.github.catvod.parser.Json{url}` 的 `parse(jxs, webUrl)` | | `3` | JAR Mix 扩展解析 | 调用当前 JAR 中 `com.github.catvod.parser.Mix{url}` 的 `parse(jxs, flag, parseName, webUrl)` | | `4` | 聚合解析 | App 内置“聚合”解析:并发尝试符合 flag 的 JSON 解析器和 Web 解析器 | 当解析 URL 已带 `?` 且 `ext` 非空时,App 会追加 `cat_ext={base64(ext)}`。 播放结果里的 `playUrl` 前缀: | 前缀 | 行为 | | --- | --- | | `json:{url}` | 临时使用 `type=1` JSON 解析器,解析地址为 `{url}` | | `parse:{name}` | 使用配置里名称等于 `{name}` 的解析器 | | 无前缀且非空 | 作为 `type=0` Web 解析地址 | ## 7. 直播配置 直播源支持传统文本、M3U 和 JSON。点播配置里的 `lives` 会同步生成直播配置,直播配置自身也可以独立加载。 JSON 直播源示例: ```json { "name": "直播源", "url": "https://example.com/live.m3u", "api": "csp_LiveSpider", "ext": "", "jar": "./spider.jar", "click": "", "logo": "https://example.com/logo/{name}.png", "epg": "https://example.com/epg.xml.gz", "ua": "Mozilla/5.0", "origin": "https://example.com", "referer": "https://example.com/", "timeZone": "Asia/Shanghai", "timeout": 30, "header": {}, "catchup": { "type": "append", "days": "7", "regex": "/PLTV/", "source": "?playseek=${(b)yyyyMMddHHmmss}-${(e)yyyyMMddHHmmss}", "replace": "/PLTV/,/TVOD/" }, "groups": [ { "name": "央视", "pass": "", "channel": [ { "name": "CCTV-1", "urls": ["https://example.com/live/cctv1.m3u8"], "logo": "https://example.com/logo/cctv1.png" } ] } ] } ``` 直播源类型: | 类型 | 识别方式 | 说明 | | --- | --- | --- | | M3U | 文本包含 `#EXTM3U` 且不是 `#genre#` 文本源 | 解析 `#EXTINF`、`group-title`、`tvg-*`、`catchup-*`、`#EXTVLCOPT`、`#KODIPROP` 等字段 | | TXT | 文本使用 `频道名,播放地址`,分组行包含 `#genre#` | 同一频道多地址可用 `#` 分隔;单条地址可用 `url|header参数` 携带 header | | JSON | 文本是 JSON array | 按 `groups[].channel[]` 结构解析 | | Spider 直播 | `api` 非空 | 调用当前直播 Spider 的 `liveContent(url)` 返回直播文本或 JSON | 直播根字段: | 字段 | 类型 | 说明 | | --- | --- | --- | | `name` | string | 直播源唯一名称 | | `url` | string | 直播源 URL;`api` 为空时 App 直接请求该地址 | | `api` | string | 直播 Spider 类名或脚本地址;非空时调用 `liveContent(url)` | | `ext` | string/object | 传给直播 Spider 的扩展参数 | | `jar` | string | 当前直播源独立 JAR,覆盖全局 `spider` | | `click` | string | 直播解析 WebView 点击脚本 | | `logo` | string | 频道 logo 模板或默认 logo。支持 `{id}`、`{name}`、`{logo}` 替换 | | `epg` | string | EPG 地址,支持多个地址用英文逗号分隔;XMLTV 支持 `.xml` 和 `.gz` | | `ua` | string | 直播请求 User-Agent,会合并进频道请求 header | | `origin` | string | 直播请求 Origin,会合并进频道请求 header | | `referer` | string | 直播请求 Referer,会合并进频道请求 header | | `timeZone` | string | EPG 时区字符串,缺省使用系统时区 | | `keep` | string | App 保存的最近直播频道,配置作者通常不需要手写 | | `timeout` | number | 播放超时,单位秒;为空时使用默认 15 秒,最小按 1 秒处理 | | `header` | object | 直播源级 header,会和 `ua/origin/referer` 合并 | | `catchup` | object | 回看规则,字段见下方 `catchup` 表 | | `core` | object | TVBus/特殊内核配置,字段见下方 `core` 表 | | `groups` | array | JSON 直播分组,元素结构见下方 `group` 表 | | `boot` | boolean | App 内保存的直播启动状态,配置作者通常不需要手写 | | `pass` | boolean | 分组密码处理开关;为 `true` 时分组名里的 `_密码` 不会被拆成密码 | `group` 字段: | 字段 | 类型 | 说明 | | --- | --- | --- | | `name` | string | 分组名称。TXT/M3U 分组名含 `_密码` 时,默认会拆出 `pass` | | `pass` | string | 分组密码;非空时该分组视为隐藏分组 | | `channel` | array | 频道列表,元素结构见下方 `channel` 表 | `channel` 字段: | 字段 | 类型 | 说明 | | --- | --- | --- | | `name` | string | 频道名称 | | `urls` | array | 播放地址数组;每项可以是 `url` 或 `url$线路名` | | `number` | string | 频道号;为空时 App 按顺序生成三位编号 | | `logo` | string | 频道 logo | | `epg` | string | 频道 EPG 名或地址;直播源 `epg` 模板可用 `{epg}` 替换该值 | | `ua` | string | 频道级 User-Agent | | `click` | string | 频道级 WebView 点击脚本 | | `format` | string | 媒体格式/MIME;M3U 的 `mpd`、`dash` 会转为 `application/dash+xml`,`hls` 会转为 `application/x-mpegURL` | | `origin` | string | 频道级 Origin | | `referer` | string | 频道级 Referer | | `tvgId` | string | EPG tvg-id;为空时使用 `tvgName` | | `tvgName` | string | EPG tvg-name;为空时使用频道 `name` | | `catchup` | object | 频道级回看规则,优先级高于直播源级 `catchup` | | `header` | object | 频道级 header,会和 `ua/origin/referer` 合并 | | `parse` | number | `0` 不强制 Web 解析,`1` 强制进入解析 WebView | | `drm` | object | DRM 配置,字段见下方 `drm` 表 | `catchup` 字段: | 字段 | 类型 | 说明 | | --- | --- | --- | | `type` | string | 只有 `default` 有特殊行为:直接使用 `source` 作为回看 URL;其它字符串按追加模式处理 | | `days` | string | 回看天数,当前代码保存该字段,回看 URL 生成逻辑不读取该值 | | `regex` | string | 频道 URL 匹配规则;为空时只要 `source` 非空就认为支持回看 | | `source` | string | 回看 URL 模板,支持 `${(b)格式}`、`${(e)格式}`、`${utc:}`、`${utcend:}` | | `replace` | string | 追加模式下的 URL 替换规则,格式 `正则,替换值` | `core` 字段用于 TVBus 等特殊内核: | 字段 | 类型 | 说明 | | --- | --- | --- | | `auth` | string | TVBus auth 地址;如果 `resp` 非空,App 会改用本地 `/tvbus` 返回 `resp` | | `name` | string | TVBus name,支持 URL 文本拉取 | | `pass` | string | TVBus pass,支持 URL 文本拉取 | | `broker` | string | TVBus broker 地址 | | `domain` | string | TVBus domain,支持 URL 文本拉取 | | `resp` | string | 本地 `/tvbus` 返回内容,支持 URL 文本拉取 | | `sign` | string | Hook 签名,和 `pkg` 同时存在时启用 Hook | | `pkg` | string | Hook 包名 | | `so` | string | TVBus so 地址或路径 | | `key` | string | 当前代码保留字段,运行时未读取 | | `option` | array | TVBus option 数组,元素为 `{ "key": "选项名", "values": ["值"] }` | `drm` 字段: | 字段 | 类型 | 说明 | | --- | --- | --- | | `key` | string | License URL 或 ClearKey JSON | | `type` | string | 包含 `widevine`、`playready`、`clearkey` 时分别映射对应 DRM UUID;其它值映射空 UUID | | `forceKey` | boolean | 是否强制使用默认 license URL | | `header` | object | DRM license 请求 header | 直播 `header` 会与 `ua`、`origin`、`referer` 合并后用于请求。频道级字段优先于直播源级字段。 ## 8. 样式配置 `style` 可用于站点、分类结果、单个 Vod 条目。 字段: ```json { "type": "rect", "ratio": 1.33, "land": 1, "circle": 0 } ``` 说明: | 字段 | 类型 | 取值和默认值 | 说明 | | --- | --- | --- | --- | | `type` | string | `rect`、`oval`、`list`;为空时按 `rect` | 展示类型。其它字符串不会命中特殊分支,最终按 `rect` 视图处理 | | `ratio` | number | `rect` 默认 `0.75`,`oval` 默认 `1.0`,最大 `4` | 图片宽高比例 | | `land` | number | `0` 或 `1` | 单条目快捷字段。`land=1` 会生成 `type=rect`,`ratio` 为空时使用 `1.33` | | `circle` | number | `0` 或 `1` | 单条目快捷字段。`circle=1` 会生成 `type=oval`,`ratio` 为空时使用 `1.0` | `land/circle/ratio` 是 Vod 条目上的快捷字段;如果同时提供 `style`,以 `style` 对象为准。 ## 9. Spider 开发 App 支持三类 Spider: - Java/JAR Spider。 - JavaScript Spider,运行于 QuickJS。 - Python Spider,运行于 Chaquopy。 Java/JAR Spider 标准方法: ```java void init(Context context, String extend) String homeContent(boolean filter) String homeVideoContent() String categoryContent(String tid, String pg, boolean filter, HashMap extend) String detailContent(List ids) String searchContent(String key, boolean quick) String searchContent(String key, boolean quick, String pg) String playerContent(String flag, String id, List vipFlags) String liveContent(String url) Object[] proxy(Map params) String action(String action) void destroy() ``` ### 9.1 返回结构 分类、搜索、详情返回 JSON;XML API 站点返回 RSS XML,App 会转成同等字段。 ```json { "class": [ { "type_id": "1", "type_name": "电影", "type_flag": "", "land": 0, "circle": 0, "ratio": 0 } ], "filters": { "1": [ { "key": "area", "name": "地区", "init": "全部", "value": [ { "n": "大陆", "v": "大陆" } ] } ] }, "list": [ { "vod_id": "123", "vod_name": "影片名", "vod_pic": "https://example.com/poster.jpg", "vod_remarks": "更新至 12 集" } ], "page": 1, "pagecount": 10, "total": 200 } ``` 顶层返回字段: | 字段 | 类型 | 说明 | | --- | --- | --- | | `class` | array | 分类列表,元素结构见下方 `class` 表 | | `filters` | object | 分类筛选表;key 为 `type_id`,value 为筛选数组 | | `list` | array | Vod 条目列表,元素结构见下方 `vod` 表 | | `page` | number/string | 当前页码,App 透传给 UI 或调用方 | | `pagecount` | number/string | 总页数;`Result` 内部字段名是 `pagecount` | | `total` | number/string | 总条数,App 透传给 UI 或调用方 | | `url` | string/array/object | 播放结果 URL 字段,结构见下方播放结果表 | | `header` | object | 播放或请求 header | | `msg` | string | 当 `code` 为 `0` 或为空时会作为 Toast 提示 | | `code` | number | 结果状态码;为空时按 `0` 处理 | `class` 元素字段: | 字段 | 类型 | 说明 | | --- | --- | --- | | `type_id` | string | 分类 ID;也可写别名 `id` | | `type_name` | string | 分类名称;也可写别名 `name` | | `type_flag` | string | 为 `1` 时分类按文件夹/子分类入口处理 | | `filters` | array | 当前分类内联筛选数组;结构同 `filters[type_id]` | | `land` | number | 分类卡片横图快捷样式,`1` 表示横图 | | `circle` | number | 分类卡片圆形快捷样式,`1` 表示圆形 | | `ratio` | number | 分类卡片图片宽高比 | 筛选字段: | 字段 | 类型 | 说明 | | --- | --- | --- | | `key` | string | 筛选参数名,会放入 `extend` 传给分类方法 | | `name` | string | 筛选显示名 | | `init` | string | 初始选中值 | | `value` | array | 筛选值数组,元素为 `{ "n": "显示名", "v": "提交值" }` | `vod` 元素字段: | 字段 | 类型 | 说明 | | --- | --- | --- | | `vod_id` | string | 详情 ID;XML 中对应 `` | | `vod_name` | string | 影片/剧集名称;XML 中对应 `` | | `type_name` | string | 类型名称;XML 中对应 `` | | `vod_pic` | string | 海报 URL;XML 中对应 `` | | `vod_remarks` | string | 备注/更新信息;XML 中对应 `` | | `vod_year` | string | 年份;XML 中对应 `` | | `vod_area` | string | 地区;XML 中对应 `` | | `vod_director` | string | 导演;XML 中对应 `` | | `vod_actor` | string | 演员;XML 中对应 `` | | `vod_content` | string | 简介;XML 中对应 `` | | `vod_play_from` | string | 播放线路名,多个线路用 `$$$` 分隔 | | `vod_play_url` | string | 播放列表,线路之间用 `$$$` 分隔;单线路内多集用 `#` 分隔;单集格式 `集名$播放地址` | | `vod_tag` | string | 为 `folder` 时条目作为文件夹/子分类入口 | | `action` | string | 点击条目时触发 Spider `action()` 或 HTTP API action,不进普通详情 | | `cate` | object | 子分类入口配置;存在时条目作为文件夹入口 | | `style` | object | 单条目覆盖样式,结构见“样式配置” | | `land` | number | 单条目横图快捷样式,`1` 表示横图 | | `circle` | number | 单条目圆形快捷样式,`1` 表示圆形 | | `ratio` | number | 单条目图片宽高比 | | `vodFlags` / XML `dl/dd` | array/XML | XML 播放线路结构;JSON 通常直接使用 `vod_play_from` 和 `vod_play_url` | 详情返回通常是 `{ "list": [vod] }`。搜索返回的 `vod` 会由 App 自动补上站点信息,用于聚合搜索和换源显示。 播放返回示例: ```json { "parse": 0, "jx": 0, "url": "https://example.com/video.m3u8", "playUrl": "", "header": { "Referer": "https://example.com/" }, "format": "application/x-mpegURL", "subs": [ { "url": "https://example.com/sub.srt", "name": "中文", "lang": "zh", "format": "application/x-subrip", "flag": 1 } ], "danmaku": [ { "name": "弹幕", "url": "https://example.com/danmaku.xml" } ], "drm": null, "position": 0, "artwork": "https://example.com/art.jpg", "flag": "线路名", "click": "", "msg": "" } ``` 播放结果字段: | 字段 | 类型 | 说明 | | --- | --- | --- | | `parse` | number | `0` 不强制 Web 解析,`1` 强制进入解析 WebView;为空时按 `0` 处理 | | `jx` | number | `1` 时等同需要解析;为空时按 `0` 处理 | | `url` | string/array/object | 播放地址。字符串表示单地址;数组按 `[显示名1, 地址1, 显示名2, 地址2]` 解析;对象结构为 `{ "values": [{ "n": "显示名", "v": "地址" }], "position": 0 }` | | `playUrl` | string | 播放前缀。支持 `json:{url}`、`parse:{name}` 或普通 Web 解析地址 | | `header` | object | 播放请求 header;Cookie 会被保存到 CookieStore | | `format` | string | 媒体 MIME/格式,传给播放器 MediaSource | | `subs` | array | 字幕数组,元素字段见下方 `subs` 表 | | `danmaku` | array | 弹幕数组,元素字段见下方 `danmaku` 表 | | `drm` | object/null | DRM 配置,字段见直播 `drm` 表 | | `position` | number | 起播位置,毫秒 | | `artwork` | string | 播放器封面 URL | | `flag` | string | 当前播放线路名;为空时 App 会使用调用播放器时传入的 flag | | `click` | string | 解析 WebView 点击脚本 | | `msg` | string | Toast 提示;仅 `code` 为 `0` 或为空时显示 | | `code` | number | 播放结果状态码,空值按 `0` | | `jxFrom` | string | 解析来源标记 | | `desc` | string | 播放描述文本 | | `key` | string | 播放站点 key;App 内部会在部分链路写入 | `subs` 元素字段: | 字段 | 类型 | 说明 | | --- | --- | --- | | `url` | string | 字幕 URL 或路径 | | `name` | string | 字幕名称 | | `lang` | string | 字幕语言 | | `format` | string | 字幕 MIME;为空时部分本地注入链路会按扩展名推断 | | `flag` | number | Media3 subtitle selection flag;为空时按默认字幕处理 | `danmaku` 元素字段: | 字段 | 类型 | 说明 | | --- | --- | --- | | `name` | string | 弹幕名称;为空时显示 `url` | | `url` | string | 弹幕 URL 或路径 | ### 9.2 Vod 条目特殊字段 | 字段 | 说明 | | --- | --- | | `action` | 点击条目时触发 Spider `action()` 或 HTTP API action,不进普通详情 | | `vod_tag: "folder"` | 条目作为文件夹/子分类入口 | | `cate` | 条目作为子分类入口 | | `style` | 单条目覆盖样式 | | `land` / `circle` / `ratio` | 单条目样式快捷字段 | ### 9.3 proxy 返回 Java/JAR Spider 代理返回: ```java new Object[]{200, "video/mp2t", inputStream, headers} ``` `Object[]` 含义: | 下标 | 类型 | 必填 | 说明 | | --- | --- | --- | --- | | `0` | number 或 `NanoHTTPD.Response` | 是 | HTTP 状态码;如果直接返回 `Response`,App 原样返回 | | `1` | string | 是 | Content-Type | | `2` | InputStream | 是 | 响应流 | | `3` | Map | 否 | 响应 header | JS Spider 普通模式 `proxy(params)` 返回数组: ```js [200, "text/plain; charset=utf-8", "content", { "Cache-Control": "no-store" }, 0] ``` JS/Python proxy 数组字段: | 下标 | 类型 | 说明 | | --- | --- | --- | | `0` | number | HTTP 状态码 | | `1` | string | Content-Type | | `2` | string/bytes | 响应内容。字符串默认按 UTF-8 输出 | | `3` | object | 响应 header,可为空 | | `4` | number | `1` 表示第 2 项是 Base64;内容含 `base64,` 前缀时 App 会自动截掉前缀再解码 | CatVod 兼容 JS proxy 模式下,`proxy(segments, headers)` 返回 JSON 字符串,运行时会按 `Res` 结构转换为本地代理响应。 ## 10. JS Spider 运行时 JS Spider 运行在 QuickJS。模块可以通过 `export default` 导出对象或工厂函数,也可以导出 CatVod 兼容的 `__jsEvalReturn()`。 当前运行时实际调用的导出方法: | 方法 | 参数 | 返回 | 说明 | | --- | --- | --- | --- | | `init(ext)` | `ext` | 任意 | 初始化。普通 JS 模式下 `ext` 为字符串或对象;CatVod 兼容模式下为 `{ stype: 3, skey, ext }` | | `home(filter)` | `filter: boolean` | string | 返回首页分类 JSON | | `homeVod()` | 无 | string | 返回首页推荐 JSON | | `category(tid, pg, filter, extend)` | `tid: string`,`pg: string`,`filter: boolean`,`extend: object` | string | 返回分类页 JSON | | `detail(id)` | `id: string` | string | 返回详情 JSON。注意 JS 运行时只传 `ids[0]`,不是数组 | | `search(key, quick)` | `key: string`,`quick: boolean` | string | 旧搜索签名 | | `search(key, quick, pg)` | `key: string`,`quick: boolean`,`pg: string` | string | 分页搜索签名 | | `play(flag, id, flags)` | `flag: string`,`id: string`,`flags: array` | string | 返回播放 JSON | | `live(url)` | `url: string` | string | 返回直播内容 | | `sniffer()` | 无 | boolean | 是否手动嗅探,映射 Java `manualVideoCheck()` | | `isVideo(url)` | `url: string` | boolean | 判断 URL 是否为视频格式 | | `proxy(params)` | 普通模式:`params: object`;CatVod 兼容模式:`segments: array, headers: object` | array 或 string | 本地代理回调,详见 proxy 返回 | | `action(action)` | `action: string` | string | 处理 `Vod.action` 或外部 action | | `destroy()` | 无 | 任意 | 释放资源 | JS 全局工具: | 工具 | 签名 | 返回 | 说明 | | --- | --- | --- | --- | | `req` | `req(url, options)` | object | 同步 HTTP。实际等价于 `http(url, { ...options, async: false })` | | `http` | `http(url, options)` | Promise 或 object | 默认返回 Promise;`options.async === false` 时同步返回 object | | `getPort` | `getPort()` | number | 当前本地 HTTP 服务端口 | | `getProxy` | `getProxy(local)` | string | 当前 JS Spider 代理 URL;`local=true` 返回 `127.0.0.1`,`false` 返回局域网 IP,自动带 `?do=js` | | `js2Proxy` | `js2Proxy(dynamic, siteType, siteKey, url, headers)` | string | CatVod 兼容代理 URL。`dynamic=true` 时使用局域网 IP,`false` 使用 `127.0.0.1` | | `joinUrl` | `joinUrl(parent, child)` | string | 按 URL 规则解析相对路径 | | `s2t` | `s2t(text)` | string | 简转繁 | | `t2s` | `t2s(text)` | string | 繁转简 | | `md5X` | `md5X(text)` | string | MD5 并写日志 | | `aesX` | `aesX(mode, encrypt, input, inBase64, key, iv, outBase64)` | string | AES 工具并写日志 | | `rsaX` | `rsaX(mode, pub, encrypt, input, inBase64, key, outBase64)` | string | RSA 工具并写日志 | | `setTimeout` | `setTimeout(fn, delay)` | null | QuickJS 定时执行 | | `local.get` | `local.get(rule, key)` | string | 读取 Native `Prefers` 字符串 | | `local.set` | `local.set(rule, key, value)` | void | 写入 Native `Prefers` 字符串 | | `local.delete` | `local.delete(rule, key)` | void | 删除 Native `Prefers` 字符串 | JS `req/http` options: | 字段 | 类型 | 取值和默认值 | 说明 | | --- | --- | --- | --- | | `async` | boolean | `http` 默认异步;`req` 固定 `false` | `false` 时同步返回 object,其它情况 Promise 返回 object | | `method` | string | `get`、`post`、`header`;默认 `get` | `post` 发送 POST;`header` 发送 HEAD;其它值按 GET | | `headers` | object | 默认 `{}` | 请求 headers | | `data` | object | 默认 `undefined` | POST 数据,配合 `postType` 使用 | | `body` | string | 默认 `undefined` | 原始请求体。仅当 `data` 为空且 header 含 `Content-Type` 时使用 | | `postType` | string | `json`、`form`、`form-data`;默认 `json` | `json` 发 JSON body;`form` 发 `application/x-www-form-urlencoded`;`form-data` 发 multipart 表单 | | `timeout` | number | 默认 `10000` | 毫秒 | | `redirect` | number | `1` 跟随重定向,`0` 不跟随;默认 `1` | 传给 OkHttp client | | `buffer` | number | `0`、`1`、`2`、`3`;默认 `0` | `0` 返回文本;`1` 返回 byte 数组形式的 JSArray;`2` 返回 Base64 字符串;`3` 返回原始 byte[] | JS `req/http` 返回 object: | 字段 | 类型 | 说明 | | --- | --- | --- | | `code` | number/string | HTTP 状态码;异常时为空字符串 | | `headers` | object | 响应头;同名多值为数组 | | `content` | string/array/bytes | 响应内容,由 `buffer` 决定;异常时为空字符串 | ## 11. Python Spider 运行时 Python Spider 运行在 Chaquopy。Spider 类继承 `base.spider.Spider` 时,当前运行时会调用下列方法: | 方法 | 参数 | 返回 | 说明 | | --- | --- | --- | --- | | `init(self, extend="")` | `extend: string` | 任意 | 初始化。运行时会先把 `getDependence()` 返回的依赖 py 文件下载到缓存目录,再调用本方法 | | `homeContent(self, filter)` | `filter: bool` | string | 返回首页分类 JSON | | `homeVideoContent(self)` | 无 | string | 返回首页推荐 JSON | | `categoryContent(self, tid, pg, filter, extend)` | `tid: string`,`pg: string`,`filter: bool`,`extend: dict` | string | 返回分类页 JSON | | `detailContent(self, ids)` | `ids: list` | string | 返回详情 JSON | | `searchContent(self, key, quick, pg="1")` | `key: string`,`quick: bool`,`pg: string` | string | 返回搜索 JSON | | `playerContent(self, flag, id, vipFlags)` | `flag: string`,`id: string`,`vipFlags: list` | string | 返回播放 JSON | | `liveContent(self, url)` | `url: string` | string | 返回直播内容 | | `localProxy(self, param)` | `param: dict` | list | 本地代理回调,结构见 proxy 返回 | | `isVideoFormat(self, url)` | `url: string` | bool | 判断 URL 是否为视频格式 | | `manualVideoCheck(self)` | 无 | bool | 是否手动嗅探 | | `action(self, action)` | `action: string` | string | 处理 `Vod.action` 或外部 action | | `destroy(self)` | 无 | 任意 | 释放资源 | | `getName(self)` | 无 | string | 返回 Spider 名称;基类默认 `None` | | `getDependence(self)` | 无 | list | 返回依赖 py 模块名列表,不带 `.py` 后缀 | Python 基类工具: | 工具 | 签名 | 说明 | | --- | --- | --- | | `loadSpider` | `self.loadSpider(name)` | 从缓存目录加载 `{name}.py` 并返回其中的 `Spider()` | | `loadModule` | `self.loadModule(name)` | 从缓存目录加载 `{name}.py` 模块 | | `regStr` | `self.regStr(reg, src, group=1)` | 正则提取字符串 | | `removeHtmlTags` | `self.removeHtmlTags(src)` | 移除 HTML 标签 | | `cleanText` | `self.cleanText(src)` | 移除 emoji 范围字符 | | `fetch` | `self.fetch(url, params=None, cookies=None, headers=None, timeout=5, verify=True, stream=False, allow_redirects=True)` | `requests.get` 包装,响应编码设为 UTF-8 | | `post` | `self.post(url, params=None, data=None, json=None, cookies=None, headers=None, timeout=5, verify=True, stream=False, allow_redirects=True)` | `requests.post` 包装,响应编码设为 UTF-8 | | `html` | `self.html(content)` | `lxml.etree.HTML(content)` | | `str2json` | `Spider.str2json(text)` | `json.loads(text)` | | `json2str` | `Spider.json2str(value)` | `json.dumps(value, ensure_ascii=False)` | | `getProxyUrl` | `self.getProxyUrl(local=True)` | 返回 Python Spider 代理 URL,自动带 `?do=py` | | `log` | `self.log(msg)` | 打印日志;dict/list 会转 JSON | | `getCache` | `self.getCache(key)` | 通过本地 `/cache` 读取字符串或 JSON;如果 JSON 对象含 `expiresAt` 且已过期会自动删除 | | `setCache` | `self.setCache(key, value)` | 通过本地 `/cache` 写入字符串、数字、dict 或 list | | `delCache` | `self.delCache(key)` | 通过本地 `/cache` 删除 key | ## 12. 播放和特殊协议 App 播放输入可以来自配置、Spider、WebHome、外部 Intent、本地 HTTP 推送。 支持或识别的协议/格式: | 协议/格式 | 说明 | | --- | --- | | `http://` / `https://` | 普通网络媒体 | | `rtsp://` / `rtmp://` / `smb://` | 外部打开和部分播放链路支持 | | `push://真实地址` | 让当前播放页再次打开一个新播放地址 | | `assets://path` | 读取 APK assets,经本地服务转换 | | `file://path` | 读取 App 本地路径,经本地服务转换 | | `proxy://query` | 转到 Spider 本地代理 | | `.strm` | 读取第一行作为真实播放地址 | | YouTube URL | 使用 NewPipe 提取播放地址;播放列表可展开成多集 | | `magnet:` | 迅雷内核解析 BT | | `.torrent` | 迅雷内核解析种子 | | `ed2k:` | 迅雷内核解析 | | `thunder:` | 详情解析阶段可展开或转换 | | `jianpian:` / `tvbox-xg:` / `ftp:` | 荐片/XG P2P 解析 | `UrlUtil.convert()` 会把下面伪协议转换成本地服务地址: ```text assets://path -> http://127.0.0.1:{port}/path file://path -> http://127.0.0.1:{port}/file/path proxy://query -> http://127.0.0.1:{port}/proxy?query ``` ## 13. 本地 HTTP 服务 App 启动后会启动 NanoHTTPD 服务,端口从 `9978` 到 `9998` 依次尝试,取第一个可用端口。 地址: ```text http://127.0.0.1:{port} http://{局域网IP}:{port} ``` `/device` 返回局域网地址、设备类型和时间。 ```json { "uuid": "android-id", "name": "device-name", "ip": "http://192.168.1.23:9978", "type": 1, "time": 1710000000000 } ``` 设备类型:`0` 电视端,`1` 手机端,`2` DLNA 设备记录。 ### 13.1 端点总览 | 路径 | 方法 | 能力 | | --- | --- | --- | | `/` | GET | 内置局域网管理网页 | | `/device` | GET/POST | 设备信息 | | `/media` | GET/POST | 当前播放状态 | | `/action` | GET/POST | 播放控制、搜索、推送、刷新、同步、设置、投放 | | `/cache` | GET/POST | 字符串缓存 | | `/file/{path}` | GET | 浏览或下载 App 本地文件 | | `/upload` | POST | 上传文件;zip 自动解压 | | `/newFolder` | POST | 新建文件夹 | | `/delFolder` | POST | 删除文件夹 | | `/delFile` | POST | 删除文件 | | `/parse` | GET/POST | 内置解析 HTML | | `/proxy` | GET/POST | Spider 本地代理 | | `/webResource` | GET/POST/PUT/PATCH/DELETE/HEAD/OPTIONS | WebHome 资源网关 | | `/pan/check` | POST/OPTIONS | 网盘检测 | | `/debug/logs` | GET | 调试日志网页 | | `/debug/logs.txt` | GET | 下载当前调试日志 | | `/debug/clear` | GET | 清空调试日志 | | `/debug/enable` / `/debug/disable` | GET | 开启或关闭调试日志 | | `/tvbus` | GET/POST | TVBus auth response | ### 13.2 `/action` `/action` 支持 `GET` 和 `POST`。返回固定为 `200 OK`,大部分动作异步投递到 App 内部事件总线;参数缺失时通常直接忽略。 `do` 取值: | do | 必要参数 | 可选参数 | 行为 | | --- | --- | --- | --- | | `search` | `word` | 无 | 触发 App 搜索 | | `push` | `url` | 无 | 推送播放地址 | | `setting` | `text` | `name` | 写入配置文本或配置 URL;`name` 为显示名称 | | `refresh` | `type` | `path`、`json` | 刷新指定模块或注入资源,`type` 取值见下表 | | `control` | `type` | 无 | 播放控制,`type` 取值见下表 | | `sync` | `type`、`mode` | `force`、`device`、`config`、`targets`、`configs` | 多设备同步,`type` 和 `mode` 取值见下表 | | `cast` | `config`、`device`、`history` | 无 | 投放到另一台 App | | `file` | `path` | 无 | 使用 App 本地文件:`.apk` 打开安装,`.srt/.ssa/.ass` 注入字幕,其它路径按配置加载 | 搜索、推送、设置: ```text GET/POST /action?do=search&word=关键词 GET/POST /action?do=push&url=播放地址 GET/POST /action?do=setting&text=配置内容或配置URL&name=显示名称 ``` `refresh.type` 取值: | type | 额外参数 | 行为 | | --- | --- | --- | | `home` | 无 | 刷新点播首页 | | `live` | 无 | 刷新直播 | | `detail` | 无 | 刷新当前详情页 | | `player` | 无 | 刷新当前播放器 | | `category` | 无 | 刷新当前分类页 | | `subtitle` | `path` | 注入字幕 URL 或路径 | | `danmaku` | `path` | 注入弹幕 URL 或路径 | | `vod` | `json` | 用 `Vod` JSON 更新当前播放页关联条目 | 播放控制 `control.type` 取值: | type | 行为 | | --- | --- | | `play` | 播放 | | `pause` | 暂停 | | `stop` | 停止播放服务 | | `prev` | 上一集/上一项 | | `next` | 下一集/下一项 | | `loop` | 切换循环行为 | | `replay` | 重播或刷新当前播放 | 多设备同步: ```text POST /action?do=sync&type=history&mode=0&force=false POST /action?do=sync&type=keep&mode=0&force=false POST /action?do=sync&type=backup&mode=1&force=false ``` `sync.type` 取值: | type | 说明 | | --- | --- | | `history` | 同步观看历史 | | `keep` | 同步收藏 | | `backup` | 按同步选项同步接口、Jar/脚本保存数据、WebHome 缓存、搜索记录、观看历史、收藏和设置 | `sync.mode` 取值: | mode | 说明 | | --- | --- | | `0` | 双向同步 | | `1` | 从远端同步到本机 | | `2` | 从本机发送到远端 | `sync.force` 取值: | force | 说明 | | --- | --- | | `false` | 合并同步 | | `true` | 同步前先清空本机对应数据再写入 | `backup` 请求使用表单字段: | 字段 | 类型 | 说明 | | --- | --- | --- | | `options` | JSON string | 同步选项。字段为 `config`、`spider`、`search`、`history`、`keep`、`webHome`、`settings`、`paths` | | `backup` | JSON string | `mode=1` 时由发送端携带的数据库和 SharedPreferences 备份数据 | | `device` | JSON string | `mode=2` 时由请求端携带的本机设备信息,远端会回传备份数据 | | `syncFiles` | file | 可选。`multipart/form-data` 文件字段,内容为同步目录压缩包;仅 `spider=true` 时使用 | 同步选项说明: | 选项 | 默认 | 说明 | | --- | --- | --- | | `config` | `true` | 接口订阅、点播/直播/壁纸配置和当前接口指针 | | `spider` | `true` | Jar/脚本保存数据。包含默认 SharedPreferences 中非 App 设置项,也会同步 `paths` 指定的外置存储目录 | | `search` | `true` | 搜索关键词和热词缓存 | | `history` | `true` | 观看历史 | | `keep` | `true` | 收藏 | | `webHome` | `true` | WebHome 通过 `fm.cache` 保存的数据 | | `settings` | `false` | App 通用设置、播放设置、弹幕设置、增强功能开关等 | | `paths` | `"TV\nTVBox\nTVData"` | 同步目录列表,每行一个外置存储相对路径;支持用户在“一键同步 -> 同步目录”里编辑 | 目录同步规则: | 项目 | 说明 | | --- | --- | | 默认目录 | `/sdcard/TV`、`/sdcard/TVBox`、`/sdcard/TVData` | | 自定义目录 | 在一键同步界面点击“同步目录”编辑,每行一个目录;可填写 `TVBox`、`TVData/cookie`,也可填写 `/sdcard/TVBox`,内部会转成外置存储相对路径 | | 传输格式 | 发送端把目录打成 zip,通过 `multipart/form-data` 的 `syncFiles` 字段发送;接收端解压到相同外置存储相对路径 | | 进度显示 | 发送端在压缩阶段显示已处理文件数、原始大小和压缩速度;上传阶段显示文件数量、原始大小、压缩后大小、上传百分比、已传/总量和实时速度;同步过程中可取消 | | 超时策略 | 使用长传输超时,正常持续上传/接收不会按普通短请求超时处理 | | 恢复行为 | 文件包先恢复,再恢复 `backup` 数据;恢复后会清理 Jar/spider 加载缓存并重新加载当前接口,让新 cookie/本地包尽快生效 | | 当前边界 | 当前是单 zip 流式传输,不是分片并发和断点续传;后续如要支持断点,需要增加文件清单、分片 hash、临时目录、续传协商和合并校验协议 | 很多接口 Jar 会把夸克、UC、阿里云盘、迅雷、115、天翼、123、PikPak、WebDAV 等账号 Cookie 或授权状态保存到 `/sdcard/TV`、`/sdcard/TVBox`、`/sdcard/TVData` 或用户自定义目录中,因此需要勾选“Jar/脚本保存数据”才能随一键同步迁移。 投放到另一台 App: ```text POST /action?do=cast ``` `cast` 参数包含 `config`、`device`、`history`,值均为 JSON 字符串。 ### 13.3 `/media` 返回当前播放器状态: ```json { "state": 3, "speed": 1.0, "duration": 3600000, "position": 600000, "url": "https://example.com/video.m3u8", "title": "标题", "artist": "", "artwork": "" } ``` `state`:`1` 其它,`2` ready,`3` playing,`6` buffering。 ### 13.4 `/cache` ```text GET/POST /cache?do=get&rule=命名空间&key=键 GET/POST /cache?do=set&rule=命名空间&key=键&value=值 GET/POST /cache?do=del&rule=命名空间&key=键 ``` 实际存储 key: ```text cache_ + (rule ? rule + "_" : "") + key ``` ### 13.5 `/file` 和文件管理 `/file/{path}`: - path 是 App 本地根目录下路径。 - 如果是目录,返回目录 JSON。 - 如果是文件,流式返回文件内容。 - 支持 Range、ETag、Accept-Ranges。 目录返回: ```json { "parent": "", "files": [ { "name": "subtitles", "path": "/subtitles", "time": "2026-05-22 12:00:00", "dir": 1 } ] } ``` 上传: ```text POST /upload ``` 上传 zip 会自动解压到目标目录。 ## 14. WebHome 自定义主页 WebHome 是 App 为 CSP 站点扩展的自定义网页首页能力。站点配置里声明 `homePage` 后,切换到该站点主页时优先加载网页。 示例: ```json { "key": "nostr_home", "name": "Nostr 推荐", "type": 3, "api": "csp_Builtin", "homePage": "./nostr.html" } ``` 如果配置文件是在线 URL,`./nostr.html` 会相对配置文件 URL 解析。例如配置来自: ```text https://example.com/config.json ``` 则 `./nostr.html` 解析为: ```text https://example.com/nostr.html ``` WebHome 不需要配置 `bridge: "full"`,当前默认注入完整 SDK。 ## 15. WebHome 运行环境 WebView 设置: | 能力 | 状态 | | --- | --- | | JavaScript | 开启 | | DOM Storage | 开启 | | Database | 开启 | | Cache | `LOAD_NO_CACHE` | | Mixed Content | 允许 | | Media Playback Requires User Gesture | 不强制 | | Cookie | 开启 | | Third-party Cookie | 开启 | | 背景 | Native WebView 透明 | App 注入: ```js window.fongmi window.fm ``` `window.fongmi` 是完整命名空间,`window.fm` 是短别名。 SDK 注入后会触发: ```js window.dispatchEvent(new CustomEvent("fmsdk")); ``` 注意:当前 SDK 由 Native 在页面加载完成后注入,页面最早执行的内联脚本可能先于 `window.fm` 运行。WebHome 如果有账号、配置、身份、同步状态等关键持久化数据,应在 `fmsdk` 后读写 `fm.cache`;如果检测到 `window.fongmiBridge` 已存在但 `window.fm` 尚未就绪,可以短暂等待 `fmsdk`,不要立刻把关键数据写入浏览器 fallback 存储。 App 从后台恢复 WebHome 时会触发: ```js window.dispatchEvent(new CustomEvent("fmresume", { detail: { time, pausedMs } })); ``` 页面可以监听这些事件重新读取配置、恢复状态或补偿播放记录。 ## 16. WebHome SDK 总览 短别名: ```js window.fm = { req, res, play, vod, ctrl, stat, search, openLive, openKeep, history, pan, check, cache, ui, device, site, config, back, reload }; ``` 对应完整接口: | `fm` | 完整接口 | | --- | --- | | `fm.req` | `fongmi.net.request` | | `fm.res` | `fongmi.net.resourceUrl` | | `fm.play` | `fongmi.player.playUrl` | | `fm.vod` | `fongmi.player.playVod` | | `fm.ctrl` | `fongmi.player.control` | | `fm.stat` | `fongmi.player.status` | | `fm.search` | `fongmi.app.search` | | `fm.openLive` | `fongmi.app.openLive` | | `fm.openKeep` | `fongmi.app.openKeep` | | `fm.history` | `fongmi.app.history` | | `fm.pan.check` | `fongmi.pan.check` | | `fm.pan.play` | `fongmi.pan.play` | | `fm.check` | `fongmi.pan.check` 的短别名 | | `fm.cache` | `fongmi.cache` | | `fm.ui.setToolbar` | `fongmi.ui.setToolbar` | | `fm.device` | `fongmi.device.info` | | `fm.site` | `fongmi.site.info` | | `fm.config` | `fongmi.config.info` | | `fm.back` | `fongmi.navigation.back` | | `fm.reload` | `fongmi.navigation.reload` | Native bridge 对超过 12000 字符的返回会自动分片,JS SDK 会自动拉取分片并还原。Native 同时注入 `window.fongmiClient = { mode, isLeanback }`,WebHome 可用它判断当前是手机端还是 TV 端;不要只依赖屏幕宽度或 UA。 `fm.ui.setToolbar(false)` 可请求隐藏原生 WebHome 顶部工具栏,`fm.ui.setToolbar(true)` 恢复显示。该能力主要用于 TV 端详情页等需要把空间留给剧照或内容的场景,手机端通常保持原生顶部操作区。 ## 17. WebHome 网络请求 ### 17.1 `fm.req(url, options)` 通过 Native OkHttp 发起请求,不受普通浏览器 CORS 限制。该接口适合 JS 读取 API 数据;图片、视频、字幕、CSS 背景这类 DOM 资源优先用 `fm.res()`。 完整调用: ```js const response = await fm.req("https://api.example.com/data", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ keyword: "仙逆" }), responseType: "json", timeout: 30, credentials: "include" }); if (!response.ok) throw new Error(response.error || `HTTP ${response.status}`); console.log(response.body); ``` `options` 字段: | 字段 | 类型 | 是否必填 | 取值和默认值 | 说明 | | --- | --- | --- | --- | --- | | `method` | string | 否 | 默认 `GET`;会转为大写;支持 OkHttp 可执行的方法:`GET`、`POST`、`PUT`、`PATCH`、`DELETE`、`HEAD`、`OPTIONS` | `GET` 和 `HEAD` 不带请求体,其它方法会发送 `body`,即使 `body` 为空也会发送空字符串 | | `headers` | object | 否 | 默认 `{}` | key/value 请求头。未提供 `User-Agent` 时,App 自动使用设置里的 UA 或播放器默认 UA;未提供 `Accept-Encoding` 时,App 自动加 `gzip` | | `body` | string | 否 | 默认 `""` | 请求体必须是字符串;JSON 请求需自行 `JSON.stringify()` | | `responseType` | string | 否 | `text`、`json`、`base64`;默认 `text` | `json` 会把响应文本解析成 JSON;`base64` 返回响应原始字节的 Base64 字符串;其它值按 `text` 处理 | | `timeout` | number | 否 | 默认 `30`,最小 `1` | 单位秒,同时用于 connect/read/write timeout | | `credentials` | string | 否 | 只有 `include` 有特殊行为 | `include` 且未手写 `Cookie` header 时,从 WebView CookieManager 读取目标 URL Cookie 并加入请求 | 返回字段: | 字段 | 类型 | 说明 | | --- | --- | --- | | `ok` | boolean | HTTP 状态码是否为 2xx | | `status` | number | HTTP 状态码;Native 请求异常时为 `500` | | `url` | string | 最终请求 URL,可能是重定向后的 URL | | `headers` | object | 响应头;同名多值响应头会以数组返回,单值响应头以字符串返回 | | `cookies` | array | 响应里的全部 `Set-Cookie` 值 | | `body` | any | 由 `responseType` 决定:`text` 为字符串,`json` 为对象/数组/值,`base64` 为字符串 | | `error` | string | 仅请求异常时返回,内容为异常消息 | 返回示例: ```json { "ok": true, "status": 200, "url": "https://api.example.com/data", "headers": { "Content-Type": "application/json" }, "cookies": [], "body": {} } ``` 编码和 Cookie 行为: - 自动处理 `gzip` 和 `deflate`;`br` 当前不支持,服务端返回 `Content-Encoding: br` 会进入异常返回。 - 响应 `Set-Cookie` 会写入 WebView CookieManager,并 `flush()` 持久化。 - 手写 `Cookie` header 时,`credentials: "include"` 不会覆盖开发者传入的 Cookie。 - `fm.req()` 使用 WebHome 专用 OkHttpClient,但会接入 App 统一 DNS 和代理选择器:配置层 `hosts`、`doh`、`proxy` 以及增强功能里的壳代理规则都会生效。 - 配置层 `headers` 不会自动注入 `fm.req()`;需要额外 Header 时必须在 `options.headers` 里显式传入。 - `/webResource` 与 `fm.res()` 同样会走统一网络策略和壳代理选择器。 ### 17.2 `fm.res(url, options)` 生成本地 `/webResource` 地址,适合给 `img.src`、`video.src`、字幕 URL、CSS 背景图等 DOM 资源使用。`fm.res()` 返回字符串,不是 Promise。 完整调用: ```js img.src = fm.res("https://image.tmdb.org/t/p/w500/xxx.jpg"); video.src = fm.res(videoUrl, { headers: { Referer: "https://example.com/" }, credentials: "include" }); ``` `options` 字段: | 字段 | 类型 | 是否必填 | 取值和默认值 | 说明 | | --- | --- | --- | --- | --- | | `headers` | object | 否 | 默认 `{}` | 会序列化到 `/webResource?headers=...`,由 Native 代发资源请求。未提供 `User-Agent` 时自动补默认 UA | | `credentials` | string | 否 | 只有 `include` 有特殊行为 | `include` 且未手写 `Cookie` header 时,从 WebView CookieManager 读取目标 URL Cookie 并加入资源请求 | `fm.res()` 生成的 URL 形态: ```text http://127.0.0.1:{port}/webResource?url={encodedUrl}&headers={encodedJson}&credentials=include ``` `/webResource` 支持的请求方法: | 方法 | 说明 | | --- | --- | | `GET` | DOM 资源默认读取方式 | | `HEAD` | 透传为上游 `HEAD` | | `POST` | 透传请求体,body 来自 `body` 参数或 POST data | | `PUT` | 透传请求体 | | `PATCH` | 透传请求体 | | `DELETE` | 透传请求体 | | `OPTIONS` | 本地 CORS 预检,直接返回 204 | 资源网关行为: - 只允许目标 URL 为 `http://` 或 `https://`。 - 会透传浏览器请求中的 `Range`,适合视频、音频、字幕、大图分段读取。 - 使用项目统一 OkHttp,会受配置层网络策略影响。 - 会复制上游响应头,但会移除 `Connection`、`Transfer-Encoding`、`Keep-Alive`、`Content-Length`。 - 会给本地响应加 CORS header:`Access-Control-Allow-Origin`、`Access-Control-Allow-Credentials`、`Access-Control-Allow-Methods`、`Access-Control-Allow-Headers`、`Access-Control-Expose-Headers`。 `fm.req` 和 `fm.res` 区别: | 能力 | 适用 | | --- | --- | | `fm.req` | JS 读取 API 数据,返回 Promise 和结构化结果 | | `fm.res` | DOM 元素加载资源,返回可直接赋给 `src` 或 CSS 的本地 URL | 普通 `fetch()` 仍会受浏览器 CORS 限制。WebHome 需要跨域时应使用 `fm.req` 或 `fm.res`。 ## 18. WebHome 播放能力 ### 18.1 播放直链 `fm.play(url, title, options)` ```js await fm.play("https://example.com/video.m3u8", "标题", { headers: { Referer: "https://example.com/" }, credentials: "include" }); ``` 参数: | 参数 | 类型 | 是否必填 | 说明 | | --- | --- | --- | --- | | `url` | string | 是 | 播放地址。可以是普通媒体 URL,也可以是 App 可识别的特殊协议地址 | | `title` | string | 否 | 播放页标题;为空时使用最终播放 URL | | `options.headers` | object | 否 | 播放请求头;传入后 SDK 会把播放 URL 转成 `/webResource` 本地网关地址 | | `options.credentials` | string | 否 | 传 `include` 时 SDK 会把播放 URL 转成 `/webResource` 本地网关地址,并让网关自动带 Cookie | 行为: - 内部调用 `VideoActivity.start(activity, SiteApi.PUSH, playUrl, title)`。 - 如果传了 `headers` 或 `credentials: "include"`,SDK 自动调用 `fm.res()` 生成本地资源地址再播放。 - 返回值为 `{}`;播放是否最终成功取决于播放器和后续解析链路。 ### 18.2 播放 CSP 影片 `fm.vod(siteKey, vodId, title, pic, options)` ```js await fm.vod("site_key", "vod_id", "影片名", "https://example.com/poster.jpg"); ``` 参数: | 参数 | 类型 | 是否必填 | 说明 | | --- | --- | --- | --- | | `siteKey` | string | 是 | 配置里 `sites[].key` | | `vodId` | string | 是 | 目标站点详情 ID,即 Spider/API 的 `vod_id` | | `title` | string | 否 | 播放/详情标题 | | `pic` | string | 否 | 海报 URL | | `options` | object | 否 | 当前 Native 接收但未使用,保留给后续扩展 | 行为: - 内部调用 `VideoActivity.start(activity, siteKey, vodId, title, pic)`。 - 进入原生详情/播放链路,后续由对应站点 Spider/API 解析详情和播放地址。 ### 18.3 播放控制 `fm.ctrl(action)` ```js await fm.ctrl("play"); await fm.ctrl("pause"); await fm.ctrl("stop"); await fm.ctrl("prev"); await fm.ctrl("next"); await fm.ctrl("loop"); await fm.ctrl("replay"); ``` `action` 取值: | action | 行为 | | --- | --- | | `play` | 当前播放器执行播放 | | `pause` | 当前播放器执行暂停 | | `stop` | 停止播放服务 | | `prev` | 切换上一集/上一项 | | `next` | 切换下一集/下一项 | | `loop` | 切换循环/单集循环行为,具体表现跟当前播放页状态一致 | | `replay` | 重播或刷新当前播放,具体表现跟播放页“重播/刷新”按钮一致 | 如果当前没有 PlaybackService,接口直接返回 `{}`,不会抛错。 ### 18.4 播放状态 `fm.stat()` ```js const status = await fm.stat(); ``` 返回字段: | 字段 | 类型 | 说明 | | --- | --- | --- | | `state` | number | 播放状态枚举,见下表 | | `speed` | number | 当前播放倍速 | | `duration` | number | 总时长,毫秒;未知时可能为负值或 0 | | `position` | number | 当前播放位置,毫秒;未知时可能为负值或 0 | | `url` | string | 当前播放器实际 URL | | `title` | string | 当前媒体标题 | | `artist` | string | 当前媒体 artist 元数据 | | `artwork` | string | 当前媒体封面 URL | `state` 取值: | state | 说明 | | --- | --- | | `1` | 其它状态:未初始化、空闲、结束、未知 | | `2` | ready,播放器已准备好但不一定正在播放 | | `3` | playing,正在播放 | | `6` | buffering,缓冲中 | 没有播放服务时,`fm.stat()` 返回 `{}`。 ## 19. WebHome App 能力 ### 19.1 搜索 `fm.search(keyword, options)` ```js await fm.search("仙逆", { direct: true }); ``` 参数: | 参数 | 类型 | 是否必填 | 取值和默认值 | 说明 | | --- | --- | --- | --- | --- | | `keyword` | string | 是 | 非空字符串 | 搜索关键词 | | `options.direct` | boolean | 否 | 默认 `false` | `true` 时调用 `SearchActivity.direct()`,尽量直接进入搜索结果列表,减少 WebHome 到原生搜索页之间的返回层级;`false` 时调用普通搜索入口 | 返回值为 `{}`。搜索页是否有结果由当前配置站点决定。 ### 19.2 收藏和直播入口 ```js await fm.openKeep(); await fm.openLive(); ``` 行为: | 接口 | 行为 | | --- | --- | | `fm.openKeep()` | 打开原生收藏页 | | `fm.openLive()` | 打开原生直播页 | 返回值均为 `{}`。 ### 19.3 最近观看 `fm.history()` ```js const history = await fm.history(); ``` 返回当前点播配置下 60 天内的最近观看列表。该接口可用于 WebHome 从原生播放页返回后补偿识别播放进度。 返回数组元素字段: | 字段 | 类型 | 说明 | | --- | --- | --- | | `key` | string | 历史唯一键,内部由 `siteKey` 和 `vodId` 组合生成 | | `vodPic` | string | 海报 URL | | `vodName` | string | 影片/剧集名称 | | `vodFlag` | string | 当前线路 flag | | `vodRemarks` | string | 当前集/备注 | | `episodeUrl` | string | 当前集播放地址或 episode 标识 | | `revSort` | boolean | 是否倒序显示选集 | | `revPlay` | boolean | 是否倒序播放 | | `createTime` | number | 最近更新时间,毫秒时间戳 | | `opening` | number | 片头跳过点,毫秒;未设置时为负值 | | `ending` | number | 片尾跳过点,毫秒;未设置时为负值 | | `position` | number | 最近播放位置,毫秒;未知时为负值 | | `duration` | number | 总时长,毫秒;未知时为负值 | | `speed` | number | 历史保存的播放倍速 | | `scale` | number | 历史保存的画面比例索引;未设置时为 `-1` | | `cid` | number | 所属点播配置 ID | ### 19.4 返回和刷新 ```js await fm.back(); await fm.reload(); ``` 行为: | 接口 | 返回值 | 行为 | | --- | --- | --- | | `fm.back()` | `{}` | App 侧执行 WebHome 返回:WebView 有 history 时 `goBack()`,否则交给原生返回逻辑 | | `fm.reload()` | `{}` | 清 WebView 缓存,并给当前 URL 追加 `_fm_reload={timestamp}` 后重新加载 | 移动端 WebHome 可见时,顶部刷新按钮也会触发 WebHome reload。 ## 20. WebHome 信息和缓存 ### 20.1 设备信息 `fm.device()` ```js const device = await fm.device(); ``` 返回 `/device` 结果: | 字段 | 类型 | 说明 | | --- | --- | --- | | `uuid` | string | Android ID 或 DLNA UDN | | `name` | string | 设备名称 | | `ip` | string | 本机 HTTP 服务地址,包含协议、局域网 IP 和端口 | | `type` | number | 设备类型枚举,见下表 | | `serial` | string | 设备序列号,可能为空 | | `eth` | string | 有线网卡 MAC,可能为空 | | `wlan` | string | 无线网卡 MAC,可能为空 | | `time` | number | 当前时间戳,毫秒 | `type` 取值: | type | 说明 | | --- | --- | | `0` | 电视端/Leanback | | `1` | 手机端/Mobile | | `2` | DLNA 设备记录 | ### 20.2 当前站点 `fm.site()` ```js const site = await fm.site(); ``` 返回字段: | 字段 | 类型 | 说明 | | --- | --- | --- | | `key` | string | 当前站点 `sites[].key` | | `name` | string | 当前站点展示名 | | `homePage` | string | 当前 WebHome URL 或配置里的首页地址 | | `type` | number | 当前站点类型;`0` XML,`1` JSON,`2` JSON API 兼容类型,`3` Spider,`4` HTTP API + Base64 ext | | `header` | object | 当前站点请求 header | ### 20.3 当前配置 `fm.config()` ```js const config = await fm.config(); ``` 返回字段: | 字段 | 类型 | 说明 | | --- | --- | --- | | `id` | number | 当前点播配置 ID | | `url` | string | 当前点播配置 URL 或本地配置标识 | | `desc` | string | 当前点播配置显示描述;优先配置名,其次 URL | | `driveCheck` | boolean | App“增强功能”页里的“网盘检测”开关状态,默认 `true` | WebHome 做网盘检测前必须先读取 `driveCheck`,关闭时不要提交检测任务。 ### 20.4 缓存 `fm.cache` ```js await fm.cache.set("key", "value", "rule"); const value = await fm.cache.get("key", "rule"); await fm.cache.del("key", "rule"); ``` 接口: | 接口 | 参数 | 返回值 | 说明 | | --- | --- | --- | --- | | `fm.cache.get(key, rule)` | `key: string`,`rule?: string` | string | 读取字符串;不存在时返回空字符串 | | `fm.cache.set(key, value, rule)` | `key: string`,`value: string`,`rule?: string` | `{}` | 写入字符串 | | `fm.cache.del(key, rule)` | `key: string`,`rule?: string` | `{}` | 删除字符串 | 实际 Native 存储 key: ```text cache_ + (rule ? rule + "_" : "") + key ``` 注意: - 只存字符串;对象需要自行 `JSON.stringify()` 和 `JSON.parse()`。 - `fm.cache` 走 App Native `Prefers`,不依赖网页 origin,适合保存 WebHome 配置、账号、同步身份、UI 偏好等需要跨 App 重启保留的数据。 - 普通 `localStorage` 已启用,仍可保存临时或浏览器预览数据,但它按 WebView origin 隔离,并且和 `fm.cache` 不是同一个存储源。 - 开发 WebHome 时建议封装统一存储层:App 内等待 `fmsdk` 后使用 `fm.cache`,电脑浏览器预览时再 fallback 到 `localStorage`。 ## 21. 网盘能力 ### 21.1 网盘检测 `fm.pan.check(items)` `fm.pan.check(items)` 调用 App 内置网盘分享链接有效性检测。`fm.check(items)` 是短别名。该能力受“增强功能 -> 网盘检测”开关控制,开关默认开启。 完整调用: ```js const config = await fm.config(); if (config.driveCheck) { const result = await fm.pan.check([ { type: "quark", url: "https://pan.quark.cn/s/xxxx", password: "" } ]); } ``` 等价完整接口: ```js await fongmi.pan.check(items); ``` 请求项字段: | 字段 | 类型 | 是否必填 | 说明 | | --- | --- | --- | --- | | `type` | string | 是 | 网盘类型。必须使用下方“检测支持的 type”表里的主值或别名 | | `url` | string | 是 | 网盘分享链接。App 会 trim 并做基础规范化 | | `password` | string | 否 | 提取码/访问码。`baidu`、`quark`、`uc` 会在规范化 URL 时补成 `pwd=` 查询参数;其它网盘按检测逻辑使用 | 检测支持的 `type` 主值: | type | 网盘 | 当前检测方式 | 支持的链接特征 | | --- | --- | --- | --- | | `aliyun` | 阿里云盘/阿里云盘分享 | 匿名分享信息接口 | 链接路径最后一段作为 `share_id` | | `quark` | 夸克网盘 | sharepage token + detail 接口 | `pan.quark.cn/s/{id}`,提取码可来自 `pwd` 或 `password` | | `uc` | UC 网盘 | 页面文本探测 | UC 分享页 URL,提取码可来自 `pwd` 或 `password` | | `baidu` | 百度网盘 | share verify + share list 接口 | `/s/{shareId}` 或 `/share/init?surl={id}`,提取码可来自 `pwd` 或 `password` | | `tianyi` | 天翼云盘 | `getShareInfoByCodeV2` 接口 | `cloud.189.cn/t/{code}` 或 URL 查询参数 `code`,也识别文本里的 `(访问码:xxxx)` | | `123` | 123 云盘 | `api/share/info` 接口 | `123pan.com/s/{shareKey}`、`123pan.cn/s/{shareKey}`、`123684/123685/123912/123592/123865.com/s/{shareKey}` | | `xunlei` | 迅雷云盘 | 迅雷分享接口 + captcha token | `pan.xunlei.com/s/{shareId}`,提取码来自 `pwd` 或 `password` | | `115` | 115 网盘 | `115cdn.com/webapi/share/snap` 接口 | 分享路径最后一段作为 `share_code`;必须提供 `password` 或 URL 查询参数 `password` | | `mobile` | 中国移动云盘/和彩云 | 移动云分享接口,内置加解密 | `yun.139.com/shareweb/#/w/i/{id}`、`caiyun.139.com/w/i/{id}`、`caiyun.139.com/m/i?...`、`caiyun.feixin.10086.cn/{id}` | 检测 `type` 别名: | 别名 | 等价主值 | | --- | --- | | `ali` | `aliyun` | | `alipan` | `aliyun` | | `123pan` | `123` | | `139` | `mobile` | | `caiyun` | `mobile` | 不在上表内的 `type` 会返回 `unsupported`,不会执行平台检测请求。磁力、电驴、`thunder`、`jianpian` 不是网盘有效性检测类型,不要提交给 `fm.pan.check()`。 返回结构: ```json { "results": [ { "type": "quark", "url": "https://pan.quark.cn/s/xxxx", "normalized_url": "https://pan.quark.cn/s/xxxx", "state": "ok", "cache_hit": false, "checked_at": 1710000000000, "expires_at": 1710003600000, "summary": "链接有效" } ] } ``` 结果字段: | 字段 | 类型 | 说明 | | --- | --- | --- | | `results` | array | 与请求 `items` 顺序一一对应 | | `type` | string | 规范化后的网盘类型主值;非法或空值可能为空字符串 | | `url` | string | 原始请求 URL trim 后的值 | | `normalized_url` | string | App 规范化后的 URL,用于缓存 key 和去重 | | `state` | string | 检测状态枚举,见下表 | | `cache_hit` | boolean | 是否命中缓存 | | `checked_at` | number | 检测时间,毫秒时间戳 | | `expires_at` | number | 缓存过期时间,毫秒时间戳 | | `summary` | string | 面向用户或调试的简短说明 | `state` 取值: | state | 说明 | UI 建议 | | --- | --- | --- | | `ok` | 链接有效 | 可上浮,绿色圆点 | | `bad` | 链接失效、为空、被取消、过期、违规不可用 | 下沉,红色圆点 | | `locked` | 需要提取码、访问码缺失或错误 | 可保留在有效结果附近,黄色圆点 | | `unsupported` | 当前 App 不支持该 `type` 检测 | 不检测或灰色圆点 | | `uncertain` | 风控、请求失败、响应异常、平台接口变化或无法确认 | 保持原顺序,灰色/蓝灰圆点 | 并发、缓存和错误行为: - 一次可提交多条。 - App 内部每批最多 10 条并发检测。 - 超过 10 条会自动拆成多批顺序执行。 - 返回顺序与请求顺序一致。 - 同一进程内同链接并发去重;相同 `type + normalized_url` 同时检测时只跑一次平台请求。 - 缓存最多保留 300 条。 - `ok` 缓存 1 小时。 - `unsupported` 缓存 24 小时。 - `bad` 缓存 6 小时。 - `locked` 缓存 12 小时。 - `uncertain` 缓存 30 分钟。 - 网络异常不写缓存。 - `items` 为空时 SDK reject,HTTP API 返回 400。 - “增强功能 -> 网盘检测”关闭时 SDK reject,HTTP API 返回 403。 本地 HTTP API: ```http POST http://127.0.0.1:{port}/pan/check Content-Type: application/json { "items": [ { "type": "quark", "url": "https://pan.quark.cn/s/xxxx", "password": "" } ] } ``` HTTP API 只支持 `POST` 和 `OPTIONS`。`OPTIONS` 用于 CORS 预检,返回 204。错误响应: ```json { "code": 403, "message": "网盘检测未开启" } ``` WebHome 列表最佳实践: - 先渲染搜索结果,再异步检测,不要阻塞首屏。 - 只检测上表支持的网盘类型,不要把磁力、电驴、光鸭、普通网页链接提交给检测接口。 - 使用 `IntersectionObserver` 只检测可见范围。 - 每次提交给 App 的 `items` 建议不超过 10 条;超过也能处理,但会被内部拆批。 - 检测状态用轻量圆点展示,不要用大段文字挤占结果列表。 - 有效和需要提取码优先,失效下沉,同状态保持原始顺序。 - `config.driveCheck === false` 时不要调用 `fm.pan.check()`,避免用户关闭检测后仍出现请求和列表跳动。 ### 21.2 网盘/推送播放 `fm.pan.play(payload)` `fm.pan.play({ type, url, password, title })` 是 WebHome 的网盘播放语义入口。当前实现不做 App 自研网盘目录枚举或直链解析,而是统一进入 App 已有的 `SiteApi.PUSH` / `push_agent` / `pvideo` 链路。因此它可以承接网盘分享链接,也可以承接磁力、电驴、`thunder`、`jianpian` 等需要推送解析的地址。 参数: | 字段 | 类型 | 是否必填 | 取值和默认值 | 说明 | | --- | --- | --- | --- | --- | | `url` | string | 是 | 非空字符串 | 网盘分享链接、磁力、电驴、`thunder`、`jianpian`、普通播放地址。可传原始链接,也可传 `push://真实地址`;App 会自动去掉开头的 `push://` | | `type` | string | 否 | 推荐使用下表主值;默认 `""` | 只用于日志和调用语义,不参与路由选择,不限制播放能力 | | `password` | string | 否 | 默认 `""` | 当前 Native `pan.play` 不读取该字段;底层 `push_agent`、JAR 或 pvideo 如果需要提取码,需要从 URL 或自身逻辑中处理 | | `title` | string | 否 | 默认使用 `url` | 原生播放页标题 | 推荐 `type` 主值: | type | 场景 | | --- | --- | | `aliyun` | 阿里云盘分享 | | `quark` | 夸克网盘分享 | | `uc` | UC 网盘分享 | | `baidu` | 百度网盘分享 | | `tianyi` | 天翼云盘分享 | | `123` | 123 云盘分享 | | `xunlei` | 迅雷云盘分享 | | `115` | 115 网盘分享 | | `mobile` | 中国移动云盘/和彩云分享 | | `magnet` | `magnet:?xt=...` | | `ed2k` | `ed2k://...` | | `thunder` | `thunder://...` | | `jianpian` | `jianpian:` 或 `tvbox-xg:` 相关地址 | | `http` | 普通 `http://` 或 `https://` 播放/分享地址且无法归类到上面类型 | 示例: ```js await fm.pan.play({ type: "quark", url: "https://pan.quark.cn/s/xxxx", password: "", title: "影片名" }); await fm.pan.play({ type: "magnet", url: "magnet:?xt=urn:btih:...", title: "磁力资源" }); ``` 行为: - 内部路由到 `SiteApi.PUSH`,也就是 `push_agent`。 - 后续播放解析交给 App 既有 `Source.get().fetch(result)`、JAR/pvideo、WebView 嗅探和播放器链路。 - 和直接使用 `push://` 的性能基本一致,额外开销只有一次 WebHome bridge 方法分发。 - `pan.play` 不受“网盘检测”开关影响,因为它是用户主动点击播放行为。 - 如果对应链接最终无法被 JAR/pvideo/WebView 嗅探解析,仍可能播放失败;WebHome 应保留错误反馈或备用打开方式。 推荐 WebHome 统一使用 `fm.pan.play()` 播放盘搜结果、磁力、电驴、`thunder`、`jianpian` 等需要进入 push 解析链路的地址,不再自行拼 `push://`。这样 API 语义更清晰,后续 App 内部如果调整播放策略,HTML 侧无需改动。 ## 22. WebHome 透明背景 Native WebView 已支持透明背景。WebHome 可以让 App 壁纸透出,但 HTML 侧必须把“页面透明”和“内容可读”分开处理:页面底层透明,卡片和控件使用半透明背景承载文字。 ### 22.1 基础页面背景 App 环境下不要给 `html`、`body`、主页容器写死不透明背景。浏览器调试环境可以保留兜底背景,避免直接打开 HTML 时一片透明。 推荐 CSS: ```css html, body { margin: 0; min-height: 100%; background: transparent; } html:not(.fm-native), html:not(.fm-native) body { background: #303840; } .app { min-height: 100vh; background: transparent; } ``` App 环境识别可以在页面初始化时加类: ```js if (window.fongmiBridge || window.fm || window.fongmi) { document.documentElement.classList.add("fm-native"); } ``` ### 22.2 半透明内容层 不要让文字直接压在复杂壁纸上。卡片、按钮、输入框、Tab、状态面板应使用统一的半透明面板色。 推荐抽成 CSS 变量,避免每个模块各写一套近似颜色: ```css :root { --panel-rgb: 76, 88, 98; --panel: rgba(var(--panel-rgb), .58); --panel-soft: rgba(var(--panel-rgb), .44); --panel-strong: rgba(var(--panel-rgb), .82); --line: rgba(255, 255, 255, .2); } .card { background: var(--panel-soft); border: 1px solid var(--line); } .card-body, .episode-card, .panel { background: var(--panel); border: 1px solid var(--line); } ``` 透明背景不是越透明越好。影视卡片文字区、分集剧情、搜索建议、状态面板这类承载文字的区域,要保证在亮壁纸和复杂壁纸上都能读清。 ### 22.3 透明浮层的层级隔离 全屏浮层如果直接设为 `background: transparent`,底下的页面内容会一起透出来,容易出现主页卡片、详情页、弹层三层内容叠在一起的情况。 正确做法是:透明浮层打开时,临时隐藏它下面的 WebHome 页面层,只让 App 壁纸透出。 详情页透明时隐藏主页: ```css body.detail-active .app { visibility: hidden; pointer-events: none; } .sheet { position: fixed; inset: 0; background: transparent; backdrop-filter: none; } ``` ```js function openDetail() { document.body.classList.add("detail-active"); detailSheet.classList.add("active"); } function closeDetail() { document.body.classList.remove("detail-active"); detailSheet.classList.remove("active"); } ``` 单集剧情这类从详情页继续打开的二级透明浮层,需要再隐藏详情页本体: ```css body.episode-active #detailSheet { visibility: hidden; pointer-events: none; } .image-viewer.episode-mode { background: transparent; backdrop-filter: none; } ``` ```js function openEpisodeView() { document.body.classList.add("episode-active"); imageViewer.classList.add("episode-mode", "active"); } function closeEpisodeView() { document.body.classList.remove("episode-active"); imageViewer.classList.remove("episode-mode", "active"); } ``` 普通图片预览不一定要透明。图片预览通常可以保留深色半透明背景和轻微模糊,让图片更沉浸;剧情文本页则更适合透明背景加半透明内容面板。 ### 22.4 实现建议 - 页面级背景透明。 - 非 App 浏览器环境提供兜底背景。 - 卡片、按钮、输入框、Tab 使用统一的中性半透明背景变量。 - 不要让文字直接压在复杂壁纸上。 - 焦点样式要柔和,不要使用突兀的高饱和边框。 - 透明全屏浮层打开时,隐藏底层 WebHome 页面,避免界面叠加。 - 透明浮层关闭时必须清理 `body` 状态类,避免底层页面无法恢复。 - 图片、海报、剧照本身不要叠加灰色遮罩,除非明确需要压暗文字背景。 - 图片预览可以保留沉浸式深色背景;剧情、详情这类信息页可以透明背景加半透明内容面板。 ### 22.5 电视端遥控器 UX 最佳实践 电视端 WebHome 不能只依赖浏览器默认 Tab 顺序或普通点击事件。遥控器实际是“当前焦点 + 上下左右 + OK + 返回”的交互模型,页面需要显式维护焦点域、返回语义和输入态。 核心原则: - 默认焦点不要放在搜索框、账号输入框等文本输入控件上。首页冷启动建议聚焦在第一个内容 Tab 或第一个可浏览内容,例如“推荐”入口,避免一打开就弹输入法。 - 所有可人工操作元素都要是稳定的可聚焦目标,例如按钮、卡片、Tab、结果项、开关、checkbox、关闭按钮、返回按钮。固定尺寸控件应有稳定的宽高、间距和 `data-*` key,动态内容变化时不要让布局跳动。 - 浮层、下拉框、状态面板、搜索建议、网盘结果列表等打开后,应把方向键限制在当前焦点域内。不能让全局“找最近元素”的逻辑穿透到下面的卡片。 - 进入局部焦点域后,返回键应先执行局部返回:建议下拉框关闭并回到搜索框,网盘结果列表返回网盘类型 Tab,状态面板关闭并回到状态按钮,剧情滚动页先滚到顶部或回到局部返回按钮;只有局部状态处理完,才交给 WebView history 或 App 返回。 - 对于打开后会覆盖后续内容的区域,应临时禁用下面区域的焦点。可以给下方区域设置 `aria-hidden="true"`,并把内部 `.focusable`、`button`、`input`、`textarea` 的 `tabindex` 改为 `-1`,关闭后恢复原值。 - 文本输入框在电视端应区分“焦点态”和“编辑态”。默认 `readonly`,允许遥控器聚焦但不弹输入法;用户按 OK、鼠标点击或触摸点击时才解除 `readonly` 进入编辑态;失焦或返回键退出编辑态时恢复 `readonly`。 - checkbox、开关和网盘类型这类二态控件应支持 OK 直接切换。部分 Android WebView 对合成 `MouseEvent("click")` 兼容不稳定,遥控器 OK 处理里可以优先调用元素原生 `.click()`,并同步标记 dirty 状态。 - 搜索建议只应由真实输入触发,不要在搜索框重新获得焦点时自动弹出上一次建议。异步建议请求应带请求序号或 abort 逻辑,用户关闭建议后,旧请求返回不能重新渲染下拉框。 - 动态列表刷新要保存焦点 key 和滚动位置。列表项使用稳定 `data-key`,渲染时先判断 keys 是否真的变化;未变化时不要整列表替换,变化时也要在 `requestAnimationFrame` 后恢复原焦点或合理 fallback。 - 盘搜、搜索结果等异步区域不要每次刷新先清空再重建,否则电视端会出现闪烁、焦点丢失和页面滚到顶部。应保留旧结果直到新结果可渲染,或给区域稳定 `min-height` 和局部 loading 状态。 - 焦点移动时使用 `focus({ preventScroll: true })`,然后只把当前区域滚到可视范围。列表内部移动优先滚动列表容器,不要让整页回到顶部。 - 当焦点元素被重渲染移除、WebView 恢复或系统吞掉焦点时,应有焦点兜底:如果当前 `document.activeElement` 不是可见可操作元素,下一次方向键先恢复到当前弹层按钮、当前 Tab、当前列表项或首页默认焦点。 - 焦点样式要能清楚看出当前项,但不要用突兀的粗白描边或强阴影。电视端更适合轻微背景提亮、柔和边框、轻微位移或缩放;结果列表这种密集区域可以用内描边和背景变化,避免最左侧加粗导致排版抖动。 - 从原生播放页返回 WebHome 时,若用户是从某个网盘结果或搜索结果点播,应恢复到原列表、原分类、原结果项和原滚动位置,方便用户继续切换下一个链接。正常冷启动则仍应回到主页。 实现上建议封装这些基础函数: - `isVisibleFocusable(el)`:统一判断元素是否可见、未禁用、没有 `tabindex=-1`,且不在 `aria-hidden=true` 的祖先内。 - `nearestFocusable(key, scope)`:只在当前焦点域内按几何位置寻找下一个焦点,不要跨弹层、跨下拉框、跨隐藏区域。 - `focusRemoteTarget(el)`:统一 `preventScroll` 聚焦和可视区域修正。 - `trapDirectionalKey(scope, key, event)`:当前弹层或列表打开时优先拦截方向键。 - `handleLocalBack(event)`:按当前局部状态处理返回键,处理成功后 `preventDefault()` 和 `stopImmediatePropagation()`。 这些规则比单纯给元素加 `tabindex` 更重要。电视端体验问题通常不是“某个按钮不能点”,而是焦点域没有边界、动态渲染替换了当前焦点、输入控件把遥控焦点误当成编辑焦点、返回键没有局部语义。 ### 22.6 WebHome 性能最佳实践 电视端 WebView 的性能目标不是单纯追求首屏跑分,而是让遥控器每次按键都能在一帧内完成焦点计算、样式更新、必要滚动和少量 DOM 追加。方向键、滚动、搜索建议、网盘检测和 relay 同步都属于高频路径,必须按低成本路径设计。 遥控器和焦点性能: - 规则区域优先使用确定性导航。海报 grid、横向 rail、Tab、单列结果列表都可以通过当前索引、列数和相邻节点直接算目标;只有复杂或不规则区域才退回几何距离搜索。 - 首页 grid 卡片建议写入稳定 `data-card-index`,方向键下移用 `index + columns`,左右用 `index +/- 1`,上移用 `index - columns`。不要在每次按键时全局 `querySelectorAll()` 后逐个读 `getBoundingClientRect()`。 - `gridTemplateColumns` 这类样式读取要缓存。缓存 key 至少包含视口宽度和 TV/Web 模式,避免每次方向键都触发布局计算。 - `focus()` 后的滚动修正放进 `requestAnimationFrame`,同一帧多次焦点变更只处理最后一个目标。长按方向键时不要同步连续 `scrollIntoView()`。 - 高频 keydown 中尽量只读当前区域的相邻节点、已缓存索引和已缓存列数。必须调用 `getComputedStyle()`、`offsetParent`、`getClientRects()`、`getBoundingClientRect()` 时,把范围限制在当前焦点域。 - `isVisibleFocusable()` 作为兜底判断即可,不要让它成为规则 grid、Tab、单列列表的常规移动路径。 - 焦点移动后靠近已渲染 grid 末尾时可以追加下一批卡片,但追加操作应合并到 `requestAnimationFrame`,不要在按键处理里同步创建大量节点。 渲染和 DOM 更新: - 列表、grid、Tab、盘搜结果都要有稳定 key。渲染前比较 key 和 render key,内容未变时只更新 active、数量、健康状态等小字段,不整块替换。 - 大列表首屏只渲染够看的几行,后续通过 `IntersectionObserver`、焦点接近末尾或滚动接近底部再追加。没有 `IntersectionObserver` 时用被动 scroll 监听加小延迟兜底。 - 批量创建节点使用 `DocumentFragment` 或 `replaceChildren(...nodes)`。不要在循环里交替读写布局。 - 异步区域不要先清空再等请求返回。搜索结果、盘搜结果、推荐区、最近观看等应保留旧内容或展示局部稳定 loading,避免闪烁、焦点丢失和页面滚动回顶部。 - 对会频繁刷新但结构相同的列表,优先 patch 节点和重排节点。只有 key 集合变化过大或缺少现有节点时再整列表重建。 - 页面滚动期间延迟非关键重渲染。可以维护 `scrollingUntil`,滚动结束后再执行推荐榜刷新、状态刷新等不影响当前操作的渲染。 - 图片使用固定 `aspect-ratio` 和稳定容器尺寸,避免海报、剧照加载后撑开布局。首屏少量图片可 `loading="eager"` 和 `fetchpriority="high"`,其它图片使用 `loading="lazy" decoding="async"`。 CSS 和视觉性能: - grid、卡片等自包含区域可使用 `contain: layout style`,减少布局和样式影响范围。 - `content-visibility: auto` 只能作为可选增强。部分 Android TV WebView 对焦点、滚动和可访问性支持不稳定,必须在目标设备验证后再启用。 - TV 模式优先稳定帧率。减少 `backdrop-filter`、大面积 blur、复杂阴影、图片 filter 和高成本透明叠层;焦点反馈优先使用 border、outline、轻量 transform 和背景提亮。 - 半透明视觉使用少量统一变量,不要每个组件单独写一套近似颜色。控件和文字承载层要保证亮壁纸、暗壁纸、复杂壁纸都可读。 - TV 焦点可以比手机/浏览器更明确,但不要依赖高成本阴影堆叠。海报卡片可用轻微上移缩放加清晰边框,密集列表用内描边或背景色变化。 网络、缓存和异步任务: - 首屏不要被远端接口阻塞。先渲染导航、状态、缓存热榜或占位,再按需加载 TMDB 列表、最近观看、盘搜结果和 relay 数据。 - 大数据索引使用 IndexedDB,不要把热榜、历史游标、几百条结果长期塞进 `localStorage`。`fm.cache` 适合配置、短期 UI 快照、少量状态和跨 WebView 持久化。 - 大量事件写入要串行队列化并合批,避免多个 IndexedDB 事务互相抢占。数据落库后用延迟刷新合并多次 UI 更新。 - 网络请求都要有超时和过期结果保护。搜索建议、盘搜轮询、详情加载、relay 查询应使用请求序号、token 或当前选中项校验,旧请求返回不能覆盖新视图。 - 可见范围检测类任务必须限流。网盘检测、图片预加载、relay 回填都应限制批大小和并发,不要一次性把全部结果丢给 Native 或网络。 - 后台恢复、锁屏恢复、播放页返回时只刷新必要区域。正常冷启动保持回主页;只有 `_fm_restore=1` 这类恢复信号才还原详情页、图片页、盘搜列表等深层状态。 ## 23. WebHome 路由和返回 页面内多层视图应使用 History API。 ```js function navigate(route, data) { history.pushState({ route, data }, "", "#" + route); render(route, data); } window.addEventListener("popstate", event => { const state = event.state || { route: "home" }; render(state.route, state.data); }); ``` App 物理返回键规则: - WebHome 可见且 WebView 有 history 时,优先 `webView.goBack()`。 - 没有网页 history 时,再执行 App 原生返回逻辑。 页面可以保存短期 UI 快照来处理后台恢复、锁屏恢复或 WebView 渲染进程重建。但正常冷启动应默认回到 WebHome 主页,避免用户关闭 App 后再次打开仍停留在上一次详情页或弹层。当前 Native 在 WebView 渲染进程恢复时会给 URL 追加 `_fm_restore=1`,WebHome 可以只在检测到这个参数时恢复详情页、图片查看器、同步面板等深层 UI;普通打开时建议忽略深层 UI 快照,最多恢复首页列表位置或直接清理快照。 ## 24. WebHome PanSou 集成 详情页可以集成 PanSou 类网盘搜索服务。 推荐配置项: ```js const panConfig = { apiBase: "https://so.252035.xyz", diskTypes: ["quark", "aliyun", "baidu", "uc", "tianyi", "xunlei", "123", "115", "mobile"], channels: [], username: "", password: "", token: "", tokenExpiresAt: 0 }; ``` 搜索接口: ```http POST /api/search ``` 请求体: ```json { "kw": "片名", "res": "merge", "src": "all", "cloud_types": ["quark", "aliyun"], "channels": ["channelName"] } ``` 认证接口: ```http POST /api/auth/login ``` 拿到 token 后: ```http Authorization: Bearer token ``` 结果处理: - 读取 `data.merged_by_type`。 - 只保留用户选择的网盘类型。 - 按 `diskType + normalizedUrl` 去重。 - 按网盘类型生成 Tab。 - 搜索结果列表设置最大高度,内部滚动。 - PanSou 结果可能异步补充,首次搜索后应轮询几次并合并新增结果。 - 只对 App 支持的网盘类型执行 `fm.pan.check()`。 - 点击结果时调用 `fm.pan.play({ type, url, password, title })`,由 App 复用 `push_agent/pvideo` 链路处理播放。 播放示例: ```js await fm.pan.play({ type: item.type, url: item.url, password: item.password, title: item.title }); ``` 115 提取码参数通常用 `password`,其它常规网盘通常用 `pwd`。 ## 25. Nostr 推荐首页实现要点 当前 `nostr.html` 是单文件推荐首页,核心能力包括: - TMDB 榜单、搜索、详情、剧照、演员、季集信息。 - Nostr 去中心化偏好热榜。 - App SDK 搜索播放。 - 播放时长采样和最近观看补偿。 - 透明背景和半透明控件。 - 状态面板、身份管理、数据删除。 - TMDB Key、盘搜地址、TG 频道、账号、密码和网盘类型配置。 - TV 大屏详情布局、剧情概要、演职员、相关推荐和推荐屏蔽。 - PanSou 搜索、认证、TG 频道、网盘检测、`fm.pan.play` 播放。 Nostr 使用: ```js kind = 30078 HOT_VECTOR_D = "heat:user:90d:v2" HOT_VECTOR_VERSION = 5 ``` ### 25.1 热度和数据模型 - 同一用户对同一影片/剧集只贡献 1 人。 - 播放超过 10 分钟才发布观看热度。 - 点击和搜索只是观看意图,不单独增加热度。 - 发布前检查本机是否已发布过该媒体,避免重复。 - 多设备导入同一 nsec 时属于同一用户身份。 - 每个用户发布一个紧凑偏好向量,而不是每部影片一条事件。向量里只保留媒体类型、TMDB ID、日期、标题和压缩后的海报路径,控制 relay 流量和本地存储体积。 - 偏好向量带 `expiration` 标签,热度窗口按 90 天维护。过期向量和过期条目要从本地索引中裁剪,避免历史数据长期影响榜单。 - 本地索引用 IndexedDB,推荐 store 分为 `media`、`userVector`、`relayCursor`。运行时用 Map 承接热榜聚合,落库只保存必要字段。 - `media` 记录保存媒体 key、标题、类型、TMDB ID、海报、贡献人数和最近贡献时间;`userVector` 保存用户 key、事件时间、过期日、下次裁剪日和媒体条目列表。 - 同一用户向量更新时要按 diff 修改媒体人数:旧向量有而新向量没有的媒体人数减 1,新向量新增的媒体人数加 1。不要每次全量重算所有用户向量。 - 本机删除数据后要写本地 tombstone,短时间内阻止再次发布,并过滤删除 cutoff 前的本机事件,避免 relay 删除未完全生效时旧数据被重新吃进榜单。 - 海报 URL 应压缩为 TMDB path 或短路径,渲染时再还原为具体尺寸地址。热榜索引里不要长期存完整大图 URL。 ### 25.2 Relay 同步和兜底 - 启动时先读取本地 IndexedDB 热榜索引并渲染,再连接 relay。已有缓存时用户能立即看到推荐,不必等 WebSocket。 - relay 订阅先取近 7 天备份窗口,限制 `limit`,收到 `EVENT` 后进入内存队列,按短延迟合批写入索引。不要每条事件都立刻刷新 UI。 - 收到 `EOSE` 或超时后结束首轮订阅,关闭订阅连接,并启动回填。连接状态、订阅完成数和 relay 结果要展示在状态面板,方便定位网络问题。 - 回填分 recent 和 history 两段:recent 补齐本地 cursor 之后的新事件,history 从历史游标向 90 天窗口回查。非主 relay 可只补近 7 天,主 relay 承担完整历史回填。 - 主回填 relay 按连接成功和首轮事件数量选择。每个 relay 的游标保存在 `relayCursor`,包括最新同步点、recent 目标、history 游标、失败次数和下次重试时间。 - 回填分页必须有上限和超时。单页使用 `limit`,每轮限制 recent/history 页数;失败时指数退避,成功后清除退避。 - relay 数据迟迟未到时,按短延迟预取 TMDB 兜底,再在展示阈值后切换到兜底推荐。只要 Nostr 热榜后来有数据,应立即切回 Nostr 推荐并取消兜底定时器。 - 发布偏好事件时向所有 relay 并发发送,每个连接设置超时,状态显示成功数、完成数和 relay 文本。不要因为一个 relay 慢而阻塞其它 relay。 - 删除数据应先清本机缓存和索引,再查询本身份 90 天窗口内的远端事件,发布 `kind: 5` 删除事件。文案要明确 relay 是否真正删除取决于 relay 策略。 ### 25.3 渲染性能 - 首页推荐 grid 使用分批渲染:首批只渲染约 3 行,后续每批约 2 行。靠近底部、焦点接近已渲染末尾或 sentinel 进入视口时再追加。 - `fillGrid()` 要保存每个 grid 的 render 状态:源数组、总数、已渲染数、item keys、render keys。key 前缀没变时复用已有 DOM,只追加新增卡片。 - 活跃列表未变化、已渲染数量足够时直接返回,不重建 grid。推荐榜、分类榜、最近观看、搜索结果都应遵循这个规则。 - `renderAll()` 可以只同步刷新 chips、状态和轻量结构,首页内容用双 `requestAnimationFrame` 延后到首帧之后渲染,减少启动和恢复时的卡顿。 - 滚动期间不做非关键重渲染。`scheduleRender()` 可根据 `scrollingUntil` 延迟执行,避免用户滚动时热榜刷新导致掉帧。 - `IntersectionObserver` 用于无限加载和网盘可见检测;不支持时用被动 scroll 监听和短延迟兜底。 - 图片属性统一由 helper 输出,首屏少数卡片 eager/high,其它 lazy/async。详情页大剧照先预加载成功再切换,避免空白闪烁。 - 详情页、搜索建议、状态面板、盘搜结果等局部区域独立更新。不要因为一个状态文本变化重渲染整个主页。 - 电视端方向键优先走规则快速路径。首页 Tab、规则 grid、搜索 rail、直播入口等区域应通过相邻节点、`data-card-index` 和缓存列数确定目标;只有详情页复杂块或兜底场景才使用几何搜索。 - `mediaCard()` 必须写入稳定 `data-card-index`,`maybeAppendGridForFocus()` 应优先 O(1) 读取索引,不能每次聚焦都扫描整个 grid。 - `gridColumns()` 的列数读取必须缓存,并在视口变化、TV/Web 模式变化或布局关键 class 改变后失效。 - `focusRemoteTarget()` 只负责 `focus({ preventScroll: true })` 和状态更新,滚动修正必须通过 `requestAnimationFrame` 合并;长按方向键时只滚动最后一个目标。 - 高频 `keydown` 中不要做全页面 `querySelectorAll()`、全量 `getBoundingClientRect()` 或同步 `scrollIntoView()`。 ### 25.4 TV 用户体验和样式 - 页面整体保持透明背景,让 App 壁纸透出;承载文字的卡片、按钮、输入框、状态面板使用中性半透明底色。不要使用高饱和大面积渐变。 - TV 模式下焦点样式要更清楚,但仍要低成本。推荐卡片可使用轻微上移缩放和清晰边框;盘搜结果用背景提亮和细边框;避免多层大阴影和大面积 blur。 - 首页海报/横图卡片的焦点可以比普通按钮更强,允许使用贴合卡片图片边缘的白色边框、轻微上移和轻微缩放;不要额外叠加大范围外发光、模糊阴影或造成图片与边框中间出现空心缝隙。 - 分类 Tab、详情页按钮、状态面板按钮这类密集控件应使用更细的边框和背景提亮,不要套用主页海报卡片的强焦点样式。 - 焦点样式应尽量使用已有 border、outline、轻量 transform 和背景色变化。避免通过改变元素尺寸产生重排;需要明显边框时应预留原始 border 宽度或使用不改变布局的 outline。 - TV 模式应减少 `backdrop-filter`,grid 和卡片使用 `contain: layout style`。详情大屏布局可以保留沉浸式剧照背景,但文字内容区必须有足够暗度和对比度。 - 详情页根据设备模式切换大屏布局。TV/leanback 优先展示横向剧照、评分、元信息、剧情概要和两个主要操作按钮;手机端保持紧凑滚动布局。 - 原生工具栏在 TV 详情页可通过 `fm.ui.setToolbar(false)` 隐藏,关闭详情或返回主页时恢复,避免顶部原生栏挤占大屏视觉空间。 - 搜索框和状态面板里的配置输入在 TV 上默认 `readonly`。按 OK 或触摸/鼠标点击才进入编辑态,失焦或返回后恢复只读,避免误弹输入法。 - 状态面板要同时承担诊断和配置入口:展示 SDK、TMDB、Nostr、盘搜、发布、身份、relays 状态,并提供 TMDB Key、盘搜地址、TG 频道、账号密码、网盘类型和身份同步。 - 推荐屏蔽适合做成长按进入选择模式。选择模式中 OK 切换屏蔽,返回退出;退出后正常推荐过滤被屏蔽项,选择模式内仍显示全部项并标记屏蔽状态。 - 搜索建议只由真实输入触发,返回键关闭建议并回到搜索框;搜索结果清空后焦点回首页内容,不让用户停在隐藏区域。 电视端性能和焦点验收建议: - 遥控器长按方向键在分类 Tab、首页卡片、搜索 rail 之间移动时,不应出现明显停顿、焦点丢失或页面跳顶。 - 从详情页、图片页、盘搜结果、原生播放页返回后,应恢复原焦点和滚动位置。 - 主页卡片焦点在 3 米外电视观看距离下必须清晰可见;分类按钮和详情页按钮则保持较细边框。 - 低端 Android TV WebView 上验证 `backdrop-filter`、复杂阴影、`content-visibility` 不影响焦点和滚动后,才允许扩大使用范围。 ### 25.5 盘搜和网盘检测 - PanSou 搜索读取 `data.merged_by_type`,只保留用户勾选的网盘类型,按 `diskType + normalizedUrl` 去重,并按网盘类型生成 Tab。 - 盘搜服务可能异步补充结果,首次搜索后可按配置的间隔轮询几次。每次轮询只合并新增或更新结果,不清空现有列表。 - 盘搜请求和轮询必须带 view token 和当前详情项校验,旧详情页的响应不能覆盖新详情页。 - Tab 渲染使用 `type:count` key,数量和 active 状态没变时只更新文本和 active 类。结果列表使用 pan key 和健康状态 key,优先 patch 节点和重排节点。 - 结果列表打开后要把后续详情块设为 `aria-hidden` 并临时 `tabindex=-1`,方向键限制在盘搜 Tab 和结果列表之间,返回键先从结果回到 Tab。 - 网盘检测只检测 App 支持的类型。使用 `IntersectionObserver` 检测可见结果,按小批量入队,单次 `fm.pan.check()` 建议最多约 10 条。 - 检测状态按优先级排序:有效、需要提取码、检测中、未检测、暂不支持、不确定、失效。用户更容易先看到可播放资源。 - 播放盘搜结果前保存详情滚动、列表滚动、active 类型、focus key 和结果项。原生播放返回后恢复原列表和焦点,方便继续试下一个链接。 - 播放统一调用 `fm.pan.play({ type, url, password, title })`,不要手工拼 `push://`。115 提取码常用 `password` 参数,其它常见网盘常用 `pwd` 参数。 ### 25.6 播放偏好采样和恢复 - 从详情搜索播放、盘搜播放、最近观看播放进入原生播放前,先记录 watch intent,并把 pending watch 存入 `fm.cache`。页面被杀或播放页返回后仍可补偿发布。 - App 环境下定时调用 `fm.stat()` 采样播放位置和总时长,记录最大观看位置。达到 10 分钟且未发布过该媒体时生成偏好事件。 - 如果播放期间 WebView 暂停或被回收,恢复时读取最近观看历史进行补偿:按标题、TMDB ID、创建时间窗口匹配播放记录,满足 10 分钟阈值再发布。 - 采样完成、观看接近结束或成功发布后清理 pending watch。不要让一次观看意图在后续启动中重复发布。 - UI 快照只保存短期状态,TTL 建议 2 小时。只在 `_fm_restore=1`、`fmresume`、`pageshow` 等恢复场景还原详情页、图片页、盘搜列表和滚动位置;正常冷启动回主页。 删除数据: - 清本机 IndexedDB 和缓存。 - 发布 Nostr `kind: 5` 删除事件。 - Relay 是否真正删除取决于 relay 策略。 - 正式上线前如果测试数据无法清干净,最干净的方式是更换 `tag`、缓存 key 或生成新身份。 ## 26. 隐藏功能与使用技巧 本节整理 App 已开放但入口不明显的能力,适合普通使用者、配置维护者和二次开发者快速掌握完整行为。 ### 26.1 手机端播放手势 点播和直播通用: - 长按播放画面:临时加速播放,松手恢复原倍速。临时速度来自设置里的 `speed`,范围 2x 到 5x。 - 左右滑动:快退或快进,滑动距离会换算为进度偏移。 - 左半屏上下滑:调节亮度。 - 右半屏上下滑:调节音量。 - 双指捏合:缩放视频画面,范围 1x 到 5x。 - 单击画面:显示或隐藏控制栏。 - 双击画面:点播非全屏时进入全屏,全屏时切换播放或暂停;直播中双击会切换控制栏显示。 - 画面边缘约 24dp 区域会避开手势识别,减少系统返回手势误触。 - 锁定播放界面后,手势调节、临时倍速、缩放等会被禁用。 点播专属: - 全屏时上下快速滑动:切上一集或下一集。 - 如果当前只有一集,上下快速滑动会刷新当前播放。 - 左右滑动过程中只显示目标时间,手指松开后才真正 seek。 直播专属: - 上下快速滑动:切换频道,方向受直播控制里的“反转”开关影响。 - 非时移直播不响应左右 seek。 ### 26.2 播放控制栏 倍速: - 点“倍速”:按预设倍速递增。 - 长按“倍速”:切换倍速模式。 - 长按画面触发的临时倍速不会永久覆盖当前影片保存的倍速。 片头片尾: - 点“片头”:把当前播放位置记录为跳过片头点。 - 点“片尾”:把当前距离结尾的时间记录为跳过片尾点。 - 长按“片头”:清除片头跳过点。 - 长按“片尾”:清除片尾跳过点。 - 片头、片尾会保存在当前影片历史中,下次打开同一影片会恢复。 字幕、轨道和弹幕: - 点文字、音频、视频轨道按钮:打开对应轨道选择。 - 长按文字轨道按钮:如果当前有字幕轨,会直接进入字幕调节。 - 点弹幕按钮:打开弹幕设置。 - 播放页也可以通过局域网 HTTP API 注入字幕或弹幕。 播放行为和信息: - 点“重播/刷新”:按当前模式重播或刷新当前播放。 - 长按“重播/刷新”:切换重播/刷新行为。 - 点“解码”:切换解码方式。 - 点“信息”:显示当前播放标题、播放 URL 和请求 headers。 - 点 URL:分享当前播放地址。 - 长按 URL:复制播放地址。 - 长按 headers:复制 headers。 ### 26.3 详情页 - 点影片标题:用当前标题发起快搜。 - 长按影片标题:进入换源或搜索匹配逻辑。 - 长按播放控制栏标题:同样进入换源或搜索匹配逻辑。 - 点演员、导演、简介:展开或收起长文本。 - 长按简介:复制简介文本。 - 点倒序按钮:切换选集正序或倒序,并保存到观看历史。 - 点更多选集:打开完整选集列表。 - 收藏、选集、线路、画质、倍速、画面比例、片头片尾、播放进度都会随历史记录保存。 - 无痕模式开启时,播放历史不会保存。 ### 26.4 电视遥控器 首页: - 首页标题获得焦点后,遥控器左/右键可以切换上一个或下一个站点。 - 首页标题获得焦点后,按上键刷新当前首页,带 3 秒冷却。 - 点击首页标题会打开站点选择弹窗。 点播播放: - 全屏且控制栏隐藏时,左/右键快退或快进,每次 10 秒,按住会连续累加。 - 左/右键松开后才执行 seek。 - 长按上键:临时加速播放,松开恢复历史记录里的倍速。 - 上键:打开控制栏并聚焦片头;如果已经接近片尾区域,会聚焦片尾。 - 下键:打开控制栏或进入下方控制区域。 - 确认键:显示控制栏或播放/暂停。 - 媒体快进/快退键:直接快进或快退。 直播播放: - 数字键输入频道号,最多 4 位,2 秒后跳转。 - 菜单键或长按确认键:打开菜单。 - 上/下键:切换频道或打开频道列表。 - 左/右键:时移直播可 seek。 - 媒体上一首/下一首、频道加减、PageUp/PageDown 会映射为上/下频道。 ### 26.5 设置页 - 长按“点播配置”:进入点播配置编辑模式。 - 长按“直播配置”:进入直播配置编辑模式。 - 长按“壁纸配置”:进入壁纸配置编辑模式。 - 点击“壁纸默认”:在内置壁纸间循环。 - 点击“壁纸刷新”:重新加载壁纸源。 - 长按“壁纸刷新”:打开壁纸历史。 - 点击“增强功能”:进入增强功能页。手机端和电视端都是独立页面,不使用弹窗。 - 增强功能页集中放置“网盘检测”“Proxy 壳代理”“一键同步”“调试日志”等能力。 - “网盘检测”默认开启,只控制显式 `pan.check` 能力,不会自动检测 App 原生搜索结果列表。 - “Proxy 壳代理”是 App 级代理开关和规则编辑入口。开启后,配置的默认代理地址和规则会通过 `OkHttp.selector()` 应用到 App 网络请求、WebHome `fm.req()` 和 `/webResource` 等统一 OkHttp 链路。 - Proxy 规则支持纯文本和 JSON raw 两种输入,也支持 UI 编辑;UI 编辑可新增、删除、排序规则。规则顺序会影响命中结果,靠前规则优先。 - “一键同步”用于局域网设备发现、推送/拉取同步、选择同步内容和同步目录,适合迁移接口、Jar 配置中心保存数据、WebHome 缓存、搜索记录、观看历史、收藏和设置。 - “调试日志”默认关闭,开启后会打开日志网页;关闭时不弹提示并清空当前进程内日志。 - 点击“版本”:手动触发版本检查。 - 手机端长按底部“直播”导航:把直播入口添加到桌面快捷方式。 Proxy 壳代理使用方式: 1. 入口:设置 -> 增强功能 -> Proxy 壳代理 -> 开启。 2. 默认代理地址:本机 Clash/sing-box 常用 `socks5://127.0.0.1:7897`。代理在另一台设备时,把 `127.0.0.1` 换成那台设备的局域网 IP。 3. 规则为空:所有非本机目标 host 走默认代理。 4. 只代理指定域名:在“文本”里填 JSON raw,格式如下。 ```json { "proxy": [ { "name": "video", "hosts": ["example.com", ".*\\.example\\.com"], "urls": ["socks5://127.0.0.1:7897"] }, { "name": "default", "hosts": ["*"] } ] } ``` 字段: | 字段 | 说明 | | --- | --- | | `hosts` | 目标 host 匹配规则;支持包含匹配、Java 正则;`*` 表示所有非本机目标 host | | `urls` | 可选;不写时使用上方默认代理地址;支持 `http://`、`https://`、`socks://`、`socks5://` | 调试:开启“调试日志”,过滤“代理”或“播放”。看到“壳代理已启用”“请求命中壳代理”“播放器通过壳代理连接”表示已生效;看到“本机服务直连”通常是正常的本地请求跳过代理。 ### 26.6 搜索、收藏和分类 - 分类页长按影片卡片:直接用该影片名进入全局搜索结果。 - 对 `indexs=1` 的索引站点,点击条目会进入聚合搜索,而不是普通详情。 - 搜索结果左侧站点分组可以快速切换来源。 - 搜索结果页标题点击可回到搜索编辑状态。 - 收藏和历史支持多设备同步。 ### 26.7 外部 App 联动 App 在 Android Manifest 中开放了多种系统入口: - 系统分享文本到 App:会当作播放地址推送播放。 - 系统搜索 `ACTION_SEARCH`:直接打开 App 搜索页。 - 用系统“打开方式”打开视频、音频、本地文件:进入播放。 - 打开 `.m3u` 或 `text/plain` 文件:作为直播配置加载。 - 打开 `.torrent`、`magnet:`、`ed2k:`、`thunder:`、`jianpian:`:进入对应解析或播放链路。 - 打开 `http`、`https`、`rtsp`、`rtmp`、`smb` 且媒体类型是视频或音频:进入播放。 示例: ```bash adb shell am start -a android.intent.action.SEARCH --es query "仙逆" com.fongmi.android.tv adb shell am start -a android.intent.action.VIEW -d "magnet:?xt=urn:btih:..." ``` 包名和 Activity 名以实际构建产物为准。 ### 26.8 局域网隐藏网页 App 启动后会开启本地 HTTP 服务,端口从 `9978` 到 `9998` 依次尝试,取第一个可用端口。访问: ```text http://设备IP:端口/ ``` 会打开内置管理网页。这个网页提供: - 搜索:远程触发 App 搜索。 - 推送:远程推送播放地址。 - 设置:远程写入配置文本或配置 URL。 - 本地:浏览 App 本地文件、上传文件、新建文件夹、删除文件或文件夹。 本地文件页技巧: - 长按文件或文件夹:删除。 - 上传 `.zip`:自动解压到目标目录。 - 点文件后选择“使用”:根据文件类型执行动作。例如 `.apk` 打开安装,字幕文件注入播放器,配置文件加载为配置。 注意:当前本地 HTTP 服务没有鉴权。局域网地址只应暴露在可信网络中。 ### 26.9 局域网 HTTP 快捷能力 以下能力可由浏览器、脚本、WebHome 或同网段设备调用。端口从 `/device` 的 `ip` 字段读取,不要写死 `9978`。 搜索、推送、设置、使用本地文件: ```text GET/POST /action?do=search&word=关键词 GET/POST /action?do=push&url=播放地址 GET/POST /action?do=setting&text=配置内容或配置URL&name=显示名称 GET/POST /action?do=file&path=本地相对路径 ``` 刷新和注入资源: ```text GET/POST /action?do=refresh&type=home GET/POST /action?do=refresh&type=live GET/POST /action?do=refresh&type=detail GET/POST /action?do=refresh&type=player GET/POST /action?do=refresh&type=category GET/POST /action?do=refresh&type=subtitle&path=字幕URL GET/POST /action?do=refresh&type=danmaku&path=弹幕URL GET/POST /action?do=refresh&type=vod&json=Vod_JSON ``` 播放控制: ```text GET/POST /action?do=control&type=play GET/POST /action?do=control&type=pause GET/POST /action?do=control&type=stop GET/POST /action?do=control&type=prev GET/POST /action?do=control&type=next GET/POST /action?do=control&type=loop GET/POST /action?do=control&type=replay ``` 播放状态: ```text GET/POST /media ``` `/media` 返回字段: | 字段 | 说明 | | --- | --- | | `state` | `1` 其它,`2` ready,`3` playing,`6` buffering | | `speed` | 倍速 | | `duration` | 总时长,毫秒 | | `position` | 当前位置,毫秒 | | `url` | 当前播放 URL | | `title` | 当前媒体标题 | | `artist` | 当前媒体 artist 元数据 | | `artwork` | 当前媒体封面 URL | 缓存接口: ```text GET/POST /cache?do=get&rule=命名空间&key=键 GET/POST /cache?do=set&rule=命名空间&key=键&value=值 GET/POST /cache?do=del&rule=命名空间&key=键 ``` 网盘检测 HTTP API: ```text POST /pan/check OPTIONS /pan/check ``` 请求体和 `type/state` 枚举见“网盘检测 `fm.pan.check(items)`”。未开启增强功能里的“网盘检测”时返回 403。 WebHome 资源网关: ```text GET/POST/PUT/PATCH/DELETE/HEAD/OPTIONS /webResource?url=目标URL ``` 只允许目标 URL 为 `http://` 或 `https://`,支持 headers、credentials 和 Range 透传,详细行为见 `fm.res()`。 调试日志: ```text GET /debug/logs GET /debug/logs.txt GET /debug/clear GET /debug/enable GET /debug/disable GET /debug/stream ``` 文件管理: ```text GET /file/{path} POST /upload POST /newFolder POST /delFolder POST /delFile ``` `/file` 支持 Range 请求,可用于本地媒体分段播放。上传 `.zip` 会自动解压。 ### 26.10 投屏和多设备同步 - 投屏设备列表支持 DLNA 设备,也支持另一台同 App 设备。 - 手机端投屏和同步弹窗支持扫码绑定设备。 - 点击普通 App 设备投屏时,会调用对方设备的 `/action?do=cast`。 - 点击 DLNA 设备投屏时,走 DLNA cast。 - 多设备同步支持观看历史和收藏。 - 同步弹窗的模式按钮可以切换同步方向。 - 在非默认同步模式下长按设备,可执行强制同步或先清本机再同步。 - 电视端会启动 DLNA Renderer,可被局域网 DLNA 控制器发现并控制播放、暂停、停止、Seek、音量和静音。 ### 26.11 配置里的隐藏能力 站点字段: - `hide: 1`:站点不显示在列表中,但仍存在于配置中。 - `searchable: 0`:禁用该站点搜索。 - `changeable: 0`:禁用该站点换源。 - `quickSearch: 0`:排除快速搜索。 - `indexs: 1`:将站点作为索引源,条目点击进入聚合搜索。 - `timeout`:单站点播放超时,单位秒。 - `style`:控制站点或条目卡片样式。 - `homePage` / `home_page` / `webHome` / `web_home`:自定义 WebHome 首页。 - `click`:给解析 WebView 注入点击脚本,可自动点播放按钮或关闭弹窗。 - `header`:给该站点请求追加 headers。 - `jar`:当前站点单独指定 Spider JAR,覆盖全局 spider。 Vod 条目字段: - `action`:条目点击后触发 Spider `action()` 或 HTTP API action,不进入普通详情。 - `vod_tag: "folder"`:条目作为文件夹或子分类入口。 - `cate`:条目作为子分类入口。 - `style` / `land` / `circle` / `ratio`:单条目覆盖展示样式。 播放结果字段: - `header`:播放请求 headers。 - `subs`:字幕列表。 - `danmaku`:弹幕源。 - `drm`:DRM license 配置。 - `artwork`:播放器封面。 - `position`:起播位置。 - `click`:解析 WebView 点击脚本。 - `msg`:Toast 提示。 ### 26.12 特殊播放协议 - `push://真实地址`:当前播放中再次打开一个新播放地址。 - `assets://path`:读取 APK assets。 - `file://path`:读取 App 本地路径。 - `proxy://query`:走 Spider 本地代理。 - `.strm`:读取文件第一行作为真实播放地址。 - YouTube 链接:用 NewPipe 提取播放地址;播放列表可展开为多集。 - `magnet:` / `.torrent` / `ed2k:`:走迅雷内核解析。 - `jianpian:` / `tvbox-xg:` / `ftp:`:走荐片/XG P2P 解析。 - `thunder:`:详情解析阶段可展开或转换。 ### 26.13 WebHome 相关技巧 - WebHome 页面可调用 `fm.search(keyword, { direct: true })`,减少返回层级。 - WebHome 可调用 `fm.history()` 读取最近观看,用于补偿网页在播放页后台时漏掉的播放进度。 - WebHome 可通过 `fm.config().driveCheck` 判断网盘检测开关。 - WebHome 可用 `fm.res(url, { headers })` 给图片、字幕、视频资源生成本地网关地址,处理跨域和 headers。 - WebHome 可用 `fm.req(url, options)` 走 Native OkHttp 请求,绕开普通浏览器 fetch 的 CORS 限制。 - WebHome 使用透明背景时,App 壁纸可以透出,适合做沉浸式主页。 ### 26.14 调试日志 调试日志用于临时排查 WebHome、SDK、爬虫请求、网盘检测和播放链路问题。 入口: - App 内:设置 -> 增强功能 -> 调试日志。 - HTTP 页面:`http://127.0.0.1:{port}/debug/logs`。 - 下载文本:`http://127.0.0.1:{port}/debug/logs.txt`。 - 清空日志:`http://127.0.0.1:{port}/debug/clear`。 - 开启或关闭:`/debug/enable`、`/debug/disable`。 - 实时拉取:`/debug/stream`,返回 `{ enabled, size, text }`,日志页默认每秒轮询一次。 行为: - 默认关闭,不长期收集。 - 开启后记录当前进程内日志,不限制 2000 条。 - 关闭时清空当前进程内日志,不显示 toast。 - 日志页会展示本机地址和局域网地址;局域网设备需要使用局域网地址访问。 - 覆盖范围包括 WebHome SDK 调用、WebView console、JS 异常、WebView provider 版本、WebView 主框架/资源加载错误、RenderProcess 崩溃、`fm.req`、资源网关、`pan.check`、`pan.play`、本地 HTTP 服务、全局 OkHttp 请求响应、SpiderDebug、push/pvideo 和播放器状态。 ### 26.15 安全提醒 这些能力是“已开放入口”,不是安全隔离边界: - 局域网 HTTP 服务没有鉴权。 - `/upload`、`/delFile`、`/delFolder`、`/action?do=setting` 有明显副作用。 - WebHome 页面一旦配置到站点,就能调用 App 注入 SDK,应只加载可信页面。 - 爬虫、配置、WebHome、局域网工具都可以触发播放、搜索、缓存和部分设置行为。 ## 27. Android 外部 Intent App 对系统开放: | Intent | 行为 | | --- | --- | | `ACTION_MAIN` | 启动 App | | `ACTION_SEARCH` | 读取 `SearchManager.QUERY` 并打开搜索 | | `ACTION_SEND` + `text/plain` | 读取 `Intent.EXTRA_TEXT` 并推送播放 | | `ACTION_VIEW` + `content/file` + `video/*` / `audio/*` | 推送播放 URI | | `ACTION_VIEW` + `content/file` + `text/plain` 或 `.m3u` | 作为直播配置加载 | | `ACTION_VIEW` + `application/x-bittorrent` | 推送播放/解析 | | `ACTION_VIEW` + `smb/rtmp/rtsp/http/https` + video/audio | 推送播放 | | `ACTION_VIEW` + `ed2k/magnet/thunder/jianpian` | 推送播放/解析 | 示例: ```bash adb shell am start -a android.intent.action.SEARCH --es query "仙逆" com.fongmi.android.tv adb shell am start -a android.intent.action.VIEW -d "magnet:?xt=urn:btih:..." ``` ## 28. DLNA 和 MediaSession 电视端: - 启动 DLNA Renderer。 - 支持投屏播放、暂停、停止、Seek、下一条、音量、静音。 MediaSession: - 系统媒体控件可控制播放、暂停、停止、上一集/下一集、Seek。 - 媒体浏览可暴露点播历史和直播频道。 ## 29. CORS、Cookie 和网络策略 WebHome: - API 数据请求优先用 `fm.req`。 - 图片、视频、字幕等 DOM 资源优先用 `fm.res`。 - `fm.req` 使用 WebHome 专用 OkHttpClient,但会接入 App 统一 DNS 和代理选择器,会继承配置里的 `hosts`、`doh`、`proxy` 和增强功能里的壳代理规则。 - `fm.req` 不自动继承配置里的 `headers`;需要 Header 时用 `options.headers` 显式传入。 - `/webResource` 使用项目统一 OkHttp,会受配置网络策略和壳代理规则影响。 - 普通浏览器 CORS 不能被网页禁用,只能通过 Native 请求或本地网关绕开。 配置层网络策略: - `headers` 可按 host 注入 header。 - `proxy` 可按 host 正则选择代理。 - `hosts` 可覆盖 DNS。 - `doh` 可配置 DNS over HTTPS。 - `ads` 可拦截广告域名。 ## 30. 安全和限制 重要限制: - 局域网 HTTP 服务当前没有鉴权。 - `/upload`、`/delFile`、`/delFolder`、`/action?do=setting` 有明显副作用。 - WebHome 页面一旦配置到站点,就能调用 App SDK,应只加载可信页面。 - 爬虫、配置、WebHome、局域网工具都可以触发播放、搜索、缓存和部分设置行为。 - 网盘检测过大批量可能触发平台风控。 - 调试日志可能包含请求 URL、Header、Cookie、播放地址等敏感信息,只应在临时排查时开启,分享日志前自行确认内容。 - Nostr 删除事件是否生效取决于 relay。 - WebView 透明背景在少数旧设备或特殊 WebView 版本上可能有兼容差异。 ## 31. 常见问题 ### 31.1 为什么 WebHome 在电脑正常、App 不正常 检查: - 是否等待 `window.fm` 注入。 - 是否使用普通 `fetch` 请求跨域 API。 - 是否需要用 `fm.req` 或 `fm.res`。 - 是否依赖了电脑浏览器才有的调试插件或关闭 CORS 设置。 - 是否给 `html/body` 写了不透明背景,导致透明 WebView 无效。 ### 31.2 为什么网盘检测不执行 检查: - App 是否安装了支持 `config.driveCheck` 的版本。 - 设置 -> 增强功能 -> “网盘检测”是否开启。该开关默认开启,但用户可以关闭。 - WebHome 是否读取了 `fm.config().driveCheck`。 - 是否只检测了支持的网盘类型。 - 是否可见范围监听没有触发。 ### 31.3 为什么播放时长统计不准 WebHome 在原生播放页期间可能暂停定时器。推荐: - 播放前记录观看意图。 - 播放中用 `fm.stat()` 采样。 - 返回后用 `fm.history()` 补偿。 - 发布热度前去重。 ### 31.4 为什么局域网端口不是 9978 端口不是固定值。App 从 `9978` 到 `9998` 依次尝试。如果 `9978` 被占用,会使用后续端口。 ### 31.5 修改什么需要重新打包 不需要重新打包: - 只修改线上 WebHome HTML/CSS/JS。 - 修改远程配置。 - 修改远程 Spider JAR、JS、Python 文件。 需要重新打包: - 修改 Android 原生代码。 - 修改 WebView 容器行为。 - 修改 App SDK 注入能力。 - 修改本地 HTTP 服务实现。 - 修改内置资源或 assets。 ## 32. 给 AI 开发 WebHome 的要求 如果让 AI 基于本文生成 WebHome 首页,建议要求: 1. 单文件 HTML,除必要第三方库外不引入构建流程。 2. App 内优先使用 `fm.req` 请求 API,避免 CORS。 3. 图片和资源优先使用 `fm.res`。 4. 搜索播放使用 `fm.search(keyword, { direct: true })`。 5. 多层页面使用 History API。 6. 移动端优先,电视端保证遥控器焦点可用。 7. 透明背景不要给 `html/body` 写死纯色背景,App 环境保持页面级透明。 8. 透明详情页、剧情页等全屏浮层打开时,要临时隐藏底层 WebHome 页面,避免主页/详情/弹层内容叠加。 9. 卡片、按钮、输入框、Tab、剧情文本等承载文字的区域要使用统一半透明面板色,不要让文字直接压在壁纸上。 10. 网盘检测必须先读 `fm.config().driveCheck`。 11. PanSou 搜索结果只检测可见范围内支持类型。 12. 所有 SDK Promise 都要捕获异常。 13. 不依赖固定端口。 14. 不加载不可信远程脚本。