5.3 KiB
5.3 KiB
多源策略模式重构设计文档
1. 背景与问题
当前架构使用单一 VideoExtractor + SiteConfig 选择器处理所有视频源,但 xb6v (星辰影视) 和 tvcat (电视猫) 差异巨大:
| 维度 | xb6v | tvcat |
|---|---|---|
| 搜索 | POST 表单 + 额外参数 | GET query 参数 |
| 详情页 | 多源标签页 + 剧集列表 | 单源"播放"标题,多源在播放页 JS 加载 |
| 播放页 | iframe/video 标签直接可用 | iframe 空,需调用 /_fetch_p/ API 获取 m3u8 |
强行用选择器配置导致代码耦合、难以维护、新增源需修改核心逻辑。
2. 设计目标
- 策略模式:每个源独立实现
SourceHandler接口 - 基类复用:通用 Jsoup 逻辑在
BaseSourceHandler,配置型源零代码 - 特殊隔离:tvcat 的 API 调用封装在
TvcatHandler内部 - 统一 UI:搜索结果、详情页、播放页 UI 完全共用
- 易扩展:新增源 = 新建 Handler + 注册,不改核心代码
3. 架构设计
3.1 核心接口
// 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 基类实现
// 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 注册中心
// 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. 优势
- 扩展性:新增源 = 新建
XxxHandler+ 注册,不改动核心代码 - 复用:xb6v 类源完全靠配置,零代码
- 隔离:tvcat 特殊逻辑(API 调用)封装在自己 Handler 里
- 测试:每个 Handler 可独立单元测试
- 统一 UI:搜索结果、详情页、播放页 UI 全共用,仅数据源不同
8. 风险与缓解
| 风险 | 缓解 |
|---|---|
| 基类过度膨胀 | 只放真正通用的逻辑,特殊情况让子类重写 |
| 选择器配置出错 | 添加配置校验,启动时检查必填字段 |
| 并发安全 | Handler 无状态,多线程安全;Registry 用单例模式 |