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
@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
@ -42,9 +45,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()) }
|
||||||
|
@ -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
|
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()
|
||||||
|
}
|
||||||
|
@ -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)
|
||||||
|
}
|
||||||
|
@ -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>
|
||||||
}
|
}
|
||||||
|
@ -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
|
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(
|
data class Manga(
|
||||||
val id: Long,
|
val id: Long,
|
||||||
val source: Long,
|
val source: Long,
|
||||||
@ -23,6 +26,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
|
||||||
@ -34,3 +51,14 @@ data class Manga(
|
|||||||
const val CHAPTER_SORTING_MASK = 0x00000300L
|
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>>
|
fun getFavoritesBySourceId(sourceId: Long): Flow<List<Manga>>
|
||||||
|
|
||||||
suspend fun resetViewerFlags(): Boolean
|
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>> {
|
internal suspend fun restoreChapters(source: Source, manga: Manga, chapters: List<Chapter>): Pair<List<Chapter>, List<Chapter>> {
|
||||||
val fetchedChapters = source.getChapterList(manga.toMangaInfo())
|
val fetchedChapters = source.getChapterList(manga.toMangaInfo())
|
||||||
.map { it.toSChapter() }
|
.map { it.toSChapter() }
|
||||||
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)
|
||||||
|
@ -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.OrientationType
|
||||||
import eu.kanade.tachiyomi.ui.reader.setting.ReadingModeType
|
import eu.kanade.tachiyomi.ui.reader.setting.ReadingModeType
|
||||||
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 {
|
||||||
|
|
||||||
@ -128,3 +129,26 @@ 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,
|
||||||
|
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
|
// [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() {
|
||||||
|
@ -362,8 +362,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.
|
||||||
|
@ -115,7 +115,7 @@ class SearchPresenter(
|
|||||||
// Update chapters read
|
// Update chapters read
|
||||||
if (migrateChapters) {
|
if (migrateChapters) {
|
||||||
try {
|
try {
|
||||||
syncChaptersWithSource(db, sourceChapters, manga, source)
|
syncChaptersWithSource(sourceChapters, manga, source)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
// Worst case, chapters won't be synced
|
// Worst case, chapters won't be synced
|
||||||
}
|
}
|
||||||
|
@ -417,7 +417,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)
|
||||||
}
|
}
|
||||||
|
@ -1,175 +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 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(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
|
|
||||||
}
|
}
|
||||||
|
@ -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),
|
||||||
@ -41,4 +73,7 @@ SET manga_id = coalesce(:mangaId, manga_id),
|
|||||||
source_order = coalesce(:sourceOrder, source_order),
|
source_order = coalesce(:sourceOrder, source_order),
|
||||||
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();
|
@ -56,4 +56,9 @@ GROUP BY source;
|
|||||||
|
|
||||||
deleteMangasNotInLibraryBySourceIds:
|
deleteMangasNotInLibraryBySourceIds:
|
||||||
DELETE FROM mangas
|
DELETE FROM mangas
|
||||||
WHERE favorite = 0 AND source IN :sourceIds;
|
WHERE favorite = 0 AND source IN :sourceIds;
|
||||||
|
|
||||||
|
updateLastUpdate:
|
||||||
|
UPDATE mangas
|
||||||
|
SET last_update = :lastUpdate
|
||||||
|
WHERE _id = :mangaId;
|
||||||
|
Loading…
Reference in New Issue
Block a user