|
|
|
|
@ -12,13 +12,15 @@ import org.koitharu.kotatsu.BuildConfig
|
|
|
|
|
import org.koitharu.kotatsu.core.model.MangaSource
|
|
|
|
|
import org.koitharu.kotatsu.core.model.isLocal
|
|
|
|
|
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
|
|
|
|
|
import org.koitharu.kotatsu.parsers.model.ContentRating
|
|
|
|
|
import org.koitharu.kotatsu.parsers.model.Manga
|
|
|
|
|
import org.koitharu.kotatsu.parsers.model.MangaChapter
|
|
|
|
|
import org.koitharu.kotatsu.parsers.model.MangaSource
|
|
|
|
|
import org.koitharu.kotatsu.parsers.model.MangaState
|
|
|
|
|
import org.koitharu.kotatsu.parsers.model.MangaTag
|
|
|
|
|
import org.koitharu.kotatsu.parsers.util.find
|
|
|
|
|
import org.koitharu.kotatsu.parsers.model.RATING_UNKNOWN
|
|
|
|
|
import org.koitharu.kotatsu.parsers.util.json.getBooleanOrDefault
|
|
|
|
|
import org.koitharu.kotatsu.parsers.util.json.getEnumValueOrNull
|
|
|
|
|
import org.koitharu.kotatsu.parsers.util.json.getFloatOrDefault
|
|
|
|
|
import org.koitharu.kotatsu.parsers.util.json.getIntOrDefault
|
|
|
|
|
import org.koitharu.kotatsu.parsers.util.json.getLongOrDefault
|
|
|
|
|
@ -34,120 +36,120 @@ class MangaIndex(source: String?) {
|
|
|
|
|
|
|
|
|
|
fun setMangaInfo(manga: Manga) {
|
|
|
|
|
require(!manga.isLocal) { "Local manga information cannot be stored" }
|
|
|
|
|
json.put("id", manga.id)
|
|
|
|
|
json.put("title", manga.title)
|
|
|
|
|
json.put("title_alt", manga.altTitle)
|
|
|
|
|
json.put("url", manga.url)
|
|
|
|
|
json.put("public_url", manga.publicUrl)
|
|
|
|
|
json.put("author", manga.author)
|
|
|
|
|
json.put("cover", manga.coverUrl)
|
|
|
|
|
json.put("description", manga.description)
|
|
|
|
|
json.put("rating", manga.rating)
|
|
|
|
|
json.put("nsfw", manga.isNsfw)
|
|
|
|
|
json.put("state", manga.state?.name)
|
|
|
|
|
json.put("source", manga.source.name)
|
|
|
|
|
json.put("cover_large", manga.largeCoverUrl)
|
|
|
|
|
json.put(KEY_ID, manga.id)
|
|
|
|
|
json.put(KEY_TITLE, manga.title)
|
|
|
|
|
json.put(KEY_TITLE_ALT, manga.altTitle)
|
|
|
|
|
json.put(KEY_URL, manga.url)
|
|
|
|
|
json.put(KEY_PUBLIC_URL, manga.publicUrl)
|
|
|
|
|
json.put(KEY_AUTHOR, manga.author)
|
|
|
|
|
json.put(KEY_COVER, manga.coverUrl)
|
|
|
|
|
json.put(KEY_DESCRIPTION, manga.description)
|
|
|
|
|
json.put(KEY_RATING, manga.rating)
|
|
|
|
|
json.put(KEY_CONTENT_RATING, manga.contentRating)
|
|
|
|
|
json.put(KEY_NSFW, manga.isNsfw) // for backward compatibility
|
|
|
|
|
json.put(KEY_STATE, manga.state?.name)
|
|
|
|
|
json.put(KEY_SOURCE, manga.source.name)
|
|
|
|
|
json.put(KEY_COVER_LARGE, manga.largeCoverUrl)
|
|
|
|
|
json.put(
|
|
|
|
|
"tags",
|
|
|
|
|
KEY_TAGS,
|
|
|
|
|
JSONArray().also { a ->
|
|
|
|
|
for (tag in manga.tags) {
|
|
|
|
|
val jo = JSONObject()
|
|
|
|
|
jo.put("key", tag.key)
|
|
|
|
|
jo.put("title", tag.title)
|
|
|
|
|
jo.put(KEY_KEY, tag.key)
|
|
|
|
|
jo.put(KEY_TITLE, tag.title)
|
|
|
|
|
a.put(jo)
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
if (!json.has("chapters")) {
|
|
|
|
|
json.put("chapters", JSONObject())
|
|
|
|
|
if (!json.has(KEY_CHAPTERS)) {
|
|
|
|
|
json.put(KEY_CHAPTERS, JSONObject())
|
|
|
|
|
}
|
|
|
|
|
json.put("app_id", BuildConfig.APPLICATION_ID)
|
|
|
|
|
json.put("app_version", BuildConfig.VERSION_CODE)
|
|
|
|
|
json.put(KEY_APP_ID, BuildConfig.APPLICATION_ID)
|
|
|
|
|
json.put(KEY_APP_VERSION, BuildConfig.VERSION_CODE)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fun getMangaInfo(): Manga? = if (json.length() == 0) null else runCatching {
|
|
|
|
|
val source = MangaSource(json.getString("source"))
|
|
|
|
|
val source = MangaSource(json.getString(KEY_SOURCE))
|
|
|
|
|
Manga(
|
|
|
|
|
id = json.getLong("id"),
|
|
|
|
|
title = json.getString("title"),
|
|
|
|
|
altTitle = json.getStringOrNull("title_alt"),
|
|
|
|
|
url = json.getString("url"),
|
|
|
|
|
publicUrl = json.getStringOrNull("public_url").orEmpty(),
|
|
|
|
|
author = json.getStringOrNull("author"),
|
|
|
|
|
largeCoverUrl = json.getStringOrNull("cover_large"),
|
|
|
|
|
id = json.getLong(KEY_ID),
|
|
|
|
|
title = json.getString(KEY_TITLE),
|
|
|
|
|
altTitle = json.getStringOrNull(KEY_TITLE_ALT),
|
|
|
|
|
url = json.getString(KEY_URL),
|
|
|
|
|
publicUrl = json.getStringOrNull(KEY_PUBLIC_URL).orEmpty(),
|
|
|
|
|
author = json.getStringOrNull(KEY_AUTHOR),
|
|
|
|
|
largeCoverUrl = json.getStringOrNull(KEY_COVER_LARGE),
|
|
|
|
|
source = source,
|
|
|
|
|
rating = json.getDouble("rating").toFloat(),
|
|
|
|
|
isNsfw = json.getBooleanOrDefault("nsfw", false),
|
|
|
|
|
coverUrl = json.getString("cover"),
|
|
|
|
|
state = json.getStringOrNull("state")?.let { stateString ->
|
|
|
|
|
MangaState.entries.find(stateString)
|
|
|
|
|
},
|
|
|
|
|
description = json.getStringOrNull("description"),
|
|
|
|
|
tags = json.getJSONArray("tags").mapJSONToSet { x ->
|
|
|
|
|
rating = json.getFloatOrDefault(KEY_RATING, RATING_UNKNOWN),
|
|
|
|
|
contentRating = json.getEnumValueOrNull(KEY_CONTENT_RATING, ContentRating::class.java)
|
|
|
|
|
?: if (json.getBooleanOrDefault(KEY_NSFW, false)) ContentRating.ADULT else null,
|
|
|
|
|
coverUrl = json.getStringOrNull(KEY_COVER),
|
|
|
|
|
state = json.getEnumValueOrNull(KEY_STATE, MangaState::class.java),
|
|
|
|
|
description = json.getStringOrNull(KEY_DESCRIPTION),
|
|
|
|
|
tags = json.getJSONArray(KEY_TAGS).mapJSONToSet { x ->
|
|
|
|
|
MangaTag(
|
|
|
|
|
title = x.getString("title").toTitleCase(),
|
|
|
|
|
key = x.getString("key"),
|
|
|
|
|
title = x.getString(KEY_TITLE).toTitleCase(),
|
|
|
|
|
key = x.getString(KEY_KEY),
|
|
|
|
|
source = source,
|
|
|
|
|
)
|
|
|
|
|
},
|
|
|
|
|
chapters = getChapters(json.getJSONObject("chapters"), source),
|
|
|
|
|
chapters = getChapters(json.getJSONObject(KEY_CHAPTERS), source),
|
|
|
|
|
)
|
|
|
|
|
}.getOrNull()
|
|
|
|
|
|
|
|
|
|
fun getCoverEntry(): String? = json.getStringOrNull("cover_entry")
|
|
|
|
|
fun getCoverEntry(): String? = json.getStringOrNull(KEY_COVER_ENTRY)
|
|
|
|
|
|
|
|
|
|
fun addChapter(chapter: IndexedValue<MangaChapter>, filename: String?) {
|
|
|
|
|
val chapters = json.getJSONObject("chapters")
|
|
|
|
|
val chapters = json.getJSONObject(KEY_CHAPTERS)
|
|
|
|
|
if (!chapters.has(chapter.value.id.toString())) {
|
|
|
|
|
val jo = JSONObject()
|
|
|
|
|
jo.put("number", chapter.value.number)
|
|
|
|
|
jo.put("volume", chapter.value.volume)
|
|
|
|
|
jo.put("url", chapter.value.url)
|
|
|
|
|
jo.put("name", chapter.value.name)
|
|
|
|
|
jo.put("uploadDate", chapter.value.uploadDate)
|
|
|
|
|
jo.put("scanlator", chapter.value.scanlator)
|
|
|
|
|
jo.put("branch", chapter.value.branch)
|
|
|
|
|
jo.put("entries", "%08d_%03d\\d{3}".format(chapter.value.branch.hashCode(), chapter.index + 1))
|
|
|
|
|
jo.put("file", filename)
|
|
|
|
|
jo.put(KEY_NUMBER, chapter.value.number)
|
|
|
|
|
jo.put(KEY_VOLUME, chapter.value.volume)
|
|
|
|
|
jo.put(KEY_URL, chapter.value.url)
|
|
|
|
|
jo.put(KEY_NAME, chapter.value.name)
|
|
|
|
|
jo.put(KEY_UPLOAD_DATE, chapter.value.uploadDate)
|
|
|
|
|
jo.put(KEY_SCANLATOR, chapter.value.scanlator)
|
|
|
|
|
jo.put(KEY_BRANCH, chapter.value.branch)
|
|
|
|
|
jo.put(KEY_ENTRIES, "%08d_%03d\\d{3}".format(chapter.value.branch.hashCode(), chapter.index + 1))
|
|
|
|
|
jo.put(KEY_FILE, filename)
|
|
|
|
|
chapters.put(chapter.value.id.toString(), jo)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fun removeChapter(id: Long): Boolean {
|
|
|
|
|
return json.has("chapters") && json.getJSONObject("chapters").remove(id.toString()) != null
|
|
|
|
|
return json.has(KEY_CHAPTERS) && json.getJSONObject(KEY_CHAPTERS).remove(id.toString()) != null
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fun getChapterFileName(chapterId: Long): String? {
|
|
|
|
|
return json.optJSONObject("chapters")?.optJSONObject(chapterId.toString())?.getStringOrNull("file")
|
|
|
|
|
return json.optJSONObject(KEY_CHAPTERS)?.optJSONObject(chapterId.toString())?.getStringOrNull(KEY_FILE)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fun setCoverEntry(name: String) {
|
|
|
|
|
json.put("cover_entry", name)
|
|
|
|
|
json.put(KEY_COVER_ENTRY, name)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fun getChapterNamesPattern(chapter: MangaChapter) = Regex(
|
|
|
|
|
json.getJSONObject("chapters")
|
|
|
|
|
json.getJSONObject(KEY_CHAPTERS)
|
|
|
|
|
.getJSONObject(chapter.id.toString())
|
|
|
|
|
.getString("entries"),
|
|
|
|
|
.getString(KEY_ENTRIES),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
fun sortChaptersByName() {
|
|
|
|
|
val jo = json.getJSONObject("chapters")
|
|
|
|
|
val jo = json.getJSONObject(KEY_CHAPTERS)
|
|
|
|
|
val list = ArrayList<JSONObject>(jo.length())
|
|
|
|
|
jo.keys().forEach { id ->
|
|
|
|
|
val item = jo.getJSONObject(id)
|
|
|
|
|
item.put("id", id)
|
|
|
|
|
item.put(KEY_ID, id)
|
|
|
|
|
list.add(item)
|
|
|
|
|
}
|
|
|
|
|
val comparator = org.koitharu.kotatsu.core.util.AlphanumComparator()
|
|
|
|
|
list.sortWith(compareBy(comparator) { it.getString("name") })
|
|
|
|
|
list.sortWith(compareBy(comparator) { it.getString(KEY_NAME) })
|
|
|
|
|
val newJo = JSONObject()
|
|
|
|
|
list.forEachIndexed { i, obj ->
|
|
|
|
|
obj.put("number", i + 1)
|
|
|
|
|
val id = obj.remove("id") as String
|
|
|
|
|
obj.put(KEY_NUMBER, i + 1)
|
|
|
|
|
val id = obj.remove(KEY_ID) as String
|
|
|
|
|
newJo.put(id, obj)
|
|
|
|
|
}
|
|
|
|
|
json.put("chapters", newJo)
|
|
|
|
|
json.put(KEY_CHAPTERS, newJo)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fun clear() {
|
|
|
|
|
@ -171,13 +173,13 @@ class MangaIndex(source: String?) {
|
|
|
|
|
chapters.add(
|
|
|
|
|
MangaChapter(
|
|
|
|
|
id = k.toLong(),
|
|
|
|
|
name = v.getString("name"),
|
|
|
|
|
url = v.getString("url"),
|
|
|
|
|
number = v.getFloatOrDefault("number", 0f),
|
|
|
|
|
volume = v.getIntOrDefault("volume", 0),
|
|
|
|
|
uploadDate = v.getLongOrDefault("uploadDate", 0L),
|
|
|
|
|
scanlator = v.getStringOrNull("scanlator"),
|
|
|
|
|
branch = v.getStringOrNull("branch"),
|
|
|
|
|
name = v.getString(KEY_NAME),
|
|
|
|
|
url = v.getString(KEY_URL),
|
|
|
|
|
number = v.getFloatOrDefault(KEY_NUMBER, 0f),
|
|
|
|
|
volume = v.getIntOrDefault(KEY_VOLUME, 0),
|
|
|
|
|
uploadDate = v.getLongOrDefault(KEY_UPLOAD_DATE, 0L),
|
|
|
|
|
scanlator = v.getStringOrNull(KEY_SCANLATOR),
|
|
|
|
|
branch = v.getStringOrNull(KEY_BRANCH),
|
|
|
|
|
source = source,
|
|
|
|
|
),
|
|
|
|
|
)
|
|
|
|
|
@ -193,6 +195,35 @@ class MangaIndex(source: String?) {
|
|
|
|
|
|
|
|
|
|
companion object {
|
|
|
|
|
|
|
|
|
|
private const val KEY_ID = "id"
|
|
|
|
|
private const val KEY_TITLE = "title"
|
|
|
|
|
private const val KEY_TITLE_ALT = "title_alt"
|
|
|
|
|
private const val KEY_URL = "url"
|
|
|
|
|
private const val KEY_PUBLIC_URL = "public_url"
|
|
|
|
|
private const val KEY_AUTHOR = "author"
|
|
|
|
|
private const val KEY_COVER = "cover"
|
|
|
|
|
private const val KEY_DESCRIPTION = "description"
|
|
|
|
|
private const val KEY_RATING = "rating"
|
|
|
|
|
private const val KEY_CONTENT_RATING = "content_rating"
|
|
|
|
|
private const val KEY_NSFW = "nsfw"
|
|
|
|
|
private const val KEY_STATE = "state"
|
|
|
|
|
private const val KEY_SOURCE = "source"
|
|
|
|
|
private const val KEY_COVER_LARGE = "cover_large"
|
|
|
|
|
private const val KEY_TAGS = "tags"
|
|
|
|
|
private const val KEY_CHAPTERS = "chapters"
|
|
|
|
|
private const val KEY_NUMBER = "number"
|
|
|
|
|
private const val KEY_VOLUME = "volume"
|
|
|
|
|
private const val KEY_NAME = "name"
|
|
|
|
|
private const val KEY_UPLOAD_DATE = "uploadDate"
|
|
|
|
|
private const val KEY_SCANLATOR = "scanlator"
|
|
|
|
|
private const val KEY_BRANCH = "branch"
|
|
|
|
|
private const val KEY_ENTRIES = "entries"
|
|
|
|
|
private const val KEY_FILE = "file"
|
|
|
|
|
private const val KEY_COVER_ENTRY = "cover_entry"
|
|
|
|
|
private const val KEY_KEY = "key"
|
|
|
|
|
private const val KEY_APP_ID = "app_id"
|
|
|
|
|
private const val KEY_APP_VERSION = "app_version"
|
|
|
|
|
|
|
|
|
|
@Blocking
|
|
|
|
|
@WorkerThread
|
|
|
|
|
fun read(fileSystem: FileSystem, path: Path): MangaIndex? = runCatchingCancellable {
|
|
|
|
|
|