From 0b1a8294cb8ef95b6f8d3b3562022c8bd3a554b6 Mon Sep 17 00:00:00 2001 From: Ktouls <75007654+Ktouls@users.noreply.github.com> Date: Thu, 15 Jan 2026 12:55:27 +0800 Subject: [PATCH] Update HttpReadAloudService.kt --- .../app/service/HttpReadAloudService.kt | 113 +++++++++++++----- 1 file changed, 81 insertions(+), 32 deletions(-) diff --git a/app/src/main/java/io/legado/app/service/HttpReadAloudService.kt b/app/src/main/java/io/legado/app/service/HttpReadAloudService.kt index ee1af9d44..27b98b294 100644 --- a/app/src/main/java/io/legado/app/service/HttpReadAloudService.kt +++ b/app/src/main/java/io/legado/app/service/HttpReadAloudService.kt @@ -28,8 +28,10 @@ import com.script.ScriptException import io.legado.app.R import io.legado.app.constant.AppLog import io.legado.app.constant.AppPattern +import io.legado.app.data.appDb import io.legado.app.data.entities.HttpTTS import io.legado.app.exception.NoStackTraceException +import io.legado.app.help.BookHelp import io.legado.app.help.config.AppConfig import io.legado.app.help.coroutine.Coroutine import io.legado.app.help.exoplayer.InputStreamDataSource @@ -65,6 +67,7 @@ import kotlin.coroutines.coroutineContext /** * 在线朗读 + * (已修改:支持跨章节预缓存 + 自定义清理时间) */ @SuppressLint("UnsafeOptInUsageError") class HttpReadAloudService : BaseReadAloudService(), @@ -150,6 +153,8 @@ class HttpReadAloudService : BaseReadAloudService(), downloadTaskActiveLock.withLock { ensureActive() val httpTts = ReadAloud.httpTTS ?: throw NoStackTraceException("tts is null") + + // 1. 处理当前正在播放的章节 contentList.forEachIndexed { index, content -> ensureActive() if (index < nowSpeak) return@forEachIndexed @@ -184,6 +189,7 @@ class HttpReadAloudService : BaseReadAloudService(), exoPlayer.addMediaItem(mediaItem) } } + // 2. 触发预下载 (新逻辑) preDownloadAudios(httpTts) } }.onError { @@ -191,26 +197,55 @@ class HttpReadAloudService : BaseReadAloudService(), } } + // ================================================================ + // 修改点:强力预缓存逻辑 (循环查库,跨章节) + // ================================================================ private suspend fun preDownloadAudios(httpTts: HttpTTS) { - val textChapter = ReadBook.nextTextChapter ?: return - val contentList = textChapter.getNeedReadAloud(0, readAloudByPage, 0, 1) - .splitToSequence("\n") - .filter { it.isNotEmpty() } - .take(AppConfig.audioPreDownloadNum) // <--- 修改:读取配置 - .toList() - contentList.forEach { content -> + val book = ReadBook.book ?: return + // 获取当前章节索引 + val currentIdx = ReadBook.textChapter?.chapterIndex ?: return + // 获取用户设置的预加载数量 (例如 50) + val limit = AppConfig.audioPreDownloadNum + + // 循环:从下一章开始,往后找 limit 个章节 + for (i in 1..limit) { currentCoroutineContext().ensureActive() - val fileName = md5SpeakFileName(content, textChapter) - 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) + + // 计算目标章节索引 + val targetIndex = currentIdx + i + + // 查数据库获取章节信息 (如果超出了总章数会返回空,就停止) + val chapter = appDb.bookChapterDao.getChapter(book.bookUrl, targetIndex) ?: break + + // 获取章节内容 (这里假设已缓存文本,或者尝试读取) + val contentString = BookHelp.getContent(book, chapter) + + // 如果内容为空 (还没下载正文),就跳过,不强行下载正文以免阻塞 + 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( httpTts: HttpTTS, downloaderChannel: Channel ) { - val textChapter = ReadBook.nextTextChapter ?: return - val contentList = textChapter.getNeedReadAloud(0, readAloudByPage, 0, 1) - .splitToSequence("\n") - .filter { it.isNotEmpty() } - .take(AppConfig.audioPreDownloadNum) // <--- 修改:读取配置 - .toList() - contentList.forEach { content -> + val book = ReadBook.book ?: return + val currentIdx = ReadBook.textChapter?.chapterIndex ?: return + val limit = AppConfig.audioPreDownloadNum + + for (i in 1..limit) { currentCoroutineContext().ensureActive() - val fileName = md5SpeakFileName(content, textChapter) - val speakText = content.replace(AppPattern.notReadAloudRegex, "") - val dataSourceFactory = createDataSourceFactory(httpTts, speakText) - val downloader = createDownloader(dataSourceFactory, fileName) - downloaderChannel.send(downloader) + val targetIndex = currentIdx + i + val chapter = appDb.bookChapterDao.getChapter(book.bookUrl, targetIndex) ?: break + val contentString = BookHelp.getContent(book, chapter) + if (contentString.isNullOrEmpty()) continue + + 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() { val titleMd5 = MD5Utils.md5Encode16(textChapter?.title ?: "") FileUtils.listDirsAndFiles(ttsFolderPath)?.forEach { val isSilentSound = it.length() == 2160L if ((!it.name.startsWith(titleMd5) - && System.currentTimeMillis() - it.lastModified() > AppConfig.audioCacheCleanTime) // <--- 修改:读取配置 + && System.currentTimeMillis() - it.lastModified() > AppConfig.audioCacheCleanTime) || isSilentSound ) { FileUtils.delete(it.absolutePath)