Created
October 11, 2025 19:49
-
-
Save ShivamKumarJha/ed31b38486a2e6f0fb531d7b0c29995b to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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