restore-feed-order (#223)

* fix: possible duplicate saved search restored if query or filters is null

* refactor: change backup structure so it allows keeping feeds' order
This commit is contained in:
Tran M. Cuong 2024-08-05 18:15:33 +07:00 committed by GitHub
parent d5844286b9
commit c43015439d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 78 additions and 65 deletions

View file

@ -14,6 +14,6 @@ class FeedBackupCreator(
* Backup global Popular/Latest feeds
*/
suspend fun backupFeeds(): List<BackupFeed> {
return handler.awaitList { feed_saved_searchQueries.selectAllGlobalNonSavedSearch(backupFeedMapper) }
return handler.awaitList { feed_saved_searchQueries.selectAllFeedWithSavedSearch(backupFeedMapper) }
}
}

View file

@ -1,7 +1,6 @@
package eu.kanade.tachiyomi.data.backup.create.creators
import eu.kanade.tachiyomi.data.backup.models.BackupSavedSearch
import eu.kanade.tachiyomi.data.backup.models.backupFeedMapper
import eu.kanade.tachiyomi.data.backup.models.backupSavedSearchMapper
import tachiyomi.data.DatabaseHandler
import uy.kohesive.injekt.Injekt
@ -12,23 +11,6 @@ class SavedSearchBackupCreator(
) {
suspend fun backupSavedSearches(): List<BackupSavedSearch> {
// KMK -->
// return handler.awaitList { saved_searchQueries.selectAll(backupSavedSearchMapper) }
val savedSearches = handler.awaitList { saved_searchQueries.selectAll() }
val feedSavedSearches =
handler.awaitList { feed_saved_searchQueries.selectAllFeedHasSavedSearch() }
return savedSearches.map { savedSearch ->
val feeds = feedSavedSearches.filter { it.saved_search == savedSearch._id }
backupSavedSearchMapper(
savedSearch._id,
savedSearch.source,
savedSearch.name,
savedSearch.query,
savedSearch.filters_json,
feeds.map { it.backupFeedMapper() },
)
}
// KMK <--
return handler.awaitList { saved_searchQueries.selectAll(backupSavedSearchMapper) }
}
}

View file

@ -2,20 +2,23 @@ package eu.kanade.tachiyomi.data.backup.models
import kotlinx.serialization.Serializable
import kotlinx.serialization.protobuf.ProtoNumber
import tachiyomi.data.SelectAllFeedHasSavedSearch
@Serializable
data class BackupFeed(
@ProtoNumber(1) val source: Long = 0,
@ProtoNumber(2) val global: Boolean = true,
@ProtoNumber(9) val savedSearch: BackupSavedSearch? = null,
)
val backupFeedMapper =
{ _: Long, source: Long, _: Long?, global: Boolean, _: Long ->
{ source: Long, global: Boolean, savedSearch: Long?, name: String?, query: String?, filtersJson: String? ->
BackupFeed(
source = source,
global = global,
savedSearch = if (savedSearch != null && name != null) {
backupSavedSearchMapper(savedSearch, source, name, query, filtersJson)
} else {
null
},
)
}
fun SelectAllFeedHasSavedSearch.backupFeedMapper() = backupFeedMapper(_id, source, saved_search, global, feed_order)

View file

@ -12,20 +12,14 @@ data class BackupSavedSearch(
@ProtoNumber(2) val query: String = "",
@ProtoNumber(3) val filterList: String = "",
@ProtoNumber(4) val source: Long = 0,
// KMK -->
@ProtoNumber(9) var backupFeeds: List<BackupFeed> = emptyList(),
// KMK <--
)
val backupSavedSearchMapper =
{ _: Long, source: Long, name: String, query: String?, filtersJson: String?, backupFeeds: List<BackupFeed> ->
{ _: Long, source: Long, name: String, query: String?, filtersJson: String? ->
BackupSavedSearch(
source = source,
name = name,
query = query.orEmpty(),
filterList = filtersJson ?: "[]",
// KMK -->
backupFeeds = backupFeeds,
// KMK <--
)
}

View file

@ -146,10 +146,10 @@ class BackupRestorer(
// KMK <--
) = launch {
ensureActive()
savedSearchRestorer.restoreSavedSearches(backupSavedSearches)
// KMK -->
feedRestorer.restoreFeeds(backupFeeds)
// KMK <--
savedSearchRestorer.restoreSavedSearches(backupSavedSearches)
restoreProgress += 1
notifier.showRestoreProgress(

View file

@ -1,6 +1,7 @@
package eu.kanade.tachiyomi.data.backup.restore.restorers
import eu.kanade.tachiyomi.data.backup.models.BackupFeed
import exh.util.nullIfBlank
import tachiyomi.data.DatabaseHandler
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
@ -8,24 +9,58 @@ import uy.kohesive.injekt.api.get
class FeedRestorer(
private val handler: DatabaseHandler = Injekt.get(),
) {
suspend fun restoreFeeds(backupFeeds: List<BackupFeed>, savedSearchId: Long? = null) {
suspend fun restoreFeeds(backupFeeds: List<BackupFeed>) {
if (backupFeeds.isEmpty()) return
handler.await(true) {
val currentFeeds = handler.awaitList {
feed_saved_searchQueries.selectAllGlobalNonSavedSearch()
feed_saved_searchQueries.selectAllFeedWithSavedSearch()
}
val currentSavedSearches = handler.awaitList {
saved_searchQueries.selectAll()
}
backupFeeds.filter { backupFeed ->
// Filter if source's global Popular/Latest feed already existed
currentFeeds.none {
it.source == backupFeed.source && backupFeed.global
// Filter out source's global Popular/Latest feed already existed
backupFeed.savedSearch == null &&
currentFeeds.none { currentFeed ->
currentFeed.source == backupFeed.source && backupFeed.global
} ||
// Filter out feed with saveSearch already existed (both global/non-global)
backupFeed.savedSearch != null &&
currentFeeds.none { currentFeed ->
currentFeed.source == backupFeed.source &&
currentFeed.global == backupFeed.global &&
currentFeed.name == backupFeed.savedSearch.name &&
currentFeed.query.orEmpty() == backupFeed.savedSearch.query &&
(currentFeed.filters_json ?: "[]") == backupFeed.savedSearch.filterList
}
}.forEach { backupFeed ->
val savedSearchId = backupFeed.savedSearch?.let {
val existedSavedSearchId = currentSavedSearches.find { currentSavedSearch ->
currentSavedSearch.source == backupFeed.source &&
currentSavedSearch.name == backupFeed.savedSearch.name &&
currentSavedSearch.query.orEmpty() == backupFeed.savedSearch.query &&
(currentSavedSearch.filters_json ?: "[]") == backupFeed.savedSearch.filterList
}?._id
existedSavedSearchId ?: handler.awaitOneExecutable(true) {
// Just in case, trying to create the associated saved_search
saved_searchQueries.insert(
source = backupFeed.source,
name = backupFeed.savedSearch.name,
query = backupFeed.savedSearch.query.nullIfBlank(),
filtersJson = backupFeed.savedSearch.filterList.nullIfBlank()
?.takeUnless { it == "[]" },
)
saved_searchQueries.selectLastInsertedRowId()
}
}
}.forEach {
feed_saved_searchQueries.insert(
sourceId = it.source,
sourceId = backupFeed.source,
savedSearch = savedSearchId,
global = it.global,
global = backupFeed.global,
)
}
}

View file

@ -8,9 +8,6 @@ import uy.kohesive.injekt.api.get
class SavedSearchRestorer(
private val handler: DatabaseHandler = Injekt.get(),
// KMK -->
private val feedRestorer: FeedRestorer = FeedRestorer(),
// KMK <--
) {
suspend fun restoreSavedSearches(backupSavedSearches: List<BackupSavedSearch>) {
if (backupSavedSearches.isEmpty()) return
@ -19,32 +16,29 @@ class SavedSearchRestorer(
handler.await(true) {
// KMK <--
val currentSavedSearches = handler.awaitList {
// KMK -->
// saved_searchQueries.selectNamesAndSources()
saved_searchQueries.selectAll()
// KMK <--
}
backupSavedSearches.forEach { backupSavedSearch ->
val existedSavedSearchId = currentSavedSearches.find {
it.source == backupSavedSearch.source &&
it.name == backupSavedSearch.name &&
it.query == backupSavedSearch.query &&
it.filters_json == backupSavedSearch.filterList
}?._id
// KMK -->
val savedSearchId = existedSavedSearchId ?: handler.awaitOneExecutable(true) {
backupSavedSearches.filter { backupSavedSearch ->
currentSavedSearches.none { currentSavedSearch ->
currentSavedSearch.source == backupSavedSearch.source &&
currentSavedSearch.name == backupSavedSearch.name &&
// KMK -->
currentSavedSearch.query.orEmpty() == backupSavedSearch.query &&
(currentSavedSearch.filters_json ?: "[]") == backupSavedSearch.filterList
// KMK <--
saved_searchQueries.insert(
source = backupSavedSearch.source,
name = backupSavedSearch.name,
query = backupSavedSearch.query.nullIfBlank(),
filtersJson = backupSavedSearch.filterList.nullIfBlank()
?.takeUnless { it == "[]" },
)
// KMK -->
saved_searchQueries.selectLastInsertedRowId()
}
feedRestorer.restoreFeeds(backupSavedSearch.backupFeeds, savedSearchId)
// KMK <--
}.forEach { backupSavedSearch ->
saved_searchQueries.insert(
source = backupSavedSearch.source,
name = backupSavedSearch.name,
query = backupSavedSearch.query.nullIfBlank(),
filtersJson = backupSavedSearch.filterList.nullIfBlank()
?.takeUnless { it == "[]" },
)
}
}
}

View file

@ -58,13 +58,18 @@ ON saved_search._id = M.saved_search;
selectLastInsertedRowId:
SELECT last_insert_rowid();
-- KMK -->
selectAllGlobalNonSavedSearch:
SELECT * FROM feed_saved_search WHERE global = 1 AND saved_search IS NULL ORDER BY feed_order;
selectAllFeedHasSavedSearch:
SELECT * FROM feed_saved_search WHERE saved_search IS NOT NULL ORDER BY feed_order;
-- KMK -->
selectAllFeedWithSavedSearch:
SELECT coalesce(f.source, s.source) source, f.global, f.saved_search, s.name, s.query, s.filters_json
FROM feed_saved_search AS f LEFT JOIN saved_search AS s
ON f.saved_search = s._id ORDER BY f.feed_order;
setOrder:
UPDATE feed_saved_search SET feed_order = :order WHERE _id = :id;