Back to Animeko

SelectorMediaSourceEngine 步骤说明

tools/datasource-test-mcp/src/main/resources/selector-engine-docs.md

6.0.09.7 KB
Original Source

SelectorMediaSourceEngine 步骤说明

Selector (CSS Selector) 数据源引擎从一个网页站点解析出某一集的可播放链接。 引擎代码位于 app/shared/app-data/src/commonMain/kotlin/domain/mediasource/web/SelectorMediaSourceEngine.kt

完整流程共 5 个引擎步骤,之后还有播放期的视频 URL 匹配:

searchSubjects → selectSubjects → searchEpisodes → selectEpisodes → selectMedia
                                                                        │
                                              播放/测试时: extractVideo (WebView) + matchWebVideo (正则)

MCP 工具对应关系:

  • selector_resolve_episode 自动跑完整流程并返回每步 trace。默认还会继续做 WebView 视频解析与 VLC 播放探测(会启动 CEF 与播放器,耗时较长),只测解析层请传 extractVideo=false。 每个搜索词默认只访问前 3 个条目的详情页(maxSubjectsPerName;App 内无此限制。条目按名称长度 升序排序,目标条目排名靠后时把它调大);
  • selector_run_step 单独跑其中一个步骤;
  • validate_selector_config 离线校验配置字段。

命名区分: MCP 工具 search_subjects 查询的是 Ani API 的条目元数据(拿 subjectId/episodeId), 与引擎步骤 searchSubjects(selector_run_stepstep 值,请求目标站点的搜索页)是两回事, 名字只差一个下划线,不要混用。

1. searchSubjects — 搜索条目列表

做什么: 把搜索关键词(番剧名)代入 searchUrl{keyword} 占位符,HTTP GET 该 URL,返回搜索结果页。

输入: searchUrl、关键词、searchUseOnlyFirstWord(只用名字的第一个词搜索)、searchRemoveSpecial(移除特殊字符,并删除「剧场版」「OVA」「总集篇」等标记词——例如「剧场版 命运石之门」会以「命运石之门」作为关键词,避免取首词时只剩「剧场版」)。

输出: SearchSubjectResult { url, document, captchaKind }document == null 表示 404;captchaKind != null 表示被人机验证(Cloudflare 等)拦截。

常见问题:

  • 返回 404 / 空页面 → searchUrl 模板错误,或站点换了搜索路径;
  • captchaKind 非空 → 站点开了防爬。本工具没有过验证码/注入浏览器 cookie 的能力, 请改用 App 内设置页的数据源测试器(支持交互过验证码);
  • 返回的是 JSON API 响应 → 配合 json-path-indexed 条目格式使用。

selector_run_step(step=searchSubjects, config, keyword) 会返回抓到的原始 HTML(默认最多 100k 字符,超长会截断,用 maxHtmlLength 调大;截断的 HTML 再拿去离线解析可能失真),便于人工检查页面结构。后续 select* 步骤也支持直接传 url 由其自行抓取,不必手动搬运 HTML。

2. selectSubjects — 解析条目列表

做什么: 用条目格式 (subjectFormatId) 配置的 CSS selector / JsonPath 从搜索结果页中提取条目(名称 + 详情页链接)。

条目格式 (三选一):

格式 id提取方式关键配置
a选择 <a> 元素,title/text 为名称,href 为链接selectLists
indexed名称与链接分别用两个 selector 选出后按序配对selectNames, selectLinks
json-path-indexed页面是 JSON API 响应,用 JsonPath 提取selectLinks, selectNames

输出: WebSearchSubjectInfo { name, fullUrl, partialUrl } 列表。返回 null 表示配置无效(selector 为空或语法错误)。

常见问题:

  • 解析出 0 个条目 → selector 与页面结构不匹配,把 searchSubjects 返回的 HTML 拿来对照调 selector;
  • 名称混入无关文字 → selector 选得太宽。

selector_run_step(step=selectSubjects, config, url 或 html) 支持直接给 HTML 离线调试。

3. searchEpisodes — 获取条目详情页

做什么: HTTP GET 某个条目的详情页(其中包含剧集/线路列表)。无解析逻辑,404 时返回 null

selector_run_step(step=searchEpisodes, url) 返回详情页原始 HTML。

4. selectEpisodes — 解析剧集列表

做什么: 用剧集格式 (channelFormatId) 配置的 selector 从详情页提取剧集(名称、播放页链接、线路)。

剧集格式 (二选一):

格式 id适用页面关键配置
no-channel只有一个剧集列表selectEpisodes, selectEpisodeLinks(可选), matchEpisodeSortFromName
index-groupedtab 分线路,每个线路一个列表selectChannelNames, matchChannelName, selectEpisodeLists, selectEpisodesFromList, selectEpisodeLinksFromList(可选), matchEpisodeSortFromName

matchEpisodeSortFromName 是从剧集名提取序号的正则,需要 (?<ep>...) 命名分组, 默认 第\s*(?<ep>.+)\s*[话集]。名为「正片」「高清版」或含分辨率字样的会被当作电影,序号视为 01。

注意剧集的相对链接以条目页所在目录(条目页 URL 去掉最后一段路径)为基拼接,与搜索结果页 用站点根 finalBaseUrl 的语义不同——所以离线传 html 调试时必须同时提供真实的 subjectUrl, 否则 playUrl 会拼错。

