Compare commits

...
Sign in to create a new pull request.

27 commits
main ... main

Author SHA1 Message Date
9c55a842ec revert 8e5f14d70e
revert Added a sort by codec feature to animetorrentio extension (#961)

Checklist:

- [x] Updated `extVersionCode` value in `build.gradle` for individual extensions
- [ ] Updated `overrideVersionCode` or `baseVersionCode` as needed for all multisrc extensions
- [ ] Referenced all related issues in the PR body (e.g. "Closes #xyz")
- [ ] 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
- [ ] Have removed `web_hi_res_512.png` when adding a new extension
- [ ] Have made sure all the icons are in png format

Co-authored-by: worldInColors <almonzer1234567@gmail.com>
Reviewed-on: Kohi-den/extensions-source#961
Co-authored-by: Ghost <>
Co-committed-by: Ghost <>
2025-06-20 01:18:38 -05:00
1018982a2e Some chinese sources update (#1022)
Close #957

Checklist:

- [x] Updated `extVersionCode` value in `build.gradle` for individual extensions
- [ ] Updated `overrideVersionCode` or `baseVersionCode` as needed for all multisrc extensions
- [x] Referenced all related issues in the PR body (e.g. "Closes #xyz")
- [ ] Added the `isNsfw = true` flag in `build.gradle` when appropriate
- [x] Have not changed source names
- [ ] 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
- [ ] Have removed `web_hi_res_512.png` when adding a new extension
- [ ] Have made sure all the icons are in png format

Co-authored-by: AlphaBoom <30779939+AlphaBoom@users.noreply.github.com>
Co-authored-by: ZhendongWu <30779939+AlphaBoom@users.noreply.github.com>
Reviewed-on: Kohi-den/extensions-source#1022
Co-authored-by: AlphaBoom <alphaboom@noreply.localhost>
Co-committed-by: AlphaBoom <alphaboom@noreply.localhost>
2025-06-20 01:05:04 -05:00
f97d742c40 feat(fr/animesama): update AnimeSama with new filters, improved search functionality and add a default player preference in param (#1018)
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

Reviewed-on: Kohi-den/extensions-source#1018
Co-authored-by: Mathis <mathis.quemener@gmail.com>
Co-committed-by: Mathis <mathis.quemener@gmail.com>
2025-06-20 01:04:42 -05:00
Ghost
9581c675cf Closes #1000 (#1001)
Checklist:

Closes #1000

- [ ] Updated `extVersionCode` value in `build.gradle` for individual extensions
- [ ] Updated `overrideVersionCode` or `baseVersionCode` as needed for all multisrc extensions
- [ ] Referenced all related issues in the PR body (e.g. "Closes #xyz")
- [ ] Added the `isNsfw = true` flag in `build.gradle` when appropriate
- [ ] Have not changed source names
- [ ] Have explicitly kept the `id` if a source's name or language were changed
- [ ] Have tested the modifications by compiling and running the extension through Android Studio
- [ ] Have removed `web_hi_res_512.png` when adding a new extension
- [ ] Have made sure all the icons are in png format

Co-authored-by: GraveEaterMadison <GraveEaterMadison@users.noreply.github.com>
Co-authored-by: Thinker <170967310+GraveEaterMadison@users.noreply.github.com>
Co-authored-by: Zero <170967310+GraveEaterMadison@users.noreply.github.com>
Reviewed-on: Kohi-den/extensions-source#1001
Co-authored-by: Ghost <>
Co-committed-by: Ghost <>
2025-06-20 01:03:32 -05:00
Ghost
8e5f14d70e Added a sort by codec feature to animetorrentio extension (#961)
Checklist:

- [x] Updated `extVersionCode` value in `build.gradle` for individual extensions
- [ ] Updated `overrideVersionCode` or `baseVersionCode` as needed for all multisrc extensions
- [ ] Referenced all related issues in the PR body (e.g. "Closes #xyz")
- [ ] 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
- [ ] Have removed `web_hi_res_512.png` when adding a new extension
- [ ] Have made sure all the icons are in png format

Co-authored-by: worldInColors <almonzer1234567@gmail.com>
Reviewed-on: Kohi-den/extensions-source#961
Co-authored-by: Ghost <>
Co-committed-by: Ghost <>
2025-06-20 01:03:02 -05:00
546ad886f2 ci: added Increase buffer size
this is needed to deploy the repo,
2025-06-20 00:42:21 -05:00
Kohi-den-Bot
29c3b5978d
[skip ci] chore: Mass-bump on extensions 2025-06-20 05:13:49 +00:00
Kohi-den-Bot
2b3563ab20
[skip ci] chore: Mass-bump on extensions 2025-06-20 04:39:52 +00:00
Ghost
0ba38fb4a7 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 <spheresox@gmail.com>
Reviewed-on: Kohi-den/extensions-source#960
Co-authored-by: Ghost <>
Co-committed-by: Ghost <>
2025-06-19 23:38:05 -05:00
b1d2972f9f ci: increased ci chunk size and removed (chunk ${{ matrix.chunk }}) in build extensions from name 2025-06-19 23:37:08 -05:00
Ghost
f84834770d [animeowl] fix: Quality preference match (#880)
Checklist:

- [x] Updated `extVersionCode` value in `build.gradle` for individual extensions
- [ ] Updated `overrideVersionCode` or `baseVersionCode` as needed for all multisrc extensions
- [ ] Referenced all related issues in the PR body (e.g. "Closes #xyz")
- [ ] Added the `isNsfw = true` flag in `build.gradle` when appropriate
- [x] Have not changed source names
- [ ] 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
- [ ] Have removed `web_hi_res_512.png` when adding a new extension
- [ ] Have made sure all the icons are in png format

Co-authored-by: Khaled <spkhalad@gmail.com>
Reviewed-on: Kohi-den/extensions-source#880
Co-authored-by: Ghost <>
Co-committed-by: Ghost <>
2025-06-19 19:08:09 -05:00
Kohi-den-Bot
aecb565aeb
[skip ci] chore: Mass-bump on extensions 2025-06-19 20:56:52 +00:00
7961308913 chore: Bump MegaCloudExtractor 2025-06-19 17:55:18 -03:00
6c07332fc6 ci: Fixed build cache to restore before save 2025-06-19 16:49:37 -03:00
52401de56a ci: Fixed wrong order of cache build 2025-06-19 16:09:23 -03:00
0adf9c4291 ci: Added missing restore cache key for build workflow 2025-06-19 16:04:50 -03:00
5068b3c838 ci: Re-enabled Bump extensions that uses a modified lib 2025-06-19 16:02:50 -03:00
6b0de66cf6 ci: Added build cache to workflow 2025-06-19 15:54:28 -03:00
d09fc02078 Manual update
cus mass bump don't wanna work
2025-06-19 13:47:41 -05:00
51962cf999 megacloud fix (#1021)
Yoinked from yuzono
2025-06-19 13:34:00 -05:00
38d2bdbb59 revert e2f32478fc
revert megacloud fix (#1020)

yoinked from yuzono
2025-06-19 13:32:58 -05:00
a4be7ee304 Update .github/workflows/build_push.yml 2025-06-19 13:30:59 -05:00
e2f32478fc megacloud fix (#1020)
yoinked from yuzono
2025-06-19 13:29:06 -05:00
7147360823 fix: Fixed Q1N source using UniversalExtractor as fallback (#1019)
Checklist:

- [ ] Updated `extVersionCode` value in `build.gradle` for individual extensions
- [x] Updated `overrideVersionCode` or `baseVersionCode` as needed for all multisrc extensions
- [ ] Referenced all related issues in the PR body (e.g. "Closes #xyz")
- [ ] Added the `isNsfw = true` flag in `build.gradle` when appropriate
- [ ] Have not changed source names
- [ ] 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
- [ ] Have removed `web_hi_res_512.png` when adding a new extension
- [ ] Have made sure all the icons are in png format

Reviewed-on: Kohi-den/extensions-source#1019
Co-authored-by: WebDitto <webditto@proton.me>
Co-committed-by: WebDitto <webditto@proton.me>
2025-06-19 12:55:09 -05:00
9a5e2a9083 Update .github/workflows/build_push.yml 2025-06-19 12:26:35 -05:00
794bddeac8 Update .github/workflows/build_pull_request.yml 2025-06-19 12:24:11 -05:00
1c4a5bdd0b Update .github/workflows/build_push.yml 2025-06-19 12:14:32 -05:00
81 changed files with 620 additions and 252 deletions

View file

@ -13,7 +13,7 @@ concurrency:
cancel-in-progress: true cancel-in-progress: true
env: env:
CI_CHUNK_SIZE: 65 CI_CHUNK_SIZE: 288
jobs: jobs:
prepare: prepare:
@ -75,7 +75,18 @@ jobs:
with: with:
cache-read-only: true 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: env:
CI_CHUNK_NUM: ${{ matrix.chunk }} CI_CHUNK_NUM: ${{ matrix.chunk }}
run: chmod +x ./gradlew && ./gradlew -p src assembleDebug run: chmod +x ./gradlew && ./gradlew -p src assembleDebug

View file

@ -37,15 +37,14 @@ jobs:
projects=(src/*/*) projects=(src/*/*)
echo "NUM_INDIVIDUAL_MODULES=${#projects[@]}" >> "$GITHUB_ENV" echo "NUM_INDIVIDUAL_MODULES=${#projects[@]}" >> "$GITHUB_ENV"
# Temporary pause because of leak of tj-actions/changed-files - name: Find lib changes
# - name: Find lib changes id: modified-libs
# id: modified-libs uses: tj-actions/changed-files@ed68ef82c095e0d48ec87eccea555d944a631a4c # v46.0.5
# uses: tj-actions/changed-files@90a06d6ba9543371ab4df8eeca0be07ca6054959 #v42 with:
# with: files: lib/
# files: lib/ files_ignore: lib/**.md
# files_ignore: lib/**.md files_separator: " "
# files_separator: " " safe_output: false
# safe_output: false
- name: Import GPG key - name: Import GPG key
uses: https://github.com/crazy-max/ghaction-import-gpg@v6 # v6.1.0 uses: https://github.com/crazy-max/ghaction-import-gpg@v6 # v6.1.0
@ -55,12 +54,11 @@ jobs:
git_user_signingkey: true git_user_signingkey: true
git_commit_gpgsign: 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
# - name: Bump extensions that uses a modified lib if: steps.modified-libs.outputs.any_changed == 'true'
# if: steps.modified-libs.outputs.any_changed == 'true' run: |
# run: | chmod +x ./.github/scripts/bump-versions.py
# chmod +x ./.github/scripts/bump-versions.py ./.github/scripts/bump-versions.py ${{ steps.modified-libs.outputs.all_changed_files }}
# ./.github/scripts/bump-versions.py ${{ steps.modified-libs.outputs.all_changed_files }}
- id: generate-matrices - id: generate-matrices
name: Create output matrices name: Create output matrices
@ -106,6 +104,17 @@ jobs:
- name: Set up Gradle - name: Set up Gradle
uses: https://github.com/gradle/actions/setup-gradle@245c8a24de79c0dbeabaf19ebcbbd3b2c36f278d # v4 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 - name: Build extensions
env: env:
CI_CHUNK_NUM: ${{ matrix.chunk }} CI_CHUNK_NUM: ${{ matrix.chunk }}
@ -114,6 +123,14 @@ jobs:
KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }} KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }}
run: chmod +x ./gradlew && ./gradlew -p src assembleRelease 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 - name: Upload APKs
uses: https://code.forgejo.org/forgejo/upload-artifact@16871d9e8cfcf27ff31822cac382bbb5450f1e1e # v4-patch uses: https://code.forgejo.org/forgejo/upload-artifact@16871d9e8cfcf27ff31822cac382bbb5450f1e1e # v4-patch
if: "github.repository == 'Kohi-den/extensions-source'" if: "github.repository == 'Kohi-den/extensions-source'"
@ -178,6 +195,10 @@ jobs:
- name: Sync repo - name: Sync repo
run: | run: |
rsync -a --delete --exclude .git --exclude .gitignore main/repo/ repo --exclude README.md --exclude repo.json 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 - name: Deploy repo
uses: https://github.com/EndBug/add-and-commit@v9 uses: https://github.com/EndBug/add-and-commit@v9

View file

@ -2,7 +2,7 @@
// solution inspired from https://github.com/drblgn/rabbit_wasm/blob/main/rabbit.ts // 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 // 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 referrer = 'https://hianime.to';
const user_agent = navigator.userAgent; const user_agent = navigator.userAgent;
let wasm; let wasm;
@ -31,7 +31,7 @@ const image_data = {
data: window.decoded_png, data: window.decoded_png,
}; };
const canvas = { 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, width: 0,
height: 0, height: 0,
style: { style: {
@ -58,7 +58,7 @@ const fake_window = {
}, },
origin: 'https://megacloud.tv', origin: 'https://megacloud.tv',
location: { 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', origin: 'https://megacloud.tv',
}, },
performance: { performance: {
@ -327,9 +327,9 @@ function initWasm() {
__wbg_createElement_03cf347ddad1c8c0: function () { __wbg_createElement_03cf347ddad1c8c0: function () {
return applyToWindow(function ( return applyToWindow(function (
// @ts-ignore // @ts-ignore
index, index,
// @ts-ignore // @ts-ignore
decodeIndex, decodeIndex,
// @ts-ignore // @ts-ignore
decodeIndexOffset) { decodeIndexOffset) {
return addToStack(canvas); return addToStack(canvas);
@ -338,9 +338,9 @@ function initWasm() {
__wbg_querySelector_118a0639aa1f51cd: function () { __wbg_querySelector_118a0639aa1f51cd: function () {
return applyToWindow(function ( return applyToWindow(function (
// @ts-ignore // @ts-ignore
index, index,
// @ts-ignore // @ts-ignore
decodeIndex, decodeIndex,
// @ts-ignore // @ts-ignore
decodeOffset) { decodeOffset) {
//let item = get(index).querySelector(decodeSub(decodeIndex, decodeOffset)); //let item = get(index).querySelector(decodeSub(decodeIndex, decodeOffset));
@ -353,11 +353,11 @@ function initWasm() {
return addToStack(nodeList); return addToStack(nodeList);
}, arguments); }, arguments);
}, },
__wbg_getAttribute_706ae88bd37410fa: function (offset, __wbg_getAttribute_706ae88bd37410fa: function (offset,
// @ts-ignore // @ts-ignore
index, index,
// @ts-ignore // @ts-ignore
decodeIndex, decodeIndex,
// @ts-ignore // @ts-ignore
decodeOffset) { decodeOffset) {
//let attr = get(index).getAttribute(decodeSub(decodeIndex, decodeOffset)); //let attr = get(index).getAttribute(decodeSub(decodeIndex, decodeOffset));
@ -676,7 +676,7 @@ async function getSources(xrax) {
let res = {}; let res = {};
try { try {
await V(); 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 + fake_window.pid +
'&v=' + '&v=' +
fake_window.localStorage.kversion + fake_window.localStorage.kversion +
@ -688,7 +688,7 @@ async function getSources(xrax) {
headers: { headers: {
'User-Agent': user_agent, 'User-Agent': user_agent,
//"Referrer": fake_window.origin + "/v2/embed-4/" + xrax + "?z=", //"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', 'X-Requested-With': 'XMLHttpRequest',
}, },
method: 'GET', method: 'GET',
@ -711,4 +711,4 @@ async function getSources(xrax) {
catch (err) { catch (err) {
console.error(err); console.error(err);
} }
} }

View file

@ -1,6 +1,8 @@
package eu.kanade.tachiyomi.lib.megacloudextractor package eu.kanade.tachiyomi.lib.megacloudextractor
import android.content.SharedPreferences 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.Track
import eu.kanade.tachiyomi.animesource.model.Video import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.lib.cryptoaes.CryptoAES import eu.kanade.tachiyomi.lib.cryptoaes.CryptoAES
@ -20,7 +22,12 @@ import okhttp3.Headers
import okhttp3.HttpUrl.Companion.toHttpUrl import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import uy.kohesive.injekt.injectLazy 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( class MegaCloudExtractor(
private val client: OkHttpClient, private val client: OkHttpClient,
private val headers: Headers, private val headers: Headers,
@ -38,16 +45,16 @@ class MegaCloudExtractor(
companion object { companion object {
private val SERVER_URL = arrayOf("https://megacloud.tv", "https://rapid-cloud.co") 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_SPLITTER = arrayOf("/e-1/", "/embed-6-v2/")
private val SOURCES_KEY = arrayOf("1", "6") 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 E1_SCRIPT_URL = "/js/player/a/v2/pro/embed-1.min.js"
private const val E6_SCRIPT_URL = "https://rapid-cloud.co/js/player/prod/e6-player-v2.min.js" private const val E6_SCRIPT_URL = "/js/player/e6-player-v2.min.js"
private val MUTEX = Mutex() private val MUTEX = Mutex()
private var shouldUpdateKey = false private var shouldUpdateKey = false
private const val PREF_KEY_KEY = "megacloud_key_" private const val PREF_KEY_KEY = "megacloud_key_"
private const val PREF_KEY_DEFAULT = "[[0, 0]]" private const val PREF_KEY_DEFAULT = "[[0, 0]]"
private inline fun <reified R> runLocked(crossinline block: () -> R) = runBlocking(Dispatchers.IO) { private inline fun <reified R> runLocked(crossinline block: () -> R) = runBlocking(Dispatchers.IO) {
MUTEX.withLock { block() } MUTEX.withLock { block() }
} }
@ -66,8 +73,8 @@ class MegaCloudExtractor(
private fun updateKey(type: String) { private fun updateKey(type: String) {
val scriptUrl = when (type) { val scriptUrl = when (type) {
"1" -> E1_SCRIPT_URL "1" -> "${SERVER_URL[0]}$E1_SCRIPT_URL"
"6" -> E6_SCRIPT_URL "6" -> "${SERVER_URL[1]}$E6_SCRIPT_URL"
else -> throw Exception("Unknown key type") else -> throw Exception("Unknown key type")
} }
val script = noCacheClient.newCall(GET(scriptUrl, cache = cacheControl)) val script = noCacheClient.newCall(GET(scriptUrl, cache = cacheControl))
@ -142,16 +149,21 @@ class MegaCloudExtractor(
} }
private fun getVideoDto(url: String): VideoDto { 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 keyType = SOURCES_KEY[type]
val id = url.substringAfter(SOURCES_SPLITTER[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) { // Previous method using WebViewResolver to get key
return webViewResolver.getSources(id)!! // if (type == 0) {
} // return webViewResolver.getSources(id)!!
// }
val srcRes = client.newCall(GET(SERVER_URL[type] + SOURCES_URL[type] + id)) val srcRes = client.newCall(GET(SERVER_URL[type] + SOURCES_URL[type] + id))
.execute() .execute()
@ -162,11 +174,82 @@ class MegaCloudExtractor(
if (!data.encrypted) return json.decodeFromString<VideoDto>(srcRes) if (!data.encrypted) return json.decodeFromString<VideoDto>(srcRes)
val ciphered = data.sources.jsonPrimitive.content val ciphered = data.sources.jsonPrimitive.content
val decrypted = json.decodeFromString<List<VideoLink>>(tryDecrypting(ciphered, keyType)) val decrypted = json.decodeFromString<List<VideoLink>>(
// tryDecrypting(ciphered, keyType),
tryDecrypting(ciphered),
)
return VideoDto(decrypted, data.tracks) 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<Map<String, String>>(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<ByteArray, ByteArray> {
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 @Serializable
data class VideoDto( data class VideoDto(
val sources: List<VideoLink>, val sources: List<VideoLink>,
@ -185,4 +268,4 @@ class MegaCloudExtractor(
@Serializable @Serializable
data class TrackDto(val file: String, val kind: String, val label: String = "") data class TrackDto(val file: String, val kind: String, val label: String = "")
} }

View file

@ -18,14 +18,33 @@ class VoeExtractor(private val client: OkHttpClient) {
private val playlistUtils by lazy { PlaylistUtils(clientDdos) } 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 @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<VideoLinkDTO>(decoded).source
}
fun videosFromUrl(url: String, prefix: String = ""): List<Video> { fun videosFromUrl(url: String, prefix: String = ""): List<Video> {
var document = clientDdos.newCall(GET(url)).execute().asJsoup() var document = clientDdos.newCall(GET(url)).execute().asJsoup()
@ -38,25 +57,12 @@ class VoeExtractor(private val client: OkHttpClient) {
document = clientDdos.newCall(GET(originalUrl)).execute().asJsoup() document = clientDdos.newCall(GET(originalUrl)).execute().asJsoup()
} }
val alternativeScript = document.select("script").find { scriptBase64Regex.containsMatchIn(it.data()) }?.data() val encodedVoeData = document.select("script").find { it.data().contains("MKGMa=\"")}?.data()
val script = document.selectFirst("script:containsData(const sources), script:containsData(var sources), script:containsData(wc0)")?.data() ?.substringAfter("MKGMa=\"")
?: alternativeScript ?.substringBefore('"') ?: return emptyList()
?: return emptyList()
val playlistUrl = when { val playlistUrl = decodeVoeData(encodedVoeData)
// Layout 1
script.contains("sources") -> {
val link = script.substringAfter("hls': '").substringBefore("'")
if (linkRegex.matches(link)) link else String(Base64.decode(link, Base64.DEFAULT))
}
// Layout 2
script.contains("wc0") || alternativeScript != null -> {
val base64 = base64Regex.find(script)!!.value
val decoded = Base64.decode(base64, Base64.DEFAULT).let(::String)
json.decodeFromString<VideoLinkDTO>(if (alternativeScript != null) decoded.reversed() else decoded).file
}
else -> return emptyList()
}
return playlistUtils.extractFromHls(playlistUrl, return playlistUtils.extractFromHls(playlistUrl,
videoNameGen = { quality -> "${prefix}Voe:$quality" } videoNameGen = { quality -> "${prefix}Voe:$quality" }
) )

View file

@ -1,7 +1,7 @@
ext { ext {
extName = 'JavGG' extName = 'JavGG'
extClass = '.Javgg' extClass = '.Javgg'
extVersionCode = 7 extVersionCode = 9
isNsfw = true isNsfw = true
} }

View file

@ -1,7 +1,7 @@
ext { ext {
extName = 'SupJav' extName = 'SupJav'
extClass = '.SupJavFactory' extClass = '.SupJavFactory'
extVersionCode = 16 extVersionCode = 18
isNsfw = true isNsfw = true
} }

View file

@ -1,7 +1,7 @@
ext { ext {
extName = 'Anime4up' extName = 'Anime4up'
extClass = '.Anime4Up' extClass = '.Anime4Up'
extVersionCode = 64 extVersionCode = 66
} }
apply from: "$rootDir/common.gradle" apply from: "$rootDir/common.gradle"

View file

@ -1,7 +1,7 @@
ext { ext {
extName = 'Arab Seed' extName = 'Arab Seed'
extClass = '.ArabSeed' extClass = '.ArabSeed'
extVersionCode = 19 extVersionCode = 21
} }
apply from: "$rootDir/common.gradle" apply from: "$rootDir/common.gradle"

View file

@ -1,7 +1,7 @@
ext { ext {
extName = 'Okanime' extName = 'Okanime'
extClass = '.Okanime' extClass = '.Okanime'
extVersionCode = 14 extVersionCode = 16
} }
apply from: "$rootDir/common.gradle" apply from: "$rootDir/common.gradle"

View file

@ -1,7 +1,7 @@
ext { ext {
extName = 'Anime-Base' extName = 'Anime-Base'
extClass = '.AnimeBase' extClass = '.AnimeBase'
extVersionCode = 33 extVersionCode = 35
isNsfw = true isNsfw = true
} }

View file

@ -1,7 +1,7 @@
ext { ext {
extName = 'Anime-Loads' extName = 'Anime-Loads'
extClass = '.AnimeLoads' extClass = '.AnimeLoads'
extVersionCode = 18 extVersionCode = 20
isNsfw = true isNsfw = true
} }

View file

@ -1,7 +1,7 @@
ext { ext {
extName = 'AnimeToast' extName = 'AnimeToast'
extClass = '.AnimeToast' extClass = '.AnimeToast'
extVersionCode = 23 extVersionCode = 25
} }
apply from: "$rootDir/common.gradle" apply from: "$rootDir/common.gradle"

View file

@ -1,7 +1,7 @@
ext { ext {
extName = 'AniWorld' extName = 'AniWorld'
extClass = '.AniWorld' extClass = '.AniWorld'
extVersionCode = 26 extVersionCode = 28
} }
apply from: "$rootDir/common.gradle" apply from: "$rootDir/common.gradle"

View file

@ -1,7 +1,7 @@
ext { ext {
extName = 'Einfach' extName = 'Einfach'
extClass = '.Einfach' extClass = '.Einfach'
extVersionCode = 18 extVersionCode = 20
} }
apply from: "$rootDir/common.gradle" apply from: "$rootDir/common.gradle"

View file

@ -1,7 +1,7 @@
ext { ext {
extName = 'FilmPalast' extName = 'FilmPalast'
extClass = '.FilmPalast' extClass = '.FilmPalast'
extVersionCode = 20 extVersionCode = 22
} }
apply from: "$rootDir/common.gradle" apply from: "$rootDir/common.gradle"

View file

@ -3,7 +3,7 @@ ext {
extClass = '.Kinoking' extClass = '.Kinoking'
themePkg = 'dooplay' themePkg = 'dooplay'
baseUrl = 'https://kinoking.cc' baseUrl = 'https://kinoking.cc'
overrideVersionCode = 24 overrideVersionCode = 26
} }
apply from: "$rootDir/common.gradle" apply from: "$rootDir/common.gradle"

View file

@ -1,7 +1,7 @@
ext { ext {
extName = 'Kool' extName = 'Kool'
extClass = '.Kool' extClass = '.Kool'
extVersionCode = 15 extVersionCode = 17
} }
apply from: "$rootDir/common.gradle" apply from: "$rootDir/common.gradle"

View file

@ -1,7 +1,7 @@
ext { ext {
extName = 'Movie4k' extName = 'Movie4k'
extClass = '.Movie4k' extClass = '.Movie4k'
extVersionCode = 12 extVersionCode = 14
} }
apply from: "$rootDir/common.gradle" apply from: "$rootDir/common.gradle"

View file

@ -1,7 +1,7 @@
ext { ext {
extName = 'Serienstream' extName = 'Serienstream'
extClass = '.Serienstream' extClass = '.Serienstream'
extVersionCode = 25 extVersionCode = 27
} }
apply from: "$rootDir/common.gradle" apply from: "$rootDir/common.gradle"

View file

@ -129,7 +129,7 @@ class AnimeOwl : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
val document = response.asJsoup() val document = response.asJsoup()
val sub = document.select("#anime-cover-sub-content .episode-node").mapIndexed { idx, it -> val sub = document.select("#anime-cover-sub-content .episode-node").mapIndexed { idx, it ->
EpisodeResponse.Episode( EpisodeResponse.Episode(
id = it.attr("title").toDoubleOrNull(), id = idx.toDouble(),
episodeIndex = idx.toString(), episodeIndex = idx.toString(),
name = it.attr("title"), name = it.attr("title"),
lang = "Sub", lang = "Sub",
@ -138,7 +138,7 @@ class AnimeOwl : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
} }
val dub = document.select("#anime-cover-dub-content .episode-node").mapIndexed { idx, it -> val dub = document.select("#anime-cover-dub-content .episode-node").mapIndexed { idx, it ->
EpisodeResponse.Episode( EpisodeResponse.Episode(
id = it.attr("title").toDoubleOrNull(), id = idx.toDouble(),
episodeIndex = idx.toString(), episodeIndex = idx.toString(),
name = it.attr("title"), name = it.attr("title"),
lang = "Dub", lang = "Dub",
@ -301,6 +301,6 @@ class AnimeOwl : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
private const val PREF_QUALITY_KEY = "preferred_quality" private const val PREF_QUALITY_KEY = "preferred_quality"
private const val PREF_QUALITY_TITLE = "Preferred quality" private const val PREF_QUALITY_TITLE = "Preferred quality"
private const val PREF_QUALITY_DEFAULT = "1080p" private const val PREF_QUALITY_DEFAULT = "1080p"
private val PREF_QUALITY_LIST = arrayOf("1080p", "720p", "480p", "360p") private val PREF_QUALITY_LIST = arrayOf("2K", "1080p", "720p", "480p", "360p")
} }
} }

View file

@ -48,8 +48,13 @@ class OwlExtractor(private val client: OkHttpClient, private val baseUrl: String
luffy.forEach { stream -> luffy.forEach { stream ->
noRedirectClient.newCall(GET("${stream.url}$jwt")).execute() noRedirectClient.newCall(GET("${stream.url}$jwt")).execute()
.use { it.headers["Location"] }?.let { .use { it.headers["Location"] }?.let {
val resolution = when {
stream.resolution?.endsWith("0") == true -> "${stream.resolution}p"
else -> stream.resolution
}
videoList.add( videoList.add(
Video(it, "${link.lang} Luffy:${stream.resolution}", it), Video(it, "${link.lang} Luffy:${resolution ?: "Unknown"}", it),
) )
} }
} }
@ -83,7 +88,10 @@ class OwlExtractor(private val client: OkHttpClient, private val baseUrl: String
return client.newCall(GET(url)).execute().let { it -> return client.newCall(GET(url)).execute().let { it ->
if (it.isSuccessful) { if (it.isSuccessful) {
it.parseAs<Stream>().url.let { it.parseAs<Stream>().url.let {
playlistUtils.extractFromHls(it, videoNameGen = { qty -> "$lang $server:$qty" }) playlistUtils.extractFromHls(
it,
videoNameGen = { qty -> "$lang $server:$qty" },
)
} }
} else { } else {
emptyList() emptyList()

View file

@ -3,7 +3,7 @@ ext {
extClass = '.AniWatchtv' extClass = '.AniWatchtv'
themePkg = 'zorotheme' themePkg = 'zorotheme'
baseUrl = 'https://aniwatchtv.to' baseUrl = 'https://aniwatchtv.to'
overrideVersionCode = 1 overrideVersionCode = 2
} }
apply from: "$rootDir/common.gradle" apply from: "$rootDir/common.gradle"

View file

@ -3,7 +3,7 @@ ext {
extClass = '.Kaido' extClass = '.Kaido'
themePkg = 'zorotheme' themePkg = 'zorotheme'
baseUrl = 'https://kaido.to' baseUrl = 'https://kaido.to'
overrideVersionCode = 9 overrideVersionCode = 10
} }
apply from: "$rootDir/common.gradle" apply from: "$rootDir/common.gradle"

View file

@ -3,7 +3,7 @@ ext {
extClass = '.HiAnime' extClass = '.HiAnime'
themePkg = 'zorotheme' themePkg = 'zorotheme'
baseUrl = 'https://hianimez.to' baseUrl = 'https://hianimez.to'
overrideVersionCode = 52 overrideVersionCode = 54
} }
apply from: "$rootDir/common.gradle" apply from: "$rootDir/common.gradle"

View file

@ -1,7 +1,7 @@
ext { ext {
extName = 'Animefenix' extName = 'Animefenix'
extClass = '.Animefenix' extClass = '.Animefenix'
extVersionCode = 59 extVersionCode = 61
} }
apply from: "$rootDir/common.gradle" apply from: "$rootDir/common.gradle"

View file

@ -1,7 +1,7 @@
ext { ext {
extName = 'Animejl' extName = 'Animejl'
extClass = '.Animejl' extClass = '.Animejl'
extVersionCode = 7 extVersionCode = 9
isNsfw = true isNsfw = true
} }

View file

@ -1,7 +1,7 @@
ext { ext {
extName = 'AnimeMovil' extName = 'AnimeMovil'
extClass = '.AnimeMovil' extClass = '.AnimeMovil'
extVersionCode = 31 extVersionCode = 33
} }
apply from: "$rootDir/common.gradle" apply from: "$rootDir/common.gradle"

View file

@ -1,7 +1,7 @@
ext { ext {
extName = 'Cine24h' extName = 'Cine24h'
extClass = '.Cine24h' extClass = '.Cine24h'
extVersionCode = 13 extVersionCode = 15
} }
apply from: "$rootDir/common.gradle" apply from: "$rootDir/common.gradle"

View file

@ -1,7 +1,7 @@
ext { ext {
extName = 'CineCalidad' extName = 'CineCalidad'
extClass = '.CineCalidad' extClass = '.CineCalidad'
extVersionCode = 19 extVersionCode = 21
} }
apply from: "$rootDir/common.gradle" apply from: "$rootDir/common.gradle"

View file

@ -1,7 +1,7 @@
ext { ext {
extName = 'Cuevana' extName = 'Cuevana'
extClass = '.CuevanaFactory' extClass = '.CuevanaFactory'
extVersionCode = 48 extVersionCode = 50
} }
apply from: "$rootDir/common.gradle" apply from: "$rootDir/common.gradle"

View file

@ -3,7 +3,7 @@ ext {
extClass = '.DeTodoPeliculas' extClass = '.DeTodoPeliculas'
themePkg = 'dooplay' themePkg = 'dooplay'
baseUrl = 'https://detodopeliculas.nu' baseUrl = 'https://detodopeliculas.nu'
overrideVersionCode = 5 overrideVersionCode = 7
} }
apply from: "$rootDir/common.gradle" apply from: "$rootDir/common.gradle"

View file

@ -1,7 +1,7 @@
ext { ext {
extName = 'Doramasflix' extName = 'Doramasflix'
extClass = '.Doramasflix' extClass = '.Doramasflix'
extVersionCode = 35 extVersionCode = 37
} }
apply from: "$rootDir/common.gradle" apply from: "$rootDir/common.gradle"

View file

@ -1,7 +1,7 @@
ext { ext {
extName = 'Doramasyt' extName = 'Doramasyt'
extClass = '.Doramasyt' extClass = '.Doramasyt'
extVersionCode = 22 extVersionCode = 24
} }
apply from: "$rootDir/common.gradle" apply from: "$rootDir/common.gradle"

View file

@ -1,7 +1,7 @@
ext { ext {
extName = 'EnNovelas' extName = 'EnNovelas'
extClass = '.EnNovelas' extClass = '.EnNovelas'
extVersionCode = 21 extVersionCode = 23
} }
apply from: "$rootDir/common.gradle" apply from: "$rootDir/common.gradle"

View file

@ -1,7 +1,7 @@
ext { ext {
extName = 'EstrenosDoramas' extName = 'EstrenosDoramas'
extClass = '.EstrenosDoramas' extClass = '.EstrenosDoramas'
extVersionCode = 7 extVersionCode = 9
} }
apply from: "$rootDir/common.gradle" apply from: "$rootDir/common.gradle"

View file

@ -3,7 +3,7 @@ ext {
extClass = '.FlixLatam' extClass = '.FlixLatam'
themePkg = 'dooplay' themePkg = 'dooplay'
baseUrl = 'https://flixlatam.com' baseUrl = 'https://flixlatam.com'
overrideVersionCode = 6 overrideVersionCode = 8
} }
apply from: "$rootDir/common.gradle" apply from: "$rootDir/common.gradle"

View file

@ -1,7 +1,7 @@
ext { ext {
extName = 'Gnula' extName = 'Gnula'
extClass = '.Gnula' extClass = '.Gnula'
extVersionCode = 33 extVersionCode = 35
} }
apply from: "$rootDir/common.gradle" apply from: "$rootDir/common.gradle"

View file

@ -1,7 +1,7 @@
ext { ext {
extName = 'Hackstore' extName = 'Hackstore'
extClass = '.Hackstore' extClass = '.Hackstore'
extVersionCode = 27 extVersionCode = 29
} }
apply from: "$rootDir/common.gradle" apply from: "$rootDir/common.gradle"

View file

@ -1,7 +1,7 @@
ext { ext {
extName = 'HentaiLA' extName = 'HentaiLA'
extClass = '.Hentaila' extClass = '.Hentaila'
extVersionCode = 36 extVersionCode = 38
isNsfw = true isNsfw = true
} }

View file

@ -1,7 +1,7 @@
ext { ext {
extName = 'HentaiTk' extName = 'HentaiTk'
extClass = '.Hentaitk' extClass = '.Hentaitk'
extVersionCode = 15 extVersionCode = 17
isNsfw = true isNsfw = true
} }

View file

@ -1,7 +1,7 @@
ext { ext {
extName = 'HomeCine' extName = 'HomeCine'
extClass = '.HomeCine' extClass = '.HomeCine'
extVersionCode = 7 extVersionCode = 9
} }
apply from: "$rootDir/common.gradle" apply from: "$rootDir/common.gradle"

View file

@ -1,7 +1,7 @@
ext { ext {
extName = 'Jkanime' extName = 'Jkanime'
extClass = '.Jkanime' extClass = '.Jkanime'
extVersionCode = 37 extVersionCode = 39
} }
apply from: "$rootDir/common.gradle" apply from: "$rootDir/common.gradle"

View file

@ -1,7 +1,7 @@
ext { ext {
extName = 'LACartoons' extName = 'LACartoons'
extClass = '.Lacartoons' extClass = '.Lacartoons'
extVersionCode = 13 extVersionCode = 15
} }
apply from: "$rootDir/common.gradle" apply from: "$rootDir/common.gradle"

View file

@ -1,7 +1,7 @@
ext { ext {
extName = 'MetroSeries' extName = 'MetroSeries'
extClass = '.MetroSeries' extClass = '.MetroSeries'
extVersionCode = 20 extVersionCode = 22
} }
apply from: "$rootDir/common.gradle" apply from: "$rootDir/common.gradle"

View file

@ -1,7 +1,7 @@
ext { ext {
extName = 'MhdFlix' extName = 'MhdFlix'
extClass = '.MhdFlix' extClass = '.MhdFlix'
extVersionCode = 8 extVersionCode = 10
} }
apply from: "$rootDir/common.gradle" apply from: "$rootDir/common.gradle"

View file

@ -1,7 +1,7 @@
ext { ext {
extName = 'MonosChinos' extName = 'MonosChinos'
extClass = '.MonosChinos' extClass = '.MonosChinos'
extVersionCode = 37 extVersionCode = 39
} }
apply from: "$rootDir/common.gradle" apply from: "$rootDir/common.gradle"

View file

@ -1,7 +1,7 @@
ext { ext {
extName = 'MundoDonghua' extName = 'MundoDonghua'
extClass = '.MundoDonghua' extClass = '.MundoDonghua'
extVersionCode = 31 extVersionCode = 33
} }
apply from: "$rootDir/common.gradle" apply from: "$rootDir/common.gradle"

View file

@ -1,7 +1,7 @@
ext { ext {
extName = 'Otakuverso' extName = 'Otakuverso'
extClass = '.Otakuverso' extClass = '.Otakuverso'
extVersionCode = 3 extVersionCode = 5
} }
apply from: "$rootDir/common.gradle" apply from: "$rootDir/common.gradle"

View file

@ -1,7 +1,7 @@
ext { ext {
extName = 'PelisForte' extName = 'PelisForte'
extClass = '.PelisForte' extClass = '.PelisForte'
extVersionCode = 32 extVersionCode = 34
} }
apply from: "$rootDir/common.gradle" apply from: "$rootDir/common.gradle"

View file

@ -1,7 +1,7 @@
ext { ext {
extName = 'Pelisplushd' extName = 'Pelisplushd'
extClass = '.PelisplushdFactory' extClass = '.PelisplushdFactory'
extVersionCode = 75 extVersionCode = 77
} }
apply from: "$rootDir/common.gradle" apply from: "$rootDir/common.gradle"

View file

@ -1,7 +1,7 @@
ext { ext {
extName = 'Serieskao' extName = 'Serieskao'
extClass = '.Serieskao' extClass = '.Serieskao'
extVersionCode = 6 extVersionCode = 8
} }
apply from: "$rootDir/common.gradle" apply from: "$rootDir/common.gradle"

View file

@ -3,7 +3,7 @@ ext {
extClass = '.SoloLatino' extClass = '.SoloLatino'
themePkg = 'dooplay' themePkg = 'dooplay'
baseUrl = 'https://sololatino.net' baseUrl = 'https://sololatino.net'
overrideVersionCode = 8 overrideVersionCode = 10
} }
apply from: "$rootDir/common.gradle" apply from: "$rootDir/common.gradle"

View file

@ -1,7 +1,7 @@
ext { ext {
extName = 'TioanimeH' extName = 'TioanimeH'
extClass = '.TioanimeHFactory' extClass = '.TioanimeHFactory'
extVersionCode = 26 extVersionCode = 28
} }
apply from: "$rootDir/common.gradle" apply from: "$rootDir/common.gradle"

View file

@ -3,7 +3,7 @@ ext {
extClass = '.Tiodonghua' extClass = '.Tiodonghua'
themePkg = 'animestream' themePkg = 'animestream'
baseUrl = 'https://anime.tiodonghua.com' baseUrl = 'https://anime.tiodonghua.com'
overrideVersionCode = 7 overrideVersionCode = 9
} }
apply from: "$rootDir/common.gradle" apply from: "$rootDir/common.gradle"

View file

@ -1,7 +1,7 @@
ext { ext {
extName = 'VerAnimes' extName = 'VerAnimes'
extClass = '.VerAnimes' extClass = '.VerAnimes'
extVersionCode = 12 extVersionCode = 14
} }
apply from: "$rootDir/common.gradle" apply from: "$rootDir/common.gradle"

View file

@ -1,7 +1,7 @@
ext { ext {
extName = 'VerSeriesOnline' extName = 'VerSeriesOnline'
extClass = '.VerSeriesOnline' extClass = '.VerSeriesOnline'
extVersionCode = 6 extVersionCode = 8
} }
apply from: "$rootDir/common.gradle" apply from: "$rootDir/common.gradle"

View file

@ -1,7 +1,7 @@
ext { ext {
extName = 'Zonaleros' extName = 'Zonaleros'
extClass = '.Zonaleros' extClass = '.Zonaleros'
extVersionCode = 3 extVersionCode = 5
} }
apply from: "$rootDir/common.gradle" apply from: "$rootDir/common.gradle"

View file

@ -1,7 +1,7 @@
ext { ext {
extName = 'Anime-Sama' extName = 'Anime-Sama'
extClass = '.AnimeSama' extClass = '.AnimeSama'
extVersionCode = 12 extVersionCode = 13
} }
apply from: "$rootDir/common.gradle" apply from: "$rootDir/common.gradle"

View file

@ -4,6 +4,7 @@ import android.app.Application
import android.content.SharedPreferences import android.content.SharedPreferences
import androidx.preference.ListPreference import androidx.preference.ListPreference
import androidx.preference.PreferenceScreen import androidx.preference.PreferenceScreen
import app.cash.quickjs.QuickJs
import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
import eu.kanade.tachiyomi.animesource.model.AnimesPage import eu.kanade.tachiyomi.animesource.model.AnimesPage
@ -17,6 +18,7 @@ import eu.kanade.tachiyomi.lib.vkextractor.VkExtractor
import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.util.asJsoup import eu.kanade.tachiyomi.util.asJsoup
import eu.kanade.tachiyomi.util.parallelCatchingFlatMap import eu.kanade.tachiyomi.util.parallelCatchingFlatMap
import eu.kanade.tachiyomi.util.parallelFlatMapBlocking
import kotlinx.serialization.encodeToString import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import okhttp3.HttpUrl.Companion.toHttpUrl import okhttp3.HttpUrl.Companion.toHttpUrl
@ -49,12 +51,11 @@ class AnimeSama : ConfigurableAnimeSource, AnimeHttpSource() {
// ============================== Popular =============================== // ============================== Popular ===============================
override fun popularAnimeParse(response: Response): AnimesPage { override fun popularAnimeParse(response: Response): AnimesPage {
val doc = response.body.string() val doc = response.asJsoup()
val page = response.request.url.fragment?.toInt() ?: 0 val page = response.request.url.fragment?.toInt() ?: 0
val regex = Regex("^\\s*carteClassique\\(\\s*.*?\\s*,\\s*\"(.*?)\".*\\)", RegexOption.MULTILINE) val chunks = doc.select("#containerPepites > div a").chunked(5)
val chunks = regex.findAll(doc).chunked(5).toList()
val seasons = chunks.getOrNull(page - 1)?.flatMap { val seasons = chunks.getOrNull(page - 1)?.flatMap {
val animeUrl = "$baseUrl/catalogue/${it.groupValues[1]}" val animeUrl = "$baseUrl${it.attr("href")}"
fetchAnimeSeasons(animeUrl) fetchAnimeSeasons(animeUrl)
}?.toList().orEmpty() }?.toList().orEmpty()
return AnimesPage(seasons, page < chunks.size) return AnimesPage(seasons, page < chunks.size)
@ -72,7 +73,7 @@ class AnimeSama : ConfigurableAnimeSource, AnimeHttpSource() {
.removePathSegment(animeUrl.pathSize - 3) .removePathSegment(animeUrl.pathSize - 3)
.build() .build()
fetchAnimeSeasons(url.toString()) fetchAnimeSeasons(url.toString())
} }.distinctBy { it.url }
return AnimesPage(seasons, false) return AnimesPage(seasons, false)
} }
override fun latestUpdatesRequest(page: Int): Request = GET(baseUrl) override fun latestUpdatesRequest(page: Int): Request = GET(baseUrl)
@ -80,29 +81,26 @@ class AnimeSama : ConfigurableAnimeSource, AnimeHttpSource() {
// =============================== Search =============================== // =============================== Search ===============================
override fun getFilterList() = AnimeSamaFilters.FILTER_LIST override fun getFilterList() = AnimeSamaFilters.FILTER_LIST
override suspend fun getSearchAnime(page: Int, query: String, filters: AnimeFilterList): AnimesPage { override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
if (query.startsWith(PREFIX_SEARCH)) { val url = "$baseUrl/catalogue/".toHttpUrl().newBuilder()
return AnimesPage(fetchAnimeSeasons("$baseUrl/catalogue/${query.removePrefix(PREFIX_SEARCH)}/"), false)
}
val params = AnimeSamaFilters.getSearchFilters(filters) val params = AnimeSamaFilters.getSearchFilters(filters)
val elements = database params.types.forEach { url.addQueryParameter("type[]", it) }
.asSequence() params.language.forEach { url.addQueryParameter("langue[]", it) }
.filter { it.select("h1, p").fold(false) { v, e -> v || e.text().contains(query, true) } } params.genres.forEach { url.addQueryParameter("genre[]", it) }
.filter { params.include.all { p -> it.className().contains(p) } } url.addQueryParameter("search", query)
.filter { params.exclude.none { p -> it.className().contains(p) } } url.addQueryParameter("page", "$page")
.filter { params.types.fold(params.types.isEmpty()) { v, p -> v || it.className().contains(p) } } return GET(url.build(), headers)
.filter { params.language.fold(params.language.isEmpty()) { v, p -> v || it.className().contains(p) } }
.chunked(5)
.toList()
if (elements.isEmpty()) return AnimesPage(emptyList(), false)
val animes = elements[page - 1].flatMap {
fetchAnimeSeasons(it.getElementsByTag("a").attr("href"))
}
return AnimesPage(animes, page < elements.size)
} }
override fun searchAnimeParse(response: Response): AnimesPage = throw UnsupportedOperationException() override fun searchAnimeParse(response: Response): AnimesPage {
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request = throw UnsupportedOperationException() val document = response.asJsoup()
val anime = document.select("#list_catalog > div a").parallelFlatMapBlocking {
fetchAnimeSeasons(it.attr("href"))
}
val page = response.request.url.queryParameterValues("page").first()
val hasNextPage = document.select("#list_pagination a:last-child").text() != page
return AnimesPage(anime, hasNextPage)
}
// =========================== Anime Details ============================ // =========================== Anime Details ============================
override suspend fun getAnimeDetails(anime: SAnime): SAnime = anime override suspend fun getAnimeDetails(anime: SAnime): SAnime = anime
@ -121,6 +119,10 @@ class AnimeSama : ConfigurableAnimeSource, AnimeHttpSource() {
override fun episodeListParse(response: Response): List<SEpisode> = throw UnsupportedOperationException() override fun episodeListParse(response: Response): List<SEpisode> = throw UnsupportedOperationException()
// ============================ Video Links ============================= // ============================ Video Links =============================
private val sibnetExtractor by lazy { SibnetExtractor(client) }
private val vkExtractor by lazy { VkExtractor(client, headers) }
private val sendvidExtractor by lazy { SendvidExtractor(client, headers) }
override suspend fun getVideoList(episode: SEpisode): List<Video> { override suspend fun getVideoList(episode: SEpisode): List<Video> {
val playerUrls = json.decodeFromString<List<List<String>>>(episode.url) val playerUrls = json.decodeFromString<List<List<String>>>(episode.url)
val videos = playerUrls.flatMapIndexed { i, it -> val videos = playerUrls.flatMapIndexed { i, it ->
@ -128,9 +130,9 @@ class AnimeSama : ConfigurableAnimeSource, AnimeHttpSource() {
it.parallelCatchingFlatMap { playerUrl -> it.parallelCatchingFlatMap { playerUrl ->
with(playerUrl) { with(playerUrl) {
when { when {
contains("sibnet.ru") -> SibnetExtractor(client).videosFromUrl(playerUrl, prefix) contains("sibnet.ru") -> sibnetExtractor.videosFromUrl(playerUrl, prefix)
contains("vk.") -> VkExtractor(client, headers).videosFromUrl(playerUrl, prefix) contains("vk.") -> vkExtractor.videosFromUrl(playerUrl, prefix)
contains("sendvid.com") -> SendvidExtractor(client, headers).videosFromUrl(playerUrl, prefix) contains("sendvid.com") -> sendvidExtractor.videosFromUrl(playerUrl, prefix)
else -> emptyList() else -> emptyList()
} }
} }
@ -140,21 +142,16 @@ class AnimeSama : ConfigurableAnimeSource, AnimeHttpSource() {
} }
// ============================ Utils ============================= // ============================ Utils =============================
private fun sanitizeEpisodesJs(doc: String) = doc
.replace(Regex("[\"\t]"), "") // Fix trash format
.replace("'", "\"") // Fix quotes
.replace(Regex("/\\*.*?\\*/", setOf(RegexOption.MULTILINE, RegexOption.DOT_MATCHES_ALL)), "") // Remove block comments
.replace(Regex("(^|,|\\[)\\s*//.*?$", RegexOption.MULTILINE), "$1") // Remove line comments
.replace(Regex(",\\s*]"), "]") // Remove trailing comma
override fun List<Video>.sort(): List<Video> { override fun List<Video>.sort(): List<Video> {
val voices = preferences.getString(PREF_VOICES_KEY, PREF_VOICES_DEFAULT)!! val voices = preferences.getString(PREF_VOICES_KEY, PREF_VOICES_DEFAULT)!!
val quality = preferences.getString(PREF_QUALITY_KEY, PREF_QUALITY_DEFAULT)!! val quality = preferences.getString(PREF_QUALITY_KEY, PREF_QUALITY_DEFAULT)!!
val player = preferences.getString(PREF_PLAYER_KEY, PREF_PLAYER_DEFAULT)!!
return this.sortedWith( return this.sortedWith(
compareBy( compareBy(
{ it.quality.contains(voices, true) }, { it.quality.contains(voices, true) },
{ it.quality.contains(quality) }, { it.quality.contains(quality) },
{ it.quality.contains(player, true) },
), ),
).reversed() ).reversed()
} }
@ -164,14 +161,17 @@ class AnimeSama : ConfigurableAnimeSource, AnimeHttpSource() {
return fetchAnimeSeasons(res) return fetchAnimeSeasons(res)
} }
private val commentRegex by lazy { Regex("/\\*.*?\\*/", RegexOption.DOT_MATCHES_ALL) }
private val seasonRegex by lazy { Regex("^\\s*panneauAnime\\(\"(.*)\", \"(.*)\"\\)", RegexOption.MULTILINE) }
private fun fetchAnimeSeasons(response: Response): List<SAnime> { private fun fetchAnimeSeasons(response: Response): List<SAnime> {
val animeDoc = response.asJsoup() val animeDoc = response.asJsoup()
val animeUrl = response.request.url val animeUrl = response.request.url
val animeName = animeDoc.getElementById("titreOeuvre")?.text() ?: "" val animeName = animeDoc.getElementById("titreOeuvre")?.text() ?: ""
val seasonRegex = Regex("^\\s*panneauAnime\\(\"(.*)\", \"(.*)\"\\)", RegexOption.MULTILINE)
val scripts = animeDoc.select("h2 + p + div > script, h2 + div > script").toString() val scripts = animeDoc.select("h2 + p + div > script, h2 + div > script").toString()
val animes = seasonRegex.findAll(scripts).flatMapIndexed { animeIndex, seasonMatch -> val uncommented = commentRegex.replace(scripts, "")
val animes = seasonRegex.findAll(uncommented).flatMapIndexed { animeIndex, seasonMatch ->
val (seasonName, seasonStem) = seasonMatch.destructured val (seasonName, seasonStem) = seasonMatch.destructured
if (seasonStem.contains("film", true)) { if (seasonStem.contains("film", true)) {
val moviesUrl = "$animeUrl/$seasonStem" val moviesUrl = "$animeUrl/$seasonStem"
@ -219,23 +219,16 @@ class AnimeSama : ConfigurableAnimeSource, AnimeHttpSource() {
private fun fetchPlayers(url: String): List<List<String>> { private fun fetchPlayers(url: String): List<List<String>> {
val docUrl = "$url/episodes.js" val docUrl = "$url/episodes.js"
val players = mutableListOf<List<String>>() val doc = client.newCall(GET(docUrl)).execute().use {
val doc = client.newCall(GET(docUrl)).execute().run { if (!it.isSuccessful) return emptyList()
if (code != 200) { it.body.string()
close()
return listOf()
}
body.string()
} }
val sanitizedDoc = sanitizeEpisodesJs(doc) val urls = QuickJs.create().use { qjs ->
for (i in 1..8) { qjs.evaluate(doc)
val numPlayers = getPlayers("eps$i", sanitizedDoc) val res = qjs.evaluate("JSON.stringify(Array.from({length: 10}, (e,i) => this[`eps\${i}`]).filter(e => e))")
if (numPlayers != null) players.add(numPlayers) json.decodeFromString<List<List<String>>>(res as String)
} }
val asPlayers = getPlayers("epsAS", sanitizedDoc) return List(urls[0].size) { i -> urls.mapNotNull { it.getOrNull(i) }.distinct() }
if (asPlayers != null) players.add(asPlayers)
if (players.isEmpty()) return emptyList()
return List(players[0].size) { i -> players.mapNotNull { it.getOrNull(i) }.distinct() }
} }
private fun getPlayers(playerName: String, doc: String): List<String>? { private fun getPlayers(playerName: String, doc: String): List<String>? {
@ -276,6 +269,22 @@ class AnimeSama : ConfigurableAnimeSource, AnimeHttpSource() {
preferences.edit().putString(key, entry).commit() preferences.edit().putString(key, entry).commit()
} }
}.also(screen::addPreference) }.also(screen::addPreference)
ListPreference(screen.context).apply {
key = PREF_PLAYER_KEY
title = "Lecteur par défaut"
entries = PLAYERS
entryValues = PLAYERS_VALUES
setDefaultValue(PREF_PLAYER_DEFAULT)
summary = "%s"
setOnPreferenceChangeListener { _, newValue ->
val selected = newValue as String
val index = findIndexOfValue(selected)
val entry = entryValues[index] as String
preferences.edit().putString(key, entry).commit()
}
}.also(screen::addPreference)
} }
companion object { companion object {
@ -291,10 +300,25 @@ class AnimeSama : ConfigurableAnimeSource, AnimeHttpSource() {
"vf", "vf",
) )
private val PLAYERS = arrayOf(
"Sendvid",
"Sibnet",
"VK",
)
private val PLAYERS_VALUES = arrayOf(
"sendvid",
"sibnet",
"vk",
)
private const val PREF_VOICES_KEY = "voices_preference" private const val PREF_VOICES_KEY = "voices_preference"
private const val PREF_VOICES_DEFAULT = "vostfr" private const val PREF_VOICES_DEFAULT = "vostfr"
private const val PREF_QUALITY_KEY = "preferred_quality" private const val PREF_QUALITY_KEY = "preferred_quality"
private const val PREF_QUALITY_DEFAULT = "1080" private const val PREF_QUALITY_DEFAULT = "1080"
private const val PREF_PLAYER_KEY = "player_preference"
private const val PREF_PLAYER_DEFAULT = "sibnet"
} }
} }

View file

@ -9,10 +9,6 @@ object AnimeSamaFilters {
private class CheckBoxVal(name: String, state: Boolean = false) : AnimeFilter.CheckBox(name, state) private class CheckBoxVal(name: String, state: Boolean = false) : AnimeFilter.CheckBox(name, state)
open class TriStateFilterList(name: String, values: List<TriFilter>) : AnimeFilter.Group<AnimeFilter.TriState>(name, values)
class TriFilter(name: String) : AnimeFilter.TriState(name)
private inline fun <reified R> AnimeFilterList.getFirst(): R { private inline fun <reified R> AnimeFilterList.getFirst(): R {
return this.filterIsInstance<R>().first() return this.filterIsInstance<R>().first()
} }
@ -30,20 +26,6 @@ object AnimeSamaFilters {
} }
} }
private inline fun <reified R> AnimeFilterList.parseTriFilter(
options: Array<Pair<String, String>>,
): List<List<String>> {
return (this.getFirst<R>() as TriStateFilterList).state
.filterNot { it.isIgnored() }
.map { filter -> filter.state to filter.name }
.groupBy { it.first }
.let {
val included = it.get(AnimeFilter.TriState.STATE_INCLUDE)?.map { options.find { o -> o.first == it.second }!!.second } ?: emptyList()
val excluded = it.get(AnimeFilter.TriState.STATE_EXCLUDE)?.map { options.find { o -> o.first == it.second }!!.second } ?: emptyList()
listOf(included, excluded)
}
}
class TypesFilter : CheckBoxFilterList( class TypesFilter : CheckBoxFilterList(
"Type", "Type",
AnimeSamaFiltersData.TYPES.map { CheckBoxVal(it.first, false) }, AnimeSamaFiltersData.TYPES.map { CheckBoxVal(it.first, false) },
@ -54,9 +36,9 @@ object AnimeSamaFilters {
AnimeSamaFiltersData.LANGUAGES.map { CheckBoxVal(it.first, false) }, AnimeSamaFiltersData.LANGUAGES.map { CheckBoxVal(it.first, false) },
) )
class GenresFilter : TriStateFilterList( class GenresFilter : CheckBoxFilterList(
"Genre", "Genre",
AnimeSamaFiltersData.GENRES.map { TriFilter(it.first) }, AnimeSamaFiltersData.GENRES.map { CheckBoxVal(it.first, false) },
) )
val FILTER_LIST get() = AnimeFilterList( val FILTER_LIST get() = AnimeFilterList(
@ -68,19 +50,15 @@ object AnimeSamaFilters {
data class SearchFilters( data class SearchFilters(
val types: List<String> = emptyList(), val types: List<String> = emptyList(),
val language: List<String> = emptyList(), val language: List<String> = emptyList(),
val include: List<String> = emptyList(), val genres: List<String> = emptyList(),
val exclude: List<String> = emptyList(),
) )
fun getSearchFilters(filters: AnimeFilterList): SearchFilters { fun getSearchFilters(filters: AnimeFilterList): SearchFilters {
if (filters.isEmpty()) return SearchFilters() if (filters.isEmpty()) return SearchFilters()
val (include, exclude) = filters.parseTriFilter<GenresFilter>(AnimeSamaFiltersData.GENRES)
return SearchFilters( return SearchFilters(
filters.parseCheckbox<TypesFilter>(AnimeSamaFiltersData.TYPES), filters.parseCheckbox<TypesFilter>(AnimeSamaFiltersData.TYPES),
filters.parseCheckbox<LangFilter>(AnimeSamaFiltersData.LANGUAGES), filters.parseCheckbox<LangFilter>(AnimeSamaFiltersData.LANGUAGES),
include, filters.parseCheckbox<GenresFilter>(AnimeSamaFiltersData.GENRES),
exclude,
) )
} }
@ -94,31 +72,111 @@ object AnimeSamaFilters {
val LANGUAGES = arrayOf( val LANGUAGES = arrayOf(
Pair("VF", "VF"), Pair("VF", "VF"),
Pair("VOSTFR", "VOSTFR"), Pair("VOSTFR", "VOSTFR"),
Pair("VASTFR", "VASTFR"),
) )
val GENRES = arrayOf( val GENRES = arrayOf(
Pair("Action", "Action"), Pair("Action", "Action"),
Pair("Adolescence", "Adolescence"),
Pair("Aliens / Extra-terrestres", "Aliens / Extra-terrestres"),
Pair("Amitié", "Amitié"),
Pair("Amour", "Amour"),
Pair("Apocalypse", "Apocalypse"),
Pair("Art", "Art"),
Pair("Arts martiaux", "Arts martiaux"),
Pair("Assassinat", "Assassinat"),
Pair("Autre monde", "Autre monde"),
Pair("Aventure", "Aventure"), Pair("Aventure", "Aventure"),
Pair("Combats", "Combats"), Pair("Combats", "Combats"),
Pair("Comédie", "Comédie"), Pair("Comédie", "Comédie"),
Pair("Crime", "Crime"),
Pair("Cyberpunk", "Cyberpunk"),
Pair("Danse", "Danse"),
Pair("Démons", "Démons"),
Pair("Détective", "Détective"),
Pair("Donghua", "Donghua"),
Pair("Drame", "Drame"), Pair("Drame", "Drame"),
Pair("Ecchi", "Ecchi"), Pair("Ecchi", "Ecchi"),
Pair("École", "School-Life"), Pair("Ecole", "Ecole"),
Pair("Fantaisie", "Fantasy"), Pair("Enquête", "Enquête"),
Pair("Famille", "Famille"),
Pair("Fantastique", "Fantastique"),
Pair("Fantasy", "Fantasy"),
Pair("Fantômes", "Fantômes"),
Pair("Futur", "Futur"),
Pair("Ghibli", "Ghibli"),
Pair("Guerre", "Guerre"),
Pair("Harcèlement", "Harcèlement"),
Pair("Harem", "Harem"),
Pair("Harem inversé", "Harem inversé"),
Pair("Histoire", "Histoire"),
Pair("Historique", "Historique"),
Pair("Horreur", "Horreur"), Pair("Horreur", "Horreur"),
Pair("Isekai", "Isekai"), Pair("Isekai", "Isekai"),
Pair("Jeunesse", "Jeunesse"),
Pair("Jeux", "Jeux"),
Pair("Jeux vidéo", "Jeux vidéo"),
Pair("Josei", "Josei"), Pair("Josei", "Josei"),
Pair("Journalisme", "Journalisme"),
Pair("Mafia", "Mafia"),
Pair("Magical girl", "Magical girl"),
Pair("Magie", "Magie"),
Pair("Maladie", "Maladie"),
Pair("Mariage", "Mariage"),
Pair("Mature", "Mature"),
Pair("Mechas", "Mechas"),
Pair("Médiéval", "Médiéval"),
Pair("Militaire", "Militaire"),
Pair("Monde virtuel", "Monde virtuel"),
Pair("Monstres", "Monstres"),
Pair("Musique", "Musique"),
Pair("Mystère", "Mystère"), Pair("Mystère", "Mystère"),
Pair("Nekketsu", "Nekketsu"),
Pair("Ninjas", "Ninjas"),
Pair("Nostalgie", "Nostalgie"),
Pair("Paranormal", "Paranormal"),
Pair("Philosophie", "Philosophie"),
Pair("Pirates", "Pirates"),
Pair("Police", "Police"),
Pair("Politique", "Politique"),
Pair("Post-apocalyptique", "Post-apocalyptique"),
Pair("Pouvoirs psychiques", "Pouvoirs psychiques"),
Pair("Préhistoire", "Préhistoire"),
Pair("Prison", "Prison"),
Pair("Psychologique", "Psychologique"), Pair("Psychologique", "Psychologique"),
Pair("Quotidien", "Slice-of-Life"), Pair("Quotidien", "Quotidien"),
Pair("Religion", "Religion"),
Pair("Réincarnation / Transmigration", "Réincarnation / Transmigration"),
Pair("Romance", "Romance"), Pair("Romance", "Romance"),
Pair("Samouraïs", "Samouraïs"),
Pair("School Life", "School Life"),
Pair("Science-Fantasy", "Science-Fantasy"),
Pair("Science-fiction", "Science-fiction"),
Pair("Scientifique", "Scientifique"),
Pair("Seinen", "Seinen"), Pair("Seinen", "Seinen"),
Pair("Shônen", "Shônen"),
Pair("Shôjo", "Shôjo"), Pair("Shôjo", "Shôjo"),
Pair("Sports", "Sports"), Pair("Shônen", "Shônen"),
Pair("Shônen-Ai", "Shônen-Ai"),
Pair("Slice of Life", "Slice of Life"),
Pair("Société", "Société"),
Pair("Sport", "Sport"),
Pair("Super pouvoirs", "Super pouvoirs"),
Pair("Super-héros", "Super-héros"),
Pair("Surnaturel", "Surnaturel"), Pair("Surnaturel", "Surnaturel"),
Pair("Survie", "Survie"),
Pair("Survival game", "Survival game"),
Pair("Technologies", "Technologies"),
Pair("Thriller", "Thriller"),
Pair("Tournois", "Tournois"), Pair("Tournois", "Tournois"),
Pair("Travail", "Travail"),
Pair("Vampires", "Vampires"),
Pair("Vengeance", "Vengeance"),
Pair("Voyage", "Voyage"),
Pair("Voyage temporel", "Voyage temporel"),
Pair("Webcomic", "Webcomic"),
Pair("Yakuza", "Yakuza"),
Pair("Yaoi", "Yaoi"), Pair("Yaoi", "Yaoi"),
Pair("Yokai", "Yokai"),
Pair("Yuri", "Yuri"), Pair("Yuri", "Yuri"),
) )
} }

View file

@ -1,7 +1,7 @@
ext { ext {
extName = 'AniSama' extName = 'AniSama'
extClass = '.AniSama' extClass = '.AniSama'
extVersionCode = 13 extVersionCode = 15
} }
apply from: "$rootDir/common.gradle" apply from: "$rootDir/common.gradle"

View file

@ -1,7 +1,7 @@
ext { ext {
extName = 'EmpireStreaming' extName = 'EmpireStreaming'
extClass = '.EmpireStreaming' extClass = '.EmpireStreaming'
extVersionCode = 19 extVersionCode = 21
} }
apply from: "$rootDir/common.gradle" apply from: "$rootDir/common.gradle"

View file

@ -3,7 +3,7 @@ ext {
extClass = '.FrenchAnime' extClass = '.FrenchAnime'
themePkg = 'datalifeengine' themePkg = 'datalifeengine'
baseUrl = 'https://french-anime.com' baseUrl = 'https://french-anime.com'
overrideVersionCode = 18 overrideVersionCode = 20
} }
apply from: "$rootDir/common.gradle" apply from: "$rootDir/common.gradle"

View file

@ -1,7 +1,7 @@
ext { ext {
extName = 'OtakuFR' extName = 'OtakuFR'
extClass = '.OtakuFR' extClass = '.OtakuFR'
extVersionCode = 27 extVersionCode = 29
} }
apply from: "$rootDir/common.gradle" apply from: "$rootDir/common.gradle"

View file

@ -1,7 +1,7 @@
ext { ext {
extName = 'Vostfree' extName = 'Vostfree'
extClass = '.Vostfree' extClass = '.Vostfree'
extVersionCode = 27 extVersionCode = 29
} }
apply from: "$rootDir/common.gradle" apply from: "$rootDir/common.gradle"

View file

@ -3,7 +3,7 @@ ext {
extClass = '.Wiflix' extClass = '.Wiflix'
themePkg = 'datalifeengine' themePkg = 'datalifeengine'
baseUrl = 'https://wiflix.voto' baseUrl = 'https://wiflix.voto'
overrideVersionCode = 16 overrideVersionCode = 18
} }
apply from: "$rootDir/common.gradle" apply from: "$rootDir/common.gradle"

View file

@ -1,7 +1,7 @@
ext { ext {
extName = 'StreamingCommunity' extName = 'StreamingCommunity'
extClass = '.StreamingCommunity' extClass = '.StreamingCommunity'
extVersionCode = 6 extVersionCode = 7
} }
apply from: "$rootDir/common.gradle" apply from: "$rootDir/common.gradle"

View file

@ -30,7 +30,7 @@ class StreamingCommunity : ConfigurableAnimeSource, AnimeHttpSource() {
override val name = "StreamingCommunity" override val name = "StreamingCommunity"
override val baseUrl = "https://streamingcommunity.spa" override val baseUrl = "https://streamingcommunity.ovh"
override val lang = "it" override val lang = "it"

View file

@ -1,7 +1,7 @@
ext { ext {
extName = 'Toonitalia' extName = 'Toonitalia'
extClass = '.Toonitalia' extClass = '.Toonitalia'
extVersionCode = 26 extVersionCode = 28
} }
apply from: "$rootDir/common.gradle" apply from: "$rootDir/common.gradle"

View file

@ -1,9 +1,10 @@
ext { ext {
extKmkVersionCode = 1
extName = 'Q1N' extName = 'Q1N'
extClass = '.Q1N' extClass = '.Q1N'
themePkg = 'dooplay' themePkg = 'dooplay'
baseUrl = 'https://q1n.net' baseUrl = 'https://q1n.net'
overrideVersionCode = 20 overrideVersionCode = 21
} }
apply from: "$rootDir/common.gradle" apply from: "$rootDir/common.gradle"
@ -13,5 +14,6 @@ dependencies {
implementation(project(":lib:filemoon-extractor")) implementation(project(":lib:filemoon-extractor"))
implementation(project(":lib:streamwish-extractor")) implementation(project(":lib:streamwish-extractor"))
implementation(project(":lib:mixdrop-extractor")) implementation(project(":lib:mixdrop-extractor"))
implementation(project(":lib:playlist-utils"))
implementation(project(":lib:streamtape-extractor")) implementation(project(":lib:streamtape-extractor"))
} }

View file

@ -1,7 +1,9 @@
package eu.kanade.tachiyomi.animeextension.pt.animesgratis package eu.kanade.tachiyomi.animeextension.pt.animesgratis
import android.util.Log
import eu.kanade.tachiyomi.animeextension.pt.animesgratis.extractors.NoaExtractor import eu.kanade.tachiyomi.animeextension.pt.animesgratis.extractors.NoaExtractor
import eu.kanade.tachiyomi.animeextension.pt.animesgratis.extractors.RuplayExtractor import eu.kanade.tachiyomi.animeextension.pt.animesgratis.extractors.RuplayExtractor
import eu.kanade.tachiyomi.animeextension.pt.animesgratis.extractors.UniversalExtractor
import eu.kanade.tachiyomi.animesource.model.SAnime import eu.kanade.tachiyomi.animesource.model.SAnime
import eu.kanade.tachiyomi.animesource.model.SEpisode import eu.kanade.tachiyomi.animesource.model.SEpisode
import eu.kanade.tachiyomi.animesource.model.Video import eu.kanade.tachiyomi.animesource.model.Video
@ -27,6 +29,8 @@ class Q1N : DooPlay(
"https://q1n.net", "https://q1n.net",
) { ) {
private val tag by lazy { javaClass.simpleName }
override val id: Long = 2969482460524685571L override val id: Long = 2969482460524685571L
override val dateFormatter by lazy { override val dateFormatter by lazy {
@ -118,10 +122,12 @@ class Q1N : DooPlay(
private val streamTapeExtractor by lazy { StreamTapeExtractor(client) } private val streamTapeExtractor by lazy { StreamTapeExtractor(client) }
private val streamWishExtractor by lazy { StreamWishExtractor(client, headers) } private val streamWishExtractor by lazy { StreamWishExtractor(client, headers) }
private val mixDropExtractor by lazy { MixDropExtractor(client) } private val mixDropExtractor by lazy { MixDropExtractor(client) }
private val universalExtractor by lazy { UniversalExtractor(client) }
private fun getPlayerVideos(player: Element): List<Video> { private fun getPlayerVideos(player: Element): List<Video> {
val name = player.selectFirst("span.title")!!.text().lowercase() val name = player.selectFirst("span.title")!!.text().lowercase()
val url = getPlayerUrl(player) ?: return emptyList() val url = getPlayerUrl(player) ?: return emptyList()
Log.d(tag, "Fetching videos from: $url")
return when { return when {
"ruplay" in name -> ruplayExtractor.videosFromUrl(url) "ruplay" in name -> ruplayExtractor.videosFromUrl(url)
"streamwish" in name -> streamWishExtractor.videosFromUrl(url) "streamwish" in name -> streamWishExtractor.videosFromUrl(url)
@ -131,7 +137,7 @@ class Q1N : DooPlay(
"noa" in name -> noaExtractor.videosFromUrl(url) "noa" in name -> noaExtractor.videosFromUrl(url)
"mdplayer" in name -> noaExtractor.videosFromUrl(url, "MDPLAYER") "mdplayer" in name -> noaExtractor.videosFromUrl(url, "MDPLAYER")
"/player/" in url -> bloggerExtractor.videosFromUrl(url, headers) "/player/" in url -> bloggerExtractor.videosFromUrl(url, headers)
else -> emptyList() else -> universalExtractor.videosFromUrl(url, headers)
} }
} }

View file

@ -0,0 +1,128 @@
package eu.kanade.tachiyomi.animeextension.pt.animesgratis.extractors
import android.annotation.SuppressLint
import android.app.Application
import android.os.Handler
import android.os.Looper
import android.util.Log
import android.webkit.WebResourceRequest
import android.webkit.WebResourceResponse
import android.webkit.WebView
import android.webkit.WebViewClient
import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.lib.playlistutils.PlaylistUtils
import okhttp3.Headers
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.OkHttpClient
import uy.kohesive.injekt.injectLazy
import java.util.Locale
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
class UniversalExtractor(private val client: OkHttpClient) {
private val tag by lazy { javaClass.simpleName }
private val context: Application by injectLazy()
private val handler by lazy { Handler(Looper.getMainLooper()) }
@SuppressLint("SetJavaScriptEnabled")
fun videosFromUrl(origRequestUrl: String, origRequestHeader: Headers): List<Video> {
Log.d(tag, "Fetching videos from: $origRequestUrl")
val host = origRequestUrl.toHttpUrl().host.substringBefore(".").proper()
val latch = CountDownLatch(1)
var webView: WebView? = null
var resultUrl = ""
val playlistUtils by lazy { PlaylistUtils(client, origRequestHeader) }
val headers = origRequestHeader.toMultimap().mapValues { it.value.getOrNull(0) ?: "" }.toMutableMap()
handler.post {
val newView = WebView(context)
webView = newView
with(newView.settings) {
javaScriptEnabled = true
domStorageEnabled = true
databaseEnabled = true
useWideViewPort = false
loadWithOverviewMode = false
userAgentString = origRequestHeader["User-Agent"]
}
newView.webViewClient = object : WebViewClient() {
override fun onPageFinished(view: WebView?, url: String?) {
Log.d(tag, "Page loaded, injecting script")
view?.evaluateJavascript(CHECK_SCRIPT) {}
}
override fun shouldInterceptRequest(
view: WebView,
request: WebResourceRequest,
): WebResourceResponse? {
val url = request.url.toString()
Log.d(tag, "Intercepted URL: $url")
if (VIDEO_REGEX.containsMatchIn(url)) {
resultUrl = url
latch.countDown()
}
return super.shouldInterceptRequest(view, request)
}
}
webView?.loadUrl("$origRequestUrl&dl=1", headers)
}
latch.await(TIMEOUT_SEC, TimeUnit.SECONDS)
handler.post {
webView?.stopLoading()
webView?.destroy()
webView = null
}
return when {
"m3u8" in resultUrl -> {
Log.d(tag, "m3u8 URL: $resultUrl")
playlistUtils.extractFromHls(resultUrl, origRequestUrl, videoNameGen = { "$host: $it" })
}
"mpd" in resultUrl -> {
Log.d(tag, "mpd URL: $resultUrl")
playlistUtils.extractFromDash(resultUrl, { it -> "$host: $it" }, referer = origRequestUrl)
}
"mp4" in resultUrl -> {
Log.d(tag, "mp4 URL: $resultUrl")
Video(resultUrl, "$host: mp4", resultUrl, Headers.headersOf("referer", origRequestUrl)).let(::listOf)
}
else -> emptyList()
}
}
private fun String.proper(): String {
return this.replaceFirstChar {
if (it.isLowerCase()) {
it.titlecase(
Locale.getDefault(),
)
} else it.toString()
}
}
companion object {
const val TIMEOUT_SEC: Long = 10
private val VIDEO_REGEX by lazy { Regex(".*\\.(mp4|m3u8|mpd)(\\?.*)?$") }
private val CHECK_SCRIPT by lazy {
"""
setInterval(() => {
var playButton = document.getElementById('player-button-container')
if (playButton) {
playButton.click()
}
var downloadButton = document.querySelector(".downloader-button")
if (downloadButton) {
if (downloadButton.href) {
location.href = downloadButton.href
} else {
downloadButton.click()
}
}
}, 2500)
""".trimIndent()
}
}
}

View file

@ -1,7 +1,7 @@
ext { ext {
extName = 'Animeler' extName = 'Animeler'
extClass = '.Animeler' extClass = '.Animeler'
extVersionCode = 18 extVersionCode = 20
} }
apply from: "$rootDir/common.gradle" apply from: "$rootDir/common.gradle"

View file

@ -1,7 +1,7 @@
ext { ext {
extName = 'Anizm' extName = 'Anizm'
extClass = '.Anizm' extClass = '.Anizm'
extVersionCode = 27 extVersionCode = 29
} }
apply from: "$rootDir/common.gradle" apply from: "$rootDir/common.gradle"

View file

@ -1,7 +1,7 @@
ext { ext {
extName = 'TR Anime Izle' extName = 'TR Anime Izle'
extClass = '.TRAnimeIzle' extClass = '.TRAnimeIzle'
extVersionCode = 23 extVersionCode = 25
} }
apply from: "$rootDir/common.gradle" apply from: "$rootDir/common.gradle"

View file

@ -1,7 +1,7 @@
ext { ext {
extName = 'Türk Anime TV' extName = 'Türk Anime TV'
extClass = '.TurkAnime' extClass = '.TurkAnime'
extVersionCode = 36 extVersionCode = 38
} }
apply from: "$rootDir/common.gradle" apply from: "$rootDir/common.gradle"

View file

@ -1,7 +1,7 @@
ext { ext {
extName = 'Hanime1' extName = 'Hanime1'
extClass = '.Hanime1' extClass = '.Hanime1'
extVersionCode = 4 extVersionCode = 5
isNsfw = true isNsfw = true
} }

View file

@ -2,6 +2,7 @@ package eu.kanade.tachiyomi.animeextension.zh.hanime1
import android.app.Application import android.app.Application
import android.content.SharedPreferences import android.content.SharedPreferences
import android.util.Log
import androidx.preference.ListPreference import androidx.preference.ListPreference
import androidx.preference.PreferenceScreen import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource
@ -16,12 +17,15 @@ import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.awaitSuccess import eu.kanade.tachiyomi.network.awaitSuccess
import eu.kanade.tachiyomi.util.asJsoup import eu.kanade.tachiyomi.util.asJsoup
import kotlinx.coroutines.CoroutineExceptionHandler import kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.serialization.encodeToString import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonElement import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.jsonArray
import kotlinx.serialization.json.jsonObject import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive import kotlinx.serialization.json.jsonPrimitive
import okhttp3.Cookie import okhttp3.Cookie
@ -65,14 +69,32 @@ class Hanime1 : AnimeHttpSource(), ConfigurableAnimeSource {
} }
override fun animeDetailsParse(response: Response): SAnime { override fun animeDetailsParse(response: Response): SAnime {
val jsoup = response.asJsoup() val doc = response.asJsoup()
return SAnime.create().apply { return SAnime.create().apply {
genre = jsoup.select(".single-video-tag").not("[data-toggle]").eachText().joinToString() genre = doc.select(".single-video-tag").not("[data-toggle]").eachText().joinToString()
author = jsoup.select("#video-artist-name").text() author = doc.select("#video-artist-name").text()
jsoup.select("script[type=application/ld+json]").first()?.data()?.let { doc.select("script[type=application/ld+json]").first()?.data()?.let {
val info = json.decodeFromString<JsonElement>(it).jsonObject val info = json.decodeFromString<JsonElement>(it).jsonObject
title = info["name"]!!.jsonPrimitive.content title = info["name"]!!.jsonPrimitive.content
description = info["description"]!!.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 +159,7 @@ class Hanime1 : AnimeHttpSource(), ConfigurableAnimeSource {
override fun searchAnimeParse(response: Response): AnimesPage { override fun searchAnimeParse(response: Response): AnimesPage {
val jsoup = response.asJsoup() 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()) { val list = if (nodes.isNotEmpty()) {
nodes.map { nodes.map {
SAnime.create().apply { SAnime.create().apply {
@ -216,11 +238,12 @@ class Hanime1 : AnimeHttpSource(), ConfigurableAnimeSource {
return chain.proceed(chain.request()) return chain.proceed(chain.request())
} }
@OptIn(DelicateCoroutinesApi::class)
private fun updateFilters() { private fun updateFilters() {
filterUpdateState = FilterUpdateState.UPDATING filterUpdateState = FilterUpdateState.UPDATING
val exceptionHandler = val exceptionHandler =
CoroutineExceptionHandler { _, _ -> filterUpdateState = FilterUpdateState.FAILED } CoroutineExceptionHandler { _, _ -> filterUpdateState = FilterUpdateState.FAILED }
CoroutineScope(Dispatchers.IO + exceptionHandler).launch { GlobalScope.launch(Dispatchers.IO + exceptionHandler) {
val jsoup = client.newCall(GET("$baseUrl/search")).awaitSuccess().asJsoup() val jsoup = client.newCall(GET("$baseUrl/search")).awaitSuccess().asJsoup()
val genreList = jsoup.select("div.genre-option div.hentai-sort-options").eachText() val genreList = jsoup.select("div.genre-option div.hentai-sort-options").eachText()
val sortList = val sortList =

View file

@ -1,7 +1,7 @@
ext { ext {
extName = 'Xfani' extName = 'Xfani'
extClass = '.Xfani' extClass = '.Xfani'
extVersionCode = 5 extVersionCode = 6
} }
apply from: "$rootDir/common.gradle" apply from: "$rootDir/common.gradle"

View file

@ -20,8 +20,9 @@ import eu.kanade.tachiyomi.network.POST
import eu.kanade.tachiyomi.network.awaitSuccess import eu.kanade.tachiyomi.network.awaitSuccess
import eu.kanade.tachiyomi.util.asJsoup import eu.kanade.tachiyomi.util.asJsoup
import kotlinx.coroutines.CoroutineExceptionHandler import kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import kotlinx.serialization.json.jsonObject import kotlinx.serialization.json.jsonObject
@ -53,7 +54,7 @@ enum class FilterUpdateState {
class Xfani : AnimeHttpSource(), ConfigurableAnimeSource { class Xfani : AnimeHttpSource(), ConfigurableAnimeSource {
override val baseUrl: String override val baseUrl: String
get() = "https://dick.xfani.com" get() = "https://dm.xifanacg.com"
override val lang: String override val lang: String
get() = "zh" get() = "zh"
override val name: String override val name: String
@ -117,10 +118,16 @@ class Xfani : AnimeHttpSource(), ConfigurableAnimeSource {
} }
override fun animeDetailsParse(response: Response): SAnime { override fun animeDetailsParse(response: Response): SAnime {
val jsoup = response.asJsoup() val doc = response.asJsoup()
return SAnime.create().apply { return SAnime.create().apply {
description = jsoup.select("#height_limit.text").text() description = doc.select("#height_limit.text").text()
title = jsoup.select(".slide-info-title").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 +241,13 @@ class Xfani : AnimeHttpSource(), ConfigurableAnimeSource {
return vodListToAnimePageList(response) return vodListToAnimePageList(response)
} }
val jsoup = response.asJsoup() 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 -> val animeList = items.map { item ->
SAnime.create().apply { SAnime.create().apply {
title = item.select(".thumb-txt").text() title = item.select("div.detail-info > a").text()
url = item.select("div.left.public-list-bj a.public-list-exp").attr("href") url = item.select("div.detail-info > a").attr("href")
thumbnail_url = thumbnail_url =
item.select("div.left.public-list-bj img[data-src]").attr("data-src") item.select("div.detail-pic 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 }
} }
} }
val tip = jsoup.select("div.pages div.page-tip").text() val tip = jsoup.select("div.pages div.page-tip").text()
@ -259,12 +260,13 @@ class Xfani : AnimeHttpSource(), ConfigurableAnimeSource {
return numbers.size == 2 && numbers[0] != numbers[1] return numbers.size == 2 && numbers[0] != numbers[1]
} }
@OptIn(DelicateCoroutinesApi::class)
private fun updateFilter() { private fun updateFilter() {
filterState = FilterUpdateState.UPDATING filterState = FilterUpdateState.UPDATING
val handler = CoroutineExceptionHandler { _, _ -> val handler = CoroutineExceptionHandler { _, _ ->
filterState = FilterUpdateState.FAILED filterState = FilterUpdateState.FAILED
} }
CoroutineScope(Dispatchers.IO + handler).launch { GlobalScope.launch(Dispatchers.IO + handler) {
val jsoup = client.newCall(GET("$baseUrl/show/1/html")).awaitSuccess().asJsoup() val jsoup = client.newCall(GET("$baseUrl/show/1/html")).awaitSuccess().asJsoup()
// update class and year filter type // update class and year filter type
val classList = jsoup.select("li[data-type=class]").eachAttr("data-val") 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 PREF_KEY_FILTER_YEAR = "PREF_KEY_FILTER_YEAR"
const val DEFAULT_VIDEO_SOURCE = "0" const val DEFAULT_VIDEO_SOURCE = "0"
val STATUS_STR_MAPPING = mapOf(
"已完结" to SAnime.COMPLETED,
)
} }
} }