diff --git a/HentaiHaven/build.gradle.kts b/HentaiHaven/build.gradle.kts new file mode 100644 index 0000000..96932ef --- /dev/null +++ b/HentaiHaven/build.gradle.kts @@ -0,0 +1,26 @@ +// use an integer for version numbers +version = 1 + + +cloudstream { + // All of these properties are optional, you can safely remove them + + description = "" + authors = listOf("Jace") + + /** + * Status int as the following: + * 0: Down + * 1: Ok + * 2: Slow + * 3: Beta only + * */ + status = 1 // will be 3 if unspecified + + // List of video source types. Users are able to filter for extensions in a given category. + // You can find a list of avaliable types here: + // https://recloudstream.github.io/cloudstream/html/app/com.lagradost.cloudstream3/-tv-type/index.html + tvTypes = listOf("NSFW") + + iconUrl = "https://www.google.com/s2/favicons?domain=hentaihaven.xxx&sz=%size%" +} diff --git a/HentaiHaven/src/main/AndroidManifest.xml b/HentaiHaven/src/main/AndroidManifest.xml new file mode 100644 index 0000000..29aec9d --- /dev/null +++ b/HentaiHaven/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/HentaiHaven/src/main/kotlin/com/jacekun/HentaiHaven.kt b/HentaiHaven/src/main/kotlin/com/jacekun/HentaiHaven.kt new file mode 100644 index 0000000..e96b40a --- /dev/null +++ b/HentaiHaven/src/main/kotlin/com/jacekun/HentaiHaven.kt @@ -0,0 +1,224 @@ +package com.jacekun + +import android.util.Log +import com.fasterxml.jackson.annotation.JsonProperty +import com.lagradost.cloudstream3.* +import com.lagradost.cloudstream3.mvvm.logError +import com.lagradost.cloudstream3.utils.ExtractorLink +import com.lagradost.cloudstream3.utils.getQualityFromName +import org.jsoup.select.Elements + +class HentaiHaven : MainAPI() { + private val globalTvType = TvType.TvSeries + override var name = "Hentai Haven" + override var mainUrl = "https://hentaihaven.xxx" + override val supportedTypes = setOf(TvType.NSFW) + override val hasDownloadSupport = false + override val hasMainPage= true + override val hasQuickSearch = false + + private data class ResponseJson( + @JsonProperty("data") val data: ResponseData? + ) + private data class ResponseData( + @JsonProperty("sources") val sources: List? = listOf() + ) + private data class ResponseSources( + @JsonProperty("src") val src: String?, + @JsonProperty("type") val type: String?, + @JsonProperty("label") val label: String? + ) + + override suspend fun getMainPage( + page: Int, + request: MainPageRequest + ): HomePageResponse { + val doc = app.get(mainUrl).document + val all = ArrayList() + + doc.getElementsByTag("body").select("div.c-tabs-item") + .select("div.vraven_home_slider").forEach { it2 -> + // Fetch row title + val title = it2?.select("div.home_slider_header")?.text() ?: "Unnamed Row" + // Fetch list of items and map + it2.select("div.page-content-listing div.item.vraven_item.badge-pos-1").let { inner -> + + all.add( + HomePageList( + name = title, + list = inner.getResults(this.name), + isHorizontalImages = false + ) + ) + } + } + return HomePageResponse(all) + } + + override suspend fun search(query: String): List { + val searchUrl = "${mainUrl}/?s=${query}&post_type=wp-manga" + return app.get(searchUrl).document + .select("div.c-tabs-item div.row.c-tabs-item__content") + .getResults(this.name) + } + + override suspend fun load(url: String): LoadResponse { + //TODO: Load polishing + val doc = app.get(url).document + //Log.i(this.name, "Result => (url) ${url}") + val poster = doc.select("meta[property=og:image]") + .firstOrNull()?.attr("content") + val title = doc.select("meta[name=title]") + .firstOrNull()?.attr("content") + ?.toString() ?: "" + val descript = doc.select("div.description-summary").text() + + val body = doc.getElementsByTag("body") + val episodes = body.select("div.page-content-listing.single-page") + .first()?.select("li") + + val year = episodes?.last() + ?.selectFirst("span.chapter-release-date") + ?.text()?.trim()?.takeLast(4)?.toIntOrNull() + + val episodeList = episodes?.mapNotNull { + val innerA = it?.selectFirst("a") ?: return@mapNotNull null + val eplink = innerA.attr("href") ?: return@mapNotNull null + val epCount = innerA.text().trim().filter { a -> a.isDigit() }.toIntOrNull() + val imageEl = innerA.selectFirst("img") + val epPoster = imageEl?.attr("src") ?: imageEl?.attr("data-src") + Episode( + name = innerA.text(), + data = eplink, + posterUrl = epPoster, + episode = epCount, + ) + } ?: listOf() + + //Log.i(this.name, "Result => (id) ${id}") + return AnimeLoadResponse( + name = title, + url = url, + apiName = this.name, + type = globalTvType, + posterUrl = poster, + year = year, + plot = descript, + episodes = mutableMapOf( + Pair(DubStatus.Subbed, episodeList.reversed()) + ) + ) + } + + override suspend fun loadLinks( + data: String, + isCasting: Boolean, + subtitleCallback: (SubtitleFile) -> Unit, + callback: (ExtractorLink) -> Unit + ): Boolean { + + try { + Log.i(name, "Loading iframe") + val requestLink = "${mainUrl}/wp-content/plugins/player-logic/api.php" + val action = "zarat_get_data_player_ajax" + val reA = Regex("(?<=var en =)(.*?)(?=';)", setOf(RegexOption.DOT_MATCHES_ALL)) + val reB = Regex("(?<=var iv =)(.*?)(?=';)", setOf(RegexOption.DOT_MATCHES_ALL)) + + app.get(data).document.selectFirst("div.player_logic_item iframe") + ?.attr("src")?.let { epLink -> + + Log.i(name, "Loading ep link => $epLink") + val scrAppGet = app.get(epLink, referer = data) + val scrDoc = scrAppGet.document.getElementsByTag("script").toString() + //Log.i(name, "Loading scrDoc => (${scrAppGet.code}) $scrDoc") + if (scrDoc.isNotBlank()) { + //en + val a = reA.find(scrDoc)?.groupValues?.getOrNull(1) + ?.trim()?.removePrefix("'") ?: "" + //iv + val b = reB.find(scrDoc)?.groupValues?.getOrNull(1) + ?.trim()?.removePrefix("'") ?: "" + + Log.i(name, "a => $a") + Log.i(name, "b => $b") + + val doc = app.post( + url = requestLink, + headers = mapOf( +// Pair("mode", "cors"), +// Pair("Content-Type", "multipart/form-data"), +// Pair("Origin", mainUrl), +// Pair("Host", mainUrl.split("//").last()), + Pair("User-Agent", USER_AGENT), + Pair("Sec-Fetch-Mode", "cors") + ), + data = mapOf( + Pair("action", action), + Pair("a", a), + Pair("b", b) + ) + ) + Log.i(name, "Response (${doc.code}) => ${doc.text}") + //AppUtils.tryParseJson(doc.text) + doc.parsedSafe()?.data?.sources?.map { m3src -> + val m3srcFile = m3src.src ?: return@map null + val label = m3src.label ?: "" + Log.i(name, "M3u8 link: $m3srcFile") + callback.invoke( + ExtractorLink( + name = "$name m3u8", + source = "$name m3u8", + url = m3srcFile, + referer = "$mainUrl/", + quality = getQualityFromName(label), + isM3u8 = true + ) + ) + } + } + } + } catch (e: Exception) { + Log.i(name, "Error => $e") + logError(e) + return false + } + return true + } + + private fun Elements?.getResults(apiName: String): List { + return this?.mapNotNull { + val innerDiv = it.select("div").firstOrNull() + val firstA = innerDiv?.selectFirst("a") + val link = fixUrlNull(firstA?.attr("href")) ?: return@mapNotNull null + val name = firstA?.attr("title") ?: "" + val year = innerDiv?.selectFirst("span.c-new-tag")?.selectFirst("a") + ?.attr("title")?.takeLast(4)?.toIntOrNull() + + val imageDiv = firstA?.selectFirst("img") + var image = imageDiv?.attr("src") + if (image.isNullOrBlank()) { + image = imageDiv?.attr("data-src") + } + + val latestEp = innerDiv?.selectFirst("div.list-chapter") + ?.selectFirst("div.chapter-item") + ?.selectFirst("a") + ?.text() + ?.filter { a -> a.isDigit() } + ?.toIntOrNull() ?: 0 + val dubStatus = mutableMapOf( + Pair(DubStatus.Subbed, latestEp) + ) + + AnimeSearchResponse( + name = name, + url = link, + apiName = apiName, + type = globalTvType, + posterUrl = image, + year = year, + episodes = dubStatus + ) + } ?: listOf() + } +} \ No newline at end of file diff --git a/HentaiHaven/src/main/kotlin/com/jacekun/HentaiHavenPlugin.kt b/HentaiHaven/src/main/kotlin/com/jacekun/HentaiHavenPlugin.kt new file mode 100644 index 0000000..bcbd935 --- /dev/null +++ b/HentaiHaven/src/main/kotlin/com/jacekun/HentaiHavenPlugin.kt @@ -0,0 +1,13 @@ +package com.jacekun + +import com.lagradost.cloudstream3.plugins.CloudstreamPlugin +import com.lagradost.cloudstream3.plugins.Plugin +import android.content.Context + +@CloudstreamPlugin +class HentaiHavenPlugin: Plugin() { + override fun load(context: Context) { + // All providers should be added in this manner. Please don't edit the providers list directly. + registerMainAPI(HentaiHaven()) + } +} \ No newline at end of file