From 36ebc198d3d5fa0b5220bc9a08c42917a20fd4e7 Mon Sep 17 00:00:00 2001
From: WebDitto <webditto@proton.me>
Date: Wed, 21 Aug 2024 21:15:27 -0300
Subject: [PATCH] fix(pt/anitube): Fixed pt/Anitube and pt/HinataSoul sources
 (#167)

---
 src/pt/anitube/build.gradle                   |   2 +-
 .../animeextension/pt/anitube/Anitube.kt      |  22 +++-
 .../pt/anitube/extractors/AnitubeExtractor.kt | 123 +++++++++++++-----
 src/pt/hinatasoul/build.gradle                |   2 +-
 .../pt/hinatasoul/HinataSoul.kt               |   2 +-
 .../extractors/HinataSoulExtractor.kt         | 105 ++++++++++++++-
 6 files changed, 211 insertions(+), 45 deletions(-)

diff --git a/src/pt/anitube/build.gradle b/src/pt/anitube/build.gradle
index d88cedbb..9b47e747 100644
--- a/src/pt/anitube/build.gradle
+++ b/src/pt/anitube/build.gradle
@@ -1,7 +1,7 @@
 ext {
     extName = 'Anitube'
     extClass = '.Anitube'
-    extVersionCode = 15
+    extVersionCode = 16
 }
 
 apply from: "$rootDir/common.gradle"
diff --git a/src/pt/anitube/src/eu/kanade/tachiyomi/animeextension/pt/anitube/Anitube.kt b/src/pt/anitube/src/eu/kanade/tachiyomi/animeextension/pt/anitube/Anitube.kt
index d5ab8012..63937fde 100644
--- a/src/pt/anitube/src/eu/kanade/tachiyomi/animeextension/pt/anitube/Anitube.kt
+++ b/src/pt/anitube/src/eu/kanade/tachiyomi/animeextension/pt/anitube/Anitube.kt
@@ -38,7 +38,7 @@ class Anitube : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
     }
 
     override fun headersBuilder() = super.headersBuilder()
-        .add("Referer", baseUrl)
+        .add("Referer", "$baseUrl/")
         .add("Accept-Language", ACCEPT_LANGUAGE)
 
     // ============================== Popular ===============================
@@ -78,7 +78,11 @@ class Anitube : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
     override fun latestUpdatesNextPageSelector() = popularAnimeNextPageSelector()
 
     // =============================== Search ===============================
-    override suspend fun getSearchAnime(page: Int, query: String, filters: AnimeFilterList): AnimesPage {
+    override suspend fun getSearchAnime(
+        page: Int,
+        query: String,
+        filters: AnimeFilterList,
+    ): AnimesPage {
         return if (query.startsWith(PREFIX_SEARCH)) {
             val path = query.removePrefix(PREFIX_SEARCH)
             client.newCall(GET("$baseUrl/$path"))
@@ -97,6 +101,7 @@ class Anitube : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
 
         return AnimesPage(listOf(details), false)
     }
+
     override fun getFilterList(): AnimeFilterList = AnitubeFilters.FILTER_LIST
 
     override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
@@ -108,7 +113,14 @@ class Anitube : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
             val char = params.initialChar
             when {
                 season.isNotBlank() -> "$baseUrl/temporada/$season/$year"
-                genre.isNotBlank() -> "$baseUrl/genero/$genre/page/$page/${char.replace("todos", "")}"
+                genre.isNotBlank() ->
+                    "$baseUrl/genero/$genre/page/$page/${
+                        char.replace(
+                            "todos",
+                            "",
+                        )
+                    }"
+
                 else -> "$baseUrl/anime/page/$page/letra/$char"
             }
         } else {
@@ -176,7 +188,9 @@ class Anitube : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
     }
 
     // ============================ Video Links =============================
-    override fun videoListParse(response: Response) = AnitubeExtractor.getVideoList(response, headers, client)
+    private val extractor by lazy { AnitubeExtractor(headers, client) }
+
+    override fun videoListParse(response: Response) = extractor.getVideoList(response)
     override fun videoListSelector() = throw UnsupportedOperationException()
     override fun videoFromElement(element: Element) = throw UnsupportedOperationException()
     override fun videoUrlParse(document: Document) = throw UnsupportedOperationException()
diff --git a/src/pt/anitube/src/eu/kanade/tachiyomi/animeextension/pt/anitube/extractors/AnitubeExtractor.kt b/src/pt/anitube/src/eu/kanade/tachiyomi/animeextension/pt/anitube/extractors/AnitubeExtractor.kt
index e2c3854c..7d218de9 100644
--- a/src/pt/anitube/src/eu/kanade/tachiyomi/animeextension/pt/anitube/extractors/AnitubeExtractor.kt
+++ b/src/pt/anitube/src/eu/kanade/tachiyomi/animeextension/pt/anitube/extractors/AnitubeExtractor.kt
@@ -1,5 +1,6 @@
 package eu.kanade.tachiyomi.animeextension.pt.anitube.extractors
 
+import android.util.Log
 import eu.kanade.tachiyomi.animesource.model.Video
 import eu.kanade.tachiyomi.network.GET
 import eu.kanade.tachiyomi.network.POST
@@ -8,9 +9,90 @@ import okhttp3.FormBody
 import okhttp3.Headers
 import okhttp3.OkHttpClient
 import okhttp3.Response
+import java.util.Calendar
+import java.util.Date
 
-object AnitubeExtractor {
-    fun getVideoList(response: Response, headers: Headers, client: OkHttpClient): List<Video> {
+class AnitubeExtractor(private val headers: Headers, private val client: OkHttpClient) {
+
+    private var authCodeCache: String = ""
+    private var authCodeDate: Date = Calendar.getInstance().getTime()
+
+    private fun getAuthCode(serverUrl: String, thumbUrl: String): String {
+        val duration = Calendar.getInstance().getTime().time - authCodeDate.time
+
+        // check the authCode in cache for 1 hour
+        if (authCodeCache.isNotBlank() && duration < 60 * 60 * 1000) {
+            Log.d("AnitubeExtractor", "Using authCode from cache")
+            return authCodeCache
+        }
+
+        Log.d("AnitubeExtractor", "Fetching new authCode")
+        val videoName = serverUrl.split('/').last()
+
+        val adsUrl =
+            client.newCall(GET("$SITE_URL/playerricas.php?name=apphd/$videoName&img=$thumbUrl&url=$serverUrl"))
+                .execute()
+                .body.string()
+                .substringAfter("ADS_URL")
+                .substringAfter('"')
+                .substringBefore('"')
+
+        Log.d("AnitubeExtractor", "ADS URL: $adsUrl")
+        val adsContent = client.newCall(GET(adsUrl)).execute().body.string()
+
+        val body = FormBody.Builder()
+            .add("category", "client")
+            .add("type", "premium")
+            .add("ad", adsContent)
+            .build()
+
+        val newHeaders = headers.newBuilder()
+            .set("Referer", SITE_URL)
+            .add("Accept", "*/*")
+            .add("Cache-Control", "no-cache")
+            .add("Pragma", "no-cache")
+            .add("Connection", "keep-alive")
+            .add("Sec-Fetch-Dest", "empty")
+            .add("Sec-Fetch-Mode", "cors")
+            .add("Sec-Fetch-Site", "same-site")
+            .build()
+
+        val publicidade =
+            client.newCall(POST("$ADS_URL/", headers = newHeaders, body = body))
+                .execute()
+                .body.string()
+                .substringAfter("\"publicidade\"")
+                .substringAfter('"')
+                .substringBefore('"')
+
+        if (publicidade.isBlank()) {
+            Log.e("AnitubeExtractor", "Failed to fetch \"publicidade\" code")
+            return ""
+        }
+
+        authCodeCache =
+            client.newCall(
+                GET(
+                    "$ADS_URL/?token=$publicidade",
+                    headers = newHeaders,
+                ),
+            )
+                .execute()
+                .body.string()
+                .substringAfter("\"publicidade\"")
+                .substringAfter('"')
+                .substringBefore('"')
+
+        if (authCodeCache.isBlank()) {
+            Log.e("AnitubeExtractor", "Failed to fetch auth code")
+        } else {
+            Log.d("AnitubeExtractor", "Auth code fetched successfully")
+        }
+
+        return authCodeCache
+    }
+
+    fun getVideoList(response: Response): List<Video> {
         val doc = response.asJsoup()
         val hasFHD = doc.selectFirst("div.abaItem:contains(FULLHD)") != null
         val serverUrl = doc.selectFirst("meta[itemprop=contentURL]")!!
@@ -28,37 +110,7 @@ object AnitubeExtractor {
             }
         } + listOf("appfullhd")
 
-        val videoName = serverUrl.split('/').last()
-
-        val adsUrl =
-            client.newCall(GET("https://www.anitube.vip/playerricas.php?name=apphd/$videoName&img=$thumbUrl&url=$serverUrl"))
-                .execute()
-                .body.string()
-                .substringAfter("ADS_URL")
-                .substringAfter('"')
-                .substringBefore('"')
-
-        val adsContent = client.newCall(GET(adsUrl)).execute().body.string()
-
-        val body = FormBody.Builder()
-            .add("category", "client")
-            .add("type", "premium")
-            .add("ad", adsContent)
-            .build()
-
-        val publicidade = client.newCall(POST("https://ads.anitube.vip/", body = body))
-            .execute()
-            .body.string()
-            .substringAfter("\"publicidade\"")
-            .substringAfter('"')
-            .substringBefore('"')
-
-        val authCode = client.newCall(GET("https://ads.anitube.vip/?token=$publicidade"))
-            .execute()
-            .body.string()
-            .substringAfter("\"publicidade\"")
-            .substringAfter('"')
-            .substringBefore('"')
+        val authCode = getAuthCode(serverUrl, thumbUrl)
 
         return qualities.mapIndexed { index, quality ->
             val path = paths[index]
@@ -66,4 +118,9 @@ object AnitubeExtractor {
             Video(url, quality, url, headers = headers)
         }.reversed()
     }
+
+    companion object {
+        val ADS_URL = "https://ads.anitube.vip"
+        val SITE_URL = "https://www.anitube.vip"
+    }
 }
diff --git a/src/pt/hinatasoul/build.gradle b/src/pt/hinatasoul/build.gradle
index 09a04203..d35ece40 100644
--- a/src/pt/hinatasoul/build.gradle
+++ b/src/pt/hinatasoul/build.gradle
@@ -1,7 +1,7 @@
 ext {
     extName = 'Hinata Soul'
     extClass = '.HinataSoul'
-    extVersionCode = 5
+    extVersionCode = 6
 }
 
 apply from: "$rootDir/common.gradle"
diff --git a/src/pt/hinatasoul/src/eu/kanade/tachiyomi/animeextension/pt/hinatasoul/HinataSoul.kt b/src/pt/hinatasoul/src/eu/kanade/tachiyomi/animeextension/pt/hinatasoul/HinataSoul.kt
index e641cfe4..0bdb4ec3 100644
--- a/src/pt/hinatasoul/src/eu/kanade/tachiyomi/animeextension/pt/hinatasoul/HinataSoul.kt
+++ b/src/pt/hinatasoul/src/eu/kanade/tachiyomi/animeextension/pt/hinatasoul/HinataSoul.kt
@@ -162,7 +162,7 @@ class HinataSoul : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
     }
 
     // ============================ Video Links =============================
-    private val extractor by lazy { HinataSoulExtractor(headers) }
+    private val extractor by lazy { HinataSoulExtractor(headers, client) }
 
     override fun videoListParse(response: Response) = extractor.getVideoList(response)
 
diff --git a/src/pt/hinatasoul/src/eu/kanade/tachiyomi/animeextension/pt/hinatasoul/extractors/HinataSoulExtractor.kt b/src/pt/hinatasoul/src/eu/kanade/tachiyomi/animeextension/pt/hinatasoul/extractors/HinataSoulExtractor.kt
index fd9c551b..e4a6bbde 100644
--- a/src/pt/hinatasoul/src/eu/kanade/tachiyomi/animeextension/pt/hinatasoul/extractors/HinataSoulExtractor.kt
+++ b/src/pt/hinatasoul/src/eu/kanade/tachiyomi/animeextension/pt/hinatasoul/extractors/HinataSoulExtractor.kt
@@ -1,11 +1,96 @@
 package eu.kanade.tachiyomi.animeextension.pt.hinatasoul.extractors
 
+import android.util.Log
 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.FormBody
 import okhttp3.Headers
+import okhttp3.OkHttpClient
 import okhttp3.Response
+import java.util.Calendar
+import java.util.Date
 
-class HinataSoulExtractor(private val headers: Headers) {
+class HinataSoulExtractor(private val headers: Headers, private val client: OkHttpClient) {
+
+    private var authCodeCache: String = ""
+    private var authCodeDate: Date = Calendar.getInstance().getTime()
+
+    private fun getAuthCode(serverUrl: String, thumbUrl: String): String {
+        val duration = Calendar.getInstance().getTime().time - authCodeDate.time
+
+        // check the authCode in cache for 1 hour
+        if (authCodeCache.isNotBlank() && duration < 60 * 60 * 1000) {
+            Log.d("HinataSoulExtractor", "Using authCode from cache")
+            return authCodeCache
+        }
+
+        Log.d("HinataSoulExtractor", "Fetching new authCode")
+        val videoName = serverUrl.split('/').last()
+
+        val adsUrl =
+            client.newCall(GET("$SITE_URL/playerricas.php?name=apphd/$videoName&img=$thumbUrl&url=$serverUrl"))
+                .execute()
+                .body.string()
+                .substringAfter("ADS_URL")
+                .substringAfter('"')
+                .substringBefore('"')
+
+        Log.d("HinataSoulExtractor", "ADS URL: $adsUrl")
+        val adsContent = client.newCall(GET(adsUrl)).execute().body.string()
+
+        val body = FormBody.Builder()
+            .add("category", "client")
+            .add("type", "premium")
+            .add("ad", adsContent)
+            .build()
+
+        val newHeaders = headers.newBuilder()
+            .set("Referer", SITE_URL)
+            .add("Accept", "*/*")
+            .add("Cache-Control", "no-cache")
+            .add("Pragma", "no-cache")
+            .add("Connection", "keep-alive")
+            .add("Sec-Fetch-Dest", "empty")
+            .add("Sec-Fetch-Mode", "cors")
+            .add("Sec-Fetch-Site", "same-site")
+            .build()
+
+        val publicidade =
+            client.newCall(POST("$ADS_URL/", headers = newHeaders, body = body))
+                .execute()
+                .body.string()
+                .substringAfter("\"publicidade\"")
+                .substringAfter('"')
+                .substringBefore('"')
+
+        if (publicidade.isBlank()) {
+            Log.e("HinataSoulExtractor", "Failed to fetch \"publicidade\" code")
+            return ""
+        }
+
+        authCodeCache =
+            client.newCall(
+                GET(
+                    "$ADS_URL/?token=$publicidade",
+                    headers = newHeaders,
+                ),
+            )
+                .execute()
+                .body.string()
+                .substringAfter("\"publicidade\"")
+                .substringAfter('"')
+                .substringBefore('"')
+
+        if (authCodeCache.isBlank()) {
+            Log.e("HinataSoulExtractor", "Failed to fetch auth code")
+        } else {
+            Log.d("HinataSoulExtractor", "Auth code fetched successfully")
+        }
+
+        return authCodeCache
+    }
 
     fun getVideoList(response: Response): List<Video> {
         val doc = response.asJsoup()
@@ -13,19 +98,29 @@ class HinataSoulExtractor(private val headers: Headers) {
         val serverUrl = doc.selectFirst("meta[itemprop=contentURL]")!!
             .attr("content")
             .replace("cdn1", "cdn3")
-        val type = serverUrl.split('/').get(3)
-        val qualities = listOfNotNull("SD", "HD", "FULLHD".takeIf { hasFHD })
+        val thumbUrl = doc.selectFirst("meta[itemprop=thumbnailUrl]")!!
+            .attr("content")
+        val type = serverUrl.split("/").get(3)
+        val qualities = listOfNotNull("SD", "HD", if (hasFHD) "FULLHD" else null)
         val paths = listOf("appsd", "apphd").let {
             if (type.endsWith("2")) {
                 it.map { path -> path + "2" }
             } else {
                 it
             }
-        } + listOfNotNull("appfullhd".takeIf { hasFHD })
+        } + listOf("appfullhd")
+
+        val authCode = getAuthCode(serverUrl, thumbUrl)
+
         return qualities.mapIndexed { index, quality ->
             val path = paths[index]
-            val url = serverUrl.replace(type, path)
+            val url = serverUrl.replace(type, path) + authCode
             Video(url, quality, url, headers = headers)
         }.reversed()
     }
+
+    companion object {
+        val ADS_URL = "https://ads.anitube.vip"
+        val SITE_URL = "https://www.anitube.vip"
+    }
 }