ChapterSourceSync.kt 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161
  1. package eu.kanade.tachiyomi.util.chapter
  2. import eu.kanade.tachiyomi.data.database.DatabaseHelper
  3. import eu.kanade.tachiyomi.data.database.models.Chapter
  4. import eu.kanade.tachiyomi.data.database.models.Manga
  5. import eu.kanade.tachiyomi.data.download.DownloadManager
  6. import eu.kanade.tachiyomi.source.Source
  7. import eu.kanade.tachiyomi.source.model.SChapter
  8. import eu.kanade.tachiyomi.source.online.HttpSource
  9. import uy.kohesive.injekt.Injekt
  10. import uy.kohesive.injekt.api.get
  11. import java.util.Date
  12. import java.util.TreeSet
  13. /**
  14. * Helper method for syncing the list of chapters from the source with the ones from the database.
  15. *
  16. * @param db the database.
  17. * @param rawSourceChapters a list of chapters from the source.
  18. * @param manga the manga of the chapters.
  19. * @param source the source of the chapters.
  20. * @return a pair of new insertions and deletions.
  21. */
  22. fun syncChaptersWithSource(
  23. db: DatabaseHelper,
  24. rawSourceChapters: List<SChapter>,
  25. manga: Manga,
  26. source: Source
  27. ): Pair<List<Chapter>, List<Chapter>> {
  28. if (rawSourceChapters.isEmpty()) {
  29. throw NoChaptersException()
  30. }
  31. val downloadManager: DownloadManager = Injekt.get()
  32. // Chapters from db.
  33. val dbChapters = db.getChapters(manga).executeAsBlocking()
  34. val sourceChapters = rawSourceChapters
  35. .distinctBy { it.url }
  36. .mapIndexed { i, sChapter ->
  37. Chapter.create().apply {
  38. copyFrom(sChapter)
  39. manga_id = manga.id
  40. source_order = i
  41. }
  42. }
  43. // Chapters from the source not in db.
  44. val toAdd = mutableListOf<Chapter>()
  45. // Chapters whose metadata have changed.
  46. val toChange = mutableListOf<Chapter>()
  47. // Chapters from the db not in source.
  48. val toDelete = dbChapters.filterNot { dbChapter ->
  49. sourceChapters.any { sourceChapter ->
  50. dbChapter.url == sourceChapter.url
  51. }
  52. }
  53. for (sourceChapter in sourceChapters) {
  54. // This forces metadata update for the main viewable things in the chapter list.
  55. if (source is HttpSource) {
  56. source.prepareNewChapter(sourceChapter, manga)
  57. }
  58. // Recognize chapter number for the chapter.
  59. ChapterRecognition.parseChapterNumber(sourceChapter, manga)
  60. val dbChapter = dbChapters.find { it.url == sourceChapter.url }
  61. // Add the chapter if not in db already, or update if the metadata changed.
  62. if (dbChapter == null) {
  63. if (sourceChapter.date_upload == 0L) {
  64. sourceChapter.date_upload = Date().time
  65. }
  66. toAdd.add(sourceChapter)
  67. } else {
  68. if (shouldUpdateDbChapter(dbChapter, sourceChapter)) {
  69. if (dbChapter.name != sourceChapter.name && downloadManager.isChapterDownloaded(dbChapter, manga)) {
  70. downloadManager.renameChapter(source, manga, dbChapter, sourceChapter)
  71. }
  72. dbChapter.scanlator = sourceChapter.scanlator
  73. dbChapter.name = sourceChapter.name
  74. dbChapter.chapter_number = sourceChapter.chapter_number
  75. dbChapter.source_order = sourceChapter.source_order
  76. if (sourceChapter.date_upload != 0L) {
  77. dbChapter.date_upload = sourceChapter.date_upload
  78. }
  79. toChange.add(dbChapter)
  80. }
  81. }
  82. }
  83. // Return if there's nothing to add, delete or change, avoiding unnecessary db transactions.
  84. if (toAdd.isEmpty() && toDelete.isEmpty() && toChange.isEmpty()) {
  85. return Pair(emptyList(), emptyList())
  86. }
  87. val readded = mutableSetOf<Chapter>()
  88. db.inTransaction {
  89. val deletedChapterNumbers = TreeSet<Float>()
  90. val deletedReadChapterNumbers = TreeSet<Float>()
  91. if (toDelete.isNotEmpty()) {
  92. for (chapter in toDelete) {
  93. if (chapter.read) {
  94. deletedReadChapterNumbers.add(chapter.chapter_number)
  95. }
  96. deletedChapterNumbers.add(chapter.chapter_number)
  97. }
  98. db.deleteChapters(toDelete).executeAsBlocking()
  99. }
  100. if (toAdd.isNotEmpty()) {
  101. // Set the date fetch for new items in reverse order to allow another sorting method.
  102. // Sources MUST return the chapters from most to less recent, which is common.
  103. var now = Date().time
  104. for (i in toAdd.indices.reversed()) {
  105. val chapter = toAdd[i]
  106. chapter.date_fetch = now++
  107. // Try to mark already read chapters as read when the source deletes them
  108. if (chapter.isRecognizedNumber && chapter.chapter_number in deletedReadChapterNumbers) {
  109. chapter.read = true
  110. }
  111. if (chapter.isRecognizedNumber && chapter.chapter_number in deletedChapterNumbers) {
  112. readded.add(chapter)
  113. }
  114. }
  115. val chapters = db.insertChapters(toAdd).executeAsBlocking()
  116. toAdd.forEach { chapter ->
  117. chapter.id = chapters.results().getValue(chapter).insertedId()
  118. }
  119. }
  120. if (toChange.isNotEmpty()) {
  121. db.insertChapters(toChange).executeAsBlocking()
  122. }
  123. // Fix order in source.
  124. db.fixChaptersSourceOrder(sourceChapters).executeAsBlocking()
  125. // Set this manga as updated since chapters were changed
  126. // Note that last_update actually represents last time the chapter list changed at all
  127. manga.last_update = Date().time
  128. db.updateLastUpdated(manga).executeAsBlocking()
  129. }
  130. return Pair(toAdd.subtract(readded).toList(), toDelete.subtract(readded).toList())
  131. }
  132. private fun shouldUpdateDbChapter(dbChapter: Chapter, sourceChapter: Chapter): Boolean {
  133. return dbChapter.scanlator != sourceChapter.scanlator || dbChapter.name != sourceChapter.name ||
  134. dbChapter.date_upload != sourceChapter.date_upload ||
  135. dbChapter.chapter_number != sourceChapter.chapter_number ||
  136. dbChapter.source_order != sourceChapter.source_order
  137. }
  138. class NoChaptersException : Exception()