Add favorite entry alternative handling, allowing parennt versions to take priority for favorites sync

This commit is contained in:
Jobobby04 2023-05-12 22:50:23 -04:00
parent e9a3463455
commit 282a0c4e16
12 changed files with 333 additions and 34 deletions

View File

@ -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<SavedSearchRepository> { SavedSearchRepositoryImpl(get()) }
addFactory { GetSavedSearchById(get()) }

View File

@ -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<ChapterChain>,
): 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<Chapter>,
chainsAsChapters: List<Chapter>,
chainsAsHistory: List<History>,

View File

@ -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<FavoriteEntry>, 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<Manga>.loadDbCategories(): Flow<Pair<Int, Manga>> {
val dbCategories = getCategories.await()

View File

@ -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<GetFavorites>()
coEvery { getFavorites.await() } returns favorites
val getCategories = mockk<GetCategories>()
coEvery { getCategories.await() } returns categories
coEvery { getCategories.await(any()) } returns categories
val getFavoriteEntries = mockk<GetFavoriteEntries>()
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
},
)
}
}
}
}

View File

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

View File

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

View File

@ -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;
addAlternative:
INSERT INTO eh_favorites_alternatives (gid, token, otherGid, otherToken)
VALUES (:gid, :token, :otherGid, :otherToken);

View File

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

View File

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

View File

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

View File

@ -0,0 +1,8 @@
package tachiyomi.domain.manga.model
data class FavoriteEntryAlternative(
val otherGid: String,
val otherToken: String,
val gid: String,
val token: String,
)

View File

@ -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<FavoriteEntry>)
suspend fun selectAll(): List<FavoriteEntry>
suspend fun addAlternative(favoriteEntryAlternative: FavoriteEntryAlternative)
}