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

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

View File

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

View File

@@ -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

View File

@@ -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)
}

View File

@@ -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

View File

@@ -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,
)
}
}

View File

@@ -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)
}

View File

@@ -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

View File

@@ -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(),
)

View File

@@ -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
}
}

View File

@@ -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> =

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(
@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 =