From dea2700da19122cbf611401a975c6328c6e093c3 Mon Sep 17 00:00:00 2001 From: xiaji Date: Tue, 9 Jun 2026 19:52:29 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=20TvcatHandler?= =?UTF-8?q?=EF=BC=8C=E6=94=AF=E6=8C=81=20/=5Ffetch=5Fp/=20API=20=E8=8E=B7?= =?UTF-8?q?=E5=8F=96=E6=92=AD=E6=94=BE=E9=93=BE=E6=8E=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../videoapp/tv/engine/tvcat/TvcatHandler.kt | 75 +++++++++++++++++++ 1 file changed, 75 insertions(+) create mode 100644 app/src/main/java/com/videoapp/tv/engine/tvcat/TvcatHandler.kt diff --git a/app/src/main/java/com/videoapp/tv/engine/tvcat/TvcatHandler.kt b/app/src/main/java/com/videoapp/tv/engine/tvcat/TvcatHandler.kt new file mode 100644 index 0000000..157e24b --- /dev/null +++ b/app/src/main/java/com/videoapp/tv/engine/tvcat/TvcatHandler.kt @@ -0,0 +1,75 @@ +package com.videoapp.tv.engine.tvcat + +import com.videoapp.tv.data.SiteConfig +import com.videoapp.tv.engine.BaseSourceHandler +import com.videoapp.tv.engine.Episode +import com.videoapp.tv.engine.PlaySource +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import org.json.JSONObject +import org.jsoup.Jsoup +import java.net.URL + +class TvcatHandler : BaseSourceHandler( + id = "tvcat", + displayName = "tvcat (电视猫)", + baseUrl = "https://tvcat.cc", + config = SiteConfig( + baseUrl = "https://tvcat.cc", + searchPath = "/search", + searchMethod = "GET", + keywordParam = "q", + resultSelector = "li.col-md-2.col-sm-3.col-4", + titleSelector = "a[title]", + coverSelector = "img", + linkSelector = "a", + categorySelector = ".text-muted", + dateSelector = "", + episodeSelector = "li.list-inline-item a", + sourceSelector = "", + sourceEpisodeGroupSelector = "", + iframeSelector = "iframe", + videoSelector = "video source, video[src]" + ) +) { + override suspend fun extractVideos(detailUrl: String): List = + withContext(Dispatchers.IO) { + val doc = Jsoup.connect(detailUrl).timeout(15000).get() + val episodes = doc.select("li.list-inline-item a").mapNotNull { ep -> + val title = ep.text().trim() + val href = ep.attr("href").trim() + if (title.isNotEmpty() && href.isNotEmpty()) { + Episode(title, buildFullUrl(href)) + } else null + } + if (episodes.isNotEmpty()) { + listOf(PlaySource("默认来源", episodes)) + } else emptyList() + } + + override suspend fun resolvePlayUrl(playUrl: String): Pair = + withContext(Dispatchers.IO) { + try { + val match = Regex("""(\d+)/ep(\d+)""").find(playUrl) + if (match != null) { + val videoId = match.groupValues[1] + val epNum = match.groupValues[2] + val apiUrl = "https://tvcat.cc/_fetch_p/$videoId/ep$epNum" + val conn = URL(apiUrl).openConnection() + conn.setRequestProperty("User-Agent", "Mozilla/5.0") + conn.setRequestProperty("Referer", playUrl) + conn.connectTimeout = 15000 + conn.readTimeout = 15000 + val json = conn.getInputStream().bufferedReader().use { it.readText() } + val obj = JSONObject(json) + val playcfgs = obj.getJSONArray("playcfgs") + if (playcfgs.length() > 0) { + val url = playcfgs.getJSONObject(0).getString("url") + Pair(url, null) + } else Pair(null, null) + } else Pair(null, null) + } catch (_: Exception) { + Pair(null, null) + } + } +}