diff --git a/app/src/main/java/eu/kanade/data/DatabaseAdapter.kt b/app/src/main/java/eu/kanade/data/DatabaseAdapter.kt index 727d2cc16..b40e89899 100644 --- a/app/src/main/java/eu/kanade/data/DatabaseAdapter.kt +++ b/app/src/main/java/eu/kanade/data/DatabaseAdapter.kt @@ -30,4 +30,15 @@ val listOfStringsAndAdapter = object : ColumnAdapter, String> { } override fun encode(value: List) = value.joinToString(separator = listOfStringsAndSeparator) } + +private const val listOfLongsSeparator = "/" +val listOfLongsAdapter = object : ColumnAdapter, String> { + override fun decode(databaseValue: String) = + if (databaseValue.isEmpty()) { + emptyList() + } else { + databaseValue.split(listOfLongsSeparator).mapNotNull { it.toLongOrNull() } + } + override fun encode(value: List) = value.joinToString(separator = listOfLongsSeparator) +} // SY <-- diff --git a/app/src/main/java/eu/kanade/data/exh/MergedMangaReferenceMapper.kt b/app/src/main/java/eu/kanade/data/exh/MergedMangaReferenceMapper.kt new file mode 100644 index 000000000..57aaa52bc --- /dev/null +++ b/app/src/main/java/eu/kanade/data/exh/MergedMangaReferenceMapper.kt @@ -0,0 +1,30 @@ +package eu.kanade.data.exh + +import exh.merged.sql.models.MergedMangaReference + +val mergedMangaReferenceMapper = { + id: Long, + isInfoManga: Boolean, + getChapterUpdates: Boolean, + chapterSortMode: Long, + chapterPriority: Long, + downloadChapters: Boolean, + mergeId: Long, + mergeUrl: String, + mangaId: Long?, + mangaUrl: String, + mangaSourceId: Long, -> + MergedMangaReference( + id = id, + isInfoManga = isInfoManga, + getChapterUpdates = getChapterUpdates, + chapterSortMode = chapterSortMode.toInt(), + chapterPriority = chapterPriority.toInt(), + downloadChapters = downloadChapters, + mergeId = mergeId, + mergeUrl = mergeUrl, + mangaId = mangaId, + mangaUrl = mangaUrl, + mangaSourceId = mangaSourceId, + ) +} diff --git a/app/src/main/java/eu/kanade/data/exh/SearchMetadataMapper.kt b/app/src/main/java/eu/kanade/data/exh/SearchMetadataMapper.kt new file mode 100644 index 000000000..2956a6814 --- /dev/null +++ b/app/src/main/java/eu/kanade/data/exh/SearchMetadataMapper.kt @@ -0,0 +1,14 @@ +package eu.kanade.data.exh + +import exh.metadata.sql.models.SearchMetadata + +val searchMetadataMapper: (Long, String?, String, String?, Int) -> SearchMetadata = + { mangaId, uploader, extra, indexedExtra, extraVersion -> + SearchMetadata( + mangaId = mangaId, + uploader = uploader, + extra = extra, + indexedExtra = indexedExtra, + extraVersion = extraVersion, + ) + } diff --git a/app/src/main/java/eu/kanade/data/exh/SearchTag.kt b/app/src/main/java/eu/kanade/data/exh/SearchTag.kt new file mode 100644 index 000000000..ccc2c14eb --- /dev/null +++ b/app/src/main/java/eu/kanade/data/exh/SearchTag.kt @@ -0,0 +1,14 @@ +package eu.kanade.data.exh + +import exh.metadata.sql.models.SearchTag + +val searchTagMapper: (Long, Long, String?, String, Int) -> SearchTag = + { id, mangaId, namespace, name, type -> + SearchTag( + id = id, + mangaId = mangaId, + namespace = namespace, + name = name, + type = type, + ) + } diff --git a/app/src/main/java/eu/kanade/data/exh/SearchTitle.kt b/app/src/main/java/eu/kanade/data/exh/SearchTitle.kt new file mode 100644 index 000000000..1e641aa54 --- /dev/null +++ b/app/src/main/java/eu/kanade/data/exh/SearchTitle.kt @@ -0,0 +1,13 @@ +package eu.kanade.data.exh + +import exh.metadata.sql.models.SearchTitle + +val searchTitleMapper: (Long, Long, String, Int) -> SearchTitle = + { id, mangaId, title, type -> + SearchTitle( + id = id, + mangaId = mangaId, + title = title, + type = type, + ) + } diff --git a/app/src/main/java/eu/kanade/tachiyomi/AppModule.kt b/app/src/main/java/eu/kanade/tachiyomi/AppModule.kt index 7d9b3273b..abe30eb3c 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/AppModule.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/AppModule.kt @@ -7,11 +7,13 @@ import androidx.sqlite.db.SupportSQLiteOpenHelper import androidx.sqlite.db.framework.FrameworkSQLiteOpenHelperFactory import com.squareup.sqldelight.android.AndroidSqliteDriver import com.squareup.sqldelight.db.SqlDriver +import data.Categories import data.History import data.Mangas import eu.kanade.data.AndroidDatabaseHandler import eu.kanade.data.DatabaseHandler import eu.kanade.data.dateAdapter +import eu.kanade.data.listOfLongsAdapter import eu.kanade.data.listOfStringsAdapter import eu.kanade.data.listOfStringsAndAdapter import eu.kanade.tachiyomi.data.cache.ChapterCache @@ -73,6 +75,11 @@ class AppModule(val app: Application) : InjektModule { filtered_scanlatorsAdapter = listOfStringsAndAdapter, // SY <-- ), + // SY --> + categoriesAdapter = Categories.Adapter( + manga_orderAdapter = listOfLongsAdapter, + ), + // SY <-- ) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/AbstractBackupManager.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/AbstractBackupManager.kt index ad28b3e27..fa03b1f93 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/AbstractBackupManager.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/AbstractBackupManager.kt @@ -3,7 +3,7 @@ package eu.kanade.tachiyomi.data.backup import android.content.Context import android.net.Uri import eu.kanade.data.DatabaseHandler -import eu.kanade.tachiyomi.data.database.DatabaseHelper +import eu.kanade.data.toLong import eu.kanade.tachiyomi.data.database.models.Chapter import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.models.toMangaInfo @@ -18,11 +18,12 @@ import eu.kanade.tachiyomi.util.chapter.syncChaptersWithSource import exh.eh.EHentaiThrottleManager import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get +import data.Mangas as DbManga abstract class AbstractBackupManager(protected val context: Context) { - internal val db: DatabaseHelper = Injekt.get() - internal val database: DatabaseHandler = Injekt.get() + protected val handler: DatabaseHandler = Injekt.get() + internal val sourceManager: SourceManager = Injekt.get() internal val trackManager: TrackManager = Injekt.get() protected val preferences: PreferencesHelper = Injekt.get() @@ -31,15 +32,16 @@ abstract class AbstractBackupManager(protected val context: Context) { protected val customMangaManager: CustomMangaManager = Injekt.get() // SY <-- - abstract fun createBackup(uri: Uri, flags: Int, isAutoBackup: Boolean): String + abstract suspend fun createBackup(uri: Uri, flags: Int, isAutoBackup: Boolean): String /** * Returns manga * * @return [Manga], null if not found */ - internal fun getMangaFromDatabase(manga: Manga): Manga? = - db.getManga(manga.url, manga.source).executeAsBlocking() + internal suspend fun getMangaFromDatabase(url: String, source: Long): DbManga? { + return handler.awaitOneOrNull { mangasQueries.getMangaByUrlAndSource(url, source) } + } /** * Fetches chapter information. @@ -72,20 +74,23 @@ abstract class AbstractBackupManager(protected val context: Context) { * * @return [Manga] from library */ - protected fun getFavoriteManga(): List = - db.getFavoriteMangas().executeAsBlocking() + protected suspend fun getFavoriteManga(): List { + return handler.awaitList { mangasQueries.getFavorites() } + } // SY --> - protected fun getReadManga(): List = - db.getReadNotInLibraryMangas().executeAsBlocking() + protected suspend fun getReadManga(): List { + return handler.awaitList { mangasQueries.getReadMangaNotInLibrary() } + } /** * Returns list containing merged manga that are possibly not in the library * * @return merged [Manga] that are possibly not in the library */ - protected fun getMergedManga(): List = - db.getMergedMangas().executeAsBlocking() + protected suspend fun getMergedManga(): List { + return handler.awaitList { mergedQueries.selectAllMergedMangas() } + } // SY <-- /** @@ -93,28 +98,125 @@ abstract class AbstractBackupManager(protected val context: Context) { * * @return id of [Manga], null if not found */ - internal fun insertManga(manga: Manga): Long? = - db.insertManga(manga).executeAsBlocking().insertedId() + internal suspend fun insertManga(manga: Manga): Long { + 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 */ - protected fun insertChapters(chapters: List) { - db.insertChapters(chapters).executeAsBlocking() + protected suspend fun insertChapters(chapters: List) { + 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 */ - protected fun updateChapters(chapters: List) { - db.updateChaptersBackup(chapters).executeAsBlocking() + protected suspend fun updateChapters(chapters: List) { + 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 */ - protected fun updateKnownChapters(chapters: List) { - db.updateKnownChaptersBackup(chapters).executeAsBlocking() + protected suspend fun updateKnownChapters(chapters: List) { + 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!!, + ) + } + } } /** diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/AbstractBackupRestore.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/AbstractBackupRestore.kt index 3738bc7c5..403b17167 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/AbstractBackupRestore.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/AbstractBackupRestore.kt @@ -2,9 +2,9 @@ package eu.kanade.tachiyomi.data.backup import android.content.Context import android.net.Uri +import eu.kanade.data.DatabaseHandler import eu.kanade.data.chapter.NoChaptersException 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.Manga import eu.kanade.tachiyomi.data.database.models.Track @@ -22,7 +22,7 @@ import java.util.Locale abstract class AbstractBackupRestore(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() // SY --> @@ -101,7 +101,22 @@ abstract class AbstractBackupRestore(protected val co if (service != null && service.isLogged) { try { 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) { errors.add(Date() to "${manga.title} - ${e.message}") } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupCreatorJob.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupCreatorJob.kt index cff2ebde7..67db545e9 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupCreatorJob.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupCreatorJob.kt @@ -3,13 +3,13 @@ package eu.kanade.tachiyomi.data.backup import android.content.Context import android.net.Uri import androidx.core.net.toUri +import androidx.work.CoroutineWorker import androidx.work.ExistingPeriodicWorkPolicy import androidx.work.ExistingWorkPolicy import androidx.work.OneTimeWorkRequestBuilder import androidx.work.PeriodicWorkRequestBuilder import androidx.work.WorkInfo import androidx.work.WorkManager -import androidx.work.Worker import androidx.work.WorkerParameters import androidx.work.workDataOf import com.hippo.unifile.UniFile @@ -24,9 +24,9 @@ import uy.kohesive.injekt.api.get import java.util.concurrent.TimeUnit 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() val notifier = BackupNotifier(context) val uri = inputData.getString(LOCATION_URI_KEY)?.let { Uri.parse(it) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/full/FullBackupManager.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/full/FullBackupManager.kt index 0b8d58728..67041dc5b 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/full/FullBackupManager.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/full/FullBackupManager.kt @@ -3,8 +3,11 @@ package eu.kanade.tachiyomi.data.backup.full import android.content.Context import android.net.Uri import com.hippo.unifile.UniFile -import eu.kanade.data.exh.savedSearchMapper -import eu.kanade.tachiyomi.Database +import data.Manga_sync +import data.Mangas +import eu.kanade.data.exh.mergedMangaReferenceMapper +import eu.kanade.data.manga.mangaMapper +import eu.kanade.domain.history.model.HistoryUpdate import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.backup.AbstractBackupManager import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_CATEGORY @@ -21,7 +24,6 @@ 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.full.models.Backup 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.BackupFlatMetadata import eu.kanade.tachiyomi.data.backup.full.models.BackupFull import eu.kanade.tachiyomi.data.backup.full.models.BackupHistory @@ -30,29 +32,29 @@ import eu.kanade.tachiyomi.data.backup.full.models.BackupMergedMangaReference import eu.kanade.tachiyomi.data.backup.full.models.BackupSavedSearch 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.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.backupMergedMangaReferenceMapper +import eu.kanade.tachiyomi.data.backup.full.models.backupSavedSearchMapper +import eu.kanade.tachiyomi.data.backup.full.models.backupTrackMapper 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.MangaCategory import eu.kanade.tachiyomi.data.database.models.Track import eu.kanade.tachiyomi.source.online.MetadataSource -import eu.kanade.tachiyomi.util.lang.launchIO import eu.kanade.tachiyomi.util.system.logcat +import exh.metadata.metadata.base.awaitFlatMetadataForManga +import exh.metadata.metadata.base.awaitInsertFlatMetadata import exh.metadata.metadata.base.getFlatMetadataForManga -import exh.metadata.metadata.base.insertFlatMetadataAsync import exh.source.MERGED_SOURCE_ID import exh.source.getMainSource -import exh.util.executeOnIO import exh.util.nullIfBlank import kotlinx.serialization.protobuf.ProtoBuf import logcat.LogPriority import okio.buffer import okio.gzip import okio.sink -import uy.kohesive.injekt.Injekt -import uy.kohesive.injekt.api.get import java.io.FileOutputStream +import java.util.Date import kotlin.math.max class FullBackupManager(context: Context) : AbstractBackupManager(context) { @@ -65,25 +67,25 @@ class FullBackupManager(context: Context) : AbstractBackupManager(context) { * @param uri path of Uri * @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 var backup: Backup? = null - db.inTransaction { - val databaseManga = getFavoriteManga() /* SY --> */ + if (flags and BACKUP_READ_MANGA_MASK == BACKUP_READ_MANGA) { - getReadManga() - } else { - emptyList() - } + getMergedManga() // SY <-- + val databaseManga = getFavoriteManga() /* SY --> */ + if (flags and BACKUP_READ_MANGA_MASK == BACKUP_READ_MANGA) { + getReadManga() + } else { + emptyList() + } + getMergedManga() // SY <-- - backup = Backup( - backupManga(databaseManga, flags), - backupCategories(flags), - emptyList(), - backupExtensionInfo(databaseManga), - backupSavedSearches(), - ) - } + backup = Backup( + backupManga(databaseManga, flags), + backupCategories(flags), + emptyList(), + backupExtensionInfo(databaseManga), + // SY --> + backupSavedSearches(), + // SY <-- + ) var file: UniFile? = null try { @@ -136,13 +138,13 @@ class FullBackupManager(context: Context) : AbstractBackupManager(context) { } } - private fun backupManga(mangas: List, flags: Int): List { + private suspend fun backupManga(mangas: List, flags: Int): List { return mangas.map { backupMangaObject(it, flags) } } - private fun backupExtensionInfo(mangas: List): List { + private fun backupExtensionInfo(mangas: List): List { return mangas .asSequence() .map { it.source } @@ -157,12 +159,10 @@ class FullBackupManager(context: Context) : AbstractBackupManager(context) { * * @return list of [BackupCategory] to be backed up */ - private fun backupCategories(options: Int): List { + private suspend fun backupCategories(options: Int): List { // Check if user wants category information in backup return if (options and BACKUP_CATEGORY_MASK == BACKUP_CATEGORY) { - db.getCategories() - .executeAsBlocking() - .map { BackupCategory.copyFrom(it) } + handler.awaitList { categoriesQueries.getCategories(backupCategoryMapper) } } else { emptyList() } @@ -174,16 +174,8 @@ class FullBackupManager(context: Context) : AbstractBackupManager(context) { * * @return list of [BackupSavedSearch] to be backed up */ - private fun backupSavedSearches(): List { - // TODO: Database handler please - return Injekt.get().saved_searchQueries.selectAll(savedSearchMapper).executeAsList().map { - BackupSavedSearch( - it.name, - it.query.orEmpty(), - it.filtersJson ?: "[]", - it.source, - ) - } + private suspend fun backupSavedSearches(): List { + return handler.awaitList { saved_searchQueries.selectAll(backupSavedSearchMapper) } } // SY <-- @@ -194,23 +186,19 @@ class FullBackupManager(context: Context) : AbstractBackupManager(context) { * @param options options for the backup * @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 val mangaObject = BackupManga.copyFrom(manga /* SY --> */, if (options and BACKUP_CUSTOM_INFO_MASK == BACKUP_CUSTOM_INFO) customMangaManager else null /* SY <-- */) // SY --> if (manga.source == MERGED_SOURCE_ID) { - manga.id?.let { mangaId -> - mangaObject.mergedMangaReferences = db.getMergedMangaReferences(mangaId) - .executeAsBlocking() - .map { BackupMergedMangaReference.copyFrom(it) } - } + mangaObject.mergedMangaReferences = handler.awaitList { mergedQueries.selectByMergeId(manga._id, backupMergedMangaReferenceMapper) } } val source = sourceManager.get(manga.source)?.getMainSource>() if (source != null) { - manga.id?.let { mangaId -> - db.getFlatMetadataForManga(mangaId).executeAsBlocking()?.let { flatMetadata -> + manga._id.let { mangaId -> + handler.getFlatMetadataForManga(mangaId)?.let { flatMetadata -> mangaObject.flatMetadata = BackupFlatMetadata.copyFrom(flatMetadata) } } @@ -220,36 +208,36 @@ class FullBackupManager(context: Context) : AbstractBackupManager(context) { // Check if user wants chapter information in backup if (options and BACKUP_CHAPTER_MASK == BACKUP_CHAPTER) { // Backup all the chapters - val chapters = db.getChapters(manga).executeAsBlocking() + val chapters = handler.awaitList { chaptersQueries.getChaptersByMangaId(manga._id, backupChapterMapper) } if (chapters.isNotEmpty()) { - mangaObject.chapters = chapters.map { BackupChapter.copyFrom(it) } + mangaObject.chapters = chapters } } // Check if user wants category information in backup if (options and BACKUP_CATEGORY_MASK == BACKUP_CATEGORY) { // Backup categories for this manga - val categoriesForManga = db.getCategoriesForManga(manga).executeAsBlocking() + val categoriesForManga = handler.awaitList { categoriesQueries.getCategoriesByMangaId(manga._id) } if (categoriesForManga.isNotEmpty()) { - mangaObject.categories = categoriesForManga.mapNotNull { it.order } + mangaObject.categories = categoriesForManga.map { it.order } } } // Check if user wants track information in backup 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()) { - mangaObject.tracking = tracks.map { BackupTracking.copyFrom(it) } + mangaObject.tracking = tracks } } // Check if user wants history information in backup if (options and BACKUP_HISTORY_MASK == BACKUP_HISTORY) { - val historyForManga = db.getHistoryByMangaId(manga.id!!).executeAsBlocking() - if (historyForManga.isNotEmpty()) { - val history = historyForManga.mapNotNull { history -> - val url = db.getChapter(history.chapter_id).executeAsBlocking()?.url - url?.let { BackupHistory(url, history.last_read) } + val historyByMangaId = handler.awaitList(true) { historyQueries.getHistoryByMangaId(manga._id) } + if (historyByMangaId.isNotEmpty()) { + val history = historyByMangaId.map { history -> + val chapter = handler.awaitOne { chaptersQueries.getChapterById(history.chapter_id) } + BackupHistory(chapter.url, history.last_read?.time ?: 0L) } if (history.isNotEmpty()) { mangaObject.history = history @@ -260,10 +248,10 @@ class FullBackupManager(context: Context) : AbstractBackupManager(context) { return mangaObject } - fun restoreMangaNoFetch(manga: Manga, dbManga: Manga) { - manga.id = dbManga.id + suspend fun restoreMangaNoFetch(manga: Manga, dbManga: Mangas) { + manga.id = dbManga._id manga.copyFrom(dbManga) - insertManga(manga) + updateManga(manga) } /** @@ -272,7 +260,7 @@ class FullBackupManager(context: Context) : AbstractBackupManager(context) { * @param manga manga that needs updating * @return Updated manga info. */ - fun restoreManga(manga: Manga): Manga { + suspend fun restoreManga(manga: Manga): Manga { return manga.also { it.initialized = it.description != null it.id = insertManga(it) @@ -284,32 +272,36 @@ class FullBackupManager(context: Context) : AbstractBackupManager(context) { * * @param backupCategories list containing categories */ - internal fun restoreCategories(backupCategories: List) { + internal suspend fun restoreCategories(backupCategories: List) { // Get categories from file and from db - val dbCategories = db.getCategories().executeAsBlocking() + val dbCategories = handler.awaitList { categoriesQueries.getCategories() } // Iterate over them - backupCategories.map { it.getCategoryImpl() }.forEach { category -> - // Used to know if the category is already in the db - var found = false - for (dbCategory in dbCategories) { - // If the category is already in the db, assign the id to the file's category - // and do nothing - if (category.name == dbCategory.name) { - category.id = dbCategory.id - found = true - break + backupCategories + .map { it.getCategoryImpl() } + .forEach { category -> + // Used to know if the category is already in the db + var found = false + for (dbCategory in dbCategories) { + // If the category is already in the db, assign the id to the file's category + // and do nothing + if (category.name == dbCategory.name) { + 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(), category.mangaOrder) + 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() - } - } } /** @@ -318,25 +310,30 @@ class FullBackupManager(context: Context) : AbstractBackupManager(context) { * @param manga the manga whose categories have to be restored. * @param categories the categories to restore. */ - internal fun restoreCategoriesForManga(manga: Manga, categories: List, backupCategories: List) { - val dbCategories = db.getCategories().executeAsBlocking() - val mangaCategoriesToUpdate = ArrayList(categories.size) + internal suspend fun restoreCategoriesForManga(manga: Manga, categories: List, backupCategories: List) { + val dbCategories = handler.awaitList { categoriesQueries.getCategories() } + val mangaCategoriesToUpdate = mutableListOf>() + categories.forEach { backupCategoryOrder -> backupCategories.firstOrNull { - it.order == backupCategoryOrder + it.order == backupCategoryOrder.toLong() }?.let { backupCategory -> dbCategories.firstOrNull { dbCategory -> dbCategory.name == backupCategory.name }?.let { dbCategory -> - mangaCategoriesToUpdate += MangaCategory.create(manga, dbCategory) + mangaCategoriesToUpdate.add(Pair(manga.id!!, dbCategory.id)) } } } // Update database if (mangaCategoriesToUpdate.isNotEmpty()) { - db.deleteOldMangasCategories(listOf(manga)).executeAsBlocking() - db.insertMangasCategories(mangaCategoriesToUpdate).executeAsBlocking() + handler.await(true) { + mangas_categoriesQueries.deleteMangaCategoryByMangaId(manga.id!!) + mangaCategoriesToUpdate.forEach { (mangaId, categoryId) -> + mangas_categoriesQueries.insert(mangaId, categoryId) + } + } } } @@ -345,28 +342,43 @@ class FullBackupManager(context: Context) : AbstractBackupManager(context) { * * @param history list containing history to be restored */ - internal fun restoreHistoryForManga(history: List) { + internal suspend fun restoreHistoryForManga(history: List) { // List containing history to be updated - val historyToBeUpdated = ArrayList(history.size) + val toUpdate = mutableListOf() 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 if (dbHistory != null) { - dbHistory.apply { - last_read = max(lastRead, dbHistory.last_read) - } - historyToBeUpdated.add(dbHistory) + dbHistory = dbHistory.copy(last_read = Date(max(lastRead, dbHistory.last_read?.time ?: 0L))) + toUpdate.add( + HistoryUpdate( + chapterId = dbHistory.chapter_id, + readAt = dbHistory.last_read!!, + sessionReadDuration = dbHistory.time_read, + ), + ) } else { // If not in database create - db.getChapter(url).executeAsBlocking()?.let { - val historyToAdd = History.create(it).apply { - last_read = lastRead + handler + .awaitOneOrNull { chaptersQueries.getChapterByUrl(url) } + ?.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, + ) + } + } } /** @@ -375,56 +387,97 @@ class FullBackupManager(context: Context) : AbstractBackupManager(context) { * @param manga the manga whose sync have to be restored. * @param tracks the track list to restore. */ - internal fun restoreTrackForManga(manga: Manga, tracks: List) { + internal suspend fun restoreTrackForManga(manga: Manga, tracks: List) { // Fix foreign keys with the current manga id tracks.map { it.manga_id = manga.id!! } // Get tracks from database - val dbTracks = db.getTracks(manga.id).executeAsBlocking() - val trackToUpdate = mutableListOf() + + val dbTracks = handler.awaitList { manga_syncQueries.getTracksByMangaId(manga.id!!) } + val toUpdate = mutableListOf() + val toInsert = mutableListOf() tracks.forEach { track -> var isInDatabase = false 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 - if (track.media_id != dbTrack.media_id) { - dbTrack.media_id = track.media_id + var temp = dbTrack + if (track.media_id != dbTrack.remote_id) { + temp = temp.copy(remote_id = track.media_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 - trackToUpdate.add(dbTrack) + toUpdate.add(temp) break } } if (!isInDatabase) { // Insert new sync. Let the db assign the id track.id = null - trackToUpdate.add(track) + toInsert.add(track) } } // Update database - if (trackToUpdate.isNotEmpty()) { - db.insertTracks(trackToUpdate).executeAsBlocking() + if (toUpdate.isNotEmpty()) { + 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) { - val dbChapters = db.getChapters(manga).executeAsBlocking() + internal suspend fun restoreChaptersForManga(manga: Manga, chapters: List) { + val dbChapters = handler.awaitList { chaptersQueries.getChaptersByMangaId(manga.id!!) } chapters.forEach { chapter -> val dbChapter = dbChapters.find { it.url == chapter.url } if (dbChapter != null) { - chapter.id = dbChapter.id + chapter.id = dbChapter._id chapter.copyFrom(dbChapter) if (dbChapter.read && !chapter.read) { chapter.read = dbChapter.read - chapter.last_page_read = dbChapter.last_page_read - } else if (chapter.last_page_read == 0 && dbChapter.last_page_read != 0) { - 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 != 0L) { + chapter.last_page_read = dbChapter.last_page_read.toInt() } if (!chapter.bookmark && dbChapter.bookmark) { chapter.bookmark = dbChapter.bookmark @@ -441,13 +494,13 @@ class FullBackupManager(context: Context) : AbstractBackupManager(context) { // SY --> internal suspend fun restoreSavedSearches(backupSavedSearches: List) { - val currentSavedSearches = database.awaitList { - saved_searchQueries.selectAll(savedSearchMapper) + val currentSavedSearches = handler.awaitList { + saved_searchQueries.selectNamesAndSources() } - database.await(true) { + handler.await { backupSavedSearches.filter { backupSavedSearch -> - currentSavedSearches.none { it.name == backupSavedSearch.name && it.source == backupSavedSearch.source } + currentSavedSearches.none { it.source == backupSavedSearch.source && it.name == backupSavedSearch.name } }.forEach { saved_searchQueries.insertSavedSearch( _id = null, @@ -467,44 +520,43 @@ class FullBackupManager(context: Context) : AbstractBackupManager(context) { * @param manga the merge manga for the references * @param backupMergedMangaReferences the list of backup manga references for the merged manga */ - internal fun restoreMergedMangaReferencesForManga(manga: Manga, backupMergedMangaReferences: List) { + internal suspend fun restoreMergedMangaReferencesForManga(mergeMangaId: Long, backupMergedMangaReferences: List) { // Get merged manga references from file and from db - val dbMergedMangaReferences = db.getMergedMangaReferences().executeAsBlocking() + val dbMergedMangaReferences = handler.awaitList { mergedQueries.selectAll(mergedMangaReferenceMapper) } // Iterate over them backupMergedMangaReferences.forEach { backupMergedMangaReference -> - // Used to know if the merged manga reference is already in the db - var found = false - for (dbMergedMangaReference in dbMergedMangaReferences) { - // If the backupMergedMangaReference is already in the db, assign the id to the file's backupMergedMangaReference - // and do nothing - if (backupMergedMangaReference.mergeUrl == dbMergedMangaReference.mergeUrl && backupMergedMangaReference.mangaUrl == dbMergedMangaReference.mangaUrl) { - found = true - break - } - } // If the backupMergedMangaReference isn't in the db, remove the id and insert a new backupMergedMangaReference // Store the inserted id in the backupMergedMangaReference - if (!found) { + if (dbMergedMangaReferences.none { backupMergedMangaReference.mergeUrl == it.mergeUrl && backupMergedMangaReference.mangaUrl == it.mangaUrl }) { // Let the db assign the id - val mergedManga = db.getManga(backupMergedMangaReference.mangaUrl, backupMergedMangaReference.mangaSourceId).executeAsBlocking() ?: return@forEach - val mergedMangaReference = backupMergedMangaReference.getMergedMangaReference() - mergedMangaReference.mergeId = manga.id - mergedMangaReference.mangaId = mergedManga.id - db.insertMergedManga(mergedMangaReference).executeAsBlocking() + val mergedManga = handler.awaitOneOrNull { mangasQueries.getMangaByUrlAndSource(backupMergedMangaReference.mangaUrl, backupMergedMangaReference.mangaSourceId, mangaMapper) } ?: return@forEach + backupMergedMangaReference.getMergedMangaReference().run { + mergeId = mergeMangaId + mangaId = mergedManga.id + handler.await { + mergedQueries.insertMerged( + null, + isInfoManga, + getChapterUpdates, + chapterSortMode.toLong(), + chapterPriority.toLong(), + downloadChapters, + mergeId!!, + mergeUrl, + mangaId, + mangaUrl, + mangaSourceId, + ) + } + } } } } - internal fun restoreFlatMetadata(manga: Manga, backupFlatMetadata: BackupFlatMetadata) { - val mangaId = manga.id ?: return - launchIO { - db.getFlatMetadataForManga(mangaId).executeOnIO().let { - if (it == null) { - val flatMetadata = backupFlatMetadata.getFlatMetadata(mangaId) - db.insertFlatMetadataAsync(flatMetadata).await() - } - } + internal suspend fun restoreFlatMetadata(mangaId: Long, backupFlatMetadata: BackupFlatMetadata) { + if (handler.awaitFlatMetadataForManga(mangaId) == null) { + handler.awaitInsertFlatMetadata(backupFlatMetadata.getFlatMetadata(mangaId)) } } // SY <-- diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/full/FullBackupRestore.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/full/FullBackupRestore.kt index 7977c8c39..1fc4d7246 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/full/FullBackupRestore.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/full/FullBackupRestore.kt @@ -66,10 +66,8 @@ class FullBackupRestore(context: Context, notifier: BackupNotifier) : AbstractBa return true } - private fun restoreCategories(backupCategories: List) { - db.inTransaction { - backupManager.restoreCategories(backupCategories) - } + private suspend fun restoreCategories(backupCategories: List) { + backupManager.restoreCategories(backupCategories) restoreProgress += 1 showRestoreProgress(restoreProgress, restoreAmount, context.getString(R.string.categories)) @@ -84,10 +82,10 @@ class FullBackupRestore(context: Context, notifier: BackupNotifier) : AbstractBa } // SY <-- - private fun restoreManga(backupManga: BackupManga, backupCategories: List) { + private suspend fun restoreManga(backupManga: BackupManga, backupCategories: List) { val manga = backupManga.getMangaImpl() 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 tracks = backupManga.getTrackingImpl() // SY --> @@ -120,7 +118,7 @@ class FullBackupRestore(context: Context, notifier: BackupNotifier) : AbstractBa * @param history history data from json * @param tracks tracking data from json */ - private fun restoreMangaData( + private suspend fun restoreMangaData( manga: Manga, chapters: List, categories: List, @@ -133,18 +131,16 @@ class FullBackupRestore(context: Context, notifier: BackupNotifier) : AbstractBa customManga: CustomMangaManager.MangaJson?, // SY --> ) { - db.inTransaction { - val dbManga = backupManager.getMangaFromDatabase(manga) - if (dbManga == null) { - // Manga not in database - restoreMangaFetch(manga, chapters, categories, history, tracks, backupCategories/* SY --> */, mergedMangaReferences, flatMetadata, customManga/* SY <-- */) - } else { - // Manga in database - // Copy information from manga already in database - backupManager.restoreMangaNoFetch(manga, dbManga) - // Fetch rest of manga information - restoreMangaNoFetch(manga, chapters, categories, history, tracks, backupCategories/* SY --> */, mergedMangaReferences, flatMetadata, customManga/* SY <-- */) - } + val dbManga = backupManager.getMangaFromDatabase(manga.url, manga.source) + if (dbManga == null) { + // Manga not in database + restoreMangaFetch(manga, chapters, categories, history, tracks, backupCategories/* SY --> */, mergedMangaReferences, flatMetadata, customManga/* SY <-- */) + } else { + // Manga in database + // Copy information from manga already in database + backupManager.restoreMangaNoFetch(manga, dbManga) + // Fetch rest of manga information + restoreMangaNoFetch(manga, chapters, categories, history, tracks, backupCategories/* SY --> */, mergedMangaReferences, flatMetadata, customManga/* SY <-- */) } } @@ -155,7 +151,7 @@ class FullBackupRestore(context: Context, notifier: BackupNotifier) : AbstractBa * @param chapters chapters of manga that needs updating * @param categories categories that need updating */ - private fun restoreMangaFetch( + private suspend fun restoreMangaFetch( manga: Manga, chapters: List, categories: List, @@ -179,7 +175,7 @@ class FullBackupRestore(context: Context, notifier: BackupNotifier) : AbstractBa } } - private fun restoreMangaNoFetch( + private suspend fun restoreMangaNoFetch( backupManga: Manga, chapters: List, categories: List, @@ -197,7 +193,7 @@ class FullBackupRestore(context: Context, notifier: BackupNotifier) : AbstractBa restoreExtraForManga(backupManga, categories, history, tracks, backupCategories/* SY --> */, mergedMangaReferences, flatMetadata, customManga/* SY <-- */) } - private fun restoreExtraForManga( + private suspend fun restoreExtraForManga( manga: Manga, categories: List, history: List, @@ -220,10 +216,10 @@ class FullBackupRestore(context: Context, notifier: BackupNotifier) : AbstractBa // SY --> // Restore merged manga references if its a merged manga - backupManager.restoreMergedMangaReferencesForManga(manga, mergedMangaReferences) + backupManager.restoreMergedMangaReferencesForManga(manga.id!!, mergedMangaReferences) // Restore flat metadata for metadata sources - flatMetadata?.let { backupManager.restoreFlatMetadata(manga, it) } + flatMetadata?.let { backupManager.restoreFlatMetadata(manga.id!!, it) } // Restore Custom Info customManga?.id = manga.id!! diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/full/models/BackupCategory.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/full/models/BackupCategory.kt index 1b46d539a..f525783cf 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/full/models/BackupCategory.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/full/models/BackupCategory.kt @@ -1,6 +1,5 @@ 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 kotlinx.serialization.Serializable import kotlinx.serialization.protobuf.ProtoNumber @@ -8,30 +7,28 @@ import kotlinx.serialization.protobuf.ProtoNumber @Serializable class BackupCategory( @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 // Bump by 100 to specify this is a 0.x value - @ProtoNumber(100) var flags: Int = 0, + @ProtoNumber(100) var flags: Long = 0, // SY specific values @ProtoNumber(600) var mangaOrder: List = emptyList(), ) { fun getCategoryImpl(): CategoryImpl { return CategoryImpl().apply { name = this@BackupCategory.name - flags = this@BackupCategory.flags - order = this@BackupCategory.order + flags = this@BackupCategory.flags.toInt() + order = this@BackupCategory.order.toInt() mangaOrder = this@BackupCategory.mangaOrder } } - - companion object { - fun copyFrom(category: Category): BackupCategory { - return BackupCategory( - name = category.name, - order = category.order, - flags = category.flags, - mangaOrder = category.mangaOrder, - ) - } - } +} + +val backupCategoryMapper = { _: Long, name: String, order: Long, flags: Long, mangaOrder: List -> + BackupCategory( + name = name, + order = order, + flags = flags, + mangaOrder = mangaOrder, + ) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/full/models/BackupChapter.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/full/models/BackupChapter.kt index ca5214533..ffca3a255 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/full/models/BackupChapter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/full/models/BackupChapter.kt @@ -1,6 +1,5 @@ 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 kotlinx.serialization.Serializable import kotlinx.serialization.protobuf.ProtoNumber @@ -15,12 +14,12 @@ data class BackupChapter( @ProtoNumber(4) var read: Boolean = false, @ProtoNumber(5) var bookmark: Boolean = false, // 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(8) var dateUpload: Long = 0, // chapterNumber is called number is 1.x @ProtoNumber(9) var chapterNumber: Float = 0F, - @ProtoNumber(10) var sourceOrder: Int = 0, + @ProtoNumber(10) var sourceOrder: Long = 0, ) { fun toChapterImpl(): ChapterImpl { return ChapterImpl().apply { @@ -30,27 +29,37 @@ data class BackupChapter( scanlator = this@BackupChapter.scanlator read = this@BackupChapter.read bookmark = this@BackupChapter.bookmark - last_page_read = this@BackupChapter.lastPageRead + last_page_read = this@BackupChapter.lastPageRead.toInt() date_fetch = this@BackupChapter.dateFetch date_upload = this@BackupChapter.dateUpload - source_order = this@BackupChapter.sourceOrder - } - } - - companion object { - fun copyFrom(chapter: Chapter): BackupChapter { - return BackupChapter( - url = chapter.url, - name = chapter.name, - chapterNumber = chapter.chapter_number, - scanlator = chapter.scanlator, - read = chapter.read, - bookmark = chapter.bookmark, - lastPageRead = chapter.last_page_read, - dateFetch = chapter.date_fetch, - dateUpload = chapter.date_upload, - sourceOrder = chapter.source_order, - ) + source_order = this@BackupChapter.sourceOrder.toInt() } } } + +val backupChapterMapper = { + _: Long, + _: Long, + url: String, + name: String, + scanlator: String?, + read: Boolean, + bookmark: Boolean, + lastPageRead: Long, + chapterNumber: Float, + source_order: Long, + dateFetch: Long, + dateUpload: Long, -> + BackupChapter( + url = url, + name = name, + chapterNumber = chapterNumber, + scanlator = scanlator, + read = read, + bookmark = bookmark, + lastPageRead = lastPageRead, + dateFetch = dateFetch, + dateUpload = dateUpload, + sourceOrder = source_order, + ) +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/full/models/BackupManga.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/full/models/BackupManga.kt index b43106c1c..403f8098b 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/full/models/BackupManga.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/full/models/BackupManga.kt @@ -1,10 +1,12 @@ package eu.kanade.tachiyomi.data.backup.full.models +import data.Mangas +import eu.kanade.data.listOfStringsAndAdapter 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.TrackImpl import eu.kanade.tachiyomi.data.library.CustomMangaManager +import eu.kanade.tachiyomi.ui.reader.setting.ReadingModeType import kotlinx.serialization.Serializable import kotlinx.serialization.protobuf.ProtoNumber @@ -29,7 +31,7 @@ data class BackupManga( @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(16) var chapters: List = emptyList(), - @ProtoNumber(17) var categories: List = emptyList(), + @ProtoNumber(17) var categories: List = emptyList(), @ProtoNumber(18) var tracking: List = emptyList(), // 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, @@ -109,28 +111,28 @@ data class BackupManga( } companion object { - fun copyFrom(manga: Manga /* SY --> */, customMangaManager: CustomMangaManager?/* SY <-- */): BackupManga { + fun copyFrom(manga: Mangas /* SY --> */, customMangaManager: CustomMangaManager?/* SY <-- */): BackupManga { return BackupManga( url = manga.url, // SY --> - title = manga.originalTitle, - artist = manga.originalArtist, - author = manga.originalAuthor, - description = manga.originalDescription, - genre = manga.getOriginalGenres() ?: emptyList(), - status = manga.originalStatus, + title = manga.title, + artist = manga.artist, + author = manga.author, + description = manga.description, + genre = manga.genre ?: emptyList(), + status = manga.status.toInt(), // SY <-- 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, - filtered_scanlators = manga.filtered_scanlators, + viewer = (manga.viewer.toInt() and ReadingModeType.MASK), + viewer_flags = manga.viewer.toInt(), + chapterFlags = manga.chapter_flags.toInt(), + filtered_scanlators = listOfStringsAndAdapter.encode(manga.filtered_scanlators.orEmpty()), // SY --> ).also { backupManga -> - customMangaManager?.getManga(manga)?.let { + customMangaManager?.getManga(manga._id)?.let { backupManga.customTitle = it.title backupManga.customArtist = it.artist backupManga.customAuthor = it.author diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/full/models/BackupMergedMangaReference.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/full/models/BackupMergedMangaReference.kt index 38e753ba1..338311c6a 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/full/models/BackupMergedMangaReference.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/full/models/BackupMergedMangaReference.kt @@ -33,19 +33,28 @@ data class BackupMergedMangaReference( id = null, ) } - - companion object { - fun copyFrom(mergedMangaReference: MergedMangaReference): BackupMergedMangaReference { - return BackupMergedMangaReference( - isInfoManga = mergedMangaReference.isInfoManga, - getChapterUpdates = mergedMangaReference.getChapterUpdates, - chapterSortMode = mergedMangaReference.chapterSortMode, - chapterPriority = mergedMangaReference.chapterPriority, - downloadChapters = mergedMangaReference.downloadChapters, - mergeUrl = mergedMangaReference.mergeUrl, - mangaUrl = mergedMangaReference.mangaUrl, - mangaSourceId = mergedMangaReference.mangaSourceId, - ) - } - } +} + +val backupMergedMangaReferenceMapper = { + _: Long, + isInfoManga: Boolean, + getChapterUpdates: Boolean, + chapterSortMode: Long, + chapterPriority: Long, + downloadChapters: Boolean, + _: Long, + mergeUrl: String, + _: Long?, + mangaUrl: String, + mangaSourceId: Long, -> + BackupMergedMangaReference( + isInfoManga = isInfoManga, + getChapterUpdates = getChapterUpdates, + chapterSortMode = chapterSortMode.toInt(), + chapterPriority = chapterPriority.toInt(), + downloadChapters = downloadChapters, + mergeUrl = mergeUrl, + mangaUrl = mangaUrl, + mangaSourceId = mangaSourceId, + ) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/full/models/BackupSavedSearch.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/full/models/BackupSavedSearch.kt index b92a4d59c..dcf102e3b 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/full/models/BackupSavedSearch.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/full/models/BackupSavedSearch.kt @@ -13,3 +13,17 @@ data class BackupSavedSearch( @ProtoNumber(3) val filterList: String = "", @ProtoNumber(4) val source: Long = 0, ) + +val backupSavedSearchMapper = { + _: Long, + source: Long, + name: String, + query: String?, + filtersJson: String?, -> + BackupSavedSearch( + source = source, + name = name, + query = query.orEmpty(), + filterList = filtersJson ?: "[]", + ) +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/full/models/BackupTracking.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/full/models/BackupTracking.kt index 5e45f8663..e87cd342d 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/full/models/BackupTracking.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/full/models/BackupTracking.kt @@ -1,6 +1,5 @@ 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 kotlinx.serialization.Serializable import kotlinx.serialization.protobuf.ProtoNumber @@ -48,23 +47,34 @@ data class BackupTracking( tracking_url = this@BackupTracking.trackingUrl } } - - companion object { - fun copyFrom(track: Track): BackupTracking { - return 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, - lastChapterRead = track.last_chapter_read, - totalChapters = track.total_chapters, - score = track.score, - status = track.status, - startedReadingDate = track.started_reading_date, - finishedReadingDate = track.finished_reading_date, - trackingUrl = track.tracking_url, - ) - } - } +} + +val backupTrackMapper = { + _id: Long, + manga_id: Long, + syncId: Long, + mediaId: Long, + libraryId: Long?, + title: String, + lastChapterRead: Double, + totalChapters: Long, + status: Long, + score: Float, + remoteUrl: String, + startDate: Long, + finishDate: Long, -> + 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, + ) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/models/MangaCategory.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/models/MangaCategory.kt index 220337088..90dacc878 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/data/database/models/MangaCategory.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/database/models/MangaCategory.kt @@ -1,5 +1,7 @@ package eu.kanade.tachiyomi.data.database.models +import data.GetCategories + class MangaCategory { var id: Long? = null @@ -16,5 +18,12 @@ class MangaCategory { mc.category_id = category.id!! 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 + } } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/source/model/SChapter.kt b/app/src/main/java/eu/kanade/tachiyomi/source/model/SChapter.kt index f7e8a14ad..62fc1f9d1 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/source/model/SChapter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/source/model/SChapter.kt @@ -1,5 +1,6 @@ package eu.kanade.tachiyomi.source.model +import data.Chapters import tachiyomi.source.model.ChapterInfo import java.io.Serializable @@ -23,6 +24,14 @@ interface SChapter : Serializable { 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 { fun create(): SChapter { return SChapterImpl() diff --git a/app/src/main/java/eu/kanade/tachiyomi/source/model/SManga.kt b/app/src/main/java/eu/kanade/tachiyomi/source/model/SManga.kt index f7ead6db2..25da12163 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/source/model/SManga.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/source/model/SManga.kt @@ -1,5 +1,6 @@ package eu.kanade.tachiyomi.source.model +import data.Mangas import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.models.MangaImpl import eu.kanade.tachiyomi.data.download.DownloadManager @@ -82,6 +83,45 @@ interface SManga : Serializable { } } + fun copyFrom(other: Mangas) { + // EXH --> + if (other.title.isNotBlank() && originalTitle != other.title) { + val oldTitle = originalTitle + title = other.title + val source = (this as? Manga)?.source + if (source != null) { + Injekt.get().renameMangaDir(oldTitle, other.title, source) + } + } + // EXH <-- + + 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 { const val UNKNOWN = 0 const val ONGOING = 1 diff --git a/app/src/main/java/exh/metadata/metadata/base/FlatMetadata.kt b/app/src/main/java/exh/metadata/metadata/base/FlatMetadata.kt index 5c30a134f..737674690 100644 --- a/app/src/main/java/exh/metadata/metadata/base/FlatMetadata.kt +++ b/app/src/main/java/exh/metadata/metadata/base/FlatMetadata.kt @@ -1,6 +1,11 @@ package exh.metadata.metadata.base import com.pushtorefresh.storio.operations.PreparedOperation +import eu.kanade.data.AndroidDatabaseHandler +import eu.kanade.data.DatabaseHandler +import eu.kanade.data.exh.searchMetadataMapper +import eu.kanade.data.exh.searchTagMapper +import eu.kanade.data.exh.searchTitleMapper import eu.kanade.tachiyomi.data.database.DatabaseHelper import exh.metadata.sql.models.SearchMetadata import exh.metadata.sql.models.SearchTag @@ -31,6 +36,29 @@ data class FlatMetadata( } } +fun DatabaseHandler.getFlatMetadataForManga(mangaId: Long): FlatMetadata? { + this as AndroidDatabaseHandler + val meta = db.search_metadataQueries.selectByMangaId(mangaId, searchMetadataMapper).executeAsOneOrNull() + return if (meta != null) { + val tags = db.search_tagsQueries.selectByMangaId(mangaId, searchTagMapper).executeAsList() + val titles = db.search_titlesQueries.selectByMangaId(mangaId, searchTitleMapper).executeAsList() + + FlatMetadata(meta, tags, titles) + } else null +} + +suspend fun DatabaseHandler.awaitFlatMetadataForManga(mangaId: Long): FlatMetadata? { + return await { + val meta = search_metadataQueries.selectByMangaId(mangaId, searchMetadataMapper).executeAsOneOrNull() + if (meta != null) { + val tags = search_tagsQueries.selectByMangaId(mangaId, searchTagMapper).executeAsList() + val titles = search_titlesQueries.selectByMangaId(mangaId, searchTitleMapper).executeAsList() + + FlatMetadata(meta, tags, titles) + } else null + } +} + fun DatabaseHelper.getFlatMetadataForManga(mangaId: Long): PreparedOperation { // We have to use fromCallable because StorIO messes up the thread scheduling if we use their rx functions val single = Single.fromCallable { @@ -92,6 +120,43 @@ private fun preparedOperationFromSingle(single: Single): PreparedOperatio } } +fun DatabaseHandler.insertFlatMetadata(flatMetadata: FlatMetadata) { + require(flatMetadata.metadata.mangaId != -1L) + + this as AndroidDatabaseHandler // todo remove when legacy backup is dead + db.transaction { + flatMetadata.metadata.let { + db.search_metadataQueries.upsert(it.mangaId, it.uploader, it.extra, it.indexedExtra, it.extraVersion) + } + db.search_tagsQueries.deleteByManga(flatMetadata.metadata.mangaId) + flatMetadata.tags.forEach { + db.search_tagsQueries.insert(it.mangaId, it.namespace, it.name, it.type) + } + db.search_titlesQueries.deleteByManga(flatMetadata.metadata.mangaId) + flatMetadata.titles.forEach { + db.search_titlesQueries.insert(it.mangaId, it.title, it.type) + } + } +} + +suspend fun DatabaseHandler.awaitInsertFlatMetadata(flatMetadata: FlatMetadata) { + require(flatMetadata.metadata.mangaId != -1L) + + await(true) { + flatMetadata.metadata.run { + search_metadataQueries.upsert(mangaId, uploader, extra, indexedExtra, extraVersion) + } + search_tagsQueries.deleteByManga(flatMetadata.metadata.mangaId) + flatMetadata.tags.forEach { + search_tagsQueries.insert(it.mangaId, it.namespace, it.name, it.type) + } + search_titlesQueries.deleteByManga(flatMetadata.metadata.mangaId) + flatMetadata.titles.forEach { + search_titlesQueries.insert(it.mangaId, it.title, it.type) + } + } +} + fun DatabaseHelper.insertFlatMetadata(flatMetadata: FlatMetadata) { require(flatMetadata.metadata.mangaId != -1L) diff --git a/app/src/main/sqldelight/data/categories.sq b/app/src/main/sqldelight/data/categories.sq index af3ea448f..26b878e8e 100644 --- a/app/src/main/sqldelight/data/categories.sq +++ b/app/src/main/sqldelight/data/categories.sq @@ -1,7 +1,36 @@ +import kotlin.collections.List; + CREATE TABLE categories( _id INTEGER NOT NULL PRIMARY KEY, name TEXT NOT NULL, sort INTEGER NOT NULL, flags INTEGER NOT NULL, - manga_order TEXT NOT NULL -); \ No newline at end of file + manga_order TEXT AS List NOT NULL +); + +getCategories: +SELECT +_id AS id, +name, +sort AS `order`, +flags, +manga_order AS `mangaOrder` +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, manga_order) +VALUES (:name, :order, :flags, :mangaOrder); + +selectLastInsertedRowId: +SELECT last_insert_rowid(); diff --git a/app/src/main/sqldelight/data/chapters.sq b/app/src/main/sqldelight/data/chapters.sq index 7b89f6c3f..060d7febe 100644 --- a/app/src/main/sqldelight/data/chapters.sq +++ b/app/src/main/sqldelight/data/chapters.sq @@ -28,6 +28,11 @@ SELECT * FROM chapters WHERE manga_id = :mangaId; +getChapterByUrl: +SELECT * +FROM chapters +WHERE url = :chapterUrl; + getMergedChaptersByMangaId: SELECT chapters.* FROM ( @@ -41,32 +46,8 @@ DELETE FROM chapters WHERE _id IN :chapterIds; insert: -INSERT INTO chapters( - manga_id, - 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 -); +INSERT INTO chapters(manga_id,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 chapters diff --git a/app/src/main/sqldelight/data/history.sq b/app/src/main/sqldelight/data/history.sq index 5b505bba6..810c8d673 100644 --- a/app/src/main/sqldelight/data/history.sq +++ b/app/src/main/sqldelight/data/history.sq @@ -11,6 +11,28 @@ CREATE TABLE history( 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: UPDATE history SET last_read = 0 diff --git a/app/src/main/sqldelight/data/manga_sync.sq b/app/src/main/sqldelight/data/manga_sync.sq index dcd18442b..dd34ef857 100644 --- a/app/src/main/sqldelight/data/manga_sync.sq +++ b/app/src/main/sqldelight/data/manga_sync.sq @@ -15,4 +15,30 @@ CREATE TABLE manga_sync( UNIQUE (manga_id, sync_id) ON CONFLICT REPLACE, FOREIGN KEY(manga_id) REFERENCES mangas (_id) ON DELETE CASCADE -); \ No newline at end of file +); + +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; diff --git a/app/src/main/sqldelight/data/mangas.sq b/app/src/main/sqldelight/data/mangas.sq index b677fc76d..4f368163a 100644 --- a/app/src/main/sqldelight/data/mangas.sq +++ b/app/src/main/sqldelight/data/mangas.sq @@ -26,11 +26,32 @@ CREATE TABLE mangas( CREATE INDEX library_favorite_index ON mangas(favorite) WHERE favorite = 1; 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: SELECT * FROM mangas WHERE _id = :id; +getMangaByUrlAndSource: +SELECT * +FROM mangas +WHERE url = :url AND source = :source; + +getFavorites: +SELECT * +FROM mangas +WHERE favorite = 1; + +getReadMangaNotInLibrary: +SELECT mangas.* +FROM mangas +WHERE favorite = 0 AND _id IN( + SELECT chapters.manga_id FROM chapters WHERE read = 1 OR last_page_read != 0 +); + getSourceIdWithFavoriteCount: SELECT source, @@ -88,3 +109,6 @@ UPDATE mangas SET cover_last_modified = coalesce(:coverLastModified, cover_last_modified), date_added = coalesce(:dateAdded, date_added) WHERE _id = :mangaId; + +selectLastInsertedRowId: +SELECT last_insert_rowid(); diff --git a/app/src/main/sqldelight/data/mangas_categories.sq b/app/src/main/sqldelight/data/mangas_categories.sq index 6db91fe16..a97c9d3ca 100644 --- a/app/src/main/sqldelight/data/mangas_categories.sq +++ b/app/src/main/sqldelight/data/mangas_categories.sq @@ -6,4 +6,12 @@ CREATE TABLE mangas_categories( ON DELETE CASCADE, FOREIGN KEY(manga_id) REFERENCES mangas (_id) ON DELETE CASCADE -); \ No newline at end of file +); + +insert: +INSERT INTO mangas_categories(manga_id, category_id) +VALUES (:mangaId, :categoryId); + +deleteMangaCategoryByMangaId: +DELETE FROM mangas_categories +WHERE manga_id = :mangaId; \ No newline at end of file diff --git a/app/src/main/sqldelight/data/saved_search.sq b/app/src/main/sqldelight/data/saved_search.sq index 90cbf82b8..5f4f55f1d 100644 --- a/app/src/main/sqldelight/data/saved_search.sq +++ b/app/src/main/sqldelight/data/saved_search.sq @@ -21,6 +21,10 @@ SELECT * FROM saved_search WHERE _id = ?; selectByIds: SELECT * FROM saved_search WHERE _id IN ?; +selectNamesAndSources: +SELECT source, name +FROM saved_search; + insertSavedSearch: INSERT INTO saved_search (_id, source, name, query, filters_json) VALUES (?, ?, ?, ?, ?);