feat: multi-source player with episode selection and auto-hide controls
- VideoExtractor: extract PlaySource (source tabs) with episodes grouped per source - SiteConfig: add sourceSelector and sourceEpisodeGroupSelector CSS selectors - PlayerActivity: source tabs + episode list at bottom, auto-hide after 4s, tap to toggle - SettingsActivity: add source selector configuration fields - Fullscreen playback with ExoPlayer or WebView fallback
This commit is contained in:
@@ -1,11 +1,14 @@
|
|||||||
package com.videoapp.tv
|
package com.videoapp.tv
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.os.Handler
|
||||||
|
import android.os.Looper
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.webkit.WebView
|
import android.webkit.WebView
|
||||||
import android.webkit.WebViewClient
|
import android.webkit.WebViewClient
|
||||||
import android.widget.Button
|
import android.widget.Button
|
||||||
import android.widget.ImageButton
|
import android.widget.ImageButton
|
||||||
|
import android.widget.LinearLayout
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
@@ -14,6 +17,7 @@ import androidx.media3.exoplayer.ExoPlayer
|
|||||||
import androidx.media3.ui.PlayerView
|
import androidx.media3.ui.PlayerView
|
||||||
import com.videoapp.tv.data.ConfigRepository
|
import com.videoapp.tv.data.ConfigRepository
|
||||||
import com.videoapp.tv.engine.Episode
|
import com.videoapp.tv.engine.Episode
|
||||||
|
import com.videoapp.tv.engine.PlaySource
|
||||||
import com.videoapp.tv.engine.VideoExtractor
|
import com.videoapp.tv.engine.VideoExtractor
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
@@ -21,8 +25,9 @@ class PlayerActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
private lateinit var playerView: PlayerView
|
private lateinit var playerView: PlayerView
|
||||||
private lateinit var playerWebView: WebView
|
private lateinit var playerWebView: WebView
|
||||||
private lateinit var episodePanel: View
|
private lateinit var controlPanel: View
|
||||||
private lateinit var episodeList: android.widget.LinearLayout
|
private lateinit var sourceList: LinearLayout
|
||||||
|
private lateinit var episodeList: LinearLayout
|
||||||
private lateinit var loadingIndicator: View
|
private lateinit var loadingIndicator: View
|
||||||
private lateinit var errorText: android.widget.TextView
|
private lateinit var errorText: android.widget.TextView
|
||||||
private lateinit var btnClose: ImageButton
|
private lateinit var btnClose: ImageButton
|
||||||
@@ -31,8 +36,13 @@ class PlayerActivity : AppCompatActivity() {
|
|||||||
private val videoExtractor = VideoExtractor()
|
private val videoExtractor = VideoExtractor()
|
||||||
private val configRepo by lazy { ConfigRepository(this) }
|
private val configRepo by lazy { ConfigRepository(this) }
|
||||||
|
|
||||||
private var episodes: List<Episode> = emptyList()
|
private var sources: List<PlaySource> = emptyList()
|
||||||
private var currentEpisodeIndex = 0
|
private var currentSourceIndex = 0
|
||||||
|
private var currentEpisode: Episode? = null
|
||||||
|
|
||||||
|
private val hideHandler = Handler(Looper.getMainLooper())
|
||||||
|
private val hideRunnable = Runnable { hideControls() }
|
||||||
|
private var controlsVisible = true
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
@@ -40,19 +50,19 @@ class PlayerActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
playerView = findViewById(R.id.player_view)
|
playerView = findViewById(R.id.player_view)
|
||||||
playerWebView = findViewById(R.id.player_webview)
|
playerWebView = findViewById(R.id.player_webview)
|
||||||
episodePanel = findViewById(R.id.episode_panel)
|
controlPanel = findViewById(R.id.control_panel)
|
||||||
|
sourceList = findViewById(R.id.source_list)
|
||||||
episodeList = findViewById(R.id.episode_list)
|
episodeList = findViewById(R.id.episode_list)
|
||||||
loadingIndicator = findViewById(R.id.loading_indicator)
|
loadingIndicator = findViewById(R.id.loading_indicator)
|
||||||
errorText = findViewById(R.id.error_text)
|
errorText = findViewById(R.id.error_text)
|
||||||
btnClose = findViewById(R.id.btn_close)
|
btnClose = findViewById(R.id.btn_close)
|
||||||
|
|
||||||
val detailUrl = intent.getStringExtra("detail_url") ?: ""
|
val detailUrl = intent.getStringExtra("detail_url") ?: ""
|
||||||
val title = intent.getStringExtra("title") ?: ""
|
|
||||||
|
|
||||||
btnClose.setOnClickListener { finish() }
|
btnClose.setOnClickListener { finish() }
|
||||||
|
|
||||||
initExoPlayer()
|
initExoPlayer()
|
||||||
loadVideos(detailUrl, title)
|
loadSources(detailUrl)
|
||||||
setupTouchListeners()
|
setupTouchListeners()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,36 +72,33 @@ class PlayerActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun loadVideos(detailUrl: String, @Suppress("UNUSED_PARAMETER") title: String) {
|
private fun loadSources(detailUrl: String) {
|
||||||
showLoading(true)
|
showLoading(true)
|
||||||
val config = configRepo.getConfig()
|
val config = configRepo.getConfig()
|
||||||
|
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
try {
|
try {
|
||||||
val videoInfo = videoExtractor.extractVideos(detailUrl, config)
|
sources = videoExtractor.extractVideos(detailUrl, config)
|
||||||
episodes = videoInfo.episodes
|
|
||||||
|
|
||||||
if (videoInfo.episodes.isNotEmpty()) {
|
if (sources.isNotEmpty()) {
|
||||||
buildEpisodeUI(videoInfo.episodes)
|
buildSourceUI()
|
||||||
playEpisode(videoInfo.episodes.first())
|
selectSource(0)
|
||||||
|
resetAutoHide()
|
||||||
} else {
|
} else {
|
||||||
episodePanel.visibility = View.GONE
|
|
||||||
tryPlayDirectly(detailUrl, config)
|
tryPlayDirectly(detailUrl, config)
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
showError("加载视频失败: ${e.message}")
|
showError("加载失败: ${e.message}")
|
||||||
showLoading(false)
|
showLoading(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun buildEpisodeUI(eps: List<Episode>) {
|
private fun buildSourceUI() {
|
||||||
episodeList.removeAllViews()
|
sourceList.removeAllViews()
|
||||||
episodePanel.visibility = View.VISIBLE
|
sources.forEachIndexed { index, source ->
|
||||||
|
|
||||||
eps.forEachIndexed { index, ep ->
|
|
||||||
val btn = Button(this).apply {
|
val btn = Button(this).apply {
|
||||||
text = ep.title
|
text = source.name
|
||||||
setBackgroundResource(R.drawable.episode_selector)
|
setBackgroundResource(R.drawable.episode_selector)
|
||||||
setTextColor(ContextCompat.getColor(this@PlayerActivity, R.color.text_primary))
|
setTextColor(ContextCompat.getColor(this@PlayerActivity, R.color.text_primary))
|
||||||
textSize = 13f
|
textSize = 13f
|
||||||
@@ -100,9 +107,49 @@ class PlayerActivity : AppCompatActivity() {
|
|||||||
isFocusable = true
|
isFocusable = true
|
||||||
isFocusableInTouchMode = true
|
isFocusableInTouchMode = true
|
||||||
setOnClickListener {
|
setOnClickListener {
|
||||||
currentEpisodeIndex = index
|
selectSource(index)
|
||||||
|
resetAutoHide()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sourceList.addView(btn)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun selectSource(index: Int) {
|
||||||
|
if (index !in sources.indices) return
|
||||||
|
currentSourceIndex = index
|
||||||
|
|
||||||
|
// Highlight selected source
|
||||||
|
for (i in 0 until sourceList.childCount) {
|
||||||
|
sourceList.getChildAt(i).isSelected = (i == index)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build episode list for this source
|
||||||
|
val source = sources[index]
|
||||||
|
buildEpisodeUI(source.episodes)
|
||||||
|
|
||||||
|
// Auto-play first episode
|
||||||
|
if (source.episodes.isNotEmpty()) {
|
||||||
|
playEpisode(source.episodes.first())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun buildEpisodeUI(eps: List<Episode>) {
|
||||||
|
episodeList.removeAllViews()
|
||||||
|
eps.forEach { ep ->
|
||||||
|
val btn = Button(this).apply {
|
||||||
|
text = ep.title
|
||||||
|
setBackgroundResource(R.drawable.episode_selector)
|
||||||
|
setTextColor(ContextCompat.getColor(this@PlayerActivity, R.color.text_primary))
|
||||||
|
textSize = 12f
|
||||||
|
minWidth = 0
|
||||||
|
setPadding(14, 6, 14, 6)
|
||||||
|
isFocusable = true
|
||||||
|
isFocusableInTouchMode = true
|
||||||
|
setOnClickListener {
|
||||||
highlightEpisode(it)
|
highlightEpisode(it)
|
||||||
playEpisode(ep)
|
playEpisode(ep)
|
||||||
|
resetAutoHide()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
episodeList.addView(btn)
|
episodeList.addView(btn)
|
||||||
@@ -110,6 +157,7 @@ class PlayerActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun playEpisode(ep: Episode) {
|
private fun playEpisode(ep: Episode) {
|
||||||
|
currentEpisode = ep
|
||||||
showLoading(true)
|
showLoading(true)
|
||||||
val config = configRepo.getConfig()
|
val config = configRepo.getConfig()
|
||||||
|
|
||||||
@@ -130,10 +178,13 @@ class PlayerActivity : AppCompatActivity() {
|
|||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
val (directUrl, iframeUrl) = videoExtractor.extractFromPlayPage(detailUrl, config)
|
val (directUrl, iframeUrl) = videoExtractor.extractFromPlayPage(detailUrl, config)
|
||||||
if (directUrl != null) {
|
if (directUrl != null) {
|
||||||
|
controlPanel.visibility = View.GONE
|
||||||
playWithExoPlayer(directUrl)
|
playWithExoPlayer(directUrl)
|
||||||
} else if (iframeUrl != null) {
|
} else if (iframeUrl != null) {
|
||||||
|
controlPanel.visibility = View.GONE
|
||||||
playWithWebView(iframeUrl)
|
playWithWebView(iframeUrl)
|
||||||
} else {
|
} else {
|
||||||
|
controlPanel.visibility = View.GONE
|
||||||
playWithWebView(detailUrl)
|
playWithWebView(detailUrl)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -186,15 +237,38 @@ class PlayerActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun setupTouchListeners() {
|
private fun setupTouchListeners() {
|
||||||
var panelVisible = true
|
val listener = View.OnClickListener { toggleControls() }
|
||||||
playerView.setOnClickListener {
|
playerView.setOnClickListener(listener)
|
||||||
panelVisible = !panelVisible
|
playerWebView.setOnClickListener(listener)
|
||||||
episodePanel.visibility = if (panelVisible) View.VISIBLE else View.GONE
|
}
|
||||||
|
|
||||||
|
private fun toggleControls() {
|
||||||
|
if (controlsVisible) {
|
||||||
|
hideControls()
|
||||||
|
} else {
|
||||||
|
showControls()
|
||||||
|
resetAutoHide()
|
||||||
}
|
}
|
||||||
playerWebView.setOnClickListener {
|
}
|
||||||
panelVisible = !panelVisible
|
|
||||||
episodePanel.visibility = if (panelVisible) View.VISIBLE else View.GONE
|
private fun showControls() {
|
||||||
|
controlsVisible = true
|
||||||
|
btnClose.visibility = View.VISIBLE
|
||||||
|
controlPanel.visibility = View.VISIBLE
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun hideControls() {
|
||||||
|
controlsVisible = false
|
||||||
|
btnClose.visibility = View.GONE
|
||||||
|
controlPanel.visibility = View.GONE
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun resetAutoHide() {
|
||||||
|
hideHandler.removeCallbacks(hideRunnable)
|
||||||
|
if (!controlsVisible) {
|
||||||
|
showControls()
|
||||||
}
|
}
|
||||||
|
hideHandler.postDelayed(hideRunnable, 4000)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPause() {
|
override fun onPause() {
|
||||||
@@ -204,6 +278,7 @@ class PlayerActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
|
hideHandler.removeCallbacks(hideRunnable)
|
||||||
exoPlayer?.release()
|
exoPlayer?.release()
|
||||||
exoPlayer = null
|
exoPlayer = null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ class SettingsActivity : AppCompatActivity() {
|
|||||||
private lateinit var editTitleSelector: EditText
|
private lateinit var editTitleSelector: EditText
|
||||||
private lateinit var editCoverSelector: EditText
|
private lateinit var editCoverSelector: EditText
|
||||||
private lateinit var editLinkSelector: EditText
|
private lateinit var editLinkSelector: EditText
|
||||||
|
private lateinit var editSourceSelector: EditText
|
||||||
|
private lateinit var editSourceEpisodeGroupSelector: EditText
|
||||||
private lateinit var btnSave: Button
|
private lateinit var btnSave: Button
|
||||||
private lateinit var btnRestore: Button
|
private lateinit var btnRestore: Button
|
||||||
|
|
||||||
@@ -38,6 +40,8 @@ class SettingsActivity : AppCompatActivity() {
|
|||||||
editTitleSelector = findViewById(R.id.edit_title_selector)
|
editTitleSelector = findViewById(R.id.edit_title_selector)
|
||||||
editCoverSelector = findViewById(R.id.edit_cover_selector)
|
editCoverSelector = findViewById(R.id.edit_cover_selector)
|
||||||
editLinkSelector = findViewById(R.id.edit_link_selector)
|
editLinkSelector = findViewById(R.id.edit_link_selector)
|
||||||
|
editSourceSelector = findViewById(R.id.edit_source_selector)
|
||||||
|
editSourceEpisodeGroupSelector = findViewById(R.id.edit_source_episode_group_selector)
|
||||||
btnSave = findViewById(R.id.btn_save)
|
btnSave = findViewById(R.id.btn_save)
|
||||||
btnRestore = findViewById(R.id.btn_restore)
|
btnRestore = findViewById(R.id.btn_restore)
|
||||||
|
|
||||||
@@ -64,6 +68,8 @@ class SettingsActivity : AppCompatActivity() {
|
|||||||
editTitleSelector.setText(config.titleSelector)
|
editTitleSelector.setText(config.titleSelector)
|
||||||
editCoverSelector.setText(config.coverSelector)
|
editCoverSelector.setText(config.coverSelector)
|
||||||
editLinkSelector.setText(config.linkSelector)
|
editLinkSelector.setText(config.linkSelector)
|
||||||
|
editSourceSelector.setText(config.sourceSelector)
|
||||||
|
editSourceEpisodeGroupSelector.setText(config.sourceEpisodeGroupSelector)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun saveConfig() {
|
private fun saveConfig() {
|
||||||
@@ -78,7 +84,9 @@ class SettingsActivity : AppCompatActivity() {
|
|||||||
resultSelector = editResultSelector.text.toString().trim(),
|
resultSelector = editResultSelector.text.toString().trim(),
|
||||||
titleSelector = editTitleSelector.text.toString().trim(),
|
titleSelector = editTitleSelector.text.toString().trim(),
|
||||||
coverSelector = editCoverSelector.text.toString().trim(),
|
coverSelector = editCoverSelector.text.toString().trim(),
|
||||||
linkSelector = editLinkSelector.text.toString().trim()
|
linkSelector = editLinkSelector.text.toString().trim(),
|
||||||
|
sourceSelector = editSourceSelector.text.toString().trim(),
|
||||||
|
sourceEpisodeGroupSelector = editSourceEpisodeGroupSelector.text.toString().trim()
|
||||||
)
|
)
|
||||||
|
|
||||||
configRepo.saveConfig(config)
|
configRepo.saveConfig(config)
|
||||||
|
|||||||
@@ -45,6 +45,12 @@ data class SiteConfig(
|
|||||||
@SerializedName("episode_selector")
|
@SerializedName("episode_selector")
|
||||||
val episodeSelector: String = "a.lBtn",
|
val episodeSelector: String = "a.lBtn",
|
||||||
|
|
||||||
|
@SerializedName("source_selector")
|
||||||
|
val sourceSelector: String = ".playfrom a, .play_source a, .source-list a",
|
||||||
|
|
||||||
|
@SerializedName("source_episode_group_selector")
|
||||||
|
val sourceEpisodeGroupSelector: String = ".playlist > ul, .play_list > ul, .episode-list",
|
||||||
|
|
||||||
@SerializedName("iframe_selector")
|
@SerializedName("iframe_selector")
|
||||||
val iframeSelector: String = ".video iframe",
|
val iframeSelector: String = ".video iframe",
|
||||||
|
|
||||||
|
|||||||
@@ -5,88 +5,68 @@ import kotlinx.coroutines.Dispatchers
|
|||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import org.jsoup.Jsoup
|
import org.jsoup.Jsoup
|
||||||
|
|
||||||
data class VideoInfo(
|
|
||||||
val detailTitle: String,
|
|
||||||
val isSeries: Boolean,
|
|
||||||
val episodes: List<Episode>,
|
|
||||||
val videoUrl: String?,
|
|
||||||
val iframeUrl: String?
|
|
||||||
)
|
|
||||||
|
|
||||||
data class Episode(
|
data class Episode(
|
||||||
val title: String,
|
val title: String,
|
||||||
val playUrl: String
|
val playUrl: String
|
||||||
)
|
)
|
||||||
|
|
||||||
|
data class PlaySource(
|
||||||
|
val name: String,
|
||||||
|
val episodes: List<Episode>
|
||||||
|
)
|
||||||
|
|
||||||
class VideoExtractor {
|
class VideoExtractor {
|
||||||
|
|
||||||
suspend fun extractVideos(
|
suspend fun extractVideos(
|
||||||
detailUrl: String,
|
detailUrl: String,
|
||||||
config: SiteConfig
|
config: SiteConfig
|
||||||
): VideoInfo = withContext(Dispatchers.IO) {
|
): List<PlaySource> = withContext(Dispatchers.IO) {
|
||||||
val doc = Jsoup.connect(detailUrl).timeout(15000).get()
|
val doc = Jsoup.connect(detailUrl).timeout(15000).get()
|
||||||
|
|
||||||
val pageTitle = doc.title()
|
// Extract source tabs
|
||||||
val episodes = mutableListOf<Episode>()
|
val sourceTabs = doc.select(config.sourceSelector)
|
||||||
var iframeUrl: String? = null
|
val sourceNames = sourceTabs.map { it.text().trim() }.filter { it.isNotEmpty() }
|
||||||
var directVideoUrl: String? = null
|
|
||||||
|
|
||||||
// Extract episodes
|
// Extract episode groups (parallel to source tabs)
|
||||||
val episodeEls = doc.select(config.episodeSelector)
|
val episodeGroups = doc.select(config.sourceEpisodeGroupSelector)
|
||||||
for (ep in episodeEls) {
|
|
||||||
|
val sources = mutableListOf<PlaySource>()
|
||||||
|
|
||||||
|
if (sourceNames.isNotEmpty() && episodeGroups.size >= sourceNames.size) {
|
||||||
|
// Pair each source with its episode group
|
||||||
|
sourceNames.forEachIndexed { i, name ->
|
||||||
|
val group = episodeGroups[i]
|
||||||
|
val episodes = extractEpisodesFromGroup(group, config)
|
||||||
|
if (episodes.isNotEmpty()) {
|
||||||
|
sources.add(PlaySource(name, episodes))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: no sources found, try direct episodes
|
||||||
|
if (sources.isEmpty()) {
|
||||||
|
val episodes = extractEpisodesFromGroup(doc, config)
|
||||||
|
if (episodes.isNotEmpty()) {
|
||||||
|
sources.add(PlaySource("默认来源", episodes))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sources
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun extractEpisodesFromGroup(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 title = ep.text().trim()
|
||||||
val href = ep.attr("href")?.trim() ?: ""
|
val href = ep.attr("href").trim()
|
||||||
if (title.isNotEmpty() && href.isNotEmpty()) {
|
if (title.isNotEmpty() && href.isNotEmpty()) {
|
||||||
val fullUrl = if (href.startsWith("http")) href
|
val fullUrl = if (href.startsWith("http")) href
|
||||||
else config.baseUrl.trimEnd('/') + "/" + href.trimStart('/')
|
else config.baseUrl.trimEnd('/') + "/" + href.trimStart('/')
|
||||||
episodes.add(Episode(title, fullUrl))
|
episodes.add(Episode(title, fullUrl))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return episodes
|
||||||
val isSeries = episodes.size > 1
|
|
||||||
|
|
||||||
// If episodes found, try to extract video from first episode's play page
|
|
||||||
if (episodes.isNotEmpty()) {
|
|
||||||
val playUrl = episodes.first().playUrl
|
|
||||||
try {
|
|
||||||
val playDoc = Jsoup.connect(playUrl).timeout(15000).get()
|
|
||||||
|
|
||||||
// Try iframe first
|
|
||||||
val iframeEl = playDoc.selectFirst(config.iframeSelector)
|
|
||||||
if (iframeEl != null) {
|
|
||||||
iframeUrl = iframeEl.attr("src")
|
|
||||||
if (iframeUrl != null && iframeUrl.startsWith("//")) {
|
|
||||||
iframeUrl = "https:$iframeUrl"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Try direct video tag
|
|
||||||
val videoSrc = playDoc.selectFirst(config.videoSelector)
|
|
||||||
if (videoSrc != null) {
|
|
||||||
directVideoUrl = videoSrc.attr("src")
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
// extraction failed, will fallback to WebView
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// No episodes found - might be a movie (single play button)
|
|
||||||
// Try to extract video directly from the detail page
|
|
||||||
val iframeEl = doc.selectFirst(config.iframeSelector)
|
|
||||||
if (iframeEl != null) {
|
|
||||||
iframeUrl = iframeEl.attr("src")
|
|
||||||
if (iframeUrl != null && iframeUrl.startsWith("//")) {
|
|
||||||
iframeUrl = "https:$iframeUrl"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
VideoInfo(
|
|
||||||
detailTitle = pageTitle,
|
|
||||||
isSeries = isSeries,
|
|
||||||
episodes = episodes,
|
|
||||||
videoUrl = directVideoUrl,
|
|
||||||
iframeUrl = iframeUrl
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun extractFromPlayPage(
|
suspend fun extractFromPlayPage(
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import androidx.lifecycle.lifecycleScope
|
|||||||
import androidx.recyclerview.widget.GridLayoutManager
|
import androidx.recyclerview.widget.GridLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.videoapp.tv.R
|
import com.videoapp.tv.R
|
||||||
import com.videoapp.tv.BrowserActivity
|
import com.videoapp.tv.PlayerActivity
|
||||||
import com.videoapp.tv.SettingsActivity
|
import com.videoapp.tv.SettingsActivity
|
||||||
import com.videoapp.tv.data.AppDatabase
|
import com.videoapp.tv.data.AppDatabase
|
||||||
import com.videoapp.tv.data.SearchHistory
|
import com.videoapp.tv.data.SearchHistory
|
||||||
@@ -185,9 +185,10 @@ class SearchFragment : Fragment() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun openPlayer(result: SearchResult) {
|
private fun openPlayer(result: SearchResult) {
|
||||||
val intent = Intent(requireContext(), BrowserActivity::class.java).apply {
|
val intent = Intent(requireContext(), PlayerActivity::class.java).apply {
|
||||||
putExtra("url", result.detailUrl)
|
putExtra("detail_url", result.detailUrl)
|
||||||
putExtra("title", result.title)
|
putExtra("title", result.title)
|
||||||
|
putExtra("category", result.category)
|
||||||
}
|
}
|
||||||
startActivity(intent)
|
startActivity(intent)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,18 +29,36 @@
|
|||||||
android:scaleType="centerInside"
|
android:scaleType="centerInside"
|
||||||
android:contentDescription="@string/back" />
|
android:contentDescription="@string/back" />
|
||||||
|
|
||||||
<FrameLayout
|
<LinearLayout
|
||||||
android:id="@+id/episode_panel"
|
android:id="@+id/control_panel"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="60dp"
|
android:layout_height="wrap_content"
|
||||||
android:layout_gravity="bottom"
|
android:layout_gravity="bottom"
|
||||||
|
android:orientation="vertical"
|
||||||
android:background="#CC000000"
|
android:background="#CC000000"
|
||||||
android:visibility="gone">
|
android:visibility="visible">
|
||||||
|
|
||||||
|
<HorizontalScrollView
|
||||||
|
android:id="@+id/source_scroll"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="44dp"
|
||||||
|
android:fillViewport="true"
|
||||||
|
android:scrollbars="none">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/source_list"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:paddingStart="12dp"
|
||||||
|
android:paddingEnd="12dp" />
|
||||||
|
</HorizontalScrollView>
|
||||||
|
|
||||||
<HorizontalScrollView
|
<HorizontalScrollView
|
||||||
android:id="@+id/episode_scroll"
|
android:id="@+id/episode_scroll"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="50dp"
|
||||||
android:fillViewport="true"
|
android:fillViewport="true"
|
||||||
android:scrollbars="none">
|
android:scrollbars="none">
|
||||||
|
|
||||||
@@ -50,10 +68,10 @@
|
|||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:gravity="center_vertical"
|
android:gravity="center_vertical"
|
||||||
android:orientation="horizontal"
|
android:orientation="horizontal"
|
||||||
android:paddingStart="16dp"
|
android:paddingStart="12dp"
|
||||||
android:paddingEnd="16dp" />
|
android:paddingEnd="12dp" />
|
||||||
</HorizontalScrollView>
|
</HorizontalScrollView>
|
||||||
</FrameLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
<ProgressBar
|
<ProgressBar
|
||||||
android:id="@+id/loading_indicator"
|
android:id="@+id/loading_indicator"
|
||||||
|
|||||||
@@ -176,6 +176,42 @@
|
|||||||
android:padding="16dp"
|
android:padding="16dp"
|
||||||
android:textColor="@color/text_primary"
|
android:textColor="@color/text_primary"
|
||||||
android:textSize="16sp"
|
android:textSize="16sp"
|
||||||
|
android:layout_marginBottom="16dp" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="播放来源选择器"
|
||||||
|
android:textColor="@color/text_secondary"
|
||||||
|
android:textSize="14sp" />
|
||||||
|
|
||||||
|
<EditText
|
||||||
|
android:id="@+id/edit_source_selector"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:background="@drawable/search_bg"
|
||||||
|
android:inputType="text"
|
||||||
|
android:padding="16dp"
|
||||||
|
android:textColor="@color/text_primary"
|
||||||
|
android:textSize="16sp"
|
||||||
|
android:layout_marginBottom="16dp" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="剧集分组选择器"
|
||||||
|
android:textColor="@color/text_secondary"
|
||||||
|
android:textSize="14sp" />
|
||||||
|
|
||||||
|
<EditText
|
||||||
|
android:id="@+id/edit_source_episode_group_selector"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:background="@drawable/search_bg"
|
||||||
|
android:inputType="text"
|
||||||
|
android:padding="16dp"
|
||||||
|
android:textColor="@color/text_primary"
|
||||||
|
android:textSize="16sp"
|
||||||
android:layout_marginBottom="24dp" />
|
android:layout_marginBottom="24dp" />
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
|
|||||||
Reference in New Issue
Block a user