New extension (#1590)

This commit is contained in:
Secozzi 2023-05-08 13:58:52 +02:00 committed by GitHub
parent afd337adad
commit 8f19319f4c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 807 additions and 0 deletions

View File

@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest />

View File

@ -0,0 +1,13 @@
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlinx-serialization'
ext {
extName = 'HolaMovies'
pkgNameSuffix = 'en.holamovies'
extClass = '.HolaMovies'
extVersionCode = 1
libVersion = '13'
}
apply from: "$rootDir/common.gradle"

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 93 KiB

View File

@ -0,0 +1,351 @@
package eu.kanade.tachiyomi.animeextension.en.holamovies
import android.app.Application
import android.content.SharedPreferences
import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.animeextension.en.holamovies.extractors.GDBotExtractor
import eu.kanade.tachiyomi.animeextension.en.holamovies.extractors.GDFlixExtractor
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.network.GET
import eu.kanade.tachiyomi.network.asObservableSuccess
import eu.kanade.tachiyomi.util.asJsoup
import kotlinx.serialization.json.Json
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import rx.Observable
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy
import java.text.SimpleDateFormat
import java.util.Locale
class HolaMovies : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
override val name = "HolaMovies"
override val baseUrl by lazy { preferences.getString("preferred_domain", "https://holamovies.org")!! }
override val lang = "en"
override val supportsLatest = false
override val client: OkHttpClient = network.cloudflareClient
private val json: Json by injectLazy()
private val preferences: SharedPreferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
}
companion object {
private val DateFormatter by lazy {
SimpleDateFormat("d MMMM yyyy", Locale.ENGLISH)
}
}
// ============================== Popular ===============================
override fun popularAnimeRequest(page: Int): Request = GET("$baseUrl/page/$page/")
override fun popularAnimeSelector(): String = "div#content > div > div.row > div"
override fun popularAnimeNextPageSelector(): String = "nav.gridlove-pagination > span.current + a"
override fun popularAnimeFromElement(element: Element): SAnime {
return SAnime.create().apply {
setUrlWithoutDomain(element.selectFirst("a")!!.attr("href").toHttpUrl().encodedPath)
thumbnail_url = element.selectFirst("img[src]")?.attr("src") ?: ""
title = element.selectFirst("h1")!!.text()
}
}
// =============================== Latest ===============================
override fun latestUpdatesRequest(page: Int): Request = GET(baseUrl)
override fun latestUpdatesSelector(): String = "div#latest-tab-pane > div.row > div.col-md-6"
override fun latestUpdatesNextPageSelector(): String? = popularAnimeNextPageSelector()
override fun latestUpdatesFromElement(element: Element): SAnime {
val thumbnailUrl = element.selectFirst("img")!!.attr("data-src")
return SAnime.create().apply {
setUrlWithoutDomain(element.selectFirst("a.animeparent")!!.attr("href"))
thumbnail_url = if (thumbnailUrl.contains(baseUrl.toHttpUrl().host)) {
thumbnailUrl
} else {
baseUrl + thumbnailUrl
}
title = element.selectFirst("span.animename")!!.text()
}
}
// =============================== Search ===============================
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
// val filterList = if (filters.isEmpty()) getFilterList() else filters
// val genreFilter = filterList.find { it is GenreFilter } as GenreFilter
// val recentFilter = filterList.find { it is RecentFilter } as RecentFilter
// val seasonFilter = filterList.find { it is SeasonFilter } as SeasonFilter
val cleanQuery = query.replace(" ", "+")
return when {
query.isNotBlank() -> GET("$baseUrl/page/$page/?s=$cleanQuery", headers)
// genreFilter.state != 0 -> GET("$baseUrl/genre/${genreFilter.toUriPart()}?page=$page")
// recentFilter.state != 0 -> GET("https://ajax.gogo-load.com/ajax/page-recent-release.html?page=$page&type=${recentFilter.toUriPart()}")
else -> GET("$baseUrl/popular.html?page=$page")
}
}
override fun searchAnimeSelector(): String = popularAnimeSelector()
override fun searchAnimeNextPageSelector(): String = popularAnimeNextPageSelector()
override fun searchAnimeFromElement(element: Element): SAnime = popularAnimeFromElement(element)
// ============================== FILTERS ===============================
// Todo - add these when the site starts working again
// override fun getFilterList(): AnimeFilterList = AnimeFilterList(
// AnimeFilter.Header("Text search ignores filters"),
// GenreFilter(),
// )
// =========================== Anime Details ============================
override fun fetchAnimeDetails(anime: SAnime): Observable<SAnime> {
return client.newCall(animeDetailsRequest(anime))
.asObservableSuccess()
.map { response ->
animeDetailsParse(response, anime).apply { initialized = true }
}
}
override fun animeDetailsParse(document: Document): SAnime = throw Exception("Not used")
private fun animeDetailsParse(response: Response, anime: SAnime): SAnime {
val document = response.asJsoup()
val oldAnime = anime
oldAnime.description = document.selectFirst("div.entry-content > p")?.text()
return oldAnime
}
// ============================== Episodes ==============================
override fun episodeListParse(response: Response): List<SEpisode> {
val document = response.asJsoup()
val episodeList = mutableListOf<SEpisode>()
val sizeRegex = Regex("""[\[\(](\d+\.?\d* ?[KMGT]B)[\]\)]""")
val zipRegex = Regex("""\bZIP\b""")
document.select("div.entry-content:has(h3,h4,p) > p:has(a[href]):not(:has(span.mb-text))").forEach {
it.select("a").forEach { link ->
if (zipRegex.find(link.text()) != null) return@forEach
val info = it.previousElementSiblings().firstOrNull { prevTag ->
arrayOf("h3", "h4", "p").contains(prevTag.normalName())
}?.text()
val size = sizeRegex.find(link.text())?.groupValues?.get(1)
val episode = SEpisode.create()
episode.name = link.text()
episode.episode_number = 1F
episode.date_upload = -1L
episode.url = link.attr("href")
episode.scanlator = "${if (size == null) "" else "$size • "}$info"
episodeList.add(episode)
}
}
// We don't want to parse multiple times
if (episodeList.isEmpty()) {
document.select("div.entry-content:has(pre:contains(episode)) > p:has(a[href])").reversed().forEach {
it.select("a").forEach { link ->
if (zipRegex.find(link.text()) != null) return@forEach
val info = it.previousElementSiblings().firstOrNull { prevTag ->
prevTag.normalName() == "p" && prevTag.selectFirst("strong") != null
}?.text()
val episodeNumber = it.previousElementSiblings().firstOrNull { prevTag ->
prevTag.normalName() == "pre" && prevTag.text().contains("episode", true)
}?.text()?.substringAfter(" ")?.toFloatOrNull() ?: 1F
val size = sizeRegex.find(link.text())?.groupValues?.get(1)
val episode = SEpisode.create()
episode.name = "Ep. $episodeNumber - ${link.text()}"
episode.episode_number = episodeNumber
episode.date_upload = -1L
episode.url = link.attr("href")
episode.scanlator = "${if (size == null) "" else "$size • "}$info"
episodeList.add(episode)
}
}
}
if (episodeList.isEmpty()) {
document.select("div.entry-content > p:has(a[href]:has(span.mb-text)), div.entry-content > em p:has(a[href]:has(span.mb-text))").reversed().forEach {
it.select("a").forEach { link ->
if (zipRegex.find(link.text()) != null) return@forEach
val title = it.previousElementSiblings().firstOrNull { prevTag ->
arrayOf("p", "h5").contains(prevTag.normalName()) && prevTag.text().isNotBlank()
}?.text() ?: "Item"
val size = sizeRegex.find(title)?.groupValues?.get(1)
?: sizeRegex.find(link.text())?.groupValues?.get(1)
val episode = SEpisode.create()
episode.name = "$title - ${link.text()}"
episode.episode_number = 1F
episode.date_upload = -1L
episode.scanlator = size
episode.url = link.attr("href")
episodeList.add(episode)
}
}
}
if (episodeList.isEmpty()) {
document.select("div.entry-content:has(pre:has(em)) > p:has(a[href])").reversed().forEach {
it.select("em a").forEach { link ->
if (zipRegex.find(link.text()) != null) return@forEach
if (link.text().contains("click here", true)) return@forEach
val title = it.previousElementSiblings().firstOrNull { prevTag ->
prevTag.normalName() == "pre"
}?.text()
val size = sizeRegex.find(link.text())?.groupValues?.get(1)
val episode = SEpisode.create()
episode.name = "$title - ${link.text()}"
episode.episode_number = 1F
episode.date_upload = -1L
episode.scanlator = size
episode.url = link.attr("href")
episodeList.add(episode)
}
}
}
if (episodeList.isEmpty()) {
document.select("div.entry-content:has(p:has(em)) > p:has(a[href])").reversed().forEach {
it.select("a").forEach { link ->
if (zipRegex.find(link.text()) != null) return@forEach
val info = it.previousElementSiblings().firstOrNull { prevTag ->
prevTag.normalName() == "p" && prevTag.text().isNotBlank() && prevTag.selectFirst("a") == null
}?.text() ?: "Item"
val size = sizeRegex.find(link.text())?.groupValues?.get(1)
val episode = SEpisode.create()
episode.name = link.text()
episode.episode_number = 1F
episode.date_upload = -1L
episode.scanlator = "${if (size == null) "" else "$size • "}$info"
episode.url = link.attr("href")
episodeList.add(episode)
}
}
}
if (episodeList.isEmpty()) {
document.select("div.entry-content > div.wp-block-buttons").reversed().forEach {
it.select("a").forEach { link ->
if (zipRegex.find(link.text()) != null) return@forEach
val info = it.previousElementSiblings().firstOrNull { prevTag ->
prevTag.normalName() == "pre" && prevTag.text().isNotBlank()
}?.text() ?: ""
val size = sizeRegex.find(link.text())?.groupValues?.get(1)
val episode = SEpisode.create()
episode.name = link.text()
episode.episode_number = 1F
episode.date_upload = -1L
episode.scanlator = "${if (size == null) "" else "$size • "}$info"
episode.url = link.attr("href")
episodeList.add(episode)
}
}
}
if (episodeList.isEmpty()) {
document.select("div.entry-content > figure.wp-block-embed").reversed().forEach {
it.select("a").forEach { link ->
if (zipRegex.find(link.text()) != null) return@forEach
val size = sizeRegex.find(link.text())?.groupValues?.get(1)
val episode = SEpisode.create()
episode.name = link.text()
episode.episode_number = 1F
episode.date_upload = -1L
episode.scanlator = size
episode.url = link.attr("href")
episodeList.add(episode)
}
}
}
if (episodeList.isEmpty()) {
document.select("div.entry-content > a[role=button][href]").reversed().forEach {
it.select("a").forEach { link ->
if (zipRegex.find(link.text()) != null) return@forEach
val size = sizeRegex.find(link.text())?.groupValues?.get(1)
val episode = SEpisode.create()
episode.name = link.text()
episode.episode_number = 1F
episode.date_upload = -1L
episode.scanlator = size
episode.url = link.attr("href")
episodeList.add(episode)
}
}
}
return episodeList
}
override fun episodeListSelector(): String = throw Exception("Not used")
override fun episodeFromElement(element: Element): SEpisode = throw Exception("Not used")
// ============================ Video Links =============================
override fun fetchVideoList(episode: SEpisode): Observable<List<Video>> {
val videoList = when {
episode.url.toHttpUrl().host.contains("gdflix") -> {
GDFlixExtractor(client, headers).videosFromUrl(episode.url)
}
episode.url.toHttpUrl().host.contains("gdtot") ||
episode.url.toHttpUrl().host.contains("gdbot") -> {
GDBotExtractor(client, headers).videosFromUrl(episode.url)
}
else -> { throw Exception("Unsupported url: ${episode.url}") }
}
return Observable.just(videoList.sort())
}
override fun videoFromElement(element: Element): Video = throw Exception("Not Used")
override fun videoListSelector(): String = throw Exception("Not Used")
override fun videoUrlParse(document: Document): String = throw Exception("Not Used")
// ============================= Utilities ==============================
override fun setupPreferenceScreen(screen: PreferenceScreen) {}
}

