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.InsertMergedReference
import tachiyomi.domain.manga.interactor.SetCustomMangaInfo import tachiyomi.domain.manga.interactor.SetCustomMangaInfo
import tachiyomi.domain.manga.interactor.SetMangaFilteredScanlators import tachiyomi.domain.manga.interactor.SetMangaFilteredScanlators
import tachiyomi.domain.manga.interactor.InsertFavoriteEntryAlternative
import tachiyomi.domain.manga.interactor.UpdateMergedSettings import tachiyomi.domain.manga.interactor.UpdateMergedSettings
import tachiyomi.domain.manga.repository.CustomMangaRepository import tachiyomi.domain.manga.repository.CustomMangaRepository
import tachiyomi.domain.manga.repository.FavoritesEntryRepository import tachiyomi.domain.manga.repository.FavoritesEntryRepository
@ -140,6 +141,7 @@ class SYDomainModule : InjektModule {
addFactory { GetFavoriteEntries(get()) } addFactory { GetFavoriteEntries(get()) }
addFactory { InsertFavoriteEntries(get()) } addFactory { InsertFavoriteEntries(get()) }
addFactory { DeleteFavoriteEntries(get()) } addFactory { DeleteFavoriteEntries(get()) }
addFactory { InsertFavoriteEntryAlternative(get()) }
addSingletonFactory<SavedSearchRepository> { SavedSearchRepositoryImpl(get()) } addSingletonFactory<SavedSearchRepository> { SavedSearchRepositoryImpl(get()) }
addFactory { GetSavedSearchById(get()) } addFactory { GetSavedSearchById(get()) }

View File

@ -2,6 +2,7 @@ package exh.eh
import android.content.Context import android.content.Context
import eu.kanade.domain.manga.interactor.UpdateManga import eu.kanade.domain.manga.interactor.UpdateManga
import exh.metadata.metadata.EHentaiSearchMetadata
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async import kotlinx.coroutines.async
import kotlinx.coroutines.coroutineScope 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.History
import tachiyomi.domain.history.model.HistoryUpdate import tachiyomi.domain.history.model.HistoryUpdate
import tachiyomi.domain.manga.interactor.GetManga 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.Manga
import tachiyomi.domain.manga.model.MangaUpdate import tachiyomi.domain.manga.model.MangaUpdate
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
@ -41,6 +44,7 @@ class EHentaiUpdateHelper(context: Context) {
private val upsertHistory: UpsertHistory by injectLazy() private val upsertHistory: UpsertHistory by injectLazy()
private val removeHistory: RemoveHistory by injectLazy() private val removeHistory: RemoveHistory by injectLazy()
private val getHistoryByMangaId: GetHistoryByMangaId by injectLazy() private val getHistoryByMangaId: GetHistoryByMangaId by injectLazy()
private val insertFavoriteEntryAlternative: InsertFavoriteEntryAlternative by injectLazy()
/** /**
* @param chapters Cannot be an empty list! * @param chapters Cannot be an empty list!
@ -123,6 +127,12 @@ class EHentaiUpdateHelper(context: Context) {
upsertHistory.await(it) 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 // Copy categories from all chains to accepted manga
val newCategories = rootsToMutate.flatMap { chapterChain -> 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>, currentChapters: List<Chapter>,
chainsAsChapters: List<Chapter>, chainsAsChapters: List<Chapter>,
chainsAsHistory: List<History>, 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.interactor.InsertFavoriteEntries
import tachiyomi.domain.manga.model.FavoriteEntry import tachiyomi.domain.manga.model.FavoriteEntry
import tachiyomi.domain.manga.model.Manga import tachiyomi.domain.manga.model.Manga
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
class LocalFavoritesStorage { class LocalFavoritesStorage(
private val getFavorites: GetFavorites by injectLazy() private val getFavorites: GetFavorites = Injekt.get(),
private val getCategories: GetCategories by injectLazy() private val getCategories: GetCategories = Injekt.get(),
private val deleteFavoriteEntries: DeleteFavoriteEntries by injectLazy() private val deleteFavoriteEntries: DeleteFavoriteEntries = Injekt.get(),
private val getFavoriteEntries: GetFavoriteEntries by injectLazy() private val getFavoriteEntries: GetFavoriteEntries = Injekt.get(),
private val insertFavoriteEntries: InsertFavoriteEntries by injectLazy() private val insertFavoriteEntries: InsertFavoriteEntries = Injekt.get(),
) {
suspend fun getChangedDbEntries() = getFavorites.await() suspend fun getChangedDbEntries() = getFavorites.await()
.asFlow() .asFlow()
@ -67,27 +69,29 @@ class LocalFavoritesStorage {
val databaseEntries = getFavoriteEntries.await() val databaseEntries = getFavoriteEntries.await()
val added = terminated.filter { val added = terminated.groupBy { it.gid to it.token }
queryListForEntry(databaseEntries, it) == null .filter { (_, values) ->
values.all { queryListForEntry(databaseEntries, it) == null }
} }
.map { it.value.first() }
val removed = databaseEntries val removed = databaseEntries
.filter { .groupBy { it.gid to it.token }
queryListForEntry(terminated, it) == null .filter { (_, values) ->
} /*.map { values.all { queryListForEntry(terminated, it) == null }
todo see what this does }
realm.copyFromRealm(it) .map { it.value.first() }
}*/
return ChangeSet(added, removed) 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) = private fun queryListForEntry(list: List<FavoriteEntry>, entry: FavoriteEntry) =
list.find { list.find { it.urlEquals(entry) && it.category == entry.category }
it.gid == entry.gid &&
it.token == entry.token &&
it.category == entry.category
}
private suspend fun Flow<Manga>.loadDbCategories(): Flow<Pair<Int, Manga>> { private suspend fun Flow<Manga>.loadDbCategories(): Flow<Pair<Int, Manga>> {
val dbCategories = getCategories.await() 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 import tachiyomi.domain.manga.model.FavoriteEntry
val favoriteEntryMapper: (Long, String, String, String, Long) -> FavoriteEntry = val favoriteEntryMapper: (String, String, String, Long, String?, String?) -> FavoriteEntry =
{ id, title, gid, token, category -> { gid, token, title, category, otherGid, otherToken ->
FavoriteEntry( FavoriteEntry(
id = id,
title = title,
gid = gid, gid = gid,
token = token, token = token,
title = title,
category = category.toInt(), category = category.toInt(),
otherGid = otherGid,
otherToken = otherToken,
) )
} }

View File

@ -2,6 +2,7 @@ package tachiyomi.data.manga
import tachiyomi.data.DatabaseHandler import tachiyomi.data.DatabaseHandler
import tachiyomi.domain.manga.model.FavoriteEntry import tachiyomi.domain.manga.model.FavoriteEntry
import tachiyomi.domain.manga.model.FavoriteEntryAlternative
import tachiyomi.domain.manga.repository.FavoritesEntryRepository import tachiyomi.domain.manga.repository.FavoritesEntryRepository
class FavoritesEntryRepositoryImpl( class FavoritesEntryRepositoryImpl(
@ -14,7 +15,12 @@ class FavoritesEntryRepositoryImpl(
override suspend fun insertAll(favoriteEntries: List<FavoriteEntry>) { override suspend fun insertAll(favoriteEntries: List<FavoriteEntry>) {
handler.await(true) { handler.await(true) {
favoriteEntries.forEach { 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> { override suspend fun selectAll(): List<FavoriteEntry> {
return handler.awaitList { eh_favoritesQueries.selectAll(favoriteEntryMapper) } 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 ( CREATE TABLE eh_favorites (
_id INTEGER NOT NULL PRIMARY KEY,
title TEXT NOT NULL,
gid TEXT NOT NULL, gid TEXT NOT NULL,
token 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: 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: insertEhFavorites:
INSERT INTO eh_favorites (_id, title, gid, token, category) VALUES (?, ?, ?, ?, ?); INSERT INTO eh_favorites (title, gid, token, category) VALUES (?, ?, ?, ?);
deleteAll: deleteAll:
DELETE FROM eh_favorites; 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 import exh.metadata.metadata.EHentaiSearchMetadata
data class FavoriteEntry( data class FavoriteEntry(
val id: Long? = null,
val title: String, val title: String,
@ -11,6 +10,10 @@ data class FavoriteEntry(
val token: String, val token: String,
val otherGid: String? = null,
val otherToken: String? = null,
val category: Int = -1, val category: Int = -1,
) { ) {
fun getUrl() = EHentaiSearchMetadata.idAndTokenToUrl(gid, token) 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 package tachiyomi.domain.manga.repository
import tachiyomi.domain.manga.model.FavoriteEntry import tachiyomi.domain.manga.model.FavoriteEntry
import tachiyomi.domain.manga.model.FavoriteEntryAlternative
interface FavoritesEntryRepository { interface FavoritesEntryRepository {
suspend fun deleteAll() suspend fun deleteAll()
@ -8,4 +9,6 @@ interface FavoritesEntryRepository {
suspend fun insertAll(favoriteEntries: List<FavoriteEntry>) suspend fun insertAll(favoriteEntries: List<FavoriteEntry>)
suspend fun selectAll(): List<FavoriteEntry> suspend fun selectAll(): List<FavoriteEntry>
suspend fun addAlternative(favoriteEntryAlternative: FavoriteEntryAlternative)
} }