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