View File

@ -0,0 +1,41 @@
package eu.kanade.tachiyomi.animeextension.en.holamovies.extractors
import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.util.asJsoup
import okhttp3.Headers
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.OkHttpClient
class GDBotExtractor(private val client: OkHttpClient, private val headers: Headers) {
private val botUrl = "https://gdbot.xyz"
fun videosFromUrl(serverUrl: String): List<Video> {
val videoList = mutableListOf<Video>()
val docHeaders = headers.newBuilder()
.add("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8")
.add("Host", botUrl.toHttpUrl().host)
.build()
val fileId = serverUrl.substringAfter("/file/")
val document = client.newCall(
GET("$botUrl/file/$fileId", headers = docHeaders),
).execute().asJsoup()
document.select("li.py-6 > a[href]").forEach {
val url = it.attr("href")
when {
url.toHttpUrl().host.contains("gdflix") -> {
videoList.addAll(GDFlixExtractor(client, headers).videosFromUrl(url))
}
// url.toHttpUrl().host.contains("gdtot") -> {
// videoList.addAll(GDTotExtractor(client, headers).videosFromUrl(url))
// }
}
}
return videoList
}
}

View File

@ -0,0 +1,174 @@
package eu.kanade.tachiyomi.animeextension.en.holamovies.extractors
import android.util.Base64
import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.POST
import eu.kanade.tachiyomi.util.asJsoup
import kotlinx.serialization.Serializable
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import okhttp3.FormBody
import okhttp3.Headers
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.OkHttpClient
import uy.kohesive.injekt.injectLazy
class GDFlixExtractor(private val client: OkHttpClient, private val headers: Headers) {
private val json: Json by injectLazy()
fun videosFromUrl(serverUrl: String): List<Video> {
val videoList = mutableListOf<Video>()
val failedMediaUrl = mutableListOf<Pair<String, String>>()
if (serverUrl.toHttpUrl().encodedPath != "/404") {
val (videos, mediaUrl) = extractVideo(EpUrl("Video", serverUrl, "Video"))
if (videos.isEmpty()) failedMediaUrl.add(Pair(mediaUrl, "Video"))
videoList.addAll(videos)
}
videoList.addAll(
failedMediaUrl.mapNotNull { (url, quality) ->
runCatching {
extractGDriveLink(url, quality)
}.getOrNull()
}.flatten(),
)
videoList.addAll(
failedMediaUrl.mapNotNull { (url, quality) ->
runCatching {
extractDriveBotLink(url)
}.getOrNull()
}.flatten(),
)
return videoList
}
private fun extractVideo(epUrl: EpUrl): Pair<List<Video>, String> {
val videoList = mutableListOf<Video>()
val qualityRegex = """(\d+)p""".toRegex()
val matchResult = qualityRegex.find(epUrl.name)
val quality = if (matchResult == null) {
epUrl.quality
} else {
matchResult.groupValues[1]
}
for (type in 1..3) {
videoList.addAll(
extractWorkerLinks(epUrl.url, quality, type),
)
}
return Pair(videoList, epUrl.url)
}
private val sizeRegex = "\\[((?:.(?!\\[))+)][ ]*\$".toRegex(RegexOption.IGNORE_CASE)
private fun extractWorkerLinks(mediaUrl: String, quality: String, type: Int): List<Video> {
val reqLink = mediaUrl.replace("/file/", "/wfile/") + "?type=$type"
val resp = client.newCall(GET(reqLink)).execute().asJsoup()
val sizeMatch = sizeRegex.find(resp.select("div.card-header").text().trim())
val size = sizeMatch?.groups?.get(1)?.value?.let { " - $it" } ?: ""
return resp.select("div.card-body div.mb-4 > a").mapIndexed { index, linkElement ->
val link = linkElement.attr("href")
val decodedLink = if (link.contains("workers.dev")) {
link
} else {
String(Base64.decode(link.substringAfter("download?url="), Base64.DEFAULT))
}
Video(
url = decodedLink,
quality = "$quality - CF $type Worker ${index + 1}$size",
videoUrl = decodedLink,
)
}
}
private fun extractGDriveLink(mediaUrl: String, quality: String): List<Video> {
val tokenClient = client.newBuilder().addInterceptor(TokenInterceptor()).build()
val response = tokenClient.newCall(GET(mediaUrl)).execute().asJsoup()
val gdBtn = response.selectFirst("div.card-body a.btn")!!
val gdLink = gdBtn.attr("href")
return GoogleDriveExtractor(client, headers).videosFromUrl(gdLink, "Gdrive")
}
private fun extractDriveBotLink(mediaUrl: String): List<Video> {
val response = client.newCall(GET(mediaUrl)).execute().asJsoup()
val flixUrlPath = response.selectFirst("script:containsData(file)")?.data() ?: return emptyList()
val flixUrl = "https://${mediaUrl.toHttpUrl().host}${flixUrlPath.substringAfter("replace(\"").substringBefore("\"")}"
val flixDocument = client.newCall(GET(flixUrl)).execute().asJsoup()
val driveBotUrl = flixDocument.selectFirst("div.card-body a.btn[href~=drivebot]")?.attr("href") ?: return emptyList()
val docHeaders = headers.newBuilder()
.add("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8")
.add("Host", driveBotUrl.toHttpUrl().host)
.build()
val documentResp = OkHttpClient().newCall(
GET(driveBotUrl, headers = docHeaders),
).execute()
val document = documentResp.asJsoup()
val sessId = documentResp.headers.firstOrNull {
it.first.startsWith("set-cookie", true) && it.second.startsWith("PHPSESSID", true)
}?.second?.substringBefore(";") ?: ""
val script = document.selectFirst("script:containsData(token)")?.data() ?: return emptyList()
val token = script.substringAfter("'token', '").substringBefore("'")
val postUrl = "https://${driveBotUrl.toHttpUrl().host}${script.substringAfter("fetch('").substringBefore("'")}"
val postHeaders = headers.newBuilder()
.add("Accept", "*/*")
.add("Cookie", sessId)
.add("Host", driveBotUrl.toHttpUrl().host)
.add("Origin", "https://${driveBotUrl.toHttpUrl().host}")
.add("Referer", mediaUrl)
.add("Sec-Fetch-Site", "same-origin")
.build()
val postBody = FormBody.Builder()
.addEncoded("token", token)
.build()
val postResp = OkHttpClient().newCall(
POST(postUrl, body = postBody, headers = postHeaders),
).execute()
val url = try {
json.decodeFromString<DriveBotResp>(postResp.body.string()).url
} catch (a: Exception) {
return emptyList()
}
val videoHeaders = headers.newBuilder()
.add("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8")
.add("Host", url.toHttpUrl().host)
.add("Referer", "https://${driveBotUrl.toHttpUrl().host}/")
.build()
return listOf(
Video(url, "DriveBot", url, headers = videoHeaders),
)
}
@Serializable
data class EpUrl(
val quality: String,
val url: String,
val name: String,
)
@Serializable
data class DriveBotResp(
val url: String,
)
}

