From 9e4b2204bb0902e098eda127b1e6c67010e672a7 Mon Sep 17 00:00:00 2001 From: AlphaBoom <30779939+AlphaBoom@users.noreply.github.com> Date: Tue, 3 Jun 2025 12:43:46 +0800 Subject: [PATCH 01/14] Hanime1: Fix anime details cover (#2) * Hanime1: Fix anime details - Use the series cover image for bangumi entries instead of the episode image. - Version bump to 5 * Hanime1: Filter out ad --- src/zh/hanime1/build.gradle | 2 +- .../animeextension/zh/hanime1/Hanime1.kt | 31 ++++++++++++++++--- 2 files changed, 27 insertions(+), 6 deletions(-) diff --git a/src/zh/hanime1/build.gradle b/src/zh/hanime1/build.gradle index 85bc719b..f1b59699 100644 --- a/src/zh/hanime1/build.gradle +++ b/src/zh/hanime1/build.gradle @@ -1,7 +1,7 @@ ext { extName = 'Hanime1' extClass = '.Hanime1' - extVersionCode = 4 + extVersionCode = 5 isNsfw = true } diff --git a/src/zh/hanime1/src/eu/kanade/tachiyomi/animeextension/zh/hanime1/Hanime1.kt b/src/zh/hanime1/src/eu/kanade/tachiyomi/animeextension/zh/hanime1/Hanime1.kt index ab8e354b..f3bdfdb9 100644 --- a/src/zh/hanime1/src/eu/kanade/tachiyomi/animeextension/zh/hanime1/Hanime1.kt +++ b/src/zh/hanime1/src/eu/kanade/tachiyomi/animeextension/zh/hanime1/Hanime1.kt @@ -2,6 +2,7 @@ package eu.kanade.tachiyomi.animeextension.zh.hanime1 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 @@ -19,9 +20,11 @@ import kotlinx.coroutines.CoroutineExceptionHandler import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.jsonArray import kotlinx.serialization.json.jsonObject import kotlinx.serialization.json.jsonPrimitive import okhttp3.Cookie @@ -65,14 +68,32 @@ class Hanime1 : AnimeHttpSource(), ConfigurableAnimeSource { } override fun animeDetailsParse(response: Response): SAnime { - val jsoup = response.asJsoup() + val doc = response.asJsoup() return SAnime.create().apply { - genre = jsoup.select(".single-video-tag").not("[data-toggle]").eachText().joinToString() - author = jsoup.select("#video-artist-name").text() - jsoup.select("script[type=application/ld+json]").first()?.data()?.let { + genre = doc.select(".single-video-tag").not("[data-toggle]").eachText().joinToString() + author = doc.select("#video-artist-name").text() + doc.select("script[type=application/ld+json]").first()?.data()?.let { val info = json.decodeFromString(it).jsonObject title = info["name"]!!.jsonPrimitive.content description = info["description"]!!.jsonPrimitive.content + thumbnail_url = info["thumbnailUrl"]?.jsonArray?.get(0)?.jsonPrimitive?.content + } + val type = doc.select("a#video-artist-name + a").text().trim() + if (type == "裏番" || type == "泡麵番") { + // Use the series cover image for bangumi entries instead of the episode image. + runBlocking { + try { + val animesPage = + getSearchAnime( + 1, + title, + AnimeFilterList(GenreFilter(arrayOf("", type)).apply { state = 1 }), + ) + thumbnail_url = animesPage.animes.first().thumbnail_url + } catch (e: Exception) { + Log.e(name, "Failed to get bangumi cover image") + } + } } } } @@ -137,7 +158,7 @@ class Hanime1 : AnimeHttpSource(), ConfigurableAnimeSource { override fun searchAnimeParse(response: Response): AnimesPage { val jsoup = response.asJsoup() - val nodes = jsoup.select("div.search-doujin-videos.hidden-xs") + val nodes = jsoup.select("div.search-doujin-videos.hidden-xs:not(:has(a[target=_blank]))") val list = if (nodes.isNotEmpty()) { nodes.map { SAnime.create().apply { From ae982dcfd5f6fc2994bbdba50e9f85e882603746 Mon Sep 17 00:00:00 2001 From: AlphaBoom <30779939+AlphaBoom@users.noreply.github.com> Date: Tue, 3 Jun 2025 12:44:20 +0800 Subject: [PATCH 02/14] Xfani: Update domain (#3) --- src/zh/xfani/build.gradle | 2 +- .../animeextension/zh/xfani/Xfani.kt | 28 +++++++++---------- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/src/zh/xfani/build.gradle b/src/zh/xfani/build.gradle index 89b38f74..937a972a 100644 --- a/src/zh/xfani/build.gradle +++ b/src/zh/xfani/build.gradle @@ -1,7 +1,7 @@ ext { extName = 'Xfani' extClass = '.Xfani' - extVersionCode = 5 + extVersionCode = 6 } apply from: "$rootDir/common.gradle" diff --git a/src/zh/xfani/src/eu/kanade/tachiyomi/animeextension/zh/xfani/Xfani.kt b/src/zh/xfani/src/eu/kanade/tachiyomi/animeextension/zh/xfani/Xfani.kt index 92583f32..eeb802e9 100644 --- a/src/zh/xfani/src/eu/kanade/tachiyomi/animeextension/zh/xfani/Xfani.kt +++ b/src/zh/xfani/src/eu/kanade/tachiyomi/animeextension/zh/xfani/Xfani.kt @@ -53,7 +53,7 @@ enum class FilterUpdateState { class Xfani : AnimeHttpSource(), ConfigurableAnimeSource { override val baseUrl: String - get() = "https://dick.xfani.com" + get() = "https://dm.xifanacg.com" override val lang: String get() = "zh" override val name: String @@ -117,10 +117,16 @@ class Xfani : AnimeHttpSource(), ConfigurableAnimeSource { } override fun animeDetailsParse(response: Response): SAnime { - val jsoup = response.asJsoup() + val doc = response.asJsoup() return SAnime.create().apply { - description = jsoup.select("#height_limit.text").text() - title = jsoup.select(".slide-info-title").text() + description = doc.select("#height_limit.text").text() + title = doc.select(".slide-info-title").text() + author = doc.select(".slide-info:contains(导演 :)").text().removePrefix("导演 :") + .removeSuffix(",") + artist = doc.select(".slide-info:contains(演员 :)").text().removePrefix("演员 :") + .removeSuffix(",") + genre = doc.select(".slide-info:contains(类型 :)").text().removePrefix("类型 :") + .removeSuffix(",").replace(",", ", ") } } @@ -234,19 +240,13 @@ class Xfani : AnimeHttpSource(), ConfigurableAnimeSource { return vodListToAnimePageList(response) } val jsoup = response.asJsoup() - val items = jsoup.select("div.public-list-box.search-box.flex.rel") + val items = jsoup.select("div.search-list") val animeList = items.map { item -> SAnime.create().apply { - title = item.select(".thumb-txt").text() - url = item.select("div.left.public-list-bj a.public-list-exp").attr("href") + title = item.select("div.detail-info > a").text() + url = item.select("div.detail-info > a").attr("href") thumbnail_url = - item.select("div.left.public-list-bj img[data-src]").attr("data-src") - author = item.select("div.thumb-actor").text().removeSuffix("/") - artist = item.select("div.thumb-director").text().removeSuffix("/") - description = item.select(".thumb-blurb").text() - genre = item.select("div.thumb-else").text() - val statusString = item.select("div.left.public-list-bj .public-list-prb").text() - status = STATUS_STR_MAPPING.getOrElse(statusString) { SAnime.ONGOING } + item.select("div.detail-pic img[data-src]").attr("data-src") } } val tip = jsoup.select("div.pages div.page-tip").text() From fc7e6bee6523c3f5266ce2b37104e758910adf56 Mon Sep 17 00:00:00 2001 From: ZhendongWu <30779939+AlphaBoom@users.noreply.github.com> Date: Fri, 6 Jun 2025 21:58:14 +0800 Subject: [PATCH 03/14] perf: remove custom coroutine scope --- .../tachiyomi/animeextension/zh/hanime1/Hanime1.kt | 6 ++++-- .../kanade/tachiyomi/animeextension/zh/xfani/Xfani.kt | 10 ++++------ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/zh/hanime1/src/eu/kanade/tachiyomi/animeextension/zh/hanime1/Hanime1.kt b/src/zh/hanime1/src/eu/kanade/tachiyomi/animeextension/zh/hanime1/Hanime1.kt index f3bdfdb9..311e7597 100644 --- a/src/zh/hanime1/src/eu/kanade/tachiyomi/animeextension/zh/hanime1/Hanime1.kt +++ b/src/zh/hanime1/src/eu/kanade/tachiyomi/animeextension/zh/hanime1/Hanime1.kt @@ -17,8 +17,9 @@ import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.awaitSuccess import eu.kanade.tachiyomi.util.asJsoup import kotlinx.coroutines.CoroutineExceptionHandler -import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking import kotlinx.serialization.encodeToString @@ -237,11 +238,12 @@ class Hanime1 : AnimeHttpSource(), ConfigurableAnimeSource { return chain.proceed(chain.request()) } + @OptIn(DelicateCoroutinesApi::class) private fun updateFilters() { filterUpdateState = FilterUpdateState.UPDATING val exceptionHandler = CoroutineExceptionHandler { _, _ -> filterUpdateState = FilterUpdateState.FAILED } - CoroutineScope(Dispatchers.IO + exceptionHandler).launch { + GlobalScope.launch(Dispatchers.IO + exceptionHandler) { val jsoup = client.newCall(GET("$baseUrl/search")).awaitSuccess().asJsoup() val genreList = jsoup.select("div.genre-option div.hentai-sort-options").eachText() val sortList = diff --git a/src/zh/xfani/src/eu/kanade/tachiyomi/animeextension/zh/xfani/Xfani.kt b/src/zh/xfani/src/eu/kanade/tachiyomi/animeextension/zh/xfani/Xfani.kt index eeb802e9..add3a049 100644 --- a/src/zh/xfani/src/eu/kanade/tachiyomi/animeextension/zh/xfani/Xfani.kt +++ b/src/zh/xfani/src/eu/kanade/tachiyomi/animeextension/zh/xfani/Xfani.kt @@ -20,8 +20,9 @@ import eu.kanade.tachiyomi.network.POST import eu.kanade.tachiyomi.network.awaitSuccess import eu.kanade.tachiyomi.util.asJsoup import kotlinx.coroutines.CoroutineExceptionHandler -import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch import kotlinx.serialization.json.Json import kotlinx.serialization.json.jsonObject @@ -259,12 +260,13 @@ class Xfani : AnimeHttpSource(), ConfigurableAnimeSource { return numbers.size == 2 && numbers[0] != numbers[1] } + @OptIn(DelicateCoroutinesApi::class) private fun updateFilter() { filterState = FilterUpdateState.UPDATING val handler = CoroutineExceptionHandler { _, _ -> filterState = FilterUpdateState.FAILED } - CoroutineScope(Dispatchers.IO + handler).launch { + GlobalScope.launch(Dispatchers.IO + handler) { val jsoup = client.newCall(GET("$baseUrl/show/1/html")).awaitSuccess().asJsoup() // update class and year filter type val classList = jsoup.select("li[data-type=class]").eachAttr("data-val") @@ -393,9 +395,5 @@ class Xfani : AnimeHttpSource(), ConfigurableAnimeSource { const val PREF_KEY_FILTER_YEAR = "PREF_KEY_FILTER_YEAR" const val DEFAULT_VIDEO_SOURCE = "0" - - val STATUS_STR_MAPPING = mapOf( - "已完结" to SAnime.COMPLETED, - ) } } From b1d2972f9ffc1ab159458558bb10d2d3f66ec5f1 Mon Sep 17 00:00:00 2001 From: AlmightyHak Date: Thu, 19 Jun 2025 23:37:08 -0500 Subject: [PATCH 04/14] ci: increased ci chunk size and removed (chunk ${{ matrix.chunk }}) in build extensions from name --- .github/workflows/build_pull_request.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build_pull_request.yml b/.github/workflows/build_pull_request.yml index 251290d9..edf31bba 100644 --- a/.github/workflows/build_pull_request.yml +++ b/.github/workflows/build_pull_request.yml @@ -13,7 +13,7 @@ concurrency: cancel-in-progress: true env: - CI_CHUNK_SIZE: 65 + CI_CHUNK_SIZE: 288 jobs: prepare: @@ -86,7 +86,7 @@ jobs: build-cache-${{ github.event.pull_request.base.sha }}- build-cache- - - name: Build extensions (chunk ${{ matrix.chunk }}) + - name: Build extensions env: CI_CHUNK_NUM: ${{ matrix.chunk }} run: chmod +x ./gradlew && ./gradlew -p src assembleDebug From 0ba38fb4a7c001b15fa7aaa916bd8a9459981385 Mon Sep 17 00:00:00 2001 From: Ghost <> Date: Thu, 19 Jun 2025 23:38:05 -0500 Subject: [PATCH 05/14] Voe: adapt to changes in obfuscation (Kohi-den#959) (#960) fixes #959 Checklist: - [x] Updated `extVersionCode` value in `build.gradle` for individual extensions - [x] Updated `overrideVersionCode` or `baseVersionCode` as needed for all multisrc extensions - [x] Referenced all related issues in the PR body (e.g. "Closes #xyz") - [x] Added the `isNsfw = true` flag in `build.gradle` when appropriate - [x] Have not changed source names - [x] Have explicitly kept the `id` if a source's name or language were changed - [x] Have tested the modifications by compiling and running the extension through Android Studio - [x] Have removed `web_hi_res_512.png` when adding a new extension - [x] Have made sure all the icons are in png format Co-authored-by: Sphereso Reviewed-on: https://kohiden.xyz/Kohi-den/extensions-source/pulls/960 Co-authored-by: Ghost <> Co-committed-by: Ghost <> --- .../lib/voeextractor/VoeExtractor.kt | 56 ++++++++++--------- 1 file changed, 31 insertions(+), 25 deletions(-) diff --git a/lib/voe-extractor/src/main/java/eu/kanade/tachiyomi/lib/voeextractor/VoeExtractor.kt b/lib/voe-extractor/src/main/java/eu/kanade/tachiyomi/lib/voeextractor/VoeExtractor.kt index 1e30fb57..fe017e16 100644 --- a/lib/voe-extractor/src/main/java/eu/kanade/tachiyomi/lib/voeextractor/VoeExtractor.kt +++ b/lib/voe-extractor/src/main/java/eu/kanade/tachiyomi/lib/voeextractor/VoeExtractor.kt @@ -18,14 +18,33 @@ class VoeExtractor(private val client: OkHttpClient) { private val playlistUtils by lazy { PlaylistUtils(clientDdos) } - private val linkRegex = "(http|https)://([\\w_-]+(?:\\.[\\w_-]+)+)([\\w.,@?^=%&:/~+#-]*[\\w@?^=%&/~+#-])".toRegex() - - private val base64Regex = Regex("'.*'") - - private val scriptBase64Regex = "(let|var)\\s+\\w+\\s*=\\s*'(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)';".toRegex() - @Serializable - data class VideoLinkDTO(val file: String) + data class VideoLinkDTO(val source: String) + + private fun decodeVoeData(data: String): String { + val shifted = data.map { char -> + when (char) { + in 'A'..'Z' -> 'A' + (char - 'A' + 13).mod(26) + in 'a'..'z' -> 'a' + (char - 'a' + 13).mod(26) + else -> char + } + }.joinToString() + + val junk = listOf("@$", "^^", "~@", "%?", "*~", "!!", "#&") + var result = shifted + for (part in junk) { + result = result.replace(part, "_") + } + val clean = result.replace("_", "") + + val transformed = String(Base64.decode(clean, Base64.DEFAULT)).map { + (it.code - 3).toChar() + }.joinToString().reversed() + + val decoded = String(Base64.decode(transformed, Base64.DEFAULT)) + + return json.decodeFromString(decoded).source + } fun videosFromUrl(url: String, prefix: String = ""): List