Fix new private parameter in tracking backup (#1463)

* Fix new private parameter in tracking backup

* Cleanup uneeded models
This commit is contained in:
Mitchell Syer
2025-06-22 20:17:31 -04:00
committed by GitHub
parent b54dc6f967
commit ee9de376a3
12 changed files with 33 additions and 511 deletions

View File

@@ -1,29 +0,0 @@
@file:Suppress("ktlint:standard:property-naming")
package suwayomi.tachidesk.manga.impl.backup.models
import eu.kanade.tachiyomi.source.model.SChapter
import java.io.Serializable
interface Chapter :
SChapter,
Serializable {
var id: Long?
var manga_id: Long?
var read: Boolean
var bookmark: Boolean
var last_page_read: Int
var date_fetch: Long
var source_order: Int
var meta: Map<String, String>
val isRecognizedNumber: Boolean
get() = chapter_number >= 0f
}

View File

@@ -1,42 +0,0 @@
@file:Suppress("ktlint:standard:property-naming")
package suwayomi.tachidesk.manga.impl.backup.models
class ChapterImpl : Chapter {
override var id: Long? = null
override var manga_id: Long? = null
override lateinit var url: String
override lateinit var name: String
override var scanlator: String? = null
override var read: Boolean = false
override var bookmark: Boolean = false
override var last_page_read: Int = 0
override var date_fetch: Long = 0
override var date_upload: Long = 0
override var chapter_number: Float = 0f
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
val chapter = other as Chapter
if (url != chapter.url) return false
return id == chapter.id
}
override fun hashCode(): Int = url.hashCode() + id.hashCode()
}

View File

@@ -1,135 +0,0 @@
@file:Suppress("ktlint:standard:property-naming")
package suwayomi.tachidesk.manga.impl.backup.models
import eu.kanade.tachiyomi.source.model.SManga
// import eu.kanade.tachiyomi.ui.reader.setting.OrientationType
// import eu.kanade.tachiyomi.ui.reader.setting.ReadingModeType
// substitute for eu.kanade.tachiyomi.ui.reader.setting.OrientationType
object OrientationType {
const val MASK = 0x00000038
}
// substitute for eu.kanade.tachiyomi.ui.reader.setting.ReadingModeType
object ReadingModeType {
const val MASK = 0x00000007
}
interface Manga : SManga {
var id: Long?
var source: Long
var favorite: Boolean
// last time the chapter list changed in any way
var last_update: Long
// predicted next update time based on latest (by date) 4 chapters' deltas
var next_update: Long
var date_added: Long
var viewer_flags: Int
var chapter_flags: Int
var cover_last_modified: Long
var meta: Map<String, String>
fun setChapterOrder(order: Int) {
setChapterFlags(order, CHAPTER_SORT_MASK)
}
fun sortDescending(): Boolean = chapter_flags and CHAPTER_SORT_MASK == CHAPTER_SORT_DESC
fun getGenres(): List<String>? = genre?.split(", ")?.map { it.trim() }
private fun setChapterFlags(
flag: Int,
mask: Int,
) {
chapter_flags = chapter_flags and mask.inv() or (flag and mask)
}
private fun setViewerFlags(
flag: Int,
mask: Int,
) {
viewer_flags = viewer_flags and mask.inv() or (flag and mask)
}
// Used to display the chapter's title one way or another
var displayMode: Int
get() = chapter_flags and CHAPTER_DISPLAY_MASK
set(mode) = setChapterFlags(mode, CHAPTER_DISPLAY_MASK)
var readFilter: Int
get() = chapter_flags and CHAPTER_READ_MASK
set(filter) = setChapterFlags(filter, CHAPTER_READ_MASK)
var downloadedFilter: Int
get() = chapter_flags and CHAPTER_DOWNLOADED_MASK
set(filter) = setChapterFlags(filter, CHAPTER_DOWNLOADED_MASK)
var bookmarkedFilter: Int
get() = chapter_flags and CHAPTER_BOOKMARKED_MASK
set(filter) = setChapterFlags(filter, CHAPTER_BOOKMARKED_MASK)
var sorting: Int
get() = chapter_flags and CHAPTER_SORTING_MASK
set(sort) = setChapterFlags(sort, CHAPTER_SORTING_MASK)
var readingModeType: Int
get() = viewer_flags and ReadingModeType.MASK
set(readingMode) = setViewerFlags(readingMode, ReadingModeType.MASK)
var orientationType: Int
get() = viewer_flags and OrientationType.MASK
set(rotationType) = setViewerFlags(rotationType, OrientationType.MASK)
companion object {
// Generic filter that does not filter anything
const val SHOW_ALL = 0x00000000
const val CHAPTER_SORT_DESC = 0x00000000
const val CHAPTER_SORT_ASC = 0x00000001
const val CHAPTER_SORT_MASK = 0x00000001
const val CHAPTER_SHOW_UNREAD = 0x00000002
const val CHAPTER_SHOW_READ = 0x00000004
const val CHAPTER_READ_MASK = 0x00000006
const val CHAPTER_SHOW_DOWNLOADED = 0x00000008
const val CHAPTER_SHOW_NOT_DOWNLOADED = 0x00000010
const val CHAPTER_DOWNLOADED_MASK = 0x00000018
const val CHAPTER_SHOW_BOOKMARKED = 0x00000020
const val CHAPTER_SHOW_NOT_BOOKMARKED = 0x00000040
const val CHAPTER_BOOKMARKED_MASK = 0x00000060
const val CHAPTER_SORTING_SOURCE = 0x00000000
const val CHAPTER_SORTING_NUMBER = 0x00000100
const val CHAPTER_SORTING_UPLOAD_DATE = 0x00000200
const val CHAPTER_SORTING_MASK = 0x00000300
const val CHAPTER_DISPLAY_NAME = 0x00000000
const val CHAPTER_DISPLAY_NUMBER = 0x00100000
const val CHAPTER_DISPLAY_MASK = 0x00100000
}
}
// fun Manga.toMangaInfo(): MangaInfo {
// return MangaInfo(
// artist = this.artist ?: "",
// author = this.author ?: "",
// cover = this.thumbnail_url ?: "",
// description = this.description ?: "",
// genres = this.getGenres() ?: emptyList(),
// key = this.url,
// status = this.status,
// title = this.title
// )
// }

