diff --git a/CHANGELOG.md b/CHANGELOG.md index b1643a230..7affa28c9 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 4c769f703..08787e1f8 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 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 3749c71fb..0982f4157 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 @@ -24,7 +24,6 @@ import eu.kanade.tachiyomi.data.download.DownloadManager import eu.kanade.tachiyomi.data.notification.Notifications import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.model.UpdateStrategy -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 @@ -39,15 +38,14 @@ import kotlinx.coroutines.ensureActive 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.category.interactor.GetCategories import tachiyomi.domain.category.model.Category import tachiyomi.domain.chapter.model.Chapter import tachiyomi.domain.chapter.model.NoChaptersException -import tachiyomi.domain.download.service.DownloadPreferences import tachiyomi.domain.library.model.LibraryManga import tachiyomi.domain.library.service.LibraryPreferences import tachiyomi.domain.library.service.LibraryPreferences.Companion.DEVICE_CHARGING @@ -78,16 +76,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() private val notifier = LibraryUpdateNotifier(context) @@ -270,9 +267,10 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet .sortedByDescending { it.sourceOrder } 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/ui/manga/MangaScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaScreenModel.kt index 99a1b1612..248342e59 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 @@ -34,7 +34,6 @@ import eu.kanade.tachiyomi.source.Source 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 kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.async @@ -48,6 +47,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 @@ -67,7 +67,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.GetDuplicateLibraryManga import tachiyomi.domain.manga.interactor.GetMangaWithChapters @@ -87,7 +86,6 @@ class MangaScreenModel( val context: Context, val mangaId: Long, private val isFromSource: Boolean, - private val downloadPreferences: DownloadPreferences = Injekt.get(), private val libraryPreferences: LibraryPreferences = Injekt.get(), readerPreferences: ReaderPreferences = Injekt.get(), private val trackerManager: TrackerManager = Injekt.get(), @@ -109,6 +107,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) { @@ -771,15 +770,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) - ) { - return@launchNonCancellable - } + val chaptersToDownload = filterChaptersForDownload.await(manga, chapters) - downloadChapters(chapters) + if (chaptersToDownload.isNotEmpty()) { + downloadChapters(chaptersToDownload) + } } } 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..515bdbde3 --- /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 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 bdd75f70b..37b9d7427 100644 --- 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