Get data from downloaded cbz
parent
5b858edc97
commit
4f02060d50
@ -0,0 +1,9 @@
|
|||||||
|
package org.koitharu.kotatsu.core.local
|
||||||
|
|
||||||
|
import java.io.File
|
||||||
|
import java.io.FilenameFilter
|
||||||
|
|
||||||
|
class CbzFilter : FilenameFilter {
|
||||||
|
|
||||||
|
override fun accept(dir: File, name: String) = name.endsWith(".cbz", ignoreCase = true)
|
||||||
|
}
|
||||||
@ -0,0 +1,94 @@
|
|||||||
|
package org.koitharu.kotatsu.core.parser
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.net.Uri
|
||||||
|
import androidx.core.net.toFile
|
||||||
|
import androidx.core.net.toUri
|
||||||
|
import org.koin.core.inject
|
||||||
|
import org.koitharu.kotatsu.core.local.CbzFilter
|
||||||
|
import org.koitharu.kotatsu.core.model.*
|
||||||
|
import org.koitharu.kotatsu.domain.MangaLoaderContext
|
||||||
|
import org.koitharu.kotatsu.domain.local.MangaIndex
|
||||||
|
import org.koitharu.kotatsu.domain.local.MangaZip
|
||||||
|
import org.koitharu.kotatsu.utils.ext.longHashCode
|
||||||
|
import org.koitharu.kotatsu.utils.ext.readText
|
||||||
|
import org.koitharu.kotatsu.utils.ext.safe
|
||||||
|
import java.io.File
|
||||||
|
import java.util.zip.ZipFile
|
||||||
|
|
||||||
|
class LocalMangaRepository(loaderContext: MangaLoaderContext) : BaseMangaRepository(loaderContext) {
|
||||||
|
|
||||||
|
private val context by loaderContext.inject<Context>()
|
||||||
|
|
||||||
|
override suspend fun getList(
|
||||||
|
offset: Int,
|
||||||
|
query: String?,
|
||||||
|
sortOrder: SortOrder?,
|
||||||
|
tag: MangaTag?
|
||||||
|
): List<Manga> {
|
||||||
|
val files = context.getExternalFilesDirs("manga")
|
||||||
|
.flatMap { x -> x?.listFiles(CbzFilter())?.toList().orEmpty() }
|
||||||
|
return files.mapNotNull { x -> safe { getDetails(x) } }
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun getDetails(manga: Manga) = manga
|
||||||
|
|
||||||
|
override suspend fun getPages(chapter: MangaChapter): List<MangaPage> {
|
||||||
|
val file = Uri.parse(chapter.url).toFile()
|
||||||
|
val zip = ZipFile(file)
|
||||||
|
val pattern = zip.getEntry(MangaZip.INDEX_ENTRY)?.let(zip::readText)?.let(::MangaIndex)
|
||||||
|
?.getChapterNamesPattern(chapter)
|
||||||
|
val entries = if (pattern != null) {
|
||||||
|
zip.entries().asSequence()
|
||||||
|
.filter { x -> !x.isDirectory && x.name.substringBefore('.').matches(pattern) }
|
||||||
|
} else {
|
||||||
|
zip.entries().asSequence().filter { x -> !x.isDirectory }
|
||||||
|
}
|
||||||
|
return entries.map { x ->
|
||||||
|
val uri = zipUri(file, x.name)
|
||||||
|
MangaPage(
|
||||||
|
id = uri.longHashCode(),
|
||||||
|
url = uri,
|
||||||
|
source = MangaSource.LOCAL
|
||||||
|
)
|
||||||
|
}.toList()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getDetails(file: File): Manga {
|
||||||
|
val zip = ZipFile(file)
|
||||||
|
val fileUri = file.toUri().toString()
|
||||||
|
val entry = zip.getEntry(MangaZip.INDEX_ENTRY)
|
||||||
|
val index = entry?.let(zip::readText)?.let(::MangaIndex)
|
||||||
|
return index?.let {
|
||||||
|
it.getMangaInfo()?.let { x ->
|
||||||
|
x.copy(
|
||||||
|
source = MangaSource.LOCAL,
|
||||||
|
url = fileUri,
|
||||||
|
coverUrl = zipUri(file, it.getCoverEntry() ?: zip.entries().nextElement().name),
|
||||||
|
chapters = x.chapters?.map { c -> c.copy(url = fileUri) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} ?: run {
|
||||||
|
val title = file.nameWithoutExtension.replace("_", " ").capitalize()
|
||||||
|
Manga(
|
||||||
|
id = file.absolutePath.longHashCode(),
|
||||||
|
title = title,
|
||||||
|
url = fileUri,
|
||||||
|
source = MangaSource.LOCAL,
|
||||||
|
coverUrl = zipUri(file, zip.entries().nextElement().name),
|
||||||
|
chapters = listOf(
|
||||||
|
MangaChapter(
|
||||||
|
id = file.absolutePath.longHashCode(),
|
||||||
|
url = fileUri,
|
||||||
|
number = 1,
|
||||||
|
source = MangaSource.LOCAL,
|
||||||
|
name = title
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun zipUri(file: File, entryName: String) =
|
||||||
|
Uri.fromParts("zip", file.path, entryName).toString()
|
||||||
|
}
|
||||||
@ -0,0 +1,108 @@
|
|||||||
|
package org.koitharu.kotatsu.domain.local
|
||||||
|
|
||||||
|
import org.json.JSONArray
|
||||||
|
import org.json.JSONObject
|
||||||
|
import org.koitharu.kotatsu.BuildConfig
|
||||||
|
import org.koitharu.kotatsu.core.model.Manga
|
||||||
|
import org.koitharu.kotatsu.core.model.MangaChapter
|
||||||
|
import org.koitharu.kotatsu.core.model.MangaSource
|
||||||
|
import org.koitharu.kotatsu.core.model.MangaTag
|
||||||
|
import org.koitharu.kotatsu.utils.ext.map
|
||||||
|
import org.koitharu.kotatsu.utils.ext.safe
|
||||||
|
|
||||||
|
class MangaIndex(source: String?) {
|
||||||
|
|
||||||
|
private val json: JSONObject = source?.let(::JSONObject) ?: JSONObject()
|
||||||
|
|
||||||
|
fun setMangaInfo(manga: Manga) {
|
||||||
|
json.put("id", manga.id)
|
||||||
|
json.put("title", manga.title)
|
||||||
|
json.put("title_alt", manga.altTitle)
|
||||||
|
json.put("url", manga.url)
|
||||||
|
json.put("cover", manga.coverUrl)
|
||||||
|
json.put("description", manga.description)
|
||||||
|
json.put("rating", manga.rating)
|
||||||
|
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("chapters", JSONObject())
|
||||||
|
json.put("app_id", BuildConfig.APPLICATION_ID)
|
||||||
|
json.put("app_version", BuildConfig.VERSION_CODE)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getMangaInfo(): Manga? = if (json.length() == 0) null else safe {
|
||||||
|
val source = MangaSource.valueOf(json.getString("source"))
|
||||||
|
Manga(
|
||||||
|
id = json.getLong("id"),
|
||||||
|
title = json.getString("title"),
|
||||||
|
altTitle = json.getString("title_alt"),
|
||||||
|
url = json.getString("url"),
|
||||||
|
source = source,
|
||||||
|
rating = json.getDouble("rating").toFloat(),
|
||||||
|
coverUrl = json.getString("cover"),
|
||||||
|
description = json.getString("description"),
|
||||||
|
tags = json.getJSONArray("tags").map { x ->
|
||||||
|
MangaTag(
|
||||||
|
title = x.getString("title"),
|
||||||
|
key = x.getString("key"),
|
||||||
|
source = source
|
||||||
|
)
|
||||||
|
}.toSet(),
|
||||||
|
chapters = getChapters(json.getJSONObject("chapters"), source)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getCoverEntry(): String? = json.optString("cover_entry")
|
||||||
|
|
||||||
|
fun addChapter(chapter: MangaChapter) {
|
||||||
|
val chapters = json.getJSONObject("chapters")
|
||||||
|
if (!chapters.has(chapter.id.toString())) {
|
||||||
|
val jo = JSONObject()
|
||||||
|
jo.put("number", chapter.number)
|
||||||
|
jo.put("url", chapter.url)
|
||||||
|
jo.put("name", chapter.name)
|
||||||
|
jo.put("entries", "%03d\\d{3}".format(chapter.number))
|
||||||
|
chapters.put(chapter.number.toString(), jo)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setCoverEntry(name: String) {
|
||||||
|
json.put("cover_entry", name)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getChapterNamesPattern(chapter: MangaChapter) = Regex(
|
||||||
|
json.getJSONObject("chapters")
|
||||||
|
.getJSONObject(chapter.id.toString())
|
||||||
|
.getString("entries")
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun getChapters(json: JSONObject, source: MangaSource): List<MangaChapter> {
|
||||||
|
val chapters = ArrayList<MangaChapter>(json.length())
|
||||||
|
for (k in json.keys()) {
|
||||||
|
val v = json.getJSONObject(k)
|
||||||
|
chapters.add(
|
||||||
|
MangaChapter(
|
||||||
|
id = k.toLong(),
|
||||||
|
name = v.getString("name"),
|
||||||
|
url = v.getString("url"),
|
||||||
|
number = v.getInt("number"),
|
||||||
|
source = source
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return chapters.sortedBy { it.number }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun toString(): String = if (BuildConfig.DEBUG) {
|
||||||
|
json.toString(4)
|
||||||
|
} else {
|
||||||
|
json.toString()
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,32 @@
|
|||||||
|
package org.koitharu.kotatsu.ui.main.list.local
|
||||||
|
|
||||||
|
import kotlinx.android.synthetic.main.fragment_list.*
|
||||||
|
import moxy.ktx.moxyPresenter
|
||||||
|
import org.koitharu.kotatsu.R
|
||||||
|
import org.koitharu.kotatsu.ui.main.list.MangaListFragment
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
class LocalListFragment : MangaListFragment<File>() {
|
||||||
|
|
||||||
|
private val presenter by moxyPresenter(factory = ::LocalListPresenter)
|
||||||
|
|
||||||
|
override fun onRequestMoreItems(offset: Int) {
|
||||||
|
if (offset == 0) {
|
||||||
|
presenter.loadList()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getTitle(): CharSequence? {
|
||||||
|
return getString(R.string.local_storage)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setUpEmptyListHolder() {
|
||||||
|
textView_holder.setText(R.string.no_saved_manga)
|
||||||
|
textView_holder.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, 0, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
fun newInstance() = LocalListFragment()
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,43 @@
|
|||||||
|
package org.koitharu.kotatsu.ui.main.list.local
|
||||||
|
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import moxy.InjectViewState
|
||||||
|
import org.koitharu.kotatsu.BuildConfig
|
||||||
|
import org.koitharu.kotatsu.core.model.MangaSource
|
||||||
|
import org.koitharu.kotatsu.core.parser.LocalMangaRepository
|
||||||
|
import org.koitharu.kotatsu.domain.MangaProviderFactory
|
||||||
|
import org.koitharu.kotatsu.ui.common.BasePresenter
|
||||||
|
import org.koitharu.kotatsu.ui.main.list.MangaListView
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
@InjectViewState
|
||||||
|
class LocalListPresenter : BasePresenter<MangaListView<File>>() {
|
||||||
|
|
||||||
|
private lateinit var repository: LocalMangaRepository
|
||||||
|
|
||||||
|
override fun onFirstViewAttach() {
|
||||||
|
repository = MangaProviderFactory.create(MangaSource.LOCAL) as LocalMangaRepository
|
||||||
|
super.onFirstViewAttach()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun loadList() {
|
||||||
|
launch {
|
||||||
|
viewState.onLoadingChanged(true)
|
||||||
|
try {
|
||||||
|
val list = withContext(Dispatchers.IO) {
|
||||||
|
repository.getList(0)
|
||||||
|
}
|
||||||
|
viewState.onListChanged(list)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
if (BuildConfig.DEBUG) {
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
|
viewState.onError(e)
|
||||||
|
} finally {
|
||||||
|
viewState.onLoadingChanged(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,7 +1,13 @@
|
|||||||
package org.koitharu.kotatsu.utils.ext
|
package org.koitharu.kotatsu.utils.ext
|
||||||
|
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
import java.util.zip.ZipEntry
|
||||||
|
import java.util.zip.ZipFile
|
||||||
|
|
||||||
fun File.sub(name: String) = File(this, name)
|
fun File.sub(name: String) = File(this, name)
|
||||||
|
|
||||||
fun File.takeIfReadable() = takeIf { it.exists() && it.canRead() }
|
fun File.takeIfReadable() = takeIf { it.exists() && it.canRead() }
|
||||||
|
|
||||||
|
fun ZipFile.readText(entry: ZipEntry) = getInputStream(entry).bufferedReader().use {
|
||||||
|
it.readText()
|
||||||
|
}
|
||||||
@ -0,0 +1,14 @@
|
|||||||
|
package org.koitharu.kotatsu.utils.ext
|
||||||
|
|
||||||
|
import org.json.JSONArray
|
||||||
|
import org.json.JSONObject
|
||||||
|
|
||||||
|
fun <T> JSONArray.map(block: (JSONObject) -> T): List<T> {
|
||||||
|
val len = length()
|
||||||
|
val result = ArrayList<T>(len)
|
||||||
|
for(i in 0 until len) {
|
||||||
|
val jo = getJSONObject(i)
|
||||||
|
result.add(block(jo))
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<paths>
|
||||||
|
<external-files-path
|
||||||
|
name="manga"
|
||||||
|
path="/manga" />
|
||||||
|
</paths>
|
||||||
Loading…
Reference in New Issue