DownloadCache.kt 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278
  1. package eu.kanade.tachiyomi.data.download
  2. import android.content.Context
  3. import android.net.Uri
  4. import com.hippo.unifile.UniFile
  5. import eu.kanade.tachiyomi.data.database.models.Chapter
  6. import eu.kanade.tachiyomi.data.database.models.Manga
  7. import eu.kanade.tachiyomi.data.preference.PreferencesHelper
  8. import eu.kanade.tachiyomi.source.SourceManager
  9. import java.util.concurrent.TimeUnit
  10. import kotlinx.coroutines.flow.onEach
  11. import uy.kohesive.injekt.Injekt
  12. import uy.kohesive.injekt.api.get
  13. /**
  14. * Cache where we dump the downloads directory from the filesystem. This class is needed because
  15. * directory checking is expensive and it slowdowns the app. The cache is invalidated by the time
  16. * defined in [renewInterval] as we don't have any control over the filesystem and the user can
  17. * delete the folders at any time without the app noticing.
  18. *
  19. * @param context the application context.
  20. * @param provider the downloads directories provider.
  21. * @param sourceManager the source manager.
  22. * @param preferences the preferences of the app.
  23. */
  24. class DownloadCache(
  25. private val context: Context,
  26. private val provider: DownloadProvider,
  27. private val sourceManager: SourceManager,
  28. private val preferences: PreferencesHelper = Injekt.get()
  29. ) {
  30. /**
  31. * The interval after which this cache should be invalidated. 1 hour shouldn't cause major
  32. * issues, as the cache is only used for UI feedback.
  33. */
  34. private val renewInterval = TimeUnit.HOURS.toMillis(1)
  35. /**
  36. * The last time the cache was refreshed.
  37. */
  38. private var lastRenew = 0L
  39. /**
  40. * The root directory for downloads.
  41. */
  42. private var rootDir = RootDirectory(getDirectoryFromPreference())
  43. init {
  44. preferences.downloadsDirectory().asFlow()
  45. .onEach {
  46. lastRenew = 0L // invalidate cache
  47. rootDir = RootDirectory(getDirectoryFromPreference())
  48. }
  49. }
  50. /**
  51. * Returns the downloads directory from the user's preferences.
  52. */
  53. private fun getDirectoryFromPreference(): UniFile {
  54. val dir = preferences.downloadsDirectory().get()
  55. return UniFile.fromUri(context, Uri.parse(dir))
  56. }
  57. /**
  58. * Returns true if the chapter is downloaded.
  59. *
  60. * @param chapter the chapter to check.
  61. * @param manga the manga of the chapter.
  62. * @param skipCache whether to skip the directory cache and check in the filesystem.
  63. */
  64. fun isChapterDownloaded(chapter: Chapter, manga: Manga, skipCache: Boolean): Boolean {
  65. if (skipCache) {
  66. val source = sourceManager.get(manga.source) ?: return false
  67. return provider.findChapterDir(chapter, manga, source) != null
  68. }
  69. checkRenew()
  70. val sourceDir = rootDir.files[manga.source]
  71. if (sourceDir != null) {
  72. val mangaDir = sourceDir.files[provider.getMangaDirName(manga)]
  73. if (mangaDir != null) {
  74. return provider.getChapterDirName(chapter) in mangaDir.files
  75. }
  76. }
  77. return false
  78. }
  79. /**
  80. * Returns the amount of downloaded chapters for a manga.
  81. *
  82. * @param manga the manga to check.
  83. */
  84. fun getDownloadCount(manga: Manga): Int {
  85. checkRenew()
  86. val sourceDir = rootDir.files[manga.source]
  87. if (sourceDir != null) {
  88. val mangaDir = sourceDir.files[provider.getMangaDirName(manga)]
  89. if (mangaDir != null) {
  90. return mangaDir.files
  91. .filter { !it.endsWith(Downloader.TMP_DIR_SUFFIX) }
  92. .size
  93. }
  94. }
  95. return 0
  96. }
  97. /**
  98. * Checks if the cache needs a renewal and performs it if needed.
  99. */
  100. @Synchronized
  101. private fun checkRenew() {
  102. if (lastRenew + renewInterval < System.currentTimeMillis()) {
  103. renew()
  104. lastRenew = System.currentTimeMillis()
  105. }
  106. }
  107. /**
  108. * Renews the downloads cache.
  109. */
  110. private fun renew() {
  111. val onlineSources = sourceManager.getOnlineSources()
  112. val sourceDirs = rootDir.dir.listFiles()
  113. .orEmpty()
  114. .associate { it.name to SourceDirectory(it) }
  115. .mapNotNullKeys { entry ->
  116. onlineSources.find { provider.getSourceDirName(it) == entry.key }?.id
  117. }
  118. rootDir.files = sourceDirs
  119. sourceDirs.values.forEach { sourceDir ->
  120. val mangaDirs = sourceDir.dir.listFiles()
  121. .orEmpty()
  122. .associateNotNullKeys { it.name to MangaDirectory(it) }
  123. sourceDir.files = mangaDirs
  124. mangaDirs.values.forEach { mangaDir ->
  125. val chapterDirs = mangaDir.dir.listFiles()
  126. .orEmpty()
  127. .mapNotNull { it.name }
  128. .toHashSet()
  129. mangaDir.files = chapterDirs
  130. }
  131. }
  132. }
  133. /**
  134. * Adds a chapter that has just been download to this cache.
  135. *
  136. * @param chapterDirName the downloaded chapter's directory name.
  137. * @param mangaUniFile the directory of the manga.
  138. * @param manga the manga of the chapter.
  139. */
  140. @Synchronized
  141. fun addChapter(chapterDirName: String, mangaUniFile: UniFile, manga: Manga) {
  142. // Retrieve the cached source directory or cache a new one
  143. var sourceDir = rootDir.files[manga.source]
  144. if (sourceDir == null) {
  145. val source = sourceManager.get(manga.source) ?: return
  146. val sourceUniFile = provider.findSourceDir(source) ?: return
  147. sourceDir = SourceDirectory(sourceUniFile)
  148. rootDir.files += manga.source to sourceDir
  149. }
  150. // Retrieve the cached manga directory or cache a new one
  151. val mangaDirName = provider.getMangaDirName(manga)
  152. var mangaDir = sourceDir.files[mangaDirName]
  153. if (mangaDir == null) {
  154. mangaDir = MangaDirectory(mangaUniFile)
  155. sourceDir.files += mangaDirName to mangaDir
  156. }
  157. // Save the chapter directory
  158. mangaDir.files += chapterDirName
  159. }
  160. /**
  161. * Removes a chapter that has been deleted from this cache.
  162. *
  163. * @param chapter the chapter to remove.
  164. * @param manga the manga of the chapter.
  165. */
  166. @Synchronized
  167. fun removeChapter(chapter: Chapter, manga: Manga) {
  168. val sourceDir = rootDir.files[manga.source] ?: return
  169. val mangaDir = sourceDir.files[provider.getMangaDirName(manga)] ?: return
  170. val chapterDirName = provider.getChapterDirName(chapter)
  171. if (chapterDirName in mangaDir.files) {
  172. mangaDir.files -= chapterDirName
  173. }
  174. }
  175. /**
  176. * Removes a list of chapters that have been deleted from this cache.
  177. *
  178. * @param chapters the list of chapter to remove.
  179. * @param manga the manga of the chapter.
  180. */
  181. @Synchronized
  182. fun removeChapters(chapters: List<Chapter>, manga: Manga) {
  183. val sourceDir = rootDir.files[manga.source] ?: return
  184. val mangaDir = sourceDir.files[provider.getMangaDirName(manga)] ?: return
  185. chapters.forEach { chapter ->
  186. val chapterDirName = provider.getChapterDirName(chapter)
  187. if (chapterDirName in mangaDir.files) {
  188. mangaDir.files -= chapterDirName
  189. }
  190. }
  191. }
  192. /**
  193. * Removes a manga that has been deleted from this cache.
  194. *
  195. * @param manga the manga to remove.
  196. */
  197. @Synchronized
  198. fun removeManga(manga: Manga) {
  199. val sourceDir = rootDir.files[manga.source] ?: return
  200. val mangaDirName = provider.getMangaDirName(manga)
  201. if (mangaDirName in sourceDir.files) {
  202. sourceDir.files -= mangaDirName
  203. }
  204. }
  205. /**
  206. * Class to store the files under the root downloads directory.
  207. */
  208. private class RootDirectory(
  209. val dir: UniFile,
  210. var files: Map<Long, SourceDirectory> = hashMapOf()
  211. )
  212. /**
  213. * Class to store the files under a source directory.
  214. */
  215. private class SourceDirectory(
  216. val dir: UniFile,
  217. var files: Map<String, MangaDirectory> = hashMapOf()
  218. )
  219. /**
  220. * Class to store the files under a manga directory.
  221. */
  222. private class MangaDirectory(
  223. val dir: UniFile,
  224. var files: Set<String> = hashSetOf()
  225. )
  226. /**
  227. * Returns a new map containing only the key entries of [transform] that are not null.
  228. */
  229. private inline fun <K, V, R> Map<out K, V>.mapNotNullKeys(transform: (Map.Entry<K?, V>) -> R?): Map<R, V> {
  230. val destination = LinkedHashMap<R, V>()
  231. forEach { element -> transform(element)?.let { destination[it] = element.value } }
  232. return destination
  233. }
  234. /**
  235. * Returns a map from a list containing only the key entries of [transform] that are not null.
  236. */
  237. private inline fun <T, K, V> Array<T>.associateNotNullKeys(transform: (T) -> Pair<K?, V>): Map<K, V> {
  238. val destination = LinkedHashMap<K, V>()
  239. for (element in this) {
  240. val (key, value) = transform(element)
  241. if (key != null) {
  242. destination[key] = value
  243. }
  244. }
  245. return destination
  246. }
  247. }