Compare commits
2 commits
main
...
khaled-0/p
Author | SHA1 | Date | |
---|---|---|---|
![]() |
ea40e3672f | ||
![]() |
4030234683 |
259 changed files with 2702 additions and 23543 deletions
2
.github/workflows/batch_close_issues.yml
vendored
2
.github/workflows/batch_close_issues.yml
vendored
|
@ -10,7 +10,7 @@ on:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
stale:
|
stale:
|
||||||
runs-on: docker
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/stale@28ca1036281a5e5922ead5184a1bbf96e5fc984e # v9
|
- uses: actions/stale@28ca1036281a5e5922ead5184a1bbf96e5fc984e # v9
|
||||||
with:
|
with:
|
||||||
|
|
17
.github/workflows/build_pull_request.yml
vendored
17
.github/workflows/build_pull_request.yml
vendored
|
@ -18,12 +18,15 @@ env:
|
||||||
jobs:
|
jobs:
|
||||||
prepare:
|
prepare:
|
||||||
name: Prepare job
|
name: Prepare job
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-latest
|
||||||
outputs:
|
outputs:
|
||||||
individualMatrix: ${{ steps.generate-matrices.outputs.individualMatrix }}
|
individualMatrix: ${{ steps.generate-matrices.outputs.individualMatrix }}
|
||||||
steps:
|
steps:
|
||||||
- name: Clone repo
|
- name: Clone repo
|
||||||
uses: https://github.com/actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4
|
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4
|
||||||
|
|
||||||
|
- name: Validate Gradle Wrapper
|
||||||
|
uses: gradle/wrapper-validation-action@a494d935f4b56874c4a5a87d19af7afcf3a163d0 # v2
|
||||||
|
|
||||||
- name: Get number of modules
|
- name: Get number of modules
|
||||||
run: |
|
run: |
|
||||||
|
@ -34,7 +37,7 @@ jobs:
|
||||||
|
|
||||||
- id: generate-matrices
|
- id: generate-matrices
|
||||||
name: Create output matrices
|
name: Create output matrices
|
||||||
uses: https://github.com/actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7
|
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7
|
||||||
with:
|
with:
|
||||||
script: |
|
script: |
|
||||||
const numIndividualModules = process.env.NUM_INDIVIDUAL_MODULES;
|
const numIndividualModules = process.env.NUM_INDIVIDUAL_MODULES;
|
||||||
|
@ -49,21 +52,21 @@ jobs:
|
||||||
build_individual:
|
build_individual:
|
||||||
name: Build individual modules
|
name: Build individual modules
|
||||||
needs: prepare
|
needs: prepare
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-latest
|
||||||
strategy:
|
strategy:
|
||||||
matrix: ${{ fromJSON(needs.prepare.outputs.individualMatrix) }}
|
matrix: ${{ fromJSON(needs.prepare.outputs.individualMatrix) }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout PR
|
- name: Checkout PR
|
||||||
uses: https://github.com/actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4
|
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4
|
||||||
|
|
||||||
- name: Set up JDK
|
- name: Set up JDK
|
||||||
uses: https://github.com/actions/setup-java@387ac29b308b003ca37ba93a6cab5eb57c8f5f93 # v4
|
uses: actions/setup-java@387ac29b308b003ca37ba93a6cab5eb57c8f5f93 # v4
|
||||||
with:
|
with:
|
||||||
java-version: 17
|
java-version: 17
|
||||||
distribution: temurin
|
distribution: temurin
|
||||||
|
|
||||||
- name: Set up Gradle
|
- name: Set up Gradle
|
||||||
uses: https://github.com/gradle/actions/setup-gradle@417ae3ccd767c252f5661f1ace9f835f9654f2b5 # v3
|
uses: gradle/actions/setup-gradle@417ae3ccd767c252f5661f1ace9f835f9654f2b5 # v3
|
||||||
with:
|
with:
|
||||||
cache-read-only: true
|
cache-read-only: true
|
||||||
|
|
||||||
|
|
71
.github/workflows/build_push.yml
vendored
71
.github/workflows/build_push.yml
vendored
|
@ -21,23 +21,16 @@ env:
|
||||||
jobs:
|
jobs:
|
||||||
prepare:
|
prepare:
|
||||||
name: Prepare job
|
name: Prepare job
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-latest
|
||||||
outputs:
|
outputs:
|
||||||
individualMatrix: ${{ steps.generate-matrices.outputs.individualMatrix }}
|
individualMatrix: ${{ steps.generate-matrices.outputs.individualMatrix }}
|
||||||
steps:
|
steps:
|
||||||
- name: Clone repo
|
- name: Clone repo
|
||||||
uses: https://github.com/actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4
|
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4
|
||||||
with:
|
with:
|
||||||
ref: main
|
ref: main
|
||||||
token: ${{ secrets.BOT_PAT }}
|
token: ${{ secrets.BOT_PAT }}
|
||||||
|
|
||||||
- name: Get number of modules
|
|
||||||
run: |
|
|
||||||
set -x
|
|
||||||
projects=(src/*/*)
|
|
||||||
export CI_CHUNK_NUM=${#projects[@]}
|
|
||||||
echo "NUM_INDIVIDUAL_MODULES=${#projects[@]}" >> $GITHUB_ENV
|
|
||||||
|
|
||||||
# Temporary pause because of leak of tj-actions/changed-files
|
# Temporary pause because of leak of tj-actions/changed-files
|
||||||
# - name: Find lib changes
|
# - name: Find lib changes
|
||||||
# id: modified-libs
|
# id: modified-libs
|
||||||
|
@ -49,7 +42,7 @@ jobs:
|
||||||
# safe_output: false
|
# safe_output: false
|
||||||
|
|
||||||
- name: Import GPG key
|
- name: Import GPG key
|
||||||
uses: https://github.com/crazy-max/ghaction-import-gpg@v6 # v6.1.0
|
uses: crazy-max/ghaction-import-gpg@v6 # v6.1.0
|
||||||
with:
|
with:
|
||||||
gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }}
|
gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }}
|
||||||
passphrase: ${{ secrets.GPG_PASSPHRASE }}
|
passphrase: ${{ secrets.GPG_PASSPHRASE }}
|
||||||
|
@ -63,9 +56,19 @@ jobs:
|
||||||
# chmod +x ./.github/scripts/bump-versions.py
|
# chmod +x ./.github/scripts/bump-versions.py
|
||||||
# ./.github/scripts/bump-versions.py ${{ steps.modified-libs.outputs.all_changed_files }}
|
# ./.github/scripts/bump-versions.py ${{ steps.modified-libs.outputs.all_changed_files }}
|
||||||
|
|
||||||
|
- name: Validate Gradle Wrapper
|
||||||
|
uses: gradle/wrapper-validation-action@a494d935f4b56874c4a5a87d19af7afcf3a163d0 # v2
|
||||||
|
|
||||||
|
- name: Get number of modules
|
||||||
|
run: |
|
||||||
|
set -x
|
||||||
|
projects=(src/*/*)
|
||||||
|
|
||||||
|
echo "NUM_INDIVIDUAL_MODULES=${#projects[@]}" >> $GITHUB_ENV
|
||||||
|
|
||||||
- id: generate-matrices
|
- id: generate-matrices
|
||||||
name: Create output matrices
|
name: Create output matrices
|
||||||
uses: https://github.com/actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7
|
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7
|
||||||
with:
|
with:
|
||||||
script: |
|
script: |
|
||||||
const numIndividualModules = process.env.NUM_INDIVIDUAL_MODULES;
|
const numIndividualModules = process.env.NUM_INDIVIDUAL_MODULES;
|
||||||
|
@ -80,32 +83,27 @@ jobs:
|
||||||
build_individual:
|
build_individual:
|
||||||
name: Build individual modules
|
name: Build individual modules
|
||||||
needs: prepare
|
needs: prepare
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-latest
|
||||||
strategy:
|
strategy:
|
||||||
matrix: ${{ fromJSON(needs.prepare.outputs.individualMatrix) }}
|
matrix: ${{ fromJSON(needs.prepare.outputs.individualMatrix) }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout main branch
|
- name: Checkout main branch
|
||||||
uses: https://github.com/actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4
|
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4
|
||||||
with:
|
with:
|
||||||
ref: main
|
ref: main
|
||||||
|
|
||||||
- name: Set up JDK
|
- name: Set up JDK
|
||||||
uses: https://github.com/actions/setup-java@387ac29b308b003ca37ba93a6cab5eb57c8f5f93 # v4
|
uses: actions/setup-java@387ac29b308b003ca37ba93a6cab5eb57c8f5f93 # v4
|
||||||
with:
|
with:
|
||||||
java-version: 17
|
java-version: 17
|
||||||
distribution: temurin
|
distribution: temurin
|
||||||
|
|
||||||
- name: Set up Android SDK
|
|
||||||
uses: https://github.com/android-actions/setup-android@00854ea68c109d98c75d956347303bf7c45b0277 # v3
|
|
||||||
with:
|
|
||||||
packages: ""
|
|
||||||
|
|
||||||
- name: Prepare signing key
|
- name: Prepare signing key
|
||||||
run: |
|
run: |
|
||||||
echo ${{ secrets.SIGNING_KEY }} | base64 -d > signingkey.jks
|
echo ${{ secrets.SIGNING_KEY }} | base64 -d > signingkey.jks
|
||||||
|
|
||||||
- name: Set up Gradle
|
- name: Set up Gradle
|
||||||
uses: https://github.com/gradle/actions/setup-gradle@245c8a24de79c0dbeabaf19ebcbbd3b2c36f278d # v4
|
uses: gradle/actions/setup-gradle@417ae3ccd767c252f5661f1ace9f835f9654f2b5 # v3
|
||||||
|
|
||||||
- name: Build extensions (chunk ${{ matrix.chunk }})
|
- name: Build extensions (chunk ${{ matrix.chunk }})
|
||||||
env:
|
env:
|
||||||
|
@ -116,8 +114,8 @@ jobs:
|
||||||
run: chmod +x ./gradlew && ./gradlew -p src assembleRelease
|
run: chmod +x ./gradlew && ./gradlew -p src assembleRelease
|
||||||
|
|
||||||
- name: Upload APKs (chunk ${{ matrix.chunk }})
|
- name: Upload APKs (chunk ${{ matrix.chunk }})
|
||||||
uses: https://code.forgejo.org/forgejo/upload-artifact@16871d9e8cfcf27ff31822cac382bbb5450f1e1e # v4-patch
|
uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4
|
||||||
if: "github.repository == 'AlmightyHak/extensions-source'"
|
if: "github.repository == 'Kohi-den/extensions-source'"
|
||||||
with:
|
with:
|
||||||
name: "individual-apks-${{ matrix.chunk }}"
|
name: "individual-apks-${{ matrix.chunk }}"
|
||||||
path: "**/*.apk"
|
path: "**/*.apk"
|
||||||
|
@ -130,31 +128,22 @@ jobs:
|
||||||
name: Publish repo
|
name: Publish repo
|
||||||
needs:
|
needs:
|
||||||
- build_individual
|
- build_individual
|
||||||
if: "github.repository == 'AlmightyHak/extensions-source'"
|
if: "github.repository == 'Kohi-den/extensions-source'"
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Setup rsync
|
|
||||||
run: |
|
|
||||||
sudo apt-get update && sudo apt install rsync -y
|
|
||||||
|
|
||||||
- name: Download APK artifacts
|
- name: Download APK artifacts
|
||||||
uses: https://code.forgejo.org/forgejo/download-artifact@d8d0a99033603453ad2255e58720b460a0555e1e # v4-patch
|
uses: actions/download-artifact@eaceaf801fd36c7dee90939fad912460b18a1ffe # v4
|
||||||
with:
|
with:
|
||||||
path: ~/apk-artifacts
|
path: ~/apk-artifacts
|
||||||
|
|
||||||
- name: Set up JDK
|
- name: Set up JDK
|
||||||
uses: https://github.com/actions/setup-java@387ac29b308b003ca37ba93a6cab5eb57c8f5f93 # v4
|
uses: actions/setup-java@387ac29b308b003ca37ba93a6cab5eb57c8f5f93 # v4
|
||||||
with:
|
with:
|
||||||
java-version: 17
|
java-version: 17
|
||||||
distribution: temurin
|
distribution: temurin
|
||||||
|
|
||||||
- name: Set up Android SDK
|
|
||||||
uses: https://github.com/android-actions/setup-android@00854ea68c109d98c75d956347303bf7c45b0277 # v3
|
|
||||||
with:
|
|
||||||
packages: "build-tools;34.0.0"
|
|
||||||
|
|
||||||
- name: Checkout main branch
|
- name: Checkout main branch
|
||||||
uses: https://github.com/actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4
|
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4
|
||||||
with:
|
with:
|
||||||
ref: main
|
ref: main
|
||||||
path: main
|
path: main
|
||||||
|
@ -169,9 +158,9 @@ jobs:
|
||||||
python ./.github/scripts/create-repo.py
|
python ./.github/scripts/create-repo.py
|
||||||
|
|
||||||
- name: Checkout repo branch
|
- name: Checkout repo branch
|
||||||
uses: https://github.com/actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4
|
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4
|
||||||
with:
|
with:
|
||||||
repository: AlmightyHak/extensions
|
repository: Kohi-den/extensions
|
||||||
token: ${{ secrets.BOT_PAT }}
|
token: ${{ secrets.BOT_PAT }}
|
||||||
ref: main
|
ref: main
|
||||||
path: repo
|
path: repo
|
||||||
|
@ -181,9 +170,9 @@ jobs:
|
||||||
rsync -a --delete --exclude .git --exclude .gitignore main/repo/ repo --exclude README.md --exclude repo.json
|
rsync -a --delete --exclude .git --exclude .gitignore main/repo/ repo --exclude README.md --exclude repo.json
|
||||||
|
|
||||||
- name: Deploy repo
|
- name: Deploy repo
|
||||||
uses: https://github.com/EndBug/add-and-commit@v9
|
uses: EndBug/add-and-commit@v9
|
||||||
with:
|
with:
|
||||||
message: "Update extensions repo"
|
message: "Update extensions repo"
|
||||||
cwd: "./repo"
|
cwd: "./repo"
|
||||||
committer_name: AlmightyHak
|
committer_name: Kohi-den-Bot
|
||||||
committer_email: almightyhak@noreply.localhost
|
committer_email: 177773202+Kohi-den-Bot@users.noreply.github.com
|
||||||
|
|
2
.github/workflows/issue_moderator.yml
vendored
2
.github/workflows/issue_moderator.yml
vendored
|
@ -8,7 +8,7 @@ on:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
autoclose:
|
autoclose:
|
||||||
runs-on: docker
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Moderate issues
|
- name: Moderate issues
|
||||||
uses: aniyomiorg/issue-moderator-action@v2
|
uses: aniyomiorg/issue-moderator-action@v2
|
||||||
|
|
2
.github/workflows/lock.yml
vendored
2
.github/workflows/lock.yml
vendored
|
@ -10,7 +10,7 @@ on:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
lock:
|
lock:
|
||||||
runs-on: docker
|
runs-on: ubuntu-latest
|
||||||
permissions:
|
permissions:
|
||||||
issues: write
|
issues: write
|
||||||
steps:
|
steps:
|
||||||
|
|
|
@ -2,9 +2,9 @@
|
||||||
|
|
||||||
just paste this into your anime repo
|
just paste this into your anime repo
|
||||||
```
|
```
|
||||||
https://kohiden.xyz/AlmightyHak/extensions/raw/branch/main/index.min.json
|
https://raw.githubusercontent.com/Kohi-den/extensions/main/index.min.json
|
||||||
```
|
```
|
||||||
If your interested in installing just the apks they can be found [Here](https://kohiden.xyz/AlmightyHak/extensions/src/branch/main/apk)
|
If your interested in installing just the apks they can be found [Here](https://github.com/Kohi-den/extensions)
|
||||||
|
|
||||||
## Support Server
|
## Support Server
|
||||||
|
|
||||||
|
|
21111
SDK/NOTICE.txt
21111
SDK/NOTICE.txt
File diff suppressed because it is too large
Load diff
BIN
SDK/adb
BIN
SDK/adb
Binary file not shown.
BIN
SDK/etc1tool
BIN
SDK/etc1tool
Binary file not shown.
BIN
SDK/fastboot
BIN
SDK/fastboot
Binary file not shown.
BIN
SDK/hprof-conv
BIN
SDK/hprof-conv
Binary file not shown.
Binary file not shown.
|
@ -1,2 +0,0 @@
|
||||||
|
|
||||||
24333f8a63b6825ea9c5514f83c2829b004d1fee
|
|
BIN
SDK/make_f2fs
BIN
SDK/make_f2fs
Binary file not shown.
Binary file not shown.
BIN
SDK/mke2fs
BIN
SDK/mke2fs
Binary file not shown.
|
@ -1,53 +0,0 @@
|
||||||
[defaults]
|
|
||||||
base_features = sparse_super,large_file,filetype,dir_index,ext_attr
|
|
||||||
default_mntopts = acl,user_xattr
|
|
||||||
enable_periodic_fsck = 0
|
|
||||||
blocksize = 4096
|
|
||||||
inode_size = 256
|
|
||||||
inode_ratio = 16384
|
|
||||||
reserved_ratio = 1.0
|
|
||||||
|
|
||||||
[fs_types]
|
|
||||||
ext3 = {
|
|
||||||
features = has_journal
|
|
||||||
}
|
|
||||||
ext4 = {
|
|
||||||
features = has_journal,extent,huge_file,dir_nlink,extra_isize,uninit_bg
|
|
||||||
inode_size = 256
|
|
||||||
}
|
|
||||||
ext4dev = {
|
|
||||||
features = has_journal,extent,huge_file,flex_bg,inline_data,64bit,dir_nlink,extra_isize
|
|
||||||
inode_size = 256
|
|
||||||
options = test_fs=1
|
|
||||||
}
|
|
||||||
small = {
|
|
||||||
blocksize = 1024
|
|
||||||
inode_size = 128
|
|
||||||
inode_ratio = 4096
|
|
||||||
}
|
|
||||||
floppy = {
|
|
||||||
blocksize = 1024
|
|
||||||
inode_size = 128
|
|
||||||
inode_ratio = 8192
|
|
||||||
}
|
|
||||||
big = {
|
|
||||||
inode_ratio = 32768
|
|
||||||
}
|
|
||||||
huge = {
|
|
||||||
inode_ratio = 65536
|
|
||||||
}
|
|
||||||
news = {
|
|
||||||
inode_ratio = 4096
|
|
||||||
}
|
|
||||||
largefile = {
|
|
||||||
inode_ratio = 1048576
|
|
||||||
blocksize = -1
|
|
||||||
}
|
|
||||||
largefile4 = {
|
|
||||||
inode_ratio = 4194304
|
|
||||||
blocksize = -1
|
|
||||||
}
|
|
||||||
hurd = {
|
|
||||||
blocksize = 4096
|
|
||||||
inode_size = 128
|
|
||||||
}
|
|
|
@ -1,2 +0,0 @@
|
||||||
Pkg.UserSrc=false
|
|
||||||
Pkg.Revision=36.0.0
|
|
BIN
SDK/sqlite3
BIN
SDK/sqlite3
Binary file not shown.
|
@ -2,4 +2,4 @@ plugins {
|
||||||
id("lib-multisrc")
|
id("lib-multisrc")
|
||||||
}
|
}
|
||||||
|
|
||||||
baseVersionCode = 4
|
baseVersionCode = 3
|
||||||
|
|
|
@ -110,8 +110,6 @@ object AniListFilters {
|
||||||
|
|
||||||
val YEAR_LIST = arrayOf(
|
val YEAR_LIST = arrayOf(
|
||||||
Pair("<Select>", ""),
|
Pair("<Select>", ""),
|
||||||
Pair("2026", "2026"),
|
|
||||||
Pair("2025", "2025"),
|
|
||||||
Pair("2024", "2024"),
|
Pair("2024", "2024"),
|
||||||
Pair("2023", "2023"),
|
Pair("2023", "2023"),
|
||||||
Pair("2022", "2022"),
|
Pair("2022", "2022"),
|
||||||
|
|
|
@ -2,4 +2,4 @@ plugins {
|
||||||
id("lib-multisrc")
|
id("lib-multisrc")
|
||||||
}
|
}
|
||||||
|
|
||||||
baseVersionCode = 3
|
baseVersionCode = 2
|
||||||
|
|
|
@ -49,13 +49,13 @@ abstract class DooPlay(
|
||||||
const val PREFIX_SEARCH = "path:"
|
const val PREFIX_SEARCH = "path:"
|
||||||
}
|
}
|
||||||
|
|
||||||
protected open val prefQualityDefault = "1080p"
|
protected open val prefQualityDefault = "720p"
|
||||||
protected open val prefQualityKey = "preferred_quality"
|
protected open val prefQualityKey = "preferred_quality"
|
||||||
protected open val prefQualityTitle = when (lang) {
|
protected open val prefQualityTitle = when (lang) {
|
||||||
"pt-BR" -> "Qualidade preferida"
|
"pt-BR" -> "Qualidade preferida"
|
||||||
else -> "Preferred quality"
|
else -> "Preferred quality"
|
||||||
}
|
}
|
||||||
protected open val prefQualityValues = arrayOf("360p", "480p", "720p", "1080p")
|
protected open val prefQualityValues = arrayOf("480p", "720p")
|
||||||
protected open val prefQualityEntries = prefQualityValues
|
protected open val prefQualityEntries = prefQualityValues
|
||||||
|
|
||||||
protected open val videoSortPrefKey = prefQualityKey
|
protected open val videoSortPrefKey = prefQualityKey
|
||||||
|
|
|
@ -2,7 +2,7 @@ plugins {
|
||||||
id("lib-multisrc")
|
id("lib-multisrc")
|
||||||
}
|
}
|
||||||
|
|
||||||
baseVersionCode = 6
|
baseVersionCode = 4
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
api(project(":lib:megacloud-extractor"))
|
api(project(":lib:megacloud-extractor"))
|
||||||
|
|
|
@ -183,7 +183,7 @@ object ZoroThemeFilters {
|
||||||
Pair("Most Watched", "most_watched"),
|
Pair("Most Watched", "most_watched"),
|
||||||
)
|
)
|
||||||
|
|
||||||
val YEARS = arrayOf(ALL) + (1917..2025).map {
|
val YEARS = arrayOf(ALL) + (1917..2024).map {
|
||||||
Pair(it.toString(), it.toString())
|
Pair(it.toString(), it.toString())
|
||||||
}.reversed().toTypedArray()
|
}.reversed().toTypedArray()
|
||||||
|
|
||||||
|
|
|
@ -1,3 +0,0 @@
|
||||||
plugins {
|
|
||||||
id("lib-android")
|
|
||||||
}
|
|
|
@ -1,82 +0,0 @@
|
||||||
package eu.kanade.tachiyomi.lib.buzzheavierextractor
|
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.animesource.model.Video
|
|
||||||
import eu.kanade.tachiyomi.network.GET
|
|
||||||
import eu.kanade.tachiyomi.util.asJsoup
|
|
||||||
import eu.kanade.tachiyomi.util.parseAs
|
|
||||||
import java.io.IOException
|
|
||||||
import kotlinx.serialization.ExperimentalSerializationApi
|
|
||||||
import kotlinx.serialization.Serializable
|
|
||||||
import okhttp3.Headers
|
|
||||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
|
||||||
import okhttp3.OkHttpClient
|
|
||||||
import okhttp3.Request
|
|
||||||
import okhttp3.Response
|
|
||||||
|
|
||||||
class BuzzheavierExtractor(
|
|
||||||
private val client: OkHttpClient,
|
|
||||||
private val headers: Headers,
|
|
||||||
) {
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
private val SIZE_REGEX = Regex("""Size\s*-\s*([0-9.]+\s*[GMK]B)""")
|
|
||||||
}
|
|
||||||
|
|
||||||
@OptIn(ExperimentalSerializationApi::class)
|
|
||||||
fun videosFromUrl(url: String, prefix: String = "Buzzheavier - ", proxyUrl: String? = null): List<Video> {
|
|
||||||
val httpUrl = url.toHttpUrl()
|
|
||||||
val id = httpUrl.pathSegments.first()
|
|
||||||
|
|
||||||
val dlHeaders = headers.newBuilder().apply {
|
|
||||||
add("Accept", "*/*")
|
|
||||||
add("HX-Current-URL", url)
|
|
||||||
add("HX-Request", "true")
|
|
||||||
add("Priority", "u=1, i")
|
|
||||||
add("Referer", url)
|
|
||||||
}.build()
|
|
||||||
|
|
||||||
val videoHeaders = headers.newBuilder().apply {
|
|
||||||
add("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7")
|
|
||||||
add("Priority", "u=0, i")
|
|
||||||
add("Referer", url)
|
|
||||||
}.build()
|
|
||||||
|
|
||||||
val siteRequest = client.newCall(GET(url)).execute()
|
|
||||||
val parsedHtml = siteRequest.asJsoup()
|
|
||||||
val detailsText = parsedHtml.selectFirst("li:contains(Details:)")?.text() ?: ""
|
|
||||||
val size = SIZE_REGEX.find(detailsText)?.groupValues?.getOrNull(1)?.trim() ?: "Unknown"
|
|
||||||
|
|
||||||
val downloadRequest = GET("https://${httpUrl.host}/$id/download", dlHeaders)
|
|
||||||
val path = client.executeWithRetry(downloadRequest, 5, 204).use { response ->
|
|
||||||
response.header("hx-redirect").orEmpty()
|
|
||||||
}
|
|
||||||
|
|
||||||
val videoUrl = if (path.isNotEmpty()) {
|
|
||||||
if (path.startsWith("http")) path else "https://${httpUrl.host}$path"
|
|
||||||
} else if (proxyUrl?.isNotEmpty() == true) {
|
|
||||||
client.executeWithRetry(GET(proxyUrl + id), 5, 200).parseAs<UrlDto>().url
|
|
||||||
} else {
|
|
||||||
return emptyList()
|
|
||||||
}
|
|
||||||
|
|
||||||
return listOf(Video(videoUrl, "${prefix}${size}", videoUrl, videoHeaders))
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun OkHttpClient.executeWithRetry(request: Request, maxRetries: Int, validCode: Int): Response {
|
|
||||||
var response: Response? = null
|
|
||||||
for (attempt in 0 until maxRetries) {
|
|
||||||
response?.close()
|
|
||||||
response = this.newCall(request).execute()
|
|
||||||
if (response.code == validCode) {
|
|
||||||
return response
|
|
||||||
}
|
|
||||||
if (attempt < maxRetries - 1) {
|
|
||||||
Thread.sleep(1000)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return response ?: throw IOException("Failed to execute request after $maxRetries attempts")
|
|
||||||
}
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class UrlDto(val url: String)
|
|
||||||
}
|
|
|
@ -1,54 +1,73 @@
|
||||||
package eu.kanade.tachiyomi.lib.chillxextractor
|
package eu.kanade.tachiyomi.lib.chillxextractor
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
import eu.kanade.tachiyomi.animesource.model.Track
|
import eu.kanade.tachiyomi.animesource.model.Track
|
||||||
import eu.kanade.tachiyomi.animesource.model.Video
|
import eu.kanade.tachiyomi.animesource.model.Video
|
||||||
|
import eu.kanade.tachiyomi.lib.cryptoaes.CryptoAES
|
||||||
import eu.kanade.tachiyomi.lib.playlistutils.PlaylistUtils
|
import eu.kanade.tachiyomi.lib.playlistutils.PlaylistUtils
|
||||||
|
import eu.kanade.tachiyomi.network.GET
|
||||||
|
import eu.kanade.tachiyomi.util.parseAs
|
||||||
|
import kotlinx.serialization.ExperimentalSerializationApi
|
||||||
|
import kotlinx.serialization.SerialName
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
import okhttp3.Headers
|
import okhttp3.Headers
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
|
||||||
class ChillxExtractor(private val client: OkHttpClient, private val headers: Headers) {
|
class ChillxExtractor(private val client: OkHttpClient, private val headers: Headers) {
|
||||||
|
private val json: Json by injectLazy()
|
||||||
private val playlistUtils by lazy { PlaylistUtils(client, headers) }
|
private val playlistUtils by lazy { PlaylistUtils(client, headers) }
|
||||||
private val webViewResolver by lazy { WebViewResolver(client, headers) }
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
private val REGEX_MASTER_JS = Regex("""\s*=\s*'([^']+)""")
|
||||||
private val REGEX_SOURCES = Regex("""sources:\s*\[\{"file":"([^"]+)""")
|
private val REGEX_SOURCES = Regex("""sources:\s*\[\{"file":"([^"]+)""")
|
||||||
private val REGEX_FILE = Regex("""file: ?"([^"]+)"""")
|
private val REGEX_FILE = Regex("""file: ?"([^"]+)"""")
|
||||||
private val REGEX_SOURCE = Regex("""source = ?"([^"]+)"""")
|
private val REGEX_SOURCE = Regex("""source = ?"([^"]+)"""")
|
||||||
private val REGEX_SUBS = Regex("""\{"file":"([^"]+)","label":"([^"]+)","kind":"captions","default":\w+\}""")
|
private val REGEX_SUBS = Regex("""\{"file":"([^"]+)","label":"([^"]+)","kind":"captions","default":\w+\}""")
|
||||||
|
private const val KEY_SOURCE = "https://raw.githubusercontent.com/Rowdy-Avocado/multi-keys/keys/index.html"
|
||||||
}
|
}
|
||||||
|
|
||||||
fun videoFromUrl(url: String, prefix: String = "Chillx - "): List<Video> {
|
fun videoFromUrl(url: String, referer: String, prefix: String = "Chillx - "): List<Video> {
|
||||||
val data = webViewResolver.getDecryptedData(url) ?: return emptyList()
|
val newHeaders = headers.newBuilder()
|
||||||
|
.set("Referer", "$referer/")
|
||||||
|
.set("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8")
|
||||||
|
.set("Accept-Language", "en-US,en;q=0.5")
|
||||||
|
.build()
|
||||||
|
|
||||||
val masterUrl = REGEX_SOURCES.find(data)?.groupValues?.get(1)
|
val body = client.newCall(GET(url, newHeaders)).execute().body.string()
|
||||||
?: REGEX_FILE.find(data)?.groupValues?.get(1)
|
|
||||||
?: REGEX_SOURCE.find(data)?.groupValues?.get(1)
|
val master = REGEX_MASTER_JS.find(body)?.groupValues?.get(1) ?: return emptyList()
|
||||||
|
val aesJson = json.decodeFromString<CryptoInfo>(master)
|
||||||
|
val key = fetchKey() ?: throw ErrorLoadingException("Unable to get key")
|
||||||
|
val decryptedScript = CryptoAES.decryptWithSalt(aesJson.ciphertext, aesJson.salt, key)
|
||||||
|
.replace("\\n", "\n")
|
||||||
|
.replace("\\", "")
|
||||||
|
|
||||||
|
val masterUrl = REGEX_SOURCES.find(decryptedScript)?.groupValues?.get(1)
|
||||||
|
?: REGEX_FILE.find(decryptedScript)?.groupValues?.get(1)
|
||||||
|
?: REGEX_SOURCE.find(decryptedScript)?.groupValues?.get(1)
|
||||||
?: return emptyList()
|
?: return emptyList()
|
||||||
|
|
||||||
val subtitleList = buildList {
|
val subtitleList = buildList {
|
||||||
val subtitles = REGEX_SUBS.findAll(data)
|
val subtitles = REGEX_SUBS.findAll(decryptedScript)
|
||||||
subtitles.forEach {
|
subtitles.forEach {
|
||||||
|
Log.d("ChillxExtractor", "Found subtitle: ${it.groupValues}")
|
||||||
add(Track(it.groupValues[1], decodeUnicodeEscape(it.groupValues[2])))
|
add(Track(it.groupValues[1], decodeUnicodeEscape(it.groupValues[2])))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val videoList = playlistUtils.extractFromHls(
|
return playlistUtils.extractFromHls(
|
||||||
playlistUrl = masterUrl,
|
playlistUrl = masterUrl,
|
||||||
referer = url,
|
referer = url,
|
||||||
videoNameGen = { "$prefix$it" },
|
videoNameGen = { "$prefix$it" },
|
||||||
subtitleList = subtitleList,
|
subtitleList = subtitleList,
|
||||||
)
|
)
|
||||||
|
|
||||||
return videoList.map {
|
|
||||||
Video(
|
|
||||||
url = it.url,
|
|
||||||
quality = it.quality,
|
|
||||||
videoUrl = it.videoUrl,
|
|
||||||
headers = it.headers,
|
|
||||||
audioTracks = it.audioTracks,
|
|
||||||
subtitleTracks = playlistUtils.fixSubtitles(it.subtitleTracks),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalSerializationApi::class)
|
||||||
|
private fun fetchKey(): String? {
|
||||||
|
return client.newCall(GET(KEY_SOURCE)).execute().parseAs<KeysData>().keys.firstOrNull()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun decodeUnicodeEscape(input: String): String {
|
private fun decodeUnicodeEscape(input: String): String {
|
||||||
|
@ -57,4 +76,16 @@ class ChillxExtractor(private val client: OkHttpClient, private val headers: Hea
|
||||||
it.groupValues[1].toInt(16).toChar().toString()
|
it.groupValues[1].toInt(16).toChar().toString()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class CryptoInfo(
|
||||||
|
@SerialName("ct") val ciphertext: String,
|
||||||
|
@SerialName("s") val salt: String,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class KeysData(
|
||||||
|
@SerialName("chillx") val keys: List<String>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
class ErrorLoadingException(message: String) : Exception(message)
|
||||||
|
|
|
@ -1,125 +0,0 @@
|
||||||
package eu.kanade.tachiyomi.lib.chillxextractor
|
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.app.Application
|
|
||||||
import android.os.Handler
|
|
||||||
import android.os.Looper
|
|
||||||
import android.webkit.JavascriptInterface
|
|
||||||
import android.webkit.WebResourceRequest
|
|
||||||
import android.webkit.WebResourceResponse
|
|
||||||
import android.webkit.WebView
|
|
||||||
import android.webkit.WebViewClient
|
|
||||||
import eu.kanade.tachiyomi.network.GET
|
|
||||||
import eu.kanade.tachiyomi.util.asJsoup
|
|
||||||
import okhttp3.Headers
|
|
||||||
import okhttp3.OkHttpClient
|
|
||||||
import uy.kohesive.injekt.injectLazy
|
|
||||||
import java.io.ByteArrayInputStream
|
|
||||||
import java.util.concurrent.CountDownLatch
|
|
||||||
import java.util.concurrent.TimeUnit
|
|
||||||
|
|
||||||
class WebViewResolver(
|
|
||||||
private val client: OkHttpClient,
|
|
||||||
private val globalHeaders: Headers,
|
|
||||||
) {
|
|
||||||
private val context: Application by injectLazy()
|
|
||||||
private val handler by lazy { Handler(Looper.getMainLooper()) }
|
|
||||||
|
|
||||||
class JsInterface(private val latch: CountDownLatch) {
|
|
||||||
var result: String? = null
|
|
||||||
|
|
||||||
@JavascriptInterface
|
|
||||||
fun passPayload(payload: String) {
|
|
||||||
result = payload
|
|
||||||
latch.countDown()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressLint("SetJavaScriptEnabled")
|
|
||||||
fun getDecryptedData(embedUrl: String): String? {
|
|
||||||
val latch = CountDownLatch(1)
|
|
||||||
var webView: WebView? = null
|
|
||||||
val jsi = JsInterface(latch)
|
|
||||||
val interfaceName = randomString()
|
|
||||||
|
|
||||||
handler.post {
|
|
||||||
val webview = WebView(context)
|
|
||||||
webView = webview
|
|
||||||
|
|
||||||
with(webview.settings) {
|
|
||||||
javaScriptEnabled = true
|
|
||||||
domStorageEnabled = true
|
|
||||||
databaseEnabled = true
|
|
||||||
useWideViewPort = false
|
|
||||||
loadWithOverviewMode = false
|
|
||||||
userAgentString = globalHeaders["User-Agent"]
|
|
||||||
}
|
|
||||||
|
|
||||||
webview.addJavascriptInterface(jsi, interfaceName)
|
|
||||||
webview.webViewClient = object : WebViewClient() {
|
|
||||||
override fun shouldInterceptRequest(
|
|
||||||
view: WebView?,
|
|
||||||
request: WebResourceRequest?
|
|
||||||
): WebResourceResponse? {
|
|
||||||
if (request?.url.toString().equals(embedUrl, true)) {
|
|
||||||
return patchBody(request!!.url.toString(), interfaceName)
|
|
||||||
?: super.shouldInterceptRequest(view, request)
|
|
||||||
}
|
|
||||||
|
|
||||||
return super.shouldInterceptRequest(view, request)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
webView?.loadUrl(embedUrl)
|
|
||||||
}
|
|
||||||
|
|
||||||
latch.await(TIMEOUT_SEC, TimeUnit.SECONDS)
|
|
||||||
|
|
||||||
handler.post {
|
|
||||||
webView?.stopLoading()
|
|
||||||
webView?.destroy()
|
|
||||||
webView = null
|
|
||||||
}
|
|
||||||
|
|
||||||
return jsi.result
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
const val TIMEOUT_SEC: Long = 30
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun randomString(length: Int = 10): String {
|
|
||||||
val charPool = ('a'..'z') + ('A'..'Z')
|
|
||||||
return List(length) { charPool.random() }.joinToString("")
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun patchBody(url: String, interfaceName: String): WebResourceResponse? {
|
|
||||||
val html = client.newCall(GET(url, globalHeaders)).execute().asJsoup()
|
|
||||||
|
|
||||||
val oldFunc = randomString()
|
|
||||||
val script = html.createElement("script").apply {
|
|
||||||
appendText(
|
|
||||||
"""
|
|
||||||
const $oldFunc = Function;
|
|
||||||
window.Function = function (...args) {
|
|
||||||
if (args.length == 1) {
|
|
||||||
window.$interfaceName.passPayload(args[0]);
|
|
||||||
}
|
|
||||||
return $oldFunc(...args);
|
|
||||||
};
|
|
||||||
""".trimIndent()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
html.body().insertChildren(0, script)
|
|
||||||
|
|
||||||
return WebResourceResponse(
|
|
||||||
"text/html",
|
|
||||||
"utf-8",
|
|
||||||
200,
|
|
||||||
"ok",
|
|
||||||
mapOf("server" to "cloudflare"),
|
|
||||||
ByteArrayInputStream(html.outerHtml().toByteArray()),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,8 +1,5 @@
|
||||||
package eu.kanade.tachiyomi.lib.filemoonextractor
|
package eu.kanade.tachiyomi.lib.filemoonextractor
|
||||||
|
|
||||||
import android.content.SharedPreferences
|
|
||||||
import androidx.preference.EditTextPreference
|
|
||||||
import androidx.preference.PreferenceScreen
|
|
||||||
import dev.datlag.jsunpacker.JsUnpacker
|
import dev.datlag.jsunpacker.JsUnpacker
|
||||||
import eu.kanade.tachiyomi.animesource.model.Track
|
import eu.kanade.tachiyomi.animesource.model.Track
|
||||||
import eu.kanade.tachiyomi.animesource.model.Video
|
import eu.kanade.tachiyomi.animesource.model.Video
|
||||||
|
@ -16,28 +13,20 @@ import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
|
||||||
class FilemoonExtractor(
|
class FilemoonExtractor(private val client: OkHttpClient) {
|
||||||
private val client: OkHttpClient,
|
|
||||||
private val preferences: SharedPreferences? = null,
|
|
||||||
) {
|
|
||||||
|
|
||||||
private val playlistUtils by lazy { PlaylistUtils(client) }
|
private val playlistUtils by lazy { PlaylistUtils(client) }
|
||||||
private val json: Json by injectLazy()
|
private val json: Json by injectLazy()
|
||||||
|
|
||||||
fun videosFromUrl(url: String, prefix: String = "Filemoon - ", headers: Headers? = null): List<Video> {
|
fun videosFromUrl(url: String, prefix: String = "Filemoon - ", headers: Headers? = null): List<Video> {
|
||||||
var httpUrl = url.toHttpUrl()
|
val httpUrl = url.toHttpUrl()
|
||||||
val videoHeaders = (headers?.newBuilder() ?: Headers.Builder())
|
val videoHeaders = (headers?.newBuilder() ?: Headers.Builder())
|
||||||
.set("Referer", url)
|
.set("Referer", url)
|
||||||
.set("Origin", "https://${httpUrl.host}")
|
.set("Origin", "https://${httpUrl.host}")
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
val doc = client.newCall(GET(url, videoHeaders)).execute().asJsoup()
|
val doc = client.newCall(GET(url, videoHeaders)).execute().asJsoup()
|
||||||
val jsEval = doc.selectFirst("script:containsData(eval):containsData(m3u8)")?.data() ?: run {
|
val jsEval = doc.selectFirst("script:containsData(eval):containsData(m3u8)")!!.data()
|
||||||
val iframeUrl = doc.selectFirst("iframe[src]")!!.attr("src")
|
|
||||||
httpUrl = iframeUrl.toHttpUrl()
|
|
||||||
val iframeDoc = client.newCall(GET(iframeUrl, videoHeaders)).execute().asJsoup()
|
|
||||||
iframeDoc.selectFirst("script:containsData(eval):containsData(m3u8)")!!.data()
|
|
||||||
}
|
|
||||||
val unpacked = JsUnpacker.unpackAndCombine(jsEval).orEmpty()
|
val unpacked = JsUnpacker.unpackAndCombine(jsEval).orEmpty()
|
||||||
val masterUrl = unpacked.takeIf(String::isNotBlank)
|
val masterUrl = unpacked.takeIf(String::isNotBlank)
|
||||||
?.substringAfter("{file:\"", "")
|
?.substringAfter("{file:\"", "")
|
||||||
|
@ -61,39 +50,14 @@ class FilemoonExtractor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val videoList = playlistUtils.extractFromHls(
|
return playlistUtils.extractFromHls(
|
||||||
masterUrl,
|
masterUrl,
|
||||||
subtitleList = subtitleTracks,
|
subtitleList = subtitleTracks,
|
||||||
referer = "https://${httpUrl.host}/",
|
referer = "https://${httpUrl.host}/",
|
||||||
videoNameGen = { "$prefix$it" },
|
videoNameGen = { "$prefix$it" },
|
||||||
)
|
)
|
||||||
|
|
||||||
val subPref = preferences?.getString(PREF_SUBTITLE_KEY, PREF_SUBTITLE_DEFAULT).orEmpty()
|
|
||||||
return videoList.map {
|
|
||||||
Video(
|
|
||||||
url = it.url,
|
|
||||||
quality = it.quality,
|
|
||||||
videoUrl = it.videoUrl,
|
|
||||||
audioTracks = it.audioTracks,
|
|
||||||
subtitleTracks = it.subtitleTracks.filter { tracks -> tracks.lang.contains(subPref, true) }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class SubtitleDto(val file: String, val label: String)
|
data class SubtitleDto(val file: String, val label: String)
|
||||||
|
|
||||||
companion object {
|
|
||||||
fun addSubtitlePref(screen: PreferenceScreen) {
|
|
||||||
EditTextPreference(screen.context).apply {
|
|
||||||
key = PREF_SUBTITLE_KEY
|
|
||||||
title = "Filemoon subtitle preference"
|
|
||||||
summary = "Leave blank to use all subs"
|
|
||||||
setDefaultValue(PREF_SUBTITLE_DEFAULT)
|
|
||||||
}.also(screen::addPreference)
|
|
||||||
}
|
|
||||||
|
|
||||||
private const val PREF_SUBTITLE_KEY = "pref_filemoon_sub_lang_key"
|
|
||||||
private const val PREF_SUBTITLE_DEFAULT = "eng"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,18 +3,16 @@ package eu.kanade.tachiyomi.lib.luluextractor
|
||||||
import eu.kanade.tachiyomi.animesource.model.Video
|
import eu.kanade.tachiyomi.animesource.model.Video
|
||||||
import eu.kanade.tachiyomi.network.GET
|
import eu.kanade.tachiyomi.network.GET
|
||||||
import okhttp3.Headers
|
import okhttp3.Headers
|
||||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import java.util.regex.Pattern
|
import java.util.regex.Pattern
|
||||||
|
|
||||||
class LuluExtractor(private val client: OkHttpClient, headers: Headers) {
|
class LuluExtractor(private val client: OkHttpClient) {
|
||||||
|
|
||||||
private val headers = headers.newBuilder()
|
private val headers = Headers.Builder()
|
||||||
.add("Referer", "https://luluvdo.com/")
|
.add("Referer", "https://luluvdo.com")
|
||||||
.add("Origin", "https://luluvdo.com")
|
.add("Origin", "https://luluvdo.com")
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
//Credit: https://github.com/skoruppa/docchi-stremio-addon/blob/main/app/players/lulustream.py
|
|
||||||
fun videosFromUrl(url: String, prefix: String): List<Video> {
|
fun videosFromUrl(url: String, prefix: String): List<Video> {
|
||||||
val videos = mutableListOf<Video>()
|
val videos = mutableListOf<Video>()
|
||||||
|
|
||||||
|
@ -24,7 +22,7 @@ class LuluExtractor(private val client: OkHttpClient, headers: Headers) {
|
||||||
val fixedUrl = fixM3u8Link(m3u8Url)
|
val fixedUrl = fixM3u8Link(m3u8Url)
|
||||||
val quality = getResolution(fixedUrl)
|
val quality = getResolution(fixedUrl)
|
||||||
|
|
||||||
videos.add(Video(fixedUrl, "${prefix}Lulu - $quality", fixedUrl, headers))
|
videos.add(Video(fixedUrl, "${prefix}Lulu - $quality", fixedUrl))
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
}
|
}
|
||||||
|
@ -52,41 +50,34 @@ class LuluExtractor(private val client: OkHttpClient, headers: Headers) {
|
||||||
|
|
||||||
private fun fixM3u8Link(link: String): String {
|
private fun fixM3u8Link(link: String): String {
|
||||||
val paramOrder = listOf("t", "s", "e", "f")
|
val paramOrder = listOf("t", "s", "e", "f")
|
||||||
val params = Pattern.compile("[?&]([^=]*)=([^&]*)").matcher(link).let { matcher ->
|
val baseUrl = link.split("?").first()
|
||||||
generateSequence { if (matcher.find()) matcher.group(1) to matcher.group(2) else null }.toList()
|
val params = link.split("?").getOrNull(1)?.split("&") ?: emptyList()
|
||||||
}
|
|
||||||
|
|
||||||
val paramDict = mutableMapOf<String, String>()
|
val paramMap = mutableMapOf<String, String>()
|
||||||
val extraParams = mutableMapOf<String, String>()
|
val extraParams = mutableMapOf(
|
||||||
|
"i" to "0.3",
|
||||||
|
"sp" to "0"
|
||||||
|
)
|
||||||
|
|
||||||
params.forEachIndexed { index, (key , value) ->
|
params.forEachIndexed { index, param ->
|
||||||
if (key.isNullOrEmpty()) {
|
val parts = param.split("=")
|
||||||
if (index < paramOrder.size) {
|
when {
|
||||||
if (value != null) {
|
parts.size == 2 -> {
|
||||||
paramDict[paramOrder[index]] = value
|
val (key, value) = parts
|
||||||
}
|
if (key in paramOrder) paramMap[key] = value
|
||||||
}
|
else extraParams[key] = value
|
||||||
} else {
|
|
||||||
if (value != null) {
|
|
||||||
extraParams[key] = value
|
|
||||||
}
|
}
|
||||||
|
index < paramOrder.size -> paramMap[paramOrder[index]] = parts.firstOrNull() ?: ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extraParams["i"] = "0.3"
|
return buildString {
|
||||||
extraParams["sp"] = "0"
|
append(baseUrl)
|
||||||
|
append("?")
|
||||||
val baseUrl = link.split("?")[0]
|
append(paramOrder.joinToString("&") { "$it=${paramMap[it]}" })
|
||||||
|
append("&")
|
||||||
val fixedLink = baseUrl.toHttpUrl().newBuilder()
|
append(extraParams.map { "${it.key}=${it.value}" }.joinToString("&"))
|
||||||
paramOrder.filter { paramDict.containsKey(it) }.forEach { key ->
|
|
||||||
fixedLink.addQueryParameter(key, paramDict[key])
|
|
||||||
}
|
}
|
||||||
extraParams.forEach { (key, value) ->
|
|
||||||
fixedLink.addQueryParameter(key, value)
|
|
||||||
}
|
|
||||||
|
|
||||||
return fixedLink.build().toString()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getResolution(m3u8Url: String): String {
|
private fun getResolution(m3u8Url: String): String {
|
||||||
|
@ -107,10 +98,11 @@ class LuluExtractor(private val client: OkHttpClient, headers: Headers) {
|
||||||
}
|
}
|
||||||
|
|
||||||
object JavaScriptUnpacker {
|
object JavaScriptUnpacker {
|
||||||
private val UNPACK_REGEX by lazy {
|
private val UNPACK_REGEX = Regex(
|
||||||
Regex("""\}\('(.*)', *(\d+), *(\d+), *'(.*?)'\.split\('\|'\)""",
|
"""}\('(.*)', *(\d+), *(\d+), *'(.*?)'\.split\('\|'\)""",
|
||||||
RegexOption.DOT_MATCHES_ALL)
|
RegexOption.DOT_MATCHES_ALL
|
||||||
}
|
)
|
||||||
|
|
||||||
fun unpack(encodedJs: String): String? {
|
fun unpack(encodedJs: String): String? {
|
||||||
val match = UNPACK_REGEX.find(encodedJs) ?: return null
|
val match = UNPACK_REGEX.find(encodedJs) ?: return null
|
||||||
val (payload, radixStr, countStr, symtabStr) = match.destructured
|
val (payload, radixStr, countStr, symtabStr) = match.destructured
|
||||||
|
@ -129,8 +121,8 @@ object JavaScriptUnpacker {
|
||||||
return Regex("""\b\w+\b""").replace(payload) { mr ->
|
return Regex("""\b\w+\b""").replace(payload) { mr ->
|
||||||
symtab.getOrNull(unbase(mr.value, radix, baseDict)) ?: mr.value
|
symtab.getOrNull(unbase(mr.value, radix, baseDict)) ?: mr.value
|
||||||
}.replace("\\", "")
|
}.replace("\\", "")
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun unbase(value: String, radix: Int, dict: Map<Char, Int>): Int {
|
private fun unbase(value: String, radix: Int, dict: Map<Char, Int>): Int {
|
||||||
var result = 0
|
var result = 0
|
||||||
var multiplier = 1
|
var multiplier = 1
|
||||||
|
|
|
@ -4,12 +4,13 @@ import eu.kanade.tachiyomi.animesource.model.Video
|
||||||
import eu.kanade.tachiyomi.network.GET
|
import eu.kanade.tachiyomi.network.GET
|
||||||
import android.util.Base64
|
import android.util.Base64
|
||||||
import eu.kanade.tachiyomi.util.asJsoup
|
import eu.kanade.tachiyomi.util.asJsoup
|
||||||
import eu.kanade.tachiyomi.util.parseAs
|
import kotlinx.serialization.json.Json
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import okhttp3.Headers
|
import okhttp3.Headers
|
||||||
import okhttp3.HttpUrl
|
import okhttp3.HttpUrl
|
||||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
|
import uy.kohesive.injekt.injectLazy
|
||||||
import java.nio.charset.Charset
|
import java.nio.charset.Charset
|
||||||
|
|
||||||
class LycorisCafeExtractor(private val client: OkHttpClient) {
|
class LycorisCafeExtractor(private val client: OkHttpClient) {
|
||||||
|
@ -18,15 +19,7 @@ class LycorisCafeExtractor(private val client: OkHttpClient) {
|
||||||
|
|
||||||
private val GETLNKURL = "https://www.lycoris.cafe/api/watch/getLink"
|
private val GETLNKURL = "https://www.lycoris.cafe/api/watch/getLink"
|
||||||
|
|
||||||
private val wordsRegex by lazy {
|
private val json: Json by injectLazy()
|
||||||
Regex(
|
|
||||||
"""\\U([0-9a-fA-F]{8})|""" + // \UXXXXXXXX
|
|
||||||
"""\\u([0-9a-fA-F]{4})|""" + // \uXXXX
|
|
||||||
"""\\x([0-9a-fA-F]{2})|""" + // \xHH
|
|
||||||
"""\\([0-7]{1,3})|""" + // \OOO (octal)
|
|
||||||
"""\\([btnfr"'$\\])""" // \n, \t, itd.
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Credit: https://github.com/skoruppa/docchi-stremio-addon/blob/main/app/players/lycoris.py
|
// Credit: https://github.com/skoruppa/docchi-stremio-addon/blob/main/app/players/lycoris.py
|
||||||
fun getVideosFromUrl(url: String, headers: Headers, prefix: String): List<Video> {
|
fun getVideosFromUrl(url: String, headers: Headers, prefix: String): List<Video> {
|
||||||
|
@ -40,63 +33,72 @@ class LycorisCafeExtractor(private val client: OkHttpClient) {
|
||||||
GET(url, headers = embedHeaders),
|
GET(url, headers = embedHeaders),
|
||||||
).execute().asJsoup()
|
).execute().asJsoup()
|
||||||
|
|
||||||
val script = document.selectFirst("script[type='application/json']")?.data() ?: return emptyList()
|
val scripts = document.select("script")
|
||||||
|
|
||||||
val scriptData = script.parseAs<ScriptBody>()
|
val episodeDataPattern = Regex("episodeInfo\\s*:\\s*(\\{.*?\\}),", RegexOption.DOT_MATCHES_ALL)
|
||||||
|
var episodeData: String? = null
|
||||||
|
|
||||||
val data = scriptData.body.parseAs<ScriptEpisode>()
|
for (script in scripts) {
|
||||||
|
val content = script.data()
|
||||||
|
val match = episodeDataPattern.find(content)
|
||||||
|
|
||||||
val linkList = data.episodeInfo.id?.let {
|
if (match != null) {
|
||||||
fetchAndDecodeVideo(client, data.episodeInfo.id.toString(), isSecondary = false)
|
episodeData = match.groupValues[1]
|
||||||
|
break
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val fhdLink = data.episodeInfo.FHD?.let {
|
val result = mutableMapOf<String, String?>()
|
||||||
fetchAndDecodeVideo(client, data.episodeInfo.FHD, isSecondary = true)
|
|
||||||
}
|
val patterns = listOf(
|
||||||
val sdLink = data.episodeInfo.SD?.let {
|
"id" to Regex("id\\s*:\\s*(\\d+)"),
|
||||||
fetchAndDecodeVideo(client, data.episodeInfo.SD, isSecondary = true)
|
"FHD" to Regex("FHD\\s*:\\s*\"([^\"]+)\""),
|
||||||
}
|
"HD" to Regex("HD\\s*:\\s*\"([^\"]+)\""),
|
||||||
val hdLink = data.episodeInfo.HD?.let {
|
"SD" to Regex("SD\\s*:\\s*\"([^\"]+)\"")
|
||||||
fetchAndDecodeVideo(client, data.episodeInfo.HD, isSecondary = true)
|
)
|
||||||
|
|
||||||
|
patterns.forEach { (key, pattern) ->
|
||||||
|
result[key] = episodeData?.let { pattern.find(it)?.groups?.get(1)?.value }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var linkList: String? = fetchAndDecodeVideo(client, result["id"].toString(), isSecondary = false).toString()
|
||||||
|
|
||||||
|
val fhdLink = fetchAndDecodeVideo(client, result["FHD"].toString(), isSecondary = true).toString()
|
||||||
|
val sdLink = fetchAndDecodeVideo(client, result["SD"].toString(), isSecondary = true).toString()
|
||||||
|
val hdLink = fetchAndDecodeVideo(client, result["HD"].toString(), isSecondary = true).toString()
|
||||||
|
|
||||||
|
|
||||||
if (linkList.isNullOrBlank() || linkList == "{}") {
|
if (linkList.isNullOrBlank() || linkList == "{}") {
|
||||||
if (!fhdLink.isNullOrBlank()) {
|
if (fhdLink.isNotEmpty()) {
|
||||||
videos.add(Video(fhdLink, "${prefix}lycoris.cafe - 1080p", fhdLink))
|
videos.add(Video(fhdLink, "${prefix}lycoris.cafe - 1080p", fhdLink))
|
||||||
}
|
}
|
||||||
if (!hdLink.isNullOrBlank()) {
|
if (hdLink.isNotEmpty()) {
|
||||||
videos.add(Video(hdLink, "${prefix}lycoris.cafe - 720p", hdLink))
|
videos.add(Video(hdLink, "${prefix}lycoris.cafe - 720p", hdLink))
|
||||||
}
|
}
|
||||||
if (!sdLink.isNullOrBlank()) {
|
if (sdLink.isNotEmpty()) {
|
||||||
videos.add(Video(sdLink, "${prefix}lycoris.cafe - 480p", sdLink))
|
videos.add(Video(sdLink, "${prefix}lycoris.cafe - 480p", sdLink))
|
||||||
}
|
}
|
||||||
}else {
|
}else {
|
||||||
val videoLinks = linkList.parseAs<VideoLinksApi>()
|
val videoLinks = Json.decodeFromString<VideoLinks>(linkList)
|
||||||
|
|
||||||
videoLinks.FHD?.takeIf { checkLinks(client, it) }?.let {
|
videoLinks.FHD?.takeIf { checkLinks(client, it) }?.let {
|
||||||
videos.add(Video(it, "${prefix}lycoris.cafe - 1080p", it))
|
videos.add(Video(it, "${prefix}lycoris.cafe - 1080p", it))
|
||||||
} ?: fhdLink?.takeIf { checkLinks(client, it) }?.let {
|
}?: videos.add(Video(fhdLink, "${prefix}lycoris.cafe - 1080p", fhdLink))
|
||||||
videos.add(Video(it, "${prefix}lycoris.cafe - 1080p", it))
|
|
||||||
}
|
|
||||||
|
|
||||||
videoLinks.HD?.takeIf { checkLinks(client, it) }?.let {
|
videoLinks.HD?.takeIf { checkLinks(client, it) }?.let {
|
||||||
videos.add(Video(it, "${prefix}lycoris.cafe - 720p", it))
|
videos.add(Video(it, "${prefix}lycoris.cafe - 720p", it))
|
||||||
} ?: hdLink?.takeIf { checkLinks(client, it) }?.let {
|
}?: videos.add(Video(hdLink, "${prefix}lycoris.cafe - 720p", hdLink))
|
||||||
videos.add(Video(it, "${prefix}lycoris.cafe - 720p", it))
|
|
||||||
}
|
|
||||||
|
|
||||||
videoLinks.SD?.takeIf { checkLinks(client, it) }?.let {
|
videoLinks.SD?.takeIf { checkLinks(client, it) }?.let {
|
||||||
videos.add(Video(it, "${prefix}lycoris.cafe - 480p", it))
|
videos.add(Video(it, "${prefix}lycoris.cafe - 480p", it))
|
||||||
} ?: sdLink?.takeIf { checkLinks(client, it) }?.let {
|
}?: videos.add(Video(sdLink, "${prefix}lycoris.cafe - 480p", sdLink))
|
||||||
videos.add(Video(it, "${prefix}lycoris.cafe - 480p", it))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return videos
|
return videos
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun decodeVideoLinks(encodedUrl: String): String? {
|
private fun decodeVideoLinks(encodedUrl: String?): Any? {
|
||||||
if (encodedUrl.isBlank()) {
|
if (encodedUrl.isNullOrEmpty()) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -119,7 +121,7 @@ class LycorisCafeExtractor(private val client: OkHttpClient) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun fetchAndDecodeVideo(client: OkHttpClient, episodeId: String, isSecondary: Boolean = false): String? {
|
private fun fetchAndDecodeVideo(client: OkHttpClient, episodeId: String, isSecondary: Boolean = false): Any? {
|
||||||
val url: HttpUrl
|
val url: HttpUrl
|
||||||
|
|
||||||
if (isSecondary) {
|
if (isSecondary) {
|
||||||
|
@ -128,24 +130,22 @@ class LycorisCafeExtractor(private val client: OkHttpClient) {
|
||||||
val finalText = unicodeEscape.toByteArray(Charsets.ISO_8859_1).toString(Charsets.UTF_8)
|
val finalText = unicodeEscape.toByteArray(Charsets.ISO_8859_1).toString(Charsets.UTF_8)
|
||||||
|
|
||||||
url = GETLNKURL.toHttpUrl().newBuilder()
|
url = GETLNKURL.toHttpUrl().newBuilder()
|
||||||
.addQueryParameter("link", finalText)
|
?.addQueryParameter("link", finalText)
|
||||||
.build()
|
?.build() ?: throw IllegalStateException("Invalid URL")
|
||||||
} else {
|
} else {
|
||||||
url = GETSECONDARYURL.toHttpUrl().newBuilder()
|
url = GETSECONDARYURL.toHttpUrl().newBuilder()
|
||||||
.addQueryParameter("id", episodeId)
|
?.addQueryParameter("id", episodeId)
|
||||||
.build()
|
?.build() ?: throw IllegalStateException("Invalid URL")
|
||||||
}
|
}
|
||||||
client.newCall(GET(url))
|
client.newCall(GET(url))
|
||||||
.execute()
|
.execute()
|
||||||
.use { response ->
|
.use { response ->
|
||||||
val data = response.body.string()
|
val data = response.body.string() ?: ""
|
||||||
return decodeVideoLinks(data)
|
return decodeVideoLinks(data)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun checkLinks(client: OkHttpClient, link: String): Boolean {
|
private fun checkLinks(client: OkHttpClient, link: String): Boolean {
|
||||||
if (!link.contains("https://")) return false
|
|
||||||
|
|
||||||
client.newCall(GET(link)).execute().use { response ->
|
client.newCall(GET(link)).execute().use { response ->
|
||||||
return response.code.toString() == "200"
|
return response.code.toString() == "200"
|
||||||
}
|
}
|
||||||
|
@ -155,7 +155,16 @@ class LycorisCafeExtractor(private val client: OkHttpClient) {
|
||||||
// 1. Obsługa kontynuacji linii (backslash + newline)
|
// 1. Obsługa kontynuacji linii (backslash + newline)
|
||||||
val withoutLineContinuation = text.replace("\\\n", "")
|
val withoutLineContinuation = text.replace("\\\n", "")
|
||||||
|
|
||||||
return wordsRegex.replace(withoutLineContinuation) { match ->
|
// 2. Regex do wykrywania wszystkich sekwencji escape
|
||||||
|
val regex = Regex(
|
||||||
|
"""\\U([0-9a-fA-F]{8})|""" + // \UXXXXXXXX
|
||||||
|
"""\\u([0-9a-fA-F]{4})|""" + // \uXXXX
|
||||||
|
"""\\x([0-9a-fA-F]{2})|""" + // \xHH
|
||||||
|
"""\\([0-7]{1,3})|""" + // \OOO (octal)
|
||||||
|
"""\\([btnfr"'$\\\\])""" // \n, \t, itd.
|
||||||
|
)
|
||||||
|
|
||||||
|
return regex.replace(withoutLineContinuation) { match ->
|
||||||
val (u8, u4, x2, octal, simple) = match.destructured
|
val (u8, u4, x2, octal, simple) = match.destructured
|
||||||
when {
|
when {
|
||||||
u8.isNotEmpty() -> handleUnicode8(u8)
|
u8.isNotEmpty() -> handleUnicode8(u8)
|
||||||
|
@ -199,32 +208,16 @@ class LycorisCafeExtractor(private val client: OkHttpClient) {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class ScriptBody(
|
data class VideoLinks(
|
||||||
val body: String
|
|
||||||
)
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class ScriptEpisode(
|
|
||||||
val episodeInfo: EpisodeInfo
|
|
||||||
)
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class EpisodeInfo(
|
|
||||||
val id: Int? = null,
|
|
||||||
val FHD: String? = null,
|
|
||||||
val HD: String? = null,
|
|
||||||
val SD: String? = null,
|
|
||||||
)
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class VideoLinksApi(
|
|
||||||
val HD: String? = null,
|
val HD: String? = null,
|
||||||
val SD: String? = null,
|
val SD: String? = null,
|
||||||
val FHD: String? = null,
|
val FHD: String? = null,
|
||||||
val Source: String? = null,
|
val Source: String? = null,
|
||||||
|
val preview: String? = null,
|
||||||
val SourceMKV: String? = null
|
val SourceMKV: String? = null
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -132,7 +132,6 @@ class MegaCloudExtractor(
|
||||||
?.filter { it.kind == "captions" }
|
?.filter { it.kind == "captions" }
|
||||||
?.map { Track(it.file, it.label) }
|
?.map { Track(it.file, it.label) }
|
||||||
.orEmpty()
|
.orEmpty()
|
||||||
.let { playlistUtils.fixSubtitles(it) }
|
|
||||||
return playlistUtils.extractFromHls(
|
return playlistUtils.extractFromHls(
|
||||||
masterUrl,
|
masterUrl,
|
||||||
videoNameGen = { "$name - $it - $type" },
|
videoNameGen = { "$name - $it - $type" },
|
||||||
|
@ -142,7 +141,7 @@ class MegaCloudExtractor(
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getVideoDto(url: String): VideoDto {
|
private fun getVideoDto(url: String): VideoDto {
|
||||||
val type = if (url.startsWith("https://megacloud.tv") or url.startsWith("https://megacloud.blog")) 0 else 1
|
val type = if (url.startsWith("https://megacloud.tv") or url.startsWith("https://megacloud.club")) 0 else 1
|
||||||
|
|
||||||
val keyType = SOURCES_KEY[type]
|
val keyType = SOURCES_KEY[type]
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
package eu.kanade.tachiyomi.lib.playlistutils
|
package eu.kanade.tachiyomi.lib.playlistutils
|
||||||
|
|
||||||
import android.net.Uri
|
|
||||||
import eu.kanade.tachiyomi.animesource.model.Track
|
import eu.kanade.tachiyomi.animesource.model.Track
|
||||||
import eu.kanade.tachiyomi.animesource.model.Video
|
import eu.kanade.tachiyomi.animesource.model.Video
|
||||||
import eu.kanade.tachiyomi.network.GET
|
import eu.kanade.tachiyomi.network.GET
|
||||||
|
@ -9,7 +8,7 @@ import okhttp3.Headers
|
||||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import okhttp3.internal.commonEmptyHeaders
|
import okhttp3.internal.commonEmptyHeaders
|
||||||
import java.io.File
|
import kotlin.math.abs
|
||||||
|
|
||||||
class PlaylistUtils(private val client: OkHttpClient, private val headers: Headers = commonEmptyHeaders) {
|
class PlaylistUtils(private val client: OkHttpClient, private val headers: Headers = commonEmptyHeaders) {
|
||||||
|
|
||||||
|
@ -137,7 +136,7 @@ class PlaylistUtils(private val client: OkHttpClient, private val headers: Heade
|
||||||
val resolution = it.substringAfter("RESOLUTION=")
|
val resolution = it.substringAfter("RESOLUTION=")
|
||||||
.substringBefore("\n")
|
.substringBefore("\n")
|
||||||
.substringAfter("x")
|
.substringAfter("x")
|
||||||
.substringBefore(",")
|
.substringBefore(",").let(::stnQuality)
|
||||||
|
|
||||||
val videoUrl = it.substringAfter("\n").substringBefore("\n").let { url ->
|
val videoUrl = it.substringAfter("\n").substringBefore("\n").let { url ->
|
||||||
getAbsoluteUrl(url, playlistUrl, masterUrlBasePath)?.trimEnd()
|
getAbsoluteUrl(url, playlistUrl, masterUrlBasePath)?.trimEnd()
|
||||||
|
@ -336,32 +335,14 @@ class PlaylistUtils(private val client: OkHttpClient, private val headers: Heade
|
||||||
|
|
||||||
// ============================= Utilities ==============================
|
// ============================= Utilities ==============================
|
||||||
|
|
||||||
private fun cleanSubtitleData(matchResult: MatchResult): String {
|
private fun stnQuality(quality: String): String {
|
||||||
val lineCount = matchResult.groupValues[1].count { it == '\n' }
|
val intQuality = quality.trim().toInt()
|
||||||
return "\n" + " \n".repeat(lineCount - 1)
|
val standardQualities = listOf(144, 240, 360, 480, 720, 1080)
|
||||||
}
|
val result = standardQualities.minByOrNull { abs(it - intQuality) } ?: quality
|
||||||
|
return "${result}p"
|
||||||
fun fixSubtitles(subtitleList: List<Track>): List<Track> {
|
|
||||||
return subtitleList.mapNotNull {
|
|
||||||
try {
|
|
||||||
val subData = client.newCall(GET(it.url)).execute().body.string()
|
|
||||||
|
|
||||||
val file = File.createTempFile("subs", "vtt")
|
|
||||||
.also(File::deleteOnExit)
|
|
||||||
|
|
||||||
file.writeText(FIX_SUBTITLE_REGEX.replace(subData, ::cleanSubtitleData))
|
|
||||||
val uri = Uri.fromFile(file)
|
|
||||||
|
|
||||||
Track(uri.toString(), it.lang)
|
|
||||||
} catch (_: Exception) {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private val FIX_SUBTITLE_REGEX = Regex("""${'$'}(\n{2,})(?!(?:\d+:)*\d+(?:\.\d+)?\s-+>\s(?:\d+:)*\d+(?:\.\d+)?)""", RegexOption.MULTILINE)
|
|
||||||
|
|
||||||
private const val PLAYLIST_SEPARATOR = "#EXT-X-STREAM-INF:"
|
private const val PLAYLIST_SEPARATOR = "#EXT-X-STREAM-INF:"
|
||||||
|
|
||||||
private val SUBTITLE_REGEX by lazy { Regex("""#EXT-X-MEDIA:TYPE=SUBTITLES.*?NAME="(.*?)".*?URI="(.*?)"""") }
|
private val SUBTITLE_REGEX by lazy { Regex("""#EXT-X-MEDIA:TYPE=SUBTITLES.*?NAME="(.*?)".*?URI="(.*?)"""") }
|
||||||
|
|
|
@ -1,7 +0,0 @@
|
||||||
plugins {
|
|
||||||
id("lib-android")
|
|
||||||
}
|
|
||||||
|
|
||||||
dependencies {
|
|
||||||
implementation(project(":lib:playlist-utils"))
|
|
||||||
}
|
|
|
@ -1,67 +0,0 @@
|
||||||
package eu.kanade.tachiyomi.lib.savefileextractor
|
|
||||||
|
|
||||||
import android.content.SharedPreferences
|
|
||||||
import androidx.preference.EditTextPreference
|
|
||||||
import androidx.preference.PreferenceScreen
|
|
||||||
import eu.kanade.tachiyomi.animesource.model.Video
|
|
||||||
import eu.kanade.tachiyomi.lib.playlistutils.PlaylistUtils
|
|
||||||
import eu.kanade.tachiyomi.network.GET
|
|
||||||
import eu.kanade.tachiyomi.util.asJsoup
|
|
||||||
import okhttp3.Headers
|
|
||||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
|
||||||
import okhttp3.OkHttpClient
|
|
||||||
|
|
||||||
class SavefileExtractor(
|
|
||||||
private val client: OkHttpClient,
|
|
||||||
private val preferences: SharedPreferences,
|
|
||||||
) {
|
|
||||||
|
|
||||||
private val playlistUtils by lazy { PlaylistUtils(client) }
|
|
||||||
|
|
||||||
fun videosFromUrl(url: String, prefix: String = "Savefile - ", headers: Headers? = null): List<Video> {
|
|
||||||
val httpUrl = url.toHttpUrl()
|
|
||||||
val videoHeaders = (headers?.newBuilder() ?: Headers.Builder())
|
|
||||||
.set("Referer", url)
|
|
||||||
.set("Origin", "https://${httpUrl.host}")
|
|
||||||
.build()
|
|
||||||
|
|
||||||
val doc = client.newCall(GET(url, videoHeaders)).execute().asJsoup()
|
|
||||||
val js = doc.selectFirst("script:containsData(m3u8)")!!.data()
|
|
||||||
val masterUrl = js.takeIf(String::isNotBlank)
|
|
||||||
?.substringAfter("{file:\"", "")
|
|
||||||
?.substringBefore("\"}", "")
|
|
||||||
?.takeIf(String::isNotBlank)
|
|
||||||
?: return emptyList()
|
|
||||||
|
|
||||||
val videoList = playlistUtils.extractFromHls(
|
|
||||||
masterUrl,
|
|
||||||
referer = "https://${httpUrl.host}/",
|
|
||||||
videoNameGen = { "$prefix$it" },
|
|
||||||
)
|
|
||||||
|
|
||||||
val subPref = preferences.getString(PREF_SUBTITLE_KEY, PREF_SUBTITLE_DEFAULT).orEmpty()
|
|
||||||
return videoList.map {
|
|
||||||
Video(
|
|
||||||
url = it.url,
|
|
||||||
quality = it.quality,
|
|
||||||
videoUrl = it.videoUrl,
|
|
||||||
audioTracks = it.audioTracks,
|
|
||||||
subtitleTracks = it.subtitleTracks.filter { tracks -> tracks.lang.contains(subPref, true) }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
fun addSubtitlePref(screen: PreferenceScreen) {
|
|
||||||
EditTextPreference(screen.context).apply {
|
|
||||||
key = PREF_SUBTITLE_KEY
|
|
||||||
title = "Savefile subtitle preference"
|
|
||||||
summary = "Leave blank to use all subs"
|
|
||||||
setDefaultValue(PREF_SUBTITLE_DEFAULT)
|
|
||||||
}.also(screen::addPreference)
|
|
||||||
}
|
|
||||||
|
|
||||||
private const val PREF_SUBTITLE_KEY = "pref_savefile_sub_lang_key"
|
|
||||||
private const val PREF_SUBTITLE_DEFAULT = "eng"
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -10,7 +10,6 @@ import kotlinx.serialization.Serializable
|
||||||
import kotlinx.serialization.SerializationException
|
import kotlinx.serialization.SerializationException
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import okhttp3.Headers
|
import okhttp3.Headers
|
||||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
|
|
||||||
class StreamWishExtractor(private val client: OkHttpClient, private val headers: Headers) {
|
class StreamWishExtractor(private val client: OkHttpClient, private val headers: Headers) {
|
||||||
|
@ -31,19 +30,16 @@ class StreamWishExtractor(private val client: OkHttpClient, private val headers:
|
||||||
script
|
script
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val masterUrl = scriptBody?.let {
|
val masterUrl = scriptBody
|
||||||
M3U8_REGEX.find(it)?.value
|
?.substringAfter("source", "")
|
||||||
}
|
?.substringAfter("file:\"", "")
|
||||||
|
?.substringBefore("\"", "")
|
||||||
|
?.takeIf(String::isNotBlank)
|
||||||
?: return emptyList()
|
?: return emptyList()
|
||||||
|
|
||||||
val subtitleList = extractSubtitles(scriptBody)
|
val subtitleList = extractSubtitles(scriptBody)
|
||||||
|
|
||||||
return playlistUtils.extractFromHls(
|
return playlistUtils.extractFromHls(masterUrl, url, videoNameGen = videoNameGen, subtitleList = subtitleList)
|
||||||
playlistUrl = masterUrl,
|
|
||||||
referer = "https://${url.toHttpUrl().host}/",
|
|
||||||
videoNameGen = videoNameGen,
|
|
||||||
subtitleList = playlistUtils.fixSubtitles(subtitleList),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getEmbedUrl(url: String): String {
|
private fun getEmbedUrl(url: String): String {
|
||||||
|
@ -61,11 +57,7 @@ class StreamWishExtractor(private val client: OkHttpClient, private val headers:
|
||||||
.substringAfter("tracks")
|
.substringAfter("tracks")
|
||||||
.substringAfter("[")
|
.substringAfter("[")
|
||||||
.substringBefore("]")
|
.substringBefore("]")
|
||||||
val fixedSubtitleStr = FIX_TRACKS_REGEX.replace(subtitleStr) { match ->
|
json.decodeFromString<List<TrackDto>>("[$subtitleStr]")
|
||||||
"\"${match.value}\""
|
|
||||||
}
|
|
||||||
|
|
||||||
json.decodeFromString<List<TrackDto>>("[$fixedSubtitleStr]")
|
|
||||||
.filter { it.kind.equals("captions", true) }
|
.filter { it.kind.equals("captions", true) }
|
||||||
.map { Track(it.file, it.label ?: "") }
|
.map { Track(it.file, it.label ?: "") }
|
||||||
} catch (e: SerializationException) {
|
} catch (e: SerializationException) {
|
||||||
|
@ -75,7 +67,4 @@ class StreamWishExtractor(private val client: OkHttpClient, private val headers:
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
private data class TrackDto(val file: String, val kind: String, val label: String? = null)
|
private data class TrackDto(val file: String, val kind: String, val label: String? = null)
|
||||||
|
|
||||||
private val M3U8_REGEX = Regex("""https[^"]*m3u8[^"]*""")
|
|
||||||
private val FIX_TRACKS_REGEX = Regex("""(?<!["])(file|kind|label)(?!["])""")
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,7 +18,6 @@ import uy.kohesive.injekt.injectLazy
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
import java.util.concurrent.CountDownLatch
|
import java.util.concurrent.CountDownLatch
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
import kotlin.math.abs
|
|
||||||
|
|
||||||
class UniversalExtractor(private val client: OkHttpClient) {
|
class UniversalExtractor(private val client: OkHttpClient) {
|
||||||
private val context: Application by injectLazy()
|
private val context: Application by injectLazy()
|
||||||
|
@ -74,7 +73,7 @@ class UniversalExtractor(private val client: OkHttpClient) {
|
||||||
|
|
||||||
for (quality in qualities) {
|
for (quality in qualities) {
|
||||||
val modifiedUrl = resultUrl.replace("M3U8_AUTO_360", "M3U8_AUTO_$quality")
|
val modifiedUrl = resultUrl.replace("M3U8_AUTO_360", "M3U8_AUTO_$quality")
|
||||||
val videos = playlistUtils.extractFromHls(modifiedUrl, origRequestUrl, videoNameGen = { "$prefix - $host: ${stnQuality(it)} $quality" + "p" })
|
val videos = playlistUtils.extractFromHls(modifiedUrl, origRequestUrl, videoNameGen = { "$prefix - $host: $it $quality" + "p" })
|
||||||
|
|
||||||
if (videos.isNotEmpty()) {
|
if (videos.isNotEmpty()) {
|
||||||
allVideos.addAll(videos)
|
allVideos.addAll(videos)
|
||||||
|
@ -90,7 +89,7 @@ class UniversalExtractor(private val client: OkHttpClient) {
|
||||||
return when {
|
return when {
|
||||||
"m3u8" in resultUrl -> {
|
"m3u8" in resultUrl -> {
|
||||||
Log.d("UniversalExtractor", "m3u8 URL: $resultUrl")
|
Log.d("UniversalExtractor", "m3u8 URL: $resultUrl")
|
||||||
playlistUtils.extractFromHls(resultUrl, origRequestUrl, videoNameGen = { "$prefix - $host: ${stnQuality(it)}" })
|
playlistUtils.extractFromHls(resultUrl, origRequestUrl, videoNameGen = { "$prefix - $host: $it" })
|
||||||
}
|
}
|
||||||
"mpd" in resultUrl -> {
|
"mpd" in resultUrl -> {
|
||||||
Log.d("UniversalExtractor", "mpd URL: $resultUrl")
|
Log.d("UniversalExtractor", "mpd URL: $resultUrl")
|
||||||
|
@ -104,13 +103,6 @@ class UniversalExtractor(private val client: OkHttpClient) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun stnQuality(quality: String): String {
|
|
||||||
val intQuality = quality.trim().toInt()
|
|
||||||
val standardQualities = listOf(144, 240, 360, 480, 720, 1080)
|
|
||||||
val result = standardQualities.minByOrNull { abs(it - intQuality) } ?: quality
|
|
||||||
return "${result}p"
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun String.proper(): String {
|
private fun String.proper(): String {
|
||||||
return this.replaceFirstChar { if (it.isLowerCase()) it.titlecase(
|
return this.replaceFirstChar { if (it.isLowerCase()) it.titlecase(
|
||||||
Locale.getDefault()) else it.toString() }
|
Locale.getDefault()) else it.toString() }
|
||||||
|
|
|
@ -19,8 +19,8 @@ if (System.getenv("CI") != "true") {
|
||||||
} else {
|
} else {
|
||||||
// Running in CI (GitHub Actions)
|
// Running in CI (GitHub Actions)
|
||||||
|
|
||||||
val chunkSize = System.getenv("CI_CHUNK_SIZE")?.toIntOrNull() ?: Int.MAX_VALUE
|
val chunkSize = System.getenv("CI_CHUNK_SIZE").toInt()
|
||||||
val chunk = System.getenv("CI_CHUNK_NUM")?.toIntOrNull() ?: 0
|
val chunk = System.getenv("CI_CHUNK_NUM").toInt()
|
||||||
|
|
||||||
// Loads individual extensions
|
// Loads individual extensions
|
||||||
File(rootDir, "src").getChunk(chunk, chunkSize)?.forEach {
|
File(rootDir, "src").getChunk(chunk, chunkSize)?.forEach {
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
ext {
|
ext {
|
||||||
extName = 'AnimeWorld India'
|
extName = 'AnimeWorld India'
|
||||||
extClass = '.AnimeWorldIndiaFactory'
|
extClass = '.AnimeWorldIndiaFactory'
|
||||||
extVersionCode = 16
|
extVersionCode = 15
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
|
|
@ -3,7 +3,7 @@ ext {
|
||||||
extClass = '.AnimeXin'
|
extClass = '.AnimeXin'
|
||||||
themePkg = 'animestream'
|
themePkg = 'animestream'
|
||||||
baseUrl = 'https://animexin.vip'
|
baseUrl = 'https://animexin.vip'
|
||||||
overrideVersionCode = 11
|
overrideVersionCode = 10
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
ext {
|
ext {
|
||||||
extName = 'AniZone'
|
extName = 'AniZone'
|
||||||
extClass = '.AniZone'
|
extClass = '.AniZone'
|
||||||
extVersionCode = 4
|
extVersionCode = 3
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
|
|
@ -3,7 +3,7 @@ ext {
|
||||||
extClass = '.ChineseAnime'
|
extClass = '.ChineseAnime'
|
||||||
themePkg = 'animestream'
|
themePkg = 'animestream'
|
||||||
baseUrl = 'https://www.chineseanime.vip'
|
baseUrl = 'https://www.chineseanime.vip'
|
||||||
overrideVersionCode = 15
|
overrideVersionCode = 13
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
ext {
|
ext {
|
||||||
extName = 'Hikari'
|
extName = 'Hikari'
|
||||||
extClass = '.Hikari'
|
extClass = '.Hikari'
|
||||||
extVersionCode = 22
|
extVersionCode = 16
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
@ -9,7 +9,6 @@ apply from: "$rootDir/common.gradle"
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation(project(':lib:chillx-extractor'))
|
implementation(project(':lib:chillx-extractor'))
|
||||||
implementation(project(':lib:filemoon-extractor'))
|
implementation(project(':lib:filemoon-extractor'))
|
||||||
implementation(project(':lib:savefile-extractor'))
|
|
||||||
implementation(project(':lib:buzzheavier-extractor'))
|
|
||||||
implementation(project(':lib:streamwish-extractor'))
|
implementation(project(':lib:streamwish-extractor'))
|
||||||
|
implementation(project(':lib:vidhide-extractor'))
|
||||||
}
|
}
|
|
@ -1,118 +0,0 @@
|
||||||
package eu.kanade.tachiyomi.animeextension.all.hikari
|
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.animesource.model.SAnime
|
|
||||||
import eu.kanade.tachiyomi.animesource.model.SEpisode
|
|
||||||
import kotlinx.serialization.SerialName
|
|
||||||
import kotlinx.serialization.Serializable
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class CatalogResponseDto<T>(
|
|
||||||
val next: String? = null,
|
|
||||||
val results: List<T>,
|
|
||||||
)
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class AnimeDto(
|
|
||||||
val uid: String,
|
|
||||||
|
|
||||||
@SerialName("ani_ename")
|
|
||||||
val aniEName: String? = null,
|
|
||||||
@SerialName("ani_name")
|
|
||||||
val aniName: String,
|
|
||||||
@SerialName("ani_poster")
|
|
||||||
val aniPoster: String? = null,
|
|
||||||
@SerialName("ani_synopsis")
|
|
||||||
val aniSynopsis: String? = null,
|
|
||||||
@SerialName("ani_synonyms")
|
|
||||||
val aniSynonyms: String? = null,
|
|
||||||
@SerialName("ani_genre")
|
|
||||||
val aniGenre: String? = null,
|
|
||||||
@SerialName("ani_studio")
|
|
||||||
val aniStudio: String? = null,
|
|
||||||
@SerialName("ani_producers")
|
|
||||||
val aniProducers: String? = null,
|
|
||||||
@SerialName("ani_stats")
|
|
||||||
val aniStats: Int? = null,
|
|
||||||
@SerialName("ani_time")
|
|
||||||
val aniTime: String? = null,
|
|
||||||
@SerialName("ani_ep")
|
|
||||||
val aniEp: String? = null,
|
|
||||||
@SerialName("ani_type")
|
|
||||||
val aniType: Int? = null,
|
|
||||||
@SerialName("ani_score")
|
|
||||||
val aniScore: Double? = null,
|
|
||||||
) {
|
|
||||||
fun toSAnime(preferEnglish: Boolean): SAnime = SAnime.create().apply {
|
|
||||||
url = uid
|
|
||||||
title = if (preferEnglish) aniEName?.takeUnless(String::isBlank) ?: aniName else aniName
|
|
||||||
thumbnail_url = aniPoster
|
|
||||||
genre = aniGenre?.split(",")?.joinToString(transform = String::trim)
|
|
||||||
artist = aniStudio
|
|
||||||
author = aniProducers?.split(",")?.joinToString(transform = String::trim)
|
|
||||||
description = buildString {
|
|
||||||
aniScore?.let { append("Score: %.2f/10\n\n".format(it)) }
|
|
||||||
aniSynopsis?.trim()?.let(::append)
|
|
||||||
append("\n\n")
|
|
||||||
aniType?.let {
|
|
||||||
val type = when (it) {
|
|
||||||
1 -> "TV"
|
|
||||||
2 -> "Movie"
|
|
||||||
3 -> "OVA"
|
|
||||||
4 -> "ONA"
|
|
||||||
5 -> "Special"
|
|
||||||
else -> "Unknown"
|
|
||||||
}
|
|
||||||
append("Type: $type\n")
|
|
||||||
}
|
|
||||||
aniEp?.let { append("Total Episode count: $it\n") }
|
|
||||||
aniTime?.let { append("Runtime: $it\n") }
|
|
||||||
aniSynonyms?.let { append("Synonyms: $it") }
|
|
||||||
}.trim()
|
|
||||||
|
|
||||||
status = when (aniStats) {
|
|
||||||
1 -> SAnime.UNKNOWN
|
|
||||||
2 -> SAnime.COMPLETED
|
|
||||||
3 -> SAnime.ONGOING
|
|
||||||
else -> SAnime.UNKNOWN
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class LatestEpisodeDto(
|
|
||||||
val uid: Int,
|
|
||||||
val title: String,
|
|
||||||
@SerialName("title_en")
|
|
||||||
val titleEn: String? = null,
|
|
||||||
val imageUrl: String,
|
|
||||||
) {
|
|
||||||
fun toSAnime(preferEnglish: Boolean): SAnime = SAnime.create().apply {
|
|
||||||
url = uid.toString()
|
|
||||||
title = if (preferEnglish) titleEn?.takeUnless(String::isBlank) ?: this@LatestEpisodeDto.title else this@LatestEpisodeDto.title
|
|
||||||
thumbnail_url = imageUrl
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class EpisodeDto(
|
|
||||||
@SerialName("ep_id_name")
|
|
||||||
val epId: String,
|
|
||||||
@SerialName("ep_name")
|
|
||||||
val epName: String? = null,
|
|
||||||
) {
|
|
||||||
fun toSEpisode(uid: String): SEpisode = SEpisode.create().apply {
|
|
||||||
url = "$uid-$epId"
|
|
||||||
name = epName?.let { "Ep. $epId - $it" } ?: "Episode $epId"
|
|
||||||
episode_number = epId.toFloatOrNull() ?: 1f
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class EmbedDto(
|
|
||||||
@SerialName("embed_type")
|
|
||||||
val embedType: String,
|
|
||||||
@SerialName("embed_name")
|
|
||||||
val embedName: String,
|
|
||||||
@SerialName("embed_frame")
|
|
||||||
val embedFrame: String,
|
|
||||||
)
|
|
|
@ -5,7 +5,7 @@ import okhttp3.HttpUrl
|
||||||
import java.util.Calendar
|
import java.util.Calendar
|
||||||
|
|
||||||
interface UriFilter {
|
interface UriFilter {
|
||||||
fun addToUri(builder: HttpUrl.Builder)
|
fun addToUri(url: HttpUrl.Builder)
|
||||||
}
|
}
|
||||||
|
|
||||||
sealed class UriPartFilter(
|
sealed class UriPartFilter(
|
||||||
|
@ -20,10 +20,7 @@ sealed class UriPartFilter(
|
||||||
),
|
),
|
||||||
UriFilter {
|
UriFilter {
|
||||||
override fun addToUri(builder: HttpUrl.Builder) {
|
override fun addToUri(builder: HttpUrl.Builder) {
|
||||||
val value = vals[state].second
|
builder.addQueryParameter(param, vals[state].second)
|
||||||
if (value.isNotEmpty()) {
|
|
||||||
builder.addQueryParameter(param, value)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -36,15 +33,13 @@ sealed class UriMultiSelectFilter(
|
||||||
) : AnimeFilter.Group<UriMultiSelectOption>(name, vals.map { UriMultiSelectOption(it.first, it.second) }), UriFilter {
|
) : AnimeFilter.Group<UriMultiSelectOption>(name, vals.map { UriMultiSelectOption(it.first, it.second) }), UriFilter {
|
||||||
override fun addToUri(builder: HttpUrl.Builder) {
|
override fun addToUri(builder: HttpUrl.Builder) {
|
||||||
val checked = state.filter { it.state }
|
val checked = state.filter { it.state }
|
||||||
if (checked.isNotEmpty()) {
|
|
||||||
builder.addQueryParameter(param, checked.joinToString(",") { it.value })
|
builder.addQueryParameter(param, checked.joinToString(",") { it.value })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
class TypeFilter : UriPartFilter(
|
class TypeFilter : UriPartFilter(
|
||||||
"Type",
|
"Type",
|
||||||
"ani_type",
|
"type",
|
||||||
arrayOf(
|
arrayOf(
|
||||||
Pair("All", ""),
|
Pair("All", ""),
|
||||||
Pair("TV", "1"),
|
Pair("TV", "1"),
|
||||||
|
@ -55,53 +50,165 @@ class TypeFilter : UriPartFilter(
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
class StatusFilter : UriPartFilter(
|
class CountryFilter : UriPartFilter(
|
||||||
"Status",
|
"Country",
|
||||||
"ani_stats",
|
"country",
|
||||||
arrayOf(
|
arrayOf(
|
||||||
Pair("All", ""),
|
Pair("All", ""),
|
||||||
Pair("Ongoing", "1"),
|
Pair("Japanese", "1"),
|
||||||
Pair("Completed", "2"),
|
Pair("Chinese", "2"),
|
||||||
Pair("Upcoming", "3"),
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
class StatusFilter : UriPartFilter(
|
||||||
|
"Status",
|
||||||
|
"stats",
|
||||||
|
arrayOf(
|
||||||
|
Pair("All", ""),
|
||||||
|
Pair("Currently Airing", "1"),
|
||||||
|
Pair("Finished Airing", "2"),
|
||||||
|
Pair("Not yet Aired", "3"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
class RatingFilter : UriPartFilter(
|
||||||
|
"Rating",
|
||||||
|
"rate",
|
||||||
|
arrayOf(
|
||||||
|
Pair("All", ""),
|
||||||
|
Pair("G", "1"),
|
||||||
|
Pair("PG", "2"),
|
||||||
|
Pair("PG-13", "3"),
|
||||||
|
Pair("R-17+", "4"),
|
||||||
|
Pair("R+", "5"),
|
||||||
|
Pair("Rx", "6"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
class SourceFilter : UriPartFilter(
|
||||||
|
"Source",
|
||||||
|
"source",
|
||||||
|
arrayOf(
|
||||||
|
Pair("All", ""),
|
||||||
|
Pair("LightNovel", "1"),
|
||||||
|
Pair("Manga", "2"),
|
||||||
|
Pair("Original", "3"),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
class SeasonFilter : UriPartFilter(
|
class SeasonFilter : UriPartFilter(
|
||||||
"Season",
|
"Season",
|
||||||
"ani_release_season",
|
"season",
|
||||||
arrayOf(
|
arrayOf(
|
||||||
Pair("All", ""),
|
Pair("All", ""),
|
||||||
Pair("Winter", "1"),
|
Pair("Spring", "1"),
|
||||||
Pair("Spring", "2"),
|
Pair("Summer", "2"),
|
||||||
Pair("Summer", "3"),
|
Pair("Fall", "3"),
|
||||||
Pair("Fall", "4"),
|
Pair("Winter", "4"),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
class YearFilter : UriPartFilter(
|
class LanguageFilter : UriPartFilter(
|
||||||
"Release Year",
|
"Language",
|
||||||
"ani_release",
|
"language",
|
||||||
|
arrayOf(
|
||||||
|
Pair("All", ""),
|
||||||
|
Pair("Raw", "1"),
|
||||||
|
Pair("Sub", "2"),
|
||||||
|
Pair("Dub", "3"),
|
||||||
|
Pair("Turk", "4"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
class SortFilter : UriPartFilter(
|
||||||
|
"Sort",
|
||||||
|
"sort",
|
||||||
|
arrayOf(
|
||||||
|
Pair("Default", "default"),
|
||||||
|
Pair("Recently Added", "recently_added"),
|
||||||
|
Pair("Recently Updated", "recently_updated"),
|
||||||
|
Pair("Score", "score"),
|
||||||
|
Pair("Name A-Z", "name_az"),
|
||||||
|
Pair("Released Date", "released_date"),
|
||||||
|
Pair("Most Watched", "most_watched"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
class YearFilter(name: String, param: String) : UriPartFilter(
|
||||||
|
name,
|
||||||
|
param,
|
||||||
YEARS,
|
YEARS,
|
||||||
) {
|
) {
|
||||||
companion object {
|
companion object {
|
||||||
private val CURRENT_YEAR by lazy {
|
private val NEXT_YEAR by lazy {
|
||||||
Calendar.getInstance()[Calendar.YEAR]
|
Calendar.getInstance()[Calendar.YEAR] + 1
|
||||||
}
|
}
|
||||||
|
|
||||||
private val YEARS = buildList {
|
private val YEARS = Array(NEXT_YEAR - 1917) { year ->
|
||||||
add(Pair("Any", ""))
|
if (year == 0) {
|
||||||
addAll(
|
Pair("Any", "")
|
||||||
(1990..CURRENT_YEAR).map {
|
} else {
|
||||||
Pair(it.toString(), it.toString())
|
(NEXT_YEAR - year).toString().let { Pair(it, it) }
|
||||||
},
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class MonthFilter(name: String, param: String) : UriPartFilter(
|
||||||
|
name,
|
||||||
|
param,
|
||||||
|
MONTHS,
|
||||||
|
) {
|
||||||
|
companion object {
|
||||||
|
private val MONTHS = Array(13) { months ->
|
||||||
|
if (months == 0) {
|
||||||
|
Pair("Any", "")
|
||||||
|
} else {
|
||||||
|
val monthStr = "%02d".format(months)
|
||||||
|
Pair(monthStr, monthStr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class DayFilter(name: String, param: String) : UriPartFilter(
|
||||||
|
name,
|
||||||
|
param,
|
||||||
|
DAYS,
|
||||||
|
) {
|
||||||
|
companion object {
|
||||||
|
private val DAYS = Array(32) { day ->
|
||||||
|
if (day == 0) {
|
||||||
|
Pair("Any", "")
|
||||||
|
} else {
|
||||||
|
val dayStr = "%02d".format(day)
|
||||||
|
Pair(dayStr, dayStr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class AiringDateFilter(
|
||||||
|
private val values: List<UriPartFilter> = PARTS,
|
||||||
|
) : AnimeFilter.Group<UriPartFilter>("Airing Date", values), UriFilter {
|
||||||
|
override fun addToUri(builder: HttpUrl.Builder) {
|
||||||
|
values.forEach {
|
||||||
|
it.addToUri(builder)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val PARTS = listOf(
|
||||||
|
YearFilter("Year", "aired_year"),
|
||||||
|
MonthFilter("Month", "aired_month"),
|
||||||
|
DayFilter("Day", "aired_day"),
|
||||||
)
|
)
|
||||||
}.toTypedArray()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class GenreFilter : UriMultiSelectFilter(
|
class GenreFilter : UriMultiSelectFilter(
|
||||||
"Genre",
|
"Genre",
|
||||||
"ani_genre",
|
"genres",
|
||||||
arrayOf(
|
arrayOf(
|
||||||
Pair("Action", "Action"),
|
Pair("Action", "Action"),
|
||||||
Pair("Adventure", "Adventure"),
|
Pair("Adventure", "Adventure"),
|
||||||
|
@ -126,7 +233,7 @@ class GenreFilter : UriMultiSelectFilter(
|
||||||
Pair("Music", "Music"),
|
Pair("Music", "Music"),
|
||||||
Pair("Mystery", "Mystery"),
|
Pair("Mystery", "Mystery"),
|
||||||
Pair("Parody", "Parody"),
|
Pair("Parody", "Parody"),
|
||||||
Pair("Policy", "Policy"),
|
Pair("Police", "Police"),
|
||||||
Pair("Psychological", "Psychological"),
|
Pair("Psychological", "Psychological"),
|
||||||
Pair("Romance", "Romance"),
|
Pair("Romance", "Romance"),
|
||||||
Pair("Samurai", "Samurai"),
|
Pair("Samurai", "Samurai"),
|
||||||
|
@ -146,12 +253,3 @@ class GenreFilter : UriMultiSelectFilter(
|
||||||
Pair("Vampire", "Vampire"),
|
Pair("Vampire", "Vampire"),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
class LanguageFilter : UriPartFilter(
|
|
||||||
"Language",
|
|
||||||
"ani_genre",
|
|
||||||
arrayOf(
|
|
||||||
Pair("Any", ""),
|
|
||||||
Pair("Portuguese", "Portuguese"),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
|
@ -1,86 +1,124 @@
|
||||||
package eu.kanade.tachiyomi.animeextension.all.hikari
|
package eu.kanade.tachiyomi.animeextension.all.hikari
|
||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import android.content.SharedPreferences
|
import android.util.Log
|
||||||
import androidx.preference.ListPreference
|
import androidx.preference.ListPreference
|
||||||
import androidx.preference.MultiSelectListPreference
|
|
||||||
import androidx.preference.PreferenceScreen
|
import androidx.preference.PreferenceScreen
|
||||||
import androidx.preference.SwitchPreferenceCompat
|
|
||||||
import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource
|
import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource
|
||||||
|
import eu.kanade.tachiyomi.animesource.model.AnimeFilter
|
||||||
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
|
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
|
||||||
import eu.kanade.tachiyomi.animesource.model.AnimesPage
|
import eu.kanade.tachiyomi.animesource.model.AnimesPage
|
||||||
import eu.kanade.tachiyomi.animesource.model.SAnime
|
import eu.kanade.tachiyomi.animesource.model.SAnime
|
||||||
import eu.kanade.tachiyomi.animesource.model.SEpisode
|
import eu.kanade.tachiyomi.animesource.model.SEpisode
|
||||||
import eu.kanade.tachiyomi.animesource.model.Video
|
import eu.kanade.tachiyomi.animesource.model.Video
|
||||||
import eu.kanade.tachiyomi.animesource.online.AnimeHttpSource
|
import eu.kanade.tachiyomi.animesource.online.ParsedAnimeHttpSource
|
||||||
import eu.kanade.tachiyomi.lib.buzzheavierextractor.BuzzheavierExtractor
|
|
||||||
import eu.kanade.tachiyomi.lib.chillxextractor.ChillxExtractor
|
import eu.kanade.tachiyomi.lib.chillxextractor.ChillxExtractor
|
||||||
import eu.kanade.tachiyomi.lib.filemoonextractor.FilemoonExtractor
|
import eu.kanade.tachiyomi.lib.filemoonextractor.FilemoonExtractor
|
||||||
import eu.kanade.tachiyomi.lib.savefileextractor.SavefileExtractor
|
|
||||||
import eu.kanade.tachiyomi.lib.streamwishextractor.StreamWishExtractor
|
import eu.kanade.tachiyomi.lib.streamwishextractor.StreamWishExtractor
|
||||||
|
import eu.kanade.tachiyomi.lib.vidhideextractor.VidHideExtractor
|
||||||
import eu.kanade.tachiyomi.network.GET
|
import eu.kanade.tachiyomi.network.GET
|
||||||
import eu.kanade.tachiyomi.util.parallelCatchingFlatMapBlocking
|
import eu.kanade.tachiyomi.util.parallelCatchingFlatMapBlocking
|
||||||
import eu.kanade.tachiyomi.util.parseAs
|
import eu.kanade.tachiyomi.util.parseAs
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
|
import org.jsoup.Jsoup
|
||||||
|
import org.jsoup.nodes.Document
|
||||||
|
import org.jsoup.nodes.Element
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
|
|
||||||
class Hikari : AnimeHttpSource(), ConfigurableAnimeSource {
|
class Hikari : ParsedAnimeHttpSource(), ConfigurableAnimeSource {
|
||||||
|
|
||||||
override val name = "Hikari"
|
override val name = "Hikari"
|
||||||
|
|
||||||
private val proxyUrl = "https://hikari.gg/hiki-proxy/extract/"
|
override val baseUrl = "https://watch.hikaritv.xyz"
|
||||||
private val apiUrl = "https://api.hikari.gg/api"
|
|
||||||
override val baseUrl = "https://hikari.gg"
|
|
||||||
|
|
||||||
override val lang = "all"
|
override val lang = "all"
|
||||||
|
|
||||||
override val versionId = 2
|
|
||||||
|
|
||||||
override val supportsLatest = true
|
override val supportsLatest = true
|
||||||
|
|
||||||
|
override fun headersBuilder() = super.headersBuilder().apply {
|
||||||
|
add("Origin", baseUrl)
|
||||||
|
add("Referer", "$baseUrl/")
|
||||||
|
}
|
||||||
|
|
||||||
private val preferences by lazy {
|
private val preferences by lazy {
|
||||||
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
|
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================== Popular ===============================
|
// ============================== Popular ===============================
|
||||||
|
|
||||||
override fun popularAnimeRequest(page: Int) = searchAnimeRequest(page, "", AnimeFilterList())
|
override fun popularAnimeRequest(page: Int): Request {
|
||||||
|
val url = "$baseUrl/ajax/getfilter?type=&country=&stats=&rate=&source=&season=&language=&aired_year=&aired_month=&aired_day=&sort=score&genres=&page=$page"
|
||||||
|
val headers = headersBuilder().set("Referer", "$baseUrl/filter").build()
|
||||||
|
return GET(url, headers)
|
||||||
|
}
|
||||||
|
|
||||||
override fun popularAnimeParse(response: Response) = searchAnimeParse(response)
|
override fun popularAnimeParse(response: Response): AnimesPage {
|
||||||
|
val parsed = response.parseAs<HtmlResponseDto>()
|
||||||
|
|
||||||
|
val hasNextPage = response.request.url.queryParameter("page")!!.toInt() < parsed.page!!.totalPages
|
||||||
|
val animeList = parsed.toHtml(baseUrl).select(popularAnimeSelector())
|
||||||
|
.map(::popularAnimeFromElement)
|
||||||
|
|
||||||
|
return AnimesPage(animeList, hasNextPage)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun popularAnimeSelector(): String = ".flw-item"
|
||||||
|
|
||||||
|
override fun popularAnimeFromElement(element: Element): SAnime = SAnime.create().apply {
|
||||||
|
setUrlWithoutDomain(element.selectFirst("a[data-id]")!!.attr("abs:href"))
|
||||||
|
thumbnail_url = element.selectFirst("img")!!.attr("abs:src")
|
||||||
|
title = element.selectFirst(".film-name")!!.text()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun popularAnimeNextPageSelector(): String? = null
|
||||||
|
|
||||||
// =============================== Latest ===============================
|
// =============================== Latest ===============================
|
||||||
|
|
||||||
override fun latestUpdatesRequest(page: Int): Request {
|
override fun latestUpdatesRequest(page: Int): Request {
|
||||||
val url = "$apiUrl/episode/new/".toHttpUrl().newBuilder().apply {
|
val url = "$baseUrl/ajax/getfilter?type=&country=&stats=&rate=&source=&season=&language=&aired_year=&aired_month=&aired_day=&sort=recently_updated&genres=&page=$page"
|
||||||
addQueryParameter("limit", "100")
|
val headers = headersBuilder().set("Referer", "$baseUrl/filter").build()
|
||||||
addQueryParameter("language", "EN")
|
|
||||||
}.build()
|
|
||||||
return GET(url, headers)
|
return GET(url, headers)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun latestUpdatesParse(response: Response): AnimesPage {
|
override fun latestUpdatesParse(response: Response): AnimesPage =
|
||||||
val data = response.parseAs<CatalogResponseDto<LatestEpisodeDto>>()
|
popularAnimeParse(response)
|
||||||
val preferEnglish = preferences.getTitleLang
|
|
||||||
|
|
||||||
val animeList = data.results.distinctBy { it.uid }.map { it.toSAnime(preferEnglish) }
|
override fun latestUpdatesSelector(): String =
|
||||||
return AnimesPage(animeList, false)
|
throw UnsupportedOperationException()
|
||||||
}
|
|
||||||
|
override fun latestUpdatesFromElement(element: Element): SAnime =
|
||||||
|
throw UnsupportedOperationException()
|
||||||
|
|
||||||
|
override fun latestUpdatesNextPageSelector(): String =
|
||||||
|
throw UnsupportedOperationException()
|
||||||
|
|
||||||
// =============================== Search ===============================
|
// =============================== Search ===============================
|
||||||
|
|
||||||
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
|
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
|
||||||
val url = "$apiUrl/anime/".toHttpUrl().newBuilder().apply {
|
val url = baseUrl.toHttpUrl().newBuilder().apply {
|
||||||
addQueryParameter("sort", "created_at")
|
if (query.isNotEmpty()) {
|
||||||
addQueryParameter("order", "asc")
|
addPathSegment("search")
|
||||||
|
addQueryParameter("keyword", query)
|
||||||
addQueryParameter("page", page.toString())
|
addQueryParameter("page", page.toString())
|
||||||
|
} else {
|
||||||
|
addPathSegment("ajax")
|
||||||
|
addPathSegment("getfilter")
|
||||||
filters.filterIsInstance<UriFilter>().forEach {
|
filters.filterIsInstance<UriFilter>().forEach {
|
||||||
it.addToUri(this)
|
it.addToUri(this)
|
||||||
}
|
}
|
||||||
|
addQueryParameter("page", page.toString())
|
||||||
|
}
|
||||||
|
}.build()
|
||||||
|
|
||||||
|
val headers = headersBuilder().apply {
|
||||||
if (query.isNotEmpty()) {
|
if (query.isNotEmpty()) {
|
||||||
addQueryParameter("search", query)
|
set("Referer", url.toString().substringBeforeLast("&page"))
|
||||||
|
} else {
|
||||||
|
set("Referer", "$baseUrl/filter")
|
||||||
}
|
}
|
||||||
}.build()
|
}.build()
|
||||||
|
|
||||||
|
@ -88,179 +126,280 @@ class Hikari : AnimeHttpSource(), ConfigurableAnimeSource {
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun searchAnimeParse(response: Response): AnimesPage {
|
override fun searchAnimeParse(response: Response): AnimesPage {
|
||||||
val data = response.parseAs<CatalogResponseDto<AnimeDto>>()
|
return if (response.request.url.encodedPath.startsWith("/search")) {
|
||||||
val preferEnglish = preferences.getTitleLang
|
super.searchAnimeParse(response)
|
||||||
|
} else {
|
||||||
val animeList = data.results.map { it.toSAnime(preferEnglish) }
|
popularAnimeParse(response)
|
||||||
val hasNextPage = data.next != null
|
|
||||||
|
|
||||||
return AnimesPage(animeList, hasNextPage)
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun searchAnimeSelector(): String = popularAnimeSelector()
|
||||||
|
|
||||||
|
override fun searchAnimeFromElement(element: Element): SAnime = popularAnimeFromElement(element)
|
||||||
|
|
||||||
|
override fun searchAnimeNextPageSelector(): String = "ul.pagination > li.active + li"
|
||||||
|
|
||||||
// ============================== Filters ===============================
|
// ============================== Filters ===============================
|
||||||
|
|
||||||
override fun getFilterList(): AnimeFilterList = AnimeFilterList(
|
override fun getFilterList(): AnimeFilterList = AnimeFilterList(
|
||||||
|
AnimeFilter.Header("Note: text search ignores filters"),
|
||||||
|
AnimeFilter.Separator(),
|
||||||
TypeFilter(),
|
TypeFilter(),
|
||||||
|
CountryFilter(),
|
||||||
StatusFilter(),
|
StatusFilter(),
|
||||||
|
RatingFilter(),
|
||||||
|
SourceFilter(),
|
||||||
SeasonFilter(),
|
SeasonFilter(),
|
||||||
YearFilter(),
|
|
||||||
GenreFilter(),
|
|
||||||
LanguageFilter(),
|
LanguageFilter(),
|
||||||
|
SortFilter(),
|
||||||
|
AiringDateFilter(),
|
||||||
|
GenreFilter(),
|
||||||
)
|
)
|
||||||
|
|
||||||
// =========================== Anime Details ============================
|
// =========================== Anime Details ============================
|
||||||
|
|
||||||
override fun getAnimeUrl(anime: SAnime): String {
|
override fun animeDetailsParse(document: Document): SAnime = SAnime.create().apply {
|
||||||
return "$baseUrl/info/${anime.url}"
|
with(document.selectFirst("#ani_detail")!!) {
|
||||||
|
title = selectFirst(".film-name")!!.text()
|
||||||
|
thumbnail_url = selectFirst(".film-poster img")!!.attr("abs:src")
|
||||||
|
description = selectFirst(".film-description > .text")?.text()
|
||||||
|
genre = select(".item-list:has(span:contains(Genres)) > a").joinToString { it.text() }
|
||||||
|
author = select(".item:has(span:contains(Studio)) > a").joinToString { it.text() }
|
||||||
|
status = selectFirst(".item:has(span:contains(Status)) > .name").parseStatus()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun animeDetailsRequest(anime: SAnime): Request {
|
private fun Element?.parseStatus(): Int = when (this?.text()?.lowercase()) {
|
||||||
return GET("$apiUrl/anime/uid/${anime.url}/", headers)
|
"currently airing" -> SAnime.ONGOING
|
||||||
}
|
"finished" -> SAnime.COMPLETED
|
||||||
|
else -> SAnime.UNKNOWN
|
||||||
override fun animeDetailsParse(response: Response): SAnime {
|
|
||||||
return response.parseAs<AnimeDto>().toSAnime(preferences.getTitleLang)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================== Episodes ==============================
|
// ============================== Episodes ==============================
|
||||||
|
|
||||||
|
private val specialCharRegex = Regex("""(?![\-_])\W{1,}""")
|
||||||
|
|
||||||
override fun episodeListRequest(anime: SAnime): Request {
|
override fun episodeListRequest(anime: SAnime): Request {
|
||||||
return GET("$apiUrl/episode/uid/${anime.url}/", headers)
|
val animeId = anime.url.split("/")[2]
|
||||||
|
|
||||||
|
val sanitized = anime.title.replace(" ", "_")
|
||||||
|
|
||||||
|
val refererUrl = baseUrl.toHttpUrl().newBuilder().apply {
|
||||||
|
addPathSegment("watch")
|
||||||
|
addQueryParameter("anime", specialCharRegex.replace(sanitized, ""))
|
||||||
|
addQueryParameter("uid", animeId)
|
||||||
|
addQueryParameter("eps", "1")
|
||||||
|
}.build()
|
||||||
|
|
||||||
|
val headers = headersBuilder()
|
||||||
|
.set("Referer", refererUrl.toString())
|
||||||
|
.build()
|
||||||
|
|
||||||
|
return GET("$baseUrl/ajax/episodelist/$animeId", headers)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun episodeListParse(response: Response): List<SEpisode> {
|
override fun episodeListParse(response: Response): List<SEpisode> {
|
||||||
val guid = response.request.url.pathSegments[3]
|
return response.parseAs<HtmlResponseDto>().toHtml(baseUrl)
|
||||||
|
.select(episodeListSelector())
|
||||||
|
.map(::episodeFromElement)
|
||||||
|
.reversed()
|
||||||
|
}
|
||||||
|
|
||||||
return response.parseAs<List<EpisodeDto>>().map { it.toSEpisode(guid) }.reversed()
|
override fun episodeListSelector() = "a[class~=ep-item]"
|
||||||
|
|
||||||
|
override fun episodeFromElement(element: Element): SEpisode {
|
||||||
|
val epText = element.selectFirst(".ssli-order")?.text()?.trim()
|
||||||
|
?: element.attr("data-number").trim()
|
||||||
|
val ep = epText.toFloatOrNull() ?: 0F
|
||||||
|
|
||||||
|
val nameText = element.selectFirst(".ep-name")?.text()?.trim()
|
||||||
|
?: element.attr("title").replace("Episode-", "Ep. ") ?: ""
|
||||||
|
|
||||||
|
return SEpisode.create().apply {
|
||||||
|
setUrlWithoutDomain(element.attr("abs:href"))
|
||||||
|
episode_number = ep
|
||||||
|
name = "Ep. $ep - $nameText"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================ Video Links =============================
|
// ============================ Video Links =============================
|
||||||
|
|
||||||
private val filemoonExtractor by lazy { FilemoonExtractor(client, preferences) }
|
private val filemoonExtractor by lazy { FilemoonExtractor(client) }
|
||||||
private val savefileExtractor by lazy { SavefileExtractor(client, preferences) }
|
private val vidHideExtractor by lazy { VidHideExtractor(client, headers) }
|
||||||
private val buzzheavierExtractor by lazy { BuzzheavierExtractor(client, headers) }
|
|
||||||
private val chillxExtractor by lazy { ChillxExtractor(client, headers) }
|
private val chillxExtractor by lazy { ChillxExtractor(client, headers) }
|
||||||
private val streamwishExtractor by lazy { StreamWishExtractor(client, headers) }
|
private val streamwishExtractor by lazy { StreamWishExtractor(client, headers) }
|
||||||
|
private val embedRegex = Regex("""getEmbed\(\s*(\d+)\s*,\s*(\d+)\s*,\s*'(\d+)'""")
|
||||||
private fun getEmbedTypeName(type: String): String {
|
|
||||||
return when (type) {
|
|
||||||
"2" -> "[SUB] "
|
|
||||||
"3" -> "[DUB] "
|
|
||||||
"4" -> "[MULTI AUDIO] "
|
|
||||||
"8" -> "[HARD-SUB] "
|
|
||||||
else -> ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun videoListRequest(episode: SEpisode): Request {
|
override fun videoListRequest(episode: SEpisode): Request {
|
||||||
val (guid, epId) = episode.url.split("-")
|
val url = (baseUrl + episode.url).toHttpUrl()
|
||||||
return GET("$apiUrl/embed/$guid/$epId/", headers)
|
val animeId = url.queryParameter("uid")!!
|
||||||
|
val episodeNum = url.queryParameter("eps")!!
|
||||||
|
|
||||||
|
val headers = headersBuilder()
|
||||||
|
.set("Referer", baseUrl + episode.url)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
return GET("$baseUrl/ajax/embedserver/$animeId/$episodeNum", headers)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun videoListParse(response: Response): List<Video> {
|
override fun videoListParse(response: Response): List<Video> {
|
||||||
val data = response.parseAs<List<EmbedDto>>()
|
val html = response.parseAs<HtmlResponseDto>().toHtml(baseUrl)
|
||||||
|
Log.d("Hikari", html.toString())
|
||||||
|
|
||||||
val selectedProviders = preferences.getStringSet(PREF_PROVIDER_KEY, PREF_PROVIDERS_DEFAULT)?.map(String::lowercase)?.toSet() ?: emptySet()
|
val headers = headersBuilder()
|
||||||
|
.set("Referer", response.request.url.toString())
|
||||||
|
.build()
|
||||||
|
|
||||||
return data.parallelCatchingFlatMapBlocking { embed ->
|
val subEmbedUrls = html.select(".servers-sub div.item.server-item").flatMap { item ->
|
||||||
val embedName = embed.embedName.lowercase()
|
val name = item.text().trim() + " (Sub)"
|
||||||
|
val onClick = item.selectFirst("a")?.attr("onclick")
|
||||||
|
if (onClick == null) {
|
||||||
|
Log.e("Hikari", "onClick attribute is null for item: $item")
|
||||||
|
return@flatMap emptyList<Pair<String, String>>()
|
||||||
|
}
|
||||||
|
|
||||||
if (embedName !in selectedProviders) return@parallelCatchingFlatMapBlocking emptyList()
|
val match = embedRegex.find(onClick)?.groupValues
|
||||||
|
if (match == null) {
|
||||||
|
Log.e("Hikari", "No match found for onClick: $onClick")
|
||||||
|
return@flatMap emptyList<Pair<String, String>>()
|
||||||
|
}
|
||||||
|
|
||||||
val prefix = getEmbedTypeName(embed.embedType) + embed.embedName
|
val url = "$baseUrl/ajax/embed/${match[1]}/${match[2]}/${match[3]}"
|
||||||
|
val iframeList = client.newCall(
|
||||||
|
GET(url, headers),
|
||||||
|
).execute().parseAs<List<String>>()
|
||||||
|
|
||||||
when (embedName) {
|
iframeList.map {
|
||||||
"streamwish" -> streamwishExtractor.videosFromUrl(embed.embedFrame, videoNameGen = { "$prefix - $it" })
|
val iframeSrc = Jsoup.parseBodyFragment(it).selectFirst("iframe")?.attr("src")
|
||||||
"filemoon" -> filemoonExtractor.videosFromUrl(embed.embedFrame, "$prefix - ")
|
if (iframeSrc == null) {
|
||||||
"sv" -> savefileExtractor.videosFromUrl(embed.embedFrame, "$prefix - ")
|
Log.e("Hikari", "iframe src is null for URL: $url")
|
||||||
"playerx" -> chillxExtractor.videoFromUrl(embed.embedFrame, "$prefix - ")
|
return@map Pair("", "")
|
||||||
"hiki" -> hikiExtraction(embed.embedFrame, "$prefix - ")
|
}
|
||||||
else -> emptyList()
|
Pair(iframeSrc, name)
|
||||||
|
}.filter { it.first.isNotEmpty() }
|
||||||
|
}
|
||||||
|
val dubEmbedUrls = html.select(".servers-dub div.item.server-item").flatMap { item ->
|
||||||
|
val name = item.text().trim() + " (Dub)"
|
||||||
|
val onClick = item.selectFirst("a")?.attr("onclick")
|
||||||
|
if (onClick == null) {
|
||||||
|
Log.e("Hikari", "onClick attribute is null for item: $item")
|
||||||
|
return@flatMap emptyList<Pair<String, String>>()
|
||||||
|
}
|
||||||
|
|
||||||
|
val match = embedRegex.find(onClick)?.groupValues
|
||||||
|
if (match == null) {
|
||||||
|
Log.e("Hikari", "No match found for onClick: $onClick")
|
||||||
|
return@flatMap emptyList<Pair<String, String>>()
|
||||||
|
}
|
||||||
|
|
||||||
|
val url = "$baseUrl/ajax/embed/${match[1]}/${match[2]}/${match[3]}"
|
||||||
|
val iframeList = client.newCall(
|
||||||
|
GET(url, headers),
|
||||||
|
).execute().parseAs<List<String>>()
|
||||||
|
|
||||||
|
iframeList.map {
|
||||||
|
val iframeSrc = Jsoup.parseBodyFragment(it).selectFirst("iframe")?.attr("src")
|
||||||
|
if (iframeSrc == null) {
|
||||||
|
Log.e("Hikari", "iframe src is null for URL: $url")
|
||||||
|
return@map Pair("", "")
|
||||||
|
}
|
||||||
|
Pair(iframeSrc, name)
|
||||||
|
}.filter { it.first.isNotEmpty() }
|
||||||
|
}
|
||||||
|
|
||||||
|
val sdEmbedUrls = html.select(".servers-sub.\\&.dub div.item.server-item").flatMap { item ->
|
||||||
|
val name = item.text().trim() + " (Sub + Dub)"
|
||||||
|
val onClick = item.selectFirst("a")?.attr("onclick")
|
||||||
|
if (onClick == null) {
|
||||||
|
Log.e("Hikari", "onClick attribute is null for item: $item")
|
||||||
|
return@flatMap emptyList<Pair<String, String>>()
|
||||||
|
}
|
||||||
|
|
||||||
|
val match = embedRegex.find(onClick)?.groupValues
|
||||||
|
if (match == null) {
|
||||||
|
Log.e("Hikari", "No match found for onClick: $onClick")
|
||||||
|
return@flatMap emptyList<Pair<String, String>>()
|
||||||
|
}
|
||||||
|
|
||||||
|
val url = "$baseUrl/ajax/embed/${match[1]}/${match[2]}/${match[3]}"
|
||||||
|
val iframeList = client.newCall(
|
||||||
|
GET(url, headers),
|
||||||
|
).execute().parseAs<List<String>>()
|
||||||
|
|
||||||
|
iframeList.map {
|
||||||
|
val iframeSrc = Jsoup.parseBodyFragment(it).selectFirst("iframe")?.attr("src")
|
||||||
|
if (iframeSrc == null) {
|
||||||
|
Log.e("Hikari", "iframe src is null for URL: $url")
|
||||||
|
return@map Pair("", "")
|
||||||
|
}
|
||||||
|
Pair(iframeSrc, name)
|
||||||
|
}.filter { it.first.isNotEmpty() }
|
||||||
|
}
|
||||||
|
|
||||||
|
return sdEmbedUrls.parallelCatchingFlatMapBlocking {
|
||||||
|
getVideosFromEmbed(it.first, it.second)
|
||||||
|
}.ifEmpty {
|
||||||
|
(subEmbedUrls + dubEmbedUrls).parallelCatchingFlatMapBlocking {
|
||||||
|
getVideosFromEmbed(it.first, it.second)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun hikiExtraction(url: String, prefix: String): List<Video> {
|
private fun getVideosFromEmbed(embedUrl: String, name: String): List<Video> = when {
|
||||||
val hikiMirror = preferences.getString(PREF_HIKI_KEY, PREF_HIKI_DEFAULT)!!
|
name.contains("vidhide", true) -> vidHideExtractor.videosFromUrl(embedUrl, videoNameGen = { s -> "$name - $s" })
|
||||||
|
embedUrl.contains("filemoon", true) -> filemoonExtractor.videosFromUrl(embedUrl, prefix = "$name - ", headers = headers)
|
||||||
|
name.contains("streamwish", true) -> streamwishExtractor.videosFromUrl(embedUrl, prefix = "$name - ")
|
||||||
|
else -> chillxExtractor.videoFromUrl(embedUrl, referer = baseUrl, prefix = "$name - ")
|
||||||
|
}
|
||||||
|
|
||||||
if (hikiMirror == "hiki") {
|
override fun videoListSelector() = ".server-item:has(a[onclick~=getEmbed])"
|
||||||
return buzzheavierExtractor.videosFromUrl(url, prefix, proxyUrl)
|
|
||||||
}
|
|
||||||
val id = url.toHttpUrl().pathSegments[0]
|
|
||||||
val videoUrl = "https://$hikiMirror/$id"
|
|
||||||
return buzzheavierExtractor.videosFromUrl(videoUrl, prefix)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun List<Video>.sort(): List<Video> {
|
override fun List<Video>.sort(): List<Video> {
|
||||||
val quality = preferences.getString(PREF_QUALITY_KEY, PREF_QUALITY_DEFAULT)!!
|
val quality = preferences.getString(PREF_QUALITY_KEY, PREF_QUALITY_DEFAULT)!!
|
||||||
val type = preferences.getString(PREF_TYPE_KEY, PREF_TYPE_DEFAULT)!!
|
|
||||||
val hoster = preferences.getString(PREF_HOSTER_KEY, PREF_HOSTER_DEFAULT)!!
|
|
||||||
|
|
||||||
return sortedWith(
|
return sortedWith(
|
||||||
compareBy(
|
compareBy(
|
||||||
{ it.quality.startsWith(type) },
|
|
||||||
{ it.quality.contains(quality) },
|
{ it.quality.contains(quality) },
|
||||||
{ QUALITY_REGEX.find(it.quality)?.groupValues?.get(1)?.toIntOrNull() ?: 0 },
|
{ QUALITY_REGEX.find(it.quality)?.groupValues?.get(1)?.toIntOrNull() ?: 0 },
|
||||||
{ it.quality.contains(hoster, true) },
|
|
||||||
),
|
),
|
||||||
).reversed()
|
).reversed()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun videoFromElement(element: Element): Video =
|
||||||
|
throw UnsupportedOperationException()
|
||||||
|
|
||||||
|
override fun videoUrlParse(document: Document): String =
|
||||||
|
throw UnsupportedOperationException()
|
||||||
|
|
||||||
// ============================= Utilities ==============================
|
// ============================= Utilities ==============================
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
class HtmlResponseDto(
|
||||||
|
val html: String,
|
||||||
|
val page: PageDto? = null,
|
||||||
|
) {
|
||||||
|
fun toHtml(baseUrl: String): Document = Jsoup.parseBodyFragment(html, baseUrl)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
class PageDto(
|
||||||
|
val totalPages: Int,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private val QUALITY_REGEX = Regex("""(\d+)p""")
|
private val QUALITY_REGEX = Regex("""(\d+)p""")
|
||||||
|
|
||||||
private const val PREF_ENGLISH_TITLE_KEY = "preferred_title_lang"
|
|
||||||
private const val PREF_ENGLISH_TITLE_DEFAULT = true
|
|
||||||
|
|
||||||
private const val PREF_QUALITY_KEY = "preferred_quality"
|
private const val PREF_QUALITY_KEY = "preferred_quality"
|
||||||
private const val PREF_QUALITY_DEFAULT = "1080"
|
private const val PREF_QUALITY_DEFAULT = "1080"
|
||||||
private val PREF_QUALITY_VALUES = arrayOf("1080", "720", "480", "360")
|
private val PREF_QUALITY_VALUES = arrayOf("1080", "720", "480", "360")
|
||||||
private val PREF_QUALITY_ENTRIES = PREF_QUALITY_VALUES.map {
|
private val PREF_QUALITY_ENTRIES = PREF_QUALITY_VALUES.map {
|
||||||
"${it}p"
|
"${it}p"
|
||||||
}.toTypedArray()
|
}.toTypedArray()
|
||||||
|
|
||||||
private val TYPE_LIST = arrayOf("[SUB] ", "[DUB] ", "[MULTI AUDIO] ", "[HARD-SUB] ")
|
|
||||||
private const val PREF_TYPE_KEY = "pref_type"
|
|
||||||
private const val PREF_TYPE_DEFAULT = ""
|
|
||||||
private val PREF_TYPE_VALUES = arrayOf("") + TYPE_LIST
|
|
||||||
private val PREF_TYPE_ENTRIES = arrayOf("Any") + TYPE_LIST
|
|
||||||
|
|
||||||
private val HOSTER_LIST = arrayOf("Streamwish", "Filemoon", "SV", "PlayerX", "Hiki")
|
|
||||||
private const val PREF_HOSTER_KEY = "pref_hoster"
|
|
||||||
private const val PREF_HOSTER_DEFAULT = ""
|
|
||||||
private val PREF_HOSTER_VALUES = arrayOf("") + HOSTER_LIST
|
|
||||||
private val PREF_HOSTER_ENTRIES = arrayOf("Any") + HOSTER_LIST
|
|
||||||
|
|
||||||
private const val PREF_HIKI_KEY = "preferred_hiki_mirror"
|
|
||||||
private const val PREF_HIKI_DEFAULT = "hiki"
|
|
||||||
private val PREF_HIKI_VALUES = arrayOf("hiki", "buzzheavier.com", "bzzhr.co", "fuckingfast.net")
|
|
||||||
private val PREF_HIKI_ENTRIES = PREF_HIKI_VALUES
|
|
||||||
|
|
||||||
// Provider
|
|
||||||
private const val PREF_PROVIDER_KEY = "provider_selection"
|
|
||||||
private val PREF_PROVIDERS = arrayOf("Streamwish", "Filemoon", "SV", "PlayerX", "Hiki")
|
|
||||||
|
|
||||||
private val PREF_PROVIDERS_VALUE = arrayOf("streamwish", "filemoon", "sv", "playerx", "hiki")
|
|
||||||
|
|
||||||
private val PREF_DEFAULT_PROVIDERS_VALUE = arrayOf("streamwish", "filemoon", "sv", "playerx", "hiki")
|
|
||||||
|
|
||||||
private val PREF_PROVIDERS_DEFAULT = PREF_DEFAULT_PROVIDERS_VALUE.toSet()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================== Settings ==============================
|
// ============================== Settings ==============================
|
||||||
|
|
||||||
private val SharedPreferences.getTitleLang
|
|
||||||
get() = getBoolean(PREF_ENGLISH_TITLE_KEY, PREF_ENGLISH_TITLE_DEFAULT)
|
|
||||||
|
|
||||||
override fun setupPreferenceScreen(screen: PreferenceScreen) {
|
override fun setupPreferenceScreen(screen: PreferenceScreen) {
|
||||||
SwitchPreferenceCompat(screen.context).apply {
|
|
||||||
key = PREF_ENGLISH_TITLE_KEY
|
|
||||||
title = "Prefer english titles"
|
|
||||||
setDefaultValue(PREF_ENGLISH_TITLE_DEFAULT)
|
|
||||||
}.also(screen::addPreference)
|
|
||||||
|
|
||||||
ListPreference(screen.context).apply {
|
ListPreference(screen.context).apply {
|
||||||
key = PREF_QUALITY_KEY
|
key = PREF_QUALITY_KEY
|
||||||
title = "Preferred quality"
|
title = "Preferred quality"
|
||||||
|
@ -268,44 +407,13 @@ class Hikari : AnimeHttpSource(), ConfigurableAnimeSource {
|
||||||
entryValues = PREF_QUALITY_VALUES
|
entryValues = PREF_QUALITY_VALUES
|
||||||
setDefaultValue(PREF_QUALITY_DEFAULT)
|
setDefaultValue(PREF_QUALITY_DEFAULT)
|
||||||
summary = "%s"
|
summary = "%s"
|
||||||
}.also(screen::addPreference)
|
|
||||||
|
|
||||||
ListPreference(screen.context).apply {
|
setOnPreferenceChangeListener { _, newValue ->
|
||||||
key = PREF_TYPE_KEY
|
val selected = newValue as String
|
||||||
title = "Preferred type"
|
val index = findIndexOfValue(selected)
|
||||||
entries = PREF_TYPE_ENTRIES
|
val entry = entryValues[index] as String
|
||||||
entryValues = PREF_TYPE_VALUES
|
preferences.edit().putString(key, entry).commit()
|
||||||
setDefaultValue(PREF_TYPE_DEFAULT)
|
}
|
||||||
summary = "%s"
|
|
||||||
}.also(screen::addPreference)
|
}.also(screen::addPreference)
|
||||||
|
|
||||||
ListPreference(screen.context).apply {
|
|
||||||
key = PREF_HOSTER_KEY
|
|
||||||
title = "Preferred hoster"
|
|
||||||
entries = PREF_HOSTER_ENTRIES
|
|
||||||
entryValues = PREF_HOSTER_VALUES
|
|
||||||
setDefaultValue(PREF_HOSTER_DEFAULT)
|
|
||||||
summary = "%s"
|
|
||||||
}.also(screen::addPreference)
|
|
||||||
|
|
||||||
MultiSelectListPreference(screen.context).apply {
|
|
||||||
key = PREF_PROVIDER_KEY
|
|
||||||
title = "Enable/Disable Video Providers"
|
|
||||||
entries = PREF_PROVIDERS
|
|
||||||
entryValues = PREF_PROVIDERS_VALUE
|
|
||||||
setDefaultValue(PREF_PROVIDERS_DEFAULT)
|
|
||||||
}.also(screen::addPreference)
|
|
||||||
|
|
||||||
ListPreference(screen.context).apply {
|
|
||||||
key = PREF_HIKI_KEY
|
|
||||||
title = "Hiki provider mirrors"
|
|
||||||
entries = PREF_HIKI_ENTRIES
|
|
||||||
entryValues = PREF_HIKI_VALUES
|
|
||||||
setDefaultValue(PREF_HIKI_DEFAULT)
|
|
||||||
summary = "%s"
|
|
||||||
}.also(screen::addPreference)
|
|
||||||
|
|
||||||
FilemoonExtractor.addSubtitlePref(screen)
|
|
||||||
SavefileExtractor.addSubtitlePref(screen)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
ext {
|
ext {
|
||||||
extName = 'JavGG'
|
extName = 'JavGG'
|
||||||
extClass = '.Javgg'
|
extClass = '.Javgg'
|
||||||
extVersionCode = 7
|
extVersionCode = 5
|
||||||
isNsfw = true
|
isNsfw = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
ext {
|
ext {
|
||||||
extName = 'Jav Guru'
|
extName = 'Jav Guru'
|
||||||
extClass = '.JavGuru'
|
extClass = '.JavGuru'
|
||||||
extVersionCode = 28
|
extVersionCode = 26
|
||||||
isNsfw = true
|
isNsfw = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,7 @@ ext {
|
||||||
extClass = '.LMAnime'
|
extClass = '.LMAnime'
|
||||||
themePkg = 'animestream'
|
themePkg = 'animestream'
|
||||||
baseUrl = 'https://lmanime.com'
|
baseUrl = 'https://lmanime.com'
|
||||||
overrideVersionCode = 11
|
overrideVersionCode = 9
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
ext {
|
ext {
|
||||||
extName = 'MissAV'
|
extName = 'MissAV'
|
||||||
extClass = '.MissAV'
|
extClass = '.MissAV'
|
||||||
extVersionCode = 16
|
extVersionCode = 15
|
||||||
isNsfw = true
|
isNsfw = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,8 +0,0 @@
|
||||||
ext {
|
|
||||||
extName = 'Newgrounds'
|
|
||||||
extClass = '.NewGrounds'
|
|
||||||
extVersionCode = 1
|
|
||||||
isNsfw = true
|
|
||||||
}
|
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
|
Binary file not shown.
Before Width: | Height: | Size: 4.1 KiB |
Binary file not shown.
Before Width: | Height: | Size: 2.3 KiB |
Binary file not shown.
Before Width: | Height: | Size: 5.7 KiB |
Binary file not shown.
Before Width: | Height: | Size: 11 KiB |
Binary file not shown.
Before Width: | Height: | Size: 16 KiB |
|
@ -1,13 +0,0 @@
|
||||||
import java.text.ParseException
|
|
||||||
import java.text.SimpleDateFormat
|
|
||||||
|
|
||||||
@Suppress("NOTHING_TO_INLINE")
|
|
||||||
inline fun SimpleDateFormat.tryParse(date: String?): Long {
|
|
||||||
date ?: return 0L
|
|
||||||
|
|
||||||
return try {
|
|
||||||
parse(date)?.time ?: 0L
|
|
||||||
} catch (_: ParseException) {
|
|
||||||
0L
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,545 +0,0 @@
|
||||||
package eu.kanade.tachiyomi.animeextension.all.newgrounds
|
|
||||||
|
|
||||||
import android.app.Application
|
|
||||||
import android.os.Handler
|
|
||||||
import android.os.Looper
|
|
||||||
import android.widget.Toast
|
|
||||||
import androidx.preference.CheckBoxPreference
|
|
||||||
import androidx.preference.ListPreference
|
|
||||||
import androidx.preference.MultiSelectListPreference
|
|
||||||
import androidx.preference.PreferenceScreen
|
|
||||||
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.network.GET
|
|
||||||
import eu.kanade.tachiyomi.util.asJsoup
|
|
||||||
import okhttp3.FormBody
|
|
||||||
import okhttp3.Headers
|
|
||||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
|
||||||
import okhttp3.Request
|
|
||||||
import okhttp3.Response
|
|
||||||
import org.json.JSONObject
|
|
||||||
import org.jsoup.nodes.Document
|
|
||||||
import org.jsoup.nodes.Element
|
|
||||||
import tryParse
|
|
||||||
import uy.kohesive.injekt.Injekt
|
|
||||||
import uy.kohesive.injekt.api.get
|
|
||||||
import java.text.SimpleDateFormat
|
|
||||||
import java.util.Locale
|
|
||||||
|
|
||||||
private const val PAGE_SIZE = 20
|
|
||||||
|
|
||||||
class NewGrounds : ParsedAnimeHttpSource(), ConfigurableAnimeSource {
|
|
||||||
|
|
||||||
override val lang = "all"
|
|
||||||
override val baseUrl = "https://www.newgrounds.com"
|
|
||||||
override val name = "Newgrounds"
|
|
||||||
override val supportsLatest = true
|
|
||||||
|
|
||||||
private val dateFormat = SimpleDateFormat("MMM dd, yyyy", Locale.ENGLISH)
|
|
||||||
|
|
||||||
private val preferences by lazy {
|
|
||||||
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
|
|
||||||
}
|
|
||||||
|
|
||||||
private val context = Injekt.get<Application>()
|
|
||||||
private val handler by lazy { Handler(Looper.getMainLooper()) }
|
|
||||||
|
|
||||||
private val videoListHeaders by lazy {
|
|
||||||
headers.newBuilder()
|
|
||||||
.add("Accept", "application/json, text/javascript, */*; q=0.01")
|
|
||||||
.add("X-Requested-With", "XMLHttpRequest")
|
|
||||||
.add("Referer", baseUrl)
|
|
||||||
.build()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Latest
|
|
||||||
|
|
||||||
private fun getLatestSection(): String {
|
|
||||||
return preferences.getString("LATEST", PREF_SECTIONS["Latest"])!!
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun latestUpdatesRequest(page: Int): Request {
|
|
||||||
val offset = (page - 1) * PAGE_SIZE
|
|
||||||
return GET("$baseUrl/${getLatestSection()}?offset=$offset", headers)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun latestUpdatesNextPageSelector(): String = "#load-more-items a"
|
|
||||||
|
|
||||||
override fun latestUpdatesParse(response: Response): AnimesPage {
|
|
||||||
checkAdultContentFiltered(response.headers)
|
|
||||||
return super.latestUpdatesParse(response)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun latestUpdatesSelector(): String = animeSelector(getLatestSection())
|
|
||||||
|
|
||||||
override fun latestUpdatesFromElement(element: Element): SAnime {
|
|
||||||
return animeFromElement(element, getLatestSection())
|
|
||||||
}
|
|
||||||
|
|
||||||
// Browse
|
|
||||||
|
|
||||||
private fun getPopularSection(): String {
|
|
||||||
return preferences.getString("POPULAR", PREF_SECTIONS["Popular"])!!
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun popularAnimeRequest(page: Int): Request {
|
|
||||||
val offset = (page - 1) * PAGE_SIZE
|
|
||||||
return GET("$baseUrl/${getPopularSection()}?offset=$offset", headers)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun popularAnimeNextPageSelector(): String = "#load-more-items a"
|
|
||||||
|
|
||||||
override fun popularAnimeParse(response: Response): AnimesPage {
|
|
||||||
checkAdultContentFiltered(response.headers)
|
|
||||||
return super.popularAnimeParse(response)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun popularAnimeSelector(): String = animeSelector(getPopularSection())
|
|
||||||
|
|
||||||
override fun popularAnimeFromElement(element: Element): SAnime {
|
|
||||||
return animeFromElement(element, getPopularSection())
|
|
||||||
}
|
|
||||||
|
|
||||||
// Search
|
|
||||||
|
|
||||||
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
|
|
||||||
val searchUrl = "$baseUrl/search/conduct/movies".toHttpUrl().newBuilder()
|
|
||||||
.addQueryParameter("page", page.toString())
|
|
||||||
|
|
||||||
if (query.isNotEmpty()) searchUrl.addQueryParameter("terms", query)
|
|
||||||
|
|
||||||
filters.findInstance<MatchAgainstFilter>().ifFilterSet {
|
|
||||||
searchUrl.addQueryParameter("match", MATCH_AGAINST.values.elementAt(it.state))
|
|
||||||
}
|
|
||||||
filters.findInstance<TuningFilterGroup>()?.state
|
|
||||||
?.findInstance<TuningExactFilter>().ifFilterSet {
|
|
||||||
searchUrl.addQueryParameter("exact", "1")
|
|
||||||
}
|
|
||||||
filters.findInstance<TuningFilterGroup>()?.state
|
|
||||||
?.findInstance<TuningAnyFilter>().ifFilterSet {
|
|
||||||
searchUrl.addQueryParameter("any", "1")
|
|
||||||
}
|
|
||||||
filters.findInstance<AuthorFilter>().ifFilterSet {
|
|
||||||
searchUrl.addQueryParameter("user", it.state)
|
|
||||||
}
|
|
||||||
filters.findInstance<GenreFilter>().ifFilterSet {
|
|
||||||
searchUrl.addQueryParameter("genre", GENRE.values.elementAt(it.state))
|
|
||||||
}
|
|
||||||
filters.findInstance<LengthFilterGroup>()?.state
|
|
||||||
?.findInstance<MinLengthFilter>().ifFilterSet {
|
|
||||||
searchUrl.addQueryParameter("min_length", it.state)
|
|
||||||
}
|
|
||||||
filters.findInstance<LengthFilterGroup>()?.state
|
|
||||||
?.findInstance<MaxLengthFilter>().ifFilterSet {
|
|
||||||
searchUrl.addQueryParameter("max_length", it.state)
|
|
||||||
}
|
|
||||||
filters.findInstance<FrontpagedFilter>().ifFilterSet {
|
|
||||||
searchUrl.addQueryParameter("frontpaged", "1")
|
|
||||||
}
|
|
||||||
filters.findInstance<DateFilterGroup>()?.state
|
|
||||||
?.findInstance<AfterDateFilter>().ifFilterSet {
|
|
||||||
searchUrl.addQueryParameter("after", it.state)
|
|
||||||
}
|
|
||||||
filters.findInstance<DateFilterGroup>()?.state
|
|
||||||
?.findInstance<BeforeDateFilter>().ifFilterSet {
|
|
||||||
searchUrl.addQueryParameter("before", it.state)
|
|
||||||
}
|
|
||||||
filters.findInstance<SortingFilter>().ifFilterSet {
|
|
||||||
if (it.state?.index != 0) {
|
|
||||||
val sortOption = SORTING.values.elementAt(it.state?.index ?: return@ifFilterSet)
|
|
||||||
val direction = if (it.state?.ascending == true) "asc" else "desc"
|
|
||||||
searchUrl.addQueryParameter(
|
|
||||||
"sort",
|
|
||||||
"$sortOption-$direction",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
filters.findInstance<TagsFilter>().ifFilterSet {
|
|
||||||
searchUrl.addQueryParameter("tags", it.state)
|
|
||||||
}
|
|
||||||
|
|
||||||
return GET(searchUrl.build(), headers)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun searchAnimeNextPageSelector(): String = "#results-load-more"
|
|
||||||
|
|
||||||
override fun searchAnimeParse(response: Response): AnimesPage {
|
|
||||||
checkAdultContentFiltered(response.headers)
|
|
||||||
return super.searchAnimeParse(response)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun searchAnimeSelector(): String = "ul.itemlist li:not(#results-load-more) a"
|
|
||||||
|
|
||||||
override fun searchAnimeFromElement(element: Element): SAnime = animeFromListElement(element)
|
|
||||||
|
|
||||||
// Etc.
|
|
||||||
|
|
||||||
override fun animeDetailsParse(document: Document): SAnime {
|
|
||||||
fun getStarRating(): String {
|
|
||||||
val score: Double = document.selectFirst("#score_number")?.text()?.toDouble() ?: 0.0
|
|
||||||
val fullStars = score.toInt()
|
|
||||||
val hasHalfStar = (score % 1) >= 0.5
|
|
||||||
val totalStars = if (hasHalfStar) fullStars + 1 else fullStars
|
|
||||||
val emptyStars = 5 - totalStars
|
|
||||||
|
|
||||||
return "✪".repeat(fullStars) + (if (hasHalfStar) "✪" else "") + "⬤".repeat(emptyStars) + " ($score)"
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getAdultRating(): String {
|
|
||||||
val rating = document.selectFirst("#embed_header h2")!!.className().substringAfter("rated-")
|
|
||||||
return when (rating) {
|
|
||||||
"e" -> "🟩 Everyone"
|
|
||||||
"t" -> "🟦 Ages 13+"
|
|
||||||
"m" -> "🟪 Ages 17+"
|
|
||||||
"a" -> "🟥 Adults Only"
|
|
||||||
else -> "❓"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getStats(): String {
|
|
||||||
val statsElement = document.selectFirst("#sidestats > dl:first-of-type")
|
|
||||||
val views = statsElement?.selectFirst("dd:first-of-type")?.text() ?: "?"
|
|
||||||
val faves = statsElement?.selectFirst("dd:nth-of-type(2)")?.text() ?: "?"
|
|
||||||
val votes = statsElement?.selectFirst("dd:nth-of-type(3)")?.text() ?: "?"
|
|
||||||
|
|
||||||
return "👀 $views | ❤️ $faves | 🗳️ $votes"
|
|
||||||
}
|
|
||||||
|
|
||||||
fun prepareDescription(): String {
|
|
||||||
val descriptionElements = preferences.getStringSet("DESCRIPTION_ELEMENTS", setOf("short"))
|
|
||||||
?: return ""
|
|
||||||
|
|
||||||
val shortDescription = document.selectFirst("meta[itemprop=\"description\"]")?.attr("content")
|
|
||||||
val longDescription = document.selectFirst("#author_comments")?.wholeText()
|
|
||||||
val statsSummary = "${getAdultRating()} | ${getStarRating()} | ${getStats()}"
|
|
||||||
|
|
||||||
val description = StringBuilder()
|
|
||||||
|
|
||||||
if (descriptionElements.contains("short")) {
|
|
||||||
description.append(shortDescription)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (descriptionElements.contains("long")) {
|
|
||||||
description.append("\n\n" + longDescription)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (descriptionElements.contains("stats") || preferences.getBoolean("STATS_SUMMARY", false)) {
|
|
||||||
description.append("\n\n" + statsSummary)
|
|
||||||
}
|
|
||||||
|
|
||||||
return description.toString()
|
|
||||||
}
|
|
||||||
|
|
||||||
val relatedPlaylistElement = document.selectFirst("div[id^=\"related_playlists\"] ")
|
|
||||||
val relatedPlaylistUrl = relatedPlaylistElement?.selectFirst("a:not([id^=\"related_playlists\"])")?.absUrl("href")
|
|
||||||
val relatedPlaylistName = relatedPlaylistElement?.selectFirst(".detail-title h4")?.text()
|
|
||||||
val isPartOfSeries = relatedPlaylistUrl?.startsWith("$baseUrl/series") ?: false
|
|
||||||
|
|
||||||
return SAnime.create().apply {
|
|
||||||
title = relatedPlaylistName.takeIf { isPartOfSeries }
|
|
||||||
?: document.selectFirst("h2[itemprop=\"name\"]")!!.text()
|
|
||||||
description = prepareDescription()
|
|
||||||
author = document.selectFirst(".authorlinks > div:first-of-type .item-details-main")?.text()
|
|
||||||
artist = document.select(".authorlinks > div:not(:first-of-type) .item-details-main").joinToString {
|
|
||||||
it.text()
|
|
||||||
}
|
|
||||||
thumbnail_url = document.selectFirst("meta[itemprop=\"thumbnailUrl\"]")?.absUrl("content")
|
|
||||||
genre = document.select(".tags li a").joinToString { it.text() } + document.selectFirst("div[id^=\"genre-view\"] dt")?.text()
|
|
||||||
status = SAnime.ONGOING.takeIf { isPartOfSeries } ?: SAnime.COMPLETED
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun episodeListSelector(): String = throw UnsupportedOperationException("Not Used")
|
|
||||||
|
|
||||||
override fun episodeFromElement(element: Element): SEpisode = throw UnsupportedOperationException("Not Used")
|
|
||||||
|
|
||||||
override suspend fun getEpisodeList(anime: SAnime): List<SEpisode> {
|
|
||||||
val response = client.newCall(GET("${baseUrl}${anime.url}", headers)).execute()
|
|
||||||
val document = response.asJsoup()
|
|
||||||
|
|
||||||
val relatedPlaylistUrl = document.selectFirst("div[id^=\"related_playlists\"] a:not([id^=\"related_playlists\"])")?.absUrl("href")
|
|
||||||
val isPartOfSeries = relatedPlaylistUrl?.startsWith("$baseUrl/series") ?: false
|
|
||||||
|
|
||||||
val episodes = if (isPartOfSeries) {
|
|
||||||
val response2 = client.newCall(GET(relatedPlaylistUrl!!, headers)).execute()
|
|
||||||
val document2 = response2.asJsoup()
|
|
||||||
parseEpisodeList(document2)
|
|
||||||
} else {
|
|
||||||
val dateString = document.selectFirst("#sidestats > dl:nth-of-type(2) > dd:first-of-type")?.text()
|
|
||||||
|
|
||||||
return listOf(
|
|
||||||
SEpisode.create().apply {
|
|
||||||
episode_number = 1f
|
|
||||||
date_upload = dateFormat.tryParse(dateString)
|
|
||||||
name = document.selectFirst("meta[name=\"title\"]")!!.attr("content")
|
|
||||||
setUrlWithoutDomain("$baseUrl${anime.url.replace("/view/","/video/")}")
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return episodes
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun episodeListRequest(anime: SAnime): Request = throw UnsupportedOperationException()
|
|
||||||
|
|
||||||
override fun episodeListParse(response: Response): List<SEpisode> = throw UnsupportedOperationException()
|
|
||||||
|
|
||||||
private fun parseEpisodeList(document: Document): List<SEpisode> {
|
|
||||||
val ids = document.select("li.visual-link-container").map { it.attr("data-visual-link") }
|
|
||||||
val formBody = FormBody.Builder()
|
|
||||||
.add("ids", ids.toString())
|
|
||||||
.add("component_params[include_author]", "1")
|
|
||||||
.add("include_all_suitabilities", "0")
|
|
||||||
.add("isAjaxRequest", "1")
|
|
||||||
.build()
|
|
||||||
|
|
||||||
val request = Request.Builder()
|
|
||||||
.url("$baseUrl/visual-links-fetch")
|
|
||||||
.post(formBody)
|
|
||||||
.headers(headers)
|
|
||||||
.build()
|
|
||||||
|
|
||||||
val response = client.newCall(request).execute()
|
|
||||||
|
|
||||||
val jsonObject = JSONObject(response.body.string())
|
|
||||||
val episodes = mutableListOf<SEpisode>()
|
|
||||||
|
|
||||||
val simples = jsonObject.getJSONObject("simples")
|
|
||||||
var index = 1
|
|
||||||
for (key in simples.keys()) {
|
|
||||||
val subObject = simples.getJSONObject(key)
|
|
||||||
|
|
||||||
for (episodeKey in subObject.keys()) {
|
|
||||||
val episodeData = subObject.getJSONObject(episodeKey)
|
|
||||||
val uploaderData = episodeData.getJSONObject("user")
|
|
||||||
|
|
||||||
val episode = SEpisode.create().apply {
|
|
||||||
episode_number = index.toFloat()
|
|
||||||
name = episodeData.getString("title")
|
|
||||||
scanlator = uploaderData.getString("user_name")
|
|
||||||
setUrlWithoutDomain("$baseUrl/portal/video/${episodeData.getString("id")}")
|
|
||||||
}
|
|
||||||
|
|
||||||
episodes.add(episode)
|
|
||||||
index++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return episodes.reversed()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun videoListRequest(episode: SEpisode): Request = GET("$baseUrl${episode.url}", videoListHeaders)
|
|
||||||
|
|
||||||
override fun videoListParse(response: Response): List<Video> {
|
|
||||||
val responseBody = response.body.string()
|
|
||||||
val json = JSONObject(responseBody)
|
|
||||||
val sources = json.getJSONObject("sources")
|
|
||||||
|
|
||||||
val videos = mutableListOf<Video>()
|
|
||||||
|
|
||||||
for (quality in sources.keys()) {
|
|
||||||
val qualityArray = sources.getJSONArray(quality)
|
|
||||||
for (i in 0 until qualityArray.length()) {
|
|
||||||
val videoObject = qualityArray.getJSONObject(i)
|
|
||||||
val videoUrl = videoObject.getString("src")
|
|
||||||
|
|
||||||
videos.add(
|
|
||||||
Video(
|
|
||||||
url = videoUrl,
|
|
||||||
quality = quality,
|
|
||||||
videoUrl = videoUrl,
|
|
||||||
headers = headers,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return videos
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun videoListSelector(): String = throw UnsupportedOperationException("Not Used")
|
|
||||||
|
|
||||||
override fun videoFromElement(element: Element): Video = throw UnsupportedOperationException("Not Used")
|
|
||||||
|
|
||||||
override fun videoUrlParse(document: Document): String = throw UnsupportedOperationException("Not Used")
|
|
||||||
|
|
||||||
// ============================== Filters ===============================
|
|
||||||
|
|
||||||
override fun getFilterList(): AnimeFilterList = AnimeFilterList(
|
|
||||||
SortingFilter(),
|
|
||||||
MatchAgainstFilter(),
|
|
||||||
TuningFilterGroup(),
|
|
||||||
GenreFilter(),
|
|
||||||
AuthorFilter(),
|
|
||||||
TagsFilter(),
|
|
||||||
LengthFilterGroup(),
|
|
||||||
DateFilterGroup(),
|
|
||||||
FrontpagedFilter(),
|
|
||||||
AnimeFilter.Separator(),
|
|
||||||
AnimeFilter.Header("Age rating: to change age rating open WebView and in Movies tab click on 🟩🟦🟪🟥 icons on the right. Then refresh search."), // uses ng_user0 cookie
|
|
||||||
)
|
|
||||||
|
|
||||||
// ============================ Preferences =============================
|
|
||||||
/*
|
|
||||||
According to the labels on the website:
|
|
||||||
Featured -> /movies/featured
|
|
||||||
Latest -> /movies/browse
|
|
||||||
Popular -> /movies/popular
|
|
||||||
Your Feed -> /social/feeds/show/favorite-artists-movies
|
|
||||||
Under Judgement -> /movies/browse?interval=all&artist-type=unjudged
|
|
||||||
*/
|
|
||||||
|
|
||||||
override fun setupPreferenceScreen(screen: PreferenceScreen) {
|
|
||||||
ListPreference(screen.context).apply {
|
|
||||||
key = "POPULAR"
|
|
||||||
title = "Popular section content"
|
|
||||||
entries = PREF_SECTIONS.keys.toTypedArray()
|
|
||||||
entryValues = PREF_SECTIONS.values.toTypedArray()
|
|
||||||
setDefaultValue(PREF_SECTIONS["Popular"])
|
|
||||||
summary = "%s"
|
|
||||||
|
|
||||||
setOnPreferenceChangeListener { _, newValue ->
|
|
||||||
val selected = newValue as String
|
|
||||||
preferences.edit().putString(key, selected).commit()
|
|
||||||
}
|
|
||||||
}.also(screen::addPreference)
|
|
||||||
|
|
||||||
ListPreference(screen.context).apply {
|
|
||||||
key = "LATEST"
|
|
||||||
title = "Latest section content"
|
|
||||||
entries = PREF_SECTIONS.keys.toTypedArray()
|
|
||||||
entryValues = PREF_SECTIONS.values.toTypedArray()
|
|
||||||
setDefaultValue(PREF_SECTIONS["Latest"])
|
|
||||||
summary = "%s"
|
|
||||||
|
|
||||||
setOnPreferenceChangeListener { _, newValue ->
|
|
||||||
val selected = newValue as String
|
|
||||||
preferences.edit().putString(key, selected).commit()
|
|
||||||
}
|
|
||||||
}.also(screen::addPreference)
|
|
||||||
|
|
||||||
MultiSelectListPreference(screen.context).apply {
|
|
||||||
key = "DESCRIPTION_ELEMENTS"
|
|
||||||
title = "Description elements"
|
|
||||||
entries = arrayOf("Short description", "Long description (author comments)", "Stats (score, favs, views)")
|
|
||||||
entryValues = arrayOf("short", "long", "stats")
|
|
||||||
setDefaultValue(setOf("short", "stats"))
|
|
||||||
summary = "Elements to be included in description"
|
|
||||||
|
|
||||||
setOnPreferenceChangeListener { _, newValue ->
|
|
||||||
val selectedItems = newValue as Set<*>
|
|
||||||
preferences.edit().putStringSet(key, selectedItems as Set<String>).apply()
|
|
||||||
true
|
|
||||||
}
|
|
||||||
}.also(screen::addPreference)
|
|
||||||
|
|
||||||
CheckBoxPreference(screen.context).apply {
|
|
||||||
key = "PROMPT_CONTENT_FILTERED"
|
|
||||||
title = "Prompt to log in"
|
|
||||||
setDefaultValue(true)
|
|
||||||
summary = "Show toast when user is not logged in and therefore adult content is not accessible"
|
|
||||||
}.also(screen::addPreference)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ========================== Helper Functions ==========================
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Chooses an extraction technique for anime information, based on section selected in Preferences
|
|
||||||
*/
|
|
||||||
private fun animeFromElement(element: Element, section: String): SAnime {
|
|
||||||
return if (section == PREF_SECTIONS["Your Feed"]) {
|
|
||||||
animeFromFeedElement(element)
|
|
||||||
} else {
|
|
||||||
animeFromGridElement(element)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Extracts anime information from element of grid-like list typical for /popular, /browse or /featured
|
|
||||||
*/
|
|
||||||
private fun animeFromGridElement(element: Element): SAnime = SAnime.create().apply {
|
|
||||||
title = element.selectFirst(".card-title h4")!!.text()
|
|
||||||
author = element.selectFirst(".card-title span")?.text()?.replace("By ", "")
|
|
||||||
description = element.selectFirst("a")?.attr("title")
|
|
||||||
thumbnail_url = element.selectFirst("img")?.absUrl("src")
|
|
||||||
setUrlWithoutDomain(element.selectFirst("a")!!.absUrl("href"))
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Extracts anime information from element of list returned in Your Feed
|
|
||||||
*/
|
|
||||||
private fun animeFromFeedElement(element: Element): SAnime = SAnime.create().apply {
|
|
||||||
title = element.selectFirst(".detail-title h4")!!.text()
|
|
||||||
author = element.selectFirst(".detail-title strong")?.text()
|
|
||||||
description = element.selectFirst(".detail-description")?.text()
|
|
||||||
thumbnail_url = element.selectFirst(".item-icon img")?.absUrl("src")
|
|
||||||
setUrlWithoutDomain(element.selectFirst("a")!!.absUrl("href"))
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Extracts anime information from element of list typical for /search or /series
|
|
||||||
*/
|
|
||||||
private fun animeFromListElement(element: Element): SAnime = SAnime.create().apply {
|
|
||||||
title = element.selectFirst(".detail-title > h4")!!.text()
|
|
||||||
author = element.selectFirst(".detail-title > span > strong")?.text()
|
|
||||||
description = element.selectFirst(".detail-description")?.text()
|
|
||||||
thumbnail_url = element.selectFirst(".item-icon img")?.absUrl("src")
|
|
||||||
setUrlWithoutDomain(element.selectFirst("a")!!.absUrl("href"))
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns CSS selector for anime, based on the section selected in Preferences
|
|
||||||
*/
|
|
||||||
private fun animeSelector(section: String): String {
|
|
||||||
return if (section == PREF_SECTIONS["Your Feed"]) {
|
|
||||||
"a.item-portalsubmission"
|
|
||||||
} else {
|
|
||||||
"a.inline-card-portalsubmission"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks if cookie with username is present in response headers.
|
|
||||||
* If cookie is missing: displays a toast with information.
|
|
||||||
*/
|
|
||||||
private fun checkAdultContentFiltered(headers: Headers) {
|
|
||||||
val usernameCookie: Boolean = headers.values("Set-Cookie").any { it.startsWith("NG_GG_username=") }
|
|
||||||
if (usernameCookie) return // user already logged in
|
|
||||||
|
|
||||||
val shouldPrompt = preferences.getBoolean("PROMPT_CONTENT_FILTERED", true)
|
|
||||||
if (shouldPrompt) {
|
|
||||||
handler.post {
|
|
||||||
Toast.makeText(context, "Log in via WebView to include adult content", Toast.LENGTH_SHORT).show()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private inline fun <reified T> Iterable<*>.findInstance() = find { it is T } as? T
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Executes the given [action] if the filter is set to a meaningful value.
|
|
||||||
*
|
|
||||||
* @param action A function to execute if the filter is set.
|
|
||||||
*/
|
|
||||||
private inline fun <T> T?.ifFilterSet(action: (T) -> Unit) where T : AnimeFilter<*> {
|
|
||||||
val state = this?.state
|
|
||||||
if (this != null && state != null && state != "" && state != 0 && state != false) {
|
|
||||||
action(this)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
private val PREF_SECTIONS = mapOf(
|
|
||||||
"Featured" to "movies/featured",
|
|
||||||
"Latest" to "movies/browse",
|
|
||||||
"Popular" to "movies/popular",
|
|
||||||
"Your Feed" to "social/feeds/show/favorite-artists-movies",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,75 +0,0 @@
|
||||||
package eu.kanade.tachiyomi.animeextension.all.newgrounds
|
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.animesource.model.AnimeFilter
|
|
||||||
|
|
||||||
class MatchAgainstFilter : AnimeFilter.Select<String>("Match against", MATCH_AGAINST.keys.toTypedArray(), 0)
|
|
||||||
|
|
||||||
class TuningExactFilter : AnimeFilter.CheckBox("exact matches", false)
|
|
||||||
class TuningAnyFilter : AnimeFilter.CheckBox("match any words", false)
|
|
||||||
class TuningFilterGroup : AnimeFilter.Group<AnimeFilter.CheckBox>(
|
|
||||||
"Tuning",
|
|
||||||
listOf(
|
|
||||||
TuningExactFilter(),
|
|
||||||
TuningAnyFilter(),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
class AuthorFilter : AnimeFilter.Text("Author")
|
|
||||||
|
|
||||||
class GenreFilter : AnimeFilter.Select<String>("Genre", GENRE.keys.toTypedArray())
|
|
||||||
|
|
||||||
class MinLengthFilter : AnimeFilter.Text("Min Length")
|
|
||||||
class MaxLengthFilter : AnimeFilter.Text("Max Length")
|
|
||||||
class LengthFilterGroup : AnimeFilter.Group<AnimeFilter.Text>(
|
|
||||||
"Length (00:00:00)",
|
|
||||||
listOf(
|
|
||||||
MinLengthFilter(),
|
|
||||||
MaxLengthFilter(),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
class FrontpagedFilter : AnimeFilter.CheckBox("Frontpaged?", false)
|
|
||||||
|
|
||||||
class AfterDateFilter : AnimeFilter.Text("On, or after")
|
|
||||||
class BeforeDateFilter : AnimeFilter.Text("Before")
|
|
||||||
class DateFilterGroup : AnimeFilter.Group<AnimeFilter.Text>(
|
|
||||||
"Date (YYYY-MM-DD)",
|
|
||||||
listOf(
|
|
||||||
AfterDateFilter(),
|
|
||||||
BeforeDateFilter(),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
class SortingFilter() : AnimeFilter.Sort("Sort by", SORTING.keys.toTypedArray(), Selection(0, true))
|
|
||||||
|
|
||||||
class TagsFilter() : AnimeFilter.Text("Tags (comma separated)")
|
|
||||||
|
|
||||||
// ===================================================================
|
|
||||||
val MATCH_AGAINST = mapOf(
|
|
||||||
"Default" to "",
|
|
||||||
"title / description / tags / author" to "tdtu",
|
|
||||||
"title / description / tags" to "tdt",
|
|
||||||
"title / description" to "td",
|
|
||||||
"title" to "t",
|
|
||||||
"description" to "d",
|
|
||||||
)
|
|
||||||
|
|
||||||
val GENRE = mapOf(
|
|
||||||
"All" to "",
|
|
||||||
"Action" to "45",
|
|
||||||
"Comedy - Original" to "60",
|
|
||||||
"Comedy - Parody" to "61",
|
|
||||||
"Drama" to "47",
|
|
||||||
"Experimental" to "49",
|
|
||||||
"Informative" to "48",
|
|
||||||
"Music Video" to "50",
|
|
||||||
"Other" to "51",
|
|
||||||
"Spam" to "55",
|
|
||||||
)
|
|
||||||
|
|
||||||
val SORTING = mapOf(
|
|
||||||
"Default (Relevance)" to "relevance",
|
|
||||||
"Date" to "date",
|
|
||||||
"Score" to "score",
|
|
||||||
"Views" to "views",
|
|
||||||
)
|
|
|
@ -1,7 +1,7 @@
|
||||||
ext {
|
ext {
|
||||||
extName = 'Sudatchi'
|
extName = 'Sudatchi'
|
||||||
extClass = '.Sudatchi'
|
extClass = '.Sudatchi'
|
||||||
extVersionCode = 13
|
extVersionCode = 12
|
||||||
isNsfw = true
|
isNsfw = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
ext {
|
ext {
|
||||||
extName = 'SupJav'
|
extName = 'SupJav'
|
||||||
extClass = '.SupJavFactory'
|
extClass = '.SupJavFactory'
|
||||||
extVersionCode = 16
|
extVersionCode = 14
|
||||||
isNsfw = true
|
isNsfw = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
ext {
|
ext {
|
||||||
extName = 'Torrentio Anime (Torrent / Debrid)'
|
extName = 'Torrentio Anime (Torrent / Debrid)'
|
||||||
extClass = '.Torrentio'
|
extClass = '.Torrentio'
|
||||||
extVersionCode = 16
|
extVersionCode = 15
|
||||||
containsNsfw = false
|
containsNsfw = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -310,7 +310,7 @@ class Torrentio : ConfigurableAnimeSource, AnimeHttpSource() {
|
||||||
val aniZipResponse = json.decodeFromString<AniZipResponse>(responseString)
|
val aniZipResponse = json.decodeFromString<AniZipResponse>(responseString)
|
||||||
|
|
||||||
return when (aniZipResponse.mappings?.type) {
|
return when (aniZipResponse.mappings?.type) {
|
||||||
"TV", "ONA", "OVA" -> {
|
"TV" -> {
|
||||||
aniZipResponse.episodes
|
aniZipResponse.episodes
|
||||||
?.let { episodes ->
|
?.let { episodes ->
|
||||||
if (preferences.getBoolean(UPCOMING_EP_KEY, UPCOMING_EP_DEFAULT)) {
|
if (preferences.getBoolean(UPCOMING_EP_KEY, UPCOMING_EP_DEFAULT)) {
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
ext {
|
ext {
|
||||||
extName = 'Anime4up'
|
extName = 'Anime4up'
|
||||||
extClass = '.Anime4Up'
|
extClass = '.Anime4Up'
|
||||||
extVersionCode = 64
|
extVersionCode = 62
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
ext {
|
ext {
|
||||||
extName = 'Anime Blkom'
|
extName = 'Anime Blkom'
|
||||||
extClass = '.AnimeBlkom'
|
extClass = '.AnimeBlkom'
|
||||||
extVersionCode = 19
|
extVersionCode = 18
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
ext {
|
ext {
|
||||||
extName = 'AnimeLek'
|
extName = 'AnimeLek'
|
||||||
extClass = '.AnimeLek'
|
extClass = '.AnimeLek'
|
||||||
extVersionCode = 32
|
extVersionCode = 31
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
ext {
|
ext {
|
||||||
extName = 'Animerco'
|
extName = 'Animerco'
|
||||||
extClass = '.Animerco'
|
extClass = '.Animerco'
|
||||||
extVersionCode = 43
|
extVersionCode = 41
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
ext {
|
ext {
|
||||||
extName = 'Arab Seed'
|
extName = 'Arab Seed'
|
||||||
extClass = '.ArabSeed'
|
extClass = '.ArabSeed'
|
||||||
extVersionCode = 19
|
extVersionCode = 17
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
ext {
|
ext {
|
||||||
extName = 'asia2tv'
|
extName = 'asia2tv'
|
||||||
extClass = '.Asia2TV'
|
extClass = '.Asia2TV'
|
||||||
extVersionCode = 24
|
extVersionCode = 22
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
ext {
|
ext {
|
||||||
extName = 'Cimaleek'
|
extName = 'Cimaleek'
|
||||||
extClass = '.Cimaleek'
|
extClass = '.Cimaleek'
|
||||||
extVersionCode = 4
|
extVersionCode = 3
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
ext {
|
ext {
|
||||||
extName = 'Egy Dead'
|
extName = 'Egy Dead'
|
||||||
extClass = '.EgyDead'
|
extClass = '.EgyDead'
|
||||||
extVersionCode = 19
|
extVersionCode = 17
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
ext {
|
ext {
|
||||||
extName = 'FASELHD'
|
extName = 'FASELHD'
|
||||||
extClass = '.FASELHD'
|
extClass = '.FASELHD'
|
||||||
extVersionCode = 18
|
extVersionCode = 17
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
ext {
|
ext {
|
||||||
extName = 'MY CIMA'
|
extName = 'MY CIMA'
|
||||||
extClass = '.MyCima'
|
extClass = '.MyCima'
|
||||||
extVersionCode = 24
|
extVersionCode = 23
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
ext {
|
ext {
|
||||||
extName = 'Okanime'
|
extName = 'Okanime'
|
||||||
extClass = '.Okanime'
|
extClass = '.Okanime'
|
||||||
extVersionCode = 14
|
extVersionCode = 13
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
ext {
|
ext {
|
||||||
extName = 'WIT ANIME'
|
extName = 'WIT ANIME'
|
||||||
extClass = '.WitAnime'
|
extClass = '.WitAnime'
|
||||||
extVersionCode = 53
|
extVersionCode = 51
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
|
|
@ -30,7 +30,7 @@ class WitAnime : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||||
|
|
||||||
override val name = "WIT ANIME"
|
override val name = "WIT ANIME"
|
||||||
|
|
||||||
override val baseUrl = "https://witanime.cyou"
|
override val baseUrl = "https://witanime.pics"
|
||||||
|
|
||||||
override val lang = "ar"
|
override val lang = "ar"
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
ext {
|
ext {
|
||||||
extName = 'Anime-Base'
|
extName = 'Anime-Base'
|
||||||
extClass = '.AnimeBase'
|
extClass = '.AnimeBase'
|
||||||
extVersionCode = 33
|
extVersionCode = 31
|
||||||
isNsfw = true
|
isNsfw = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
ext {
|
ext {
|
||||||
extName = 'Anime-Loads'
|
extName = 'Anime-Loads'
|
||||||
extClass = '.AnimeLoads'
|
extClass = '.AnimeLoads'
|
||||||
extVersionCode = 18
|
extVersionCode = 17
|
||||||
isNsfw = true
|
isNsfw = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
ext {
|
ext {
|
||||||
extName = 'AnimeToast'
|
extName = 'AnimeToast'
|
||||||
extClass = '.AnimeToast'
|
extClass = '.AnimeToast'
|
||||||
extVersionCode = 23
|
extVersionCode = 21
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
ext {
|
ext {
|
||||||
extName = 'AniWorld'
|
extName = 'AniWorld'
|
||||||
extClass = '.AniWorld'
|
extClass = '.AniWorld'
|
||||||
extVersionCode = 26
|
extVersionCode = 25
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
|
|
@ -3,7 +3,7 @@ ext {
|
||||||
extClass = '.Cinemathek'
|
extClass = '.Cinemathek'
|
||||||
themePkg = 'dooplay'
|
themePkg = 'dooplay'
|
||||||
baseUrl = 'https://cinemathek.net'
|
baseUrl = 'https://cinemathek.net'
|
||||||
overrideVersionCode = 26
|
overrideVersionCode = 24
|
||||||
isNsfw = true
|
isNsfw = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
ext {
|
ext {
|
||||||
extName = 'Einfach'
|
extName = 'Einfach'
|
||||||
extClass = '.Einfach'
|
extClass = '.Einfach'
|
||||||
extVersionCode = 18
|
extVersionCode = 16
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
ext {
|
ext {
|
||||||
extName = 'FilmPalast'
|
extName = 'FilmPalast'
|
||||||
extClass = '.FilmPalast'
|
extClass = '.FilmPalast'
|
||||||
extVersionCode = 20
|
extVersionCode = 19
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
|
|
@ -3,7 +3,7 @@ ext {
|
||||||
extClass = '.Kinoking'
|
extClass = '.Kinoking'
|
||||||
themePkg = 'dooplay'
|
themePkg = 'dooplay'
|
||||||
baseUrl = 'https://kinoking.cc'
|
baseUrl = 'https://kinoking.cc'
|
||||||
overrideVersionCode = 24
|
overrideVersionCode = 23
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
ext {
|
ext {
|
||||||
extName = 'Kool'
|
extName = 'Kool'
|
||||||
extClass = '.Kool'
|
extClass = '.Kool'
|
||||||
extVersionCode = 15
|
extVersionCode = 13
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
ext {
|
ext {
|
||||||
extName = 'Moflix-Stream'
|
extName = 'Moflix-Stream'
|
||||||
extClass = '.MoflixStream'
|
extClass = '.MoflixStream'
|
||||||
extVersionCode = 17
|
extVersionCode = 15
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
ext {
|
ext {
|
||||||
extName = 'Movie4k'
|
extName = 'Movie4k'
|
||||||
extClass = '.Movie4k'
|
extClass = '.Movie4k'
|
||||||
extVersionCode = 12
|
extVersionCode = 11
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
ext {
|
ext {
|
||||||
extName = 'Serienstream'
|
extName = 'Serienstream'
|
||||||
extClass = '.Serienstream'
|
extClass = '.Serienstream'
|
||||||
extVersionCode = 25
|
extVersionCode = 24
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
ext {
|
ext {
|
||||||
extName = 'AllAnime'
|
extName = 'AllAnime'
|
||||||
extClass = '.AllAnime'
|
extClass = '.AllAnime'
|
||||||
extVersionCode = 37
|
extVersionCode = 35
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
ext {
|
ext {
|
||||||
extName = 'AllAnimeChi'
|
extName = 'AllAnimeChi'
|
||||||
extClass = '.AllAnimeChi'
|
extClass = '.AllAnimeChi'
|
||||||
extVersionCode = 14
|
extVersionCode = 12
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
|
|
@ -2,8 +2,8 @@ ext {
|
||||||
extName = 'AnimeKhor'
|
extName = 'AnimeKhor'
|
||||||
extClass = '.AnimeKhor'
|
extClass = '.AnimeKhor'
|
||||||
themePkg = 'animestream'
|
themePkg = 'animestream'
|
||||||
baseUrl = 'https://animekhor.org'
|
baseUrl = 'https://animekhor.xyz'
|
||||||
overrideVersionCode = 9
|
overrideVersionCode = 6
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
|
|
@ -9,7 +9,7 @@ import eu.kanade.tachiyomi.multisrc.animestream.AnimeStream
|
||||||
class AnimeKhor : AnimeStream(
|
class AnimeKhor : AnimeStream(
|
||||||
"en",
|
"en",
|
||||||
"AnimeKhor",
|
"AnimeKhor",
|
||||||
"https://animekhor.org",
|
"https://animekhor.xyz",
|
||||||
) {
|
) {
|
||||||
// ============================ Video Links =============================
|
// ============================ Video Links =============================
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,7 @@ ext {
|
||||||
extClass = '.Animenosub'
|
extClass = '.Animenosub'
|
||||||
themePkg = 'animestream'
|
themePkg = 'animestream'
|
||||||
baseUrl = 'https://animenosub.com'
|
baseUrl = 'https://animenosub.com'
|
||||||
overrideVersionCode = 10
|
overrideVersionCode = 8
|
||||||
isNsfw = true
|
isNsfw = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -129,7 +129,7 @@ class AnimeOwl : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||||
val document = response.asJsoup()
|
val document = response.asJsoup()
|
||||||
val sub = document.select("#anime-cover-sub-content .episode-node").mapIndexed { idx, it ->
|
val sub = document.select("#anime-cover-sub-content .episode-node").mapIndexed { idx, it ->
|
||||||
EpisodeResponse.Episode(
|
EpisodeResponse.Episode(
|
||||||
id = it.attr("title").toDoubleOrNull(),
|
id = idx.toDouble(),
|
||||||
episodeIndex = idx.toString(),
|
episodeIndex = idx.toString(),
|
||||||
name = it.attr("title"),
|
name = it.attr("title"),
|
||||||
lang = "Sub",
|
lang = "Sub",
|
||||||
|
@ -138,7 +138,7 @@ class AnimeOwl : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||||
}
|
}
|
||||||
val dub = document.select("#anime-cover-dub-content .episode-node").mapIndexed { idx, it ->
|
val dub = document.select("#anime-cover-dub-content .episode-node").mapIndexed { idx, it ->
|
||||||
EpisodeResponse.Episode(
|
EpisodeResponse.Episode(
|
||||||
id = it.attr("title").toDoubleOrNull(),
|
id = idx.toDouble(),
|
||||||
episodeIndex = idx.toString(),
|
episodeIndex = idx.toString(),
|
||||||
name = it.attr("title"),
|
name = it.attr("title"),
|
||||||
lang = "Dub",
|
lang = "Dub",
|
||||||
|
@ -301,6 +301,6 @@ class AnimeOwl : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||||
private const val PREF_QUALITY_KEY = "preferred_quality"
|
private const val PREF_QUALITY_KEY = "preferred_quality"
|
||||||
private const val PREF_QUALITY_TITLE = "Preferred quality"
|
private const val PREF_QUALITY_TITLE = "Preferred quality"
|
||||||
private const val PREF_QUALITY_DEFAULT = "1080p"
|
private const val PREF_QUALITY_DEFAULT = "1080p"
|
||||||
private val PREF_QUALITY_LIST = arrayOf("1080p", "720p", "480p", "360p")
|
private val PREF_QUALITY_LIST = arrayOf("2K", "1080p", "720p", "480p", "360p")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -48,8 +48,13 @@ class OwlExtractor(private val client: OkHttpClient, private val baseUrl: String
|
||||||
luffy.forEach { stream ->
|
luffy.forEach { stream ->
|
||||||
noRedirectClient.newCall(GET("${stream.url}$jwt")).execute()
|
noRedirectClient.newCall(GET("${stream.url}$jwt")).execute()
|
||||||
.use { it.headers["Location"] }?.let {
|
.use { it.headers["Location"] }?.let {
|
||||||
|
val resolution = when {
|
||||||
|
stream.resolution?.endsWith("0") == true -> "${stream.resolution}p"
|
||||||
|
else -> stream.resolution
|
||||||
|
}
|
||||||
|
|
||||||
videoList.add(
|
videoList.add(
|
||||||
Video(it, "${link.lang} Luffy:${stream.resolution}", it),
|
Video(it, "${link.lang} Luffy:${resolution ?: "Unknown"}", it),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -83,7 +88,10 @@ class OwlExtractor(private val client: OkHttpClient, private val baseUrl: String
|
||||||
return client.newCall(GET(url)).execute().let { it ->
|
return client.newCall(GET(url)).execute().let { it ->
|
||||||
if (it.isSuccessful) {
|
if (it.isSuccessful) {
|
||||||
it.parseAs<Stream>().url.let {
|
it.parseAs<Stream>().url.let {
|
||||||
playlistUtils.extractFromHls(it, videoNameGen = { qty -> "$lang $server:$qty" })
|
playlistUtils.extractFromHls(
|
||||||
|
it,
|
||||||
|
videoNameGen = { qty -> "$lang $server:$qty" },
|
||||||
|
)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
emptyList()
|
emptyList()
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
ext {
|
ext {
|
||||||
extName = 'AnimePahe'
|
extName = 'AnimePahe'
|
||||||
extClass = '.AnimePahe'
|
extClass = '.AnimePahe'
|
||||||
extVersionCode = 31
|
extVersionCode = 29
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
|
|
@ -153,16 +153,7 @@ class AnimePahe : ConfigurableAnimeSource, AnimeHttpSource() {
|
||||||
override fun episodeListParse(response: Response): List<SEpisode> {
|
override fun episodeListParse(response: Response): List<SEpisode> {
|
||||||
val url = response.request.url.toString()
|
val url = response.request.url.toString()
|
||||||
val session = url.substringAfter("&id=").substringBefore("&")
|
val session = url.substringAfter("&id=").substringBefore("&")
|
||||||
val episodeList = recursivePages(response, session)
|
return recursivePages(response, session)
|
||||||
|
|
||||||
return episodeList
|
|
||||||
.sortedBy { it.date_upload } // Optional, makes sure it's in correct order
|
|
||||||
.mapIndexed { index, episode ->
|
|
||||||
episode.episode_number = (index + 1).toFloat()
|
|
||||||
episode.name = "Episode ${index + 1}"
|
|
||||||
episode
|
|
||||||
}
|
|
||||||
.reversed()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun parseEpisodePage(episodes: List<EpisodeDto>, animeSession: String): MutableList<SEpisode> {
|
private fun parseEpisodePage(episodes: List<EpisodeDto>, animeSession: String): MutableList<SEpisode> {
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
ext {
|
ext {
|
||||||
extName = 'AnimeTake'
|
extName = 'AnimeTake'
|
||||||
extClass = '.AnimeTake'
|
extClass = '.AnimeTake'
|
||||||
extVersionCode = 9
|
extVersionCode = 7
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
|
|
@ -2,7 +2,7 @@ ext {
|
||||||
extName = 'AniPlay'
|
extName = 'AniPlay'
|
||||||
extClass = '.AniPlay'
|
extClass = '.AniPlay'
|
||||||
themePkg = 'anilist'
|
themePkg = 'anilist'
|
||||||
overrideVersionCode = 20
|
overrideVersionCode = 15
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue