From 0da7ad6f1a15e8462d8270fc36ea9f135c3b8d29 Mon Sep 17 00:00:00 2001 From: FooIbar <118464521+FooIbar@users.noreply.github.com> Date: Sun, 4 Feb 2024 01:33:18 +0800 Subject: [PATCH] Don't copy archives to temp files when opening (#326) --- app/build.gradle.kts | 2 +- app/proguard-rules.pro | 3 ++ .../java/eu/kanade/tachiyomi/di/AppModule.kt | 3 -- .../tachiyomi/ui/reader/ReaderViewModel.kt | 5 +- .../ui/reader/loader/ChapterLoader.kt | 10 ++-- .../ui/reader/loader/DownloadPageLoader.kt | 5 +- .../ui/reader/loader/EpubPageLoader.kt | 6 +-- .../ui/reader/loader/RarPageLoader.kt | 5 +- .../ui/reader/loader/ZipPageLoader.kt | 11 ++--- core/common/build.gradle.kts | 1 + .../kanade/tachiyomi/util/storage/EpubFile.kt | 13 +++--- .../core/common/storage/UniFileExtensions.kt | 7 +++ .../common/storage/UniFileTempFileManager.kt | 46 ------------------- gradle/libs.versions.toml | 2 + source-local/build.gradle.kts | 2 +- .../tachiyomi/source/local/LocalSource.kt | 19 ++++---- 16 files changed, 48 insertions(+), 92 deletions(-) delete mode 100644 core/common/src/main/kotlin/tachiyomi/core/common/storage/UniFileTempFileManager.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 705981b4b..867e8f851 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -208,7 +208,7 @@ dependencies { // Disk implementation(libs.disklrucache) implementation(libs.unifile) - implementation(libs.junrar) + implementation(libs.bundles.archive) // Preferences implementation(libs.preferencektx) diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index 64220f2b1..c44a8da74 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -72,6 +72,9 @@ # XmlUtil -keep public enum nl.adaptivity.xmlutil.EventType { *; } +# Apache Commons Compress +-keep class * extends org.apache.commons.compress.archivers.zip.ZipExtraField { (); } + # Firebase -keep class com.google.firebase.installations.** { *; } -keep interface com.google.firebase.installations.** { *; } diff --git a/app/src/main/java/eu/kanade/tachiyomi/di/AppModule.kt b/app/src/main/java/eu/kanade/tachiyomi/di/AppModule.kt index 9fb7b2fb5..9997a0711 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/di/AppModule.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/di/AppModule.kt @@ -27,7 +27,6 @@ import nl.adaptivity.xmlutil.XmlDeclMode import nl.adaptivity.xmlutil.core.XmlVersion import nl.adaptivity.xmlutil.serialization.XML import tachiyomi.core.common.storage.AndroidStorageFolderProvider -import tachiyomi.core.common.storage.UniFileTempFileManager import tachiyomi.data.AndroidDatabaseHandler import tachiyomi.data.Database import tachiyomi.data.DatabaseHandler @@ -112,8 +111,6 @@ class AppModule(val app: Application) : InjektModule { ProtoBuf } - addSingletonFactory { UniFileTempFileManager(app) } - addSingletonFactory { ChapterCache(app, get()) } addSingletonFactory { CoverCache(app) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderViewModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderViewModel.kt index c6170f2a9..a59f0500b 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderViewModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderViewModel.kt @@ -55,7 +55,6 @@ import kotlinx.coroutines.flow.update import kotlinx.coroutines.runBlocking import logcat.LogPriority import tachiyomi.core.common.preference.toggle -import tachiyomi.core.common.storage.UniFileTempFileManager import tachiyomi.core.common.util.lang.launchIO import tachiyomi.core.common.util.lang.launchNonCancellable import tachiyomi.core.common.util.lang.withIOContext @@ -86,7 +85,6 @@ class ReaderViewModel @JvmOverloads constructor( private val sourceManager: SourceManager = Injekt.get(), private val downloadManager: DownloadManager = Injekt.get(), private val downloadProvider: DownloadProvider = Injekt.get(), - private val tempFileManager: UniFileTempFileManager = Injekt.get(), private val imageSaver: ImageSaver = Injekt.get(), preferences: BasePreferences = Injekt.get(), val readerPreferences: ReaderPreferences = Injekt.get(), @@ -271,7 +269,7 @@ class ReaderViewModel @JvmOverloads constructor( val context = Injekt.get() val source = sourceManager.getOrStub(manga.source) - loader = ChapterLoader(context, downloadManager, downloadProvider, tempFileManager, manga, source) + loader = ChapterLoader(context, downloadManager, downloadProvider, manga, source) loadChapter(loader!!, chapterList.first { chapterId == it.chapter.id }) Result.success(true) @@ -907,7 +905,6 @@ class ReaderViewModel @JvmOverloads constructor( private fun deletePendingChapters() { viewModelScope.launchNonCancellable { downloadManager.deletePendingChapters() - tempFileManager.deleteTempFiles() } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/ChapterLoader.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/ChapterLoader.kt index f0edbe7f3..1cd18bceb 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/ChapterLoader.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/ChapterLoader.kt @@ -8,7 +8,7 @@ import eu.kanade.tachiyomi.source.Source import eu.kanade.tachiyomi.source.online.HttpSource import eu.kanade.tachiyomi.ui.reader.model.ReaderChapter import tachiyomi.core.common.i18n.stringResource -import tachiyomi.core.common.storage.UniFileTempFileManager +import tachiyomi.core.common.storage.openReadOnlyChannel import tachiyomi.core.common.util.lang.withIOContext import tachiyomi.core.common.util.system.logcat import tachiyomi.domain.manga.model.Manga @@ -24,7 +24,6 @@ class ChapterLoader( private val context: Context, private val downloadManager: DownloadManager, private val downloadProvider: DownloadProvider, - private val tempFileManager: UniFileTempFileManager, private val manga: Manga, private val source: Source, ) { @@ -92,18 +91,17 @@ class ChapterLoader( source, downloadManager, downloadProvider, - tempFileManager, ) source is LocalSource -> source.getFormat(chapter.chapter).let { format -> when (format) { is Format.Directory -> DirectoryPageLoader(format.file) - is Format.Zip -> ZipPageLoader(tempFileManager.createTempFile(format.file)) + is Format.Zip -> ZipPageLoader(format.file.openReadOnlyChannel(context)) is Format.Rar -> try { - RarPageLoader(tempFileManager.createTempFile(format.file)) + RarPageLoader(format.file.openInputStream()) } catch (e: UnsupportedRarV5Exception) { error(context.stringResource(MR.strings.loader_rar5_error)) } - is Format.Epub -> EpubPageLoader(tempFileManager.createTempFile(format.file)) + is Format.Epub -> EpubPageLoader(format.file.openReadOnlyChannel(context)) } } source is HttpSource -> HttpPageLoader(chapter, source) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/DownloadPageLoader.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/DownloadPageLoader.kt index 1d18d7b47..abef28540 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/DownloadPageLoader.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/DownloadPageLoader.kt @@ -10,7 +10,7 @@ import eu.kanade.tachiyomi.source.Source import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.ui.reader.model.ReaderChapter import eu.kanade.tachiyomi.ui.reader.model.ReaderPage -import tachiyomi.core.common.storage.UniFileTempFileManager +import tachiyomi.core.common.storage.openReadOnlyChannel import tachiyomi.domain.manga.model.Manga import uy.kohesive.injekt.injectLazy @@ -23,7 +23,6 @@ internal class DownloadPageLoader( private val source: Source, private val downloadManager: DownloadManager, private val downloadProvider: DownloadProvider, - private val tempFileManager: UniFileTempFileManager, ) : PageLoader() { private val context: Application by injectLazy() @@ -48,7 +47,7 @@ internal class DownloadPageLoader( } private suspend fun getPagesFromArchive(file: UniFile): List { - val loader = ZipPageLoader(tempFileManager.createTempFile(file)).also { zipPageLoader = it } + val loader = ZipPageLoader(file.openReadOnlyChannel(context)).also { zipPageLoader = it } return loader.getPages() } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/EpubPageLoader.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/EpubPageLoader.kt index 324af51bf..baf65324b 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/EpubPageLoader.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/EpubPageLoader.kt @@ -3,14 +3,14 @@ package eu.kanade.tachiyomi.ui.reader.loader import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.ui.reader.model.ReaderPage import eu.kanade.tachiyomi.util.storage.EpubFile -import java.io.File +import java.nio.channels.SeekableByteChannel /** * Loader used to load a chapter from a .epub file. */ -internal class EpubPageLoader(file: File) : PageLoader() { +internal class EpubPageLoader(channel: SeekableByteChannel) : PageLoader() { - private val epub = EpubFile(file) + private val epub = EpubFile(channel) override var isLocal: Boolean = true diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/RarPageLoader.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/RarPageLoader.kt index 868ab92b2..b1db3300d 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/RarPageLoader.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/RarPageLoader.kt @@ -6,7 +6,6 @@ import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.ui.reader.model.ReaderPage import eu.kanade.tachiyomi.util.lang.compareToCaseInsensitiveNaturalOrder import tachiyomi.core.common.util.system.ImageUtil -import java.io.File import java.io.InputStream import java.io.PipedInputStream import java.io.PipedOutputStream @@ -15,9 +14,9 @@ import java.util.concurrent.Executors /** * Loader used to load a chapter from a .rar or .cbr file. */ -internal class RarPageLoader(file: File) : PageLoader() { +internal class RarPageLoader(inputStream: InputStream) : PageLoader() { - private val rar = Archive(file) + private val rar = Archive(inputStream) override var isLocal: Boolean = true diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/ZipPageLoader.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/ZipPageLoader.kt index e839d8a55..fadba25c4 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/ZipPageLoader.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/ZipPageLoader.kt @@ -3,22 +3,21 @@ package eu.kanade.tachiyomi.ui.reader.loader import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.ui.reader.model.ReaderPage import eu.kanade.tachiyomi.util.lang.compareToCaseInsensitiveNaturalOrder +import org.apache.commons.compress.archivers.zip.ZipFile import tachiyomi.core.common.util.system.ImageUtil -import java.io.File -import java.nio.charset.StandardCharsets -import java.util.zip.ZipFile +import java.nio.channels.SeekableByteChannel /** * Loader used to load a chapter from a .zip or .cbz file. */ -internal class ZipPageLoader(file: File) : PageLoader() { +internal class ZipPageLoader(channel: SeekableByteChannel) : PageLoader() { - private val zip = ZipFile(file, StandardCharsets.ISO_8859_1) + private val zip = ZipFile(channel) override var isLocal: Boolean = true override suspend fun getPages(): List { - return zip.entries().asSequence() + return zip.entries.asSequence() .filter { !it.isDirectory && ImageUtil.isImage(it.name) { zip.getInputStream(it) } } .sortedWith { f1, f2 -> f1.name.compareToCaseInsensitiveNaturalOrder(f2.name) } .mapIndexed { i, entry -> diff --git a/core/common/build.gradle.kts b/core/common/build.gradle.kts index 560e7285f..aaf0f794b 100644 --- a/core/common/build.gradle.kts +++ b/core/common/build.gradle.kts @@ -32,6 +32,7 @@ dependencies { implementation(libs.image.decoder) implementation(libs.unifile) + implementation(libs.bundles.archive) api(kotlinx.coroutines.core) api(kotlinx.serialization.json) diff --git a/core/common/src/main/kotlin/eu/kanade/tachiyomi/util/storage/EpubFile.kt b/core/common/src/main/kotlin/eu/kanade/tachiyomi/util/storage/EpubFile.kt index a00ee69e7..f664c175b 100644 --- a/core/common/src/main/kotlin/eu/kanade/tachiyomi/util/storage/EpubFile.kt +++ b/core/common/src/main/kotlin/eu/kanade/tachiyomi/util/storage/EpubFile.kt @@ -1,22 +1,23 @@ package eu.kanade.tachiyomi.util.storage +import org.apache.commons.compress.archivers.zip.ZipArchiveEntry +import org.apache.commons.compress.archivers.zip.ZipFile import org.jsoup.Jsoup import org.jsoup.nodes.Document import java.io.Closeable import java.io.File import java.io.InputStream -import java.util.zip.ZipEntry -import java.util.zip.ZipFile +import java.nio.channels.SeekableByteChannel /** * Wrapper over ZipFile to load files in epub format. */ -class EpubFile(file: File) : Closeable { +class EpubFile(channel: SeekableByteChannel) : Closeable { /** * Zip file of this epub. */ - private val zip = ZipFile(file) + private val zip = ZipFile(channel) /** * Path separator used by this epub. @@ -33,14 +34,14 @@ class EpubFile(file: File) : Closeable { /** * Returns an input stream for reading the contents of the specified zip file entry. */ - fun getInputStream(entry: ZipEntry): InputStream { + fun getInputStream(entry: ZipArchiveEntry): InputStream { return zip.getInputStream(entry) } /** * Returns the zip file entry for the specified name, or null if not found. */ - fun getEntry(name: String): ZipEntry? { + fun getEntry(name: String): ZipArchiveEntry? { return zip.getEntry(name) } diff --git a/core/common/src/main/kotlin/tachiyomi/core/common/storage/UniFileExtensions.kt b/core/common/src/main/kotlin/tachiyomi/core/common/storage/UniFileExtensions.kt index 8bbd9b3a7..257fe210d 100644 --- a/core/common/src/main/kotlin/tachiyomi/core/common/storage/UniFileExtensions.kt +++ b/core/common/src/main/kotlin/tachiyomi/core/common/storage/UniFileExtensions.kt @@ -1,6 +1,9 @@ package tachiyomi.core.common.storage +import android.content.Context +import android.os.ParcelFileDescriptor import com.hippo.unifile.UniFile +import java.nio.channels.FileChannel val UniFile.extension: String? get() = name?.substringAfterLast('.') @@ -10,3 +13,7 @@ val UniFile.nameWithoutExtension: String? val UniFile.displayablePath: String get() = filePath ?: uri.toString() + +fun UniFile.openReadOnlyChannel(context: Context): FileChannel { + return ParcelFileDescriptor.AutoCloseInputStream(context.contentResolver.openFileDescriptor(uri, "r")).channel +} diff --git a/core/common/src/main/kotlin/tachiyomi/core/common/storage/UniFileTempFileManager.kt b/core/common/src/main/kotlin/tachiyomi/core/common/storage/UniFileTempFileManager.kt deleted file mode 100644 index 4ccc5bf43..000000000 --- a/core/common/src/main/kotlin/tachiyomi/core/common/storage/UniFileTempFileManager.kt +++ /dev/null @@ -1,46 +0,0 @@ -package tachiyomi.core.common.storage - -import android.content.Context -import android.os.Build -import android.os.FileUtils -import com.hippo.unifile.UniFile -import java.io.BufferedOutputStream -import java.io.File - -class UniFileTempFileManager( - private val context: Context, -) { - - private val dir = File(context.externalCacheDir, "tmp") - - fun createTempFile(file: UniFile): File { - dir.mkdirs() - - val inputStream = context.contentResolver.openInputStream(file.uri)!! - val tempFile = File.createTempFile( - file.nameWithoutExtension.orEmpty().padEnd(3), // Prefix must be 3+ chars - null, - dir, - ) - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - FileUtils.copy(inputStream, tempFile.outputStream()) - } else { - BufferedOutputStream(tempFile.outputStream()).use { tmpOut -> - inputStream.use { input -> - val buffer = ByteArray(8192) - var count: Int - while (input.read(buffer).also { count = it } > 0) { - tmpOut.write(buffer, 0, count) - } - } - } - } - - return tempFile - } - - fun deleteTempFiles() { - dir.deleteRecursively() - } -} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 576d08176..c6c51a7ab 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -32,6 +32,7 @@ jsoup = "org.jsoup:jsoup:1.17.2" disklrucache = "com.jakewharton:disklrucache:2.0.2" unifile = "com.github.tachiyomiorg:unifile:7c257e1c64" +common-compress = "org.apache.commons:commons-compress:1.25.0" junrar = "com.github.junrar:junrar:7.5.5" sqlite-framework = { module = "androidx.sqlite:sqlite-framework", version.ref = "sqlite" } @@ -100,6 +101,7 @@ detekt-rules-formatting = { module = "io.gitlab.arturbosch.detekt:detekt-formatt detekt-rules-compose = { module = "io.nlopez.compose.rules:detekt", version.ref = "detektCompose" } [bundles] +archive = ["common-compress", "junrar"] okhttp = ["okhttp-core", "okhttp-logging", "okhttp-brotli", "okhttp-dnsoverhttps"] js-engine = ["quickjs-android"] sqlite = ["sqlite-framework", "sqlite-ktx", "sqlite-android"] diff --git a/source-local/build.gradle.kts b/source-local/build.gradle.kts index b30b8acd4..46de484ce 100644 --- a/source-local/build.gradle.kts +++ b/source-local/build.gradle.kts @@ -12,7 +12,7 @@ kotlin { api(projects.i18n) implementation(libs.unifile) - implementation(libs.junrar) + implementation(libs.bundles.archive) } } val androidMain by getting { diff --git a/source-local/src/androidMain/kotlin/tachiyomi/source/local/LocalSource.kt b/source-local/src/androidMain/kotlin/tachiyomi/source/local/LocalSource.kt index e33dbbf5f..10aa1e464 100644 --- a/source-local/src/androidMain/kotlin/tachiyomi/source/local/LocalSource.kt +++ b/source-local/src/androidMain/kotlin/tachiyomi/source/local/LocalSource.kt @@ -18,15 +18,16 @@ import kotlinx.serialization.json.decodeFromStream import logcat.LogPriority import nl.adaptivity.xmlutil.AndroidXmlReader import nl.adaptivity.xmlutil.serialization.XML +import org.apache.commons.compress.archivers.zip.ZipFile import tachiyomi.core.common.i18n.stringResource import tachiyomi.core.metadata.comicinfo.COMIC_INFO_FILE import tachiyomi.core.metadata.comicinfo.ComicInfo import tachiyomi.core.metadata.comicinfo.copyFromComicInfo import tachiyomi.core.metadata.comicinfo.getComicInfo import tachiyomi.core.metadata.tachiyomi.MangaDetails -import tachiyomi.core.common.storage.UniFileTempFileManager import tachiyomi.core.common.storage.extension import tachiyomi.core.common.storage.nameWithoutExtension +import tachiyomi.core.common.storage.openReadOnlyChannel import tachiyomi.core.common.util.lang.withIOContext import tachiyomi.core.common.util.system.ImageUtil import tachiyomi.core.common.util.system.logcat @@ -43,7 +44,6 @@ import uy.kohesive.injekt.injectLazy import java.io.File import java.io.InputStream import java.nio.charset.StandardCharsets -import java.util.zip.ZipFile import kotlin.time.Duration.Companion.days import com.github.junrar.Archive as JunrarArchive import tachiyomi.domain.source.model.Source as DomainSource @@ -56,7 +56,6 @@ actual class LocalSource( private val json: Json by injectLazy() private val xml: XML by injectLazy() - private val tempFileManager: UniFileTempFileManager by injectLazy() private val POPULAR_FILTERS = FilterList(OrderBy.Popular(context)) private val LATEST_FILTERS = FilterList(OrderBy.Latest(context)) @@ -214,7 +213,7 @@ actual class LocalSource( for (chapter in chapterArchives) { when (Format.valueOf(chapter)) { is Format.Zip -> { - ZipFile(tempFileManager.createTempFile(chapter)).use { zip: ZipFile -> + ZipFile(chapter.openReadOnlyChannel(context)).use { zip: ZipFile -> zip.getEntry(COMIC_INFO_FILE)?.let { comicInfoFile -> zip.getInputStream(comicInfoFile).buffered().use { stream -> return copyComicInfoFile(stream, folderPath) @@ -223,7 +222,7 @@ actual class LocalSource( } } is Format.Rar -> { - JunrarArchive(tempFileManager.createTempFile(chapter)).use { rar -> + JunrarArchive(chapter.openInputStream()).use { rar -> rar.fileHeaders.firstOrNull { it.fileName == COMIC_INFO_FILE }?.let { comicInfoFile -> rar.getInputStream(comicInfoFile).buffered().use { stream -> return copyComicInfoFile(stream, folderPath) @@ -273,7 +272,7 @@ actual class LocalSource( val format = Format.valueOf(chapterFile) if (format is Format.Epub) { - EpubFile(tempFileManager.createTempFile(format.file)).use { epub -> + EpubFile(format.file.openReadOnlyChannel(context)).use { epub -> epub.fillMetadata(manga, this) } } @@ -332,8 +331,8 @@ actual class LocalSource( entry?.let { coverManager.update(manga, it.openInputStream()) } } is Format.Zip -> { - ZipFile(tempFileManager.createTempFile(format.file)).use { zip -> - val entry = zip.entries().toList() + ZipFile(format.file.openReadOnlyChannel(context)).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) } } @@ -341,7 +340,7 @@ actual class LocalSource( } } is Format.Rar -> { - JunrarArchive(tempFileManager.createTempFile(format.file)).use { archive -> + JunrarArchive(format.file.openInputStream()).use { archive -> val entry = archive.fileHeaders .sortedWith { f1, f2 -> f1.fileName.compareToCaseInsensitiveNaturalOrder(f2.fileName) } .find { !it.isDirectory && ImageUtil.isImage(it.fileName) { archive.getInputStream(it) } } @@ -350,7 +349,7 @@ actual class LocalSource( } } is Format.Epub -> { - EpubFile(tempFileManager.createTempFile(format.file)).use { epub -> + EpubFile(format.file.openReadOnlyChannel(context)).use { epub -> val entry = epub.getImagesFromPages() .firstOrNull() ?.let { epub.getEntry(it) }