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

5.3 KiB
Raw Blame History

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

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 核心接口

// 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 保存 currentSourceIdApp 启动时 SourceRegistry.init() 恢复

7. 优势

  1. 扩展性:新增源 = 新建 XxxHandler + 注册,不改动核心代码
  2. 复用xb6v 类源完全靠配置,零代码
  3. 隔离tvcat 特殊逻辑API 调用)封装在自己 Handler 里
  4. 测试:每个 Handler 可独立单元测试
  5. 统一 UI:搜索结果、详情页、播放页 UI 全共用,仅数据源不同

8. 风险与缓解

风险 缓解
基类过度膨胀 只放真正通用的逻辑,特殊情况让子类重写
选择器配置出错 添加配置校验,启动时检查必填字段
并发安全 Handler 无状态多线程安全Registry 用单例模式