Compare commits
1 commit
main
...
Sphereso/v
Author | SHA1 | Date | |
---|---|---|---|
![]() |
d4d56a94a2 |
203 changed files with 837 additions and 22596 deletions
.github/workflows
README.mdSDK
NOTICE.txtadbetc1toolfastboothprof-conv
lib64
licenses
make_f2fsmake_f2fs_casefoldmke2fsmke2fs.confsource.propertiessqlite3lib-multisrc
lib
buzzheavier-extractor
chillx-extractor/src/main/java/eu/kanade/tachiyomi/lib/chillxextractor
filemoon-extractor/src/main/java/eu/kanade/tachiyomi/lib/filemoonextractor
playlist-utils/src/main/java/eu/kanade/tachiyomi/lib/playlistutils
savefile-extractor
streamwish-extractor/src/main/java/eu/kanade/tachiyomi/lib/streamwishextractor
universal-extractor/src/main/java/eu/kanade/tachiyomi/lib/universalextractor
voe-extractor/src/main/java/eu/kanade/tachiyomi/lib/voeextractor
src
all
animeworldindia
animexin
anizone
chineseanime
hikari
javgg
javguru
lmanime
missav
sudatchi
supjav
ar
anime4up
animeblkom
animelek
animerco
arabseed
asia2tv
cimaleek
egydead
faselhd
mycima
okanime
witanime
de
animebase
animeloads
animetoast
aniworld
cinemathek
einfach
filmpalast
kinoking
kool
moflixstream
movie4k
serienstream
en
allanime
allanimechi
animekhor
animenosub
animeowl
animepahe
animetake
aniplay
aniwatch
build.gradle
res
mipmap-hdpi
mipmap-mdpi
mipmap-xhdpi
mipmap-xxhdpi
mipmap-xxxhdpi
src/eu/kanade/tachiyomi/animeextension/en/aniwatch
asiaflix
donghuastream
dopebox
jpfilms
build.gradle
res
mipmap-hdpi
mipmap-mdpi
mipmap-xhdpi
mipmap-xxhdpi
mipmap-xxxhdpi
src/eu/kanade/tachiyomi/animeextension/en/jpfilms
25
.github/workflows/batch_close_issues.yml
vendored
Normal file
25
.github/workflows/batch_close_issues.yml
vendored
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
name: "Batch close stale issues"
|
||||||
|
|
||||||
|
on:
|
||||||
|
# Monthly
|
||||||
|
schedule:
|
||||||
|
- cron: '0 0 1 * *'
|
||||||
|
# Manual trigger
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
stale:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/stale@28ca1036281a5e5922ead5184a1bbf96e5fc984e # v9
|
||||||
|
with:
|
||||||
|
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
# Close everything older than ~6 months
|
||||||
|
days-before-issue-stale: 180
|
||||||
|
days-before-issue-close: 0
|
||||||
|
exempt-issue-labels: "do-not-autoclose,Meta request"
|
||||||
|
close-issue-message: "In an effort to have a more manageable issue backlog, we're closing older requests that weren't addressed since there's a low chance of it being addressed if it hasn't already. If your request is still relevant, please [open a new request](https://github.com/aniyomiorg/aniyomi-extensions/issues/new/choose)."
|
||||||
|
close-issue-reason: not_planned
|
||||||
|
ascending: true
|
||||||
|
operations-per-run: 250
|
26
.github/workflows/build_pull_request.yml
vendored
26
.github/workflows/build_pull_request.yml
vendored
|
@ -8,18 +8,25 @@ on:
|
||||||
- '!.github/**'
|
- '!.github/**'
|
||||||
- '.github/workflows/build_pull_request.yml'
|
- '.github/workflows/build_pull_request.yml'
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.event.pull_request.number }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
env:
|
env:
|
||||||
CI_CHUNK_SIZE: 65
|
CI_CHUNK_SIZE: 65
|
||||||
|
|
||||||
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: |
|
||||||
|
@ -30,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;
|
||||||
|
@ -45,26 +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 Android SDK
|
|
||||||
uses: https://github.com/android-actions/setup-android@00854ea68c109d98c75d956347303bf7c45b0277 # v3
|
|
||||||
with:
|
|
||||||
packages: ""
|
|
||||||
|
|
||||||
- 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/actions/wrapper-validation@v4
|
||||||
|
|
||||||
|
- name: Get number of modules
|
||||||
|
run: |
|
||||||
|
set -x
|
||||||
|
projects=(src/*/*)
|
||||||
|
|
||||||
|
echo "NUM_INDIVIDUAL_MODULES=${#projects[@]}" >> $GITHUB_ENV
|
||||||
|
|
||||||
- id: generate-matrices
|
- id: generate-matrices
|
||||||
name: Create output matrices
|
name: Create output matrices
|
||||||
uses: https://github.com/actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7
|
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7
|
||||||
with:
|
with:
|
||||||
script: |
|
script: |
|
||||||
const numIndividualModules = process.env.NUM_INDIVIDUAL_MODULES;
|
const numIndividualModules = process.env.NUM_INDIVIDUAL_MODULES;
|
||||||
|
@ -80,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
|
||||||
|
|
51
.github/workflows/issue_moderator.yml
vendored
Normal file
51
.github/workflows/issue_moderator.yml
vendored
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
name: Issue moderator
|
||||||
|
|
||||||
|
on:
|
||||||
|
issues:
|
||||||
|
types: [opened, edited, reopened]
|
||||||
|
issue_comment:
|
||||||
|
types: [created]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
autoclose:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Moderate issues
|
||||||
|
uses: aniyomiorg/issue-moderator-action@v2
|
||||||
|
with:
|
||||||
|
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
duplicate-label: Duplicate
|
||||||
|
|
||||||
|
duplicate-check-enabled: true
|
||||||
|
duplicate-check-labels: |
|
||||||
|
["Source request", "Domain changed"]
|
||||||
|
|
||||||
|
existing-check-enabled: true
|
||||||
|
existing-check-labels: |
|
||||||
|
["Source request", "Domain changed"]
|
||||||
|
|
||||||
|
auto-close-rules: |
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"type": "body",
|
||||||
|
"regex": ".*DELETE THIS SECTION IF YOU HAVE READ AND ACKNOWLEDGED IT.*",
|
||||||
|
"message": "The acknowledgment section was not removed."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "body",
|
||||||
|
"regex": ".*\\* (Aniyomi version|Android version|Device): \\?.*",
|
||||||
|
"message": "Requested information in the template was not filled out."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "title",
|
||||||
|
"regex": ".*(Source name|Short description).*",
|
||||||
|
"message": "You did not fill out the description in the title."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "both",
|
||||||
|
"regex": ".*(?:fail(?:ed|ure|s)?|can\\s*(?:no|')?t|(?:not|un).*able|(?<!n[o']?t )blocked by|error) (?:to )?(?:get past|by ?pass|penetrate)?.*cloud ?fl?are.*",
|
||||||
|
"ignoreCase": true,
|
||||||
|
"message": "Refer to the **Solving Cloudflare issues** section at https://aniyomi.org/help/guides/troubleshooting/#solving-cloudflare-issues. If it doesn't work, migrate to other sources or wait until they lower their protection."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
auto-close-ignore-label: do-not-autoclose
|
21
.github/workflows/lock.yml
vendored
Normal file
21
.github/workflows/lock.yml
vendored
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
name: Lock threads
|
||||||
|
|
||||||
|
on:
|
||||||
|
# Daily
|
||||||
|
schedule:
|
||||||
|
- cron: '0 0 * * *'
|
||||||
|
# Manual trigger
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
lock:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
issues: write
|
||||||
|
steps:
|
||||||
|
- uses: dessant/lock-threads@1bf7ec25051fe7c00bdd17e6a7cf3d7bfb7dc771 # v5
|
||||||
|
with:
|
||||||
|
github-token: ${{ github.token }}
|
||||||
|
issue-inactive-days: '2'
|
||||||
|
pr-inactive-days: '2'
|
10
README.md
10
README.md
|
@ -1,16 +1,10 @@
|
||||||
## Donation
|
|
||||||
|
|
||||||
Support this project by helping keep the servers up
|
|
||||||
|
|
||||||
[](https://ko-fi.com/T6T3124BZN)
|
|
||||||
|
|
||||||
## Guide
|
## Guide
|
||||||
|
|
||||||
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 = 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 = 5
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
api(project(":lib:megacloud-extractor"))
|
api(project(":lib:megacloud-extractor"))
|
||||||
|
|
|
@ -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"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,6 +10,7 @@ import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import okhttp3.internal.commonEmptyHeaders
|
import okhttp3.internal.commonEmptyHeaders
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
import kotlin.math.abs
|
||||||
|
|
||||||
class PlaylistUtils(private val client: OkHttpClient, private val headers: Headers = commonEmptyHeaders) {
|
class PlaylistUtils(private val client: OkHttpClient, private val headers: Headers = commonEmptyHeaders) {
|
||||||
|
|
||||||
|
@ -137,7 +138,7 @@ class PlaylistUtils(private val client: OkHttpClient, private val headers: Heade
|
||||||
val resolution = it.substringAfter("RESOLUTION=")
|
val resolution = it.substringAfter("RESOLUTION=")
|
||||||
.substringBefore("\n")
|
.substringBefore("\n")
|
||||||
.substringAfter("x")
|
.substringAfter("x")
|
||||||
.substringBefore(",")
|
.substringBefore(",").let(::stnQuality)
|
||||||
|
|
||||||
val videoUrl = it.substringAfter("\n").substringBefore("\n").let { url ->
|
val videoUrl = it.substringAfter("\n").substringBefore("\n").let { url ->
|
||||||
getAbsoluteUrl(url, playlistUrl, masterUrlBasePath)?.trimEnd()
|
getAbsoluteUrl(url, playlistUrl, masterUrlBasePath)?.trimEnd()
|
||||||
|
@ -336,6 +337,13 @@ class PlaylistUtils(private val client: OkHttpClient, private val headers: Heade
|
||||||
|
|
||||||
// ============================= Utilities ==============================
|
// ============================= Utilities ==============================
|
||||||
|
|
||||||
|
private fun stnQuality(quality: String): String {
|
||||||
|
val intQuality = quality.trim().toInt()
|
||||||
|
val standardQualities = listOf(144, 240, 360, 480, 720, 1080)
|
||||||
|
val result = standardQualities.minByOrNull { abs(it - intQuality) } ?: quality
|
||||||
|
return "${result}p"
|
||||||
|
}
|
||||||
|
|
||||||
private fun cleanSubtitleData(matchResult: MatchResult): String {
|
private fun cleanSubtitleData(matchResult: MatchResult): String {
|
||||||
val lineCount = matchResult.groupValues[1].count { it == '\n' }
|
val lineCount = matchResult.groupValues[1].count { it == '\n' }
|
||||||
return "\n" + " \n".repeat(lineCount - 1)
|
return "\n" + " \n".repeat(lineCount - 1)
|
||||||
|
|
|
@ -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() }
|
||||||
|
|
|
@ -18,14 +18,33 @@ class VoeExtractor(private val client: OkHttpClient) {
|
||||||
|
|
||||||
private val playlistUtils by lazy { PlaylistUtils(clientDdos) }
|
private val playlistUtils by lazy { PlaylistUtils(clientDdos) }
|
||||||
|
|
||||||
private val linkRegex = "(http|https)://([\\w_-]+(?:\\.[\\w_-]+)+)([\\w.,@?^=%&:/~+#-]*[\\w@?^=%&/~+#-])".toRegex()
|
|
||||||
|
|
||||||
private val base64Regex = Regex("'.*'")
|
|
||||||
|
|
||||||
private val scriptBase64Regex = "(let|var)\\s+\\w+\\s*=\\s*'(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)';".toRegex()
|
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class VideoLinkDTO(val file: String)
|
data class VideoLinkDTO(val source: String)
|
||||||
|
|
||||||
|
private fun decodeVoeData(data: String): String {
|
||||||
|
val shifted = data.map { char ->
|
||||||
|
when (char) {
|
||||||
|
in 'A'..'Z' -> 'A' + (char - 'A' + 13).mod(26)
|
||||||
|
in 'a'..'z' -> 'a' + (char - 'a' + 13).mod(26)
|
||||||
|
else -> char
|
||||||
|
}
|
||||||
|
}.joinToString()
|
||||||
|
|
||||||
|
val junk = listOf("@$", "^^", "~@", "%?", "*~", "!!", "#&")
|
||||||
|
var result = shifted
|
||||||
|
for (part in junk) {
|
||||||
|
result = result.replace(part, "_")
|
||||||
|
}
|
||||||
|
val clean = result.replace("_", "")
|
||||||
|
|
||||||
|
val transformed = String(Base64.decode(clean, Base64.DEFAULT)).map {
|
||||||
|
(it.code - 3).toChar()
|
||||||
|
}.joinToString().reversed()
|
||||||
|
|
||||||
|
val decoded = String(Base64.decode(transformed, Base64.DEFAULT))
|
||||||
|
|
||||||
|
return json.decodeFromString<VideoLinkDTO>(decoded).source
|
||||||
|
}
|
||||||
|
|
||||||
fun videosFromUrl(url: String, prefix: String = ""): List<Video> {
|
fun videosFromUrl(url: String, prefix: String = ""): List<Video> {
|
||||||
var document = clientDdos.newCall(GET(url)).execute().asJsoup()
|
var document = clientDdos.newCall(GET(url)).execute().asJsoup()
|
||||||
|
@ -38,25 +57,12 @@ class VoeExtractor(private val client: OkHttpClient) {
|
||||||
document = clientDdos.newCall(GET(originalUrl)).execute().asJsoup()
|
document = clientDdos.newCall(GET(originalUrl)).execute().asJsoup()
|
||||||
}
|
}
|
||||||
|
|
||||||
val alternativeScript = document.select("script").find { scriptBase64Regex.containsMatchIn(it.data()) }?.data()
|
val encodedVoeData = document.select("script").find { it.data().contains("MKGMa=\"")}?.data()
|
||||||
val script = document.selectFirst("script:containsData(const sources), script:containsData(var sources), script:containsData(wc0)")?.data()
|
?.substringAfter("MKGMa=\"")
|
||||||
?: alternativeScript
|
?.substringBefore('"') ?: return emptyList()
|
||||||
?: return emptyList()
|
|
||||||
val playlistUrl = when {
|
val playlistUrl = decodeVoeData(encodedVoeData)
|
||||||
// Layout 1
|
|
||||||
script.contains("sources") -> {
|
|
||||||
val link = script.substringAfter("hls': '").substringBefore("'")
|
|
||||||
if (linkRegex.matches(link)) link else String(Base64.decode(link, Base64.DEFAULT))
|
|
||||||
}
|
|
||||||
// Layout 2
|
|
||||||
script.contains("wc0") || alternativeScript != null -> {
|
|
||||||
val base64 = base64Regex.find(script)!!.value
|
|
||||||
val decoded = Base64.decode(base64, Base64.DEFAULT).let(::String)
|
|
||||||
|
|
||||||
json.decodeFromString<VideoLinkDTO>(if (alternativeScript != null) decoded.reversed() else decoded).file
|
|
||||||
}
|
|
||||||
else -> return emptyList()
|
|
||||||
}
|
|
||||||
return playlistUtils.extractFromHls(playlistUrl,
|
return playlistUtils.extractFromHls(playlistUrl,
|
||||||
videoNameGen = { quality -> "${prefix}Voe:$quality" }
|
videoNameGen = { quality -> "${prefix}Voe:$quality" }
|
||||||
)
|
)
|
||||||
|
|
|
@ -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,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 = '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"
|
||||||
|
|
|
@ -3,7 +3,7 @@ ext {
|
||||||
extClass = '.AnimeKhor'
|
extClass = '.AnimeKhor'
|
||||||
themePkg = 'animestream'
|
themePkg = 'animestream'
|
||||||
baseUrl = 'https://animekhor.org'
|
baseUrl = 'https://animekhor.org'
|
||||||
overrideVersionCode = 9
|
overrideVersionCode = 7
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
ext {
|
ext {
|
||||||
extName = 'AnimeOwl'
|
extName = 'AnimeOwl'
|
||||||
extClass = '.AnimeOwl'
|
extClass = '.AnimeOwl'
|
||||||
extVersionCode = 24
|
extVersionCode = 23
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
ext {
|
ext {
|
||||||
extName = 'AnimePahe'
|
extName = 'AnimePahe'
|
||||||
extClass = '.AnimePahe'
|
extClass = '.AnimePahe'
|
||||||
extVersionCode = 31
|
extVersionCode = 30
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
|
|
@ -162,7 +162,6 @@ class AnimePahe : ConfigurableAnimeSource, AnimeHttpSource() {
|
||||||
episode.name = "Episode ${index + 1}"
|
episode.name = "Episode ${index + 1}"
|
||||||
episode
|
episode
|
||||||
}
|
}
|
||||||
.reversed()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun parseEpisodePage(episodes: List<EpisodeDto>, animeSession: String): MutableList<SEpisode> {
|
private fun parseEpisodePage(episodes: List<EpisodeDto>, animeSession: String): MutableList<SEpisode> {
|
||||||
|
|
|
@ -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 = 17
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
|
|
@ -285,8 +285,9 @@ class AniPlay : AniListAnimeHttpSource(), ConfigurableAnimeSource {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
when (serverName) {
|
when (serverName) {
|
||||||
"Yuki" -> {
|
// yuki
|
||||||
val url = "https://yukiproxy.aniplaynow.live/m3u8-proxy?url=${defaultSource.url}&headers={\"Referer\":\"https://megacloud.club/\"}"
|
PREF_SERVER_ENTRIES[1] -> {
|
||||||
|
val url = "https://yukiprox.aniplaynow.live/m3u8-proxy?url=${defaultSource.url}&headers={\"Referer\":\"https://megacloud.club/\"}"
|
||||||
return playlistUtils.extractFromHls(
|
return playlistUtils.extractFromHls(
|
||||||
playlistUrl = url,
|
playlistUrl = url,
|
||||||
videoNameGen = { quality -> "$serverName - $quality - $typeName" },
|
videoNameGen = { quality -> "$serverName - $quality - $typeName" },
|
||||||
|
@ -307,8 +308,9 @@ class AniPlay : AniListAnimeHttpSource(), ConfigurableAnimeSource {
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
"Pahe" -> {
|
// pahe
|
||||||
val url = "https://paheproxy.aniplaynow.live/proxy?url=${defaultSource.url}&headers={\"Referer\":\"https://kwik.si/\"}"
|
PREF_SERVER_ENTRIES[2] -> {
|
||||||
|
val url = "https://prox.aniplaynow.live/?url=${defaultSource.url}&ref=https://kwik.si"
|
||||||
val headers = headers.newBuilder().apply {
|
val headers = headers.newBuilder().apply {
|
||||||
set("Accept", "*/*")
|
set("Accept", "*/*")
|
||||||
set("Origin", baseUrl)
|
set("Origin", baseUrl)
|
||||||
|
@ -563,8 +565,8 @@ class AniPlay : AniListAnimeHttpSource(), ConfigurableAnimeSource {
|
||||||
private const val PREF_DOMAIN_DEFAULT = "aniplaynow.live"
|
private const val PREF_DOMAIN_DEFAULT = "aniplaynow.live"
|
||||||
|
|
||||||
private const val PREF_SERVER_KEY = "server"
|
private const val PREF_SERVER_KEY = "server"
|
||||||
private val PREF_SERVER_ENTRIES = arrayOf("Pahe", "Yuki") // , "Hika")
|
private val PREF_SERVER_ENTRIES = arrayOf("Maze", "Yuki", "Pahe", "Kuro")
|
||||||
private val PREF_SERVER_ENTRY_VALUES = arrayOf("pahe", "yuki") // , "hika")
|
private val PREF_SERVER_ENTRY_VALUES = arrayOf("maze", "yuki", "pahe", "kuro")
|
||||||
private const val PREF_SERVER_DEFAULT = "yuki"
|
private const val PREF_SERVER_DEFAULT = "yuki"
|
||||||
private const val SERVER_UNKNOWN = "Other"
|
private const val SERVER_UNKNOWN = "Other"
|
||||||
|
|
||||||
|
|
|
@ -1,9 +0,0 @@
|
||||||
ext {
|
|
||||||
extName = 'AniWatchtv'
|
|
||||||
extClass = '.AniWatchtv'
|
|
||||||
themePkg = 'zorotheme'
|
|
||||||
baseUrl = 'https://aniwatchtv.to'
|
|
||||||
overrideVersionCode = 1
|
|
||||||
}
|
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
|
Binary file not shown.
Before ![]() (image error) Size: 4 KiB |
Binary file not shown.
Before ![]() (image error) Size: 2.4 KiB |
Binary file not shown.
Before ![]() (image error) Size: 5.5 KiB |
Binary file not shown.
Before ![]() (image error) Size: 8.6 KiB |
Binary file not shown.
Before ![]() (image error) Size: 12 KiB |
|
@ -1,40 +0,0 @@
|
||||||
package eu.kanade.tachiyomi.animeextension.en.aniwatch
|
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.animesource.model.SAnime
|
|
||||||
import eu.kanade.tachiyomi.animesource.model.Video
|
|
||||||
import eu.kanade.tachiyomi.lib.megacloudextractor.MegaCloudExtractor
|
|
||||||
import eu.kanade.tachiyomi.multisrc.zorotheme.ZoroTheme
|
|
||||||
import eu.kanade.tachiyomi.network.GET
|
|
||||||
import okhttp3.Request
|
|
||||||
import org.jsoup.nodes.Element
|
|
||||||
|
|
||||||
class AniWatchtv : ZoroTheme(
|
|
||||||
"en",
|
|
||||||
"AniWatchtv",
|
|
||||||
"https://aniwatchtv.to",
|
|
||||||
hosterNames = listOf(
|
|
||||||
"VidSrc",
|
|
||||||
"MegaCloud",
|
|
||||||
),
|
|
||||||
) {
|
|
||||||
override val id = 8051984946387208343L
|
|
||||||
|
|
||||||
override val ajaxRoute = "/v2"
|
|
||||||
|
|
||||||
private val megaCloudExtractor by lazy { MegaCloudExtractor(client, headers, preferences) }
|
|
||||||
|
|
||||||
override fun latestUpdatesRequest(page: Int): Request = GET("$baseUrl/recently-updated?page=$page", docHeaders)
|
|
||||||
|
|
||||||
override fun popularAnimeFromElement(element: Element): SAnime {
|
|
||||||
return super.popularAnimeFromElement(element).apply {
|
|
||||||
url = url.substringBefore("?")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun extractVideo(server: VideoData): List<Video> {
|
|
||||||
return when (server.name) {
|
|
||||||
"VidSrc", "MegaCloud" -> megaCloudExtractor.getVideosFromUrl(server.link, server.type, server.name)
|
|
||||||
else -> emptyList()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,7 +1,7 @@
|
||||||
ext {
|
ext {
|
||||||
extName = 'AsiaFlix'
|
extName = 'AsiaFlix'
|
||||||
extClass = '.AsiaFlix'
|
extClass = '.AsiaFlix'
|
||||||
extVersionCode = 18
|
extVersionCode = 16
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
|
|
@ -3,7 +3,7 @@ ext {
|
||||||
extClass = '.DonghuaStream'
|
extClass = '.DonghuaStream'
|
||||||
themePkg = 'animestream'
|
themePkg = 'animestream'
|
||||||
baseUrl = 'https://donghuastream.org'
|
baseUrl = 'https://donghuastream.org'
|
||||||
overrideVersionCode = 10
|
overrideVersionCode = 9
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
|
|
@ -3,7 +3,7 @@ ext {
|
||||||
extClass = '.DopeBox'
|
extClass = '.DopeBox'
|
||||||
themePkg = 'dopeflix'
|
themePkg = 'dopeflix'
|
||||||
baseUrl = 'https://dopebox.to'
|
baseUrl = 'https://dopebox.to'
|
||||||
overrideVersionCode = 12
|
overrideVersionCode = 11
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
|
@ -1,12 +0,0 @@
|
||||||
ext {
|
|
||||||
extName = 'JPFilms'
|
|
||||||
extClass = '.JPFilms'
|
|
||||||
extVersionCode = 1
|
|
||||||
isNsfw = true
|
|
||||||
}
|
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
|
||||||
|
|
||||||
dependencies {
|
|
||||||
implementation(project(":lib:playlist-utils"))
|
|
||||||
}
|
|
Binary file not shown.
Before ![]() (image error) Size: 6.8 KiB |
Binary file not shown.
Before ![]() (image error) Size: 3.6 KiB |
Binary file not shown.
Before ![]() (image error) Size: 10 KiB |
Binary file not shown.
Before ![]() (image error) Size: 20 KiB |
Binary file not shown.
Before ![]() (image error) Size: 31 KiB |
|
@ -1,406 +0,0 @@
|
||||||
package eu.kanade.tachiyomi.animeextension.en.jpfilms
|
|
||||||
|
|
||||||
import android.app.Application
|
|
||||||
import android.content.SharedPreferences
|
|
||||||
import android.util.Log
|
|
||||||
import androidx.preference.ListPreference
|
|
||||||
import androidx.preference.PreferenceScreen
|
|
||||||
import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource
|
|
||||||
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
|
|
||||||
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.lib.playlistutils.PlaylistUtils
|
|
||||||
import eu.kanade.tachiyomi.network.GET
|
|
||||||
import eu.kanade.tachiyomi.util.asJsoup
|
|
||||||
import kotlinx.serialization.SerialName
|
|
||||||
import kotlinx.serialization.Serializable
|
|
||||||
import kotlinx.serialization.json.Json
|
|
||||||
import okhttp3.Headers
|
|
||||||
import okhttp3.Request
|
|
||||||
import okhttp3.Response
|
|
||||||
import org.jsoup.nodes.Document
|
|
||||||
import org.jsoup.nodes.Element
|
|
||||||
import uy.kohesive.injekt.Injekt
|
|
||||||
import uy.kohesive.injekt.api.get
|
|
||||||
|
|
||||||
class JPFilms : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
|
||||||
override val name = "JPFilms"
|
|
||||||
override val baseUrl = "https://jp-films.com"
|
|
||||||
override val lang = "en"
|
|
||||||
override val supportsLatest = true
|
|
||||||
|
|
||||||
private val preferences: SharedPreferences by lazy {
|
|
||||||
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================== Popular Anime ==============================
|
|
||||||
override fun popularAnimeSelector(): String =
|
|
||||||
"div.item"
|
|
||||||
|
|
||||||
override fun popularAnimeRequest(page: Int): Request = GET("https://jp-films.com/wp-content/themes/halimmovies/halim-ajax.php?action=halim_get_popular_post&showpost=50&type=all")
|
|
||||||
|
|
||||||
override fun popularAnimeFromElement(element: Element): SAnime {
|
|
||||||
val anime = SAnime.create()
|
|
||||||
anime.setUrlWithoutDomain(element.select("a").attr("href"))
|
|
||||||
anime.title = element.select("h3.title").text()
|
|
||||||
anime.thumbnail_url = element.selectFirst("img")?.attr("abs:data-src")
|
|
||||||
Log.d("JPFilmsDebug", "Thumbnail URL: ${anime.thumbnail_url}")
|
|
||||||
return anime
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun popularAnimeNextPageSelector(): String? = null
|
|
||||||
|
|
||||||
// ============================== Latest Anime ==============================
|
|
||||||
override fun latestUpdatesSelector(): String =
|
|
||||||
"#ajax-vertical-widget-movie > div.item, " +
|
|
||||||
"#ajax-vertical-widget-tv_series > div.item"
|
|
||||||
|
|
||||||
override fun latestUpdatesRequest(page: Int): Request = GET(baseUrl)
|
|
||||||
|
|
||||||
override fun latestUpdatesFromElement(element: Element): SAnime {
|
|
||||||
val anime = SAnime.create()
|
|
||||||
anime.setUrlWithoutDomain(element.select("a").attr("href"))
|
|
||||||
anime.title = element.select("h3.title").text()
|
|
||||||
anime.thumbnail_url = element.select("img").attr("data-src")
|
|
||||||
Log.d("JPFilmsDebug", "Poster: ${anime.thumbnail_url}")
|
|
||||||
return anime
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun latestUpdatesNextPageSelector(): String? = null
|
|
||||||
|
|
||||||
// ============================== Search Anime ==============================
|
|
||||||
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
|
|
||||||
val searchQuery = query.replace(" ", "+")
|
|
||||||
return GET("$baseUrl/?s=$searchQuery", headers)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun searchAnimeSelector(): String = "#main-contents > section > div.halim_box > article"
|
|
||||||
|
|
||||||
override fun searchAnimeFromElement(element: Element): SAnime {
|
|
||||||
val anime = SAnime.create()
|
|
||||||
anime.setUrlWithoutDomain(element.select("a.halim-thumb").attr("href"))
|
|
||||||
anime.title = element.select("a.halim-thumb").attr("title")
|
|
||||||
anime.thumbnail_url = element.select("img").attr("data-src")
|
|
||||||
Log.d("JPFilmsDebug", "Poster: ${anime.thumbnail_url}")
|
|
||||||
return anime
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun searchAnimeNextPageSelector(): String? = null
|
|
||||||
|
|
||||||
// ============================== Anime Details ==============================
|
|
||||||
override suspend fun getAnimeDetails(anime: SAnime): SAnime {
|
|
||||||
val document = client.newCall(GET(baseUrl + anime.url, headers)).execute().asJsoup()
|
|
||||||
anime.title = document.select("h1.entry-title").text()
|
|
||||||
anime.genre = document.select("p.category a").joinToString(", ") { it.text() }
|
|
||||||
anime.description = document.select("#content > div > div.entry-content.htmlwrap.clearfix > div.video-item.halim-entry-box article p").text()
|
|
||||||
anime.thumbnail_url = document.select("#content > div > div.halim-movie-wrapper.tpl-2 > div > div.movie-poster.col-md-4 > img").attr("data-src")
|
|
||||||
anime.author = "forsyth47"
|
|
||||||
return anime
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun animeDetailsParse(document: Document): SAnime = throw UnsupportedOperationException()
|
|
||||||
|
|
||||||
// ============================== Episode List ==============================
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class JsonLdData(
|
|
||||||
@SerialName("@type") val type: String? = null,
|
|
||||||
)
|
|
||||||
|
|
||||||
override fun episodeListSelector(): String {
|
|
||||||
throw UnsupportedOperationException("Not used because we override episodeListParse.")
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun episodeListParse(response: Response): List<SEpisode> {
|
|
||||||
val document = response.asJsoup()
|
|
||||||
|
|
||||||
// Extract JSON-LD data to determine if it's a Movie or TVSeries
|
|
||||||
val jsonLdScript = document.selectFirst("script[type=application/ld+json]:not(.rank-math-schema)")?.data()
|
|
||||||
Log.d("JPFilmsDebug", "JSON-LD Script: $jsonLdScript")
|
|
||||||
|
|
||||||
val jsonLdData = json.decodeFromString<JsonLdData>(jsonLdScript ?: "{}")
|
|
||||||
Log.d("JPFilmsDebug", "JSON-LD Data: $jsonLdData")
|
|
||||||
|
|
||||||
val isMovie = jsonLdData.type == "Movie"
|
|
||||||
Log.d("JPFilmsDebug", "Type: ${if (isMovie) "Movie" else "TVSeries"}")
|
|
||||||
|
|
||||||
val serverAvailable = document.select("#halim-list-server > ul > li")
|
|
||||||
Log.d("JPFilmsDebug", "Server Available: $serverAvailable")
|
|
||||||
|
|
||||||
var freeServerFound: Boolean = false
|
|
||||||
val episodeContainerSelector = run {
|
|
||||||
freeServerFound = false
|
|
||||||
var selectedContainer: String? = null
|
|
||||||
|
|
||||||
// Iterate through each server div
|
|
||||||
for (serverDiv in serverAvailable) {
|
|
||||||
Log.d("JPFilmsDebug", "Server Div: $serverDiv")
|
|
||||||
|
|
||||||
// Log the title of the current server div
|
|
||||||
val title = serverDiv.select("li > a").text()
|
|
||||||
Log.d("JPFilmsDebug", "Server Div Title: $title")
|
|
||||||
|
|
||||||
// Check if the current server contains a <li> with a title containing "FREE"
|
|
||||||
val hasFreeServer = title.contains("FREE")
|
|
||||||
Log.d("JPFilmsDebug", "Has Free Server: $hasFreeServer")
|
|
||||||
|
|
||||||
if (hasFreeServer) {
|
|
||||||
// Mark that a FREE server was found
|
|
||||||
freeServerFound = true
|
|
||||||
Log.d("JPFilmsDebug", "FREE Server Found")
|
|
||||||
|
|
||||||
// Select this server's container
|
|
||||||
selectedContainer = "${serverDiv.select("a").attr("href")} > div > ul"
|
|
||||||
break // Exit the loop once a FREE server is found
|
|
||||||
} else if (!freeServerFound) {
|
|
||||||
// If no FREE server is found yet, select the first available server
|
|
||||||
selectedContainer = "${serverDiv.select("a").attr("href")} > div > ul"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return the selected container or an empty string if none is found
|
|
||||||
selectedContainer ?: ""
|
|
||||||
}
|
|
||||||
Log.d("JPFilmsDebug", "Episode Container Selector: $episodeContainerSelector")
|
|
||||||
|
|
||||||
// Extract all <li> elements from the selected container
|
|
||||||
val episodeElements = document.select("$episodeContainerSelector > li")
|
|
||||||
Log.d("JPFilmsDebug", "Episode Elements: $episodeElements")
|
|
||||||
|
|
||||||
return episodeElements.map { element ->
|
|
||||||
SEpisode.create().apply {
|
|
||||||
// Get the href attribute from either the anchor tag or the span tag
|
|
||||||
var href = if (element.select("a").hasAttr("href")) {
|
|
||||||
element.select("a").attr("href")
|
|
||||||
} else {
|
|
||||||
element.select("span").attr("data-href")
|
|
||||||
}
|
|
||||||
if (!freeServerFound) {
|
|
||||||
href = "$href?svid=2"
|
|
||||||
}
|
|
||||||
setUrlWithoutDomain(href)
|
|
||||||
Log.d("JPFilmsDebug", "Episode URL: $href")
|
|
||||||
|
|
||||||
// Determine if the episode belongs to a FREE or VIP server
|
|
||||||
val isFreeServer = element.select("a").attr("title").contains("FREE") ||
|
|
||||||
element.select("span").text().contains("FREE")
|
|
||||||
val serverPrefix = if (isFreeServer) "[FREE] " else "[VIP] "
|
|
||||||
|
|
||||||
// Use the title attribute of the anchor tag as the episode name
|
|
||||||
name = serverPrefix + (
|
|
||||||
element.select("a").attr("title").ifEmpty {
|
|
||||||
element.select("span").text()
|
|
||||||
}
|
|
||||||
)
|
|
||||||
Log.d("JPFilmsDebug", "Episode Name: $name")
|
|
||||||
|
|
||||||
// Generate an episode number based on the text content
|
|
||||||
episode_number = element.text()
|
|
||||||
.filter { it.isDigit() }
|
|
||||||
.toFloatOrNull() ?: 1F
|
|
||||||
Log.d("JPFilmsDebug", "Episode Number: $episode_number")
|
|
||||||
}
|
|
||||||
}.reversed()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun episodeFromElement(element: Element): SEpisode {
|
|
||||||
throw UnsupportedOperationException("Not used because we override episodeListParse.")
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================== Video List ==============================
|
|
||||||
|
|
||||||
// Define the JSON serializer
|
|
||||||
private val json = Json { ignoreUnknownKeys = true }
|
|
||||||
|
|
||||||
override fun videoListParse(response: Response): List<Video> {
|
|
||||||
Log.d("JPFilmsDebug", "Episode Chosen Response URL: ${response.request.url}")
|
|
||||||
val document = response.asJsoup()
|
|
||||||
val postId = extractPostId(document)
|
|
||||||
val episodeSlug = response.request.url.pathSegments.last().split("-").dropLast(1).joinToString("-")
|
|
||||||
|
|
||||||
// Debugging: Log episode slug and post ID
|
|
||||||
Log.d("JPFilmsDebug", "Episode Slug: $episodeSlug")
|
|
||||||
Log.d("JPFilmsDebug", "Post ID: $postId")
|
|
||||||
|
|
||||||
// Helper function to construct the player URL
|
|
||||||
fun getPlayerUrl(serverId: Int, subsvId: String? = null): String {
|
|
||||||
return "$baseUrl/wp-content/themes/halimmovies/player.php?" +
|
|
||||||
"episode_slug=$episodeSlug&" +
|
|
||||||
"server_id=$serverId&" +
|
|
||||||
(if (subsvId != null) "subsv_id=$subsvId&" else "") +
|
|
||||||
"post_id=$postId&" +
|
|
||||||
"nonce=8c934fd387&custom_var="
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create custom headers to match Postman
|
|
||||||
val customHeaders = Headers.Builder()
|
|
||||||
.add("sec-ch-ua-platform", "\"macOS\"")
|
|
||||||
.add("X-Requested-With", "XMLHttpRequest")
|
|
||||||
.add("User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36")
|
|
||||||
.add("Accept", "text/html, */*; q=0.01")
|
|
||||||
.add("sec-ch-ua", "\"Brave\";v=\"135\", \"Not-A.Brand\";v=\"8\", \"Chromium\";v=\"135\"")
|
|
||||||
.add("DNT", "1")
|
|
||||||
.add("sec-ch-ua-mobile", "?0")
|
|
||||||
.add("Sec-GPC", "1")
|
|
||||||
.add("Sec-Fetch-Site", "same-origin")
|
|
||||||
.add("Sec-Fetch-Mode", "cors")
|
|
||||||
.add("Sec-Fetch-Dest", "empty")
|
|
||||||
.add("Host", "jp-films.com")
|
|
||||||
.build()
|
|
||||||
|
|
||||||
// Helper function to fetch and parse the player response
|
|
||||||
fun fetchAndParsePlayerResponse(playerUrl: String): Pair<String, String> {
|
|
||||||
// Debugging: Log the constructed player URL
|
|
||||||
Log.d("JPFilmsDebug", "Player URL: $playerUrl")
|
|
||||||
|
|
||||||
// Make the request with custom headers
|
|
||||||
val playerResponse = client.newCall(GET(playerUrl, customHeaders)).execute().body.string()
|
|
||||||
|
|
||||||
// Debugging: Log the player response
|
|
||||||
Log.d("JPFilmsDebug", "Player Response: $playerResponse")
|
|
||||||
|
|
||||||
// Parse the JSON response into a strongly-typed structure
|
|
||||||
val jsonResponse = json.decodeFromString<PlayerResponse>(playerResponse)
|
|
||||||
|
|
||||||
// Extract the 'sources' field from the parsed JSON
|
|
||||||
val sources = jsonResponse.data?.sources ?: ""
|
|
||||||
|
|
||||||
// Debugging: Log the extracted sources
|
|
||||||
Log.d("JPFilmsDebug", "Extracted Sources: $sources")
|
|
||||||
|
|
||||||
// Extract the HLS URL using string manipulation
|
|
||||||
val hlsUrl = sources.split("source src=\"").getOrNull(1)?.split("\" type=")?.getOrNull(0) ?: ""
|
|
||||||
|
|
||||||
// Debugging: Log the extracted HLS URL
|
|
||||||
Log.d("JPFilmsDebug", "Extracted HLS URL: $hlsUrl")
|
|
||||||
|
|
||||||
return Pair(sources, hlsUrl)
|
|
||||||
}
|
|
||||||
|
|
||||||
val serverAvailable = document.select("#halim-list-server > ul > li")
|
|
||||||
Log.d("JPFilmsDebug", "Server Available: $serverAvailable")
|
|
||||||
|
|
||||||
val episodeContainerSelector = run {
|
|
||||||
var freeServerFound: Boolean = false
|
|
||||||
var selectedContainer: String? = null
|
|
||||||
|
|
||||||
// Iterate through each server div
|
|
||||||
for (serverDiv in serverAvailable) {
|
|
||||||
Log.d("JPFilmsDebug", "Server Div: $serverDiv")
|
|
||||||
|
|
||||||
// Log the title of the current server div
|
|
||||||
val title = serverDiv.select("li > a").text()
|
|
||||||
Log.d("JPFilmsDebug", "Server Div Title: $title")
|
|
||||||
|
|
||||||
// Check if the current server contains a <li> with a title containing "FREE"
|
|
||||||
val hasFreeServer = title.contains("FREE")
|
|
||||||
Log.d("JPFilmsDebug", "Has Free Server: $hasFreeServer")
|
|
||||||
|
|
||||||
if (hasFreeServer) {
|
|
||||||
// Mark that a FREE server was found
|
|
||||||
freeServerFound = true
|
|
||||||
Log.d("JPFilmsDebug", "FREE Server Found")
|
|
||||||
|
|
||||||
// Select this server's container
|
|
||||||
selectedContainer = "${serverDiv.select("a").attr("href")} > div > ul"
|
|
||||||
break // Exit the loop once a FREE server is found
|
|
||||||
} else if (!freeServerFound) {
|
|
||||||
// If no FREE server is found yet, select the first available server
|
|
||||||
selectedContainer = "${serverDiv.select("a").attr("href")} > div > ul"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return the selected container or an empty string if none is found
|
|
||||||
selectedContainer ?: ""
|
|
||||||
}
|
|
||||||
|
|
||||||
val episodeElements = document.select("$episodeContainerSelector > li")
|
|
||||||
Log.d("JPFilmsDebug", "Episode Elements: $episodeElements")
|
|
||||||
|
|
||||||
val targetEpisodeElement = episodeElements.firstOrNull { element ->
|
|
||||||
element.select("span").attr("data-episode-slug") == episodeSlug
|
|
||||||
} ?: run {
|
|
||||||
Log.e("JPFilmsDebug", "No matching episode element found for slug: $episodeSlug")
|
|
||||||
return emptyList() // Exit early if no matching element is found
|
|
||||||
}
|
|
||||||
|
|
||||||
// Extract the server ID from the target <li> element
|
|
||||||
val serverId = targetEpisodeElement.select("span").attr("data-server").toIntOrNull() ?: 0
|
|
||||||
|
|
||||||
// Debugging: Log the extracted server ID
|
|
||||||
Log.d("JPFilmsDebug", "Extracted Server ID: $serverId")
|
|
||||||
|
|
||||||
// First attempt with server_id=serverId and no subsvId
|
|
||||||
var subsvId: String? = null
|
|
||||||
val playerUrl1 = getPlayerUrl(serverId = serverId, subsvId = subsvId)
|
|
||||||
val (_, hlsUrl1) = fetchAndParsePlayerResponse(playerUrl1)
|
|
||||||
|
|
||||||
// Retry with subsvId=2 if the first attempt fails
|
|
||||||
val hlsUrl = if (hlsUrl1.isEmpty()) {
|
|
||||||
subsvId = "2"
|
|
||||||
val playerUrl2 = getPlayerUrl(serverId = serverId, subsvId = subsvId)
|
|
||||||
val (_, hlsUrl2) = fetchAndParsePlayerResponse(playerUrl2)
|
|
||||||
hlsUrl2
|
|
||||||
} else {
|
|
||||||
hlsUrl1
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return the video list if the HLS URL is found, otherwise return an empty list
|
|
||||||
return if (hlsUrl.isNotEmpty()) {
|
|
||||||
PlaylistUtils(client).extractFromHls(hlsUrl, referer = baseUrl)
|
|
||||||
} else {
|
|
||||||
emptyList()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Data classes for JSON parsing
|
|
||||||
@Serializable
|
|
||||||
data class PlayerResponse(
|
|
||||||
val data: PlayerData? = null,
|
|
||||||
)
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class PlayerData(
|
|
||||||
val status: Boolean? = null,
|
|
||||||
val sources: String? = null,
|
|
||||||
)
|
|
||||||
|
|
||||||
private fun extractPostId(document: Document): String {
|
|
||||||
val bodyClass = document.select("body").attr("class")
|
|
||||||
return Regex("postid-(\\d+)").find(bodyClass)?.groupValues?.get(1) ?: ""
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun videoListSelector(): String = throw UnsupportedOperationException()
|
|
||||||
|
|
||||||
override fun videoFromElement(element: Element): Video = throw UnsupportedOperationException()
|
|
||||||
|
|
||||||
override fun videoUrlParse(document: Document): String = throw UnsupportedOperationException()
|
|
||||||
|
|
||||||
// ============================== ToDo ==============================
|
|
||||||
// Plan to add option to change between original title and translated title
|
|
||||||
// Plan to add backup server too.
|
|
||||||
// ============================== Preferences ==============================
|
|
||||||
override fun setupPreferenceScreen(screen: PreferenceScreen) {
|
|
||||||
ListPreference(screen.context).apply {
|
|
||||||
key = Companion.PREF_TITLE_STYLE_KEY
|
|
||||||
title = "Preferred Title Style"
|
|
||||||
entries = arrayOf("Original", "Translated")
|
|
||||||
entryValues = arrayOf("original", "translated")
|
|
||||||
setDefaultValue("translated")
|
|
||||||
summary = "%s"
|
|
||||||
|
|
||||||
setOnPreferenceChangeListener { _, newValue ->
|
|
||||||
preferences.edit().putString(key, newValue as String).commit()
|
|
||||||
}
|
|
||||||
}.also(screen::addPreference)
|
|
||||||
}
|
|
||||||
|
|
||||||
private val SharedPreferences.titleStyle
|
|
||||||
get() = getString(Companion.PREF_TITLE_STYLE_KEY, "translated")!!
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
private const val PREF_TITLE_STYLE_KEY = "preferred_title_style"
|
|
||||||
}
|
|
||||||
}
|
|
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