feat: 添加 BaseSourceHandler 抽象基类
This commit is contained in:
156
app/src/main/java/com/videoapp/tv/engine/BaseSourceHandler.kt
Normal file
156
app/src/main/java/com/videoapp/tv/engine/BaseSourceHandler.kt
Normal 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('/')}"
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user