# 多源策略模式重构设计文档 ## 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 用单例模式 |