Initial commit

This commit is contained in:
almightyhak 2024-06-20 11:54:12 +07:00
commit 98ed7e8839
2263 changed files with 108711 additions and 0 deletions

View file

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application>
<activity
android:name=".tr.hentaizm.HentaiZMUrlActivity"
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="www.hentaizm.fun"
android:pathPattern="/hentai-detay/..*"
android:scheme="https" />
</intent-filter>
</activity>
</application>
</manifest>

View file

@ -0,0 +1,8 @@
ext {
extName = 'HentaiZM'
extClass = '.HentaiZM'
extVersionCode = 1
isNsfw = true
}
apply from: "$rootDir/common.gradle"

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View file

@ -0,0 +1,227 @@
package eu.kanade.tachiyomi.animeextension.tr.hentaizm
import android.app.Application
import androidx.preference.ListPreference
import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.animeextension.tr.hentaizm.extractors.VideaExtractor
import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
import eu.kanade.tachiyomi.animesource.model.AnimesPage
import eu.kanade.tachiyomi.animesource.model.SAnime
import eu.kanade.tachiyomi.animesource.model.SEpisode
import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.animesource.online.ParsedAnimeHttpSource
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.POST
import eu.kanade.tachiyomi.network.awaitSuccess
import eu.kanade.tachiyomi.util.asJsoup
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import okhttp3.FormBody
import okhttp3.Request
import okhttp3.Response
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
class HentaiZM : ParsedAnimeHttpSource(), ConfigurableAnimeSource {
override val name = "HentaiZM"
override val baseUrl = "https://www.hentaizm.fun"
override val lang = "tr"
override val supportsLatest = true
override fun headersBuilder() = super.headersBuilder()
.add("Origin", baseUrl)
.add("Referer", "$baseUrl/")
private val preferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
}
init {
runBlocking {
withContext(Dispatchers.IO) {
val body = FormBody.Builder()
.add("user", "demo")
.add("pass", "demo") // peak security
.add("redirect_to", baseUrl)
.build()
val headers = headersBuilder()
.add("X-Requested-With", "XMLHttpRequest")
.build()
client.newCall(POST("$baseUrl/giris", headers, body)).execute()
.close()
}
}
}
// ============================== Popular ===============================
override fun popularAnimeRequest(page: Int) = GET("$baseUrl/en-cok-izlenenler/page/$page", headers)
override fun popularAnimeParse(response: Response) =
super.popularAnimeParse(response).let { page ->
val animes = page.animes.distinctBy { it.url }
AnimesPage(animes, page.hasNextPage)
}
override fun popularAnimeSelector() = "div.moviefilm"
override fun popularAnimeFromElement(element: Element) = SAnime.create().apply {
title = element.selectFirst("div.movief > a")!!.text()
.substringBefore(". Bölüm")
.substringBeforeLast(" ")
element.selectFirst("img")!!.attr("abs:src").also {
thumbnail_url = it
val slug = it.substringAfterLast("/").substringBefore(".")
setUrlWithoutDomain("/hentai-detay/$slug")
}
}
override fun popularAnimeNextPageSelector() = "span.current + a"
// =============================== Latest ===============================
override fun latestUpdatesRequest(page: Int) = GET("$baseUrl/yeni-eklenenler?c=${page - 1}", headers)
override fun latestUpdatesParse(response: Response) =
super.latestUpdatesParse(response).let { page ->
val animes = page.animes.distinctBy { it.url }
AnimesPage(animes, page.hasNextPage)
}
override fun latestUpdatesSelector() = popularAnimeSelector()
override fun latestUpdatesFromElement(element: Element) = popularAnimeFromElement(element)
override fun latestUpdatesNextPageSelector() = "a[rel=next]:contains(Sonraki Sayfa)"
// =============================== Search ===============================
override suspend fun getSearchAnime(page: Int, query: String, filters: AnimeFilterList): AnimesPage {
return if (query.startsWith(PREFIX_SEARCH)) { // URL intent handler
val id = query.removePrefix(PREFIX_SEARCH)
client.newCall(GET("$baseUrl/hentai-detay/$id"))
.awaitSuccess()
.use(::searchAnimeByIdParse)
} else {
super.getSearchAnime(page, query, filters)
}
}
private fun searchAnimeByIdParse(response: Response): AnimesPage {
val details = animeDetailsParse(response.asJsoup())
return AnimesPage(listOf(details), false)
}
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
return GET("$baseUrl/page/$page/?s=$query", headers)
}
override fun searchAnimeParse(response: Response) = popularAnimeParse(response)
override fun searchAnimeSelector() = throw UnsupportedOperationException()
override fun searchAnimeFromElement(element: Element) = throw UnsupportedOperationException()
override fun searchAnimeNextPageSelector() = null
// =========================== Anime Details ============================
override fun animeDetailsParse(document: Document) = SAnime.create().apply {
setUrlWithoutDomain(document.location())
val content = document.selectFirst("div.filmcontent")!!
title = content.selectFirst("h1")!!.text()
thumbnail_url = content.selectFirst("img")!!.attr("abs:src")
genre = content.select("tr:contains(Hentai Türü) > td > a").eachText().joinToString()
description = content.selectFirst("tr:contains(Özet) + tr > td")
?.text()
?.takeIf(String::isNotBlank)
}
// ============================== Episodes ==============================
override fun episodeListSelector() = "div#Bolumler li > a"
override fun episodeFromElement(element: Element) = SEpisode.create().apply {
setUrlWithoutDomain(element.attr("href"))
element.text().also {
val num = it.substringBeforeLast(". Bölüm", "")
.substringAfterLast(" ")
.ifBlank { "1" }
episode_number = num.toFloatOrNull() ?: 1F
name = "$num. Bölüm"
}
}
// ============================ Video Links =============================
private val videaExtractor by lazy { VideaExtractor(client) }
override fun videoListParse(response: Response): List<Video> {
val doc = response.asJsoup()
val videaItem = doc.selectFirst("div.alternatif a:contains(Videa)")!!
val path = videaItem.attr("onclick").substringAfter("../../").substringBefore("'")
val req = client.newCall(GET("$baseUrl/$path", headers)).execute()
.asJsoup()
val videaUrl = req.selectFirst("iframe")!!.attr("abs:src")
return videaExtractor.videosFromUrl(videaUrl)
}
private val qualityRegex by lazy { Regex("""(\d+)p""") }
override fun List<Video>.sort(): List<Video> {
val quality = preferences.getString(PREF_QUALITY_KEY, PREF_QUALITY_DEFAULT)!!
return sortedWith(
compareBy(
{ it.quality.contains(quality) },
{ qualityRegex.find(it.quality)?.groupValues?.get(1)?.toIntOrNull() ?: 0 },
),
).reversed()
}
override fun videoListSelector(): String {
throw UnsupportedOperationException()
}
override fun videoFromElement(element: Element): Video {
throw UnsupportedOperationException()
}
override fun videoUrlParse(document: Document): String {
throw UnsupportedOperationException()
}
// ============================== Settings ==============================
override fun setupPreferenceScreen(screen: PreferenceScreen) {
ListPreference(screen.context).apply {
key = PREF_QUALITY_KEY
title = PREF_QUALITY_TITLE
entries = PREF_QUALITY_ENTRIES
entryValues = PREF_QUALITY_VALUES
setDefaultValue(PREF_QUALITY_DEFAULT)
summary = "%s"
setOnPreferenceChangeListener { _, newValue ->
val selected = newValue as String
val index = findIndexOfValue(selected)
val entry = entryValues[index] as String
preferences.edit().putString(key, entry).commit()
}
}.also(screen::addPreference)
}
companion object {
const val PREFIX_SEARCH = "id:"
private const val PREF_QUALITY_KEY = "pref_quality_key"
private const val PREF_QUALITY_TITLE = "Preferred quality"
private const val PREF_QUALITY_DEFAULT = "720p"
private val PREF_QUALITY_ENTRIES = arrayOf("1080p", "720p", "480p", "360p", "240p")
private val PREF_QUALITY_VALUES = PREF_QUALITY_ENTRIES
}
}

