docs: 多源策略模式重构设计文档
This commit is contained in:
@@ -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 用单例模式 |
|
||||
Reference in New Issue
Block a user