feat: 添加 BaseSourceHandler 抽象基类

This commit is contained in:
xiaji
2026-06-09 19:51:38 +08:00
parent 507e3fdbb4
commit 08ab90bb47

View File

@@ -0,0 +1,156 @@
package com.videoapp.tv.engine
import com.videoapp.tv.data.SearchResult
import com.videoapp.tv.data.SiteConfig
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.jsoup.Connection
import org.jsoup.Jsoup
abstract class BaseSourceHandler(
override val id: String,
override val displayName: String,
override val baseUrl: String,
protected val config: SiteConfig
) : SourceHandler {
override suspend fun search(
keyword: String,
onResult: suspend (List<SearchResult>) -> Unit,
onError: suspend (String) -> Unit
) {
try {
val results = withContext(Dispatchers.IO) {
val url = "${baseUrl.trimEnd('/')}/${config.searchPath.trimStart('/')}"
val connection: Connection = Jsoup.connect(url)
.data(config.keywordParam, keyword)
.timeout(15000)
config.extraParams.forEach { (key, value) ->
connection.data(key, value)
}
val method = config.searchMethod.uppercase()
val doc = if (method == "GET") {
connection.get()
} else {
connection.post()
}
val items = doc.select(config.resultSelector)
val resultsList = mutableListOf<SearchResult>()
for (item in items) {
try {
val titleEl = item.selectFirst(config.titleSelector) ?: continue
val linkEl = item.selectFirst(config.linkSelector)
val coverEl = item.selectFirst(config.coverSelector)
val categoryEl = item.selectFirst(config.categorySelector)
val dateEl = item.selectFirst(config.dateSelector)
val title = titleEl.text().trim()
val detailUrl = buildFullUrl(linkEl?.attr("href")?.trim() ?: "")
val coverUrl = buildFullUrl(coverEl?.attr("src")?.trim() ?: "")
val category = categoryEl?.text()?.trim() ?: ""
val date = dateEl?.text()?.trim() ?: ""
if (title.isNotEmpty() && detailUrl.isNotEmpty()) {
resultsList.add(
SearchResult(
title = title,
coverUrl = coverUrl,
detailUrl = detailUrl,
category = category,
date = date
)
)
}
} catch (_: Exception) {
}
}
resultsList
}
if (results.isEmpty()) {
onError("未找到结果")
} else {
onResult(results)
}
} catch (e: Exception) {
onError("搜索失败: ${e.message}")
}
}
override suspend fun extractVideos(detailUrl: String): List<PlaySource> =
withContext(Dispatchers.IO) {
val doc = Jsoup.connect(detailUrl).timeout(15000).get()
val sourceTabs = doc.select(config.sourceSelector)
val sourceNames = sourceTabs.map { it.text().trim() }.filter { it.isNotEmpty() }
val episodeGroups = doc.select(config.sourceEpisodeGroupSelector)
val sources = mutableListOf<PlaySource>()
if (sourceNames.isNotEmpty() && episodeGroups.size >= sourceNames.size) {
sourceNames.forEachIndexed { i, name ->
val group = episodeGroups[i]
val episodes = extractEpisodes(group, config)
if (episodes.isNotEmpty()) {
sources.add(PlaySource(name, episodes))
}
}
}
if (sources.isEmpty()) {
val episodes = extractEpisodes(doc, config)
if (episodes.isNotEmpty()) {
sources.add(PlaySource("默认来源", episodes))
}
}
sources
}
override suspend fun resolvePlayUrl(playUrl: String): Pair<String?, String?> =
withContext(Dispatchers.IO) {
try {
val doc = Jsoup.connect(playUrl).timeout(15000).get()
val iframeEl = doc.selectFirst(config.iframeSelector)
var iframeUrl = iframeEl?.attr("src")
if (iframeUrl != null && iframeUrl.startsWith("//")) {
iframeUrl = "https:$iframeUrl"
}
val videoSrc = doc.selectFirst(config.videoSelector)
val directUrl = videoSrc?.attr("src")
Pair(directUrl, iframeUrl)
} catch (_: Exception) {
Pair(null, null)
}
}
private fun extractEpisodes(
container: org.jsoup.nodes.Element,
config: SiteConfig
): List<Episode> {
val episodes = mutableListOf<Episode>()
val eps = container.select(config.episodeSelector)
for (ep in eps) {
val title = ep.text().trim()
val href = ep.attr("href").trim()
if (title.isNotEmpty() && href.isNotEmpty()) {
episodes.add(Episode(title, buildFullUrl(href)))
}
}
return episodes
}
protected fun buildFullUrl(href: String): String {
if (href.isEmpty()) return ""
if (href.startsWith("http")) return href
return "${baseUrl.trimEnd('/')}/${href.trimStart('/')}"
}
}