diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 30278a7c6..cc43933c2 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -210,6 +210,7 @@ dependencies { implementation(libs.disklrucache) implementation(libs.unifile) implementation(libs.junrar) + implementation(libs.bundles.sevenzip) // Preferences implementation(libs.preferencektx) 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 b8f97c5f4..cbe9c27c5 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 @@ -98,6 +98,7 @@ class ChapterLoader( when (format) { is Format.Directory -> DirectoryPageLoader(format.file) is Format.Zip -> ZipPageLoader(tempFileManager.createTempFile(format.file)) + is Format.SevenZip -> SevenZipPageLoader(format.file.toTempFile(context)) is Format.Rar -> try { RarPageLoader(tempFileManager.createTempFile(format.file)) } catch (e: UnsupportedRarV5Exception) { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/SevenZipPageLoader.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/SevenZipPageLoader.kt new file mode 100644 index 000000000..9e4ca829a --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/SevenZipPageLoader.kt @@ -0,0 +1,36 @@ +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.SevenZUtil.getImages +import org.apache.commons.compress.archivers.sevenz.SevenZFile +import java.io.File + +/** + * Loader used to load a chapter from a .7z or .cb7 file. + */ +internal class SevenZipPageLoader(file: File) : PageLoader() { + + private val zip by lazy { SevenZFile(file) } + + override var isLocal: Boolean = true + + override suspend fun getPages(): List { + return zip.getImages() + .mapIndexed { i, entry -> + ReaderPage(i).apply { + stream = { entry.copyOf().inputStream() } + status = Page.State.READY + } + }.toList() + } + + override suspend fun loadPage(page: ReaderPage) { + check(!isRecycled) + } + + override fun recycle() { + super.recycle() + zip.close() + } +} diff --git a/core/build.gradle.kts b/core/build.gradle.kts index e90a1fd06..6267cae2b 100644 --- a/core/build.gradle.kts +++ b/core/build.gradle.kts @@ -32,6 +32,7 @@ dependencies { implementation(libs.image.decoder) implementation(libs.unifile) + implementation(libs.bundles.sevenzip) api(kotlinx.coroutines.core) api(kotlinx.serialization.json) diff --git a/core/src/main/java/eu/kanade/tachiyomi/util/storage/SevenZUtil.kt b/core/src/main/java/eu/kanade/tachiyomi/util/storage/SevenZUtil.kt new file mode 100644 index 000000000..bc30d59a4 --- /dev/null +++ b/core/src/main/java/eu/kanade/tachiyomi/util/storage/SevenZUtil.kt @@ -0,0 +1,16 @@ +package eu.kanade.tachiyomi.util.storage + +import eu.kanade.tachiyomi.util.lang.compareToCaseInsensitiveNaturalOrder +import org.apache.commons.compress.archivers.sevenz.SevenZFile +import tachiyomi.core.util.system.ImageUtil +import java.io.InputStream + +object SevenZUtil { + fun SevenZFile.getImages(): Sequence { + return generateSequence { runCatching { getNextEntry() }.getOrNull() } + .filter { !it.isDirectory && ImageUtil.isImage(it.name) { getInputStream(it) } } + .sortedWith { f1, f2 -> f1.name.compareToCaseInsensitiveNaturalOrder(f2.name) } + .map(::getInputStream) + .map { it.use(InputStream::readBytes) } // ByteArray + } +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 479c3b44a..e0fb5d262 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -31,6 +31,8 @@ jsoup = "org.jsoup:jsoup:1.17.2" disklrucache = "com.jakewharton:disklrucache:2.0.2" unifile = "com.github.tachiyomiorg:unifile:7c257e1c64" junrar = "com.github.junrar:junrar:7.5.5" +common-compress = "org.apache.commons:commons-compress:1.25.0" +xz = "org.tukaani:xz:1.9" sqlite-framework = { module = "androidx.sqlite:sqlite-framework", version.ref = "sqlite" } sqlite-ktx = { module = "androidx.sqlite:sqlite-ktx", version.ref = "sqlite" } @@ -104,4 +106,5 @@ shizuku = ["shizuku-api", "shizuku-provider"] sqldelight = ["sqldelight-android-driver", "sqldelight-coroutines", "sqldelight-android-paging"] voyager = ["voyager-navigator", "voyager-screenmodel", "voyager-tab-navigator", "voyager-transitions"] richtext = ["richtext-commonmark", "richtext-m3"] -test = ["junit", "kotest-assertions", "mockk"] \ No newline at end of file +test = ["junit", "kotest-assertions", "mockk"] +sevenzip = ["common-compress", "xz"] \ No newline at end of file diff --git a/source-local/build.gradle.kts b/source-local/build.gradle.kts index 98eb4d55a..21f3eafa1 100644 --- a/source-local/build.gradle.kts +++ b/source-local/build.gradle.kts @@ -13,6 +13,7 @@ kotlin { implementation(libs.unifile) implementation(libs.junrar) + implementation(libs.bundles.sevenzip) } } 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 93bb2ec6f..a887b0025 100644 --- a/source-local/src/androidMain/kotlin/tachiyomi/source/local/LocalSource.kt +++ b/source-local/src/androidMain/kotlin/tachiyomi/source/local/LocalSource.kt @@ -11,6 +11,7 @@ import eu.kanade.tachiyomi.source.model.SChapter import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.util.lang.compareToCaseInsensitiveNaturalOrder import eu.kanade.tachiyomi.util.storage.EpubFile +import eu.kanade.tachiyomi.util.storage.SevenZUtil.getImages import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll import kotlinx.serialization.json.Json @@ -18,6 +19,7 @@ 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.sevenz.SevenZFile import tachiyomi.core.i18n.stringResource import tachiyomi.core.metadata.comicinfo.COMIC_INFO_FILE import tachiyomi.core.metadata.comicinfo.ComicInfo @@ -340,6 +342,13 @@ actual class LocalSource( entry?.let { coverManager.update(manga, zip.getInputStream(it)) } } } + is Format.SevenZip -> { + SevenZFile(format.file.toTempFile(context)).use { archive -> + val entry = archive.getImages().firstOrNull() + + entry?.let { coverManager.update(manga, it.inputStream()) } + } + } is Format.Rar -> { JunrarArchive(tempFileManager.createTempFile(format.file)).use { archive -> val entry = archive.fileHeaders diff --git a/source-local/src/commonMain/kotlin/tachiyomi/source/local/io/Archive.kt b/source-local/src/commonMain/kotlin/tachiyomi/source/local/io/Archive.kt index a8f5a0740..c83ddac9d 100644 --- a/source-local/src/commonMain/kotlin/tachiyomi/source/local/io/Archive.kt +++ b/source-local/src/commonMain/kotlin/tachiyomi/source/local/io/Archive.kt @@ -5,7 +5,7 @@ import tachiyomi.core.storage.extension object Archive { - private val SUPPORTED_ARCHIVE_TYPES = listOf("zip", "cbz", "rar", "cbr", "epub") + private val SUPPORTED_ARCHIVE_TYPES = listOf("zip", "cbz", "7z", "cb7", "rar", "cbr", "epub") fun isSupported(file: UniFile): Boolean { return file.extension in SUPPORTED_ARCHIVE_TYPES diff --git a/source-local/src/commonMain/kotlin/tachiyomi/source/local/io/Format.kt b/source-local/src/commonMain/kotlin/tachiyomi/source/local/io/Format.kt index 0f29ae8ab..57e71cafa 100644 --- a/source-local/src/commonMain/kotlin/tachiyomi/source/local/io/Format.kt +++ b/source-local/src/commonMain/kotlin/tachiyomi/source/local/io/Format.kt @@ -6,6 +6,7 @@ import tachiyomi.core.storage.extension sealed interface Format { data class Directory(val file: UniFile) : Format data class Zip(val file: UniFile) : Format + data class SevenZip(val file: UniFile) : Format data class Rar(val file: UniFile) : Format data class Epub(val file: UniFile) : Format @@ -17,6 +18,7 @@ sealed interface Format { when { isDirectory -> Directory(this) extension.equals("zip", true) || extension.equals("cbz", true) -> Zip(this) + extension.equals("7z", true) || extension.equals("cb7", true) -> SevenZip(this) extension.equals("rar", true) || extension.equals("cbr", true) -> Rar(this) extension.equals("epub", true) -> Epub(this) else -> throw UnknownFormatException()