曲库同步时效 30 天: 未到期启动不联网, 到期后才同步服务器数据库
- DatabaseBootstrapper: 新增 SYNC_TTL_DAYS=30 与 db_sync_at.txt 时间戳 (lastSyncAt/isSyncDue/syncRemainingMs/syncStatusText/markSynced) - initializeDatabase: 本地曲库可用且未到期时跳过服务器同步(无网络请求); 到期或本地不可用才查版本并在有更新时下载 - 设置页: 曲库行显示「30 天同步: N 天后/已到期」; 更新按钮改为手动强制检查 - download(): 成功后记录同步时间(去掉一次多余的远端版本请求)
This commit is contained in:
@@ -96,5 +96,6 @@ $BT/apksigner verify 目标名.apk
|
||||
- **歌曲源**:`app/src/main/assets/mobile/ktv_api.js` 的 `CONFIG.HOSTS`(多源自动排序,失败源沉底)
|
||||
- **JS 热更新**:改动 `ktv_api.js` 必须同步提升 `KTV_BRIDGE_API: N` 标记(JS 与 `KtvJsBridge.kt` 两处校验),否则设备仍用缓存旧脚本
|
||||
- **曲库**:只走官方 Muse 同步(`MuseDbSync`),不再依赖代码仓库数据库文件
|
||||
- **曲库同步时效**:默认 **30 天**(`DatabaseBootstrapper.SYNC_TTL_DAYS`)。未到期启动**不联网**、直接用本地曲库;到期后才查询服务器版本,有更新才下载。时效起点记录在 `db_sync_at.txt`,由 `DatabaseBootstrapper.markSynced()` 写入(下载成功或校验为最新时)。设置页「曲库 → 更新」为手动强制检查(绕过时效,仅在服务器版本更新时下载)
|
||||
- **麦克风**:`MicActivator` 常驻占用输入通道(TV/投影需占用后才有演唱声音输出),外接设备热插拔会自动重建
|
||||
- **仓库可见性**:Gitea 仓库保持 **public**(若后续把 App 更新/热更新端点迁到 Gitea,非 public 会导致 403)
|
||||
|
||||
@@ -9,12 +9,55 @@ import java.util.Locale
|
||||
*
|
||||
* 说明:已移除分片/镜像数据库下载路径(database_publish 不再发布到代码仓库,
|
||||
* 代码仓库此后只存代码)。曲库统一从官方 Muse 服务(mconn.cherryonline.cn mls-api)
|
||||
* 同步,由 [MuseDbSync] 负责登录、同步、校验与安装;本类只做版本文件与可读错误包装。
|
||||
* 同步,由 [MuseDbSync] 负责登录、同步、校验与安装;本类负责同步时效、版本文件
|
||||
* 与可读错误包装。
|
||||
*
|
||||
* 同步时效:默认 **30 天**([SYNC_TTL_DAYS])。未到期时启动不联网同步,直接用
|
||||
* 本地曲库;到期后才向服务器查询版本并在有更新时下载。
|
||||
*/
|
||||
object DatabaseBootstrapper {
|
||||
private const val TAG = "DatabaseBootstrapper"
|
||||
|
||||
/** 曲库同步时效(天):达到时效后才触发服务器同步。 */
|
||||
const val SYNC_TTL_DAYS = 30
|
||||
private const val SYNC_TTL_MS = SYNC_TTL_DAYS * 24L * 60 * 60 * 1000
|
||||
private const val DAY_MS = 24L * 60 * 60 * 1000
|
||||
|
||||
private val versionFile: File get() = File(MuseDatabase.defaultDbFile().parentFile, "db_version.txt")
|
||||
private val syncStampFile: File get() = File(MuseDatabase.defaultDbFile().parentFile, "db_sync_at.txt")
|
||||
|
||||
/** 上次成功同步(或成功校验为最新)的时间戳(毫秒);无记录返回 0。 */
|
||||
fun lastSyncAt(): Long = runCatching {
|
||||
syncStampFile.takeIf { it.exists() }?.readText()?.trim()?.toLongOrNull()
|
||||
}.getOrNull() ?: 0L
|
||||
|
||||
/** 记录一次成功同步/校验,作为 30 天时效的起点。 */
|
||||
fun markSynced(at: Long = System.currentTimeMillis()) {
|
||||
runCatching { syncStampFile.writeText(at.toString()) }
|
||||
}
|
||||
|
||||
/** 同步是否已到期;无记录(从未同步)视为到期。 */
|
||||
fun isSyncDue(now: Long = System.currentTimeMillis()): Boolean {
|
||||
val last = lastSyncAt()
|
||||
return last <= 0L || now - last >= SYNC_TTL_MS
|
||||
}
|
||||
|
||||
/** 距离下次同步的剩余毫秒;已到期或无记录返回 0。 */
|
||||
fun syncRemainingMs(now: Long = System.currentTimeMillis()): Long {
|
||||
val last = lastSyncAt()
|
||||
if (last <= 0L) return 0L
|
||||
return (last + SYNC_TTL_MS - now).coerceAtLeast(0L)
|
||||
}
|
||||
|
||||
/** 同步状态文案(用于设置界面展示)。 */
|
||||
fun syncStatusText(now: Long = System.currentTimeMillis()): String {
|
||||
val last = lastSyncAt()
|
||||
if (last <= 0L) return "未同步"
|
||||
val remaining = syncRemainingMs(now)
|
||||
if (remaining <= 0L) return "已到期"
|
||||
val days = (remaining + DAY_MS - 1) / DAY_MS
|
||||
return "$days 天后同步"
|
||||
}
|
||||
|
||||
/** 远端曲库版本(官方 Muse);失败返回 null。 */
|
||||
fun fetchRemoteVersion(): String? = MuseDbSync.fetchVersion()
|
||||
@@ -23,16 +66,12 @@ object DatabaseBootstrapper {
|
||||
versionFile.takeIf { it.exists() }?.readText()?.trim()?.takeIf { it.isNotEmpty() }
|
||||
}.getOrNull()
|
||||
|
||||
private fun saveLocalDbVersion(version: String) {
|
||||
runCatching { versionFile.writeText(version.trim()) }
|
||||
}
|
||||
|
||||
/** 下载曲库:仅官方 Muse 同步源。 */
|
||||
/** 下载曲库:仅官方 Muse 同步源;成功后记录同步时间(重置 30 天时效)。 */
|
||||
fun download(onProgress: (Int) -> Unit): Result<File> {
|
||||
return runCatching {
|
||||
val file = MuseDbSync.download(onProgress)
|
||||
// MuseDbSync 内部已保存版本;此处幂等兜底。
|
||||
MuseDbSync.fetchVersion()?.let(::saveLocalDbVersion)
|
||||
// MuseDbSync 内部已保存版本文件;这里记录同步时间作为时效起点。
|
||||
markSynced()
|
||||
file
|
||||
}.mapError(::readableError)
|
||||
}
|
||||
|
||||
@@ -586,23 +586,37 @@ class MainActivity : AppCompatActivity() {
|
||||
databaseBootstrapRunning = true
|
||||
showDatabaseLoading(true, "正在初始化曲库...", null)
|
||||
io.execute {
|
||||
val remoteVersion = DatabaseBootstrapper.fetchRemoteVersion()
|
||||
val localVersion = DatabaseBootstrapper.getLocalDbVersion()
|
||||
val updateRequired = remoteVersion != null && remoteVersion != localVersion
|
||||
var bootstrapError: Throwable? = null
|
||||
var usedOldDatabase = false
|
||||
|
||||
var ok = if (updateRequired) false else library.muse.open()
|
||||
if (!ok) {
|
||||
library.muse.close()
|
||||
val result = DatabaseBootstrapper.download(::showDatabaseDownloadProgress)
|
||||
bootstrapError = result.exceptionOrNull()
|
||||
ok = result.isSuccess && library.muse.open()
|
||||
if (!ok) {
|
||||
// A failed online update must not make an existing local catalog unusable.
|
||||
usedOldDatabase = library.muse.open()
|
||||
ok = usedOldDatabase
|
||||
// 先尝试打开本地曲库;仅当本地不可用或已过 30 天时效时才联网同步。
|
||||
var ok = library.muse.open()
|
||||
val syncDue = DatabaseBootstrapper.isSyncDue()
|
||||
if (!ok || syncDue) {
|
||||
val reason = if (!ok) "本地曲库不可用" else "已到 ${DatabaseBootstrapper.SYNC_TTL_DAYS} 天同步时效"
|
||||
Log.i(TAG, "触发曲库服务器同步:$reason")
|
||||
val remoteVersion = DatabaseBootstrapper.fetchRemoteVersion()
|
||||
if (remoteVersion != null) {
|
||||
val localVersion = DatabaseBootstrapper.getLocalDbVersion()
|
||||
if (!ok || remoteVersion != localVersion) {
|
||||
library.muse.close()
|
||||
val result = DatabaseBootstrapper.download(::showDatabaseDownloadProgress)
|
||||
bootstrapError = result.exceptionOrNull()
|
||||
ok = result.isSuccess && library.muse.open()
|
||||
if (!ok) {
|
||||
// A failed online update must not make an existing local catalog unusable.
|
||||
usedOldDatabase = library.muse.open()
|
||||
ok = usedOldDatabase
|
||||
}
|
||||
} else {
|
||||
// 版本一致:本次已完成有效校验,重置时效起点。
|
||||
DatabaseBootstrapper.markSynced()
|
||||
}
|
||||
} else {
|
||||
Log.w(TAG, "曲库同步到期,但获取远端版本失败,本次继续使用本地曲库")
|
||||
}
|
||||
} else {
|
||||
Log.i(TAG, "曲库未到期(${DatabaseBootstrapper.syncStatusText()}),跳过服务器同步")
|
||||
}
|
||||
|
||||
val count = if (ok) library.muse.songCount() else 0
|
||||
@@ -8274,7 +8288,8 @@ class MainActivity : AppCompatActivity() {
|
||||
"统计中…",
|
||||
"更新",
|
||||
) {
|
||||
io.execute { library.muse.close(); library.muse.open(); main.post { showSettingsSection(1, false) } }
|
||||
// 手动强制检查更新(绕过 30 天时效; 仅当服务器版本更新时才下载)
|
||||
forceDatabaseSync()
|
||||
},
|
||||
SettingsEntry(R.drawable.ott_ic_data_setting, "U盘加歌", "扫描U盘内按规定命名的歌曲文件,并添加到曲库中", "立即加歌") { toast("未检测到U盘") },
|
||||
SettingsEntry(R.drawable.ott_ic_setting_storage_space, "预留存储空间", "当前 ${String.format(Locale.ROOT, "%.1f", reserveStorageGb)} GB") { showReserveStorageDialog() },
|
||||
@@ -8315,9 +8330,55 @@ class MainActivity : AppCompatActivity() {
|
||||
io.execute {
|
||||
val count = runCatching { library.muse.songCount() }.getOrDefault(0)
|
||||
val downloaded = runCatching { countDownloadedFiles() }.getOrDefault(0)
|
||||
// 同步时效(30 天): 未到期不联网, 到期后才触发服务器同步。
|
||||
val syncText = DatabaseBootstrapper.syncStatusText()
|
||||
main.post {
|
||||
if (activeSettingsAdapter === adapter) {
|
||||
adapter.updateSubtitle("曲库", "数据库 $count 首 本地已下载 $downloaded 首")
|
||||
adapter.updateSubtitle(
|
||||
"曲库",
|
||||
"数据库 $count 首 本地已下载 $downloaded 首 · ${DatabaseBootstrapper.SYNC_TTL_DAYS} 天同步: $syncText",
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 手动强制检查曲库更新(绕过 30 天时效)。
|
||||
* 仅在服务器版本更新(或本地库不可用)时才下载, 避免无谓的大文件传输。
|
||||
*/
|
||||
private fun forceDatabaseSync() {
|
||||
showDatabaseLoading(true, "正在检查曲库更新...", null)
|
||||
io.execute {
|
||||
val remoteVersion = DatabaseBootstrapper.fetchRemoteVersion()
|
||||
if (remoteVersion == null) {
|
||||
main.post {
|
||||
showDatabaseLoading(false, "", null)
|
||||
toast("检查失败:无法连接曲库服务器")
|
||||
showSettingsSection(1, false)
|
||||
}
|
||||
return@execute
|
||||
}
|
||||
val localVersion = DatabaseBootstrapper.getLocalDbVersion()
|
||||
if (library.muse.isAvailable() && remoteVersion == localVersion) {
|
||||
DatabaseBootstrapper.markSynced()
|
||||
main.post {
|
||||
showDatabaseLoading(false, "", null)
|
||||
toast("曲库已是最新版本")
|
||||
showSettingsSection(1, false)
|
||||
}
|
||||
return@execute
|
||||
}
|
||||
library.muse.close()
|
||||
val result = DatabaseBootstrapper.download(::showDatabaseDownloadProgress)
|
||||
val ok = result.isSuccess && library.muse.open()
|
||||
main.post {
|
||||
if (ok) {
|
||||
showDatabaseLoading(false, "", null)
|
||||
toast("曲库同步完成")
|
||||
showSettingsSection(1, false)
|
||||
} else {
|
||||
showDatabaseLoadingFailure("曲库同步失败:${result.exceptionOrNull()?.message.orEmpty()}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user