Download manga to cbz directly

This commit is contained in:
Koitharu
2022-04-18 16:42:37 +03:00
parent aaf9c6a0bf
commit d3e9ce874a
18 changed files with 340 additions and 394 deletions

View File

@@ -9,11 +9,11 @@ import coil.fetch.FetchResult
import coil.fetch.Fetcher
import coil.fetch.SourceResult
import coil.size.Size
import java.util.zip.ZipFile
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runInterruptible
import okio.buffer
import okio.source
import java.util.zip.ZipFile
class CbzFetcher : Fetcher<Uri> {

View File

@@ -28,14 +28,17 @@ class MangaIndex(source: String?) {
json.put("state", manga.state?.name)
json.put("source", manga.source.name)
json.put("cover_large", manga.largeCoverUrl)
json.put("tags", JSONArray().also { a ->
for (tag in manga.tags) {
val jo = JSONObject()
jo.put("key", tag.key)
jo.put("title", tag.title)
a.put(jo)
json.put(
"tags",
JSONArray().also { a ->
for (tag in manga.tags) {
val jo = JSONObject()
jo.put("key", tag.key)
jo.put("title", tag.title)
a.put(jo)
}
}
})
)
if (!append || !json.has("chapters")) {
json.put("chapters", JSONObject())
}

View File

@@ -1,70 +0,0 @@
package org.koitharu.kotatsu.local.data
import androidx.annotation.CheckResult
import androidx.annotation.WorkerThread
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaChapter
import org.koitharu.kotatsu.parsers.util.toFileNameSafe
import org.koitharu.kotatsu.utils.ext.takeIfReadable
import java.io.File
@WorkerThread
class MangaZip(val file: File) {
private val writableCbz = WritableCbzFile(file)
private var index = MangaIndex(null)
suspend fun prepare(manga: Manga) {
writableCbz.prepare(overwrite = true)
index = MangaIndex(writableCbz[INDEX_ENTRY].takeIfReadable()?.readText())
index.setMangaInfo(manga, append = true)
}
suspend fun cleanup() {
writableCbz.cleanup()
}
@CheckResult
suspend fun compress(): Boolean {
writableCbz[INDEX_ENTRY].writeText(index.toString())
return writableCbz.flush()
}
suspend fun addCover(file: File, ext: String) {
val name = buildString {
append(FILENAME_PATTERN.format(0, 0))
if (ext.isNotEmpty() && ext.length <= 4) {
append('.')
append(ext)
}
}
writableCbz.put(name, file)
index.setCoverEntry(name)
}
suspend fun addPage(chapter: MangaChapter, file: File, pageNumber: Int, ext: String) {
val name = buildString {
append(FILENAME_PATTERN.format(chapter.number, pageNumber))
if (ext.isNotEmpty() && ext.length <= 4) {
append('.')
append(ext)
}
}
writableCbz.put(name, file)
index.addChapter(chapter)
}
companion object {
private const val FILENAME_PATTERN = "%03d%03d"
const val INDEX_ENTRY = "index.json"
fun findInDir(root: File, manga: Manga): MangaZip {
val name = manga.title.toFileNameSafe() + ".cbz"
val file = File(root, name)
return MangaZip(file)
}
}
}

View File

@@ -2,13 +2,13 @@ package org.koitharu.kotatsu.local.data
import android.content.Context
import com.tomclaw.cache.DiskLruCache
import java.io.File
import java.io.InputStream
import kotlinx.coroutines.flow.MutableStateFlow
import org.koitharu.kotatsu.parsers.util.longHashCode
import org.koitharu.kotatsu.utils.FileSize
import org.koitharu.kotatsu.utils.ext.subdir
import org.koitharu.kotatsu.utils.ext.takeIfReadable
import java.io.File
import java.io.InputStream
class PagesCache(context: Context) {
@@ -60,4 +60,4 @@ class PagesCache(context: Context) {
progress.value = (bytesCopied.toDouble() / contentLength.toDouble()).toFloat()
}
}
}
}

View File

@@ -1,99 +0,0 @@
package org.koitharu.kotatsu.local.data
import androidx.annotation.CheckResult
import kotlinx.coroutines.*
import org.koitharu.kotatsu.utils.ext.deleteAwait
import java.io.File
import java.io.FileInputStream
import java.io.FileOutputStream
import java.util.zip.ZipEntry
import java.util.zip.ZipInputStream
import java.util.zip.ZipOutputStream
class WritableCbzFile(private val file: File) {
private val dir = File(file.parentFile, file.nameWithoutExtension)
suspend fun prepare(overwrite: Boolean) = withContext(Dispatchers.IO) {
if (!dir.list().isNullOrEmpty()) {
if (overwrite) {
dir.deleteRecursively()
} else {
throw IllegalStateException("Dir ${dir.name} is not empty")
}
}
if (!dir.exists()) {
dir.mkdir()
}
if (!file.exists()) {
return@withContext
}
ZipInputStream(FileInputStream(file)).use { zip ->
var entry = zip.nextEntry
while (entry != null && currentCoroutineContext().isActive) {
val target = File(dir.path + File.separator + entry.name)
runInterruptible {
target.parentFile?.mkdirs()
target.outputStream().use { out ->
zip.copyTo(out)
}
}
zip.closeEntry()
entry = zip.nextEntry
}
}
}
suspend fun cleanup() = withContext(Dispatchers.IO) {
dir.deleteRecursively()
}
@CheckResult
suspend fun flush() = withContext(Dispatchers.IO) {
val tempFile = File(file.path + ".tmp")
if (tempFile.exists()) {
tempFile.deleteAwait()
}
try {
runInterruptible {
ZipOutputStream(FileOutputStream(tempFile)).use { zip ->
dir.listFiles()?.forEach {
zipFile(it, it.name, zip)
}
zip.flush()
}
}
tempFile.renameTo(file)
} finally {
if (tempFile.exists()) {
tempFile.deleteAwait()
}
}
}
operator fun get(name: String) = File(dir, name)
suspend fun put(name: String, file: File) = runInterruptible(Dispatchers.IO) {
file.copyTo(this[name], overwrite = true)
}
private fun zipFile(fileToZip: File, fileName: String, zipOut: ZipOutputStream) {
if (fileToZip.isDirectory) {
if (fileName.endsWith("/")) {
zipOut.putNextEntry(ZipEntry(fileName))
} else {
zipOut.putNextEntry(ZipEntry("$fileName/"))
}
zipOut.closeEntry()
fileToZip.listFiles()?.forEach { childFile ->
zipFile(childFile, "$fileName/${childFile.name}", zipOut)
}
} else {
FileInputStream(fileToZip).use { fis ->
val zipEntry = ZipEntry(fileName)
zipOut.putNextEntry(zipEntry)
fis.copyTo(zipOut)
}
}
}
}

View File

@@ -0,0 +1,111 @@
package org.koitharu.kotatsu.local.domain
import androidx.annotation.WorkerThread
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runInterruptible
import okio.Closeable
import org.koitharu.kotatsu.core.zip.ZipOutput
import org.koitharu.kotatsu.local.data.MangaIndex
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaChapter
import org.koitharu.kotatsu.parsers.util.toFileNameSafe
import org.koitharu.kotatsu.utils.ext.deleteAwait
import java.io.File
import java.util.zip.ZipFile
class CbzMangaOutput(
val file: File,
manga: Manga,
) : Closeable {
private val output = ZipOutput(File(file.path + ".tmp"))
private val index = MangaIndex(null)
init {
index.setMangaInfo(manga, false)
}
suspend fun mergeWithExisting() {
if (file.exists()) {
runInterruptible(Dispatchers.IO) {
mergeWith(file)
}
}
}
suspend fun addCover(file: File, ext: String) {
val name = buildString {
append(FILENAME_PATTERN.format(0, 0))
if (ext.isNotEmpty() && ext.length <= 4) {
append('.')
append(ext)
}
}
output.put(name, file)
index.setCoverEntry(name)
}
suspend fun addPage(chapter: MangaChapter, file: File, pageNumber: Int, ext: String) {
val name = buildString {
append(FILENAME_PATTERN.format(chapter.number, pageNumber))
if (ext.isNotEmpty() && ext.length <= 4) {
append('.')
append(ext)
}
}
output.put(name, file)
index.addChapter(chapter)
}
suspend fun finalize() {
output.put(ENTRY_NAME_INDEX, index.toString())
output.finish()
output.close()
file.deleteAwait()
output.file.renameTo(file)
}
suspend fun cleanup() {
output.file.deleteAwait()
}
override fun close() {
output.close()
}
@WorkerThread
private fun mergeWith(other: File) {
var otherIndex: MangaIndex? = null
ZipFile(other).use { zip ->
for (entry in zip.entries()) {
if (entry.name == ENTRY_NAME_INDEX) {
otherIndex = MangaIndex(
zip.getInputStream(entry).use {
it.reader().readText()
}
)
} else {
output.copyEntryFrom(zip, entry)
}
}
}
otherIndex?.getMangaInfo()?.chapters?.let { chapters ->
for (chapter in chapters) {
index.addChapter(chapter)
}
}
}
companion object {
private const val FILENAME_PATTERN = "%03d%03d"
const val ENTRY_NAME_INDEX = "index.json"
fun createNew(root: File, manga: Manga): CbzMangaOutput {
val name = manga.title.toFileNameSafe() + ".cbz"
val file = File(root, name)
return CbzMangaOutput(file, manga)
}
}
}

View File

@@ -14,7 +14,6 @@ import org.koitharu.kotatsu.core.parser.MangaRepository
import org.koitharu.kotatsu.local.data.CbzFilter
import org.koitharu.kotatsu.local.data.LocalStorageManager
import org.koitharu.kotatsu.local.data.MangaIndex
import org.koitharu.kotatsu.local.data.MangaZip
import org.koitharu.kotatsu.parsers.model.*
import org.koitharu.kotatsu.parsers.util.longHashCode
import org.koitharu.kotatsu.parsers.util.toCamelCase
@@ -59,7 +58,7 @@ class LocalMangaRepository(private val storageManager: LocalStorageManager) : Ma
val uri = Uri.parse(chapter.url)
val file = uri.toFile()
val zip = ZipFile(file)
val index = zip.getEntry(MangaZip.INDEX_ENTRY)?.let(zip::readText)?.let(::MangaIndex)
val index = zip.getEntry(CbzMangaOutput.ENTRY_NAME_INDEX)?.let(zip::readText)?.let(::MangaIndex)
var entries = zip.entries().asSequence()
entries = if (index != null) {
val pattern = index.getChapterNamesPattern(chapter)
@@ -97,7 +96,7 @@ class LocalMangaRepository(private val storageManager: LocalStorageManager) : Ma
@SuppressLint("DefaultLocale")
fun getFromFile(file: File): Manga = ZipFile(file).use { zip ->
val fileUri = file.toUri().toString()
val entry = zip.getEntry(MangaZip.INDEX_ENTRY)
val entry = zip.getEntry(CbzMangaOutput.ENTRY_NAME_INDEX)
val index = entry?.let(zip::readText)?.let(::MangaIndex)
val info = index?.getMangaInfo()
if (index != null && info != null) {
@@ -158,7 +157,7 @@ class LocalMangaRepository(private val storageManager: LocalStorageManager) : Ma
}.getOrNull() ?: return null
return runInterruptible(Dispatchers.IO) {
ZipFile(file).use { zip ->
val entry = zip.getEntry(MangaZip.INDEX_ENTRY)
val entry = zip.getEntry(CbzMangaOutput.ENTRY_NAME_INDEX)
val index = entry?.let(zip::readText)?.let(::MangaIndex)
index?.getMangaInfo()
}
@@ -170,7 +169,7 @@ class LocalMangaRepository(private val storageManager: LocalStorageManager) : Ma
return runInterruptible(Dispatchers.IO) {
for (file in files) {
val index = ZipFile(file).use { zip ->
val entry = zip.getEntry(MangaZip.INDEX_ENTRY)
val entry = zip.getEntry(CbzMangaOutput.ENTRY_NAME_INDEX)
entry?.let(zip::readText)?.let(::MangaIndex)
} ?: continue
val info = index.getMangaInfo() ?: continue