mirror of
https://github.com/mihonapp/mihon.git
synced 2024-11-21 20:47:03 -05: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:
parent
1e64542f14
commit
120943a8b3
20 changed files with 478 additions and 162 deletions
|
@ -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),
|
||||
|
@ -42,3 +74,6 @@ SET manga_id = coalesce(:mangaId, manga_id),
|
|||
date_fetch = coalesce(:dateFetch, date_fetch),
|
||||
date_upload = coalesce(:dateUpload, date_upload)
|
||||
WHERE _id = :chapterId;
|
||||
|
||||
selectLastInsertedRowId:
|
||||
SELECT last_insert_rowid();
|
|
@ -57,3 +57,8 @@ GROUP BY source;
|
|||
deleteMangasNotInLibraryBySourceIds:
|
||||
DELETE FROM mangas
|
||||
WHERE favorite = 0 AND source IN :sourceIds;
|
||||
|
||||
updateLastUpdate:
|
||||
UPDATE mangas
|
||||
SET last_update = :lastUpdate
|
||||
WHERE _id = :mangaId;
|
||||
|
|
Loading…
Reference in a new issue