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(""), ""), + 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