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>
(cherry picked from commit 120943a8b37eaf847ca1073676a8293288c28e12)

# Conflicts:
#	app/src/main/java/eu/kanade/domain/manga/model/Manga.kt
#	app/src/main/java/eu/kanade/tachiyomi/data/backup/AbstractBackupManager.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/SearchPresenter.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaPresenter.kt
#	app/src/main/java/eu/kanade/tachiyomi/util/chapter/ChapterSourceSync.kt
#	app/src/main/sqldelight/data/mangas.sq
This commit is contained in:
AntsyLich 2022-06-11 21:38:39 +06:00 committed by Jobobby04
parent d9430e4eae
commit d1d9c53af3
27 changed files with 527 additions and 187 deletions

View File

@ -18,3 +18,16 @@ val listOfStringsAdapter = object : ColumnAdapter<List<String>, String> {
} }
override fun encode(value: List<String>) = value.joinToString(separator = listOfStringsSeparator) override fun encode(value: List<String>) = value.joinToString(separator = listOfStringsSeparator)
} }
// SY -->
private const val listOfStringsAndSeparator = " & "
val listOfStringsAndAdapter = object : ColumnAdapter<List<String>, String> {
override fun decode(databaseValue: String) =
if (databaseValue.isEmpty()) {
emptyList()
} else {
databaseValue.split(listOfStringsAndSeparator)
}
override fun encode(value: List<String>) = value.joinToString(separator = listOfStringsAndSeparator)
}
// SY <--

View File

