Compare commits

..

1 commit

Author SHA1 Message Date
Sphereso
d4d56a94a2 Voe: adapt to changes in obfuscation (Kohi-den#959) 2025-04-29 14:59:58 +02:00
204 changed files with 734 additions and 22570 deletions

View file

@ -10,7 +10,7 @@ on:
jobs: jobs:
stale: stale:
runs-on: docker runs-on: ubuntu-latest
steps: steps:
- uses: actions/stale@28ca1036281a5e5922ead5184a1bbf96e5fc984e # v9 - uses: actions/stale@28ca1036281a5e5922ead5184a1bbf96e5fc984e # v9
with: with:

View file

@ -18,12 +18,15 @@ env:
jobs: jobs:
prepare: prepare:
name: Prepare job name: Prepare job
runs-on: docker runs-on: ubuntu-latest
outputs: outputs:
individualMatrix: ${{ steps.generate-matrices.outputs.individualMatrix }} individualMatrix: ${{ steps.generate-matrices.outputs.individualMatrix }}
steps: steps:
- name: Clone repo - name: Clone repo
uses: https://github.com/actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4 uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4
- name: Validate Gradle Wrapper
uses: gradle/wrapper-validation-action@a494d935f4b56874c4a5a87d19af7afcf3a163d0 # v2
- name: Get number of modules - name: Get number of modules
run: | run: |
@ -34,7 +37,7 @@ jobs:
- id: generate-matrices - id: generate-matrices
name: Create output matrices name: Create output matrices
uses: https://github.com/actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7 uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7
with: with:
script: | script: |
const numIndividualModules = process.env.NUM_INDIVIDUAL_MODULES; const numIndividualModules = process.env.NUM_INDIVIDUAL_MODULES;
@ -49,21 +52,21 @@ jobs:
build_individual: build_individual:
name: Build individual modules name: Build individual modules
needs: prepare needs: prepare
runs-on: docker runs-on: ubuntu-latest
strategy: strategy:
matrix: ${{ fromJSON(needs.prepare.outputs.individualMatrix) }} matrix: ${{ fromJSON(needs.prepare.outputs.individualMatrix) }}
steps: steps:
- name: Checkout PR - name: Checkout PR
uses: https://github.com/actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4 uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4
- name: Set up JDK - name: Set up JDK
uses: https://github.com/actions/setup-java@387ac29b308b003ca37ba93a6cab5eb57c8f5f93 # v4 uses: actions/setup-java@387ac29b308b003ca37ba93a6cab5eb57c8f5f93 # v4
with: with:
java-version: 17 java-version: 17
distribution: temurin distribution: temurin
- name: Set up Gradle - name: Set up Gradle
uses: https://github.com/gradle/actions/setup-gradle@417ae3ccd767c252f5661f1ace9f835f9654f2b5 # v3 uses: gradle/actions/setup-gradle@417ae3ccd767c252f5661f1ace9f835f9654f2b5 # v3
with: with:
cache-read-only: true cache-read-only: true

View file

@ -21,23 +21,16 @@ env:
jobs: jobs:
prepare: prepare:
name: Prepare job name: Prepare job
runs-on: ubuntu-22.04 runs-on: ubuntu-latest
outputs: outputs:
individualMatrix: ${{ steps.generate-matrices.outputs.individualMatrix }} individualMatrix: ${{ steps.generate-matrices.outputs.individualMatrix }}
steps: steps:
- name: Clone repo - name: Clone repo
uses: https://github.com/actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4 uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4
with: with:
ref: main ref: main
token: ${{ secrets.BOT_PAT }} token: ${{ secrets.BOT_PAT }}
- name: Get number of modules
run: |
set -x
projects=(src/*/*)
export CI_CHUNK_NUM=${#projects[@]}
echo "NUM_INDIVIDUAL_MODULES=${#projects[@]}" >> $GITHUB_ENV
# Temporary pause because of leak of tj-actions/changed-files # Temporary pause because of leak of tj-actions/changed-files
# - name: Find lib changes # - name: Find lib changes
# id: modified-libs # id: modified-libs
@ -49,7 +42,7 @@ jobs:
# 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: crazy-max/ghaction-import-gpg@v6 # v6.1.0
with: with:
gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }} gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }}
passphrase: ${{ secrets.GPG_PASSPHRASE }} passphrase: ${{ secrets.GPG_PASSPHRASE }}
@ -63,9 +56,19 @@ jobs:
# 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 }}
- name: Validate Gradle Wrapper
uses: gradle/actions/wrapper-validation@v4
- name: Get number of modules
run: |
set -x
projects=(src/*/*)
echo "NUM_INDIVIDUAL_MODULES=${#projects[@]}" >> $GITHUB_ENV
- id: generate-matrices - id: generate-matrices
name: Create output matrices name: Create output matrices
uses: https://github.com/actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7 uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7
with: with:
script: | script: |
const numIndividualModules = process.env.NUM_INDIVIDUAL_MODULES; const numIndividualModules = process.env.NUM_INDIVIDUAL_MODULES;
@ -80,17 +83,17 @@ jobs:
build_individual: build_individual:
name: Build individual modules name: Build individual modules
needs: prepare needs: prepare
runs-on: ubuntu-22.04 runs-on: ubuntu-latest
strategy: strategy:
matrix: ${{ fromJSON(needs.prepare.outputs.individualMatrix) }} matrix: ${{ fromJSON(needs.prepare.outputs.individualMatrix) }}
steps: steps:
- name: Checkout main branch - name: Checkout main branch
uses: https://github.com/actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4 uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4
with: with:
ref: main ref: main
- name: Set up JDK - name: Set up JDK
uses: https://github.com/actions/setup-java@387ac29b308b003ca37ba93a6cab5eb57c8f5f93 # v4 uses: actions/setup-java@387ac29b308b003ca37ba93a6cab5eb57c8f5f93 # v4
with: with:
java-version: 17 java-version: 17
distribution: temurin distribution: temurin
@ -100,7 +103,7 @@ jobs:
echo ${{ secrets.SIGNING_KEY }} | base64 -d > signingkey.jks echo ${{ secrets.SIGNING_KEY }} | base64 -d > signingkey.jks
- name: Set up Gradle - name: Set up Gradle
uses: https://github.com/gradle/actions/setup-gradle@245c8a24de79c0dbeabaf19ebcbbd3b2c36f278d # v4 uses: gradle/actions/setup-gradle@417ae3ccd767c252f5661f1ace9f835f9654f2b5 # v3
- name: Build extensions (chunk ${{ matrix.chunk }}) - name: Build extensions (chunk ${{ matrix.chunk }})
env: env:
@ -111,7 +114,7 @@ jobs:
run: chmod +x ./gradlew && ./gradlew -p src assembleRelease run: chmod +x ./gradlew && ./gradlew -p src assembleRelease
- name: Upload APKs (chunk ${{ matrix.chunk }}) - name: Upload APKs (chunk ${{ matrix.chunk }})
uses: https://github.com/actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4 uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4
if: "github.repository == 'Kohi-den/extensions-source'" if: "github.repository == 'Kohi-den/extensions-source'"
with: with:
name: "individual-apks-${{ matrix.chunk }}" name: "individual-apks-${{ matrix.chunk }}"
@ -125,22 +128,22 @@ jobs:
name: Publish repo name: Publish repo
needs: needs:
- build_individual - build_individual
if: "github.repository == 'AlmightyHak/extensions-source'" if: "github.repository == 'Kohi-den/extensions-source'"
runs-on: ubuntu-22.04 runs-on: ubuntu-latest
steps: steps:
- name: Download APK artifacts - name: Download APK artifacts
uses: https://github.com/actions/download-artifact@eaceaf801fd36c7dee90939fad912460b18a1ffe # v4 uses: actions/download-artifact@eaceaf801fd36c7dee90939fad912460b18a1ffe # v4
with: with:
path: ~/apk-artifacts path: ~/apk-artifacts
- name: Set up JDK - name: Set up JDK
uses: https://github.com/actions/setup-java@387ac29b308b003ca37ba93a6cab5eb57c8f5f93 # v4 uses: actions/setup-java@387ac29b308b003ca37ba93a6cab5eb57c8f5f93 # v4
with: with:
java-version: 17 java-version: 17
distribution: temurin distribution: temurin
- name: Checkout main branch - name: Checkout main branch
uses: https://github.com/actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4 uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4
with: with:
ref: main ref: main
path: main path: main
@ -155,9 +158,9 @@ jobs:
python ./.github/scripts/create-repo.py python ./.github/scripts/create-repo.py
- name: Checkout repo branch - name: Checkout repo branch
uses: https://github.com/actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4 uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4
with: with:
repository: AlmightyHak/extensions repository: Kohi-den/extensions
token: ${{ secrets.BOT_PAT }} token: ${{ secrets.BOT_PAT }}
ref: main ref: main
path: repo path: repo
@ -167,9 +170,9 @@ jobs:
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: Deploy repo - name: Deploy repo
uses: https://github.com/EndBug/add-and-commit@v9 uses: EndBug/add-and-commit@v9
with: with:
message: "Update extensions repo" message: "Update extensions repo"
cwd: "./repo" cwd: "./repo"
committer_name: AlmightyHak committer_name: Kohi-den-Bot
committer_email: almightyhak@noreply.localhost committer_email: 177773202+Kohi-den-Bot@users.noreply.github.com

View file

@ -8,7 +8,7 @@ on:
jobs: jobs:
autoclose: autoclose:
runs-on: docker runs-on: ubuntu-latest
steps: steps:
- name: Moderate issues - name: Moderate issues
uses: aniyomiorg/issue-moderator-action@v2 uses: aniyomiorg/issue-moderator-action@v2

View file

@ -10,7 +10,7 @@ on:
jobs: jobs:
lock: lock:
runs-on: docker runs-on: ubuntu-latest
permissions: permissions:
issues: write issues: write
steps: steps:

View file

@ -2,9 +2,9 @@
just paste this into your anime repo just paste this into your anime repo
``` ```
https://kohiden.xyz/AlmightyHak/extensions/raw/branch/main/index.min.json https://raw.githubusercontent.com/Kohi-den/extensions/main/index.min.json
``` ```
If your interested in installing just the apks they can be found [Here](https://kohiden.xyz/AlmightyHak/extensions/src/branch/main/apk) If your interested in installing just the apks they can be found [Here](https://github.com/Kohi-den/extensions)
## Support Server ## Support Server

File diff suppressed because it is too large Load diff

BIN
SDK/adb

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -1,2 +0,0 @@
24333f8a63b6825ea9c5514f83c2829b004d1fee

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -1,53 +0,0 @@
[defaults]
base_features = sparse_super,large_file,filetype,dir_index,ext_attr
default_mntopts = acl,user_xattr
enable_periodic_fsck = 0
blocksize = 4096
inode_size = 256
inode_ratio = 16384
reserved_ratio = 1.0
[fs_types]
ext3 = {
features = has_journal
}
ext4 = {
features = has_journal,extent,huge_file,dir_nlink,extra_isize,uninit_bg
inode_size = 256
}
ext4dev = {
features = has_journal,extent,huge_file,flex_bg,inline_data,64bit,dir_nlink,extra_isize
inode_size = 256
options = test_fs=1
}
small = {
blocksize = 1024
inode_size = 128
inode_ratio = 4096
}
floppy = {
blocksize = 1024
inode_size = 128
inode_ratio = 8192
}
big = {
inode_ratio = 32768
}
huge = {
inode_ratio = 65536
}
news = {
inode_ratio = 4096
}
largefile = {
inode_ratio = 1048576
blocksize = -1
}
largefile4 = {
inode_ratio = 4194304
blocksize = -1
}
hurd = {
blocksize = 4096
inode_size = 128
}

View file

@ -1,2 +0,0 @@
Pkg.UserSrc=false
Pkg.Revision=36.0.0

Binary file not shown.

View file

@ -2,4 +2,4 @@ plugins {
id("lib-multisrc") id("lib-multisrc")
} }
baseVersionCode = 3 baseVersionCode = 2

View file

@ -49,13 +49,13 @@ abstract class DooPlay(
const val PREFIX_SEARCH = "path:" const val PREFIX_SEARCH = "path:"
} }
protected open val prefQualityDefault = "1080p" protected open val prefQualityDefault = "720p"
protected open val prefQualityKey = "preferred_quality" protected open val prefQualityKey = "preferred_quality"
protected open val prefQualityTitle = when (lang) { protected open val prefQualityTitle = when (lang) {
"pt-BR" -> "Qualidade preferida" "pt-BR" -> "Qualidade preferida"
else -> "Preferred quality" else -> "Preferred quality"
} }
protected open val prefQualityValues = arrayOf("360p", "480p", "720p", "1080p") protected open val prefQualityValues = arrayOf("480p", "720p")
protected open val prefQualityEntries = prefQualityValues protected open val prefQualityEntries = prefQualityValues
protected open val videoSortPrefKey = prefQualityKey protected open val videoSortPrefKey = prefQualityKey

