Skip to content

Instantly share code, notes, and snippets.

@ShivamKumarJha
Created October 11, 2025 19:49
Show Gist options
  • Select an option

  • Save ShivamKumarJha/ed31b38486a2e6f0fb531d7b0c29995b to your computer and use it in GitHub Desktop.

Select an option

Save ShivamKumarJha/ed31b38486a2e6f0fb531d7b0c29995b to your computer and use it in GitHub Desktop.
From 8e307bb49681fa229205676b9cd23c30428ead9c Mon Sep 17 00:00:00 2001
From: ShivamKumarJha <[email protected]>
Date: Sat, 11 Oct 2025 19:27:26 +0530
Subject: [PATCH] SourcePirateXPlay
Signed-off-by: ShivamKumarJha <[email protected]>
---
settings.gradle.kts | 1 +
source/all/build.gradle.kts | 1 +
.../com/shivamkumarjha/source_all/Get.kt | 2 +
.../source_hianime/SourceHiAnime.kt | 2 +-
.../shivamkumarjha/source_main/SourceId.kt | 10 +
source/piratexplay/.gitignore | 1 +
source/piratexplay/build.gradle.kts | 47 ++
.../src/androidMain/AndroidManifest.xml | 4 +
.../shivamkumarjha/source_piratexplay/Get.kt | 10 +
.../source_piratexplay/SourcePirateXPlay.kt | 612 ++++++++++++++++++
.../source_piratexplay/model/ScheduleAnime.kt | 17 +
.../model/ScheduleAnimeResponse.kt | 9 +
.../source_piratexplay/model/Server.kt | 10 +
13 files changed, 725 insertions(+), 1 deletion(-)
create mode 100644 source/piratexplay/.gitignore
create mode 100644 source/piratexplay/build.gradle.kts
create mode 100644 source/piratexplay/src/androidMain/AndroidManifest.xml
create mode 100644 source/piratexplay/src/commonMain/kotlin/com/shivamkumarjha/source_piratexplay/Get.kt
create mode 100644 source/piratexplay/src/commonMain/kotlin/com/shivamkumarjha/source_piratexplay/SourcePirateXPlay.kt
create mode 100644 source/piratexplay/src/commonMain/kotlin/com/shivamkumarjha/source_piratexplay/model/ScheduleAnime.kt
create mode 100644 source/piratexplay/src/commonMain/kotlin/com/shivamkumarjha/source_piratexplay/model/ScheduleAnimeResponse.kt
create mode 100644 source/piratexplay/src/commonMain/kotlin/com/shivamkumarjha/source_piratexplay/model/Server.kt
diff --git a/settings.gradle.kts b/settings.gradle.kts
index 80999b83..75bafd11 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -119,6 +119,7 @@ include(":source:multimovies")
include(":source:iptvs")
include(":source:mxplayer")
include(":source:tvgarden")
+include(":source:piratexplay")
include(":utility:datetime")
include(":utility:logging")
include(":utility:jsunpacker")
diff --git a/source/all/build.gradle.kts b/source/all/build.gradle.kts
index 422c4480..8332364f 100644
--- a/source/all/build.gradle.kts
+++ b/source/all/build.gradle.kts
@@ -131,6 +131,7 @@ kotlin {
implementation(project(":source:iptvs"))
implementation(project(":source:mxplayer"))
implementation(project(":source:tvgarden"))
+ implementation(project(":source:piratexplay"))
}
}
}
\ No newline at end of file
diff --git a/source/all/src/commonMain/kotlin/com/shivamkumarjha/source_all/Get.kt b/source/all/src/commonMain/kotlin/com/shivamkumarjha/source_all/Get.kt
index 4cd77e89..9d0af6c8 100644
--- a/source/all/src/commonMain/kotlin/com/shivamkumarjha/source_all/Get.kt
+++ b/source/all/src/commonMain/kotlin/com/shivamkumarjha/source_all/Get.kt
@@ -94,6 +94,7 @@ import com.shivamkumarjha.source_ogporn.getSourceOGPorn
import com.shivamkumarjha.source_onlinemovieshindi.getSourceOnlineMoviesHindi
import com.shivamkumarjha.source_p560movie.getSource560PMovie
import com.shivamkumarjha.source_pagalnew.getSourcePagalNew
+import com.shivamkumarjha.source_piratexplay.getSourcePirateXPlay
import com.shivamkumarjha.source_pornhat.getSourcePornHat
import com.shivamkumarjha.source_roshytv.getSourceRoshyTV
import com.shivamkumarjha.source_sextbnet.getSourceSexTBNet
@@ -240,4 +241,5 @@ fun getSourceById(
SourceId.OPPLEX -> getSourceOpplexTV(httpClient)
SourceId.IPTV_ZEE5_TV -> getSourceZee5TV(httpClient)
SourceId.TV_GARDEN -> getSourceTVGarden(httpClient)
+ SourceId.PIRATE_X_PLAY -> getSourcePirateXPlay(httpClient, dateTime)
}
\ No newline at end of file
diff --git a/source/main/src/commonMain/kotlin/com/shivamkumarjha/source_main/SourceId.kt b/source/main/src/commonMain/kotlin/com/shivamkumarjha/source_main/SourceId.kt
index 3d1a6017..5d0607f6 100644
--- a/source/main/src/commonMain/kotlin/com/shivamkumarjha/source_main/SourceId.kt
+++ b/source/main/src/commonMain/kotlin/com/shivamkumarjha/source_main/SourceId.kt
@@ -1112,6 +1112,16 @@ enum class SourceId(
SourceType.LIVE,
priority = Priority.HIGH,
),
+ PIRATE_X_PLAY(
+ "PirateX Play",
+ "Watch subbed or dubbed anime, cartoon etc in ultra HD quality in HINDI!",
+ Image(
+ HttpRequestData("https://piratexplay.cc/public/logo/logo_img.png"),
+ 823F / 200F,
+ ),
+ SourceType.ANIME,
+ priority = Priority.HIGH,
+ ),
}
fun SourceType.isAdult(): Boolean =
diff --git a/source/piratexplay/.gitignore b/source/piratexplay/.gitignore
new file mode 100644
index 00000000..779bf0ea
--- /dev/null
+++ b/source/piratexplay/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/source/piratexplay/build.gradle.kts b/source/piratexplay/build.gradle.kts
new file mode 100644
index 00000000..d455202b
--- /dev/null
+++ b/source/piratexplay/build.gradle.kts
@@ -0,0 +1,47 @@
+import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl
+
+plugins {
+ alias(libs.plugins.kotlinMultiplatform)
+ alias(libs.plugins.androidKotlinMultiplatformLibrary)
+ alias(libs.plugins.kotlinx.serialization)
+}
+
+kotlin {
+
+ androidLibrary {
+ namespace = "com.shivamkumarjha.source_piratexplay"
+ compileSdk = libs.versions.android.compileSdk.get().toInt()
+ minSdk = libs.versions.android.minSdk.get().toInt()
+ }
+
+ val xcfName = "source:piratexplayKit"
+
+ iosX64 {
+ binaries.framework {
+ baseName = xcfName
+ }
+ }
+
+ iosArm64 {
+ binaries.framework {
+ baseName = xcfName
+ }
+ }
+
+ iosSimulatorArm64 {
+ binaries.framework {
+ baseName = xcfName
+ }
+ }
+
+ jvm("desktop")
+
+ @OptIn(ExperimentalWasmDsl::class)
+ wasmJs { browser() }
+
+ sourceSets {
+ commonMain.dependencies {
+ api(project(":source:main"))
+ }
+ }
+}
\ No newline at end of file
diff --git a/source/piratexplay/src/androidMain/AndroidManifest.xml b/source/piratexplay/src/androidMain/AndroidManifest.xml
new file mode 100644
index 00000000..61391c19
--- /dev/null
+++ b/source/piratexplay/src/androidMain/AndroidManifest.xml
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest>
+
+</manifest>
\ No newline at end of file
diff --git a/source/piratexplay/src/commonMain/kotlin/com/shivamkumarjha/source_piratexplay/Get.kt b/source/piratexplay/src/commonMain/kotlin/com/shivamkumarjha/source_piratexplay/Get.kt
new file mode 100644
index 00000000..916d7b02
--- /dev/null
+++ b/source/piratexplay/src/commonMain/kotlin/com/shivamkumarjha/source_piratexplay/Get.kt
@@ -0,0 +1,10 @@
+package com.shivamkumarjha.source_piratexplay
+
+import com.shivamkumarjha.source_main.Source
+import com.shivamkumarjha.utility_datetime.DateTime
+import io.ktor.client.HttpClient
+
+fun getSourcePirateXPlay(
+ httpClient: HttpClient,
+ dateTime: DateTime,
+): Source = SourcePirateXPlay(httpClient, dateTime)
diff --git a/source/piratexplay/src/commonMain/kotlin/com/shivamkumarjha/source_piratexplay/SourcePirateXPlay.kt b/source/piratexplay/src/commonMain/kotlin/com/shivamkumarjha/source_piratexplay/SourcePirateXPlay.kt
new file mode 100644
index 00000000..b5593c49
--- /dev/null
+++ b/source/piratexplay/src/commonMain/kotlin/com/shivamkumarjha/source_piratexplay/SourcePirateXPlay.kt
@@ -0,0 +1,612 @@
+package com.shivamkumarjha.source_piratexplay
+
+import com.fleeksoft.ksoup.Ksoup
+import com.fleeksoft.ksoup.nodes.Element
+import com.shivamkumarjha.media_common.model.EmbedLink
+import com.shivamkumarjha.media_common.model.MediaPlayerItem
+import com.shivamkumarjha.network_client.getEmbedHeaders
+import com.shivamkumarjha.network_common.HttpRequestData
+import com.shivamkumarjha.network_common.Resource
+import com.shivamkumarjha.source_main.SourceId
+import com.shivamkumarjha.source_main.base.BaseSource
+import com.shivamkumarjha.source_main.model.Image
+import com.shivamkumarjha.source_main.model.MediaLink
+import com.shivamkumarjha.source_main.model.MetaData
+import com.shivamkumarjha.source_main.model.PageLink
+import com.shivamkumarjha.source_main.model.SortFilter
+import com.shivamkumarjha.source_main.model.SourceComponent
+import com.shivamkumarjha.source_main.model.SourceEpisode
+import com.shivamkumarjha.source_main.model.SourceMedia
+import com.shivamkumarjha.source_main.model.SourceMediaSlider
+import com.shivamkumarjha.source_main.model.SourceRequest
+import com.shivamkumarjha.source_main.model.SourceScreen
+import com.shivamkumarjha.source_piratexplay.model.ScheduleAnimeResponse
+import com.shivamkumarjha.source_piratexplay.model.Server
+import com.shivamkumarjha.utility_datetime.DateTime
+import com.shivamkumarjha.utility_logging.logger
+import io.ktor.client.HttpClient
+import io.ktor.client.call.body
+import io.ktor.client.request.get
+import io.ktor.client.request.header
+import io.ktor.client.request.parameter
+import io.ktor.client.statement.bodyAsText
+import io.ktor.http.HttpHeaders
+import io.ktor.http.isSuccess
+
+internal class SourcePirateXPlay(
+ private val httpClient: HttpClient,
+ private val dateTime: DateTime,
+) : BaseSource(SourceId.PIRATE_X_PLAY) {
+
+ private companion object {
+ private const val TAG = "PirateXPlay"
+ private const val URL_BASE = "https://piratexplay.cc"
+ private const val URL_HOME = "${URL_BASE}/home"
+ private const val URL_HOME_NAV_PAGE = "${URL_BASE}/src/component/header.php"
+ private const val URL_HOME_SCHEDULE = "${URL_BASE}/src/assets/api/schedule-proxy.php"
+ private const val URL_HOME_TRENDING = "${URL_BASE}/src/component/anime/trending.php"
+ private const val URL_SERVER = "${URL_BASE}/src/ajax/server.php"
+ private const val URL_SUB = "${URL_BASE}/src/player/sub.php"
+ private const val URL_SEARCH = "${URL_BASE}/search"
+ private const val PAGE_INITIAL = 1
+ }
+
+ override val tag: String
+ get() = TAG
+
+ override fun getInitialPageNumber(url: String): Int = PAGE_INITIAL
+
+ override fun getSortFilters(url: String): List<SortFilter> = emptyList()
+
+ override suspend fun getHome(
+ request: SourceRequest.Home
+ ): Resource<SourceScreen.Home> = parseHtml(
+ httpClient,
+ HttpRequestData(URL_HOME),
+ ) { document, _ ->
+ val components: ArrayList<SourceComponent> = arrayListOf()
+
+ components += getHomeHeaders()
+
+ val azLinks = document.toPageLinks("ul.ulclear.az-list a")
+ if (azLinks.isNotEmpty()) {
+ components.add(SourceComponent.ContentsComponent("A-Z", pageLinks = azLinks))
+ }
+
+ val mediaSlider = document.toSliderMediaList()
+ if (mediaSlider.isNotEmpty()) {
+ components.add(
+ SourceComponent.ContentsComponent(
+ heading = "Spotlight",
+ sourceMediaSlider = SourceMediaSlider(mediaSlider),
+ )
+ )
+ }
+
+ document.select("div.anif-block").forEach { elements ->
+ val heading = elements.select("div.anif-block-header").text()
+ val href = elements.select("div.more a").attr("href")
+ val pageUrl = if (href.isEmpty()) null else URL_BASE + href
+ val mediaList = elements.toAnifBlockMediaList()
+ if (mediaList.isNotEmpty()) {
+ components.add(
+ SourceComponent.ContentsComponent(
+ heading = heading,
+ pageUrl = pageUrl,
+ mediaList = mediaList,
+ )
+ )
+ }
+ }
+
+ document.select("section.block_area").forEach { elements ->
+ val heading = elements.select(".cat-heading").text()
+ val href = elements.select("div.float-right.viewmore a").attr("href")
+ val pageUrl = if (href.isEmpty()) null else URL_BASE + href
+ val mediaList = elements.toMediaList()
+ if (mediaList.isNotEmpty()) {
+ components.add(
+ SourceComponent.ContentsComponent(
+ heading = heading,
+ pageUrl = pageUrl,
+ mediaList = mediaList,
+ )
+ )
+ }
+ }
+
+ listOf(
+ Pair("#top-viewed-day", "Top today"),
+ Pair("#top-viewed-week", "Top week"),
+ Pair("#top-viewed-month", "Top month"),
+ ).forEach { pair ->
+ val mediaList = document.select(pair.first).mapNotNull { elements ->
+ val list = elements.toTopMediaList()
+ if (list.isEmpty()) return@mapNotNull null
+ list
+ }.flatten().distinctBy { it.sourceRequest }
+ if (mediaList.isNotEmpty()) {
+ components.add(
+ SourceComponent.ContentsComponent(
+ heading = pair.second,
+ mediaList = mediaList,
+ )
+ )
+ }
+ }
+
+ val trending = getTrendingMedia()
+ if (trending.isNotEmpty()) {
+ components.add(SourceComponent.ContentsComponent("Trending", mediaList = trending))
+ }
+
+ val schedule = getHomeSchedule()
+ if (schedule.isNotEmpty()) {
+ components.add(SourceComponent.ContentsComponent("Schedule", mediaLinks = schedule))
+ }
+
+ SourceScreen.Home(
+ title = sourceId.title,
+ components = components,
+ searchHint = "Search...",
+ )
+ }
+
+ override suspend fun getDetail(
+ request: SourceRequest.Detail,
+ ): Resource<SourceScreen.Detail> = parseHtml(
+ httpClient,
+ HttpRequestData(request.detailUrl),
+ ) { document, _ ->
+ val components: ArrayList<SourceComponent> = arrayListOf()
+
+ val title = document.select(".film-name.dynamic-name").text()
+ val imageUrl =
+ document.select("div.anisc-poster img").attr("src").replace("/w342/", "/w500/")
+ val metaData: ArrayList<MetaData> = arrayListOf()
+ val description = buildString {
+ document.select("div.anisc-info div.item.item-title").forEach { elements ->
+ append(elements.text())
+ append("\n")
+ }
+ }.trim()
+ metaData.add(MetaData.Description(description))
+ val sub = document.select(".film-stats .tick-item.tick-sub").text()
+ if (sub.isNotEmpty()) {
+ metaData.add(MetaData.EpisodesSubbed(sub))
+ }
+ val dub = document.select(".film-stats .tick-item.tick-dub").text()
+ if (dub.isNotEmpty()) {
+ metaData.add(MetaData.EpisodesDubbed(dub))
+ }
+ val sourceMedia = SourceMedia(
+ sourceRequest = request,
+ image = Image(
+ httpRequestData = HttpRequestData(imageUrl),
+ aspectRatio = 500F / 750F,
+ ),
+ title = title,
+ metaData = metaData,
+ )
+ components.add(SourceComponent.Detail(sourceMedia))
+
+ val tags = document.toPageLinks("div.anisc-info a")
+ if (tags.isNotEmpty()) {
+ components.add(SourceComponent.ContentsComponent("Tags", pageLinks = tags))
+ }
+
+ val seasons = document.toSeasons()
+ if (seasons.isNotEmpty()) {
+ components.add(SourceComponent.ContentsComponent("Seasons", mediaList = seasons))
+ }
+
+ val recommendations = document.toMediaList()
+ if (recommendations.isNotEmpty()) {
+ components.add(
+ SourceComponent.ContentsComponent(
+ heading = "Recommendations",
+ mediaList = recommendations,
+ )
+ )
+ }
+
+ SourceScreen.Detail(
+ request,
+ title = title,
+ components = components,
+ mediaRequest = SourceRequest.Episodes(request.detailUrl),
+ )
+ }
+
+ override suspend fun getEpisodes(
+ request: SourceRequest.Episodes,
+ ): Resource<SourceScreen.Episodes> = parseHtml(
+ httpClient,
+ HttpRequestData(request.detailUrl.replace("/details/", "/watch/")),
+ ) { document, _ ->
+ val components: ArrayList<SourceComponent> = arrayListOf()
+ val title = document.select("div.anis-content .film-name").text()
+ val imageUrl = document.select("div.anisc-poster img").attr("src")
+ val image = Image(HttpRequestData(imageUrl), 500F / 750F)
+
+ val episodes = document.select("a.ssl-item.ep-item").mapNotNull { elements ->
+ val episodeTitle = elements.select("div.ep-name").text().trim()
+ val episodeNumber = elements.attr("data-episodeno")
+ val episodeUrl = elements.attr("data-id")
+ if (episodeTitle.isEmpty() || episodeNumber.isEmpty() || episodeUrl.isEmpty()) return@mapNotNull null
+ SourceEpisode.EpisodeText(
+ sourceRequest = SourceRequest.Media(
+ detailUrl = request.detailUrl,
+ episodeUrl = episodeUrl,
+ title = title,
+ description = episodeTitle,
+ image = image,
+ ),
+ number = episodeNumber,
+ title = episodeTitle,
+ )
+ }
+ if (episodes.isNotEmpty()) {
+ components.add(SourceComponent.Episodes(episodes))
+ }
+
+ SourceScreen.Episodes(
+ request,
+ title = title,
+ components = components,
+ )
+ }
+
+ override suspend fun getMedia(
+ request: SourceRequest.Media, callback: suspend (SourceScreen) -> Unit
+ ): Resource<SourceScreen.Media> {
+ val slug = request.episodeUrl
+ if (slug.isNullOrEmpty()) return Resource.Error(null, Throwable("Param missing."))
+
+ val servers: Map<String, List<Server>> = httpClient.get(URL_SERVER) {
+ header(HttpHeaders.Referrer, "${URL_BASE}/watch/${slug}")
+ parameter("episodeId", slug)
+ }.body()
+ logger.debug("Servers: $servers", TAG)
+
+ val embedLinks = servers.map { (key, value) ->
+ value.mapIndexedNotNull { index, server ->
+ logger.debug("Trying $key ${server.serverName}", TAG)
+ return@mapIndexedNotNull try {
+ getEmbedLink(slug, server.serverName, key, request.detailUrl)
+ } catch (e: Exception) {
+ logger.error("$key ${server.serverId} ${server.serverName} failed.", e, TAG)
+ null
+ }
+ }
+ }.flatten().distinctBy { it.httpRequestData.url }
+
+ return Resource.Success(
+ SourceScreen.Media(
+ request,
+ MediaPlayerItem(embedLinks = embedLinks),
+ )
+ )
+ }
+
+ override suspend fun getPaging(
+ request: SourceRequest.Paging,
+ ): Resource<SourceScreen.Paging> = parseHtml(
+ httpClient,
+ HttpRequestData(
+ url = request.pageUrl,
+ queries = mapOf("page" to request.pageNumber.toString()),
+ )
+ ) { document, _ ->
+ val components: ArrayList<SourceComponent> = arrayListOf()
+ val mediaList = document.toMediaList()
+ if (mediaList.isNotEmpty()) {
+ components.add(SourceComponent.PagingComponent(mediaList = mediaList))
+ }
+ SourceScreen.Paging(request, components)
+ }
+
+ override suspend fun getSearch(
+ request: SourceRequest.Search,
+ ): Resource<SourceScreen.Search> = parseHtml(
+ httpClient,
+ HttpRequestData(
+ url = URL_SEARCH,
+ queries = mapOf(
+ "page" to request.pageNumber.toString(),
+ "keyword" to request.query,
+ ),
+ )
+ ) { document, _ ->
+ val components: ArrayList<SourceComponent> = arrayListOf()
+ val mediaList = document.toMediaList()
+ if (mediaList.isNotEmpty()) {
+ components.add(SourceComponent.PagingComponent(mediaList = mediaList))
+ }
+ SourceScreen.Search(request, components)
+ }
+
+ private fun Element.toPageLinks(selector: String): List<PageLink> =
+ this.select(selector).mapNotNull { element ->
+ val title = element.text().ifEmpty { element.attr("title") }
+ val url = element.attr("href").replace("../", "/")
+ if (title.isEmpty() || url.isEmpty()) return@mapNotNull null
+ if (url == "/") return@mapNotNull null
+ if (url.endsWith("/home")) return@mapNotNull null
+ PageLink(title, if (url.startsWith(URL_BASE)) url else URL_BASE + url)
+ }.distinctBy { it.url }
+
+ private fun Element.toMediaList(): List<SourceMedia> =
+ this.select("div.flw-item").mapNotNull { element ->
+ val url = element.select("a").attr("href")
+ val title = element.select("div.film-detail .film-name a").text()
+ val imageUrl = element.select("img.film-poster-img").attr("data-src")
+ if (title.isEmpty() || url.isEmpty() || imageUrl.isEmpty()) return@mapNotNull null
+ val metaData: ArrayList<MetaData> = arrayListOf()
+ val duration = element.select(".fdi-item.fdi-duration").text()
+ if (duration.isNotEmpty()) {
+ metaData.add(MetaData.Duration(duration))
+ }
+ val sub = element.select(".tick-item.tick-sub").text()
+ if (sub.isNotEmpty()) {
+ metaData.add(MetaData.EpisodesSubbed(sub))
+ }
+ val dub = element.select(".tick-item.tick-dub").text()
+ if (dub.isNotEmpty()) {
+ metaData.add(MetaData.EpisodesDubbed(dub))
+ }
+ val episodeCount = element.select(".tick-item.tick-eps").text()
+ if (episodeCount.isNotEmpty()) {
+ metaData.add(MetaData.EpisodesCount(episodeCount))
+ }
+ SourceMedia(
+ sourceRequest = SourceRequest.Detail(URL_BASE + url),
+ image = Image(
+ httpRequestData = HttpRequestData(imageUrl),
+ aspectRatio = 300F / 450F,
+ ),
+ title = title,
+ metaData = metaData,
+ )
+ }.distinctBy { it.sourceRequest }
+
+ private fun Element.toAnifBlockMediaList(): List<SourceMedia> =
+ this.select("li").mapNotNull { element ->
+ val url = element.select("div.film-detail a").attr("href")
+ val title = element.select("div.film-detail a").attr("data-en").ifEmpty {
+ element.select("div.film-detail a").attr("title").ifEmpty {
+ element.select("div.film-detail a").text()
+ }
+ }
+ val imageUrl =
+ element.select("div.film-poster img").attr("data-src").replace("/w92/", "/w342/")
+ if (title.isEmpty() || url.isEmpty() || imageUrl.isEmpty()) return@mapNotNull null
+ SourceMedia(
+ sourceRequest = SourceRequest.Detail(URL_BASE + url),
+ image = Image(
+ httpRequestData = HttpRequestData(imageUrl),
+ aspectRatio = 342F / 489F,
+ ),
+ title = title,
+ )
+ }.distinctBy { it.sourceRequest }
+
+ private fun Element.toTopMediaList(): List<SourceMedia> =
+ this.select("li").mapNotNull { element ->
+ val url = element.select("div.film-detail a").attr("href")
+ val title = element.select("div.film-detail .film-name").text()
+ val imageUrl =
+ element.select("div.film-poster img").attr("data-src").replace("/w92/", "/w342/")
+ if (title.isEmpty() || url.isEmpty() || imageUrl.isEmpty()) return@mapNotNull null
+ val metaData: ArrayList<MetaData> = arrayListOf()
+ val sub = element.select(".tick-item.tick-sub").text()
+ if (sub.isNotEmpty()) {
+ metaData.add(MetaData.EpisodesSubbed(sub))
+ }
+ val dub = element.select(".tick-item.tick-dub").text()
+ if (dub.isNotEmpty()) {
+ metaData.add(MetaData.EpisodesDubbed(dub))
+ }
+ val episodeCount = element.select(".tick-item.tick-eps").text()
+ if (episodeCount.isNotEmpty()) {
+ metaData.add(MetaData.EpisodesCount(episodeCount))
+ }
+ SourceMedia(
+ sourceRequest = SourceRequest.Detail(URL_BASE + url),
+ image = Image(
+ httpRequestData = HttpRequestData(imageUrl),
+ aspectRatio = 342F / 489F,
+ ),
+ title = title,
+ metaData = metaData,
+ )
+ }.distinctBy { it.sourceRequest }
+
+ private fun Element.toSliderMediaList(): List<SourceMedia> =
+ this.select("div.swiper-slide").mapNotNull { elements ->
+ val detailUrl =
+ elements.select("div.desi-buttons a.btn.btn-secondary.btn-radius").attr("href")
+ val imageUrl = elements.select("img.film-backdrop-img").attr("src")
+ val title = elements.select("div.desi-head-title.dynamic-name").text()
+ if (detailUrl.isEmpty() || imageUrl.isEmpty() || title.isEmpty()) return@mapNotNull null
+ val metaData: ArrayList<MetaData> = arrayListOf()
+ val duration = elements.select(".scd-item:eq(1)").text()
+ if (duration.isNotEmpty()) {
+ metaData.add(MetaData.Duration(duration))
+ }
+ val date = elements.select(".scd-item:eq(2)").text()
+ if (date.isNotEmpty()) {
+ metaData.add(MetaData.ReleaseDate(date))
+ }
+ val description = elements.select("div.desi-description").text()
+ if (description.isNotEmpty()) {
+ metaData.add(MetaData.Description(description))
+ }
+ SourceMedia(
+ sourceRequest = SourceRequest.Detail(URL_BASE + detailUrl),
+ image = Image(
+ httpRequestData = HttpRequestData(imageUrl),
+ aspectRatio = 780F / 439F,
+ ),
+ title = title,
+ metaData = metaData,
+ )
+ }.distinctBy { it.sourceRequest }
+
+ private fun Element.toTrendingMediaList(): List<SourceMedia> =
+ this.select("div.swiper-slide").mapNotNull { element ->
+ val url = element.select("a.film-poster").attr("href")
+ val title = element.select("div.film-title").text()
+ val imageUrl = element.select("img.film-poster-img").attr("data-src")
+ if (title.isEmpty() || url.isEmpty() || imageUrl.isEmpty()) return@mapNotNull null
+ SourceMedia(
+ sourceRequest = SourceRequest.Detail(URL_BASE + url),
+ image = Image(
+ httpRequestData = HttpRequestData(imageUrl),
+ aspectRatio = 185F / 278F,
+ ),
+ title = title,
+ )
+ }.distinctBy { it.sourceRequest }
+
+ private fun Element.toSeasons(): List<SourceMedia> =
+ this.select("a.os-item").mapNotNull { element ->
+ val url = element.attr("href")
+ val title = element.select("div.title").text()
+ val imageUrl = element.select("div.season-poster").attr("style").substringAfter("url(")
+ .substringBefore(");")
+ if (title.isEmpty() || url.isEmpty() || imageUrl.isEmpty()) return@mapNotNull null
+ SourceMedia(
+ sourceRequest = SourceRequest.Detail(url),
+ image = Image(
+ httpRequestData = HttpRequestData(imageUrl),
+ aspectRatio = 342F / 513F,
+ ),
+ title = title,
+ )
+ }.distinctBy { it.sourceRequest }
+
+ private suspend fun getHomeHeaders(): List<SourceComponent> {
+ val components: ArrayList<SourceComponent> = arrayListOf()
+ try {
+ val response = httpClient.get(URL_HOME_NAV_PAGE)
+ if (!response.status.isSuccess()) return emptyList()
+ val body = response.bodyAsText()
+ val document = Ksoup.parse(body)
+
+ val explore = document.select("ul.nav.sidebar_menu-list > li").mapNotNull { elements ->
+ val hasSubMenu = elements.toString().contains("sub-menu")
+ if (hasSubMenu) return@mapNotNull null
+ val title = elements.select("a.nav-link").attr("title")
+ val url = elements.select("a.nav-link").attr("href")
+ if (title.isEmpty() || url.isEmpty()) return@mapNotNull null
+ if (url == "/") return@mapNotNull null
+ if (url.endsWith("/home")) return@mapNotNull null
+ PageLink(title, if (url.startsWith(URL_BASE)) url else URL_BASE + url)
+ } + document.select("div.hs-toggles > a.hst-item").mapNotNull { elements ->
+ val title = elements.attr("data-original-title")
+ val url = elements.attr("href")
+ if (title.isEmpty() || url.isEmpty()) return@mapNotNull null
+ if (url.endsWith("/random")) return@mapNotNull null
+ PageLink(title, if (url.startsWith(URL_BASE)) url else URL_BASE + url)
+ }
+ if (explore.isNotEmpty()) {
+ components.add(
+ SourceComponent.ContentsComponent(
+ "Explore", pageLinks = explore.distinctBy { it.url })
+ )
+ }
+
+ document.select("li.nav-item").forEach { elements ->
+ val heading = elements.select("div.nav-link").text()
+ val pageLinks = elements.toPageLinks("ul.nav a")
+ if (pageLinks.isNotEmpty()) {
+ components.add(
+ SourceComponent.ContentsComponent(
+ heading = heading,
+ pageLinks = pageLinks,
+ )
+ )
+ }
+ }
+ } catch (e: Exception) {
+ logger.error("getHomeHeaders failed.", e, TAG)
+ }
+ return components
+ }
+
+ private suspend fun getHomeSchedule(): List<MediaLink> {
+ return try {
+ val response: ScheduleAnimeResponse = httpClient.get(URL_HOME_SCHEDULE) {
+ header(HttpHeaders.Referrer, URL_HOME)
+ header("X-Requested-With", "schedule-anime-world")
+ parameter("date", dateTime.getCurrentYYYYMMDD())
+ }.body()
+ response.results.map { anime ->
+ MediaLink(
+ title = anime.title,
+ sourceRequest = SourceRequest.Detail("${URL_BASE}/details/${anime.tmdbLink}"),
+ )
+ }
+ } catch (e: Exception) {
+ logger.error("getHomeSchedule failed.", e, TAG)
+ emptyList()
+ }
+ }
+
+ private suspend fun getTrendingMedia(): List<SourceMedia> {
+ return try {
+ val response = httpClient.get(URL_HOME_TRENDING)
+ if (!response.status.isSuccess()) return emptyList()
+ val body = response.bodyAsText()
+ val document = Ksoup.parse(body)
+ document.toTrendingMediaList()
+ } catch (e: Exception) {
+ logger.error("getHomeSchedule failed.", e, TAG)
+ emptyList()
+ }
+ }
+
+ private suspend fun getEmbedLink(
+ id: String,
+ server: String,
+ type: String,
+ detailUrl: String,
+ ): EmbedLink? {
+ if (server.equals("Download", ignoreCase = true)) {
+ logger.debug("Ignoring download server $id $server $type", TAG)
+ return null
+ }
+ val subResponse = httpClient.get(URL_SUB) {
+ parameter("id", id)
+ parameter("server", server)
+ header(HttpHeaders.Referrer, "${URL_BASE}/")
+ }
+ if (!subResponse.status.isSuccess()) {
+ logger.debug("Sub response $server $type failed.", TAG)
+ return null
+ }
+ val iFrameUrl = Ksoup.parse(subResponse.bodyAsText()).select("iframe").attr("src")
+
+ val iFrameResponse = httpClient.get(iFrameUrl) {
+ header(HttpHeaders.Referrer, URL_SUB)
+ }
+ if (!iFrameResponse.status.isSuccess()) {
+ logger.debug("iFrame response $server $type failed.", TAG)
+ return null
+ }
+ val embedUrl = Ksoup.parse(iFrameResponse.bodyAsText()).select("iframe").attr("src")
+ if (embedUrl.isEmpty()) {
+ logger.debug("Embed URL $server $type empty.", TAG)
+ return null
+ }
+ logger.debug("$type $server: $embedUrl", TAG)
+ return EmbedLink(
+ name = server,
+ httpRequestData = HttpRequestData(
+ url = embedUrl,
+ headers = detailUrl.getEmbedHeaders(),
+ )
+ )
+ }
+}
diff --git a/source/piratexplay/src/commonMain/kotlin/com/shivamkumarjha/source_piratexplay/model/ScheduleAnime.kt b/source/piratexplay/src/commonMain/kotlin/com/shivamkumarjha/source_piratexplay/model/ScheduleAnime.kt
new file mode 100644
index 00000000..aee6ff63
--- /dev/null
+++ b/source/piratexplay/src/commonMain/kotlin/com/shivamkumarjha/source_piratexplay/model/ScheduleAnime.kt
@@ -0,0 +1,17 @@
+package com.shivamkumarjha.source_piratexplay.model
+
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+
+@Serializable
+internal data class ScheduleAnime(
+ @SerialName("data_id") val dataId: String,
+ @SerialName("title") val title: String,
+ @SerialName("releaseDate") val releaseDate: String,
+ @SerialName("time") val time: String,
+ @SerialName("episode_no") val episodeNo: String,
+ @SerialName("tmdb_link") val tmdbLink: String,
+ @SerialName("starting_episode") val startingEpisode: Int,
+ @SerialName("season") val season: String,
+ @SerialName("updated") val updated: String,
+)
\ No newline at end of file
diff --git a/source/piratexplay/src/commonMain/kotlin/com/shivamkumarjha/source_piratexplay/model/ScheduleAnimeResponse.kt b/source/piratexplay/src/commonMain/kotlin/com/shivamkumarjha/source_piratexplay/model/ScheduleAnimeResponse.kt
new file mode 100644
index 00000000..3f099072
--- /dev/null
+++ b/source/piratexplay/src/commonMain/kotlin/com/shivamkumarjha/source_piratexplay/model/ScheduleAnimeResponse.kt
@@ -0,0 +1,9 @@
+package com.shivamkumarjha.source_piratexplay.model
+
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+
+@Serializable
+internal data class ScheduleAnimeResponse(
+ @SerialName("results") val results: List<ScheduleAnime>,
+)
\ No newline at end of file
diff --git a/source/piratexplay/src/commonMain/kotlin/com/shivamkumarjha/source_piratexplay/model/Server.kt b/source/piratexplay/src/commonMain/kotlin/com/shivamkumarjha/source_piratexplay/model/Server.kt
new file mode 100644
index 00000000..d3a4df00
--- /dev/null
+++ b/source/piratexplay/src/commonMain/kotlin/com/shivamkumarjha/source_piratexplay/model/Server.kt
@@ -0,0 +1,10 @@
+package com.shivamkumarjha.source_piratexplay.model
+
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+
+@Serializable
+internal data class Server(
+ @SerialName("serverName") val serverName: String,
+ @SerialName("serverId") val serverId: Int,
+)
\ No newline at end of file
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment