Yeet Hitomi (#11613)
This commit is contained in:
parent
7113c5166a
commit
40fab1d9b5
|
@ -1,36 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
package="eu.kanade.tachiyomi.extension">
|
|
||||||
|
|
||||||
<application>
|
|
||||||
<activity
|
|
||||||
android:name=".all.hitomi.HitomiActivity"
|
|
||||||
android:excludeFromRecents="true"
|
|
||||||
android:exported="true"
|
|
||||||
android:theme="@android:style/Theme.NoDisplay">
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="android.intent.action.VIEW" />
|
|
||||||
|
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
|
||||||
<category android:name="android.intent.category.BROWSABLE" />
|
|
||||||
|
|
||||||
<data
|
|
||||||
android:host="hitomi.la"
|
|
||||||
android:pathPattern="/manga/..*"
|
|
||||||
android:scheme="https" />
|
|
||||||
<data
|
|
||||||
android:host="hitomi.la"
|
|
||||||
android:pathPattern="/doujinshi/..*"
|
|
||||||
android:scheme="https" />
|
|
||||||
<data
|
|
||||||
android:host="hitomi.la"
|
|
||||||
android:pathPattern="/cg/..*"
|
|
||||||
android:scheme="https" />
|
|
||||||
<data
|
|
||||||
android:host="hitomi.la"
|
|
||||||
android:pathPattern="/gamecg/..*"
|
|
||||||
android:scheme="https" />
|
|
||||||
</intent-filter>
|
|
||||||
</activity>
|
|
||||||
</application>
|
|
||||||
</manifest>
|
|
|
@ -1,13 +0,0 @@
|
||||||
apply plugin: 'com.android.application'
|
|
||||||
apply plugin: 'kotlin-android'
|
|
||||||
apply plugin: 'kotlinx-serialization'
|
|
||||||
|
|
||||||
ext {
|
|
||||||
extName = 'Hitomi.la'
|
|
||||||
pkgNameSuffix = 'all.hitomi'
|
|
||||||
extClass = '.HitomiFactory'
|
|
||||||
extVersionCode = 16
|
|
||||||
isNsfw = true
|
|
||||||
}
|
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
|
Binary file not shown.
Before Width: | Height: | Size: 3.2 KiB |
Binary file not shown.
Before Width: | Height: | Size: 1.8 KiB |
Binary file not shown.
Before Width: | Height: | Size: 4.0 KiB |
Binary file not shown.
Before Width: | Height: | Size: 8.2 KiB |
Binary file not shown.
Before Width: | Height: | Size: 12 KiB |
Binary file not shown.
Before Width: | Height: | Size: 56 KiB |
|
@ -1,93 +0,0 @@
|
||||||
package eu.kanade.tachiyomi.extension.all.hitomi
|
|
||||||
|
|
||||||
import java.nio.ByteBuffer
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Simple cursor for use on byte arrays
|
|
||||||
* @author nulldev
|
|
||||||
*/
|
|
||||||
class ByteCursor(val content: ByteArray) {
|
|
||||||
var index = -1
|
|
||||||
private set
|
|
||||||
private var mark = -1
|
|
||||||
|
|
||||||
fun mark() {
|
|
||||||
mark = index
|
|
||||||
}
|
|
||||||
|
|
||||||
fun jumpToMark() {
|
|
||||||
index = mark
|
|
||||||
}
|
|
||||||
|
|
||||||
fun jumpToIndex(index: Int) {
|
|
||||||
this.index = index
|
|
||||||
}
|
|
||||||
|
|
||||||
fun next(): Byte {
|
|
||||||
return content[++index]
|
|
||||||
}
|
|
||||||
|
|
||||||
fun next(count: Int): ByteArray {
|
|
||||||
val res = content.sliceArray(index + 1..index + count)
|
|
||||||
skip(count)
|
|
||||||
return res
|
|
||||||
}
|
|
||||||
|
|
||||||
// Used to perform conversions
|
|
||||||
private fun byteBuffer(count: Int): ByteBuffer {
|
|
||||||
return ByteBuffer.wrap(next(count))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Epic hack to get an unsigned short properly...
|
|
||||||
fun fakeNextShortInt(): Int = ByteBuffer
|
|
||||||
.wrap(arrayOf(0x00, 0x00, *next(2).toTypedArray()).toByteArray())
|
|
||||||
.getInt(0)
|
|
||||||
|
|
||||||
// fun nextShort(): Short = byteBuffer(2).getShort(0)
|
|
||||||
fun nextInt(): Int = byteBuffer(4).getInt(0)
|
|
||||||
fun nextLong(): Long = byteBuffer(8).getLong(0)
|
|
||||||
fun nextFloat(): Float = byteBuffer(4).getFloat(0)
|
|
||||||
fun nextDouble(): Double = byteBuffer(8).getDouble(0)
|
|
||||||
|
|
||||||
fun skip(count: Int) {
|
|
||||||
index += count
|
|
||||||
}
|
|
||||||
|
|
||||||
fun expect(vararg bytes: Byte) {
|
|
||||||
if (bytes.size > remaining()) {
|
|
||||||
throw IllegalStateException("Unexpected end of content!")
|
|
||||||
}
|
|
||||||
|
|
||||||
for (i in 0..bytes.lastIndex) {
|
|
||||||
val expected = bytes[i]
|
|
||||||
val actual = content[index + i + 1]
|
|
||||||
|
|
||||||
if (expected != actual) {
|
|
||||||
throw IllegalStateException("Unexpected content (expected: $expected, actual: $actual)!")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
index += bytes.size
|
|
||||||
}
|
|
||||||
|
|
||||||
fun checkEqual(vararg bytes: Byte): Boolean {
|
|
||||||
if (bytes.size > remaining()) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
for (i in 0..bytes.lastIndex) {
|
|
||||||
val expected = bytes[i]
|
|
||||||
val actual = content[index + i + 1]
|
|
||||||
|
|
||||||
if (expected != actual) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
fun atEnd() = index >= content.size - 1
|
|
||||||
|
|
||||||
fun remaining() = content.size - index - 1
|
|
||||||
}
|
|
|
@ -1,504 +0,0 @@
|
||||||
package eu.kanade.tachiyomi.extension.all.hitomi
|
|
||||||
|
|
||||||
import android.app.Application
|
|
||||||
import android.content.SharedPreferences
|
|
||||||
import com.squareup.duktape.Duktape
|
|
||||||
import eu.kanade.tachiyomi.network.GET
|
|
||||||
import eu.kanade.tachiyomi.network.asObservableSuccess
|
|
||||||
import eu.kanade.tachiyomi.source.ConfigurableSource
|
|
||||||
import eu.kanade.tachiyomi.source.model.Filter
|
|
||||||
import eu.kanade.tachiyomi.source.model.FilterList
|
|
||||||
import eu.kanade.tachiyomi.source.model.MangasPage
|
|
||||||
import eu.kanade.tachiyomi.source.model.Page
|
|
||||||
import eu.kanade.tachiyomi.source.model.SChapter
|
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
|
||||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
|
||||||
import eu.kanade.tachiyomi.util.asJsoup
|
|
||||||
import kotlinx.serialization.decodeFromString
|
|
||||||
import kotlinx.serialization.json.Json
|
|
||||||
import okhttp3.Request
|
|
||||||
import okhttp3.Response
|
|
||||||
import rx.Observable
|
|
||||||
import rx.Single
|
|
||||||
import rx.schedulers.Schedulers
|
|
||||||
import uy.kohesive.injekt.Injekt
|
|
||||||
import uy.kohesive.injekt.api.get
|
|
||||||
import uy.kohesive.injekt.injectLazy
|
|
||||||
import java.net.URLEncoder
|
|
||||||
import java.util.Locale
|
|
||||||
import androidx.preference.CheckBoxPreference as AndroidXCheckBoxPreference
|
|
||||||
import androidx.preference.PreferenceScreen as AndroidXPreferenceScreen
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Ported from TachiyomiSy
|
|
||||||
* Original work by NerdNumber9 for TachiyomiEH
|
|
||||||
*/
|
|
||||||
|
|
||||||
open class Hitomi(override val lang: String, private val nozomiLang: String) : HttpSource(), ConfigurableSource {
|
|
||||||
|
|
||||||
override val supportsLatest = true
|
|
||||||
|
|
||||||
override val name = if (nozomiLang == "all") "Hitomi.la unfiltered" else "Hitomi.la"
|
|
||||||
|
|
||||||
override val id by lazy { if (lang == "all") 2703068117101782422 else super.id }
|
|
||||||
|
|
||||||
override val baseUrl = BASE_URL
|
|
||||||
|
|
||||||
private val json: Json by injectLazy()
|
|
||||||
|
|
||||||
private var gg: String? = null
|
|
||||||
|
|
||||||
// Popular
|
|
||||||
|
|
||||||
override fun fetchPopularManga(page: Int): Observable<MangasPage> {
|
|
||||||
return client.newCall(popularMangaRequest(page))
|
|
||||||
.asObservableSuccess()
|
|
||||||
.flatMap { responseToMangas(it) }
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun popularMangaRequest(page: Int) = HitomiNozomi.rangedGet(
|
|
||||||
"$LTN_BASE_URL/popular-$nozomiLang.nozomi",
|
|
||||||
100L * (page - 1),
|
|
||||||
99L + 100 * (page - 1)
|
|
||||||
)
|
|
||||||
|
|
||||||
private fun responseToMangas(response: Response): Observable<MangasPage> {
|
|
||||||
val range = response.header("Content-Range")!!
|
|
||||||
val total = range.substringAfter('/').toLong()
|
|
||||||
val end = range.substringBefore('/').substringAfter('-').toLong()
|
|
||||||
val body = response.body!!
|
|
||||||
return parseNozomiPage(body.bytes())
|
|
||||||
.map {
|
|
||||||
MangasPage(it, end < total - 1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun parseNozomiPage(array: ByteArray): Observable<List<SManga>> {
|
|
||||||
val cursor = ByteCursor(array)
|
|
||||||
val ids = (1..array.size / 4).map {
|
|
||||||
cursor.nextInt()
|
|
||||||
}
|
|
||||||
|
|
||||||
return nozomiIdsToMangas(ids).toObservable()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun nozomiIdsToMangas(ids: List<Int>): Single<List<SManga>> {
|
|
||||||
return Single.zip(
|
|
||||||
ids.map { int ->
|
|
||||||
client.newCall(GET("$LTN_BASE_URL/galleryblock/$int.html"))
|
|
||||||
.asObservableSuccess()
|
|
||||||
.subscribeOn(Schedulers.io()) // Perform all these requests in parallel
|
|
||||||
.map { parseGalleryBlock(it) }
|
|
||||||
.toSingle()
|
|
||||||
}
|
|
||||||
) { it.map { m -> m as SManga } }
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun parseGalleryBlock(response: Response): SManga {
|
|
||||||
val doc = response.asJsoup()
|
|
||||||
return SManga.create().apply {
|
|
||||||
val titleElement = doc.selectFirst("h1")
|
|
||||||
title = titleElement.text()
|
|
||||||
thumbnail_url = "https:" + if (useHqThumbPref()) {
|
|
||||||
doc.selectFirst("img").attr("srcset").substringBefore(' ')
|
|
||||||
} else {
|
|
||||||
doc.selectFirst("img").attr("src")
|
|
||||||
}
|
|
||||||
url = titleElement.child(0).attr("href")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun popularMangaParse(response: Response) = throw UnsupportedOperationException("Not used")
|
|
||||||
|
|
||||||
// Latest
|
|
||||||
|
|
||||||
override fun fetchLatestUpdates(page: Int): Observable<MangasPage> {
|
|
||||||
return client.newCall(latestUpdatesRequest(page))
|
|
||||||
.asObservableSuccess()
|
|
||||||
.flatMap { responseToMangas(it) }
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun latestUpdatesRequest(page: Int) = HitomiNozomi.rangedGet(
|
|
||||||
"$LTN_BASE_URL/index-$nozomiLang.nozomi",
|
|
||||||
100L * (page - 1),
|
|
||||||
99L + 100 * (page - 1)
|
|
||||||
)
|
|
||||||
|
|
||||||
override fun latestUpdatesParse(response: Response) = throw UnsupportedOperationException("Not used")
|
|
||||||
|
|
||||||
// Search
|
|
||||||
|
|
||||||
private var cachedTagIndexVersion: Long? = null
|
|
||||||
private var tagIndexVersionCacheTime: Long = 0
|
|
||||||
private fun tagIndexVersion(): Single<Long> {
|
|
||||||
val sCachedTagIndexVersion = cachedTagIndexVersion
|
|
||||||
return if (sCachedTagIndexVersion == null ||
|
|
||||||
tagIndexVersionCacheTime + INDEX_VERSION_CACHE_TIME_MS < System.currentTimeMillis()
|
|
||||||
) {
|
|
||||||
HitomiNozomi.getIndexVersion(client, "tagindex").subscribeOn(Schedulers.io()).doOnNext {
|
|
||||||
cachedTagIndexVersion = it
|
|
||||||
tagIndexVersionCacheTime = System.currentTimeMillis()
|
|
||||||
}.toSingle()
|
|
||||||
} else {
|
|
||||||
Single.just(sCachedTagIndexVersion)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private var cachedGalleryIndexVersion: Long? = null
|
|
||||||
private var galleryIndexVersionCacheTime: Long = 0
|
|
||||||
private fun galleryIndexVersion(): Single<Long> {
|
|
||||||
val sCachedGalleryIndexVersion = cachedGalleryIndexVersion
|
|
||||||
return if (sCachedGalleryIndexVersion == null ||
|
|
||||||
galleryIndexVersionCacheTime + INDEX_VERSION_CACHE_TIME_MS < System.currentTimeMillis()
|
|
||||||
) {
|
|
||||||
HitomiNozomi.getIndexVersion(client, "galleriesindex").subscribeOn(Schedulers.io()).doOnNext {
|
|
||||||
cachedGalleryIndexVersion = it
|
|
||||||
galleryIndexVersionCacheTime = System.currentTimeMillis()
|
|
||||||
}.toSingle()
|
|
||||||
} else {
|
|
||||||
Single.just(sCachedGalleryIndexVersion)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
|
|
||||||
return if (query.startsWith(PREFIX_ID_SEARCH)) {
|
|
||||||
val id = NOZOMI_ID_PATTERN.find(query.removePrefix(PREFIX_ID_SEARCH))!!.value.toInt()
|
|
||||||
nozomiIdsToMangas(listOf(id)).map { mangas ->
|
|
||||||
MangasPage(mangas, false)
|
|
||||||
}.toObservable()
|
|
||||||
} else {
|
|
||||||
if (query.isBlank()) {
|
|
||||||
val area = filters.filterIsInstance<TypeFilter>()
|
|
||||||
.joinToString("") {
|
|
||||||
(it as UriPartFilter).toUriPart()
|
|
||||||
}
|
|
||||||
val keyword = filters.filterIsInstance<Text>().toString()
|
|
||||||
.replace("[", "").replace("]", "")
|
|
||||||
val popular = filters.filterIsInstance<SortFilter>()
|
|
||||||
.joinToString("") {
|
|
||||||
(it as UriPartFilter).toUriPart()
|
|
||||||
} == "true"
|
|
||||||
|
|
||||||
// TODO Cache the results coming out of HitomiNozomi (this TODO dates back to TachiyomiEH)
|
|
||||||
val hn = Single.zip(tagIndexVersion(), galleryIndexVersion()) { tv, gv -> tv to gv }
|
|
||||||
.map { HitomiNozomi(client, it.first, it.second) }
|
|
||||||
val base = hn.flatMap { n ->
|
|
||||||
n.getGalleryIdsForQuery("$area:${URLEncoder.encode(keyword, "utf-8")}", nozomiLang, popular).map { n to it.toSet() }
|
|
||||||
}
|
|
||||||
base.flatMap { (_, ids) ->
|
|
||||||
val chunks = ids.chunked(PAGE_SIZE)
|
|
||||||
|
|
||||||
nozomiIdsToMangas(chunks[page - 1]).map { mangas ->
|
|
||||||
MangasPage(mangas, page < chunks.size)
|
|
||||||
}
|
|
||||||
}.toObservable()
|
|
||||||
} else {
|
|
||||||
val splitQuery = query.toLowerCase(Locale.ENGLISH).split(" ")
|
|
||||||
|
|
||||||
val positive = splitQuery.filter {
|
|
||||||
COMMON_WORDS.any { word ->
|
|
||||||
it !== word
|
|
||||||
} && !it.startsWith('-')
|
|
||||||
}.toMutableList()
|
|
||||||
if (nozomiLang != "all") positive += "language:$nozomiLang"
|
|
||||||
val negative = (splitQuery - positive).map { it.removePrefix("-") }
|
|
||||||
|
|
||||||
// TODO Cache the results coming out of HitomiNozomi (this TODO dates back to TachiyomiEH)
|
|
||||||
val hn = Single.zip(tagIndexVersion(), galleryIndexVersion()) { tv, gv -> tv to gv }
|
|
||||||
.map { HitomiNozomi(client, it.first, it.second) }
|
|
||||||
|
|
||||||
var base = if (positive.isEmpty()) {
|
|
||||||
hn.flatMap { n ->
|
|
||||||
n.getGalleryIdsFromNozomi(null, "index", "all", false)
|
|
||||||
.map { n to it.toSet() }
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
val q = positive.removeAt(0)
|
|
||||||
hn.flatMap { n -> n.getGalleryIdsForQuery(q, nozomiLang, false).map { n to it.toSet() } }
|
|
||||||
}
|
|
||||||
|
|
||||||
base = positive.fold(base) { acc, q ->
|
|
||||||
acc.flatMap { (nozomi, mangas) ->
|
|
||||||
nozomi.getGalleryIdsForQuery(q, nozomiLang, false).map {
|
|
||||||
nozomi to mangas.intersect(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
base = negative.fold(base) { acc, q ->
|
|
||||||
acc.flatMap { (nozomi, mangas) ->
|
|
||||||
nozomi.getGalleryIdsForQuery(q, nozomiLang, false).map {
|
|
||||||
nozomi to (mangas - it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
base.flatMap { (_, ids) ->
|
|
||||||
val chunks = ids.chunked(PAGE_SIZE)
|
|
||||||
|
|
||||||
nozomiIdsToMangas(chunks[page - 1]).map { mangas ->
|
|
||||||
MangasPage(mangas, page < chunks.size)
|
|
||||||
}
|
|
||||||
}.toObservable()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList) = throw UnsupportedOperationException("Not used")
|
|
||||||
override fun searchMangaParse(response: Response) = throw UnsupportedOperationException("Not used")
|
|
||||||
|
|
||||||
// Filter
|
|
||||||
|
|
||||||
override fun getFilterList() = FilterList(
|
|
||||||
Filter.Header(Filter_SEARCH_MESSAGE),
|
|
||||||
Filter.Separator(),
|
|
||||||
SortFilter(),
|
|
||||||
TypeFilter(),
|
|
||||||
Text("Keyword")
|
|
||||||
)
|
|
||||||
|
|
||||||
private class TypeFilter : UriPartFilter(
|
|
||||||
"category",
|
|
||||||
Array(FILTER_CATEGORIES.size) { i ->
|
|
||||||
val category = FILTER_CATEGORIES[i]
|
|
||||||
Pair(category, category)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
private class SortFilter : UriPartFilter(
|
|
||||||
"Ordered by",
|
|
||||||
arrayOf(
|
|
||||||
Pair("Date Added", "false"),
|
|
||||||
Pair("Popularity", "true")
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
private open class UriPartFilter(
|
|
||||||
displayName: String,
|
|
||||||
val pair: Array<Pair<String, String>>,
|
|
||||||
defaultState: Int = 0
|
|
||||||
) : Filter.Select<String>(displayName, pair.map { it.first }.toTypedArray(), defaultState) {
|
|
||||||
open fun toUriPart() = pair[state].second
|
|
||||||
}
|
|
||||||
|
|
||||||
private class Text(name: String) : Filter.Text(name) {
|
|
||||||
override fun toString(): String {
|
|
||||||
return state
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Details
|
|
||||||
|
|
||||||
override fun mangaDetailsParse(response: Response): SManga {
|
|
||||||
val document = response.asJsoup()
|
|
||||||
fun String.replaceSpaces() = this.replace(" ", "_")
|
|
||||||
|
|
||||||
return SManga.create().apply {
|
|
||||||
title = document.select("div.gallery h1 a").joinToString { it.text() }
|
|
||||||
thumbnail_url = document.select("div.cover img").attr("abs:src")
|
|
||||||
author = document.select("div.gallery h2 a").joinToString { it.text() }
|
|
||||||
val tableInfo = document.select("table tr")
|
|
||||||
.map { tr ->
|
|
||||||
val key = tr.select("td:first-child").text()
|
|
||||||
val value = with(tr.select("td:last-child a")) {
|
|
||||||
when (key) {
|
|
||||||
"Series", "Characters" -> {
|
|
||||||
if (text().isNotEmpty())
|
|
||||||
joinToString { "${attr("href").removePrefix("/").substringBefore("/")}:${it.text().replaceSpaces()}" } else null
|
|
||||||
}
|
|
||||||
"Tags" -> joinToString { element ->
|
|
||||||
element.text().let {
|
|
||||||
when {
|
|
||||||
it.contains("♀") -> "female:${it.substringBeforeLast(" ").replaceSpaces()}"
|
|
||||||
it.contains("♂") -> "male:${it.substringBeforeLast(" ").replaceSpaces()}"
|
|
||||||
else -> it
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else -> joinToString { it.text() }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Pair(key, value)
|
|
||||||
}
|
|
||||||
.plus(Pair("Date uploaded", document.select("div.gallery span.date").text()))
|
|
||||||
.toMap()
|
|
||||||
description = tableInfo.filterNot { it.value.isNullOrEmpty() || it.key in listOf("Series", "Characters", "Tags") }.entries.joinToString("\n") { "${it.key}: ${it.value}" }
|
|
||||||
genre = listOfNotNull(tableInfo["Series"], tableInfo["Characters"], tableInfo["Tags"]).joinToString()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Chapters
|
|
||||||
|
|
||||||
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
|
|
||||||
return Observable.just(
|
|
||||||
listOf(
|
|
||||||
SChapter.create().apply {
|
|
||||||
url = manga.url
|
|
||||||
name = "Chapter"
|
|
||||||
chapter_number = 0.0f
|
|
||||||
}
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun chapterListParse(response: Response) = throw UnsupportedOperationException("Not used")
|
|
||||||
|
|
||||||
// Pages
|
|
||||||
|
|
||||||
private fun hlIdFromUrl(url: String) =
|
|
||||||
url.split('/').last().split('-').last().substringBeforeLast('.')
|
|
||||||
|
|
||||||
override fun pageListRequest(chapter: SChapter): Request {
|
|
||||||
return GET("$LTN_BASE_URL/galleries/${hlIdFromUrl(chapter.url)}.js")
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun pageListParse(response: Response): List<Page> {
|
|
||||||
if (gg.isNullOrEmpty()) {
|
|
||||||
val response = client.newCall(GET("$LTN_BASE_URL/gg.js")).execute()
|
|
||||||
gg = response.body!!.string()
|
|
||||||
}
|
|
||||||
val duktape = Duktape.create()
|
|
||||||
duktape.evaluate(gg)
|
|
||||||
|
|
||||||
val str = response.body!!.string()
|
|
||||||
val json = json.decodeFromString<HitomiChapterDto>(str.removePrefix("var galleryinfo = "))
|
|
||||||
val pages = json.files.mapIndexed { i, jsonElement ->
|
|
||||||
// https://ltn.hitomi.la/reader.js
|
|
||||||
// function make_image_element()
|
|
||||||
val hash = jsonElement.hash
|
|
||||||
var ext = jsonElement.name.split('.').last()
|
|
||||||
var path = "images"
|
|
||||||
var secondSubdomain = "b"
|
|
||||||
if (hitomiAlwaysWebp() && jsonElement.haswebp == 1) {
|
|
||||||
path = "webp"
|
|
||||||
ext = "webp"
|
|
||||||
secondSubdomain = "a"
|
|
||||||
}
|
|
||||||
if (hitomiAlwaysAvif() && jsonElement.hasavif == 1) {
|
|
||||||
path = "avif"
|
|
||||||
ext = "avif"
|
|
||||||
secondSubdomain = "a"
|
|
||||||
}
|
|
||||||
|
|
||||||
val b = duktape.evaluate("gg.b;") as String
|
|
||||||
val s = duktape.evaluate("gg.s(\"$hash\");") as String
|
|
||||||
val m = duktape.evaluate("gg.m($s).toString();") as String
|
|
||||||
|
|
||||||
var firstSubdomain = "a"
|
|
||||||
if (m == "1") {
|
|
||||||
firstSubdomain = "b"
|
|
||||||
}
|
|
||||||
|
|
||||||
Page(i, "", "https://$firstSubdomain$secondSubdomain.hitomi.la/$path/$b$s/$hash.$ext")
|
|
||||||
}
|
|
||||||
duktape.close()
|
|
||||||
return pages
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun imageRequest(page: Page): Request {
|
|
||||||
val request = super.imageRequest(page)
|
|
||||||
val hlId = request.url.pathSegments.let {
|
|
||||||
it[it.lastIndex - 1]
|
|
||||||
}
|
|
||||||
return request.newBuilder()
|
|
||||||
.header("Referer", "$BASE_URL/reader/$hlId.html")
|
|
||||||
.build()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun imageUrlParse(response: Response) = throw UnsupportedOperationException("Not used")
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
private const val INDEX_VERSION_CACHE_TIME_MS = 1000 * 60 * 10
|
|
||||||
private const val PAGE_SIZE = 25
|
|
||||||
|
|
||||||
const val PREFIX_ID_SEARCH = "id:"
|
|
||||||
val NOZOMI_ID_PATTERN = "[0-9]*(?=.html)".toRegex()
|
|
||||||
val HEXADECIMAL = "0[xX][0-9a-fA-F]+".toRegex()
|
|
||||||
|
|
||||||
// Common English words and Japanese particles
|
|
||||||
private val COMMON_WORDS = listOf(
|
|
||||||
"a", "be", "boy", "de", "girl", "ga", "i", "is", "ka", "na",
|
|
||||||
"ni", "ne", "no", "suru", "to", "wa", "wo", "yo",
|
|
||||||
"b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z"
|
|
||||||
)
|
|
||||||
|
|
||||||
// From HitomiSearchMetaData
|
|
||||||
const val LTN_BASE_URL = "https://ltn.hitomi.la"
|
|
||||||
const val BASE_URL = "https://hitomi.la"
|
|
||||||
|
|
||||||
// Filter
|
|
||||||
private val FILTER_CATEGORIES = listOf(
|
|
||||||
"tag", "male", "female", "type",
|
|
||||||
"artist", "series", "character", "group"
|
|
||||||
)
|
|
||||||
private const val Filter_SEARCH_MESSAGE = "NOTE: Ignored if using text search!"
|
|
||||||
|
|
||||||
// Preferences
|
|
||||||
private const val WEBP_PREF_KEY = "HITOMI_WEBP"
|
|
||||||
private const val WEBP_PREF_TITLE = "Webp pages"
|
|
||||||
private const val WEBP_PREF_SUMMARY = "Download webp pages instead of jpeg (when available)"
|
|
||||||
private const val WEBP_PREF_DEFAULT_VALUE = true
|
|
||||||
|
|
||||||
private const val AVIF_PREF_KEY = "HITOMI_AVIF"
|
|
||||||
private const val AVIF_PREF_TITLE = "Avif pages"
|
|
||||||
private const val AVIF_PREF_SUMMARY = "Download avif pages instead of jpeg or webp (when available)"
|
|
||||||
private const val AVIF_PREF_DEFAULT_VALUE = true
|
|
||||||
|
|
||||||
private const val COVER_PREF_KEY = "HITOMI_COVERS"
|
|
||||||
private const val COVER_PREF_TITLE = "Use HQ covers"
|
|
||||||
private const val COVER_PREF_SUMMARY = "See HQ covers while browsing"
|
|
||||||
private const val COVER_PREF_DEFAULT_VALUE = true
|
|
||||||
}
|
|
||||||
|
|
||||||
// Preferences
|
|
||||||
|
|
||||||
private val preferences: SharedPreferences by lazy {
|
|
||||||
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun setupPreferenceScreen(screen: AndroidXPreferenceScreen) {
|
|
||||||
val webpPref = AndroidXCheckBoxPreference(screen.context).apply {
|
|
||||||
key = "${WEBP_PREF_KEY}_$lang"
|
|
||||||
title = WEBP_PREF_TITLE
|
|
||||||
summary = WEBP_PREF_SUMMARY
|
|
||||||
setDefaultValue(WEBP_PREF_DEFAULT_VALUE)
|
|
||||||
|
|
||||||
setOnPreferenceChangeListener { _, newValue ->
|
|
||||||
val checkValue = newValue as Boolean
|
|
||||||
preferences.edit().putBoolean("${WEBP_PREF_KEY}_$lang", checkValue).commit()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val avifPref = AndroidXCheckBoxPreference(screen.context).apply {
|
|
||||||
key = "${AVIF_PREF_KEY}_$lang"
|
|
||||||
title = AVIF_PREF_TITLE
|
|
||||||
summary = AVIF_PREF_SUMMARY
|
|
||||||
setDefaultValue(AVIF_PREF_DEFAULT_VALUE)
|
|
||||||
|
|
||||||
setOnPreferenceChangeListener { _, newValue ->
|
|
||||||
val checkValue = newValue as Boolean
|
|
||||||
preferences.edit().putBoolean("${AVIF_PREF_KEY}_$lang", checkValue).commit()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val coverPref = AndroidXCheckBoxPreference(screen.context).apply {
|
|
||||||
key = "${COVER_PREF_KEY}_$lang"
|
|
||||||
title = COVER_PREF_TITLE
|
|
||||||
summary = COVER_PREF_SUMMARY
|
|
||||||
setDefaultValue(COVER_PREF_DEFAULT_VALUE)
|
|
||||||
|
|
||||||
setOnPreferenceChangeListener { _, newValue ->
|
|
||||||
val checkValue = newValue as Boolean
|
|
||||||
preferences.edit().putBoolean("${COVER_PREF_KEY}_$lang", checkValue).commit()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
screen.addPreference(webpPref)
|
|
||||||
screen.addPreference(avifPref)
|
|
||||||
screen.addPreference(coverPref)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun hitomiAlwaysWebp(): Boolean = preferences.getBoolean("${WEBP_PREF_KEY}_$lang", WEBP_PREF_DEFAULT_VALUE)
|
|
||||||
private fun hitomiAlwaysAvif(): Boolean = preferences.getBoolean("${AVIF_PREF_KEY}_$lang", AVIF_PREF_DEFAULT_VALUE)
|
|
||||||
private fun useHqThumbPref(): Boolean = preferences.getBoolean("${COVER_PREF_KEY}_$lang", COVER_PREF_DEFAULT_VALUE)
|
|
||||||
}
|
|
|
@ -1,38 +0,0 @@
|
||||||
package eu.kanade.tachiyomi.extension.all.hitomi
|
|
||||||
|
|
||||||
import android.app.Activity
|
|
||||||
import android.content.ActivityNotFoundException
|
|
||||||
import android.content.Intent
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.util.Log
|
|
||||||
import kotlin.system.exitProcess
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Springboard that accepts https://hitomi.la/cg/xxxx intents
|
|
||||||
* and redirects them to the main Tachiyomi process.
|
|
||||||
*/
|
|
||||||
class HitomiActivity : Activity() {
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
val pathSegments = intent?.data?.pathSegments
|
|
||||||
if (pathSegments != null && pathSegments.size > 1) {
|
|
||||||
val id = pathSegments[1]
|
|
||||||
val mainIntent = Intent().apply {
|
|
||||||
action = "eu.kanade.tachiyomi.SEARCH"
|
|
||||||
putExtra("query", "${Hitomi.PREFIX_ID_SEARCH}$id")
|
|
||||||
putExtra("filter", packageName)
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
startActivity(mainIntent)
|
|
||||||
} catch (e: ActivityNotFoundException) {
|
|
||||||
Log.e("HitomiActivity", e.toString())
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Log.e("HitomiActivity", "Could not parse URI from intent $intent")
|
|
||||||
}
|
|
||||||
|
|
||||||
finish()
|
|
||||||
exitProcess(0)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,16 +0,0 @@
|
||||||
package eu.kanade.tachiyomi.extension.all.hitomi
|
|
||||||
|
|
||||||
import kotlinx.serialization.Serializable
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class HitomiChapterDto(
|
|
||||||
val files: List<HitomiFileDto> = emptyList(),
|
|
||||||
)
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class HitomiFileDto(
|
|
||||||
val name: String,
|
|
||||||
val hasavif: Int,
|
|
||||||
val hash: String,
|
|
||||||
val haswebp: Int,
|
|
||||||
)
|
|
|
@ -1,50 +0,0 @@
|
||||||
package eu.kanade.tachiyomi.extension.all.hitomi
|
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.source.Source
|
|
||||||
import eu.kanade.tachiyomi.source.SourceFactory
|
|
||||||
|
|
||||||
class HitomiFactory : SourceFactory {
|
|
||||||
override fun createSources(): List<Source> = languageList
|
|
||||||
.map { Hitomi(it.first, it.second) }
|
|
||||||
}
|
|
||||||
|
|
||||||
private val languageList = listOf(
|
|
||||||
Pair("all", "all"), // all languages
|
|
||||||
Pair("id", "indonesian"),
|
|
||||||
Pair("ca", "catalan"),
|
|
||||||
Pair("ceb", "cebuano"),
|
|
||||||
Pair("cs", "czech"),
|
|
||||||
Pair("da", "danish"),
|
|
||||||
Pair("de", "german"),
|
|
||||||
Pair("et", "estonian"),
|
|
||||||
Pair("en", "english"),
|
|
||||||
Pair("es", "spanish"),
|
|
||||||
Pair("eo", "esperanto"),
|
|
||||||
Pair("fr", "french"),
|
|
||||||
Pair("it", "italian"),
|
|
||||||
Pair("la", "latin"),
|
|
||||||
Pair("hu", "hungarian"),
|
|
||||||
Pair("nl", "dutch"),
|
|
||||||
Pair("no", "norwegian"),
|
|
||||||
Pair("pl", "polish"),
|
|
||||||
Pair("pt-BR", "portuguese"),
|
|
||||||
Pair("ro", "romanian"),
|
|
||||||
Pair("sq", "albanian"),
|
|
||||||
Pair("sk", "slovak"),
|
|
||||||
Pair("fi", "finnish"),
|
|
||||||
Pair("sv", "swedish"),
|
|
||||||
Pair("tl", "tagalog"),
|
|
||||||
Pair("vi", "vietnamese"),
|
|
||||||
Pair("tr", "turkish"),
|
|
||||||
Pair("el", "greek"),
|
|
||||||
Pair("mn", "mongolian"),
|
|
||||||
Pair("ru", "russian"),
|
|
||||||
Pair("uk", "ukrainian"),
|
|
||||||
Pair("he", "hebrew"),
|
|
||||||
Pair("ar", "arabic"),
|
|
||||||
Pair("fa", "persian"),
|
|
||||||
Pair("th", "thai"),
|
|
||||||
Pair("ko", "korean"),
|
|
||||||
Pair("zh", "chinese"),
|
|
||||||
Pair("ja", "japanese")
|
|
||||||
)
|
|
|
@ -1,257 +0,0 @@
|
||||||
package eu.kanade.tachiyomi.extension.all.hitomi
|
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.extension.all.hitomi.Hitomi.Companion.LTN_BASE_URL
|
|
||||||
import eu.kanade.tachiyomi.network.GET
|
|
||||||
import eu.kanade.tachiyomi.network.asObservable
|
|
||||||
import eu.kanade.tachiyomi.network.asObservableSuccess
|
|
||||||
import okhttp3.Headers
|
|
||||||
import okhttp3.OkHttpClient
|
|
||||||
import okhttp3.Request
|
|
||||||
import rx.Observable
|
|
||||||
import rx.Single
|
|
||||||
import java.security.MessageDigest
|
|
||||||
|
|
||||||
private typealias HashedTerm = ByteArray
|
|
||||||
|
|
||||||
private data class DataPair(val offset: Long, val length: Int)
|
|
||||||
private data class Node(
|
|
||||||
val keys: List<ByteArray>,
|
|
||||||
val datas: List<DataPair>,
|
|
||||||
val subnodeAddresses: List<Long>
|
|
||||||
)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Kotlin port of the hitomi.la search algorithm
|
|
||||||
* @author NerdNumber9
|
|
||||||
*/
|
|
||||||
class HitomiNozomi(
|
|
||||||
private val client: OkHttpClient,
|
|
||||||
private val tagIndexVersion: Long,
|
|
||||||
private val galleriesIndexVersion: Long
|
|
||||||
) {
|
|
||||||
fun getGalleryIdsForQuery(query: String, language: String, popular: Boolean): Single<List<Int>> {
|
|
||||||
if (':' in query) {
|
|
||||||
val sides = query.split(':')
|
|
||||||
val namespace = sides[0]
|
|
||||||
var tag = sides[1]
|
|
||||||
|
|
||||||
var area: String? = namespace
|
|
||||||
if (namespace == "female" || namespace == "male") {
|
|
||||||
area = "tag"
|
|
||||||
tag = query
|
|
||||||
} else if (namespace == "language") {
|
|
||||||
return getGalleryIdsFromNozomi(null, "index", tag, popular)
|
|
||||||
}
|
|
||||||
|
|
||||||
return getGalleryIdsFromNozomi(area, tag, language, popular)
|
|
||||||
}
|
|
||||||
|
|
||||||
val key = hashTerm(query)
|
|
||||||
val field = "galleries"
|
|
||||||
|
|
||||||
return getNodeAtAddress(field, 0).flatMap { node ->
|
|
||||||
if (node == null) {
|
|
||||||
Single.just(null)
|
|
||||||
} else {
|
|
||||||
BSearch(field, key, node).flatMap { data ->
|
|
||||||
if (data == null) {
|
|
||||||
Single.just(null)
|
|
||||||
} else {
|
|
||||||
getGalleryIdsFromData(data)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getGalleryIdsFromData(data: DataPair?): Single<List<Int>> {
|
|
||||||
if (data == null) {
|
|
||||||
return Single.just(emptyList())
|
|
||||||
}
|
|
||||||
|
|
||||||
val url = "$LTN_BASE_URL/$GALLERIES_INDEX_DIR/galleries.$galleriesIndexVersion.data"
|
|
||||||
val (offset, length) = data
|
|
||||||
if (length > 100000000 || length <= 0) {
|
|
||||||
return Single.just(emptyList())
|
|
||||||
}
|
|
||||||
|
|
||||||
return client.newCall(rangedGet(url, offset, offset + length - 1))
|
|
||||||
.asObservable()
|
|
||||||
.map {
|
|
||||||
it.body?.bytes() ?: ByteArray(0)
|
|
||||||
}
|
|
||||||
.onErrorReturn { ByteArray(0) }
|
|
||||||
.map { inbuf ->
|
|
||||||
if (inbuf.isEmpty()) {
|
|
||||||
return@map emptyList<Int>()
|
|
||||||
}
|
|
||||||
|
|
||||||
val view = ByteCursor(inbuf)
|
|
||||||
val numberOfGalleryIds = view.nextInt()
|
|
||||||
|
|
||||||
val expectedLength = numberOfGalleryIds * 4 + 4
|
|
||||||
|
|
||||||
if (numberOfGalleryIds > 10000000 ||
|
|
||||||
numberOfGalleryIds <= 0 ||
|
|
||||||
inbuf.size != expectedLength
|
|
||||||
) {
|
|
||||||
return@map emptyList<Int>()
|
|
||||||
}
|
|
||||||
|
|
||||||
(1..numberOfGalleryIds).map {
|
|
||||||
view.nextInt()
|
|
||||||
}
|
|
||||||
}.toSingle()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Suppress("FunctionName")
|
|
||||||
private fun BSearch(field: String, key: ByteArray, node: Node?): Single<DataPair?> {
|
|
||||||
fun compareByteArrays(dv1: ByteArray, dv2: ByteArray): Int {
|
|
||||||
val top = dv1.size.coerceAtMost(dv2.size)
|
|
||||||
for (i in 0 until top) {
|
|
||||||
val dv1i = dv1[i].toInt() and 0xFF
|
|
||||||
val dv2i = dv2[i].toInt() and 0xFF
|
|
||||||
if (dv1i < dv2i) {
|
|
||||||
return -1
|
|
||||||
} else if (dv1i > dv2i) {
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
fun locateKey(key: ByteArray, node: Node): Pair<Boolean, Int> {
|
|
||||||
var cmpResult = -1
|
|
||||||
var lastI = 0
|
|
||||||
for (nodeKey in node.keys) {
|
|
||||||
cmpResult = compareByteArrays(key, nodeKey)
|
|
||||||
if (cmpResult <= 0) break
|
|
||||||
lastI++
|
|
||||||
}
|
|
||||||
return (cmpResult == 0) to lastI
|
|
||||||
}
|
|
||||||
|
|
||||||
fun isLeaf(node: Node): Boolean {
|
|
||||||
return !node.subnodeAddresses.any {
|
|
||||||
it != 0L
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (node == null || node.keys.isEmpty()) {
|
|
||||||
return Single.just(null)
|
|
||||||
}
|
|
||||||
|
|
||||||
val (there, where) = locateKey(key, node)
|
|
||||||
if (there) {
|
|
||||||
return Single.just(node.datas[where])
|
|
||||||
} else if (isLeaf(node)) {
|
|
||||||
return Single.just(null)
|
|
||||||
}
|
|
||||||
|
|
||||||
return getNodeAtAddress(field, node.subnodeAddresses[where]).flatMap { newNode ->
|
|
||||||
BSearch(field, key, newNode)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun decodeNode(data: ByteArray): Node {
|
|
||||||
val view = ByteCursor(data)
|
|
||||||
|
|
||||||
val numberOfKeys = view.nextInt()
|
|
||||||
|
|
||||||
val keys = (1..numberOfKeys).map {
|
|
||||||
val keySize = view.nextInt()
|
|
||||||
view.next(keySize)
|
|
||||||
}
|
|
||||||
|
|
||||||
val numberOfDatas = view.nextInt()
|
|
||||||
val datas = (1..numberOfDatas).map {
|
|
||||||
val offset = view.nextLong()
|
|
||||||
val length = view.nextInt()
|
|
||||||
DataPair(offset, length)
|
|
||||||
}
|
|
||||||
|
|
||||||
val numberOfSubnodeAddresses = B + 1
|
|
||||||
val subnodeAddresses = (1..numberOfSubnodeAddresses).map {
|
|
||||||
view.nextLong()
|
|
||||||
}
|
|
||||||
|
|
||||||
return Node(keys, datas, subnodeAddresses)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getNodeAtAddress(field: String, address: Long): Single<Node?> {
|
|
||||||
var url = "$LTN_BASE_URL/$INDEX_DIR/$field.$tagIndexVersion.index"
|
|
||||||
if (field == "galleries") {
|
|
||||||
url = "$LTN_BASE_URL/$GALLERIES_INDEX_DIR/galleries.$galleriesIndexVersion.index"
|
|
||||||
}
|
|
||||||
|
|
||||||
return client.newCall(rangedGet(url, address, address + MAX_NODE_SIZE - 1))
|
|
||||||
.asObservableSuccess()
|
|
||||||
.map {
|
|
||||||
it.body?.bytes() ?: ByteArray(0)
|
|
||||||
}
|
|
||||||
.onErrorReturn { ByteArray(0) }
|
|
||||||
.map { nodedata ->
|
|
||||||
if (nodedata.isNotEmpty()) {
|
|
||||||
decodeNode(nodedata)
|
|
||||||
} else null
|
|
||||||
}.toSingle()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getGalleryIdsFromNozomi(area: String?, tag: String, language: String, popular: Boolean): Single<List<Int>> {
|
|
||||||
val replacedTag = tag.replace('_', ' ')
|
|
||||||
var nozomiAddress = "$LTN_BASE_URL/$COMPRESSED_NOZOMI_PREFIX/$replacedTag-$language$NOZOMI_EXTENSION"
|
|
||||||
if (area != null) {
|
|
||||||
nozomiAddress = if (popular) {
|
|
||||||
"$LTN_BASE_URL/$COMPRESSED_NOZOMI_PREFIX/$area/popular/$replacedTag-$language$NOZOMI_EXTENSION"
|
|
||||||
} else {
|
|
||||||
"$LTN_BASE_URL/$COMPRESSED_NOZOMI_PREFIX/$area/$replacedTag-$language$NOZOMI_EXTENSION"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return client.newCall(
|
|
||||||
Request.Builder()
|
|
||||||
.url(nozomiAddress)
|
|
||||||
.build()
|
|
||||||
)
|
|
||||||
.asObservableSuccess()
|
|
||||||
.map { resp ->
|
|
||||||
val body = resp.body!!.bytes()
|
|
||||||
val cursor = ByteCursor(body)
|
|
||||||
(1..body.size / 4).map {
|
|
||||||
cursor.nextInt()
|
|
||||||
}
|
|
||||||
}.toSingle()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun hashTerm(query: String): HashedTerm {
|
|
||||||
val md = MessageDigest.getInstance("SHA-256")
|
|
||||||
md.update(query.toByteArray(HASH_CHARSET))
|
|
||||||
return md.digest().copyOf(4)
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
private const val INDEX_DIR = "tagindex"
|
|
||||||
private const val GALLERIES_INDEX_DIR = "galleriesindex"
|
|
||||||
private const val COMPRESSED_NOZOMI_PREFIX = "n"
|
|
||||||
private const val NOZOMI_EXTENSION = ".nozomi"
|
|
||||||
private const val MAX_NODE_SIZE = 464
|
|
||||||
private const val B = 16
|
|
||||||
|
|
||||||
private val HASH_CHARSET = Charsets.UTF_8
|
|
||||||
|
|
||||||
fun rangedGet(url: String, rangeBegin: Long, rangeEnd: Long?): Request {
|
|
||||||
return GET(
|
|
||||||
url,
|
|
||||||
Headers.Builder()
|
|
||||||
.add("Range", "bytes=$rangeBegin-${rangeEnd ?: ""}")
|
|
||||||
.build()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getIndexVersion(httpClient: OkHttpClient, name: String): Observable<Long> {
|
|
||||||
return httpClient.newCall(GET("$LTN_BASE_URL/$name/version?_=${System.currentTimeMillis()}"))
|
|
||||||
.asObservableSuccess()
|
|
||||||
.map { it.body!!.string().toLong() }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
Loading…
Reference in New Issue