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

View File

@ -2,7 +2,7 @@ package eu.kanade.data.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 ->
Manga(
id = id,
@ -24,6 +24,8 @@ val mangaMapper: (Long, Long, String, String?, String?, String?, List<String>?,
// SY <--
thumbnailUrl = thumbnailUrl,
initialized = initialized,
// SY -->
filteredScanlators = filteredScanlators,
// SY <--
)
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,12 @@
package eu.kanade.domain.manga.interactor
import eu.kanade.domain.manga.repository.MangaRepository
class UpdateMangaLastUpdate(
private val mangaRepository: MangaRepository,
) {
suspend fun await(mangaId: Long, lastUpdate: Long) {
mangaRepository.updateLastUpdate(mangaId, lastUpdate)
}
}

View File

@ -1,7 +1,9 @@
package eu.kanade.domain.manga.model
import eu.kanade.tachiyomi.data.library.CustomMangaManager
import eu.kanade.tachiyomi.source.model.SManga
import uy.kohesive.injekt.injectLazy
import eu.kanade.tachiyomi.data.database.models.Manga as DbManga
data class Manga(
val id: Long,
@ -24,8 +26,8 @@ data class Manga(
val thumbnailUrl: String?,
val initialized: Boolean,
// SY -->
val filteredScanlators: String?,
// SY <--
val filteredScanlators: List<String>?,
// SY <--
) {
// SY -->
@ -55,6 +57,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
@ -70,3 +86,14 @@ data class Manga(
// 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>>
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.dateAdapter
import eu.kanade.data.listOfStringsAdapter
import eu.kanade.data.listOfStringsAndAdapter
import eu.kanade.tachiyomi.data.cache.ChapterCache
import eu.kanade.tachiyomi.data.cache.CoverCache
import eu.kanade.tachiyomi.data.database.DatabaseHelper
@ -68,6 +69,9 @@ class AppModule(val app: Application) : InjektModule {
),
mangasAdapter = Mangas.Adapter(
genreAdapter = listOfStringsAdapter,
// SY -->
filtered_scanlatorsAdapter = listOfStringsAndAdapter,
// SY <--
),
)
}

View File

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

View File

@ -3,7 +3,9 @@ package eu.kanade.tachiyomi.data.database.models
import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.ui.reader.setting.OrientationType
import eu.kanade.tachiyomi.ui.reader.setting.ReadingModeType
import exh.md.utils.MdUtil
import tachiyomi.source.model.MangaInfo
import eu.kanade.domain.manga.model.Manga as DomainManga
interface Manga : SManga {
@ -136,3 +138,27 @@ 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,
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
// in case manga gets new chapter
return syncChaptersWithSource(db, chapters, dbManga, source)
return syncChaptersWithSource(chapters, dbManga, source)
}
private suspend fun updateCovers() {

View File

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

View File

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

View File

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

View File

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

View File

@ -1,191 +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 exh.source.isEhBasedManga
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(/* 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
return Pair(addedDbChapters, deletedDbChapters)
}

View File

@ -151,7 +151,7 @@ class GalleryAdder {
}.map { it.toSChapter() }
if (chapterList.isNotEmpty()) {
syncChaptersWithSource(db, chapterList, manga, source)
syncChaptersWithSource(chapterList, manga, source)
}
} catch (e: Exception) {
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())
.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()
} catch (t: Throwable) {
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.Pururin
import eu.kanade.tachiyomi.source.online.english.Tsumino
import eu.kanade.domain.manga.model.Manga as DomainManga
/**
* Source helpers
@ -101,6 +102,8 @@ fun Source.isMdBasedSource() = id in mangaDexSourceIds
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) {
this.source()
} else {

View File

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

View File

@ -20,7 +20,7 @@ CREATE TABLE mangas(
chapter_flags INTEGER NOT NULL,
cover_last_modified 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;
@ -67,4 +67,9 @@ WHERE favorite = 0 AND source IN :sourceIdsAND AND _id NOT IN (
SELECT manga_id FROM merged WHERE manga_id != merge_id
) AND _id NOT IN (
SELECT manga_id FROM chapters WHERE read = 1 OR last_page_read != 0
);
);
updateLastUpdate:
UPDATE mangas
SET last_update = :lastUpdate
WHERE _id = :mangaId;