Feature/remove download ahead logic (#867)

* Remove download ahead logic

Unnecessary on server side, should just be done by the client

* Rename "autoDownloadAheadLimit" to "autoDownloadNewChaptersLimit"

* Deprecate the old field

* Update Stable WebUI

* Update Stable WebUI

---------

Co-authored-by: Syer10 <syer10@users.noreply.github.com>
This commit is contained in:
schroda
2024-02-17 17:23:13 +01:00
committed by GitHub
parent 9edbc7f1d7
commit 6fbd2f1079
9 changed files with 28 additions and 132 deletions

View File

@@ -12,7 +12,7 @@ const val MainClass = "suwayomi.tachidesk.MainKt"
// should be bumped with each stable release // should be bumped with each stable release
val tachideskVersion = System.getenv("ProductVersion") ?: "v0.7.0" val tachideskVersion = System.getenv("ProductVersion") ?: "v0.7.0"
val webUIRevisionTag = System.getenv("WebUIRevision") ?: "r1335" val webUIRevisionTag = System.getenv("WebUIRevision") ?: "r1397"
// counts commits on the current checked out branch // counts commits on the current checked out branch
val getTachideskRevision = { val getTachideskRevision = {

View File

@@ -7,7 +7,6 @@ import org.jetbrains.exposed.sql.transactions.transaction
import suwayomi.tachidesk.graphql.types.ChapterType import suwayomi.tachidesk.graphql.types.ChapterType
import suwayomi.tachidesk.graphql.types.DownloadStatus import suwayomi.tachidesk.graphql.types.DownloadStatus
import suwayomi.tachidesk.manga.impl.Chapter import suwayomi.tachidesk.manga.impl.Chapter
import suwayomi.tachidesk.manga.impl.Manga
import suwayomi.tachidesk.manga.impl.download.DownloadManager import suwayomi.tachidesk.manga.impl.download.DownloadManager
import suwayomi.tachidesk.manga.impl.download.model.Status import suwayomi.tachidesk.manga.impl.download.model.Status
import suwayomi.tachidesk.manga.model.table.ChapterTable import suwayomi.tachidesk.manga.model.table.ChapterTable
@@ -269,20 +268,4 @@ class DownloadMutation {
) )
} }
} }
data class DownloadAheadInput(
val clientMutationId: String? = null,
val mangaIds: List<Int> = emptyList(),
val latestReadChapterIds: List<Int>? = null,
)
data class DownloadAheadPayload(val clientMutationId: String?)
fun downloadAhead(input: DownloadAheadInput): DownloadAheadPayload {
val (clientMutationId, mangaIds, latestReadChapterIds) = input
Manga.downloadAhead(mangaIds, latestReadChapterIds ?: emptyList())
return DownloadAheadPayload(clientMutationId)
}
} }

View File

@@ -53,7 +53,8 @@ class SettingsMutation {
updateSetting(settings.downloadsPath, serverConfig.downloadsPath) updateSetting(settings.downloadsPath, serverConfig.downloadsPath)
updateSetting(settings.autoDownloadNewChapters, serverConfig.autoDownloadNewChapters) updateSetting(settings.autoDownloadNewChapters, serverConfig.autoDownloadNewChapters)
updateSetting(settings.excludeEntryWithUnreadChapters, serverConfig.excludeEntryWithUnreadChapters) updateSetting(settings.excludeEntryWithUnreadChapters, serverConfig.excludeEntryWithUnreadChapters)
updateSetting(settings.autoDownloadAheadLimit, serverConfig.autoDownloadAheadLimit) updateSetting(settings.autoDownloadAheadLimit, serverConfig.autoDownloadNewChaptersLimit) // deprecated
updateSetting(settings.autoDownloadNewChaptersLimit, serverConfig.autoDownloadNewChaptersLimit)
// extension // extension
updateSetting(settings.extensionRepos, serverConfig.extensionRepos) updateSetting(settings.extensionRepos, serverConfig.extensionRepos)

View File

@@ -7,6 +7,7 @@
package suwayomi.tachidesk.graphql.types package suwayomi.tachidesk.graphql.types
import com.expediagroup.graphql.generator.annotations.GraphQLDeprecated
import suwayomi.tachidesk.graphql.server.primitives.Node import suwayomi.tachidesk.graphql.server.primitives.Node
import suwayomi.tachidesk.server.ServerConfig import suwayomi.tachidesk.server.ServerConfig
import suwayomi.tachidesk.server.serverConfig import suwayomi.tachidesk.server.serverConfig
@@ -38,7 +39,13 @@ interface Settings : Node {
val downloadsPath: String? val downloadsPath: String?
val autoDownloadNewChapters: Boolean? val autoDownloadNewChapters: Boolean?
val excludeEntryWithUnreadChapters: Boolean? val excludeEntryWithUnreadChapters: Boolean?
@GraphQLDeprecated(
"Replaced with autoDownloadNewChaptersLimit",
replaceWith = ReplaceWith("autoDownloadNewChaptersLimit"),
)
val autoDownloadAheadLimit: Int? val autoDownloadAheadLimit: Int?
val autoDownloadNewChaptersLimit: Int?
// extension // extension
val extensionRepos: List<String>? val extensionRepos: List<String>?
@@ -99,7 +106,12 @@ data class PartialSettingsType(
override val downloadsPath: String?, override val downloadsPath: String?,
override val autoDownloadNewChapters: Boolean?, override val autoDownloadNewChapters: Boolean?,
override val excludeEntryWithUnreadChapters: Boolean?, override val excludeEntryWithUnreadChapters: Boolean?,
@GraphQLDeprecated(
"Replaced with autoDownloadNewChaptersLimit",
replaceWith = ReplaceWith("autoDownloadNewChaptersLimit"),
)
override val autoDownloadAheadLimit: Int?, override val autoDownloadAheadLimit: Int?,
override val autoDownloadNewChaptersLimit: Int?,
// extension // extension
override val extensionRepos: List<String>?, override val extensionRepos: List<String>?,
// requests // requests
@@ -152,7 +164,12 @@ class SettingsType(
override val downloadsPath: String, override val downloadsPath: String,
override val autoDownloadNewChapters: Boolean, override val autoDownloadNewChapters: Boolean,
override val excludeEntryWithUnreadChapters: Boolean, override val excludeEntryWithUnreadChapters: Boolean,
@GraphQLDeprecated(
"Replaced with autoDownloadNewChaptersLimit",
replaceWith = ReplaceWith("autoDownloadNewChaptersLimit"),
)
override val autoDownloadAheadLimit: Int, override val autoDownloadAheadLimit: Int,
override val autoDownloadNewChaptersLimit: Int,
// extension // extension
override val extensionRepos: List<String>, override val extensionRepos: List<String>,
// requests // requests
@@ -204,7 +221,8 @@ class SettingsType(
config.downloadsPath.value, config.downloadsPath.value,
config.autoDownloadNewChapters.value, config.autoDownloadNewChapters.value,
config.excludeEntryWithUnreadChapters.value, config.excludeEntryWithUnreadChapters.value,
config.autoDownloadAheadLimit.value, config.autoDownloadNewChaptersLimit.value, // deprecated
config.autoDownloadNewChaptersLimit.value,
// extension // extension
config.extensionRepos.value, config.extensionRepos.value,
// requests // requests

View File

@@ -310,7 +310,7 @@ object Chapter {
"mangaId= $mangaId, " + "mangaId= $mangaId, " +
"prevNumberOfChapters= $prevNumberOfChapters, " + "prevNumberOfChapters= $prevNumberOfChapters, " +
"updatedChapterList= ${updatedChapterList.size}, " + "updatedChapterList= ${updatedChapterList.size}, " +
"downloadAheadLimit= ${serverConfig.autoDownloadAheadLimit.value}" + "autoDownloadNewChaptersLimit= ${serverConfig.autoDownloadNewChaptersLimit.value}" +
")", ")",
) )
@@ -389,8 +389,8 @@ object Chapter {
} }
val firstChapterToDownloadIndex = val firstChapterToDownloadIndex =
if (serverConfig.autoDownloadAheadLimit.value > 0) { if (serverConfig.autoDownloadNewChaptersLimit.value > 0) {
(numberOfNewChapters - serverConfig.autoDownloadAheadLimit.value).coerceAtLeast(0) (numberOfNewChapters - serverConfig.autoDownloadNewChaptersLimit.value).coerceAtLeast(0)
} else { } else {
0 0
} }

View File

@@ -27,8 +27,6 @@ import org.kodein.di.conf.global
import org.kodein.di.instance import org.kodein.di.instance
import suwayomi.tachidesk.manga.impl.MangaList.proxyThumbnailUrl import suwayomi.tachidesk.manga.impl.MangaList.proxyThumbnailUrl
import suwayomi.tachidesk.manga.impl.Source.getSource import suwayomi.tachidesk.manga.impl.Source.getSource
import suwayomi.tachidesk.manga.impl.download.DownloadManager
import suwayomi.tachidesk.manga.impl.download.DownloadManager.EnqueueInput
import suwayomi.tachidesk.manga.impl.download.fileProvider.impl.MissingThumbnailException import suwayomi.tachidesk.manga.impl.download.fileProvider.impl.MissingThumbnailException
import suwayomi.tachidesk.manga.impl.track.Track import suwayomi.tachidesk.manga.impl.track.Track
import suwayomi.tachidesk.manga.impl.util.network.await import suwayomi.tachidesk.manga.impl.util.network.await
@@ -47,15 +45,11 @@ import suwayomi.tachidesk.manga.model.table.MangaStatus
import suwayomi.tachidesk.manga.model.table.MangaTable import suwayomi.tachidesk.manga.model.table.MangaTable
import suwayomi.tachidesk.manga.model.table.toDataClass import suwayomi.tachidesk.manga.model.table.toDataClass
import suwayomi.tachidesk.server.ApplicationDirs import suwayomi.tachidesk.server.ApplicationDirs
import suwayomi.tachidesk.server.serverConfig
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
import java.io.File import java.io.File
import java.io.IOException import java.io.IOException
import java.io.InputStream import java.io.InputStream
import java.time.Instant import java.time.Instant
import java.util.Timer
import java.util.TimerTask
import java.util.concurrent.ConcurrentHashMap
private val logger = KotlinLogging.logger { } private val logger = KotlinLogging.logger { }
@@ -339,104 +333,4 @@ object Manga {
clearCachedImage(applicationDirs.tempThumbnailCacheRoot, fileName) clearCachedImage(applicationDirs.tempThumbnailCacheRoot, fileName)
clearCachedImage(applicationDirs.thumbnailDownloadsRoot, fileName) clearCachedImage(applicationDirs.thumbnailDownloadsRoot, fileName)
} }
private val downloadAheadQueue = ConcurrentHashMap<String, ConcurrentHashMap.KeySetView<Int, Boolean>>()
private var downloadAheadTimer: Timer? = null
private const val MANGAS_KEY = "mangaIds"
private const val CHAPTERS_KEY = "chapterIds"
fun downloadAhead(
mangaIds: List<Int>,
latestReadChapterIds: List<Int> = emptyList(),
) {
if (serverConfig.autoDownloadAheadLimit.value == 0) {
return
}
val updateDownloadAheadQueue = { key: String, ids: List<Int> ->
val idSet = downloadAheadQueue[key] ?: ConcurrentHashMap.newKeySet()
idSet.addAll(ids)
downloadAheadQueue[key] = idSet
}
updateDownloadAheadQueue(MANGAS_KEY, mangaIds)
updateDownloadAheadQueue(CHAPTERS_KEY, latestReadChapterIds)
// handle cases where this function gets called multiple times in quick succession.
// this could happen in case e.g. multiple chapters get marked as read without batching the operation
downloadAheadTimer?.cancel()
downloadAheadTimer =
Timer().apply {
schedule(
object : TimerTask() {
override fun run() {
downloadAheadChapters(
downloadAheadQueue[MANGAS_KEY]?.toList().orEmpty(),
downloadAheadQueue[CHAPTERS_KEY]?.toList().orEmpty(),
)
downloadAheadQueue.clear()
}
},
5000,
)
}
}
/**
* Downloads the latest unread and not downloaded chapters for each passed manga id.
*
* To pass a specific chapter as the latest read chapter for a manga, it can be provided in the "latestReadChapterIds" list.
* This makes it possible to handle cases, where the actual latest read chapter isn't marked as read yet.
* E.g. the client marks a chapter as read and at the same time sends the "downloadAhead" mutation.
* In this case, the latest read chapter could potentially be the one, that just got send to get marked as read by the client.
* Without providing it in "latestReadChapterIds" it could be incorrectly included in the chapters, that will get downloaded.
*
* The latest read chapter will be considered the starting point.
* E.g.:
* - 20 chapters
* - chapter 15 marked as read
* - 16 - 20 marked as unread
* - 10 - 14 marked as unread
*
* will download the unread chapters starting from chapter 15
*/
private fun downloadAheadChapters(
mangaIds: List<Int>,
latestReadChapterIds: List<Int>,
) {
val mangaToLatestReadChapterIndex =
transaction {
ChapterTable.select { (ChapterTable.manga inList mangaIds) and (ChapterTable.isRead eq true) }
.orderBy(ChapterTable.sourceOrder to SortOrder.DESC).groupBy { it[ChapterTable.manga].value }
}.mapValues { (_, chapters) -> chapters.firstOrNull()?.let { it[ChapterTable.sourceOrder] } ?: 0 }
val mangaToUnreadChaptersMap =
transaction {
ChapterTable.select { (ChapterTable.manga inList mangaIds) and (ChapterTable.isRead eq false) }
.orderBy(ChapterTable.sourceOrder to SortOrder.DESC)
.groupBy { it[ChapterTable.manga].value }
}
val chapterIdsToDownload =
mangaToUnreadChaptersMap.map { (mangaId, unreadChapters) ->
val latestReadChapterIndex = mangaToLatestReadChapterIndex[mangaId] ?: 0
val lastChapterToDownloadIndex =
unreadChapters.indexOfLast {
it[ChapterTable.sourceOrder] > latestReadChapterIndex &&
it[ChapterTable.id].value !in latestReadChapterIds
}
val unreadChaptersToConsider = unreadChapters.subList(0, lastChapterToDownloadIndex + 1)
val firstChapterToDownloadIndex =
(unreadChaptersToConsider.size - serverConfig.autoDownloadAheadLimit.value).coerceAtLeast(0)
unreadChaptersToConsider.subList(firstChapterToDownloadIndex, lastChapterToDownloadIndex + 1)
.filter { !it[ChapterTable.isDownloaded] }
.map { it[ChapterTable.id].value }
}.flatten()
logger.info { "downloadAheadChapters: download chapters [${chapterIdsToDownload.joinToString(", ")}]" }
DownloadManager.dequeue(mangaIds, chapterIdsToDownload)
DownloadManager.enqueue(EnqueueInput(chapterIdsToDownload))
}
} }

