Use SQLDelight for mass migration

This commit is contained in:
Jobobby04 2022-07-03 00:33:03 -04:00
parent 664f9b1484
commit e71c9e2775
4 changed files with 148 additions and 113 deletions

View File

@ -1,18 +1,17 @@
package eu.kanade.tachiyomi.ui.browse.migration.advanced.process
import eu.kanade.tachiyomi.data.database.DatabaseHelper
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.domain.manga.interactor.GetMangaById
import eu.kanade.domain.manga.model.Manga
import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.SourceManager
import exh.util.DeferredField
import exh.util.executeOnIO
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.flow.MutableStateFlow
import kotlin.coroutines.CoroutineContext
class MigratingManga(
private val db: DatabaseHelper,
private val getMangaById: GetMangaById,
private val sourceManager: SourceManager,
val mangaId: Long,
parentContext: CoroutineContext,
@ -29,7 +28,7 @@ class MigratingManga(
@Volatile
private var manga: Manga? = null
suspend fun manga(): Manga? {
if (manga == null) manga = db.getManga(mangaId).executeOnIO()
if (manga == null) manga = getMangaById.await(mangaId)
return manga
}

View File

@ -15,17 +15,20 @@ import androidx.recyclerview.widget.LinearLayoutManager
import androidx.vectordrawable.graphics.drawable.VectorDrawableCompat
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import dev.chrisbanes.insetter.applyInsetter
import eu.kanade.domain.chapter.interactor.SyncChaptersWithSource
import eu.kanade.domain.manga.interactor.GetMangaById
import eu.kanade.domain.manga.interactor.UpdateManga
import eu.kanade.domain.manga.model.toDbManga
import eu.kanade.domain.manga.model.toMangaInfo
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.DatabaseHelper
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.database.models.toMangaInfo
import eu.kanade.tachiyomi.data.database.models.toDomainManga
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.databinding.MigrationListControllerBinding
import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.source.model.toSChapter
import eu.kanade.tachiyomi.source.model.toSManga
import eu.kanade.tachiyomi.source.online.all.EHentai
import eu.kanade.tachiyomi.ui.base.changehandler.OneWayFadeChangeHandler
import eu.kanade.tachiyomi.ui.base.controller.BaseController
@ -35,15 +38,12 @@ import eu.kanade.tachiyomi.ui.browse.migration.MigrationMangaDialog
import eu.kanade.tachiyomi.ui.browse.migration.advanced.design.PreMigrationController
import eu.kanade.tachiyomi.ui.browse.migration.search.SearchController
import eu.kanade.tachiyomi.ui.manga.MangaController
import eu.kanade.tachiyomi.util.chapter.syncChaptersWithSource
import eu.kanade.tachiyomi.util.lang.launchUI
import eu.kanade.tachiyomi.util.lang.withIOContext
import eu.kanade.tachiyomi.util.system.getResourceColor
import eu.kanade.tachiyomi.util.system.logcat
import eu.kanade.tachiyomi.util.system.toast
import exh.eh.EHentaiThrottleManager
import exh.smartsearch.SmartSearchEngine
import exh.util.executeOnIO
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
@ -70,11 +70,13 @@ class MigrationListController(bundle: Bundle? = null) :
val config: MigrationProcedureConfig? = args.getParcelable(CONFIG_EXTRA)
private val db: DatabaseHelper by injectLazy()
private val preferences: PreferencesHelper by injectLazy()
private val sourceManager: SourceManager by injectLazy()
private val smartSearchEngine = SmartSearchEngine(config?.extraSearchParams)
private val syncChaptersWithSource: SyncChaptersWithSource by injectLazy()
private val getMangaById: GetMangaById by injectLazy()
private val updateManga: UpdateManga by injectLazy()
private val migrationScope = CoroutineScope(Job() + Dispatchers.IO)
var migrationsJob: Job? = null
@ -107,7 +109,7 @@ class MigrationListController(bundle: Bundle? = null) :
val newMigratingManga = migratingManga ?: run {
val new = config.mangaIds.map {
MigratingManga(db, sourceManager, it, migrationScope.coroutineContext)
MigratingManga(getMangaById, sourceManager, it, migrationScope.coroutineContext)
}
migratingManga = new.toMutableList()
new
@ -172,16 +174,16 @@ class MigrationListController(bundle: Bundle? = null) :
sourceSemaphore.withPermit {
try {
val searchResult = if (useSmartSearch) {
smartSearchEngine.smartSearch(source, mangaObj.originalTitle)
smartSearchEngine.smartSearch(source, mangaObj.ogTitle)
} else {
smartSearchEngine.normalSearch(source, mangaObj.originalTitle)
smartSearchEngine.normalSearch(source, mangaObj.ogTitle)
}
if (searchResult != null && !(searchResult.url == mangaObj.url && source.id == mangaObj.source)) {
val localManga = smartSearchEngine.networkToLocalManga(
searchResult,
source.id,
)
).toDomainManga()!!
val chapters = if (source is EHentai) {
source.getChapterList(localManga.toMangaInfo(), throttleManager::throttle)
@ -190,7 +192,7 @@ class MigrationListController(bundle: Bundle? = null) :
}
try {
syncChaptersWithSource(chapters.map { it.toSChapter() }, localManga, source)
syncChaptersWithSource.await(chapters.map { it.toSChapter() }, localManga, source)
} catch (e: Exception) {
return@async2 null
}
@ -212,13 +214,13 @@ class MigrationListController(bundle: Bundle? = null) :
validSources.forEachIndexed { index, source ->
val searchResult = try {
val searchResult = if (useSmartSearch) {
smartSearchEngine.smartSearch(source, mangaObj.originalTitle)
smartSearchEngine.smartSearch(source, mangaObj.ogTitle)
} else {
smartSearchEngine.normalSearch(source, mangaObj.originalTitle)
smartSearchEngine.normalSearch(source, mangaObj.ogTitle)
}
if (searchResult != null) {
val localManga = smartSearchEngine.networkToLocalManga(searchResult, source.id)
val localManga = smartSearchEngine.networkToLocalManga(searchResult, source.id).toDomainManga()!!
val chapters = try {
if (source is EHentai) {
source.getChapterList(localManga.toMangaInfo(), throttleManager::throttle)
@ -229,9 +231,7 @@ class MigrationListController(bundle: Bundle? = null) :
this@MigrationListController.logcat(LogPriority.ERROR, e)
emptyList()
}
withIOContext {
syncChaptersWithSource(chapters, localManga, source)
}
syncChaptersWithSource.await(chapters, localManga, source)
localManga
} else null
} catch (e: CancellationException) {
@ -252,12 +252,10 @@ class MigrationListController(bundle: Bundle? = null) :
continue
}
if (result != null && result.thumbnail_url == null) {
if (result != null && result.thumbnailUrl == null) {
try {
val newManga = sourceManager.getOrStub(result.source).getMangaDetails(result.toMangaInfo())
result.copyFrom(newManga.toSManga())
db.insertManga(result).executeOnIO()
updateManga.awaitUpdateFromSource(result, newManga, true)
} catch (e: CancellationException) {
// Ignore cancellations
throw e
@ -335,7 +333,7 @@ class MigrationListController(bundle: Bundle? = null) :
} else {
sources.filter { it.id != manga.source }
}
val searchController = SearchController(manga, validSources)
val searchController = SearchController(manga.toDbManga(), validSources)
searchController.targetController = this@MigrationListController
router.pushController(searchController)
}
@ -359,11 +357,11 @@ class MigrationListController(bundle: Bundle? = null) :
adapter?.notifyItemChanged(firstIndex)
launchUI {
val result = CoroutineScope(migratingManga.manga.migrationJob).async {
val localManga = smartSearchEngine.networkToLocalManga(manga, source.id)
val localManga = smartSearchEngine.networkToLocalManga(manga, source.id).toDomainManga()!!
try {
val chapters = source.getChapterList(localManga.toMangaInfo())
.map { it.toSChapter() }
syncChaptersWithSource(chapters, localManga, source)
syncChaptersWithSource.await(chapters, localManga, source)
} catch (e: Exception) {
return@async null
}
@ -373,9 +371,7 @@ class MigrationListController(bundle: Bundle? = null) :
if (result != null) {
try {
val newManga = sourceManager.getOrStub(result.source).getMangaDetails(result.toMangaInfo())
result.copyFrom(newManga.toSManga())
db.insertManga(result).executeOnIO()
updateManga.awaitUpdateFromSource(result, newManga, true)
} catch (e: CancellationException) {
// Ignore cancellations
throw e
@ -413,14 +409,14 @@ class MigrationListController(bundle: Bundle? = null) :
val hasDetails = router.backstack.any { it.controller is MangaController }
if (hasDetails) {
val manga = migratingManga?.firstOrNull()?.searchResult?.get()?.let {
db.getManga(it).executeOnIO()
getMangaById.await(it)
}
if (manga != null) {
val newStack = router.backstack.filter {
it.controller !is MangaController &&
it.controller !is MigrationListController &&
it.controller !is PreMigrationController
} + MangaController(manga.id!!).withFadeTransaction()
} + MangaController(manga.id).withFadeTransaction()
router.setBackstack(newStack, OneWayFadeChangeHandler())
return@launchUI
}

View File

@ -2,15 +2,28 @@ package eu.kanade.tachiyomi.ui.browse.migration.advanced.process
import android.view.MenuItem
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.kanade.data.DatabaseHandler
import eu.kanade.data.history.historyMapper
import eu.kanade.domain.category.interactor.GetCategories
import eu.kanade.domain.category.interactor.SetMangaCategories
import eu.kanade.domain.chapter.interactor.GetChapterByMangaId
import eu.kanade.domain.chapter.interactor.UpdateChapter
import eu.kanade.domain.chapter.model.Chapter
import eu.kanade.domain.chapter.model.ChapterUpdate
import eu.kanade.domain.history.interactor.UpsertHistory
import eu.kanade.domain.history.model.HistoryUpdate
import eu.kanade.domain.manga.interactor.GetMangaById
import eu.kanade.domain.manga.interactor.UpdateManga
import eu.kanade.domain.manga.model.Manga
import eu.kanade.domain.manga.model.MangaUpdate
import eu.kanade.domain.manga.model.hasCustomCover
import eu.kanade.domain.manga.model.toDbManga
import eu.kanade.domain.track.interactor.DeleteTrack
import eu.kanade.domain.track.interactor.GetTracks
import eu.kanade.domain.track.interactor.InsertTrack
import eu.kanade.tachiyomi.data.cache.CoverCache
import eu.kanade.tachiyomi.data.database.DatabaseHelper
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.preference.PreferencesHelper
import eu.kanade.tachiyomi.ui.browse.migration.MigrationFlags
import eu.kanade.tachiyomi.util.hasCustomCover
import eu.kanade.tachiyomi.util.lang.launchUI
import eu.kanade.tachiyomi.util.lang.withIOContext
import kotlinx.coroutines.cancel
@ -19,9 +32,19 @@ import uy.kohesive.injekt.injectLazy
class MigrationProcessAdapter(
val controller: MigrationListController,
) : FlexibleAdapter<MigrationProcessItem>(null, controller, true) {
private val db: DatabaseHelper by injectLazy()
private val handler: DatabaseHandler by injectLazy()
private val preferences: PreferencesHelper by injectLazy()
private val coverCache: CoverCache by injectLazy()
private val getMangaById: GetMangaById by injectLazy()
private val updateManga: UpdateManga by injectLazy()
private val updateChapter: UpdateChapter by injectLazy()
private val getChapterByMangaId: GetChapterByMangaId by injectLazy()
private val upsertHistory: UpsertHistory by injectLazy()
private val getCategories: GetCategories by injectLazy()
private val setMangaCategories: SetMangaCategories by injectLazy()
private val getTracks: GetTracks by injectLazy()
private val insertTrack: InsertTrack by injectLazy()
private val deleteTrack: DeleteTrack by injectLazy()
var items: List<MigrationProcessItem> = emptyList()
@ -55,19 +78,16 @@ class MigrationProcessAdapter(
suspend fun performMigrations(copy: Boolean) {
withIOContext {
db.inTransaction {
currentItems.forEach { migratingManga ->
val manga = migratingManga.manga
if (manga.searchResult.initialized) {
val toMangaObj =
db.getManga(manga.searchResult.get() ?: return@forEach).executeAsBlocking()
?: return@forEach
migrateMangaInternal(
manga.manga() ?: return@forEach,
toMangaObj,
!copy,
)
}
currentItems.forEach { migratingManga ->
val manga = migratingManga.manga
if (manga.searchResult.initialized) {
val toMangaObj = getMangaById.await(manga.searchResult.get() ?: return@forEach)
?: return@forEach
migrateMangaInternal(
manga.manga() ?: return@forEach,
toMangaObj,
!copy,
)
}
}
}
@ -76,16 +96,15 @@ class MigrationProcessAdapter(
fun migrateManga(position: Int, copy: Boolean) {
launchUI {
val manga = getItem(position)?.manga ?: return@launchUI
db.inTransaction {
val toMangaObj =
db.getManga(manga.searchResult.get() ?: return@launchUI).executeAsBlocking()
?: return@launchUI
migrateMangaInternal(
manga.manga() ?: return@launchUI,
toMangaObj,
!copy,
)
}
val toMangaObj = getMangaById.await(manga.searchResult.get() ?: return@launchUI)
?: return@launchUI
migrateMangaInternal(
manga.manga() ?: return@launchUI,
toMangaObj,
!copy,
)
removeManga(position)
}
}
@ -107,7 +126,7 @@ class MigrationProcessAdapter(
sourceFinished()
}
private fun migrateMangaInternal(
private suspend fun migrateMangaInternal(
prevManga: Manga,
manga: Manga,
replace: Boolean,
@ -116,69 +135,87 @@ class MigrationProcessAdapter(
val flags = preferences.migrateFlags().get()
// Update chapters read
if (MigrationFlags.hasChapters(flags)) {
val prevMangaChapters = db.getChapters(prevManga).executeAsBlocking()
val prevMangaChapters = getChapterByMangaId.await(prevManga.id)
val maxChapterRead =
prevMangaChapters.filter(Chapter::read).maxOfOrNull(Chapter::chapter_number)
val dbChapters = db.getChapters(manga).executeAsBlocking()
val prevHistoryList = db.getHistoryByMangaId(prevManga.id!!).executeAsBlocking()
val historyList = mutableListOf<History>()
prevMangaChapters.filter(Chapter::read).maxOfOrNull(Chapter::chapterNumber)
val dbChapters = getChapterByMangaId.await(manga.id)
val prevHistoryList = handler.awaitList { historyQueries.getHistoryByMangaId(prevManga.id, historyMapper) }
val chapterUpdates = mutableListOf<ChapterUpdate>()
val historyUpdates = mutableListOf<HistoryUpdate>()
dbChapters.forEach { chapter ->
if (chapter.isRecognizedNumber) {
val prevChapter =
prevMangaChapters.find { it.isRecognizedNumber && it.chapter_number == chapter.chapter_number }
val prevChapter = prevMangaChapters.find { it.isRecognizedNumber && it.chapterNumber == chapter.chapterNumber }
if (prevChapter != null) {
chapter.bookmark = prevChapter.bookmark
chapter.read = prevChapter.read
chapter.date_fetch = prevChapter.date_fetch
prevHistoryList.find { it.chapter_id == prevChapter.id }?.let { prevHistory ->
val history = History.create(chapter).apply { last_read = prevHistory.last_read }
historyList.add(history)
chapterUpdates += ChapterUpdate(
id = chapter.id,
bookmark = prevChapter.bookmark,
read = prevChapter.read,
dateFetch = prevChapter.dateFetch,
)
prevHistoryList.find { it.chapterId == prevChapter.id }?.let { prevHistory ->
historyUpdates += HistoryUpdate(
chapter.id,
prevHistory.readAt ?: return@let,
prevHistory.readDuration,
)
}
} else if (maxChapterRead != null && chapter.chapter_number <= maxChapterRead) {
chapter.read = true
} else if (maxChapterRead != null && chapter.chapterNumber <= maxChapterRead) {
chapterUpdates += ChapterUpdate(
id = chapter.id,
read = true,
)
}
}
}
db.insertChapters(dbChapters).executeAsBlocking()
db.upsertHistoryLastRead(historyList).executeAsBlocking()
updateChapter.awaitAll(chapterUpdates)
historyUpdates.forEach {
upsertHistory.await(it)
}
}
// Update categories
if (MigrationFlags.hasCategories(flags)) {
val categories = db.getCategoriesForManga(prevManga).executeAsBlocking()
val mangaCategories = categories.map { MangaCategory.create(manga, it) }
db.setMangaCategories(mangaCategories, listOf(manga))
val categories = getCategories.await(prevManga.id)
setMangaCategories.await(manga.id, categories.map { it.id })
}
// Update track
if (MigrationFlags.hasTracks(flags)) {
val tracks = db.getTracks(prevManga.id).executeAsBlocking()
val tracks = getTracks.await(prevManga.id)
if (tracks.isNotEmpty()) {
tracks.forEach { track ->
track.id = null
track.manga_id = manga.id!!
getTracks.await(manga.id).forEach {
deleteTrack.await(manga.id, it.syncId)
}
db.insertTracks(tracks).executeAsBlocking()
insertTrack.awaitAll(tracks.map { it.copy(mangaId = manga.id) })
}
}
// Update custom cover
if (MigrationFlags.hasCustomCover(flags) && prevManga.hasCustomCover(coverCache)) {
coverCache.setCustomCoverToCache(manga, coverCache.getCustomCoverFile(prevManga.id).inputStream())
coverCache.setCustomCoverToCache(manga.toDbManga(), coverCache.getCustomCoverFile(prevManga.id).inputStream())
}
var mangaUpdate = MangaUpdate(manga.id, favorite = true, dateAdded = System.currentTimeMillis())
var prevMangaUpdate: MangaUpdate? = null
// Update extras
if (MigrationFlags.hasExtra(flags)) {
manga.chapter_flags = prevManga.chapter_flags
manga.viewer_flags = prevManga.viewer_flags
mangaUpdate = mangaUpdate.copy(
chapterFlags = prevManga.chapterFlags,
viewerFlags = prevManga.viewerFlags,
)
}
// Update favorite status
if (replace) {
prevManga.favorite = false
manga.date_added = prevManga.date_added
prevManga.date_added = 0
db.updateMangaFavorite(prevManga).executeAsBlocking()
} else {
manga.date_added = System.currentTimeMillis()
prevMangaUpdate = MangaUpdate(
id = prevManga.id,
favorite = false,
dateAdded = 0,
)
mangaUpdate = mangaUpdate.copy(
dateAdded = prevManga.dateAdded,
)
}
manga.favorite = true
db.updateMangaMigrate(manga).executeAsBlocking()
updateManga.awaitAll(listOfNotNull(mangaUpdate, prevMangaUpdate))
}
}

View File

@ -6,9 +6,11 @@ import androidx.core.view.isInvisible
import androidx.core.view.isVisible
import coil.dispose
import eu.davidea.viewholders.FlexibleViewHolder
import eu.kanade.domain.chapter.interactor.GetChapterByMangaId
import eu.kanade.domain.manga.interactor.GetMangaById
import eu.kanade.domain.manga.interactor.GetMergedReferencesById
import eu.kanade.domain.manga.model.Manga
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.DatabaseHelper
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.databinding.MigrationMangaCardBinding
import eu.kanade.tachiyomi.databinding.MigrationProcessItemBinding
import eu.kanade.tachiyomi.source.Source
@ -19,7 +21,6 @@ import eu.kanade.tachiyomi.util.lang.launchUI
import eu.kanade.tachiyomi.util.view.loadAutoPause
import eu.kanade.tachiyomi.util.view.setVectorCompat
import exh.source.MERGED_SOURCE_ID
import exh.util.executeOnIO
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import reactivecircus.flowbinding.android.view.clicks
@ -30,8 +31,10 @@ class MigrationProcessHolder(
private val view: View,
private val adapter: MigrationProcessAdapter,
) : FlexibleViewHolder(view, adapter) {
private val db: DatabaseHelper by injectLazy()
private val sourceManager: SourceManager by injectLazy()
private val getMangaById: GetMangaById by injectLazy()
private val getChapterByMangaId: GetChapterByMangaId by injectLazy()
private val getMergedReferencesById: GetMergedReferencesById by injectLazy()
private var item: MigrationProcessItem? = null
private val binding = MigrationProcessItemBinding.bind(view)
@ -67,7 +70,7 @@ class MigrationProcessHolder(
.onEach {
adapter.controller.router.pushController(
MangaController(
manga.id!!,
manga.id,
true,
),
)
@ -86,7 +89,7 @@ class MigrationProcessHolder(
}*/
val searchResult = item.manga.searchResult.get()?.let {
db.getManga(it).executeOnIO()
getMangaById.await(it)
}
val resultSource = searchResult?.source?.let {
sourceManager.get(it)
@ -103,7 +106,7 @@ class MigrationProcessHolder(
.onEach {
adapter.controller.router.pushController(
MangaController(
searchResult.id!!,
searchResult.id,
true,
),
)
@ -143,24 +146,24 @@ class MigrationProcessHolder(
title.text = if (manga.title.isBlank()) {
view.context.getString(R.string.unknown)
} else {
manga.originalTitle
manga.ogTitle
}
mangaSourceLabel.text = if (source.id == MERGED_SOURCE_ID) {
db.getMergedMangaReferences(manga.id!!).executeOnIO().map {
getMergedReferencesById.await(manga.id).map {
sourceManager.getOrStub(it.mangaSourceId).toString()
}.distinct().joinToString()
} else {
source.toString()
}
val chapters = db.getChapters(manga).executeOnIO()
val chapters = getChapterByMangaId.await(manga.id)
// For rounded corners
badges.leftBadges.clipToOutline = true
badges.rightBadges.clipToOutline = true
badges.unreadText.isVisible = true
badges.unreadText.text = chapters.size.toString()
val latestChapter = chapters.maxByOrNull { it.chapter_number }?.chapter_number ?: -1f
val latestChapter = chapters.maxOfOrNull { it.chapterNumber } ?: -1f
if (latestChapter > 0f) {
mangaLastChapterLabel.text = root.context.getString(