mirror of
				https://github.com/mihonapp/mihon.git
				synced 2025-11-04 08:08:55 +01:00 
			
		
		
		
	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:
		@@ -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()
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -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)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -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()) }
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -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())
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -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()
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -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)
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -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>
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -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)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -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
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -8,4 +8,6 @@ interface MangaRepository {
 | 
			
		||||
    fun getFavoritesBySourceId(sourceId: Long): Flow<List<Manga>>
 | 
			
		||||
 | 
			
		||||
    suspend fun resetViewerFlags(): Boolean
 | 
			
		||||
 | 
			
		||||
    suspend fun updateLastUpdate(mangaId: Long, lastUpdate: Long)
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -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)
 | 
			
		||||
 
 | 
			
		||||
@@ -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,
 | 
			
		||||
    )
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -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() {
 | 
			
		||||
 
 | 
			
		||||
@@ -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.
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
                }
 | 
			
		||||
 
 | 
			
		||||
@@ -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)
 | 
			
		||||
                }
 | 
			
		||||
 
 | 
			
		||||
@@ -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)
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -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();
 | 
			
		||||
@@ -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;
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user