Compare commits
337 Commits
37334289a1
...
93d81ea038
Author | SHA1 | Date |
---|---|---|
Sofie | 93d81ea038 | |
Sofie | e007714701 | |
KingLucius | 805f80b2ac | |
KingLucius | b5fb0997c4 | |
KingLucius | ca918b1581 | |
IndusAryan | 09779b4ee0 | |
Sofie | 012d38398e | |
Ömer Faruk Sancak | d1db4c3370 | |
Sarlay | 8d318ca84a | |
KingLucius | eea6e13346 | |
CranberrySoup | 2b7d102716 | |
Osten | 9ea7674a0f | |
IndusAryan | 3dcf7076d0 | |
Sofie | 8b14fcb881 | |
IndusAryan | 01f21e0fe8 | |
coxju | bdef6524e7 | |
Cloudburst | f40a8d9418 | |
IndusAryan | 03fcb106ac | |
coxju | 636e157c63 | |
CranberrySoup | 5af1b80cb7 | |
coxju | 5dfc08aabb | |
coxju | 1676094488 | |
Sir Aguacata | 19145c6cc4 | |
coxju | ebb72d6a0c | |
Sir Aguacata | 399b28c75b | |
IndusAryan | 601483e103 | |
recloudstream[bot] | 9733d0b316 | |
Weblate (bot) | 0cf199248a | |
Sofie | 2624947b5b | |
coxju | 31c783d0b4 | |
firelight | 9f1b172f34 | |
IndusAryan | 93dce8682e | |
IndusAryan | 723c653b07 | |
coxju | 0c73f5e59a | |
coxju | 0eb152c5db | |
IndusAryan | 8c5ab86714 | |
Ömer Faruk Sancak | 85a769a898 | |
Sir Aguacata | 96aa56209b | |
coxju | d71d3890b5 | |
IndusAryan | 19b1a40cf8 | |
Yutatsu | e5f483b0b2 | |
coxju | 6f1e0bef80 | |
LagradOst | 5e6272be3f | |
coxju | 97ec98b9e2 | |
coxju | 42fd0b5c76 | |
Ömer Faruk Sancak | 42774f6183 | |
Sofie | f687508521 | |
recloudstream[bot] | dbba6d7f27 | |
Hosted Weblate | f4da170a57 | |
Cloudburst | 2a1876f54c | |
wrongwrong | f1d0a8e955 | |
IndusAryan | 1c6be2d5cb | |
Horis | fc802cdcdd | |
Ömer Faruk Sancak | 2cfdab5498 | |
recloudstream[bot] | 4c2ee28d5a | |
firelight | 657f2fbcb2 | |
Hosted Weblate | b5ac668493 | |
Sofie | 9d3b2ba3d2 | |
Osten | 5f51a8f7bc | |
LagradOst | e886fde8b8 | |
Sofie | 1356a954f3 | |
firelight | 3d90af29eb | |
recloudstream[bot] | 2a4ce89452 | |
firelight | 0543f1ffae | |
coxju | a5f7920bca | |
Hosted Weblate | e8fe2944bb | |
Luna712 | db91552f39 | |
Luna712 | 484c21cc1c | |
Sofie | ff9144ef54 | |
Luna712 | 10a477c2bd | |
firelight | 6d51c59b18 | |
Sofie | f98ce0558d | |
firelight | f5e6d98cb0 | |
coxju | 91dc83e6a3 | |
IndusAryan | fe30a85a1c | |
yueehaoo | 6e5a52e440 | |
coxju | 410cedc128 | |
Luna712 | 3c152e04d1 | |
Osten | d0aed5e51a | |
Funny-Pen-7005 | 530619c8d0 | |
firelight | 3ef8f3030c | |
Funny-Pen-7005 | 2d87983eca | |
Sofie | 6f3a8c1cd2 | |
recloudstream[bot] | dfd6ce7651 | |
firelight | 88ad64b3b0 | |
Hosted Weblate | cebdbd2199 | |
IndusAryan | 25b042fb83 | |
Hosted Weblate | fac0ef4c25 | |
IndusAryan | f7bc83024a | |
Hosted Weblate | 5b170c0573 | |
firelight | 38cc121755 | |
Hosted Weblate | 951b2110ad | |
IndusAryan | d4aefc4e64 | |
Hosted Weblate | c324eaf543 | |
recloudstream[bot] | 962ff1c058 | |
firelight | 7165b57268 | |
Hosted Weblate | e80dc63381 | |
firelight | fa7ebc05b3 | |
IndusAryan | df0122c146 | |
IndusAryan | b49368100b | |
Sofie | 0077cebaa6 | |
IndusAryan | a2085202ec | |
CranberrySoup | 765071ebef | |
self-similarity | e11d36aed8 | |
Luna712 | 5bf2b4ead2 | |
Cloudburst | de61501b22 | |
Cloudburst | 685884e67f | |
Luna712 | 6db295a799 | |
self-similarity | 2b60e3a893 | |
Luna712 | 3adf036135 | |
IndusAryan | c4aab5e5a8 | |
KingLucius | 7e2908c0bb | |
Sofie | 22a0c25d83 | |
self-similarity | 11136fe63d | |
Luna712 | a6786aaf98 | |
Luna712 | 5b0cbbf09f | |
IndusAryan | 6a8c251013 | |
KingLucius | 908f83c50e | |
recloudstream[bot] | 6f40d2750f | |
Weblate (bot) | 199f5b3a9d | |
KingLucius | 6ce9f29331 | |
firelight | 8b73c35e43 | |
Luna712 | 65313b4579 | |
IndusAryan | a8fdf5e8f2 | |
Luna712 | 87c5aada8f | |
Luna712 | f0e429436f | |
self-similarity | 137d833d4a | |
Luna712 | b2e0b7dec8 | |
Luna712 | d542febcda | |
Luna712 | f0ebfa47c8 | |
Luna712 | 51a877f405 | |
Luna712 | 4b93524e57 | |
firelight | ef36bccc90 | |
Luna712 | 968bd59188 | |
Luna712 | e4ba852007 | |
Luna712 | 504258bf15 | |
KingLucius | 48053164dc | |
Luna712 | 2a4468eb44 | |
Luna712 | 5153a74d4f | |
KingLucius | 138dea88c4 | |
IndusAryan | eb58cb1184 | |
firelight | c9bffef7cb | |
IndusAryan | a7a6f2282a | |
Luna712 | 8ed7418fe4 | |
Luna712 | 3cb2196e62 | |
KingLucius | 7e9d1ded7f | |
Luna712 | b7322ffb19 | |
CranberrySoup | fd1620f3d7 | |
KingLucius | fc8c0e809d | |
self-similarity | 1ccd3d732d | |
LagradOst | bb6a17e23c | |
recloudstream[bot] | 2f2bbd7d88 | |
Sir Aguacata | 749d131099 | |
Weblate (bot) | de6dfec452 | |
LagradOst | b4da93c1de | |
LagradOst | dd45ac9e8a | |
LagradOst | b47209483a | |
self-similarity | 91b195241e | |
KingLucius | abbad1bc94 | |
KingLucius | b120a7bce2 | |
Luna712 | 5b4fd8d77d | |
LagradOst | d277d8a9aa | |
LagradOst | 33eb3a3b29 | |
LagradOst | f14557fe6a | |
LagradOst | 77294dc68e | |
Luna712 | 0a327ccbda | |
LagradOst | 177b1e47f3 | |
Luna712 | 3f5119525c | |
Luna712 | b5d4c3bd27 | |
Luna712 | cc00e73e16 | |
Luna712 | 462073bd74 | |
LagradOst | 08060314ad | |
Luna712 | 1d90858f64 | |
LagradOst | bd05a67f26 | |
KingLucius | bb8cbb5167 | |
KingLucius | 16c2290090 | |
KingLucius | 194678c419 | |
Osten | 0351053d80 | |
Sofie | 74060e7da3 | |
IndusAryan | 1e2a11d6e4 | |
CranberrySoup | b8917ffa39 | |
CranberrySoup | d4fff7cee6 | |
LagradOst | 527a6388a9 | |
CranberrySoup | 15333123cd | |
LagradOst | 2ae5b6cefb | |
LagradOst | 0d2a19b350 | |
LagradOst | bff9727f96 | |
LagradOst | a82059cb57 | |
CranberrySoup | 627c1bb223 | |
CranberrySoup | 24977a8d62 | |
LagradOst | 6957a8f95d | |
Sofie | 2bed79b1f1 | |
self-similarity | a5450e5da2 | |
LagradOst | 8fe34d3d2a | |
LagradOst | 7d6ba8c7a4 | |
self-similarity | 2baa75496e | |
Sofie | 01e7acdeac | |
LagradOst | 10bc688eaf | |
LagradOst | 7f7c81828a | |
KingLucius | f6b0ea8dfa | |
LagradOst | 0afbc90cd2 | |
LagradOst | 85c4c74222 | |
LagradOst | b6e99d7358 | |
self-similarity | 130cc16e25 | |
recloudstream[bot] | 1629db2fc9 | |
Weblate (bot) | f05c65cf5c | |
IndusAryan | 4ddd78ebb6 | |
LagradOst | 49731cd699 | |
LagradOst | 3fe247fb19 | |
CranberrySoup | 0839775172 | |
LagradOst | 6211b02e85 | |
Sofie | 9c991f2abd | |
LagradOst | 6089cbc484 | |
LagradOst | ce1f48978b | |
LagradOst | f01820059b | |
LagradOst | 7d3b8c464e | |
LagradOst | 8193e39b30 | |
recloudstream[bot] | 557003895b | |
Weblate (bot) | d0c03321b9 | |
Sofie | 2d82480398 | |
LagradOst | b38a9b1ff5 | |
LagradOst | 1a4cbcaea0 | |
LagradOst | 9b4701fe91 | |
LagradOst | c92ac3e8b3 | |
LagradOst | 39ff6ef8ef | |
LagradOst | 460b1be525 | |
CranberrySoup | 9a1358e295 | |
LagradOst | 823ffd8708 | |
LagradOst | 5bad6aca35 | |
LagradOst | e2502de02c | |
LagradOst | bac2ee9805 | |
LagradOst | d436171a2f | |
LagradOst | 3ea6b1a8d5 | |
LagradOst | afcbdeecc8 | |
LagradOst | 4e28e5f8cc | |
LagradOst | 1901eb371e | |
LagradOst | c4852ce440 | |
self-similarity | a3009af4f5 | |
recloudstream[bot] | 6948bf8073 | |
Weblate (bot) | 61ca0a56be | |
LagradOst | 98b6417140 | |
LagradOst | 10c1ea2f02 | |
LagradOst | b3abf1e45f | |
IndusAryan | f571596bbc | |
LagradOst | e20e3dcfd3 | |
LagradOst | 35e1b8b4dc | |
LagradOst | a05616e3e8 | |
LagradOst | 56cb3d7181 | |
IndusAryan | e95dc1db2a | |
LagradOst | 8f6e8a8e99 | |
IndusAryan | 61d63b17d8 | |
LagradOst | 590c74111c | |
LagradOst | c2b951a078 | |
LagradOst | cbaca158fa | |
LagradOst | 20da3807a2 | |
IndusAryan | d247640dcf | |
IndusAryan | d536dffaf5 | |
self-similarity | 4e01d327c6 | |
LagradOst | 4d98690adb | |
IndusAryan | 74867bed1c | |
LagradOst | 0eb241e6cb | |
LagradOst | 3ab9e11350 | |
self-similarity | d2d2e41fb3 | |
LagradOst | dd4f4a2b78 | |
LagradOst | e43b4808d1 | |
LagradOst | 3ac462ae96 | |
self-similarity | ecd529f73b | |
Cloudburst | 2d65aefc76 | |
recloudstream[bot] | 3af0bf750c | |
Weblate (bot) | 72871c18b5 | |
Cloudburst | 44a2146c12 | |
Sofie | bbbb7c4982 | |
self-similarity | ca6700e28d | |
LagradOst | 5103ad09dc | |
LagradOst | f5c4864a3c | |
recloudstream[bot] | 653982a6bd | |
Weblate (bot) | 22c0022684 | |
LagradOst | 7e6a28bb99 | |
Vu Hoan Huy | c5f6f36fc7 | |
self-similarity | 3137a68552 | |
LagradOst | 2475088f76 | |
Osten | 87d85429f8 | |
self-similarity | 32e243ce94 | |
LagradOst | 180987e2d0 | |
LagradOst | b06f098447 | |
recloudstream[bot] | 6ff4f4c1ce | |
Weblate (bot) | 0afc9f15d2 | |
Jace | 827cbbb0b5 | |
Sofie | a8ed8773de | |
Osten | 363ffa26de | |
LagradOst | 7c60ccdef2 | |
LagradOst | d5316bff9b | |
Osten | 8dae4c2b0f | |
recloudstream[bot] | 6b87fb7831 | |
Cloudburst | 6c325cf721 | |
LagradOst | 04ef6043b0 | |
Cloudburst | 661dfc0927 | |
Osten | 4b4e006f4a | |
LagradOst | 3bdbb35754 | |
LagradOst | c987f7581e | |
LagradOst | c98f35fd94 | |
LagradOst | a1824c86a3 | |
Rex_sa | bfb3313137 | |
LagradOst | 31da089eb1 | |
LagradOst | 3e4a5bdf4c | |
Mater Yoda | 446f774fb4 | |
LagradOst | 51a6e917b5 | |
LagradOst | 35084389a1 | |
LagradOst | 5aa9019d6d | |
LagradOst | da6577e587 | |
LagradOst | 9755bbacb9 | |
LagradOst | 3ae44d5675 | |
LagradOst | 483ce2854f | |
LagradOst | 21e5a1e244 | |
LagradOst | ed0d374721 | |
LagradOst | 4fcf396591 | |
LagradOst | 03d50a943a | |
LagradOst | d5c42f7d5a | |
LagradOst | 4f28aef8f2 | |
LagradOst | f30506a394 | |
LagradOst | 4d6e64adb6 | |
LagradOst | afadf121f4 | |
LagradOst | a2a4da5a29 | |
LagradOst | 6bc5d86ff9 | |
LagradOst | f209c7286e | |
LagradOst | c946115900 | |
LagradOst | 04f52f4a6d | |
LagradOst | 273a947f8e | |
LagradOst | 647e91bc4b | |
Osten | c3296f3210 | |
LagradOst | 166a21f74e | |
LagradOst | 05a0d3cd81 | |
Sofie | 927453d9fe | |
recloudstream[bot] | 9237817bd3 | |
Hosted Weblate | 525bf8d861 | |
Sofie | 847957362f | |
Nexus | 51c1089162 |
|
@ -19,23 +19,23 @@ jobs:
|
||||||
steps:
|
steps:
|
||||||
- name: Generate access token
|
- name: Generate access token
|
||||||
id: generate_token
|
id: generate_token
|
||||||
uses: tibdex/github-app-token@v1
|
uses: tibdex/github-app-token@v2
|
||||||
with:
|
with:
|
||||||
app_id: ${{ secrets.GH_APP_ID }}
|
app_id: ${{ secrets.GH_APP_ID }}
|
||||||
private_key: ${{ secrets.GH_APP_KEY }}
|
private_key: ${{ secrets.GH_APP_KEY }}
|
||||||
repository: "recloudstream/secrets"
|
repository: "recloudstream/secrets"
|
||||||
- name: Generate access token (archive)
|
- name: Generate access token (archive)
|
||||||
id: generate_archive_token
|
id: generate_archive_token
|
||||||
uses: tibdex/github-app-token@v1
|
uses: tibdex/github-app-token@v2
|
||||||
with:
|
with:
|
||||||
app_id: ${{ secrets.GH_APP_ID }}
|
app_id: ${{ secrets.GH_APP_ID }}
|
||||||
private_key: ${{ secrets.GH_APP_KEY }}
|
private_key: ${{ secrets.GH_APP_KEY }}
|
||||||
repository: "recloudstream/cloudstream-archive"
|
repository: "recloudstream/cloudstream-archive"
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v4
|
||||||
- name: Set up JDK 11
|
- name: Set up JDK 17
|
||||||
uses: actions/setup-java@v2
|
uses: actions/setup-java@v4
|
||||||
with:
|
with:
|
||||||
java-version: '11'
|
java-version: '17'
|
||||||
distribution: 'adopt'
|
distribution: 'adopt'
|
||||||
- name: Grant execute permission for gradlew
|
- name: Grant execute permission for gradlew
|
||||||
run: chmod +x gradlew
|
run: chmod +x gradlew
|
||||||
|
@ -56,7 +56,9 @@ jobs:
|
||||||
SIGNING_KEY_ALIAS: "key0"
|
SIGNING_KEY_ALIAS: "key0"
|
||||||
SIGNING_KEY_PASSWORD: ${{ steps.fetch_keystore.outputs.key_pwd }}
|
SIGNING_KEY_PASSWORD: ${{ steps.fetch_keystore.outputs.key_pwd }}
|
||||||
SIGNING_STORE_PASSWORD: ${{ steps.fetch_keystore.outputs.key_pwd }}
|
SIGNING_STORE_PASSWORD: ${{ steps.fetch_keystore.outputs.key_pwd }}
|
||||||
- uses: actions/checkout@v3
|
SIMKL_CLIENT_ID: ${{ secrets.SIMKL_CLIENT_ID }}
|
||||||
|
SIMKL_CLIENT_SECRET: ${{ secrets.SIMKL_CLIENT_SECRET }}
|
||||||
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
repository: "recloudstream/cloudstream-archive"
|
repository: "recloudstream/cloudstream-archive"
|
||||||
token: ${{ steps.generate_archive_token.outputs.token }}
|
token: ${{ steps.generate_archive_token.outputs.token }}
|
||||||
|
|
|
@ -20,7 +20,7 @@ jobs:
|
||||||
steps:
|
steps:
|
||||||
- name: Generate access token
|
- name: Generate access token
|
||||||
id: generate_token
|
id: generate_token
|
||||||
uses: tibdex/github-app-token@v1
|
uses: tibdex/github-app-token@v2
|
||||||
with:
|
with:
|
||||||
app_id: ${{ secrets.GH_APP_ID }}
|
app_id: ${{ secrets.GH_APP_ID }}
|
||||||
private_key: ${{ secrets.GH_APP_KEY }}
|
private_key: ${{ secrets.GH_APP_KEY }}
|
||||||
|
@ -42,13 +42,14 @@ jobs:
|
||||||
cd $GITHUB_WORKSPACE/dokka/
|
cd $GITHUB_WORKSPACE/dokka/
|
||||||
rm -rf "./-cloudstream"
|
rm -rf "./-cloudstream"
|
||||||
|
|
||||||
- name: Setup JDK 11
|
- name: Setup JDK 17
|
||||||
uses: actions/setup-java@v1
|
uses: actions/setup-java@v4
|
||||||
with:
|
with:
|
||||||
java-version: 11
|
java-version: 17
|
||||||
|
distribution: 'adopt'
|
||||||
|
|
||||||
- name: Setup Android SDK
|
- name: Setup Android SDK
|
||||||
uses: android-actions/setup-android@v2
|
uses: android-actions/setup-android@v3
|
||||||
|
|
||||||
- name: Generate Dokka
|
- name: Generate Dokka
|
||||||
run: |
|
run: |
|
||||||
|
|
|
@ -10,7 +10,7 @@ jobs:
|
||||||
steps:
|
steps:
|
||||||
- name: Generate access token
|
- name: Generate access token
|
||||||
id: generate_token
|
id: generate_token
|
||||||
uses: tibdex/github-app-token@v1
|
uses: tibdex/github-app-token@v2
|
||||||
with:
|
with:
|
||||||
app_id: ${{ secrets.GH_APP_ID }}
|
app_id: ${{ secrets.GH_APP_ID }}
|
||||||
private_key: ${{ secrets.GH_APP_KEY }}
|
private_key: ${{ secrets.GH_APP_KEY }}
|
||||||
|
@ -27,7 +27,7 @@ jobs:
|
||||||
comment-body: '${index}. ${similarity} #${number}'
|
comment-body: '${index}. ${similarity} #${number}'
|
||||||
- name: Label if possible duplicate
|
- name: Label if possible duplicate
|
||||||
if: steps.similarity.outputs.similar-issues-found =='true'
|
if: steps.similarity.outputs.similar-issues-found =='true'
|
||||||
uses: actions/github-script@v6
|
uses: actions/github-script@v7
|
||||||
with:
|
with:
|
||||||
github-token: ${{ steps.generate_token.outputs.token }}
|
github-token: ${{ steps.generate_token.outputs.token }}
|
||||||
script: |
|
script: |
|
||||||
|
@ -37,7 +37,7 @@ jobs:
|
||||||
repo: context.repo.repo,
|
repo: context.repo.repo,
|
||||||
labels: ["possible duplicate"]
|
labels: ["possible duplicate"]
|
||||||
})
|
})
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v4
|
||||||
- name: Automatically close issues that dont follow the issue template
|
- name: Automatically close issues that dont follow the issue template
|
||||||
uses: lucasbento/auto-close-issues@v1.0.2
|
uses: lucasbento/auto-close-issues@v1.0.2
|
||||||
with:
|
with:
|
||||||
|
@ -68,7 +68,7 @@ jobs:
|
||||||
Found provider name: `${{ steps.provider_check.outputs.name }}`
|
Found provider name: `${{ steps.provider_check.outputs.name }}`
|
||||||
- name: Label if mentions provider
|
- name: Label if mentions provider
|
||||||
if: steps.provider_check.outputs.name != 'none'
|
if: steps.provider_check.outputs.name != 'none'
|
||||||
uses: actions/github-script@v6
|
uses: actions/github-script@v7
|
||||||
with:
|
with:
|
||||||
github-token: ${{ steps.generate_token.outputs.token }}
|
github-token: ${{ steps.generate_token.outputs.token }}
|
||||||
script: |
|
script: |
|
||||||
|
|
|
@ -18,16 +18,16 @@ jobs:
|
||||||
steps:
|
steps:
|
||||||
- name: Generate access token
|
- name: Generate access token
|
||||||
id: generate_token
|
id: generate_token
|
||||||
uses: tibdex/github-app-token@v1
|
uses: tibdex/github-app-token@v2
|
||||||
with:
|
with:
|
||||||
app_id: ${{ secrets.GH_APP_ID }}
|
app_id: ${{ secrets.GH_APP_ID }}
|
||||||
private_key: ${{ secrets.GH_APP_KEY }}
|
private_key: ${{ secrets.GH_APP_KEY }}
|
||||||
repository: "recloudstream/secrets"
|
repository: "recloudstream/secrets"
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v4
|
||||||
- name: Set up JDK 11
|
- name: Set up JDK 17
|
||||||
uses: actions/setup-java@v2
|
uses: actions/setup-java@v4
|
||||||
with:
|
with:
|
||||||
java-version: '11'
|
java-version: '17'
|
||||||
distribution: 'adopt'
|
distribution: 'adopt'
|
||||||
- name: Grant execute permission for gradlew
|
- name: Grant execute permission for gradlew
|
||||||
run: chmod +x gradlew
|
run: chmod +x gradlew
|
||||||
|
@ -43,11 +43,14 @@ jobs:
|
||||||
echo "key_pwd=$KEY_PWD" >> $GITHUB_OUTPUT
|
echo "key_pwd=$KEY_PWD" >> $GITHUB_OUTPUT
|
||||||
- name: Run Gradle
|
- name: Run Gradle
|
||||||
run: |
|
run: |
|
||||||
./gradlew assemblePrerelease makeJar androidSourcesJar
|
./gradlew assemblePrerelease build androidSourcesJar
|
||||||
|
./gradlew makeJar # for classes.jar, has to be done after assemblePrerelease
|
||||||
env:
|
env:
|
||||||
SIGNING_KEY_ALIAS: "key0"
|
SIGNING_KEY_ALIAS: "key0"
|
||||||
SIGNING_KEY_PASSWORD: ${{ steps.fetch_keystore.outputs.key_pwd }}
|
SIGNING_KEY_PASSWORD: ${{ steps.fetch_keystore.outputs.key_pwd }}
|
||||||
SIGNING_STORE_PASSWORD: ${{ steps.fetch_keystore.outputs.key_pwd }}
|
SIGNING_STORE_PASSWORD: ${{ steps.fetch_keystore.outputs.key_pwd }}
|
||||||
|
SIMKL_CLIENT_ID: ${{ secrets.SIMKL_CLIENT_ID }}
|
||||||
|
SIMKL_CLIENT_SECRET: ${{ secrets.SIMKL_CLIENT_SECRET }}
|
||||||
- name: Create pre-release
|
- name: Create pre-release
|
||||||
uses: "marvinpinto/action-automatic-releases@latest"
|
uses: "marvinpinto/action-automatic-releases@latest"
|
||||||
with:
|
with:
|
||||||
|
|
|
@ -6,18 +6,18 @@ jobs:
|
||||||
build:
|
build:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v4
|
||||||
- name: Set up JDK 11
|
- name: Set up JDK 17
|
||||||
uses: actions/setup-java@v2
|
uses: actions/setup-java@v4
|
||||||
with:
|
with:
|
||||||
java-version: '11'
|
java-version: '17'
|
||||||
distribution: 'adopt'
|
distribution: 'adopt'
|
||||||
- name: Grant execute permission for gradlew
|
- name: Grant execute permission for gradlew
|
||||||
run: chmod +x gradlew
|
run: chmod +x gradlew
|
||||||
- name: Run Gradle
|
- name: Run Gradle
|
||||||
run: ./gradlew assemblePrereleaseDebug
|
run: ./gradlew assemblePrereleaseDebug
|
||||||
- name: Upload Artifact
|
- name: Upload Artifact
|
||||||
uses: actions/upload-artifact@v2
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: pull-request-build
|
name: pull-request-build
|
||||||
path: "app/build/outputs/apk/prerelease/debug/*.apk"
|
path: "app/build/outputs/apk/prerelease/debug/*.apk"
|
||||||
|
|
|
@ -18,12 +18,12 @@ jobs:
|
||||||
steps:
|
steps:
|
||||||
- name: Generate access token
|
- name: Generate access token
|
||||||
id: generate_token
|
id: generate_token
|
||||||
uses: tibdex/github-app-token@v1
|
uses: tibdex/github-app-token@v2
|
||||||
with:
|
with:
|
||||||
app_id: ${{ secrets.GH_APP_ID }}
|
app_id: ${{ secrets.GH_APP_ID }}
|
||||||
private_key: ${{ secrets.GH_APP_KEY }}
|
private_key: ${{ secrets.GH_APP_KEY }}
|
||||||
repository: "recloudstream/cloudstream"
|
repository: "recloudstream/cloudstream"
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
token: ${{ steps.generate_token.outputs.token }}
|
token: ${{ steps.generate_token.outputs.token }}
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<project version="4">
|
<project version="4">
|
||||||
<component name="CompilerConfiguration">
|
<component name="CompilerConfiguration">
|
||||||
<bytecodeTargetLevel target="11" />
|
<bytecodeTargetLevel target="17" />
|
||||||
</component>
|
</component>
|
||||||
</project>
|
</project>
|
|
@ -8,7 +8,7 @@
|
||||||
<option name="testRunner" value="GRADLE" />
|
<option name="testRunner" value="GRADLE" />
|
||||||
<option name="distributionType" value="DEFAULT_WRAPPED" />
|
<option name="distributionType" value="DEFAULT_WRAPPED" />
|
||||||
<option name="externalProjectPath" value="$PROJECT_DIR$" />
|
<option name="externalProjectPath" value="$PROJECT_DIR$" />
|
||||||
<option name="gradleJvm" value="11" />
|
<option name="gradleJvm" value="jbr-17" />
|
||||||
<option name="modules">
|
<option name="modules">
|
||||||
<set>
|
<set>
|
||||||
<option value="$PROJECT_DIR$" />
|
<option value="$PROJECT_DIR$" />
|
||||||
|
|
|
@ -9,8 +9,9 @@
|
||||||
+ **AdFree**, No ads whatsoever
|
+ **AdFree**, No ads whatsoever
|
||||||
+ No tracking/analytics
|
+ No tracking/analytics
|
||||||
+ Bookmarks
|
+ Bookmarks
|
||||||
+ Download and stream movies, tv-shows and anime
|
+ Phone and TV support
|
||||||
+ Chromecast
|
+ Chromecast
|
||||||
|
+ Extension system for personal customization
|
||||||
|
|
||||||
### Supported languages:
|
### Supported languages:
|
||||||
<a href="https://hosted.weblate.org/engage/cloudstream/">
|
<a href="https://hosted.weblate.org/engage/cloudstream/">
|
||||||
|
|
|
@ -0,0 +1,6 @@
|
||||||
|
# Set this to the minimum version your project supports.
|
||||||
|
cmake_minimum_required(VERSION 3.18)
|
||||||
|
project(CrashHandler)
|
||||||
|
find_library(log-lib log)
|
||||||
|
add_library(native-lib SHARED src/main/cpp/native-lib.cpp)
|
||||||
|
target_link_libraries(native-lib ${log-lib})
|
|
@ -1,13 +1,13 @@
|
||||||
import com.android.build.gradle.api.BaseVariantOutput
|
import com.android.build.gradle.internal.cxx.configure.gradleLocalProperties
|
||||||
import org.jetbrains.dokka.gradle.DokkaTask
|
import org.jetbrains.dokka.gradle.DokkaTask
|
||||||
|
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
|
||||||
import java.io.ByteArrayOutputStream
|
import java.io.ByteArrayOutputStream
|
||||||
import java.net.URL
|
import java.net.URL
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
id("com.android.application")
|
id("com.android.application")
|
||||||
|
id("com.google.devtools.ksp")
|
||||||
id("kotlin-android")
|
id("kotlin-android")
|
||||||
id("kotlin-kapt")
|
|
||||||
id("kotlin-android-extensions")
|
|
||||||
id("org.jetbrains.dokka")
|
id("org.jetbrains.dokka")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -19,7 +19,7 @@ fun String.execute() = ByteArrayOutputStream().use { baot ->
|
||||||
workingDir = projectDir
|
workingDir = projectDir
|
||||||
commandLine = this@execute.split(Regex("\\s"))
|
commandLine = this@execute.split(Regex("\\s"))
|
||||||
standardOutput = baot
|
standardOutput = baot
|
||||||
}.exitValue == 0)
|
}.exitValue == 0)
|
||||||
String(baot.toByteArray()).trim()
|
String(baot.toByteArray()).trim()
|
||||||
else null
|
else null
|
||||||
}
|
}
|
||||||
|
@ -28,6 +28,18 @@ android {
|
||||||
testOptions {
|
testOptions {
|
||||||
unitTests.isReturnDefaultValues = true
|
unitTests.isReturnDefaultValues = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
viewBinding {
|
||||||
|
enable = true
|
||||||
|
}
|
||||||
|
|
||||||
|
/* disable this for now
|
||||||
|
externalNativeBuild {
|
||||||
|
cmake {
|
||||||
|
path("CMakeLists.txt")
|
||||||
|
}
|
||||||
|
}*/
|
||||||
|
|
||||||
signingConfigs {
|
signingConfigs {
|
||||||
create("prerelease") {
|
create("prerelease") {
|
||||||
if (prereleaseStoreFile != null) {
|
if (prereleaseStoreFile != null) {
|
||||||
|
@ -39,33 +51,44 @@ android {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
compileSdk = 33
|
compileSdk = 34
|
||||||
buildToolsVersion = "30.0.3"
|
buildToolsVersion = "34.0.0"
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId = "com.lagradost.cloudstream3"
|
applicationId = "com.lagradost.cloudstream3"
|
||||||
minSdk = 21
|
minSdk = 21
|
||||||
targetSdk = 33
|
targetSdk = 33 /* Android 14 is Fu*ked
|
||||||
|
^ https://developer.android.com/about/versions/14/behavior-changes-14#safer-dynamic-code-loading*/
|
||||||
versionCode = 59
|
versionCode = 63
|
||||||
versionName = "4.0.1"
|
versionName = "4.3.1"
|
||||||
|
|
||||||
resValue("string", "app_version", "${defaultConfig.versionName}${versionNameSuffix ?: ""}")
|
resValue("string", "app_version", "${defaultConfig.versionName}${versionNameSuffix ?: ""}")
|
||||||
|
|
||||||
resValue("string", "commit_hash", "git rev-parse --short HEAD".execute() ?: "")
|
resValue("string", "commit_hash", "git rev-parse --short HEAD".execute() ?: "")
|
||||||
|
|
||||||
resValue("bool", "is_prerelease", "false")
|
resValue("bool", "is_prerelease", "false")
|
||||||
|
|
||||||
|
// Reads local.properties
|
||||||
|
val localProperties = gradleLocalProperties(rootDir)
|
||||||
|
|
||||||
buildConfigField(
|
buildConfigField(
|
||||||
"String",
|
"String",
|
||||||
"BUILDDATE",
|
"BUILDDATE",
|
||||||
"new java.text.SimpleDateFormat(\"yyyy-MM-dd HH:mm\").format(new java.util.Date(" + System.currentTimeMillis() + "L));"
|
"new java.text.SimpleDateFormat(\"yyyy-MM-dd HH:mm\").format(new java.util.Date(" + System.currentTimeMillis() + "L));"
|
||||||
)
|
)
|
||||||
|
buildConfigField(
|
||||||
|
"String",
|
||||||
|
"SIMKL_CLIENT_ID",
|
||||||
|
"\"" + (System.getenv("SIMKL_CLIENT_ID") ?: localProperties["simkl.id"]) + "\""
|
||||||
|
)
|
||||||
|
buildConfigField(
|
||||||
|
"String",
|
||||||
|
"SIMKL_CLIENT_SECRET",
|
||||||
|
"\"" + (System.getenv("SIMKL_CLIENT_SECRET") ?: localProperties["simkl.secret"]) + "\""
|
||||||
|
)
|
||||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
|
|
||||||
kapt {
|
ksp {
|
||||||
includeCompileClasspath = true
|
arg("room.schemaLocation", "$projectDir/schemas")
|
||||||
|
arg("exportSchema", "true")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -74,14 +97,21 @@ android {
|
||||||
isDebuggable = false
|
isDebuggable = false
|
||||||
isMinifyEnabled = false
|
isMinifyEnabled = false
|
||||||
isShrinkResources = false
|
isShrinkResources = false
|
||||||
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
|
proguardFiles(
|
||||||
|
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||||
|
"proguard-rules.pro"
|
||||||
|
)
|
||||||
}
|
}
|
||||||
debug {
|
debug {
|
||||||
isDebuggable = true
|
isDebuggable = true
|
||||||
applicationIdSuffix = ".debug"
|
applicationIdSuffix = ".debug"
|
||||||
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
|
proguardFiles(
|
||||||
|
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||||
|
"proguard-rules.pro"
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
flavorDimensions.add("state")
|
flavorDimensions.add("state")
|
||||||
productFlavors {
|
productFlavors {
|
||||||
create("stable") {
|
create("stable") {
|
||||||
|
@ -98,20 +128,22 @@ android {
|
||||||
versionCode = (System.currentTimeMillis() / 60000).toInt()
|
versionCode = (System.currentTimeMillis() / 60000).toInt()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
compileOptions {
|
compileOptions {
|
||||||
isCoreLibraryDesugaringEnabled = true
|
isCoreLibraryDesugaringEnabled = true
|
||||||
|
|
||||||
sourceCompatibility = JavaVersion.VERSION_1_8
|
sourceCompatibility = JavaVersion.VERSION_1_8
|
||||||
targetCompatibility = JavaVersion.VERSION_1_8
|
targetCompatibility = JavaVersion.VERSION_1_8
|
||||||
}
|
}
|
||||||
kotlinOptions {
|
|
||||||
jvmTarget = "1.8"
|
|
||||||
freeCompilerArgs = listOf("-Xjvm-default=compatibility")
|
|
||||||
}
|
|
||||||
lint {
|
lint {
|
||||||
abortOnError = false
|
abortOnError = false
|
||||||
checkReleaseBuilds = false
|
checkReleaseBuilds = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
buildFeatures {
|
||||||
|
buildConfig = true
|
||||||
|
}
|
||||||
|
|
||||||
namespace = "com.lagradost.cloudstream3"
|
namespace = "com.lagradost.cloudstream3"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -120,124 +152,104 @@ repositories {
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation("com.google.android.mediahome:video:1.0.0")
|
// Testing
|
||||||
implementation("androidx.test.ext:junit-ktx:1.1.3")
|
|
||||||
testImplementation("org.json:json:20180813")
|
|
||||||
|
|
||||||
implementation("androidx.core:core-ktx:1.8.0")
|
|
||||||
implementation("androidx.appcompat:appcompat:1.4.2") // need target 32 for 1.5.0
|
|
||||||
|
|
||||||
// dont change this to 1.6.0 it looks ugly af
|
|
||||||
implementation("com.google.android.material:material:1.5.0")
|
|
||||||
implementation("androidx.constraintlayout:constraintlayout:2.1.4")
|
|
||||||
implementation("androidx.navigation:navigation-fragment-ktx:2.5.1")
|
|
||||||
implementation("androidx.navigation:navigation-ui-ktx:2.5.1")
|
|
||||||
implementation("androidx.lifecycle:lifecycle-livedata-ktx:2.5.1")
|
|
||||||
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.1")
|
|
||||||
testImplementation("junit:junit:4.13.2")
|
testImplementation("junit:junit:4.13.2")
|
||||||
androidTestImplementation("androidx.test.ext:junit:1.1.3")
|
testImplementation("org.json:json:20231013")
|
||||||
androidTestImplementation("androidx.test.espresso:espresso-core:3.4.0")
|
androidTestImplementation("androidx.test:core")
|
||||||
|
implementation("androidx.test.ext:junit-ktx:1.1.5")
|
||||||
|
androidTestImplementation("androidx.test.ext:junit:1.1.5")
|
||||||
|
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
|
||||||
|
|
||||||
//implementation("io.karn:khttp-android:0.1.2") //okhttp instead
|
// Android Core & Lifecycle
|
||||||
// implementation("org.jsoup:jsoup:1.13.1")
|
implementation("androidx.core:core-ktx:1.12.0")
|
||||||
implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.13.1")
|
implementation("androidx.appcompat:appcompat:1.6.1")
|
||||||
|
implementation("androidx.navigation:navigation-ui-ktx:2.7.6")
|
||||||
implementation("androidx.preference:preference-ktx:1.2.0")
|
implementation("androidx.lifecycle:lifecycle-livedata-ktx:2.7.0")
|
||||||
|
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.7.0")
|
||||||
implementation("com.github.bumptech.glide:glide:4.13.1")
|
implementation("androidx.navigation:navigation-fragment-ktx:2.7.6")
|
||||||
kapt("com.github.bumptech.glide:compiler:4.13.1")
|
|
||||||
implementation("com.github.bumptech.glide:okhttp3-integration:4.13.0")
|
|
||||||
|
|
||||||
|
// Design & UI
|
||||||
implementation("jp.wasabeef:glide-transformations:4.3.0")
|
implementation("jp.wasabeef:glide-transformations:4.3.0")
|
||||||
|
implementation("androidx.preference:preference-ktx:1.2.1")
|
||||||
|
implementation("com.google.android.material:material:1.10.0")
|
||||||
|
implementation("androidx.constraintlayout:constraintlayout:2.1.4")
|
||||||
implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0")
|
implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0")
|
||||||
|
|
||||||
// implementation("androidx.leanback:leanback-paging:1.1.0-alpha09")
|
// Glide Module
|
||||||
|
ksp("com.github.bumptech.glide:ksp:4.16.0")
|
||||||
|
implementation("com.github.bumptech.glide:glide:4.16.0")
|
||||||
|
implementation("com.github.bumptech.glide:okhttp3-integration:4.16.0")
|
||||||
|
|
||||||
// Exoplayer
|
// For KSP -> Official Annotation Processors are Not Yet Supported for KSP
|
||||||
implementation("com.google.android.exoplayer:exoplayer:2.18.2")
|
ksp("dev.zacsweers.autoservice:auto-service-ksp:1.1.0")
|
||||||
implementation("com.google.android.exoplayer:extension-cast:2.18.2")
|
implementation("com.google.guava:guava:32.1.3-android")
|
||||||
implementation("com.google.android.exoplayer:extension-mediasession:2.18.2")
|
implementation("dev.zacsweers.autoservice:auto-service-ksp:1.1.0")
|
||||||
implementation("com.google.android.exoplayer:extension-okhttp:2.18.2")
|
|
||||||
// Use the Jellyfin ffmpeg extension for easy ffmpeg audio decoding in exoplayer. Thank you Jellyfin <3
|
|
||||||
// implementation("org.jellyfin.exoplayer:exoplayer-ffmpeg-extension:2.18.2+1")
|
|
||||||
|
|
||||||
//implementation("com.google.android.exoplayer:extension-leanback:2.14.0")
|
// Media 3 (ExoPlayer)
|
||||||
|
implementation("androidx.media3:media3-ui:1.1.1")
|
||||||
|
implementation("androidx.media3:media3-cast:1.1.1")
|
||||||
|
implementation("androidx.media3:media3-common:1.1.1")
|
||||||
|
implementation("androidx.media3:media3-session:1.1.1")
|
||||||
|
implementation("androidx.media3:media3-exoplayer:1.1.1")
|
||||||
|
implementation("com.google.android.mediahome:video:1.0.0")
|
||||||
|
implementation("androidx.media3:media3-exoplayer-hls:1.1.1")
|
||||||
|
implementation("androidx.media3:media3-exoplayer-dash:1.1.1")
|
||||||
|
implementation("androidx.media3:media3-datasource-okhttp:1.1.1")
|
||||||
|
|
||||||
// Bug reports
|
// PlayBack
|
||||||
implementation("ch.acra:acra-core:5.8.4")
|
implementation("com.jaredrummler:colorpicker:1.1.0") // Subtitle Color Picker
|
||||||
implementation("ch.acra:acra-toast:5.8.4")
|
implementation("com.github.recloudstream:media-ffmpeg:1.1.0") // Custom FF-MPEG Lib for Audio Codecs
|
||||||
|
implementation("com.github.teamnewpipe:NewPipeExtractor:6dc25f7") /* For Trailers
|
||||||
|
^ Update to Latest Commits if Trailers Misbehave, github.com/TeamNewPipe/NewPipeExtractor/commits/dev */
|
||||||
|
implementation("com.github.albfernandez:juniversalchardet:2.4.0") // Subtitle Decoding
|
||||||
|
|
||||||
compileOnly("com.google.auto.service:auto-service-annotations:1.0")
|
// Crash Reports (AcraApplication.kt)
|
||||||
//either for java sources:
|
implementation("ch.acra:acra-core:5.11.3")
|
||||||
annotationProcessor("com.google.auto.service:auto-service:1.0")
|
implementation("ch.acra:acra-toast:5.11.3")
|
||||||
//or for kotlin sources (requires kapt gradle plugin):
|
|
||||||
kapt("com.google.auto.service:auto-service:1.0")
|
|
||||||
|
|
||||||
// subtitle color picker
|
|
||||||
implementation("com.jaredrummler:colorpicker:1.1.0")
|
|
||||||
|
|
||||||
//run JS
|
|
||||||
// do not upgrade to 1.7.14, since in 1.7.14 Rhino uses the `SourceVersion` class, which is not
|
|
||||||
// available on Android (even when using desugaring), and `NoClassDefFoundError` is thrown
|
|
||||||
implementation("org.mozilla:rhino:1.7.13")
|
|
||||||
|
|
||||||
// TorrentStream
|
|
||||||
//implementation("com.github.TorrentStream:TorrentStream-Android:2.7.0")
|
|
||||||
|
|
||||||
// Downloading
|
|
||||||
implementation("androidx.work:work-runtime:2.8.0")
|
|
||||||
implementation("androidx.work:work-runtime-ktx:2.8.0")
|
|
||||||
|
|
||||||
// Networking
|
|
||||||
// implementation("com.squareup.okhttp3:okhttp:4.9.2")
|
|
||||||
// implementation("com.squareup.okhttp3:okhttp-dnsoverhttps:4.9.1")
|
|
||||||
implementation("com.github.Blatzar:NiceHttp:0.4.2")
|
|
||||||
// To fix SSL fuckery on android 9
|
|
||||||
implementation("org.conscrypt:conscrypt-android:2.2.1")
|
|
||||||
// Util to skip the URI file fuckery 🙏
|
|
||||||
implementation("com.github.tachiyomiorg:unifile:17bec43")
|
|
||||||
|
|
||||||
// API because cba maintaining it myself
|
|
||||||
implementation("com.uwetrottmann.tmdb2:tmdb-java:2.6.0")
|
|
||||||
|
|
||||||
implementation("com.github.discord:OverlappingPanels:0.1.3")
|
|
||||||
// debugImplementation because LeakCanary should only run in debug builds.
|
|
||||||
// debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.7'
|
|
||||||
|
|
||||||
// for shimmer when loading
|
|
||||||
implementation("com.facebook.shimmer:shimmer:0.5.0")
|
|
||||||
|
|
||||||
|
// UI Stuff
|
||||||
|
implementation("com.facebook.shimmer:shimmer:0.5.0") // Shimmering Effect (Loading Skeleton)
|
||||||
|
implementation("androidx.palette:palette-ktx:1.0.0") // Palette For Images -> Colors
|
||||||
implementation("androidx.tvprovider:tvprovider:1.0.0")
|
implementation("androidx.tvprovider:tvprovider:1.0.0")
|
||||||
|
implementation("com.github.discord:OverlappingPanels:0.1.5") // Gestures
|
||||||
|
implementation("com.github.rubensousa:previewseekbar-media3:1.1.1.0") // SeekBar Preview
|
||||||
|
|
||||||
// used for subtitle decoding https://github.com/albfernandez/juniversalchardet
|
// Extensions & Other Libs
|
||||||
implementation("com.github.albfernandez:juniversalchardet:2.4.0")
|
implementation("org.mozilla:rhino:1.7.13") /* run JavaScript
|
||||||
|
^ Don't Bump RhinoJS to 1.7.14,`NoClassDefFoundError` Occurs and Trailers won't play (even with Desugaring)
|
||||||
|
NewPipeExtractor Issue */
|
||||||
|
implementation("me.xdrop:fuzzywuzzy:1.4.0") // Library/Ext Searching with Levenshtein Distance
|
||||||
|
implementation("com.github.LagradOst:SafeFile:0.0.6") // To Prevent the URI File Fu*kery
|
||||||
|
implementation("org.conscrypt:conscrypt-android:2.5.2") // To Fix SSL Fu*kery on Android 9
|
||||||
|
implementation("com.uwetrottmann.tmdb2:tmdb-java:2.10.0") // TMDB API v3 Wrapper Made with RetroFit
|
||||||
|
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.0.4")
|
||||||
|
implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.13.1") /* JSON Parser
|
||||||
|
^ Don't Bump Jackson above 2.13.1 , Crashes on Android TV's and FireSticks that have Min API
|
||||||
|
Level 25 or Less. */
|
||||||
|
|
||||||
// slow af yt
|
// Downloading & Networking
|
||||||
//implementation("com.github.HaarigerHarald:android-youtubeExtractor:master-SNAPSHOT")
|
implementation("androidx.work:work-runtime:2.9.0")
|
||||||
|
implementation("androidx.work:work-runtime-ktx:2.9.0")
|
||||||
// newpipe yt taken from https://github.com/TeamNewPipe/NewPipe/blob/dev/app/build.gradle#L204
|
implementation("com.github.Blatzar:NiceHttp:0.4.5") // HTTP Lib
|
||||||
implementation("com.github.TeamNewPipe:NewPipeExtractor:master-SNAPSHOT")
|
|
||||||
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:1.1.6")
|
|
||||||
|
|
||||||
// Library/extensions searching with Levenshtein distance
|
|
||||||
implementation("me.xdrop:fuzzywuzzy:1.4.0")
|
|
||||||
|
|
||||||
// color pallette for images -> colors
|
|
||||||
implementation("androidx.palette:palette-ktx:1.0.0")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks.register("androidSourcesJar", Jar::class) {
|
tasks.register("androidSourcesJar", Jar::class) {
|
||||||
archiveClassifier.set("sources")
|
archiveClassifier.set("sources")
|
||||||
from(android.sourceSets.getByName("main").java.srcDirs) //full sources
|
from(android.sourceSets.getByName("main").java.srcDirs) // Full Sources
|
||||||
}
|
}
|
||||||
|
|
||||||
// this is used by the gradlew plugin
|
// For GradLew Plugin
|
||||||
tasks.register("makeJar", Copy::class) {
|
tasks.register("makeJar", Copy::class) {
|
||||||
from("build/intermediates/compile_app_classes_jar/prereleaseDebug")
|
from("build/intermediates/compile_app_classes_jar/prereleaseDebug")
|
||||||
into("build")
|
into("build")
|
||||||
include("classes.jar")
|
include("classes.jar")
|
||||||
dependsOn("build")
|
}
|
||||||
|
|
||||||
|
tasks.withType<KotlinCompile> {
|
||||||
|
kotlinOptions {
|
||||||
|
jvmTarget = "1.8"
|
||||||
|
freeCompilerArgs = listOf("-Xjvm-default=all-compatibility")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks.withType<DokkaTask>().configureEach {
|
tasks.withType<DokkaTask>().configureEach {
|
||||||
|
@ -250,6 +262,7 @@ tasks.withType<DokkaTask>().configureEach {
|
||||||
|
|
||||||
// URL showing where the source code can be accessed through the web browser
|
// URL showing where the source code can be accessed through the web browser
|
||||||
remoteUrl.set(URL("https://github.com/recloudstream/cloudstream/tree/master/app/src/main/java"))
|
remoteUrl.set(URL("https://github.com/recloudstream/cloudstream/tree/master/app/src/main/java"))
|
||||||
|
|
||||||
// Suffix which is used to append the line number to the URL. Use #L for GitHub
|
// Suffix which is used to append the line number to the URL. Use #L for GitHub
|
||||||
remoteLineSuffix.set("#L")
|
remoteLineSuffix.set("#L")
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,33 @@
|
||||||
package com.lagradost.cloudstream3
|
package com.lagradost.cloudstream3
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.os.PersistableBundle
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import androidx.test.core.app.ActivityScenario
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
|
import androidx.viewbinding.ViewBinding
|
||||||
|
import com.lagradost.cloudstream3.databinding.FragmentHomeBinding
|
||||||
|
import com.lagradost.cloudstream3.databinding.FragmentHomeTvBinding
|
||||||
|
import com.lagradost.cloudstream3.databinding.FragmentLibraryBinding
|
||||||
|
import com.lagradost.cloudstream3.databinding.FragmentLibraryTvBinding
|
||||||
|
import com.lagradost.cloudstream3.databinding.FragmentPlayerBinding
|
||||||
|
import com.lagradost.cloudstream3.databinding.FragmentPlayerTvBinding
|
||||||
|
import com.lagradost.cloudstream3.databinding.FragmentResultBinding
|
||||||
|
import com.lagradost.cloudstream3.databinding.FragmentResultTvBinding
|
||||||
|
import com.lagradost.cloudstream3.databinding.FragmentSearchBinding
|
||||||
|
import com.lagradost.cloudstream3.databinding.FragmentSearchTvBinding
|
||||||
|
import com.lagradost.cloudstream3.databinding.HomeResultGridBinding
|
||||||
|
import com.lagradost.cloudstream3.databinding.HomepageParentBinding
|
||||||
|
import com.lagradost.cloudstream3.databinding.HomepageParentEmulatorBinding
|
||||||
|
import com.lagradost.cloudstream3.databinding.HomepageParentTvBinding
|
||||||
|
import com.lagradost.cloudstream3.databinding.PlayerCustomLayoutBinding
|
||||||
|
import com.lagradost.cloudstream3.databinding.PlayerCustomLayoutTvBinding
|
||||||
|
import com.lagradost.cloudstream3.databinding.RepositoryItemBinding
|
||||||
|
import com.lagradost.cloudstream3.databinding.RepositoryItemTvBinding
|
||||||
|
import com.lagradost.cloudstream3.databinding.SearchResultGridBinding
|
||||||
|
import com.lagradost.cloudstream3.databinding.SearchResultGridExpandedBinding
|
||||||
|
import com.lagradost.cloudstream3.databinding.TrailerCustomLayoutBinding
|
||||||
import com.lagradost.cloudstream3.utils.SubtitleHelper
|
import com.lagradost.cloudstream3.utils.SubtitleHelper
|
||||||
import com.lagradost.cloudstream3.utils.TestingUtils
|
import com.lagradost.cloudstream3.utils.TestingUtils
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
|
@ -8,16 +35,23 @@ import org.junit.Assert
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import org.junit.runner.RunWith
|
import org.junit.runner.RunWith
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Instrumented test, which will execute on an Android device.
|
* Instrumented test, which will execute on an Android device.
|
||||||
*
|
*
|
||||||
* See [testing documentation](http://d.android.com/tools/testing).
|
* See [testing documentation](http://d.android.com/tools/testing).
|
||||||
*/
|
*/
|
||||||
|
class TestApplication : Activity() {
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?, persistentState: PersistableBundle?) {
|
||||||
|
super.onCreate(savedInstanceState, persistentState)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@RunWith(AndroidJUnit4::class)
|
@RunWith(AndroidJUnit4::class)
|
||||||
class ExampleInstrumentedTest {
|
class ExampleInstrumentedTest {
|
||||||
private fun getAllProviders(): List<MainAPI> {
|
private fun getAllProviders(): Array<MainAPI> {
|
||||||
println("Providers: ${APIHolder.allProviders.size}")
|
println("Providers: ${APIHolder.allProviders.size}")
|
||||||
return APIHolder.allProviders //.filter { !it.usesWebView }
|
return APIHolder.allProviders.toTypedArray() //.filter { !it.usesWebView }
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -26,6 +60,76 @@ class ExampleInstrumentedTest {
|
||||||
println("Done providersExist")
|
println("Done providersExist")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Throws
|
||||||
|
private inline fun <reified T : ViewBinding> testAllLayouts(
|
||||||
|
activity: Activity,
|
||||||
|
vararg layouts: Int
|
||||||
|
) {
|
||||||
|
|
||||||
|
val bind = T::class.java.methods.first { it.name == "bind" }
|
||||||
|
val inflater = LayoutInflater.from(activity)
|
||||||
|
for (layout in layouts) {
|
||||||
|
val root = inflater.inflate(layout, null, false)
|
||||||
|
bind.invoke(null, root)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Throws
|
||||||
|
fun layoutTest() {
|
||||||
|
ActivityScenario.launch(MainActivity::class.java).use { scenario ->
|
||||||
|
scenario.onActivity { activity: MainActivity ->
|
||||||
|
// FragmentHomeHeadBinding and FragmentHomeHeadTvBinding CANT be the same
|
||||||
|
//testAllLayouts<FragmentHomeHeadBinding>(activity, R.layout.fragment_home_head, R.layout.fragment_home_head_tv)
|
||||||
|
//testAllLayouts<FragmentHomeHeadTvBinding>(activity, R.layout.fragment_home_head, R.layout.fragment_home_head_tv)
|
||||||
|
|
||||||
|
// main cant be tested
|
||||||
|
// testAllLayouts<ActivityMainTvBinding>(activity,R.layout.activity_main, R.layout.activity_main_tv)
|
||||||
|
// testAllLayouts<ActivityMainBinding>(activity,R.layout.activity_main, R.layout.activity_main_tv)
|
||||||
|
//testAllLayouts<ActivityMainBinding>(activity, R.layout.activity_main_tv)
|
||||||
|
|
||||||
|
testAllLayouts<FragmentPlayerBinding>(activity, R.layout.fragment_player,R.layout.fragment_player_tv)
|
||||||
|
testAllLayouts<FragmentPlayerTvBinding>(activity, R.layout.fragment_player,R.layout.fragment_player_tv)
|
||||||
|
|
||||||
|
// testAllLayouts<FragmentResultBinding>(activity, R.layout.fragment_result,R.layout.fragment_result_tv)
|
||||||
|
// testAllLayouts<FragmentResultTvBinding>(activity, R.layout.fragment_result,R.layout.fragment_result_tv)
|
||||||
|
|
||||||
|
testAllLayouts<PlayerCustomLayoutBinding>(activity, R.layout.player_custom_layout,R.layout.player_custom_layout_tv, R.layout.trailer_custom_layout)
|
||||||
|
testAllLayouts<PlayerCustomLayoutTvBinding>(activity, R.layout.player_custom_layout,R.layout.player_custom_layout_tv, R.layout.trailer_custom_layout)
|
||||||
|
testAllLayouts<TrailerCustomLayoutBinding>(activity, R.layout.player_custom_layout,R.layout.player_custom_layout_tv, R.layout.trailer_custom_layout)
|
||||||
|
|
||||||
|
testAllLayouts<RepositoryItemBinding>(activity, R.layout.repository_item_tv, R.layout.repository_item)
|
||||||
|
testAllLayouts<RepositoryItemTvBinding>(activity, R.layout.repository_item_tv, R.layout.repository_item)
|
||||||
|
|
||||||
|
testAllLayouts<RepositoryItemBinding>(activity, R.layout.repository_item_tv, R.layout.repository_item)
|
||||||
|
testAllLayouts<RepositoryItemTvBinding>(activity, R.layout.repository_item_tv, R.layout.repository_item)
|
||||||
|
|
||||||
|
testAllLayouts<FragmentHomeBinding>(activity, R.layout.fragment_home_tv, R.layout.fragment_home)
|
||||||
|
testAllLayouts<FragmentHomeTvBinding>(activity, R.layout.fragment_home_tv, R.layout.fragment_home)
|
||||||
|
|
||||||
|
testAllLayouts<FragmentSearchBinding>(activity, R.layout.fragment_search_tv, R.layout.fragment_search)
|
||||||
|
testAllLayouts<FragmentSearchTvBinding>(activity, R.layout.fragment_search_tv, R.layout.fragment_search)
|
||||||
|
|
||||||
|
testAllLayouts<HomeResultGridBinding>(activity, R.layout.home_result_grid_expanded, R.layout.home_result_grid)
|
||||||
|
//testAllLayouts<HomeResultGridExpandedBinding>(activity, R.layout.home_result_grid_expanded, R.layout.home_result_grid) ??? fails ???
|
||||||
|
|
||||||
|
testAllLayouts<SearchResultGridExpandedBinding>(activity, R.layout.search_result_grid, R.layout.search_result_grid_expanded)
|
||||||
|
testAllLayouts<SearchResultGridBinding>(activity, R.layout.search_result_grid, R.layout.search_result_grid_expanded)
|
||||||
|
|
||||||
|
|
||||||
|
// testAllLayouts<HomeScrollViewBinding>(activity, R.layout.home_scroll_view, R.layout.home_scroll_view_tv)
|
||||||
|
// testAllLayouts<HomeScrollViewTvBinding>(activity, R.layout.home_scroll_view, R.layout.home_scroll_view_tv)
|
||||||
|
|
||||||
|
testAllLayouts<HomepageParentTvBinding>(activity, R.layout.homepage_parent_tv, R.layout.homepage_parent_emulator, R.layout.homepage_parent)
|
||||||
|
testAllLayouts<HomepageParentEmulatorBinding>(activity, R.layout.homepage_parent_tv, R.layout.homepage_parent_emulator, R.layout.homepage_parent)
|
||||||
|
testAllLayouts<HomepageParentBinding>(activity, R.layout.homepage_parent_tv, R.layout.homepage_parent_emulator, R.layout.homepage_parent)
|
||||||
|
|
||||||
|
testAllLayouts<FragmentLibraryTvBinding>(activity, R.layout.fragment_library_tv, R.layout.fragment_library)
|
||||||
|
testAllLayouts<FragmentLibraryBinding>(activity, R.layout.fragment_library_tv, R.layout.fragment_library)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@Throws(AssertionError::class)
|
@Throws(AssertionError::class)
|
||||||
fun providerCorrectData() {
|
fun providerCorrectData() {
|
||||||
|
@ -49,7 +153,7 @@ class ExampleInstrumentedTest {
|
||||||
@Test
|
@Test
|
||||||
fun providerCorrectHomepage() {
|
fun providerCorrectHomepage() {
|
||||||
runBlocking {
|
runBlocking {
|
||||||
getAllProviders().amap { api ->
|
getAllProviders().toList().amap { api ->
|
||||||
TestingUtils.testHomepage(api, ::println)
|
TestingUtils.testHomepage(api, ::println)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
<uses-permission android:name="android.permission.INTERNET" /> <!-- unless you only use cs3 as a player for downloaded stuff, you need this -->
|
<uses-permission android:name="android.permission.INTERNET" /> <!-- unless you only use cs3 as a player for downloaded stuff, you need this -->
|
||||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> <!-- Downloads -->
|
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> <!-- Downloads -->
|
||||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <!-- Downloads on low api devices -->
|
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <!-- Downloads on low api devices -->
|
||||||
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" /> <!-- Plugin API -->
|
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" tools:ignore="ScopedStorage" /> <!-- Plugin API -->
|
||||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" /> <!-- some dependency needs this -->
|
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" /> <!-- some dependency needs this -->
|
||||||
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" /> <!-- Used for player vertical slide -->
|
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" /> <!-- Used for player vertical slide -->
|
||||||
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" /> <!-- Used for app update -->
|
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" /> <!-- Used for app update -->
|
||||||
|
@ -15,7 +15,13 @@
|
||||||
<uses-permission android:name="android.permission.UPDATE_PACKAGES_WITHOUT_USER_ACTION" /> <!-- Used for updates without prompt -->
|
<uses-permission android:name="android.permission.UPDATE_PACKAGES_WITHOUT_USER_ACTION" /> <!-- Used for updates without prompt -->
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> <!-- Used for update service -->
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> <!-- Used for update service -->
|
||||||
|
|
||||||
<!-- <permission android:name="android.permission.QUERY_ALL_PACKAGES" /> <!– Used for getting if vlc is installed –> -->
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
|
||||||
|
|
||||||
|
<!-- Required for getting arbitrary Aniyomi packages -->
|
||||||
|
<uses-permission
|
||||||
|
android:name="android.permission.QUERY_ALL_PACKAGES"
|
||||||
|
tools:ignore="QueryAllPackagesPermission" />
|
||||||
|
|
||||||
<!-- Fixes android tv fuckery -->
|
<!-- Fixes android tv fuckery -->
|
||||||
<uses-feature
|
<uses-feature
|
||||||
android:name="android.hardware.touchscreen"
|
android:name="android.hardware.touchscreen"
|
||||||
|
@ -35,9 +41,11 @@
|
||||||
<application
|
<application
|
||||||
android:name=".AcraApplication"
|
android:name=".AcraApplication"
|
||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
|
android:enableOnBackInvokedCallback="true"
|
||||||
android:appCategory="video"
|
android:appCategory="video"
|
||||||
android:banner="@mipmap/ic_banner"
|
android:banner="@mipmap/ic_banner"
|
||||||
android:fullBackupContent="@xml/backup_descriptor"
|
android:fullBackupContent="@xml/backup_descriptor"
|
||||||
|
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||||
android:icon="@mipmap/ic_launcher"
|
android:icon="@mipmap/ic_launcher"
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
android:largeHeap="true"
|
android:largeHeap="true"
|
||||||
|
@ -45,7 +53,7 @@
|
||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
android:theme="@style/AppTheme"
|
android:theme="@style/AppTheme"
|
||||||
android:usesCleartextTraffic="true"
|
android:usesCleartextTraffic="true"
|
||||||
tools:targetApi="o">
|
tools:targetApi="tiramisu">
|
||||||
|
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="com.google.android.gms.cast.framework.OPTIONS_PROVIDER_CLASS_NAME"
|
android:name="com.google.android.gms.cast.framework.OPTIONS_PROVIDER_CLASS_NAME"
|
||||||
|
@ -61,7 +69,9 @@
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
android:resizeableActivity="true"
|
android:resizeableActivity="true"
|
||||||
android:screenOrientation="userLandscape"
|
android:screenOrientation="userLandscape"
|
||||||
android:supportsPictureInPicture="true">
|
android:supportsPictureInPicture="true"
|
||||||
|
android:taskAffinity="com.lagradost.cloudstream3.downloadedplayer"
|
||||||
|
android:launchMode="singleTask">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.VIEW" />
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
|
||||||
|
@ -92,12 +102,6 @@
|
||||||
android:launchMode="singleTask"
|
android:launchMode="singleTask"
|
||||||
android:resizeableActivity="true"
|
android:resizeableActivity="true"
|
||||||
android:supportsPictureInPicture="true">
|
android:supportsPictureInPicture="true">
|
||||||
<intent-filter android:exported="true">
|
|
||||||
<action android:name="android.intent.action.MAIN" />
|
|
||||||
|
|
||||||
<category android:name="android.intent.category.LAUNCHER" />
|
|
||||||
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
|
|
||||||
</intent-filter>
|
|
||||||
|
|
||||||
<!-- cloudstreamplayer://encodedUrl?name=Dune -->
|
<!-- cloudstreamplayer://encodedUrl?name=Dune -->
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
|
@ -161,6 +165,21 @@
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
|
<activity
|
||||||
|
android:name=".ui.account.AccountSelectActivity"
|
||||||
|
android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout|keyboard|keyboardHidden"
|
||||||
|
android:exported="true">
|
||||||
|
<intent-filter android:exported="true">
|
||||||
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
|
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
|
||||||
|
</intent-filter>
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
</intent-filter>
|
||||||
|
</activity>
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.EasterEggMonke"
|
android:name=".ui.EasterEggMonke"
|
||||||
android:exported="true" />
|
android:exported="true" />
|
||||||
|
@ -168,13 +187,14 @@
|
||||||
<receiver
|
<receiver
|
||||||
android:name=".receivers.VideoDownloadRestartReceiver"
|
android:name=".receivers.VideoDownloadRestartReceiver"
|
||||||
android:enabled="false"
|
android:enabled="false"
|
||||||
android:exported="true">
|
android:exported="false">
|
||||||
<intent-filter android:exported="true">
|
<intent-filter android:exported="false">
|
||||||
<action android:name="restart_service" />
|
<action android:name="restart_service" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</receiver>
|
</receiver>
|
||||||
|
|
||||||
<service
|
<service
|
||||||
|
android:foregroundServiceType="dataSync"
|
||||||
android:name=".services.VideoDownloadService"
|
android:name=".services.VideoDownloadService"
|
||||||
android:enabled="true"
|
android:enabled="true"
|
||||||
android:exported="false" />
|
android:exported="false" />
|
||||||
|
@ -184,6 +204,7 @@
|
||||||
android:exported="false" />
|
android:exported="false" />
|
||||||
|
|
||||||
<service
|
<service
|
||||||
|
android:foregroundServiceType="dataSync"
|
||||||
android:name=".utils.PackageInstallerService"
|
android:name=".utils.PackageInstallerService"
|
||||||
android:exported="false" />
|
android:exported="false" />
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,28 @@
|
||||||
|
#include <jni.h>
|
||||||
|
#include <csignal>
|
||||||
|
#include <android/log.h>
|
||||||
|
|
||||||
|
#define TAG "CloudStream Crash Handler"
|
||||||
|
volatile sig_atomic_t gSignalStatus = 0;
|
||||||
|
void handleNativeCrash(int signal) {
|
||||||
|
gSignalStatus = signal;
|
||||||
|
}
|
||||||
|
|
||||||
|
extern "C" JNIEXPORT void JNICALL
|
||||||
|
Java_com_lagradost_cloudstream3_NativeCrashHandler_initNativeCrashHandler(JNIEnv *env, jobject) {
|
||||||
|
#define REGISTER_SIGNAL(X) signal(X, handleNativeCrash);
|
||||||
|
REGISTER_SIGNAL(SIGSEGV)
|
||||||
|
#undef REGISTER_SIGNAL
|
||||||
|
}
|
||||||
|
|
||||||
|
//extern "C" JNIEXPORT void JNICALL
|
||||||
|
//Java_com_lagradost_cloudstream3_NativeCrashHandler_triggerNativeCrash(JNIEnv *env, jobject thiz) {
|
||||||
|
// int *p = nullptr;
|
||||||
|
// *p = 0;
|
||||||
|
//}
|
||||||
|
|
||||||
|
extern "C" JNIEXPORT int JNICALL
|
||||||
|
Java_com_lagradost_cloudstream3_NativeCrashHandler_getSignalStatus(JNIEnv *env, jobject) {
|
||||||
|
//__android_log_print(ANDROID_LOG_INFO, TAG, "Got signal status %d", gSignalStatus);
|
||||||
|
return gSignalStatus;
|
||||||
|
}
|
|
@ -8,7 +8,6 @@ import android.content.Intent
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.fragment.app.FragmentActivity
|
import androidx.fragment.app.FragmentActivity
|
||||||
import com.google.auto.service.AutoService
|
|
||||||
import com.lagradost.cloudstream3.mvvm.normalSafeApiCall
|
import com.lagradost.cloudstream3.mvvm.normalSafeApiCall
|
||||||
import com.lagradost.cloudstream3.mvvm.suspendSafeApiCall
|
import com.lagradost.cloudstream3.mvvm.suspendSafeApiCall
|
||||||
import com.lagradost.cloudstream3.plugins.PluginManager
|
import com.lagradost.cloudstream3.plugins.PluginManager
|
||||||
|
@ -37,22 +36,21 @@ import java.lang.ref.WeakReference
|
||||||
import kotlin.concurrent.thread
|
import kotlin.concurrent.thread
|
||||||
import kotlin.system.exitProcess
|
import kotlin.system.exitProcess
|
||||||
|
|
||||||
|
|
||||||
class CustomReportSender : ReportSender {
|
class CustomReportSender : ReportSender {
|
||||||
// Sends all your crashes to google forms
|
// Sends all your crashes to google forms
|
||||||
override fun send(context: Context, errorContent: CrashReportData) {
|
override fun send(context: Context, errorContent: CrashReportData) {
|
||||||
println("Sending report")
|
println("Sending report")
|
||||||
val url =
|
val url =
|
||||||
"https://docs.google.com/forms/d/e/1FAIpQLSdOlbgCx7NeaxjvEGyEQlqdh2nCvwjm2vwpP1VwW7REj9Ri3Q/formResponse"
|
"https://docs.google.com/forms/d/e/1FAIpQLSfO4r353BJ79TTY_-t5KWSIJT2xfqcQWY81xjAA1-1N0U2eSg/formResponse"
|
||||||
val data = mapOf(
|
val data = mapOf(
|
||||||
"entry.753293084" to errorContent.toJSON()
|
"entry.1993829403" to errorContent.toJSON()
|
||||||
)
|
)
|
||||||
|
|
||||||
thread { // to not run it on main thread
|
thread { // to not run it on main thread
|
||||||
runBlocking {
|
runBlocking {
|
||||||
suspendSafeApiCall {
|
suspendSafeApiCall {
|
||||||
val post = app.post(url, data = data)
|
app.post(url, data = data)
|
||||||
println("Report response: $post")
|
//println("Report response: $post")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -65,7 +63,6 @@ class CustomReportSender : ReportSender {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@AutoService(ReportSenderFactory::class)
|
|
||||||
class CustomSenderFactory : ReportSenderFactory {
|
class CustomSenderFactory : ReportSenderFactory {
|
||||||
override fun create(context: Context, config: CoreConfiguration): ReportSender {
|
override fun create(context: Context, config: CoreConfiguration): ReportSender {
|
||||||
return CustomReportSender()
|
return CustomReportSender()
|
||||||
|
@ -104,12 +101,17 @@ class ExceptionHandler(val errorFile: File, val onError: (() -> Unit)) :
|
||||||
}
|
}
|
||||||
|
|
||||||
class AcraApplication : Application() {
|
class AcraApplication : Application() {
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
Thread.setDefaultUncaughtExceptionHandler(ExceptionHandler(filesDir.resolve("last_error")) {
|
//NativeCrashHandler.initCrashHandler()
|
||||||
|
ExceptionHandler(filesDir.resolve("last_error")) {
|
||||||
val intent = context!!.packageManager.getLaunchIntentForPackage(context!!.packageName)
|
val intent = context!!.packageManager.getLaunchIntentForPackage(context!!.packageName)
|
||||||
startActivity(Intent.makeRestartActivityTask(intent!!.component))
|
startActivity(Intent.makeRestartActivityTask(intent!!.component))
|
||||||
})
|
}.also {
|
||||||
|
exceptionHandler = it
|
||||||
|
Thread.setDefaultUncaughtExceptionHandler(it)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun attachBaseContext(base: Context?) {
|
override fun attachBaseContext(base: Context?) {
|
||||||
|
@ -121,10 +123,10 @@ class AcraApplication : Application() {
|
||||||
buildConfigClass = BuildConfig::class.java
|
buildConfigClass = BuildConfig::class.java
|
||||||
reportFormat = StringFormat.JSON
|
reportFormat = StringFormat.JSON
|
||||||
|
|
||||||
reportContent = arrayOf(
|
reportContent = listOf(
|
||||||
ReportField.BUILD_CONFIG, ReportField.USER_CRASH_DATE,
|
ReportField.BUILD_CONFIG, ReportField.USER_CRASH_DATE,
|
||||||
ReportField.ANDROID_VERSION, ReportField.PHONE_MODEL,
|
ReportField.ANDROID_VERSION, ReportField.PHONE_MODEL,
|
||||||
ReportField.STACK_TRACE
|
ReportField.STACK_TRACE,
|
||||||
)
|
)
|
||||||
|
|
||||||
// removed this due to bug when starting the app, moved it to when it actually crashes
|
// removed this due to bug when starting the app, moved it to when it actually crashes
|
||||||
|
@ -137,6 +139,8 @@ class AcraApplication : Application() {
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
var exceptionHandler: ExceptionHandler? = null
|
||||||
|
|
||||||
/** Use to get activity from Context */
|
/** Use to get activity from Context */
|
||||||
tailrec fun Context.getActivity(): Activity? = this as? Activity
|
tailrec fun Context.getActivity(): Activity? = this as? Activity
|
||||||
?: (this as? ContextWrapper)?.baseContext?.getActivity()
|
?: (this as? ContextWrapper)?.baseContext?.getActivity()
|
||||||
|
@ -148,6 +152,14 @@ class AcraApplication : Application() {
|
||||||
_context = WeakReference(value)
|
_context = WeakReference(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun <T : Any> getKeyClass(path: String, valueType: Class<T>): T? {
|
||||||
|
return context?.getKey(path, valueType)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun <T : Any> setKeyClass(path: String, value: T) {
|
||||||
|
context?.setKey(path, value)
|
||||||
|
}
|
||||||
|
|
||||||
fun removeKeys(folder: String): Int? {
|
fun removeKeys(folder: String): Int? {
|
||||||
return context?.removeKeys(folder)
|
return context?.removeKeys(folder)
|
||||||
}
|
}
|
||||||
|
@ -203,6 +215,5 @@ class AcraApplication : Application() {
|
||||||
activity?.supportFragmentManager?.fragments?.lastOrNull()
|
activity?.supportFragmentManager?.fragments?.lastOrNull()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,8 +7,14 @@ import android.content.Context
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
import android.content.res.Resources
|
import android.content.res.Resources
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
|
import android.util.DisplayMetrics
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.view.*
|
import android.view.Gravity
|
||||||
|
import android.view.KeyEvent
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.View.NO_ID
|
||||||
|
import android.view.ViewGroup
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.activity.ComponentActivity
|
import androidx.activity.ComponentActivity
|
||||||
|
@ -18,8 +24,11 @@ import androidx.annotation.StringRes
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.appcompat.widget.SearchView
|
import androidx.appcompat.widget.SearchView
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.core.view.children
|
||||||
import androidx.preference.PreferenceManager
|
import androidx.preference.PreferenceManager
|
||||||
import com.google.android.gms.cast.framework.CastSession
|
import com.google.android.gms.cast.framework.CastSession
|
||||||
|
import com.google.android.material.chip.ChipGroup
|
||||||
|
import com.google.android.material.navigationrail.NavigationRailView
|
||||||
import com.lagradost.cloudstream3.AcraApplication.Companion.getKey
|
import com.lagradost.cloudstream3.AcraApplication.Companion.getKey
|
||||||
import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey
|
import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey
|
||||||
import com.lagradost.cloudstream3.mvvm.logError
|
import com.lagradost.cloudstream3.mvvm.logError
|
||||||
|
@ -27,6 +36,7 @@ import com.lagradost.cloudstream3.ui.player.PlayerEventType
|
||||||
import com.lagradost.cloudstream3.ui.result.ResultFragment
|
import com.lagradost.cloudstream3.ui.result.ResultFragment
|
||||||
import com.lagradost.cloudstream3.ui.result.UiText
|
import com.lagradost.cloudstream3.ui.result.UiText
|
||||||
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.updateTv
|
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.updateTv
|
||||||
|
import com.lagradost.cloudstream3.utils.AppUtils.isRtl
|
||||||
import com.lagradost.cloudstream3.utils.DataStoreHelper
|
import com.lagradost.cloudstream3.utils.DataStoreHelper
|
||||||
import com.lagradost.cloudstream3.utils.Event
|
import com.lagradost.cloudstream3.utils.Event
|
||||||
import com.lagradost.cloudstream3.utils.UIHelper
|
import com.lagradost.cloudstream3.utils.UIHelper
|
||||||
|
@ -34,14 +44,50 @@ import com.lagradost.cloudstream3.utils.UIHelper.hasPIPPermission
|
||||||
import com.lagradost.cloudstream3.utils.UIHelper.shouldShowPIPMode
|
import com.lagradost.cloudstream3.utils.UIHelper.shouldShowPIPMode
|
||||||
import com.lagradost.cloudstream3.utils.UIHelper.toPx
|
import com.lagradost.cloudstream3.utils.UIHelper.toPx
|
||||||
import org.schabi.newpipe.extractor.NewPipe
|
import org.schabi.newpipe.extractor.NewPipe
|
||||||
import java.util.*
|
import java.lang.ref.WeakReference
|
||||||
|
import java.util.Locale
|
||||||
|
import kotlin.math.max
|
||||||
|
import kotlin.math.min
|
||||||
|
|
||||||
|
enum class FocusDirection {
|
||||||
|
Start,
|
||||||
|
End,
|
||||||
|
Up,
|
||||||
|
Down,
|
||||||
|
}
|
||||||
|
|
||||||
object CommonActivity {
|
object CommonActivity {
|
||||||
|
|
||||||
|
private var _activity: WeakReference<Activity>? = null
|
||||||
|
var activity
|
||||||
|
get() = _activity?.get()
|
||||||
|
private set(value) {
|
||||||
|
_activity = WeakReference(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
@MainThread
|
||||||
|
fun setActivityInstance(newActivity: Activity?) {
|
||||||
|
activity = newActivity
|
||||||
|
}
|
||||||
|
|
||||||
@MainThread
|
@MainThread
|
||||||
fun Activity?.getCastSession(): CastSession? {
|
fun Activity?.getCastSession(): CastSession? {
|
||||||
return (this as MainActivity?)?.mSessionManager?.currentCastSession
|
return (this as MainActivity?)?.mSessionManager?.currentCastSession
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val displayMetrics: DisplayMetrics = Resources.getSystem().displayMetrics
|
||||||
|
|
||||||
|
// screenWidth and screenHeight does always
|
||||||
|
// refer to the screen while in landscape mode
|
||||||
|
val screenWidth: Int
|
||||||
|
get() {
|
||||||
|
return max(displayMetrics.widthPixels, displayMetrics.heightPixels)
|
||||||
|
}
|
||||||
|
val screenHeight: Int
|
||||||
|
get() {
|
||||||
|
return min(displayMetrics.widthPixels, displayMetrics.heightPixels)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
var canEnterPipMode: Boolean = false
|
var canEnterPipMode: Boolean = false
|
||||||
var canShowPipMode: Boolean = false
|
var canShowPipMode: Boolean = false
|
||||||
|
@ -56,6 +102,30 @@ object CommonActivity {
|
||||||
|
|
||||||
var currentToast: Toast? = null
|
var currentToast: Toast? = null
|
||||||
|
|
||||||
|
fun showToast(@StringRes message: Int, duration: Int? = null) {
|
||||||
|
val act = activity ?: return
|
||||||
|
act.runOnUiThread {
|
||||||
|
showToast(act, act.getString(message), duration)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun showToast(message: String?, duration: Int? = null) {
|
||||||
|
val act = activity ?: return
|
||||||
|
act.runOnUiThread {
|
||||||
|
showToast(act, message, duration)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun showToast(message: UiText?, duration: Int? = null) {
|
||||||
|
val act = activity ?: return
|
||||||
|
if (message == null) return
|
||||||
|
act.runOnUiThread {
|
||||||
|
showToast(act, message.asString(act), duration)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@MainThread
|
||||||
fun showToast(act: Activity?, text: UiText, duration: Int) {
|
fun showToast(act: Activity?, text: UiText, duration: Int) {
|
||||||
if (act == null) return
|
if (act == null) return
|
||||||
text.asStringNull(act)?.let {
|
text.asStringNull(act)?.let {
|
||||||
|
@ -138,22 +208,25 @@ object CommonActivity {
|
||||||
setLocale(this, localeCode)
|
setLocale(this, localeCode)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun init(act: ComponentActivity?) {
|
fun init(act: Activity) {
|
||||||
if (act == null) return
|
setActivityInstance(act)
|
||||||
|
|
||||||
|
val componentActivity = activity as? ComponentActivity ?: return
|
||||||
|
|
||||||
//https://stackoverflow.com/questions/52594181/how-to-know-if-user-has-disabled-picture-in-picture-feature-permission
|
//https://stackoverflow.com/questions/52594181/how-to-know-if-user-has-disabled-picture-in-picture-feature-permission
|
||||||
//https://developer.android.com/guide/topics/ui/picture-in-picture
|
//https://developer.android.com/guide/topics/ui/picture-in-picture
|
||||||
canShowPipMode =
|
canShowPipMode =
|
||||||
Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && // OS SUPPORT
|
Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && // OS SUPPORT
|
||||||
act.packageManager.hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE) && // HAS FEATURE, MIGHT BE BLOCKED DUE TO POWER DRAIN
|
componentActivity.packageManager.hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE) && // HAS FEATURE, MIGHT BE BLOCKED DUE TO POWER DRAIN
|
||||||
act.hasPIPPermission() // CHECK IF FEATURE IS ENABLED IN SETTINGS
|
componentActivity.hasPIPPermission() // CHECK IF FEATURE IS ENABLED IN SETTINGS
|
||||||
|
|
||||||
act.updateLocale()
|
componentActivity.updateLocale()
|
||||||
act.updateTv()
|
componentActivity.updateTv()
|
||||||
NewPipe.init(DownloaderTestImpl.getInstance())
|
NewPipe.init(DownloaderTestImpl.getInstance())
|
||||||
|
|
||||||
for (resumeApp in resumeApps) {
|
for (resumeApp in resumeApps) {
|
||||||
resumeApp.launcher =
|
resumeApp.launcher =
|
||||||
act.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
componentActivity.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
||||||
val resultCode = result.resultCode
|
val resultCode = result.resultCode
|
||||||
val data = result.data
|
val data = result.data
|
||||||
if (resultCode == AppCompatActivity.RESULT_OK && data != null && resumeApp.position != null && resumeApp.duration != null) {
|
if (resultCode == AppCompatActivity.RESULT_OK && data != null && resumeApp.position != null && resumeApp.duration != null) {
|
||||||
|
@ -170,11 +243,11 @@ object CommonActivity {
|
||||||
// Ask for notification permissions on Android 13
|
// Ask for notification permissions on Android 13
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU &&
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU &&
|
||||||
ContextCompat.checkSelfPermission(
|
ContextCompat.checkSelfPermission(
|
||||||
act,
|
componentActivity,
|
||||||
Manifest.permission.POST_NOTIFICATIONS
|
Manifest.permission.POST_NOTIFICATIONS
|
||||||
) != PackageManager.PERMISSION_GRANTED
|
) != PackageManager.PERMISSION_GRANTED
|
||||||
) {
|
) {
|
||||||
val requestPermissionLauncher = act.registerForActivityResult(
|
val requestPermissionLauncher = componentActivity.registerForActivityResult(
|
||||||
ActivityResultContracts.RequestPermission()
|
ActivityResultContracts.RequestPermission()
|
||||||
) { isGranted: Boolean ->
|
) { isGranted: Boolean ->
|
||||||
Log.d(TAG, "Notification permission: $isGranted")
|
Log.d(TAG, "Notification permission: $isGranted")
|
||||||
|
@ -222,18 +295,22 @@ object CommonActivity {
|
||||||
"AmoledLight" -> R.style.AmoledModeLight
|
"AmoledLight" -> R.style.AmoledModeLight
|
||||||
"Monet" -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S)
|
"Monet" -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S)
|
||||||
R.style.MonetMode else R.style.AppTheme
|
R.style.MonetMode else R.style.AppTheme
|
||||||
|
|
||||||
else -> R.style.AppTheme
|
else -> R.style.AppTheme
|
||||||
}
|
}
|
||||||
|
|
||||||
val currentOverlayTheme =
|
val currentOverlayTheme =
|
||||||
when (settingsManager.getString(act.getString(R.string.primary_color_key), "Normal")) {
|
when (settingsManager.getString(act.getString(R.string.primary_color_key), "Normal")) {
|
||||||
"Normal" -> R.style.OverlayPrimaryColorNormal
|
"Normal" -> R.style.OverlayPrimaryColorNormal
|
||||||
|
"DandelionYellow" -> R.style.OverlayPrimaryColorDandelionYellow
|
||||||
"CarnationPink" -> R.style.OverlayPrimaryColorCarnationPink
|
"CarnationPink" -> R.style.OverlayPrimaryColorCarnationPink
|
||||||
|
"Orange" -> R.style.OverlayPrimaryColorOrange
|
||||||
"DarkGreen" -> R.style.OverlayPrimaryColorDarkGreen
|
"DarkGreen" -> R.style.OverlayPrimaryColorDarkGreen
|
||||||
"Maroon" -> R.style.OverlayPrimaryColorMaroon
|
"Maroon" -> R.style.OverlayPrimaryColorMaroon
|
||||||
"NavyBlue" -> R.style.OverlayPrimaryColorNavyBlue
|
"NavyBlue" -> R.style.OverlayPrimaryColorNavyBlue
|
||||||
"Grey" -> R.style.OverlayPrimaryColorGrey
|
"Grey" -> R.style.OverlayPrimaryColorGrey
|
||||||
"White" -> R.style.OverlayPrimaryColorWhite
|
"White" -> R.style.OverlayPrimaryColorWhite
|
||||||
|
"CoolBlue" -> R.style.OverlayPrimaryColorCoolBlue
|
||||||
"Brown" -> R.style.OverlayPrimaryColorBrown
|
"Brown" -> R.style.OverlayPrimaryColorBrown
|
||||||
"Purple" -> R.style.OverlayPrimaryColorPurple
|
"Purple" -> R.style.OverlayPrimaryColorPurple
|
||||||
"Green" -> R.style.OverlayPrimaryColorGreen
|
"Green" -> R.style.OverlayPrimaryColorGreen
|
||||||
|
@ -244,8 +321,10 @@ object CommonActivity {
|
||||||
"Pink" -> R.style.OverlayPrimaryColorPink
|
"Pink" -> R.style.OverlayPrimaryColorPink
|
||||||
"Monet" -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S)
|
"Monet" -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S)
|
||||||
R.style.OverlayPrimaryColorMonet else R.style.OverlayPrimaryColorNormal
|
R.style.OverlayPrimaryColorMonet else R.style.OverlayPrimaryColorNormal
|
||||||
|
|
||||||
"Monet2" -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S)
|
"Monet2" -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S)
|
||||||
R.style.OverlayPrimaryColorMonetTwo else R.style.OverlayPrimaryColorNormal
|
R.style.OverlayPrimaryColorMonetTwo else R.style.OverlayPrimaryColorNormal
|
||||||
|
|
||||||
else -> R.style.OverlayPrimaryColorNormal
|
else -> R.style.OverlayPrimaryColorNormal
|
||||||
}
|
}
|
||||||
act.theme.applyStyle(currentTheme, true)
|
act.theme.applyStyle(currentTheme, true)
|
||||||
|
@ -257,55 +336,138 @@ object CommonActivity {
|
||||||
) // THEME IS SET BEFORE VIEW IS CREATED TO APPLY THE THEME TO THE MAIN VIEW
|
) // THEME IS SET BEFORE VIEW IS CREATED TO APPLY THE THEME TO THE MAIN VIEW
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getNextFocus(
|
/** because we want closes find, aka when multiple have the same id, we go to parent
|
||||||
act: Activity?,
|
until the correct one is found */
|
||||||
|
private fun localLook(from: View, id: Int): View? {
|
||||||
|
if (id == NO_ID) return null
|
||||||
|
var currentLook: View = from
|
||||||
|
// limit to 15 look depth
|
||||||
|
for (i in 0..15) {
|
||||||
|
currentLook.findViewById<View?>(id)?.let { return it }
|
||||||
|
currentLook = (currentLook.parent as? View) ?: break
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
/*var currentLook: View = view
|
||||||
|
while (true) {
|
||||||
|
val tmpNext = currentLook.findViewById<View?>(nextId)
|
||||||
|
if (tmpNext != null) {
|
||||||
|
next = tmpNext
|
||||||
|
break
|
||||||
|
}
|
||||||
|
currentLook = currentLook.parent as? View ?: break
|
||||||
|
}*/
|
||||||
|
|
||||||
|
private fun View.hasContent() : Boolean {
|
||||||
|
return isShown && when(this) {
|
||||||
|
//is RecyclerView -> this.childCount > 0
|
||||||
|
is ViewGroup -> this.childCount > 0
|
||||||
|
else -> true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** skips the initial stage of searching for an id using the view, see getNextFocus for specification */
|
||||||
|
fun continueGetNextFocus(
|
||||||
|
root: Any?,
|
||||||
|
view: View,
|
||||||
|
direction: FocusDirection,
|
||||||
|
nextId: Int,
|
||||||
|
depth: Int = 0
|
||||||
|
): View? {
|
||||||
|
if (nextId == NO_ID) return null
|
||||||
|
|
||||||
|
// do an initial search for the view, in case the localLook is too deep we can use this as
|
||||||
|
// an early break and backup view
|
||||||
|
var next =
|
||||||
|
when (root) {
|
||||||
|
is Activity -> root.findViewById(nextId)
|
||||||
|
is View -> root.rootView.findViewById<View?>(nextId)
|
||||||
|
else -> null
|
||||||
|
} ?: return null
|
||||||
|
|
||||||
|
next = localLook(view, nextId) ?: next
|
||||||
|
val shown = next.hasContent()
|
||||||
|
|
||||||
|
// if cant focus but visible then break and let android decide
|
||||||
|
// the exception if is the view is a parent and has children that wants focus
|
||||||
|
val hasChildrenThatWantsFocus = (next as? ViewGroup)?.let { parent ->
|
||||||
|
parent.descendantFocusability == ViewGroup.FOCUS_AFTER_DESCENDANTS && parent.childCount > 0
|
||||||
|
} ?: false
|
||||||
|
if (!next.isFocusable && shown && !hasChildrenThatWantsFocus) return null
|
||||||
|
|
||||||
|
// if not shown then continue because we will "skip" over views to get to a replacement
|
||||||
|
if (!shown) {
|
||||||
|
// we don't want a while true loop, so we let android decide if we find a recursive view
|
||||||
|
if (next == view) return null
|
||||||
|
return getNextFocus(root, next, direction, depth + 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
(when (next) {
|
||||||
|
is ChipGroup -> {
|
||||||
|
next.children.firstOrNull { it.isFocusable && it.isShown }
|
||||||
|
}
|
||||||
|
|
||||||
|
is NavigationRailView -> {
|
||||||
|
next.findViewById(next.selectedItemId) ?: next.findViewById(R.id.navigation_home)
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> null
|
||||||
|
})?.let {
|
||||||
|
return it
|
||||||
|
}
|
||||||
|
|
||||||
|
// nothing wrong with the view found, return it
|
||||||
|
return next
|
||||||
|
}
|
||||||
|
|
||||||
|
/** recursively looks for a next focus up to a depth of 10,
|
||||||
|
* this is used to override the normal shit focus system
|
||||||
|
* because this application has a lot of invisible views that messes with some tv devices*/
|
||||||
|
fun getNextFocus(
|
||||||
|
root: Any?,
|
||||||
view: View?,
|
view: View?,
|
||||||
direction: FocusDirection,
|
direction: FocusDirection,
|
||||||
depth: Int = 0
|
depth: Int = 0
|
||||||
): Int? {
|
): View? {
|
||||||
if (view == null || depth >= 10 || act == null) {
|
// if input is invalid let android decide + depth test to not crash if loop is found
|
||||||
|
if (view == null || depth >= 10 || root == null) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
val nextId = when (direction) {
|
var nextId = when (direction) {
|
||||||
FocusDirection.Left -> {
|
FocusDirection.Start -> {
|
||||||
view.nextFocusLeftId
|
if (view.isRtl())
|
||||||
|
view.nextFocusRightId
|
||||||
|
else
|
||||||
|
view.nextFocusLeftId
|
||||||
}
|
}
|
||||||
|
|
||||||
FocusDirection.Up -> {
|
FocusDirection.Up -> {
|
||||||
view.nextFocusUpId
|
view.nextFocusUpId
|
||||||
}
|
}
|
||||||
FocusDirection.Right -> {
|
|
||||||
view.nextFocusRightId
|
FocusDirection.End -> {
|
||||||
|
if (view.isRtl())
|
||||||
|
view.nextFocusLeftId
|
||||||
|
else
|
||||||
|
view.nextFocusRightId
|
||||||
}
|
}
|
||||||
|
|
||||||
FocusDirection.Down -> {
|
FocusDirection.Down -> {
|
||||||
view.nextFocusDownId
|
view.nextFocusDownId
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return if (nextId != -1) {
|
if (nextId == NO_ID) {
|
||||||
val next = act.findViewById<View?>(nextId)
|
// if not specified then use forward id
|
||||||
//println("NAME: ${next.accessibilityClassName} | ${next?.isShown}" )
|
nextId = view.nextFocusForwardId
|
||||||
|
// if view is still not found to next focus then return and let android decide
|
||||||
if (next?.isShown == false) {
|
if (nextId == NO_ID)
|
||||||
getNextFocus(act, next, direction, depth + 1)
|
return null
|
||||||
} else {
|
|
||||||
if (depth == 0) {
|
|
||||||
null
|
|
||||||
} else {
|
|
||||||
nextId
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
}
|
}
|
||||||
|
return continueGetNextFocus(root, view, direction, nextId, depth)
|
||||||
}
|
}
|
||||||
|
|
||||||
enum class FocusDirection {
|
|
||||||
Left,
|
|
||||||
Right,
|
|
||||||
Up,
|
|
||||||
Down,
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onKeyDown(act: Activity?, keyCode: Int, event: KeyEvent?) {
|
fun onKeyDown(act: Activity?, keyCode: Int, event: KeyEvent?) {
|
||||||
//println("Keycode: $keyCode")
|
//println("Keycode: $keyCode")
|
||||||
|
@ -328,30 +490,39 @@ object CommonActivity {
|
||||||
KeyEvent.KEYCODE_FORWARD, KeyEvent.KEYCODE_D, KeyEvent.KEYCODE_MEDIA_SKIP_FORWARD, KeyEvent.KEYCODE_MEDIA_FAST_FORWARD -> {
|
KeyEvent.KEYCODE_FORWARD, KeyEvent.KEYCODE_D, KeyEvent.KEYCODE_MEDIA_SKIP_FORWARD, KeyEvent.KEYCODE_MEDIA_FAST_FORWARD -> {
|
||||||
PlayerEventType.SeekForward
|
PlayerEventType.SeekForward
|
||||||
}
|
}
|
||||||
|
|
||||||
KeyEvent.KEYCODE_A, KeyEvent.KEYCODE_MEDIA_SKIP_BACKWARD, KeyEvent.KEYCODE_MEDIA_REWIND -> {
|
KeyEvent.KEYCODE_A, KeyEvent.KEYCODE_MEDIA_SKIP_BACKWARD, KeyEvent.KEYCODE_MEDIA_REWIND -> {
|
||||||
PlayerEventType.SeekBack
|
PlayerEventType.SeekBack
|
||||||
}
|
}
|
||||||
|
|
||||||
KeyEvent.KEYCODE_MEDIA_NEXT, KeyEvent.KEYCODE_BUTTON_R1, KeyEvent.KEYCODE_N -> {
|
KeyEvent.KEYCODE_MEDIA_NEXT, KeyEvent.KEYCODE_BUTTON_R1, KeyEvent.KEYCODE_N -> {
|
||||||
PlayerEventType.NextEpisode
|
PlayerEventType.NextEpisode
|
||||||
}
|
}
|
||||||
|
|
||||||
KeyEvent.KEYCODE_MEDIA_PREVIOUS, KeyEvent.KEYCODE_BUTTON_L1, KeyEvent.KEYCODE_B -> {
|
KeyEvent.KEYCODE_MEDIA_PREVIOUS, KeyEvent.KEYCODE_BUTTON_L1, KeyEvent.KEYCODE_B -> {
|
||||||
PlayerEventType.PrevEpisode
|
PlayerEventType.PrevEpisode
|
||||||
}
|
}
|
||||||
|
|
||||||
KeyEvent.KEYCODE_MEDIA_PAUSE -> {
|
KeyEvent.KEYCODE_MEDIA_PAUSE -> {
|
||||||
PlayerEventType.Pause
|
PlayerEventType.Pause
|
||||||
}
|
}
|
||||||
|
|
||||||
KeyEvent.KEYCODE_MEDIA_PLAY, KeyEvent.KEYCODE_BUTTON_START -> {
|
KeyEvent.KEYCODE_MEDIA_PLAY, KeyEvent.KEYCODE_BUTTON_START -> {
|
||||||
PlayerEventType.Play
|
PlayerEventType.Play
|
||||||
}
|
}
|
||||||
|
|
||||||
KeyEvent.KEYCODE_L, KeyEvent.KEYCODE_NUMPAD_7, KeyEvent.KEYCODE_7 -> {
|
KeyEvent.KEYCODE_L, KeyEvent.KEYCODE_NUMPAD_7, KeyEvent.KEYCODE_7 -> {
|
||||||
PlayerEventType.Lock
|
PlayerEventType.Lock
|
||||||
}
|
}
|
||||||
|
|
||||||
KeyEvent.KEYCODE_H, KeyEvent.KEYCODE_MENU -> {
|
KeyEvent.KEYCODE_H, KeyEvent.KEYCODE_MENU -> {
|
||||||
PlayerEventType.ToggleHide
|
PlayerEventType.ToggleHide
|
||||||
}
|
}
|
||||||
|
|
||||||
KeyEvent.KEYCODE_M, KeyEvent.KEYCODE_VOLUME_MUTE -> {
|
KeyEvent.KEYCODE_M, KeyEvent.KEYCODE_VOLUME_MUTE -> {
|
||||||
PlayerEventType.ToggleMute
|
PlayerEventType.ToggleMute
|
||||||
}
|
}
|
||||||
|
|
||||||
KeyEvent.KEYCODE_S, KeyEvent.KEYCODE_NUMPAD_9, KeyEvent.KEYCODE_9 -> {
|
KeyEvent.KEYCODE_S, KeyEvent.KEYCODE_NUMPAD_9, KeyEvent.KEYCODE_9 -> {
|
||||||
PlayerEventType.ShowMirrors
|
PlayerEventType.ShowMirrors
|
||||||
}
|
}
|
||||||
|
@ -359,21 +530,27 @@ object CommonActivity {
|
||||||
KeyEvent.KEYCODE_O, KeyEvent.KEYCODE_NUMPAD_8, KeyEvent.KEYCODE_8 -> {
|
KeyEvent.KEYCODE_O, KeyEvent.KEYCODE_NUMPAD_8, KeyEvent.KEYCODE_8 -> {
|
||||||
PlayerEventType.SearchSubtitlesOnline
|
PlayerEventType.SearchSubtitlesOnline
|
||||||
}
|
}
|
||||||
|
|
||||||
KeyEvent.KEYCODE_E, KeyEvent.KEYCODE_NUMPAD_3, KeyEvent.KEYCODE_3 -> {
|
KeyEvent.KEYCODE_E, KeyEvent.KEYCODE_NUMPAD_3, KeyEvent.KEYCODE_3 -> {
|
||||||
PlayerEventType.ShowSpeed
|
PlayerEventType.ShowSpeed
|
||||||
}
|
}
|
||||||
|
|
||||||
KeyEvent.KEYCODE_R, KeyEvent.KEYCODE_NUMPAD_0, KeyEvent.KEYCODE_0 -> {
|
KeyEvent.KEYCODE_R, KeyEvent.KEYCODE_NUMPAD_0, KeyEvent.KEYCODE_0 -> {
|
||||||
PlayerEventType.Resize
|
PlayerEventType.Resize
|
||||||
}
|
}
|
||||||
|
|
||||||
KeyEvent.KEYCODE_C, KeyEvent.KEYCODE_NUMPAD_4, KeyEvent.KEYCODE_4 -> {
|
KeyEvent.KEYCODE_C, KeyEvent.KEYCODE_NUMPAD_4, KeyEvent.KEYCODE_4 -> {
|
||||||
PlayerEventType.SkipOp
|
PlayerEventType.SkipOp
|
||||||
}
|
}
|
||||||
|
|
||||||
KeyEvent.KEYCODE_V, KeyEvent.KEYCODE_NUMPAD_5, KeyEvent.KEYCODE_5 -> {
|
KeyEvent.KEYCODE_V, KeyEvent.KEYCODE_NUMPAD_5, KeyEvent.KEYCODE_5 -> {
|
||||||
PlayerEventType.SkipCurrentChapter
|
PlayerEventType.SkipCurrentChapter
|
||||||
}
|
}
|
||||||
|
|
||||||
KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE, KeyEvent.KEYCODE_P, KeyEvent.KEYCODE_SPACE, KeyEvent.KEYCODE_NUMPAD_ENTER, KeyEvent.KEYCODE_ENTER -> { // space is not captured due to navigation
|
KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE, KeyEvent.KEYCODE_P, KeyEvent.KEYCODE_SPACE, KeyEvent.KEYCODE_NUMPAD_ENTER, KeyEvent.KEYCODE_ENTER -> { // space is not captured due to navigation
|
||||||
PlayerEventType.PlayPauseToggle
|
PlayerEventType.PlayPauseToggle
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> null
|
else -> null
|
||||||
}?.let { playerEvent ->
|
}?.let { playerEvent ->
|
||||||
playerEventListener?.invoke(playerEvent)
|
playerEventListener?.invoke(playerEvent)
|
||||||
|
@ -386,64 +563,64 @@ object CommonActivity {
|
||||||
//}
|
//}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** overrides focus and custom key events */
|
||||||
fun dispatchKeyEvent(act: Activity?, event: KeyEvent?): Boolean? {
|
fun dispatchKeyEvent(act: Activity?, event: KeyEvent?): Boolean? {
|
||||||
if (act == null) return null
|
if (act == null) return null
|
||||||
|
val currentFocus = act.currentFocus
|
||||||
|
|
||||||
event?.keyCode?.let { keyCode ->
|
event?.keyCode?.let { keyCode ->
|
||||||
when (event.action) {
|
if (currentFocus == null || event.action != KeyEvent.ACTION_DOWN) return@let
|
||||||
KeyEvent.ACTION_DOWN -> {
|
val nextView = when (keyCode) {
|
||||||
if (act.currentFocus != null) {
|
KeyEvent.KEYCODE_DPAD_LEFT -> getNextFocus(
|
||||||
val next = when (keyCode) {
|
act,
|
||||||
KeyEvent.KEYCODE_DPAD_LEFT -> getNextFocus(
|
currentFocus,
|
||||||
act,
|
FocusDirection.Start
|
||||||
act.currentFocus,
|
)
|
||||||
FocusDirection.Left
|
|
||||||
)
|
|
||||||
KeyEvent.KEYCODE_DPAD_RIGHT -> getNextFocus(
|
|
||||||
act,
|
|
||||||
act.currentFocus,
|
|
||||||
FocusDirection.Right
|
|
||||||
)
|
|
||||||
KeyEvent.KEYCODE_DPAD_UP -> getNextFocus(
|
|
||||||
act,
|
|
||||||
act.currentFocus,
|
|
||||||
FocusDirection.Up
|
|
||||||
)
|
|
||||||
KeyEvent.KEYCODE_DPAD_DOWN -> getNextFocus(
|
|
||||||
act,
|
|
||||||
act.currentFocus,
|
|
||||||
FocusDirection.Down
|
|
||||||
)
|
|
||||||
|
|
||||||
else -> null
|
KeyEvent.KEYCODE_DPAD_RIGHT -> getNextFocus(
|
||||||
}
|
act,
|
||||||
|
currentFocus,
|
||||||
|
FocusDirection.End
|
||||||
|
)
|
||||||
|
|
||||||
if (next != null && next != -1) {
|
KeyEvent.KEYCODE_DPAD_UP -> getNextFocus(
|
||||||
val nextView = act.findViewById<View?>(next)
|
act,
|
||||||
if (nextView != null) {
|
currentFocus,
|
||||||
nextView.requestFocus()
|
FocusDirection.Up
|
||||||
keyEventListener?.invoke(Pair(event, true))
|
)
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
when (keyCode) {
|
KeyEvent.KEYCODE_DPAD_DOWN -> getNextFocus(
|
||||||
KeyEvent.KEYCODE_DPAD_CENTER -> {
|
act,
|
||||||
if (act.currentFocus is SearchView || act.currentFocus is SearchView.SearchAutoComplete) {
|
currentFocus,
|
||||||
UIHelper.showInputMethod(act.currentFocus?.findFocus())
|
FocusDirection.Down
|
||||||
}
|
)
|
||||||
}
|
|
||||||
}
|
else -> null
|
||||||
}
|
|
||||||
//println("Keycode: $keyCode")
|
|
||||||
//showToast(
|
|
||||||
// this,
|
|
||||||
// "Got Keycode $keyCode | ${KeyEvent.keyCodeToString(keyCode)} \n ${event?.action}",
|
|
||||||
// Toast.LENGTH_LONG
|
|
||||||
//)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
// println("NEXT FOCUS : $nextView")
|
||||||
|
if (nextView != null) {
|
||||||
|
nextView.requestFocus()
|
||||||
|
keyEventListener?.invoke(Pair(event, true))
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER &&
|
||||||
|
(act.currentFocus is SearchView || act.currentFocus is SearchView.SearchAutoComplete)
|
||||||
|
) {
|
||||||
|
UIHelper.showInputMethod(act.currentFocus?.findFocus())
|
||||||
|
}
|
||||||
|
|
||||||
|
//println("Keycode: $keyCode")
|
||||||
|
//showToast(
|
||||||
|
// this,
|
||||||
|
// "Got Keycode $keyCode | ${KeyEvent.keyCodeToString(keyCode)} \n ${event?.action}",
|
||||||
|
// Toast.LENGTH_LONG
|
||||||
|
//)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// if someone else want to override the focus then don't handle the event as it is already
|
||||||
|
// consumed. used in video player
|
||||||
if (keyEventListener?.invoke(Pair(event, false)) == true) {
|
if (keyEventListener?.invoke(Pair(event, false)) == true) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
|
@ -50,7 +50,7 @@ class DownloaderTestImpl private constructor(builder: OkHttpClient.Builder) : Do
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val USER_AGENT =
|
private const val USER_AGENT =
|
||||||
"Mozilla/5.0 (Windows NT 10.0; rv:91.0) Gecko/20100101 Firefox/91.0"
|
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36"
|
||||||
private var instance: DownloaderTestImpl? = null
|
private var instance: DownloaderTestImpl? = null
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -9,27 +9,32 @@ import androidx.preference.PreferenceManager
|
||||||
import com.fasterxml.jackson.annotation.JsonProperty
|
import com.fasterxml.jackson.annotation.JsonProperty
|
||||||
import com.fasterxml.jackson.databind.DeserializationFeature
|
import com.fasterxml.jackson.databind.DeserializationFeature
|
||||||
import com.fasterxml.jackson.databind.json.JsonMapper
|
import com.fasterxml.jackson.databind.json.JsonMapper
|
||||||
import com.fasterxml.jackson.module.kotlin.KotlinModule
|
import com.fasterxml.jackson.module.kotlin.kotlinModule
|
||||||
import com.lagradost.cloudstream3.mvvm.logError
|
import com.lagradost.cloudstream3.mvvm.logError
|
||||||
|
import com.lagradost.cloudstream3.mvvm.normalSafeApiCall
|
||||||
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.aniListApi
|
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.aniListApi
|
||||||
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.malApi
|
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.malApi
|
||||||
|
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.simklApi
|
||||||
import com.lagradost.cloudstream3.syncproviders.SyncIdName
|
import com.lagradost.cloudstream3.syncproviders.SyncIdName
|
||||||
|
import com.lagradost.cloudstream3.syncproviders.providers.SimklApi
|
||||||
import com.lagradost.cloudstream3.ui.player.SubtitleData
|
import com.lagradost.cloudstream3.ui.player.SubtitleData
|
||||||
import com.lagradost.cloudstream3.utils.*
|
import com.lagradost.cloudstream3.utils.*
|
||||||
import com.lagradost.cloudstream3.utils.AppUtils.toJson
|
import com.lagradost.cloudstream3.utils.AppUtils.toJson
|
||||||
import com.lagradost.cloudstream3.utils.Coroutines.mainWork
|
import com.lagradost.cloudstream3.utils.Coroutines.mainWork
|
||||||
import com.lagradost.cloudstream3.utils.Coroutines.threadSafeListOf
|
import com.lagradost.cloudstream3.utils.Coroutines.threadSafeListOf
|
||||||
|
import com.lagradost.nicehttp.RequestBodyTypes
|
||||||
import okhttp3.Interceptor
|
import okhttp3.Interceptor
|
||||||
import org.mozilla.javascript.Scriptable
|
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
||||||
|
import okhttp3.RequestBody.Companion.toRequestBody
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import kotlin.math.absoluteValue
|
import kotlin.math.absoluteValue
|
||||||
|
|
||||||
const val USER_AGENT =
|
const val USER_AGENT =
|
||||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36"
|
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36"
|
||||||
|
|
||||||
//val baseHeader = mapOf("User-Agent" to USER_AGENT)
|
//val baseHeader = mapOf("User-Agent" to USER_AGENT)
|
||||||
val mapper = JsonMapper.builder().addModule(KotlinModule())
|
val mapper = JsonMapper.builder().addModule(kotlinModule())
|
||||||
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false).build()!!
|
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false).build()!!
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -50,8 +55,10 @@ object APIHolder {
|
||||||
val allProviders = threadSafeListOf<MainAPI>()
|
val allProviders = threadSafeListOf<MainAPI>()
|
||||||
|
|
||||||
fun initAll() {
|
fun initAll() {
|
||||||
for (api in allProviders) {
|
synchronized(allProviders) {
|
||||||
api.init()
|
for (api in allProviders) {
|
||||||
|
api.init()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
apiMap = null
|
apiMap = null
|
||||||
}
|
}
|
||||||
|
@ -64,27 +71,35 @@ object APIHolder {
|
||||||
var apiMap: Map<String, Int>? = null
|
var apiMap: Map<String, Int>? = null
|
||||||
|
|
||||||
fun addPluginMapping(plugin: MainAPI) {
|
fun addPluginMapping(plugin: MainAPI) {
|
||||||
apis = apis + plugin
|
synchronized(apis) {
|
||||||
|
apis = apis + plugin
|
||||||
|
}
|
||||||
initMap(true)
|
initMap(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun removePluginMapping(plugin: MainAPI) {
|
fun removePluginMapping(plugin: MainAPI) {
|
||||||
apis = apis.filter { it != plugin }
|
synchronized(apis) {
|
||||||
|
apis = apis.filter { it != plugin }
|
||||||
|
}
|
||||||
initMap(true)
|
initMap(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun initMap(forcedUpdate: Boolean = false) {
|
private fun initMap(forcedUpdate: Boolean = false) {
|
||||||
if (apiMap == null || forcedUpdate)
|
synchronized(apis) {
|
||||||
apiMap = apis.mapIndexed { index, api -> api.name to index }.toMap()
|
if (apiMap == null || forcedUpdate)
|
||||||
|
apiMap = apis.mapIndexed { index, api -> api.name to index }.toMap()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getApiFromNameNull(apiName: String?): MainAPI? {
|
fun getApiFromNameNull(apiName: String?): MainAPI? {
|
||||||
if (apiName == null) return null
|
if (apiName == null) return null
|
||||||
synchronized(allProviders) {
|
synchronized(allProviders) {
|
||||||
initMap()
|
initMap()
|
||||||
return apiMap?.get(apiName)?.let { apis.getOrNull(it) }
|
synchronized(apis) {
|
||||||
// Leave the ?. null check, it can crash regardless
|
return apiMap?.get(apiName)?.let { apis.getOrNull(it) }
|
||||||
?: allProviders.firstOrNull { it.name == apiName }
|
// Leave the ?. null check, it can crash regardless
|
||||||
|
?: allProviders.firstOrNull { it.name == apiName }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -164,10 +179,17 @@ object APIHolder {
|
||||||
|
|
||||||
private var trackerCache: HashMap<String, AniSearch> = hashMapOf()
|
private var trackerCache: HashMap<String, AniSearch> = hashMapOf()
|
||||||
|
|
||||||
|
/** backwards compatibility, use getTracker4 instead */
|
||||||
|
suspend fun getTracker(
|
||||||
|
titles: List<String>,
|
||||||
|
types: Set<TrackerType>?,
|
||||||
|
year: Int?,
|
||||||
|
): Tracker? = getTracker(titles, types, year, false)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get anime tracker information based on title, year and type.
|
* Get anime tracker information based on title, year and type.
|
||||||
* Both titles are attempted to be matched with both Romaji and English title.
|
* Both titles are attempted to be matched with both Romaji and English title.
|
||||||
* Uses the consumet api.
|
* Uses the anilist api.
|
||||||
*
|
*
|
||||||
* @param titles uses first index to search, but if you have multiple titles and want extra guarantee to match you can also have that
|
* @param titles uses first index to search, but if you have multiple titles and want extra guarantee to match you can also have that
|
||||||
* @param types Optional parameter to narrow down the scope to Movies, TV, etc. See TrackerType.getTypes()
|
* @param types Optional parameter to narrow down the scope to Movies, TV, etc. See TrackerType.getTypes()
|
||||||
|
@ -176,7 +198,8 @@ object APIHolder {
|
||||||
suspend fun getTracker(
|
suspend fun getTracker(
|
||||||
titles: List<String>,
|
titles: List<String>,
|
||||||
types: Set<TrackerType>?,
|
types: Set<TrackerType>?,
|
||||||
year: Int?
|
year: Int?,
|
||||||
|
lessAccurate: Boolean
|
||||||
): Tracker? {
|
): Tracker? {
|
||||||
return try {
|
return try {
|
||||||
require(titles.isNotEmpty()) { "titles must no be empty when calling getTracker" }
|
require(titles.isNotEmpty()) { "titles must no be empty when calling getTracker" }
|
||||||
|
@ -184,30 +207,70 @@ object APIHolder {
|
||||||
val mainTitle = titles[0]
|
val mainTitle = titles[0]
|
||||||
val search =
|
val search =
|
||||||
trackerCache[mainTitle]
|
trackerCache[mainTitle]
|
||||||
?: app.get("https://api.consumet.org/meta/anilist/$mainTitle")
|
?: searchAnilist(mainTitle)?.also {
|
||||||
.parsedSafe<AniSearch>()?.also {
|
trackerCache[mainTitle] = it
|
||||||
trackerCache[mainTitle] = it
|
} ?: return null
|
||||||
} ?: return null
|
|
||||||
|
|
||||||
val res = search.results?.find { media ->
|
val res = search.data?.page?.media?.find { media ->
|
||||||
val matchingYears = year == null || media.releaseDate == year
|
val matchingYears = year == null || media.seasonYear == year
|
||||||
val matchingTitles = media.title?.let { title ->
|
val matchingTitles = media.title?.let { title ->
|
||||||
titles.any { userTitle ->
|
titles.any { userTitle ->
|
||||||
title.isMatchingTitles(userTitle)
|
title.isMatchingTitles(userTitle)
|
||||||
}
|
}
|
||||||
} ?: false
|
} ?: false
|
||||||
|
|
||||||
val matchingTypes = types?.any { it.name.equals(media.type, true) } == true
|
val matchingTypes = types?.any { it.name.equals(media.format, true) } == true
|
||||||
matchingTitles && matchingTypes && matchingYears
|
if(lessAccurate) matchingTitles || matchingTypes && matchingYears else matchingTitles && matchingTypes && matchingYears
|
||||||
} ?: return null
|
} ?: return null
|
||||||
|
|
||||||
Tracker(res.malId, res.aniId, res.image, res.cover)
|
Tracker(res.idMal, res.id.toString(), res.coverImage?.extraLarge ?: res.coverImage?.large, res.bannerImage)
|
||||||
} catch (t: Throwable) {
|
} catch (t: Throwable) {
|
||||||
logError(t)
|
logError(t)
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private suspend fun searchAnilist(
|
||||||
|
title: String?,
|
||||||
|
): AniSearch? {
|
||||||
|
val query = """
|
||||||
|
query (
|
||||||
|
${'$'}page: Int = 1
|
||||||
|
${'$'}search: String
|
||||||
|
${'$'}sort: [MediaSort] = [POPULARITY_DESC, SCORE_DESC]
|
||||||
|
${'$'}type: MediaType
|
||||||
|
) {
|
||||||
|
Page(page: ${'$'}page, perPage: 20) {
|
||||||
|
media(
|
||||||
|
search: ${'$'}search
|
||||||
|
sort: ${'$'}sort
|
||||||
|
type: ${'$'}type
|
||||||
|
) {
|
||||||
|
id
|
||||||
|
idMal
|
||||||
|
title { romaji english }
|
||||||
|
coverImage { extraLarge large }
|
||||||
|
bannerImage
|
||||||
|
seasonYear
|
||||||
|
format
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""".trimIndent().trim()
|
||||||
|
|
||||||
|
val data = mapOf(
|
||||||
|
"query" to query,
|
||||||
|
"variables" to mapOf(
|
||||||
|
"search" to title,
|
||||||
|
"sort" to "SEARCH_MATCH",
|
||||||
|
"type" to "ANIME",
|
||||||
|
)
|
||||||
|
).toJson().toRequestBody(RequestBodyTypes.JSON.toMediaTypeOrNull())
|
||||||
|
|
||||||
|
return app.post("https://graphql.anilist.co", requestBody = data)
|
||||||
|
.parsedSafe()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
fun Context.getApiSettings(): HashSet<String> {
|
fun Context.getApiSettings(): HashSet<String> {
|
||||||
//val settingsManager = PreferenceManager.getDefaultSharedPreferences(this)
|
//val settingsManager = PreferenceManager.getDefaultSharedPreferences(this)
|
||||||
|
@ -215,7 +278,7 @@ object APIHolder {
|
||||||
val hashSet = HashSet<String>()
|
val hashSet = HashSet<String>()
|
||||||
val activeLangs = getApiProviderLangSettings()
|
val activeLangs = getApiProviderLangSettings()
|
||||||
val hasUniversal = activeLangs.contains(AllLanguagesName)
|
val hasUniversal = activeLangs.contains(AllLanguagesName)
|
||||||
hashSet.addAll(apis.filter { hasUniversal || activeLangs.contains(it.lang) }
|
hashSet.addAll(synchronized(apis) { apis.filter { hasUniversal || activeLangs.contains(it.lang) } }
|
||||||
.map { it.name })
|
.map { it.name })
|
||||||
|
|
||||||
/*val set = settingsManager.getStringSet(
|
/*val set = settingsManager.getStringSet(
|
||||||
|
@ -314,8 +377,9 @@ object APIHolder {
|
||||||
} ?: default
|
} ?: default
|
||||||
val langs = this.getApiProviderLangSettings()
|
val langs = this.getApiProviderLangSettings()
|
||||||
val hasUniversal = langs.contains(AllLanguagesName)
|
val hasUniversal = langs.contains(AllLanguagesName)
|
||||||
val allApis = apis.filter { hasUniversal || langs.contains(it.lang) }
|
val allApis = synchronized(apis) {
|
||||||
.filter { api -> api.hasMainPage || !hasHomePageIsRequired }
|
apis.filter { api -> (hasUniversal || langs.contains(api.lang)) && (api.hasMainPage || !hasHomePageIsRequired) }
|
||||||
|
}
|
||||||
return if (currentPrefMedia.isEmpty()) {
|
return if (currentPrefMedia.isEmpty()) {
|
||||||
allApis
|
allApis
|
||||||
} else {
|
} else {
|
||||||
|
@ -736,6 +800,7 @@ fun fixTitle(str: String): String {
|
||||||
.replaceFirstChar { char -> if (char.isLowerCase()) char.titlecase(Locale.getDefault()) else it }
|
.replaceFirstChar { char -> if (char.isLowerCase()) char.titlecase(Locale.getDefault()) else it }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get rhino context in a safe way as it needs to be initialized on the main thread.
|
* Get rhino context in a safe way as it needs to be initialized on the main thread.
|
||||||
* Make sure you get the scope using: val scope: Scriptable = rhino.initSafeStandardObjects()
|
* Make sure you get the scope using: val scope: Scriptable = rhino.initSafeStandardObjects()
|
||||||
|
@ -801,6 +866,19 @@ enum class TvType(value: Int?) {
|
||||||
Others(12)
|
Others(12)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public enum class AutoDownloadMode(val value: Int) {
|
||||||
|
Disable(0),
|
||||||
|
FilterByLang(1),
|
||||||
|
All(2),
|
||||||
|
NsfwOnly(3)
|
||||||
|
;
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
infix fun getEnum(value: Int): AutoDownloadMode? =
|
||||||
|
AutoDownloadMode.values().firstOrNull { it.value == value }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// IN CASE OF FUTURE ANIME MOVIE OR SMTH
|
// IN CASE OF FUTURE ANIME MOVIE OR SMTH
|
||||||
fun TvType.isMovieType(): Boolean {
|
fun TvType.isMovieType(): Boolean {
|
||||||
return this == TvType.Movie || this == TvType.AnimeMovie || this == TvType.Torrent || this == TvType.Live
|
return this == TvType.Movie || this == TvType.AnimeMovie || this == TvType.Torrent || this == TvType.Live
|
||||||
|
@ -1115,14 +1193,16 @@ interface LoadResponse {
|
||||||
var syncData: MutableMap<String, String>
|
var syncData: MutableMap<String, String>
|
||||||
var posterHeaders: Map<String, String>?
|
var posterHeaders: Map<String, String>?
|
||||||
var backgroundPosterUrl: String?
|
var backgroundPosterUrl: String?
|
||||||
|
var contentRating: String?
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private val malIdPrefix = malApi.idPrefix
|
private val malIdPrefix = malApi.idPrefix
|
||||||
private val aniListIdPrefix = aniListApi.idPrefix
|
private val aniListIdPrefix = aniListApi.idPrefix
|
||||||
|
private val simklIdPrefix = simklApi.idPrefix
|
||||||
var isTrailersEnabled = true
|
var isTrailersEnabled = true
|
||||||
|
|
||||||
fun LoadResponse.isMovie(): Boolean {
|
fun LoadResponse.isMovie(): Boolean {
|
||||||
return this.type.isMovieType()
|
return this.type.isMovieType() || this is MovieLoadResponse
|
||||||
}
|
}
|
||||||
|
|
||||||
@JvmName("addActorNames")
|
@JvmName("addActorNames")
|
||||||
|
@ -1140,6 +1220,20 @@ interface LoadResponse {
|
||||||
this.actors = actors?.map { (actor, role) -> ActorData(actor, role = role) }
|
this.actors = actors?.map { (actor, role) -> ActorData(actor, role = role) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Internal helper function to add simkl ids from other databases.
|
||||||
|
*/
|
||||||
|
private fun LoadResponse.addSimklId(
|
||||||
|
database: SimklApi.Companion.SyncServices,
|
||||||
|
id: String?
|
||||||
|
) {
|
||||||
|
normalSafeApiCall {
|
||||||
|
this.syncData[simklIdPrefix] =
|
||||||
|
SimklApi.addIdToString(this.syncData[simklIdPrefix], database, id.toString())
|
||||||
|
?: return@normalSafeApiCall
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@JvmName("addActorsOnly")
|
@JvmName("addActorsOnly")
|
||||||
fun LoadResponse.addActors(actors: List<Actor>?) {
|
fun LoadResponse.addActors(actors: List<Actor>?) {
|
||||||
this.actors = actors?.map { actor -> ActorData(actor) }
|
this.actors = actors?.map { actor -> ActorData(actor) }
|
||||||
|
@ -1153,12 +1247,30 @@ interface LoadResponse {
|
||||||
return this.syncData[aniListIdPrefix]
|
return this.syncData[aniListIdPrefix]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun LoadResponse.getImdbId(): String? {
|
||||||
|
return normalSafeApiCall {
|
||||||
|
SimklApi.readIdFromString(this.syncData[simklIdPrefix])?.get(SimklApi.Companion.SyncServices.Imdb)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun LoadResponse.getTMDbId(): String? {
|
||||||
|
return normalSafeApiCall {
|
||||||
|
SimklApi.readIdFromString(this.syncData[simklIdPrefix])?.get(SimklApi.Companion.SyncServices.Tmdb)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun LoadResponse.addMalId(id: Int?) {
|
fun LoadResponse.addMalId(id: Int?) {
|
||||||
this.syncData[malIdPrefix] = (id ?: return).toString()
|
this.syncData[malIdPrefix] = (id ?: return).toString()
|
||||||
|
this.addSimklId(SimklApi.Companion.SyncServices.Mal, id.toString())
|
||||||
}
|
}
|
||||||
|
|
||||||
fun LoadResponse.addAniListId(id: Int?) {
|
fun LoadResponse.addAniListId(id: Int?) {
|
||||||
this.syncData[aniListIdPrefix] = (id ?: return).toString()
|
this.syncData[aniListIdPrefix] = (id ?: return).toString()
|
||||||
|
this.addSimklId(SimklApi.Companion.SyncServices.AniList, id.toString())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun LoadResponse.addSimklId(id: Int?) {
|
||||||
|
this.addSimklId(SimklApi.Companion.SyncServices.Simkl, id.toString())
|
||||||
}
|
}
|
||||||
|
|
||||||
fun LoadResponse.addImdbUrl(url: String?) {
|
fun LoadResponse.addImdbUrl(url: String?) {
|
||||||
|
@ -1240,6 +1352,7 @@ interface LoadResponse {
|
||||||
|
|
||||||
fun LoadResponse.addImdbId(id: String?) {
|
fun LoadResponse.addImdbId(id: String?) {
|
||||||
// TODO add imdb sync
|
// TODO add imdb sync
|
||||||
|
this.addSimklId(SimklApi.Companion.SyncServices.Imdb, id)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun LoadResponse.addTrackId(id: String?) {
|
fun LoadResponse.addTrackId(id: String?) {
|
||||||
|
@ -1252,6 +1365,7 @@ interface LoadResponse {
|
||||||
|
|
||||||
fun LoadResponse.addTMDbId(id: String?) {
|
fun LoadResponse.addTMDbId(id: String?) {
|
||||||
// TODO add TMDb sync
|
// TODO add TMDb sync
|
||||||
|
this.addSimklId(SimklApi.Companion.SyncServices.Tmdb, id)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun LoadResponse.addRating(text: String?) {
|
fun LoadResponse.addRating(text: String?) {
|
||||||
|
@ -1352,6 +1466,15 @@ interface EpisodeResponse {
|
||||||
var nextAiring: NextAiring?
|
var nextAiring: NextAiring?
|
||||||
var seasonNames: List<SeasonData>?
|
var seasonNames: List<SeasonData>?
|
||||||
fun getLatestEpisodes(): Map<DubStatus, Int?>
|
fun getLatestEpisodes(): Map<DubStatus, Int?>
|
||||||
|
|
||||||
|
/** Count all episodes in all previous seasons up until this episode to get a total count.
|
||||||
|
* Example:
|
||||||
|
* Season 1: 10 episodes.
|
||||||
|
* Season 2: 6 episodes.
|
||||||
|
*
|
||||||
|
* getTotalEpisodeIndex(episode = 3, season = 2) -> 10 + 3 = 13
|
||||||
|
* */
|
||||||
|
fun getTotalEpisodeIndex(episode: Int, season: Int): Int
|
||||||
}
|
}
|
||||||
|
|
||||||
@JvmName("addSeasonNamesString")
|
@JvmName("addSeasonNamesString")
|
||||||
|
@ -1389,7 +1512,37 @@ data class TorrentLoadResponse(
|
||||||
override var syncData: MutableMap<String, String> = mutableMapOf(),
|
override var syncData: MutableMap<String, String> = mutableMapOf(),
|
||||||
override var posterHeaders: Map<String, String>? = null,
|
override var posterHeaders: Map<String, String>? = null,
|
||||||
override var backgroundPosterUrl: String? = null,
|
override var backgroundPosterUrl: String? = null,
|
||||||
) : LoadResponse
|
override var contentRating: String? = null,
|
||||||
|
) : LoadResponse {
|
||||||
|
/**
|
||||||
|
* Secondary constructor for backwards compatibility without contentRating.
|
||||||
|
* Remove this constructor after there is a new stable release and extensions are updated to support contentRating.
|
||||||
|
*/
|
||||||
|
constructor(
|
||||||
|
name: String,
|
||||||
|
url: String,
|
||||||
|
apiName: String,
|
||||||
|
magnet: String?,
|
||||||
|
torrent: String?,
|
||||||
|
plot: String?,
|
||||||
|
type: TvType = TvType.Torrent,
|
||||||
|
posterUrl: String? = null,
|
||||||
|
year: Int? = null,
|
||||||
|
rating: Int? = null,
|
||||||
|
tags: List<String>? = null,
|
||||||
|
duration: Int? = null,
|
||||||
|
trailers: MutableList<TrailerData> = mutableListOf(),
|
||||||
|
recommendations: List<SearchResponse>? = null,
|
||||||
|
actors: List<ActorData>? = null,
|
||||||
|
comingSoon: Boolean = false,
|
||||||
|
syncData: MutableMap<String, String> = mutableMapOf(),
|
||||||
|
posterHeaders: Map<String, String>? = null,
|
||||||
|
backgroundPosterUrl: String? = null,
|
||||||
|
) : this(
|
||||||
|
name, url, apiName, magnet, torrent, plot, type, posterUrl, year, rating, tags, duration, trailers,
|
||||||
|
recommendations, actors, comingSoon, syncData, posterHeaders, backgroundPosterUrl, null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
data class AnimeLoadResponse(
|
data class AnimeLoadResponse(
|
||||||
var engName: String? = null,
|
var engName: String? = null,
|
||||||
|
@ -1420,6 +1573,7 @@ data class AnimeLoadResponse(
|
||||||
override var nextAiring: NextAiring? = null,
|
override var nextAiring: NextAiring? = null,
|
||||||
override var seasonNames: List<SeasonData>? = null,
|
override var seasonNames: List<SeasonData>? = null,
|
||||||
override var backgroundPosterUrl: String? = null,
|
override var backgroundPosterUrl: String? = null,
|
||||||
|
override var contentRating: String? = null,
|
||||||
) : LoadResponse, EpisodeResponse {
|
) : LoadResponse, EpisodeResponse {
|
||||||
override fun getLatestEpisodes(): Map<DubStatus, Int?> {
|
override fun getLatestEpisodes(): Map<DubStatus, Int?> {
|
||||||
return episodes.map { (status, episodes) ->
|
return episodes.map { (status, episodes) ->
|
||||||
|
@ -1431,6 +1585,54 @@ data class AnimeLoadResponse(
|
||||||
.takeUnless { it == Int.MIN_VALUE }
|
.takeUnless { it == Int.MIN_VALUE }
|
||||||
}.toMap()
|
}.toMap()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun getTotalEpisodeIndex(episode: Int, season: Int): Int {
|
||||||
|
val displayMap = this.seasonNames?.associate { it.season to it.displaySeason } ?: emptyMap()
|
||||||
|
|
||||||
|
return this.episodes.maxOf { (_, episodes) ->
|
||||||
|
episodes.count { episodeData ->
|
||||||
|
// Prioritize display season as actual season may be something random to fit multiple seasons into one.
|
||||||
|
val episodeSeason = displayMap[episodeData.season] ?: episodeData.season ?: Int.MIN_VALUE
|
||||||
|
// Count all episodes from season 1 to below the current season.
|
||||||
|
episodeSeason in 1..<season
|
||||||
|
}
|
||||||
|
} + episode
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Secondary constructor for backwards compatibility without contentRating.
|
||||||
|
* Remove this constructor after there is a new stable release and extensions are updated to support contentRating.
|
||||||
|
*/
|
||||||
|
constructor(
|
||||||
|
engName: String? = null,
|
||||||
|
japName: String? = null,
|
||||||
|
name: String,
|
||||||
|
url: String,
|
||||||
|
apiName: String,
|
||||||
|
type: TvType,
|
||||||
|
posterUrl: String? = null,
|
||||||
|
year: Int? = null,
|
||||||
|
episodes: MutableMap<DubStatus, List<Episode>> = mutableMapOf(),
|
||||||
|
showStatus: ShowStatus? = null,
|
||||||
|
plot: String? = null,
|
||||||
|
tags: List<String>? = null,
|
||||||
|
synonyms: List<String>? = null,
|
||||||
|
rating: Int? = null,
|
||||||
|
duration: Int? = null,
|
||||||
|
trailers: MutableList<TrailerData> = mutableListOf(),
|
||||||
|
recommendations: List<SearchResponse>? = null,
|
||||||
|
actors: List<ActorData>? = null,
|
||||||
|
comingSoon: Boolean = false,
|
||||||
|
syncData: MutableMap<String, String> = mutableMapOf(),
|
||||||
|
posterHeaders: Map<String, String>? = null,
|
||||||
|
nextAiring: NextAiring? = null,
|
||||||
|
seasonNames: List<SeasonData>? = null,
|
||||||
|
backgroundPosterUrl: String? = null,
|
||||||
|
) : this(
|
||||||
|
engName, japName, name, url, apiName, type, posterUrl, year, episodes, showStatus, plot, tags,
|
||||||
|
synonyms, rating, duration, trailers, recommendations, actors, comingSoon, syncData, posterHeaders,
|
||||||
|
nextAiring, seasonNames, backgroundPosterUrl, null
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -1482,7 +1684,36 @@ data class LiveStreamLoadResponse(
|
||||||
override var syncData: MutableMap<String, String> = mutableMapOf(),
|
override var syncData: MutableMap<String, String> = mutableMapOf(),
|
||||||
override var posterHeaders: Map<String, String>? = null,
|
override var posterHeaders: Map<String, String>? = null,
|
||||||
override var backgroundPosterUrl: String? = null,
|
override var backgroundPosterUrl: String? = null,
|
||||||
) : LoadResponse
|
override var contentRating: String? = null,
|
||||||
|
) : LoadResponse {
|
||||||
|
/**
|
||||||
|
* Secondary constructor for backwards compatibility without contentRating.
|
||||||
|
* Remove this constructor after there is a new stable release and extensions are updated to support contentRating.
|
||||||
|
*/
|
||||||
|
constructor(
|
||||||
|
name: String,
|
||||||
|
url: String,
|
||||||
|
apiName: String,
|
||||||
|
dataUrl: String,
|
||||||
|
posterUrl: String? = null,
|
||||||
|
year: Int? = null,
|
||||||
|
plot: String? = null,
|
||||||
|
type: TvType = TvType.Live,
|
||||||
|
rating: Int? = null,
|
||||||
|
tags: List<String>? = null,
|
||||||
|
duration: Int? = null,
|
||||||
|
trailers: MutableList<TrailerData> = mutableListOf(),
|
||||||
|
recommendations: List<SearchResponse>? = null,
|
||||||
|
actors: List<ActorData>? = null,
|
||||||
|
comingSoon: Boolean = false,
|
||||||
|
syncData: MutableMap<String, String> = mutableMapOf(),
|
||||||
|
posterHeaders: Map<String, String>? = null,
|
||||||
|
backgroundPosterUrl: String? = null,
|
||||||
|
) : this(
|
||||||
|
name, url, apiName, dataUrl, posterUrl, year, plot, type, rating, tags, duration, trailers,
|
||||||
|
recommendations, actors, comingSoon, syncData, posterHeaders, backgroundPosterUrl, null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
data class MovieLoadResponse(
|
data class MovieLoadResponse(
|
||||||
override var name: String,
|
override var name: String,
|
||||||
|
@ -1505,7 +1736,36 @@ data class MovieLoadResponse(
|
||||||
override var syncData: MutableMap<String, String> = mutableMapOf(),
|
override var syncData: MutableMap<String, String> = mutableMapOf(),
|
||||||
override var posterHeaders: Map<String, String>? = null,
|
override var posterHeaders: Map<String, String>? = null,
|
||||||
override var backgroundPosterUrl: String? = null,
|
override var backgroundPosterUrl: String? = null,
|
||||||
) : LoadResponse
|
override var contentRating: String? = null,
|
||||||
|
) : LoadResponse {
|
||||||
|
/**
|
||||||
|
* Secondary constructor for backwards compatibility without contentRating.
|
||||||
|
* Remove this constructor after there is a new stable release and extensions are updated to support contentRating.
|
||||||
|
*/
|
||||||
|
constructor(
|
||||||
|
name: String,
|
||||||
|
url: String,
|
||||||
|
apiName: String,
|
||||||
|
type: TvType,
|
||||||
|
dataUrl: String,
|
||||||
|
posterUrl: String? = null,
|
||||||
|
year: Int? = null,
|
||||||
|
plot: String? = null,
|
||||||
|
rating: Int? = null,
|
||||||
|
tags: List<String>? = null,
|
||||||
|
duration: Int? = null,
|
||||||
|
trailers: MutableList<TrailerData> = mutableListOf(),
|
||||||
|
recommendations: List<SearchResponse>? = null,
|
||||||
|
actors: List<ActorData>? = null,
|
||||||
|
comingSoon: Boolean = false,
|
||||||
|
syncData: MutableMap<String, String> = mutableMapOf(),
|
||||||
|
posterHeaders: Map<String, String>? = null,
|
||||||
|
backgroundPosterUrl: String? = null,
|
||||||
|
) : this(
|
||||||
|
name, url, apiName, type, dataUrl, posterUrl, year, plot, rating, tags, duration, trailers,
|
||||||
|
recommendations, actors, comingSoon, syncData, posterHeaders, backgroundPosterUrl,null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun <T> MainAPI.newMovieLoadResponse(
|
suspend fun <T> MainAPI.newMovieLoadResponse(
|
||||||
name: String,
|
name: String,
|
||||||
|
@ -1629,6 +1889,7 @@ data class TvSeriesLoadResponse(
|
||||||
override var nextAiring: NextAiring? = null,
|
override var nextAiring: NextAiring? = null,
|
||||||
override var seasonNames: List<SeasonData>? = null,
|
override var seasonNames: List<SeasonData>? = null,
|
||||||
override var backgroundPosterUrl: String? = null,
|
override var backgroundPosterUrl: String? = null,
|
||||||
|
override var contentRating: String? = null,
|
||||||
) : LoadResponse, EpisodeResponse {
|
) : LoadResponse, EpisodeResponse {
|
||||||
override fun getLatestEpisodes(): Map<DubStatus, Int?> {
|
override fun getLatestEpisodes(): Map<DubStatus, Int?> {
|
||||||
val maxSeason =
|
val maxSeason =
|
||||||
|
@ -1639,6 +1900,49 @@ data class TvSeriesLoadResponse(
|
||||||
.takeUnless { it == Int.MIN_VALUE }
|
.takeUnless { it == Int.MIN_VALUE }
|
||||||
return mapOf(DubStatus.None to max)
|
return mapOf(DubStatus.None to max)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun getTotalEpisodeIndex(episode: Int, season: Int): Int {
|
||||||
|
val displayMap = this.seasonNames?.associate { it.season to it.displaySeason } ?: emptyMap()
|
||||||
|
|
||||||
|
return episodes.count { episodeData ->
|
||||||
|
// Prioritize display season as actual season may be something random to fit multiple seasons into one.
|
||||||
|
val episodeSeason = displayMap[episodeData.season] ?: episodeData.season ?: Int.MIN_VALUE
|
||||||
|
// Count all episodes from season 1 to below the current season.
|
||||||
|
episodeSeason in 1..<season
|
||||||
|
} + episode
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Secondary constructor for backwards compatibility without contentRating.
|
||||||
|
* Remove this constructor after there is a new stable release and extensions are updated to support contentRating.
|
||||||
|
*/
|
||||||
|
constructor(
|
||||||
|
name: String,
|
||||||
|
url: String,
|
||||||
|
apiName: String,
|
||||||
|
type: TvType,
|
||||||
|
episodes: List<Episode>,
|
||||||
|
posterUrl: String? = null,
|
||||||
|
year: Int? = null,
|
||||||
|
plot: String? = null,
|
||||||
|
showStatus: ShowStatus? = null,
|
||||||
|
rating: Int? = null,
|
||||||
|
tags: List<String>? = null,
|
||||||
|
duration: Int? = null,
|
||||||
|
trailers: MutableList<TrailerData> = mutableListOf(),
|
||||||
|
recommendations: List<SearchResponse>? = null,
|
||||||
|
actors: List<ActorData>? = null,
|
||||||
|
comingSoon: Boolean = false,
|
||||||
|
syncData: MutableMap<String, String> = mutableMapOf(),
|
||||||
|
posterHeaders: Map<String, String>? = null,
|
||||||
|
nextAiring: NextAiring? = null,
|
||||||
|
seasonNames: List<SeasonData>? = null,
|
||||||
|
backgroundPosterUrl: String? = null,
|
||||||
|
) : this(
|
||||||
|
name, url, apiName, type, episodes, posterUrl, year, plot, showStatus, rating, tags, duration,
|
||||||
|
trailers, recommendations, actors, comingSoon, syncData, posterHeaders, nextAiring, seasonNames,
|
||||||
|
backgroundPosterUrl, null
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun MainAPI.newTvSeriesLoadResponse(
|
suspend fun MainAPI.newTvSeriesLoadResponse(
|
||||||
|
@ -1679,30 +1983,42 @@ data class Tracker(
|
||||||
val cover: String? = null,
|
val cover: String? = null,
|
||||||
)
|
)
|
||||||
|
|
||||||
data class Title(
|
data class AniSearch(
|
||||||
@JsonProperty("romaji") val romaji: String? = null,
|
@JsonProperty("data") var data: Data? = Data()
|
||||||
@JsonProperty("english") val english: String? = null,
|
|
||||||
) {
|
) {
|
||||||
fun isMatchingTitles(title: String?): Boolean {
|
data class Data(
|
||||||
if (title == null) return false
|
@JsonProperty("Page") var page: Page? = Page()
|
||||||
return english.equals(title, true) || romaji.equals(title, true)
|
) {
|
||||||
|
data class Page(
|
||||||
|
@JsonProperty("media") var media: ArrayList<Media> = arrayListOf()
|
||||||
|
) {
|
||||||
|
data class Media(
|
||||||
|
@JsonProperty("title") var title: Title? = null,
|
||||||
|
@JsonProperty("id") var id: Int? = null,
|
||||||
|
@JsonProperty("idMal") var idMal: Int? = null,
|
||||||
|
@JsonProperty("seasonYear") var seasonYear: Int? = null,
|
||||||
|
@JsonProperty("format") var format: String? = null,
|
||||||
|
@JsonProperty("coverImage") var coverImage: CoverImage? = null,
|
||||||
|
@JsonProperty("bannerImage") var bannerImage: String? = null,
|
||||||
|
) {
|
||||||
|
data class CoverImage(
|
||||||
|
@JsonProperty("extraLarge") var extraLarge: String? = null,
|
||||||
|
@JsonProperty("large") var large: String? = null,
|
||||||
|
)
|
||||||
|
data class Title(
|
||||||
|
@JsonProperty("romaji") var romaji: String? = null,
|
||||||
|
@JsonProperty("english") var english: String? = null,
|
||||||
|
) {
|
||||||
|
fun isMatchingTitles(title: String?): Boolean {
|
||||||
|
if (title == null) return false
|
||||||
|
return english.equals(title, true) || romaji.equals(title, true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
data class Results(
|
|
||||||
@JsonProperty("id") val aniId: String? = null,
|
|
||||||
@JsonProperty("malId") val malId: Int? = null,
|
|
||||||
@JsonProperty("title") val title: Title? = null,
|
|
||||||
@JsonProperty("releaseDate") val releaseDate: Int? = null,
|
|
||||||
@JsonProperty("type") val type: String? = null,
|
|
||||||
@JsonProperty("image") val image: String? = null,
|
|
||||||
@JsonProperty("cover") val cover: String? = null,
|
|
||||||
)
|
|
||||||
|
|
||||||
data class AniSearch(
|
|
||||||
@JsonProperty("results") val results: ArrayList<Results>? = arrayListOf()
|
|
||||||
)
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* used for the getTracker() method
|
* used for the getTracker() method
|
||||||
**/
|
**/
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,53 @@
|
||||||
|
package com.lagradost.cloudstream3
|
||||||
|
|
||||||
|
import com.lagradost.cloudstream3.MainActivity.Companion.lastError
|
||||||
|
import com.lagradost.cloudstream3.mvvm.logError
|
||||||
|
import com.lagradost.cloudstream3.plugins.PluginManager.checkSafeModeFile
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
object NativeCrashHandler {
|
||||||
|
// external fun triggerNativeCrash()
|
||||||
|
/*private external fun initNativeCrashHandler()
|
||||||
|
private external fun getSignalStatus(): Int
|
||||||
|
|
||||||
|
private fun initSignalPolling() = CoroutineScope(Dispatchers.IO).launch {
|
||||||
|
|
||||||
|
//launch {
|
||||||
|
// delay(10000)
|
||||||
|
// triggerNativeCrash()
|
||||||
|
//}
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
delay(10_000)
|
||||||
|
val signal = getSignalStatus()
|
||||||
|
// Signal is initialized to zero
|
||||||
|
if (signal == 0) continue
|
||||||
|
|
||||||
|
// Do not crash in safe mode!
|
||||||
|
if (lastError != null) continue
|
||||||
|
if (checkSafeModeFile()) continue
|
||||||
|
|
||||||
|
AcraApplication.exceptionHandler?.uncaughtException(
|
||||||
|
Thread.currentThread(),
|
||||||
|
RuntimeException("Native crash with code: $signal. Try uninstalling extensions.\n")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun initCrashHandler() {
|
||||||
|
try {
|
||||||
|
System.loadLibrary("native-lib")
|
||||||
|
initNativeCrashHandler()
|
||||||
|
} catch (t: Throwable) {
|
||||||
|
// Make debug crash.
|
||||||
|
if (BuildConfig.DEBUG) throw t
|
||||||
|
logError(t)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
initSignalPolling()
|
||||||
|
}*/
|
||||||
|
}
|
|
@ -1,7 +1,7 @@
|
||||||
package com.lagradost.cloudstream3.extractors
|
package com.lagradost.cloudstream3.extractors
|
||||||
|
|
||||||
|
import com.lagradost.cloudstream3.SubtitleFile
|
||||||
import com.lagradost.cloudstream3.app
|
import com.lagradost.cloudstream3.app
|
||||||
import com.lagradost.cloudstream3.base64Decode
|
|
||||||
import com.lagradost.cloudstream3.utils.*
|
import com.lagradost.cloudstream3.utils.*
|
||||||
|
|
||||||
open class Acefile : ExtractorApi() {
|
open class Acefile : ExtractorApi() {
|
||||||
|
@ -9,31 +9,35 @@ open class Acefile : ExtractorApi() {
|
||||||
override val mainUrl = "https://acefile.co"
|
override val mainUrl = "https://acefile.co"
|
||||||
override val requiresReferer = false
|
override val requiresReferer = false
|
||||||
|
|
||||||
override suspend fun getUrl(url: String, referer: String?): List<ExtractorLink> {
|
override suspend fun getUrl(
|
||||||
val sources = mutableListOf<ExtractorLink>()
|
url: String,
|
||||||
app.get(url).document.select("script").map { script ->
|
referer: String?,
|
||||||
if (script.data().contains("eval(function(p,a,c,k,e,d)")) {
|
subtitleCallback: (SubtitleFile) -> Unit,
|
||||||
val data = getAndUnpack(script.data())
|
callback: (ExtractorLink) -> Unit
|
||||||
val id = data.substringAfter("{\"id\":\"").substringBefore("\",")
|
) {
|
||||||
val key = data.substringAfter("var nfck=\"").substringBefore("\";")
|
val id = "/(?:d|download|player|f|file)/(\\w+)".toRegex().find(url)?.groupValues?.get(1)
|
||||||
app.get("https://acefile.co/local/$id?key=$key").text.let {
|
val script = getAndUnpack(app.get("$mainUrl/player/${id ?: return}").text)
|
||||||
base64Decode(
|
val service = """service\s*=\s*['"]([^'"]+)""".toRegex().find(script)?.groupValues?.get(1)
|
||||||
it.substringAfter("JSON.parse(atob(\"").substringBefore("\"))")
|
val serverUrl = """['"](\S+check&id\S+?)['"]""".toRegex().find(script)?.groupValues?.get(1)
|
||||||
).let { res ->
|
?.replace("\"+service+\"", service ?: return)
|
||||||
sources.add(
|
|
||||||
ExtractorLink(
|
val video = app.get(serverUrl ?: return, referer = "$mainUrl/").parsedSafe<Source>()?.data
|
||||||
name,
|
|
||||||
name,
|
callback.invoke(
|
||||||
res.substringAfter("\"file\":\"").substringBefore("\","),
|
ExtractorLink(
|
||||||
"$mainUrl/",
|
this.name,
|
||||||
Qualities.Unknown.value,
|
this.name,
|
||||||
)
|
video ?: return,
|
||||||
)
|
"",
|
||||||
}
|
Qualities.Unknown.value,
|
||||||
}
|
INFER_TYPE
|
||||||
}
|
)
|
||||||
}
|
)
|
||||||
return sources
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
data class Source(
|
||||||
|
val data: String? = null,
|
||||||
|
)
|
||||||
|
|
||||||
}
|
}
|
|
@ -9,7 +9,7 @@ import java.net.URI
|
||||||
|
|
||||||
open class AsianLoad : ExtractorApi() {
|
open class AsianLoad : ExtractorApi() {
|
||||||
override var name = "AsianLoad"
|
override var name = "AsianLoad"
|
||||||
override var mainUrl = "https://asianembed.io"
|
override var mainUrl = "https://asianhdplay.pro"
|
||||||
override val requiresReferer = true
|
override val requiresReferer = true
|
||||||
|
|
||||||
private val sourceRegex = Regex("""sources:[\W\w]*?file:\s*?["'](.*?)["']""")
|
private val sourceRegex = Regex("""sources:[\W\w]*?file:\s*?["'](.*?)["']""")
|
||||||
|
@ -43,4 +43,4 @@ open class AsianLoad : ExtractorApi() {
|
||||||
return extractedLinksList
|
return extractedLinksList
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,13 +1,11 @@
|
||||||
package com.lagradost.cloudstream3.extractors
|
package com.lagradost.cloudstream3.extractors
|
||||||
|
|
||||||
import com.lagradost.cloudstream3.*
|
import com.lagradost.cloudstream3.USER_AGENT
|
||||||
import com.lagradost.cloudstream3.utils.Qualities
|
import com.lagradost.cloudstream3.app
|
||||||
|
import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson
|
||||||
import com.lagradost.cloudstream3.utils.ExtractorApi
|
import com.lagradost.cloudstream3.utils.ExtractorApi
|
||||||
import com.lagradost.cloudstream3.utils.ExtractorLink
|
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||||
import com.lagradost.cloudstream3.utils.M3u8Helper.Companion.generateM3u8
|
import com.lagradost.cloudstream3.utils.Qualities
|
||||||
import com.fasterxml.jackson.annotation.JsonProperty
|
|
||||||
import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson
|
|
||||||
import android.util.Log
|
|
||||||
import java.net.URLDecoder
|
import java.net.URLDecoder
|
||||||
|
|
||||||
open class Cda: ExtractorApi() {
|
open class Cda: ExtractorApi() {
|
||||||
|
|
|
@ -2,15 +2,17 @@ package com.lagradost.cloudstream3.extractors
|
||||||
|
|
||||||
import com.fasterxml.jackson.annotation.JsonProperty
|
import com.fasterxml.jackson.annotation.JsonProperty
|
||||||
import com.lagradost.cloudstream3.*
|
import com.lagradost.cloudstream3.*
|
||||||
|
import com.lagradost.cloudstream3.extractors.helper.*
|
||||||
|
import com.lagradost.cloudstream3.extractors.helper.AesHelper.cryptoAESHandler
|
||||||
import com.lagradost.cloudstream3.utils.AppUtils
|
import com.lagradost.cloudstream3.utils.AppUtils
|
||||||
import com.lagradost.cloudstream3.utils.ExtractorApi
|
import com.lagradost.cloudstream3.utils.ExtractorApi
|
||||||
import com.lagradost.cloudstream3.utils.ExtractorLink
|
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||||
import com.lagradost.cloudstream3.utils.Qualities
|
import com.lagradost.cloudstream3.utils.M3u8Helper
|
||||||
import javax.crypto.Cipher
|
|
||||||
import javax.crypto.SecretKeyFactory
|
class Moviesapi : Chillx() {
|
||||||
import javax.crypto.spec.IvParameterSpec
|
override val name = "Moviesapi"
|
||||||
import javax.crypto.spec.PBEKeySpec
|
override val mainUrl = "https://w1.moviesapi.club"
|
||||||
import javax.crypto.spec.SecretKeySpec
|
}
|
||||||
|
|
||||||
class Bestx : Chillx() {
|
class Bestx : Chillx() {
|
||||||
override val name = "Bestx"
|
override val name = "Bestx"
|
||||||
|
@ -21,14 +23,12 @@ class Watchx : Chillx() {
|
||||||
override val name = "Watchx"
|
override val name = "Watchx"
|
||||||
override val mainUrl = "https://watchx.top"
|
override val mainUrl = "https://watchx.top"
|
||||||
}
|
}
|
||||||
|
|
||||||
open class Chillx : ExtractorApi() {
|
open class Chillx : ExtractorApi() {
|
||||||
override val name = "Chillx"
|
override val name = "Chillx"
|
||||||
override val mainUrl = "https://chillx.top"
|
override val mainUrl = "https://chillx.top"
|
||||||
override val requiresReferer = true
|
override val requiresReferer = true
|
||||||
|
private var key: String? = null
|
||||||
companion object {
|
|
||||||
private const val KEY = "4VqE3#N7zt&HEP^a"
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun getUrl(
|
override suspend fun getUrl(
|
||||||
url: String,
|
url: String,
|
||||||
|
@ -36,18 +36,36 @@ open class Chillx : ExtractorApi() {
|
||||||
subtitleCallback: (SubtitleFile) -> Unit,
|
subtitleCallback: (SubtitleFile) -> Unit,
|
||||||
callback: (ExtractorLink) -> Unit
|
callback: (ExtractorLink) -> Unit
|
||||||
) {
|
) {
|
||||||
val master = Regex("MasterJS\\s*=\\s*'([^']+)").find(
|
val master = Regex("\\s*=\\s*'([^']+)").find(
|
||||||
app.get(
|
app.get(
|
||||||
url,
|
url,
|
||||||
referer = referer
|
referer = referer ?: "",
|
||||||
|
headers = mapOf(
|
||||||
|
"Accept" to "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8",
|
||||||
|
"Accept-Language" to "en-US,en;q=0.5",
|
||||||
|
)
|
||||||
).text
|
).text
|
||||||
)?.groupValues?.get(1)
|
)?.groupValues?.get(1)
|
||||||
val encData = AppUtils.tryParseJson<AESData>(base64Decode(master ?: return))
|
val decrypt = cryptoAESHandler(master ?: return, getKey().toByteArray(), false)?.replace("\\", "") ?: throw ErrorLoadingException("failed to decrypt")
|
||||||
val decrypt = cryptoAESHandler(encData ?: return, KEY, false)
|
|
||||||
|
|
||||||
val source = Regex("""sources:\s*\[\{"file":"([^"]+)""").find(decrypt)?.groupValues?.get(1)
|
val source = Regex(""""?file"?:\s*"([^"]+)""").find(decrypt)?.groupValues?.get(1)
|
||||||
val tracks = Regex("""tracks:\s*\[(.+)]""").find(decrypt)?.groupValues?.get(1)
|
|
||||||
|
|
||||||
|
val subtitles = Regex("""subtitle"?:\s*"([^"]+)""").find(decrypt)?.groupValues?.get(1)
|
||||||
|
val subtitlePattern = """\[(.*?)\](https?://[^\s,]+)""".toRegex()
|
||||||
|
val matches = subtitlePattern.findAll(subtitles ?: "")
|
||||||
|
val languageUrlPairs = matches.map { matchResult ->
|
||||||
|
val (language, url) = matchResult.destructured
|
||||||
|
decodeUnicodeEscape(language) to url
|
||||||
|
}.toList()
|
||||||
|
|
||||||
|
languageUrlPairs.forEach{ (name, file) ->
|
||||||
|
subtitleCallback.invoke(
|
||||||
|
SubtitleFile(
|
||||||
|
name,
|
||||||
|
file
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
// required
|
// required
|
||||||
val headers = mapOf(
|
val headers = mapOf(
|
||||||
"Accept" to "*/*",
|
"Accept" to "*/*",
|
||||||
|
@ -58,75 +76,27 @@ open class Chillx : ExtractorApi() {
|
||||||
"Origin" to mainUrl,
|
"Origin" to mainUrl,
|
||||||
)
|
)
|
||||||
|
|
||||||
callback.invoke(
|
M3u8Helper.generateM3u8(
|
||||||
ExtractorLink(
|
name,
|
||||||
name,
|
source ?: return,
|
||||||
name,
|
"$mainUrl/",
|
||||||
source ?: return,
|
headers = headers
|
||||||
"$mainUrl/",
|
).forEach(callback)
|
||||||
Qualities.P1080.value,
|
|
||||||
headers = headers,
|
|
||||||
isM3u8 = true
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
AppUtils.tryParseJson<List<Tracks>>("[$tracks]")
|
|
||||||
?.filter { it.kind == "captions" }?.map { track ->
|
|
||||||
subtitleCallback.invoke(
|
|
||||||
SubtitleFile(
|
|
||||||
track.label ?: "",
|
|
||||||
track.file ?: return@map null
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun cryptoAESHandler(
|
private fun decodeUnicodeEscape(input: String): String {
|
||||||
data: AESData,
|
val regex = Regex("u([0-9a-fA-F]{4})")
|
||||||
pass: String,
|
return regex.replace(input) {
|
||||||
encrypt: Boolean = true
|
it.groupValues[1].toInt(16).toChar().toString()
|
||||||
): String {
|
|
||||||
val factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA512")
|
|
||||||
val spec = PBEKeySpec(
|
|
||||||
pass.toCharArray(),
|
|
||||||
data.salt?.hexToByteArray(),
|
|
||||||
data.iterations?.toIntOrNull() ?: 1,
|
|
||||||
256
|
|
||||||
)
|
|
||||||
val key = factory.generateSecret(spec)
|
|
||||||
val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding")
|
|
||||||
return if (!encrypt) {
|
|
||||||
cipher.init(
|
|
||||||
Cipher.DECRYPT_MODE,
|
|
||||||
SecretKeySpec(key.encoded, "AES"),
|
|
||||||
IvParameterSpec(data.iv?.hexToByteArray())
|
|
||||||
)
|
|
||||||
String(cipher.doFinal(base64DecodeArray(data.ciphertext.toString())))
|
|
||||||
} else {
|
|
||||||
cipher.init(
|
|
||||||
Cipher.ENCRYPT_MODE,
|
|
||||||
SecretKeySpec(key.encoded, "AES"),
|
|
||||||
IvParameterSpec(data.iv?.hexToByteArray())
|
|
||||||
)
|
|
||||||
base64Encode(cipher.doFinal(data.ciphertext?.toByteArray()))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun getKey() = key ?: fetchKey().also { key = it }
|
||||||
|
|
||||||
private fun String.hexToByteArray(): ByteArray {
|
private suspend fun fetchKey(): String {
|
||||||
check(length % 2 == 0) { "Must have an even length" }
|
return app.get("https://raw.githubusercontent.com/Sofie99/Resources/main/chillix_key.json").parsed()
|
||||||
return chunked(2)
|
|
||||||
.map { it.toInt(16).toByte() }
|
|
||||||
|
|
||||||
.toByteArray()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
data class AESData(
|
|
||||||
@JsonProperty("ciphertext") val ciphertext: String? = null,
|
|
||||||
@JsonProperty("iv") val iv: String? = null,
|
|
||||||
@JsonProperty("salt") val salt: String? = null,
|
|
||||||
@JsonProperty("iterations") val iterations: String? = null,
|
|
||||||
)
|
|
||||||
|
|
||||||
data class Tracks(
|
data class Tracks(
|
||||||
@JsonProperty("file") val file: String? = null,
|
@JsonProperty("file") val file: String? = null,
|
||||||
@JsonProperty("label") val label: String? = null,
|
@JsonProperty("label") val label: String? = null,
|
||||||
|
|
|
@ -0,0 +1,69 @@
|
||||||
|
// ! Bu araç @keyiflerolsun tarafından | @KekikAkademi için yazılmıştır.
|
||||||
|
|
||||||
|
package com.lagradost.cloudstream3.extractors
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
import com.lagradost.cloudstream3.*
|
||||||
|
import com.lagradost.cloudstream3.utils.*
|
||||||
|
|
||||||
|
open class ContentX : ExtractorApi() {
|
||||||
|
override val name = "ContentX"
|
||||||
|
override val mainUrl = "https://contentx.me"
|
||||||
|
override val requiresReferer = true
|
||||||
|
|
||||||
|
override suspend fun getUrl(url: String, referer: String?, subtitleCallback: (SubtitleFile) -> Unit, callback: (ExtractorLink) -> Unit) {
|
||||||
|
val ext_ref = referer ?: ""
|
||||||
|
Log.d("Kekik_${this.name}", "url » ${url}")
|
||||||
|
|
||||||
|
val i_source = app.get(url, referer=ext_ref).text
|
||||||
|
val i_extract = Regex("""window\.openPlayer\('([^']+)'""").find(i_source)!!.groups[1]?.value ?: throw ErrorLoadingException("i_extract is null")
|
||||||
|
|
||||||
|
val sub_urls = mutableSetOf<String>()
|
||||||
|
Regex("""\"file\":\"([^\"]+)\",\"label\":\"([^\"]+)\"""").findAll(i_source).forEach {
|
||||||
|
val (sub_url, sub_lang) = it.destructured
|
||||||
|
|
||||||
|
if (sub_url in sub_urls) { return@forEach }
|
||||||
|
sub_urls.add(sub_url)
|
||||||
|
|
||||||
|
subtitleCallback.invoke(
|
||||||
|
SubtitleFile(
|
||||||
|
lang = sub_lang.replace("\\u0131", "ı").replace("\\u0130", "İ").replace("\\u00fc", "ü").replace("\\u00e7", "ç"),
|
||||||
|
url = fixUrl(sub_url.replace("\\", ""))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val vid_source = app.get("${mainUrl}/source2.php?v=${i_extract}", referer=ext_ref).text
|
||||||
|
val vid_extract = Regex("""file\":\"([^\"]+)""").find(vid_source)!!.groups[1]?.value ?: throw ErrorLoadingException("vid_extract is null")
|
||||||
|
val m3u_link = vid_extract.replace("\\", "")
|
||||||
|
|
||||||
|
callback.invoke(
|
||||||
|
ExtractorLink(
|
||||||
|
source = this.name,
|
||||||
|
name = this.name,
|
||||||
|
url = m3u_link,
|
||||||
|
referer = url,
|
||||||
|
quality = Qualities.Unknown.value,
|
||||||
|
isM3u8 = true
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
val i_dublaj = Regex(""",\"([^']+)\",\"Türkçe""").find(i_source)!!.groups[1]?.value
|
||||||
|
if (i_dublaj != null) {
|
||||||
|
val dublaj_source = app.get("${mainUrl}/source2.php?v=${i_dublaj}", referer=ext_ref).text
|
||||||
|
val dublaj_extract = Regex("""file\":\"([^\"]+)""").find(dublaj_source)!!.groups[1]?.value ?: throw ErrorLoadingException("dublaj_extract is null")
|
||||||
|
val dublaj_link = dublaj_extract.replace("\\", "")
|
||||||
|
|
||||||
|
callback.invoke(
|
||||||
|
ExtractorLink(
|
||||||
|
source = "${this.name} Türkçe Dublaj",
|
||||||
|
name = "${this.name} Türkçe Dublaj",
|
||||||
|
url = dublaj_link,
|
||||||
|
referer = url,
|
||||||
|
quality = Qualities.Unknown.value,
|
||||||
|
isM3u8 = true
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -7,7 +7,6 @@ import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson
|
||||||
import com.lagradost.cloudstream3.utils.ExtractorApi
|
import com.lagradost.cloudstream3.utils.ExtractorApi
|
||||||
import com.lagradost.cloudstream3.utils.ExtractorLink
|
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||||
import com.lagradost.cloudstream3.utils.M3u8Helper.Companion.generateM3u8
|
import com.lagradost.cloudstream3.utils.M3u8Helper.Companion.generateM3u8
|
||||||
import com.lagradost.cloudstream3.utils.Qualities
|
|
||||||
import java.net.URL
|
import java.net.URL
|
||||||
|
|
||||||
open class Dailymotion : ExtractorApi() {
|
open class Dailymotion : ExtractorApi() {
|
||||||
|
@ -27,21 +26,16 @@ open class Dailymotion : ExtractorApi() {
|
||||||
callback: (ExtractorLink) -> Unit
|
callback: (ExtractorLink) -> Unit
|
||||||
) {
|
) {
|
||||||
val embedUrl = getEmbedUrl(url) ?: return
|
val embedUrl = getEmbedUrl(url) ?: return
|
||||||
val doc = app.get(embedUrl).document
|
val req = app.get(embedUrl)
|
||||||
val prefix = "window.__PLAYER_CONFIG__ = "
|
val prefix = "window.__PLAYER_CONFIG__ = "
|
||||||
val configStr = doc.selectFirst("script:containsData($prefix)")?.data() ?: return
|
val configStr = req.document.selectFirst("script:containsData($prefix)")?.data() ?: return
|
||||||
val config = tryParseJson<Config>(configStr.substringAfter(prefix)) ?: return
|
val config = tryParseJson<Config>(configStr.substringAfter(prefix).substringBefore(";").trim()) ?: return
|
||||||
val id = getVideoId(embedUrl) ?: return
|
val id = getVideoId(embedUrl) ?: return
|
||||||
val dmV1st = config.dmInternalData.v1st
|
val dmV1st = config.dmInternalData.v1st
|
||||||
val dmTs = config.dmInternalData.ts
|
val dmTs = config.dmInternalData.ts
|
||||||
val metaDataUrl =
|
val embedder = config.context.embedder
|
||||||
"$mainUrl/player/metadata/video/$id?locale=en&dmV1st=$dmV1st&dmTs=$dmTs&is_native_app=0"
|
val metaDataUrl = "$mainUrl/player/metadata/video/$id?embedder=$embedder&locale=en-US&dmV1st=$dmV1st&dmTs=$dmTs&is_native_app=0"
|
||||||
val cookies = mapOf(
|
val metaData = app.get(metaDataUrl, referer = embedUrl, cookies = req.cookies)
|
||||||
"v1st" to dmV1st,
|
|
||||||
"dmvk" to config.context.dmvk,
|
|
||||||
"ts" to dmTs.toString()
|
|
||||||
)
|
|
||||||
val metaData = app.get(metaDataUrl, referer = embedUrl, cookies = cookies)
|
|
||||||
.parsedSafe<MetaData>() ?: return
|
.parsedSafe<MetaData>() ?: return
|
||||||
metaData.qualities.forEach { (_, video) ->
|
metaData.qualities.forEach { (_, video) ->
|
||||||
video.forEach {
|
video.forEach {
|
||||||
|
@ -84,13 +78,13 @@ open class Dailymotion : ExtractorApi() {
|
||||||
)
|
)
|
||||||
|
|
||||||
data class InternalData(
|
data class InternalData(
|
||||||
val ts: Int,
|
val ts: Long,
|
||||||
val v1st: String
|
val v1st: String
|
||||||
)
|
)
|
||||||
|
|
||||||
data class Context(
|
data class Context(
|
||||||
@JsonProperty("access_token") val accessToken: String?,
|
@JsonProperty("access_token") val accessToken: String?,
|
||||||
val dmvk: String,
|
val embedder: String?,
|
||||||
)
|
)
|
||||||
|
|
||||||
data class MetaData(
|
data class MetaData(
|
||||||
|
|
|
@ -7,6 +7,10 @@ import com.lagradost.cloudstream3.utils.Qualities
|
||||||
import com.lagradost.cloudstream3.utils.getQualityFromName
|
import com.lagradost.cloudstream3.utils.getQualityFromName
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
|
|
||||||
|
class Dooood : DoodLaExtractor() {
|
||||||
|
override var mainUrl = "https://dooood.com"
|
||||||
|
}
|
||||||
|
|
||||||
class DoodWfExtractor : DoodLaExtractor() {
|
class DoodWfExtractor : DoodLaExtractor() {
|
||||||
override var mainUrl = "https://dood.wf"
|
override var mainUrl = "https://dood.wf"
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,39 @@
|
||||||
|
package com.lagradost.cloudstream3.extractors
|
||||||
|
|
||||||
|
import com.lagradost.cloudstream3.app
|
||||||
|
import com.lagradost.cloudstream3.utils.ExtractorApi
|
||||||
|
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||||
|
import com.lagradost.cloudstream3.utils.Qualities
|
||||||
|
|
||||||
|
open class EmturbovidExtractor : ExtractorApi() {
|
||||||
|
override var name = "Emturbovid"
|
||||||
|
override var mainUrl = "https://emturbovid.com"
|
||||||
|
override val requiresReferer = false
|
||||||
|
|
||||||
|
override suspend fun getUrl(url: String, referer: String?): List<ExtractorLink>? {
|
||||||
|
val response = app.get(
|
||||||
|
url, referer = referer ?: "$mainUrl/"
|
||||||
|
)
|
||||||
|
val playerScript =
|
||||||
|
response.document.selectXpath("//script[contains(text(),'var urlPlay')]")
|
||||||
|
.html()
|
||||||
|
|
||||||
|
val sources = mutableListOf<ExtractorLink>()
|
||||||
|
if (playerScript.isNotBlank()) {
|
||||||
|
val m3u8Url =
|
||||||
|
playerScript.substringAfter("var urlPlay = '").substringBefore("'")
|
||||||
|
|
||||||
|
sources.add(
|
||||||
|
ExtractorLink(
|
||||||
|
source = name,
|
||||||
|
name = name,
|
||||||
|
url = m3u8Url,
|
||||||
|
referer = "$mainUrl/",
|
||||||
|
quality = Qualities.Unknown.value,
|
||||||
|
isM3u8 = true
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return sources
|
||||||
|
}
|
||||||
|
}
|
|
@ -5,6 +5,16 @@ import com.lagradost.cloudstream3.app
|
||||||
import com.lagradost.cloudstream3.utils.*
|
import com.lagradost.cloudstream3.utils.*
|
||||||
import com.lagradost.cloudstream3.utils.M3u8Helper.Companion.generateM3u8
|
import com.lagradost.cloudstream3.utils.M3u8Helper.Companion.generateM3u8
|
||||||
|
|
||||||
|
class Guccihide : Filesim() {
|
||||||
|
override val name = "Guccihide"
|
||||||
|
override var mainUrl = "https://guccihide.com"
|
||||||
|
}
|
||||||
|
|
||||||
|
class Ahvsh : Filesim() {
|
||||||
|
override val name = "Ahvsh"
|
||||||
|
override var mainUrl = "https://ahvsh.com"
|
||||||
|
}
|
||||||
|
|
||||||
class Moviesm4u : Filesim() {
|
class Moviesm4u : Filesim() {
|
||||||
override val mainUrl = "https://moviesm4u.com"
|
override val mainUrl = "https://moviesm4u.com"
|
||||||
override val name = "Moviesm4u"
|
override val name = "Moviesm4u"
|
||||||
|
@ -15,6 +25,11 @@ class FileMoonIn : Filesim() {
|
||||||
override val name = "FileMoon"
|
override val name = "FileMoon"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class StreamhideTo : Filesim() {
|
||||||
|
override val mainUrl = "https://streamhide.to"
|
||||||
|
override val name = "Streamhide"
|
||||||
|
}
|
||||||
|
|
||||||
class StreamhideCom : Filesim() {
|
class StreamhideCom : Filesim() {
|
||||||
override var name: String = "Streamhide"
|
override var name: String = "Streamhide"
|
||||||
override var mainUrl: String = "https://streamhide.com"
|
override var mainUrl: String = "https://streamhide.com"
|
||||||
|
@ -42,7 +57,7 @@ class FileMoonSx : Filesim() {
|
||||||
open class Filesim : ExtractorApi() {
|
open class Filesim : ExtractorApi() {
|
||||||
override val name = "Filesim"
|
override val name = "Filesim"
|
||||||
override val mainUrl = "https://files.im"
|
override val mainUrl = "https://files.im"
|
||||||
override val requiresReferer = false
|
override val requiresReferer = true
|
||||||
|
|
||||||
override suspend fun getUrl(
|
override suspend fun getUrl(
|
||||||
url: String,
|
url: String,
|
||||||
|
@ -50,27 +65,19 @@ open class Filesim : ExtractorApi() {
|
||||||
subtitleCallback: (SubtitleFile) -> Unit,
|
subtitleCallback: (SubtitleFile) -> Unit,
|
||||||
callback: (ExtractorLink) -> Unit
|
callback: (ExtractorLink) -> Unit
|
||||||
) {
|
) {
|
||||||
val response = app.get(url, referer = mainUrl).document
|
val response = app.get(url, referer = referer)
|
||||||
response.select("script[type=text/javascript]").map { script ->
|
val script = if (!getPacked(response.text).isNullOrEmpty()) {
|
||||||
if (script.data().contains(Regex("eval\\(function\\(p,a,c,k,e,[rd]"))) {
|
getAndUnpack(response.text)
|
||||||
val unpackedscript = getAndUnpack(script.data())
|
} else {
|
||||||
val m3u8Regex = Regex("file.\"(.*?m3u8.*?)\"")
|
response.document.selectFirst("script:containsData(sources:)")?.data()
|
||||||
val m3u8 = m3u8Regex.find(unpackedscript)?.destructured?.component1() ?: ""
|
|
||||||
if (m3u8.isNotEmpty()) {
|
|
||||||
generateM3u8(
|
|
||||||
name,
|
|
||||||
m3u8,
|
|
||||||
mainUrl
|
|
||||||
).forEach(callback)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
val m3u8 =
|
||||||
|
Regex("file:\\s*\"(.*?m3u8.*?)\"").find(script ?: return)?.groupValues?.getOrNull(1)
|
||||||
|
generateM3u8(
|
||||||
|
name,
|
||||||
|
m3u8 ?: return,
|
||||||
|
mainUrl
|
||||||
|
).forEach(callback)
|
||||||
}
|
}
|
||||||
|
|
||||||
/* private data class ResponseSource(
|
|
||||||
@JsonProperty("file") val file: String,
|
|
||||||
@JsonProperty("type") val type: String?,
|
|
||||||
@JsonProperty("label") val label: String?
|
|
||||||
) */
|
|
||||||
|
|
||||||
}
|
}
|
|
@ -2,14 +2,10 @@ package com.lagradost.cloudstream3.extractors
|
||||||
|
|
||||||
import com.fasterxml.jackson.annotation.JsonProperty
|
import com.fasterxml.jackson.annotation.JsonProperty
|
||||||
import com.lagradost.cloudstream3.*
|
import com.lagradost.cloudstream3.*
|
||||||
|
import com.lagradost.cloudstream3.extractors.helper.AesHelper.cryptoAESHandler
|
||||||
import com.lagradost.cloudstream3.utils.*
|
import com.lagradost.cloudstream3.utils.*
|
||||||
import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson
|
import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson
|
||||||
import org.jsoup.nodes.Element
|
import org.jsoup.nodes.Element
|
||||||
import java.security.DigestException
|
|
||||||
import java.security.MessageDigest
|
|
||||||
import javax.crypto.Cipher
|
|
||||||
import javax.crypto.spec.IvParameterSpec
|
|
||||||
import javax.crypto.spec.SecretKeySpec
|
|
||||||
|
|
||||||
class DatabaseGdrive2 : Gdriveplayer() {
|
class DatabaseGdrive2 : Gdriveplayer() {
|
||||||
override var mainUrl = "https://databasegdriveplayer.co"
|
override var mainUrl = "https://databasegdriveplayer.co"
|
||||||
|
@ -65,78 +61,6 @@ open class Gdriveplayer : ExtractorApi() {
|
||||||
?.data()?.let { getAndUnpack(it) }
|
?.data()?.let { getAndUnpack(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun String.decodeHex(): ByteArray {
|
|
||||||
check(length % 2 == 0) { "Must have an even length" }
|
|
||||||
return chunked(2)
|
|
||||||
.map { it.toInt(16).toByte() }
|
|
||||||
.toByteArray()
|
|
||||||
}
|
|
||||||
|
|
||||||
// https://stackoverflow.com/a/41434590/8166854
|
|
||||||
private fun GenerateKeyAndIv(
|
|
||||||
password: ByteArray,
|
|
||||||
salt: ByteArray,
|
|
||||||
hashAlgorithm: String = "MD5",
|
|
||||||
keyLength: Int = 32,
|
|
||||||
ivLength: Int = 16,
|
|
||||||
iterations: Int = 1
|
|
||||||
): List<ByteArray>? {
|
|
||||||
|
|
||||||
val md = MessageDigest.getInstance(hashAlgorithm)
|
|
||||||
val digestLength = md.digestLength
|
|
||||||
val targetKeySize = keyLength + ivLength
|
|
||||||
val requiredLength = (targetKeySize + digestLength - 1) / digestLength * digestLength
|
|
||||||
val generatedData = ByteArray(requiredLength)
|
|
||||||
var generatedLength = 0
|
|
||||||
|
|
||||||
try {
|
|
||||||
md.reset()
|
|
||||||
|
|
||||||
while (generatedLength < targetKeySize) {
|
|
||||||
if (generatedLength > 0)
|
|
||||||
md.update(
|
|
||||||
generatedData,
|
|
||||||
generatedLength - digestLength,
|
|
||||||
digestLength
|
|
||||||
)
|
|
||||||
|
|
||||||
md.update(password)
|
|
||||||
md.update(salt, 0, 8)
|
|
||||||
md.digest(generatedData, generatedLength, digestLength)
|
|
||||||
|
|
||||||
for (i in 1 until iterations) {
|
|
||||||
md.update(generatedData, generatedLength, digestLength)
|
|
||||||
md.digest(generatedData, generatedLength, digestLength)
|
|
||||||
}
|
|
||||||
|
|
||||||
generatedLength += digestLength
|
|
||||||
}
|
|
||||||
return listOf(
|
|
||||||
generatedData.copyOfRange(0, keyLength),
|
|
||||||
generatedData.copyOfRange(keyLength, targetKeySize)
|
|
||||||
)
|
|
||||||
} catch (e: DigestException) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun cryptoAESHandler(
|
|
||||||
data: AesData,
|
|
||||||
pass: ByteArray,
|
|
||||||
encrypt: Boolean = true
|
|
||||||
): String? {
|
|
||||||
val (key, iv) = GenerateKeyAndIv(pass, data.s.decodeHex()) ?: return null
|
|
||||||
val cipher = Cipher.getInstance("AES/CBC/NoPadding")
|
|
||||||
return if (!encrypt) {
|
|
||||||
cipher.init(Cipher.DECRYPT_MODE, SecretKeySpec(key, "AES"), IvParameterSpec(iv))
|
|
||||||
String(cipher.doFinal(base64DecodeArray(data.ct)))
|
|
||||||
} else {
|
|
||||||
cipher.init(Cipher.DECRYPT_MODE, SecretKeySpec(key, "AES"), IvParameterSpec(iv))
|
|
||||||
base64Encode(cipher.doFinal(data.ct.toByteArray()))
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun Regex.first(str: String): String? {
|
private fun Regex.first(str: String): String? {
|
||||||
return find(str)?.groupValues?.getOrNull(1)
|
return find(str)?.groupValues?.getOrNull(1)
|
||||||
}
|
}
|
||||||
|
@ -154,14 +78,14 @@ open class Gdriveplayer : ExtractorApi() {
|
||||||
val document = app.get(url).document
|
val document = app.get(url).document
|
||||||
|
|
||||||
val eval = unpackJs(document)?.replace("\\", "") ?: return
|
val eval = unpackJs(document)?.replace("\\", "") ?: return
|
||||||
val data = tryParseJson<AesData>(Regex("data='(\\S+?)'").first(eval)) ?: return
|
val data = Regex("data='(\\S+?)'").first(eval) ?: return
|
||||||
val password = Regex("null,['|\"](\\w+)['|\"]").first(eval)
|
val password = Regex("null,['|\"](\\w+)['|\"]").first(eval)
|
||||||
?.split(Regex("\\D+"))
|
?.split(Regex("\\D+"))
|
||||||
?.joinToString("") {
|
?.joinToString("") {
|
||||||
Char(it.toInt()).toString()
|
Char(it.toInt()).toString()
|
||||||
}.let { Regex("var pass = \"(\\S+?)\"").first(it ?: return)?.toByteArray() }
|
}.let { Regex("var pass = \"(\\S+?)\"").first(it ?: return)?.toByteArray() }
|
||||||
?: throw ErrorLoadingException("can't find password")
|
?: throw ErrorLoadingException("can't find password")
|
||||||
val decryptedData = cryptoAESHandler(data, password, false)?.let { getAndUnpack(it) }?.replace("\\", "")
|
val decryptedData = cryptoAESHandler(data, password, false, "AES/CBC/NoPadding")?.let { getAndUnpack(it) }?.replace("\\", "")
|
||||||
|
|
||||||
val sourceData = decryptedData?.substringAfter("sources:[")?.substringBefore("],")
|
val sourceData = decryptedData?.substringAfter("sources:[")?.substringBefore("],")
|
||||||
val subData = decryptedData?.substringAfter("tracks:[")?.substringBefore("],")
|
val subData = decryptedData?.substringAfter("tracks:[")?.substringBefore("],")
|
||||||
|
@ -194,12 +118,6 @@ open class Gdriveplayer : ExtractorApi() {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
data class AesData(
|
|
||||||
@JsonProperty("ct") val ct: String,
|
|
||||||
@JsonProperty("iv") val iv: String,
|
|
||||||
@JsonProperty("s") val s: String
|
|
||||||
)
|
|
||||||
|
|
||||||
data class Tracks(
|
data class Tracks(
|
||||||
@JsonProperty("file") val file: String,
|
@JsonProperty("file") val file: String,
|
||||||
@JsonProperty("kind") val kind: String,
|
@JsonProperty("kind") val kind: String,
|
||||||
|
|
|
@ -19,9 +19,12 @@ open class Gofile : ExtractorApi() {
|
||||||
subtitleCallback: (SubtitleFile) -> Unit,
|
subtitleCallback: (SubtitleFile) -> Unit,
|
||||||
callback: (ExtractorLink) -> Unit
|
callback: (ExtractorLink) -> Unit
|
||||||
) {
|
) {
|
||||||
val id = Regex("/(?:\\?c=|d/)([\\da-zA-Z]+)").find(url)?.groupValues?.get(1)
|
val id = Regex("/(?:\\?c=|d/)([\\da-zA-Z-]+)").find(url)?.groupValues?.get(1)
|
||||||
val token = app.get("$mainApi/createAccount").parsedSafe<Account>()?.data?.get("token")
|
val token = app.get("$mainApi/createAccount").parsedSafe<Account>()?.data?.get("token")
|
||||||
app.get("$mainApi/getContent?contentId=$id&token=$token&websiteToken=12345")
|
val websiteToken = app.get("$mainUrl/dist/js/alljs.js").text.let {
|
||||||
|
Regex("fetchData.wt\\s*=\\s*\"([^\"]+)").find(it)?.groupValues?.get(1)
|
||||||
|
}
|
||||||
|
app.get("$mainApi/getContent?contentId=$id&token=$token&wt=$websiteToken")
|
||||||
.parsedSafe<Source>()?.data?.contents?.forEach {
|
.parsedSafe<Source>()?.data?.contents?.forEach {
|
||||||
callback.invoke(
|
callback.invoke(
|
||||||
ExtractorLink(
|
ExtractorLink(
|
||||||
|
@ -56,4 +59,4 @@ open class Gofile : ExtractorApi() {
|
||||||
@JsonProperty("data") val data: Data? = null,
|
@JsonProperty("data") val data: Data? = null,
|
||||||
)
|
)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,71 @@
|
||||||
|
// ! Bu araç @keyiflerolsun tarafından | @KekikAkademi için yazılmıştır.
|
||||||
|
|
||||||
|
package com.lagradost.cloudstream3.extractors
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
import com.lagradost.cloudstream3.*
|
||||||
|
import com.lagradost.cloudstream3.utils.*
|
||||||
|
import com.lagradost.cloudstream3.extractors.helper.AesHelper
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty
|
||||||
|
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
||||||
|
import com.fasterxml.jackson.module.kotlin.readValue
|
||||||
|
|
||||||
|
open class HDMomPlayer : ExtractorApi() {
|
||||||
|
override val name = "HDMomPlayer"
|
||||||
|
override val mainUrl = "https://hdmomplayer.com"
|
||||||
|
override val requiresReferer = true
|
||||||
|
|
||||||
|
override suspend fun getUrl(url: String, referer: String?, subtitleCallback: (SubtitleFile) -> Unit, callback: (ExtractorLink) -> Unit) {
|
||||||
|
val m3u_link:String?
|
||||||
|
val ext_ref = referer ?: ""
|
||||||
|
val i_source = app.get(url, referer=ext_ref).text
|
||||||
|
|
||||||
|
val bePlayer = Regex("""bePlayer\('([^']+)',\s*'(\{[^\}]+\})'\);""").find(i_source)?.groupValues
|
||||||
|
if (bePlayer != null) {
|
||||||
|
val bePlayerPass = bePlayer.get(1)
|
||||||
|
val bePlayerData = bePlayer.get(2)
|
||||||
|
val encrypted = AesHelper.cryptoAESHandler(bePlayerData, bePlayerPass.toByteArray(), false)?.replace("\\", "") ?: throw ErrorLoadingException("failed to decrypt")
|
||||||
|
Log.d("Kekik_${this.name}", "encrypted » ${encrypted}")
|
||||||
|
|
||||||
|
m3u_link = Regex("""video_location\":\"([^\"]+)""").find(encrypted)?.groupValues?.get(1)
|
||||||
|
} else {
|
||||||
|
m3u_link = Regex("""file:\"([^\"]+)""").find(i_source)?.groupValues?.get(1)
|
||||||
|
|
||||||
|
val track_str = Regex("""tracks:\[([^\]]+)""").find(i_source)?.groupValues?.get(1)
|
||||||
|
if (track_str != null) {
|
||||||
|
val tracks:List<Track> = jacksonObjectMapper().readValue("[${track_str}]")
|
||||||
|
|
||||||
|
for (track in tracks) {
|
||||||
|
if (track.file == null || track.label == null) continue
|
||||||
|
if (track.label.contains("Forced")) continue
|
||||||
|
|
||||||
|
subtitleCallback.invoke(
|
||||||
|
SubtitleFile(
|
||||||
|
lang = track.label,
|
||||||
|
url = fixUrl(mainUrl + track.file)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
callback.invoke(
|
||||||
|
ExtractorLink(
|
||||||
|
source = this.name,
|
||||||
|
name = this.name,
|
||||||
|
url = m3u_link ?: throw ErrorLoadingException("m3u link not found"),
|
||||||
|
referer = url,
|
||||||
|
quality = Qualities.Unknown.value,
|
||||||
|
isM3u8 = true
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
data class Track(
|
||||||
|
@JsonProperty("file") val file: String?,
|
||||||
|
@JsonProperty("label") val label: String?,
|
||||||
|
@JsonProperty("kind") val kind: String?,
|
||||||
|
@JsonProperty("language") val language: String?,
|
||||||
|
@JsonProperty("default") val default: String?
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,59 @@
|
||||||
|
// ! Bu araç @keyiflerolsun tarafından | @KekikAkademi için yazılmıştır.
|
||||||
|
|
||||||
|
package com.lagradost.cloudstream3.extractors
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
import com.lagradost.cloudstream3.*
|
||||||
|
import com.lagradost.cloudstream3.utils.*
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty
|
||||||
|
|
||||||
|
open class HDPlayerSystem : ExtractorApi() {
|
||||||
|
override val name = "HDPlayerSystem"
|
||||||
|
override val mainUrl = "https://hdplayersystem.live"
|
||||||
|
override val requiresReferer = true
|
||||||
|
|
||||||
|
override suspend fun getUrl(url: String, referer: String?, subtitleCallback: (SubtitleFile) -> Unit, callback: (ExtractorLink) -> Unit) {
|
||||||
|
val ext_ref = referer ?: ""
|
||||||
|
val vid_id = if (url.contains("video/")) {
|
||||||
|
url.substringAfter("video/")
|
||||||
|
} else {
|
||||||
|
url.substringAfter("?data=")
|
||||||
|
}
|
||||||
|
val post_url = "${mainUrl}/player/index.php?data=${vid_id}&do=getVideo"
|
||||||
|
Log.d("Kekik_${this.name}", "post_url » ${post_url}")
|
||||||
|
|
||||||
|
val response = app.post(
|
||||||
|
post_url,
|
||||||
|
data = mapOf(
|
||||||
|
"hash" to vid_id,
|
||||||
|
"r" to ext_ref
|
||||||
|
),
|
||||||
|
referer = ext_ref,
|
||||||
|
headers = mapOf(
|
||||||
|
"Content-Type" to "application/x-www-form-urlencoded; charset=UTF-8",
|
||||||
|
"X-Requested-With" to "XMLHttpRequest"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
val video_response = response.parsedSafe<SystemResponse>() ?: throw ErrorLoadingException("failed to parse response")
|
||||||
|
val m3u_link = video_response.securedLink
|
||||||
|
|
||||||
|
callback.invoke(
|
||||||
|
ExtractorLink(
|
||||||
|
source = this.name,
|
||||||
|
name = this.name,
|
||||||
|
url = m3u_link,
|
||||||
|
referer = ext_ref,
|
||||||
|
quality = Qualities.Unknown.value,
|
||||||
|
type = INFER_TYPE
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
data class SystemResponse(
|
||||||
|
@JsonProperty("hls") val hls: String,
|
||||||
|
@JsonProperty("videoImage") val videoImage: String? = null,
|
||||||
|
@JsonProperty("videoSource") val videoSource: String,
|
||||||
|
@JsonProperty("securedLink") val securedLink: String
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,8 @@
|
||||||
|
// ! Bu araç @keyiflerolsun tarafından | @KekikAkademi için yazılmıştır.
|
||||||
|
|
||||||
|
package com.lagradost.cloudstream3.extractors
|
||||||
|
|
||||||
|
class HDStreamAble : PeaceMakerst() {
|
||||||
|
override var name = "HDStreamAble"
|
||||||
|
override var mainUrl = "https://hdstreamable.com"
|
||||||
|
}
|
|
@ -0,0 +1,23 @@
|
||||||
|
// ! Bu araç @keyiflerolsun tarafından | @KekikAkademi için yazılmıştır.
|
||||||
|
|
||||||
|
package com.lagradost.cloudstream3.extractors
|
||||||
|
|
||||||
|
class Hotlinger : ContentX() {
|
||||||
|
override var name = "Hotlinger"
|
||||||
|
override var mainUrl = "https://hotlinger.com"
|
||||||
|
}
|
||||||
|
|
||||||
|
class FourCX : ContentX() {
|
||||||
|
override var name = "FourCX"
|
||||||
|
override var mainUrl = "https://four.contentx.me"
|
||||||
|
}
|
||||||
|
|
||||||
|
class PlayRu : ContentX() {
|
||||||
|
override var name = "PlayRu"
|
||||||
|
override var mainUrl = "https://playru.net"
|
||||||
|
}
|
||||||
|
|
||||||
|
class FourPlayRu : ContentX() {
|
||||||
|
override var name = "FourPlayRu"
|
||||||
|
override var mainUrl = "https://four.playru.net"
|
||||||
|
}
|
|
@ -7,7 +7,7 @@ import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson
|
||||||
|
|
||||||
class Neonime7n : Hxfile() {
|
class Neonime7n : Hxfile() {
|
||||||
override val name = "Neonime7n"
|
override val name = "Neonime7n"
|
||||||
override val mainUrl = "https://7njctn.neonime.watch"
|
override val mainUrl = "https://neonime.fun"
|
||||||
override val redirect = false
|
override val redirect = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -19,7 +19,7 @@ class Neonime8n : Hxfile() {
|
||||||
|
|
||||||
class KotakAnimeid : Hxfile() {
|
class KotakAnimeid : Hxfile() {
|
||||||
override val name = "KotakAnimeid"
|
override val name = "KotakAnimeid"
|
||||||
override val mainUrl = "https://kotakanimeid.com"
|
override val mainUrl = "https://nontonanimeid.bio"
|
||||||
override val requiresReferer = true
|
override val requiresReferer = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -97,4 +97,4 @@ open class Hxfile : ExtractorApi() {
|
||||||
@JsonProperty("label") val label: String?
|
@JsonProperty("label") val label: String?
|
||||||
)
|
)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,7 +18,8 @@ open class Linkbox : ExtractorApi() {
|
||||||
subtitleCallback: (SubtitleFile) -> Unit,
|
subtitleCallback: (SubtitleFile) -> Unit,
|
||||||
callback: (ExtractorLink) -> Unit
|
callback: (ExtractorLink) -> Unit
|
||||||
) {
|
) {
|
||||||
val id = Regex("""(?:/f/|/file/|\?id=)(\w+)""").find(url)?.groupValues?.get(1)
|
val token = Regex("""(?:/f/|/file/|\?id=)(\w+)""").find(url)?.groupValues?.get(1)
|
||||||
|
val id = app.get("$mainUrl/api/file/share_out_list/?sortField=utime&sortAsc=0&pageNo=1&pageSize=50&shareToken=$token").parsedSafe<Responses>()?.data?.itemId
|
||||||
app.get("$mainUrl/api/file/detail?itemId=$id", referer = url)
|
app.get("$mainUrl/api/file/detail?itemId=$id", referer = url)
|
||||||
.parsedSafe<Responses>()?.data?.itemInfo?.resolutionList?.map { link ->
|
.parsedSafe<Responses>()?.data?.itemInfo?.resolutionList?.map { link ->
|
||||||
callback.invoke(
|
callback.invoke(
|
||||||
|
@ -44,6 +45,7 @@ open class Linkbox : ExtractorApi() {
|
||||||
|
|
||||||
data class Data(
|
data class Data(
|
||||||
@JsonProperty("itemInfo") val itemInfo: ItemInfo? = null,
|
@JsonProperty("itemInfo") val itemInfo: ItemInfo? = null,
|
||||||
|
@JsonProperty("itemId") val itemId: String? = null,
|
||||||
)
|
)
|
||||||
|
|
||||||
data class Responses(
|
data class Responses(
|
||||||
|
|
|
@ -0,0 +1,54 @@
|
||||||
|
// ! Bu araç @keyiflerolsun tarafından | @KekikAkademi için yazılmıştır.
|
||||||
|
|
||||||
|
package com.lagradost.cloudstream3.extractors
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
import com.lagradost.cloudstream3.*
|
||||||
|
import com.lagradost.cloudstream3.utils.*
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty
|
||||||
|
|
||||||
|
open class MailRu : ExtractorApi() {
|
||||||
|
override val name = "MailRu"
|
||||||
|
override val mainUrl = "https://my.mail.ru"
|
||||||
|
override val requiresReferer = false
|
||||||
|
|
||||||
|
override suspend fun getUrl(url: String, referer: String?, subtitleCallback: (SubtitleFile) -> Unit, callback: (ExtractorLink) -> Unit) {
|
||||||
|
val ext_ref = referer ?: ""
|
||||||
|
Log.d("Kekik_${this.name}", "url » ${url}")
|
||||||
|
|
||||||
|
val vid_id = url.substringAfter("video/embed/").trim()
|
||||||
|
val video_req = app.get("${mainUrl}/+/video/meta/${vid_id}", referer=url)
|
||||||
|
val video_key = video_req.cookies["video_key"].toString()
|
||||||
|
Log.d("Kekik_${this.name}", "video_key » ${video_key}")
|
||||||
|
|
||||||
|
val video_data = AppUtils.tryParseJson<MailRuData>(video_req.text) ?: throw ErrorLoadingException("Video not found")
|
||||||
|
|
||||||
|
for (video in video_data.videos) {
|
||||||
|
Log.d("Kekik_${this.name}", "video » ${video}")
|
||||||
|
|
||||||
|
val video_url = if (video.url.startsWith("//")) "https:${video.url}" else video.url
|
||||||
|
|
||||||
|
callback.invoke(
|
||||||
|
ExtractorLink(
|
||||||
|
source = this.name,
|
||||||
|
name = this.name,
|
||||||
|
url = video_url,
|
||||||
|
referer = url,
|
||||||
|
headers = mapOf("Cookie" to "video_key=${video_key}"),
|
||||||
|
quality = getQualityFromName(video.key),
|
||||||
|
isM3u8 = false
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data class MailRuData(
|
||||||
|
@JsonProperty("provider") val provider: String,
|
||||||
|
@JsonProperty("videos") val videos: List<MailRuVideoData>
|
||||||
|
)
|
||||||
|
|
||||||
|
data class MailRuVideoData(
|
||||||
|
@JsonProperty("url") val url: String,
|
||||||
|
@JsonProperty("key") val key: String
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,43 @@
|
||||||
|
package com.lagradost.cloudstream3.extractors
|
||||||
|
|
||||||
|
import com.lagradost.cloudstream3.SubtitleFile
|
||||||
|
import com.lagradost.cloudstream3.app
|
||||||
|
import com.lagradost.cloudstream3.utils.ExtractorApi
|
||||||
|
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||||
|
import com.lagradost.cloudstream3.utils.INFER_TYPE
|
||||||
|
import com.lagradost.cloudstream3.utils.Qualities
|
||||||
|
|
||||||
|
open class Mediafire : ExtractorApi() {
|
||||||
|
override val name = "Mediafire"
|
||||||
|
override val mainUrl = "https://www.mediafire.com"
|
||||||
|
override val requiresReferer = true
|
||||||
|
|
||||||
|
override suspend fun getUrl(
|
||||||
|
url: String,
|
||||||
|
referer: String?,
|
||||||
|
subtitleCallback: (SubtitleFile) -> Unit,
|
||||||
|
callback: (ExtractorLink) -> Unit
|
||||||
|
) {
|
||||||
|
val res = app.get(url, referer = referer).document
|
||||||
|
val title = res.select("div.dl-btn-label").text()
|
||||||
|
val video = res.selectFirst("a#downloadButton")?.attr("href")
|
||||||
|
|
||||||
|
callback.invoke(
|
||||||
|
ExtractorLink(
|
||||||
|
this.name,
|
||||||
|
this.name,
|
||||||
|
video ?: return,
|
||||||
|
"",
|
||||||
|
getQuality(title),
|
||||||
|
INFER_TYPE
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getQuality(str: String?): Int {
|
||||||
|
return Regex("(\\d{3,4})[pP]").find(str ?: "")?.groupValues?.getOrNull(1)?.toIntOrNull()
|
||||||
|
?: Qualities.Unknown.value
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -7,14 +7,12 @@ import com.lagradost.cloudstream3.utils.ExtractorApi
|
||||||
import com.lagradost.cloudstream3.utils.ExtractorLink
|
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||||
import com.lagradost.cloudstream3.utils.M3u8Helper
|
import com.lagradost.cloudstream3.utils.M3u8Helper
|
||||||
|
|
||||||
class SpeedoStream1 : SpeedoStream() {
|
open class Minoplres : ExtractorApi() {
|
||||||
override val mainUrl = "https://speedostream.pm"
|
|
||||||
}
|
|
||||||
|
|
||||||
open class SpeedoStream : ExtractorApi() {
|
override val name = "Minoplres" // formerly SpeedoStream
|
||||||
override val name = "SpeedoStream"
|
|
||||||
override val mainUrl = "https://speedostream.com"
|
|
||||||
override val requiresReferer = true
|
override val requiresReferer = true
|
||||||
|
override val mainUrl = "https://minoplres.xyz" // formerly speedostream.bond
|
||||||
|
private val hostUrl = "https://minoplres.xyz"
|
||||||
|
|
||||||
override suspend fun getUrl(url: String, referer: String?): List<ExtractorLink> {
|
override suspend fun getUrl(url: String, referer: String?): List<ExtractorLink> {
|
||||||
val sources = mutableListOf<ExtractorLink>()
|
val sources = mutableListOf<ExtractorLink>()
|
||||||
|
@ -26,7 +24,7 @@ open class SpeedoStream : ExtractorApi() {
|
||||||
M3u8Helper.generateM3u8(
|
M3u8Helper.generateM3u8(
|
||||||
name,
|
name,
|
||||||
it.file,
|
it.file,
|
||||||
"$mainUrl/",
|
"$hostUrl/",
|
||||||
).forEach { m3uData -> sources.add(m3uData) }
|
).forEach { m3uData -> sources.add(m3uData) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -37,6 +35,4 @@ open class SpeedoStream : ExtractorApi() {
|
||||||
private data class File(
|
private data class File(
|
||||||
@JsonProperty("file") val file: String,
|
@JsonProperty("file") val file: String,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
|
@ -7,7 +7,7 @@ import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||||
import com.lagradost.cloudstream3.utils.M3u8Helper
|
import com.lagradost.cloudstream3.utils.M3u8Helper
|
||||||
|
|
||||||
class MoviehabNet : Moviehab() {
|
class MoviehabNet : Moviehab() {
|
||||||
override var mainUrl = "https://play.moviehab.net"
|
override var mainUrl = "https://play.moviehab.asia"
|
||||||
}
|
}
|
||||||
|
|
||||||
open class Moviehab : ExtractorApi() {
|
open class Moviehab : ExtractorApi() {
|
||||||
|
@ -41,4 +41,4 @@ open class Moviehab : ExtractorApi() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,24 +10,39 @@ open class Mp4Upload : ExtractorApi() {
|
||||||
override var name = "Mp4Upload"
|
override var name = "Mp4Upload"
|
||||||
override var mainUrl = "https://www.mp4upload.com"
|
override var mainUrl = "https://www.mp4upload.com"
|
||||||
private val srcRegex = Regex("""player\.src\("(.*?)"""")
|
private val srcRegex = Regex("""player\.src\("(.*?)"""")
|
||||||
override val requiresReferer = true
|
private val srcRegex2 = Regex("""player\.src\([\w\W]*src: "(.*?)"""")
|
||||||
|
|
||||||
|
override val requiresReferer = true
|
||||||
|
private val idMatch = Regex("""mp4upload\.com/(embed-|)([A-Za-z0-9]*)""")
|
||||||
override suspend fun getUrl(url: String, referer: String?): List<ExtractorLink>? {
|
override suspend fun getUrl(url: String, referer: String?): List<ExtractorLink>? {
|
||||||
with(app.get(url)) {
|
val realUrl = idMatch.find(url)?.groupValues?.get(2)?.let { id ->
|
||||||
getAndUnpack(this.text).let { unpackedText ->
|
"$mainUrl/embed-$id.html"
|
||||||
val quality = unpackedText.lowercase().substringAfter(" height=").substringBefore(" ").toIntOrNull()
|
} ?: url
|
||||||
srcRegex.find(unpackedText)?.groupValues?.get(1)?.let { link ->
|
val response = app.get(realUrl)
|
||||||
return listOf(
|
val unpackedText = getAndUnpack(response.text)
|
||||||
ExtractorLink(
|
val quality =
|
||||||
name,
|
unpackedText.lowercase().substringAfter(" height=").substringBefore(" ").toIntOrNull()
|
||||||
name,
|
srcRegex.find(unpackedText)?.groupValues?.get(1)?.let { link ->
|
||||||
link,
|
return listOf(
|
||||||
url,
|
ExtractorLink(
|
||||||
quality ?: Qualities.Unknown.value,
|
name,
|
||||||
)
|
name,
|
||||||
)
|
link,
|
||||||
}
|
url,
|
||||||
}
|
quality ?: Qualities.Unknown.value,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
srcRegex2.find(unpackedText)?.groupValues?.get(1)?.let { link ->
|
||||||
|
return listOf(
|
||||||
|
ExtractorLink(
|
||||||
|
name,
|
||||||
|
name,
|
||||||
|
link,
|
||||||
|
url,
|
||||||
|
quality ?: Qualities.Unknown.value,
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,7 +9,7 @@ import java.net.URI
|
||||||
|
|
||||||
open class MultiQuality : ExtractorApi() {
|
open class MultiQuality : ExtractorApi() {
|
||||||
override var name = "MultiQuality"
|
override var name = "MultiQuality"
|
||||||
override var mainUrl = "https://gogo-play.net"
|
override var mainUrl = "https://anihdplay.com"
|
||||||
private val sourceRegex = Regex("""file:\s*['"](.*?)['"],label:\s*['"](.*?)['"]""")
|
private val sourceRegex = Regex("""file:\s*['"](.*?)['"],label:\s*['"](.*?)['"]""")
|
||||||
private val m3u8Regex = Regex(""".*?(\d*).m3u8""")
|
private val m3u8Regex = Regex(""".*?(\d*).m3u8""")
|
||||||
private val urlRegex = Regex("""(.*?)([^/]+$)""")
|
private val urlRegex = Regex("""(.*?)([^/]+$)""")
|
||||||
|
@ -56,4 +56,4 @@ open class MultiQuality : ExtractorApi() {
|
||||||
return extractedLinksList
|
return extractedLinksList
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,61 @@
|
||||||
|
// ! Bu araç @keyiflerolsun tarafından | @KekikAkademi için yazılmıştır.
|
||||||
|
|
||||||
|
package com.lagradost.cloudstream3.extractors
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
import com.lagradost.cloudstream3.*
|
||||||
|
import com.lagradost.cloudstream3.utils.*
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty
|
||||||
|
|
||||||
|
open class Odnoklassniki : ExtractorApi() {
|
||||||
|
override val name = "Odnoklassniki"
|
||||||
|
override val mainUrl = "https://odnoklassniki.ru"
|
||||||
|
override val requiresReferer = false
|
||||||
|
|
||||||
|
override suspend fun getUrl(url: String, referer: String?, subtitleCallback: (SubtitleFile) -> Unit, callback: (ExtractorLink) -> Unit) {
|
||||||
|
val ext_ref = referer ?: ""
|
||||||
|
Log.d("Kekik_${this.name}", "url » ${url}")
|
||||||
|
|
||||||
|
val user_agent = mapOf("User-Agent" to "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Mobile Safari/537.36")
|
||||||
|
|
||||||
|
val video_req = app.get(url, headers=user_agent).text.replace("\\"", "\"").replace("\\\\", "\\")
|
||||||
|
.replace(Regex("\\\\u([0-9A-Fa-f]{4})")) { matchResult ->
|
||||||
|
Integer.parseInt(matchResult.groupValues[1], 16).toChar().toString()
|
||||||
|
}
|
||||||
|
val videos_str = Regex("""\"videos\":(\[[^\]]*\])""").find(video_req)?.groupValues?.get(1) ?: throw ErrorLoadingException("Video not found")
|
||||||
|
val videos = AppUtils.tryParseJson<List<OkRuVideo>>(videos_str) ?: throw ErrorLoadingException("Video not found")
|
||||||
|
|
||||||
|
for (video in videos) {
|
||||||
|
Log.d("Kekik_${this.name}", "video » ${video}")
|
||||||
|
|
||||||
|
val video_url = if (video.url.startsWith("//")) "https:${video.url}" else video.url
|
||||||
|
|
||||||
|
val quality = video.name.uppercase()
|
||||||
|
.replace("MOBILE", "144p")
|
||||||
|
.replace("LOWEST", "240p")
|
||||||
|
.replace("LOW", "360p")
|
||||||
|
.replace("SD", "480p")
|
||||||
|
.replace("HD", "720p")
|
||||||
|
.replace("FULL", "1080p")
|
||||||
|
.replace("QUAD", "1440p")
|
||||||
|
.replace("ULTRA", "4k")
|
||||||
|
|
||||||
|
callback.invoke(
|
||||||
|
ExtractorLink(
|
||||||
|
source = this.name,
|
||||||
|
name = this.name,
|
||||||
|
url = video_url,
|
||||||
|
referer = url,
|
||||||
|
quality = getQualityFromName(quality),
|
||||||
|
headers = user_agent,
|
||||||
|
isM3u8 = false
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data class OkRuVideo(
|
||||||
|
@JsonProperty("name") val name: String,
|
||||||
|
@JsonProperty("url") val url: String,
|
||||||
|
)
|
||||||
|
}
|
|
@ -1,67 +1,13 @@
|
||||||
|
// ! Bu araç @keyiflerolsun tarafından | @KekikAkademi için yazılmıştır.
|
||||||
|
|
||||||
package com.lagradost.cloudstream3.extractors
|
package com.lagradost.cloudstream3.extractors
|
||||||
|
|
||||||
import com.fasterxml.jackson.annotation.JsonProperty
|
class OkRuSSL : Odnoklassniki() {
|
||||||
import com.lagradost.cloudstream3.utils.*
|
override var name = "OkRuSSL"
|
||||||
import com.lagradost.cloudstream3.app
|
|
||||||
import com.lagradost.cloudstream3.utils.AppUtils.parseJson
|
|
||||||
|
|
||||||
data class DataOptionsJson (
|
|
||||||
@JsonProperty("flashvars") var flashvars : Flashvars? = Flashvars(),
|
|
||||||
)
|
|
||||||
data class Flashvars (
|
|
||||||
@JsonProperty("metadata") var metadata : String? = null,
|
|
||||||
@JsonProperty("hlsManifestUrl") var hlsManifestUrl : String? = null, //m3u8
|
|
||||||
)
|
|
||||||
|
|
||||||
data class MetadataOkru (
|
|
||||||
@JsonProperty("videos") var videos: ArrayList<Videos> = arrayListOf(),
|
|
||||||
)
|
|
||||||
|
|
||||||
data class Videos (
|
|
||||||
@JsonProperty("name") var name : String,
|
|
||||||
@JsonProperty("url") var url : String,
|
|
||||||
@JsonProperty("seekSchema") var seekSchema : Int? = null,
|
|
||||||
@JsonProperty("disallowed") var disallowed : Boolean? = null
|
|
||||||
)
|
|
||||||
|
|
||||||
class OkRuHttps: OkRu(){
|
|
||||||
override var mainUrl = "https://ok.ru"
|
override var mainUrl = "https://ok.ru"
|
||||||
}
|
}
|
||||||
|
|
||||||
open class OkRu : ExtractorApi() {
|
class OkRuHTTP : Odnoklassniki() {
|
||||||
override var name = "Okru"
|
override var name = "OkRuHTTP"
|
||||||
override var mainUrl = "http://ok.ru"
|
override var mainUrl = "http://ok.ru"
|
||||||
override val requiresReferer = false
|
}
|
||||||
|
|
||||||
override suspend fun getUrl(url: String, referer: String?): List<ExtractorLink>? {
|
|
||||||
val doc = app.get(url).document
|
|
||||||
val sources = ArrayList<ExtractorLink>()
|
|
||||||
val datajson = doc.select("div[data-options]").attr("data-options")
|
|
||||||
if (datajson.isNotBlank()) {
|
|
||||||
val main = parseJson<DataOptionsJson>(datajson)
|
|
||||||
val metadatajson = parseJson<MetadataOkru>(main.flashvars?.metadata!!)
|
|
||||||
val servers = metadatajson.videos
|
|
||||||
servers.forEach {
|
|
||||||
val quality = it.name.uppercase()
|
|
||||||
.replace("MOBILE","144p")
|
|
||||||
.replace("LOWEST","240p")
|
|
||||||
.replace("LOW","360p")
|
|
||||||
.replace("SD","480p")
|
|
||||||
.replace("HD","720p")
|
|
||||||
.replace("FULL","1080p")
|
|
||||||
.replace("QUAD","1440p")
|
|
||||||
.replace("ULTRA","4k")
|
|
||||||
val extractedurl = it.url.replace("\\\\u0026", "&")
|
|
||||||
sources.add(ExtractorLink(
|
|
||||||
name,
|
|
||||||
name = this.name,
|
|
||||||
extractedurl,
|
|
||||||
url,
|
|
||||||
getQualityFromName(quality),
|
|
||||||
isM3u8 = false
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return sources
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -0,0 +1,89 @@
|
||||||
|
// ! Bu araç @keyiflerolsun tarafından | @KekikAkademi için yazılmıştır.
|
||||||
|
|
||||||
|
package com.lagradost.cloudstream3.extractors
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
import com.lagradost.cloudstream3.*
|
||||||
|
import com.lagradost.cloudstream3.utils.*
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty
|
||||||
|
|
||||||
|
open class PeaceMakerst : ExtractorApi() {
|
||||||
|
override val name = "PeaceMakerst"
|
||||||
|
override val mainUrl = "https://peacemakerst.com"
|
||||||
|
override val requiresReferer = true
|
||||||
|
|
||||||
|
override suspend fun getUrl(url: String, referer: String?, subtitleCallback: (SubtitleFile) -> Unit, callback: (ExtractorLink) -> Unit) {
|
||||||
|
val m3u_link:String?
|
||||||
|
val ext_ref = referer ?: ""
|
||||||
|
val post_url = "${url}?do=getVideo"
|
||||||
|
Log.d("Kekik_${this.name}", "post_url » ${post_url}")
|
||||||
|
|
||||||
|
val response = app.post(
|
||||||
|
post_url,
|
||||||
|
data = mapOf(
|
||||||
|
"hash" to url.substringAfter("video/"),
|
||||||
|
"r" to ext_ref,
|
||||||
|
"s" to ""
|
||||||
|
),
|
||||||
|
referer = ext_ref,
|
||||||
|
headers = mapOf(
|
||||||
|
"Content-Type" to "application/x-www-form-urlencoded; charset=UTF-8",
|
||||||
|
"X-Requested-With" to "XMLHttpRequest"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if (response.text.contains("teve2.com.tr\\/embed\\/")) {
|
||||||
|
val teve2_id = response.text.substringAfter("teve2.com.tr\\/embed\\/").substringBefore("\"")
|
||||||
|
val teve2_response = app.get(
|
||||||
|
"https://www.teve2.com.tr/action/media/${teve2_id}",
|
||||||
|
referer = "https://www.teve2.com.tr/embed/${teve2_id}"
|
||||||
|
).parsedSafe<Teve2ApiResponse>() ?: throw ErrorLoadingException("teve2 response is null")
|
||||||
|
|
||||||
|
m3u_link = teve2_response.media.link.serviceUrl + "//" + teve2_response.media.link.securePath
|
||||||
|
} else {
|
||||||
|
val video_response = response.parsedSafe<PeaceResponse>() ?: throw ErrorLoadingException("peace response is null")
|
||||||
|
val video_sources = video_response.videoSources
|
||||||
|
if (video_sources.isNotEmpty()) {
|
||||||
|
m3u_link = video_sources.lastOrNull()?.file
|
||||||
|
} else {
|
||||||
|
m3u_link = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
callback.invoke(
|
||||||
|
ExtractorLink(
|
||||||
|
source = this.name,
|
||||||
|
name = this.name,
|
||||||
|
url = m3u_link ?: throw ErrorLoadingException("m3u link not found"),
|
||||||
|
referer = ext_ref,
|
||||||
|
quality = Qualities.Unknown.value,
|
||||||
|
type = INFER_TYPE
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
data class PeaceResponse(
|
||||||
|
@JsonProperty("videoImage") val videoImage: String?,
|
||||||
|
@JsonProperty("videoSources") val videoSources: List<VideoSource>,
|
||||||
|
@JsonProperty("sIndex") val sIndex: String,
|
||||||
|
@JsonProperty("sourceList") val sourceList: Map<String, String>
|
||||||
|
)
|
||||||
|
|
||||||
|
data class VideoSource(
|
||||||
|
@JsonProperty("file") val file: String,
|
||||||
|
@JsonProperty("label") val label: String,
|
||||||
|
@JsonProperty("type") val type: String
|
||||||
|
)
|
||||||
|
|
||||||
|
data class Teve2ApiResponse(
|
||||||
|
@JsonProperty("Media") val media: Teve2Media
|
||||||
|
)
|
||||||
|
|
||||||
|
data class Teve2Media(
|
||||||
|
@JsonProperty("Link") val link: Teve2Link
|
||||||
|
)
|
||||||
|
|
||||||
|
data class Teve2Link(
|
||||||
|
@JsonProperty("ServiceUrl") val serviceUrl: String,
|
||||||
|
@JsonProperty("SecurePath") val securePath: String
|
||||||
|
)
|
||||||
|
}
|
|
@ -5,6 +5,7 @@ import com.lagradost.cloudstream3.amap
|
||||||
import com.lagradost.cloudstream3.app
|
import com.lagradost.cloudstream3.app
|
||||||
import com.lagradost.cloudstream3.mvvm.suspendSafeApiCall
|
import com.lagradost.cloudstream3.mvvm.suspendSafeApiCall
|
||||||
import com.lagradost.cloudstream3.utils.ExtractorLink
|
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||||
|
import com.lagradost.cloudstream3.utils.INFER_TYPE
|
||||||
import com.lagradost.cloudstream3.utils.extractorApis
|
import com.lagradost.cloudstream3.utils.extractorApis
|
||||||
import com.lagradost.cloudstream3.utils.getQualityFromName
|
import com.lagradost.cloudstream3.utils.getQualityFromName
|
||||||
import com.lagradost.cloudstream3.utils.loadExtractor
|
import com.lagradost.cloudstream3.utils.loadExtractor
|
||||||
|
@ -66,7 +67,7 @@ open class Pelisplus(val mainUrl: String) {
|
||||||
href,
|
href,
|
||||||
page.url,
|
page.url,
|
||||||
getQualityFromName(qual),
|
getQualityFromName(qual),
|
||||||
element.attr("href").contains(".m3u8")
|
type = INFER_TYPE
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,25 @@
|
||||||
|
// ! Bu araç @keyiflerolsun tarafından | @KekikAkademi için yazılmıştır.
|
||||||
|
|
||||||
|
package com.lagradost.cloudstream3.extractors
|
||||||
|
|
||||||
|
import com.lagradost.cloudstream3.*
|
||||||
|
import com.lagradost.cloudstream3.utils.*
|
||||||
|
|
||||||
|
open class PixelDrain : ExtractorApi() {
|
||||||
|
override val name = "PixelDrain"
|
||||||
|
override val mainUrl = "https://pixeldrain.com"
|
||||||
|
override val requiresReferer = true
|
||||||
|
|
||||||
|
override suspend fun getUrl(url: String, referer: String?, subtitleCallback: (SubtitleFile) -> Unit, callback: (ExtractorLink) -> Unit) {
|
||||||
|
val mId = Regex("/([ul]/[\\da-zA-Z\\-]+)(?:\\?download)?").find(url)?.groupValues?.get(1)?.split("/")
|
||||||
|
callback.invoke(
|
||||||
|
ExtractorLink(
|
||||||
|
this.name,
|
||||||
|
this.name,
|
||||||
|
"$mainUrl/api/file/${mId?.last() ?: return}?download",
|
||||||
|
url,
|
||||||
|
Qualities.Unknown.value,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,199 @@
|
||||||
|
package com.lagradost.cloudstream3.extractors
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty
|
||||||
|
import com.lagradost.cloudstream3.ErrorLoadingException
|
||||||
|
import com.lagradost.cloudstream3.SubtitleFile
|
||||||
|
import com.lagradost.cloudstream3.app
|
||||||
|
import com.lagradost.cloudstream3.base64DecodeArray
|
||||||
|
import com.lagradost.cloudstream3.base64Encode
|
||||||
|
import com.lagradost.cloudstream3.utils.AppUtils
|
||||||
|
import com.lagradost.cloudstream3.utils.AppUtils.parseJson
|
||||||
|
import com.lagradost.cloudstream3.utils.ExtractorApi
|
||||||
|
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||||
|
import com.lagradost.cloudstream3.utils.M3u8Helper
|
||||||
|
import java.nio.charset.StandardCharsets
|
||||||
|
import java.security.MessageDigest
|
||||||
|
import javax.crypto.Cipher
|
||||||
|
import javax.crypto.spec.IvParameterSpec
|
||||||
|
import javax.crypto.spec.SecretKeySpec
|
||||||
|
|
||||||
|
class Megacloud : Rabbitstream() {
|
||||||
|
override val name = "Megacloud"
|
||||||
|
override val mainUrl = "https://megacloud.tv"
|
||||||
|
override val embed = "embed-2/ajax/e-1"
|
||||||
|
private val scriptUrl = "$mainUrl/js/player/a/prod/e1-player.min.js"
|
||||||
|
|
||||||
|
override suspend fun extractRealKey(sources: String): Pair<String, String> {
|
||||||
|
val rawKeys = getKeys()
|
||||||
|
val sourcesArray = sources.toCharArray()
|
||||||
|
|
||||||
|
var extractedKey = ""
|
||||||
|
var currentIndex = 0
|
||||||
|
for (index in rawKeys) {
|
||||||
|
val start = index[0] + currentIndex
|
||||||
|
val end = start + index[1]
|
||||||
|
for (i in start until end) {
|
||||||
|
extractedKey += sourcesArray[i].toString()
|
||||||
|
sourcesArray[i] = ' '
|
||||||
|
}
|
||||||
|
currentIndex += index[1]
|
||||||
|
}
|
||||||
|
|
||||||
|
return extractedKey to sourcesArray.joinToString("").replace(" ", "")
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun getKeys(): List<List<Int>> {
|
||||||
|
val script = app.get(scriptUrl).text
|
||||||
|
fun matchingKey(value: String): String {
|
||||||
|
return Regex(",$value=((?:0x)?([0-9a-fA-F]+))").find(script)?.groupValues?.get(1)
|
||||||
|
?.removePrefix("0x") ?: throw ErrorLoadingException("Failed to match the key")
|
||||||
|
}
|
||||||
|
|
||||||
|
val regex = Regex("case\\s*0x[0-9a-f]+:(?![^;]*=partKey)\\s*\\w+\\s*=\\s*(\\w+)\\s*,\\s*\\w+\\s*=\\s*(\\w+);")
|
||||||
|
val indexPairs = regex.findAll(script).toList().map { match ->
|
||||||
|
val matchKey1 = matchingKey(match.groupValues[1])
|
||||||
|
val matchKey2 = matchingKey(match.groupValues[2])
|
||||||
|
try {
|
||||||
|
listOf(matchKey1.toInt(16), matchKey2.toInt(16))
|
||||||
|
} catch (e: NumberFormatException) {
|
||||||
|
emptyList()
|
||||||
|
}
|
||||||
|
}.filter { it.isNotEmpty() }
|
||||||
|
|
||||||
|
return indexPairs
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
class Dokicloud : Rabbitstream() {
|
||||||
|
override val name = "Dokicloud"
|
||||||
|
override val mainUrl = "https://dokicloud.one"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Code found in https://github.com/eatmynerds/key
|
||||||
|
// special credits to @eatmynerds for providing key
|
||||||
|
open class Rabbitstream : ExtractorApi() {
|
||||||
|
override val name = "Rabbitstream"
|
||||||
|
override val mainUrl = "https://rabbitstream.net"
|
||||||
|
override val requiresReferer = false
|
||||||
|
open val embed = "ajax/embed-4"
|
||||||
|
open val key = "https://raw.githubusercontent.com/eatmynerds/key/e4/key.txt"
|
||||||
|
|
||||||
|
override suspend fun getUrl(
|
||||||
|
url: String,
|
||||||
|
referer: String?,
|
||||||
|
subtitleCallback: (SubtitleFile) -> Unit,
|
||||||
|
callback: (ExtractorLink) -> Unit
|
||||||
|
) {
|
||||||
|
val id = url.substringAfterLast("/").substringBefore("?")
|
||||||
|
|
||||||
|
val response = app.get(
|
||||||
|
"$mainUrl/$embed/getSources?id=$id",
|
||||||
|
referer = mainUrl,
|
||||||
|
headers = mapOf("X-Requested-With" to "XMLHttpRequest")
|
||||||
|
)
|
||||||
|
|
||||||
|
val encryptedMap = response.parsedSafe<SourcesEncrypted>()
|
||||||
|
val sources = encryptedMap?.sources
|
||||||
|
val decryptedSources = if (sources == null || encryptedMap.encrypted == false) {
|
||||||
|
response.parsedSafe()
|
||||||
|
} else {
|
||||||
|
val (key, encData) = extractRealKey(sources)
|
||||||
|
val decrypted = decryptMapped<List<Sources>>(encData, key)
|
||||||
|
SourcesResponses(
|
||||||
|
sources = decrypted,
|
||||||
|
tracks = encryptedMap.tracks
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
decryptedSources?.sources?.map { source ->
|
||||||
|
M3u8Helper.generateM3u8(
|
||||||
|
name,
|
||||||
|
source?.file ?: return@map,
|
||||||
|
"$mainUrl/",
|
||||||
|
).forEach(callback)
|
||||||
|
}
|
||||||
|
|
||||||
|
decryptedSources?.tracks?.map { track ->
|
||||||
|
subtitleCallback.invoke(
|
||||||
|
SubtitleFile(
|
||||||
|
track?.label ?: return@map,
|
||||||
|
track.file ?: return@map
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
open suspend fun extractRealKey(sources: String): Pair<String, String> {
|
||||||
|
val rawKeys = parseJson<List<Int>>(app.get(key).text)
|
||||||
|
val extractedKey = base64Encode(rawKeys.map { it.toByte() }.toByteArray())
|
||||||
|
return extractedKey to sources
|
||||||
|
}
|
||||||
|
|
||||||
|
private inline fun <reified T> decryptMapped(input: String, key: String): T? {
|
||||||
|
val decrypt = decrypt(input, key)
|
||||||
|
return AppUtils.tryParseJson(decrypt)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun decrypt(input: String, key: String): String {
|
||||||
|
return decryptSourceUrl(
|
||||||
|
generateKey(
|
||||||
|
base64DecodeArray(input).copyOfRange(8, 16),
|
||||||
|
key.toByteArray()
|
||||||
|
), input
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun generateKey(salt: ByteArray, secret: ByteArray): ByteArray {
|
||||||
|
var key = md5(secret + salt)
|
||||||
|
var currentKey = key
|
||||||
|
while (currentKey.size < 48) {
|
||||||
|
key = md5(key + secret + salt)
|
||||||
|
currentKey += key
|
||||||
|
}
|
||||||
|
return currentKey
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun md5(input: ByteArray): ByteArray {
|
||||||
|
return MessageDigest.getInstance("MD5").digest(input)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun decryptSourceUrl(decryptionKey: ByteArray, sourceUrl: String): String {
|
||||||
|
val cipherData = base64DecodeArray(sourceUrl)
|
||||||
|
val encrypted = cipherData.copyOfRange(16, cipherData.size)
|
||||||
|
val aesCBC = Cipher.getInstance("AES/CBC/PKCS5Padding")
|
||||||
|
aesCBC.init(
|
||||||
|
Cipher.DECRYPT_MODE,
|
||||||
|
SecretKeySpec(decryptionKey.copyOfRange(0, 32), "AES"),
|
||||||
|
IvParameterSpec(decryptionKey.copyOfRange(32, decryptionKey.size))
|
||||||
|
)
|
||||||
|
val decryptedData = aesCBC?.doFinal(encrypted) ?: throw ErrorLoadingException("Cipher not found")
|
||||||
|
return String(decryptedData, StandardCharsets.UTF_8)
|
||||||
|
}
|
||||||
|
|
||||||
|
data class Tracks(
|
||||||
|
@JsonProperty("file") val file: String? = null,
|
||||||
|
@JsonProperty("label") val label: String? = null,
|
||||||
|
@JsonProperty("kind") val kind: String? = null,
|
||||||
|
)
|
||||||
|
|
||||||
|
data class Sources(
|
||||||
|
@JsonProperty("file") val file: String? = null,
|
||||||
|
@JsonProperty("type") val type: String? = null,
|
||||||
|
@JsonProperty("label") val label: String? = null,
|
||||||
|
)
|
||||||
|
|
||||||
|
data class SourcesResponses(
|
||||||
|
@JsonProperty("sources") val sources: List<Sources?>? = emptyList(),
|
||||||
|
@JsonProperty("tracks") val tracks: List<Tracks?>? = emptyList(),
|
||||||
|
)
|
||||||
|
|
||||||
|
data class SourcesEncrypted(
|
||||||
|
@JsonProperty("sources") val sources: String? = null,
|
||||||
|
@JsonProperty("encrypted") val encrypted: Boolean? = null,
|
||||||
|
@JsonProperty("tracks") val tracks: List<Tracks?>? = emptyList(),
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,50 @@
|
||||||
|
// ! Bu araç @keyiflerolsun tarafından | @KekikAkademi için yazılmıştır.
|
||||||
|
|
||||||
|
package com.lagradost.cloudstream3.extractors
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
import com.lagradost.cloudstream3.*
|
||||||
|
import com.lagradost.cloudstream3.utils.*
|
||||||
|
|
||||||
|
open class RapidVid : ExtractorApi() {
|
||||||
|
override val name = "RapidVid"
|
||||||
|
override val mainUrl = "https://rapidvid.net"
|
||||||
|
override val requiresReferer = true
|
||||||
|
|
||||||
|
override suspend fun getUrl(url: String, referer: String?, subtitleCallback: (SubtitleFile) -> Unit, callback: (ExtractorLink) -> Unit) {
|
||||||
|
val ext_ref = referer ?: ""
|
||||||
|
val video_req = app.get(url, referer=ext_ref).text
|
||||||
|
|
||||||
|
val sub_urls = mutableSetOf<String>()
|
||||||
|
Regex("""captions\",\"file\":\"([^\"]+)\",\"label\":\"([^\"]+)\"""").findAll(video_req).forEach {
|
||||||
|
val (sub_url, sub_lang) = it.destructured
|
||||||
|
|
||||||
|
if (sub_url in sub_urls) { return@forEach }
|
||||||
|
sub_urls.add(sub_url)
|
||||||
|
|
||||||
|
subtitleCallback.invoke(
|
||||||
|
SubtitleFile(
|
||||||
|
lang = sub_lang.replace("\\u0131", "ı").replace("\\u0130", "İ").replace("\\u00fc", "ü").replace("\\u00e7", "ç"),
|
||||||
|
url = fixUrl(sub_url.replace("\\", ""))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val extracted_value = Regex("""file": "(.*)",""").find(video_req)?.groupValues?.get(1) ?: throw ErrorLoadingException("File not found")
|
||||||
|
|
||||||
|
val bytes = extracted_value.split("\\x").filter { it.isNotEmpty() }.map { it.toInt(16).toByte() }.toByteArray()
|
||||||
|
val decoded = String(bytes, Charsets.UTF_8)
|
||||||
|
Log.d("Kekik_${this.name}", "decoded » ${decoded}")
|
||||||
|
|
||||||
|
callback.invoke(
|
||||||
|
ExtractorLink(
|
||||||
|
source = this.name,
|
||||||
|
name = this.name,
|
||||||
|
url = decoded,
|
||||||
|
referer = ext_ref,
|
||||||
|
quality = Qualities.Unknown.value,
|
||||||
|
isM3u8 = true
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,33 @@
|
||||||
|
// ! Bu araç @keyiflerolsun tarafından | @KekikAkademi için yazılmıştır.
|
||||||
|
|
||||||
|
package com.lagradost.cloudstream3.extractors
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
import com.lagradost.cloudstream3.*
|
||||||
|
import com.lagradost.cloudstream3.utils.*
|
||||||
|
|
||||||
|
open class SibNet : ExtractorApi() {
|
||||||
|
override val name = "SibNet"
|
||||||
|
override val mainUrl = "https://video.sibnet.ru"
|
||||||
|
override val requiresReferer = true
|
||||||
|
|
||||||
|
override suspend fun getUrl(url: String, referer: String?, subtitleCallback: (SubtitleFile) -> Unit, callback: (ExtractorLink) -> Unit) {
|
||||||
|
val ext_ref = referer ?: ""
|
||||||
|
val i_source = app.get(url, referer=ext_ref).text
|
||||||
|
var m3u_link = Regex("""player.src\(\[\{src: \"([^\"]+)""").find(i_source)?.groupValues?.get(1) ?: throw ErrorLoadingException("m3u link not found")
|
||||||
|
|
||||||
|
m3u_link = "${mainUrl}${m3u_link}"
|
||||||
|
Log.d("Kekik_${this.name}", "m3u_link » ${m3u_link}")
|
||||||
|
|
||||||
|
callback.invoke(
|
||||||
|
ExtractorLink(
|
||||||
|
source = this.name,
|
||||||
|
name = this.name,
|
||||||
|
url = m3u_link,
|
||||||
|
referer = url,
|
||||||
|
quality = Qualities.Unknown.value,
|
||||||
|
type = INFER_TYPE
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -8,6 +8,31 @@ import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||||
import com.lagradost.cloudstream3.utils.M3u8Helper
|
import com.lagradost.cloudstream3.utils.M3u8Helper
|
||||||
import kotlin.random.Random
|
import kotlin.random.Random
|
||||||
|
|
||||||
|
class Sblona : StreamSB() {
|
||||||
|
override var name = "Sblona"
|
||||||
|
override var mainUrl = "https://sblona.com"
|
||||||
|
}
|
||||||
|
|
||||||
|
class Lvturbo : StreamSB() {
|
||||||
|
override var name = "Lvturbo"
|
||||||
|
override var mainUrl = "https://lvturbo.com"
|
||||||
|
}
|
||||||
|
|
||||||
|
class Sbrapid : StreamSB() {
|
||||||
|
override var name = "Sbrapid"
|
||||||
|
override var mainUrl = "https://sbrapid.com"
|
||||||
|
}
|
||||||
|
|
||||||
|
class Sbface : StreamSB() {
|
||||||
|
override var name = "Sbface"
|
||||||
|
override var mainUrl = "https://sbface.com"
|
||||||
|
}
|
||||||
|
|
||||||
|
class Sbsonic : StreamSB() {
|
||||||
|
override var name = "Sbsonic"
|
||||||
|
override var mainUrl = "https://sbsonic.com"
|
||||||
|
}
|
||||||
|
|
||||||
class Vidgomunimesb : StreamSB() {
|
class Vidgomunimesb : StreamSB() {
|
||||||
override var mainUrl = "https://vidgomunimesb.xyz"
|
override var mainUrl = "https://vidgomunimesb.xyz"
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,6 +9,10 @@ class StreamTapeNet : StreamTape() {
|
||||||
override var mainUrl = "https://streamtape.net"
|
override var mainUrl = "https://streamtape.net"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class StreamTapeXyz : StreamTape() {
|
||||||
|
override var mainUrl = "https://streamtape.xyz"
|
||||||
|
}
|
||||||
|
|
||||||
class ShaveTape : StreamTape(){
|
class ShaveTape : StreamTape(){
|
||||||
override var mainUrl = "https://shavetape.cash"
|
override var mainUrl = "https://shavetape.cash"
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,34 @@
|
||||||
|
package com.lagradost.cloudstream3.extractors
|
||||||
|
|
||||||
|
import com.lagradost.cloudstream3.app
|
||||||
|
import com.lagradost.cloudstream3.network.WebViewResolver
|
||||||
|
import com.lagradost.cloudstream3.utils.ExtractorApi
|
||||||
|
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||||
|
import com.lagradost.cloudstream3.utils.Qualities
|
||||||
|
|
||||||
|
open class StreamWishExtractor : ExtractorApi() {
|
||||||
|
override var name = "StreamWish"
|
||||||
|
override var mainUrl = "https://streamwish.to"
|
||||||
|
override val requiresReferer = false
|
||||||
|
|
||||||
|
override suspend fun getUrl(url: String, referer: String?): List<ExtractorLink>? {
|
||||||
|
val response = app.get(
|
||||||
|
url, referer = referer ?: "$mainUrl/", interceptor = WebViewResolver(
|
||||||
|
Regex("""master\.m3u8""")
|
||||||
|
)
|
||||||
|
)
|
||||||
|
val sources = mutableListOf<ExtractorLink>()
|
||||||
|
if (response.url.contains("m3u8"))
|
||||||
|
sources.add(
|
||||||
|
ExtractorLink(
|
||||||
|
source = name,
|
||||||
|
name = name,
|
||||||
|
url = response.url,
|
||||||
|
referer = referer ?: "$mainUrl/",
|
||||||
|
quality = Qualities.Unknown.value,
|
||||||
|
isM3u8 = true
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return sources
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,42 @@
|
||||||
|
package com.lagradost.cloudstream3.extractors
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty
|
||||||
|
import com.lagradost.cloudstream3.app
|
||||||
|
import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson
|
||||||
|
import com.lagradost.cloudstream3.utils.ExtractorApi
|
||||||
|
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||||
|
import com.lagradost.cloudstream3.utils.getAndUnpack
|
||||||
|
import com.lagradost.cloudstream3.utils.Qualities
|
||||||
|
import com.lagradost.cloudstream3.utils.M3u8Helper
|
||||||
|
|
||||||
|
open class StreamoUpload : ExtractorApi() {
|
||||||
|
override val name = "StreamoUpload"
|
||||||
|
override val mainUrl = "https://streamoupload.xyz"
|
||||||
|
override val requiresReferer = true
|
||||||
|
|
||||||
|
override suspend fun getUrl(url: String, referer: String?): List<ExtractorLink> {
|
||||||
|
val sources = mutableListOf<ExtractorLink>()
|
||||||
|
val response = app.get(url, referer = referer)
|
||||||
|
val scriptElements = response.document.select("script").map { script ->
|
||||||
|
if (script.data().contains("eval(function(p,a,c,k,e,d)")) {
|
||||||
|
val data = getAndUnpack(script.data())
|
||||||
|
.substringAfter("sources:[")
|
||||||
|
.substringBefore("],")
|
||||||
|
.replace("file", "\"file\"")
|
||||||
|
.trim()
|
||||||
|
tryParseJson<File>(data)?.let {
|
||||||
|
M3u8Helper.generateM3u8(
|
||||||
|
name,
|
||||||
|
it.file,
|
||||||
|
"$mainUrl/",
|
||||||
|
).forEach { m3uData -> sources.add(m3uData) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return sources
|
||||||
|
}
|
||||||
|
|
||||||
|
private data class File(
|
||||||
|
@JsonProperty("file") val file: String,
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,73 @@
|
||||||
|
// ! Bu araç @keyiflerolsun tarafından | @KekikAkademi için yazılmıştır.
|
||||||
|
|
||||||
|
package com.lagradost.cloudstream3.extractors
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
import com.lagradost.cloudstream3.*
|
||||||
|
import com.lagradost.cloudstream3.utils.*
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty
|
||||||
|
|
||||||
|
open class TRsTX : ExtractorApi() {
|
||||||
|
override val name = "TRsTX"
|
||||||
|
override val mainUrl = "https://trstx.org"
|
||||||
|
override val requiresReferer = true
|
||||||
|
|
||||||
|
override suspend fun getUrl(url: String, referer: String?, subtitleCallback: (SubtitleFile) -> Unit, callback: (ExtractorLink) -> Unit) {
|
||||||
|
val ext_ref = referer ?: ""
|
||||||
|
|
||||||
|
val video_req = app.get(url, referer=ext_ref).text
|
||||||
|
|
||||||
|
val file = Regex("""file\":\"([^\"]+)""").find(video_req)?.groupValues?.get(1) ?: throw ErrorLoadingException("File not found")
|
||||||
|
val postLink = "${mainUrl}/" + file.replace("\\", "")
|
||||||
|
val rawList = app.post(postLink, referer=ext_ref).parsedSafe<List<Any>>() ?: throw ErrorLoadingException("Post link not found")
|
||||||
|
|
||||||
|
val postJson: List<TrstxVideoData> = rawList.drop(1).map { item ->
|
||||||
|
val mapItem = item as Map<*, *>
|
||||||
|
TrstxVideoData(
|
||||||
|
title = mapItem["title"] as? String,
|
||||||
|
file = mapItem["file"] as? String
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Log.d("Kekik_${this.name}", "postJson » ${postJson}")
|
||||||
|
|
||||||
|
val vid_links = mutableSetOf<String>()
|
||||||
|
val vid_map = mutableListOf<Map<String, String>>()
|
||||||
|
for (item in postJson) {
|
||||||
|
if (item.file == null || item.title == null) continue
|
||||||
|
|
||||||
|
val fileUrl = "${mainUrl}/playlist/" + item.file.substring(1) + ".txt"
|
||||||
|
val videoData = app.post(fileUrl, referer=ext_ref).text
|
||||||
|
|
||||||
|
if (videoData in vid_links) { continue }
|
||||||
|
vid_links.add(videoData)
|
||||||
|
|
||||||
|
vid_map.add(mapOf(
|
||||||
|
"title" to item.title,
|
||||||
|
"videoData" to videoData
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
for (mapEntry in vid_map) {
|
||||||
|
Log.d("Kekik_${this.name}", "mapEntry » ${mapEntry}")
|
||||||
|
val title = mapEntry["title"] ?: continue
|
||||||
|
val m3u_link = mapEntry["videoData"] ?: continue
|
||||||
|
|
||||||
|
callback.invoke(
|
||||||
|
ExtractorLink(
|
||||||
|
source = this.name,
|
||||||
|
name = "${this.name} - ${title}",
|
||||||
|
url = m3u_link,
|
||||||
|
referer = ext_ref,
|
||||||
|
quality = Qualities.Unknown.value,
|
||||||
|
type = INFER_TYPE
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data class TrstxVideoData(
|
||||||
|
@JsonProperty("title") val title: String? = null,
|
||||||
|
@JsonProperty("file") val file: String? = null
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,45 @@
|
||||||
|
// ! Bu araç @keyiflerolsun tarafından | @KekikAkademi için yazılmıştır.
|
||||||
|
|
||||||
|
package com.lagradost.cloudstream3.extractors
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
import com.lagradost.cloudstream3.*
|
||||||
|
import com.lagradost.cloudstream3.utils.*
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty
|
||||||
|
|
||||||
|
open class TauVideo : ExtractorApi() {
|
||||||
|
override val name = "TauVideo"
|
||||||
|
override val mainUrl = "https://tau-video.xyz"
|
||||||
|
override val requiresReferer = true
|
||||||
|
|
||||||
|
override suspend fun getUrl(url: String, referer: String?, subtitleCallback: (SubtitleFile) -> Unit, callback: (ExtractorLink) -> Unit) {
|
||||||
|
val ext_ref = referer ?: ""
|
||||||
|
val video_key = url.split("/").last()
|
||||||
|
val video_url = "${mainUrl}/api/video/${video_key}"
|
||||||
|
Log.d("Kekik_${this.name}", "video_url » ${video_url}")
|
||||||
|
|
||||||
|
val api = app.get(video_url).parsedSafe<TauVideoUrls>() ?: throw ErrorLoadingException("TauVideo")
|
||||||
|
|
||||||
|
for (video in api.urls) {
|
||||||
|
callback.invoke(
|
||||||
|
ExtractorLink(
|
||||||
|
source = this.name,
|
||||||
|
name = this.name,
|
||||||
|
url = video.url,
|
||||||
|
referer = ext_ref,
|
||||||
|
quality = getQualityFromName(video.label),
|
||||||
|
type = INFER_TYPE
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data class TauVideoUrls(
|
||||||
|
@JsonProperty("urls") val urls: List<TauVideoData>
|
||||||
|
)
|
||||||
|
|
||||||
|
data class TauVideoData(
|
||||||
|
@JsonProperty("url") val url: String,
|
||||||
|
@JsonProperty("label") val label: String,
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,45 @@
|
||||||
|
package com.lagradost.cloudstream3.extractors
|
||||||
|
|
||||||
|
import com.lagradost.cloudstream3.SubtitleFile
|
||||||
|
import com.lagradost.cloudstream3.app
|
||||||
|
import com.lagradost.cloudstream3.utils.ExtractorApi
|
||||||
|
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||||
|
import com.lagradost.cloudstream3.utils.Qualities
|
||||||
|
|
||||||
|
open class Userscloud : ExtractorApi() {
|
||||||
|
override val name = "Userscloud"
|
||||||
|
override val mainUrl = "https://userscloud.com"
|
||||||
|
override val requiresReferer = false
|
||||||
|
|
||||||
|
override suspend fun getUrl(
|
||||||
|
url: String,
|
||||||
|
referer: String?,
|
||||||
|
subtitleCallback: (SubtitleFile) -> Unit,
|
||||||
|
callback: (ExtractorLink) -> Unit
|
||||||
|
) {
|
||||||
|
val res = app.get(url).document
|
||||||
|
val video = res.selectFirst("video#vjsplayer source")?.attr("src")
|
||||||
|
val quality = res.selectFirst("div.innerTB h2 b")?.text()
|
||||||
|
callback.invoke(
|
||||||
|
ExtractorLink(
|
||||||
|
this.name,
|
||||||
|
this.name,
|
||||||
|
video ?: return,
|
||||||
|
"$mainUrl/",
|
||||||
|
getQuality(quality),
|
||||||
|
headers = mapOf(
|
||||||
|
"Accept" to "video/webm,video/ogg,video/*;q=0.9,application/ogg;q=0.7,audio/*;q=0.6,*/*;q=0.5",
|
||||||
|
"Range" to "bytes=0-",
|
||||||
|
"Sec-Fetch-Dest" to "video",
|
||||||
|
"Sec-Fetch-Mode" to "no-cors",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getQuality(str: String?): Int {
|
||||||
|
return Regex("(\\d{3,4})[pP]").find(str ?: "")?.groupValues?.getOrNull(1)?.toIntOrNull()
|
||||||
|
?: Qualities.Unknown.value
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,50 @@
|
||||||
|
// ! Bu araç @keyiflerolsun tarafından | @KekikAkademi için yazılmıştır.
|
||||||
|
|
||||||
|
package com.lagradost.cloudstream3.extractors
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
import com.lagradost.cloudstream3.*
|
||||||
|
import com.lagradost.cloudstream3.utils.*
|
||||||
|
|
||||||
|
open class VidMoxy : ExtractorApi() {
|
||||||
|
override val name = "VidMoxy"
|
||||||
|
override val mainUrl = "https://vidmoxy.com"
|
||||||
|
override val requiresReferer = true
|
||||||
|
|
||||||
|
override suspend fun getUrl(url: String, referer: String?, subtitleCallback: (SubtitleFile) -> Unit, callback: (ExtractorLink) -> Unit) {
|
||||||
|
val ext_ref = referer ?: ""
|
||||||
|
val video_req = app.get(url, referer=ext_ref).text
|
||||||
|
|
||||||
|
val sub_urls = mutableSetOf<String>()
|
||||||
|
Regex("""captions\",\"file\":\"([^\"]+)\",\"label\":\"([^\"]+)\"""").findAll(video_req).forEach {
|
||||||
|
val (sub_url, sub_lang) = it.destructured
|
||||||
|
|
||||||
|
if (sub_url in sub_urls) { return@forEach }
|
||||||
|
sub_urls.add(sub_url)
|
||||||
|
|
||||||
|
subtitleCallback.invoke(
|
||||||
|
SubtitleFile(
|
||||||
|
lang = sub_lang.replace("\\u0131", "ı").replace("\\u0130", "İ").replace("\\u00fc", "ü").replace("\\u00e7", "ç"),
|
||||||
|
url = fixUrl(sub_url.replace("\\", ""))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val extracted_value = Regex("""file": "(.*)",""").find(video_req)?.groupValues?.get(1) ?: throw ErrorLoadingException("File not found")
|
||||||
|
|
||||||
|
val bytes = extracted_value.split("\\x").filter { it.isNotEmpty() }.map { it.toInt(16).toByte() }.toByteArray()
|
||||||
|
val decoded = String(bytes, Charsets.UTF_8)
|
||||||
|
Log.d("Kekik_${this.name}", "decoded » ${decoded}")
|
||||||
|
|
||||||
|
callback.invoke(
|
||||||
|
ExtractorLink(
|
||||||
|
source = this.name,
|
||||||
|
name = this.name,
|
||||||
|
url = decoded,
|
||||||
|
referer = ext_ref,
|
||||||
|
quality = Qualities.Unknown.value,
|
||||||
|
isM3u8 = true
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,72 @@
|
||||||
|
// ! Bu araç @keyiflerolsun tarafından | @KekikAkademi için yazılmıştır.
|
||||||
|
|
||||||
|
package com.lagradost.cloudstream3.extractors
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
import com.lagradost.cloudstream3.*
|
||||||
|
import com.lagradost.cloudstream3.utils.*
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty
|
||||||
|
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
||||||
|
import com.fasterxml.jackson.module.kotlin.readValue
|
||||||
|
|
||||||
|
open class VideoSeyred : ExtractorApi() {
|
||||||
|
override val name = "VideoSeyred"
|
||||||
|
override val mainUrl = "https://videoseyred.in"
|
||||||
|
override val requiresReferer = true
|
||||||
|
|
||||||
|
override suspend fun getUrl(url: String, referer: String?, subtitleCallback: (SubtitleFile) -> Unit, callback: (ExtractorLink) -> Unit) {
|
||||||
|
val ext_ref = referer ?: ""
|
||||||
|
val video_id = url.substringAfter("embed/").substringBefore("?")
|
||||||
|
val video_url = "${mainUrl}/playlist/${video_id}.json"
|
||||||
|
Log.d("Kekik_${this.name}", "video_url » ${video_url}")
|
||||||
|
|
||||||
|
val response_raw = app.get(video_url)
|
||||||
|
val response_list:List<VideoSeyredSource> = jacksonObjectMapper().readValue(response_raw.text) ?: throw ErrorLoadingException("VideoSeyred")
|
||||||
|
val response = response_list[0] ?: throw ErrorLoadingException("VideoSeyred")
|
||||||
|
|
||||||
|
for (track in response.tracks) {
|
||||||
|
if (track.label != null && track.kind == "captions") {
|
||||||
|
subtitleCallback.invoke(
|
||||||
|
SubtitleFile(
|
||||||
|
lang = track.label,
|
||||||
|
url = fixUrl(track.file)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (source in response.sources) {
|
||||||
|
callback.invoke(
|
||||||
|
ExtractorLink(
|
||||||
|
source = this.name,
|
||||||
|
name = this.name,
|
||||||
|
url = source.file,
|
||||||
|
referer = ext_ref,
|
||||||
|
quality = Qualities.Unknown.value,
|
||||||
|
type = INFER_TYPE
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data class VideoSeyredSource(
|
||||||
|
@JsonProperty("image") val image: String,
|
||||||
|
@JsonProperty("title") val title: String,
|
||||||
|
@JsonProperty("sources") val sources: List<VSSource>,
|
||||||
|
@JsonProperty("tracks") val tracks: List<VSTrack>
|
||||||
|
)
|
||||||
|
|
||||||
|
data class VSSource(
|
||||||
|
@JsonProperty("file") val file: String,
|
||||||
|
@JsonProperty("type") val type: String,
|
||||||
|
@JsonProperty("default") val default: String
|
||||||
|
)
|
||||||
|
|
||||||
|
data class VSTrack(
|
||||||
|
@JsonProperty("file") val file: String,
|
||||||
|
@JsonProperty("kind") val kind: String,
|
||||||
|
@JsonProperty("language") val language: String? = null,
|
||||||
|
@JsonProperty("label") val label: String? = null,
|
||||||
|
@JsonProperty("default") val default: String? = null
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,34 @@
|
||||||
|
package com.lagradost.cloudstream3.extractors
|
||||||
|
|
||||||
|
import com.lagradost.cloudstream3.app
|
||||||
|
import com.lagradost.cloudstream3.network.WebViewResolver
|
||||||
|
import com.lagradost.cloudstream3.utils.ExtractorApi
|
||||||
|
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||||
|
import com.lagradost.cloudstream3.utils.Qualities
|
||||||
|
|
||||||
|
open class VidhideExtractor : ExtractorApi() {
|
||||||
|
override var name = "VidHide"
|
||||||
|
override var mainUrl = "https://vidhide.com"
|
||||||
|
override val requiresReferer = false
|
||||||
|
|
||||||
|
override suspend fun getUrl(url: String, referer: String?): List<ExtractorLink>? {
|
||||||
|
val response = app.get(
|
||||||
|
url, referer = referer ?: "$mainUrl/", interceptor = WebViewResolver(
|
||||||
|
Regex("""master\.m3u8""")
|
||||||
|
)
|
||||||
|
)
|
||||||
|
val sources = mutableListOf<ExtractorLink>()
|
||||||
|
if (response.url.contains("m3u8"))
|
||||||
|
sources.add(
|
||||||
|
ExtractorLink(
|
||||||
|
source = name,
|
||||||
|
name = name,
|
||||||
|
url = response.url,
|
||||||
|
referer = referer ?: "$mainUrl/",
|
||||||
|
quality = Qualities.Unknown.value,
|
||||||
|
isM3u8 = true
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return sources
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,116 @@
|
||||||
|
package com.lagradost.cloudstream3.extractors
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty
|
||||||
|
import com.lagradost.cloudstream3.SubtitleFile
|
||||||
|
import com.lagradost.cloudstream3.app
|
||||||
|
import com.lagradost.cloudstream3.base64Encode
|
||||||
|
import com.lagradost.cloudstream3.utils.ExtractorApi
|
||||||
|
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||||
|
import com.lagradost.cloudstream3.utils.M3u8Helper
|
||||||
|
import javax.crypto.Cipher
|
||||||
|
import javax.crypto.spec.SecretKeySpec
|
||||||
|
|
||||||
|
// Code found in https://github.com/KillerDogeEmpire/vidplay-keys
|
||||||
|
// special credits to @KillerDogeEmpire for providing key
|
||||||
|
|
||||||
|
class MyCloud : Vidplay() {
|
||||||
|
override val name = "MyCloud"
|
||||||
|
override val mainUrl = "https://mcloud.bz"
|
||||||
|
}
|
||||||
|
|
||||||
|
class VidplayOnline : Vidplay() {
|
||||||
|
override val mainUrl = "https://vidplay.online"
|
||||||
|
}
|
||||||
|
|
||||||
|
open class Vidplay : ExtractorApi() {
|
||||||
|
override val name = "Vidplay"
|
||||||
|
override val mainUrl = "https://vidplay.site"
|
||||||
|
override val requiresReferer = true
|
||||||
|
open val key =
|
||||||
|
"https://raw.githubusercontent.com/KillerDogeEmpire/vidplay-keys/keys/keys.json"
|
||||||
|
|
||||||
|
override suspend fun getUrl(
|
||||||
|
url: String,
|
||||||
|
referer: String?,
|
||||||
|
subtitleCallback: (SubtitleFile) -> Unit,
|
||||||
|
callback: (ExtractorLink) -> Unit
|
||||||
|
) {
|
||||||
|
val id = url.substringBefore("?").substringAfterLast("/")
|
||||||
|
val encodeId = encodeId(id, getKeys())
|
||||||
|
val mediaUrl = callFutoken(encodeId, url)
|
||||||
|
val res = app.get(
|
||||||
|
"$mediaUrl", headers = mapOf(
|
||||||
|
"Accept" to "application/json, text/javascript, */*; q=0.01",
|
||||||
|
"X-Requested-With" to "XMLHttpRequest",
|
||||||
|
), referer = url
|
||||||
|
).parsedSafe<Response>()?.result
|
||||||
|
|
||||||
|
res?.sources?.map {
|
||||||
|
M3u8Helper.generateM3u8(
|
||||||
|
this.name,
|
||||||
|
it.file ?: return@map,
|
||||||
|
"$mainUrl/"
|
||||||
|
).forEach(callback)
|
||||||
|
}
|
||||||
|
|
||||||
|
res?.tracks?.filter { it.kind == "captions" }?.map {
|
||||||
|
subtitleCallback.invoke(
|
||||||
|
SubtitleFile(it.label ?: return@map, it.file ?: return@map)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun getKeys(): List<String> {
|
||||||
|
return app.get(key).parsed()
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun callFutoken(id: String, url: String): String? {
|
||||||
|
val script = app.get("$mainUrl/futoken").text
|
||||||
|
val k = "k='(\\S+)'".toRegex().find(script)?.groupValues?.get(1) ?: return null
|
||||||
|
val a = mutableListOf(k)
|
||||||
|
for (i in id.indices) {
|
||||||
|
a.add((k[i % k.length].code + id[i].code).toString())
|
||||||
|
}
|
||||||
|
return "$mainUrl/mediainfo/${a.joinToString(",")}?${url.substringAfter("?")}"
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun encodeId(id: String, keyList: List<String>): String {
|
||||||
|
val cipher1 = Cipher.getInstance("RC4")
|
||||||
|
val cipher2 = Cipher.getInstance("RC4")
|
||||||
|
cipher1.init(
|
||||||
|
Cipher.DECRYPT_MODE,
|
||||||
|
SecretKeySpec(keyList[0].toByteArray(), "RC4"),
|
||||||
|
cipher1.parameters
|
||||||
|
)
|
||||||
|
cipher2.init(
|
||||||
|
Cipher.DECRYPT_MODE,
|
||||||
|
SecretKeySpec(keyList[1].toByteArray(), "RC4"),
|
||||||
|
cipher2.parameters
|
||||||
|
)
|
||||||
|
var input = id.toByteArray()
|
||||||
|
input = cipher1.doFinal(input)
|
||||||
|
input = cipher2.doFinal(input)
|
||||||
|
return base64Encode(input).replace("/", "_")
|
||||||
|
}
|
||||||
|
|
||||||
|
data class Tracks(
|
||||||
|
@JsonProperty("file") val file: String? = null,
|
||||||
|
@JsonProperty("label") val label: String? = null,
|
||||||
|
@JsonProperty("kind") val kind: String? = null,
|
||||||
|
)
|
||||||
|
|
||||||
|
data class Sources(
|
||||||
|
@JsonProperty("file") val file: String? = null,
|
||||||
|
)
|
||||||
|
|
||||||
|
data class Result(
|
||||||
|
@JsonProperty("sources") val sources: ArrayList<Sources>? = arrayListOf(),
|
||||||
|
@JsonProperty("tracks") val tracks: ArrayList<Tracks>? = arrayListOf(),
|
||||||
|
)
|
||||||
|
|
||||||
|
data class Response(
|
||||||
|
@JsonProperty("result") val result: Result? = null,
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
|
@ -5,6 +5,7 @@ import com.lagradost.cloudstream3.amap
|
||||||
import com.lagradost.cloudstream3.app
|
import com.lagradost.cloudstream3.app
|
||||||
import com.lagradost.cloudstream3.argamap
|
import com.lagradost.cloudstream3.argamap
|
||||||
import com.lagradost.cloudstream3.utils.ExtractorLink
|
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||||
|
import com.lagradost.cloudstream3.utils.INFER_TYPE
|
||||||
import com.lagradost.cloudstream3.utils.extractorApis
|
import com.lagradost.cloudstream3.utils.extractorApis
|
||||||
import com.lagradost.cloudstream3.utils.getQualityFromName
|
import com.lagradost.cloudstream3.utils.getQualityFromName
|
||||||
import com.lagradost.cloudstream3.utils.loadExtractor
|
import com.lagradost.cloudstream3.utils.loadExtractor
|
||||||
|
@ -70,7 +71,7 @@ class Vidstream(val mainUrl: String) {
|
||||||
href,
|
href,
|
||||||
page.url,
|
page.url,
|
||||||
getQualityFromName(qual),
|
getQualityFromName(qual),
|
||||||
element.attr("href").contains(".m3u8")
|
type = INFER_TYPE
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,6 +7,7 @@ import com.lagradost.cloudstream3.extractors.helper.NineAnimeHelper.encrypt
|
||||||
import com.lagradost.cloudstream3.app
|
import com.lagradost.cloudstream3.app
|
||||||
import com.lagradost.cloudstream3.utils.ExtractorApi
|
import com.lagradost.cloudstream3.utils.ExtractorApi
|
||||||
import com.lagradost.cloudstream3.utils.ExtractorLink
|
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||||
|
import com.lagradost.cloudstream3.utils.INFER_TYPE
|
||||||
import com.lagradost.cloudstream3.utils.Qualities
|
import com.lagradost.cloudstream3.utils.Qualities
|
||||||
|
|
||||||
class Vidstreamz : WcoStream() {
|
class Vidstreamz : WcoStream() {
|
||||||
|
@ -126,8 +127,7 @@ open class WcoStream : ExtractorApi() {
|
||||||
|
|
||||||
if (!response.text.startsWith("{")) throw ErrorLoadingException("Seems like 9Anime kiddies changed stuff again, Go touch some grass for bout an hour Or use a different Server")
|
if (!response.text.startsWith("{")) throw ErrorLoadingException("Seems like 9Anime kiddies changed stuff again, Go touch some grass for bout an hour Or use a different Server")
|
||||||
return response.parsed<Response>().data.media.sources.map {
|
return response.parsed<Response>().data.media.sources.map {
|
||||||
ExtractorLink(name, it.file,it.file,host,Qualities.Unknown.value,it.file.contains(".m3u8"))
|
ExtractorLink(name, it.file, it.file, host, Qualities.Unknown.value, type = INFER_TYPE)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,35 @@
|
||||||
|
package com.lagradost.cloudstream3.extractors
|
||||||
|
|
||||||
|
import com.lagradost.cloudstream3.SubtitleFile
|
||||||
|
import com.lagradost.cloudstream3.app
|
||||||
|
import com.lagradost.cloudstream3.utils.ExtractorApi
|
||||||
|
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||||
|
import com.lagradost.cloudstream3.utils.INFER_TYPE
|
||||||
|
import com.lagradost.cloudstream3.utils.Qualities
|
||||||
|
|
||||||
|
open class Wibufile : ExtractorApi() {
|
||||||
|
override val name: String = "Wibufile"
|
||||||
|
override val mainUrl: String = "https://wibufile.com"
|
||||||
|
override val requiresReferer = false
|
||||||
|
|
||||||
|
override suspend fun getUrl(
|
||||||
|
url: String,
|
||||||
|
referer: String?,
|
||||||
|
subtitleCallback: (SubtitleFile) -> Unit,
|
||||||
|
callback: (ExtractorLink) -> Unit
|
||||||
|
) {
|
||||||
|
val res = app.get(url).text
|
||||||
|
val video = Regex("src: ['\"](.*?)['\"]").find(res)?.groupValues?.get(1)
|
||||||
|
|
||||||
|
callback.invoke(
|
||||||
|
ExtractorLink(
|
||||||
|
name,
|
||||||
|
name,
|
||||||
|
video ?: return,
|
||||||
|
"$mainUrl/",
|
||||||
|
Qualities.Unknown.value,
|
||||||
|
type = INFER_TYPE
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -70,19 +70,18 @@ open class YoutubeExtractor : ExtractorApi() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ytVideos[url]?.mapNotNull {
|
ytVideos[url]?.mapNotNull {
|
||||||
if (it.isVideoOnly || it.height <= 0) return@mapNotNull null
|
if (it.isVideoOnly() || it.height <= 0) return@mapNotNull null
|
||||||
|
|
||||||
ExtractorLink(
|
ExtractorLink(
|
||||||
this.name,
|
this.name,
|
||||||
this.name,
|
this.name,
|
||||||
it.url ?: return@mapNotNull null,
|
it.content ?: return@mapNotNull null,
|
||||||
"",
|
"",
|
||||||
it.height
|
it.height
|
||||||
)
|
)
|
||||||
}?.forEach(callback)
|
}?.forEach(callback)
|
||||||
ytVideosSubtitles[url]?.mapNotNull {
|
ytVideosSubtitles[url]?.mapNotNull {
|
||||||
SubtitleFile(it.languageTag ?: return@mapNotNull null, it.url ?: return@mapNotNull null)
|
SubtitleFile(it.languageTag ?: return@mapNotNull null, it.content ?: return@mapNotNull null)
|
||||||
}?.forEach(subtitleCallback)
|
}?.forEach(subtitleCallback)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
|
@ -0,0 +1,100 @@
|
||||||
|
package com.lagradost.cloudstream3.extractors.helper
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty
|
||||||
|
import com.lagradost.cloudstream3.base64DecodeArray
|
||||||
|
import com.lagradost.cloudstream3.base64Encode
|
||||||
|
import com.lagradost.cloudstream3.utils.AppUtils
|
||||||
|
import java.security.DigestException
|
||||||
|
import java.security.MessageDigest
|
||||||
|
import javax.crypto.Cipher
|
||||||
|
import javax.crypto.spec.IvParameterSpec
|
||||||
|
import javax.crypto.spec.SecretKeySpec
|
||||||
|
|
||||||
|
object AesHelper {
|
||||||
|
|
||||||
|
private const val HASH = "AES/CBC/PKCS5PADDING"
|
||||||
|
private const val KDF = "MD5"
|
||||||
|
|
||||||
|
fun cryptoAESHandler(
|
||||||
|
data: String,
|
||||||
|
pass: ByteArray,
|
||||||
|
encrypt: Boolean = true,
|
||||||
|
padding: String = HASH,
|
||||||
|
): String? {
|
||||||
|
val parse = AppUtils.tryParseJson<AesData>(data) ?: return null
|
||||||
|
val (key, iv) = generateKeyAndIv(
|
||||||
|
pass,
|
||||||
|
parse.s.hexToByteArray(),
|
||||||
|
ivLength = parse.iv.length / 2,
|
||||||
|
saltLength = parse.s.length / 2
|
||||||
|
) ?: return null
|
||||||
|
val cipher = Cipher.getInstance(padding)
|
||||||
|
return if (!encrypt) {
|
||||||
|
cipher.init(Cipher.DECRYPT_MODE, SecretKeySpec(key, "AES"), IvParameterSpec(iv))
|
||||||
|
String(cipher.doFinal(base64DecodeArray(parse.ct)))
|
||||||
|
} else {
|
||||||
|
cipher.init(Cipher.ENCRYPT_MODE, SecretKeySpec(key, "AES"), IvParameterSpec(iv))
|
||||||
|
base64Encode(cipher.doFinal(parse.ct.toByteArray()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// https://stackoverflow.com/a/41434590/8166854
|
||||||
|
fun generateKeyAndIv(
|
||||||
|
password: ByteArray,
|
||||||
|
salt: ByteArray,
|
||||||
|
hashAlgorithm: String = KDF,
|
||||||
|
keyLength: Int = 32,
|
||||||
|
ivLength: Int,
|
||||||
|
saltLength: Int,
|
||||||
|
iterations: Int = 1
|
||||||
|
): Pair<ByteArray,ByteArray>? {
|
||||||
|
|
||||||
|
val md = MessageDigest.getInstance(hashAlgorithm)
|
||||||
|
val digestLength = md.digestLength
|
||||||
|
val targetKeySize = keyLength + ivLength
|
||||||
|
val requiredLength = (targetKeySize + digestLength - 1) / digestLength * digestLength
|
||||||
|
val generatedData = ByteArray(requiredLength)
|
||||||
|
var generatedLength = 0
|
||||||
|
|
||||||
|
try {
|
||||||
|
md.reset()
|
||||||
|
|
||||||
|
while (generatedLength < targetKeySize) {
|
||||||
|
if (generatedLength > 0)
|
||||||
|
md.update(
|
||||||
|
generatedData,
|
||||||
|
generatedLength - digestLength,
|
||||||
|
digestLength
|
||||||
|
)
|
||||||
|
|
||||||
|
md.update(password)
|
||||||
|
md.update(salt, 0, saltLength)
|
||||||
|
md.digest(generatedData, generatedLength, digestLength)
|
||||||
|
|
||||||
|
for (i in 1 until iterations) {
|
||||||
|
md.update(generatedData, generatedLength, digestLength)
|
||||||
|
md.digest(generatedData, generatedLength, digestLength)
|
||||||
|
}
|
||||||
|
|
||||||
|
generatedLength += digestLength
|
||||||
|
}
|
||||||
|
return generatedData.copyOfRange(0, keyLength) to generatedData.copyOfRange(keyLength, targetKeySize)
|
||||||
|
} catch (e: DigestException) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun String.hexToByteArray(): ByteArray {
|
||||||
|
check(length % 2 == 0) { "Must have an even length" }
|
||||||
|
return chunked(2)
|
||||||
|
.map { it.toInt(16).toByte() }
|
||||||
|
.toByteArray()
|
||||||
|
}
|
||||||
|
|
||||||
|
private data class AesData(
|
||||||
|
@JsonProperty("ct") val ct: String,
|
||||||
|
@JsonProperty("iv") val iv: String,
|
||||||
|
@JsonProperty("s") val s: String
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
|
@ -21,10 +21,11 @@ class CrossTmdbProvider : TmdbProvider() {
|
||||||
return Regex("""[^a-zA-Z0-9-]""").replace(name, "")
|
return Regex("""[^a-zA-Z0-9-]""").replace(name, "")
|
||||||
}
|
}
|
||||||
|
|
||||||
private val validApis by lazy {
|
private val validApis
|
||||||
apis.filter { it.lang == this.lang && it::class.java != this::class.java }
|
get() =
|
||||||
//.distinctBy { it.uniqueId }
|
synchronized(apis) { apis.filter { it.lang == this.lang && it::class.java != this::class.java } }
|
||||||
}
|
//.distinctBy { it.uniqueId }
|
||||||
|
|
||||||
|
|
||||||
data class CrossMetaData(
|
data class CrossMetaData(
|
||||||
@JsonProperty("isSuccess") val isSuccess: Boolean,
|
@JsonProperty("isSuccess") val isSuccess: Boolean,
|
||||||
|
@ -60,7 +61,8 @@ class CrossTmdbProvider : TmdbProvider() {
|
||||||
|
|
||||||
override suspend fun load(url: String): LoadResponse? {
|
override suspend fun load(url: String): LoadResponse? {
|
||||||
val base = super.load(url)?.apply {
|
val base = super.load(url)?.apply {
|
||||||
this.recommendations = this.recommendations?.filterIsInstance<MovieSearchResponse>() // TODO REMOVE
|
this.recommendations =
|
||||||
|
this.recommendations?.filterIsInstance<MovieSearchResponse>() // TODO REMOVE
|
||||||
val matchName = filterName(this.name)
|
val matchName = filterName(this.name)
|
||||||
when (this) {
|
when (this) {
|
||||||
is MovieLoadResponse -> {
|
is MovieLoadResponse -> {
|
||||||
|
@ -98,6 +100,7 @@ class CrossTmdbProvider : TmdbProvider() {
|
||||||
this.dataUrl =
|
this.dataUrl =
|
||||||
CrossMetaData(true, data.map { it.apiName to it.dataUrl }).toJson()
|
CrossMetaData(true, data.map { it.apiName to it.dataUrl }).toJson()
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
throw ErrorLoadingException("Nothing besides movies are implemented for this provider")
|
throw ErrorLoadingException("Nothing besides movies are implemented for this provider")
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,70 +0,0 @@
|
||||||
package com.lagradost.cloudstream3.metaproviders
|
|
||||||
|
|
||||||
import com.lagradost.cloudstream3.*
|
|
||||||
import com.lagradost.cloudstream3.LoadResponse.Companion.addAniListId
|
|
||||||
import com.lagradost.cloudstream3.LoadResponse.Companion.addTrailer
|
|
||||||
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.aniListApi
|
|
||||||
import com.lagradost.cloudstream3.syncproviders.SyncAPI
|
|
||||||
import com.lagradost.cloudstream3.syncproviders.providers.AniListApi
|
|
||||||
import com.lagradost.cloudstream3.syncproviders.providers.MALApi
|
|
||||||
import com.lagradost.cloudstream3.utils.SyncUtil
|
|
||||||
|
|
||||||
// wont be implemented
|
|
||||||
class MultiAnimeProvider : MainAPI() {
|
|
||||||
override var name = "MultiAnime"
|
|
||||||
override var lang = "en"
|
|
||||||
override val usesWebView = true
|
|
||||||
override val supportedTypes = setOf(TvType.Anime)
|
|
||||||
private val syncApi: SyncAPI = aniListApi
|
|
||||||
|
|
||||||
private val syncUtilType by lazy {
|
|
||||||
when (syncApi) {
|
|
||||||
is AniListApi -> "anilist"
|
|
||||||
is MALApi -> "myanimelist"
|
|
||||||
else -> throw ErrorLoadingException("Invalid Api")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private val validApis by lazy {
|
|
||||||
APIHolder.apis.filter {
|
|
||||||
it.lang == this.lang && it::class.java != this::class.java && it.supportedTypes.contains(
|
|
||||||
TvType.Anime
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun filterName(name: String): String {
|
|
||||||
return Regex("""[^a-zA-Z0-9-]""").replace(name, "")
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun search(query: String): List<SearchResponse>? {
|
|
||||||
return syncApi.search(query)?.map {
|
|
||||||
AnimeSearchResponse(it.name, it.url, this.name, TvType.Anime, it.posterUrl)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun load(url: String): LoadResponse? {
|
|
||||||
return syncApi.getResult(url)?.let { res ->
|
|
||||||
val data = SyncUtil.getUrlsFromId(res.id, syncUtilType).amap { url ->
|
|
||||||
validApis.firstOrNull { api -> url.startsWith(api.mainUrl) }?.load(url)
|
|
||||||
}.filterNotNull()
|
|
||||||
|
|
||||||
val type =
|
|
||||||
if (data.any { it.type == TvType.AnimeMovie }) TvType.AnimeMovie else TvType.Anime
|
|
||||||
|
|
||||||
newAnimeLoadResponse(
|
|
||||||
res.title ?: throw ErrorLoadingException("No Title found"),
|
|
||||||
url,
|
|
||||||
type
|
|
||||||
) {
|
|
||||||
posterUrl = res.posterUrl
|
|
||||||
plot = res.synopsis
|
|
||||||
tags = res.genres
|
|
||||||
rating = res.publicScore
|
|
||||||
addTrailer(res.trailers)
|
|
||||||
addAniListId(res.id.toIntOrNull())
|
|
||||||
recommendations = res.recommendations
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -151,6 +151,8 @@ open class TmdbProvider : MainAPI() {
|
||||||
recommendations = (this@toLoadResponse.recommendations
|
recommendations = (this@toLoadResponse.recommendations
|
||||||
?: this@toLoadResponse.similar)?.results?.map { it.toSearchResponse() }
|
?: this@toLoadResponse.similar)?.results?.map { it.toSearchResponse() }
|
||||||
addActors(credits?.cast?.toList().toActors())
|
addActors(credits?.cast?.toList().toActors())
|
||||||
|
|
||||||
|
contentRating = fetchContentRating(id, "US")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -193,6 +195,8 @@ open class TmdbProvider : MainAPI() {
|
||||||
recommendations = (this@toLoadResponse.recommendations
|
recommendations = (this@toLoadResponse.recommendations
|
||||||
?: this@toLoadResponse.similar)?.results?.map { it.toSearchResponse() }
|
?: this@toLoadResponse.similar)?.results?.map { it.toSearchResponse() }
|
||||||
addActors(credits?.cast?.toList().toActors())
|
addActors(credits?.cast?.toList().toActors())
|
||||||
|
|
||||||
|
contentRating = fetchContentRating(id, "US")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -264,6 +268,26 @@ open class TmdbProvider : MainAPI() {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
open suspend fun fetchContentRating(id: Int?, country: String): String? {
|
||||||
|
id ?: return null
|
||||||
|
|
||||||
|
val contentRatings = tmdb.tvService().content_ratings(id).awaitResponse().body()?.results
|
||||||
|
return if (!contentRatings.isNullOrEmpty()) {
|
||||||
|
contentRatings.firstOrNull { it: ContentRating ->
|
||||||
|
it.iso_3166_1 == country
|
||||||
|
}?.rating
|
||||||
|
} else {
|
||||||
|
val releaseDates = tmdb.moviesService().releaseDates(id).awaitResponse().body()?.results
|
||||||
|
val certification = releaseDates?.firstOrNull { it: ReleaseDatesResult ->
|
||||||
|
it.iso_3166_1 == country
|
||||||
|
}?.release_dates?.firstOrNull { it: ReleaseDate ->
|
||||||
|
!it.certification.isNullOrBlank()
|
||||||
|
}?.certification
|
||||||
|
|
||||||
|
certification
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Possible to add recommendations and such here.
|
// Possible to add recommendations and such here.
|
||||||
override suspend fun load(url: String): LoadResponse? {
|
override suspend fun load(url: String): LoadResponse? {
|
||||||
// https://www.themoviedb.org/movie/7445-brothers
|
// https://www.themoviedb.org/movie/7445-brothers
|
||||||
|
|
|
@ -57,32 +57,6 @@ fun <T> LifecycleOwner.observeNullable(liveData: LiveData<T>, action: (t: T) ->
|
||||||
liveData.observe(this) { action(it) }
|
liveData.observe(this) { action(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
inline fun <reified T : Any> some(value: T?): Some<T> {
|
|
||||||
return if (value == null) {
|
|
||||||
Some.None
|
|
||||||
} else {
|
|
||||||
Some.Success(value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
sealed class Some<out T> {
|
|
||||||
data class Success<out T>(val value: T) : Some<T>()
|
|
||||||
object None : Some<Nothing>()
|
|
||||||
|
|
||||||
override fun toString(): String {
|
|
||||||
return when (this) {
|
|
||||||
is None -> "None"
|
|
||||||
is Success -> "Some(${value.toString()})"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
sealed class ResourceSome<out T> {
|
|
||||||
data class Success<out T>(val value: T) : ResourceSome<T>()
|
|
||||||
object None : ResourceSome<Nothing>()
|
|
||||||
data class Loading(val data: Any? = null) : ResourceSome<Nothing>()
|
|
||||||
}
|
|
||||||
|
|
||||||
sealed class Resource<out T> {
|
sealed class Resource<out T> {
|
||||||
data class Success<out T>(val value: T) : Resource<T>()
|
data class Success<out T>(val value: T) : Resource<T>()
|
||||||
data class Failure(
|
data class Failure(
|
||||||
|
@ -155,6 +129,70 @@ fun CoroutineScope.launchSafe(
|
||||||
return this.launch(context, start, obj)
|
return this.launch(context, start, obj)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun<T> throwAbleToResource(
|
||||||
|
throwable: Throwable
|
||||||
|
): Resource<T> {
|
||||||
|
return when (throwable) {
|
||||||
|
is NullPointerException -> {
|
||||||
|
for (line in throwable.stackTrace) {
|
||||||
|
if (line?.fileName?.endsWith("provider.kt", ignoreCase = true) == true) {
|
||||||
|
return Resource.Failure(
|
||||||
|
false,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
"NullPointerException at ${line.fileName} ${line.lineNumber}\nSite might have updated or added Cloudflare/DDOS protection"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
safeFail(throwable)
|
||||||
|
}
|
||||||
|
is SocketTimeoutException, is InterruptedIOException -> {
|
||||||
|
Resource.Failure(
|
||||||
|
true,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
"Connection Timeout\nPlease try again later."
|
||||||
|
)
|
||||||
|
}
|
||||||
|
is HttpException -> {
|
||||||
|
Resource.Failure(
|
||||||
|
false,
|
||||||
|
throwable.statusCode,
|
||||||
|
null,
|
||||||
|
throwable.message ?: "HttpException"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
is UnknownHostException -> {
|
||||||
|
Resource.Failure(true, null, null, "Cannot connect to server, try again later.\n${throwable.message}")
|
||||||
|
}
|
||||||
|
is ErrorLoadingException -> {
|
||||||
|
Resource.Failure(
|
||||||
|
true,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
throwable.message ?: "Error loading, try again later."
|
||||||
|
)
|
||||||
|
}
|
||||||
|
is NotImplementedError -> {
|
||||||
|
Resource.Failure(false, null, null, "This operation is not implemented.")
|
||||||
|
}
|
||||||
|
is SSLHandshakeException -> {
|
||||||
|
Resource.Failure(
|
||||||
|
true,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
(throwable.message ?: "SSLHandshakeException") + "\nTry a VPN or DNS."
|
||||||
|
)
|
||||||
|
}
|
||||||
|
is CancellationException -> {
|
||||||
|
throwable.cause?.let {
|
||||||
|
throwAbleToResource(it)
|
||||||
|
} ?: safeFail(throwable)
|
||||||
|
}
|
||||||
|
else -> safeFail(throwable)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun <T> safeApiCall(
|
suspend fun <T> safeApiCall(
|
||||||
apiCall: suspend () -> T,
|
apiCall: suspend () -> T,
|
||||||
): Resource<T> {
|
): Resource<T> {
|
||||||
|
@ -163,60 +201,7 @@ suspend fun <T> safeApiCall(
|
||||||
Resource.Success(apiCall.invoke())
|
Resource.Success(apiCall.invoke())
|
||||||
} catch (throwable: Throwable) {
|
} catch (throwable: Throwable) {
|
||||||
logError(throwable)
|
logError(throwable)
|
||||||
when (throwable) {
|
throwAbleToResource(throwable)
|
||||||
is NullPointerException -> {
|
|
||||||
for (line in throwable.stackTrace) {
|
|
||||||
if (line?.fileName?.endsWith("provider.kt", ignoreCase = true) == true) {
|
|
||||||
return@withContext Resource.Failure(
|
|
||||||
false,
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
"NullPointerException at ${line.fileName} ${line.lineNumber}\nSite might have updated or added Cloudflare/DDOS protection"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
safeFail(throwable)
|
|
||||||
}
|
|
||||||
is SocketTimeoutException, is InterruptedIOException -> {
|
|
||||||
Resource.Failure(
|
|
||||||
true,
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
"Connection Timeout\nPlease try again later."
|
|
||||||
)
|
|
||||||
}
|
|
||||||
is HttpException -> {
|
|
||||||
Resource.Failure(
|
|
||||||
false,
|
|
||||||
throwable.statusCode,
|
|
||||||
null,
|
|
||||||
throwable.message ?: "HttpException"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
is UnknownHostException -> {
|
|
||||||
Resource.Failure(true, null, null, "Cannot connect to server, try again later.")
|
|
||||||
}
|
|
||||||
is ErrorLoadingException -> {
|
|
||||||
Resource.Failure(
|
|
||||||
true,
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
throwable.message ?: "Error loading, try again later."
|
|
||||||
)
|
|
||||||
}
|
|
||||||
is NotImplementedError -> {
|
|
||||||
Resource.Failure(false, null, null, "This operation is not implemented.")
|
|
||||||
}
|
|
||||||
is SSLHandshakeException -> {
|
|
||||||
Resource.Failure(
|
|
||||||
true,
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
(throwable.message ?: "SSLHandshakeException") + "\nTry a VPN or DNS."
|
|
||||||
)
|
|
||||||
}
|
|
||||||
else -> safeFail(throwable)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -17,6 +17,8 @@ import java.net.URI
|
||||||
class CloudflareKiller : Interceptor {
|
class CloudflareKiller : Interceptor {
|
||||||
companion object {
|
companion object {
|
||||||
const val TAG = "CloudflareKiller"
|
const val TAG = "CloudflareKiller"
|
||||||
|
private val ERROR_CODES = listOf(403, 503)
|
||||||
|
private val CLOUDFLARE_SERVERS = listOf("cloudflare-nginx", "cloudflare")
|
||||||
fun parseCookieMap(cookie: String): Map<String, String> {
|
fun parseCookieMap(cookie: String): Map<String, String> {
|
||||||
return cookie.split(";").associate {
|
return cookie.split(";").associate {
|
||||||
val split = it.split("=")
|
val split = it.split("=")
|
||||||
|
@ -48,15 +50,23 @@ class CloudflareKiller : Interceptor {
|
||||||
|
|
||||||
override fun intercept(chain: Interceptor.Chain): Response = runBlocking {
|
override fun intercept(chain: Interceptor.Chain): Response = runBlocking {
|
||||||
val request = chain.request()
|
val request = chain.request()
|
||||||
val cookies = savedCookies[request.url.host]
|
|
||||||
|
|
||||||
if (cookies == null) {
|
when (val cookies = savedCookies[request.url.host]) {
|
||||||
bypassCloudflare(request)?.let {
|
null -> {
|
||||||
Log.d(TAG, "Succeeded bypassing cloudflare: ${request.url}")
|
val response = chain.proceed(request)
|
||||||
return@runBlocking it
|
if(!(response.header("Server") in CLOUDFLARE_SERVERS && response.code in ERROR_CODES)) {
|
||||||
|
return@runBlocking response
|
||||||
|
} else {
|
||||||
|
response.close()
|
||||||
|
bypassCloudflare(request)?.let {
|
||||||
|
Log.d(TAG, "Succeeded bypassing cloudflare: ${request.url}")
|
||||||
|
return@runBlocking it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
return@runBlocking proceed(request, cookies)
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
return@runBlocking proceed(request, cookies)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
debugWarning({ true }) { "Failed cloudflare at: ${request.url}" }
|
debugWarning({ true }) { "Failed cloudflare at: ${request.url}" }
|
||||||
|
|
|
@ -2,6 +2,8 @@ package com.lagradost.cloudstream3.network
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.net.http.SslError
|
import android.net.http.SslError
|
||||||
|
import android.os.Handler
|
||||||
|
import android.os.Looper
|
||||||
import android.webkit.*
|
import android.webkit.*
|
||||||
import com.lagradost.cloudstream3.AcraApplication
|
import com.lagradost.cloudstream3.AcraApplication
|
||||||
import com.lagradost.cloudstream3.AcraApplication.Companion.context
|
import com.lagradost.cloudstream3.AcraApplication.Companion.context
|
||||||
|
@ -27,16 +29,39 @@ import java.net.URI
|
||||||
* @param additionalUrls this will make resolveUsingWebView also return all other requests matching the list of Regex.
|
* @param additionalUrls this will make resolveUsingWebView also return all other requests matching the list of Regex.
|
||||||
* @param userAgent if null then will use the default user agent
|
* @param userAgent if null then will use the default user agent
|
||||||
* @param useOkhttp will try to use the okhttp client as much as possible, but this might cause some requests to fail. Disable for cloudflare.
|
* @param useOkhttp will try to use the okhttp client as much as possible, but this might cause some requests to fail. Disable for cloudflare.
|
||||||
|
* @param script pass custom js to execute
|
||||||
|
* @param scriptCallback will be called with the result from custom js
|
||||||
|
* @param timeout close webview after timeout
|
||||||
* */
|
* */
|
||||||
class WebViewResolver(
|
class WebViewResolver(
|
||||||
val interceptUrl: Regex,
|
val interceptUrl: Regex,
|
||||||
val additionalUrls: List<Regex> = emptyList(),
|
val additionalUrls: List<Regex> = emptyList(),
|
||||||
val userAgent: String? = USER_AGENT,
|
val userAgent: String? = USER_AGENT,
|
||||||
val useOkhttp: Boolean = true
|
val useOkhttp: Boolean = true,
|
||||||
|
val script: String? = null,
|
||||||
|
val scriptCallback: ((String) -> Unit)? = null,
|
||||||
|
val timeout: Long = DEFAULT_TIMEOUT
|
||||||
) :
|
) :
|
||||||
Interceptor {
|
Interceptor {
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
interceptUrl: Regex,
|
||||||
|
additionalUrls: List<Regex> = emptyList(),
|
||||||
|
userAgent: String? = USER_AGENT,
|
||||||
|
useOkhttp: Boolean = true,
|
||||||
|
script: String? = null,
|
||||||
|
scriptCallback: ((String) -> Unit)? = null,
|
||||||
|
) : this(interceptUrl, additionalUrls, userAgent, useOkhttp, script, scriptCallback, DEFAULT_TIMEOUT)
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
interceptUrl: Regex,
|
||||||
|
additionalUrls: List<Regex> = emptyList(),
|
||||||
|
userAgent: String? = USER_AGENT,
|
||||||
|
useOkhttp: Boolean = true
|
||||||
|
) : this(interceptUrl, additionalUrls, userAgent, useOkhttp, null, null, DEFAULT_TIMEOUT)
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
private const val DEFAULT_TIMEOUT = 60_000L
|
||||||
var webViewUserAgent: String? = null
|
var webViewUserAgent: String? = null
|
||||||
|
|
||||||
@JvmName("getWebViewUserAgent1")
|
@JvmName("getWebViewUserAgent1")
|
||||||
|
@ -136,6 +161,14 @@ class WebViewResolver(
|
||||||
val webViewUrl = request.url.toString()
|
val webViewUrl = request.url.toString()
|
||||||
println("Loading WebView URL: $webViewUrl")
|
println("Loading WebView URL: $webViewUrl")
|
||||||
|
|
||||||
|
if (script != null) {
|
||||||
|
val handler = Handler(Looper.getMainLooper())
|
||||||
|
handler.post {
|
||||||
|
view.evaluateJavascript("$script")
|
||||||
|
{ scriptCallback?.invoke(it) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (interceptUrl.containsMatchIn(webViewUrl)) {
|
if (interceptUrl.containsMatchIn(webViewUrl)) {
|
||||||
fixedRequest = request.toRequest()?.also {
|
fixedRequest = request.toRequest()?.also {
|
||||||
requestCallBack(it)
|
requestCallBack(it)
|
||||||
|
@ -241,7 +274,7 @@ class WebViewResolver(
|
||||||
|
|
||||||
var loop = 0
|
var loop = 0
|
||||||
// Timeouts after this amount, 60s
|
// Timeouts after this amount, 60s
|
||||||
val totalTime = 60000L
|
val totalTime = timeout
|
||||||
|
|
||||||
val delayTime = 100L
|
val delayTime = 100L
|
||||||
|
|
||||||
|
|
|
@ -36,7 +36,9 @@ abstract class Plugin {
|
||||||
Log.i(PLUGIN_TAG, "Adding ${element.name} (${element.mainUrl}) MainAPI")
|
Log.i(PLUGIN_TAG, "Adding ${element.name} (${element.mainUrl}) MainAPI")
|
||||||
element.sourcePlugin = this.__filename
|
element.sourcePlugin = this.__filename
|
||||||
// Race condition causing which would case duplicates if not for distinctBy
|
// Race condition causing which would case duplicates if not for distinctBy
|
||||||
APIHolder.allProviders.add(element)
|
synchronized(APIHolder.allProviders) {
|
||||||
|
APIHolder.allProviders.add(element)
|
||||||
|
}
|
||||||
APIHolder.addPluginMapping(element)
|
APIHolder.addPluginMapping(element)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -51,10 +53,14 @@ abstract class Plugin {
|
||||||
}
|
}
|
||||||
|
|
||||||
class Manifest {
|
class Manifest {
|
||||||
@JsonProperty("name") var name: String? = null
|
@JsonProperty("name")
|
||||||
@JsonProperty("pluginClassName") var pluginClassName: String? = null
|
var name: String? = null
|
||||||
@JsonProperty("version") var version: Int? = null
|
@JsonProperty("pluginClassName")
|
||||||
@JsonProperty("requiresResources") var requiresResources: Boolean = false
|
var pluginClassName: String? = null
|
||||||
|
@JsonProperty("version")
|
||||||
|
var version: Int? = null
|
||||||
|
@JsonProperty("requiresResources")
|
||||||
|
var requiresResources: Boolean = false
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -137,6 +137,20 @@ object PluginManager {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes all generated oat files which will force Android to recompile the dex extensions.
|
||||||
|
* This might fix unrecoverable SIGSEGV exceptions when old oat files are loaded in a new app update.
|
||||||
|
*/
|
||||||
|
fun deleteAllOatFiles(context: Context) {
|
||||||
|
File("${context.filesDir}/${ONLINE_PLUGINS_FOLDER}").listFiles()?.forEach { repo ->
|
||||||
|
repo.listFiles { file -> file.name == "oat" && file.isDirectory }?.forEach { file ->
|
||||||
|
val success = file.deleteRecursively()
|
||||||
|
Log.i(TAG, "Deleted oat directory: ${file.absolutePath} Success=$success")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
fun getPluginsOnline(): Array<PluginData> {
|
fun getPluginsOnline(): Array<PluginData> {
|
||||||
return getKey(PLUGINS_KEY) ?: emptyArray()
|
return getKey(PLUGINS_KEY) ?: emptyArray()
|
||||||
}
|
}
|
||||||
|
@ -163,7 +177,11 @@ object PluginManager {
|
||||||
private val classLoaders: MutableMap<PathClassLoader, Plugin> =
|
private val classLoaders: MutableMap<PathClassLoader, Plugin> =
|
||||||
HashMap<PathClassLoader, Plugin>()
|
HashMap<PathClassLoader, Plugin>()
|
||||||
|
|
||||||
private var loadedLocalPlugins = false
|
var loadedLocalPlugins = false
|
||||||
|
private set
|
||||||
|
|
||||||
|
var loadedOnlinePlugins = false
|
||||||
|
private set
|
||||||
private val gson = Gson()
|
private val gson = Gson()
|
||||||
|
|
||||||
private suspend fun maybeLoadPlugin(context: Context, file: File) {
|
private suspend fun maybeLoadPlugin(context: Context, file: File) {
|
||||||
|
@ -277,6 +295,7 @@ object PluginManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
// ioSafe {
|
// ioSafe {
|
||||||
|
loadedOnlinePlugins = true
|
||||||
afterPluginsLoadedEvent.invoke(false)
|
afterPluginsLoadedEvent.invoke(false)
|
||||||
// }
|
// }
|
||||||
|
|
||||||
|
@ -289,7 +308,7 @@ object PluginManager {
|
||||||
* 2. Fetch all not downloaded plugins
|
* 2. Fetch all not downloaded plugins
|
||||||
* 3. Download them and reload plugins
|
* 3. Download them and reload plugins
|
||||||
**/
|
**/
|
||||||
fun downloadNotExistingPluginsAndLoad(activity: Activity) {
|
fun downloadNotExistingPluginsAndLoad(activity: Activity, mode: AutoDownloadMode) {
|
||||||
val newDownloadPlugins = mutableListOf<String>()
|
val newDownloadPlugins = mutableListOf<String>()
|
||||||
val urls = (getKey<Array<RepositoryData>>(REPOSITORIES_KEY)
|
val urls = (getKey<Array<RepositoryData>>(REPOSITORIES_KEY)
|
||||||
?: emptyArray()) + PREBUILT_REPOSITORIES
|
?: emptyArray()) + PREBUILT_REPOSITORIES
|
||||||
|
@ -303,6 +322,8 @@ object PluginManager {
|
||||||
// Iterate online repos and returns not downloaded plugins
|
// Iterate online repos and returns not downloaded plugins
|
||||||
val notDownloadedPlugins = onlinePlugins.mapNotNull { onlineData ->
|
val notDownloadedPlugins = onlinePlugins.mapNotNull { onlineData ->
|
||||||
val sitePlugin = onlineData.second
|
val sitePlugin = onlineData.second
|
||||||
|
val tvtypes = sitePlugin.tvTypes ?: listOf()
|
||||||
|
|
||||||
//Don't include empty urls
|
//Don't include empty urls
|
||||||
if (sitePlugin.url.isBlank()) {
|
if (sitePlugin.url.isBlank()) {
|
||||||
return@mapNotNull null
|
return@mapNotNull null
|
||||||
|
@ -317,22 +338,29 @@ object PluginManager {
|
||||||
return@mapNotNull null
|
return@mapNotNull null
|
||||||
}
|
}
|
||||||
|
|
||||||
//Omit lang not selected on language setting
|
//Omit non-NSFW if mode is set to NSFW only
|
||||||
val lang = sitePlugin.language ?: return@mapNotNull null
|
if (mode == AutoDownloadMode.NsfwOnly) {
|
||||||
//If set to 'universal', don't skip any language
|
if (tvtypes.contains(TvType.NSFW.name) == false) {
|
||||||
if (!providerLang.contains(AllLanguagesName) && !providerLang.contains(lang)) {
|
return@mapNotNull null
|
||||||
return@mapNotNull null
|
|
||||||
}
|
|
||||||
//Log.i(TAG, "sitePlugin lang => $lang")
|
|
||||||
|
|
||||||
//Omit NSFW, if disabled
|
|
||||||
sitePlugin.tvTypes?.let { tvtypes ->
|
|
||||||
if (!settingsForProvider.enableAdult) {
|
|
||||||
if (tvtypes.contains(TvType.NSFW.name)) {
|
|
||||||
return@mapNotNull null
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
//Omit NSFW, if disabled
|
||||||
|
if (!settingsForProvider.enableAdult) {
|
||||||
|
if (tvtypes.contains(TvType.NSFW.name)) {
|
||||||
|
return@mapNotNull null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//Omit lang not selected on language setting
|
||||||
|
if (mode == AutoDownloadMode.FilterByLang) {
|
||||||
|
val lang = sitePlugin.language ?: return@mapNotNull null
|
||||||
|
//If set to 'universal', don't skip any language
|
||||||
|
if (!providerLang.contains(AllLanguagesName) && !providerLang.contains(lang)) {
|
||||||
|
return@mapNotNull null
|
||||||
|
}
|
||||||
|
//Log.i(TAG, "sitePlugin lang => $lang")
|
||||||
|
}
|
||||||
|
|
||||||
val savedData = PluginData(
|
val savedData = PluginData(
|
||||||
url = sitePlugin.url,
|
url = sitePlugin.url,
|
||||||
internalName = sitePlugin.internalName,
|
internalName = sitePlugin.internalName,
|
||||||
|
@ -449,6 +477,14 @@ object PluginManager {
|
||||||
Log.i(TAG, "Loading plugin: $data")
|
Log.i(TAG, "Loading plugin: $data")
|
||||||
|
|
||||||
return try {
|
return try {
|
||||||
|
// in case of android 14 then
|
||||||
|
try {
|
||||||
|
File(filePath).setReadOnly()
|
||||||
|
} catch (t: Throwable) {
|
||||||
|
Log.e(TAG, "Failed to set dex as readonly")
|
||||||
|
logError(t)
|
||||||
|
}
|
||||||
|
|
||||||
val loader = PathClassLoader(filePath, context.classLoader)
|
val loader = PathClassLoader(filePath, context.classLoader)
|
||||||
var manifest: Plugin.Manifest
|
var manifest: Plugin.Manifest
|
||||||
loader.getResourceAsStream("manifest.json").use { stream ->
|
loader.getResourceAsStream("manifest.json").use { stream ->
|
||||||
|
@ -531,10 +567,14 @@ object PluginManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
// remove all registered apis
|
// remove all registered apis
|
||||||
APIHolder.apis.filter { api -> api.sourcePlugin == plugin.__filename }.forEach {
|
synchronized(APIHolder.apis) {
|
||||||
removePluginMapping(it)
|
APIHolder.apis.filter { api -> api.sourcePlugin == plugin.__filename }.forEach {
|
||||||
|
removePluginMapping(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
synchronized(APIHolder.allProviders) {
|
||||||
|
APIHolder.allProviders.removeIf { provider: MainAPI -> provider.sourcePlugin == plugin.__filename }
|
||||||
}
|
}
|
||||||
APIHolder.allProviders.removeIf { provider: MainAPI -> provider.sourcePlugin == plugin.__filename }
|
|
||||||
extractorApis.removeIf { provider: ExtractorApi -> provider.sourcePlugin == plugin.__filename }
|
extractorApis.removeIf { provider: ExtractorApi -> provider.sourcePlugin == plugin.__filename }
|
||||||
|
|
||||||
classLoaders.values.removeIf { v -> v == plugin }
|
classLoaders.values.removeIf { v -> v == plugin }
|
||||||
|
@ -692,4 +732,4 @@ object PluginManager {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,22 +8,14 @@ import com.lagradost.cloudstream3.AcraApplication.Companion.setKey
|
||||||
import com.lagradost.cloudstream3.R
|
import com.lagradost.cloudstream3.R
|
||||||
import java.security.MessageDigest
|
import java.security.MessageDigest
|
||||||
import com.lagradost.cloudstream3.app
|
import com.lagradost.cloudstream3.app
|
||||||
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
|
|
||||||
import com.lagradost.cloudstream3.utils.Coroutines.main
|
import com.lagradost.cloudstream3.utils.Coroutines.main
|
||||||
import kotlinx.coroutines.delay
|
|
||||||
import kotlinx.coroutines.sync.Mutex
|
import kotlinx.coroutines.sync.Mutex
|
||||||
import kotlinx.coroutines.sync.withLock
|
import kotlinx.coroutines.sync.withLock
|
||||||
|
|
||||||
object VotingApi { // please do not cheat the votes lol
|
object VotingApi { // please do not cheat the votes lol
|
||||||
private const val LOGKEY = "VotingApi"
|
private const val LOGKEY = "VotingApi"
|
||||||
|
|
||||||
enum class VoteType(val value: Int) {
|
private const val apiDomain = "https://counterapi.com/api"
|
||||||
UPVOTE(1),
|
|
||||||
DOWNVOTE(-1),
|
|
||||||
NONE(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
private val apiDomain = "https://api.countapi.xyz"
|
|
||||||
|
|
||||||
private fun transformUrl(url: String): String = // dont touch or all votes get reset
|
private fun transformUrl(url: String): String = // dont touch or all votes get reset
|
||||||
MessageDigest
|
MessageDigest
|
||||||
|
@ -35,12 +27,12 @@ object VotingApi { // please do not cheat the votes lol
|
||||||
return getVotes(url)
|
return getVotes(url)
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun SitePlugin.vote(requestType: VoteType): Int {
|
fun SitePlugin.hasVoted(): Boolean {
|
||||||
return vote(url, requestType)
|
return hasVoted(url)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun SitePlugin.getVoteType(): VoteType {
|
suspend fun SitePlugin.vote(): Int {
|
||||||
return getVoteType(url)
|
return vote(url)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun SitePlugin.canVote(): Boolean {
|
fun SitePlugin.canVote(): Boolean {
|
||||||
|
@ -50,28 +42,31 @@ object VotingApi { // please do not cheat the votes lol
|
||||||
// Plugin url to Int
|
// Plugin url to Int
|
||||||
private val votesCache = mutableMapOf<String, Int>()
|
private val votesCache = mutableMapOf<String, Int>()
|
||||||
|
|
||||||
suspend fun getVotes(pluginUrl: String): Int {
|
private fun getRepository(pluginUrl: String) = pluginUrl
|
||||||
val url = "${apiDomain}/get/cs3-votes/${transformUrl(pluginUrl)}"
|
.split("/")
|
||||||
|
.drop(2)
|
||||||
|
.take(3)
|
||||||
|
.joinToString("-")
|
||||||
|
|
||||||
|
private suspend fun readVote(pluginUrl: String): Int {
|
||||||
|
var url = "${apiDomain}/cs-${getRepository(pluginUrl)}/vote/${transformUrl(pluginUrl)}?readOnly=true"
|
||||||
Log.d(LOGKEY, "Requesting: $url")
|
Log.d(LOGKEY, "Requesting: $url")
|
||||||
return votesCache[pluginUrl] ?: app.get(url).parsedSafe<Result>()?.value?.also {
|
return app.get(url).parsedSafe<Result>()?.value ?: 0
|
||||||
votesCache[pluginUrl] = it
|
}
|
||||||
} ?: (0.also {
|
|
||||||
ioSafe {
|
private suspend fun writeVote(pluginUrl: String): Boolean {
|
||||||
createBucket(pluginUrl)
|
var url = "${apiDomain}/cs-${getRepository(pluginUrl)}/vote/${transformUrl(pluginUrl)}"
|
||||||
|
Log.d(LOGKEY, "Requesting: $url")
|
||||||
|
return app.get(url).parsedSafe<Result>()?.value != null
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun getVotes(pluginUrl: String): Int =
|
||||||
|
votesCache[pluginUrl] ?: readVote(pluginUrl).also {
|
||||||
|
votesCache[pluginUrl] = it
|
||||||
}
|
}
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getVoteType(pluginUrl: String): VoteType {
|
fun hasVoted(pluginUrl: String) =
|
||||||
return getKey("cs3-votes/${transformUrl(pluginUrl)}") ?: VoteType.NONE
|
getKey("cs3-votes/${transformUrl(pluginUrl)}") ?: false
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun createBucket(pluginUrl: String) {
|
|
||||||
val url =
|
|
||||||
"${apiDomain}/create?namespace=cs3-votes&key=${transformUrl(pluginUrl)}&value=0&update_lowerbound=-2&update_upperbound=2&enable_reset=0"
|
|
||||||
Log.d(LOGKEY, "Requesting: $url")
|
|
||||||
app.get(url)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun canVote(pluginUrl: String): Boolean {
|
fun canVote(pluginUrl: String): Boolean {
|
||||||
if (!PluginManager.urlPlugins.contains(pluginUrl)) return false
|
if (!PluginManager.urlPlugins.contains(pluginUrl)) return false
|
||||||
|
@ -79,7 +74,7 @@ object VotingApi { // please do not cheat the votes lol
|
||||||
}
|
}
|
||||||
|
|
||||||
private val voteLock = Mutex()
|
private val voteLock = Mutex()
|
||||||
suspend fun vote(pluginUrl: String, requestType: VoteType): Int {
|
suspend fun vote(pluginUrl: String): Int {
|
||||||
// Prevent multiple requests at the same time.
|
// Prevent multiple requests at the same time.
|
||||||
voteLock.withLock {
|
voteLock.withLock {
|
||||||
if (!canVote(pluginUrl)) {
|
if (!canVote(pluginUrl)) {
|
||||||
|
@ -90,33 +85,21 @@ object VotingApi { // please do not cheat the votes lol
|
||||||
return getVotes(pluginUrl)
|
return getVotes(pluginUrl)
|
||||||
}
|
}
|
||||||
|
|
||||||
val savedType: VoteType =
|
if (hasVoted(pluginUrl)) {
|
||||||
getKey("cs3-votes/${transformUrl(pluginUrl)}") ?: VoteType.NONE
|
main {
|
||||||
|
Toast.makeText(context, R.string.already_voted, Toast.LENGTH_SHORT)
|
||||||
val newType = if (requestType == savedType) VoteType.NONE else requestType
|
.show()
|
||||||
val changeValue = if (requestType == savedType) {
|
}
|
||||||
-requestType.value
|
return getVotes(pluginUrl)
|
||||||
} else if (savedType == VoteType.NONE) {
|
|
||||||
requestType.value
|
|
||||||
} else if (savedType != requestType) {
|
|
||||||
-savedType.value + requestType.value
|
|
||||||
} else 0
|
|
||||||
|
|
||||||
// Pre-emptively set vote key
|
|
||||||
setKey("cs3-votes/${transformUrl(pluginUrl)}", newType)
|
|
||||||
|
|
||||||
val url =
|
|
||||||
"${apiDomain}/update/cs3-votes/${transformUrl(pluginUrl)}?amount=${changeValue}"
|
|
||||||
Log.d(LOGKEY, "Requesting: $url")
|
|
||||||
val res = app.get(url).parsedSafe<Result>()?.value
|
|
||||||
|
|
||||||
if (res == null) {
|
|
||||||
// "Refund" key if the response is invalid
|
|
||||||
setKey("cs3-votes/${transformUrl(pluginUrl)}", savedType)
|
|
||||||
} else {
|
|
||||||
votesCache[pluginUrl] = res
|
|
||||||
}
|
}
|
||||||
return res ?: 0
|
|
||||||
|
|
||||||
|
if (writeVote(pluginUrl)) {
|
||||||
|
setKey("cs3-votes/${transformUrl(pluginUrl)}", true)
|
||||||
|
votesCache[pluginUrl] = votesCache[pluginUrl]?.plus(1) ?: 1
|
||||||
|
}
|
||||||
|
|
||||||
|
return getVotes(pluginUrl)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,96 @@
|
||||||
|
package com.lagradost.cloudstream3.services
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.core.app.NotificationCompat
|
||||||
|
import androidx.work.Constraints
|
||||||
|
import androidx.work.CoroutineWorker
|
||||||
|
import androidx.work.ExistingPeriodicWorkPolicy
|
||||||
|
import androidx.work.ForegroundInfo
|
||||||
|
import androidx.work.PeriodicWorkRequest
|
||||||
|
import androidx.work.WorkManager
|
||||||
|
import androidx.work.WorkerParameters
|
||||||
|
import com.lagradost.cloudstream3.R
|
||||||
|
import com.lagradost.cloudstream3.utils.AppUtils.createNotificationChannel
|
||||||
|
import com.lagradost.cloudstream3.utils.BackupUtils
|
||||||
|
import com.lagradost.cloudstream3.utils.UIHelper.colorFromAttribute
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
|
const val BACKUP_CHANNEL_ID = "cloudstream3.backups"
|
||||||
|
const val BACKUP_WORK_NAME = "work_backup"
|
||||||
|
const val BACKUP_CHANNEL_NAME = "Backups"
|
||||||
|
const val BACKUP_CHANNEL_DESCRIPTION = "Notifications for background backups"
|
||||||
|
const val BACKUP_NOTIFICATION_ID = 938712898 // Random unique
|
||||||
|
|
||||||
|
class BackupWorkManager(val context: Context, workerParams: WorkerParameters) :
|
||||||
|
CoroutineWorker(context, workerParams) {
|
||||||
|
companion object {
|
||||||
|
fun enqueuePeriodicWork(context: Context?, intervalHours: Long) {
|
||||||
|
if (context == null) return
|
||||||
|
|
||||||
|
if (intervalHours == 0L) {
|
||||||
|
WorkManager.getInstance(context).cancelUniqueWork(BACKUP_WORK_NAME)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val constraints = Constraints.Builder()
|
||||||
|
.setRequiresStorageNotLow(true)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
val periodicSyncDataWork =
|
||||||
|
PeriodicWorkRequest.Builder(
|
||||||
|
BackupWorkManager::class.java,
|
||||||
|
intervalHours,
|
||||||
|
TimeUnit.HOURS
|
||||||
|
)
|
||||||
|
.addTag(BACKUP_WORK_NAME)
|
||||||
|
.setConstraints(constraints)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
WorkManager.getInstance(context).enqueueUniquePeriodicWork(
|
||||||
|
BACKUP_WORK_NAME,
|
||||||
|
ExistingPeriodicWorkPolicy.UPDATE,
|
||||||
|
periodicSyncDataWork
|
||||||
|
)
|
||||||
|
|
||||||
|
// Uncomment below for testing
|
||||||
|
|
||||||
|
// val oneTimeBackupWork =
|
||||||
|
// OneTimeWorkRequest.Builder(BackupWorkManager::class.java)
|
||||||
|
// .addTag(BACKUP_WORK_NAME)
|
||||||
|
// .setConstraints(constraints)
|
||||||
|
// .build()
|
||||||
|
//
|
||||||
|
// WorkManager.getInstance(context).enqueue(oneTimeBackupWork)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val backupNotificationBuilder =
|
||||||
|
NotificationCompat.Builder(context, BACKUP_CHANNEL_ID)
|
||||||
|
.setColorized(true)
|
||||||
|
.setOnlyAlertOnce(true)
|
||||||
|
.setSilent(true)
|
||||||
|
.setAutoCancel(true)
|
||||||
|
.setContentTitle(context.getString(R.string.pref_category_backup))
|
||||||
|
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
|
||||||
|
.setColor(context.colorFromAttribute(R.attr.colorPrimary))
|
||||||
|
.setSmallIcon(R.drawable.ic_cloudstream_monochrome_big)
|
||||||
|
|
||||||
|
override suspend fun doWork(): Result {
|
||||||
|
context.createNotificationChannel(
|
||||||
|
BACKUP_CHANNEL_ID,
|
||||||
|
BACKUP_CHANNEL_NAME,
|
||||||
|
BACKUP_CHANNEL_DESCRIPTION
|
||||||
|
)
|
||||||
|
|
||||||
|
setForeground(
|
||||||
|
ForegroundInfo(
|
||||||
|
BACKUP_NOTIFICATION_ID,
|
||||||
|
backupNotificationBuilder.build()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
BackupUtils.backup(context)
|
||||||
|
|
||||||
|
return Result.success()
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,11 +1,23 @@
|
||||||
package com.lagradost.cloudstream3.subtitles
|
package com.lagradost.cloudstream3.subtitles
|
||||||
|
|
||||||
import androidx.annotation.WorkerThread
|
import androidx.annotation.WorkerThread
|
||||||
|
import androidx.core.net.toUri
|
||||||
|
import com.lagradost.cloudstream3.MainActivity.Companion.deleteFileOnExit
|
||||||
|
import com.lagradost.cloudstream3.app
|
||||||
import com.lagradost.cloudstream3.subtitles.AbstractSubtitleEntities.SubtitleEntity
|
import com.lagradost.cloudstream3.subtitles.AbstractSubtitleEntities.SubtitleEntity
|
||||||
import com.lagradost.cloudstream3.subtitles.AbstractSubtitleEntities.SubtitleSearch
|
import com.lagradost.cloudstream3.subtitles.AbstractSubtitleEntities.SubtitleSearch
|
||||||
import com.lagradost.cloudstream3.syncproviders.AuthAPI
|
import com.lagradost.cloudstream3.syncproviders.AuthAPI
|
||||||
|
import com.lagradost.cloudstream3.ui.player.SubtitleOrigin
|
||||||
|
import okio.BufferedSource
|
||||||
|
import okio.buffer
|
||||||
|
import okio.sink
|
||||||
|
import okio.source
|
||||||
|
import java.io.File
|
||||||
|
import java.util.zip.ZipInputStream
|
||||||
|
|
||||||
interface AbstractSubProvider {
|
interface AbstractSubProvider {
|
||||||
|
val idPrefix: String
|
||||||
|
|
||||||
@WorkerThread
|
@WorkerThread
|
||||||
suspend fun search(query: SubtitleSearch): List<SubtitleEntity>? {
|
suspend fun search(query: SubtitleSearch): List<SubtitleEntity>? {
|
||||||
throw NotImplementedError()
|
throw NotImplementedError()
|
||||||
|
@ -15,6 +27,98 @@ interface AbstractSubProvider {
|
||||||
suspend fun load(data: SubtitleEntity): String? {
|
suspend fun load(data: SubtitleEntity): String? {
|
||||||
throw NotImplementedError()
|
throw NotImplementedError()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@WorkerThread
|
||||||
|
suspend fun SubtitleResource.getResources(data: SubtitleEntity) {
|
||||||
|
this.addUrl(load(data))
|
||||||
|
}
|
||||||
|
|
||||||
|
@WorkerThread
|
||||||
|
suspend fun getResource(data: SubtitleEntity): SubtitleResource {
|
||||||
|
return SubtitleResource().apply {
|
||||||
|
this.getResources(data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A builder for subtitle files.
|
||||||
|
* @see addUrl
|
||||||
|
* @see addFile
|
||||||
|
*/
|
||||||
|
class SubtitleResource {
|
||||||
|
fun downloadFile(source: BufferedSource): File {
|
||||||
|
val file = File.createTempFile("temp-subtitle", ".tmp").apply {
|
||||||
|
deleteFileOnExit(this)
|
||||||
|
}
|
||||||
|
val sink = file.sink().buffer()
|
||||||
|
sink.writeAll(source)
|
||||||
|
sink.close()
|
||||||
|
source.close()
|
||||||
|
|
||||||
|
return file
|
||||||
|
}
|
||||||
|
|
||||||
|
fun unzip(file: File): List<Pair<String, File>> {
|
||||||
|
val entries = mutableListOf<Pair<String, File>>()
|
||||||
|
|
||||||
|
ZipInputStream(file.inputStream()).use { zipInputStream ->
|
||||||
|
var zipEntry = zipInputStream.nextEntry
|
||||||
|
|
||||||
|
while (zipEntry != null) {
|
||||||
|
val tempFile = File.createTempFile("unzipped-subtitle", ".tmp").apply {
|
||||||
|
deleteFileOnExit(this)
|
||||||
|
}
|
||||||
|
entries.add(zipEntry.name to tempFile)
|
||||||
|
|
||||||
|
tempFile.sink().buffer().use { buffer ->
|
||||||
|
buffer.writeAll(zipInputStream.source())
|
||||||
|
}
|
||||||
|
|
||||||
|
zipEntry = zipInputStream.nextEntry
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return entries
|
||||||
|
}
|
||||||
|
|
||||||
|
data class SingleSubtitleResource(
|
||||||
|
val name: String?,
|
||||||
|
val url: String,
|
||||||
|
val origin: SubtitleOrigin
|
||||||
|
)
|
||||||
|
|
||||||
|
private var resources: MutableList<SingleSubtitleResource> = mutableListOf()
|
||||||
|
|
||||||
|
fun getSubtitles(): List<SingleSubtitleResource> {
|
||||||
|
return resources.toList()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun addUrl(url: String?, name: String? = null) {
|
||||||
|
if (url == null) return
|
||||||
|
this.resources.add(
|
||||||
|
SingleSubtitleResource(name, url, SubtitleOrigin.URL)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun addFile(file: File, name: String? = null) {
|
||||||
|
this.resources.add(
|
||||||
|
SingleSubtitleResource(name, file.toUri().toString(), SubtitleOrigin.DOWNLOADED_FILE)
|
||||||
|
)
|
||||||
|
deleteFileOnExit(file)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun addZipUrl(
|
||||||
|
url: String,
|
||||||
|
nameGenerator: (String, File) -> String? = { _, _ -> null }
|
||||||
|
) {
|
||||||
|
val source = app.get(url).okhttpResponse.body.source()
|
||||||
|
val zip = downloadFile(source)
|
||||||
|
val realFiles = unzip(zip)
|
||||||
|
zip.deleteRecursively()
|
||||||
|
realFiles.forEach { (name, subtitleFile) ->
|
||||||
|
addFile(subtitleFile, nameGenerator(name, subtitleFile))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
interface AbstractSubApi : AbstractSubProvider, AuthAPI
|
interface AbstractSubApi : AbstractSubProvider, AuthAPI
|
|
@ -3,6 +3,7 @@ package com.lagradost.cloudstream3.syncproviders
|
||||||
import com.lagradost.cloudstream3.AcraApplication.Companion.getKey
|
import com.lagradost.cloudstream3.AcraApplication.Companion.getKey
|
||||||
import com.lagradost.cloudstream3.AcraApplication.Companion.removeKeys
|
import com.lagradost.cloudstream3.AcraApplication.Companion.removeKeys
|
||||||
import com.lagradost.cloudstream3.AcraApplication.Companion.setKey
|
import com.lagradost.cloudstream3.AcraApplication.Companion.setKey
|
||||||
|
import com.lagradost.cloudstream3.syncproviders.providers.SubScene
|
||||||
import com.lagradost.cloudstream3.syncproviders.providers.*
|
import com.lagradost.cloudstream3.syncproviders.providers.*
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
|
@ -11,26 +12,28 @@ abstract class AccountManager(private val defIndex: Int) : AuthAPI {
|
||||||
val malApi = MALApi(0)
|
val malApi = MALApi(0)
|
||||||
val aniListApi = AniListApi(0)
|
val aniListApi = AniListApi(0)
|
||||||
val openSubtitlesApi = OpenSubtitlesApi(0)
|
val openSubtitlesApi = OpenSubtitlesApi(0)
|
||||||
|
val simklApi = SimklApi(0)
|
||||||
val indexSubtitlesApi = IndexSubtitleApi()
|
val indexSubtitlesApi = IndexSubtitleApi()
|
||||||
val addic7ed = Addic7ed()
|
val addic7ed = Addic7ed()
|
||||||
|
val subScene = SubScene()
|
||||||
val localListApi = LocalList()
|
val localListApi = LocalList()
|
||||||
|
|
||||||
// used to login via app intent
|
// used to login via app intent
|
||||||
val OAuth2Apis
|
val OAuth2Apis
|
||||||
get() = listOf<OAuth2API>(
|
get() = listOf<OAuth2API>(
|
||||||
malApi, aniListApi
|
malApi, aniListApi, simklApi
|
||||||
)
|
)
|
||||||
|
|
||||||
// this needs init with context and can be accessed in settings
|
// this needs init with context and can be accessed in settings
|
||||||
val accountManagers
|
val accountManagers
|
||||||
get() = listOf(
|
get() = listOf(
|
||||||
malApi, aniListApi, openSubtitlesApi, //nginxApi
|
malApi, aniListApi, openSubtitlesApi, simklApi //nginxApi
|
||||||
)
|
)
|
||||||
|
|
||||||
// used for active syncing
|
// used for active syncing
|
||||||
val SyncApis
|
val SyncApis
|
||||||
get() = listOf(
|
get() = listOf(
|
||||||
SyncRepo(malApi), SyncRepo(aniListApi), SyncRepo(localListApi)
|
SyncRepo(malApi), SyncRepo(aniListApi), SyncRepo(localListApi), SyncRepo(simklApi)
|
||||||
)
|
)
|
||||||
|
|
||||||
val inAppAuths
|
val inAppAuths
|
||||||
|
@ -40,7 +43,8 @@ abstract class AccountManager(private val defIndex: Int) : AuthAPI {
|
||||||
get() = listOf(
|
get() = listOf(
|
||||||
openSubtitlesApi,
|
openSubtitlesApi,
|
||||||
indexSubtitlesApi, // they got anti scraping measures in place :(
|
indexSubtitlesApi, // they got anti scraping measures in place :(
|
||||||
addic7ed
|
addic7ed,
|
||||||
|
subScene
|
||||||
)
|
)
|
||||||
|
|
||||||
const val appString = "cloudstreamapp"
|
const val appString = "cloudstreamapp"
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
package com.lagradost.cloudstream3.syncproviders
|
package com.lagradost.cloudstream3.syncproviders
|
||||||
|
|
||||||
import com.lagradost.cloudstream3.*
|
import com.lagradost.cloudstream3.*
|
||||||
|
import com.lagradost.cloudstream3.ui.SyncWatchType
|
||||||
|
import com.lagradost.cloudstream3.ui.WatchType
|
||||||
import com.lagradost.cloudstream3.ui.library.ListSorting
|
import com.lagradost.cloudstream3.ui.library.ListSorting
|
||||||
import com.lagradost.cloudstream3.ui.result.UiText
|
import com.lagradost.cloudstream3.ui.result.UiText
|
||||||
import me.xdrop.fuzzywuzzy.FuzzySearch
|
import me.xdrop.fuzzywuzzy.FuzzySearch
|
||||||
|
@ -10,7 +12,8 @@ enum class SyncIdName {
|
||||||
MyAnimeList,
|
MyAnimeList,
|
||||||
Trakt,
|
Trakt,
|
||||||
Imdb,
|
Imdb,
|
||||||
LocalList
|
Simkl,
|
||||||
|
LocalList,
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SyncAPI : OAuth2API {
|
interface SyncAPI : OAuth2API {
|
||||||
|
@ -35,9 +38,9 @@ interface SyncAPI : OAuth2API {
|
||||||
4 -> PlanToWatch
|
4 -> PlanToWatch
|
||||||
5 -> ReWatching
|
5 -> ReWatching
|
||||||
*/
|
*/
|
||||||
suspend fun score(id: String, status: SyncStatus): Boolean
|
suspend fun score(id: String, status: AbstractSyncStatus): Boolean
|
||||||
|
|
||||||
suspend fun getStatus(id: String): SyncStatus?
|
suspend fun getStatus(id: String): AbstractSyncStatus?
|
||||||
|
|
||||||
suspend fun getResult(id: String): SyncResult?
|
suspend fun getResult(id: String): SyncResult?
|
||||||
|
|
||||||
|
@ -59,14 +62,25 @@ interface SyncAPI : OAuth2API {
|
||||||
override var id: Int? = null,
|
override var id: Int? = null,
|
||||||
) : SearchResponse
|
) : SearchResponse
|
||||||
|
|
||||||
data class SyncStatus(
|
abstract class AbstractSyncStatus {
|
||||||
val status: Int,
|
abstract var status: SyncWatchType
|
||||||
|
|
||||||
/** 1-10 */
|
/** 1-10 */
|
||||||
val score: Int?,
|
abstract var score: Int?
|
||||||
val watchedEpisodes: Int?,
|
abstract var watchedEpisodes: Int?
|
||||||
var isFavorite: Boolean? = null,
|
abstract var isFavorite: Boolean?
|
||||||
var maxEpisodes: Int? = null,
|
abstract var maxEpisodes: Int?
|
||||||
)
|
}
|
||||||
|
|
||||||
|
|
||||||
|
data class SyncStatus(
|
||||||
|
override var status: SyncWatchType,
|
||||||
|
/** 1-10 */
|
||||||
|
override var score: Int?,
|
||||||
|
override var watchedEpisodes: Int?,
|
||||||
|
override var isFavorite: Boolean? = null,
|
||||||
|
override var maxEpisodes: Int? = null,
|
||||||
|
) : AbstractSyncStatus()
|
||||||
|
|
||||||
data class SyncResult(
|
data class SyncResult(
|
||||||
/**Used to verify*/
|
/**Used to verify*/
|
||||||
|
@ -155,5 +169,8 @@ interface SyncAPI : OAuth2API {
|
||||||
override var posterHeaders: Map<String, String>?,
|
override var posterHeaders: Map<String, String>?,
|
||||||
override var quality: SearchQuality?,
|
override var quality: SearchQuality?,
|
||||||
override var id: Int? = null,
|
override var id: Int? = null,
|
||||||
|
val plot : String? = null,
|
||||||
|
val rating: Int? = null,
|
||||||
|
val tags: List<String>? = null,
|
||||||
) : SearchResponse
|
) : SearchResponse
|
||||||
}
|
}
|
|
@ -18,11 +18,11 @@ class SyncRepo(private val repo: SyncAPI) {
|
||||||
repo.requireLibraryRefresh = value
|
repo.requireLibraryRefresh = value
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun score(id: String, status: SyncAPI.SyncStatus): Resource<Boolean> {
|
suspend fun score(id: String, status: SyncAPI.AbstractSyncStatus): Resource<Boolean> {
|
||||||
return safeApiCall { repo.score(id, status) }
|
return safeApiCall { repo.score(id, status) }
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun getStatus(id: String): Resource<SyncAPI.SyncStatus> {
|
suspend fun getStatus(id: String): Resource<SyncAPI.AbstractSyncStatus> {
|
||||||
return safeApiCall { repo.getStatus(id) ?: throw ErrorLoadingException("No data") }
|
return safeApiCall { repo.getStatus(id) ?: throw ErrorLoadingException("No data") }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -13,6 +13,7 @@ import com.lagradost.cloudstream3.syncproviders.AccountManager
|
||||||
import com.lagradost.cloudstream3.syncproviders.AuthAPI
|
import com.lagradost.cloudstream3.syncproviders.AuthAPI
|
||||||
import com.lagradost.cloudstream3.syncproviders.SyncAPI
|
import com.lagradost.cloudstream3.syncproviders.SyncAPI
|
||||||
import com.lagradost.cloudstream3.syncproviders.SyncIdName
|
import com.lagradost.cloudstream3.syncproviders.SyncIdName
|
||||||
|
import com.lagradost.cloudstream3.ui.SyncWatchType
|
||||||
import com.lagradost.cloudstream3.ui.library.ListSorting
|
import com.lagradost.cloudstream3.ui.library.ListSorting
|
||||||
import com.lagradost.cloudstream3.ui.result.txt
|
import com.lagradost.cloudstream3.ui.result.txt
|
||||||
import com.lagradost.cloudstream3.utils.AppUtils.parseJson
|
import com.lagradost.cloudstream3.utils.AppUtils.parseJson
|
||||||
|
@ -158,23 +159,23 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getStatus(id: String): SyncAPI.SyncStatus? {
|
override suspend fun getStatus(id: String): SyncAPI.AbstractSyncStatus? {
|
||||||
val internalId = id.toIntOrNull() ?: return null
|
val internalId = id.toIntOrNull() ?: return null
|
||||||
val data = getDataAboutId(internalId) ?: return null
|
val data = getDataAboutId(internalId) ?: return null
|
||||||
|
|
||||||
return SyncAPI.SyncStatus(
|
return SyncAPI.SyncStatus(
|
||||||
score = data.score,
|
score = data.score,
|
||||||
watchedEpisodes = data.progress,
|
watchedEpisodes = data.progress,
|
||||||
status = data.type?.value ?: return null,
|
status = SyncWatchType.fromInternalId(data.type?.value ?: return null),
|
||||||
isFavorite = data.isFavourite,
|
isFavorite = data.isFavourite,
|
||||||
maxEpisodes = data.episodes,
|
maxEpisodes = data.episodes,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun score(id: String, status: SyncAPI.SyncStatus): Boolean {
|
override suspend fun score(id: String, status: SyncAPI.AbstractSyncStatus): Boolean {
|
||||||
return postDataAboutId(
|
return postDataAboutId(
|
||||||
id.toIntOrNull() ?: return false,
|
id.toIntOrNull() ?: return false,
|
||||||
fromIntToAnimeStatus(status.status),
|
fromIntToAnimeStatus(status.status.internalId),
|
||||||
status.score,
|
status.score,
|
||||||
status.watchedEpisodes
|
status.watchedEpisodes
|
||||||
).also {
|
).also {
|
||||||
|
@ -595,7 +596,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
//@JsonProperty("source") val source: String,
|
//@JsonProperty("source") val source: String,
|
||||||
@JsonProperty("episodes") val episodes: Int,
|
@JsonProperty("episodes") val episodes: Int,
|
||||||
@JsonProperty("title") val title: Title,
|
@JsonProperty("title") val title: Title,
|
||||||
//@JsonProperty("description") val description: String,
|
@JsonProperty("description") val description: String?,
|
||||||
@JsonProperty("coverImage") val coverImage: CoverImage,
|
@JsonProperty("coverImage") val coverImage: CoverImage,
|
||||||
@JsonProperty("synonyms") val synonyms: List<String>,
|
@JsonProperty("synonyms") val synonyms: List<String>,
|
||||||
@JsonProperty("nextAiringEpisode") val nextAiringEpisode: SeasonNextAiringEpisode?,
|
@JsonProperty("nextAiringEpisode") val nextAiringEpisode: SeasonNextAiringEpisode?,
|
||||||
|
@ -629,7 +630,8 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
?: this.media.coverImage.medium,
|
?: this.media.coverImage.medium,
|
||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
null
|
null,
|
||||||
|
plot = this.media.description
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,6 +23,47 @@ class IndexSubtitleApi : AbstractSubApi {
|
||||||
companion object {
|
companion object {
|
||||||
const val host = "https://indexsubtitle.com"
|
const val host = "https://indexsubtitle.com"
|
||||||
const val TAG = "INDEXSUBS"
|
const val TAG = "INDEXSUBS"
|
||||||
|
|
||||||
|
fun getOrdinal(num: Int?): String? {
|
||||||
|
return when (num) {
|
||||||
|
1 -> "First"
|
||||||
|
2 -> "Second"
|
||||||
|
3 -> "Third"
|
||||||
|
4 -> "Fourth"
|
||||||
|
5 -> "Fifth"
|
||||||
|
6 -> "Sixth"
|
||||||
|
7 -> "Seventh"
|
||||||
|
8 -> "Eighth"
|
||||||
|
9 -> "Ninth"
|
||||||
|
10 -> "Tenth"
|
||||||
|
11 -> "Eleventh"
|
||||||
|
12 -> "Twelfth"
|
||||||
|
13 -> "Thirteenth"
|
||||||
|
14 -> "Fourteenth"
|
||||||
|
15 -> "Fifteenth"
|
||||||
|
16 -> "Sixteenth"
|
||||||
|
17 -> "Seventeenth"
|
||||||
|
18 -> "Eighteenth"
|
||||||
|
19 -> "Nineteenth"
|
||||||
|
20 -> "Twentieth"
|
||||||
|
21 -> "Twenty-First"
|
||||||
|
22 -> "Twenty-Second"
|
||||||
|
23 -> "Twenty-Third"
|
||||||
|
24 -> "Twenty-Fourth"
|
||||||
|
25 -> "Twenty-Fifth"
|
||||||
|
26 -> "Twenty-Sixth"
|
||||||
|
27 -> "Twenty-Seventh"
|
||||||
|
28 -> "Twenty-Eighth"
|
||||||
|
29 -> "Twenty-Ninth"
|
||||||
|
30 -> "Thirtieth"
|
||||||
|
31 -> "Thirty-First"
|
||||||
|
32 -> "Thirty-Second"
|
||||||
|
33 -> "Thirty-Third"
|
||||||
|
34 -> "Thirty-Fourth"
|
||||||
|
35 -> "Thirty-Fifth"
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun fixUrl(url: String): String {
|
private fun fixUrl(url: String): String {
|
||||||
|
@ -44,47 +85,6 @@ class IndexSubtitleApi : AbstractSubApi {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getOrdinal(num: Int?): String? {
|
|
||||||
return when (num) {
|
|
||||||
1 -> "First"
|
|
||||||
2 -> "Second"
|
|
||||||
3 -> "Third"
|
|
||||||
4 -> "Fourth"
|
|
||||||
5 -> "Fifth"
|
|
||||||
6 -> "Sixth"
|
|
||||||
7 -> "Seventh"
|
|
||||||
8 -> "Eighth"
|
|
||||||
9 -> "Ninth"
|
|
||||||
10 -> "Tenth"
|
|
||||||
11 -> "Eleventh"
|
|
||||||
12 -> "Twelfth"
|
|
||||||
13 -> "Thirteenth"
|
|
||||||
14 -> "Fourteenth"
|
|
||||||
15 -> "Fifteenth"
|
|
||||||
16 -> "Sixteenth"
|
|
||||||
17 -> "Seventeenth"
|
|
||||||
18 -> "Eighteenth"
|
|
||||||
19 -> "Nineteenth"
|
|
||||||
20 -> "Twentieth"
|
|
||||||
21 -> "Twenty-First"
|
|
||||||
22 -> "Twenty-Second"
|
|
||||||
23 -> "Twenty-Third"
|
|
||||||
24 -> "Twenty-Fourth"
|
|
||||||
25 -> "Twenty-Fifth"
|
|
||||||
26 -> "Twenty-Sixth"
|
|
||||||
27 -> "Twenty-Seventh"
|
|
||||||
28 -> "Twenty-Eighth"
|
|
||||||
29 -> "Twenty-Ninth"
|
|
||||||
30 -> "Thirtieth"
|
|
||||||
31 -> "Thirty-First"
|
|
||||||
32 -> "Thirty-Second"
|
|
||||||
33 -> "Thirty-Third"
|
|
||||||
34 -> "Thirty-Fourth"
|
|
||||||
35 -> "Thirty-Fifth"
|
|
||||||
else -> null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun isRightEps(text: String, seasonNum: Int?, epNum: Int?): Boolean {
|
private fun isRightEps(text: String, seasonNum: Int?, epNum: Int?): Boolean {
|
||||||
val FILTER_EPS_REGEX =
|
val FILTER_EPS_REGEX =
|
||||||
Regex("(?i)((Chapter\\s?0?${epNum})|((Season)?\\s?0?${seasonNum}?\\s?(Episode)\\s?0?${epNum}[^0-9]))|(?i)((S?0?${seasonNum}?E0?${epNum}[^0-9])|(0?${seasonNum}[a-z]0?${epNum}[^0-9]))")
|
Regex("(?i)((Chapter\\s?0?${epNum})|((Season)?\\s?0?${seasonNum}?\\s?(Episode)\\s?0?${epNum}[^0-9]))|(?i)((S?0?${seasonNum}?E0?${epNum}[^0-9])|(0?${seasonNum}[a-z]0?${epNum}[^0-9]))")
|
||||||
|
|
|
@ -8,7 +8,9 @@ import com.lagradost.cloudstream3.syncproviders.SyncIdName
|
||||||
import com.lagradost.cloudstream3.ui.WatchType
|
import com.lagradost.cloudstream3.ui.WatchType
|
||||||
import com.lagradost.cloudstream3.ui.library.ListSorting
|
import com.lagradost.cloudstream3.ui.library.ListSorting
|
||||||
import com.lagradost.cloudstream3.ui.result.txt
|
import com.lagradost.cloudstream3.ui.result.txt
|
||||||
|
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTrueTvSettings
|
||||||
import com.lagradost.cloudstream3.utils.Coroutines.ioWork
|
import com.lagradost.cloudstream3.utils.Coroutines.ioWork
|
||||||
|
import com.lagradost.cloudstream3.utils.DataStoreHelper.getAllFavorites
|
||||||
import com.lagradost.cloudstream3.utils.DataStoreHelper.getAllSubscriptions
|
import com.lagradost.cloudstream3.utils.DataStoreHelper.getAllSubscriptions
|
||||||
import com.lagradost.cloudstream3.utils.DataStoreHelper.getAllWatchStateIds
|
import com.lagradost.cloudstream3.utils.DataStoreHelper.getAllWatchStateIds
|
||||||
import com.lagradost.cloudstream3.utils.DataStoreHelper.getBookmarkedData
|
import com.lagradost.cloudstream3.utils.DataStoreHelper.getBookmarkedData
|
||||||
|
@ -45,11 +47,11 @@ class LocalList : SyncAPI {
|
||||||
|
|
||||||
override val mainUrl = ""
|
override val mainUrl = ""
|
||||||
override val syncIdName = SyncIdName.LocalList
|
override val syncIdName = SyncIdName.LocalList
|
||||||
override suspend fun score(id: String, status: SyncAPI.SyncStatus): Boolean {
|
override suspend fun score(id: String, status: SyncAPI.AbstractSyncStatus): Boolean {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getStatus(id: String): SyncAPI.SyncStatus? {
|
override suspend fun getStatus(id: String): SyncAPI.AbstractSyncStatus? {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -69,29 +71,52 @@ class LocalList : SyncAPI {
|
||||||
}?.distinctBy { it.first } ?: return null
|
}?.distinctBy { it.first } ?: return null
|
||||||
|
|
||||||
val list = ioWork {
|
val list = ioWork {
|
||||||
watchStatusIds.groupBy {
|
val isTrueTv = isTrueTvSettings()
|
||||||
it.second.stringRes
|
|
||||||
}.mapValues { group ->
|
val baseMap = WatchType.values().filter { it != WatchType.NONE }.associate {
|
||||||
|
// None is not something to display
|
||||||
|
it.stringRes to emptyList<SyncAPI.LibraryItem>()
|
||||||
|
} + mapOf(
|
||||||
|
R.string.favorites_list_name to emptyList()
|
||||||
|
) + if (!isTrueTv) {
|
||||||
|
mapOf(
|
||||||
|
R.string.subscription_list_name to emptyList()
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
emptyMap()
|
||||||
|
}
|
||||||
|
|
||||||
|
val watchStatusMap = watchStatusIds.groupBy { it.second.stringRes }.mapValues { group ->
|
||||||
group.value.mapNotNull {
|
group.value.mapNotNull {
|
||||||
getBookmarkedData(it.first)?.toLibraryItem(it.first.toString())
|
getBookmarkedData(it.first)?.toLibraryItem(it.first.toString())
|
||||||
}
|
}
|
||||||
} + mapOf(R.string.subscription_list_name to getAllSubscriptions().mapNotNull {
|
}
|
||||||
|
|
||||||
|
val favoritesMap = mapOf(R.string.favorites_list_name to getAllFavorites().mapNotNull {
|
||||||
it.toLibraryItem()
|
it.toLibraryItem()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Don't show subscriptions on TV
|
||||||
|
val result = if (isTrueTv) {
|
||||||
|
baseMap + watchStatusMap + favoritesMap
|
||||||
|
} else {
|
||||||
|
val subscriptionsMap = mapOf(R.string.subscription_list_name to getAllSubscriptions().mapNotNull {
|
||||||
|
it.toLibraryItem()
|
||||||
|
})
|
||||||
|
|
||||||
|
baseMap + watchStatusMap + subscriptionsMap + favoritesMap
|
||||||
|
}
|
||||||
|
|
||||||
|
result
|
||||||
}
|
}
|
||||||
|
|
||||||
val baseMap = WatchType.values().filter { it != WatchType.NONE }.associate {
|
|
||||||
// None is not something to display
|
|
||||||
it.stringRes to emptyList<SyncAPI.LibraryItem>()
|
|
||||||
} + mapOf(R.string.subscription_list_name to emptyList())
|
|
||||||
|
|
||||||
return SyncAPI.LibraryMetadata(
|
return SyncAPI.LibraryMetadata(
|
||||||
(baseMap + list).map { SyncAPI.LibraryList(txt(it.key), it.value) },
|
list.map { SyncAPI.LibraryList(txt(it.key), it.value) },
|
||||||
setOf(
|
setOf(
|
||||||
ListSorting.AlphabeticalA,
|
ListSorting.AlphabeticalA,
|
||||||
ListSorting.AlphabeticalZ,
|
ListSorting.AlphabeticalZ,
|
||||||
// ListSorting.UpdatedNew,
|
ListSorting.UpdatedNew,
|
||||||
// ListSorting.UpdatedOld,
|
ListSorting.UpdatedOld,
|
||||||
// ListSorting.RatingHigh,
|
// ListSorting.RatingHigh,
|
||||||
// ListSorting.RatingLow,
|
// ListSorting.RatingLow,
|
||||||
)
|
)
|
||||||
|
|
|
@ -16,6 +16,7 @@ import com.lagradost.cloudstream3.syncproviders.AccountManager
|
||||||
import com.lagradost.cloudstream3.syncproviders.AuthAPI
|
import com.lagradost.cloudstream3.syncproviders.AuthAPI
|
||||||
import com.lagradost.cloudstream3.syncproviders.SyncAPI
|
import com.lagradost.cloudstream3.syncproviders.SyncAPI
|
||||||
import com.lagradost.cloudstream3.syncproviders.SyncIdName
|
import com.lagradost.cloudstream3.syncproviders.SyncIdName
|
||||||
|
import com.lagradost.cloudstream3.ui.SyncWatchType
|
||||||
import com.lagradost.cloudstream3.ui.library.ListSorting
|
import com.lagradost.cloudstream3.ui.library.ListSorting
|
||||||
import com.lagradost.cloudstream3.ui.result.txt
|
import com.lagradost.cloudstream3.ui.result.txt
|
||||||
import com.lagradost.cloudstream3.utils.AppUtils.parseJson
|
import com.lagradost.cloudstream3.utils.AppUtils.parseJson
|
||||||
|
@ -91,10 +92,10 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
return Regex("""/anime/((.*)/|(.*))""").find(url)!!.groupValues.first()
|
return Regex("""/anime/((.*)/|(.*))""").find(url)!!.groupValues.first()
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun score(id: String, status: SyncAPI.SyncStatus): Boolean {
|
override suspend fun score(id: String, status: SyncAPI.AbstractSyncStatus): Boolean {
|
||||||
return setScoreRequest(
|
return setScoreRequest(
|
||||||
id.toIntOrNull() ?: return false,
|
id.toIntOrNull() ?: return false,
|
||||||
fromIntToAnimeStatus(status.status),
|
fromIntToAnimeStatus(status.status.internalId),
|
||||||
status.score,
|
status.score,
|
||||||
status.watchedEpisodes
|
status.watchedEpisodes
|
||||||
).also {
|
).also {
|
||||||
|
@ -245,7 +246,7 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
getDataAboutMalId(internalId)?.my_list_status //?: throw ErrorLoadingException("No my_list_status")
|
getDataAboutMalId(internalId)?.my_list_status //?: throw ErrorLoadingException("No my_list_status")
|
||||||
return SyncAPI.SyncStatus(
|
return SyncAPI.SyncStatus(
|
||||||
score = data?.score,
|
score = data?.score,
|
||||||
status = malStatusAsString.indexOf(data?.status),
|
status = SyncWatchType.fromInternalId(malStatusAsString.indexOf(data?.status)) ,
|
||||||
isFavorite = null,
|
isFavorite = null,
|
||||||
watchedEpisodes = data?.num_episodes_watched,
|
watchedEpisodes = data?.num_episodes_watched,
|
||||||
)
|
)
|
||||||
|
@ -442,6 +443,7 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
this.node.main_picture?.large ?: this.node.main_picture?.medium,
|
this.node.main_picture?.large ?: this.node.main_picture?.medium,
|
||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
|
plot = this.node.synopsis,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,12 +2,13 @@ package com.lagradost.cloudstream3.syncproviders.providers
|
||||||
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import com.fasterxml.jackson.annotation.JsonProperty
|
import com.fasterxml.jackson.annotation.JsonProperty
|
||||||
import com.google.common.collect.BiMap
|
|
||||||
import com.google.common.collect.HashBiMap
|
|
||||||
import com.lagradost.cloudstream3.*
|
|
||||||
import com.lagradost.cloudstream3.AcraApplication.Companion.getKey
|
import com.lagradost.cloudstream3.AcraApplication.Companion.getKey
|
||||||
import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey
|
import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey
|
||||||
import com.lagradost.cloudstream3.AcraApplication.Companion.setKey
|
import com.lagradost.cloudstream3.AcraApplication.Companion.setKey
|
||||||
|
import com.lagradost.cloudstream3.ErrorLoadingException
|
||||||
|
import com.lagradost.cloudstream3.R
|
||||||
|
import com.lagradost.cloudstream3.TvType
|
||||||
|
import com.lagradost.cloudstream3.app
|
||||||
import com.lagradost.cloudstream3.mvvm.logError
|
import com.lagradost.cloudstream3.mvvm.logError
|
||||||
import com.lagradost.cloudstream3.subtitles.AbstractSubApi
|
import com.lagradost.cloudstream3.subtitles.AbstractSubApi
|
||||||
import com.lagradost.cloudstream3.subtitles.AbstractSubtitleEntities
|
import com.lagradost.cloudstream3.subtitles.AbstractSubtitleEntities
|
||||||
|
@ -15,8 +16,8 @@ import com.lagradost.cloudstream3.syncproviders.AuthAPI
|
||||||
import com.lagradost.cloudstream3.syncproviders.InAppAuthAPI
|
import com.lagradost.cloudstream3.syncproviders.InAppAuthAPI
|
||||||
import com.lagradost.cloudstream3.syncproviders.InAppAuthAPIManager
|
import com.lagradost.cloudstream3.syncproviders.InAppAuthAPIManager
|
||||||
import com.lagradost.cloudstream3.utils.AppUtils
|
import com.lagradost.cloudstream3.utils.AppUtils
|
||||||
import java.net.URLEncoder
|
import okhttp3.Interceptor
|
||||||
import java.nio.charset.StandardCharsets
|
import okhttp3.Response
|
||||||
|
|
||||||
class OpenSubtitlesApi(index: Int) : InAppAuthAPIManager(index), AbstractSubApi {
|
class OpenSubtitlesApi(index: Int) : InAppAuthAPIManager(index), AbstractSubApi {
|
||||||
override val idPrefix = "opensubtitles"
|
override val idPrefix = "opensubtitles"
|
||||||
|
@ -36,6 +37,23 @@ class OpenSubtitlesApi(index: Int) : InAppAuthAPIManager(index), AbstractSubApi
|
||||||
var currentSession: SubtitleOAuthEntity? = null
|
var currentSession: SubtitleOAuthEntity? = null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val headerInterceptor = OpenSubtitleInterceptor()
|
||||||
|
|
||||||
|
/** Automatically adds required api headers */
|
||||||
|
private class OpenSubtitleInterceptor : Interceptor {
|
||||||
|
/** Required user agent! */
|
||||||
|
private val userAgent = "Cloudstream3 v0.1"
|
||||||
|
override fun intercept(chain: Interceptor.Chain): Response {
|
||||||
|
return chain.proceed(
|
||||||
|
chain.request().newBuilder()
|
||||||
|
.removeHeader("user-agent")
|
||||||
|
.addHeader("user-agent", userAgent)
|
||||||
|
.addHeader("Api-Key", apiKey)
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun canDoRequest(): Boolean {
|
private fun canDoRequest(): Boolean {
|
||||||
return unixTimeMs > currentCoolDown
|
return unixTimeMs > currentCoolDown
|
||||||
}
|
}
|
||||||
|
@ -98,13 +116,13 @@ class OpenSubtitlesApi(index: Int) : InAppAuthAPIManager(index), AbstractSubApi
|
||||||
val response = app.post(
|
val response = app.post(
|
||||||
url = "$host/login",
|
url = "$host/login",
|
||||||
headers = mapOf(
|
headers = mapOf(
|
||||||
"Api-Key" to apiKey,
|
"Content-Type" to "application/json",
|
||||||
"Content-Type" to "application/json"
|
|
||||||
),
|
),
|
||||||
data = mapOf(
|
data = mapOf(
|
||||||
"username" to username,
|
"username" to username,
|
||||||
"password" to password
|
"password" to password
|
||||||
)
|
),
|
||||||
|
interceptor = headerInterceptor
|
||||||
)
|
)
|
||||||
//Log.i(TAG, "Responsecode = ${response.code}")
|
//Log.i(TAG, "Responsecode = ${response.code}")
|
||||||
//Log.i(TAG, "Result => ${response.text}")
|
//Log.i(TAG, "Result => ${response.text}")
|
||||||
|
@ -149,11 +167,13 @@ class OpenSubtitlesApi(index: Int) : InAppAuthAPIManager(index), AbstractSubApi
|
||||||
// "pt" to "pt-PT",
|
// "pt" to "pt-PT",
|
||||||
// "pt" to "pt-BR"
|
// "pt" to "pt-BR"
|
||||||
)
|
)
|
||||||
private fun fixLanguage(language: String?) : String? {
|
|
||||||
|
private fun fixLanguage(language: String?): String? {
|
||||||
return languageExceptions[language] ?: language
|
return languageExceptions[language] ?: language
|
||||||
}
|
}
|
||||||
|
|
||||||
// O(n) but good enough, BiMap did not want to work properly
|
// O(n) but good enough, BiMap did not want to work properly
|
||||||
private fun fixLanguageReverse(language: String?) : String? {
|
private fun fixLanguageReverse(language: String?): String? {
|
||||||
return languageExceptions.entries.firstOrNull { it.value == language }?.key ?: language
|
return languageExceptions.entries.firstOrNull { it.value == language }?.key ?: language
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -183,9 +203,9 @@ class OpenSubtitlesApi(index: Int) : InAppAuthAPIManager(index), AbstractSubApi
|
||||||
val req = app.get(
|
val req = app.get(
|
||||||
url = searchQueryUrl,
|
url = searchQueryUrl,
|
||||||
headers = mapOf(
|
headers = mapOf(
|
||||||
Pair("Api-Key", apiKey),
|
|
||||||
Pair("Content-Type", "application/json")
|
Pair("Content-Type", "application/json")
|
||||||
)
|
),
|
||||||
|
interceptor = headerInterceptor
|
||||||
)
|
)
|
||||||
Log.i(TAG, "Search Req => ${req.text}")
|
Log.i(TAG, "Search Req => ${req.text}")
|
||||||
if (!req.isSuccessful) {
|
if (!req.isSuccessful) {
|
||||||
|
@ -207,7 +227,7 @@ class OpenSubtitlesApi(index: Int) : InAppAuthAPIManager(index), AbstractSubApi
|
||||||
//Use any valid name/title in hierarchy
|
//Use any valid name/title in hierarchy
|
||||||
val name = filename ?: featureDetails?.movieName ?: featureDetails?.title
|
val name = filename ?: featureDetails?.movieName ?: featureDetails?.title
|
||||||
?: featureDetails?.parentTitle ?: attr.release ?: query.query
|
?: featureDetails?.parentTitle ?: attr.release ?: query.query
|
||||||
val lang = fixLanguageReverse(attr.language)?: ""
|
val lang = fixLanguageReverse(attr.language) ?: ""
|
||||||
val resEpNum = featureDetails?.episodeNumber ?: query.epNumber
|
val resEpNum = featureDetails?.episodeNumber ?: query.epNumber
|
||||||
val resSeasonNum = featureDetails?.seasonNumber ?: query.seasonNumber
|
val resSeasonNum = featureDetails?.seasonNumber ?: query.seasonNumber
|
||||||
val year = featureDetails?.year ?: query.year
|
val year = featureDetails?.year ?: query.year
|
||||||
|
@ -251,13 +271,13 @@ class OpenSubtitlesApi(index: Int) : InAppAuthAPIManager(index), AbstractSubApi
|
||||||
"Authorization",
|
"Authorization",
|
||||||
"Bearer ${currentSession?.access_token ?: throw ErrorLoadingException("No access token active in current session")}"
|
"Bearer ${currentSession?.access_token ?: throw ErrorLoadingException("No access token active in current session")}"
|
||||||
),
|
),
|
||||||
Pair("Api-Key", apiKey),
|
|
||||||
Pair("Content-Type", "application/json"),
|
Pair("Content-Type", "application/json"),
|
||||||
Pair("Accept", "*/*")
|
Pair("Accept", "*/*")
|
||||||
),
|
),
|
||||||
data = mapOf(
|
data = mapOf(
|
||||||
Pair("file_id", data.data)
|
Pair("file_id", data.data)
|
||||||
)
|
),
|
||||||
|
interceptor = headerInterceptor
|
||||||
)
|
)
|
||||||
Log.i(TAG, "Request result => (${req.code}) ${req.text}")
|
Log.i(TAG, "Request result => (${req.code}) ${req.text}")
|
||||||
//Log.i(TAG, "Request headers => ${req.headers}")
|
//Log.i(TAG, "Request headers => ${req.headers}")
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,118 @@
|
||||||
|
package com.lagradost.cloudstream3.syncproviders.providers
|
||||||
|
|
||||||
|
import com.lagradost.cloudstream3.app
|
||||||
|
import com.lagradost.cloudstream3.mvvm.debugPrint
|
||||||
|
import com.lagradost.cloudstream3.subtitles.AbstractSubProvider
|
||||||
|
import com.lagradost.cloudstream3.subtitles.AbstractSubtitleEntities
|
||||||
|
import com.lagradost.cloudstream3.subtitles.SubtitleResource
|
||||||
|
import com.lagradost.cloudstream3.syncproviders.providers.IndexSubtitleApi.Companion.getOrdinal
|
||||||
|
import com.lagradost.cloudstream3.utils.SubtitleHelper
|
||||||
|
|
||||||
|
class SubScene : AbstractSubProvider {
|
||||||
|
val mainUrl = "https://subscene.com"
|
||||||
|
val name = "Subscene"
|
||||||
|
override val idPrefix = "subscene"
|
||||||
|
|
||||||
|
override suspend fun search(query: AbstractSubtitleEntities.SubtitleSearch): List<AbstractSubtitleEntities.SubtitleEntity>? {
|
||||||
|
val seasonName =
|
||||||
|
query.seasonNumber?.let { number ->
|
||||||
|
// Need to translate "7" to "Seventh Season"
|
||||||
|
getOrdinal(number)?.let { words -> " - $words Season" }
|
||||||
|
} ?: ""
|
||||||
|
|
||||||
|
val fullQuery = query.query + seasonName
|
||||||
|
|
||||||
|
val doc = app.post(
|
||||||
|
"$mainUrl/subtitles/searchbytitle",
|
||||||
|
data = mapOf("query" to fullQuery, "l" to "")
|
||||||
|
).document
|
||||||
|
|
||||||
|
return doc.select("div.title a").map { element ->
|
||||||
|
val href = "$mainUrl${element.attr("href")}"
|
||||||
|
val title = element.text()
|
||||||
|
|
||||||
|
AbstractSubtitleEntities.SubtitleEntity(
|
||||||
|
idPrefix = idPrefix,
|
||||||
|
name = title,
|
||||||
|
source = name,
|
||||||
|
data = href,
|
||||||
|
lang = query.lang ?: "en",
|
||||||
|
epNumber = query.epNumber
|
||||||
|
)
|
||||||
|
}.distinctBy { it.data }
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun SubtitleResource.getResources(data: AbstractSubtitleEntities.SubtitleEntity) {
|
||||||
|
val resultDoc = app.get(data.data).document
|
||||||
|
val queryLanguage = SubtitleHelper.fromTwoLettersToLanguage(data.lang) ?: "English"
|
||||||
|
|
||||||
|
val results = resultDoc.select("table tbody tr").mapNotNull { element ->
|
||||||
|
val anchor = element.select("a")
|
||||||
|
val href = anchor.attr("href") ?: return@mapNotNull null
|
||||||
|
val fixedHref = "$mainUrl${href}"
|
||||||
|
val spans = anchor.select("span")
|
||||||
|
val language = spans.firstOrNull()?.text()
|
||||||
|
val title = spans.getOrNull(1)?.text()
|
||||||
|
val isPositive = anchor.select("span.positive-icon").isNotEmpty()
|
||||||
|
|
||||||
|
TableElement(title, language, fixedHref, isPositive)
|
||||||
|
}.sortedBy {
|
||||||
|
it.getScore(queryLanguage, data.epNumber)
|
||||||
|
}
|
||||||
|
|
||||||
|
debugPrint { "$name found subtitles: ${results.takeLast(3)}" }
|
||||||
|
// Last = highest score
|
||||||
|
val selectedResult = results.lastOrNull() ?: return
|
||||||
|
|
||||||
|
val subtitleDocument = app.get(selectedResult.href).document
|
||||||
|
val subtitleDownloadUrl =
|
||||||
|
"$mainUrl${subtitleDocument.select("div.download a").attr("href")}"
|
||||||
|
|
||||||
|
this.addZipUrl(subtitleDownloadUrl) { name, _ ->
|
||||||
|
name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class to manage the various different subtitle results and rank them.
|
||||||
|
*/
|
||||||
|
data class TableElement(
|
||||||
|
val title: String?,
|
||||||
|
val language: String?,
|
||||||
|
val href: String,
|
||||||
|
val isPositive: Boolean
|
||||||
|
) {
|
||||||
|
private fun matchesLanguage(other: String): Boolean {
|
||||||
|
return language != null && (language.contains(other, ignoreCase = true) ||
|
||||||
|
other.contains(language, ignoreCase = true))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scores in this order:
|
||||||
|
* Preferred Language > Episode number > Positive rating > English Language
|
||||||
|
*/
|
||||||
|
fun getScore(queryLanguage: String, episodeNum: Int?): Int {
|
||||||
|
var score = 0
|
||||||
|
if (this.matchesLanguage(queryLanguage)) {
|
||||||
|
score += 8
|
||||||
|
}
|
||||||
|
// Matches Episode 7 using "E07" with any number of leading zeroes
|
||||||
|
if (episodeNum != null && title != null && title.contains(
|
||||||
|
Regex(
|
||||||
|
"""E0*${episodeNum}""",
|
||||||
|
RegexOption.IGNORE_CASE
|
||||||
|
)
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
score += 4
|
||||||
|
}
|
||||||
|
if (isPositive) {
|
||||||
|
score += 2
|
||||||
|
}
|
||||||
|
if (this.matchesLanguage("English")) {
|
||||||
|
score += 1
|
||||||
|
}
|
||||||
|
return score
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,16 +1,24 @@
|
||||||
package com.lagradost.cloudstream3.ui
|
package com.lagradost.cloudstream3.ui
|
||||||
|
|
||||||
import com.lagradost.cloudstream3.*
|
|
||||||
import com.lagradost.cloudstream3.APIHolder.unixTime
|
import com.lagradost.cloudstream3.APIHolder.unixTime
|
||||||
import com.lagradost.cloudstream3.APIHolder.unixTimeMS
|
import com.lagradost.cloudstream3.APIHolder.unixTimeMS
|
||||||
|
import com.lagradost.cloudstream3.DubStatus
|
||||||
|
import com.lagradost.cloudstream3.ErrorLoadingException
|
||||||
|
import com.lagradost.cloudstream3.HomePageResponse
|
||||||
|
import com.lagradost.cloudstream3.LoadResponse
|
||||||
|
import com.lagradost.cloudstream3.MainAPI
|
||||||
import com.lagradost.cloudstream3.MainActivity.Companion.afterPluginsLoadedEvent
|
import com.lagradost.cloudstream3.MainActivity.Companion.afterPluginsLoadedEvent
|
||||||
|
import com.lagradost.cloudstream3.MainPageRequest
|
||||||
|
import com.lagradost.cloudstream3.SearchResponse
|
||||||
|
import com.lagradost.cloudstream3.SubtitleFile
|
||||||
|
import com.lagradost.cloudstream3.TvType
|
||||||
|
import com.lagradost.cloudstream3.fixUrl
|
||||||
import com.lagradost.cloudstream3.mvvm.Resource
|
import com.lagradost.cloudstream3.mvvm.Resource
|
||||||
import com.lagradost.cloudstream3.mvvm.logError
|
import com.lagradost.cloudstream3.mvvm.logError
|
||||||
import com.lagradost.cloudstream3.mvvm.safeApiCall
|
import com.lagradost.cloudstream3.mvvm.safeApiCall
|
||||||
import com.lagradost.cloudstream3.utils.Coroutines.threadSafeListOf
|
import com.lagradost.cloudstream3.utils.Coroutines.threadSafeListOf
|
||||||
import com.lagradost.cloudstream3.utils.ExtractorLink
|
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
|
||||||
import kotlinx.coroutines.GlobalScope.coroutineContext
|
import kotlinx.coroutines.GlobalScope.coroutineContext
|
||||||
import kotlinx.coroutines.async
|
import kotlinx.coroutines.async
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
|
@ -174,7 +182,7 @@ class APIRepository(val api: MainAPI) {
|
||||||
data: String,
|
data: String,
|
||||||
isCasting: Boolean,
|
isCasting: Boolean,
|
||||||
subtitleCallback: (SubtitleFile) -> Unit,
|
subtitleCallback: (SubtitleFile) -> Unit,
|
||||||
callback: (ExtractorLink) -> Unit
|
callback: (ExtractorLink) -> Unit,
|
||||||
): Boolean {
|
): Boolean {
|
||||||
if (isInvalidData(data)) return false // this makes providers cleaner
|
if (isInvalidData(data)) return false // this makes providers cleaner
|
||||||
return try {
|
return try {
|
||||||
|
|
|
@ -8,7 +8,7 @@ import android.widget.*
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
import com.fasterxml.jackson.databind.DeserializationFeature
|
import com.fasterxml.jackson.databind.DeserializationFeature
|
||||||
import com.fasterxml.jackson.databind.json.JsonMapper
|
import com.fasterxml.jackson.databind.json.JsonMapper
|
||||||
import com.fasterxml.jackson.module.kotlin.KotlinModule
|
import com.fasterxml.jackson.module.kotlin.kotlinModule
|
||||||
import com.google.android.gms.cast.MediaQueueItem
|
import com.google.android.gms.cast.MediaQueueItem
|
||||||
import com.google.android.gms.cast.MediaSeekOptions
|
import com.google.android.gms.cast.MediaSeekOptions
|
||||||
import com.google.android.gms.cast.MediaStatus.REPEAT_MODE_REPEAT_OFF
|
import com.google.android.gms.cast.MediaStatus.REPEAT_MODE_REPEAT_OFF
|
||||||
|
@ -25,6 +25,7 @@ import com.lagradost.cloudstream3.mvvm.logError
|
||||||
import com.lagradost.cloudstream3.mvvm.safeApiCall
|
import com.lagradost.cloudstream3.mvvm.safeApiCall
|
||||||
import com.lagradost.cloudstream3.sortSubs
|
import com.lagradost.cloudstream3.sortSubs
|
||||||
import com.lagradost.cloudstream3.sortUrls
|
import com.lagradost.cloudstream3.sortUrls
|
||||||
|
import com.lagradost.cloudstream3.ui.player.LoadType
|
||||||
import com.lagradost.cloudstream3.ui.player.RepoLinkGenerator
|
import com.lagradost.cloudstream3.ui.player.RepoLinkGenerator
|
||||||
import com.lagradost.cloudstream3.ui.player.SubtitleData
|
import com.lagradost.cloudstream3.ui.player.SubtitleData
|
||||||
import com.lagradost.cloudstream3.ui.result.ResultEpisode
|
import com.lagradost.cloudstream3.ui.result.ResultEpisode
|
||||||
|
@ -97,7 +98,7 @@ data class MetadataHolder(
|
||||||
|
|
||||||
class SelectSourceController(val view: ImageView, val activity: ControllerActivity) :
|
class SelectSourceController(val view: ImageView, val activity: ControllerActivity) :
|
||||||
UIController() {
|
UIController() {
|
||||||
private val mapper: JsonMapper = JsonMapper.builder().addModule(KotlinModule())
|
private val mapper: JsonMapper = JsonMapper.builder().addModule(kotlinModule())
|
||||||
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false).build()
|
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false).build()
|
||||||
|
|
||||||
init {
|
init {
|
||||||
|
@ -294,7 +295,8 @@ class SelectSourceController(val view: ImageView, val activity: ControllerActivi
|
||||||
val generator = RepoLinkGenerator(listOf(epData))
|
val generator = RepoLinkGenerator(listOf(epData))
|
||||||
|
|
||||||
val isSuccessful = safeApiCall {
|
val isSuccessful = safeApiCall {
|
||||||
generator.generateLinks(clearCache = false, isCasting = true,
|
generator.generateLinks(
|
||||||
|
clearCache = false, type = LoadType.Chromecast,
|
||||||
callback = {
|
callback = {
|
||||||
it.first?.let { link ->
|
it.first?.let { link ->
|
||||||
currentLinks.add(link)
|
currentLinks.add(link)
|
||||||
|
|
|
@ -3,6 +3,7 @@ package com.lagradost.cloudstream3.ui
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.util.AttributeSet
|
import android.util.AttributeSet
|
||||||
import android.view.View
|
import android.view.View
|
||||||
|
import androidx.core.view.children
|
||||||
import androidx.recyclerview.widget.GridLayoutManager
|
import androidx.recyclerview.widget.GridLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import kotlin.math.abs
|
import kotlin.math.abs
|
||||||
|
@ -24,7 +25,7 @@ class GrdLayoutManager(val context: Context, _spanCount: Int) :
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onRequestChildFocus(
|
/*override fun onRequestChildFocus(
|
||||||
parent: RecyclerView,
|
parent: RecyclerView,
|
||||||
state: RecyclerView.State,
|
state: RecyclerView.State,
|
||||||
child: View,
|
child: View,
|
||||||
|
@ -32,13 +33,17 @@ class GrdLayoutManager(val context: Context, _spanCount: Int) :
|
||||||
): Boolean {
|
): Boolean {
|
||||||
// android.widget.FrameLayout$LayoutParams cannot be cast to androidx.recyclerview.widget.RecyclerView$LayoutParams
|
// android.widget.FrameLayout$LayoutParams cannot be cast to androidx.recyclerview.widget.RecyclerView$LayoutParams
|
||||||
return try {
|
return try {
|
||||||
val pos = maxOf(0, getPosition(focused!!) - 2)
|
if(focused != null) {
|
||||||
parent.scrollToPosition(pos)
|
// val pos = maxOf(0, getPosition(focused) - 2) // IDK WHY
|
||||||
|
val pos = getPosition(focused)
|
||||||
|
if(pos >= 0) parent.scrollToPosition(pos)
|
||||||
|
}
|
||||||
|
|
||||||
super.onRequestChildFocus(parent, state, child, focused)
|
super.onRequestChildFocus(parent, state, child, focused)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
}
|
}*/
|
||||||
|
|
||||||
// Allows moving right and left with focus https://gist.github.com/vganin/8930b41f55820ec49e4d
|
// Allows moving right and left with focus https://gist.github.com/vganin/8930b41f55820ec49e4d
|
||||||
override fun onInterceptFocusSearch(focused: View, direction: Int): View? {
|
override fun onInterceptFocusSearch(focused: View, direction: Int): View? {
|
||||||
|
@ -65,32 +70,47 @@ class GrdLayoutManager(val context: Context, _spanCount: Int) :
|
||||||
val spanCount = this.spanCount
|
val spanCount = this.spanCount
|
||||||
val orientation = this.orientation
|
val orientation = this.orientation
|
||||||
|
|
||||||
if (orientation == VERTICAL) {
|
// fixes arabic by inverting left and right layout focus
|
||||||
|
val correctDirection = if (this.isLayoutRTL) {
|
||||||
when (direction) {
|
when (direction) {
|
||||||
|
View.FOCUS_RIGHT -> View.FOCUS_LEFT
|
||||||
|
View.FOCUS_LEFT -> View.FOCUS_RIGHT
|
||||||
|
else -> direction
|
||||||
|
}
|
||||||
|
} else direction
|
||||||
|
|
||||||
|
if (orientation == VERTICAL) {
|
||||||
|
when (correctDirection) {
|
||||||
View.FOCUS_DOWN -> {
|
View.FOCUS_DOWN -> {
|
||||||
return spanCount
|
return spanCount
|
||||||
}
|
}
|
||||||
|
|
||||||
View.FOCUS_UP -> {
|
View.FOCUS_UP -> {
|
||||||
return -spanCount
|
return -spanCount
|
||||||
}
|
}
|
||||||
|
|
||||||
View.FOCUS_RIGHT -> {
|
View.FOCUS_RIGHT -> {
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
View.FOCUS_LEFT -> {
|
View.FOCUS_LEFT -> {
|
||||||
return -1
|
return -1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (orientation == HORIZONTAL) {
|
} else if (orientation == HORIZONTAL) {
|
||||||
when (direction) {
|
when (correctDirection) {
|
||||||
View.FOCUS_DOWN -> {
|
View.FOCUS_DOWN -> {
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
View.FOCUS_UP -> {
|
View.FOCUS_UP -> {
|
||||||
return -1
|
return -1
|
||||||
}
|
}
|
||||||
|
|
||||||
View.FOCUS_RIGHT -> {
|
View.FOCUS_RIGHT -> {
|
||||||
return spanCount
|
return spanCount
|
||||||
}
|
}
|
||||||
|
|
||||||
View.FOCUS_LEFT -> {
|
View.FOCUS_LEFT -> {
|
||||||
return -spanCount
|
return -spanCount
|
||||||
}
|
}
|
||||||
|
@ -142,4 +162,32 @@ class AutofitRecyclerView @JvmOverloads constructor(context: Context, attrs: Att
|
||||||
|
|
||||||
layoutManager = manager
|
layoutManager = manager
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Recyclerview wherein the max item width or height is set by the biggest view to prevent inconsistent view sizes.
|
||||||
|
*/
|
||||||
|
class MaxRecyclerView(ctx: Context, attrs: AttributeSet) : RecyclerView(ctx, attrs) {
|
||||||
|
private var biggestObserved: Int = 0
|
||||||
|
private val orientation = LayoutManager.getProperties(context, attrs, 0, 0).orientation
|
||||||
|
private val isHorizontal = orientation == HORIZONTAL
|
||||||
|
private fun View.updateMaxSize() {
|
||||||
|
if (isHorizontal) {
|
||||||
|
this.minimumHeight = biggestObserved
|
||||||
|
} else {
|
||||||
|
this.minimumWidth = biggestObserved
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onChildAttachedToWindow(child: View) {
|
||||||
|
child.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED)
|
||||||
|
val observed = if (isHorizontal) child.measuredHeight else child.measuredWidth
|
||||||
|
if (observed > biggestObserved) {
|
||||||
|
biggestObserved = observed
|
||||||
|
children.forEach { it.updateMaxSize() }
|
||||||
|
} else {
|
||||||
|
child.updateMaxSize()
|
||||||
|
}
|
||||||
|
super.onChildAttachedToWindow(child)
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -16,14 +16,16 @@ import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.appcompat.widget.AppCompatImageView
|
import androidx.appcompat.widget.AppCompatImageView
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import com.lagradost.cloudstream3.R
|
import com.lagradost.cloudstream3.R
|
||||||
import kotlinx.android.synthetic.main.activity_easter_egg_monke.*
|
import com.lagradost.cloudstream3.databinding.ActivityEasterEggMonkeBinding
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
class EasterEggMonke : AppCompatActivity() {
|
class EasterEggMonke : AppCompatActivity() {
|
||||||
|
|
||||||
|
lateinit var binding : ActivityEasterEggMonkeBinding
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
setContentView(R.layout.activity_easter_egg_monke)
|
|
||||||
|
binding = ActivityEasterEggMonkeBinding.inflate(layoutInflater)
|
||||||
|
setContentView(binding.root)
|
||||||
|
|
||||||
val handler = Handler(mainLooper)
|
val handler = Handler(mainLooper)
|
||||||
lateinit var runnable: Runnable
|
lateinit var runnable: Runnable
|
||||||
|
@ -32,15 +34,14 @@ class EasterEggMonke : AppCompatActivity() {
|
||||||
handler.postDelayed(runnable, 300)
|
handler.postDelayed(runnable, 300)
|
||||||
}
|
}
|
||||||
handler.postDelayed(runnable, 1000)
|
handler.postDelayed(runnable, 1000)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun shower() {
|
private fun shower() {
|
||||||
|
|
||||||
val containerW = frame.width
|
val containerW = binding.frame.width
|
||||||
val containerH = frame.height
|
val containerH = binding.frame.height
|
||||||
var starW: Float = monke.width.toFloat()
|
var starW: Float = binding.monke.width.toFloat()
|
||||||
var starH: Float = monke.height.toFloat()
|
var starH: Float = binding.monke.height.toFloat()
|
||||||
|
|
||||||
val newStar = AppCompatImageView(this)
|
val newStar = AppCompatImageView(this)
|
||||||
val idx = (monkeys.size * Math.random()).toInt()
|
val idx = (monkeys.size * Math.random()).toInt()
|
||||||
|
@ -48,7 +49,7 @@ class EasterEggMonke : AppCompatActivity() {
|
||||||
newStar.isVisible = true
|
newStar.isVisible = true
|
||||||
newStar.layoutParams = FrameLayout.LayoutParams(FrameLayout.LayoutParams.WRAP_CONTENT,
|
newStar.layoutParams = FrameLayout.LayoutParams(FrameLayout.LayoutParams.WRAP_CONTENT,
|
||||||
FrameLayout.LayoutParams.WRAP_CONTENT)
|
FrameLayout.LayoutParams.WRAP_CONTENT)
|
||||||
frame.addView(newStar)
|
binding.frame.addView(newStar)
|
||||||
|
|
||||||
newStar.scaleX = Math.random().toFloat() * 1.5f + newStar.scaleX
|
newStar.scaleX = Math.random().toFloat() * 1.5f + newStar.scaleX
|
||||||
newStar.scaleY = newStar.scaleX
|
newStar.scaleY = newStar.scaleX
|
||||||
|
@ -70,7 +71,7 @@ class EasterEggMonke : AppCompatActivity() {
|
||||||
|
|
||||||
set.addListener(object : AnimatorListenerAdapter() {
|
set.addListener(object : AnimatorListenerAdapter() {
|
||||||
override fun onAnimationEnd(animation: Animator) {
|
override fun onAnimationEnd(animation: Animator) {
|
||||||
frame.removeView(newStar)
|
binding.frame.removeView(newStar)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -15,4 +15,27 @@ enum class WatchType(val internalId: Int, @StringRes val stringRes: Int, @Drawab
|
||||||
companion object {
|
companion object {
|
||||||
fun fromInternalId(id: Int?) = values().find { value -> value.internalId == id } ?: NONE
|
fun fromInternalId(id: Int?) = values().find { value -> value.internalId == id } ?: NONE
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum class SyncWatchType(val internalId: Int, @StringRes val stringRes: Int, @DrawableRes val iconRes: Int) {
|
||||||
|
/*
|
||||||
|
-1 -> None
|
||||||
|
0 -> Watching
|
||||||
|
1 -> Completed
|
||||||
|
2 -> OnHold
|
||||||
|
3 -> Dropped
|
||||||
|
4 -> PlanToWatch
|
||||||
|
5 -> ReWatching
|
||||||
|
*/
|
||||||
|
NONE(-1, R.string.type_none, R.drawable.ic_baseline_add_24),
|
||||||
|
WATCHING(0, R.string.type_watching, R.drawable.ic_baseline_bookmark_24),
|
||||||
|
COMPLETED(1, R.string.type_completed, R.drawable.ic_baseline_bookmark_24),
|
||||||
|
ONHOLD(2, R.string.type_on_hold, R.drawable.ic_baseline_bookmark_24),
|
||||||
|
DROPPED(3, R.string.type_dropped, R.drawable.ic_baseline_bookmark_24),
|
||||||
|
PLANTOWATCH(4, R.string.type_plan_to_watch, R.drawable.ic_baseline_bookmark_24),
|
||||||
|
REWATCHING(5, R.string.type_re_watching, R.drawable.ic_baseline_bookmark_24);
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun fromInternalId(id: Int?) = values().find { value -> value.internalId == id } ?: NONE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -12,20 +12,23 @@ import androidx.fragment.app.Fragment
|
||||||
import androidx.fragment.app.FragmentActivity
|
import androidx.fragment.app.FragmentActivity
|
||||||
import androidx.navigation.fragment.findNavController
|
import androidx.navigation.fragment.findNavController
|
||||||
import com.lagradost.cloudstream3.MainActivity
|
import com.lagradost.cloudstream3.MainActivity
|
||||||
import com.lagradost.cloudstream3.R
|
|
||||||
import com.lagradost.cloudstream3.USER_AGENT
|
import com.lagradost.cloudstream3.USER_AGENT
|
||||||
|
import com.lagradost.cloudstream3.databinding.FragmentWebviewBinding
|
||||||
import com.lagradost.cloudstream3.network.WebViewResolver
|
import com.lagradost.cloudstream3.network.WebViewResolver
|
||||||
import com.lagradost.cloudstream3.utils.AppUtils.loadRepository
|
import com.lagradost.cloudstream3.utils.AppUtils.loadRepository
|
||||||
import kotlinx.android.synthetic.main.fragment_webview.*
|
|
||||||
|
|
||||||
class WebviewFragment : Fragment() {
|
class WebviewFragment : Fragment() {
|
||||||
|
|
||||||
|
var binding: FragmentWebviewBinding? = null
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
val url = arguments?.getString(WEBVIEW_URL) ?: "".also {
|
val url = arguments?.getString(WEBVIEW_URL) ?: "".also {
|
||||||
findNavController().popBackStack()
|
findNavController().popBackStack()
|
||||||
}
|
}
|
||||||
|
|
||||||
web_view.webViewClient = object : WebViewClient() {
|
binding?.webView?.webViewClient = object : WebViewClient() {
|
||||||
override fun shouldOverrideUrlLoading(
|
override fun shouldOverrideUrlLoading(
|
||||||
view: WebView?,
|
view: WebView?,
|
||||||
request: WebResourceRequest?
|
request: WebResourceRequest?
|
||||||
|
@ -40,24 +43,28 @@ class WebviewFragment : Fragment() {
|
||||||
return super.shouldOverrideUrlLoading(view, request)
|
return super.shouldOverrideUrlLoading(view, request)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
binding?.webView?.apply {
|
||||||
|
WebViewResolver.webViewUserAgent = settings.userAgentString
|
||||||
|
|
||||||
WebViewResolver.webViewUserAgent = web_view.settings.userAgentString
|
addJavascriptInterface(RepoApi(activity), "RepoApi")
|
||||||
|
settings.javaScriptEnabled = true
|
||||||
web_view.addJavascriptInterface(RepoApi(activity), "RepoApi")
|
settings.userAgentString = USER_AGENT
|
||||||
web_view.settings.javaScriptEnabled = true
|
settings.domStorageEnabled = true
|
||||||
web_view.settings.userAgentString = USER_AGENT
|
|
||||||
web_view.settings.domStorageEnabled = true
|
|
||||||
// WebView.setWebContentsDebuggingEnabled(true)
|
// WebView.setWebContentsDebuggingEnabled(true)
|
||||||
|
|
||||||
web_view.loadUrl(url)
|
loadUrl(url)
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateView(
|
override fun onCreateView(
|
||||||
inflater: LayoutInflater, container: ViewGroup?,
|
inflater: LayoutInflater, container: ViewGroup?,
|
||||||
savedInstanceState: Bundle?
|
savedInstanceState: Bundle?
|
||||||
): View? {
|
): View {
|
||||||
|
val localBinding = FragmentWebviewBinding.inflate(inflater, container, false)
|
||||||
|
binding = localBinding
|
||||||
// Inflate the layout for this fragment
|
// Inflate the layout for this fragment
|
||||||
return inflater.inflate(R.layout.fragment_webview, container, false)
|
return localBinding.root//inflater.inflate(R.layout.fragment_webview, container, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
@ -70,7 +77,7 @@ class WebviewFragment : Fragment() {
|
||||||
|
|
||||||
private class RepoApi(val activity: FragmentActivity?) {
|
private class RepoApi(val activity: FragmentActivity?) {
|
||||||
@JavascriptInterface
|
@JavascriptInterface
|
||||||
fun installRepo(repoUrl: String) {
|
fun installRepo(repoUrl: String) {
|
||||||
activity?.loadRepository(repoUrl)
|
activity?.loadRepository(repoUrl)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,198 @@
|
||||||
|
package com.lagradost.cloudstream3.ui.account
|
||||||
|
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.core.view.isVisible
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import androidx.viewbinding.ViewBinding
|
||||||
|
import com.lagradost.cloudstream3.R
|
||||||
|
import com.lagradost.cloudstream3.databinding.AccountListItemAddBinding
|
||||||
|
import com.lagradost.cloudstream3.databinding.AccountListItemBinding
|
||||||
|
import com.lagradost.cloudstream3.databinding.AccountListItemEditBinding
|
||||||
|
import com.lagradost.cloudstream3.ui.account.AccountHelper.showAccountEditDialog
|
||||||
|
import com.lagradost.cloudstream3.ui.result.setImage
|
||||||
|
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings
|
||||||
|
import com.lagradost.cloudstream3.utils.DataStoreHelper
|
||||||
|
import com.lagradost.cloudstream3.utils.UIHelper.setImage
|
||||||
|
|
||||||
|
class AccountAdapter(
|
||||||
|
private val accounts: List<DataStoreHelper.Account>,
|
||||||
|
private val accountSelectCallback: (DataStoreHelper.Account) -> Unit,
|
||||||
|
private val accountCreateCallback: (DataStoreHelper.Account) -> Unit,
|
||||||
|
private val accountEditCallback: (DataStoreHelper.Account) -> Unit,
|
||||||
|
private val accountDeleteCallback: (DataStoreHelper.Account) -> Unit
|
||||||
|
) : RecyclerView.Adapter<AccountAdapter.AccountViewHolder>() {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val VIEW_TYPE_SELECT_ACCOUNT = 0
|
||||||
|
const val VIEW_TYPE_ADD_ACCOUNT = 1
|
||||||
|
const val VIEW_TYPE_EDIT_ACCOUNT = 2
|
||||||
|
}
|
||||||
|
|
||||||
|
inner class AccountViewHolder(private val binding: ViewBinding) :
|
||||||
|
RecyclerView.ViewHolder(binding.root) {
|
||||||
|
|
||||||
|
fun bind(account: DataStoreHelper.Account?) {
|
||||||
|
when (binding) {
|
||||||
|
is AccountListItemBinding -> binding.apply {
|
||||||
|
if (account == null) return@apply
|
||||||
|
|
||||||
|
val isTv = isTvSettings() || !root.isInTouchMode
|
||||||
|
|
||||||
|
val isLastUsedAccount = account.keyIndex == DataStoreHelper.selectedKeyIndex
|
||||||
|
|
||||||
|
accountName.text = account.name
|
||||||
|
accountImage.setImage(account.image)
|
||||||
|
lockIcon.isVisible = account.lockPin != null
|
||||||
|
outline.isVisible = !isTv && isLastUsedAccount
|
||||||
|
|
||||||
|
if (isTv) {
|
||||||
|
// For emulator but this is fine on TV also
|
||||||
|
root.isFocusableInTouchMode = true
|
||||||
|
if (isLastUsedAccount) {
|
||||||
|
root.requestFocus()
|
||||||
|
}
|
||||||
|
|
||||||
|
root.foreground = ContextCompat.getDrawable(
|
||||||
|
root.context,
|
||||||
|
R.drawable.outline_drawable
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
root.setOnLongClickListener {
|
||||||
|
showAccountEditDialog(
|
||||||
|
context = root.context,
|
||||||
|
account = account,
|
||||||
|
isNewAccount = false,
|
||||||
|
accountEditCallback = { account -> accountEditCallback.invoke(account) },
|
||||||
|
accountDeleteCallback = { account -> accountDeleteCallback.invoke(account) }
|
||||||
|
)
|
||||||
|
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
root.setOnClickListener {
|
||||||
|
accountSelectCallback.invoke(account)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
is AccountListItemEditBinding -> binding.apply {
|
||||||
|
if (account == null) return@apply
|
||||||
|
|
||||||
|
val isTv = isTvSettings() || !root.isInTouchMode
|
||||||
|
|
||||||
|
val isLastUsedAccount = account.keyIndex == DataStoreHelper.selectedKeyIndex
|
||||||
|
|
||||||
|
accountName.text = account.name
|
||||||
|
accountImage.setImage(
|
||||||
|
account.image,
|
||||||
|
fadeIn = false,
|
||||||
|
radius = 10
|
||||||
|
)
|
||||||
|
lockIcon.isVisible = account.lockPin != null
|
||||||
|
outline.isVisible = !isTv && isLastUsedAccount
|
||||||
|
|
||||||
|
if (isTv) {
|
||||||
|
// For emulator but this is fine on TV also
|
||||||
|
root.isFocusableInTouchMode = true
|
||||||
|
if (isLastUsedAccount) {
|
||||||
|
root.requestFocus()
|
||||||
|
}
|
||||||
|
|
||||||
|
root.foreground = ContextCompat.getDrawable(
|
||||||
|
root.context,
|
||||||
|
R.drawable.outline_drawable
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
root.setOnClickListener {
|
||||||
|
showAccountEditDialog(
|
||||||
|
context = root.context,
|
||||||
|
account = account,
|
||||||
|
isNewAccount = false,
|
||||||
|
accountEditCallback = { account -> accountEditCallback.invoke(account) },
|
||||||
|
accountDeleteCallback = { account -> accountDeleteCallback.invoke(account) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
is AccountListItemAddBinding -> binding.apply {
|
||||||
|
root.setOnClickListener {
|
||||||
|
val remainingImages =
|
||||||
|
DataStoreHelper.profileImages.toSet() - accounts.filter { it.customImage == null }
|
||||||
|
.mapNotNull { DataStoreHelper.profileImages.getOrNull(it.defaultImageIndex) }.toSet()
|
||||||
|
|
||||||
|
val image =
|
||||||
|
DataStoreHelper.profileImages.indexOf(remainingImages.randomOrNull() ?: DataStoreHelper.profileImages.random())
|
||||||
|
val keyIndex = (accounts.maxOfOrNull { it.keyIndex } ?: 0) + 1
|
||||||
|
|
||||||
|
val accountName = root.context.getString(R.string.account)
|
||||||
|
|
||||||
|
showAccountEditDialog(
|
||||||
|
root.context,
|
||||||
|
DataStoreHelper.Account(
|
||||||
|
keyIndex = keyIndex,
|
||||||
|
name = "$accountName $keyIndex",
|
||||||
|
customImage = null,
|
||||||
|
defaultImageIndex = image
|
||||||
|
),
|
||||||
|
isNewAccount = true,
|
||||||
|
accountEditCallback = { account -> accountCreateCallback.invoke(account) },
|
||||||
|
accountDeleteCallback = {}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AccountViewHolder =
|
||||||
|
AccountViewHolder(
|
||||||
|
binding = when (viewType) {
|
||||||
|
VIEW_TYPE_SELECT_ACCOUNT -> {
|
||||||
|
AccountListItemBinding.inflate(
|
||||||
|
LayoutInflater.from(parent.context),
|
||||||
|
parent,
|
||||||
|
false
|
||||||
|
)
|
||||||
|
}
|
||||||
|
VIEW_TYPE_ADD_ACCOUNT -> {
|
||||||
|
AccountListItemAddBinding.inflate(
|
||||||
|
LayoutInflater.from(parent.context),
|
||||||
|
parent,
|
||||||
|
false
|
||||||
|
)
|
||||||
|
}
|
||||||
|
VIEW_TYPE_EDIT_ACCOUNT -> {
|
||||||
|
AccountListItemEditBinding.inflate(
|
||||||
|
LayoutInflater.from(parent.context),
|
||||||
|
parent,
|
||||||
|
false
|
||||||
|
)
|
||||||
|
}
|
||||||
|
else -> throw IllegalArgumentException("Invalid view type")
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
override fun onBindViewHolder(holder: AccountViewHolder, position: Int) {
|
||||||
|
holder.bind(accounts.getOrNull(position))
|
||||||
|
}
|
||||||
|
|
||||||
|
var viewType = 0
|
||||||
|
|
||||||
|
override fun getItemViewType(position: Int): Int {
|
||||||
|
if (viewType != 0 && position != accounts.count()) {
|
||||||
|
return viewType
|
||||||
|
}
|
||||||
|
|
||||||
|
return when (position) {
|
||||||
|
accounts.count() -> VIEW_TYPE_ADD_ACCOUNT
|
||||||
|
else -> VIEW_TYPE_SELECT_ACCOUNT
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getItemCount(): Int {
|
||||||
|
return accounts.count() + 1
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,356 @@
|
||||||
|
package com.lagradost.cloudstream3.ui.account
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.DialogInterface
|
||||||
|
import android.content.Intent
|
||||||
|
import android.text.Editable
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.inputmethod.EditorInfo
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.annotation.StringRes
|
||||||
|
import androidx.appcompat.app.AlertDialog
|
||||||
|
import androidx.core.view.isGone
|
||||||
|
import androidx.core.view.isVisible
|
||||||
|
import androidx.core.widget.doOnTextChanged
|
||||||
|
import androidx.lifecycle.ViewModelProvider
|
||||||
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import com.google.android.material.bottomsheet.BottomSheetDialog
|
||||||
|
import com.lagradost.cloudstream3.AcraApplication.Companion.getActivity
|
||||||
|
import com.lagradost.cloudstream3.MainActivity
|
||||||
|
import com.lagradost.cloudstream3.R
|
||||||
|
import com.lagradost.cloudstream3.databinding.AccountEditDialogBinding
|
||||||
|
import com.lagradost.cloudstream3.databinding.AccountSelectLinearBinding
|
||||||
|
import com.lagradost.cloudstream3.databinding.LockPinDialogBinding
|
||||||
|
import com.lagradost.cloudstream3.mvvm.logError
|
||||||
|
import com.lagradost.cloudstream3.mvvm.observe
|
||||||
|
import com.lagradost.cloudstream3.ui.result.setImage
|
||||||
|
import com.lagradost.cloudstream3.ui.result.setLinearListLayout
|
||||||
|
import com.lagradost.cloudstream3.utils.AppUtils.setDefaultFocus
|
||||||
|
import com.lagradost.cloudstream3.utils.DataStoreHelper
|
||||||
|
import com.lagradost.cloudstream3.utils.DataStoreHelper.getDefaultAccount
|
||||||
|
import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe
|
||||||
|
import com.lagradost.cloudstream3.utils.UIHelper.showInputMethod
|
||||||
|
|
||||||
|
object AccountHelper {
|
||||||
|
fun showAccountEditDialog(
|
||||||
|
context: Context,
|
||||||
|
account: DataStoreHelper.Account,
|
||||||
|
isNewAccount: Boolean,
|
||||||
|
accountEditCallback: (DataStoreHelper.Account) -> Unit,
|
||||||
|
accountDeleteCallback: (DataStoreHelper.Account) -> Unit
|
||||||
|
) {
|
||||||
|
val binding = AccountEditDialogBinding.inflate(LayoutInflater.from(context), null, false)
|
||||||
|
val builder = AlertDialog.Builder(context, R.style.AlertDialogCustom)
|
||||||
|
.setView(binding.root)
|
||||||
|
|
||||||
|
var currentEditAccount = account
|
||||||
|
val dialog = builder.show()
|
||||||
|
|
||||||
|
if (!isNewAccount) binding.title.setText(R.string.edit_account)
|
||||||
|
|
||||||
|
// Set up the dialog content
|
||||||
|
binding.accountName.text = Editable.Factory.getInstance()?.newEditable(account.name)
|
||||||
|
binding.accountName.doOnTextChanged { text, _, _, _ ->
|
||||||
|
currentEditAccount = currentEditAccount.copy(name = text?.toString() ?: "")
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.deleteBtt.isGone = isNewAccount
|
||||||
|
binding.deleteBtt.setOnClickListener {
|
||||||
|
val dialogClickListener = DialogInterface.OnClickListener { _, which ->
|
||||||
|
when (which) {
|
||||||
|
DialogInterface.BUTTON_POSITIVE -> {
|
||||||
|
accountDeleteCallback.invoke(account)
|
||||||
|
dialog?.dismissSafe()
|
||||||
|
}
|
||||||
|
|
||||||
|
DialogInterface.BUTTON_NEGATIVE -> {
|
||||||
|
dialog?.dismissSafe()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
AlertDialog.Builder(context).setTitle(R.string.delete).setMessage(
|
||||||
|
context.getString(R.string.delete_message).format(
|
||||||
|
currentEditAccount.name
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.setPositiveButton(R.string.delete, dialogClickListener)
|
||||||
|
.setNegativeButton(R.string.cancel, dialogClickListener)
|
||||||
|
.show().setDefaultFocus()
|
||||||
|
} catch (t: Throwable) {
|
||||||
|
logError(t)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.cancelBtt.setOnClickListener {
|
||||||
|
dialog?.dismissSafe()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle the profile picture and its interactions
|
||||||
|
binding.accountImage.setImage(account.image)
|
||||||
|
binding.accountImage.setOnClickListener {
|
||||||
|
// Roll the image forwards once
|
||||||
|
currentEditAccount =
|
||||||
|
currentEditAccount.copy(defaultImageIndex = (currentEditAccount.defaultImageIndex + 1) % DataStoreHelper.profileImages.size)
|
||||||
|
binding.accountImage.setImage(currentEditAccount.image)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle applying changes
|
||||||
|
binding.applyBtt.setOnClickListener {
|
||||||
|
if (currentEditAccount.lockPin != null) {
|
||||||
|
// Ask for the current PIN
|
||||||
|
showPinInputDialog(context, currentEditAccount.lockPin, false) { pin ->
|
||||||
|
if (pin == null) return@showPinInputDialog
|
||||||
|
// PIN is correct, proceed to update the account
|
||||||
|
accountEditCallback.invoke(currentEditAccount)
|
||||||
|
dialog.dismissSafe()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// No lock PIN set, proceed to update the account
|
||||||
|
accountEditCallback.invoke(currentEditAccount)
|
||||||
|
dialog.dismissSafe()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle setting or changing the PIN
|
||||||
|
if (currentEditAccount.keyIndex == getDefaultAccount(context).keyIndex) {
|
||||||
|
binding.lockProfileCheckbox.isVisible = false
|
||||||
|
if (currentEditAccount.lockPin != null) {
|
||||||
|
currentEditAccount = currentEditAccount.copy(lockPin = null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var canSetPin = true
|
||||||
|
|
||||||
|
binding.lockProfileCheckbox.isChecked = currentEditAccount.lockPin != null
|
||||||
|
|
||||||
|
binding.lockProfileCheckbox.setOnCheckedChangeListener { _, isChecked ->
|
||||||
|
if (isChecked) {
|
||||||
|
if (canSetPin) {
|
||||||
|
showPinInputDialog(context, null, true) { pin ->
|
||||||
|
if (pin == null) {
|
||||||
|
binding.lockProfileCheckbox.isChecked = false
|
||||||
|
return@showPinInputDialog
|
||||||
|
}
|
||||||
|
|
||||||
|
currentEditAccount = currentEditAccount.copy(lockPin = pin)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (currentEditAccount.lockPin != null) {
|
||||||
|
// Ask for the current PIN
|
||||||
|
showPinInputDialog(context, currentEditAccount.lockPin, true) { pin ->
|
||||||
|
if (pin == null || pin != currentEditAccount.lockPin) {
|
||||||
|
canSetPin = false
|
||||||
|
binding.lockProfileCheckbox.isChecked = true
|
||||||
|
} else {
|
||||||
|
currentEditAccount = currentEditAccount.copy(lockPin = null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
canSetPin = true
|
||||||
|
}
|
||||||
|
|
||||||
|
fun showPinInputDialog(
|
||||||
|
context: Context,
|
||||||
|
currentPin: String?,
|
||||||
|
editAccount: Boolean,
|
||||||
|
forStartup: Boolean = false,
|
||||||
|
errorText: String? = null,
|
||||||
|
callback: (String?) -> Unit
|
||||||
|
) {
|
||||||
|
fun TextView.visibleWithText(@StringRes textRes: Int) {
|
||||||
|
isVisible = true
|
||||||
|
setText(textRes)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun TextView.visibleWithText(text: String?) {
|
||||||
|
isVisible = true
|
||||||
|
setText(text)
|
||||||
|
}
|
||||||
|
|
||||||
|
val binding = LockPinDialogBinding.inflate(LayoutInflater.from(context))
|
||||||
|
|
||||||
|
val isPinSet = currentPin != null
|
||||||
|
val isNewPin = editAccount && !isPinSet
|
||||||
|
val isEditPin = editAccount && isPinSet
|
||||||
|
|
||||||
|
val titleRes = if (isEditPin) R.string.enter_current_pin else R.string.enter_pin
|
||||||
|
|
||||||
|
var isPinValid = false
|
||||||
|
|
||||||
|
val builder = AlertDialog.Builder(context, R.style.AlertDialogCustom)
|
||||||
|
.setView(binding.root)
|
||||||
|
.setTitle(titleRes)
|
||||||
|
.setNegativeButton(R.string.cancel) { _, _ ->
|
||||||
|
callback.invoke(null)
|
||||||
|
}
|
||||||
|
.setOnCancelListener {
|
||||||
|
callback.invoke(null)
|
||||||
|
}
|
||||||
|
.setOnDismissListener {
|
||||||
|
if (!isPinValid) {
|
||||||
|
callback.invoke(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (forStartup) {
|
||||||
|
val currentAccount = DataStoreHelper.accounts.firstOrNull {
|
||||||
|
it.keyIndex == DataStoreHelper.selectedKeyIndex
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.setTitle(context.getString(R.string.enter_pin_with_name, currentAccount?.name))
|
||||||
|
builder.setOnDismissListener {
|
||||||
|
if (!isPinValid) {
|
||||||
|
context.getActivity()?.finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// So that if they don't know the PIN for the current account,
|
||||||
|
// they don't get completely locked out
|
||||||
|
builder.setNeutralButton(R.string.use_default_account) { _, _ ->
|
||||||
|
val activity = context.getActivity()
|
||||||
|
if (activity is AccountSelectActivity) {
|
||||||
|
isPinValid = true
|
||||||
|
activity.viewModel.handleAccountSelect(getDefaultAccount(context), activity)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isNewPin) {
|
||||||
|
if (errorText != null) binding.pinEditTextError.visibleWithText(errorText)
|
||||||
|
builder.setPositiveButton(R.string.setup_done) { _, _ ->
|
||||||
|
if (!isPinValid) {
|
||||||
|
// If the done button is pressed and there is an error,
|
||||||
|
// ask again, and mention the error that caused this.
|
||||||
|
showPinInputDialog(
|
||||||
|
context = binding.root.context,
|
||||||
|
currentPin = null,
|
||||||
|
editAccount = true,
|
||||||
|
errorText = binding.pinEditTextError.text.toString(),
|
||||||
|
callback = callback
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
val enteredPin = binding.pinEditText.text.toString()
|
||||||
|
callback.invoke(enteredPin)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val dialog = builder.create()
|
||||||
|
|
||||||
|
binding.pinEditText.doOnTextChanged { text, _, _, _ ->
|
||||||
|
val enteredPin = text.toString()
|
||||||
|
val isEnteredPinValid = enteredPin.length == 4
|
||||||
|
|
||||||
|
if (isEnteredPinValid) {
|
||||||
|
if (isPinSet) {
|
||||||
|
if (enteredPin != currentPin) {
|
||||||
|
binding.pinEditTextError.visibleWithText(R.string.pin_error_incorrect)
|
||||||
|
binding.pinEditText.text = null
|
||||||
|
isPinValid = false
|
||||||
|
} else {
|
||||||
|
binding.pinEditTextError.isVisible = false
|
||||||
|
isPinValid = true
|
||||||
|
|
||||||
|
callback.invoke(enteredPin)
|
||||||
|
dialog.dismissSafe()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
binding.pinEditTextError.isVisible = false
|
||||||
|
isPinValid = true
|
||||||
|
}
|
||||||
|
} else if (isNewPin) {
|
||||||
|
binding.pinEditTextError.visibleWithText(R.string.pin_error_length)
|
||||||
|
isPinValid = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Detect IME_ACTION_DONE
|
||||||
|
binding.pinEditText.setOnEditorActionListener { _, actionId, _ ->
|
||||||
|
if (actionId == EditorInfo.IME_ACTION_DONE && isPinValid) {
|
||||||
|
val enteredPin = binding.pinEditText.text.toString()
|
||||||
|
callback.invoke(enteredPin)
|
||||||
|
dialog.dismissSafe()
|
||||||
|
}
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
// We don't want to accidentally have the dialog dismiss when clicking outside of it.
|
||||||
|
// That is what the cancel button is for.
|
||||||
|
dialog.setCanceledOnTouchOutside(false)
|
||||||
|
|
||||||
|
dialog.show()
|
||||||
|
|
||||||
|
// Auto focus on PIN input and show keyboard
|
||||||
|
binding.pinEditText.requestFocus()
|
||||||
|
binding.pinEditText.postDelayed({
|
||||||
|
showInputMethod(binding.pinEditText)
|
||||||
|
}, 200)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Activity?.showAccountSelectLinear() {
|
||||||
|
val activity = this as? MainActivity ?: return
|
||||||
|
val viewModel = ViewModelProvider(activity)[AccountViewModel::class.java]
|
||||||
|
|
||||||
|
val binding: AccountSelectLinearBinding = AccountSelectLinearBinding.inflate(
|
||||||
|
LayoutInflater.from(activity)
|
||||||
|
)
|
||||||
|
|
||||||
|
val builder = BottomSheetDialog(activity)
|
||||||
|
builder.setContentView(binding.root)
|
||||||
|
builder.show()
|
||||||
|
|
||||||
|
binding.manageAccountsButton.setOnClickListener {
|
||||||
|
val accountSelectIntent = Intent(activity, AccountSelectActivity::class.java)
|
||||||
|
accountSelectIntent.putExtra("isEditingFromMainActivity", true)
|
||||||
|
activity.startActivity(accountSelectIntent)
|
||||||
|
builder.dismissSafe()
|
||||||
|
}
|
||||||
|
|
||||||
|
val recyclerView: RecyclerView = binding.accountRecyclerView
|
||||||
|
|
||||||
|
val itemSize = recyclerView.resources.getDimensionPixelSize(
|
||||||
|
R.dimen.account_select_linear_item_size
|
||||||
|
)
|
||||||
|
|
||||||
|
recyclerView.addItemDecoration(AccountSelectLinearItemDecoration(itemSize))
|
||||||
|
|
||||||
|
recyclerView.setLinearListLayout(isHorizontal = true)
|
||||||
|
|
||||||
|
val currentAccount = DataStoreHelper.accounts.firstOrNull {
|
||||||
|
it.keyIndex == DataStoreHelper.selectedKeyIndex
|
||||||
|
} ?: getDefaultAccount(activity)
|
||||||
|
|
||||||
|
// We want to make sure the accounts are up-to-date
|
||||||
|
viewModel.handleAccountSelect(
|
||||||
|
currentAccount,
|
||||||
|
activity,
|
||||||
|
reloadForActivity = true
|
||||||
|
)
|
||||||
|
|
||||||
|
activity.observe(viewModel.accounts) { liveAccounts ->
|
||||||
|
recyclerView.adapter = AccountAdapter(
|
||||||
|
liveAccounts,
|
||||||
|
accountSelectCallback = { account ->
|
||||||
|
viewModel.handleAccountSelect(account, activity)
|
||||||
|
builder.dismissSafe()
|
||||||
|
},
|
||||||
|
accountCreateCallback = { viewModel.handleAccountUpdate(it, activity) },
|
||||||
|
accountEditCallback = { viewModel.handleAccountUpdate(it, activity) },
|
||||||
|
accountDeleteCallback = { viewModel.handleAccountDelete(it, activity) }
|
||||||
|
)
|
||||||
|
|
||||||
|
activity.observe(viewModel.selectedKeyIndex) { selectedKeyIndex ->
|
||||||
|
// Scroll to current account (which is focused by default)
|
||||||
|
val layoutManager = recyclerView.layoutManager as LinearLayoutManager
|
||||||
|
layoutManager.scrollToPositionWithOffset(selectedKeyIndex, 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,168 @@
|
||||||
|
package com.lagradost.cloudstream3.ui.account
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.Bundle
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.lifecycle.ViewModelProvider
|
||||||
|
import androidx.preference.PreferenceManager
|
||||||
|
import androidx.recyclerview.widget.GridLayoutManager
|
||||||
|
import com.lagradost.cloudstream3.CommonActivity
|
||||||
|
import com.lagradost.cloudstream3.CommonActivity.loadThemes
|
||||||
|
import com.lagradost.cloudstream3.CommonActivity.showToast
|
||||||
|
import com.lagradost.cloudstream3.MainActivity
|
||||||
|
import com.lagradost.cloudstream3.R
|
||||||
|
import com.lagradost.cloudstream3.databinding.ActivityAccountSelectBinding
|
||||||
|
import com.lagradost.cloudstream3.mvvm.observe
|
||||||
|
import com.lagradost.cloudstream3.ui.AutofitRecyclerView
|
||||||
|
import com.lagradost.cloudstream3.ui.account.AccountAdapter.Companion.VIEW_TYPE_EDIT_ACCOUNT
|
||||||
|
import com.lagradost.cloudstream3.ui.account.AccountAdapter.Companion.VIEW_TYPE_SELECT_ACCOUNT
|
||||||
|
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings
|
||||||
|
import com.lagradost.cloudstream3.utils.DataStoreHelper.accounts
|
||||||
|
import com.lagradost.cloudstream3.utils.DataStoreHelper.selectedKeyIndex
|
||||||
|
import com.lagradost.cloudstream3.utils.DataStoreHelper.setAccount
|
||||||
|
import com.lagradost.cloudstream3.utils.UIHelper.colorFromAttribute
|
||||||
|
|
||||||
|
class AccountSelectActivity : AppCompatActivity() {
|
||||||
|
|
||||||
|
lateinit var viewModel: AccountViewModel
|
||||||
|
|
||||||
|
@SuppressLint("NotifyDataSetChanged")
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
loadThemes(this)
|
||||||
|
|
||||||
|
window.navigationBarColor = colorFromAttribute(R.attr.primaryBlackBackground)
|
||||||
|
|
||||||
|
// Are we editing and coming from MainActivity?
|
||||||
|
val isEditingFromMainActivity = intent.getBooleanExtra(
|
||||||
|
"isEditingFromMainActivity",
|
||||||
|
false
|
||||||
|
)
|
||||||
|
|
||||||
|
val settingsManager = PreferenceManager.getDefaultSharedPreferences(this)
|
||||||
|
val skipStartup = settingsManager.getBoolean(
|
||||||
|
getString(R.string.skip_startup_account_select_key),
|
||||||
|
false
|
||||||
|
) || accounts.count() <= 1
|
||||||
|
|
||||||
|
viewModel = ViewModelProvider(this)[AccountViewModel::class.java]
|
||||||
|
|
||||||
|
// Don't show account selection if there is only
|
||||||
|
// one account that exists
|
||||||
|
if (!isEditingFromMainActivity && skipStartup) {
|
||||||
|
val currentAccount = accounts.firstOrNull { it.keyIndex == selectedKeyIndex }
|
||||||
|
if (currentAccount?.lockPin != null) {
|
||||||
|
CommonActivity.init(this)
|
||||||
|
viewModel.handleAccountSelect(currentAccount, this, true)
|
||||||
|
observe(viewModel.isAllowedLogin) { isAllowedLogin ->
|
||||||
|
if (isAllowedLogin) {
|
||||||
|
// We are allowed to continue to MainActivity
|
||||||
|
navigateToMainActivity()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (accounts.count() > 1) {
|
||||||
|
showToast(this, getString(
|
||||||
|
R.string.logged_account,
|
||||||
|
currentAccount?.name
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
navigateToMainActivity()
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
CommonActivity.init(this)
|
||||||
|
|
||||||
|
val binding = ActivityAccountSelectBinding.inflate(layoutInflater)
|
||||||
|
setContentView(binding.root)
|
||||||
|
|
||||||
|
val recyclerView: AutofitRecyclerView = binding.accountRecyclerView
|
||||||
|
|
||||||
|
observe(viewModel.accounts) { liveAccounts ->
|
||||||
|
val adapter = AccountAdapter(
|
||||||
|
liveAccounts,
|
||||||
|
// Handle the selected account
|
||||||
|
accountSelectCallback = {
|
||||||
|
viewModel.handleAccountSelect(it, this)
|
||||||
|
observe(viewModel.isAllowedLogin) { isAllowedLogin ->
|
||||||
|
if (isAllowedLogin) {
|
||||||
|
// We are allowed to continue to MainActivity
|
||||||
|
navigateToMainActivity()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
accountCreateCallback = { viewModel.handleAccountUpdate(it, this) },
|
||||||
|
accountEditCallback = {
|
||||||
|
viewModel.handleAccountUpdate(it, this)
|
||||||
|
|
||||||
|
// We came from MainActivity, return there
|
||||||
|
// and switch to the edited account
|
||||||
|
if (isEditingFromMainActivity) {
|
||||||
|
setAccount(it)
|
||||||
|
navigateToMainActivity()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
accountDeleteCallback = { viewModel.handleAccountDelete(it,this) }
|
||||||
|
)
|
||||||
|
|
||||||
|
recyclerView.adapter = adapter
|
||||||
|
|
||||||
|
if (isTvSettings()) {
|
||||||
|
binding.editAccountButton.setBackgroundResource(
|
||||||
|
R.drawable.player_button_tv_attr_no_bg
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
observe(viewModel.selectedKeyIndex) { selectedKeyIndex ->
|
||||||
|
// Scroll to current account (which is focused by default)
|
||||||
|
val layoutManager = recyclerView.layoutManager as GridLayoutManager
|
||||||
|
layoutManager.scrollToPositionWithOffset(selectedKeyIndex, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
observe(viewModel.isEditing) { isEditing ->
|
||||||
|
if (isEditing) {
|
||||||
|
binding.editAccountButton.setImageResource(R.drawable.ic_baseline_close_24)
|
||||||
|
binding.title.setText(R.string.manage_accounts)
|
||||||
|
adapter.viewType = VIEW_TYPE_EDIT_ACCOUNT
|
||||||
|
} else {
|
||||||
|
binding.editAccountButton.setImageResource(R.drawable.ic_baseline_edit_24)
|
||||||
|
binding.title.setText(R.string.select_an_account)
|
||||||
|
adapter.viewType = VIEW_TYPE_SELECT_ACCOUNT
|
||||||
|
}
|
||||||
|
|
||||||
|
adapter.notifyDataSetChanged()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isEditingFromMainActivity) {
|
||||||
|
viewModel.setIsEditing(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.editAccountButton.setOnClickListener {
|
||||||
|
// We came from MainActivity, return there
|
||||||
|
// and resume its state
|
||||||
|
if (isEditingFromMainActivity) {
|
||||||
|
navigateToMainActivity()
|
||||||
|
return@setOnClickListener
|
||||||
|
}
|
||||||
|
|
||||||
|
viewModel.toggleIsEditing()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isTvSettings()) {
|
||||||
|
recyclerView.spanCount = if (liveAccounts.count() + 1 <= 6) {
|
||||||
|
liveAccounts.count() + 1
|
||||||
|
} else 6
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun navigateToMainActivity() {
|
||||||
|
val mainIntent = Intent(this, MainActivity::class.java)
|
||||||
|
startActivity(mainIntent)
|
||||||
|
finish() // Finish the account selection activity
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,14 @@
|
||||||
|
package com.lagradost.cloudstream3.ui.account
|
||||||
|
|
||||||
|
import android.graphics.Rect
|
||||||
|
import android.view.View
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
|
||||||
|
class AccountSelectLinearItemDecoration(private val size: Int) : RecyclerView.ItemDecoration() {
|
||||||
|
override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State) {
|
||||||
|
val layoutParams = view.layoutParams as RecyclerView.LayoutParams
|
||||||
|
layoutParams.width = size
|
||||||
|
layoutParams.height = size
|
||||||
|
view.layoutParams = layoutParams
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,123 @@
|
||||||
|
package com.lagradost.cloudstream3.ui.account
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.lifecycle.LiveData
|
||||||
|
import androidx.lifecycle.MutableLiveData
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import com.lagradost.cloudstream3.AcraApplication.Companion.context
|
||||||
|
import com.lagradost.cloudstream3.AcraApplication.Companion.removeKeys
|
||||||
|
import com.lagradost.cloudstream3.ui.account.AccountHelper.showPinInputDialog
|
||||||
|
import com.lagradost.cloudstream3.utils.DataStoreHelper
|
||||||
|
import com.lagradost.cloudstream3.utils.DataStoreHelper.getAccounts
|
||||||
|
import com.lagradost.cloudstream3.utils.DataStoreHelper.getDefaultAccount
|
||||||
|
import com.lagradost.cloudstream3.utils.DataStoreHelper.setAccount
|
||||||
|
|
||||||
|
class AccountViewModel : ViewModel() {
|
||||||
|
private fun getAllAccounts(): List<DataStoreHelper.Account> {
|
||||||
|
return context?.let { getAccounts(it) } ?: DataStoreHelper.accounts.toList()
|
||||||
|
}
|
||||||
|
|
||||||
|
private val _accounts: MutableLiveData<List<DataStoreHelper.Account>> = MutableLiveData(getAllAccounts())
|
||||||
|
val accounts: LiveData<List<DataStoreHelper.Account>> = _accounts
|
||||||
|
|
||||||
|
private val _isEditing = MutableLiveData(false)
|
||||||
|
val isEditing: LiveData<Boolean> = _isEditing
|
||||||
|
|
||||||
|
private val _isAllowedLogin = MutableLiveData(false)
|
||||||
|
val isAllowedLogin: LiveData<Boolean> = _isAllowedLogin
|
||||||
|
|
||||||
|
private val _selectedKeyIndex = MutableLiveData(
|
||||||
|
getAllAccounts().indexOfFirst {
|
||||||
|
it.keyIndex == DataStoreHelper.selectedKeyIndex
|
||||||
|
}
|
||||||
|
)
|
||||||
|
val selectedKeyIndex: LiveData<Int> = _selectedKeyIndex
|
||||||
|
|
||||||
|
fun setIsEditing(value: Boolean) {
|
||||||
|
_isEditing.postValue(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun toggleIsEditing() {
|
||||||
|
_isEditing.postValue(!(_isEditing.value ?: false))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun handleAccountUpdate(
|
||||||
|
account: DataStoreHelper.Account,
|
||||||
|
context: Context
|
||||||
|
) {
|
||||||
|
val currentAccounts = getAccounts(context).toMutableList()
|
||||||
|
|
||||||
|
val overrideIndex = currentAccounts.indexOfFirst { it.keyIndex == account.keyIndex }
|
||||||
|
|
||||||
|
if (overrideIndex != -1) {
|
||||||
|
currentAccounts[overrideIndex] = account
|
||||||
|
} else currentAccounts.add(account)
|
||||||
|
|
||||||
|
val currentHomePage = DataStoreHelper.currentHomePage
|
||||||
|
|
||||||
|
setAccount(account)
|
||||||
|
|
||||||
|
DataStoreHelper.currentHomePage = currentHomePage
|
||||||
|
DataStoreHelper.accounts = currentAccounts.toTypedArray()
|
||||||
|
|
||||||
|
_accounts.postValue(getAccounts(context))
|
||||||
|
_selectedKeyIndex.postValue(getAccounts(context).indexOf(account))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun handleAccountDelete(
|
||||||
|
account: DataStoreHelper.Account,
|
||||||
|
context: Context
|
||||||
|
) {
|
||||||
|
removeKeys(account.keyIndex.toString())
|
||||||
|
|
||||||
|
val currentAccounts = getAccounts(context).toMutableList()
|
||||||
|
|
||||||
|
currentAccounts.removeIf { it.keyIndex == account.keyIndex }
|
||||||
|
|
||||||
|
DataStoreHelper.accounts = currentAccounts.toTypedArray()
|
||||||
|
|
||||||
|
if (account.keyIndex == DataStoreHelper.selectedKeyIndex) {
|
||||||
|
setAccount(getDefaultAccount(context))
|
||||||
|
}
|
||||||
|
|
||||||
|
_accounts.postValue(getAccounts(context))
|
||||||
|
_selectedKeyIndex.postValue(getAllAccounts().indexOfFirst {
|
||||||
|
it.keyIndex == DataStoreHelper.selectedKeyIndex
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fun handleAccountSelect(
|
||||||
|
account: DataStoreHelper.Account,
|
||||||
|
context: Context,
|
||||||
|
forStartup: Boolean = false,
|
||||||
|
reloadForActivity: Boolean = false
|
||||||
|
) {
|
||||||
|
if (reloadForActivity) {
|
||||||
|
_accounts.postValue(getAccounts(context))
|
||||||
|
_selectedKeyIndex.postValue(getAccounts(context).indexOf(account))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the selected account has a lock PIN set
|
||||||
|
if (account.lockPin != null) {
|
||||||
|
// The selected account has a PIN set, prompt the user to enter the PIN
|
||||||
|
showPinInputDialog(
|
||||||
|
context,
|
||||||
|
account.lockPin,
|
||||||
|
false,
|
||||||
|
forStartup
|
||||||
|
) { pin ->
|
||||||
|
if (pin == null) return@showPinInputDialog
|
||||||
|
// Pin is correct, proceed
|
||||||
|
_isAllowedLogin.postValue(true)
|
||||||
|
_selectedKeyIndex.postValue(getAccounts(context).indexOf(account))
|
||||||
|
setAccount(account)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// No PIN set for the selected account, proceed
|
||||||
|
_isAllowedLogin.postValue(true)
|
||||||
|
_selectedKeyIndex.postValue(getAccounts(context).indexOf(account))
|
||||||
|
setAccount(account)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -5,6 +5,7 @@ import android.content.DialogInterface
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
import com.lagradost.cloudstream3.AcraApplication.Companion.getKey
|
import com.lagradost.cloudstream3.AcraApplication.Companion.getKey
|
||||||
|
import com.lagradost.cloudstream3.CommonActivity.activity
|
||||||
import com.lagradost.cloudstream3.CommonActivity.showToast
|
import com.lagradost.cloudstream3.CommonActivity.showToast
|
||||||
import com.lagradost.cloudstream3.R
|
import com.lagradost.cloudstream3.R
|
||||||
import com.lagradost.cloudstream3.mvvm.logError
|
import com.lagradost.cloudstream3.mvvm.logError
|
||||||
|
@ -19,7 +20,7 @@ import com.lagradost.cloudstream3.utils.VideoDownloadHelper
|
||||||
import com.lagradost.cloudstream3.utils.VideoDownloadManager
|
import com.lagradost.cloudstream3.utils.VideoDownloadManager
|
||||||
|
|
||||||
object DownloadButtonSetup {
|
object DownloadButtonSetup {
|
||||||
fun handleDownloadClick(activity: Activity?, click: DownloadClickEvent) {
|
fun handleDownloadClick(click: DownloadClickEvent) {
|
||||||
val id = click.data.id
|
val id = click.data.id
|
||||||
if (click.data !is VideoDownloadHelper.DownloadEpisodeCached) return
|
if (click.data !is VideoDownloadHelper.DownloadEpisodeCached) return
|
||||||
when (click.action) {
|
when (click.action) {
|
||||||
|
@ -89,9 +90,9 @@ object DownloadButtonSetup {
|
||||||
)?.fileLength
|
)?.fileLength
|
||||||
?: 0
|
?: 0
|
||||||
if (length > 0) {
|
if (length > 0) {
|
||||||
showToast(act, R.string.delete, Toast.LENGTH_LONG)
|
showToast(R.string.delete, Toast.LENGTH_LONG)
|
||||||
} else {
|
} else {
|
||||||
showToast(act, R.string.download, Toast.LENGTH_LONG)
|
showToast(R.string.download, Toast.LENGTH_LONG)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue