diff --git a/JavSubProvider/build.gradle.kts b/JavSubProvider/build.gradle.kts
new file mode 100644
index 0000000..d815975
--- /dev/null
+++ b/JavSubProvider/build.gradle.kts
@@ -0,0 +1,24 @@
+// use an integer for version numbers
+version = 1
+
+
+cloudstream {
+ // All of these properties are optional, you can safely remove them
+
+ description = "High quality JAV subbed"
+ 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")
+}
diff --git a/JavSubProvider/src/main/AndroidManifest.xml b/JavSubProvider/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..1863f02
--- /dev/null
+++ b/JavSubProvider/src/main/AndroidManifest.xml
@@ -0,0 +1,2 @@
+
+
\ No newline at end of file
diff --git a/JavSubProvider/src/main/kotlin/com/jacekun/JavSubProvider.kt b/JavSubProvider/src/main/kotlin/com/jacekun/JavSubProvider.kt
new file mode 100644
index 0000000..40d536e
--- /dev/null
+++ b/JavSubProvider/src/main/kotlin/com/jacekun/JavSubProvider.kt
@@ -0,0 +1,241 @@
+package com.jacekun
+
+import android.util.Log
+import com.fasterxml.jackson.annotation.JsonProperty
+import com.lagradost.cloudstream3.*
+import com.lagradost.cloudstream3.extractors.FEmbed
+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.Jsoup
+
+class JavSubProvider : MainAPI() {
+ override var name = "JavSub"
+ override var mainUrl = "https://javsub.co"
+ 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
+
+ private val prefixTag = "dummyTag" //For use on stream links to differentiate links
+ private val tvType = TvType.NSFW
+
+ data class ResponseMovieDetails(
+ @JsonProperty("name") val name: String?,
+ @JsonProperty("description") val description: String?,
+ @JsonProperty("thumbnailUrl") val thumbnailUrl: String?,
+ @JsonProperty("uploadDate") val uploadDate: String?,
+ @JsonProperty("contentUrl") val contentUrl: String?
+ )
+
+ override val mainPage = mainPageOf(
+ "$mainUrl/page/" to "Main Page",
+ )
+
+ override suspend fun getMainPage(page: Int, request: MainPageRequest): HomePageResponse {
+ val categoryData = request.data
+ val categoryName = request.name
+ val pagedlink = if (page > 0) categoryData + page else categoryData
+ val document = app.get(pagedlink).document
+ val homepage = document.select("main#main-content").map { it2 ->
+ val inner = it2?.select("article > div.post-item-wrap") ?: return@map null
+ //Log.i(this.name, "inner => $inner")
+ val elements: List = inner.mapNotNull {
+ //Log.i(this.name, "Inner content => $innerArticle")
+ val innerA = it.selectFirst("div.blog-pic-wrap > a")?: return@mapNotNull null
+ val link = fixUrlNull(innerA.attr("href")) ?: return@mapNotNull null
+
+ val imgArticle = innerA.selectFirst("img")
+ val name = innerA.attr("title") ?: imgArticle?.attr("alt") ?: ""
+ val image = imgArticle?.attr("data-src")
+ val year = null
+ //Log.i(this.name, "image => $image")
+
+ MovieSearchResponse(
+ name = name,
+ url = link,
+ apiName = this.name,
+ type = tvType,
+ posterUrl = image,
+ year = year
+ )
+ }.distinctBy { a -> a.url }
+
+ HomePageList(
+ name = categoryName,
+ list = elements,
+ isHorizontalImages = true
+ )
+ }.filterNotNull().filter { a -> a.list.isNotEmpty() }
+ //TODO: Replace 'homepage.first()' with 'homepage' after adding overload on newHomePageResponse()
+ if (homepage.isNotEmpty()) {
+ return newHomePageResponse(
+ list = homepage.first(),
+ hasNext = true
+ )
+ }
+ 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("main#main-content")?.select("article")
+
+ return document?.mapNotNull {
+ if (it == null) { return@mapNotNull null }
+ val innerA = it.selectFirst("div.blog-pic-wrap > a")?: return@mapNotNull null
+ val link = fixUrlNull(innerA.attr("href")) ?: return@mapNotNull null
+
+ val imgArticle = innerA.selectFirst("img")
+ val title = innerA.attr("title") ?: imgArticle?.attr("alt") ?: ""
+ val image = imgArticle?.attr("data-src")
+ val year = null
+
+ MovieSearchResponse(
+ name = title,
+ url = link,
+ apiName = this.name,
+ type = tvType,
+ posterUrl = image,
+ year = year
+ )
+ }?.distinctBy { b -> b.url } ?: listOf()
+ }
+
+ override suspend fun load(url: String): LoadResponse {
+ val document = app.get(url).document
+ val body = document.getElementsByTag("body")
+
+ // Default values
+ var title = ""
+ var poster : String? = null
+ var year : Int? = null
+ var descript : String? = null
+
+ // Video details
+ var scriptJson = ""
+ run breaking@{
+ body.select("script").forEach {
+ val scrAttr = it?.attr("type") ?: return@forEach
+ if (scrAttr.equals("application/ld+json", ignoreCase = true)) {
+ scriptJson = it.html() ?: ""
+ return@breaking
+ }
+ }
+ }
+ //Log.i(this.name, "Result => (scriptJson) $scriptJson")
+
+ // Video stream
+ val playerIframes: MutableList = try {
+ //Note: Fetch all multi-link urls
+ document.selectFirst("div.series-listing")?.select("a")?.mapNotNull {
+ it?.attr("href") ?: return@mapNotNull null
+ }?.toMutableList() ?: mutableListOf()
+ } catch (e: Exception) {
+ Log.i(this.name, "Result => Exception (load) $e")
+ mutableListOf()
+ }
+
+ // JAV Info
+ tryParseJson(scriptJson)?.let {
+ val contentUrl = it.contentUrl
+ title = it.name ?: ""
+ poster = it.thumbnailUrl
+ year = it.uploadDate?.take(4)?.toIntOrNull()
+ descript = "Title: $title ${System.lineSeparator()} ${it.description}"
+
+ // Add additional links, Raw link without needing to fetch from JavSub API
+ //if (!contentUrl.isNullOrBlank()) {
+ //playerIframes.add("$prefixTag$contentUrl")
+ //}
+ //Log.i(this.name, "Result => (contentUrl) $contentUrl")
+ }
+
+ Log.i(this.name, "Result => (playerIframes) ${playerIframes.toJson()}")
+
+ return MovieLoadResponse(
+ name = title,
+ url = url,
+ apiName = this.name,
+ type = tvType,
+ dataUrl = playerIframes.toJson(),
+ posterUrl = poster,
+ year = year,
+ plot = descript
+ )
+ }
+
+ override suspend fun loadLinks(
+ data: String,
+ isCasting: Boolean,
+ subtitleCallback: (SubtitleFile) -> Unit,
+ callback: (ExtractorLink) -> Unit
+ ): Boolean {
+
+ var count = 0
+ tryParseJson>(data)?.apmap { link ->
+ Log.i(this.name, "Result => (link) $link")
+ if (link.startsWith(prefixTag)) {
+ val linkUrl = link.removePrefix(prefixTag)
+ val success = extractStreamLink(linkUrl, subtitleCallback, callback)
+ if (success) {
+ count++
+ }
+ }
+ else {
+ val innerDoc =
+ app.get(link).document.selectFirst("script#beeteam368_obj_wes-js-extra")
+ var innerText = innerDoc?.html() ?: ""
+ if (innerText.isNotBlank()) {
+ "(?<=single_video_url\":)(.*)(?=,)".toRegex().find(innerText)
+ ?.groupValues?.get(0)?.let { iframe ->
+ innerText = iframe.trim().trim('"')
+ }
+ Jsoup.parse(innerText)?.selectFirst("iframe")?.attr("src")?.let { server ->
+ val serverLink = server.replace("\\", "").replace("\"", "")
+ val success = extractStreamLink(serverLink, subtitleCallback, callback)
+ if (success) {
+ count++
+ }
+ Log.i(this.name, "Result => (streamLink add) $serverLink")
+ }
+ }
+ }
+ }
+ //Log.i(this.name, "Result => count: $count")
+ return count > 0
+ }
+
+ private suspend fun extractStreamLink(
+ link: String,
+ subtitleCallback: (SubtitleFile) -> Unit,
+ callback: (ExtractorLink) -> Unit)
+ : Boolean {
+ if (link.isNotBlank()) {
+ when {
+ link.contains("watch-jav") -> {
+ val extractor = FEmbed()
+ extractor.domainUrl = "embedsito.com"
+ extractor.getSafeUrl(
+ url = link,
+ referer = mainUrl,
+ subtitleCallback = subtitleCallback,
+ callback = callback
+ )
+ return true
+ }
+ else -> {
+ return loadExtractor(
+ url = link,
+ referer = mainUrl,
+ subtitleCallback = subtitleCallback,
+ callback = callback
+ )
+ }
+ }
+ }
+ return false
+ }
+}
\ No newline at end of file
diff --git a/JavSubProvider/src/main/kotlin/com/jacekun/JavSubProviderPlugin.kt b/JavSubProvider/src/main/kotlin/com/jacekun/JavSubProviderPlugin.kt
new file mode 100644
index 0000000..8e82d6d
--- /dev/null
+++ b/JavSubProvider/src/main/kotlin/com/jacekun/JavSubProviderPlugin.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 TestPlugin: Plugin() {
+ override fun load(context: Context) {
+ // All providers should be added in this manner. Please don't edit the providers list directly.
+ registerMainAPI(JavSubProvider())
+ }
+}
\ No newline at end of file
diff --git a/build.gradle.kts b/build.gradle.kts
index 26f4b82..7a039fe 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -77,6 +77,7 @@ subprojects {
implementation(kotlin("stdlib")) // adds standard kotlin features, like listOf, mapOf etc
implementation("com.github.Blatzar:NiceHttp:0.3.2") // http library
implementation("org.jsoup:jsoup:1.13.1") // html parser
+ implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.13.1")
}
}