Remove simultaneous downloads

This commit is contained in:
inorichi 2018-02-18 17:34:22 +01:00
parent d75c6b0c36
commit 7b4ac7998a
7 changed files with 22 additions and 306 deletions

View file

@ -36,7 +36,6 @@ internal class DownloadNotifier(private val context: Context) {
* The size of queue on start download. * The size of queue on start download.
*/ */
var initialQueueSize = 0 var initialQueueSize = 0
get() = field
set(value) { set(value) {
if (value != 0){ if (value != 0){
isSingleChapter = (value == 1) isSingleChapter = (value == 1)
@ -44,11 +43,6 @@ internal class DownloadNotifier(private val context: Context) {
field = value field = value
} }
/**
* Simultaneous download setting > 1.
*/
var multipleDownloadThreads = false
/** /**
* Updated when error is thrown * Updated when error is thrown
*/ */
@ -91,36 +85,10 @@ internal class DownloadNotifier(private val context: Context) {
/** /**
* Called when download progress changes. * Called when download progress changes.
* Note: Only accepted when multi download active.
*
* @param queue the queue containing downloads.
*/
fun onProgressChange(queue: DownloadQueue) {
if (multipleDownloadThreads) {
doOnProgressChange(null, queue)
}
}
/**
* Called when download progress changes.
* Note: Only accepted when single download active.
* *
* @param download download object containing download information. * @param download download object containing download information.
* @param queue the queue containing downloads.
*/ */
fun onProgressChange(download: Download, queue: DownloadQueue) { fun onProgressChange(download: Download) {
if (!multipleDownloadThreads) {
doOnProgressChange(download, queue)
}
}
/**
* Show notification progress of chapter.
*
* @param download download object containing download information.
* @param queue the queue containing downloads.
*/
private fun doOnProgressChange(download: Download?, queue: DownloadQueue) {
// Create notification // Create notification
with(notification) { with(notification) {
// Check if first call. // Check if first call.
@ -133,28 +101,13 @@ internal class DownloadNotifier(private val context: Context) {
isDownloading = true isDownloading = true
} }
if (multipleDownloadThreads) { val title = download.manga.title.chop(15)
setContentTitle(context.getString(R.string.app_name)) val quotedTitle = Pattern.quote(title)
val chapter = download.chapter.name.replaceFirst("$quotedTitle[\\s]*[-]*[\\s]*".toRegex(RegexOption.IGNORE_CASE), "")
// Reset the queue size if the download progress is negative setContentTitle("$title - $chapter".chop(30))
if ((initialQueueSize - queue.size) < 0) setContentText(context.getString(R.string.chapter_downloading_progress)
initialQueueSize = queue.size .format(download.downloadedImages, download.pages!!.size))
setProgress(download.pages!!.size, download.downloadedImages, false)
setContentText(context.getString(R.string.chapter_downloading_progress)
.format(initialQueueSize - queue.size, initialQueueSize))
setProgress(initialQueueSize, initialQueueSize - queue.size, false)
} else {
download?.let {
val title = it.manga.title.chop(15)
val quotedTitle = Pattern.quote(title)
val chapter = download.chapter.name.replaceFirst("$quotedTitle[\\s]*[-]*[\\s]*".toRegex(RegexOption.IGNORE_CASE), "")
setContentTitle("$title - $chapter".chop(30))
setContentText(context.getString(R.string.chapter_downloading_progress)
.format(it.downloadedImages, it.pages!!.size))
setProgress(it.pages!!.size, it.downloadedImages, false)
}
}
} }
// Displays the progress bar on notification // Displays the progress bar on notification
notification.show() notification.show()

View file

@ -9,8 +9,6 @@ import eu.kanade.tachiyomi.data.database.models.Chapter
import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.download.model.Download import eu.kanade.tachiyomi.data.download.model.Download
import eu.kanade.tachiyomi.data.download.model.DownloadQueue import eu.kanade.tachiyomi.data.download.model.DownloadQueue
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.preference.getOrDefault
import eu.kanade.tachiyomi.source.SourceManager import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.online.HttpSource import eu.kanade.tachiyomi.source.online.HttpSource
@ -21,7 +19,6 @@ import okhttp3.Response
import rx.Observable import rx.Observable
import rx.android.schedulers.AndroidSchedulers import rx.android.schedulers.AndroidSchedulers
import rx.schedulers.Schedulers import rx.schedulers.Schedulers
import rx.subjects.BehaviorSubject
import rx.subscriptions.CompositeSubscription import rx.subscriptions.CompositeSubscription
import timber.log.Timber import timber.log.Timber
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
@ -39,9 +36,11 @@ import uy.kohesive.injekt.injectLazy
* @param provider the downloads directory provider. * @param provider the downloads directory provider.
* @param cache the downloads cache, used to add the downloads to the cache after their completion. * @param cache the downloads cache, used to add the downloads to the cache after their completion.
*/ */
class Downloader(private val context: Context, class Downloader(
private val provider: DownloadProvider, private val context: Context,
private val cache: DownloadCache) { private val provider: DownloadProvider,
private val cache: DownloadCache
) {
/** /**
* Store for persisting downloads across restarts. * Store for persisting downloads across restarts.
@ -58,11 +57,6 @@ class Downloader(private val context: Context,
*/ */
private val sourceManager: SourceManager by injectLazy() private val sourceManager: SourceManager by injectLazy()
/**
* Preferences.
*/
private val preferences: PreferencesHelper by injectLazy()
/** /**
* Notifier for the downloader state and progress. * Notifier for the downloader state and progress.
*/ */
@ -73,11 +67,6 @@ class Downloader(private val context: Context,
*/ */
private val subscriptions = CompositeSubscription() private val subscriptions = CompositeSubscription()
/**
* Subject to do a live update of the number of simultaneous downloads.
*/
private val threadsSubject = BehaviorSubject.create<Int>()
/** /**
* Relay to send a list of downloads to the downloader. * Relay to send a list of downloads to the downloader.
*/ */
@ -116,9 +105,6 @@ class Downloader(private val context: Context,
val pending = queue.filter { it.status != Download.DOWNLOADED } val pending = queue.filter { it.status != Download.DOWNLOADED }
pending.forEach { if (it.status != Download.QUEUE) it.status = Download.QUEUE } pending.forEach { if (it.status != Download.QUEUE) it.status = Download.QUEUE }
// Show download notification when simultaneous download > 1.
notifier.onProgressChange(queue)
downloadsRelay.call(pending) downloadsRelay.call(pending)
return !pending.isEmpty() return !pending.isEmpty()
} }
@ -185,14 +171,8 @@ class Downloader(private val context: Context,
subscriptions.clear() subscriptions.clear()
subscriptions += preferences.downloadThreads().asObservable() subscriptions += downloadsRelay.concatMapIterable { it }
.subscribe { .concatMap { downloadChapter(it).subscribeOn(Schedulers.io()) }
threadsSubject.onNext(it)
notifier.multipleDownloadThreads = it > 1
}
subscriptions += downloadsRelay.flatMap { Observable.from(it) }
.lift(DynamicConcurrentMergeOperator<Download, Download>({ downloadChapter(it) }, threadsSubject))
.onBackpressureBuffer() .onBackpressureBuffer()
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.subscribe({ completeDownload(it) .subscribe({ completeDownload(it)
@ -250,15 +230,9 @@ class Downloader(private val context: Context,
// Initialize queue size. // Initialize queue size.
notifier.initialQueueSize = queue.size notifier.initialQueueSize = queue.size
// Initial multi-thread
notifier.multipleDownloadThreads = preferences.downloadThreads().getOrDefault() > 1
if (isRunning) { if (isRunning) {
// Send the list of downloads to the downloader. // Send the list of downloads to the downloader.
downloadsRelay.call(chaptersToQueue) downloadsRelay.call(chaptersToQueue)
} else {
// Show initial notification.
notifier.onProgressChange(queue)
} }
// Start downloader if needed // Start downloader if needed
@ -273,7 +247,8 @@ class Downloader(private val context: Context,
* *
* @param download the chapter to be downloaded. * @param download the chapter to be downloaded.
*/ */
private fun downloadChapter(download: Download): Observable<Download> { private fun downloadChapter(download: Download): Observable<Download> = Observable.defer {
Timber.e("Thread: ${Thread.currentThread()}")
val chapterDirname = provider.getChapterDirName(download.chapter) val chapterDirname = provider.getChapterDirName(download.chapter)
val mangaDir = provider.getMangaDir(download.manga, download.source) val mangaDir = provider.getMangaDir(download.manga, download.source)
val tmpDir = mangaDir.createDirectory("${chapterDirname}_tmp") val tmpDir = mangaDir.createDirectory("${chapterDirname}_tmp")
@ -292,7 +267,7 @@ class Downloader(private val context: Context,
Observable.just(download.pages!!) Observable.just(download.pages!!)
} }
return pageListObservable pageListObservable
.doOnNext { _ -> .doOnNext { _ ->
// Delete all temporary (unfinished) files // Delete all temporary (unfinished) files
tmpDir.listFiles() tmpDir.listFiles()
@ -307,7 +282,7 @@ class Downloader(private val context: Context,
// Start downloading images, consider we can have downloaded images already // Start downloading images, consider we can have downloaded images already
.concatMap { page -> getOrDownloadImage(page, download, tmpDir) } .concatMap { page -> getOrDownloadImage(page, download, tmpDir) }
// Do when page is downloaded. // Do when page is downloaded.
.doOnNext { notifier.onProgressChange(download, queue) } .doOnNext { notifier.onProgressChange(download) }
.toList() .toList()
.map { _ -> download } .map { _ -> download }
// Do after download completes // Do after download completes
@ -318,7 +293,7 @@ class Downloader(private val context: Context,
notifier.onError(error.message, download.chapter.name) notifier.onError(error.message, download.chapter.name)
download download
} }
.subscribeOn(Schedulers.io())
} }
/** /**
@ -448,7 +423,6 @@ class Downloader(private val context: Context,
if (download.status == Download.DOWNLOADED) { if (download.status == Download.DOWNLOADED) {
// remove downloaded chapter from queue // remove downloaded chapter from queue
queue.remove(download) queue.remove(download)
notifier.onProgressChange(queue)
} }
if (areAllDownloadsFinished()) { if (areAllDownloadsFinished()) {
if (notifier.isSingleChapter && !notifier.errorThrown) { if (notifier.isSingleChapter && !notifier.errorThrown) {
@ -465,4 +439,4 @@ class Downloader(private val context: Context,
return queue.none { it.status <= Download.DOWNLOADING } return queue.none { it.status <= Download.DOWNLOADING }
} }
} }

View file

@ -67,8 +67,6 @@ object PreferenceKeys {
const val downloadsDirectory = "download_directory" const val downloadsDirectory = "download_directory"
const val downloadThreads = "pref_download_slots_key"
const val downloadOnlyOverWifi = "pref_download_only_over_wifi_key" const val downloadOnlyOverWifi = "pref_download_only_over_wifi_key"
const val numberOfBackups = "backup_slots" const val numberOfBackups = "backup_slots"

View file

@ -123,8 +123,6 @@ class PreferencesHelper(val context: Context) {
fun downloadsDirectory() = rxPrefs.getString(Keys.downloadsDirectory, defaultDownloadsDir.toString()) fun downloadsDirectory() = rxPrefs.getString(Keys.downloadsDirectory, defaultDownloadsDir.toString())
fun downloadThreads() = rxPrefs.getInteger(Keys.downloadThreads, 1)
fun downloadOnlyOverWifi() = prefs.getBoolean(Keys.downloadOnlyOverWifi, true) fun downloadOnlyOverWifi() = prefs.getBoolean(Keys.downloadOnlyOverWifi, true)
fun numberOfBackups() = rxPrefs.getInteger(Keys.numberOfBackups, 1) fun numberOfBackups() = rxPrefs.getInteger(Keys.numberOfBackups, 1)

View file

@ -12,7 +12,6 @@ import android.support.v4.content.ContextCompat
import android.support.v7.preference.PreferenceScreen import android.support.v7.preference.PreferenceScreen
import com.afollestad.materialdialogs.MaterialDialog import com.afollestad.materialdialogs.MaterialDialog
import com.hippo.unifile.UniFile import com.hippo.unifile.UniFile
import com.nononsenseapps.filepicker.FilePickerActivity
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.DatabaseHelper import eu.kanade.tachiyomi.data.database.DatabaseHelper
import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.PreferencesHelper
@ -20,7 +19,6 @@ import eu.kanade.tachiyomi.data.preference.getOrDefault
import eu.kanade.tachiyomi.ui.base.controller.DialogController import eu.kanade.tachiyomi.ui.base.controller.DialogController
import eu.kanade.tachiyomi.util.DiskUtil import eu.kanade.tachiyomi.util.DiskUtil
import eu.kanade.tachiyomi.util.getFilePicker import eu.kanade.tachiyomi.util.getFilePicker
import eu.kanade.tachiyomi.widget.CustomLayoutPickerActivity
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
@ -63,14 +61,6 @@ class SettingsDownloadController : SettingsController() {
titleRes = R.string.pref_download_only_over_wifi titleRes = R.string.pref_download_only_over_wifi
defaultValue = true defaultValue = true
} }
intListPreference {
key = Keys.downloadThreads
titleRes = R.string.pref_download_slots
entries = arrayOf("1", "2", "3")
entryValues = arrayOf("1", "2", "3")
defaultValue = "1"
summary = "%s"
}
preferenceCategory { preferenceCategory {
titleRes = R.string.pref_remove_after_read titleRes = R.string.pref_remove_after_read
@ -206,4 +196,4 @@ class SettingsDownloadController : SettingsController() {
const val DOWNLOAD_DIR_PRE_L = 103 const val DOWNLOAD_DIR_PRE_L = 103
const val DOWNLOAD_DIR_L = 104 const val DOWNLOAD_DIR_L = 104
} }
} }

View file

@ -1,196 +0,0 @@
package eu.kanade.tachiyomi.util;
import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import rx.Observable;
import rx.Observable.Operator;
import rx.Subscriber;
import rx.Subscription;
import rx.functions.Action0;
import rx.functions.Action1;
import rx.functions.Func1;
import rx.subscriptions.CompositeSubscription;
import rx.subscriptions.Subscriptions;
public class DynamicConcurrentMergeOperator<T, R> implements Operator<R, T> {
private final Func1<? super T, ? extends Observable<? extends R>> mapper;
private final Observable<Integer> workerCount;
public DynamicConcurrentMergeOperator(
Func1<? super T, ? extends Observable<? extends R>> mapper,
Observable<Integer> workerCount) {
this.mapper = mapper;
this.workerCount = workerCount;
}
@Override
public Subscriber<? super T> call(Subscriber<? super R> t) {
DynamicConcurrentMerge<T, R> parent = new DynamicConcurrentMerge<>(t, mapper);
t.add(parent);
parent.init(workerCount);
return parent;
}
static final class DynamicConcurrentMerge<T, R> extends Subscriber<T> {
private final Subscriber<? super R> actual;
private final Func1<? super T, ? extends Observable<? extends R>> mapper;
private final Queue<T> queue;
private final CopyOnWriteArrayList<DynamicWorker<T, R>> workers;
private final CompositeSubscription composite;
private final AtomicInteger wipActive;
private final AtomicBoolean once;
private long id;
public DynamicConcurrentMerge(Subscriber<? super R> actual,
Func1<? super T, ? extends Observable<? extends R>> mapper) {
this.actual = actual;
this.mapper = mapper;
this.queue = new ConcurrentLinkedQueue<>();
this.workers = new CopyOnWriteArrayList<>();
this.composite = new CompositeSubscription();
this.wipActive = new AtomicInteger(1);
this.once = new AtomicBoolean();
this.add(composite);
this.request(0);
}
public void init(Observable<Integer> workerCount) {
Subscription wc = workerCount.subscribe(new Action1<Integer>() {
@Override
public void call(Integer n) {
int n0 = workers.size();
if (n0 < n) {
for (int i = n0; i < n; i++) {
DynamicWorker<T, R> dw = new DynamicWorker<>(++id, DynamicConcurrentMerge.this);
workers.add(dw);
DynamicConcurrentMerge.this.request(1);
dw.tryNext();
}
} else if (n0 > n) {
for (int i = 0; i < n; i++) {
workers.get(i).start();
}
for (int i = n0 - 1; i >= n; i--) {
workers.get(i).stop();
}
}
if (!once.get() && once.compareAndSet(false, true)) {
DynamicConcurrentMerge.this.request(n);
}
}
}, new Action1<Throwable>() {
@Override
public void call(Throwable e) {DynamicConcurrentMerge.this.onError(e);}
});
composite.add(wc);
}
void requestMore(long n) {
request(n);
}
@Override
public void onNext(T t) {
queue.offer(t);
wipActive.getAndIncrement();
for (DynamicWorker<T, R> w : workers) {
w.tryNext();
}
}
@Override
public void onError(Throwable e) {
composite.unsubscribe();
actual.onError(e);
}
@Override
public void onCompleted() {
if (wipActive.decrementAndGet() == 0) {
actual.onCompleted();
}
}
}
static final class DynamicWorker<T, R> {
private final long id;
private final AtomicBoolean running;
private final DynamicConcurrentMerge<T, R> parent;
private final AtomicBoolean stop;
public DynamicWorker(long id, DynamicConcurrentMerge<T, R> parent) {
this.id = id;
this.parent = parent;
this.stop = new AtomicBoolean();
this.running = new AtomicBoolean();
}
public void tryNext() {
if (!running.get() && running.compareAndSet(false, true)) {
T t;
if (stop.get()) {
parent.workers.remove(this);
return;
}
t = parent.queue.poll();
if (t == null) {
running.set(false);
return;
}
Observable<? extends R> out = parent.mapper.call(t);
final Subscriber<R> s = new Subscriber<R>() {
@Override
public void onNext(R t) {
parent.actual.onNext(t);
}
@Override
public void onError(Throwable e) {
parent.onError(e);
}
@Override
public void onCompleted() {
parent.onCompleted();
if (parent.wipActive.get() != 0) {
running.set(false);
parent.requestMore(1);
tryNext();
}
}
};
parent.composite.add(s);
s.add(Subscriptions.create(new Action0() {
@Override
public void call() {parent.composite.remove(s);}
}));
out.subscribe(s);
}
}
public void start() {
stop.set(false);
tryNext();
}
public void stop() {
stop.set(true);
if (running.compareAndSet(false, true)) {
parent.workers.remove(this);
}
}
}
}

View file

@ -219,7 +219,6 @@
<!-- Downloads section --> <!-- Downloads section -->
<string name="pref_download_directory">Downloads directory</string> <string name="pref_download_directory">Downloads directory</string>
<string name="pref_download_slots">Simultaneous downloads</string>
<string name="pref_download_only_over_wifi">Only download over Wi-Fi</string> <string name="pref_download_only_over_wifi">Only download over Wi-Fi</string>
<string name="pref_remove_after_marked_as_read">Remove when marked as read</string> <string name="pref_remove_after_marked_as_read">Remove when marked as read</string>
<string name="pref_remove_after_read">Remove after read</string> <string name="pref_remove_after_read">Remove after read</string>