View file

@ -0,0 +1,41 @@
package eu.kanade.tachiyomi.animeextension.tr.hentaizm
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://www.hentaizm.fun/hentai-detay/<item> intents
* and redirects them to the main Aniyomi process.
*/
class HentaiZMUrlActivity : Activity() {
private val tag = javaClass.simpleName
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val pathSegments = intent?.data?.pathSegments
if (pathSegments != null && pathSegments.size > 1) {
val item = pathSegments[1]
val mainIntent = Intent().apply {
action = "eu.kanade.tachiyomi.ANIMESEARCH"
putExtra("query", "${HentaiZM.PREFIX_SEARCH}$item")
putExtra("filter", packageName)
}
try {
startActivity(mainIntent)
} catch (e: ActivityNotFoundException) {
Log.e(tag, e.toString())
}
} else {
Log.e(tag, "could not parse uri from intent $intent")
}
finish()
exitProcess(0)
}
}

View file

@ -0,0 +1,74 @@
package eu.kanade.tachiyomi.animeextension.tr.hentaizm.extractors
import android.util.Base64
import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.network.GET
import okhttp3.Headers
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.OkHttpClient
import org.jsoup.Jsoup
import javax.crypto.Cipher
import javax.crypto.spec.SecretKeySpec
class VideaExtractor(private val client: OkHttpClient) {
fun videosFromUrl(url: String): List<Video> {
val body = client.newCall(GET(url)).execute().body.string()
val nonce = NONCE_REGEX.find(body)?.groupValues?.elementAt(1) ?: return emptyList()
val paramL = nonce.substring(0, 32)
val paramS = nonce.substring(32)
val result = (0..31).joinToString("") {
val index = it - (STUPID_KEY.indexOf(paramL.elementAt(it)) - 31)
paramS.elementAt(index).toString()
}
val seed = getRandomString(8)
val requestUrl = REQUEST_URL.toHttpUrl().newBuilder()
.addQueryParameter("_s", seed)
.addQueryParameter("_t", result.substring(0, 16))
.addQueryParameter("v", url.toHttpUrl().queryParameter("v") ?: "")
.build()
val headers = Headers.headersOf("referer", url, "origin", "https://videa.hu")
val response = client.newCall(GET(requestUrl.toString(), headers)).execute()
val doc = response.body.string().let {
when {
it.startsWith("<?xml") -> Jsoup.parse(it)
else -> {
val key = result.substring(16) + seed + response.headers["x-videa-xs"]
val b64dec = Base64.decode(it, Base64.DEFAULT)
Jsoup.parse(decryptXml(b64dec, key))
}
}
}
return doc.select("video_source").mapNotNull {
val name = it.attr("name")
val quality = "Videa - $name"
val hash = doc.selectFirst("hash_value_$name")?.text()
?: return@mapNotNull null
val videoUrl = "https:" + it.text() + "?md5=$hash&expires=${it.attr("exp")}"
Video(videoUrl, quality, videoUrl, headers)
}
}
private fun decryptXml(xml: ByteArray, key: String): String {
val rc4Key = SecretKeySpec(key.toByteArray(), "RC4")
val cipher = Cipher.getInstance("RC4")
cipher.init(Cipher.DECRYPT_MODE, rc4Key, cipher.getParameters())
return cipher.doFinal(xml).toString(Charsets.UTF_8)
}
private fun getRandomString(length: Int = 8): String {
val allowedChars = ('A'..'Z') + ('a'..'z') + ('0'..'9')
return (1..length)
.map { allowedChars.random() }
.joinToString("")
}
companion object {
private val NONCE_REGEX by lazy { Regex("_xt\\s*=\\s*\"([^\"]+)\"") }
private const val REQUEST_URL = "https://videa.hu/player/xml?platform=desktop"
private const val STUPID_KEY = "xHb0ZvME5q8CBcoQi6AngerDu3FGO9fkUlwPmLVY_RTzj2hJIS4NasXWKy1td7p"
}
}