소스 검색

Local Source - qol, cleanup and cover related fixes (#7166)

* Local Source - qol, cleanup and cover related fixes

* Review Changes

(cherry picked from commit ad17eb138609d684fd5929c3cb7dc644e3a3ec95)
FourTOne5 2 년 전
부모
커밋
940409a4c3
2개의 변경된 파일205개의 추가작업 그리고 142개의 파일을 삭제
  1. 193 131
      app/src/main/java/eu/kanade/tachiyomi/source/LocalSource.kt
  2. 12 11
      app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderPresenter.kt

+ 193 - 131
app/src/main/java/eu/kanade/tachiyomi/source/LocalSource.kt

@@ -1,8 +1,11 @@
 package eu.kanade.tachiyomi.source
 
 import android.content.Context
+import androidx.core.net.toUri
 import com.github.junrar.Archive
+import com.hippo.unifile.UniFile
 import eu.kanade.tachiyomi.R
+import eu.kanade.tachiyomi.data.cache.CoverCache
 import eu.kanade.tachiyomi.source.model.Filter
 import eu.kanade.tachiyomi.source.model.FilterList
 import eu.kanade.tachiyomi.source.model.MangasPage
@@ -30,6 +33,8 @@ import logcat.LogPriority
 import rx.Observable
 import tachiyomi.source.model.ChapterInfo
 import tachiyomi.source.model.MangaInfo
+import uy.kohesive.injekt.Injekt
+import uy.kohesive.injekt.api.get
 import uy.kohesive.injekt.injectLazy
 import java.io.File
 import java.io.FileInputStream
@@ -37,130 +42,104 @@ import java.io.InputStream
 import java.util.concurrent.TimeUnit
 import java.util.zip.ZipFile
 
-class LocalSource(private val context: Context) : CatalogueSource, UnmeteredSource {
+class LocalSource(
+    private val context: Context,
+    private val coverCache: CoverCache = Injekt.get(),
+) : CatalogueSource, UnmeteredSource {
 
-    companion object {
-        const val ID = 0L
-        const val HELP_URL = "https://tachiyomi.org/help/guides/local-manga/"
-
-        private const val COVER_NAME = "cover.jpg"
-        private val LATEST_THRESHOLD = TimeUnit.MILLISECONDS.convert(7, TimeUnit.DAYS)
-
-        fun updateCover(context: Context, manga: SManga, input: InputStream): File? {
-            val dir = getBaseDirectories(context).firstOrNull()
-            if (dir == null) {
-                input.close()
-                return null
-            }
-            var cover = getCoverFile(File("${dir.absolutePath}/${manga.url}"))
-            if (cover == null) {
-                cover = File("${dir.absolutePath}/${manga.url}", COVER_NAME)
-            }
-            // It might not exist if using the external SD card
-            cover.parentFile?.mkdirs()
-            input.use {
-                cover.outputStream().use {
-                    input.copyTo(it)
-                }
-            }
-            manga.thumbnail_url = cover.absolutePath
-            return cover
-        }
-
-        /**
-         * Returns valid cover file inside [parent] directory.
-         */
-        private fun getCoverFile(parent: File): File? {
-            return parent.listFiles()?.find { it.nameWithoutExtension == "cover" }?.takeIf {
-                it.isFile && ImageUtil.isImage(it.name) { it.inputStream() }
-            }
-        }
+    private val json: Json by injectLazy()
 
-        private fun getBaseDirectories(context: Context): List<File> {
-            val c = context.getString(R.string.app_name) + File.separator + "local"
-            return DiskUtil.getExternalStorages(context).map { File(it.absolutePath, c) }
-        }
-    }
+    override val name: String = context.getString(R.string.local_source)
 
-    private val json: Json by injectLazy()
+    override val id: Long = ID
 
-    override val id = ID
-    override val name = context.getString(R.string.local_source)
-    override val lang = "other"
-    override val supportsLatest = true
+    override val lang: String = "other"
 
     override fun toString() = name
 
+    override val supportsLatest: Boolean = true
+
+    // Browse related
     override fun fetchPopularManga(page: Int) = fetchSearchManga(page, "", POPULAR_FILTERS)
 
+    override fun fetchLatestUpdates(page: Int) = fetchSearchManga(page, "", LATEST_FILTERS)
+
     override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
-        val baseDirs = getBaseDirectories(context)
+        val baseDirsFiles = getBaseDirectoriesFiles(context)
 
-        val time = if (filters === LATEST_FILTERS) System.currentTimeMillis() - LATEST_THRESHOLD else 0L
-        var mangaDirs = baseDirs
-            .asSequence()
-            .mapNotNull { it.listFiles()?.toList() }
-            .flatten()
-            .filter { it.isDirectory }
-            .filterNot { it.name.startsWith('.') }
-            .filter { if (time == 0L) it.name.contains(query, ignoreCase = true) else it.lastModified() >= time }
+        var mangaDirs = baseDirsFiles
+            // Filter out files that are hidden and is not a folder
+            .filter { it.isDirectory && !it.name.startsWith('.') }
             .distinctBy { it.name }
 
-        val state = ((if (filters.isEmpty()) POPULAR_FILTERS else filters)[0] as OrderBy).state
-        when (state?.index) {
-            0 -> {
-                mangaDirs = if (state.ascending) {
-                    mangaDirs.sortedWith(compareBy(String.CASE_INSENSITIVE_ORDER, { it.name }))
-                } else {
-                    mangaDirs.sortedWith(compareByDescending(String.CASE_INSENSITIVE_ORDER, { it.name }))
-                }
+        val lastModifiedLimit = if (filters === LATEST_FILTERS) System.currentTimeMillis() - LATEST_THRESHOLD else 0L
+        // Filter by query or last modified
+        mangaDirs = mangaDirs.filter {
+            if (lastModifiedLimit == 0L) {
+                it.name.contains(query, ignoreCase = true)
+            } else {
+                it.lastModified() >= lastModifiedLimit
             }
-            1 -> {
-                mangaDirs = if (state.ascending) {
-                    mangaDirs.sortedBy(File::lastModified)
-                } else {
-                    mangaDirs.sortedByDescending(File::lastModified)
+        }
+
+        filters.forEach { filter ->
+            when (filter) {
+                is OrderBy -> {
+                    when (filter.state!!.index) {
+                        0 -> {
+                            mangaDirs = if (filter.state!!.ascending) {
+                                mangaDirs.sortedWith(compareBy(String.CASE_INSENSITIVE_ORDER) { it.name })
+                            } else {
+                                mangaDirs.sortedWith(compareByDescending(String.CASE_INSENSITIVE_ORDER) { it.name })
+                            }
+                        }
+                        1 -> {
+                            mangaDirs = if (filter.state!!.ascending) {
+                                mangaDirs.sortedBy(File::lastModified)
+                            } else {
+                                mangaDirs.sortedByDescending(File::lastModified)
+                            }
+                        }
+                    }
                 }
+
+                else -> { /* Do nothing */ }
             }
         }
 
+        // Transform mangaDirs to list of SManga
         val mangas = mangaDirs.map { mangaDir ->
             SManga.create().apply {
                 title = mangaDir.name
                 url = mangaDir.name
 
                 // Try to find the cover
-                for (dir in baseDirs) {
-                    val cover = getCoverFile(File("${dir.absolutePath}/$url"))
-                    if (cover != null && cover.exists()) {
-                        thumbnail_url = cover.absolutePath
-                        break
-                    }
+                val cover = getCoverFile(mangaDir.name, baseDirsFiles)
+                if (cover != null && cover.exists()) {
+                    thumbnail_url = cover.absolutePath
                 }
+            }
+        }
 
-                val sManga = this
-                val mangaInfo = this.toMangaInfo()
-                runBlocking {
-                    val chapters = getChapterList(mangaInfo)
-                    if (chapters.isNotEmpty()) {
-                        val chapter = chapters.last().toSChapter()
-                        val format = getFormat(chapter)
-                        if (format is Format.Epub) {
-                            EpubFile(format.file).use { epub ->
-                                epub.fillMangaMetadata(sManga)
-                            }
-                        }
+        // Fetch chapters of all the manga
+        mangas.forEach { manga ->
+            val mangaInfo = manga.toMangaInfo()
+            runBlocking {
+                val chapters = getChapterList(mangaInfo)
+                if (chapters.isNotEmpty()) {
+                    val chapter = chapters.last().toSChapter()
+                    val format = getFormat(chapter)
 
-                        // Copy the cover from the first chapter found.
-                        if (thumbnail_url == null) {
-                            try {
-                                val dest = updateCover(chapter, sManga)
-                                thumbnail_url = dest?.absolutePath
-                            } catch (e: Exception) {
-                                logcat(LogPriority.ERROR, e)
-                            }
+                    if (format is Format.Epub) {
+                        EpubFile(format.file).use { epub ->
+                            epub.fillMangaMetadata(manga)
                         }
                     }
+
+                    // Copy the cover from the first chapter found if not available
+                    if (manga.thumbnail_url == null) {
+                        updateCover(chapter, manga)
+                    }
                 }
             }
         }
@@ -168,38 +147,44 @@ class LocalSource(private val context: Context) : CatalogueSource, UnmeteredSour
         return Observable.just(MangasPage(mangas.toList(), false))
     }
 
-    override fun fetchLatestUpdates(page: Int) = fetchSearchManga(page, "", LATEST_FILTERS)
-
+    // Manga details related
     override suspend fun getMangaDetails(manga: MangaInfo): MangaInfo {
-        val localDetails = getBaseDirectories(context)
-            .asSequence()
-            .mapNotNull { File(it, manga.key).listFiles()?.toList() }
-            .flatten()
+        var mangaInfo = manga
+
+        val baseDirsFile = getBaseDirectoriesFiles(context)
+
+        val coverFile = getCoverFile(manga.key, baseDirsFile)
+
+        coverFile?.let {
+            mangaInfo = mangaInfo.copy(cover = it.absolutePath)
+        }
+
+        val localDetails = getMangaDirsFiles(manga.key, baseDirsFile)
             .firstOrNull { it.extension.equals("json", ignoreCase = true) }
 
-        return if (localDetails != null) {
+        if (localDetails != null) {
             val obj = json.decodeFromStream<JsonObject>(localDetails.inputStream())
 
-            manga.copy(
-                title = obj["title"]?.jsonPrimitive?.contentOrNull ?: manga.title,
-                author = obj["author"]?.jsonPrimitive?.contentOrNull ?: manga.author,
-                artist = obj["artist"]?.jsonPrimitive?.contentOrNull ?: manga.artist,
-                description = obj["description"]?.jsonPrimitive?.contentOrNull ?: manga.description,
-                genres = obj["genre"]?.jsonArray?.map { it.jsonPrimitive.content } ?: manga.genres,
-                status = obj["status"]?.jsonPrimitive?.intOrNull ?: manga.status,
+            mangaInfo = mangaInfo.copy(
+                title = obj["title"]?.jsonPrimitive?.contentOrNull ?: mangaInfo.title,
+                author = obj["author"]?.jsonPrimitive?.contentOrNull ?: mangaInfo.author,
+                artist = obj["artist"]?.jsonPrimitive?.contentOrNull ?: mangaInfo.artist,
+                description = obj["description"]?.jsonPrimitive?.contentOrNull ?: mangaInfo.description,
+                genres = obj["genre"]?.jsonArray?.map { it.jsonPrimitive.content } ?: mangaInfo.genres,
+                status = obj["status"]?.jsonPrimitive?.intOrNull ?: mangaInfo.status,
             )
-        } else {
-            manga
         }
+
+        return mangaInfo
     }
 
+    // Chapters
     override suspend fun getChapterList(manga: MangaInfo): List<ChapterInfo> {
         val sManga = manga.toSManga()
 
-        val chapters = getBaseDirectories(context)
-            .asSequence()
-            .mapNotNull { File(it, manga.key).listFiles()?.toList() }
-            .flatten()
+        val baseDirsFile = getBaseDirectoriesFiles(context)
+        return getMangaDirsFiles(manga.key, baseDirsFile)
+            // Only keep supported formats
             .filter { it.isDirectory || isSupportedFile(it.extension) }
             .map { chapterFile ->
                 SChapter.create().apply {
@@ -211,14 +196,14 @@ class LocalSource(private val context: Context) : CatalogueSource, UnmeteredSour
                     }
                     date_upload = chapterFile.lastModified()
 
+                    ChapterRecognition.parseChapterNumber(this, sManga)
+
                     val format = getFormat(chapterFile)
                     if (format is Format.Epub) {
                         EpubFile(format.file).use { epub ->
                             epub.fillChapterMetadata(this)
                         }
                     }
-
-                    ChapterRecognition.parseChapterNumber(this, sManga)
                 }
             }
             .map { it.toChapterInfo() }
@@ -227,12 +212,24 @@ class LocalSource(private val context: Context) : CatalogueSource, UnmeteredSour
                 if (c == 0) c2.name.compareToCaseInsensitiveNaturalOrder(c1.name) else c
             }
             .toList()
-
-        return chapters
     }
 
-    override suspend fun getPageList(chapter: ChapterInfo) = throw Exception("Unused")
+    // Filters
+    override fun getFilterList() = FilterList(OrderBy(context))
+
+    private val POPULAR_FILTERS = FilterList(OrderBy(context))
+    private val LATEST_FILTERS = FilterList(OrderBy(context).apply { state = Filter.Sort.Selection(1, false) })
+
+    private class OrderBy(context: Context) : Filter.Sort(
+        context.getString(R.string.local_filter_order_by),
+        arrayOf(context.getString(R.string.title), context.getString(R.string.date)),
+        Selection(0, true),
+    )
+
+    // Unused stuff
+    override suspend fun getPageList(chapter: ChapterInfo) = throw UnsupportedOperationException("Unused")
 
+    // Miscellaneous
     private fun isSupportedFile(extension: String): Boolean {
         return extension.lowercase() in SUPPORTED_ARCHIVE_TYPES
     }
@@ -296,25 +293,90 @@ class LocalSource(private val context: Context) : CatalogueSource, UnmeteredSour
                 }
             }
         }
+            .also { coverCache.clearMemoryCache() }
     }
 
-    override fun getFilterList() = POPULAR_FILTERS
-
-    private val POPULAR_FILTERS = FilterList(OrderBy(context))
-    private val LATEST_FILTERS = FilterList(OrderBy(context).apply { state = Filter.Sort.Selection(1, false) })
-
-    private class OrderBy(context: Context) : Filter.Sort(
-        context.getString(R.string.local_filter_order_by),
-        arrayOf(context.getString(R.string.title), context.getString(R.string.date)),
-        Selection(0, true),
-    )
-
     sealed class Format {
         data class Directory(val file: File) : Format()
         data class Zip(val file: File) : Format()
         data class Rar(val file: File) : Format()
         data class Epub(val file: File) : Format()
     }
+
+    companion object {
+        const val ID = 0L
+        const val HELP_URL = "https://tachiyomi.org/help/guides/local-manga/"
+
+        private const val DEFAULT_COVER_NAME = "cover.jpg"
+        private val LATEST_THRESHOLD = TimeUnit.MILLISECONDS.convert(7, TimeUnit.DAYS)
+
+        private fun getBaseDirectories(context: Context): Sequence<File> {
+            val localFolder = context.getString(R.string.app_name) + File.separator + "local"
+            return DiskUtil.getExternalStorages(context)
+                .map { File(it.absolutePath, localFolder) }
+                .asSequence()
+        }
+
+        private fun getBaseDirectoriesFiles(context: Context): Sequence<File> {
+            return getBaseDirectories(context)
+                // Get all the files inside all baseDir
+                .flatMap { it.listFiles().orEmpty().toList() }
+        }
+
+        private fun getMangaDir(mangaUrl: String, baseDirsFile: Sequence<File>): File? {
+            return baseDirsFile
+                // Get the first mangaDir or null
+                .firstOrNull { it.isDirectory && it.name == mangaUrl }
+        }
+
+        private fun getMangaDirsFiles(mangaUrl: String, baseDirsFile: Sequence<File>): Sequence<File> {
+            return baseDirsFile
+                // Filter out ones that are not related to the manga and is not a directory
+                .filter { it.isDirectory && it.name == mangaUrl }
+                // Get all the files inside the filtered folders
+                .flatMap { it.listFiles().orEmpty().toList() }
+        }
+
+        private fun getCoverFile(mangaUrl: String, baseDirsFile: Sequence<File>): File? {
+            return getMangaDirsFiles(mangaUrl, baseDirsFile)
+                // Get all file whose names start with 'cover'
+                .filter { it.isFile && it.nameWithoutExtension.equals("cover", ignoreCase = true) }
+                // Get the first actual image
+                .firstOrNull {
+                    ImageUtil.isImage(it.name) { it.inputStream() }
+                }
+        }
+
+        fun updateCover(context: Context, manga: SManga, inputStream: InputStream): File? {
+            val baseDirsFiles = getBaseDirectoriesFiles(context)
+
+            val mangaDir = getMangaDir(manga.url, baseDirsFiles)
+            if (mangaDir == null) {
+                inputStream.close()
+                return null
+            }
+
+            var coverFile = getCoverFile(manga.url, baseDirsFiles)
+            if (coverFile == null) {
+                coverFile = File(mangaDir.absolutePath, DEFAULT_COVER_NAME)
+
+            }
+
+            // It might not exist at this point
+            coverFile.parentFile?.mkdirs()
+            inputStream.use { input ->
+                coverFile.outputStream().use { output ->
+                    input.copyTo(output)
+                }
+            }
+
+            // Create a .nomedia file
+            DiskUtil.createNoMediaFile(UniFile.fromFile(mangaDir), context)
+
+            manga.thumbnail_url = coverFile.absolutePath
+            return coverFile
+        }
+    }
 }
 
 private val SUPPORTED_ARCHIVE_TYPES = listOf("zip", "cbz", "rar", "cbr", "epub")

+ 12 - 11
app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderPresenter.kt

@@ -4,7 +4,6 @@ import android.app.Application
 import android.net.Uri
 import android.os.Bundle
 import com.jakewharton.rxrelay.BehaviorRelay
-import eu.kanade.tachiyomi.R
 import eu.kanade.tachiyomi.data.cache.CoverCache
 import eu.kanade.tachiyomi.data.database.DatabaseHelper
 import eu.kanade.tachiyomi.data.database.models.History
@@ -675,20 +674,22 @@ class ReaderPresenter(
 
         Observable
             .fromCallable {
-                if (manga.isLocal()) {
-                    val context = Injekt.get<Application>()
-                    LocalSource.updateCover(context, manga, stream())
-                    manga.updateCoverLastModified(db)
-                    R.string.cover_updated
-                    SetAsCoverResult.Success
-                } else {
-                    if (manga.favorite) {
-                        coverCache.setCustomCoverToCache(manga, stream())
+                stream().use {
+                    if (manga.isLocal()) {
+                        val context = Injekt.get<Application>()
+                        LocalSource.updateCover(context, manga, it)
                         manga.updateCoverLastModified(db)
                         coverCache.clearMemoryCache()
                         SetAsCoverResult.Success
                     } else {
-                        SetAsCoverResult.AddToLibraryFirst
+                        if (manga.favorite) {
+                            coverCache.setCustomCoverToCache(manga, it)
+                            manga.updateCoverLastModified(db)
+                            coverCache.clearMemoryCache()
+                            SetAsCoverResult.Success
+                        } else {
+                            SetAsCoverResult.AddToLibraryFirst
+                        }
                     }
                 }
             }