View File

@@ -97,7 +97,7 @@ class ServerConfig(getConfig: () -> Config, val moduleName: String = SERVER_CONF
val downloadsPath: MutableStateFlow<String> by OverrideConfigValue(StringConfigAdapter) val downloadsPath: MutableStateFlow<String> by OverrideConfigValue(StringConfigAdapter)
val autoDownloadNewChapters: MutableStateFlow<Boolean> by OverrideConfigValue(BooleanConfigAdapter) val autoDownloadNewChapters: MutableStateFlow<Boolean> by OverrideConfigValue(BooleanConfigAdapter)
val excludeEntryWithUnreadChapters: MutableStateFlow<Boolean> by OverrideConfigValue(BooleanConfigAdapter) val excludeEntryWithUnreadChapters: MutableStateFlow<Boolean> by OverrideConfigValue(BooleanConfigAdapter)
val autoDownloadAheadLimit: MutableStateFlow<Int> by OverrideConfigValue(IntConfigAdapter) val autoDownloadNewChaptersLimit: MutableStateFlow<Int> by OverrideConfigValue(IntConfigAdapter)
// extensions // extensions
val extensionRepos: MutableStateFlow<List<String>> by OverrideConfigValues(StringConfigAdapter) val extensionRepos: MutableStateFlow<List<String>> by OverrideConfigValues(StringConfigAdapter)

View File

@@ -21,7 +21,7 @@ server.downloadAsCbz = false
server.downloadsPath = "" server.downloadsPath = ""
server.autoDownloadNewChapters = false # if new chapters that have been retrieved should get automatically downloaded server.autoDownloadNewChapters = false # if new chapters that have been retrieved should get automatically downloaded
server.excludeEntryWithUnreadChapters = true # ignore automatic chapter downloads of entries with unread chapters server.excludeEntryWithUnreadChapters = true # ignore automatic chapter downloads of entries with unread chapters
server.autoDownloadAheadLimit = 0 # 0 to disable it - how many unread downloaded chapters should be available - if the limit is reached, new chapters won't be downloaded automatically. this limit will also be applied to the auto download of new chapters on an update server.autoDownloadNewChaptersLimit = 0 # 0 to disable it - how many unread downloaded chapters should be available - if the limit is reached, new chapters won't be downloaded automatically. this limit will also be applied to the auto download of new chapters on an update
# extension repos # extension repos
server.extensionRepos = [ server.extensionRepos = [

View File

@@ -11,7 +11,7 @@ server.socksProxyPort = ""
server.downloadAsCbz = false server.downloadAsCbz = false
server.autoDownloadNewChapters = false server.autoDownloadNewChapters = false
server.excludeEntryWithUnreadChapters = true server.excludeEntryWithUnreadChapters = true
server.autoDownloadAheadLimit = 0 server.autoDownloadNewChaptersLimit = 0
# requests # requests
server.maxSourcesInParallel = 10 server.maxSourcesInParallel = 10