forked from AlmightyHak/extensions-source
Compare commits
145 commits
khaled-0/p
...
main
Author | SHA1 | Date | |
---|---|---|---|
79fb9ab58f | |||
0d50b51a21 | |||
f1d629819f | |||
72a93f55be | |||
75970fa635 | |||
4d7f6b8ad4 | |||
7b045c9903 | |||
8c7a1722d5 | |||
b7826e016e | |||
7449e1eb74 | |||
03708116ff | |||
41f6097374 | |||
56b27d20eb | |||
ec65a3317f | |||
d9e55be893 | |||
b20a69b504 | |||
ae190cc371 | |||
ecfe891379 | |||
b29c4f9550 | |||
3c9526fb74 | |||
8c958b5d8f | |||
d0d8660b8b | |||
b172b25bb1 | |||
6b5807be97 | |||
0c6d703c39 | |||
92b54c9a55 | |||
3676197ea6 | |||
0d5077630d | |||
69dfe2b499 | |||
3b7284cbe5 | |||
ce3522ff69 | |||
0295eaef40 | |||
c96032233f | |||
a2f7b2a077 | |||
584c2e6b53 | |||
a10e63df38 | |||
09533654d5 | |||
ec77adae21 | |||
5167cba9bf | |||
31d422a8ca | |||
2d87503f99 | |||
8ef18b236a | |||
89c957d1ec | |||
a873046b39 | |||
015eb36372 | |||
3ffb365748 | |||
67672ea590 | |||
afccef1e7d | |||
4158a18ba6 | |||
3db3d04443 | |||
8738e281d8 | |||
808ad21843 | |||
7d7aca37d8 | |||
0c2df7e6af | |||
1cfee212d7 | |||
78b864fc50 | |||
8223b8d3fa | |||
2904ce54e9 | |||
d1a72a27c6 | |||
724c661a25 | |||
0c844c9649 | |||
1b9fabf062 | |||
0fc8a33f3e | |||
627c34f404 | |||
1de715339a | |||
f789a38449 | |||
4782731860 | |||
ed7858a223 | |||
51b027fd78 | |||
ea5dd44967 | |||
2cc07ffe89 | |||
4f86e01e36 | |||
46fe6d9c11 | |||
4f7456323d | |||
fc37330c4f | |||
14132789f4 | |||
1b8adc6b13 | |||
5d6c3fff93 | |||
db54be041e | |||
40570cf6ae | |||
8fd317320e | |||
9e592e90ab | |||
0bddcda190 | |||
f096796f42 | |||
709104f0b9 | |||
e029421762 | |||
a95087c637 | |||
49400c0ceb | |||
75c7bf2d22 | |||
fd8a3c4619 | |||
![]() |
38368f56ac | ||
![]() |
66ca8544d3 | ||
![]() |
edd2fba397 | ||
![]() |
c82b40bb41 | ||
![]() |
26dbc15b45 | ||
![]() |
82a8b81de2 | ||
![]() |
a4d3a117cf | ||
![]() |
dd28b05f01 | ||
![]() |
36a480cd46 | ||
![]() |
b93c551b30 | ||
![]() |
b490d21610 | ||
![]() |
5d902c3576 | ||
![]() |
2f53d6b581 | ||
![]() |
7b73c023a6 | ||
![]() |
821cbc1d59 | ||
![]() |
45cff438ce | ||
![]() |
0f2d3adc46 | ||
![]() |
f568df679e | ||
![]() |
f57e2ea5af | ||
![]() |
ac1938c1e4 | ||
![]() |
6461450cf5 | ||
![]() |
71c9d5d7ea | ||
![]() |
48dd054ccd | ||
![]() |
c59029cada | ||
![]() |
7779fa173e | ||
![]() |
e240125371 | ||
![]() |
f21489af9b | ||
![]() |
1b10911a8b | ||
![]() |
626ac52a05 | ||
![]() |
ed7be63bbc | ||
![]() |
334745ef2d | ||
![]() |
8ae130841e | ||
![]() |
1aa0084dd9 | ||
![]() |
49e5558f19 | ||
![]() |
160f5531dd | ||
![]() |
c4ff62639d | ||
![]() |
f9f86e46ae | ||
![]() |
2b1e2a14f1 | ||
![]() |
19127e1c07 | ||
![]() |
a3bb5a57bd | ||
![]() |
9e36c94090 | ||
![]() |
06b67ac120 | ||
![]() |
a099d6abcf | ||
![]() |
96cb8d34c9 | ||
![]() |
07b096361a | ||
![]() |
6a7137b1d0 | ||
![]() |
235c2b3f41 | ||
![]() |
686405f61f | ||
![]() |
6c38697f19 | ||
![]() |
37d08b9c8a | ||
![]() |
f2cd1223b8 | ||
![]() |
4df27e2211 | ||
![]() |
07b02f4489 | ||
![]() |
e1d6459e4f | ||
![]() |
8d9e763dc4 |
259 changed files with 23549 additions and 2798 deletions
.github/workflows
README.mdSDK
NOTICE.txtadbetc1toolfastboothprof-conv
lib64
licenses
make_f2fsmake_f2fs_casefoldmke2fsmke2fs.confsource.propertiessqlite3lib-multisrc
anilist
dooplay
zorotheme
lib
buzzheavier-extractor
chillx-extractor/src/main/java/eu/kanade/tachiyomi/lib/chillxextractor
filemoon-extractor/src/main/java/eu/kanade/tachiyomi/lib/filemoonextractor
lulu-extractor/src/main/java/eu/kanade/tachiyomi/lib/luluextractor
lycoris-extractor/src/main/java/eu/kanade/tachiyomi/lib/lycorisextractor
megacloud-extractor/src/main/java/eu/kanade/tachiyomi/lib/megacloudextractor
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
src
all
animeworldindia
animexin
anizone
chineseanime
hikari
javgg
javguru
lmanime
missav
newgrounds
build.gradle
res
mipmap-hdpi
mipmap-mdpi
mipmap-xhdpi
mipmap-xxhdpi
mipmap-xxxhdpi
src/eu/kanade/tachiyomi/animeextension/all/newgrounds
sudatchi
supjav
torrentioanime
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/src/eu/kanade/tachiyomi/animeextension/en/animeowl
animepahe
animetake
aniplay
25
.github/workflows/batch_close_issues.yml
vendored
25
.github/workflows/batch_close_issues.yml
vendored
|
@ -1,25 +0,0 @@
|
||||||
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,25 +8,18 @@ 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-latest
|
runs-on: ubuntu-22.04
|
||||||
outputs:
|
outputs:
|
||||||
individualMatrix: ${{ steps.generate-matrices.outputs.individualMatrix }}
|
individualMatrix: ${{ steps.generate-matrices.outputs.individualMatrix }}
|
||||||
steps:
|
steps:
|
||||||
- name: Clone repo
|
- name: Clone repo
|
||||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4
|
uses: https://github.com/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: |
|
||||||
|
@ -37,7 +30,7 @@ jobs:
|
||||||
|
|
||||||
- id: generate-matrices
|
- id: generate-matrices
|
||||||
name: Create output matrices
|
name: Create output matrices
|
||||||
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7
|
uses: https://github.com/actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7
|
||||||
with:
|
with:
|
||||||
script: |
|
script: |
|
||||||
const numIndividualModules = process.env.NUM_INDIVIDUAL_MODULES;
|
const numIndividualModules = process.env.NUM_INDIVIDUAL_MODULES;
|
||||||
|
@ -52,21 +45,26 @@ jobs:
|
||||||
build_individual:
|
build_individual:
|
||||||
name: Build individual modules
|
name: Build individual modules
|
||||||
needs: prepare
|
needs: prepare
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-22.04
|
||||||
strategy:
|
strategy:
|
||||||
matrix: ${{ fromJSON(needs.prepare.outputs.individualMatrix) }}
|
matrix: ${{ fromJSON(needs.prepare.outputs.individualMatrix) }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout PR
|
- name: Checkout PR
|
||||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4
|
uses: https://github.com/actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4
|
||||||
|
|
||||||
- name: Set up JDK
|
- name: Set up JDK
|
||||||
uses: actions/setup-java@387ac29b308b003ca37ba93a6cab5eb57c8f5f93 # v4
|
uses: https://github.com/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: gradle/actions/setup-gradle@417ae3ccd767c252f5661f1ace9f835f9654f2b5 # v3
|
uses: https://github.com/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,16 +21,23 @@ env:
|
||||||
jobs:
|
jobs:
|
||||||
prepare:
|
prepare:
|
||||||
name: Prepare job
|
name: Prepare job
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-22.04
|
||||||
outputs:
|
outputs:
|
||||||
individualMatrix: ${{ steps.generate-matrices.outputs.individualMatrix }}
|
individualMatrix: ${{ steps.generate-matrices.outputs.individualMatrix }}
|
||||||
steps:
|
steps:
|
||||||
- name: Clone repo
|
- name: Clone repo
|
||||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4
|
uses: https://github.com/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
|
||||||
|
@ -42,7 +49,7 @@ jobs:
|
||||||
# safe_output: false
|
# safe_output: false
|
||||||
|
|
||||||
- name: Import GPG key
|
- name: Import GPG key
|
||||||
uses: crazy-max/ghaction-import-gpg@v6 # v6.1.0
|
uses: https://github.com/crazy-max/ghaction-import-gpg@v6 # v6.1.0
|
||||||
with:
|
with:
|
||||||
gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }}
|
gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }}
|
||||||
passphrase: ${{ secrets.GPG_PASSPHRASE }}
|
passphrase: ${{ secrets.GPG_PASSPHRASE }}
|
||||||
|
@ -56,19 +63,9 @@ jobs:
|
||||||
# chmod +x ./.github/scripts/bump-versions.py
|
# chmod +x ./.github/scripts/bump-versions.py
|
||||||
# ./.github/scripts/bump-versions.py ${{ steps.modified-libs.outputs.all_changed_files }}
|
# ./.github/scripts/bump-versions.py ${{ steps.modified-libs.outputs.all_changed_files }}
|
||||||
|
|
||||||
- name: Validate Gradle Wrapper
|
|
||||||
uses: gradle/wrapper-validation-action@a494d935f4b56874c4a5a87d19af7afcf3a163d0 # v2
|
|
||||||
|
|
||||||
- name: Get number of modules
|
|
||||||
run: |
|
|
||||||
set -x
|
|
||||||
projects=(src/*/*)
|
|
||||||
|
|
||||||
echo "NUM_INDIVIDUAL_MODULES=${#projects[@]}" >> $GITHUB_ENV
|
|
||||||
|
|
||||||
- id: generate-matrices
|
- id: generate-matrices
|
||||||
name: Create output matrices
|
name: Create output matrices
|
||||||
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7
|
uses: https://github.com/actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7
|
||||||
with:
|
with:
|
||||||
script: |
|
script: |
|
||||||
const numIndividualModules = process.env.NUM_INDIVIDUAL_MODULES;
|
const numIndividualModules = process.env.NUM_INDIVIDUAL_MODULES;
|
||||||
|
@ -83,27 +80,32 @@ jobs:
|
||||||
build_individual:
|
build_individual:
|
||||||
name: Build individual modules
|
name: Build individual modules
|
||||||
needs: prepare
|
needs: prepare
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-22.04
|
||||||
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: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4
|
uses: https://github.com/actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4
|
||||||
with:
|
with:
|
||||||
ref: main
|
ref: main
|
||||||
|
|
||||||
- name: Set up JDK
|
- name: Set up JDK
|
||||||
uses: actions/setup-java@387ac29b308b003ca37ba93a6cab5eb57c8f5f93 # v4
|
uses: https://github.com/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: gradle/actions/setup-gradle@417ae3ccd767c252f5661f1ace9f835f9654f2b5 # v3
|
uses: https://github.com/gradle/actions/setup-gradle@245c8a24de79c0dbeabaf19ebcbbd3b2c36f278d # v4
|
||||||
|
|
||||||
- name: Build extensions (chunk ${{ matrix.chunk }})
|
- name: Build extensions (chunk ${{ matrix.chunk }})
|
||||||
env:
|
env:
|
||||||
|
@ -114,8 +116,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: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4
|
uses: https://code.forgejo.org/forgejo/upload-artifact@16871d9e8cfcf27ff31822cac382bbb5450f1e1e # v4-patch
|
||||||
if: "github.repository == 'Kohi-den/extensions-source'"
|
if: "github.repository == 'AlmightyHak/extensions-source'"
|
||||||
with:
|
with:
|
||||||
name: "individual-apks-${{ matrix.chunk }}"
|
name: "individual-apks-${{ matrix.chunk }}"
|
||||||
path: "**/*.apk"
|
path: "**/*.apk"
|
||||||
|
@ -128,22 +130,31 @@ jobs:
|
||||||
name: Publish repo
|
name: Publish repo
|
||||||
needs:
|
needs:
|
||||||
- build_individual
|
- build_individual
|
||||||
if: "github.repository == 'Kohi-den/extensions-source'"
|
if: "github.repository == 'AlmightyHak/extensions-source'"
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-22.04
|
||||||
steps:
|
steps:
|
||||||
|
- name: Setup rsync
|
||||||
|
run: |
|
||||||
|
sudo apt-get update && sudo apt install rsync -y
|
||||||
|
|
||||||
- name: Download APK artifacts
|
- name: Download APK artifacts
|
||||||
uses: actions/download-artifact@eaceaf801fd36c7dee90939fad912460b18a1ffe # v4
|
uses: https://code.forgejo.org/forgejo/download-artifact@d8d0a99033603453ad2255e58720b460a0555e1e # v4-patch
|
||||||
with:
|
with:
|
||||||
path: ~/apk-artifacts
|
path: ~/apk-artifacts
|
||||||
|
|
||||||
- name: Set up JDK
|
- name: Set up JDK
|
||||||
uses: actions/setup-java@387ac29b308b003ca37ba93a6cab5eb57c8f5f93 # v4
|
uses: https://github.com/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: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4
|
uses: https://github.com/actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4
|
||||||
with:
|
with:
|
||||||
ref: main
|
ref: main
|
||||||
path: main
|
path: main
|
||||||
|
@ -158,9 +169,9 @@ jobs:
|
||||||
python ./.github/scripts/create-repo.py
|
python ./.github/scripts/create-repo.py
|
||||||
|
|
||||||
- name: Checkout repo branch
|
- name: Checkout repo branch
|
||||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4
|
uses: https://github.com/actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4
|
||||||
with:
|
with:
|
||||||
repository: Kohi-den/extensions
|
repository: AlmightyHak/extensions
|
||||||
token: ${{ secrets.BOT_PAT }}
|
token: ${{ secrets.BOT_PAT }}
|
||||||
ref: main
|
ref: main
|
||||||
path: repo
|
path: repo
|
||||||
|
@ -170,9 +181,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: EndBug/add-and-commit@v9
|
uses: https://github.com/EndBug/add-and-commit@v9
|
||||||
with:
|
with:
|
||||||
message: "Update extensions repo"
|
message: "Update extensions repo"
|
||||||
cwd: "./repo"
|
cwd: "./repo"
|
||||||
committer_name: Kohi-den-Bot
|
committer_name: AlmightyHak
|
||||||
committer_email: 177773202+Kohi-den-Bot@users.noreply.github.com
|
committer_email: almightyhak@noreply.localhost
|
||||||
|
|
51
.github/workflows/issue_moderator.yml
vendored
51
.github/workflows/issue_moderator.yml
vendored
|
@ -1,51 +0,0 @@
|
||||||
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
21
.github/workflows/lock.yml
vendored
|
@ -1,21 +0,0 @@
|
||||||
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,10 +1,16 @@
|
||||||
|
## 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://raw.githubusercontent.com/Kohi-den/extensions/main/index.min.json
|
https://kohiden.xyz/AlmightyHak/extensions/raw/branch/main/index.min.json
|
||||||
```
|
```
|
||||||
If your interested in installing just the apks they can be found [Here](https://github.com/Kohi-den/extensions)
|
If your interested in installing just the apks they can be found [Here](https://kohiden.xyz/AlmightyHak/extensions/src/branch/main/apk)
|
||||||
|
|
||||||
## Support Server
|
## Support Server
|
||||||
|
|
||||||
|
|
21111
SDK/NOTICE.txt
Normal file
21111
SDK/NOTICE.txt
Normal file
File diff suppressed because it is too large
Load diff
BIN
SDK/adb
Normal file
BIN
SDK/adb
Normal file
Binary file not shown.
BIN
SDK/etc1tool
Normal file
BIN
SDK/etc1tool
Normal file
Binary file not shown.
BIN
SDK/fastboot
Normal file
BIN
SDK/fastboot
Normal file
Binary file not shown.
BIN
SDK/hprof-conv
Normal file
BIN
SDK/hprof-conv
Normal file
Binary file not shown.
BIN
SDK/lib64/libc++.so
Normal file
BIN
SDK/lib64/libc++.so
Normal file
Binary file not shown.
2
SDK/licenses/android-sdk-license
Normal file
2
SDK/licenses/android-sdk-license
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
|
||||||
|
24333f8a63b6825ea9c5514f83c2829b004d1fee
|
BIN
SDK/make_f2fs
Normal file
BIN
SDK/make_f2fs
Normal file
Binary file not shown.
BIN
SDK/make_f2fs_casefold
Normal file
BIN
SDK/make_f2fs_casefold
Normal file
Binary file not shown.
BIN
SDK/mke2fs
Normal file
BIN
SDK/mke2fs
Normal file
Binary file not shown.
53
SDK/mke2fs.conf
Normal file
53
SDK/mke2fs.conf
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
[defaults]
|
||||||
|
base_features = sparse_super,large_file,filetype,dir_index,ext_attr
|
||||||
|
default_mntopts = acl,user_xattr
|
||||||
|
enable_periodic_fsck = 0
|
||||||
|
blocksize = 4096
|
||||||
|
inode_size = 256
|
||||||
|
inode_ratio = 16384
|
||||||
|
reserved_ratio = 1.0
|
||||||
|
|
||||||
|
[fs_types]
|
||||||
|
ext3 = {
|
||||||
|
features = has_journal
|
||||||
|
}
|
||||||
|
ext4 = {
|
||||||
|
features = has_journal,extent,huge_file,dir_nlink,extra_isize,uninit_bg
|
||||||
|
inode_size = 256
|
||||||
|
}
|
||||||
|
ext4dev = {
|
||||||
|
features = has_journal,extent,huge_file,flex_bg,inline_data,64bit,dir_nlink,extra_isize
|
||||||
|
inode_size = 256
|
||||||
|
options = test_fs=1
|
||||||
|
}
|
||||||
|
small = {
|
||||||
|
blocksize = 1024
|
||||||
|
inode_size = 128
|
||||||
|
inode_ratio = 4096
|
||||||
|
}
|
||||||
|
floppy = {
|
||||||
|
blocksize = 1024
|
||||||
|
inode_size = 128
|
||||||
|
inode_ratio = 8192
|
||||||
|
}
|
||||||
|
big = {
|
||||||
|
inode_ratio = 32768
|
||||||
|
}
|
||||||
|
huge = {
|
||||||
|
inode_ratio = 65536
|
||||||
|
}
|
||||||
|
news = {
|
||||||
|
inode_ratio = 4096
|
||||||
|
}
|
||||||
|
largefile = {
|
||||||
|
inode_ratio = 1048576
|
||||||
|
blocksize = -1
|
||||||
|
}
|
||||||
|
largefile4 = {
|
||||||
|
inode_ratio = 4194304
|
||||||
|
blocksize = -1
|
||||||
|
}
|
||||||
|
hurd = {
|
||||||
|
blocksize = 4096
|
||||||
|
inode_size = 128
|
||||||
|
}
|
2
SDK/source.properties
Normal file
2
SDK/source.properties
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
Pkg.UserSrc=false
|
||||||
|
Pkg.Revision=36.0.0
|
BIN
SDK/sqlite3
Normal file
BIN
SDK/sqlite3
Normal file
Binary file not shown.
|
@ -2,4 +2,4 @@ plugins {
|
||||||
id("lib-multisrc")
|
id("lib-multisrc")
|
||||||
}
|
}
|
||||||
|
|
||||||
baseVersionCode = 3
|
baseVersionCode = 4
|
||||||
|
|
|
@ -110,6 +110,8 @@ object AniListFilters {
|
||||||
|
|
||||||
val YEAR_LIST = arrayOf(
|
val YEAR_LIST = arrayOf(
|
||||||
Pair("<Select>", ""),
|
Pair("<Select>", ""),
|
||||||
|
Pair("2026", "2026"),
|
||||||
|
Pair("2025", "2025"),
|
||||||
Pair("2024", "2024"),
|
Pair("2024", "2024"),
|
||||||
Pair("2023", "2023"),
|
Pair("2023", "2023"),
|
||||||
Pair("2022", "2022"),
|
Pair("2022", "2022"),
|
||||||
|
|
|
@ -2,4 +2,4 @@ plugins {
|
||||||
id("lib-multisrc")
|
id("lib-multisrc")
|
||||||
}
|
}
|
||||||
|
|
||||||
baseVersionCode = 2
|
baseVersionCode = 3
|
||||||
|
|
|
@ -49,13 +49,13 @@ abstract class DooPlay(
|
||||||
const val PREFIX_SEARCH = "path:"
|
const val PREFIX_SEARCH = "path:"
|
||||||
}
|
}
|
||||||
|
|
||||||
protected open val prefQualityDefault = "720p"
|
protected open val prefQualityDefault = "1080p"
|
||||||
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("480p", "720p")
|
protected open val prefQualityValues = arrayOf("360p", "480p", "720p", "1080p")
|
||||||
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 = 4
|
baseVersionCode = 6
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
api(project(":lib:megacloud-extractor"))
|
api(project(":lib:megacloud-extractor"))
|
||||||
|
|
|
@ -183,7 +183,7 @@ object ZoroThemeFilters {
|
||||||
Pair("Most Watched", "most_watched"),
|
Pair("Most Watched", "most_watched"),
|
||||||
)
|
)
|
||||||
|
|
||||||
val YEARS = arrayOf(ALL) + (1917..2024).map {
|
val YEARS = arrayOf(ALL) + (1917..2025).map {
|
||||||
Pair(it.toString(), it.toString())
|
Pair(it.toString(), it.toString())
|
||||||
}.reversed().toTypedArray()
|
}.reversed().toTypedArray()
|
||||||
|
|
||||||
|
|
3
lib/buzzheavier-extractor/build.gradle.kts
Normal file
3
lib/buzzheavier-extractor/build.gradle.kts
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
plugins {
|
||||||
|
id("lib-android")
|
||||||
|
}
|
|
@ -0,0 +1,82 @@
|
||||||
|
package eu.kanade.tachiyomi.lib.buzzheavierextractor
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.animesource.model.Video
|
||||||
|
import eu.kanade.tachiyomi.network.GET
|
||||||
|
import eu.kanade.tachiyomi.util.asJsoup
|
||||||
|
import eu.kanade.tachiyomi.util.parseAs
|
||||||
|
import java.io.IOException
|
||||||
|
import kotlinx.serialization.ExperimentalSerializationApi
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import okhttp3.Headers
|
||||||
|
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
|
import okhttp3.Request
|
||||||
|
import okhttp3.Response
|
||||||
|
|
||||||
|
class BuzzheavierExtractor(
|
||||||
|
private val client: OkHttpClient,
|
||||||
|
private val headers: Headers,
|
||||||
|
) {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val SIZE_REGEX = Regex("""Size\s*-\s*([0-9.]+\s*[GMK]B)""")
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalSerializationApi::class)
|
||||||
|
fun videosFromUrl(url: String, prefix: String = "Buzzheavier - ", proxyUrl: String? = null): List<Video> {
|
||||||
|
val httpUrl = url.toHttpUrl()
|
||||||
|
val id = httpUrl.pathSegments.first()
|
||||||
|
|
||||||
|
val dlHeaders = headers.newBuilder().apply {
|
||||||
|
add("Accept", "*/*")
|
||||||
|
add("HX-Current-URL", url)
|
||||||
|
add("HX-Request", "true")
|
||||||
|
add("Priority", "u=1, i")
|
||||||
|
add("Referer", url)
|
||||||
|
}.build()
|
||||||
|
|
||||||
|
val videoHeaders = headers.newBuilder().apply {
|
||||||
|
add("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7")
|
||||||
|
add("Priority", "u=0, i")
|
||||||
|
add("Referer", url)
|
||||||
|
}.build()
|
||||||
|
|
||||||
|
val siteRequest = client.newCall(GET(url)).execute()
|
||||||
|
val parsedHtml = siteRequest.asJsoup()
|
||||||
|
val detailsText = parsedHtml.selectFirst("li:contains(Details:)")?.text() ?: ""
|
||||||
|
val size = SIZE_REGEX.find(detailsText)?.groupValues?.getOrNull(1)?.trim() ?: "Unknown"
|
||||||
|
|
||||||
|
val downloadRequest = GET("https://${httpUrl.host}/$id/download", dlHeaders)
|
||||||
|
val path = client.executeWithRetry(downloadRequest, 5, 204).use { response ->
|
||||||
|
response.header("hx-redirect").orEmpty()
|
||||||
|
}
|
||||||
|
|
||||||
|
val videoUrl = if (path.isNotEmpty()) {
|
||||||
|
if (path.startsWith("http")) path else "https://${httpUrl.host}$path"
|
||||||
|
} else if (proxyUrl?.isNotEmpty() == true) {
|
||||||
|
client.executeWithRetry(GET(proxyUrl + id), 5, 200).parseAs<UrlDto>().url
|
||||||
|
} else {
|
||||||
|
return emptyList()
|
||||||
|
}
|
||||||
|
|
||||||
|
return listOf(Video(videoUrl, "${prefix}${size}", videoUrl, videoHeaders))
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun OkHttpClient.executeWithRetry(request: Request, maxRetries: Int, validCode: Int): Response {
|
||||||
|
var response: Response? = null
|
||||||
|
for (attempt in 0 until maxRetries) {
|
||||||
|
response?.close()
|
||||||
|
response = this.newCall(request).execute()
|
||||||
|
if (response.code == validCode) {
|
||||||
|
return response
|
||||||
|
}
|
||||||
|
if (attempt < maxRetries - 1) {
|
||||||
|
Thread.sleep(1000)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return response ?: throw IOException("Failed to execute request after $maxRetries attempts")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class UrlDto(val url: String)
|
||||||
|
}
|
|
@ -1,73 +1,54 @@
|
||||||
package eu.kanade.tachiyomi.lib.chillxextractor
|
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, referer: String, prefix: String = "Chillx - "): List<Video> {
|
fun videoFromUrl(url: String, prefix: String = "Chillx - "): List<Video> {
|
||||||
val newHeaders = headers.newBuilder()
|
val data = webViewResolver.getDecryptedData(url) ?: return emptyList()
|
||||||
.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 body = client.newCall(GET(url, newHeaders)).execute().body.string()
|
val masterUrl = REGEX_SOURCES.find(data)?.groupValues?.get(1)
|
||||||
|
?: REGEX_FILE.find(data)?.groupValues?.get(1)
|
||||||
val master = REGEX_MASTER_JS.find(body)?.groupValues?.get(1) ?: return emptyList()
|
?: REGEX_SOURCE.find(data)?.groupValues?.get(1)
|
||||||
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(decryptedScript)
|
val subtitles = REGEX_SUBS.findAll(data)
|
||||||
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])))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return playlistUtils.extractFromHls(
|
val videoList = playlistUtils.extractFromHls(
|
||||||
playlistUrl = masterUrl,
|
playlistUrl = masterUrl,
|
||||||
referer = url,
|
referer = url,
|
||||||
videoNameGen = { "$prefix$it" },
|
videoNameGen = { "$prefix$it" },
|
||||||
subtitleList = subtitleList,
|
subtitleList = subtitleList,
|
||||||
)
|
)
|
||||||
}
|
|
||||||
|
|
||||||
@OptIn(ExperimentalSerializationApi::class)
|
return videoList.map {
|
||||||
private fun fetchKey(): String? {
|
Video(
|
||||||
return client.newCall(GET(KEY_SOURCE)).execute().parseAs<KeysData>().keys.firstOrNull()
|
url = it.url,
|
||||||
|
quality = it.quality,
|
||||||
|
videoUrl = it.videoUrl,
|
||||||
|
headers = it.headers,
|
||||||
|
audioTracks = it.audioTracks,
|
||||||
|
subtitleTracks = playlistUtils.fixSubtitles(it.subtitleTracks),
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun decodeUnicodeEscape(input: String): String {
|
private fun decodeUnicodeEscape(input: String): String {
|
||||||
|
@ -76,16 +57,4 @@ class ChillxExtractor(private val client: OkHttpClient, private val headers: Hea
|
||||||
it.groupValues[1].toInt(16).toChar().toString()
|
it.groupValues[1].toInt(16).toChar().toString()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class CryptoInfo(
|
|
||||||
@SerialName("ct") val ciphertext: String,
|
|
||||||
@SerialName("s") val salt: String,
|
|
||||||
)
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class KeysData(
|
|
||||||
@SerialName("chillx") val keys: List<String>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
class ErrorLoadingException(message: String) : Exception(message)
|
|
||||||
|
|
|
@ -0,0 +1,125 @@
|
||||||
|
package eu.kanade.tachiyomi.lib.chillxextractor
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.app.Application
|
||||||
|
import android.os.Handler
|
||||||
|
import android.os.Looper
|
||||||
|
import android.webkit.JavascriptInterface
|
||||||
|
import android.webkit.WebResourceRequest
|
||||||
|
import android.webkit.WebResourceResponse
|
||||||
|
import android.webkit.WebView
|
||||||
|
import android.webkit.WebViewClient
|
||||||
|
import eu.kanade.tachiyomi.network.GET
|
||||||
|
import eu.kanade.tachiyomi.util.asJsoup
|
||||||
|
import okhttp3.Headers
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
import java.io.ByteArrayInputStream
|
||||||
|
import java.util.concurrent.CountDownLatch
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
|
class WebViewResolver(
|
||||||
|
private val client: OkHttpClient,
|
||||||
|
private val globalHeaders: Headers,
|
||||||
|
) {
|
||||||
|
private val context: Application by injectLazy()
|
||||||
|
private val handler by lazy { Handler(Looper.getMainLooper()) }
|
||||||
|
|
||||||
|
class JsInterface(private val latch: CountDownLatch) {
|
||||||
|
var result: String? = null
|
||||||
|
|
||||||
|
@JavascriptInterface
|
||||||
|
fun passPayload(payload: String) {
|
||||||
|
result = payload
|
||||||
|
latch.countDown()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("SetJavaScriptEnabled")
|
||||||
|
fun getDecryptedData(embedUrl: String): String? {
|
||||||
|
val latch = CountDownLatch(1)
|
||||||
|
var webView: WebView? = null
|
||||||
|
val jsi = JsInterface(latch)
|
||||||
|
val interfaceName = randomString()
|
||||||
|
|
||||||
|
handler.post {
|
||||||
|
val webview = WebView(context)
|
||||||
|
webView = webview
|
||||||
|
|
||||||
|
with(webview.settings) {
|
||||||
|
javaScriptEnabled = true
|
||||||
|
domStorageEnabled = true
|
||||||
|
databaseEnabled = true
|
||||||
|
useWideViewPort = false
|
||||||
|
loadWithOverviewMode = false
|
||||||
|
userAgentString = globalHeaders["User-Agent"]
|
||||||
|
}
|
||||||
|
|
||||||
|
webview.addJavascriptInterface(jsi, interfaceName)
|
||||||
|
webview.webViewClient = object : WebViewClient() {
|
||||||
|
override fun shouldInterceptRequest(
|
||||||
|
view: WebView?,
|
||||||
|
request: WebResourceRequest?
|
||||||
|
): WebResourceResponse? {
|
||||||
|
if (request?.url.toString().equals(embedUrl, true)) {
|
||||||
|
return patchBody(request!!.url.toString(), interfaceName)
|
||||||
|
?: super.shouldInterceptRequest(view, request)
|
||||||
|
}
|
||||||
|
|
||||||
|
return super.shouldInterceptRequest(view, request)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
webView?.loadUrl(embedUrl)
|
||||||
|
}
|
||||||
|
|
||||||
|
latch.await(TIMEOUT_SEC, TimeUnit.SECONDS)
|
||||||
|
|
||||||
|
handler.post {
|
||||||
|
webView?.stopLoading()
|
||||||
|
webView?.destroy()
|
||||||
|
webView = null
|
||||||
|
}
|
||||||
|
|
||||||
|
return jsi.result
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val TIMEOUT_SEC: Long = 30
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun randomString(length: Int = 10): String {
|
||||||
|
val charPool = ('a'..'z') + ('A'..'Z')
|
||||||
|
return List(length) { charPool.random() }.joinToString("")
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun patchBody(url: String, interfaceName: String): WebResourceResponse? {
|
||||||
|
val html = client.newCall(GET(url, globalHeaders)).execute().asJsoup()
|
||||||
|
|
||||||
|
val oldFunc = randomString()
|
||||||
|
val script = html.createElement("script").apply {
|
||||||
|
appendText(
|
||||||
|
"""
|
||||||
|
const $oldFunc = Function;
|
||||||
|
window.Function = function (...args) {
|
||||||
|
if (args.length == 1) {
|
||||||
|
window.$interfaceName.passPayload(args[0]);
|
||||||
|
}
|
||||||
|
return $oldFunc(...args);
|
||||||
|
};
|
||||||
|
""".trimIndent()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
html.body().insertChildren(0, script)
|
||||||
|
|
||||||
|
return WebResourceResponse(
|
||||||
|
"text/html",
|
||||||
|
"utf-8",
|
||||||
|
200,
|
||||||
|
"ok",
|
||||||
|
mapOf("server" to "cloudflare"),
|
||||||
|
ByteArrayInputStream(html.outerHtml().toByteArray()),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,5 +1,8 @@
|
||||||
package eu.kanade.tachiyomi.lib.filemoonextractor
|
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
|
||||||
|
@ -13,20 +16,28 @@ import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
|
||||||
class FilemoonExtractor(private val client: OkHttpClient) {
|
class FilemoonExtractor(
|
||||||
|
private val client: OkHttpClient,
|
||||||
|
private val preferences: SharedPreferences? = null,
|
||||||
|
) {
|
||||||
|
|
||||||
private val playlistUtils by lazy { PlaylistUtils(client) }
|
private val 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> {
|
||||||
val httpUrl = url.toHttpUrl()
|
var 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()
|
val jsEval = doc.selectFirst("script:containsData(eval):containsData(m3u8)")?.data() ?: run {
|
||||||
|
val iframeUrl = doc.selectFirst("iframe[src]")!!.attr("src")
|
||||||
|
httpUrl = iframeUrl.toHttpUrl()
|
||||||
|
val iframeDoc = client.newCall(GET(iframeUrl, videoHeaders)).execute().asJsoup()
|
||||||
|
iframeDoc.selectFirst("script:containsData(eval):containsData(m3u8)")!!.data()
|
||||||
|
}
|
||||||
val unpacked = JsUnpacker.unpackAndCombine(jsEval).orEmpty()
|
val unpacked = JsUnpacker.unpackAndCombine(jsEval).orEmpty()
|
||||||
val masterUrl = unpacked.takeIf(String::isNotBlank)
|
val masterUrl = unpacked.takeIf(String::isNotBlank)
|
||||||
?.substringAfter("{file:\"", "")
|
?.substringAfter("{file:\"", "")
|
||||||
|
@ -50,14 +61,39 @@ class FilemoonExtractor(private val client: OkHttpClient) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return playlistUtils.extractFromHls(
|
val videoList = playlistUtils.extractFromHls(
|
||||||
masterUrl,
|
masterUrl,
|
||||||
subtitleList = subtitleTracks,
|
subtitleList = subtitleTracks,
|
||||||
referer = "https://${httpUrl.host}/",
|
referer = "https://${httpUrl.host}/",
|
||||||
videoNameGen = { "$prefix$it" },
|
videoNameGen = { "$prefix$it" },
|
||||||
)
|
)
|
||||||
|
|
||||||
|
val subPref = preferences?.getString(PREF_SUBTITLE_KEY, PREF_SUBTITLE_DEFAULT).orEmpty()
|
||||||
|
return videoList.map {
|
||||||
|
Video(
|
||||||
|
url = it.url,
|
||||||
|
quality = it.quality,
|
||||||
|
videoUrl = it.videoUrl,
|
||||||
|
audioTracks = it.audioTracks,
|
||||||
|
subtitleTracks = it.subtitleTracks.filter { tracks -> tracks.lang.contains(subPref, true) }
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class SubtitleDto(val file: String, val label: String)
|
data class SubtitleDto(val file: String, val label: String)
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun addSubtitlePref(screen: PreferenceScreen) {
|
||||||
|
EditTextPreference(screen.context).apply {
|
||||||
|
key = PREF_SUBTITLE_KEY
|
||||||
|
title = "Filemoon subtitle preference"
|
||||||
|
summary = "Leave blank to use all subs"
|
||||||
|
setDefaultValue(PREF_SUBTITLE_DEFAULT)
|
||||||
|
}.also(screen::addPreference)
|
||||||
|
}
|
||||||
|
|
||||||
|
private const val PREF_SUBTITLE_KEY = "pref_filemoon_sub_lang_key"
|
||||||
|
private const val PREF_SUBTITLE_DEFAULT = "eng"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,16 +3,18 @@ package eu.kanade.tachiyomi.lib.luluextractor
|
||||||
import eu.kanade.tachiyomi.animesource.model.Video
|
import eu.kanade.tachiyomi.animesource.model.Video
|
||||||
import eu.kanade.tachiyomi.network.GET
|
import eu.kanade.tachiyomi.network.GET
|
||||||
import okhttp3.Headers
|
import okhttp3.Headers
|
||||||
|
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import java.util.regex.Pattern
|
import java.util.regex.Pattern
|
||||||
|
|
||||||
class LuluExtractor(private val client: OkHttpClient) {
|
class LuluExtractor(private val client: OkHttpClient, headers: Headers) {
|
||||||
|
|
||||||
private val headers = Headers.Builder()
|
private val headers = headers.newBuilder()
|
||||||
.add("Referer", "https://luluvdo.com")
|
.add("Referer", "https://luluvdo.com/")
|
||||||
.add("Origin", "https://luluvdo.com")
|
.add("Origin", "https://luluvdo.com")
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
|
//Credit: https://github.com/skoruppa/docchi-stremio-addon/blob/main/app/players/lulustream.py
|
||||||
fun videosFromUrl(url: String, prefix: String): List<Video> {
|
fun videosFromUrl(url: String, prefix: String): List<Video> {
|
||||||
val videos = mutableListOf<Video>()
|
val videos = mutableListOf<Video>()
|
||||||
|
|
||||||
|
@ -22,7 +24,7 @@ class LuluExtractor(private val client: OkHttpClient) {
|
||||||
val fixedUrl = fixM3u8Link(m3u8Url)
|
val fixedUrl = fixM3u8Link(m3u8Url)
|
||||||
val quality = getResolution(fixedUrl)
|
val quality = getResolution(fixedUrl)
|
||||||
|
|
||||||
videos.add(Video(fixedUrl, "${prefix}Lulu - $quality", fixedUrl))
|
videos.add(Video(fixedUrl, "${prefix}Lulu - $quality", fixedUrl, headers))
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
}
|
}
|
||||||
|
@ -50,34 +52,41 @@ class LuluExtractor(private val client: OkHttpClient) {
|
||||||
|
|
||||||
private fun fixM3u8Link(link: String): String {
|
private fun fixM3u8Link(link: String): String {
|
||||||
val paramOrder = listOf("t", "s", "e", "f")
|
val paramOrder = listOf("t", "s", "e", "f")
|
||||||
val baseUrl = link.split("?").first()
|
val params = Pattern.compile("[?&]([^=]*)=([^&]*)").matcher(link).let { matcher ->
|
||||||
val params = link.split("?").getOrNull(1)?.split("&") ?: emptyList()
|
generateSequence { if (matcher.find()) matcher.group(1) to matcher.group(2) else null }.toList()
|
||||||
|
}
|
||||||
|
|
||||||
val paramMap = mutableMapOf<String, String>()
|
val paramDict = mutableMapOf<String, String>()
|
||||||
val extraParams = mutableMapOf(
|
val extraParams = mutableMapOf<String, String>()
|
||||||
"i" to "0.3",
|
|
||||||
"sp" to "0"
|
|
||||||
)
|
|
||||||
|
|
||||||
params.forEachIndexed { index, param ->
|
params.forEachIndexed { index, (key , value) ->
|
||||||
val parts = param.split("=")
|
if (key.isNullOrEmpty()) {
|
||||||
when {
|
if (index < paramOrder.size) {
|
||||||
parts.size == 2 -> {
|
if (value != null) {
|
||||||
val (key, value) = parts
|
paramDict[paramOrder[index]] = value
|
||||||
if (key in paramOrder) paramMap[key] = value
|
}
|
||||||
else extraParams[key] = value
|
}
|
||||||
|
} else {
|
||||||
|
if (value != null) {
|
||||||
|
extraParams[key] = value
|
||||||
}
|
}
|
||||||
index < paramOrder.size -> paramMap[paramOrder[index]] = parts.firstOrNull() ?: ""
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return buildString {
|
extraParams["i"] = "0.3"
|
||||||
append(baseUrl)
|
extraParams["sp"] = "0"
|
||||||
append("?")
|
|
||||||
append(paramOrder.joinToString("&") { "$it=${paramMap[it]}" })
|
val baseUrl = link.split("?")[0]
|
||||||
append("&")
|
|
||||||
append(extraParams.map { "${it.key}=${it.value}" }.joinToString("&"))
|
val fixedLink = baseUrl.toHttpUrl().newBuilder()
|
||||||
|
paramOrder.filter { paramDict.containsKey(it) }.forEach { key ->
|
||||||
|
fixedLink.addQueryParameter(key, paramDict[key])
|
||||||
}
|
}
|
||||||
|
extraParams.forEach { (key, value) ->
|
||||||
|
fixedLink.addQueryParameter(key, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
return fixedLink.build().toString()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getResolution(m3u8Url: String): String {
|
private fun getResolution(m3u8Url: String): String {
|
||||||
|
@ -98,11 +107,10 @@ class LuluExtractor(private val client: OkHttpClient) {
|
||||||
}
|
}
|
||||||
|
|
||||||
object JavaScriptUnpacker {
|
object JavaScriptUnpacker {
|
||||||
private val UNPACK_REGEX = Regex(
|
private val UNPACK_REGEX by lazy {
|
||||||
"""}\('(.*)', *(\d+), *(\d+), *'(.*?)'\.split\('\|'\)""",
|
Regex("""\}\('(.*)', *(\d+), *(\d+), *'(.*?)'\.split\('\|'\)""",
|
||||||
RegexOption.DOT_MATCHES_ALL
|
RegexOption.DOT_MATCHES_ALL)
|
||||||
)
|
}
|
||||||
|
|
||||||
fun unpack(encodedJs: String): String? {
|
fun unpack(encodedJs: String): String? {
|
||||||
val match = UNPACK_REGEX.find(encodedJs) ?: return null
|
val match = UNPACK_REGEX.find(encodedJs) ?: return null
|
||||||
val (payload, radixStr, countStr, symtabStr) = match.destructured
|
val (payload, radixStr, countStr, symtabStr) = match.destructured
|
||||||
|
@ -121,8 +129,8 @@ object JavaScriptUnpacker {
|
||||||
return Regex("""\b\w+\b""").replace(payload) { mr ->
|
return Regex("""\b\w+\b""").replace(payload) { mr ->
|
||||||
symtab.getOrNull(unbase(mr.value, radix, baseDict)) ?: mr.value
|
symtab.getOrNull(unbase(mr.value, radix, baseDict)) ?: mr.value
|
||||||
}.replace("\\", "")
|
}.replace("\\", "")
|
||||||
}
|
|
||||||
|
|
||||||
|
}
|
||||||
private fun unbase(value: String, radix: Int, dict: Map<Char, Int>): Int {
|
private fun unbase(value: String, radix: Int, dict: Map<Char, Int>): Int {
|
||||||
var result = 0
|
var result = 0
|
||||||
var multiplier = 1
|
var multiplier = 1
|
||||||
|
|
|
@ -4,13 +4,12 @@ import eu.kanade.tachiyomi.animesource.model.Video
|
||||||
import eu.kanade.tachiyomi.network.GET
|
import eu.kanade.tachiyomi.network.GET
|
||||||
import android.util.Base64
|
import android.util.Base64
|
||||||
import eu.kanade.tachiyomi.util.asJsoup
|
import eu.kanade.tachiyomi.util.asJsoup
|
||||||
import kotlinx.serialization.json.Json
|
import eu.kanade.tachiyomi.util.parseAs
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import okhttp3.Headers
|
import okhttp3.Headers
|
||||||
import okhttp3.HttpUrl
|
import okhttp3.HttpUrl
|
||||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import uy.kohesive.injekt.injectLazy
|
|
||||||
import java.nio.charset.Charset
|
import java.nio.charset.Charset
|
||||||
|
|
||||||
class LycorisCafeExtractor(private val client: OkHttpClient) {
|
class LycorisCafeExtractor(private val client: OkHttpClient) {
|
||||||
|
@ -19,7 +18,15 @@ class LycorisCafeExtractor(private val client: OkHttpClient) {
|
||||||
|
|
||||||
private val GETLNKURL = "https://www.lycoris.cafe/api/watch/getLink"
|
private val GETLNKURL = "https://www.lycoris.cafe/api/watch/getLink"
|
||||||
|
|
||||||
private val json: Json by injectLazy()
|
private val wordsRegex by lazy {
|
||||||
|
Regex(
|
||||||
|
"""\\U([0-9a-fA-F]{8})|""" + // \UXXXXXXXX
|
||||||
|
"""\\u([0-9a-fA-F]{4})|""" + // \uXXXX
|
||||||
|
"""\\x([0-9a-fA-F]{2})|""" + // \xHH
|
||||||
|
"""\\([0-7]{1,3})|""" + // \OOO (octal)
|
||||||
|
"""\\([btnfr"'$\\])""" // \n, \t, itd.
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// Credit: https://github.com/skoruppa/docchi-stremio-addon/blob/main/app/players/lycoris.py
|
// Credit: https://github.com/skoruppa/docchi-stremio-addon/blob/main/app/players/lycoris.py
|
||||||
fun getVideosFromUrl(url: String, headers: Headers, prefix: String): List<Video> {
|
fun getVideosFromUrl(url: String, headers: Headers, prefix: String): List<Video> {
|
||||||
|
@ -33,72 +40,63 @@ class LycorisCafeExtractor(private val client: OkHttpClient) {
|
||||||
GET(url, headers = embedHeaders),
|
GET(url, headers = embedHeaders),
|
||||||
).execute().asJsoup()
|
).execute().asJsoup()
|
||||||
|
|
||||||
val scripts = document.select("script")
|
val script = document.selectFirst("script[type='application/json']")?.data() ?: return emptyList()
|
||||||
|
|
||||||
val episodeDataPattern = Regex("episodeInfo\\s*:\\s*(\\{.*?\\}),", RegexOption.DOT_MATCHES_ALL)
|
val scriptData = script.parseAs<ScriptBody>()
|
||||||
var episodeData: String? = null
|
|
||||||
|
|
||||||
for (script in scripts) {
|
val data = scriptData.body.parseAs<ScriptEpisode>()
|
||||||
val content = script.data()
|
|
||||||
val match = episodeDataPattern.find(content)
|
|
||||||
|
|
||||||
if (match != null) {
|
val linkList = data.episodeInfo.id?.let {
|
||||||
episodeData = match.groupValues[1]
|
fetchAndDecodeVideo(client, data.episodeInfo.id.toString(), isSecondary = false)
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val result = mutableMapOf<String, String?>()
|
val fhdLink = data.episodeInfo.FHD?.let {
|
||||||
|
fetchAndDecodeVideo(client, data.episodeInfo.FHD, isSecondary = true)
|
||||||
val patterns = listOf(
|
}
|
||||||
"id" to Regex("id\\s*:\\s*(\\d+)"),
|
val sdLink = data.episodeInfo.SD?.let {
|
||||||
"FHD" to Regex("FHD\\s*:\\s*\"([^\"]+)\""),
|
fetchAndDecodeVideo(client, data.episodeInfo.SD, isSecondary = true)
|
||||||
"HD" to Regex("HD\\s*:\\s*\"([^\"]+)\""),
|
}
|
||||||
"SD" to Regex("SD\\s*:\\s*\"([^\"]+)\"")
|
val hdLink = data.episodeInfo.HD?.let {
|
||||||
)
|
fetchAndDecodeVideo(client, data.episodeInfo.HD, isSecondary = true)
|
||||||
|
|
||||||
patterns.forEach { (key, pattern) ->
|
|
||||||
result[key] = episodeData?.let { pattern.find(it)?.groups?.get(1)?.value }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var linkList: String? = fetchAndDecodeVideo(client, result["id"].toString(), isSecondary = false).toString()
|
|
||||||
|
|
||||||
val fhdLink = fetchAndDecodeVideo(client, result["FHD"].toString(), isSecondary = true).toString()
|
|
||||||
val sdLink = fetchAndDecodeVideo(client, result["SD"].toString(), isSecondary = true).toString()
|
|
||||||
val hdLink = fetchAndDecodeVideo(client, result["HD"].toString(), isSecondary = true).toString()
|
|
||||||
|
|
||||||
|
|
||||||
if (linkList.isNullOrBlank() || linkList == "{}") {
|
if (linkList.isNullOrBlank() || linkList == "{}") {
|
||||||
if (fhdLink.isNotEmpty()) {
|
if (!fhdLink.isNullOrBlank()) {
|
||||||
videos.add(Video(fhdLink, "${prefix}lycoris.cafe - 1080p", fhdLink))
|
videos.add(Video(fhdLink, "${prefix}lycoris.cafe - 1080p", fhdLink))
|
||||||
}
|
}
|
||||||
if (hdLink.isNotEmpty()) {
|
if (!hdLink.isNullOrBlank()) {
|
||||||
videos.add(Video(hdLink, "${prefix}lycoris.cafe - 720p", hdLink))
|
videos.add(Video(hdLink, "${prefix}lycoris.cafe - 720p", hdLink))
|
||||||
}
|
}
|
||||||
if (sdLink.isNotEmpty()) {
|
if (!sdLink.isNullOrBlank()) {
|
||||||
videos.add(Video(sdLink, "${prefix}lycoris.cafe - 480p", sdLink))
|
videos.add(Video(sdLink, "${prefix}lycoris.cafe - 480p", sdLink))
|
||||||
}
|
}
|
||||||
}else {
|
} else {
|
||||||
val videoLinks = Json.decodeFromString<VideoLinks>(linkList)
|
val videoLinks = linkList.parseAs<VideoLinksApi>()
|
||||||
|
|
||||||
videoLinks.FHD?.takeIf { checkLinks(client, it) }?.let {
|
videoLinks.FHD?.takeIf { checkLinks(client, it) }?.let {
|
||||||
videos.add(Video(it, "${prefix}lycoris.cafe - 1080p", it))
|
videos.add(Video(it, "${prefix}lycoris.cafe - 1080p", it))
|
||||||
}?: videos.add(Video(fhdLink, "${prefix}lycoris.cafe - 1080p", fhdLink))
|
} ?: fhdLink?.takeIf { checkLinks(client, it) }?.let {
|
||||||
|
videos.add(Video(it, "${prefix}lycoris.cafe - 1080p", it))
|
||||||
|
}
|
||||||
|
|
||||||
videoLinks.HD?.takeIf { checkLinks(client, it) }?.let {
|
videoLinks.HD?.takeIf { checkLinks(client, it) }?.let {
|
||||||
videos.add(Video(it, "${prefix}lycoris.cafe - 720p", it))
|
videos.add(Video(it, "${prefix}lycoris.cafe - 720p", it))
|
||||||
}?: videos.add(Video(hdLink, "${prefix}lycoris.cafe - 720p", hdLink))
|
} ?: hdLink?.takeIf { checkLinks(client, it) }?.let {
|
||||||
|
videos.add(Video(it, "${prefix}lycoris.cafe - 720p", it))
|
||||||
|
}
|
||||||
|
|
||||||
videoLinks.SD?.takeIf { checkLinks(client, it) }?.let {
|
videoLinks.SD?.takeIf { checkLinks(client, it) }?.let {
|
||||||
videos.add(Video(it, "${prefix}lycoris.cafe - 480p", it))
|
videos.add(Video(it, "${prefix}lycoris.cafe - 480p", it))
|
||||||
}?: videos.add(Video(sdLink, "${prefix}lycoris.cafe - 480p", sdLink))
|
} ?: sdLink?.takeIf { checkLinks(client, it) }?.let {
|
||||||
|
videos.add(Video(it, "${prefix}lycoris.cafe - 480p", it))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return videos
|
return videos
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun decodeVideoLinks(encodedUrl: String?): Any? {
|
private fun decodeVideoLinks(encodedUrl: String): String? {
|
||||||
if (encodedUrl.isNullOrEmpty()) {
|
if (encodedUrl.isBlank()) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -121,31 +119,33 @@ class LycorisCafeExtractor(private val client: OkHttpClient) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun fetchAndDecodeVideo(client: OkHttpClient, episodeId: String, isSecondary: Boolean = false): Any? {
|
private fun fetchAndDecodeVideo(client: OkHttpClient, episodeId: String, isSecondary: Boolean = false): String? {
|
||||||
val url: HttpUrl
|
val url: HttpUrl
|
||||||
|
|
||||||
if (isSecondary) {
|
if (isSecondary) {
|
||||||
val convertedText = episodeId.toByteArray(Charset.forName("UTF-8")).toString(Charset.forName("ISO-8859-1"))
|
val convertedText = episodeId.toByteArray(Charset.forName("UTF-8")).toString(Charset.forName("ISO-8859-1"))
|
||||||
val unicodeEscape = decodePythonEscape(convertedText)
|
val unicodeEscape = decodePythonEscape(convertedText)
|
||||||
val finalText = unicodeEscape.toByteArray(Charsets.ISO_8859_1).toString(Charsets.UTF_8)
|
val finalText = unicodeEscape.toByteArray(Charsets.ISO_8859_1).toString(Charsets.UTF_8)
|
||||||
|
|
||||||
url = GETLNKURL.toHttpUrl().newBuilder()
|
url = GETLNKURL.toHttpUrl().newBuilder()
|
||||||
?.addQueryParameter("link", finalText)
|
.addQueryParameter("link", finalText)
|
||||||
?.build() ?: throw IllegalStateException("Invalid URL")
|
.build()
|
||||||
} else {
|
} else {
|
||||||
url = GETSECONDARYURL.toHttpUrl().newBuilder()
|
url = GETSECONDARYURL.toHttpUrl().newBuilder()
|
||||||
?.addQueryParameter("id", episodeId)
|
.addQueryParameter("id", episodeId)
|
||||||
?.build() ?: throw IllegalStateException("Invalid URL")
|
.build()
|
||||||
|
}
|
||||||
|
client.newCall(GET(url))
|
||||||
|
.execute()
|
||||||
|
.use { response ->
|
||||||
|
val data = response.body.string()
|
||||||
|
return decodeVideoLinks(data)
|
||||||
}
|
}
|
||||||
client.newCall(GET(url))
|
|
||||||
.execute()
|
|
||||||
.use { response ->
|
|
||||||
val data = response.body.string() ?: ""
|
|
||||||
return decodeVideoLinks(data)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun checkLinks(client: OkHttpClient, link: String): Boolean {
|
private fun checkLinks(client: OkHttpClient, link: String): Boolean {
|
||||||
|
if (!link.contains("https://")) return false
|
||||||
|
|
||||||
client.newCall(GET(link)).execute().use { response ->
|
client.newCall(GET(link)).execute().use { response ->
|
||||||
return response.code.toString() == "200"
|
return response.code.toString() == "200"
|
||||||
}
|
}
|
||||||
|
@ -155,16 +155,7 @@ class LycorisCafeExtractor(private val client: OkHttpClient) {
|
||||||
// 1. Obsługa kontynuacji linii (backslash + newline)
|
// 1. Obsługa kontynuacji linii (backslash + newline)
|
||||||
val withoutLineContinuation = text.replace("\\\n", "")
|
val withoutLineContinuation = text.replace("\\\n", "")
|
||||||
|
|
||||||
// 2. Regex do wykrywania wszystkich sekwencji escape
|
return wordsRegex.replace(withoutLineContinuation) { match ->
|
||||||
val regex = Regex(
|
|
||||||
"""\\U([0-9a-fA-F]{8})|""" + // \UXXXXXXXX
|
|
||||||
"""\\u([0-9a-fA-F]{4})|""" + // \uXXXX
|
|
||||||
"""\\x([0-9a-fA-F]{2})|""" + // \xHH
|
|
||||||
"""\\([0-7]{1,3})|""" + // \OOO (octal)
|
|
||||||
"""\\([btnfr"'$\\\\])""" // \n, \t, itd.
|
|
||||||
)
|
|
||||||
|
|
||||||
return regex.replace(withoutLineContinuation) { match ->
|
|
||||||
val (u8, u4, x2, octal, simple) = match.destructured
|
val (u8, u4, x2, octal, simple) = match.destructured
|
||||||
when {
|
when {
|
||||||
u8.isNotEmpty() -> handleUnicode8(u8)
|
u8.isNotEmpty() -> handleUnicode8(u8)
|
||||||
|
@ -208,16 +199,32 @@ class LycorisCafeExtractor(private val client: OkHttpClient) {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class VideoLinks(
|
data class ScriptBody(
|
||||||
|
val body: String
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class ScriptEpisode(
|
||||||
|
val episodeInfo: EpisodeInfo
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class EpisodeInfo(
|
||||||
|
val id: Int? = null,
|
||||||
|
val FHD: String? = null,
|
||||||
|
val HD: String? = null,
|
||||||
|
val SD: String? = null,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class VideoLinksApi(
|
||||||
val HD: String? = null,
|
val HD: String? = null,
|
||||||
val SD: String? = null,
|
val SD: String? = null,
|
||||||
val FHD: String? = null,
|
val FHD: String? = null,
|
||||||
val Source: String? = null,
|
val Source: String? = null,
|
||||||
val preview: String? = null,
|
|
||||||
val SourceMKV: String? = null
|
val SourceMKV: String? = null
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -132,6 +132,7 @@ class MegaCloudExtractor(
|
||||||
?.filter { it.kind == "captions" }
|
?.filter { it.kind == "captions" }
|
||||||
?.map { Track(it.file, it.label) }
|
?.map { Track(it.file, it.label) }
|
||||||
.orEmpty()
|
.orEmpty()
|
||||||
|
.let { playlistUtils.fixSubtitles(it) }
|
||||||
return playlistUtils.extractFromHls(
|
return playlistUtils.extractFromHls(
|
||||||
masterUrl,
|
masterUrl,
|
||||||
videoNameGen = { "$name - $it - $type" },
|
videoNameGen = { "$name - $it - $type" },
|
||||||
|
@ -141,7 +142,7 @@ class MegaCloudExtractor(
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getVideoDto(url: String): VideoDto {
|
private fun getVideoDto(url: String): VideoDto {
|
||||||
val type = if (url.startsWith("https://megacloud.tv") or url.startsWith("https://megacloud.club")) 0 else 1
|
val type = if (url.startsWith("https://megacloud.tv") or url.startsWith("https://megacloud.blog")) 0 else 1
|
||||||
|
|
||||||
val keyType = SOURCES_KEY[type]
|
val keyType = SOURCES_KEY[type]
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
package eu.kanade.tachiyomi.lib.playlistutils
|
package eu.kanade.tachiyomi.lib.playlistutils
|
||||||
|
|
||||||
|
import android.net.Uri
|
||||||
import eu.kanade.tachiyomi.animesource.model.Track
|
import eu.kanade.tachiyomi.animesource.model.Track
|
||||||
import eu.kanade.tachiyomi.animesource.model.Video
|
import eu.kanade.tachiyomi.animesource.model.Video
|
||||||
import eu.kanade.tachiyomi.network.GET
|
import eu.kanade.tachiyomi.network.GET
|
||||||
|
@ -8,7 +9,7 @@ import okhttp3.Headers
|
||||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import okhttp3.internal.commonEmptyHeaders
|
import okhttp3.internal.commonEmptyHeaders
|
||||||
import kotlin.math.abs
|
import java.io.File
|
||||||
|
|
||||||
class PlaylistUtils(private val client: OkHttpClient, private val headers: Headers = commonEmptyHeaders) {
|
class PlaylistUtils(private val client: OkHttpClient, private val headers: Headers = commonEmptyHeaders) {
|
||||||
|
|
||||||
|
@ -136,7 +137,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(",").let(::stnQuality)
|
.substringBefore(",")
|
||||||
|
|
||||||
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()
|
||||||
|
@ -335,14 +336,32 @@ class PlaylistUtils(private val client: OkHttpClient, private val headers: Heade
|
||||||
|
|
||||||
// ============================= Utilities ==============================
|
// ============================= Utilities ==============================
|
||||||
|
|
||||||
private fun stnQuality(quality: String): String {
|
private fun cleanSubtitleData(matchResult: MatchResult): String {
|
||||||
val intQuality = quality.trim().toInt()
|
val lineCount = matchResult.groupValues[1].count { it == '\n' }
|
||||||
val standardQualities = listOf(144, 240, 360, 480, 720, 1080)
|
return "\n" + " \n".repeat(lineCount - 1)
|
||||||
val result = standardQualities.minByOrNull { abs(it - intQuality) } ?: quality
|
}
|
||||||
return "${result}p"
|
|
||||||
|
fun fixSubtitles(subtitleList: List<Track>): List<Track> {
|
||||||
|
return subtitleList.mapNotNull {
|
||||||
|
try {
|
||||||
|
val subData = client.newCall(GET(it.url)).execute().body.string()
|
||||||
|
|
||||||
|
val file = File.createTempFile("subs", "vtt")
|
||||||
|
.also(File::deleteOnExit)
|
||||||
|
|
||||||
|
file.writeText(FIX_SUBTITLE_REGEX.replace(subData, ::cleanSubtitleData))
|
||||||
|
val uri = Uri.fromFile(file)
|
||||||
|
|
||||||
|
Track(uri.toString(), it.lang)
|
||||||
|
} catch (_: Exception) {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
private val FIX_SUBTITLE_REGEX = Regex("""${'$'}(\n{2,})(?!(?:\d+:)*\d+(?:\.\d+)?\s-+>\s(?:\d+:)*\d+(?:\.\d+)?)""", RegexOption.MULTILINE)
|
||||||
|
|
||||||
private const val PLAYLIST_SEPARATOR = "#EXT-X-STREAM-INF:"
|
private const val PLAYLIST_SEPARATOR = "#EXT-X-STREAM-INF:"
|
||||||
|
|
||||||
private val SUBTITLE_REGEX by lazy { Regex("""#EXT-X-MEDIA:TYPE=SUBTITLES.*?NAME="(.*?)".*?URI="(.*?)"""") }
|
private val SUBTITLE_REGEX by lazy { Regex("""#EXT-X-MEDIA:TYPE=SUBTITLES.*?NAME="(.*?)".*?URI="(.*?)"""") }
|
||||||
|
|
7
lib/savefile-extractor/build.gradle.kts
Normal file
7
lib/savefile-extractor/build.gradle.kts
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
plugins {
|
||||||
|
id("lib-android")
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation(project(":lib:playlist-utils"))
|
||||||
|
}
|
|
@ -0,0 +1,67 @@
|
||||||
|
package eu.kanade.tachiyomi.lib.savefileextractor
|
||||||
|
|
||||||
|
import android.content.SharedPreferences
|
||||||
|
import androidx.preference.EditTextPreference
|
||||||
|
import androidx.preference.PreferenceScreen
|
||||||
|
import eu.kanade.tachiyomi.animesource.model.Video
|
||||||
|
import eu.kanade.tachiyomi.lib.playlistutils.PlaylistUtils
|
||||||
|
import eu.kanade.tachiyomi.network.GET
|
||||||
|
import eu.kanade.tachiyomi.util.asJsoup
|
||||||
|
import okhttp3.Headers
|
||||||
|
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
|
|
||||||
|
class SavefileExtractor(
|
||||||
|
private val client: OkHttpClient,
|
||||||
|
private val preferences: SharedPreferences,
|
||||||
|
) {
|
||||||
|
|
||||||
|
private val playlistUtils by lazy { PlaylistUtils(client) }
|
||||||
|
|
||||||
|
fun videosFromUrl(url: String, prefix: String = "Savefile - ", headers: Headers? = null): List<Video> {
|
||||||
|
val httpUrl = url.toHttpUrl()
|
||||||
|
val videoHeaders = (headers?.newBuilder() ?: Headers.Builder())
|
||||||
|
.set("Referer", url)
|
||||||
|
.set("Origin", "https://${httpUrl.host}")
|
||||||
|
.build()
|
||||||
|
|
||||||
|
val doc = client.newCall(GET(url, videoHeaders)).execute().asJsoup()
|
||||||
|
val js = doc.selectFirst("script:containsData(m3u8)")!!.data()
|
||||||
|
val masterUrl = js.takeIf(String::isNotBlank)
|
||||||
|
?.substringAfter("{file:\"", "")
|
||||||
|
?.substringBefore("\"}", "")
|
||||||
|
?.takeIf(String::isNotBlank)
|
||||||
|
?: return emptyList()
|
||||||
|
|
||||||
|
val videoList = playlistUtils.extractFromHls(
|
||||||
|
masterUrl,
|
||||||
|
referer = "https://${httpUrl.host}/",
|
||||||
|
videoNameGen = { "$prefix$it" },
|
||||||
|
)
|
||||||
|
|
||||||
|
val subPref = preferences.getString(PREF_SUBTITLE_KEY, PREF_SUBTITLE_DEFAULT).orEmpty()
|
||||||
|
return videoList.map {
|
||||||
|
Video(
|
||||||
|
url = it.url,
|
||||||
|
quality = it.quality,
|
||||||
|
videoUrl = it.videoUrl,
|
||||||
|
audioTracks = it.audioTracks,
|
||||||
|
subtitleTracks = it.subtitleTracks.filter { tracks -> tracks.lang.contains(subPref, true) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun addSubtitlePref(screen: PreferenceScreen) {
|
||||||
|
EditTextPreference(screen.context).apply {
|
||||||
|
key = PREF_SUBTITLE_KEY
|
||||||
|
title = "Savefile subtitle preference"
|
||||||
|
summary = "Leave blank to use all subs"
|
||||||
|
setDefaultValue(PREF_SUBTITLE_DEFAULT)
|
||||||
|
}.also(screen::addPreference)
|
||||||
|
}
|
||||||
|
|
||||||
|
private const val PREF_SUBTITLE_KEY = "pref_savefile_sub_lang_key"
|
||||||
|
private const val PREF_SUBTITLE_DEFAULT = "eng"
|
||||||
|
}
|
||||||
|
}
|
|
@ -10,6 +10,7 @@ import kotlinx.serialization.Serializable
|
||||||
import kotlinx.serialization.SerializationException
|
import kotlinx.serialization.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) {
|
||||||
|
@ -30,16 +31,19 @@ class StreamWishExtractor(private val client: OkHttpClient, private val headers:
|
||||||
script
|
script
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val masterUrl = scriptBody
|
val masterUrl = scriptBody?.let {
|
||||||
?.substringAfter("source", "")
|
M3U8_REGEX.find(it)?.value
|
||||||
?.substringAfter("file:\"", "")
|
}
|
||||||
?.substringBefore("\"", "")
|
|
||||||
?.takeIf(String::isNotBlank)
|
|
||||||
?: return emptyList()
|
?: return emptyList()
|
||||||
|
|
||||||
val subtitleList = extractSubtitles(scriptBody)
|
val subtitleList = extractSubtitles(scriptBody)
|
||||||
|
|
||||||
return playlistUtils.extractFromHls(masterUrl, url, videoNameGen = videoNameGen, subtitleList = subtitleList)
|
return playlistUtils.extractFromHls(
|
||||||
|
playlistUrl = masterUrl,
|
||||||
|
referer = "https://${url.toHttpUrl().host}/",
|
||||||
|
videoNameGen = videoNameGen,
|
||||||
|
subtitleList = playlistUtils.fixSubtitles(subtitleList),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getEmbedUrl(url: String): String {
|
private fun getEmbedUrl(url: String): String {
|
||||||
|
@ -57,7 +61,11 @@ class StreamWishExtractor(private val client: OkHttpClient, private val headers:
|
||||||
.substringAfter("tracks")
|
.substringAfter("tracks")
|
||||||
.substringAfter("[")
|
.substringAfter("[")
|
||||||
.substringBefore("]")
|
.substringBefore("]")
|
||||||
json.decodeFromString<List<TrackDto>>("[$subtitleStr]")
|
val fixedSubtitleStr = FIX_TRACKS_REGEX.replace(subtitleStr) { match ->
|
||||||
|
"\"${match.value}\""
|
||||||
|
}
|
||||||
|
|
||||||
|
json.decodeFromString<List<TrackDto>>("[$fixedSubtitleStr]")
|
||||||
.filter { it.kind.equals("captions", true) }
|
.filter { it.kind.equals("captions", true) }
|
||||||
.map { Track(it.file, it.label ?: "") }
|
.map { Track(it.file, it.label ?: "") }
|
||||||
} catch (e: SerializationException) {
|
} catch (e: SerializationException) {
|
||||||
|
@ -67,4 +75,7 @@ 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,6 +18,7 @@ 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()
|
||||||
|
@ -73,7 +74,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: $it $quality" + "p" })
|
val videos = playlistUtils.extractFromHls(modifiedUrl, origRequestUrl, videoNameGen = { "$prefix - $host: ${stnQuality(it)} $quality" + "p" })
|
||||||
|
|
||||||
if (videos.isNotEmpty()) {
|
if (videos.isNotEmpty()) {
|
||||||
allVideos.addAll(videos)
|
allVideos.addAll(videos)
|
||||||
|
@ -89,7 +90,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: $it" })
|
playlistUtils.extractFromHls(resultUrl, origRequestUrl, videoNameGen = { "$prefix - $host: ${stnQuality(it)}" })
|
||||||
}
|
}
|
||||||
"mpd" in resultUrl -> {
|
"mpd" in resultUrl -> {
|
||||||
Log.d("UniversalExtractor", "mpd URL: $resultUrl")
|
Log.d("UniversalExtractor", "mpd URL: $resultUrl")
|
||||||
|
@ -103,6 +104,13 @@ class UniversalExtractor(private val client: OkHttpClient) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun stnQuality(quality: String): String {
|
||||||
|
val intQuality = quality.trim().toInt()
|
||||||
|
val standardQualities = listOf(144, 240, 360, 480, 720, 1080)
|
||||||
|
val result = standardQualities.minByOrNull { abs(it - intQuality) } ?: quality
|
||||||
|
return "${result}p"
|
||||||
|
}
|
||||||
|
|
||||||
private fun String.proper(): String {
|
private fun String.proper(): String {
|
||||||
return this.replaceFirstChar { if (it.isLowerCase()) it.titlecase(
|
return this.replaceFirstChar { if (it.isLowerCase()) it.titlecase(
|
||||||
Locale.getDefault()) else it.toString() }
|
Locale.getDefault()) else it.toString() }
|
||||||
|
|
|
@ -19,8 +19,8 @@ if (System.getenv("CI") != "true") {
|
||||||
} else {
|
} else {
|
||||||
// Running in CI (GitHub Actions)
|
// Running in CI (GitHub Actions)
|
||||||
|
|
||||||
val chunkSize = System.getenv("CI_CHUNK_SIZE").toInt()
|
val chunkSize = System.getenv("CI_CHUNK_SIZE")?.toIntOrNull() ?: Int.MAX_VALUE
|
||||||
val chunk = System.getenv("CI_CHUNK_NUM").toInt()
|
val chunk = System.getenv("CI_CHUNK_NUM")?.toIntOrNull() ?: 0
|
||||||
|
|
||||||
// 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 = 15
|
extVersionCode = 16
|
||||||
}
|
}
|
||||||
|
|
||||||
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 = 10
|
overrideVersionCode = 11
|
||||||
}
|
}
|
||||||
|
|
||||||
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 = 3
|
extVersionCode = 4
|
||||||
}
|
}
|
||||||
|
|
||||||
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 = 13
|
overrideVersionCode = 15
|
||||||
}
|
}
|
||||||
|
|
||||||
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 = 16
|
extVersionCode = 22
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
@ -9,6 +9,7 @@ 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'))
|
|
||||||
}
|
}
|
|
@ -0,0 +1,118 @@
|
||||||
|
package eu.kanade.tachiyomi.animeextension.all.hikari
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.animesource.model.SAnime
|
||||||
|
import eu.kanade.tachiyomi.animesource.model.SEpisode
|
||||||
|
import kotlinx.serialization.SerialName
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class CatalogResponseDto<T>(
|
||||||
|
val next: String? = null,
|
||||||
|
val results: List<T>,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class AnimeDto(
|
||||||
|
val uid: String,
|
||||||
|
|
||||||
|
@SerialName("ani_ename")
|
||||||
|
val aniEName: String? = null,
|
||||||
|
@SerialName("ani_name")
|
||||||
|
val aniName: String,
|
||||||
|
@SerialName("ani_poster")
|
||||||
|
val aniPoster: String? = null,
|
||||||
|
@SerialName("ani_synopsis")
|
||||||
|
val aniSynopsis: String? = null,
|
||||||
|
@SerialName("ani_synonyms")
|
||||||
|
val aniSynonyms: String? = null,
|
||||||
|
@SerialName("ani_genre")
|
||||||
|
val aniGenre: String? = null,
|
||||||
|
@SerialName("ani_studio")
|
||||||
|
val aniStudio: String? = null,
|
||||||
|
@SerialName("ani_producers")
|
||||||
|
val aniProducers: String? = null,
|
||||||
|
@SerialName("ani_stats")
|
||||||
|
val aniStats: Int? = null,
|
||||||
|
@SerialName("ani_time")
|
||||||
|
val aniTime: String? = null,
|
||||||
|
@SerialName("ani_ep")
|
||||||
|
val aniEp: String? = null,
|
||||||
|
@SerialName("ani_type")
|
||||||
|
val aniType: Int? = null,
|
||||||
|
@SerialName("ani_score")
|
||||||
|
val aniScore: Double? = null,
|
||||||
|
) {
|
||||||
|
fun toSAnime(preferEnglish: Boolean): SAnime = SAnime.create().apply {
|
||||||
|
url = uid
|
||||||
|
title = if (preferEnglish) aniEName?.takeUnless(String::isBlank) ?: aniName else aniName
|
||||||
|
thumbnail_url = aniPoster
|
||||||
|
genre = aniGenre?.split(",")?.joinToString(transform = String::trim)
|
||||||
|
artist = aniStudio
|
||||||
|
author = aniProducers?.split(",")?.joinToString(transform = String::trim)
|
||||||
|
description = buildString {
|
||||||
|
aniScore?.let { append("Score: %.2f/10\n\n".format(it)) }
|
||||||
|
aniSynopsis?.trim()?.let(::append)
|
||||||
|
append("\n\n")
|
||||||
|
aniType?.let {
|
||||||
|
val type = when (it) {
|
||||||
|
1 -> "TV"
|
||||||
|
2 -> "Movie"
|
||||||
|
3 -> "OVA"
|
||||||
|
4 -> "ONA"
|
||||||
|
5 -> "Special"
|
||||||
|
else -> "Unknown"
|
||||||
|
}
|
||||||
|
append("Type: $type\n")
|
||||||
|
}
|
||||||
|
aniEp?.let { append("Total Episode count: $it\n") }
|
||||||
|
aniTime?.let { append("Runtime: $it\n") }
|
||||||
|
aniSynonyms?.let { append("Synonyms: $it") }
|
||||||
|
}.trim()
|
||||||
|
|
||||||
|
status = when (aniStats) {
|
||||||
|
1 -> SAnime.UNKNOWN
|
||||||
|
2 -> SAnime.COMPLETED
|
||||||
|
3 -> SAnime.ONGOING
|
||||||
|
else -> SAnime.UNKNOWN
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class LatestEpisodeDto(
|
||||||
|
val uid: Int,
|
||||||
|
val title: String,
|
||||||
|
@SerialName("title_en")
|
||||||
|
val titleEn: String? = null,
|
||||||
|
val imageUrl: String,
|
||||||
|
) {
|
||||||
|
fun toSAnime(preferEnglish: Boolean): SAnime = SAnime.create().apply {
|
||||||
|
url = uid.toString()
|
||||||
|
title = if (preferEnglish) titleEn?.takeUnless(String::isBlank) ?: this@LatestEpisodeDto.title else this@LatestEpisodeDto.title
|
||||||
|
thumbnail_url = imageUrl
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class EpisodeDto(
|
||||||
|
@SerialName("ep_id_name")
|
||||||
|
val epId: String,
|
||||||
|
@SerialName("ep_name")
|
||||||
|
val epName: String? = null,
|
||||||
|
) {
|
||||||
|
fun toSEpisode(uid: String): SEpisode = SEpisode.create().apply {
|
||||||
|
url = "$uid-$epId"
|
||||||
|
name = epName?.let { "Ep. $epId - $it" } ?: "Episode $epId"
|
||||||
|
episode_number = epId.toFloatOrNull() ?: 1f
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class EmbedDto(
|
||||||
|
@SerialName("embed_type")
|
||||||
|
val embedType: String,
|
||||||
|
@SerialName("embed_name")
|
||||||
|
val embedName: String,
|
||||||
|
@SerialName("embed_frame")
|
||||||
|
val embedFrame: String,
|
||||||
|
)
|
|
@ -5,7 +5,7 @@ import okhttp3.HttpUrl
|
||||||
import java.util.Calendar
|
import java.util.Calendar
|
||||||
|
|
||||||
interface UriFilter {
|
interface UriFilter {
|
||||||
fun addToUri(url: HttpUrl.Builder)
|
fun addToUri(builder: HttpUrl.Builder)
|
||||||
}
|
}
|
||||||
|
|
||||||
sealed class UriPartFilter(
|
sealed class UriPartFilter(
|
||||||
|
@ -20,7 +20,10 @@ sealed class UriPartFilter(
|
||||||
),
|
),
|
||||||
UriFilter {
|
UriFilter {
|
||||||
override fun addToUri(builder: HttpUrl.Builder) {
|
override fun addToUri(builder: HttpUrl.Builder) {
|
||||||
builder.addQueryParameter(param, vals[state].second)
|
val value = vals[state].second
|
||||||
|
if (value.isNotEmpty()) {
|
||||||
|
builder.addQueryParameter(param, value)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -33,13 +36,15 @@ sealed class UriMultiSelectFilter(
|
||||||
) : AnimeFilter.Group<UriMultiSelectOption>(name, vals.map { UriMultiSelectOption(it.first, it.second) }), UriFilter {
|
) : 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 }
|
||||||
builder.addQueryParameter(param, checked.joinToString(",") { it.value })
|
if (checked.isNotEmpty()) {
|
||||||
|
builder.addQueryParameter(param, checked.joinToString(",") { it.value })
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class TypeFilter : UriPartFilter(
|
class TypeFilter : UriPartFilter(
|
||||||
"Type",
|
"Type",
|
||||||
"type",
|
"ani_type",
|
||||||
arrayOf(
|
arrayOf(
|
||||||
Pair("All", ""),
|
Pair("All", ""),
|
||||||
Pair("TV", "1"),
|
Pair("TV", "1"),
|
||||||
|
@ -50,165 +55,53 @@ class TypeFilter : UriPartFilter(
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
class CountryFilter : UriPartFilter(
|
|
||||||
"Country",
|
|
||||||
"country",
|
|
||||||
arrayOf(
|
|
||||||
Pair("All", ""),
|
|
||||||
Pair("Japanese", "1"),
|
|
||||||
Pair("Chinese", "2"),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
class StatusFilter : UriPartFilter(
|
class StatusFilter : UriPartFilter(
|
||||||
"Status",
|
"Status",
|
||||||
"stats",
|
"ani_stats",
|
||||||
arrayOf(
|
arrayOf(
|
||||||
Pair("All", ""),
|
Pair("All", ""),
|
||||||
Pair("Currently Airing", "1"),
|
Pair("Ongoing", "1"),
|
||||||
Pair("Finished Airing", "2"),
|
Pair("Completed", "2"),
|
||||||
Pair("Not yet Aired", "3"),
|
Pair("Upcoming", "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",
|
||||||
"season",
|
"ani_release_season",
|
||||||
arrayOf(
|
arrayOf(
|
||||||
Pair("All", ""),
|
Pair("All", ""),
|
||||||
Pair("Spring", "1"),
|
Pair("Winter", "1"),
|
||||||
Pair("Summer", "2"),
|
Pair("Spring", "2"),
|
||||||
Pair("Fall", "3"),
|
Pair("Summer", "3"),
|
||||||
Pair("Winter", "4"),
|
Pair("Fall", "4"),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
class LanguageFilter : UriPartFilter(
|
class YearFilter : UriPartFilter(
|
||||||
"Language",
|
"Release Year",
|
||||||
"language",
|
"ani_release",
|
||||||
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 NEXT_YEAR by lazy {
|
private val CURRENT_YEAR by lazy {
|
||||||
Calendar.getInstance()[Calendar.YEAR] + 1
|
Calendar.getInstance()[Calendar.YEAR]
|
||||||
}
|
}
|
||||||
|
|
||||||
private val YEARS = Array(NEXT_YEAR - 1917) { year ->
|
private val YEARS = buildList {
|
||||||
if (year == 0) {
|
add(Pair("Any", ""))
|
||||||
Pair("Any", "")
|
addAll(
|
||||||
} else {
|
(1990..CURRENT_YEAR).map {
|
||||||
(NEXT_YEAR - year).toString().let { Pair(it, it) }
|
Pair(it.toString(), it.toString())
|
||||||
}
|
},
|
||||||
}
|
)
|
||||||
}
|
}.toTypedArray()
|
||||||
}
|
|
||||||
|
|
||||||
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"),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class GenreFilter : UriMultiSelectFilter(
|
class GenreFilter : UriMultiSelectFilter(
|
||||||
"Genre",
|
"Genre",
|
||||||
"genres",
|
"ani_genre",
|
||||||
arrayOf(
|
arrayOf(
|
||||||
Pair("Action", "Action"),
|
Pair("Action", "Action"),
|
||||||
Pair("Adventure", "Adventure"),
|
Pair("Adventure", "Adventure"),
|
||||||
|
@ -233,7 +126,7 @@ class GenreFilter : UriMultiSelectFilter(
|
||||||
Pair("Music", "Music"),
|
Pair("Music", "Music"),
|
||||||
Pair("Mystery", "Mystery"),
|
Pair("Mystery", "Mystery"),
|
||||||
Pair("Parody", "Parody"),
|
Pair("Parody", "Parody"),
|
||||||
Pair("Police", "Police"),
|
Pair("Policy", "Policy"),
|
||||||
Pair("Psychological", "Psychological"),
|
Pair("Psychological", "Psychological"),
|
||||||
Pair("Romance", "Romance"),
|
Pair("Romance", "Romance"),
|
||||||
Pair("Samurai", "Samurai"),
|
Pair("Samurai", "Samurai"),
|
||||||
|
@ -253,3 +146,12 @@ class GenreFilter : UriMultiSelectFilter(
|
||||||
Pair("Vampire", "Vampire"),
|
Pair("Vampire", "Vampire"),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
class LanguageFilter : UriPartFilter(
|
||||||
|
"Language",
|
||||||
|
"ani_genre",
|
||||||
|
arrayOf(
|
||||||
|
Pair("Any", ""),
|
||||||
|
Pair("Portuguese", "Portuguese"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
|
@ -1,48 +1,45 @@
|
||||||
package eu.kanade.tachiyomi.animeextension.all.hikari
|
package eu.kanade.tachiyomi.animeextension.all.hikari
|
||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import android.util.Log
|
import android.content.SharedPreferences
|
||||||
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.ParsedAnimeHttpSource
|
import eu.kanade.tachiyomi.animesource.online.AnimeHttpSource
|
||||||
|
import eu.kanade.tachiyomi.lib.buzzheavierextractor.BuzzheavierExtractor
|
||||||
import eu.kanade.tachiyomi.lib.chillxextractor.ChillxExtractor
|
import eu.kanade.tachiyomi.lib.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 : ParsedAnimeHttpSource(), ConfigurableAnimeSource {
|
class Hikari : AnimeHttpSource(), ConfigurableAnimeSource {
|
||||||
|
|
||||||
override val name = "Hikari"
|
override val name = "Hikari"
|
||||||
|
|
||||||
override val baseUrl = "https://watch.hikaritv.xyz"
|
private val proxyUrl = "https://hikari.gg/hiki-proxy/extract/"
|
||||||
|
private val apiUrl = "https://api.hikari.gg/api"
|
||||||
|
override val baseUrl = "https://hikari.gg"
|
||||||
|
|
||||||
override val lang = "all"
|
override val lang = "all"
|
||||||
|
|
||||||
override val supportsLatest = true
|
override val versionId = 2
|
||||||
|
|
||||||
override fun headersBuilder() = super.headersBuilder().apply {
|
override val supportsLatest = true
|
||||||
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)
|
||||||
|
@ -50,75 +47,40 @@ class Hikari : ParsedAnimeHttpSource(), ConfigurableAnimeSource {
|
||||||
|
|
||||||
// ============================== Popular ===============================
|
// ============================== Popular ===============================
|
||||||
|
|
||||||
override fun popularAnimeRequest(page: Int): Request {
|
override fun popularAnimeRequest(page: Int) = searchAnimeRequest(page, "", AnimeFilterList())
|
||||||
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): AnimesPage {
|
override fun popularAnimeParse(response: Response) = searchAnimeParse(response)
|
||||||
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 = "$baseUrl/ajax/getfilter?type=&country=&stats=&rate=&source=&season=&language=&aired_year=&aired_month=&aired_day=&sort=recently_updated&genres=&page=$page"
|
val url = "$apiUrl/episode/new/".toHttpUrl().newBuilder().apply {
|
||||||
val headers = headersBuilder().set("Referer", "$baseUrl/filter").build()
|
addQueryParameter("limit", "100")
|
||||||
|
addQueryParameter("language", "EN")
|
||||||
|
}.build()
|
||||||
return GET(url, headers)
|
return GET(url, headers)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun latestUpdatesParse(response: Response): AnimesPage =
|
override fun latestUpdatesParse(response: Response): AnimesPage {
|
||||||
popularAnimeParse(response)
|
val data = response.parseAs<CatalogResponseDto<LatestEpisodeDto>>()
|
||||||
|
val preferEnglish = preferences.getTitleLang
|
||||||
|
|
||||||
override fun latestUpdatesSelector(): String =
|
val animeList = data.results.distinctBy { it.uid }.map { it.toSAnime(preferEnglish) }
|
||||||
throw UnsupportedOperationException()
|
return AnimesPage(animeList, false)
|
||||||
|
}
|
||||||
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 = baseUrl.toHttpUrl().newBuilder().apply {
|
val url = "$apiUrl/anime/".toHttpUrl().newBuilder().apply {
|
||||||
if (query.isNotEmpty()) {
|
addQueryParameter("sort", "created_at")
|
||||||
addPathSegment("search")
|
addQueryParameter("order", "asc")
|
||||||
addQueryParameter("keyword", query)
|
addQueryParameter("page", page.toString())
|
||||||
addQueryParameter("page", page.toString())
|
filters.filterIsInstance<UriFilter>().forEach {
|
||||||
} else {
|
it.addToUri(this)
|
||||||
addPathSegment("ajax")
|
|
||||||
addPathSegment("getfilter")
|
|
||||||
filters.filterIsInstance<UriFilter>().forEach {
|
|
||||||
it.addToUri(this)
|
|
||||||
}
|
|
||||||
addQueryParameter("page", page.toString())
|
|
||||||
}
|
}
|
||||||
}.build()
|
|
||||||
|
|
||||||
val headers = headersBuilder().apply {
|
|
||||||
if (query.isNotEmpty()) {
|
if (query.isNotEmpty()) {
|
||||||
set("Referer", url.toString().substringBeforeLast("&page"))
|
addQueryParameter("search", query)
|
||||||
} else {
|
|
||||||
set("Referer", "$baseUrl/filter")
|
|
||||||
}
|
}
|
||||||
}.build()
|
}.build()
|
||||||
|
|
||||||
|
@ -126,280 +88,179 @@ class Hikari : ParsedAnimeHttpSource(), ConfigurableAnimeSource {
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun searchAnimeParse(response: Response): AnimesPage {
|
override fun searchAnimeParse(response: Response): AnimesPage {
|
||||||
return if (response.request.url.encodedPath.startsWith("/search")) {
|
val data = response.parseAs<CatalogResponseDto<AnimeDto>>()
|
||||||
super.searchAnimeParse(response)
|
val preferEnglish = preferences.getTitleLang
|
||||||
} else {
|
|
||||||
popularAnimeParse(response)
|
val animeList = data.results.map { it.toSAnime(preferEnglish) }
|
||||||
}
|
val hasNextPage = data.next != null
|
||||||
|
|
||||||
|
return AnimesPage(animeList, hasNextPage)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun searchAnimeSelector(): String = popularAnimeSelector()
|
|
||||||
|
|
||||||
override fun searchAnimeFromElement(element: Element): SAnime = popularAnimeFromElement(element)
|
|
||||||
|
|
||||||
override fun searchAnimeNextPageSelector(): String = "ul.pagination > li.active + li"
|
|
||||||
|
|
||||||
// ============================== Filters ===============================
|
// ============================== 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(),
|
||||||
LanguageFilter(),
|
YearFilter(),
|
||||||
SortFilter(),
|
|
||||||
AiringDateFilter(),
|
|
||||||
GenreFilter(),
|
GenreFilter(),
|
||||||
|
LanguageFilter(),
|
||||||
)
|
)
|
||||||
|
|
||||||
// =========================== Anime Details ============================
|
// =========================== Anime Details ============================
|
||||||
|
|
||||||
override fun animeDetailsParse(document: Document): SAnime = SAnime.create().apply {
|
override fun getAnimeUrl(anime: SAnime): String {
|
||||||
with(document.selectFirst("#ani_detail")!!) {
|
return "$baseUrl/info/${anime.url}"
|
||||||
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()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun Element?.parseStatus(): Int = when (this?.text()?.lowercase()) {
|
override fun animeDetailsRequest(anime: SAnime): Request {
|
||||||
"currently airing" -> SAnime.ONGOING
|
return GET("$apiUrl/anime/uid/${anime.url}/", headers)
|
||||||
"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 {
|
||||||
val animeId = anime.url.split("/")[2]
|
return GET("$apiUrl/episode/uid/${anime.url}/", headers)
|
||||||
|
|
||||||
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> {
|
||||||
return response.parseAs<HtmlResponseDto>().toHtml(baseUrl)
|
val guid = response.request.url.pathSegments[3]
|
||||||
.select(episodeListSelector())
|
|
||||||
.map(::episodeFromElement)
|
|
||||||
.reversed()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun episodeListSelector() = "a[class~=ep-item]"
|
return response.parseAs<List<EpisodeDto>>().map { it.toSEpisode(guid) }.reversed()
|
||||||
|
|
||||||
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) }
|
private val filemoonExtractor by lazy { FilemoonExtractor(client, preferences) }
|
||||||
private val vidHideExtractor by lazy { VidHideExtractor(client, headers) }
|
private val savefileExtractor by lazy { SavefileExtractor(client, preferences) }
|
||||||
|
private val buzzheavierExtractor by lazy { BuzzheavierExtractor(client, headers) }
|
||||||
private val chillxExtractor by lazy { ChillxExtractor(client, headers) }
|
private val 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 url = (baseUrl + episode.url).toHttpUrl()
|
val (guid, epId) = episode.url.split("-")
|
||||||
val animeId = url.queryParameter("uid")!!
|
return GET("$apiUrl/embed/$guid/$epId/", headers)
|
||||||
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 html = response.parseAs<HtmlResponseDto>().toHtml(baseUrl)
|
val data = response.parseAs<List<EmbedDto>>()
|
||||||
Log.d("Hikari", html.toString())
|
|
||||||
|
|
||||||
val headers = headersBuilder()
|
val selectedProviders = preferences.getStringSet(PREF_PROVIDER_KEY, PREF_PROVIDERS_DEFAULT)?.map(String::lowercase)?.toSet() ?: emptySet()
|
||||||
.set("Referer", response.request.url.toString())
|
|
||||||
.build()
|
|
||||||
|
|
||||||
val subEmbedUrls = html.select(".servers-sub div.item.server-item").flatMap { item ->
|
return data.parallelCatchingFlatMapBlocking { embed ->
|
||||||
val name = item.text().trim() + " (Sub)"
|
val embedName = embed.embedName.lowercase()
|
||||||
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 (embedName !in selectedProviders) return@parallelCatchingFlatMapBlocking emptyList()
|
||||||
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 prefix = getEmbedTypeName(embed.embedType) + embed.embedName
|
||||||
val iframeList = client.newCall(
|
|
||||||
GET(url, headers),
|
|
||||||
).execute().parseAs<List<String>>()
|
|
||||||
|
|
||||||
iframeList.map {
|
when (embedName) {
|
||||||
val iframeSrc = Jsoup.parseBodyFragment(it).selectFirst("iframe")?.attr("src")
|
"streamwish" -> streamwishExtractor.videosFromUrl(embed.embedFrame, videoNameGen = { "$prefix - $it" })
|
||||||
if (iframeSrc == null) {
|
"filemoon" -> filemoonExtractor.videosFromUrl(embed.embedFrame, "$prefix - ")
|
||||||
Log.e("Hikari", "iframe src is null for URL: $url")
|
"sv" -> savefileExtractor.videosFromUrl(embed.embedFrame, "$prefix - ")
|
||||||
return@map Pair("", "")
|
"playerx" -> chillxExtractor.videoFromUrl(embed.embedFrame, "$prefix - ")
|
||||||
}
|
"hiki" -> hikiExtraction(embed.embedFrame, "$prefix - ")
|
||||||
Pair(iframeSrc, name)
|
else -> emptyList()
|
||||||
}.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 getVideosFromEmbed(embedUrl: String, name: String): List<Video> = when {
|
private fun hikiExtraction(url: String, prefix: String): List<Video> {
|
||||||
name.contains("vidhide", true) -> vidHideExtractor.videosFromUrl(embedUrl, videoNameGen = { s -> "$name - $s" })
|
val hikiMirror = preferences.getString(PREF_HIKI_KEY, PREF_HIKI_DEFAULT)!!
|
||||||
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 - ")
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun videoListSelector() = ".server-item:has(a[onclick~=getEmbed])"
|
if (hikiMirror == "hiki") {
|
||||||
|
return buzzheavierExtractor.videosFromUrl(url, prefix, proxyUrl)
|
||||||
|
}
|
||||||
|
val id = url.toHttpUrl().pathSegments[0]
|
||||||
|
val videoUrl = "https://$hikiMirror/$id"
|
||||||
|
return buzzheavierExtractor.videosFromUrl(videoUrl, prefix)
|
||||||
|
}
|
||||||
|
|
||||||
override fun List<Video>.sort(): List<Video> {
|
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"
|
||||||
|
@ -407,13 +268,44 @@ class Hikari : ParsedAnimeHttpSource(), ConfigurableAnimeSource {
|
||||||
entryValues = PREF_QUALITY_VALUES
|
entryValues = PREF_QUALITY_VALUES
|
||||||
setDefaultValue(PREF_QUALITY_DEFAULT)
|
setDefaultValue(PREF_QUALITY_DEFAULT)
|
||||||
summary = "%s"
|
summary = "%s"
|
||||||
|
|
||||||
setOnPreferenceChangeListener { _, newValue ->
|
|
||||||
val selected = newValue as String
|
|
||||||
val index = findIndexOfValue(selected)
|
|
||||||
val entry = entryValues[index] as String
|
|
||||||
preferences.edit().putString(key, entry).commit()
|
|
||||||
}
|
|
||||||
}.also(screen::addPreference)
|
}.also(screen::addPreference)
|
||||||
|
|
||||||
|
ListPreference(screen.context).apply {
|
||||||
|
key = PREF_TYPE_KEY
|
||||||
|
title = "Preferred type"
|
||||||
|
entries = PREF_TYPE_ENTRIES
|
||||||
|
entryValues = PREF_TYPE_VALUES
|
||||||
|
setDefaultValue(PREF_TYPE_DEFAULT)
|
||||||
|
summary = "%s"
|
||||||
|
}.also(screen::addPreference)
|
||||||
|
|
||||||
|
ListPreference(screen.context).apply {
|
||||||
|
key = PREF_HOSTER_KEY
|
||||||
|
title = "Preferred hoster"
|
||||||
|
entries = PREF_HOSTER_ENTRIES
|
||||||
|
entryValues = PREF_HOSTER_VALUES
|
||||||
|
setDefaultValue(PREF_HOSTER_DEFAULT)
|
||||||
|
summary = "%s"
|
||||||
|
}.also(screen::addPreference)
|
||||||
|
|
||||||
|
MultiSelectListPreference(screen.context).apply {
|
||||||
|
key = PREF_PROVIDER_KEY
|
||||||
|
title = "Enable/Disable Video Providers"
|
||||||
|
entries = PREF_PROVIDERS
|
||||||
|
entryValues = PREF_PROVIDERS_VALUE
|
||||||
|
setDefaultValue(PREF_PROVIDERS_DEFAULT)
|
||||||
|
}.also(screen::addPreference)
|
||||||
|
|
||||||
|
ListPreference(screen.context).apply {
|
||||||
|
key = PREF_HIKI_KEY
|
||||||
|
title = "Hiki provider mirrors"
|
||||||
|
entries = PREF_HIKI_ENTRIES
|
||||||
|
entryValues = PREF_HIKI_VALUES
|
||||||
|
setDefaultValue(PREF_HIKI_DEFAULT)
|
||||||
|
summary = "%s"
|
||||||
|
}.also(screen::addPreference)
|
||||||
|
|
||||||
|
FilemoonExtractor.addSubtitlePref(screen)
|
||||||
|
SavefileExtractor.addSubtitlePref(screen)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
ext {
|
ext {
|
||||||
extName = 'JavGG'
|
extName = 'JavGG'
|
||||||
extClass = '.Javgg'
|
extClass = '.Javgg'
|
||||||
extVersionCode = 5
|
extVersionCode = 7
|
||||||
isNsfw = true
|
isNsfw = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
ext {
|
ext {
|
||||||
extName = 'Jav Guru'
|
extName = 'Jav Guru'
|
||||||
extClass = '.JavGuru'
|
extClass = '.JavGuru'
|
||||||
extVersionCode = 26
|
extVersionCode = 28
|
||||||
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 = 9
|
overrideVersionCode = 11
|
||||||
}
|
}
|
||||||
|
|
||||||
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 = 15
|
extVersionCode = 16
|
||||||
isNsfw = true
|
isNsfw = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
8
src/all/newgrounds/build.gradle
Normal file
8
src/all/newgrounds/build.gradle
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
ext {
|
||||||
|
extName = 'Newgrounds'
|
||||||
|
extClass = '.NewGrounds'
|
||||||
|
extVersionCode = 1
|
||||||
|
isNsfw = true
|
||||||
|
}
|
||||||
|
|
||||||
|
apply from: "$rootDir/common.gradle"
|
BIN
src/all/newgrounds/res/mipmap-hdpi/ic_launcher.png
Normal file
BIN
src/all/newgrounds/res/mipmap-hdpi/ic_launcher.png
Normal file
Binary file not shown.
After ![]() (image error) Size: 4.1 KiB |
BIN
src/all/newgrounds/res/mipmap-mdpi/ic_launcher.png
Normal file
BIN
src/all/newgrounds/res/mipmap-mdpi/ic_launcher.png
Normal file
Binary file not shown.
After ![]() (image error) Size: 2.3 KiB |
BIN
src/all/newgrounds/res/mipmap-xhdpi/ic_launcher.png
Normal file
BIN
src/all/newgrounds/res/mipmap-xhdpi/ic_launcher.png
Normal file
Binary file not shown.
After ![]() (image error) Size: 5.7 KiB |
BIN
src/all/newgrounds/res/mipmap-xxhdpi/ic_launcher.png
Normal file
BIN
src/all/newgrounds/res/mipmap-xxhdpi/ic_launcher.png
Normal file
Binary file not shown.
After ![]() (image error) Size: 11 KiB |
BIN
src/all/newgrounds/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
BIN
src/all/newgrounds/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
Binary file not shown.
After ![]() (image error) Size: 16 KiB |
|
@ -0,0 +1,13 @@
|
||||||
|
import java.text.ParseException
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
|
||||||
|
@Suppress("NOTHING_TO_INLINE")
|
||||||
|
inline fun SimpleDateFormat.tryParse(date: String?): Long {
|
||||||
|
date ?: return 0L
|
||||||
|
|
||||||
|
return try {
|
||||||
|
parse(date)?.time ?: 0L
|
||||||
|
} catch (_: ParseException) {
|
||||||
|
0L
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,545 @@
|
||||||
|
package eu.kanade.tachiyomi.animeextension.all.newgrounds
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
|
import android.os.Handler
|
||||||
|
import android.os.Looper
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.preference.CheckBoxPreference
|
||||||
|
import androidx.preference.ListPreference
|
||||||
|
import androidx.preference.MultiSelectListPreference
|
||||||
|
import androidx.preference.PreferenceScreen
|
||||||
|
import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource
|
||||||
|
import eu.kanade.tachiyomi.animesource.model.AnimeFilter
|
||||||
|
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
|
||||||
|
import eu.kanade.tachiyomi.animesource.model.AnimesPage
|
||||||
|
import eu.kanade.tachiyomi.animesource.model.SAnime
|
||||||
|
import eu.kanade.tachiyomi.animesource.model.SEpisode
|
||||||
|
import eu.kanade.tachiyomi.animesource.model.Video
|
||||||
|
import eu.kanade.tachiyomi.animesource.online.ParsedAnimeHttpSource
|
||||||
|
import eu.kanade.tachiyomi.network.GET
|
||||||
|
import eu.kanade.tachiyomi.util.asJsoup
|
||||||
|
import okhttp3.FormBody
|
||||||
|
import okhttp3.Headers
|
||||||
|
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||||
|
import okhttp3.Request
|
||||||
|
import okhttp3.Response
|
||||||
|
import org.json.JSONObject
|
||||||
|
import org.jsoup.nodes.Document
|
||||||
|
import org.jsoup.nodes.Element
|
||||||
|
import tryParse
|
||||||
|
import uy.kohesive.injekt.Injekt
|
||||||
|
import uy.kohesive.injekt.api.get
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
|
private const val PAGE_SIZE = 20
|
||||||
|
|
||||||
|
class NewGrounds : ParsedAnimeHttpSource(), ConfigurableAnimeSource {
|
||||||
|
|
||||||
|
override val lang = "all"
|
||||||
|
override val baseUrl = "https://www.newgrounds.com"
|
||||||
|
override val name = "Newgrounds"
|
||||||
|
override val supportsLatest = true
|
||||||
|
|
||||||
|
private val dateFormat = SimpleDateFormat("MMM dd, yyyy", Locale.ENGLISH)
|
||||||
|
|
||||||
|
private val preferences by lazy {
|
||||||
|
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
|
||||||
|
}
|
||||||
|
|
||||||
|
private val context = Injekt.get<Application>()
|
||||||
|
private val handler by lazy { Handler(Looper.getMainLooper()) }
|
||||||
|
|
||||||
|
private val videoListHeaders by lazy {
|
||||||
|
headers.newBuilder()
|
||||||
|
.add("Accept", "application/json, text/javascript, */*; q=0.01")
|
||||||
|
.add("X-Requested-With", "XMLHttpRequest")
|
||||||
|
.add("Referer", baseUrl)
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Latest
|
||||||
|
|
||||||
|
private fun getLatestSection(): String {
|
||||||
|
return preferences.getString("LATEST", PREF_SECTIONS["Latest"])!!
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun latestUpdatesRequest(page: Int): Request {
|
||||||
|
val offset = (page - 1) * PAGE_SIZE
|
||||||
|
return GET("$baseUrl/${getLatestSection()}?offset=$offset", headers)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun latestUpdatesNextPageSelector(): String = "#load-more-items a"
|
||||||
|
|
||||||
|
override fun latestUpdatesParse(response: Response): AnimesPage {
|
||||||
|
checkAdultContentFiltered(response.headers)
|
||||||
|
return super.latestUpdatesParse(response)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun latestUpdatesSelector(): String = animeSelector(getLatestSection())
|
||||||
|
|
||||||
|
override fun latestUpdatesFromElement(element: Element): SAnime {
|
||||||
|
return animeFromElement(element, getLatestSection())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Browse
|
||||||
|
|
||||||
|
private fun getPopularSection(): String {
|
||||||
|
return preferences.getString("POPULAR", PREF_SECTIONS["Popular"])!!
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun popularAnimeRequest(page: Int): Request {
|
||||||
|
val offset = (page - 1) * PAGE_SIZE
|
||||||
|
return GET("$baseUrl/${getPopularSection()}?offset=$offset", headers)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun popularAnimeNextPageSelector(): String = "#load-more-items a"
|
||||||
|
|
||||||
|
override fun popularAnimeParse(response: Response): AnimesPage {
|
||||||
|
checkAdultContentFiltered(response.headers)
|
||||||
|
return super.popularAnimeParse(response)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun popularAnimeSelector(): String = animeSelector(getPopularSection())
|
||||||
|
|
||||||
|
override fun popularAnimeFromElement(element: Element): SAnime {
|
||||||
|
return animeFromElement(element, getPopularSection())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Search
|
||||||
|
|
||||||
|
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
|
||||||
|
val searchUrl = "$baseUrl/search/conduct/movies".toHttpUrl().newBuilder()
|
||||||
|
.addQueryParameter("page", page.toString())
|
||||||
|
|
||||||
|
if (query.isNotEmpty()) searchUrl.addQueryParameter("terms", query)
|
||||||
|
|
||||||
|
filters.findInstance<MatchAgainstFilter>().ifFilterSet {
|
||||||
|
searchUrl.addQueryParameter("match", MATCH_AGAINST.values.elementAt(it.state))
|
||||||
|
}
|
||||||
|
filters.findInstance<TuningFilterGroup>()?.state
|
||||||
|
?.findInstance<TuningExactFilter>().ifFilterSet {
|
||||||
|
searchUrl.addQueryParameter("exact", "1")
|
||||||
|
}
|
||||||
|
filters.findInstance<TuningFilterGroup>()?.state
|
||||||
|
?.findInstance<TuningAnyFilter>().ifFilterSet {
|
||||||
|
searchUrl.addQueryParameter("any", "1")
|
||||||
|
}
|
||||||
|
filters.findInstance<AuthorFilter>().ifFilterSet {
|
||||||
|
searchUrl.addQueryParameter("user", it.state)
|
||||||
|
}
|
||||||
|
filters.findInstance<GenreFilter>().ifFilterSet {
|
||||||
|
searchUrl.addQueryParameter("genre", GENRE.values.elementAt(it.state))
|
||||||
|
}
|
||||||
|
filters.findInstance<LengthFilterGroup>()?.state
|
||||||
|
?.findInstance<MinLengthFilter>().ifFilterSet {
|
||||||
|
searchUrl.addQueryParameter("min_length", it.state)
|
||||||
|
}
|
||||||
|
filters.findInstance<LengthFilterGroup>()?.state
|
||||||
|
?.findInstance<MaxLengthFilter>().ifFilterSet {
|
||||||
|
searchUrl.addQueryParameter("max_length", it.state)
|
||||||
|
}
|
||||||
|
filters.findInstance<FrontpagedFilter>().ifFilterSet {
|
||||||
|
searchUrl.addQueryParameter("frontpaged", "1")
|
||||||
|
}
|
||||||
|
filters.findInstance<DateFilterGroup>()?.state
|
||||||
|
?.findInstance<AfterDateFilter>().ifFilterSet {
|
||||||
|
searchUrl.addQueryParameter("after", it.state)
|
||||||
|
}
|
||||||
|
filters.findInstance<DateFilterGroup>()?.state
|
||||||
|
?.findInstance<BeforeDateFilter>().ifFilterSet {
|
||||||
|
searchUrl.addQueryParameter("before", it.state)
|
||||||
|
}
|
||||||
|
filters.findInstance<SortingFilter>().ifFilterSet {
|
||||||
|
if (it.state?.index != 0) {
|
||||||
|
val sortOption = SORTING.values.elementAt(it.state?.index ?: return@ifFilterSet)
|
||||||
|
val direction = if (it.state?.ascending == true) "asc" else "desc"
|
||||||
|
searchUrl.addQueryParameter(
|
||||||
|
"sort",
|
||||||
|
"$sortOption-$direction",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
filters.findInstance<TagsFilter>().ifFilterSet {
|
||||||
|
searchUrl.addQueryParameter("tags", it.state)
|
||||||
|
}
|
||||||
|
|
||||||
|
return GET(searchUrl.build(), headers)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun searchAnimeNextPageSelector(): String = "#results-load-more"
|
||||||
|
|
||||||
|
override fun searchAnimeParse(response: Response): AnimesPage {
|
||||||
|
checkAdultContentFiltered(response.headers)
|
||||||
|
return super.searchAnimeParse(response)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun searchAnimeSelector(): String = "ul.itemlist li:not(#results-load-more) a"
|
||||||
|
|
||||||
|
override fun searchAnimeFromElement(element: Element): SAnime = animeFromListElement(element)
|
||||||
|
|
||||||
|
// Etc.
|
||||||
|
|
||||||
|
override fun animeDetailsParse(document: Document): SAnime {
|
||||||
|
fun getStarRating(): String {
|
||||||
|
val score: Double = document.selectFirst("#score_number")?.text()?.toDouble() ?: 0.0
|
||||||
|
val fullStars = score.toInt()
|
||||||
|
val hasHalfStar = (score % 1) >= 0.5
|
||||||
|
val totalStars = if (hasHalfStar) fullStars + 1 else fullStars
|
||||||
|
val emptyStars = 5 - totalStars
|
||||||
|
|
||||||
|
return "✪".repeat(fullStars) + (if (hasHalfStar) "✪" else "") + "⬤".repeat(emptyStars) + " ($score)"
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getAdultRating(): String {
|
||||||
|
val rating = document.selectFirst("#embed_header h2")!!.className().substringAfter("rated-")
|
||||||
|
return when (rating) {
|
||||||
|
"e" -> "🟩 Everyone"
|
||||||
|
"t" -> "🟦 Ages 13+"
|
||||||
|
"m" -> "🟪 Ages 17+"
|
||||||
|
"a" -> "🟥 Adults Only"
|
||||||
|
else -> "❓"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getStats(): String {
|
||||||
|
val statsElement = document.selectFirst("#sidestats > dl:first-of-type")
|
||||||
|
val views = statsElement?.selectFirst("dd:first-of-type")?.text() ?: "?"
|
||||||
|
val faves = statsElement?.selectFirst("dd:nth-of-type(2)")?.text() ?: "?"
|
||||||
|
val votes = statsElement?.selectFirst("dd:nth-of-type(3)")?.text() ?: "?"
|
||||||
|
|
||||||
|
return "👀 $views | ❤️ $faves | 🗳️ $votes"
|
||||||
|
}
|
||||||
|
|
||||||
|
fun prepareDescription(): String {
|
||||||
|
val descriptionElements = preferences.getStringSet("DESCRIPTION_ELEMENTS", setOf("short"))
|
||||||
|
?: return ""
|
||||||
|
|
||||||
|
val shortDescription = document.selectFirst("meta[itemprop=\"description\"]")?.attr("content")
|
||||||
|
val longDescription = document.selectFirst("#author_comments")?.wholeText()
|
||||||
|
val statsSummary = "${getAdultRating()} | ${getStarRating()} | ${getStats()}"
|
||||||
|
|
||||||
|
val description = StringBuilder()
|
||||||
|
|
||||||
|
if (descriptionElements.contains("short")) {
|
||||||
|
description.append(shortDescription)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (descriptionElements.contains("long")) {
|
||||||
|
description.append("\n\n" + longDescription)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (descriptionElements.contains("stats") || preferences.getBoolean("STATS_SUMMARY", false)) {
|
||||||
|
description.append("\n\n" + statsSummary)
|
||||||
|
}
|
||||||
|
|
||||||
|
return description.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
val relatedPlaylistElement = document.selectFirst("div[id^=\"related_playlists\"] ")
|
||||||
|
val relatedPlaylistUrl = relatedPlaylistElement?.selectFirst("a:not([id^=\"related_playlists\"])")?.absUrl("href")
|
||||||
|
val relatedPlaylistName = relatedPlaylistElement?.selectFirst(".detail-title h4")?.text()
|
||||||
|
val isPartOfSeries = relatedPlaylistUrl?.startsWith("$baseUrl/series") ?: false
|
||||||
|
|
||||||
|
return SAnime.create().apply {
|
||||||
|
title = relatedPlaylistName.takeIf { isPartOfSeries }
|
||||||
|
?: document.selectFirst("h2[itemprop=\"name\"]")!!.text()
|
||||||
|
description = prepareDescription()
|
||||||
|
author = document.selectFirst(".authorlinks > div:first-of-type .item-details-main")?.text()
|
||||||
|
artist = document.select(".authorlinks > div:not(:first-of-type) .item-details-main").joinToString {
|
||||||
|
it.text()
|
||||||
|
}
|
||||||
|
thumbnail_url = document.selectFirst("meta[itemprop=\"thumbnailUrl\"]")?.absUrl("content")
|
||||||
|
genre = document.select(".tags li a").joinToString { it.text() } + document.selectFirst("div[id^=\"genre-view\"] dt")?.text()
|
||||||
|
status = SAnime.ONGOING.takeIf { isPartOfSeries } ?: SAnime.COMPLETED
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun episodeListSelector(): String = throw UnsupportedOperationException("Not Used")
|
||||||
|
|
||||||
|
override fun episodeFromElement(element: Element): SEpisode = throw UnsupportedOperationException("Not Used")
|
||||||
|
|
||||||
|
override suspend fun getEpisodeList(anime: SAnime): List<SEpisode> {
|
||||||
|
val response = client.newCall(GET("${baseUrl}${anime.url}", headers)).execute()
|
||||||
|
val document = response.asJsoup()
|
||||||
|
|
||||||
|
val relatedPlaylistUrl = document.selectFirst("div[id^=\"related_playlists\"] a:not([id^=\"related_playlists\"])")?.absUrl("href")
|
||||||
|
val isPartOfSeries = relatedPlaylistUrl?.startsWith("$baseUrl/series") ?: false
|
||||||
|
|
||||||
|
val episodes = if (isPartOfSeries) {
|
||||||
|
val response2 = client.newCall(GET(relatedPlaylistUrl!!, headers)).execute()
|
||||||
|
val document2 = response2.asJsoup()
|
||||||
|
parseEpisodeList(document2)
|
||||||
|
} else {
|
||||||
|
val dateString = document.selectFirst("#sidestats > dl:nth-of-type(2) > dd:first-of-type")?.text()
|
||||||
|
|
||||||
|
return listOf(
|
||||||
|
SEpisode.create().apply {
|
||||||
|
episode_number = 1f
|
||||||
|
date_upload = dateFormat.tryParse(dateString)
|
||||||
|
name = document.selectFirst("meta[name=\"title\"]")!!.attr("content")
|
||||||
|
setUrlWithoutDomain("$baseUrl${anime.url.replace("/view/","/video/")}")
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return episodes
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun episodeListRequest(anime: SAnime): Request = throw UnsupportedOperationException()
|
||||||
|
|
||||||
|
override fun episodeListParse(response: Response): List<SEpisode> = throw UnsupportedOperationException()
|
||||||
|
|
||||||
|
private fun parseEpisodeList(document: Document): List<SEpisode> {
|
||||||
|
val ids = document.select("li.visual-link-container").map { it.attr("data-visual-link") }
|
||||||
|
val formBody = FormBody.Builder()
|
||||||
|
.add("ids", ids.toString())
|
||||||
|
.add("component_params[include_author]", "1")
|
||||||
|
.add("include_all_suitabilities", "0")
|
||||||
|
.add("isAjaxRequest", "1")
|
||||||
|
.build()
|
||||||
|
|
||||||
|
val request = Request.Builder()
|
||||||
|
.url("$baseUrl/visual-links-fetch")
|
||||||
|
.post(formBody)
|
||||||
|
.headers(headers)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
val response = client.newCall(request).execute()
|
||||||
|
|
||||||
|
val jsonObject = JSONObject(response.body.string())
|
||||||
|
val episodes = mutableListOf<SEpisode>()
|
||||||
|
|
||||||
|
val simples = jsonObject.getJSONObject("simples")
|
||||||
|
var index = 1
|
||||||
|
for (key in simples.keys()) {
|
||||||
|
val subObject = simples.getJSONObject(key)
|
||||||
|
|
||||||
|
for (episodeKey in subObject.keys()) {
|
||||||
|
val episodeData = subObject.getJSONObject(episodeKey)
|
||||||
|
val uploaderData = episodeData.getJSONObject("user")
|
||||||
|
|
||||||
|
val episode = SEpisode.create().apply {
|
||||||
|
episode_number = index.toFloat()
|
||||||
|
name = episodeData.getString("title")
|
||||||
|
scanlator = uploaderData.getString("user_name")
|
||||||
|
setUrlWithoutDomain("$baseUrl/portal/video/${episodeData.getString("id")}")
|
||||||
|
}
|
||||||
|
|
||||||
|
episodes.add(episode)
|
||||||
|
index++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return episodes.reversed()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun videoListRequest(episode: SEpisode): Request = GET("$baseUrl${episode.url}", videoListHeaders)
|
||||||
|
|
||||||
|
override fun videoListParse(response: Response): List<Video> {
|
||||||
|
val responseBody = response.body.string()
|
||||||
|
val json = JSONObject(responseBody)
|
||||||
|
val sources = json.getJSONObject("sources")
|
||||||
|
|
||||||
|
val videos = mutableListOf<Video>()
|
||||||
|
|
||||||
|
for (quality in sources.keys()) {
|
||||||
|
val qualityArray = sources.getJSONArray(quality)
|
||||||
|
for (i in 0 until qualityArray.length()) {
|
||||||
|
val videoObject = qualityArray.getJSONObject(i)
|
||||||
|
val videoUrl = videoObject.getString("src")
|
||||||
|
|
||||||
|
videos.add(
|
||||||
|
Video(
|
||||||
|
url = videoUrl,
|
||||||
|
quality = quality,
|
||||||
|
videoUrl = videoUrl,
|
||||||
|
headers = headers,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return videos
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun videoListSelector(): String = throw UnsupportedOperationException("Not Used")
|
||||||
|
|
||||||
|
override fun videoFromElement(element: Element): Video = throw UnsupportedOperationException("Not Used")
|
||||||
|
|
||||||
|
override fun videoUrlParse(document: Document): String = throw UnsupportedOperationException("Not Used")
|
||||||
|
|
||||||
|
// ============================== Filters ===============================
|
||||||
|
|
||||||
|
override fun getFilterList(): AnimeFilterList = AnimeFilterList(
|
||||||
|
SortingFilter(),
|
||||||
|
MatchAgainstFilter(),
|
||||||
|
TuningFilterGroup(),
|
||||||
|
GenreFilter(),
|
||||||
|
AuthorFilter(),
|
||||||
|
TagsFilter(),
|
||||||
|
LengthFilterGroup(),
|
||||||
|
DateFilterGroup(),
|
||||||
|
FrontpagedFilter(),
|
||||||
|
AnimeFilter.Separator(),
|
||||||
|
AnimeFilter.Header("Age rating: to change age rating open WebView and in Movies tab click on 🟩🟦🟪🟥 icons on the right. Then refresh search."), // uses ng_user0 cookie
|
||||||
|
)
|
||||||
|
|
||||||
|
// ============================ Preferences =============================
|
||||||
|
/*
|
||||||
|
According to the labels on the website:
|
||||||
|
Featured -> /movies/featured
|
||||||
|
Latest -> /movies/browse
|
||||||
|
Popular -> /movies/popular
|
||||||
|
Your Feed -> /social/feeds/show/favorite-artists-movies
|
||||||
|
Under Judgement -> /movies/browse?interval=all&artist-type=unjudged
|
||||||
|
*/
|
||||||
|
|
||||||
|
override fun setupPreferenceScreen(screen: PreferenceScreen) {
|
||||||
|
ListPreference(screen.context).apply {
|
||||||
|
key = "POPULAR"
|
||||||
|
title = "Popular section content"
|
||||||
|
entries = PREF_SECTIONS.keys.toTypedArray()
|
||||||
|
entryValues = PREF_SECTIONS.values.toTypedArray()
|
||||||
|
setDefaultValue(PREF_SECTIONS["Popular"])
|
||||||
|
summary = "%s"
|
||||||
|
|
||||||
|
setOnPreferenceChangeListener { _, newValue ->
|
||||||
|
val selected = newValue as String
|
||||||
|
preferences.edit().putString(key, selected).commit()
|
||||||
|
}
|
||||||
|
}.also(screen::addPreference)
|
||||||
|
|
||||||
|
ListPreference(screen.context).apply {
|
||||||
|
key = "LATEST"
|
||||||
|
title = "Latest section content"
|
||||||
|
entries = PREF_SECTIONS.keys.toTypedArray()
|
||||||
|
entryValues = PREF_SECTIONS.values.toTypedArray()
|
||||||
|
setDefaultValue(PREF_SECTIONS["Latest"])
|
||||||
|
summary = "%s"
|
||||||
|
|
||||||
|
setOnPreferenceChangeListener { _, newValue ->
|
||||||
|
val selected = newValue as String
|
||||||
|
preferences.edit().putString(key, selected).commit()
|
||||||
|
}
|
||||||
|
}.also(screen::addPreference)
|
||||||
|
|
||||||
|
MultiSelectListPreference(screen.context).apply {
|
||||||
|
key = "DESCRIPTION_ELEMENTS"
|
||||||
|
title = "Description elements"
|
||||||
|
entries = arrayOf("Short description", "Long description (author comments)", "Stats (score, favs, views)")
|
||||||
|
entryValues = arrayOf("short", "long", "stats")
|
||||||
|
setDefaultValue(setOf("short", "stats"))
|
||||||
|
summary = "Elements to be included in description"
|
||||||
|
|
||||||
|
setOnPreferenceChangeListener { _, newValue ->
|
||||||
|
val selectedItems = newValue as Set<*>
|
||||||
|
preferences.edit().putStringSet(key, selectedItems as Set<String>).apply()
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}.also(screen::addPreference)
|
||||||
|
|
||||||
|
CheckBoxPreference(screen.context).apply {
|
||||||
|
key = "PROMPT_CONTENT_FILTERED"
|
||||||
|
title = "Prompt to log in"
|
||||||
|
setDefaultValue(true)
|
||||||
|
summary = "Show toast when user is not logged in and therefore adult content is not accessible"
|
||||||
|
}.also(screen::addPreference)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========================== Helper Functions ==========================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Chooses an extraction technique for anime information, based on section selected in Preferences
|
||||||
|
*/
|
||||||
|
private fun animeFromElement(element: Element, section: String): SAnime {
|
||||||
|
return if (section == PREF_SECTIONS["Your Feed"]) {
|
||||||
|
animeFromFeedElement(element)
|
||||||
|
} else {
|
||||||
|
animeFromGridElement(element)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extracts anime information from element of grid-like list typical for /popular, /browse or /featured
|
||||||
|
*/
|
||||||
|
private fun animeFromGridElement(element: Element): SAnime = SAnime.create().apply {
|
||||||
|
title = element.selectFirst(".card-title h4")!!.text()
|
||||||
|
author = element.selectFirst(".card-title span")?.text()?.replace("By ", "")
|
||||||
|
description = element.selectFirst("a")?.attr("title")
|
||||||
|
thumbnail_url = element.selectFirst("img")?.absUrl("src")
|
||||||
|
setUrlWithoutDomain(element.selectFirst("a")!!.absUrl("href"))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extracts anime information from element of list returned in Your Feed
|
||||||
|
*/
|
||||||
|
private fun animeFromFeedElement(element: Element): SAnime = SAnime.create().apply {
|
||||||
|
title = element.selectFirst(".detail-title h4")!!.text()
|
||||||
|
author = element.selectFirst(".detail-title strong")?.text()
|
||||||
|
description = element.selectFirst(".detail-description")?.text()
|
||||||
|
thumbnail_url = element.selectFirst(".item-icon img")?.absUrl("src")
|
||||||
|
setUrlWithoutDomain(element.selectFirst("a")!!.absUrl("href"))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extracts anime information from element of list typical for /search or /series
|
||||||
|
*/
|
||||||
|
private fun animeFromListElement(element: Element): SAnime = SAnime.create().apply {
|
||||||
|
title = element.selectFirst(".detail-title > h4")!!.text()
|
||||||
|
author = element.selectFirst(".detail-title > span > strong")?.text()
|
||||||
|
description = element.selectFirst(".detail-description")?.text()
|
||||||
|
thumbnail_url = element.selectFirst(".item-icon img")?.absUrl("src")
|
||||||
|
setUrlWithoutDomain(element.selectFirst("a")!!.absUrl("href"))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns CSS selector for anime, based on the section selected in Preferences
|
||||||
|
*/
|
||||||
|
private fun animeSelector(section: String): String {
|
||||||
|
return if (section == PREF_SECTIONS["Your Feed"]) {
|
||||||
|
"a.item-portalsubmission"
|
||||||
|
} else {
|
||||||
|
"a.inline-card-portalsubmission"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if cookie with username is present in response headers.
|
||||||
|
* If cookie is missing: displays a toast with information.
|
||||||
|
*/
|
||||||
|
private fun checkAdultContentFiltered(headers: Headers) {
|
||||||
|
val usernameCookie: Boolean = headers.values("Set-Cookie").any { it.startsWith("NG_GG_username=") }
|
||||||
|
if (usernameCookie) return // user already logged in
|
||||||
|
|
||||||
|
val shouldPrompt = preferences.getBoolean("PROMPT_CONTENT_FILTERED", true)
|
||||||
|
if (shouldPrompt) {
|
||||||
|
handler.post {
|
||||||
|
Toast.makeText(context, "Log in via WebView to include adult content", Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private inline fun <reified T> Iterable<*>.findInstance() = find { it is T } as? T
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Executes the given [action] if the filter is set to a meaningful value.
|
||||||
|
*
|
||||||
|
* @param action A function to execute if the filter is set.
|
||||||
|
*/
|
||||||
|
private inline fun <T> T?.ifFilterSet(action: (T) -> Unit) where T : AnimeFilter<*> {
|
||||||
|
val state = this?.state
|
||||||
|
if (this != null && state != null && state != "" && state != 0 && state != false) {
|
||||||
|
action(this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val PREF_SECTIONS = mapOf(
|
||||||
|
"Featured" to "movies/featured",
|
||||||
|
"Latest" to "movies/browse",
|
||||||
|
"Popular" to "movies/popular",
|
||||||
|
"Your Feed" to "social/feeds/show/favorite-artists-movies",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,75 @@
|
||||||
|
package eu.kanade.tachiyomi.animeextension.all.newgrounds
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.animesource.model.AnimeFilter
|
||||||
|
|
||||||
|
class MatchAgainstFilter : AnimeFilter.Select<String>("Match against", MATCH_AGAINST.keys.toTypedArray(), 0)
|
||||||
|
|
||||||
|
class TuningExactFilter : AnimeFilter.CheckBox("exact matches", false)
|
||||||
|
class TuningAnyFilter : AnimeFilter.CheckBox("match any words", false)
|
||||||
|
class TuningFilterGroup : AnimeFilter.Group<AnimeFilter.CheckBox>(
|
||||||
|
"Tuning",
|
||||||
|
listOf(
|
||||||
|
TuningExactFilter(),
|
||||||
|
TuningAnyFilter(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
class AuthorFilter : AnimeFilter.Text("Author")
|
||||||
|
|
||||||
|
class GenreFilter : AnimeFilter.Select<String>("Genre", GENRE.keys.toTypedArray())
|
||||||
|
|
||||||
|
class MinLengthFilter : AnimeFilter.Text("Min Length")
|
||||||
|
class MaxLengthFilter : AnimeFilter.Text("Max Length")
|
||||||
|
class LengthFilterGroup : AnimeFilter.Group<AnimeFilter.Text>(
|
||||||
|
"Length (00:00:00)",
|
||||||
|
listOf(
|
||||||
|
MinLengthFilter(),
|
||||||
|
MaxLengthFilter(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
class FrontpagedFilter : AnimeFilter.CheckBox("Frontpaged?", false)
|
||||||
|
|
||||||
|
class AfterDateFilter : AnimeFilter.Text("On, or after")
|
||||||
|
class BeforeDateFilter : AnimeFilter.Text("Before")
|
||||||
|
class DateFilterGroup : AnimeFilter.Group<AnimeFilter.Text>(
|
||||||
|
"Date (YYYY-MM-DD)",
|
||||||
|
listOf(
|
||||||
|
AfterDateFilter(),
|
||||||
|
BeforeDateFilter(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
class SortingFilter() : AnimeFilter.Sort("Sort by", SORTING.keys.toTypedArray(), Selection(0, true))
|
||||||
|
|
||||||
|
class TagsFilter() : AnimeFilter.Text("Tags (comma separated)")
|
||||||
|
|
||||||
|
// ===================================================================
|
||||||
|
val MATCH_AGAINST = mapOf(
|
||||||
|
"Default" to "",
|
||||||
|
"title / description / tags / author" to "tdtu",
|
||||||
|
"title / description / tags" to "tdt",
|
||||||
|
"title / description" to "td",
|
||||||
|
"title" to "t",
|
||||||
|
"description" to "d",
|
||||||
|
)
|
||||||
|
|
||||||
|
val GENRE = mapOf(
|
||||||
|
"All" to "",
|
||||||
|
"Action" to "45",
|
||||||
|
"Comedy - Original" to "60",
|
||||||
|
"Comedy - Parody" to "61",
|
||||||
|
"Drama" to "47",
|
||||||
|
"Experimental" to "49",
|
||||||
|
"Informative" to "48",
|
||||||
|
"Music Video" to "50",
|
||||||
|
"Other" to "51",
|
||||||
|
"Spam" to "55",
|
||||||
|
)
|
||||||
|
|
||||||
|
val SORTING = mapOf(
|
||||||
|
"Default (Relevance)" to "relevance",
|
||||||
|
"Date" to "date",
|
||||||
|
"Score" to "score",
|
||||||
|
"Views" to "views",
|
||||||
|
)
|
|
@ -1,7 +1,7 @@
|
||||||
ext {
|
ext {
|
||||||
extName = 'Sudatchi'
|
extName = 'Sudatchi'
|
||||||
extClass = '.Sudatchi'
|
extClass = '.Sudatchi'
|
||||||
extVersionCode = 12
|
extVersionCode = 13
|
||||||
isNsfw = true
|
isNsfw = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
ext {
|
ext {
|
||||||
extName = 'SupJav'
|
extName = 'SupJav'
|
||||||
extClass = '.SupJavFactory'
|
extClass = '.SupJavFactory'
|
||||||
extVersionCode = 14
|
extVersionCode = 16
|
||||||
isNsfw = true
|
isNsfw = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
ext {
|
ext {
|
||||||
extName = 'Torrentio Anime (Torrent / Debrid)'
|
extName = 'Torrentio Anime (Torrent / Debrid)'
|
||||||
extClass = '.Torrentio'
|
extClass = '.Torrentio'
|
||||||
extVersionCode = 15
|
extVersionCode = 16
|
||||||
containsNsfw = false
|
containsNsfw = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -310,7 +310,7 @@ class Torrentio : ConfigurableAnimeSource, AnimeHttpSource() {
|
||||||
val aniZipResponse = json.decodeFromString<AniZipResponse>(responseString)
|
val aniZipResponse = json.decodeFromString<AniZipResponse>(responseString)
|
||||||
|
|
||||||
return when (aniZipResponse.mappings?.type) {
|
return when (aniZipResponse.mappings?.type) {
|
||||||
"TV" -> {
|
"TV", "ONA", "OVA" -> {
|
||||||
aniZipResponse.episodes
|
aniZipResponse.episodes
|
||||||
?.let { episodes ->
|
?.let { episodes ->
|
||||||
if (preferences.getBoolean(UPCOMING_EP_KEY, UPCOMING_EP_DEFAULT)) {
|
if (preferences.getBoolean(UPCOMING_EP_KEY, UPCOMING_EP_DEFAULT)) {
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
ext {
|
ext {
|
||||||
extName = 'Anime4up'
|
extName = 'Anime4up'
|
||||||
extClass = '.Anime4Up'
|
extClass = '.Anime4Up'
|
||||||
extVersionCode = 62
|
extVersionCode = 64
|
||||||
}
|
}
|
||||||
|
|
||||||
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 = 18
|
extVersionCode = 19
|
||||||
}
|
}
|
||||||
|
|
||||||
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 = 31
|
extVersionCode = 32
|
||||||
}
|
}
|
||||||
|
|
||||||
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 = 41
|
extVersionCode = 43
|
||||||
}
|
}
|
||||||
|
|
||||||
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 = 17
|
extVersionCode = 19
|
||||||
}
|
}
|
||||||
|
|
||||||
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 = 22
|
extVersionCode = 24
|
||||||
}
|
}
|
||||||
|
|
||||||
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 = 3
|
extVersionCode = 4
|
||||||
}
|
}
|
||||||
|
|
||||||
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 = 17
|
extVersionCode = 19
|
||||||
}
|
}
|
||||||
|
|
||||||
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 = 17
|
extVersionCode = 18
|
||||||
}
|
}
|
||||||
|
|
||||||
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 = 23
|
extVersionCode = 24
|
||||||
}
|
}
|
||||||
|
|
||||||
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 = 13
|
extVersionCode = 14
|
||||||
}
|
}
|
||||||
|
|
||||||
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 = 51
|
extVersionCode = 53
|
||||||
}
|
}
|
||||||
|
|
||||||
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.pics"
|
override val baseUrl = "https://witanime.cyou"
|
||||||
|
|
||||||
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 = 31
|
extVersionCode = 33
|
||||||
isNsfw = true
|
isNsfw = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
ext {
|
ext {
|
||||||
extName = 'Anime-Loads'
|
extName = 'Anime-Loads'
|
||||||
extClass = '.AnimeLoads'
|
extClass = '.AnimeLoads'
|
||||||
extVersionCode = 17
|
extVersionCode = 18
|
||||||
isNsfw = true
|
isNsfw = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
ext {
|
ext {
|
||||||
extName = 'AnimeToast'
|
extName = 'AnimeToast'
|
||||||
extClass = '.AnimeToast'
|
extClass = '.AnimeToast'
|
||||||
extVersionCode = 21
|
extVersionCode = 23
|
||||||
}
|
}
|
||||||
|
|
||||||
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 = 25
|
extVersionCode = 26
|
||||||
}
|
}
|
||||||
|
|
||||||
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 = 24
|
overrideVersionCode = 26
|
||||||
isNsfw = true
|
isNsfw = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
ext {
|
ext {
|
||||||
extName = 'Einfach'
|
extName = 'Einfach'
|
||||||
extClass = '.Einfach'
|
extClass = '.Einfach'
|
||||||
extVersionCode = 16
|
extVersionCode = 18
|
||||||
}
|
}
|
||||||
|
|
||||||
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 = 19
|
extVersionCode = 20
|
||||||
}
|
}
|
||||||
|
|
||||||
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 = 23
|
overrideVersionCode = 24
|
||||||
}
|
}
|
||||||
|
|
||||||
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 = 13
|
extVersionCode = 15
|
||||||
}
|
}
|
||||||
|
|
||||||
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 = 15
|
extVersionCode = 17
|
||||||
}
|
}
|
||||||
|
|
||||||
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 = 11
|
extVersionCode = 12
|
||||||
}
|
}
|
||||||
|
|
||||||
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 = 24
|
extVersionCode = 25
|
||||||
}
|
}
|
||||||
|
|
||||||
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 = 35
|
extVersionCode = 37
|
||||||
}
|
}
|
||||||
|
|
||||||
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 = 12
|
extVersionCode = 14
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
|
|
@ -2,8 +2,8 @@ ext {
|
||||||
extName = 'AnimeKhor'
|
extName = 'AnimeKhor'
|
||||||
extClass = '.AnimeKhor'
|
extClass = '.AnimeKhor'
|
||||||
themePkg = 'animestream'
|
themePkg = 'animestream'
|
||||||
baseUrl = 'https://animekhor.xyz'
|
baseUrl = 'https://animekhor.org'
|
||||||
overrideVersionCode = 6
|
overrideVersionCode = 9
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
|
|
@ -9,7 +9,7 @@ import eu.kanade.tachiyomi.multisrc.animestream.AnimeStream
|
||||||
class AnimeKhor : AnimeStream(
|
class AnimeKhor : AnimeStream(
|
||||||
"en",
|
"en",
|
||||||
"AnimeKhor",
|
"AnimeKhor",
|
||||||
"https://animekhor.xyz",
|
"https://animekhor.org",
|
||||||
) {
|
) {
|
||||||
// ============================ Video Links =============================
|
// ============================ Video Links =============================
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,7 @@ ext {
|
||||||
extClass = '.Animenosub'
|
extClass = '.Animenosub'
|
||||||
themePkg = 'animestream'
|
themePkg = 'animestream'
|
||||||
baseUrl = 'https://animenosub.com'
|
baseUrl = 'https://animenosub.com'
|
||||||
overrideVersionCode = 8
|
overrideVersionCode = 10
|
||||||
isNsfw = true
|
isNsfw = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -129,7 +129,7 @@ class AnimeOwl : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||||
val document = response.asJsoup()
|
val document = response.asJsoup()
|
||||||
val sub = document.select("#anime-cover-sub-content .episode-node").mapIndexed { idx, it ->
|
val sub = document.select("#anime-cover-sub-content .episode-node").mapIndexed { idx, it ->
|
||||||
EpisodeResponse.Episode(
|
EpisodeResponse.Episode(
|
||||||
id = idx.toDouble(),
|
id = it.attr("title").toDoubleOrNull(),
|
||||||
episodeIndex = idx.toString(),
|
episodeIndex = idx.toString(),
|
||||||
name = it.attr("title"),
|
name = it.attr("title"),
|
||||||
lang = "Sub",
|
lang = "Sub",
|
||||||
|
@ -138,7 +138,7 @@ class AnimeOwl : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||||
}
|
}
|
||||||
val dub = document.select("#anime-cover-dub-content .episode-node").mapIndexed { idx, it ->
|
val dub = document.select("#anime-cover-dub-content .episode-node").mapIndexed { idx, it ->
|
||||||
EpisodeResponse.Episode(
|
EpisodeResponse.Episode(
|
||||||
id = idx.toDouble(),
|
id = it.attr("title").toDoubleOrNull(),
|
||||||
episodeIndex = idx.toString(),
|
episodeIndex = idx.toString(),
|
||||||
name = it.attr("title"),
|
name = it.attr("title"),
|
||||||
lang = "Dub",
|
lang = "Dub",
|
||||||
|
@ -301,6 +301,6 @@ class AnimeOwl : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||||
private const val PREF_QUALITY_KEY = "preferred_quality"
|
private const val PREF_QUALITY_KEY = "preferred_quality"
|
||||||
private const val PREF_QUALITY_TITLE = "Preferred quality"
|
private const val PREF_QUALITY_TITLE = "Preferred quality"
|
||||||
private const val PREF_QUALITY_DEFAULT = "1080p"
|
private const val PREF_QUALITY_DEFAULT = "1080p"
|
||||||
private val PREF_QUALITY_LIST = arrayOf("2K", "1080p", "720p", "480p", "360p")
|
private val PREF_QUALITY_LIST = arrayOf("1080p", "720p", "480p", "360p")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -48,13 +48,8 @@ class OwlExtractor(private val client: OkHttpClient, private val baseUrl: String
|
||||||
luffy.forEach { stream ->
|
luffy.forEach { stream ->
|
||||||
noRedirectClient.newCall(GET("${stream.url}$jwt")).execute()
|
noRedirectClient.newCall(GET("${stream.url}$jwt")).execute()
|
||||||
.use { it.headers["Location"] }?.let {
|
.use { it.headers["Location"] }?.let {
|
||||||
val resolution = when {
|
|
||||||
stream.resolution?.endsWith("0") == true -> "${stream.resolution}p"
|
|
||||||
else -> stream.resolution
|
|
||||||
}
|
|
||||||
|
|
||||||
videoList.add(
|
videoList.add(
|
||||||
Video(it, "${link.lang} Luffy:${resolution ?: "Unknown"}", it),
|
Video(it, "${link.lang} Luffy:${stream.resolution}", it),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -88,10 +83,7 @@ class OwlExtractor(private val client: OkHttpClient, private val baseUrl: String
|
||||||
return client.newCall(GET(url)).execute().let { it ->
|
return client.newCall(GET(url)).execute().let { it ->
|
||||||
if (it.isSuccessful) {
|
if (it.isSuccessful) {
|
||||||
it.parseAs<Stream>().url.let {
|
it.parseAs<Stream>().url.let {
|
||||||
playlistUtils.extractFromHls(
|
playlistUtils.extractFromHls(it, videoNameGen = { qty -> "$lang $server:$qty" })
|
||||||
it,
|
|
||||||
videoNameGen = { qty -> "$lang $server:$qty" },
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
emptyList()
|
emptyList()
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
ext {
|
ext {
|
||||||
extName = 'AnimePahe'
|
extName = 'AnimePahe'
|
||||||
extClass = '.AnimePahe'
|
extClass = '.AnimePahe'
|
||||||
extVersionCode = 29
|
extVersionCode = 31
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
|
|
@ -153,7 +153,16 @@ class AnimePahe : ConfigurableAnimeSource, AnimeHttpSource() {
|
||||||
override fun episodeListParse(response: Response): List<SEpisode> {
|
override fun episodeListParse(response: Response): List<SEpisode> {
|
||||||
val url = response.request.url.toString()
|
val url = response.request.url.toString()
|
||||||
val session = url.substringAfter("&id=").substringBefore("&")
|
val session = url.substringAfter("&id=").substringBefore("&")
|
||||||
return recursivePages(response, session)
|
val episodeList = recursivePages(response, session)
|
||||||
|
|
||||||
|
return episodeList
|
||||||
|
.sortedBy { it.date_upload } // Optional, makes sure it's in correct order
|
||||||
|
.mapIndexed { index, episode ->
|
||||||
|
episode.episode_number = (index + 1).toFloat()
|
||||||
|
episode.name = "Episode ${index + 1}"
|
||||||
|
episode
|
||||||
|
}
|
||||||
|
.reversed()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun parseEpisodePage(episodes: List<EpisodeDto>, animeSession: String): MutableList<SEpisode> {
|
private fun parseEpisodePage(episodes: List<EpisodeDto>, animeSession: String): MutableList<SEpisode> {
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
ext {
|
ext {
|
||||||
extName = 'AnimeTake'
|
extName = 'AnimeTake'
|
||||||
extClass = '.AnimeTake'
|
extClass = '.AnimeTake'
|
||||||
extVersionCode = 7
|
extVersionCode = 9
|
||||||
}
|
}
|
||||||
|
|
||||||
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 = 15
|
overrideVersionCode = 20
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue