From e19c62a8aef2203eb287ef2f450c95b642f7f2f0 Mon Sep 17 00:00:00 2001 From: Dani <17619547+shabnix@users.noreply.github.com> Date: Fri, 23 Aug 2024 11:43:46 +0200 Subject: [PATCH] Add option to skip downloading duplicate read chapters (#1125) * Add query to get chapter count by manga and chapter number * Add functions to get chapter count by manga and chapter number * Only count read chapters * Add interactor * Savepoint * Extract new chapter logic to separate function * Update javadocs * Add preference to toggle new functionality * Add todo * Add debug logcat * Use string resource instead of hardcoding title * Add temporary logcat for debugging * Fix detekt issues * Update javadocs * Update download unread chapters preference * Remove debug logcat calls * Update javadocs * Resolve issue where read chapters were still being downloaded during manual manga fetch * Apply code review changes * Apply code review changes * Revert "Apply code review changes" This reverts commit 1a2dce78acc66a7c529ce5b572bdaf94804b1a30. * Revert "Apply code review changes" This reverts commit ac2a77829313967ad39ce3cb0c0231083b9d640d. * Group download chapter logic inside the interactor GetChaptersToDownload * Update javadocs * Apply code review * Apply code review * Apply code review * Update CHANGELOG.md to include the new feature * Run spotless * Update domain/src/main/java/mihon/domain/chapter/interactor/FilterChaptersForDownload.kt --------- Co-authored-by: AntsyLich <59261191+AntsyLich@users.noreply.github.com> (cherry picked from commit ca968f162ef7a61a9036b7ab9bea407a6334801d) # Conflicts: # CHANGELOG.md # app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateJob.kt # app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaScreenModel.kt --- .../java/eu/kanade/domain/DomainModule.kt | 2 + .../settings/screen/SettingsDownloadScreen.kt | 6 ++ .../data/library/LibraryUpdateJob.kt | 14 ++- .../source/online/all/MergedSource.kt | 24 +++-- .../tachiyomi/ui/manga/MangaScreenModel.kt | 16 ++-- .../kanade/tachiyomi/util/MangaExtensions.kt | 26 ------ .../interactor/FilterChaptersForDownload.kt | 88 +++++++++++++++++++ .../download/service/DownloadPreferences.kt | 2 + .../moko-resources/base/strings.xml | 1 + 9 files changed, 122 insertions(+), 57 deletions(-) create mode 100644 domain/src/main/java/mihon/domain/chapter/interactor/FilterChaptersForDownload.kt diff --git a/app/src/main/java/eu/kanade/domain/DomainModule.kt b/app/src/main/java/eu/kanade/domain/DomainModule.kt index 29a69f022..eba1fe6a2 100644 --- a/app/src/main/java/eu/kanade/domain/DomainModule.kt +++ b/app/src/main/java/eu/kanade/domain/DomainModule.kt @@ -24,6 +24,7 @@ import eu.kanade.domain.track.interactor.RefreshTracks import eu.kanade.domain.track.interactor.SyncChapterProgressWithTrack import eu.kanade.domain.track.interactor.TrackChapter import mihon.data.repository.ExtensionRepoRepositoryImpl +import mihon.domain.chapter.interactor.FilterChaptersForDownload import mihon.domain.extensionrepo.interactor.CreateExtensionRepo import mihon.domain.extensionrepo.interactor.DeleteExtensionRepo import mihon.domain.extensionrepo.interactor.GetExtensionRepo @@ -152,6 +153,7 @@ class DomainModule : InjektModule { addFactory { ShouldUpdateDbChapter() } addFactory { SyncChaptersWithSource(get(), get(), get(), get(), get(), get(), get(), get()) } addFactory { GetAvailableScanlators(get()) } + addFactory { FilterChaptersForDownload(get(), get(), get(), get()) } addSingletonFactory { HistoryRepositoryImpl(get()) } addFactory { GetHistory(get()) } diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsDownloadScreen.kt b/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsDownloadScreen.kt index 072013415..27f0504ce 100644 --- a/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsDownloadScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsDownloadScreen.kt @@ -120,6 +120,7 @@ object SettingsDownloadScreen : SearchableSettings { allCategories: List, ): Preference.PreferenceGroup { val downloadNewChaptersPref = downloadPreferences.downloadNewChapters() + val downloadNewUnreadChaptersOnlyPref = downloadPreferences.downloadNewUnreadChaptersOnly() val downloadNewChapterCategoriesPref = downloadPreferences.downloadNewChapterCategories() val downloadNewChapterCategoriesExcludePref = downloadPreferences.downloadNewChapterCategoriesExclude() @@ -152,6 +153,11 @@ object SettingsDownloadScreen : SearchableSettings { pref = downloadNewChaptersPref, title = stringResource(MR.strings.pref_download_new), ), + Preference.PreferenceItem.SwitchPreference( + pref = downloadNewUnreadChaptersOnlyPref, + title = stringResource(MR.strings.pref_download_new_unread_chapters_only), + enabled = downloadNewChapters, + ), Preference.PreferenceItem.TextPreference( title = stringResource(MR.strings.categories), subtitle = getCategoriesLabel( diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateJob.kt b/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateJob.kt index 7c179733d..7053c68ca 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateJob.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateJob.kt @@ -34,7 +34,6 @@ import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.model.UpdateStrategy import eu.kanade.tachiyomi.source.online.all.MergedSource import eu.kanade.tachiyomi.util.prepUpdateCover -import eu.kanade.tachiyomi.util.shouldDownloadNewChapters import eu.kanade.tachiyomi.util.storage.getUriCompat import eu.kanade.tachiyomi.util.system.createFileInCacheDir import eu.kanade.tachiyomi.util.system.isConnectedToWifi @@ -58,17 +57,16 @@ import kotlinx.coroutines.runBlocking import kotlinx.coroutines.sync.Semaphore import kotlinx.coroutines.sync.withPermit import logcat.LogPriority +import mihon.domain.chapter.interactor.FilterChaptersForDownload import tachiyomi.core.common.i18n.stringResource import tachiyomi.core.common.preference.getAndSet import tachiyomi.core.common.util.lang.withIOContext import tachiyomi.core.common.util.system.logcat import tachiyomi.domain.UnsortedPreferences -import tachiyomi.domain.category.interactor.GetCategories import tachiyomi.domain.category.model.Category import tachiyomi.domain.chapter.interactor.GetChaptersByMangaId import tachiyomi.domain.chapter.model.Chapter import tachiyomi.domain.chapter.model.NoChaptersException -import tachiyomi.domain.download.service.DownloadPreferences import tachiyomi.domain.library.model.GroupLibraryMode import tachiyomi.domain.library.model.LibraryGroup import tachiyomi.domain.library.model.LibraryManga @@ -108,16 +106,15 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet CoroutineWorker(context, workerParams) { private val sourceManager: SourceManager = Injekt.get() - private val downloadPreferences: DownloadPreferences = Injekt.get() private val libraryPreferences: LibraryPreferences = Injekt.get() private val downloadManager: DownloadManager = Injekt.get() private val coverCache: CoverCache = Injekt.get() private val getLibraryManga: GetLibraryManga = Injekt.get() private val getManga: GetManga = Injekt.get() private val updateManga: UpdateManga = Injekt.get() - private val getCategories: GetCategories = Injekt.get() private val syncChaptersWithSource: SyncChaptersWithSource = Injekt.get() private val fetchInterval: FetchInterval = Injekt.get() + private val filterChaptersForDownload: FilterChaptersForDownload = Injekt.get() // SY --> private val getFavorites: GetFavorites = Injekt.get() @@ -425,9 +422,10 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet // SY <-- if (newChapters.isNotEmpty()) { - val categoryIds = getCategories.await(manga.id).map { it.id } - if (manga.shouldDownloadNewChapters(categoryIds, downloadPreferences)) { - downloadChapters(manga, newChapters) + val chaptersToDownload = filterChaptersForDownload.await(manga, newChapters) + + if (chaptersToDownload.isNotEmpty()) { + downloadChapters(manga, chaptersToDownload) hasDownloads.set(true) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/source/online/all/MergedSource.kt b/app/src/main/java/eu/kanade/tachiyomi/source/online/all/MergedSource.kt index 52b839cd3..d9852f9eb 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/source/online/all/MergedSource.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/source/online/all/MergedSource.kt @@ -11,7 +11,6 @@ import eu.kanade.tachiyomi.source.model.SChapter import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.model.copy import eu.kanade.tachiyomi.source.online.HttpSource -import eu.kanade.tachiyomi.util.shouldDownloadNewChapters import exh.source.MERGED_SOURCE_ID import kotlinx.coroutines.CancellationException import kotlinx.coroutines.async @@ -19,6 +18,7 @@ import kotlinx.coroutines.awaitAll import kotlinx.coroutines.supervisorScope import kotlinx.coroutines.sync.Semaphore import kotlinx.coroutines.sync.withPermit +import mihon.domain.chapter.interactor.FilterChaptersForDownload import okhttp3.Response import tachiyomi.core.common.util.lang.withIOContext import tachiyomi.domain.category.interactor.GetCategories @@ -42,6 +42,7 @@ class MergedSource : HttpSource() { private val sourceManager: SourceManager by injectLazy() private val downloadManager: DownloadManager by injectLazy() private val downloadPreferences: DownloadPreferences by injectLazy() + private val filterChaptersForDownload: FilterChaptersForDownload by injectLazy() override val id: Long = MERGED_SOURCE_ID @@ -119,13 +120,6 @@ class MergedSource : HttpSource() { "Manga references are empty, chapters unavailable, merge is likely corrupted" } - val ifDownloadNewChapters = downloadChapters && - manga.shouldDownloadNewChapters( - getCategories.await(manga.id).map { - it.id - }, - downloadPreferences, - ) val semaphore = Semaphore(5) var exception: Exception? = null return supervisorScope { @@ -142,11 +136,15 @@ class MergedSource : HttpSource() { val chapterList = source.getChapterList(loadedManga.toSManga()) val results = syncChaptersWithSource.await(chapterList, loadedManga, source) - if (ifDownloadNewChapters && reference.downloadChapters) { - downloadManager.downloadChapters( - loadedManga, - results, - ) + + if (reference.downloadChapters) { + val chaptersToDownload = filterChaptersForDownload.await(manga, results) + if (chaptersToDownload.isNotEmpty()) { + downloadManager.downloadChapters( + loadedManga, + chaptersToDownload, + ) + } } results } else { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaScreenModel.kt index 2e072499b..c30bef39a 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaScreenModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaScreenModel.kt @@ -44,7 +44,6 @@ import eu.kanade.tachiyomi.source.online.all.MergedSource import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences import eu.kanade.tachiyomi.util.chapter.getNextUnread import eu.kanade.tachiyomi.util.removeCovers -import eu.kanade.tachiyomi.util.shouldDownloadNewChapters import exh.debug.DebugToggles import exh.eh.EHentaiUpdateHelper import exh.log.xLogD @@ -78,6 +77,7 @@ import kotlinx.coroutines.flow.update import kotlinx.coroutines.isActive import kotlinx.coroutines.launch import logcat.LogPriority +import mihon.domain.chapter.interactor.FilterChaptersForDownload import tachiyomi.core.common.i18n.stringResource import tachiyomi.core.common.preference.CheckboxState import tachiyomi.core.common.preference.TriState @@ -99,7 +99,6 @@ import tachiyomi.domain.chapter.model.ChapterUpdate import tachiyomi.domain.chapter.model.NoChaptersException import tachiyomi.domain.chapter.service.calculateChapterGap import tachiyomi.domain.chapter.service.getChapterSort -import tachiyomi.domain.download.service.DownloadPreferences import tachiyomi.domain.library.service.LibraryPreferences import tachiyomi.domain.manga.interactor.DeleteByMergeId import tachiyomi.domain.manga.interactor.DeleteMangaById @@ -140,7 +139,6 @@ class MangaScreenModel( val mangaId: Long, private val isFromSource: Boolean, val smartSearched: Boolean, - private val downloadPreferences: DownloadPreferences = Injekt.get(), private val libraryPreferences: LibraryPreferences = Injekt.get(), readerPreferences: ReaderPreferences = Injekt.get(), uiPreferences: UiPreferences = Injekt.get(), @@ -180,6 +178,7 @@ class MangaScreenModel( private val addTracks: AddTracks = Injekt.get(), private val setMangaCategories: SetMangaCategories = Injekt.get(), private val mangaRepository: MangaRepository = Injekt.get(), + private val filterChaptersForDownload: FilterChaptersForDownload = Injekt.get(), val snackbarHostState: SnackbarHostState = SnackbarHostState(), ) : StateScreenModel(State.Loading) { @@ -1313,14 +1312,11 @@ class MangaScreenModel( private fun downloadNewChapters(chapters: List) { screenModelScope.launchNonCancellable { val manga = successState?.manga ?: return@launchNonCancellable - val categories = getCategories.await(manga.id).map { it.id } - if (chapters.isEmpty() || - !manga.shouldDownloadNewChapters(categories, downloadPreferences) || - manga.isEhBasedManga() - ) { - return@launchNonCancellable + val chaptersToDownload = filterChaptersForDownload.await(manga, chapters) + + if (chaptersToDownload.isNotEmpty() /* SY --> */ && !manga.isEhBasedManga() /* SY <-- */) { + downloadChapters(chaptersToDownload) } - downloadChapters(chapters) } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/MangaExtensions.kt b/app/src/main/java/eu/kanade/tachiyomi/util/MangaExtensions.kt index 50834eadb..8e05869ec 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/util/MangaExtensions.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/util/MangaExtensions.kt @@ -5,7 +5,6 @@ import eu.kanade.domain.manga.model.hasCustomCover import eu.kanade.domain.manga.model.toSManga import eu.kanade.tachiyomi.data.cache.CoverCache import eu.kanade.tachiyomi.source.model.SManga -import tachiyomi.domain.download.service.DownloadPreferences import tachiyomi.domain.manga.model.Manga import tachiyomi.source.local.image.LocalCoverManager import tachiyomi.source.local.isLocal @@ -50,31 +49,6 @@ fun Manga.removeCovers(coverCache: CoverCache = Injekt.get()): Manga { } } -fun Manga.shouldDownloadNewChapters(dbCategories: List, preferences: DownloadPreferences): Boolean { - if (!favorite) return false - - val categories = dbCategories.ifEmpty { listOf(0L) } - - // Boolean to determine if user wants to automatically download new chapters. - val downloadNewChapters = preferences.downloadNewChapters().get() - if (!downloadNewChapters) return false - - val includedCategories = preferences.downloadNewChapterCategories().get().map { it.toLong() } - val excludedCategories = preferences.downloadNewChapterCategoriesExclude().get().map { it.toLong() } - - // Default: Download from all categories - if (includedCategories.isEmpty() && excludedCategories.isEmpty()) return true - - // In excluded category - if (categories.any { it in excludedCategories }) return false - - // Included category not selected - if (includedCategories.isEmpty()) return true - - // In included category - return categories.any { it in includedCategories } -} - suspend fun Manga.editCover( coverManager: LocalCoverManager, stream: InputStream, diff --git a/domain/src/main/java/mihon/domain/chapter/interactor/FilterChaptersForDownload.kt b/domain/src/main/java/mihon/domain/chapter/interactor/FilterChaptersForDownload.kt new file mode 100644 index 000000000..1fe002ff1 --- /dev/null +++ b/domain/src/main/java/mihon/domain/chapter/interactor/FilterChaptersForDownload.kt @@ -0,0 +1,88 @@ +package mihon.domain.chapter.interactor + +import exh.source.MERGED_SOURCE_ID +import tachiyomi.domain.category.interactor.GetCategories +import tachiyomi.domain.chapter.interactor.GetChaptersByMangaId +import tachiyomi.domain.chapter.interactor.GetMergedChaptersByMangaId +import tachiyomi.domain.chapter.model.Chapter +import tachiyomi.domain.download.service.DownloadPreferences +import tachiyomi.domain.manga.model.Manga + +/** + * Interactor responsible for determining which chapters of a manga should be downloaded. + * + * @property getChaptersByMangaId Interactor for retrieving chapters by manga ID. + * @property downloadPreferences User preferences related to chapter downloads. + * @property getCategories Interactor for retrieving categories associated with a manga. + */ +class FilterChaptersForDownload( + private val getChaptersByMangaId: GetChaptersByMangaId, + private val getMergedChaptersByMangaId: GetMergedChaptersByMangaId, + private val downloadPreferences: DownloadPreferences, + private val getCategories: GetCategories, +) { + + /** + * Determines which chapters of a manga should be downloaded based on user preferences. + * + * @param manga The manga for which chapters may be downloaded. + * @param newChapters The list of new chapters available for the manga. + * @return A list of chapters that should be downloaded + */ + suspend fun await(manga: Manga, newChapters: List): List { + if ( + newChapters.isEmpty() || + !downloadPreferences.downloadNewChapters().get() || + !manga.shouldDownloadNewChapters() + ) { + return emptyList() + } + + if (!downloadPreferences.downloadNewUnreadChaptersOnly().get()) return newChapters + + // SY --> + val existingChapters = if (manga.source == MERGED_SOURCE_ID) { + getMergedChaptersByMangaId.await(manga.id) + } else { + getChaptersByMangaId.await(manga.id) + } + + val readChapterNumbers = existingChapters + // SY <-- + .asSequence() + .filter { it.read && it.isRecognizedNumber } + .map { it.chapterNumber } + .toSet() + + return newChapters.filterNot { it.chapterNumber in readChapterNumbers } + } + + /** + * Determines whether new chapters should be downloaded for the manga based on user preferences and the + * categories to which the manga belongs. + * + * @return `true` if chapters of the manga should be downloaded + */ + private suspend fun Manga.shouldDownloadNewChapters(): Boolean { + if (!favorite) return false + + val categories = getCategories.await(id).map { it.id }.ifEmpty { listOf(DEFAULT_CATEGORY_ID) } + val includedCategories = downloadPreferences.downloadNewChapterCategories().get().map { it.toLong() } + val excludedCategories = downloadPreferences.downloadNewChapterCategoriesExclude().get().map { it.toLong() } + + return when { + // Default Download from all categories + includedCategories.isEmpty() && excludedCategories.isEmpty() -> true + // In excluded category + categories.any { it in excludedCategories } -> false + // Included category not selected + includedCategories.isEmpty() -> true + // In included category + else -> categories.any { it in includedCategories } + } + } + + companion object { + private const val DEFAULT_CATEGORY_ID = 0L + } +} diff --git a/domain/src/main/java/tachiyomi/domain/download/service/DownloadPreferences.kt b/domain/src/main/java/tachiyomi/domain/download/service/DownloadPreferences.kt index a0625e5a7..9251b39db 100644 --- a/domain/src/main/java/tachiyomi/domain/download/service/DownloadPreferences.kt +++ b/domain/src/main/java/tachiyomi/domain/download/service/DownloadPreferences.kt @@ -42,4 +42,6 @@ class DownloadPreferences( "download_new_categories_exclude", emptySet(), ) + + fun downloadNewUnreadChaptersOnly() = preferenceStore.getBoolean("download_new_unread_chapters_only", false) } diff --git a/i18n/src/commonMain/moko-resources/base/strings.xml b/i18n/src/commonMain/moko-resources/base/strings.xml index 3f147ecc2..95e5e7b19 100755 --- a/i18n/src/commonMain/moko-resources/base/strings.xml +++ b/i18n/src/commonMain/moko-resources/base/strings.xml @@ -484,6 +484,7 @@ Fifth to last read chapter Auto-download Download new chapters + Skip downloading duplicate read chapters Entries in excluded categories will not be downloaded even if they are also in included categories. Download ahead Auto download while reading