|
|
|
|
@ -1,10 +1,17 @@
|
|
|
|
|
package org.koitharu.kotatsu.parsers.site.all
|
|
|
|
|
|
|
|
|
|
import kotlinx.coroutines.*
|
|
|
|
|
import kotlinx.coroutines.sync.*
|
|
|
|
|
import androidx.collection.ArraySet
|
|
|
|
|
import kotlinx.coroutines.async
|
|
|
|
|
import kotlinx.coroutines.awaitAll
|
|
|
|
|
import kotlinx.coroutines.coroutineScope
|
|
|
|
|
import kotlinx.coroutines.sync.Mutex
|
|
|
|
|
import kotlinx.coroutines.sync.withLock
|
|
|
|
|
import okhttp3.Headers
|
|
|
|
|
import org.json.*
|
|
|
|
|
import org.koitharu.kotatsu.parsers.*
|
|
|
|
|
import org.json.JSONArray
|
|
|
|
|
import org.json.JSONObject
|
|
|
|
|
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
|
|
|
|
import org.koitharu.kotatsu.parsers.MangaParser
|
|
|
|
|
import org.koitharu.kotatsu.parsers.MangaSourceParser
|
|
|
|
|
import org.koitharu.kotatsu.parsers.config.ConfigKey
|
|
|
|
|
import org.koitharu.kotatsu.parsers.model.*
|
|
|
|
|
import org.koitharu.kotatsu.parsers.util.*
|
|
|
|
|
@ -24,14 +31,12 @@ class HitomiLaParser(context: MangaLoaderContext) : MangaParser(context, MangaSo
|
|
|
|
|
|
|
|
|
|
private val ltnBaseUrl get() = "https://${getDomain("ltn")}"
|
|
|
|
|
|
|
|
|
|
override val availableSortOrders: Set<SortOrder> =
|
|
|
|
|
EnumSet.of(
|
|
|
|
|
override val availableSortOrders: Set<SortOrder> = EnumSet.of(
|
|
|
|
|
SortOrder.NEWEST,
|
|
|
|
|
SortOrder.POPULARITY,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
private val localeMap: Map<Locale, String> =
|
|
|
|
|
mapOf(
|
|
|
|
|
private val localeMap: Map<Locale, String> = mapOf(
|
|
|
|
|
Locale("id") to "indonesian",
|
|
|
|
|
Locale("jv") to "javanese",
|
|
|
|
|
Locale("ca") to "catalan",
|
|
|
|
|
@ -59,19 +64,14 @@ class HitomiLaParser(context: MangaLoaderContext) : MangaParser(context, MangaSo
|
|
|
|
|
Locale.JAPANESE to "japanese",
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
private fun Locale?.getSiteLang(): String {
|
|
|
|
|
return when (this) {
|
|
|
|
|
private fun Locale?.getSiteLang(): String = when (this) {
|
|
|
|
|
null -> "all"
|
|
|
|
|
else -> localeMap[this] ?: "all"
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
override suspend fun getAvailableLocales(): Set<Locale> {
|
|
|
|
|
return localeMap.keys
|
|
|
|
|
}
|
|
|
|
|
override suspend fun getAvailableLocales(): Set<Locale> = localeMap.keys
|
|
|
|
|
|
|
|
|
|
override suspend fun getAvailableTags(): Set<MangaTag> {
|
|
|
|
|
return coroutineScope {
|
|
|
|
|
override suspend fun getAvailableTags(): Set<MangaTag> = coroutineScope {
|
|
|
|
|
('a'..'z').map { alphabet ->
|
|
|
|
|
async {
|
|
|
|
|
val doc = webClient.httpGet("https://$domain/alltags-$alphabet.html").parseHtml()
|
|
|
|
|
@ -100,20 +100,23 @@ class HitomiLaParser(context: MangaLoaderContext) : MangaParser(context, MangaSo
|
|
|
|
|
}
|
|
|
|
|
}.awaitAll().flatten().toSet()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private var cachedSearchIds: List<Int> = emptyList()
|
|
|
|
|
|
|
|
|
|
override suspend fun getList(
|
|
|
|
|
offset: Int,
|
|
|
|
|
filter: MangaListFilter?,
|
|
|
|
|
): List<Manga> {
|
|
|
|
|
return when (filter) {
|
|
|
|
|
): List<Manga> = when (filter) {
|
|
|
|
|
is MangaListFilter.Advanced -> {
|
|
|
|
|
if (filter.tags.isEmpty()) {
|
|
|
|
|
when (filter.sortOrder) {
|
|
|
|
|
SortOrder.POPULARITY -> {
|
|
|
|
|
getGalleryIDsFromNozomi("popular", "today", filter.locale.getSiteLang(), offset.nextOffsetRange())
|
|
|
|
|
getGalleryIDsFromNozomi(
|
|
|
|
|
"popular",
|
|
|
|
|
"today",
|
|
|
|
|
filter.locale.getSiteLang(),
|
|
|
|
|
offset.nextOffsetRange(),
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
else -> {
|
|
|
|
|
@ -142,7 +145,6 @@ class HitomiLaParser(context: MangaLoaderContext) : MangaParser(context, MangaSo
|
|
|
|
|
|
|
|
|
|
else -> getGalleryIDsFromNozomi(null, "popular", "all", offset.nextOffsetRange())
|
|
|
|
|
}.toMangaList()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private fun Int.nextOffsetRange(): LongRange {
|
|
|
|
|
val bytes = this * 4L
|
|
|
|
|
@ -243,6 +245,7 @@ class HitomiLaParser(context: MangaLoaderContext) : MangaParser(context, MangaSo
|
|
|
|
|
area = "tag"
|
|
|
|
|
tag = it
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
"language" -> {
|
|
|
|
|
area = null
|
|
|
|
|
lang = tag
|
|
|
|
|
@ -255,9 +258,7 @@ class HitomiLaParser(context: MangaLoaderContext) : MangaParser(context, MangaSo
|
|
|
|
|
|
|
|
|
|
val key = hashTerm(it)
|
|
|
|
|
val node = getGalleryNodeAtAddress(0)
|
|
|
|
|
val data =
|
|
|
|
|
bSearch(key, node)
|
|
|
|
|
?: return emptySet()
|
|
|
|
|
val data = bSearch(key, node) ?: return emptySet()
|
|
|
|
|
|
|
|
|
|
return getGalleryIDsFromData(data)
|
|
|
|
|
}
|
|
|
|
|
@ -266,7 +267,7 @@ class HitomiLaParser(context: MangaLoaderContext) : MangaParser(context, MangaSo
|
|
|
|
|
private suspend fun getGalleryIDsFromData(data: Pair<Long, Int>): Set<Int> {
|
|
|
|
|
val url = "$ltnBaseUrl/galleriesindex/galleries.${galleriesIndexVersion.get()}.data"
|
|
|
|
|
val (offset, length) = data
|
|
|
|
|
require(length in 0..100000000) {
|
|
|
|
|
require(length in 1..100000000) {
|
|
|
|
|
"Length $length is too long"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@ -283,10 +284,11 @@ class HitomiLaParser(context: MangaLoaderContext) : MangaParser(context, MangaSo
|
|
|
|
|
|
|
|
|
|
val expectedLength = numberOfGalleryIDs * 4 + 4
|
|
|
|
|
|
|
|
|
|
if (numberOfGalleryIDs > 10000000 || numberOfGalleryIDs <= 0) {
|
|
|
|
|
throw IllegalArgumentException("number_of_galleryids $numberOfGalleryIDs is too long")
|
|
|
|
|
} else if (inbuf.size != expectedLength) {
|
|
|
|
|
throw IllegalArgumentException("inbuf.byteLength ${inbuf.size} != expected_length $expectedLength")
|
|
|
|
|
require(numberOfGalleryIDs in 1..10000000) {
|
|
|
|
|
"number_of_galleryids $numberOfGalleryIDs is too long"
|
|
|
|
|
}
|
|
|
|
|
require(inbuf.size == expectedLength) {
|
|
|
|
|
"inbuf.byteLength ${inbuf.size} != expected_length $expectedLength"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (i in 0.until(numberOfGalleryIDs))
|
|
|
|
|
@ -316,7 +318,7 @@ class HitomiLaParser(context: MangaLoaderContext) : MangaParser(context, MangaSo
|
|
|
|
|
return 0
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private fun locateKey(
|
|
|
|
|
fun locateKey(
|
|
|
|
|
key: UByteArray,
|
|
|
|
|
node: Node,
|
|
|
|
|
): Pair<Boolean, Int> {
|
|
|
|
|
@ -331,7 +333,7 @@ class HitomiLaParser(context: MangaLoaderContext) : MangaParser(context, MangaSo
|
|
|
|
|
return Pair(false, node.keys.size)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private fun isLeaf(node: Node): Boolean {
|
|
|
|
|
fun isLeaf(node: Node): Boolean {
|
|
|
|
|
for (subnode in node.subNodeAddresses)
|
|
|
|
|
if (subnode != 0L) {
|
|
|
|
|
return false
|
|
|
|
|
@ -361,8 +363,7 @@ class HitomiLaParser(context: MangaLoaderContext) : MangaParser(context, MangaSo
|
|
|
|
|
language: String,
|
|
|
|
|
range: LongRange? = null,
|
|
|
|
|
): Set<Int> {
|
|
|
|
|
val nozomiAddress =
|
|
|
|
|
when (area) {
|
|
|
|
|
val nozomiAddress = when (area) {
|
|
|
|
|
null -> "$ltnBaseUrl/$tag-$language.nozomi"
|
|
|
|
|
else -> "$ltnBaseUrl/$area/$tag-$language.nozomi"
|
|
|
|
|
}
|
|
|
|
|
@ -370,8 +371,7 @@ class HitomiLaParser(context: MangaLoaderContext) : MangaParser(context, MangaSo
|
|
|
|
|
val bytes = getRangedResponse(nozomiAddress, range)
|
|
|
|
|
val nozomi = mutableSetOf<Int>()
|
|
|
|
|
|
|
|
|
|
val arrayBuffer =
|
|
|
|
|
ByteBuffer
|
|
|
|
|
val arrayBuffer = ByteBuffer
|
|
|
|
|
.wrap(bytes)
|
|
|
|
|
.order(ByteOrder.BIG_ENDIAN)
|
|
|
|
|
|
|
|
|
|
@ -381,8 +381,7 @@ class HitomiLaParser(context: MangaLoaderContext) : MangaParser(context, MangaSo
|
|
|
|
|
return nozomi
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private val galleriesIndexVersion =
|
|
|
|
|
SuspendLazy {
|
|
|
|
|
private val galleriesIndexVersion = SuspendLazy {
|
|
|
|
|
webClient.httpGet("$ltnBaseUrl/galleriesindex/version?_=${System.currentTimeMillis()}").parseRaw()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@ -393,8 +392,7 @@ class HitomiLaParser(context: MangaLoaderContext) : MangaParser(context, MangaSo
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
private fun decodeNode(data: ByteArray): Node {
|
|
|
|
|
val buffer =
|
|
|
|
|
ByteBuffer
|
|
|
|
|
val buffer = ByteBuffer
|
|
|
|
|
.wrap(data)
|
|
|
|
|
.order(ByteOrder.BIG_ENDIAN)
|
|
|
|
|
|
|
|
|
|
@ -447,8 +445,7 @@ class HitomiLaParser(context: MangaLoaderContext) : MangaParser(context, MangaSo
|
|
|
|
|
url: String,
|
|
|
|
|
range: LongRange? = null,
|
|
|
|
|
): ByteArray {
|
|
|
|
|
val rangeHeaders =
|
|
|
|
|
when (range) {
|
|
|
|
|
val rangeHeaders = when (range) {
|
|
|
|
|
null -> Headers.headersOf()
|
|
|
|
|
else -> Headers.headersOf("Range", "bytes=${range.first}-${range.last}")
|
|
|
|
|
}
|
|
|
|
|
@ -464,8 +461,7 @@ class HitomiLaParser(context: MangaLoaderContext) : MangaParser(context, MangaSo
|
|
|
|
|
return MessageDigest.getInstance("SHA-256").digest(data)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private suspend fun Collection<Int>.toMangaList(): List<Manga> {
|
|
|
|
|
return coroutineScope {
|
|
|
|
|
private suspend fun Collection<Int>.toMangaList(): List<Manga> = coroutineScope {
|
|
|
|
|
map { id ->
|
|
|
|
|
async {
|
|
|
|
|
runCatching {
|
|
|
|
|
@ -496,11 +492,9 @@ class HitomiLaParser(context: MangaLoaderContext) : MangaParser(context, MangaSo
|
|
|
|
|
}
|
|
|
|
|
}.awaitAll().filterNotNull()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
override suspend fun getDetails(manga: Manga): Manga {
|
|
|
|
|
val json =
|
|
|
|
|
webClient.httpGet("$ltnBaseUrl/galleries/${manga.url}.js")
|
|
|
|
|
val json = webClient.httpGet("$ltnBaseUrl/galleries/${manga.url}.js")
|
|
|
|
|
.parseRaw()
|
|
|
|
|
.substringAfter("var galleryinfo = ")
|
|
|
|
|
.let(::JSONObject)
|
|
|
|
|
@ -539,8 +533,7 @@ class HitomiLaParser(context: MangaLoaderContext) : MangaParser(context, MangaSo
|
|
|
|
|
?.mapToTags("group")
|
|
|
|
|
?.let(::addAll)
|
|
|
|
|
},
|
|
|
|
|
chapters =
|
|
|
|
|
listOf(
|
|
|
|
|
chapters = listOf(
|
|
|
|
|
MangaChapter(
|
|
|
|
|
id = generateUid(manga.url),
|
|
|
|
|
url = manga.url,
|
|
|
|
|
@ -595,8 +588,7 @@ class HitomiLaParser(context: MangaLoaderContext) : MangaParser(context, MangaSo
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
override suspend fun getRelatedManga(seed: Manga): List<Manga> {
|
|
|
|
|
val json =
|
|
|
|
|
webClient.httpGet("$ltnBaseUrl/galleries/${seed.url}.js")
|
|
|
|
|
val json = webClient.httpGet("$ltnBaseUrl/galleries/${seed.url}.js")
|
|
|
|
|
.parseRaw()
|
|
|
|
|
.substringAfter("var galleryinfo = ")
|
|
|
|
|
.let(::JSONObject)
|
|
|
|
|
@ -608,8 +600,7 @@ class HitomiLaParser(context: MangaLoaderContext) : MangaParser(context, MangaSo
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
override suspend fun getPages(chapter: MangaChapter): List<MangaPage> {
|
|
|
|
|
val json =
|
|
|
|
|
webClient.httpGet("$ltnBaseUrl/galleries/${chapter.url}.js")
|
|
|
|
|
val json = webClient.httpGet("$ltnBaseUrl/galleries/${chapter.url}.js")
|
|
|
|
|
.parseRaw()
|
|
|
|
|
.substringAfter("var galleryinfo = ")
|
|
|
|
|
.let(::JSONObject)
|
|
|
|
|
@ -637,8 +628,7 @@ class HitomiLaParser(context: MangaLoaderContext) : MangaParser(context, MangaSo
|
|
|
|
|
private val subdomainOffsetMap = mutableMapOf<Int, Int>()
|
|
|
|
|
private var commonImageId = ""
|
|
|
|
|
|
|
|
|
|
private suspend fun refreshScript() =
|
|
|
|
|
mutex.withLock {
|
|
|
|
|
private suspend fun refreshScript() = mutex.withLock {
|
|
|
|
|
if (scriptLastRetrieval == null || (scriptLastRetrieval!! + 60000) < System.currentTimeMillis()) {
|
|
|
|
|
val ggScript = webClient.httpGet("$ltnBaseUrl/gg.js?_=${System.currentTimeMillis()}").parseRaw()
|
|
|
|
|
|
|
|
|
|
|