From bab20b7849f20d2480b77fcefcea8534d61a90a5 Mon Sep 17 00:00:00 2001 From: KillerDogeEmpire Date: Mon, 12 Feb 2024 17:18:58 -0800 Subject: [PATCH] New Source Pornhits --- Pornhits/build.gradle.kts | 28 +++ Pornhits/src/main/AndroidManifest.xml | 2 + .../kotlin/com/KillerDogeEmpire/Pornhits.kt | 216 ++++++++++++++++++ .../com/KillerDogeEmpire/PornhitsProvider.kt | 14 ++ 4 files changed, 260 insertions(+) create mode 100644 Pornhits/build.gradle.kts create mode 100644 Pornhits/src/main/AndroidManifest.xml create mode 100644 Pornhits/src/main/kotlin/com/KillerDogeEmpire/Pornhits.kt create mode 100644 Pornhits/src/main/kotlin/com/KillerDogeEmpire/PornhitsProvider.kt diff --git a/Pornhits/build.gradle.kts b/Pornhits/build.gradle.kts new file mode 100644 index 0000000..5b5c045 --- /dev/null +++ b/Pornhits/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 = "Pornhits" + authors = listOf("KillerDogeEmpire, Coxju") + + /** + * 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=pornhits.com&sz=%size%" + + language = "en" +} diff --git a/Pornhits/src/main/AndroidManifest.xml b/Pornhits/src/main/AndroidManifest.xml new file mode 100644 index 0000000..0862a59 --- /dev/null +++ b/Pornhits/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/Pornhits/src/main/kotlin/com/KillerDogeEmpire/Pornhits.kt b/Pornhits/src/main/kotlin/com/KillerDogeEmpire/Pornhits.kt new file mode 100644 index 0000000..a8f34c9 --- /dev/null +++ b/Pornhits/src/main/kotlin/com/KillerDogeEmpire/Pornhits.kt @@ -0,0 +1,216 @@ +package com.KillerDogeEmpire + +import com.lagradost.cloudstream3.HomePageList +import com.lagradost.cloudstream3.HomePageResponse +import com.lagradost.cloudstream3.LoadResponse +import com.lagradost.cloudstream3.MainAPI +import com.lagradost.cloudstream3.MainPageRequest +import com.lagradost.cloudstream3.SearchResponse +import com.lagradost.cloudstream3.SubtitleFile +import com.lagradost.cloudstream3.TvType +import com.lagradost.cloudstream3.VPNStatus +import com.lagradost.cloudstream3.app +import com.lagradost.cloudstream3.fixUrl +import com.lagradost.cloudstream3.fixUrlNull +import com.lagradost.cloudstream3.mainPageOf +import com.lagradost.cloudstream3.newHomePageResponse +import com.lagradost.cloudstream3.newMovieLoadResponse +import com.lagradost.cloudstream3.newMovieSearchResponse +import com.lagradost.cloudstream3.utils.ExtractorLink +import com.lagradost.cloudstream3.utils.Qualities +import org.json.JSONObject +import org.jsoup.nodes.Element + +class Pornhits : MainAPI() { + override var mainUrl = "https://www.pornhits.com" + override var name = "Pornhits" + override val hasMainPage = true + override val hasDownloadSupport = true + override val vpnStatus = VPNStatus.MightBeNeeded + override val supportedTypes = setOf(TvType.NSFW) + + override val mainPage = mainPageOf( + "$mainUrl/videos.php?p=%d&s=l" to "Latest", + "$mainUrl/videos.php?p=%d&s=pd" to "Popular last day", + "$mainUrl/videos.php?p=%d&s=bd" to "Top Rated (day)", + "$mainUrl/videos.php?p=%d&s=pw" to "Popular last week", + "$mainUrl/videos.php?p=%d&s=bw" to "Top Rated (week)", + "$mainUrl/videos.php?p=%d&s=pm" to "Popular last month", + "$mainUrl/videos.php?p=%d&s=bm" to "Top Rated (month)", + ) + + override suspend fun getMainPage( + page: Int, request: MainPageRequest + ): HomePageResponse { + val document = app.get(request.data.format(page)).document + val home = + document.select("div.main-content section.main-container div.list-videos article.item") + .mapNotNull { + it.toSearchResult() + } + return newHomePageResponse( + list = HomePageList( + name = request.name, list = home, isHorizontalImages = true + ), hasNext = true + ) + } + + private fun Element.toSearchResult(): SearchResponse? { + val title = this.selectFirst("div.item-info h2.title")?.text() ?: return null + val href = fixUrl(this.selectFirst("a")!!.attr("href")) + val posterUrl = fixUrlNull(this.select("a div.img img").attr("data-original")) + return newMovieSearchResponse(title, href, TvType.Movie) { + this.posterUrl = posterUrl + } + + } + + override suspend fun search(query: String): List { + val searchResponse = mutableListOf() + for (i in 1..15) { + val document = app.get( + "$mainUrl/videos.php?p=${i}&q=${query.trim().replace(" ", "+")}" + ).document + val results = + document.select("div.main-content section.main-container div.list-videos article.item") + .mapNotNull { + it.toSearchResult() + } + searchResponse.addAll(results) + if (results.isEmpty()) break + } + return searchResponse + } + + override suspend fun load(url: String): LoadResponse { + val document = app.get(url).document + + val title = + document.selectFirst("section.video-holder div.video-info div.info-holder article#tab_video_info.tab-content div.headline h1") + ?.text() + ?: "" + val poster = fixUrlNull( + document.selectXpath("//script[contains(text(),'var schemaJson')]").first()?.data() + ?.replace("\"", "") + ?.substringAfter("thumbnailUrl:") + ?.substringBefore(",uploadDate:") + ?.trim() ?: "" + ) + val tags = + document.select(" section.video-holder div.video-info div.info-holder article#tab_video_info.tab-content div.block-details div.info h3.item a") + .map { it.text() } + val recommendations = + document.select("div.related-videos div.list-videos article.item") + .mapNotNull { + it.toSearchResult() + } + + return newMovieLoadResponse(title, url, TvType.NSFW, url) { + this.posterUrl = poster + this.tags = tags + this.recommendations = recommendations + } + } + + override suspend fun loadLinks( + data: String, + isCasting: Boolean, + subtitleCallback: (SubtitleFile) -> Unit, + callback: (ExtractorLink) -> Unit + ): Boolean { + val document = app.get(data).document + + val script = + document.selectXpath("//script[contains(text(),'let vpage_data')]").first()?.html() + var isVHQ = false + if (script != null && script.contains("VHQ")) { + isVHQ = true + } + val pattern = Regex("""window\.initPlayer\((.*])\);""") + val matchResult = pattern.find(script ?: "") + + val jsonArray = matchResult?.groups?.get(1)?.value + + val encodedString = getEncodedString(jsonArray) ?: "" + + val decodedString = customBase64Decoder(encodedString) + + val videos = JSONObject("{ videos:$decodedString}").getJSONArray("videos") + val externalLinkList = mutableListOf() + for (i in 0 until videos.length()) { + val video = videos.getJSONObject(i) + var quality = Qualities.Unknown.value + var isM3u8 = false + if (video.getString("format").contains("lq")) { + quality = Qualities.P480.value + } + if (video.getString("format").contains("hq")) { + quality = Qualities.P720.value + } + var url = customBase64Decoder(video.getString("video_url")) + if (isVHQ) { + url = "$url&f=video.m3u8" + isM3u8 = true + quality = Qualities.Unknown.value + } + externalLinkList.add( + ExtractorLink( + this.name, + this.name, + fixUrl(url), + referer = mainUrl, + quality = quality, + isM3u8 = isM3u8 + ) + ) + if (isVHQ) break + } + + externalLinkList.forEach(callback) + return true + } + + private fun customBase64Decoder(encodedString: String): String { + val base64CharacterSet = "АВСDЕFGHIJKLМNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789.,~" + var decodedString = "" + var currentIndex = 0 + + Regex("[^АВСЕМA-Za-z0-9.,~]").find(encodedString)?.let { + println("Error decoding URL") + } + + val sanitizedString = encodedString.replace("[^АВСЕМA-Za-z0-9.,~]".toRegex(), "") + + do { + val firstCharIndex = base64CharacterSet.indexOf(sanitizedString[currentIndex++]) + val secondCharIndex = base64CharacterSet.indexOf(sanitizedString[currentIndex++]) + val thirdCharIndex = base64CharacterSet.indexOf(sanitizedString[currentIndex++]) + val fourthCharIndex = base64CharacterSet.indexOf(sanitizedString[currentIndex++]) + + val reconstructedFirstChar = (firstCharIndex shl 2) or (secondCharIndex shr 4) + val reconstructedSecondChar = ((15 and secondCharIndex) shl 4) or (thirdCharIndex shr 2) + val lastPart = ((3 and thirdCharIndex) shl 6) or fourthCharIndex + + decodedString += reconstructedFirstChar.toChar().toString() + if (64 != thirdCharIndex) { + decodedString += reconstructedSecondChar.toChar().toString() + } + if (64 != fourthCharIndex) { + decodedString += lastPart.toChar().toString() + } + } while (currentIndex < sanitizedString.length) + return java.net.URLDecoder.decode(decodedString, "UTF-8") + } + + private fun getEncodedString(json: String?): String? { + val stringPattern = Regex("""'([^']+)',""") + + val stringMatch = stringPattern.find(json ?: "") + + return when { + stringMatch != null -> stringMatch.groups[1]?.value + else -> null + } + } + +} \ No newline at end of file diff --git a/Pornhits/src/main/kotlin/com/KillerDogeEmpire/PornhitsProvider.kt b/Pornhits/src/main/kotlin/com/KillerDogeEmpire/PornhitsProvider.kt new file mode 100644 index 0000000..f4df368 --- /dev/null +++ b/Pornhits/src/main/kotlin/com/KillerDogeEmpire/PornhitsProvider.kt @@ -0,0 +1,14 @@ +package com.KillerDogeEmpire + +import com.lagradost.cloudstream3.plugins.CloudstreamPlugin +import com.lagradost.cloudstream3.plugins.Plugin +import android.content.Context +import com.KillerDogeEmpire.Pornhits + +@CloudstreamPlugin +class PornhitsProvider: Plugin() { + override fun load(context: Context) { + // All providers should be added in this manner. Please don't edit the providers list directly. + registerMainAPI(Pornhits()) + } +} \ No newline at end of file