Merge branch 'Kohi-den:main' into main

This commit is contained in:
Zero 2025-05-10 15:41:05 +05:30 committed by GitHub
commit 5e1e4775f7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 499 additions and 34 deletions

View file

@ -1,22 +1,27 @@
package eu.kanade.tachiyomi.lib.buzzheavierextractor package eu.kanade.tachiyomi.lib.buzzheavierextractor
import eu.kanade.tachiyomi.animesource.model.Video import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.util.asJsoup import eu.kanade.tachiyomi.util.asJsoup
import eu.kanade.tachiyomi.util.parseAs import eu.kanade.tachiyomi.util.parseAs
import java.io.IOException
import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import okhttp3.Headers import okhttp3.Headers
import okhttp3.HttpUrl.Companion.toHttpUrl import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.internal.EMPTY_HEADERS import okhttp3.Request
import okhttp3.Response
class BuzzheavierExtractor( class BuzzheavierExtractor(
private val client: OkHttpClient, private val client: OkHttpClient,
private val headers: Headers, private val headers: Headers,
) { ) {
companion object {
private val SIZE_REGEX = Regex("""Size\s*-\s*([0-9.]+\s*[GMK]B)""")
}
@OptIn(ExperimentalSerializationApi::class) @OptIn(ExperimentalSerializationApi::class)
fun videosFromUrl(url: String, prefix: String = "Buzzheavier - ", proxyUrl: String? = null): List<Video> { fun videosFromUrl(url: String, prefix: String = "Buzzheavier - ", proxyUrl: String? = null): List<Video> {
val httpUrl = url.toHttpUrl() val httpUrl = url.toHttpUrl()
@ -24,29 +29,52 @@ class BuzzheavierExtractor(
val dlHeaders = headers.newBuilder().apply { val dlHeaders = headers.newBuilder().apply {
add("Accept", "*/*") add("Accept", "*/*")
add("Host", httpUrl.host)
add("HX-Current-URL", url) add("HX-Current-URL", url)
add("HX-Request", "true") add("HX-Request", "true")
add("Priority", "u=1, i")
add("Referer", url) add("Referer", url)
}.build() }.build()
val videoHeaders = headers.newBuilder().apply { val videoHeaders = headers.newBuilder().apply {
add("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7")
add("Priority", "u=0, i")
add("Referer", url) add("Referer", url)
}.build() }.build()
val path = client.newCall( val siteRequest = client.newCall(GET(url)).execute()
GET("https://${httpUrl.host}/$id/download", dlHeaders) val parsedHtml = siteRequest.asJsoup()
).execute().headers["hx-redirect"].orEmpty() val detailsText = parsedHtml.selectFirst("li:contains(Details:)")?.text() ?: ""
val size = SIZE_REGEX.find(detailsText)?.groupValues?.getOrNull(1)?.trim() ?: "Unknown"
return if (path.isNotEmpty()) { val downloadRequest = GET("https://${httpUrl.host}/$id/download", dlHeaders)
val videoUrl = if (path.startsWith("http")) path else "https://${httpUrl.host}$path" val path = client.executeWithRetry(downloadRequest, 5, 204).use { response ->
listOf(Video(videoUrl, "${prefix}Video", videoUrl, videoHeaders)) response.header("hx-redirect").orEmpty()
} else if (proxyUrl?.isNotEmpty() == true) {
val videoUrl = client.newCall(GET(proxyUrl + id)).execute().parseAs<UrlDto>().url
listOf(Video(videoUrl, "${prefix}Video", videoUrl, videoHeaders))
} else {
emptyList()
} }
val videoUrl = if (path.isNotEmpty()) {
if (path.startsWith("http")) path else "https://${httpUrl.host}$path"
} else if (proxyUrl?.isNotEmpty() == true) {
client.executeWithRetry(GET(proxyUrl + id), 5, 200).parseAs<UrlDto>().url
} else {
return emptyList()
}
return listOf(Video(videoUrl, "${prefix}${size}", videoUrl, videoHeaders))
}
private fun OkHttpClient.executeWithRetry(request: Request, maxRetries: Int, validCode: Int): Response {
var response: Response? = null
for (attempt in 0 until maxRetries) {
response?.close()
response = this.newCall(request).execute()
if (response.code == validCode) {
return response
}
if (attempt < maxRetries - 1) {
Thread.sleep(1000)
}
}
return response ?: throw IOException("Failed to execute request after $maxRetries attempts")
} }
@Serializable @Serializable

View file

@ -1,7 +1,7 @@
ext { ext {
extName = 'Hikari' extName = 'Hikari'
extClass = '.Hikari' extClass = '.Hikari'
extVersionCode = 21 extVersionCode = 22
} }
apply from: "$rootDir/common.gradle" apply from: "$rootDir/common.gradle"

View file

@ -29,24 +29,50 @@ data class AnimeDto(
val aniGenre: String? = null, val aniGenre: String? = null,
@SerialName("ani_studio") @SerialName("ani_studio")
val aniStudio: String? = null, val aniStudio: String? = null,
@SerialName("ani_producers")
val aniProducers: String? = null,
@SerialName("ani_stats") @SerialName("ani_stats")
val aniStats: Int? = null, val aniStats: Int? = null,
@SerialName("ani_time")
val aniTime: String? = null,
@SerialName("ani_ep")
val aniEp: String? = null,
@SerialName("ani_type")
val aniType: Int? = null,
@SerialName("ani_score")
val aniScore: Double? = null,
) { ) {
fun toSAnime(preferEnglish: Boolean): SAnime = SAnime.create().apply { fun toSAnime(preferEnglish: Boolean): SAnime = SAnime.create().apply {
url = uid url = uid
title = if (preferEnglish) aniEName?.takeUnless(String::isBlank) ?: aniName else aniName title = if (preferEnglish) aniEName?.takeUnless(String::isBlank) ?: aniName else aniName
thumbnail_url = aniPoster thumbnail_url = aniPoster
genre = aniGenre?.split(",")?.joinToString(transform = String::trim) genre = aniGenre?.split(",")?.joinToString(transform = String::trim)
author = aniStudio artist = aniStudio
author = aniProducers?.split(",")?.joinToString(transform = String::trim)
description = buildString { description = buildString {
aniScore?.let { append("Score: %.2f/10\n\n".format(it)) }
aniSynopsis?.trim()?.let(::append) aniSynopsis?.trim()?.let(::append)
append("\n\n") append("\n\n")
aniType?.let {
val type = when (it) {
1 -> "TV"
2 -> "Movie"
3 -> "OVA"
4 -> "ONA"
5 -> "Special"
else -> "Unknown"
}
append("Type: $type\n")
}
aniEp?.let { append("Total Episode count: $it\n") }
aniTime?.let { append("Runtime: $it\n") }
aniSynonyms?.let { append("Synonyms: $it") } aniSynonyms?.let { append("Synonyms: $it") }
}.trim() }.trim()
status = when (aniStats) { status = when (aniStats) {
1 -> SAnime.ONGOING 1 -> SAnime.UNKNOWN
2 -> SAnime.COMPLETED 2 -> SAnime.COMPLETED
3 -> SAnime.ONGOING
else -> SAnime.UNKNOWN else -> SAnime.UNKNOWN
} }
} }

View file

@ -294,11 +294,6 @@ class Hikari : AnimeHttpSource(), ConfigurableAnimeSource {
entries = PREF_PROVIDERS entries = PREF_PROVIDERS
entryValues = PREF_PROVIDERS_VALUE entryValues = PREF_PROVIDERS_VALUE
setDefaultValue(PREF_PROVIDERS_DEFAULT) setDefaultValue(PREF_PROVIDERS_DEFAULT)
setOnPreferenceChangeListener { _, newValue ->
@Suppress("UNCHECKED_CAST")
preferences.edit().putStringSet(key, newValue as Set<String>).commit()
}
}.also(screen::addPreference) }.also(screen::addPreference)
ListPreference(screen.context).apply { ListPreference(screen.context).apply {

View file

@ -1,7 +1,7 @@
ext { ext {
extName = 'WIT ANIME' extName = 'WIT ANIME'
extClass = '.WitAnime' extClass = '.WitAnime'
extVersionCode = 52 extVersionCode = 53
} }
apply from: "$rootDir/common.gradle" apply from: "$rootDir/common.gradle"

View file

@ -30,7 +30,7 @@ class WitAnime : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
override val name = "WIT ANIME" override val name = "WIT ANIME"
override val baseUrl = "https://witanime.pics" override val baseUrl = "https://witanime.cyou"
override val lang = "ar" override val lang = "ar"

View file

@ -2,7 +2,7 @@ ext {
extName = 'AniPlay' extName = 'AniPlay'
extClass = '.AniPlay' extClass = '.AniPlay'
themePkg = 'anilist' themePkg = 'anilist'
overrideVersionCode = 19 overrideVersionCode = 20
} }
apply from: "$rootDir/common.gradle" apply from: "$rootDir/common.gradle"

View file

@ -285,9 +285,8 @@ class AniPlay : AniListAnimeHttpSource(), ConfigurableAnimeSource {
try { try {
when (serverName) { when (serverName) {
// yuki "Yuki" -> {
PREF_SERVER_ENTRIES[1] -> { val url = "https://yukiproxy.aniplaynow.live/m3u8-proxy?url=${defaultSource.url}&headers={\"Referer\":\"https://megacloud.club/\"}"
val url = "https://yukiprox.aniplaynow.live/m3u8-proxy?url=${defaultSource.url}&headers={\"Referer\":\"https://megacloud.club/\"}"
return playlistUtils.extractFromHls( return playlistUtils.extractFromHls(
playlistUrl = url, playlistUrl = url,
videoNameGen = { quality -> "$serverName - $quality - $typeName" }, videoNameGen = { quality -> "$serverName - $quality - $typeName" },
@ -308,9 +307,8 @@ class AniPlay : AniListAnimeHttpSource(), ConfigurableAnimeSource {
}, },
) )
} }
// pahe "Pahe" -> {
PREF_SERVER_ENTRIES[2] -> { val url = "https://paheproxy.aniplaynow.live/proxy?url=${defaultSource.url}&headers={\"Referer\":\"https://kwik.si/\"}"
val url = "https://prox.aniplaynow.live/?url=${defaultSource.url}&ref=https://kwik.si"
val headers = headers.newBuilder().apply { val headers = headers.newBuilder().apply {
set("Accept", "*/*") set("Accept", "*/*")
set("Origin", baseUrl) set("Origin", baseUrl)
@ -565,8 +563,8 @@ class AniPlay : AniListAnimeHttpSource(), ConfigurableAnimeSource {
private const val PREF_DOMAIN_DEFAULT = "aniplaynow.live" private const val PREF_DOMAIN_DEFAULT = "aniplaynow.live"
private const val PREF_SERVER_KEY = "server" private const val PREF_SERVER_KEY = "server"
private val PREF_SERVER_ENTRIES = arrayOf("Maze", "Yuki", "Pahe", "Kuro") private val PREF_SERVER_ENTRIES = arrayOf("Pahe", "Yuki") // , "Hika")
private val PREF_SERVER_ENTRY_VALUES = arrayOf("maze", "yuki", "pahe", "kuro") private val PREF_SERVER_ENTRY_VALUES = arrayOf("pahe", "yuki") // , "hika")
private const val PREF_SERVER_DEFAULT = "yuki" private const val PREF_SERVER_DEFAULT = "yuki"
private const val SERVER_UNKNOWN = "Other" private const val SERVER_UNKNOWN = "Other"

View file

@ -0,0 +1,12 @@
ext {
extName = 'JPFilms'
extClass = '.JPFilms'
extVersionCode = 1
isNsfw = true
}
apply from: "$rootDir/common.gradle"
dependencies {
implementation(project(":lib:playlist-utils"))
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

View file

@ -0,0 +1,406 @@
package eu.kanade.tachiyomi.animeextension.en.jpfilms
import android.app.Application
import android.content.SharedPreferences
import android.util.Log
import androidx.preference.ListPreference
import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
import eu.kanade.tachiyomi.animesource.model.SAnime
import eu.kanade.tachiyomi.animesource.model.SEpisode
import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.animesource.online.ParsedAnimeHttpSource
import eu.kanade.tachiyomi.lib.playlistutils.PlaylistUtils
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.util.asJsoup
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import okhttp3.Headers
import okhttp3.Request
import okhttp3.Response
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
class JPFilms : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
override val name = "JPFilms"
override val baseUrl = "https://jp-films.com"
override val lang = "en"
override val supportsLatest = true
private val preferences: SharedPreferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
}
// ============================== Popular Anime ==============================
override fun popularAnimeSelector(): String =
"div.item"
override fun popularAnimeRequest(page: Int): Request = GET("https://jp-films.com/wp-content/themes/halimmovies/halim-ajax.php?action=halim_get_popular_post&showpost=50&type=all")
override fun popularAnimeFromElement(element: Element): SAnime {
val anime = SAnime.create()
anime.setUrlWithoutDomain(element.select("a").attr("href"))
anime.title = element.select("h3.title").text()
anime.thumbnail_url = element.selectFirst("img")?.attr("abs:data-src")
Log.d("JPFilmsDebug", "Thumbnail URL: ${anime.thumbnail_url}")
return anime
}
override fun popularAnimeNextPageSelector(): String? = null
// ============================== Latest Anime ==============================
override fun latestUpdatesSelector(): String =
"#ajax-vertical-widget-movie > div.item, " +
"#ajax-vertical-widget-tv_series > div.item"
override fun latestUpdatesRequest(page: Int): Request = GET(baseUrl)
override fun latestUpdatesFromElement(element: Element): SAnime {
val anime = SAnime.create()
anime.setUrlWithoutDomain(element.select("a").attr("href"))
anime.title = element.select("h3.title").text()
anime.thumbnail_url = element.select("img").attr("data-src")
Log.d("JPFilmsDebug", "Poster: ${anime.thumbnail_url}")
return anime
}
override fun latestUpdatesNextPageSelector(): String? = null
// ============================== Search Anime ==============================
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
val searchQuery = query.replace(" ", "+")
return GET("$baseUrl/?s=$searchQuery", headers)
}
override fun searchAnimeSelector(): String = "#main-contents > section > div.halim_box > article"
override fun searchAnimeFromElement(element: Element): SAnime {
val anime = SAnime.create()
anime.setUrlWithoutDomain(element.select("a.halim-thumb").attr("href"))
anime.title = element.select("a.halim-thumb").attr("title")
anime.thumbnail_url = element.select("img").attr("data-src")
Log.d("JPFilmsDebug", "Poster: ${anime.thumbnail_url}")
return anime
}
override fun searchAnimeNextPageSelector(): String? = null
// ============================== Anime Details ==============================
override suspend fun getAnimeDetails(anime: SAnime): SAnime {
val document = client.newCall(GET(baseUrl + anime.url, headers)).execute().asJsoup()
anime.title = document.select("h1.entry-title").text()
anime.genre = document.select("p.category a").joinToString(", ") { it.text() }
anime.description = document.select("#content > div > div.entry-content.htmlwrap.clearfix > div.video-item.halim-entry-box article p").text()
anime.thumbnail_url = document.select("#content > div > div.halim-movie-wrapper.tpl-2 > div > div.movie-poster.col-md-4 > img").attr("data-src")
anime.author = "forsyth47"
return anime
}
override fun animeDetailsParse(document: Document): SAnime = throw UnsupportedOperationException()
// ============================== Episode List ==============================
@Serializable
data class JsonLdData(
@SerialName("@type") val type: String? = null,
)
override fun episodeListSelector(): String {
throw UnsupportedOperationException("Not used because we override episodeListParse.")
}
override fun episodeListParse(response: Response): List<SEpisode> {
val document = response.asJsoup()
// Extract JSON-LD data to determine if it's a Movie or TVSeries
val jsonLdScript = document.selectFirst("script[type=application/ld+json]:not(.rank-math-schema)")?.data()
Log.d("JPFilmsDebug", "JSON-LD Script: $jsonLdScript")
val jsonLdData = json.decodeFromString<JsonLdData>(jsonLdScript ?: "{}")
Log.d("JPFilmsDebug", "JSON-LD Data: $jsonLdData")
val isMovie = jsonLdData.type == "Movie"
Log.d("JPFilmsDebug", "Type: ${if (isMovie) "Movie" else "TVSeries"}")
val serverAvailable = document.select("#halim-list-server > ul > li")
Log.d("JPFilmsDebug", "Server Available: $serverAvailable")
var freeServerFound: Boolean = false
val episodeContainerSelector = run {
freeServerFound = false
var selectedContainer: String? = null
// Iterate through each server div
for (serverDiv in serverAvailable) {
Log.d("JPFilmsDebug", "Server Div: $serverDiv")
// Log the title of the current server div
val title = serverDiv.select("li > a").text()
Log.d("JPFilmsDebug", "Server Div Title: $title")
// Check if the current server contains a <li> with a title containing "FREE"
val hasFreeServer = title.contains("FREE")
Log.d("JPFilmsDebug", "Has Free Server: $hasFreeServer")
if (hasFreeServer) {
// Mark that a FREE server was found
freeServerFound = true
Log.d("JPFilmsDebug", "FREE Server Found")
// Select this server's container
selectedContainer = "${serverDiv.select("a").attr("href")} > div > ul"
break // Exit the loop once a FREE server is found
} else if (!freeServerFound) {
// If no FREE server is found yet, select the first available server
selectedContainer = "${serverDiv.select("a").attr("href")} > div > ul"
}
}
// Return the selected container or an empty string if none is found
selectedContainer ?: ""
}
Log.d("JPFilmsDebug", "Episode Container Selector: $episodeContainerSelector")
// Extract all <li> elements from the selected container
val episodeElements = document.select("$episodeContainerSelector > li")
Log.d("JPFilmsDebug", "Episode Elements: $episodeElements")
return episodeElements.map { element ->
SEpisode.create().apply {
// Get the href attribute from either the anchor tag or the span tag
var href = if (element.select("a").hasAttr("href")) {
element.select("a").attr("href")
} else {
element.select("span").attr("data-href")
}
if (!freeServerFound) {
href = "$href?svid=2"
}
setUrlWithoutDomain(href)
Log.d("JPFilmsDebug", "Episode URL: $href")
// Determine if the episode belongs to a FREE or VIP server
val isFreeServer = element.select("a").attr("title").contains("FREE") ||
element.select("span").text().contains("FREE")
val serverPrefix = if (isFreeServer) "[FREE] " else "[VIP] "
// Use the title attribute of the anchor tag as the episode name
name = serverPrefix + (
element.select("a").attr("title").ifEmpty {
element.select("span").text()
}
)
Log.d("JPFilmsDebug", "Episode Name: $name")
// Generate an episode number based on the text content
episode_number = element.text()
.filter { it.isDigit() }
.toFloatOrNull() ?: 1F
Log.d("JPFilmsDebug", "Episode Number: $episode_number")
}
}.reversed()
}
override fun episodeFromElement(element: Element): SEpisode {
throw UnsupportedOperationException("Not used because we override episodeListParse.")
}
// ============================== Video List ==============================
// Define the JSON serializer
private val json = Json { ignoreUnknownKeys = true }
override fun videoListParse(response: Response): List<Video> {
Log.d("JPFilmsDebug", "Episode Chosen Response URL: ${response.request.url}")
val document = response.asJsoup()
val postId = extractPostId(document)
val episodeSlug = response.request.url.pathSegments.last().split("-").dropLast(1).joinToString("-")
// Debugging: Log episode slug and post ID
Log.d("JPFilmsDebug", "Episode Slug: $episodeSlug")
Log.d("JPFilmsDebug", "Post ID: $postId")
// Helper function to construct the player URL
fun getPlayerUrl(serverId: Int, subsvId: String? = null): String {
return "$baseUrl/wp-content/themes/halimmovies/player.php?" +
"episode_slug=$episodeSlug&" +
"server_id=$serverId&" +
(if (subsvId != null) "subsv_id=$subsvId&" else "") +
"post_id=$postId&" +
"nonce=8c934fd387&custom_var="
}
// Create custom headers to match Postman
val customHeaders = Headers.Builder()
.add("sec-ch-ua-platform", "\"macOS\"")
.add("X-Requested-With", "XMLHttpRequest")
.add("User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36")
.add("Accept", "text/html, */*; q=0.01")
.add("sec-ch-ua", "\"Brave\";v=\"135\", \"Not-A.Brand\";v=\"8\", \"Chromium\";v=\"135\"")
.add("DNT", "1")
.add("sec-ch-ua-mobile", "?0")
.add("Sec-GPC", "1")
.add("Sec-Fetch-Site", "same-origin")
.add("Sec-Fetch-Mode", "cors")
.add("Sec-Fetch-Dest", "empty")
.add("Host", "jp-films.com")
.build()
// Helper function to fetch and parse the player response
fun fetchAndParsePlayerResponse(playerUrl: String): Pair<String, String> {
// Debugging: Log the constructed player URL
Log.d("JPFilmsDebug", "Player URL: $playerUrl")
// Make the request with custom headers
val playerResponse = client.newCall(GET(playerUrl, customHeaders)).execute().body.string()
// Debugging: Log the player response
Log.d("JPFilmsDebug", "Player Response: $playerResponse")
// Parse the JSON response into a strongly-typed structure
val jsonResponse = json.decodeFromString<PlayerResponse>(playerResponse)
// Extract the 'sources' field from the parsed JSON
val sources = jsonResponse.data?.sources ?: ""
// Debugging: Log the extracted sources
Log.d("JPFilmsDebug", "Extracted Sources: $sources")
// Extract the HLS URL using string manipulation
val hlsUrl = sources.split("source src=\"").getOrNull(1)?.split("\" type=")?.getOrNull(0) ?: ""
// Debugging: Log the extracted HLS URL
Log.d("JPFilmsDebug", "Extracted HLS URL: $hlsUrl")
return Pair(sources, hlsUrl)
}
val serverAvailable = document.select("#halim-list-server > ul > li")
Log.d("JPFilmsDebug", "Server Available: $serverAvailable")
val episodeContainerSelector = run {
var freeServerFound: Boolean = false
var selectedContainer: String? = null
// Iterate through each server div
for (serverDiv in serverAvailable) {
Log.d("JPFilmsDebug", "Server Div: $serverDiv")
// Log the title of the current server div
val title = serverDiv.select("li > a").text()
Log.d("JPFilmsDebug", "Server Div Title: $title")
// Check if the current server contains a <li> with a title containing "FREE"
val hasFreeServer = title.contains("FREE")
Log.d("JPFilmsDebug", "Has Free Server: $hasFreeServer")
if (hasFreeServer) {
// Mark that a FREE server was found
freeServerFound = true
Log.d("JPFilmsDebug", "FREE Server Found")
// Select this server's container
selectedContainer = "${serverDiv.select("a").attr("href")} > div > ul"
break // Exit the loop once a FREE server is found
} else if (!freeServerFound) {
// If no FREE server is found yet, select the first available server
selectedContainer = "${serverDiv.select("a").attr("href")} > div > ul"
}
}
// Return the selected container or an empty string if none is found
selectedContainer ?: ""
}
val episodeElements = document.select("$episodeContainerSelector > li")
Log.d("JPFilmsDebug", "Episode Elements: $episodeElements")
val targetEpisodeElement = episodeElements.firstOrNull { element ->
element.select("span").attr("data-episode-slug") == episodeSlug
} ?: run {
Log.e("JPFilmsDebug", "No matching episode element found for slug: $episodeSlug")
return emptyList() // Exit early if no matching element is found
}
// Extract the server ID from the target <li> element
val serverId = targetEpisodeElement.select("span").attr("data-server").toIntOrNull() ?: 0
// Debugging: Log the extracted server ID
Log.d("JPFilmsDebug", "Extracted Server ID: $serverId")
// First attempt with server_id=serverId and no subsvId
var subsvId: String? = null
val playerUrl1 = getPlayerUrl(serverId = serverId, subsvId = subsvId)
val (_, hlsUrl1) = fetchAndParsePlayerResponse(playerUrl1)
// Retry with subsvId=2 if the first attempt fails
val hlsUrl = if (hlsUrl1.isEmpty()) {
subsvId = "2"
val playerUrl2 = getPlayerUrl(serverId = serverId, subsvId = subsvId)
val (_, hlsUrl2) = fetchAndParsePlayerResponse(playerUrl2)
hlsUrl2
} else {
hlsUrl1
}
// Return the video list if the HLS URL is found, otherwise return an empty list
return if (hlsUrl.isNotEmpty()) {
PlaylistUtils(client).extractFromHls(hlsUrl, referer = baseUrl)
} else {
emptyList()
}
}
// Data classes for JSON parsing
@Serializable
data class PlayerResponse(
val data: PlayerData? = null,
)
@Serializable
data class PlayerData(
val status: Boolean? = null,
val sources: String? = null,
)
private fun extractPostId(document: Document): String {
val bodyClass = document.select("body").attr("class")
return Regex("postid-(\\d+)").find(bodyClass)?.groupValues?.get(1) ?: ""
}
override fun videoListSelector(): String = throw UnsupportedOperationException()
override fun videoFromElement(element: Element): Video = throw UnsupportedOperationException()
override fun videoUrlParse(document: Document): String = throw UnsupportedOperationException()
// ============================== ToDo ==============================
// Plan to add option to change between original title and translated title
// Plan to add backup server too.
// ============================== Preferences ==============================
override fun setupPreferenceScreen(screen: PreferenceScreen) {
ListPreference(screen.context).apply {
key = Companion.PREF_TITLE_STYLE_KEY
title = "Preferred Title Style"
entries = arrayOf("Original", "Translated")
entryValues = arrayOf("original", "translated")
setDefaultValue("translated")
summary = "%s"
setOnPreferenceChangeListener { _, newValue ->
preferences.edit().putString(key, newValue as String).commit()
}
}.also(screen::addPreference)
}
private val SharedPreferences.titleStyle
get() = getString(Companion.PREF_TITLE_STYLE_KEY, "translated")!!
companion object {
private const val PREF_TITLE_STYLE_KEY = "preferred_title_style"
}
}