Files
QieKan-3.0/app/src/main/java/io/legato/kazusa/model/ReadManga.kt
T
2025-06-04 13:33:51 +08:00

625 lines
21 KiB
Kotlin

package io.legato.kazusa.model
import io.legato.kazusa.constant.AppLog
import io.legato.kazusa.data.appDb
import io.legato.kazusa.data.entities.Book
import io.legato.kazusa.data.entities.BookChapter
import io.legato.kazusa.data.entities.BookProgress
import io.legato.kazusa.data.entities.BookSource
import io.legato.kazusa.data.entities.ReadRecord
import io.legato.kazusa.help.AppWebDav
import io.legato.kazusa.help.ConcurrentRateLimiter
import io.legato.kazusa.help.book.BookHelp
import io.legato.kazusa.help.book.ContentProcessor
import io.legato.kazusa.help.book.isLocal
import io.legato.kazusa.help.book.isSameNameAuthor
import io.legato.kazusa.help.book.readSimulating
import io.legato.kazusa.help.book.simulatedTotalChapterNum
import io.legato.kazusa.help.book.update
import io.legato.kazusa.help.config.AppConfig
import io.legato.kazusa.help.coroutine.Coroutine
import io.legato.kazusa.help.globalExecutor
import io.legato.kazusa.model.webBook.WebBook
import io.legato.kazusa.ui.book.manga.entities.BaseMangaPage
import io.legato.kazusa.ui.book.manga.entities.MangaChapter
import io.legato.kazusa.ui.book.manga.entities.MangaContent
import io.legato.kazusa.ui.book.manga.entities.MangaPage
import io.legato.kazusa.ui.book.manga.entities.ReaderLoading
import io.legato.kazusa.utils.mapIndexed
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.CoroutineStart
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.Job
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancelChildren
import kotlinx.coroutines.delay
import kotlinx.coroutines.ensureActive
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.toList
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Semaphore
import kotlin.math.min
@Suppress("MemberVisibilityCanBePrivate")
object ReadManga : CoroutineScope by MainScope() {
var inBookshelf = false
var book: Book? = null
val executor = globalExecutor
var durChapterIndex = 0 //章节位置
var chapterSize = 0//总章节
var durChapterPos = 0
var chapterChanged = false
var prevMangaChapter: MangaChapter? = null
var curMangaChapter: MangaChapter? = null
var nextMangaChapter: MangaChapter? = null
var bookSource: BookSource? = null
var readStartTime: Long = System.currentTimeMillis()
private val readRecord = ReadRecord()
private val loadingChapters = arrayListOf<Int>()
var simulatedChapterSize = 0
var mCallback: Callback? = null
var preDownloadTask: Job? = null
val downloadedChapters = hashSetOf<Int>()
val downloadFailChapters = hashMapOf<Int, Int>()
val downloadScope = CoroutineScope(SupervisorJob() + IO)
val preDownloadSemaphore = Semaphore(2)
var rateLimiter = ConcurrentRateLimiter(null)
val mangaContents get() = buildMangaContent()
val hasNextChapter get() = durChapterIndex < simulatedChapterSize - 1
fun resetData(book: Book) {
ReadManga.book = book
readRecord.bookName = book.name
readRecord.readTime = appDb.readRecordDao.getReadTime(book.name) ?: 0
chapterSize = appDb.bookChapterDao.getChapterCount(book.bookUrl)
simulatedChapterSize = if (book.readSimulating()) {
book.simulatedTotalChapterNum()
} else {
chapterSize
}
durChapterIndex = book.durChapterIndex
durChapterPos = book.durChapterPos
clearMangaChapter()
upWebBook(book)
synchronized(this) {
loadingChapters.clear()
downloadedChapters.clear()
downloadFailChapters.clear()
}
}
fun upData(book: Book) {
ReadManga.book = book
chapterSize = appDb.bookChapterDao.getChapterCount(book.bookUrl)
simulatedChapterSize = if (book.readSimulating()) {
book.simulatedTotalChapterNum()
} else {
chapterSize
}
if (durChapterIndex != book.durChapterIndex) {
durChapterIndex = book.durChapterIndex
durChapterPos = book.durChapterPos
clearMangaChapter()
}
upWebBook(book)
synchronized(this) {
loadingChapters.clear()
downloadedChapters.clear()
downloadFailChapters.clear()
}
}
fun upWebBook(book: Book) {
appDb.bookSourceDao.getBookSource(book.origin)?.let {
bookSource = it
rateLimiter = ConcurrentRateLimiter(it)
} ?: let {
bookSource = null
}
}
fun clearMangaChapter() {
prevMangaChapter = null
curMangaChapter = null
nextMangaChapter = null
}
//每次切换章节更新阅读记录
fun upReadTime() {
executor.execute {
if (!AppConfig.enableReadRecord) {
return@execute
}
readRecord.readTime = readRecord.readTime + System.currentTimeMillis() - readStartTime
readStartTime = System.currentTimeMillis()
readRecord.lastRead = System.currentTimeMillis()
appDb.readRecordDao.insert(readRecord)
}
}
@Synchronized
private fun addLoading(index: Int): Boolean {
if (loadingChapters.contains(index)) return false
loadingChapters.add(index)
return true
}
@Synchronized
fun removeLoading(index: Int) {
loadingChapters.remove(index)
}
fun loadContent() {
clearMangaChapter()
loadContent(durChapterIndex)
loadContent(durChapterIndex + 1)
loadContent(durChapterIndex - 1)
}
fun loadOrUpContent() {
if (curMangaChapter == null) {
loadContent(durChapterIndex)
} else {
mCallback?.upContent()
}
if (nextMangaChapter == null) {
loadContent(durChapterIndex + 1)
}
if (prevMangaChapter == null) {
loadContent(durChapterIndex - 1)
}
}
private fun loadContent(index: Int) {
Coroutine.async {
val book = book!!
val chapter = appDb.bookChapterDao.getChapter(book.bookUrl, index) ?: return@async
if (addLoading(index)) {
BookHelp.getContent(book, chapter)?.let {
contentLoadFinish(chapter, it)
} ?: run {
download(downloadScope, chapter)
}
}
}.onError {
AppLog.put("加载正文出错\n${it.localizedMessage}")
}
}
/**
* 内容加载完成
*/
suspend fun contentLoadFinish(
chapter: BookChapter,
content: String?,
errorMsg: String = "加载内容失败",
canceled: Boolean = false
) {
removeLoading(chapter.index)
if (canceled || chapter.index !in durChapterIndex - 1..durChapterIndex + 1) {
return
}
when (val offset = chapter.index - durChapterIndex) {
0 -> {
if (content == null) {
mCallback?.loadFail(errorMsg)
return
}
if (content.isEmpty() && !chapter.isVolume) {
mCallback?.loadFail("正文内容为空")
return
}
val mangaChapter = getManageChapter(chapter, content)
if (mangaChapter.imageCount == 0 && !chapter.isVolume) {
mCallback?.loadFail("正文没有图片")
return
}
curMangaChapter = mangaChapter
mCallback?.upContent()
}
-1, 1 -> {
if (content == null || (!chapter.isVolume && content.isEmpty())) {
return
}
val mangaChapter = getManageChapter(chapter, content)
if (mangaChapter.imageCount == 0 && !chapter.isVolume) {
return
}
when (offset) {
-1 -> prevMangaChapter = mangaChapter
1 -> nextMangaChapter = mangaChapter
}
mCallback?.upContent()
}
}
}
private fun buildMangaContent(): MangaContent {
val items = arrayListOf<BaseMangaPage>()
var pos = 0
var curFinish = false
var nextFinish = false
prevMangaChapter?.let {
pos += it.pages.size
items.addAll(it.pages)
}
curMangaChapter?.let {
curFinish = true
items.addAll(it.pages)
durChapterPos = if (it.imageCount > 0) {
durChapterPos.coerceIn(0, it.imageCount - 1)
} else {
0
}
pos += durChapterPos
if (!AppConfig.hideMangaTitle && it.imageCount > 0) {
pos++
}
}
nextMangaChapter?.let {
nextFinish = true
items.addAll(it.pages)
}
return MangaContent(pos, items, curFinish, nextFinish)
}
/**
* 加载下一章
*/
fun moveToNextChapter(toFirst: Boolean = false): Boolean {
if (durChapterIndex < simulatedChapterSize - 1) {
if (toFirst) {
mCallback?.showLoading()
durChapterPos = 0
}
durChapterIndex++
prevMangaChapter = curMangaChapter
curMangaChapter = nextMangaChapter
nextMangaChapter = null
if (curMangaChapter == null) {
mCallback?.startLoad()
loadContent(durChapterIndex)
} else {
mCallback?.upContent()
}
loadContent(durChapterIndex + 1)
saveRead()
AppLog.putDebug("moveToNextChapter-curPageChanged()")
curPageChanged()
return true
} else {
AppLog.putDebug("跳转下一章失败,没有下一章")
return false
}
}
fun moveToPrevChapter(toFirst: Boolean = false): Boolean {
if (durChapterIndex > 0) {
if (toFirst) {
mCallback?.showLoading()
durChapterPos = 0
}
durChapterIndex--
nextMangaChapter = curMangaChapter
curMangaChapter = prevMangaChapter
prevMangaChapter = null
if (curMangaChapter == null) {
loadContent(durChapterIndex)
} else {
mCallback?.upContent()
}
loadContent(durChapterIndex - 1)
saveRead()
return true
}
return false
}
fun curPageChanged() {
upReadTime()
preDownload()
}
fun saveRead(pageChanged: Boolean = false) {
executor.execute {
val book = book ?: return@execute
book.lastCheckCount = 0
book.durChapterTime = System.currentTimeMillis()
val chapterChanged = book.durChapterIndex != durChapterIndex
book.durChapterIndex = durChapterIndex
book.durChapterPos = durChapterPos
if (!pageChanged || chapterChanged) {
appDb.bookChapterDao.getChapter(book.bookUrl, durChapterIndex)?.let {
book.durChapterTitle = it.getDisplayTitle(
ContentProcessor.get(book.name, book.origin).getTitleReplaceRules(),
book.getUseReplaceRule()
)
}
}
appDb.bookDao.update(book)
}
}
private fun downloadNetworkContent(
bookSource: BookSource,
scope: CoroutineScope,
chapter: BookChapter,
book: Book,
semaphore: Semaphore?,
success: suspend (String) -> Unit = {},
error: suspend () -> Unit = {},
cancel: suspend () -> Unit = {},
) {
WebBook.getContent(
scope,
bookSource,
book,
chapter,
start = CoroutineStart.LAZY,
executeContext = IO,
semaphore = semaphore
).onSuccess { content ->
success.invoke(content)
}.onError {
error.invoke()
}.onCancel {
cancel.invoke()
}.start()
}
private fun preDownload() {
if (book?.isLocal == true) return
executor.execute {
if (AppConfig.preDownloadNum < 2) {
return@execute
}
preDownloadTask?.cancel()
preDownloadTask = launch(IO) {
//预下载
launch {
val maxChapterIndex =
min(durChapterIndex + AppConfig.preDownloadNum, chapterSize)
for (i in durChapterIndex.plus(2)..maxChapterIndex) {
if (downloadedChapters.contains(i)) continue
if ((downloadFailChapters[i] ?: 0) >= 3) continue
downloadIndex(i)
}
}
launch {
val minChapterIndex = durChapterIndex - min(5, AppConfig.preDownloadNum)
for (i in durChapterIndex.minus(2) downTo minChapterIndex) {
if (downloadedChapters.contains(i)) continue
if ((downloadFailChapters[i] ?: 0) >= 3) continue
downloadIndex(i)
}
}
}
}
}
fun cancelPreDownloadTask() {
if (curMangaChapter != null && nextMangaChapter != null) {
preDownloadTask?.cancel()
downloadScope.coroutineContext.cancelChildren()
}
}
private suspend fun downloadIndex(index: Int) {
if (index < 0) return
if (index > chapterSize - 1) {
upToc()
return
}
val book = book ?: return
val chapter = appDb.bookChapterDao.getChapter(book.bookUrl, index) ?: return
if (BookHelp.hasContent(book, chapter)) {
downloadedChapters.add(chapter.index)
} else {
delay(1000)
if (addLoading(index)) {
download(downloadScope, chapter, preDownloadSemaphore)
}
}
}
/**
* 获取正文
*/
private suspend fun download(
scope: CoroutineScope,
chapter: BookChapter,
semaphore: Semaphore? = null,
) {
val book = book ?: return removeLoading(chapter.index)
val bookSource = bookSource
if (bookSource != null) {
downloadNetworkContent(bookSource, scope, chapter, book, semaphore, success = {
downloadedChapters.add(chapter.index)
downloadFailChapters.remove(chapter.index)
contentLoadFinish(chapter, it)
}, error = {
downloadFailChapters[chapter.index] =
(downloadFailChapters[chapter.index] ?: 0) + 1
contentLoadFinish(chapter, null)
}, cancel = {
contentLoadFinish(chapter, null, canceled = true)
})
} else {
contentLoadFinish(chapter, null, "加载内容失败 没有书源")
}
}
@Synchronized
fun upToc() {
val bookSource = bookSource ?: return
val book = book ?: return
if (!book.canUpdate) return
if (System.currentTimeMillis() - book.lastCheckTime < 600000) return
book.lastCheckTime = System.currentTimeMillis()
WebBook.getChapterList(this, bookSource, book).onSuccess(IO) { cList ->
if (book.bookUrl == ReadManga.book?.bookUrl
&& cList.size > chapterSize
) {
appDb.bookChapterDao.delByBook(book.bookUrl)
appDb.bookChapterDao.insert(*cList.toTypedArray())
saveRead()
chapterSize = cList.size
simulatedChapterSize = book.simulatedTotalChapterNum()
nextMangaChapter ?: loadContent(durChapterIndex + 1)
}
}
}
fun uploadProgress(successAction: (() -> Unit)? = null) {
book?.let {
launch(IO) {
AppWebDav.uploadBookProgress(it)
ensureActive()
it.update()
successAction?.invoke()
}
}
}
/**
* 同步阅读进度
* 如果当前进度快于服务器进度或者没有进度进行上传,如果慢与服务器进度则执行传入动作
*/
fun syncProgress(
newProgressAction: ((progress: BookProgress) -> Unit)? = null,
uploadSuccessAction: (() -> Unit)? = null,
syncSuccessAction: (() -> Unit)? = null,
) {
if (!AppConfig.syncBookProgress) return
val book = book ?: return
Coroutine.async {
AppWebDav.getBookProgress(book)
}.onError {
AppLog.put("拉取阅读进度失败", it)
}.onSuccess { progress ->
if (progress == null || progress.durChapterIndex < book.durChapterIndex ||
(progress.durChapterIndex == book.durChapterIndex
&& progress.durChapterPos < book.durChapterPos)
) {
// 服务器没有进度或者进度比服务器快,上传现有进度
Coroutine.async {
AppWebDav.uploadBookProgress(BookProgress(book), uploadSuccessAction)
book.update()
}
} else if (progress.durChapterIndex > book.durChapterIndex ||
progress.durChapterPos > book.durChapterPos
) {
// 进度比服务器慢,执行传入动作
newProgressAction?.invoke(progress)
} else {
syncSuccessAction?.invoke()
}
}
}
fun setProgress(progress: BookProgress) {
if (progress.durChapterIndex < chapterSize &&
(durChapterIndex != progress.durChapterIndex
|| durChapterPos != progress.durChapterPos)
) {
mCallback?.showLoading()
if (progress.durChapterIndex == durChapterIndex) {
durChapterPos = progress.durChapterPos
mCallback?.upContent()
} else {
durChapterIndex = progress.durChapterIndex
durChapterPos = progress.durChapterPos
loadContent()
}
saveRead()
}
}
fun showLoading() {
mCallback?.showLoading()
}
fun onChapterListUpdated(newBook: Book) {
if (newBook.isSameNameAuthor(book)) {
book = newBook
chapterSize = newBook.totalChapterNum
simulatedChapterSize = newBook.simulatedTotalChapterNum()
if (simulatedChapterSize > 0 && durChapterIndex > simulatedChapterSize - 1) {
durChapterIndex = simulatedChapterSize - 1
}
if (mCallback == null) {
clearMangaChapter()
} else {
loadContent()
}
}
}
/**
* 注册回调
*/
fun register(cb: Callback) {
mCallback = cb
}
/**
* 取消注册回调
*/
fun unregister(cb: Callback) {
if (mCallback === cb) {
mCallback = null
}
preDownloadTask?.cancel()
preDownloadTask = null
downloadScope.coroutineContext.cancelChildren()
coroutineContext.cancelChildren()
}
private suspend fun getManageChapter(chapter: BookChapter, content: String): MangaChapter {
val list = BookHelp.flowImages(chapter, content)
.distinctUntilChanged().mapIndexed { index, src ->
MangaPage(
chapterIndex = chapter.index,
chapterSize = chapterSize,
mImageUrl = src,
index = index,
mChapterName = chapter.title
)
}.toList()
val imageCount = list.size
list.forEach {
it.imageCount = imageCount
}
if (AppConfig.hideMangaTitle && imageCount > 0) {
return MangaChapter(chapter, list, imageCount)
}
val pages = mutableListOf<BaseMangaPage>()
if (imageCount == 0 && chapter.isVolume) {
pages.add(ReaderLoading(chapter.index, -1, chapter.title, true))
} else {
pages.add(ReaderLoading(chapter.index, -1, "阅读 ${chapter.title}"))
pages.addAll(list)
pages.add(ReaderLoading(chapter.index, imageCount, "已读完 ${chapter.title}"))
}
return MangaChapter(chapter, pages, imageCount)
}
interface Callback {
fun upContent()
fun loadFail(msg: String)
fun sureNewProgress(progress: BookProgress)
fun showLoading()
fun startLoad()
}
}