feat: 添加 TvcatHandler,支持 /_fetch_p/ API 获取播放链接

This commit is contained in:
xiaji
2026-06-09 19:52:29 +08:00
parent a75225332b
commit dea2700da1

View File

@@ -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<PlaySource> =
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<String?, String?> =
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)
}
}
}