feat(download): auto-download chapters during library update

Add configurable library-update download modes (disabled, next X unread, all)
with settings UI and enqueue downloads after chapter sync completes.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
littlecodedragon 2026-07-10 10:12:10 +02:00
parent 4fce1fa940
commit a81d717ca3
8 changed files with 106 additions and 3 deletions

View file

@ -27,6 +27,7 @@ import eu.kanade.domain.track.interactor.SyncChapterProgressWithTrack
import eu.kanade.domain.track.interactor.TrackChapter import eu.kanade.domain.track.interactor.TrackChapter
import mihon.data.repository.ExtensionRepoRepositoryImpl import mihon.data.repository.ExtensionRepoRepositoryImpl
import mihon.domain.chapter.interactor.FilterChaptersForDownload import mihon.domain.chapter.interactor.FilterChaptersForDownload
import mihon.domain.chapter.interactor.GetChaptersForLibraryUpdateDownload
import mihon.domain.extensionrepo.interactor.CreateExtensionRepo import mihon.domain.extensionrepo.interactor.CreateExtensionRepo
import mihon.domain.extensionrepo.interactor.DeleteExtensionRepo import mihon.domain.extensionrepo.interactor.DeleteExtensionRepo
import mihon.domain.extensionrepo.interactor.GetExtensionRepo import mihon.domain.extensionrepo.interactor.GetExtensionRepo
@ -171,6 +172,7 @@ class DomainModule : InjektModule {
addFactory { SyncChaptersWithSource(get(), get(), get(), get(), get(), get(), get(), get(), get()) } addFactory { SyncChaptersWithSource(get(), get(), get(), get(), get(), get(), get(), get(), get()) }
addFactory { GetAvailableScanlators(get()) } addFactory { GetAvailableScanlators(get()) }
addFactory { FilterChaptersForDownload(get(), get(), get(), get()) } addFactory { FilterChaptersForDownload(get(), get(), get(), get()) }
addFactory { GetChaptersForLibraryUpdateDownload(get(), get()) }
addSingletonFactory<HistoryRepository> { HistoryRepositoryImpl(get()) } addSingletonFactory<HistoryRepository> { HistoryRepositoryImpl(get()) }
addFactory { GetHistory(get()) } addFactory { GetHistory(get()) }

View file

@ -18,6 +18,7 @@ import kotlinx.collections.immutable.toImmutableMap
import tachiyomi.domain.category.interactor.GetCategories import tachiyomi.domain.category.interactor.GetCategories
import tachiyomi.domain.category.model.Category import tachiyomi.domain.category.model.Category
import tachiyomi.domain.download.service.DownloadPreferences import tachiyomi.domain.download.service.DownloadPreferences
import tachiyomi.domain.download.service.LibraryUpdateDownloadMode
import tachiyomi.i18n.MR import tachiyomi.i18n.MR
import tachiyomi.i18n.kmk.KMR import tachiyomi.i18n.kmk.KMR
import tachiyomi.presentation.core.i18n.pluralStringResource import tachiyomi.presentation.core.i18n.pluralStringResource
@ -77,6 +78,7 @@ object SettingsDownloadScreen : SearchableSettings {
allCategories = allCategories, allCategories = allCategories,
), ),
getDownloadAheadGroup(downloadPreferences = downloadPreferences), getDownloadAheadGroup(downloadPreferences = downloadPreferences),
getLibraryUpdateDownloadGroup(downloadPreferences = downloadPreferences),
// KMK --> // KMK -->
getDownloadCacheRenewInterval(downloadPreferences = downloadPreferences), getDownloadCacheRenewInterval(downloadPreferences = downloadPreferences),
// KMK <-- // KMK <--
@ -216,6 +218,39 @@ object SettingsDownloadScreen : SearchableSettings {
) )
} }
@Composable
private fun getLibraryUpdateDownloadGroup(
downloadPreferences: DownloadPreferences,
): Preference.PreferenceGroup {
val modePref = downloadPreferences.libraryUpdateDownloadMode()
val amountPref = downloadPreferences.libraryUpdateDownloadAmount()
val mode by modePref.collectAsState()
return Preference.PreferenceGroup(
title = stringResource(KMR.strings.library_update_download),
preferenceItems = persistentListOf(
Preference.PreferenceItem.ListPreference(
preference = modePref,
entries = persistentMapOf(
LibraryUpdateDownloadMode.DISABLED to stringResource(MR.strings.disabled),
LibraryUpdateDownloadMode.NEXT_X to stringResource(KMR.strings.library_update_download_mode_next_x),
LibraryUpdateDownloadMode.ALL to stringResource(KMR.strings.library_update_download_mode_all),
),
title = stringResource(KMR.strings.library_update_download_mode),
),
Preference.PreferenceItem.ListPreference(
preference = amountPref,
entries = listOf(2, 3, 5, 10)
.associateWith { pluralStringResource(MR.plurals.next_unread_chapters, count = it, it) }
.toImmutableMap(),
title = stringResource(KMR.strings.library_update_download_amount),
enabled = mode == LibraryUpdateDownloadMode.NEXT_X,
),
Preference.PreferenceItem.InfoPreference(stringResource(KMR.strings.library_update_download_info)),
),
)
}
// KMK --> // KMK -->
@Composable @Composable
private fun getDownloadCacheRenewInterval( private fun getDownloadCacheRenewInterval(

View file

@ -58,6 +58,7 @@ import kotlinx.coroutines.sync.Semaphore
import kotlinx.coroutines.sync.withPermit import kotlinx.coroutines.sync.withPermit
import logcat.LogPriority import logcat.LogPriority
import mihon.domain.chapter.interactor.FilterChaptersForDownload import mihon.domain.chapter.interactor.FilterChaptersForDownload
import mihon.domain.chapter.interactor.GetChaptersForLibraryUpdateDownload
import tachiyomi.core.common.i18n.stringResource import tachiyomi.core.common.i18n.stringResource
import tachiyomi.core.common.preference.getAndSet import tachiyomi.core.common.preference.getAndSet
import tachiyomi.core.common.util.lang.withIOContext import tachiyomi.core.common.util.lang.withIOContext
@ -119,6 +120,7 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
private val syncChaptersWithSource: SyncChaptersWithSource = Injekt.get() private val syncChaptersWithSource: SyncChaptersWithSource = Injekt.get()
private val fetchInterval: FetchInterval = Injekt.get() private val fetchInterval: FetchInterval = Injekt.get()
private val filterChaptersForDownload: FilterChaptersForDownload = Injekt.get() private val filterChaptersForDownload: FilterChaptersForDownload = Injekt.get()
private val getChaptersForLibraryUpdateDownload: GetChaptersForLibraryUpdateDownload = Injekt.get()
// SY --> // SY -->
private val getFavorites: GetFavorites = Injekt.get() private val getFavorites: GetFavorites = Injekt.get()
@ -465,6 +467,12 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
// Convert to the manga that contains new chapters // Convert to the manga that contains new chapters
newUpdates.add(manga to newChapters.toTypedArray()) newUpdates.add(manga to newChapters.toTypedArray())
} }
val libraryUpdateChapters = getChaptersForLibraryUpdateDownload.await(manga)
if (libraryUpdateChapters.isNotEmpty()) {
downloadChapters(manga, libraryUpdateChapters)
hasDownloads.store(true)
}
clearErrorFromDB(mangaId = manga.id) clearErrorFromDB(mangaId = manga.id)
} catch (e: Throwable) { } catch (e: Throwable) {
val errorMessage = when (e) { val errorMessage = when (e) {
@ -493,9 +501,10 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
if (newUpdates.isNotEmpty()) { if (newUpdates.isNotEmpty()) {
notifier.showUpdateNotifications(newUpdates) notifier.showUpdateNotifications(newUpdates)
if (hasDownloads.load()) { }
downloadManager.startDownloads()
} if (hasDownloads.load()) {
downloadManager.startDownloads()
} }
if (failedUpdates.isNotEmpty()) { if (failedUpdates.isNotEmpty()) {

View file

@ -0,0 +1,31 @@
package mihon.domain.chapter.interactor
import tachiyomi.domain.chapter.model.Chapter
import tachiyomi.domain.download.service.DownloadPreferences
import tachiyomi.domain.download.service.LibraryUpdateDownloadMode
import tachiyomi.domain.history.interactor.GetNextChapters
import tachiyomi.domain.manga.model.Manga
class GetChaptersForLibraryUpdateDownload(
private val downloadPreferences: DownloadPreferences,
private val getNextChapters: GetNextChapters,
) {
suspend fun await(manga: Manga): List<Chapter> {
if (!manga.favorite) return emptyList()
val mode = downloadPreferences.libraryUpdateDownloadMode().get()
if (mode == LibraryUpdateDownloadMode.DISABLED) return emptyList()
val chapters = getNextChapters.await(manga.id)
if (chapters.isEmpty()) return emptyList()
return when (mode) {
LibraryUpdateDownloadMode.NEXT_X -> {
chapters.take(downloadPreferences.libraryUpdateDownloadAmount().get())
}
LibraryUpdateDownloadMode.ALL -> chapters
else -> emptyList()
}
}
}

View file

@ -47,6 +47,13 @@ class DownloadPreferences(
// KMK --> // KMK -->
fun downloadCacheRenewInterval() = preferenceStore.getInt("download_cache_renew_interval", 1) fun downloadCacheRenewInterval() = preferenceStore.getInt("download_cache_renew_interval", 1)
fun libraryUpdateDownloadMode() = preferenceStore.getInt(
"library_update_download_mode",
LibraryUpdateDownloadMode.DISABLED,
)
fun libraryUpdateDownloadAmount() = preferenceStore.getInt("library_update_download_amount", 10)
// KMK <-- // KMK <--
companion object { companion object {

View file

@ -0,0 +1,7 @@
package tachiyomi.domain.download.service
object LibraryUpdateDownloadMode {
const val DISABLED = 0
const val NEXT_X = 1
const val ALL = 2
}

View file

@ -113,6 +113,12 @@
<string name="download_cache_renew_interval_12hour">12 hours</string> <string name="download_cache_renew_interval_12hour">12 hours</string>
<string name="download_cache_renew_interval_24hour">1 day</string> <string name="download_cache_renew_interval_24hour">1 day</string>
<string name="download_cache_renew_interval_info">If set to "manual", either refresh each manga or using the "Reindex downloads" action in "Advanced"</string> <string name="download_cache_renew_interval_info">If set to "manual", either refresh each manga or using the "Reindex downloads" action in "Advanced"</string>
<string name="library_update_download">Download on library update</string>
<string name="library_update_download_mode">Download chapters on library update</string>
<string name="library_update_download_mode_next_x">Next unread chapters from current position</string>
<string name="library_update_download_mode_all">All unread chapters from current position</string>
<string name="library_update_download_amount">Number of chapters to download</string>
<string name="library_update_download_info">Downloads unread chapters from your current reading position when the library is updated. Already downloaded chapters are skipped.</string>
<!-- Tracking section --> <!-- Tracking section -->
<string name="pref_auto_sync_progress_from_trackers">Auto sync progress from Trackers</string> <string name="pref_auto_sync_progress_from_trackers">Auto sync progress from Trackers</string>
<string name="sync_progress_from_trackers_up_to_chapter">Sync progress from Trackers up to chapter %d</string> <string name="sync_progress_from_trackers_up_to_chapter">Sync progress from Trackers up to chapter %d</string>

View file

@ -12,6 +12,12 @@
<string name="download_cache_renew_interval_12hour">12 Stunden</string> <string name="download_cache_renew_interval_12hour">12 Stunden</string>
<string name="download_cache_renew_interval_24hour">1 Tag</string> <string name="download_cache_renew_interval_24hour">1 Tag</string>
<string name="download_cache_renew_interval_info">Wenn auf \"Manuell\" gesetzt, aktualisieren Sie entweder jedes Manga oder verwenden Sie die Aktion \"Downloads neu indizieren\" in \"Erweitert\"</string> <string name="download_cache_renew_interval_info">Wenn auf \"Manuell\" gesetzt, aktualisieren Sie entweder jedes Manga oder verwenden Sie die Aktion \"Downloads neu indizieren\" in \"Erweitert\"</string>
<string name="library_update_download">Bei Bibliotheksaktualisierung herunterladen</string>
<string name="library_update_download_mode">Kapitel bei Bibliotheksaktualisierung herunterladen</string>
<string name="library_update_download_mode_next_x">Nächste ungelesene Kapitel ab aktueller Leseposition</string>
<string name="library_update_download_mode_all">Alle ungelesenen Kapitel ab aktueller Leseposition</string>
<string name="library_update_download_amount">Anzahl der herunterzuladenden Kapitel</string>
<string name="library_update_download_info">Lädt ungelesene Kapitel ab deiner aktuellen Leseposition herunter, wenn die Bibliothek aktualisiert wird. Bereits heruntergeladene Kapitel werden übersprungen.</string>
<string name="pref_clean_invalid_downloads">Ungültige Downloads bereinigen</string> <string name="pref_clean_invalid_downloads">Ungültige Downloads bereinigen</string>
<string name="pref_clean_invalid_downloads_summary">Finden und entfernen Sie alle Downloads, Dateien, Ordner die nicht in Ihrer Bibliothek gespeichert sind</string> <string name="pref_clean_invalid_downloads_summary">Finden und entfernen Sie alle Downloads, Dateien, Ordner die nicht in Ihrer Bibliothek gespeichert sind</string>
<string name="invalid_downloads_cleaned">Ungültige Downloads bereinigt</string> <string name="invalid_downloads_cleaned">Ungültige Downloads bereinigt</string>