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() var simulatedChapterSize = 0 var mCallback: Callback? = null var preDownloadTask: Job? = null val downloadedChapters = hashSetOf() val downloadFailChapters = hashMapOf() 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() 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() 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() } }