From f1efc5dc5d0b3e9840406a812a6cec5ac3a113f0 Mon Sep 17 00:00:00 2001 From: Hermes Agent Date: Thu, 8 Oct 2026 13:26:35 +0800 Subject: [PATCH] =?UTF-8?q?v8.8.0=20=E5=88=87=E5=8F=B0=E6=8F=90=E9=80=9F?= =?UTF-8?q?=EF=BC=9A=E6=AF=8F=E9=A2=91=E9=81=93=E7=8B=AC=E7=AB=8B=E9=94=81?= =?UTF-8?q?(=E9=A2=84=E5=8F=96=E4=B8=8D=E5=86=8D=E9=98=BB=E5=A1=9E)+TTL30m?= =?UTF-8?q?in+=E5=8F=AA=E9=A2=84=E5=8F=96=E9=AB=98=E4=BA=AE=E9=A2=91?= =?UTF-8?q?=E9=81=93+=E7=BC=93=E5=AD=98=E5=A4=B1=E6=95=88=E8=87=AA?= =?UTF-8?q?=E5=8A=A8=E5=88=B7=E6=96=B0=E6=BA=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/build.gradle.kts | 4 +- .../main/java/com/iptv807/tv/MainActivity.kt | 62 ++++++++++++++----- .../java/com/iptv807/tv/SourceResolver.kt | 13 +++- 3 files changed, 58 insertions(+), 21 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 90a6b60..0e95081 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -11,8 +11,8 @@ android { applicationId = "com.iptv807.tv" minSdk = 21 targetSdk = 35 - versionCode = 870 - versionName = "8.7.0" + versionCode = 880 + versionName = "8.8.0" } buildTypes { diff --git a/app/src/main/java/com/iptv807/tv/MainActivity.kt b/app/src/main/java/com/iptv807/tv/MainActivity.kt index fa3d2b8..c6b4046 100644 --- a/app/src/main/java/com/iptv807/tv/MainActivity.kt +++ b/app/src/main/java/com/iptv807/tv/MainActivity.kt @@ -67,7 +67,8 @@ class MainActivity : Activity() { override fun run() { if (loadingMask.visibility != View.VISIBLE) return val s = ((System.currentTimeMillis() - loadStart) / 1000 + 1).toInt() - loadingText.text = "正在加载 $s 秒,精彩继续…" + loadingText.text = if (loadChannelName.isEmpty()) "正在加载 $s 秒,精彩继续…" + else "正在加载「$loadChannelName」$s 秒…" handler.postDelayed(this, 1000) } } @@ -81,6 +82,8 @@ class MainActivity : Activity() { private var isVip = false private var lastPlayError = "" private var lastSelfTest = "" + private var loadChannelName = "" + private var triedFresh = false private var failureReport = "" private var wasStopped = false private var resumeBtn: TextView? = null @@ -228,10 +231,8 @@ class MainActivity : Activity() { if (savedInstanceState == null && channels.isNotEmpty()) { val defIdx = channels.indexOfFirst { it.name.contains("TVB翡翠台") }.let { if (it >= 0) it else 0 } playChannel(defIdx) - // 启动后台预取默认频道所在分组,之后切台秒开 - val g = channels.getOrNull(defIdx)?.group - val gi = ChannelData.groups().indexOfFirst { it.name == g } - if (gi >= 0) handler.postDelayed({ prefetchGroup(gi) }, 2500) + // 启动后预取默认频道的相邻频道(很轻量,1 个一个来) + handler.postDelayed({ schedulePrefetch(channels.getOrNull(defIdx)) }, 2000) } } @@ -422,6 +423,13 @@ class MainActivity : Activity() { if (ev.action == MotionEvent.ACTION_DOWN || ev.action == MotionEvent.ACTION_MOVE) resetHideTimer() v.onTouchEvent(ev) } + onItemSelectedListener = object : AdapterView.OnItemSelectedListener { + override fun onItemSelected(parent: AdapterView<*>?, v: View?, pos: Int, id: Long) { + val list = channelsOfGroup(curGroupPos) + if (pos in list.indices) schedulePrefetch(list[pos]) + } + override fun onNothingSelected(parent: AdapterView<*>?) { } + } setOnScrollListener(object : AbsListView.OnScrollListener { override fun onScrollStateChanged(v: AbsListView, s: Int) { resetHideTimer() } override fun onScroll(v: AbsListView, a: Int, b: Int, c: Int) { resetHideTimer() } @@ -432,26 +440,27 @@ class MainActivity : Activity() { } private fun updateChannelListOfGroup(pos: Int) { - prefetchGroup(pos) val list = channelsOfGroup(pos) channelList?.adapter = RowAdapter(list.map { "%3d %s".format(it.num, it.name) }) } private fun hidePanel() { handler.removeCallbacks(hidePanelTask); panelLayout?.visibility = View.GONE } - /** 后台预取某分组所有频道的源,让切台变秒切(每次运行只做一轮) */ - private var prefetchedGroups = HashSet() - private fun prefetchGroup(gpos: Int) { - if (prefetchedGroups.contains(gpos)) return - prefetchedGroups.add(gpos) - val list = channelsOfGroup(gpos) + /** 只预取"当前高亮的那一个频道"(1.2 秒去抖动)——源站很脆,不做整组预取 */ + private val prefetchTask = Runnable { + val ch = prefetchTarget ?: return@Runnable + if (SourceResolver.hasCache(ch.tid, ch.id)) return@Runnable Thread { - for (ch in list) { - try { SourceResolver.resolveLines(ch.tid, ch.id) } catch (e: Exception) { } - try { Thread.sleep(250) } catch (ie: InterruptedException) { } - } + try { SourceResolver.resolveLines(ch.tid, ch.id) } catch (e: Exception) { } }.start() } + private var prefetchTarget: ChannelData.Channel? = null + private fun schedulePrefetch(ch: ChannelData.Channel?) { + if (ch == null) return + prefetchTarget = ch + handler.removeCallbacks(prefetchTask) + handler.postDelayed(prefetchTask, 1200) + } private fun resetHideTimer() { handler.removeCallbacks(hidePanelTask) handler.postDelayed(hidePanelTask, 5000) @@ -473,6 +482,8 @@ class MainActivity : Activity() { val isSameChannel = (idx == currentChannel) currentChannel = idx; currentLine = line val ch = channels[idx] + if (!isSameChannel) triedFresh = false + loadChannelName = ch.name if (!isSameChannel || loadingMask.visibility != View.VISIBLE) { beginLoading() // 只有换台才重置计时;同台换线路时继续计时 } @@ -565,8 +576,25 @@ class MainActivity : Activity() { val ch = channels.getOrNull(currentChannel) ?: return currentLine++ if (currentLine > 12) { + // 可能是缓存里的 token 过期了 → 强制刷新一次源再试一轮 + if (!triedFresh) { + triedFresh = true + statusText.text = "线路失效,正在刷新源…" + Thread { + val fresh = try { SourceResolver.resolveFresh(ch.tid, ch.id) } catch (e: Exception) { null } + runOnUiThread { + if (!fresh.isNullOrEmpty()) { currentLine = 0; playChannel(currentChannel, 0) } + else { + val vn = try { packageManager.getPackageInfo(packageName, 0).versionName } catch (e: Exception) { "?" } + statusText.text = "⚠ ${ch.num} ${ch.name} 全部线路失败\n$lastPlayError\n解析: ${SourceResolver.lastError}\n版本: v$vn" + hideLoading() + } + } + }.start() + return + } val vn = try { packageManager.getPackageInfo(packageName, 0).versionName } catch (e: Exception) { "?" } - statusText.text = "⚠ ${ch.num} ${ch.name} 全部线路失败\n$lastPlayError\n解析: ${SourceResolver.lastError}\n$failureReport\n版本: v$vn" + statusText.text = "⚠ ${ch.num} ${ch.name} 全部线路失败\n$lastPlayError\n解析: ${SourceResolver.lastError}\n版本: v$vn" hideLoading() return } diff --git a/app/src/main/java/com/iptv807/tv/SourceResolver.kt b/app/src/main/java/com/iptv807/tv/SourceResolver.kt index e066821..6c6fa46 100644 --- a/app/src/main/java/com/iptv807/tv/SourceResolver.kt +++ b/app/src/main/java/com/iptv807/tv/SourceResolver.kt @@ -27,7 +27,9 @@ object SourceResolver { @Volatile private var cache: MutableMap? = null private data class Cached(val lines: List, val ts: Long) - private const val TTL = 8 * 60 * 1000L + /** 每频道独立锁:避免后台预取把用户切台的请求挡住 */ + private val locks = ConcurrentHashMap() + private const val TTL = 30 * 60 * 1000L private const val BASE = "https://m.iptv807.com/" private const val UA = "Mozilla/5.0 (Linux; Android 12; SM-S901B) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Mobile Safari/537.36" @@ -56,6 +58,12 @@ object SourceResolver { /** 兼容之前的 method signature */ fun resolve(tid: String, id: String): List? = resolveLines(tid, id) + /** 该频道是否已有可用缓存(不触发网络请求) */ + fun hasCache(tid: String, id: String): Boolean { + val c = cache?.get("$tid-$id") ?: return false + return System.currentTimeMillis() - c.ts < TTL + } + /** 强制丢弃缓存重新抓源(用于"缓存线路全失效"时) */ fun resolveFresh(tid: String, id: String): List? { cache?.remove("$tid-$id") @@ -66,7 +74,8 @@ object SourceResolver { val key = "$tid-$id" cache?.get(key)?.let { if (System.currentTimeMillis() - it.ts < TTL) return it.lines } - synchronized(this) { + val lock = locks.getOrPut(key) { Any() } + synchronized(lock) { // double-check cache?.get(key)?.let { if (System.currentTimeMillis() - it.ts < TTL) return it.lines } try {