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:
@@ -1,9 +1,10 @@
|
||||
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.statements.BatchUpdateStatement
|
||||
import org.jetbrains.exposed.sql.transactions.transaction
|
||||
import org.jetbrains.exposed.sql.update
|
||||
import suwayomi.tachidesk.global.model.table.GlobalMetaTable
|
||||
|
||||
/*
|
||||
@@ -18,20 +19,32 @@ object GlobalMeta {
|
||||
key: String,
|
||||
value: String,
|
||||
) {
|
||||
transaction {
|
||||
val meta =
|
||||
transaction {
|
||||
GlobalMetaTable.selectAll().where { GlobalMetaTable.key eq key }
|
||||
}.firstOrNull()
|
||||
modifyMetas(mapOf(key to value))
|
||||
}
|
||||
|
||||
if (meta == null) {
|
||||
GlobalMetaTable.insert {
|
||||
it[GlobalMetaTable.key] = key
|
||||
it[GlobalMetaTable.value] = value
|
||||
fun modifyMetas(meta: Map<String, String>) {
|
||||
transaction {
|
||||
val dbMetaMap =
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,6 +64,8 @@ class BackupMutation {
|
||||
includeChapters = input?.includeChapters ?: true,
|
||||
includeTracking = true,
|
||||
includeHistory = true,
|
||||
includeClientData = true,
|
||||
includeServerSettings = true,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package suwayomi.tachidesk.graphql.mutations
|
||||
|
||||
import com.expediagroup.graphql.generator.annotations.GraphQLIgnore
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import suwayomi.tachidesk.graphql.types.PartialSettingsType
|
||||
import suwayomi.tachidesk.graphql.types.Settings
|
||||
@@ -110,7 +111,8 @@ class SettingsMutation {
|
||||
configSetting.value = newSetting
|
||||
}
|
||||
|
||||
private fun updateSettings(settings: Settings) {
|
||||
@GraphQLIgnore
|
||||
fun updateSettings(settings: Settings) {
|
||||
updateSetting(settings.ip, serverConfig.ip)
|
||||
updateSetting(settings.port, serverConfig.port)
|
||||
|
||||
@@ -123,11 +125,11 @@ class SettingsMutation {
|
||||
updateSetting(settings.socksProxyPassword, serverConfig.socksProxyPassword)
|
||||
|
||||
// webUI
|
||||
updateSetting(settings.webUIFlavor?.uiName, serverConfig.webUIFlavor)
|
||||
updateSetting(settings.webUIFlavor, serverConfig.webUIFlavor)
|
||||
updateSetting(settings.initialOpenInBrowserEnabled, serverConfig.initialOpenInBrowserEnabled)
|
||||
updateSetting(settings.webUIInterface?.name?.lowercase(), serverConfig.webUIInterface)
|
||||
updateSetting(settings.webUIInterface, serverConfig.webUIInterface)
|
||||
updateSetting(settings.electronPath, serverConfig.electronPath)
|
||||
updateSetting(settings.webUIChannel?.name?.lowercase(), serverConfig.webUIChannel)
|
||||
updateSetting(settings.webUIChannel, serverConfig.webUIChannel)
|
||||
updateSetting(settings.webUIUpdateCheckInterval, serverConfig.webUIUpdateCheckInterval)
|
||||
|
||||
// downloader
|
||||
@@ -182,6 +184,7 @@ class SettingsMutation {
|
||||
updateSetting(settings.flareSolverrAsResponseFallback, serverConfig.flareSolverrAsResponseFallback)
|
||||
|
||||
// opds
|
||||
updateSetting(settings.opdsUseBinaryFileSizes, serverConfig.opdsUseBinaryFileSizes)
|
||||
updateSetting(settings.opdsItemsPerPage, serverConfig.opdsItemsPerPage)
|
||||
updateSetting(settings.opdsEnablePageReadProgress, serverConfig.opdsEnablePageReadProgress)
|
||||
updateSetting(settings.opdsMarkAsReadOnDownload, serverConfig.opdsMarkAsReadOnDownload)
|
||||
|
||||
@@ -3,7 +3,6 @@ package suwayomi.tachidesk.graphql.queries
|
||||
import com.expediagroup.graphql.generator.annotations.GraphQLDeprecated
|
||||
import suwayomi.tachidesk.global.impl.AppUpdate
|
||||
import suwayomi.tachidesk.graphql.types.AboutWebUI
|
||||
import suwayomi.tachidesk.graphql.types.WebUIChannel
|
||||
import suwayomi.tachidesk.graphql.types.WebUIFlavor
|
||||
import suwayomi.tachidesk.graphql.types.WebUIUpdateCheck
|
||||
import suwayomi.tachidesk.graphql.types.WebUIUpdateStatus
|
||||
@@ -63,7 +62,7 @@ class InfoQuery {
|
||||
future {
|
||||
val (version, updateAvailable) = WebInterfaceManager.isUpdateAvailable(WebUIFlavor.current, raiseError = true)
|
||||
WebUIUpdateCheck(
|
||||
channel = WebUIChannel.from(serverConfig.webUIChannel.value),
|
||||
channel = serverConfig.webUIChannel.value,
|
||||
tag = version,
|
||||
updateAvailable,
|
||||
)
|
||||
|
||||
@@ -8,6 +8,8 @@ enum class BackupRestoreState {
|
||||
FAILURE,
|
||||
RESTORING_CATEGORIES,
|
||||
RESTORING_MANGA,
|
||||
RESTORING_META,
|
||||
RESTORING_SETTINGS,
|
||||
}
|
||||
|
||||
data class BackupRestoreStatus(
|
||||
@@ -40,7 +42,19 @@ fun ProtoBackupImport.BackupRestoreState.toStatus(): BackupRestoreStatus =
|
||||
BackupRestoreStatus(
|
||||
state = BackupRestoreState.RESTORING_CATEGORIES,
|
||||
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 ->
|
||||
BackupRestoreStatus(
|
||||
|
||||
@@ -95,6 +95,7 @@ interface Settings : Node {
|
||||
val flareSolverrAsResponseFallback: Boolean?
|
||||
|
||||
// opds
|
||||
val opdsUseBinaryFileSizes: Boolean?
|
||||
val opdsItemsPerPage: Int?
|
||||
val opdsEnablePageReadProgress: Boolean?
|
||||
val opdsMarkAsReadOnDownload: Boolean?
|
||||
@@ -169,6 +170,7 @@ data class PartialSettingsType(
|
||||
override val flareSolverrSessionTtl: Int?,
|
||||
override val flareSolverrAsResponseFallback: Boolean?,
|
||||
// opds
|
||||
override val opdsUseBinaryFileSizes: Boolean?,
|
||||
override val opdsItemsPerPage: Int?,
|
||||
override val opdsEnablePageReadProgress: Boolean?,
|
||||
override val opdsMarkAsReadOnDownload: Boolean?,
|
||||
@@ -243,6 +245,7 @@ class SettingsType(
|
||||
override val flareSolverrSessionTtl: Int,
|
||||
override val flareSolverrAsResponseFallback: Boolean,
|
||||
// opds
|
||||
override val opdsUseBinaryFileSizes: Boolean,
|
||||
override val opdsItemsPerPage: Int,
|
||||
override val opdsEnablePageReadProgress: Boolean,
|
||||
override val opdsMarkAsReadOnDownload: Boolean,
|
||||
@@ -261,11 +264,11 @@ class SettingsType(
|
||||
config.socksProxyUsername.value,
|
||||
config.socksProxyPassword.value,
|
||||
// webUI
|
||||
WebUIFlavor.from(config.webUIFlavor.value),
|
||||
config.webUIFlavor.value,
|
||||
config.initialOpenInBrowserEnabled.value,
|
||||
WebUIInterface.from(config.webUIInterface.value),
|
||||
config.webUIInterface.value,
|
||||
config.electronPath.value,
|
||||
WebUIChannel.from(config.webUIChannel.value),
|
||||
config.webUIChannel.value,
|
||||
config.webUIUpdateCheckInterval.value,
|
||||
// downloader
|
||||
config.downloadAsCbz.value,
|
||||
@@ -311,6 +314,7 @@ class SettingsType(
|
||||
config.flareSolverrSessionTtl.value,
|
||||
config.flareSolverrAsResponseFallback.value,
|
||||
// opds
|
||||
config.opdsUseBinaryFileSizes.value,
|
||||
config.opdsItemsPerPage.value,
|
||||
config.opdsEnablePageReadProgress.value,
|
||||
config.opdsMarkAsReadOnDownload.value,
|
||||
|
||||
@@ -34,11 +34,6 @@ data class WebUIUpdateStatus(
|
||||
enum class WebUIInterface {
|
||||
BROWSER,
|
||||
ELECTRON,
|
||||
;
|
||||
|
||||
companion object {
|
||||
fun from(value: String): WebUIInterface = entries.find { it.name.lowercase() == value.lowercase() } ?: BROWSER
|
||||
}
|
||||
}
|
||||
|
||||
enum class WebUIChannel {
|
||||
@@ -49,8 +44,6 @@ enum class WebUIChannel {
|
||||
|
||||
companion object {
|
||||
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
|
||||
|
||||
val current: WebUIFlavor
|
||||
get() = from(serverConfig.webUIFlavor.value)
|
||||
get() = serverConfig.webUIFlavor.value
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,6 +90,8 @@ object BackupController {
|
||||
includeChapters = true,
|
||||
includeTracking = true,
|
||||
includeHistory = true,
|
||||
includeClientData = true,
|
||||
includeServerSettings = true,
|
||||
),
|
||||
)
|
||||
}.thenApply { ctx.result(it) }
|
||||
@@ -122,6 +124,8 @@ object BackupController {
|
||||
includeChapters = true,
|
||||
includeTracking = true,
|
||||
includeHistory = true,
|
||||
includeClientData = true,
|
||||
includeServerSettings = true,
|
||||
),
|
||||
)
|
||||
}.thenApply { ctx.result(it) }
|
||||
|
||||
@@ -7,14 +7,15 @@ package suwayomi.tachidesk.manga.impl
|
||||
* 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/. */
|
||||
|
||||
import org.jetbrains.exposed.dao.id.EntityID
|
||||
import org.jetbrains.exposed.sql.SortOrder
|
||||
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
|
||||
import org.jetbrains.exposed.sql.and
|
||||
import org.jetbrains.exposed.sql.andWhere
|
||||
import org.jetbrains.exposed.sql.batchInsert
|
||||
import org.jetbrains.exposed.sql.deleteWhere
|
||||
import org.jetbrains.exposed.sql.insert
|
||||
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.update
|
||||
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.MangaTable
|
||||
import suwayomi.tachidesk.manga.model.table.toDataClass
|
||||
import kotlin.collections.component1
|
||||
import kotlin.collections.orEmpty
|
||||
|
||||
object Category {
|
||||
/**
|
||||
@@ -193,26 +196,72 @@ object Category {
|
||||
.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(
|
||||
categoryId: Int,
|
||||
key: String,
|
||||
value: String,
|
||||
) {
|
||||
transaction {
|
||||
val meta =
|
||||
transaction {
|
||||
CategoryMetaTable.selectAll().where { (CategoryMetaTable.ref eq categoryId) and (CategoryMetaTable.key eq key) }
|
||||
}.firstOrNull()
|
||||
modifyCategoriesMetas(mapOf(categoryId to mapOf(key to value)))
|
||||
}
|
||||
|
||||
if (meta == null) {
|
||||
CategoryMetaTable.insert {
|
||||
it[CategoryMetaTable.key] = key
|
||||
it[CategoryMetaTable.value] = value
|
||||
it[CategoryMetaTable.ref] = categoryId
|
||||
fun modifyCategoriesMetas(metaByCategoryId: Map<Int, Map<String, String>>) {
|
||||
transaction {
|
||||
val categoryIds = metaByCategoryId.keys
|
||||
val metaKeys = metaByCategoryId.flatMap { it.value.keys }
|
||||
|
||||
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) }) {
|
||||
it[CategoryMetaTable.value] = value
|
||||
|
||||
val newMetaByCategoryId =
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,7 +24,6 @@ import org.jetbrains.exposed.sql.SqlExpressionBuilder.inList
|
||||
import org.jetbrains.exposed.sql.and
|
||||
import org.jetbrains.exposed.sql.batchInsert
|
||||
import org.jetbrains.exposed.sql.deleteWhere
|
||||
import org.jetbrains.exposed.sql.insert
|
||||
import org.jetbrains.exposed.sql.selectAll
|
||||
import org.jetbrains.exposed.sql.statements.BatchUpdateStatement
|
||||
import org.jetbrains.exposed.sql.transactions.transaction
|
||||
@@ -102,7 +101,7 @@ object Chapter {
|
||||
}
|
||||
|
||||
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 ->
|
||||
|
||||
@@ -126,7 +125,7 @@ object Chapter {
|
||||
downloaded = dbChapter[ChapterTable.isDownloaded],
|
||||
pageCount = dbChapter[ChapterTable.pageCount],
|
||||
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 {
|
||||
ChapterMetaTable
|
||||
.selectAll()
|
||||
.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] } }
|
||||
.withDefault { emptyMap() }
|
||||
}
|
||||
@@ -593,22 +592,57 @@ object Chapter {
|
||||
key: String,
|
||||
value: String,
|
||||
) {
|
||||
modifyChaptersMetas(mapOf(chapterId to mapOf(key to value)))
|
||||
}
|
||||
|
||||
fun modifyChaptersMetas(metaByChapterId: Map<Int, Map<String, String>>) {
|
||||
transaction {
|
||||
val meta =
|
||||
val chapterIds = metaByChapterId.keys
|
||||
val metaKeys = metaByChapterId.flatMap { it.value.keys }
|
||||
|
||||
val dbMetaByChapterId =
|
||||
ChapterMetaTable
|
||||
.selectAll()
|
||||
.where { (ChapterMetaTable.ref eq chapterId) and (ChapterMetaTable.key eq key) }
|
||||
.firstOrNull()
|
||||
.where { (ChapterMetaTable.ref inList chapterIds) and (ChapterMetaTable.key inList metaKeys) }
|
||||
.groupBy { it[ChapterMetaTable.ref].value }
|
||||
|
||||
if (meta == null) {
|
||||
ChapterMetaTable.insert {
|
||||
it[ChapterMetaTable.key] = key
|
||||
it[ChapterMetaTable.value] = value
|
||||
it[ref] = chapterId
|
||||
val existingMetaByMetaId =
|
||||
chapterIds.flatMap { chapterId ->
|
||||
val dbMetaByKey = dbMetaByChapterId[chapterId].orEmpty().associateBy { it[ChapterMetaTable.key] }
|
||||
val existingMetas = metaByChapterId[chapterId].orEmpty().filter { (key) -> key in dbMetaByKey.keys }
|
||||
|
||||
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) }) {
|
||||
it[ChapterMetaTable.value] = value
|
||||
|
||||
val newMetaByChapterId =
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,11 +20,13 @@ import io.github.oshai.kotlinlogging.KotlinLogging
|
||||
import io.javalin.http.HttpStatus
|
||||
import okhttp3.CacheControl
|
||||
import okhttp3.Response
|
||||
import org.jetbrains.exposed.dao.id.EntityID
|
||||
import org.jetbrains.exposed.sql.ResultRow
|
||||
import org.jetbrains.exposed.sql.SortOrder
|
||||
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.statements.BatchUpdateStatement
|
||||
import org.jetbrains.exposed.sql.transactions.transaction
|
||||
import org.jetbrains.exposed.sql.update
|
||||
import suwayomi.tachidesk.manga.impl.MangaList.proxyThumbnailUrl
|
||||
@@ -256,22 +258,57 @@ object Manga {
|
||||
key: String,
|
||||
value: String,
|
||||
) {
|
||||
modifyMangasMetas(mapOf(mangaId to mapOf(key to value)))
|
||||
}
|
||||
|
||||
fun modifyMangasMetas(metaByMangaId: Map<Int, Map<String, String>>) {
|
||||
transaction {
|
||||
val meta =
|
||||
val mangaIds = metaByMangaId.keys
|
||||
val metaKeys = metaByMangaId.flatMap { it.value.keys }
|
||||
|
||||
val dbMetaByMangaId =
|
||||
MangaMetaTable
|
||||
.selectAll()
|
||||
.where { (MangaMetaTable.ref eq mangaId) and (MangaMetaTable.key eq key) }
|
||||
.firstOrNull()
|
||||
.where { (MangaMetaTable.ref inList mangaIds) and (MangaMetaTable.key inList metaKeys) }
|
||||
.groupBy { it[MangaMetaTable.ref].value }
|
||||
|
||||
if (meta == null) {
|
||||
MangaMetaTable.insert {
|
||||
it[MangaMetaTable.key] = key
|
||||
it[MangaMetaTable.value] = value
|
||||
it[MangaMetaTable.ref] = mangaId
|
||||
val existingMetaByMetaId =
|
||||
mangaIds.flatMap { mangaId ->
|
||||
val metaByKey = dbMetaByMangaId[mangaId].orEmpty().associateBy { it[MangaMetaTable.key] }
|
||||
val existingMetas = metaByMangaId[mangaId].orEmpty().filter { (key) -> key in metaByKey.keys }
|
||||
|
||||
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) }) {
|
||||
it[MangaMetaTable.value] = value
|
||||
|
||||
val newMetaByMangaId =
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,11 +14,12 @@ import eu.kanade.tachiyomi.source.sourcePreferences
|
||||
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||
import io.javalin.json.JsonMapper
|
||||
import io.javalin.json.fromJsonString
|
||||
import org.jetbrains.exposed.dao.id.EntityID
|
||||
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.statements.BatchUpdateStatement
|
||||
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.util.source.GetCatalogueSource.getCatalogueSourceOrNull
|
||||
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.SourceMetaTable
|
||||
import suwayomi.tachidesk.manga.model.table.SourceTable
|
||||
import uy.kohesive.injekt.api.get
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import xyz.nulldev.androidcompat.androidimpl.CustomContext
|
||||
|
||||
@@ -151,26 +151,72 @@ object Source {
|
||||
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(
|
||||
sourceId: Long,
|
||||
key: String,
|
||||
value: String,
|
||||
) {
|
||||
transaction {
|
||||
val meta =
|
||||
transaction {
|
||||
SourceMetaTable.selectAll().where { (SourceMetaTable.ref eq sourceId) and (SourceMetaTable.key eq key) }
|
||||
}.firstOrNull()
|
||||
modifySourceMetas(mapOf(sourceId to mapOf(key to value)))
|
||||
}
|
||||
|
||||
if (meta == null) {
|
||||
SourceMetaTable.insert {
|
||||
it[SourceMetaTable.key] = key
|
||||
it[SourceMetaTable.value] = value
|
||||
it[SourceMetaTable.ref] = sourceId
|
||||
fun modifySourceMetas(metaBySourceIds: Map<Long, Map<String, String>>) {
|
||||
transaction {
|
||||
val sourceIds = metaBySourceIds.keys
|
||||
val metaKeys = metaBySourceIds.flatMap { it.value.keys }
|
||||
|
||||
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) }) {
|
||||
it[SourceMetaTable.value] = value
|
||||
|
||||
val newMetaBySourceId =
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,4 +13,6 @@ data class BackupFlags(
|
||||
val includeChapters: Boolean,
|
||||
val includeTracking: Boolean,
|
||||
val includeHistory: Boolean,
|
||||
val includeClientData: Boolean,
|
||||
val includeServerSettings: Boolean,
|
||||
)
|
||||
|
||||
@@ -22,6 +22,8 @@ interface Chapter :
|
||||
|
||||
var source_order: Int
|
||||
|
||||
var meta: Map<String, String>
|
||||
|
||||
val isRecognizedNumber: Boolean
|
||||
get() = chapter_number >= 0f
|
||||
}
|
||||
|
||||
@@ -27,6 +27,8 @@ class ChapterImpl : Chapter {
|
||||
|
||||
override var source_order: Int = 0
|
||||
|
||||
override var meta: Map<String, String> = emptyMap()
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (other == null || javaClass != other.javaClass) return false
|
||||
|
||||
@@ -37,6 +37,8 @@ interface Manga : SManga {
|
||||
|
||||
var cover_last_modified: Long
|
||||
|
||||
var meta: Map<String, String>
|
||||
|
||||
fun setChapterOrder(order: Int) {
|
||||
setChapterFlags(order, CHAPTER_SORT_MASK)
|
||||
}
|
||||
|
||||
@@ -37,6 +37,8 @@ open class MangaImpl : Manga {
|
||||
|
||||
override var initialized: Boolean = false
|
||||
|
||||
override var meta: Map<String, String> = emptyMap()
|
||||
|
||||
/** Reader mode value
|
||||
* ref: https://github.com/tachiyomiorg/tachiyomi/blob/ff369010074b058bb734ce24c66508300e6e9ac6/app/src/main/java/eu/kanade/tachiyomi/ui/reader/setting/ReadingModeType.kt#L8
|
||||
* 0 -> Default
|
||||
|
||||
+149
-27
@@ -24,13 +24,18 @@ import org.jetbrains.exposed.sql.Query
|
||||
import org.jetbrains.exposed.sql.SortOrder
|
||||
import org.jetbrains.exposed.sql.selectAll
|
||||
import org.jetbrains.exposed.sql.transactions.transaction
|
||||
import suwayomi.tachidesk.global.impl.GlobalMeta
|
||||
import suwayomi.tachidesk.manga.impl.Category
|
||||
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.proto.models.Backup
|
||||
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.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.track.Track
|
||||
@@ -123,6 +128,8 @@ object ProtoBackupExport : ProtoBackupBase() {
|
||||
includeChapters = true,
|
||||
includeTracking = true,
|
||||
includeHistory = true,
|
||||
includeClientData = true,
|
||||
includeServerSettings = true,
|
||||
),
|
||||
).use { input ->
|
||||
val automatedBackupDir = File(applicationDirs.automatedBackupRoot)
|
||||
@@ -181,8 +188,10 @@ object ProtoBackupExport : ProtoBackupBase() {
|
||||
transaction {
|
||||
Backup(
|
||||
backupManga(databaseManga, flags),
|
||||
backupCategories(),
|
||||
backupExtensionInfo(databaseManga),
|
||||
backupCategories(flags),
|
||||
backupExtensionInfo(databaseManga, flags),
|
||||
backupGlobalMeta(flags),
|
||||
backupServerSettings(flags),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -220,6 +229,10 @@ object ProtoBackupExport : ProtoBackupBase() {
|
||||
|
||||
val mangaId = mangaRow[MangaTable.id].value
|
||||
|
||||
if (flags.includeClientData) {
|
||||
backupManga.meta = Manga.getMangaMetaMap(mangaId)
|
||||
}
|
||||
|
||||
if (flags.includeChapters) {
|
||||
val chapters =
|
||||
transaction {
|
||||
@@ -231,6 +244,7 @@ object ProtoBackupExport : ProtoBackupBase() {
|
||||
ChapterTable.toDataClass(it)
|
||||
}
|
||||
}
|
||||
val chapterToMeta = Chapter.getChaptersMetaMaps(chapters.map { it.id })
|
||||
|
||||
backupManga.chapters =
|
||||
chapters.map {
|
||||
@@ -245,7 +259,11 @@ object ProtoBackupExport : ProtoBackupBase() {
|
||||
it.uploadDate,
|
||||
it.chapterNumber,
|
||||
chapters.size - it.index,
|
||||
)
|
||||
).apply {
|
||||
if (flags.includeClientData) {
|
||||
this.meta = chapterToMeta[it.id] ?: emptyMap()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -287,31 +305,135 @@ object ProtoBackupExport : ProtoBackupBase() {
|
||||
backupManga
|
||||
}
|
||||
|
||||
private fun backupCategories(): List<BackupCategory> =
|
||||
CategoryTable
|
||||
.selectAll()
|
||||
.orderBy(CategoryTable.order to SortOrder.ASC)
|
||||
.map {
|
||||
CategoryTable.toDataClass(it)
|
||||
}.filter { it.id != Category.DEFAULT_CATEGORY_ID }
|
||||
.map {
|
||||
BackupCategory(
|
||||
it.name,
|
||||
it.order,
|
||||
0, // not supported in Tachidesk
|
||||
)
|
||||
}
|
||||
private fun backupCategories(flags: BackupFlags): List<BackupCategory> {
|
||||
val categories =
|
||||
CategoryTable
|
||||
.selectAll()
|
||||
.orderBy(CategoryTable.order to SortOrder.ASC)
|
||||
.map { CategoryTable.toDataClass(it) }
|
||||
val categoryToMeta = Category.getCategoriesMetaMaps(categories.map { it.id })
|
||||
|
||||
private fun backupExtensionInfo(mangas: Query): List<BackupSource> =
|
||||
mangas
|
||||
.asSequence()
|
||||
.map { it[MangaTable.sourceReference] }
|
||||
.distinct()
|
||||
.map {
|
||||
val sourceRow = SourceTable.selectAll().where { SourceTable.id eq it }.firstOrNull()
|
||||
return categories.map {
|
||||
BackupCategory(
|
||||
it.name,
|
||||
it.order,
|
||||
0, // not supported in Tachidesk
|
||||
).apply {
|
||||
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(
|
||||
sourceRow?.get(SourceTable.name) ?: "",
|
||||
it,
|
||||
)
|
||||
source?.get(SourceTable.name) ?: "",
|
||||
mangaSourceId,
|
||||
).apply {
|
||||
if (flags.includeClientData) {
|
||||
this.meta = sourceToMeta[mangaSourceId] ?: emptyMap()
|
||||
}
|
||||
}
|
||||
}.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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+102
-21
@@ -30,10 +30,16 @@ 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.update
|
||||
import suwayomi.tachidesk.global.impl.GlobalMeta
|
||||
import suwayomi.tachidesk.graphql.mutations.SettingsMutation
|
||||
import suwayomi.tachidesk.graphql.types.toStatus
|
||||
import suwayomi.tachidesk.manga.impl.Category
|
||||
import suwayomi.tachidesk.manga.impl.Category.modifyCategoriesMetas
|
||||
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.modifyMangasMetas
|
||||
import suwayomi.tachidesk.manga.impl.Source.modifySourceMetas
|
||||
import suwayomi.tachidesk.manga.impl.backup.models.Chapter
|
||||
import suwayomi.tachidesk.manga.impl.backup.models.Manga
|
||||
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.BackupHistory
|
||||
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.track.tracker.TrackerManager
|
||||
import suwayomi.tachidesk.manga.impl.track.tracker.model.toTrack
|
||||
@@ -82,6 +90,17 @@ object ProtoBackupImport : ProtoBackupBase() {
|
||||
data object Failure : BackupRestoreState()
|
||||
|
||||
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,
|
||||
) : BackupRestoreState()
|
||||
|
||||
@@ -177,12 +196,29 @@ object ProtoBackupImport : ProtoBackupBase() {
|
||||
|
||||
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)
|
||||
|
||||
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
|
||||
sourceMapping = backup.getSourceMap()
|
||||
|
||||
@@ -191,8 +227,8 @@ object ProtoBackupImport : ProtoBackupBase() {
|
||||
updateRestoreState(
|
||||
id,
|
||||
BackupRestoreState.RestoringManga(
|
||||
current = index + 1,
|
||||
totalManga = backup.backupManga.size,
|
||||
current = getRestoreAmount(index + 1),
|
||||
totalManga = restoreAmount,
|
||||
title = manga.title,
|
||||
),
|
||||
)
|
||||
@@ -225,6 +261,15 @@ object ProtoBackupImport : ProtoBackupBase() {
|
||||
private fun restoreCategories(backupCategories: List<BackupCategory>): Map<Int, Int> {
|
||||
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) ->
|
||||
backupCategory.order to categoryIds[index]
|
||||
}
|
||||
@@ -318,6 +363,10 @@ object ProtoBackupImport : ProtoBackupBase() {
|
||||
// delete thumbnail in case cached data still exists
|
||||
clearThumbnail(mangaId)
|
||||
|
||||
if (manga.meta.isNotEmpty()) {
|
||||
modifyMangasMetas(mapOf(mangaId to manga.meta))
|
||||
}
|
||||
|
||||
// merge chapter data
|
||||
restoreMangaChapterData(mangaId, restoreMode, chapters)
|
||||
|
||||
@@ -358,26 +407,28 @@ object ProtoBackupImport : ProtoBackupBase() {
|
||||
) = dbTransaction {
|
||||
val (chaptersToInsert, chaptersToUpdateToDbChapter) = getMangaChapterToRestoreInfo(mangaId, restoreMode, chapters)
|
||||
|
||||
ChapterTable.batchInsert(chaptersToInsert) { chapter ->
|
||||
this[ChapterTable.url] = chapter.url
|
||||
this[ChapterTable.name] = chapter.name
|
||||
if (chapter.date_upload == 0L) {
|
||||
this[ChapterTable.date_upload] = chapter.date_fetch
|
||||
} else {
|
||||
this[ChapterTable.date_upload] = chapter.date_upload
|
||||
}
|
||||
this[ChapterTable.chapter_number] = chapter.chapter_number
|
||||
this[ChapterTable.scanlator] = chapter.scanlator
|
||||
val insertedChapterIds =
|
||||
ChapterTable
|
||||
.batchInsert(chaptersToInsert) { chapter ->
|
||||
this[ChapterTable.url] = chapter.url
|
||||
this[ChapterTable.name] = chapter.name
|
||||
if (chapter.date_upload == 0L) {
|
||||
this[ChapterTable.date_upload] = chapter.date_fetch
|
||||
} else {
|
||||
this[ChapterTable.date_upload] = chapter.date_upload
|
||||
}
|
||||
this[ChapterTable.chapter_number] = chapter.chapter_number
|
||||
this[ChapterTable.scanlator] = chapter.scanlator
|
||||
|
||||
this[ChapterTable.sourceOrder] = chaptersToInsert.size - chapter.source_order
|
||||
this[ChapterTable.manga] = mangaId
|
||||
this[ChapterTable.sourceOrder] = chaptersToInsert.size - chapter.source_order
|
||||
this[ChapterTable.manga] = mangaId
|
||||
|
||||
this[ChapterTable.isRead] = chapter.read
|
||||
this[ChapterTable.lastPageRead] = chapter.last_page_read.coerceAtLeast(0)
|
||||
this[ChapterTable.isBookmarked] = chapter.bookmark
|
||||
this[ChapterTable.isRead] = chapter.read
|
||||
this[ChapterTable.lastPageRead] = chapter.last_page_read.coerceAtLeast(0)
|
||||
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()) {
|
||||
BatchUpdateStatement(ChapterTable).apply {
|
||||
@@ -391,6 +442,20 @@ object ProtoBackupImport : ProtoBackupBase() {
|
||||
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(
|
||||
@@ -440,5 +505,21 @@ object ProtoBackupImport : ProtoBackupBase() {
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -13,6 +13,9 @@ data class Backup(
|
||||
// Bump by 100 to specify this is a 0.x value
|
||||
// @ProtoNumber(100) var brokenBackupSources: List<BrokenBackupSource> = 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> =
|
||||
backupSources
|
||||
|
||||
+2
@@ -10,4 +10,6 @@ class BackupCategory(
|
||||
// @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
|
||||
@ProtoNumber(100) var flags: Int = 0,
|
||||
// suwayomi
|
||||
@ProtoNumber(9000) var meta: Map<String, String> = emptyMap(),
|
||||
)
|
||||
|
||||
+3
@@ -20,6 +20,8 @@ data class BackupChapter(
|
||||
// chapterNumber is called number is 1.x
|
||||
@ProtoNumber(9) var chapterNumber: Float = 0F,
|
||||
@ProtoNumber(10) var sourceOrder: Int = 0,
|
||||
// suwayomi
|
||||
@ProtoNumber(9000) var meta: Map<String, String> = emptyMap(),
|
||||
) {
|
||||
fun toChapterImpl(): ChapterImpl =
|
||||
ChapterImpl().apply {
|
||||
@@ -33,5 +35,6 @@ data class BackupChapter(
|
||||
date_fetch = this@BackupChapter.dateFetch
|
||||
date_upload = this@BackupChapter.dateUpload
|
||||
source_order = this@BackupChapter.sourceOrder
|
||||
meta = this@BackupChapter.meta
|
||||
}
|
||||
}
|
||||
|
||||
+3
@@ -38,6 +38,8 @@ data class BackupManga(
|
||||
@ProtoNumber(103) var viewer_flags: Int? = null,
|
||||
@ProtoNumber(104) var history: List<BackupHistory> = emptyList(),
|
||||
@ProtoNumber(105) var updateStrategy: UpdateStrategy = UpdateStrategy.ALWAYS_UPDATE,
|
||||
// suwayomi
|
||||
@ProtoNumber(9000) var meta: Map<String, String> = emptyMap(),
|
||||
) {
|
||||
fun getMangaImpl(): MangaImpl =
|
||||
MangaImpl().apply {
|
||||
@@ -55,6 +57,7 @@ data class BackupManga(
|
||||
viewer_flags = this@BackupManga.viewer_flags ?: this@BackupManga.viewer
|
||||
chapter_flags = this@BackupManga.chapterFlags
|
||||
update_strategy = this@BackupManga.updateStrategy
|
||||
meta = this@BackupManga.meta
|
||||
}
|
||||
|
||||
fun getChaptersImpl(): List<ChapterImpl> =
|
||||
|
||||
+80
@@ -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
|
||||
+2
@@ -8,6 +8,8 @@ import kotlinx.serialization.protobuf.ProtoNumber
|
||||
data class BackupSource(
|
||||
@ProtoNumber(1) var name: String = "",
|
||||
@ProtoNumber(2) var sourceId: Long,
|
||||
// suwayomi
|
||||
@ProtoNumber(9000) var meta: Map<String, String> = emptyMap(),
|
||||
) {
|
||||
companion object {
|
||||
fun copyFrom(source: Source): BackupSource =
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
package suwayomi.tachidesk.server
|
||||
|
||||
import org.jetbrains.exposed.sql.SortOrder
|
||||
|
||||
interface ConfigAdapter<T> {
|
||||
fun toType(configValue: String): T
|
||||
}
|
||||
@@ -22,6 +20,8 @@ object DoubleConfigAdapter : ConfigAdapter<Double> {
|
||||
override fun toType(configValue: String): Double = configValue.toDouble()
|
||||
}
|
||||
|
||||
object SortOrderConfigAdapter : ConfigAdapter<SortOrder> {
|
||||
override fun toType(configValue: String): SortOrder = SortOrder.valueOf(configValue)
|
||||
class EnumConfigAdapter<T : Enum<T>>(
|
||||
private val enumClass: Class<T>,
|
||||
) : ConfigAdapter<T> {
|
||||
override fun toType(configValue: String): T = java.lang.Enum.valueOf(enumClass, configValue.uppercase())
|
||||
}
|
||||
|
||||
@@ -24,6 +24,9 @@ import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.mapLatest
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
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.SystemPropertyOverridableConfigModule
|
||||
import kotlin.reflect.KProperty
|
||||
@@ -99,11 +102,11 @@ class ServerConfig(
|
||||
|
||||
// webUI
|
||||
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 webUIInterface: MutableStateFlow<String> by OverrideConfigValue(StringConfigAdapter)
|
||||
val webUIInterface: MutableStateFlow<WebUIInterface> by OverrideConfigValue(EnumConfigAdapter(WebUIInterface::class.java))
|
||||
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)
|
||||
|
||||
// downloader
|
||||
@@ -171,7 +174,7 @@ class ServerConfig(
|
||||
val opdsMarkAsReadOnDownload: MutableStateFlow<Boolean> by OverrideConfigValue(BooleanConfigAdapter)
|
||||
val opdsShowOnlyUnreadChapters: 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)
|
||||
fun <T> subscribeTo(
|
||||
|
||||
@@ -25,7 +25,7 @@ object Browser {
|
||||
if (serverConfig.webUIEnabled.value) {
|
||||
val appBaseUrl = getAppBaseUrl()
|
||||
|
||||
if (serverConfig.webUIInterface.value == WebUIInterface.ELECTRON.name.lowercase()) {
|
||||
if (serverConfig.webUIInterface.value == WebUIInterface.ELECTRON) {
|
||||
try {
|
||||
val electronPath = serverConfig.electronPath.value
|
||||
electronInstances.add(ProcessBuilder(electronPath, appBaseUrl).start())
|
||||
|
||||
@@ -125,7 +125,7 @@ object WebInterfaceManager {
|
||||
}
|
||||
|
||||
return AboutWebUI(
|
||||
channel = WebUIChannel.from(serverConfig.webUIChannel.value),
|
||||
channel = serverConfig.webUIChannel.value,
|
||||
tag = currentVersion,
|
||||
)
|
||||
}
|
||||
@@ -138,7 +138,7 @@ object WebInterfaceManager {
|
||||
WebUIUpdateStatus(
|
||||
info =
|
||||
WebUIUpdateInfo(
|
||||
channel = WebUIChannel.from(serverConfig.webUIChannel.value),
|
||||
channel = serverConfig.webUIChannel.value,
|
||||
tag = version,
|
||||
),
|
||||
state,
|
||||
@@ -168,7 +168,7 @@ object WebInterfaceManager {
|
||||
private fun scheduleWebUIUpdateCheck() {
|
||||
HAScheduler.descheduleCron(currentUpdateTaskId)
|
||||
|
||||
val isAutoUpdateDisabled = !isAutoUpdateEnabled() || serverConfig.webUIFlavor.value == WebUIFlavor.CUSTOM.uiName
|
||||
val isAutoUpdateDisabled = !isAutoUpdateEnabled() || serverConfig.webUIFlavor.value == WebUIFlavor.CUSTOM
|
||||
if (isAutoUpdateDisabled) {
|
||||
return
|
||||
}
|
||||
@@ -216,7 +216,7 @@ object WebInterfaceManager {
|
||||
}
|
||||
|
||||
suspend fun setupWebUI() {
|
||||
if (serverConfig.webUIFlavor.value == WebUIFlavor.CUSTOM.uiName) {
|
||||
if (serverConfig.webUIFlavor.value == WebUIFlavor.CUSTOM) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -320,7 +320,7 @@ object WebInterfaceManager {
|
||||
if (!flavor.isDefault()) {
|
||||
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) }
|
||||
if (!fallbackToBundledVersion) {
|
||||
@@ -523,7 +523,7 @@ object WebInterfaceManager {
|
||||
)
|
||||
|
||||
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" }
|
||||
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
|
||||
val isUnknownStableVersion = webUIVersion == "STABLEPREVIEW"
|
||||
|
||||
if (!WebUIChannel.doesConfigChannelEqual(WebUIChannel.from(webUIVersion))) {
|
||||
if (serverConfig.webUIChannel.value != WebUIChannel.from(webUIVersion)) {
|
||||
// allow only STABLE versions for STABLE channel
|
||||
if (WebUIChannel.doesConfigChannelEqual(WebUIChannel.STABLE) && !isUnknownStableVersion) {
|
||||
if (serverConfig.webUIChannel.value == WebUIChannel.STABLE && !isUnknownStableVersion) {
|
||||
continue
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user