From fc13ea684341aa206e3c6dd78cf5d137fb71f370 Mon Sep 17 00:00:00 2001 From: Nexus <79303560+Nexus-Gits@users.noreply.github.com> Date: Sun, 13 Aug 2023 17:56:41 +0530 Subject: [PATCH] Add files via upload --- Hanime/build.gradle.kts | 28 ++ Hanime/src/main/AndroidManifest.xml | 2 + Hanime/src/main/kotlin/com/jacekun/Hanime.kt | 307 ++++++++++++++++++ .../main/kotlin/com/jacekun/HanimePlugin.kt | 13 + HentaiHaven/build.gradle.kts | 28 ++ HentaiHaven/src/main/AndroidManifest.xml | 2 + .../main/kotlin/com/jacekun/HentaiHaven.kt | 224 +++++++++++++ .../kotlin/com/jacekun/HentaiHavenPlugin.kt | 13 + 8 files changed, 617 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 create mode 100644 HentaiHaven/build.gradle.kts create mode 100644 HentaiHaven/src/main/AndroidManifest.xml create mode 100644 HentaiHaven/src/main/kotlin/com/jacekun/HentaiHaven.kt create mode 100644 HentaiHaven/src/main/kotlin/com/jacekun/HentaiHavenPlugin.kt diff --git a/Hanime/build.gradle.kts b/Hanime/build.gradle.kts new file mode 100644 index 00000000..b794771c --- /dev/null +++ b/Hanime/build.gradle.kts @@ -0,0 +1,28 @@ +// use an integer for version numbers +version = 5 + + +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%" + + language = "en" +} diff --git a/Hanime/src/main/AndroidManifest.xml b/Hanime/src/main/AndroidManifest.xml new file mode 100644 index 00000000..29aec9de --- /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 00000000..fe2bb5e1 --- /dev/null +++ b/Hanime/src/main/kotlin/com/jacekun/Hanime.kt @@ -0,0 +1,307 @@ +package com.jacekun + +import com.lagradost.cloudstream3.MainAPI +import com.lagradost.cloudstream3.TvType +import android.annotation.SuppressLint +import android.util.Log +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.network.CloudflareKiller +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 + //private val interceptor = CloudflareKiller() + private var globalHeaders = mapOf() + private val DEV = "DevDebug" + + 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) + + 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() + } + } + } + } + + 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 requestGet = app.get("https://hanime.tv/") + globalHeaders = requestGet.headers.toMap() + val data = requestGet.text + val jsonText = Regex("""window\.__NUXT__=(.*?);""").find(data)?.destructured?.component1() + val titles = ArrayList() + val items = ArrayList() + + tryParseJson(jsonText)?.let { json -> + 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())) + } + } + + 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 emptyList(), + "tags_mode" to "AND", + "brands" to emptyList(), + "blacklist" to emptyList(), + "order_by" to "created_at_unix", + "ordering" to "desc", + "page" to 0 + ) + val headers = mapOf( + Pair("Origin", mainUrl), + Pair("Sec-Fetch-Mode", "cors"), + Pair("Sec-Fetch-Site", "cross-site"), + Pair("TE", "trailers"), + Pair("User-Agent", USER_AGENT), + ) + val response = app.post( + url = link, + json = data, + headers = globalHeaders + ) + val responseText = response.text + val titles = ArrayList() + val searchResults = ArrayList() + + Log.i(DEV, "Response => (${response.code}) ${responseText}") + tryParseJson?>(responseText)?.reversed()?.forEach { + val rawName = it?.name ?: return@forEach + val title = getTitle(rawName) + 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 00000000..f94c69b2 --- /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 diff --git a/HentaiHaven/build.gradle.kts b/HentaiHaven/build.gradle.kts new file mode 100644 index 00000000..9f1c8b3e --- /dev/null +++ b/HentaiHaven/build.gradle.kts @@ -0,0 +1,28 @@ +// use an integer for version numbers +version = 5 + + +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%" + + language = "en" +} diff --git a/HentaiHaven/src/main/AndroidManifest.xml b/HentaiHaven/src/main/AndroidManifest.xml new file mode 100644 index 00000000..29aec9de --- /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 00000000..e88f9fed --- /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.NSFW + 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 + + 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() + } + + 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? + ) +} \ 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 00000000..bcbd9354 --- /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