Compare commits

...
Sign in to create a new pull request.

117 commits

Author SHA1 Message Date
837c1826ff Update local.properties 2025-05-22 04:40:53 -05:00
e5e8761230 Update local.properties 2025-05-22 04:35:52 -05:00
ce3522ff69 Update build.gradle.kts 2025-05-22 00:07:39 -05:00
0295eaef40 Update build.gradle.kts 2025-05-22 00:05:25 -05:00
c96032233f Update .github/workflows/build_push.yml 2025-05-21 23:59:07 -05:00
a2f7b2a077 Add local.properties 2025-05-21 23:58:33 -05:00
584c2e6b53 Update .github/workflows/build_push.yml 2025-05-21 23:54:08 -05:00
a10e63df38 Upload files to "SDK/licenses" 2025-05-21 23:49:46 -05:00
09533654d5 Update .github/workflows/build_push.yml 2025-05-21 23:43:02 -05:00
ec77adae21 Upload files to "SDK/lib64" 2025-05-21 23:41:52 -05:00
5167cba9bf Upload files to "SDK" 2025-05-21 23:41:25 -05:00
31d422a8ca Upload files to "SDK" 2025-05-21 23:41:10 -05:00
2d87503f99 Upload files to "SDK" 2025-05-21 23:40:46 -05:00
8ef18b236a Update settings.gradle.kts 2025-05-21 13:16:11 -05:00
89c957d1ec Update .github/workflows/build_push.yml 2025-05-21 10:07:15 -05:00
a873046b39 Update .github/workflows/build_push.yml 2025-05-21 10:02:40 -05:00
015eb36372 Update .github/workflows/build_push.yml 2025-05-21 09:57:29 -05:00
3ffb365748 Update .github/workflows/build_push.yml 2025-05-21 09:49:48 -05:00
67672ea590 Update .github/workflows/build_push.yml 2025-05-21 09:47:23 -05:00
afccef1e7d Update .github/workflows/build_push.yml 2025-05-21 09:45:06 -05:00
4158a18ba6 Update .github/workflows/build_push.yml 2025-05-21 09:38:35 -05:00
3db3d04443 Update .github/workflows/build_push.yml 2025-05-21 09:36:43 -05:00
8738e281d8 Update .github/workflows/build_push.yml 2025-05-21 09:35:18 -05:00
808ad21843 Update settings.gradle.kts 2025-05-21 09:33:30 -05:00
7d7aca37d8 Update settings.gradle.kts 2025-05-21 09:29:01 -05:00
0c2df7e6af Update settings.gradle.kts 2025-05-21 09:27:46 -05:00
1cfee212d7 Update settings.gradle.kts 2025-05-21 09:26:16 -05:00
78b864fc50 Update .github/workflows/build_push.yml 2025-05-21 09:24:44 -05:00
8223b8d3fa Update .github/workflows/build_push.yml 2025-05-21 09:18:28 -05:00
2904ce54e9 Update .github/workflows/build_push.yml 2025-05-21 09:11:35 -05:00
d1a72a27c6 Update .github/workflows/build_push.yml 2025-05-21 09:10:00 -05:00
724c661a25 Update .github/workflows/build_push.yml 2025-05-21 09:09:13 -05:00
0c844c9649 Update .github/workflows/build_push.yml 2025-05-21 09:07:48 -05:00
1b9fabf062 Update settings.gradle.kts 2025-05-21 09:03:48 -05:00
0fc8a33f3e Update settings.gradle.kts 2025-05-21 09:02:27 -05:00
627c34f404 Update .github/workflows/build_push.yml 2025-05-21 09:01:35 -05:00
1de715339a Update settings.gradle.kts 2025-05-21 08:55:57 -05:00
f789a38449 Update settings.gradle.kts 2025-05-21 08:54:34 -05:00
4782731860 Update .github/workflows/build_push.yml 2025-05-21 08:46:45 -05:00
ed7858a223 Update .github/workflows/build_push.yml 2025-05-21 08:42:44 -05:00
51b027fd78 Update .github/workflows/build_push.yml 2025-05-21 08:41:44 -05:00
ea5dd44967 Update .github/workflows/build_push.yml 2025-05-21 08:36:26 -05:00
2cc07ffe89 Update .github/workflows/build_push.yml 2025-05-21 08:34:48 -05:00
4f86e01e36 Update .github/workflows/build_pull_request.yml 2025-05-21 08:33:27 -05:00
46fe6d9c11 Update .github/workflows/build_pull_request.yml 2025-05-21 08:32:10 -05:00
4f7456323d Update .github/workflows/build_push.yml 2025-05-21 08:07:16 -05:00
fc37330c4f Update .github/workflows/build_push.yml 2025-05-21 08:01:52 -05:00
14132789f4 Update .github/workflows/build_push.yml 2025-05-21 07:53:06 -05:00
1b8adc6b13 Update .github/workflows/build_push.yml 2025-05-21 07:47:09 -05:00
5d6c3fff93 Update .github/workflows/build_push.yml 2025-05-21 06:46:51 -05:00
db54be041e Update .github/workflows/build_push.yml 2025-05-21 06:42:42 -05:00
40570cf6ae Update .github/workflows/build_push.yml 2025-05-21 06:39:18 -05:00
8fd317320e Update .github/workflows/build_push.yml 2025-05-21 06:36:56 -05:00
9e592e90ab Update .github/workflows/build_push.yml 2025-05-21 02:03:45 -05:00
0bddcda190 Update .github/workflows/build_push.yml 2025-05-21 00:30:04 -05:00
f096796f42 Update README.md 2025-05-20 03:50:22 -05:00
709104f0b9 Merge pull request 'balls' (#1009) from balls into main
Reviewed-on: http://192.168.1.99/AlmightyHak/extensions-source/pulls/1009
2025-05-20 00:48:06 -05:00
e029421762 Update .github/workflows/lock.yml 2025-05-20 00:46:55 -05:00
a95087c637 Update .github/workflows/issue_moderator.yml 2025-05-20 00:45:55 -05:00
49400c0ceb Update .github/workflows/build_push.yml 2025-05-20 00:41:27 -05:00
75c7bf2d22 Update .github/workflows/build_pull_request.yml 2025-05-20 00:39:39 -05:00
fd8a3c4619 Update .github/workflows/batch_close_issues.yml 2025-05-20 00:38:16 -05:00
Arkai1
38368f56ac
AniWatch (#999)
* Update build.gradle

* Update build.gradle

* Update and rename AniWatch.kt to AniWatchtv.kt

* Update AniWatchtv.kt
2025-05-12 11:15:15 +02:00
Arkai1
66ca8544d3
Added Aniwatchtv (#998)
* Added Aniwatchtv

* Added newline

* Fixed class

* Update AniWatch.kt
2025-05-11 19:12:11 +02:00
Joshua S
edd2fba397
Added new extension JPFilms.kt (#978) 2025-05-09 14:15:07 +07:00
Josef František Straka
c82b40bb41
fix(Aniplay): changed proxy url (#996)
* fix

* version bump

* removed redundant curly braces
2025-05-09 14:05:06 +07:00
Arkai1
26dbc15b45
(ar/witanime) Domain Change (#985)
* Update WitAnime.kt

* Update build.gradle
2025-05-06 17:08:11 +02:00
Sadwhy
82a8b81de2
Update hikari with minor fixes. Buzzheavier extractor less prone to failure. (#986)
* Tighter extraction

* Bump version

* More details

* Removed unchecked cast
2025-05-06 16:58:29 +02:00
V3u47ZoN
a4d3a117cf
Fix playlist-utils (#980)
* dont standardize qualities for everything

* bump all ext depending directly or indirectly on playlist-utils
2025-05-03 15:55:17 +02:00
V3u47ZoN
dd28b05f01
Update chillx extractor (#979)
* Update chillxextractor

* bump exts using chillx
2025-05-03 12:20:12 +02:00
Arkai1
36a480cd46
(en/wcostream) Domain changed and fixed search issue (#973)
* Update WCOStream.kt

* Update build.gradle
2025-05-02 16:41:15 +02:00
Zero
b93c551b30
fixed streamingcommunity domain (#972)
* Fixed the episode number problem for animepahe

* Changed hianime domain to hianime.bz

* Bump AnimePahe extVersionCode to 30 for improved episode sorting and numbering

* Update HiAnime.kt

* Changed streaming community domain

* Bumped to 6

---------

Co-authored-by: GraveEaterMadison <GraveEaterMadison@users.noreply.github.com>
2025-05-02 09:12:57 +02:00
kana-shii
b490d21610
AnimePahe: Reverse episode order (#940)
* Update AnimePahe.kt

* Update build.gradle

* Update AnimePahe.kt
2025-05-01 19:09:49 +02:00
Sadwhy
5d902c3576
(all/hikari) provider selection and hiki mirrors (#970)
* Update Hikari.kt

* Update build.gradle
2025-05-01 16:11:46 +02:00
V3u47ZoN
2f53d6b581
fix buzzheavier (#969) 2025-05-01 13:47:41 +02:00
Arkai1
7b73c023a6
Added more quality preferences in Dooplay (#968)
* Update DooPlay.kt

* Update build.gradle.kts
2025-05-01 13:39:32 +02:00
V3u47ZoN
821cbc1d59
Fix hikari (#963)
* add hikari

* mass bump due for extractor changes
2025-05-01 12:16:57 +02:00
V3u47ZoN
45cff438ce
bump aniplay & exts using zorotheme (#954)
* bump exts using zorotheme

* Update build.gradle
2025-04-29 18:05:01 +02:00
Arkai1
0f2d3adc46
Added 2025 filter and set preferred Domain to .mx (#953)
* Update KickAssAnime.kt

* Update KickAssAnimeFilters.kt

* Update build.gradle
2025-04-29 14:52:07 +02:00
Arkai1
f568df679e
(hi/animesaga) Updated Logo (#955)
* Add files via upload

* Add files via upload

* Add files via upload

* Add files via upload

* Add files via upload

* Update build.gradle

* Update AniSAGA.kt
2025-04-29 14:51:49 +02:00
V3u47ZoN
f57e2ea5af
Kickassanime & Hianime: Fix subtitles (#937)
* Fix subtitles

* Update PlaylistUtils.kt

* Update MegaCloudExtractor.kt

* Update AniPlay.kt

* Update MegaCloudExtractor.kt

* Fix regex

* Fix subtitles for kickassanime

* Update build.gradle

* Update build.gradle

* Update build.gradle.kts

* Update build.gradle.kts

* Update build.gradle

* Update build.gradle
2025-04-24 13:53:03 +02:00
krysanify
ac1938c1e4
RIP Big 3: FMovies, DramaCool, GogoAnime/Anitaku (#917)
* src(en/dramacool) dead

* src(en/fmovies) dead

* src(en/gogoanime) dead
2025-04-18 08:34:25 +07:00
GregiPlays
6461450cf5
Merge pull request #918 from Arkai1/Hianime
(en/zoro) Some changes and fixes
2025-04-17 09:05:28 +02:00
Arkai1
71c9d5d7ea
Update HiAnime.kt 2025-04-17 12:12:00 +05:30
Arkai1
48dd054ccd
Update HiAnime.kt 2025-04-17 12:04:01 +05:30
Arkai1
c59029cada
Update HiAnime.kt 2025-04-17 11:48:40 +05:30
Arkai1
7779fa173e
Update build.gradle 2025-04-17 11:16:02 +05:30
Arkai1
e240125371
Update HiAnime.kt 2025-04-17 11:15:30 +05:30
GregiPlays
f21489af9b
Merge pull request #912 from Arkai1/Hianime-fix
(en/zoro) Hianime again changed its domain added proxies for Hianime
2025-04-16 19:28:23 +02:00
GregiPlays
1b10911a8b
Merge pull request #895 from Arkai1/Torrentio-Anime
(all/torrentioanime) Fixing Some episode not shown due to ONA and OVA class
2025-04-16 19:17:52 +02:00
Arkai1
626ac52a05
Nevermind this file 2025-04-16 18:26:53 +05:30
Arkai1
ed7be63bbc
Update hianime-build.yml 2025-04-16 18:22:40 +05:30
Arkai1
334745ef2d
Update hianime-build.yml 2025-04-16 18:15:52 +05:30
Arkai1
8ae130841e
Create hianime-build.yml 2025-04-16 18:14:17 +05:30
Arkai1
1aa0084dd9
Update src/en/zoro/src/eu/kanade/tachiyomi/animeextension/en/zoro/HiAnime.kt
Co-authored-by: krysanify <chrispermana@gmail.com>
2025-04-16 14:07:56 +05:30
Arkai1
49e5558f19
Update HiAnime.kt 2025-04-16 13:54:39 +05:30
Arkai1
160f5531dd
Update HiAnime.kt 2025-04-16 13:49:25 +05:30
Arkai1
c4ff62639d
Update build.gradle 2025-04-16 13:12:30 +05:30
Arkai1
f9f86e46ae
Update HiAnime.kt 2025-04-16 13:10:23 +05:30
GregiPlays
2b1e2a14f1
Merge pull request #909 from Arkai1/Hianime-fix
(en/zoro) Hianime is back with official Domain changes source link
2025-04-16 04:29:06 +02:00
Arkai1
19127e1c07
Update build.gradle 2025-04-15 22:01:17 +05:30
Arkai1
a3bb5a57bd
Update HiAnime.kt 2025-04-15 22:00:39 +05:30
wasu
9e36c94090
Add Newgrounds.com (#877)
* new source: NewGrounds

* add icons

* add search and search filters

* display toast when Adult Content was filtered (login required)

* better sorting filter; let user adjust description

* if playlist available treat as series

* make sure anime is part of series (not playlist or collection)

* remove hi_res image

* remove unnecessary comment

* add NSFW flag

* simplify episode url extraction

* adjust stats summary in description

* use baseUrl in Referer header

* eliminate the need for restart after changing preferences
2025-04-11 09:31:27 +07:00
Arkai1
06b67ac120
Update Torrentio.kt 2025-04-10 20:24:56 +05:30
Arkai1
a099d6abcf
Update Torrentio.kt 2025-04-10 20:23:38 +05:30
Arkai1
96cb8d34c9
(en/zoro) Fixed Hianime with new domain (#897)
* Update HiAnime.kt

* Update build.gradle
2025-04-10 21:36:19 +07:00
Arkai1
07b096361a
Update build.gradle 2025-04-10 09:46:44 +05:30
Arkai1
6a7137b1d0
Update Torrentio.kt 2025-04-10 09:41:19 +05:30
Josef František Straka
235c2b3f41
fix(en/Aniplay): working again and header fetching (#894)
* header fetching

* version bump

* modified video fetching

* longer delay

* better header fetching

* video headers and minor changes

* version bump
2025-04-10 09:02:53 +07:00
Arkai1
686405f61f
Domain Change (en/AnimeKhor) .xyz to .org (#868)
* Update AnimeKhor.kt

* Update build.gradle
2025-04-10 00:48:38 +07:00
Arkai1
6c38697f19
(en/kickassanime) Adding kaa.mx (#867)
* Update KickAssAnime.kt

* Update build.gradle
2025-04-10 00:48:26 +07:00
Hak
37d08b9c8a mass bump (#893) 2025-04-10 00:45:35 +07:00
Arkai1
f2cd1223b8
Added 2025 and 2026 Year Filter for HiAnime and Aniplay (#891)
* Update AniListFilters.kt

* Update ZoroThemeFilters.kt

* Update build.gradle.kts

* Update build.gradle.kts
2025-04-10 00:27:44 +07:00
Zero
4df27e2211
Fix AnimePahe episode sorting and numbering logic (#881)
* Fixed the episode number problem for animepahe

* Changed hianime domain to hianime.bz

* Bump AnimePahe extVersionCode to 30 for improved episode sorting and numbering

* Update HiAnime.kt

---------

Co-authored-by: GraveEaterMadison <GraveEaterMadison@users.noreply.github.com>
2025-04-10 00:27:31 +07:00
Arkai1
07b02f4489
Update MegaCloudExtractor.kt (#890) 2025-04-10 00:21:52 +07:00
Josef František Straka
e1d6459e4f
new validate gradle wrapper (#872) 2025-04-06 20:43:01 +02:00
Cezary
8d9e763dc4
fix(lib/lycoris&lulu) Repair decode json and work LuluStream (#810)
* fix(lib/lycoris): fix parse json

* fix(lib/lycoris): small changes

* fix(lib/lycoris): small changes v2

* fix(lib/lycoris): small changes v3

* fix(lib/lycoris): small changes v4

* fix(lib/lycoris): small changes v5

* fix(lib/lycoris&lulu): big change v1

* fix(lib/lycoris&lulu): small change v2

* fix(lib/lycoris&lulu): small change v3

* fix(lib/lycoris&lulu&docchi): small change v4

* fix(pl/docchi): tiny change v1

* fix(lib/lulu): tiny change v2
2025-04-06 20:41:23 +02:00
259 changed files with 23519 additions and 2683 deletions

View file

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

View file

@ -18,15 +18,12 @@ env:
jobs: jobs:
prepare: prepare:
name: Prepare job name: Prepare job
runs-on: ubuntu-latest runs-on: docker
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 +34,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 +49,21 @@ jobs:
build_individual: build_individual:
name: Build individual modules name: Build individual modules
needs: prepare needs: prepare
runs-on: ubuntu-latest runs-on: docker
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 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

View file

@ -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,17 +80,17 @@ 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
@ -103,7 +100,7 @@ jobs:
echo ${{ secrets.SIGNING_KEY }} | base64 -d > signingkey.jks echo ${{ secrets.SIGNING_KEY }} | base64 -d > signingkey.jks
- name: Set up Gradle - name: Set up Gradle
uses: 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,7 +111,7 @@ jobs:
run: chmod +x ./gradlew && ./gradlew -p src assembleRelease run: chmod +x ./gradlew && ./gradlew -p src assembleRelease
- name: Upload APKs (chunk ${{ matrix.chunk }}) - name: Upload APKs (chunk ${{ matrix.chunk }})
uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4 uses: https://github.com/actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4
if: "github.repository == 'Kohi-den/extensions-source'" if: "github.repository == 'Kohi-den/extensions-source'"
with: with:
name: "individual-apks-${{ matrix.chunk }}" name: "individual-apks-${{ matrix.chunk }}"
@ -128,22 +125,22 @@ 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: Download APK artifacts - name: Download APK artifacts
uses: actions/download-artifact@eaceaf801fd36c7dee90939fad912460b18a1ffe # v4 uses: https://github.com/actions/download-artifact@eaceaf801fd36c7dee90939fad912460b18a1ffe # v4
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: 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 +155,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 +167,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

View file

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

View file

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

View file

@ -2,9 +2,9 @@
just paste this into your anime repo just paste this into your anime repo
``` ```
https://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

File diff suppressed because it is too large Load diff

BIN
SDK/adb Normal file

Binary file not shown.

BIN
SDK/etc1tool Normal file

Binary file not shown.

BIN
SDK/fastboot Normal file

Binary file not shown.

BIN
SDK/hprof-conv Normal file

Binary file not shown.

BIN
SDK/lib64/libc++.so Normal file

Binary file not shown.

View file

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

BIN
SDK/make_f2fs Normal file

Binary file not shown.

BIN
SDK/make_f2fs_casefold Normal file

Binary file not shown.

BIN
SDK/mke2fs Normal file

Binary file not shown.

53
SDK/mke2fs.conf Normal file
View 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
View file

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

BIN
SDK/sqlite3 Normal file

Binary file not shown.

View file

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

View file

@ -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"),

View file

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

View file

@ -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

View file

@ -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"))

View file

@ -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()

View file

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

View file

@ -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)
}

View file

@ -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)

View file

@ -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()),
)
}
}

View file

@ -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"
}
} }

View file

@ -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 extraParams = mutableMapOf( val paramDict = mutableMapOf<String, String>()
"i" to "0.3", val extraParams = mutableMapOf<String, String>()
"sp" to "0"
) params.forEachIndexed { index, (key , value) ->
if (key.isNullOrEmpty()) {
params.forEachIndexed { index, param -> if (index < paramOrder.size) {
val parts = param.split("=") if (value != null) {
when { paramDict[paramOrder[index]] = value
parts.size == 2 -> { }
val (key, value) = parts }
if (key in paramOrder) paramMap[key] = value } else {
else extraParams[key] = value 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

View file

@ -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,7 +119,7 @@ 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) {
@ -130,22 +128,24 @@ class LycorisCafeExtractor(private val client: OkHttpClient) {
val finalText = unicodeEscape.toByteArray(Charsets.ISO_8859_1).toString(Charsets.UTF_8) val finalText = unicodeEscape.toByteArray(Charsets.ISO_8859_1).toString(Charsets.UTF_8)
url = GETLNKURL.toHttpUrl().newBuilder() url = GETLNKURL.toHttpUrl().newBuilder()
?.addQueryParameter("link", finalText) .addQueryParameter("link", finalText)
?.build() ?: 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)) client.newCall(GET(url))
.execute() .execute()
.use { response -> .use { response ->
val data = response.body.string() ?: "" val data = response.body.string()
return decodeVideoLinks(data) return decodeVideoLinks(data)
} }
} }
private fun checkLinks(client: OkHttpClient, link: String): Boolean { private fun checkLinks(client: OkHttpClient, link: String): Boolean {
if (!link.contains("https://")) return false
client.newCall(GET(link)).execute().use { response -> client.newCall(GET(link)).execute().use { response ->
return response.code.toString() == "200" return response.code.toString() == "200"
} }
@ -155,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
) )
} }

View file

@ -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]

View file

@ -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" + "&nbsp;\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="(.*?)"""") }

View file

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

View file

@ -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"
}
}

View file

@ -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)(?!["])""")
} }

View file

@ -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() }

1
local.properties Normal file
View file

@ -0,0 +1 @@
sdk.dir=/workspace/AlmightyHak/extensions-source/SDK

View file

@ -19,8 +19,8 @@ if (System.getenv("CI") != "true") {
} else { } else {
// Running in CI (GitHub Actions) // Running in CI (GitHub Actions)
val chunkSize = System.getenv("CI_CHUNK_SIZE").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 {

View file

@ -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"

View file

@ -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"

View file

@ -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"

View file

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

View file

@ -1,7 +1,7 @@
ext { ext {
extName = 'Hikari' extName = 'Hikari'
extClass = '.Hikari' extClass = '.Hikari'
extVersionCode = 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'))
} }

View file

@ -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,
)

View file

@ -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 }
if (checked.isNotEmpty()) {
builder.addQueryParameter(param, checked.joinToString(",") { it.value }) builder.addQueryParameter(param, checked.joinToString(",") { it.value })
} }
} }
}
class TypeFilter : UriPartFilter( class TypeFilter : UriPartFilter(
"Type", "Type",
"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())
} },
}
}
}
class MonthFilter(name: String, param: String) : UriPartFilter(
name,
param,
MONTHS,
) {
companion object {
private val MONTHS = Array(13) { months ->
if (months == 0) {
Pair("Any", "")
} else {
val monthStr = "%02d".format(months)
Pair(monthStr, monthStr)
}
}
}
}
class DayFilter(name: String, param: String) : UriPartFilter(
name,
param,
DAYS,
) {
companion object {
private val DAYS = Array(32) { day ->
if (day == 0) {
Pair("Any", "")
} else {
val dayStr = "%02d".format(day)
Pair(dayStr, dayStr)
}
}
}
}
class AiringDateFilter(
private val values: List<UriPartFilter> = PARTS,
) : AnimeFilter.Group<UriPartFilter>("Airing Date", values), UriFilter {
override fun addToUri(builder: HttpUrl.Builder) {
values.forEach {
it.addToUri(builder)
}
}
companion object {
private val PARTS = listOf(
YearFilter("Year", "aired_year"),
MonthFilter("Month", "aired_month"),
DayFilter("Day", "aired_day"),
) )
}.toTypedArray()
} }
} }
class GenreFilter : UriMultiSelectFilter( class GenreFilter : UriMultiSelectFilter(
"Genre", "Genre",
"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"),
),
)

View file

@ -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())
} else {
addPathSegment("ajax")
addPathSegment("getfilter")
filters.filterIsInstance<UriFilter>().forEach { filters.filterIsInstance<UriFilter>().forEach {
it.addToUri(this) it.addToUri(this)
} }
addQueryParameter("page", page.toString())
}
}.build()
val headers = headersBuilder().apply {
if (query.isNotEmpty()) { if (query.isNotEmpty()) {
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)
} }
} }

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,8 @@
ext {
extName = 'Newgrounds'
extClass = '.NewGrounds'
extVersionCode = 1
isNsfw = true
}
apply from: "$rootDir/common.gradle"

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View file