@ -2,6 +2,7 @@ package eu.kanade.data.chapter
import eu.kanade.data.DatabaseHandler import eu.kanade.data.DatabaseHandler
import eu.kanade.data.toLong import eu.kanade.data.toLong
import eu.kanade.domain.chapter.model.Chapter
import eu.kanade.domain.chapter.model.ChapterUpdate import eu.kanade.domain.chapter.model.ChapterUpdate
import eu.kanade.domain.chapter.repository.ChapterRepository import eu.kanade.domain.chapter.repository.ChapterRepository
import eu.kanade.tachiyomi.util.system.logcat import eu.kanade.tachiyomi.util.system.logcat
@ -11,6 +12,33 @@ class ChapterRepositoryImpl(
private val handler: DatabaseHandler, private val handler: DatabaseHandler,
) : ChapterRepository { ) : 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) { override suspend fun update(chapterUpdate: ChapterUpdate) {
try { try {
handler.await { handler.await {
@ -33,4 +61,46 @@ class ChapterRepositoryImpl(
logcat(LogPriority.ERROR, e) 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

@ -2,7 +2,7 @@ package eu.kanade.data.manga
import eu.kanade.domain.manga.model.Manga import eu.kanade.domain.manga.model.Manga
val mangaMapper: (Long, Long, String, String?, String?, String?, List<String>?, String, Long, String?, Boolean, Long?, Long?, Boolean, Long, Long, Long, Long, String?) -> Manga = val mangaMapper: (Long, Long, String, String?, String?, String?, List<String>?, String, Long, String?, Boolean, Long?, Long?, Boolean, Long, Long, Long, Long, List<String>?) -> Manga =
{ id, source, url, artist, author, description, genre, title, status, thumbnailUrl, favorite, lastUpdate, _, initialized, viewer, chapterFlags, coverLastModified, dateAdded, filteredScanlators -> { id, source, url, artist, author, description, genre, title, status, thumbnailUrl, favorite, lastUpdate, _, initialized, viewer, chapterFlags, coverLastModified, dateAdded, filteredScanlators ->
Manga( Manga(
id = id, id = id,
@ -24,6 +24,8 @@ val mangaMapper: (Long, Long, String, String?, String?, String?, List<String>?,
// SY <-- // SY <--
thumbnailUrl = thumbnailUrl, thumbnailUrl = thumbnailUrl,
initialized = initialized, initialized = initialized,
// SY -->
filteredScanlators = filteredScanlators, filteredScanlators = filteredScanlators,
// SY <--
) )
} }

View File

@ -24,4 +24,12 @@ class MangaRepositoryImpl(
false 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.history.HistoryRepositoryImpl
import eu.kanade.data.manga.MangaRepositoryImpl import eu.kanade.data.manga.MangaRepositoryImpl
import eu.kanade.data.source.SourceRepositoryImpl 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.interactor.UpdateChapter
import eu.kanade.domain.chapter.repository.ChapterRepository import eu.kanade.domain.chapter.repository.ChapterRepository
import eu.kanade.domain.extension.interactor.GetExtensionLanguages 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.history.repository.HistoryRepository
import eu.kanade.domain.manga.interactor.GetFavoritesBySourceId import eu.kanade.domain.manga.interactor.GetFavoritesBySourceId
import eu.kanade.domain.manga.interactor.ResetViewerFlags 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.manga.repository.MangaRepository
import eu.kanade.domain.source.interactor.GetEnabledSources import eu.kanade.domain.source.interactor.GetEnabledSources
import eu.kanade.domain.source.interactor.GetLanguagesWithSources import eu.kanade.domain.source.interactor.GetLanguagesWithSources
@ -47,9 +50,12 @@ class DomainModule : InjektModule {
addFactory { GetFavoritesBySourceId(get()) } addFactory { GetFavoritesBySourceId(get()) }
addFactory { GetNextChapter(get()) } addFactory { GetNextChapter(get()) }
addFactory { ResetViewerFlags(get()) } addFactory { ResetViewerFlags(get()) }
addFactory { UpdateMangaLastUpdate(get()) }
addSingletonFactory<ChapterRepository> { ChapterRepositoryImpl(get()) } addSingletonFactory<ChapterRepository> { ChapterRepositoryImpl(get()) }
addFactory { UpdateChapter(get()) } addFactory { UpdateChapter(get()) }
addFactory { ShouldUpdateDbChapter() }
addFactory { SyncChaptersWithSource(get(), get(), get(), get()) }
addSingletonFactory<HistoryRepository> { HistoryRepositoryImpl(get()) } addSingletonFactory<HistoryRepository> { HistoryRepositoryImpl(get()) }
addFactory { DeleteHistoryTable(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,196 @@
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 exh.source.isEhBasedManga
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
}
// --> EXH (carry over reading progress)
if (manga.isEhBasedManga()) {
val finalAdded = updatedToAdd.subtract(reAdded)
if (finalAdded.isNotEmpty()) {
val max = dbChapters.maxOfOrNull { it.lastPageRead }
if (max != null && max > 0) {
updatedToAdd = updatedToAdd.map {
if (it !in reAdded) {
it.copy(lastPageRead = max)
} else it
}
}
}
}
// <-- EXH
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 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( data class Chapter(
val id: Long, val id: Long,
val mangaId: Long, val mangaId: Long,
@ -13,4 +16,61 @@ data class Chapter(
val dateUpload: Long, val dateUpload: Long,
val chapterNumber: Float, val chapterNumber: Float,
val scanlator: String?, 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 chapterNumber: Float? = null,
val scanlator: String? = 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 package eu.kanade.domain.chapter.repository
import eu.kanade.domain.chapter.model.Chapter
import eu.kanade.domain.chapter.model.ChapterUpdate import eu.kanade.domain.chapter.model.ChapterUpdate
interface ChapterRepository { interface ChapterRepository {
suspend fun addAll(chapters: List<Chapter>): List<Chapter>
suspend fun update(chapterUpdate: ChapterUpdate) 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,7 +1,9 @@
package eu.kanade.domain.manga.model package eu.kanade.domain.manga.model
import eu.kanade.tachiyomi.data.library.CustomMangaManager import eu.kanade.tachiyomi.data.library.CustomMangaManager
import eu.kanade.tachiyomi.source.model.SManga
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
import eu.kanade.tachiyomi.data.database.models.Manga as DbManga
data class Manga( data class Manga(
val id: Long, val id: Long,
@ -24,8 +26,8 @@ data class Manga(
val thumbnailUrl: String?, val thumbnailUrl: String?,
val initialized: Boolean, val initialized: Boolean,
// SY --> // SY -->
val filteredScanlators: String?, val filteredScanlators: List<String>?,
// SY <-- // SY <--
) { ) {
// SY --> // SY -->
@ -55,6 +57,20 @@ data class Manga(
val sorting: Long val sorting: Long
get() = chapterFlags and CHAPTER_SORTING_MASK 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 { companion object {
// Generic filter that does not filter anything // Generic filter that does not filter anything
@ -70,3 +86,14 @@ data class Manga(
// SY <-- // SY <--
} }
} }
// 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>> fun getFavoritesBySourceId(sourceId: Long): Flow<List<Manga>>
suspend fun resetViewerFlags(): Boolean suspend fun resetViewerFlags(): Boolean
suspend fun updateLastUpdate(mangaId: Long, lastUpdate: Long)
} }

View File

@ -13,6 +13,7 @@ import eu.kanade.data.AndroidDatabaseHandler
import eu.kanade.data.DatabaseHandler import eu.kanade.data.DatabaseHandler
import eu.kanade.data.dateAdapter import eu.kanade.data.dateAdapter
import eu.kanade.data.listOfStringsAdapter import eu.kanade.data.listOfStringsAdapter
import eu.kanade.data.listOfStringsAndAdapter
import eu.kanade.tachiyomi.data.cache.ChapterCache import eu.kanade.tachiyomi.data.cache.ChapterCache
import eu.kanade.tachiyomi.data.cache.CoverCache import eu.kanade.tachiyomi.data.cache.CoverCache
import eu.kanade.tachiyomi.data.database.DatabaseHelper import eu.kanade.tachiyomi.data.database.DatabaseHelper
@ -68,6 +69,9 @@ class AppModule(val app: Application) : InjektModule {
), ),
mangasAdapter = Mangas.Adapter( mangasAdapter = Mangas.Adapter(
genreAdapter = listOfStringsAdapter, genreAdapter = listOfStringsAdapter,
// SY -->
filtered_scanlatorsAdapter = listOfStringsAndAdapter,
// SY <--
), ),
) )
} }

View File

@ -59,7 +59,7 @@ abstract class AbstractBackupManager(protected val context: Context) {
.map { it.toSChapter() } .map { it.toSChapter() }
} }
// SY <-- // SY <--
val syncedChapters = syncChaptersWithSource(db, fetchedChapters, manga, source) val syncedChapters = syncChaptersWithSource(fetchedChapters, manga, source)
if (syncedChapters.first.isNotEmpty()) { if (syncedChapters.first.isNotEmpty()) {
chapters.forEach { it.manga_id = manga.id } chapters.forEach { it.manga_id = manga.id }
updateChapters(chapters) updateChapters(chapters)

View File

@ -3,7 +3,9 @@ package eu.kanade.tachiyomi.data.database.models
import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.ui.reader.setting.OrientationType import eu.kanade.tachiyomi.ui.reader.setting.OrientationType
import eu.kanade.tachiyomi.ui.reader.setting.ReadingModeType import eu.kanade.tachiyomi.ui.reader.setting.ReadingModeType
import exh.md.utils.MdUtil
import tachiyomi.source.model.MangaInfo import tachiyomi.source.model.MangaInfo
import eu.kanade.domain.manga.model.Manga as DomainManga
interface Manga : SManga { interface Manga : SManga {
@ -136,3 +138,27 @@ fun Manga.toMangaInfo(): MangaInfo {
title = this.title, 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,
ogTitle = title,
ogArtist = artist,
ogAuthor = author,
ogDescription = description,
ogGenre = getGenres(),
ogStatus = status.toLong(),
thumbnailUrl = thumbnail_url,
initialized = initialized,
filteredScanlators = MdUtil.getScanlators(filtered_scanlators).toList(),
)
}

View File

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

View File

@ -397,8 +397,7 @@ abstract class HttpSource : CatalogueSource {
* @param chapter the chapter to be added. * @param chapter the chapter to be added.
* @param manga the manga of the chapter. * @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. * Returns the list of filters for the source.

View File

@ -175,7 +175,7 @@ class MergedSource : HttpSource() {
val chapterList = source.getChapterList(loadedManga.toMangaInfo()) val chapterList = source.getChapterList(loadedManga.toMangaInfo())
.map(ChapterInfo::toSChapter) .map(ChapterInfo::toSChapter)
val results = val results =
syncChaptersWithSource(db, chapterList, loadedManga, source) syncChaptersWithSource(chapterList, loadedManga, source)
if (ifDownloadNewChapters && reference.downloadChapters) { if (ifDownloadNewChapters && reference.downloadChapters) {
downloadManager.downloadChapters( downloadManager.downloadChapters(
loadedManga, loadedManga,

View File

@ -190,7 +190,7 @@ class MigrationListController(bundle: Bundle? = null) :
} }
try { try {
syncChaptersWithSource(db, chapters.map { it.toSChapter() }, localManga, source) syncChaptersWithSource(chapters.map { it.toSChapter() }, localManga, source)
} catch (e: Exception) { } catch (e: Exception) {
return@async2 null return@async2 null
} }
@ -230,7 +230,7 @@ class MigrationListController(bundle: Bundle? = null) :
emptyList() emptyList()
} }
withIOContext { withIOContext {
syncChaptersWithSource(db, chapters, localManga, source) syncChaptersWithSource(chapters, localManga, source)
} }
localManga localManga
} else null } else null
@ -363,7 +363,7 @@ class MigrationListController(bundle: Bundle? = null) :
try { try {
val chapters = source.getChapterList(localManga.toMangaInfo()) val chapters = source.getChapterList(localManga.toMangaInfo())
.map { it.toSChapter() } .map { it.toSChapter() }
syncChaptersWithSource(db, chapters, localManga, source) syncChaptersWithSource(chapters, localManga, source)
} catch (e: Exception) { } catch (e: Exception) {
return@async null return@async null
} }

View File

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

View File

@ -1,191 +1,37 @@
package eu.kanade.tachiyomi.util.chapter package eu.kanade.tachiyomi.util.chapter
import eu.kanade.data.chapter.NoChaptersException import eu.kanade.domain.chapter.interactor.SyncChaptersWithSource
import eu.kanade.tachiyomi.data.database.DatabaseHelper import eu.kanade.domain.chapter.model.toDbChapter
import eu.kanade.tachiyomi.data.database.models.Chapter import eu.kanade.tachiyomi.data.database.models.toDomainManga
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.download.DownloadManager
import eu.kanade.tachiyomi.source.LocalSource
import eu.kanade.tachiyomi.source.Source import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.model.SChapter import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.online.HttpSource import kotlinx.coroutines.runBlocking
import exh.source.isEhBasedManga
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import java.util.Date import eu.kanade.tachiyomi.data.database.models.Chapter as DbChapter
import java.util.TreeSet import eu.kanade.tachiyomi.data.database.models.Manga as DbManga
import kotlin.math.max
/** /**
* Helper method for syncing the list of chapters from the source with the ones from the database. * 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 rawSourceChapters a list of chapters from the source.
* @param manga the manga of the chapters. * @param manga the manga of the chapters.
* @param source the source of the chapters. * @param source the source of the chapters.
* @return a pair of new insertions and deletions. * @return a pair of new insertions and deletions.
*/ */
fun syncChaptersWithSource( fun syncChaptersWithSource(
db: DatabaseHelper,
rawSourceChapters: List<SChapter>, rawSourceChapters: List<SChapter>,
manga: Manga, manga: DbManga,
source: Source, source: Source,
): Pair<List<Chapter>, List<Chapter>> { syncChaptersWithSource: SyncChaptersWithSource = Injekt.get(),
if (rawSourceChapters.isEmpty() && source !is LocalSource) { ): Pair<List<DbChapter>, List<DbChapter>> {
throw NoChaptersException() 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. return Pair(addedDbChapters, deletedDbChapters)
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(/* SY --> */ manga.originalTitle /* SY <-- */, 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)
}
}
// --> EXH (carry over reading progress)
if (manga.isEhBasedManga()) {
val finalAdded = toAdd.subtract(readded)
if (finalAdded.isNotEmpty()) {
val max = dbChapters.maxOfOrNull { it.last_page_read }
if (max != null && max > 0) {
finalAdded.forEach { chapter ->
chapter.last_page_read = max
}
}
}
}
// <-- EXH
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
} }

View File

@ -151,7 +151,7 @@ class GalleryAdder {
}.map { it.toSChapter() } }.map { it.toSChapter() }
if (chapterList.isNotEmpty()) { if (chapterList.isNotEmpty()) {
syncChaptersWithSource(db, chapterList, manga, source) syncChaptersWithSource(chapterList, manga, source)
} }
} catch (e: Exception) { } catch (e: Exception) {
logger.w(context.getString(R.string.gallery_adder_chapter_fetch_error, manga.title), e) logger.w(context.getString(R.string.gallery_adder_chapter_fetch_error, manga.title), e)

View File

@ -210,7 +210,7 @@ class EHentaiUpdateWorker(private val context: Context, workerParams: WorkerPara
val newChapters = source.getChapterList(manga.toMangaInfo()) val newChapters = source.getChapterList(manga.toMangaInfo())
.map { it.toSChapter() } .map { it.toSChapter() }
val (new, _) = syncChaptersWithSource(db, newChapters, manga, source) // Not suspending, but does block, maybe fix this? val (new, _) = syncChaptersWithSource(newChapters, manga, source) // Not suspending, but does block, maybe fix this?
return new to db.getChapters(manga).executeOnIO() return new to db.getChapters(manga).executeOnIO()
} catch (t: Throwable) { } catch (t: Throwable) {
if (t is EHentai.GalleryNotFoundException) { if (t is EHentai.GalleryNotFoundException) {

View File

@ -11,6 +11,7 @@ import eu.kanade.tachiyomi.source.online.english.EightMuses
import eu.kanade.tachiyomi.source.online.english.HBrowse import eu.kanade.tachiyomi.source.online.english.HBrowse
import eu.kanade.tachiyomi.source.online.english.Pururin import eu.kanade.tachiyomi.source.online.english.Pururin
import eu.kanade.tachiyomi.source.online.english.Tsumino import eu.kanade.tachiyomi.source.online.english.Tsumino
import eu.kanade.domain.manga.model.Manga as DomainManga
/** /**
* Source helpers * Source helpers
@ -101,6 +102,8 @@ fun Source.isMdBasedSource() = id in mangaDexSourceIds
fun Manga.isEhBasedManga() = source == EH_SOURCE_ID || source == EXH_SOURCE_ID fun Manga.isEhBasedManga() = source == EH_SOURCE_ID || source == EXH_SOURCE_ID
fun DomainManga.isEhBasedManga() = source == EH_SOURCE_ID || source == EXH_SOURCE_ID
fun Source.getMainSource(): Source = if (this is EnhancedHttpSource) { fun Source.getMainSource(): Source = if (this is EnhancedHttpSource) {
this.source() this.source()
} else { } else {

View File

@ -28,6 +28,38 @@ SELECT *
FROM chapters FROM chapters
WHERE manga_id = :mangaId; 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:
UPDATE chapters UPDATE chapters
SET manga_id = coalesce(:mangaId, manga_id), 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_fetch = coalesce(:dateFetch, date_fetch),
date_upload = coalesce(:dateUpload, date_upload) date_upload = coalesce(:dateUpload, date_upload)
WHERE _id = :chapterId; WHERE _id = :chapterId;
selectLastInsertedRowId:
SELECT last_insert_rowid();

View File

@ -20,7 +20,7 @@ CREATE TABLE mangas(
chapter_flags INTEGER NOT NULL, chapter_flags INTEGER NOT NULL,
cover_last_modified INTEGER AS Long NOT NULL, cover_last_modified INTEGER AS Long NOT NULL,
date_added INTEGER AS Long NOT NULL, date_added INTEGER AS Long NOT NULL,
filtered_scanlators TEXT filtered_scanlators TEXT AS List<String>
); );
CREATE INDEX library_favorite_index ON mangas(favorite) WHERE favorite = 1; CREATE INDEX library_favorite_index ON mangas(favorite) WHERE favorite = 1;
@ -68,3 +68,8 @@ WHERE favorite = 0 AND source IN :sourceIdsAND AND _id NOT IN (
) AND _id NOT IN ( ) AND _id NOT IN (
SELECT manga_id FROM chapters WHERE read = 1 OR last_page_read != 0 SELECT manga_id FROM chapters WHERE read = 1 OR last_page_read != 0
); );
updateLastUpdate:
UPDATE mangas
SET last_update = :lastUpdate
WHERE _id = :mangaId;