Compare commits
78 commits
Sphereso/v
...
main
Author | SHA1 | Date | |
---|---|---|---|
837c1826ff | |||
e5e8761230 | |||
ce3522ff69 | |||
0295eaef40 | |||
c96032233f | |||
a2f7b2a077 | |||
584c2e6b53 | |||
a10e63df38 | |||
09533654d5 | |||
ec77adae21 | |||
5167cba9bf | |||
31d422a8ca | |||
2d87503f99 | |||
8ef18b236a | |||
89c957d1ec | |||
a873046b39 | |||
015eb36372 | |||
3ffb365748 | |||
67672ea590 | |||
afccef1e7d | |||
4158a18ba6 | |||
3db3d04443 | |||
8738e281d8 | |||
808ad21843 | |||
7d7aca37d8 | |||
0c2df7e6af | |||
1cfee212d7 | |||
78b864fc50 | |||
8223b8d3fa | |||
2904ce54e9 | |||
d1a72a27c6 | |||
724c661a25 | |||
0c844c9649 | |||
1b9fabf062 | |||
0fc8a33f3e | |||
627c34f404 | |||
1de715339a | |||
f789a38449 | |||
4782731860 | |||
ed7858a223 | |||
51b027fd78 | |||
ea5dd44967 | |||
2cc07ffe89 | |||
4f86e01e36 | |||
46fe6d9c11 | |||
4f7456323d | |||
fc37330c4f | |||
14132789f4 | |||
1b8adc6b13 | |||
5d6c3fff93 | |||
db54be041e | |||
40570cf6ae | |||
8fd317320e | |||
9e592e90ab | |||
0bddcda190 | |||
f096796f42 | |||
709104f0b9 | |||
e029421762 | |||
a95087c637 | |||
49400c0ceb | |||
75c7bf2d22 | |||
fd8a3c4619 | |||
![]() |
38368f56ac | ||
![]() |
66ca8544d3 | ||
![]() |
edd2fba397 | ||
![]() |
c82b40bb41 | ||
![]() |
26dbc15b45 | ||
![]() |
82a8b81de2 | ||
![]() |
a4d3a117cf | ||
![]() |
dd28b05f01 | ||
![]() |
36a480cd46 | ||
![]() |
b93c551b30 | ||
![]() |
b490d21610 | ||
![]() |
5d902c3576 | ||
![]() |
2f53d6b581 | ||
![]() |
7b73c023a6 | ||
![]() |
821cbc1d59 | ||
![]() |
45cff438ce |
2
.github/workflows/batch_close_issues.yml
vendored
|
@ -10,7 +10,7 @@ on:
|
|||
|
||||
jobs:
|
||||
stale:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: docker
|
||||
steps:
|
||||
- uses: actions/stale@28ca1036281a5e5922ead5184a1bbf96e5fc984e # v9
|
||||
with:
|
||||
|
|
17
.github/workflows/build_pull_request.yml
vendored
|
@ -18,15 +18,12 @@ env:
|
|||
jobs:
|
||||
prepare:
|
||||
name: Prepare job
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: docker
|
||||
outputs:
|
||||
individualMatrix: ${{ steps.generate-matrices.outputs.individualMatrix }}
|
||||
steps:
|
||||
- name: Clone repo
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4
|
||||
|
||||
- name: Validate Gradle Wrapper
|
||||
uses: gradle/wrapper-validation-action@a494d935f4b56874c4a5a87d19af7afcf3a163d0 # v2
|
||||
uses: https://github.com/actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4
|
||||
|
||||
- name: Get number of modules
|
||||
run: |
|
||||
|
@ -37,7 +34,7 @@ jobs:
|
|||
|
||||
- id: generate-matrices
|
||||
name: Create output matrices
|
||||
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7
|
||||
uses: https://github.com/actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7
|
||||
with:
|
||||
script: |
|
||||
const numIndividualModules = process.env.NUM_INDIVIDUAL_MODULES;
|
||||
|
@ -52,21 +49,21 @@ jobs:
|
|||
build_individual:
|
||||
name: Build individual modules
|
||||
needs: prepare
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: docker
|
||||
strategy:
|
||||
matrix: ${{ fromJSON(needs.prepare.outputs.individualMatrix) }}
|
||||
steps:
|
||||
- name: Checkout PR
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4
|
||||
uses: https://github.com/actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4
|
||||
|
||||
- name: Set up JDK
|
||||
uses: actions/setup-java@387ac29b308b003ca37ba93a6cab5eb57c8f5f93 # v4
|
||||
uses: https://github.com/actions/setup-java@387ac29b308b003ca37ba93a6cab5eb57c8f5f93 # v4
|
||||
with:
|
||||
java-version: 17
|
||||
distribution: temurin
|
||||
|
||||
- name: Set up Gradle
|
||||
uses: gradle/actions/setup-gradle@417ae3ccd767c252f5661f1ace9f835f9654f2b5 # v3
|
||||
uses: https://github.com/gradle/actions/setup-gradle@417ae3ccd767c252f5661f1ace9f835f9654f2b5 # v3
|
||||
with:
|
||||
cache-read-only: true
|
||||
|
||||
|
|
57
.github/workflows/build_push.yml
vendored
|
@ -21,15 +21,22 @@ env:
|
|||
jobs:
|
||||
prepare:
|
||||
name: Prepare job
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-22.04
|
||||
outputs:
|
||||
individualMatrix: ${{ steps.generate-matrices.outputs.individualMatrix }}
|
||||
steps:
|
||||
- name: Clone repo
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4
|
||||
uses: https://github.com/actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4
|
||||
with:
|
||||
ref: main
|
||||
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
|
||||
# - name: Find lib changes
|
||||
|
@ -42,7 +49,7 @@ jobs:
|
|||
# safe_output: false
|
||||
|
||||
- name: Import GPG key
|
||||
uses: crazy-max/ghaction-import-gpg@v6 # v6.1.0
|
||||
uses: https://github.com/crazy-max/ghaction-import-gpg@v6 # v6.1.0
|
||||
with:
|
||||
gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }}
|
||||
passphrase: ${{ secrets.GPG_PASSPHRASE }}
|
||||
|
@ -55,20 +62,10 @@ jobs:
|
|||
# run: |
|
||||
# chmod +x ./.github/scripts/bump-versions.py
|
||||
# ./.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
|
||||
name: Create output matrices
|
||||
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7
|
||||
uses: https://github.com/actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7
|
||||
with:
|
||||
script: |
|
||||
const numIndividualModules = process.env.NUM_INDIVIDUAL_MODULES;
|
||||
|
@ -83,17 +80,17 @@ jobs:
|
|||
build_individual:
|
||||
name: Build individual modules
|
||||
needs: prepare
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-22.04
|
||||
strategy:
|
||||
matrix: ${{ fromJSON(needs.prepare.outputs.individualMatrix) }}
|
||||
steps:
|
||||
- name: Checkout main branch
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4
|
||||
uses: https://github.com/actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4
|
||||
with:
|
||||
ref: main
|
||||
|
||||
- name: Set up JDK
|
||||
uses: actions/setup-java@387ac29b308b003ca37ba93a6cab5eb57c8f5f93 # v4
|
||||
uses: https://github.com/actions/setup-java@387ac29b308b003ca37ba93a6cab5eb57c8f5f93 # v4
|
||||
with:
|
||||
java-version: 17
|
||||
distribution: temurin
|
||||
|
@ -103,7 +100,7 @@ jobs:
|
|||
echo ${{ secrets.SIGNING_KEY }} | base64 -d > signingkey.jks
|
||||
|
||||
- name: Set up Gradle
|
||||
uses: gradle/actions/setup-gradle@417ae3ccd767c252f5661f1ace9f835f9654f2b5 # v3
|
||||
uses: https://github.com/gradle/actions/setup-gradle@245c8a24de79c0dbeabaf19ebcbbd3b2c36f278d # v4
|
||||
|
||||
- name: Build extensions (chunk ${{ matrix.chunk }})
|
||||
env:
|
||||
|
@ -114,7 +111,7 @@ jobs:
|
|||
run: chmod +x ./gradlew && ./gradlew -p src assembleRelease
|
||||
|
||||
- name: Upload APKs (chunk ${{ matrix.chunk }})
|
||||
uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4
|
||||
uses: https://github.com/actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4
|
||||
if: "github.repository == 'Kohi-den/extensions-source'"
|
||||
with:
|
||||
name: "individual-apks-${{ matrix.chunk }}"
|
||||
|
@ -128,22 +125,22 @@ jobs:
|
|||
name: Publish repo
|
||||
needs:
|
||||
- build_individual
|
||||
if: "github.repository == 'Kohi-den/extensions-source'"
|
||||
runs-on: ubuntu-latest
|
||||
if: "github.repository == 'AlmightyHak/extensions-source'"
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Download APK artifacts
|
||||
uses: actions/download-artifact@eaceaf801fd36c7dee90939fad912460b18a1ffe # v4
|
||||
uses: https://github.com/actions/download-artifact@eaceaf801fd36c7dee90939fad912460b18a1ffe # v4
|
||||
with:
|
||||
path: ~/apk-artifacts
|
||||
|
||||
- name: Set up JDK
|
||||
uses: actions/setup-java@387ac29b308b003ca37ba93a6cab5eb57c8f5f93 # v4
|
||||
uses: https://github.com/actions/setup-java@387ac29b308b003ca37ba93a6cab5eb57c8f5f93 # v4
|
||||
with:
|
||||
java-version: 17
|
||||
distribution: temurin
|
||||
|
||||
- name: Checkout main branch
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4
|
||||
uses: https://github.com/actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4
|
||||
with:
|
||||
ref: main
|
||||
path: main
|
||||
|
@ -158,9 +155,9 @@ jobs:
|
|||
python ./.github/scripts/create-repo.py
|
||||
|
||||
- name: Checkout repo branch
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4
|
||||
uses: https://github.com/actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4
|
||||
with:
|
||||
repository: Kohi-den/extensions
|
||||
repository: AlmightyHak/extensions
|
||||
token: ${{ secrets.BOT_PAT }}
|
||||
ref: main
|
||||
path: repo
|
||||
|
@ -170,9 +167,9 @@ jobs:
|
|||
rsync -a --delete --exclude .git --exclude .gitignore main/repo/ repo --exclude README.md --exclude repo.json
|
||||
|
||||
- name: Deploy repo
|
||||
uses: EndBug/add-and-commit@v9
|
||||
uses: https://github.com/EndBug/add-and-commit@v9
|
||||
with:
|
||||
message: "Update extensions repo"
|
||||
cwd: "./repo"
|
||||
committer_name: Kohi-den-Bot
|
||||
committer_email: 177773202+Kohi-den-Bot@users.noreply.github.com
|
||||
committer_name: AlmightyHak
|
||||
committer_email: almightyhak@noreply.localhost
|
||||
|
|
2
.github/workflows/issue_moderator.yml
vendored
|
@ -8,7 +8,7 @@ on:
|
|||
|
||||
jobs:
|
||||
autoclose:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: docker
|
||||
steps:
|
||||
- name: Moderate issues
|
||||
uses: aniyomiorg/issue-moderator-action@v2
|
||||
|
|
2
.github/workflows/lock.yml
vendored
|
@ -10,7 +10,7 @@ on:
|
|||
|
||||
jobs:
|
||||
lock:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: docker
|
||||
permissions:
|
||||
issues: write
|
||||
steps:
|
||||
|
|
|
@ -2,9 +2,9 @@
|
|||
|
||||
just paste this into your anime repo
|
||||
```
|
||||
https://raw.githubusercontent.com/Kohi-den/extensions/main/index.min.json
|
||||
https://kohiden.xyz/AlmightyHak/extensions/raw/branch/main/index.min.json
|
||||
```
|
||||
If your interested in installing just the apks they can be found [Here](https://github.com/Kohi-den/extensions)
|
||||
If your interested in installing just the apks they can be found [Here](https://kohiden.xyz/AlmightyHak/extensions/src/branch/main/apk)
|
||||
|
||||
## Support Server
|
||||
|
||||
|
|
21111
SDK/NOTICE.txt
Normal file
BIN
SDK/adb
Normal file
BIN
SDK/etc1tool
Normal file
BIN
SDK/fastboot
Normal file
BIN
SDK/hprof-conv
Normal file
BIN
SDK/lib64/libc++.so
Normal file
2
SDK/licenses/android-sdk-license
Normal file
|
@ -0,0 +1,2 @@
|
|||
|
||||
24333f8a63b6825ea9c5514f83c2829b004d1fee
|
BIN
SDK/make_f2fs
Normal file
BIN
SDK/make_f2fs_casefold
Normal file
BIN
SDK/mke2fs
Normal file
53
SDK/mke2fs.conf
Normal file
|
@ -0,0 +1,53 @@
|
|||
[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
|
||||
}
|
2
SDK/source.properties
Normal file
|
@ -0,0 +1,2 @@
|
|||
Pkg.UserSrc=false
|
||||
Pkg.Revision=36.0.0
|
BIN
SDK/sqlite3
Normal file
|
@ -18,4 +18,4 @@ allprojects {
|
|||
jvmTarget = JavaVersion.VERSION_1_8.toString()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -2,4 +2,4 @@ plugins {
|
|||
id("lib-multisrc")
|
||||
}
|
||||
|
||||
baseVersionCode = 2
|
||||
baseVersionCode = 3
|
||||
|
|
|
@ -49,13 +49,13 @@ abstract class DooPlay(
|
|||
const val PREFIX_SEARCH = "path:"
|
||||
}
|
||||
|
||||
protected open val prefQualityDefault = "720p"
|
||||
protected open val prefQualityDefault = "1080p"
|
||||
protected open val prefQualityKey = "preferred_quality"
|
||||
protected open val prefQualityTitle = when (lang) {
|
||||
"pt-BR" -> "Qualidade preferida"
|
||||
else -> "Preferred quality"
|
||||
}
|
||||
protected open val prefQualityValues = arrayOf("480p", "720p")
|
||||
protected open val prefQualityValues = arrayOf("360p", "480p", "720p", "1080p")
|
||||
protected open val prefQualityEntries = prefQualityValues
|
||||
|
||||
protected open val videoSortPrefKey = prefQualityKey
|
||||
|
|
|
@ -2,7 +2,7 @@ plugins {
|
|||
id("lib-multisrc")
|
||||
}
|
||||
|
||||
baseVersionCode = 5
|
||||
baseVersionCode = 6
|
||||
|
||||
dependencies {
|
||||
api(project(":lib:megacloud-extractor"))
|
||||
|
|
3
lib/buzzheavier-extractor/build.gradle.kts
Normal file
|
@ -0,0 +1,3 @@
|
|||
plugins {
|
||||
id("lib-android")
|
||||
}
|
|
@ -0,0 +1,82 @@
|
|||
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)
|
||||
}
|
|
@ -1,73 +1,54 @@
|
|||
package eu.kanade.tachiyomi.lib.chillxextractor
|
||||
|
||||
import android.util.Log
|
||||
import eu.kanade.tachiyomi.animesource.model.Track
|
||||
import eu.kanade.tachiyomi.animesource.model.Video
|
||||
import eu.kanade.tachiyomi.lib.cryptoaes.CryptoAES
|
||||
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.OkHttpClient
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
|
||||
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 webViewResolver by lazy { WebViewResolver(client, headers) }
|
||||
|
||||
companion object {
|
||||
private val REGEX_MASTER_JS = Regex("""\s*=\s*'([^']+)""")
|
||||
private val REGEX_SOURCES = Regex("""sources:\s*\[\{"file":"([^"]+)""")
|
||||
private val REGEX_FILE = Regex("""file: ?"([^"]+)"""")
|
||||
private val REGEX_SOURCE = Regex("""source = ?"([^"]+)"""")
|
||||
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, referer: String, prefix: String = "Chillx - "): List<Video> {
|
||||
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()
|
||||
fun videoFromUrl(url: String, prefix: String = "Chillx - "): List<Video> {
|
||||
val data = webViewResolver.getDecryptedData(url) ?: return emptyList()
|
||||
|
||||
val body = client.newCall(GET(url, newHeaders)).execute().body.string()
|
||||
|
||||
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)
|
||||
val masterUrl = REGEX_SOURCES.find(data)?.groupValues?.get(1)
|
||||
?: REGEX_FILE.find(data)?.groupValues?.get(1)
|
||||
?: REGEX_SOURCE.find(data)?.groupValues?.get(1)
|
||||
?: return emptyList()
|
||||
|
||||
val subtitleList = buildList {
|
||||
val subtitles = REGEX_SUBS.findAll(decryptedScript)
|
||||
val subtitles = REGEX_SUBS.findAll(data)
|
||||
subtitles.forEach {
|
||||
Log.d("ChillxExtractor", "Found subtitle: ${it.groupValues}")
|
||||
add(Track(it.groupValues[1], decodeUnicodeEscape(it.groupValues[2])))
|
||||
}
|
||||
}
|
||||
|
||||
return playlistUtils.extractFromHls(
|
||||
val videoList = playlistUtils.extractFromHls(
|
||||
playlistUrl = masterUrl,
|
||||
referer = url,
|
||||
videoNameGen = { "$prefix$it" },
|
||||
subtitleList = subtitleList,
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalSerializationApi::class)
|
||||
private fun fetchKey(): String? {
|
||||
return client.newCall(GET(KEY_SOURCE)).execute().parseAs<KeysData>().keys.firstOrNull()
|
||||
return videoList.map {
|
||||
Video(
|
||||
url = it.url,
|
||||
quality = it.quality,
|
||||
videoUrl = it.videoUrl,
|
||||
headers = it.headers,
|
||||
audioTracks = it.audioTracks,
|
||||
subtitleTracks = playlistUtils.fixSubtitles(it.subtitleTracks),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun decodeUnicodeEscape(input: String): String {
|
||||
|
@ -76,16 +57,4 @@ class ChillxExtractor(private val client: OkHttpClient, private val headers: Hea
|
|||
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)
|
||||
|
|
|
@ -0,0 +1,125 @@
|
|||
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()),
|
||||
)
|
||||
}
|
||||
}
|
|
@ -1,5 +1,8 @@
|
|||
package eu.kanade.tachiyomi.lib.filemoonextractor
|
||||
|
||||
import android.content.SharedPreferences
|
||||
import androidx.preference.EditTextPreference
|
||||
import androidx.preference.PreferenceScreen
|
||||
import dev.datlag.jsunpacker.JsUnpacker
|
||||
import eu.kanade.tachiyomi.animesource.model.Track
|
||||
import eu.kanade.tachiyomi.animesource.model.Video
|
||||
|
@ -13,20 +16,28 @@ import okhttp3.HttpUrl.Companion.toHttpUrl
|
|||
import okhttp3.OkHttpClient
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
|
||||
class FilemoonExtractor(private val client: OkHttpClient) {
|
||||
class FilemoonExtractor(
|
||||
private val client: OkHttpClient,
|
||||
private val preferences: SharedPreferences? = null,
|
||||
) {
|
||||
|
||||
private val playlistUtils by lazy { PlaylistUtils(client) }
|
||||
private val json: Json by injectLazy()
|
||||
|
||||
fun videosFromUrl(url: String, prefix: String = "Filemoon - ", headers: Headers? = null): List<Video> {
|
||||
val httpUrl = url.toHttpUrl()
|
||||
var 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 jsEval = doc.selectFirst("script:containsData(eval):containsData(m3u8)")!!.data()
|
||||
val jsEval = doc.selectFirst("script:containsData(eval):containsData(m3u8)")?.data() ?: run {
|
||||
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 masterUrl = unpacked.takeIf(String::isNotBlank)
|
||||
?.substringAfter("{file:\"", "")
|
||||
|
@ -50,14 +61,39 @@ class FilemoonExtractor(private val client: OkHttpClient) {
|
|||
}
|
||||
}
|
||||
|
||||
return playlistUtils.extractFromHls(
|
||||
val videoList = playlistUtils.extractFromHls(
|
||||
masterUrl,
|
||||
subtitleList = subtitleTracks,
|
||||
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) }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,7 +10,6 @@ import okhttp3.HttpUrl.Companion.toHttpUrl
|
|||
import okhttp3.OkHttpClient
|
||||
import okhttp3.internal.commonEmptyHeaders
|
||||
import java.io.File
|
||||
import kotlin.math.abs
|
||||
|
||||
class PlaylistUtils(private val client: OkHttpClient, private val headers: Headers = commonEmptyHeaders) {
|
||||
|
||||
|
@ -138,7 +137,7 @@ class PlaylistUtils(private val client: OkHttpClient, private val headers: Heade
|
|||
val resolution = it.substringAfter("RESOLUTION=")
|
||||
.substringBefore("\n")
|
||||
.substringAfter("x")
|
||||
.substringBefore(",").let(::stnQuality)
|
||||
.substringBefore(",")
|
||||
|
||||
val videoUrl = it.substringAfter("\n").substringBefore("\n").let { url ->
|
||||
getAbsoluteUrl(url, playlistUrl, masterUrlBasePath)?.trimEnd()
|
||||
|
@ -337,13 +336,6 @@ class PlaylistUtils(private val client: OkHttpClient, private val headers: Heade
|
|||
|
||||
// ============================= 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 {
|
||||
val lineCount = matchResult.groupValues[1].count { it == '\n' }
|
||||
return "\n" + " \n".repeat(lineCount - 1)
|
||||
|
|
7
lib/savefile-extractor/build.gradle.kts
Normal file
|
@ -0,0 +1,7 @@
|
|||
plugins {
|
||||
id("lib-android")
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(project(":lib:playlist-utils"))
|
||||
}
|
|
@ -0,0 +1,67 @@
|
|||
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"
|
||||
}
|
||||
}
|
|
@ -10,6 +10,7 @@ import kotlinx.serialization.Serializable
|
|||
import kotlinx.serialization.SerializationException
|
||||
import kotlinx.serialization.json.Json
|
||||
import okhttp3.Headers
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import okhttp3.OkHttpClient
|
||||
|
||||
class StreamWishExtractor(private val client: OkHttpClient, private val headers: Headers) {
|
||||
|
@ -30,16 +31,19 @@ class StreamWishExtractor(private val client: OkHttpClient, private val headers:
|
|||
script
|
||||
}
|
||||
}
|
||||
val masterUrl = scriptBody
|
||||
?.substringAfter("source", "")
|
||||
?.substringAfter("file:\"", "")
|
||||
?.substringBefore("\"", "")
|
||||
?.takeIf(String::isNotBlank)
|
||||
val masterUrl = scriptBody?.let {
|
||||
M3U8_REGEX.find(it)?.value
|
||||
}
|
||||
?: return emptyList()
|
||||
|
||||
val subtitleList = extractSubtitles(scriptBody)
|
||||
|
||||
return playlistUtils.extractFromHls(masterUrl, url, videoNameGen = videoNameGen, subtitleList = subtitleList)
|
||||
return playlistUtils.extractFromHls(
|
||||
playlistUrl = masterUrl,
|
||||
referer = "https://${url.toHttpUrl().host}/",
|
||||
videoNameGen = videoNameGen,
|
||||
subtitleList = playlistUtils.fixSubtitles(subtitleList),
|
||||
)
|
||||
}
|
||||
|
||||
private fun getEmbedUrl(url: String): String {
|
||||
|
@ -57,7 +61,11 @@ class StreamWishExtractor(private val client: OkHttpClient, private val headers:
|
|||
.substringAfter("tracks")
|
||||
.substringAfter("[")
|
||||
.substringBefore("]")
|
||||
json.decodeFromString<List<TrackDto>>("[$subtitleStr]")
|
||||
val fixedSubtitleStr = FIX_TRACKS_REGEX.replace(subtitleStr) { match ->
|
||||
"\"${match.value}\""
|
||||
}
|
||||
|
||||
json.decodeFromString<List<TrackDto>>("[$fixedSubtitleStr]")
|
||||
.filter { it.kind.equals("captions", true) }
|
||||
.map { Track(it.file, it.label ?: "") }
|
||||
} catch (e: SerializationException) {
|
||||
|
@ -67,4 +75,7 @@ class StreamWishExtractor(private val client: OkHttpClient, private val headers:
|
|||
|
||||
@Serializable
|
||||
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)(?!["])""")
|
||||
}
|
||||
|
|
|
@ -18,6 +18,7 @@ import uy.kohesive.injekt.injectLazy
|
|||
import java.util.Locale
|
||||
import java.util.concurrent.CountDownLatch
|
||||
import java.util.concurrent.TimeUnit
|
||||
import kotlin.math.abs
|
||||
|
||||
class UniversalExtractor(private val client: OkHttpClient) {
|
||||
private val context: Application by injectLazy()
|
||||
|
@ -73,7 +74,7 @@ class UniversalExtractor(private val client: OkHttpClient) {
|
|||
|
||||
for (quality in qualities) {
|
||||
val modifiedUrl = resultUrl.replace("M3U8_AUTO_360", "M3U8_AUTO_$quality")
|
||||
val videos = playlistUtils.extractFromHls(modifiedUrl, origRequestUrl, videoNameGen = { "$prefix - $host: $it $quality" + "p" })
|
||||
val videos = playlistUtils.extractFromHls(modifiedUrl, origRequestUrl, videoNameGen = { "$prefix - $host: ${stnQuality(it)} $quality" + "p" })
|
||||
|
||||
if (videos.isNotEmpty()) {
|
||||
allVideos.addAll(videos)
|
||||
|
@ -89,7 +90,7 @@ class UniversalExtractor(private val client: OkHttpClient) {
|
|||
return when {
|
||||
"m3u8" in resultUrl -> {
|
||||
Log.d("UniversalExtractor", "m3u8 URL: $resultUrl")
|
||||
playlistUtils.extractFromHls(resultUrl, origRequestUrl, videoNameGen = { "$prefix - $host: $it" })
|
||||
playlistUtils.extractFromHls(resultUrl, origRequestUrl, videoNameGen = { "$prefix - $host: ${stnQuality(it)}" })
|
||||
}
|
||||
"mpd" in resultUrl -> {
|
||||
Log.d("UniversalExtractor", "mpd URL: $resultUrl")
|
||||
|
@ -103,6 +104,13 @@ 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 {
|
||||
return this.replaceFirstChar { if (it.isLowerCase()) it.titlecase(
|
||||
Locale.getDefault()) else it.toString() }
|
||||
|
|
|
@ -18,33 +18,14 @@ class VoeExtractor(private val client: OkHttpClient) {
|
|||
|
||||
private val playlistUtils by lazy { PlaylistUtils(clientDdos) }
|
||||
|
||||
private val linkRegex = "(http|https)://([\\w_-]+(?:\\.[\\w_-]+)+)([\\w.,@?^=%&:/~+#-]*[\\w@?^=%&/~+#-])".toRegex()
|
||||
|
||||
private val base64Regex = Regex("'.*'")
|
||||
|
||||
private val scriptBase64Regex = "(let|var)\\s+\\w+\\s*=\\s*'(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)';".toRegex()
|
||||
|
||||
@Serializable
|
||||
data class VideoLinkDTO(val 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
|
||||
}
|
||||
data class VideoLinkDTO(val file: String)
|
||||
|
||||
fun videosFromUrl(url: String, prefix: String = ""): List<Video> {
|
||||
var document = clientDdos.newCall(GET(url)).execute().asJsoup()
|
||||
|
@ -57,12 +38,25 @@ class VoeExtractor(private val client: OkHttpClient) {
|
|||
document = clientDdos.newCall(GET(originalUrl)).execute().asJsoup()
|
||||
}
|
||||
|
||||
val encodedVoeData = document.select("script").find { it.data().contains("MKGMa=\"")}?.data()
|
||||
?.substringAfter("MKGMa=\"")
|
||||
?.substringBefore('"') ?: return emptyList()
|
||||
|
||||
val playlistUrl = decodeVoeData(encodedVoeData)
|
||||
val alternativeScript = document.select("script").find { scriptBase64Regex.containsMatchIn(it.data()) }?.data()
|
||||
val script = document.selectFirst("script:containsData(const sources), script:containsData(var sources), script:containsData(wc0)")?.data()
|
||||
?: alternativeScript
|
||||
?: return emptyList()
|
||||
val playlistUrl = when {
|
||||
// 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,
|
||||
videoNameGen = { quality -> "${prefix}Voe:$quality" }
|
||||
)
|
||||
|
|
1
local.properties
Normal file
|
@ -0,0 +1 @@
|
|||
sdk.dir=/workspace/AlmightyHak/extensions-source/SDK
|
|
@ -19,8 +19,8 @@ if (System.getenv("CI") != "true") {
|
|||
} else {
|
||||
// Running in CI (GitHub Actions)
|
||||
|
||||
val chunkSize = System.getenv("CI_CHUNK_SIZE").toInt()
|
||||
val chunk = System.getenv("CI_CHUNK_NUM").toInt()
|
||||
val chunkSize = System.getenv("CI_CHUNK_SIZE")?.toIntOrNull() ?: Int.MAX_VALUE
|
||||
val chunk = System.getenv("CI_CHUNK_NUM")?.toIntOrNull() ?: 0
|
||||
|
||||
// Loads individual extensions
|
||||
File(rootDir, "src").getChunk(chunk, chunkSize)?.forEach {
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
ext {
|
||||
extName = 'AnimeWorld India'
|
||||
extClass = '.AnimeWorldIndiaFactory'
|
||||
extVersionCode = 15
|
||||
extVersionCode = 16
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
||||
|
|
|
@ -3,7 +3,7 @@ ext {
|
|||
extClass = '.AnimeXin'
|
||||
themePkg = 'animestream'
|
||||
baseUrl = 'https://animexin.vip'
|
||||
overrideVersionCode = 10
|
||||
overrideVersionCode = 11
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
ext {
|
||||
extName = 'AniZone'
|
||||
extClass = '.AniZone'
|
||||
extVersionCode = 3
|
||||
extVersionCode = 4
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
||||
|
|
|
@ -3,7 +3,7 @@ ext {
|
|||
extClass = '.ChineseAnime'
|
||||
themePkg = 'animestream'
|
||||
baseUrl = 'https://www.chineseanime.vip'
|
||||
overrideVersionCode = 13
|
||||
overrideVersionCode = 15
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
ext {
|
||||
extName = 'Hikari'
|
||||
extClass = '.Hikari'
|
||||
extVersionCode = 16
|
||||
extVersionCode = 22
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
||||
|
@ -9,6 +9,7 @@ apply from: "$rootDir/common.gradle"
|
|||
dependencies {
|
||||
implementation(project(':lib:chillx-extractor'))
|
||||
implementation(project(':lib:filemoon-extractor'))
|
||||
implementation(project(':lib:savefile-extractor'))
|
||||
implementation(project(':lib:buzzheavier-extractor'))
|
||||
implementation(project(':lib:streamwish-extractor'))
|
||||
implementation(project(':lib:vidhide-extractor'))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,118 @@
|
|||
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,
|
||||
)
|
|
@ -5,7 +5,7 @@ import okhttp3.HttpUrl
|
|||
import java.util.Calendar
|
||||
|
||||
interface UriFilter {
|
||||
fun addToUri(url: HttpUrl.Builder)
|
||||
fun addToUri(builder: HttpUrl.Builder)
|
||||
}
|
||||
|
||||
sealed class UriPartFilter(
|
||||
|
@ -20,7 +20,10 @@ sealed class UriPartFilter(
|
|||
),
|
||||
UriFilter {
|
||||
override fun addToUri(builder: HttpUrl.Builder) {
|
||||
builder.addQueryParameter(param, vals[state].second)
|
||||
val value = vals[state].second
|
||||
if (value.isNotEmpty()) {
|
||||
builder.addQueryParameter(param, value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -33,13 +36,15 @@ sealed class UriMultiSelectFilter(
|
|||
) : AnimeFilter.Group<UriMultiSelectOption>(name, vals.map { UriMultiSelectOption(it.first, it.second) }), UriFilter {
|
||||
override fun addToUri(builder: HttpUrl.Builder) {
|
||||
val checked = state.filter { it.state }
|
||||
builder.addQueryParameter(param, checked.joinToString(",") { it.value })
|
||||
if (checked.isNotEmpty()) {
|
||||
builder.addQueryParameter(param, checked.joinToString(",") { it.value })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class TypeFilter : UriPartFilter(
|
||||
"Type",
|
||||
"type",
|
||||
"ani_type",
|
||||
arrayOf(
|
||||
Pair("All", ""),
|
||||
Pair("TV", "1"),
|
||||
|
@ -50,165 +55,53 @@ class TypeFilter : UriPartFilter(
|
|||
),
|
||||
)
|
||||
|
||||
class CountryFilter : UriPartFilter(
|
||||
"Country",
|
||||
"country",
|
||||
arrayOf(
|
||||
Pair("All", ""),
|
||||
Pair("Japanese", "1"),
|
||||
Pair("Chinese", "2"),
|
||||
),
|
||||
)
|
||||
|
||||
class StatusFilter : UriPartFilter(
|
||||
"Status",
|
||||
"stats",
|
||||
"ani_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"),
|
||||
Pair("Ongoing", "1"),
|
||||
Pair("Completed", "2"),
|
||||
Pair("Upcoming", "3"),
|
||||
),
|
||||
)
|
||||
|
||||
class SeasonFilter : UriPartFilter(
|
||||
"Season",
|
||||
"season",
|
||||
"ani_release_season",
|
||||
arrayOf(
|
||||
Pair("All", ""),
|
||||
Pair("Spring", "1"),
|
||||
Pair("Summer", "2"),
|
||||
Pair("Fall", "3"),
|
||||
Pair("Winter", "4"),
|
||||
Pair("Winter", "1"),
|
||||
Pair("Spring", "2"),
|
||||
Pair("Summer", "3"),
|
||||
Pair("Fall", "4"),
|
||||
),
|
||||
)
|
||||
|
||||
class LanguageFilter : UriPartFilter(
|
||||
"Language",
|
||||
"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,
|
||||
class YearFilter : UriPartFilter(
|
||||
"Release Year",
|
||||
"ani_release",
|
||||
YEARS,
|
||||
) {
|
||||
companion object {
|
||||
private val NEXT_YEAR by lazy {
|
||||
Calendar.getInstance()[Calendar.YEAR] + 1
|
||||
private val CURRENT_YEAR by lazy {
|
||||
Calendar.getInstance()[Calendar.YEAR]
|
||||
}
|
||||
|
||||
private val YEARS = Array(NEXT_YEAR - 1917) { year ->
|
||||
if (year == 0) {
|
||||
Pair("Any", "")
|
||||
} else {
|
||||
(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"),
|
||||
)
|
||||
private val YEARS = buildList {
|
||||
add(Pair("Any", ""))
|
||||
addAll(
|
||||
(1990..CURRENT_YEAR).map {
|
||||
Pair(it.toString(), it.toString())
|
||||
},
|
||||
)
|
||||
}.toTypedArray()
|
||||
}
|
||||
}
|
||||
|
||||
class GenreFilter : UriMultiSelectFilter(
|
||||
"Genre",
|
||||
"genres",
|
||||
"ani_genre",
|
||||
arrayOf(
|
||||
Pair("Action", "Action"),
|
||||
Pair("Adventure", "Adventure"),
|
||||
|
@ -233,7 +126,7 @@ class GenreFilter : UriMultiSelectFilter(
|
|||
Pair("Music", "Music"),
|
||||
Pair("Mystery", "Mystery"),
|
||||
Pair("Parody", "Parody"),
|
||||
Pair("Police", "Police"),
|
||||
Pair("Policy", "Policy"),
|
||||
Pair("Psychological", "Psychological"),
|
||||
Pair("Romance", "Romance"),
|
||||
Pair("Samurai", "Samurai"),
|
||||
|
@ -253,3 +146,12 @@ class GenreFilter : UriMultiSelectFilter(
|
|||
Pair("Vampire", "Vampire"),
|
||||
),
|
||||
)
|
||||
|
||||
class LanguageFilter : UriPartFilter(
|
||||
"Language",
|
||||
"ani_genre",
|
||||
arrayOf(
|
||||
Pair("Any", ""),
|
||||
Pair("Portuguese", "Portuguese"),
|
||||
),
|
||||
)
|
||||
|
|
|
@ -1,48 +1,45 @@
|
|||
package eu.kanade.tachiyomi.animeextension.all.hikari
|
||||
|
||||
import android.app.Application
|
||||
import android.util.Log
|
||||
import android.content.SharedPreferences
|
||||
import androidx.preference.ListPreference
|
||||
import androidx.preference.MultiSelectListPreference
|
||||
import androidx.preference.PreferenceScreen
|
||||
import androidx.preference.SwitchPreferenceCompat
|
||||
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.AnimesPage
|
||||
import eu.kanade.tachiyomi.animesource.model.SAnime
|
||||
import eu.kanade.tachiyomi.animesource.model.SEpisode
|
||||
import eu.kanade.tachiyomi.animesource.model.Video
|
||||
import eu.kanade.tachiyomi.animesource.online.ParsedAnimeHttpSource
|
||||
import eu.kanade.tachiyomi.animesource.online.AnimeHttpSource
|
||||
import eu.kanade.tachiyomi.lib.buzzheavierextractor.BuzzheavierExtractor
|
||||
import eu.kanade.tachiyomi.lib.chillxextractor.ChillxExtractor
|
||||
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.vidhideextractor.VidHideExtractor
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.util.parallelCatchingFlatMapBlocking
|
||||
import eu.kanade.tachiyomi.util.parseAs
|
||||
import kotlinx.serialization.Serializable
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import okhttp3.Request
|
||||
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.api.get
|
||||
|
||||
class Hikari : ParsedAnimeHttpSource(), ConfigurableAnimeSource {
|
||||
class Hikari : AnimeHttpSource(), ConfigurableAnimeSource {
|
||||
|
||||
override val name = "Hikari"
|
||||
|
||||
override val baseUrl = "https://watch.hikaritv.xyz"
|
||||
private val proxyUrl = "https://hikari.gg/hiki-proxy/extract/"
|
||||
private val apiUrl = "https://api.hikari.gg/api"
|
||||
override val baseUrl = "https://hikari.gg"
|
||||
|
||||
override val lang = "all"
|
||||
|
||||
override val supportsLatest = true
|
||||
override val versionId = 2
|
||||
|
||||
override fun headersBuilder() = super.headersBuilder().apply {
|
||||
add("Origin", baseUrl)
|
||||
add("Referer", "$baseUrl/")
|
||||
}
|
||||
override val supportsLatest = true
|
||||
|
||||
private val preferences by lazy {
|
||||
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
|
||||
|
@ -50,75 +47,40 @@ class Hikari : ParsedAnimeHttpSource(), ConfigurableAnimeSource {
|
|||
|
||||
// ============================== Popular ===============================
|
||||
|
||||
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 popularAnimeRequest(page: Int) = searchAnimeRequest(page, "", AnimeFilterList())
|
||||
|
||||
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
|
||||
override fun popularAnimeParse(response: Response) = searchAnimeParse(response)
|
||||
|
||||
// =============================== Latest ===============================
|
||||
|
||||
override fun latestUpdatesRequest(page: Int): Request {
|
||||
val url = "$baseUrl/ajax/getfilter?type=&country=&stats=&rate=&source=&season=&language=&aired_year=&aired_month=&aired_day=&sort=recently_updated&genres=&page=$page"
|
||||
val headers = headersBuilder().set("Referer", "$baseUrl/filter").build()
|
||||
val url = "$apiUrl/episode/new/".toHttpUrl().newBuilder().apply {
|
||||
addQueryParameter("limit", "100")
|
||||
addQueryParameter("language", "EN")
|
||||
}.build()
|
||||
return GET(url, headers)
|
||||
}
|
||||
|
||||
override fun latestUpdatesParse(response: Response): AnimesPage =
|
||||
popularAnimeParse(response)
|
||||
override fun latestUpdatesParse(response: Response): AnimesPage {
|
||||
val data = response.parseAs<CatalogResponseDto<LatestEpisodeDto>>()
|
||||
val preferEnglish = preferences.getTitleLang
|
||||
|
||||
override fun latestUpdatesSelector(): String =
|
||||
throw UnsupportedOperationException()
|
||||
|
||||
override fun latestUpdatesFromElement(element: Element): SAnime =
|
||||
throw UnsupportedOperationException()
|
||||
|
||||
override fun latestUpdatesNextPageSelector(): String =
|
||||
throw UnsupportedOperationException()
|
||||
val animeList = data.results.distinctBy { it.uid }.map { it.toSAnime(preferEnglish) }
|
||||
return AnimesPage(animeList, false)
|
||||
}
|
||||
|
||||
// =============================== Search ===============================
|
||||
|
||||
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
|
||||
val url = baseUrl.toHttpUrl().newBuilder().apply {
|
||||
if (query.isNotEmpty()) {
|
||||
addPathSegment("search")
|
||||
addQueryParameter("keyword", query)
|
||||
addQueryParameter("page", page.toString())
|
||||
} else {
|
||||
addPathSegment("ajax")
|
||||
addPathSegment("getfilter")
|
||||
filters.filterIsInstance<UriFilter>().forEach {
|
||||
it.addToUri(this)
|
||||
}
|
||||
addQueryParameter("page", page.toString())
|
||||
val url = "$apiUrl/anime/".toHttpUrl().newBuilder().apply {
|
||||
addQueryParameter("sort", "created_at")
|
||||
addQueryParameter("order", "asc")
|
||||
addQueryParameter("page", page.toString())
|
||||
filters.filterIsInstance<UriFilter>().forEach {
|
||||
it.addToUri(this)
|
||||
}
|
||||
}.build()
|
||||
|
||||
val headers = headersBuilder().apply {
|
||||
if (query.isNotEmpty()) {
|
||||
set("Referer", url.toString().substringBeforeLast("&page"))
|
||||
} else {
|
||||
set("Referer", "$baseUrl/filter")
|
||||
addQueryParameter("search", query)
|
||||
}
|
||||
}.build()
|
||||
|
||||
|
@ -126,280 +88,179 @@ class Hikari : ParsedAnimeHttpSource(), ConfigurableAnimeSource {
|
|||
}
|
||||
|
||||
override fun searchAnimeParse(response: Response): AnimesPage {
|
||||
return if (response.request.url.encodedPath.startsWith("/search")) {
|
||||
super.searchAnimeParse(response)
|
||||
} else {
|
||||
popularAnimeParse(response)
|
||||
}
|
||||
val data = response.parseAs<CatalogResponseDto<AnimeDto>>()
|
||||
val preferEnglish = preferences.getTitleLang
|
||||
|
||||
val animeList = data.results.map { it.toSAnime(preferEnglish) }
|
||||
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 ===============================
|
||||
|
||||
override fun getFilterList(): AnimeFilterList = AnimeFilterList(
|
||||
AnimeFilter.Header("Note: text search ignores filters"),
|
||||
AnimeFilter.Separator(),
|
||||
TypeFilter(),
|
||||
CountryFilter(),
|
||||
StatusFilter(),
|
||||
RatingFilter(),
|
||||
SourceFilter(),
|
||||
SeasonFilter(),
|
||||
LanguageFilter(),
|
||||
SortFilter(),
|
||||
AiringDateFilter(),
|
||||
YearFilter(),
|
||||
GenreFilter(),
|
||||
LanguageFilter(),
|
||||
)
|
||||
|
||||
// =========================== Anime Details ============================
|
||||
|
||||
override fun animeDetailsParse(document: Document): SAnime = SAnime.create().apply {
|
||||
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 getAnimeUrl(anime: SAnime): String {
|
||||
return "$baseUrl/info/${anime.url}"
|
||||
}
|
||||
|
||||
private fun Element?.parseStatus(): Int = when (this?.text()?.lowercase()) {
|
||||
"currently airing" -> SAnime.ONGOING
|
||||
"finished" -> SAnime.COMPLETED
|
||||
else -> SAnime.UNKNOWN
|
||||
override fun animeDetailsRequest(anime: SAnime): Request {
|
||||
return GET("$apiUrl/anime/uid/${anime.url}/", headers)
|
||||
}
|
||||
|
||||
override fun animeDetailsParse(response: Response): SAnime {
|
||||
return response.parseAs<AnimeDto>().toSAnime(preferences.getTitleLang)
|
||||
}
|
||||
|
||||
// ============================== Episodes ==============================
|
||||
|
||||
private val specialCharRegex = Regex("""(?![\-_])\W{1,}""")
|
||||
|
||||
override fun episodeListRequest(anime: SAnime): Request {
|
||||
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)
|
||||
return GET("$apiUrl/episode/uid/${anime.url}/", headers)
|
||||
}
|
||||
|
||||
override fun episodeListParse(response: Response): List<SEpisode> {
|
||||
return response.parseAs<HtmlResponseDto>().toHtml(baseUrl)
|
||||
.select(episodeListSelector())
|
||||
.map(::episodeFromElement)
|
||||
.reversed()
|
||||
}
|
||||
val guid = response.request.url.pathSegments[3]
|
||||
|
||||
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"
|
||||
}
|
||||
return response.parseAs<List<EpisodeDto>>().map { it.toSEpisode(guid) }.reversed()
|
||||
}
|
||||
|
||||
// ============================ Video Links =============================
|
||||
|
||||
private val filemoonExtractor by lazy { FilemoonExtractor(client) }
|
||||
private val vidHideExtractor by lazy { VidHideExtractor(client, headers) }
|
||||
private val filemoonExtractor by lazy { FilemoonExtractor(client, preferences) }
|
||||
private val savefileExtractor by lazy { SavefileExtractor(client, preferences) }
|
||||
private val buzzheavierExtractor by lazy { BuzzheavierExtractor(client, headers) }
|
||||
private val chillxExtractor by lazy { ChillxExtractor(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 {
|
||||
val url = (baseUrl + episode.url).toHttpUrl()
|
||||
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)
|
||||
val (guid, epId) = episode.url.split("-")
|
||||
return GET("$apiUrl/embed/$guid/$epId/", headers)
|
||||
}
|
||||
|
||||
override fun videoListParse(response: Response): List<Video> {
|
||||
val html = response.parseAs<HtmlResponseDto>().toHtml(baseUrl)
|
||||
Log.d("Hikari", html.toString())
|
||||
val data = response.parseAs<List<EmbedDto>>()
|
||||
|
||||
val headers = headersBuilder()
|
||||
.set("Referer", response.request.url.toString())
|
||||
.build()
|
||||
val selectedProviders = preferences.getStringSet(PREF_PROVIDER_KEY, PREF_PROVIDERS_DEFAULT)?.map(String::lowercase)?.toSet() ?: emptySet()
|
||||
|
||||
val subEmbedUrls = html.select(".servers-sub div.item.server-item").flatMap { item ->
|
||||
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>>()
|
||||
}
|
||||
return data.parallelCatchingFlatMapBlocking { embed ->
|
||||
val embedName = embed.embedName.lowercase()
|
||||
|
||||
val match = embedRegex.find(onClick)?.groupValues
|
||||
if (match == null) {
|
||||
Log.e("Hikari", "No match found for onClick: $onClick")
|
||||
return@flatMap emptyList<Pair<String, String>>()
|
||||
}
|
||||
if (embedName !in selectedProviders) return@parallelCatchingFlatMapBlocking emptyList()
|
||||
|
||||
val url = "$baseUrl/ajax/embed/${match[1]}/${match[2]}/${match[3]}"
|
||||
val iframeList = client.newCall(
|
||||
GET(url, headers),
|
||||
).execute().parseAs<List<String>>()
|
||||
val prefix = getEmbedTypeName(embed.embedType) + embed.embedName
|
||||
|
||||
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 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)
|
||||
when (embedName) {
|
||||
"streamwish" -> streamwishExtractor.videosFromUrl(embed.embedFrame, videoNameGen = { "$prefix - $it" })
|
||||
"filemoon" -> filemoonExtractor.videosFromUrl(embed.embedFrame, "$prefix - ")
|
||||
"sv" -> savefileExtractor.videosFromUrl(embed.embedFrame, "$prefix - ")
|
||||
"playerx" -> chillxExtractor.videoFromUrl(embed.embedFrame, "$prefix - ")
|
||||
"hiki" -> hikiExtraction(embed.embedFrame, "$prefix - ")
|
||||
else -> emptyList()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getVideosFromEmbed(embedUrl: String, name: String): List<Video> = when {
|
||||
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 - ")
|
||||
}
|
||||
private fun hikiExtraction(url: String, prefix: String): List<Video> {
|
||||
val hikiMirror = preferences.getString(PREF_HIKI_KEY, PREF_HIKI_DEFAULT)!!
|
||||
|
||||
override fun videoListSelector() = ".server-item:has(a[onclick~=getEmbed])"
|
||||
if (hikiMirror == "hiki") {
|
||||
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> {
|
||||
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(
|
||||
compareBy(
|
||||
{ it.quality.startsWith(type) },
|
||||
{ it.quality.contains(quality) },
|
||||
{ QUALITY_REGEX.find(it.quality)?.groupValues?.get(1)?.toIntOrNull() ?: 0 },
|
||||
{ it.quality.contains(hoster, true) },
|
||||
),
|
||||
).reversed()
|
||||
}
|
||||
|
||||
override fun videoFromElement(element: Element): Video =
|
||||
throw UnsupportedOperationException()
|
||||
|
||||
override fun videoUrlParse(document: Document): String =
|
||||
throw UnsupportedOperationException()
|
||||
|
||||
// ============================= 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 {
|
||||
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_DEFAULT = "1080"
|
||||
private val PREF_QUALITY_VALUES = arrayOf("1080", "720", "480", "360")
|
||||
private val PREF_QUALITY_ENTRIES = PREF_QUALITY_VALUES.map {
|
||||
"${it}p"
|
||||
}.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 ==============================
|
||||
|
||||
private val SharedPreferences.getTitleLang
|
||||
get() = getBoolean(PREF_ENGLISH_TITLE_KEY, PREF_ENGLISH_TITLE_DEFAULT)
|
||||
|
||||
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 {
|
||||
key = PREF_QUALITY_KEY
|
||||
title = "Preferred quality"
|
||||
|
@ -407,13 +268,44 @@ class Hikari : ParsedAnimeHttpSource(), ConfigurableAnimeSource {
|
|||
entryValues = PREF_QUALITY_VALUES
|
||||
setDefaultValue(PREF_QUALITY_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)
|
||||
|
||||
ListPreference(screen.context).apply {
|
||||
key = PREF_TYPE_KEY
|
||||
title = "Preferred type"
|
||||
entries = PREF_TYPE_ENTRIES
|
||||
entryValues = PREF_TYPE_VALUES
|
||||
setDefaultValue(PREF_TYPE_DEFAULT)
|
||||
summary = "%s"
|
||||
}.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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
ext {
|
||||
extName = 'JavGG'
|
||||
extClass = '.Javgg'
|
||||
extVersionCode = 5
|
||||
extVersionCode = 7
|
||||
isNsfw = true
|
||||
}
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
ext {
|
||||
extName = 'Jav Guru'
|
||||
extClass = '.JavGuru'
|
||||
extVersionCode = 26
|
||||
extVersionCode = 28
|
||||
isNsfw = true
|
||||
}
|
||||
|
||||
|
|
|
@ -3,7 +3,7 @@ ext {
|
|||
extClass = '.LMAnime'
|
||||
themePkg = 'animestream'
|
||||
baseUrl = 'https://lmanime.com'
|
||||
overrideVersionCode = 9
|
||||
overrideVersionCode = 11
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
ext {
|
||||
extName = 'MissAV'
|
||||
extClass = '.MissAV'
|
||||
extVersionCode = 15
|
||||
extVersionCode = 16
|
||||
isNsfw = true
|
||||
}
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
ext {
|
||||
extName = 'Sudatchi'
|
||||
extClass = '.Sudatchi'
|
||||
extVersionCode = 12
|
||||
extVersionCode = 13
|
||||
isNsfw = true
|
||||
}
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
ext {
|
||||
extName = 'SupJav'
|
||||
extClass = '.SupJavFactory'
|
||||
extVersionCode = 14
|
||||
extVersionCode = 16
|
||||
isNsfw = true
|
||||
}
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
ext {
|
||||
extName = 'Anime4up'
|
||||
extClass = '.Anime4Up'
|
||||
extVersionCode = 62
|
||||
extVersionCode = 64
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
ext {
|
||||
extName = 'Anime Blkom'
|
||||
extClass = '.AnimeBlkom'
|
||||
extVersionCode = 18
|
||||
extVersionCode = 19
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
ext {
|
||||
extName = 'AnimeLek'
|
||||
extClass = '.AnimeLek'
|
||||
extVersionCode = 31
|
||||
extVersionCode = 32
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
ext {
|
||||
extName = 'Animerco'
|
||||
extClass = '.Animerco'
|
||||
extVersionCode = 41
|
||||
extVersionCode = 43
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
ext {
|
||||
extName = 'Arab Seed'
|
||||
extClass = '.ArabSeed'
|
||||
extVersionCode = 17
|
||||
extVersionCode = 19
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
ext {
|
||||
extName = 'asia2tv'
|
||||
extClass = '.Asia2TV'
|
||||
extVersionCode = 22
|
||||
extVersionCode = 24
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
ext {
|
||||
extName = 'Cimaleek'
|
||||
extClass = '.Cimaleek'
|
||||
extVersionCode = 3
|
||||
extVersionCode = 4
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
ext {
|
||||
extName = 'Egy Dead'
|
||||
extClass = '.EgyDead'
|
||||
extVersionCode = 17
|
||||
extVersionCode = 19
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
ext {
|
||||
extName = 'FASELHD'
|
||||
extClass = '.FASELHD'
|
||||
extVersionCode = 17
|
||||
extVersionCode = 18
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
ext {
|
||||
extName = 'MY CIMA'
|
||||
extClass = '.MyCima'
|
||||
extVersionCode = 23
|
||||
extVersionCode = 24
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
ext {
|
||||
extName = 'Okanime'
|
||||
extClass = '.Okanime'
|
||||
extVersionCode = 13
|
||||
extVersionCode = 14
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
ext {
|
||||
extName = 'WIT ANIME'
|
||||
extClass = '.WitAnime'
|
||||
extVersionCode = 51
|
||||
extVersionCode = 53
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
||||
|
@ -12,4 +12,4 @@ dependencies {
|
|||
implementation(project(':lib:okru-extractor'))
|
||||
implementation(project(':lib:mp4upload-extractor'))
|
||||
implementation(project(':lib:vidbom-extractor'))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -30,7 +30,7 @@ class WitAnime : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
|||
|
||||
override val name = "WIT ANIME"
|
||||
|
||||
override val baseUrl = "https://witanime.pics"
|
||||
override val baseUrl = "https://witanime.cyou"
|
||||
|
||||
override val lang = "ar"
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
ext {
|
||||
extName = 'Anime-Base'
|
||||
extClass = '.AnimeBase'
|
||||
extVersionCode = 31
|
||||
extVersionCode = 33
|
||||
isNsfw = true
|
||||
}
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
ext {
|
||||
extName = 'Anime-Loads'
|
||||
extClass = '.AnimeLoads'
|
||||
extVersionCode = 17
|
||||
extVersionCode = 18
|
||||
isNsfw = true
|
||||
}
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
ext {
|
||||
extName = 'AnimeToast'
|
||||
extClass = '.AnimeToast'
|
||||
extVersionCode = 21
|
||||
extVersionCode = 23
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
ext {
|
||||
extName = 'AniWorld'
|
||||
extClass = '.AniWorld'
|
||||
extVersionCode = 25
|
||||
extVersionCode = 26
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
||||
|
|
|
@ -3,7 +3,7 @@ ext {
|
|||
extClass = '.Cinemathek'
|
||||
themePkg = 'dooplay'
|
||||
baseUrl = 'https://cinemathek.net'
|
||||
overrideVersionCode = 24
|
||||
overrideVersionCode = 26
|
||||
isNsfw = true
|
||||
}
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
ext {
|
||||
extName = 'Einfach'
|
||||
extClass = '.Einfach'
|
||||
extVersionCode = 16
|
||||
extVersionCode = 18
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
ext {
|
||||
extName = 'FilmPalast'
|
||||
extClass = '.FilmPalast'
|
||||
extVersionCode = 19
|
||||
extVersionCode = 20
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
||||
|
|
|
@ -3,7 +3,7 @@ ext {
|
|||
extClass = '.Kinoking'
|
||||
themePkg = 'dooplay'
|
||||
baseUrl = 'https://kinoking.cc'
|
||||
overrideVersionCode = 23
|
||||
overrideVersionCode = 24
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
ext {
|
||||
extName = 'Kool'
|
||||
extClass = '.Kool'
|
||||
extVersionCode = 13
|
||||
extVersionCode = 15
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
ext {
|
||||
extName = 'Moflix-Stream'
|
||||
extClass = '.MoflixStream'
|
||||
extVersionCode = 15
|
||||
extVersionCode = 17
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
ext {
|
||||
extName = 'Movie4k'
|
||||
extClass = '.Movie4k'
|
||||
extVersionCode = 11
|
||||
extVersionCode = 12
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
ext {
|
||||
extName = 'Serienstream'
|
||||
extClass = '.Serienstream'
|
||||
extVersionCode = 24
|
||||
extVersionCode = 25
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
ext {
|
||||
extName = 'AllAnime'
|
||||
extClass = '.AllAnime'
|
||||
extVersionCode = 35
|
||||
extVersionCode = 37
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
ext {
|
||||
extName = 'AllAnimeChi'
|
||||
extClass = '.AllAnimeChi'
|
||||
extVersionCode = 12
|
||||
extVersionCode = 14
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
||||
|
|
|
@ -3,7 +3,7 @@ ext {
|
|||
extClass = '.AnimeKhor'
|
||||
themePkg = 'animestream'
|
||||
baseUrl = 'https://animekhor.org'
|
||||
overrideVersionCode = 7
|
||||
overrideVersionCode = 9
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
||||
|
|
|
@ -3,7 +3,7 @@ ext {
|
|||
extClass = '.Animenosub'
|
||||
themePkg = 'animestream'
|
||||
baseUrl = 'https://animenosub.com'
|
||||
overrideVersionCode = 8
|
||||
overrideVersionCode = 10
|
||||
isNsfw = true
|
||||
}
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
ext {
|
||||
extName = 'AnimeOwl'
|
||||
extClass = '.AnimeOwl'
|
||||
extVersionCode = 23
|
||||
extVersionCode = 24
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
ext {
|
||||
extName = 'AnimePahe'
|
||||
extClass = '.AnimePahe'
|
||||
extVersionCode = 30
|
||||
extVersionCode = 31
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
||||
|
|
|
@ -162,6 +162,7 @@ class AnimePahe : ConfigurableAnimeSource, AnimeHttpSource() {
|
|||
episode.name = "Episode ${index + 1}"
|
||||
episode
|
||||
}
|
||||
.reversed()
|
||||
}
|
||||
|
||||
private fun parseEpisodePage(episodes: List<EpisodeDto>, animeSession: String): MutableList<SEpisode> {
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
ext {
|
||||
extName = 'AnimeTake'
|
||||
extClass = '.AnimeTake'
|
||||
extVersionCode = 7
|
||||
extVersionCode = 9
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
||||
|
|
|
@ -2,7 +2,7 @@ ext {
|
|||
extName = 'AniPlay'
|
||||
extClass = '.AniPlay'
|
||||
themePkg = 'anilist'
|
||||
overrideVersionCode = 17
|
||||
overrideVersionCode = 20
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
||||
|
|
|
@ -285,9 +285,8 @@ class AniPlay : AniListAnimeHttpSource(), ConfigurableAnimeSource {
|
|||
|
||||
try {
|
||||
when (serverName) {
|
||||
// yuki
|
||||
PREF_SERVER_ENTRIES[1] -> {
|
||||
val url = "https://yukiprox.aniplaynow.live/m3u8-proxy?url=${defaultSource.url}&headers={\"Referer\":\"https://megacloud.club/\"}"
|
||||
"Yuki" -> {
|
||||
val url = "https://yukiproxy.aniplaynow.live/m3u8-proxy?url=${defaultSource.url}&headers={\"Referer\":\"https://megacloud.club/\"}"
|
||||
return playlistUtils.extractFromHls(
|
||||
playlistUrl = url,
|
||||
videoNameGen = { quality -> "$serverName - $quality - $typeName" },
|
||||
|
@ -308,9 +307,8 @@ class AniPlay : AniListAnimeHttpSource(), ConfigurableAnimeSource {
|
|||
},
|
||||
)
|
||||
}
|
||||
// pahe
|
||||
PREF_SERVER_ENTRIES[2] -> {
|
||||
val url = "https://prox.aniplaynow.live/?url=${defaultSource.url}&ref=https://kwik.si"
|
||||
"Pahe" -> {
|
||||
val url = "https://paheproxy.aniplaynow.live/proxy?url=${defaultSource.url}&headers={\"Referer\":\"https://kwik.si/\"}"
|
||||
val headers = headers.newBuilder().apply {
|
||||
set("Accept", "*/*")
|
||||
set("Origin", baseUrl)
|
||||
|
@ -565,8 +563,8 @@ class AniPlay : AniListAnimeHttpSource(), ConfigurableAnimeSource {
|
|||
private const val PREF_DOMAIN_DEFAULT = "aniplaynow.live"
|
||||
|
||||
private const val PREF_SERVER_KEY = "server"
|
||||
private val PREF_SERVER_ENTRIES = arrayOf("Maze", "Yuki", "Pahe", "Kuro")
|
||||
private val PREF_SERVER_ENTRY_VALUES = arrayOf("maze", "yuki", "pahe", "kuro")
|
||||
private val PREF_SERVER_ENTRIES = arrayOf("Pahe", "Yuki") // , "Hika")
|
||||
private val PREF_SERVER_ENTRY_VALUES = arrayOf("pahe", "yuki") // , "hika")
|
||||
private const val PREF_SERVER_DEFAULT = "yuki"
|
||||
private const val SERVER_UNKNOWN = "Other"
|
||||
|
||||
|
|
9
src/en/aniwatch/build.gradle
Normal file
|
@ -0,0 +1,9 @@
|
|||
ext {
|
||||
extName = 'AniWatchtv'
|
||||
extClass = '.AniWatchtv'
|
||||
themePkg = 'zorotheme'
|
||||
baseUrl = 'https://aniwatchtv.to'
|
||||
overrideVersionCode = 1
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
BIN
src/en/aniwatch/res/mipmap-hdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 4 KiB |
BIN
src/en/aniwatch/res/mipmap-mdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 2.4 KiB |
BIN
src/en/aniwatch/res/mipmap-xhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 5.5 KiB |
BIN
src/en/aniwatch/res/mipmap-xxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 8.6 KiB |
BIN
src/en/aniwatch/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 12 KiB |
|
@ -0,0 +1,40 @@
|
|||
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()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,7 +1,7 @@
|
|||
ext {
|
||||
extName = 'AsiaFlix'
|
||||
extClass = '.AsiaFlix'
|
||||
extVersionCode = 16
|
||||
extVersionCode = 18
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
||||
|
|
|
@ -3,7 +3,7 @@ ext {
|
|||
extClass = '.DonghuaStream'
|
||||
themePkg = 'animestream'
|
||||
baseUrl = 'https://donghuastream.org'
|
||||
overrideVersionCode = 9
|
||||
overrideVersionCode = 10
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
||||
|
|
|
@ -3,7 +3,7 @@ ext {
|
|||
extClass = '.DopeBox'
|
||||
themePkg = 'dopeflix'
|
||||
baseUrl = 'https://dopebox.to'
|
||||
overrideVersionCode = 11
|
||||
overrideVersionCode = 12
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
12
src/en/jpfilms/build.gradle
Normal file
|
@ -0,0 +1,12 @@
|
|||
ext {
|
||||
extName = 'JPFilms'
|
||||
extClass = '.JPFilms'
|
||||
extVersionCode = 1
|
||||
isNsfw = true
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
||||
|
||||
dependencies {
|
||||
implementation(project(":lib:playlist-utils"))
|
||||
}
|
BIN
src/en/jpfilms/res/mipmap-hdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 6.8 KiB |
BIN
src/en/jpfilms/res/mipmap-mdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 3.6 KiB |
BIN
src/en/jpfilms/res/mipmap-xhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 10 KiB |
BIN
src/en/jpfilms/res/mipmap-xxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 20 KiB |