diff --git a/app/src/main/java/eu/kanade/data/manga/MangaRepositoryImpl.kt b/app/src/main/java/eu/kanade/data/manga/MangaRepositoryImpl.kt index addc23864..562584c6e 100644 --- a/app/src/main/java/eu/kanade/data/manga/MangaRepositoryImpl.kt +++ b/app/src/main/java/eu/kanade/data/manga/MangaRepositoryImpl.kt @@ -1,7 +1,10 @@ package eu.kanade.data.manga import eu.kanade.data.DatabaseHandler +import eu.kanade.data.listOfStringsAdapter +import eu.kanade.data.toLong import eu.kanade.domain.manga.model.Manga +import eu.kanade.domain.manga.model.MangaUpdate import eu.kanade.domain.manga.repository.MangaRepository import eu.kanade.tachiyomi.util.system.logcat import kotlinx.coroutines.flow.Flow @@ -11,6 +14,10 @@ class MangaRepositoryImpl( private val handler: DatabaseHandler, ) : MangaRepository { + override suspend fun getMangaById(id: Long): Manga { + return handler.awaitOne { mangasQueries.getMangaById(id, mangaMapper) } + } + override fun getFavoritesBySourceId(sourceId: Long): Flow> { return handler.subscribeToList { mangasQueries.getFavoriteBySourceId(sourceId, mangaMapper) } } @@ -25,11 +32,33 @@ class MangaRepositoryImpl( } } - override suspend fun updateLastUpdate(mangaId: Long, lastUpdate: Long) { - try { - handler.await { mangasQueries.updateLastUpdate(lastUpdate, mangaId) } + override suspend fun update(update: MangaUpdate): Boolean { + return try { + handler.await { + mangasQueries.update( + source = update.source, + url = update.url, + artist = update.artist, + author = update.author, + description = update.description, + genre = update.genre?.let(listOfStringsAdapter::encode), + title = update.title, + status = update.status, + thumbnailUrl = update.thumbnailUrl, + favorite = update.favorite?.toLong(), + lastUpdate = update.lastUpdate, + initialized = update.initialized?.toLong(), + viewer = update.viewerFlags, + chapterFlags = update.chapterFlags, + coverLastModified = update.coverLastModified, + dateAdded = update.dateAdded, + mangaId = update.id, + ) + } + true } catch (e: Exception) { logcat(LogPriority.ERROR, e) + false } } } diff --git a/app/src/main/java/eu/kanade/domain/DomainModule.kt b/app/src/main/java/eu/kanade/domain/DomainModule.kt index 9d7023a06..9be7f13d2 100644 --- a/app/src/main/java/eu/kanade/domain/DomainModule.kt +++ b/app/src/main/java/eu/kanade/domain/DomainModule.kt @@ -20,8 +20,9 @@ import eu.kanade.domain.history.interactor.RemoveHistoryByMangaId import eu.kanade.domain.history.interactor.UpsertHistory import eu.kanade.domain.history.repository.HistoryRepository import eu.kanade.domain.manga.interactor.GetFavoritesBySourceId +import eu.kanade.domain.manga.interactor.GetMangaById import eu.kanade.domain.manga.interactor.ResetViewerFlags -import eu.kanade.domain.manga.interactor.UpdateMangaLastUpdate +import eu.kanade.domain.manga.interactor.UpdateManga import eu.kanade.domain.manga.repository.MangaRepository import eu.kanade.domain.source.interactor.GetEnabledSources import eu.kanade.domain.source.interactor.GetLanguagesWithSources @@ -43,9 +44,10 @@ class DomainModule : InjektModule { override fun InjektRegistrar.registerInjectables() { addSingletonFactory { MangaRepositoryImpl(get()) } addFactory { GetFavoritesBySourceId(get()) } + addFactory { GetMangaById(get()) } addFactory { GetNextChapter(get()) } addFactory { ResetViewerFlags(get()) } - addFactory { UpdateMangaLastUpdate(get()) } + addFactory { UpdateManga(get()) } addSingletonFactory { ChapterRepositoryImpl(get()) } addFactory { UpdateChapter(get()) } diff --git a/app/src/main/java/eu/kanade/domain/chapter/interactor/SyncChaptersWithSource.kt b/app/src/main/java/eu/kanade/domain/chapter/interactor/SyncChaptersWithSource.kt index a9358c20d..51029fd0b 100644 --- a/app/src/main/java/eu/kanade/domain/chapter/interactor/SyncChaptersWithSource.kt +++ b/app/src/main/java/eu/kanade/domain/chapter/interactor/SyncChaptersWithSource.kt @@ -5,7 +5,7 @@ import eu.kanade.domain.chapter.model.Chapter import eu.kanade.domain.chapter.model.toChapterUpdate import eu.kanade.domain.chapter.model.toDbChapter import eu.kanade.domain.chapter.repository.ChapterRepository -import eu.kanade.domain.manga.interactor.UpdateMangaLastUpdate +import eu.kanade.domain.manga.interactor.UpdateManga import eu.kanade.domain.manga.model.Manga import eu.kanade.domain.manga.model.toDbManga import eu.kanade.tachiyomi.data.download.DownloadManager @@ -24,7 +24,7 @@ class SyncChaptersWithSource( private val downloadManager: DownloadManager = Injekt.get(), private val chapterRepository: ChapterRepository = Injekt.get(), private val shouldUpdateDbChapter: ShouldUpdateDbChapter = Injekt.get(), - private val updateMangaLastUpdate: UpdateMangaLastUpdate = Injekt.get(), + private val updateManga: UpdateManga = Injekt.get(), ) { suspend fun await( @@ -171,7 +171,7 @@ class SyncChaptersWithSource( // Set this manga as updated since chapters were changed // Note that last_update actually represents last time the chapter list changed at all - updateMangaLastUpdate.await(manga.id, Date().time) + updateManga.awaitUpdateLastUpdate(manga.id) @Suppress("ConvertArgumentToSet") // See tachiyomiorg/tachiyomi#6372. return Pair(updatedToAdd.subtract(reAdded).toList(), toDelete.subtract(reAdded).toList()) diff --git a/app/src/main/java/eu/kanade/domain/manga/interactor/GetMangaById.kt b/app/src/main/java/eu/kanade/domain/manga/interactor/GetMangaById.kt new file mode 100644 index 000000000..9513c0c47 --- /dev/null +++ b/app/src/main/java/eu/kanade/domain/manga/interactor/GetMangaById.kt @@ -0,0 +1,20 @@ +package eu.kanade.domain.manga.interactor + +import eu.kanade.domain.manga.model.Manga +import eu.kanade.domain.manga.repository.MangaRepository +import eu.kanade.tachiyomi.util.system.logcat +import logcat.LogPriority + +class GetMangaById( + private val mangaRepository: MangaRepository, +) { + + suspend fun await(id: Long): Manga? { + return try { + mangaRepository.getMangaById(id) + } catch (e: Exception) { + logcat(LogPriority.ERROR, e) + null + } + } +} diff --git a/app/src/main/java/eu/kanade/domain/manga/interactor/UpdateManga.kt b/app/src/main/java/eu/kanade/domain/manga/interactor/UpdateManga.kt new file mode 100644 index 000000000..babb7ac41 --- /dev/null +++ b/app/src/main/java/eu/kanade/domain/manga/interactor/UpdateManga.kt @@ -0,0 +1,61 @@ +package eu.kanade.domain.manga.interactor + +import eu.kanade.domain.manga.model.Manga +import eu.kanade.domain.manga.model.MangaUpdate +import eu.kanade.domain.manga.model.hasCustomCover +import eu.kanade.domain.manga.model.isLocal +import eu.kanade.domain.manga.model.toDbManga +import eu.kanade.domain.manga.repository.MangaRepository +import eu.kanade.tachiyomi.data.cache.CoverCache +import tachiyomi.source.model.MangaInfo +import java.util.Date + +class UpdateManga( + private val mangaRepository: MangaRepository, +) { + + suspend fun awaitUpdateFromSource( + localManga: Manga, + remoteManga: MangaInfo, + manualFetch: Boolean, + coverCache: CoverCache, + ): Boolean { + // if the manga isn't a favorite, set its title from source and update in db + val title = if (!localManga.favorite) remoteManga.title else null + + // Never refresh covers if the url is empty to avoid "losing" existing covers + val updateCover = remoteManga.cover.isNotEmpty() && (manualFetch || localManga.thumbnailUrl != remoteManga.cover) + val coverLastModified = if (updateCover) { + when { + localManga.isLocal() -> Date().time + localManga.hasCustomCover(coverCache) -> { + coverCache.deleteFromCache(localManga.toDbManga(), false) + null + } + else -> { + coverCache.deleteFromCache(localManga.toDbManga(), false) + Date().time + } + } + } else null + + return mangaRepository.update( + MangaUpdate( + id = localManga.id, + title = title?.takeIf { it.isNotEmpty() }, + coverLastModified = coverLastModified, + author = remoteManga.author, + artist = remoteManga.artist, + description = remoteManga.description, + genre = remoteManga.genres, + thumbnailUrl = remoteManga.cover.takeIf { it.isNotEmpty() }, + status = remoteManga.status.toLong(), + initialized = true, + ), + ) + } + + suspend fun awaitUpdateLastUpdate(mangaId: Long): Boolean { + return mangaRepository.update(MangaUpdate(id = mangaId, lastUpdate = Date().time)) + } +} diff --git a/app/src/main/java/eu/kanade/domain/manga/interactor/UpdateMangaLastUpdate.kt b/app/src/main/java/eu/kanade/domain/manga/interactor/UpdateMangaLastUpdate.kt deleted file mode 100644 index 641192744..000000000 --- a/app/src/main/java/eu/kanade/domain/manga/interactor/UpdateMangaLastUpdate.kt +++ /dev/null @@ -1,12 +0,0 @@ -package eu.kanade.domain.manga.interactor - -import eu.kanade.domain.manga.repository.MangaRepository - -class UpdateMangaLastUpdate( - private val mangaRepository: MangaRepository, -) { - - suspend fun await(mangaId: Long, lastUpdate: Long) { - mangaRepository.updateLastUpdate(mangaId, lastUpdate) - } -} diff --git a/app/src/main/java/eu/kanade/domain/manga/model/Manga.kt b/app/src/main/java/eu/kanade/domain/manga/model/Manga.kt index 312bff395..30c3b4a24 100644 --- a/app/src/main/java/eu/kanade/domain/manga/model/Manga.kt +++ b/app/src/main/java/eu/kanade/domain/manga/model/Manga.kt @@ -1,6 +1,11 @@ package eu.kanade.domain.manga.model +import eu.kanade.tachiyomi.data.cache.CoverCache +import eu.kanade.tachiyomi.source.LocalSource import eu.kanade.tachiyomi.source.model.SManga +import tachiyomi.source.model.MangaInfo +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get import eu.kanade.tachiyomi.data.database.models.Manga as DbManga data class Manga( @@ -62,3 +67,20 @@ fun Manga.toDbManga(): DbManga = DbManga.create(url, title, source).also { it.chapter_flags = chapterFlags.toInt() it.cover_last_modified = coverLastModified } + +fun Manga.toMangaInfo(): MangaInfo = MangaInfo( + artist = artist ?: "", + author = author ?: "", + cover = thumbnailUrl ?: "", + description = description ?: "", + genres = genre ?: emptyList(), + key = url, + status = status.toInt(), + title = title, +) + +fun Manga.isLocal(): Boolean = source == LocalSource.ID + +fun Manga.hasCustomCover(coverCache: CoverCache = Injekt.get()): Boolean { + return coverCache.getCustomCoverFile(id).exists() +} diff --git a/app/src/main/java/eu/kanade/domain/manga/model/MangaUpdate.kt b/app/src/main/java/eu/kanade/domain/manga/model/MangaUpdate.kt new file mode 100644 index 000000000..0d18659d4 --- /dev/null +++ b/app/src/main/java/eu/kanade/domain/manga/model/MangaUpdate.kt @@ -0,0 +1,21 @@ +package eu.kanade.domain.manga.model + +data class MangaUpdate( + val id: Long, + val source: Long? = null, + val favorite: Boolean? = null, + val lastUpdate: Long? = null, + val dateAdded: Long? = null, + val viewerFlags: Long? = null, + val chapterFlags: Long? = null, + val coverLastModified: Long? = null, + val url: String? = null, + val title: String? = null, + val artist: String? = null, + val author: String? = null, + val description: String? = null, + val genre: List? = null, + val status: Long? = null, + val thumbnailUrl: String? = null, + val initialized: Boolean? = null, +) diff --git a/app/src/main/java/eu/kanade/domain/manga/repository/MangaRepository.kt b/app/src/main/java/eu/kanade/domain/manga/repository/MangaRepository.kt index 83367d103..875cc9cdc 100644 --- a/app/src/main/java/eu/kanade/domain/manga/repository/MangaRepository.kt +++ b/app/src/main/java/eu/kanade/domain/manga/repository/MangaRepository.kt @@ -1,13 +1,16 @@ package eu.kanade.domain.manga.repository import eu.kanade.domain.manga.model.Manga +import eu.kanade.domain.manga.model.MangaUpdate import kotlinx.coroutines.flow.Flow interface MangaRepository { + suspend fun getMangaById(id: Long): Manga + fun getFavoritesBySourceId(sourceId: Long): Flow> suspend fun resetViewerFlags(): Boolean - suspend fun updateLastUpdate(mangaId: Long, lastUpdate: Long) + suspend fun update(update: MangaUpdate): Boolean } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/queries/MangaQueries.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/queries/MangaQueries.kt index 0bfc93d0a..953e05c05 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/database/queries/MangaQueries.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/database/queries/MangaQueries.kt @@ -10,7 +10,6 @@ import eu.kanade.tachiyomi.data.database.resolvers.LibraryMangaGetResolver import eu.kanade.tachiyomi.data.database.resolvers.MangaCoverLastModifiedPutResolver import eu.kanade.tachiyomi.data.database.resolvers.MangaFavoritePutResolver import eu.kanade.tachiyomi.data.database.resolvers.MangaFlagsPutResolver -import eu.kanade.tachiyomi.data.database.resolvers.MangaLastUpdatedPutResolver import eu.kanade.tachiyomi.data.database.tables.CategoryTable import eu.kanade.tachiyomi.data.database.tables.ChapterTable import eu.kanade.tachiyomi.data.database.tables.MangaCategoryTable @@ -101,11 +100,6 @@ interface MangaQueries : DbProvider { .withPutResolver(MangaFlagsPutResolver(MangaTable.COL_VIEWER, Manga::viewer_flags)) .prepare() - fun updateLastUpdated(manga: Manga) = db.put() - .`object`(manga) - .withPutResolver(MangaLastUpdatedPutResolver()) - .prepare() - fun updateMangaFavorite(manga: Manga) = db.put() .`object`(manga) .withPutResolver(MangaFavoritePutResolver()) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/resolvers/MangaLastUpdatedPutResolver.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/resolvers/MangaLastUpdatedPutResolver.kt deleted file mode 100644 index e2dcb22dc..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/data/database/resolvers/MangaLastUpdatedPutResolver.kt +++ /dev/null @@ -1,32 +0,0 @@ -package eu.kanade.tachiyomi.data.database.resolvers - -import androidx.core.content.contentValuesOf -import com.pushtorefresh.storio.sqlite.StorIOSQLite -import com.pushtorefresh.storio.sqlite.operations.put.PutResolver -import com.pushtorefresh.storio.sqlite.operations.put.PutResult -import com.pushtorefresh.storio.sqlite.queries.UpdateQuery -import eu.kanade.tachiyomi.data.database.inTransactionReturn -import eu.kanade.tachiyomi.data.database.models.Manga -import eu.kanade.tachiyomi.data.database.tables.MangaTable - -class MangaLastUpdatedPutResolver : PutResolver() { - - override fun performPut(db: StorIOSQLite, manga: Manga) = db.inTransactionReturn { - val updateQuery = mapToUpdateQuery(manga) - val contentValues = mapToContentValues(manga) - - val numberOfRowsUpdated = db.lowLevel().update(updateQuery, contentValues) - PutResult.newUpdateResult(numberOfRowsUpdated, updateQuery.table()) - } - - fun mapToUpdateQuery(manga: Manga) = UpdateQuery.builder() - .table(MangaTable.TABLE) - .where("${MangaTable.COL_ID} = ?") - .whereArgs(manga.id) - .build() - - fun mapToContentValues(manga: Manga) = - contentValuesOf( - MangaTable.COL_LAST_UPDATE to manga.last_update, - ) -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateService.kt b/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateService.kt index a702ba548..f7a9277f8 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateService.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateService.kt @@ -7,6 +7,11 @@ import android.os.IBinder import android.os.PowerManager import androidx.core.content.ContextCompat import eu.kanade.data.chapter.NoChaptersException +import eu.kanade.domain.chapter.interactor.SyncChaptersWithSource +import eu.kanade.domain.chapter.model.toDbChapter +import eu.kanade.domain.manga.interactor.GetMangaById +import eu.kanade.domain.manga.interactor.UpdateManga +import eu.kanade.domain.manga.model.toMangaInfo import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.cache.CoverCache import eu.kanade.tachiyomi.data.database.DatabaseHelper @@ -14,6 +19,7 @@ import eu.kanade.tachiyomi.data.database.models.Category import eu.kanade.tachiyomi.data.database.models.Chapter import eu.kanade.tachiyomi.data.database.models.LibraryManga import eu.kanade.tachiyomi.data.database.models.Manga +import eu.kanade.tachiyomi.data.database.models.toDomainManga import eu.kanade.tachiyomi.data.database.models.toMangaInfo import eu.kanade.tachiyomi.data.download.DownloadManager import eu.kanade.tachiyomi.data.download.DownloadService @@ -29,10 +35,8 @@ import eu.kanade.tachiyomi.data.track.TrackService import eu.kanade.tachiyomi.source.SourceManager import eu.kanade.tachiyomi.source.UnmeteredSource import eu.kanade.tachiyomi.source.model.SManga -import eu.kanade.tachiyomi.source.model.toMangaInfo import eu.kanade.tachiyomi.source.model.toSChapter import eu.kanade.tachiyomi.source.model.toSManga -import eu.kanade.tachiyomi.util.chapter.syncChaptersWithSource import eu.kanade.tachiyomi.util.chapter.syncChaptersWithTrackServiceTwoWay import eu.kanade.tachiyomi.util.lang.withIOContext import eu.kanade.tachiyomi.util.prepUpdateCover @@ -55,12 +59,15 @@ import kotlinx.coroutines.supervisorScope import kotlinx.coroutines.sync.Semaphore import kotlinx.coroutines.sync.withPermit import logcat.LogPriority +import tachiyomi.source.model.MangaInfo import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get import java.io.File import java.util.concurrent.CopyOnWriteArrayList import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicInteger +import eu.kanade.domain.chapter.model.Chapter as DomainChapter +import eu.kanade.domain.manga.model.Manga as DomainManga /** * This class will take care of updating the chapters of the manga from the library. It can be @@ -77,6 +84,9 @@ class LibraryUpdateService( val downloadManager: DownloadManager = Injekt.get(), val trackManager: TrackManager = Injekt.get(), val coverCache: CoverCache = Injekt.get(), + private val getMangaById: GetMangaById = Injekt.get(), + private val updateManga: UpdateManga = Injekt.get(), + private val syncChaptersWithSource: SyncChaptersWithSource = Injekt.get(), ) : Service() { private lateinit var wakeLock: PowerManager.WakeLock @@ -302,7 +312,7 @@ class LibraryUpdateService( } // Don't continue to update if manga not in library - db.getManga(manga.id!!).executeAsBlocking() ?: return@forEach + manga.id?.let { getMangaById.await(it) } ?: return@forEach withUpdateNotification( currentlyUpdatingManga, @@ -322,19 +332,22 @@ class LibraryUpdateService( else -> { // Convert to the manga that contains new chapters - val (newChapters, _) = updateManga(mangaWithNotif) + mangaWithNotif.toDomainManga()?.let { domainManga -> + val (newChapters, _) = updateManga(domainManga) + val newDbChapters = newChapters.map { it.toDbChapter() } - if (newChapters.isNotEmpty()) { - if (mangaWithNotif.shouldDownloadNewChapters(db, preferences)) { - downloadChapters(mangaWithNotif, newChapters) - hasDownloads.set(true) + if (newChapters.isNotEmpty()) { + if (mangaWithNotif.shouldDownloadNewChapters(db, preferences)) { + downloadChapters(mangaWithNotif, newDbChapters) + hasDownloads.set(true) + } + + // Convert to the manga that contains new chapters + newUpdates.add( + mangaWithNotif to newDbChapters.sortedByDescending { ch -> ch.source_order } + .toTypedArray(), + ) } - - // Convert to the manga that contains new chapters - newUpdates.add( - mangaWithNotif to newChapters.sortedByDescending { ch -> ch.source_order } - .toTypedArray(), - ) } } } @@ -394,39 +407,27 @@ class LibraryUpdateService( * @param manga the manga to update. * @return a pair of the inserted and removed chapters. */ - private suspend fun updateManga(manga: Manga): Pair, List> { + private suspend fun updateManga(manga: DomainManga): Pair, List> { val source = sourceManager.getOrStub(manga.source) - var updatedManga: SManga = manga + val mangaInfo: MangaInfo = manga.toMangaInfo() - // Update manga details metadata + // Update manga metadata if needed if (preferences.autoUpdateMetadata()) { - val updatedMangaDetails = source.getMangaDetails(manga.toMangaInfo()) - val sManga = updatedMangaDetails.toSManga() - // Avoid "losing" existing cover - if (!sManga.thumbnail_url.isNullOrEmpty()) { - manga.prepUpdateCover(coverCache, sManga, false) - } else { - sManga.thumbnail_url = manga.thumbnail_url - } - - updatedManga = sManga + val updatedMangaInfo = source.getMangaDetails(manga.toMangaInfo()) + updateManga.awaitUpdateFromSource(manga, updatedMangaInfo, manualFetch = false, coverCache) } - val chapters = source.getChapterList(updatedManga.toMangaInfo()) + val chapters = source.getChapterList(mangaInfo) .map { it.toSChapter() } // Get manga from database to account for if it was removed during the update - val dbManga = db.getManga(manga.id!!).executeAsBlocking() + val dbManga = getMangaById.await(manga.id) ?: return Pair(emptyList(), emptyList()) - // Copy into [dbManga] to retain favourite value - dbManga.copyFrom(updatedManga) - db.insertManga(dbManga).executeAsBlocking() - // [dbmanga] was used so that manga data doesn't get overwritten // in case manga gets new chapter - return syncChaptersWithSource(chapters, dbManga, source) + return syncChaptersWithSource.await(chapters, dbManga, source) } private suspend fun updateCovers() { diff --git a/app/src/main/sqldelight/data/mangas.sq b/app/src/main/sqldelight/data/mangas.sq index 154a2c080..c042853ac 100644 --- a/app/src/main/sqldelight/data/mangas.sq +++ b/app/src/main/sqldelight/data/mangas.sq @@ -58,7 +58,22 @@ deleteMangasNotInLibraryBySourceIds: DELETE FROM mangas WHERE favorite = 0 AND source IN :sourceIds; -updateLastUpdate: -UPDATE mangas -SET last_update = :lastUpdate +update: +UPDATE mangas SET + source = coalesce(:source, source), + url = coalesce(:url, url), + artist = coalesce(:artist, artist), + author = coalesce(:author, author), + description = coalesce(:description, description), + genre = coalesce(:genre, genre), + title = coalesce(:title, title), + status = coalesce(:status, status), + thumbnail_url = coalesce(:thumbnailUrl, thumbnail_url), + favorite = coalesce(:favorite, favorite), + last_update = coalesce(:lastUpdate, last_update), + initialized = coalesce(:initialized, initialized), + viewer = coalesce(:viewer, viewer), + chapter_flags = coalesce(:chapterFlags, chapter_flags), + cover_last_modified = coalesce(:coverLastModified, cover_last_modified), + date_added = coalesce(:dateAdded, date_added) WHERE _id = :mangaId;