@ -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
}
}

View file

@ -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",
)
}
}

View file

@ -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",
)

View file

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

View file

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

View file

@ -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
} }

View file

@ -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)) {

View file

@ -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"

View file

@ -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"

View file

@ -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"

View file

@ -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"

View file

@ -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"

View file

@ -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"

View file

@ -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"

View file

@ -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"

View file

@ -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"

View file

@ -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"

View file

@ -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"

View file

@ -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"

View file

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

View file

@ -1,7 +1,7 @@
ext { ext {
extName = 'Anime-Base' extName = 'Anime-Base'
extClass = '.AnimeBase' extClass = '.AnimeBase'
extVersionCode = 31 extVersionCode = 33
isNsfw = true isNsfw = true
} }

View file

@ -1,7 +1,7 @@
ext { ext {
extName = 'Anime-Loads' extName = 'Anime-Loads'
extClass = '.AnimeLoads' extClass = '.AnimeLoads'
extVersionCode = 17 extVersionCode = 18
isNsfw = true isNsfw = true
} }

View file

@ -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"

View file

@ -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"

View file

@ -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
} }

View file

@ -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"

View file

@ -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"

View file

@ -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"

View file

@ -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"

View file

@ -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"

View file

@ -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"

View file

@ -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"

View file

@ -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"

View file

@ -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"

View file

@ -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"

View file

@ -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 =============================

View file

@ -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
} }

View file

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

View file

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

View file

@ -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> {

View file

@ -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"

View file

@ -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