mirror of
https://github.com/null2264/yokai.git
synced 2025-06-21 10:44:42 +00:00
feat: The actual unified storage
This commit is contained in:
parent
6887d779ef
commit
64d6879893
25 changed files with 256 additions and 380 deletions
|
@ -1,7 +1,10 @@
|
|||
package eu.kanade.tachiyomi.source
|
||||
|
||||
import android.content.Context
|
||||
import androidx.core.net.toFile
|
||||
import com.github.junrar.Archive
|
||||
import com.hippo.unifile.UniFile
|
||||
import dev.yokai.domain.storage.StorageManager
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.source.model.Filter
|
||||
import eu.kanade.tachiyomi.source.model.FilterList
|
||||
|
@ -10,9 +13,11 @@ import eu.kanade.tachiyomi.source.model.SChapter
|
|||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import eu.kanade.tachiyomi.util.chapter.ChapterRecognition
|
||||
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.extension
|
||||
import eu.kanade.tachiyomi.util.system.nameWithoutExtension
|
||||
import eu.kanade.tachiyomi.util.system.writeText
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.encodeToString
|
||||
|
@ -20,7 +25,6 @@ import kotlinx.serialization.json.Json
|
|||
import kotlinx.serialization.json.decodeFromStream
|
||||
import timber.log.Timber
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.io.File
|
||||
import java.io.FileInputStream
|
||||
import java.io.InputStream
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
@ -35,16 +39,16 @@ class LocalSource(private val context: Context) : CatalogueSource, UnmeteredSour
|
|||
private val LATEST_THRESHOLD = TimeUnit.MILLISECONDS.convert(7, TimeUnit.DAYS)
|
||||
private val langMap = hashMapOf<String, String>()
|
||||
|
||||
fun getMangaLang(manga: SManga, context: Context): String {
|
||||
fun getMangaLang(manga: SManga): String {
|
||||
return langMap.getOrPut(manga.url) {
|
||||
val localDetails = getBaseDirectories(context)
|
||||
val localDetails = getBaseDirectories()
|
||||
.asSequence()
|
||||
.mapNotNull { File(it, manga.url).listFiles()?.toList() }
|
||||
.mapNotNull { it.findFile(manga.url)?.listFiles()?.toList() }
|
||||
.flatten()
|
||||
.firstOrNull { it.extension.equals("json", ignoreCase = true) }
|
||||
|
||||
return if (localDetails != null) {
|
||||
val obj = Json.decodeFromStream<MangaJson>(localDetails.inputStream())
|
||||
val obj = Json.decodeFromStream<MangaJson>(localDetails.openInputStream())
|
||||
obj.lang ?: "other"
|
||||
} else {
|
||||
"other"
|
||||
|
@ -52,49 +56,39 @@ class LocalSource(private val context: Context) : CatalogueSource, UnmeteredSour
|
|||
}
|
||||
}
|
||||
|
||||
fun updateCover(context: Context, manga: SManga, input: InputStream): File? {
|
||||
val dir = getBaseDirectories(context).firstOrNull()
|
||||
fun updateCover(manga: SManga, input: InputStream): UniFile? {
|
||||
val dir = getBaseDirectories().firstOrNull()
|
||||
if (dir == null) {
|
||||
input.close()
|
||||
return null
|
||||
}
|
||||
var cover = getCoverFile(File("${dir.absolutePath}/${manga.url}"))
|
||||
var cover = getCoverFile(dir.findFile(manga.url))
|
||||
if (cover == null) {
|
||||
cover = File("${dir.absolutePath}/${manga.url}", COVER_NAME)
|
||||
cover = dir.findFile(manga.url)?.findFile(COVER_NAME)!!
|
||||
}
|
||||
// It might not exist if using the external SD card
|
||||
cover.parentFile?.mkdirs()
|
||||
cover.parentFile?.parentFile?.createDirectory(cover.parentFile?.name)
|
||||
input.use {
|
||||
cover.outputStream().use {
|
||||
cover.openOutputStream().use {
|
||||
input.copyTo(it)
|
||||
}
|
||||
}
|
||||
manga.thumbnail_url = cover.absolutePath
|
||||
manga.thumbnail_url = cover.filePath
|
||||
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 getCoverFile(parent: UniFile?): UniFile? {
|
||||
return parent?.listFiles()?.find { it.nameWithoutExtension == "cover" }?.takeIf {
|
||||
it.isFile && ImageUtil.isImage(it.name.orEmpty()) { it.openInputStream() }
|
||||
}
|
||||
}
|
||||
|
||||
private fun getBaseDirectories(context: Context): List<File> {
|
||||
val library = context.getString(R.string.app_short_name) + File.separator + "local"
|
||||
val normalized = context.getString(R.string.app_normalized_name) + File.separator + "local"
|
||||
val j2k = "TachiyomiJ2K" + File.separator + "local"
|
||||
val tachi = "Tachiyomi" + File.separator + "local"
|
||||
return DiskUtil.getExternalStorages(context).map {
|
||||
listOf(
|
||||
File(it.absolutePath, library),
|
||||
File(it.absolutePath, normalized),
|
||||
File(it.absolutePath, j2k),
|
||||
File(it.absolutePath, tachi),
|
||||
)
|
||||
}.flatten()
|
||||
private fun getBaseDirectories(): List<UniFile> {
|
||||
val storageManager: StorageManager by injectLazy()
|
||||
return listOf(storageManager.getLocalSourceDirectory()!!)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -114,7 +108,7 @@ class LocalSource(private val context: Context) : CatalogueSource, UnmeteredSour
|
|||
query: String,
|
||||
filters: FilterList,
|
||||
): MangasPage {
|
||||
val baseDirs = getBaseDirectories(context)
|
||||
val baseDirs = getBaseDirectories()
|
||||
|
||||
val time =
|
||||
if (filters === latestFilters) System.currentTimeMillis() - LATEST_THRESHOLD else 0L
|
||||
|
@ -123,38 +117,38 @@ class LocalSource(private val context: Context) : CatalogueSource, UnmeteredSour
|
|||
.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 }
|
||||
.filterNot { it.name.orEmpty().startsWith('.') }
|
||||
.filter { if (time == 0L) it.name.orEmpty().contains(query, ignoreCase = true) else it.lastModified() >= time }
|
||||
.distinctBy { it.name }
|
||||
|
||||
val state = ((if (filters.isEmpty()) popularFilters else filters)[0] as OrderBy).state
|
||||
when (state?.index) {
|
||||
0 -> {
|
||||
mangaDirs = if (state.ascending) {
|
||||
mangaDirs.sortedWith(compareBy(String.CASE_INSENSITIVE_ORDER) { it.name })
|
||||
mangaDirs.sortedWith(compareBy(String.CASE_INSENSITIVE_ORDER) { it.name.orEmpty() })
|
||||
} else {
|
||||
mangaDirs.sortedWith(compareByDescending(String.CASE_INSENSITIVE_ORDER) { it.name })
|
||||
mangaDirs.sortedWith(compareByDescending(String.CASE_INSENSITIVE_ORDER) { it.name.orEmpty()})
|
||||
}
|
||||
}
|
||||
1 -> {
|
||||
mangaDirs = if (state.ascending) {
|
||||
mangaDirs.sortedBy(File::lastModified)
|
||||
mangaDirs.sortedBy(UniFile::lastModified)
|
||||
} else {
|
||||
mangaDirs.sortedByDescending(File::lastModified)
|
||||
mangaDirs.sortedByDescending(UniFile::lastModified)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val mangas = mangaDirs.map { mangaDir ->
|
||||
SManga.create().apply {
|
||||
title = mangaDir.name
|
||||
url = mangaDir.name
|
||||
title = mangaDir.name.orEmpty()
|
||||
url = mangaDir.name.orEmpty()
|
||||
|
||||
// Try to find the cover
|
||||
for (dir in baseDirs) {
|
||||
val cover = getCoverFile(File("${dir.absolutePath}/$url"))
|
||||
val cover = getCoverFile(mangaDir.findFile(url))
|
||||
if (cover != null && cover.exists()) {
|
||||
thumbnail_url = cover.absolutePath
|
||||
thumbnail_url = cover.filePath
|
||||
break
|
||||
}
|
||||
}
|
||||
|
@ -166,7 +160,7 @@ class LocalSource(private val context: Context) : CatalogueSource, UnmeteredSour
|
|||
val chapter = chapters.last()
|
||||
val format = getFormat(chapter)
|
||||
if (format is Format.Epub) {
|
||||
EpubFile(format.file).use { epub ->
|
||||
EpubFile(format.file.uri.toFile()).use { epub ->
|
||||
epub.fillMangaMetadata(manga)
|
||||
}
|
||||
}
|
||||
|
@ -175,7 +169,7 @@ class LocalSource(private val context: Context) : CatalogueSource, UnmeteredSour
|
|||
if (thumbnail_url == null) {
|
||||
try {
|
||||
val dest = updateCover(chapter, manga)
|
||||
thumbnail_url = dest?.absolutePath
|
||||
thumbnail_url = dest?.filePath
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e)
|
||||
}
|
||||
|
@ -191,14 +185,14 @@ class LocalSource(private val context: Context) : CatalogueSource, UnmeteredSour
|
|||
override suspend fun getLatestUpdates(page: Int) = getSearchManga(page, "", latestFilters)
|
||||
|
||||
override suspend fun getMangaDetails(manga: SManga): SManga {
|
||||
val localDetails = getBaseDirectories(context)
|
||||
val localDetails = getBaseDirectories()
|
||||
.asSequence()
|
||||
.mapNotNull { File(it, manga.url).listFiles()?.toList() }
|
||||
.mapNotNull { it.findFile(manga.url)?.listFiles()?.toList() }
|
||||
.flatten()
|
||||
.firstOrNull { it.extension.equals("json", ignoreCase = true) }
|
||||
|
||||
return if (localDetails != null) {
|
||||
val obj = json.decodeFromStream<MangaJson>(localDetails.inputStream())
|
||||
val obj = json.decodeFromStream<MangaJson>(localDetails.openInputStream())
|
||||
|
||||
obj.lang?.let { langMap[manga.url] = it }
|
||||
SManga.create().apply {
|
||||
|
@ -215,13 +209,13 @@ class LocalSource(private val context: Context) : CatalogueSource, UnmeteredSour
|
|||
}
|
||||
|
||||
fun updateMangaInfo(manga: SManga, lang: String?) {
|
||||
val directory = getBaseDirectories(context).map { File(it, manga.url) }.find {
|
||||
it.exists()
|
||||
val directory = getBaseDirectories().map { it.findFile(manga.url) }.find {
|
||||
it?.exists() == true
|
||||
} ?: return
|
||||
lang?.let { langMap[manga.url] = it }
|
||||
val json = Json { prettyPrint = true }
|
||||
val existingFileName = directory.listFiles()?.find { it.extension == "json" }?.name
|
||||
val file = File(directory, existingFileName ?: "info.json")
|
||||
val existingFileName = directory.listFiles()?.find { it.name.orEmpty().endsWith("json", true) }?.name
|
||||
val file = directory.findFile(existingFileName ?: "info.json")!!
|
||||
file.writeText(json.encodeToString(manga.toJson(lang)))
|
||||
}
|
||||
|
||||
|
@ -256,24 +250,24 @@ class LocalSource(private val context: Context) : CatalogueSource, UnmeteredSour
|
|||
}
|
||||
|
||||
override suspend fun getChapterList(manga: SManga): List<SChapter> {
|
||||
val chapters = getBaseDirectories(context)
|
||||
val chapters = getBaseDirectories()
|
||||
.asSequence()
|
||||
.mapNotNull { File(it, manga.url).listFiles()?.toList() }
|
||||
.mapNotNull { it.findFile(manga.url)?.listFiles()?.toList() }
|
||||
.flatten()
|
||||
.filter { it.isDirectory || isSupportedFile(it.extension) }
|
||||
.filter { it.isDirectory || isSupportedFile(it.extension.orEmpty()) }
|
||||
.map { chapterFile ->
|
||||
SChapter.create().apply {
|
||||
url = "${manga.url}/${chapterFile.name}"
|
||||
name = if (chapterFile.isDirectory) {
|
||||
chapterFile.name
|
||||
chapterFile.name.orEmpty()
|
||||
} else {
|
||||
chapterFile.nameWithoutExtension
|
||||
chapterFile.nameWithoutExtension.orEmpty()
|
||||
}
|
||||
date_upload = chapterFile.lastModified()
|
||||
|
||||
val format = getFormat(chapterFile)
|
||||
if (format is Format.Epub) {
|
||||
EpubFile(format.file).use { epub ->
|
||||
EpubFile(format.file.uri.toFile()).use { epub ->
|
||||
epub.fillChapterMetadata(this)
|
||||
}
|
||||
}
|
||||
|
@ -297,18 +291,18 @@ class LocalSource(private val context: Context) : CatalogueSource, UnmeteredSour
|
|||
}
|
||||
|
||||
fun getFormat(chapter: SChapter): Format {
|
||||
val baseDirs = getBaseDirectories(context)
|
||||
val baseDirs = getBaseDirectories()
|
||||
|
||||
for (dir in baseDirs) {
|
||||
val chapFile = File(dir, chapter.url)
|
||||
if (!chapFile.exists()) continue
|
||||
val chapFile = dir.findFile(chapter.url)
|
||||
if (chapFile == null || !chapFile.exists()) continue
|
||||
|
||||
return getFormat(chapFile)
|
||||
}
|
||||
throw Exception(context.getString(R.string.chapter_not_found))
|
||||
}
|
||||
|
||||
private fun getFormat(file: File) = with(file) {
|
||||
private fun getFormat(file: UniFile) = with(file) {
|
||||
when {
|
||||
isDirectory -> Format.Directory(this)
|
||||
extension.equals("zip", true) || extension.equals("cbz", true) -> Format.Zip(this)
|
||||
|
@ -318,41 +312,41 @@ class LocalSource(private val context: Context) : CatalogueSource, UnmeteredSour
|
|||
}
|
||||
}
|
||||
|
||||
private fun updateCover(chapter: SChapter, manga: SManga): File? {
|
||||
private fun updateCover(chapter: SChapter, manga: SManga): UniFile? {
|
||||
return try {
|
||||
when (val format = getFormat(chapter)) {
|
||||
is Format.Directory -> {
|
||||
val entry = format.file.listFiles()
|
||||
?.sortedWith { f1, f2 -> f1.name.compareToCaseInsensitiveNaturalOrder(f2.name) }
|
||||
?.find { !it.isDirectory && ImageUtil.isImage(it.name) { FileInputStream(it) } }
|
||||
?.sortedWith { f1, f2 -> f1.name.orEmpty().compareToCaseInsensitiveNaturalOrder(f2.name.orEmpty()) }
|
||||
?.find { !it.isDirectory && ImageUtil.isImage(it.name.orEmpty()) { FileInputStream(it.uri.toFile()) } }
|
||||
|
||||
entry?.let { updateCover(context, manga, it.inputStream()) }
|
||||
entry?.let { updateCover(manga, it.openInputStream()) }
|
||||
}
|
||||
is Format.Zip -> {
|
||||
ZipFile(format.file).use { zip ->
|
||||
ZipFile(format.file.uri.toFile()).use { zip ->
|
||||
val entry = zip.entries().toList()
|
||||
.sortedWith { f1, f2 -> f1.name.compareToCaseInsensitiveNaturalOrder(f2.name) }
|
||||
.find { !it.isDirectory && ImageUtil.isImage(it.name) { zip.getInputStream(it) } }
|
||||
|
||||
entry?.let { updateCover(context, manga, zip.getInputStream(it)) }
|
||||
entry?.let { updateCover(manga, zip.getInputStream(it)) }
|
||||
}
|
||||
}
|
||||
is Format.Rar -> {
|
||||
Archive(format.file).use { archive ->
|
||||
Archive(format.file.uri.toFile()).use { archive ->
|
||||
val entry = archive.fileHeaders
|
||||
.sortedWith { f1, f2 -> f1.fileName.compareToCaseInsensitiveNaturalOrder(f2.fileName) }
|
||||
.find { !it.isDirectory && ImageUtil.isImage(it.fileName) { archive.getInputStream(it) } }
|
||||
|
||||
entry?.let { updateCover(context, manga, archive.getInputStream(it)) }
|
||||
entry?.let { updateCover(manga, archive.getInputStream(it)) }
|
||||
}
|
||||
}
|
||||
is Format.Epub -> {
|
||||
EpubFile(format.file).use { epub ->
|
||||
EpubFile(format.file.uri.toFile()).use { epub ->
|
||||
val entry = epub.getImagesFromPages()
|
||||
.firstOrNull()
|
||||
?.let { epub.getEntry(it) }
|
||||
|
||||
entry?.let { updateCover(context, manga, epub.getInputStream(it)) }
|
||||
entry?.let { updateCover(manga, epub.getInputStream(it)) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -374,10 +368,10 @@ class LocalSource(private val context: Context) : CatalogueSource, UnmeteredSour
|
|||
)
|
||||
|
||||
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()
|
||||
data class Directory(val file: UniFile) : Format()
|
||||
data class Zip(val file: UniFile) : Format()
|
||||
data class Rar(val file: UniFile) : Format()
|
||||
data class Epub(val file: UniFile) : Format()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue