diff --git a/Xhamster/build.gradle.kts b/Xhamster/build.gradle.kts new file mode 100644 index 0000000..55dd225 --- /dev/null +++ b/Xhamster/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 = "Xhamster" + 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=xhamster.com&sz=%size%" + + language = "en" +} diff --git a/Xhamster/src/main/AndroidManifest.xml b/Xhamster/src/main/AndroidManifest.xml new file mode 100644 index 0000000..0862a59 --- /dev/null +++ b/Xhamster/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/Xhamster/src/main/kotlin/com/KillerDogeEmpire/Xhamster.kt b/Xhamster/src/main/kotlin/com/KillerDogeEmpire/Xhamster.kt new file mode 100644 index 0000000..2bf19e5 --- /dev/null +++ b/Xhamster/src/main/kotlin/com/KillerDogeEmpire/Xhamster.kt @@ -0,0 +1,119 @@ +package com.KillerDogeEmpire + +import com.lagradost.cloudstream3.* +import com.lagradost.cloudstream3.utils.ExtractorLink +import com.lagradost.cloudstream3.utils.Qualities +import org.jsoup.nodes.Element + +class Xhamster : MainAPI() { + override var mainUrl = "https://xhamster.com" + override var name = "Xhamster" + override val hasMainPage = true + override val hasDownloadSupport = true + override val vpnStatus = VPNStatus.MightBeNeeded + override val supportedTypes = setOf(TvType.NSFW) + + override val mainPage = mainPageOf( + "$mainUrl/newest/" to "Newest", + "$mainUrl/most-viewed/weekly/" to "Most viewed weekly", + "$mainUrl/most-viewed/monthly/" to "Most viewed monthly", + "$mainUrl/most-viewed" to "Most viewed all time", + "$mainUrl/most-viewed/weekly/" to "Most viewed weekly" + ) + + override suspend fun getMainPage( + page: Int, request: MainPageRequest + ): HomePageResponse { + val document = app.get(request.data + page + "?x_platform_switch=desktop").document + val home = document.select("div.thumb-list div.thumb-list__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("a.video-thumb-info__name")?.text() ?: return null + val href = fixUrl(this.selectFirst("a.video-thumb-info__name")!!.attr("href")) + val posterUrl = fixUrlNull(this.select("img.thumb-image-container__image").attr("src")) + return newMovieSearchResponse(title, href, TvType.Movie) { + this.posterUrl = posterUrl + } + + } + + override suspend fun search(query: String): List { + val searchResponse = mutableListOf() + for (i in 0 until 15) { + val document = app.get( + "$mainUrl/search/${query.replace(" ", "+")}/?page=$i&x_platform_switch=desktop" + ).document + val results = document.select("div.thumb-list div.thumb-list__item").mapNotNull { + it.toSearchResult() + } + if (!searchResponse.containsAll(results)) { + searchResponse.addAll(results) + } else { + break + } + if (results.isEmpty()) break + } + return searchResponse + } + + override suspend fun load(url: String): LoadResponse { + val document = app.get(url).document + + val title = document.selectFirst("div.with-player-container h1")?.text()?.trim().toString() + val poster = fixUrlNull( + document.selectFirst("div.xp-preload-image")?.attr("style")?.substringAfter("https:") + ?.substringBefore("\');") + ) + val tags = + document.select(" nav#video-tags-list-container ul.root-8199e.video-categories-tags.collapsed-8199e li.item-8199e a.video-tag") + .map { it.text() } + val recommendations = + document.select("div.related-container div.thumb-list div.thumb-list__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 { + app.get( + url = data + ).let { response -> + callback( + ExtractorLink( + source = name, + name = name, + url = fixUrl( + response.document.selectXpath("//link[contains(@href,'.m3u8')]")[0]?.attr( + "href" + ).toString() + ), + referer = mainUrl, + quality = Qualities.Unknown.value, + isM3u8 = true + ) + ) + } + + return true + } + +} \ No newline at end of file diff --git a/Xhamster/src/main/kotlin/com/KillerDogeEmpire/XhamsterProvider.kt b/Xhamster/src/main/kotlin/com/KillerDogeEmpire/XhamsterProvider.kt new file mode 100644 index 0000000..6dc7711 --- /dev/null +++ b/Xhamster/src/main/kotlin/com/KillerDogeEmpire/XhamsterProvider.kt @@ -0,0 +1,13 @@ +package com.KillerDogeEmpire + +import com.lagradost.cloudstream3.plugins.CloudstreamPlugin +import com.lagradost.cloudstream3.plugins.Plugin +import android.content.Context + +@CloudstreamPlugin +class XhamsterProvider: Plugin() { + override fun load(context: Context) { + // All providers should be added in this manner. Please don't edit the providers list directly. + registerMainAPI(Xhamster()) + } +} \ No newline at end of file