diff --git a/docs/superpowers/specs/2026-06-09-multi-source-strategy-design.md b/docs/superpowers/specs/2026-06-09-multi-source-strategy-design.md new file mode 100644 index 0000000..5fd081f --- /dev/null +++ b/docs/superpowers/specs/2026-06-09-multi-source-strategy-design.md @@ -0,0 +1,148 @@ +# 多源策略模式重构设计文档 + +## 1. 背景与问题 + +当前架构使用单一 `VideoExtractor` + `SiteConfig` 选择器处理所有视频源,但 xb6v (星辰影视) 和 tvcat (电视猫) 差异巨大: + +| 维度 | xb6v | tvcat | +|------|------|-------| +| 搜索 | POST 表单 + 额外参数 | GET query 参数 | +| 详情页 | 多源标签页 + 剧集列表 | 单源"播放"标题,多源在播放页 JS 加载 | +| 播放页 | iframe/video 标签直接可用 | iframe 空,需调用 `/_fetch_p/` API 获取 m3u8 | + +强行用选择器配置导致代码耦合、难以维护、新增源需修改核心逻辑。 + +## 2. 设计目标 + +1. **策略模式**:每个源独立实现 `SourceHandler` 接口 +2. **基类复用**:通用 Jsoup 逻辑在 `BaseSourceHandler`,配置型源零代码 +3. **特殊隔离**:tvcat 的 API 调用封装在 `TvcatHandler` 内部 +4. **统一 UI**:搜索结果、详情页、播放页 UI 完全共用 +5. **易扩展**:新增源 = 新建 Handler + 注册,不改核心代码 + +## 3. 架构设计 + +### 3.1 核心接口 + +```kotlin +// engine/SourceHandler.kt +interface SourceHandler { + val id: String + val displayName: String + val baseUrl: String + + suspend fun search( + keyword: String, + onResult: suspend (List) -> Unit, + onError: suspend (String) -> Unit + ) + + suspend fun extractVideos(detailUrl: String): List + + suspend fun resolvePlayUrl(playUrl: String): Pair +} +``` + +### 3.2 基类实现 + +```kotlin +// engine/BaseSourceHandler.kt +abstract class BaseSourceHandler( + override val id: String, + override val displayName: String, + override val baseUrl: String, + protected val config: SiteConfig +) : SourceHandler { + + // 通用搜索:POST/GET、参数注入、结果解析 + override suspend fun search(...) { ... } + + // 通用详情页:sourceSelector + sourceEpisodeGroupSelector 配对,fallback 到 episodeSelector + override suspend fun extractVideos(detailUrl): List { ... } + + // 通用播放页:iframeSelector / videoSelector + override suspend fun resolvePlayUrl(playUrl): Pair { ... } + + protected fun buildFullUrl(href: String): String { ... } +} +``` + +### 3.3 具体 Handler + +**Xb6vHandler** - 完全复用基类,仅需正确的 `SiteConfig` 选择器配置 + +**TvcatHandler** - 重写两个关键方法: +- `extractVideos()`:详情页只有一个源"播放",直接用 `li.list-inline-item a` 提取所有剧集 +- `resolvePlayUrl()`:解析 `/vod-play/{id}/ep{num}`,调用 `${baseUrl}/_fetch_p/{id}/ep{num}` API,返回第一个 m3u8 URL + +### 3.4 注册中心 + +```kotlin +// engine/SourceRegistry.kt +object SourceRegistry { + private val handlers = mutableMapOf() + + fun register(handler: SourceHandler) { handlers[handler.id] = handler } + fun get(id: String): SourceHandler? = handlers[id] + fun getAll(): List = handlers.values.toList() + + fun init(context: Context) { + val configRepo = ConfigRepository(context) + register(Xb6vHandler(configRepo.getXb6vConfig())) + register(TvcatHandler()) + } +} +``` + +### 3.5 集成点 + +**SettingsActivity** - Spinner 显示 `SourceRegistry.getAll().map { it.displayName }`,选择时保存 `currentSourceId` + +**PlayerActivity** - 通过 `SourceRegistry.get(config.currentSourceId)` 获取 Handler,调用 `extractVideos()` 和 `resolvePlayUrl()` + +**SearchFragment** - 同理通过 Registry 获取当前源的 Handler 执行搜索 + +## 4. 数据流 + +| 场景 | 调用链 | +|------|--------| +| 搜索 | SearchFragment → SourceRegistry.get(id).search() → BaseSourceHandler.search() | +| 详情页解析 | PlayerActivity.loadSources() → handler.extractVideos() → 基类或子类重写 | +| 播放链接解析 | PlayerActivity.playEpisode() → handler.resolvePlayUrl() → 基类或子类重写 | + +## 5. 文件结构 + +``` +engine/ +├── SourceHandler.kt (新建,接口) +├── BaseSourceHandler.kt (新建,抽象基类) +├── SourceRegistry.kt (新建,注册中心) +├── VideoExtractor.kt (废弃,保留兼容) +├── NativeSearch.kt (废弃,保留兼容) +├── xb6v/ +│ └── Xb6vHandler.kt (新建) +└── tvcat/ + └── TvcatHandler.kt (新建,含 API 调用) +``` + +## 6. 错误处理 + +- 网络异常:基类统一 `try-catch`,返回空列表/空 Pair,上层显示错误 +- 解析失败:选择器匹配不到 → fallback 逻辑(基类已有) +- 源切换:Settings 保存 `currentSourceId`,App 启动时 `SourceRegistry.init()` 恢复 + +## 7. 优势 + +1. **扩展性**:新增源 = 新建 `XxxHandler` + 注册,不改动核心代码 +2. **复用**:xb6v 类源完全靠配置,零代码 +3. **隔离**:tvcat 特殊逻辑(API 调用)封装在自己 Handler 里 +4. **测试**:每个 Handler 可独立单元测试 +5. **统一 UI**:搜索结果、详情页、播放页 UI 全共用,仅数据源不同 + +## 8. 风险与缓解 + +| 风险 | 缓解 | +|------|------| +| 基类过度膨胀 | 只放真正通用的逻辑,特殊情况让子类重写 | +| 选择器配置出错 | 添加配置校验,启动时检查必填字段 | +| 并发安全 | Handler 无状态,多线程安全;Registry 用单例模式 | \ No newline at end of file