From b001fbfff30579c8e1d05771d107f1c68e858c00 Mon Sep 17 00:00:00 2001 From: AwkwardPeak7 <48650614+AwkwardPeak7@users.noreply.github.com> Date: Thu, 25 Jan 2024 23:24:26 +0500 Subject: [PATCH] add hitomi.la (#581) * hitomi.la * source factory * suggestions * sort filter --- src/all/hitomi/AndroidManifest.xml | 2 + src/all/hitomi/build.gradle | 8 + .../hitomi/res/mipmap-hdpi/ic_launcher.png | Bin 0 -> 3480 bytes .../hitomi/res/mipmap-mdpi/ic_launcher.png | Bin 0 -> 2006 bytes .../hitomi/res/mipmap-xhdpi/ic_launcher.png | Bin 0 -> 4477 bytes .../hitomi/res/mipmap-xxhdpi/ic_launcher.png | Bin 0 -> 8090 bytes .../hitomi/res/mipmap-xxxhdpi/ic_launcher.png | Bin 0 -> 11479 bytes .../tachiyomi/extension/all/hitomi/Hitomi.kt | 615 ++++++++++++++++++ .../extension/all/hitomi/HitomiDto.kt | 82 +++ .../extension/all/hitomi/HitomiFactory.kt | 34 + 10 files changed, 741 insertions(+) create mode 100644 src/all/hitomi/AndroidManifest.xml create mode 100644 src/all/hitomi/build.gradle create mode 100644 src/all/hitomi/res/mipmap-hdpi/ic_launcher.png create mode 100644 src/all/hitomi/res/mipmap-mdpi/ic_launcher.png create mode 100644 src/all/hitomi/res/mipmap-xhdpi/ic_launcher.png create mode 100644 src/all/hitomi/res/mipmap-xxhdpi/ic_launcher.png create mode 100644 src/all/hitomi/res/mipmap-xxxhdpi/ic_launcher.png create mode 100644 src/all/hitomi/src/eu/kanade/tachiyomi/extension/all/hitomi/Hitomi.kt create mode 100644 src/all/hitomi/src/eu/kanade/tachiyomi/extension/all/hitomi/HitomiDto.kt create mode 100644 src/all/hitomi/src/eu/kanade/tachiyomi/extension/all/hitomi/HitomiFactory.kt diff --git a/src/all/hitomi/AndroidManifest.xml b/src/all/hitomi/AndroidManifest.xml new file mode 100644 index 000000000..8072ee00d --- /dev/null +++ b/src/all/hitomi/AndroidManifest.xml @@ -0,0 +1,2 @@ + + diff --git a/src/all/hitomi/build.gradle b/src/all/hitomi/build.gradle new file mode 100644 index 000000000..c32dfc2a7 --- /dev/null +++ b/src/all/hitomi/build.gradle @@ -0,0 +1,8 @@ +ext { + extName = 'Hitomi' + extClass = '.HitomiFactory' + extVersionCode = 25 + 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 new file mode 100644 index 0000000000000000000000000000000000000000..4c66ae35deb07691c0296f280b22723af3494beb GIT binary patch literal 3480 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 LdTM1VHevq<2D=U* literal 0 HcmV?d00001 diff --git a/src/all/hitomi/res/mipmap-xxhdpi/ic_launcher.png b/src/all/hitomi/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..6a56665d6e474864a93c3cfaea47c3f1153c1104 GIT binary patch literal 8090 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 literal 0 HcmV?d00001 diff --git a/src/all/hitomi/res/mipmap-xxxhdpi/ic_launcher.png b/src/all/hitomi/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..55a36d8062585cae811d9fb341bd1ad5a68cf801 GIT binary patch literal 11479 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 Q<5ULEpQ*@~%b5iKACnN7M*si- literal 0 HcmV?d00001 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 new file mode 100644 index 000000000..27c4a26d9 --- /dev/null +++ b/src/all/hitomi/src/eu/kanade/tachiyomi/extension/all/hitomi/Hitomi.kt @@ -0,0 +1,615 @@ +package eu.kanade.tachiyomi.extension.all.hitomi + +import eu.kanade.tachiyomi.network.GET +import eu.kanade.tachiyomi.network.await +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.model.UpdateStrategy +import eu.kanade.tachiyomi.source.online.HttpSource +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import kotlinx.serialization.decodeFromString +import kotlinx.serialization.json.Json +import okhttp3.Call +import okhttp3.Request +import okhttp3.Response +import rx.Observable +import uy.kohesive.injekt.injectLazy +import java.nio.ByteBuffer +import java.nio.ByteOrder +import java.security.MessageDigest +import java.text.SimpleDateFormat +import java.util.LinkedList +import java.util.Locale +import kotlin.math.min + +@OptIn(ExperimentalUnsignedTypes::class) +class Hitomi( + override val lang: String, + private val nozomiLang: String, +) : HttpSource() { + + override val name = "Hitomi" + + private val domain = "hitomi.la" + + override val baseUrl = "https://$domain" + + private val ltnUrl = "https://ltn.$domain" + + override val supportsLatest = true + + private val json: Json by injectLazy() + + override val client = network.cloudflareClient + + override fun headersBuilder() = super.headersBuilder() + .set("referer", "$baseUrl/") + .set("origin", baseUrl) + + override fun fetchPopularManga(page: Int): Observable = Observable.fromCallable { + runBlocking { getPopularManga(page) } + } + + private suspend fun getPopularManga(page: Int): MangasPage { + val entries = getGalleryIDsFromNozomi("popular", "today", nozomiLang, page.nextPageRange()) + .toMangaList() + + return MangasPage(entries, entries.size >= 24) + } + + override fun fetchLatestUpdates(page: Int): Observable = Observable.fromCallable { + runBlocking { getLatestUpdates(page) } + } + + private suspend fun getLatestUpdates(page: Int): MangasPage { + val entries = getGalleryIDsFromNozomi(null, "index", nozomiLang, page.nextPageRange()) + .toMangaList() + + return MangasPage(entries, entries.size >= 24) + } + + private lateinit var searchResponse: List + + override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable = Observable.fromCallable { + runBlocking { getSearchManga(page, query, filters) } + } + + private suspend fun getSearchManga(page: Int, query: String, filters: FilterList): MangasPage { + if (page == 1) { + searchResponse = hitomiSearch( + query.trim(), + filters.filterIsInstance().firstOrNull()?.state == 0, + nozomiLang, + ).toList() + } + + val end = min(page * 25, searchResponse.size) + val entries = searchResponse.subList((page - 1) * 25, end) + .toMangaList() + + return MangasPage(entries, end != searchResponse.size) + } + + private class SortFilter : Filter.Select("Sort By", arrayOf("Popularity", "Updated")) + + override fun getFilterList(): FilterList { + return FilterList(SortFilter()) + } + + private fun Int.nextPageRange(): LongRange { + val byteOffset = ((this - 1) * 25) * 4L + return byteOffset.until(byteOffset + 100) + } + + private suspend fun getRangedResponse(url: String, range: LongRange?): ByteArray { + val rangeHeaders = when (range) { + null -> headers + else -> headersBuilder() + .set("Range", "bytes=${range.first}-${range.last}") + .build() + } + + return client.newCall(GET(url, rangeHeaders)).awaitSuccess().use { it.body.bytes() } + } + + private suspend fun hitomiSearch( + query: String, + sortByPopularity: Boolean = false, + language: String = "all", + ): Set = + coroutineScope { + val terms = query + .trim() + .replace(Regex("""^\?"""), "") + .lowercase() + .split(Regex("\\s+")) + .map { + it.replace('_', ' ') + } + + val positiveTerms = LinkedList() + val negativeTerms = LinkedList() + + for (term in terms) { + if (term.startsWith("-")) { + negativeTerms.push(term.removePrefix("-")) + } else if (term.isNotBlank()) { + positiveTerms.push(term) + } + } + + val positiveResults = positiveTerms.map { + async { + runCatching { + getGalleryIDsForQuery(it, language) + }.getOrDefault(emptySet()) + } + } + + val negativeResults = negativeTerms.map { + async { + runCatching { + getGalleryIDsForQuery(it, language) + }.getOrDefault(emptySet()) + } + } + + val results = when { + sortByPopularity -> getGalleryIDsFromNozomi(null, "popular", language) + positiveTerms.isEmpty() -> getGalleryIDsFromNozomi(null, "index", language) + else -> emptySet() + }.toMutableSet() + + fun filterPositive(newResults: Set) { + when { + results.isEmpty() -> results.addAll(newResults) + else -> results.retainAll(newResults) + } + } + + fun filterNegative(newResults: Set) { + results.removeAll(newResults) + } + + // positive results + positiveResults.forEach { + filterPositive(it.await()) + } + + // negative results + negativeResults.forEach { + filterNegative(it.await()) + } + + results + } + + // search.js + private suspend fun getGalleryIDsForQuery( + query: String, + language: String = "all", + ): Set { + query.replace("_", " ").let { + if (it.indexOf(':') > -1) { + val sides = it.split(":") + val ns = sides[0] + var tag = sides[1] + + var area: String? = ns + var lang = language + when (ns) { + "female", "male" -> { + area = "tag" + tag = it + } + + "language" -> { + area = null + lang = tag + tag = "index" + } + } + + return getGalleryIDsFromNozomi(area, tag, lang) + } + + val key = hashTerm(it) + val node = getGalleryNodeAtAddress(0) + val data = bSearch(key, node) ?: return emptySet() + + return getGalleryIDsFromData(data) + } + } + + private suspend fun getGalleryIDsFromData(data: Pair): Set { + val url = "$ltnUrl/galleriesindex/galleries.$galleriesIndexVersion.data" + val (offset, length) = data + require(length in 1..100000000) { + "Length $length is too long" + } + + val inbuf = getRangedResponse(url, offset.until(offset + length)) + + val galleryIDs = mutableSetOf() + + val buffer = + ByteBuffer + .wrap(inbuf) + .order(ByteOrder.BIG_ENDIAN) + + val numberOfGalleryIDs = buffer.int + + val expectedLength = numberOfGalleryIDs * 4 + 4 + + require(numberOfGalleryIDs in 1..10000000) { + "number_of_galleryids $numberOfGalleryIDs is too long" + } + require(inbuf.size == expectedLength) { + "inbuf.byteLength ${inbuf.size} != expected_length $expectedLength" + } + + for (i in 0.until(numberOfGalleryIDs)) + galleryIDs.add(buffer.int) + + return galleryIDs + } + + private tailrec suspend fun bSearch( + key: UByteArray, + node: Node, + ): Pair? { + fun compareArrayBuffers( + dv1: UByteArray, + dv2: UByteArray, + ): Int { + val top = min(dv1.size, dv2.size) + + for (i in 0.until(top)) { + if (dv1[i] < dv2[i]) { + return -1 + } else if (dv1[i] > dv2[i]) { + return 1 + } + } + + return 0 + } + + fun locateKey( + key: UByteArray, + node: Node, + ): Pair { + for (i in node.keys.indices) { + val cmpResult = compareArrayBuffers(key, node.keys[i]) + + if (cmpResult <= 0) { + return Pair(cmpResult == 0, i) + } + } + + return Pair(false, node.keys.size) + } + + fun isLeaf(node: Node): Boolean { + for (subnode in node.subNodeAddresses) + if (subnode != 0L) { + return false + } + + return true + } + + if (node.keys.isEmpty()) { + return null + } + + val (there, where) = locateKey(key, node) + if (there) { + return node.datas[where] + } else if (isLeaf(node)) { + return null + } + + val nextNode = getGalleryNodeAtAddress(node.subNodeAddresses[where]) + return bSearch(key, nextNode) + } + + private suspend fun getGalleryIDsFromNozomi( + area: String?, + tag: String, + language: String, + range: LongRange? = null, + ): Set { + val nozomiAddress = when (area) { + null -> "$ltnUrl/$tag-$language.nozomi" + else -> "$ltnUrl/$area/$tag-$language.nozomi" + } + + val bytes = getRangedResponse(nozomiAddress, range) + val nozomi = mutableSetOf() + + val arrayBuffer = ByteBuffer + .wrap(bytes) + .order(ByteOrder.BIG_ENDIAN) + + while (arrayBuffer.hasRemaining()) + nozomi.add(arrayBuffer.int) + + return nozomi + } + + private val galleriesIndexVersion by lazy { + client.newCall( + GET("$ltnUrl/galleriesindex/version?_=${System.currentTimeMillis()}", headers), + ).execute().use { it.body.string() } + } + + private data class Node( + val keys: List, + val datas: List>, + val subNodeAddresses: List, + ) + + private fun decodeNode(data: ByteArray): Node { + val buffer = ByteBuffer + .wrap(data) + .order(ByteOrder.BIG_ENDIAN) + + val uData = data.toUByteArray() + + val numberOfKeys = buffer.int + val keys = ArrayList() + + for (i in 0.until(numberOfKeys)) { + val keySize = buffer.int + + if (keySize == 0 || keySize > 32) { + throw Exception("fatal: !keySize || keySize > 32") + } + + keys.add(uData.sliceArray(buffer.position().until(buffer.position() + keySize))) + buffer.position(buffer.position() + keySize) + } + + val numberOfDatas = buffer.int + val datas = ArrayList>() + + for (i in 0.until(numberOfDatas)) { + val offset = buffer.long + val length = buffer.int + + datas.add(Pair(offset, length)) + } + + val numberOfSubNodeAddresses = 16 + 1 + val subNodeAddresses = ArrayList() + + for (i in 0.until(numberOfSubNodeAddresses)) { + val subNodeAddress = buffer.long + subNodeAddresses.add(subNodeAddress) + } + + return Node(keys, datas, subNodeAddresses) + } + + private suspend fun getGalleryNodeAtAddress(address: Long): Node { + val url = "$ltnUrl/galleriesindex/galleries.$galleriesIndexVersion.index" + + val nodedata = getRangedResponse(url, address.until(address + 464)) + + return decodeNode(nodedata) + } + + private fun hashTerm(term: String): UByteArray { + return sha256(term.toByteArray()).copyOfRange(0, 4).toUByteArray() + } + + private fun sha256(data: ByteArray): ByteArray { + return MessageDigest.getInstance("SHA-256").digest(data) + } + + private suspend fun Collection.toMangaList() = coroutineScope { + map { id -> + async { + runCatching { + client.newCall(GET("$ltnUrl/galleries/$id.js", headers)) + .awaitSuccess() + .parseScriptAs() + .toSManga() + }.getOrNull() + } + }.awaitAll().filterNotNull() + } + + private suspend fun Gallery.toSManga() = SManga.create().apply { + title = this@toSManga.title + url = galleryurl + author = groups?.joinToString { it.formatted } + artist = artists?.joinToString { it.formatted } + genre = tags?.joinToString { it.formatted } + thumbnail_url = files.first().let { + val hash = it.hash + val imageId = imageIdFromHash(hash) + val subDomain = 'a' + subdomainOffset(imageId) + + "https://${subDomain}tn.$domain/webpbigtn/${thumbPathFromHash(hash)}/$hash.webp" + } + description = buildString { + characters?.joinToString { it.formatted }?.let { + append("Characters: ", it, "\n") + } + parodys?.joinToString { it.formatted }?.let { + append("Parodies: ", it, "\n") + } + } + status = SManga.COMPLETED + update_strategy = UpdateStrategy.ONLY_FETCH_ONCE + initialized = true + } + + override fun mangaDetailsRequest(manga: SManga): Request { + val id = manga.url + .substringAfterLast("-") + .substringBefore(".") + + return GET("$ltnUrl/galleries/$id.js", headers) + } + + override fun mangaDetailsParse(response: Response): SManga { + return response.parseScriptAs().let { + runBlocking { it.toSManga() } + } + } + + override fun getMangaUrl(manga: SManga) = baseUrl + manga.url + + override fun chapterListRequest(manga: SManga): Request { + val id = manga.url + .substringAfterLast("-") + .substringBefore(".") + + return GET("$ltnUrl/galleries/$id.js#${manga.url}", headers) + } + + override fun chapterListParse(response: Response): List { + val gallery = response.parseScriptAs() + val mangaUrl = response.request.url.fragment!! + + return listOf( + SChapter.create().apply { + name = "Chapter" + url = mangaUrl + scanlator = gallery.type + date_upload = runCatching { + dateFormat.parse(gallery.date.substringBeforeLast("-"))!!.time + }.getOrDefault(0L) + }, + ) + } + + private val dateFormat = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.ENGLISH) + + override fun getChapterUrl(chapter: SChapter) = baseUrl + chapter.url + + override fun pageListRequest(chapter: SChapter): Request { + val id = chapter.url + .substringAfterLast("-") + .substringBefore(".") + + return GET("$ltnUrl/galleries/$id.js", headers) + } + + override fun pageListParse(response: Response): List { + val gallery = response.parseScriptAs() + + return gallery.files.mapIndexed { idx, img -> + runBlocking { + val hash = img.hash + val commonId = commonImageId() + val imageId = imageIdFromHash(hash) + val subDomain = 'a' + subdomainOffset(imageId) + + Page( + idx, + "$baseUrl/reader/$id.html", + "https://${subDomain}a.$domain/webp/$commonId$imageId/$hash.webp", + ) + } + } + } + + override fun imageRequest(page: Page): Request { + val imageHeaders = headersBuilder() + .set("Accept", "image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8") + .set("Referer", page.url) + .build() + + return GET(page.imageUrl!!, imageHeaders) + } + + private inline fun Response.parseScriptAs(): T = + parseAs { it.substringAfter("var galleryinfo = ") } + + private inline fun Response.parseAs(transform: (String) -> String = { body -> body }): T { + val body = use { it.body.string() } + val transformed = transform(body) + + return json.decodeFromString(transformed) + } + + private suspend fun Call.awaitSuccess() = + await().also { + require(it.isSuccessful) { + it.close() + "HTTP error ${it.code}" + } + } + + // ------------------ gg.js ------------------ + private var scriptLastRetrieval: Long? = null + private val mutex = Mutex() + private var subdomainOffsetDefault = 0 + private val subdomainOffsetMap = mutableMapOf() + private var commonImageId = "" + + private suspend fun refreshScript() = mutex.withLock { + if (scriptLastRetrieval == null || (scriptLastRetrieval!! + 60000) < System.currentTimeMillis()) { + val ggScript = client.newCall( + GET("$ltnUrl/gg.js?_=${System.currentTimeMillis()}", headers), + ).awaitSuccess().use { it.body.string() } + + subdomainOffsetDefault = Regex("var o = (\\d)").find(ggScript)!!.groupValues[1].toInt() + val o = Regex("o = (\\d); break;").find(ggScript)!!.groupValues[1].toInt() + + subdomainOffsetMap.clear() + Regex("case (\\d+):").findAll(ggScript).forEach { + val case = it.groupValues[1].toInt() + subdomainOffsetMap[case] = o + } + + commonImageId = Regex("b: '(.+)'").find(ggScript)!!.groupValues[1] + + scriptLastRetrieval = System.currentTimeMillis() + } + } + + // m <-- gg.js + private suspend fun subdomainOffset(imageId: Int): Int { + refreshScript() + return subdomainOffsetMap[imageId] ?: subdomainOffsetDefault + } + + // b <-- gg.js + private suspend fun commonImageId(): String { + refreshScript() + return commonImageId + } + + // s <-- gg.js + private fun imageIdFromHash(hash: String): Int { + val match = Regex("(..)(.)$").find(hash) + return match!!.groupValues.let { it[2] + it[1] }.toInt(16) + } + + // real_full_path_from_hash <-- common.js + private fun thumbPathFromHash(hash: String): String { + return hash.replace(Regex("""^.*(..)(.)$"""), "$2/$1") + } + + override fun popularMangaParse(response: Response) = throw UnsupportedOperationException() + override fun popularMangaRequest(page: Int) = throw UnsupportedOperationException() + override fun latestUpdatesRequest(page: Int) = throw UnsupportedOperationException() + override fun latestUpdatesParse(response: Response) = throw UnsupportedOperationException() + override fun searchMangaRequest(page: Int, query: String, filters: FilterList) = throw UnsupportedOperationException() + override fun searchMangaParse(response: Response) = throw UnsupportedOperationException() + override fun imageUrlParse(response: Response) = throw UnsupportedOperationException() +} 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 new file mode 100644 index 000000000..0e5a6d5be --- /dev/null +++ b/src/all/hitomi/src/eu/kanade/tachiyomi/extension/all/hitomi/HitomiDto.kt @@ -0,0 +1,82 @@ +package eu.kanade.tachiyomi.extension.all.hitomi + +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.JsonPrimitive + +@Serializable +data class Gallery( + val galleryurl: String, + val title: String, + val date: String, + val type: String, + val tags: List?, + val artists: List?, + val groups: List?, + val characters: List?, + val parodys: List?, + val files: List, +) + +@Serializable +data class ImageFile( + val hash: String, +) + +@Serializable +data class Tag( + val female: JsonPrimitive?, + val male: JsonPrimitive?, + val tag: String, +) { + val formatted get() = if (female?.content == "1") { + "${tag.toCamelCase()} (Female)" + } else if (male?.content == "1") { + "${tag.toCamelCase()} (Male)" + } else { + tag.toCamelCase() + } +} + +@Serializable +data class Artist( + val artist: String, +) { + val formatted get() = artist.toCamelCase() +} + +@Serializable +data class Group( + val group: String, +) { + val formatted get() = group.toCamelCase() +} + +@Serializable +data class Character( + val character: String, +) { + val formatted get() = character.toCamelCase() +} + +@Serializable +data class Parody( + val parody: String, +) { + val formatted get() = parody.toCamelCase() +} + +private fun String.toCamelCase(): String { + val result = StringBuilder(length) + var capitalize = true + for (char in this) { + result.append( + if (capitalize) { + char.uppercase() + } else { + char.lowercase() + }, + ) + capitalize = char.isWhitespace() + } + return result.toString() +} 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 new file mode 100644 index 000000000..fd3667021 --- /dev/null +++ b/src/all/hitomi/src/eu/kanade/tachiyomi/extension/all/hitomi/HitomiFactory.kt @@ -0,0 +1,34 @@ +package eu.kanade.tachiyomi.extension.all.hitomi + +import eu.kanade.tachiyomi.source.SourceFactory + +class HitomiFactory : SourceFactory { + override fun createSources() = listOf( + Hitomi("all", "all"), + Hitomi("en", "english"), + Hitomi("id", "indonesian"), + Hitomi("jv", "javanese"), + Hitomi("ca", "catalan"), + Hitomi("ceb", "cebuano"), + Hitomi("cs", "czech"), + Hitomi("da", "danish"), + Hitomi("de", "german"), + Hitomi("et", "estonian"), + Hitomi("es", "spanish"), + Hitomi("eo", "esperanto"), + Hitomi("fr", "french"), + Hitomi("it", "italian"), + Hitomi("hi", "hindi"), + Hitomi("hu", "hungarian"), + Hitomi("pl", "polish"), + Hitomi("pt", "portuguese"), + Hitomi("vi", "vietnamese"), + Hitomi("tr", "turkish"), + Hitomi("ru", "russian"), + Hitomi("uk", "ukrainian"), + Hitomi("ar", "arabic"), + Hitomi("ko", "korean"), + Hitomi("zh", "chinese"), + Hitomi("ja", "japanese"), + ) +}