View File

@@ -1,69 +0,0 @@
@file:Suppress("ktlint:standard:property-naming")
package suwayomi.tachidesk.manga.impl.backup.models
import eu.kanade.tachiyomi.source.model.UpdateStrategy
open class MangaImpl : Manga {
override var id: Long? = null
override var source: Long = -1
override lateinit var url: String
override lateinit var title: String
override var artist: String? = null
override var author: String? = null
override var description: String? = null
override var genre: String? = null
override var status: Int = 0
override var thumbnail_url: String? = null
override var update_strategy: UpdateStrategy = UpdateStrategy.ALWAYS_UPDATE
override var favorite: Boolean = false
override var last_update: Long = 0
override var next_update: Long = 0
override var date_added: Long = 0
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
* 1 -> Left to Right
* 2 -> Right to Left
* 3 -> Vertical
* 4 -> Webtoon
* 5 -> Continues Vertical
*/
override var viewer_flags: Int = 0
/** Contains some useful info about
*/
override var chapter_flags: Int = 0
override var cover_last_modified: Long = 0
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other == null || javaClass != other.javaClass) return false
val manga = other as Manga
if (url != manga.url) return false
return id == manga.id
}
override fun hashCode(): Int = url.hashCode() + id.hashCode()
}

View File

@@ -1,33 +0,0 @@
@file:Suppress("ktlint:standard:property-naming")
package suwayomi.tachidesk.manga.impl.backup.models
import java.io.Serializable
interface Track : Serializable {
var id: Long?
var manga_id: Long
var sync_id: Int
var media_id: Long
var library_id: Long?
var title: String
var last_chapter_read: Int
var total_chapters: Int
var score: Float
var status: Int
var started_reading_date: Long
var finished_reading_date: Long
var tracking_url: String
}

View File

@@ -1,49 +0,0 @@
@file:Suppress("ktlint:standard:property-naming")
package suwayomi.tachidesk.manga.impl.backup.models
class TrackImpl : Track {
override var id: Long? = null
override var manga_id: Long = 0
override var sync_id: Int = 0
override var media_id: Long = 0L
override var library_id: Long? = null
override lateinit var title: String
override var last_chapter_read: Int = 0
override var total_chapters: Int = 0
override var score: Float = 0f
override var status: Int = 0
override var started_reading_date: Long = 0
override var finished_reading_date: Long = 0
override var tracking_url: String = ""
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other == null || javaClass != other.javaClass) return false
other as Track
if (manga_id != other.manga_id) return false
if (sync_id != other.sync_id) return false
return media_id == other.media_id
}
override fun hashCode(): Int {
var result = (manga_id xor manga_id.ushr(32)).toInt()
result = 31 * result + sync_id
result = (31 * result + media_id).toInt()
return result
}
}

View File