View file

@ -2,7 +2,7 @@ plugins {
id("lib-multisrc") id("lib-multisrc")
} }
baseVersionCode = 6 baseVersionCode = 5
dependencies { dependencies {
api(project(":lib:megacloud-extractor")) api(project(":lib:megacloud-extractor"))

View file

@ -1,3 +0,0 @@
plugins {
id("lib-android")
}

View file

@ -1,82 +0,0 @@
package eu.kanade.tachiyomi.lib.buzzheavierextractor
import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.util.asJsoup
import eu.kanade.tachiyomi.util.parseAs
import java.io.IOException
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.Serializable
import okhttp3.Headers
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
class BuzzheavierExtractor(
private val client: OkHttpClient,
private val headers: Headers,
) {
companion object {
private val SIZE_REGEX = Regex("""Size\s*-\s*([0-9.]+\s*[GMK]B)""")
}
@OptIn(ExperimentalSerializationApi::class)
fun videosFromUrl(url: String, prefix: String = "Buzzheavier - ", proxyUrl: String? = null): List<Video> {
val httpUrl = url.toHttpUrl()
val id = httpUrl.pathSegments.first()
val dlHeaders = headers.newBuilder().apply {
add("Accept", "*/*")
add("HX-Current-URL", url)
add("HX-Request", "true")
add("Priority", "u=1, i")
add("Referer", url)
}.build()
val videoHeaders = headers.newBuilder().apply {
add("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7")
add("Priority", "u=0, i")
add("Referer", url)
}.build()
val siteRequest = client.newCall(GET(url)).execute()
val parsedHtml = siteRequest.asJsoup()
val detailsText = parsedHtml.selectFirst("li:contains(Details:)")?.text() ?: ""
val size = SIZE_REGEX.find(detailsText)?.groupValues?.getOrNull(1)?.trim() ?: "Unknown"
val downloadRequest = GET("https://${httpUrl.host}/$id/download", dlHeaders)
val path = client.executeWithRetry(downloadRequest, 5, 204).use { response ->
response.header("hx-redirect").orEmpty()
}
val videoUrl = if (path.isNotEmpty()) {
if (path.startsWith("http")) path else "https://${httpUrl.host}$path"
} else if (proxyUrl?.isNotEmpty() == true) {
client.executeWithRetry(GET(proxyUrl + id), 5, 200).parseAs<UrlDto>().url
} else {
return emptyList()
}
return listOf(Video(videoUrl, "${prefix}${size}", videoUrl, videoHeaders))
}
private fun OkHttpClient.executeWithRetry(request: Request, maxRetries: Int, validCode: Int): Response {
var response: Response? = null
for (attempt in 0 until maxRetries) {
response?.close()
response = this.newCall(request).execute()
if (response.code == validCode) {
return response
}
if (attempt < maxRetries - 1) {
Thread.sleep(1000)
}
}
return response ?: throw IOException("Failed to execute request after $maxRetries attempts")
}
@Serializable
data class UrlDto(val url: String)
}

View file

@ -1,54 +1,73 @@
package eu.kanade.tachiyomi.lib.chillxextractor package eu.kanade.tachiyomi.lib.chillxextractor
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.playlistutils.PlaylistUtils import eu.kanade.tachiyomi.lib.playlistutils.PlaylistUtils
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.util.parseAs
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import okhttp3.Headers import okhttp3.Headers
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import uy.kohesive.injekt.injectLazy
class ChillxExtractor(private val client: OkHttpClient, private val headers: Headers) { class ChillxExtractor(private val client: OkHttpClient, private val headers: Headers) {
private val json: Json by injectLazy()
private val playlistUtils by lazy { PlaylistUtils(client, headers) } private val playlistUtils by lazy { PlaylistUtils(client, headers) }
private val webViewResolver by lazy { WebViewResolver(client, headers) }
companion object { companion object {
private val REGEX_MASTER_JS = Regex("""\s*=\s*'([^']+)""")
private val REGEX_SOURCES = Regex("""sources:\s*\[\{"file":"([^"]+)""") private val REGEX_SOURCES = Regex("""sources:\s*\[\{"file":"([^"]+)""")
private val REGEX_FILE = Regex("""file: ?"([^"]+)"""") private val REGEX_FILE = Regex("""file: ?"([^"]+)"""")
private val REGEX_SOURCE = Regex("""source = ?"([^"]+)"""") private val REGEX_SOURCE = Regex("""source = ?"([^"]+)"""")
private val REGEX_SUBS = Regex("""\{"file":"([^"]+)","label":"([^"]+)","kind":"captions","default":\w+\}""") private val REGEX_SUBS = Regex("""\{"file":"([^"]+)","label":"([^"]+)","kind":"captions","default":\w+\}""")
private const val KEY_SOURCE = "https://raw.githubusercontent.com/Rowdy-Avocado/multi-keys/keys/index.html"
} }
fun videoFromUrl(url: String, prefix: String = "Chillx - "): List<Video> { fun videoFromUrl(url: String, referer: String, prefix: String = "Chillx - "): List<Video> {
val data = webViewResolver.getDecryptedData(url) ?: return emptyList() val newHeaders = headers.newBuilder()
.set("Referer", "$referer/")
.set("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8")
.set("Accept-Language", "en-US,en;q=0.5")
.build()
val masterUrl = REGEX_SOURCES.find(data)?.groupValues?.get(1) val body = client.newCall(GET(url, newHeaders)).execute().body.string()
?: REGEX_FILE.find(data)?.groupValues?.get(1)
?: REGEX_SOURCE.find(data)?.groupValues?.get(1) val master = REGEX_MASTER_JS.find(body)?.groupValues?.get(1) ?: return emptyList()
val aesJson = json.decodeFromString<CryptoInfo>(master)
val key = fetchKey() ?: throw ErrorLoadingException("Unable to get key")
val decryptedScript = CryptoAES.decryptWithSalt(aesJson.ciphertext, aesJson.salt, key)
.replace("\\n", "\n")
.replace("\\", "")
val masterUrl = REGEX_SOURCES.find(decryptedScript)?.groupValues?.get(1)
?: REGEX_FILE.find(decryptedScript)?.groupValues?.get(1)
?: REGEX_SOURCE.find(decryptedScript)?.groupValues?.get(1)
?: return emptyList() ?: return emptyList()
val subtitleList = buildList { val subtitleList = buildList {
val subtitles = REGEX_SUBS.findAll(data) val subtitles = REGEX_SUBS.findAll(decryptedScript)
subtitles.forEach { subtitles.forEach {
Log.d("ChillxExtractor", "Found subtitle: ${it.groupValues}")
add(Track(it.groupValues[1], decodeUnicodeEscape(it.groupValues[2]))) add(Track(it.groupValues[1], decodeUnicodeEscape(it.groupValues[2])))
} }
} }
val videoList = playlistUtils.extractFromHls( return playlistUtils.extractFromHls(
playlistUrl = masterUrl, playlistUrl = masterUrl,
referer = url, referer = url,
videoNameGen = { "$prefix$it" }, videoNameGen = { "$prefix$it" },
subtitleList = subtitleList, subtitleList = subtitleList,
) )
return videoList.map {
Video(
url = it.url,
quality = it.quality,
videoUrl = it.videoUrl,
headers = it.headers,
audioTracks = it.audioTracks,
subtitleTracks = playlistUtils.fixSubtitles(it.subtitleTracks),
)
} }
@OptIn(ExperimentalSerializationApi::class)
private fun fetchKey(): String? {
return client.newCall(GET(KEY_SOURCE)).execute().parseAs<KeysData>().keys.firstOrNull()
} }
private fun decodeUnicodeEscape(input: String): String { private fun decodeUnicodeEscape(input: String): String {
@ -57,4 +76,16 @@ class ChillxExtractor(private val client: OkHttpClient, private val headers: Hea
it.groupValues[1].toInt(16).toChar().toString() it.groupValues[1].toInt(16).toChar().toString()
} }
} }
@Serializable
data class CryptoInfo(
@SerialName("ct") val ciphertext: String,
@SerialName("s") val salt: String,
)
@Serializable
data class KeysData(
@SerialName("chillx") val keys: List<String>
)
} }
class ErrorLoadingException(message: String) : Exception(message)

View file

@ -1,125 +0,0 @@
package eu.kanade.tachiyomi.lib.chillxextractor
import android.annotation.SuppressLint
import android.app.Application
import android.os.Handler
import android.os.Looper
import android.webkit.JavascriptInterface
import android.webkit.WebResourceRequest
import android.webkit.WebResourceResponse
import android.webkit.WebView
import android.webkit.WebViewClient
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.util.asJsoup
import okhttp3.Headers
import okhttp3.OkHttpClient
import uy.kohesive.injekt.injectLazy
import java.io.ByteArrayInputStream
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
class WebViewResolver(
private val client: OkHttpClient,
private val globalHeaders: Headers,
) {
private val context: Application by injectLazy()
private val handler by lazy { Handler(Looper.getMainLooper()) }
class JsInterface(private val latch: CountDownLatch) {
var result: String? = null
@JavascriptInterface
fun passPayload(payload: String) {
result = payload
latch.countDown()
}
}
@SuppressLint("SetJavaScriptEnabled")
fun getDecryptedData(embedUrl: String): String? {
val latch = CountDownLatch(1)
var webView: WebView? = null
val jsi = JsInterface(latch)
val interfaceName = randomString()
handler.post {
val webview = WebView(context)
webView = webview
with(webview.settings) {
javaScriptEnabled = true
domStorageEnabled = true
databaseEnabled = true
useWideViewPort = false
loadWithOverviewMode = false
userAgentString = globalHeaders["User-Agent"]
}
webview.addJavascriptInterface(jsi, interfaceName)
webview.webViewClient = object : WebViewClient() {
override fun shouldInterceptRequest(
view: WebView?,
request: WebResourceRequest?
): WebResourceResponse? {
if (request?.url.toString().equals(embedUrl, true)) {
return patchBody(request!!.url.toString(), interfaceName)
?: super.shouldInterceptRequest(view, request)
}
return super.shouldInterceptRequest(view, request)
}
}
webView?.loadUrl(embedUrl)
}
latch.await(TIMEOUT_SEC, TimeUnit.SECONDS)
handler.post {
webView?.stopLoading()
webView?.destroy()
webView = null
}
return jsi.result
}
companion object {
const val TIMEOUT_SEC: Long = 30
}
private fun randomString(length: Int = 10): String {
val charPool = ('a'..'z') + ('A'..'Z')
return List(length) { charPool.random() }.joinToString("")
}
private fun patchBody(url: String, interfaceName: String): WebResourceResponse? {
val html = client.newCall(GET(url, globalHeaders)).execute().asJsoup()
val oldFunc = randomString()
val script = html.createElement("script").apply {
appendText(
"""
const $oldFunc = Function;
window.Function = function (...args) {
if (args.length == 1) {
window.$interfaceName.passPayload(args[0]);
}
return $oldFunc(...args);
};
""".trimIndent()
)
}
html.body().insertChildren(0, script)
return WebResourceResponse(
"text/html",
"utf-8",
200,
"ok",
mapOf("server" to "cloudflare"),
ByteArrayInputStream(html.outerHtml().toByteArray()),
)
}
}

View file

@ -1,8 +1,5 @@
package eu.kanade.tachiyomi.lib.filemoonextractor package eu.kanade.tachiyomi.lib.filemoonextractor
import android.content.SharedPreferences
import androidx.preference.EditTextPreference
import androidx.preference.PreferenceScreen
import dev.datlag.jsunpacker.JsUnpacker import dev.datlag.jsunpacker.JsUnpacker
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
@ -16,28 +13,20 @@ import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
class FilemoonExtractor( class FilemoonExtractor(private val client: OkHttpClient) {
private val client: OkHttpClient,
private val preferences: SharedPreferences? = null,
) {
private val playlistUtils by lazy { PlaylistUtils(client) } private val playlistUtils by lazy { PlaylistUtils(client) }
private val json: Json by injectLazy() private val json: Json by injectLazy()
fun videosFromUrl(url: String, prefix: String = "Filemoon - ", headers: Headers? = null): List<Video> { fun videosFromUrl(url: String, prefix: String = "Filemoon - ", headers: Headers? = null): List<Video> {
var httpUrl = url.toHttpUrl() val httpUrl = url.toHttpUrl()
val videoHeaders = (headers?.newBuilder() ?: Headers.Builder()) val videoHeaders = (headers?.newBuilder() ?: Headers.Builder())
.set("Referer", url) .set("Referer", url)
.set("Origin", "https://${httpUrl.host}") .set("Origin", "https://${httpUrl.host}")
.build() .build()
val doc = client.newCall(GET(url, videoHeaders)).execute().asJsoup() val doc = client.newCall(GET(url, videoHeaders)).execute().asJsoup()
val jsEval = doc.selectFirst("script:containsData(eval):containsData(m3u8)")?.data() ?: run { val jsEval = doc.selectFirst("script:containsData(eval):containsData(m3u8)")!!.data()
val iframeUrl = doc.selectFirst("iframe[src]")!!.attr("src")
httpUrl = iframeUrl.toHttpUrl()
val iframeDoc = client.newCall(GET(iframeUrl, videoHeaders)).execute().asJsoup()
iframeDoc.selectFirst("script:containsData(eval):containsData(m3u8)")!!.data()
}
val unpacked = JsUnpacker.unpackAndCombine(jsEval).orEmpty() val unpacked = JsUnpacker.unpackAndCombine(jsEval).orEmpty()
val masterUrl = unpacked.takeIf(String::isNotBlank) val masterUrl = unpacked.takeIf(String::isNotBlank)
?.substringAfter("{file:\"", "") ?.substringAfter("{file:\"", "")
@ -61,39 +50,14 @@ class FilemoonExtractor(
} }
} }
val videoList = playlistUtils.extractFromHls( return playlistUtils.extractFromHls(
masterUrl, masterUrl,
subtitleList = subtitleTracks, subtitleList = subtitleTracks,
referer = "https://${httpUrl.host}/", referer = "https://${httpUrl.host}/",
videoNameGen = { "$prefix$it" }, videoNameGen = { "$prefix$it" },
) )
val subPref = preferences?.getString(PREF_SUBTITLE_KEY, PREF_SUBTITLE_DEFAULT).orEmpty()
return videoList.map {
Video(
url = it.url,
quality = it.quality,
videoUrl = it.videoUrl,
audioTracks = it.audioTracks,
subtitleTracks = it.subtitleTracks.filter { tracks -> tracks.lang.contains(subPref, true) }
)
}
} }
@Serializable @Serializable
data class SubtitleDto(val file: String, val label: String) data class SubtitleDto(val file: String, val label: String)
companion object {
fun addSubtitlePref(screen: PreferenceScreen) {
EditTextPreference(screen.context).apply {
key = PREF_SUBTITLE_KEY
title = "Filemoon subtitle preference"
summary = "Leave blank to use all subs"
setDefaultValue(PREF_SUBTITLE_DEFAULT)
}.also(screen::addPreference)
}
private const val PREF_SUBTITLE_KEY = "pref_filemoon_sub_lang_key"
private const val PREF_SUBTITLE_DEFAULT = "eng"
}
} }

View file

@ -10,6 +10,7 @@ import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.internal.commonEmptyHeaders import okhttp3.internal.commonEmptyHeaders
import java.io.File import java.io.File
import kotlin.math.abs
class PlaylistUtils(private val client: OkHttpClient, private val headers: Headers = commonEmptyHeaders) { class PlaylistUtils(private val client: OkHttpClient, private val headers: Headers = commonEmptyHeaders) {
@ -137,7 +138,7 @@ class PlaylistUtils(private val client: OkHttpClient, private val headers: Heade
val resolution = it.substringAfter("RESOLUTION=") val resolution = it.substringAfter("RESOLUTION=")
.substringBefore("\n") .substringBefore("\n")
.substringAfter("x") .substringAfter("x")
.substringBefore(",") .substringBefore(",").let(::stnQuality)
val videoUrl = it.substringAfter("\n").substringBefore("\n").let { url -> val videoUrl = it.substringAfter("\n").substringBefore("\n").let { url ->
getAbsoluteUrl(url, playlistUrl, masterUrlBasePath)?.trimEnd() getAbsoluteUrl(url, playlistUrl, masterUrlBasePath)?.trimEnd()
@ -336,6 +337,13 @@ class PlaylistUtils(private val client: OkHttpClient, private val headers: Heade
// ============================= Utilities ============================== // ============================= Utilities ==============================
private fun stnQuality(quality: String): String {
val intQuality = quality.trim().toInt()
val standardQualities = listOf(144, 240, 360, 480, 720, 1080)
val result = standardQualities.minByOrNull { abs(it - intQuality) } ?: quality
return "${result}p"
}
private fun cleanSubtitleData(matchResult: MatchResult): String { private fun cleanSubtitleData(matchResult: MatchResult): String {
val lineCount = matchResult.groupValues[1].count { it == '\n' } val lineCount = matchResult.groupValues[1].count { it == '\n' }
return "\n" + "&nbsp;\n".repeat(lineCount - 1) return "\n" + "&nbsp;\n".repeat(lineCount - 1)

View file

@ -1,7 +0,0 @@
plugins {
id("lib-android")
}
dependencies {
implementation(project(":lib:playlist-utils"))
}

View file

@ -1,67 +0,0 @@
package eu.kanade.tachiyomi.lib.savefileextractor
import android.content.SharedPreferences
import androidx.preference.EditTextPreference
import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.lib.playlistutils.PlaylistUtils
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.util.asJsoup
import okhttp3.Headers
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.OkHttpClient
class SavefileExtractor(
private val client: OkHttpClient,
private val preferences: SharedPreferences,
) {
private val playlistUtils by lazy { PlaylistUtils(client) }
fun videosFromUrl(url: String, prefix: String = "Savefile - ", headers: Headers? = null): List<Video> {
val httpUrl = url.toHttpUrl()
val videoHeaders = (headers?.newBuilder() ?: Headers.Builder())
.set("Referer", url)
.set("Origin", "https://${httpUrl.host}")
.build()
val doc = client.newCall(GET(url, videoHeaders)).execute().asJsoup()
val js = doc.selectFirst("script:containsData(m3u8)")!!.data()
val masterUrl = js.takeIf(String::isNotBlank)
?.substringAfter("{file:\"", "")
?.substringBefore("\"}", "")
?.takeIf(String::isNotBlank)
?: return emptyList()
val videoList = playlistUtils.extractFromHls(
masterUrl,
referer = "https://${httpUrl.host}/",
videoNameGen = { "$prefix$it" },
)
val subPref = preferences.getString(PREF_SUBTITLE_KEY, PREF_SUBTITLE_DEFAULT).orEmpty()
return videoList.map {
Video(
url = it.url,
quality = it.quality,
videoUrl = it.videoUrl,
audioTracks = it.audioTracks,
subtitleTracks = it.subtitleTracks.filter { tracks -> tracks.lang.contains(subPref, true) }
)
}
}
companion object {
fun addSubtitlePref(screen: PreferenceScreen) {
EditTextPreference(screen.context).apply {
key = PREF_SUBTITLE_KEY
title = "Savefile subtitle preference"
summary = "Leave blank to use all subs"
setDefaultValue(PREF_SUBTITLE_DEFAULT)
}.also(screen::addPreference)
}
private const val PREF_SUBTITLE_KEY = "pref_savefile_sub_lang_key"
private const val PREF_SUBTITLE_DEFAULT = "eng"
}
}

View file

@ -10,7 +10,6 @@ import kotlinx.serialization.Serializable
import kotlinx.serialization.SerializationException import kotlinx.serialization.SerializationException
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import okhttp3.Headers import okhttp3.Headers
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
class StreamWishExtractor(private val client: OkHttpClient, private val headers: Headers) { class StreamWishExtractor(private val client: OkHttpClient, private val headers: Headers) {
@ -31,19 +30,16 @@ class StreamWishExtractor(private val client: OkHttpClient, private val headers:
script script
} }
} }
val masterUrl = scriptBody?.let { val masterUrl = scriptBody
M3U8_REGEX.find(it)?.value ?.substringAfter("source", "")
} ?.substringAfter("file:\"", "")
?.substringBefore("\"", "")
?.takeIf(String::isNotBlank)
?: return emptyList() ?: return emptyList()
val subtitleList = extractSubtitles(scriptBody) val subtitleList = extractSubtitles(scriptBody)
return playlistUtils.extractFromHls( return playlistUtils.extractFromHls(masterUrl, url, videoNameGen = videoNameGen, subtitleList = subtitleList)
playlistUrl = masterUrl,
referer = "https://${url.toHttpUrl().host}/",
videoNameGen = videoNameGen,
subtitleList = playlistUtils.fixSubtitles(subtitleList),
)
} }
private fun getEmbedUrl(url: String): String { private fun getEmbedUrl(url: String): String {
@ -61,11 +57,7 @@ class StreamWishExtractor(private val client: OkHttpClient, private val headers:
.substringAfter("tracks") .substringAfter("tracks")
.substringAfter("[") .substringAfter("[")
.substringBefore("]") .substringBefore("]")
val fixedSubtitleStr = FIX_TRACKS_REGEX.replace(subtitleStr) { match -> json.decodeFromString<List<TrackDto>>("[$subtitleStr]")
"\"${match.value}\""
}
json.decodeFromString<List<TrackDto>>("[$fixedSubtitleStr]")
.filter { it.kind.equals("captions", true) } .filter { it.kind.equals("captions", true) }
.map { Track(it.file, it.label ?: "") } .map { Track(it.file, it.label ?: "") }
} catch (e: SerializationException) { } catch (e: SerializationException) {
@ -75,7 +67,4 @@ class StreamWishExtractor(private val client: OkHttpClient, private val headers:
@Serializable @Serializable
private data class TrackDto(val file: String, val kind: String, val label: String? = null) private data class TrackDto(val file: String, val kind: String, val label: String? = null)
private val M3U8_REGEX = Regex("""https[^"]*m3u8[^"]*""")
private val FIX_TRACKS_REGEX = Regex("""(?<!["])(file|kind|label)(?!["])""")
} }

View file

@ -18,7 +18,6 @@ import uy.kohesive.injekt.injectLazy
import java.util.Locale import java.util.Locale
import java.util.concurrent.CountDownLatch import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import kotlin.math.abs
class UniversalExtractor(private val client: OkHttpClient) { class UniversalExtractor(private val client: OkHttpClient) {
private val context: Application by injectLazy() private val context: Application by injectLazy()
@ -74,7 +73,7 @@ class UniversalExtractor(private val client: OkHttpClient) {
for (quality in qualities) { for (quality in qualities) {
val modifiedUrl = resultUrl.replace("M3U8_AUTO_360", "M3U8_AUTO_$quality") val modifiedUrl = resultUrl.replace("M3U8_AUTO_360", "M3U8_AUTO_$quality")
val videos = playlistUtils.extractFromHls(modifiedUrl, origRequestUrl, videoNameGen = { "$prefix - $host: ${stnQuality(it)} $quality" + "p" }) val videos = playlistUtils.extractFromHls(modifiedUrl, origRequestUrl, videoNameGen = { "$prefix - $host: $it $quality" + "p" })
if (videos.isNotEmpty()) { if (videos.isNotEmpty()) {
allVideos.addAll(videos) allVideos.addAll(videos)
@ -90,7 +89,7 @@ class UniversalExtractor(private val client: OkHttpClient) {
return when { return when {
"m3u8" in resultUrl -> { "m3u8" in resultUrl -> {
Log.d("UniversalExtractor", "m3u8 URL: $resultUrl") Log.d("UniversalExtractor", "m3u8 URL: $resultUrl")
playlistUtils.extractFromHls(resultUrl, origRequestUrl, videoNameGen = { "$prefix - $host: ${stnQuality(it)}" }) playlistUtils.extractFromHls(resultUrl, origRequestUrl, videoNameGen = { "$prefix - $host: $it" })
} }
"mpd" in resultUrl -> { "mpd" in resultUrl -> {
Log.d("UniversalExtractor", "mpd URL: $resultUrl") Log.d("UniversalExtractor", "mpd URL: $resultUrl")
@ -104,13 +103,6 @@ class UniversalExtractor(private val client: OkHttpClient) {
} }
} }
private fun stnQuality(quality: String): String {
val intQuality = quality.trim().toInt()
val standardQualities = listOf(144, 240, 360, 480, 720, 1080)
val result = standardQualities.minByOrNull { abs(it - intQuality) } ?: quality
return "${result}p"
}
private fun String.proper(): String { private fun String.proper(): String {
return this.replaceFirstChar { if (it.isLowerCase()) it.titlecase( return this.replaceFirstChar { if (it.isLowerCase()) it.titlecase(
Locale.getDefault()) else it.toString() } Locale.getDefault()) else it.toString() }

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 +0,0 @@
sdk.dir=/workspace/AlmightyHak/extensions-source/SDK

View file

@ -19,8 +19,8 @@ if (System.getenv("CI") != "true") {
} else { } else {
// Running in CI (GitHub Actions) // Running in CI (GitHub Actions)
val chunkSize = System.getenv("CI_CHUNK_SIZE")?.toIntOrNull() ?: Int.MAX_VALUE val chunkSize = System.getenv("CI_CHUNK_SIZE").toInt()
val chunk = System.getenv("CI_CHUNK_NUM")?.toIntOrNull() ?: 0 val chunk = System.getenv("CI_CHUNK_NUM").toInt()
// Loads individual extensions // Loads individual extensions
File(rootDir, "src").getChunk(chunk, chunkSize)?.forEach { File(rootDir, "src").getChunk(chunk, chunkSize)?.forEach {

View file

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

View file

@ -3,7 +3,7 @@ ext {
extClass = '.AnimeXin' extClass = '.AnimeXin'
themePkg = 'animestream' themePkg = 'animestream'
baseUrl = 'https://animexin.vip' baseUrl = 'https://animexin.vip'
overrideVersionCode = 11 overrideVersionCode = 10
} }
apply from: "$rootDir/common.gradle" apply from: "$rootDir/common.gradle"

View file

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

View file

@ -3,7 +3,7 @@ ext {
extClass = '.ChineseAnime' extClass = '.ChineseAnime'
themePkg = 'animestream' themePkg = 'animestream'
baseUrl = 'https://www.chineseanime.vip' baseUrl = 'https://www.chineseanime.vip'
overrideVersionCode = 15 overrideVersionCode = 13
} }
apply from: "$rootDir/common.gradle" apply from: "$rootDir/common.gradle"

View file

@ -1,7 +1,7 @@
ext { ext {
extName = 'Hikari' extName = 'Hikari'
extClass = '.Hikari' extClass = '.Hikari'
extVersionCode = 22 extVersionCode = 16
} }
apply from: "$rootDir/common.gradle" apply from: "$rootDir/common.gradle"
@ -9,7 +9,6 @@ apply from: "$rootDir/common.gradle"
dependencies { dependencies {
implementation(project(':lib:chillx-extractor')) implementation(project(':lib:chillx-extractor'))
implementation(project(':lib:filemoon-extractor')) implementation(project(':lib:filemoon-extractor'))
implementation(project(':lib:savefile-extractor'))
implementation(project(':lib:buzzheavier-extractor'))
implementation(project(':lib:streamwish-extractor')) implementation(project(':lib:streamwish-extractor'))
implementation(project(':lib:vidhide-extractor'))
} }

View file

@ -1,118 +0,0 @@
package eu.kanade.tachiyomi.animeextension.all.hikari
import eu.kanade.tachiyomi.animesource.model.SAnime
import eu.kanade.tachiyomi.animesource.model.SEpisode
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class CatalogResponseDto<T>(
val next: String? = null,
val results: List<T>,
)
@Serializable
data class AnimeDto(
val uid: String,
@SerialName("ani_ename")
val aniEName: String? = null,
@SerialName("ani_name")
val aniName: String,
@SerialName("ani_poster")
val aniPoster: String? = null,
@SerialName("ani_synopsis")
val aniSynopsis: String? = null,
@SerialName("ani_synonyms")
val aniSynonyms: String? = null,
@SerialName("ani_genre")
val aniGenre: String? = null,
@SerialName("ani_studio")
val aniStudio: String? = null,
@SerialName("ani_producers")
val aniProducers: String? = null,
@SerialName("ani_stats")
val aniStats: Int? = null,
@SerialName("ani_time")
val aniTime: String? = null,
@SerialName("ani_ep")
val aniEp: String? = null,
@SerialName("ani_type")
val aniType: Int? = null,
@SerialName("ani_score")
val aniScore: Double? = null,
) {
fun toSAnime(preferEnglish: Boolean): SAnime = SAnime.create().apply {
url = uid
title = if (preferEnglish) aniEName?.takeUnless(String::isBlank) ?: aniName else aniName
thumbnail_url = aniPoster
genre = aniGenre?.split(",")?.joinToString(transform = String::trim)
artist = aniStudio
author = aniProducers?.split(",")?.joinToString(transform = String::trim)
description = buildString {
aniScore?.let { append("Score: %.2f/10\n\n".format(it)) }
aniSynopsis?.trim()?.let(::append)
append("\n\n")
aniType?.let {
val type = when (it) {
1 -> "TV"
2 -> "Movie"
3 -> "OVA"
4 -> "ONA"
5 -> "Special"
else -> "Unknown"
}
append("Type: $type\n")
}
aniEp?.let { append("Total Episode count: $it\n") }
aniTime?.let { append("Runtime: $it\n") }
aniSynonyms?.let { append("Synonyms: $it") }
}.trim()
status = when (aniStats) {
1 -> SAnime.UNKNOWN
2 -> SAnime.COMPLETED
3 -> SAnime.ONGOING
else -> SAnime.UNKNOWN
}
}
}
@Serializable
data class LatestEpisodeDto(
val uid: Int,
val title: String,
@SerialName("title_en")
val titleEn: String? = null,
val imageUrl: String,
) {
fun toSAnime(preferEnglish: Boolean): SAnime = SAnime.create().apply {
url = uid.toString()
title = if (preferEnglish) titleEn?.takeUnless(String::isBlank) ?: this@LatestEpisodeDto.title else this@LatestEpisodeDto.title
thumbnail_url = imageUrl
}
}
@Serializable
data class EpisodeDto(
@SerialName("ep_id_name")
val epId: String,
@SerialName("ep_name")
val epName: String? = null,
) {
fun toSEpisode(uid: String): SEpisode = SEpisode.create().apply {
url = "$uid-$epId"
name = epName?.let { "Ep. $epId - $it" } ?: "Episode $epId"
episode_number = epId.toFloatOrNull() ?: 1f
}
}
@Serializable
data class EmbedDto(
@SerialName("embed_type")
val embedType: String,
@SerialName("embed_name")
val embedName: String,
@SerialName("embed_frame")
val embedFrame: String,
)

View file

@ -5,7 +5,7 @@ import okhttp3.HttpUrl
import java.util.Calendar import java.util.Calendar
interface UriFilter { interface UriFilter {
fun addToUri(builder: HttpUrl.Builder) fun addToUri(url: HttpUrl.Builder)
} }
sealed class UriPartFilter( sealed class UriPartFilter(
@ -20,10 +20,7 @@ sealed class UriPartFilter(
), ),
UriFilter { UriFilter {
override fun addToUri(builder: HttpUrl.Builder) { override fun addToUri(builder: HttpUrl.Builder) {
val value = vals[state].second builder.addQueryParameter(param, vals[state].second)
if (value.isNotEmpty()) {
builder.addQueryParameter(param, value)
}
} }
} }
@ -36,15 +33,13 @@ sealed class UriMultiSelectFilter(
) : AnimeFilter.Group<UriMultiSelectOption>(name, vals.map { UriMultiSelectOption(it.first, it.second) }), UriFilter { ) : AnimeFilter.Group<UriMultiSelectOption>(name, vals.map { UriMultiSelectOption(it.first, it.second) }), UriFilter {
override fun addToUri(builder: HttpUrl.Builder) { override fun addToUri(builder: HttpUrl.Builder) {
val checked = state.filter { it.state } val checked = state.filter { it.state }
if (checked.isNotEmpty()) {
builder.addQueryParameter(param, checked.joinToString(",") { it.value }) builder.addQueryParameter(param, checked.joinToString(",") { it.value })
} }
}
} }
class TypeFilter : UriPartFilter( class TypeFilter : UriPartFilter(
"Type", "Type",
"ani_type", "type",
arrayOf( arrayOf(
Pair("All", ""), Pair("All", ""),
Pair("TV", "1"), Pair("TV", "1"),
@ -55,53 +50,165 @@ class TypeFilter : UriPartFilter(
), ),
) )
class StatusFilter : UriPartFilter( class CountryFilter : UriPartFilter(
"Status", "Country",
"ani_stats", "country",
arrayOf( arrayOf(
Pair("All", ""), Pair("All", ""),
Pair("Ongoing", "1"), Pair("Japanese", "1"),
Pair("Completed", "2"), Pair("Chinese", "2"),
Pair("Upcoming", "3"), ),
)
class StatusFilter : UriPartFilter(
"Status",
"stats",
arrayOf(
Pair("All", ""),
Pair("Currently Airing", "1"),
Pair("Finished Airing", "2"),
Pair("Not yet Aired", "3"),
),
)
class RatingFilter : UriPartFilter(
"Rating",
"rate",
arrayOf(
Pair("All", ""),
Pair("G", "1"),
Pair("PG", "2"),
Pair("PG-13", "3"),
Pair("R-17+", "4"),
Pair("R+", "5"),
Pair("Rx", "6"),
),
)
class SourceFilter : UriPartFilter(
"Source",
"source",
arrayOf(
Pair("All", ""),
Pair("LightNovel", "1"),
Pair("Manga", "2"),
Pair("Original", "3"),
), ),
) )
class SeasonFilter : UriPartFilter( class SeasonFilter : UriPartFilter(
"Season", "Season",
"ani_release_season", "season",
arrayOf( arrayOf(
Pair("All", ""), Pair("All", ""),
Pair("Winter", "1"), Pair("Spring", "1"),
Pair("Spring", "2"), Pair("Summer", "2"),
Pair("Summer", "3"), Pair("Fall", "3"),
Pair("Fall", "4"), Pair("Winter", "4"),
), ),
) )
class YearFilter : UriPartFilter( class LanguageFilter : UriPartFilter(
"Release Year", "Language",
"ani_release", "language",
arrayOf(
Pair("All", ""),
Pair("Raw", "1"),
Pair("Sub", "2"),
Pair("Dub", "3"),
Pair("Turk", "4"),
),
)
class SortFilter : UriPartFilter(
"Sort",
"sort",
arrayOf(
Pair("Default", "default"),
Pair("Recently Added", "recently_added"),
Pair("Recently Updated", "recently_updated"),
Pair("Score", "score"),
Pair("Name A-Z", "name_az"),
Pair("Released Date", "released_date"),
Pair("Most Watched", "most_watched"),
),
)
class YearFilter(name: String, param: String) : UriPartFilter(
name,
param,
YEARS, YEARS,
) { ) {
companion object { companion object {
private val CURRENT_YEAR by lazy { private val NEXT_YEAR by lazy {
Calendar.getInstance()[Calendar.YEAR] Calendar.getInstance()[Calendar.YEAR] + 1
} }
private val YEARS = buildList { private val YEARS = Array(NEXT_YEAR - 1917) { year ->
add(Pair("Any", "")) if (year == 0) {
addAll( Pair("Any", "")
(1990..CURRENT_YEAR).map { } else {
Pair(it.toString(), it.toString()) (NEXT_YEAR - year).toString().let { Pair(it, it) }
}, }
}
}
}
class MonthFilter(name: String, param: String) : UriPartFilter(
name,
param,
MONTHS,
) {
companion object {
private val MONTHS = Array(13) { months ->
if (months == 0) {
Pair("Any", "")
} else {
val monthStr = "%02d".format(months)
Pair(monthStr, monthStr)
}
}
}
}
class DayFilter(name: String, param: String) : UriPartFilter(
name,
param,
DAYS,
) {
companion object {
private val DAYS = Array(32) { day ->
if (day == 0) {
Pair("Any", "")
} else {
val dayStr = "%02d".format(day)
Pair(dayStr, dayStr)
}
}
}
}
class AiringDateFilter(
private val values: List<UriPartFilter> = PARTS,
) : AnimeFilter.Group<UriPartFilter>("Airing Date", values), UriFilter {
override fun addToUri(builder: HttpUrl.Builder) {
values.forEach {
it.addToUri(builder)
}
}
companion object {
private val PARTS = listOf(
YearFilter("Year", "aired_year"),
MonthFilter("Month", "aired_month"),
DayFilter("Day", "aired_day"),
) )
}.toTypedArray()
} }
} }
class GenreFilter : UriMultiSelectFilter( class GenreFilter : UriMultiSelectFilter(
"Genre", "Genre",
"ani_genre", "genres",
arrayOf( arrayOf(
Pair("Action", "Action"), Pair("Action", "Action"),
Pair("Adventure", "Adventure"), Pair("Adventure", "Adventure"),
@ -126,7 +233,7 @@ class GenreFilter : UriMultiSelectFilter(
Pair("Music", "Music"), Pair("Music", "Music"),
Pair("Mystery", "Mystery"), Pair("Mystery", "Mystery"),
Pair("Parody", "Parody"), Pair("Parody", "Parody"),
Pair("Policy", "Policy"), Pair("Police", "Police"),
Pair("Psychological", "Psychological"), Pair("Psychological", "Psychological"),
Pair("Romance", "Romance"), Pair("Romance", "Romance"),
Pair("Samurai", "Samurai"), Pair("Samurai", "Samurai"),
@ -146,12 +253,3 @@ class GenreFilter : UriMultiSelectFilter(
Pair("Vampire", "Vampire"), Pair("Vampire", "Vampire"),
), ),
) )
class LanguageFilter : UriPartFilter(
"Language",
"ani_genre",
arrayOf(
Pair("Any", ""),
Pair("Portuguese", "Portuguese"),
),
)

View file

@ -1,86 +1,124 @@
package eu.kanade.tachiyomi.animeextension.all.hikari package eu.kanade.tachiyomi.animeextension.all.hikari
import android.app.Application import android.app.Application
import android.content.SharedPreferences import android.util.Log
import androidx.preference.ListPreference import androidx.preference.ListPreference
import androidx.preference.MultiSelectListPreference
import androidx.preference.PreferenceScreen import androidx.preference.PreferenceScreen
import androidx.preference.SwitchPreferenceCompat
import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource
import eu.kanade.tachiyomi.animesource.model.AnimeFilter
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
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
import eu.kanade.tachiyomi.animesource.online.AnimeHttpSource import eu.kanade.tachiyomi.animesource.online.ParsedAnimeHttpSource
import eu.kanade.tachiyomi.lib.buzzheavierextractor.BuzzheavierExtractor
import eu.kanade.tachiyomi.lib.chillxextractor.ChillxExtractor import eu.kanade.tachiyomi.lib.chillxextractor.ChillxExtractor
import eu.kanade.tachiyomi.lib.filemoonextractor.FilemoonExtractor import eu.kanade.tachiyomi.lib.filemoonextractor.FilemoonExtractor
import eu.kanade.tachiyomi.lib.savefileextractor.SavefileExtractor
import eu.kanade.tachiyomi.lib.streamwishextractor.StreamWishExtractor import eu.kanade.tachiyomi.lib.streamwishextractor.StreamWishExtractor
import eu.kanade.tachiyomi.lib.vidhideextractor.VidHideExtractor
import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.util.parallelCatchingFlatMapBlocking import eu.kanade.tachiyomi.util.parallelCatchingFlatMapBlocking
import eu.kanade.tachiyomi.util.parseAs import eu.kanade.tachiyomi.util.parseAs
import kotlinx.serialization.Serializable
import okhttp3.HttpUrl.Companion.toHttpUrl import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Request import okhttp3.Request
import okhttp3.Response import okhttp3.Response
import org.jsoup.Jsoup
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
class Hikari : AnimeHttpSource(), ConfigurableAnimeSource { class Hikari : ParsedAnimeHttpSource(), ConfigurableAnimeSource {
override val name = "Hikari" override val name = "Hikari"
private val proxyUrl = "https://hikari.gg/hiki-proxy/extract/" override val baseUrl = "https://watch.hikaritv.xyz"
private val apiUrl = "https://api.hikari.gg/api"
override val baseUrl = "https://hikari.gg"
override val lang = "all" override val lang = "all"
override val versionId = 2
override val supportsLatest = true override val supportsLatest = true
override fun headersBuilder() = super.headersBuilder().apply {
add("Origin", baseUrl)
add("Referer", "$baseUrl/")
}
private val preferences by lazy { private val preferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000) Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
} }
// ============================== Popular =============================== // ============================== Popular ===============================
override fun popularAnimeRequest(page: Int) = searchAnimeRequest(page, "", AnimeFilterList()) override fun popularAnimeRequest(page: Int): Request {
val url = "$baseUrl/ajax/getfilter?type=&country=&stats=&rate=&source=&season=&language=&aired_year=&aired_month=&aired_day=&sort=score&genres=&page=$page"
val headers = headersBuilder().set("Referer", "$baseUrl/filter").build()
return GET(url, headers)
}
override fun popularAnimeParse(response: Response) = searchAnimeParse(response) override fun popularAnimeParse(response: Response): AnimesPage {
val parsed = response.parseAs<HtmlResponseDto>()
val hasNextPage = response.request.url.queryParameter("page")!!.toInt() < parsed.page!!.totalPages
val animeList = parsed.toHtml(baseUrl).select(popularAnimeSelector())
.map(::popularAnimeFromElement)
return AnimesPage(animeList, hasNextPage)
}
override fun popularAnimeSelector(): String = ".flw-item"
override fun popularAnimeFromElement(element: Element): SAnime = SAnime.create().apply {
setUrlWithoutDomain(element.selectFirst("a[data-id]")!!.attr("abs:href"))
thumbnail_url = element.selectFirst("img")!!.attr("abs:src")
title = element.selectFirst(".film-name")!!.text()
}
override fun popularAnimeNextPageSelector(): String? = null
// =============================== Latest =============================== // =============================== Latest ===============================
override fun latestUpdatesRequest(page: Int): Request { override fun latestUpdatesRequest(page: Int): Request {
val url = "$apiUrl/episode/new/".toHttpUrl().newBuilder().apply { val url = "$baseUrl/ajax/getfilter?type=&country=&stats=&rate=&source=&season=&language=&aired_year=&aired_month=&aired_day=&sort=recently_updated&genres=&page=$page"
addQueryParameter("limit", "100") val headers = headersBuilder().set("Referer", "$baseUrl/filter").build()
addQueryParameter("language", "EN")
}.build()
return GET(url, headers) return GET(url, headers)
} }
override fun latestUpdatesParse(response: Response): AnimesPage { override fun latestUpdatesParse(response: Response): AnimesPage =
val data = response.parseAs<CatalogResponseDto<LatestEpisodeDto>>() popularAnimeParse(response)
val preferEnglish = preferences.getTitleLang
val animeList = data.results.distinctBy { it.uid }.map { it.toSAnime(preferEnglish) } override fun latestUpdatesSelector(): String =
return AnimesPage(animeList, false) throw UnsupportedOperationException()
}
override fun latestUpdatesFromElement(element: Element): SAnime =
throw UnsupportedOperationException()
override fun latestUpdatesNextPageSelector(): String =
throw UnsupportedOperationException()
// =============================== Search =============================== // =============================== Search ===============================
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request { override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
val url = "$apiUrl/anime/".toHttpUrl().newBuilder().apply { val url = baseUrl.toHttpUrl().newBuilder().apply {
addQueryParameter("sort", "created_at") if (query.isNotEmpty()) {
addQueryParameter("order", "asc") addPathSegment("search")
addQueryParameter("keyword", query)
addQueryParameter("page", page.toString()) addQueryParameter("page", page.toString())
} else {
addPathSegment("ajax")
addPathSegment("getfilter")
filters.filterIsInstance<UriFilter>().forEach { filters.filterIsInstance<UriFilter>().forEach {
it.addToUri(this) it.addToUri(this)
} }
addQueryParameter("page", page.toString())
}
}.build()
val headers = headersBuilder().apply {
if (query.isNotEmpty()) { if (query.isNotEmpty()) {
addQueryParameter("search", query) set("Referer", url.toString().substringBeforeLast("&page"))
} else {
set("Referer", "$baseUrl/filter")
} }
}.build() }.build()
@ -88,179 +126,280 @@ class Hikari : AnimeHttpSource(), ConfigurableAnimeSource {
} }
override fun searchAnimeParse(response: Response): AnimesPage { override fun searchAnimeParse(response: Response): AnimesPage {
val data = response.parseAs<CatalogResponseDto<AnimeDto>>() return if (response.request.url.encodedPath.startsWith("/search")) {
val preferEnglish = preferences.getTitleLang super.searchAnimeParse(response)
} else {
val animeList = data.results.map { it.toSAnime(preferEnglish) } popularAnimeParse(response)
val hasNextPage = data.next != null
return AnimesPage(animeList, hasNextPage)
} }
}
override fun searchAnimeSelector(): String = popularAnimeSelector()
override fun searchAnimeFromElement(element: Element): SAnime = popularAnimeFromElement(element)
override fun searchAnimeNextPageSelector(): String = "ul.pagination > li.active + li"
// ============================== Filters =============================== // ============================== Filters ===============================
override fun getFilterList(): AnimeFilterList = AnimeFilterList( override fun getFilterList(): AnimeFilterList = AnimeFilterList(
AnimeFilter.Header("Note: text search ignores filters"),
AnimeFilter.Separator(),
TypeFilter(), TypeFilter(),
CountryFilter(),
StatusFilter(), StatusFilter(),
RatingFilter(),
SourceFilter(),
SeasonFilter(), SeasonFilter(),
YearFilter(),
GenreFilter(),
LanguageFilter(), LanguageFilter(),
SortFilter(),
AiringDateFilter(),
GenreFilter(),
) )
// =========================== Anime Details ============================ // =========================== Anime Details ============================
override fun getAnimeUrl(anime: SAnime): String { override fun animeDetailsParse(document: Document): SAnime = SAnime.create().apply {
return "$baseUrl/info/${anime.url}" with(document.selectFirst("#ani_detail")!!) {
title = selectFirst(".film-name")!!.text()
thumbnail_url = selectFirst(".film-poster img")!!.attr("abs:src")
description = selectFirst(".film-description > .text")?.text()
genre = select(".item-list:has(span:contains(Genres)) > a").joinToString { it.text() }
author = select(".item:has(span:contains(Studio)) > a").joinToString { it.text() }
status = selectFirst(".item:has(span:contains(Status)) > .name").parseStatus()
}
} }
override fun animeDetailsRequest(anime: SAnime): Request { private fun Element?.parseStatus(): Int = when (this?.text()?.lowercase()) {
return GET("$apiUrl/anime/uid/${anime.url}/", headers) "currently airing" -> SAnime.ONGOING
} "finished" -> SAnime.COMPLETED
else -> SAnime.UNKNOWN
override fun animeDetailsParse(response: Response): SAnime {
return response.parseAs<AnimeDto>().toSAnime(preferences.getTitleLang)
} }
// ============================== Episodes ============================== // ============================== Episodes ==============================
private val specialCharRegex = Regex("""(?![\-_])\W{1,}""")
override fun episodeListRequest(anime: SAnime): Request { override fun episodeListRequest(anime: SAnime): Request {
return GET("$apiUrl/episode/uid/${anime.url}/", headers) val animeId = anime.url.split("/")[2]
val sanitized = anime.title.replace(" ", "_")
val refererUrl = baseUrl.toHttpUrl().newBuilder().apply {
addPathSegment("watch")
addQueryParameter("anime", specialCharRegex.replace(sanitized, ""))
addQueryParameter("uid", animeId)
addQueryParameter("eps", "1")
}.build()
val headers = headersBuilder()
.set("Referer", refererUrl.toString())
.build()
return GET("$baseUrl/ajax/episodelist/$animeId", headers)
} }
override fun episodeListParse(response: Response): List<SEpisode> { override fun episodeListParse(response: Response): List<SEpisode> {
val guid = response.request.url.pathSegments[3] return response.parseAs<HtmlResponseDto>().toHtml(baseUrl)
.select(episodeListSelector())
.map(::episodeFromElement)
.reversed()
}
return response.parseAs<List<EpisodeDto>>().map { it.toSEpisode(guid) }.reversed() override fun episodeListSelector() = "a[class~=ep-item]"
override fun episodeFromElement(element: Element): SEpisode {
val epText = element.selectFirst(".ssli-order")?.text()?.trim()
?: element.attr("data-number").trim()
val ep = epText.toFloatOrNull() ?: 0F
val nameText = element.selectFirst(".ep-name")?.text()?.trim()
?: element.attr("title").replace("Episode-", "Ep. ") ?: ""
return SEpisode.create().apply {
setUrlWithoutDomain(element.attr("abs:href"))
episode_number = ep
name = "Ep. $ep - $nameText"
}
} }
// ============================ Video Links ============================= // ============================ Video Links =============================
private val filemoonExtractor by lazy { FilemoonExtractor(client, preferences) } private val filemoonExtractor by lazy { FilemoonExtractor(client) }
private val savefileExtractor by lazy { SavefileExtractor(client, preferences) } private val vidHideExtractor by lazy { VidHideExtractor(client, headers) }
private val buzzheavierExtractor by lazy { BuzzheavierExtractor(client, headers) }
private val chillxExtractor by lazy { ChillxExtractor(client, headers) } private val chillxExtractor by lazy { ChillxExtractor(client, headers) }
private val streamwishExtractor by lazy { StreamWishExtractor(client, headers) } private val streamwishExtractor by lazy { StreamWishExtractor(client, headers) }
private val embedRegex = Regex("""getEmbed\(\s*(\d+)\s*,\s*(\d+)\s*,\s*'(\d+)'""")
private fun getEmbedTypeName(type: String): String {
return when (type) {
"2" -> "[SUB] "
"3" -> "[DUB] "
"4" -> "[MULTI AUDIO] "
"8" -> "[HARD-SUB] "
else -> ""
}
}
override fun videoListRequest(episode: SEpisode): Request { override fun videoListRequest(episode: SEpisode): Request {
val (guid, epId) = episode.url.split("-") val url = (baseUrl + episode.url).toHttpUrl()
return GET("$apiUrl/embed/$guid/$epId/", headers) val animeId = url.queryParameter("uid")!!
val episodeNum = url.queryParameter("eps")!!
val headers = headersBuilder()
.set("Referer", baseUrl + episode.url)
.build()
return GET("$baseUrl/ajax/embedserver/$animeId/$episodeNum", headers)
} }
override fun videoListParse(response: Response): List<Video> { override fun videoListParse(response: Response): List<Video> {
val data = response.parseAs<List<EmbedDto>>() val html = response.parseAs<HtmlResponseDto>().toHtml(baseUrl)
Log.d("Hikari", html.toString())
val selectedProviders = preferences.getStringSet(PREF_PROVIDER_KEY, PREF_PROVIDERS_DEFAULT)?.map(String::lowercase)?.toSet() ?: emptySet() val headers = headersBuilder()
.set("Referer", response.request.url.toString())
.build()
return data.parallelCatchingFlatMapBlocking { embed -> val subEmbedUrls = html.select(".servers-sub div.item.server-item").flatMap { item ->
val embedName = embed.embedName.lowercase() val name = item.text().trim() + " (Sub)"
val onClick = item.selectFirst("a")?.attr("onclick")
if (onClick == null) {
Log.e("Hikari", "onClick attribute is null for item: $item")
return@flatMap emptyList<Pair<String, String>>()
}
if (embedName !in selectedProviders) return@parallelCatchingFlatMapBlocking emptyList() val match = embedRegex.find(onClick)?.groupValues
if (match == null) {
Log.e("Hikari", "No match found for onClick: $onClick")
return@flatMap emptyList<Pair<String, String>>()
}
val prefix = getEmbedTypeName(embed.embedType) + embed.embedName val url = "$baseUrl/ajax/embed/${match[1]}/${match[2]}/${match[3]}"
val iframeList = client.newCall(
GET(url, headers),
).execute().parseAs<List<String>>()
when (embedName) { iframeList.map {
"streamwish" -> streamwishExtractor.videosFromUrl(embed.embedFrame, videoNameGen = { "$prefix - $it" }) val iframeSrc = Jsoup.parseBodyFragment(it).selectFirst("iframe")?.attr("src")
"filemoon" -> filemoonExtractor.videosFromUrl(embed.embedFrame, "$prefix - ") if (iframeSrc == null) {
"sv" -> savefileExtractor.videosFromUrl(embed.embedFrame, "$prefix - ") Log.e("Hikari", "iframe src is null for URL: $url")
"playerx" -> chillxExtractor.videoFromUrl(embed.embedFrame, "$prefix - ") return@map Pair("", "")
"hiki" -> hikiExtraction(embed.embedFrame, "$prefix - ") }
else -> emptyList() Pair(iframeSrc, name)
}.filter { it.first.isNotEmpty() }
}
val dubEmbedUrls = html.select(".servers-dub div.item.server-item").flatMap { item ->
val name = item.text().trim() + " (Dub)"
val onClick = item.selectFirst("a")?.attr("onclick")
if (onClick == null) {
Log.e("Hikari", "onClick attribute is null for item: $item")
return@flatMap emptyList<Pair<String, String>>()
}
val match = embedRegex.find(onClick)?.groupValues
if (match == null) {
Log.e("Hikari", "No match found for onClick: $onClick")
return@flatMap emptyList<Pair<String, String>>()
}
val url = "$baseUrl/ajax/embed/${match[1]}/${match[2]}/${match[3]}"
val iframeList = client.newCall(
GET(url, headers),
).execute().parseAs<List<String>>()
iframeList.map {
val iframeSrc = Jsoup.parseBodyFragment(it).selectFirst("iframe")?.attr("src")
if (iframeSrc == null) {
Log.e("Hikari", "iframe src is null for URL: $url")
return@map Pair("", "")
}
Pair(iframeSrc, name)
}.filter { it.first.isNotEmpty() }
}
val sdEmbedUrls = html.select(".servers-sub.\\&.dub div.item.server-item").flatMap { item ->
val name = item.text().trim() + " (Sub + Dub)"
val onClick = item.selectFirst("a")?.attr("onclick")
if (onClick == null) {
Log.e("Hikari", "onClick attribute is null for item: $item")
return@flatMap emptyList<Pair<String, String>>()
}
val match = embedRegex.find(onClick)?.groupValues
if (match == null) {
Log.e("Hikari", "No match found for onClick: $onClick")
return@flatMap emptyList<Pair<String, String>>()
}
val url = "$baseUrl/ajax/embed/${match[1]}/${match[2]}/${match[3]}"
val iframeList = client.newCall(
GET(url, headers),
).execute().parseAs<List<String>>()
iframeList.map {
val iframeSrc = Jsoup.parseBodyFragment(it).selectFirst("iframe")?.attr("src")
if (iframeSrc == null) {
Log.e("Hikari", "iframe src is null for URL: $url")
return@map Pair("", "")
}
Pair(iframeSrc, name)
}.filter { it.first.isNotEmpty() }
}
return sdEmbedUrls.parallelCatchingFlatMapBlocking {
getVideosFromEmbed(it.first, it.second)
}.ifEmpty {
(subEmbedUrls + dubEmbedUrls).parallelCatchingFlatMapBlocking {
getVideosFromEmbed(it.first, it.second)
} }
} }
} }
private fun hikiExtraction(url: String, prefix: String): List<Video> { private fun getVideosFromEmbed(embedUrl: String, name: String): List<Video> = when {
val hikiMirror = preferences.getString(PREF_HIKI_KEY, PREF_HIKI_DEFAULT)!! name.contains("vidhide", true) -> vidHideExtractor.videosFromUrl(embedUrl, videoNameGen = { s -> "$name - $s" })
embedUrl.contains("filemoon", true) -> filemoonExtractor.videosFromUrl(embedUrl, prefix = "$name - ", headers = headers)
name.contains("streamwish", true) -> streamwishExtractor.videosFromUrl(embedUrl, prefix = "$name - ")
else -> chillxExtractor.videoFromUrl(embedUrl, referer = baseUrl, prefix = "$name - ")
}
if (hikiMirror == "hiki") { override fun videoListSelector() = ".server-item:has(a[onclick~=getEmbed])"
return buzzheavierExtractor.videosFromUrl(url, prefix, proxyUrl)
}
val id = url.toHttpUrl().pathSegments[0]
val videoUrl = "https://$hikiMirror/$id"
return buzzheavierExtractor.videosFromUrl(videoUrl, prefix)
}
override fun List<Video>.sort(): List<Video> { override fun List<Video>.sort(): List<Video> {
val quality = preferences.getString(PREF_QUALITY_KEY, PREF_QUALITY_DEFAULT)!! val quality = preferences.getString(PREF_QUALITY_KEY, PREF_QUALITY_DEFAULT)!!
val type = preferences.getString(PREF_TYPE_KEY, PREF_TYPE_DEFAULT)!!
val hoster = preferences.getString(PREF_HOSTER_KEY, PREF_HOSTER_DEFAULT)!!
return sortedWith( return sortedWith(
compareBy( compareBy(
{ it.quality.startsWith(type) },
{ it.quality.contains(quality) }, { it.quality.contains(quality) },
{ QUALITY_REGEX.find(it.quality)?.groupValues?.get(1)?.toIntOrNull() ?: 0 }, { QUALITY_REGEX.find(it.quality)?.groupValues?.get(1)?.toIntOrNull() ?: 0 },
{ it.quality.contains(hoster, true) },
), ),
).reversed() ).reversed()
} }
override fun videoFromElement(element: Element): Video =
throw UnsupportedOperationException()
override fun videoUrlParse(document: Document): String =
throw UnsupportedOperationException()
// ============================= Utilities ============================== // ============================= Utilities ==============================
@Serializable
class HtmlResponseDto(
val html: String,
val page: PageDto? = null,
) {
fun toHtml(baseUrl: String): Document = Jsoup.parseBodyFragment(html, baseUrl)
@Serializable
class PageDto(
val totalPages: Int,
)
}
companion object { companion object {
private val QUALITY_REGEX = Regex("""(\d+)p""") private val QUALITY_REGEX = Regex("""(\d+)p""")
private const val PREF_ENGLISH_TITLE_KEY = "preferred_title_lang"
private const val PREF_ENGLISH_TITLE_DEFAULT = true
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 val PREF_QUALITY_VALUES = arrayOf("1080", "720", "480", "360") private val PREF_QUALITY_VALUES = arrayOf("1080", "720", "480", "360")
private val PREF_QUALITY_ENTRIES = PREF_QUALITY_VALUES.map { private val PREF_QUALITY_ENTRIES = PREF_QUALITY_VALUES.map {
"${it}p" "${it}p"
}.toTypedArray() }.toTypedArray()
private val TYPE_LIST = arrayOf("[SUB] ", "[DUB] ", "[MULTI AUDIO] ", "[HARD-SUB] ")
private const val PREF_TYPE_KEY = "pref_type"
private const val PREF_TYPE_DEFAULT = ""
private val PREF_TYPE_VALUES = arrayOf("") + TYPE_LIST
private val PREF_TYPE_ENTRIES = arrayOf("Any") + TYPE_LIST
private val HOSTER_LIST = arrayOf("Streamwish", "Filemoon", "SV", "PlayerX", "Hiki")
private const val PREF_HOSTER_KEY = "pref_hoster"
private const val PREF_HOSTER_DEFAULT = ""
private val PREF_HOSTER_VALUES = arrayOf("") + HOSTER_LIST
private val PREF_HOSTER_ENTRIES = arrayOf("Any") + HOSTER_LIST
private const val PREF_HIKI_KEY = "preferred_hiki_mirror"
private const val PREF_HIKI_DEFAULT = "hiki"
private val PREF_HIKI_VALUES = arrayOf("hiki", "buzzheavier.com", "bzzhr.co", "fuckingfast.net")
private val PREF_HIKI_ENTRIES = PREF_HIKI_VALUES
// Provider
private const val PREF_PROVIDER_KEY = "provider_selection"
private val PREF_PROVIDERS = arrayOf("Streamwish", "Filemoon", "SV", "PlayerX", "Hiki")
private val PREF_PROVIDERS_VALUE = arrayOf("streamwish", "filemoon", "sv", "playerx", "hiki")
private val PREF_DEFAULT_PROVIDERS_VALUE = arrayOf("streamwish", "filemoon", "sv", "playerx", "hiki")
private val PREF_PROVIDERS_DEFAULT = PREF_DEFAULT_PROVIDERS_VALUE.toSet()
} }
// ============================== Settings ============================== // ============================== Settings ==============================
private val SharedPreferences.getTitleLang
get() = getBoolean(PREF_ENGLISH_TITLE_KEY, PREF_ENGLISH_TITLE_DEFAULT)
override fun setupPreferenceScreen(screen: PreferenceScreen) { override fun setupPreferenceScreen(screen: PreferenceScreen) {
SwitchPreferenceCompat(screen.context).apply {
key = PREF_ENGLISH_TITLE_KEY
title = "Prefer english titles"
setDefaultValue(PREF_ENGLISH_TITLE_DEFAULT)
}.also(screen::addPreference)
ListPreference(screen.context).apply { ListPreference(screen.context).apply {
key = PREF_QUALITY_KEY key = PREF_QUALITY_KEY
title = "Preferred quality" title = "Preferred quality"
@ -268,44 +407,13 @@ class Hikari : AnimeHttpSource(), ConfigurableAnimeSource {
entryValues = PREF_QUALITY_VALUES entryValues = PREF_QUALITY_VALUES
setDefaultValue(PREF_QUALITY_DEFAULT) setDefaultValue(PREF_QUALITY_DEFAULT)
summary = "%s" summary = "%s"
}.also(screen::addPreference)
ListPreference(screen.context).apply { setOnPreferenceChangeListener { _, newValue ->
key = PREF_TYPE_KEY val selected = newValue as String
title = "Preferred type" val index = findIndexOfValue(selected)
entries = PREF_TYPE_ENTRIES val entry = entryValues[index] as String
entryValues = PREF_TYPE_VALUES preferences.edit().putString(key, entry).commit()
setDefaultValue(PREF_TYPE_DEFAULT) }
summary = "%s"
}.also(screen::addPreference) }.also(screen::addPreference)
ListPreference(screen.context).apply {
key = PREF_HOSTER_KEY
title = "Preferred hoster"
entries = PREF_HOSTER_ENTRIES
entryValues = PREF_HOSTER_VALUES
setDefaultValue(PREF_HOSTER_DEFAULT)
summary = "%s"
}.also(screen::addPreference)
MultiSelectListPreference(screen.context).apply {
key = PREF_PROVIDER_KEY
title = "Enable/Disable Video Providers"
entries = PREF_PROVIDERS
entryValues = PREF_PROVIDERS_VALUE
setDefaultValue(PREF_PROVIDERS_DEFAULT)
}.also(screen::addPreference)
ListPreference(screen.context).apply {
key = PREF_HIKI_KEY
title = "Hiki provider mirrors"
entries = PREF_HIKI_ENTRIES
entryValues = PREF_HIKI_VALUES
setDefaultValue(PREF_HIKI_DEFAULT)
summary = "%s"
}.also(screen::addPreference)
FilemoonExtractor.addSubtitlePref(screen)
SavefileExtractor.addSubtitlePref(screen)
} }
} }

View file

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

View file

@ -1,7 +1,7 @@
ext { ext {
extName = 'Jav Guru' extName = 'Jav Guru'
extClass = '.JavGuru' extClass = '.JavGuru'
extVersionCode = 28 extVersionCode = 26
isNsfw = true isNsfw = true
} }

View file

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

View file

@ -1,7 +1,7 @@
ext { ext {
extName = 'MissAV' extName = 'MissAV'
extClass = '.MissAV' extClass = '.MissAV'
extVersionCode = 16 extVersionCode = 15
isNsfw = true isNsfw = true
} }

View file

@ -1,7 +1,7 @@
ext { ext {
extName = 'Sudatchi' extName = 'Sudatchi'
extClass = '.Sudatchi' extClass = '.Sudatchi'
extVersionCode = 13 extVersionCode = 12
isNsfw = true isNsfw = true
} }

View file

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

View file

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

View file

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

View file

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

View file

@ -1,7 +1,7 @@
ext { ext {
extName = 'Animerco' extName = 'Animerco'
extClass = '.Animerco' extClass = '.Animerco'
extVersionCode = 43 extVersionCode = 41
} }
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 = 17
} }
apply from: "$rootDir/common.gradle" apply from: "$rootDir/common.gradle"

View file

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

View file

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

View file

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

View file

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

View file

@ -1,7 +1,7 @@
ext { ext {
extName = 'MY CIMA' extName = 'MY CIMA'
extClass = '.MyCima' extClass = '.MyCima'
extVersionCode = 24 extVersionCode = 23
} }
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 = 13
} }
apply from: "$rootDir/common.gradle" apply from: "$rootDir/common.gradle"

View file

@ -1,7 +1,7 @@
ext { ext {
extName = 'WIT ANIME' extName = 'WIT ANIME'
extClass = '.WitAnime' extClass = '.WitAnime'
extVersionCode = 53 extVersionCode = 51
} }
apply from: "$rootDir/common.gradle" apply from: "$rootDir/common.gradle"

View file

@ -30,7 +30,7 @@ class WitAnime : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
override val name = "WIT ANIME" override val name = "WIT ANIME"
override val baseUrl = "https://witanime.cyou" override val baseUrl = "https://witanime.pics"
override val lang = "ar" override val lang = "ar"

View file

@ -1,7 +1,7 @@
ext { ext {
extName = 'Anime-Base' extName = 'Anime-Base'
extClass = '.AnimeBase' extClass = '.AnimeBase'
extVersionCode = 33 extVersionCode = 31
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 = 17
isNsfw = true isNsfw = true
} }

View file

@ -1,7 +1,7 @@
ext { ext {
extName = 'AnimeToast' extName = 'AnimeToast'
extClass = '.AnimeToast' extClass = '.AnimeToast'
extVersionCode = 23 extVersionCode = 21
} }
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 = 25
} }
apply from: "$rootDir/common.gradle" apply from: "$rootDir/common.gradle"

View file

@ -3,7 +3,7 @@ ext {
extClass = '.Cinemathek' extClass = '.Cinemathek'
themePkg = 'dooplay' themePkg = 'dooplay'
baseUrl = 'https://cinemathek.net' baseUrl = 'https://cinemathek.net'
overrideVersionCode = 26 overrideVersionCode = 24
isNsfw = true isNsfw = true
} }

View file

@ -1,7 +1,7 @@
ext { ext {
extName = 'Einfach' extName = 'Einfach'
extClass = '.Einfach' extClass = '.Einfach'
extVersionCode = 18 extVersionCode = 16
} }
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 = 19
} }
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 = 23
} }
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 = 13
} }
apply from: "$rootDir/common.gradle" apply from: "$rootDir/common.gradle"

View file

@ -1,7 +1,7 @@
ext { ext {
extName = 'Moflix-Stream' extName = 'Moflix-Stream'
extClass = '.MoflixStream' extClass = '.MoflixStream'
extVersionCode = 17 extVersionCode = 15
} }
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 = 11
} }
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 = 24
} }
apply from: "$rootDir/common.gradle" apply from: "$rootDir/common.gradle"

View file

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

View file

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

View file

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

View file

@ -3,7 +3,7 @@ ext {
extClass = '.Animenosub' extClass = '.Animenosub'
themePkg = 'animestream' themePkg = 'animestream'
baseUrl = 'https://animenosub.com' baseUrl = 'https://animenosub.com'
overrideVersionCode = 10 overrideVersionCode = 8
isNsfw = true isNsfw = true
} }

View file

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

View file

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

View file

@ -162,7 +162,6 @@ class AnimePahe : ConfigurableAnimeSource, AnimeHttpSource() {
episode.name = "Episode ${index + 1}" episode.name = "Episode ${index + 1}"
episode episode
} }
.reversed()
} }
private fun parseEpisodePage(episodes: List<EpisodeDto>, animeSession: String): MutableList<SEpisode> { private fun parseEpisodePage(episodes: List<EpisodeDto>, animeSession: String): MutableList<SEpisode> {

View file

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

View file

@ -2,7 +2,7 @@ ext {
extName = 'AniPlay' extName = 'AniPlay'
extClass = '.AniPlay' extClass = '.AniPlay'
themePkg = 'anilist' themePkg = 'anilist'
overrideVersionCode = 20 overrideVersionCode = 17
} }
apply from: "$rootDir/common.gradle" apply from: "$rootDir/common.gradle"

View file

@ -285,8 +285,9 @@ class AniPlay : AniListAnimeHttpSource(), ConfigurableAnimeSource {
try { try {
when (serverName) { when (serverName) {
"Yuki" -> { // yuki
val url = "https://yukiproxy.aniplaynow.live/m3u8-proxy?url=${defaultSource.url}&headers={\"Referer\":\"https://megacloud.club/\"}" PREF_SERVER_ENTRIES[1] -> {
val url = "https://yukiprox.aniplaynow.live/m3u8-proxy?url=${defaultSource.url}&headers={\"Referer\":\"https://megacloud.club/\"}"
return playlistUtils.extractFromHls( return playlistUtils.extractFromHls(
playlistUrl = url, playlistUrl = url,
videoNameGen = { quality -> "$serverName - $quality - $typeName" }, videoNameGen = { quality -> "$serverName - $quality - $typeName" },
@ -307,8 +308,9 @@ class AniPlay : AniListAnimeHttpSource(), ConfigurableAnimeSource {
}, },
) )
} }
"Pahe" -> { // pahe
val url = "https://paheproxy.aniplaynow.live/proxy?url=${defaultSource.url}&headers={\"Referer\":\"https://kwik.si/\"}" PREF_SERVER_ENTRIES[2] -> {
val url = "https://prox.aniplaynow.live/?url=${defaultSource.url}&ref=https://kwik.si"
val headers = headers.newBuilder().apply { val headers = headers.newBuilder().apply {
set("Accept", "*/*") set("Accept", "*/*")
set("Origin", baseUrl) set("Origin", baseUrl)
@ -563,8 +565,8 @@ class AniPlay : AniListAnimeHttpSource(), ConfigurableAnimeSource {
private const val PREF_DOMAIN_DEFAULT = "aniplaynow.live" private const val PREF_DOMAIN_DEFAULT = "aniplaynow.live"
private const val PREF_SERVER_KEY = "server" private const val PREF_SERVER_KEY = "server"
private val PREF_SERVER_ENTRIES = arrayOf("Pahe", "Yuki") // , "Hika") private val PREF_SERVER_ENTRIES = arrayOf("Maze", "Yuki", "Pahe", "Kuro")
private val PREF_SERVER_ENTRY_VALUES = arrayOf("pahe", "yuki") // , "hika") private val PREF_SERVER_ENTRY_VALUES = arrayOf("maze", "yuki", "pahe", "kuro")
private const val PREF_SERVER_DEFAULT = "yuki" private const val PREF_SERVER_DEFAULT = "yuki"
private const val SERVER_UNKNOWN = "Other" private const val SERVER_UNKNOWN = "Other"

View file

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

View file

@ -1,40 +0,0 @@
package eu.kanade.tachiyomi.animeextension.en.aniwatch
import eu.kanade.tachiyomi.animesource.model.SAnime
import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.lib.megacloudextractor.MegaCloudExtractor
import eu.kanade.tachiyomi.multisrc.zorotheme.ZoroTheme
import eu.kanade.tachiyomi.network.GET
import okhttp3.Request
import org.jsoup.nodes.Element
class AniWatchtv : ZoroTheme(
"en",
"AniWatchtv",
"https://aniwatchtv.to",
hosterNames = listOf(
"VidSrc",
"MegaCloud",
),
) {
override val id = 8051984946387208343L
override val ajaxRoute = "/v2"
private val megaCloudExtractor by lazy { MegaCloudExtractor(client, headers, preferences) }
override fun latestUpdatesRequest(page: Int): Request = GET("$baseUrl/recently-updated?page=$page", docHeaders)
override fun popularAnimeFromElement(element: Element): SAnime {
return super.popularAnimeFromElement(element).apply {
url = url.substringBefore("?")
}
}
override fun extractVideo(server: VideoData): List<Video> {
return when (server.name) {
"VidSrc", "MegaCloud" -> megaCloudExtractor.getVideosFromUrl(server.link, server.type, server.name)
else -> emptyList()
}
}
}

View file

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

View file

@ -3,7 +3,7 @@ ext {
extClass = '.DonghuaStream' extClass = '.DonghuaStream'
themePkg = 'animestream' themePkg = 'animestream'
baseUrl = 'https://donghuastream.org' baseUrl = 'https://donghuastream.org'
overrideVersionCode = 10 overrideVersionCode = 9
} }
apply from: "$rootDir/common.gradle" apply from: "$rootDir/common.gradle"

View file

@ -3,7 +3,7 @@ ext {
extClass = '.DopeBox' extClass = '.DopeBox'
themePkg = 'dopeflix' themePkg = 'dopeflix'
baseUrl = 'https://dopebox.to' baseUrl = 'https://dopebox.to'
overrideVersionCode = 12 overrideVersionCode = 11
} }
apply from: "$rootDir/common.gradle" apply from: "$rootDir/common.gradle"

View file

@ -1,12 +0,0 @@
ext {
extName = 'JPFilms'
extClass = '.JPFilms'
extVersionCode = 1
isNsfw = true
}
apply from: "$rootDir/common.gradle"
dependencies {
implementation(project(":lib:playlist-utils"))
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

Some files were not shown because too many files have changed in this diff Show more