From 72a6419affbfdb6d10200146dd11d6a7356d91c7 Mon Sep 17 00:00:00 2001 From: tarsin Date: Fri, 17 Feb 2023 14:21:24 +0800 Subject: [PATCH] Reorganize SpiderQueen code --- .../com/hippo/ehviewer/spider/SpiderQueen.kt | 367 ++++++++++++++++-- .../ehviewer/spider/SpiderQueenWorker.kt | 322 --------------- 2 files changed, 336 insertions(+), 353 deletions(-) delete mode 100644 app/src/main/java/com/hippo/ehviewer/spider/SpiderQueenWorker.kt diff --git a/app/src/main/java/com/hippo/ehviewer/spider/SpiderQueen.kt b/app/src/main/java/com/hippo/ehviewer/spider/SpiderQueen.kt index 9b04a8d01..c515697ff 100644 --- a/app/src/main/java/com/hippo/ehviewer/spider/SpiderQueen.kt +++ b/app/src/main/java/com/hippo/ehviewer/spider/SpiderQueen.kt @@ -1,5 +1,6 @@ /* * Copyright 2016 Hippo Seven + * Rewrite with Kotlin coroutines, Tarsin Norbin 2023 * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,13 +16,17 @@ */ package com.hippo.ehviewer.spider +import android.util.Log import androidx.annotation.IntDef import androidx.collection.LongSparseArray import androidx.collection.set import com.hippo.ehviewer.EhApplication.Companion.okHttpClient import com.hippo.ehviewer.GetText import com.hippo.ehviewer.R +import com.hippo.ehviewer.Settings +import com.hippo.ehviewer.client.EhEngine import com.hippo.ehviewer.client.EhRequestBuilder +import com.hippo.ehviewer.client.EhUrl import com.hippo.ehviewer.client.EhUrl.getGalleryDetailUrl import com.hippo.ehviewer.client.EhUrl.getGalleryMultiPageViewerUrl import com.hippo.ehviewer.client.EhUrl.referer @@ -35,12 +40,21 @@ import com.hippo.ehviewer.client.parser.GalleryPageUrlParser import com.hippo.image.Image import com.hippo.unifile.UniFile import com.hippo.util.ExceptionUtils +import com.hippo.yorozuya.StringUtils import eu.kanade.tachiyomi.util.lang.launchIO import eu.kanade.tachiyomi.util.lang.launchNonCancellable import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.cancel +import kotlinx.coroutines.currentCoroutineContext +import kotlinx.coroutines.delay +import kotlinx.coroutines.ensureActive +import kotlinx.coroutines.launch +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.Semaphore +import kotlinx.coroutines.sync.withLock +import kotlinx.coroutines.sync.withPermit import moe.tarsin.coroutines.runSuspendCatching import okhttp3.executeAsync import java.util.concurrent.ConcurrentHashMap @@ -48,8 +62,11 @@ import java.util.concurrent.atomic.AtomicInteger import kotlin.coroutines.CoroutineContext class SpiderQueen private constructor(val galleryInfo: GalleryInfo) : CoroutineScope { - val mSpiderDen: SpiderDen = SpiderDen(galleryInfo) + @Volatile + lateinit var mPageStateArray: IntArray lateinit var mSpiderInfo: SpiderInfo + + val mSpiderDen: SpiderDen = SpiderDen(galleryInfo) val mPagePercentMap = ConcurrentHashMap() private val mPageStateLock = Any() private val mDownloadedPages = AtomicInteger(0) @@ -57,12 +74,10 @@ class SpiderQueen private constructor(val galleryInfo: GalleryInfo) : CoroutineS private val mPageErrorMap = ConcurrentHashMap() private val mSpiderListeners: MutableList = ArrayList() - @Volatile - lateinit var mPageStateArray: IntArray private var mReadReference = 0 private var mDownloadReference = 0 - private val mWorkerScope = SpiderQueenWorker(this) + private val mWorkerScope = SpiderQueenWorker() fun addOnSpiderListener(listener: OnSpiderListener) { synchronized(mSpiderListeners) { mSpiderListeners.add(listener) } @@ -74,32 +89,33 @@ class SpiderQueen private constructor(val galleryInfo: GalleryInfo) : CoroutineS private fun notifyGetPages(pages: Int) { synchronized(mSpiderListeners) { - for (listener in mSpiderListeners) { - listener.onGetPages(pages) - } + mSpiderListeners.forEach { it.onGetPages(pages) } } } fun notifyGet509(index: Int) { synchronized(mSpiderListeners) { - for (listener in mSpiderListeners) { - listener.onGet509(index) - } + mSpiderListeners.forEach { it.onGet509(index) } } } fun notifyPageDownload(index: Int, contentLength: Long, receivedSize: Long, bytesRead: Int) { synchronized(mSpiderListeners) { - for (listener in mSpiderListeners) { - listener.onPageDownload(index, contentLength, receivedSize, bytesRead) + mSpiderListeners.forEach { + it.onPageDownload( + index, + contentLength, + receivedSize, + bytesRead + ) } } } private fun notifyPageSuccess(index: Int) { synchronized(mSpiderListeners) { - for (listener in mSpiderListeners) { - listener.onPageSuccess( + mSpiderListeners.forEach { + it.onPageSuccess( index, mFinishedPages.get(), mDownloadedPages.get(), @@ -111,8 +127,8 @@ class SpiderQueen private constructor(val galleryInfo: GalleryInfo) : CoroutineS private fun notifyPageFailure(index: Int, error: String?) { synchronized(mSpiderListeners) { - for (listener in mSpiderListeners) { - listener.onPageFailure( + mSpiderListeners.forEach { + it.onPageFailure( index, error, mFinishedPages.get(), @@ -125,8 +141,8 @@ class SpiderQueen private constructor(val galleryInfo: GalleryInfo) : CoroutineS private fun notifyAllPageDownloaded() { synchronized(mSpiderListeners) { - for (listener in mSpiderListeners) { - listener.onFinish( + mSpiderListeners.forEach { + it.onFinish( mFinishedPages.get(), mDownloadedPages.get(), mPageStateArray.size @@ -137,21 +153,21 @@ class SpiderQueen private constructor(val galleryInfo: GalleryInfo) : CoroutineS fun notifyGetImageSuccess(index: Int, image: Image) { synchronized(mSpiderListeners) { - for (listener in mSpiderListeners) { - listener.onGetImageSuccess(index, image) + mSpiderListeners.forEach { + it.onGetImageSuccess(index, image) } } } fun notifyGetImageFailure(index: Int, error: String) { synchronized(mSpiderListeners) { - for (listener in mSpiderListeners) { - listener.onGetImageFailure(index, error) + mSpiderListeners.forEach { + it.onGetImageFailure(index, error) } } } - private var mInDownloadMode = false + private var downloadMode = false @Synchronized private fun updateMode() { @@ -164,7 +180,7 @@ class SpiderQueen private constructor(val galleryInfo: GalleryInfo) : CoroutineS // Update download page val intoDownloadMode = mode == MODE_DOWNLOAD - if (intoDownloadMode && !mInDownloadMode) { + if (intoDownloadMode && !downloadMode) { // Clear download state synchronized(mPageStateLock) { val temp: IntArray = mPageStateArray @@ -184,7 +200,7 @@ class SpiderQueen private constructor(val galleryInfo: GalleryInfo) : CoroutineS } mWorkerScope.enterDownloadMode() } - mInDownloadMode = intoDownloadMode + downloadMode = intoDownloadMode } private fun setMode(@Mode mode: Int) { @@ -211,14 +227,14 @@ class SpiderQueen private constructor(val galleryInfo: GalleryInfo) : CoroutineS } } - private var prepareJob = launchIO { doPrepare() } + private val prepareJob = launchIO { doPrepare() } private suspend fun doPrepare() { mSpiderInfo = readSpiderInfoFromLocal() ?: readSpiderInfoFromInternet() ?: return mPageStateArray = IntArray(mSpiderInfo.pages) } - suspend fun awaitReady() { + private suspend fun awaitReady() { prepareJob.join() notifyGetPages(mSpiderInfo.pages) } @@ -520,9 +536,298 @@ class SpiderQueen private constructor(val galleryInfo: GalleryInfo) : CoroutineS override val coroutineContext: CoroutineContext get() = Dispatchers.IO + Job() + + inner class SpiderQueenWorker { + private val mFetcherJobMap = hashMapOf() + private val mSemaphore = Semaphore(Settings.getMultiThreadDownload()) + private val pTokenLock = Mutex() + private var showKey: String? = null + private val showKeyLock = Mutex() + private val mDownloadDelay = Settings.getDownloadDelay() + private var isDownloadMode = false + + fun cancelDecode(index: Int) { + decoder.cancel(index) + } + + @Synchronized + fun enterDownloadMode() { + if (isDownloadMode) return + updateRAList((0 until size).toList()) + isDownloadMode = true + } + + fun updateRAList(list: List, cancelBounds: Pair = 0 to Int.MAX_VALUE) { + if (isDownloadMode) return + synchronized(mFetcherJobMap) { + mFetcherJobMap.forEach { (i, job) -> + if (i < cancelBounds.first || i > cancelBounds.second) + job.cancel() + } + list.forEach { + if (mFetcherJobMap[it]?.isActive != true) + doLaunchDownloadJob(it, false) + } + } + } + + private fun doLaunchDownloadJob(index: Int, force: Boolean) { + val state = mPageStateArray[index] + if (!force && state == STATE_FINISHED) return + val currentJob = mFetcherJobMap[index] + if (force) currentJob?.cancel() + if (currentJob?.isActive != true) { + mFetcherJobMap[index] = launch { + mSemaphore.withPermit { + doInJob(index, force) + } + } + } + } + + @JvmOverloads + fun launch(index: Int, force: Boolean = false) { + check(index in 0 until size) + if (!isDownloadMode) synchronized(mFetcherJobMap) { doLaunchDownloadJob(index, force) } + if (force) decoder.cancel(index) + decoder.launch(index) + } + + private suspend fun doInJob(index: Int, force: Boolean) { + fun getPToken(index: Int): String? { + if (index !in 0 until size) return null + return mSpiderInfo.pTokenMap[index].takeIf { it != TOKEN_FAILED } + ?: getPTokenFromInternet(index) + ?: getPTokenFromInternet(index) + ?: getPTokenFromMultiPageViewer(index) + } + updatePageState(index, STATE_DOWNLOADING) + if (!force && index in mSpiderDen) { + return updatePageState(index, STATE_FINISHED) + } + if (force) { + pTokenLock.withLock { + val pToken = mSpiderInfo.pTokenMap[index] + if (pToken == TOKEN_FAILED) mSpiderInfo.pTokenMap.remove(index) + } + } + val previousPToken: String? + val pToken: String + + pTokenLock.withLock { + pToken = getPToken(index) ?: return updatePageState( + index, + STATE_FAILED, + PTOKEN_FAILED_MESSAGE + ).also { + mSpiderInfo.pTokenMap[index] = TOKEN_FAILED + } + previousPToken = getPToken(index - 1) + } + + var skipHathKey: String? = null + val skipHathKeys = mutableListOf() + var originImageUrl: String? = null + var error: String? = null + var forceHtml = false + var leakSkipHathKey = false + repeat(3) { + var imageUrl: String? = null + var localShowKey: String? + + showKeyLock.withLock { + localShowKey = showKey + if (localShowKey == null || forceHtml) { + if (leakSkipHathKey) return@repeat + var pageUrl = EhUrl.getPageUrl(mSpiderInfo.gid, index, pToken) + // Add skipHathKey + if (skipHathKey != null) { + pageUrl += if ("?" in pageUrl) { + "&nl=$skipHathKey" + } else { + "?nl=$skipHathKey" + } + } + runSuspendCatching { + EhEngine.getGalleryPage(pageUrl, mSpiderInfo.gid, mSpiderInfo.token) + .also { + if (StringUtils.endsWith(it.imageUrl, URL_509_SUFFIX_ARRAY)) { + // Get 509 + notifyGet509(index) + error = ERROR_509 + return@repeat + } + } + }.onSuccess { result -> + imageUrl = result.imageUrl + skipHathKey = result.skipHathKey.takeIf { it.isNotBlank() } + originImageUrl = result.originImageUrl + localShowKey = result.showKey + + if (skipHathKey != null) { + if (skipHathKey in skipHathKeys) { + // Duplicate skip hath key + leakSkipHathKey = true + } else { + skipHathKeys.add(skipHathKey!!) + } + } else { + leakSkipHathKey = true + } + + showKey = result.showKey + }.onFailure { + if (it is ParseException && "Key mismatch" == it.message) { + // Show key is wrong, enter a new loop to get the new show key + if (showKey == localShowKey) showKey = null + return@repeat + } else { + error = ExceptionUtils.getReadableString(it) + return@repeat + } + } + } + } + + if (imageUrl == null) { + if (localShowKey == null) { + error = "ShowKey error" + return@repeat + } + runSuspendCatching { + EhEngine.getGalleryPageApi( + mSpiderInfo.gid, + index, + pToken, + localShowKey, + previousPToken + ).also { + if (StringUtils.endsWith(it.imageUrl, URL_509_SUFFIX_ARRAY)) { + // Get 509 + notifyGet509(index) + error = ERROR_509 + return@repeat + } + } + }.onFailure { + if (it is ParseException && "Key mismatch" == it.message) { + // Show key is wrong, enter a new loop to get the new show key + if (showKey == localShowKey) showKey = null + } else { + error = ExceptionUtils.getReadableString(it) + } + return@repeat + }.onSuccess { + imageUrl = it.imageUrl + skipHathKey = it.skipHathKey + originImageUrl = it.originImageUrl + } + } + + val targetImageUrl: String? + val referer: String? + + if (Settings.getDownloadOriginImage() && !originImageUrl.isNullOrBlank()) { + targetImageUrl = originImageUrl + referer = EhUrl.getPageUrl(mSpiderInfo.gid, index, pToken) + } else { + targetImageUrl = imageUrl + referer = null + } + if (targetImageUrl == null) { + error = "TargetImageUrl error" + return@repeat + } + Log.d(WORKER_DEBUG_TAG, targetImageUrl) + + runSuspendCatching { + Log.d(WORKER_DEBUG_TAG, "Start download image $index") + val success: Boolean = mSpiderDen.makeHttpCallAndSaveImage( + index, + targetImageUrl, + referer + ) { contentLength: Long, receivedSize: Long, bytesRead: Int -> + mPagePercentMap[index] = receivedSize.toFloat() / contentLength + notifyPageDownload(index, contentLength, receivedSize, bytesRead) + } + + if (!success) { + Log.e(WORKER_DEBUG_TAG, "Can't download all of image data") + error = "Incomplete" + forceHtml = true + return@repeat + } + + if (mSpiderDen.checkPlainText(index)) { + error = "" + forceHtml = true + return@repeat + } + + Log.d(WORKER_DEBUG_TAG, "Download image succeed $index") + + updatePageState(index, STATE_FINISHED) + delay(mDownloadDelay.toLong()) + return + }.onFailure { + it.printStackTrace() + error = NETWORK_ERROR + } + Log.d(WORKER_DEBUG_TAG, "End download image $index") + } + mSpiderDen.remove(index) + updatePageState(index, STATE_FAILED, error) + } + + private val decoder = Decoder() + + inner class Decoder { + private val mSemaphore = Semaphore(4) + private val mDecodeJobMap = hashMapOf() + + fun cancel(index: Int) { + synchronized(mDecodeJobMap) { + mDecodeJobMap.remove(index)?.cancel() + } + } + + fun launch(index: Int) { + synchronized(mDecodeJobMap) { + val currentJob = mDecodeJobMap[index] + if (currentJob?.isActive != true) { + mDecodeJobMap[index] = launch { + doInJob(index) + } + } + } + } + + private suspend fun doInJob(index: Int) { + mFetcherJobMap[index]?.takeIf { it.isActive }?.join() + val src = mSpiderDen.getImageSource(index) ?: return + val image = mSemaphore.withPermit { Image.decode(src) } + runCatching { + currentCoroutineContext().ensureActive() + }.onFailure { + image?.recycle() + throw it + } + if (image == null) { + notifyGetImageFailure(index, DECODE_ERROR) + } else { + notifyGetImageSuccess(index, image) + } + } + } + } } -var PTOKEN_FAILED_MESSAGE = GetText.getString(R.string.error_get_ptoken_error) -var ERROR_509 = GetText.getString(R.string.error_509) -var NETWORK_ERROR = GetText.getString(R.string.error_socket) -var DECODE_ERROR = GetText.getString(R.string.error_decoding_failed) \ No newline at end of file +private val PTOKEN_FAILED_MESSAGE = GetText.getString(R.string.error_get_ptoken_error) +private val ERROR_509 = GetText.getString(R.string.error_509) +private val NETWORK_ERROR = GetText.getString(R.string.error_socket) +private val DECODE_ERROR = GetText.getString(R.string.error_decoding_failed) +private val URL_509_SUFFIX_ARRAY = arrayOf( + "/509.gif", + "/509s.gif" +) +private const val WORKER_DEBUG_TAG = "SpiderQueenWorker" \ No newline at end of file diff --git a/app/src/main/java/com/hippo/ehviewer/spider/SpiderQueenWorker.kt b/app/src/main/java/com/hippo/ehviewer/spider/SpiderQueenWorker.kt deleted file mode 100644 index 4f5c9faf5..000000000 --- a/app/src/main/java/com/hippo/ehviewer/spider/SpiderQueenWorker.kt +++ /dev/null @@ -1,322 +0,0 @@ -package com.hippo.ehviewer.spider - -import android.util.Log -import com.hippo.ehviewer.Settings -import com.hippo.ehviewer.client.EhEngine.getGalleryPage -import com.hippo.ehviewer.client.EhEngine.getGalleryPageApi -import com.hippo.ehviewer.client.EhUrl.getPageUrl -import com.hippo.ehviewer.client.exception.ParseException -import com.hippo.ehviewer.spider.SpiderQueen.Companion.STATE_FAILED -import com.hippo.ehviewer.spider.SpiderQueen.Companion.STATE_FINISHED -import com.hippo.image.Image.Companion.decode -import com.hippo.util.ExceptionUtils -import com.hippo.yorozuya.StringUtils -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Job -import kotlinx.coroutines.currentCoroutineContext -import kotlinx.coroutines.delay -import kotlinx.coroutines.ensureActive -import kotlinx.coroutines.launch -import kotlinx.coroutines.sync.Mutex -import kotlinx.coroutines.sync.Semaphore -import kotlinx.coroutines.sync.withLock -import kotlinx.coroutines.sync.withPermit -import moe.tarsin.coroutines.runSuspendCatching - -class SpiderQueenWorker(private val queen: SpiderQueen) : CoroutineScope by queen { - private val spiderDen - get() = queen.mSpiderDen - private val spiderInfo - get() = queen.mSpiderInfo - private val mFetcherJobMap = hashMapOf() - private val mSemaphore = Semaphore(Settings.getMultiThreadDownload()) - private val pTokenLock = Mutex() - private var showKey: String? = null - private val showKeyLock = Mutex() - private val mDownloadDelay = Settings.getDownloadDelay() - private var isDownloadMode = false - private val size - get() = queen.size - - fun cancelDecode(index: Int) { - decoder.cancel(index) - } - - @Synchronized - fun enterDownloadMode() { - if (isDownloadMode) return - updateRAList((0 until size).toList()) - isDownloadMode = true - } - - fun updateRAList(list: List, cancelBounds: Pair = 0 to Int.MAX_VALUE) { - if (isDownloadMode) return - synchronized(mFetcherJobMap) { - mFetcherJobMap.forEach { (i, job) -> - if (i < cancelBounds.first || i > cancelBounds.second) - job.cancel() - } - list.forEach { - if (mFetcherJobMap[it]?.isActive != true) - doLaunchDownloadJob(it, false) - } - } - } - - private fun doLaunchDownloadJob(index: Int, force: Boolean) { - val state = queen.mPageStateArray[index] - if (!force && state == STATE_FINISHED) return - val currentJob = mFetcherJobMap[index] - if (force) currentJob?.cancel() - if (currentJob?.isActive != true) { - mFetcherJobMap[index] = launch { - mSemaphore.withPermit { - doInJob(index, force) - } - } - } - } - - @JvmOverloads - fun launch(index: Int, force: Boolean = false) { - check(index in 0 until size) - if (!isDownloadMode) synchronized(mFetcherJobMap) { doLaunchDownloadJob(index, force) } - if (force) decoder.cancel(index) - decoder.launch(index) - } - - private suspend fun doInJob(index: Int, force: Boolean) { - fun getPToken(index: Int): String? { - if (index !in 0 until size) return null - return spiderInfo.pTokenMap[index].takeIf { it != TOKEN_FAILED } - ?: queen.getPTokenFromInternet(index) - ?: queen.getPTokenFromInternet(index) - ?: queen.getPTokenFromMultiPageViewer(index) - } - queen.updatePageState(index, SpiderQueen.STATE_DOWNLOADING) - if (!force && index in spiderDen) { - return queen.updatePageState(index, STATE_FINISHED) - } - if (force) { - pTokenLock.withLock { - val pToken = spiderInfo.pTokenMap[index] - if (pToken == TOKEN_FAILED) spiderInfo.pTokenMap.remove(index) - } - } - val previousPToken: String? - val pToken: String - - pTokenLock.withLock { - pToken = getPToken(index) ?: return queen.updatePageState( - index, - STATE_FAILED, - PTOKEN_FAILED_MESSAGE - ).also { - spiderInfo.pTokenMap[index] = TOKEN_FAILED - } - previousPToken = getPToken(index - 1) - } - - var skipHathKey: String? = null - val skipHathKeys = mutableListOf() - var originImageUrl: String? = null - var error: String? = null - var forceHtml = false - var leakSkipHathKey = false - repeat(3) { - var imageUrl: String? = null - var localShowKey: String? - - showKeyLock.withLock { - localShowKey = showKey - if (localShowKey == null || forceHtml) { - if (leakSkipHathKey) return@repeat - var pageUrl = getPageUrl(spiderInfo.gid, index, pToken) - // Add skipHathKey - if (skipHathKey != null) { - pageUrl += if ("?" in pageUrl) { - "&nl=$skipHathKey" - } else { - "?nl=$skipHathKey" - } - } - runSuspendCatching { - getGalleryPage(pageUrl, spiderInfo.gid, spiderInfo.token).also { - if (StringUtils.endsWith(it.imageUrl, URL_509_SUFFIX_ARRAY)) { - // Get 509 - queen.notifyGet509(index) - error = ERROR_509 - return@repeat - } - } - }.onSuccess { result -> - imageUrl = result.imageUrl - skipHathKey = result.skipHathKey.takeIf { it.isNotBlank() } - originImageUrl = result.originImageUrl - localShowKey = result.showKey - - if (skipHathKey != null) { - if (skipHathKey in skipHathKeys) { - // Duplicate skip hath key - leakSkipHathKey = true - } else { - skipHathKeys.add(skipHathKey!!) - } - } else { - leakSkipHathKey = true - } - - showKey = result.showKey - }.onFailure { - if (it is ParseException && "Key mismatch" == it.message) { - // Show key is wrong, enter a new loop to get the new show key - if (showKey == localShowKey) showKey = null - return@repeat - } else { - error = ExceptionUtils.getReadableString(it) - return@repeat - } - } - } - } - - if (imageUrl == null) { - if (localShowKey == null) { - error = "ShowKey error" - return@repeat - } - runSuspendCatching { - getGalleryPageApi( - spiderInfo.gid, - index, - pToken, - localShowKey, - previousPToken - ).also { - if (StringUtils.endsWith(it.imageUrl, URL_509_SUFFIX_ARRAY)) { - // Get 509 - queen.notifyGet509(index) - error = ERROR_509 - return@repeat - } - } - }.onFailure { - if (it is ParseException && "Key mismatch" == it.message) { - // Show key is wrong, enter a new loop to get the new show key - if (showKey == localShowKey) showKey = null - } else { - error = ExceptionUtils.getReadableString(it) - } - return@repeat - }.onSuccess { - imageUrl = it.imageUrl - skipHathKey = it.skipHathKey - originImageUrl = it.originImageUrl - } - } - - val targetImageUrl: String? - val referer: String? - - if (Settings.getDownloadOriginImage() && !originImageUrl.isNullOrBlank()) { - targetImageUrl = originImageUrl - referer = getPageUrl(spiderInfo.gid, index, pToken) - } else { - targetImageUrl = imageUrl - referer = null - } - if (targetImageUrl == null) { - error = "TargetImageUrl error" - return@repeat - } - Log.d(DEBUG_TAG, targetImageUrl) - - runSuspendCatching { - Log.d(DEBUG_TAG, "Start download image $index") - val success: Boolean = spiderDen.makeHttpCallAndSaveImage( - index, - targetImageUrl, - referer - ) { contentLength: Long, receivedSize: Long, bytesRead: Int -> - queen.mPagePercentMap[index] = receivedSize.toFloat() / contentLength - queen.notifyPageDownload(index, contentLength, receivedSize, bytesRead) - } - - if (!success) { - Log.e(DEBUG_TAG, "Can't download all of image data") - error = "Incomplete" - forceHtml = true - return@repeat - } - - if (spiderDen.checkPlainText(index)) { - error = "" - forceHtml = true - return@repeat - } - - Log.d(DEBUG_TAG, "Download image succeed $index") - - queen.updatePageState(index, STATE_FINISHED) - delay(mDownloadDelay.toLong()) - return - }.onFailure { - it.printStackTrace() - error = NETWORK_ERROR - } - Log.d(DEBUG_TAG, "End download image $index") - } - spiderDen.remove(index) - queen.updatePageState(index, STATE_FAILED, error) - } - - companion object { - private val URL_509_SUFFIX_ARRAY = arrayOf( - "/509.gif", - "/509s.gif" - ) - - private const val DEBUG_TAG = "SpiderQueenWorker" - } - - private val decoder = Decoder() - - inner class Decoder { - private val mSemaphore = Semaphore(4) - private val mDecodeJobMap = hashMapOf() - - fun cancel(index: Int) { - synchronized(mDecodeJobMap) { - mDecodeJobMap.remove(index)?.cancel() - } - } - - fun launch(index: Int) { - synchronized(mDecodeJobMap) { - val currentJob = mDecodeJobMap[index] - if (currentJob?.isActive != true) { - mDecodeJobMap[index] = launch { - doInJob(index) - } - } - } - } - - private suspend fun doInJob(index: Int) { - mFetcherJobMap[index]?.takeIf { it.isActive }?.join() - val src = spiderDen.getImageSource(index) ?: return - val image = mSemaphore.withPermit { decode(src) } - runCatching { - currentCoroutineContext().ensureActive() - }.onFailure { - image?.recycle() - throw it - } - if (image == null) { - queen.notifyGetImageFailure(index, DECODE_ERROR) - } else { - queen.notifyGetImageSuccess(index, image) - } - } - } -}