麦动KTV: 移除Gitee数据库(改官方Muse同步), 歌曲源切w.w345.my, 远程日志+广告屏蔽

- DatabaseBootstrapper: 删除 Gitee 分片/manifest/blob 下载逻辑, 曲库只走官方 MuseDbSync
- build.gradle: 移除 GITEE_DATABASE_* 构建参数
- MainActivity: 重置数据库文案改为官方同步
- ktv_api.js: 歌曲源仅 w.w345.my, 缓存标记 7, 广告/错号资源拦截+重试
- 远程日志: 点歌记录 编号/标题/请求源/状态
This commit is contained in:
zijunchiang
2026-09-04 21:23:21 +08:00
commit 0c26f6da68
1287 changed files with 25218 additions and 0 deletions
@@ -0,0 +1,112 @@
package com.local.ktv
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.os.Handler
import android.os.Looper
import android.util.LruCache
import android.widget.ImageView
import java.io.File
import java.io.FileOutputStream
import java.lang.ref.WeakReference
import java.net.HttpURLConnection
import java.net.URL
import java.security.MessageDigest
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.CopyOnWriteArrayList
import java.util.concurrent.Executors
/**
* Singer-image loader following the original app's Glide behavior: recycled-view protection,
* memory and disk cache, request coalescing and sampled decoding instead of full-size bitmaps.
*/
object SingerAvatarLoader {
private const val MAX_DOWNLOAD_BYTES = 8L * 1024L * 1024L
private val executor = Executors.newFixedThreadPool(4)
private val main = Handler(Looper.getMainLooper())
private val memory = object : LruCache<String, Bitmap>(32 * 1024) {
override fun sizeOf(key: String, value: Bitmap): Int = value.byteCount / 1024
}
private val waiting = ConcurrentHashMap<String, CopyOnWriteArrayList<WeakReference<ImageView>>>()
fun load(view: ImageView, imageUrl: String?, placeholder: Int) {
val key = imageUrl.orEmpty()
view.tag = key
view.setImageResource(placeholder)
if (key.isEmpty()) return
memory.get(key)?.let { bitmap ->
view.setImageBitmap(bitmap)
return
}
val waiters = waiting.computeIfAbsent(key) { CopyOnWriteArrayList() }
waiters += WeakReference(view)
if (waiters.size > 1) return
val cacheDir = File(view.context.applicationContext.cacheDir, "singer_avatars").apply { mkdirs() }
executor.execute {
val bitmap = runCatching { decode(key, cacheDir) }.getOrNull()
if (bitmap != null) memory.put(key, bitmap)
val targets = waiting.remove(key).orEmpty()
main.post {
if (bitmap != null) targets.forEach { reference ->
reference.get()?.takeIf { it.tag == key }?.setImageBitmap(bitmap)
}
}
}
}
private fun decode(value: String, cacheDir: File): Bitmap? {
if (value.startsWith("/")) return decodeSampled(File(value))
val cacheFile = File(cacheDir, sha256(value) + ".img")
if (cacheFile.isFile && cacheFile.length() > 0) {
decodeSampled(cacheFile)?.let { return it }
cacheFile.delete()
}
val temporary = File(cacheDir, cacheFile.name + ".tmp")
val connection = URL(value).openConnection() as HttpURLConnection
try {
connection.connectTimeout = 4_000
connection.readTimeout = 8_000
connection.instanceFollowRedirects = true
connection.setRequestProperty("User-Agent", "MaidongKTV/${BuildConfig.VERSION_NAME}")
require(connection.responseCode in 200..299)
require(connection.contentLengthLong <= 0 || connection.contentLengthLong <= MAX_DOWNLOAD_BYTES)
var total = 0L
connection.inputStream.buffered().use { input ->
FileOutputStream(temporary).buffered().use { output ->
val buffer = ByteArray(32 * 1024)
while (true) {
val count = input.read(buffer)
if (count < 0) break
total += count
require(total <= MAX_DOWNLOAD_BYTES)
output.write(buffer, 0, count)
}
}
}
if (!temporary.renameTo(cacheFile)) {
temporary.copyTo(cacheFile, overwrite = true)
temporary.delete()
}
return decodeSampled(cacheFile)
} finally {
connection.disconnect()
temporary.delete()
}
}
private fun decodeSampled(file: File): Bitmap? {
val bounds = BitmapFactory.Options().apply { inJustDecodeBounds = true }
BitmapFactory.decodeFile(file.absolutePath, bounds)
if (bounds.outWidth <= 0 || bounds.outHeight <= 0) return null
var sample = 1
while (bounds.outWidth / sample > 256 || bounds.outHeight / sample > 256) sample *= 2
return BitmapFactory.decodeFile(file.absolutePath, BitmapFactory.Options().apply {
inSampleSize = sample
inPreferredConfig = Bitmap.Config.RGB_565
})
}
private fun sha256(value: String): String = MessageDigest.getInstance("SHA-256")
.digest(value.toByteArray(Charsets.UTF_8)).joinToString("") { "%02x".format(it) }
}