|
|
|
@ -9,8 +9,10 @@ import java.io.File
|
|
|
|
import kotlinx.coroutines.*
|
|
|
|
import kotlinx.coroutines.*
|
|
|
|
import kotlinx.coroutines.flow.MutableStateFlow
|
|
|
|
import kotlinx.coroutines.flow.MutableStateFlow
|
|
|
|
import kotlinx.coroutines.sync.Semaphore
|
|
|
|
import kotlinx.coroutines.sync.Semaphore
|
|
|
|
|
|
|
|
import kotlinx.coroutines.sync.withPermit
|
|
|
|
import okhttp3.OkHttpClient
|
|
|
|
import okhttp3.OkHttpClient
|
|
|
|
import okhttp3.Request
|
|
|
|
import okhttp3.Request
|
|
|
|
|
|
|
|
import okhttp3.internal.closeQuietly
|
|
|
|
import okio.IOException
|
|
|
|
import okio.IOException
|
|
|
|
import org.koitharu.kotatsu.R
|
|
|
|
import org.koitharu.kotatsu.R
|
|
|
|
import org.koitharu.kotatsu.core.network.CommonHeaders
|
|
|
|
import org.koitharu.kotatsu.core.network.CommonHeaders
|
|
|
|
@ -59,102 +61,112 @@ class DownloadManager(
|
|
|
|
DownloadState.Queued(startId = startId, manga = manga, cover = null),
|
|
|
|
DownloadState.Queued(startId = startId, manga = manga, cover = null),
|
|
|
|
)
|
|
|
|
)
|
|
|
|
val pausingHandle = PausingHandle()
|
|
|
|
val pausingHandle = PausingHandle()
|
|
|
|
val job = downloadMangaImpl(manga, chaptersIds?.takeUnless { it.isEmpty() }, stateFlow, pausingHandle, startId)
|
|
|
|
val job = coroutineScope.launch(Dispatchers.Default + errorStateHandler(stateFlow)) {
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
|
|
downloadMangaImpl(manga, chaptersIds?.takeUnless { it.isEmpty() }, stateFlow, pausingHandle, startId)
|
|
|
|
|
|
|
|
} catch (e: CancellationException) { // handle cancellation if not handled already
|
|
|
|
|
|
|
|
val state = stateFlow.value
|
|
|
|
|
|
|
|
if (state !is DownloadState.Cancelled) {
|
|
|
|
|
|
|
|
stateFlow.value = DownloadState.Cancelled(startId, state.manga, state.cover)
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
throw e
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
return PausingProgressJob(job, stateFlow, pausingHandle)
|
|
|
|
return PausingProgressJob(job, stateFlow, pausingHandle)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private fun downloadMangaImpl(
|
|
|
|
private suspend fun downloadMangaImpl(
|
|
|
|
manga: Manga,
|
|
|
|
manga: Manga,
|
|
|
|
chaptersIds: LongArray?,
|
|
|
|
chaptersIds: LongArray?,
|
|
|
|
outState: MutableStateFlow<DownloadState>,
|
|
|
|
outState: MutableStateFlow<DownloadState>,
|
|
|
|
pausingHandle: PausingHandle,
|
|
|
|
pausingHandle: PausingHandle,
|
|
|
|
startId: Int,
|
|
|
|
startId: Int,
|
|
|
|
): Job = coroutineScope.launch(Dispatchers.Default + errorStateHandler(outState)) {
|
|
|
|
) {
|
|
|
|
@Suppress("NAME_SHADOWING")
|
|
|
|
@Suppress("NAME_SHADOWING")
|
|
|
|
var manga = manga
|
|
|
|
var manga = manga
|
|
|
|
val chaptersIdsSet = chaptersIds?.toMutableSet()
|
|
|
|
val chaptersIdsSet = chaptersIds?.toMutableSet()
|
|
|
|
val cover = loadCover(manga)
|
|
|
|
val cover = loadCover(manga)
|
|
|
|
outState.value = DownloadState.Queued(startId, manga, cover)
|
|
|
|
outState.value = DownloadState.Queued(startId, manga, cover)
|
|
|
|
localMangaRepository.lockManga(manga.id)
|
|
|
|
withMangaLock(manga) {
|
|
|
|
semaphore.acquire()
|
|
|
|
semaphore.withPermit {
|
|
|
|
coroutineContext[WakeLockNode]?.acquire()
|
|
|
|
outState.value = DownloadState.Preparing(startId, manga, null)
|
|
|
|
outState.value = DownloadState.Preparing(startId, manga, null)
|
|
|
|
val destination = localMangaRepository.getOutputDir()
|
|
|
|
val destination = localMangaRepository.getOutputDir()
|
|
|
|
checkNotNull(destination) { context.getString(R.string.cannot_find_available_storage) }
|
|
|
|
checkNotNull(destination) { context.getString(R.string.cannot_find_available_storage) }
|
|
|
|
val tempFileName = "${manga.id}_$startId.tmp"
|
|
|
|
val tempFileName = "${manga.id}_$startId.tmp"
|
|
|
|
var output: CbzMangaOutput? = null
|
|
|
|
var output: CbzMangaOutput? = null
|
|
|
|
try {
|
|
|
|
try {
|
|
|
|
if (manga.source == MangaSource.LOCAL) {
|
|
|
|
if (manga.source == MangaSource.LOCAL) {
|
|
|
|
manga = localMangaRepository.getRemoteManga(manga)
|
|
|
|
manga = localMangaRepository.getRemoteManga(manga) ?: error("Cannot obtain remote manga instance")
|
|
|
|
?: error("Cannot obtain remote manga instance")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
val repo = MangaRepository(manga.source)
|
|
|
|
val repo = MangaRepository(manga.source)
|
|
|
|
outState.value = DownloadState.Preparing(startId, manga, cover)
|
|
|
|
outState.value = DownloadState.Preparing(startId, manga, cover)
|
|
|
|
val data = if (manga.chapters.isNullOrEmpty()) repo.getDetails(manga) else manga
|
|
|
|
val data = if (manga.chapters.isNullOrEmpty()) repo.getDetails(manga) else manga
|
|
|
|
output = CbzMangaOutput.get(destination, data)
|
|
|
|
output = CbzMangaOutput.get(destination, data)
|
|
|
|
val coverUrl = data.largeCoverUrl ?: data.coverUrl
|
|
|
|
val coverUrl = data.largeCoverUrl ?: data.coverUrl
|
|
|
|
downloadFile(coverUrl, data.publicUrl, destination, tempFileName).let { file ->
|
|
|
|
downloadFile(coverUrl, data.publicUrl, destination, tempFileName).let { file ->
|
|
|
|
output.addCover(file, MimeTypeMap.getFileExtensionFromUrl(coverUrl))
|
|
|
|
output.addCover(file, MimeTypeMap.getFileExtensionFromUrl(coverUrl))
|
|
|
|
}
|
|
|
|
|
|
|
|
val chapters = checkNotNull(
|
|
|
|
|
|
|
|
if (chaptersIdsSet == null) {
|
|
|
|
|
|
|
|
data.chapters
|
|
|
|
|
|
|
|
} else {
|
|
|
|
|
|
|
|
data.chapters?.filter { x -> chaptersIdsSet.remove(x.id) }
|
|
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
) { "Chapters list must not be null" }
|
|
|
|
|
|
|
|
check(chapters.isNotEmpty()) { "Chapters list must not be empty" }
|
|
|
|
|
|
|
|
check(chaptersIdsSet.isNullOrEmpty()) {
|
|
|
|
|
|
|
|
"${chaptersIdsSet?.size} of ${chaptersIds?.size} requested chapters not found in manga"
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
for ((chapterIndex, chapter) in chapters.withIndex()) {
|
|
|
|
|
|
|
|
val pages = runFailsafe(outState, pausingHandle) {
|
|
|
|
|
|
|
|
repo.getPages(chapter)
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
for ((pageIndex, page) in pages.withIndex()) {
|
|
|
|
|
|
|
|
runFailsafe(outState, pausingHandle) {
|
|
|
|
|
|
|
|
val url = repo.getPageUrl(page)
|
|
|
|
|
|
|
|
val file = cache[url] ?: downloadFile(url, page.referer, destination, tempFileName)
|
|
|
|
|
|
|
|
output.addPage(
|
|
|
|
|
|
|
|
chapter = chapter,
|
|
|
|
|
|
|
|
file = file,
|
|
|
|
|
|
|
|
pageNumber = pageIndex,
|
|
|
|
|
|
|
|
ext = MimeTypeMap.getFileExtensionFromUrl(url),
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
outState.value = DownloadState.Progress(
|
|
|
|
val chapters = checkNotNull(
|
|
|
|
startId = startId,
|
|
|
|
if (chaptersIdsSet == null) {
|
|
|
|
manga = data,
|
|
|
|
data.chapters
|
|
|
|
cover = cover,
|
|
|
|
} else {
|
|
|
|
totalChapters = chapters.size,
|
|
|
|
data.chapters?.filter { x -> chaptersIdsSet.remove(x.id) }
|
|
|
|
currentChapter = chapterIndex,
|
|
|
|
},
|
|
|
|
totalPages = pages.size,
|
|
|
|
) { "Chapters list must not be null" }
|
|
|
|
currentPage = pageIndex,
|
|
|
|
check(chapters.isNotEmpty()) { "Chapters list must not be empty" }
|
|
|
|
)
|
|
|
|
check(chaptersIdsSet.isNullOrEmpty()) {
|
|
|
|
|
|
|
|
"${chaptersIdsSet?.size} of ${chaptersIds?.size} requested chapters not found in manga"
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
for ((chapterIndex, chapter) in chapters.withIndex()) {
|
|
|
|
|
|
|
|
val pages = runFailsafe(outState, pausingHandle) {
|
|
|
|
|
|
|
|
repo.getPages(chapter)
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
for ((pageIndex, page) in pages.withIndex()) {
|
|
|
|
|
|
|
|
runFailsafe(outState, pausingHandle) {
|
|
|
|
|
|
|
|
val url = repo.getPageUrl(page)
|
|
|
|
|
|
|
|
val file = cache[url] ?: downloadFile(url, page.referer, destination, tempFileName)
|
|
|
|
|
|
|
|
output.addPage(
|
|
|
|
|
|
|
|
chapter = chapter,
|
|
|
|
|
|
|
|
file = file,
|
|
|
|
|
|
|
|
pageNumber = pageIndex,
|
|
|
|
|
|
|
|
ext = MimeTypeMap.getFileExtensionFromUrl(url),
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
outState.value = DownloadState.Progress(
|
|
|
|
|
|
|
|
startId = startId,
|
|
|
|
|
|
|
|
manga = data,
|
|
|
|
|
|
|
|
cover = cover,
|
|
|
|
|
|
|
|
totalChapters = chapters.size,
|
|
|
|
|
|
|
|
currentChapter = chapterIndex,
|
|
|
|
|
|
|
|
totalPages = pages.size,
|
|
|
|
|
|
|
|
currentPage = pageIndex,
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
if (settings.isDownloadsSlowdownEnabled) {
|
|
|
|
if (settings.isDownloadsSlowdownEnabled) {
|
|
|
|
delay(SLOWDOWN_DELAY)
|
|
|
|
delay(SLOWDOWN_DELAY)
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
outState.value = DownloadState.PostProcessing(startId, data, cover)
|
|
|
|
|
|
|
|
output.mergeWithExisting()
|
|
|
|
|
|
|
|
output.finalize()
|
|
|
|
|
|
|
|
val localManga = localMangaRepository.getFromFile(output.file)
|
|
|
|
|
|
|
|
outState.value = DownloadState.Done(startId, data, cover, localManga)
|
|
|
|
|
|
|
|
} catch (e: CancellationException) {
|
|
|
|
|
|
|
|
outState.value = DownloadState.Cancelled(startId, manga, cover)
|
|
|
|
|
|
|
|
throw e
|
|
|
|
|
|
|
|
} catch (e: Throwable) {
|
|
|
|
|
|
|
|
e.printStackTraceDebug()
|
|
|
|
|
|
|
|
outState.value = DownloadState.Error(startId, manga, cover, e, false)
|
|
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
|
|
withContext(NonCancellable) {
|
|
|
|
|
|
|
|
output?.closeQuietly()
|
|
|
|
|
|
|
|
output?.cleanup()
|
|
|
|
|
|
|
|
File(destination, tempFileName).deleteAwait()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
outState.value = DownloadState.PostProcessing(startId, data, cover)
|
|
|
|
|
|
|
|
output.mergeWithExisting()
|
|
|
|
|
|
|
|
output.finalize()
|
|
|
|
|
|
|
|
val localManga = localMangaRepository.getFromFile(output.file)
|
|
|
|
|
|
|
|
outState.value = DownloadState.Done(startId, data, cover, localManga)
|
|
|
|
|
|
|
|
} catch (e: CancellationException) {
|
|
|
|
|
|
|
|
outState.value = DownloadState.Cancelled(startId, manga, cover)
|
|
|
|
|
|
|
|
throw e
|
|
|
|
|
|
|
|
} catch (e: Throwable) {
|
|
|
|
|
|
|
|
e.printStackTraceDebug()
|
|
|
|
|
|
|
|
outState.value = DownloadState.Error(startId, manga, cover, e, false)
|
|
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
|
|
withContext(NonCancellable) {
|
|
|
|
|
|
|
|
output?.cleanup()
|
|
|
|
|
|
|
|
File(destination, tempFileName).deleteAwait()
|
|
|
|
|
|
|
|
coroutineContext[WakeLockNode]?.release()
|
|
|
|
|
|
|
|
semaphore.release()
|
|
|
|
|
|
|
|
localMangaRepository.unlockManga(manga.id)
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@ -203,6 +215,7 @@ class DownloadManager(
|
|
|
|
|
|
|
|
|
|
|
|
private fun errorStateHandler(outState: MutableStateFlow<DownloadState>) =
|
|
|
|
private fun errorStateHandler(outState: MutableStateFlow<DownloadState>) =
|
|
|
|
CoroutineExceptionHandler { _, throwable ->
|
|
|
|
CoroutineExceptionHandler { _, throwable ->
|
|
|
|
|
|
|
|
throwable.printStackTraceDebug()
|
|
|
|
val prevValue = outState.value
|
|
|
|
val prevValue = outState.value
|
|
|
|
outState.value = DownloadState.Error(
|
|
|
|
outState.value = DownloadState.Error(
|
|
|
|
startId = prevValue.startId,
|
|
|
|
startId = prevValue.startId,
|
|
|
|
@ -224,6 +237,13 @@ class DownloadManager(
|
|
|
|
).drawable
|
|
|
|
).drawable
|
|
|
|
}.getOrNull()
|
|
|
|
}.getOrNull()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private suspend inline fun <T> withMangaLock(manga: Manga, block: () -> T) = try {
|
|
|
|
|
|
|
|
localMangaRepository.lockManga(manga.id)
|
|
|
|
|
|
|
|
block()
|
|
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
|
|
localMangaRepository.unlockManga(manga.id)
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
class Factory(
|
|
|
|
class Factory(
|
|
|
|
private val context: Context,
|
|
|
|
private val context: Context,
|
|
|
|
private val imageLoader: ImageLoader,
|
|
|
|
private val imageLoader: ImageLoader,
|
|
|
|
|