Skip to content

Commit

Permalink
Add option to skip downloading duplicate read chapters (mihonapp/miho…
Browse files Browse the repository at this point in the history
…n#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 ca968f1)
  • Loading branch information
shabnix authored and cuong-tran committed Aug 24, 2024
1 parent eaceb3e commit cb431b4
Show file tree
Hide file tree
Showing 10 changed files with 119 additions and 61 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
2 changes: 2 additions & 0 deletions app/src/main/java/eu/kanade/domain/DomainModule.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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<HistoryRepository> { HistoryRepositoryImpl(get()) }
addFactory { GetHistory(get()) }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -124,6 +125,7 @@ object SettingsDownloadScreen : SearchableSettings {
allCategories: List<Category>,
): Preference.PreferenceGroup {
val downloadNewChaptersPref = downloadPreferences.downloadNewChapters()
val downloadNewUnreadChaptersOnlyPref = downloadPreferences.downloadNewUnreadChaptersOnly()
val downloadNewChapterCategoriesPref = downloadPreferences.downloadNewChapterCategories()
val downloadNewChapterCategoriesExcludePref = downloadPreferences.downloadNewChapterCategoriesExclude()

Expand Down Expand Up @@ -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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,19 +11,17 @@ 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
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
Expand All @@ -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

Expand Down Expand Up @@ -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 {
Expand All @@ -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,
Expand Down
22 changes: 9 additions & 13 deletions app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaScreenModel.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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(),
Expand Down Expand Up @@ -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<MangaScreenModel.State>(State.Loading) {

Expand Down Expand Up @@ -1559,17 +1558,14 @@ class MangaScreenModel(
private fun downloadNewChapters(chapters: List<Chapter>) {
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)
}
}

Expand Down
26 changes: 0 additions & 26 deletions app/src/main/java/eu/kanade/tachiyomi/util/MangaExtensions.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -50,31 +49,6 @@ fun Manga.removeCovers(coverCache: CoverCache = Injekt.get()): Manga {
}
}

fun Manga.shouldDownloadNewChapters(dbCategories: List<Long>, 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,
Expand Down
Original file line number Diff line number Diff line change
@@ -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<Chapter>): List<Chapter> {
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
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 <--
Expand Down
1 change: 1 addition & 0 deletions i18n/src/commonMain/moko-resources/base/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -486,6 +486,7 @@
<string name="fifth_to_last">Fifth to last read chapter</string>
<string name="pref_category_auto_download">Auto-download</string>
<string name="pref_download_new">Download new chapters</string>
<string name="pref_download_new_unread_chapters_only">Skip downloading duplicate read chapters</string>
<string name="pref_download_new_categories_details">Entries in excluded categories will not be downloaded even if they are also in included categories.</string>
<string name="download_ahead">Download ahead</string>
<string name="auto_download_while_reading">Auto download while reading</string>
Expand Down

0 comments on commit cb431b4

Please sign in to comment.