Rework database queries for the new schema (#4022)

* query - category

Key every single-row statement on :id and list columns on one line. A
lookup by primary key no longer needs LIMIT 1, and resetting every
category's flags takes a value rather than a nullable nobody passed.

Assisted-by: Claude:claude-opus-5

* query - chapter

List columns in the table's order and name bind and mapper parameters
after the columns they fill. Inserts pass their arguments by name,
since reordering the columns put the upload and fetch dates side by
side with the same type.

With (manga_id, remote_url) unique, looking a chapter up by them no
longer needs LIMIT 1. Available scanlators come back distinct instead of
one row per chapter, which is all the only caller wanted. insert was
insertReturningId without the RETURNING, so restore uses the latter and
the former goes, along with the unused getChapterByUrl.

Inserting skips a chapter that a sync running alongside stored first,
rather than failing the whole batch and losing every new chapter of
that update.

Assisted-by: Claude:claude-opus-5

* query - excluded_scanlator

An entry excludes a scanlator at most once now. Inserting one that is
already excluded does nothing, so restoring a backup that lists one twice
drops the repeat instead of failing the entry, and restore no longer
reads the stored exclusions first to skip them itself.

Saving an unchanged selection no longer runs an empty delete, which
still made every chapter query run again.

Assisted-by: Claude:claude-opus-5

* query - extension_store

Take the upsert's new values from excluded and drop a WHERE its
conflict target already implies. That also retires a
`signing_key =: signingKey` that only parsed by accident.

Assisted-by: Claude:claude-opus-5

* query - history

Restoring an entry's history looked up the history row and, failing
that, the chapter for every item in the backup, one query each. It now
reads the entry's chapters and history once and matches them in memory,
which leaves getHistoryByChapterUrlAndMangaId without callers.

Creating a backup looked up the chapter of every history row one at a
time and failed on a row whose chapter was gone. It reads the entry's
chapters once and leaves such a row out.

The upsert takes its new values from excluded, drops a trailing WHERE
its conflict target already pins, and names its parameters after the
columns they fill.

Assisted-by: Claude:claude-opus-5

* query - manga

List columns in the table's order and name bind and mapper parameters
after the columns they fill, forwarding mapper arguments by name.

With (source_id, remote_url) unique, looking an entry up by them no
longer needs LIMIT 1, and fetching a network entry becomes a single
upsert that refreshes an existing row only while it is outside the
library, instead of an insert guarded by NOT EXISTS followed by an
update of the same row.

"Is anything read" checks use EXISTS rather than IN over a DISTINCT
subquery, category membership drops a coalesce on a column that is
never null, and the unused getAllManga goes. Applying the default
chapter flags to the library is one transaction instead of one per
entry, and restore checks backup entries against the stored urls as a
set rather than a list.

Assisted-by: Claude:claude-opus-5

* query - manga_category

An entry belongs to a category at most once now. Inserting a pairing
that already exists does nothing, so setting an entry's categories from
a list that names one twice no longer fails, which restore could produce
when two backup categories resolve to the same local one.

Assisted-by: Claude:claude-opus-5

* query - manga_track

List columns in the table's order and name bind and mapper parameters
after the columns they fill. That retires the sync-era :syncId,
:mediaId, :totalChapter and :trackingUrl.

Assisted-by: Claude:claude-opus-5

* query - source

List columns in the table's order and name the upsert's parameters
after its columns. The upsert takes the conflicting row's values from
excluded instead of rebinding them, and drops a WHERE its conflict
target already implies. The call passes its two string arguments by
name, since swapping them would still compile.

Assisted-by: Claude:claude-opus-5

* query - historyView

The latest read entry is read from the tables through the index on history's read time instead
of aggregating the whole view for one row: ~50ms to under 1ms on a 114k chapter library.

Assisted-by: Claude:claude-opus-5

* query - libraryView

List the columns like the other queries.

Assisted-by: Claude:claude-opus-5

* query - updatesView

Drop a coalesce on category_id, which is never null, from the queries
over updatesView, and the unused getRecentUpdates. The view definitions
themselves are unchanged, so no migration is needed.

The read and unread lookups become separate queries with the status
written into each. SQLite only uses a partial index when the query's own
condition matches it, which a bound parameter never does, so the unread
lookup the widget follows walked nearly the whole fetch date index,
~60ms each time on a 114k chapter library and after every chapter or
history write. With the status written in, it's served by the partial
index over unread chapters, ~8ms.

Assisted-by: Claude:claude-opus-5

* Query browse entries only while they are shown

Each loaded entry kept a live query for as long as the screen existed,
so every write to manga re-ran all of them. An entry now starts from the
row the listing stored and queries only while shown.

Assisted-by: Claude:claude-opus-5

* Read library selection categories from memory

The change category dialog and category updates read each selected
entry's categories from the loaded library instead of querying them one
at a time.

Assisted-by: Claude:claude-opus-5
This commit is contained in:
AntsyLich
2026-09-28 21:57:53 +06:00
committed by GitHub
parent 93cc075116
commit 2d1d2e4cab
28 changed files with 706 additions and 749 deletions
@@ -62,9 +62,11 @@ class MangaBackupCreator(
if (options.history) { if (options.history) {
val historyByMangaId = getHistory.await(manga.id) val historyByMangaId = getHistory.await(manga.id)
if (historyByMangaId.isNotEmpty()) { if (historyByMangaId.isNotEmpty()) {
val history = historyByMangaId.map { history -> val chapterUrlsById = chapterRepository.getChapterByMangaId(manga.id).associate { it.id to it.url }
val chapter = checkNotNull(chapterRepository.getChapterById(history.chapterId)) val history = historyByMangaId.mapNotNull { history ->
BackupHistory(chapter.url, history.readAt?.time ?: 0L, history.readDuration) // A chapter removed since its history was read takes that history with it
val url = chapterUrlsById[history.chapterId] ?: return@mapNotNull null
BackupHistory(url, history.readAt?.time ?: 0L, history.readDuration)
} }
if (history.isNotEmpty()) { if (history.isNotEmpty()) {
mangaObject.history = history mangaObject.history = history
@@ -32,6 +32,7 @@ import eu.kanade.tachiyomi.util.removeCovers
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.WhileSubscribed
import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.filter
@@ -56,6 +57,7 @@ import tachiyomi.domain.manga.model.MangaWithChapterCount
import tachiyomi.domain.source.interactor.GetRemoteManga import tachiyomi.domain.source.interactor.GetRemoteManga
import tachiyomi.domain.source.service.SourceManager import tachiyomi.domain.source.service.SourceManager
import kotlin.time.Clock import kotlin.time.Clock
import kotlin.time.Duration.Companion.seconds
import eu.kanade.tachiyomi.source.model.Filter as SourceModelFilter import eu.kanade.tachiyomi.source.model.Filter as SourceModelFilter
@AssistedInject @AssistedInject
@@ -133,7 +135,7 @@ class BrowseSourceViewModel(
pagingData.map { manga -> pagingData.map { manga ->
getManga.subscribe(manga.url, manga.source) getManga.subscribe(manga.url, manga.source)
.map { it ?: manga } .map { it ?: manga }
.stateIn(viewModelScope) .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5.seconds), manga)
} }
.filter { !hideInLibraryItems || !it.value.favorite } .filter { !hideInLibraryItems || !it.value.favorite }
} }
@@ -458,34 +458,19 @@ class LibraryViewModel(
} }
} }
/** private suspend fun getCategoryIds(manga: Manga): Set<Long> {
* Returns the common categories for the given list of manga. return state.value.libraryData.favoritesById[manga.id]
* ?.libraryManga
* @param mangas the list of manga. ?.categories
*/ // The library reports no category as 0
private suspend fun getCommonCategories(mangas: List<Manga>): Collection<Category> { ?.filterTo(mutableSetOf()) { it != 0L }
if (mangas.isEmpty()) return emptyList() ?: getCategories.await(manga.id).mapTo(mutableSetOf()) { it.id }
return mangas
.map { getCategories.await(it.id).toSet() }
.reduce { set1, set2 -> set1.intersect(set2) }
} }
suspend fun getNextUnreadChapter(manga: Manga): Chapter? { suspend fun getNextUnreadChapter(manga: Manga): Chapter? {
return getChaptersByMangaId.await(manga.id, applyScanlatorFilter = true).getNextUnread(manga, downloadManager) return getChaptersByMangaId.await(manga.id, applyScanlatorFilter = true).getNextUnread(manga, downloadManager)
} }
/**
* Returns the mix (non-common) categories for the given list of manga.
*
* @param mangas the list of manga.
*/
private suspend fun getMixCategories(mangas: List<Manga>): Collection<Category> {
if (mangas.isEmpty()) return emptyList()
val mangaCategories = mangas.map { getCategories.await(it.id).toSet() }
val common = mangaCategories.reduce { set1, set2 -> set1.intersect(set2) }
return mangaCategories.flatten().distinct().subtract(common)
}
/** /**
* Queues the amount specified of unread chapters from the list of selected manga * Queues the amount specified of unread chapters from the list of selected manga
*/ */
@@ -599,8 +584,7 @@ class LibraryViewModel(
fun setMangaCategories(mangaList: List<Manga>, addCategories: List<Long>, removeCategories: List<Long>) { fun setMangaCategories(mangaList: List<Manga>, addCategories: List<Long>, removeCategories: List<Long>) {
viewModelScope.launchNonCancellable { viewModelScope.launchNonCancellable {
mangaList.forEach { manga -> mangaList.forEach { manga ->
val categoryIds = getCategories.await(manga.id) val categoryIds = getCategoryIds(manga)
.map { it.id }
.subtract(removeCategories.toSet()) .subtract(removeCategories.toSet())
.plus(addCategories) .plus(addCategories)
.toList() .toList()
@@ -729,13 +713,12 @@ class LibraryViewModel(
// Hide the default category because it has a different behavior than the ones from db. // Hide the default category because it has a different behavior than the ones from db.
val categories = state.value.displayedCategories.filter { it.id != 0L } val categories = state.value.displayedCategories.filter { it.id != 0L }
// Get indexes of the common categories to preselect. val mangaCategoryIds = mangaList.map { getCategoryIds(it) }
val common = getCommonCategories(mangaList) val common = mangaCategoryIds.reduceOrNull { set1, set2 -> set1 intersect set2 }.orEmpty()
// Get indexes of the mix categories to preselect. val mix = mangaCategoryIds.flatten().toSet() - common
val mix = getMixCategories(mangaList)
val preselected = categories val preselected = categories
.map { .map {
when (it) { when (it.id) {
in common -> CheckboxState.State.Checked(it) in common -> CheckboxState.State.Checked(it)
in mix -> CheckboxState.TriState.Exclude(it) in mix -> CheckboxState.TriState.Exclude(it)
else -> CheckboxState.State.None(it) else -> CheckboxState.State.None(it)
@@ -34,11 +34,12 @@ class RestoreRepositoryImpl(
private val trackRepository: TrackRepository, private val trackRepository: TrackRepository,
) : RestoreRepository { ) : RestoreRepository {
override suspend fun getMangaUrlsBySourceId(): Map<Long, List<String>> { override suspend fun getMangaUrlsBySourceId(): Map<Long, Set<String>> {
return database.mangaQueries return database.mangaQueries
.getAllMangaSourceAndUrl() .getAllMangaSourceAndUrl()
.awaitAsList() .awaitAsList()
.groupBy({ it.source_id }, { it.remote_url }) .groupBy({ it.source_id }, { it.remote_url })
.mapValues { it.value.toSet() }
} }
override suspend fun restoreManga(entries: List<RestoredManga>, update: suspend (Manga) -> MangaUpdate) { override suspend fun restoreManga(entries: List<RestoredManga>, update: suspend (Manga) -> MangaUpdate) {
@@ -93,49 +94,49 @@ class RestoreRepositoryImpl(
private suspend fun updateManga(manga: Manga): Manga { private suspend fun updateManga(manga: Manga): Manga {
database.mangaQueries.updateFromBackup( database.mangaQueries.updateFromBackup(
favoriteAt = manga.favoriteAt, userFavoriteAt = manga.favoriteAt,
artist = manga.artist, remoteArtist = manga.artist,
author = manga.author, remoteAuthor = manga.author,
description = manga.description, remoteDescription = manga.description,
genre = manga.genre, remoteGenre = manga.genre,
title = manga.title, remoteTitle = manga.title,
status = manga.status, remoteStatus = manga.status,
thumbnailUrl = manga.thumbnailUrl, remoteCover = manga.thumbnailUrl,
lastUpdate = manga.lastUpdate, stateChapterLastUpdate = manga.lastUpdate,
initialized = manga.initialized, stateInitialized = manga.initialized,
viewer = manga.viewerFlags, userReaderFlags = manga.viewerFlags,
chapterFlags = manga.chapterFlags, userChapterFlags = manga.chapterFlags,
coverLastModified = manga.coverLastModified, stateCoverLastModified = manga.coverLastModified,
mangaId = manga.id, id = manga.id,
updateStrategy = manga.updateStrategy, remoteUpdateStrategy = manga.updateStrategy,
notes = manga.notes, userNotes = manga.notes,
memo = manga.memo, remoteMemo = manga.memo,
) )
return manga return manga
} }
private suspend fun insertManga(manga: Manga): Long { private suspend fun insertManga(manga: Manga): Long {
return database.mangaQueries.insertReturningId( return database.mangaQueries.insertReturningId(
favoriteAt = manga.favoriteAt, userFavoriteAt = manga.favoriteAt,
source = manga.source, sourceId = manga.source,
url = manga.url, remoteUrl = manga.url,
artist = manga.artist, remoteArtist = manga.artist,
author = manga.author, remoteAuthor = manga.author,
description = manga.description, remoteDescription = manga.description,
genre = manga.genre, remoteGenre = manga.genre,
title = manga.title, remoteTitle = manga.title,
status = manga.status, remoteStatus = manga.status,
thumbnailUrl = manga.thumbnailUrl, remoteCover = manga.thumbnailUrl,
lastUpdate = manga.lastUpdate, stateChapterLastUpdate = manga.lastUpdate,
nextUpdate = 0L, stateChapterNextUpdate = 0L,
calculateInterval = 0L, stateChapterFetchInterval = 0L,
initialized = manga.initialized, stateInitialized = manga.initialized,
viewerFlags = manga.viewerFlags, userReaderFlags = manga.viewerFlags,
chapterFlags = manga.chapterFlags, userChapterFlags = manga.chapterFlags,
coverLastModified = manga.coverLastModified, stateCoverLastModified = manga.coverLastModified,
updateStrategy = manga.updateStrategy, remoteUpdateStrategy = manga.updateStrategy,
notes = manga.notes, userNotes = manga.notes,
memo = manga.memo, remoteMemo = manga.memo,
) )
.awaitAsOne() .awaitAsOne()
} }
@@ -183,28 +184,29 @@ class RestoreRepositoryImpl(
.partition { it.id > 0 } .partition { it.id > 0 }
newChapters.forEach { chapter -> newChapters.forEach { chapter ->
database.chapterQueries.insert( database.chapterQueries.insertReturningId(
mangaId = chapter.mangaId, mangaId = chapter.mangaId,
url = chapter.url, remoteUrl = chapter.url,
name = chapter.name, remoteName = chapter.name,
scanlator = chapter.scanlator, remoteScanlator = chapter.scanlator,
read = chapter.read, userRead = chapter.read,
bookmark = chapter.bookmark, userBookmark = chapter.bookmark,
lastPageRead = chapter.lastPageRead, userLastPageRead = chapter.lastPageRead,
chapterNumber = chapter.chapterNumber, remoteChapterNumber = chapter.chapterNumber,
sourceOrder = chapter.sourceOrder, remoteOrder = chapter.sourceOrder,
dateFetch = chapter.dateFetch, stateDateFetch = chapter.dateFetch,
dateUpload = chapter.dateUpload, remoteDateUpload = chapter.dateUpload,
memo = chapter.memo, remoteMemo = chapter.memo,
) )
.awaitAsOneOrNull()
} }
existingChapters.forEach { chapter -> existingChapters.forEach { chapter ->
database.chapterQueries.updateFromBackup( database.chapterQueries.updateFromBackup(
read = chapter.read, userRead = chapter.read,
bookmark = chapter.bookmark, userBookmark = chapter.bookmark,
lastPageRead = chapter.lastPageRead, userLastPageRead = chapter.lastPageRead,
chapterId = chapter.id, id = chapter.id,
memo = chapter.memo, remoteMemo = chapter.memo,
) )
} }
} }
@@ -213,28 +215,30 @@ class RestoreRepositoryImpl(
this.copy(id = 0L, mangaId = 0L, dateFetch = 0L, dateUpload = 0L) this.copy(id = 0L, mangaId = 0L, dateFetch = 0L, dateUpload = 0L)
private suspend fun restoreHistory(manga: Manga, restoredHistory: List<RestoredHistory>) { private suspend fun restoreHistory(manga: Manga, restoredHistory: List<RestoredHistory>) {
if (restoredHistory.isEmpty()) return
val chapterIdsByUrl = chapterRepository.getChapterByMangaId(manga.id).associate { it.url to it.id }
val dbHistoryByChapterId = database.historyQueries
.getHistoryByMangaId(manga.id)
.awaitAsList()
.associateBy { it.chapter_id }
val toUpdate = restoredHistory val toUpdate = restoredHistory
.groupBy { it.chapterUrl } // Chapter doesn't exist; skip
.mapNotNull { (chapterUrl, copies) -> .filter { it.chapterUrl in chapterIdsByUrl }
// A backup of a library that still had duplicate chapters carries a history entry for
// each copy; they are one chapter now, read as late as any copy and for as long as all
.groupBy { chapterIdsByUrl.getValue(it.chapterUrl) }
.map { (chapterId, copies) ->
val readAt = copies.maxOf { it.readAt?.time ?: 0L } val readAt = copies.maxOf { it.readAt?.time ?: 0L }
val readDuration = copies.sumOf { it.readDuration } val readDuration = copies.sumOf { it.readDuration }
val dbHistory = database.historyQueries val dbHistory = dbHistoryByChapterId[chapterId]
.getHistoryByChapterUrlAndMangaId(chapterUrl, manga.id)
.awaitAsOneOrNull()
if (dbHistory == null) {
val chapter = database.chapterQueries
.getChapterByUrlAndMangaId(chapterUrl, manga.id)
.awaitAsOneOrNull()
// Chapter doesn't exist; skip
?: return@mapNotNull null
// New history entry // New history entry
return@mapNotNull Triple(chapter.id, Date(readAt), readDuration) ?: return@map Triple(chapterId, Date(readAt), readDuration)
}
// Update history entry // Update history entry. 0 is kept rather than written as NULL, since it marks history
// the user removed.
Triple( Triple(
dbHistory.chapter_id, chapterId,
Date(max(readAt, dbHistory.read_at?.time ?: 0L)), Date(max(readAt, dbHistory.read_at?.time ?: 0L)),
max(readDuration, dbHistory.read_duration) - dbHistory.read_duration, max(readDuration, dbHistory.read_duration) - dbHistory.read_duration,
) )
@@ -271,15 +275,15 @@ class RestoreRepositoryImpl(
existingTracks.forEach { track -> existingTracks.forEach { track ->
database.manga_trackQueries.update( database.manga_trackQueries.update(
mangaId = track.mangaId, mangaId = track.mangaId,
syncId = track.trackerId, trackerId = track.trackerId,
mediaId = track.remoteId, remoteId = track.remoteId,
libraryId = track.libraryId, libraryId = track.libraryId,
title = track.title, title = track.title,
lastChapterRead = track.lastChapterRead, lastChapterRead = track.lastChapterRead,
totalChapter = track.totalChapters, totalChapters = track.totalChapters,
status = track.status, status = track.status,
score = track.score, score = track.score,
trackingUrl = track.remoteUrl, remoteUrl = track.remoteUrl,
startDate = track.startDate, startDate = track.startDate,
finishDate = track.finishDate, finishDate = track.finishDate,
`private` = track.private, `private` = track.private,
@@ -289,11 +293,6 @@ class RestoreRepositoryImpl(
} }
private suspend fun restoreExcludedScanlators(manga: Manga, excludedScanlators: List<String>) { private suspend fun restoreExcludedScanlators(manga: Manga, excludedScanlators: List<String>) {
if (excludedScanlators.isEmpty()) return excludedScanlators.forEach { database.excluded_scanlatorQueries.insert(manga.id, it) }
val existingExcludedScanlators = database.excluded_scanlatorQueries
.getExcludedScanlatorsByMangaId(manga.id)
.awaitAsList()
val toInsert = excludedScanlators.filter { it !in existingExcludedScanlators }
toInsert.forEach { database.excluded_scanlatorQueries.insert(manga.id, it) }
} }
} }
@@ -61,14 +61,14 @@ class CategoryRepositoryImpl(
} }
override suspend fun updateName(categoryId: Long, name: String) { override suspend fun updateName(categoryId: Long, name: String) {
database.categoryQueries.updateName(name = name, categoryId = categoryId) database.categoryQueries.updateName(name = name, id = categoryId)
} }
override suspend fun updateFlags(categoryId: Long, flags: Long) { override suspend fun updateFlags(categoryId: Long, flags: Long) {
database.categoryQueries.updateFlags(flags = flags, categoryId = categoryId) database.categoryQueries.updateFlags(flags = flags, id = categoryId)
} }
override suspend fun updateAllFlags(flags: Long?) { override suspend fun updateAllFlags(flags: Long) {
database.categoryQueries.updateAllFlags(flags = flags) database.categoryQueries.updateAllFlags(flags = flags)
} }
@@ -77,16 +77,16 @@ class CategoryRepositoryImpl(
val current = database.categoryQueries.getUserCategoryIds().awaitAsList() val current = database.categoryQueries.getUserCategoryIds().awaitAsList()
val ids = orderedIds.filter { it in current } + current.filterNot { it in orderedIds } val ids = orderedIds.filter { it in current } + current.filterNot { it in orderedIds }
ids.forEachIndexed { index, categoryId -> ids.forEachIndexed { index, categoryId ->
database.categoryQueries.updateOrder(order = -index - 2L, categoryId = categoryId) database.categoryQueries.updateOrder(order = -index - 2L, id = categoryId)
} }
ids.forEachIndexed { index, categoryId -> ids.forEachIndexed { index, categoryId ->
database.categoryQueries.updateOrder(order = index.toLong(), categoryId = categoryId) database.categoryQueries.updateOrder(order = index.toLong(), id = categoryId)
} }
} }
} }
override suspend fun delete(categoryId: Long) { override suspend fun delete(categoryId: Long) {
database.categoryQueries.delete(categoryId = categoryId) database.categoryQueries.delete(id = categoryId)
} }
private fun mapCategory( private fun mapCategory(
@@ -57,30 +57,30 @@ class ChapterRepositoryImpl(
val stored = added.filter { existing.add(it.mangaId to it.url) }.map { chapter -> val stored = added.filter { existing.add(it.mangaId to it.url) }.map { chapter ->
val chapterId = database.chapterQueries.insertReturningId( val chapterId = database.chapterQueries.insertReturningId(
mangaId = chapter.mangaId, mangaId = chapter.mangaId,
url = chapter.url, remoteUrl = chapter.url,
name = chapter.name, remoteName = chapter.name,
scanlator = chapter.scanlator, remoteScanlator = chapter.scanlator,
read = chapter.read, userRead = chapter.read,
bookmark = chapter.bookmark, userBookmark = chapter.bookmark,
lastPageRead = chapter.lastPageRead, userLastPageRead = chapter.lastPageRead,
chapterNumber = chapter.chapterNumber, remoteChapterNumber = chapter.chapterNumber,
sourceOrder = chapter.sourceOrder, remoteOrder = chapter.sourceOrder,
dateFetch = chapter.dateFetch, stateDateFetch = chapter.dateFetch,
dateUpload = chapter.dateUpload, remoteDateUpload = chapter.dateUpload,
memo = chapter.memo, remoteMemo = chapter.memo,
) )
.awaitAsOne() .awaitAsOne()
chapter.copy(id = chapterId) chapter.copy(id = chapterId)
} }
updated.forEach { chapterUpdate -> updated.forEach { chapterUpdate ->
database.chapterQueries.updateRemote( database.chapterQueries.updateRemote(
name = chapterUpdate.name, remoteName = chapterUpdate.name,
scanlator = chapterUpdate.scanlator, remoteScanlator = chapterUpdate.scanlator,
chapterNumber = chapterUpdate.chapterNumber, remoteChapterNumber = chapterUpdate.chapterNumber,
sourceOrder = chapterUpdate.sourceOrder, remoteOrder = chapterUpdate.sourceOrder,
dateUpload = chapterUpdate.dateUpload, remoteDateUpload = chapterUpdate.dateUpload,
chapterId = chapterUpdate.id, id = chapterUpdate.id,
memo = chapterUpdate.memo, remoteMemo = chapterUpdate.memo,
) )
} }
stored stored
@@ -91,11 +91,11 @@ class ChapterRepositoryImpl(
database.transaction { database.transaction {
chapterUpdates.forEach { chapterUpdate -> chapterUpdates.forEach { chapterUpdate ->
database.chapterQueries.update( database.chapterQueries.update(
read = chapterUpdate.read, userRead = chapterUpdate.read,
bookmark = chapterUpdate.bookmark, userBookmark = chapterUpdate.bookmark,
lastPageRead = chapterUpdate.lastPageRead, userLastPageRead = chapterUpdate.lastPageRead,
dateFetch = chapterUpdate.dateFetch, stateDateFetch = chapterUpdate.dateFetch,
chapterId = chapterUpdate.id, id = chapterUpdate.id,
) )
} }
} }
@@ -147,38 +147,37 @@ class ChapterRepositoryImpl(
override suspend fun getChapterByUrlAndMangaId(url: String, mangaId: Long): Chapter? { override suspend fun getChapterByUrlAndMangaId(url: String, mangaId: Long): Chapter? {
return database.chapterQueries return database.chapterQueries
.getChapterByUrlAndMangaId(url, mangaId, ::mapChapter) .getChapterByUrlAndMangaId(mangaId = mangaId, remoteUrl = url, mapper = ::mapChapter)
.awaitAsOneOrNull() .awaitAsOneOrNull()
} }
@Suppress("UNUSED_PARAMETER")
private fun mapChapter( private fun mapChapter(
id: Long, id: Long,
mangaId: Long, mangaId: Long,
url: String, remoteUrl: String,
name: String, remoteName: String,
scanlator: String?, remoteScanlator: String?,
read: Boolean, remoteChapterNumber: Double,
bookmark: Boolean, remoteDateUpload: Long,
lastPageRead: Long, remoteOrder: Long,
chapterNumber: Double, remoteMemo: JsonObject,
sourceOrder: Long, userRead: Boolean,
dateFetch: Long, userBookmark: Boolean,
dateUpload: Long, userLastPageRead: Long,
memo: JsonObject, stateDateFetch: Long,
): Chapter = Chapter( ): Chapter = Chapter(
id = id, id = id,
mangaId = mangaId, mangaId = mangaId,
read = read, read = userRead,
bookmark = bookmark, bookmark = userBookmark,
lastPageRead = lastPageRead, lastPageRead = userLastPageRead,
dateFetch = dateFetch, dateFetch = stateDateFetch,
sourceOrder = sourceOrder, sourceOrder = remoteOrder,
url = url, url = remoteUrl,
name = name, name = remoteName,
dateUpload = dateUpload, dateUpload = remoteDateUpload,
chapterNumber = chapterNumber, chapterNumber = remoteChapterNumber,
scanlator = scanlator, scanlator = remoteScanlator,
memo = memo, memo = remoteMemo,
) )
} }
@@ -9,72 +9,72 @@ import tachiyomi.domain.manga.model.MangaWithChapterCount
object MangaMapper { object MangaMapper {
fun mapManga( fun mapManga(
id: Long, id: Long,
source: Long, sourceId: Long,
url: String, remoteUrl: String,
artist: String?, remoteTitle: String,
author: String?, remoteAuthor: String?,
description: String?, remoteArtist: String?,
genre: List<String>?, remoteDescription: String?,
title: String, remoteGenre: List<String>?,
status: Long, remoteStatus: Long,
thumbnailUrl: String?, remoteCover: String?,
lastUpdate: Long?, remoteUpdateStrategy: UpdateStrategy,
nextUpdate: Long?, remoteMemo: JsonObject,
initialized: Boolean, userFavoriteAt: Long?,
viewerFlags: Long, userNotes: String,
chapterFlags: Long, userReaderFlags: Long,
coverLastModified: Long, userChapterFlags: Long,
updateStrategy: UpdateStrategy, stateChapterLastUpdate: Long?,
calculateInterval: Long, stateChapterNextUpdate: Long?,
notes: String, stateChapterFetchInterval: Long,
memo: JsonObject, stateCoverLastModified: Long,
favoriteAt: Long?, stateInitialized: Boolean,
): Manga = Manga( ): Manga = Manga(
id = id, id = id,
source = source, source = sourceId,
lastUpdate = lastUpdate ?: 0, favoriteAt = userFavoriteAt,
nextUpdate = nextUpdate ?: 0, lastUpdate = stateChapterLastUpdate ?: 0,
fetchInterval = calculateInterval.toInt(), nextUpdate = stateChapterNextUpdate ?: 0,
favoriteAt = favoriteAt, fetchInterval = stateChapterFetchInterval.toInt(),
viewerFlags = viewerFlags, viewerFlags = userReaderFlags,
chapterFlags = chapterFlags, chapterFlags = userChapterFlags,
coverLastModified = coverLastModified, coverLastModified = stateCoverLastModified,
url = url, url = remoteUrl,
title = title, title = remoteTitle,
artist = artist, artist = remoteArtist,
author = author, author = remoteAuthor,
description = description, description = remoteDescription,
genre = genre, genre = remoteGenre,
status = status, status = remoteStatus,
thumbnailUrl = thumbnailUrl, thumbnailUrl = remoteCover,
updateStrategy = updateStrategy, updateStrategy = remoteUpdateStrategy,
initialized = initialized, initialized = stateInitialized,
notes = notes, notes = userNotes,
memo = memo, memo = remoteMemo,
) )
fun mapLibraryManga( fun mapLibraryManga(
id: Long, id: Long,
source: Long, sourceId: Long,
url: String, remoteUrl: String,
artist: String?, remoteTitle: String,
author: String?, remoteAuthor: String?,
description: String?, remoteArtist: String?,
genre: List<String>?, remoteDescription: String?,
title: String, remoteGenre: List<String>?,
status: Long, remoteStatus: Long,
thumbnailUrl: String?, remoteCover: String?,
lastUpdate: Long?, remoteUpdateStrategy: UpdateStrategy,
nextUpdate: Long?, remoteMemo: JsonObject,
initialized: Boolean, userFavoriteAt: Long,
viewerFlags: Long, userNotes: String,
chapterFlags: Long, userReaderFlags: Long,
coverLastModified: Long, userChapterFlags: Long,
updateStrategy: UpdateStrategy, stateChapterLastUpdate: Long?,
calculateInterval: Long, stateChapterNextUpdate: Long?,
notes: String, stateChapterFetchInterval: Long,
memo: JsonObject, stateCoverLastModified: Long,
favoriteAt: Long?, stateInitialized: Boolean,
totalCount: Long, totalCount: Long,
readCount: Double, readCount: Double,
latestUpload: Long, latestUpload: Long,
@@ -85,26 +85,26 @@ object MangaMapper {
): LibraryManga = LibraryManga( ): LibraryManga = LibraryManga(
manga = mapManga( manga = mapManga(
id = id, id = id,
source = source, sourceId = sourceId,
url = url, remoteUrl = remoteUrl,
artist = artist, remoteTitle = remoteTitle,
author = author, remoteAuthor = remoteAuthor,
description = description, remoteArtist = remoteArtist,
genre = genre, remoteDescription = remoteDescription,
title = title, remoteGenre = remoteGenre,
status = status, remoteStatus = remoteStatus,
thumbnailUrl = thumbnailUrl, remoteCover = remoteCover,
lastUpdate = lastUpdate, remoteUpdateStrategy = remoteUpdateStrategy,
nextUpdate = nextUpdate, remoteMemo = remoteMemo,
initialized = initialized, userFavoriteAt = userFavoriteAt,
viewerFlags = viewerFlags, userNotes = userNotes,
chapterFlags = chapterFlags, userReaderFlags = userReaderFlags,
coverLastModified = coverLastModified, userChapterFlags = userChapterFlags,
updateStrategy = updateStrategy, stateChapterLastUpdate = stateChapterLastUpdate,
calculateInterval = calculateInterval, stateChapterNextUpdate = stateChapterNextUpdate,
notes = notes, stateChapterFetchInterval = stateChapterFetchInterval,
memo = memo, stateCoverLastModified = stateCoverLastModified,
favoriteAt = favoriteAt, stateInitialized = stateInitialized,
), ),
categories = categories.split(",").map { it.toLong() }, categories = categories.split(",").map { it.toLong() },
totalChapters = totalCount, totalChapters = totalCount,
@@ -117,51 +117,51 @@ object MangaMapper {
fun mapMangaWithChapterCount( fun mapMangaWithChapterCount(
id: Long, id: Long,
source: Long, sourceId: Long,
url: String, remoteUrl: String,
artist: String?, remoteTitle: String,
author: String?, remoteAuthor: String?,
description: String?, remoteArtist: String?,
genre: List<String>?, remoteDescription: String?,
title: String, remoteGenre: List<String>?,
status: Long, remoteStatus: Long,
thumbnailUrl: String?, remoteCover: String?,
lastUpdate: Long?, remoteUpdateStrategy: UpdateStrategy,
nextUpdate: Long?, remoteMemo: JsonObject,
initialized: Boolean, userFavoriteAt: Long?,
viewerFlags: Long, userNotes: String,
chapterFlags: Long, userReaderFlags: Long,
coverLastModified: Long, userChapterFlags: Long,
updateStrategy: UpdateStrategy, stateChapterLastUpdate: Long?,
calculateInterval: Long, stateChapterNextUpdate: Long?,
notes: String, stateChapterFetchInterval: Long,
memo: JsonObject, stateCoverLastModified: Long,
favoriteAt: Long?, stateInitialized: Boolean,
totalCount: Long, chapterCount: Long,
): MangaWithChapterCount = MangaWithChapterCount( ): MangaWithChapterCount = MangaWithChapterCount(
manga = mapManga( manga = mapManga(
id = id, id = id,
source = source, sourceId = sourceId,
url = url, remoteUrl = remoteUrl,
artist = artist, remoteTitle = remoteTitle,
author = author, remoteAuthor = remoteAuthor,
description = description, remoteArtist = remoteArtist,
genre = genre, remoteDescription = remoteDescription,
title = title, remoteGenre = remoteGenre,
status = status, remoteStatus = remoteStatus,
thumbnailUrl = thumbnailUrl, remoteCover = remoteCover,
lastUpdate = lastUpdate, remoteUpdateStrategy = remoteUpdateStrategy,
nextUpdate = nextUpdate, remoteMemo = remoteMemo,
initialized = initialized, userFavoriteAt = userFavoriteAt,
viewerFlags = viewerFlags, userNotes = userNotes,
chapterFlags = chapterFlags, userReaderFlags = userReaderFlags,
coverLastModified = coverLastModified, userChapterFlags = userChapterFlags,
updateStrategy = updateStrategy, stateChapterLastUpdate = stateChapterLastUpdate,
calculateInterval = calculateInterval, stateChapterNextUpdate = stateChapterNextUpdate,
notes = notes, stateChapterFetchInterval = stateChapterFetchInterval,
memo = memo, stateCoverLastModified = stateCoverLastModified,
favoriteAt = favoriteAt, stateInitialized = stateInitialized,
), ),
chapterCount = totalCount, chapterCount = chapterCount,
) )
} }
@@ -48,13 +48,13 @@ class MangaRepositoryImpl(
override suspend fun getMangaByUrlAndSourceId(url: String, sourceId: Long): Manga? { override suspend fun getMangaByUrlAndSourceId(url: String, sourceId: Long): Manga? {
return database.mangaQueries return database.mangaQueries
.getMangaByUrlAndSource(url, sourceId, MangaMapper::mapManga) .getMangaByUrlAndSource(sourceId = sourceId, remoteUrl = url, mapper = MangaMapper::mapManga)
.awaitAsOneOrNull() .awaitAsOneOrNull()
} }
override fun getMangaByUrlAndSourceIdAsFlow(url: String, sourceId: Long): Flow<Manga?> { override fun getMangaByUrlAndSourceIdAsFlow(url: String, sourceId: Long): Flow<Manga?> {
return database.mangaQueries return database.mangaQueries
.getMangaByUrlAndSource(url, sourceId, MangaMapper::mapManga) .getMangaByUrlAndSource(sourceId = sourceId, remoteUrl = url, mapper = MangaMapper::mapManga)
.subscribeToOneOrNull() .subscribeToOneOrNull()
} }
@@ -161,7 +161,11 @@ class MangaRepositoryImpl(
excludedScanlators.minus(current).forEach { scanlator -> excludedScanlators.minus(current).forEach { scanlator ->
database.excluded_scanlatorQueries.insert(mangaId, scanlator) database.excluded_scanlatorQueries.insert(mangaId, scanlator)
} }
database.excluded_scanlatorQueries.remove(mangaId, current.minus(excludedScanlators)) val toRemove = current.minus(excludedScanlators)
// An empty delete still tells every chapter query to run again
if (toRemove.isNotEmpty()) {
database.excluded_scanlatorQueries.remove(mangaId, toRemove)
}
} }
} }
@@ -189,25 +193,25 @@ class MangaRepositoryImpl(
return database.transactionWithResult { return database.transactionWithResult {
manga.map { manga.map {
database.mangaQueries.insertNetworkManga( database.mangaQueries.insertNetworkManga(
source = it.source, sourceId = it.source,
url = it.url, remoteUrl = it.url,
artist = it.artist, remoteArtist = it.artist,
author = it.author, remoteAuthor = it.author,
description = it.description, remoteDescription = it.description,
genre = it.genre, remoteGenre = it.genre,
title = it.title, remoteTitle = it.title,
status = it.status, remoteStatus = it.status,
thumbnailUrl = it.thumbnailUrl, remoteCover = it.thumbnailUrl,
favoriteAt = it.favoriteAt, userFavoriteAt = it.favoriteAt,
lastUpdate = it.lastUpdate, stateChapterLastUpdate = it.lastUpdate,
nextUpdate = it.nextUpdate, stateChapterNextUpdate = it.nextUpdate,
calculateInterval = it.fetchInterval.toLong(), stateChapterFetchInterval = it.fetchInterval.toLong(),
initialized = it.initialized, stateInitialized = it.initialized,
viewerFlags = it.viewerFlags, userReaderFlags = it.viewerFlags,
chapterFlags = it.chapterFlags, userChapterFlags = it.chapterFlags,
coverLastModified = it.coverLastModified, stateCoverLastModified = it.coverLastModified,
updateStrategy = it.updateStrategy, remoteUpdateStrategy = it.updateStrategy,
memo = it.memo, remoteMemo = it.memo,
updateTitle = it.title.isNotBlank(), updateTitle = it.title.isNotBlank(),
updateCover = !it.thumbnailUrl.isNullOrBlank(), updateCover = !it.thumbnailUrl.isNullOrBlank(),
updateDetails = it.initialized, updateDetails = it.initialized,
@@ -221,18 +225,18 @@ class MangaRepositoryImpl(
override suspend fun updateRemote(update: MangaRemoteUpdate): Boolean { override suspend fun updateRemote(update: MangaRemoteUpdate): Boolean {
return try { return try {
database.mangaQueries.updateRemote( database.mangaQueries.updateRemote(
artist = update.artist, remoteArtist = update.artist,
author = update.author, remoteAuthor = update.author,
description = update.description, remoteDescription = update.description,
genre = update.genre, remoteGenre = update.genre,
title = update.title, remoteTitle = update.title,
status = update.status, remoteStatus = update.status,
thumbnailUrl = update.thumbnailUrl, remoteCover = update.thumbnailUrl,
initialized = update.initialized, stateInitialized = update.initialized,
coverLastModified = update.coverLastModified, stateCoverLastModified = update.coverLastModified,
updateStrategy = update.updateStrategy, remoteUpdateStrategy = update.updateStrategy,
memo = update.memo, remoteMemo = update.memo,
mangaId = update.id, id = update.id,
) )
true true
} catch (e: Exception) { } catch (e: Exception) {
@@ -246,16 +250,16 @@ class MangaRepositoryImpl(
mangaUpdates.forEach { value -> mangaUpdates.forEach { value ->
with(value) { with(value) {
database.mangaQueries.update( database.mangaQueries.update(
favoriteAtSet = isSet(::favoriteAt), userFavoriteAtSet = isSet(::favoriteAt),
favoriteAt = favoriteAt, userFavoriteAt = favoriteAt,
lastUpdate = lastUpdate, stateChapterLastUpdate = lastUpdate,
nextUpdate = nextUpdate, stateChapterNextUpdate = nextUpdate,
calculateInterval = fetchInterval?.toLong(), stateChapterFetchInterval = fetchInterval?.toLong(),
viewer = viewerFlags, userReaderFlags = viewerFlags,
chapterFlags = chapterFlags, userChapterFlags = chapterFlags,
coverLastModified = coverLastModified, stateCoverLastModified = coverLastModified,
mangaId = id, id = id,
notes = notes, userNotes = notes,
) )
} }
} }
@@ -31,12 +31,12 @@ class StubSourceRepositoryImpl(
} }
override suspend fun upsertStubSource(id: Long, lang: String, name: String) { override suspend fun upsertStubSource(id: Long, lang: String, name: String) {
database.sourceQueries.upsert(id = id, lang = lang, name = name) database.sourceQueries.upsert(id = id, name = name, language = lang)
} }
private fun mapStubSource( private fun mapStubSource(
id: Long, id: Long,
lang: String,
name: String, name: String,
lang: String,
): StubSource = StubSource(id = id, lang = lang, name = name) ): StubSource = StubSource(id = id, lang = lang, name = name)
} }
@@ -6,22 +6,22 @@ object TrackMapper {
fun mapTrack( fun mapTrack(
id: Long, id: Long,
mangaId: Long, mangaId: Long,
syncId: Long, trackerId: Long,
remoteId: Long, remoteId: Long,
remoteUrl: String,
libraryId: Long?, libraryId: Long?,
title: String, title: String,
lastChapterRead: Double, lastChapterRead: Double,
totalChapters: Long, totalChapters: Long,
status: Long, status: Long,
score: Double, score: Double,
remoteUrl: String,
startDate: Long, startDate: Long,
finishDate: Long, finishDate: Long,
private: Boolean, private: Boolean,
): Track = Track( ): Track = Track(
id = id, id = id,
mangaId = mangaId, mangaId = mangaId,
trackerId = syncId, trackerId = trackerId,
remoteId = remoteId, remoteId = remoteId,
libraryId = libraryId, libraryId = libraryId,
title = title, title = title,
@@ -46,7 +46,7 @@ class TrackRepositoryImpl(
override suspend fun delete(mangaId: Long, trackerId: Long) { override suspend fun delete(mangaId: Long, trackerId: Long) {
database.manga_trackQueries.delete( database.manga_trackQueries.delete(
mangaId = mangaId, mangaId = mangaId,
syncId = trackerId, trackerId = trackerId,
) )
} }
@@ -63,7 +63,7 @@ class TrackRepositoryImpl(
tracks.forEach { mangaTrack -> tracks.forEach { mangaTrack ->
database.manga_trackQueries.upsert( database.manga_trackQueries.upsert(
mangaId = mangaTrack.mangaId, mangaId = mangaTrack.mangaId,
syncId = mangaTrack.trackerId, trackerId = mangaTrack.trackerId,
remoteId = mangaTrack.remoteId, remoteId = mangaTrack.remoteId,
libraryId = mangaTrack.libraryId, libraryId = mangaTrack.libraryId,
title = mangaTrack.title, title = mangaTrack.title,
@@ -1,5 +1,6 @@
package tachiyomi.data.updates package tachiyomi.data.updates
import app.cash.sqldelight.Query
import app.cash.sqldelight.async.coroutines.awaitAsList import app.cash.sqldelight.async.coroutines.awaitAsList
import dev.zacsweers.metro.AppScope import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.ContributesBinding import dev.zacsweers.metro.ContributesBinding
@@ -25,14 +26,7 @@ class UpdatesRepositoryImpl(
after: Long, after: Long,
limit: Long, limit: Long,
): List<UpdatesWithRelations> { ): List<UpdatesWithRelations> {
return database.updatesViewQueries return getUpdatesByReadStatus(read = read, after = after, limit = limit).awaitAsList()
.getUpdatesByReadStatus(
read = read,
after = after,
limit = limit,
mapper = ::mapUpdatesWithRelations,
)
.awaitAsList()
} }
override fun subscribeAll( override fun subscribeAll(
@@ -67,14 +61,23 @@ class UpdatesRepositoryImpl(
after: Long, after: Long,
limit: Long, limit: Long,
): Flow<List<UpdatesWithRelations>> { ): Flow<List<UpdatesWithRelations>> {
return database.updatesViewQueries return getUpdatesByReadStatus(read = read, after = after, limit = limit).subscribeToList()
.getUpdatesByReadStatus( }
read = read,
private fun getUpdatesByReadStatus(read: Boolean, after: Long, limit: Long): Query<UpdatesWithRelations> {
return if (read) {
database.updatesViewQueries.getReadUpdates(
after = after, after = after,
limit = limit, limit = limit,
mapper = ::mapUpdatesWithRelations, mapper = ::mapUpdatesWithRelations,
) )
.subscribeToList() } else {
database.updatesViewQueries.getUnreadUpdates(
after = after,
limit = limit,
mapper = ::mapUpdatesWithRelations,
)
}
} }
@Suppress("UNUSED_PARAMETER") @Suppress("UNUSED_PARAMETER")
@@ -19,24 +19,15 @@ END;
getCategory: getCategory:
SELECT id, name, `order`, flags SELECT id, name, `order`, flags
FROM category FROM category
WHERE id = :id WHERE id = :id;
LIMIT 1;
getCategories: getCategories:
SELECT SELECT id, name, `order`, flags
id,
name,
`order`,
flags
FROM category FROM category
ORDER BY `order`; ORDER BY `order`;
getCategoriesByMangaId: getCategoriesByMangaId:
SELECT SELECT C.id, C.name, C.`order`, C.flags
C.id,
C.name,
C.`order`,
C.flags
FROM category C FROM category C
JOIN manga_category MC JOIN manga_category MC
ON C.id = MC.category_id ON C.id = MC.category_id
@@ -44,18 +35,19 @@ WHERE MC.manga_id = :mangaId
ORDER BY C.`order`; ORDER BY C.`order`;
insert: insert:
-- The position is taken inside the insert, so two categories created at once cannot claim the same one
INSERT INTO category(name, `order`, flags) INSERT INTO category(name, `order`, flags)
SELECT :name, coalesce(max(`order`), -1) + 1, :flags SELECT :name, coalesce(max(`order`), -1) + 1, :flags
FROM category; FROM category;
delete: delete:
DELETE FROM category DELETE FROM category
WHERE id = :categoryId; WHERE id = :id;
updateName: updateName:
UPDATE category UPDATE category
SET name = :name SET name = :name
WHERE id = :categoryId; WHERE id = :id;
getUserCategoryIds: getUserCategoryIds:
SELECT id SELECT id
@@ -66,13 +58,13 @@ ORDER BY `order`;
updateOrder: updateOrder:
UPDATE category UPDATE category
SET `order` = :order SET `order` = :order
WHERE id = :categoryId; WHERE id = :id;
updateFlags: updateFlags:
UPDATE category UPDATE category
SET flags = :flags SET flags = :flags
WHERE id = :categoryId; WHERE id = :id;
updateAllFlags: updateAllFlags:
UPDATE category SET UPDATE category
flags = coalesce(:flags, flags); SET flags = :flags;
@@ -25,19 +25,17 @@ CREATE INDEX chapter_unread_state_date_fetch_index ON chapter(state_date_fetch)
getChapterById: getChapterById:
SELECT SELECT
id, manga_id, remote_url, remote_name, remote_scanlator, id, manga_id, remote_url, remote_name, remote_scanlator, remote_chapter_number,
user_read, user_bookmark, user_last_page_read, remote_chapter_number, remote_order, remote_date_upload, remote_order, remote_memo, user_read, user_bookmark,
state_date_fetch, remote_date_upload, user_last_page_read, state_date_fetch
remote_memo
FROM chapter FROM chapter
WHERE id = :id; WHERE id = :id;
getChaptersByMangaId: getChaptersByMangaId:
SELECT SELECT
C.id, C.manga_id, C.remote_url, C.remote_name, C.remote_scanlator, C.id, C.manga_id, C.remote_url, C.remote_name, C.remote_scanlator, C.remote_chapter_number,
C.user_read, C.user_bookmark, C.user_last_page_read, C.remote_chapter_number, C.remote_order, C.remote_date_upload, C.remote_order, C.remote_memo, C.user_read, C.user_bookmark,
C.state_date_fetch, C.remote_date_upload, C.user_last_page_read, C.state_date_fetch
C.remote_memo
FROM chapter C FROM chapter C
LEFT JOIN excluded_scanlator ES LEFT JOIN excluded_scanlator ES
ON C.manga_id = ES.manga_id ON C.manga_id = ES.manga_id
@@ -49,75 +47,70 @@ AND (
); );
getScanlatorsByMangaId: getScanlatorsByMangaId:
SELECT remote_scanlator SELECT DISTINCT remote_scanlator
FROM chapter FROM chapter
WHERE manga_id = :mangaId; WHERE manga_id = :mangaId;
getBookmarkedChaptersByMangaId: getBookmarkedChaptersByMangaId:
SELECT SELECT
id, manga_id, remote_url, remote_name, remote_scanlator, id, manga_id, remote_url, remote_name, remote_scanlator, remote_chapter_number,
user_read, user_bookmark, user_last_page_read, remote_chapter_number, remote_order, remote_date_upload, remote_order, remote_memo, user_read, user_bookmark,
state_date_fetch, remote_date_upload, user_last_page_read, state_date_fetch
remote_memo
FROM chapter FROM chapter
WHERE user_bookmark WHERE manga_id = :mangaId
AND manga_id = :mangaId; AND user_bookmark;
getChapterByUrl:
SELECT
id, manga_id, remote_url, remote_name, remote_scanlator,
user_read, user_bookmark, user_last_page_read, remote_chapter_number, remote_order,
state_date_fetch, remote_date_upload,
remote_memo
FROM chapter
WHERE remote_url = :chapterUrl;
getChapterByUrlAndMangaId: getChapterByUrlAndMangaId:
SELECT SELECT
id, manga_id, remote_url, remote_name, remote_scanlator, id, manga_id, remote_url, remote_name, remote_scanlator, remote_chapter_number,
user_read, user_bookmark, user_last_page_read, remote_chapter_number, remote_order, remote_date_upload, remote_order, remote_memo, user_read, user_bookmark,
state_date_fetch, remote_date_upload, user_last_page_read, state_date_fetch
remote_memo
FROM chapter FROM chapter
WHERE remote_url = :chapterUrl WHERE manga_id = :mangaId
AND manga_id = :mangaId AND remote_url = :remoteUrl;
LIMIT 1;
removeChaptersWithIds: removeChaptersWithIds:
DELETE FROM chapter DELETE FROM chapter
WHERE id IN :chapterIds; WHERE id IN :chapterIds;
insert:
INSERT INTO chapter(manga_id, remote_url, remote_name, remote_scanlator, user_read, user_bookmark, user_last_page_read, remote_chapter_number, remote_order, state_date_fetch, remote_date_upload, remote_memo)
VALUES (:mangaId, :url, :name, :scanlator, :read, :bookmark, :lastPageRead, :chapterNumber, :sourceOrder, :dateFetch, :dateUpload, :memo);
insertReturningId: insertReturningId:
INSERT INTO chapter(manga_id, remote_url, remote_name, remote_scanlator, user_read, user_bookmark, user_last_page_read, remote_chapter_number, remote_order, state_date_fetch, remote_date_upload, remote_memo) INSERT INTO chapter(
VALUES (:mangaId, :url, :name, :scanlator, :read, :bookmark, :lastPageRead, :chapterNumber, :sourceOrder, :dateFetch, :dateUpload, :memo) manga_id, remote_url, remote_name, remote_scanlator, remote_chapter_number,
remote_date_upload, remote_order, remote_memo, user_read, user_bookmark,
user_last_page_read, state_date_fetch
)
VALUES (
:mangaId, :remoteUrl, :remoteName, :remoteScanlator, :remoteChapterNumber,
:remoteDateUpload, :remoteOrder, :remoteMemo, :userRead, :userBookmark,
:userLastPageRead, :stateDateFetch
)
-- Two syncs of the same entry can race; the chapter the other one already stored is skipped rather
-- than failing the whole batch
ON CONFLICT(manga_id, remote_url) DO NOTHING
RETURNING id; RETURNING id;
update: update:
UPDATE chapter UPDATE chapter
SET user_read = coalesce(:read, user_read), SET user_read = coalesce(:userRead, user_read),
user_bookmark = coalesce(:bookmark, user_bookmark), user_bookmark = coalesce(:userBookmark, user_bookmark),
user_last_page_read = coalesce(:lastPageRead, user_last_page_read), user_last_page_read = coalesce(:userLastPageRead, user_last_page_read),
state_date_fetch = coalesce(:dateFetch, state_date_fetch) state_date_fetch = coalesce(:stateDateFetch, state_date_fetch)
WHERE id = :chapterId; WHERE id = :id;
updateFromBackup: updateFromBackup:
UPDATE chapter UPDATE chapter
SET user_read = coalesce(:read, user_read), SET user_read = coalesce(:userRead, user_read),
user_bookmark = coalesce(:bookmark, user_bookmark), user_bookmark = coalesce(:userBookmark, user_bookmark),
user_last_page_read = coalesce(:lastPageRead, user_last_page_read), user_last_page_read = coalesce(:userLastPageRead, user_last_page_read),
remote_memo = coalesce(:memo, remote_memo) remote_memo = coalesce(:remoteMemo, remote_memo)
WHERE id = :chapterId; WHERE id = :id;
updateRemote: updateRemote:
UPDATE chapter UPDATE chapter
SET remote_name = :name, SET remote_name = :remoteName,
remote_scanlator = coalesce(:scanlator, remote_scanlator), remote_scanlator = coalesce(:remoteScanlator, remote_scanlator),
remote_chapter_number = :chapterNumber, remote_chapter_number = :remoteChapterNumber,
remote_order = :sourceOrder, remote_order = :remoteOrder,
remote_date_upload = coalesce(:dateUpload, remote_date_upload), remote_date_upload = coalesce(:remoteDateUpload, remote_date_upload),
remote_memo = :memo remote_memo = :remoteMemo
WHERE id = :chapterId; WHERE id = :id;
@@ -9,7 +9,8 @@ CREATE TABLE excluded_scanlator(
insert: insert:
INSERT INTO excluded_scanlator(manga_id, scanlator) INSERT INTO excluded_scanlator(manga_id, scanlator)
VALUES (:mangaId, :scanlator); VALUES (:mangaId, :scanlator)
ON CONFLICT(manga_id, scanlator) DO NOTHING;
remove: remove:
DELETE FROM excluded_scanlator DELETE FROM excluded_scanlator
@@ -21,23 +21,26 @@ SELECT
FROM extension_store; FROM extension_store;
getCount: getCount:
SELECT COUNT(*) SELECT count(*)
FROM extension_store; FROM extension_store;
upsert: upsert:
INSERT INTO extension_store(index_url, name, badge_label, signing_key, contact_website, contact_discord, is_legacy, extension_list_url) INSERT INTO extension_store(
VALUES (:indexUrl, :name, :badgeLabel, :signingKey, :contactWebsite, :contactDiscord, :isLegacy, :extensionListUrl) index_url, name, badge_label, signing_key, contact_website, contact_discord, is_legacy,
ON CONFLICT(index_url) extension_list_url
DO UPDATE )
SET VALUES (
name = :name, :indexUrl, :name, :badgeLabel, :signingKey, :contactWebsite, :contactDiscord, :isLegacy,
badge_label = :badgeLabel, :extensionListUrl
signing_key =: signingKey, )
contact_website = :contactWebsite, ON CONFLICT(index_url) DO UPDATE SET
contact_discord = :contactDiscord, name = excluded.name,
is_legacy = :isLegacy, badge_label = excluded.badge_label,
extension_list_url = :extensionListUrl signing_key = excluded.signing_key,
WHERE index_url = :indexUrl; contact_website = excluded.contact_website,
contact_discord = excluded.contact_discord,
is_legacy = excluded.is_legacy,
extension_list_url = excluded.extension_list_url;
delete: delete:
DELETE FROM extension_store DELETE FROM extension_store
@@ -16,30 +16,14 @@ CREATE INDEX history_manga_id_index ON history(manga_id);
CREATE INDEX history_read_at_index ON history(read_at); CREATE INDEX history_read_at_index ON history(read_at);
getHistoryByMangaId: getHistoryByMangaId:
SELECT SELECT id, chapter_id, read_at, read_duration
H.id, FROM history
H.chapter_id, WHERE manga_id = :mangaId;
H.read_at,
H.read_duration
FROM history H
WHERE H.manga_id = :mangaId;
getHistoryByChapterUrlAndMangaId:
SELECT
H.id,
H.chapter_id,
H.read_at,
H.read_duration
FROM history H
JOIN chapter C
ON H.chapter_id = C.id
WHERE C.remote_url = :chapterUrl AND C.manga_id = :mangaId
LIMIT 1;
resetHistoryById: resetHistoryById:
UPDATE history UPDATE history
SET read_at = 0 SET read_at = 0
WHERE id = :historyId; WHERE id = :id;
resetHistoryByMangaId: resetHistoryByMangaId:
UPDATE history UPDATE history
@@ -55,15 +39,12 @@ WHERE read_at = 0;
upsert: upsert:
INSERT INTO history(manga_id, chapter_id, read_at, read_duration) INSERT INTO history(manga_id, chapter_id, read_at, read_duration)
SELECT C.manga_id, :chapterId, :readAt, :readDuration SELECT manga_id, id, :readAt, :readDuration
FROM chapter C FROM chapter
WHERE C.id = :chapterId WHERE id = :chapterId
ON CONFLICT(chapter_id) ON CONFLICT(chapter_id) DO UPDATE SET
DO UPDATE read_at = excluded.read_at,
SET read_duration = read_duration + excluded.read_duration;
read_at = :readAt,
read_duration = read_duration + :readDuration
WHERE chapter_id = :chapterId;
getReadDuration: getReadDuration:
SELECT coalesce(sum(read_duration), 0) SELECT coalesce(sum(read_duration), 0)
+162 -166
View File
@@ -33,73 +33,68 @@ CREATE INDEX manga_user_favorite_at_index ON manga(user_favorite_at) WHERE user_
getMangaById: getMangaById:
SELECT SELECT
id, source_id, remote_url, remote_artist, remote_author, remote_description, remote_genre, remote_title, id, source_id, remote_url, remote_title, remote_author, remote_artist, remote_description,
remote_status, remote_cover, state_chapter_last_update, state_chapter_next_update, state_initialized, remote_genre, remote_status, remote_cover, remote_update_strategy, remote_memo,
user_reader_flags, user_chapter_flags, state_cover_last_modified, remote_update_strategy, user_favorite_at, user_notes, user_reader_flags, user_chapter_flags,
state_chapter_fetch_interval, user_notes, remote_memo, user_favorite_at state_chapter_last_update, state_chapter_next_update, state_chapter_fetch_interval,
state_cover_last_modified, state_initialized
FROM manga FROM manga
WHERE id = :id; WHERE id = :id;
-- TODO: this should ideally never really have more than 1 result
getMangaByUrlAndSource: getMangaByUrlAndSource:
SELECT SELECT
id, source_id, remote_url, remote_artist, remote_author, remote_description, remote_genre, remote_title, id, source_id, remote_url, remote_title, remote_author, remote_artist, remote_description,
remote_status, remote_cover, state_chapter_last_update, state_chapter_next_update, state_initialized, remote_genre, remote_status, remote_cover, remote_update_strategy, remote_memo,
user_reader_flags, user_chapter_flags, state_cover_last_modified, remote_update_strategy, user_favorite_at, user_notes, user_reader_flags, user_chapter_flags,
state_chapter_fetch_interval, user_notes, remote_memo, user_favorite_at state_chapter_last_update, state_chapter_next_update, state_chapter_fetch_interval,
state_cover_last_modified, state_initialized
FROM manga FROM manga
WHERE remote_url = :url WHERE source_id = :sourceId
AND source_id = :source AND remote_url = :remoteUrl;
LIMIT 1;
getFavorites: getFavorites:
SELECT SELECT
id, source_id, remote_url, remote_artist, remote_author, remote_description, remote_genre, remote_title, id, source_id, remote_url, remote_title, remote_author, remote_artist, remote_description,
remote_status, remote_cover, state_chapter_last_update, state_chapter_next_update, state_initialized, remote_genre, remote_status, remote_cover, remote_update_strategy, remote_memo,
user_reader_flags, user_chapter_flags, state_cover_last_modified, remote_update_strategy, user_favorite_at, user_notes, user_reader_flags, user_chapter_flags,
state_chapter_fetch_interval, user_notes, remote_memo, user_favorite_at state_chapter_last_update, state_chapter_next_update, state_chapter_fetch_interval,
state_cover_last_modified, state_initialized
FROM manga FROM manga
WHERE user_favorite_at IS NOT NULL; WHERE user_favorite_at IS NOT NULL;
getReadMangaNotInLibrary: getReadMangaNotInLibrary:
SELECT SELECT
id, source_id, remote_url, remote_artist, remote_author, remote_description, remote_genre, remote_title, id, source_id, remote_url, remote_title, remote_author, remote_artist, remote_description,
remote_status, remote_cover, state_chapter_last_update, state_chapter_next_update, state_initialized, remote_genre, remote_status, remote_cover, remote_update_strategy, remote_memo,
user_reader_flags, user_chapter_flags, state_cover_last_modified, remote_update_strategy, user_favorite_at, user_notes, user_reader_flags, user_chapter_flags,
state_chapter_fetch_interval, user_notes, remote_memo, user_favorite_at state_chapter_last_update, state_chapter_next_update, state_chapter_fetch_interval,
state_cover_last_modified, state_initialized
FROM manga FROM manga
WHERE user_favorite_at IS NULL AND id IN ( WHERE user_favorite_at IS NULL
SELECT DISTINCT chapter.manga_id AND EXISTS (
SELECT 1
FROM chapter FROM chapter
WHERE user_read = 1 OR user_last_page_read != 0 WHERE chapter.manga_id = manga.id
AND (user_read = 1 OR user_last_page_read != 0)
); );
getAllManga:
SELECT
id, source_id, remote_url, remote_artist, remote_author, remote_description, remote_genre, remote_title,
remote_status, remote_cover, state_chapter_last_update, state_chapter_next_update, state_initialized,
user_reader_flags, user_chapter_flags, state_cover_last_modified, remote_update_strategy,
state_chapter_fetch_interval, user_notes, remote_memo, user_favorite_at
FROM manga;
getAllMangaSourceAndUrl: getAllMangaSourceAndUrl:
SELECT source_id, remote_url SELECT source_id, remote_url
FROM manga; FROM manga;
getSourceIdWithFavoriteCount: getSourceIdWithFavoriteCount:
SELECT SELECT source_id, count(*)
source_id,
count(*)
FROM manga FROM manga
WHERE user_favorite_at IS NOT NULL WHERE user_favorite_at IS NOT NULL
GROUP BY source_id; GROUP BY source_id;
getFavoriteBySourceId: getFavoriteBySourceId:
SELECT SELECT
id, source_id, remote_url, remote_artist, remote_author, remote_description, remote_genre, remote_title, id, source_id, remote_url, remote_title, remote_author, remote_artist, remote_description,
remote_status, remote_cover, state_chapter_last_update, state_chapter_next_update, state_initialized, remote_genre, remote_status, remote_cover, remote_update_strategy, remote_memo,
user_reader_flags, user_chapter_flags, state_cover_last_modified, remote_update_strategy, user_favorite_at, user_notes, user_reader_flags, user_chapter_flags,
state_chapter_fetch_interval, user_notes, remote_memo, user_favorite_at state_chapter_last_update, state_chapter_next_update, state_chapter_fetch_interval,
state_cover_last_modified, state_initialized
FROM manga FROM manga
WHERE user_favorite_at IS NOT NULL WHERE user_favorite_at IS NOT NULL
AND source_id = :sourceId; AND source_id = :sourceId;
@@ -107,57 +102,43 @@ AND source_id = :sourceId;
getDuplicateLibraryManga: getDuplicateLibraryManga:
WITH WITH
track_dupes AS ( track_dupes AS (
SELECT DISTINCT S2.manga_id SELECT DISTINCT T2.manga_id
FROM manga_track S1 FROM manga_track T1
INNER JOIN manga_track S2 JOIN manga_track T2
ON S1.tracker_id = S2.tracker_id ON T1.tracker_id = T2.tracker_id
AND S1.remote_id = S2.remote_id AND T1.remote_id = T2.remote_id
AND S1.remote_id != 0 AND T1.remote_id != 0
AND S1.manga_id != S2.manga_id AND T1.manga_id != T2.manga_id
WHERE S1.manga_id = :id WHERE T1.manga_id = :id
),
duplicates AS (
SELECT
M.id, M.source_id, M.remote_url, M.remote_artist, M.remote_author, M.remote_description, M.remote_genre, M.remote_title,
M.remote_status, M.remote_cover, M.state_chapter_last_update, M.state_chapter_next_update, M.state_initialized,
M.user_reader_flags, M.user_chapter_flags, M.state_cover_last_modified, M.remote_update_strategy,
M.state_chapter_fetch_interval, M.user_notes, M.remote_memo, M.user_favorite_at
FROM manga M
LEFT JOIN track_dupes D
ON D.manga_id = id
WHERE user_favorite_at IS NOT NULL
AND id != :id
AND (lower(remote_title) LIKE '%' || lower(:title) || '%' OR D.manga_id IS NOT NULL)
),
chapter_counts AS (
SELECT
M.id AS manga_id,
count(*) AS chapter_count
FROM duplicates M
JOIN chapter C
ON M.id = C.manga_id
LEFT JOIN excluded_scanlator ES
ON C.manga_id = ES.manga_id
AND C.remote_scanlator = ES.scanlator
WHERE ES.scanlator IS NULL
GROUP BY M.id
) )
SELECT SELECT
M.id, M.source_id, M.remote_url, M.remote_artist, M.remote_author, M.remote_description, M.remote_genre, M.remote_title, M.id, M.source_id, M.remote_url, M.remote_title, M.remote_author, M.remote_artist,
M.remote_status, M.remote_cover, M.state_chapter_last_update, M.state_chapter_next_update, M.state_initialized, M.remote_description, M.remote_genre, M.remote_status, M.remote_cover,
M.user_reader_flags, M.user_chapter_flags, M.state_cover_last_modified, M.remote_update_strategy, M.remote_update_strategy, M.remote_memo, M.user_favorite_at, M.user_notes,
M.state_chapter_fetch_interval, M.user_notes, M.remote_memo, M.user_favorite_at, M.user_reader_flags, M.user_chapter_flags, M.state_chapter_last_update,
coalesce(CC.chapter_count, 0) AS chapter_count M.state_chapter_next_update, M.state_chapter_fetch_interval,
FROM duplicates M M.state_cover_last_modified, M.state_initialized,
LEFT JOIN chapter_counts CC (
ON M.id = CC.manga_id; SELECT count(*)
FROM chapter C
LEFT JOIN excluded_scanlator ES
ON C.manga_id = ES.manga_id
AND C.remote_scanlator = ES.scanlator
WHERE C.manga_id = M.id
AND ES.scanlator IS NULL
) AS chapter_count
FROM manga M
WHERE M.user_favorite_at IS NOT NULL
AND M.id != :id
AND (lower(M.remote_title) LIKE '%' || lower(:remoteTitle) || '%' OR M.id IN (SELECT manga_id FROM track_dupes));
getUpcomingManga: getUpcomingManga:
SELECT SELECT
id, source_id, remote_url, remote_artist, remote_author, remote_description, remote_genre, remote_title, id, source_id, remote_url, remote_title, remote_author, remote_artist, remote_description,
remote_status, remote_cover, state_chapter_last_update, state_chapter_next_update, state_initialized, remote_genre, remote_status, remote_cover, remote_update_strategy, remote_memo,
user_reader_flags, user_chapter_flags, state_cover_last_modified, remote_update_strategy, user_favorite_at, user_notes, user_reader_flags, user_chapter_flags,
state_chapter_fetch_interval, user_notes, remote_memo, user_favorite_at state_chapter_last_update, state_chapter_next_update, state_chapter_fetch_interval,
state_cover_last_modified, state_initialized
FROM manga FROM manga
WHERE state_chapter_next_update >= :startOfDay WHERE state_chapter_next_update >= :startOfDay
AND user_favorite_at IS NOT NULL AND user_favorite_at IS NOT NULL
@@ -168,7 +149,7 @@ AND (
OR EXISTS ( OR EXISTS (
SELECT 1 FROM manga_category SELECT 1 FROM manga_category
WHERE manga_category.manga_id = manga.id WHERE manga_category.manga_id = manga.id
AND COALESCE(manga_category.category_id, 0) IN :includedCategories AND manga_category.category_id IN :includedCategories
) )
OR (0 IN :includedCategories AND NOT EXISTS ( OR (0 IN :includedCategories AND NOT EXISTS (
SELECT 1 FROM manga_category WHERE manga_category.manga_id = manga.id) SELECT 1 FROM manga_category WHERE manga_category.manga_id = manga.id)
@@ -181,7 +162,7 @@ AND (
NOT EXISTS ( NOT EXISTS (
SELECT 1 FROM manga_category SELECT 1 FROM manga_category
WHERE manga_category.manga_id = manga.id WHERE manga_category.manga_id = manga.id
AND COALESCE(manga_category.category_id, 0) IN :excludedCategories AND manga_category.category_id IN :excludedCategories
) )
AND ( AND (
0 NOT IN :excludedCategories 0 NOT IN :excludedCategories
@@ -193,10 +174,11 @@ ORDER BY state_chapter_next_update ASC;
resetViewerFlags: resetViewerFlags:
UPDATE manga UPDATE manga
SET user_reader_flags = 0; SET user_reader_flags = 0
WHERE user_reader_flags != 0;
getSourceIdsWithNonLibraryManga: getSourceIdsWithNonLibraryManga:
SELECT source_id, COUNT(*) AS manga_count SELECT source_id, count(*) AS manga_count
FROM manga FROM manga
WHERE user_favorite_at IS NULL WHERE user_favorite_at IS NULL
GROUP BY source_id; GROUP BY source_id;
@@ -207,105 +189,119 @@ WHERE user_favorite_at IS NULL
AND source_id IN :sourceIds AND source_id IN :sourceIds
AND ( AND (
:keepReadManga = 0 :keepReadManga = 0
OR id NOT IN ( OR NOT EXISTS (
SELECT DISTINCT manga_id SELECT 1
FROM chapter FROM chapter
WHERE user_read = 1 WHERE chapter.manga_id = manga.id
OR user_last_page_read != 0 AND (user_read = 1 OR user_last_page_read != 0)
) )
); );
insertReturningId: insertReturningId:
INSERT INTO manga(source_id, remote_url, remote_artist, remote_author, remote_description, remote_genre, remote_title, remote_status, remote_cover, user_favorite_at, state_chapter_last_update, state_chapter_next_update, state_initialized, user_reader_flags, user_chapter_flags, state_cover_last_modified, remote_update_strategy, state_chapter_fetch_interval, user_notes, remote_memo) INSERT INTO manga(
VALUES (:source, :url, :artist, :author, :description, :genre, :title, :status, :thumbnailUrl, :favoriteAt, :lastUpdate, :nextUpdate, :initialized, :viewerFlags, :chapterFlags, :coverLastModified, :updateStrategy, :calculateInterval, :notes, :memo) source_id, remote_url, remote_title, remote_author, remote_artist, remote_description,
remote_genre, remote_status, remote_cover, remote_update_strategy, remote_memo,
user_favorite_at, user_notes, user_reader_flags, user_chapter_flags,
state_chapter_last_update, state_chapter_next_update, state_chapter_fetch_interval,
state_cover_last_modified, state_initialized
)
VALUES (
:sourceId, :remoteUrl, :remoteTitle, :remoteAuthor, :remoteArtist, :remoteDescription,
:remoteGenre, :remoteStatus, :remoteCover, :remoteUpdateStrategy, :remoteMemo,
:userFavoriteAt, :userNotes, :userReaderFlags, :userChapterFlags,
:stateChapterLastUpdate, :stateChapterNextUpdate, :stateChapterFetchInterval,
:stateCoverLastModified, :stateInitialized
)
RETURNING id; RETURNING id;
update: update:
UPDATE manga SET UPDATE manga SET
-- NULL is a real value here (not in the library), which coalesce cannot write -- NULL is a real value here (not in the library), which coalesce cannot write
user_favorite_at = CASE WHEN :favoriteAtSet THEN :favoriteAt ELSE user_favorite_at END, user_favorite_at = CASE WHEN :userFavoriteAtSet THEN :userFavoriteAt ELSE user_favorite_at END,
state_chapter_last_update = coalesce(:lastUpdate, state_chapter_last_update), user_notes = coalesce(:userNotes, user_notes),
state_chapter_next_update = coalesce(:nextUpdate, state_chapter_next_update), user_reader_flags = coalesce(:userReaderFlags, user_reader_flags),
user_reader_flags = coalesce(:viewer, user_reader_flags), user_chapter_flags = coalesce(:userChapterFlags, user_chapter_flags),
user_chapter_flags = coalesce(:chapterFlags, user_chapter_flags), state_chapter_last_update = coalesce(:stateChapterLastUpdate, state_chapter_last_update),
state_cover_last_modified = coalesce(:coverLastModified, state_cover_last_modified), state_chapter_next_update = coalesce(:stateChapterNextUpdate, state_chapter_next_update),
state_chapter_fetch_interval = coalesce(:calculateInterval, state_chapter_fetch_interval), state_chapter_fetch_interval = coalesce(:stateChapterFetchInterval, state_chapter_fetch_interval),
user_notes = coalesce(:notes, user_notes) state_cover_last_modified = coalesce(:stateCoverLastModified, state_cover_last_modified)
WHERE id = :mangaId; WHERE id = :id;
updateFromBackup: updateFromBackup:
UPDATE manga SET UPDATE manga SET
remote_artist = coalesce(:artist, remote_artist), remote_title = coalesce(:remoteTitle, remote_title),
remote_author = coalesce(:author, remote_author), remote_author = coalesce(:remoteAuthor, remote_author),
remote_description = coalesce(:description, remote_description), remote_artist = coalesce(:remoteArtist, remote_artist),
remote_genre = coalesce(:genre, remote_genre), remote_description = coalesce(:remoteDescription, remote_description),
remote_title = coalesce(:title, remote_title), remote_genre = coalesce(:remoteGenre, remote_genre),
remote_status = coalesce(:status, remote_status), remote_status = coalesce(:remoteStatus, remote_status),
remote_cover = coalesce(:thumbnailUrl, remote_cover), remote_cover = coalesce(:remoteCover, remote_cover),
user_favorite_at = :favoriteAt, remote_update_strategy = coalesce(:remoteUpdateStrategy, remote_update_strategy),
state_chapter_last_update = coalesce(:lastUpdate, state_chapter_last_update), remote_memo = coalesce(:remoteMemo, remote_memo),
state_initialized = coalesce(:initialized, state_initialized), user_favorite_at = :userFavoriteAt,
user_reader_flags = coalesce(:viewer, user_reader_flags), user_notes = coalesce(:userNotes, user_notes),
user_chapter_flags = coalesce(:chapterFlags, user_chapter_flags), user_reader_flags = coalesce(:userReaderFlags, user_reader_flags),
state_cover_last_modified = coalesce(:coverLastModified, state_cover_last_modified), user_chapter_flags = coalesce(:userChapterFlags, user_chapter_flags),
remote_update_strategy = coalesce(:updateStrategy, remote_update_strategy), state_chapter_last_update = coalesce(:stateChapterLastUpdate, state_chapter_last_update),
user_notes = coalesce(:notes, user_notes), state_cover_last_modified = coalesce(:stateCoverLastModified, state_cover_last_modified),
remote_memo = coalesce(:memo, remote_memo) state_initialized = coalesce(:stateInitialized, state_initialized)
WHERE id = :mangaId; WHERE id = :id;
updateRemote: updateRemote:
UPDATE manga SET UPDATE manga SET
remote_artist = coalesce(:artist, remote_artist), remote_title = coalesce(:remoteTitle, remote_title),
remote_author = coalesce(:author, remote_author), remote_author = coalesce(:remoteAuthor, remote_author),
remote_description = coalesce(:description, remote_description), remote_artist = coalesce(:remoteArtist, remote_artist),
remote_genre = coalesce(:genre, remote_genre), remote_description = coalesce(:remoteDescription, remote_description),
remote_title = coalesce(:title, remote_title), remote_genre = coalesce(:remoteGenre, remote_genre),
remote_status = :status, remote_status = :remoteStatus,
remote_cover = coalesce(:thumbnailUrl, remote_cover), remote_cover = coalesce(:remoteCover, remote_cover),
state_initialized = :initialized, remote_update_strategy = :remoteUpdateStrategy,
state_cover_last_modified = coalesce(:coverLastModified, state_cover_last_modified), remote_memo = :remoteMemo,
remote_update_strategy = :updateStrategy, state_cover_last_modified = coalesce(:stateCoverLastModified, state_cover_last_modified),
remote_memo = :memo state_initialized = :stateInitialized
WHERE id = :mangaId; WHERE id = :id;
insertNetworkManga { insertNetworkManga {
-- Insert the manga if it doesn't exist already -- A new entry is inserted as the source reported it. An existing one takes the fresh details
-- only while it is outside the library, so a library entry keeps what the user already has.
INSERT INTO manga( INSERT INTO manga(
source_id, remote_url, remote_artist, remote_author, remote_description, remote_genre, remote_title, remote_status, remote_cover, user_favorite_at, source_id, remote_url, remote_title, remote_author, remote_artist, remote_description,
state_chapter_last_update, state_chapter_next_update, state_initialized, user_reader_flags, user_chapter_flags, state_cover_last_modified, remote_genre, remote_status, remote_cover, remote_update_strategy, remote_memo,
remote_update_strategy, state_chapter_fetch_interval, user_notes, remote_memo user_favorite_at, user_notes, user_reader_flags, user_chapter_flags,
state_chapter_last_update, state_chapter_next_update, state_chapter_fetch_interval,
state_cover_last_modified, state_initialized
) )
SELECT VALUES (
:source, :url, :artist, :author, :description, :genre, :title, :status, :thumbnailUrl, :favoriteAt, :sourceId, :remoteUrl, :remoteTitle, :remoteAuthor, :remoteArtist, :remoteDescription,
:lastUpdate, :nextUpdate, :initialized, :viewerFlags, :chapterFlags, :coverLastModified, :remoteGenre, :remoteStatus, :remoteCover, :remoteUpdateStrategy, :remoteMemo,
:updateStrategy, :calculateInterval, '', :memo :userFavoriteAt, '', :userReaderFlags, :userChapterFlags,
WHERE NOT EXISTS(SELECT 0 FROM manga WHERE source_id = :source AND remote_url = :url); :stateChapterLastUpdate, :stateChapterNextUpdate, :stateChapterFetchInterval,
:stateCoverLastModified, :stateInitialized
)
ON CONFLICT(source_id, remote_url) DO UPDATE SET
remote_title = CASE WHEN :updateTitle THEN excluded.remote_title ELSE remote_title END,
remote_cover = CASE WHEN :updateCover THEN excluded.remote_cover ELSE remote_cover END,
remote_author = CASE WHEN :updateDetails THEN excluded.remote_author ELSE remote_author END,
remote_artist = CASE WHEN :updateDetails THEN excluded.remote_artist ELSE remote_artist END,
remote_description = CASE WHEN :updateDetails THEN excluded.remote_description ELSE remote_description END,
remote_genre = CASE WHEN :updateDetails THEN excluded.remote_genre ELSE remote_genre END,
remote_status = CASE WHEN :updateDetails THEN excluded.remote_status ELSE remote_status END,
remote_update_strategy = CASE
WHEN :updateDetails THEN excluded.remote_update_strategy
ELSE remote_update_strategy
END,
remote_memo = excluded.remote_memo,
state_initialized = :updateDetails
WHERE user_favorite_at IS NULL;
-- Update the relevant details if applicable and not favorite
UPDATE manga
SET
remote_title = CASE WHEN :updateTitle THEN :title ELSE remote_title END,
remote_cover = CASE WHEN :updateCover THEN :thumbnailUrl ELSE remote_cover END,
remote_author = CASE WHEN :updateDetails THEN :author ELSE remote_author END,
remote_artist = CASE WHEN :updateDetails THEN :artist ELSE remote_artist END,
remote_description = CASE WHEN :updateDetails THEN :description ELSE remote_description END,
remote_genre = CASE WHEN :updateDetails THEN :genre ELSE remote_genre END,
remote_status = CASE WHEN :updateDetails THEN :status ELSE remote_status END,
remote_update_strategy = CASE WHEN :updateDetails THEN :updateStrategy ELSE remote_update_strategy END,
state_initialized = :updateDetails,
remote_memo = :memo
WHERE source_id = :source
AND remote_url = :url
AND user_favorite_at IS NULL;
-- Finally return the manga
SELECT SELECT
id, source_id, remote_url, remote_artist, remote_author, remote_description, remote_genre, remote_title, id, source_id, remote_url, remote_title, remote_author, remote_artist, remote_description,
remote_status, remote_cover, state_chapter_last_update, state_chapter_next_update, state_initialized, remote_genre, remote_status, remote_cover, remote_update_strategy, remote_memo,
user_reader_flags, user_chapter_flags, state_cover_last_modified, remote_update_strategy, user_favorite_at, user_notes, user_reader_flags, user_chapter_flags,
state_chapter_fetch_interval, user_notes, remote_memo, user_favorite_at state_chapter_last_update, state_chapter_next_update, state_chapter_fetch_interval,
state_cover_last_modified, state_initialized
FROM manga FROM manga
WHERE source_id = :source WHERE source_id = :sourceId
AND remote_url = :url AND remote_url = :remoteUrl;
LIMIT 1;
} }
@@ -12,7 +12,8 @@ CREATE INDEX manga_category_category_id_index ON manga_category(category_id);
insert: insert:
INSERT INTO manga_category(manga_id, category_id) INSERT INTO manga_category(manga_id, category_id)
VALUES (:mangaId, :categoryId); VALUES (:mangaId, :categoryId)
ON CONFLICT(manga_id, category_id) DO NOTHING;
deleteMangaCategoryByMangaId: deleteMangaCategoryByMangaId:
DELETE FROM manga_category DELETE FROM manga_category
@@ -24,63 +24,64 @@ CREATE INDEX manga_track_tracker_id_remote_id_index ON manga_track(tracker_id, r
delete: delete:
DELETE FROM manga_track DELETE FROM manga_track
WHERE manga_id = :mangaId AND tracker_id = :syncId; WHERE manga_id = :mangaId
AND tracker_id = :trackerId;
getTracks: getTracks:
SELECT SELECT
id, manga_id, tracker_id, remote_id, library_id, id, manga_id, tracker_id, remote_id, remote_url, library_id, title, last_chapter_read,
title, last_chapter_read, total_chapters, status, score, total_chapters, status, score, start_date, finish_date, private
remote_url, start_date, finish_date, private
FROM manga_track; FROM manga_track;
getTrackById: getTrackById:
SELECT SELECT
id, manga_id, tracker_id, remote_id, library_id, id, manga_id, tracker_id, remote_id, remote_url, library_id, title, last_chapter_read,
title, last_chapter_read, total_chapters, status, score, total_chapters, status, score, start_date, finish_date, private
remote_url, start_date, finish_date, private
FROM manga_track FROM manga_track
WHERE id = :id; WHERE id = :id;
getTracksByMangaId: getTracksByMangaId:
SELECT SELECT
id, manga_id, tracker_id, remote_id, library_id, id, manga_id, tracker_id, remote_id, remote_url, library_id, title, last_chapter_read,
title, last_chapter_read, total_chapters, status, score, total_chapters, status, score, start_date, finish_date, private
remote_url, start_date, finish_date, private
FROM manga_track FROM manga_track
WHERE manga_id = :mangaId; WHERE manga_id = :mangaId;
upsert: upsert:
INSERT INTO manga_track(manga_id,tracker_id,remote_id,library_id,title,last_chapter_read,total_chapters,status,score,remote_url,start_date,finish_date,private) INSERT INTO manga_track(
VALUES (:mangaId,:syncId,:remoteId,:libraryId,:title,:lastChapterRead,:totalChapters,:status,:score,:remoteUrl,:startDate,:finishDate,:private) manga_id, tracker_id, remote_id, remote_url, library_id, title, last_chapter_read,
ON CONFLICT(manga_id, tracker_id) total_chapters, status, score, start_date, finish_date, private
DO UPDATE )
SET VALUES (
remote_id = :remoteId, :mangaId, :trackerId, :remoteId, :remoteUrl, :libraryId, :title, :lastChapterRead,
library_id = :libraryId, :totalChapters, :status, :score, :startDate, :finishDate, :private
title = :title, )
last_chapter_read = :lastChapterRead, ON CONFLICT(manga_id, tracker_id) DO UPDATE SET
total_chapters = :totalChapters, remote_id = excluded.remote_id,
status = :status, remote_url = excluded.remote_url,
score = :score, library_id = excluded.library_id,
remote_url = :remoteUrl, title = excluded.title,
start_date = :startDate, last_chapter_read = excluded.last_chapter_read,
finish_date = :finishDate, total_chapters = excluded.total_chapters,
private = :private status = excluded.status,
WHERE manga_id = :mangaId AND tracker_id = :syncId; score = excluded.score,
start_date = excluded.start_date,
finish_date = excluded.finish_date,
private = excluded.private;
update: update:
UPDATE manga_track UPDATE manga_track
SET SET
manga_id = coalesce(:mangaId, manga_id), manga_id = coalesce(:mangaId, manga_id),
tracker_id = coalesce(:syncId, tracker_id), tracker_id = coalesce(:trackerId, tracker_id),
remote_id = coalesce(:mediaId, remote_id), remote_id = coalesce(:remoteId, remote_id),
remote_url = coalesce(:remoteUrl, remote_url),
library_id = coalesce(:libraryId, library_id), library_id = coalesce(:libraryId, library_id),
title = coalesce(:title, title), title = coalesce(:title, title),
last_chapter_read = coalesce(:lastChapterRead, last_chapter_read), last_chapter_read = coalesce(:lastChapterRead, last_chapter_read),
total_chapters = coalesce(:totalChapter, total_chapters), total_chapters = coalesce(:totalChapters, total_chapters),
status = coalesce(:status, status), status = coalesce(:status, status),
score = coalesce(:score, score), score = coalesce(:score, score),
remote_url = coalesce(:trackingUrl, remote_url),
start_date = coalesce(:startDate, start_date), start_date = coalesce(:startDate, start_date),
finish_date = coalesce(:finishDate, finish_date), finish_date = coalesce(:finishDate, finish_date),
private = coalesce(:private, private) private = coalesce(:private, private)
@@ -5,20 +5,17 @@ CREATE TABLE source(
); );
findAll: findAll:
SELECT id, language, name SELECT id, name, language
FROM source; FROM source;
findOne: findOne:
SELECT id, language, name SELECT id, name, language
FROM source FROM source
WHERE id = :id; WHERE id = :id;
upsert: upsert:
INSERT INTO source(id, language, name) INSERT INTO source(id, name, language)
VALUES (:id, :lang, :name) VALUES (:id, :name, :language)
ON CONFLICT(id) ON CONFLICT(id) DO UPDATE SET
DO UPDATE name = excluded.name,
SET language = excluded.language;
language = :lang,
name = :name
WHERE id = :id;
@@ -46,18 +46,22 @@ ORDER BY readAt DESC;
getLatestHistory: getLatestHistory:
SELECT SELECT
id, history.id AS id,
mangaId, manga.id AS mangaId,
chapterId, chapter.id AS chapterId,
title, manga.remote_title AS title,
thumbnailUrl, manga.remote_cover AS thumbnailUrl,
sourceId, manga.source_id AS sourceId,
favorite, manga.user_favorite_at IS NOT NULL AS favorite,
coverLastModified, manga.state_cover_last_modified AS coverLastModified,
chapterNumber, chapter.remote_chapter_number AS chapterNumber,
readAt, history.read_at AS readAt,
readDuration history.read_duration AS readDuration
FROM historyView FROM history
WHERE historyView.readAt > 0 JOIN manga
ORDER BY readAt DESC ON manga.id = history.manga_id
JOIN chapter
ON chapter.id = history.chapter_id
WHERE history.read_at > 0
ORDER BY history.read_at DESC
LIMIT 1; LIMIT 1;
@@ -40,11 +40,10 @@ WHERE M.user_favorite_at IS NOT NULL;
library: library:
SELECT SELECT
id, source_id, remote_url, remote_artist, remote_author, id, source_id, remote_url, remote_title, remote_author, remote_artist, remote_description,
remote_description, remote_genre, remote_title, remote_status, remote_cover, remote_genre, remote_status, remote_cover, remote_update_strategy, remote_memo,
state_chapter_last_update, state_chapter_next_update, state_initialized, user_reader_flags, user_favorite_at, user_notes, user_reader_flags, user_chapter_flags,
user_chapter_flags, state_cover_last_modified, remote_update_strategy, state_chapter_fetch_interval, state_chapter_last_update, state_chapter_next_update, state_chapter_fetch_interval,
user_notes, remote_memo, state_cover_last_modified, state_initialized, totalCount, readCount, latestUpload,
user_favorite_at, totalCount, readCount, latestUpload, chapterFetchedAt, chapterFetchedAt, lastRead, bookmarkCount, categories
lastRead, bookmarkCount, categories
FROM libraryView; FROM libraryView;
@@ -25,23 +25,11 @@ WHERE user_favorite_at IS NOT NULL
AND state_date_fetch > user_favorite_at AND state_date_fetch > user_favorite_at
ORDER BY state_date_fetch DESC; ORDER BY state_date_fetch DESC;
getRecentUpdates:
SELECT
mangaId, mangaTitle, chapterId, chapterName,
scanlator, chapterUrl, read, bookmark,
lastPageRead, sourceId, favorite, thumbnailUrl,
coverLastModified, dateUpload, dateFetch, excludedScanlator
FROM updatesView
WHERE dateUpload > :after
ORDER BY dateFetch DESC
LIMIT :limit;
getRecentUpdatesWithFilters: getRecentUpdatesWithFilters:
SELECT SELECT
mangaId, mangaTitle, chapterId, chapterName, mangaId, mangaTitle, chapterId, chapterName, scanlator, chapterUrl, read, bookmark,
scanlator, chapterUrl, read, bookmark, lastPageRead, sourceId, favorite, thumbnailUrl, coverLastModified, dateUpload, dateFetch,
lastPageRead, sourceId, favorite, thumbnailUrl, excludedScanlator
coverLastModified, dateUpload, dateFetch, excludedScanlator
FROM updatesView FROM updatesView
WHERE dateUpload > :after WHERE dateUpload > :after
AND (:read IS NULL OR read = :read) AND (:read IS NULL OR read = :read)
@@ -52,19 +40,17 @@ AND (
OR (:started = 0 AND lastPageRead = 0 AND read = 0) OR (:started = 0 AND lastPageRead = 0 AND read = 0)
) )
AND (:bookmarked IS NULL OR bookmark = :bookmarked) AND (:bookmarked IS NULL OR bookmark = :bookmarked)
AND ( AND (excludedScanlator IS NULL OR :hideExcludedScanlators = 0)
(excludedScanlator IS NULL OR :hideExcludedScanlators = 0)
)
AND ( AND (
-- includedEmpty being true expresses "don't care" state and bypasses the membership filter -- includedEmpty being true expresses "don't care" state and bypasses the membership filter
:includedEmpty :includedEmpty
OR EXISTS ( OR EXISTS (
SELECT 1 FROM manga_category SELECT 1 FROM manga_category
WHERE manga_category.manga_id = mangaId WHERE manga_category.manga_id = mangaId
AND COALESCE(manga_category.category_id, 0) IN :includedCategories AND manga_category.category_id IN :includedCategories
) )
OR (0 IN :includedCategories AND NOT EXISTS ( OR (0 IN :includedCategories AND NOT EXISTS (
SELECT 1 FROM manga_category mc WHERE mc.manga_id = mangaId) SELECT 1 FROM manga_category WHERE manga_category.manga_id = mangaId)
) )
) )
AND ( AND (
@@ -74,7 +60,7 @@ AND (
NOT EXISTS ( NOT EXISTS (
SELECT 1 FROM manga_category SELECT 1 FROM manga_category
WHERE manga_category.manga_id = mangaId WHERE manga_category.manga_id = mangaId
AND COALESCE(manga_category.category_id, 0) IN :excludedCategories AND manga_category.category_id IN :excludedCategories
) )
AND ( AND (
0 NOT IN :excludedCategories 0 NOT IN :excludedCategories
@@ -85,14 +71,24 @@ AND (
ORDER BY dateFetch DESC ORDER BY dateFetch DESC
LIMIT :limit; LIMIT :limit;
getUpdatesByReadStatus: getReadUpdates:
SELECT SELECT
mangaId, mangaTitle, chapterId, chapterName, mangaId, mangaTitle, chapterId, chapterName, scanlator, chapterUrl, read, bookmark,
scanlator, chapterUrl, read, bookmark, lastPageRead, sourceId, favorite, thumbnailUrl, coverLastModified, dateUpload, dateFetch,
lastPageRead, sourceId, favorite, thumbnailUrl, excludedScanlator
coverLastModified, dateUpload, dateFetch, excludedScanlator
FROM updatesView FROM updatesView
WHERE read = :read WHERE read = 1
AND dateUpload > :after
ORDER BY dateFetch DESC
LIMIT :limit;
getUnreadUpdates:
SELECT
mangaId, mangaTitle, chapterId, chapterName, scanlator, chapterUrl, read, bookmark,
lastPageRead, sourceId, favorite, thumbnailUrl, coverLastModified, dateUpload, dateFetch,
excludedScanlator
FROM updatesView
WHERE read = 0
AND dateUpload > :after AND dateUpload > :after
ORDER BY dateFetch DESC ORDER BY dateFetch DESC
LIMIT :limit; LIMIT :limit;
@@ -6,7 +6,7 @@ import tachiyomi.domain.manga.model.MangaUpdate
interface RestoreRepository { interface RestoreRepository {
suspend fun getMangaUrlsBySourceId(): Map<Long, List<String>> suspend fun getMangaUrlsBySourceId(): Map<Long, Set<String>>
/** /**
* Restores [entries] all together: either every one of them is restored or none is. [update] is what to change on each * Restores [entries] all together: either every one of them is restored or none is. [update] is what to change on each
@@ -24,7 +24,7 @@ interface CategoryRepository {
suspend fun updateFlags(categoryId: Long, flags: Long) suspend fun updateFlags(categoryId: Long, flags: Long)
suspend fun updateAllFlags(flags: Long?) suspend fun updateAllFlags(flags: Long)
suspend fun updateAllOrders(orderedIds: List<Long>) suspend fun updateAllOrders(orderedIds: List<Long>)
@@ -16,23 +16,27 @@ class SetMangaDefaultChapterFlags(
suspend fun await(manga: Manga) { suspend fun await(manga: Manga) {
withNonCancellableContext { withNonCancellableContext {
with(libraryPreferences) { setDefaultFlags(listOf(manga.id))
setMangaChapterFlags.awaitSetAllFlags(
mangaId = manga.id,
unreadFilter = filterChapterByRead.get(),
downloadedFilter = filterChapterByDownloaded.get(),
bookmarkedFilter = filterChapterByBookmarked.get(),
sortingMode = sortChapterBySourceOrNumber.get(),
sortingDirection = sortChapterByAscendingOrDescending.get(),
displayMode = displayChapterByNameOrNumber.get(),
)
}
} }
} }
suspend fun awaitAll() { suspend fun awaitAll() {
withNonCancellableContext { withNonCancellableContext {
getFavorites.await().forEach { await(it) } setDefaultFlags(getFavorites.await().map { it.id })
}
}
private suspend fun setDefaultFlags(mangaIds: List<Long>) {
with(libraryPreferences) {
setMangaChapterFlags.awaitSetAllFlags(
mangaIds = mangaIds,
unreadFilter = filterChapterByRead.get(),
downloadedFilter = filterChapterByDownloaded.get(),
bookmarkedFilter = filterChapterByBookmarked.get(),
sortingMode = sortChapterBySourceOrNumber.get(),
sortingDirection = sortChapterByAscendingOrDescending.get(),
displayMode = displayChapterByNameOrNumber.get(),
)
} }
} }
} }
@@ -67,7 +67,7 @@ class SetMangaChapterFlags(
} }
suspend fun awaitSetAllFlags( suspend fun awaitSetAllFlags(
mangaId: Long, mangaIds: List<Long>,
unreadFilter: Long, unreadFilter: Long,
downloadedFilter: Long, downloadedFilter: Long,
bookmarkedFilter: Long, bookmarkedFilter: Long,
@@ -75,16 +75,13 @@ class SetMangaChapterFlags(
sortingDirection: Long, sortingDirection: Long,
displayMode: Long, displayMode: Long,
): Boolean { ): Boolean {
return mangaRepository.update( val flags = 0L.setFlag(unreadFilter, Manga.CHAPTER_UNREAD_MASK)
MangaUpdate(mangaId) { .setFlag(downloadedFilter, Manga.CHAPTER_DOWNLOADED_MASK)
chapterFlags = 0L.setFlag(unreadFilter, Manga.CHAPTER_UNREAD_MASK) .setFlag(bookmarkedFilter, Manga.CHAPTER_BOOKMARKED_MASK)
.setFlag(downloadedFilter, Manga.CHAPTER_DOWNLOADED_MASK) .setFlag(sortingMode, Manga.CHAPTER_SORTING_MASK)
.setFlag(bookmarkedFilter, Manga.CHAPTER_BOOKMARKED_MASK) .setFlag(sortingDirection, Manga.CHAPTER_SORT_DIR_MASK)
.setFlag(sortingMode, Manga.CHAPTER_SORTING_MASK) .setFlag(displayMode, Manga.CHAPTER_DISPLAY_MASK)
.setFlag(sortingDirection, Manga.CHAPTER_SORT_DIR_MASK) return mangaRepository.updateAll(mangaIds.map { MangaUpdate(it) { chapterFlags = flags } })
.setFlag(displayMode, Manga.CHAPTER_DISPLAY_MASK)
},
)
} }
private fun Long.setFlag(flag: Long, mask: Long): Long { private fun Long.setFlag(flag: Long, mask: Long): Long {