From 47567f010a64979602ac7f093332430470c2aac7 Mon Sep 17 00:00:00 2001
From: Jace <54625750+Jacekun@users.noreply.github.com>
Date: Sat, 3 Sep 2022 17:42:03 +0800
Subject: [PATCH] Add hanime provider
---
Hanime/build.gradle.kts | 26 ++
Hanime/src/main/AndroidManifest.xml | 2 +
Hanime/src/main/kotlin/com/jacekun/Hanime.kt | 278 ++++++++++++++++++
.../main/kotlin/com/jacekun/HanimePlugin.kt | 13 +
4 files changed, 319 insertions(+)
create mode 100644 Hanime/build.gradle.kts
create mode 100644 Hanime/src/main/AndroidManifest.xml
create mode 100644 Hanime/src/main/kotlin/com/jacekun/Hanime.kt
create mode 100644 Hanime/src/main/kotlin/com/jacekun/HanimePlugin.kt
diff --git a/Hanime/build.gradle.kts b/Hanime/build.gradle.kts
new file mode 100644
index 0000000..e818800
--- /dev/null
+++ b/Hanime/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("ArjixWasTaken", "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=hanime.tv&sz=%size%"
+}
diff --git a/Hanime/src/main/AndroidManifest.xml b/Hanime/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..29aec9d
--- /dev/null
+++ b/Hanime/src/main/AndroidManifest.xml
@@ -0,0 +1,2 @@
+
+
\ No newline at end of file
diff --git a/Hanime/src/main/kotlin/com/jacekun/Hanime.kt b/Hanime/src/main/kotlin/com/jacekun/Hanime.kt
new file mode 100644
index 0000000..b6b07a7
--- /dev/null
+++ b/Hanime/src/main/kotlin/com/jacekun/Hanime.kt
@@ -0,0 +1,278 @@
+package com.jacekun
+
+import com.lagradost.cloudstream3.MainAPI
+import com.lagradost.cloudstream3.TvType
+import android.annotation.SuppressLint
+import com.fasterxml.jackson.annotation.JsonProperty
+import com.lagradost.cloudstream3.*
+import com.lagradost.cloudstream3.utils.ExtractorLink
+import com.lagradost.cloudstream3.utils.getQualityFromName
+import com.fasterxml.jackson.module.kotlin.readValue
+import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson
+import java.text.SimpleDateFormat
+import java.util.*
+import kotlin.collections.ArrayList
+
+//Credits https://github.com/ArjixWasTaken/CloudStream-3/blob/master/app/src/main/java/com/ArjixWasTaken/cloudstream3/animeproviders/HanimeProvider.kt
+
+class Hanime : MainAPI() {
+ private val globalTvType = TvType.NSFW
+
+ companion object {
+ @SuppressLint("SimpleDateFormat")
+ fun unixToYear(timestamp: Int): Int? {
+ val sdf = SimpleDateFormat("yyyy")
+ val netDate = Date(timestamp * 1000L)
+ val date = sdf.format(netDate)
+
+ return date.toIntOrNull()
+ }
+ private fun isNumber(num: String) = (num.toIntOrNull() != null)
+
+ private fun getTitle(title: String): String {
+ if (title.contains(" Ep ")) {
+ return title.split(" Ep ")[0].trim()
+ } else {
+ if (isNumber(title.trim().split(" ").last())) {
+ val split = title.trim().split(" ")
+ return split.slice(0..split.size-2).joinToString(" ").trim()
+ } else {
+ return title.trim()
+ }
+ }
+ }
+ }
+
+ override var mainUrl = "https://hanime.tv"
+ override var name = "Hanime"
+ override val hasQuickSearch = false
+ override val hasMainPage = true
+ override val hasDownloadSupport = true
+ override val supportedTypes = setOf(TvType.NSFW)
+
+ private data class HpHentaiVideos (
+ @JsonProperty("id") val id : Int,
+ @JsonProperty("name") val name : String,
+ @JsonProperty("slug") val slug : String,
+ @JsonProperty("released_at_unix") val releasedAt : Int,
+ @JsonProperty("poster_url") val posterUrl : String,
+ @JsonProperty("cover_url") val coverUrl : String
+ )
+ private data class HpSections (
+ @JsonProperty("title") val title : String,
+ @JsonProperty("hentai_video_ids") val hentaiVideoIds : List
+ )
+ private data class HpLanding (
+ @JsonProperty("sections") val sections : List,
+ @JsonProperty("hentai_videos") val hentaiVideos : List
+ )
+ private data class HpData (
+ @JsonProperty("landing") val landing : HpLanding
+ )
+ private data class HpState (
+ @JsonProperty("data") val data : HpData
+ )
+ private data class HpHanimeHomePage (
+ @JsonProperty("state") val state : HpState
+ )
+
+ private fun getHentaiByIdFromList(id: Int, list: List): HpHentaiVideos? {
+ for (item in list) {
+ if (item.id == id) {
+ return item
+ }
+ }
+ return null
+ }
+
+ override suspend fun getMainPage(
+ page: Int,
+ request: MainPageRequest
+ ): HomePageResponse {
+
+ val data = app.get("https://hanime.tv/").text
+ val jsonText = Regex("""window\.__NUXT__=(.*?);""").find(data)!!.destructured.component1()
+ val json = mapper.readValue(jsonText)
+ val titles = ArrayList()
+ val items = ArrayList()
+
+ try {
+ json.state.data.landing.sections.forEach { section ->
+ items.add(HomePageList(section.title, (section.hentaiVideoIds.map {
+ val hentai = getHentaiByIdFromList(it, json.state.data.landing.hentaiVideos)!!
+ val title = getTitle(hentai.name)
+ if (!titles.contains(title)) {
+ titles.add(title)
+ AnimeSearchResponse(
+ title,
+ "https://hanime.tv/videos/hentai/${hentai.slug}?id=${hentai.id}&title=${title}",
+ this.name,
+ globalTvType,
+ hentai.coverUrl,
+ null,
+ EnumSet.of(DubStatus.Subbed),
+ )
+ } else {
+ null
+ }
+ }).filterNotNull()))
+ }
+ } catch (e: Exception) {
+ e.printStackTrace()
+ }
+
+ if (items.size <= 0) throw ErrorLoadingException()
+ return HomePageResponse(items)
+ }
+
+ data class HanimeSearchResult (
+ @JsonProperty("id") val id : Int,
+ @JsonProperty("name") val name : String,
+ @JsonProperty("slug") val slug : String,
+ @JsonProperty("titles") val titles : List?,
+ @JsonProperty("cover_url") val coverUrl : String,
+ @JsonProperty("tags") val tags : List,
+ @JsonProperty("released_at") val releasedAt : Int
+ )
+
+ override suspend fun search(query: String): ArrayList {
+ val link = "https://search.htv-services.com/"
+ val data = mapOf("search_text" to query, "tags" to listOf(), "tags_mode" to "AND", "brands" to listOf(), "blacklist" to listOf(), "order_by" to "created_at_unix", "ordering" to "desc", "page" to 0)
+ val response = khttp.post(link, json=data).jsonObject.getString("hits").let { mapper.readValue>(it) }
+ val titles = ArrayList()
+ val searchResults = ArrayList()
+
+ response.reversed().forEach {
+ val title = getTitle(it.name)
+ if (!titles.contains(title)) {
+ titles.add(title)
+ searchResults.add(
+ AnimeSearchResponse(
+ title,
+ "https://hanime.tv/videos/hentai/${it.slug}?id=${it.id}&title=${title}",
+ this.name,
+ globalTvType,
+ it.coverUrl,
+ unixToYear(it.releasedAt),
+ EnumSet.of(DubStatus.Subbed),
+ it.titles?.get(0),
+ )
+ )
+ }
+ }
+ return searchResults
+ }
+
+ private data class HentaiTags (
+ @JsonProperty("text") val text : String
+ )
+
+ private data class HentaiVideo (
+ @JsonProperty("name") val name : String,
+ @JsonProperty("description") val description : String,
+ @JsonProperty("cover_url") val coverUrl : String,
+ @JsonProperty("released_at_unix") val releasedAtUnix : Int,
+ @JsonProperty("hentai_tags") val hentaiTags : List
+ )
+
+ private data class HentaiFranchiseHentaiVideos (
+ @JsonProperty("id") val id : Int,
+ @JsonProperty("name") val name : String,
+ @JsonProperty("poster_url") val posterUrl : String,
+ @JsonProperty("released_at_unix") val releasedAtUnix : Int
+ )
+
+ private data class Streams (
+ @JsonProperty("height") val height : String,
+ @JsonProperty("filesize_mbs") val filesizeMbs : Int,
+ @JsonProperty("url") val url : String,
+ )
+
+ private data class Servers (
+ @JsonProperty("name") val name : String,
+ @JsonProperty("streams") val streams : List
+ )
+
+ private data class VideosManifest (
+ @JsonProperty("servers") val servers : List
+ )
+
+ private data class HanimeEpisodeData (
+ @JsonProperty("hentai_video") val hentaiVideo : HentaiVideo,
+ @JsonProperty("hentai_tags") val hentaiTags : List,
+ @JsonProperty("hentai_franchise_hentai_videos") val hentaiFranchiseHentaiVideos : List,
+ @JsonProperty("videos_manifest") val videosManifest: VideosManifest,
+ )
+
+ override suspend fun load(url: String): LoadResponse {
+ val params: List> = url.split("?")[1].split("&").map {
+ val split = it.split("=")
+ Pair(split[0], split[1])
+ }
+ val id = params[0].second
+ val title = params[1].second
+
+ val uri = "$mainUrl/api/v8/video?id=${id}&"
+ val response = app.get(uri)
+
+ val data = mapper.readValue(response.text)
+
+ val tags = data.hentaiTags.map { it.text }
+
+ val episodes = data.hentaiFranchiseHentaiVideos.map {
+ Episode(
+ data = "$mainUrl/api/v8/video?id=${it.id}&",
+ name = it.name,
+ posterUrl = it.posterUrl
+ )
+ }
+
+ return AnimeLoadResponse(
+ title,
+ null,
+ title,
+ url,
+ this.name,
+ globalTvType,
+ data.hentaiVideo.coverUrl,
+ unixToYear(data.hentaiVideo.releasedAtUnix),
+ hashMapOf(DubStatus.Subbed to episodes),
+ null,
+ data.hentaiVideo.description.replace(Regex("?p>"), ""),
+ tags,
+ )
+ }
+
+ override suspend fun loadLinks(
+ data: String,
+ isCasting: Boolean,
+ subtitleCallback: (SubtitleFile) -> Unit,
+ callback: (ExtractorLink) -> Unit
+ ): Boolean {
+ val res = app.get(data).text
+ val response = tryParseJson(res)
+
+ val streams = ArrayList()
+
+ response?.videosManifest?.servers?.map { server ->
+ server.streams.forEach {
+ if (it.url.isNotEmpty()) {
+ streams.add(
+ ExtractorLink(
+ source ="Hanime",
+ name ="Hanime - ${server.name} - ${it.filesizeMbs}mb",
+ url = it.url,
+ referer = "",
+ quality = getQualityFromName(it.height),
+ isM3u8 = true
+ ))
+ }
+ }
+ }
+
+ streams.forEach {
+ callback(it)
+ }
+ return true
+ }
+}
diff --git a/Hanime/src/main/kotlin/com/jacekun/HanimePlugin.kt b/Hanime/src/main/kotlin/com/jacekun/HanimePlugin.kt
new file mode 100644
index 0000000..f94c69b
--- /dev/null
+++ b/Hanime/src/main/kotlin/com/jacekun/HanimePlugin.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 HanimePlugin: Plugin() {
+ override fun load(context: Context) {
+ // All providers should be added in this manner. Please don't edit the providers list directly.
+ registerMainAPI(Hanime())
+ }
+}
\ No newline at end of file