View File

@ -0,0 +1,41 @@
package eu.kanade.tachiyomi.animeextension.en.holamovies.extractors
import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.util.asJsoup
import okhttp3.Headers
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.OkHttpClient
class GDTotExtractor(private val client: OkHttpClient, private val headers: Headers) {
fun videosFromUrl(serverUrl: String): List<Video> {
val videoList = mutableListOf<Video>()
val docHeaders = headers.newBuilder()
.add("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8")
.add("Host", serverUrl.toHttpUrl().host)
.build()
val docResp = client.newCall(
GET(serverUrl, headers = docHeaders),
).execute()
val sessId = docResp.headers.firstOrNull {
it.first.startsWith("set-cookie", true) && it.second.startsWith("PHPSESSID", true)
}?.second?.substringBefore(";") ?: ""
val ddlUrl = serverUrl.replace("/file/", "/ddl/")
val ddlHeaders = docHeaders.newBuilder()
.add("Cookie", sessId)
.add("Referer", serverUrl)
.build()
val document = client.newCall(
GET(ddlUrl, headers = ddlHeaders),
).execute().asJsoup()
// TODO - Finish it
return videoList
}
}

View File

@ -0,0 +1,98 @@
package eu.kanade.tachiyomi.animeextension.en.holamovies.extractors
import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.POST
import eu.kanade.tachiyomi.util.asJsoup
import okhttp3.Headers
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient
import okhttp3.RequestBody.Companion.toRequestBody
class GoogleDriveExtractor(private val client: OkHttpClient, private val headers: Headers) {
// Needs to be the form of `https://drive.google.com/uc?id=GOOGLEDRIVEITEMID`
fun videosFromUrl(itemUrl: String, videoName: String = "Video"): List<Video> {
val itemHeaders = headers.newBuilder()
.add("Accept", "*/*")
.add("Accept-Language", "en-US,en;q=0.5")
.add("Connection", "keep-alive")
.add("Cookie", getCookie(itemUrl))
.add("Host", "drive.google.com")
.build()
val itemResponse = client.newCall(
GET(itemUrl, headers = itemHeaders),
).execute()
val noRedirectClient = OkHttpClient().newBuilder().followRedirects(false).build()
val document = itemResponse.asJsoup()
val itemSize = document.selectFirst("span.uc-name-size")?.let {
" ${it.ownText().trim()} "
} ?: ""
val url = document.selectFirst("form#download-form")?.attr("action") ?: return emptyList()
val redirectHeaders = headers.newBuilder()
.add("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8")
.add("Connection", "keep-alive")
.add("Content-Length", "0")
.add("Content-Type", "application/x-www-form-urlencoded")
.add("Cookie", getCookie(url))
.add("Host", "drive.google.com")
.add("Origin", "https://drive.google.com")
.add("Referer", url.substringBeforeLast("&at="))
.build()
val response = noRedirectClient.newCall(
POST(url, headers = redirectHeaders, body = "".toRequestBody("application/x-www-form-urlencoded".toMediaType())),
).execute()
val redirected = response.headers["location"] ?: return listOf(Video(url, videoName + itemSize, url))
val redirectedHeaders = headers.newBuilder()
.add("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8")
.add("Connection", "keep-alive")
.add("Host", redirected.toHttpUrl().host)
.add("Referer", "https://drive.google.com/")
.build()
val redirectedResponseHeaders = noRedirectClient.newCall(
GET(redirected, headers = redirectedHeaders),
).execute().headers
val authCookie = redirectedResponseHeaders.firstOrNull {
it.first == "set-cookie" && it.second.startsWith("AUTH_")
}?.second?.substringBefore(";") ?: return listOf(Video(url, videoName + itemSize, url))
val newRedirected = redirectedResponseHeaders["location"] ?: return listOf(Video(redirected, videoName + itemSize, redirected))
val googleDriveRedirectHeaders = headers.newBuilder()
.add("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8")
.add("Connection", "keep-alive")
.add("Cookie", getCookie(newRedirected))
.add("Host", "drive.google.com")
.add("Referer", "https://drive.google.com/")
.build()
val googleDriveRedirectUrl = noRedirectClient.newCall(
GET(newRedirected, headers = googleDriveRedirectHeaders),
).execute().headers["location"]!!
val videoHeaders = headers.newBuilder()
.add("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8")
.add("Connection", "keep-alive")
.add("Cookie", authCookie)
.add("Host", googleDriveRedirectUrl.toHttpUrl().host)
.add("Referer", "https://drive.google.com/")
.build()
return listOf(
Video(googleDriveRedirectUrl, videoName + itemSize, googleDriveRedirectUrl, headers = videoHeaders),
)
}
private fun getCookie(url: String): String {
val cookieList = client.cookieJar.loadForRequest(url.toHttpUrl())
return if (cookieList.isNotEmpty()) {
cookieList.joinToString("; ") { "${it.name}=${it.value}" }
} else {
""
}
}
}

