diff --git a/app/src/main/java/eu/kanade/data/exh/FavoriteEntry.kt b/app/src/main/java/eu/kanade/data/exh/FavoriteEntry.kt new file mode 100644 index 000000000..e22403339 --- /dev/null +++ b/app/src/main/java/eu/kanade/data/exh/FavoriteEntry.kt @@ -0,0 +1,14 @@ +package eu.kanade.data.exh + +import exh.favorites.sql.models.FavoriteEntry + +val favoriteEntryMapper: (Long, String, String, String, Long) -> FavoriteEntry = + { id, title, gid, token, category -> + FavoriteEntry( + id = id, + title = title, + gid = gid, + token = token, + category = category.toInt(), + ) + } diff --git a/app/src/main/java/eu/kanade/domain/history/interactor/RemoveHistoryById.kt b/app/src/main/java/eu/kanade/domain/history/interactor/RemoveHistoryById.kt index 4eb15568c..fa101fe13 100644 --- a/app/src/main/java/eu/kanade/domain/history/interactor/RemoveHistoryById.kt +++ b/app/src/main/java/eu/kanade/domain/history/interactor/RemoveHistoryById.kt @@ -10,4 +10,10 @@ class RemoveHistoryById( suspend fun await(history: HistoryWithRelations) { repository.resetHistory(history.id) } + + // SY --> + suspend fun await(historyId: Long) { + repository.resetHistory(historyId) + } + // SY <-- } 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 4c0eb87cb..13df390be 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 @@ -44,7 +44,6 @@ import eu.kanade.tachiyomi.source.online.MetadataSource 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.source.MERGED_SOURCE_ID import exh.source.getMainSource import exh.util.nullIfBlank @@ -198,7 +197,7 @@ class FullBackupManager(context: Context) : AbstractBackupManager(context) { val source = sourceManager.get(manga.source)?.getMainSource>() if (source != null) { - handler.getFlatMetadataForManga(manga.id)?.let { flatMetadata -> + handler.awaitFlatMetadataForManga(manga.id)?.let { flatMetadata -> mangaObject.flatMetadata = BackupFlatMetadata.copyFrom(flatMetadata) } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/source/online/MetadataSource.kt b/app/src/main/java/eu/kanade/tachiyomi/source/online/MetadataSource.kt index e343e0f4b..093ccdd78 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/source/online/MetadataSource.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/source/online/MetadataSource.kt @@ -1,6 +1,8 @@ package eu.kanade.tachiyomi.source.online import androidx.compose.runtime.Composable +import eu.kanade.data.DatabaseHandler +import eu.kanade.domain.manga.interactor.GetMangaByUrlAndSource import eu.kanade.tachiyomi.data.database.DatabaseHelper import eu.kanade.tachiyomi.data.database.models.Chapter import eu.kanade.tachiyomi.data.database.models.Manga @@ -12,8 +14,8 @@ import eu.kanade.tachiyomi.ui.manga.MangaScreenState import eu.kanade.tachiyomi.util.lang.awaitSingle import eu.kanade.tachiyomi.util.lang.runAsObservable import exh.metadata.metadata.base.RaisedSearchMetadata -import exh.metadata.metadata.base.getFlatMetadataForManga -import exh.metadata.metadata.base.insertFlatMetadata +import exh.metadata.metadata.base.awaitFlatMetadataForManga +import exh.metadata.metadata.base.awaitInsertFlatMetadata import rx.Completable import rx.Single import tachiyomi.source.model.MangaInfo @@ -26,6 +28,8 @@ import kotlin.reflect.KClass */ interface MetadataSource : CatalogueSource { val db: DatabaseHelper get() = Injekt.get() + val handler: DatabaseHandler get() = Injekt.get() + val getMangaByUrlAndSource: GetMangaByUrlAndSource get() = Injekt.get() /** * The class of the metadata used by this source @@ -59,14 +63,14 @@ interface MetadataSource : CatalogueSource { suspend fun parseToManga(manga: MangaInfo, input: I): MangaInfo { val mangaId = manga.id() val metadata = if (mangaId != null) { - val flatMetadata = db.getFlatMetadataForManga(mangaId).executeAsBlocking() + val flatMetadata = handler.awaitFlatMetadataForManga(mangaId) flatMetadata?.raise(metaClass) ?: newMetaInstance() } else newMetaInstance() parseIntoMetadata(metadata, input) if (mangaId != null) { metadata.mangaId = mangaId - db.insertFlatMetadata(metadata.flatten()) + handler.awaitInsertFlatMetadata(metadata.flatten()) } return metadata.createMangaInfo(manga) @@ -95,7 +99,7 @@ interface MetadataSource : CatalogueSource { */ suspend fun fetchOrLoadMetadata(mangaId: Long?, inputProducer: suspend () -> I): M { val meta = if (mangaId != null) { - val flatMetadata = db.getFlatMetadataForManga(mangaId).executeAsBlocking() + val flatMetadata = handler.awaitFlatMetadataForManga(mangaId) flatMetadata?.raise(metaClass) } else { null @@ -106,15 +110,16 @@ interface MetadataSource : CatalogueSource { parseIntoMetadata(newMeta, input) if (mangaId != null) { newMeta.mangaId = mangaId - db.insertFlatMetadata(newMeta.flatten()).let { newMeta } - } else newMeta + handler.awaitInsertFlatMetadata(newMeta.flatten()) + } + newMeta } } @Composable fun DescriptionComposable(state: MangaScreenState.Success, openMetadataViewer: () -> Unit, search: (String) -> Unit) - fun MangaInfo.id() = db.getManga(key, id).executeAsBlocking()?.id + suspend fun MangaInfo.id() = getMangaByUrlAndSource.await(key, id)?.id val SManga.id get() = (this as? Manga)?.id val SChapter.mangaId get() = (this as? Chapter)?.manga_id } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryController.kt index 40e814f48..76bdac3a7 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryController.kt @@ -807,7 +807,7 @@ class LibraryController( ?.setMessage(activity!!.getString(R.string.favorites_sync_bad_library_state, status.message)) ?.setCancelable(false) ?.setPositiveButton(R.string.show_gallery) { _, _ -> - openManga(status.manga) + openManga(status.manga.toDbManga()) presenter.favoritesSync.status.value = FavoritesSyncStatus.Idle(activity!!) } ?.setNegativeButton(android.R.string.ok) { _, _ -> diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaPresenter.kt index 09c66f4a5..395f83eed 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaPresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaPresenter.kt @@ -239,7 +239,7 @@ class MangaPresenter( if (chapters.isNotEmpty() && manga.isEhBasedManga() && DebugToggles.ENABLE_EXH_ROOT_REDIRECT.enabled) { // Check for gallery in library and accept manga with lowest id // Find chapters sharing same root - updateHelper.findAcceptedRootAndDiscardOthers(manga.source, chapters.map { it.toDbChapter() }) + updateHelper.findAcceptedRootAndDiscardOthers(manga.source, chapters) .onEach { (acceptedChain, _) -> // Redirect if we are not the accepted root if (manga.id != acceptedChain.manga.id && acceptedChain.manga.favorite) { @@ -250,7 +250,7 @@ class MangaPresenter( val update = (ourChapterUrls - acceptedChapterUrls).isNotEmpty() redirectFlow.emit( EXHRedirect( - acceptedChain.manga.id!!, + acceptedChain.manga.id, update, ), ) diff --git a/app/src/main/java/exh/GalleryAdder.kt b/app/src/main/java/exh/GalleryAdder.kt index 9d896f9ca..6394b76da 100755 --- a/app/src/main/java/exh/GalleryAdder.kt +++ b/app/src/main/java/exh/GalleryAdder.kt @@ -2,33 +2,38 @@ package exh import android.content.Context import androidx.core.net.toUri +import eu.kanade.data.DatabaseHandler +import eu.kanade.data.chapter.chapterMapper +import eu.kanade.data.manga.mangaMapper +import eu.kanade.domain.chapter.interactor.SyncChaptersWithSource +import eu.kanade.domain.chapter.model.Chapter +import eu.kanade.domain.manga.interactor.GetMangaById +import eu.kanade.domain.manga.interactor.GetMangaByUrlAndSource +import eu.kanade.domain.manga.interactor.UpdateManga +import eu.kanade.domain.manga.model.Manga +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.Chapter -import eu.kanade.tachiyomi.data.database.models.Manga -import eu.kanade.tachiyomi.data.database.models.toMangaInfo import eu.kanade.tachiyomi.data.preference.PreferencesHelper 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.UrlImportableSource import eu.kanade.tachiyomi.source.online.all.EHentai -import eu.kanade.tachiyomi.util.chapter.syncChaptersWithSource import exh.log.xLogStack import exh.source.getMainSource import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get -import uy.kohesive.injekt.injectLazy -class GalleryAdder { +class GalleryAdder( + private val handler: DatabaseHandler = Injekt.get(), + private val getMangaByUrlAndSource: GetMangaByUrlAndSource = Injekt.get(), + private val getMangaById: GetMangaById = Injekt.get(), + private val updateManga: UpdateManga = Injekt.get(), + private val syncChaptersWithSource: SyncChaptersWithSource = Injekt.get(), + private val sourceManager: SourceManager = Injekt.get(), +) { - private val db: DatabaseHelper by injectLazy() - - private val sourceManager: SourceManager by injectLazy() - - private val filters: Pair, Set> = run { - val preferences = Injekt.get() - preferences.enabledLanguages().get() to preferences.disabledSources().get().map { it.toLong() }.toSet() + private val filters: Pair, Set> = Injekt.get().run { + enabledLanguages().get() to disabledSources().get().map { it.toLong() }.toSet() } private val logger = xLogStack() @@ -115,33 +120,28 @@ class GalleryAdder { } ?: return GalleryAddEvent.Fail.UnknownType(url, context) // Use manga in DB if possible, otherwise, make a new manga - val manga = db.getManga(cleanedMangaUrl, source.id).executeAsBlocking() - ?: Manga.create(source.id).apply { - this.url = cleanedMangaUrl - title = realMangaUrl + var manga = getMangaByUrlAndSource.await(cleanedMangaUrl, source.id) + ?: handler.awaitOne(true) { + // Insert created manga if not in DB before fetching details + // This allows us to keep the metadata when fetching details + mangasQueries.insertEmpty( + source = source.id, + url = cleanedMangaUrl, + title = realMangaUrl, + ) + mangasQueries.selectLastInsertRow(mangaMapper) } - // Insert created manga if not in DB before fetching details - // This allows us to keep the metadata when fetching details - if (manga.id == null) { - db.insertManga(manga).executeAsBlocking().insertedId()?.let { - manga.id = it - } - } - // Fetch and copy details val newManga = source.getMangaDetails(manga.toMangaInfo()) - - manga.copyFrom(newManga.toSManga()) - manga.initialized = true + updateManga.awaitUpdateFromSource(manga, newManga, false, Injekt.get()) + manga = getMangaById.await(manga.id)!! if (fav) { - manga.favorite = true - manga.date_added = System.currentTimeMillis() + updateManga.awaitUpdateFavorite(manga.id, true) + manga = manga.copy(favorite = true) } - db.insertManga(manga).executeAsBlocking() - // Fetch and copy chapters try { val chapterList = if (source is EHentai) { @@ -151,7 +151,7 @@ class GalleryAdder { }.map { it.toSChapter() } if (chapterList.isNotEmpty()) { - syncChaptersWithSource(chapterList, manga, source) + syncChaptersWithSource.await(chapterList, manga, source) } } catch (e: Exception) { logger.w(context.getString(R.string.gallery_adder_chapter_fetch_error, manga.title), e) @@ -159,7 +159,7 @@ class GalleryAdder { } return if (cleanedChapterUrl != null) { - val chapter = db.getChapter(cleanedChapterUrl, manga.id!!).executeAsBlocking() + val chapter = handler.awaitOneOrNull { chaptersQueries.getChapterByUrlAndMangaId(cleanedChapterUrl, manga.id, chapterMapper) } if (chapter != null) { GalleryAddEvent.Success(url, manga, context, chapter) } else { diff --git a/app/src/main/java/exh/debug/DebugFunctions.kt b/app/src/main/java/exh/debug/DebugFunctions.kt index 27de8b418..523218538 100644 --- a/app/src/main/java/exh/debug/DebugFunctions.kt +++ b/app/src/main/java/exh/debug/DebugFunctions.kt @@ -4,19 +4,21 @@ import android.app.Application import androidx.work.WorkManager import com.pushtorefresh.storio.sqlite.queries.RawQuery import eu.kanade.data.DatabaseHandler +import eu.kanade.data.manga.mangaMapper +import eu.kanade.domain.manga.interactor.GetFavorites +import eu.kanade.domain.manga.interactor.UpdateManga +import eu.kanade.domain.manga.model.toMangaInfo import eu.kanade.tachiyomi.data.database.DatabaseHelper -import eu.kanade.tachiyomi.data.database.models.toMangaInfo import eu.kanade.tachiyomi.data.database.tables.MangaTable import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.source.SourceManager -import eu.kanade.tachiyomi.source.model.toSManga import eu.kanade.tachiyomi.source.online.all.NHentai import exh.EXHMigrations import exh.eh.EHentaiThrottleManager import exh.eh.EHentaiUpdateWorker import exh.metadata.metadata.EHentaiSearchMetadata -import exh.metadata.metadata.base.getFlatMetadataForManga -import exh.metadata.metadata.base.insertFlatMetadataAsync +import exh.metadata.metadata.base.awaitFlatMetadataForManga +import exh.metadata.metadata.base.awaitInsertFlatMetadata import exh.source.EH_SOURCE_ID import exh.source.EXH_SOURCE_ID import exh.source.isEhBasedManga @@ -35,9 +37,11 @@ import java.util.UUID object DebugFunctions { val app: Application by injectLazy() val db: DatabaseHelper by injectLazy() - val database: DatabaseHandler by injectLazy() + val handler: DatabaseHandler by injectLazy() val prefs: PreferencesHelper by injectLazy() val sourceManager: SourceManager by injectLazy() + val updateManga: UpdateManga by injectLazy() + val getFavorites: GetFavorites by injectLazy() fun forceUpgradeMigration() { prefs.ehLastVersionCode().set(1) @@ -59,10 +63,10 @@ object DebugFunctions { }.toList() allManga.forEach { manga -> - val meta = db.getFlatMetadataForManga(manga.id!!).executeOnIO()?.raise() ?: return@forEach + val meta = handler.awaitFlatMetadataForManga(manga.id!!)?.raise() ?: return@forEach // remove age flag meta.aged = false - db.insertFlatMetadataAsync(meta.flatten()).await() + handler.awaitInsertFlatMetadata(meta.flatten()) } } } @@ -73,69 +77,49 @@ object DebugFunctions { fun resetEHGalleriesForUpdater() { throttleManager.resetThrottle() runBlocking { - val metadataManga = db.getFavoriteMangaWithMetadata().executeOnIO() + val allManga = handler + .awaitList { mangasQueries.getEhMangaWithMetadata(EH_SOURCE_ID, EXH_SOURCE_ID, mangaMapper) } - val allManga = metadataManga.asFlow().cancellable().mapNotNull { manga -> - if (manga.isEhBasedManga()) manga - else null - }.toList() val eh = sourceManager.get(EH_SOURCE_ID) val ex = sourceManager.get(EXH_SOURCE_ID) allManga.forEach { manga -> throttleManager.throttle() - ( - when (manga.source) { - EH_SOURCE_ID -> eh - EXH_SOURCE_ID -> ex - else -> return@forEach - } - )?.getMangaDetails(manga.toMangaInfo())?.let { networkManga -> - manga.copyFrom(networkManga.toSManga()) - manga.initialized = true - db.insertManga(manga).executeOnIO() - } + + val networkManga = when (manga.source) { + EH_SOURCE_ID -> eh + EXH_SOURCE_ID -> ex + else -> return@forEach + }?.getMangaDetails(manga.toMangaInfo()) ?: return@forEach + + updateManga.awaitUpdateFromSource(manga, networkManga, true) } } } fun getEHMangaListWithAgedFlagInfo(): String { - val galleries = mutableListOf(String()) - runBlocking { - val metadataManga = db.getFavoriteMangaWithMetadata().executeOnIO() + return runBlocking { + val allManga = handler + .awaitList { mangasQueries.getEhMangaWithMetadata(EH_SOURCE_ID, EXH_SOURCE_ID, mangaMapper) } - val allManga = metadataManga.asFlow().cancellable().mapNotNull { manga -> - if (manga.isEhBasedManga()) manga - else null - }.toList() - - allManga.forEach { manga -> - val meta = db.getFlatMetadataForManga(manga.id!!).executeOnIO()?.raise() ?: return@forEach - galleries += "Aged: ${meta.aged}\t Title: ${manga.title}" + allManga.map { manga -> + val meta = handler.awaitFlatMetadataForManga(manga.id)?.raise() ?: return@map + "Aged: ${meta.aged}\t Title: ${manga.title}" } - } - return galleries.joinToString(",\n") + }.joinToString(",\n") } fun countAgedFlagInEXHManga(): Int { - var agedAmount = 0 - runBlocking { - val metadataManga = db.getFavoriteMangaWithMetadata().executeOnIO() - - val allManga = metadataManga.asFlow().cancellable().mapNotNull { manga -> - if (manga.isEhBasedManga()) manga - else null - }.toList() - - allManga.forEach { manga -> - val meta = db.getFlatMetadataForManga(manga.id!!).executeOnIO()?.raise() ?: return@forEach - if (meta.aged) { - // remove age flag - agedAmount++ + return runBlocking { + handler + .awaitList { mangasQueries.getEhMangaWithMetadata(EH_SOURCE_ID, EXH_SOURCE_ID, mangaMapper) } + .count { manga -> + val meta = handler.awaitFlatMetadataForManga(manga.id) + ?.raise() + ?: return@count false + meta.aged } - } } - return agedAmount } fun addAllMangaInDatabaseToLibrary() { @@ -154,7 +138,7 @@ object DebugFunctions { } } - fun countMangaInDatabaseInLibrary() = db.getMangas().executeAsBlocking().count { it.favorite } + fun countMangaInDatabaseInLibrary() = runBlocking { getFavorites.await().size } fun countMangaInDatabaseNotInLibrary() = db.getMangas().executeAsBlocking().count { !it.favorite } @@ -166,7 +150,7 @@ object DebugFunctions { it.favorite && db.getSearchMetadataForManga(it.id!!).executeAsBlocking() == null } - fun clearSavedSearches() = runBlocking { database.await { saved_searchQueries.deleteAll() } } + fun clearSavedSearches() = runBlocking { handler.await { saved_searchQueries.deleteAll() } } fun listAllSources() = sourceManager.getCatalogueSources().joinToString("\n") { "${it.id}: ${it.name} (${it.lang.uppercase()})" diff --git a/app/src/main/java/exh/eh/EHentaiUpdateHelper.kt b/app/src/main/java/exh/eh/EHentaiUpdateHelper.kt index 2511a4f18..28738f179 100644 --- a/app/src/main/java/exh/eh/EHentaiUpdateHelper.kt +++ b/app/src/main/java/exh/eh/EHentaiUpdateHelper.kt @@ -1,12 +1,23 @@ package exh.eh import android.content.Context -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 exh.util.executeOnIO +import eu.kanade.data.DatabaseHandler +import eu.kanade.data.chapter.chapterMapper +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.model.Chapter +import eu.kanade.domain.chapter.model.ChapterUpdate +import eu.kanade.domain.chapter.repository.ChapterRepository +import eu.kanade.domain.history.interactor.RemoveHistoryById +import eu.kanade.domain.history.interactor.UpsertHistory +import eu.kanade.domain.history.model.History +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 kotlinx.coroutines.Dispatchers import kotlinx.coroutines.async import kotlinx.coroutines.coroutineScope @@ -24,7 +35,15 @@ class EHentaiUpdateHelper(context: Context) { File(context.filesDir, "exh-plt.maftable"), GalleryEntry.Serializer(), ) - private val db: DatabaseHelper by injectLazy() + private val handler: DatabaseHandler by injectLazy() + private val getChapterByMangaId: GetChapterByMangaId by injectLazy() + private val getMangaById: GetMangaById by injectLazy() + private val updateManga: UpdateManga by injectLazy() + private val setMangaCategories: SetMangaCategories by injectLazy() + private val getCategories: GetCategories by injectLazy() + private val chapterRepository: ChapterRepository by injectLazy() + private val upsertHistory: UpsertHistory by injectLazy() + private val removeHistoryById: RemoveHistoryById by injectLazy() /** * @param chapters Cannot be an empty list! @@ -36,7 +55,8 @@ class EHentaiUpdateHelper(context: Context) { val chainsFlow = flowOf(chapters) .map { chapterList -> chapterList.flatMap { chapter -> - db.getChapters(chapter.url).executeOnIO().mapNotNull { it.manga_id } + handler.awaitList { chaptersQueries.getChapterByUrl(chapter.url, chapterMapper) } + .map { it.mangaId } }.distinct() } .map { mangaIds -> @@ -44,13 +64,13 @@ class EHentaiUpdateHelper(context: Context) { .mapNotNull { mangaId -> coroutineScope { val manga = async(Dispatchers.IO) { - db.getManga(mangaId).executeAsBlocking() + getMangaById.await(mangaId) } val chapterList = async(Dispatchers.IO) { - db.getChapters(mangaId).executeAsBlocking() + getChapterByMangaId.await(mangaId) } val history = async(Dispatchers.IO) { - db.getHistoryByMangaId(mangaId).executeAsBlocking() + handler.awaitList { historyQueries.getHistoryByMangaId(mangaId, historyMapper) } } ChapterChain( manga.await() ?: return@coroutineScope null, @@ -64,66 +84,66 @@ class EHentaiUpdateHelper(context: Context) { // Accept oldest chain val chainsWithAccepted = chainsFlow.map { chains -> - val acceptedChain = chains.minByOrNull { it.manga.id!! }!! + val acceptedChain = chains.minBy { it.manga.id } acceptedChain to chains } return chainsWithAccepted.map { (accepted, chains) -> val toDiscard = chains.filter { it.manga.favorite && it.manga.id != accepted.manga.id } + val mangaUpdates = mutableListOf() val chainsAsChapters = chains.flatMap { it.chapters } val chainsAsHistory = chains.flatMap { it.history } if (toDiscard.isNotEmpty()) { // Copy chain chapters to curChapters - val (newChapters, new) = getChapterList(accepted, toDiscard, chainsAsChapters) - val (history, urlHistory, deleteHistory) = getHistory(newChapters, chainsAsChapters, chainsAsHistory) + val (chapterUpdates, newChapters, new) = getChapterList(accepted, toDiscard, chainsAsChapters) toDiscard.forEach { - it.manga.favorite = false - it.manga.date_added = 0 + mangaUpdates += MangaUpdate( + id = it.manga.id, + favorite = false, + dateAdded = 0, + ) } if (!accepted.manga.favorite) { - accepted.manga.favorite = true - accepted.manga.date_added = System.currentTimeMillis() + mangaUpdates += MangaUpdate( + id = accepted.manga.id, + favorite = true, + dateAdded = System.currentTimeMillis(), + ) } - val newAccepted = ChapterChain(accepted.manga, newChapters, history + urlHistory.map { it.second }) + val newAccepted = ChapterChain(accepted.manga, newChapters, emptyList()) val rootsToMutate = toDiscard + newAccepted - db.inTransaction { - // Apply changes to all manga - db.insertMangas(rootsToMutate.map { it.manga }).executeAsBlocking() - // Insert new chapters for accepted manga - val chapterPutResults = db.insertChapters(newAccepted.chapters).executeAsBlocking().results() + // Apply changes to all manga + updateManga.awaitAll(mangaUpdates) + // Insert new chapters for accepted manga + chapterRepository.updateAll(chapterUpdates) + chapterRepository.addAll(newChapters) - // Delete the duplicate history first - if (deleteHistory.isNotEmpty()) { - db.deleteHistoryIds(deleteHistory).executeAsBlocking() - } - // Get a updated history list - val newHistory = urlHistory.mapNotNull { (url, history) -> - val result = chapterPutResults.firstNotNullOfOrNull { (chapter, result) -> - if (chapter.url == url) { - result.insertedId() - } else null - } - if (result != null) { - history.chapter_id = result - history - } else null - } + history - // Copy the new history chapter ids - db.updateHistoryChapterIds(newHistory).executeAsBlocking() + val (newHistory, deleteHistory) = getHistory(getChapterByMangaId.await(accepted.manga.id), chainsAsChapters, chainsAsHistory) - // Copy categories from all chains to accepted manga - val newCategories = rootsToMutate.flatMap { - db.getCategoriesForManga(it.manga).executeAsBlocking() - }.distinctBy { it.id }.map { - MangaCategory.create(newAccepted.manga, it) + // Delete the duplicate history first + if (deleteHistory.isNotEmpty()) { + deleteHistory.forEach { + removeHistoryById.await(it) } - db.setMangaCategories(newCategories, rootsToMutate.map { it.manga }) + } + // Insert new history + newHistory.forEach { + upsertHistory.await(it) + } + + // Copy categories from all chains to accepted manga + + val newCategories = rootsToMutate.flatMap { + getCategories.await(it.manga.id).map { it.id } + }.distinct() + rootsToMutate.forEach { + setMangaCategories.await(it.manga.id, newCategories) } Triple(newAccepted, toDiscard, new) @@ -140,105 +160,105 @@ class EHentaiUpdateHelper(context: Context) { } } - data class HistoryUpdates( - val history: List, - val urlHistory: List>, - val historyToDelete: List, - ) - - private fun getHistory( - newChapters: List, + fun getHistory( + currentChapters: List, chainsAsChapters: List, chainsAsHistory: List, - ): HistoryUpdates { - val historyMap = chainsAsHistory - .groupBy { history -> - chainsAsChapters.find { it.id == history.chapter_id }?.url.orEmpty() - } - .filterKeys { it.isNotBlank() } - val latestHistory = historyMap.mapValues { entry -> - entry.value.maxByOrNull { - it.time_read - }!! - } - val oldHistory = historyMap.flatMap { entry -> - val topEntry = entry.value.maxByOrNull { - it.time_read - }!! - entry.value - topEntry - }.mapNotNull { it.id } - return HistoryUpdates( - latestHistory.filter { (_, history) -> - val oldChapter = chainsAsChapters.find { it.id == history.chapter_id } - val newChapter = newChapters.find { it.url == oldChapter?.url } - if (oldChapter != newChapter && newChapter?.id != null) { - history.chapter_id = newChapter.id!! - true - } else false - }.mapNotNull { it.value }, - latestHistory.mapNotNull { (url, history) -> - val oldChapter = chainsAsChapters.find { it.id == history.chapter_id } - val newChapter = newChapters.find { it.url == oldChapter?.url } - if (oldChapter != newChapter && newChapter?.id == null) { - url to history - } else { - null + ): Pair, List> { + val history = chainsAsHistory.groupBy { history -> chainsAsChapters.find { it.id == history.chapterId }?.url } + val newHistory = currentChapters.mapNotNull { chapter -> + val newHistory = history[chapter.url] + ?.maxByOrNull { + it.readAt?.time ?: 0 } - }, - oldHistory, - ) + ?.takeIf { it.chapterId != chapter.id && it.readAt != null } + if (newHistory != null) { + HistoryUpdate(chapter.id, newHistory.readAt!!, newHistory.readDuration) + } else null + } + val currentChapterIds = currentChapters.map { it.id } + val historyToDelete = chainsAsHistory.filterNot { it.chapterId in currentChapterIds } + .map { it.id } + return newHistory to historyToDelete } private fun getChapterList( accepted: ChapterChain, toDiscard: List, chainsAsChapters: List, - ): Pair, Boolean> { + ): Triple, List, Boolean> { var new = false return toDiscard .flatMap { chain -> chain.chapters } .fold(accepted.chapters) { curChapters, chapter -> - val existing = curChapters.find { it.url == chapter.url } + val newLastPageRead = chainsAsChapters.maxOfOrNull { it.lastPageRead } - val newLastPageRead = chainsAsChapters.maxOfOrNull { it.last_page_read } - - if (existing != null) { - existing.read = existing.read || chapter.read - existing.last_page_read = existing.last_page_read.coerceAtLeast(chapter.last_page_read) - if (newLastPageRead != null && existing.last_page_read <= 0) { - existing.last_page_read = newLastPageRead + if (curChapters.any { it.url == chapter.url }) { + curChapters.map { + if (it.url == chapter.url) { + val read = it.read || chapter.read + var lastPageRead = it.lastPageRead.coerceAtLeast(chapter.lastPageRead) + if (newLastPageRead != null && lastPageRead <= 0) { + lastPageRead = newLastPageRead + } + val bookmark = it.bookmark || chapter.bookmark + it.copy( + read = read, + lastPageRead = lastPageRead, + bookmark = bookmark, + ) + } else it } - existing.bookmark = existing.bookmark || chapter.bookmark - curChapters } else { new = true - curChapters + Chapter.create().apply { - manga_id = accepted.manga.id - url = chapter.url - name = chapter.name - read = chapter.read - bookmark = chapter.bookmark - - last_page_read = chapter.last_page_read - if (newLastPageRead != null && last_page_read <= 0) { - last_page_read = newLastPageRead - } - - date_fetch = chapter.date_fetch - date_upload = chapter.date_upload - } + curChapters + Chapter( + id = -1, + mangaId = accepted.manga.id, + url = chapter.url, + name = chapter.name, + read = chapter.read, + bookmark = chapter.bookmark, + lastPageRead = if (newLastPageRead != null && chapter.lastPageRead <= 0) { + newLastPageRead + } else chapter.lastPageRead, + dateFetch = chapter.dateFetch, + dateUpload = chapter.dateUpload, + chapterNumber = -1F, + scanlator = null, + sourceOrder = -1, + ) } } - .sortedBy { it.date_upload } + .sortedBy { it.dateUpload } .let { chapters -> - chapters.onEachIndexed { index, chapter -> - chapter.name = "v${index + 1}: " + chapter.name.substringAfter(" ") - chapter.chapter_number = index + 1f - chapter.source_order = chapters.lastIndex - index + val updates = mutableListOf() + val newChapters = mutableListOf() + chapters.mapIndexed { index, chapter -> + val name = "v${index + 1}: " + chapter.name.substringAfter(" ") + val chapterNumber = index + 1f + val sourceOrder = chapters.lastIndex - index.toLong() + when (chapter.id) { + -1L -> newChapters.add( + chapter.copy( + name = name, + chapterNumber = chapterNumber, + sourceOrder = sourceOrder, + ), + ) + else -> updates.add( + ChapterUpdate( + id = chapter.id, + name = name.takeUnless { chapter.name == it }, + chapterNumber = chapterNumber.takeUnless { chapter.chapterNumber == it }, + sourceOrder = sourceOrder.takeUnless { chapter.sourceOrder == it }, + ), + ) + } } - } to new + Triple(updates.toList(), newChapters.toList(), new) + } } } diff --git a/app/src/main/java/exh/eh/EHentaiUpdateWorker.kt b/app/src/main/java/exh/eh/EHentaiUpdateWorker.kt index b46d178a0..af7fe1405 100644 --- a/app/src/main/java/exh/eh/EHentaiUpdateWorker.kt +++ b/app/src/main/java/exh/eh/EHentaiUpdateWorker.kt @@ -11,29 +11,35 @@ import androidx.work.WorkManager import androidx.work.WorkerParameters import com.elvishew.xlog.Logger import com.elvishew.xlog.XLog +import eu.kanade.data.DatabaseHandler +import eu.kanade.data.manga.mangaMapper +import eu.kanade.domain.chapter.interactor.GetChapterByMangaId +import eu.kanade.domain.chapter.interactor.SyncChaptersWithSource +import eu.kanade.domain.chapter.model.Chapter +import eu.kanade.domain.chapter.model.toDbChapter +import eu.kanade.domain.manga.interactor.UpdateManga +import eu.kanade.domain.manga.model.Manga +import eu.kanade.domain.manga.model.toDbManga +import eu.kanade.domain.manga.model.toMangaInfo 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.toMangaInfo import eu.kanade.tachiyomi.data.library.LibraryUpdateNotifier import eu.kanade.tachiyomi.data.preference.DEVICE_CHARGING import eu.kanade.tachiyomi.data.preference.DEVICE_ONLY_ON_WIFI import eu.kanade.tachiyomi.data.preference.PreferencesHelper 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.util.chapter.syncChaptersWithSource import eu.kanade.tachiyomi.util.system.isConnectedToWifi import exh.debug.DebugToggles import exh.eh.EHentaiUpdateWorkerConstants.UPDATES_PER_ITERATION import exh.log.xLog import exh.metadata.metadata.EHentaiSearchMetadata -import exh.metadata.metadata.base.getFlatMetadataForManga +import exh.metadata.metadata.base.awaitFlatMetadataForManga import exh.metadata.metadata.base.insertFlatMetadataAsync +import exh.source.EH_SOURCE_ID +import exh.source.EXH_SOURCE_ID import exh.source.isEhBasedManga import exh.util.cancellable -import exh.util.executeOnIO import kotlinx.coroutines.flow.asFlow import kotlinx.coroutines.flow.mapNotNull import kotlinx.coroutines.flow.single @@ -49,10 +55,14 @@ import kotlin.time.Duration.Companion.days class EHentaiUpdateWorker(private val context: Context, workerParams: WorkerParameters) : CoroutineWorker(context, workerParams) { private val db: DatabaseHelper by injectLazy() + private val handler: DatabaseHandler by injectLazy() private val prefs: PreferencesHelper by injectLazy() private val sourceManager: SourceManager by injectLazy() private val updateHelper: EHentaiUpdateHelper by injectLazy() private val logger: Logger = xLog() + private val updateManga: UpdateManga by injectLazy() + private val syncChaptersWithSource: SyncChaptersWithSource by injectLazy() + private val getChapterByMangaId: GetChapterByMangaId by injectLazy() private val updateNotifier by lazy { LibraryUpdateNotifier(context) } @@ -76,7 +86,7 @@ class EHentaiUpdateWorker(private val context: Context, workerParams: WorkerPara val startTime = System.currentTimeMillis() logger.d("Finding manga with metadata...") - val metadataManga = db.getFavoriteMangaWithMetadata().executeOnIO() + val metadataManga = handler.awaitList { mangasQueries.getEhMangaWithMetadata(EH_SOURCE_ID, EXH_SOURCE_ID, mangaMapper) } logger.d("Filtering manga and raising metadata...") val curTime = System.currentTimeMillis() @@ -85,7 +95,7 @@ class EHentaiUpdateWorker(private val context: Context, workerParams: WorkerPara return@mapNotNull null } - val meta = db.getFlatMetadataForManga(manga.id!!).executeOnIO() + val meta = handler.awaitFlatMetadataForManga(manga.id) ?: return@mapNotNull null val raisedMeta = meta.raise() @@ -95,8 +105,8 @@ class EHentaiUpdateWorker(private val context: Context, workerParams: WorkerPara return@mapNotNull null } - val chapter = db.getChapters(manga.id!!).executeOnIO().minByOrNull { - it.date_upload + val chapter = getChapterByMangaId.await(manga.id).minByOrNull { + it.dateUpload } UpdateEntry(manga, raisedMeta, chapter) @@ -176,8 +186,8 @@ class EHentaiUpdateWorker(private val context: Context, workerParams: WorkerPara updatedManga += acceptedRoot.manga to new.toTypedArray() } - modifiedThisIteration += acceptedRoot.manga.id!! - modifiedThisIteration += discardedRoots.map { it.manga.id!! } + modifiedThisIteration += acceptedRoot.manga.id + modifiedThisIteration += discardedRoots.map { it.manga.id } updatedThisIteration++ } } finally { @@ -192,7 +202,7 @@ class EHentaiUpdateWorker(private val context: Context, workerParams: WorkerPara ) if (updatedManga.isNotEmpty()) { - updateNotifier.showUpdateNotifications(updatedManga) + updateNotifier.showUpdateNotifications(updatedManga.map { it.first.toDbManga() to it.second.map { it.toDbChapter() }.toTypedArray() }) } } } @@ -204,17 +214,16 @@ class EHentaiUpdateWorker(private val context: Context, workerParams: WorkerPara try { val updatedManga = source.getMangaDetails(manga.toMangaInfo()) - manga.copyFrom(updatedManga.toSManga()) - db.insertManga(manga).executeOnIO() + updateManga.awaitUpdateFromSource(manga, updatedManga, false) val newChapters = source.getChapterList(manga.toMangaInfo()) .map { it.toSChapter() } - val (new, _) = syncChaptersWithSource(newChapters, manga, source) // Not suspending, but does block, maybe fix this? - return new to db.getChapters(manga).executeOnIO() + val (new, _) = syncChaptersWithSource.await(newChapters, manga, source) + return new to getChapterByMangaId.await(manga.id) } catch (t: Throwable) { if (t is EHentai.GalleryNotFoundException) { - val meta = db.getFlatMetadataForManga(manga.id!!).executeOnIO()?.raise() + val meta = handler.awaitFlatMetadataForManga(manga.id)?.raise() if (meta != null) { // Age dead galleries logger.d("Aged %s - notfound", manga.id) diff --git a/app/src/main/java/exh/favorites/FavoritesSyncHelper.kt b/app/src/main/java/exh/favorites/FavoritesSyncHelper.kt index ae8f4d646..d534a7565 100644 --- a/app/src/main/java/exh/favorites/FavoritesSyncHelper.kt +++ b/app/src/main/java/exh/favorites/FavoritesSyncHelper.kt @@ -3,18 +3,23 @@ package exh.favorites import android.content.Context import android.net.wifi.WifiManager import android.os.PowerManager +import eu.kanade.data.AndroidDatabaseHandler +import eu.kanade.data.DatabaseHandler +import eu.kanade.domain.category.interactor.GetCategories +import eu.kanade.domain.category.interactor.SetMangaCategories +import eu.kanade.domain.category.model.Category +import eu.kanade.domain.manga.interactor.GetMangaByUrlAndSource +import eu.kanade.domain.manga.interactor.UpdateManga +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.Category -import eu.kanade.tachiyomi.data.database.models.Manga -import eu.kanade.tachiyomi.data.database.models.MangaCategory +import eu.kanade.tachiyomi.data.database.models.toDomainManga import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.network.POST import eu.kanade.tachiyomi.network.await import eu.kanade.tachiyomi.source.SourceManager import eu.kanade.tachiyomi.source.online.all.EHentai -import eu.kanade.tachiyomi.util.lang.launchUI import eu.kanade.tachiyomi.util.lang.withIOContext +import eu.kanade.tachiyomi.util.lang.withUIContext import eu.kanade.tachiyomi.util.system.powerManager import eu.kanade.tachiyomi.util.system.toast import exh.GalleryAddEvent @@ -29,13 +34,11 @@ import exh.source.isEhBasedManga import exh.util.ignore import exh.util.wifiManager import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.cancel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.launch -import kotlinx.coroutines.newSingleThreadContext import okhttp3.FormBody import okhttp3.Request import uy.kohesive.injekt.Injekt @@ -43,16 +46,18 @@ import uy.kohesive.injekt.api.get import uy.kohesive.injekt.injectLazy import kotlin.time.Duration.Companion.seconds +// TODO only apply database changes after sync class FavoritesSyncHelper(val context: Context) { - private val db: DatabaseHelper by injectLazy() + private val handler: DatabaseHandler by injectLazy() + private val getCategories: GetCategories by injectLazy() + private val getMangaByUrlAndSource: GetMangaByUrlAndSource by injectLazy() + private val updateManga: UpdateManga by injectLazy() + private val setMangaCategories: SetMangaCategories by injectLazy() private val prefs: PreferencesHelper by injectLazy() private val scope = CoroutineScope(Job() + Dispatchers.Main) - @OptIn(DelicateCoroutinesApi::class) - private val dispatcher = newSingleThreadContext("Favorites-sync-worker") - private val exh by lazy { Injekt.get().get(EXH_SOURCE_ID) as? EHentai ?: EHentai(0, true, context) @@ -79,7 +84,7 @@ class FavoritesSyncHelper(val context: Context) { status.value = FavoritesSyncStatus.Initializing(context) - scope.launch(dispatcher) { beginSync() } + scope.launch(Dispatchers.IO) { beginSync() } } private suspend fun beginSync() { @@ -91,14 +96,14 @@ class FavoritesSyncHelper(val context: Context) { // Validate library state status.value = FavoritesSyncStatus.Processing(context.getString(R.string.favorites_sync_verifying_library), context = context) - val libraryManga = db.getLibraryMangas().executeAsBlocking() + val libraryManga = handler.awaitList { (handler as AndroidDatabaseHandler).getLibraryQuery() } val seenManga = HashSet(libraryManga.size) libraryManga.forEach { if (!it.isEhBasedManga()) return@forEach if (it.id in seenManga) { - val inCategories = db.getCategoriesForManga(it).executeAsBlocking() - status.value = FavoritesSyncStatus.BadLibraryState.MangaInMultipleCategories(it, inCategories, context) + val inCategories = getCategories.await(it.id!!) + status.value = FavoritesSyncStatus.BadLibraryState.MangaInMultipleCategories(it.toDomainManga()!!, inCategories, context) logger.w(context.getString(R.string.favorites_sync_manga_multiple_categories_error, it.id)) return @@ -139,32 +144,30 @@ class FavoritesSyncHelper(val context: Context) { // Do not update galleries while syncing favorites EHentaiUpdateWorker.cancelBackground(context) - db.inTransaction { - status.value = FavoritesSyncStatus.Processing(context.getString(R.string.favorites_sync_calculating_remote_changes), context = context) - val remoteChanges = storage.getChangedRemoteEntries(favorites.first) - val localChanges = if (prefs.exhReadOnlySync().get()) { - null // Do not build local changes if they are not going to be applied - } else { - status.value = FavoritesSyncStatus.Processing(context.getString(R.string.favorites_sync_calculating_local_changes), context = context) - storage.getChangedDbEntries() - } - - // Apply remote categories - status.value = FavoritesSyncStatus.Processing(context.getString(R.string.favorites_sync_syncing_category_names), context = context) - applyRemoteCategories(favorites.second) - - // Apply change sets - applyChangeSetToLocal(errorList, remoteChanges) - if (localChanges != null) { - applyChangeSetToRemote(errorList, localChanges) - } - - status.value = FavoritesSyncStatus.Processing(context.getString(R.string.favorites_sync_cleaning_up), context = context) - storage.snapshotEntries() + status.value = FavoritesSyncStatus.Processing(context.getString(R.string.favorites_sync_calculating_remote_changes), context = context) + val remoteChanges = storage.getChangedRemoteEntries(favorites.first) + val localChanges = if (prefs.exhReadOnlySync().get()) { + null // Do not build local changes if they are not going to be applied + } else { + status.value = FavoritesSyncStatus.Processing(context.getString(R.string.favorites_sync_calculating_local_changes), context = context) + storage.getChangedDbEntries() } - launchUI { - context.toast(context.getString(R.string.favorites_sync_complete)) + // Apply remote categories + status.value = FavoritesSyncStatus.Processing(context.getString(R.string.favorites_sync_syncing_category_names), context = context) + applyRemoteCategories(favorites.second) + + // Apply change sets + applyChangeSetToLocal(errorList, remoteChanges) + if (localChanges != null) { + applyChangeSetToRemote(errorList, localChanges) + } + + status.value = FavoritesSyncStatus.Processing(context.getString(R.string.favorites_sync_cleaning_up), context = context) + storage.snapshotEntries() + + withUIContext { + context.toast(R.string.favorites_sync_complete) } } catch (e: IgnoredException) { // Do not display error as this error has already been reported @@ -196,46 +199,33 @@ class FavoritesSyncHelper(val context: Context) { } } - private fun applyRemoteCategories(categories: List) { - val localCategories = db.getCategories().executeAsBlocking() + private suspend fun applyRemoteCategories(categories: List) { + val localCategories = getCategories.await() val newLocalCategories = localCategories.toMutableList() - var changed = false - categories.forEachIndexed { index, remote -> val local = localCategories.getOrElse(index) { - changed = true + val newCategoryId = handler.awaitOne(true) { + categoriesQueries.insert(remote, index.toLong(), 0L, emptyList()) + categoriesQueries.selectLastInsertedRowId() + } + Category(newCategoryId, remote, index.toLong(), 0L, emptyList()) + .also { newLocalCategories += it } + } - Category.create(remote).apply { - order = index - - // Going through categories list from front to back - // If category does not exist, list size <= category index - // Thus, we can just add it here and not worry about indexing - newLocalCategories += this + // Ensure consistent ordering and naming + if (local.name != remote || local.order != index.toLong()) { + handler.await { + categoriesQueries.update( + categoryId = local.id, + order = index.toLong().takeIf { it != local.order }, + name = remote.takeIf { it != local.name }, + flags = null, + mangaOrder = null, + ) } } - - if (local.name != remote) { - changed = true - - local.name = remote - } - } - - // Ensure consistent ordering - newLocalCategories.forEachIndexed { index, category -> - if (category.order != index) { - changed = true - - category.order = index - } - } - - // Only insert categories if changed - if (changed) { - db.insertCategories(newLocalCategories).executeAsBlocking() } } @@ -339,27 +329,25 @@ class FavoritesSyncHelper(val context: Context) { // Consider both EX and EH sources listOf( - db.getManga(url, EXH_SOURCE_ID), - db.getManga(url, EH_SOURCE_ID), + EXH_SOURCE_ID, + EH_SOURCE_ID, ).forEach { - val manga = it.executeAsBlocking() + val manga = getMangaByUrlAndSource.await(url, it) if (manga?.favorite == true) { - manga.favorite = false - manga.date_added = 0 - db.updateMangaFavorite(manga).executeAsBlocking() + updateManga.awaitUpdateFavorite(manga.id, false) removedManga += manga } } } // Can't do too many DB OPs in one go - removedManga.chunked(10).forEach { - db.deleteOldMangasCategories(it).executeAsBlocking() + removedManga.forEach { + setMangaCategories.await(it.id, emptyList()) } - val insertedMangaCategories = mutableListOf>() - val categories = db.getCategories().executeAsBlocking() + val insertedMangaCategories = mutableListOf>() + val categories = getCategories.await() // Apply additions throttleManager.resetThrottle() @@ -402,18 +390,13 @@ class FavoritesSyncHelper(val context: Context) { throw IgnoredException() } } else if (result is GalleryAddEvent.Success) { - insertedMangaCategories += MangaCategory.create( - result.manga, - categories[it.category], - ) to result.manga + insertedMangaCategories += categories[it.category].id to result.manga } } // Can't do too many DB OPs in one go - insertedMangaCategories.chunked(10).map { mangaCategories -> - mangaCategories.map { it.first } to mangaCategories.map { it.second } - }.forEach { - db.setMangaCategories(it.first, it.second) + insertedMangaCategories.forEach { (category, manga) -> + setMangaCategories.await(manga.id, listOf(category)) } } @@ -424,7 +407,6 @@ class FavoritesSyncHelper(val context: Context) { fun onDestroy() { scope.cancel() - dispatcher.close() } companion object { diff --git a/app/src/main/java/exh/favorites/LocalFavoritesStorage.kt b/app/src/main/java/exh/favorites/LocalFavoritesStorage.kt index 3853e40f1..57f9f0087 100644 --- a/app/src/main/java/exh/favorites/LocalFavoritesStorage.kt +++ b/app/src/main/java/exh/favorites/LocalFavoritesStorage.kt @@ -1,56 +1,77 @@ package exh.favorites -import eu.kanade.tachiyomi.data.database.DatabaseHelper -import eu.kanade.tachiyomi.data.database.models.Manga +import eu.kanade.data.DatabaseHandler +import eu.kanade.data.exh.favoriteEntryMapper +import eu.kanade.domain.category.interactor.GetCategories +import eu.kanade.domain.manga.interactor.GetFavorites +import eu.kanade.domain.manga.model.Manga +import eu.kanade.tachiyomi.data.database.models.toDomainManga import eu.kanade.tachiyomi.source.online.all.EHentai import exh.favorites.sql.models.FavoriteEntry import exh.metadata.metadata.EHentaiSearchMetadata import exh.source.isEhBasedManga +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.asFlow +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.mapNotNull +import kotlinx.coroutines.flow.toList import uy.kohesive.injekt.injectLazy class LocalFavoritesStorage { - private val db: DatabaseHelper by injectLazy() + private val handler: DatabaseHandler by injectLazy() + private val getFavorites: GetFavorites by injectLazy() + private val getCategories: GetCategories by injectLazy() - fun getChangedDbEntries() = db.getFavoriteMangas() - .executeAsBlocking() - .asSequence() + suspend fun getChangedDbEntries() = getFavorites.await() + .asFlow() .loadDbCategories() .parseToFavoriteEntries() .getChangedEntries() - fun getChangedRemoteEntries(entries: List) = entries - .asSequence() + suspend fun getChangedRemoteEntries(entries: List) = entries + .asFlow() .map { it.fav to it.manga.apply { + id = -1 favorite = true date_added = System.currentTimeMillis() - } + }.toDomainManga()!! } .parseToFavoriteEntries() .getChangedEntries() - fun snapshotEntries() { - val dbMangas = db.getFavoriteMangas() - .executeAsBlocking() - .asSequence() + suspend fun snapshotEntries() { + val dbMangas = getFavorites.await() + .asFlow() .loadDbCategories() .parseToFavoriteEntries() // Delete old snapshot - db.deleteAllFavoriteEntries().executeAsBlocking() + handler.await { eh_favoritesQueries.deleteAll() } // Insert new snapshots - db.insertFavoriteEntries(dbMangas.toList()).executeAsBlocking() + handler.await(true) { + dbMangas.toList().forEach { + eh_favoritesQueries.insertEhFavorites( + it.id, + it.title, + it.gid, + it.token, + it.category.toLong(), + ) + } + } } - fun clearSnapshots() { - db.deleteAllFavoriteEntries().executeAsBlocking() + suspend fun clearSnapshots() { + handler.await { eh_favoritesQueries.deleteAll() } } - private fun Sequence.getChangedEntries(): ChangeSet { + private suspend fun Flow.getChangedEntries(): ChangeSet { val terminated = toList() - val databaseEntries = db.getFavoriteEntries().executeAsBlocking() + val databaseEntries = handler.awaitList { eh_favoritesQueries.selectAll(favoriteEntryMapper) } val added = terminated.filter { queryListForEntry(databaseEntries, it) == null @@ -74,11 +95,11 @@ class LocalFavoritesStorage { it.category == entry.category } - private fun Sequence.loadDbCategories(): Sequence> { - val dbCategories = db.getCategories().executeAsBlocking() + private suspend fun Flow.loadDbCategories(): Flow> { + val dbCategories = getCategories.await() return filter(::validateDbManga).mapNotNull { - val category = db.getCategoriesForManga(it).executeAsBlocking() + val category = getCategories.await(it.id) dbCategories.indexOf( category.firstOrNull() @@ -87,12 +108,12 @@ class LocalFavoritesStorage { } } - private fun Sequence>.parseToFavoriteEntries() = + private fun Flow>.parseToFavoriteEntries() = filter { (_, manga) -> validateDbManga(manga) }.mapNotNull { (categoryId, manga) -> FavoriteEntry( - title = manga.originalTitle, + title = manga.ogTitle, gid = EHentaiSearchMetadata.galleryId(manga.url), token = EHentaiSearchMetadata.galleryToken(manga.url), category = categoryId, diff --git a/app/src/main/java/exh/md/handlers/ApiMangaParser.kt b/app/src/main/java/exh/md/handlers/ApiMangaParser.kt index ae3d46f4c..998273417 100644 --- a/app/src/main/java/exh/md/handlers/ApiMangaParser.kt +++ b/app/src/main/java/exh/md/handlers/ApiMangaParser.kt @@ -1,6 +1,7 @@ package exh.md.handlers -import eu.kanade.tachiyomi.data.database.DatabaseHelper +import eu.kanade.data.DatabaseHandler +import eu.kanade.domain.manga.interactor.GetMangaByUrlAndSource import eu.kanade.tachiyomi.source.model.SManga import exh.log.xLogE import exh.md.dto.ChapterDataDto @@ -12,8 +13,8 @@ import exh.md.utils.MdUtil import exh.md.utils.asMdMap import exh.metadata.metadata.MangaDexSearchMetadata import exh.metadata.metadata.base.RaisedTag -import exh.metadata.metadata.base.getFlatMetadataForManga -import exh.metadata.metadata.base.insertFlatMetadata +import exh.metadata.metadata.base.awaitFlatMetadataForManga +import exh.metadata.metadata.base.awaitInsertFlatMetadata import exh.util.capitalize import exh.util.floor import exh.util.nullIfEmpty @@ -25,7 +26,8 @@ import java.util.Locale class ApiMangaParser( private val lang: String, ) { - val db: DatabaseHelper by injectLazy() + private val handler: DatabaseHandler by injectLazy() + private val getMangaByUrlAndSource: GetMangaByUrlAndSource by injectLazy() val metaClass = MangaDexSearchMetadata::class @@ -37,23 +39,23 @@ class ApiMangaParser( }?.call() ?: error("Could not find no-args constructor for meta class: ${metaClass.qualifiedName}!") - fun parseToManga( + suspend fun parseToManga( manga: MangaInfo, sourceId: Long, input: MangaDto, simpleChapters: List, statistics: StatisticsMangaDto?, ): MangaInfo { - val mangaId = db.getManga(manga.key, sourceId).executeAsBlocking()?.id + val mangaId = getMangaByUrlAndSource.await(manga.key, sourceId)?.id val metadata = if (mangaId != null) { - val flatMetadata = db.getFlatMetadataForManga(mangaId).executeAsBlocking() + val flatMetadata = handler.awaitFlatMetadataForManga(mangaId) flatMetadata?.raise(metaClass) ?: newMetaInstance() } else newMetaInstance() parseIntoMetadata(metadata, input, simpleChapters, statistics) if (mangaId != null) { metadata.mangaId = mangaId - db.insertFlatMetadata(metadata.flatten()) + handler.awaitInsertFlatMetadata(metadata.flatten()) } return metadata.createMangaInfo(manga) diff --git a/app/src/main/java/exh/md/similar/MangaDexSimilarPager.kt b/app/src/main/java/exh/md/similar/MangaDexSimilarPager.kt index a77207112..620915ad3 100644 --- a/app/src/main/java/exh/md/similar/MangaDexSimilarPager.kt +++ b/app/src/main/java/exh/md/similar/MangaDexSimilarPager.kt @@ -1,7 +1,7 @@ package exh.md.similar -import eu.kanade.tachiyomi.data.database.models.Manga -import eu.kanade.tachiyomi.data.database.models.toMangaInfo +import eu.kanade.domain.manga.model.Manga +import eu.kanade.domain.manga.model.toMangaInfo import eu.kanade.tachiyomi.source.model.MetadataMangasPage import eu.kanade.tachiyomi.source.online.all.MangaDex import eu.kanade.tachiyomi.ui.browse.source.browse.NoResultsException diff --git a/app/src/main/java/exh/md/similar/MangaDexSimilarPresenter.kt b/app/src/main/java/exh/md/similar/MangaDexSimilarPresenter.kt index 5b685d1fc..22dc23d64 100644 --- a/app/src/main/java/exh/md/similar/MangaDexSimilarPresenter.kt +++ b/app/src/main/java/exh/md/similar/MangaDexSimilarPresenter.kt @@ -1,26 +1,30 @@ package exh.md.similar -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.model.FilterList import eu.kanade.tachiyomi.source.online.all.MangaDex import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourcePresenter import eu.kanade.tachiyomi.ui.browse.source.browse.Pager import exh.source.getMainSource -import uy.kohesive.injekt.injectLazy +import kotlinx.coroutines.runBlocking +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get /** * Presenter of [MangaDexSimilarController]. Inherit BrowseCataloguePresenter. */ -class MangaDexSimilarPresenter(val mangaId: Long, sourceId: Long) : BrowseSourcePresenter(sourceId) { +class MangaDexSimilarPresenter( + val mangaId: Long, + sourceId: Long, + private val getMangaById: GetMangaById = Injekt.get(), +) : BrowseSourcePresenter(sourceId) { var manga: Manga? = null - val db: DatabaseHelper by injectLazy() - override fun createPager(query: String, filters: FilterList): Pager { val sourceAsMangaDex = source.getMainSource() as MangaDex - this.manga = db.getManga(mangaId).executeAsBlocking() + this.manga = runBlocking { getMangaById.await(mangaId) } return MangaDexSimilarPager(manga!!, sourceAsMangaDex) } } diff --git a/app/src/main/java/exh/recs/RecommendsPager.kt b/app/src/main/java/exh/recs/RecommendsPager.kt index 9aa641fa6..dc8ad3be1 100644 --- a/app/src/main/java/exh/recs/RecommendsPager.kt +++ b/app/src/main/java/exh/recs/RecommendsPager.kt @@ -1,6 +1,6 @@ package exh.recs -import eu.kanade.tachiyomi.data.database.models.Manga +import eu.kanade.domain.manga.model.Manga import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.NetworkHelper import eu.kanade.tachiyomi.network.POST @@ -203,7 +203,7 @@ open class RecommendsPager( val recs = apiList.firstNotNullOfOrNull { (key, api) -> try { - val recs = api.getRecsBySearch(manga.originalTitle) + val recs = api.getRecsBySearch(manga.ogTitle) logcat { key.toString() + " > Results: " + recs.count() } recs } catch (e: Exception) { diff --git a/app/src/main/java/exh/recs/RecommendsPresenter.kt b/app/src/main/java/exh/recs/RecommendsPresenter.kt index 586e9eff6..924f273ca 100644 --- a/app/src/main/java/exh/recs/RecommendsPresenter.kt +++ b/app/src/main/java/exh/recs/RecommendsPresenter.kt @@ -1,23 +1,27 @@ package exh.recs -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.model.FilterList import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourcePresenter import eu.kanade.tachiyomi.ui.browse.source.browse.Pager -import uy.kohesive.injekt.injectLazy +import kotlinx.coroutines.runBlocking +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get /** * Presenter of [RecommendsController]. Inherit BrowseCataloguePresenter. */ -class RecommendsPresenter(val mangaId: Long, sourceId: Long) : BrowseSourcePresenter(sourceId) { +class RecommendsPresenter( + val mangaId: Long, + sourceId: Long, + private val getMangaById: GetMangaById = Injekt.get(), +) : BrowseSourcePresenter(sourceId) { var manga: Manga? = null - val db: DatabaseHelper by injectLazy() - override fun createPager(query: String, filters: FilterList): Pager { - this.manga = db.getManga(mangaId).executeAsBlocking() + this.manga = runBlocking { getMangaById.await(mangaId) } return RecommendsPager(manga!!) } } diff --git a/app/src/main/java/exh/ui/intercept/InterceptActivity.kt b/app/src/main/java/exh/ui/intercept/InterceptActivity.kt index b62b65295..5a80f2df4 100755 --- a/app/src/main/java/exh/ui/intercept/InterceptActivity.kt +++ b/app/src/main/java/exh/ui/intercept/InterceptActivity.kt @@ -6,9 +6,9 @@ import android.view.MenuItem import androidx.core.view.isVisible import androidx.lifecycle.lifecycleScope import com.google.android.material.dialog.MaterialAlertDialogBuilder +import eu.kanade.domain.chapter.model.Chapter +import eu.kanade.domain.manga.model.Manga import eu.kanade.tachiyomi.R -import eu.kanade.tachiyomi.data.database.models.Chapter -import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.databinding.EhActivityInterceptBinding import eu.kanade.tachiyomi.source.online.UrlImportableSource import eu.kanade.tachiyomi.ui.base.activity.BaseActivity @@ -70,7 +70,7 @@ class InterceptActivity : BaseActivity() { onBackPressed() startActivity( if (it.chapter != null) { - ReaderActivity.newIntent(this, it.manga.id!!, it.chapter.id!!) + ReaderActivity.newIntent(this, it.manga.id, it.chapter.id) } else { Intent(this, MainActivity::class.java) .setAction(MainActivity.SHORTCUT_MANGA) @@ -133,9 +133,7 @@ class InterceptActivity : BaseActivity() { val result = galleryAdder.addGallery(this@InterceptActivity, gallery, forceSource = source) status.value = when (result) { - is GalleryAddEvent.Success -> result.manga.id?.let { - InterceptResult.Success(it, result.manga, result.chapter) - } ?: InterceptResult.Failure(getString(R.string.manga_id_is_null)) + is GalleryAddEvent.Success -> InterceptResult.Success(result.manga.id, result.manga, result.chapter) is GalleryAddEvent.Fail -> InterceptResult.Failure(result.logMessage) } } diff --git a/app/src/main/java/exh/util/MangaType.kt b/app/src/main/java/exh/util/MangaType.kt index 8390b4d40..0c3dc9998 100644 --- a/app/src/main/java/exh/util/MangaType.kt +++ b/app/src/main/java/exh/util/MangaType.kt @@ -8,6 +8,7 @@ import eu.kanade.tachiyomi.ui.reader.setting.ReadingModeType import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get import java.util.Locale +import eu.kanade.domain.manga.model.Manga as DomainManga fun Manga.mangaType(context: Context): String { return context.getString( @@ -48,6 +49,30 @@ fun Manga.mangaType(sourceName: String? = Injekt.get().get(source } } +fun DomainManga.mangaType(sourceName: String? = Injekt.get().get(source)?.name): MangaType { + val currentTags = genre.orEmpty() + return when { + currentTags.any { tag -> isMangaTag(tag) } -> { + MangaType.TYPE_MANGA + } + currentTags.any { tag -> isWebtoonTag(tag) } || sourceName?.let { isWebtoonSource(it) } == true -> { + MangaType.TYPE_WEBTOON + } + currentTags.any { tag -> isComicTag(tag) } || sourceName?.let { isComicSource(it) } == true -> { + MangaType.TYPE_COMIC + } + currentTags.any { tag -> isManhuaTag(tag) } || sourceName?.let { isManhuaSource(it) } == true -> { + MangaType.TYPE_MANHUA + } + currentTags.any { tag -> isManhwaTag(tag) } || sourceName?.let { isManhwaSource(it) } == true -> { + MangaType.TYPE_MANHWA + } + else -> { + MangaType.TYPE_MANGA + } + } +} + /** * The type the reader should use. Different from manga type as certain manga has different * read types diff --git a/app/src/main/java/exh/util/SearchOverride.kt b/app/src/main/java/exh/util/SearchOverride.kt index c85a9f70a..2b4573152 100644 --- a/app/src/main/java/exh/util/SearchOverride.kt +++ b/app/src/main/java/exh/util/SearchOverride.kt @@ -24,7 +24,7 @@ fun UrlImportableSource.urlImportFetchSearchManga(context: Context, query: Strin .map { res -> MangasPage( if (res is GalleryAddEvent.Success) { - listOf(res.manga) + listOf(res.manga.toSManga()) } else { emptyList() }, diff --git a/app/src/main/sqldelight/data/mangas.sq b/app/src/main/sqldelight/data/mangas.sq index 74f632da1..36337f980 100644 --- a/app/src/main/sqldelight/data/mangas.sq +++ b/app/src/main/sqldelight/data/mangas.sq @@ -30,6 +30,9 @@ 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); +insertEmpty: +INSERT INTO mangas (source, url, title, artist, author, description, genre, status, thumbnail_url, favorite, last_update, next_update, initialized, viewer, chapter_flags, cover_last_modified, date_added, filtered_scanlators) VALUES (?, ?, ?, NULL, NULL, NULL, NULL, 0, NULL, 0, NULL, NULL, 0, 0, 0, 0, 0, NULL); + getMangaById: SELECT * FROM mangas @@ -186,3 +189,14 @@ WHERE _id = :mangaId; selectLastInsertedRowId: SELECT last_insert_rowid(); + +getEhMangaWithMetadata: +SELECT mangas.* FROM mangas +INNER JOIN search_metadata + ON mangas._id = search_metadata.manga_id +WHERE mangas.favorite = 1 AND (mangas.source = :eh OR mangas.source = :exh); + +selectLastInsertRow: +SELECT * +FROM mangas +WHERE _id = last_insert_rowid();