DownloadManager.kt 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380
  1. package eu.kanade.tachiyomi.data.download
  2. import android.content.Context
  3. import eu.kanade.domain.category.interactor.GetCategories
  4. import eu.kanade.domain.chapter.model.Chapter
  5. import eu.kanade.domain.download.service.DownloadPreferences
  6. import eu.kanade.domain.manga.model.Manga
  7. import eu.kanade.tachiyomi.R
  8. import eu.kanade.tachiyomi.data.database.models.toDomainChapter
  9. import eu.kanade.tachiyomi.data.download.model.Download
  10. import eu.kanade.tachiyomi.data.download.model.DownloadQueue
  11. import eu.kanade.tachiyomi.source.Source
  12. import eu.kanade.tachiyomi.source.SourceManager
  13. import eu.kanade.tachiyomi.source.model.Page
  14. import eu.kanade.tachiyomi.util.lang.launchIO
  15. import eu.kanade.tachiyomi.util.system.logcat
  16. import kotlinx.coroutines.runBlocking
  17. import logcat.LogPriority
  18. import rx.Observable
  19. import uy.kohesive.injekt.Injekt
  20. import uy.kohesive.injekt.api.get
  21. /**
  22. * This class is used to manage chapter downloads in the application. It must be instantiated once
  23. * and retrieved through dependency injection. You can use this class to queue new chapters or query
  24. * downloaded chapters.
  25. */
  26. class DownloadManager(
  27. private val context: Context,
  28. private val provider: DownloadProvider = Injekt.get(),
  29. private val cache: DownloadCache = Injekt.get(),
  30. private val getCategories: GetCategories = Injekt.get(),
  31. private val sourceManager: SourceManager = Injekt.get(),
  32. private val downloadPreferences: DownloadPreferences = Injekt.get(),
  33. ) {
  34. /**
  35. * Downloader whose only task is to download chapters.
  36. */
  37. private val downloader = Downloader(context, provider, cache)
  38. /**
  39. * Queue to delay the deletion of a list of chapters until triggered.
  40. */
  41. private val pendingDeleter = DownloadPendingDeleter(context)
  42. /**
  43. * Downloads queue, where the pending chapters are stored.
  44. */
  45. val queue: DownloadQueue
  46. get() = downloader.queue
  47. /**
  48. * Tells the downloader to begin downloads.
  49. *
  50. * @return true if it's started, false otherwise (empty queue).
  51. */
  52. fun startDownloads(): Boolean {
  53. return downloader.start()
  54. }
  55. /**
  56. * Tells the downloader to stop downloads.
  57. *
  58. * @param reason an optional reason for being stopped, used to notify the user.
  59. */
  60. fun stopDownloads(reason: String? = null) {
  61. downloader.stop(reason)
  62. }
  63. /**
  64. * Tells the downloader to pause downloads.
  65. */
  66. fun pauseDownloads() {
  67. downloader.pause()
  68. }
  69. /**
  70. * Empties the download queue.
  71. *
  72. * @param isNotification value that determines if status is set (needed for view updates)
  73. */
  74. fun clearQueue(isNotification: Boolean = false) {
  75. downloader.clearQueue(isNotification)
  76. }
  77. fun startDownloadNow(chapterId: Long?) {
  78. if (chapterId == null) return
  79. val download = downloader.queue.find { it.chapter.id == chapterId }
  80. // If not in queue try to start a new download
  81. val toAdd = download ?: runBlocking { Download.fromChapterId(chapterId) } ?: return
  82. val queue = downloader.queue.toMutableList()
  83. download?.let { queue.remove(it) }
  84. queue.add(0, toAdd)
  85. reorderQueue(queue)
  86. if (downloader.isPaused()) {
  87. if (DownloadService.isRunning(context)) {
  88. downloader.start()
  89. } else {
  90. DownloadService.start(context)
  91. }
  92. }
  93. }
  94. /**
  95. * Reorders the download queue.
  96. *
  97. * @param downloads value to set the download queue to
  98. */
  99. fun reorderQueue(downloads: List<Download>) {
  100. val wasRunning = downloader.isRunning
  101. if (downloads.isEmpty()) {
  102. DownloadService.stop(context)
  103. downloader.queue.clear()
  104. return
  105. }
  106. downloader.pause()
  107. downloader.queue.clear()
  108. downloader.queue.addAll(downloads)
  109. if (wasRunning) {
  110. downloader.start()
  111. }
  112. }
  113. /**
  114. * Tells the downloader to enqueue the given list of chapters.
  115. *
  116. * @param manga the manga of the chapters.
  117. * @param chapters the list of chapters to enqueue.
  118. * @param autoStart whether to start the downloader after enqueing the chapters.
  119. */
  120. fun downloadChapters(manga: Manga, chapters: List<Chapter>, autoStart: Boolean = true) {
  121. downloader.queueChapters(manga, chapters, autoStart)
  122. }
  123. /**
  124. * Tells the downloader to enqueue the given list of downloads at the start of the queue.
  125. *
  126. * @param downloads the list of downloads to enqueue.
  127. */
  128. fun addDownloadsToStartOfQueue(downloads: List<Download>) {
  129. if (downloads.isEmpty()) return
  130. queue.toMutableList().apply {
  131. addAll(0, downloads)
  132. reorderQueue(this)
  133. }
  134. if (!DownloadService.isRunning(context)) DownloadService.start(context)
  135. }
  136. /**
  137. * Builds the page list of a downloaded chapter.
  138. *
  139. * @param source the source of the chapter.
  140. * @param manga the manga of the chapter.
  141. * @param chapter the downloaded chapter.
  142. * @return an observable containing the list of pages from the chapter.
  143. */
  144. fun buildPageList(source: Source, manga: Manga, chapter: Chapter): Observable<List<Page>> {
  145. val chapterDir = provider.findChapterDir(chapter.name, chapter.scanlator, manga.title, source)
  146. return Observable.fromCallable {
  147. val files = chapterDir?.listFiles().orEmpty()
  148. .filter { "image" in it.type.orEmpty() }
  149. if (files.isEmpty()) {
  150. throw Exception(context.getString(R.string.page_list_empty_error))
  151. }
  152. files.sortedBy { it.name }
  153. .mapIndexed { i, file ->
  154. Page(i, uri = file.uri).apply { status = Page.READY }
  155. }
  156. }
  157. }
  158. /**
  159. * Returns true if the chapter is downloaded.
  160. *
  161. * @param chapterName the name of the chapter to query.
  162. * @param chapterScanlator scanlator of the chapter to query
  163. * @param mangaTitle the title of the manga to query.
  164. * @param sourceId the id of the source of the chapter.
  165. * @param skipCache whether to skip the directory cache and check in the filesystem.
  166. */
  167. fun isChapterDownloaded(
  168. chapterName: String,
  169. chapterScanlator: String?,
  170. mangaTitle: String,
  171. sourceId: Long,
  172. skipCache: Boolean = false,
  173. ): Boolean {
  174. return cache.isChapterDownloaded(chapterName, chapterScanlator, mangaTitle, sourceId, skipCache)
  175. }
  176. /**
  177. * Returns the download from queue if the chapter is queued for download
  178. * else it will return null which means that the chapter is not queued for download
  179. *
  180. * @param chapter the chapter to check.
  181. */
  182. fun getChapterDownloadOrNull(chapter: Chapter): Download? {
  183. return downloader.queue
  184. .firstOrNull { it.chapter.id == chapter.id && it.chapter.manga_id == chapter.mangaId }
  185. }
  186. /**
  187. * Returns the amount of downloaded chapters for a manga.
  188. *
  189. * @param manga the manga to check.
  190. */
  191. fun getDownloadCount(manga: Manga): Int {
  192. return cache.getDownloadCount(manga)
  193. }
  194. fun deletePendingDownloads(downloads: List<Download>) {
  195. val domainChapters = downloads.map { it.chapter.toDomainChapter()!! }
  196. removeFromDownloadQueue(domainChapters)
  197. }
  198. /**
  199. * Deletes the directories of a list of downloaded chapters.
  200. *
  201. * @param chapters the list of chapters to delete.
  202. * @param manga the manga of the chapters.
  203. * @param source the source of the chapters.
  204. */
  205. fun deleteChapters(chapters: List<Chapter>, manga: Manga, source: Source) {
  206. val filteredChapters = getChaptersToDelete(chapters, manga)
  207. if (filteredChapters.isNotEmpty()) {
  208. launchIO {
  209. removeFromDownloadQueue(filteredChapters)
  210. val (mangaDir, chapterDirs) = provider.findChapterDirs(filteredChapters, manga, source)
  211. chapterDirs.forEach { it.delete() }
  212. cache.removeChapters(filteredChapters, manga)
  213. // Delete manga directory if empty
  214. if (mangaDir?.listFiles()?.isEmpty() == true) {
  215. mangaDir.delete()
  216. cache.removeManga(manga)
  217. // Delete source directory if empty
  218. val sourceDir = provider.findSourceDir(source)
  219. if (sourceDir?.listFiles()?.isEmpty() == true) {
  220. sourceDir.delete()
  221. cache.removeSource(source)
  222. }
  223. }
  224. }
  225. }
  226. }
  227. private fun removeFromDownloadQueue(chapters: List<Chapter>) {
  228. val wasRunning = downloader.isRunning
  229. if (wasRunning) {
  230. downloader.pause()
  231. }
  232. downloader.queue.remove(chapters)
  233. if (wasRunning) {
  234. if (downloader.queue.isEmpty()) {
  235. DownloadService.stop(context)
  236. downloader.stop()
  237. } else if (downloader.queue.isNotEmpty()) {
  238. downloader.start()
  239. }
  240. }
  241. }
  242. /**
  243. * Deletes the directory of a downloaded manga.
  244. *
  245. * @param manga the manga to delete.
  246. * @param source the source of the manga.
  247. */
  248. fun deleteManga(manga: Manga, source: Source) {
  249. launchIO {
  250. downloader.queue.remove(manga)
  251. provider.findMangaDir(manga.title, source)?.delete()
  252. cache.removeManga(manga)
  253. }
  254. }
  255. /**
  256. * Adds a list of chapters to be deleted later.
  257. *
  258. * @param chapters the list of chapters to delete.
  259. * @param manga the manga of the chapters.
  260. */
  261. fun enqueueDeleteChapters(chapters: List<Chapter>, manga: Manga) {
  262. pendingDeleter.addChapters(getChaptersToDelete(chapters, manga), manga)
  263. }
  264. /**
  265. * Triggers the execution of the deletion of pending chapters.
  266. */
  267. fun deletePendingChapters() {
  268. val pendingChapters = pendingDeleter.getPendingChapters()
  269. for ((manga, chapters) in pendingChapters) {
  270. val source = sourceManager.get(manga.source) ?: continue
  271. deleteChapters(chapters, manga, source)
  272. }
  273. }
  274. /**
  275. * Renames source download folder
  276. *
  277. * @param oldSource the old source.
  278. * @param newSource the new source.
  279. */
  280. fun renameSource(oldSource: Source, newSource: Source) {
  281. val oldFolder = provider.findSourceDir(oldSource) ?: return
  282. val newName = provider.getSourceDirName(newSource)
  283. val capitalizationChanged = oldFolder.name.equals(newName, ignoreCase = true)
  284. if (capitalizationChanged) {
  285. val tempName = newName + "_tmp"
  286. if (oldFolder.renameTo(tempName).not()) {
  287. logcat(LogPriority.ERROR) { "Failed to rename source download folder: ${oldFolder.name}." }
  288. return
  289. }
  290. }
  291. if (oldFolder.renameTo(newName).not()) {
  292. logcat(LogPriority.ERROR) { "Failed to rename source download folder: ${oldFolder.name}." }
  293. }
  294. }
  295. /**
  296. * Renames an already downloaded chapter
  297. *
  298. * @param source the source of the manga.
  299. * @param manga the manga of the chapter.
  300. * @param oldChapter the existing chapter with the old name.
  301. * @param newChapter the target chapter with the new name.
  302. */
  303. fun renameChapter(source: Source, manga: Manga, oldChapter: Chapter, newChapter: Chapter) {
  304. val oldNames = provider.getValidChapterDirNames(oldChapter.name, oldChapter.scanlator)
  305. val mangaDir = provider.getMangaDir(manga.title, source)
  306. // Assume there's only 1 version of the chapter name formats present
  307. val oldDownload = oldNames.asSequence()
  308. .mapNotNull { mangaDir.findFile(it) }
  309. .firstOrNull() ?: return
  310. var newName = provider.getChapterDirName(newChapter.name, newChapter.scanlator)
  311. if (oldDownload.isFile && oldDownload.name?.endsWith(".cbz") == true) {
  312. newName += ".cbz"
  313. }
  314. if (oldDownload.renameTo(newName)) {
  315. cache.removeChapter(oldChapter, manga)
  316. cache.addChapter(newName, mangaDir, manga)
  317. } else {
  318. logcat(LogPriority.ERROR) { "Could not rename downloaded chapter: ${oldNames.joinToString()}." }
  319. }
  320. }
  321. private fun getChaptersToDelete(chapters: List<Chapter>, manga: Manga): List<Chapter> {
  322. // Retrieve the categories that are set to exclude from being deleted on read
  323. val categoriesToExclude = downloadPreferences.removeExcludeCategories().get().map(String::toLong)
  324. val categoriesForManga = runBlocking { getCategories.await(manga.id) }
  325. .map { it.id }
  326. .takeUnless { it.isEmpty() }
  327. ?: listOf(0)
  328. return if (categoriesForManga.intersect(categoriesToExclude).isNotEmpty()) {
  329. chapters.filterNot { it.read }
  330. } else if (!downloadPreferences.removeBookmarkedChapters().get()) {
  331. chapters.filterNot { it.bookmark }
  332. } else {
  333. chapters
  334. }
  335. }
  336. }