|
@@ -11,6 +11,7 @@ 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.constant.EventBus
|
|
import io.legado.app.constant.EventBus
|
|
|
|
|
+import io.legado.app.data.entities.HttpTTS
|
|
|
import io.legado.app.exception.ConcurrentException
|
|
import io.legado.app.exception.ConcurrentException
|
|
|
import io.legado.app.exception.NoStackTraceException
|
|
import io.legado.app.exception.NoStackTraceException
|
|
|
import io.legado.app.help.config.AppConfig
|
|
import io.legado.app.help.config.AppConfig
|
|
@@ -20,9 +21,13 @@ import io.legado.app.model.ReadBook
|
|
|
import io.legado.app.model.analyzeRule.AnalyzeUrl
|
|
import io.legado.app.model.analyzeRule.AnalyzeUrl
|
|
|
import io.legado.app.utils.*
|
|
import io.legado.app.utils.*
|
|
|
import kotlinx.coroutines.*
|
|
import kotlinx.coroutines.*
|
|
|
|
|
+import kotlinx.coroutines.Dispatchers.IO
|
|
|
|
|
+import kotlinx.coroutines.sync.Mutex
|
|
|
|
|
+import kotlinx.coroutines.sync.withLock
|
|
|
import okhttp3.Response
|
|
import okhttp3.Response
|
|
|
import org.mozilla.javascript.WrappedException
|
|
import org.mozilla.javascript.WrappedException
|
|
|
import java.io.File
|
|
import java.io.File
|
|
|
|
|
+import java.io.InputStream
|
|
|
import java.net.ConnectException
|
|
import java.net.ConnectException
|
|
|
import java.net.SocketTimeoutException
|
|
import java.net.SocketTimeoutException
|
|
|
|
|
|
|
@@ -41,9 +46,9 @@ class HttpReadAloudService : BaseReadAloudService(),
|
|
|
private var speechRate: Int = AppConfig.speechRatePlay + 5
|
|
private var speechRate: Int = AppConfig.speechRatePlay + 5
|
|
|
private var downloadTask: Coroutine<*>? = null
|
|
private var downloadTask: Coroutine<*>? = null
|
|
|
private var playIndexJob: Job? = null
|
|
private var playIndexJob: Job? = null
|
|
|
- private var downloadTaskIsActive = false
|
|
|
|
|
private var downloadErrorNo: Int = 0
|
|
private var downloadErrorNo: Int = 0
|
|
|
private var playErrorNo = 0
|
|
private var playErrorNo = 0
|
|
|
|
|
+ private val downloadTaskActiveLock = Mutex()
|
|
|
|
|
|
|
|
override fun onCreate() {
|
|
override fun onCreate() {
|
|
|
super.onCreate()
|
|
super.onCreate()
|
|
@@ -71,7 +76,7 @@ class HttpReadAloudService : BaseReadAloudService(),
|
|
|
val file = getSpeakFileAsMd5(fileName)
|
|
val file = getSpeakFileAsMd5(fileName)
|
|
|
if (file.exists()) {
|
|
if (file.exists()) {
|
|
|
playAudio(file)
|
|
playAudio(file)
|
|
|
- } else if (!downloadTaskIsActive) {
|
|
|
|
|
|
|
+ } else if (!downloadTaskActiveLock.isLocked) {
|
|
|
downloadAudio()
|
|
downloadAudio()
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
@@ -97,123 +102,111 @@ class HttpReadAloudService : BaseReadAloudService(),
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
private fun downloadAudio() {
|
|
private fun downloadAudio() {
|
|
|
- launch {
|
|
|
|
|
- downloadTask?.cancel()
|
|
|
|
|
- while (downloadTaskIsActive) {
|
|
|
|
|
- //在线tts大部分只能单线程,等待上次访问结束
|
|
|
|
|
- delay(100)
|
|
|
|
|
- }
|
|
|
|
|
- downloadTask = execute {
|
|
|
|
|
|
|
+ downloadTask?.cancel()
|
|
|
|
|
+ downloadTask = execute {
|
|
|
|
|
+ downloadTaskActiveLock.withLock {
|
|
|
|
|
+ ensureActive()
|
|
|
removeCacheFile()
|
|
removeCacheFile()
|
|
|
val httpTts = ReadAloud.httpTTS ?: throw NoStackTraceException("tts is null")
|
|
val httpTts = ReadAloud.httpTTS ?: throw NoStackTraceException("tts is null")
|
|
|
contentList.forEachIndexed { index, content ->
|
|
contentList.forEachIndexed { index, content ->
|
|
|
ensureActive()
|
|
ensureActive()
|
|
|
val fileName = md5SpeakFileName(content)
|
|
val fileName = md5SpeakFileName(content)
|
|
|
val speakText = content.replace(AppPattern.notReadAloudRegex, "")
|
|
val speakText = content.replace(AppPattern.notReadAloudRegex, "")
|
|
|
- if (hasSpeakFile(fileName)) { //已经下载好的语音缓存
|
|
|
|
|
- if (index == nowSpeak) {
|
|
|
|
|
- val file = getSpeakFileAsMd5(fileName)
|
|
|
|
|
- playAudio(file)
|
|
|
|
|
- }
|
|
|
|
|
- } else if (speakText.isEmpty()) {
|
|
|
|
|
- AppLog.put(
|
|
|
|
|
- "阅读段落内容为空,使用无声音频代替。\n朗读文本:$content"
|
|
|
|
|
- )
|
|
|
|
|
|
|
+ if (speakText.isEmpty()) {
|
|
|
|
|
+ AppLog.put("阅读段落内容为空,使用无声音频代替。\n朗读文本:$content")
|
|
|
createSilentSound(fileName)
|
|
createSilentSound(fileName)
|
|
|
- if (index == nowSpeak) {
|
|
|
|
|
- val file = getSpeakFileAsMd5(fileName)
|
|
|
|
|
- playAudio(file)
|
|
|
|
|
- }
|
|
|
|
|
- return@forEachIndexed
|
|
|
|
|
- } else {
|
|
|
|
|
|
|
+ } else if (!hasSpeakFile(fileName)) {
|
|
|
runCatching {
|
|
runCatching {
|
|
|
- val analyzeUrl = AnalyzeUrl(
|
|
|
|
|
- httpTts.url,
|
|
|
|
|
- speakText = speakText,
|
|
|
|
|
- speakSpeed = speechRate,
|
|
|
|
|
- source = httpTts,
|
|
|
|
|
- headerMapF = httpTts.getHeaderMap(true)
|
|
|
|
|
- )
|
|
|
|
|
- var response = analyzeUrl.getResponseAwait()
|
|
|
|
|
- ensureActive()
|
|
|
|
|
- httpTts.loginCheckJs?.takeIf { checkJs ->
|
|
|
|
|
- checkJs.isNotBlank()
|
|
|
|
|
- }?.let { checkJs ->
|
|
|
|
|
- response = analyzeUrl.evalJS(checkJs, response) as Response
|
|
|
|
|
|
|
+ val inputStream = getSpeakStream(httpTts, speakText)
|
|
|
|
|
+ if (inputStream != null) {
|
|
|
|
|
+ createSpeakFile(fileName, inputStream)
|
|
|
|
|
+ } else {
|
|
|
|
|
+ createSilentSound(fileName)
|
|
|
}
|
|
}
|
|
|
- httpTts.contentType?.takeIf { ct ->
|
|
|
|
|
- ct.isNotBlank()
|
|
|
|
|
- }?.let { ct ->
|
|
|
|
|
- response.headers["Content-Type"]?.let { contentType ->
|
|
|
|
|
- if (!contentType.matches(ct.toRegex())) {
|
|
|
|
|
- throw NoStackTraceException("TTS服务器返回错误:" + response.body!!.string())
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
- ensureActive()
|
|
|
|
|
- response.body!!.bytes().let { bytes ->
|
|
|
|
|
- val file = createSpeakFileAsMd5IfNotExist(fileName)
|
|
|
|
|
- file.writeBytes(bytes)
|
|
|
|
|
- if (index == nowSpeak) {
|
|
|
|
|
- playAudio(file)
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
- downloadErrorNo = 0
|
|
|
|
|
}.onFailure {
|
|
}.onFailure {
|
|
|
when (it) {
|
|
when (it) {
|
|
|
is CancellationException -> Unit
|
|
is CancellationException -> Unit
|
|
|
- is ConcurrentException -> {
|
|
|
|
|
- delay(it.waitTime.toLong())
|
|
|
|
|
- downloadAudio()
|
|
|
|
|
- }
|
|
|
|
|
- is ScriptException, is WrappedException -> {
|
|
|
|
|
- AppLog.put("js错误\n${it.localizedMessage}", it)
|
|
|
|
|
- toastOnUi("js错误\n${it.localizedMessage}")
|
|
|
|
|
- it.printOnDebug()
|
|
|
|
|
- cancel()
|
|
|
|
|
- pauseReadAloud()
|
|
|
|
|
- }
|
|
|
|
|
- is SocketTimeoutException, is ConnectException -> {
|
|
|
|
|
- downloadErrorNo++
|
|
|
|
|
- if (downloadErrorNo > 5) {
|
|
|
|
|
- val msg = "tts超时或连接错误超过5次\n${it.localizedMessage}"
|
|
|
|
|
- AppLog.put(msg, it)
|
|
|
|
|
- toastOnUi(msg)
|
|
|
|
|
- pauseReadAloud()
|
|
|
|
|
- } else {
|
|
|
|
|
- downloadAudio()
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
- else -> {
|
|
|
|
|
- downloadErrorNo++
|
|
|
|
|
- val msg = "tts下载错误\n${it.localizedMessage}"
|
|
|
|
|
- AppLog.put(msg, it)
|
|
|
|
|
- it.printOnDebug()
|
|
|
|
|
- if (downloadErrorNo > 5) {
|
|
|
|
|
- AppLog.put("TTS服务器连续5次错误,已暂停阅读。")
|
|
|
|
|
- toastOnUi("TTS服务器连续5次错误,已暂停阅读。")
|
|
|
|
|
- pauseReadAloud()
|
|
|
|
|
- } else {
|
|
|
|
|
- AppLog.put("TTS下载音频出错,使用无声音频代替。\n朗读文本:$content")
|
|
|
|
|
- createSilentSound(fileName)
|
|
|
|
|
- if (index == nowSpeak) {
|
|
|
|
|
- val file = getSpeakFileAsMd5(fileName)
|
|
|
|
|
- playAudio(file)
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ else -> pauseReadAloud()
|
|
|
}
|
|
}
|
|
|
|
|
+ return@execute
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ if (index == nowSpeak) {
|
|
|
|
|
+ val file = getSpeakFileAsMd5(fileName)
|
|
|
|
|
+ playAudio(file)
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }.onError(IO) {
|
|
|
|
|
+ AppLog.put("朗读下载出错\n${it.localizedMessage}", it)
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private suspend fun getSpeakStream(httpTts: HttpTTS, speakText: String): InputStream? {
|
|
|
|
|
+ while (true) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ val analyzeUrl = AnalyzeUrl(
|
|
|
|
|
+ httpTts.url,
|
|
|
|
|
+ speakText = speakText,
|
|
|
|
|
+ speakSpeed = speechRate,
|
|
|
|
|
+ source = httpTts,
|
|
|
|
|
+ headerMapF = httpTts.getHeaderMap(true)
|
|
|
|
|
+ )
|
|
|
|
|
+ var response = analyzeUrl.getResponseAwait()
|
|
|
|
|
+ ensureActive()
|
|
|
|
|
+ val checkJs = httpTts.loginCheckJs
|
|
|
|
|
+ if (checkJs?.isNotBlank() == true) {
|
|
|
|
|
+ response = analyzeUrl.evalJS(checkJs, response) as Response
|
|
|
|
|
+ }
|
|
|
|
|
+ val ct = httpTts.contentType
|
|
|
|
|
+ if (ct?.isNotBlank() == true) {
|
|
|
|
|
+ response.headers["Content-Type"]?.let { contentType ->
|
|
|
|
|
+ if (!contentType.matches(ct.toRegex())) {
|
|
|
|
|
+ throw NoStackTraceException("TTS服务器返回错误:" + response.body!!.string())
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ ensureActive()
|
|
|
|
|
+ downloadErrorNo = 0
|
|
|
|
|
+ return response.body!!.byteStream()
|
|
|
|
|
+ } catch (e: Exception) {
|
|
|
|
|
+ when (e) {
|
|
|
|
|
+ is CancellationException -> throw e
|
|
|
|
|
+ is ConcurrentException -> delay(e.waitTime.toLong())
|
|
|
|
|
+ is ScriptException, is WrappedException -> {
|
|
|
|
|
+ AppLog.put("js错误\n${e.localizedMessage}", e)
|
|
|
|
|
+ toastOnUi("js错误\n${e.localizedMessage}")
|
|
|
|
|
+ e.printOnDebug()
|
|
|
|
|
+ throw e
|
|
|
|
|
+ }
|
|
|
|
|
+ is SocketTimeoutException, is ConnectException -> {
|
|
|
|
|
+ downloadErrorNo++
|
|
|
|
|
+ if (downloadErrorNo > 5) {
|
|
|
|
|
+ val msg = "tts超时或连接错误超过5次\n${e.localizedMessage}"
|
|
|
|
|
+ AppLog.put(msg, e)
|
|
|
|
|
+ toastOnUi(msg)
|
|
|
|
|
+ throw e
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ else -> {
|
|
|
|
|
+ downloadErrorNo++
|
|
|
|
|
+ val msg = "tts下载错误\n${e.localizedMessage}"
|
|
|
|
|
+ AppLog.put(msg, e)
|
|
|
|
|
+ e.printOnDebug()
|
|
|
|
|
+ if (downloadErrorNo > 5) {
|
|
|
|
|
+ val msg1 = "TTS服务器连续5次错误,已暂停阅读。"
|
|
|
|
|
+ AppLog.put(msg1)
|
|
|
|
|
+ toastOnUi(msg1)
|
|
|
|
|
+ throw e
|
|
|
|
|
+ } else {
|
|
|
|
|
+ AppLog.put("TTS下载音频出错,使用无声音频代替。\n朗读文本:$speakText")
|
|
|
|
|
+ break
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
- }.onStart {
|
|
|
|
|
- downloadTaskIsActive = true
|
|
|
|
|
- }.onError {
|
|
|
|
|
- AppLog.put("朗读下载出错\n${it.localizedMessage}", it)
|
|
|
|
|
- }.onFinally {
|
|
|
|
|
- downloadTaskIsActive = false
|
|
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
+ return null
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
@Synchronized
|
|
@Synchronized
|
|
@@ -238,7 +231,7 @@ class HttpReadAloudService : BaseReadAloudService(),
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
private fun createSilentSound(fileName: String) {
|
|
private fun createSilentSound(fileName: String) {
|
|
|
- val file = createSpeakFileAsMd5IfNotExist(fileName)
|
|
|
|
|
|
|
+ val file = createSpeakFile(fileName)
|
|
|
file.writeBytes(resources.openRawResource(R.raw.silent_sound).readBytes())
|
|
file.writeBytes(resources.openRawResource(R.raw.silent_sound).readBytes())
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -250,10 +243,18 @@ class HttpReadAloudService : BaseReadAloudService(),
|
|
|
return File("${ttsFolderPath}$name.mp3")
|
|
return File("${ttsFolderPath}$name.mp3")
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- private fun createSpeakFileAsMd5IfNotExist(name: String): File {
|
|
|
|
|
|
|
+ private fun createSpeakFile(name: String): File {
|
|
|
return FileUtils.createFileIfNotExist("${ttsFolderPath}$name.mp3")
|
|
return FileUtils.createFileIfNotExist("${ttsFolderPath}$name.mp3")
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ private fun createSpeakFile(name: String, inputStream: InputStream) {
|
|
|
|
|
+ FileUtils.createFileIfNotExist("${ttsFolderPath}$name.mp3").outputStream().use { out ->
|
|
|
|
|
+ inputStream.use {
|
|
|
|
|
+ it.copyTo(out)
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
/**
|
|
/**
|
|
|
* 移除缓存文件
|
|
* 移除缓存文件
|
|
|
*/
|
|
*/
|