diff --git a/app/src/main/java/com/videoapp/tv/engine/BaseSourceHandler.kt b/app/src/main/java/com/videoapp/tv/engine/BaseSourceHandler.kt new file mode 100644 index 0000000..55273f1 --- /dev/null +++ b/app/src/main/java/com/videoapp/tv/engine/BaseSourceHandler.kt @@ -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) -> 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() + + 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 = + 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() + + 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 = + 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 { + val episodes = mutableListOf() + 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('/')}" + } +}