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