Use SQLDelight for most SY specific things

This commit is contained in:
Jobobby04 2022-07-02 23:52:03 -04:00
parent 3cf4c3128f
commit 664f9b1484
21 changed files with 480 additions and 393 deletions

View File

@ -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(),
)
}

View File

@ -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 <--
}

View File

@ -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<MetadataSource<*, *>>()
if (source != null) {
handler.getFlatMetadataForManga(manga.id)?.let { flatMetadata ->
handler.awaitFlatMetadataForManga(manga.id)?.let { flatMetadata ->
mangaObject.flatMetadata = BackupFlatMetadata.copyFrom(flatMetadata)
}
}

View File

@ -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<M : RaisedSearchMetadata, I> : 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<M : RaisedSearchMetadata, I> : 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<M : RaisedSearchMetadata, I> : 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<M : RaisedSearchMetadata, I> : 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
}

View File

@ -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) { _, _ ->

View File

@ -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,
),
)

View File

@ -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<String>, Set<Long>> = run {
val preferences = Injekt.get<PreferencesHelper>()
preferences.enabledLanguages().get() to preferences.disabledSources().get().map { it.toLong() }.toSet()
private val filters: Pair<Set<String>, Set<Long>> = Injekt.get<PreferencesHelper>().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 {

View File

@ -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<EHentaiSearchMetadata>() ?: return@forEach
val meta = handler.awaitFlatMetadataForManga(manga.id!!)?.raise<EHentaiSearchMetadata>() ?: 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<EHentaiSearchMetadata>() ?: return@forEach
galleries += "Aged: ${meta.aged}\t Title: ${manga.title}"
allManga.map { manga ->
val meta = handler.awaitFlatMetadataForManga(manga.id)?.raise<EHentaiSearchMetadata>() ?: 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<EHentaiSearchMetadata>() ?: 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<EHentaiSearchMetadata>()
?: 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()})"

View File

@ -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<MangaUpdate>()
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<History>,
val urlHistory: List<Pair<String, History>>,
val historyToDelete: List<Long>,
)
private fun getHistory(
newChapters: List<Chapter>,
fun getHistory(
currentChapters: List<Chapter>,
chainsAsChapters: List<Chapter>,
chainsAsHistory: List<History>,
): 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<HistoryUpdate>, List<Long>> {
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<ChapterChain>,
chainsAsChapters: List<Chapter>,
): Pair<List<Chapter>, Boolean> {
): Triple<List<ChapterUpdate>, List<Chapter>, 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<ChapterUpdate>()
val newChapters = mutableListOf<Chapter>()
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)
}
}
}

View File

@ -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<EHentaiSearchMetadata>()
@ -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<EHentaiSearchMetadata>()
val meta = handler.awaitFlatMetadataForManga(manga.id)?.raise<EHentaiSearchMetadata>()
if (meta != null) {
// Age dead galleries
logger.d("Aged %s - notfound", manga.id)

View File

@ -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<SourceManager>().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<Long>(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<String>) {
val localCategories = db.getCategories().executeAsBlocking()
private suspend fun applyRemoteCategories(categories: List<String>) {
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<Pair<MangaCategory, Manga>>()
val categories = db.getCategories().executeAsBlocking()
val insertedMangaCategories = mutableListOf<Pair<Long, Manga>>()
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 {

View File

@ -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<EHentai.ParsedManga>) = entries
.asSequence()
suspend fun getChangedRemoteEntries(entries: List<EHentai.ParsedManga>) = 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<FavoriteEntry>.getChangedEntries(): ChangeSet {
private suspend fun Flow<FavoriteEntry>.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<Manga>.loadDbCategories(): Sequence<Pair<Int, Manga>> {
val dbCategories = db.getCategories().executeAsBlocking()
private suspend fun Flow<Manga>.loadDbCategories(): Flow<Pair<Int, Manga>> {
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<Pair<Int, Manga>>.parseToFavoriteEntries() =
private fun Flow<Pair<Int, Manga>>.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,

View File

@ -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<String>,
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)

View File

@ -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

View File

@ -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)
}
}

View File

@ -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) {

View File

@ -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!!)
}
}

View File

@ -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)
}
}

View File

@ -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<SourceManager>().get(source
}
}
fun DomainManga.mangaType(sourceName: String? = Injekt.get<SourceManager>().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

View File

@ -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()
},

View File

@ -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();