hitomi: formatting

AwkwardPeak7 2 years ago
parent b61c5e8f12
commit 495c9fad33
No known key found for this signature in database

@ -20,43 +20,44 @@ import kotlin.math.min
@OptIn(ExperimentalUnsignedTypes::class) @OptIn(ExperimentalUnsignedTypes::class)
@MangaSourceParser("HITOMILA", "Hitomi.La", type = ContentType.HENTAI) @MangaSourceParser("HITOMILA", "Hitomi.La", type = ContentType.HENTAI)
class HitomiLaParser(context: MangaLoaderContext) : MangaParser(context, MangaSource.HITOMILA) { class HitomiLaParser(context: MangaLoaderContext) : MangaParser(context, MangaSource.HITOMILA) {
override val configKeyDomain = ConfigKey.Domain("hitomi.la") override val configKeyDomain = ConfigKey.Domain("hitomi.la")
private val ltnBaseUrl get() = "https://${getDomain("ltn")}" private val ltnBaseUrl get() = "https://${getDomain("ltn")}"
override val availableSortOrders: Set<SortOrder> = EnumSet.of( override val availableSortOrders: Set<SortOrder> =
SortOrder.NEWEST, EnumSet.of(
SortOrder.POPULARITY, SortOrder.NEWEST,
) SortOrder.POPULARITY,
)
private val localeMap: Map<Locale, String> = mapOf( private val localeMap: Map<Locale, String> =
Locale("id") to "indonesian", mapOf(
Locale("jv") to "javanese", Locale("id") to "indonesian",
Locale("ca") to "catalan", Locale("jv") to "javanese",
Locale("ceb") to "cebuano", Locale("ca") to "catalan",
Locale("cs") to "czech", Locale("ceb") to "cebuano",
Locale("da") to "danish", Locale("cs") to "czech",
Locale("de") to "german", Locale("da") to "danish",
Locale("et") to "estonian", Locale("de") to "german",
Locale.ENGLISH to "english", Locale("et") to "estonian",
Locale("es") to "spanish", Locale.ENGLISH to "english",
Locale("eo") to "esperanto", Locale("es") to "spanish",
Locale("fr") to "french", Locale("eo") to "esperanto",
Locale("it") to "italian", Locale("fr") to "french",
Locale("hi") to "hindi", Locale("it") to "italian",
Locale("hu") to "hungarian", Locale("hi") to "hindi",
Locale("pl") to "polish", Locale("hu") to "hungarian",
Locale("pt") to "portuguese", Locale("pl") to "polish",
Locale("vi") to "vietnamese", Locale("pt") to "portuguese",
Locale("tr") to "turkish", Locale("vi") to "vietnamese",
Locale("ru") to "russian", Locale("tr") to "turkish",
Locale("uk") to "ukrainian", Locale("ru") to "russian",
Locale("ar") to "arabic", Locale("uk") to "ukrainian",
Locale.KOREAN to "korean", Locale("ar") to "arabic",
Locale.CHINESE to "chinese", Locale.KOREAN to "korean",
Locale.JAPANESE to "japanese", Locale.CHINESE to "chinese",
) Locale.JAPANESE to "japanese",
)
private fun Locale?.getSiteLang(): String { private fun Locale?.getSiteLang(): String {
return when (this) { return when (this) {
@ -76,14 +77,16 @@ class HitomiLaParser(context: MangaLoaderContext) : MangaParser(context, MangaSo
val doc = webClient.httpGet("https://$domain/alltags-$alphabet.html").parseHtml() val doc = webClient.httpGet("https://$domain/alltags-$alphabet.html").parseHtml()
doc.select(".posts > li").mapNotNull { element -> doc.select(".posts > li").mapNotNull { element ->
val num = element.ownText().let { val num =
Regex("""\((\d+)\)""").find(it)?.groupValues?.get(1)?.toIntOrNull() ?: 0 element.ownText().let {
} Regex("""\((\d+)\)""").find(it)?.groupValues?.get(1)?.toIntOrNull() ?: 0
}
if (num > 100) { if (num > 100) {
val url = element.selectFirst("a") val url = element.selectFirst("a")
val href = url?.attrAsRelativeUrl("href") val href =
?: return@mapNotNull null url?.attrAsRelativeUrl("href")
?: return@mapNotNull null
MangaTag( MangaTag(
title = url.ownText().toCamelCase(), title = url.ownText().toCamelCase(),
@ -101,7 +104,10 @@ class HitomiLaParser(context: MangaLoaderContext) : MangaParser(context, MangaSo
private var cachedSearchIds: List<Int> = emptyList() private var cachedSearchIds: List<Int> = emptyList()
override suspend fun getList(offset: Int, filter: MangaListFilter?): List<Manga> { override suspend fun getList(
offset: Int,
filter: MangaListFilter?,
): List<Manga> {
return when (filter) { return when (filter) {
is MangaListFilter.Advanced -> { is MangaListFilter.Advanced -> {
if (filter.tags.isEmpty()) { if (filter.tags.isEmpty()) {
@ -116,18 +122,19 @@ class HitomiLaParser(context: MangaLoaderContext) : MangaParser(context, MangaSo
} }
} else { } else {
if (offset == 0) { if (offset == 0) {
val query = filter.tags.joinToString(" ") { it.key }.let { val query =
val lang = filter.locale.getSiteLang() filter.tags.joinToString(" ") { it.key }.let {
if (lang != "all") { val lang = filter.locale.getSiteLang()
"$it language:$lang" if (lang != "all") {
} else { "$it language:$lang"
it } else {
it
}
} }
}
cachedSearchIds = hitomiSearch(query,filter.sortOrder == SortOrder.POPULARITY).toList() cachedSearchIds = hitomiSearch(query, filter.sortOrder == SortOrder.POPULARITY).toList()
} }
cachedSearchIds.subList(offset, min(offset+25, cachedSearchIds.size)) cachedSearchIds.subList(offset, min(offset + 25, cachedSearchIds.size))
} }
} }
@ -135,7 +142,7 @@ class HitomiLaParser(context: MangaLoaderContext) : MangaParser(context, MangaSo
if (offset == 0) { if (offset == 0) {
cachedSearchIds = hitomiSearch(filter.query, filter.sortOrder == SortOrder.POPULARITY).toList() cachedSearchIds = hitomiSearch(filter.query, filter.sortOrder == SortOrder.POPULARITY).toList()
} }
cachedSearchIds.subList(offset, min(offset+25, cachedSearchIds.size)) cachedSearchIds.subList(offset, min(offset + 25, cachedSearchIds.size))
} }
else -> getGalleryIDsFromNozomi(null, "popular", "all", offset.nextOffsetRange()) else -> getGalleryIDsFromNozomi(null, "popular", "all", offset.nextOffsetRange())
@ -143,85 +150,94 @@ class HitomiLaParser(context: MangaLoaderContext) : MangaParser(context, MangaSo
} }
private fun Int.nextOffsetRange(): LongRange { private fun Int.nextOffsetRange(): LongRange {
val bytes = this*4L val bytes = this * 4L
return bytes.until(bytes+100L) return bytes.until(bytes + 100L)
} }
private suspend fun hitomiSearch(query: String, sortByPopularity: Boolean = false) : Set<Int> = coroutineScope { private suspend fun hitomiSearch(
val terms = query query: String,
.trim() sortByPopularity: Boolean = false,
.replace(Regex("""^\?"""), "") ): Set<Int> =
.lowercase() coroutineScope {
.split(Regex("\\s+")) val terms =
.map { query
it.replace('_', ' ') .trim()
.replace(Regex("""^\?"""), "")
.lowercase()
.split(Regex("\\s+"))
.map {
it.replace('_', ' ')
}
val positiveTerms = LinkedList<String>()
val negativeTerms = LinkedList<String>()
for (term in terms) {
if (term.startsWith("-")) {
negativeTerms.push(term.removePrefix("-"))
} else if (term.isNotBlank()) {
positiveTerms.push(term)
}
} }
val positiveTerms = LinkedList<String>() val positiveResults =
val negativeTerms = LinkedList<String>() positiveTerms.map {
async {
runCatching {
getGalleryIDsForQuery(it)
}.getOrDefault(emptySet())
}
}
for (term in terms) { val negativeResults =
if (term.startsWith("-")) negativeTerms.map {
negativeTerms.push(term.removePrefix("-")) async {
else if (term.isNotBlank()) runCatching {
positiveTerms.push(term) getGalleryIDsForQuery(it)
} }.getOrDefault(emptySet())
}
}
val positiveResults = positiveTerms.map { val results =
async { when {
runCatching { sortByPopularity -> getGalleryIDsFromNozomi(null, "popular", "all")
getGalleryIDsForQuery(it) positiveTerms.isEmpty() -> getGalleryIDsFromNozomi(null, "index", "all")
}.getOrDefault(emptySet()) else -> emptySet()
}.toMutableSet()
fun filterPositive(newResults: Set<Int>) {
when {
results.isEmpty() -> results.addAll(newResults)
else -> results.retainAll(newResults)
}
} }
}
val negativeResults = negativeTerms.map { fun filterNegative(newResults: Set<Int>) {
async { results.removeAll(newResults)
runCatching {
getGalleryIDsForQuery(it)
}.getOrDefault(emptySet())
} }
}
val results = when {
sortByPopularity -> getGalleryIDsFromNozomi(null, "popular", "all")
positiveTerms.isEmpty() -> getGalleryIDsFromNozomi(null, "index", "all")
else -> emptySet()
}.toMutableSet()
fun filterPositive(newResults: Set<Int>) { // positive results
when { positiveResults.forEach {
results.isEmpty() -> results.addAll(newResults) filterPositive(it.await())
else -> results.retainAll(newResults)
} }
}
fun filterNegative(newResults: Set<Int>) {
results.removeAll(newResults)
}
//positive results // negative results
positiveResults.forEach { negativeResults.forEach {
filterPositive(it.await()) filterNegative(it.await())
} }
//negative results results
negativeResults.forEach {
filterNegative(it.await())
} }
results // search.js
} private suspend fun getGalleryIDsForQuery(query: String): Set<Int> {
//search.js
private suspend fun getGalleryIDsForQuery(query: String) : Set<Int> {
query.replace("_", " ").let { query.replace("_", " ").let {
if (it.indexOf(':') > -1) { if (it.indexOf(':') > -1) {
val sides = it.split(":") val sides = it.split(":")
val ns = sides[0] val ns = sides[0]
var tag = sides[1] var tag = sides[1]
var area : String? = ns var area: String? = ns
var language = "all" var language = "all"
when (ns) { when (ns) {
"female", "male" -> { "female", "male" -> {
@ -240,35 +256,39 @@ class HitomiLaParser(context: MangaLoaderContext) : MangaParser(context, MangaSo
val key = hashTerm(it) val key = hashTerm(it)
val node = getGalleryNodeAtAddress(0) val node = getGalleryNodeAtAddress(0)
val data = bSearch(key, node) val data =
?: return emptySet() bSearch(key, node)
?: return emptySet()
return getGalleryIDsFromData(data) return getGalleryIDsFromData(data)
} }
} }
private suspend fun getGalleryIDsFromData(data: Pair<Long, Int>) : Set<Int> { private suspend fun getGalleryIDsFromData(data: Pair<Long, Int>): Set<Int> {
val url = "$ltnBaseUrl/galleriesindex/galleries.${galleriesIndexVersion.get()}.data" val url = "$ltnBaseUrl/galleriesindex/galleries.${galleriesIndexVersion.get()}.data"
val (offset, length) = data val (offset, length) = data
if (length > 100000000 || length <= 0) if (length > 100000000 || length <= 0) {
throw Exception("length $length is too long") throw Exception("length $length is too long")
}
val inbuf = getRangedResponse(url, offset.until(offset+length)) val inbuf = getRangedResponse(url, offset.until(offset + length))
val galleryIDs = mutableSetOf<Int>() val galleryIDs = mutableSetOf<Int>()
val buffer = ByteBuffer val buffer =
.wrap(inbuf) ByteBuffer
.order(ByteOrder.BIG_ENDIAN) .wrap(inbuf)
.order(ByteOrder.BIG_ENDIAN)
val numberOfGalleryIDs = buffer.int val numberOfGalleryIDs = buffer.int
val expectedLength = numberOfGalleryIDs*4+4 val expectedLength = numberOfGalleryIDs * 4 + 4
if (numberOfGalleryIDs > 10000000 || numberOfGalleryIDs <= 0) if (numberOfGalleryIDs > 10000000 || numberOfGalleryIDs <= 0) {
throw Exception("number_of_galleryids $numberOfGalleryIDs is too long") throw Exception("number_of_galleryids $numberOfGalleryIDs is too long")
else if (inbuf.size != expectedLength) } else if (inbuf.size != expectedLength) {
throw Exception("inbuf.byteLength ${inbuf.size} != expected_length $expectedLength") throw Exception("inbuf.byteLength ${inbuf.size} != expected_length $expectedLength")
}
for (i in 0.until(numberOfGalleryIDs)) for (i in 0.until(numberOfGalleryIDs))
galleryIDs.add(buffer.int) galleryIDs.add(buffer.int)
@ -276,64 +296,85 @@ class HitomiLaParser(context: MangaLoaderContext) : MangaParser(context, MangaSo
return galleryIDs return galleryIDs
} }
private suspend fun bSearch(key: UByteArray, node: Node) : Pair<Long, Int>? { private suspend fun bSearch(
fun compareArrayBuffers(dv1: UByteArray, dv2: UByteArray) : Int { key: UByteArray,
node: Node,
): Pair<Long, Int>? {
fun compareArrayBuffers(
dv1: UByteArray,
dv2: UByteArray,
): Int {
val top = min(dv1.size, dv2.size) val top = min(dv1.size, dv2.size)
for (i in 0.until(top)) { for (i in 0.until(top)) {
if (dv1[i] < dv2[i]) if (dv1[i] < dv2[i]) {
return -1 return -1
else if (dv1[i] > dv2[i]) } else if (dv1[i] > dv2[i]) {
return 1 return 1
}
} }
return 0 return 0
} }
fun locateKey(key: UByteArray, node: Node) : Pair<Boolean, Int> { fun locateKey(
key: UByteArray,
node: Node,
): Pair<Boolean, Int> {
for (i in node.keys.indices) { for (i in node.keys.indices) {
val cmpResult = compareArrayBuffers(key, node.keys[i]) val cmpResult = compareArrayBuffers(key, node.keys[i])
if (cmpResult <= 0) if (cmpResult <= 0) {
return Pair(cmpResult==0, i) return Pair(cmpResult == 0, i)
}
} }
return Pair(false, node.keys.size) return Pair(false, node.keys.size)
} }
fun isLeaf(node: Node) : Boolean { fun isLeaf(node: Node): Boolean {
for (subnode in node.subNodeAddresses) for (subnode in node.subNodeAddresses)
if (subnode != 0L) if (subnode != 0L) {
return false return false
}
return true return true
} }
if (node.keys.isEmpty()) if (node.keys.isEmpty()) {
return null return null
}
val (there, where) = locateKey(key, node) val (there, where) = locateKey(key, node)
if (there) if (there) {
return node.datas[where] return node.datas[where]
else if (isLeaf(node)) } else if (isLeaf(node)) {
return null return null
}
val nextNode = getGalleryNodeAtAddress(node.subNodeAddresses[where]) val nextNode = getGalleryNodeAtAddress(node.subNodeAddresses[where])
return bSearch(key, nextNode) return bSearch(key, nextNode)
} }
private suspend fun getGalleryIDsFromNozomi(area: String?, tag: String, language: String, range: LongRange? = null) : Set<Int> { private suspend fun getGalleryIDsFromNozomi(
val nozomiAddress = when(area) { area: String?,
null -> "$ltnBaseUrl/$tag-$language.nozomi" tag: String,
else -> "$ltnBaseUrl/$area/$tag-$language.nozomi" language: String,
} range: LongRange? = null,
): Set<Int> {
val nozomiAddress =
when (area) {
null -> "$ltnBaseUrl/$tag-$language.nozomi"
else -> "$ltnBaseUrl/$area/$tag-$language.nozomi"
}
val bytes = getRangedResponse(nozomiAddress, range) val bytes = getRangedResponse(nozomiAddress, range)
val nozomi = mutableSetOf<Int>() val nozomi = mutableSetOf<Int>()
val arrayBuffer = ByteBuffer val arrayBuffer =
.wrap(bytes) ByteBuffer
.order(ByteOrder.BIG_ENDIAN) .wrap(bytes)
.order(ByteOrder.BIG_ENDIAN)
while (arrayBuffer.hasRemaining()) while (arrayBuffer.hasRemaining())
nozomi.add(arrayBuffer.int) nozomi.add(arrayBuffer.int)
@ -341,9 +382,10 @@ class HitomiLaParser(context: MangaLoaderContext) : MangaParser(context, MangaSo
return nozomi return nozomi
} }
private val galleriesIndexVersion = SuspendLazy { private val galleriesIndexVersion =
webClient.httpGet("$ltnBaseUrl/galleriesindex/version?_=${System.currentTimeMillis()}").parseRaw() SuspendLazy {
} webClient.httpGet("$ltnBaseUrl/galleriesindex/version?_=${System.currentTimeMillis()}").parseRaw()
}
private data class Node( private data class Node(
val keys: List<UByteArray>, val keys: List<UByteArray>,
@ -351,10 +393,11 @@ class HitomiLaParser(context: MangaLoaderContext) : MangaParser(context, MangaSo
val subNodeAddresses: List<Long>, val subNodeAddresses: List<Long>,
) )
private fun decodeNode(data: ByteArray) : Node { private fun decodeNode(data: ByteArray): Node {
val buffer = ByteBuffer val buffer =
.wrap(data) ByteBuffer
.order(ByteOrder.BIG_ENDIAN) .wrap(data)
.order(ByteOrder.BIG_ENDIAN)
val uData = data.toUByteArray() val uData = data.toUByteArray()
@ -364,11 +407,12 @@ class HitomiLaParser(context: MangaLoaderContext) : MangaParser(context, MangaSo
for (i in 0.until(numberOfKeys)) { for (i in 0.until(numberOfKeys)) {
val keySize = buffer.int val keySize = buffer.int
if (keySize == 0 || keySize > 32) if (keySize == 0 || keySize > 32) {
throw Exception("fatal: !keySize || keySize > 32") throw Exception("fatal: !keySize || keySize > 32")
}
keys.add(uData.sliceArray(buffer.position().until(buffer.position()+keySize))) keys.add(uData.sliceArray(buffer.position().until(buffer.position() + keySize)))
buffer.position(buffer.position()+keySize) buffer.position(buffer.position() + keySize)
} }
val numberOfDatas = buffer.int val numberOfDatas = buffer.int
@ -392,7 +436,7 @@ class HitomiLaParser(context: MangaLoaderContext) : MangaParser(context, MangaSo
return Node(keys, datas, subNodeAddresses) return Node(keys, datas, subNodeAddresses)
} }
private suspend fun getGalleryNodeAtAddress(address: Long) : Node { private suspend fun getGalleryNodeAtAddress(address: Long): Node {
val url = "$ltnBaseUrl/galleriesindex/galleries.${galleriesIndexVersion.get()}.index" val url = "$ltnBaseUrl/galleriesindex/galleries.${galleriesIndexVersion.get()}.index"
val nodedata = getRangedResponse(url, address.until(address + 464)) val nodedata = getRangedResponse(url, address.until(address + 464))
@ -400,20 +444,24 @@ class HitomiLaParser(context: MangaLoaderContext) : MangaParser(context, MangaSo
return decodeNode(nodedata) return decodeNode(nodedata)
} }
private suspend fun getRangedResponse(url: String, range: LongRange? = null) : ByteArray { private suspend fun getRangedResponse(
val rangeHeaders = when (range) { url: String,
null -> Headers.headersOf() range: LongRange? = null,
else -> Headers.headersOf("Range", "bytes=${range.first}-${range.last}") ): ByteArray {
} val rangeHeaders =
when (range) {
null -> Headers.headersOf()
else -> Headers.headersOf("Range", "bytes=${range.first}-${range.last}")
}
return webClient.httpGet(url, rangeHeaders).parseBytes() return webClient.httpGet(url, rangeHeaders).parseBytes()
} }
private fun hashTerm(term: String) : UByteArray { private fun hashTerm(term: String): UByteArray {
return sha256(term.toByteArray()).copyOfRange(0, 4).toUByteArray() return sha256(term.toByteArray()).copyOfRange(0, 4).toUByteArray()
} }
private fun sha256(data: ByteArray) : ByteArray { private fun sha256(data: ByteArray): ByteArray {
return MessageDigest.getInstance("SHA-256").digest(data) return MessageDigest.getInstance("SHA-256").digest(data)
} }
@ -428,12 +476,15 @@ class HitomiLaParser(context: MangaLoaderContext) : MangaParser(context, MangaSo
id = generateUid(id.toString()), id = generateUid(id.toString()),
title = doc.selectFirstOrThrow("h1").text(), title = doc.selectFirstOrThrow("h1").text(),
url = id.toString(), url = id.toString(),
coverUrl = "https:" + doc.selectFirstOrThrow("picture > source") coverUrl =
.attr("data-srcset") "https:" +
.substringBefore(" "), doc.selectFirstOrThrow("picture > source")
publicUrl = doc.selectFirstOrThrow("h1 > a") .attr("data-srcset")
.attrAsRelativeUrl("href") .substringBefore(" "),
.toAbsoluteUrl(domain), publicUrl =
doc.selectFirstOrThrow("h1 > a")
.attrAsRelativeUrl("href")
.toAbsoluteUrl(domain),
author = null, author = null,
tags = emptySet(), tags = emptySet(),
isNsfw = true, isNsfw = true,
@ -449,54 +500,59 @@ class HitomiLaParser(context: MangaLoaderContext) : MangaParser(context, MangaSo
} }
override suspend fun getDetails(manga: Manga): Manga { override suspend fun getDetails(manga: Manga): Manga {
val json = webClient.httpGet("$ltnBaseUrl/galleries/${manga.url}.js") val json =
.parseRaw() webClient.httpGet("$ltnBaseUrl/galleries/${manga.url}.js")
.substringAfter("var galleryinfo = ") .parseRaw()
.let(::JSONObject) .substringAfter("var galleryinfo = ")
.let(::JSONObject)
return manga.copy( return manga.copy(
title = json.getString("title"), title = json.getString("title"),
largeCoverUrl = json.getJSONArray("files").getJSONObject(0).let { largeCoverUrl =
val hash = it.getString("hash") json.getJSONArray("files").getJSONObject(0).let {
val commonId = commonImageId() val hash = it.getString("hash")
val imageId = imageIdFromHash(hash) val commonId = commonImageId()
val subDomain = 'a' + subdomainOffset(imageId) val imageId = imageIdFromHash(hash)
val subDomain = 'a' + subdomainOffset(imageId)
"https://${getDomain("${subDomain}a")}/webp/$commonId$imageId/$hash.webp"
}, "https://${getDomain("${subDomain}a")}/webp/$commonId$imageId/$hash.webp"
author = json.optJSONArray("artists") },
?.mapJSON { it.getString("artist").toCamelCase() } author =
?.joinToString(),
publicUrl = json.getString("galleryurl").toAbsoluteUrl(domain),
tags = buildSet {
json.optJSONArray("characters")
?.mapToTags("character")
?.let(::addAll)
json.optJSONArray("tags")
?.mapToTags("tag")
?.let(::addAll)
json.optJSONArray("artists") json.optJSONArray("artists")
?.mapToTags("artist") ?.mapJSON { it.getString("artist").toCamelCase() }
?.let(::addAll) ?.joinToString(),
json.optJSONArray("parodys") publicUrl = json.getString("galleryurl").toAbsoluteUrl(domain),
?.mapToTags("parody") tags =
?.let(::addAll) buildSet {
json.optJSONArray("groups") json.optJSONArray("characters")
?.mapToTags("group") ?.mapToTags("character")
?.let(::addAll) ?.let(::addAll)
}, json.optJSONArray("tags")
chapters = listOf( ?.mapToTags("tag")
MangaChapter( ?.let(::addAll)
id = generateUid(manga.url), json.optJSONArray("artists")
url = manga.url, ?.mapToTags("artist")
name = json.getString("title"), ?.let(::addAll)
scanlator = json.getString("type").toTitleCase(), json.optJSONArray("parodys")
number = 1, ?.mapToTags("parody")
branch = json.getString("language_localname"), ?.let(::addAll)
source = source, json.optJSONArray("groups")
uploadDate = dateFormat.tryParse(json.getString("date").substringBeforeLast("-")), ?.mapToTags("group")
) ?.let(::addAll)
) },
chapters =
listOf(
MangaChapter(
id = generateUid(manga.url),
url = manga.url,
name = json.getString("title"),
scanlator = json.getString("type").toTitleCase(),
number = 1,
branch = json.getString("language_localname"),
source = source,
uploadDate = dateFormat.tryParse(json.getString("date").substringBeforeLast("-")),
),
),
) )
} }
@ -508,17 +564,18 @@ class HitomiLaParser(context: MangaLoaderContext) : MangaParser(context, MangaSo
val tags = mutableSetOf<MangaTag>() val tags = mutableSetOf<MangaTag>()
mapJSON { mapJSON {
MangaTag( MangaTag(
title = it.getString(key).toCamelCase().let { title -> title =
if (it.getStringOrNull("female")?.toIntOrNull() == 1) { it.getString(key).toCamelCase().let { title ->
"$title" if (it.getStringOrNull("female")?.toIntOrNull() == 1) {
} else if (it.getStringOrNull("male")?.toIntOrNull() == 1) { "$title"
"$title" } else if (it.getStringOrNull("male")?.toIntOrNull() == 1) {
} else { "$title"
title } else {
} title
}, }
},
key = it.getString("url").tagUrlToTag(), key = it.getString("url").tagUrlToTag(),
source = source source = source,
).let(tags::add) ).let(tags::add)
} }
return tags return tags
@ -527,10 +584,11 @@ class HitomiLaParser(context: MangaLoaderContext) : MangaParser(context, MangaSo
private fun String.tagUrlToTag(): String { private fun String.tagUrlToTag(): String {
val urlContent = this.split("/") val urlContent = this.split("/")
val ns = urlContent[1] val ns = urlContent[1]
val tag = urlContent[2] val tag =
.substringBeforeLast("-") urlContent[2]
.urlDecode() .substringBeforeLast("-")
.replace(" ", "_") .urlDecode()
.replace(" ", "_")
return if (tag.split(":")[0] in listOf("female", "male")) { return if (tag.split(":")[0] in listOf("female", "male")) {
tag tag
@ -540,10 +598,11 @@ class HitomiLaParser(context: MangaLoaderContext) : MangaParser(context, MangaSo
} }
override suspend fun getRelatedManga(seed: Manga): List<Manga> { override suspend fun getRelatedManga(seed: Manga): List<Manga> {
val json = webClient.httpGet("$ltnBaseUrl/galleries/${seed.url}.js") val json =
.parseRaw() webClient.httpGet("$ltnBaseUrl/galleries/${seed.url}.js")
.substringAfter("var galleryinfo = ") .parseRaw()
.let(::JSONObject) .substringAfter("var galleryinfo = ")
.let(::JSONObject)
// any better way to get List<Int> from this json? // any better way to get List<Int> from this json?
return json.getJSONArray("related").let { return json.getJSONArray("related").let {
@ -552,10 +611,11 @@ class HitomiLaParser(context: MangaLoaderContext) : MangaParser(context, MangaSo
} }
override suspend fun getPages(chapter: MangaChapter): List<MangaPage> { override suspend fun getPages(chapter: MangaChapter): List<MangaPage> {
val json = webClient.httpGet("$ltnBaseUrl/galleries/${chapter.url}.js") val json =
.parseRaw() webClient.httpGet("$ltnBaseUrl/galleries/${chapter.url}.js")
.substringAfter("var galleryinfo = ") .parseRaw()
.let(::JSONObject) .substringAfter("var galleryinfo = ")
.let(::JSONObject)
return json.getJSONArray("files").mapJSON { image -> return json.getJSONArray("files").mapJSON { image ->
val hash = image.getString("hash") val hash = image.getString("hash")
@ -564,15 +624,15 @@ class HitomiLaParser(context: MangaLoaderContext) : MangaParser(context, MangaSo
val subDomain = 'a' + subdomainOffset(imageId) val subDomain = 'a' + subdomainOffset(imageId)
MangaPage( MangaPage(
id= generateUid(hash), id = generateUid(hash),
url = "https://${getDomain("${subDomain}a")}/webp/$commonId$imageId/$hash.webp", url = "https://${getDomain("${subDomain}a")}/webp/$commonId$imageId/$hash.webp",
preview = "https://${getDomain("${subDomain}tn")}/webpsmalltn/${thumbPathFromHash(hash)}/$hash.webp", preview = "https://${getDomain("${subDomain}tn")}/webpsmalltn/${thumbPathFromHash(hash)}/$hash.webp",
source = source source = source,
) )
} }
} }
/// ---> // / --->
private var scriptLastRetrieval: Long? = null private var scriptLastRetrieval: Long? = null
private val mutex = Mutex() private val mutex = Mutex()
@ -580,24 +640,25 @@ class HitomiLaParser(context: MangaLoaderContext) : MangaParser(context, MangaSo
private val subdomainOffsetMap = mutableMapOf<Int, Int>() private val subdomainOffsetMap = mutableMapOf<Int, Int>()
private var commonImageId = "" private var commonImageId = ""
private suspend fun refreshScript() = mutex.withLock { private suspend fun refreshScript() =
if (scriptLastRetrieval == null || (scriptLastRetrieval!! + 60000) < System.currentTimeMillis()) { mutex.withLock {
val ggScript = webClient.httpGet("$ltnBaseUrl/gg.js?_=${System.currentTimeMillis()}").parseRaw() if (scriptLastRetrieval == null || (scriptLastRetrieval!! + 60000) < System.currentTimeMillis()) {
val ggScript = webClient.httpGet("$ltnBaseUrl/gg.js?_=${System.currentTimeMillis()}").parseRaw()
subdomainOffsetDefault = Regex("var o = (\\d)").find(ggScript)!!.groupValues[1].toInt() subdomainOffsetDefault = Regex("var o = (\\d)").find(ggScript)!!.groupValues[1].toInt()
val o = Regex("o = (\\d); break;").find(ggScript)!!.groupValues[1].toInt() val o = Regex("o = (\\d); break;").find(ggScript)!!.groupValues[1].toInt()
subdomainOffsetMap.clear() subdomainOffsetMap.clear()
Regex("case (\\d+):").findAll(ggScript).forEach { Regex("case (\\d+):").findAll(ggScript).forEach {
val case = it.groupValues[1].toInt() val case = it.groupValues[1].toInt()
subdomainOffsetMap[case] = o subdomainOffsetMap[case] = o
} }
commonImageId = Regex("b: '(.+)'").find(ggScript)!!.groupValues[1] commonImageId = Regex("b: '(.+)'").find(ggScript)!!.groupValues[1]
scriptLastRetrieval = System.currentTimeMillis() scriptLastRetrieval = System.currentTimeMillis()
}
} }
}
// m <-- gg.js // m <-- gg.js
private suspend fun subdomainOffset(imageId: Int): Int { private suspend fun subdomainOffset(imageId: Int): Int {
@ -614,7 +675,7 @@ class HitomiLaParser(context: MangaLoaderContext) : MangaParser(context, MangaSo
// s <-- gg.js // s <-- gg.js
private fun imageIdFromHash(hash: String): Int { private fun imageIdFromHash(hash: String): Int {
val match = Regex("(..)(.)$").find(hash) val match = Regex("(..)(.)$").find(hash)
return match!!.groupValues.let { it[2]+it[1] }.toInt(16) return match!!.groupValues.let { it[2] + it[1] }.toInt(16)
} }
// real_full_path_from_hash <-- common.js // real_full_path_from_hash <-- common.js

Loading…
Cancel
Save