@@ -290,6 +290,7 @@ object ProtoBackupExport : ProtoBackupBase() {
startedReadingDate = it.record.startDate,
finishedReadingDate = it.record.finishDate,
trackingUrl = it.record.remoteUrl,
private = it.record.private,
)
}
}

View File

@@ -40,12 +40,11 @@ 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
import suwayomi.tachidesk.manga.impl.backup.proto.ProtoBackupValidator.validate
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.BackupHistory
import suwayomi.tachidesk.manga.impl.backup.proto.models.BackupManga
import suwayomi.tachidesk.manga.impl.backup.proto.models.BackupServerSettings
@@ -62,8 +61,8 @@ import java.io.InputStream
import java.util.Date
import java.util.Timer
import java.util.TimerTask
import java.util.concurrent.TimeUnit
import kotlin.math.max
import kotlin.time.Duration.Companion.milliseconds
import suwayomi.tachidesk.manga.impl.track.Track as Tracker
enum class RestoreMode {
@@ -144,7 +143,7 @@ object ProtoBackupImport : ProtoBackupBase() {
}
@OptIn(DelicateCoroutinesApi::class)
suspend fun restore(sourceStream: InputStream): String {
fun restore(sourceStream: InputStream): String {
val restoreId = System.currentTimeMillis().toString()
logger.info { "restore($restoreId): queued" }
@@ -279,25 +278,24 @@ object ProtoBackupImport : ProtoBackupBase() {
backupManga: BackupManga,
categoryMapping: Map<Int, Int>,
) {
val manga = backupManga.getMangaImpl()
val chapters = backupManga.getChaptersImpl()
val chapters = backupManga.chapters
val categories = backupManga.categories
val history = backupManga.history
val dbCategoryIds = categories.map { categoryMapping[it]!! }
try {
restoreMangaData(manga, chapters, dbCategoryIds, history, backupManga.tracking)
restoreMangaData(backupManga, chapters, dbCategoryIds, history, backupManga.tracking)
} catch (e: Exception) {
val sourceName = sourceMapping[manga.source] ?: manga.source.toString()
errors.add(Date() to "${manga.title} [$sourceName]: ${e.message}")
val sourceName = sourceMapping[backupManga.source] ?: backupManga.source.toString()
errors.add(Date() to "${backupManga.title} [$sourceName]: ${e.message}")
}
}
@Suppress("UNUSED_PARAMETER") // TODO: remove
private fun restoreMangaData(
manga: Manga,
chapters: List<Chapter>,
manga: BackupManga,
chapters: List<BackupChapter>,
categoryIds: List<Int>,
history: List<BackupHistory>,
tracks: List<BackupTracking>,
@@ -324,10 +322,10 @@ object ProtoBackupImport : ProtoBackupBase() {
it[artist] = manga.artist
it[author] = manga.author
it[description] = manga.description
it[genre] = manga.genre
it[genre] = manga.genre.joinToString()
it[status] = manga.status
it[thumbnail_url] = manga.thumbnail_url
it[updateStrategy] = manga.update_strategy.name
it[thumbnail_url] = manga.thumbnailUrl
it[updateStrategy] = manga.updateStrategy.name
it[sourceReference] = manga.source
@@ -335,7 +333,7 @@ object ProtoBackupImport : ProtoBackupBase() {
it[inLibrary] = manga.favorite
it[inLibraryAt] = TimeUnit.MILLISECONDS.toSeconds(manga.date_added)
it[inLibraryAt] = manga.dateAdded.milliseconds.inWholeSeconds
}.value
} else {
val dbMangaId = dbManga[MangaTable.id].value
@@ -345,16 +343,16 @@ object ProtoBackupImport : ProtoBackupBase() {
it[artist] = manga.artist ?: dbManga[artist]
it[author] = manga.author ?: dbManga[author]
it[description] = manga.description ?: dbManga[description]
it[genre] = manga.genre ?: dbManga[genre]
it[genre] = manga.genre.ifEmpty { null }?.joinToString() ?: dbManga[genre]
it[status] = manga.status
it[thumbnail_url] = manga.thumbnail_url ?: dbManga[thumbnail_url]
it[updateStrategy] = manga.update_strategy.name
it[thumbnail_url] = manga.thumbnailUrl ?: dbManga[thumbnail_url]
it[updateStrategy] = manga.updateStrategy.name
it[initialized] = dbManga[initialized] || manga.description != null
it[inLibrary] = manga.favorite || dbManga[inLibrary]
it[inLibraryAt] = TimeUnit.MILLISECONDS.toSeconds(manga.date_added)
it[inLibraryAt] = manga.dateAdded.milliseconds.inWholeSeconds
}
dbMangaId
@@ -384,8 +382,8 @@ object ProtoBackupImport : ProtoBackupBase() {
private fun getMangaChapterToRestoreInfo(
mangaId: Int,
restoreMode: RestoreMode,
chapters: List<Chapter>,
): Pair<List<Chapter>, List<Pair<Chapter, ResultRow>>> {
chapters: List<BackupChapter>,
): Pair<List<BackupChapter>, List<Pair<BackupChapter, ResultRow>>> {
val uniqueChapters = chapters.distinctBy { it.url }
if (restoreMode == RestoreMode.NEW) {
@@ -403,7 +401,7 @@ object ProtoBackupImport : ProtoBackupBase() {
private fun restoreMangaChapterData(
mangaId: Int,
restoreMode: RestoreMode,
chapters: List<Chapter>,
chapters: List<BackupChapter>,
) = dbTransaction {
val (chaptersToInsert, chaptersToUpdateToDbChapter) = getMangaChapterToRestoreInfo(mangaId, restoreMode, chapters)
@@ -412,22 +410,22 @@ object ProtoBackupImport : ProtoBackupBase() {
.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
if (chapter.dateUpload == 0L) {
this[ChapterTable.date_upload] = chapter.dateFetch
} else {
this[ChapterTable.date_upload] = chapter.date_upload
this[ChapterTable.date_upload] = chapter.dateUpload
}
this[ChapterTable.chapter_number] = chapter.chapter_number
this[ChapterTable.chapter_number] = chapter.chapterNumber
this[ChapterTable.scanlator] = chapter.scanlator
this[ChapterTable.sourceOrder] = chaptersToInsert.size - chapter.source_order
this[ChapterTable.sourceOrder] = chaptersToInsert.size - chapter.sourceOrder
this[ChapterTable.manga] = mangaId
this[ChapterTable.isRead] = chapter.read
this[ChapterTable.lastPageRead] = chapter.last_page_read.coerceAtLeast(0)
this[ChapterTable.lastPageRead] = chapter.lastPageRead.coerceAtLeast(0)
this[ChapterTable.isBookmarked] = chapter.bookmark
this[ChapterTable.fetchedAt] = TimeUnit.MILLISECONDS.toSeconds(chapter.date_fetch)
this[ChapterTable.fetchedAt] = chapter.dateFetch.milliseconds.inWholeSeconds
}.map { it[ChapterTable.id].value }
if (chaptersToUpdateToDbChapter.isNotEmpty()) {
@@ -436,7 +434,7 @@ object ProtoBackupImport : ProtoBackupBase() {
addBatch(EntityID(dbChapter[ChapterTable.id].value, ChapterTable))
this[ChapterTable.isRead] = backupChapter.read || dbChapter[ChapterTable.isRead]
this[ChapterTable.lastPageRead] =
max(backupChapter.last_page_read, dbChapter[ChapterTable.lastPageRead]).coerceAtLeast(0)
max(backupChapter.lastPageRead, dbChapter[ChapterTable.lastPageRead]).coerceAtLeast(0)
this[ChapterTable.isBookmarked] = backupChapter.bookmark || dbChapter[ChapterTable.isBookmarked]
}
execute(this@dbTransaction)

View File

@@ -2,7 +2,6 @@ package suwayomi.tachidesk.manga.impl.backup.proto.models
import kotlinx.serialization.Serializable
import kotlinx.serialization.protobuf.ProtoNumber
import suwayomi.tachidesk.manga.impl.backup.models.ChapterImpl
@Serializable
data class BackupChapter(
@@ -22,19 +21,4 @@ data class BackupChapter(
@ProtoNumber(10) var sourceOrder: Int = 0,
// suwayomi
@ProtoNumber(9000) var meta: Map<String, String> = emptyMap(),
) {
fun toChapterImpl(): ChapterImpl =
ChapterImpl().apply {
url = this@BackupChapter.url
name = this@BackupChapter.name
chapter_number = this@BackupChapter.chapterNumber
scanlator = this@BackupChapter.scanlator
read = this@BackupChapter.read
bookmark = this@BackupChapter.bookmark
last_page_read = this@BackupChapter.lastPageRead
date_fetch = this@BackupChapter.dateFetch
date_upload = this@BackupChapter.dateUpload
source_order = this@BackupChapter.sourceOrder
meta = this@BackupChapter.meta
}
}
)

View File

@@ -3,10 +3,6 @@ package suwayomi.tachidesk.manga.impl.backup.proto.models
import eu.kanade.tachiyomi.source.model.UpdateStrategy
import kotlinx.serialization.Serializable
import kotlinx.serialization.protobuf.ProtoNumber
import suwayomi.tachidesk.manga.impl.backup.models.ChapterImpl
import suwayomi.tachidesk.manga.impl.backup.models.Manga
import suwayomi.tachidesk.manga.impl.backup.models.MangaImpl
import suwayomi.tachidesk.manga.impl.backup.models.TrackImpl
@Serializable
data class BackupManga(
@@ -40,53 +36,4 @@ data class BackupManga(
@ProtoNumber(105) var updateStrategy: UpdateStrategy = UpdateStrategy.ALWAYS_UPDATE,
// suwayomi
@ProtoNumber(9000) var meta: Map<String, String> = emptyMap(),
) {
fun getMangaImpl(): MangaImpl =
MangaImpl().apply {
url = this@BackupManga.url
title = this@BackupManga.title
artist = this@BackupManga.artist
author = this@BackupManga.author
description = this@BackupManga.description
genre = this@BackupManga.genre.joinToString()
status = this@BackupManga.status
thumbnail_url = this@BackupManga.thumbnailUrl
favorite = this@BackupManga.favorite
source = this@BackupManga.source
date_added = this@BackupManga.dateAdded
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> =
chapters.map {
it.toChapterImpl()
}
fun getTrackingImpl(): List<TrackImpl> =
tracking.map {
it.getTrackingImpl()
}
companion object {
fun copyFrom(manga: Manga): BackupManga =
BackupManga(
url = manga.url,
title = manga.title,
artist = manga.artist,
author = manga.author,
description = manga.description,
genre = manga.getGenres() ?: emptyList(),
status = manga.status,
thumbnailUrl = manga.thumbnail_url,
favorite = manga.favorite,
source = manga.source,
dateAdded = manga.date_added,
viewer = manga.readingModeType,
viewer_flags = manga.viewer_flags,
chapterFlags = manga.chapter_flags,
)
}
}
)

View File

@@ -1,6 +1,5 @@
package suwayomi.tachidesk.manga.impl.backup.proto.models
import eu.kanade.tachiyomi.source.Source
import kotlinx.serialization.Serializable
import kotlinx.serialization.protobuf.ProtoNumber
@@ -10,12 +9,4 @@ data class BackupSource(
@ProtoNumber(2) var sourceId: Long,
// suwayomi
@ProtoNumber(9000) var meta: Map<String, String> = emptyMap(),
) {
companion object {
fun copyFrom(source: Source): BackupSource =
BackupSource(
name = source.name,
sourceId = source.id,
)
}
}
)

View File

@@ -2,8 +2,6 @@ package suwayomi.tachidesk.manga.impl.backup.proto.models
import kotlinx.serialization.Serializable
import kotlinx.serialization.protobuf.ProtoNumber
import suwayomi.tachidesk.manga.impl.backup.models.Track
import suwayomi.tachidesk.manga.impl.backup.models.TrackImpl
@Serializable
data class BackupTracking(
@@ -28,44 +26,4 @@ data class BackupTracking(
@ProtoNumber(11) var finishedReadingDate: Long = 0,
@ProtoNumber(12) var private: Boolean = false,
@ProtoNumber(100) var mediaId: Long = 0,
) {
fun getTrackingImpl(): TrackImpl =
TrackImpl().apply {
sync_id = this@BackupTracking.syncId
media_id =
if (this@BackupTracking.mediaIdInt != 0) {
this@BackupTracking.mediaIdInt.toLong()
} else {
this@BackupTracking.mediaId
}
library_id = this@BackupTracking.libraryId
title = this@BackupTracking.title
// convert from float to int because of 1.x types
last_chapter_read = this@BackupTracking.lastChapterRead.toInt()
total_chapters = this@BackupTracking.totalChapters
score = this@BackupTracking.score
status = this@BackupTracking.status
started_reading_date = this@BackupTracking.startedReadingDate
finished_reading_date = this@BackupTracking.finishedReadingDate
tracking_url = this@BackupTracking.trackingUrl
}
companion object {
fun copyFrom(track: Track): BackupTracking =
BackupTracking(
syncId = track.sync_id,
mediaId = track.media_id,
// forced not null so its compatible with 1.x backup system
libraryId = track.library_id!!,
title = track.title,
// convert to float for 1.x
lastChapterRead = track.last_chapter_read.toFloat(),
totalChapters = track.total_chapters,
score = track.score,
status = track.status,
startedReadingDate = track.started_reading_date,
finishedReadingDate = track.finished_reading_date,
trackingUrl = track.tracking_url,
)
}
}
)