Update HttpReadAloudService.kt

This commit is contained in:
Ktouls authored and Kudomaga committed 2026-01-17 06:39:29 +08:00
1 parent ac65fd620d
commit 0b1a8294cb
1 file changed
+81 -32
@@ -28,8 +28,10 @@ import com.script.ScriptException
import io.legado.app.R import io.legado.app.R
import io.legado.app.constant.AppLog import io.legado.app.constant.AppLog
import io.legado.app.constant.AppPattern import io.legado.app.constant.AppPattern
import io.legado.app.data.appDb
import io.legado.app.data.entities.HttpTTS import io.legado.app.data.entities.HttpTTS
import io.legado.app.exception.NoStackTraceException import io.legado.app.exception.NoStackTraceException
import io.legado.app.help.BookHelp
import io.legado.app.help.config.AppConfig import io.legado.app.help.config.AppConfig
import io.legado.app.help.coroutine.Coroutine import io.legado.app.help.coroutine.Coroutine
import io.legado.app.help.exoplayer.InputStreamDataSource import io.legado.app.help.exoplayer.InputStreamDataSource
@@ -65,6 +67,7 @@ import kotlin.coroutines.coroutineContext
/** /**
* 在线朗读 * 在线朗读
* (已修改:支持跨章节预缓存 + 自定义清理时间)
*/ */
@SuppressLint("UnsafeOptInUsageError") @SuppressLint("UnsafeOptInUsageError")
class HttpReadAloudService : BaseReadAloudService(), class HttpReadAloudService : BaseReadAloudService(),
@@ -150,6 +153,8 @@ class HttpReadAloudService : BaseReadAloudService(),
downloadTaskActiveLock.withLock { downloadTaskActiveLock.withLock {
ensureActive() ensureActive()
val httpTts = ReadAloud.httpTTS ?: throw NoStackTraceException("tts is null") val httpTts = ReadAloud.httpTTS ?: throw NoStackTraceException("tts is null")
// 1. 处理当前正在播放的章节
contentList.forEachIndexed { index, content -> contentList.forEachIndexed { index, content ->
ensureActive() ensureActive()
if (index < nowSpeak) return@forEachIndexed if (index < nowSpeak) return@forEachIndexed
@@ -184,6 +189,7 @@ class HttpReadAloudService : BaseReadAloudService(),
exoPlayer.addMediaItem(mediaItem) exoPlayer.addMediaItem(mediaItem)
} }
} }
// 2. 触发预下载 (新逻辑)
preDownloadAudios(httpTts) preDownloadAudios(httpTts)
} }
}.onError { }.onError {
@@ -191,26 +197,55 @@ class HttpReadAloudService : BaseReadAloudService(),
} }
} }
// ================================================================
// 修改点:强力预缓存逻辑 (循环查库,跨章节)
// ================================================================
private suspend fun preDownloadAudios(httpTts: HttpTTS) { private suspend fun preDownloadAudios(httpTts: HttpTTS) {
val textChapter = ReadBook.nextTextChapter ?: return val book = ReadBook.book ?: return
val contentList = textChapter.getNeedReadAloud(0, readAloudByPage, 0, 1) // 获取当前章节索引
.splitToSequence("\n") val currentIdx = ReadBook.textChapter?.chapterIndex ?: return
.filter { it.isNotEmpty() } // 获取用户设置的预加载数量 (例如 50)
.take(AppConfig.audioPreDownloadNum) // <--- 修改:读取配置 val limit = AppConfig.audioPreDownloadNum
.toList()
contentList.forEach { content -> // 循环:从下一章开始,往后找 limit 个章节
for (i in 1..limit) {
currentCoroutineContext().ensureActive() currentCoroutineContext().ensureActive()
val fileName = md5SpeakFileName(content, textChapter)
val speakText = content.replace(AppPattern.notReadAloudRegex, "") // 计算目标章节索引
if (speakText.isEmpty()) { val targetIndex = currentIdx + i
createSilentSound(fileName)
} else if (!hasSpeakFile(fileName)) { // 查数据库获取章节信息 (如果超出了总章数会返回空,就停止)
runCatching { val chapter = appDb.bookChapterDao.getChapter(book.bookUrl, targetIndex) ?: break
val inputStream = getSpeakStream(httpTts, speakText)
if (inputStream != null) { // 获取章节内容 (这里假设已缓存文本,或者尝试读取)
createSpeakFile(fileName, inputStream) val contentString = BookHelp.getContent(book, chapter)
} else {
createSilentSound(fileName) // 如果内容为空 (还没下载正文),就跳过,不强行下载正文以免阻塞
if (contentString.isNullOrEmpty()) continue
// 模拟 TextChapter 的分割逻辑,转成段落列表
val contentList = contentString.split("\n").filter { it.isNotEmpty() }
// 逐段检查并下载音频
contentList.forEach { content ->
currentCoroutineContext().ensureActive()
// 注意:跨章节时 MD5 需要基于 targetIndex 对应的 title (虽然 MD5Utils.md5Encode16(chapter.title) 可能和 TextChapter 不完全一致,但通常足够)
// 为了保险,我们手动拼装 MD5 文件名逻辑
val titleMd5 = MD5Utils.md5Encode16(chapter.title)
val contentMd5 = MD5Utils.md5Encode16("${ReadAloud.httpTTS?.url}-|-$speechRate-|-$content")
val fileName = "${titleMd5}_${contentMd5}"
val speakText = content.replace(AppPattern.notReadAloudRegex, "")
if (speakText.isEmpty()) {
createSilentSound(fileName)
} else if (!hasSpeakFile(fileName)) {
runCatching {
val inputStream = getSpeakStream(httpTts, speakText)
if (inputStream != null) {
createSpeakFile(fileName, inputStream)
} else {
createSilentSound(fileName)
}
} }
} }
} }
@@ -257,23 +292,37 @@ class HttpReadAloudService : BaseReadAloudService(),
} }
} }
// ================================================================
// 修改点:流式预缓存逻辑 (循环查库,跨章节)
// ================================================================
private suspend fun preDownloadAudiosStream( private suspend fun preDownloadAudiosStream(
httpTts: HttpTTS, httpTts: HttpTTS,
downloaderChannel: Channel<Downloader> downloaderChannel: Channel<Downloader>
) { ) {
val textChapter = ReadBook.nextTextChapter ?: return val book = ReadBook.book ?: return
val contentList = textChapter.getNeedReadAloud(0, readAloudByPage, 0, 1) val currentIdx = ReadBook.textChapter?.chapterIndex ?: return
.splitToSequence("\n") val limit = AppConfig.audioPreDownloadNum
.filter { it.isNotEmpty() }
.take(AppConfig.audioPreDownloadNum) // <--- 修改:读取配置 for (i in 1..limit) {
.toList()
contentList.forEach { content ->
currentCoroutineContext().ensureActive() currentCoroutineContext().ensureActive()
val fileName = md5SpeakFileName(content, textChapter) val targetIndex = currentIdx + i
val speakText = content.replace(AppPattern.notReadAloudRegex, "") val chapter = appDb.bookChapterDao.getChapter(book.bookUrl, targetIndex) ?: break
val dataSourceFactory = createDataSourceFactory(httpTts, speakText) val contentString = BookHelp.getContent(book, chapter)
val downloader = createDownloader(dataSourceFactory, fileName) if (contentString.isNullOrEmpty()) continue
downloaderChannel.send(downloader)
val contentList = contentString.split("\n").filter { it.isNotEmpty() }
contentList.forEach { content ->
currentCoroutineContext().ensureActive()
val titleMd5 = MD5Utils.md5Encode16(chapter.title)
val contentMd5 = MD5Utils.md5Encode16("${ReadAloud.httpTTS?.url}-|-$speechRate-|-$content")
val fileName = "${titleMd5}_${contentMd5}"
val speakText = content.replace(AppPattern.notReadAloudRegex, "")
val dataSourceFactory = createDataSourceFactory(httpTts, speakText)
val downloader = createDownloader(dataSourceFactory, fileName)
downloaderChannel.send(downloader)
}
} }
} }
@@ -429,14 +478,14 @@ class HttpReadAloudService : BaseReadAloudService(),
} }
/** /**
* 移除缓存文件 * 移除缓存文件 (支持自定义清理时间)
*/ */
private fun removeCacheFile() { private fun removeCacheFile() {
val titleMd5 = MD5Utils.md5Encode16(textChapter?.title ?: "") val titleMd5 = MD5Utils.md5Encode16(textChapter?.title ?: "")
FileUtils.listDirsAndFiles(ttsFolderPath)?.forEach { FileUtils.listDirsAndFiles(ttsFolderPath)?.forEach {
val isSilentSound = it.length() == 2160L val isSilentSound = it.length() == 2160L
if ((!it.name.startsWith(titleMd5) if ((!it.name.startsWith(titleMd5)
&& System.currentTimeMillis() - it.lastModified() > AppConfig.audioCacheCleanTime) // <--- 修改:读取配置 && System.currentTimeMillis() - it.lastModified() > AppConfig.audioCacheCleanTime)
|| isSilentSound || isSilentSound
) { ) {
FileUtils.delete(it.absolutePath) FileUtils.delete(it.absolutePath)