Use UniFile for local source file handling

This commit is contained in:
arkon 2023-11-26 15:59:31 -05:00
parent 46aeab9a7a
commit ca54984344
20 changed files with 110 additions and 95 deletions

View File

@ -25,7 +25,7 @@ import kotlinx.serialization.json.Json
import nl.adaptivity.xmlutil.XmlDeclMode import nl.adaptivity.xmlutil.XmlDeclMode
import nl.adaptivity.xmlutil.core.XmlVersion import nl.adaptivity.xmlutil.core.XmlVersion
import nl.adaptivity.xmlutil.serialization.XML import nl.adaptivity.xmlutil.serialization.XML
import tachiyomi.core.provider.AndroidStorageFolderProvider import tachiyomi.core.storage.AndroidStorageFolderProvider
import tachiyomi.data.AndroidDatabaseHandler import tachiyomi.data.AndroidDatabaseHandler
import tachiyomi.data.Database import tachiyomi.data.Database
import tachiyomi.data.DatabaseHandler import tachiyomi.data.DatabaseHandler
@ -125,7 +125,7 @@ class AppModule(val app: Application) : InjektModule {
addSingletonFactory { ImageSaver(app) } addSingletonFactory { ImageSaver(app) }
addSingletonFactory { AndroidStorageFolderProvider(app) } addSingletonFactory { AndroidStorageFolderProvider(app) }
addSingletonFactory { LocalSourceFileSystem(get<AndroidStorageFolderProvider>()) } addSingletonFactory { LocalSourceFileSystem(app, get<AndroidStorageFolderProvider>()) }
addSingletonFactory { LocalCoverManager(app, get()) } addSingletonFactory { LocalCoverManager(app, get()) }
// Asynchronously init expensive components for a faster cold start // Asynchronously init expensive components for a faster cold start

View File

@ -11,7 +11,7 @@ import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences
import eu.kanade.tachiyomi.util.system.isDevFlavor import eu.kanade.tachiyomi.util.system.isDevFlavor
import tachiyomi.core.preference.AndroidPreferenceStore import tachiyomi.core.preference.AndroidPreferenceStore
import tachiyomi.core.preference.PreferenceStore import tachiyomi.core.preference.PreferenceStore
import tachiyomi.core.provider.AndroidStorageFolderProvider import tachiyomi.core.storage.AndroidStorageFolderProvider
import tachiyomi.domain.backup.service.BackupPreferences import tachiyomi.domain.backup.service.BackupPreferences
import tachiyomi.domain.download.service.DownloadPreferences import tachiyomi.domain.download.service.DownloadPreferences
import tachiyomi.domain.library.service.LibraryPreferences import tachiyomi.domain.library.service.LibraryPreferences

View File

@ -1,25 +1,24 @@
package eu.kanade.tachiyomi.ui.reader.loader package eu.kanade.tachiyomi.ui.reader.loader
import com.hippo.unifile.UniFile
import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
import eu.kanade.tachiyomi.util.lang.compareToCaseInsensitiveNaturalOrder import eu.kanade.tachiyomi.util.lang.compareToCaseInsensitiveNaturalOrder
import tachiyomi.core.util.system.ImageUtil import tachiyomi.core.util.system.ImageUtil
import java.io.File
import java.io.FileInputStream
/** /**
* Loader used to load a chapter from a directory given on [file]. * Loader used to load a chapter from a directory given on [file].
*/ */
internal class DirectoryPageLoader(val file: File) : PageLoader() { internal class DirectoryPageLoader(val file: UniFile) : PageLoader() {
override var isLocal: Boolean = true override var isLocal: Boolean = true
override suspend fun getPages(): List<ReaderPage> { override suspend fun getPages(): List<ReaderPage> {
return file.listFiles() return file.listFiles()
?.filter { !it.isDirectory && ImageUtil.isImage(it.name) { FileInputStream(it) } } ?.filter { !it.isDirectory && ImageUtil.isImage(it.name) { it.openInputStream() } }
?.sortedWith { f1, f2 -> f1.name.compareToCaseInsensitiveNaturalOrder(f2.name) } ?.sortedWith { f1, f2 -> f1.name.orEmpty().compareToCaseInsensitiveNaturalOrder(f2.name.orEmpty()) }
?.mapIndexed { i, file -> ?.mapIndexed { i, file ->
val streamFn = { FileInputStream(file) } val streamFn = { file.openInputStream() }
ReaderPage(i).apply { ReaderPage(i).apply {
stream = streamFn stream = streamFn
status = Page.State.READY status = Page.State.READY

View File

@ -12,7 +12,6 @@ import eu.kanade.tachiyomi.ui.reader.model.ReaderChapter
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
import tachiyomi.domain.manga.model.Manga import tachiyomi.domain.manga.model.Manga
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
import java.io.File
/** /**
* Loader used to load a chapter from the downloaded chapters. * Loader used to load a chapter from the downloaded chapters.
@ -47,7 +46,7 @@ internal class DownloadPageLoader(
} }
private suspend fun getPagesFromArchive(chapterPath: UniFile): List<ReaderPage> { private suspend fun getPagesFromArchive(chapterPath: UniFile): List<ReaderPage> {
val loader = ZipPageLoader(File(chapterPath.filePath!!)).also { zipPageLoader = it } val loader = ZipPageLoader(chapterPath).also { zipPageLoader = it }
return loader.getPages() return loader.getPages()
} }

View File

@ -1,14 +1,14 @@
package eu.kanade.tachiyomi.ui.reader.loader package eu.kanade.tachiyomi.ui.reader.loader
import com.hippo.unifile.UniFile
import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
import eu.kanade.tachiyomi.util.storage.EpubFile import eu.kanade.tachiyomi.util.storage.EpubFile
import java.io.File
/** /**
* Loader used to load a chapter from a .epub file. * Loader used to load a chapter from a .epub file.
*/ */
internal class EpubPageLoader(file: File) : PageLoader() { internal class EpubPageLoader(file: UniFile) : PageLoader() {
private val epub = EpubFile(file) private val epub = EpubFile(file)

View File

@ -2,11 +2,12 @@ package eu.kanade.tachiyomi.ui.reader.loader
import com.github.junrar.Archive import com.github.junrar.Archive
import com.github.junrar.rarfile.FileHeader import com.github.junrar.rarfile.FileHeader
import com.hippo.unifile.UniFile
import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
import eu.kanade.tachiyomi.util.lang.compareToCaseInsensitiveNaturalOrder import eu.kanade.tachiyomi.util.lang.compareToCaseInsensitiveNaturalOrder
import tachiyomi.core.storage.toFile
import tachiyomi.core.util.system.ImageUtil import tachiyomi.core.util.system.ImageUtil
import java.io.File
import java.io.InputStream import java.io.InputStream
import java.io.PipedInputStream import java.io.PipedInputStream
import java.io.PipedOutputStream import java.io.PipedOutputStream
@ -14,9 +15,9 @@ import java.io.PipedOutputStream
/** /**
* Loader used to load a chapter from a .rar or .cbr file. * Loader used to load a chapter from a .rar or .cbr file.
*/ */
internal class RarPageLoader(file: File) : PageLoader() { internal class RarPageLoader(file: UniFile) : PageLoader() {
private val rar = Archive(file) private val rar = Archive(file.toFile())
override var isLocal: Boolean = true override var isLocal: Boolean = true

View File

@ -1,23 +1,24 @@
package eu.kanade.tachiyomi.ui.reader.loader package eu.kanade.tachiyomi.ui.reader.loader
import android.os.Build import android.os.Build
import com.hippo.unifile.UniFile
import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
import eu.kanade.tachiyomi.util.lang.compareToCaseInsensitiveNaturalOrder import eu.kanade.tachiyomi.util.lang.compareToCaseInsensitiveNaturalOrder
import tachiyomi.core.storage.toFile
import tachiyomi.core.util.system.ImageUtil import tachiyomi.core.util.system.ImageUtil
import java.io.File
import java.nio.charset.StandardCharsets import java.nio.charset.StandardCharsets
import java.util.zip.ZipFile import java.util.zip.ZipFile
/** /**
* Loader used to load a chapter from a .zip or .cbz file. * Loader used to load a chapter from a .zip or .cbz file.
*/ */
internal class ZipPageLoader(file: File) : PageLoader() { internal class ZipPageLoader(file: UniFile) : PageLoader() {
private val zip = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { private val zip = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
ZipFile(file, StandardCharsets.ISO_8859_1) ZipFile(file.toFile(), StandardCharsets.ISO_8859_1)
} else { } else {
ZipFile(file) ZipFile(file.toFile())
} }
override var isLocal: Boolean = true override var isLocal: Boolean = true

View File

@ -1,7 +1,9 @@
package eu.kanade.tachiyomi.util.storage package eu.kanade.tachiyomi.util.storage
import com.hippo.unifile.UniFile
import org.jsoup.Jsoup import org.jsoup.Jsoup
import org.jsoup.nodes.Document import org.jsoup.nodes.Document
import tachiyomi.core.storage.toFile
import java.io.Closeable import java.io.Closeable
import java.io.File import java.io.File
import java.io.InputStream import java.io.InputStream
@ -11,12 +13,12 @@ import java.util.zip.ZipFile
/** /**
* Wrapper over ZipFile to load files in epub format. * Wrapper over ZipFile to load files in epub format.
*/ */
class EpubFile(file: File) : Closeable { class EpubFile(file: UniFile) : Closeable {
/** /**
* Zip file of this epub. * Zip file of this epub.
*/ */
private val zip = ZipFile(file) private val zip = ZipFile(file.toFile())
/** /**
* Path separator used by this epub. * Path separator used by this epub.

View File

@ -1,4 +1,4 @@
package tachiyomi.core.provider package tachiyomi.core.storage
import android.content.Context import android.content.Context
import android.os.Environment import android.os.Environment

View File

@ -1,4 +1,4 @@
package tachiyomi.core.provider package tachiyomi.core.storage
import java.io.File import java.io.File

View File

@ -1,9 +1,12 @@
package tachiyomi.core.storage package tachiyomi.core.storage
import com.hippo.unifile.UniFile import com.hippo.unifile.UniFile
import java.io.File
val UniFile.extension: String? val UniFile.extension: String?
get() = name?.substringAfterLast('.') get() = name?.substringAfterLast('.')
val UniFile.nameWithoutExtension: String? val UniFile.nameWithoutExtension: String?
get() = name?.substringBeforeLast('.') get() = name?.substringBeforeLast('.')
fun UniFile.toFile(): File? = filePath?.let { File(it) }

View File

@ -37,7 +37,9 @@ import kotlin.math.min
object ImageUtil { object ImageUtil {
fun isImage(name: String, openStream: (() -> InputStream)? = null): Boolean { fun isImage(name: String?, openStream: (() -> InputStream)? = null): Boolean {
if (name == null) return false
val contentType = try { val contentType = try {
URLConnection.guessContentTypeFromName(name) URLConnection.guessContentTypeFromName(name)
} catch (e: Exception) { } catch (e: Exception) {

View File

@ -1,7 +1,7 @@
package tachiyomi.domain.storage.service package tachiyomi.domain.storage.service
import tachiyomi.core.preference.PreferenceStore import tachiyomi.core.preference.PreferenceStore
import tachiyomi.core.provider.FolderProvider import tachiyomi.core.storage.FolderProvider
class StoragePreferences( class StoragePreferences(
private val folderProvider: FolderProvider, private val folderProvider: FolderProvider,

View File

@ -22,6 +22,9 @@ import tachiyomi.core.metadata.comicinfo.ComicInfo
import tachiyomi.core.metadata.comicinfo.copyFromComicInfo import tachiyomi.core.metadata.comicinfo.copyFromComicInfo
import tachiyomi.core.metadata.comicinfo.getComicInfo import tachiyomi.core.metadata.comicinfo.getComicInfo
import tachiyomi.core.metadata.tachiyomi.MangaDetails import tachiyomi.core.metadata.tachiyomi.MangaDetails
import tachiyomi.core.storage.extension
import tachiyomi.core.storage.nameWithoutExtension
import tachiyomi.core.storage.toFile
import tachiyomi.core.util.lang.withIOContext import tachiyomi.core.util.lang.withIOContext
import tachiyomi.core.util.system.ImageUtil import tachiyomi.core.util.system.ImageUtil
import tachiyomi.core.util.system.logcat import tachiyomi.core.util.system.logcat
@ -37,7 +40,6 @@ import tachiyomi.source.local.metadata.fillChapterMetadata
import tachiyomi.source.local.metadata.fillMangaMetadata import tachiyomi.source.local.metadata.fillMangaMetadata
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
import java.io.File import java.io.File
import java.io.FileInputStream
import java.io.InputStream import java.io.InputStream
import java.nio.charset.StandardCharsets import java.nio.charset.StandardCharsets
import java.util.zip.ZipFile import java.util.zip.ZipFile
@ -83,11 +85,11 @@ actual class LocalSource(
} }
var mangaDirs = baseDirFiles var mangaDirs = baseDirFiles
// Filter out files that are hidden and is not a folder // Filter out files that are hidden and is not a folder
.filter { it.isDirectory && !it.name.startsWith('.') } .filter { it.isDirectory && !it.name.orEmpty().startsWith('.') }
.distinctBy { it.name } .distinctBy { it.name }
.filter { // Filter by query or last modified .filter { // Filter by query or last modified
if (lastModifiedLimit == 0L) { if (lastModifiedLimit == 0L) {
it.name.contains(query, ignoreCase = true) it.name.orEmpty().contains(query, ignoreCase = true)
} else { } else {
it.lastModified() >= lastModifiedLimit it.lastModified() >= lastModifiedLimit
} }
@ -97,16 +99,16 @@ actual class LocalSource(
when (filter) { when (filter) {
is OrderBy.Popular -> { is OrderBy.Popular -> {
mangaDirs = if (filter.state!!.ascending) { mangaDirs = if (filter.state!!.ascending) {
mangaDirs.sortedWith(compareBy(String.CASE_INSENSITIVE_ORDER) { it.name }) mangaDirs.sortedWith(compareBy(String.CASE_INSENSITIVE_ORDER) { it.name.orEmpty() })
} else { } else {
mangaDirs.sortedWith(compareByDescending(String.CASE_INSENSITIVE_ORDER) { it.name }) mangaDirs.sortedWith(compareByDescending(String.CASE_INSENSITIVE_ORDER) { it.name.orEmpty() })
} }
} }
is OrderBy.Latest -> { is OrderBy.Latest -> {
mangaDirs = if (filter.state!!.ascending) { mangaDirs = if (filter.state!!.ascending) {
mangaDirs.sortedBy(File::lastModified) mangaDirs.sortedBy(UniFile::lastModified)
} else { } else {
mangaDirs.sortedByDescending(File::lastModified) mangaDirs.sortedByDescending(UniFile::lastModified)
} }
} }
@ -119,13 +121,13 @@ actual class LocalSource(
// Transform mangaDirs to list of SManga // Transform mangaDirs to list of SManga
val mangas = mangaDirs.map { mangaDir -> val mangas = mangaDirs.map { mangaDir ->
SManga.create().apply { SManga.create().apply {
title = mangaDir.name title = mangaDir.name.orEmpty()
url = mangaDir.name url = mangaDir.name.orEmpty()
// Try to find the cover // Try to find the cover
coverManager.find(mangaDir.name) coverManager.find(mangaDir.name.orEmpty())
?.takeIf(File::exists) ?.takeIf(UniFile::exists)
?.let { thumbnail_url = it.absolutePath } ?.let { thumbnail_url = it.uri.toString() }
} }
} }
@ -155,7 +157,7 @@ actual class LocalSource(
// Manga details related // Manga details related
override suspend fun getMangaDetails(manga: SManga): SManga = withIOContext { override suspend fun getMangaDetails(manga: SManga): SManga = withIOContext {
coverManager.find(manga.url)?.let { coverManager.find(manga.url)?.let {
manga.thumbnail_url = it.absolutePath manga.thumbnail_url = it.uri.toString()
} }
// Augment manga details based on metadata files // Augment manga details based on metadata files
@ -174,13 +176,13 @@ actual class LocalSource(
// Top level ComicInfo.xml // Top level ComicInfo.xml
comicInfoFile != null -> { comicInfoFile != null -> {
noXmlFile?.delete() noXmlFile?.delete()
setMangaDetailsFromComicInfoFile(comicInfoFile.inputStream(), manga) setMangaDetailsFromComicInfoFile(comicInfoFile.openInputStream(), manga)
} }
// Old custom JSON format // Old custom JSON format
// TODO: remove support for this entirely after a while // TODO: remove support for this entirely after a while
legacyJsonDetailsFile != null -> { legacyJsonDetailsFile != null -> {
json.decodeFromStream<MangaDetails>(legacyJsonDetailsFile.inputStream()).run { json.decodeFromStream<MangaDetails>(legacyJsonDetailsFile.openInputStream()).run {
title?.let { manga.title = it } title?.let { manga.title = it }
author?.let { manga.author = it } author?.let { manga.author = it }
artist?.let { manga.artist = it } artist?.let { manga.artist = it }
@ -190,7 +192,7 @@ actual class LocalSource(
} }
// Replace with ComicInfo.xml file // Replace with ComicInfo.xml file
val comicInfo = manga.getComicInfo() val comicInfo = manga.getComicInfo()
UniFile.fromFile(mangaDir) mangaDir
?.createFile(COMIC_INFO_FILE) ?.createFile(COMIC_INFO_FILE)
?.openOutputStream() ?.openOutputStream()
?.use { ?.use {
@ -206,7 +208,7 @@ actual class LocalSource(
.filter(Archive::isSupported) .filter(Archive::isSupported)
.toList() .toList()
val folderPath = mangaDir?.absolutePath val folderPath = mangaDir?.filePath
val copiedFile = copyComicInfoFileFromArchive(chapterArchives, folderPath) val copiedFile = copyComicInfoFileFromArchive(chapterArchives, folderPath)
if (copiedFile != null) { if (copiedFile != null) {
@ -224,11 +226,11 @@ actual class LocalSource(
return@withIOContext manga return@withIOContext manga
} }
private fun copyComicInfoFileFromArchive(chapterArchives: List<File>, folderPath: String?): File? { private fun copyComicInfoFileFromArchive(chapterArchives: List<UniFile>, folderPath: String?): File? {
for (chapter in chapterArchives) { for (chapter in chapterArchives) {
when (Format.valueOf(chapter)) { when (Format.valueOf(chapter)) {
is Format.Zip -> { is Format.Zip -> {
ZipFile(chapter).use { zip: ZipFile -> ZipFile(chapter.toFile()).use { zip: ZipFile ->
zip.getEntry(COMIC_INFO_FILE)?.let { comicInfoFile -> zip.getEntry(COMIC_INFO_FILE)?.let { comicInfoFile ->
zip.getInputStream(comicInfoFile).buffered().use { stream -> zip.getInputStream(comicInfoFile).buffered().use { stream ->
return copyComicInfoFile(stream, folderPath) return copyComicInfoFile(stream, folderPath)
@ -237,7 +239,7 @@ actual class LocalSource(
} }
} }
is Format.Rar -> { is Format.Rar -> {
JunrarArchive(chapter).use { rar -> JunrarArchive(chapter.toFile()).use { rar ->
rar.fileHeaders.firstOrNull { it.fileName == COMIC_INFO_FILE }?.let { comicInfoFile -> rar.fileHeaders.firstOrNull { it.fileName == COMIC_INFO_FILE }?.let { comicInfoFile ->
rar.getInputStream(comicInfoFile).buffered().use { stream -> rar.getInputStream(comicInfoFile).buffered().use { stream ->
return copyComicInfoFile(stream, folderPath) return copyComicInfoFile(stream, folderPath)
@ -276,9 +278,9 @@ actual class LocalSource(
SChapter.create().apply { SChapter.create().apply {
url = "${manga.url}/${chapterFile.name}" url = "${manga.url}/${chapterFile.name}"
name = if (chapterFile.isDirectory) { name = if (chapterFile.isDirectory) {
chapterFile.name chapterFile.name.orEmpty()
} else { } else {
chapterFile.nameWithoutExtension chapterFile.nameWithoutExtension.orEmpty()
} }
date_upload = chapterFile.lastModified() date_upload = chapterFile.lastModified()
chapter_number = ChapterRecognition chapter_number = ChapterRecognition
@ -308,8 +310,8 @@ actual class LocalSource(
fun getFormat(chapter: SChapter): Format { fun getFormat(chapter: SChapter): Format {
try { try {
return File(fileSystem.getBaseDirectory(), chapter.url) return fileSystem.getBaseDirectory()
.takeIf { it.exists() } ?.findFile(chapter.url)
?.let(Format.Companion::valueOf) ?.let(Format.Companion::valueOf)
?: throw Exception(context.stringResource(MR.strings.chapter_not_found)) ?: throw Exception(context.stringResource(MR.strings.chapter_not_found))
} catch (e: Format.UnknownFormatException) { } catch (e: Format.UnknownFormatException) {
@ -319,18 +321,24 @@ actual class LocalSource(
} }
} }
private fun updateCover(chapter: SChapter, manga: SManga): File? { private fun updateCover(chapter: SChapter, manga: SManga): UniFile? {
return try { return try {
when (val format = getFormat(chapter)) { when (val format = getFormat(chapter)) {
is Format.Directory -> { is Format.Directory -> {
val entry = format.file.listFiles() val entry = format.file.listFiles()
?.sortedWith { f1, f2 -> f1.name.compareToCaseInsensitiveNaturalOrder(f2.name) } ?.sortedWith { f1, f2 ->
?.find { !it.isDirectory && ImageUtil.isImage(it.name) { FileInputStream(it) } } f1.name.orEmpty().compareToCaseInsensitiveNaturalOrder(
f2.name.orEmpty(),
)
}
?.find {
!it.isDirectory && ImageUtil.isImage(it.name) { it.openInputStream() }
}
entry?.let { coverManager.update(manga, it.inputStream()) } entry?.let { coverManager.update(manga, it.openInputStream()) }
} }
is Format.Zip -> { is Format.Zip -> {
ZipFile(format.file).use { zip -> ZipFile(format.file.toFile()).use { zip ->
val entry = zip.entries().toList() val entry = zip.entries().toList()
.sortedWith { f1, f2 -> f1.name.compareToCaseInsensitiveNaturalOrder(f2.name) } .sortedWith { f1, f2 -> f1.name.compareToCaseInsensitiveNaturalOrder(f2.name) }
.find { !it.isDirectory && ImageUtil.isImage(it.name) { zip.getInputStream(it) } } .find { !it.isDirectory && ImageUtil.isImage(it.name) { zip.getInputStream(it) } }
@ -339,7 +347,7 @@ actual class LocalSource(
} }
} }
is Format.Rar -> { is Format.Rar -> {
JunrarArchive(format.file).use { archive -> JunrarArchive(format.file.toFile()).use { archive ->
val entry = archive.fileHeaders val entry = archive.fileHeaders
.sortedWith { f1, f2 -> f1.fileName.compareToCaseInsensitiveNaturalOrder(f2.fileName) } .sortedWith { f1, f2 -> f1.fileName.compareToCaseInsensitiveNaturalOrder(f2.fileName) }
.find { !it.isDirectory && ImageUtil.isImage(it.fileName) { archive.getInputStream(it) } } .find { !it.isDirectory && ImageUtil.isImage(it.fileName) { archive.getInputStream(it) } }

View File

@ -4,9 +4,9 @@ import android.content.Context
import com.hippo.unifile.UniFile import com.hippo.unifile.UniFile
import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.util.storage.DiskUtil import eu.kanade.tachiyomi.util.storage.DiskUtil
import tachiyomi.core.storage.nameWithoutExtension
import tachiyomi.core.util.system.ImageUtil import tachiyomi.core.util.system.ImageUtil
import tachiyomi.source.local.io.LocalSourceFileSystem import tachiyomi.source.local.io.LocalSourceFileSystem
import java.io.File
import java.io.InputStream import java.io.InputStream
private const val DEFAULT_COVER_NAME = "cover.jpg" private const val DEFAULT_COVER_NAME = "cover.jpg"
@ -16,43 +16,37 @@ actual class LocalCoverManager(
private val fileSystem: LocalSourceFileSystem, private val fileSystem: LocalSourceFileSystem,
) { ) {
actual fun find(mangaUrl: String): File? { actual fun find(mangaUrl: String): UniFile? {
return fileSystem.getFilesInMangaDirectory(mangaUrl) return fileSystem.getFilesInMangaDirectory(mangaUrl)
// Get all file whose names start with "cover" // Get all file whose names start with "cover"
.filter { it.isFile && it.nameWithoutExtension.equals("cover", ignoreCase = true) } .filter { it.isFile && it.nameWithoutExtension.equals("cover", ignoreCase = true) }
// Get the first actual image // Get the first actual image
.firstOrNull { .firstOrNull {
ImageUtil.isImage(it.name) { it.inputStream() } ImageUtil.isImage(it.name) { it.openInputStream() }
} }
} }
actual fun update( actual fun update(
manga: SManga, manga: SManga,
inputStream: InputStream, inputStream: InputStream,
): File? { ): UniFile? {
val directory = fileSystem.getMangaDirectory(manga.url) val directory = fileSystem.getMangaDirectory(manga.url)
if (directory == null) { if (directory == null) {
inputStream.close() inputStream.close()
return null return null
} }
var targetFile = find(manga.url) val targetFile = find(manga.url) ?: directory.createFile(DEFAULT_COVER_NAME)
if (targetFile == null) {
targetFile = File(directory.absolutePath, DEFAULT_COVER_NAME)
targetFile.createNewFile()
}
// It might not exist at this point
targetFile.parentFile?.mkdirs()
inputStream.use { input -> inputStream.use { input ->
targetFile.outputStream().use { output -> targetFile.openOutputStream().use { output ->
input.copyTo(output) input.copyTo(output)
} }
} }
DiskUtil.createNoMediaFile(UniFile.fromFile(directory), context) DiskUtil.createNoMediaFile(directory, context)
manga.thumbnail_url = targetFile.absolutePath manga.thumbnail_url = targetFile.uri.toString()
return targetFile return targetFile
} }
} }

View File

@ -1,27 +1,31 @@
package tachiyomi.source.local.io package tachiyomi.source.local.io
import tachiyomi.core.provider.FolderProvider import android.content.Context
import java.io.File import androidx.core.net.toUri
import com.hippo.unifile.UniFile
import tachiyomi.core.storage.FolderProvider
actual class LocalSourceFileSystem( actual class LocalSourceFileSystem(
private val context: Context,
private val folderProvider: FolderProvider, private val folderProvider: FolderProvider,
) { ) {
actual fun getBaseDirectory(): File { actual fun getBaseDirectory(): UniFile? {
return File(folderProvider.directory(), "local") return UniFile.fromUri(context, folderProvider.path().toUri())
?.createDirectory("local")
} }
actual fun getFilesInBaseDirectory(): List<File> { actual fun getFilesInBaseDirectory(): List<UniFile> {
return getBaseDirectory().listFiles().orEmpty().toList() return getBaseDirectory()?.listFiles().orEmpty().toList()
} }
actual fun getMangaDirectory(name: String): File? { actual fun getMangaDirectory(name: String): UniFile? {
return getFilesInBaseDirectory() return getFilesInBaseDirectory()
// Get the first mangaDir or null // Get the first mangaDir or null
.firstOrNull { it.isDirectory && it.name == name } .firstOrNull { it.isDirectory && it.name == name }
} }
actual fun getFilesInMangaDirectory(name: String): List<File> { actual fun getFilesInMangaDirectory(name: String): List<UniFile> {
return getFilesInBaseDirectory() return getFilesInBaseDirectory()
// Filter out ones that are not related to the manga and is not a directory // Filter out ones that are not related to the manga and is not a directory
.filter { it.isDirectory && it.name == name } .filter { it.isDirectory && it.name == name }

View File

@ -1,12 +1,12 @@
package tachiyomi.source.local.image package tachiyomi.source.local.image
import com.hippo.unifile.UniFile
import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.model.SManga
import java.io.File
import java.io.InputStream import java.io.InputStream
expect class LocalCoverManager { expect class LocalCoverManager {
fun find(mangaUrl: String): File? fun find(mangaUrl: String): UniFile?
fun update(manga: SManga, inputStream: InputStream): File? fun update(manga: SManga, inputStream: InputStream): UniFile?
} }

View File

@ -1,12 +1,13 @@
package tachiyomi.source.local.io package tachiyomi.source.local.io
import java.io.File import com.hippo.unifile.UniFile
import tachiyomi.core.storage.extension
object Archive { object Archive {
private val SUPPORTED_ARCHIVE_TYPES = listOf("zip", "cbz", "rar", "cbr", "epub") private val SUPPORTED_ARCHIVE_TYPES = listOf("zip", "cbz", "rar", "cbr", "epub")
fun isSupported(file: File): Boolean = with(file) { fun isSupported(file: UniFile): Boolean {
return extension.lowercase() in SUPPORTED_ARCHIVE_TYPES return file.extension in SUPPORTED_ARCHIVE_TYPES
} }
} }

View File

@ -1,18 +1,19 @@
package tachiyomi.source.local.io package tachiyomi.source.local.io
import java.io.File import com.hippo.unifile.UniFile
import tachiyomi.core.storage.extension
sealed interface Format { sealed interface Format {
data class Directory(val file: File) : Format data class Directory(val file: UniFile) : Format
data class Zip(val file: File) : Format data class Zip(val file: UniFile) : Format
data class Rar(val file: File) : Format data class Rar(val file: UniFile) : Format
data class Epub(val file: File) : Format data class Epub(val file: UniFile) : Format
class UnknownFormatException : Exception() class UnknownFormatException : Exception()
companion object { companion object {
fun valueOf(file: File) = with(file) { fun valueOf(file: UniFile) = with(file) {
when { when {
isDirectory -> Directory(this) isDirectory -> Directory(this)
extension.equals("zip", true) || extension.equals("cbz", true) -> Zip(this) extension.equals("zip", true) || extension.equals("cbz", true) -> Zip(this)

View File

@ -1,14 +1,14 @@
package tachiyomi.source.local.io package tachiyomi.source.local.io
import java.io.File import com.hippo.unifile.UniFile
expect class LocalSourceFileSystem { expect class LocalSourceFileSystem {
fun getBaseDirectory(): File fun getBaseDirectory(): UniFile?
fun getFilesInBaseDirectory(): List<File> fun getFilesInBaseDirectory(): List<UniFile>
fun getMangaDirectory(name: String): File? fun getMangaDirectory(name: String): UniFile?
fun getFilesInMangaDirectory(name: String): List<File> fun getFilesInMangaDirectory(name: String): List<UniFile>
} }