diff --git a/app/src/main/java/eu/kanade/domain/SYDomainModule.kt b/app/src/main/java/eu/kanade/domain/SYDomainModule.kt index 394641fef..9b81af591 100644 --- a/app/src/main/java/eu/kanade/domain/SYDomainModule.kt +++ b/app/src/main/java/eu/kanade/domain/SYDomainModule.kt @@ -52,6 +52,7 @@ import tachiyomi.domain.manga.interactor.InsertFlatMetadata import tachiyomi.domain.manga.interactor.InsertMergedReference import tachiyomi.domain.manga.interactor.SetCustomMangaInfo import tachiyomi.domain.manga.interactor.SetMangaFilteredScanlators +import tachiyomi.domain.manga.interactor.InsertFavoriteEntryAlternative import tachiyomi.domain.manga.interactor.UpdateMergedSettings import tachiyomi.domain.manga.repository.CustomMangaRepository import tachiyomi.domain.manga.repository.FavoritesEntryRepository @@ -140,6 +141,7 @@ class SYDomainModule : InjektModule { addFactory { GetFavoriteEntries(get()) } addFactory { InsertFavoriteEntries(get()) } addFactory { DeleteFavoriteEntries(get()) } + addFactory { InsertFavoriteEntryAlternative(get()) } addSingletonFactory { SavedSearchRepositoryImpl(get()) } addFactory { GetSavedSearchById(get()) } diff --git a/app/src/main/java/exh/eh/EHentaiUpdateHelper.kt b/app/src/main/java/exh/eh/EHentaiUpdateHelper.kt index aed45f1df..00c1ff9ea 100644 --- a/app/src/main/java/exh/eh/EHentaiUpdateHelper.kt +++ b/app/src/main/java/exh/eh/EHentaiUpdateHelper.kt @@ -2,6 +2,7 @@ package exh.eh import android.content.Context import eu.kanade.domain.manga.interactor.UpdateManga +import exh.metadata.metadata.EHentaiSearchMetadata import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.async import kotlinx.coroutines.coroutineScope @@ -18,6 +19,8 @@ import tachiyomi.domain.history.interactor.UpsertHistory import tachiyomi.domain.history.model.History import tachiyomi.domain.history.model.HistoryUpdate import tachiyomi.domain.manga.interactor.GetManga +import tachiyomi.domain.manga.interactor.InsertFavoriteEntryAlternative +import tachiyomi.domain.manga.model.FavoriteEntryAlternative import tachiyomi.domain.manga.model.Manga import tachiyomi.domain.manga.model.MangaUpdate import uy.kohesive.injekt.injectLazy @@ -41,6 +44,7 @@ class EHentaiUpdateHelper(context: Context) { private val upsertHistory: UpsertHistory by injectLazy() private val removeHistory: RemoveHistory by injectLazy() private val getHistoryByMangaId: GetHistoryByMangaId by injectLazy() + private val insertFavoriteEntryAlternative: InsertFavoriteEntryAlternative by injectLazy() /** * @param chapters Cannot be an empty list! @@ -123,6 +127,12 @@ class EHentaiUpdateHelper(context: Context) { upsertHistory.await(it) } + // Update favorites entry database + val favoriteEntryUpdate = getFavoriteEntryAlternative(accepted, toDiscard) + if (favoriteEntryUpdate != null) { + insertFavoriteEntryAlternative.await(favoriteEntryUpdate) + } + // Copy categories from all chains to accepted manga val newCategories = rootsToMutate.flatMap { chapterChain -> @@ -145,7 +155,24 @@ class EHentaiUpdateHelper(context: Context) { } } - fun getHistory( + private fun getFavoriteEntryAlternative( + accepted: ChapterChain, + toDiscard: List, + ): FavoriteEntryAlternative? { + val favorite = toDiscard.find { it.manga.favorite } ?: return null + + val gid = EHentaiSearchMetadata.galleryId(accepted.manga.url) + val token = EHentaiSearchMetadata.galleryToken(accepted.manga.url) + + return FavoriteEntryAlternative( + otherGid = gid, + otherToken = token, + gid = EHentaiSearchMetadata.galleryId(favorite.manga.url), + token = EHentaiSearchMetadata.galleryToken(favorite.manga.url), + ) + } + + private fun getHistory( currentChapters: List, chainsAsChapters: List, chainsAsHistory: List, diff --git a/app/src/main/java/exh/favorites/LocalFavoritesStorage.kt b/app/src/main/java/exh/favorites/LocalFavoritesStorage.kt index 02d522cd6..59fc816db 100644 --- a/app/src/main/java/exh/favorites/LocalFavoritesStorage.kt +++ b/app/src/main/java/exh/favorites/LocalFavoritesStorage.kt @@ -19,14 +19,16 @@ import tachiyomi.domain.manga.interactor.GetFavorites import tachiyomi.domain.manga.interactor.InsertFavoriteEntries import tachiyomi.domain.manga.model.FavoriteEntry import tachiyomi.domain.manga.model.Manga -import uy.kohesive.injekt.injectLazy +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get -class LocalFavoritesStorage { - private val getFavorites: GetFavorites by injectLazy() - private val getCategories: GetCategories by injectLazy() - private val deleteFavoriteEntries: DeleteFavoriteEntries by injectLazy() - private val getFavoriteEntries: GetFavoriteEntries by injectLazy() - private val insertFavoriteEntries: InsertFavoriteEntries by injectLazy() +class LocalFavoritesStorage( + private val getFavorites: GetFavorites = Injekt.get(), + private val getCategories: GetCategories = Injekt.get(), + private val deleteFavoriteEntries: DeleteFavoriteEntries = Injekt.get(), + private val getFavoriteEntries: GetFavoriteEntries = Injekt.get(), + private val insertFavoriteEntries: InsertFavoriteEntries = Injekt.get(), +) { suspend fun getChangedDbEntries() = getFavorites.await() .asFlow() @@ -67,27 +69,29 @@ class LocalFavoritesStorage { val databaseEntries = getFavoriteEntries.await() - val added = terminated.filter { - queryListForEntry(databaseEntries, it) == null - } + val added = terminated.groupBy { it.gid to it.token } + .filter { (_, values) -> + values.all { queryListForEntry(databaseEntries, it) == null } + } + .map { it.value.first() } val removed = databaseEntries - .filter { - queryListForEntry(terminated, it) == null - } /*.map { - todo see what this does - realm.copyFromRealm(it) - }*/ + .groupBy { it.gid to it.token } + .filter { (_, values) -> + values.all { queryListForEntry(terminated, it) == null } + } + .map { it.value.first() } return ChangeSet(added, removed) } + private fun FavoriteEntry.urlEquals(other: FavoriteEntry) = (gid == other.gid && token == other.token) || + (otherGid != null && otherToken != null && (otherGid == other.gid && otherToken == other.token)) || + (other.otherGid != null && other.otherToken != null && (gid == other.otherGid && token == other.otherToken)) || + (otherGid != null && otherToken != null && other.otherGid != null && other.otherToken != null && otherGid == other.otherGid && otherToken == other.otherToken) + private fun queryListForEntry(list: List, entry: FavoriteEntry) = - list.find { - it.gid == entry.gid && - it.token == entry.token && - it.category == entry.category - } + list.find { it.urlEquals(entry) && it.category == entry.category } private suspend fun Flow.loadDbCategories(): Flow> { val dbCategories = getCategories.await() diff --git a/app/src/test/kotlin/Tester.kt b/app/src/test/kotlin/Tester.kt new file mode 100644 index 000000000..023053a73 --- /dev/null +++ b/app/src/test/kotlin/Tester.kt @@ -0,0 +1,178 @@ + +import eu.kanade.tachiyomi.data.backup.models.BackupSerializer +import eu.kanade.tachiyomi.source.model.SManga +import eu.kanade.tachiyomi.source.online.all.EHentai +import exh.favorites.LocalFavoritesStorage +import exh.metadata.metadata.EHentaiSearchMetadata +import exh.source.EXH_SOURCE_ID +import io.kotest.inspectors.shouldForAll +import io.mockk.coEvery +import io.mockk.mockk +import kotlinx.coroutines.runBlocking +import kotlinx.serialization.protobuf.ProtoBuf +import okio.buffer +import okio.gzip +import okio.sink +import okio.source +import org.junit.jupiter.api.BeforeAll +import org.junit.jupiter.api.Disabled +import org.junit.jupiter.api.Test +import tachiyomi.domain.category.interactor.GetCategories +import tachiyomi.domain.category.model.Category +import tachiyomi.domain.manga.interactor.GetCustomMangaInfo +import tachiyomi.domain.manga.interactor.GetFavoriteEntries +import tachiyomi.domain.manga.interactor.GetFavorites +import tachiyomi.domain.manga.model.CustomMangaInfo +import tachiyomi.domain.manga.model.FavoriteEntry +import tachiyomi.domain.manga.model.Manga +import tachiyomi.domain.manga.repository.CustomMangaRepository +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.addSingletonFactory +import java.io.File + +class Tester { + + @Disabled + @Test + fun stripBackup() { + val bytes = File("D:\\Downloads\\pacthiyomi_2023-05-08_13-30.proto (1).gz") + .inputStream().source().buffer() + .gzip().buffer() + .readByteArray() + val backup = ProtoBuf.decodeFromByteArray(BackupSerializer, bytes) + val newBytes = ProtoBuf.encodeToByteArray( + BackupSerializer, + backup.copy( + backupManga = backup.backupManga.filter { it.favorite }, + ), + ) + File("D:\\Downloads\\pacthiyomi_2023-05-08_13-30 (2).proto.gz").outputStream().sink().gzip().buffer().use { + it.write(newBytes) + } + } + + @Test + fun localFavoritesStorageTester(): Unit = runBlocking { + val favorites = listOf( + Manga.create().copy( + id = 1, + favorite = true, + source = EXH_SOURCE_ID, + url = "/g/gid/token", + ), + // an alias for gid2/token2 + Manga.create().copy( + id = 3, + favorite = true, + source = EXH_SOURCE_ID, + url = "/g/gid3/token3", + ), + // add this one to library + Manga.create().copy( + id = 3, + favorite = true, + source = EXH_SOURCE_ID, + url = "/g/gid4/token4", + ), + ) + val categories = listOf( + Category( + id = 1, + name = "a", + order = 1, + flags = 0, + ), + ) + val favoriteEntries = listOf( + FavoriteEntry( + gid = "gid", + token = "token", + title = "a", + category = 0, + ), + FavoriteEntry( + gid = "gid2", + token = "token2", + title = "a", + category = 0, + ), + // the alias for gid2/token2 + FavoriteEntry( + gid = "gid2", + token = "token2", + title = "a", + category = 0, + otherGid = "gid3", + otherToken = "token3", + ), + // removed on remote and local + FavoriteEntry( + gid = "gid6", + token = "token6", + title = "a", + category = 0, + ), + ) + + val getFavorites = mockk() + coEvery { getFavorites.await() } returns favorites + + val getCategories = mockk() + coEvery { getCategories.await() } returns categories + coEvery { getCategories.await(any()) } returns categories + + val getFavoriteEntries = mockk() + coEvery { getFavoriteEntries.await() } returns favoriteEntries + + val storage = LocalFavoritesStorage( + getFavorites = getFavorites, + getCategories = getCategories, + deleteFavoriteEntries = mockk(), + getFavoriteEntries = getFavoriteEntries, + insertFavoriteEntries = mockk(), + ) + + val (added, removed) = storage.getChangedDbEntries() + added.shouldForAll { it.gid == "gid4" && it.token == "token4" } + removed.shouldForAll { it.gid == "gid6" && it.token == "token6" } + + val (remoteAdded, remoteRemoved) = storage.getChangedRemoteEntries( + listOf( + EHentai.ParsedManga( + 0, + SManga("/g/gid/token", "a"), + EHentaiSearchMetadata(), + ), + EHentai.ParsedManga( + 0, + SManga("/g/gid2/token2", "a"), + EHentaiSearchMetadata(), + ), + // added on remote + EHentai.ParsedManga( + 0, + SManga("/g/gid5/token5", "a"), + EHentaiSearchMetadata(), + ), + ), + ) + + remoteAdded.shouldForAll { it.gid == "gid5" && it.token == "token5" } + remoteRemoved.shouldForAll { it.gid == "gid6" && it.token == "token6" } + } + + companion object { + @JvmStatic + @BeforeAll + fun before() { + Injekt.addSingletonFactory { + GetCustomMangaInfo( + object : CustomMangaRepository { + override fun get(mangaId: Long) = null + override fun set(mangaInfo: CustomMangaInfo) = Unit + }, + ) + } + } + } +} diff --git a/data/src/main/java/tachiyomi/data/manga/FavoriteEntry.kt b/data/src/main/java/tachiyomi/data/manga/FavoriteEntry.kt index 6357f642d..e1c349afe 100644 --- a/data/src/main/java/tachiyomi/data/manga/FavoriteEntry.kt +++ b/data/src/main/java/tachiyomi/data/manga/FavoriteEntry.kt @@ -2,13 +2,14 @@ package tachiyomi.data.manga import tachiyomi.domain.manga.model.FavoriteEntry -val favoriteEntryMapper: (Long, String, String, String, Long) -> FavoriteEntry = - { id, title, gid, token, category -> +val favoriteEntryMapper: (String, String, String, Long, String?, String?) -> FavoriteEntry = + { gid, token, title, category, otherGid, otherToken -> FavoriteEntry( - id = id, - title = title, gid = gid, token = token, + title = title, category = category.toInt(), + otherGid = otherGid, + otherToken = otherToken, ) } diff --git a/data/src/main/java/tachiyomi/data/manga/FavoritesEntryRepositoryImpl.kt b/data/src/main/java/tachiyomi/data/manga/FavoritesEntryRepositoryImpl.kt index 3e81a6c89..b50da06ae 100644 --- a/data/src/main/java/tachiyomi/data/manga/FavoritesEntryRepositoryImpl.kt +++ b/data/src/main/java/tachiyomi/data/manga/FavoritesEntryRepositoryImpl.kt @@ -2,6 +2,7 @@ package tachiyomi.data.manga import tachiyomi.data.DatabaseHandler import tachiyomi.domain.manga.model.FavoriteEntry +import tachiyomi.domain.manga.model.FavoriteEntryAlternative import tachiyomi.domain.manga.repository.FavoritesEntryRepository class FavoritesEntryRepositoryImpl( @@ -14,7 +15,12 @@ class FavoritesEntryRepositoryImpl( override suspend fun insertAll(favoriteEntries: List) { handler.await(true) { favoriteEntries.forEach { - eh_favoritesQueries.insertEhFavorites(it.id, it.title, it.gid, it.token, it.category.toLong()) + eh_favoritesQueries.insertEhFavorites( + title = it.title, + gid = it.gid, + token = it.token, + category = it.category.toLong(), + ) } } } @@ -22,4 +28,15 @@ class FavoritesEntryRepositoryImpl( override suspend fun selectAll(): List { return handler.awaitList { eh_favoritesQueries.selectAll(favoriteEntryMapper) } } + + override suspend fun addAlternative(favoriteEntryAlternative: FavoriteEntryAlternative) { + handler.await { + eh_favoritesQueries.addAlternative( + otherGid = favoriteEntryAlternative.otherGid, + otherToken = favoriteEntryAlternative.otherToken, + gid = favoriteEntryAlternative.gid, + token = favoriteEntryAlternative.token, + ) + } + } } diff --git a/data/src/main/sqldelight/tachiyomi/data/eh_favorites.sq b/data/src/main/sqldelight/tachiyomi/data/eh_favorites.sq index e3b7cace6..f1d2716ed 100644 --- a/data/src/main/sqldelight/tachiyomi/data/eh_favorites.sq +++ b/data/src/main/sqldelight/tachiyomi/data/eh_favorites.sq @@ -1,16 +1,34 @@ CREATE TABLE eh_favorites ( - _id INTEGER NOT NULL PRIMARY KEY, - title TEXT NOT NULL, gid TEXT NOT NULL, token TEXT NOT NULL, - category INTEGER NOT NULL + title TEXT NOT NULL, + category INTEGER NOT NULL, + PRIMARY KEY (gid, token) ); +CREATE TABLE eh_favorites_alternatives ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + gid TEXT NOT NULL, + token TEXT NOT NULL, + otherGid TEXT NOT NULL, + otherToken TEXT NOT NULL, + FOREIGN KEY (gid, token) REFERENCES eh_favorites(gid, token) +); + +CREATE INDEX eh_favorites_alternatives_gid_token_index ON eh_favorites_alternatives(gid, token); +CREATE INDEX eh_favorites_alternatives_other_gid_token_index ON eh_favorites_alternatives(otherGid, otherToken); + selectAll: -SELECT * FROM eh_favorites; +SELECT f.gid, f.token, f.title, f.category, a.otherGid, a.otherToken +FROM eh_favorites AS f +LEFT JOIN eh_favorites_alternatives AS a ON f.gid = a.gid AND f.token = a.token; insertEhFavorites: -INSERT INTO eh_favorites (_id, title, gid, token, category) VALUES (?, ?, ?, ?, ?); +INSERT INTO eh_favorites (title, gid, token, category) VALUES (?, ?, ?, ?); deleteAll: -DELETE FROM eh_favorites; \ No newline at end of file +DELETE FROM eh_favorites; + +addAlternative: +INSERT INTO eh_favorites_alternatives (gid, token, otherGid, otherToken) +VALUES (:gid, :token, :otherGid, :otherToken); \ No newline at end of file diff --git a/data/src/main/sqldelight/tachiyomi/migrations/26.sqm b/data/src/main/sqldelight/tachiyomi/migrations/26.sqm new file mode 100644 index 000000000..862798886 --- /dev/null +++ b/data/src/main/sqldelight/tachiyomi/migrations/26.sqm @@ -0,0 +1,25 @@ +ALTER TABLE eh_favorites RENAME TO eh_favorites_temp; +CREATE TABLE eh_favorites ( + gid TEXT NOT NULL, + token TEXT NOT NULL, + title TEXT NOT NULL, + category INTEGER NOT NULL, + PRIMARY KEY (gid, token) +); +INSERT INTO eh_favorites +SELECT gid, token, title, category +FROM eh_favorites_temp; + +DROP TABLE IF EXISTS eh_favorites_temp; + +CREATE TABLE eh_favorites_alternatives ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + gid TEXT NOT NULL, + token TEXT NOT NULL, + otherGid TEXT NOT NULL, + otherToken TEXT NOT NULL, + FOREIGN KEY (gid, token) REFERENCES eh_favorites(gid, token) +); + +CREATE INDEX eh_favorites_alternatives_gid_token_index ON eh_favorites_alternatives(gid, token); +CREATE INDEX eh_favorites_alternatives_other_gid_token_index ON eh_favorites_alternatives(otherGid, otherToken); \ No newline at end of file diff --git a/domain/src/main/java/tachiyomi/domain/manga/interactor/InsertFavoriteEntryAlternative.kt b/domain/src/main/java/tachiyomi/domain/manga/interactor/InsertFavoriteEntryAlternative.kt new file mode 100644 index 000000000..ee035cb30 --- /dev/null +++ b/domain/src/main/java/tachiyomi/domain/manga/interactor/InsertFavoriteEntryAlternative.kt @@ -0,0 +1,13 @@ +package tachiyomi.domain.manga.interactor + +import tachiyomi.domain.manga.model.FavoriteEntryAlternative +import tachiyomi.domain.manga.repository.FavoritesEntryRepository + +class InsertFavoriteEntryAlternative( + private val favoriteEntryRepository: FavoritesEntryRepository, +) { + + suspend fun await(entry: FavoriteEntryAlternative) { + return favoriteEntryRepository.addAlternative(entry) + } +} diff --git a/domain/src/main/java/tachiyomi/domain/manga/model/FavoriteEntry.kt b/domain/src/main/java/tachiyomi/domain/manga/model/FavoriteEntry.kt index 21f9d086f..865e6e909 100644 --- a/domain/src/main/java/tachiyomi/domain/manga/model/FavoriteEntry.kt +++ b/domain/src/main/java/tachiyomi/domain/manga/model/FavoriteEntry.kt @@ -3,7 +3,6 @@ package tachiyomi.domain.manga.model import exh.metadata.metadata.EHentaiSearchMetadata data class FavoriteEntry( - val id: Long? = null, val title: String, @@ -11,6 +10,10 @@ data class FavoriteEntry( val token: String, + val otherGid: String? = null, + + val otherToken: String? = null, + val category: Int = -1, ) { fun getUrl() = EHentaiSearchMetadata.idAndTokenToUrl(gid, token) diff --git a/domain/src/main/java/tachiyomi/domain/manga/model/FavoriteEntryAlternative.kt b/domain/src/main/java/tachiyomi/domain/manga/model/FavoriteEntryAlternative.kt new file mode 100644 index 000000000..8f0166119 --- /dev/null +++ b/domain/src/main/java/tachiyomi/domain/manga/model/FavoriteEntryAlternative.kt @@ -0,0 +1,8 @@ +package tachiyomi.domain.manga.model + +data class FavoriteEntryAlternative( + val otherGid: String, + val otherToken: String, + val gid: String, + val token: String, +) diff --git a/domain/src/main/java/tachiyomi/domain/manga/repository/FavoritesEntryRepository.kt b/domain/src/main/java/tachiyomi/domain/manga/repository/FavoritesEntryRepository.kt index 66bfe8dcb..62e66db57 100644 --- a/domain/src/main/java/tachiyomi/domain/manga/repository/FavoritesEntryRepository.kt +++ b/domain/src/main/java/tachiyomi/domain/manga/repository/FavoritesEntryRepository.kt @@ -1,6 +1,7 @@ package tachiyomi.domain.manga.repository import tachiyomi.domain.manga.model.FavoriteEntry +import tachiyomi.domain.manga.model.FavoriteEntryAlternative interface FavoritesEntryRepository { suspend fun deleteAll() @@ -8,4 +9,6 @@ interface FavoritesEntryRepository { suspend fun insertAll(favoriteEntries: List) suspend fun selectAll(): List + + suspend fun addAlternative(favoriteEntryAlternative: FavoriteEntryAlternative) }