diff --git a/app/src/main/java/eu/kanade/data/DatabaseAdapter.kt b/app/src/main/java/eu/kanade/data/DatabaseAdapter.kt index d51e2c514..727d2cc16 100644 --- a/app/src/main/java/eu/kanade/data/DatabaseAdapter.kt +++ b/app/src/main/java/eu/kanade/data/DatabaseAdapter.kt @@ -18,3 +18,16 @@ val listOfStringsAdapter = object : ColumnAdapter, String> { } override fun encode(value: List) = value.joinToString(separator = listOfStringsSeparator) } + +// SY --> +private const val listOfStringsAndSeparator = " & " +val listOfStringsAndAdapter = object : ColumnAdapter, String> { + override fun decode(databaseValue: String) = + if (databaseValue.isEmpty()) { + emptyList() + } else { + databaseValue.split(listOfStringsAndSeparator) + } + override fun encode(value: List) = value.joinToString(separator = listOfStringsAndSeparator) +} +// SY <-- diff --git a/app/src/main/java/eu/kanade/data/chapter/ChapterRepositoryImpl.kt b/app/src/main/java/eu/kanade/data/chapter/ChapterRepositoryImpl.kt index 1fa5d9fd2..58fb236cc 100644 --- a/app/src/main/java/eu/kanade/data/chapter/ChapterRepositoryImpl.kt +++ b/app/src/main/java/eu/kanade/data/chapter/ChapterRepositoryImpl.kt @@ -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): List { + 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) { + 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) { + try { + handler.await { chaptersQueries.removeChaptersWithIds(chapterIds) } + } catch (e: Exception) { + logcat(LogPriority.ERROR, e) + } + } + + override suspend fun getChapterByMangaId(mangaId: Long): List { + return try { + handler.awaitList { chaptersQueries.getChapterByMangaId(mangaId, chapterMapper) } + } catch (e: Exception) { + logcat(LogPriority.ERROR, e) + emptyList() + } + } } diff --git a/app/src/main/java/eu/kanade/data/manga/MangaMapper.kt b/app/src/main/java/eu/kanade/data/manga/MangaMapper.kt index 13df33008..da6041628 100644 --- a/app/src/main/java/eu/kanade/data/manga/MangaMapper.kt +++ b/app/src/main/java/eu/kanade/data/manga/MangaMapper.kt @@ -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, Long, String?, Boolean, Long?, Long?, Boolean, Long, Long, Long, Long, String?) -> Manga = +val mangaMapper: (Long, Long, String, String?, String?, String?, List?, String, Long, String?, Boolean, Long?, Long?, Boolean, Long, Long, Long, Long, List?) -> 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?, // SY <-- thumbnailUrl = thumbnailUrl, initialized = initialized, + // SY --> filteredScanlators = filteredScanlators, + // SY <-- ) } diff --git a/app/src/main/java/eu/kanade/data/manga/MangaRepositoryImpl.kt b/app/src/main/java/eu/kanade/data/manga/MangaRepositoryImpl.kt index f5d0788f6..addc23864 100644 --- a/app/src/main/java/eu/kanade/data/manga/MangaRepositoryImpl.kt +++ b/app/src/main/java/eu/kanade/data/manga/MangaRepositoryImpl.kt @@ -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) + } + } } diff --git a/app/src/main/java/eu/kanade/domain/DomainModule.kt b/app/src/main/java/eu/kanade/domain/DomainModule.kt index c8f1f3998..c728d9cba 100644 --- a/app/src/main/java/eu/kanade/domain/DomainModule.kt +++ b/app/src/main/java/eu/kanade/domain/DomainModule.kt @@ -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 { ChapterRepositoryImpl(get()) } addFactory { UpdateChapter(get()) } + addFactory { ShouldUpdateDbChapter() } + addFactory { SyncChaptersWithSource(get(), get(), get(), get()) } addSingletonFactory { HistoryRepositoryImpl(get()) } addFactory { DeleteHistoryTable(get()) } diff --git a/app/src/main/java/eu/kanade/domain/chapter/interactor/ShouldUpdateDbChapter.kt b/app/src/main/java/eu/kanade/domain/chapter/interactor/ShouldUpdateDbChapter.kt new file mode 100644 index 000000000..e0d455feb --- /dev/null +++ b/app/src/main/java/eu/kanade/domain/chapter/interactor/ShouldUpdateDbChapter.kt @@ -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 + } +} diff --git a/app/src/main/java/eu/kanade/domain/chapter/interactor/SyncChaptersWithSource.kt b/app/src/main/java/eu/kanade/domain/chapter/interactor/SyncChaptersWithSource.kt new file mode 100644 index 000000000..0591b8d10 --- /dev/null +++ b/app/src/main/java/eu/kanade/domain/chapter/interactor/SyncChaptersWithSource.kt @@ -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, + manga: Manga, + source: Source, + ): Pair, List> { + 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() + + // Chapters whose metadata have changed. + val toChange = mutableListOf() + + // 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() + + val deletedChapterNumbers = TreeSet() + val deletedReadChapterNumbers = TreeSet() + + 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()) + } +} diff --git a/app/src/main/java/eu/kanade/domain/chapter/model/Chapter.kt b/app/src/main/java/eu/kanade/domain/chapter/model/Chapter.kt index 27ed9a46f..ce9005786 100644 --- a/app/src/main/java/eu/kanade/domain/chapter/model/Chapter.kt +++ b/app/src/main/java/eu/kanade/domain/chapter/model/Chapter.kt @@ -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() +} diff --git a/app/src/main/java/eu/kanade/domain/chapter/model/ChapterUpdate.kt b/app/src/main/java/eu/kanade/domain/chapter/model/ChapterUpdate.kt index 2c9042c47..8e073f358 100644 --- a/app/src/main/java/eu/kanade/domain/chapter/model/ChapterUpdate.kt +++ b/app/src/main/java/eu/kanade/domain/chapter/model/ChapterUpdate.kt @@ -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) +} diff --git a/app/src/main/java/eu/kanade/domain/chapter/repository/ChapterRepository.kt b/app/src/main/java/eu/kanade/domain/chapter/repository/ChapterRepository.kt index 9f3bb73d7..9ffa37260 100644 --- a/app/src/main/java/eu/kanade/domain/chapter/repository/ChapterRepository.kt +++ b/app/src/main/java/eu/kanade/domain/chapter/repository/ChapterRepository.kt @@ -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): List + suspend fun update(chapterUpdate: ChapterUpdate) + + suspend fun updateAll(chapterUpdates: List) + + suspend fun removeChaptersWithIds(chapterIds: List) + + suspend fun getChapterByMangaId(mangaId: Long): List } diff --git a/app/src/main/java/eu/kanade/domain/manga/interactor/UpdateMangaLastUpdate.kt b/app/src/main/java/eu/kanade/domain/manga/interactor/UpdateMangaLastUpdate.kt new file mode 100644 index 000000000..641192744 --- /dev/null +++ b/app/src/main/java/eu/kanade/domain/manga/interactor/UpdateMangaLastUpdate.kt @@ -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) + } +} diff --git a/app/src/main/java/eu/kanade/domain/manga/model/Manga.kt b/app/src/main/java/eu/kanade/domain/manga/model/Manga.kt index 751acfec8..df5fe85e4 100644 --- a/app/src/main/java/eu/kanade/domain/manga/model/Manga.kt +++ b/app/src/main/java/eu/kanade/domain/manga/model/Manga.kt @@ -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?, + // 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 +} diff --git a/app/src/main/java/eu/kanade/domain/manga/repository/MangaRepository.kt b/app/src/main/java/eu/kanade/domain/manga/repository/MangaRepository.kt index 0dcf68082..83367d103 100644 --- a/app/src/main/java/eu/kanade/domain/manga/repository/MangaRepository.kt +++ b/app/src/main/java/eu/kanade/domain/manga/repository/MangaRepository.kt @@ -8,4 +8,6 @@ interface MangaRepository { fun getFavoritesBySourceId(sourceId: Long): Flow> suspend fun resetViewerFlags(): Boolean + + suspend fun updateLastUpdate(mangaId: Long, lastUpdate: Long) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/AppModule.kt b/app/src/main/java/eu/kanade/tachiyomi/AppModule.kt index 89e07e86e..7d9b3273b 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/AppModule.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/AppModule.kt @@ -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 <-- ), ) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/AbstractBackupManager.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/AbstractBackupManager.kt index f74daef15..ad28b3e27 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/AbstractBackupManager.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/AbstractBackupManager.kt @@ -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) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/models/Manga.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/models/Manga.kt index 4fa3a1449..2c26755d5 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/data/database/models/Manga.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/database/models/Manga.kt @@ -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(), + ) +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateService.kt b/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateService.kt index 80b33023d..5aa800206 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateService.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateService.kt @@ -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() { diff --git a/app/src/main/java/eu/kanade/tachiyomi/source/online/HttpSource.kt b/app/src/main/java/eu/kanade/tachiyomi/source/online/HttpSource.kt index 5082c5f3d..ab64a7faf 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/source/online/HttpSource.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/source/online/HttpSource.kt @@ -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. diff --git a/app/src/main/java/eu/kanade/tachiyomi/source/online/all/MergedSource.kt b/app/src/main/java/eu/kanade/tachiyomi/source/online/all/MergedSource.kt index 5190aeb09..c326e1e08 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/source/online/all/MergedSource.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/source/online/all/MergedSource.kt @@ -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, diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/advanced/process/MigrationListController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/advanced/process/MigrationListController.kt index 9377ce474..ab6af9137 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/advanced/process/MigrationListController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/advanced/process/MigrationListController.kt @@ -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 } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaPresenter.kt index bfaf8ede0..5106b9bd9 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaPresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaPresenter.kt @@ -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) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/chapter/ChapterSourceSync.kt b/app/src/main/java/eu/kanade/tachiyomi/util/chapter/ChapterSourceSync.kt index 558b22ae3..cec3615c6 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/util/chapter/ChapterSourceSync.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/util/chapter/ChapterSourceSync.kt @@ -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, - manga: Manga, + manga: DbManga, source: Source, -): Pair, List> { - if (rawSourceChapters.isEmpty() && source !is LocalSource) { - throw NoChaptersException() + syncChaptersWithSource: SyncChaptersWithSource = Injekt.get(), +): Pair, List> { + 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() - - // Chapters whose metadata have changed. - val toChange = mutableListOf() - - // 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() - - db.inTransaction { - val deletedChapterNumbers = TreeSet() - val deletedReadChapterNumbers = TreeSet() - - 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) } diff --git a/app/src/main/java/exh/GalleryAdder.kt b/app/src/main/java/exh/GalleryAdder.kt index 63611157f..9d896f9ca 100755 --- a/app/src/main/java/exh/GalleryAdder.kt +++ b/app/src/main/java/exh/GalleryAdder.kt @@ -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) diff --git a/app/src/main/java/exh/eh/EHentaiUpdateWorker.kt b/app/src/main/java/exh/eh/EHentaiUpdateWorker.kt index 2fa60b298..b46d178a0 100644 --- a/app/src/main/java/exh/eh/EHentaiUpdateWorker.kt +++ b/app/src/main/java/exh/eh/EHentaiUpdateWorker.kt @@ -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) { diff --git a/app/src/main/java/exh/source/SourceHelper.kt b/app/src/main/java/exh/source/SourceHelper.kt index 7ed69054b..9a3802b61 100644 --- a/app/src/main/java/exh/source/SourceHelper.kt +++ b/app/src/main/java/exh/source/SourceHelper.kt @@ -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 { diff --git a/app/src/main/sqldelight/data/chapters.sq b/app/src/main/sqldelight/data/chapters.sq index 226d9b6bd..c7d8634a5 100644 --- a/app/src/main/sqldelight/data/chapters.sq +++ b/app/src/main/sqldelight/data/chapters.sq @@ -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; \ No newline at end of file +WHERE _id = :chapterId; + +selectLastInsertedRowId: +SELECT last_insert_rowid(); \ No newline at end of file diff --git a/app/src/main/sqldelight/data/mangas.sq b/app/src/main/sqldelight/data/mangas.sq index d94e60798..1afd1c6c3 100644 --- a/app/src/main/sqldelight/data/mangas.sq +++ b/app/src/main/sqldelight/data/mangas.sq @@ -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 ); 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 -); \ No newline at end of file +); + +updateLastUpdate: +UPDATE mangas +SET last_update = :lastUpdate +WHERE _id = :mangaId;