From dcb5c7ef2f2bd12fde0f117abe76de7e490c40c4 Mon Sep 17 00:00:00 2001
From: almightyhak <134626626+almightyhak@users.noreply.github.com>
Date: Sun, 4 Aug 2024 00:32:19 +0700
Subject: [PATCH] Aniwave updates (#98)

* new encryption keys

* updated extVersionCode to 73

* fixed null exception in episodeListRequest

* removed old domains

* added custom domain preference

* added new episode path id resolution

* updated/fixed custom domain preference setting

*updated build_push.yml
---
 .github/workflows/build_push.yml              |   2 +-
 src/en/nineanime/build.gradle                 |   2 +-
 .../animeextension/en/nineanime/Aniwave.kt    | 109 +++++++++++++-----
 3 files changed, 85 insertions(+), 28 deletions(-)

diff --git a/.github/workflows/build_push.yml b/.github/workflows/build_push.yml
index d984a6c6..abcdb4d7 100644
--- a/.github/workflows/build_push.yml
+++ b/.github/workflows/build_push.yml
@@ -44,7 +44,7 @@ jobs:
       - name: Bump extensions that uses a modified lib
         if: steps.modified-libs.outputs.any_changed == 'true'
         run: |
-          ./.github/scripts/bump-versions.py ${{ steps.modified-libs.outputs.all_changed_files }}
+          chmod +x ./.github/scripts/bump-versions.py ${{ steps.modified-libs.outputs.all_changed_files }}
 
       - name: Validate Gradle Wrapper
         uses: gradle/wrapper-validation-action@a494d935f4b56874c4a5a87d19af7afcf3a163d0 # v2
diff --git a/src/en/nineanime/build.gradle b/src/en/nineanime/build.gradle
index e572ab0b..1b97c417 100644
--- a/src/en/nineanime/build.gradle
+++ b/src/en/nineanime/build.gradle
@@ -1,7 +1,7 @@
 ext {
     extName = 'Aniwave'
     extClass = '.Aniwave'
-    extVersionCode = 72
+    extVersionCode = 73
 }
 
 apply from: "$rootDir/common.gradle"
diff --git a/src/en/nineanime/src/eu/kanade/tachiyomi/animeextension/en/nineanime/Aniwave.kt b/src/en/nineanime/src/eu/kanade/tachiyomi/animeextension/en/nineanime/Aniwave.kt
index f8cdc324..ad57ef24 100644
--- a/src/en/nineanime/src/eu/kanade/tachiyomi/animeextension/en/nineanime/Aniwave.kt
+++ b/src/en/nineanime/src/eu/kanade/tachiyomi/animeextension/en/nineanime/Aniwave.kt
@@ -2,7 +2,10 @@ package eu.kanade.tachiyomi.animeextension.en.nineanime
 
 import android.app.Application
 import android.content.SharedPreferences
+import android.util.Log
+import android.webkit.URLUtil
 import android.widget.Toast
+import androidx.preference.EditTextPreference
 import androidx.preference.ListPreference
 import androidx.preference.MultiSelectListPreference
 import androidx.preference.PreferenceScreen
@@ -38,7 +41,12 @@ class Aniwave : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
     override val id: Long = 98855593379717478
 
     override val baseUrl by lazy {
-        preferences.getString(PREF_DOMAIN_KEY, PREF_DOMAIN_DEFAULT)!!
+        val customDomain = preferences.getString(PREF_CUSTOM_DOMAIN_KEY, null)
+        if (customDomain.isNullOrBlank()) {
+            preferences.getString(PREF_DOMAIN_KEY, PREF_DOMAIN_DEFAULT)!!
+        } else {
+            customDomain
+        }
     }
 
     override val lang = "en"
@@ -89,7 +97,7 @@ class Aniwave : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
     override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
         val filters = AniwaveFilters.getSearchParameters(filters)
 
-        val vrf = if (query.isNotBlank()) utils.vrfEncrypt(KEY_ENCRYPT, query) else ""
+        val vrf = if (query.isNotBlank()) utils.vrfEncrypt(ENCRYPTION_KEY, query) else ""
         var url = "$baseUrl/filter?keyword=$query"
 
         if (filters.genre.isNotBlank()) url += filters.genre
@@ -116,30 +124,39 @@ class Aniwave : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
 
     // =========================== Anime Details ============================
 
-    override fun animeDetailsParse(document: Document): SAnime = SAnime.create().apply {
-        title = document.select("h1.title").text()
-        genre = document.select("div:contains(Genre) > span > a").joinToString { it.text() }
-        description = document.select("div.synopsis > div.shorting > div.content").text()
-        author = document.select("div:contains(Studio) > span > a").text()
-        status = parseStatus(document.select("div:contains(Status) > span").text())
+    override fun animeDetailsParse(document: Document): SAnime {
+        val anime = SAnime.create()
+        val newDocument = resolveSearchAnime(anime, document)
+        anime.apply {
+            title = newDocument.select("h1.title").text()
+            genre = newDocument.select("div:contains(Genre) > span > a").joinToString { it.text() }
+            description = newDocument.select("div.synopsis > div.shorting > div.content").text()
+            author = newDocument.select("div:contains(Studio) > span > a").text()
+            status = parseStatus(newDocument.select("div:contains(Status) > span").text())
 
-        val altName = "Other name(s): "
-        document.select("h1.title").attr("data-jp").let {
-            if (it.isNotBlank()) {
-                description = when {
-                    description.isNullOrBlank() -> altName + it
-                    else -> description + "\n\n$altName" + it
+            val altName = "Other name(s): "
+            newDocument.select("h1.title").attr("data-jp").let {
+                if (it.isNotBlank()) {
+                    description = when {
+                        description.isNullOrBlank() -> altName + it
+                        else -> description + "\n\n$altName" + it
+                    }
                 }
             }
         }
+        return anime
     }
 
     // ============================== Episodes ==============================
 
     override fun episodeListRequest(anime: SAnime): Request {
-        val id = client.newCall(GET(baseUrl + anime.url)).execute().asJsoup()
-            .selectFirst("div[data-id]")!!.attr("data-id")
-        val vrf = utils.vrfEncrypt(KEY_ENCRYPT, id)
+        Log.i(name, "episodeListRequest")
+        val response = client.newCall(GET(baseUrl + anime.url)).execute()
+        var document = response.asJsoup()
+        document = resolveSearchAnime(anime, document)
+        val id = document.selectFirst("div[data-id]")?.attr("data-id") ?: throw Exception("ID not found")
+
+        val vrf = utils.vrfEncrypt(ENCRYPTION_KEY, id)
 
         val listHeaders = headers.newBuilder().apply {
             add("Accept", "application/json, text/javascript, */*; q=0.01")
@@ -195,7 +212,7 @@ class Aniwave : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
 
     override fun videoListRequest(episode: SEpisode): Request {
         val ids = episode.url.substringBefore("&")
-        val vrf = utils.vrfEncrypt(KEY_ENCRYPT, ids)
+        val vrf = utils.vrfEncrypt(ENCRYPTION_KEY, ids)
         val url = "/ajax/server/list/$ids?vrf=$vrf"
         val epurl = episode.url.substringAfter("epurl=")
 
@@ -248,7 +265,7 @@ class Aniwave : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
     private val mp4uploadExtractor by lazy { Mp4uploadExtractor(client) }
 
     private fun extractVideo(server: VideoData, epUrl: String): List<Video> {
-        val vrf = utils.vrfEncrypt(KEY_ENCRYPT, server.serverId)
+        val vrf = utils.vrfEncrypt(ENCRYPTION_KEY, server.serverId)
 
         val listHeaders = headers.newBuilder().apply {
             add("Accept", "application/json, text/javascript, */*; q=0.01")
@@ -263,7 +280,7 @@ class Aniwave : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
 
         return runCatching {
             val parsed = response.parseAs<ServerResponse>()
-            val embedLink = utils.vrfDecrypt(KEY_DECRYPT, parsed.result.url)
+            val embedLink = utils.vrfDecrypt(DECRYPTION_KEY, parsed.result.url)
             when (server.serverName) {
                 "vidstream", "megaf" -> {
                     vidsrcExtractor.videosFromUrl(embedLink, server.serverName, server.type)
@@ -308,6 +325,16 @@ class Aniwave : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
         }
     }
 
+    private fun resolveSearchAnime(anime: SAnime, document: Document): Document {
+        if (document.location().startsWith("$baseUrl/filter?keyword=")) { // redirected to search
+            val element = document.selectFirst(searchAnimeSelector())
+            val foundAnimePath = element?.selectFirst("a[href]")?.attr("href") ?: throw Exception("Search element not found (resolveSearch)")
+            anime.url = foundAnimePath // probably doesn't work as intended
+            return client.newCall(GET(baseUrl + foundAnimePath)).execute().asJsoup()
+        }
+        return document
+    }
+
     private fun getHosters(): Set<String> {
         val hosterSelection = preferences.getStringSet(PREF_HOSTER_KEY, PREF_HOSTER_DEFAULT)!!
         var invalidRecord = false
@@ -338,6 +365,8 @@ class Aniwave : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
         private const val PREF_DOMAIN_KEY = "preferred_domain"
         private const val PREF_DOMAIN_DEFAULT = "https://aniwave.to"
 
+        private const val PREF_CUSTOM_DOMAIN_KEY = "custom_domain"
+
         private const val PREF_QUALITY_KEY = "preferred_quality"
         private const val PREF_QUALITY_DEFAULT = "1080"
 
@@ -371,22 +400,25 @@ class Aniwave : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
         private val TYPES = arrayOf("Sub", "Softsub", "Dub")
         private val PREF_TYPES_TOGGLE_DEFAULT = TYPES.toSet()
 
-        // https://rowdy-avocado.github.io/multi-keys/
-        private const val KEY_DECRYPT = "ctpAbOz5u7S6OMkx"
-        private const val KEY_ENCRYPT = "p01EDKu734HJP1Tm"
+        private const val DECRYPTION_KEY = "ctpAbOz5u7S6OMkx"
+        private const val ENCRYPTION_KEY = "T78s2WjTc7hSIZZR"
     }
 
     // ============================== Settings ==============================
 
     override fun setupPreferenceScreen(screen: PreferenceScreen) {
         // validate hosters preferences and if invalid reset
-        getHosters()
+        try {
+            getHosters()
+        } catch (e: Exception) {
+            Log.w(name, e.toString())
+        }
 
         ListPreference(screen.context).apply {
             key = PREF_DOMAIN_KEY
             title = "Preferred domain"
-            entries = arrayOf("aniwave.to", "aniwave.li", "aniwave.ws", "aniwave.vc")
-            entryValues = arrayOf("https://aniwave.to", "https://aniwave.li", "https://aniwave.ws", "https://aniwave.vc")
+            entries = arrayOf("aniwave.to", "aniwavetv.to (unofficial)")
+            entryValues = arrayOf("https://aniwave.to", "https://aniwavetv.to")
             setDefaultValue(PREF_DOMAIN_DEFAULT)
             summary = "%s"
 
@@ -482,5 +514,30 @@ class Aniwave : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
                 preferences.edit().putStringSet(key, newValue as Set<String>).commit()
             }
         }.also(screen::addPreference)
+
+        EditTextPreference(screen.context).apply {
+            key = PREF_CUSTOM_DOMAIN_KEY
+            title = "Custom domain"
+            setDefaultValue(null)
+            val currentValue = preferences.getString(PREF_CUSTOM_DOMAIN_KEY, null)
+            summary = if (currentValue.isNullOrBlank()) {
+                "Custom domain of your choosing"
+            } else {
+                "Domain: \"$currentValue\". \nLeave blank to disable. Overrides any domain preferences!"
+            }
+
+            setOnPreferenceChangeListener { _, newValue ->
+                val newDomain = newValue as String
+                if (newDomain.isBlank() || URLUtil.isValidUrl(newDomain)) {
+                    summary = "Restart to apply changes"
+                    Toast.makeText(screen.context, "Restart Aniyomi to apply changes", Toast.LENGTH_LONG).show()
+                    preferences.edit().putString(key, newDomain).apply()
+                    true
+                } else {
+                    Toast.makeText(screen.context, "Invalid url. Url example: https://aniwave.to", Toast.LENGTH_LONG).show()
+                    false
+                }
+            }
+        }.also(screen::addPreference)
     }
 }