Files
android-tv/docs/superpowers/specs/2026-06-09-multi-source-strategy-design.md
2026-06-09 18:49:27 +08:00

148 lines
5.3 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 多源策略模式重构设计文档
## 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 用单例模式 |