Make syncChaptersWithSource use sqldelight (#7263)

* Make `syncChaptersWithSource` use sqldelight

Will break chapter list live update on current ui

Co-Authored-By: Ivan Iskandar <12537387+ivaniskandar@users.noreply.github.com>

* Review Changes

Co-authored-by: Ivan Iskandar <12537387+ivaniskandar@users.noreply.github.com>
This commit is contained in:
AntsyLich 2022-06-11 21:38:39 +06:00 committed by GitHub
parent 1e64542f14
commit 120943a8b3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 478 additions and 162 deletions

View File

@ -2,6 +2,7 @@ package eu.kanade.data.chapter
import eu.kanade.data.DatabaseHandler
import eu.kanade.data.toLong
import eu.kanade.domain.chapter.model.Chapter
import eu.kanade.domain.chapter.model.ChapterUpdate
import eu.kanade.domain.chapter.repository.ChapterRepository
import eu.kanade.tachiyomi.util.system.logcat
@ -11,6 +12,33 @@ class ChapterRepositoryImpl(
private val handler: DatabaseHandler,
) : ChapterRepository {
override suspend fun addAll(chapters: List<Chapter>): List<Chapter> {
return try {
handler.await(inTransaction = true) {
chapters.map { chapter ->
chaptersQueries.insert(
chapter.mangaId,
chapter.url,
chapter.name,
chapter.scanlator,
chapter.read,
chapter.bookmark,
chapter.lastPageRead,
chapter.chapterNumber,
chapter.sourceOrder,
chapter.dateFetch,
chapter.dateUpload,
)
val lastInsertId = chaptersQueries.selectLastInsertedRowId().executeAsOne()
chapter.copy(id = lastInsertId)
}
}
} catch (e: Exception) {
logcat(LogPriority.ERROR, e)
emptyList()
}
}
override suspend fun update(chapterUpdate: ChapterUpdate) {
try {
handler.await {
@ -33,4 +61,46 @@ class ChapterRepositoryImpl(
logcat(LogPriority.ERROR, e)
}
}
override suspend fun updateAll(chapterUpdates: List<ChapterUpdate>) {
try {
handler.await(inTransaction = true) {
chapterUpdates.forEach { chapterUpdate ->
chaptersQueries.update(
chapterUpdate.mangaId,
chapterUpdate.url,
chapterUpdate.name,
chapterUpdate.scanlator,
chapterUpdate.read?.toLong(),
chapterUpdate.bookmark?.toLong(),
chapterUpdate.lastPageRead,
chapterUpdate.chapterNumber?.toDouble(),
chapterUpdate.sourceOrder,
chapterUpdate.dateFetch,
chapterUpdate.dateUpload,
chapterId = chapterUpdate.id,
)
}
}
} catch (e: Exception) {
logcat(LogPriority.ERROR, e)
}
}
override suspend fun removeChaptersWithIds(chapterIds: List<Long>) {
try {
handler.await { chaptersQueries.removeChaptersWithIds(chapterIds) }
} catch (e: Exception) {
logcat(LogPriority.ERROR, e)
}
}
override suspend fun getChapterByMangaId(mangaId: Long): List<Chapter> {
return try {
handler.awaitList { chaptersQueries.getChapterByMangaId(mangaId, chapterMapper) }
} catch (e: Exception) {
logcat(LogPriority.ERROR, e)
emptyList()
}
}
}

View File

@ -24,4 +24,12 @@ class MangaRepositoryImpl(
false
}
}
override suspend fun updateLastUpdate(mangaId: Long, lastUpdate: Long) {
try {
handler.await { mangasQueries.updateLastUpdate(lastUpdate, mangaId) }
} catch (e: Exception) {
logcat(LogPriority.ERROR, e)
}
}
}

View File

@ -4,6 +4,8 @@ import eu.kanade.data.chapter.ChapterRepositoryImpl
import eu.kanade.data.history.HistoryRepositoryImpl
import eu.kanade.data.manga.MangaRepositoryImpl
import eu.kanade.data.source.SourceRepositoryImpl
import eu.kanade.domain.chapter.interactor.ShouldUpdateDbChapter
import eu.kanade.domain.chapter.interactor.SyncChaptersWithSource
import eu.kanade.domain.chapter.interactor.UpdateChapter
import eu.kanade.domain.chapter.repository.ChapterRepository
import eu.kanade.domain.extension.interactor.GetExtensionLanguages
@ -19,6 +21,7 @@ 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.ResetViewerFlags
import eu.kanade.domain.manga.interactor.UpdateMangaLastUpdate
import eu.kanade.domain.manga.repository.MangaRepository
import eu.kanade.domain.source.interactor.GetEnabledSources
import eu.kanade.domain.source.interactor.GetLanguagesWithSources
@ -42,9 +45,12 @@ class DomainModule : InjektModule {
addFactory { GetFavoritesBySourceId(get()) }
addFactory { GetNextChapter(get()) }
addFactory { ResetViewerFlags(get()) }
addFactory { UpdateMangaLastUpdate(get()) }
addSingletonFactory<ChapterRepository> { ChapterRepositoryImpl(get()) }
addFactory { UpdateChapter(get()) }
addFactory { ShouldUpdateDbChapter() }
addFactory { SyncChaptersWithSource(get(), get(), get(), get()) }
addSingletonFactory<HistoryRepository> { HistoryRepositoryImpl(get()) }
addFactory { DeleteHistoryTable(get()) }

View File

@ -0,0 +1,13 @@
package eu.kanade.domain.chapter.interactor
import eu.kanade.domain.chapter.model.Chapter
class ShouldUpdateDbChapter {
fun await(dbChapter: Chapter, sourceChapter: Chapter): Boolean {
return dbChapter.scanlator != sourceChapter.scanlator || dbChapter.name != sourceChapter.name ||
dbChapter.dateUpload != sourceChapter.dateUpload ||
dbChapter.chapterNumber != sourceChapter.chapterNumber ||
dbChapter.sourceOrder != sourceChapter.sourceOrder
}
}

View File

@ -0,0 +1,179 @@
package eu.kanade.domain.chapter.interactor
import eu.kanade.data.chapter.NoChaptersException
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.model.Manga
import eu.kanade.domain.manga.model.toDbManga
import eu.kanade.tachiyomi.data.download.DownloadManager
import eu.kanade.tachiyomi.source.LocalSource
import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.util.chapter.ChapterRecognition
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.lang.Long.max
import java.util.Date
import java.util.TreeSet
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(),
) {
suspend fun await(
rawSourceChapters: List<SChapter>,
manga: Manga,
source: Source,
): Pair<List<Chapter>, List<Chapter>> {
if (rawSourceChapters.isEmpty() && source.id != LocalSource.ID) {
throw NoChaptersException()
}
val sourceChapters = rawSourceChapters
.distinctBy { it.url }
.mapIndexed { i, sChapter ->
Chapter.create()
.copyFromSChapter(sChapter)
.copy(mangaId = manga.id, sourceOrder = i.toLong())
}
// Chapters from db.
val dbChapters = chapterRepository.getChapterByMangaId(manga.id)
// Chapters from the source not in db.
val toAdd = mutableListOf<Chapter>()
// Chapters whose metadata have changed.
val toChange = mutableListOf<Chapter>()
// Chapters from the db not in source.
val toDelete = dbChapters.filterNot { dbChapter ->
sourceChapters.any { sourceChapter ->
dbChapter.url == sourceChapter.url
}
}
val rightNow = Date().time
// Used to not set upload date of older chapters
// to a higher value than newer chapters
var maxSeenUploadDate = 0L
val sManga = manga.toSManga()
for (sourceChapter in sourceChapters) {
var chapter = sourceChapter
// Update metadata from source if necessary.
if (source is HttpSource) {
val sChapter = chapter.toSChapter()
source.prepareNewChapter(sChapter, sManga)
chapter = chapter.copyFromSChapter(sChapter)
}
// Recognize chapter number for the chapter.
val chapterNumber = ChapterRecognition.parseChapterNumber(manga.title, chapter.name, chapter.chapterNumber)
chapter = chapter.copy(chapterNumber = chapterNumber)
val dbChapter = dbChapters.find { it.url == chapter.url }
if (dbChapter == null) {
if (chapter.dateUpload == 0L) {
val altDateUpload = if (maxSeenUploadDate == 0L) rightNow else maxSeenUploadDate
chapter = chapter.copy(dateUpload = altDateUpload)
} else {
maxSeenUploadDate = max(maxSeenUploadDate, sourceChapter.dateUpload)
}
toAdd.add(chapter)
} else {
if (shouldUpdateDbChapter.await(dbChapter, chapter)) {
if (dbChapter.name != chapter.name && downloadManager.isChapterDownloaded(dbChapter.toDbChapter(), manga.toDbManga())) {
downloadManager.renameChapter(source, manga.toDbManga(), dbChapter.toDbChapter(), chapter.toDbChapter())
}
chapter = dbChapter.copy(
name = sourceChapter.name,
chapterNumber = sourceChapter.chapterNumber,
scanlator = sourceChapter.scanlator,
sourceOrder = sourceChapter.sourceOrder,
)
if (sourceChapter.dateUpload != 0L) {
chapter = chapter.copy(dateUpload = sourceChapter.dateUpload)
}
toChange.add(chapter)
}
}
}
// Return if there's nothing to add, delete or change, avoiding unnecessary db transactions.
if (toAdd.isEmpty() && toDelete.isEmpty() && toChange.isEmpty()) {
return Pair(emptyList(), emptyList())
}
val reAdded = mutableListOf<Chapter>()
val deletedChapterNumbers = TreeSet<Float>()
val deletedReadChapterNumbers = TreeSet<Float>()
toDelete.forEach { chapter ->
if (chapter.read) {
deletedReadChapterNumbers.add(chapter.chapterNumber)
}
deletedChapterNumbers.add(chapter.chapterNumber)
}
val deletedChapterNumberDateFetchMap = toDelete.sortedByDescending { it.dateFetch }
.associate { it.chapterNumber to it.dateFetch }
// Date fetch is set in such a way that the upper ones will have bigger value than the lower ones
// Sources MUST return the chapters from most to less recent, which is common.
val now = Date().time
var itemCount = toAdd.size
var updatedToAdd = toAdd.map { toAddItem ->
var chapter = toAddItem.copy(dateFetch = now + itemCount--)
if (chapter.isRecognizedNumber.not() && chapter.chapterNumber !in deletedChapterNumbers) return@map chapter
if (chapter.chapterNumber in deletedReadChapterNumbers) {
chapter = chapter.copy(read = true)
}
// Try to to use the fetch date of the original entry to not pollute 'Updates' tab
val oldDateFetch = deletedChapterNumberDateFetchMap[chapter.chapterNumber]
oldDateFetch?.let {
chapter = chapter.copy(dateFetch = it)
}
reAdded.add(chapter)
chapter
}
if (toDelete.isNotEmpty()) {
val toDeleteIds = toDelete.map { it.id }
chapterRepository.removeChaptersWithIds(toDeleteIds)
}
if (updatedToAdd.isNotEmpty()) {
updatedToAdd = chapterRepository.addAll(updatedToAdd)
}
if (toChange.isNotEmpty()) {
val chapterUpdates = toChange.map { it.toChapterUpdate() }
chapterRepository.updateAll(chapterUpdates)
}
// 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)
@Suppress("ConvertArgumentToSet") // See tachiyomiorg/tachiyomi#6372.
return Pair(updatedToAdd.subtract(reAdded).toList(), toDelete.subtract(reAdded).toList())
}
}

View File

@ -1,5 +1,8 @@
package eu.kanade.domain.chapter.model
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.data.database.models.Chapter as DbChapter
data class Chapter(
val id: Long,
val mangaId: Long,
@ -13,4 +16,61 @@ data class Chapter(
val dateUpload: Long,
val chapterNumber: Float,
val scanlator: String?,
)
) {
val isRecognizedNumber: Boolean
get() = chapterNumber >= 0f
fun toSChapter(): SChapter {
return SChapter.create().also {
it.url = url
it.name = name
it.date_upload = dateUpload
it.chapter_number = chapterNumber
it.scanlator = scanlator
}
}
fun copyFromSChapter(sChapter: SChapter): Chapter {
return this.copy(
name = sChapter.name,
url = sChapter.url,
dateUpload = sChapter.date_upload,
chapterNumber = sChapter.chapter_number,
scanlator = sChapter.scanlator,
)
}
companion object {
fun create(): Chapter {
return Chapter(
id = -1,
mangaId = -1,
read = false,
bookmark = false,
lastPageRead = 0,
dateFetch = 0,
sourceOrder = 0,
url = "",
name = "",
dateUpload = -1,
chapterNumber = -1f,
scanlator = null,
)
}
}
}
// TODO: Remove when all deps are migrated
fun Chapter.toDbChapter(): DbChapter = DbChapter.create().also {
it.id = id
it.manga_id = mangaId
it.url = url
it.name = name
it.scanlator = scanlator
it.read = read
it.bookmark = bookmark
it.last_page_read = lastPageRead.toInt()
it.date_fetch = dateFetch
it.chapter_number = chapterNumber
it.source_order = sourceOrder.toInt()
}

View File

@ -14,3 +14,7 @@ data class ChapterUpdate(
val chapterNumber: Float? = null,
val scanlator: String? = null,
)
fun Chapter.toChapterUpdate(): ChapterUpdate {
return ChapterUpdate(id, mangaId, read, bookmark, lastPageRead, dateFetch, sourceOrder, url, name, dateUpload, chapterNumber, scanlator)
}

View File

@ -1,8 +1,17 @@
package eu.kanade.domain.chapter.repository
import eu.kanade.domain.chapter.model.Chapter
import eu.kanade.domain.chapter.model.ChapterUpdate
interface ChapterRepository {
suspend fun addAll(chapters: List<Chapter>): List<Chapter>
suspend fun update(chapterUpdate: ChapterUpdate)
suspend fun updateAll(chapterUpdates: List<ChapterUpdate>)
suspend fun removeChaptersWithIds(chapterIds: List<Long>)
suspend fun getChapterByMangaId(mangaId: Long): List<Chapter>
}

View File

@ -0,0 +1,12 @@
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)
}
}

