diff --git a/XvideosProvider/build.gradle.kts b/XvideosProvider/build.gradle.kts
new file mode 100644
index 0000000..d815975
--- /dev/null
+++ b/XvideosProvider/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/XvideosProvider/src/main/AndroidManifest.xml b/XvideosProvider/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..1863f02
--- /dev/null
+++ b/XvideosProvider/src/main/AndroidManifest.xml
@@ -0,0 +1,2 @@
+
+
\ No newline at end of file
diff --git a/XvideosProvider/src/main/kotlin/com/jacekun/XvideosProvider.kt b/XvideosProvider/src/main/kotlin/com/jacekun/XvideosProvider.kt
new file mode 100644
index 0000000..663fdc6
--- /dev/null
+++ b/XvideosProvider/src/main/kotlin/com/jacekun/XvideosProvider.kt
@@ -0,0 +1,189 @@
+package com.jacekun
+
+import com.lagradost.cloudstream3.*
+import com.lagradost.cloudstream3.mvvm.logError
+import com.lagradost.cloudstream3.utils.*
+
+
+class XvideosProvider : MainAPI() {
+ private val globalTvType = TvType.NSFW
+ override var mainUrl = "https://www.xvideos.com"
+ override var name = "Xvideos"
+ override val hasMainPage = true
+ override val hasChromecastSupport = true
+ override val hasDownloadSupport = true
+ override val supportedTypes = setOf(globalTvType)
+
+ override val mainPage = mainPageOf(
+ Pair(mainUrl, "Main Page"),
+ Pair("$mainUrl/new/", "New")
+ )
+
+ override suspend fun getMainPage(page: Int, request: MainPageRequest): HomePageResponse {
+ val categoryData = request.data
+ val categoryName = request.name
+ val isPaged = categoryData.endsWith('/')
+ val pagedLink = if (isPaged) categoryData + page else categoryData
+ try {
+ if (!isPaged && page < 2 || isPaged) {
+ val soup = app.get(pagedLink).document
+ val home = soup.select("div.thumb-block").mapNotNull {
+ if (it == null) { return@mapNotNull null }
+ val title = it.selectFirst("p.title a")?.text() ?: ""
+ val link = fixUrlNull(it.selectFirst("div.thumb a")?.attr("href")) ?: return@mapNotNull null
+ val image = it.selectFirst("div.thumb a img")?.attr("data-src")
+ MovieSearchResponse(
+ name = title,
+ url = link,
+ apiName = this.name,
+ type = globalTvType,
+ posterUrl = image,
+ year = null
+ )
+ }
+ if (home.isNotEmpty()) {
+ return newHomePageResponse(
+ list = HomePageList(
+ name = categoryName,
+ list = home,
+ isHorizontalImages = true
+ ),
+ hasNext = true
+ )
+ } else {
+ throw ErrorLoadingException("No homepage data found!")
+ }
+ }
+ } catch (e: Exception) {
+ //e.printStackTrace()
+ logError(e)
+ }
+ throw ErrorLoadingException()
+ }
+
+ override suspend fun search(query: String): List {
+ val url = "$mainUrl?k=${query}"
+ val document = app.get(url).document
+ return document.select("div.thumb-block").mapNotNull {
+ val title = it.selectFirst("p.title a")?.text()
+ ?: it.selectFirst("p.profile-name a")?.text()
+ ?: ""
+ val href = fixUrlNull(it.selectFirst("div.thumb a")?.attr("href")) ?: return@mapNotNull null
+ val image = if (href.contains("channels") || href.contains("pornstars")) null else it.selectFirst("div.thumb-inside a img")?.attr("data-src")
+ val finaltitle = if (href.contains("channels") || href.contains("pornstars")) "" else title
+ MovieSearchResponse(
+ name = finaltitle,
+ url = href,
+ apiName = this.name,
+ type = globalTvType,
+ posterUrl = image
+ )
+
+ }.toList()
+ }
+ override suspend fun load(url: String): LoadResponse? {
+ val soup = app.get(url).document
+ val title = if (url.contains("channels")||url.contains("pornstars")) soup.selectFirst("html.xv-responsive.is-desktop head title")?.text() else
+ soup.selectFirst(".page-title")?.text()
+ val poster: String? = if (url.contains("channels") || url.contains("pornstars")) soup.selectFirst(".profile-pic img")?.attr("data-src") else
+ soup.selectFirst("head meta[property=og:image]")?.attr("content")
+ val tags = soup.select(".video-tags-list li a")
+ .map { it?.text()?.trim().toString().replace(", ","") }
+ val episodes = soup.select("div.thumb-block").mapNotNull {
+ val href = it?.selectFirst("a")?.attr("href") ?: return@mapNotNull null
+ val name = it.selectFirst("p.title a")?.text() ?: ""
+ val epthumb = it.selectFirst("div.thumb a img")?.attr("data-src")
+ Episode(
+ name = name,
+ data = href,
+ posterUrl = epthumb,
+ )
+ }
+ val tvType = if (url.contains("channels") || url.contains("pornstars")) TvType.TvSeries else globalTvType
+ return when (tvType) {
+ TvType.TvSeries -> {
+ TvSeriesLoadResponse(
+ name = title ?: "",
+ url = url,
+ apiName = this.name,
+ type = globalTvType,
+ episodes = episodes,
+ posterUrl = poster,
+ plot = title,
+ showStatus = ShowStatus.Ongoing,
+ tags = tags,
+ )
+ }
+ TvType.NSFW -> {
+ MovieLoadResponse(
+ name = title ?: "",
+ url = url,
+ apiName = this.name,
+ type = tvType,
+ dataUrl = url,
+ posterUrl = poster,
+ plot = title,
+ tags = tags,
+ )
+ }
+ else -> null
+ }
+ }
+ override suspend fun loadLinks(
+ data: String,
+ isCasting: Boolean,
+ subtitleCallback: (SubtitleFile) -> Unit,
+ callback: (ExtractorLink) -> Unit
+ ): Boolean {
+ app.get(data).document.select("script").apmap { script ->
+ if (script.data().contains("HTML5Player")) {
+ val extractedlink = script.data().substringAfter(".setVideoHLS('")
+ .substringBefore("');")
+ if (extractedlink.isNotBlank()) {
+ M3u8Helper().m3u8Generation(
+ M3u8Helper.M3u8Stream(
+ extractedlink,
+ headers = app.get(data).headers.toMap()
+ ), true
+ ).map { stream ->
+ callback(
+ ExtractorLink(
+ source = this.name,
+ name = "${this.name} m3u8",
+ url = stream.streamUrl,
+ referer = data,
+ quality = getQualityFromName(stream.quality?.toString()),
+ isM3u8 = true
+ )
+ )
+ }
+ }
+ val mp4linkhigh = script.data().substringAfter("html5player.setVideoUrlHigh('").substringBefore("');")
+ if (mp4linkhigh.isNotBlank()) {
+ callback(
+ ExtractorLink(
+ source = this.name,
+ name = "${this.name} MP4 High",
+ url = mp4linkhigh,
+ referer = data,
+ quality = Qualities.Unknown.value,
+ )
+ )
+ }
+ val mp4linklow = script.data().substringAfter("html5player.setVideoUrlLow('").substringBefore("');")
+ if (mp4linklow.isNotBlank()) {
+ callback(
+ ExtractorLink(
+ source = this.name,
+ name = "${this.name} MP4 Low",
+ url = mp4linklow,
+ referer = data,
+ quality = Qualities.Unknown.value,
+ )
+ )
+ }
+ }
+ }
+ return true
+ }
+}
diff --git a/XvideosProvider/src/main/kotlin/com/jacekun/XvideosProviderPlugin.kt b/XvideosProvider/src/main/kotlin/com/jacekun/XvideosProviderPlugin.kt
new file mode 100644
index 0000000..2234f3e
--- /dev/null
+++ b/XvideosProvider/src/main/kotlin/com/jacekun/XvideosProviderPlugin.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 XvideosProviderPlugin: Plugin() {
+ override fun load(context: Context) {
+ // All providers should be added in this manner. Please don't edit the providers list directly.
+ registerMainAPI(XvideosProvider())
+ }
+}
\ No newline at end of file