diff --git a/Hahomoe/src/main/kotlin/com/jacekun/Hahomoe.kt b/Hahomoe/src/main/kotlin/com/jacekun/Hahomoe.kt
index 2b608cb..16c2f0f 100644
--- a/Hahomoe/src/main/kotlin/com/jacekun/Hahomoe.kt
+++ b/Hahomoe/src/main/kotlin/com/jacekun/Hahomoe.kt
@@ -5,6 +5,7 @@ import com.lagradost.cloudstream3.*
import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.utils.getQualityFromName
import com.fasterxml.jackson.annotation.JsonProperty
+import com.lagradost.cloudstream3.mvvm.logError
import java.text.SimpleDateFormat
import java.util.*
import khttp.structures.cookie.CookieJar
@@ -90,6 +91,7 @@ class Hahomoe : MainAPI() {
}
} catch (e: Exception) {
e.printStackTrace()
+ logError(e)
}
}
if (items.size <= 0) throw ErrorLoadingException()
diff --git a/JavHD/build.gradle.kts b/JavHD/build.gradle.kts
new file mode 100644
index 0000000..0bf89c3
--- /dev/null
+++ b/JavHD/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("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=javhd.icu&sz=%size%"
+}
diff --git a/JavHD/src/main/AndroidManifest.xml b/JavHD/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..29aec9d
--- /dev/null
+++ b/JavHD/src/main/AndroidManifest.xml
@@ -0,0 +1,2 @@
+
+
\ No newline at end of file
diff --git a/JavHD/src/main/kotlin/com/jacekun/JavHD.kt b/JavHD/src/main/kotlin/com/jacekun/JavHD.kt
new file mode 100644
index 0000000..f5cea16
--- /dev/null
+++ b/JavHD/src/main/kotlin/com/jacekun/JavHD.kt
@@ -0,0 +1,280 @@
+package com.jacekun
+
+import android.util.Log
+import com.lagradost.cloudstream3.*
+import com.lagradost.cloudstream3.app
+import com.lagradost.cloudstream3.utils.AppUtils.toJson
+import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson
+import com.lagradost.cloudstream3.utils.ExtractorLink
+import com.lagradost.cloudstream3.utils.loadExtractor
+import org.jsoup.nodes.Element
+
+class JavHD : MainAPI() {
+ private val globalTvType = TvType.NSFW
+ override var name = "JavHD"
+ override var mainUrl = "https://javhd.icu"
+ override val supportedTypes: Set get() = setOf(TvType.NSFW)
+ override val hasDownloadSupport: Boolean get() = true
+ override val hasMainPage: Boolean get() = true
+ override val hasQuickSearch: Boolean get() = false
+
+ override val mainPage = mainPageOf(
+ "$mainUrl/page/" to "Main Page",
+ )
+
+ private val prefix = "JAV HD"
+
+ override suspend fun getMainPage(page: Int, request: MainPageRequest): HomePageResponse {
+ val homePageList = mutableListOf()
+ val pagedlink = if (page > 0) request.data + page else request.data
+ val document = app.get(pagedlink).document
+ val mainbody = document.getElementsByTag("body").select("div.container")
+ //Log.i(this.name, "Result => (mainbody) ${mainbody}")
+
+ var count = 0
+ val titles = mainbody.select("div.section-header").mapNotNull {
+ val text = it?.text() ?: return@mapNotNull null
+ count++
+ Pair(count, text)
+ }
+
+ //Log.i(this.name, "Result => (titles) ${titles}")
+ val entries = mainbody.select("div#video-widget-3016")
+ count = 0
+ entries.forEach { it2 ->
+ count++
+ // Fetch row title
+ val pair = titles.filter { aa -> aa.first == count }
+ val title = if (pair.isNotEmpty()) { pair[0].second } else { "" }
+ // Fetch list of items and map
+ val inner = it2.select("div.col-md-3.col-sm-6.col-xs-6.item.responsive-height.post")
+ val elements: List = inner.mapNotNull {
+ // Inner element
+ val aa = it.selectFirst("div.item-img > a") ?: return@mapNotNull null
+ // Video details
+ val link = aa.attr("href") ?: return@mapNotNull null
+ val name = aa.attr("title").cleanTitle()
+ val image = aa.select("img").attr("src")
+ val year = null
+ //Log.i(this.name, "Result => (link) ${link}")
+ //Log.i(this.name, "Result => (image) ${image}")
+
+ MovieSearchResponse(
+ name = name,
+ url = link,
+ apiName = this.name,
+ type = globalTvType,
+ posterUrl = image,
+ year = year,
+ id = null,
+ )
+ }.distinctBy { a -> a.url }
+
+ if (elements.isNotEmpty()) {
+ homePageList.add(
+ HomePageList(
+ name = title,
+ list = elements,
+ isHorizontalImages = true
+ )
+ )
+ }
+ }
+ if (homePageList.isNotEmpty()) {
+ HomePageResponse(
+ items = homePageList,
+ hasNext = homePageList.any{ it.list.isNotEmpty() }
+ )
+ }
+ throw ErrorLoadingException("No homepage data found!")
+ }
+
+ override suspend fun search(query: String): List {
+ val url = "$mainUrl/?s=$query"
+ val document = app.get(url).document.getElementsByTag("body")
+ .select("div.container > div.row")
+ .select("div.col-md-8.col-sm-12.main-content")
+ .select("div.row.video-section.meta-maxwidth-230")
+ .select("div.item.responsive-height.col-md-4.col-sm-6.col-xs-6")
+ //Log.i(this.name, "Result => $document")
+ return document.mapNotNull {
+ val content = it.selectFirst("div.item-img > a") ?: return@mapNotNull null
+ //Log.i(this.name, "Result => $content")
+ val link = fixUrlNull(content.attr("href")) ?: return@mapNotNull null
+ val imgContent = content.select("img")
+ val title = imgContent.attr("alt").cleanTitle()
+ val image = imgContent.attr("src").trim('\'')
+ val year = null
+ //Log.i(this.name, "Result => Title: ${title}, Image: ${image}")
+
+ MovieSearchResponse(
+ name = title,
+ url = link,
+ apiName = this.name,
+ type = globalTvType,
+ posterUrl = image,
+ year = year
+ )
+ }.distinctBy { it.url }
+ }
+
+ override suspend fun load(url: String): LoadResponse {
+ val document = app.get(url).document
+ val body = document.getElementsByTag("body")
+ .select("div.container > div.row")
+ .select("div.col-md-8.col-sm-12.main-content")
+ .firstOrNull()
+ //Log.i(this.name, "Result => ${body}")
+ val videoDetailsEl = body?.select("div.video-details")
+ val innerBody = videoDetailsEl?.select("div.post-entry")
+ val innerDiv = innerBody?.select("div")?.firstOrNull()
+
+ // Video details
+ val poster = innerDiv?.select("img")?.attr("src")
+ val title = innerDiv?.selectFirst("p.wp-caption-text")?.text()?.cleanTitle() ?: ""
+ //Log.i(this.name, "Result => (title) $title")
+ val descript = innerBody?.select("p")?.get(0)?.text()?.cleanTitle()
+ //Log.i(this.name, "ApiError => (innerDiv) ${innerBody?.select("p")}")
+
+ val re = Regex("[^0-9]")
+ var yearString = videoDetailsEl?.select("span.date")?.firstOrNull()?.text()
+ //Log.i(this.name, "Result => (yearString) ${yearString}")
+ yearString = yearString?.let { re.replace(it, "").trim() }
+ //Log.i(this.name, "Result => (yearString) ${yearString}")
+ val year = yearString?.takeLast(4)?.toIntOrNull()
+ val tags = mutableListOf()
+ videoDetailsEl?.select("span.meta")?.forEach {
+ //Log.i(this.name, "Result => (span meta) $it")
+ val caption = it?.selectFirst("span.meta-info")?.text()?.trim()?.lowercase() ?: ""
+ when (caption) {
+ "category", "tag" -> {
+ val tagtexts = it.select("a").mapNotNull { tag ->
+ tag?.text()?.trim() ?: return@mapNotNull null
+ }
+ if (tagtexts.isNotEmpty()) {
+ tags.addAll(tagtexts.filter { a -> a.isNotBlank() }.distinct())
+ }
+ }
+ }
+ }
+
+ val recs = body?.select("div.latest-wrapper div.item.active > div")?.mapNotNull {
+ val innerAImg = it?.select("div.item-img") ?: return@mapNotNull null
+ val aName = it.select("h3 > a").text().cleanTitle()
+ val aImg = innerAImg.select("img").attr("src")
+ val aUrl = innerAImg.select("a").get(0)?.attr("href") ?: return@mapNotNull null
+ MovieSearchResponse(
+ url = aUrl,
+ name = aName,
+ type = globalTvType,
+ posterUrl = aImg,
+ year = null,
+ apiName = this.name
+ )
+ }
+
+ // Video links, find if it contains multiple scene links
+ //val sceneList = mutableListOf()
+ val sceneList = body?.select("ul.pagination.post-tape > li")?.apmap { section ->
+ val innerA = section?.select("a") ?: return@apmap null
+ val vidlink = fixUrlNull(innerA.attr("href")) ?: return@apmap null
+ Log.i(this.name, "Result => (vidlink) $vidlink")
+
+ val sceneCount = innerA.text().toIntOrNull()
+ val viddoc = app.get(vidlink).document.getElementsByTag("body").get(0)
+ val streamEpLink = viddoc?.getValidLinks()?.removeInvalidLinks() ?: ""
+ Episode(
+ name = "Scene $sceneCount",
+ season = null,
+ episode = sceneCount,
+ data = streamEpLink,
+ posterUrl = poster,
+ date = null
+ )
+ }?.filterNotNull() ?: listOf()
+ if (sceneList.isNotEmpty()) {
+ return TvSeriesLoadResponse(
+ name = title,
+ url = url,
+ apiName = this.name,
+ type = TvType.TvSeries,
+ episodes = sceneList.filter { it.data.isNotBlank() },
+ posterUrl = poster,
+ year = year,
+ plot = descript,
+ tags = tags,
+ recommendations = recs
+ )
+ }
+ val videoLinks = body?.getValidLinks()?.removeInvalidLinks() ?: ""
+ return MovieLoadResponse(
+ name = title,
+ url = url,
+ apiName = this.name,
+ type = globalTvType,
+ dataUrl = videoLinks,
+ posterUrl = poster,
+ year = year,
+ plot = descript,
+ tags = tags,
+ recommendations = recs
+ )
+ }
+
+ override suspend fun loadLinks(
+ data: String,
+ isCasting: Boolean,
+ subtitleCallback: (SubtitleFile) -> Unit,
+ callback: (ExtractorLink) -> Unit
+ ): Boolean {
+
+ var count = 0
+ tryParseJson>(data.trim())?.apmap { vid ->
+ Log.i(this.name, "Result => (vid) $vid")
+ if (vid.startsWith("http")) {
+ count++
+ when {
+ vid.startsWith("https://javhdfree.icu") -> {
+ FEmbed().getSafeUrl(
+ url = vid,
+ referer = vid,
+ subtitleCallback = subtitleCallback,
+ callback = callback
+ )
+ }
+ vid.startsWith("https://viewsb.com") -> {
+ val url = vid.replace("viewsb.com", "watchsb.com")
+ WatchSB().getSafeUrl(
+ url = url,
+ referer = url,
+ subtitleCallback = subtitleCallback,
+ callback = callback
+ )
+ }
+ else -> {
+ loadExtractor(
+ url = vid,
+ referer = vid,
+ subtitleCallback = subtitleCallback,
+ callback = callback
+ )
+ }
+ }
+ }
+ }
+ return count > 0
+ }
+
+ private fun Element?.getValidLinks(): List? =
+ this?.select("iframe")?.mapNotNull { iframe ->
+ //Log.i("debug", "Result => (iframe) $iframe")
+ fixUrlNull(iframe.attr("src")) ?: return@mapNotNull null
+ }?.toList()
+
+ private fun List.removeInvalidLinks(): String =
+ this.filter { a -> a.isNotBlank() && !a.startsWith("https://a.realsrv.com") }.toJson()
+
+ private fun String.cleanTitle(): String =
+ this.trim().removePrefix(prefix).trim()
+
+}
\ No newline at end of file
diff --git a/JavHD/src/main/kotlin/com/jacekun/JavHDPlugin.kt b/JavHD/src/main/kotlin/com/jacekun/JavHDPlugin.kt
new file mode 100644
index 0000000..8070f8c
--- /dev/null
+++ b/JavHD/src/main/kotlin/com/jacekun/JavHDPlugin.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 JavHDPlugin: Plugin() {
+ override fun load(context: Context) {
+ // All providers should be added in this manner. Please don't edit the providers list directly.
+ registerMainAPI(JavHD())
+ }
+}
\ No newline at end of file