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:
parent
4462e75ccd
commit
94a90c6cec
@ -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,20 +315,25 @@ class Downloader(
|
|||||||
Observable.just(download.pages!!)
|
Observable.just(download.pages!!)
|
||||||
}
|
}
|
||||||
|
|
||||||
pageListObservable.doOnNext { _ ->
|
pageListObservable
|
||||||
// Delete all temporary (unfinished) files
|
.doOnNext { _ ->
|
||||||
tmpDir.listFiles()?.filter { it.name!!.endsWith(".tmp") }?.forEach { it.delete() }
|
// Delete all temporary (unfinished) files
|
||||||
|
tmpDir.listFiles()
|
||||||
|
?.filter { it.name!!.endsWith(".tmp") }
|
||||||
|
?.forEach { it.delete() }
|
||||||
|
|
||||||
download.downloadedImages = 0
|
download.downloadedImages = 0
|
||||||
download.status = Download.DOWNLOADING
|
download.status = Download.DOWNLOADING
|
||||||
}
|
}
|
||||||
// Get all the URLs to the source images, fetch pages if necessary
|
// Get all the URLs to the source images, fetch pages if necessary
|
||||||
.flatMap { download.source.fetchAllImageUrlsFromPageList(it) }
|
.flatMap { download.source.fetchAllImageUrlsFromPageList(it) }
|
||||||
// Start downloading images, consider we can have downloaded images already
|
// Start downloading images, consider we can have downloaded images already
|
||||||
// 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,19 +437,20 @@ 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)
|
||||||
val file = tmpDir.createFile("$filename.tmp")
|
.map { response ->
|
||||||
try {
|
val file = tmpDir.createFile("$filename.tmp")
|
||||||
response.body!!.source().saveTo(file.openOutputStream())
|
try {
|
||||||
val extension = getImageExtension(response, file)
|
response.body!!.source().saveTo(file.openOutputStream())
|
||||||
file.renameTo("$filename.$extension")
|
val extension = getImageExtension(response, file)
|
||||||
} catch (e: Exception) {
|
file.renameTo("$filename.$extension")
|
||||||
response.close()
|
} catch (e: Exception) {
|
||||||
file.delete()
|
response.close()
|
||||||
throw e
|
file.delete()
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
file
|
||||||
}
|
}
|
||||||
file
|
|
||||||
}
|
|
||||||
// Retry 3 times, waiting 2, 4 and 8 seconds between attempts.
|
// Retry 3 times, waiting 2, 4 and 8 seconds between attempts.
|
||||||
.retryWhen(RetryWithDelay(3, { (2 shl it - 1) * 1000 }, Schedulers.trampoline()))
|
.retryWhen(RetryWithDelay(3, { (2 shl it - 1) * 1000 }, Schedulers.trampoline()))
|
||||||
}
|
}
|
||||||
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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.
|
||||||
*/
|
*/
|
||||||
|
@ -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)
|
||||||
|
|
||||||
|
@ -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>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user