Minor updates to downloader from upstream

Such as available space check and using io thread instead of ui

Co-Authored-By: arkon <4098258+arkon@users.noreply.github.com>
This commit is contained in:
Jays2Kings 2021-04-19 00:14:24 -04:00
parent 4462e75ccd
commit 94a90c6cec
4 changed files with 86 additions and 48 deletions

View File

@ -5,6 +5,7 @@ import android.webkit.MimeTypeMap
import com.hippo.unifile.UniFile import com.hippo.unifile.UniFile
import com.jakewharton.rxrelay.BehaviorRelay import com.jakewharton.rxrelay.BehaviorRelay
import com.jakewharton.rxrelay.PublishRelay import com.jakewharton.rxrelay.PublishRelay
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.cache.ChapterCache import eu.kanade.tachiyomi.data.cache.ChapterCache
import eu.kanade.tachiyomi.data.database.models.Chapter import eu.kanade.tachiyomi.data.database.models.Chapter
import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.models.Manga
@ -20,8 +21,8 @@ import eu.kanade.tachiyomi.util.lang.plusAssign
import eu.kanade.tachiyomi.util.storage.DiskUtil import eu.kanade.tachiyomi.util.storage.DiskUtil
import eu.kanade.tachiyomi.util.storage.saveTo import eu.kanade.tachiyomi.util.storage.saveTo
import eu.kanade.tachiyomi.util.system.ImageUtil import eu.kanade.tachiyomi.util.system.ImageUtil
import eu.kanade.tachiyomi.util.system.launchIO
import eu.kanade.tachiyomi.util.system.launchNow import eu.kanade.tachiyomi.util.system.launchNow
import eu.kanade.tachiyomi.util.system.launchUI
import kotlinx.coroutines.async import kotlinx.coroutines.async
import okhttp3.Response import okhttp3.Response
import rx.Observable import rx.Observable
@ -29,6 +30,7 @@ import rx.android.schedulers.AndroidSchedulers
import rx.schedulers.Schedulers import rx.schedulers.Schedulers
import rx.subscriptions.CompositeSubscription import rx.subscriptions.CompositeSubscription
import timber.log.Timber import timber.log.Timber
import uy.kohesive.injekt.injectLazy
import java.io.File import java.io.File
/** /**
@ -52,6 +54,8 @@ class Downloader(
private val sourceManager: SourceManager private val sourceManager: SourceManager
) { ) {
private val chapterCache: ChapterCache by injectLazy()
/** /**
* Store for persisting downloads across restarts. * Store for persisting downloads across restarts.
*/ */
@ -119,7 +123,9 @@ class Downloader(
*/ */
fun stop(reason: String? = null) { fun stop(reason: String? = null) {
destroySubscriptions() destroySubscriptions()
queue.filter { it.status == Download.DOWNLOADING }.forEach { it.status = Download.ERROR } queue
.filter { it.status == Download.DOWNLOADING }
.forEach { it.status = Download.ERROR }
if (reason != null) { if (reason != null) {
notifier.onWarning(reason) notifier.onWarning(reason)
@ -142,7 +148,9 @@ class Downloader(
*/ */
fun pause() { fun pause() {
destroySubscriptions() destroySubscriptions()
queue.filter { it.status == Download.DOWNLOADING }.forEach { it.status = Download.QUEUE } queue
.filter { it.status == Download.DOWNLOADING }
.forEach { it.status = Download.QUEUE }
notifier.paused = true notifier.paused = true
} }
@ -161,7 +169,8 @@ class Downloader(
// Needed to update the chapter view // Needed to update the chapter view
if (isNotification) { if (isNotification) {
queue.filter { it.status == Download.QUEUE } queue
.filter { it.status == Download.QUEUE }
.forEach { it.status = Download.NOT_DOWNLOADED } .forEach { it.status = Download.NOT_DOWNLOADED }
} }
queue.clear() queue.clear()
@ -207,7 +216,7 @@ class Downloader(
}, },
5 5
) )
.onBackpressureBuffer() .onBackpressureLatest()
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.subscribe( .subscribe(
{ {
@ -239,8 +248,8 @@ class Downloader(
* @param chapters the list of chapters to download. * @param chapters the list of chapters to download.
* @param autoStart whether to start the downloader after enqueing the chapters. * @param autoStart whether to start the downloader after enqueing the chapters.
*/ */
fun queueChapters(manga: Manga, chapters: List<Chapter>, autoStart: Boolean) = launchUI { fun queueChapters(manga: Manga, chapters: List<Chapter>, autoStart: Boolean) = launchIO {
val source = sourceManager.get(manga.source) as? HttpSource ?: return@launchUI val source = sourceManager.get(manga.source) as? HttpSource ?: return@launchIO
val wasEmpty = queue.isEmpty() val wasEmpty = queue.isEmpty()
// Called in background thread, the operation can be slow with SAF. // Called in background thread, the operation can be slow with SAF.
val chaptersWithoutDir = async { val chaptersWithoutDir = async {
@ -281,15 +290,23 @@ class Downloader(
* @param download the chapter to be downloaded. * @param download the chapter to be downloaded.
*/ */
private fun downloadChapter(download: Download): Observable<Download> = Observable.defer { private fun downloadChapter(download: Download): Observable<Download> = Observable.defer {
val chapterDirname = provider.getChapterDirName(download.chapter)
val mangaDir = provider.getMangaDir(download.manga, download.source) val mangaDir = provider.getMangaDir(download.manga, download.source)
val availSpace = DiskUtil.getAvailableStorageSpace(mangaDir)
if (availSpace != -1L && availSpace < MIN_DISK_SPACE) {
download.status = Download.ERROR
notifier.onError(context.getString(R.string.couldnt_download_low_space), download.chapter.name)
return@defer Observable.just(download)
}
val chapterDirname = provider.getChapterDirName(download.chapter)
val tmpDir = mangaDir.createDirectory(chapterDirname + TMP_DIR_SUFFIX) val tmpDir = mangaDir.createDirectory(chapterDirname + TMP_DIR_SUFFIX)
val pageListObservable = if (download.pages == null) { val pageListObservable = if (download.pages == null) {
// Pull page list from network and add them to download object // Pull page list from network and add them to download object
download.source.fetchPageList(download.chapter).doOnNext { pages -> download.source.fetchPageList(download.chapter).doOnNext { pages ->
if (pages.isEmpty()) { if (pages.isEmpty()) {
throw Exception("Page list is empty") throw Exception(context.getString(R.string.no_pages_found))
} }
download.pages = pages download.pages = pages
} }
@ -298,9 +315,12 @@ class Downloader(
Observable.just(download.pages!!) Observable.just(download.pages!!)
} }
pageListObservable.doOnNext { _ -> pageListObservable
.doOnNext { _ ->
// Delete all temporary (unfinished) files // Delete all temporary (unfinished) files
tmpDir.listFiles()?.filter { it.name!!.endsWith(".tmp") }?.forEach { it.delete() } tmpDir.listFiles()
?.filter { it.name!!.endsWith(".tmp") }
?.forEach { it.delete() }
download.downloadedImages = 0 download.downloadedImages = 0
download.status = Download.DOWNLOADING download.status = Download.DOWNLOADING
@ -311,7 +331,9 @@ class Downloader(
// Concurrently do 5 pages at a time // Concurrently do 5 pages at a time
.flatMap({ page -> getOrDownloadImage(page, download, tmpDir) }, 5) .flatMap({ page -> getOrDownloadImage(page, download, tmpDir) }, 5)
// Do when page is downloaded. // Do when page is downloaded.
.doOnNext { notifier.onProgressChange(download) }.toList().map { _ -> download } .doOnNext { notifier.onProgressChange(download) }
.toList()
.map { download }
// Do after download completes // Do after download completes
.doOnNext { ensureSuccessfulDownload(download, mangaDir, tmpDir, chapterDirname) } .doOnNext { ensureSuccessfulDownload(download, mangaDir, tmpDir, chapterDirname) }
// If the page list threw, it will resume here // If the page list threw, it will resume here
@ -336,7 +358,9 @@ class Downloader(
tmpDir: UniFile tmpDir: UniFile
): Observable<Page> { ): Observable<Page> {
// If the image URL is empty, do nothing // If the image URL is empty, do nothing
if (page.imageUrl == null) return Observable.just(page) if (page.imageUrl == null) {
return Observable.just(page)
}
val filename = String.format("%03d", page.number) val filename = String.format("%03d", page.number)
val tmpFile = tmpDir.findFile("$filename.tmp") val tmpFile = tmpDir.findFile("$filename.tmp")
@ -346,16 +370,11 @@ class Downloader(
// Try to find the image file. // Try to find the image file.
val imageFile = tmpDir.listFiles()!!.find { it.name!!.startsWith("$filename.") } val imageFile = tmpDir.listFiles()!!.find { it.name!!.startsWith("$filename.") }
val cache = ChapterCache(context)
// If the image is already downloaded, do nothing. Otherwise download from network // If the image is already downloaded, do nothing. Otherwise download from network
val pageObservable = when { val pageObservable = when {
imageFile != null -> Observable.just(imageFile) imageFile != null -> Observable.just(imageFile)
cache.isImageInCache(page.imageUrl!!) -> moveFromCache( chapterCache.isImageInCache(page.imageUrl!!) -> moveImageFromCache(chapterCache.getImageFile(page.imageUrl!!), tmpDir, filename)
page,
cache.getImageFile(page.imageUrl!!),
tmpDir,
filename
)
else -> downloadImage(page, download.source, tmpDir, filename) else -> downloadImage(page, download.source, tmpDir, filename)
} }
@ -366,7 +385,8 @@ class Downloader(
page.progress = 100 page.progress = 100
download.downloadedImages++ download.downloadedImages++
page.status = Page.READY page.status = Page.READY
}.map { page } }
.map { page }
// Mark this page as error and allow to download the remaining // Mark this page as error and allow to download the remaining
.onErrorReturn { .onErrorReturn {
page.progress = 0 page.progress = 0
@ -376,30 +396,27 @@ class Downloader(
} }
/** /**
* Returns the observable which takes from the downloaded image from cache * Return the observable which copies the image from cache.
* *
* @param page the page to download. * @param cacheFile the file from cache.
* @param file the file from cache
* @param tmpDir the temporary directory of the download. * @param tmpDir the temporary directory of the download.
* @param filename the filename of the image. * @param filename the filename of the image.
*/ */
private fun moveFromCache( private fun moveImageFromCache(
page: Page, cacheFile: File,
file: File,
tmpDir: UniFile, tmpDir: UniFile,
filename: String filename: String
): Observable<UniFile> { ): Observable<UniFile> {
return Observable.just(file).map { return Observable.just(cacheFile).map {
val tmpFile = tmpDir.createFile("$filename.tmp") val tmpFile = tmpDir.createFile("$filename.tmp")
val inputStream = file.inputStream() cacheFile.inputStream().use { input ->
inputStream.use { input ->
tmpFile.openOutputStream().use { output -> tmpFile.openOutputStream().use { output ->
input.copyTo(output) input.copyTo(output)
} }
} }
val extension = ImageUtil.findImageType(file.inputStream()) ?: return@map tmpFile val extension = ImageUtil.findImageType(cacheFile.inputStream()) ?: return@map tmpFile
tmpFile.renameTo("$filename.${extension.extension}") tmpFile.renameTo("$filename.${extension.extension}")
file.delete() cacheFile.delete()
tmpFile tmpFile
} }
} }
@ -420,7 +437,8 @@ class Downloader(
): Observable<UniFile> { ): Observable<UniFile> {
page.status = Page.DOWNLOAD_IMAGE page.status = Page.DOWNLOAD_IMAGE
page.progress = 0 page.progress = 0
return source.fetchImage(page).map { response -> return source.fetchImage(page)
.map { response ->
val file = tmpDir.createFile("$filename.tmp") val file = tmpDir.createFile("$filename.tmp")
try { try {
response.body!!.source().saveTo(file.openOutputStream()) response.body!!.source().saveTo(file.openOutputStream())
@ -514,5 +532,8 @@ class Downloader(
companion object { companion object {
const val TMP_DIR_SUFFIX = "_tmp" const val TMP_DIR_SUFFIX = "_tmp"
// Arbitrary minimum required space to start a download: 50 MB
const val MIN_DISK_SPACE = 50 * 1024 * 1024
} }
} }

View File

@ -4,6 +4,7 @@ import android.content.Context
import android.content.Intent import android.content.Intent
import android.net.Uri import android.net.Uri
import android.os.Environment import android.os.Environment
import android.os.StatFs
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.os.EnvironmentCompat import androidx.core.os.EnvironmentCompat
import com.hippo.unifile.UniFile import com.hippo.unifile.UniFile
@ -28,6 +29,18 @@ object DiskUtil {
return size return size
} }
/**
* Gets the available space for the disk that a file path points to, in bytes.
*/
fun getAvailableStorageSpace(f: UniFile): Long {
return try {
val stat = StatFs(f.uri.path)
stat.availableBlocksLong * stat.blockSizeLong
} catch (_: Exception) {
-1L
}
}
/** /**
* Returns the root folders of all the available external storages. * Returns the root folders of all the available external storages.
*/ */

View File

@ -11,6 +11,9 @@ import kotlinx.coroutines.withContext
fun launchUI(block: suspend CoroutineScope.() -> Unit): Job = fun launchUI(block: suspend CoroutineScope.() -> Unit): Job =
GlobalScope.launch(Dispatchers.Main, CoroutineStart.DEFAULT, block) GlobalScope.launch(Dispatchers.Main, CoroutineStart.DEFAULT, block)
fun launchIO(block: suspend CoroutineScope.() -> Unit): Job =
GlobalScope.launch(Dispatchers.IO, CoroutineStart.DEFAULT, block)
fun launchNow(block: suspend CoroutineScope.() -> Unit): Job = fun launchNow(block: suspend CoroutineScope.() -> Unit): Job =
GlobalScope.launch(Dispatchers.Main, CoroutineStart.UNDISPATCHED, block) GlobalScope.launch(Dispatchers.Main, CoroutineStart.UNDISPATCHED, block)

View File

@ -760,6 +760,7 @@
<string name="manage_whats_downloading">Manage what\'s downloading</string> <string name="manage_whats_downloading">Manage what\'s downloading</string>
<string name="visit_recents_for_download_queue">Visit the recents tab to access the download <string name="visit_recents_for_download_queue">Visit the recents tab to access the download
queue. You can also double tap or press and hold for quicker access</string> queue. You can also double tap or press and hold for quicker access</string>
<string name="couldnt_download_low_space">Couldn\'t download chapters due to low disk space</string>
<!-- Download Notification --> <!-- Download Notification -->
<string name="could_not_download_unexpected_error">Could not download chapter due to unexpected error</string> <string name="could_not_download_unexpected_error">Could not download chapter due to unexpected error</string>