diff --git a/app/src/main/java/eu/kanade/tachiyomi/source/LocalSource.kt b/app/src/main/java/eu/kanade/tachiyomi/source/LocalSource.kt
index 58a0753f3..bc580147a 100755
--- a/app/src/main/java/eu/kanade/tachiyomi/source/LocalSource.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/source/LocalSource.kt
@@ -2,7 +2,9 @@ package eu.kanade.tachiyomi.source
 
 import android.content.Context
 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.data.preference.PreferencesHelper
 import eu.kanade.tachiyomi.source.model.Filter
 import eu.kanade.tachiyomi.source.model.FilterList
@@ -18,16 +20,16 @@ import eu.kanade.tachiyomi.util.lang.compareToCaseInsensitiveNaturalOrder
 import eu.kanade.tachiyomi.util.storage.DiskUtil
 import eu.kanade.tachiyomi.util.storage.EpubFile
 import eu.kanade.tachiyomi.util.system.ImageUtil
-import eu.kanade.tachiyomi.util.system.logcat
 import kotlinx.coroutines.runBlocking
 import kotlinx.serialization.Serializable
 import kotlinx.serialization.json.Json
 import kotlinx.serialization.json.decodeFromStream
 import kotlinx.serialization.json.encodeToStream
-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
@@ -35,50 +37,10 @@ import java.io.InputStream
 import java.util.concurrent.TimeUnit
 import java.util.zip.ZipFile
 
-class LocalSource(private val context: Context) : 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 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) }
-        }
-    }
+class LocalSource(
+    private val context: Context,
+    private val coverCache: CoverCache = Injekt.get(),
+) : CatalogueSource, UnmeteredSource {
 
     private val json: Json by injectLazy()
 
@@ -86,86 +48,100 @@ class LocalSource(private val context: Context) : CatalogueSource, UnmeteredSour
     private val preferences: PreferencesHelper by injectLazy()
     // SY <--
 
-    override val id = ID
-    override val name = context.getString(R.string.local_source)
-    override val lang = "other"
-    override val supportsLatest = true
+    override val name: String = context.getString(R.string.local_source)
+
+    override val id: Long = ID
+
+    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)
         // SY -->
         val allowLocalSourceHiddenFolders = preferences.allowLocalSourceHiddenFolders().get()
         // SY <--
 
-        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('.') /* SY --> */ && !allowLocalSourceHiddenFolders /* SY <-- */ }
-            .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 && /* SY --> */ (!it.name.startsWith('.') || allowLocalSourceHiddenFolders) /* SY <-- */ }
             .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 }))
-                }
-            }
-            1 -> {
-                mangaDirs = if (state.ascending) {
-                    mangaDirs.sortedBy(File::lastModified)
-                } else {
-                    mangaDirs.sortedByDescending(File::lastModified)
-                }
+        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
             }
         }
 
+        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)
+                    }
                 }
             }
         }
@@ -200,38 +176,44 @@ class LocalSource(private val context: Context) : CatalogueSource, UnmeteredSour
     )
     // SY <--
 
-    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 mangaJson = json.decodeFromStream<MangaJson>(localDetails.inputStream())
 
-            manga.copy(
-                title = mangaJson.title ?: manga.title,
-                author = mangaJson.author ?: manga.author,
-                artist = mangaJson.artist ?: manga.artist,
-                description = mangaJson.description ?: manga.description,
-                genres = mangaJson.genre ?: manga.genres,
-                status = mangaJson.status ?: manga.status,
+            mangaInfo = mangaInfo.copy(
+                title = mangaJson.title ?: mangaInfo.title,
+                author = mangaJson.author ?: mangaInfo.author,
+                artist = mangaJson.artist ?: mangaInfo.artist,
+                description = mangaJson.description ?: mangaInfo.description,
+                genres = mangaJson.genre ?: mangaInfo.genres,
+                status = mangaJson.status ?: 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 {
@@ -243,14 +225,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() }
@@ -259,12 +241,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
     }
@@ -328,25 +322,89 @@ 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")
diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderPresenter.kt
index db7377629..6b43ac805 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderPresenter.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderPresenter.kt
@@ -7,7 +7,6 @@ import android.net.Uri
 import android.os.Bundle
 import androidx.annotation.ColorInt
 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.Chapter
@@ -885,20 +884,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
+                        }
                     }
                 }
             }