Use SQLDelight in Backup/Restore (#7295)

* Use SQLDelight in Backup/Restore

* Use CoroutineWorker
pull/7303/head
Andreas 2 years ago committed by GitHub
parent 3c9f96d621
commit fd5da2de3a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -2,7 +2,8 @@ package eu.kanade.tachiyomi.data.backup
import android.content.Context import android.content.Context
import android.net.Uri import android.net.Uri
import eu.kanade.tachiyomi.data.database.DatabaseHelper import eu.kanade.data.DatabaseHandler
import eu.kanade.data.toLong
import eu.kanade.tachiyomi.data.database.models.Chapter import eu.kanade.tachiyomi.data.database.models.Chapter
import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.database.models.toMangaInfo import eu.kanade.tachiyomi.data.database.models.toMangaInfo
@ -14,23 +15,26 @@ import eu.kanade.tachiyomi.source.model.toSChapter
import eu.kanade.tachiyomi.util.chapter.syncChaptersWithSource import eu.kanade.tachiyomi.util.chapter.syncChaptersWithSource
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import data.Mangas as DbManga
abstract class AbstractBackupManager(protected val context: Context) { abstract class AbstractBackupManager(protected val context: Context) {
internal val db: DatabaseHelper = Injekt.get() protected val handler: DatabaseHandler = Injekt.get()
internal val sourceManager: SourceManager = Injekt.get() internal val sourceManager: SourceManager = Injekt.get()
internal val trackManager: TrackManager = Injekt.get() internal val trackManager: TrackManager = Injekt.get()
protected val preferences: PreferencesHelper = Injekt.get() protected val preferences: PreferencesHelper = Injekt.get()
abstract fun createBackup(uri: Uri, flags: Int, isAutoBackup: Boolean): String abstract suspend fun createBackup(uri: Uri, flags: Int, isAutoBackup: Boolean): String
/** /**
* Returns manga * Returns manga
* *
* @return [Manga], null if not found * @return [Manga], null if not found
*/ */
internal fun getMangaFromDatabase(manga: Manga): Manga? = internal suspend fun getMangaFromDatabase(url: String, source: Long): DbManga? {
db.getManga(manga.url, manga.source).executeAsBlocking() return handler.awaitOneOrNull { mangasQueries.getMangaByUrlAndSource(url, source) }
}
/** /**
* Fetches chapter information. * Fetches chapter information.
@ -56,36 +60,134 @@ abstract class AbstractBackupManager(protected val context: Context) {
* *
* @return [Manga] from library * @return [Manga] from library
*/ */
protected fun getFavoriteManga(): List<Manga> = protected suspend fun getFavoriteManga(): List<DbManga> {
db.getFavoriteMangas().executeAsBlocking() return handler.awaitList { mangasQueries.getFavorites() }
}
/** /**
* Inserts manga and returns id * Inserts manga and returns id
* *
* @return id of [Manga], null if not found * @return id of [Manga], null if not found
*/ */
internal fun insertManga(manga: Manga): Long? = internal suspend fun insertManga(manga: Manga): Long {
db.insertManga(manga).executeAsBlocking().insertedId() return handler.awaitOne(true) {
mangasQueries.insert(
source = manga.source,
url = manga.url,
artist = manga.artist,
author = manga.author,
description = manga.description,
genre = manga.getGenres(),
title = manga.title,
status = manga.status.toLong(),
thumbnail_url = manga.thumbnail_url,
favorite = manga.favorite,
last_update = manga.last_update,
next_update = 0L,
initialized = manga.initialized,
viewer = manga.viewer_flags.toLong(),
chapter_flags = manga.chapter_flags.toLong(),
cover_last_modified = manga.cover_last_modified,
date_added = manga.date_added,
)
mangasQueries.selectLastInsertedRowId()
}
}
internal suspend fun updateManga(manga: Manga): Long {
handler.await(true) {
mangasQueries.update(
source = manga.source,
url = manga.url,
artist = manga.artist,
author = manga.author,
description = manga.description,
genre = manga.genre,
title = manga.title,
status = manga.status.toLong(),
thumbnailUrl = manga.thumbnail_url,
favorite = manga.favorite.toLong(),
lastUpdate = manga.last_update,
initialized = manga.initialized.toLong(),
viewer = manga.viewer_flags.toLong(),
chapterFlags = manga.chapter_flags.toLong(),
coverLastModified = manga.cover_last_modified,
dateAdded = manga.date_added,
mangaId = manga.id!!,
)
}
return manga.id!!
}
/** /**
* Inserts list of chapters * Inserts list of chapters
*/ */
protected fun insertChapters(chapters: List<Chapter>) { protected suspend fun insertChapters(chapters: List<Chapter>) {
db.insertChapters(chapters).executeAsBlocking() handler.await(true) {
chapters.forEach { chapter ->
chaptersQueries.insert(
chapter.manga_id!!,
chapter.url,
chapter.name,
chapter.scanlator,
chapter.read,
chapter.bookmark,
chapter.last_page_read.toLong(),
chapter.chapter_number,
chapter.source_order.toLong(),
chapter.date_fetch,
chapter.date_upload,
)
}
}
} }
/** /**
* Updates a list of chapters * Updates a list of chapters
*/ */
protected fun updateChapters(chapters: List<Chapter>) { protected suspend fun updateChapters(chapters: List<Chapter>) {
db.updateChaptersBackup(chapters).executeAsBlocking() handler.await(true) {
chapters.forEach { chapter ->
chaptersQueries.update(
chapter.manga_id!!,
chapter.url,
chapter.name,
chapter.scanlator,
chapter.read.toLong(),
chapter.bookmark.toLong(),
chapter.last_page_read.toLong(),
chapter.chapter_number.toDouble(),
chapter.source_order.toLong(),
chapter.date_fetch,
chapter.date_upload,
chapter.id!!,
)
}
}
} }
/** /**
* Updates a list of chapters with known database ids * Updates a list of chapters with known database ids
*/ */
protected fun updateKnownChapters(chapters: List<Chapter>) { protected suspend fun updateKnownChapters(chapters: List<Chapter>) {
db.updateKnownChaptersBackup(chapters).executeAsBlocking() handler.await(true) {
chapters.forEach { chapter ->
chaptersQueries.update(
mangaId = null,
url = null,
name = null,
scanlator = null,
read = chapter.read.toLong(),
bookmark = chapter.bookmark.toLong(),
lastPageRead = chapter.last_page_read.toLong(),
chapterNumber = null,
sourceOrder = null,
dateFetch = null,
dateUpload = null,
chapterId = chapter.id!!,
)
}
}
} }
/** /**

@ -2,9 +2,9 @@ package eu.kanade.tachiyomi.data.backup
import android.content.Context import android.content.Context
import android.net.Uri import android.net.Uri
import eu.kanade.data.DatabaseHandler
import eu.kanade.data.chapter.NoChaptersException import eu.kanade.data.chapter.NoChaptersException
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.DatabaseHelper
import eu.kanade.tachiyomi.data.database.models.Chapter import eu.kanade.tachiyomi.data.database.models.Chapter
import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.database.models.Track import eu.kanade.tachiyomi.data.database.models.Track
@ -20,7 +20,7 @@ import java.util.Locale
abstract class AbstractBackupRestore<T : AbstractBackupManager>(protected val context: Context, protected val notifier: BackupNotifier) { abstract class AbstractBackupRestore<T : AbstractBackupManager>(protected val context: Context, protected val notifier: BackupNotifier) {
protected val db: DatabaseHelper by injectLazy() protected val handler: DatabaseHandler by injectLazy()
protected val trackManager: TrackManager by injectLazy() protected val trackManager: TrackManager by injectLazy()
var job: Job? = null var job: Job? = null
@ -91,7 +91,22 @@ abstract class AbstractBackupRestore<T : AbstractBackupManager>(protected val co
if (service != null && service.isLogged) { if (service != null && service.isLogged) {
try { try {
val updatedTrack = service.refresh(track) val updatedTrack = service.refresh(track)
db.insertTrack(updatedTrack).executeAsBlocking() handler.await {
manga_syncQueries.insert(
updatedTrack.manga_id,
updatedTrack.sync_id.toLong(),
updatedTrack.media_id,
updatedTrack.library_id,
updatedTrack.title,
updatedTrack.last_chapter_read.toDouble(),
updatedTrack.total_chapters.toLong(),
updatedTrack.status.toLong(),
updatedTrack.score,
updatedTrack.tracking_url,
updatedTrack.started_reading_date,
updatedTrack.finished_reading_date,
)
}
} catch (e: Exception) { } catch (e: Exception) {
errors.add(Date() to "${manga.title} - ${e.message}") errors.add(Date() to "${manga.title} - ${e.message}")
} }

@ -3,13 +3,13 @@ package eu.kanade.tachiyomi.data.backup
import android.content.Context import android.content.Context
import android.net.Uri import android.net.Uri
import androidx.core.net.toUri import androidx.core.net.toUri
import androidx.work.CoroutineWorker
import androidx.work.ExistingPeriodicWorkPolicy import androidx.work.ExistingPeriodicWorkPolicy
import androidx.work.ExistingWorkPolicy import androidx.work.ExistingWorkPolicy
import androidx.work.OneTimeWorkRequestBuilder import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.PeriodicWorkRequestBuilder import androidx.work.PeriodicWorkRequestBuilder
import androidx.work.WorkInfo import androidx.work.WorkInfo
import androidx.work.WorkManager import androidx.work.WorkManager
import androidx.work.Worker
import androidx.work.WorkerParameters import androidx.work.WorkerParameters
import androidx.work.workDataOf import androidx.work.workDataOf
import com.hippo.unifile.UniFile import com.hippo.unifile.UniFile
@ -24,9 +24,9 @@ import uy.kohesive.injekt.api.get
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
class BackupCreatorJob(private val context: Context, workerParams: WorkerParameters) : class BackupCreatorJob(private val context: Context, workerParams: WorkerParameters) :
Worker(context, workerParams) { CoroutineWorker(context, workerParams) {
override fun doWork(): Result { override suspend fun doWork(): Result {
val preferences = Injekt.get<PreferencesHelper>() val preferences = Injekt.get<PreferencesHelper>()
val notifier = BackupNotifier(context) val notifier = BackupNotifier(context)
val uri = inputData.getString(LOCATION_URI_KEY)?.let { Uri.parse(it) } val uri = inputData.getString(LOCATION_URI_KEY)?.let { Uri.parse(it) }

@ -3,6 +3,9 @@ package eu.kanade.tachiyomi.data.backup.full
import android.content.Context import android.content.Context
import android.net.Uri import android.net.Uri
import com.hippo.unifile.UniFile import com.hippo.unifile.UniFile
import data.Manga_sync
import data.Mangas
import eu.kanade.domain.history.model.HistoryUpdate
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.backup.AbstractBackupManager import eu.kanade.tachiyomi.data.backup.AbstractBackupManager
import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_CATEGORY import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_CATEGORY
@ -15,17 +18,16 @@ import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_TRACK
import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_TRACK_MASK import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_TRACK_MASK
import eu.kanade.tachiyomi.data.backup.full.models.Backup import eu.kanade.tachiyomi.data.backup.full.models.Backup
import eu.kanade.tachiyomi.data.backup.full.models.BackupCategory import eu.kanade.tachiyomi.data.backup.full.models.BackupCategory
import eu.kanade.tachiyomi.data.backup.full.models.BackupChapter
import eu.kanade.tachiyomi.data.backup.full.models.BackupFull import eu.kanade.tachiyomi.data.backup.full.models.BackupFull
import eu.kanade.tachiyomi.data.backup.full.models.BackupHistory import eu.kanade.tachiyomi.data.backup.full.models.BackupHistory
import eu.kanade.tachiyomi.data.backup.full.models.BackupManga import eu.kanade.tachiyomi.data.backup.full.models.BackupManga
import eu.kanade.tachiyomi.data.backup.full.models.BackupSerializer import eu.kanade.tachiyomi.data.backup.full.models.BackupSerializer
import eu.kanade.tachiyomi.data.backup.full.models.BackupSource import eu.kanade.tachiyomi.data.backup.full.models.BackupSource
import eu.kanade.tachiyomi.data.backup.full.models.BackupTracking import eu.kanade.tachiyomi.data.backup.full.models.backupCategoryMapper
import eu.kanade.tachiyomi.data.backup.full.models.backupChapterMapper
import eu.kanade.tachiyomi.data.backup.full.models.backupTrackMapper
import eu.kanade.tachiyomi.data.database.models.Chapter import eu.kanade.tachiyomi.data.database.models.Chapter
import eu.kanade.tachiyomi.data.database.models.History
import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.database.models.MangaCategory
import eu.kanade.tachiyomi.data.database.models.Track import eu.kanade.tachiyomi.data.database.models.Track
import eu.kanade.tachiyomi.util.system.logcat import eu.kanade.tachiyomi.util.system.logcat
import kotlinx.serialization.protobuf.ProtoBuf import kotlinx.serialization.protobuf.ProtoBuf
@ -34,6 +36,7 @@ import okio.buffer
import okio.gzip import okio.gzip
import okio.sink import okio.sink
import java.io.FileOutputStream import java.io.FileOutputStream
import java.util.Date
import kotlin.math.max import kotlin.math.max
class FullBackupManager(context: Context) : AbstractBackupManager(context) { class FullBackupManager(context: Context) : AbstractBackupManager(context) {
@ -46,20 +49,18 @@ class FullBackupManager(context: Context) : AbstractBackupManager(context) {
* @param uri path of Uri * @param uri path of Uri
* @param isAutoBackup backup called from scheduled backup job * @param isAutoBackup backup called from scheduled backup job
*/ */
override fun createBackup(uri: Uri, flags: Int, isAutoBackup: Boolean): String { override suspend fun createBackup(uri: Uri, flags: Int, isAutoBackup: Boolean): String {
// Create root object // Create root object
var backup: Backup? = null var backup: Backup? = null
db.inTransaction { val databaseManga = getFavoriteManga()
val databaseManga = getFavoriteManga()
backup = Backup( backup = Backup(
backupManga(databaseManga, flags), backupManga(databaseManga, flags),
backupCategories(flags), backupCategories(flags),
emptyList(), emptyList(),
backupExtensionInfo(databaseManga), backupExtensionInfo(databaseManga),
) )
}
var file: UniFile? = null var file: UniFile? = null
try { try {
@ -112,13 +113,13 @@ class FullBackupManager(context: Context) : AbstractBackupManager(context) {
} }
} }
private fun backupManga(mangas: List<Manga>, flags: Int): List<BackupManga> { private suspend fun backupManga(mangas: List<Mangas>, flags: Int): List<BackupManga> {
return mangas.map { return mangas.map {
backupMangaObject(it, flags) backupMangaObject(it, flags)
} }
} }
private fun backupExtensionInfo(mangas: List<Manga>): List<BackupSource> { private fun backupExtensionInfo(mangas: List<Mangas>): List<BackupSource> {
return mangas return mangas
.asSequence() .asSequence()
.map { it.source } .map { it.source }
@ -133,12 +134,10 @@ class FullBackupManager(context: Context) : AbstractBackupManager(context) {
* *
* @return list of [BackupCategory] to be backed up * @return list of [BackupCategory] to be backed up
*/ */
private fun backupCategories(options: Int): List<BackupCategory> { private suspend fun backupCategories(options: Int): List<BackupCategory> {
// Check if user wants category information in backup // Check if user wants category information in backup
return if (options and BACKUP_CATEGORY_MASK == BACKUP_CATEGORY) { return if (options and BACKUP_CATEGORY_MASK == BACKUP_CATEGORY) {
db.getCategories() handler.awaitList { categoriesQueries.getCategories(backupCategoryMapper) }
.executeAsBlocking()
.map { BackupCategory.copyFrom(it) }
} else { } else {
emptyList() emptyList()
} }
@ -151,43 +150,43 @@ class FullBackupManager(context: Context) : AbstractBackupManager(context) {
* @param options options for the backup * @param options options for the backup
* @return [BackupManga] containing manga in a serializable form * @return [BackupManga] containing manga in a serializable form
*/ */
private fun backupMangaObject(manga: Manga, options: Int): BackupManga { private suspend fun backupMangaObject(manga: Mangas, options: Int): BackupManga {
// Entry for this manga // Entry for this manga
val mangaObject = BackupManga.copyFrom(manga) val mangaObject = BackupManga.copyFrom(manga)
// Check if user wants chapter information in backup // Check if user wants chapter information in backup
if (options and BACKUP_CHAPTER_MASK == BACKUP_CHAPTER) { if (options and BACKUP_CHAPTER_MASK == BACKUP_CHAPTER) {
// Backup all the chapters // Backup all the chapters
val chapters = db.getChapters(manga).executeAsBlocking() val chapters = handler.awaitList { chaptersQueries.getChaptersByMangaId(manga._id, backupChapterMapper) }
if (chapters.isNotEmpty()) { if (chapters.isNotEmpty()) {
mangaObject.chapters = chapters.map { BackupChapter.copyFrom(it) } mangaObject.chapters = chapters
} }
} }
// Check if user wants category information in backup // Check if user wants category information in backup
if (options and BACKUP_CATEGORY_MASK == BACKUP_CATEGORY) { if (options and BACKUP_CATEGORY_MASK == BACKUP_CATEGORY) {
// Backup categories for this manga // Backup categories for this manga
val categoriesForManga = db.getCategoriesForManga(manga).executeAsBlocking() val categoriesForManga = handler.awaitList { categoriesQueries.getCategoriesByMangaId(manga._id) }
if (categoriesForManga.isNotEmpty()) { if (categoriesForManga.isNotEmpty()) {
mangaObject.categories = categoriesForManga.mapNotNull { it.order } mangaObject.categories = categoriesForManga.map { it.order }
} }
} }
// Check if user wants track information in backup // Check if user wants track information in backup
if (options and BACKUP_TRACK_MASK == BACKUP_TRACK) { if (options and BACKUP_TRACK_MASK == BACKUP_TRACK) {
val tracks = db.getTracks(manga.id).executeAsBlocking() val tracks = handler.awaitList { manga_syncQueries.getTracksByMangaId(manga._id, backupTrackMapper) }
if (tracks.isNotEmpty()) { if (tracks.isNotEmpty()) {
mangaObject.tracking = tracks.map { BackupTracking.copyFrom(it) } mangaObject.tracking = tracks
} }
} }
// Check if user wants history information in backup // Check if user wants history information in backup
if (options and BACKUP_HISTORY_MASK == BACKUP_HISTORY) { if (options and BACKUP_HISTORY_MASK == BACKUP_HISTORY) {
val historyForManga = db.getHistoryByMangaId(manga.id!!).executeAsBlocking() val historyByMangaId = handler.awaitList(true) { historyQueries.getHistoryByMangaId(manga._id) }
if (historyForManga.isNotEmpty()) { if (historyByMangaId.isNotEmpty()) {
val history = historyForManga.mapNotNull { history -> val history = historyByMangaId.map { history ->
val url = db.getChapter(history.chapter_id).executeAsBlocking()?.url val chapter = handler.awaitOne { chaptersQueries.getChapterById(history.chapter_id) }
url?.let { BackupHistory(url, history.last_read) } BackupHistory(chapter.url, history.last_read?.time ?: 0L)
} }
if (history.isNotEmpty()) { if (history.isNotEmpty()) {
mangaObject.history = history mangaObject.history = history
@ -198,10 +197,10 @@ class FullBackupManager(context: Context) : AbstractBackupManager(context) {
return mangaObject return mangaObject
} }
fun restoreMangaNoFetch(manga: Manga, dbManga: Manga) { suspend fun restoreMangaNoFetch(manga: Manga, dbManga: Mangas) {
manga.id = dbManga.id manga.id = dbManga._id
manga.copyFrom(dbManga) manga.copyFrom(dbManga)
insertManga(manga) updateManga(manga)
} }
/** /**
@ -210,7 +209,7 @@ class FullBackupManager(context: Context) : AbstractBackupManager(context) {
* @param manga manga that needs updating * @param manga manga that needs updating
* @return Updated manga info. * @return Updated manga info.
*/ */
fun restoreManga(manga: Manga): Manga { suspend fun restoreManga(manga: Manga): Manga {
return manga.also { return manga.also {
it.initialized = it.description != null it.initialized = it.description != null
it.id = insertManga(it) it.id = insertManga(it)
@ -222,32 +221,36 @@ class FullBackupManager(context: Context) : AbstractBackupManager(context) {
* *
* @param backupCategories list containing categories * @param backupCategories list containing categories
*/ */
internal fun restoreCategories(backupCategories: List<BackupCategory>) { internal suspend fun restoreCategories(backupCategories: List<BackupCategory>) {
// Get categories from file and from db // Get categories from file and from db
val dbCategories = db.getCategories().executeAsBlocking() val dbCategories = handler.awaitList { categoriesQueries.getCategories() }
// Iterate over them // Iterate over them
backupCategories.map { it.getCategoryImpl() }.forEach { category -> backupCategories
// Used to know if the category is already in the db .map { it.getCategoryImpl() }
var found = false .forEach { category ->
for (dbCategory in dbCategories) { // Used to know if the category is already in the db
// If the category is already in the db, assign the id to the file's category var found = false
// and do nothing for (dbCategory in dbCategories) {
if (category.name == dbCategory.name) { // If the category is already in the db, assign the id to the file's category
category.id = dbCategory.id // and do nothing
found = true if (category.name == dbCategory.name) {
break category.id = dbCategory.id.toInt()
found = true
break
}
}
// If the category isn't in the db, remove the id and insert a new category
// Store the inserted id in the category
if (!found) {
// Let the db assign the id
category.id = null
category.id = handler.awaitOne {
categoriesQueries.insert(category.name, category.order.toLong(), category.flags.toLong())
categoriesQueries.selectLastInsertedRowId()
}.toInt()
} }
} }
// If the category isn't in the db, remove the id and insert a new category
// Store the inserted id in the category
if (!found) {
// Let the db assign the id
category.id = null
val result = db.insertCategory(category).executeAsBlocking()
category.id = result.insertedId()?.toInt()
}
}
} }
/** /**
@ -256,25 +259,30 @@ class FullBackupManager(context: Context) : AbstractBackupManager(context) {
* @param manga the manga whose categories have to be restored. * @param manga the manga whose categories have to be restored.
* @param categories the categories to restore. * @param categories the categories to restore.
*/ */
internal fun restoreCategoriesForManga(manga: Manga, categories: List<Int>, backupCategories: List<BackupCategory>) { internal suspend fun restoreCategoriesForManga(manga: Manga, categories: List<Int>, backupCategories: List<BackupCategory>) {
val dbCategories = db.getCategories().executeAsBlocking() val dbCategories = handler.awaitList { categoriesQueries.getCategories() }
val mangaCategoriesToUpdate = ArrayList<MangaCategory>(categories.size) val mangaCategoriesToUpdate = mutableListOf<Pair<Long, Long>>()
categories.forEach { backupCategoryOrder -> categories.forEach { backupCategoryOrder ->
backupCategories.firstOrNull { backupCategories.firstOrNull {
it.order == backupCategoryOrder it.order == backupCategoryOrder.toLong()
}?.let { backupCategory -> }?.let { backupCategory ->
dbCategories.firstOrNull { dbCategory -> dbCategories.firstOrNull { dbCategory ->
dbCategory.name == backupCategory.name dbCategory.name == backupCategory.name
}?.let { dbCategory -> }?.let { dbCategory ->
mangaCategoriesToUpdate += MangaCategory.create(manga, dbCategory) mangaCategoriesToUpdate.add(Pair(manga.id!!, dbCategory.id))
} }
} }
} }
// Update database // Update database
if (mangaCategoriesToUpdate.isNotEmpty()) { if (mangaCategoriesToUpdate.isNotEmpty()) {
db.deleteOldMangasCategories(listOf(manga)).executeAsBlocking() handler.await(true) {
db.insertMangasCategories(mangaCategoriesToUpdate).executeAsBlocking() mangas_categoriesQueries.deleteMangaCategoryByMangaId(manga.id!!)
mangaCategoriesToUpdate.forEach { (mangaId, categoryId) ->
mangas_categoriesQueries.insert(mangaId, categoryId)
}
}
} }
} }
@ -283,28 +291,43 @@ class FullBackupManager(context: Context) : AbstractBackupManager(context) {
* *
* @param history list containing history to be restored * @param history list containing history to be restored
*/ */
internal fun restoreHistoryForManga(history: List<BackupHistory>) { internal suspend fun restoreHistoryForManga(history: List<BackupHistory>) {
// List containing history to be updated // List containing history to be updated
val historyToBeUpdated = ArrayList<History>(history.size) val toUpdate = mutableListOf<HistoryUpdate>()
for ((url, lastRead) in history) { for ((url, lastRead) in history) {
val dbHistory = db.getHistoryByChapterUrl(url).executeAsBlocking() var dbHistory = handler.awaitOneOrNull { historyQueries.getHistoryByChapterUrl(url) }
// Check if history already in database and update // Check if history already in database and update
if (dbHistory != null) { if (dbHistory != null) {
dbHistory.apply { dbHistory = dbHistory.copy(last_read = Date(max(lastRead, dbHistory.last_read?.time ?: 0L)))
last_read = max(lastRead, dbHistory.last_read) toUpdate.add(
} HistoryUpdate(
historyToBeUpdated.add(dbHistory) chapterId = dbHistory.chapter_id,
readAt = dbHistory.last_read!!,
sessionReadDuration = dbHistory.time_read,
),
)
} else { } else {
// If not in database create // If not in database create
db.getChapter(url).executeAsBlocking()?.let { handler
val historyToAdd = History.create(it).apply { .awaitOneOrNull { chaptersQueries.getChapterByUrl(url) }
last_read = lastRead ?.let {
HistoryUpdate(
chapterId = it._id,
readAt = Date(lastRead),
sessionReadDuration = 0,
)
} }
historyToBeUpdated.add(historyToAdd)
}
} }
} }
db.upsertHistoryLastRead(historyToBeUpdated).executeAsBlocking() handler.await(true) {
toUpdate.forEach { payload ->
historyQueries.upsert(
payload.chapterId,
payload.readAt,
payload.sessionReadDuration,
)
}
}
} }
/** /**
@ -313,56 +336,97 @@ class FullBackupManager(context: Context) : AbstractBackupManager(context) {
* @param manga the manga whose sync have to be restored. * @param manga the manga whose sync have to be restored.
* @param tracks the track list to restore. * @param tracks the track list to restore.
*/ */
internal fun restoreTrackForManga(manga: Manga, tracks: List<Track>) { internal suspend fun restoreTrackForManga(manga: Manga, tracks: List<Track>) {
// Fix foreign keys with the current manga id // Fix foreign keys with the current manga id
tracks.map { it.manga_id = manga.id!! } tracks.map { it.manga_id = manga.id!! }
// Get tracks from database // Get tracks from database
val dbTracks = db.getTracks(manga.id).executeAsBlocking()
val trackToUpdate = mutableListOf<Track>() val dbTracks = handler.awaitList { manga_syncQueries.getTracksByMangaId(manga.id!!) }
val toUpdate = mutableListOf<Manga_sync>()
val toInsert = mutableListOf<Track>()
tracks.forEach { track -> tracks.forEach { track ->
var isInDatabase = false var isInDatabase = false
for (dbTrack in dbTracks) { for (dbTrack in dbTracks) {
if (track.sync_id == dbTrack.sync_id) { if (track.sync_id == dbTrack.sync_id.toInt()) {
// The sync is already in the db, only update its fields // The sync is already in the db, only update its fields
if (track.media_id != dbTrack.media_id) { var temp = dbTrack
dbTrack.media_id = track.media_id if (track.media_id != dbTrack.remote_id) {
temp = temp.copy(remote_id = track.media_id)
} }
if (track.library_id != dbTrack.library_id) { if (track.library_id != dbTrack.library_id) {
dbTrack.library_id = track.library_id temp = temp.copy(library_id = track.library_id)
} }
dbTrack.last_chapter_read = max(dbTrack.last_chapter_read, track.last_chapter_read) temp = temp.copy(last_chapter_read = max(dbTrack.last_chapter_read, track.last_chapter_read.toDouble()))
isInDatabase = true isInDatabase = true
trackToUpdate.add(dbTrack) toUpdate.add(temp)
break break
} }
} }
if (!isInDatabase) { if (!isInDatabase) {
// Insert new sync. Let the db assign the id // Insert new sync. Let the db assign the id
track.id = null track.id = null
trackToUpdate.add(track) toInsert.add(track)
} }
} }
// Update database // Update database
if (trackToUpdate.isNotEmpty()) { if (toUpdate.isNotEmpty()) {
db.insertTracks(trackToUpdate).executeAsBlocking() handler.await(true) {
toUpdate.forEach { track ->
manga_syncQueries.update(
track.manga_id,
track.sync_id,
track.remote_id,
track.library_id,
track.title,
track.last_chapter_read,
track.total_chapters,
track.status,
track.score.toDouble(),
track.remote_url,
track.start_date,
track.finish_date,
track._id,
)
}
}
}
if (toInsert.isNotEmpty()) {
handler.await(true) {
toInsert.forEach { track ->
manga_syncQueries.insert(
track.manga_id,
track.sync_id.toLong(),
track.media_id,
track.library_id,
track.title,
track.last_chapter_read.toDouble(),
track.total_chapters.toLong(),
track.status.toLong(),
track.score,
track.tracking_url,
track.started_reading_date,
track.finished_reading_date,
)
}
}
} }
} }
internal fun restoreChaptersForManga(manga: Manga, chapters: List<Chapter>) { internal suspend fun restoreChaptersForManga(manga: Manga, chapters: List<Chapter>) {
val dbChapters = db.getChapters(manga).executeAsBlocking() val dbChapters = handler.awaitList { chaptersQueries.getChaptersByMangaId(manga.id!!) }
chapters.forEach { chapter -> chapters.forEach { chapter ->
val dbChapter = dbChapters.find { it.url == chapter.url } val dbChapter = dbChapters.find { it.url == chapter.url }
if (dbChapter != null) { if (dbChapter != null) {
chapter.id = dbChapter.id chapter.id = dbChapter._id
chapter.copyFrom(dbChapter) chapter.copyFrom(dbChapter)
if (dbChapter.read && !chapter.read) { if (dbChapter.read && !chapter.read) {
chapter.read = dbChapter.read chapter.read = dbChapter.read
chapter.last_page_read = dbChapter.last_page_read chapter.last_page_read = dbChapter.last_page_read.toInt()
} else if (chapter.last_page_read == 0 && dbChapter.last_page_read != 0) { } else if (chapter.last_page_read == 0 && dbChapter.last_page_read != 0L) {
chapter.last_page_read = dbChapter.last_page_read chapter.last_page_read = dbChapter.last_page_read.toInt()
} }
if (!chapter.bookmark && dbChapter.bookmark) { if (!chapter.bookmark && dbChapter.bookmark) {
chapter.bookmark = dbChapter.bookmark chapter.bookmark = dbChapter.bookmark

@ -51,19 +51,17 @@ class FullBackupRestore(context: Context, notifier: BackupNotifier) : AbstractBa
return true return true
} }
private fun restoreCategories(backupCategories: List<BackupCategory>) { private suspend fun restoreCategories(backupCategories: List<BackupCategory>) {
db.inTransaction { backupManager.restoreCategories(backupCategories)
backupManager.restoreCategories(backupCategories)
}
restoreProgress += 1 restoreProgress += 1
showRestoreProgress(restoreProgress, restoreAmount, context.getString(R.string.categories)) showRestoreProgress(restoreProgress, restoreAmount, context.getString(R.string.categories))
} }
private fun restoreManga(backupManga: BackupManga, backupCategories: List<BackupCategory>) { private suspend fun restoreManga(backupManga: BackupManga, backupCategories: List<BackupCategory>) {
val manga = backupManga.getMangaImpl() val manga = backupManga.getMangaImpl()
val chapters = backupManga.getChaptersImpl() val chapters = backupManga.getChaptersImpl()
val categories = backupManga.categories val categories = backupManga.categories.map { it.toInt() }
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() val tracks = backupManga.getTrackingImpl()
@ -87,7 +85,7 @@ class FullBackupRestore(context: Context, notifier: BackupNotifier) : AbstractBa
* @param history history data from json * @param history history data from json
* @param tracks tracking data from json * @param tracks tracking data from json
*/ */
private fun restoreMangaData( private suspend fun restoreMangaData(
manga: Manga, manga: Manga,
chapters: List<Chapter>, chapters: List<Chapter>,
categories: List<Int>, categories: List<Int>,
@ -95,18 +93,16 @@ class FullBackupRestore(context: Context, notifier: BackupNotifier) : AbstractBa
tracks: List<Track>, tracks: List<Track>,
backupCategories: List<BackupCategory>, backupCategories: List<BackupCategory>,
) { ) {
db.inTransaction { val dbManga = backupManager.getMangaFromDatabase(manga.url, manga.source)
val dbManga = backupManager.getMangaFromDatabase(manga) if (dbManga == null) {
if (dbManga == null) { // Manga not in database
// Manga not in database restoreMangaFetch(manga, chapters, categories, history, tracks, backupCategories)
restoreMangaFetch(manga, chapters, categories, history, tracks, backupCategories) } else {
} else { // Manga in database
// Manga in database // Copy information from manga already in database
// Copy information from manga already in database backupManager.restoreMangaNoFetch(manga, dbManga)
backupManager.restoreMangaNoFetch(manga, dbManga) // Fetch rest of manga information
// Fetch rest of manga information restoreMangaNoFetch(manga, chapters, categories, history, tracks, backupCategories)
restoreMangaNoFetch(manga, chapters, categories, history, tracks, backupCategories)
}
} }
} }
@ -117,7 +113,7 @@ class FullBackupRestore(context: Context, notifier: BackupNotifier) : AbstractBa
* @param chapters chapters of manga that needs updating * @param chapters chapters of manga that needs updating
* @param categories categories that need updating * @param categories categories that need updating
*/ */
private fun restoreMangaFetch( private suspend fun restoreMangaFetch(
manga: Manga, manga: Manga,
chapters: List<Chapter>, chapters: List<Chapter>,
categories: List<Int>, categories: List<Int>,
@ -137,7 +133,7 @@ class FullBackupRestore(context: Context, notifier: BackupNotifier) : AbstractBa
} }
} }
private fun restoreMangaNoFetch( private suspend fun restoreMangaNoFetch(
backupManga: Manga, backupManga: Manga,
chapters: List<Chapter>, chapters: List<Chapter>,
categories: List<Int>, categories: List<Int>,
@ -150,7 +146,7 @@ class FullBackupRestore(context: Context, notifier: BackupNotifier) : AbstractBa
restoreExtraForManga(backupManga, categories, history, tracks, backupCategories) restoreExtraForManga(backupManga, categories, history, tracks, backupCategories)
} }
private fun restoreExtraForManga(manga: Manga, categories: List<Int>, history: List<BackupHistory>, tracks: List<Track>, backupCategories: List<BackupCategory>) { private suspend fun restoreExtraForManga(manga: Manga, categories: List<Int>, history: List<BackupHistory>, tracks: List<Track>, backupCategories: List<BackupCategory>) {
// Restore categories // Restore categories
backupManager.restoreCategoriesForManga(manga, categories, backupCategories) backupManager.restoreCategoriesForManga(manga, categories, backupCategories)

@ -1,6 +1,5 @@
package eu.kanade.tachiyomi.data.backup.full.models package eu.kanade.tachiyomi.data.backup.full.models
import eu.kanade.tachiyomi.data.database.models.Category
import eu.kanade.tachiyomi.data.database.models.CategoryImpl import eu.kanade.tachiyomi.data.database.models.CategoryImpl
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import kotlinx.serialization.protobuf.ProtoNumber import kotlinx.serialization.protobuf.ProtoNumber
@ -8,26 +7,24 @@ import kotlinx.serialization.protobuf.ProtoNumber
@Serializable @Serializable
class BackupCategory( class BackupCategory(
@ProtoNumber(1) var name: String, @ProtoNumber(1) var name: String,
@ProtoNumber(2) var order: Int = 0, @ProtoNumber(2) var order: Long = 0,
// @ProtoNumber(3) val updateInterval: Int = 0, 1.x value not used in 0.x // @ProtoNumber(3) val updateInterval: Int = 0, 1.x value not used in 0.x
// Bump by 100 to specify this is a 0.x value // Bump by 100 to specify this is a 0.x value
@ProtoNumber(100) var flags: Int = 0, @ProtoNumber(100) var flags: Long = 0,
) { ) {
fun getCategoryImpl(): CategoryImpl { fun getCategoryImpl(): CategoryImpl {
return CategoryImpl().apply { return CategoryImpl().apply {
name = this@BackupCategory.name name = this@BackupCategory.name
flags = this@BackupCategory.flags flags = this@BackupCategory.flags.toInt()
order = this@BackupCategory.order order = this@BackupCategory.order.toInt()
} }
} }
}
companion object { val backupCategoryMapper = { _: Long, name: String, order: Long, flags: Long ->
fun copyFrom(category: Category): BackupCategory { BackupCategory(
return BackupCategory( name = name,
name = category.name, order = order,
order = category.order, flags = flags,
flags = category.flags, )
)
}
}
} }

@ -1,6 +1,5 @@
package eu.kanade.tachiyomi.data.backup.full.models package eu.kanade.tachiyomi.data.backup.full.models
import eu.kanade.tachiyomi.data.database.models.Chapter
import eu.kanade.tachiyomi.data.database.models.ChapterImpl import eu.kanade.tachiyomi.data.database.models.ChapterImpl
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import kotlinx.serialization.protobuf.ProtoNumber import kotlinx.serialization.protobuf.ProtoNumber
@ -15,12 +14,12 @@ data class BackupChapter(
@ProtoNumber(4) var read: Boolean = false, @ProtoNumber(4) var read: Boolean = false,
@ProtoNumber(5) var bookmark: Boolean = false, @ProtoNumber(5) var bookmark: Boolean = false,
// lastPageRead is called progress in 1.x // lastPageRead is called progress in 1.x
@ProtoNumber(6) var lastPageRead: Int = 0, @ProtoNumber(6) var lastPageRead: Long = 0,
@ProtoNumber(7) var dateFetch: Long = 0, @ProtoNumber(7) var dateFetch: Long = 0,
@ProtoNumber(8) var dateUpload: Long = 0, @ProtoNumber(8) var dateUpload: Long = 0,
// chapterNumber is called number is 1.x // chapterNumber is called number is 1.x
@ProtoNumber(9) var chapterNumber: Float = 0F, @ProtoNumber(9) var chapterNumber: Float = 0F,
@ProtoNumber(10) var sourceOrder: Int = 0, @ProtoNumber(10) var sourceOrder: Long = 0,
) { ) {
fun toChapterImpl(): ChapterImpl { fun toChapterImpl(): ChapterImpl {
return ChapterImpl().apply { return ChapterImpl().apply {
@ -30,27 +29,37 @@ data class BackupChapter(
scanlator = this@BackupChapter.scanlator scanlator = this@BackupChapter.scanlator
read = this@BackupChapter.read read = this@BackupChapter.read
bookmark = this@BackupChapter.bookmark bookmark = this@BackupChapter.bookmark
last_page_read = this@BackupChapter.lastPageRead last_page_read = this@BackupChapter.lastPageRead.toInt()
date_fetch = this@BackupChapter.dateFetch date_fetch = this@BackupChapter.dateFetch
date_upload = this@BackupChapter.dateUpload date_upload = this@BackupChapter.dateUpload
source_order = this@BackupChapter.sourceOrder source_order = this@BackupChapter.sourceOrder.toInt()
} }
} }
}
companion object { val backupChapterMapper = {
fun copyFrom(chapter: Chapter): BackupChapter { _: Long,
return BackupChapter( _: Long,
url = chapter.url, url: String,
name = chapter.name, name: String,
chapterNumber = chapter.chapter_number, scanlator: String?,
scanlator = chapter.scanlator, read: Boolean,
read = chapter.read, bookmark: Boolean,
bookmark = chapter.bookmark, lastPageRead: Long,
lastPageRead = chapter.last_page_read, chapterNumber: Float,
dateFetch = chapter.date_fetch, source_order: Long,
dateUpload = chapter.date_upload, dateFetch: Long,
sourceOrder = chapter.source_order, dateUpload: Long, ->
) BackupChapter(
} url = url,
} name = name,
chapterNumber = chapterNumber,
scanlator = scanlator,
read = read,
bookmark = bookmark,
lastPageRead = lastPageRead,
dateFetch = dateFetch,
dateUpload = dateUpload,
sourceOrder = source_order,
)
} }

@ -1,9 +1,10 @@
package eu.kanade.tachiyomi.data.backup.full.models package eu.kanade.tachiyomi.data.backup.full.models
import data.Mangas
import eu.kanade.tachiyomi.data.database.models.ChapterImpl import eu.kanade.tachiyomi.data.database.models.ChapterImpl
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.database.models.MangaImpl import eu.kanade.tachiyomi.data.database.models.MangaImpl
import eu.kanade.tachiyomi.data.database.models.TrackImpl import eu.kanade.tachiyomi.data.database.models.TrackImpl
import eu.kanade.tachiyomi.ui.reader.setting.ReadingModeType
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import kotlinx.serialization.protobuf.ProtoNumber import kotlinx.serialization.protobuf.ProtoNumber
@ -28,7 +29,7 @@ data class BackupManga(
@ProtoNumber(14) var viewer: Int = 0, // Replaced by viewer_flags @ProtoNumber(14) var viewer: Int = 0, // Replaced by viewer_flags
// @ProtoNumber(15) val flags: Int = 0, 1.x value, not used in 0.x // @ProtoNumber(15) val flags: Int = 0, 1.x value, not used in 0.x
@ProtoNumber(16) var chapters: List<BackupChapter> = emptyList(), @ProtoNumber(16) var chapters: List<BackupChapter> = emptyList(),
@ProtoNumber(17) var categories: List<Int> = emptyList(), @ProtoNumber(17) var categories: List<Long> = emptyList(),
@ProtoNumber(18) var tracking: List<BackupTracking> = emptyList(), @ProtoNumber(18) var tracking: List<BackupTracking> = emptyList(),
// Bump by 100 for values that are not saved/implemented in 1.x but are used in 0.x // Bump by 100 for values that are not saved/implemented in 1.x but are used in 0.x
@ProtoNumber(100) var favorite: Boolean = true, @ProtoNumber(100) var favorite: Boolean = true,
@ -68,22 +69,22 @@ data class BackupManga(
} }
companion object { companion object {
fun copyFrom(manga: Manga): BackupManga { fun copyFrom(manga: Mangas): BackupManga {
return BackupManga( return BackupManga(
url = manga.url, url = manga.url,
title = manga.title, title = manga.title,
artist = manga.artist, artist = manga.artist,
author = manga.author, author = manga.author,
description = manga.description, description = manga.description,
genre = manga.getGenres() ?: emptyList(), genre = manga.genre ?: emptyList(),
status = manga.status, status = manga.status.toInt(),
thumbnailUrl = manga.thumbnail_url, thumbnailUrl = manga.thumbnail_url,
favorite = manga.favorite, favorite = manga.favorite,
source = manga.source, source = manga.source,
dateAdded = manga.date_added, dateAdded = manga.date_added,
viewer = manga.readingModeType, viewer = (manga.viewer.toInt() and ReadingModeType.MASK),
viewer_flags = manga.viewer_flags, viewer_flags = manga.viewer.toInt(),
chapterFlags = manga.chapter_flags, chapterFlags = manga.chapter_flags.toInt(),
) )
} }
} }

@ -1,6 +1,5 @@
package eu.kanade.tachiyomi.data.backup.full.models package eu.kanade.tachiyomi.data.backup.full.models
import eu.kanade.tachiyomi.data.database.models.Track
import eu.kanade.tachiyomi.data.database.models.TrackImpl import eu.kanade.tachiyomi.data.database.models.TrackImpl
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import kotlinx.serialization.protobuf.ProtoNumber import kotlinx.serialization.protobuf.ProtoNumber
@ -48,23 +47,34 @@ data class BackupTracking(
tracking_url = this@BackupTracking.trackingUrl tracking_url = this@BackupTracking.trackingUrl
} }
} }
}
companion object { val backupTrackMapper = {
fun copyFrom(track: Track): BackupTracking { _id: Long,
return BackupTracking( manga_id: Long,
syncId = track.sync_id, syncId: Long,
mediaId = track.media_id, mediaId: Long,
// forced not null so its compatible with 1.x backup system libraryId: Long?,
libraryId = track.library_id!!, title: String,
title = track.title, lastChapterRead: Double,
lastChapterRead = track.last_chapter_read, totalChapters: Long,
totalChapters = track.total_chapters, status: Long,
score = track.score, score: Float,
status = track.status, remoteUrl: String,
startedReadingDate = track.started_reading_date, startDate: Long,
finishedReadingDate = track.finished_reading_date, finishDate: Long, ->
trackingUrl = track.tracking_url, BackupTracking(
) syncId = syncId.toInt(),
} mediaId = mediaId,
} // forced not null so its compatible with 1.x backup system
libraryId = libraryId ?: 0,
title = title,
lastChapterRead = lastChapterRead.toFloat(),
totalChapters = totalChapters.toInt(),
score = score,
status = status.toInt(),
startedReadingDate = startDate,
finishedReadingDate = finishDate,
trackingUrl = remoteUrl,
)
} }

@ -16,7 +16,6 @@ import eu.kanade.tachiyomi.data.database.models.MangaCategory
import eu.kanade.tachiyomi.data.database.models.Track import eu.kanade.tachiyomi.data.database.models.Track
import eu.kanade.tachiyomi.data.database.queries.CategoryQueries import eu.kanade.tachiyomi.data.database.queries.CategoryQueries
import eu.kanade.tachiyomi.data.database.queries.ChapterQueries import eu.kanade.tachiyomi.data.database.queries.ChapterQueries
import eu.kanade.tachiyomi.data.database.queries.HistoryQueries
import eu.kanade.tachiyomi.data.database.queries.MangaCategoryQueries import eu.kanade.tachiyomi.data.database.queries.MangaCategoryQueries
import eu.kanade.tachiyomi.data.database.queries.MangaQueries import eu.kanade.tachiyomi.data.database.queries.MangaQueries
import eu.kanade.tachiyomi.data.database.queries.TrackQueries import eu.kanade.tachiyomi.data.database.queries.TrackQueries
@ -27,7 +26,7 @@ import eu.kanade.tachiyomi.data.database.queries.TrackQueries
class DatabaseHelper( class DatabaseHelper(
openHelper: SupportSQLiteOpenHelper, openHelper: SupportSQLiteOpenHelper,
) : ) :
MangaQueries, ChapterQueries, TrackQueries, CategoryQueries, MangaCategoryQueries, HistoryQueries { MangaQueries, ChapterQueries, TrackQueries, CategoryQueries, MangaCategoryQueries {
override val db = DefaultStorIOSQLite.builder() override val db = DefaultStorIOSQLite.builder()
.sqliteOpenHelper(openHelper) .sqliteOpenHelper(openHelper)

@ -1,5 +1,7 @@
package eu.kanade.tachiyomi.data.database.models package eu.kanade.tachiyomi.data.database.models
import data.GetCategories
class MangaCategory { class MangaCategory {
var id: Long? = null var id: Long? = null
@ -16,5 +18,12 @@ class MangaCategory {
mc.category_id = category.id!! mc.category_id = category.id!!
return mc return mc
} }
fun create(manga: Manga, category: GetCategories): MangaCategory {
val mc = MangaCategory()
mc.manga_id = manga.id!!
mc.category_id = category.id.toInt()
return mc
}
} }
} }

@ -1,42 +0,0 @@
package eu.kanade.tachiyomi.data.database.queries
import com.pushtorefresh.storio.sqlite.queries.RawQuery
import eu.kanade.tachiyomi.data.database.DbProvider
import eu.kanade.tachiyomi.data.database.models.History
import eu.kanade.tachiyomi.data.database.resolvers.HistoryUpsertResolver
import eu.kanade.tachiyomi.data.database.tables.HistoryTable
interface HistoryQueries : DbProvider {
fun getHistoryByMangaId(mangaId: Long) = db.get()
.listOfObjects(History::class.java)
.withQuery(
RawQuery.builder()
.query(getHistoryByMangaId())
.args(mangaId)
.observesTables(HistoryTable.TABLE)
.build(),
)
.prepare()
fun getHistoryByChapterUrl(chapterUrl: String) = db.get()
.`object`(History::class.java)
.withQuery(
RawQuery.builder()
.query(getHistoryByChapterUrl())
.args(chapterUrl)
.observesTables(HistoryTable.TABLE)
.build(),
)
.prepare()
/**
* Updates the history last read.
* Inserts history object if not yet in database
* @param historyList history object list
*/
fun upsertHistoryLastRead(historyList: List<History>) = db.put()
.objects(historyList)
.withPutResolver(HistoryUpsertResolver())
.prepare()
}

@ -1,5 +1,6 @@
package eu.kanade.tachiyomi.source.model package eu.kanade.tachiyomi.source.model
import data.Chapters
import tachiyomi.source.model.ChapterInfo import tachiyomi.source.model.ChapterInfo
import java.io.Serializable import java.io.Serializable
@ -23,6 +24,14 @@ interface SChapter : Serializable {
scanlator = other.scanlator scanlator = other.scanlator
} }
fun copyFrom(other: Chapters) {
name = other.name
url = other.url
date_upload = other.date_upload
chapter_number = other.chapter_number
scanlator = other.scanlator
}
companion object { companion object {
fun create(): SChapter { fun create(): SChapter {
return SChapterImpl() return SChapterImpl()

@ -1,5 +1,6 @@
package eu.kanade.tachiyomi.source.model package eu.kanade.tachiyomi.source.model
import data.Mangas
import tachiyomi.source.model.MangaInfo import tachiyomi.source.model.MangaInfo
import java.io.Serializable import java.io.Serializable
@ -51,6 +52,34 @@ interface SManga : Serializable {
} }
} }
fun copyFrom(other: Mangas) {
if (other.author != null) {
author = other.author
}
if (other.artist != null) {
artist = other.artist
}
if (other.description != null) {
description = other.description
}
if (other.genre != null) {
genre = other.genre.joinToString(separator = ", ")
}
if (other.thumbnail_url != null) {
thumbnail_url = other.thumbnail_url
}
status = other.status.toInt()
if (!initialized) {
initialized = other.initialized
}
}
companion object { companion object {
const val UNKNOWN = 0 const val UNKNOWN = 0
const val ONGOING = 1 const val ONGOING = 1

@ -3,4 +3,30 @@ CREATE TABLE categories(
name TEXT NOT NULL, name TEXT NOT NULL,
sort INTEGER NOT NULL, sort INTEGER NOT NULL,
flags INTEGER NOT NULL flags INTEGER NOT NULL
); );
getCategories:
SELECT
_id AS id,
name,
sort AS `order`,
flags
FROM categories;
getCategoriesByMangaId:
SELECT
C._id AS id,
C.name,
C.sort AS `order`,
C.flags
FROM categories C
JOIN mangas_categories MC
ON C._id = MC.category_id
WHERE MC.manga_id = :mangaId;
insert:
INSERT INTO categories(name, sort, flags)
VALUES (:name, :order, :flags);
selectLastInsertedRowId:
SELECT last_insert_rowid();

@ -28,37 +28,18 @@ SELECT *
FROM chapters FROM chapters
WHERE manga_id = :mangaId; WHERE manga_id = :mangaId;
getChapterByUrl:
SELECT *
FROM chapters
WHERE url = :chapterUrl;
removeChaptersWithIds: removeChaptersWithIds:
DELETE FROM chapters DELETE FROM chapters
WHERE _id IN :chapterIds; WHERE _id IN :chapterIds;
insert: insert:
INSERT INTO chapters( INSERT INTO chapters(manga_id,url,name,scanlator,read,bookmark,last_page_read,chapter_number,source_order,date_fetch,date_upload)
manga_id, VALUES (:mangaId,:url,:name,:scanlator,:read,:bookmark,:lastPageRead,:chapterNumber,:sourceOrder,:dateFetch,:dateUpload);
url,
name,
scanlator,
read,
bookmark,
last_page_read,
chapter_number,
source_order,
date_fetch,
date_upload
)
VALUES (
:mangaId,
:url,
:name,
:scanlator,
:read,
:bookmark,
:lastPageRead,
:chapterNumber,
:sourceOrder,
:dateFetch,
:dateUpload
);
update: update:
UPDATE chapters UPDATE chapters

@ -11,6 +11,28 @@ CREATE TABLE history(
CREATE INDEX history_history_chapter_id_index ON history(chapter_id); CREATE INDEX history_history_chapter_id_index ON history(chapter_id);
getHistoryByMangaId:
SELECT
H._id,
H.chapter_id,
H.last_read,
H.time_read
FROM history H
JOIN chapters C
ON H.chapter_id = C._id
WHERE C.manga_id = :mangaId AND C._id = H.chapter_id;
getHistoryByChapterUrl:
SELECT
H._id,
H.chapter_id,
H.last_read,
H.time_read
FROM history H
JOIN chapters C
ON H.chapter_id = C._id
WHERE C.url = :chapterUrl AND C._id = H.chapter_id;
resetHistoryById: resetHistoryById:
UPDATE history UPDATE history
SET last_read = 0 SET last_read = 0

@ -15,4 +15,30 @@ CREATE TABLE manga_sync(
UNIQUE (manga_id, sync_id) ON CONFLICT REPLACE, UNIQUE (manga_id, sync_id) ON CONFLICT REPLACE,
FOREIGN KEY(manga_id) REFERENCES mangas (_id) FOREIGN KEY(manga_id) REFERENCES mangas (_id)
ON DELETE CASCADE ON DELETE CASCADE
); );
getTracksByMangaId:
SELECT *
FROM manga_sync
WHERE manga_id = :mangaId;
insert:
INSERT INTO manga_sync(manga_id,sync_id,remote_id,library_id,title,last_chapter_read,total_chapters,status,score,remote_url,start_date,finish_date)
VALUES (:mangaId,:syncId,:remoteId,:libraryId,:title,:lastChapterRead,:totalChapters,:status,:score,:remoteUrl,:startDate,:finishDate);
update:
UPDATE manga_sync
SET
manga_id = coalesce(:mangaId, manga_id),
sync_id = coalesce(:syncId, sync_id),
remote_id = coalesce(:mediaId, remote_id),
library_id = coalesce(:libraryId, library_id),
title = coalesce(:title, title),
last_chapter_read = coalesce(:lastChapterRead, last_chapter_read),
total_chapters = coalesce(:totalChapter, total_chapters),
status = coalesce(:status, status),
score = coalesce(:score, score),
remote_url = coalesce(:trackingUrl, remote_url),
start_date = coalesce(:startDate, start_date),
finish_date = coalesce(:finishDate, finish_date)
WHERE _id = :id;

@ -25,11 +25,25 @@ CREATE TABLE mangas(
CREATE INDEX library_favorite_index ON mangas(favorite) WHERE favorite = 1; CREATE INDEX library_favorite_index ON mangas(favorite) WHERE favorite = 1;
CREATE INDEX mangas_url_index ON mangas(url); CREATE INDEX mangas_url_index ON mangas(url);
insert:
INSERT INTO mangas(source,url,artist,author,description,genre,title,status,thumbnail_url,favorite,last_update,next_update,initialized,viewer,chapter_flags,cover_last_modified,date_added)
VALUES (:source,:url,:artist,:author,:description,:genre,:title,:status,:thumbnail_url,:favorite,:last_update,:next_update,:initialized,:viewer,:chapter_flags,:cover_last_modified,:date_added);
getMangaById: getMangaById:
SELECT * SELECT *
FROM mangas FROM mangas
WHERE _id = :id; WHERE _id = :id;
getMangaByUrlAndSource:
SELECT *
FROM mangas
WHERE url = :url AND source = :source;
getFavorites:
SELECT *
FROM mangas
WHERE favorite = 1;
getSourceIdWithFavoriteCount: getSourceIdWithFavoriteCount:
SELECT SELECT
source, source,
@ -77,3 +91,6 @@ UPDATE mangas SET
cover_last_modified = coalesce(:coverLastModified, cover_last_modified), cover_last_modified = coalesce(:coverLastModified, cover_last_modified),
date_added = coalesce(:dateAdded, date_added) date_added = coalesce(:dateAdded, date_added)
WHERE _id = :mangaId; WHERE _id = :mangaId;
selectLastInsertedRowId:
SELECT last_insert_rowid();

@ -6,4 +6,12 @@ CREATE TABLE mangas_categories(
ON DELETE CASCADE, ON DELETE CASCADE,
FOREIGN KEY(manga_id) REFERENCES mangas (_id) FOREIGN KEY(manga_id) REFERENCES mangas (_id)
ON DELETE CASCADE ON DELETE CASCADE
); );
insert:
INSERT INTO mangas_categories(manga_id, category_id)
VALUES (:mangaId, :categoryId);
deleteMangaCategoryByMangaId:
DELETE FROM mangas_categories
WHERE manga_id = :mangaId;
Loading…
Cancel
Save