Merge branch 'Kohi-den:main' into main
This commit is contained in:
commit
5e1e4775f7
15 changed files with 499 additions and 34 deletions
|
@ -1,22 +1,27 @@
|
|||
package eu.kanade.tachiyomi.lib.buzzheavierextractor
|
||||
|
||||
|
||||
import eu.kanade.tachiyomi.animesource.model.Video
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.util.asJsoup
|
||||
import eu.kanade.tachiyomi.util.parseAs
|
||||
import java.io.IOException
|
||||
import kotlinx.serialization.ExperimentalSerializationApi
|
||||
import kotlinx.serialization.Serializable
|
||||
import okhttp3.Headers
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.internal.EMPTY_HEADERS
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
|
||||
class BuzzheavierExtractor(
|
||||
private val client: OkHttpClient,
|
||||
private val headers: Headers,
|
||||
) {
|
||||
|
||||
companion object {
|
||||
private val SIZE_REGEX = Regex("""Size\s*-\s*([0-9.]+\s*[GMK]B)""")
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalSerializationApi::class)
|
||||
fun videosFromUrl(url: String, prefix: String = "Buzzheavier - ", proxyUrl: String? = null): List<Video> {
|
||||
val httpUrl = url.toHttpUrl()
|
||||
|
@ -24,29 +29,52 @@ class BuzzheavierExtractor(
|
|||
|
||||
val dlHeaders = headers.newBuilder().apply {
|
||||
add("Accept", "*/*")
|
||||
add("Host", httpUrl.host)
|
||||
add("HX-Current-URL", url)
|
||||
add("HX-Request", "true")
|
||||
add("Priority", "u=1, i")
|
||||
add("Referer", url)
|
||||
}.build()
|
||||
|
||||
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)
|
||||
}.build()
|
||||
|
||||
val path = client.newCall(
|
||||
GET("https://${httpUrl.host}/$id/download", dlHeaders)
|
||||
).execute().headers["hx-redirect"].orEmpty()
|
||||
val siteRequest = client.newCall(GET(url)).execute()
|
||||
val parsedHtml = siteRequest.asJsoup()
|
||||
val detailsText = parsedHtml.selectFirst("li:contains(Details:)")?.text() ?: ""
|
||||
val size = SIZE_REGEX.find(detailsText)?.groupValues?.getOrNull(1)?.trim() ?: "Unknown"
|
||||
|
||||
return if (path.isNotEmpty()) {
|
||||
val videoUrl = if (path.startsWith("http")) path else "https://${httpUrl.host}$path"
|
||||
listOf(Video(videoUrl, "${prefix}Video", videoUrl, videoHeaders))
|
||||
} 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 downloadRequest = GET("https://${httpUrl.host}/$id/download", dlHeaders)
|
||||
val path = client.executeWithRetry(downloadRequest, 5, 204).use { response ->
|
||||
response.header("hx-redirect").orEmpty()
|
||||
}
|
||||
|
||||
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
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
ext {
|
||||
extName = 'Hikari'
|
||||
extClass = '.Hikari'
|
||||
extVersionCode = 21
|
||||
extVersionCode = 22
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
||||
|
|
|
@ -29,24 +29,50 @@ data class AnimeDto(
|
|||
val aniGenre: String? = null,
|
||||
@SerialName("ani_studio")
|
||||
val aniStudio: String? = null,
|
||||
@SerialName("ani_producers")
|
||||
val aniProducers: String? = null,
|
||||
@SerialName("ani_stats")
|
||||
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 {
|
||||
url = uid
|
||||
title = if (preferEnglish) aniEName?.takeUnless(String::isBlank) ?: aniName else aniName
|
||||
thumbnail_url = aniPoster
|
||||
genre = aniGenre?.split(",")?.joinToString(transform = String::trim)
|
||||
author = aniStudio
|
||||
artist = aniStudio
|
||||
author = aniProducers?.split(",")?.joinToString(transform = String::trim)
|
||||
description = buildString {
|
||||
aniScore?.let { append("Score: %.2f/10\n\n".format(it)) }
|
||||
aniSynopsis?.trim()?.let(::append)
|
||||
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") }
|
||||
}.trim()
|
||||
|
||||
status = when (aniStats) {
|
||||
1 -> SAnime.ONGOING
|
||||
1 -> SAnime.UNKNOWN
|
||||
2 -> SAnime.COMPLETED
|
||||
3 -> SAnime.ONGOING
|
||||
else -> SAnime.UNKNOWN
|
||||
}
|
||||
}
|
||||
|
|
|
@ -294,11 +294,6 @@ class Hikari : AnimeHttpSource(), ConfigurableAnimeSource {
|
|||
entries = PREF_PROVIDERS
|
||||
entryValues = PREF_PROVIDERS_VALUE
|
||||
setDefaultValue(PREF_PROVIDERS_DEFAULT)
|
||||
|
||||
setOnPreferenceChangeListener { _, newValue ->
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
preferences.edit().putStringSet(key, newValue as Set<String>).commit()
|
||||
}
|
||||
}.also(screen::addPreference)
|
||||
|
||||
ListPreference(screen.context).apply {
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
ext {
|
||||
extName = 'WIT ANIME'
|
||||
extClass = '.WitAnime'
|
||||
extVersionCode = 52
|
||||
extVersionCode = 53
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
||||
|
|
|
@ -30,7 +30,7 @@ class WitAnime : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
|||
|
||||
override val name = "WIT ANIME"
|
||||
|
||||
override val baseUrl = "https://witanime.pics"
|
||||
override val baseUrl = "https://witanime.cyou"
|
||||
|
||||
override val lang = "ar"
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@ ext {
|
|||
extName = 'AniPlay'
|
||||
extClass = '.AniPlay'
|
||||
themePkg = 'anilist'
|
||||
overrideVersionCode = 19
|
||||
overrideVersionCode = 20
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
||||
|
|
|
@ -285,9 +285,8 @@ class AniPlay : AniListAnimeHttpSource(), ConfigurableAnimeSource {
|
|||
|
||||
try {
|
||||
when (serverName) {
|
||||
// yuki
|
||||
PREF_SERVER_ENTRIES[1] -> {
|
||||
val url = "https://yukiprox.aniplaynow.live/m3u8-proxy?url=${defaultSource.url}&headers={\"Referer\":\"https://megacloud.club/\"}"
|
||||
"Yuki" -> {
|
||||
val url = "https://yukiproxy.aniplaynow.live/m3u8-proxy?url=${defaultSource.url}&headers={\"Referer\":\"https://megacloud.club/\"}"
|
||||
return playlistUtils.extractFromHls(
|
||||
playlistUrl = url,
|
||||
videoNameGen = { quality -> "$serverName - $quality - $typeName" },
|
||||
|
@ -308,9 +307,8 @@ class AniPlay : AniListAnimeHttpSource(), ConfigurableAnimeSource {
|
|||
},
|
||||
)
|
||||
}
|
||||
// pahe
|
||||
PREF_SERVER_ENTRIES[2] -> {
|
||||
val url = "https://prox.aniplaynow.live/?url=${defaultSource.url}&ref=https://kwik.si"
|
||||
"Pahe" -> {
|
||||
val url = "https://paheproxy.aniplaynow.live/proxy?url=${defaultSource.url}&headers={\"Referer\":\"https://kwik.si/\"}"
|
||||
val headers = headers.newBuilder().apply {
|
||||
set("Accept", "*/*")
|
||||
set("Origin", baseUrl)
|
||||
|
@ -565,8 +563,8 @@ class AniPlay : AniListAnimeHttpSource(), ConfigurableAnimeSource {
|
|||
private const val PREF_DOMAIN_DEFAULT = "aniplaynow.live"
|
||||
|
||||
private const val PREF_SERVER_KEY = "server"
|
||||
private val PREF_SERVER_ENTRIES = arrayOf("Maze", "Yuki", "Pahe", "Kuro")
|
||||
private val PREF_SERVER_ENTRY_VALUES = arrayOf("maze", "yuki", "pahe", "kuro")
|
||||
private val PREF_SERVER_ENTRIES = arrayOf("Pahe", "Yuki") // , "Hika")
|
||||
private val PREF_SERVER_ENTRY_VALUES = arrayOf("pahe", "yuki") // , "hika")
|
||||
private const val PREF_SERVER_DEFAULT = "yuki"
|
||||
private const val SERVER_UNKNOWN = "Other"
|
||||
|
||||
|
|
12
src/en/jpfilms/build.gradle
Normal file
12
src/en/jpfilms/build.gradle
Normal 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"))
|
||||
}
|
BIN
src/en/jpfilms/res/mipmap-hdpi/ic_launcher.png
Normal file
BIN
src/en/jpfilms/res/mipmap-hdpi/ic_launcher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 6.8 KiB |
BIN
src/en/jpfilms/res/mipmap-mdpi/ic_launcher.png
Normal file
BIN
src/en/jpfilms/res/mipmap-mdpi/ic_launcher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.6 KiB |
BIN
src/en/jpfilms/res/mipmap-xhdpi/ic_launcher.png
Normal file
BIN
src/en/jpfilms/res/mipmap-xhdpi/ic_launcher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 10 KiB |
BIN
src/en/jpfilms/res/mipmap-xxhdpi/ic_launcher.png
Normal file
BIN
src/en/jpfilms/res/mipmap-xxhdpi/ic_launcher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 20 KiB |
BIN
src/en/jpfilms/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
BIN
src/en/jpfilms/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 31 KiB |
|
@ -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"
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue