diff --git a/src/all/hitomi/AndroidManifest.xml b/src/all/hitomi/AndroidManifest.xml
deleted file mode 100644
index 10e2eea17..000000000
--- a/src/all/hitomi/AndroidManifest.xml
+++ /dev/null
@@ -1,36 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/src/all/hitomi/build.gradle b/src/all/hitomi/build.gradle
deleted file mode 100644
index 94c1905d1..000000000
--- a/src/all/hitomi/build.gradle
+++ /dev/null
@@ -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"
diff --git a/src/all/hitomi/res/mipmap-hdpi/ic_launcher.png b/src/all/hitomi/res/mipmap-hdpi/ic_launcher.png
deleted file mode 100644
index a534633b8..000000000
Binary files a/src/all/hitomi/res/mipmap-hdpi/ic_launcher.png and /dev/null differ
diff --git a/src/all/hitomi/res/mipmap-mdpi/ic_launcher.png b/src/all/hitomi/res/mipmap-mdpi/ic_launcher.png
deleted file mode 100644
index 5c6f3b8f8..000000000
Binary files a/src/all/hitomi/res/mipmap-mdpi/ic_launcher.png and /dev/null differ
diff --git a/src/all/hitomi/res/mipmap-xhdpi/ic_launcher.png b/src/all/hitomi/res/mipmap-xhdpi/ic_launcher.png
deleted file mode 100644
index f2477f79a..000000000
Binary files a/src/all/hitomi/res/mipmap-xhdpi/ic_launcher.png and /dev/null differ
diff --git a/src/all/hitomi/res/mipmap-xxhdpi/ic_launcher.png b/src/all/hitomi/res/mipmap-xxhdpi/ic_launcher.png
deleted file mode 100644
index 48549d3f4..000000000
Binary files a/src/all/hitomi/res/mipmap-xxhdpi/ic_launcher.png and /dev/null differ
diff --git a/src/all/hitomi/res/mipmap-xxxhdpi/ic_launcher.png b/src/all/hitomi/res/mipmap-xxxhdpi/ic_launcher.png
deleted file mode 100644
index 97f74cc71..000000000
Binary files a/src/all/hitomi/res/mipmap-xxxhdpi/ic_launcher.png and /dev/null differ
diff --git a/src/all/hitomi/res/web_hi_res_512.png b/src/all/hitomi/res/web_hi_res_512.png
deleted file mode 100644
index a851e8798..000000000
Binary files a/src/all/hitomi/res/web_hi_res_512.png and /dev/null differ
diff --git a/src/all/hitomi/src/eu/kanade/tachiyomi/extension/all/hitomi/ByteCursor.kt b/src/all/hitomi/src/eu/kanade/tachiyomi/extension/all/hitomi/ByteCursor.kt
deleted file mode 100644
index 1c6b03061..000000000
--- a/src/all/hitomi/src/eu/kanade/tachiyomi/extension/all/hitomi/ByteCursor.kt
+++ /dev/null
@@ -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
-}
diff --git a/src/all/hitomi/src/eu/kanade/tachiyomi/extension/all/hitomi/Hitomi.kt b/src/all/hitomi/src/eu/kanade/tachiyomi/extension/all/hitomi/Hitomi.kt
deleted file mode 100644
index 2d6954d37..000000000
--- a/src/all/hitomi/src/eu/kanade/tachiyomi/extension/all/hitomi/Hitomi.kt
+++ /dev/null
@@ -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 {
- 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 {
- 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> {
- val cursor = ByteCursor(array)
- val ids = (1..array.size / 4).map {
- cursor.nextInt()
- }
-
- return nozomiIdsToMangas(ids).toObservable()
- }
-
- private fun nozomiIdsToMangas(ids: List): Single> {
- 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 {
- 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 {
- 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 {
- 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 {
- 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()
- .joinToString("") {
- (it as UriPartFilter).toUriPart()
- }
- val keyword = filters.filterIsInstance().toString()
- .replace("[", "").replace("]", "")
- val popular = filters.filterIsInstance()
- .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>,
- defaultState: Int = 0
- ) : Filter.Select(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> {
- 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 {
- 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(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().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)
-}
diff --git a/src/all/hitomi/src/eu/kanade/tachiyomi/extension/all/hitomi/HitomiActivity.kt b/src/all/hitomi/src/eu/kanade/tachiyomi/extension/all/hitomi/HitomiActivity.kt
deleted file mode 100644
index 324595d34..000000000
--- a/src/all/hitomi/src/eu/kanade/tachiyomi/extension/all/hitomi/HitomiActivity.kt
+++ /dev/null
@@ -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)
- }
-}
diff --git a/src/all/hitomi/src/eu/kanade/tachiyomi/extension/all/hitomi/HitomiDto.kt b/src/all/hitomi/src/eu/kanade/tachiyomi/extension/all/hitomi/HitomiDto.kt
deleted file mode 100644
index a38e49982..000000000
--- a/src/all/hitomi/src/eu/kanade/tachiyomi/extension/all/hitomi/HitomiDto.kt
+++ /dev/null
@@ -1,16 +0,0 @@
-package eu.kanade.tachiyomi.extension.all.hitomi
-
-import kotlinx.serialization.Serializable
-
-@Serializable
-data class HitomiChapterDto(
- val files: List = emptyList(),
-)
-
-@Serializable
-data class HitomiFileDto(
- val name: String,
- val hasavif: Int,
- val hash: String,
- val haswebp: Int,
-)
diff --git a/src/all/hitomi/src/eu/kanade/tachiyomi/extension/all/hitomi/HitomiFactory.kt b/src/all/hitomi/src/eu/kanade/tachiyomi/extension/all/hitomi/HitomiFactory.kt
deleted file mode 100644
index 3978ca8d5..000000000
--- a/src/all/hitomi/src/eu/kanade/tachiyomi/extension/all/hitomi/HitomiFactory.kt
+++ /dev/null
@@ -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 = 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")
-)
diff --git a/src/all/hitomi/src/eu/kanade/tachiyomi/extension/all/hitomi/HitomiNozomi.kt b/src/all/hitomi/src/eu/kanade/tachiyomi/extension/all/hitomi/HitomiNozomi.kt
deleted file mode 100644
index 9ec7d1c62..000000000
--- a/src/all/hitomi/src/eu/kanade/tachiyomi/extension/all/hitomi/HitomiNozomi.kt
+++ /dev/null
@@ -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,
- val datas: List,
- val subnodeAddresses: List
-)
-
-/**
- * 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> {
- 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> {
- 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()
- }
-
- val view = ByteCursor(inbuf)
- val numberOfGalleryIds = view.nextInt()
-
- val expectedLength = numberOfGalleryIds * 4 + 4
-
- if (numberOfGalleryIds > 10000000 ||
- numberOfGalleryIds <= 0 ||
- inbuf.size != expectedLength
- ) {
- return@map emptyList()
- }
-
- (1..numberOfGalleryIds).map {
- view.nextInt()
- }
- }.toSingle()
- }
-
- @Suppress("FunctionName")
- private fun BSearch(field: String, key: ByteArray, node: Node?): Single {
- 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 {
- 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 {
- 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> {
- 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 {
- return httpClient.newCall(GET("$LTN_BASE_URL/$name/version?_=${System.currentTimeMillis()}"))
- .asObservableSuccess()
- .map { it.body!!.string().toLong() }
- }
- }
-}