diff --git a/Example/build.gradle.kts b/Example/build.gradle.kts index 683c0be..aad9fd6 100644 --- a/Example/build.gradle.kts +++ b/Example/build.gradle.kts @@ -15,12 +15,12 @@ cloudstream { * 2: Slow * 3: Beta only * */ - status = 1 // will be 3 if unspecified + status = 0 // 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 = "" + iconUrl = "https://www.google.com/s2/favicons?domain=example.com&sz=%size%" } diff --git a/Example/src/main/kotlin/com/jacekun/Example.kt b/Example/src/main/kotlin/com/jacekun/Example.kt index 09f6a9d..1aad8d1 100644 --- a/Example/src/main/kotlin/com/jacekun/Example.kt +++ b/Example/src/main/kotlin/com/jacekun/Example.kt @@ -4,5 +4,6 @@ import com.lagradost.cloudstream3.MainAPI import com.lagradost.cloudstream3.TvType class Example : MainAPI() { - private val globalTvType = TvType.Movie + private val DEV = "DevDebug" + private val globaltvType = TvType.Movie } \ No newline at end of file diff --git a/JavTube/build.gradle.kts b/JavTube/build.gradle.kts new file mode 100644 index 0000000..e2109d4 --- /dev/null +++ b/JavTube/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 = "Watch Jav Tube FULL HD" + 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=javtube.watch&sz=%size%" +} diff --git a/JavTube/src/main/AndroidManifest.xml b/JavTube/src/main/AndroidManifest.xml new file mode 100644 index 0000000..29aec9d --- /dev/null +++ b/JavTube/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/JavTube/src/main/kotlin/com/jacekun/JavTube.kt b/JavTube/src/main/kotlin/com/jacekun/JavTube.kt new file mode 100644 index 0000000..acc42e7 --- /dev/null +++ b/JavTube/src/main/kotlin/com/jacekun/JavTube.kt @@ -0,0 +1,243 @@ +package com.jacekun + +import android.util.Log +import com.fasterxml.jackson.annotation.JsonProperty +import com.lagradost.cloudstream3.* +import com.lagradost.cloudstream3.app +import com.lagradost.cloudstream3.utils.AppUtils +import com.lagradost.cloudstream3.utils.AppUtils.toJson +import com.lagradost.cloudstream3.utils.ExtractorLink +import com.lagradost.cloudstream3.utils.getQualityFromName +import com.lagradost.cloudstream3.utils.loadExtractor +import org.jsoup.Jsoup + +class JavTube : MainAPI() { + private val DEV = "DevDebug" + private val globaltvType = TvType.Movie + override var name = "JavTube" + override var mainUrl = "https://javtube.watch" + override val supportedTypes = setOf(TvType.NSFW) + override val hasDownloadSupport = true + override val hasMainPage = true + override val hasQuickSearch = false + + override suspend fun getMainPage( + page: Int, + request: MainPageRequest + ): HomePageResponse { + val document = app.get(mainUrl).document + val all = ArrayList() + + // Fetch row title + val title = "Latest videos" + // Fetch list of items and map + val inner = document.selectFirst("div.videos-list")?.select("article") ?: return HomePageResponse(all) + //Log.i(DEV, "Inner => $inner") + val elements: List = inner.mapNotNull { + + //Log.i(DEV, "Inner content => $innerArticle") + val aa = it.select("a").last() ?: return@mapNotNull null + val link = fixUrlNull(aa.attr("href")) ?: return@mapNotNull null + + val imgArticle = aa.select("img") + val name = imgArticle.attr("alt") ?: "" + var image = imgArticle.attr("data-src") + if (image.isNullOrEmpty()) { + image = imgArticle.attr("src") + } + + MovieSearchResponse( + name = name, + url = link, + apiName = this.name, + type = globaltvType, + posterUrl = image, + year = null, + id = null, + ) + }.distinctBy { a -> a.url } + + all.add( + HomePageList( + title, elements + ) + ) + + return HomePageResponse(all.filter { a -> a.list.isNotEmpty() }) + } + + override suspend fun search(query: String): List { + val url = "$mainUrl/search/$query" + val document = app.get(url).document.select("article#post") + + return document.mapNotNull { + val innerA = it?.selectFirst("a") ?: return@mapNotNull null + val linkUrl = fixUrlNull(innerA.attr("href")) ?: return@mapNotNull null + if (linkUrl.startsWith("https://javtube.watch/tag/")) { + //Log.i(DEV, "Result => (innerA) $innerA") + return@mapNotNull null + } + + val title = innerA.select("header.entry-header").text() + val imgLink = innerA.select("img") + var image = imgLink.attr("data-src") + if (image.isNullOrEmpty()) { + image = imgLink.attr("src") + } + val year = null + + MovieSearchResponse( + name = title, + url = linkUrl, + apiName = this.name, + type = globaltvType, + posterUrl = image, + year = year + ) + } + } + + override suspend fun load(url: String): LoadResponse { + val document = app.get(url).document + //Log.i(DEV, "Result => ${body}") + + // Video details + val content = document.selectFirst("article#post")?.select("div.video-player") + //Log.i(DEV, "Result => (content) $content") + val title = content?.select("meta[itemprop=\"name\"]")?.attr("content") ?: "" + val descript =content?.select("meta[itemprop=\"description\"]")?.attr("content") + //Log.i(DEV, "Result => (descript) $descript") + val year = null + + // Poster Image + val poster = content?.select("meta[itemprop=\"thumbnailUrl\"]")?.attr("content") + //Log.i(DEV, "Result => (poster) $poster") + + //TODO: Fetch links + //Video stream + val streamUrl: String = try { + val strPost = "post(\"https://javtube.watch/hash-javtubewatch\"" + val scripts = document.select("script").toString() + val idxA = scripts.indexOf(strPost) + val firstParse = scripts.substring(idxA) + val idxB = firstParse.indexOf("function") + + val secondParse = firstParse.substring(strPost.length, idxB).trim().trim(',') + .replace("num:", "\"num\":") + .replace(":'", ":\"") + .replace("'}", "\"}") + .trim().trim(',') + .trimEnd('}') + "$secondParse,\"url\":\"${url}\"}" + } catch (e: Exception) { + Log.i(DEV, "Result => Exception (load) $e") + "" + } + Log.i(DEV, "streamUrl => $streamUrl") + return MovieLoadResponse( + name = title, + url = url, + apiName = this.name, + type = globaltvType, + dataUrl = streamUrl, + posterUrl = poster, + year = year, + plot = descript, + ) + } + + //TODO: LoadLinks + override suspend fun loadLinks( + data: String, + isCasting: Boolean, + subtitleCallback: (SubtitleFile) -> Unit, + callback: (ExtractorLink) -> Unit + ): Boolean { + if (data.isEmpty()) return false + if (data == "about:blank") return false + + AppUtils.tryParseJson(data)?.let { reqdata -> + Log.i(DEV, "Referer => ${reqdata.url}") + app.post( + url = "$mainUrl/hash-javtubewatch", + referer = reqdata.url, + data = mapOf( + Pair("migboob", reqdata.migboob), + Pair("mix", reqdata.mix), + Pair("num", reqdata.num), + ), + headers = mapOf( + Pair("Origin", mainUrl), + Pair("Sec-Fetch-Mode", "cors"), + Pair("User-Agent", USER_AGENT), + ) + ).let { postreq -> + Log.i(DEV, "Post => (${postreq.code}) ${postreq.text}") + + val doc = Jsoup.parse(postreq.text) + val src = doc.selectFirst("iframe")?.attr("src") ?: "" + Log.i(DEV, "Post Url => $src") + + val id = src.trimEnd('/').split("/").last() + val newUrl = "https://fembed-hd.com/api/source/${id}" + Log.i(DEV, "newUrl => $newUrl") + loadExtractor( + url = newUrl, + referer = reqdata.url, + callback = callback, + subtitleCallback = subtitleCallback + ) + //TODO: Fix headers, returning 403 Forbidden + /*val headers = mapOf( + Pair("Host", "javjav.top"), + Pair("Origin", src), + Pair("Referer", reqdata.url), + Pair("User-Agent", USER_AGENT), + ) + Log.i(DEV, "headers => ${headers.toJson()}") + val postlink = app.post( + url = newUrl, + headers = mapOf( + Pair("Host", "javjav.top"), + Pair("Origin", src), + Pair("Referer", reqdata.url), + Pair("User-Agent", USER_AGENT), + ) + ) + Log.i(DEV, "Post Link => (${postlink.code}) ${postlink.text}") + + val streamLinks = AppUtils.tryParseJson(postlink.text)?.data ?: listOf() + streamLinks.forEach{ stream -> + callback.invoke( + ExtractorLink( + source = "JavTube", + name = name, + url = stream.file, + referer = reqdata.url, + quality = getQualityFromName(stream.label) + ) + ) + }*/ + } + } + + return true + } + + private data class JsonRequest( + @JsonProperty("migboob") val migboob: String, + @JsonProperty("mix") val mix: String, + @JsonProperty("num") val num: String, + @JsonProperty("url") val url: String + ) + + private data class JsonResponse( + @JsonProperty("success") val success: Boolean, + @JsonProperty("data") val data: List? + ) + private data class JsonResponseData( + @JsonProperty("file") val file: String, + @JsonProperty("label") val label: String, + //val type: String // Mp4 + ) +} \ No newline at end of file diff --git a/JavTube/src/main/kotlin/com/jacekun/JavTubePlugin.kt b/JavTube/src/main/kotlin/com/jacekun/JavTubePlugin.kt new file mode 100644 index 0000000..9c8ba7f --- /dev/null +++ b/JavTube/src/main/kotlin/com/jacekun/JavTubePlugin.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 JavTubePlugin: Plugin() { + override fun load(context: Context) { + // All providers should be added in this manner. Please don't edit the providers list directly. + registerMainAPI(JavTube()) + } +} \ No newline at end of file