From 74090049eee586efe753b54551dc59781e070747 Mon Sep 17 00:00:00 2001 From: Li ZongYing Date: Wed, 22 Jan 2025 22:53:59 +0800 Subject: [PATCH] add logo cache --- HISTORY.md | 11 ++ .../java/com/lizongying/mytv0/ImageHelper.kt | 159 +++++++++++------- .../java/com/lizongying/mytv0/InfoFragment.kt | 18 +- .../java/com/lizongying/mytv0/ListAdapter.kt | 26 ++- .../com/lizongying/mytv0/MainViewModel.kt | 75 ++++++--- .../com/lizongying/mytv0/MyTVApplication.kt | 4 + .../lizongying/mytv0/MyTVExceptionHandler.kt | 9 +- app/src/main/java/com/lizongying/mytv0/SP.kt | 3 +- .../java/com/lizongying/mytv0/SimpleServer.kt | 8 +- .../main/java/com/lizongying/mytv0/Utils.kt | 14 +- version.json | 2 +- 11 files changed, 195 insertions(+), 134 deletions(-) diff --git a/HISTORY.md b/HISTORY.md index 65b2566b..81cde67c 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,5 +1,16 @@ ## 更新日誌 +### v1.3.8.16 + +* 增加LOGO緩存 +* 支持設置多個EPG地址 + +### v1.3.8.15-kitkat + +* 修復頻道記憶失敗的問題 +* 打開頻道列表同時顯示組 +* 增加EPG緩存 + ### v1.3.8.15 * 修復頻道記憶失敗的問題 diff --git a/app/src/main/java/com/lizongying/mytv0/ImageHelper.kt b/app/src/main/java/com/lizongying/mytv0/ImageHelper.kt index 152d503b..84607cf9 100644 --- a/app/src/main/java/com/lizongying/mytv0/ImageHelper.kt +++ b/app/src/main/java/com/lizongying/mytv0/ImageHelper.kt @@ -3,73 +3,106 @@ package com.lizongying.mytv0 import android.content.Context import android.graphics.Bitmap import android.graphics.drawable.BitmapDrawable -import android.graphics.drawable.Drawable -import android.os.Handler +import android.util.Log import android.widget.ImageView import com.bumptech.glide.Glide -import com.bumptech.glide.load.DataSource -import com.bumptech.glide.load.engine.GlideException -import com.bumptech.glide.request.RequestListener -import com.bumptech.glide.request.target.Target +import com.lizongying.mytv0.requests.HttpClient +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import java.io.File -fun loadNextUrl( - context: Context, - imageView: ImageView, - bitmap: Bitmap, - urlList: List, - index: Int, - handler: Handler, - onSuccess: (Int) -> Unit -) { - if (urlList.isEmpty()) { - return - } - if (index >= urlList.size) { - return + +class ImageHelper(private val context: Context) { + val cacheDir = context.cacheDir + val files: MutableMap = mutableMapOf() + + init { + val dir = File(cacheDir, LOGO) + if (!dir.exists()) { + dir.mkdir() + } + dir.listFiles()?.forEach { file -> + val name = file.name.substringBeforeLast(".") + files[name] = file + } } - val url = urlList[index] - if (url.isEmpty()) { - Glide.with(context) - .load(bitmap) - .fitCenter() - .into(imageView) - } else { - Glide.with(context) - .load(url) - .listener(object : RequestListener { - override fun onResourceReady( - resource: Drawable, - model: Any, - target: Target?, - dataSource: DataSource, - isFirstResource: Boolean - ): Boolean { - onSuccess(index) - return false - } - override fun onLoadFailed( - e: GlideException?, - model: Any?, - target: Target, - isFirstResource: Boolean - ): Boolean { - handler.post { - loadNextUrl( - context, - imageView, - bitmap, - urlList, - index + 1, - handler, - onSuccess - ) - } - return true + private suspend fun downloadImage(url: String, file: File): Boolean { + return withContext(Dispatchers.IO) { + try { + val request = okhttp3.Request.Builder() + .url(url) + .build() + + HttpClient.okHttpClient.newCall(request).execute().use { response -> + if (!response.isSuccessful) return@withContext false + response.bodyAlias()?.byteStream()?.copyTo(file.outputStream()) + Log.i(TAG, "downloadImage success $url") + true } - }) - .placeholder(BitmapDrawable(context.resources, bitmap)) - .fitCenter() - .into(imageView) + } catch (e: Exception) { + Log.e(TAG, "downloadImage", e) + false + } + } + } + + suspend fun preloadImage( + key: String, + urlList: List, + ) { + val file = files[key] + if (file != null) { + Log.i(TAG, "image exists ${file.absolutePath}") + return + } + + if (urlList.isEmpty()) { + return + } + + for (url in urlList) { + val ext = url.substringAfterLast(".") + val file = File(cacheDir, "$LOGO/$key.$ext") + if (downloadImage(url, file)) { + Log.i(TAG, "image download success ${file.absolutePath}") + break + } + } + } + + fun loadImage( + key: String, + imageView: ImageView, + bitmap: Bitmap, + url: String, + ) { + val file = files[key] + if (file != null) { + Log.i(TAG, "image exists ${file.absolutePath}") + Glide.with(context) + .load(file) + .fitCenter() + .into(imageView) + return + } + + if (url.isEmpty()) { + Glide.with(context) + .load(bitmap) + .fitCenter() + .into(imageView) + } else { + Glide.with(context) + .load(url) + .placeholder(BitmapDrawable(context.resources, bitmap)) + .fitCenter() + .into(imageView) + } + } + + companion object { + const val TAG = "ImageHelper" + const val LOGO = "logo" } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/lizongying/mytv0/InfoFragment.kt b/app/src/main/java/com/lizongying/mytv0/InfoFragment.kt index c76eaf52..0f4a37e5 100644 --- a/app/src/main/java/com/lizongying/mytv0/InfoFragment.kt +++ b/app/src/main/java/com/lizongying/mytv0/InfoFragment.kt @@ -14,7 +14,6 @@ import androidx.core.view.marginBottom import androidx.core.view.marginStart import androidx.core.view.marginTop import androidx.fragment.app.Fragment -import com.lizongying.mytv0.Utils.getUrls import com.lizongying.mytv0.databinding.InfoBinding import com.lizongying.mytv0.models.TVModel @@ -79,6 +78,8 @@ class InfoFragment : Fragment() { } val context = requireContext() + val application = context.applicationContext as MyTVApplication + val imageHelper = application.imageHelper binding.title.text = tvModel.tv.title @@ -107,17 +108,12 @@ class InfoFragment : Fragment() { canvas.drawText(channelNum.toString(), x, y, paint) val url = tvModel.tv.logo - val name = tvModel.tv.name - var urls = - getUrls( - "live.fanmingming.com/tv/$name.png" - ) + getUrls("https://raw.githubusercontent.com/fanmingming/live/main/tv/$name.png") - if (url.isNotEmpty()) { - urls = (getUrls(url) + urls).distinct() - } - loadNextUrl(context, binding.logo, bitmap, urls, 0, handler) { - tvModel.tv.logo = urls[it] + var name = tvModel.tv.name + if (name.isEmpty()) { + name = tvModel.tv.title } + + imageHelper.loadImage(name, binding.logo, bitmap, url) } } diff --git a/app/src/main/java/com/lizongying/mytv0/ListAdapter.kt b/app/src/main/java/com/lizongying/mytv0/ListAdapter.kt index 9cdbb4d5..ffb32add 100644 --- a/app/src/main/java/com/lizongying/mytv0/ListAdapter.kt +++ b/app/src/main/java/com/lizongying/mytv0/ListAdapter.kt @@ -4,8 +4,6 @@ import android.content.Context import android.graphics.Bitmap import android.graphics.Canvas import android.graphics.Paint -import android.os.Handler -import android.os.Looper import android.util.Log import android.view.KeyEvent import android.view.LayoutInflater @@ -18,7 +16,6 @@ import androidx.core.content.ContextCompat import androidx.core.view.setPadding import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView -import com.lizongying.mytv0.Utils.getUrls import com.lizongying.mytv0.databinding.ListItemBinding import com.lizongying.mytv0.models.TVListModel import com.lizongying.mytv0.models.TVModel @@ -187,7 +184,11 @@ class ListAdapter( viewHolder.bindTitle(tvModel.tv.title) - viewHolder.bindImage(tvModel.tv.logo, tvModel.tv.id, tvModel.tv.name, tvModel) + var name = tvModel.tv.name + if (name.isEmpty()) { + name = tvModel.tv.title + } + viewHolder.bindImage(tvModel.tv.logo, tvModel.tv.id, name, tvModel) } } @@ -196,13 +197,15 @@ class ListAdapter( class ViewHolder(private val context: Context, val binding: ListItemBinding) : RecyclerView.ViewHolder(binding.root) { - val handler = Handler(Looper.getMainLooper()) + val application = context.applicationContext as MyTVApplication + val imageHelper = application.imageHelper + fun bindTitle(text: String) { binding.title.text = text } - fun bindImage(url: String?, id: Int, name: String, tvModel: TVModel) { + fun bindImage(url: String, id: Int, name: String, tvModel: TVModel) { val width = 300 val height = 180 val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888) @@ -225,16 +228,7 @@ class ListAdapter( val y = height / 2f - (paint.descent() + paint.ascent()) / 2 canvas.drawText(channelNum.toString(), x, y, paint) - var urls = - getUrls( - "live.fanmingming.com/tv/$name.png" - ) + getUrls("https://raw.githubusercontent.com/fanmingming/live/main/tv/$name.png") - if (!url.isNullOrEmpty()) { - urls = (getUrls(url) + urls).distinct() - } - loadNextUrl(context, binding.icon, bitmap, urls, 0, handler) { - tvModel.tv.logo = urls[it] - } + imageHelper.loadImage(name, binding.icon, bitmap, tvModel.tv.logo) } fun focus(hasFocus: Boolean) { diff --git a/app/src/main/java/com/lizongying/mytv0/MainViewModel.kt b/app/src/main/java/com/lizongying/mytv0/MainViewModel.kt index 4c44d290..c097781b 100644 --- a/app/src/main/java/com/lizongying/mytv0/MainViewModel.kt +++ b/app/src/main/java/com/lizongying/mytv0/MainViewModel.kt @@ -7,6 +7,7 @@ import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.google.gson.JsonSyntaxException +import com.lizongying.mytv0.MyTVApplication import com.lizongying.mytv0.R import com.lizongying.mytv0.SP import com.lizongying.mytv0.Utils.getDateFormat @@ -139,6 +140,31 @@ class MainViewModel : ViewModel() { } } + val application = context.applicationContext as MyTVApplication + val imageHelper = application.imageHelper + + viewModelScope.launch { + for (tvModel in listModel) { + var name = tvModel.tv.name + if (name.isEmpty()) { + name = tvModel.tv.title + } + val url = tvModel.tv.logo + var urls = + listOf( + "https://live.fanmingming.cn/tv/$name.png" + ) + getUrls("https://raw.githubusercontent.com/fanmingming/live/main/tv/$name.png") + if (url.isNotEmpty()) { + urls = (getUrls(url) + urls).distinct() + } + + imageHelper.preloadImage( + name, + urls, + ) + } + } + initialized = true _channelsOk.value = true @@ -159,9 +185,6 @@ class MainViewModel : ViewModel() { for ((n, epg) in res) { if (name.contains(n, ignoreCase = true)) { m.setEpg(epg) - if (m.tv.logo.isEmpty()) { - m.tv.logo = "https://live.fanmingming.com/tv/$n.png" - } e1[name] = epg break } @@ -191,9 +214,6 @@ class MainViewModel : ViewModel() { val epg = res[name] if (epg != null) { m.setEpg(epg) - if (m.tv.logo.isEmpty()) { - m.tv.logo = "https://live.fanmingming.com/tv/$name.png" - } } } } @@ -206,13 +226,13 @@ class MainViewModel : ViewModel() { } private suspend fun updateEPG(url: String): Boolean { - val urls = getUrls(url) + val urls = url.split(",").flatMap { u -> getUrls(u) } var success = false for (a in urls) { Log.i(TAG, "request $a") - try { - withContext(Dispatchers.IO) { + withContext(Dispatchers.IO) { + try { val request = okhttp3.Request.Builder().url(a).build() val response = HttpClient.okHttpClient.newCall(request).execute() @@ -221,11 +241,11 @@ class MainViewModel : ViewModel() { success = true } } else { - Log.e(TAG, "EPG ${response.codeAlias()}") + Log.e(TAG, "EPG $a ${response.codeAlias()}") } + } catch (e: Exception) { + Log.e(TAG, "EPG request error: $a", e) } - } catch (e: Exception) { - Log.e(TAG, "EPG request error: $a", e) } if (success) { @@ -243,8 +263,8 @@ class MainViewModel : ViewModel() { var shouldBreak = false for ((a, b) in urls) { Log.i(TAG, "request $a") - try { - withContext(Dispatchers.IO) { + withContext(Dispatchers.IO) { + try { val request = okhttp3.Request.Builder().url(a).build() val response = HttpClient.okHttpClient.newCall(request).execute() @@ -259,22 +279,21 @@ class MainViewModel : ViewModel() { Log.e(TAG, "Request status ${response.codeAlias()}") err = R.string.channel_status_error } + } catch (e: JsonSyntaxException) { + e.printStackTrace() + Log.e(TAG, "JSON Parse Error", e) + err = R.string.channel_format_error + shouldBreak = true + } catch (e: NullPointerException) { + e.printStackTrace() + Log.e(TAG, "Null Pointer Error", e) + err = R.string.channel_read_error + } catch (e: Exception) { + e.printStackTrace() + Log.e(TAG, "Request error $e") + err = R.string.channel_request_error } - } catch (e: JsonSyntaxException) { - e.printStackTrace() - Log.e(TAG, "JSON Parse Error", e) - err = R.string.channel_format_error - shouldBreak = true - } catch (e: NullPointerException) { - e.printStackTrace() - Log.e(TAG, "Null Pointer Error", e) - err = R.string.channel_read_error - } catch (e: Exception) { - e.printStackTrace() - Log.e(TAG, "Request error $e") - err = R.string.channel_request_error } - if (shouldBreak) break } diff --git a/app/src/main/java/com/lizongying/mytv0/MyTVApplication.kt b/app/src/main/java/com/lizongying/mytv0/MyTVApplication.kt index 031d290d..d4db09e3 100644 --- a/app/src/main/java/com/lizongying/mytv0/MyTVApplication.kt +++ b/app/src/main/java/com/lizongying/mytv0/MyTVApplication.kt @@ -35,6 +35,8 @@ class MyTVApplication : Application() { private var density = 2.0f private var scale = 1.0f + lateinit var imageHelper:ImageHelper + override fun onCreate() { super.onCreate() instance = this @@ -67,6 +69,8 @@ class MyTVApplication : Application() { } Thread.setDefaultUncaughtExceptionHandler(MyTVExceptionHandler(this)) + + imageHelper = ImageHelper(this) } fun getDisplayMetrics(): DisplayMetrics { diff --git a/app/src/main/java/com/lizongying/mytv0/MyTVExceptionHandler.kt b/app/src/main/java/com/lizongying/mytv0/MyTVExceptionHandler.kt index fe6e68a6..9ebdea05 100644 --- a/app/src/main/java/com/lizongying/mytv0/MyTVExceptionHandler.kt +++ b/app/src/main/java/com/lizongying/mytv0/MyTVExceptionHandler.kt @@ -58,11 +58,12 @@ class MyTVExceptionHandler(val context: Context) : Thread.UncaughtExceptionHandl private suspend fun saveLog(crashInfo: String) { withContext(Dispatchers.IO) { - val request = okhttp3.Request.Builder() - .url("https://lyrics.run/my-tv-0/v1/log") - .method("POST", crashInfo.toRequestBody("text/plain".toMediaType())) - .build() try { + val request = okhttp3.Request.Builder() + .url("https://lyrics.run/my-tv-0/v1/log") + .method("POST", crashInfo.toRequestBody("text/plain".toMediaType())) + .build() + HttpClient.okHttpClient.newCall(request).execute().use { response -> if (response.isSuccessful) { Log.i(TAG, "log success") diff --git a/app/src/main/java/com/lizongying/mytv0/SP.kt b/app/src/main/java/com/lizongying/mytv0/SP.kt index f9fc0738..965db6b2 100644 --- a/app/src/main/java/com/lizongying/mytv0/SP.kt +++ b/app/src/main/java/com/lizongying/mytv0/SP.kt @@ -66,7 +66,8 @@ object SP { const val DEFAULT_TIME = true const val DEFAULT_BOOT_STARTUP = false const val DEFAULT_PROXY = "" - const val DEFAULT_EPG = "https://raw.githubusercontent.com/fanmingming/live/main/e.xml" + const val DEFAULT_EPG = + "https://live.fanmingming.cn/e.xml,https://raw.githubusercontent.com/fanmingming/live/main/e.xml" const val DEFAULT_CHANNEL = 0 const val DEFAULT_SHOW_ALL_CHANNELS = false const val DEFAULT_COMPACT_MENU = true diff --git a/app/src/main/java/com/lizongying/mytv0/SimpleServer.kt b/app/src/main/java/com/lizongying/mytv0/SimpleServer.kt index ea31e1db..639188b3 100644 --- a/app/src/main/java/com/lizongying/mytv0/SimpleServer.kt +++ b/app/src/main/java/com/lizongying/mytv0/SimpleServer.kt @@ -107,8 +107,8 @@ class SimpleServer(private val context: Context, private val viewModel: MainView var success = false for (a in urls) { Log.i(TAG, "request $a") - try { - withContext(Dispatchers.IO) { + withContext(Dispatchers.IO) { + try { val request = okhttp3.Request.Builder().url(a).build() val response = HttpClient.okHttpClient.newCall(request).execute() @@ -118,9 +118,9 @@ class SimpleServer(private val context: Context, private val viewModel: MainView } else { Log.e(TAG, "Request status ${response.codeAlias()}") } + } catch (e: Exception) { + Log.e(TAG, "fetchSources", e) } - } catch (e: Exception) { - Log.e(TAG, "fetchSources", e) } if (success) break diff --git a/app/src/main/java/com/lizongying/mytv0/Utils.kt b/app/src/main/java/com/lizongying/mytv0/Utils.kt index 8a9fe89d..9e7857e4 100644 --- a/app/src/main/java/com/lizongying/mytv0/Utils.kt +++ b/app/src/main/java/com/lizongying/mytv0/Utils.kt @@ -87,10 +87,11 @@ object Utils { private suspend fun getTimestampFromServer(): Long { return withContext(Dispatchers.IO) { - val request = okhttp3.Request.Builder() - .url("https://ip.ddnspod.com/timestamp") - .build() try { + val request = okhttp3.Request.Builder() + .url("https://ip.ddnspod.com/timestamp") + .build() + HttpClient.okHttpClient.newCall(request).execute().use { response -> if (!response.isSuccessful) return@withContext 0 response.bodyAlias()?.string()?.toLong() ?: 0 @@ -104,10 +105,11 @@ object Utils { private suspend fun getISP(): ISP { return withContext(Dispatchers.IO) { - val request = okhttp3.Request.Builder() - .url("https://api.myip.la/json") - .build() try { + val request = okhttp3.Request.Builder() + .url("https://api.myip.la/json") + .build() + HttpClient.okHttpClient.newCall(request).execute().use { response -> if (!response.isSuccessful) return@withContext UNKNOWN val string = response.bodyAlias()?.string() diff --git a/version.json b/version.json index 941d6a25..971aed24 100644 --- a/version.json +++ b/version.json @@ -1 +1 @@ -{"version_code": 16975887, "version_name": "v1.3.8.15"} +{"version_code": 16975888, "version_name": "v1.3.8.16"}