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