docs: 多源策略模式重构设计文档

This commit is contained in:
xiaji
2026-06-09 18:49:27 +08:00
parent b82cc74aad
commit 4f00412962

View File

@@ -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<SearchResult>) -> Unit,
onError: suspend (String) -> Unit
)
suspend fun extractVideos(detailUrl: String): List<PlaySource>
suspend fun resolvePlayUrl(playUrl: String): Pair<String?, String?>
}
```
### 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<PlaySource> { ... }
// 通用播放页iframeSelector / videoSelector
override suspend fun resolvePlayUrl(playUrl): Pair<String?, String?> { ... }
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<String, SourceHandler>()
fun register(handler: SourceHandler) { handlers[handler.id] = handler }
fun get(id: String): SourceHandler? = handlers[id]
fun getAll(): List<SourceHandler> = 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 用单例模式 |