View File

@ -0,0 +1,87 @@
package eu.kanade.tachiyomi.animeextension.en.holamovies.extractors
import android.annotation.SuppressLint
import android.app.Application
import android.os.Handler
import android.os.Looper
import android.webkit.JavascriptInterface
import android.webkit.WebView
import android.webkit.WebViewClient
import eu.kanade.tachiyomi.network.GET
import okhttp3.Interceptor
import okhttp3.Request
import okhttp3.Response
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.util.concurrent.CountDownLatch
class TokenInterceptor : Interceptor {
private val context = Injekt.get<Application>()
private val handler by lazy { Handler(Looper.getMainLooper()) }
class JsObject(private val latch: CountDownLatch, var payload: String = "") {
@JavascriptInterface
fun passPayload(passedPayload: String) {
payload = passedPayload
latch.countDown()
}
}
override fun intercept(chain: Interceptor.Chain): Response {
val originalRequest = chain.request()
val newRequest = resolveWithWebView(originalRequest) ?: originalRequest
return chain.proceed(newRequest)
}
@SuppressLint("SetJavaScriptEnabled")
private fun resolveWithWebView(request: Request): Request? {
val latch = CountDownLatch(1)
var webView: WebView? = null
val origRequestUrl = request.url.toString()
val jsinterface = JsObject(latch)
// Get url with token with promise
val jsScript = """
(async () => {
var data = await generate("direct");
window.android.passPayload(data.url);
})();""".trim()
val headers = request.headers.toMultimap().mapValues { it.value.getOrNull(0) ?: "" }.toMutableMap()
handler.post {
val webview = WebView(context)
webView = webview
with(webview.settings) {
javaScriptEnabled = true
domStorageEnabled = true
databaseEnabled = true
useWideViewPort = false
loadWithOverviewMode = false
userAgentString = "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/109.0"
webview.addJavascriptInterface(jsinterface, "android")
webview.webViewClient = object : WebViewClient() {
override fun onPageFinished(view: WebView?, url: String?) {
view?.evaluateJavascript(jsScript) {}
}
}
webView?.loadUrl(origRequestUrl, headers)
}
}
latch.await()
handler.post {
webView?.stopLoading()
webView?.destroy()
webView = null
}
return if (jsinterface.payload.isNotBlank()) GET(jsinterface.payload) else null
}
}