mirror of
https://github.com/Suwayomi/Suwayomi-Server.git
synced 2026-07-03 10:54:38 -05:00
Feature/backup tracking (#940)
* Include tracking in validation of backup * Always return track records Not clear why an empty list should be returned in case no trackers are logged in * Include tracking in backup creation * Restore tracking from backup
This commit is contained in:
@@ -16,14 +16,20 @@ class BackupQuery {
|
|||||||
val name: String,
|
val name: String,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
data class ValidateBackupTracker(
|
||||||
|
val name: String,
|
||||||
|
)
|
||||||
|
|
||||||
data class ValidateBackupResult(
|
data class ValidateBackupResult(
|
||||||
val missingSources: List<ValidateBackupSource>,
|
val missingSources: List<ValidateBackupSource>,
|
||||||
|
val missingTrackers: List<ValidateBackupTracker>,
|
||||||
)
|
)
|
||||||
|
|
||||||
fun validateBackup(input: ValidateBackupInput): ValidateBackupResult {
|
fun validateBackup(input: ValidateBackupInput): ValidateBackupResult {
|
||||||
val result = ProtoBackupValidator.validate(input.backup.content)
|
val result = ProtoBackupValidator.validate(input.backup.content)
|
||||||
return ValidateBackupResult(
|
return ValidateBackupResult(
|
||||||
result.missingSourceIds.map { ValidateBackupSource(it.first, it.second) },
|
result.missingSourceIds.map { ValidateBackupSource(it.first, it.second) },
|
||||||
|
result.missingTrackers.map { ValidateBackupTracker(it) },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ interface Track : Serializable {
|
|||||||
|
|
||||||
var sync_id: Int
|
var sync_id: Int
|
||||||
|
|
||||||
var media_id: Int
|
var media_id: Long
|
||||||
|
|
||||||
var library_id: Long?
|
var library_id: Long?
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ class TrackImpl : Track {
|
|||||||
|
|
||||||
override var sync_id: Int = 0
|
override var sync_id: Int = 0
|
||||||
|
|
||||||
override var media_id: Int = 0
|
override var media_id: Long = 0L
|
||||||
|
|
||||||
override var library_id: Long? = null
|
override var library_id: Long? = null
|
||||||
|
|
||||||
@@ -43,7 +43,7 @@ class TrackImpl : Track {
|
|||||||
override fun hashCode(): Int {
|
override fun hashCode(): Int {
|
||||||
var result = (manga_id xor manga_id.ushr(32)).toInt()
|
var result = (manga_id xor manga_id.ushr(32)).toInt()
|
||||||
result = 31 * result + sync_id
|
result = 31 * result + sync_id
|
||||||
result = 31 * result + media_id
|
result = (31 * result + media_id).toInt()
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,6 +31,8 @@ import suwayomi.tachidesk.manga.impl.backup.proto.models.BackupChapter
|
|||||||
import suwayomi.tachidesk.manga.impl.backup.proto.models.BackupManga
|
import suwayomi.tachidesk.manga.impl.backup.proto.models.BackupManga
|
||||||
import suwayomi.tachidesk.manga.impl.backup.proto.models.BackupSerializer
|
import suwayomi.tachidesk.manga.impl.backup.proto.models.BackupSerializer
|
||||||
import suwayomi.tachidesk.manga.impl.backup.proto.models.BackupSource
|
import suwayomi.tachidesk.manga.impl.backup.proto.models.BackupSource
|
||||||
|
import suwayomi.tachidesk.manga.impl.backup.proto.models.BackupTracking
|
||||||
|
import suwayomi.tachidesk.manga.impl.track.Track
|
||||||
import suwayomi.tachidesk.manga.model.table.CategoryTable
|
import suwayomi.tachidesk.manga.model.table.CategoryTable
|
||||||
import suwayomi.tachidesk.manga.model.table.ChapterTable
|
import suwayomi.tachidesk.manga.model.table.ChapterTable
|
||||||
import suwayomi.tachidesk.manga.model.table.MangaStatus
|
import suwayomi.tachidesk.manga.model.table.MangaStatus
|
||||||
@@ -230,9 +232,32 @@ object ProtoBackupExport : ProtoBackupBase() {
|
|||||||
backupManga.categories = CategoryManga.getMangaCategories(mangaId).map { it.order }
|
backupManga.categories = CategoryManga.getMangaCategories(mangaId).map { it.order }
|
||||||
}
|
}
|
||||||
|
|
||||||
// if(flags.includeTracking) {
|
if (flags.includeTracking) {
|
||||||
// backupManga.tracking = TODO()
|
val tracks =
|
||||||
// }
|
Track.getTrackRecordsByMangaId(mangaRow[MangaTable.id].value).mapNotNull {
|
||||||
|
if (it.record == null) {
|
||||||
|
null
|
||||||
|
} else {
|
||||||
|
BackupTracking(
|
||||||
|
syncId = it.record.trackerId,
|
||||||
|
// forced not null so its compatible with 1.x backup system
|
||||||
|
libraryId = it.record.libraryId ?: 0,
|
||||||
|
mediaId = it.record.remoteId,
|
||||||
|
title = it.record.title,
|
||||||
|
lastChapterRead = it.record.lastChapterRead.toFloat(),
|
||||||
|
totalChapters = it.record.totalChapters,
|
||||||
|
score = it.record.score.toFloat(),
|
||||||
|
status = it.record.status,
|
||||||
|
startedReadingDate = it.record.startDate,
|
||||||
|
finishedReadingDate = it.record.finishDate,
|
||||||
|
trackingUrl = it.record.remoteUrl,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (tracks.isNotEmpty()) {
|
||||||
|
backupManga.tracking = tracks
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// if (flags.includeHistory) {
|
// if (flags.includeHistory) {
|
||||||
// backupManga.history = TODO()
|
// backupManga.history = TODO()
|
||||||
|
|||||||
@@ -34,22 +34,26 @@ import suwayomi.tachidesk.manga.impl.CategoryManga
|
|||||||
import suwayomi.tachidesk.manga.impl.Manga.clearThumbnail
|
import suwayomi.tachidesk.manga.impl.Manga.clearThumbnail
|
||||||
import suwayomi.tachidesk.manga.impl.backup.models.Chapter
|
import suwayomi.tachidesk.manga.impl.backup.models.Chapter
|
||||||
import suwayomi.tachidesk.manga.impl.backup.models.Manga
|
import suwayomi.tachidesk.manga.impl.backup.models.Manga
|
||||||
import suwayomi.tachidesk.manga.impl.backup.models.Track
|
|
||||||
import suwayomi.tachidesk.manga.impl.backup.proto.ProtoBackupValidator.ValidationResult
|
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.ProtoBackupValidator.validate
|
||||||
import suwayomi.tachidesk.manga.impl.backup.proto.models.BackupCategory
|
import suwayomi.tachidesk.manga.impl.backup.proto.models.BackupCategory
|
||||||
import suwayomi.tachidesk.manga.impl.backup.proto.models.BackupHistory
|
import suwayomi.tachidesk.manga.impl.backup.proto.models.BackupHistory
|
||||||
import suwayomi.tachidesk.manga.impl.backup.proto.models.BackupManga
|
import suwayomi.tachidesk.manga.impl.backup.proto.models.BackupManga
|
||||||
import suwayomi.tachidesk.manga.impl.backup.proto.models.BackupSerializer
|
import suwayomi.tachidesk.manga.impl.backup.proto.models.BackupSerializer
|
||||||
|
import suwayomi.tachidesk.manga.impl.backup.proto.models.BackupTracking
|
||||||
|
import suwayomi.tachidesk.manga.impl.track.tracker.model.toTrack
|
||||||
|
import suwayomi.tachidesk.manga.impl.track.tracker.model.toTrackRecordDataClass
|
||||||
|
import suwayomi.tachidesk.manga.model.dataclass.TrackRecordDataClass
|
||||||
import suwayomi.tachidesk.manga.model.table.CategoryTable
|
import suwayomi.tachidesk.manga.model.table.CategoryTable
|
||||||
import suwayomi.tachidesk.manga.model.table.ChapterTable
|
import suwayomi.tachidesk.manga.model.table.ChapterTable
|
||||||
import suwayomi.tachidesk.manga.model.table.MangaTable
|
import suwayomi.tachidesk.manga.model.table.MangaTable
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
import java.lang.Integer.max
|
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
import java.util.Timer
|
import java.util.Timer
|
||||||
import java.util.TimerTask
|
import java.util.TimerTask
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
|
import kotlin.math.max
|
||||||
|
import suwayomi.tachidesk.manga.impl.track.Track as Tracker
|
||||||
|
|
||||||
object ProtoBackupImport : ProtoBackupBase() {
|
object ProtoBackupImport : ProtoBackupBase() {
|
||||||
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
|
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
|
||||||
@@ -239,10 +243,9 @@ object ProtoBackupImport : ProtoBackupBase() {
|
|||||||
val chapters = backupManga.getChaptersImpl()
|
val chapters = backupManga.getChaptersImpl()
|
||||||
val categories = backupManga.categories
|
val categories = backupManga.categories
|
||||||
val history = backupManga.brokenHistory.map { BackupHistory(it.url, it.lastRead) } + backupManga.history
|
val history = backupManga.brokenHistory.map { BackupHistory(it.url, it.lastRead) } + backupManga.history
|
||||||
val tracks = backupManga.getTrackingImpl()
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
restoreMangaData(manga, chapters, categories, history, tracks, backupCategories, categoryMapping)
|
restoreMangaData(manga, chapters, categories, history, backupManga.tracking, backupCategories, categoryMapping)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
val sourceName = sourceMapping[manga.source] ?: manga.source.toString()
|
val sourceName = sourceMapping[manga.source] ?: manga.source.toString()
|
||||||
errors.add(Date() to "${manga.title} [$sourceName]: ${e.message}")
|
errors.add(Date() to "${manga.title} [$sourceName]: ${e.message}")
|
||||||
@@ -255,7 +258,7 @@ object ProtoBackupImport : ProtoBackupBase() {
|
|||||||
chapters: List<Chapter>,
|
chapters: List<Chapter>,
|
||||||
categories: List<Int>,
|
categories: List<Int>,
|
||||||
history: List<BackupHistory>,
|
history: List<BackupHistory>,
|
||||||
tracks: List<Track>,
|
tracks: List<BackupTracking>,
|
||||||
backupCategories: List<BackupCategory>,
|
backupCategories: List<BackupCategory>,
|
||||||
categoryMapping: Map<Int, Int>,
|
categoryMapping: Map<Int, Int>,
|
||||||
) {
|
) {
|
||||||
@@ -265,127 +268,159 @@ object ProtoBackupImport : ProtoBackupBase() {
|
|||||||
.firstOrNull()
|
.firstOrNull()
|
||||||
}
|
}
|
||||||
|
|
||||||
if (dbManga == null) { // Manga not in database
|
val mangaId =
|
||||||
transaction {
|
if (dbManga == null) { // Manga not in database
|
||||||
// insert manga to database
|
transaction {
|
||||||
val mangaId =
|
// insert manga to database
|
||||||
MangaTable.insertAndGetId {
|
val mangaId =
|
||||||
it[url] = manga.url
|
MangaTable.insertAndGetId {
|
||||||
it[title] = manga.title
|
it[url] = manga.url
|
||||||
|
it[title] = manga.title
|
||||||
|
|
||||||
it[artist] = manga.artist
|
it[artist] = manga.artist
|
||||||
it[author] = manga.author
|
it[author] = manga.author
|
||||||
it[description] = manga.description
|
it[description] = manga.description
|
||||||
it[genre] = manga.genre
|
it[genre] = manga.genre
|
||||||
|
it[status] = manga.status
|
||||||
|
it[thumbnail_url] = manga.thumbnail_url
|
||||||
|
it[updateStrategy] = manga.update_strategy.name
|
||||||
|
|
||||||
|
it[sourceReference] = manga.source
|
||||||
|
|
||||||
|
it[initialized] = manga.description != null
|
||||||
|
|
||||||
|
it[inLibrary] = manga.favorite
|
||||||
|
|
||||||
|
it[inLibraryAt] = TimeUnit.MILLISECONDS.toSeconds(manga.date_added)
|
||||||
|
}.value
|
||||||
|
|
||||||
|
// delete thumbnail in case cached data still exists
|
||||||
|
clearThumbnail(mangaId)
|
||||||
|
|
||||||
|
// insert chapter data
|
||||||
|
val chaptersLength = chapters.size
|
||||||
|
ChapterTable.batchInsert(chapters) { 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] = chaptersLength - chapter.source_order
|
||||||
|
this[ChapterTable.manga] = mangaId
|
||||||
|
|
||||||
|
this[ChapterTable.isRead] = chapter.read
|
||||||
|
this[ChapterTable.lastPageRead] = chapter.last_page_read
|
||||||
|
this[ChapterTable.isBookmarked] = chapter.bookmark
|
||||||
|
|
||||||
|
this[ChapterTable.fetchedAt] = TimeUnit.MILLISECONDS.toSeconds(chapter.date_fetch)
|
||||||
|
}
|
||||||
|
|
||||||
|
// insert categories
|
||||||
|
categories.forEach { backupCategoryOrder ->
|
||||||
|
CategoryManga.addMangaToCategory(mangaId, categoryMapping[backupCategoryOrder]!!)
|
||||||
|
}
|
||||||
|
|
||||||
|
mangaId
|
||||||
|
}
|
||||||
|
} else { // Manga in database
|
||||||
|
transaction {
|
||||||
|
val mangaId = dbManga[MangaTable.id].value
|
||||||
|
|
||||||
|
// Merge manga data
|
||||||
|
MangaTable.update({ MangaTable.id eq mangaId }) {
|
||||||
|
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[status] = manga.status
|
it[status] = manga.status
|
||||||
it[thumbnail_url] = manga.thumbnail_url
|
it[thumbnail_url] = manga.thumbnail_url ?: dbManga[thumbnail_url]
|
||||||
it[updateStrategy] = manga.update_strategy.name
|
it[updateStrategy] = manga.update_strategy.name
|
||||||
|
|
||||||
it[sourceReference] = manga.source
|
it[initialized] = dbManga[initialized] || manga.description != null
|
||||||
|
|
||||||
it[initialized] = manga.description != null
|
it[inLibrary] = manga.favorite || dbManga[inLibrary]
|
||||||
|
|
||||||
it[inLibrary] = manga.favorite
|
|
||||||
|
|
||||||
it[inLibraryAt] = TimeUnit.MILLISECONDS.toSeconds(manga.date_added)
|
it[inLibraryAt] = TimeUnit.MILLISECONDS.toSeconds(manga.date_added)
|
||||||
}.value
|
|
||||||
|
|
||||||
// delete thumbnail in case cached data still exists
|
|
||||||
clearThumbnail(mangaId)
|
|
||||||
|
|
||||||
// insert chapter data
|
|
||||||
val chaptersLength = chapters.size
|
|
||||||
ChapterTable.batchInsert(chapters) { 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] = chaptersLength - chapter.source_order
|
// merge chapter data
|
||||||
this[ChapterTable.manga] = mangaId
|
val chaptersLength = chapters.size
|
||||||
|
val dbChapters = ChapterTable.select { ChapterTable.manga eq mangaId }
|
||||||
|
|
||||||
this[ChapterTable.isRead] = chapter.read
|
chapters.forEach { chapter ->
|
||||||
this[ChapterTable.lastPageRead] = chapter.last_page_read
|
val dbChapter = dbChapters.find { it[ChapterTable.url] == chapter.url }
|
||||||
this[ChapterTable.isBookmarked] = chapter.bookmark
|
|
||||||
|
|
||||||
this[ChapterTable.fetchedAt] = TimeUnit.MILLISECONDS.toSeconds(chapter.date_fetch)
|
if (dbChapter == null) {
|
||||||
}
|
ChapterTable.insert {
|
||||||
|
it[url] = chapter.url
|
||||||
|
it[name] = chapter.name
|
||||||
|
if (chapter.date_upload == 0L) {
|
||||||
|
it[date_upload] = chapter.date_fetch
|
||||||
|
} else {
|
||||||
|
it[date_upload] = chapter.date_upload
|
||||||
|
}
|
||||||
|
it[chapter_number] = chapter.chapter_number
|
||||||
|
it[scanlator] = chapter.scanlator
|
||||||
|
|
||||||
// insert categories
|
it[sourceOrder] = chaptersLength - chapter.source_order
|
||||||
categories.forEach { backupCategoryOrder ->
|
it[ChapterTable.manga] = mangaId
|
||||||
CategoryManga.addMangaToCategory(mangaId, categoryMapping[backupCategoryOrder]!!)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else { // Manga in database
|
|
||||||
transaction {
|
|
||||||
val mangaId = dbManga[MangaTable.id].value
|
|
||||||
|
|
||||||
// Merge manga data
|
it[isRead] = chapter.read
|
||||||
MangaTable.update({ MangaTable.id eq mangaId }) {
|
it[lastPageRead] = chapter.last_page_read
|
||||||
it[artist] = manga.artist ?: dbManga[artist]
|
it[isBookmarked] = chapter.bookmark
|
||||||
it[author] = manga.author ?: dbManga[author]
|
}
|
||||||
it[description] = manga.description ?: dbManga[description]
|
} else {
|
||||||
it[genre] = manga.genre ?: dbManga[genre]
|
ChapterTable.update({ (ChapterTable.url eq dbChapter[ChapterTable.url]) and (ChapterTable.manga eq mangaId) }) {
|
||||||
it[status] = manga.status
|
it[isRead] = chapter.read || dbChapter[isRead]
|
||||||
it[thumbnail_url] = manga.thumbnail_url ?: dbManga[thumbnail_url]
|
it[lastPageRead] = max(chapter.last_page_read, dbChapter[lastPageRead])
|
||||||
it[updateStrategy] = manga.update_strategy.name
|
it[isBookmarked] = chapter.bookmark || dbChapter[isBookmarked]
|
||||||
|
|
||||||
it[initialized] = dbManga[initialized] || manga.description != null
|
|
||||||
|
|
||||||
it[inLibrary] = manga.favorite || dbManga[inLibrary]
|
|
||||||
|
|
||||||
it[inLibraryAt] = TimeUnit.MILLISECONDS.toSeconds(manga.date_added)
|
|
||||||
}
|
|
||||||
|
|
||||||
// merge chapter data
|
|
||||||
val chaptersLength = chapters.size
|
|
||||||
val dbChapters = ChapterTable.select { ChapterTable.manga eq mangaId }
|
|
||||||
|
|
||||||
chapters.forEach { chapter ->
|
|
||||||
val dbChapter = dbChapters.find { it[ChapterTable.url] == chapter.url }
|
|
||||||
|
|
||||||
if (dbChapter == null) {
|
|
||||||
ChapterTable.insert {
|
|
||||||
it[url] = chapter.url
|
|
||||||
it[name] = chapter.name
|
|
||||||
if (chapter.date_upload == 0L) {
|
|
||||||
it[date_upload] = chapter.date_fetch
|
|
||||||
} else {
|
|
||||||
it[date_upload] = chapter.date_upload
|
|
||||||
}
|
}
|
||||||
it[chapter_number] = chapter.chapter_number
|
|
||||||
it[scanlator] = chapter.scanlator
|
|
||||||
|
|
||||||
it[sourceOrder] = chaptersLength - chapter.source_order
|
|
||||||
it[ChapterTable.manga] = mangaId
|
|
||||||
|
|
||||||
it[isRead] = chapter.read
|
|
||||||
it[lastPageRead] = chapter.last_page_read
|
|
||||||
it[isBookmarked] = chapter.bookmark
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
ChapterTable.update({ (ChapterTable.url eq dbChapter[ChapterTable.url]) and (ChapterTable.manga eq mangaId) }) {
|
|
||||||
it[isRead] = chapter.read || dbChapter[isRead]
|
|
||||||
it[lastPageRead] = max(chapter.last_page_read, dbChapter[lastPageRead])
|
|
||||||
it[isBookmarked] = chapter.bookmark || dbChapter[isBookmarked]
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// merge categories
|
// merge categories
|
||||||
categories.forEach { backupCategoryOrder ->
|
categories.forEach { backupCategoryOrder ->
|
||||||
CategoryManga.addMangaToCategory(mangaId, categoryMapping[backupCategoryOrder]!!)
|
CategoryManga.addMangaToCategory(mangaId, categoryMapping[backupCategoryOrder]!!)
|
||||||
|
}
|
||||||
|
|
||||||
|
mangaId
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
val dbTrackRecordsByTrackerId =
|
||||||
|
Tracker.getTrackRecordsByMangaId(mangaId)
|
||||||
|
.mapNotNull { it.record?.toTrack() }
|
||||||
|
.associateBy { it.sync_id }
|
||||||
|
|
||||||
|
val (existingTracks, newTracks) =
|
||||||
|
tracks.mapNotNull { backupTrack ->
|
||||||
|
val track = backupTrack.toTrack(mangaId)
|
||||||
|
val dbTrack =
|
||||||
|
dbTrackRecordsByTrackerId[backupTrack.syncId]
|
||||||
|
?: // new track
|
||||||
|
return@mapNotNull track
|
||||||
|
|
||||||
|
if (track.toTrackRecordDataClass().forComparison() == dbTrack.toTrackRecordDataClass().forComparison()) {
|
||||||
|
return@mapNotNull null
|
||||||
|
}
|
||||||
|
|
||||||
|
dbTrack.also {
|
||||||
|
it.media_id = track.media_id
|
||||||
|
it.library_id = track.library_id
|
||||||
|
it.last_chapter_read = max(dbTrack.last_chapter_read, track.last_chapter_read)
|
||||||
|
}
|
||||||
|
}.partition { (it.id ?: -1) > 0 }
|
||||||
|
|
||||||
|
existingTracks.forEach(Tracker::updateTrackRecord)
|
||||||
|
newTracks.forEach(Tracker::insertTrackRecord)
|
||||||
|
|
||||||
// TODO: insert/merge history
|
// TODO: insert/merge history
|
||||||
|
|
||||||
// TODO: insert/merge tracking
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun TrackRecordDataClass.forComparison() = this.copy(id = 0, mangaId = 0)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import org.jetbrains.exposed.sql.select
|
|||||||
import org.jetbrains.exposed.sql.transactions.transaction
|
import org.jetbrains.exposed.sql.transactions.transaction
|
||||||
import suwayomi.tachidesk.manga.impl.backup.proto.models.Backup
|
import suwayomi.tachidesk.manga.impl.backup.proto.models.Backup
|
||||||
import suwayomi.tachidesk.manga.impl.backup.proto.models.BackupSerializer
|
import suwayomi.tachidesk.manga.impl.backup.proto.models.BackupSerializer
|
||||||
|
import suwayomi.tachidesk.manga.impl.track.tracker.TrackerManager
|
||||||
import suwayomi.tachidesk.manga.model.table.SourceTable
|
import suwayomi.tachidesk.manga.model.table.SourceTable
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
|
|
||||||
@@ -39,17 +40,18 @@ object ProtoBackupValidator {
|
|||||||
sources.filter { SourceTable.select { SourceTable.id eq it.key }.firstOrNull() == null }
|
sources.filter { SourceTable.select { SourceTable.id eq it.key }.firstOrNull() == null }
|
||||||
}
|
}
|
||||||
|
|
||||||
// val trackers = backup.backupManga
|
val trackers =
|
||||||
// .flatMap { it.tracking }
|
backup.backupManga
|
||||||
// .map { it.syncId }
|
.flatMap { it.tracking }
|
||||||
// .distinct()
|
.map { it.syncId }
|
||||||
|
.distinct()
|
||||||
|
|
||||||
val missingTrackers = listOf("")
|
val missingTrackers =
|
||||||
// val missingTrackers = trackers
|
trackers
|
||||||
// .mapNotNull { trackManager.getService(it) }
|
.mapNotNull { TrackerManager.getTracker(it) }
|
||||||
// .filter { !it.isLogged }
|
.filter { !it.isLoggedIn }
|
||||||
// .map { context.getString(it.nameRes()) }
|
.map { it.name }
|
||||||
// .sorted()
|
.sorted()
|
||||||
|
|
||||||
return ValidationResult(
|
return ValidationResult(
|
||||||
missingSources
|
missingSources
|
||||||
|
|||||||
@@ -8,11 +8,12 @@ import suwayomi.tachidesk.manga.impl.backup.models.TrackImpl
|
|||||||
@Serializable
|
@Serializable
|
||||||
data class BackupTracking(
|
data class BackupTracking(
|
||||||
// in 1.x some of these values have different types or names
|
// in 1.x some of these values have different types or names
|
||||||
// syncId is called siteId in 1,x
|
|
||||||
@ProtoNumber(1) var syncId: Int,
|
@ProtoNumber(1) var syncId: Int,
|
||||||
// LibraryId is not null in 1.x
|
// LibraryId is not null in 1.x
|
||||||
@ProtoNumber(2) var libraryId: Long,
|
@ProtoNumber(2) var libraryId: Long,
|
||||||
@ProtoNumber(3) var mediaId: Int = 0,
|
@Deprecated("Use mediaId instead", level = DeprecationLevel.WARNING)
|
||||||
|
@ProtoNumber(3)
|
||||||
|
var mediaIdInt: Int = 0,
|
||||||
// trackingUrl is called mediaUrl in 1.x
|
// trackingUrl is called mediaUrl in 1.x
|
||||||
@ProtoNumber(4) var trackingUrl: String = "",
|
@ProtoNumber(4) var trackingUrl: String = "",
|
||||||
@ProtoNumber(5) var title: String = "",
|
@ProtoNumber(5) var title: String = "",
|
||||||
@@ -25,11 +26,17 @@ data class BackupTracking(
|
|||||||
@ProtoNumber(10) var startedReadingDate: Long = 0,
|
@ProtoNumber(10) var startedReadingDate: Long = 0,
|
||||||
// finishedReadingDate is called endReadTime in 1.x
|
// finishedReadingDate is called endReadTime in 1.x
|
||||||
@ProtoNumber(11) var finishedReadingDate: Long = 0,
|
@ProtoNumber(11) var finishedReadingDate: Long = 0,
|
||||||
|
@ProtoNumber(100) var mediaId: Long = 0,
|
||||||
) {
|
) {
|
||||||
fun getTrackingImpl(): TrackImpl {
|
fun getTrackingImpl(): TrackImpl {
|
||||||
return TrackImpl().apply {
|
return TrackImpl().apply {
|
||||||
sync_id = this@BackupTracking.syncId
|
sync_id = this@BackupTracking.syncId
|
||||||
media_id = this@BackupTracking.mediaId
|
media_id =
|
||||||
|
if (this@BackupTracking.mediaIdInt != 0) {
|
||||||
|
this@BackupTracking.mediaIdInt.toLong()
|
||||||
|
} else {
|
||||||
|
this@BackupTracking.mediaId
|
||||||
|
}
|
||||||
library_id = this@BackupTracking.libraryId
|
library_id = this@BackupTracking.libraryId
|
||||||
title = this@BackupTracking.title
|
title = this@BackupTracking.title
|
||||||
// convert from float to int because of 1.x types
|
// convert from float to int because of 1.x types
|
||||||
|
|||||||
@@ -74,9 +74,6 @@ object Track {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun getTrackRecordsByMangaId(mangaId: Int): List<MangaTrackerDataClass> {
|
fun getTrackRecordsByMangaId(mangaId: Int): List<MangaTrackerDataClass> {
|
||||||
if (!TrackerManager.hasLoggedTracker()) {
|
|
||||||
return emptyList()
|
|
||||||
}
|
|
||||||
val recordMap =
|
val recordMap =
|
||||||
transaction {
|
transaction {
|
||||||
TrackRecordTable.select { TrackRecordTable.mangaId eq mangaId }
|
TrackRecordTable.select { TrackRecordTable.mangaId eq mangaId }
|
||||||
@@ -342,7 +339,7 @@ object Track {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun upsertTrackRecord(track: Track): Int {
|
fun upsertTrackRecord(track: Track): Int {
|
||||||
return transaction {
|
return transaction {
|
||||||
val existingRecord =
|
val existingRecord =
|
||||||
TrackRecordTable.select {
|
TrackRecordTable.select {
|
||||||
@@ -352,41 +349,53 @@ object Track {
|
|||||||
.singleOrNull()
|
.singleOrNull()
|
||||||
|
|
||||||
if (existingRecord != null) {
|
if (existingRecord != null) {
|
||||||
TrackRecordTable.update({
|
updateTrackRecord(track)
|
||||||
(TrackRecordTable.mangaId eq track.manga_id) and
|
|
||||||
(TrackRecordTable.trackerId eq track.sync_id)
|
|
||||||
}) {
|
|
||||||
it[remoteId] = track.media_id
|
|
||||||
it[libraryId] = track.library_id
|
|
||||||
it[title] = track.title
|
|
||||||
it[lastChapterRead] = track.last_chapter_read.toDouble()
|
|
||||||
it[totalChapters] = track.total_chapters
|
|
||||||
it[status] = track.status
|
|
||||||
it[score] = track.score.toDouble()
|
|
||||||
it[remoteUrl] = track.tracking_url
|
|
||||||
it[startDate] = track.started_reading_date
|
|
||||||
it[finishDate] = track.finished_reading_date
|
|
||||||
}
|
|
||||||
existingRecord[TrackRecordTable.id].value
|
existingRecord[TrackRecordTable.id].value
|
||||||
} else {
|
} else {
|
||||||
TrackRecordTable.insertAndGetId {
|
insertTrackRecord(track)
|
||||||
it[mangaId] = track.manga_id
|
|
||||||
it[trackerId] = track.sync_id
|
|
||||||
it[remoteId] = track.media_id
|
|
||||||
it[libraryId] = track.library_id
|
|
||||||
it[title] = track.title
|
|
||||||
it[lastChapterRead] = track.last_chapter_read.toDouble()
|
|
||||||
it[totalChapters] = track.total_chapters
|
|
||||||
it[status] = track.status
|
|
||||||
it[score] = track.score.toDouble()
|
|
||||||
it[remoteUrl] = track.tracking_url
|
|
||||||
it[startDate] = track.started_reading_date
|
|
||||||
it[finishDate] = track.finished_reading_date
|
|
||||||
}.value
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun updateTrackRecord(track: Track): Int =
|
||||||
|
transaction {
|
||||||
|
TrackRecordTable.update(
|
||||||
|
{
|
||||||
|
(TrackRecordTable.mangaId eq track.manga_id) and
|
||||||
|
(TrackRecordTable.trackerId eq track.sync_id)
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
it[remoteId] = track.media_id
|
||||||
|
it[libraryId] = track.library_id
|
||||||
|
it[title] = track.title
|
||||||
|
it[lastChapterRead] = track.last_chapter_read.toDouble()
|
||||||
|
it[totalChapters] = track.total_chapters
|
||||||
|
it[status] = track.status
|
||||||
|
it[score] = track.score.toDouble()
|
||||||
|
it[remoteUrl] = track.tracking_url
|
||||||
|
it[startDate] = track.started_reading_date
|
||||||
|
it[finishDate] = track.finished_reading_date
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun insertTrackRecord(track: Track): Int =
|
||||||
|
transaction {
|
||||||
|
TrackRecordTable.insertAndGetId {
|
||||||
|
it[mangaId] = track.manga_id
|
||||||
|
it[trackerId] = track.sync_id
|
||||||
|
it[remoteId] = track.media_id
|
||||||
|
it[libraryId] = track.library_id
|
||||||
|
it[title] = track.title
|
||||||
|
it[lastChapterRead] = track.last_chapter_read.toDouble()
|
||||||
|
it[totalChapters] = track.total_chapters
|
||||||
|
it[status] = track.status
|
||||||
|
it[score] = track.score.toDouble()
|
||||||
|
it[remoteUrl] = track.tracking_url
|
||||||
|
it[startDate] = track.started_reading_date
|
||||||
|
it[finishDate] = track.finished_reading_date
|
||||||
|
}.value
|
||||||
|
}
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class LoginInput(
|
data class LoginInput(
|
||||||
val trackerId: Int,
|
val trackerId: Int,
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
package suwayomi.tachidesk.manga.impl.track.tracker.model
|
package suwayomi.tachidesk.manga.impl.track.tracker.model
|
||||||
|
|
||||||
import org.jetbrains.exposed.sql.ResultRow
|
import org.jetbrains.exposed.sql.ResultRow
|
||||||
|
import suwayomi.tachidesk.manga.impl.backup.proto.models.BackupTracking
|
||||||
import suwayomi.tachidesk.manga.model.dataclass.TrackRecordDataClass
|
import suwayomi.tachidesk.manga.model.dataclass.TrackRecordDataClass
|
||||||
import suwayomi.tachidesk.manga.model.table.TrackRecordTable
|
import suwayomi.tachidesk.manga.model.table.TrackRecordTable
|
||||||
|
import suwayomi.tachidesk.manga.model.table.TrackRecordTable.lastChapterRead
|
||||||
|
import suwayomi.tachidesk.manga.model.table.TrackRecordTable.remoteUrl
|
||||||
|
|
||||||
fun ResultRow.toTrackRecordDataClass(): TrackRecordDataClass =
|
fun ResultRow.toTrackRecordDataClass(): TrackRecordDataClass =
|
||||||
TrackRecordDataClass(
|
TrackRecordDataClass(
|
||||||
@@ -36,3 +39,52 @@ fun ResultRow.toTrack(): Track =
|
|||||||
it.started_reading_date = this[TrackRecordTable.startDate]
|
it.started_reading_date = this[TrackRecordTable.startDate]
|
||||||
it.finished_reading_date = this[TrackRecordTable.finishDate]
|
it.finished_reading_date = this[TrackRecordTable.finishDate]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun BackupTracking.toTrack(mangaId: Int): Track =
|
||||||
|
Track.create(syncId).also {
|
||||||
|
it.id = -1
|
||||||
|
it.manga_id = mangaId
|
||||||
|
it.media_id = mediaId
|
||||||
|
it.library_id = libraryId
|
||||||
|
it.title = title
|
||||||
|
it.last_chapter_read = lastChapterRead
|
||||||
|
it.total_chapters = totalChapters
|
||||||
|
it.status = status
|
||||||
|
it.score = score
|
||||||
|
it.tracking_url = trackingUrl
|
||||||
|
it.started_reading_date = startedReadingDate
|
||||||
|
it.finished_reading_date = finishedReadingDate
|
||||||
|
}
|
||||||
|
|
||||||
|
fun TrackRecordDataClass.toTrack(): Track =
|
||||||
|
Track.create(trackerId).also {
|
||||||
|
it.id = id
|
||||||
|
it.manga_id = mangaId
|
||||||
|
it.media_id = remoteId
|
||||||
|
it.library_id = libraryId
|
||||||
|
it.title = title
|
||||||
|
it.last_chapter_read = lastChapterRead.toFloat()
|
||||||
|
it.total_chapters = totalChapters
|
||||||
|
it.status = status
|
||||||
|
it.score = score.toFloat()
|
||||||
|
it.tracking_url = remoteUrl
|
||||||
|
it.started_reading_date = startDate
|
||||||
|
it.finished_reading_date = finishDate
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Track.toTrackRecordDataClass(): TrackRecordDataClass =
|
||||||
|
TrackRecordDataClass(
|
||||||
|
id = id ?: -1,
|
||||||
|
mangaId = manga_id,
|
||||||
|
trackerId = sync_id,
|
||||||
|
remoteId = media_id,
|
||||||
|
libraryId = library_id,
|
||||||
|
title = title,
|
||||||
|
lastChapterRead = last_chapter_read.toDouble(),
|
||||||
|
totalChapters = total_chapters,
|
||||||
|
status = status,
|
||||||
|
score = score.toDouble(),
|
||||||
|
remoteUrl = tracking_url,
|
||||||
|
startDate = started_reading_date,
|
||||||
|
finishDate = finished_reading_date,
|
||||||
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user