输出: SelectedChannelEpisodes { channels, episodes };episodes 每项为 WebSearchEpisodeInfo { channel, name, episodeSortOrEp, playUrl }。返回 null 表示配置无效。

常见问题:

  • 0 个剧集 → selector 不匹配;
  • episodeSortOrEp 为空 → matchEpisodeSortFromName 没匹配上剧集名,后续 selectMedia 会因此过滤掉该集;
  • playUrl 错误 → 剧集元素不是 <a> 时需要配 selectEpisodeLinks(FromList)

selector_run_step(step=selectEpisodes, config, url 或 html+subjectUrl)

5. selectMedia — 转换并过滤

做什么: 把剧集信息转换为 Media 对象,并按查询上下文过滤:

  • episodeSortOrEp == null 的剧集直接丢弃;
  • filterByEpisodeSort == true 时只保留序号与目标集数(sort 或 ep)一致的剧集;
  • 字幕语言从线路名/剧集名猜测(如「简中」「繁中」),否则用 defaultSubtitleLanguage

输出: SelectMediaResult { originalList, filteredList }。trace 里的 filteredOut 列出被过滤掉的候选,便于排查「有剧集但匹配不上目标集数」的问题。

selector_run_step(step=selectMedia, config, episodes, query)

播放期: extractVideo 与 matchWebVideo

拿到 playUrl(播放页)后还不是最终视频地址。播放时 App 用 WebView (桌面为 CEF) 加载播放页, 拦截页面发出的所有网络请求,用 matchVideo 配置逐个匹配:

  1. matchNestedUrl 匹配到的 URL → 作为嵌套页面继续加载(enableNestedUrl 开启时);
  2. matchVideoUrl 匹配到的 URL → 即最终视频地址,(?<v>...) 命名分组可只取其中一段,没有分组则取整个 URL;
  3. 都不匹配 → 忽略该请求。

匹配成功后,视频请求会附带 addHeadersToVideo 配置的 User-Agent / Referer; WebView 加载播放页时会注入 cookies(每行一个 name=value)。

MCP 工具:

  • selector_run_step(step=matchWebVideo, config, url) — 离线测试某个 URL 会被判为「视频 / 嵌套页 / 忽略」,调 matchVideoUrl 正则用;
  • selector_run_step(step=extractVideo, url, config?) — 真实启动 WebView 加载播放页并拦截视频 URL;
  • probe_video(videoUrl, headers) — 对最终视频 URL 做 HTTP 探测,并用 Animeko 桌面端同款播放器 (VLC) 真实播放几秒,报告分辨率、时长、编码、帧率、码率。

配置 JSON 形态与示例

validate_selector_config / selector_resolve_episode / selector_run_stepconfig 参数接受:

  1. App 导出格式: {"factoryId": "web-selector", "version": 2, "arguments": {...}};
  2. 裸 arguments: {"name": "...", "searchConfig": {...}};
  3. 裸 searchConfig: {"searchUrl": "...", ...};
  4. 订阅列表: {"mediaSources": [{...}, ...]}(取第一个 web-selector 源)。

没有现成配置时,可以从下面这个最小可用示例开始(App 导出格式,来自一个真实站点; 未列出的字段都有合理默认值,name/description/iconUrl 为必填):

json
{
  "factoryId": "web-selector",
  "version": 2,
  "arguments": {
    "name": "稀饭动漫",
    "description": "",
    "iconUrl": "https://dm1.xfdm.pro/upload/site/20240308-1/813e41f81d6f85bfd7a44bf8a813f9e5.png",
    "searchConfig": {
      "searchUrl": "https://dm1.xfdm.pro/search.html?wd={keyword}",
      "subjectFormatId": "indexed",
      "selectorSubjectFormatIndexed": {
        "selectNames": "body > .box-width .search-box .thumb-content > .thumb-txt",
        "selectLinks": "body > .box-width .search-box .thumb-menu > a"
      },
      "channelFormatId": "index-grouped",
      "selectorChannelFormatFlattened": {
        "selectChannelNames": ".anthology-tab > .swiper-wrapper a",
        "matchChannelName": "^()?(?<ch>.+?)(\\d+)?$",
        "selectEpisodeLists": ".anthology-list-box",
        "selectEpisodesFromList": "a"
      }
    },
    "tier": 2
  }
}

也可以在 App 内导出现成配置: 设置 → 数据源 → 选择一个 Selector 源 → 导出,得到的就是上述格式 1。

调试建议流程

  1. 没有配置时,从上方「最小可用示例」或 App 导出的 JSON 开始;validate_selector_config 先排除语法错误;
  2. search_subjects / get_subject_episodes 拿到目标条目与 episodeId(这是 Ani API 元数据查询,不是引擎步骤);
  3. selector_resolve_episode 跑全流程,看哪一步的 trace 先出问题(只测解析层传 extractVideo=false,更快且不弹窗);
  4. selector_run_step 单独重跑该步骤(可传 HTML 离线迭代 selector,也可直接传 url);
  5. 视频链接解析不出来时用 matchWebVideo 调正则、extractVideo 看 WebView 实际拦截到了什么;
  6. 最后 probe_video 验证视频真实可播放。