diff --git a/.github/workflows/build_pull_request.yml b/.github/workflows/build_pull_request.yml index c0be32ca..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: @@ -75,7 +75,18 @@ jobs: with: cache-read-only: true - - name: Build extensions (chunk ${{ matrix.chunk }}) + - name: Restore build cache + uses: https://github.com/actions/cache/restore@v3 + with: + path: | + src/**/build + !src/**/build/outputs + key: build-cache-${{ github.event.pull_request.base.sha }}-${{ matrix.chunk }} + restore-keys: | + build-cache-${{ github.event.pull_request.base.sha }}- + build-cache- + + - name: Build extensions env: CI_CHUNK_NUM: ${{ matrix.chunk }} run: chmod +x ./gradlew && ./gradlew -p src assembleDebug diff --git a/.github/workflows/build_push.yml b/.github/workflows/build_push.yml index 9451f7b2..419bc51c 100644 --- a/.github/workflows/build_push.yml +++ b/.github/workflows/build_push.yml @@ -37,15 +37,14 @@ jobs: projects=(src/*/*) echo "NUM_INDIVIDUAL_MODULES=${#projects[@]}" >> "$GITHUB_ENV" - # Temporary pause because of leak of tj-actions/changed-files - # - name: Find lib changes - # id: modified-libs - # uses: tj-actions/changed-files@90a06d6ba9543371ab4df8eeca0be07ca6054959 #v42 - # with: - # files: lib/ - # files_ignore: lib/**.md - # files_separator: " " - # safe_output: false + - name: Find lib changes + id: modified-libs + uses: tj-actions/changed-files@ed68ef82c095e0d48ec87eccea555d944a631a4c # v46.0.5 + with: + files: lib/ + files_ignore: lib/**.md + files_separator: " " + safe_output: false - name: Import GPG key uses: https://github.com/crazy-max/ghaction-import-gpg@v6 # v6.1.0 @@ -55,12 +54,11 @@ jobs: git_user_signingkey: true git_commit_gpgsign: true - # # This step is going to commit, but this will not trigger another workflow. - # - name: Bump extensions that uses a modified lib - # if: steps.modified-libs.outputs.any_changed == 'true' - # run: | - # chmod +x ./.github/scripts/bump-versions.py - # ./.github/scripts/bump-versions.py ${{ steps.modified-libs.outputs.all_changed_files }} + - name: Bump extensions that uses a modified lib + if: steps.modified-libs.outputs.any_changed == 'true' + run: | + chmod +x ./.github/scripts/bump-versions.py + ./.github/scripts/bump-versions.py ${{ steps.modified-libs.outputs.all_changed_files }} - id: generate-matrices name: Create output matrices @@ -106,6 +104,17 @@ jobs: - name: Set up Gradle uses: https://github.com/gradle/actions/setup-gradle@245c8a24de79c0dbeabaf19ebcbbd3b2c36f278d # v4 + - name: Restore build cache + uses: https://github.com/actions/cache/restore@v3 + with: + path: | + src/**/build + !src/**/build/outputs + key: build-cache-${{ github.sha }}-${{ matrix.chunk }} + restore-keys: | + build-cache-${{ github.sha }}- + build-cache- + - name: Build extensions env: CI_CHUNK_NUM: ${{ matrix.chunk }} @@ -114,6 +123,14 @@ jobs: KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }} run: chmod +x ./gradlew && ./gradlew -p src assembleRelease + - name: Store build cache + uses: https://github.com/actions/cache/save@v3 + with: + path: | + src/**/build + !src/**/build/outputs + key: build-cache-${{ github.sha }}-${{ matrix.chunk }} + - name: Upload APKs uses: https://code.forgejo.org/forgejo/upload-artifact@16871d9e8cfcf27ff31822cac382bbb5450f1e1e # v4-patch if: "github.repository == 'Kohi-den/extensions-source'" @@ -178,6 +195,10 @@ jobs: - name: Sync repo run: | rsync -a --delete --exclude .git --exclude .gitignore main/repo/ repo --exclude README.md --exclude repo.json + + - name: Increase buffer size + run: | + git config --global http.postBuffer 157286400 - name: Deploy repo uses: https://github.com/EndBug/add-and-commit@v9 diff --git a/lib/megacloud-extractor/src/main/assets/megacloud.getsrcs.js b/lib/megacloud-extractor/src/main/assets/megacloud.getsrcs.js index 68b29a79..a8b05bf3 100644 --- a/lib/megacloud-extractor/src/main/assets/megacloud.getsrcs.js +++ b/lib/megacloud-extractor/src/main/assets/megacloud.getsrcs.js @@ -2,7 +2,7 @@ // solution inspired from https://github.com/drblgn/rabbit_wasm/blob/main/rabbit.ts // solution inspired from https://github.com/shimizudev/consumet.ts/blob/master/dist/extractors/megacloud/megacloud.getsrcs.js -const embed_url = 'https://megacloud.tv/embed-2/e-1/'; +const embed_url = 'https://megacloud.tv/embed-2/v2/e-1/'; const referrer = 'https://hianime.to'; const user_agent = navigator.userAgent; let wasm; @@ -31,7 +31,7 @@ const image_data = { data: window.decoded_png, }; const canvas = { - baseUrl: 'https://megacloud.tv/embed-2/e-1/1hnXq7VzX0Ex?k=1', + baseUrl: 'https://megacloud.tv/embed-2/v2/e-1/1hnXq7VzX0Ex?k=1', width: 0, height: 0, style: { @@ -58,7 +58,7 @@ const fake_window = { }, origin: 'https://megacloud.tv', location: { - href: 'https://megacloud.tv/embed-2/e-1/1hnXq7VzX0Ex?k=1', + href: 'https://megacloud.tv/embed-2/v2/e-1/1hnXq7VzX0Ex?k=1', origin: 'https://megacloud.tv', }, performance: { @@ -327,9 +327,9 @@ function initWasm() { __wbg_createElement_03cf347ddad1c8c0: function () { return applyToWindow(function ( // @ts-ignore - index, + index, // @ts-ignore - decodeIndex, + decodeIndex, // @ts-ignore decodeIndexOffset) { return addToStack(canvas); @@ -338,9 +338,9 @@ function initWasm() { __wbg_querySelector_118a0639aa1f51cd: function () { return applyToWindow(function ( // @ts-ignore - index, + index, // @ts-ignore - decodeIndex, + decodeIndex, // @ts-ignore decodeOffset) { //let item = get(index).querySelector(decodeSub(decodeIndex, decodeOffset)); @@ -353,11 +353,11 @@ function initWasm() { return addToStack(nodeList); }, arguments); }, - __wbg_getAttribute_706ae88bd37410fa: function (offset, + __wbg_getAttribute_706ae88bd37410fa: function (offset, // @ts-ignore - index, + index, // @ts-ignore - decodeIndex, + decodeIndex, // @ts-ignore decodeOffset) { //let attr = get(index).getAttribute(decodeSub(decodeIndex, decodeOffset)); @@ -676,7 +676,7 @@ async function getSources(xrax) { let res = {}; try { await V(); - let getSourcesUrl = 'https://megacloud.tv/embed-2/ajax/e-1/getSources?id=' + + let getSourcesUrl = 'https://megacloud.tv/embed-2/v2/e-1/getSources?id=' + fake_window.pid + '&v=' + fake_window.localStorage.kversion + @@ -688,7 +688,7 @@ async function getSources(xrax) { headers: { 'User-Agent': user_agent, //"Referrer": fake_window.origin + "/v2/embed-4/" + xrax + "?z=", - Referer: embed_url + xrax + '?k=1', + Referer: embed_url + xrax + '?k=1&autoPlay=1&oa=0&asi=1', 'X-Requested-With': 'XMLHttpRequest', }, method: 'GET', @@ -711,4 +711,4 @@ async function getSources(xrax) { catch (err) { console.error(err); } -} +} \ No newline at end of file diff --git a/lib/megacloud-extractor/src/main/java/eu/kanade/tachiyomi/lib/megacloudextractor/MegaCloudExtractor.kt b/lib/megacloud-extractor/src/main/java/eu/kanade/tachiyomi/lib/megacloudextractor/MegaCloudExtractor.kt index 6b4d2fcf..b6d82ea4 100644 --- a/lib/megacloud-extractor/src/main/java/eu/kanade/tachiyomi/lib/megacloudextractor/MegaCloudExtractor.kt +++ b/lib/megacloud-extractor/src/main/java/eu/kanade/tachiyomi/lib/megacloudextractor/MegaCloudExtractor.kt @@ -1,6 +1,8 @@ package eu.kanade.tachiyomi.lib.megacloudextractor import android.content.SharedPreferences +import android.util.Base64 +import android.util.Log import eu.kanade.tachiyomi.animesource.model.Track import eu.kanade.tachiyomi.animesource.model.Video import eu.kanade.tachiyomi.lib.cryptoaes.CryptoAES @@ -20,7 +22,12 @@ import okhttp3.Headers import okhttp3.HttpUrl.Companion.toHttpUrl import okhttp3.OkHttpClient import uy.kohesive.injekt.injectLazy +import java.security.MessageDigest +import javax.crypto.Cipher +import javax.crypto.spec.IvParameterSpec +import javax.crypto.spec.SecretKeySpec +// MegaCloudExtractor class MegaCloudExtractor( private val client: OkHttpClient, private val headers: Headers, @@ -38,16 +45,16 @@ class MegaCloudExtractor( companion object { private val SERVER_URL = arrayOf("https://megacloud.tv", "https://rapid-cloud.co") - private val SOURCES_URL = arrayOf("/embed-2/ajax/e-1/getSources?id=", "/ajax/embed-6-v2/getSources?id=") + private val SOURCES_URL = arrayOf("/embed-2/v2/e-1/getSources?id=", "/ajax/embed-6-v2/getSources?id=") private val SOURCES_SPLITTER = arrayOf("/e-1/", "/embed-6-v2/") private val SOURCES_KEY = arrayOf("1", "6") - private const val E1_SCRIPT_URL = "https://megacloud.tv/js/player/a/prod/e1-player.min.js" - private const val E6_SCRIPT_URL = "https://rapid-cloud.co/js/player/prod/e6-player-v2.min.js" + private const val E1_SCRIPT_URL = "/js/player/a/v2/pro/embed-1.min.js" + private const val E6_SCRIPT_URL = "/js/player/e6-player-v2.min.js" private val MUTEX = Mutex() private var shouldUpdateKey = false private const val PREF_KEY_KEY = "megacloud_key_" private const val PREF_KEY_DEFAULT = "[[0, 0]]" - + private inline fun runLocked(crossinline block: () -> R) = runBlocking(Dispatchers.IO) { MUTEX.withLock { block() } } @@ -66,8 +73,8 @@ class MegaCloudExtractor( private fun updateKey(type: String) { val scriptUrl = when (type) { - "1" -> E1_SCRIPT_URL - "6" -> E6_SCRIPT_URL + "1" -> "${SERVER_URL[0]}$E1_SCRIPT_URL" + "6" -> "${SERVER_URL[1]}$E6_SCRIPT_URL" else -> throw Exception("Unknown key type") } val script = noCacheClient.newCall(GET(scriptUrl, cache = cacheControl)) @@ -142,16 +149,21 @@ class MegaCloudExtractor( } private fun getVideoDto(url: String): VideoDto { - val type = if (url.startsWith("https://megacloud.tv") or url.startsWith("https://megacloud.blog")) 0 else 1 + val type = if ( + url.startsWith("https://megacloud.tv") || + url.startsWith("https://megacloud.blog") + ) 0 else 1 val keyType = SOURCES_KEY[type] val id = url.substringAfter(SOURCES_SPLITTER[type], "") - .substringBefore("?", "").ifEmpty { throw Exception("I HATE THE ANTICHRIST") } + .substringBefore("?", "") + .ifEmpty { throw Exception("Failed to extract ID from URL") } - if (type == 0) { - return webViewResolver.getSources(id)!! - } + // Previous method using WebViewResolver to get key + // if (type == 0) { + // return webViewResolver.getSources(id)!! + // } val srcRes = client.newCall(GET(SERVER_URL[type] + SOURCES_URL[type] + id)) .execute() @@ -162,11 +174,82 @@ class MegaCloudExtractor( if (!data.encrypted) return json.decodeFromString(srcRes) val ciphered = data.sources.jsonPrimitive.content - val decrypted = json.decodeFromString>(tryDecrypting(ciphered, keyType)) + val decrypted = json.decodeFromString>( + // tryDecrypting(ciphered, keyType), + tryDecrypting(ciphered), + ) return VideoDto(decrypted, data.tracks) } + var megaKey: String? = null + + private fun tryDecrypting(ciphered: String): String { + return megaKey?.let { key -> + try { + decryptOpenSSL(ciphered, key).also { + Log.i("MegaCloudExtractor", "Decrypted URL: $it") + } + } catch (e: RuntimeException) { + Log.e("MegaCloudExtractor", "Decryption failed with existing key: ${e.message}") + decryptWithNewKey(ciphered) + } + } ?: decryptWithNewKey(ciphered) + } + + private fun decryptWithNewKey(ciphered: String): String { + val newKey = requestNewKey() + megaKey = newKey + return decryptOpenSSL(ciphered, newKey).also { + Log.i("MegaCloudExtractor", "Decrypted URL with new key: $it") + } + } + + private fun requestNewKey(): String = + client.newCall(GET("https://raw.githubusercontent.com/yogesh-hacker/MegacloudKeys/refs/heads/main/keys.json")) + .execute() + .use { response -> + if (!response.isSuccessful) throw IllegalStateException("Failed to fetch keys.json") + val jsonStr = response.body.string() + if (jsonStr.isEmpty()) throw IllegalStateException("keys.json is empty") + val key = json.decodeFromString>(jsonStr)["mega"] + ?: throw IllegalStateException("Mega key not found in keys.json") + Log.i("MegaCloudExtractor", "Using Mega Key: $key") + megaKey = key + key + } + + private fun decryptOpenSSL(encBase64: String, password: String): String { + try { + val data = Base64.decode(encBase64, Base64.NO_WRAP) // Base64.DEFAULT or Base64.NO_WRAP + require(data.copyOfRange(0, 8).contentEquals("Salted__".toByteArray())) + val salt = data.copyOfRange(8, 16) + val (key, iv) = opensslKeyIv(password.toByteArray(), salt) + + val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding") + val secretKey = SecretKeySpec(key, "AES") + val ivSpec = IvParameterSpec(iv) + cipher.init(Cipher.DECRYPT_MODE, secretKey, ivSpec) + + val decrypted = cipher.doFinal(data.copyOfRange(16, data.size)) + return String(decrypted) + } catch (e: Exception) { + Log.e("DecryptOpenSSL", "Decryption failed: ${e.message}") + throw RuntimeException("Decryption failed: ${e.message}", e) + } + } + + private fun opensslKeyIv(password: ByteArray, salt: ByteArray, keyLen: Int = 32, ivLen: Int = 16): Pair { + var d = ByteArray(0) + var d_i = ByteArray(0) + while (d.size < keyLen + ivLen) { + val md = MessageDigest.getInstance("MD5") + d_i = md.digest(d_i + password + salt) + d += d_i + } + return Pair(d.copyOfRange(0, keyLen), d.copyOfRange(keyLen, keyLen + ivLen)) + } + @Serializable data class VideoDto( val sources: List, @@ -185,4 +268,4 @@ class MegaCloudExtractor( @Serializable data class TrackDto(val file: String, val kind: String, val label: String = "") -} +} \ No newline at end of file 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