Compare commits
466 Commits
Author | SHA1 | Date |
---|---|---|
KingLucius | e697bf7554 | |
Luna712 | db2bf5e7be | |
KingLucius | 469a71236b | |
CranberrySoup | 4d5cd288ab | |
KingLucius | af828de8d5 | |
CranberrySoup | ee4d1dedc5 | |
KingLucius | f1cc4db89c | |
b4byhuey | 3874cb9f9d | |
phisher98 | 0a5399d9b6 | |
KingLucius | 71bd48f493 | |
KingLucius | 83c473d9f8 | |
RowdyRushya | c28a3cb987 | |
int3debug | d3828eeafe | |
int3debug | c07e6d3222 | |
KingLucius | 949b5830b6 | |
b4byhuey | ff1ffbeb83 | |
Luna712 | 138e1a1f0e | |
KingLucius | 004c481a5e | |
b4byhuey | e2946cad6b | |
int3debug | e6b9d621f9 | |
KingLucius | 0019f85501 | |
IndusAryan | 0744189020 | |
Ömer Faruk Sancak | 4399a612df | |
KingLucius | e01ff4d843 | |
KingLucius | 6cef9f7ea2 | |
int3debug | 9a18ef6411 | |
CranberrySoup | 6df3ef14f6 | |
int3debug | 5db541d7cc | |
CranberrySoup | aa8972870c | |
Rushikesh Chavan | afdc4988ac | |
KingLucius | e6c111532d | |
recloudstream[bot] | ffa7b0248a | |
firelight | c13d290377 | |
Hosted Weblate | 1bf7e14eab | |
phisher98 | 145ceea50f | |
Hosted Weblate | 2fad760426 | |
KingLucius | ff0dea3fbb | |
Hosted Weblate | 44e5b86176 | |
KingLucius | d8f89df163 | |
Hosted Weblate | a74563d003 | |
firelight | 0a24661e4c | |
firelight | ed2bdf44fb | |
IndusAryan | 51d91bf9a7 | |
firelight | fb89fd60b8 | |
recloudstream[bot] | 7db7742c73 | |
firelight | b246d80861 | |
Hosted Weblate | d321aba3a7 | |
IndusAryan | 22937424fa | |
Hosted Weblate | 7f0034e872 | |
IndusAryan | 35e38a53ad | |
Hosted Weblate | 6d8a31809d | |
KingLucius | 650c7583af | |
Hosted Weblate | 34af3a4b2f | |
int3debug | 9ef1f1cc41 | |
Hosted Weblate | 6ede44d85f | |
KingLucius | 2f03ca7de9 | |
Hosted Weblate | 7ce2dfc4aa | |
int3debug | 16510923d2 | |
Hosted Weblate | a9c2c0644a | |
firelight | 4468ce3d80 | |
IndusAryan | faeb71da2c | |
int3debug | 86bc0b8345 | |
int3debug | 1ff0b5dccd | |
KingLucius | a2e63174be | |
Osten | eb60be54ed | |
Osten | 8d5b73495d | |
KingLucius | a3bb853691 | |
Osten | 375b3ec46e | |
Osten | ad67b9ddab | |
KingLucius | 638cc4fee9 | |
IndusAryan | 4817b29b9c | |
IndusAryan | 040ac77b1a | |
firelight | 527046766a | |
Luna712 | adc653943b | |
IndusAryan | 81df68e137 | |
Luna712 | a01bb9e55b | |
Luna712 | 807bd85fa9 | |
KingLucius | 510d11f705 | |
firelight | bd69054f5d | |
firelight | 694e7abbdf | |
firelight | e3f9f255c7 | |
IndusAryan | 21b341e12f | |
IndusAryan | e3999d6e9a | |
Mater Yoda | f0f4ec87bc | |
self-similarity | 809a38507b | |
KingLucius | 1a380a3239 | |
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 | |
Nexus | da0be63b7c | |
Saksham Shekher | a95fcfc9db | |
imgbot[bot] | 40a963588f | |
LagradOst | 906f1fdc9a | |
LagradOst | b5566af401 | |
Hexated | 0d431fd508 | |
recloudstream[bot] | b115817357 | |
Hosted Weblate | c0a8461b87 | |
Shif-Jess | 8c9d52bc0e | |
recloudstream[bot] | 0f00b1baf0 | |
Cloudburst | ae1aaa3d7d | |
Cloudburst | b37aa55343 | |
jhih_yu | 77d4ecd7c6 | |
Cloudburst | 3b21ec3794 | |
recloudstream[bot] | 386ce75df1 | |
Hosted Weblate | 27155e0f7e | |
recloudstream[bot] | 3121b5b123 | |
Hosted Weblate | 8a5ddcd126 | |
Cloudburst | 42bf8ed08e | |
Horis | fb3576ea52 | |
recloudstream[bot] | 56a680fa9c | |
Hosted Weblate | 633aef8783 | |
Cloudburst | a12d234ef4 | |
reduplicated | bdb45b69d3 | |
reduplicated | 4449347593 | |
Shif-Jess | b356ad9e61 | |
Sarlay | 94e7eb8e9d | |
Shif-Jess | 4f9016713f | |
recloudstream[bot] | 4ed65f8e07 | |
Hosted Weblate | 7317278f57 | |
Shif-Jess | 53293dadd0 | |
LagradOst | 67b0549fd2 | |
Osten | 52d495f425 | |
Cloudburst | 0cbee70683 | |
Lag | 4235c826a5 | |
Cloudburst | 5245eff6e1 | |
Lag | 9c40abc4d3 | |
Lag | 019399952f | |
Lag | cc99899cf1 | |
Lag | 8fff809b79 | |
recloudstream[bot] | 67318a62a3 | |
Hosted Weblate | 288c5ffa39 | |
Lag | 8ebf5185a3 |
Binary file not shown.
Before Width: | Height: | Size: 58 KiB |
Binary file not shown.
Before Width: | Height: | Size: 136 KiB |
|
@ -56,6 +56,8 @@ for file in glob.glob(f"{XML_NAME}*/strings.xml"):
|
|||
if child.text.startswith("\\@string/"):
|
||||
print(f"[{file}] fixing {child.attrib['name']}")
|
||||
child.text = child.text.replace("\\@string/", "@string/")
|
||||
tree.write(file, encoding="utf-8", method="xml", pretty_print=True, xml_declaration=True)
|
||||
with open(file, 'wb') as fp:
|
||||
fp.write(b'<?xml version="1.0" encoding="utf-8"?>\n')
|
||||
tree.write(fp, encoding="utf-8", method="xml", pretty_print=True, xml_declaration=False)
|
||||
except ET.ParseError as ex:
|
||||
print(f"[{file}] {ex}")
|
||||
print(f"[{file}] {ex}")
|
||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 48 KiB |
Binary file not shown.
Before Width: | Height: | Size: 96 KiB |
Binary file not shown.
Before Width: | Height: | Size: 149 KiB |
|
@ -19,23 +19,23 @@ jobs:
|
|||
steps:
|
||||
- name: Generate access token
|
||||
id: generate_token
|
||||
uses: tibdex/github-app-token@v1
|
||||
uses: tibdex/github-app-token@v2
|
||||
with:
|
||||
app_id: ${{ secrets.GH_APP_ID }}
|
||||
private_key: ${{ secrets.GH_APP_KEY }}
|
||||
repository: "recloudstream/secrets"
|
||||
- name: Generate access token (archive)
|
||||
id: generate_archive_token
|
||||
uses: tibdex/github-app-token@v1
|
||||
uses: tibdex/github-app-token@v2
|
||||
with:
|
||||
app_id: ${{ secrets.GH_APP_ID }}
|
||||
private_key: ${{ secrets.GH_APP_KEY }}
|
||||
repository: "recloudstream/cloudstream-archive"
|
||||
- uses: actions/checkout@v2
|
||||
- name: Set up JDK 11
|
||||
uses: actions/setup-java@v2
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up JDK 17
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
java-version: '11'
|
||||
java-version: '17'
|
||||
distribution: 'adopt'
|
||||
- name: Grant execute permission for gradlew
|
||||
run: chmod +x gradlew
|
||||
|
@ -56,7 +56,9 @@ jobs:
|
|||
SIGNING_KEY_ALIAS: "key0"
|
||||
SIGNING_KEY_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:
|
||||
repository: "recloudstream/cloudstream-archive"
|
||||
token: ${{ steps.generate_archive_token.outputs.token }}
|
||||
|
|
|
@ -20,7 +20,7 @@ jobs:
|
|||
steps:
|
||||
- name: Generate access token
|
||||
id: generate_token
|
||||
uses: tibdex/github-app-token@v1
|
||||
uses: tibdex/github-app-token@v2
|
||||
with:
|
||||
app_id: ${{ secrets.GH_APP_ID }}
|
||||
private_key: ${{ secrets.GH_APP_KEY }}
|
||||
|
@ -42,13 +42,14 @@ jobs:
|
|||
cd $GITHUB_WORKSPACE/dokka/
|
||||
rm -rf "./-cloudstream"
|
||||
|
||||
- name: Setup JDK 11
|
||||
uses: actions/setup-java@v1
|
||||
- name: Setup JDK 17
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
java-version: 11
|
||||
java-version: 17
|
||||
distribution: 'adopt'
|
||||
|
||||
- name: Setup Android SDK
|
||||
uses: android-actions/setup-android@v2
|
||||
uses: android-actions/setup-android@v3
|
||||
|
||||
- name: Generate Dokka
|
||||
run: |
|
||||
|
|
|
@ -10,7 +10,7 @@ jobs:
|
|||
steps:
|
||||
- name: Generate access token
|
||||
id: generate_token
|
||||
uses: tibdex/github-app-token@v1
|
||||
uses: tibdex/github-app-token@v2
|
||||
with:
|
||||
app_id: ${{ secrets.GH_APP_ID }}
|
||||
private_key: ${{ secrets.GH_APP_KEY }}
|
||||
|
@ -27,7 +27,7 @@ jobs:
|
|||
comment-body: '${index}. ${similarity} #${number}'
|
||||
- name: Label if possible duplicate
|
||||
if: steps.similarity.outputs.similar-issues-found =='true'
|
||||
uses: actions/github-script@v6
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
github-token: ${{ steps.generate_token.outputs.token }}
|
||||
script: |
|
||||
|
@ -37,7 +37,7 @@ jobs:
|
|||
repo: context.repo.repo,
|
||||
labels: ["possible duplicate"]
|
||||
})
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v4
|
||||
- name: Automatically close issues that dont follow the issue template
|
||||
uses: lucasbento/auto-close-issues@v1.0.2
|
||||
with:
|
||||
|
@ -68,7 +68,7 @@ jobs:
|
|||
Found provider name: `${{ steps.provider_check.outputs.name }}`
|
||||
- name: Label if mentions provider
|
||||
if: steps.provider_check.outputs.name != 'none'
|
||||
uses: actions/github-script@v6
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
github-token: ${{ steps.generate_token.outputs.token }}
|
||||
script: |
|
||||
|
|
|
@ -18,16 +18,16 @@ jobs:
|
|||
steps:
|
||||
- name: Generate access token
|
||||
id: generate_token
|
||||
uses: tibdex/github-app-token@v1
|
||||
uses: tibdex/github-app-token@v2
|
||||
with:
|
||||
app_id: ${{ secrets.GH_APP_ID }}
|
||||
private_key: ${{ secrets.GH_APP_KEY }}
|
||||
repository: "recloudstream/secrets"
|
||||
- uses: actions/checkout@v2
|
||||
- name: Set up JDK 11
|
||||
uses: actions/setup-java@v2
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up JDK 17
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
java-version: '11'
|
||||
java-version: '17'
|
||||
distribution: 'adopt'
|
||||
- name: Grant execute permission for gradlew
|
||||
run: chmod +x gradlew
|
||||
|
@ -43,11 +43,14 @@ jobs:
|
|||
echo "key_pwd=$KEY_PWD" >> $GITHUB_OUTPUT
|
||||
- name: Run Gradle
|
||||
run: |
|
||||
./gradlew assemblePrerelease makeJar androidSourcesJar
|
||||
./gradlew assemblePrerelease build androidSourcesJar
|
||||
./gradlew makeJar # for classes.jar, has to be done after assemblePrerelease
|
||||
env:
|
||||
SIGNING_KEY_ALIAS: "key0"
|
||||
SIGNING_KEY_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
|
||||
uses: "marvinpinto/action-automatic-releases@latest"
|
||||
with:
|
||||
|
|
|
@ -6,18 +6,18 @@ jobs:
|
|||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Set up JDK 11
|
||||
uses: actions/setup-java@v2
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up JDK 17
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
java-version: '11'
|
||||
java-version: '17'
|
||||
distribution: 'adopt'
|
||||
- name: Grant execute permission for gradlew
|
||||
run: chmod +x gradlew
|
||||
- name: Run Gradle
|
||||
run: ./gradlew assemblePrereleaseDebug
|
||||
- name: Upload Artifact
|
||||
uses: actions/upload-artifact@v2
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: pull-request-build
|
||||
path: "app/build/outputs/apk/prerelease/debug/*.apk"
|
||||
|
|
|
@ -18,12 +18,12 @@ jobs:
|
|||
steps:
|
||||
- name: Generate access token
|
||||
id: generate_token
|
||||
uses: tibdex/github-app-token@v1
|
||||
uses: tibdex/github-app-token@v2
|
||||
with:
|
||||
app_id: ${{ secrets.GH_APP_ID }}
|
||||
private_key: ${{ secrets.GH_APP_KEY }}
|
||||
repository: "recloudstream/cloudstream"
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
token: ${{ steps.generate_token.outputs.token }}
|
||||
- name: Install dependencies
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="CompilerConfiguration">
|
||||
<bytecodeTargetLevel target="11" />
|
||||
<bytecodeTargetLevel target="17" />
|
||||
</component>
|
||||
</project>
|
||||
</project>
|
|
@ -4,17 +4,16 @@
|
|||
<component name="GradleSettings">
|
||||
<option name="linkedExternalProjectsSettings">
|
||||
<GradleProjectSettings>
|
||||
<option name="delegatedBuild" value="true" />
|
||||
<option name="testRunner" value="GRADLE" />
|
||||
<option name="distributionType" value="DEFAULT_WRAPPED" />
|
||||
<option name="externalProjectPath" value="$PROJECT_DIR$" />
|
||||
<option name="gradleJvm" value="11" />
|
||||
<option name="gradleJvm" value="#GRADLE_LOCAL_JAVA_HOME" />
|
||||
<option name="modules">
|
||||
<set>
|
||||
<option value="$PROJECT_DIR$" />
|
||||
<option value="$PROJECT_DIR$/app" />
|
||||
<option value="$PROJECT_DIR$/library" />
|
||||
</set>
|
||||
</option>
|
||||
<option name="resolveExternalAnnotations" value="false" />
|
||||
</GradleProjectSettings>
|
||||
</option>
|
||||
</component>
|
||||
|
|
10
README.md
10
README.md
|
@ -9,15 +9,11 @@
|
|||
+ **AdFree**, No ads whatsoever
|
||||
+ No tracking/analytics
|
||||
+ Bookmarks
|
||||
+ Download and stream movies, tv-shows and anime
|
||||
+ Phone and TV support
|
||||
+ Chromecast
|
||||
|
||||
### Screenshots:
|
||||
|
||||
<img src="./.github/home.jpg" height="400"/><img src="./.github/search.jpg" height="400"/><img src="./.github/downloads.jpg" height="400"/><img src="./.github/results.jpg" height="400"/>
|
||||
<img src="./.github/player.jpg" height="200"/>
|
||||
+ Extension system for personal customization
|
||||
|
||||
### Supported languages:
|
||||
<a href="https://hosted.weblate.org/engage/cloudstream/">
|
||||
<img src="https://hosted.weblate.org/widgets/cloudstream/-/app/multi-auto.svg" alt="Translation status" />
|
||||
</a>
|
||||
</a>
|
||||
|
|
|
@ -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,25 +1,27 @@
|
|||
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.kotlin.gradle.plugin.mpp.pm20.util.archivesName
|
||||
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.net.URL
|
||||
|
||||
plugins {
|
||||
id("com.android.application")
|
||||
id("com.google.devtools.ksp")
|
||||
id("kotlin-android")
|
||||
id("kotlin-kapt")
|
||||
id("kotlin-android-extensions")
|
||||
id("org.jetbrains.dokka")
|
||||
}
|
||||
|
||||
val tmpFilePath = System.getProperty("user.home") + "/work/_temp/keystore/"
|
||||
val prereleaseStoreFile: File? = File(tmpFilePath).listFiles()?.first()
|
||||
var isLibraryDebug = false
|
||||
|
||||
fun String.execute() = ByteArrayOutputStream().use { baot ->
|
||||
if (project.exec {
|
||||
workingDir = projectDir
|
||||
commandLine = this@execute.split(Regex("\\s"))
|
||||
standardOutput = baot
|
||||
}.exitValue == 0)
|
||||
}.exitValue == 0)
|
||||
String(baot.toByteArray()).trim()
|
||||
else null
|
||||
}
|
||||
|
@ -28,6 +30,18 @@ android {
|
|||
testOptions {
|
||||
unitTests.isReturnDefaultValues = true
|
||||
}
|
||||
|
||||
viewBinding {
|
||||
enable = true
|
||||
}
|
||||
|
||||
/* disable this for now
|
||||
externalNativeBuild {
|
||||
cmake {
|
||||
path("CMakeLists.txt")
|
||||
}
|
||||
}*/
|
||||
|
||||
signingConfigs {
|
||||
create("prerelease") {
|
||||
if (prereleaseStoreFile != null) {
|
||||
|
@ -39,33 +53,44 @@ android {
|
|||
}
|
||||
}
|
||||
|
||||
compileSdk = 33
|
||||
buildToolsVersion = "30.0.3"
|
||||
compileSdk = 34
|
||||
buildToolsVersion = "34.0.0"
|
||||
|
||||
defaultConfig {
|
||||
applicationId = "com.lagradost.cloudstream3"
|
||||
minSdk = 21
|
||||
targetSdk = 33
|
||||
|
||||
versionCode = 57
|
||||
versionName = "4.0.0"
|
||||
targetSdk = 33 /* Android 14 is Fu*ked
|
||||
^ https://developer.android.com/about/versions/14/behavior-changes-14#safer-dynamic-code-loading*/
|
||||
versionCode = 63
|
||||
versionName = "4.3.2"
|
||||
|
||||
resValue("string", "app_version", "${defaultConfig.versionName}${versionNameSuffix ?: ""}")
|
||||
|
||||
resValue("string", "commit_hash", "git rev-parse --short HEAD".execute() ?: "")
|
||||
|
||||
resValue("bool", "is_prerelease", "false")
|
||||
|
||||
// Reads local.properties
|
||||
val localProperties = gradleLocalProperties(rootDir)
|
||||
|
||||
buildConfigField(
|
||||
"long",
|
||||
"BUILD_DATE",
|
||||
"${System.currentTimeMillis()}"
|
||||
)
|
||||
buildConfigField(
|
||||
"String",
|
||||
"BUILDDATE",
|
||||
"new java.text.SimpleDateFormat(\"yyyy-MM-dd HH:mm\").format(new java.util.Date(" + System.currentTimeMillis() + "L));"
|
||||
"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"
|
||||
|
||||
kapt {
|
||||
includeCompileClasspath = true
|
||||
ksp {
|
||||
arg("room.schemaLocation", "$projectDir/schemas")
|
||||
arg("exportSchema", "true")
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -74,14 +99,22 @@ android {
|
|||
isDebuggable = false
|
||||
isMinifyEnabled = false
|
||||
isShrinkResources = false
|
||||
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
|
||||
proguardFiles(
|
||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||
"proguard-rules.pro"
|
||||
)
|
||||
}
|
||||
debug {
|
||||
isLibraryDebug = true
|
||||
isDebuggable = true
|
||||
applicationIdSuffix = ".debug"
|
||||
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
|
||||
proguardFiles(
|
||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||
"proguard-rules.pro"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
flavorDimensions.add("state")
|
||||
productFlavors {
|
||||
create("stable") {
|
||||
|
@ -98,20 +131,22 @@ android {
|
|||
versionCode = (System.currentTimeMillis() / 60000).toInt()
|
||||
}
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
isCoreLibraryDesugaringEnabled = true
|
||||
|
||||
sourceCompatibility = JavaVersion.VERSION_1_8
|
||||
targetCompatibility = JavaVersion.VERSION_1_8
|
||||
}
|
||||
kotlinOptions {
|
||||
jvmTarget = "1.8"
|
||||
freeCompilerArgs = listOf("-Xjvm-default=compatibility")
|
||||
}
|
||||
|
||||
lint {
|
||||
abortOnError = false
|
||||
checkReleaseBuilds = false
|
||||
}
|
||||
|
||||
buildFeatures {
|
||||
buildConfig = true
|
||||
}
|
||||
|
||||
namespace = "com.lagradost.cloudstream3"
|
||||
}
|
||||
|
||||
|
@ -120,122 +155,124 @@ repositories {
|
|||
}
|
||||
|
||||
dependencies {
|
||||
implementation("com.google.android.mediahome:video:1.0.0")
|
||||
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")
|
||||
// Testing
|
||||
testImplementation("junit:junit:4.13.2")
|
||||
androidTestImplementation("androidx.test.ext:junit:1.1.3")
|
||||
androidTestImplementation("androidx.test.espresso:espresso-core:3.4.0")
|
||||
testImplementation("org.json:json:20240303")
|
||||
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
|
||||
// implementation("org.jsoup:jsoup:1.13.1")
|
||||
implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.13.1")
|
||||
|
||||
implementation("androidx.preference:preference-ktx:1.2.0")
|
||||
|
||||
implementation("com.github.bumptech.glide:glide:4.13.1")
|
||||
kapt("com.github.bumptech.glide:compiler:4.13.1")
|
||||
implementation("com.github.bumptech.glide:okhttp3-integration:4.13.0")
|
||||
// Android Core & Lifecycle
|
||||
implementation("androidx.core:core-ktx:1.12.0")
|
||||
implementation("androidx.appcompat:appcompat:1.6.1")
|
||||
implementation("androidx.navigation:navigation-ui-ktx:2.7.7")
|
||||
implementation("androidx.lifecycle:lifecycle-livedata-ktx:2.7.0")
|
||||
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.7.0")
|
||||
implementation("androidx.navigation:navigation-fragment-ktx:2.7.7")
|
||||
|
||||
// Design & UI
|
||||
implementation("jp.wasabeef:glide-transformations:4.3.0")
|
||||
|
||||
implementation("androidx.preference:preference-ktx:1.2.1")
|
||||
implementation("com.google.android.material:material:1.11.0")
|
||||
implementation("androidx.constraintlayout:constraintlayout:2.1.4")
|
||||
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
|
||||
implementation("com.google.android.exoplayer:exoplayer:2.18.2")
|
||||
implementation("com.google.android.exoplayer:extension-cast:2.18.2")
|
||||
implementation("com.google.android.exoplayer:extension-mediasession:2.18.2")
|
||||
implementation("com.google.android.exoplayer:extension-okhttp:2.18.2")
|
||||
// For KSP -> Official Annotation Processors are Not Yet Supported for KSP
|
||||
ksp("dev.zacsweers.autoservice:auto-service-ksp:1.1.0")
|
||||
implementation("com.google.guava:guava:32.1.3-android")
|
||||
implementation("dev.zacsweers.autoservice:auto-service-ksp:1.1.0")
|
||||
|
||||
//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
|
||||
implementation("ch.acra:acra-core:5.8.4")
|
||||
implementation("ch.acra:acra-toast:5.8.4")
|
||||
// PlayBack
|
||||
implementation("com.jaredrummler:colorpicker:1.1.0") // Subtitle Color Picker
|
||||
implementation("com.github.recloudstream:media-ffmpeg:1.1.0") // Custom FF-MPEG Lib for Audio Codecs
|
||||
implementation("com.github.TeamNewPipe.NewPipeExtractor:NewPipeExtractor:6dc25f7b97") /* 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")
|
||||
//either for java sources:
|
||||
annotationProcessor("com.google.auto.service:auto-service:1.0")
|
||||
//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")
|
||||
// Crash Reports (AcraApplication.kt)
|
||||
implementation("ch.acra:acra-core:5.11.3")
|
||||
implementation("ch.acra:acra-toast:5.11.3")
|
||||
|
||||
// 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("com.github.discord:OverlappingPanels:0.1.5") // Gestures
|
||||
implementation ("androidx.biometric:biometric:1.2.0-alpha05") // Fingerprint Authentication
|
||||
implementation("com.github.rubensousa:previewseekbar-media3:1.1.1.0") // SeekBar Preview
|
||||
|
||||
// used for subtitle decoding https://github.com/albfernandez/juniversalchardet
|
||||
implementation("com.github.albfernandez:juniversalchardet:2.4.0")
|
||||
// Extensions & Other Libs
|
||||
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
|
||||
//implementation("com.github.HaarigerHarald:android-youtubeExtractor:master-SNAPSHOT")
|
||||
// Downloading & Networking
|
||||
implementation("androidx.work:work-runtime:2.9.0")
|
||||
implementation("androidx.work:work-runtime-ktx:2.9.0")
|
||||
implementation("com.github.Blatzar:NiceHttp:0.4.11") // HTTP Lib
|
||||
|
||||
// newpipe yt taken from https://github.com/TeamNewPipe/NewPipe/blob/dev/app/build.gradle#L190
|
||||
implementation("com.github.TeamNewPipe:NewPipeExtractor:9ffdd0948b2ecd82655f5ff2a3e127b2b7695d5b")
|
||||
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")
|
||||
implementation(project(":library") {
|
||||
this.extra.set("isDebug", isLibraryDebug)
|
||||
})
|
||||
}
|
||||
|
||||
tasks.register("androidSourcesJar", Jar::class) {
|
||||
tasks.register<Jar>("androidSourcesJar") {
|
||||
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
|
||||
tasks.register("makeJar", Copy::class) {
|
||||
from("build/intermediates/compile_app_classes_jar/prereleaseDebug")
|
||||
into("build")
|
||||
include("classes.jar")
|
||||
dependsOn("build")
|
||||
tasks.register<Copy>("copyJar") {
|
||||
from(
|
||||
"build/intermediates/compile_app_classes_jar/prereleaseDebug",
|
||||
"../library/build/libs"
|
||||
)
|
||||
into("build/app-classes")
|
||||
include("classes.jar", "library-jvm*.jar")
|
||||
// Remove the version
|
||||
rename("library-jvm.*.jar", "library-jvm.jar")
|
||||
}
|
||||
|
||||
// Merge the app classes and the library classes into classes.jar
|
||||
tasks.register<Jar>("makeJar") {
|
||||
dependsOn(tasks.getByName("copyJar"))
|
||||
from(
|
||||
zipTree("build/app-classes/classes.jar"),
|
||||
zipTree("build/app-classes/library-jvm.jar")
|
||||
)
|
||||
destinationDirectory.set(layout.buildDirectory)
|
||||
archivesName = "classes"
|
||||
}
|
||||
|
||||
tasks.withType<KotlinCompile> {
|
||||
kotlinOptions {
|
||||
jvmTarget = "1.8"
|
||||
freeCompilerArgs = listOf("-Xjvm-default=all-compatibility")
|
||||
}
|
||||
}
|
||||
|
||||
tasks.withType<DokkaTask>().configureEach {
|
||||
|
@ -248,6 +285,7 @@ tasks.withType<DokkaTask>().configureEach {
|
|||
|
||||
// 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"))
|
||||
|
||||
// Suffix which is used to append the line number to the URL. Use #L for GitHub
|
||||
remoteLineSuffix.set("#L")
|
||||
}
|
||||
|
|
|
@ -1,6 +1,33 @@
|
|||
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.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.TestingUtils
|
||||
import kotlinx.coroutines.runBlocking
|
||||
|
@ -8,16 +35,23 @@ import org.junit.Assert
|
|||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
|
||||
/**
|
||||
* Instrumented test, which will execute on an Android device.
|
||||
*
|
||||
* 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)
|
||||
class ExampleInstrumentedTest {
|
||||
private fun getAllProviders(): List<MainAPI> {
|
||||
private fun getAllProviders(): Array<MainAPI> {
|
||||
println("Providers: ${APIHolder.allProviders.size}")
|
||||
return APIHolder.allProviders //.filter { !it.usesWebView }
|
||||
return APIHolder.allProviders.toTypedArray() //.filter { !it.usesWebView }
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -26,6 +60,76 @@ class ExampleInstrumentedTest {
|
|||
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
|
||||
@Throws(AssertionError::class)
|
||||
fun providerCorrectData() {
|
||||
|
@ -49,7 +153,7 @@ class ExampleInstrumentedTest {
|
|||
@Test
|
||||
fun providerCorrectHomepage() {
|
||||
runBlocking {
|
||||
getAllProviders().amap { api ->
|
||||
getAllProviders().toList().amap { api ->
|
||||
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.READ_EXTERNAL_STORAGE" /> <!-- Downloads -->
|
||||
<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.MODIFY_AUDIO_SETTINGS" /> <!-- Used for player vertical slide -->
|
||||
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" /> <!-- Used for app update -->
|
||||
|
@ -14,8 +14,14 @@
|
|||
<uses-permission android:name="com.android.providers.tv.permission.WRITE_EPG_DATA" /> <!-- Used for Android TV watch next -->
|
||||
<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.USE_BIOMETRIC" />
|
||||
<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" />
|
||||
|
||||
<!-- <permission android:name="android.permission.QUERY_ALL_PACKAGES" /> <!– Used for getting if vlc is installed –> -->
|
||||
<!-- Fixes android tv fuckery -->
|
||||
<uses-feature
|
||||
android:name="android.hardware.touchscreen"
|
||||
|
@ -35,9 +41,11 @@
|
|||
<application
|
||||
android:name=".AcraApplication"
|
||||
android:allowBackup="true"
|
||||
android:enableOnBackInvokedCallback="true"
|
||||
android:appCategory="video"
|
||||
android:banner="@mipmap/ic_banner"
|
||||
android:fullBackupContent="@xml/backup_descriptor"
|
||||
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:largeHeap="true"
|
||||
|
@ -45,7 +53,7 @@
|
|||
android:supportsRtl="true"
|
||||
android:theme="@style/AppTheme"
|
||||
android:usesCleartextTraffic="true"
|
||||
tools:targetApi="o">
|
||||
tools:targetApi="tiramisu">
|
||||
|
||||
<meta-data
|
||||
android:name="com.google.android.gms.cast.framework.OPTIONS_PROVIDER_CLASS_NAME"
|
||||
|
@ -61,7 +69,9 @@
|
|||
android:exported="true"
|
||||
android:resizeableActivity="true"
|
||||
android:screenOrientation="userLandscape"
|
||||
android:supportsPictureInPicture="true">
|
||||
android:supportsPictureInPicture="true"
|
||||
android:taskAffinity="com.lagradost.cloudstream3.downloadedplayer"
|
||||
android:launchMode="singleTask">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
|
@ -92,11 +102,15 @@
|
|||
android:launchMode="singleTask"
|
||||
android:resizeableActivity="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" />
|
||||
<!-- cloudstreamplayer://encodedUrl?name=Dune -->
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data android:scheme="cloudstreamplayer" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
@ -151,6 +165,21 @@
|
|||
</intent-filter>
|
||||
</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
|
||||
android:name=".ui.EasterEggMonke"
|
||||
android:exported="true" />
|
||||
|
@ -158,13 +187,14 @@
|
|||
<receiver
|
||||
android:name=".receivers.VideoDownloadRestartReceiver"
|
||||
android:enabled="false"
|
||||
android:exported="true">
|
||||
<intent-filter android:exported="true">
|
||||
android:exported="false">
|
||||
<intent-filter android:exported="false">
|
||||
<action android:name="restart_service" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<service
|
||||
android:foregroundServiceType="dataSync"
|
||||
android:name=".services.VideoDownloadService"
|
||||
android:enabled="true"
|
||||
android:exported="false" />
|
||||
|
@ -174,6 +204,7 @@
|
|||
android:exported="false" />
|
||||
|
||||
<service
|
||||
android:foregroundServiceType="dataSync"
|
||||
android:name=".utils.PackageInstallerService"
|
||||
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,11 +8,12 @@ import android.content.Intent
|
|||
import android.widget.Toast
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import com.google.auto.service.AutoService
|
||||
import com.lagradost.cloudstream3.mvvm.normalSafeApiCall
|
||||
import com.lagradost.cloudstream3.mvvm.suspendSafeApiCall
|
||||
import com.lagradost.cloudstream3.plugins.PluginManager
|
||||
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings
|
||||
import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR
|
||||
import com.lagradost.cloudstream3.ui.settings.Globals.TV
|
||||
import com.lagradost.cloudstream3.ui.settings.Globals.isLayout
|
||||
import com.lagradost.cloudstream3.utils.AppUtils.openBrowser
|
||||
import com.lagradost.cloudstream3.utils.Coroutines.runOnMainThread
|
||||
import com.lagradost.cloudstream3.utils.DataStore.getKey
|
||||
|
@ -32,27 +33,25 @@ import org.acra.sender.ReportSenderFactory
|
|||
import java.io.File
|
||||
import java.io.FileNotFoundException
|
||||
import java.io.PrintStream
|
||||
import java.lang.Exception
|
||||
import java.lang.ref.WeakReference
|
||||
import kotlin.concurrent.thread
|
||||
import kotlin.system.exitProcess
|
||||
|
||||
|
||||
class CustomReportSender : ReportSender {
|
||||
// Sends all your crashes to google forms
|
||||
override fun send(context: Context, errorContent: CrashReportData) {
|
||||
println("Sending report")
|
||||
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(
|
||||
"entry.753293084" to errorContent.toJSON()
|
||||
"entry.1993829403" to errorContent.toJSON()
|
||||
)
|
||||
|
||||
thread { // to not run it on main thread
|
||||
runBlocking {
|
||||
suspendSafeApiCall {
|
||||
val post = app.post(url, data = data)
|
||||
println("Report response: $post")
|
||||
app.post(url, data = data)
|
||||
//println("Report response: $post")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -65,7 +64,6 @@ class CustomReportSender : ReportSender {
|
|||
}
|
||||
}
|
||||
|
||||
@AutoService(ReportSenderFactory::class)
|
||||
class CustomSenderFactory : ReportSenderFactory {
|
||||
override fun create(context: Context, config: CoreConfiguration): ReportSender {
|
||||
return CustomReportSender()
|
||||
|
@ -104,12 +102,17 @@ class ExceptionHandler(val errorFile: File, val onError: (() -> Unit)) :
|
|||
}
|
||||
|
||||
class AcraApplication : Application() {
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
Thread.setDefaultUncaughtExceptionHandler(ExceptionHandler(filesDir.resolve("last_error")) {
|
||||
//NativeCrashHandler.initCrashHandler()
|
||||
ExceptionHandler(filesDir.resolve("last_error")) {
|
||||
val intent = context!!.packageManager.getLaunchIntentForPackage(context!!.packageName)
|
||||
startActivity(Intent.makeRestartActivityTask(intent!!.component))
|
||||
})
|
||||
}.also {
|
||||
exceptionHandler = it
|
||||
Thread.setDefaultUncaughtExceptionHandler(it)
|
||||
}
|
||||
}
|
||||
|
||||
override fun attachBaseContext(base: Context?) {
|
||||
|
@ -121,10 +124,10 @@ class AcraApplication : Application() {
|
|||
buildConfigClass = BuildConfig::class.java
|
||||
reportFormat = StringFormat.JSON
|
||||
|
||||
reportContent = arrayOf(
|
||||
reportContent = listOf(
|
||||
ReportField.BUILD_CONFIG, ReportField.USER_CRASH_DATE,
|
||||
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
|
||||
|
@ -137,6 +140,8 @@ class AcraApplication : Application() {
|
|||
}
|
||||
|
||||
companion object {
|
||||
var exceptionHandler: ExceptionHandler? = null
|
||||
|
||||
/** Use to get activity from Context */
|
||||
tailrec fun Context.getActivity(): Activity? = this as? Activity
|
||||
?: (this as? ContextWrapper)?.baseContext?.getActivity()
|
||||
|
@ -148,6 +153,14 @@ class AcraApplication : Application() {
|
|||
_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? {
|
||||
return context?.removeKeys(folder)
|
||||
}
|
||||
|
@ -199,10 +212,9 @@ class AcraApplication : Application() {
|
|||
fun openBrowser(url: String, activity: FragmentActivity?) {
|
||||
openBrowser(
|
||||
url,
|
||||
isTvSettings(),
|
||||
isLayout(TV or EMULATOR),
|
||||
activity?.supportFragmentManager?.fragments?.lastOrNull()
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,9 +7,13 @@ import android.content.Context
|
|||
import android.content.pm.PackageManager
|
||||
import android.content.res.Resources
|
||||
import android.os.Build
|
||||
import android.util.DisplayMetrics
|
||||
import android.util.Log
|
||||
import android.view.*
|
||||
import android.widget.TextView
|
||||
import android.view.Gravity
|
||||
import android.view.KeyEvent
|
||||
import android.view.View
|
||||
import android.view.View.NO_ID
|
||||
import android.view.ViewGroup
|
||||
import android.widget.Toast
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
|
@ -18,15 +22,21 @@ import androidx.annotation.StringRes
|
|||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.appcompat.widget.SearchView
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.view.children
|
||||
import androidx.preference.PreferenceManager
|
||||
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.removeKey
|
||||
import com.lagradost.cloudstream3.MainActivity.Companion.resumeApps
|
||||
import com.lagradost.cloudstream3.databinding.ToastBinding
|
||||
import com.lagradost.cloudstream3.mvvm.logError
|
||||
import com.lagradost.cloudstream3.ui.player.PlayerEventType
|
||||
import com.lagradost.cloudstream3.ui.result.ResultFragment
|
||||
import com.lagradost.cloudstream3.ui.result.UiText
|
||||
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.updateTv
|
||||
import com.lagradost.cloudstream3.ui.settings.Globals.updateTv
|
||||
import com.lagradost.cloudstream3.utils.AppUtils.isRtl
|
||||
import com.lagradost.cloudstream3.utils.DataStoreHelper
|
||||
import com.lagradost.cloudstream3.utils.Event
|
||||
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.toPx
|
||||
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 {
|
||||
|
||||
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
|
||||
fun Activity?.getCastSession(): CastSession? {
|
||||
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 canShowPipMode: Boolean = false
|
||||
|
@ -53,9 +99,32 @@ object CommonActivity {
|
|||
var playerEventListener: ((PlayerEventType) -> Unit)? = null
|
||||
var keyEventListener: ((Pair<KeyEvent?, Boolean>) -> Boolean)? = null
|
||||
|
||||
private 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) {
|
||||
if (act == null) return
|
||||
text.asStringNull(act)?.let {
|
||||
|
@ -86,25 +155,19 @@ object CommonActivity {
|
|||
} catch (e: Exception) {
|
||||
logError(e)
|
||||
}
|
||||
|
||||
try {
|
||||
val inflater =
|
||||
act.getSystemService(AppCompatActivity.LAYOUT_INFLATER_SERVICE) as LayoutInflater
|
||||
|
||||
val layout: View = inflater.inflate(
|
||||
R.layout.toast,
|
||||
act.findViewById<View>(R.id.toast_layout_root) as ViewGroup?
|
||||
)
|
||||
|
||||
val text = layout.findViewById(R.id.text) as TextView
|
||||
text.text = message.trim()
|
||||
val binding = ToastBinding.inflate(act.layoutInflater)
|
||||
binding.text.text = message.trim()
|
||||
|
||||
// custom toasts are deprecated and won't appear when cs3 sets minSDK to api30 (A11)
|
||||
val toast = Toast(act)
|
||||
toast.setGravity(Gravity.CENTER_HORIZONTAL or Gravity.BOTTOM, 0, 5.toPx)
|
||||
toast.duration = duration ?: Toast.LENGTH_SHORT
|
||||
toast.view = layout
|
||||
//https://github.com/PureWriter/ToastCompat
|
||||
toast.show()
|
||||
toast.setGravity(Gravity.CENTER_HORIZONTAL or Gravity.BOTTOM, 0, 5.toPx)
|
||||
toast.view = binding.root
|
||||
currentToast = toast
|
||||
toast.show()
|
||||
|
||||
} catch (e: Exception) {
|
||||
logError(e)
|
||||
}
|
||||
|
@ -138,22 +201,25 @@ object CommonActivity {
|
|||
setLocale(this, localeCode)
|
||||
}
|
||||
|
||||
fun init(act: ComponentActivity?) {
|
||||
if (act == null) return
|
||||
fun init(act: Activity) {
|
||||
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://developer.android.com/guide/topics/ui/picture-in-picture
|
||||
canShowPipMode =
|
||||
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
|
||||
act.hasPIPPermission() // CHECK IF FEATURE IS ENABLED IN SETTINGS
|
||||
componentActivity.packageManager.hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE) && // HAS FEATURE, MIGHT BE BLOCKED DUE TO POWER DRAIN
|
||||
componentActivity.hasPIPPermission() // CHECK IF FEATURE IS ENABLED IN SETTINGS
|
||||
|
||||
act.updateLocale()
|
||||
act.updateTv()
|
||||
componentActivity.updateLocale()
|
||||
componentActivity.updateTv()
|
||||
NewPipe.init(DownloaderTestImpl.getInstance())
|
||||
|
||||
for (resumeApp in resumeApps) {
|
||||
resumeApp.launcher =
|
||||
act.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
||||
componentActivity.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
||||
val resultCode = result.resultCode
|
||||
val data = result.data
|
||||
if (resultCode == AppCompatActivity.RESULT_OK && data != null && resumeApp.position != null && resumeApp.duration != null) {
|
||||
|
@ -170,11 +236,11 @@ object CommonActivity {
|
|||
// Ask for notification permissions on Android 13
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU &&
|
||||
ContextCompat.checkSelfPermission(
|
||||
act,
|
||||
componentActivity,
|
||||
Manifest.permission.POST_NOTIFICATIONS
|
||||
) != PackageManager.PERMISSION_GRANTED
|
||||
) {
|
||||
val requestPermissionLauncher = act.registerForActivityResult(
|
||||
val requestPermissionLauncher = componentActivity.registerForActivityResult(
|
||||
ActivityResultContracts.RequestPermission()
|
||||
) { isGranted: Boolean ->
|
||||
Log.d(TAG, "Notification permission: $isGranted")
|
||||
|
@ -222,18 +288,22 @@ object CommonActivity {
|
|||
"AmoledLight" -> R.style.AmoledModeLight
|
||||
"Monet" -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S)
|
||||
R.style.MonetMode else R.style.AppTheme
|
||||
|
||||
else -> R.style.AppTheme
|
||||
}
|
||||
|
||||
val currentOverlayTheme =
|
||||
when (settingsManager.getString(act.getString(R.string.primary_color_key), "Normal")) {
|
||||
"Normal" -> R.style.OverlayPrimaryColorNormal
|
||||
"DandelionYellow" -> R.style.OverlayPrimaryColorDandelionYellow
|
||||
"CarnationPink" -> R.style.OverlayPrimaryColorCarnationPink
|
||||
"Orange" -> R.style.OverlayPrimaryColorOrange
|
||||
"DarkGreen" -> R.style.OverlayPrimaryColorDarkGreen
|
||||
"Maroon" -> R.style.OverlayPrimaryColorMaroon
|
||||
"NavyBlue" -> R.style.OverlayPrimaryColorNavyBlue
|
||||
"Grey" -> R.style.OverlayPrimaryColorGrey
|
||||
"White" -> R.style.OverlayPrimaryColorWhite
|
||||
"CoolBlue" -> R.style.OverlayPrimaryColorCoolBlue
|
||||
"Brown" -> R.style.OverlayPrimaryColorBrown
|
||||
"Purple" -> R.style.OverlayPrimaryColorPurple
|
||||
"Green" -> R.style.OverlayPrimaryColorGreen
|
||||
|
@ -242,10 +312,13 @@ object CommonActivity {
|
|||
"Banana" -> R.style.OverlayPrimaryColorBanana
|
||||
"Party" -> R.style.OverlayPrimaryColorParty
|
||||
"Pink" -> R.style.OverlayPrimaryColorPink
|
||||
"Lavender" -> R.style.OverlayPrimaryColorLavender
|
||||
"Monet" -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S)
|
||||
R.style.OverlayPrimaryColorMonet else R.style.OverlayPrimaryColorNormal
|
||||
|
||||
"Monet2" -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S)
|
||||
R.style.OverlayPrimaryColorMonetTwo else R.style.OverlayPrimaryColorNormal
|
||||
|
||||
else -> R.style.OverlayPrimaryColorNormal
|
||||
}
|
||||
act.theme.applyStyle(currentTheme, true)
|
||||
|
@ -257,55 +330,138 @@ object CommonActivity {
|
|||
) // THEME IS SET BEFORE VIEW IS CREATED TO APPLY THE THEME TO THE MAIN VIEW
|
||||
}
|
||||
|
||||
private fun getNextFocus(
|
||||
act: Activity?,
|
||||
/** because we want closes find, aka when multiple have the same id, we go to parent
|
||||
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?,
|
||||
direction: FocusDirection,
|
||||
depth: Int = 0
|
||||
): Int? {
|
||||
if (view == null || depth >= 10 || act == null) {
|
||||
): View? {
|
||||
// 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
|
||||
}
|
||||
|
||||
val nextId = when (direction) {
|
||||
FocusDirection.Left -> {
|
||||
view.nextFocusLeftId
|
||||
var nextId = when (direction) {
|
||||
FocusDirection.Start -> {
|
||||
if (view.isRtl())
|
||||
view.nextFocusRightId
|
||||
else
|
||||
view.nextFocusLeftId
|
||||
}
|
||||
|
||||
FocusDirection.Up -> {
|
||||
view.nextFocusUpId
|
||||
}
|
||||
FocusDirection.Right -> {
|
||||
view.nextFocusRightId
|
||||
|
||||
FocusDirection.End -> {
|
||||
if (view.isRtl())
|
||||
view.nextFocusLeftId
|
||||
else
|
||||
view.nextFocusRightId
|
||||
}
|
||||
|
||||
FocusDirection.Down -> {
|
||||
view.nextFocusDownId
|
||||
}
|
||||
}
|
||||
|
||||
return if (nextId != -1) {
|
||||
val next = act.findViewById<View?>(nextId)
|
||||
//println("NAME: ${next.accessibilityClassName} | ${next?.isShown}" )
|
||||
|
||||
if (next?.isShown == false) {
|
||||
getNextFocus(act, next, direction, depth + 1)
|
||||
} else {
|
||||
if (depth == 0) {
|
||||
null
|
||||
} else {
|
||||
nextId
|
||||
}
|
||||
}
|
||||
} else {
|
||||
null
|
||||
if (nextId == NO_ID) {
|
||||
// if not specified then use forward id
|
||||
nextId = view.nextFocusForwardId
|
||||
// if view is still not found to next focus then return and let android decide
|
||||
if (nextId == NO_ID)
|
||||
return null
|
||||
}
|
||||
return continueGetNextFocus(root, view, direction, nextId, depth)
|
||||
}
|
||||
|
||||
enum class FocusDirection {
|
||||
Left,
|
||||
Right,
|
||||
Up,
|
||||
Down,
|
||||
}
|
||||
|
||||
fun onKeyDown(act: Activity?, keyCode: Int, event: KeyEvent?) {
|
||||
//println("Keycode: $keyCode")
|
||||
|
@ -328,30 +484,39 @@ object CommonActivity {
|
|||
KeyEvent.KEYCODE_FORWARD, KeyEvent.KEYCODE_D, KeyEvent.KEYCODE_MEDIA_SKIP_FORWARD, KeyEvent.KEYCODE_MEDIA_FAST_FORWARD -> {
|
||||
PlayerEventType.SeekForward
|
||||
}
|
||||
|
||||
KeyEvent.KEYCODE_A, KeyEvent.KEYCODE_MEDIA_SKIP_BACKWARD, KeyEvent.KEYCODE_MEDIA_REWIND -> {
|
||||
PlayerEventType.SeekBack
|
||||
}
|
||||
|
||||
KeyEvent.KEYCODE_MEDIA_NEXT, KeyEvent.KEYCODE_BUTTON_R1, KeyEvent.KEYCODE_N -> {
|
||||
PlayerEventType.NextEpisode
|
||||
}
|
||||
|
||||
KeyEvent.KEYCODE_MEDIA_PREVIOUS, KeyEvent.KEYCODE_BUTTON_L1, KeyEvent.KEYCODE_B -> {
|
||||
PlayerEventType.PrevEpisode
|
||||
}
|
||||
|
||||
KeyEvent.KEYCODE_MEDIA_PAUSE -> {
|
||||
PlayerEventType.Pause
|
||||
}
|
||||
|
||||
KeyEvent.KEYCODE_MEDIA_PLAY, KeyEvent.KEYCODE_BUTTON_START -> {
|
||||
PlayerEventType.Play
|
||||
}
|
||||
|
||||
KeyEvent.KEYCODE_L, KeyEvent.KEYCODE_NUMPAD_7, KeyEvent.KEYCODE_7 -> {
|
||||
PlayerEventType.Lock
|
||||
}
|
||||
|
||||
KeyEvent.KEYCODE_H, KeyEvent.KEYCODE_MENU -> {
|
||||
PlayerEventType.ToggleHide
|
||||
}
|
||||
|
||||
KeyEvent.KEYCODE_M, KeyEvent.KEYCODE_VOLUME_MUTE -> {
|
||||
PlayerEventType.ToggleMute
|
||||
}
|
||||
|
||||
KeyEvent.KEYCODE_S, KeyEvent.KEYCODE_NUMPAD_9, KeyEvent.KEYCODE_9 -> {
|
||||
PlayerEventType.ShowMirrors
|
||||
}
|
||||
|
@ -359,21 +524,27 @@ object CommonActivity {
|
|||
KeyEvent.KEYCODE_O, KeyEvent.KEYCODE_NUMPAD_8, KeyEvent.KEYCODE_8 -> {
|
||||
PlayerEventType.SearchSubtitlesOnline
|
||||
}
|
||||
|
||||
KeyEvent.KEYCODE_E, KeyEvent.KEYCODE_NUMPAD_3, KeyEvent.KEYCODE_3 -> {
|
||||
PlayerEventType.ShowSpeed
|
||||
}
|
||||
|
||||
KeyEvent.KEYCODE_R, KeyEvent.KEYCODE_NUMPAD_0, KeyEvent.KEYCODE_0 -> {
|
||||
PlayerEventType.Resize
|
||||
}
|
||||
|
||||
KeyEvent.KEYCODE_C, KeyEvent.KEYCODE_NUMPAD_4, KeyEvent.KEYCODE_4 -> {
|
||||
PlayerEventType.SkipOp
|
||||
}
|
||||
|
||||
KeyEvent.KEYCODE_V, KeyEvent.KEYCODE_NUMPAD_5, KeyEvent.KEYCODE_5 -> {
|
||||
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
|
||||
PlayerEventType.PlayPauseToggle
|
||||
}
|
||||
|
||||
else -> null
|
||||
}?.let { playerEvent ->
|
||||
playerEventListener?.invoke(playerEvent)
|
||||
|
@ -386,64 +557,64 @@ object CommonActivity {
|
|||
//}
|
||||
}
|
||||
|
||||
/** overrides focus and custom key events */
|
||||
fun dispatchKeyEvent(act: Activity?, event: KeyEvent?): Boolean? {
|
||||
if (act == null) return null
|
||||
val currentFocus = act.currentFocus
|
||||
|
||||
event?.keyCode?.let { keyCode ->
|
||||
when (event.action) {
|
||||
KeyEvent.ACTION_DOWN -> {
|
||||
if (act.currentFocus != null) {
|
||||
val next = when (keyCode) {
|
||||
KeyEvent.KEYCODE_DPAD_LEFT -> getNextFocus(
|
||||
act,
|
||||
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
|
||||
)
|
||||
if (currentFocus == null || event.action != KeyEvent.ACTION_DOWN) return@let
|
||||
val nextView = when (keyCode) {
|
||||
KeyEvent.KEYCODE_DPAD_LEFT -> getNextFocus(
|
||||
act,
|
||||
currentFocus,
|
||||
FocusDirection.Start
|
||||
)
|
||||
|
||||
else -> null
|
||||
}
|
||||
KeyEvent.KEYCODE_DPAD_RIGHT -> getNextFocus(
|
||||
act,
|
||||
currentFocus,
|
||||
FocusDirection.End
|
||||
)
|
||||
|
||||
if (next != null && next != -1) {
|
||||
val nextView = act.findViewById<View?>(next)
|
||||
if (nextView != null) {
|
||||
nextView.requestFocus()
|
||||
keyEventListener?.invoke(Pair(event, true))
|
||||
return true
|
||||
}
|
||||
}
|
||||
KeyEvent.KEYCODE_DPAD_UP -> getNextFocus(
|
||||
act,
|
||||
currentFocus,
|
||||
FocusDirection.Up
|
||||
)
|
||||
|
||||
when (keyCode) {
|
||||
KeyEvent.KEYCODE_DPAD_CENTER -> {
|
||||
if (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
|
||||
//)
|
||||
}
|
||||
KeyEvent.KEYCODE_DPAD_DOWN -> getNextFocus(
|
||||
act,
|
||||
currentFocus,
|
||||
FocusDirection.Down
|
||||
)
|
||||
|
||||
else -> null
|
||||
}
|
||||
// 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) {
|
||||
return true
|
||||
}
|
||||
|
|
|
@ -50,7 +50,7 @@ class DownloaderTestImpl private constructor(builder: OkHttpClient.Builder) : Do
|
|||
|
||||
companion object {
|
||||
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
|
||||
|
||||
/**
|
||||
|
|
|
@ -9,35 +9,38 @@ import androidx.preference.PreferenceManager
|
|||
import com.fasterxml.jackson.annotation.JsonProperty
|
||||
import com.fasterxml.jackson.databind.DeserializationFeature
|
||||
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.normalSafeApiCall
|
||||
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.aniListApi
|
||||
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.providers.SimklApi
|
||||
import com.lagradost.cloudstream3.ui.player.SubtitleData
|
||||
import com.lagradost.cloudstream3.ui.result.ResultViewModel2
|
||||
import com.lagradost.cloudstream3.utils.*
|
||||
import com.lagradost.cloudstream3.utils.AppUtils.toJson
|
||||
import com.lagradost.cloudstream3.utils.Coroutines.mainWork
|
||||
import com.lagradost.cloudstream3.utils.Coroutines.threadSafeListOf
|
||||
import com.lagradost.nicehttp.RequestBodyTypes
|
||||
import okhttp3.Interceptor
|
||||
import org.mozilla.javascript.Scriptable
|
||||
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
||||
import okhttp3.RequestBody.Companion.toRequestBody
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
import kotlin.math.absoluteValue
|
||||
|
||||
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"
|
||||
|
||||
//val baseHeader = mapOf("User-Agent" to USER_AGENT)
|
||||
val mapper = JsonMapper.builder().addModule(KotlinModule())
|
||||
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false).build()!!
|
||||
|
||||
/**
|
||||
* Defines the constant for the all languages preference, if this is set then it is
|
||||
* the equivalent of all languages being set
|
||||
**/
|
||||
const val AllLanguagesName = "universal"
|
||||
|
||||
//val baseHeader = mapOf("User-Agent" to USER_AGENT)
|
||||
val mapper = JsonMapper.builder().addModule(kotlinModule())
|
||||
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false).build()!!
|
||||
|
||||
object APIHolder {
|
||||
val unixTime: Long
|
||||
get() = System.currentTimeMillis() / 1000L
|
||||
|
@ -50,8 +53,10 @@ object APIHolder {
|
|||
val allProviders = threadSafeListOf<MainAPI>()
|
||||
|
||||
fun initAll() {
|
||||
for (api in allProviders) {
|
||||
api.init()
|
||||
synchronized(allProviders) {
|
||||
for (api in allProviders) {
|
||||
api.init()
|
||||
}
|
||||
}
|
||||
apiMap = null
|
||||
}
|
||||
|
@ -64,27 +69,35 @@ object APIHolder {
|
|||
var apiMap: Map<String, Int>? = null
|
||||
|
||||
fun addPluginMapping(plugin: MainAPI) {
|
||||
apis = apis + plugin
|
||||
synchronized(apis) {
|
||||
apis = apis + plugin
|
||||
}
|
||||
initMap(true)
|
||||
}
|
||||
|
||||
fun removePluginMapping(plugin: MainAPI) {
|
||||
apis = apis.filter { it != plugin }
|
||||
synchronized(apis) {
|
||||
apis = apis.filter { it != plugin }
|
||||
}
|
||||
initMap(true)
|
||||
}
|
||||
|
||||
private fun initMap(forcedUpdate: Boolean = false) {
|
||||
if (apiMap == null || forcedUpdate)
|
||||
apiMap = apis.mapIndexed { index, api -> api.name to index }.toMap()
|
||||
synchronized(apis) {
|
||||
if (apiMap == null || forcedUpdate)
|
||||
apiMap = apis.mapIndexed { index, api -> api.name to index }.toMap()
|
||||
}
|
||||
}
|
||||
|
||||
fun getApiFromNameNull(apiName: String?): MainAPI? {
|
||||
if (apiName == null) return null
|
||||
synchronized(allProviders) {
|
||||
initMap()
|
||||
return apiMap?.get(apiName)?.let { apis.getOrNull(it) }
|
||||
// Leave the ?. null check, it can crash regardless
|
||||
?: allProviders.firstOrNull { it.name == apiName }
|
||||
synchronized(apis) {
|
||||
return apiMap?.get(apiName)?.let { apis.getOrNull(it) }
|
||||
// Leave the ?. null check, it can crash regardless
|
||||
?: allProviders.firstOrNull { it.name == apiName }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -104,7 +117,9 @@ object APIHolder {
|
|||
}
|
||||
|
||||
fun LoadResponse.getId(): Int {
|
||||
return getLoadResponseIdFromUrl(url, apiName)
|
||||
// this fixes an issue with outdated api as getLoadResponseIdFromUrl might be fucked
|
||||
return (if (this is ResultViewModel2.LoadResponseFromSearch) this.id else null)
|
||||
?: getLoadResponseIdFromUrl(url, apiName)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -164,10 +179,17 @@ object APIHolder {
|
|||
|
||||
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.
|
||||
* 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 types Optional parameter to narrow down the scope to Movies, TV, etc. See TrackerType.getTypes()
|
||||
|
@ -176,7 +198,8 @@ object APIHolder {
|
|||
suspend fun getTracker(
|
||||
titles: List<String>,
|
||||
types: Set<TrackerType>?,
|
||||
year: Int?
|
||||
year: Int?,
|
||||
lessAccurate: Boolean
|
||||
): Tracker? {
|
||||
return try {
|
||||
require(titles.isNotEmpty()) { "titles must no be empty when calling getTracker" }
|
||||
|
@ -184,30 +207,75 @@ object APIHolder {
|
|||
val mainTitle = titles[0]
|
||||
val search =
|
||||
trackerCache[mainTitle]
|
||||
?: app.get("https://api.consumet.org/meta/anilist/$mainTitle")
|
||||
.parsedSafe<AniSearch>()?.also {
|
||||
trackerCache[mainTitle] = it
|
||||
} ?: return null
|
||||
?: searchAnilist(mainTitle)?.also {
|
||||
trackerCache[mainTitle] = it
|
||||
} ?: return null
|
||||
|
||||
val res = search.results?.find { media ->
|
||||
val matchingYears = year == null || media.releaseDate == year
|
||||
val res = search.data?.page?.media?.find { media ->
|
||||
val matchingYears = year == null || media.seasonYear == year
|
||||
val matchingTitles = media.title?.let { title ->
|
||||
titles.any { userTitle ->
|
||||
title.isMatchingTitles(userTitle)
|
||||
}
|
||||
} ?: false
|
||||
|
||||
val matchingTypes = types?.any { it.name.equals(media.type, true) } == true
|
||||
matchingTitles && matchingTypes && matchingYears
|
||||
val matchingTypes = types?.any { it.name.equals(media.format, true) } == true
|
||||
if (lessAccurate) matchingTitles || matchingTypes && matchingYears else matchingTitles && matchingTypes && matchingYears
|
||||
} ?: 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) {
|
||||
logError(t)
|
||||
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> {
|
||||
//val settingsManager = PreferenceManager.getDefaultSharedPreferences(this)
|
||||
|
@ -215,7 +283,7 @@ object APIHolder {
|
|||
val hashSet = HashSet<String>()
|
||||
val activeLangs = getApiProviderLangSettings()
|
||||
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 })
|
||||
|
||||
/*val set = settingsManager.getStringSet(
|
||||
|
@ -314,8 +382,9 @@ object APIHolder {
|
|||
} ?: default
|
||||
val langs = this.getApiProviderLangSettings()
|
||||
val hasUniversal = langs.contains(AllLanguagesName)
|
||||
val allApis = apis.filter { hasUniversal || langs.contains(it.lang) }
|
||||
.filter { api -> api.hasMainPage || !hasHomePageIsRequired }
|
||||
val allApis = synchronized(apis) {
|
||||
apis.filter { api -> (hasUniversal || langs.contains(api.lang)) && (api.hasMainPage || !hasHomePageIsRequired) }
|
||||
}
|
||||
return if (currentPrefMedia.isEmpty()) {
|
||||
allApis
|
||||
} else {
|
||||
|
@ -677,8 +746,6 @@ fun base64Encode(array: ByteArray): String {
|
|||
}
|
||||
}
|
||||
|
||||
class ErrorLoadingException(message: String? = null) : Exception(message)
|
||||
|
||||
fun MainAPI.fixUrlNull(url: String?): String? {
|
||||
if (url.isNullOrEmpty()) {
|
||||
return null
|
||||
|
@ -736,6 +803,7 @@ fun fixTitle(str: String): String {
|
|||
.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.
|
||||
* Make sure you get the scope using: val scope: Scriptable = rhino.initSafeStandardObjects()
|
||||
|
@ -798,7 +866,25 @@ enum class TvType(value: Int?) {
|
|||
AsianDrama(9),
|
||||
Live(10),
|
||||
NSFW(11),
|
||||
Others(12)
|
||||
Others(12),
|
||||
Music(13),
|
||||
AudioBook(14),
|
||||
|
||||
/** Wont load the built in player, make your own interaction */
|
||||
CustomMedia(15),
|
||||
}
|
||||
|
||||
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
|
||||
|
@ -1115,14 +1201,16 @@ interface LoadResponse {
|
|||
var syncData: MutableMap<String, String>
|
||||
var posterHeaders: Map<String, String>?
|
||||
var backgroundPosterUrl: String?
|
||||
var contentRating: String?
|
||||
|
||||
companion object {
|
||||
private val malIdPrefix = malApi.idPrefix
|
||||
private val aniListIdPrefix = aniListApi.idPrefix
|
||||
private val simklIdPrefix = simklApi.idPrefix
|
||||
var isTrailersEnabled = true
|
||||
|
||||
fun LoadResponse.isMovie(): Boolean {
|
||||
return this.type.isMovieType()
|
||||
return this.type.isMovieType() || this is MovieLoadResponse
|
||||
}
|
||||
|
||||
@JvmName("addActorNames")
|
||||
|
@ -1140,6 +1228,20 @@ interface LoadResponse {
|
|||
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")
|
||||
fun LoadResponse.addActors(actors: List<Actor>?) {
|
||||
this.actors = actors?.map { actor -> ActorData(actor) }
|
||||
|
@ -1153,12 +1255,32 @@ interface LoadResponse {
|
|||
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?) {
|
||||
this.syncData[malIdPrefix] = (id ?: return).toString()
|
||||
this.addSimklId(SimklApi.Companion.SyncServices.Mal, id.toString())
|
||||
}
|
||||
|
||||
fun LoadResponse.addAniListId(id: Int?) {
|
||||
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?) {
|
||||
|
@ -1240,6 +1362,7 @@ interface LoadResponse {
|
|||
|
||||
fun LoadResponse.addImdbId(id: String?) {
|
||||
// TODO add imdb sync
|
||||
this.addSimklId(SimklApi.Companion.SyncServices.Imdb, id)
|
||||
}
|
||||
|
||||
fun LoadResponse.addTrackId(id: String?) {
|
||||
|
@ -1252,6 +1375,7 @@ interface LoadResponse {
|
|||
|
||||
fun LoadResponse.addTMDbId(id: String?) {
|
||||
// TODO add TMDb sync
|
||||
this.addSimklId(SimklApi.Companion.SyncServices.Tmdb, id)
|
||||
}
|
||||
|
||||
fun LoadResponse.addRating(text: String?) {
|
||||
|
@ -1330,11 +1454,24 @@ fun TvType?.isEpisodeBased(): Boolean {
|
|||
return (this == TvType.TvSeries || this == TvType.Anime || this == TvType.AsianDrama)
|
||||
}
|
||||
|
||||
|
||||
data class NextAiring(
|
||||
val episode: Int,
|
||||
val unixTime: Long,
|
||||
)
|
||||
val season: Int? = null,
|
||||
) {
|
||||
/**
|
||||
* Secondary constructor for backwards compatibility without season.
|
||||
* TODO Remove this constructor after there is a new stable release and extensions are updated to support season.
|
||||
*/
|
||||
constructor(
|
||||
episode: Int,
|
||||
unixTime: Long,
|
||||
) : this (
|
||||
episode,
|
||||
unixTime,
|
||||
null
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param season To be mapped with episode season, not shown in UI if displaySeason is defined
|
||||
|
@ -1352,6 +1489,15 @@ interface EpisodeResponse {
|
|||
var nextAiring: NextAiring?
|
||||
var seasonNames: List<SeasonData>?
|
||||
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")
|
||||
|
@ -1389,7 +1535,55 @@ data class TorrentLoadResponse(
|
|||
override var syncData: MutableMap<String, String> = mutableMapOf(),
|
||||
override var posterHeaders: Map<String, 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(
|
||||
var engName: String? = null,
|
||||
|
@ -1420,6 +1614,7 @@ data class AnimeLoadResponse(
|
|||
override var nextAiring: NextAiring? = null,
|
||||
override var seasonNames: List<SeasonData>? = null,
|
||||
override var backgroundPosterUrl: String? = null,
|
||||
override var contentRating: String? = null,
|
||||
) : LoadResponse, EpisodeResponse {
|
||||
override fun getLatestEpisodes(): Map<DubStatus, Int?> {
|
||||
return episodes.map { (status, episodes) ->
|
||||
|
@ -1431,6 +1626,77 @@ data class AnimeLoadResponse(
|
|||
.takeUnless { it == Int.MIN_VALUE }
|
||||
}.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 +1748,36 @@ data class LiveStreamLoadResponse(
|
|||
override var syncData: MutableMap<String, String> = mutableMapOf(),
|
||||
override var posterHeaders: Map<String, 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(
|
||||
override var name: String,
|
||||
|
@ -1505,7 +1800,36 @@ data class MovieLoadResponse(
|
|||
override var syncData: MutableMap<String, String> = mutableMapOf(),
|
||||
override var posterHeaders: Map<String, 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(
|
||||
name: String,
|
||||
|
@ -1629,6 +1953,7 @@ data class TvSeriesLoadResponse(
|
|||
override var nextAiring: NextAiring? = null,
|
||||
override var seasonNames: List<SeasonData>? = null,
|
||||
override var backgroundPosterUrl: String? = null,
|
||||
override var contentRating: String? = null,
|
||||
) : LoadResponse, EpisodeResponse {
|
||||
override fun getLatestEpisodes(): Map<DubStatus, Int?> {
|
||||
val maxSeason =
|
||||
|
@ -1639,6 +1964,69 @@ data class TvSeriesLoadResponse(
|
|||
.takeUnless { it == Int.MIN_VALUE }
|
||||
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(
|
||||
|
@ -1679,30 +2067,43 @@ data class Tracker(
|
|||
val cover: String? = null,
|
||||
)
|
||||
|
||||
data class Title(
|
||||
@JsonProperty("romaji") val romaji: String? = null,
|
||||
@JsonProperty("english") val english: String? = null,
|
||||
data class AniSearch(
|
||||
@JsonProperty("data") var data: Data? = Data()
|
||||
) {
|
||||
fun isMatchingTitles(title: String?): Boolean {
|
||||
if (title == null) return false
|
||||
return english.equals(title, true) || romaji.equals(title, true)
|
||||
data class Data(
|
||||
@JsonProperty("Page") var page: Page? = Page()
|
||||
) {
|
||||
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
|
||||
**/
|
||||
|
|
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
|
||||
|
||||
import com.lagradost.cloudstream3.SubtitleFile
|
||||
import com.lagradost.cloudstream3.app
|
||||
import com.lagradost.cloudstream3.base64Decode
|
||||
import com.lagradost.cloudstream3.utils.*
|
||||
|
||||
open class Acefile : ExtractorApi() {
|
||||
|
@ -9,31 +9,35 @@ open class Acefile : ExtractorApi() {
|
|||
override val mainUrl = "https://acefile.co"
|
||||
override val requiresReferer = false
|
||||
|
||||
override suspend fun getUrl(url: String, referer: String?): List<ExtractorLink> {
|
||||
val sources = mutableListOf<ExtractorLink>()
|
||||
app.get(url).document.select("script").map { script ->
|
||||
if (script.data().contains("eval(function(p,a,c,k,e,d)")) {
|
||||
val data = getAndUnpack(script.data())
|
||||
val id = data.substringAfter("{\"id\":\"").substringBefore("\",")
|
||||
val key = data.substringAfter("var nfck=\"").substringBefore("\";")
|
||||
app.get("https://acefile.co/local/$id?key=$key").text.let {
|
||||
base64Decode(
|
||||
it.substringAfter("JSON.parse(atob(\"").substringBefore("\"))")
|
||||
).let { res ->
|
||||
sources.add(
|
||||
ExtractorLink(
|
||||
name,
|
||||
name,
|
||||
res.substringAfter("\"file\":\"").substringBefore("\","),
|
||||
"$mainUrl/",
|
||||
Qualities.Unknown.value,
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return sources
|
||||
override suspend fun getUrl(
|
||||
url: String,
|
||||
referer: String?,
|
||||
subtitleCallback: (SubtitleFile) -> Unit,
|
||||
callback: (ExtractorLink) -> Unit
|
||||
) {
|
||||
val id = "/(?:d|download|player|f|file)/(\\w+)".toRegex().find(url)?.groupValues?.get(1)
|
||||
val script = getAndUnpack(app.get("$mainUrl/player/${id ?: return}").text)
|
||||
val service = """service\s*=\s*['"]([^'"]+)""".toRegex().find(script)?.groupValues?.get(1)
|
||||
val serverUrl = """['"](\S+check&id\S+?)['"]""".toRegex().find(script)?.groupValues?.get(1)
|
||||
?.replace("\"+service+\"", service ?: return)
|
||||
|
||||
val video = app.get(serverUrl ?: return, referer = "$mainUrl/").parsedSafe<Source>()?.data
|
||||
|
||||
callback.invoke(
|
||||
ExtractorLink(
|
||||
this.name,
|
||||
this.name,
|
||||
video ?: return,
|
||||
"",
|
||||
Qualities.Unknown.value,
|
||||
INFER_TYPE
|
||||
)
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
data class Source(
|
||||
val data: String? = null,
|
||||
)
|
||||
|
||||
}
|
|
@ -9,7 +9,7 @@ import java.net.URI
|
|||
|
||||
open class AsianLoad : ExtractorApi() {
|
||||
override var name = "AsianLoad"
|
||||
override var mainUrl = "https://asianembed.io"
|
||||
override var mainUrl = "https://asianhdplay.pro"
|
||||
override val requiresReferer = true
|
||||
|
||||
private val sourceRegex = Regex("""sources:[\W\w]*?file:\s*?["'](.*?)["']""")
|
||||
|
@ -43,4 +43,4 @@ open class AsianLoad : ExtractorApi() {
|
|||
return extractedLinksList
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,7 +4,7 @@ import com.lagradost.cloudstream3.utils.*
|
|||
|
||||
open class ByteShare : ExtractorApi() {
|
||||
override val name = "ByteShare"
|
||||
override val mainUrl = "https://byteshare.net"
|
||||
override val mainUrl = "https://byteshare.to"
|
||||
override val requiresReferer = false
|
||||
|
||||
override suspend fun getUrl(url: String, referer: String?): List<ExtractorLink> {
|
||||
|
@ -20,4 +20,4 @@ open class ByteShare : ExtractorApi() {
|
|||
)
|
||||
return sources
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,13 +1,11 @@
|
|||
package com.lagradost.cloudstream3.extractors
|
||||
|
||||
import com.lagradost.cloudstream3.*
|
||||
import com.lagradost.cloudstream3.utils.Qualities
|
||||
import com.lagradost.cloudstream3.USER_AGENT
|
||||
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.M3u8Helper.Companion.generateM3u8
|
||||
import com.fasterxml.jackson.annotation.JsonProperty
|
||||
import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson
|
||||
import android.util.Log
|
||||
import com.lagradost.cloudstream3.utils.Qualities
|
||||
import java.net.URLDecoder
|
||||
|
||||
open class Cda: ExtractorApi() {
|
||||
|
|
|
@ -0,0 +1,107 @@
|
|||
package com.lagradost.cloudstream3.extractors
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty
|
||||
import com.lagradost.cloudstream3.*
|
||||
import com.lagradost.cloudstream3.extractors.helper.AesHelper.cryptoAESHandler
|
||||
import com.lagradost.cloudstream3.utils.ExtractorApi
|
||||
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||
import com.lagradost.cloudstream3.utils.M3u8Helper
|
||||
|
||||
class Moviesapi : Chillx() {
|
||||
override val name = "Moviesapi"
|
||||
override val mainUrl = "https://w1.moviesapi.club"
|
||||
}
|
||||
|
||||
class Bestx : Chillx() {
|
||||
override val name = "Bestx"
|
||||
override val mainUrl = "https://bestx.stream"
|
||||
}
|
||||
|
||||
class Watchx : Chillx() {
|
||||
override val name = "Watchx"
|
||||
override val mainUrl = "https://watchx.top"
|
||||
}
|
||||
|
||||
open class Chillx : ExtractorApi() {
|
||||
override val name = "Chillx"
|
||||
override val mainUrl = "https://chillx.top"
|
||||
override val requiresReferer = true
|
||||
|
||||
companion object {
|
||||
private var key: String? = null
|
||||
|
||||
suspend fun fetchKey(): String {
|
||||
return if (key != null) {
|
||||
key!!
|
||||
} else {
|
||||
val fetch = app.get("https://raw.githubusercontent.com/rushi-chavan/multi-keys/keys/keys.json").parsedSafe<Keys>()?.key?.get(0) ?: throw ErrorLoadingException("Unable to get key")
|
||||
key = fetch
|
||||
key!!
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("NAME_SHADOWING")
|
||||
override suspend fun getUrl(
|
||||
url: String,
|
||||
referer: String?,
|
||||
subtitleCallback: (SubtitleFile) -> Unit,
|
||||
callback: (ExtractorLink) -> Unit
|
||||
) {
|
||||
val master = Regex("""JScript[\w+]?\s*=\s*'([^']+)""").find(
|
||||
app.get(
|
||||
url,
|
||||
referer = url,
|
||||
).text
|
||||
)?.groupValues?.get(1)
|
||||
val key = fetchKey()
|
||||
val decrypt = cryptoAESHandler(master ?: "", key.toByteArray(), false)?.replace("\\", "") ?: throw ErrorLoadingException("failed to decrypt")
|
||||
val source = Regex(""""?file"?:\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
|
||||
val headers = mapOf(
|
||||
"Accept" to "*/*",
|
||||
"Connection" to "keep-alive",
|
||||
"Sec-Fetch-Dest" to "empty",
|
||||
"Sec-Fetch-Mode" to "cors",
|
||||
"Sec-Fetch-Site" to "cross-site",
|
||||
"Origin" to mainUrl,
|
||||
)
|
||||
|
||||
M3u8Helper.generateM3u8(
|
||||
name,
|
||||
source ?: return,
|
||||
"$mainUrl/",
|
||||
headers = headers
|
||||
).forEach(callback)
|
||||
}
|
||||
|
||||
private fun decodeUnicodeEscape(input: String): String {
|
||||
val regex = Regex("u([0-9a-fA-F]{4})")
|
||||
return regex.replace(input) {
|
||||
it.groupValues[1].toInt(16).toChar().toString()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
data class Keys(
|
||||
@JsonProperty("chillx") val key: List<String>
|
||||
)
|
||||
|
||||
}
|
|
@ -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,13 +7,18 @@ import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson
|
|||
import com.lagradost.cloudstream3.utils.ExtractorApi
|
||||
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||
import com.lagradost.cloudstream3.utils.M3u8Helper.Companion.generateM3u8
|
||||
import com.lagradost.cloudstream3.utils.Qualities
|
||||
import java.net.URL
|
||||
|
||||
class Geodailymotion : Dailymotion() {
|
||||
override val name = "GeoDailymotion"
|
||||
override val mainUrl = "https://geo.dailymotion.com"
|
||||
}
|
||||
|
||||
open class Dailymotion : ExtractorApi() {
|
||||
override val mainUrl = "https://www.dailymotion.com"
|
||||
override val name = "Dailymotion"
|
||||
override val requiresReferer = false
|
||||
private val baseUrl = "https://www.dailymotion.com"
|
||||
|
||||
@Suppress("RegExpSimplifiable")
|
||||
private val videoIdRegex = "^[kx][a-zA-Z0-9]+\$".toRegex()
|
||||
|
@ -27,21 +32,16 @@ open class Dailymotion : ExtractorApi() {
|
|||
callback: (ExtractorLink) -> Unit
|
||||
) {
|
||||
val embedUrl = getEmbedUrl(url) ?: return
|
||||
val doc = app.get(embedUrl).document
|
||||
val req = app.get(embedUrl)
|
||||
val prefix = "window.__PLAYER_CONFIG__ = "
|
||||
val configStr = doc.selectFirst("script:containsData($prefix)")?.data() ?: return
|
||||
val config = tryParseJson<Config>(configStr.substringAfter(prefix)) ?: return
|
||||
val configStr = req.document.selectFirst("script:containsData($prefix)")?.data() ?: return
|
||||
val config = tryParseJson<Config>(configStr.substringAfter(prefix).substringBefore(";").trim()) ?: return
|
||||
val id = getVideoId(embedUrl) ?: return
|
||||
val dmV1st = config.dmInternalData.v1st
|
||||
val dmTs = config.dmInternalData.ts
|
||||
val metaDataUrl =
|
||||
"$mainUrl/player/metadata/video/$id?locale=en&dmV1st=$dmV1st&dmTs=$dmTs&is_native_app=0"
|
||||
val cookies = mapOf(
|
||||
"v1st" to dmV1st,
|
||||
"dmvk" to config.context.dmvk,
|
||||
"ts" to dmTs.toString()
|
||||
)
|
||||
val metaData = app.get(metaDataUrl, referer = embedUrl, cookies = cookies)
|
||||
val embedder = config.context.embedder
|
||||
val metaDataUrl = "$baseUrl/player/metadata/video/$id?embedder=$embedder&locale=en-US&dmV1st=$dmV1st&dmTs=$dmTs&is_native_app=0"
|
||||
val metaData = app.get(metaDataUrl, referer = embedUrl, cookies = req.cookies)
|
||||
.parsedSafe<MetaData>() ?: return
|
||||
metaData.qualities.forEach { (_, video) ->
|
||||
video.forEach {
|
||||
|
@ -51,16 +51,19 @@ open class Dailymotion : ExtractorApi() {
|
|||
}
|
||||
|
||||
private fun getEmbedUrl(url: String): String? {
|
||||
if (url.contains("/embed/")) {
|
||||
return url
|
||||
}
|
||||
val vid = getVideoId(url) ?: return null
|
||||
return "$mainUrl/embed/video/$vid"
|
||||
if (url.contains("/embed/") || url.contains("/video/")) {
|
||||
return url
|
||||
}
|
||||
if (url.contains("geo.dailymotion.com")) {
|
||||
val videoId = url.substringAfter("video=")
|
||||
return "$baseUrl/embed/video/$videoId"
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
private fun getVideoId(url: String): String? {
|
||||
val path = URL(url).path
|
||||
val id = path.substringAfter("video/")
|
||||
val id = path.substringAfter("/video/")
|
||||
if (id.matches(videoIdRegex)) {
|
||||
return id
|
||||
}
|
||||
|
@ -84,13 +87,13 @@ open class Dailymotion : ExtractorApi() {
|
|||
)
|
||||
|
||||
data class InternalData(
|
||||
val ts: Int,
|
||||
val ts: Long,
|
||||
val v1st: String
|
||||
)
|
||||
|
||||
data class Context(
|
||||
@JsonProperty("access_token") val accessToken: String?,
|
||||
val dmvk: String,
|
||||
val embedder: String?,
|
||||
)
|
||||
|
||||
data class MetaData(
|
||||
|
|
|
@ -7,6 +7,10 @@ import com.lagradost.cloudstream3.utils.Qualities
|
|||
import com.lagradost.cloudstream3.utils.getQualityFromName
|
||||
import kotlinx.coroutines.delay
|
||||
|
||||
class Dooood : DoodLaExtractor() {
|
||||
override var mainUrl = "https://dooood.com"
|
||||
}
|
||||
|
||||
class DoodWfExtractor : DoodLaExtractor() {
|
||||
override var mainUrl = "https://dood.wf"
|
||||
}
|
||||
|
@ -58,7 +62,7 @@ open class DoodLaExtractor : ExtractorApi() {
|
|||
val quality = Regex("\\d{3,4}p").find(response0.substringAfter("<title>").substringBefore("</title>"))?.groupValues?.get(0)
|
||||
return listOf(
|
||||
ExtractorLink(
|
||||
trueUrl,
|
||||
this.name,
|
||||
this.name,
|
||||
trueUrl,
|
||||
mainUrl,
|
||||
|
|
|
@ -0,0 +1,27 @@
|
|||
package com.lagradost.cloudstream3.extractors
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty
|
||||
import com.lagradost.cloudstream3.app
|
||||
import com.lagradost.cloudstream3.utils.*
|
||||
import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson
|
||||
|
||||
open class EPlayExtractor : ExtractorApi() {
|
||||
override var name = "EPlay"
|
||||
override var mainUrl = "https://eplayvid.net"
|
||||
override val requiresReferer = true
|
||||
|
||||
override suspend fun getUrl(url: String, referer: String?): List<ExtractorLink>? {
|
||||
val response = app.get(url).document
|
||||
val trueUrl = response.select("source").attr("src")
|
||||
return listOf(
|
||||
ExtractorLink(
|
||||
this.name,
|
||||
this.name,
|
||||
trueUrl,
|
||||
mainUrl,
|
||||
getQualityFromName(""), // this needs to be auto
|
||||
false
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
|
@ -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,40 @@ import com.lagradost.cloudstream3.app
|
|||
import com.lagradost.cloudstream3.utils.*
|
||||
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() {
|
||||
override val mainUrl = "https://moviesm4u.com"
|
||||
override val name = "Moviesm4u"
|
||||
}
|
||||
|
||||
class FileMoonIn : Filesim() {
|
||||
override val mainUrl = "https://filemoon.in"
|
||||
override val name = "FileMoon"
|
||||
}
|
||||
|
||||
class StreamhideTo : Filesim() {
|
||||
override val mainUrl = "https://streamhide.to"
|
||||
override val name = "Streamhide"
|
||||
}
|
||||
|
||||
class StreamhideCom : Filesim() {
|
||||
override var name: String = "Streamhide"
|
||||
override var mainUrl: String = "https://streamhide.com"
|
||||
}
|
||||
|
||||
class Movhide : Filesim() {
|
||||
override var name: String = "Movhide"
|
||||
override var mainUrl: String = "https://movhide.pro"
|
||||
}
|
||||
|
||||
class Ztreamhub : Filesim() {
|
||||
override val mainUrl: String = "https://ztreamhub.com" //Here 'cause works
|
||||
|
@ -23,7 +57,7 @@ class FileMoonSx : Filesim() {
|
|||
open class Filesim : ExtractorApi() {
|
||||
override val name = "Filesim"
|
||||
override val mainUrl = "https://files.im"
|
||||
override val requiresReferer = false
|
||||
override val requiresReferer = true
|
||||
|
||||
override suspend fun getUrl(
|
||||
url: String,
|
||||
|
@ -31,27 +65,19 @@ open class Filesim : ExtractorApi() {
|
|||
subtitleCallback: (SubtitleFile) -> Unit,
|
||||
callback: (ExtractorLink) -> Unit
|
||||
) {
|
||||
val response = app.get(url, referer = mainUrl).document
|
||||
response.select("script[type=text/javascript]").map { script ->
|
||||
if (script.data().contains(Regex("eval\\(function\\(p,a,c,k,e,[rd]"))) {
|
||||
val unpackedscript = getAndUnpack(script.data())
|
||||
val m3u8Regex = Regex("file.\\\"(.*?m3u8.*?)\\\"")
|
||||
val m3u8 = m3u8Regex.find(unpackedscript)?.destructured?.component1() ?: ""
|
||||
if (m3u8.isNotEmpty()) {
|
||||
generateM3u8(
|
||||
name,
|
||||
m3u8,
|
||||
mainUrl
|
||||
).forEach(callback)
|
||||
}
|
||||
}
|
||||
val response = app.get(url, referer = referer)
|
||||
val script = if (!getPacked(response.text).isNullOrEmpty()) {
|
||||
getAndUnpack(response.text)
|
||||
} else {
|
||||
response.document.selectFirst("script:containsData(sources:)")?.data()
|
||||
}
|
||||
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.lagradost.cloudstream3.*
|
||||
import com.lagradost.cloudstream3.extractors.helper.AesHelper.cryptoAESHandler
|
||||
import com.lagradost.cloudstream3.utils.*
|
||||
import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson
|
||||
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() {
|
||||
override var mainUrl = "https://databasegdriveplayer.co"
|
||||
|
@ -65,78 +61,6 @@ open class Gdriveplayer : ExtractorApi() {
|
|||
?.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? {
|
||||
return find(str)?.groupValues?.getOrNull(1)
|
||||
}
|
||||
|
@ -154,14 +78,14 @@ open class Gdriveplayer : ExtractorApi() {
|
|||
val document = app.get(url).document
|
||||
|
||||
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)
|
||||
?.split(Regex("\\D+"))
|
||||
?.joinToString("") {
|
||||
Char(it.toInt()).toString()
|
||||
}.let { Regex("var pass = \"(\\S+?)\"").first(it ?: return)?.toByteArray() }
|
||||
?: 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 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(
|
||||
@JsonProperty("file") val file: String,
|
||||
@JsonProperty("kind") val kind: String,
|
||||
|
|
|
@ -0,0 +1,62 @@
|
|||
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.utils.ExtractorApi
|
||||
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||
import com.lagradost.cloudstream3.utils.Qualities
|
||||
|
||||
open class Gofile : ExtractorApi() {
|
||||
override val name = "Gofile"
|
||||
override val mainUrl = "https://gofile.io"
|
||||
override val requiresReferer = false
|
||||
private val mainApi = "https://api.gofile.io"
|
||||
|
||||
override suspend fun getUrl(
|
||||
url: String,
|
||||
referer: String?,
|
||||
subtitleCallback: (SubtitleFile) -> Unit,
|
||||
callback: (ExtractorLink) -> Unit
|
||||
) {
|
||||
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 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 {
|
||||
callback.invoke(
|
||||
ExtractorLink(
|
||||
this.name,
|
||||
this.name,
|
||||
it.value["link"] ?: return,
|
||||
"",
|
||||
getQuality(it.value["name"]),
|
||||
headers = mapOf(
|
||||
"Cookie" to "accountToken=$token"
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private fun getQuality(str: String?): Int {
|
||||
return Regex("(\\d{3,4})[pP]").find(str ?: "")?.groupValues?.getOrNull(1)?.toIntOrNull()
|
||||
?: Qualities.Unknown.value
|
||||
}
|
||||
|
||||
data class Account(
|
||||
@JsonProperty("data") val data: HashMap<String, String>? = null,
|
||||
)
|
||||
|
||||
data class Data(
|
||||
@JsonProperty("contents") val contents: HashMap<String, HashMap<String, String>>? = null,
|
||||
)
|
||||
|
||||
data class Source(
|
||||
@JsonProperty("data") val data: Data? = null,
|
||||
)
|
||||
|
||||
}
|
|
@ -58,7 +58,7 @@ open class GuardareStream : ExtractorApi() {
|
|||
jsonVideoData.data.forEach {
|
||||
callback.invoke(
|
||||
ExtractorLink(
|
||||
it.file + ".${it.type}",
|
||||
this.name,
|
||||
this.name,
|
||||
it.file + ".${it.type}",
|
||||
mainUrl,
|
||||
|
|
|
@ -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() {
|
||||
override val name = "Neonime7n"
|
||||
override val mainUrl = "https://7njctn.neonime.watch"
|
||||
override val mainUrl = "https://neonime.fun"
|
||||
override val redirect = false
|
||||
}
|
||||
|
||||
|
@ -19,7 +19,7 @@ class Neonime8n : Hxfile() {
|
|||
|
||||
class KotakAnimeid : Hxfile() {
|
||||
override val name = "KotakAnimeid"
|
||||
override val mainUrl = "https://kotakanimeid.com"
|
||||
override val mainUrl = "https://nontonanimeid.bio"
|
||||
override val requiresReferer = true
|
||||
}
|
||||
|
||||
|
@ -97,4 +97,4 @@ open class Hxfile : ExtractorApi() {
|
|||
@JsonProperty("label") val label: String?
|
||||
)
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,37 @@
|
|||
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
|
||||
import com.lagradost.cloudstream3.utils.httpsify
|
||||
|
||||
open class Krakenfiles : ExtractorApi() {
|
||||
override val name = "Krakenfiles"
|
||||
override val mainUrl = "https://krakenfiles.com"
|
||||
override val requiresReferer = false
|
||||
|
||||
override suspend fun getUrl(
|
||||
url: String,
|
||||
referer: String?,
|
||||
subtitleCallback: (SubtitleFile) -> Unit,
|
||||
callback: (ExtractorLink) -> Unit
|
||||
) {
|
||||
val id = Regex("/(?:view|embed-video)/([\\da-zA-Z]+)").find(url)?.groupValues?.get(1)
|
||||
val doc = app.get("$mainUrl/embed-video/$id").document
|
||||
val link = doc.selectFirst("source")?.attr("src")
|
||||
|
||||
callback.invoke(
|
||||
ExtractorLink(
|
||||
this.name,
|
||||
this.name,
|
||||
httpsify(link ?: return),
|
||||
"",
|
||||
Qualities.Unknown.value
|
||||
)
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -18,7 +18,8 @@ open class Linkbox : ExtractorApi() {
|
|||
subtitleCallback: (SubtitleFile) -> 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)
|
||||
.parsedSafe<Responses>()?.data?.itemInfo?.resolutionList?.map { link ->
|
||||
callback.invoke(
|
||||
|
@ -44,6 +45,7 @@ open class Linkbox : ExtractorApi() {
|
|||
|
||||
data class Data(
|
||||
@JsonProperty("itemInfo") val itemInfo: ItemInfo? = null,
|
||||
@JsonProperty("itemId") val itemId: String? = null,
|
||||
)
|
||||
|
||||
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.M3u8Helper
|
||||
|
||||
class SpeedoStream1 : SpeedoStream() {
|
||||
override val mainUrl = "https://speedostream.nl"
|
||||
}
|
||||
open class Minoplres : ExtractorApi() {
|
||||
|
||||
open class SpeedoStream : ExtractorApi() {
|
||||
override val name = "SpeedoStream"
|
||||
override val mainUrl = "https://speedostream.com"
|
||||
override val name = "Minoplres" // formerly SpeedoStream
|
||||
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> {
|
||||
val sources = mutableListOf<ExtractorLink>()
|
||||
|
@ -26,7 +24,7 @@ open class SpeedoStream : ExtractorApi() {
|
|||
M3u8Helper.generateM3u8(
|
||||
name,
|
||||
it.file,
|
||||
"$mainUrl/",
|
||||
"$hostUrl/",
|
||||
).forEach { m3uData -> sources.add(m3uData) }
|
||||
}
|
||||
}
|
||||
|
@ -37,6 +35,4 @@ open class SpeedoStream : ExtractorApi() {
|
|||
private data class File(
|
||||
@JsonProperty("file") val file: String,
|
||||
)
|
||||
|
||||
|
||||
}
|
||||
}
|
|
@ -7,7 +7,7 @@ import com.lagradost.cloudstream3.utils.ExtractorLink
|
|||
import com.lagradost.cloudstream3.utils.M3u8Helper
|
||||
|
||||
class MoviehabNet : Moviehab() {
|
||||
override var mainUrl = "https://play.moviehab.net"
|
||||
override var mainUrl = "https://play.moviehab.asia"
|
||||
}
|
||||
|
||||
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 mainUrl = "https://www.mp4upload.com"
|
||||
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>? {
|
||||
with(app.get(url)) {
|
||||
getAndUnpack(this.text).let { unpackedText ->
|
||||
val quality = unpackedText.lowercase().substringAfter(" height=").substringBefore(" ").toIntOrNull()
|
||||
srcRegex.find(unpackedText)?.groupValues?.get(1)?.let { link ->
|
||||
return listOf(
|
||||
ExtractorLink(
|
||||
name,
|
||||
name,
|
||||
link,
|
||||
url,
|
||||
quality ?: Qualities.Unknown.value,
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
val realUrl = idMatch.find(url)?.groupValues?.get(2)?.let { id ->
|
||||
"$mainUrl/embed-$id.html"
|
||||
} ?: url
|
||||
val response = app.get(realUrl)
|
||||
val unpackedText = getAndUnpack(response.text)
|
||||
val quality =
|
||||
unpackedText.lowercase().substringAfter(" height=").substringBefore(" ").toIntOrNull()
|
||||
srcRegex.find(unpackedText)?.groupValues?.get(1)?.let { link ->
|
||||
return listOf(
|
||||
ExtractorLink(
|
||||
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
|
||||
}
|
||||
|
|
|
@ -9,7 +9,7 @@ import java.net.URI
|
|||
|
||||
open class MultiQuality : ExtractorApi() {
|
||||
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 m3u8Regex = Regex(""".*?(\d*).m3u8""")
|
||||
private val urlRegex = Regex("""(.*?)([^/]+$)""")
|
||||
|
@ -56,4 +56,4 @@ open class MultiQuality : ExtractorApi() {
|
|||
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
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty
|
||||
import com.lagradost.cloudstream3.utils.*
|
||||
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(){
|
||||
class OkRuSSL : Odnoklassniki() {
|
||||
override var name = "OkRuSSL"
|
||||
override var mainUrl = "https://ok.ru"
|
||||
}
|
||||
|
||||
open class OkRu : ExtractorApi() {
|
||||
override var name = "Okru"
|
||||
class OkRuHTTP : Odnoklassniki() {
|
||||
override var name = "OkRuHTTP"
|
||||
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.mvvm.suspendSafeApiCall
|
||||
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||
import com.lagradost.cloudstream3.utils.INFER_TYPE
|
||||
import com.lagradost.cloudstream3.utils.extractorApis
|
||||
import com.lagradost.cloudstream3.utils.getQualityFromName
|
||||
import com.lagradost.cloudstream3.utils.loadExtractor
|
||||
|
@ -66,7 +67,7 @@ open class Pelisplus(val mainUrl: String) {
|
|||
href,
|
||||
page.url,
|
||||
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
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
|
@ -6,6 +6,51 @@ import com.lagradost.cloudstream3.app
|
|||
import com.lagradost.cloudstream3.utils.ExtractorApi
|
||||
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||
import com.lagradost.cloudstream3.utils.M3u8Helper
|
||||
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() {
|
||||
override var mainUrl = "https://vidgomunimesb.xyz"
|
||||
}
|
||||
|
||||
class Sbasian : StreamSB() {
|
||||
override var mainUrl = "https://sbasian.pro"
|
||||
override var name = "Sbasian"
|
||||
}
|
||||
|
||||
class Sbnet : StreamSB() {
|
||||
override var name = "Sbnet"
|
||||
override var mainUrl = "https://sbnet.one"
|
||||
}
|
||||
|
||||
class Keephealth : StreamSB() {
|
||||
override var name = "Keephealth"
|
||||
override var mainUrl = "https://keephealth.info"
|
||||
}
|
||||
|
||||
class Sbspeed : StreamSB() {
|
||||
override var name = "Sbspeed"
|
||||
|
@ -81,24 +126,66 @@ class StreamSB11 : StreamSB() {
|
|||
override var mainUrl = "https://sbbrisk.com"
|
||||
}
|
||||
|
||||
// This is a modified version of https://github.com/jmir1/aniyomi-extensions/blob/master/src/en/genoanime/src/eu/kanade/tachiyomi/animeextension/en/genoanime/extractors/StreamSBExtractor.kt
|
||||
// The following code is under the Apache License 2.0 https://github.com/jmir1/aniyomi-extensions/blob/master/LICENSE
|
||||
class Sblongvu : StreamSB() {
|
||||
override var mainUrl = "https://sblongvu.com"
|
||||
}
|
||||
|
||||
open class StreamSB : ExtractorApi() {
|
||||
override var name = "StreamSB"
|
||||
override var mainUrl = "https://watchsb.com"
|
||||
override val requiresReferer = false
|
||||
private val alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
|
||||
|
||||
private val hexArray = "0123456789ABCDEF".toCharArray()
|
||||
override suspend fun getUrl(
|
||||
url: String,
|
||||
referer: String?,
|
||||
subtitleCallback: (SubtitleFile) -> Unit,
|
||||
callback: (ExtractorLink) -> Unit
|
||||
) {
|
||||
val regexID =
|
||||
Regex("(embed-[a-zA-Z\\d]{0,8}[a-zA-Z\\d_-]+|/e/[a-zA-Z\\d]{0,8}[a-zA-Z\\d_-]+)")
|
||||
val id = regexID.findAll(url).map {
|
||||
it.value.replace(Regex("(embed-|/e/)"), "")
|
||||
}.first()
|
||||
val master = "$mainUrl/375664356a494546326c4b797c7c6e756577776778623171737/${encodeId(id)}"
|
||||
val headers = mapOf(
|
||||
"watchsb" to "sbstream",
|
||||
)
|
||||
val mapped = app.get(
|
||||
master.lowercase(),
|
||||
headers = headers,
|
||||
referer = url,
|
||||
).parsedSafe<Main>()
|
||||
M3u8Helper.generateM3u8(
|
||||
name,
|
||||
mapped?.streamData?.file ?: return,
|
||||
url,
|
||||
headers = headers
|
||||
).forEach(callback)
|
||||
|
||||
private fun bytesToHex(bytes: ByteArray): String {
|
||||
val hexChars = CharArray(bytes.size * 2)
|
||||
for (j in bytes.indices) {
|
||||
val v = bytes[j].toInt() and 0xFF
|
||||
|
||||
hexChars[j * 2] = hexArray[v ushr 4]
|
||||
hexChars[j * 2 + 1] = hexArray[v and 0x0F]
|
||||
mapped.streamData.subs?.map {sub ->
|
||||
subtitleCallback.invoke(
|
||||
SubtitleFile(
|
||||
sub.label.toString(),
|
||||
sub.file ?: return@map null,
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun encodeId(id: String): String {
|
||||
val code = "${createHashTable()}||$id||${createHashTable()}||streamsb"
|
||||
return code.toCharArray().joinToString("") { char ->
|
||||
char.code.toString(16)
|
||||
}
|
||||
}
|
||||
|
||||
private fun createHashTable(): String {
|
||||
return buildString {
|
||||
repeat(12) {
|
||||
append(alphabet[Random.nextInt(alphabet.length)])
|
||||
}
|
||||
}
|
||||
return String(hexChars)
|
||||
}
|
||||
|
||||
data class Subs (
|
||||
|
@ -122,42 +209,4 @@ open class StreamSB : ExtractorApi() {
|
|||
@JsonProperty("status_code") val statusCode: Int,
|
||||
)
|
||||
|
||||
override suspend fun getUrl(
|
||||
url: String,
|
||||
referer: String?,
|
||||
subtitleCallback: (SubtitleFile) -> Unit,
|
||||
callback: (ExtractorLink) -> Unit
|
||||
) {
|
||||
val regexID =
|
||||
Regex("(embed-[a-zA-Z0-9]{0,8}[a-zA-Z0-9_-]+|/e/[a-zA-Z0-9]{0,8}[a-zA-Z0-9_-]+)")
|
||||
val id = regexID.findAll(url).map {
|
||||
it.value.replace(Regex("(embed-|/e/)"), "")
|
||||
}.first()
|
||||
// val master = "$mainUrl/sources48/6d6144797752744a454267617c7c${bytesToHex.lowercase()}7c7c4e61755a56456f34385243727c7c73747265616d7362/6b4a33767968506e4e71374f7c7c343837323439333133333462353935333633373836643638376337633462333634663539343137373761333635313533333835333763376333393636363133393635366136323733343435323332376137633763373337343732363536313664373336327c7c504d754478413835306633797c7c73747265616d7362"
|
||||
val master = "$mainUrl/sources15/" + bytesToHex("||$id||||streamsb".toByteArray()) + "/"
|
||||
val headers = mapOf(
|
||||
"watchsb" to "sbstream",
|
||||
)
|
||||
val mapped = app.get(
|
||||
master.lowercase(),
|
||||
headers = headers,
|
||||
referer = url,
|
||||
).parsedSafe<Main>()
|
||||
// val urlmain = mapped.streamData.file.substringBefore("/hls/")
|
||||
M3u8Helper.generateM3u8(
|
||||
name,
|
||||
mapped?.streamData?.file ?: return,
|
||||
url,
|
||||
headers = headers
|
||||
).forEach(callback)
|
||||
|
||||
mapped.streamData.subs?.map {sub ->
|
||||
subtitleCallback.invoke(
|
||||
SubtitleFile(
|
||||
sub.label.toString(),
|
||||
sub.file ?: return@map null,
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,6 +9,10 @@ class StreamTapeNet : StreamTape() {
|
|||
override var mainUrl = "https://streamtape.net"
|
||||
}
|
||||
|
||||
class StreamTapeXyz : StreamTape() {
|
||||
override var mainUrl = "https://streamtape.xyz"
|
||||
}
|
||||
|
||||
class ShaveTape : StreamTape(){
|
||||
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
|
||||
)
|
||||
}
|
|
@ -30,7 +30,7 @@ open class Tantifilm : ExtractorApi() {
|
|||
val jsonvideodata = parseJson<TantifilmJsonData>(response)
|
||||
return jsonvideodata.data.map {
|
||||
ExtractorLink(
|
||||
it.file+".${it.type}",
|
||||
this.name,
|
||||
this.name,
|
||||
it.file+".${it.type}",
|
||||
mainUrl,
|
||||
|
|
|
@ -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,51 @@
|
|||
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.utils.AppUtils
|
||||
import com.lagradost.cloudstream3.utils.ExtractorApi
|
||||
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||
import com.lagradost.cloudstream3.utils.Qualities
|
||||
|
||||
open class Uservideo : ExtractorApi() {
|
||||
override val name: String = "Uservideo"
|
||||
override val mainUrl: String = "https://uservideo.xyz"
|
||||
override val requiresReferer = false
|
||||
|
||||
override suspend fun getUrl(
|
||||
url: String,
|
||||
referer: String?,
|
||||
subtitleCallback: (SubtitleFile) -> Unit,
|
||||
callback: (ExtractorLink) -> Unit
|
||||
) {
|
||||
val script = app.get(url).document.selectFirst("script:containsData(hosts =)")?.data()
|
||||
val host = script?.substringAfter("hosts = [\"")?.substringBefore("\"];")
|
||||
val servers = script?.substringAfter("servers = \"")?.substringBefore("\";")
|
||||
|
||||
val sources = app.get("$host/s/$servers").text.substringAfter("\"sources\":[").substringBefore("],").let {
|
||||
AppUtils.tryParseJson<List<Sources>>("[$it]")
|
||||
}
|
||||
val quality = Regex("(\\d{3,4})[Pp]").find(url)?.groupValues?.getOrNull(1)?.toIntOrNull()
|
||||
|
||||
sources?.map { source ->
|
||||
callback.invoke(
|
||||
ExtractorLink(
|
||||
name,
|
||||
name,
|
||||
source.src ?: return@map null,
|
||||
url,
|
||||
quality ?: Qualities.Unknown.value,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
data class Sources(
|
||||
@JsonProperty("src") val src: String? = null,
|
||||
@JsonProperty("type") val type: String? = null,
|
||||
@JsonProperty("label") val label: String? = null,
|
||||
)
|
||||
|
||||
}
|
|
@ -0,0 +1,51 @@
|
|||
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.utils.ExtractorApi
|
||||
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||
import com.lagradost.cloudstream3.utils.getQualityFromName
|
||||
|
||||
open class Vicloud : ExtractorApi() {
|
||||
override val name: String = "Vicloud"
|
||||
override val mainUrl: String = "https://vicloud.sbs"
|
||||
override val requiresReferer = false
|
||||
|
||||
override suspend fun getUrl(
|
||||
url: String,
|
||||
referer: String?,
|
||||
subtitleCallback: (SubtitleFile) -> Unit,
|
||||
callback: (ExtractorLink) -> Unit
|
||||
) {
|
||||
val id = Regex("\"apiQuery\":\"(.*?)\"").find(app.get(url).text)?.groupValues?.getOrNull(1)
|
||||
app.get(
|
||||
"$mainUrl/api/?$id=&_=${System.currentTimeMillis()}",
|
||||
headers = mapOf(
|
||||
"X-Requested-With" to "XMLHttpRequest"
|
||||
),
|
||||
referer = url
|
||||
).parsedSafe<Responses>()?.sources?.map { source ->
|
||||
callback.invoke(
|
||||
ExtractorLink(
|
||||
name,
|
||||
name,
|
||||
source.file ?: return@map null,
|
||||
url,
|
||||
getQualityFromName(source.label),
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private data class Sources(
|
||||
@JsonProperty("file") val file: String? = null,
|
||||
@JsonProperty("label") val label: String? = null,
|
||||
)
|
||||
|
||||
private data class Responses(
|
||||
@JsonProperty("sources") val sources: List<Sources>? = arrayListOf(),
|
||||
)
|
||||
|
||||
}
|
|
@ -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,65 @@
|
|||
package com.lagradost.cloudstream3.extractors
|
||||
|
||||
import android.util.Base64
|
||||
import com.fasterxml.jackson.annotation.JsonProperty
|
||||
import com.lagradost.cloudstream3.SubtitleFile
|
||||
import com.lagradost.cloudstream3.amap
|
||||
import com.lagradost.cloudstream3.app
|
||||
import com.lagradost.cloudstream3.utils.ExtractorApi
|
||||
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||
import java.net.URLDecoder
|
||||
import javax.crypto.Cipher
|
||||
import javax.crypto.spec.SecretKeySpec
|
||||
|
||||
class VidSrcTo : ExtractorApi() {
|
||||
override val name = "VidSrcTo"
|
||||
override val mainUrl = "https://vidsrc.to"
|
||||
override val requiresReferer = true
|
||||
|
||||
override suspend fun getUrl(
|
||||
url: String,
|
||||
referer: String?,
|
||||
subtitleCallback: (SubtitleFile) -> Unit,
|
||||
callback: (ExtractorLink) -> Unit
|
||||
) {
|
||||
val mediaId = app.get(url).document.selectFirst("ul.episodes li a")?.attr("data-id") ?: return
|
||||
val res = app.get("$mainUrl/ajax/embed/episode/$mediaId/sources").parsedSafe<VidsrctoEpisodeSources>() ?: return
|
||||
if (res.status != 200) return
|
||||
res.result?.amap { source ->
|
||||
val embedRes = app.get("$mainUrl/ajax/embed/source/${source.id}").parsedSafe<VidsrctoEmbedSource>() ?: return@amap
|
||||
val finalUrl = DecryptUrl(embedRes.result.encUrl)
|
||||
if(finalUrl.equals(embedRes.result.encUrl)) return@amap
|
||||
when (source.title) {
|
||||
"Vidplay" -> AnyVidplay(finalUrl.substringBefore("/e/")).getUrl(finalUrl, referer, subtitleCallback, callback)
|
||||
"Filemoon" -> FileMoon().getUrl(finalUrl, referer, subtitleCallback, callback)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun DecryptUrl(encUrl: String): String {
|
||||
var data = encUrl.toByteArray()
|
||||
data = Base64.decode(data, Base64.URL_SAFE)
|
||||
val rc4Key = SecretKeySpec("WXrUARXb1aDLaZjI".toByteArray(), "RC4")
|
||||
val cipher = Cipher.getInstance("RC4")
|
||||
cipher.init(Cipher.DECRYPT_MODE, rc4Key, cipher.parameters)
|
||||
data = cipher.doFinal(data)
|
||||
return URLDecoder.decode(data.toString(Charsets.UTF_8), "utf-8")
|
||||
}
|
||||
|
||||
data class VidsrctoEpisodeSources(
|
||||
@JsonProperty("status") val status: Int,
|
||||
@JsonProperty("result") val result: List<VidsrctoResult>?
|
||||
)
|
||||
|
||||
data class VidsrctoResult(
|
||||
@JsonProperty("id") val id: String,
|
||||
@JsonProperty("title") val title: String
|
||||
)
|
||||
|
||||
data class VidsrctoEmbedSource(
|
||||
@JsonProperty("status") val status: Int,
|
||||
@JsonProperty("result") val result: VidsrctoUrl
|
||||
)
|
||||
|
||||
data class VidsrctoUrl(@JsonProperty("url") val encUrl: String)
|
||||
}
|
|
@ -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,101 @@
|
|||
package com.lagradost.cloudstream3.extractors
|
||||
|
||||
import android.util.Log
|
||||
import com.lagradost.cloudstream3.SubtitleFile
|
||||
import com.lagradost.cloudstream3.app
|
||||
import com.lagradost.cloudstream3.utils.AppUtils
|
||||
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
|
||||
import org.mozilla.javascript.Context
|
||||
import org.mozilla.javascript.NativeJSON
|
||||
import org.mozilla.javascript.NativeObject
|
||||
import org.mozilla.javascript.Scriptable
|
||||
import java.util.Base64
|
||||
|
||||
open class Vidguardto : ExtractorApi() {
|
||||
override val name = "Vidguard"
|
||||
override val mainUrl = "https://vidguard.to"
|
||||
override val requiresReferer = false
|
||||
|
||||
override suspend fun getUrl(
|
||||
url: String,
|
||||
referer: String?,
|
||||
subtitleCallback: (SubtitleFile) -> Unit,
|
||||
callback: (ExtractorLink) -> Unit
|
||||
) {
|
||||
val res = app.get(url)
|
||||
val resc = res.document.select("script:containsData(eval)").firstOrNull()?.data()
|
||||
resc?.let {
|
||||
val jsonStr2 = AppUtils.parseJson<SvgObject>(runJS2(it))
|
||||
val watchlink = sigDecode(jsonStr2.stream)
|
||||
|
||||
callback.invoke(
|
||||
ExtractorLink(
|
||||
this.name,
|
||||
name,
|
||||
watchlink,
|
||||
this.mainUrl,
|
||||
Qualities.Unknown.value,
|
||||
INFER_TYPE
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun sigDecode(url: String): String {
|
||||
val sig = url.split("sig=")[1].split("&")[0]
|
||||
var t = ""
|
||||
for (v in sig.chunked(2)) {
|
||||
val byteValue = Integer.parseInt(v, 16) xor 2
|
||||
t += byteValue.toChar()
|
||||
}
|
||||
val padding = when (t.length % 4) {
|
||||
2 -> "=="
|
||||
3 -> "="
|
||||
else -> ""
|
||||
}
|
||||
val decoded = Base64.getDecoder().decode((t + padding).toByteArray(Charsets.UTF_8))
|
||||
t = String(decoded).dropLast(5).reversed()
|
||||
val charArray = t.toCharArray()
|
||||
for (i in 0 until charArray.size - 1 step 2) {
|
||||
val temp = charArray[i]
|
||||
charArray[i] = charArray[i + 1]
|
||||
charArray[i + 1] = temp
|
||||
}
|
||||
val modifiedSig = String(charArray).dropLast(5)
|
||||
return url.replace(sig, modifiedSig)
|
||||
}
|
||||
|
||||
private fun runJS2(hideMyHtmlContent: String): String {
|
||||
Log.d("runJS", "start")
|
||||
val rhino = Context.enter()
|
||||
rhino.initSafeStandardObjects()
|
||||
rhino.optimizationLevel = -1
|
||||
val scope: Scriptable = rhino.initSafeStandardObjects()
|
||||
scope.put("window", scope, scope)
|
||||
var result = ""
|
||||
try {
|
||||
Log.d("runJS", "Executing JavaScript: $hideMyHtmlContent")
|
||||
rhino.evaluateString(scope, hideMyHtmlContent, "JavaScript", 1, null)
|
||||
val svgObject = scope.get("svg", scope)
|
||||
result = if (svgObject is NativeObject) {
|
||||
NativeJSON.stringify(Context.getCurrentContext(), scope, svgObject, null, null).toString()
|
||||
} else {
|
||||
Context.toString(svgObject)
|
||||
}
|
||||
Log.d("runJS", "Result: $result")
|
||||
} catch (e: Exception) {
|
||||
Log.e("runJS", "Error executing JavaScript", e)
|
||||
} finally {
|
||||
Context.exit()
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
data class SvgObject(
|
||||
val stream: String,
|
||||
val hash: String
|
||||
)
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -25,9 +25,13 @@ open class Vidmoly : ExtractorApi() {
|
|||
subtitleCallback: (SubtitleFile) -> Unit,
|
||||
callback: (ExtractorLink) -> Unit
|
||||
) {
|
||||
|
||||
val headers = 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",
|
||||
"Sec-Fetch-Dest" to "iframe"
|
||||
)
|
||||
val script = app.get(
|
||||
url,
|
||||
headers = headers,
|
||||
referer = referer,
|
||||
).document.select("script")
|
||||
.find { it.data().contains("sources:") }?.data()
|
||||
|
@ -66,4 +70,4 @@ open class Vidmoly : ExtractorApi() {
|
|||
@JsonProperty("kind") val kind: String? = null,
|
||||
)
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,120 @@
|
|||
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 AnyVidplay(hostUrl: String) : Vidplay() {
|
||||
override val mainUrl = hostUrl
|
||||
}
|
||||
|
||||
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", referer = url).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.argamap
|
||||
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||
import com.lagradost.cloudstream3.utils.INFER_TYPE
|
||||
import com.lagradost.cloudstream3.utils.extractorApis
|
||||
import com.lagradost.cloudstream3.utils.getQualityFromName
|
||||
import com.lagradost.cloudstream3.utils.loadExtractor
|
||||
|
@ -70,7 +71,7 @@ class Vidstream(val mainUrl: String) {
|
|||
href,
|
||||
page.url,
|
||||
getQualityFromName(qual),
|
||||
element.attr("href").contains(".m3u8")
|
||||
type = INFER_TYPE
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,15 +1,46 @@
|
|||
package com.lagradost.cloudstream3.extractors
|
||||
|
||||
import android.util.Base64
|
||||
import com.fasterxml.jackson.annotation.JsonProperty
|
||||
import com.lagradost.cloudstream3.SubtitleFile
|
||||
import com.lagradost.cloudstream3.app
|
||||
import com.lagradost.cloudstream3.utils.AppUtils
|
||||
import com.lagradost.cloudstream3.utils.ExtractorApi
|
||||
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||
import com.lagradost.cloudstream3.utils.M3u8Helper
|
||||
|
||||
class Tubeless : Voe() {
|
||||
override val name = "Tubeless"
|
||||
override val mainUrl = "https://tubelessceliolymph.com"
|
||||
}
|
||||
|
||||
class Simpulumlamerop : Voe() {
|
||||
override val name = "Simplum"
|
||||
override var mainUrl = "https://simpulumlamerop.com"
|
||||
}
|
||||
|
||||
class Urochsunloath : Voe() {
|
||||
override val name = "Uroch"
|
||||
override var mainUrl = "https://urochsunloath.com"
|
||||
}
|
||||
|
||||
class Yipsu : Voe() {
|
||||
override val name = "Yipsu"
|
||||
override var mainUrl = "https://yip.su"
|
||||
}
|
||||
|
||||
class MetaGnathTuggers : Voe() {
|
||||
override val name = "Metagnath"
|
||||
override val mainUrl = "https://metagnathtuggers.com"
|
||||
}
|
||||
|
||||
open class Voe : ExtractorApi() {
|
||||
override val name = "Voe"
|
||||
override val mainUrl = "https://voe.sx"
|
||||
override val requiresReferer = true
|
||||
|
||||
private val linkRegex = "(http|https)://([\\w_-]+(?:\\.[\\w_-]+)+)([\\w.,@?^=%&:/~+#-]*[\\w@?^=%&/~+#-])".toRegex()
|
||||
private val base64Regex = Regex("'.*'")
|
||||
|
||||
override suspend fun getUrl(
|
||||
url: String,
|
||||
|
@ -18,15 +49,36 @@ open class Voe : ExtractorApi() {
|
|||
callback: (ExtractorLink) -> Unit
|
||||
) {
|
||||
val res = app.get(url, referer = referer).document
|
||||
val link = res.select("script").find { it.data().contains("const sources") }?.data()
|
||||
?.substringAfter("\"hls\": \"")?.substringBefore("\",")
|
||||
|
||||
M3u8Helper.generateM3u8(
|
||||
name,
|
||||
link ?: return,
|
||||
"$mainUrl/",
|
||||
headers = mapOf("Origin" to "$mainUrl/")
|
||||
).forEach(callback)
|
||||
val script = res.select("script").find { it.data().contains("sources =") }?.data()
|
||||
val link = Regex("[\"']hls[\"']:\\s*[\"'](.*)[\"']").find(script ?: return)?.groupValues?.get(1)
|
||||
|
||||
val videoLinks = mutableListOf<String>()
|
||||
|
||||
if (!link.isNullOrBlank()) {
|
||||
videoLinks.add(
|
||||
when {
|
||||
linkRegex.matches(link) -> link
|
||||
else -> String(Base64.decode(link, Base64.DEFAULT))
|
||||
}
|
||||
)
|
||||
} else {
|
||||
val link2 = base64Regex.find(script)?.value ?: return
|
||||
val decoded = Base64.decode(link2, Base64.DEFAULT).toString()
|
||||
val videoLinkDTO = AppUtils.parseJson<WcoSources>(decoded)
|
||||
videoLinkDTO.let { videoLinks.add(it.toString()) }
|
||||
}
|
||||
|
||||
videoLinks.forEach { videoLink ->
|
||||
M3u8Helper.generateM3u8(
|
||||
name,
|
||||
videoLink,
|
||||
"$mainUrl/",
|
||||
headers = mapOf("Origin" to "$mainUrl/")
|
||||
).forEach(callback)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data class WcoSources(
|
||||
@JsonProperty("VideoLinkDTO") val VideoLinkDTO: String,
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,54 +0,0 @@
|
|||
package com.lagradost.cloudstream3.extractors
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty
|
||||
import com.lagradost.cloudstream3.app
|
||||
import com.lagradost.cloudstream3.utils.AppUtils.parseJson
|
||||
import com.lagradost.cloudstream3.utils.ExtractorApi
|
||||
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||
import com.lagradost.cloudstream3.utils.getQualityFromName
|
||||
|
||||
open class VoeExtractor : ExtractorApi() {
|
||||
override val name: String = "Voe"
|
||||
override val mainUrl: String = "https://voe.sx"
|
||||
override val requiresReferer = false
|
||||
|
||||
private data class ResponseLinks(
|
||||
@JsonProperty("hls") val hls: String?,
|
||||
@JsonProperty("mp4") val mp4: String?,
|
||||
@JsonProperty("video_height") val label: Int?
|
||||
//val type: String // Mp4
|
||||
)
|
||||
|
||||
override suspend fun getUrl(url: String, referer: String?): List<ExtractorLink> {
|
||||
val html = app.get(url).text
|
||||
if (html.isNotBlank()) {
|
||||
val src = html.substringAfter("const sources =").substringBefore(";")
|
||||
// Remove last comma, it is not proper json otherwise
|
||||
.replace("0,", "0")
|
||||
// Make json use the proper quotes
|
||||
.replace("'", "\"")
|
||||
|
||||
//Log.i(this.name, "Result => (src) ${src}")
|
||||
parseJson<ResponseLinks?>(src)?.let { voeLink ->
|
||||
//Log.i(this.name, "Result => (voeLink) ${voeLink}")
|
||||
|
||||
// Always defaults to the hls link, but returns the mp4 if null
|
||||
val linkUrl = voeLink.hls ?: voeLink.mp4
|
||||
val linkLabel = voeLink.label?.toString() ?: ""
|
||||
if (!linkUrl.isNullOrEmpty()) {
|
||||
return listOf(
|
||||
ExtractorLink(
|
||||
name = this.name,
|
||||
source = this.name,
|
||||
url = linkUrl,
|
||||
quality = getQualityFromName(linkLabel),
|
||||
referer = url,
|
||||
isM3u8 = voeLink.hls != null
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
return emptyList()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,39 @@
|
|||
package com.lagradost.cloudstream3.extractors
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty
|
||||
import com.lagradost.cloudstream3.app
|
||||
import com.lagradost.cloudstream3.utils.*
|
||||
import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson
|
||||
import com.lagradost.cloudstream3.utils.JsUnpacker
|
||||
import com.lagradost.cloudstream3.utils.ExtractorApi
|
||||
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||
import com.lagradost.cloudstream3.utils.Qualities
|
||||
import com.lagradost.cloudstream3.utils.getQualityFromName
|
||||
import java.net.URI
|
||||
|
||||
|
||||
open class Vtbe : ExtractorApi() {
|
||||
override var name = "Vtbe"
|
||||
override var mainUrl = "https://vtbe.to"
|
||||
override val requiresReferer = true
|
||||
|
||||
override suspend fun getUrl(url: String, referer: String?): List<ExtractorLink>? {
|
||||
val response = app.get(url,referer=mainUrl).document
|
||||
val extractedpack =response.selectFirst("script:containsData(function(p,a,c,k,e,d))")?.data().toString()
|
||||
JsUnpacker(extractedpack).unpack()?.let { unPacked ->
|
||||
Regex("sources:\\[\\{file:\"(.*?)\"").find(unPacked)?.groupValues?.get(1)?.let { link ->
|
||||
return listOf(
|
||||
ExtractorLink(
|
||||
this.name,
|
||||
this.name,
|
||||
link,
|
||||
referer ?: "",
|
||||
Qualities.Unknown.value,
|
||||
URI(link).path.endsWith(".m3u8")
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
}
|
|
@ -7,6 +7,7 @@ import com.lagradost.cloudstream3.extractors.helper.NineAnimeHelper.encrypt
|
|||
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
|
||||
|
||||
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")
|
||||
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
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
|
@ -8,6 +8,16 @@ import com.lagradost.cloudstream3.utils.ExtractorApi
|
|||
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||
import com.lagradost.cloudstream3.utils.getQualityFromName
|
||||
|
||||
class StreamM4u : XStreamCdn() {
|
||||
override val name: String = "StreamM4u"
|
||||
override val mainUrl: String = "https://streamm4u.club"
|
||||
}
|
||||
|
||||
class Fembed9hd : XStreamCdn() {
|
||||
override var mainUrl = "https://fembed9hd.com"
|
||||
override var name = "Fembed9hd"
|
||||
}
|
||||
|
||||
class Cdnplayer: XStreamCdn() {
|
||||
override val name: String = "Cdnplayer"
|
||||
override val mainUrl: String = "https://cdnplayer.online"
|
||||
|
|
|
@ -70,19 +70,18 @@ open class YoutubeExtractor : ExtractorApi() {
|
|||
}
|
||||
}
|
||||
ytVideos[url]?.mapNotNull {
|
||||
if (it.isVideoOnly || it.height <= 0) return@mapNotNull null
|
||||
if (it.isVideoOnly() || it.height <= 0) return@mapNotNull null
|
||||
|
||||
ExtractorLink(
|
||||
this.name,
|
||||
this.name,
|
||||
it.url ?: return@mapNotNull null,
|
||||
it.content ?: return@mapNotNull null,
|
||||
"",
|
||||
it.height
|
||||
)
|
||||
}?.forEach(callback)
|
||||
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)
|
||||
}
|
||||
|
||||
}
|
|
@ -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
|
||||
)
|
||||
|
||||
}
|
|
@ -0,0 +1,158 @@
|
|||
package com.lagradost.cloudstream3.extractors.helper
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty
|
||||
import com.lagradost.cloudstream3.app
|
||||
import com.lagradost.cloudstream3.base64Decode
|
||||
import com.lagradost.cloudstream3.base64DecodeArray
|
||||
import com.lagradost.cloudstream3.base64Encode
|
||||
import com.lagradost.cloudstream3.mvvm.normalSafeApiCall
|
||||
import com.lagradost.cloudstream3.mvvm.safeApiCall
|
||||
import com.lagradost.cloudstream3.utils.AppUtils
|
||||
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||
import com.lagradost.cloudstream3.utils.M3u8Helper
|
||||
import com.lagradost.cloudstream3.utils.getQualityFromName
|
||||
import org.jsoup.nodes.Document
|
||||
import java.net.URI
|
||||
import javax.crypto.Cipher
|
||||
import javax.crypto.spec.IvParameterSpec
|
||||
import javax.crypto.spec.SecretKeySpec
|
||||
|
||||
object GogoHelper {
|
||||
|
||||
/**
|
||||
* @param id base64Decode(show_id) + IV
|
||||
* @return the encryption key
|
||||
* */
|
||||
private fun getKey(id: String): String? {
|
||||
return normalSafeApiCall {
|
||||
id.map {
|
||||
it.code.toString(16)
|
||||
}.joinToString("").substring(0, 32)
|
||||
}
|
||||
}
|
||||
|
||||
// https://github.com/saikou-app/saikou/blob/45d0a99b8a72665a29a1eadfb38c506b842a29d7/app/src/main/java/ani/saikou/parsers/anime/extractors/GogoCDN.kt#L97
|
||||
// No Licence on the function
|
||||
private fun cryptoHandler(
|
||||
string: String,
|
||||
iv: String,
|
||||
secretKeyString: String,
|
||||
encrypt: Boolean = true
|
||||
): String {
|
||||
//println("IV: $iv, Key: $secretKeyString, encrypt: $encrypt, Message: $string")
|
||||
val ivParameterSpec = IvParameterSpec(iv.toByteArray())
|
||||
val secretKey = SecretKeySpec(secretKeyString.toByteArray(), "AES")
|
||||
val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding")
|
||||
return if (!encrypt) {
|
||||
cipher.init(Cipher.DECRYPT_MODE, secretKey, ivParameterSpec)
|
||||
String(cipher.doFinal(base64DecodeArray(string)))
|
||||
} else {
|
||||
cipher.init(Cipher.ENCRYPT_MODE, secretKey, ivParameterSpec)
|
||||
base64Encode(cipher.doFinal(string.toByteArray()))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param iframeUrl something like https://gogoplay4.com/streaming.php?id=XXXXXX
|
||||
* @param mainApiName used for ExtractorLink names and source
|
||||
* @param iv secret iv from site, required non-null if isUsingAdaptiveKeys is off
|
||||
* @param secretKey secret key for decryption from site, required non-null if isUsingAdaptiveKeys is off
|
||||
* @param secretDecryptKey secret key to decrypt the response json, required non-null if isUsingAdaptiveKeys is off
|
||||
* @param isUsingAdaptiveKeys generates keys from IV and ID, see getKey()
|
||||
* @param isUsingAdaptiveData generate encrypt-ajax data based on $("script[data-name='episode']")[0].dataset.value
|
||||
* */
|
||||
suspend fun extractVidstream(
|
||||
iframeUrl: String,
|
||||
mainApiName: String,
|
||||
callback: (ExtractorLink) -> Unit,
|
||||
iv: String?,
|
||||
secretKey: String?,
|
||||
secretDecryptKey: String?,
|
||||
// This could be removed, but i prefer it verbose
|
||||
isUsingAdaptiveKeys: Boolean,
|
||||
isUsingAdaptiveData: Boolean,
|
||||
// If you don't want to re-fetch the document
|
||||
iframeDocument: Document? = null
|
||||
) = safeApiCall {
|
||||
if ((iv == null || secretKey == null || secretDecryptKey == null) && !isUsingAdaptiveKeys)
|
||||
return@safeApiCall
|
||||
|
||||
val id = Regex("id=([^&]+)").find(iframeUrl)!!.value.removePrefix("id=")
|
||||
|
||||
var document: Document? = iframeDocument
|
||||
val foundIv =
|
||||
iv ?: (document ?: app.get(iframeUrl).document.also { document = it })
|
||||
.select("""div.wrapper[class*=container]""")
|
||||
.attr("class").split("-").lastOrNull() ?: return@safeApiCall
|
||||
val foundKey = secretKey ?: getKey(base64Decode(id) + foundIv) ?: return@safeApiCall
|
||||
val foundDecryptKey = secretDecryptKey ?: foundKey
|
||||
|
||||
val uri = URI(iframeUrl)
|
||||
val mainUrl = "https://" + uri.host
|
||||
|
||||
val encryptedId = cryptoHandler(id, foundIv, foundKey)
|
||||
val encryptRequestData = if (isUsingAdaptiveData) {
|
||||
// Only fetch the document if necessary
|
||||
val realDocument = document ?: app.get(iframeUrl).document
|
||||
val dataEncrypted =
|
||||
realDocument.select("script[data-name='episode']").attr("data-value")
|
||||
val headers = cryptoHandler(dataEncrypted, foundIv, foundKey, false)
|
||||
"id=$encryptedId&alias=$id&" + headers.substringAfter("&")
|
||||
} else {
|
||||
"id=$encryptedId&alias=$id"
|
||||
}
|
||||
|
||||
val jsonResponse =
|
||||
app.get(
|
||||
"$mainUrl/encrypt-ajax.php?$encryptRequestData",
|
||||
headers = mapOf("X-Requested-With" to "XMLHttpRequest")
|
||||
)
|
||||
val dataencrypted =
|
||||
jsonResponse.text.substringAfter("{\"data\":\"").substringBefore("\"}")
|
||||
val datadecrypted = cryptoHandler(dataencrypted, foundIv, foundDecryptKey, false)
|
||||
val sources = AppUtils.parseJson<GogoSources>(datadecrypted)
|
||||
|
||||
suspend fun invokeGogoSource(
|
||||
source: GogoSource,
|
||||
sourceCallback: (ExtractorLink) -> Unit
|
||||
) {
|
||||
if (source.file.contains(".m3u8")) {
|
||||
M3u8Helper.generateM3u8(
|
||||
mainApiName,
|
||||
source.file,
|
||||
mainUrl,
|
||||
headers = mapOf("Origin" to "https://plyr.link")
|
||||
).forEach(sourceCallback)
|
||||
} else {
|
||||
sourceCallback.invoke(
|
||||
ExtractorLink(
|
||||
mainApiName,
|
||||
mainApiName,
|
||||
source.file,
|
||||
mainUrl,
|
||||
getQualityFromName(source.label),
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
sources.source?.forEach {
|
||||
invokeGogoSource(it, callback)
|
||||
}
|
||||
sources.sourceBk?.forEach {
|
||||
invokeGogoSource(it, callback)
|
||||
}
|
||||
}
|
||||
|
||||
data class GogoSources(
|
||||
@JsonProperty("source") val source: List<GogoSource>?,
|
||||
@JsonProperty("sourceBk") val sourceBk: List<GogoSource>?,
|
||||
)
|
||||
|
||||
data class GogoSource(
|
||||
@JsonProperty("file") val file: String,
|
||||
@JsonProperty("label") val label: String?,
|
||||
@JsonProperty("type") val type: String?,
|
||||
@JsonProperty("default") val default: String? = null
|
||||
)
|
||||
}
|
|
@ -21,10 +21,11 @@ class CrossTmdbProvider : TmdbProvider() {
|
|||
return Regex("""[^a-zA-Z0-9-]""").replace(name, "")
|
||||
}
|
||||
|
||||
private val validApis by lazy {
|
||||
apis.filter { it.lang == this.lang && it::class.java != this::class.java }
|
||||
//.distinctBy { it.uniqueId }
|
||||
}
|
||||
private val validApis
|
||||
get() =
|
||||
synchronized(apis) { apis.filter { it.lang == this.lang && it::class.java != this::class.java } }
|
||||
//.distinctBy { it.uniqueId }
|
||||
|
||||
|
||||
data class CrossMetaData(
|
||||
@JsonProperty("isSuccess") val isSuccess: Boolean,
|
||||
|
@ -60,7 +61,8 @@ class CrossTmdbProvider : TmdbProvider() {
|
|||
|
||||
override suspend fun load(url: String): LoadResponse? {
|
||||
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)
|
||||
when (this) {
|
||||
is MovieLoadResponse -> {
|
||||
|
@ -98,6 +100,7 @@ class CrossTmdbProvider : TmdbProvider() {
|
|||
this.dataUrl =
|
||||
CrossMetaData(true, data.map { it.apiName to it.dataUrl }).toJson()
|
||||
}
|
||||
|
||||
else -> {
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -105,6 +105,7 @@ open class TmdbProvider : MainAPI() {
|
|||
this.id,
|
||||
episode.episode_number,
|
||||
episode.season_number,
|
||||
this.name ?: this.original_name,
|
||||
).toJson(),
|
||||
episode.name,
|
||||
episode.season_number,
|
||||
|
@ -122,6 +123,7 @@ open class TmdbProvider : MainAPI() {
|
|||
this.id,
|
||||
episodeNum,
|
||||
season.season_number,
|
||||
this.name ?: this.original_name,
|
||||
).toJson(),
|
||||
season = season.season_number
|
||||
)
|
||||
|
@ -151,6 +153,8 @@ open class TmdbProvider : MainAPI() {
|
|||
recommendations = (this@toLoadResponse.recommendations
|
||||
?: this@toLoadResponse.similar)?.results?.map { it.toSearchResponse() }
|
||||
addActors(credits?.cast?.toList().toActors())
|
||||
|
||||
contentRating = fetchContentRating(id, "US")
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -193,6 +197,8 @@ open class TmdbProvider : MainAPI() {
|
|||
recommendations = (this@toLoadResponse.recommendations
|
||||
?: this@toLoadResponse.similar)?.results?.map { it.toSearchResponse() }
|
||||
addActors(credits?.cast?.toList().toActors())
|
||||
|
||||
contentRating = fetchContentRating(id, "US")
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -264,6 +270,26 @@ open class TmdbProvider : MainAPI() {
|
|||
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.
|
||||
override suspend fun load(url: String): LoadResponse? {
|
||||
// https://www.themoviedb.org/movie/7445-brothers
|
||||
|
|
|
@ -0,0 +1,450 @@
|
|||
package com.lagradost.cloudstream3.metaproviders
|
||||
|
||||
import android.net.Uri
|
||||
import com.lagradost.cloudstream3.*
|
||||
import com.fasterxml.jackson.annotation.JsonAlias
|
||||
import com.fasterxml.jackson.annotation.JsonProperty
|
||||
import com.lagradost.cloudstream3.APIHolder.unixTimeMS
|
||||
import com.lagradost.cloudstream3.LoadResponse.Companion.addImdbId
|
||||
import com.lagradost.cloudstream3.LoadResponse.Companion.addTMDbId
|
||||
import com.lagradost.cloudstream3.LoadResponse.Companion.addTrailer
|
||||
import com.lagradost.cloudstream3.mvvm.logError
|
||||
import com.lagradost.cloudstream3.utils.AppUtils.parseJson
|
||||
import com.lagradost.cloudstream3.utils.AppUtils.toJson
|
||||
import java.util.Locale
|
||||
import java.text.SimpleDateFormat
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
open class TraktProvider : MainAPI() {
|
||||
override var name = "Trakt"
|
||||
override val hasMainPage = true
|
||||
override val providerType = ProviderType.MetaProvider
|
||||
override val supportedTypes = setOf(
|
||||
TvType.Movie,
|
||||
TvType.TvSeries,
|
||||
TvType.Anime,
|
||||
)
|
||||
|
||||
private val traktClientId = base64Decode("N2YzODYwYWQzNGI4ZTZmOTdmN2I5MTA0ZWQzMzEwOGI0MmQ3MTdlMTM0MmM2NGMxMTg5NGE1MjUyYTQ3NjE3Zg==")
|
||||
private val traktApiUrl = base64Decode("aHR0cHM6Ly9hcGl6LnRyYWt0LnR2")
|
||||
|
||||
override val mainPage = mainPageOf(
|
||||
"$traktApiUrl/movies/trending" to "Trending Movies", //Most watched movies right now
|
||||
"$traktApiUrl/movies/popular" to "Popular Movies", //The most popular movies for all time
|
||||
"$traktApiUrl/shows/trending" to "Trending Shows", //Most watched Shows right now
|
||||
"$traktApiUrl/shows/popular" to "Popular Shows", //The most popular Shows for all time
|
||||
)
|
||||
|
||||
override suspend fun getMainPage(page: Int, request: MainPageRequest): HomePageResponse {
|
||||
|
||||
val apiResponse = getApi("${request.data}?extended=cloud9,full&page=$page")
|
||||
|
||||
val results = parseJson<List<MediaDetails>>(apiResponse).map { element ->
|
||||
element.toSearchResponse()
|
||||
}
|
||||
return newHomePageResponse(request.name, results)
|
||||
}
|
||||
|
||||
private fun MediaDetails.toSearchResponse(): SearchResponse {
|
||||
|
||||
val media = this.media ?: this
|
||||
val mediaType = if (media.ids?.tvdb == null) TvType.Movie else TvType.TvSeries
|
||||
val poster = media.images?.poster?.firstOrNull()
|
||||
|
||||
if (mediaType == TvType.Movie) {
|
||||
return newMovieSearchResponse(
|
||||
name = media.title!!,
|
||||
url = Data(
|
||||
type = mediaType,
|
||||
mediaDetails = media,
|
||||
).toJson(),
|
||||
type = TvType.Movie,
|
||||
) {
|
||||
posterUrl = fixPath(poster)
|
||||
}
|
||||
} else {
|
||||
return newTvSeriesSearchResponse(
|
||||
name = media.title!!,
|
||||
url = Data(
|
||||
type = mediaType,
|
||||
mediaDetails = media,
|
||||
).toJson(),
|
||||
type = TvType.TvSeries,
|
||||
) {
|
||||
this.posterUrl = fixPath(poster)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun search(query: String): List<SearchResponse>? {
|
||||
val apiResponse = getApi("$traktApiUrl/search/movie,show?extended=cloud9,full&limit=20&page=1&query=$query")
|
||||
|
||||
val results = parseJson<List<MediaDetails>>(apiResponse).map { element ->
|
||||
element.toSearchResponse()
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
override suspend fun load(url: String): LoadResponse {
|
||||
|
||||
val data = parseJson<Data>(url)
|
||||
val mediaDetails = data.mediaDetails
|
||||
val moviesOrShows = if (data.type == TvType.Movie) "movies" else "shows"
|
||||
|
||||
val posterUrl = mediaDetails?.images?.poster?.firstOrNull()
|
||||
val backDropUrl = mediaDetails?.images?.fanart?.firstOrNull()
|
||||
|
||||
val resActor = getApi("$traktApiUrl/$moviesOrShows/${mediaDetails?.ids?.trakt}/people?extended=cloud9,full")
|
||||
|
||||
val actors = parseJson<People>(resActor).cast?.map {
|
||||
ActorData(
|
||||
Actor(
|
||||
name = it.person?.name!!,
|
||||
image = getWidthImageUrl(it.person.images?.headshot?.firstOrNull(), "w500")
|
||||
),
|
||||
roleString = it.character
|
||||
)
|
||||
}
|
||||
|
||||
val resRelated = getApi("$traktApiUrl/$moviesOrShows/${mediaDetails?.ids?.trakt}/related?extended=cloud9,full&limit=20")
|
||||
|
||||
val relatedMedia = parseJson<List<MediaDetails>>(resRelated).map { it.toSearchResponse() }
|
||||
|
||||
val isCartoon = mediaDetails?.genres?.contains("animation") == true || mediaDetails?.genres?.contains("anime") == true
|
||||
val isAnime = isCartoon && (mediaDetails?.language == "zh" || mediaDetails?.language == "ja")
|
||||
val isAsian = !isAnime && (mediaDetails?.language == "zh" || mediaDetails?.language == "ko")
|
||||
val isBollywood = mediaDetails?.country == "in"
|
||||
|
||||
if (data.type == TvType.Movie) {
|
||||
|
||||
val linkData = LinkData(
|
||||
id = mediaDetails?.ids?.tmdb,
|
||||
traktId = mediaDetails?.ids?.trakt,
|
||||
traktSlug = mediaDetails?.ids?.slug,
|
||||
tmdbId = mediaDetails?.ids?.tmdb,
|
||||
imdbId = mediaDetails?.ids?.imdb.toString(),
|
||||
tvdbId = mediaDetails?.ids?.tvdb,
|
||||
tvrageId = mediaDetails?.ids?.tvrage,
|
||||
type = data.type.toString(),
|
||||
title = mediaDetails?.title,
|
||||
year = mediaDetails?.year,
|
||||
orgTitle = mediaDetails?.title,
|
||||
isAnime = isAnime,
|
||||
//jpTitle = later if needed as it requires another network request,
|
||||
airedDate = mediaDetails?.released
|
||||
?: mediaDetails?.firstAired,
|
||||
isAsian = isAsian,
|
||||
isBollywood = isBollywood,
|
||||
).toJson()
|
||||
|
||||
return newMovieLoadResponse(
|
||||
name = mediaDetails?.title!!,
|
||||
url = data.toJson(),
|
||||
dataUrl = linkData.toJson(),
|
||||
type = if (isAnime) TvType.AnimeMovie else TvType.Movie,
|
||||
) {
|
||||
this.name = mediaDetails.title
|
||||
this.type = if (isAnime) TvType.AnimeMovie else TvType.Movie
|
||||
this.posterUrl = getOriginalWidthImageUrl(posterUrl)
|
||||
this.year = mediaDetails.year
|
||||
this.plot = mediaDetails.overview
|
||||
this.rating = mediaDetails.rating?.times(1000)?.roundToInt()
|
||||
this.tags = mediaDetails.genres
|
||||
this.duration = mediaDetails.runtime
|
||||
this.recommendations = relatedMedia
|
||||
this.actors = actors
|
||||
this.comingSoon = isUpcoming(mediaDetails.released)
|
||||
//posterHeaders
|
||||
this.backgroundPosterUrl = getOriginalWidthImageUrl(backDropUrl)
|
||||
this.contentRating = mediaDetails.certification
|
||||
addTrailer(mediaDetails.trailer)
|
||||
addImdbId(mediaDetails.ids?.imdb)
|
||||
addTMDbId(mediaDetails.ids?.tmdb.toString())
|
||||
}
|
||||
} else {
|
||||
|
||||
val resSeasons = getApi("$traktApiUrl/shows/${mediaDetails?.ids?.trakt.toString()}/seasons?extended=cloud9,full,episodes")
|
||||
val episodes = mutableListOf<Episode>()
|
||||
val seasons = parseJson<List<Seasons>>(resSeasons)
|
||||
val seasonsNames = mutableListOf<SeasonData>()
|
||||
var nextAir: NextAiring? = null
|
||||
|
||||
seasons.forEach { season ->
|
||||
|
||||
seasonsNames.add(
|
||||
SeasonData(
|
||||
season.number!!,
|
||||
season.title
|
||||
)
|
||||
)
|
||||
|
||||
season.episodes?.map { episode ->
|
||||
|
||||
val linkData = LinkData(
|
||||
id = mediaDetails?.ids?.tmdb,
|
||||
traktId = mediaDetails?.ids?.trakt,
|
||||
traktSlug = mediaDetails?.ids?.slug,
|
||||
tmdbId = mediaDetails?.ids?.tmdb,
|
||||
imdbId = mediaDetails?.ids?.imdb.toString(),
|
||||
tvdbId = mediaDetails?.ids?.tvdb,
|
||||
tvrageId = mediaDetails?.ids?.tvrage,
|
||||
type = data.type.toString(),
|
||||
season = episode.season,
|
||||
episode = episode.number,
|
||||
title = mediaDetails?.title,
|
||||
year = mediaDetails?.year,
|
||||
orgTitle = mediaDetails?.title,
|
||||
isAnime = isAnime,
|
||||
airedYear = mediaDetails?.year,
|
||||
lastSeason = seasons.size,
|
||||
epsTitle = episode.title,
|
||||
//jpTitle = later if needed as it requires another network request,
|
||||
date = episode.firstAired,
|
||||
airedDate = episode.firstAired,
|
||||
isAsian = isAsian,
|
||||
isBollywood = isBollywood,
|
||||
isCartoon = isCartoon
|
||||
).toJson()
|
||||
|
||||
episodes.add(
|
||||
Episode(
|
||||
data = linkData.toJson(),
|
||||
name = episode.title,
|
||||
season = episode.season,
|
||||
episode = episode.number,
|
||||
posterUrl = fixPath(episode.images?.screenshot?.firstOrNull()),
|
||||
rating = episode.rating?.times(10)?.roundToInt(),
|
||||
description = episode.overview,
|
||||
).apply {
|
||||
this.addDate(episode.firstAired)
|
||||
if (nextAir == null && this.date != null && this.date!! > unixTimeMS) {
|
||||
nextAir = NextAiring(
|
||||
episode = this.episode!!,
|
||||
unixTime = this.date!!.div(1000L),
|
||||
season = if (this.season == 1) null else this.season,
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return newTvSeriesLoadResponse(
|
||||
name = mediaDetails?.title!!,
|
||||
url = data.toJson(),
|
||||
type = if (isAnime) TvType.Anime else TvType.TvSeries,
|
||||
episodes = episodes
|
||||
) {
|
||||
this.name = mediaDetails.title
|
||||
this.type = if (isAnime) TvType.Anime else TvType.TvSeries
|
||||
this.episodes = episodes
|
||||
this.posterUrl = getOriginalWidthImageUrl(posterUrl)
|
||||
this.year = mediaDetails.year
|
||||
this.plot = mediaDetails.overview
|
||||
this.showStatus = getStatus(mediaDetails.status)
|
||||
this.rating = mediaDetails.rating?.times(1000)?.roundToInt()
|
||||
this.tags = mediaDetails.genres
|
||||
this.duration = mediaDetails.runtime
|
||||
this.recommendations = relatedMedia
|
||||
this.actors = actors
|
||||
this.comingSoon = isUpcoming(mediaDetails.released)
|
||||
//posterHeaders
|
||||
this.nextAiring = nextAir
|
||||
this.seasonNames = seasonsNames
|
||||
this.backgroundPosterUrl = getOriginalWidthImageUrl(backDropUrl)
|
||||
this.contentRating = mediaDetails.certification
|
||||
addTrailer(mediaDetails.trailer)
|
||||
addImdbId(mediaDetails.ids?.imdb)
|
||||
addTMDbId(mediaDetails.ids?.tmdb.toString())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun getApi(url: String) : String {
|
||||
return app.get(
|
||||
url = url,
|
||||
headers = mapOf(
|
||||
"Content-Type" to "application/json",
|
||||
"trakt-api-version" to "2",
|
||||
"trakt-api-key" to traktClientId,
|
||||
)
|
||||
).toString()
|
||||
}
|
||||
|
||||
private fun isUpcoming(dateString: String?): Boolean {
|
||||
return try {
|
||||
val format = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault())
|
||||
val dateTime = dateString?.let { format.parse(it)?.time } ?: return false
|
||||
APIHolder.unixTimeMS < dateTime
|
||||
} catch (t: Throwable) {
|
||||
logError(t)
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
private fun getStatus(t: String?): ShowStatus {
|
||||
return when (t) {
|
||||
"returning series" -> ShowStatus.Ongoing
|
||||
"continuing" -> ShowStatus.Ongoing
|
||||
else -> ShowStatus.Completed
|
||||
}
|
||||
}
|
||||
|
||||
private fun fixPath(url: String?): String? {
|
||||
url ?: return null
|
||||
return "https://$url"
|
||||
}
|
||||
|
||||
private fun getWidthImageUrl(path: String?, width: String) : String? {
|
||||
if (path == null) return null
|
||||
if (!path.contains("image.tmdb.org")) return fixPath(path)
|
||||
val fileName = Uri.parse(path).lastPathSegment ?: return null
|
||||
return "https://image.tmdb.org/t/p/${width}/${fileName}"
|
||||
}
|
||||
|
||||
private fun getOriginalWidthImageUrl(path: String?) : String? {
|
||||
if (path == null) return null
|
||||
if (!path.contains("image.tmdb.org")) return fixPath(path)
|
||||
return getWidthImageUrl(path, "original")
|
||||
}
|
||||
|
||||
data class Data(
|
||||
val type: TvType? = null,
|
||||
val mediaDetails: MediaDetails? = null,
|
||||
)
|
||||
|
||||
data class MediaDetails(
|
||||
@JsonProperty("title") val title: String? = null,
|
||||
@JsonProperty("year") val year: Int? = null,
|
||||
@JsonProperty("ids") val ids: Ids? = null,
|
||||
@JsonProperty("tagline") val tagline: String? = null,
|
||||
@JsonProperty("overview") val overview: String? = null,
|
||||
@JsonProperty("released") val released: String? = null,
|
||||
@JsonProperty("runtime") val runtime: Int? = null,
|
||||
@JsonProperty("country") val country: String? = null,
|
||||
@JsonProperty("updatedAt") val updatedAt: String? = null,
|
||||
@JsonProperty("trailer") val trailer: String? = null,
|
||||
@JsonProperty("homepage") val homepage: String? = null,
|
||||
@JsonProperty("status") val status: String? = null,
|
||||
@JsonProperty("rating") val rating: Double? = null,
|
||||
@JsonProperty("votes") val votes: Long? = null,
|
||||
@JsonProperty("comment_count") val commentCount: Long? = null,
|
||||
@JsonProperty("language") val language: String? = null,
|
||||
@JsonProperty("languages") val languages: List<String>? = null,
|
||||
@JsonProperty("available_translations") val availableTranslations: List<String>? = null,
|
||||
@JsonProperty("genres") val genres: List<String>? = null,
|
||||
@JsonProperty("certification") val certification: String? = null,
|
||||
@JsonProperty("aired_episodes") val airedEpisodes: Int? = null,
|
||||
@JsonProperty("first_aired") val firstAired: String? = null,
|
||||
@JsonProperty("airs") val airs: Airs? = null,
|
||||
@JsonProperty("network") val network: String? = null,
|
||||
@JsonProperty("images") val images: Images? = null,
|
||||
@JsonProperty("movie") @JsonAlias("show") val media: MediaDetails? = null
|
||||
)
|
||||
|
||||
data class Airs(
|
||||
@JsonProperty("day") val day: String? = null,
|
||||
@JsonProperty("time") val time: String? = null,
|
||||
@JsonProperty("timezone") val timezone: String? = null,
|
||||
)
|
||||
|
||||
data class Ids(
|
||||
@JsonProperty("trakt") val trakt: Int? = null,
|
||||
@JsonProperty("slug") val slug: String? = null,
|
||||
@JsonProperty("tvdb") val tvdb: Int? = null,
|
||||
@JsonProperty("imdb") val imdb: String? = null,
|
||||
@JsonProperty("tmdb") val tmdb: Int? = null,
|
||||
@JsonProperty("tvrage") val tvrage: String? = null,
|
||||
)
|
||||
|
||||
data class Images(
|
||||
@JsonProperty("fanart") val fanart: List<String>? = null,
|
||||
@JsonProperty("poster") val poster: List<String>? = null,
|
||||
@JsonProperty("logo") val logo: List<String>? = null,
|
||||
@JsonProperty("clearart") val clearart: List<String>? = null,
|
||||
@JsonProperty("banner") val banner: List<String>? = null,
|
||||
@JsonProperty("thumb") val thumb: List<String>? = null,
|
||||
@JsonProperty("screenshot") val screenshot: List<String>? = null,
|
||||
@JsonProperty("headshot") val headshot: List<String>? = null,
|
||||
)
|
||||
|
||||
data class People(
|
||||
@JsonProperty("cast") val cast: List<Cast>? = null,
|
||||
)
|
||||
|
||||
data class Cast(
|
||||
@JsonProperty("character") val character: String? = null,
|
||||
@JsonProperty("characters") val characters: List<String>? = null,
|
||||
@JsonProperty("episode_count") val episodeCount: Long? = null,
|
||||
@JsonProperty("person") val person: Person? = null,
|
||||
@JsonProperty("images") val images: Images? = null,
|
||||
)
|
||||
|
||||
data class Person(
|
||||
@JsonProperty("name") val name: String? = null,
|
||||
@JsonProperty("ids") val ids: Ids? = null,
|
||||
@JsonProperty("images") val images: Images? = null,
|
||||
)
|
||||
|
||||
data class Seasons(
|
||||
@JsonProperty("aired_episodes") val airedEpisodes: Int? = null,
|
||||
@JsonProperty("episode_count") val episodeCount: Int? = null,
|
||||
@JsonProperty("episodes") val episodes: List<TraktEpisode>? = null,
|
||||
@JsonProperty("first_aired") val firstAired: String? = null,
|
||||
@JsonProperty("ids") val ids: Ids? = null,
|
||||
@JsonProperty("images") val images: Images? = null,
|
||||
@JsonProperty("network") val network: String? = null,
|
||||
@JsonProperty("number") val number: Int? = null,
|
||||
@JsonProperty("overview") val overview: String? = null,
|
||||
@JsonProperty("rating") val rating: Double? = null,
|
||||
@JsonProperty("title") val title: String? = null,
|
||||
@JsonProperty("updated_at") val updatedAt: String? = null,
|
||||
@JsonProperty("votes") val votes: Int? = null,
|
||||
)
|
||||
|
||||
data class TraktEpisode(
|
||||
@JsonProperty("available_translations") val availableTranslations: List<String>? = null,
|
||||
@JsonProperty("comment_count") val commentCount: Int? = null,
|
||||
@JsonProperty("episode_type") val episodeType: String? = null,
|
||||
@JsonProperty("first_aired") val firstAired: String? = null,
|
||||
@JsonProperty("ids") val ids: Ids? = null,
|
||||
@JsonProperty("images") val images: Images? = null,
|
||||
@JsonProperty("number") val number: Int? = null,
|
||||
@JsonProperty("number_abs") val numberAbs: Int? = null,
|
||||
@JsonProperty("overview") val overview: String? = null,
|
||||
@JsonProperty("rating") val rating: Double? = null,
|
||||
@JsonProperty("runtime") val runtime: Int? = null,
|
||||
@JsonProperty("season") val season: Int? = null,
|
||||
@JsonProperty("title") val title: String? = null,
|
||||
@JsonProperty("updated_at") val updatedAt: String? = null,
|
||||
@JsonProperty("votes") val votes: Int? = null,
|
||||
)
|
||||
|
||||
data class LinkData(
|
||||
val id: Int? = null,
|
||||
val traktId: Int? = null,
|
||||
val traktSlug: String? = null,
|
||||
val tmdbId: Int? = null,
|
||||
val imdbId: String? = null,
|
||||
val tvdbId: Int? = null,
|
||||
val tvrageId: String? = null,
|
||||
val type: String? = null,
|
||||
val season: Int? = null,
|
||||
val episode: Int? = null,
|
||||
val aniId: String? = null,
|
||||
val animeId: String? = null,
|
||||
val title: String? = null,
|
||||
val year: Int? = null,
|
||||
val orgTitle: String? = null,
|
||||
val isAnime: Boolean = false,
|
||||
val airedYear: Int? = null,
|
||||
val lastSeason: Int? = null,
|
||||
val epsTitle: String? = null,
|
||||
val jpTitle: String? = null,
|
||||
val date: String? = null,
|
||||
val airedDate: String? = null,
|
||||
val isAsian: Boolean = false,
|
||||
val isBollywood: Boolean = false,
|
||||
val isCartoon: Boolean = false,
|
||||
)
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
package com.lagradost.cloudstream3.mvvm
|
||||
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.LiveData
|
||||
|
||||
/** NOTE: Only one observer at a time per value */
|
||||
fun <T> LifecycleOwner.observe(liveData: LiveData<T>, action: (t: T) -> Unit) {
|
||||
liveData.removeObservers(this)
|
||||
liveData.observe(this) { it?.let { t -> action(t) } }
|
||||
}
|
||||
|
||||
/** NOTE: Only one observer at a time per value */
|
||||
fun <T> LifecycleOwner.observeNullable(liveData: LiveData<T>, action: (t: T) -> Unit) {
|
||||
liveData.removeObservers(this)
|
||||
liveData.observe(this) { action(it) }
|
||||
}
|
|
@ -17,6 +17,8 @@ import java.net.URI
|
|||
class CloudflareKiller : Interceptor {
|
||||
companion object {
|
||||
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> {
|
||||
return cookie.split(";").associate {
|
||||
val split = it.split("=")
|
||||
|
@ -48,15 +50,23 @@ class CloudflareKiller : Interceptor {
|
|||
|
||||
override fun intercept(chain: Interceptor.Chain): Response = runBlocking {
|
||||
val request = chain.request()
|
||||
val cookies = savedCookies[request.url.host]
|
||||
|
||||
if (cookies == null) {
|
||||
bypassCloudflare(request)?.let {
|
||||
Log.d(TAG, "Succeeded bypassing cloudflare: ${request.url}")
|
||||
return@runBlocking it
|
||||
when (val cookies = savedCookies[request.url.host]) {
|
||||
null -> {
|
||||
val response = chain.proceed(request)
|
||||
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}" }
|
||||
|
|
|
@ -2,6 +2,8 @@ package com.lagradost.cloudstream3.network
|
|||
|
||||
import android.annotation.SuppressLint
|
||||
import android.net.http.SslError
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.webkit.*
|
||||
import com.lagradost.cloudstream3.AcraApplication
|
||||
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 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 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(
|
||||
val interceptUrl: Regex,
|
||||
val additionalUrls: List<Regex> = emptyList(),
|
||||
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 {
|
||||
|
||||
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 {
|
||||
private const val DEFAULT_TIMEOUT = 60_000L
|
||||
var webViewUserAgent: String? = null
|
||||
|
||||
@JvmName("getWebViewUserAgent1")
|
||||
|
@ -136,6 +161,14 @@ class WebViewResolver(
|
|||
val webViewUrl = request.url.toString()
|
||||
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)) {
|
||||
fixedRequest = request.toRequest()?.also {
|
||||
requestCallBack(it)
|
||||
|
@ -241,7 +274,7 @@ class WebViewResolver(
|
|||
|
||||
var loop = 0
|
||||
// Timeouts after this amount, 60s
|
||||
val totalTime = 60000L
|
||||
val totalTime = timeout
|
||||
|
||||
val delayTime = 100L
|
||||
|
||||
|
|
|
@ -36,7 +36,9 @@ abstract class Plugin {
|
|||
Log.i(PLUGIN_TAG, "Adding ${element.name} (${element.mainUrl}) MainAPI")
|
||||
element.sourcePlugin = this.__filename
|
||||
// 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)
|
||||
}
|
||||
|
||||
|
@ -51,10 +53,14 @@ abstract class Plugin {
|
|||
}
|
||||
|
||||
class Manifest {
|
||||
@JsonProperty("name") var name: String? = null
|
||||
@JsonProperty("pluginClassName") var pluginClassName: String? = null
|
||||
@JsonProperty("version") var version: Int? = null
|
||||
@JsonProperty("requiresResources") var requiresResources: Boolean = false
|
||||
@JsonProperty("name")
|
||||
var name: String? = null
|
||||
@JsonProperty("pluginClassName")
|
||||
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> {
|
||||
return getKey(PLUGINS_KEY) ?: emptyArray()
|
||||
}
|
||||
|
@ -163,7 +177,11 @@ object PluginManager {
|
|||
private val classLoaders: MutableMap<PathClassLoader, Plugin> =
|
||||
HashMap<PathClassLoader, Plugin>()
|
||||
|
||||
private var loadedLocalPlugins = false
|
||||
var loadedLocalPlugins = false
|
||||
private set
|
||||
|
||||
var loadedOnlinePlugins = false
|
||||
private set
|
||||
private val gson = Gson()
|
||||
|
||||
private suspend fun maybeLoadPlugin(context: Context, file: File) {
|
||||
|
@ -277,6 +295,7 @@ object PluginManager {
|
|||
}
|
||||
|
||||
// ioSafe {
|
||||
loadedOnlinePlugins = true
|
||||
afterPluginsLoadedEvent.invoke(false)
|
||||
// }
|
||||
|
||||
|
@ -289,7 +308,7 @@ object PluginManager {
|
|||
* 2. Fetch all not downloaded plugins
|
||||
* 3. Download them and reload plugins
|
||||
**/
|
||||
fun downloadNotExistingPluginsAndLoad(activity: Activity) {
|
||||
fun downloadNotExistingPluginsAndLoad(activity: Activity, mode: AutoDownloadMode) {
|
||||
val newDownloadPlugins = mutableListOf<String>()
|
||||
val urls = (getKey<Array<RepositoryData>>(REPOSITORIES_KEY)
|
||||
?: emptyArray()) + PREBUILT_REPOSITORIES
|
||||
|
@ -303,6 +322,8 @@ object PluginManager {
|
|||
// Iterate online repos and returns not downloaded plugins
|
||||
val notDownloadedPlugins = onlinePlugins.mapNotNull { onlineData ->
|
||||
val sitePlugin = onlineData.second
|
||||
val tvtypes = sitePlugin.tvTypes ?: listOf()
|
||||
|
||||
//Don't include empty urls
|
||||
if (sitePlugin.url.isBlank()) {
|
||||
return@mapNotNull null
|
||||
|
@ -317,22 +338,29 @@ object PluginManager {
|
|||
return@mapNotNull null
|
||||
}
|
||||
|
||||
//Omit lang not selected on language setting
|
||||
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")
|
||||
|
||||
//Omit NSFW, if disabled
|
||||
sitePlugin.tvTypes?.let { tvtypes ->
|
||||
if (!settingsForProvider.enableAdult) {
|
||||
if (tvtypes.contains(TvType.NSFW.name)) {
|
||||
return@mapNotNull null
|
||||
}
|
||||
//Omit non-NSFW if mode is set to NSFW only
|
||||
if (mode == AutoDownloadMode.NsfwOnly) {
|
||||
if (tvtypes.contains(TvType.NSFW.name) == false) {
|
||||
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(
|
||||
url = sitePlugin.url,
|
||||
internalName = sitePlugin.internalName,
|
||||
|
@ -401,7 +429,6 @@ object PluginManager {
|
|||
**/
|
||||
fun loadAllLocalPlugins(context: Context, forceReload: Boolean) {
|
||||
val dir = File(LOCAL_PLUGINS_PATH)
|
||||
removeKey(PLUGINS_KEY_LOCAL)
|
||||
|
||||
if (!dir.exists()) {
|
||||
val res = dir.mkdirs()
|
||||
|
@ -449,6 +476,14 @@ object PluginManager {
|
|||
Log.i(TAG, "Loading plugin: $data")
|
||||
|
||||
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)
|
||||
var manifest: Plugin.Manifest
|
||||
loader.getResourceAsStream("manifest.json").use { stream ->
|
||||
|
@ -531,10 +566,14 @@ object PluginManager {
|
|||
}
|
||||
|
||||
// remove all registered apis
|
||||
APIHolder.apis.filter { api -> api.sourcePlugin == plugin.__filename }.forEach {
|
||||
removePluginMapping(it)
|
||||
synchronized(APIHolder.apis) {
|
||||
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 }
|
||||
|
||||
classLoaders.values.removeIf { v -> v == plugin }
|
||||
|
@ -692,4 +731,4 @@ object PluginManager {
|
|||
return null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -95,15 +95,11 @@ object RepositoryManager {
|
|||
}
|
||||
} else if (fixedUrl.matches("^[a-zA-Z0-9!_-]+$".toRegex())) {
|
||||
suspendSafeApiCall {
|
||||
app.get("https://l.cloudstream.cf/${fixedUrl}", allowRedirects = false).let {
|
||||
it.headers["Location"]?.let { url ->
|
||||
return@suspendSafeApiCall if (!url.startsWith("https://cutt.ly/branded-domains")) url
|
||||
else null
|
||||
}
|
||||
app.get("https://cutt.ly/${fixedUrl}", allowRedirects = false).let { it2 ->
|
||||
it2.headers["Location"]?.let { url ->
|
||||
return@suspendSafeApiCall if (url.startsWith("https://cutt.ly/404")) url else null
|
||||
}
|
||||
app.get("https://cutt.ly/${fixedUrl}", allowRedirects = false).let { it2 ->
|
||||
it2.headers["Location"]?.let { url ->
|
||||
if (url.startsWith("https://cutt.ly/404")) return@suspendSafeApiCall null
|
||||
if (url.removeSuffix("/") == "https://cutt.ly") return@suspendSafeApiCall null
|
||||
return@suspendSafeApiCall url
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,22 +8,14 @@ import com.lagradost.cloudstream3.AcraApplication.Companion.setKey
|
|||
import com.lagradost.cloudstream3.R
|
||||
import java.security.MessageDigest
|
||||
import com.lagradost.cloudstream3.app
|
||||
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
|
||||
import com.lagradost.cloudstream3.utils.Coroutines.main
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
|
||||
object VotingApi { // please do not cheat the votes lol
|
||||
private const val LOGKEY = "VotingApi"
|
||||
|
||||
enum class VoteType(val value: Int) {
|
||||
UPVOTE(1),
|
||||
DOWNVOTE(-1),
|
||||
NONE(0)
|
||||
}
|
||||
|
||||
private val apiDomain = "https://api.countapi.xyz"
|
||||
private const val apiDomain = "https://counterapi.com/api"
|
||||
|
||||
private fun transformUrl(url: String): String = // dont touch or all votes get reset
|
||||
MessageDigest
|
||||
|
@ -35,12 +27,12 @@ object VotingApi { // please do not cheat the votes lol
|
|||
return getVotes(url)
|
||||
}
|
||||
|
||||
suspend fun SitePlugin.vote(requestType: VoteType): Int {
|
||||
return vote(url, requestType)
|
||||
fun SitePlugin.hasVoted(): Boolean {
|
||||
return hasVoted(url)
|
||||
}
|
||||
|
||||
fun SitePlugin.getVoteType(): VoteType {
|
||||
return getVoteType(url)
|
||||
suspend fun SitePlugin.vote(): Int {
|
||||
return vote(url)
|
||||
}
|
||||
|
||||
fun SitePlugin.canVote(): Boolean {
|
||||
|
@ -50,28 +42,31 @@ object VotingApi { // please do not cheat the votes lol
|
|||
// Plugin url to Int
|
||||
private val votesCache = mutableMapOf<String, Int>()
|
||||
|
||||
suspend fun getVotes(pluginUrl: String): Int {
|
||||
val url = "${apiDomain}/get/cs3-votes/${transformUrl(pluginUrl)}"
|
||||
private fun getRepository(pluginUrl: String) = 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")
|
||||
return votesCache[pluginUrl] ?: app.get(url).parsedSafe<Result>()?.value?.also {
|
||||
votesCache[pluginUrl] = it
|
||||
} ?: (0.also {
|
||||
ioSafe {
|
||||
createBucket(pluginUrl)
|
||||
return app.get(url).parsedSafe<Result>()?.value ?: 0
|
||||
}
|
||||
|
||||
private suspend fun writeVote(pluginUrl: String): Boolean {
|
||||
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 {
|
||||
return getKey("cs3-votes/${transformUrl(pluginUrl)}") ?: VoteType.NONE
|
||||
}
|
||||
|
||||
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 hasVoted(pluginUrl: String) =
|
||||
getKey("cs3-votes/${transformUrl(pluginUrl)}") ?: false
|
||||
|
||||
fun canVote(pluginUrl: String): Boolean {
|
||||
if (!PluginManager.urlPlugins.contains(pluginUrl)) return false
|
||||
|
@ -79,7 +74,7 @@ object VotingApi { // please do not cheat the votes lol
|
|||
}
|
||||
|
||||
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.
|
||||
voteLock.withLock {
|
||||
if (!canVote(pluginUrl)) {
|
||||
|
@ -90,33 +85,21 @@ object VotingApi { // please do not cheat the votes lol
|
|||
return getVotes(pluginUrl)
|
||||
}
|
||||
|
||||
val savedType: VoteType =
|
||||
getKey("cs3-votes/${transformUrl(pluginUrl)}") ?: VoteType.NONE
|
||||
|
||||
val newType = if (requestType == savedType) VoteType.NONE else requestType
|
||||
val changeValue = if (requestType == savedType) {
|
||||
-requestType.value
|
||||
} 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
|
||||
if (hasVoted(pluginUrl)) {
|
||||
main {
|
||||
Toast.makeText(context, R.string.already_voted, Toast.LENGTH_SHORT)
|
||||
.show()
|
||||
}
|
||||
return getVotes(pluginUrl)
|
||||
}
|
||||
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()
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue