From cb431b40328ea0b822130977b038462ddcf3214d 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 (mihonapp/mihon#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) --- CHANGELOG.md | 1 + .../java/eu/kanade/domain/DomainModule.kt | 2 + .../settings/screen/SettingsDownloadScreen.kt | 7 ++ .../data/library/LibraryUpdateJob.kt | 14 ++-- .../source/online/all/MergedSource.kt | 28 +++---- .../tachiyomi/ui/manga/MangaScreenModel.kt | 22 +++--- .../kanade/tachiyomi/util/MangaExtensions.kt | 26 ------- .../interactor/FilterChaptersForDownload.kt | 77 +++++++++++++++++++ .../download/service/DownloadPreferences.kt | 2 + .../moko-resources/base/strings.xml | 1 + 10 files changed, 119 insertions(+), 61 deletions(-) create mode 100644 domain/src/main/java/mihon/domain/chapter/interactor/FilterChaptersForDownload.kt diff --git a/CHANGELOG.md b/CHANGELOG.md index b1643a2302..7affa28c93 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Copy Tracker URL option to tracker sheet ([@mm12](https://github.com/mm12)) ([#1101](https://github.com/mihonapp/mihon/pull/1101)) - A button to exclude all scanlators in exclude scanlators dialog ([@AntsyLich](https://github.com/AntsyLich)) ([`84b2164`](https://github.com/mihonapp/mihon/commit/84b2164787a795f3fd757c325cbfb6ef660ac3a3)) - Open in browser option to reader menu ([@mm12](https://github.com/mm12)) ([#1110](https://github.com/mihonapp/mihon/pull/1110)) +- Option to skip downloading duplicate read chapters ([@shabnix](https://github.com/shabnix)) ([#1125](https://github.com/mihonapp/mihon/pull/1125)) ### Changed - Read archive files from memory instead of extracting files to internal storage ([@FooIbar](https://github.com/FooIbar)) ([#326](https://github.com/mihonapp/mihon/pull/326)) diff --git a/app/src/main/java/eu/kanade/domain/DomainModule.kt b/app/src/main/java/eu/kanade/domain/DomainModule.kt index 29a69f0226..3ac447b44d 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()) } 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 18919604c7..94b64b4a00 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 @@ -28,6 +28,7 @@ import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get object SettingsDownloadScreen : SearchableSettings { + private fun readResolve(): Any = SettingsDownloadScreen @ReadOnlyComposable @Composable @@ -124,6 +125,7 @@ object SettingsDownloadScreen : SearchableSettings { allCategories: List, ): Preference.PreferenceGroup { val downloadNewChaptersPref = downloadPreferences.downloadNewChapters() + val downloadNewUnreadChaptersOnlyPref = downloadPreferences.downloadNewUnreadChaptersOnly() val downloadNewChapterCategoriesPref = downloadPreferences.downloadNewChapterCategories() val downloadNewChapterCategoriesExcludePref = downloadPreferences.downloadNewChapterCategoriesExclude() @@ -156,6 +158,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 19cbe42340..2ec063a37b 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateJob.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateJob.kt @@ -35,7 +35,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 @@ -59,17 +58,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 @@ -109,16 +107,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() @@ -444,9 +441,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 52b839cd31..2b630b2daf 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,11 +18,10 @@ 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 import tachiyomi.domain.chapter.model.Chapter -import tachiyomi.domain.download.service.DownloadPreferences import tachiyomi.domain.manga.interactor.GetManga import tachiyomi.domain.manga.interactor.GetMergedReferencesById import tachiyomi.domain.manga.interactor.NetworkToLocalManga @@ -38,10 +36,12 @@ class MergedSource : HttpSource() { private val syncChaptersWithSource: SyncChaptersWithSource by injectLazy() private val networkToLocalManga: NetworkToLocalManga by injectLazy() private val updateManga: UpdateManga by injectLazy() - private val getCategories: GetCategories by injectLazy() private val sourceManager: SourceManager by injectLazy() private val downloadManager: DownloadManager by injectLazy() - private val downloadPreferences: DownloadPreferences by injectLazy() + + // KMK --> + private val filterChaptersForDownload: FilterChaptersForDownload by injectLazy() + // KMK <-- override val id: Long = MERGED_SOURCE_ID @@ -119,13 +119,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 { @@ -140,9 +133,16 @@ class MergedSource : HttpSource() { val (source, loadedManga, reference) = it.load() if (loadedManga != null && reference.getChapterUpdates) { val chapterList = source.getChapterList(loadedManga.toSManga()) - val results = + val chapters = syncChaptersWithSource.await(chapterList, loadedManga, source) - if (ifDownloadNewChapters && reference.downloadChapters) { + // KMK --> + // KMK: this should check if user preferences & manga's categories allowed to download + // KMK: the checking for skip duplicated-read chapters might not work for merged mangas but won't affect the download + val results = filterChaptersForDownload.await(loadedManga, chapters) + if (downloadChapters && + // KMK <-- + reference.downloadChapters + ) { downloadManager.downloadChapters( loadedManga, results, 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 e92eaba9bc..96240706e1 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 @@ -65,7 +65,6 @@ import eu.kanade.tachiyomi.ui.manga.RelatedManga.Companion.sorted 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 eu.kanade.tachiyomi.util.system.getBitmapOrNull import eu.kanade.tachiyomi.util.system.toast import exh.debug.DebugToggles @@ -102,6 +101,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 @@ -123,7 +123,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 @@ -170,7 +169,6 @@ class MangaScreenModel( private val isFromSource: Boolean, val smartSearched: Boolean, // SY <-- - private val downloadPreferences: DownloadPreferences = Injekt.get(), private val libraryPreferences: LibraryPreferences = Injekt.get(), readerPreferences: ReaderPreferences = Injekt.get(), private val uiPreferences: UiPreferences = Injekt.get(), @@ -215,6 +213,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) { @@ -1559,17 +1558,14 @@ 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) || - // EXH --> - manga.isEhBasedManga() - // EXH <-- - ) { - return@launchNonCancellable + // EXH --> + if (manga.isEhBasedManga()) return@launchNonCancellable + // EXH <-- + val chaptersToDownload = filterChaptersForDownload.await(manga, chapters) + + if (chaptersToDownload.isNotEmpty()) { + 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 4071a55aa5..8372f294a1 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 0000000000..515bdbde3e --- /dev/null +++ b/domain/src/main/java/mihon/domain/chapter/interactor/FilterChaptersForDownload.kt @@ -0,0 +1,77 @@ +package mihon.domain.chapter.interactor + +import tachiyomi.domain.category.interactor.GetCategories +import tachiyomi.domain.chapter.interactor.GetChaptersByMangaId +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 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 + + val readChapterNumbers = getChaptersByMangaId.await(manga.id) + .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 96af591e53..dbb7d7d535 100644 --- a/domain/src/main/java/tachiyomi/domain/download/service/DownloadPreferences.kt +++ b/domain/src/main/java/tachiyomi/domain/download/service/DownloadPreferences.kt @@ -43,6 +43,8 @@ class DownloadPreferences( emptySet(), ) + fun downloadNewUnreadChaptersOnly() = preferenceStore.getBoolean("download_new_unread_chapters_only", false) + // KMK --> fun downloadCacheRenewInterval() = preferenceStore.getInt("download_cache_renew_interval", 1) // KMK <-- diff --git a/i18n/src/commonMain/moko-resources/base/strings.xml b/i18n/src/commonMain/moko-resources/base/strings.xml index 5ad03bbd1f..7ef4a036b9 100644 --- a/i18n/src/commonMain/moko-resources/base/strings.xml +++ b/i18n/src/commonMain/moko-resources/base/strings.xml @@ -486,6 +486,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