Feature/backup suwayomi data (#1430)

* Export meta data

* Import meta data

* Add missing "opdsUseBinaryFileSize" setting to gql

* Export server settings

* Import server settings

* Streamline server config enum handling

* Use "restore amount" in backup import progress
This commit is contained in:
schroda
2025-06-15 23:14:13 +02:00
committed by GitHub
parent 483e3a760f
commit 4086a73727
29 changed files with 662 additions and 155 deletions

View File

@@ -1,9 +1,10 @@
package suwayomi.tachidesk.global.impl package suwayomi.tachidesk.global.impl
import org.jetbrains.exposed.sql.insert import org.jetbrains.exposed.dao.id.EntityID
import org.jetbrains.exposed.sql.batchInsert
import org.jetbrains.exposed.sql.selectAll import org.jetbrains.exposed.sql.selectAll
import org.jetbrains.exposed.sql.statements.BatchUpdateStatement
import org.jetbrains.exposed.sql.transactions.transaction import org.jetbrains.exposed.sql.transactions.transaction
import org.jetbrains.exposed.sql.update
import suwayomi.tachidesk.global.model.table.GlobalMetaTable import suwayomi.tachidesk.global.model.table.GlobalMetaTable
/* /*
@@ -18,20 +19,32 @@ object GlobalMeta {
key: String, key: String,
value: String, value: String,
) { ) {
transaction { modifyMetas(mapOf(key to value))
val meta = }
transaction {
GlobalMetaTable.selectAll().where { GlobalMetaTable.key eq key }
}.firstOrNull()
if (meta == null) { fun modifyMetas(meta: Map<String, String>) {
GlobalMetaTable.insert { transaction {
it[GlobalMetaTable.key] = key val dbMetaMap =
it[GlobalMetaTable.value] = value GlobalMetaTable
.selectAll()
.where { GlobalMetaTable.key inList meta.keys }
.associateBy { it[GlobalMetaTable.key] }
val (existingMeta, newMeta) = meta.toList().partition { (key) -> key in dbMetaMap.keys }
if (existingMeta.isNotEmpty()) {
BatchUpdateStatement(GlobalMetaTable).apply {
existingMeta.forEach { (key, value) ->
addBatch(EntityID(dbMetaMap[key]!![GlobalMetaTable.id].value, GlobalMetaTable))
this[GlobalMetaTable.value] = value
}
execute(this@transaction)
} }
} else { }
GlobalMetaTable.update({ GlobalMetaTable.key eq key }) {
it[GlobalMetaTable.value] = value if (newMeta.isNotEmpty()) {
GlobalMetaTable.batchInsert(newMeta) { (key, value) ->
this[GlobalMetaTable.key] = key
this[GlobalMetaTable.value] = value
} }
} }
} }

View File

@@ -64,6 +64,8 @@ class BackupMutation {
includeChapters = input?.includeChapters ?: true, includeChapters = input?.includeChapters ?: true,
includeTracking = true, includeTracking = true,
includeHistory = true, includeHistory = true,
includeClientData = true,
includeServerSettings = true,
), ),
) )

View File

@@ -1,5 +1,6 @@
package suwayomi.tachidesk.graphql.mutations package suwayomi.tachidesk.graphql.mutations
import com.expediagroup.graphql.generator.annotations.GraphQLIgnore
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import suwayomi.tachidesk.graphql.types.PartialSettingsType import suwayomi.tachidesk.graphql.types.PartialSettingsType
import suwayomi.tachidesk.graphql.types.Settings import suwayomi.tachidesk.graphql.types.Settings
@@ -110,7 +111,8 @@ class SettingsMutation {
configSetting.value = newSetting configSetting.value = newSetting
} }
private fun updateSettings(settings: Settings) { @GraphQLIgnore
fun updateSettings(settings: Settings) {
updateSetting(settings.ip, serverConfig.ip) updateSetting(settings.ip, serverConfig.ip)
updateSetting(settings.port, serverConfig.port) updateSetting(settings.port, serverConfig.port)
@@ -123,11 +125,11 @@ class SettingsMutation {
updateSetting(settings.socksProxyPassword, serverConfig.socksProxyPassword) updateSetting(settings.socksProxyPassword, serverConfig.socksProxyPassword)
// webUI // webUI
updateSetting(settings.webUIFlavor?.uiName, serverConfig.webUIFlavor) updateSetting(settings.webUIFlavor, serverConfig.webUIFlavor)
updateSetting(settings.initialOpenInBrowserEnabled, serverConfig.initialOpenInBrowserEnabled) updateSetting(settings.initialOpenInBrowserEnabled, serverConfig.initialOpenInBrowserEnabled)
updateSetting(settings.webUIInterface?.name?.lowercase(), serverConfig.webUIInterface) updateSetting(settings.webUIInterface, serverConfig.webUIInterface)
updateSetting(settings.electronPath, serverConfig.electronPath) updateSetting(settings.electronPath, serverConfig.electronPath)
updateSetting(settings.webUIChannel?.name?.lowercase(), serverConfig.webUIChannel) updateSetting(settings.webUIChannel, serverConfig.webUIChannel)
updateSetting(settings.webUIUpdateCheckInterval, serverConfig.webUIUpdateCheckInterval) updateSetting(settings.webUIUpdateCheckInterval, serverConfig.webUIUpdateCheckInterval)
// downloader // downloader
@@ -182,6 +184,7 @@ class SettingsMutation {
updateSetting(settings.flareSolverrAsResponseFallback, serverConfig.flareSolverrAsResponseFallback) updateSetting(settings.flareSolverrAsResponseFallback, serverConfig.flareSolverrAsResponseFallback)
// opds // opds
updateSetting(settings.opdsUseBinaryFileSizes, serverConfig.opdsUseBinaryFileSizes)
updateSetting(settings.opdsItemsPerPage, serverConfig.opdsItemsPerPage) updateSetting(settings.opdsItemsPerPage, serverConfig.opdsItemsPerPage)
updateSetting(settings.opdsEnablePageReadProgress, serverConfig.opdsEnablePageReadProgress) updateSetting(settings.opdsEnablePageReadProgress, serverConfig.opdsEnablePageReadProgress)
updateSetting(settings.opdsMarkAsReadOnDownload, serverConfig.opdsMarkAsReadOnDownload) updateSetting(settings.opdsMarkAsReadOnDownload, serverConfig.opdsMarkAsReadOnDownload)

View File

@@ -3,7 +3,6 @@ package suwayomi.tachidesk.graphql.queries
import com.expediagroup.graphql.generator.annotations.GraphQLDeprecated import com.expediagroup.graphql.generator.annotations.GraphQLDeprecated
import suwayomi.tachidesk.global.impl.AppUpdate import suwayomi.tachidesk.global.impl.AppUpdate
import suwayomi.tachidesk.graphql.types.AboutWebUI import suwayomi.tachidesk.graphql.types.AboutWebUI
import suwayomi.tachidesk.graphql.types.WebUIChannel
import suwayomi.tachidesk.graphql.types.WebUIFlavor import suwayomi.tachidesk.graphql.types.WebUIFlavor
import suwayomi.tachidesk.graphql.types.WebUIUpdateCheck import suwayomi.tachidesk.graphql.types.WebUIUpdateCheck
import suwayomi.tachidesk.graphql.types.WebUIUpdateStatus import suwayomi.tachidesk.graphql.types.WebUIUpdateStatus
@@ -63,7 +62,7 @@ class InfoQuery {
future { future {
val (version, updateAvailable) = WebInterfaceManager.isUpdateAvailable(WebUIFlavor.current, raiseError = true) val (version, updateAvailable) = WebInterfaceManager.isUpdateAvailable(WebUIFlavor.current, raiseError = true)
WebUIUpdateCheck( WebUIUpdateCheck(
channel = WebUIChannel.from(serverConfig.webUIChannel.value), channel = serverConfig.webUIChannel.value,
tag = version, tag = version,
updateAvailable, updateAvailable,
) )

View File

@@ -8,6 +8,8 @@ enum class BackupRestoreState {
FAILURE, FAILURE,
RESTORING_CATEGORIES, RESTORING_CATEGORIES,
RESTORING_MANGA, RESTORING_MANGA,
RESTORING_META,
RESTORING_SETTINGS,
} }
data class BackupRestoreStatus( data class BackupRestoreStatus(
@@ -40,7 +42,19 @@ fun ProtoBackupImport.BackupRestoreState.toStatus(): BackupRestoreStatus =
BackupRestoreStatus( BackupRestoreStatus(
state = BackupRestoreState.RESTORING_CATEGORIES, state = BackupRestoreState.RESTORING_CATEGORIES,
totalManga = totalManga, totalManga = totalManga,
mangaProgress = 0, mangaProgress = current,
)
is ProtoBackupImport.BackupRestoreState.RestoringMeta ->
BackupRestoreStatus(
state = BackupRestoreState.RESTORING_META,
totalManga = totalManga,
mangaProgress = current,
)
is ProtoBackupImport.BackupRestoreState.RestoringSettings ->
BackupRestoreStatus(
state = BackupRestoreState.RESTORING_SETTINGS,
totalManga = totalManga,
mangaProgress = current,
) )
is ProtoBackupImport.BackupRestoreState.RestoringManga -> is ProtoBackupImport.BackupRestoreState.RestoringManga ->
BackupRestoreStatus( BackupRestoreStatus(

View File

@@ -95,6 +95,7 @@ interface Settings : Node {
val flareSolverrAsResponseFallback: Boolean? val flareSolverrAsResponseFallback: Boolean?
// opds // opds
val opdsUseBinaryFileSizes: Boolean?
val opdsItemsPerPage: Int? val opdsItemsPerPage: Int?
val opdsEnablePageReadProgress: Boolean? val opdsEnablePageReadProgress: Boolean?
val opdsMarkAsReadOnDownload: Boolean? val opdsMarkAsReadOnDownload: Boolean?
@@ -169,6 +170,7 @@ data class PartialSettingsType(
override val flareSolverrSessionTtl: Int?, override val flareSolverrSessionTtl: Int?,
override val flareSolverrAsResponseFallback: Boolean?, override val flareSolverrAsResponseFallback: Boolean?,
// opds // opds
override val opdsUseBinaryFileSizes: Boolean?,
override val opdsItemsPerPage: Int?, override val opdsItemsPerPage: Int?,
override val opdsEnablePageReadProgress: Boolean?, override val opdsEnablePageReadProgress: Boolean?,
override val opdsMarkAsReadOnDownload: Boolean?, override val opdsMarkAsReadOnDownload: Boolean?,
@@ -243,6 +245,7 @@ class SettingsType(
override val flareSolverrSessionTtl: Int, override val flareSolverrSessionTtl: Int,
override val flareSolverrAsResponseFallback: Boolean, override val flareSolverrAsResponseFallback: Boolean,
// opds // opds
override val opdsUseBinaryFileSizes: Boolean,
override val opdsItemsPerPage: Int, override val opdsItemsPerPage: Int,
override val opdsEnablePageReadProgress: Boolean, override val opdsEnablePageReadProgress: Boolean,
override val opdsMarkAsReadOnDownload: Boolean, override val opdsMarkAsReadOnDownload: Boolean,
@@ -261,11 +264,11 @@ class SettingsType(
config.socksProxyUsername.value, config.socksProxyUsername.value,
config.socksProxyPassword.value, config.socksProxyPassword.value,
// webUI // webUI
WebUIFlavor.from(config.webUIFlavor.value), config.webUIFlavor.value,
config.initialOpenInBrowserEnabled.value, config.initialOpenInBrowserEnabled.value,
WebUIInterface.from(config.webUIInterface.value), config.webUIInterface.value,
config.electronPath.value, config.electronPath.value,
WebUIChannel.from(config.webUIChannel.value), config.webUIChannel.value,
config.webUIUpdateCheckInterval.value, config.webUIUpdateCheckInterval.value,
// downloader // downloader
config.downloadAsCbz.value, config.downloadAsCbz.value,
@@ -311,6 +314,7 @@ class SettingsType(
config.flareSolverrSessionTtl.value, config.flareSolverrSessionTtl.value,
config.flareSolverrAsResponseFallback.value, config.flareSolverrAsResponseFallback.value,
// opds // opds
config.opdsUseBinaryFileSizes.value,
config.opdsItemsPerPage.value, config.opdsItemsPerPage.value,
config.opdsEnablePageReadProgress.value, config.opdsEnablePageReadProgress.value,
config.opdsMarkAsReadOnDownload.value, config.opdsMarkAsReadOnDownload.value,

View File

@@ -34,11 +34,6 @@ data class WebUIUpdateStatus(
enum class WebUIInterface { enum class WebUIInterface {
BROWSER, BROWSER,
ELECTRON, ELECTRON,
;
companion object {
fun from(value: String): WebUIInterface = entries.find { it.name.lowercase() == value.lowercase() } ?: BROWSER
}
} }
enum class WebUIChannel { enum class WebUIChannel {
@@ -49,8 +44,6 @@ enum class WebUIChannel {
companion object { companion object {
fun from(channel: String): WebUIChannel = entries.find { it.name.lowercase() == channel.lowercase() } ?: STABLE fun from(channel: String): WebUIChannel = entries.find { it.name.lowercase() == channel.lowercase() } ?: STABLE
fun doesConfigChannelEqual(channel: WebUIChannel): Boolean = serverConfig.webUIChannel.value.equals(channel.name, true)
} }
} }
@@ -92,6 +85,6 @@ enum class WebUIFlavor(
fun from(value: String): WebUIFlavor = entries.find { it.uiName == value } ?: default fun from(value: String): WebUIFlavor = entries.find { it.uiName == value } ?: default
val current: WebUIFlavor val current: WebUIFlavor
get() = from(serverConfig.webUIFlavor.value) get() = serverConfig.webUIFlavor.value
} }
} }

View File

@@ -90,6 +90,8 @@ object BackupController {
includeChapters = true, includeChapters = true,
includeTracking = true, includeTracking = true,
includeHistory = true, includeHistory = true,
includeClientData = true,
includeServerSettings = true,
), ),
) )
}.thenApply { ctx.result(it) } }.thenApply { ctx.result(it) }
@@ -122,6 +124,8 @@ object BackupController {
includeChapters = true, includeChapters = true,
includeTracking = true, includeTracking = true,
includeHistory = true, includeHistory = true,
includeClientData = true,
includeServerSettings = true,
), ),
) )
}.thenApply { ctx.result(it) } }.thenApply { ctx.result(it) }

View File

@@ -7,14 +7,15 @@ package suwayomi.tachidesk.manga.impl
* License, v. 2.0. If a copy of the MPL was not distributed with this * License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import org.jetbrains.exposed.dao.id.EntityID
import org.jetbrains.exposed.sql.SortOrder import org.jetbrains.exposed.sql.SortOrder
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
import org.jetbrains.exposed.sql.and import org.jetbrains.exposed.sql.and
import org.jetbrains.exposed.sql.andWhere import org.jetbrains.exposed.sql.andWhere
import org.jetbrains.exposed.sql.batchInsert import org.jetbrains.exposed.sql.batchInsert
import org.jetbrains.exposed.sql.deleteWhere import org.jetbrains.exposed.sql.deleteWhere
import org.jetbrains.exposed.sql.insert
import org.jetbrains.exposed.sql.selectAll import org.jetbrains.exposed.sql.selectAll
import org.jetbrains.exposed.sql.statements.BatchUpdateStatement
import org.jetbrains.exposed.sql.transactions.transaction import org.jetbrains.exposed.sql.transactions.transaction
import org.jetbrains.exposed.sql.update import org.jetbrains.exposed.sql.update
import suwayomi.tachidesk.manga.model.dataclass.CategoryDataClass import suwayomi.tachidesk.manga.model.dataclass.CategoryDataClass
@@ -23,6 +24,8 @@ import suwayomi.tachidesk.manga.model.table.CategoryMetaTable
import suwayomi.tachidesk.manga.model.table.CategoryTable import suwayomi.tachidesk.manga.model.table.CategoryTable
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 kotlin.collections.component1
import kotlin.collections.orEmpty
object Category { object Category {
/** /**
@@ -193,26 +196,72 @@ object Category {
.associate { it[CategoryMetaTable.key] to it[CategoryMetaTable.value] } .associate { it[CategoryMetaTable.key] to it[CategoryMetaTable.value] }
} }
fun getCategoriesMetaMaps(ids: List<Int>): Map<Int, Map<String, String>> =
transaction {
CategoryMetaTable
.selectAll()
.where { CategoryMetaTable.ref inList ids }
.groupBy { it[CategoryMetaTable.ref].value }
.mapValues { it.value.associate { it[CategoryMetaTable.key] to it[CategoryMetaTable.value] } }
.withDefault { emptyMap() }
}
fun modifyMeta( fun modifyMeta(
categoryId: Int, categoryId: Int,
key: String, key: String,
value: String, value: String,
) { ) {
transaction { modifyCategoriesMetas(mapOf(categoryId to mapOf(key to value)))
val meta = }
transaction {
CategoryMetaTable.selectAll().where { (CategoryMetaTable.ref eq categoryId) and (CategoryMetaTable.key eq key) }
}.firstOrNull()
if (meta == null) { fun modifyCategoriesMetas(metaByCategoryId: Map<Int, Map<String, String>>) {
CategoryMetaTable.insert { transaction {
it[CategoryMetaTable.key] = key val categoryIds = metaByCategoryId.keys
it[CategoryMetaTable.value] = value val metaKeys = metaByCategoryId.flatMap { it.value.keys }
it[CategoryMetaTable.ref] = categoryId
val dbMetaByCategoryId =
CategoryMetaTable
.selectAll()
.where { (CategoryMetaTable.ref inList categoryIds) and (CategoryMetaTable.key inList metaKeys) }
.groupBy { it[CategoryMetaTable.ref].value }
val existingMetaByMetaId =
categoryIds.flatMap { categoryId ->
val dbMetaByKey = dbMetaByCategoryId[categoryId].orEmpty().associateBy { it[CategoryMetaTable.key] }
val existingMetas = metaByCategoryId[categoryId].orEmpty().filter { (key) -> key in dbMetaByKey.keys }
existingMetas.map { entry ->
val metaId = dbMetaByKey[entry.key]!![CategoryMetaTable.id].value
metaId to entry
}
} }
} else {
CategoryMetaTable.update({ (CategoryMetaTable.ref eq categoryId) and (CategoryMetaTable.key eq key) }) { val newMetaByCategoryId =
it[CategoryMetaTable.value] = value categoryIds.flatMap { categoryID ->
val dbMetaByKey = dbMetaByCategoryId[categoryID].orEmpty().associateBy { it[CategoryMetaTable.key] }
metaByCategoryId[categoryID]
.orEmpty()
.filter { entry -> entry.key !in dbMetaByKey.keys }
.map { entry -> categoryID to entry }
}
if (existingMetaByMetaId.isNotEmpty()) {
BatchUpdateStatement(CategoryMetaTable).apply {
existingMetaByMetaId.forEach { (metaId, entry) ->
addBatch(EntityID(metaId, CategoryMetaTable))
this[CategoryMetaTable.value] = entry.value
}
execute(this@transaction)
}
}
if (newMetaByCategoryId.isNotEmpty()) {
CategoryMetaTable.batchInsert(newMetaByCategoryId) { (categoryId, entry) ->
this[CategoryMetaTable.ref] = EntityID(categoryId, CategoryTable)
this[CategoryMetaTable.key] = entry.key
this[CategoryMetaTable.value] = entry.value
} }
} }
} }

View File

@@ -24,7 +24,6 @@ import org.jetbrains.exposed.sql.SqlExpressionBuilder.inList
import org.jetbrains.exposed.sql.and import org.jetbrains.exposed.sql.and
import org.jetbrains.exposed.sql.batchInsert import org.jetbrains.exposed.sql.batchInsert
import org.jetbrains.exposed.sql.deleteWhere import org.jetbrains.exposed.sql.deleteWhere
import org.jetbrains.exposed.sql.insert
import org.jetbrains.exposed.sql.selectAll import org.jetbrains.exposed.sql.selectAll
import org.jetbrains.exposed.sql.statements.BatchUpdateStatement import org.jetbrains.exposed.sql.statements.BatchUpdateStatement
import org.jetbrains.exposed.sql.transactions.transaction import org.jetbrains.exposed.sql.transactions.transaction
@@ -102,7 +101,7 @@ object Chapter {
} }
val chapterIds = chapterList.map { dbChapterMap.getValue(it.url)[ChapterTable.id] } val chapterIds = chapterList.map { dbChapterMap.getValue(it.url)[ChapterTable.id] }
val chapterMetas = getChaptersMetaMaps(chapterIds) val chapterMetas = getChaptersMetaMaps(chapterIds.map { it.value })
return chapterList.mapIndexed { index, it -> return chapterList.mapIndexed { index, it ->
@@ -126,7 +125,7 @@ object Chapter {
downloaded = dbChapter[ChapterTable.isDownloaded], downloaded = dbChapter[ChapterTable.isDownloaded],
pageCount = dbChapter[ChapterTable.pageCount], pageCount = dbChapter[ChapterTable.pageCount],
chapterCount = chapterList.size, chapterCount = chapterList.size,
meta = chapterMetas.getValue(dbChapter[ChapterTable.id]), meta = chapterMetas.getValue(dbChapter[ChapterTable.id].value),
) )
} }
} }
@@ -553,12 +552,12 @@ object Chapter {
} }
} }
fun getChaptersMetaMaps(chapterIds: List<EntityID<Int>>): Map<EntityID<Int>, Map<String, String>> = fun getChaptersMetaMaps(chapterIds: List<Int>): Map<Int, Map<String, String>> =
transaction { transaction {
ChapterMetaTable ChapterMetaTable
.selectAll() .selectAll()
.where { ChapterMetaTable.ref inList chapterIds } .where { ChapterMetaTable.ref inList chapterIds }
.groupBy { it[ChapterMetaTable.ref] } .groupBy { it[ChapterMetaTable.ref].value }
.mapValues { it.value.associate { it[ChapterMetaTable.key] to it[ChapterMetaTable.value] } } .mapValues { it.value.associate { it[ChapterMetaTable.key] to it[ChapterMetaTable.value] } }
.withDefault { emptyMap() } .withDefault { emptyMap() }
} }
@@ -593,22 +592,57 @@ object Chapter {
key: String, key: String,
value: String, value: String,
) { ) {
modifyChaptersMetas(mapOf(chapterId to mapOf(key to value)))
}
fun modifyChaptersMetas(metaByChapterId: Map<Int, Map<String, String>>) {
transaction { transaction {
val meta = val chapterIds = metaByChapterId.keys
val metaKeys = metaByChapterId.flatMap { it.value.keys }
val dbMetaByChapterId =
ChapterMetaTable ChapterMetaTable
.selectAll() .selectAll()
.where { (ChapterMetaTable.ref eq chapterId) and (ChapterMetaTable.key eq key) } .where { (ChapterMetaTable.ref inList chapterIds) and (ChapterMetaTable.key inList metaKeys) }
.firstOrNull() .groupBy { it[ChapterMetaTable.ref].value }
if (meta == null) { val existingMetaByMetaId =
ChapterMetaTable.insert { chapterIds.flatMap { chapterId ->
it[ChapterMetaTable.key] = key val dbMetaByKey = dbMetaByChapterId[chapterId].orEmpty().associateBy { it[ChapterMetaTable.key] }
it[ChapterMetaTable.value] = value val existingMetas = metaByChapterId[chapterId].orEmpty().filter { (key) -> key in dbMetaByKey.keys }
it[ref] = chapterId
existingMetas.map { entry ->
val metaId = dbMetaByKey[entry.key]!![ChapterMetaTable.id].value
metaId to entry
}
} }
} else {
ChapterMetaTable.update({ (ChapterMetaTable.ref eq chapterId) and (ChapterMetaTable.key eq key) }) { val newMetaByChapterId =
it[ChapterMetaTable.value] = value chapterIds.flatMap { chapterId ->
val dbMetaByKey = dbMetaByChapterId[chapterId].orEmpty().associateBy { it[ChapterMetaTable.key] }
metaByChapterId[chapterId]
.orEmpty()
.filter { entry -> entry.key !in dbMetaByKey.keys }
.map { entry -> chapterId to entry }
}
if (existingMetaByMetaId.isNotEmpty()) {
BatchUpdateStatement(ChapterMetaTable).apply {
existingMetaByMetaId.forEach { (metaId, entry) ->
addBatch(EntityID(metaId, ChapterMetaTable))
this[ChapterMetaTable.value] = entry.value
}
execute(this@transaction)
}
}
if (newMetaByChapterId.isNotEmpty()) {
ChapterMetaTable.batchInsert(newMetaByChapterId) { (chapterId, entry) ->
this[ChapterMetaTable.ref] = EntityID(chapterId, ChapterTable)
this[ChapterMetaTable.key] = entry.key
this[ChapterMetaTable.value] = entry.value
} }
} }
} }

View File

@@ -20,11 +20,13 @@ import io.github.oshai.kotlinlogging.KotlinLogging
import io.javalin.http.HttpStatus import io.javalin.http.HttpStatus
import okhttp3.CacheControl import okhttp3.CacheControl
import okhttp3.Response import okhttp3.Response
import org.jetbrains.exposed.dao.id.EntityID
import org.jetbrains.exposed.sql.ResultRow import org.jetbrains.exposed.sql.ResultRow
import org.jetbrains.exposed.sql.SortOrder import org.jetbrains.exposed.sql.SortOrder
import org.jetbrains.exposed.sql.and import org.jetbrains.exposed.sql.and
import org.jetbrains.exposed.sql.insert import org.jetbrains.exposed.sql.batchInsert
import org.jetbrains.exposed.sql.selectAll import org.jetbrains.exposed.sql.selectAll
import org.jetbrains.exposed.sql.statements.BatchUpdateStatement
import org.jetbrains.exposed.sql.transactions.transaction import org.jetbrains.exposed.sql.transactions.transaction
import org.jetbrains.exposed.sql.update import org.jetbrains.exposed.sql.update
import suwayomi.tachidesk.manga.impl.MangaList.proxyThumbnailUrl import suwayomi.tachidesk.manga.impl.MangaList.proxyThumbnailUrl
@@ -256,22 +258,57 @@ object Manga {
key: String, key: String,
value: String, value: String,
) { ) {
modifyMangasMetas(mapOf(mangaId to mapOf(key to value)))
}
fun modifyMangasMetas(metaByMangaId: Map<Int, Map<String, String>>) {
transaction { transaction {
val meta = val mangaIds = metaByMangaId.keys
val metaKeys = metaByMangaId.flatMap { it.value.keys }
val dbMetaByMangaId =
MangaMetaTable MangaMetaTable
.selectAll() .selectAll()
.where { (MangaMetaTable.ref eq mangaId) and (MangaMetaTable.key eq key) } .where { (MangaMetaTable.ref inList mangaIds) and (MangaMetaTable.key inList metaKeys) }
.firstOrNull() .groupBy { it[MangaMetaTable.ref].value }
if (meta == null) { val existingMetaByMetaId =
MangaMetaTable.insert { mangaIds.flatMap { mangaId ->
it[MangaMetaTable.key] = key val metaByKey = dbMetaByMangaId[mangaId].orEmpty().associateBy { it[MangaMetaTable.key] }
it[MangaMetaTable.value] = value val existingMetas = metaByMangaId[mangaId].orEmpty().filter { (key) -> key in metaByKey.keys }
it[MangaMetaTable.ref] = mangaId
existingMetas.map { entry ->
val metaId = metaByKey[entry.key]!![MangaMetaTable.id].value
metaId to entry
}
} }
} else {
MangaMetaTable.update({ (MangaMetaTable.ref eq mangaId) and (MangaMetaTable.key eq key) }) { val newMetaByMangaId =
it[MangaMetaTable.value] = value mangaIds.flatMap { mangaId ->
val metaByKey = dbMetaByMangaId[mangaId].orEmpty().associateBy { it[MangaMetaTable.key] }
metaByMangaId[mangaId]
.orEmpty()
.filter { entry -> entry.key !in metaByKey.keys }
.map { entry -> mangaId to entry }
}
if (existingMetaByMetaId.isNotEmpty()) {
BatchUpdateStatement(MangaMetaTable).apply {
existingMetaByMetaId.forEach { (metaId, entry) ->
addBatch(EntityID(metaId, MangaMetaTable))
this[MangaMetaTable.value] = entry.value
}
execute(this@transaction)
}
}
if (newMetaByMangaId.isNotEmpty()) {
MangaMetaTable.batchInsert(newMetaByMangaId) { (mangaId, entry) ->
this[MangaMetaTable.ref] = EntityID(mangaId, MangaTable)
this[MangaMetaTable.key] = entry.key
this[MangaMetaTable.value] = entry.value
} }
} }
} }

View File

@@ -14,11 +14,12 @@ import eu.kanade.tachiyomi.source.sourcePreferences
import io.github.oshai.kotlinlogging.KotlinLogging import io.github.oshai.kotlinlogging.KotlinLogging
import io.javalin.json.JsonMapper import io.javalin.json.JsonMapper
import io.javalin.json.fromJsonString import io.javalin.json.fromJsonString
import org.jetbrains.exposed.dao.id.EntityID
import org.jetbrains.exposed.sql.and import org.jetbrains.exposed.sql.and
import org.jetbrains.exposed.sql.insert import org.jetbrains.exposed.sql.batchInsert
import org.jetbrains.exposed.sql.selectAll import org.jetbrains.exposed.sql.selectAll
import org.jetbrains.exposed.sql.statements.BatchUpdateStatement
import org.jetbrains.exposed.sql.transactions.transaction import org.jetbrains.exposed.sql.transactions.transaction
import org.jetbrains.exposed.sql.update
import suwayomi.tachidesk.manga.impl.extension.Extension.getExtensionIconUrl import suwayomi.tachidesk.manga.impl.extension.Extension.getExtensionIconUrl
import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource.getCatalogueSourceOrNull import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource.getCatalogueSourceOrNull
import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource.getCatalogueSourceOrStub import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource.getCatalogueSourceOrStub
@@ -27,7 +28,6 @@ import suwayomi.tachidesk.manga.model.dataclass.SourceDataClass
import suwayomi.tachidesk.manga.model.table.ExtensionTable import suwayomi.tachidesk.manga.model.table.ExtensionTable
import suwayomi.tachidesk.manga.model.table.SourceMetaTable import suwayomi.tachidesk.manga.model.table.SourceMetaTable
import suwayomi.tachidesk.manga.model.table.SourceTable import suwayomi.tachidesk.manga.model.table.SourceTable
import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
import xyz.nulldev.androidcompat.androidimpl.CustomContext import xyz.nulldev.androidcompat.androidimpl.CustomContext
@@ -151,26 +151,72 @@ object Source {
unregisterCatalogueSource(sourceId) unregisterCatalogueSource(sourceId)
} }
fun getSourcesMetaMaps(ids: List<Long>): Map<Long, Map<String, String>> =
transaction {
SourceMetaTable
.selectAll()
.where { SourceMetaTable.ref inList ids }
.groupBy { it[SourceMetaTable.ref] }
.mapValues { it.value.associate { it[SourceMetaTable.key] to it[SourceMetaTable.value] } }
.withDefault { emptyMap() }
}
fun modifyMeta( fun modifyMeta(
sourceId: Long, sourceId: Long,
key: String, key: String,
value: String, value: String,
) { ) {
transaction { modifySourceMetas(mapOf(sourceId to mapOf(key to value)))
val meta = }
transaction {
SourceMetaTable.selectAll().where { (SourceMetaTable.ref eq sourceId) and (SourceMetaTable.key eq key) }
}.firstOrNull()
if (meta == null) { fun modifySourceMetas(metaBySourceIds: Map<Long, Map<String, String>>) {
SourceMetaTable.insert { transaction {
it[SourceMetaTable.key] = key val sourceIds = metaBySourceIds.keys
it[SourceMetaTable.value] = value val metaKeys = metaBySourceIds.flatMap { it.value.keys }
it[SourceMetaTable.ref] = sourceId
val dbMetaBySourceId =
SourceMetaTable
.selectAll()
.where { (SourceMetaTable.ref inList sourceIds) and (SourceMetaTable.key inList metaKeys) }
.groupBy { it[SourceMetaTable.ref] }
val existingMetaByMetaId =
sourceIds.flatMap { sourceId ->
val metaByKey = dbMetaBySourceId[sourceId].orEmpty().associateBy { it[SourceMetaTable.key] }
val existingMetas = metaBySourceIds[sourceId].orEmpty().filter { (key) -> key in metaByKey.keys }
existingMetas.map { entry ->
val metaId = metaByKey[entry.key]!![SourceMetaTable.id].value
metaId to entry
}
} }
} else {
SourceMetaTable.update({ (SourceMetaTable.ref eq sourceId) and (SourceMetaTable.key eq key) }) { val newMetaBySourceId =
it[SourceMetaTable.value] = value sourceIds.flatMap { sourceId ->
val metaByKey = dbMetaBySourceId[sourceId].orEmpty().associateBy { it[SourceMetaTable.key] }
metaBySourceIds[sourceId]
.orEmpty()
.filter { entry -> entry.key !in metaByKey.keys }
.map { entry -> sourceId to entry }
}
if (existingMetaByMetaId.isNotEmpty()) {
BatchUpdateStatement(SourceMetaTable).apply {
existingMetaByMetaId.forEach { (metaId, entry) ->
addBatch(EntityID(metaId, SourceMetaTable))
this[SourceMetaTable.value] = entry.value
}
execute(this@transaction)
}
}
if (newMetaBySourceId.isNotEmpty()) {
SourceMetaTable.batchInsert(newMetaBySourceId) { (sourceId, entry) ->
this[SourceMetaTable.ref] = sourceId
this[SourceMetaTable.key] = entry.key
this[SourceMetaTable.value] = entry.value
} }
} }
} }

View File

@@ -13,4 +13,6 @@ data class BackupFlags(
val includeChapters: Boolean, val includeChapters: Boolean,
val includeTracking: Boolean, val includeTracking: Boolean,
val includeHistory: Boolean, val includeHistory: Boolean,
val includeClientData: Boolean,
val includeServerSettings: Boolean,
) )

View File

@@ -22,6 +22,8 @@ interface Chapter :
var source_order: Int var source_order: Int
var meta: Map<String, String>
val isRecognizedNumber: Boolean val isRecognizedNumber: Boolean
get() = chapter_number >= 0f get() = chapter_number >= 0f
} }

View File

@@ -27,6 +27,8 @@ class ChapterImpl : Chapter {
override var source_order: Int = 0 override var source_order: Int = 0
override var meta: Map<String, String> = emptyMap()
override fun equals(other: Any?): Boolean { override fun equals(other: Any?): Boolean {
if (this === other) return true if (this === other) return true
if (other == null || javaClass != other.javaClass) return false if (other == null || javaClass != other.javaClass) return false

View File

@@ -37,6 +37,8 @@ interface Manga : SManga {
var cover_last_modified: Long var cover_last_modified: Long
var meta: Map<String, String>
fun setChapterOrder(order: Int) { fun setChapterOrder(order: Int) {
setChapterFlags(order, CHAPTER_SORT_MASK) setChapterFlags(order, CHAPTER_SORT_MASK)
} }

View File

@@ -37,6 +37,8 @@ open class MangaImpl : Manga {
override var initialized: Boolean = false override var initialized: Boolean = false
override var meta: Map<String, String> = emptyMap()
/** Reader mode value /** Reader mode value
* ref: https://github.com/tachiyomiorg/tachiyomi/blob/ff369010074b058bb734ce24c66508300e6e9ac6/app/src/main/java/eu/kanade/tachiyomi/ui/reader/setting/ReadingModeType.kt#L8 * ref: https://github.com/tachiyomiorg/tachiyomi/blob/ff369010074b058bb734ce24c66508300e6e9ac6/app/src/main/java/eu/kanade/tachiyomi/ui/reader/setting/ReadingModeType.kt#L8
* 0 -> Default * 0 -> Default

View File

@@ -24,13 +24,18 @@ import org.jetbrains.exposed.sql.Query
import org.jetbrains.exposed.sql.SortOrder import org.jetbrains.exposed.sql.SortOrder
import org.jetbrains.exposed.sql.selectAll import org.jetbrains.exposed.sql.selectAll
import org.jetbrains.exposed.sql.transactions.transaction import org.jetbrains.exposed.sql.transactions.transaction
import suwayomi.tachidesk.global.impl.GlobalMeta
import suwayomi.tachidesk.manga.impl.Category import suwayomi.tachidesk.manga.impl.Category
import suwayomi.tachidesk.manga.impl.CategoryManga import suwayomi.tachidesk.manga.impl.CategoryManga
import suwayomi.tachidesk.manga.impl.Chapter
import suwayomi.tachidesk.manga.impl.Manga
import suwayomi.tachidesk.manga.impl.Source
import suwayomi.tachidesk.manga.impl.backup.BackupFlags import suwayomi.tachidesk.manga.impl.backup.BackupFlags
import suwayomi.tachidesk.manga.impl.backup.proto.models.Backup import suwayomi.tachidesk.manga.impl.backup.proto.models.Backup
import suwayomi.tachidesk.manga.impl.backup.proto.models.BackupCategory import suwayomi.tachidesk.manga.impl.backup.proto.models.BackupCategory
import suwayomi.tachidesk.manga.impl.backup.proto.models.BackupChapter import suwayomi.tachidesk.manga.impl.backup.proto.models.BackupChapter
import suwayomi.tachidesk.manga.impl.backup.proto.models.BackupManga import suwayomi.tachidesk.manga.impl.backup.proto.models.BackupManga
import suwayomi.tachidesk.manga.impl.backup.proto.models.BackupServerSettings
import suwayomi.tachidesk.manga.impl.backup.proto.models.BackupSource import suwayomi.tachidesk.manga.impl.backup.proto.models.BackupSource
import suwayomi.tachidesk.manga.impl.backup.proto.models.BackupTracking import suwayomi.tachidesk.manga.impl.backup.proto.models.BackupTracking
import suwayomi.tachidesk.manga.impl.track.Track import suwayomi.tachidesk.manga.impl.track.Track
@@ -123,6 +128,8 @@ object ProtoBackupExport : ProtoBackupBase() {
includeChapters = true, includeChapters = true,
includeTracking = true, includeTracking = true,
includeHistory = true, includeHistory = true,
includeClientData = true,
includeServerSettings = true,
), ),
).use { input -> ).use { input ->
val automatedBackupDir = File(applicationDirs.automatedBackupRoot) val automatedBackupDir = File(applicationDirs.automatedBackupRoot)
@@ -181,8 +188,10 @@ object ProtoBackupExport : ProtoBackupBase() {
transaction { transaction {
Backup( Backup(
backupManga(databaseManga, flags), backupManga(databaseManga, flags),
backupCategories(), backupCategories(flags),
backupExtensionInfo(databaseManga), backupExtensionInfo(databaseManga, flags),
backupGlobalMeta(flags),
backupServerSettings(flags),
) )
} }
@@ -220,6 +229,10 @@ object ProtoBackupExport : ProtoBackupBase() {
val mangaId = mangaRow[MangaTable.id].value val mangaId = mangaRow[MangaTable.id].value
if (flags.includeClientData) {
backupManga.meta = Manga.getMangaMetaMap(mangaId)
}
if (flags.includeChapters) { if (flags.includeChapters) {
val chapters = val chapters =
transaction { transaction {
@@ -231,6 +244,7 @@ object ProtoBackupExport : ProtoBackupBase() {
ChapterTable.toDataClass(it) ChapterTable.toDataClass(it)
} }
} }
val chapterToMeta = Chapter.getChaptersMetaMaps(chapters.map { it.id })
backupManga.chapters = backupManga.chapters =
chapters.map { chapters.map {
@@ -245,7 +259,11 @@ object ProtoBackupExport : ProtoBackupBase() {
it.uploadDate, it.uploadDate,
it.chapterNumber, it.chapterNumber,
chapters.size - it.index, chapters.size - it.index,
) ).apply {
if (flags.includeClientData) {
this.meta = chapterToMeta[it.id] ?: emptyMap()
}
}
} }
} }
@@ -287,31 +305,135 @@ object ProtoBackupExport : ProtoBackupBase() {
backupManga backupManga
} }
private fun backupCategories(): List<BackupCategory> = private fun backupCategories(flags: BackupFlags): List<BackupCategory> {
CategoryTable val categories =
.selectAll() CategoryTable
.orderBy(CategoryTable.order to SortOrder.ASC) .selectAll()
.map { .orderBy(CategoryTable.order to SortOrder.ASC)
CategoryTable.toDataClass(it) .map { CategoryTable.toDataClass(it) }
}.filter { it.id != Category.DEFAULT_CATEGORY_ID } val categoryToMeta = Category.getCategoriesMetaMaps(categories.map { it.id })
.map {
BackupCategory(
it.name,
it.order,
0, // not supported in Tachidesk
)
}
private fun backupExtensionInfo(mangas: Query): List<BackupSource> = return categories.map {
mangas BackupCategory(
.asSequence() it.name,
.map { it[MangaTable.sourceReference] } it.order,
.distinct() 0, // not supported in Tachidesk
.map { ).apply {
val sourceRow = SourceTable.selectAll().where { SourceTable.id eq it }.firstOrNull() if (flags.includeClientData) {
this.meta = categoryToMeta[it.id] ?: emptyMap()
}
}
}
}
private fun backupExtensionInfo(
mangas: Query,
flags: BackupFlags,
): List<BackupSource> {
val inLibraryMangaSourceIds =
mangas
.asSequence()
.map { it[MangaTable.sourceReference] }
.distinct()
.toList()
val sources = SourceTable.selectAll().where { SourceTable.id inList inLibraryMangaSourceIds }
val sourceToMeta = Source.getSourcesMetaMaps(sources.map { it[SourceTable.id].value })
return inLibraryMangaSourceIds
.map { mangaSourceId ->
val source = sources.firstOrNull { it[SourceTable.id].value == mangaSourceId }
BackupSource( BackupSource(
sourceRow?.get(SourceTable.name) ?: "", source?.get(SourceTable.name) ?: "",
it, mangaSourceId,
) ).apply {
if (flags.includeClientData) {
this.meta = sourceToMeta[mangaSourceId] ?: emptyMap()
}
}
}.toList() }.toList()
}
private fun backupGlobalMeta(flags: BackupFlags): Map<String, String> {
if (!flags.includeClientData) {
return emptyMap()
}
return GlobalMeta.getMetaMap()
}
private fun backupServerSettings(flags: BackupFlags): BackupServerSettings? {
if (!flags.includeServerSettings) {
return null
}
return BackupServerSettings(
ip = serverConfig.ip.value,
port = serverConfig.port.value,
// socks
socksProxyEnabled = serverConfig.socksProxyEnabled.value,
socksProxyVersion = serverConfig.socksProxyVersion.value,
socksProxyHost = serverConfig.socksProxyHost.value,
socksProxyPort = serverConfig.socksProxyPort.value,
socksProxyUsername = serverConfig.socksProxyUsername.value,
socksProxyPassword = serverConfig.socksProxyPassword.value,
// webUI
webUIFlavor = serverConfig.webUIFlavor.value,
initialOpenInBrowserEnabled = serverConfig.initialOpenInBrowserEnabled.value,
webUIInterface = serverConfig.webUIInterface.value,
electronPath = serverConfig.electronPath.value,
webUIChannel = serverConfig.webUIChannel.value,
webUIUpdateCheckInterval = serverConfig.webUIUpdateCheckInterval.value,
// downloader
downloadAsCbz = serverConfig.downloadAsCbz.value,
downloadsPath = serverConfig.downloadsPath.value,
autoDownloadNewChapters = serverConfig.autoDownloadNewChapters.value,
excludeEntryWithUnreadChapters = serverConfig.excludeEntryWithUnreadChapters.value,
autoDownloadAheadLimit = 0, // deprecated
autoDownloadNewChaptersLimit = serverConfig.autoDownloadNewChaptersLimit.value,
autoDownloadIgnoreReUploads = serverConfig.autoDownloadIgnoreReUploads.value,
// extension
extensionRepos = serverConfig.extensionRepos.value,
// requests
maxSourcesInParallel = serverConfig.maxSourcesInParallel.value,
// updater
excludeUnreadChapters = serverConfig.excludeUnreadChapters.value,
excludeNotStarted = serverConfig.excludeNotStarted.value,
excludeCompleted = serverConfig.excludeCompleted.value,
globalUpdateInterval = serverConfig.globalUpdateInterval.value,
updateMangas = serverConfig.updateMangas.value,
// Authentication
basicAuthEnabled = serverConfig.basicAuthEnabled.value,
basicAuthUsername = serverConfig.basicAuthUsername.value,
basicAuthPassword = serverConfig.basicAuthPassword.value,
// misc
debugLogsEnabled = serverConfig.debugLogsEnabled.value,
gqlDebugLogsEnabled = false, // deprecated
systemTrayEnabled = serverConfig.systemTrayEnabled.value,
maxLogFiles = serverConfig.maxLogFiles.value,
maxLogFileSize = serverConfig.maxLogFileSize.value,
maxLogFolderSize = serverConfig.maxLogFolderSize.value,
// backup
backupPath = serverConfig.backupPath.value,
backupTime = serverConfig.backupTime.value,
backupInterval = serverConfig.backupInterval.value,
backupTTL = serverConfig.backupTTL.value,
// local source
localSourcePath = serverConfig.localSourcePath.value,
// cloudflare bypass
flareSolverrEnabled = serverConfig.flareSolverrEnabled.value,
flareSolverrUrl = serverConfig.flareSolverrUrl.value,
flareSolverrTimeout = serverConfig.flareSolverrTimeout.value,
flareSolverrSessionName = serverConfig.flareSolverrSessionName.value,
flareSolverrSessionTtl = serverConfig.flareSolverrSessionTtl.value,
flareSolverrAsResponseFallback = serverConfig.flareSolverrAsResponseFallback.value,
// opds
opdsUseBinaryFileSizes = serverConfig.opdsUseBinaryFileSizes.value,
opdsItemsPerPage = serverConfig.opdsItemsPerPage.value,
opdsEnablePageReadProgress = serverConfig.opdsEnablePageReadProgress.value,
opdsMarkAsReadOnDownload = serverConfig.opdsMarkAsReadOnDownload.value,
opdsShowOnlyUnreadChapters = serverConfig.opdsShowOnlyUnreadChapters.value,
opdsShowOnlyDownloadedChapters = serverConfig.opdsShowOnlyDownloadedChapters.value,
opdsChapterSortOrder = serverConfig.opdsChapterSortOrder.value,
)
}
} }

View File

@@ -30,10 +30,16 @@ import org.jetbrains.exposed.sql.selectAll
import org.jetbrains.exposed.sql.statements.BatchUpdateStatement import org.jetbrains.exposed.sql.statements.BatchUpdateStatement
import org.jetbrains.exposed.sql.transactions.transaction import org.jetbrains.exposed.sql.transactions.transaction
import org.jetbrains.exposed.sql.update import org.jetbrains.exposed.sql.update
import suwayomi.tachidesk.global.impl.GlobalMeta
import suwayomi.tachidesk.graphql.mutations.SettingsMutation
import suwayomi.tachidesk.graphql.types.toStatus import suwayomi.tachidesk.graphql.types.toStatus
import suwayomi.tachidesk.manga.impl.Category import suwayomi.tachidesk.manga.impl.Category
import suwayomi.tachidesk.manga.impl.Category.modifyCategoriesMetas
import suwayomi.tachidesk.manga.impl.CategoryManga import suwayomi.tachidesk.manga.impl.CategoryManga
import suwayomi.tachidesk.manga.impl.Chapter.modifyChaptersMetas
import suwayomi.tachidesk.manga.impl.Manga.clearThumbnail import suwayomi.tachidesk.manga.impl.Manga.clearThumbnail
import suwayomi.tachidesk.manga.impl.Manga.modifyMangasMetas
import suwayomi.tachidesk.manga.impl.Source.modifySourceMetas
import suwayomi.tachidesk.manga.impl.backup.models.Chapter import suwayomi.tachidesk.manga.impl.backup.models.Chapter
import suwayomi.tachidesk.manga.impl.backup.models.Manga import suwayomi.tachidesk.manga.impl.backup.models.Manga
import suwayomi.tachidesk.manga.impl.backup.proto.ProtoBackupValidator.ValidationResult import suwayomi.tachidesk.manga.impl.backup.proto.ProtoBackupValidator.ValidationResult
@@ -42,6 +48,8 @@ import suwayomi.tachidesk.manga.impl.backup.proto.models.Backup
import suwayomi.tachidesk.manga.impl.backup.proto.models.BackupCategory import suwayomi.tachidesk.manga.impl.backup.proto.models.BackupCategory
import suwayomi.tachidesk.manga.impl.backup.proto.models.BackupHistory import suwayomi.tachidesk.manga.impl.backup.proto.models.BackupHistory
import suwayomi.tachidesk.manga.impl.backup.proto.models.BackupManga import suwayomi.tachidesk.manga.impl.backup.proto.models.BackupManga
import suwayomi.tachidesk.manga.impl.backup.proto.models.BackupServerSettings
import suwayomi.tachidesk.manga.impl.backup.proto.models.BackupSource
import suwayomi.tachidesk.manga.impl.backup.proto.models.BackupTracking import suwayomi.tachidesk.manga.impl.backup.proto.models.BackupTracking
import suwayomi.tachidesk.manga.impl.track.tracker.TrackerManager import suwayomi.tachidesk.manga.impl.track.tracker.TrackerManager
import suwayomi.tachidesk.manga.impl.track.tracker.model.toTrack import suwayomi.tachidesk.manga.impl.track.tracker.model.toTrack
@@ -82,6 +90,17 @@ object ProtoBackupImport : ProtoBackupBase() {
data object Failure : BackupRestoreState() data object Failure : BackupRestoreState()
data class RestoringCategories( data class RestoringCategories(
val current: Int,
val totalManga: Int,
) : BackupRestoreState()
data class RestoringMeta(
val current: Int,
val totalManga: Int,
) : BackupRestoreState()
data class RestoringSettings(
val current: Int,
val totalManga: Int, val totalManga: Int,
) : BackupRestoreState() ) : BackupRestoreState()
@@ -177,12 +196,29 @@ object ProtoBackupImport : ProtoBackupBase() {
val validationResult = validate(backup) val validationResult = validate(backup)
restoreAmount = backup.backupManga.size + 1 // +1 for categories val restoreCategories = 1
val restoreMeta = 1
val restoreSettings = 1
val getRestoreAmount = { size: Int -> size + restoreCategories + restoreMeta + restoreSettings }
restoreAmount = getRestoreAmount(backup.backupManga.size)
updateRestoreState(id, BackupRestoreState.RestoringCategories(backup.backupManga.size)) updateRestoreState(id, BackupRestoreState.RestoringCategories(restoreCategories, restoreAmount))
val categoryMapping = restoreCategories(backup.backupCategories) val categoryMapping = restoreCategories(backup.backupCategories)
updateRestoreState(id, BackupRestoreState.RestoringMeta(restoreCategories + restoreMeta, restoreAmount))
restoreGlobalMeta(backup.meta)
restoreSourceMeta(backup.backupSources)
updateRestoreState(
id,
BackupRestoreState.RestoringSettings(restoreCategories + restoreMeta + restoreSettings, restoreAmount),
)
restoreServerSettings(backup.serverSettings)
// Store source mapping for error messages // Store source mapping for error messages
sourceMapping = backup.getSourceMap() sourceMapping = backup.getSourceMap()
@@ -191,8 +227,8 @@ object ProtoBackupImport : ProtoBackupBase() {
updateRestoreState( updateRestoreState(
id, id,
BackupRestoreState.RestoringManga( BackupRestoreState.RestoringManga(
current = index + 1, current = getRestoreAmount(index + 1),
totalManga = backup.backupManga.size, totalManga = restoreAmount,
title = manga.title, title = manga.title,
), ),
) )
@@ -225,6 +261,15 @@ object ProtoBackupImport : ProtoBackupBase() {
private fun restoreCategories(backupCategories: List<BackupCategory>): Map<Int, Int> { private fun restoreCategories(backupCategories: List<BackupCategory>): Map<Int, Int> {
val categoryIds = Category.createCategories(backupCategories.map { it.name }) val categoryIds = Category.createCategories(backupCategories.map { it.name })
val metaEntryByCategoryId =
categoryIds
.zip(backupCategories)
.associate { (categoryId, backupCategory) ->
categoryId to backupCategory.meta
}
modifyCategoriesMetas(metaEntryByCategoryId)
return backupCategories.withIndex().associate { (index, backupCategory) -> return backupCategories.withIndex().associate { (index, backupCategory) ->
backupCategory.order to categoryIds[index] backupCategory.order to categoryIds[index]
} }
@@ -318,6 +363,10 @@ object ProtoBackupImport : ProtoBackupBase() {
// delete thumbnail in case cached data still exists // delete thumbnail in case cached data still exists
clearThumbnail(mangaId) clearThumbnail(mangaId)
if (manga.meta.isNotEmpty()) {
modifyMangasMetas(mapOf(mangaId to manga.meta))
}
// merge chapter data // merge chapter data
restoreMangaChapterData(mangaId, restoreMode, chapters) restoreMangaChapterData(mangaId, restoreMode, chapters)
@@ -358,26 +407,28 @@ object ProtoBackupImport : ProtoBackupBase() {
) = dbTransaction { ) = dbTransaction {
val (chaptersToInsert, chaptersToUpdateToDbChapter) = getMangaChapterToRestoreInfo(mangaId, restoreMode, chapters) val (chaptersToInsert, chaptersToUpdateToDbChapter) = getMangaChapterToRestoreInfo(mangaId, restoreMode, chapters)
ChapterTable.batchInsert(chaptersToInsert) { chapter -> val insertedChapterIds =
this[ChapterTable.url] = chapter.url ChapterTable
this[ChapterTable.name] = chapter.name .batchInsert(chaptersToInsert) { chapter ->
if (chapter.date_upload == 0L) { this[ChapterTable.url] = chapter.url
this[ChapterTable.date_upload] = chapter.date_fetch this[ChapterTable.name] = chapter.name
} else { if (chapter.date_upload == 0L) {
this[ChapterTable.date_upload] = chapter.date_upload this[ChapterTable.date_upload] = chapter.date_fetch
} } else {
this[ChapterTable.chapter_number] = chapter.chapter_number this[ChapterTable.date_upload] = chapter.date_upload
this[ChapterTable.scanlator] = chapter.scanlator }
this[ChapterTable.chapter_number] = chapter.chapter_number
this[ChapterTable.scanlator] = chapter.scanlator
this[ChapterTable.sourceOrder] = chaptersToInsert.size - chapter.source_order this[ChapterTable.sourceOrder] = chaptersToInsert.size - chapter.source_order
this[ChapterTable.manga] = mangaId this[ChapterTable.manga] = mangaId
this[ChapterTable.isRead] = chapter.read this[ChapterTable.isRead] = chapter.read
this[ChapterTable.lastPageRead] = chapter.last_page_read.coerceAtLeast(0) this[ChapterTable.lastPageRead] = chapter.last_page_read.coerceAtLeast(0)
this[ChapterTable.isBookmarked] = chapter.bookmark this[ChapterTable.isBookmarked] = chapter.bookmark
this[ChapterTable.fetchedAt] = TimeUnit.MILLISECONDS.toSeconds(chapter.date_fetch) this[ChapterTable.fetchedAt] = TimeUnit.MILLISECONDS.toSeconds(chapter.date_fetch)
} }.map { it[ChapterTable.id].value }
if (chaptersToUpdateToDbChapter.isNotEmpty()) { if (chaptersToUpdateToDbChapter.isNotEmpty()) {
BatchUpdateStatement(ChapterTable).apply { BatchUpdateStatement(ChapterTable).apply {
@@ -391,6 +442,20 @@ object ProtoBackupImport : ProtoBackupBase() {
execute(this@dbTransaction) execute(this@dbTransaction)
} }
} }
val chaptersToInsertByChapterId = insertedChapterIds.zip(chaptersToInsert)
val chapterToUpdateByChapterId =
chaptersToUpdateToDbChapter.map { (backupChapter, dbChapter) ->
dbChapter[ChapterTable.id].value to
backupChapter
}
val metaEntryByChapterId =
(chaptersToInsertByChapterId + chapterToUpdateByChapterId)
.associate { (chapterId, backupChapter) ->
chapterId to backupChapter.meta
}
modifyChaptersMetas(metaEntryByChapterId)
} }
private fun restoreMangaCategoryData( private fun restoreMangaCategoryData(
@@ -440,5 +505,21 @@ object ProtoBackupImport : ProtoBackupBase() {
Tracker.insertTrackRecords(newTracks) Tracker.insertTrackRecords(newTracks)
} }
private fun restoreGlobalMeta(meta: Map<String, String>) {
GlobalMeta.modifyMetas(meta)
}
private fun restoreSourceMeta(backupSources: List<BackupSource>) {
modifySourceMetas(backupSources.associateBy { it.sourceId }.mapValues { it.value.meta })
}
private fun restoreServerSettings(backupServerSettings: BackupServerSettings?) {
if (backupServerSettings == null) {
return
}
SettingsMutation().updateSettings(backupServerSettings)
}
private fun TrackRecordDataClass.forComparison() = this.copy(id = 0, mangaId = 0) private fun TrackRecordDataClass.forComparison() = this.copy(id = 0, mangaId = 0)
} }

View File

@@ -13,6 +13,9 @@ data class Backup(
// Bump by 100 to specify this is a 0.x value // Bump by 100 to specify this is a 0.x value
// @ProtoNumber(100) var brokenBackupSources: List<BrokenBackupSource> = emptyList(), // @ProtoNumber(100) var brokenBackupSources: List<BrokenBackupSource> = emptyList(),
@ProtoNumber(101) var backupSources: List<BackupSource> = emptyList(), @ProtoNumber(101) var backupSources: List<BackupSource> = emptyList(),
// suwayomi
@ProtoNumber(9000) var meta: Map<String, String> = emptyMap(),
@ProtoNumber(9001) var serverSettings: BackupServerSettings?,
) { ) {
fun getSourceMap(): Map<Long, String> = fun getSourceMap(): Map<Long, String> =
backupSources backupSources

View File

@@ -10,4 +10,6 @@ class BackupCategory(
// @ProtoNumber(3) val updateInterval: Int = 0, 1.x value not used in 0.x // @ProtoNumber(3) val updateInterval: Int = 0, 1.x value not used in 0.x
// Bump by 100 to specify this is a 0.x value // Bump by 100 to specify this is a 0.x value
@ProtoNumber(100) var flags: Int = 0, @ProtoNumber(100) var flags: Int = 0,
// suwayomi
@ProtoNumber(9000) var meta: Map<String, String> = emptyMap(),
) )

View File

@@ -20,6 +20,8 @@ data class BackupChapter(
// chapterNumber is called number is 1.x // chapterNumber is called number is 1.x
@ProtoNumber(9) var chapterNumber: Float = 0F, @ProtoNumber(9) var chapterNumber: Float = 0F,
@ProtoNumber(10) var sourceOrder: Int = 0, @ProtoNumber(10) var sourceOrder: Int = 0,
// suwayomi
@ProtoNumber(9000) var meta: Map<String, String> = emptyMap(),
) { ) {
fun toChapterImpl(): ChapterImpl = fun toChapterImpl(): ChapterImpl =
ChapterImpl().apply { ChapterImpl().apply {
@@ -33,5 +35,6 @@ data class BackupChapter(
date_fetch = this@BackupChapter.dateFetch date_fetch = this@BackupChapter.dateFetch
date_upload = this@BackupChapter.dateUpload date_upload = this@BackupChapter.dateUpload
source_order = this@BackupChapter.sourceOrder source_order = this@BackupChapter.sourceOrder
meta = this@BackupChapter.meta
} }
} }

View File

@@ -38,6 +38,8 @@ data class BackupManga(
@ProtoNumber(103) var viewer_flags: Int? = null, @ProtoNumber(103) var viewer_flags: Int? = null,
@ProtoNumber(104) var history: List<BackupHistory> = emptyList(), @ProtoNumber(104) var history: List<BackupHistory> = emptyList(),
@ProtoNumber(105) var updateStrategy: UpdateStrategy = UpdateStrategy.ALWAYS_UPDATE, @ProtoNumber(105) var updateStrategy: UpdateStrategy = UpdateStrategy.ALWAYS_UPDATE,
// suwayomi
@ProtoNumber(9000) var meta: Map<String, String> = emptyMap(),
) { ) {
fun getMangaImpl(): MangaImpl = fun getMangaImpl(): MangaImpl =
MangaImpl().apply { MangaImpl().apply {
@@ -55,6 +57,7 @@ data class BackupManga(
viewer_flags = this@BackupManga.viewer_flags ?: this@BackupManga.viewer viewer_flags = this@BackupManga.viewer_flags ?: this@BackupManga.viewer
chapter_flags = this@BackupManga.chapterFlags chapter_flags = this@BackupManga.chapterFlags
update_strategy = this@BackupManga.updateStrategy update_strategy = this@BackupManga.updateStrategy
meta = this@BackupManga.meta
} }
fun getChaptersImpl(): List<ChapterImpl> = fun getChaptersImpl(): List<ChapterImpl> =

View File

@@ -0,0 +1,80 @@
package suwayomi.tachidesk.manga.impl.backup.proto.models
import kotlinx.serialization.Serializable
import kotlinx.serialization.protobuf.ProtoNumber
import org.jetbrains.exposed.sql.SortOrder
import suwayomi.tachidesk.graphql.types.Settings
import suwayomi.tachidesk.graphql.types.WebUIChannel
import suwayomi.tachidesk.graphql.types.WebUIFlavor
import suwayomi.tachidesk.graphql.types.WebUIInterface
@Serializable
data class BackupServerSettings(
@ProtoNumber(1) override var ip: String,
@ProtoNumber(2) override var port: Int,
// socks
@ProtoNumber(3) override var socksProxyEnabled: Boolean,
@ProtoNumber(4) override var socksProxyVersion: Int,
@ProtoNumber(5) override var socksProxyHost: String,
@ProtoNumber(6) override var socksProxyPort: String,
@ProtoNumber(7) override var socksProxyUsername: String,
@ProtoNumber(8) override var socksProxyPassword: String,
// webUI
@ProtoNumber(9) override var webUIFlavor: WebUIFlavor,
@ProtoNumber(10) override var initialOpenInBrowserEnabled: Boolean,
@ProtoNumber(11) override var webUIInterface: WebUIInterface,
@ProtoNumber(12) override var electronPath: String,
@ProtoNumber(13) override var webUIChannel: WebUIChannel,
@ProtoNumber(14) override var webUIUpdateCheckInterval: Double,
// downloader
@ProtoNumber(15) override var downloadAsCbz: Boolean,
@ProtoNumber(16) override var downloadsPath: String,
@ProtoNumber(17) override var autoDownloadNewChapters: Boolean,
@ProtoNumber(18) override var excludeEntryWithUnreadChapters: Boolean,
@ProtoNumber(19) override var autoDownloadAheadLimit: Int,
@ProtoNumber(20) override var autoDownloadNewChaptersLimit: Int,
@ProtoNumber(21) override var autoDownloadIgnoreReUploads: Boolean,
// extension
@ProtoNumber(22) override var extensionRepos: List<String>,
// requests
@ProtoNumber(23) override var maxSourcesInParallel: Int,
// updater
@ProtoNumber(24) override var excludeUnreadChapters: Boolean,
@ProtoNumber(25) override var excludeNotStarted: Boolean,
@ProtoNumber(26) override var excludeCompleted: Boolean,
@ProtoNumber(27) override var globalUpdateInterval: Double,
@ProtoNumber(28) override var updateMangas: Boolean,
// Authentication
@ProtoNumber(29) override var basicAuthEnabled: Boolean,
@ProtoNumber(30) override var basicAuthUsername: String,
@ProtoNumber(31) override var basicAuthPassword: String,
// misc
@ProtoNumber(32) override var debugLogsEnabled: Boolean,
@ProtoNumber(33) override var gqlDebugLogsEnabled: Boolean,
@ProtoNumber(34) override var systemTrayEnabled: Boolean,
@ProtoNumber(35) override var maxLogFiles: Int,
@ProtoNumber(36) override var maxLogFileSize: String,
@ProtoNumber(37) override var maxLogFolderSize: String,
// backup
@ProtoNumber(38) override var backupPath: String,
@ProtoNumber(39) override var backupTime: String,
@ProtoNumber(40) override var backupInterval: Int,
@ProtoNumber(41) override var backupTTL: Int,
// local source
@ProtoNumber(42) override var localSourcePath: String,
// cloudflare bypass
@ProtoNumber(43) override var flareSolverrEnabled: Boolean,
@ProtoNumber(44) override var flareSolverrUrl: String,
@ProtoNumber(45) override var flareSolverrTimeout: Int,
@ProtoNumber(46) override var flareSolverrSessionName: String,
@ProtoNumber(47) override var flareSolverrSessionTtl: Int,
@ProtoNumber(48) override var flareSolverrAsResponseFallback: Boolean,
// opds
@ProtoNumber(49) override var opdsUseBinaryFileSizes: Boolean,
@ProtoNumber(50) override var opdsItemsPerPage: Int,
@ProtoNumber(51) override var opdsEnablePageReadProgress: Boolean,
@ProtoNumber(52) override var opdsMarkAsReadOnDownload: Boolean,
@ProtoNumber(53) override var opdsShowOnlyUnreadChapters: Boolean,
@ProtoNumber(54) override var opdsShowOnlyDownloadedChapters: Boolean,
@ProtoNumber(55) override var opdsChapterSortOrder: SortOrder,
) : Settings

View File

@@ -8,6 +8,8 @@ import kotlinx.serialization.protobuf.ProtoNumber
data class BackupSource( data class BackupSource(
@ProtoNumber(1) var name: String = "", @ProtoNumber(1) var name: String = "",
@ProtoNumber(2) var sourceId: Long, @ProtoNumber(2) var sourceId: Long,
// suwayomi
@ProtoNumber(9000) var meta: Map<String, String> = emptyMap(),
) { ) {
companion object { companion object {
fun copyFrom(source: Source): BackupSource = fun copyFrom(source: Source): BackupSource =

View File

@@ -1,7 +1,5 @@
package suwayomi.tachidesk.server package suwayomi.tachidesk.server
import org.jetbrains.exposed.sql.SortOrder
interface ConfigAdapter<T> { interface ConfigAdapter<T> {
fun toType(configValue: String): T fun toType(configValue: String): T
} }
@@ -22,6 +20,8 @@ object DoubleConfigAdapter : ConfigAdapter<Double> {
override fun toType(configValue: String): Double = configValue.toDouble() override fun toType(configValue: String): Double = configValue.toDouble()
} }
object SortOrderConfigAdapter : ConfigAdapter<SortOrder> { class EnumConfigAdapter<T : Enum<T>>(
override fun toType(configValue: String): SortOrder = SortOrder.valueOf(configValue) private val enumClass: Class<T>,
) : ConfigAdapter<T> {
override fun toType(configValue: String): T = java.lang.Enum.valueOf(enumClass, configValue.uppercase())
} }

View File

@@ -24,6 +24,9 @@ import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.mapLatest import kotlinx.coroutines.flow.mapLatest
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import org.jetbrains.exposed.sql.SortOrder import org.jetbrains.exposed.sql.SortOrder
import suwayomi.tachidesk.graphql.types.WebUIChannel
import suwayomi.tachidesk.graphql.types.WebUIFlavor
import suwayomi.tachidesk.graphql.types.WebUIInterface
import xyz.nulldev.ts.config.GlobalConfigManager import xyz.nulldev.ts.config.GlobalConfigManager
import xyz.nulldev.ts.config.SystemPropertyOverridableConfigModule import xyz.nulldev.ts.config.SystemPropertyOverridableConfigModule
import kotlin.reflect.KProperty import kotlin.reflect.KProperty
@@ -99,11 +102,11 @@ class ServerConfig(
// webUI // webUI
val webUIEnabled: MutableStateFlow<Boolean> by OverrideConfigValue(BooleanConfigAdapter) val webUIEnabled: MutableStateFlow<Boolean> by OverrideConfigValue(BooleanConfigAdapter)
val webUIFlavor: MutableStateFlow<String> by OverrideConfigValue(StringConfigAdapter) val webUIFlavor: MutableStateFlow<WebUIFlavor> by OverrideConfigValue(EnumConfigAdapter(WebUIFlavor::class.java))
val initialOpenInBrowserEnabled: MutableStateFlow<Boolean> by OverrideConfigValue(BooleanConfigAdapter) val initialOpenInBrowserEnabled: MutableStateFlow<Boolean> by OverrideConfigValue(BooleanConfigAdapter)
val webUIInterface: MutableStateFlow<String> by OverrideConfigValue(StringConfigAdapter) val webUIInterface: MutableStateFlow<WebUIInterface> by OverrideConfigValue(EnumConfigAdapter(WebUIInterface::class.java))
val electronPath: MutableStateFlow<String> by OverrideConfigValue(StringConfigAdapter) val electronPath: MutableStateFlow<String> by OverrideConfigValue(StringConfigAdapter)
val webUIChannel: MutableStateFlow<String> by OverrideConfigValue(StringConfigAdapter) val webUIChannel: MutableStateFlow<WebUIChannel> by OverrideConfigValue(EnumConfigAdapter(WebUIChannel::class.java))
val webUIUpdateCheckInterval: MutableStateFlow<Double> by OverrideConfigValue(DoubleConfigAdapter) val webUIUpdateCheckInterval: MutableStateFlow<Double> by OverrideConfigValue(DoubleConfigAdapter)
// downloader // downloader
@@ -171,7 +174,7 @@ class ServerConfig(
val opdsMarkAsReadOnDownload: MutableStateFlow<Boolean> by OverrideConfigValue(BooleanConfigAdapter) val opdsMarkAsReadOnDownload: MutableStateFlow<Boolean> by OverrideConfigValue(BooleanConfigAdapter)
val opdsShowOnlyUnreadChapters: MutableStateFlow<Boolean> by OverrideConfigValue(BooleanConfigAdapter) val opdsShowOnlyUnreadChapters: MutableStateFlow<Boolean> by OverrideConfigValue(BooleanConfigAdapter)
val opdsShowOnlyDownloadedChapters: MutableStateFlow<Boolean> by OverrideConfigValue(BooleanConfigAdapter) val opdsShowOnlyDownloadedChapters: MutableStateFlow<Boolean> by OverrideConfigValue(BooleanConfigAdapter)
val opdsChapterSortOrder: MutableStateFlow<SortOrder> by OverrideConfigValue(SortOrderConfigAdapter) val opdsChapterSortOrder: MutableStateFlow<SortOrder> by OverrideConfigValue(EnumConfigAdapter(SortOrder::class.java))
@OptIn(ExperimentalCoroutinesApi::class) @OptIn(ExperimentalCoroutinesApi::class)
fun <T> subscribeTo( fun <T> subscribeTo(

View File

@@ -25,7 +25,7 @@ object Browser {
if (serverConfig.webUIEnabled.value) { if (serverConfig.webUIEnabled.value) {
val appBaseUrl = getAppBaseUrl() val appBaseUrl = getAppBaseUrl()
if (serverConfig.webUIInterface.value == WebUIInterface.ELECTRON.name.lowercase()) { if (serverConfig.webUIInterface.value == WebUIInterface.ELECTRON) {
try { try {
val electronPath = serverConfig.electronPath.value val electronPath = serverConfig.electronPath.value
electronInstances.add(ProcessBuilder(electronPath, appBaseUrl).start()) electronInstances.add(ProcessBuilder(electronPath, appBaseUrl).start())

View File

@@ -125,7 +125,7 @@ object WebInterfaceManager {
} }
return AboutWebUI( return AboutWebUI(
channel = WebUIChannel.from(serverConfig.webUIChannel.value), channel = serverConfig.webUIChannel.value,
tag = currentVersion, tag = currentVersion,
) )
} }
@@ -138,7 +138,7 @@ object WebInterfaceManager {
WebUIUpdateStatus( WebUIUpdateStatus(
info = info =
WebUIUpdateInfo( WebUIUpdateInfo(
channel = WebUIChannel.from(serverConfig.webUIChannel.value), channel = serverConfig.webUIChannel.value,
tag = version, tag = version,
), ),
state, state,
@@ -168,7 +168,7 @@ object WebInterfaceManager {
private fun scheduleWebUIUpdateCheck() { private fun scheduleWebUIUpdateCheck() {
HAScheduler.descheduleCron(currentUpdateTaskId) HAScheduler.descheduleCron(currentUpdateTaskId)
val isAutoUpdateDisabled = !isAutoUpdateEnabled() || serverConfig.webUIFlavor.value == WebUIFlavor.CUSTOM.uiName val isAutoUpdateDisabled = !isAutoUpdateEnabled() || serverConfig.webUIFlavor.value == WebUIFlavor.CUSTOM
if (isAutoUpdateDisabled) { if (isAutoUpdateDisabled) {
return return
} }
@@ -216,7 +216,7 @@ object WebInterfaceManager {
} }
suspend fun setupWebUI() { suspend fun setupWebUI() {
if (serverConfig.webUIFlavor.value == WebUIFlavor.CUSTOM.uiName) { if (serverConfig.webUIFlavor.value == WebUIFlavor.CUSTOM) {
return return
} }
@@ -320,7 +320,7 @@ object WebInterfaceManager {
if (!flavor.isDefault()) { if (!flavor.isDefault()) {
log.warn { "fallback to default webUI \"${WebUIFlavor.default.uiName}\"" } log.warn { "fallback to default webUI \"${WebUIFlavor.default.uiName}\"" }
serverConfig.webUIFlavor.value = WebUIFlavor.default.uiName serverConfig.webUIFlavor.value = WebUIFlavor.default
val fallbackToBundledVersion = !doDownload { getLatestCompatibleVersion(flavor) } val fallbackToBundledVersion = !doDownload { getLatestCompatibleVersion(flavor) }
if (!fallbackToBundledVersion) { if (!fallbackToBundledVersion) {
@@ -523,7 +523,7 @@ object WebInterfaceManager {
) )
private suspend fun getLatestCompatibleVersion(flavor: WebUIFlavor): String { private suspend fun getLatestCompatibleVersion(flavor: WebUIFlavor): String {
if (WebUIChannel.doesConfigChannelEqual(WebUIChannel.BUNDLED)) { if (serverConfig.webUIChannel.value == WebUIChannel.BUNDLED) {
logger.debug { "getLatestCompatibleVersion: Channel is \"${WebUIChannel.BUNDLED}\", do not check for update" } logger.debug { "getLatestCompatibleVersion: Channel is \"${WebUIChannel.BUNDLED}\", do not check for update" }
return BuildConfig.WEBUI_TAG return BuildConfig.WEBUI_TAG
} }
@@ -558,9 +558,9 @@ object WebInterfaceManager {
// is a STABLE webUI release, without a specified webUI version, which requires same handling as the PREVIEW release // is a STABLE webUI release, without a specified webUI version, which requires same handling as the PREVIEW release
val isUnknownStableVersion = webUIVersion == "STABLEPREVIEW" val isUnknownStableVersion = webUIVersion == "STABLEPREVIEW"
if (!WebUIChannel.doesConfigChannelEqual(WebUIChannel.from(webUIVersion))) { if (serverConfig.webUIChannel.value != WebUIChannel.from(webUIVersion)) {
// allow only STABLE versions for STABLE channel // allow only STABLE versions for STABLE channel
if (WebUIChannel.doesConfigChannelEqual(WebUIChannel.STABLE) && !isUnknownStableVersion) { if (serverConfig.webUIChannel.value == WebUIChannel.STABLE && !isUnknownStableVersion) {
continue continue
} }