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() } - } - } -}