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
|
||||
|
||||
import java.io.File
|
||||
import java.util.zip.ZipEntry
|
||||
import java.util.zip.ZipFile
|
||||
|
||||
fun File.sub(name: String) = File(this, name)
|
||||
|
||||
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