View File

@ -1,5 +1,8 @@
package eu.kanade.domain.manga.model
import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.data.database.models.Manga as DbManga
data class Manga(
val id: Long,
val source: Long,
@ -23,6 +26,20 @@ data class Manga(
val sorting: Long
get() = chapterFlags and CHAPTER_SORTING_MASK
fun toSManga(): SManga {
return SManga.create().also {
it.url = url
it.title = title
it.artist = artist
it.author = author
it.description = description
it.genre = genre.orEmpty().joinToString()
it.status = status.toInt()
it.thumbnail_url = thumbnailUrl
it.initialized = initialized
}
}
companion object {
// Generic filter that does not filter anything
@ -34,3 +51,14 @@ data class Manga(
const val CHAPTER_SORTING_MASK = 0x00000300L
}
}
// TODO: Remove when all deps are migrated
fun Manga.toDbManga(): DbManga = DbManga.create(url, title, source).also {
it.id = id
it.favorite = favorite
it.last_update = lastUpdate
it.date_added = dateAdded
it.viewer_flags = viewerFlags.toInt()
it.chapter_flags = chapterFlags.toInt()
it.cover_last_modified = coverLastModified
}

View File

@ -8,4 +8,6 @@ interface MangaRepository {
fun getFavoritesBySourceId(sourceId: Long): Flow<List<Manga>>
suspend fun resetViewerFlags(): Boolean
suspend fun updateLastUpdate(mangaId: Long, lastUpdate: Long)
}

View File

@ -43,7 +43,7 @@ abstract class AbstractBackupManager(protected val context: Context) {
internal suspend fun restoreChapters(source: Source, manga: Manga, chapters: List<Chapter>): Pair<List<Chapter>, List<Chapter>> {
val fetchedChapters = source.getChapterList(manga.toMangaInfo())
.map { it.toSChapter() }
val syncedChapters = syncChaptersWithSource(db, fetchedChapters, manga, source)
val syncedChapters = syncChaptersWithSource(fetchedChapters, manga, source)
if (syncedChapters.first.isNotEmpty()) {
chapters.forEach { it.manga_id = manga.id }
updateChapters(chapters)

View File

@ -4,6 +4,7 @@ import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.ui.reader.setting.OrientationType
import eu.kanade.tachiyomi.ui.reader.setting.ReadingModeType
import tachiyomi.source.model.MangaInfo
import eu.kanade.domain.manga.model.Manga as DomainManga
interface Manga : SManga {
@ -128,3 +129,26 @@ fun Manga.toMangaInfo(): MangaInfo {
title = this.title,
)
}
fun Manga.toDomainManga(): DomainManga? {
val mangaId = id ?: return null
return DomainManga(
id = mangaId,
source = source,
favorite = favorite,
lastUpdate = last_update,
dateAdded = date_added,
viewerFlags = viewer_flags.toLong(),
chapterFlags = chapter_flags.toLong(),
coverLastModified = cover_last_modified,
url = url,
title = title,
artist = artist,
author = author,
description = description,
genre = getGenres(),
status = status.toLong(),
thumbnailUrl = thumbnail_url,
initialized = initialized,
)
}

View File

@ -426,7 +426,7 @@ class LibraryUpdateService(
// [dbmanga] was used so that manga data doesn't get overwritten
// in case manga gets new chapter
return syncChaptersWithSource(db, chapters, dbManga, source)
return syncChaptersWithSource(chapters, dbManga, source)
}
private suspend fun updateCovers() {

View File

@ -362,8 +362,7 @@ abstract class HttpSource : CatalogueSource {
* @param chapter the chapter to be added.
* @param manga the manga of the chapter.
*/
open fun prepareNewChapter(chapter: SChapter, manga: SManga) {
}
open fun prepareNewChapter(chapter: SChapter, manga: SManga) {}
/**
* Returns the list of filters for the source.

View File

@ -115,7 +115,7 @@ class SearchPresenter(
// Update chapters read
if (migrateChapters) {
try {
syncChaptersWithSource(db, sourceChapters, manga, source)
syncChaptersWithSource(sourceChapters, manga, source)
} catch (e: Exception) {
// Worst case, chapters won't be synced
}

View File

@ -417,7 +417,7 @@ class MangaPresenter(
val chapters = source.getChapterList(manga.toMangaInfo())
.map { it.toSChapter() }
val (newChapters, _) = syncChaptersWithSource(db, chapters, manga, source)
val (newChapters, _) = syncChaptersWithSource(chapters, manga, source)
if (manualFetch) {
downloadNewChapters(newChapters)
}

View File

@ -1,175 +1,37 @@
package eu.kanade.tachiyomi.util.chapter
import eu.kanade.data.chapter.NoChaptersException
import eu.kanade.tachiyomi.data.database.DatabaseHelper
import eu.kanade.tachiyomi.data.database.models.Chapter
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.download.DownloadManager
import eu.kanade.tachiyomi.source.LocalSource
import eu.kanade.domain.chapter.interactor.SyncChaptersWithSource
import eu.kanade.domain.chapter.model.toDbChapter
import eu.kanade.tachiyomi.data.database.models.toDomainManga
import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.online.HttpSource
import kotlinx.coroutines.runBlocking
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.util.Date
import java.util.TreeSet
import kotlin.math.max
import eu.kanade.tachiyomi.data.database.models.Chapter as DbChapter
import eu.kanade.tachiyomi.data.database.models.Manga as DbManga
/**
* Helper method for syncing the list of chapters from the source with the ones from the database.
*
* @param db the database.
* @param rawSourceChapters a list of chapters from the source.
* @param manga the manga of the chapters.
* @param source the source of the chapters.
* @return a pair of new insertions and deletions.
*/
fun syncChaptersWithSource(
db: DatabaseHelper,
rawSourceChapters: List<SChapter>,
manga: Manga,
manga: DbManga,
source: Source,
): Pair<List<Chapter>, List<Chapter>> {
if (rawSourceChapters.isEmpty() && source !is LocalSource) {
throw NoChaptersException()
syncChaptersWithSource: SyncChaptersWithSource = Injekt.get(),
): Pair<List<DbChapter>, List<DbChapter>> {
val domainManga = manga.toDomainManga() ?: return Pair(emptyList(), emptyList())
val (added, deleted) = runBlocking {
syncChaptersWithSource.await(rawSourceChapters, domainManga, source)
}
val downloadManager: DownloadManager = Injekt.get()
val addedDbChapters = added.map { it.toDbChapter() }
val deletedDbChapters = deleted.map { it.toDbChapter() }
// Chapters from db.
val dbChapters = db.getChapters(manga).executeAsBlocking()
val sourceChapters = rawSourceChapters
.distinctBy { it.url }
.mapIndexed { i, sChapter ->
Chapter.create().apply {
copyFrom(sChapter)
manga_id = manga.id
source_order = i
}
}
// Chapters from the source not in db.
val toAdd = mutableListOf<Chapter>()
// Chapters whose metadata have changed.
val toChange = mutableListOf<Chapter>()
// Chapters from the db not in source.
val toDelete = dbChapters.filterNot { dbChapter ->
sourceChapters.any { sourceChapter ->
dbChapter.url == sourceChapter.url
}
}
var maxTimestamp = 0L // in previous chapters to add
val rightNow = Date().time
for (sourceChapter in sourceChapters) {
// This forces metadata update for the main viewable things in the chapter list.
if (source is HttpSource) {
source.prepareNewChapter(sourceChapter, manga)
}
// Recognize chapter number for the chapter.
sourceChapter.chapter_number = ChapterRecognition.parseChapterNumber(manga.title, sourceChapter.name, sourceChapter.chapter_number)
val dbChapter = dbChapters.find { it.url == sourceChapter.url }
// Add the chapter if not in db already, or update if the metadata changed.
if (dbChapter == null) {
if (sourceChapter.date_upload == 0L) {
sourceChapter.date_upload = if (maxTimestamp == 0L) rightNow else maxTimestamp
} else {
maxTimestamp = max(maxTimestamp, sourceChapter.date_upload)
}
toAdd.add(sourceChapter)
} else {
if (shouldUpdateDbChapter(dbChapter, sourceChapter)) {
if (dbChapter.name != sourceChapter.name && downloadManager.isChapterDownloaded(dbChapter, manga)) {
downloadManager.renameChapter(source, manga, dbChapter, sourceChapter)
}
dbChapter.scanlator = sourceChapter.scanlator
dbChapter.name = sourceChapter.name
dbChapter.chapter_number = sourceChapter.chapter_number
dbChapter.source_order = sourceChapter.source_order
if (sourceChapter.date_upload != 0L) {
dbChapter.date_upload = sourceChapter.date_upload
}
toChange.add(dbChapter)
}
}
}
// Return if there's nothing to add, delete or change, avoiding unnecessary db transactions.
if (toAdd.isEmpty() && toDelete.isEmpty() && toChange.isEmpty()) {
return Pair(emptyList(), emptyList())
}
// Keep it a List instead of a Set. See #6372.
val readded = mutableListOf<Chapter>()
db.inTransaction {
val deletedChapterNumbers = TreeSet<Float>()
val deletedReadChapterNumbers = TreeSet<Float>()
if (toDelete.isNotEmpty()) {
for (chapter in toDelete) {
if (chapter.read) {
deletedReadChapterNumbers.add(chapter.chapter_number)
}
deletedChapterNumbers.add(chapter.chapter_number)
}
db.deleteChapters(toDelete).executeAsBlocking()
}
if (toAdd.isNotEmpty()) {
// Set the date fetch for new items in reverse order to allow another sorting method.
// Sources MUST return the chapters from most to less recent, which is common.
var now = Date().time
for (i in toAdd.indices.reversed()) {
val chapter = toAdd[i]
chapter.date_fetch = now++
if (chapter.isRecognizedNumber && chapter.chapter_number in deletedChapterNumbers) {
// Try to mark already read chapters as read when the source deletes them
if (chapter.chapter_number in deletedReadChapterNumbers) {
chapter.read = true
}
// Try to to use the fetch date it originally had to not pollute 'Updates' tab
toDelete.filter { it.chapter_number == chapter.chapter_number }
.minByOrNull { it.date_fetch }!!.let {
chapter.date_fetch = it.date_fetch
}
readded.add(chapter)
}
}
val chapters = db.insertChapters(toAdd).executeAsBlocking()
toAdd.forEach { chapter ->
chapter.id = chapters.results().getValue(chapter).insertedId()
}
}
if (toChange.isNotEmpty()) {
db.insertChapters(toChange).executeAsBlocking()
}
// Fix order in source.
db.fixChaptersSourceOrder(sourceChapters).executeAsBlocking()
// Set this manga as updated since chapters were changed
// Note that last_update actually represents last time the chapter list changed at all
manga.last_update = Date().time
db.updateLastUpdated(manga).executeAsBlocking()
}
@Suppress("ConvertArgumentToSet")
return Pair(toAdd.subtract(readded).toList(), toDelete.subtract(readded).toList())
}
private fun shouldUpdateDbChapter(dbChapter: Chapter, sourceChapter: Chapter): Boolean {
return dbChapter.scanlator != sourceChapter.scanlator || dbChapter.name != sourceChapter.name ||
dbChapter.date_upload != sourceChapter.date_upload ||
dbChapter.chapter_number != sourceChapter.chapter_number ||
dbChapter.source_order != sourceChapter.source_order
return Pair(addedDbChapters, deletedDbChapters)
}

View File

@ -28,6 +28,38 @@ SELECT *
FROM chapters
WHERE manga_id = :mangaId;
removeChaptersWithIds:
DELETE FROM chapters
WHERE _id IN :chapterIds;
insert:
INSERT INTO chapters(
manga_id,
url,
name,
scanlator,
read,
bookmark,
last_page_read,
chapter_number,
source_order,
date_fetch,
date_upload
)
VALUES (
:mangaId,
:url,
:name,
:scanlator,
:read,
:bookmark,
:lastPageRead,
:chapterNumber,
:sourceOrder,
:dateFetch,
:dateUpload
);
update:
UPDATE chapters
SET manga_id = coalesce(:mangaId, manga_id),
@ -41,4 +73,7 @@ SET manga_id = coalesce(:mangaId, manga_id),
source_order = coalesce(:sourceOrder, source_order),
date_fetch = coalesce(:dateFetch, date_fetch),
date_upload = coalesce(:dateUpload, date_upload)
WHERE _id = :chapterId;
WHERE _id = :chapterId;
selectLastInsertedRowId:
SELECT last_insert_rowid();

View File

@ -56,4 +56,9 @@ GROUP BY source;
deleteMangasNotInLibraryBySourceIds:
DELETE FROM mangas
WHERE favorite = 0 AND source IN :sourceIds;
WHERE favorite = 0 AND source IN :sourceIds;
updateLastUpdate:
UPDATE mangas
SET last_update = :lastUpdate
WHERE _id = :mangaId;