From d61ba80bf6be095673c81fc29f18dfd1a4da67dc Mon Sep 17 00:00:00 2001 From: Koitharu Date: Sun, 17 Apr 2022 09:47:21 +0300 Subject: [PATCH] Add additional checks to download task #50 --- .../org/koitharu/kotatsu/core/model/Manga.kt | 22 ---- .../core/model/parcelable/ParcelableManga.kt | 9 +- .../kotatsu/details/ui/DetailsActivity.kt | 2 +- .../download/domain/DownloadManager.kt | 102 ++++++++++-------- .../download/ui/service/DownloadService.kt | 12 +-- .../select/FavouriteCategoriesDialog.kt | 3 +- .../kotatsu/reader/ui/ReaderActivity.kt | 6 +- .../kotatsu/utils/ext/CollectionExt.kt | 2 - 8 files changed, 73 insertions(+), 85 deletions(-) diff --git a/app/src/main/java/org/koitharu/kotatsu/core/model/Manga.kt b/app/src/main/java/org/koitharu/kotatsu/core/model/Manga.kt index 3079b6c76..ed5594c42 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/model/Manga.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/model/Manga.kt @@ -3,26 +3,4 @@ package org.koitharu.kotatsu.core.model import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.util.mapToSet -fun Manga.withoutChapters() = if (chapters.isNullOrEmpty()) { - this -} else { - Manga( - id = id, - title = title, - altTitle = altTitle, - url = url, - publicUrl = publicUrl, - rating = rating, - isNsfw = isNsfw, - coverUrl = coverUrl, - tags = tags, - state = state, - author = author, - largeCoverUrl = largeCoverUrl, - description = description, - chapters = null, - source = source, - ) -} - fun Collection.ids() = mapToSet { it.id } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/core/model/parcelable/ParcelableManga.kt b/app/src/main/java/org/koitharu/kotatsu/core/model/parcelable/ParcelableManga.kt index fd9b6cfa1..b302ce634 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/model/parcelable/ParcelableManga.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/model/parcelable/ParcelableManga.kt @@ -10,13 +10,18 @@ private const val MAX_SAFE_CHAPTERS_COUNT = 40 // this is 100% safe class ParcelableManga( val manga: Manga, + private val withChapters: Boolean, ) : Parcelable { - constructor(parcel: Parcel) : this(parcel.readManga()) + constructor(parcel: Parcel) : this(parcel.readManga(), true) override fun writeToParcel(parcel: Parcel, flags: Int) { val chapters = manga.chapters - if (chapters == null || chapters.size <= MAX_SAFE_CHAPTERS_COUNT) { + if (!withChapters || chapters == null) { + manga.writeToParcel(parcel, flags, withChapters = false) + return + } + if (chapters.size <= MAX_SAFE_CHAPTERS_COUNT) { // fast path manga.writeToParcel(parcel, flags, withChapters = true) return diff --git a/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsActivity.kt b/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsActivity.kt index ef427f091..3ef3d8517 100644 --- a/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsActivity.kt +++ b/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsActivity.kt @@ -332,7 +332,7 @@ class DetailsActivity : BaseActivity(), TabLayoutMediato fun newIntent(context: Context, manga: Manga): Intent { return Intent(context, DetailsActivity::class.java) - .putExtra(MangaIntent.KEY_MANGA, ParcelableManga(manga)) + .putExtra(MangaIntent.KEY_MANGA, ParcelableManga(manga, withChapters = true)) } fun newIntent(context: Context, mangaId: Long): Intent { diff --git a/app/src/main/java/org/koitharu/kotatsu/download/domain/DownloadManager.kt b/app/src/main/java/org/koitharu/kotatsu/download/domain/DownloadManager.kt index 0b3c67ccb..3373273ea 100644 --- a/app/src/main/java/org/koitharu/kotatsu/download/domain/DownloadManager.kt +++ b/app/src/main/java/org/koitharu/kotatsu/download/domain/DownloadManager.kt @@ -7,6 +7,7 @@ import android.webkit.MimeTypeMap import coil.ImageLoader import coil.request.ImageRequest import coil.size.Scale +import java.io.File import kotlinx.coroutines.* import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.sync.Semaphore @@ -21,12 +22,12 @@ import org.koitharu.kotatsu.local.data.MangaZip import org.koitharu.kotatsu.local.data.PagesCache import org.koitharu.kotatsu.local.domain.LocalMangaRepository import org.koitharu.kotatsu.parsers.model.Manga +import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.parsers.util.await import org.koitharu.kotatsu.utils.ext.deleteAwait import org.koitharu.kotatsu.utils.ext.referer import org.koitharu.kotatsu.utils.ext.waitForNetwork import org.koitharu.kotatsu.utils.progress.ProgressJob -import java.io.File private const val MAX_DOWNLOAD_ATTEMPTS = 3 private const val MAX_PARALLEL_DOWNLOADS = 2 @@ -55,22 +56,24 @@ class DownloadManager( fun downloadManga( manga: Manga, - chaptersIds: Set?, + chaptersIds: LongArray?, startId: Int, ): ProgressJob { val stateFlow = MutableStateFlow( DownloadState.Queued(startId = startId, manga = manga, cover = null) ) - val job = downloadMangaImpl(manga, chaptersIds, stateFlow, startId) + val job = downloadMangaImpl(manga, chaptersIds?.takeUnless { it.isEmpty() }, stateFlow, startId) return ProgressJob(job, stateFlow) } private fun downloadMangaImpl( manga: Manga, - chaptersIds: Set?, + chaptersIds: LongArray?, outState: MutableStateFlow, startId: Int, ): Job = coroutineScope.launch(Dispatchers.Default + errorStateHandler(outState)) { + @Suppress("NAME_SHADOWING") var manga = manga + val chaptersIdsSet = chaptersIds?.toMutableSet() semaphore.acquire() coroutineContext[WakeLockNode]?.acquire() outState.value = DownloadState.Preparing(startId, manga, null) @@ -79,6 +82,9 @@ class DownloadManager( checkNotNull(destination) { context.getString(R.string.cannot_find_available_storage) } var output: MangaZip? = null try { + if (manga.source == MangaSource.LOCAL) { + manga = localMangaRepository.getRemoteManga(manga) ?: error("Cannot obtain remote manga instance") + } val repo = MangaRepository(manga.source) cover = runCatching { imageLoader.execute( @@ -91,48 +97,51 @@ class DownloadManager( ).drawable }.getOrNull() outState.value = DownloadState.Preparing(startId, manga, cover) - val data = if (manga.chapters == null) repo.getDetails(manga) else manga + val data = if (manga.chapters.isNullOrEmpty()) repo.getDetails(manga) else manga output = MangaZip.findInDir(destination, data) output.prepare(data) val coverUrl = data.largeCoverUrl ?: data.coverUrl downloadFile(coverUrl, data.publicUrl, destination).let { file -> output.addCover(file, MimeTypeMap.getFileExtensionFromUrl(coverUrl)) } - val chapters = if (chaptersIds == null) { - data.chapters.orEmpty() - } else { - data.chapters.orEmpty().filter { x -> x.id in chaptersIds } + 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()) { - if (chaptersIds == null || chapter.id in chaptersIds) { - val pages = repo.getPages(chapter) - for ((pageIndex, page) in pages.withIndex()) { - failsafe@ do { - try { - val url = repo.getPageUrl(page) - val file = - cache[url] ?: downloadFile(url, page.referer, destination) - output.addPage( - chapter, - file, - pageIndex, - MimeTypeMap.getFileExtensionFromUrl(url) - ) - } catch (e: IOException) { - outState.value = DownloadState.WaitingForNetwork(startId, data, cover) - connectivityManager.waitForNetwork() - continue@failsafe - } - } while (false) + val pages = repo.getPages(chapter) + for ((pageIndex, page) in pages.withIndex()) { + failsafe@ do { + try { + val url = repo.getPageUrl(page) + val file = cache[url] ?: downloadFile(url, page.referer, destination) + output.addPage( + chapter = chapter, + file = file, + pageNumber = pageIndex, + ext = MimeTypeMap.getFileExtensionFromUrl(url), + ) + } catch (e: IOException) { + outState.value = DownloadState.WaitingForNetwork(startId, data, cover) + connectivityManager.waitForNetwork() + continue@failsafe + } + } while (false) - outState.value = DownloadState.Progress( - startId, data, cover, - totalChapters = chapters.size, - currentChapter = chapterIndex, - totalPages = pages.size, - currentPage = pageIndex, - ) - } + outState.value = DownloadState.Progress( + startId, data, cover, + totalChapters = chapters.size, + currentChapter = chapterIndex, + totalPages = pages.size, + currentPage = pageIndex, + ) } } outState.value = DownloadState.PostProcessing(startId, data, cover) @@ -189,13 +198,14 @@ class DownloadManager( } } - private fun errorStateHandler(outState: MutableStateFlow) = CoroutineExceptionHandler { _, throwable -> - val prevValue = outState.value - outState.value = DownloadState.Error( - startId = prevValue.startId, - manga = prevValue.manga, - cover = prevValue.cover, - error = throwable, - ) - } + private fun errorStateHandler(outState: MutableStateFlow) = + CoroutineExceptionHandler { _, throwable -> + val prevValue = outState.value + outState.value = DownloadState.Error( + startId = prevValue.startId, + manga = prevValue.manga, + cover = prevValue.cover, + error = throwable, + ) + } } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/download/ui/service/DownloadService.kt b/app/src/main/java/org/koitharu/kotatsu/download/ui/service/DownloadService.kt index a005346f4..41bb3e273 100644 --- a/app/src/main/java/org/koitharu/kotatsu/download/ui/service/DownloadService.kt +++ b/app/src/main/java/org/koitharu/kotatsu/download/ui/service/DownloadService.kt @@ -24,7 +24,6 @@ import org.koitharu.kotatsu.R import org.koitharu.kotatsu.base.ui.BaseService import org.koitharu.kotatsu.base.ui.dialog.CheckBoxAlertDialog import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga -import org.koitharu.kotatsu.core.model.withoutChapters import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.download.domain.DownloadManager import org.koitharu.kotatsu.download.domain.DownloadState @@ -32,7 +31,6 @@ import org.koitharu.kotatsu.download.domain.WakeLockNode import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.utils.ext.connectivityManager import org.koitharu.kotatsu.utils.ext.throttle -import org.koitharu.kotatsu.utils.ext.toArraySet import org.koitharu.kotatsu.utils.progress.ProgressJob import java.util.concurrent.TimeUnit @@ -66,7 +64,7 @@ class DownloadService : BaseService() { override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { super.onStartCommand(intent, flags, startId) val manga = intent?.getParcelableExtra(EXTRA_MANGA)?.manga - val chapters = intent?.getLongArrayExtra(EXTRA_CHAPTERS_IDS)?.toArraySet() + val chapters = intent?.getLongArrayExtra(EXTRA_CHAPTERS_IDS) return if (manga != null) { jobs[startId] = downloadManga(startId, manga, chapters) jobCount.value = jobs.size @@ -96,7 +94,7 @@ class DownloadService : BaseService() { private fun downloadManga( startId: Int, manga: Manga, - chaptersIds: Set?, + chaptersIds: LongArray?, ): ProgressJob { val job = downloadManager.downloadManga(manga, chaptersIds, startId) listenJob(job) @@ -118,7 +116,7 @@ class DownloadService : BaseService() { (job.progressValue as? DownloadState.Done)?.let { sendBroadcast( Intent(ACTION_DOWNLOAD_COMPLETE) - .putExtra(EXTRA_MANGA, ParcelableManga(it.localManga.withoutChapters())) + .putExtra(EXTRA_MANGA, ParcelableManga(it.localManga, withChapters = false)) ) } notificationSwitcher.detach( @@ -178,7 +176,7 @@ class DownloadService : BaseService() { } confirmDataTransfer(context) { val intent = Intent(context, DownloadService::class.java) - intent.putExtra(EXTRA_MANGA, ParcelableManga(manga)) + intent.putExtra(EXTRA_MANGA, ParcelableManga(manga, withChapters = false)) if (chaptersIds != null) { intent.putExtra(EXTRA_CHAPTERS_IDS, chaptersIds.toLongArray()) } @@ -194,7 +192,7 @@ class DownloadService : BaseService() { confirmDataTransfer(context) { for (item in manga) { val intent = Intent(context, DownloadService::class.java) - intent.putExtra(EXTRA_MANGA, ParcelableManga(item)) + intent.putExtra(EXTRA_MANGA, ParcelableManga(item, withChapters = false)) ContextCompat.startForegroundService(context, intent) } } diff --git a/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/select/FavouriteCategoriesDialog.kt b/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/select/FavouriteCategoriesDialog.kt index 184c6cfe6..43eec185e 100644 --- a/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/select/FavouriteCategoriesDialog.kt +++ b/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/select/FavouriteCategoriesDialog.kt @@ -15,7 +15,6 @@ import org.koitharu.kotatsu.base.ui.BaseBottomSheet import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener import org.koitharu.kotatsu.core.model.FavouriteCategory import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga -import org.koitharu.kotatsu.core.model.withoutChapters import org.koitharu.kotatsu.databinding.DialogFavoriteCategoriesBinding import org.koitharu.kotatsu.favourites.ui.categories.CategoriesEditDelegate import org.koitharu.kotatsu.favourites.ui.categories.select.adapter.MangaCategoriesAdapter @@ -99,7 +98,7 @@ class FavouriteCategoriesDialog : fun show(fm: FragmentManager, manga: Collection) = FavouriteCategoriesDialog().withArgs(1) { putParcelableArrayList( KEY_MANGA_LIST, - manga.mapTo(ArrayList(manga.size)) { ParcelableManga(it.withoutChapters()) } + manga.mapTo(ArrayList(manga.size)) { ParcelableManga(it, withChapters = false) } ) }.show(fm, TAG) } diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderActivity.kt b/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderActivity.kt index 6a363cf4b..914b96ae3 100644 --- a/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderActivity.kt +++ b/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderActivity.kt @@ -410,18 +410,18 @@ class ReaderActivity : fun newIntent(context: Context, manga: Manga): Intent { return Intent(context, ReaderActivity::class.java) - .putExtra(MangaIntent.KEY_MANGA, ParcelableManga(manga)) + .putExtra(MangaIntent.KEY_MANGA, ParcelableManga(manga, withChapters = true)) } fun newIntent(context: Context, manga: Manga, branch: String?): Intent { return Intent(context, ReaderActivity::class.java) - .putExtra(MangaIntent.KEY_MANGA, ParcelableManga(manga)) + .putExtra(MangaIntent.KEY_MANGA, ParcelableManga(manga, withChapters = true)) .putExtra(EXTRA_BRANCH, branch) } fun newIntent(context: Context, manga: Manga, state: ReaderState?): Intent { return Intent(context, ReaderActivity::class.java) - .putExtra(MangaIntent.KEY_MANGA, ParcelableManga(manga)) + .putExtra(MangaIntent.KEY_MANGA, ParcelableManga(manga, withChapters = true)) .putExtra(EXTRA_STATE, state) } diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/ext/CollectionExt.kt b/app/src/main/java/org/koitharu/kotatsu/utils/ext/CollectionExt.kt index 916d83491..04bc82e1d 100644 --- a/app/src/main/java/org/koitharu/kotatsu/utils/ext/CollectionExt.kt +++ b/app/src/main/java/org/koitharu/kotatsu/utils/ext/CollectionExt.kt @@ -3,8 +3,6 @@ package org.koitharu.kotatsu.utils.ext import androidx.collection.ArraySet import java.util.* -fun LongArray.toArraySet(): Set = createSet(size) { i -> this[i] } - fun > Array.names() = Array(size) { i -> this[i].name }