Compare commits
553 Commits
Author | SHA1 | Date |
---|---|---|
KingLucius | b3e3dadc72 | |
KingLucius | b87fdfbf85 | |
KingLucius | dff56026de | |
IndusAryan | 5502e478c4 | |
CranberrySoup | 960f8449b7 | |
Ömer Faruk Sancak | d0852449a5 | |
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 | |
Cloudburst | 7bfcf25df4 | |
Lag | 2d7126d71f | |
Lag | 40a4f319b6 | |
Lag | 19dc1a2456 | |
Cloudburst | ac1012bcb8 | |
Cloudburst | ec3950ed4f | |
Hosted Weblate | 3e2b0f2a17 | |
LikDev-256 | 29174dbb30 | |
Lag | 7b47f93190 | |
Lag | 13ee8e21d0 | |
recloudstream[bot] | 3a5d872545 | |
Hosted Weblate | fab55d82c4 | |
Hosted Weblate | 8b2881f5f6 | |
PokerFace | 37244ab0f7 | |
Lag | e85b31c35d | |
Hosted Weblate | 1eaa4620dc | |
no-commit | 76545f55c3 | |
Stormunblessed | f0515c4dc9 | |
no-commit | ab324b93e8 | |
Stormunblessed | d6df24eff2 | |
Hosted Weblate | e5834d485b | |
Sarlay | 6524eb220b | |
Allen Baby | 2926dc6c8e | |
Hexated | f722785a37 | |
Lag | aeab423d29 | |
recloudstream[bot] | 1da6a92569 | |
Cloudburst | b2fa765a2d | |
Cloudburst | bec0a2e7b9 | |
Cloudburst | 51137701f2 | |
Hosted Weblate | 5f12d067f9 | |
no-commit | 00a91ca5fb | |
LikDev-256 | 33aecfbba5 | |
no-commit | 0185854682 | |
no-commit | b4065b69be | |
MhmdIbrahim1 | b6ac155350 | |
Lag | aacd57cb5d | |
Lag | 3dd0fc6c8e | |
Lag | 135f63afff | |
Lag | 789cd14ef6 | |
recloudstream[bot] | 9d0cce47a6 | |
Lag | 4a8ee55018 | |
Stormunblessed | df6c395acb | |
Cloudburst | 1117271a71 | |
Cloudburst | 7b11b9b585 | |
recloudstream[bot] | 5c20b479e5 | |
Hosted Weblate | 0d2613d183 | |
Cloudburst | dd38556102 | |
reduplicated | 84493b7f3b | |
reduplicated | 4596afee06 | |
Sir Aguacata | 3e2c2a5c86 | |
Terry Hanoman | 6c646d65a8 | |
LagradOst | 329966732f | |
Hosted Weblate | 19b2cae851 | |
recloudstream[bot] | 45eb9758e3 | |
Cloudburst | f6be6081dc | |
recloudstream[bot] | 80f22cea16 | |
Cloudburst | bf78fc95c2 | |
Cloudburst | a148f347cd | |
Cloudburst | ff9942407b | |
Hosted Weblate | 0ea624ff14 | |
Cloudburst | f939e4cff2 | |
no-commit | 2ff90c03ca | |
no-commit | 9988753432 | |
LagradOst | b0921161a3 | |
Cloudburst | 490381451b | |
hexated | b26a41bdaf | |
Hosted Weblate | c7c5fa250e | |
Blatzar | 6e9b1cb855 | |
Blatzar | fd2648df45 | |
Blatzar | 9905618a47 | |
LagradOst | 2771dcb612 | |
LagradOst | 3c82548c20 | |
recloudstream[bot] | 9d11dc76a1 | |
Hosted Weblate | 2a1311673a | |
Blatzar | 83d2e692e0 | |
Blatzar | b2389bf14c | |
LiJu09 | b2b16fccc5 | |
Blatzar | 01f1edab3c | |
reduplicated | 5050ff65c0 | |
Hosted Weblate | de720983a6 | |
Blatzar | 0b4de81811 |
Binary file not shown.
Before Width: | Height: | Size: 58 KiB |
Binary file not shown.
Before Width: | Height: | Size: 136 KiB |
|
@ -1,13 +1,14 @@
|
||||||
import re
|
import re
|
||||||
import glob
|
import glob
|
||||||
import requests
|
import requests
|
||||||
|
import lxml.etree as ET # builtin library doesn't preserve comments
|
||||||
|
|
||||||
|
|
||||||
SETTINGS_PATH = "app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsGeneral.kt"
|
SETTINGS_PATH = "app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsGeneral.kt"
|
||||||
START_MARKER = "/* begin language list */"
|
START_MARKER = "/* begin language list */"
|
||||||
END_MARKER = "/* end language list */"
|
END_MARKER = "/* end language list */"
|
||||||
XML_NAME = "app/src/main/res/values-"
|
XML_NAME = "app/src/main/res/values-"
|
||||||
ISO_MAP_URL = "https://gist.githubusercontent.com/Josantonius/b455e315bc7f790d14b136d61d9ae469/raw"
|
ISO_MAP_URL = "https://raw.githubusercontent.com/haliaeetus/iso-639/master/data/iso_639-1.min.json"
|
||||||
INDENT = " "*4
|
INDENT = " "*4
|
||||||
|
|
||||||
iso_map = requests.get(ISO_MAP_URL, timeout=300).json()
|
iso_map = requests.get(ISO_MAP_URL, timeout=300).json()
|
||||||
|
@ -27,7 +28,8 @@ for lang in re.finditer(r'Triple\("(.*)", "(.*)", "(.*)"\)', rest):
|
||||||
for folder in glob.glob(f"{XML_NAME}*"):
|
for folder in glob.glob(f"{XML_NAME}*"):
|
||||||
iso = folder[len(XML_NAME):]
|
iso = folder[len(XML_NAME):]
|
||||||
if iso not in languages.keys():
|
if iso not in languages.keys():
|
||||||
languages[iso] = ("", iso_map.get(iso.lower(),iso))
|
entry = iso_map.get(iso.lower(),{'nativeName':iso})
|
||||||
|
languages[iso] = ("", entry['nativeName'].split(',')[0])
|
||||||
|
|
||||||
# Create triples
|
# Create triples
|
||||||
triples = []
|
triples = []
|
||||||
|
@ -44,4 +46,18 @@ open(SETTINGS_PATH, "w+",encoding='utf-8').write(
|
||||||
"\n" +
|
"\n" +
|
||||||
END_MARKER +
|
END_MARKER +
|
||||||
after_src
|
after_src
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Go through each values.xml file and fix escaped \@string
|
||||||
|
for file in glob.glob(f"{XML_NAME}*/strings.xml"):
|
||||||
|
try:
|
||||||
|
tree = ET.parse(file)
|
||||||
|
for child in tree.getroot():
|
||||||
|
if child.text.startswith("\\@string/"):
|
||||||
|
print(f"[{file}] fixing {child.attrib['name']}")
|
||||||
|
child.text = child.text.replace("\\@string/", "@string/")
|
||||||
|
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}")
|
||||||
|
|
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:
|
steps:
|
||||||
- name: Generate access token
|
- name: Generate access token
|
||||||
id: generate_token
|
id: generate_token
|
||||||
uses: tibdex/github-app-token@v1
|
uses: tibdex/github-app-token@v2
|
||||||
with:
|
with:
|
||||||
app_id: ${{ secrets.GH_APP_ID }}
|
app_id: ${{ secrets.GH_APP_ID }}
|
||||||
private_key: ${{ secrets.GH_APP_KEY }}
|
private_key: ${{ secrets.GH_APP_KEY }}
|
||||||
repository: "recloudstream/secrets"
|
repository: "recloudstream/secrets"
|
||||||
- name: Generate access token (archive)
|
- name: Generate access token (archive)
|
||||||
id: generate_archive_token
|
id: generate_archive_token
|
||||||
uses: tibdex/github-app-token@v1
|
uses: tibdex/github-app-token@v2
|
||||||
with:
|
with:
|
||||||
app_id: ${{ secrets.GH_APP_ID }}
|
app_id: ${{ secrets.GH_APP_ID }}
|
||||||
private_key: ${{ secrets.GH_APP_KEY }}
|
private_key: ${{ secrets.GH_APP_KEY }}
|
||||||
repository: "recloudstream/cloudstream-archive"
|
repository: "recloudstream/cloudstream-archive"
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v4
|
||||||
- name: Set up JDK 11
|
- name: Set up JDK 17
|
||||||
uses: actions/setup-java@v2
|
uses: actions/setup-java@v4
|
||||||
with:
|
with:
|
||||||
java-version: '11'
|
java-version: '17'
|
||||||
distribution: 'adopt'
|
distribution: 'adopt'
|
||||||
- name: Grant execute permission for gradlew
|
- name: Grant execute permission for gradlew
|
||||||
run: chmod +x gradlew
|
run: chmod +x gradlew
|
||||||
|
@ -56,7 +56,9 @@ jobs:
|
||||||
SIGNING_KEY_ALIAS: "key0"
|
SIGNING_KEY_ALIAS: "key0"
|
||||||
SIGNING_KEY_PASSWORD: ${{ steps.fetch_keystore.outputs.key_pwd }}
|
SIGNING_KEY_PASSWORD: ${{ steps.fetch_keystore.outputs.key_pwd }}
|
||||||
SIGNING_STORE_PASSWORD: ${{ steps.fetch_keystore.outputs.key_pwd }}
|
SIGNING_STORE_PASSWORD: ${{ steps.fetch_keystore.outputs.key_pwd }}
|
||||||
- uses: actions/checkout@v3
|
SIMKL_CLIENT_ID: ${{ secrets.SIMKL_CLIENT_ID }}
|
||||||
|
SIMKL_CLIENT_SECRET: ${{ secrets.SIMKL_CLIENT_SECRET }}
|
||||||
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
repository: "recloudstream/cloudstream-archive"
|
repository: "recloudstream/cloudstream-archive"
|
||||||
token: ${{ steps.generate_archive_token.outputs.token }}
|
token: ${{ steps.generate_archive_token.outputs.token }}
|
||||||
|
|
|
@ -20,7 +20,7 @@ jobs:
|
||||||
steps:
|
steps:
|
||||||
- name: Generate access token
|
- name: Generate access token
|
||||||
id: generate_token
|
id: generate_token
|
||||||
uses: tibdex/github-app-token@v1
|
uses: tibdex/github-app-token@v2
|
||||||
with:
|
with:
|
||||||
app_id: ${{ secrets.GH_APP_ID }}
|
app_id: ${{ secrets.GH_APP_ID }}
|
||||||
private_key: ${{ secrets.GH_APP_KEY }}
|
private_key: ${{ secrets.GH_APP_KEY }}
|
||||||
|
@ -42,13 +42,14 @@ jobs:
|
||||||
cd $GITHUB_WORKSPACE/dokka/
|
cd $GITHUB_WORKSPACE/dokka/
|
||||||
rm -rf "./-cloudstream"
|
rm -rf "./-cloudstream"
|
||||||
|
|
||||||
- name: Setup JDK 11
|
- name: Setup JDK 17
|
||||||
uses: actions/setup-java@v1
|
uses: actions/setup-java@v4
|
||||||
with:
|
with:
|
||||||
java-version: 11
|
java-version: 17
|
||||||
|
distribution: 'adopt'
|
||||||
|
|
||||||
- name: Setup Android SDK
|
- name: Setup Android SDK
|
||||||
uses: android-actions/setup-android@v2
|
uses: android-actions/setup-android@v3
|
||||||
|
|
||||||
- name: Generate Dokka
|
- name: Generate Dokka
|
||||||
run: |
|
run: |
|
||||||
|
|
|
@ -10,11 +10,12 @@ jobs:
|
||||||
steps:
|
steps:
|
||||||
- name: Generate access token
|
- name: Generate access token
|
||||||
id: generate_token
|
id: generate_token
|
||||||
uses: tibdex/github-app-token@v1
|
uses: tibdex/github-app-token@v2
|
||||||
with:
|
with:
|
||||||
app_id: ${{ secrets.GH_APP_ID }}
|
app_id: ${{ secrets.GH_APP_ID }}
|
||||||
private_key: ${{ secrets.GH_APP_KEY }}
|
private_key: ${{ secrets.GH_APP_KEY }}
|
||||||
- name: Similarity analysis
|
- name: Similarity analysis
|
||||||
|
id: similarity
|
||||||
uses: actions-cool/issues-similarity-analysis@v1
|
uses: actions-cool/issues-similarity-analysis@v1
|
||||||
with:
|
with:
|
||||||
token: ${{ steps.generate_token.outputs.token }}
|
token: ${{ steps.generate_token.outputs.token }}
|
||||||
|
@ -24,7 +25,19 @@ jobs:
|
||||||
### Your issue looks similar to these issues:
|
### Your issue looks similar to these issues:
|
||||||
Please close if duplicate.
|
Please close if duplicate.
|
||||||
comment-body: '${index}. ${similarity} #${number}'
|
comment-body: '${index}. ${similarity} #${number}'
|
||||||
- uses: actions/checkout@v2
|
- name: Label if possible duplicate
|
||||||
|
if: steps.similarity.outputs.similar-issues-found =='true'
|
||||||
|
uses: actions/github-script@v7
|
||||||
|
with:
|
||||||
|
github-token: ${{ steps.generate_token.outputs.token }}
|
||||||
|
script: |
|
||||||
|
github.rest.issues.addLabels({
|
||||||
|
issue_number: context.issue.number,
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
labels: ["possible duplicate"]
|
||||||
|
})
|
||||||
|
- uses: actions/checkout@v4
|
||||||
- name: Automatically close issues that dont follow the issue template
|
- name: Automatically close issues that dont follow the issue template
|
||||||
uses: lucasbento/auto-close-issues@v1.0.2
|
uses: lucasbento/auto-close-issues@v1.0.2
|
||||||
with:
|
with:
|
||||||
|
@ -53,6 +66,18 @@ jobs:
|
||||||
Please do not report any provider bugs here. This repository does not contain any providers. Please find the appropriate repository and report your issue there or join the [discord](https://discord.gg/5Hus6fM).
|
Please do not report any provider bugs here. This repository does not contain any providers. Please find the appropriate repository and report your issue there or join the [discord](https://discord.gg/5Hus6fM).
|
||||||
|
|
||||||
Found provider name: `${{ steps.provider_check.outputs.name }}`
|
Found provider name: `${{ steps.provider_check.outputs.name }}`
|
||||||
|
- name: Label if mentions provider
|
||||||
|
if: steps.provider_check.outputs.name != 'none'
|
||||||
|
uses: actions/github-script@v7
|
||||||
|
with:
|
||||||
|
github-token: ${{ steps.generate_token.outputs.token }}
|
||||||
|
script: |
|
||||||
|
github.rest.issues.addLabels({
|
||||||
|
issue_number: context.issue.number,
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
labels: ["possible provider issue"]
|
||||||
|
})
|
||||||
- name: Add eyes reaction to all issues
|
- name: Add eyes reaction to all issues
|
||||||
uses: actions-cool/emoji-helper@v1.0.0
|
uses: actions-cool/emoji-helper@v1.0.0
|
||||||
with:
|
with:
|
||||||
|
|
|
@ -18,16 +18,16 @@ jobs:
|
||||||
steps:
|
steps:
|
||||||
- name: Generate access token
|
- name: Generate access token
|
||||||
id: generate_token
|
id: generate_token
|
||||||
uses: tibdex/github-app-token@v1
|
uses: tibdex/github-app-token@v2
|
||||||
with:
|
with:
|
||||||
app_id: ${{ secrets.GH_APP_ID }}
|
app_id: ${{ secrets.GH_APP_ID }}
|
||||||
private_key: ${{ secrets.GH_APP_KEY }}
|
private_key: ${{ secrets.GH_APP_KEY }}
|
||||||
repository: "recloudstream/secrets"
|
repository: "recloudstream/secrets"
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v4
|
||||||
- name: Set up JDK 11
|
- name: Set up JDK 17
|
||||||
uses: actions/setup-java@v2
|
uses: actions/setup-java@v4
|
||||||
with:
|
with:
|
||||||
java-version: '11'
|
java-version: '17'
|
||||||
distribution: 'adopt'
|
distribution: 'adopt'
|
||||||
- name: Grant execute permission for gradlew
|
- name: Grant execute permission for gradlew
|
||||||
run: chmod +x gradlew
|
run: chmod +x gradlew
|
||||||
|
@ -43,11 +43,14 @@ jobs:
|
||||||
echo "key_pwd=$KEY_PWD" >> $GITHUB_OUTPUT
|
echo "key_pwd=$KEY_PWD" >> $GITHUB_OUTPUT
|
||||||
- name: Run Gradle
|
- name: Run Gradle
|
||||||
run: |
|
run: |
|
||||||
./gradlew assemblePrerelease makeJar androidSourcesJar
|
./gradlew assemblePrerelease build androidSourcesJar
|
||||||
|
./gradlew makeJar # for classes.jar, has to be done after assemblePrerelease
|
||||||
env:
|
env:
|
||||||
SIGNING_KEY_ALIAS: "key0"
|
SIGNING_KEY_ALIAS: "key0"
|
||||||
SIGNING_KEY_PASSWORD: ${{ steps.fetch_keystore.outputs.key_pwd }}
|
SIGNING_KEY_PASSWORD: ${{ steps.fetch_keystore.outputs.key_pwd }}
|
||||||
SIGNING_STORE_PASSWORD: ${{ steps.fetch_keystore.outputs.key_pwd }}
|
SIGNING_STORE_PASSWORD: ${{ steps.fetch_keystore.outputs.key_pwd }}
|
||||||
|
SIMKL_CLIENT_ID: ${{ secrets.SIMKL_CLIENT_ID }}
|
||||||
|
SIMKL_CLIENT_SECRET: ${{ secrets.SIMKL_CLIENT_SECRET }}
|
||||||
- name: Create pre-release
|
- name: Create pre-release
|
||||||
uses: "marvinpinto/action-automatic-releases@latest"
|
uses: "marvinpinto/action-automatic-releases@latest"
|
||||||
with:
|
with:
|
||||||
|
|
|
@ -6,18 +6,18 @@ jobs:
|
||||||
build:
|
build:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v4
|
||||||
- name: Set up JDK 11
|
- name: Set up JDK 17
|
||||||
uses: actions/setup-java@v2
|
uses: actions/setup-java@v4
|
||||||
with:
|
with:
|
||||||
java-version: '11'
|
java-version: '17'
|
||||||
distribution: 'adopt'
|
distribution: 'adopt'
|
||||||
- name: Grant execute permission for gradlew
|
- name: Grant execute permission for gradlew
|
||||||
run: chmod +x gradlew
|
run: chmod +x gradlew
|
||||||
- name: Run Gradle
|
- name: Run Gradle
|
||||||
run: ./gradlew assemblePrereleaseDebug
|
run: ./gradlew assemblePrereleaseDebug
|
||||||
- name: Upload Artifact
|
- name: Upload Artifact
|
||||||
uses: actions/upload-artifact@v2
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: pull-request-build
|
name: pull-request-build
|
||||||
path: "app/build/outputs/apk/prerelease/debug/*.apk"
|
path: "app/build/outputs/apk/prerelease/debug/*.apk"
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
name: Update locale lists
|
name: Fix locale issues
|
||||||
|
|
||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
@ -9,7 +9,7 @@ on:
|
||||||
- master
|
- master
|
||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
group: "locale-list"
|
group: "locale"
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
@ -18,14 +18,17 @@ jobs:
|
||||||
steps:
|
steps:
|
||||||
- name: Generate access token
|
- name: Generate access token
|
||||||
id: generate_token
|
id: generate_token
|
||||||
uses: tibdex/github-app-token@v1
|
uses: tibdex/github-app-token@v2
|
||||||
with:
|
with:
|
||||||
app_id: ${{ secrets.GH_APP_ID }}
|
app_id: ${{ secrets.GH_APP_ID }}
|
||||||
private_key: ${{ secrets.GH_APP_KEY }}
|
private_key: ${{ secrets.GH_APP_KEY }}
|
||||||
repository: "recloudstream/cloudstream"
|
repository: "recloudstream/cloudstream"
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
token: ${{ steps.generate_token.outputs.token }}
|
token: ${{ steps.generate_token.outputs.token }}
|
||||||
|
- name: Install dependencies
|
||||||
|
run: |
|
||||||
|
pip3 install lxml
|
||||||
- name: Edit files
|
- name: Edit files
|
||||||
run: |
|
run: |
|
||||||
python3 .github/locales.py
|
python3 .github/locales.py
|
||||||
|
@ -35,5 +38,5 @@ jobs:
|
||||||
git config --local user.name "recloudstream[bot]"
|
git config --local user.name "recloudstream[bot]"
|
||||||
git add .
|
git add .
|
||||||
# "echo" returns true so the build succeeds, even if no changed files
|
# "echo" returns true so the build succeeds, even if no changed files
|
||||||
git commit -m 'update list of locales' || echo
|
git commit -m 'chore(locales): fix locale issues' || echo
|
||||||
git push
|
git push
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<project version="4">
|
<project version="4">
|
||||||
<component name="CompilerConfiguration">
|
<component name="CompilerConfiguration">
|
||||||
<bytecodeTargetLevel target="11" />
|
<bytecodeTargetLevel target="17" />
|
||||||
</component>
|
</component>
|
||||||
</project>
|
</project>
|
|
@ -4,17 +4,16 @@
|
||||||
<component name="GradleSettings">
|
<component name="GradleSettings">
|
||||||
<option name="linkedExternalProjectsSettings">
|
<option name="linkedExternalProjectsSettings">
|
||||||
<GradleProjectSettings>
|
<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="externalProjectPath" value="$PROJECT_DIR$" />
|
||||||
<option name="gradleJvm" value="11" />
|
<option name="gradleJvm" value="#GRADLE_LOCAL_JAVA_HOME" />
|
||||||
<option name="modules">
|
<option name="modules">
|
||||||
<set>
|
<set>
|
||||||
<option value="$PROJECT_DIR$" />
|
<option value="$PROJECT_DIR$" />
|
||||||
<option value="$PROJECT_DIR$/app" />
|
<option value="$PROJECT_DIR$/app" />
|
||||||
|
<option value="$PROJECT_DIR$/library" />
|
||||||
</set>
|
</set>
|
||||||
</option>
|
</option>
|
||||||
|
<option name="resolveExternalAnnotations" value="false" />
|
||||||
</GradleProjectSettings>
|
</GradleProjectSettings>
|
||||||
</option>
|
</option>
|
||||||
</component>
|
</component>
|
||||||
|
|
10
README.md
10
README.md
|
@ -9,15 +9,11 @@
|
||||||
+ **AdFree**, No ads whatsoever
|
+ **AdFree**, No ads whatsoever
|
||||||
+ No tracking/analytics
|
+ No tracking/analytics
|
||||||
+ Bookmarks
|
+ Bookmarks
|
||||||
+ Download and stream movies, tv-shows and anime
|
+ Phone and TV support
|
||||||
+ Chromecast
|
+ Chromecast
|
||||||
|
+ Extension system for personal customization
|
||||||
### 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"/>
|
|
||||||
|
|
||||||
### Supported languages:
|
### Supported languages:
|
||||||
<a href="https://hosted.weblate.org/engage/cloudstream/">
|
<a href="https://hosted.weblate.org/engage/cloudstream/">
|
||||||
<img src="https://hosted.weblate.org/widgets/cloudstream/-/app/multi-auto.svg" alt="Translation status" />
|
<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.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.io.ByteArrayOutputStream
|
||||||
import java.net.URL
|
import java.net.URL
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
id("com.android.application")
|
id("com.android.application")
|
||||||
|
id("com.google.devtools.ksp")
|
||||||
id("kotlin-android")
|
id("kotlin-android")
|
||||||
id("kotlin-kapt")
|
|
||||||
id("kotlin-android-extensions")
|
|
||||||
id("org.jetbrains.dokka")
|
id("org.jetbrains.dokka")
|
||||||
}
|
}
|
||||||
|
|
||||||
val tmpFilePath = System.getProperty("user.home") + "/work/_temp/keystore/"
|
val tmpFilePath = System.getProperty("user.home") + "/work/_temp/keystore/"
|
||||||
val prereleaseStoreFile: File? = File(tmpFilePath).listFiles()?.first()
|
val prereleaseStoreFile: File? = File(tmpFilePath).listFiles()?.first()
|
||||||
|
var isLibraryDebug = false
|
||||||
|
|
||||||
fun String.execute() = ByteArrayOutputStream().use { baot ->
|
fun String.execute() = ByteArrayOutputStream().use { baot ->
|
||||||
if (project.exec {
|
if (project.exec {
|
||||||
workingDir = projectDir
|
workingDir = projectDir
|
||||||
commandLine = this@execute.split(Regex("\\s"))
|
commandLine = this@execute.split(Regex("\\s"))
|
||||||
standardOutput = baot
|
standardOutput = baot
|
||||||
}.exitValue == 0)
|
}.exitValue == 0)
|
||||||
String(baot.toByteArray()).trim()
|
String(baot.toByteArray()).trim()
|
||||||
else null
|
else null
|
||||||
}
|
}
|
||||||
|
@ -28,6 +30,18 @@ android {
|
||||||
testOptions {
|
testOptions {
|
||||||
unitTests.isReturnDefaultValues = true
|
unitTests.isReturnDefaultValues = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
viewBinding {
|
||||||
|
enable = true
|
||||||
|
}
|
||||||
|
|
||||||
|
/* disable this for now
|
||||||
|
externalNativeBuild {
|
||||||
|
cmake {
|
||||||
|
path("CMakeLists.txt")
|
||||||
|
}
|
||||||
|
}*/
|
||||||
|
|
||||||
signingConfigs {
|
signingConfigs {
|
||||||
create("prerelease") {
|
create("prerelease") {
|
||||||
if (prereleaseStoreFile != null) {
|
if (prereleaseStoreFile != null) {
|
||||||
|
@ -39,33 +53,44 @@ android {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
compileSdk = 33
|
compileSdk = 34
|
||||||
buildToolsVersion = "30.0.3"
|
buildToolsVersion = "34.0.0"
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId = "com.lagradost.cloudstream3"
|
applicationId = "com.lagradost.cloudstream3"
|
||||||
minSdk = 21
|
minSdk = 21
|
||||||
targetSdk = 33
|
targetSdk = 33 /* Android 14 is Fu*ked
|
||||||
|
^ https://developer.android.com/about/versions/14/behavior-changes-14#safer-dynamic-code-loading*/
|
||||||
versionCode = 55
|
versionCode = 63
|
||||||
versionName = "3.4.0"
|
versionName = "4.3.2"
|
||||||
|
|
||||||
resValue("string", "app_version", "${defaultConfig.versionName}${versionNameSuffix ?: ""}")
|
resValue("string", "app_version", "${defaultConfig.versionName}${versionNameSuffix ?: ""}")
|
||||||
|
|
||||||
resValue("string", "commit_hash", "git rev-parse --short HEAD".execute() ?: "")
|
resValue("string", "commit_hash", "git rev-parse --short HEAD".execute() ?: "")
|
||||||
|
|
||||||
resValue("bool", "is_prerelease", "false")
|
resValue("bool", "is_prerelease", "false")
|
||||||
|
|
||||||
|
// Reads local.properties
|
||||||
|
val localProperties = gradleLocalProperties(rootDir)
|
||||||
|
|
||||||
|
buildConfigField(
|
||||||
|
"long",
|
||||||
|
"BUILD_DATE",
|
||||||
|
"${System.currentTimeMillis()}"
|
||||||
|
)
|
||||||
buildConfigField(
|
buildConfigField(
|
||||||
"String",
|
"String",
|
||||||
"BUILDDATE",
|
"SIMKL_CLIENT_ID",
|
||||||
"new java.text.SimpleDateFormat(\"yyyy-MM-dd HH:mm\").format(new java.util.Date(" + System.currentTimeMillis() + "L));"
|
"\"" + (System.getenv("SIMKL_CLIENT_ID") ?: localProperties["simkl.id"]) + "\""
|
||||||
|
)
|
||||||
|
buildConfigField(
|
||||||
|
"String",
|
||||||
|
"SIMKL_CLIENT_SECRET",
|
||||||
|
"\"" + (System.getenv("SIMKL_CLIENT_SECRET") ?: localProperties["simkl.secret"]) + "\""
|
||||||
)
|
)
|
||||||
|
|
||||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
|
|
||||||
kapt {
|
ksp {
|
||||||
includeCompileClasspath = true
|
arg("room.schemaLocation", "$projectDir/schemas")
|
||||||
|
arg("exportSchema", "true")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -74,14 +99,22 @@ android {
|
||||||
isDebuggable = false
|
isDebuggable = false
|
||||||
isMinifyEnabled = false
|
isMinifyEnabled = false
|
||||||
isShrinkResources = false
|
isShrinkResources = false
|
||||||
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
|
proguardFiles(
|
||||||
|
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||||
|
"proguard-rules.pro"
|
||||||
|
)
|
||||||
}
|
}
|
||||||
debug {
|
debug {
|
||||||
|
isLibraryDebug = true
|
||||||
isDebuggable = true
|
isDebuggable = true
|
||||||
applicationIdSuffix = ".debug"
|
applicationIdSuffix = ".debug"
|
||||||
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
|
proguardFiles(
|
||||||
|
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||||
|
"proguard-rules.pro"
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
flavorDimensions.add("state")
|
flavorDimensions.add("state")
|
||||||
productFlavors {
|
productFlavors {
|
||||||
create("stable") {
|
create("stable") {
|
||||||
|
@ -98,20 +131,22 @@ android {
|
||||||
versionCode = (System.currentTimeMillis() / 60000).toInt()
|
versionCode = (System.currentTimeMillis() / 60000).toInt()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
compileOptions {
|
compileOptions {
|
||||||
isCoreLibraryDesugaringEnabled = true
|
isCoreLibraryDesugaringEnabled = true
|
||||||
|
|
||||||
sourceCompatibility = JavaVersion.VERSION_1_8
|
sourceCompatibility = JavaVersion.VERSION_1_8
|
||||||
targetCompatibility = JavaVersion.VERSION_1_8
|
targetCompatibility = JavaVersion.VERSION_1_8
|
||||||
}
|
}
|
||||||
kotlinOptions {
|
|
||||||
jvmTarget = "1.8"
|
|
||||||
freeCompilerArgs = listOf("-Xjvm-default=compatibility")
|
|
||||||
}
|
|
||||||
lint {
|
lint {
|
||||||
abortOnError = false
|
abortOnError = false
|
||||||
checkReleaseBuilds = false
|
checkReleaseBuilds = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
buildFeatures {
|
||||||
|
buildConfig = true
|
||||||
|
}
|
||||||
|
|
||||||
namespace = "com.lagradost.cloudstream3"
|
namespace = "com.lagradost.cloudstream3"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -120,119 +155,122 @@ repositories {
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation("com.google.android.mediahome:video:1.0.0")
|
// Testing
|
||||||
implementation("androidx.test.ext:junit-ktx:1.1.3")
|
|
||||||
testImplementation("org.json:json:20180813")
|
|
||||||
|
|
||||||
implementation("androidx.core:core-ktx:1.8.0")
|
|
||||||
implementation("androidx.appcompat:appcompat:1.4.2") // need target 32 for 1.5.0
|
|
||||||
|
|
||||||
// dont change this to 1.6.0 it looks ugly af
|
|
||||||
implementation("com.google.android.material:material:1.5.0")
|
|
||||||
implementation("androidx.constraintlayout:constraintlayout:2.1.4")
|
|
||||||
implementation("androidx.navigation:navigation-fragment-ktx:2.5.1")
|
|
||||||
implementation("androidx.navigation:navigation-ui-ktx:2.5.1")
|
|
||||||
implementation("androidx.lifecycle:lifecycle-livedata-ktx:2.5.1")
|
|
||||||
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.1")
|
|
||||||
testImplementation("junit:junit:4.13.2")
|
testImplementation("junit:junit:4.13.2")
|
||||||
androidTestImplementation("androidx.test.ext:junit:1.1.3")
|
testImplementation("org.json:json:20240303")
|
||||||
androidTestImplementation("androidx.test.espresso:espresso-core:3.4.0")
|
androidTestImplementation("androidx.test:core")
|
||||||
|
implementation("androidx.test.ext:junit-ktx:1.1.5")
|
||||||
|
androidTestImplementation("androidx.test.ext:junit:1.1.5")
|
||||||
|
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
|
||||||
|
|
||||||
//implementation("io.karn:khttp-android:0.1.2") //okhttp instead
|
// Android Core & Lifecycle
|
||||||
// implementation("org.jsoup:jsoup:1.13.1")
|
implementation("androidx.core:core-ktx:1.13.1")
|
||||||
implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.13.1")
|
implementation("androidx.appcompat:appcompat:1.6.1")
|
||||||
|
implementation("androidx.navigation:navigation-ui-ktx:2.7.7")
|
||||||
implementation("androidx.preference:preference-ktx:1.2.0")
|
implementation("androidx.lifecycle:lifecycle-livedata-ktx:2.7.0")
|
||||||
|
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.7.0")
|
||||||
implementation("com.github.bumptech.glide:glide:4.13.1")
|
implementation("androidx.navigation:navigation-fragment-ktx:2.7.7")
|
||||||
kapt("com.github.bumptech.glide:compiler:4.13.1")
|
|
||||||
implementation("com.github.bumptech.glide:okhttp3-integration:4.13.0")
|
|
||||||
|
|
||||||
|
// Design & UI
|
||||||
implementation("jp.wasabeef:glide-transformations:4.3.0")
|
implementation("jp.wasabeef:glide-transformations:4.3.0")
|
||||||
|
implementation("androidx.preference:preference-ktx:1.2.1")
|
||||||
|
implementation("com.google.android.material:material:1.12.0")
|
||||||
|
implementation("androidx.constraintlayout:constraintlayout:2.1.4")
|
||||||
implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0")
|
implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0")
|
||||||
|
|
||||||
// implementation("androidx.leanback:leanback-paging:1.1.0-alpha09")
|
// Glide Module
|
||||||
|
ksp("com.github.bumptech.glide:ksp:4.16.0")
|
||||||
|
implementation("com.github.bumptech.glide:glide:4.16.0")
|
||||||
|
implementation("com.github.bumptech.glide:okhttp3-integration:4.16.0")
|
||||||
|
|
||||||
// Exoplayer
|
// For KSP -> Official Annotation Processors are Not Yet Supported for KSP
|
||||||
implementation("com.google.android.exoplayer:exoplayer:2.18.2")
|
ksp("dev.zacsweers.autoservice:auto-service-ksp:1.1.0")
|
||||||
implementation("com.google.android.exoplayer:extension-cast:2.18.2")
|
implementation("com.google.guava:guava:33.2.0-android")
|
||||||
implementation("com.google.android.exoplayer:extension-mediasession:2.18.2")
|
implementation("dev.zacsweers.autoservice:auto-service-ksp:1.1.0")
|
||||||
implementation("com.google.android.exoplayer:extension-okhttp:2.18.2")
|
|
||||||
|
|
||||||
//implementation("com.google.android.exoplayer:extension-leanback:2.14.0")
|
// Media 3 (ExoPlayer)
|
||||||
|
implementation("androidx.media3:media3-ui:1.1.1")
|
||||||
|
implementation("androidx.media3:media3-cast:1.1.1")
|
||||||
|
implementation("androidx.media3:media3-common:1.1.1")
|
||||||
|
implementation("androidx.media3:media3-session:1.1.1")
|
||||||
|
implementation("androidx.media3:media3-exoplayer:1.1.1")
|
||||||
|
implementation("com.google.android.mediahome:video:1.0.0")
|
||||||
|
implementation("androidx.media3:media3-exoplayer-hls:1.1.1")
|
||||||
|
implementation("androidx.media3:media3-exoplayer-dash:1.1.1")
|
||||||
|
implementation("androidx.media3:media3-datasource-okhttp:1.1.1")
|
||||||
|
|
||||||
// Bug reports
|
// PlayBack
|
||||||
implementation("ch.acra:acra-core:5.8.4")
|
implementation("com.jaredrummler:colorpicker:1.1.0") // Subtitle Color Picker
|
||||||
implementation("ch.acra:acra-toast:5.8.4")
|
implementation("com.github.recloudstream:media-ffmpeg:1.1.0") // Custom FF-MPEG Lib for Audio Codecs
|
||||||
|
implementation("com.github.teamnewpipe:NewPipeExtractor:fafd471") /* For Trailers
|
||||||
|
^ Update to Latest Commits if Trailers Misbehave, github.com/TeamNewPipe/NewPipeExtractor/commits/dev */
|
||||||
|
implementation("com.github.albfernandez:juniversalchardet:2.4.0") // Subtitle Decoding
|
||||||
|
|
||||||
compileOnly("com.google.auto.service:auto-service-annotations:1.0")
|
// Crash Reports (AcraApplication.kt)
|
||||||
//either for java sources:
|
implementation("ch.acra:acra-core:5.11.3")
|
||||||
annotationProcessor("com.google.auto.service:auto-service:1.0")
|
implementation("ch.acra:acra-toast:5.11.3")
|
||||||
//or for kotlin sources (requires kapt gradle plugin):
|
|
||||||
kapt("com.google.auto.service:auto-service:1.0")
|
|
||||||
|
|
||||||
// subtitle color picker
|
|
||||||
implementation("com.jaredrummler:colorpicker:1.1.0")
|
|
||||||
|
|
||||||
//run JS
|
|
||||||
// do not upgrade to 1.7.14, since in 1.7.14 Rhino uses the `SourceVersion` class, which is not
|
|
||||||
// available on Android (even when using desugaring), and `NoClassDefFoundError` is thrown
|
|
||||||
implementation("org.mozilla:rhino:1.7.13")
|
|
||||||
|
|
||||||
// TorrentStream
|
|
||||||
//implementation("com.github.TorrentStream:TorrentStream-Android:2.7.0")
|
|
||||||
|
|
||||||
// Downloading
|
|
||||||
implementation("androidx.work:work-runtime:2.7.1")
|
|
||||||
implementation("androidx.work:work-runtime-ktx:2.7.1")
|
|
||||||
|
|
||||||
// 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.1")
|
|
||||||
// To fix SSL fuckery on android 9
|
|
||||||
implementation("org.conscrypt:conscrypt-android:2.2.1")
|
|
||||||
// Util to skip the URI file fuckery 🙏
|
|
||||||
implementation("com.github.tachiyomiorg:unifile:17bec43")
|
|
||||||
|
|
||||||
// API because cba maintaining it myself
|
|
||||||
implementation("com.uwetrottmann.tmdb2:tmdb-java:2.6.0")
|
|
||||||
|
|
||||||
implementation("com.github.discord:OverlappingPanels:0.1.3")
|
|
||||||
// debugImplementation because LeakCanary should only run in debug builds.
|
|
||||||
// debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.7'
|
|
||||||
|
|
||||||
// for shimmer when loading
|
|
||||||
implementation("com.facebook.shimmer:shimmer:0.5.0")
|
|
||||||
|
|
||||||
|
// UI Stuff
|
||||||
|
implementation("com.facebook.shimmer:shimmer:0.5.0") // Shimmering Effect (Loading Skeleton)
|
||||||
|
implementation("androidx.palette:palette-ktx:1.0.0") // Palette For Images -> Colors
|
||||||
implementation("androidx.tvprovider:tvprovider:1.0.0")
|
implementation("androidx.tvprovider:tvprovider:1.0.0")
|
||||||
|
implementation("com.github.discord:OverlappingPanels:0.1.5") // Gestures
|
||||||
|
implementation ("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
|
// Extensions & Other Libs
|
||||||
implementation("com.github.albfernandez:juniversalchardet:2.4.0")
|
implementation("org.mozilla:rhino:1.7.15") // run JavaScript
|
||||||
|
implementation("me.xdrop:fuzzywuzzy:1.4.0") // Library/Ext Searching with Levenshtein Distance
|
||||||
|
implementation("com.github.LagradOst:SafeFile:0.0.6") // To Prevent the URI File Fu*kery
|
||||||
|
implementation("org.conscrypt:conscrypt-android:2.5.2") // To Fix SSL Fu*kery on Android 9
|
||||||
|
implementation("com.uwetrottmann.tmdb2:tmdb-java:2.10.0") // TMDB API v3 Wrapper Made with RetroFit
|
||||||
|
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.0.4")
|
||||||
|
implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.13.1") /* JSON Parser
|
||||||
|
^ Don't Bump Jackson above 2.13.1 , Crashes on Android TV's and FireSticks that have Min API
|
||||||
|
Level 25 or Less. */
|
||||||
|
|
||||||
// slow af yt
|
// Downloading & Networking
|
||||||
//implementation("com.github.HaarigerHarald:android-youtubeExtractor:master-SNAPSHOT")
|
implementation("androidx.work:work-runtime:2.9.0")
|
||||||
|
implementation("androidx.work:work-runtime-ktx:2.9.0")
|
||||||
|
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(project(":library") {
|
||||||
implementation("com.github.TeamNewPipe:NewPipeExtractor:9ffdd0948b2ecd82655f5ff2a3e127b2b7695d5b")
|
this.extra.set("isDebug", isLibraryDebug)
|
||||||
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:1.1.6")
|
})
|
||||||
|
|
||||||
// Library/extensions searching with Levenshtein distance
|
|
||||||
implementation("me.xdrop:fuzzywuzzy:1.4.0")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks.register("androidSourcesJar", Jar::class) {
|
tasks.register<Jar>("androidSourcesJar") {
|
||||||
archiveClassifier.set("sources")
|
archiveClassifier.set("sources")
|
||||||
from(android.sourceSets.getByName("main").java.srcDirs) //full sources
|
from(android.sourceSets.getByName("main").java.srcDirs) // Full Sources
|
||||||
}
|
}
|
||||||
|
|
||||||
// this is used by the gradlew plugin
|
tasks.register<Copy>("copyJar") {
|
||||||
tasks.register("makeJar", Copy::class) {
|
from(
|
||||||
from("build/intermediates/compile_app_classes_jar/prereleaseDebug")
|
"build/intermediates/compile_app_classes_jar/prereleaseDebug",
|
||||||
into("build")
|
"../library/build/libs"
|
||||||
include("classes.jar")
|
)
|
||||||
dependsOn("build")
|
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 {
|
tasks.withType<DokkaTask>().configureEach {
|
||||||
|
@ -245,9 +283,10 @@ tasks.withType<DokkaTask>().configureEach {
|
||||||
|
|
||||||
// URL showing where the source code can be accessed through the web browser
|
// URL showing where the source code can be accessed through the web browser
|
||||||
remoteUrl.set(URL("https://github.com/recloudstream/cloudstream/tree/master/app/src/main/java"))
|
remoteUrl.set(URL("https://github.com/recloudstream/cloudstream/tree/master/app/src/main/java"))
|
||||||
|
|
||||||
// Suffix which is used to append the line number to the URL. Use #L for GitHub
|
// Suffix which is used to append the line number to the URL. Use #L for GitHub
|
||||||
remoteLineSuffix.set("#L")
|
remoteLineSuffix.set("#L")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,155 +1,57 @@
|
||||||
package com.lagradost.cloudstream3
|
package com.lagradost.cloudstream3
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.os.PersistableBundle
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import androidx.test.core.app.ActivityScenario
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
import com.lagradost.cloudstream3.mvvm.logError
|
import androidx.viewbinding.ViewBinding
|
||||||
import com.lagradost.cloudstream3.utils.Qualities
|
import com.lagradost.cloudstream3.databinding.FragmentHomeBinding
|
||||||
|
import com.lagradost.cloudstream3.databinding.FragmentHomeTvBinding
|
||||||
|
import com.lagradost.cloudstream3.databinding.FragmentLibraryBinding
|
||||||
|
import com.lagradost.cloudstream3.databinding.FragmentLibraryTvBinding
|
||||||
|
import com.lagradost.cloudstream3.databinding.FragmentPlayerBinding
|
||||||
|
import com.lagradost.cloudstream3.databinding.FragmentPlayerTvBinding
|
||||||
|
import com.lagradost.cloudstream3.databinding.FragmentResultBinding
|
||||||
|
import com.lagradost.cloudstream3.databinding.FragmentResultTvBinding
|
||||||
|
import com.lagradost.cloudstream3.databinding.FragmentSearchBinding
|
||||||
|
import com.lagradost.cloudstream3.databinding.FragmentSearchTvBinding
|
||||||
|
import com.lagradost.cloudstream3.databinding.HomeResultGridBinding
|
||||||
|
import com.lagradost.cloudstream3.databinding.HomepageParentBinding
|
||||||
|
import com.lagradost.cloudstream3.databinding.HomepageParentEmulatorBinding
|
||||||
|
import com.lagradost.cloudstream3.databinding.HomepageParentTvBinding
|
||||||
|
import com.lagradost.cloudstream3.databinding.PlayerCustomLayoutBinding
|
||||||
|
import com.lagradost.cloudstream3.databinding.PlayerCustomLayoutTvBinding
|
||||||
|
import com.lagradost.cloudstream3.databinding.RepositoryItemBinding
|
||||||
|
import com.lagradost.cloudstream3.databinding.RepositoryItemTvBinding
|
||||||
|
import com.lagradost.cloudstream3.databinding.SearchResultGridBinding
|
||||||
|
import com.lagradost.cloudstream3.databinding.SearchResultGridExpandedBinding
|
||||||
|
import com.lagradost.cloudstream3.databinding.TrailerCustomLayoutBinding
|
||||||
import com.lagradost.cloudstream3.utils.SubtitleHelper
|
import com.lagradost.cloudstream3.utils.SubtitleHelper
|
||||||
|
import com.lagradost.cloudstream3.utils.TestingUtils
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import org.junit.Assert
|
import org.junit.Assert
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import org.junit.runner.RunWith
|
import org.junit.runner.RunWith
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Instrumented test, which will execute on an Android device.
|
* Instrumented test, which will execute on an Android device.
|
||||||
*
|
*
|
||||||
* See [testing documentation](http://d.android.com/tools/testing).
|
* See [testing documentation](http://d.android.com/tools/testing).
|
||||||
*/
|
*/
|
||||||
|
class TestApplication : Activity() {
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?, persistentState: PersistableBundle?) {
|
||||||
|
super.onCreate(savedInstanceState, persistentState)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@RunWith(AndroidJUnit4::class)
|
@RunWith(AndroidJUnit4::class)
|
||||||
class ExampleInstrumentedTest {
|
class ExampleInstrumentedTest {
|
||||||
//@Test
|
private fun getAllProviders(): Array<MainAPI> {
|
||||||
//fun useAppContext() {
|
println("Providers: ${APIHolder.allProviders.size}")
|
||||||
// // Context of the app under test.
|
return APIHolder.allProviders.toTypedArray() //.filter { !it.usesWebView }
|
||||||
// val appContext = InstrumentationRegistry.getInstrumentation().targetContext
|
|
||||||
// assertEquals("com.lagradost.cloudstream3", appContext.packageName)
|
|
||||||
//}
|
|
||||||
|
|
||||||
private fun getAllProviders(): List<MainAPI> {
|
|
||||||
return APIHolder.allProviders //.filter { !it.usesWebView }
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun loadLinks(api: MainAPI, url: String?): Boolean {
|
|
||||||
Assert.assertNotNull("Api ${api.name} has invalid url on episode", url)
|
|
||||||
if (url == null) return true
|
|
||||||
var linksLoaded = 0
|
|
||||||
try {
|
|
||||||
val success = api.loadLinks(url, false, {}) { link ->
|
|
||||||
Assert.assertTrue(
|
|
||||||
"Api ${api.name} returns link with invalid Quality",
|
|
||||||
Qualities.values().map { it.value }.contains(link.quality)
|
|
||||||
)
|
|
||||||
Assert.assertTrue(
|
|
||||||
"Api ${api.name} returns link with invalid url ${link.url}",
|
|
||||||
link.url.length > 4
|
|
||||||
)
|
|
||||||
linksLoaded++
|
|
||||||
}
|
|
||||||
if (success) {
|
|
||||||
return linksLoaded > 0
|
|
||||||
}
|
|
||||||
Assert.assertTrue("Api ${api.name} has returns false on .loadLinks", success)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
if (e.cause is NotImplementedError) {
|
|
||||||
Assert.fail("Provider has not implemented .loadLinks")
|
|
||||||
}
|
|
||||||
logError(e)
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun testSingleProviderApi(api: MainAPI): Boolean {
|
|
||||||
val searchQueries = listOf("over", "iron", "guy")
|
|
||||||
var correctResponses = 0
|
|
||||||
var searchResult: List<SearchResponse>? = null
|
|
||||||
for (query in searchQueries) {
|
|
||||||
val response = try {
|
|
||||||
api.search(query)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
if (e.cause is NotImplementedError) {
|
|
||||||
Assert.fail("Provider has not implemented .search")
|
|
||||||
}
|
|
||||||
logError(e)
|
|
||||||
null
|
|
||||||
}
|
|
||||||
if (!response.isNullOrEmpty()) {
|
|
||||||
correctResponses++
|
|
||||||
if (searchResult == null) {
|
|
||||||
searchResult = response
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (correctResponses == 0 || searchResult == null) {
|
|
||||||
System.err.println("Api ${api.name} did not return any valid search responses")
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
var validResults = false
|
|
||||||
for (result in searchResult) {
|
|
||||||
Assert.assertEquals(
|
|
||||||
"Invalid apiName on response on ${api.name}",
|
|
||||||
result.apiName,
|
|
||||||
api.name
|
|
||||||
)
|
|
||||||
val load = api.load(result.url) ?: continue
|
|
||||||
Assert.assertEquals(
|
|
||||||
"Invalid apiName on load on ${api.name}",
|
|
||||||
load.apiName,
|
|
||||||
result.apiName
|
|
||||||
)
|
|
||||||
Assert.assertTrue(
|
|
||||||
"Api ${api.name} on load does not contain any of the supportedTypes",
|
|
||||||
api.supportedTypes.contains(load.type)
|
|
||||||
)
|
|
||||||
when (load) {
|
|
||||||
is AnimeLoadResponse -> {
|
|
||||||
val gotNoEpisodes =
|
|
||||||
load.episodes.keys.isEmpty() || load.episodes.keys.any { load.episodes[it].isNullOrEmpty() }
|
|
||||||
|
|
||||||
if (gotNoEpisodes) {
|
|
||||||
println("Api ${api.name} got no episodes on ${load.url}")
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
val url = (load.episodes[load.episodes.keys.first()])?.first()?.data
|
|
||||||
validResults = loadLinks(api, url)
|
|
||||||
if (!validResults) continue
|
|
||||||
}
|
|
||||||
is MovieLoadResponse -> {
|
|
||||||
val gotNoEpisodes = load.dataUrl.isBlank()
|
|
||||||
if (gotNoEpisodes) {
|
|
||||||
println("Api ${api.name} got no movie on ${load.url}")
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
validResults = loadLinks(api, load.dataUrl)
|
|
||||||
if (!validResults) continue
|
|
||||||
}
|
|
||||||
is TvSeriesLoadResponse -> {
|
|
||||||
val gotNoEpisodes = load.episodes.isEmpty()
|
|
||||||
if (gotNoEpisodes) {
|
|
||||||
println("Api ${api.name} got no episodes on ${load.url}")
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
validResults = loadLinks(api, load.episodes.first().data)
|
|
||||||
if (!validResults) continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
if (!validResults) {
|
|
||||||
System.err.println("Api ${api.name} did not load on any")
|
|
||||||
}
|
|
||||||
|
|
||||||
return validResults
|
|
||||||
} catch (e: Exception) {
|
|
||||||
if (e.cause is NotImplementedError) {
|
|
||||||
Assert.fail("Provider has not implemented .load")
|
|
||||||
}
|
|
||||||
logError(e)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -158,7 +60,78 @@ class ExampleInstrumentedTest {
|
||||||
println("Done providersExist")
|
println("Done providersExist")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Throws
|
||||||
|
private inline fun <reified T : ViewBinding> testAllLayouts(
|
||||||
|
activity: Activity,
|
||||||
|
vararg layouts: Int
|
||||||
|
) {
|
||||||
|
|
||||||
|
val bind = T::class.java.methods.first { it.name == "bind" }
|
||||||
|
val inflater = LayoutInflater.from(activity)
|
||||||
|
for (layout in layouts) {
|
||||||
|
val root = inflater.inflate(layout, null, false)
|
||||||
|
bind.invoke(null, root)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@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() {
|
fun providerCorrectData() {
|
||||||
val isoNames = SubtitleHelper.languages.map { it.ISO_639_1 }
|
val isoNames = SubtitleHelper.languages.map { it.ISO_639_1 }
|
||||||
Assert.assertFalse("ISO does not contain any languages", isoNames.isNullOrEmpty())
|
Assert.assertFalse("ISO does not contain any languages", isoNames.isNullOrEmpty())
|
||||||
|
@ -180,68 +153,21 @@ class ExampleInstrumentedTest {
|
||||||
@Test
|
@Test
|
||||||
fun providerCorrectHomepage() {
|
fun providerCorrectHomepage() {
|
||||||
runBlocking {
|
runBlocking {
|
||||||
getAllProviders().amap { api ->
|
getAllProviders().toList().amap { api ->
|
||||||
if (api.hasMainPage) {
|
TestingUtils.testHomepage(api, ::println)
|
||||||
try {
|
|
||||||
val f = api.mainPage.first()
|
|
||||||
val homepage =
|
|
||||||
api.getMainPage(1, MainPageRequest(f.name, f.data, f.horizontalImages))
|
|
||||||
when {
|
|
||||||
homepage == null -> {
|
|
||||||
System.err.println("Homepage provider ${api.name} did not correctly load homepage!")
|
|
||||||
}
|
|
||||||
homepage.items.isEmpty() -> {
|
|
||||||
System.err.println("Homepage provider ${api.name} does not contain any items!")
|
|
||||||
}
|
|
||||||
homepage.items.any { it.list.isEmpty() } -> {
|
|
||||||
System.err.println("Homepage provider ${api.name} does not have any items on result!")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
if (e.cause is NotImplementedError) {
|
|
||||||
Assert.fail("Provider marked as hasMainPage, while in reality is has not been implemented")
|
|
||||||
}
|
|
||||||
logError(e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
println("Done providerCorrectHomepage")
|
println("Done providerCorrectHomepage")
|
||||||
}
|
}
|
||||||
|
|
||||||
// @Test
|
|
||||||
// fun testSingleProvider() {
|
|
||||||
// testSingleProviderApi(ThenosProvider())
|
|
||||||
// }
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun providerCorrect() {
|
fun testAllProvidersCorrect() {
|
||||||
runBlocking {
|
runBlocking {
|
||||||
val invalidProvider = ArrayList<Pair<MainAPI, Exception?>>()
|
TestingUtils.getDeferredProviderTests(
|
||||||
val providers = getAllProviders()
|
this,
|
||||||
providers.amap { api ->
|
getAllProviders(),
|
||||||
try {
|
::println
|
||||||
println("Trying $api")
|
) { _, _ -> }
|
||||||
if (testSingleProviderApi(api)) {
|
|
||||||
println("Success $api")
|
|
||||||
} else {
|
|
||||||
System.err.println("Error $api")
|
|
||||||
invalidProvider.add(Pair(api, null))
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
logError(e)
|
|
||||||
invalidProvider.add(Pair(api, e))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (invalidProvider.isEmpty()) {
|
|
||||||
println("No Invalid providers! :D")
|
|
||||||
} else {
|
|
||||||
println("Invalid providers are: ")
|
|
||||||
for (provider in invalidProvider) {
|
|
||||||
println("${provider.first}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
println("Done providerCorrect")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
<uses-permission android:name="android.permission.INTERNET" /> <!-- unless you only use cs3 as a player for downloaded stuff, you need this -->
|
<uses-permission android:name="android.permission.INTERNET" /> <!-- unless you only use cs3 as a player for downloaded stuff, you need this -->
|
||||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> <!-- Downloads -->
|
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> <!-- Downloads -->
|
||||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <!-- Downloads on low api devices -->
|
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <!-- Downloads on low api devices -->
|
||||||
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" /> <!-- Plugin API -->
|
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" tools:ignore="ScopedStorage" /> <!-- Plugin API -->
|
||||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" /> <!-- some dependency needs this -->
|
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" /> <!-- some dependency needs this -->
|
||||||
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" /> <!-- Used for player vertical slide -->
|
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" /> <!-- Used for player vertical slide -->
|
||||||
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" /> <!-- Used for app update -->
|
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" /> <!-- Used for app update -->
|
||||||
|
@ -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="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.UPDATE_PACKAGES_WITHOUT_USER_ACTION" /> <!-- Used for updates without prompt -->
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> <!-- Used for update service -->
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> <!-- Used for update service -->
|
||||||
|
<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 -->
|
<!-- Fixes android tv fuckery -->
|
||||||
<uses-feature
|
<uses-feature
|
||||||
android:name="android.hardware.touchscreen"
|
android:name="android.hardware.touchscreen"
|
||||||
|
@ -35,9 +41,11 @@
|
||||||
<application
|
<application
|
||||||
android:name=".AcraApplication"
|
android:name=".AcraApplication"
|
||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
|
android:enableOnBackInvokedCallback="true"
|
||||||
android:appCategory="video"
|
android:appCategory="video"
|
||||||
android:banner="@mipmap/ic_banner"
|
android:banner="@mipmap/ic_banner"
|
||||||
android:fullBackupContent="@xml/backup_descriptor"
|
android:fullBackupContent="@xml/backup_descriptor"
|
||||||
|
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||||
android:icon="@mipmap/ic_launcher"
|
android:icon="@mipmap/ic_launcher"
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
android:largeHeap="true"
|
android:largeHeap="true"
|
||||||
|
@ -45,7 +53,7 @@
|
||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
android:theme="@style/AppTheme"
|
android:theme="@style/AppTheme"
|
||||||
android:usesCleartextTraffic="true"
|
android:usesCleartextTraffic="true"
|
||||||
tools:targetApi="o">
|
tools:targetApi="tiramisu">
|
||||||
|
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="com.google.android.gms.cast.framework.OPTIONS_PROVIDER_CLASS_NAME"
|
android:name="com.google.android.gms.cast.framework.OPTIONS_PROVIDER_CLASS_NAME"
|
||||||
|
@ -61,7 +69,9 @@
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
android:resizeableActivity="true"
|
android:resizeableActivity="true"
|
||||||
android:screenOrientation="userLandscape"
|
android:screenOrientation="userLandscape"
|
||||||
android:supportsPictureInPicture="true">
|
android:supportsPictureInPicture="true"
|
||||||
|
android:taskAffinity="com.lagradost.cloudstream3.downloadedplayer"
|
||||||
|
android:launchMode="singleTask">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.VIEW" />
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
|
||||||
|
@ -92,11 +102,15 @@
|
||||||
android:launchMode="singleTask"
|
android:launchMode="singleTask"
|
||||||
android:resizeableActivity="true"
|
android:resizeableActivity="true"
|
||||||
android:supportsPictureInPicture="true">
|
android:supportsPictureInPicture="true">
|
||||||
<intent-filter android:exported="true">
|
|
||||||
<action android:name="android.intent.action.MAIN" />
|
|
||||||
|
|
||||||
<category android:name="android.intent.category.LAUNCHER" />
|
<!-- cloudstreamplayer://encodedUrl?name=Dune -->
|
||||||
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
|
<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>
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.VIEW" />
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
@ -151,6 +165,21 @@
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
|
<activity
|
||||||
|
android:name=".ui.account.AccountSelectActivity"
|
||||||
|
android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout|keyboard|keyboardHidden"
|
||||||
|
android:exported="true">
|
||||||
|
<intent-filter android:exported="true">
|
||||||
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
|
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
|
||||||
|
</intent-filter>
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
</intent-filter>
|
||||||
|
</activity>
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.EasterEggMonke"
|
android:name=".ui.EasterEggMonke"
|
||||||
android:exported="true" />
|
android:exported="true" />
|
||||||
|
@ -158,13 +187,14 @@
|
||||||
<receiver
|
<receiver
|
||||||
android:name=".receivers.VideoDownloadRestartReceiver"
|
android:name=".receivers.VideoDownloadRestartReceiver"
|
||||||
android:enabled="false"
|
android:enabled="false"
|
||||||
android:exported="true">
|
android:exported="false">
|
||||||
<intent-filter android:exported="true">
|
<intent-filter android:exported="false">
|
||||||
<action android:name="restart_service" />
|
<action android:name="restart_service" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</receiver>
|
</receiver>
|
||||||
|
|
||||||
<service
|
<service
|
||||||
|
android:foregroundServiceType="dataSync"
|
||||||
android:name=".services.VideoDownloadService"
|
android:name=".services.VideoDownloadService"
|
||||||
android:enabled="true"
|
android:enabled="true"
|
||||||
android:exported="false" />
|
android:exported="false" />
|
||||||
|
@ -174,6 +204,7 @@
|
||||||
android:exported="false" />
|
android:exported="false" />
|
||||||
|
|
||||||
<service
|
<service
|
||||||
|
android:foregroundServiceType="dataSync"
|
||||||
android:name=".utils.PackageInstallerService"
|
android:name=".utils.PackageInstallerService"
|
||||||
android:exported="false" />
|
android:exported="false" />
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,28 @@
|
||||||
|
#include <jni.h>
|
||||||
|
#include <csignal>
|
||||||
|
#include <android/log.h>
|
||||||
|
|
||||||
|
#define TAG "CloudStream Crash Handler"
|
||||||
|
volatile sig_atomic_t gSignalStatus = 0;
|
||||||
|
void handleNativeCrash(int signal) {
|
||||||
|
gSignalStatus = signal;
|
||||||
|
}
|
||||||
|
|
||||||
|
extern "C" JNIEXPORT void JNICALL
|
||||||
|
Java_com_lagradost_cloudstream3_NativeCrashHandler_initNativeCrashHandler(JNIEnv *env, jobject) {
|
||||||
|
#define REGISTER_SIGNAL(X) signal(X, handleNativeCrash);
|
||||||
|
REGISTER_SIGNAL(SIGSEGV)
|
||||||
|
#undef REGISTER_SIGNAL
|
||||||
|
}
|
||||||
|
|
||||||
|
//extern "C" JNIEXPORT void JNICALL
|
||||||
|
//Java_com_lagradost_cloudstream3_NativeCrashHandler_triggerNativeCrash(JNIEnv *env, jobject thiz) {
|
||||||
|
// int *p = nullptr;
|
||||||
|
// *p = 0;
|
||||||
|
//}
|
||||||
|
|
||||||
|
extern "C" JNIEXPORT int JNICALL
|
||||||
|
Java_com_lagradost_cloudstream3_NativeCrashHandler_getSignalStatus(JNIEnv *env, jobject) {
|
||||||
|
//__android_log_print(ANDROID_LOG_INFO, TAG, "Got signal status %d", gSignalStatus);
|
||||||
|
return gSignalStatus;
|
||||||
|
}
|
|
@ -8,11 +8,12 @@ import android.content.Intent
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.fragment.app.FragmentActivity
|
import androidx.fragment.app.FragmentActivity
|
||||||
import com.google.auto.service.AutoService
|
|
||||||
import com.lagradost.cloudstream3.mvvm.normalSafeApiCall
|
import com.lagradost.cloudstream3.mvvm.normalSafeApiCall
|
||||||
import com.lagradost.cloudstream3.mvvm.suspendSafeApiCall
|
import com.lagradost.cloudstream3.mvvm.suspendSafeApiCall
|
||||||
import com.lagradost.cloudstream3.plugins.PluginManager
|
import com.lagradost.cloudstream3.plugins.PluginManager
|
||||||
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.AppUtils.openBrowser
|
||||||
import com.lagradost.cloudstream3.utils.Coroutines.runOnMainThread
|
import com.lagradost.cloudstream3.utils.Coroutines.runOnMainThread
|
||||||
import com.lagradost.cloudstream3.utils.DataStore.getKey
|
import com.lagradost.cloudstream3.utils.DataStore.getKey
|
||||||
|
@ -32,27 +33,25 @@ import org.acra.sender.ReportSenderFactory
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.FileNotFoundException
|
import java.io.FileNotFoundException
|
||||||
import java.io.PrintStream
|
import java.io.PrintStream
|
||||||
import java.lang.Exception
|
|
||||||
import java.lang.ref.WeakReference
|
import java.lang.ref.WeakReference
|
||||||
import kotlin.concurrent.thread
|
import kotlin.concurrent.thread
|
||||||
import kotlin.system.exitProcess
|
import kotlin.system.exitProcess
|
||||||
|
|
||||||
|
|
||||||
class CustomReportSender : ReportSender {
|
class CustomReportSender : ReportSender {
|
||||||
// Sends all your crashes to google forms
|
// Sends all your crashes to google forms
|
||||||
override fun send(context: Context, errorContent: CrashReportData) {
|
override fun send(context: Context, errorContent: CrashReportData) {
|
||||||
println("Sending report")
|
println("Sending report")
|
||||||
val url =
|
val url =
|
||||||
"https://docs.google.com/forms/u/0/d/e/1FAIpQLSe9Vff8oHGMRXcjgCXZwkjvx3eBdNpn4DzjO0FkcWEU1gEQpA/formResponse"
|
"https://docs.google.com/forms/d/e/1FAIpQLSfO4r353BJ79TTY_-t5KWSIJT2xfqcQWY81xjAA1-1N0U2eSg/formResponse"
|
||||||
val data = mapOf(
|
val data = mapOf(
|
||||||
"entry.1586460852" to errorContent.toJSON()
|
"entry.1993829403" to errorContent.toJSON()
|
||||||
)
|
)
|
||||||
|
|
||||||
thread { // to not run it on main thread
|
thread { // to not run it on main thread
|
||||||
runBlocking {
|
runBlocking {
|
||||||
suspendSafeApiCall {
|
suspendSafeApiCall {
|
||||||
val post = app.post(url, data = data)
|
app.post(url, data = data)
|
||||||
println("Report response: $post")
|
//println("Report response: $post")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -65,7 +64,6 @@ class CustomReportSender : ReportSender {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@AutoService(ReportSenderFactory::class)
|
|
||||||
class CustomSenderFactory : ReportSenderFactory {
|
class CustomSenderFactory : ReportSenderFactory {
|
||||||
override fun create(context: Context, config: CoreConfiguration): ReportSender {
|
override fun create(context: Context, config: CoreConfiguration): ReportSender {
|
||||||
return CustomReportSender()
|
return CustomReportSender()
|
||||||
|
@ -104,12 +102,17 @@ class ExceptionHandler(val errorFile: File, val onError: (() -> Unit)) :
|
||||||
}
|
}
|
||||||
|
|
||||||
class AcraApplication : Application() {
|
class AcraApplication : Application() {
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
Thread.setDefaultUncaughtExceptionHandler(ExceptionHandler(filesDir.resolve("last_error")) {
|
//NativeCrashHandler.initCrashHandler()
|
||||||
|
ExceptionHandler(filesDir.resolve("last_error")) {
|
||||||
val intent = context!!.packageManager.getLaunchIntentForPackage(context!!.packageName)
|
val intent = context!!.packageManager.getLaunchIntentForPackage(context!!.packageName)
|
||||||
startActivity(Intent.makeRestartActivityTask(intent!!.component))
|
startActivity(Intent.makeRestartActivityTask(intent!!.component))
|
||||||
})
|
}.also {
|
||||||
|
exceptionHandler = it
|
||||||
|
Thread.setDefaultUncaughtExceptionHandler(it)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun attachBaseContext(base: Context?) {
|
override fun attachBaseContext(base: Context?) {
|
||||||
|
@ -121,10 +124,10 @@ class AcraApplication : Application() {
|
||||||
buildConfigClass = BuildConfig::class.java
|
buildConfigClass = BuildConfig::class.java
|
||||||
reportFormat = StringFormat.JSON
|
reportFormat = StringFormat.JSON
|
||||||
|
|
||||||
reportContent = arrayOf(
|
reportContent = listOf(
|
||||||
ReportField.BUILD_CONFIG, ReportField.USER_CRASH_DATE,
|
ReportField.BUILD_CONFIG, ReportField.USER_CRASH_DATE,
|
||||||
ReportField.ANDROID_VERSION, ReportField.PHONE_MODEL,
|
ReportField.ANDROID_VERSION, ReportField.PHONE_MODEL,
|
||||||
ReportField.STACK_TRACE
|
ReportField.STACK_TRACE,
|
||||||
)
|
)
|
||||||
|
|
||||||
// removed this due to bug when starting the app, moved it to when it actually crashes
|
// removed this due to bug when starting the app, moved it to when it actually crashes
|
||||||
|
@ -137,6 +140,8 @@ class AcraApplication : Application() {
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
var exceptionHandler: ExceptionHandler? = null
|
||||||
|
|
||||||
/** Use to get activity from Context */
|
/** Use to get activity from Context */
|
||||||
tailrec fun Context.getActivity(): Activity? = this as? Activity
|
tailrec fun Context.getActivity(): Activity? = this as? Activity
|
||||||
?: (this as? ContextWrapper)?.baseContext?.getActivity()
|
?: (this as? ContextWrapper)?.baseContext?.getActivity()
|
||||||
|
@ -148,6 +153,14 @@ class AcraApplication : Application() {
|
||||||
_context = WeakReference(value)
|
_context = WeakReference(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun <T : Any> getKeyClass(path: String, valueType: Class<T>): T? {
|
||||||
|
return context?.getKey(path, valueType)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun <T : Any> setKeyClass(path: String, value: T) {
|
||||||
|
context?.setKey(path, value)
|
||||||
|
}
|
||||||
|
|
||||||
fun removeKeys(folder: String): Int? {
|
fun removeKeys(folder: String): Int? {
|
||||||
return context?.removeKeys(folder)
|
return context?.removeKeys(folder)
|
||||||
}
|
}
|
||||||
|
@ -199,10 +212,9 @@ class AcraApplication : Application() {
|
||||||
fun openBrowser(url: String, activity: FragmentActivity?) {
|
fun openBrowser(url: String, activity: FragmentActivity?) {
|
||||||
openBrowser(
|
openBrowser(
|
||||||
url,
|
url,
|
||||||
isTvSettings(),
|
isLayout(TV or EMULATOR),
|
||||||
activity?.supportFragmentManager?.fragments?.lastOrNull()
|
activity?.supportFragmentManager?.fragments?.lastOrNull()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,9 +7,13 @@ import android.content.Context
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
import android.content.res.Resources
|
import android.content.res.Resources
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
|
import android.util.DisplayMetrics
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.view.*
|
import android.view.Gravity
|
||||||
import android.widget.TextView
|
import android.view.KeyEvent
|
||||||
|
import android.view.View
|
||||||
|
import android.view.View.NO_ID
|
||||||
|
import android.view.ViewGroup
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.activity.ComponentActivity
|
import androidx.activity.ComponentActivity
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
|
@ -18,15 +22,21 @@ import androidx.annotation.StringRes
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.appcompat.widget.SearchView
|
import androidx.appcompat.widget.SearchView
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.core.view.children
|
||||||
import androidx.preference.PreferenceManager
|
import androidx.preference.PreferenceManager
|
||||||
import com.google.android.gms.cast.framework.CastSession
|
import com.google.android.gms.cast.framework.CastSession
|
||||||
|
import com.google.android.material.chip.ChipGroup
|
||||||
|
import com.google.android.material.navigationrail.NavigationRailView
|
||||||
import com.lagradost.cloudstream3.AcraApplication.Companion.getKey
|
import com.lagradost.cloudstream3.AcraApplication.Companion.getKey
|
||||||
import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey
|
import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey
|
||||||
|
import com.lagradost.cloudstream3.MainActivity.Companion.resumeApps
|
||||||
|
import com.lagradost.cloudstream3.databinding.ToastBinding
|
||||||
import com.lagradost.cloudstream3.mvvm.logError
|
import com.lagradost.cloudstream3.mvvm.logError
|
||||||
import com.lagradost.cloudstream3.ui.player.PlayerEventType
|
import com.lagradost.cloudstream3.ui.player.PlayerEventType
|
||||||
import com.lagradost.cloudstream3.ui.result.ResultFragment
|
import com.lagradost.cloudstream3.ui.result.ResultFragment
|
||||||
import com.lagradost.cloudstream3.ui.result.UiText
|
import com.lagradost.cloudstream3.ui.result.UiText
|
||||||
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.updateTv
|
import com.lagradost.cloudstream3.ui.settings.Globals.updateTv
|
||||||
|
import com.lagradost.cloudstream3.utils.AppUtils.isRtl
|
||||||
import com.lagradost.cloudstream3.utils.DataStoreHelper
|
import com.lagradost.cloudstream3.utils.DataStoreHelper
|
||||||
import com.lagradost.cloudstream3.utils.Event
|
import com.lagradost.cloudstream3.utils.Event
|
||||||
import com.lagradost.cloudstream3.utils.UIHelper
|
import com.lagradost.cloudstream3.utils.UIHelper
|
||||||
|
@ -34,14 +44,50 @@ import com.lagradost.cloudstream3.utils.UIHelper.hasPIPPermission
|
||||||
import com.lagradost.cloudstream3.utils.UIHelper.shouldShowPIPMode
|
import com.lagradost.cloudstream3.utils.UIHelper.shouldShowPIPMode
|
||||||
import com.lagradost.cloudstream3.utils.UIHelper.toPx
|
import com.lagradost.cloudstream3.utils.UIHelper.toPx
|
||||||
import org.schabi.newpipe.extractor.NewPipe
|
import org.schabi.newpipe.extractor.NewPipe
|
||||||
import java.util.*
|
import java.lang.ref.WeakReference
|
||||||
|
import java.util.Locale
|
||||||
|
import kotlin.math.max
|
||||||
|
import kotlin.math.min
|
||||||
|
|
||||||
|
enum class FocusDirection {
|
||||||
|
Start,
|
||||||
|
End,
|
||||||
|
Up,
|
||||||
|
Down,
|
||||||
|
}
|
||||||
|
|
||||||
object CommonActivity {
|
object CommonActivity {
|
||||||
|
|
||||||
|
private var _activity: WeakReference<Activity>? = null
|
||||||
|
var activity
|
||||||
|
get() = _activity?.get()
|
||||||
|
private set(value) {
|
||||||
|
_activity = WeakReference(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
@MainThread
|
||||||
|
fun setActivityInstance(newActivity: Activity?) {
|
||||||
|
activity = newActivity
|
||||||
|
}
|
||||||
|
|
||||||
@MainThread
|
@MainThread
|
||||||
fun Activity?.getCastSession(): CastSession? {
|
fun Activity?.getCastSession(): CastSession? {
|
||||||
return (this as MainActivity?)?.mSessionManager?.currentCastSession
|
return (this as MainActivity?)?.mSessionManager?.currentCastSession
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val displayMetrics: DisplayMetrics = Resources.getSystem().displayMetrics
|
||||||
|
|
||||||
|
// screenWidth and screenHeight does always
|
||||||
|
// refer to the screen while in landscape mode
|
||||||
|
val screenWidth: Int
|
||||||
|
get() {
|
||||||
|
return max(displayMetrics.widthPixels, displayMetrics.heightPixels)
|
||||||
|
}
|
||||||
|
val screenHeight: Int
|
||||||
|
get() {
|
||||||
|
return min(displayMetrics.widthPixels, displayMetrics.heightPixels)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
var canEnterPipMode: Boolean = false
|
var canEnterPipMode: Boolean = false
|
||||||
var canShowPipMode: Boolean = false
|
var canShowPipMode: Boolean = false
|
||||||
|
@ -53,9 +99,32 @@ object CommonActivity {
|
||||||
var playerEventListener: ((PlayerEventType) -> Unit)? = null
|
var playerEventListener: ((PlayerEventType) -> Unit)? = null
|
||||||
var keyEventListener: ((Pair<KeyEvent?, Boolean>) -> Boolean)? = 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) {
|
fun showToast(act: Activity?, text: UiText, duration: Int) {
|
||||||
if (act == null) return
|
if (act == null) return
|
||||||
text.asStringNull(act)?.let {
|
text.asStringNull(act)?.let {
|
||||||
|
@ -86,25 +155,19 @@ object CommonActivity {
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
logError(e)
|
logError(e)
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
val inflater =
|
val binding = ToastBinding.inflate(act.layoutInflater)
|
||||||
act.getSystemService(AppCompatActivity.LAYOUT_INFLATER_SERVICE) as LayoutInflater
|
binding.text.text = message.trim()
|
||||||
|
|
||||||
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()
|
|
||||||
|
|
||||||
|
// custom toasts are deprecated and won't appear when cs3 sets minSDK to api30 (A11)
|
||||||
val toast = Toast(act)
|
val toast = Toast(act)
|
||||||
toast.setGravity(Gravity.CENTER_HORIZONTAL or Gravity.BOTTOM, 0, 5.toPx)
|
|
||||||
toast.duration = duration ?: Toast.LENGTH_SHORT
|
toast.duration = duration ?: Toast.LENGTH_SHORT
|
||||||
toast.view = layout
|
toast.setGravity(Gravity.CENTER_HORIZONTAL or Gravity.BOTTOM, 0, 5.toPx)
|
||||||
//https://github.com/PureWriter/ToastCompat
|
toast.view = binding.root
|
||||||
toast.show()
|
|
||||||
currentToast = toast
|
currentToast = toast
|
||||||
|
toast.show()
|
||||||
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
logError(e)
|
logError(e)
|
||||||
}
|
}
|
||||||
|
@ -138,22 +201,25 @@ object CommonActivity {
|
||||||
setLocale(this, localeCode)
|
setLocale(this, localeCode)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun init(act: ComponentActivity?) {
|
fun init(act: Activity) {
|
||||||
if (act == null) return
|
setActivityInstance(act)
|
||||||
|
|
||||||
|
val componentActivity = activity as? ComponentActivity ?: return
|
||||||
|
|
||||||
//https://stackoverflow.com/questions/52594181/how-to-know-if-user-has-disabled-picture-in-picture-feature-permission
|
//https://stackoverflow.com/questions/52594181/how-to-know-if-user-has-disabled-picture-in-picture-feature-permission
|
||||||
//https://developer.android.com/guide/topics/ui/picture-in-picture
|
//https://developer.android.com/guide/topics/ui/picture-in-picture
|
||||||
canShowPipMode =
|
canShowPipMode =
|
||||||
Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && // OS SUPPORT
|
Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && // OS SUPPORT
|
||||||
act.packageManager.hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE) && // HAS FEATURE, MIGHT BE BLOCKED DUE TO POWER DRAIN
|
componentActivity.packageManager.hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE) && // HAS FEATURE, MIGHT BE BLOCKED DUE TO POWER DRAIN
|
||||||
act.hasPIPPermission() // CHECK IF FEATURE IS ENABLED IN SETTINGS
|
componentActivity.hasPIPPermission() // CHECK IF FEATURE IS ENABLED IN SETTINGS
|
||||||
|
|
||||||
act.updateLocale()
|
componentActivity.updateLocale()
|
||||||
act.updateTv()
|
componentActivity.updateTv()
|
||||||
NewPipe.init(DownloaderTestImpl.getInstance())
|
NewPipe.init(DownloaderTestImpl.getInstance())
|
||||||
|
|
||||||
for (resumeApp in resumeApps) {
|
for (resumeApp in resumeApps) {
|
||||||
resumeApp.launcher =
|
resumeApp.launcher =
|
||||||
act.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
componentActivity.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
||||||
val resultCode = result.resultCode
|
val resultCode = result.resultCode
|
||||||
val data = result.data
|
val data = result.data
|
||||||
if (resultCode == AppCompatActivity.RESULT_OK && data != null && resumeApp.position != null && resumeApp.duration != null) {
|
if (resultCode == AppCompatActivity.RESULT_OK && data != null && resumeApp.position != null && resumeApp.duration != null) {
|
||||||
|
@ -170,11 +236,11 @@ object CommonActivity {
|
||||||
// Ask for notification permissions on Android 13
|
// Ask for notification permissions on Android 13
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU &&
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU &&
|
||||||
ContextCompat.checkSelfPermission(
|
ContextCompat.checkSelfPermission(
|
||||||
act,
|
componentActivity,
|
||||||
Manifest.permission.POST_NOTIFICATIONS
|
Manifest.permission.POST_NOTIFICATIONS
|
||||||
) != PackageManager.PERMISSION_GRANTED
|
) != PackageManager.PERMISSION_GRANTED
|
||||||
) {
|
) {
|
||||||
val requestPermissionLauncher = act.registerForActivityResult(
|
val requestPermissionLauncher = componentActivity.registerForActivityResult(
|
||||||
ActivityResultContracts.RequestPermission()
|
ActivityResultContracts.RequestPermission()
|
||||||
) { isGranted: Boolean ->
|
) { isGranted: Boolean ->
|
||||||
Log.d(TAG, "Notification permission: $isGranted")
|
Log.d(TAG, "Notification permission: $isGranted")
|
||||||
|
@ -222,18 +288,22 @@ object CommonActivity {
|
||||||
"AmoledLight" -> R.style.AmoledModeLight
|
"AmoledLight" -> R.style.AmoledModeLight
|
||||||
"Monet" -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S)
|
"Monet" -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S)
|
||||||
R.style.MonetMode else R.style.AppTheme
|
R.style.MonetMode else R.style.AppTheme
|
||||||
|
|
||||||
else -> R.style.AppTheme
|
else -> R.style.AppTheme
|
||||||
}
|
}
|
||||||
|
|
||||||
val currentOverlayTheme =
|
val currentOverlayTheme =
|
||||||
when (settingsManager.getString(act.getString(R.string.primary_color_key), "Normal")) {
|
when (settingsManager.getString(act.getString(R.string.primary_color_key), "Normal")) {
|
||||||
"Normal" -> R.style.OverlayPrimaryColorNormal
|
"Normal" -> R.style.OverlayPrimaryColorNormal
|
||||||
|
"DandelionYellow" -> R.style.OverlayPrimaryColorDandelionYellow
|
||||||
"CarnationPink" -> R.style.OverlayPrimaryColorCarnationPink
|
"CarnationPink" -> R.style.OverlayPrimaryColorCarnationPink
|
||||||
|
"Orange" -> R.style.OverlayPrimaryColorOrange
|
||||||
"DarkGreen" -> R.style.OverlayPrimaryColorDarkGreen
|
"DarkGreen" -> R.style.OverlayPrimaryColorDarkGreen
|
||||||
"Maroon" -> R.style.OverlayPrimaryColorMaroon
|
"Maroon" -> R.style.OverlayPrimaryColorMaroon
|
||||||
"NavyBlue" -> R.style.OverlayPrimaryColorNavyBlue
|
"NavyBlue" -> R.style.OverlayPrimaryColorNavyBlue
|
||||||
"Grey" -> R.style.OverlayPrimaryColorGrey
|
"Grey" -> R.style.OverlayPrimaryColorGrey
|
||||||
"White" -> R.style.OverlayPrimaryColorWhite
|
"White" -> R.style.OverlayPrimaryColorWhite
|
||||||
|
"CoolBlue" -> R.style.OverlayPrimaryColorCoolBlue
|
||||||
"Brown" -> R.style.OverlayPrimaryColorBrown
|
"Brown" -> R.style.OverlayPrimaryColorBrown
|
||||||
"Purple" -> R.style.OverlayPrimaryColorPurple
|
"Purple" -> R.style.OverlayPrimaryColorPurple
|
||||||
"Green" -> R.style.OverlayPrimaryColorGreen
|
"Green" -> R.style.OverlayPrimaryColorGreen
|
||||||
|
@ -242,10 +312,13 @@ object CommonActivity {
|
||||||
"Banana" -> R.style.OverlayPrimaryColorBanana
|
"Banana" -> R.style.OverlayPrimaryColorBanana
|
||||||
"Party" -> R.style.OverlayPrimaryColorParty
|
"Party" -> R.style.OverlayPrimaryColorParty
|
||||||
"Pink" -> R.style.OverlayPrimaryColorPink
|
"Pink" -> R.style.OverlayPrimaryColorPink
|
||||||
|
"Lavender" -> R.style.OverlayPrimaryColorLavender
|
||||||
"Monet" -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S)
|
"Monet" -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S)
|
||||||
R.style.OverlayPrimaryColorMonet else R.style.OverlayPrimaryColorNormal
|
R.style.OverlayPrimaryColorMonet else R.style.OverlayPrimaryColorNormal
|
||||||
|
|
||||||
"Monet2" -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S)
|
"Monet2" -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S)
|
||||||
R.style.OverlayPrimaryColorMonetTwo else R.style.OverlayPrimaryColorNormal
|
R.style.OverlayPrimaryColorMonetTwo else R.style.OverlayPrimaryColorNormal
|
||||||
|
|
||||||
else -> R.style.OverlayPrimaryColorNormal
|
else -> R.style.OverlayPrimaryColorNormal
|
||||||
}
|
}
|
||||||
act.theme.applyStyle(currentTheme, true)
|
act.theme.applyStyle(currentTheme, true)
|
||||||
|
@ -257,55 +330,138 @@ object CommonActivity {
|
||||||
) // THEME IS SET BEFORE VIEW IS CREATED TO APPLY THE THEME TO THE MAIN VIEW
|
) // THEME IS SET BEFORE VIEW IS CREATED TO APPLY THE THEME TO THE MAIN VIEW
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getNextFocus(
|
/** because we want closes find, aka when multiple have the same id, we go to parent
|
||||||
act: Activity?,
|
until the correct one is found */
|
||||||
|
private fun localLook(from: View, id: Int): View? {
|
||||||
|
if (id == NO_ID) return null
|
||||||
|
var currentLook: View = from
|
||||||
|
// limit to 15 look depth
|
||||||
|
for (i in 0..15) {
|
||||||
|
currentLook.findViewById<View?>(id)?.let { return it }
|
||||||
|
currentLook = (currentLook.parent as? View) ?: break
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
/*var currentLook: View = view
|
||||||
|
while (true) {
|
||||||
|
val tmpNext = currentLook.findViewById<View?>(nextId)
|
||||||
|
if (tmpNext != null) {
|
||||||
|
next = tmpNext
|
||||||
|
break
|
||||||
|
}
|
||||||
|
currentLook = currentLook.parent as? View ?: break
|
||||||
|
}*/
|
||||||
|
|
||||||
|
private fun View.hasContent() : Boolean {
|
||||||
|
return isShown && when(this) {
|
||||||
|
//is RecyclerView -> this.childCount > 0
|
||||||
|
is ViewGroup -> this.childCount > 0
|
||||||
|
else -> true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** skips the initial stage of searching for an id using the view, see getNextFocus for specification */
|
||||||
|
fun continueGetNextFocus(
|
||||||
|
root: Any?,
|
||||||
|
view: View,
|
||||||
|
direction: FocusDirection,
|
||||||
|
nextId: Int,
|
||||||
|
depth: Int = 0
|
||||||
|
): View? {
|
||||||
|
if (nextId == NO_ID) return null
|
||||||
|
|
||||||
|
// do an initial search for the view, in case the localLook is too deep we can use this as
|
||||||
|
// an early break and backup view
|
||||||
|
var next =
|
||||||
|
when (root) {
|
||||||
|
is Activity -> root.findViewById(nextId)
|
||||||
|
is View -> root.rootView.findViewById<View?>(nextId)
|
||||||
|
else -> null
|
||||||
|
} ?: return null
|
||||||
|
|
||||||
|
next = localLook(view, nextId) ?: next
|
||||||
|
val shown = next.hasContent()
|
||||||
|
|
||||||
|
// if cant focus but visible then break and let android decide
|
||||||
|
// the exception if is the view is a parent and has children that wants focus
|
||||||
|
val hasChildrenThatWantsFocus = (next as? ViewGroup)?.let { parent ->
|
||||||
|
parent.descendantFocusability == ViewGroup.FOCUS_AFTER_DESCENDANTS && parent.childCount > 0
|
||||||
|
} ?: false
|
||||||
|
if (!next.isFocusable && shown && !hasChildrenThatWantsFocus) return null
|
||||||
|
|
||||||
|
// if not shown then continue because we will "skip" over views to get to a replacement
|
||||||
|
if (!shown) {
|
||||||
|
// we don't want a while true loop, so we let android decide if we find a recursive view
|
||||||
|
if (next == view) return null
|
||||||
|
return getNextFocus(root, next, direction, depth + 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
(when (next) {
|
||||||
|
is ChipGroup -> {
|
||||||
|
next.children.firstOrNull { it.isFocusable && it.isShown }
|
||||||
|
}
|
||||||
|
|
||||||
|
is NavigationRailView -> {
|
||||||
|
next.findViewById(next.selectedItemId) ?: next.findViewById(R.id.navigation_home)
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> null
|
||||||
|
})?.let {
|
||||||
|
return it
|
||||||
|
}
|
||||||
|
|
||||||
|
// nothing wrong with the view found, return it
|
||||||
|
return next
|
||||||
|
}
|
||||||
|
|
||||||
|
/** recursively looks for a next focus up to a depth of 10,
|
||||||
|
* this is used to override the normal shit focus system
|
||||||
|
* because this application has a lot of invisible views that messes with some tv devices*/
|
||||||
|
fun getNextFocus(
|
||||||
|
root: Any?,
|
||||||
view: View?,
|
view: View?,
|
||||||
direction: FocusDirection,
|
direction: FocusDirection,
|
||||||
depth: Int = 0
|
depth: Int = 0
|
||||||
): Int? {
|
): View? {
|
||||||
if (view == null || depth >= 10 || act == null) {
|
// if input is invalid let android decide + depth test to not crash if loop is found
|
||||||
|
if (view == null || depth >= 10 || root == null) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
val nextId = when (direction) {
|
var nextId = when (direction) {
|
||||||
FocusDirection.Left -> {
|
FocusDirection.Start -> {
|
||||||
view.nextFocusLeftId
|
if (view.isRtl())
|
||||||
|
view.nextFocusRightId
|
||||||
|
else
|
||||||
|
view.nextFocusLeftId
|
||||||
}
|
}
|
||||||
|
|
||||||
FocusDirection.Up -> {
|
FocusDirection.Up -> {
|
||||||
view.nextFocusUpId
|
view.nextFocusUpId
|
||||||
}
|
}
|
||||||
FocusDirection.Right -> {
|
|
||||||
view.nextFocusRightId
|
FocusDirection.End -> {
|
||||||
|
if (view.isRtl())
|
||||||
|
view.nextFocusLeftId
|
||||||
|
else
|
||||||
|
view.nextFocusRightId
|
||||||
}
|
}
|
||||||
|
|
||||||
FocusDirection.Down -> {
|
FocusDirection.Down -> {
|
||||||
view.nextFocusDownId
|
view.nextFocusDownId
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return if (nextId != -1) {
|
if (nextId == NO_ID) {
|
||||||
val next = act.findViewById<View?>(nextId)
|
// if not specified then use forward id
|
||||||
//println("NAME: ${next.accessibilityClassName} | ${next?.isShown}" )
|
nextId = view.nextFocusForwardId
|
||||||
|
// if view is still not found to next focus then return and let android decide
|
||||||
if (next?.isShown == false) {
|
if (nextId == NO_ID)
|
||||||
getNextFocus(act, next, direction, depth + 1)
|
return null
|
||||||
} else {
|
|
||||||
if (depth == 0) {
|
|
||||||
null
|
|
||||||
} else {
|
|
||||||
nextId
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
}
|
}
|
||||||
|
return continueGetNextFocus(root, view, direction, nextId, depth)
|
||||||
}
|
}
|
||||||
|
|
||||||
enum class FocusDirection {
|
|
||||||
Left,
|
|
||||||
Right,
|
|
||||||
Up,
|
|
||||||
Down,
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onKeyDown(act: Activity?, keyCode: Int, event: KeyEvent?) {
|
fun onKeyDown(act: Activity?, keyCode: Int, event: KeyEvent?) {
|
||||||
//println("Keycode: $keyCode")
|
//println("Keycode: $keyCode")
|
||||||
|
@ -328,30 +484,39 @@ object CommonActivity {
|
||||||
KeyEvent.KEYCODE_FORWARD, KeyEvent.KEYCODE_D, KeyEvent.KEYCODE_MEDIA_SKIP_FORWARD, KeyEvent.KEYCODE_MEDIA_FAST_FORWARD -> {
|
KeyEvent.KEYCODE_FORWARD, KeyEvent.KEYCODE_D, KeyEvent.KEYCODE_MEDIA_SKIP_FORWARD, KeyEvent.KEYCODE_MEDIA_FAST_FORWARD -> {
|
||||||
PlayerEventType.SeekForward
|
PlayerEventType.SeekForward
|
||||||
}
|
}
|
||||||
|
|
||||||
KeyEvent.KEYCODE_A, KeyEvent.KEYCODE_MEDIA_SKIP_BACKWARD, KeyEvent.KEYCODE_MEDIA_REWIND -> {
|
KeyEvent.KEYCODE_A, KeyEvent.KEYCODE_MEDIA_SKIP_BACKWARD, KeyEvent.KEYCODE_MEDIA_REWIND -> {
|
||||||
PlayerEventType.SeekBack
|
PlayerEventType.SeekBack
|
||||||
}
|
}
|
||||||
|
|
||||||
KeyEvent.KEYCODE_MEDIA_NEXT, KeyEvent.KEYCODE_BUTTON_R1, KeyEvent.KEYCODE_N -> {
|
KeyEvent.KEYCODE_MEDIA_NEXT, KeyEvent.KEYCODE_BUTTON_R1, KeyEvent.KEYCODE_N -> {
|
||||||
PlayerEventType.NextEpisode
|
PlayerEventType.NextEpisode
|
||||||
}
|
}
|
||||||
|
|
||||||
KeyEvent.KEYCODE_MEDIA_PREVIOUS, KeyEvent.KEYCODE_BUTTON_L1, KeyEvent.KEYCODE_B -> {
|
KeyEvent.KEYCODE_MEDIA_PREVIOUS, KeyEvent.KEYCODE_BUTTON_L1, KeyEvent.KEYCODE_B -> {
|
||||||
PlayerEventType.PrevEpisode
|
PlayerEventType.PrevEpisode
|
||||||
}
|
}
|
||||||
|
|
||||||
KeyEvent.KEYCODE_MEDIA_PAUSE -> {
|
KeyEvent.KEYCODE_MEDIA_PAUSE -> {
|
||||||
PlayerEventType.Pause
|
PlayerEventType.Pause
|
||||||
}
|
}
|
||||||
|
|
||||||
KeyEvent.KEYCODE_MEDIA_PLAY, KeyEvent.KEYCODE_BUTTON_START -> {
|
KeyEvent.KEYCODE_MEDIA_PLAY, KeyEvent.KEYCODE_BUTTON_START -> {
|
||||||
PlayerEventType.Play
|
PlayerEventType.Play
|
||||||
}
|
}
|
||||||
|
|
||||||
KeyEvent.KEYCODE_L, KeyEvent.KEYCODE_NUMPAD_7, KeyEvent.KEYCODE_7 -> {
|
KeyEvent.KEYCODE_L, KeyEvent.KEYCODE_NUMPAD_7, KeyEvent.KEYCODE_7 -> {
|
||||||
PlayerEventType.Lock
|
PlayerEventType.Lock
|
||||||
}
|
}
|
||||||
|
|
||||||
KeyEvent.KEYCODE_H, KeyEvent.KEYCODE_MENU -> {
|
KeyEvent.KEYCODE_H, KeyEvent.KEYCODE_MENU -> {
|
||||||
PlayerEventType.ToggleHide
|
PlayerEventType.ToggleHide
|
||||||
}
|
}
|
||||||
|
|
||||||
KeyEvent.KEYCODE_M, KeyEvent.KEYCODE_VOLUME_MUTE -> {
|
KeyEvent.KEYCODE_M, KeyEvent.KEYCODE_VOLUME_MUTE -> {
|
||||||
PlayerEventType.ToggleMute
|
PlayerEventType.ToggleMute
|
||||||
}
|
}
|
||||||
|
|
||||||
KeyEvent.KEYCODE_S, KeyEvent.KEYCODE_NUMPAD_9, KeyEvent.KEYCODE_9 -> {
|
KeyEvent.KEYCODE_S, KeyEvent.KEYCODE_NUMPAD_9, KeyEvent.KEYCODE_9 -> {
|
||||||
PlayerEventType.ShowMirrors
|
PlayerEventType.ShowMirrors
|
||||||
}
|
}
|
||||||
|
@ -359,21 +524,27 @@ object CommonActivity {
|
||||||
KeyEvent.KEYCODE_O, KeyEvent.KEYCODE_NUMPAD_8, KeyEvent.KEYCODE_8 -> {
|
KeyEvent.KEYCODE_O, KeyEvent.KEYCODE_NUMPAD_8, KeyEvent.KEYCODE_8 -> {
|
||||||
PlayerEventType.SearchSubtitlesOnline
|
PlayerEventType.SearchSubtitlesOnline
|
||||||
}
|
}
|
||||||
|
|
||||||
KeyEvent.KEYCODE_E, KeyEvent.KEYCODE_NUMPAD_3, KeyEvent.KEYCODE_3 -> {
|
KeyEvent.KEYCODE_E, KeyEvent.KEYCODE_NUMPAD_3, KeyEvent.KEYCODE_3 -> {
|
||||||
PlayerEventType.ShowSpeed
|
PlayerEventType.ShowSpeed
|
||||||
}
|
}
|
||||||
|
|
||||||
KeyEvent.KEYCODE_R, KeyEvent.KEYCODE_NUMPAD_0, KeyEvent.KEYCODE_0 -> {
|
KeyEvent.KEYCODE_R, KeyEvent.KEYCODE_NUMPAD_0, KeyEvent.KEYCODE_0 -> {
|
||||||
PlayerEventType.Resize
|
PlayerEventType.Resize
|
||||||
}
|
}
|
||||||
|
|
||||||
KeyEvent.KEYCODE_C, KeyEvent.KEYCODE_NUMPAD_4, KeyEvent.KEYCODE_4 -> {
|
KeyEvent.KEYCODE_C, KeyEvent.KEYCODE_NUMPAD_4, KeyEvent.KEYCODE_4 -> {
|
||||||
PlayerEventType.SkipOp
|
PlayerEventType.SkipOp
|
||||||
}
|
}
|
||||||
|
|
||||||
KeyEvent.KEYCODE_V, KeyEvent.KEYCODE_NUMPAD_5, KeyEvent.KEYCODE_5 -> {
|
KeyEvent.KEYCODE_V, KeyEvent.KEYCODE_NUMPAD_5, KeyEvent.KEYCODE_5 -> {
|
||||||
PlayerEventType.SkipCurrentChapter
|
PlayerEventType.SkipCurrentChapter
|
||||||
}
|
}
|
||||||
|
|
||||||
KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE, KeyEvent.KEYCODE_P, KeyEvent.KEYCODE_SPACE, KeyEvent.KEYCODE_NUMPAD_ENTER, KeyEvent.KEYCODE_ENTER -> { // space is not captured due to navigation
|
KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE, KeyEvent.KEYCODE_P, KeyEvent.KEYCODE_SPACE, KeyEvent.KEYCODE_NUMPAD_ENTER, KeyEvent.KEYCODE_ENTER -> { // space is not captured due to navigation
|
||||||
PlayerEventType.PlayPauseToggle
|
PlayerEventType.PlayPauseToggle
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> null
|
else -> null
|
||||||
}?.let { playerEvent ->
|
}?.let { playerEvent ->
|
||||||
playerEventListener?.invoke(playerEvent)
|
playerEventListener?.invoke(playerEvent)
|
||||||
|
@ -386,64 +557,64 @@ object CommonActivity {
|
||||||
//}
|
//}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** overrides focus and custom key events */
|
||||||
fun dispatchKeyEvent(act: Activity?, event: KeyEvent?): Boolean? {
|
fun dispatchKeyEvent(act: Activity?, event: KeyEvent?): Boolean? {
|
||||||
if (act == null) return null
|
if (act == null) return null
|
||||||
|
val currentFocus = act.currentFocus
|
||||||
|
|
||||||
event?.keyCode?.let { keyCode ->
|
event?.keyCode?.let { keyCode ->
|
||||||
when (event.action) {
|
if (currentFocus == null || event.action != KeyEvent.ACTION_DOWN) return@let
|
||||||
KeyEvent.ACTION_DOWN -> {
|
val nextView = when (keyCode) {
|
||||||
if (act.currentFocus != null) {
|
KeyEvent.KEYCODE_DPAD_LEFT -> getNextFocus(
|
||||||
val next = when (keyCode) {
|
act,
|
||||||
KeyEvent.KEYCODE_DPAD_LEFT -> getNextFocus(
|
currentFocus,
|
||||||
act,
|
FocusDirection.Start
|
||||||
act.currentFocus,
|
)
|
||||||
FocusDirection.Left
|
|
||||||
)
|
|
||||||
KeyEvent.KEYCODE_DPAD_RIGHT -> getNextFocus(
|
|
||||||
act,
|
|
||||||
act.currentFocus,
|
|
||||||
FocusDirection.Right
|
|
||||||
)
|
|
||||||
KeyEvent.KEYCODE_DPAD_UP -> getNextFocus(
|
|
||||||
act,
|
|
||||||
act.currentFocus,
|
|
||||||
FocusDirection.Up
|
|
||||||
)
|
|
||||||
KeyEvent.KEYCODE_DPAD_DOWN -> getNextFocus(
|
|
||||||
act,
|
|
||||||
act.currentFocus,
|
|
||||||
FocusDirection.Down
|
|
||||||
)
|
|
||||||
|
|
||||||
else -> null
|
KeyEvent.KEYCODE_DPAD_RIGHT -> getNextFocus(
|
||||||
}
|
act,
|
||||||
|
currentFocus,
|
||||||
|
FocusDirection.End
|
||||||
|
)
|
||||||
|
|
||||||
if (next != null && next != -1) {
|
KeyEvent.KEYCODE_DPAD_UP -> getNextFocus(
|
||||||
val nextView = act.findViewById<View?>(next)
|
act,
|
||||||
if (nextView != null) {
|
currentFocus,
|
||||||
nextView.requestFocus()
|
FocusDirection.Up
|
||||||
keyEventListener?.invoke(Pair(event, true))
|
)
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
when (keyCode) {
|
KeyEvent.KEYCODE_DPAD_DOWN -> getNextFocus(
|
||||||
KeyEvent.KEYCODE_DPAD_CENTER -> {
|
act,
|
||||||
if (act.currentFocus is SearchView || act.currentFocus is SearchView.SearchAutoComplete) {
|
currentFocus,
|
||||||
UIHelper.showInputMethod(act.currentFocus?.findFocus())
|
FocusDirection.Down
|
||||||
}
|
)
|
||||||
}
|
|
||||||
}
|
else -> null
|
||||||
}
|
|
||||||
//println("Keycode: $keyCode")
|
|
||||||
//showToast(
|
|
||||||
// this,
|
|
||||||
// "Got Keycode $keyCode | ${KeyEvent.keyCodeToString(keyCode)} \n ${event?.action}",
|
|
||||||
// Toast.LENGTH_LONG
|
|
||||||
//)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
// println("NEXT FOCUS : $nextView")
|
||||||
|
if (nextView != null) {
|
||||||
|
nextView.requestFocus()
|
||||||
|
keyEventListener?.invoke(Pair(event, true))
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER &&
|
||||||
|
(act.currentFocus is SearchView || act.currentFocus is SearchView.SearchAutoComplete)
|
||||||
|
) {
|
||||||
|
UIHelper.showInputMethod(act.currentFocus?.findFocus())
|
||||||
|
}
|
||||||
|
|
||||||
|
//println("Keycode: $keyCode")
|
||||||
|
//showToast(
|
||||||
|
// this,
|
||||||
|
// "Got Keycode $keyCode | ${KeyEvent.keyCodeToString(keyCode)} \n ${event?.action}",
|
||||||
|
// Toast.LENGTH_LONG
|
||||||
|
//)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// if someone else want to override the focus then don't handle the event as it is already
|
||||||
|
// consumed. used in video player
|
||||||
if (keyEventListener?.invoke(Pair(event, false)) == true) {
|
if (keyEventListener?.invoke(Pair(event, false)) == true) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
|
@ -50,7 +50,7 @@ class DownloaderTestImpl private constructor(builder: OkHttpClient.Builder) : Do
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val USER_AGENT =
|
private const val USER_AGENT =
|
||||||
"Mozilla/5.0 (Windows NT 10.0; rv:91.0) Gecko/20100101 Firefox/91.0"
|
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36"
|
||||||
private var instance: DownloaderTestImpl? = null
|
private var instance: DownloaderTestImpl? = null
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -9,33 +9,38 @@ import androidx.preference.PreferenceManager
|
||||||
import com.fasterxml.jackson.annotation.JsonProperty
|
import com.fasterxml.jackson.annotation.JsonProperty
|
||||||
import com.fasterxml.jackson.databind.DeserializationFeature
|
import com.fasterxml.jackson.databind.DeserializationFeature
|
||||||
import com.fasterxml.jackson.databind.json.JsonMapper
|
import com.fasterxml.jackson.databind.json.JsonMapper
|
||||||
import com.fasterxml.jackson.module.kotlin.KotlinModule
|
import com.fasterxml.jackson.module.kotlin.kotlinModule
|
||||||
import com.lagradost.cloudstream3.mvvm.logError
|
import com.lagradost.cloudstream3.mvvm.logError
|
||||||
|
import com.lagradost.cloudstream3.mvvm.normalSafeApiCall
|
||||||
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.aniListApi
|
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.aniListApi
|
||||||
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.malApi
|
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.malApi
|
||||||
|
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.simklApi
|
||||||
|
import com.lagradost.cloudstream3.syncproviders.SyncIdName
|
||||||
|
import com.lagradost.cloudstream3.syncproviders.providers.SimklApi
|
||||||
import com.lagradost.cloudstream3.ui.player.SubtitleData
|
import com.lagradost.cloudstream3.ui.player.SubtitleData
|
||||||
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings
|
import com.lagradost.cloudstream3.ui.result.ResultViewModel2
|
||||||
|
import com.lagradost.cloudstream3.utils.*
|
||||||
import com.lagradost.cloudstream3.utils.AppUtils.toJson
|
import com.lagradost.cloudstream3.utils.AppUtils.toJson
|
||||||
|
import com.lagradost.cloudstream3.utils.Coroutines.mainWork
|
||||||
import com.lagradost.cloudstream3.utils.Coroutines.threadSafeListOf
|
import com.lagradost.cloudstream3.utils.Coroutines.threadSafeListOf
|
||||||
import com.lagradost.cloudstream3.utils.ExtractorLink
|
import com.lagradost.nicehttp.RequestBodyTypes
|
||||||
import okhttp3.Interceptor
|
import okhttp3.Interceptor
|
||||||
|
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
||||||
|
import okhttp3.RequestBody.Companion.toRequestBody
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import kotlin.math.absoluteValue
|
import kotlin.math.absoluteValue
|
||||||
|
|
||||||
const val USER_AGENT =
|
|
||||||
"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
|
* Defines the constant for the all languages preference, if this is set then it is
|
||||||
* the equivalent of all languages being set
|
* the equivalent of all languages being set
|
||||||
**/
|
**/
|
||||||
const val AllLanguagesName = "universal"
|
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 {
|
object APIHolder {
|
||||||
val unixTime: Long
|
val unixTime: Long
|
||||||
get() = System.currentTimeMillis() / 1000L
|
get() = System.currentTimeMillis() / 1000L
|
||||||
|
@ -48,8 +53,10 @@ object APIHolder {
|
||||||
val allProviders = threadSafeListOf<MainAPI>()
|
val allProviders = threadSafeListOf<MainAPI>()
|
||||||
|
|
||||||
fun initAll() {
|
fun initAll() {
|
||||||
for (api in allProviders) {
|
synchronized(allProviders) {
|
||||||
api.init()
|
for (api in allProviders) {
|
||||||
|
api.init()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
apiMap = null
|
apiMap = null
|
||||||
}
|
}
|
||||||
|
@ -62,27 +69,35 @@ object APIHolder {
|
||||||
var apiMap: Map<String, Int>? = null
|
var apiMap: Map<String, Int>? = null
|
||||||
|
|
||||||
fun addPluginMapping(plugin: MainAPI) {
|
fun addPluginMapping(plugin: MainAPI) {
|
||||||
apis = apis + plugin
|
synchronized(apis) {
|
||||||
|
apis = apis + plugin
|
||||||
|
}
|
||||||
initMap(true)
|
initMap(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun removePluginMapping(plugin: MainAPI) {
|
fun removePluginMapping(plugin: MainAPI) {
|
||||||
apis = apis.filter { it != plugin }
|
synchronized(apis) {
|
||||||
|
apis = apis.filter { it != plugin }
|
||||||
|
}
|
||||||
initMap(true)
|
initMap(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun initMap(forcedUpdate: Boolean = false) {
|
private fun initMap(forcedUpdate: Boolean = false) {
|
||||||
if (apiMap == null || forcedUpdate)
|
synchronized(apis) {
|
||||||
apiMap = apis.mapIndexed { index, api -> api.name to index }.toMap()
|
if (apiMap == null || forcedUpdate)
|
||||||
|
apiMap = apis.mapIndexed { index, api -> api.name to index }.toMap()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getApiFromNameNull(apiName: String?): MainAPI? {
|
fun getApiFromNameNull(apiName: String?): MainAPI? {
|
||||||
if (apiName == null) return null
|
if (apiName == null) return null
|
||||||
synchronized(allProviders) {
|
synchronized(allProviders) {
|
||||||
initMap()
|
initMap()
|
||||||
return apiMap?.get(apiName)?.let { apis.getOrNull(it) }
|
synchronized(apis) {
|
||||||
|
return apiMap?.get(apiName)?.let { apis.getOrNull(it) }
|
||||||
// Leave the ?. null check, it can crash regardless
|
// Leave the ?. null check, it can crash regardless
|
||||||
?: allProviders.firstOrNull { it?.name == apiName }
|
?: allProviders.firstOrNull { it.name == apiName }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -102,7 +117,9 @@ object APIHolder {
|
||||||
}
|
}
|
||||||
|
|
||||||
fun LoadResponse.getId(): Int {
|
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)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -160,13 +177,113 @@ object APIHolder {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 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()
|
||||||
|
* @param year Optional parameter to only get anime with a specific year
|
||||||
|
**/
|
||||||
|
suspend fun getTracker(
|
||||||
|
titles: List<String>,
|
||||||
|
types: Set<TrackerType>?,
|
||||||
|
year: Int?,
|
||||||
|
lessAccurate: Boolean
|
||||||
|
): Tracker? {
|
||||||
|
return try {
|
||||||
|
require(titles.isNotEmpty()) { "titles must no be empty when calling getTracker" }
|
||||||
|
|
||||||
|
val mainTitle = titles[0]
|
||||||
|
val search =
|
||||||
|
trackerCache[mainTitle]
|
||||||
|
?: searchAnilist(mainTitle)?.also {
|
||||||
|
trackerCache[mainTitle] = it
|
||||||
|
} ?: return null
|
||||||
|
|
||||||
|
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.format, true) } == true
|
||||||
|
if (lessAccurate) matchingTitles || matchingTypes && matchingYears else matchingTitles && matchingTypes && matchingYears
|
||||||
|
} ?: return null
|
||||||
|
|
||||||
|
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> {
|
fun Context.getApiSettings(): HashSet<String> {
|
||||||
//val settingsManager = PreferenceManager.getDefaultSharedPreferences(this)
|
//val settingsManager = PreferenceManager.getDefaultSharedPreferences(this)
|
||||||
|
|
||||||
val hashSet = HashSet<String>()
|
val hashSet = HashSet<String>()
|
||||||
val activeLangs = getApiProviderLangSettings()
|
val activeLangs = getApiProviderLangSettings()
|
||||||
val hasUniversal = activeLangs.contains(AllLanguagesName)
|
val hasUniversal = activeLangs.contains(AllLanguagesName)
|
||||||
hashSet.addAll(apis.filter { hasUniversal || activeLangs.contains(it.lang) }
|
hashSet.addAll(synchronized(apis) { apis.filter { hasUniversal || activeLangs.contains(it.lang) } }
|
||||||
.map { it.name })
|
.map { it.name })
|
||||||
|
|
||||||
/*val set = settingsManager.getStringSet(
|
/*val set = settingsManager.getStringSet(
|
||||||
|
@ -265,8 +382,9 @@ object APIHolder {
|
||||||
} ?: default
|
} ?: default
|
||||||
val langs = this.getApiProviderLangSettings()
|
val langs = this.getApiProviderLangSettings()
|
||||||
val hasUniversal = langs.contains(AllLanguagesName)
|
val hasUniversal = langs.contains(AllLanguagesName)
|
||||||
val allApis = apis.filter { hasUniversal || langs.contains(it.lang) }
|
val allApis = synchronized(apis) {
|
||||||
.filter { api -> api.hasMainPage || !hasHomePageIsRequired }
|
apis.filter { api -> (hasUniversal || langs.contains(api.lang)) && (api.hasMainPage || !hasHomePageIsRequired) }
|
||||||
|
}
|
||||||
return if (currentPrefMedia.isEmpty()) {
|
return if (currentPrefMedia.isEmpty()) {
|
||||||
allApis
|
allApis
|
||||||
} else {
|
} else {
|
||||||
|
@ -318,6 +436,57 @@ object APIHolder {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
// THIS IS WORK IN PROGRESS API
|
||||||
|
interface ITag {
|
||||||
|
val name: UiText
|
||||||
|
}
|
||||||
|
|
||||||
|
data class SimpleTag(override val name: UiText, val data: String) : ITag
|
||||||
|
|
||||||
|
enum class SelectType {
|
||||||
|
SingleSelect,
|
||||||
|
MultiSelect,
|
||||||
|
MultiSelectAndExclude,
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class SelectValue {
|
||||||
|
Selected,
|
||||||
|
Excluded,
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GenreSelector {
|
||||||
|
val title: UiText
|
||||||
|
val id : Int
|
||||||
|
}
|
||||||
|
|
||||||
|
data class TagSelector(
|
||||||
|
override val title: UiText,
|
||||||
|
override val id : Int,
|
||||||
|
val tags: Set<ITag>,
|
||||||
|
val defaultTags : Set<ITag> = setOf(),
|
||||||
|
val selectType: SelectType = SelectType.SingleSelect,
|
||||||
|
) : GenreSelector
|
||||||
|
|
||||||
|
data class BoolSelector(
|
||||||
|
override val title: UiText,
|
||||||
|
override val id : Int,
|
||||||
|
|
||||||
|
val defaultValue : Boolean = false,
|
||||||
|
) : GenreSelector
|
||||||
|
|
||||||
|
data class InputField(
|
||||||
|
override val title: UiText,
|
||||||
|
override val id : Int,
|
||||||
|
|
||||||
|
val hint : UiText? = null,
|
||||||
|
) : GenreSelector
|
||||||
|
|
||||||
|
// This response describes how a user might filter the homepage or search results
|
||||||
|
data class GenreResponse(
|
||||||
|
val searchSelectors : List<GenreSelector>,
|
||||||
|
val filterSelectors: List<GenreSelector> = searchSelectors
|
||||||
|
) */
|
||||||
|
|
||||||
/*
|
/*
|
||||||
0 = Site not good
|
0 = Site not good
|
||||||
|
@ -459,6 +628,20 @@ abstract class MainAPI {
|
||||||
open val hasMainPage = false
|
open val hasMainPage = false
|
||||||
open val hasQuickSearch = false
|
open val hasQuickSearch = false
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A set of which ids the provider can open with getLoadUrl()
|
||||||
|
* If the set contains SyncIdName.Imdb then getLoadUrl() can be started with
|
||||||
|
* an Imdb class which inherits from SyncId.
|
||||||
|
*
|
||||||
|
* getLoadUrl() is then used to get page url based on that ID.
|
||||||
|
*
|
||||||
|
* Example:
|
||||||
|
* "tt6723592" -> getLoadUrl(ImdbSyncId("tt6723592")) -> "mainUrl/imdb/tt6723592" -> load("mainUrl/imdb/tt6723592")
|
||||||
|
*
|
||||||
|
* This is used to launch pages from personal lists or recommendations using IDs.
|
||||||
|
**/
|
||||||
|
open val supportedSyncNames = setOf<SyncIdName>()
|
||||||
|
|
||||||
open val supportedTypes = setOf(
|
open val supportedTypes = setOf(
|
||||||
TvType.Movie,
|
TvType.Movie,
|
||||||
TvType.TvSeries,
|
TvType.TvSeries,
|
||||||
|
@ -529,6 +712,14 @@ abstract class MainAPI {
|
||||||
open fun getVideoInterceptor(extractorLink: ExtractorLink): Interceptor? {
|
open fun getVideoInterceptor(extractorLink: ExtractorLink): Interceptor? {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the load() url based on a sync ID like IMDb or MAL.
|
||||||
|
* Only contains SyncIds based on supportedSyncUrls.
|
||||||
|
**/
|
||||||
|
open suspend fun getLoadUrl(name: SyncIdName, id: String): String? {
|
||||||
|
return null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Might need a different implementation for desktop*/
|
/** Might need a different implementation for desktop*/
|
||||||
|
@ -555,8 +746,6 @@ fun base64Encode(array: ByteArray): String {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class ErrorLoadingException(message: String? = null) : Exception(message)
|
|
||||||
|
|
||||||
fun MainAPI.fixUrlNull(url: String?): String? {
|
fun MainAPI.fixUrlNull(url: String?): String? {
|
||||||
if (url.isNullOrEmpty()) {
|
if (url.isNullOrEmpty()) {
|
||||||
return null
|
return null
|
||||||
|
@ -615,6 +804,20 @@ fun fixTitle(str: String): String {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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()
|
||||||
|
* Use like the following: rhino.evaluateString(scope, js, "JavaScript", 1, null)
|
||||||
|
**/
|
||||||
|
suspend fun getRhinoContext(): org.mozilla.javascript.Context {
|
||||||
|
return Coroutines.mainWork {
|
||||||
|
val rhino = org.mozilla.javascript.Context.enter()
|
||||||
|
rhino.initSafeStandardObjects()
|
||||||
|
rhino.optimizationLevel = -1
|
||||||
|
rhino
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** https://www.imdb.com/title/tt2861424/ -> tt2861424 */
|
/** https://www.imdb.com/title/tt2861424/ -> tt2861424 */
|
||||||
fun imdbUrlToId(url: String): String? {
|
fun imdbUrlToId(url: String): String? {
|
||||||
return Regex("/title/(tt[0-9]*)").find(url)?.groupValues?.get(1)
|
return Regex("/title/(tt[0-9]*)").find(url)?.groupValues?.get(1)
|
||||||
|
@ -663,7 +866,25 @@ enum class TvType(value: Int?) {
|
||||||
AsianDrama(9),
|
AsianDrama(9),
|
||||||
Live(10),
|
Live(10),
|
||||||
NSFW(11),
|
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
|
// IN CASE OF FUTURE ANIME MOVIE OR SMTH
|
||||||
|
@ -980,14 +1201,16 @@ interface LoadResponse {
|
||||||
var syncData: MutableMap<String, String>
|
var syncData: MutableMap<String, String>
|
||||||
var posterHeaders: Map<String, String>?
|
var posterHeaders: Map<String, String>?
|
||||||
var backgroundPosterUrl: String?
|
var backgroundPosterUrl: String?
|
||||||
|
var contentRating: String?
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private val malIdPrefix = malApi.idPrefix
|
private val malIdPrefix = malApi.idPrefix
|
||||||
private val aniListIdPrefix = aniListApi.idPrefix
|
private val aniListIdPrefix = aniListApi.idPrefix
|
||||||
|
private val simklIdPrefix = simklApi.idPrefix
|
||||||
var isTrailersEnabled = true
|
var isTrailersEnabled = true
|
||||||
|
|
||||||
fun LoadResponse.isMovie(): Boolean {
|
fun LoadResponse.isMovie(): Boolean {
|
||||||
return this.type.isMovieType()
|
return this.type.isMovieType() || this is MovieLoadResponse
|
||||||
}
|
}
|
||||||
|
|
||||||
@JvmName("addActorNames")
|
@JvmName("addActorNames")
|
||||||
|
@ -1005,6 +1228,20 @@ interface LoadResponse {
|
||||||
this.actors = actors?.map { (actor, role) -> ActorData(actor, role = role) }
|
this.actors = actors?.map { (actor, role) -> ActorData(actor, role = role) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Internal helper function to add simkl ids from other databases.
|
||||||
|
*/
|
||||||
|
private fun LoadResponse.addSimklId(
|
||||||
|
database: SimklApi.Companion.SyncServices,
|
||||||
|
id: String?
|
||||||
|
) {
|
||||||
|
normalSafeApiCall {
|
||||||
|
this.syncData[simklIdPrefix] =
|
||||||
|
SimklApi.addIdToString(this.syncData[simklIdPrefix], database, id.toString())
|
||||||
|
?: return@normalSafeApiCall
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@JvmName("addActorsOnly")
|
@JvmName("addActorsOnly")
|
||||||
fun LoadResponse.addActors(actors: List<Actor>?) {
|
fun LoadResponse.addActors(actors: List<Actor>?) {
|
||||||
this.actors = actors?.map { actor -> ActorData(actor) }
|
this.actors = actors?.map { actor -> ActorData(actor) }
|
||||||
|
@ -1018,12 +1255,32 @@ interface LoadResponse {
|
||||||
return this.syncData[aniListIdPrefix]
|
return this.syncData[aniListIdPrefix]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun LoadResponse.getImdbId(): String? {
|
||||||
|
return normalSafeApiCall {
|
||||||
|
SimklApi.readIdFromString(this.syncData[simklIdPrefix])
|
||||||
|
?.get(SimklApi.Companion.SyncServices.Imdb)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun LoadResponse.getTMDbId(): String? {
|
||||||
|
return normalSafeApiCall {
|
||||||
|
SimklApi.readIdFromString(this.syncData[simklIdPrefix])
|
||||||
|
?.get(SimklApi.Companion.SyncServices.Tmdb)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun LoadResponse.addMalId(id: Int?) {
|
fun LoadResponse.addMalId(id: Int?) {
|
||||||
this.syncData[malIdPrefix] = (id ?: return).toString()
|
this.syncData[malIdPrefix] = (id ?: return).toString()
|
||||||
|
this.addSimklId(SimklApi.Companion.SyncServices.Mal, id.toString())
|
||||||
}
|
}
|
||||||
|
|
||||||
fun LoadResponse.addAniListId(id: Int?) {
|
fun LoadResponse.addAniListId(id: Int?) {
|
||||||
this.syncData[aniListIdPrefix] = (id ?: return).toString()
|
this.syncData[aniListIdPrefix] = (id ?: return).toString()
|
||||||
|
this.addSimklId(SimklApi.Companion.SyncServices.AniList, id.toString())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun LoadResponse.addSimklId(id: Int?) {
|
||||||
|
this.addSimklId(SimklApi.Companion.SyncServices.Simkl, id.toString())
|
||||||
}
|
}
|
||||||
|
|
||||||
fun LoadResponse.addImdbUrl(url: String?) {
|
fun LoadResponse.addImdbUrl(url: String?) {
|
||||||
|
@ -1105,6 +1362,7 @@ interface LoadResponse {
|
||||||
|
|
||||||
fun LoadResponse.addImdbId(id: String?) {
|
fun LoadResponse.addImdbId(id: String?) {
|
||||||
// TODO add imdb sync
|
// TODO add imdb sync
|
||||||
|
this.addSimklId(SimklApi.Companion.SyncServices.Imdb, id)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun LoadResponse.addTrackId(id: String?) {
|
fun LoadResponse.addTrackId(id: String?) {
|
||||||
|
@ -1117,6 +1375,7 @@ interface LoadResponse {
|
||||||
|
|
||||||
fun LoadResponse.addTMDbId(id: String?) {
|
fun LoadResponse.addTMDbId(id: String?) {
|
||||||
// TODO add TMDb sync
|
// TODO add TMDb sync
|
||||||
|
this.addSimklId(SimklApi.Companion.SyncServices.Tmdb, id)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun LoadResponse.addRating(text: String?) {
|
fun LoadResponse.addRating(text: String?) {
|
||||||
|
@ -1192,14 +1451,27 @@ fun LoadResponse?.isAnimeBased(): Boolean {
|
||||||
|
|
||||||
fun TvType?.isEpisodeBased(): Boolean {
|
fun TvType?.isEpisodeBased(): Boolean {
|
||||||
if (this == null) return false
|
if (this == null) return false
|
||||||
return (this == TvType.TvSeries || this == TvType.Anime)
|
return (this == TvType.TvSeries || this == TvType.Anime || this == TvType.AsianDrama)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
data class NextAiring(
|
data class NextAiring(
|
||||||
val episode: Int,
|
val episode: Int,
|
||||||
val unixTime: Long,
|
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
|
* @param season To be mapped with episode season, not shown in UI if displaySeason is defined
|
||||||
|
@ -1216,6 +1488,16 @@ interface EpisodeResponse {
|
||||||
var showStatus: ShowStatus?
|
var showStatus: ShowStatus?
|
||||||
var nextAiring: NextAiring?
|
var nextAiring: NextAiring?
|
||||||
var seasonNames: List<SeasonData>?
|
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")
|
@JvmName("addSeasonNamesString")
|
||||||
|
@ -1253,7 +1535,55 @@ data class TorrentLoadResponse(
|
||||||
override var syncData: MutableMap<String, String> = mutableMapOf(),
|
override var syncData: MutableMap<String, String> = mutableMapOf(),
|
||||||
override var posterHeaders: Map<String, String>? = null,
|
override var posterHeaders: Map<String, String>? = null,
|
||||||
override var backgroundPosterUrl: String? = null,
|
override var backgroundPosterUrl: String? = null,
|
||||||
) : LoadResponse
|
override var contentRating: String? = null,
|
||||||
|
) : LoadResponse {
|
||||||
|
/**
|
||||||
|
* Secondary constructor for backwards compatibility without contentRating.
|
||||||
|
* Remove this constructor after there is a new stable release and extensions are updated to support contentRating.
|
||||||
|
*/
|
||||||
|
constructor(
|
||||||
|
name: String,
|
||||||
|
url: String,
|
||||||
|
apiName: String,
|
||||||
|
magnet: String?,
|
||||||
|
torrent: String?,
|
||||||
|
plot: String?,
|
||||||
|
type: TvType = TvType.Torrent,
|
||||||
|
posterUrl: String? = null,
|
||||||
|
year: Int? = null,
|
||||||
|
rating: Int? = null,
|
||||||
|
tags: List<String>? = null,
|
||||||
|
duration: Int? = null,
|
||||||
|
trailers: MutableList<TrailerData> = mutableListOf(),
|
||||||
|
recommendations: List<SearchResponse>? = null,
|
||||||
|
actors: List<ActorData>? = null,
|
||||||
|
comingSoon: Boolean = false,
|
||||||
|
syncData: MutableMap<String, String> = mutableMapOf(),
|
||||||
|
posterHeaders: Map<String, String>? = null,
|
||||||
|
backgroundPosterUrl: String? = null,
|
||||||
|
) : this(
|
||||||
|
name,
|
||||||
|
url,
|
||||||
|
apiName,
|
||||||
|
magnet,
|
||||||
|
torrent,
|
||||||
|
plot,
|
||||||
|
type,
|
||||||
|
posterUrl,
|
||||||
|
year,
|
||||||
|
rating,
|
||||||
|
tags,
|
||||||
|
duration,
|
||||||
|
trailers,
|
||||||
|
recommendations,
|
||||||
|
actors,
|
||||||
|
comingSoon,
|
||||||
|
syncData,
|
||||||
|
posterHeaders,
|
||||||
|
backgroundPosterUrl,
|
||||||
|
null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
data class AnimeLoadResponse(
|
data class AnimeLoadResponse(
|
||||||
var engName: String? = null,
|
var engName: String? = null,
|
||||||
|
@ -1284,7 +1614,90 @@ data class AnimeLoadResponse(
|
||||||
override var nextAiring: NextAiring? = null,
|
override var nextAiring: NextAiring? = null,
|
||||||
override var seasonNames: List<SeasonData>? = null,
|
override var seasonNames: List<SeasonData>? = null,
|
||||||
override var backgroundPosterUrl: String? = null,
|
override var backgroundPosterUrl: String? = null,
|
||||||
) : LoadResponse, EpisodeResponse
|
override var contentRating: String? = null,
|
||||||
|
) : LoadResponse, EpisodeResponse {
|
||||||
|
override fun getLatestEpisodes(): Map<DubStatus, Int?> {
|
||||||
|
return episodes.map { (status, episodes) ->
|
||||||
|
val maxSeason = episodes.maxOfOrNull { it.season ?: Int.MIN_VALUE }
|
||||||
|
.takeUnless { it == Int.MIN_VALUE }
|
||||||
|
status to episodes
|
||||||
|
.filter { it.season == maxSeason }
|
||||||
|
.maxOfOrNull { it.episode ?: Int.MIN_VALUE }
|
||||||
|
.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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* If episodes already exist appends the list.
|
* If episodes already exist appends the list.
|
||||||
|
@ -1335,7 +1748,36 @@ data class LiveStreamLoadResponse(
|
||||||
override var syncData: MutableMap<String, String> = mutableMapOf(),
|
override var syncData: MutableMap<String, String> = mutableMapOf(),
|
||||||
override var posterHeaders: Map<String, String>? = null,
|
override var posterHeaders: Map<String, String>? = null,
|
||||||
override var backgroundPosterUrl: String? = null,
|
override var backgroundPosterUrl: String? = null,
|
||||||
) : LoadResponse
|
override var contentRating: String? = null,
|
||||||
|
) : LoadResponse {
|
||||||
|
/**
|
||||||
|
* Secondary constructor for backwards compatibility without contentRating.
|
||||||
|
* Remove this constructor after there is a new stable release and extensions are updated to support contentRating.
|
||||||
|
*/
|
||||||
|
constructor(
|
||||||
|
name: String,
|
||||||
|
url: String,
|
||||||
|
apiName: String,
|
||||||
|
dataUrl: String,
|
||||||
|
posterUrl: String? = null,
|
||||||
|
year: Int? = null,
|
||||||
|
plot: String? = null,
|
||||||
|
type: TvType = TvType.Live,
|
||||||
|
rating: Int? = null,
|
||||||
|
tags: List<String>? = null,
|
||||||
|
duration: Int? = null,
|
||||||
|
trailers: MutableList<TrailerData> = mutableListOf(),
|
||||||
|
recommendations: List<SearchResponse>? = null,
|
||||||
|
actors: List<ActorData>? = null,
|
||||||
|
comingSoon: Boolean = false,
|
||||||
|
syncData: MutableMap<String, String> = mutableMapOf(),
|
||||||
|
posterHeaders: Map<String, String>? = null,
|
||||||
|
backgroundPosterUrl: String? = null,
|
||||||
|
) : this(
|
||||||
|
name, url, apiName, dataUrl, posterUrl, year, plot, type, rating, tags, duration, trailers,
|
||||||
|
recommendations, actors, comingSoon, syncData, posterHeaders, backgroundPosterUrl, null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
data class MovieLoadResponse(
|
data class MovieLoadResponse(
|
||||||
override var name: String,
|
override var name: String,
|
||||||
|
@ -1358,7 +1800,36 @@ data class MovieLoadResponse(
|
||||||
override var syncData: MutableMap<String, String> = mutableMapOf(),
|
override var syncData: MutableMap<String, String> = mutableMapOf(),
|
||||||
override var posterHeaders: Map<String, String>? = null,
|
override var posterHeaders: Map<String, String>? = null,
|
||||||
override var backgroundPosterUrl: String? = null,
|
override var backgroundPosterUrl: String? = null,
|
||||||
) : LoadResponse
|
override var contentRating: String? = null,
|
||||||
|
) : LoadResponse {
|
||||||
|
/**
|
||||||
|
* Secondary constructor for backwards compatibility without contentRating.
|
||||||
|
* Remove this constructor after there is a new stable release and extensions are updated to support contentRating.
|
||||||
|
*/
|
||||||
|
constructor(
|
||||||
|
name: String,
|
||||||
|
url: String,
|
||||||
|
apiName: String,
|
||||||
|
type: TvType,
|
||||||
|
dataUrl: String,
|
||||||
|
posterUrl: String? = null,
|
||||||
|
year: Int? = null,
|
||||||
|
plot: String? = null,
|
||||||
|
rating: Int? = null,
|
||||||
|
tags: List<String>? = null,
|
||||||
|
duration: Int? = null,
|
||||||
|
trailers: MutableList<TrailerData> = mutableListOf(),
|
||||||
|
recommendations: List<SearchResponse>? = null,
|
||||||
|
actors: List<ActorData>? = null,
|
||||||
|
comingSoon: Boolean = false,
|
||||||
|
syncData: MutableMap<String, String> = mutableMapOf(),
|
||||||
|
posterHeaders: Map<String, String>? = null,
|
||||||
|
backgroundPosterUrl: String? = null,
|
||||||
|
) : this(
|
||||||
|
name, url, apiName, type, dataUrl, posterUrl, year, plot, rating, tags, duration, trailers,
|
||||||
|
recommendations, actors, comingSoon, syncData, posterHeaders, backgroundPosterUrl, null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun <T> MainAPI.newMovieLoadResponse(
|
suspend fun <T> MainAPI.newMovieLoadResponse(
|
||||||
name: String,
|
name: String,
|
||||||
|
@ -1482,7 +1953,81 @@ data class TvSeriesLoadResponse(
|
||||||
override var nextAiring: NextAiring? = null,
|
override var nextAiring: NextAiring? = null,
|
||||||
override var seasonNames: List<SeasonData>? = null,
|
override var seasonNames: List<SeasonData>? = null,
|
||||||
override var backgroundPosterUrl: String? = null,
|
override var backgroundPosterUrl: String? = null,
|
||||||
) : LoadResponse, EpisodeResponse
|
override var contentRating: String? = null,
|
||||||
|
) : LoadResponse, EpisodeResponse {
|
||||||
|
override fun getLatestEpisodes(): Map<DubStatus, Int?> {
|
||||||
|
val maxSeason =
|
||||||
|
episodes.maxOfOrNull { it.season ?: Int.MIN_VALUE }.takeUnless { it == Int.MIN_VALUE }
|
||||||
|
val max = episodes
|
||||||
|
.filter { it.season == maxSeason }
|
||||||
|
.maxOfOrNull { it.episode ?: Int.MIN_VALUE }
|
||||||
|
.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(
|
suspend fun MainAPI.newTvSeriesLoadResponse(
|
||||||
name: String,
|
name: String,
|
||||||
|
@ -1514,3 +2059,74 @@ fun fetchUrls(text: String?): List<String> {
|
||||||
|
|
||||||
fun String?.toRatingInt(): Int? =
|
fun String?.toRatingInt(): Int? =
|
||||||
this?.replace(" ", "")?.trim()?.toDoubleOrNull()?.absoluteValue?.times(1000f)?.toInt()
|
this?.replace(" ", "")?.trim()?.toDoubleOrNull()?.absoluteValue?.times(1000f)?.toInt()
|
||||||
|
|
||||||
|
data class Tracker(
|
||||||
|
val malId: Int? = null,
|
||||||
|
val aniId: String? = null,
|
||||||
|
val image: String? = null,
|
||||||
|
val cover: String? = null,
|
||||||
|
)
|
||||||
|
|
||||||
|
data class AniSearch(
|
||||||
|
@JsonProperty("data") var data: Data? = Data()
|
||||||
|
) {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* used for the getTracker() method
|
||||||
|
**/
|
||||||
|
enum class TrackerType {
|
||||||
|
MOVIE,
|
||||||
|
TV,
|
||||||
|
TV_SHORT,
|
||||||
|
ONA,
|
||||||
|
OVA,
|
||||||
|
SPECIAL,
|
||||||
|
MUSIC;
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun getTypes(type: TvType): Set<TrackerType> {
|
||||||
|
return when (type) {
|
||||||
|
TvType.Movie -> setOf(MOVIE)
|
||||||
|
TvType.AnimeMovie -> setOf(MOVIE)
|
||||||
|
TvType.TvSeries -> setOf(TV, TV_SHORT)
|
||||||
|
TvType.Anime -> setOf(TV, TV_SHORT, ONA, OVA)
|
||||||
|
TvType.OVA -> setOf(OVA, SPECIAL, ONA)
|
||||||
|
TvType.Others -> setOf(MUSIC)
|
||||||
|
else -> emptySet()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,53 @@
|
||||||
|
package com.lagradost.cloudstream3
|
||||||
|
|
||||||
|
import com.lagradost.cloudstream3.MainActivity.Companion.lastError
|
||||||
|
import com.lagradost.cloudstream3.mvvm.logError
|
||||||
|
import com.lagradost.cloudstream3.plugins.PluginManager.checkSafeModeFile
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
object NativeCrashHandler {
|
||||||
|
// external fun triggerNativeCrash()
|
||||||
|
/*private external fun initNativeCrashHandler()
|
||||||
|
private external fun getSignalStatus(): Int
|
||||||
|
|
||||||
|
private fun initSignalPolling() = CoroutineScope(Dispatchers.IO).launch {
|
||||||
|
|
||||||
|
//launch {
|
||||||
|
// delay(10000)
|
||||||
|
// triggerNativeCrash()
|
||||||
|
//}
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
delay(10_000)
|
||||||
|
val signal = getSignalStatus()
|
||||||
|
// Signal is initialized to zero
|
||||||
|
if (signal == 0) continue
|
||||||
|
|
||||||
|
// Do not crash in safe mode!
|
||||||
|
if (lastError != null) continue
|
||||||
|
if (checkSafeModeFile()) continue
|
||||||
|
|
||||||
|
AcraApplication.exceptionHandler?.uncaughtException(
|
||||||
|
Thread.currentThread(),
|
||||||
|
RuntimeException("Native crash with code: $signal. Try uninstalling extensions.\n")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun initCrashHandler() {
|
||||||
|
try {
|
||||||
|
System.loadLibrary("native-lib")
|
||||||
|
initNativeCrashHandler()
|
||||||
|
} catch (t: Throwable) {
|
||||||
|
// Make debug crash.
|
||||||
|
if (BuildConfig.DEBUG) throw t
|
||||||
|
logError(t)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
initSignalPolling()
|
||||||
|
}*/
|
||||||
|
}
|
|
@ -1,7 +1,7 @@
|
||||||
package com.lagradost.cloudstream3.extractors
|
package com.lagradost.cloudstream3.extractors
|
||||||
|
|
||||||
|
import com.lagradost.cloudstream3.SubtitleFile
|
||||||
import com.lagradost.cloudstream3.app
|
import com.lagradost.cloudstream3.app
|
||||||
import com.lagradost.cloudstream3.base64Decode
|
|
||||||
import com.lagradost.cloudstream3.utils.*
|
import com.lagradost.cloudstream3.utils.*
|
||||||
|
|
||||||
open class Acefile : ExtractorApi() {
|
open class Acefile : ExtractorApi() {
|
||||||
|
@ -9,31 +9,35 @@ open class Acefile : ExtractorApi() {
|
||||||
override val mainUrl = "https://acefile.co"
|
override val mainUrl = "https://acefile.co"
|
||||||
override val requiresReferer = false
|
override val requiresReferer = false
|
||||||
|
|
||||||
override suspend fun getUrl(url: String, referer: String?): List<ExtractorLink> {
|
override suspend fun getUrl(
|
||||||
val sources = mutableListOf<ExtractorLink>()
|
url: String,
|
||||||
app.get(url).document.select("script").map { script ->
|
referer: String?,
|
||||||
if (script.data().contains("eval(function(p,a,c,k,e,d)")) {
|
subtitleCallback: (SubtitleFile) -> Unit,
|
||||||
val data = getAndUnpack(script.data())
|
callback: (ExtractorLink) -> Unit
|
||||||
val id = data.substringAfter("{\"id\":\"").substringBefore("\",")
|
) {
|
||||||
val key = data.substringAfter("var nfck=\"").substringBefore("\";")
|
val id = "/(?:d|download|player|f|file)/(\\w+)".toRegex().find(url)?.groupValues?.get(1)
|
||||||
app.get("https://acefile.co/local/$id?key=$key").text.let {
|
val script = getAndUnpack(app.get("$mainUrl/player/${id ?: return}").text)
|
||||||
base64Decode(
|
val service = """service\s*=\s*['"]([^'"]+)""".toRegex().find(script)?.groupValues?.get(1)
|
||||||
it.substringAfter("JSON.parse(atob(\"").substringBefore("\"))")
|
val serverUrl = """['"](\S+check&id\S+?)['"]""".toRegex().find(script)?.groupValues?.get(1)
|
||||||
).let { res ->
|
?.replace("\"+service+\"", service ?: return)
|
||||||
sources.add(
|
|
||||||
ExtractorLink(
|
val video = app.get(serverUrl ?: return, referer = "$mainUrl/").parsedSafe<Source>()?.data
|
||||||
name,
|
|
||||||
name,
|
callback.invoke(
|
||||||
res.substringAfter("\"file\":\"").substringBefore("\","),
|
ExtractorLink(
|
||||||
"$mainUrl/",
|
this.name,
|
||||||
Qualities.Unknown.value,
|
this.name,
|
||||||
)
|
video ?: return,
|
||||||
)
|
"",
|
||||||
}
|
Qualities.Unknown.value,
|
||||||
}
|
INFER_TYPE
|
||||||
}
|
)
|
||||||
}
|
)
|
||||||
return sources
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
data class Source(
|
||||||
|
val data: String? = null,
|
||||||
|
)
|
||||||
|
|
||||||
}
|
}
|
|
@ -9,7 +9,7 @@ import java.net.URI
|
||||||
|
|
||||||
open class AsianLoad : ExtractorApi() {
|
open class AsianLoad : ExtractorApi() {
|
||||||
override var name = "AsianLoad"
|
override var name = "AsianLoad"
|
||||||
override var mainUrl = "https://asianembed.io"
|
override var mainUrl = "https://asianhdplay.pro"
|
||||||
override val requiresReferer = true
|
override val requiresReferer = true
|
||||||
|
|
||||||
private val sourceRegex = Regex("""sources:[\W\w]*?file:\s*?["'](.*?)["']""")
|
private val sourceRegex = Regex("""sources:[\W\w]*?file:\s*?["'](.*?)["']""")
|
||||||
|
@ -43,4 +43,4 @@ open class AsianLoad : ExtractorApi() {
|
||||||
return extractedLinksList
|
return extractedLinksList
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,23 @@
|
||||||
|
package com.lagradost.cloudstream3.extractors
|
||||||
|
|
||||||
|
import com.lagradost.cloudstream3.utils.*
|
||||||
|
|
||||||
|
open class ByteShare : ExtractorApi() {
|
||||||
|
override val name = "ByteShare"
|
||||||
|
override val mainUrl = "https://byteshare.to"
|
||||||
|
override val requiresReferer = false
|
||||||
|
|
||||||
|
override suspend fun getUrl(url: String, referer: String?): List<ExtractorLink> {
|
||||||
|
val sources = mutableListOf<ExtractorLink>()
|
||||||
|
sources.add(
|
||||||
|
ExtractorLink(
|
||||||
|
name,
|
||||||
|
name,
|
||||||
|
url.replace("/embed/", "/download/"),
|
||||||
|
"",
|
||||||
|
Qualities.Unknown.value,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return sources
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,13 +1,11 @@
|
||||||
package com.lagradost.cloudstream3.extractors
|
package com.lagradost.cloudstream3.extractors
|
||||||
|
|
||||||
import com.lagradost.cloudstream3.*
|
import com.lagradost.cloudstream3.USER_AGENT
|
||||||
import com.lagradost.cloudstream3.utils.Qualities
|
import com.lagradost.cloudstream3.app
|
||||||
|
import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson
|
||||||
import com.lagradost.cloudstream3.utils.ExtractorApi
|
import com.lagradost.cloudstream3.utils.ExtractorApi
|
||||||
import com.lagradost.cloudstream3.utils.ExtractorLink
|
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||||
import com.lagradost.cloudstream3.utils.M3u8Helper.Companion.generateM3u8
|
import com.lagradost.cloudstream3.utils.Qualities
|
||||||
import com.fasterxml.jackson.annotation.JsonProperty
|
|
||||||
import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson
|
|
||||||
import android.util.Log
|
|
||||||
import java.net.URLDecoder
|
import java.net.URLDecoder
|
||||||
|
|
||||||
open class Cda: ExtractorApi() {
|
open class Cda: ExtractorApi() {
|
||||||
|
|
|
@ -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
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -6,13 +6,19 @@ import com.lagradost.cloudstream3.app
|
||||||
import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson
|
import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson
|
||||||
import com.lagradost.cloudstream3.utils.ExtractorApi
|
import com.lagradost.cloudstream3.utils.ExtractorApi
|
||||||
import com.lagradost.cloudstream3.utils.ExtractorLink
|
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||||
import com.lagradost.cloudstream3.utils.Qualities
|
import com.lagradost.cloudstream3.utils.M3u8Helper.Companion.generateM3u8
|
||||||
import java.net.URL
|
import java.net.URL
|
||||||
|
|
||||||
|
class Geodailymotion : Dailymotion() {
|
||||||
|
override val name = "GeoDailymotion"
|
||||||
|
override val mainUrl = "https://geo.dailymotion.com"
|
||||||
|
}
|
||||||
|
|
||||||
open class Dailymotion : ExtractorApi() {
|
open class Dailymotion : ExtractorApi() {
|
||||||
override val mainUrl = "https://www.dailymotion.com"
|
override val mainUrl = "https://www.dailymotion.com"
|
||||||
override val name = "Dailymotion"
|
override val name = "Dailymotion"
|
||||||
override val requiresReferer = false
|
override val requiresReferer = false
|
||||||
|
private val baseUrl = "https://www.dailymotion.com"
|
||||||
|
|
||||||
@Suppress("RegExpSimplifiable")
|
@Suppress("RegExpSimplifiable")
|
||||||
private val videoIdRegex = "^[kx][a-zA-Z0-9]+\$".toRegex()
|
private val videoIdRegex = "^[kx][a-zA-Z0-9]+\$".toRegex()
|
||||||
|
@ -26,68 +32,68 @@ open class Dailymotion : ExtractorApi() {
|
||||||
callback: (ExtractorLink) -> Unit
|
callback: (ExtractorLink) -> Unit
|
||||||
) {
|
) {
|
||||||
val embedUrl = getEmbedUrl(url) ?: return
|
val embedUrl = getEmbedUrl(url) ?: return
|
||||||
val doc = app.get(embedUrl).document
|
val req = app.get(embedUrl)
|
||||||
val prefix = "window.__PLAYER_CONFIG__ = "
|
val prefix = "window.__PLAYER_CONFIG__ = "
|
||||||
val configStr = doc.selectFirst("script:containsData($prefix)")?.data() ?: return
|
val configStr = req.document.selectFirst("script:containsData($prefix)")?.data() ?: return
|
||||||
val config = tryParseJson<Config>(configStr.substringAfter(prefix)) ?: return
|
val config = tryParseJson<Config>(configStr.substringAfter(prefix).substringBefore(";").trim()) ?: return
|
||||||
val id = getVideoId(embedUrl) ?: return
|
val id = getVideoId(embedUrl) ?: return
|
||||||
val dmV1st = config.dmInternalData.v1st
|
val dmV1st = config.dmInternalData.v1st
|
||||||
val dmTs = config.dmInternalData.ts
|
val dmTs = config.dmInternalData.ts
|
||||||
val metaDataUrl =
|
val embedder = config.context.embedder
|
||||||
"$mainUrl/player/metadata/video/$id?locale=en&dmV1st=$dmV1st&dmTs=$dmTs&is_native_app=0"
|
val metaDataUrl = "$baseUrl/player/metadata/video/$id?embedder=$embedder&locale=en-US&dmV1st=$dmV1st&dmTs=$dmTs&is_native_app=0"
|
||||||
val cookies = mapOf(
|
val metaData = app.get(metaDataUrl, referer = embedUrl, cookies = req.cookies)
|
||||||
"v1st" to dmV1st,
|
|
||||||
"dmvk" to config.context.dmvk,
|
|
||||||
"ts" to dmTs.toString()
|
|
||||||
)
|
|
||||||
val metaData = app.get(metaDataUrl, referer = embedUrl, cookies = cookies)
|
|
||||||
.parsedSafe<MetaData>() ?: return
|
.parsedSafe<MetaData>() ?: return
|
||||||
metaData.qualities.forEach { (key, video) ->
|
metaData.qualities.forEach { (_, video) ->
|
||||||
video.forEach {
|
video.forEach {
|
||||||
callback.invoke(
|
getStream(it.url, this.name, callback)
|
||||||
ExtractorLink(
|
|
||||||
name,
|
|
||||||
"$name $key",
|
|
||||||
it.url,
|
|
||||||
"",
|
|
||||||
Qualities.Unknown.value,
|
|
||||||
true
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getEmbedUrl(url: String): String? {
|
private fun getEmbedUrl(url: String): String? {
|
||||||
if (url.contains("/embed/")) {
|
if (url.contains("/embed/") || url.contains("/video/")) {
|
||||||
return url
|
return url
|
||||||
}
|
|
||||||
val vid = getVideoId(url) ?: return null
|
|
||||||
return "$mainUrl/embed/video/$vid"
|
|
||||||
}
|
}
|
||||||
|
if (url.contains("geo.dailymotion.com")) {
|
||||||
|
val videoId = url.substringAfter("video=")
|
||||||
|
return "$baseUrl/embed/video/$videoId"
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
private fun getVideoId(url: String): String? {
|
private fun getVideoId(url: String): String? {
|
||||||
val path = URL(url).path
|
val path = URL(url).path
|
||||||
val id = path.substringAfter("video/")
|
val id = path.substringAfter("/video/")
|
||||||
if (id.matches(videoIdRegex)) {
|
if (id.matches(videoIdRegex)) {
|
||||||
return id
|
return id
|
||||||
}
|
}
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private suspend fun getStream(
|
||||||
|
streamLink: String,
|
||||||
|
name: String,
|
||||||
|
callback: (ExtractorLink) -> Unit
|
||||||
|
) {
|
||||||
|
return generateM3u8(
|
||||||
|
name,
|
||||||
|
streamLink,
|
||||||
|
"",
|
||||||
|
).forEach(callback)
|
||||||
|
}
|
||||||
data class Config(
|
data class Config(
|
||||||
val context: Context,
|
val context: Context,
|
||||||
val dmInternalData: InternalData
|
val dmInternalData: InternalData
|
||||||
)
|
)
|
||||||
|
|
||||||
data class InternalData(
|
data class InternalData(
|
||||||
val ts: Int,
|
val ts: Long,
|
||||||
val v1st: String
|
val v1st: String
|
||||||
)
|
)
|
||||||
|
|
||||||
data class Context(
|
data class Context(
|
||||||
@JsonProperty("access_token") val accessToken: String?,
|
@JsonProperty("access_token") val accessToken: String?,
|
||||||
val dmvk: String,
|
val embedder: String?,
|
||||||
)
|
)
|
||||||
|
|
||||||
data class MetaData(
|
data class MetaData(
|
||||||
|
|
|
@ -7,6 +7,10 @@ import com.lagradost.cloudstream3.utils.Qualities
|
||||||
import com.lagradost.cloudstream3.utils.getQualityFromName
|
import com.lagradost.cloudstream3.utils.getQualityFromName
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
|
|
||||||
|
class Dooood : DoodLaExtractor() {
|
||||||
|
override var mainUrl = "https://dooood.com"
|
||||||
|
}
|
||||||
|
|
||||||
class DoodWfExtractor : DoodLaExtractor() {
|
class DoodWfExtractor : DoodLaExtractor() {
|
||||||
override var mainUrl = "https://dood.wf"
|
override var mainUrl = "https://dood.wf"
|
||||||
}
|
}
|
||||||
|
@ -38,6 +42,9 @@ class DoodWsExtractor : DoodLaExtractor() {
|
||||||
override var mainUrl = "https://dood.ws"
|
override var mainUrl = "https://dood.ws"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class DoodYtExtractor : DoodLaExtractor() {
|
||||||
|
override var mainUrl = "https://dood.yt"
|
||||||
|
}
|
||||||
|
|
||||||
open class DoodLaExtractor : ExtractorApi() {
|
open class DoodLaExtractor : ExtractorApi() {
|
||||||
override var name = "DoodStream"
|
override var name = "DoodStream"
|
||||||
|
@ -55,7 +62,7 @@ open class DoodLaExtractor : ExtractorApi() {
|
||||||
val quality = Regex("\\d{3,4}p").find(response0.substringAfter("<title>").substringBefore("</title>"))?.groupValues?.get(0)
|
val quality = Regex("\\d{3,4}p").find(response0.substringAfter("<title>").substringBefore("</title>"))?.groupValues?.get(0)
|
||||||
return listOf(
|
return listOf(
|
||||||
ExtractorLink(
|
ExtractorLink(
|
||||||
trueUrl,
|
this.name,
|
||||||
this.name,
|
this.name,
|
||||||
trueUrl,
|
trueUrl,
|
||||||
mainUrl,
|
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
|
||||||
|
}
|
||||||
|
}
|
|
@ -16,26 +16,7 @@ open class Evoload : ExtractorApi() {
|
||||||
|
|
||||||
|
|
||||||
override suspend fun getUrl(url: String, referer: String?): List<ExtractorLink> {
|
override suspend fun getUrl(url: String, referer: String?): List<ExtractorLink> {
|
||||||
val lang = url.substring(0, 2)
|
val id = url.replace("https://evoload.io/e/", "") // wanted media id
|
||||||
val flag =
|
|
||||||
if (lang == "vo") {
|
|
||||||
" \uD83C\uDDEC\uD83C\uDDE7"
|
|
||||||
}
|
|
||||||
else if (lang == "vf"){
|
|
||||||
" \uD83C\uDDE8\uD83C\uDDF5"
|
|
||||||
} else {
|
|
||||||
""
|
|
||||||
}
|
|
||||||
|
|
||||||
val cleaned_url = if (lang == "ht") { // if url doesn't contain a flag and the url starts with http://
|
|
||||||
url
|
|
||||||
} else {
|
|
||||||
url.substring(2, url.length)
|
|
||||||
}
|
|
||||||
//println(lang)
|
|
||||||
//println(cleaned_url)
|
|
||||||
|
|
||||||
val id = cleaned_url.replace("https://evoload.io/e/", "") // wanted media id
|
|
||||||
val csrv_token = app.get("https://csrv.evosrv.com/captcha?m412548=").text // whatever that is
|
val csrv_token = app.get("https://csrv.evosrv.com/captcha?m412548=").text // whatever that is
|
||||||
val captchaPass = app.get("https://cd2.evosrv.com/html/jsx/e.jsx").text.take(300).split("captcha_pass = '")[1].split("\'")[0] //extract the captcha pass from the js response (located in the 300 first chars)
|
val captchaPass = app.get("https://cd2.evosrv.com/html/jsx/e.jsx").text.take(300).split("captcha_pass = '")[1].split("\'")[0] //extract the captcha pass from the js response (located in the 300 first chars)
|
||||||
val payload = mapOf("code" to id, "csrv_token" to csrv_token, "pass" to captchaPass)
|
val payload = mapOf("code" to id, "csrv_token" to csrv_token, "pass" to captchaPass)
|
||||||
|
@ -44,9 +25,9 @@ open class Evoload : ExtractorApi() {
|
||||||
return listOf(
|
return listOf(
|
||||||
ExtractorLink(
|
ExtractorLink(
|
||||||
name,
|
name,
|
||||||
name + flag,
|
name,
|
||||||
link,
|
link,
|
||||||
cleaned_url,
|
url,
|
||||||
Qualities.Unknown.value,
|
Qualities.Unknown.value,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,38 +1,83 @@
|
||||||
package com.lagradost.cloudstream3.extractors
|
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.app
|
||||||
import com.lagradost.cloudstream3.utils.*
|
import com.lagradost.cloudstream3.utils.*
|
||||||
import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson
|
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
|
||||||
|
override val name = "Zstreamhub"
|
||||||
|
}
|
||||||
|
class FileMoon : Filesim() {
|
||||||
|
override val mainUrl = "https://filemoon.to"
|
||||||
|
override val name = "FileMoon"
|
||||||
|
}
|
||||||
|
|
||||||
|
class FileMoonSx : Filesim() {
|
||||||
|
override val mainUrl = "https://filemoon.sx"
|
||||||
|
override val name = "FileMoonSx"
|
||||||
|
}
|
||||||
|
|
||||||
open class Filesim : ExtractorApi() {
|
open class Filesim : ExtractorApi() {
|
||||||
override val name = "Filesim"
|
override val name = "Filesim"
|
||||||
override val mainUrl = "https://files.im"
|
override val mainUrl = "https://files.im"
|
||||||
override val requiresReferer = false
|
override val requiresReferer = true
|
||||||
|
|
||||||
override suspend fun getUrl(url: String, referer: String?): List<ExtractorLink> {
|
override suspend fun getUrl(
|
||||||
val sources = mutableListOf<ExtractorLink>()
|
url: String,
|
||||||
with(app.get(url).document) {
|
referer: String?,
|
||||||
this.select("script").map { script ->
|
subtitleCallback: (SubtitleFile) -> Unit,
|
||||||
if (script.data().contains("eval(function(p,a,c,k,e,d)")) {
|
callback: (ExtractorLink) -> Unit
|
||||||
val data = getAndUnpack(script.data()).substringAfter("sources:[").substringBefore("]")
|
) {
|
||||||
tryParseJson<List<ResponseSource>>("[$data]")?.map {
|
val response = app.get(url, referer = referer)
|
||||||
M3u8Helper.generateM3u8(
|
val script = if (!getPacked(response.text).isNullOrEmpty()) {
|
||||||
name,
|
getAndUnpack(response.text)
|
||||||
it.file,
|
} else {
|
||||||
"$mainUrl/",
|
response.document.selectFirst("script:containsData(sources:)")?.data()
|
||||||
).forEach { m3uData -> sources.add(m3uData) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return sources
|
val m3u8 =
|
||||||
|
Regex("file:\\s*\"(.*?m3u8.*?)\"").find(script ?: return)?.groupValues?.getOrNull(1)
|
||||||
|
generateM3u8(
|
||||||
|
name,
|
||||||
|
m3u8 ?: return,
|
||||||
|
mainUrl
|
||||||
|
).forEach(callback)
|
||||||
}
|
}
|
||||||
|
|
||||||
private data class ResponseSource(
|
|
||||||
@JsonProperty("file") val file: String,
|
|
||||||
@JsonProperty("type") val type: String?,
|
|
||||||
@JsonProperty("label") val label: String?
|
|
||||||
)
|
|
||||||
|
|
||||||
}
|
}
|
|
@ -2,14 +2,10 @@ package com.lagradost.cloudstream3.extractors
|
||||||
|
|
||||||
import com.fasterxml.jackson.annotation.JsonProperty
|
import com.fasterxml.jackson.annotation.JsonProperty
|
||||||
import com.lagradost.cloudstream3.*
|
import com.lagradost.cloudstream3.*
|
||||||
|
import com.lagradost.cloudstream3.extractors.helper.AesHelper.cryptoAESHandler
|
||||||
import com.lagradost.cloudstream3.utils.*
|
import com.lagradost.cloudstream3.utils.*
|
||||||
import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson
|
import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson
|
||||||
import org.jsoup.nodes.Element
|
import org.jsoup.nodes.Element
|
||||||
import java.security.DigestException
|
|
||||||
import java.security.MessageDigest
|
|
||||||
import javax.crypto.Cipher
|
|
||||||
import javax.crypto.spec.IvParameterSpec
|
|
||||||
import javax.crypto.spec.SecretKeySpec
|
|
||||||
|
|
||||||
class DatabaseGdrive2 : Gdriveplayer() {
|
class DatabaseGdrive2 : Gdriveplayer() {
|
||||||
override var mainUrl = "https://databasegdriveplayer.co"
|
override var mainUrl = "https://databasegdriveplayer.co"
|
||||||
|
@ -65,78 +61,6 @@ open class Gdriveplayer : ExtractorApi() {
|
||||||
?.data()?.let { getAndUnpack(it) }
|
?.data()?.let { getAndUnpack(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun String.decodeHex(): ByteArray {
|
|
||||||
check(length % 2 == 0) { "Must have an even length" }
|
|
||||||
return chunked(2)
|
|
||||||
.map { it.toInt(16).toByte() }
|
|
||||||
.toByteArray()
|
|
||||||
}
|
|
||||||
|
|
||||||
// https://stackoverflow.com/a/41434590/8166854
|
|
||||||
private fun GenerateKeyAndIv(
|
|
||||||
password: ByteArray,
|
|
||||||
salt: ByteArray,
|
|
||||||
hashAlgorithm: String = "MD5",
|
|
||||||
keyLength: Int = 32,
|
|
||||||
ivLength: Int = 16,
|
|
||||||
iterations: Int = 1
|
|
||||||
): List<ByteArray>? {
|
|
||||||
|
|
||||||
val md = MessageDigest.getInstance(hashAlgorithm)
|
|
||||||
val digestLength = md.digestLength
|
|
||||||
val targetKeySize = keyLength + ivLength
|
|
||||||
val requiredLength = (targetKeySize + digestLength - 1) / digestLength * digestLength
|
|
||||||
val generatedData = ByteArray(requiredLength)
|
|
||||||
var generatedLength = 0
|
|
||||||
|
|
||||||
try {
|
|
||||||
md.reset()
|
|
||||||
|
|
||||||
while (generatedLength < targetKeySize) {
|
|
||||||
if (generatedLength > 0)
|
|
||||||
md.update(
|
|
||||||
generatedData,
|
|
||||||
generatedLength - digestLength,
|
|
||||||
digestLength
|
|
||||||
)
|
|
||||||
|
|
||||||
md.update(password)
|
|
||||||
md.update(salt, 0, 8)
|
|
||||||
md.digest(generatedData, generatedLength, digestLength)
|
|
||||||
|
|
||||||
for (i in 1 until iterations) {
|
|
||||||
md.update(generatedData, generatedLength, digestLength)
|
|
||||||
md.digest(generatedData, generatedLength, digestLength)
|
|
||||||
}
|
|
||||||
|
|
||||||
generatedLength += digestLength
|
|
||||||
}
|
|
||||||
return listOf(
|
|
||||||
generatedData.copyOfRange(0, keyLength),
|
|
||||||
generatedData.copyOfRange(keyLength, targetKeySize)
|
|
||||||
)
|
|
||||||
} catch (e: DigestException) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun cryptoAESHandler(
|
|
||||||
data: AesData,
|
|
||||||
pass: ByteArray,
|
|
||||||
encrypt: Boolean = true
|
|
||||||
): String? {
|
|
||||||
val (key, iv) = GenerateKeyAndIv(pass, data.s.decodeHex()) ?: return null
|
|
||||||
val cipher = Cipher.getInstance("AES/CBC/NoPadding")
|
|
||||||
return if (!encrypt) {
|
|
||||||
cipher.init(Cipher.DECRYPT_MODE, SecretKeySpec(key, "AES"), IvParameterSpec(iv))
|
|
||||||
String(cipher.doFinal(base64DecodeArray(data.ct)))
|
|
||||||
} else {
|
|
||||||
cipher.init(Cipher.DECRYPT_MODE, SecretKeySpec(key, "AES"), IvParameterSpec(iv))
|
|
||||||
base64Encode(cipher.doFinal(data.ct.toByteArray()))
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun Regex.first(str: String): String? {
|
private fun Regex.first(str: String): String? {
|
||||||
return find(str)?.groupValues?.getOrNull(1)
|
return find(str)?.groupValues?.getOrNull(1)
|
||||||
}
|
}
|
||||||
|
@ -154,14 +78,14 @@ open class Gdriveplayer : ExtractorApi() {
|
||||||
val document = app.get(url).document
|
val document = app.get(url).document
|
||||||
|
|
||||||
val eval = unpackJs(document)?.replace("\\", "") ?: return
|
val eval = unpackJs(document)?.replace("\\", "") ?: return
|
||||||
val data = tryParseJson<AesData>(Regex("data='(\\S+?)'").first(eval)) ?: return
|
val data = Regex("data='(\\S+?)'").first(eval) ?: return
|
||||||
val password = Regex("null,['|\"](\\w+)['|\"]").first(eval)
|
val password = Regex("null,['|\"](\\w+)['|\"]").first(eval)
|
||||||
?.split(Regex("\\D+"))
|
?.split(Regex("\\D+"))
|
||||||
?.joinToString("") {
|
?.joinToString("") {
|
||||||
Char(it.toInt()).toString()
|
Char(it.toInt()).toString()
|
||||||
}.let { Regex("var pass = \"(\\S+?)\"").first(it ?: return)?.toByteArray() }
|
}.let { Regex("var pass = \"(\\S+?)\"").first(it ?: return)?.toByteArray() }
|
||||||
?: throw ErrorLoadingException("can't find password")
|
?: throw ErrorLoadingException("can't find password")
|
||||||
val decryptedData = cryptoAESHandler(data, password, false)?.let { getAndUnpack(it) }?.replace("\\", "")
|
val decryptedData = cryptoAESHandler(data, password, false, "AES/CBC/NoPadding")?.let { getAndUnpack(it) }?.replace("\\", "")
|
||||||
|
|
||||||
val sourceData = decryptedData?.substringAfter("sources:[")?.substringBefore("],")
|
val sourceData = decryptedData?.substringAfter("sources:[")?.substringBefore("],")
|
||||||
val subData = decryptedData?.substringAfter("tracks:[")?.substringBefore("],")
|
val subData = decryptedData?.substringAfter("tracks:[")?.substringBefore("],")
|
||||||
|
@ -194,12 +118,6 @@ open class Gdriveplayer : ExtractorApi() {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
data class AesData(
|
|
||||||
@JsonProperty("ct") val ct: String,
|
|
||||||
@JsonProperty("iv") val iv: String,
|
|
||||||
@JsonProperty("s") val s: String
|
|
||||||
)
|
|
||||||
|
|
||||||
data class Tracks(
|
data class Tracks(
|
||||||
@JsonProperty("file") val file: String,
|
@JsonProperty("file") val file: String,
|
||||||
@JsonProperty("kind") val kind: String,
|
@JsonProperty("kind") val kind: String,
|
||||||
|
|
|
@ -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,
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
|
@ -6,6 +6,11 @@ import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||||
import com.lagradost.cloudstream3.app
|
import com.lagradost.cloudstream3.app
|
||||||
import com.lagradost.cloudstream3.utils.*
|
import com.lagradost.cloudstream3.utils.*
|
||||||
|
|
||||||
|
class Vanfem : GuardareStream() {
|
||||||
|
override var name = "Vanfem"
|
||||||
|
override var mainUrl = "https://vanfem.com/"
|
||||||
|
}
|
||||||
|
|
||||||
class CineGrabber : GuardareStream() {
|
class CineGrabber : GuardareStream() {
|
||||||
override var name = "CineGrabber"
|
override var name = "CineGrabber"
|
||||||
override var mainUrl = "https://cinegrabber.com"
|
override var mainUrl = "https://cinegrabber.com"
|
||||||
|
@ -53,7 +58,7 @@ open class GuardareStream : ExtractorApi() {
|
||||||
jsonVideoData.data.forEach {
|
jsonVideoData.data.forEach {
|
||||||
callback.invoke(
|
callback.invoke(
|
||||||
ExtractorLink(
|
ExtractorLink(
|
||||||
it.file + ".${it.type}",
|
this.name,
|
||||||
this.name,
|
this.name,
|
||||||
it.file + ".${it.type}",
|
it.file + ".${it.type}",
|
||||||
mainUrl,
|
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,28 @@
|
||||||
|
// ! 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"
|
||||||
|
}
|
||||||
|
|
||||||
|
class FourPichive : ContentX() {
|
||||||
|
override var name = "FourPichive"
|
||||||
|
override var mainUrl = "https://four.pichive.online"
|
||||||
|
}
|
|
@ -7,7 +7,7 @@ import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson
|
||||||
|
|
||||||
class Neonime7n : Hxfile() {
|
class Neonime7n : Hxfile() {
|
||||||
override val name = "Neonime7n"
|
override val name = "Neonime7n"
|
||||||
override val mainUrl = "https://7njctn.neonime.watch"
|
override val mainUrl = "https://neonime.fun"
|
||||||
override val redirect = false
|
override val redirect = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -19,7 +19,7 @@ class Neonime8n : Hxfile() {
|
||||||
|
|
||||||
class KotakAnimeid : Hxfile() {
|
class KotakAnimeid : Hxfile() {
|
||||||
override val name = "KotakAnimeid"
|
override val name = "KotakAnimeid"
|
||||||
override val mainUrl = "https://kotakanimeid.com"
|
override val mainUrl = "https://nontonanimeid.bio"
|
||||||
override val requiresReferer = true
|
override val requiresReferer = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -97,4 +97,4 @@ open class Hxfile : ExtractorApi() {
|
||||||
@JsonProperty("label") val label: String?
|
@JsonProperty("label") val label: String?
|
||||||
)
|
)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,31 +18,38 @@ open class Linkbox : ExtractorApi() {
|
||||||
subtitleCallback: (SubtitleFile) -> Unit,
|
subtitleCallback: (SubtitleFile) -> Unit,
|
||||||
callback: (ExtractorLink) -> Unit
|
callback: (ExtractorLink) -> Unit
|
||||||
) {
|
) {
|
||||||
val id = Regex("""(/file/|id=)(\S+)[&/?]""").find(url)?.groupValues?.get(2)
|
val token = Regex("""(?:/f/|/file/|\?id=)(\w+)""").find(url)?.groupValues?.get(1)
|
||||||
app.get("$mainUrl/api/open/get_url?itemId=$id", referer=url).parsedSafe<Responses>()?.data?.rList?.map { link ->
|
val id = app.get("$mainUrl/api/file/share_out_list/?sortField=utime&sortAsc=0&pageNo=1&pageSize=50&shareToken=$token").parsedSafe<Responses>()?.data?.itemId
|
||||||
callback.invoke(
|
app.get("$mainUrl/api/file/detail?itemId=$id", referer = url)
|
||||||
ExtractorLink(
|
.parsedSafe<Responses>()?.data?.itemInfo?.resolutionList?.map { link ->
|
||||||
name,
|
callback.invoke(
|
||||||
name,
|
ExtractorLink(
|
||||||
link.url,
|
name,
|
||||||
url,
|
name,
|
||||||
getQualityFromName(link.resolution)
|
link.url ?: return@map null,
|
||||||
|
url,
|
||||||
|
getQualityFromName(link.resolution)
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
data class RList(
|
data class Resolutions(
|
||||||
@JsonProperty("url") val url: String,
|
@JsonProperty("url") val url: String? = null,
|
||||||
@JsonProperty("resolution") val resolution: String?,
|
@JsonProperty("resolution") val resolution: String? = null,
|
||||||
|
)
|
||||||
|
|
||||||
|
data class ItemInfo(
|
||||||
|
@JsonProperty("resolutionList") val resolutionList: ArrayList<Resolutions>? = arrayListOf(),
|
||||||
)
|
)
|
||||||
|
|
||||||
data class Data(
|
data class Data(
|
||||||
@JsonProperty("rList") val rList: List<RList>?,
|
@JsonProperty("itemInfo") val itemInfo: ItemInfo? = null,
|
||||||
|
@JsonProperty("itemId") val itemId: String? = null,
|
||||||
)
|
)
|
||||||
|
|
||||||
data class Responses(
|
data class Responses(
|
||||||
@JsonProperty("data") val data: Data?,
|
@JsonProperty("data") val data: Data? = null,
|
||||||
)
|
)
|
||||||
|
|
||||||
}
|
}
|
|
@ -0,0 +1,54 @@
|
||||||
|
// ! Bu araç @keyiflerolsun tarafından | @KekikAkademi için yazılmıştır.
|
||||||
|
|
||||||
|
package com.lagradost.cloudstream3.extractors
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
import com.lagradost.cloudstream3.*
|
||||||
|
import com.lagradost.cloudstream3.utils.*
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty
|
||||||
|
|
||||||
|
open class MailRu : ExtractorApi() {
|
||||||
|
override val name = "MailRu"
|
||||||
|
override val mainUrl = "https://my.mail.ru"
|
||||||
|
override val requiresReferer = false
|
||||||
|
|
||||||
|
override suspend fun getUrl(url: String, referer: String?, subtitleCallback: (SubtitleFile) -> Unit, callback: (ExtractorLink) -> Unit) {
|
||||||
|
val ext_ref = referer ?: ""
|
||||||
|
Log.d("Kekik_${this.name}", "url » ${url}")
|
||||||
|
|
||||||
|
val vid_id = url.substringAfter("video/embed/").trim()
|
||||||
|
val video_req = app.get("${mainUrl}/+/video/meta/${vid_id}", referer=url)
|
||||||
|
val video_key = video_req.cookies["video_key"].toString()
|
||||||
|
Log.d("Kekik_${this.name}", "video_key » ${video_key}")
|
||||||
|
|
||||||
|
val video_data = AppUtils.tryParseJson<MailRuData>(video_req.text) ?: throw ErrorLoadingException("Video not found")
|
||||||
|
|
||||||
|
for (video in video_data.videos) {
|
||||||
|
Log.d("Kekik_${this.name}", "video » ${video}")
|
||||||
|
|
||||||
|
val video_url = if (video.url.startsWith("//")) "https:${video.url}" else video.url
|
||||||
|
|
||||||
|
callback.invoke(
|
||||||
|
ExtractorLink(
|
||||||
|
source = this.name,
|
||||||
|
name = this.name,
|
||||||
|
url = video_url,
|
||||||
|
referer = url,
|
||||||
|
headers = mapOf("Cookie" to "video_key=${video_key}"),
|
||||||
|
quality = getQualityFromName(video.key),
|
||||||
|
isM3u8 = false
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data class MailRuData(
|
||||||
|
@JsonProperty("provider") val provider: String,
|
||||||
|
@JsonProperty("videos") val videos: List<MailRuVideoData>
|
||||||
|
)
|
||||||
|
|
||||||
|
data class MailRuVideoData(
|
||||||
|
@JsonProperty("url") val url: String,
|
||||||
|
@JsonProperty("key") val key: String
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,43 @@
|
||||||
|
package com.lagradost.cloudstream3.extractors
|
||||||
|
|
||||||
|
import com.lagradost.cloudstream3.SubtitleFile
|
||||||
|
import com.lagradost.cloudstream3.app
|
||||||
|
import com.lagradost.cloudstream3.utils.ExtractorApi
|
||||||
|
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||||
|
import com.lagradost.cloudstream3.utils.INFER_TYPE
|
||||||
|
import com.lagradost.cloudstream3.utils.Qualities
|
||||||
|
|
||||||
|
open class Mediafire : ExtractorApi() {
|
||||||
|
override val name = "Mediafire"
|
||||||
|
override val mainUrl = "https://www.mediafire.com"
|
||||||
|
override val requiresReferer = true
|
||||||
|
|
||||||
|
override suspend fun getUrl(
|
||||||
|
url: String,
|
||||||
|
referer: String?,
|
||||||
|
subtitleCallback: (SubtitleFile) -> Unit,
|
||||||
|
callback: (ExtractorLink) -> Unit
|
||||||
|
) {
|
||||||
|
val res = app.get(url, referer = referer).document
|
||||||
|
val title = res.select("div.dl-btn-label").text()
|
||||||
|
val video = res.selectFirst("a#downloadButton")?.attr("href")
|
||||||
|
|
||||||
|
callback.invoke(
|
||||||
|
ExtractorLink(
|
||||||
|
this.name,
|
||||||
|
this.name,
|
||||||
|
video ?: return,
|
||||||
|
"",
|
||||||
|
getQuality(title),
|
||||||
|
INFER_TYPE
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getQuality(str: String?): Int {
|
||||||
|
return Regex("(\\d{3,4})[pP]").find(str ?: "")?.groupValues?.getOrNull(1)?.toIntOrNull()
|
||||||
|
?: Qualities.Unknown.value
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -7,14 +7,12 @@ import com.lagradost.cloudstream3.utils.ExtractorApi
|
||||||
import com.lagradost.cloudstream3.utils.ExtractorLink
|
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||||
import com.lagradost.cloudstream3.utils.M3u8Helper
|
import com.lagradost.cloudstream3.utils.M3u8Helper
|
||||||
|
|
||||||
class SpeedoStream1 : SpeedoStream() {
|
open class Minoplres : ExtractorApi() {
|
||||||
override val mainUrl = "https://speedostream.nl"
|
|
||||||
}
|
|
||||||
|
|
||||||
open class SpeedoStream : ExtractorApi() {
|
override val name = "Minoplres" // formerly SpeedoStream
|
||||||
override val name = "SpeedoStream"
|
|
||||||
override val mainUrl = "https://speedostream.com"
|
|
||||||
override val requiresReferer = true
|
override val requiresReferer = true
|
||||||
|
override val mainUrl = "https://minoplres.xyz" // formerly speedostream.bond
|
||||||
|
private val hostUrl = "https://minoplres.xyz"
|
||||||
|
|
||||||
override suspend fun getUrl(url: String, referer: String?): List<ExtractorLink> {
|
override suspend fun getUrl(url: String, referer: String?): List<ExtractorLink> {
|
||||||
val sources = mutableListOf<ExtractorLink>()
|
val sources = mutableListOf<ExtractorLink>()
|
||||||
|
@ -26,7 +24,7 @@ open class SpeedoStream : ExtractorApi() {
|
||||||
M3u8Helper.generateM3u8(
|
M3u8Helper.generateM3u8(
|
||||||
name,
|
name,
|
||||||
it.file,
|
it.file,
|
||||||
"$mainUrl/",
|
"$hostUrl/",
|
||||||
).forEach { m3uData -> sources.add(m3uData) }
|
).forEach { m3uData -> sources.add(m3uData) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -37,6 +35,4 @@ open class SpeedoStream : ExtractorApi() {
|
||||||
private data class File(
|
private data class File(
|
||||||
@JsonProperty("file") val file: String,
|
@JsonProperty("file") val file: String,
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
|
||||||
}
|
|
|
@ -7,7 +7,7 @@ import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||||
import com.lagradost.cloudstream3.utils.M3u8Helper
|
import com.lagradost.cloudstream3.utils.M3u8Helper
|
||||||
|
|
||||||
class MoviehabNet : Moviehab() {
|
class MoviehabNet : Moviehab() {
|
||||||
override var mainUrl = "https://play.moviehab.net"
|
override var mainUrl = "https://play.moviehab.asia"
|
||||||
}
|
}
|
||||||
|
|
||||||
open class Moviehab : ExtractorApi() {
|
open class Moviehab : ExtractorApi() {
|
||||||
|
@ -41,4 +41,4 @@ open class Moviehab : ExtractorApi() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,24 +10,39 @@ open class Mp4Upload : ExtractorApi() {
|
||||||
override var name = "Mp4Upload"
|
override var name = "Mp4Upload"
|
||||||
override var mainUrl = "https://www.mp4upload.com"
|
override var mainUrl = "https://www.mp4upload.com"
|
||||||
private val srcRegex = Regex("""player\.src\("(.*?)"""")
|
private val srcRegex = Regex("""player\.src\("(.*?)"""")
|
||||||
override val requiresReferer = true
|
private val srcRegex2 = Regex("""player\.src\([\w\W]*src: "(.*?)"""")
|
||||||
|
|
||||||
|
override val requiresReferer = true
|
||||||
|
private val idMatch = Regex("""mp4upload\.com/(embed-|)([A-Za-z0-9]*)""")
|
||||||
override suspend fun getUrl(url: String, referer: String?): List<ExtractorLink>? {
|
override suspend fun getUrl(url: String, referer: String?): List<ExtractorLink>? {
|
||||||
with(app.get(url)) {
|
val realUrl = idMatch.find(url)?.groupValues?.get(2)?.let { id ->
|
||||||
getAndUnpack(this.text).let { unpackedText ->
|
"$mainUrl/embed-$id.html"
|
||||||
val quality = unpackedText.lowercase().substringAfter(" height=").substringBefore(" ").toIntOrNull()
|
} ?: url
|
||||||
srcRegex.find(unpackedText)?.groupValues?.get(1)?.let { link ->
|
val response = app.get(realUrl)
|
||||||
return listOf(
|
val unpackedText = getAndUnpack(response.text)
|
||||||
ExtractorLink(
|
val quality =
|
||||||
name,
|
unpackedText.lowercase().substringAfter(" height=").substringBefore(" ").toIntOrNull()
|
||||||
name,
|
srcRegex.find(unpackedText)?.groupValues?.get(1)?.let { link ->
|
||||||
link,
|
return listOf(
|
||||||
url,
|
ExtractorLink(
|
||||||
quality ?: Qualities.Unknown.value,
|
name,
|
||||||
)
|
name,
|
||||||
)
|
link,
|
||||||
}
|
url,
|
||||||
}
|
quality ?: Qualities.Unknown.value,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
srcRegex2.find(unpackedText)?.groupValues?.get(1)?.let { link ->
|
||||||
|
return listOf(
|
||||||
|
ExtractorLink(
|
||||||
|
name,
|
||||||
|
name,
|
||||||
|
link,
|
||||||
|
url,
|
||||||
|
quality ?: Qualities.Unknown.value,
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,7 +9,7 @@ import java.net.URI
|
||||||
|
|
||||||
open class MultiQuality : ExtractorApi() {
|
open class MultiQuality : ExtractorApi() {
|
||||||
override var name = "MultiQuality"
|
override var name = "MultiQuality"
|
||||||
override var mainUrl = "https://gogo-play.net"
|
override var mainUrl = "https://anihdplay.com"
|
||||||
private val sourceRegex = Regex("""file:\s*['"](.*?)['"],label:\s*['"](.*?)['"]""")
|
private val sourceRegex = Regex("""file:\s*['"](.*?)['"],label:\s*['"](.*?)['"]""")
|
||||||
private val m3u8Regex = Regex(""".*?(\d*).m3u8""")
|
private val m3u8Regex = Regex(""".*?(\d*).m3u8""")
|
||||||
private val urlRegex = Regex("""(.*?)([^/]+$)""")
|
private val urlRegex = Regex("""(.*?)([^/]+$)""")
|
||||||
|
@ -56,4 +56,4 @@ open class MultiQuality : ExtractorApi() {
|
||||||
return extractedLinksList
|
return extractedLinksList
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,61 @@
|
||||||
|
// ! Bu araç @keyiflerolsun tarafından | @KekikAkademi için yazılmıştır.
|
||||||
|
|
||||||
|
package com.lagradost.cloudstream3.extractors
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
import com.lagradost.cloudstream3.*
|
||||||
|
import com.lagradost.cloudstream3.utils.*
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty
|
||||||
|
|
||||||
|
open class Odnoklassniki : ExtractorApi() {
|
||||||
|
override val name = "Odnoklassniki"
|
||||||
|
override val mainUrl = "https://odnoklassniki.ru"
|
||||||
|
override val requiresReferer = false
|
||||||
|
|
||||||
|
override suspend fun getUrl(url: String, referer: String?, subtitleCallback: (SubtitleFile) -> Unit, callback: (ExtractorLink) -> Unit) {
|
||||||
|
val ext_ref = referer ?: ""
|
||||||
|
Log.d("Kekik_${this.name}", "url » ${url}")
|
||||||
|
|
||||||
|
val user_agent = mapOf("User-Agent" to "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Mobile Safari/537.36")
|
||||||
|
|
||||||
|
val video_req = app.get(url, headers=user_agent).text.replace("\\"", "\"").replace("\\\\", "\\")
|
||||||
|
.replace(Regex("\\\\u([0-9A-Fa-f]{4})")) { matchResult ->
|
||||||
|
Integer.parseInt(matchResult.groupValues[1], 16).toChar().toString()
|
||||||
|
}
|
||||||
|
val videos_str = Regex("""\"videos\":(\[[^\]]*\])""").find(video_req)?.groupValues?.get(1) ?: throw ErrorLoadingException("Video not found")
|
||||||
|
val videos = AppUtils.tryParseJson<List<OkRuVideo>>(videos_str) ?: throw ErrorLoadingException("Video not found")
|
||||||
|
|
||||||
|
for (video in videos) {
|
||||||
|
Log.d("Kekik_${this.name}", "video » ${video}")
|
||||||
|
|
||||||
|
val video_url = if (video.url.startsWith("//")) "https:${video.url}" else video.url
|
||||||
|
|
||||||
|
val quality = video.name.uppercase()
|
||||||
|
.replace("MOBILE", "144p")
|
||||||
|
.replace("LOWEST", "240p")
|
||||||
|
.replace("LOW", "360p")
|
||||||
|
.replace("SD", "480p")
|
||||||
|
.replace("HD", "720p")
|
||||||
|
.replace("FULL", "1080p")
|
||||||
|
.replace("QUAD", "1440p")
|
||||||
|
.replace("ULTRA", "4k")
|
||||||
|
|
||||||
|
callback.invoke(
|
||||||
|
ExtractorLink(
|
||||||
|
source = this.name,
|
||||||
|
name = this.name,
|
||||||
|
url = video_url,
|
||||||
|
referer = url,
|
||||||
|
quality = getQualityFromName(quality),
|
||||||
|
headers = user_agent,
|
||||||
|
isM3u8 = false
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data class OkRuVideo(
|
||||||
|
@JsonProperty("name") val name: String,
|
||||||
|
@JsonProperty("url") val url: String,
|
||||||
|
)
|
||||||
|
}
|
|
@ -1,67 +1,13 @@
|
||||||
|
// ! Bu araç @keyiflerolsun tarafından | @KekikAkademi için yazılmıştır.
|
||||||
|
|
||||||
package com.lagradost.cloudstream3.extractors
|
package com.lagradost.cloudstream3.extractors
|
||||||
|
|
||||||
import com.fasterxml.jackson.annotation.JsonProperty
|
class OkRuSSL : Odnoklassniki() {
|
||||||
import com.lagradost.cloudstream3.utils.*
|
override var name = "OkRuSSL"
|
||||||
import com.lagradost.cloudstream3.app
|
|
||||||
import com.lagradost.cloudstream3.utils.AppUtils.parseJson
|
|
||||||
|
|
||||||
data class DataOptionsJson (
|
|
||||||
@JsonProperty("flashvars") var flashvars : Flashvars? = Flashvars(),
|
|
||||||
)
|
|
||||||
data class Flashvars (
|
|
||||||
@JsonProperty("metadata") var metadata : String? = null,
|
|
||||||
@JsonProperty("hlsManifestUrl") var hlsManifestUrl : String? = null, //m3u8
|
|
||||||
)
|
|
||||||
|
|
||||||
data class MetadataOkru (
|
|
||||||
@JsonProperty("videos") var videos: ArrayList<Videos> = arrayListOf(),
|
|
||||||
)
|
|
||||||
|
|
||||||
data class Videos (
|
|
||||||
@JsonProperty("name") var name : String,
|
|
||||||
@JsonProperty("url") var url : String,
|
|
||||||
@JsonProperty("seekSchema") var seekSchema : Int? = null,
|
|
||||||
@JsonProperty("disallowed") var disallowed : Boolean? = null
|
|
||||||
)
|
|
||||||
|
|
||||||
class OkRuHttps: OkRu(){
|
|
||||||
override var mainUrl = "https://ok.ru"
|
override var mainUrl = "https://ok.ru"
|
||||||
}
|
}
|
||||||
|
|
||||||
open class OkRu : ExtractorApi() {
|
class OkRuHTTP : Odnoklassniki() {
|
||||||
override var name = "Okru"
|
override var name = "OkRuHTTP"
|
||||||
override var mainUrl = "http://ok.ru"
|
override var mainUrl = "http://ok.ru"
|
||||||
override val requiresReferer = false
|
}
|
||||||
|
|
||||||
override suspend fun getUrl(url: String, referer: String?): List<ExtractorLink>? {
|
|
||||||
val doc = app.get(url).document
|
|
||||||
val sources = ArrayList<ExtractorLink>()
|
|
||||||
val datajson = doc.select("div[data-options]").attr("data-options")
|
|
||||||
if (datajson.isNotBlank()) {
|
|
||||||
val main = parseJson<DataOptionsJson>(datajson)
|
|
||||||
val metadatajson = parseJson<MetadataOkru>(main.flashvars?.metadata!!)
|
|
||||||
val servers = metadatajson.videos
|
|
||||||
servers.forEach {
|
|
||||||
val quality = it.name.uppercase()
|
|
||||||
.replace("MOBILE","144p")
|
|
||||||
.replace("LOWEST","240p")
|
|
||||||
.replace("LOW","360p")
|
|
||||||
.replace("SD","480p")
|
|
||||||
.replace("HD","720p")
|
|
||||||
.replace("FULL","1080p")
|
|
||||||
.replace("QUAD","1440p")
|
|
||||||
.replace("ULTRA","4k")
|
|
||||||
val extractedurl = it.url.replace("\\\\u0026", "&")
|
|
||||||
sources.add(ExtractorLink(
|
|
||||||
name,
|
|
||||||
name = this.name,
|
|
||||||
extractedurl,
|
|
||||||
url,
|
|
||||||
getQualityFromName(quality),
|
|
||||||
isM3u8 = false
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return sources
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -0,0 +1,89 @@
|
||||||
|
// ! Bu araç @keyiflerolsun tarafından | @KekikAkademi için yazılmıştır.
|
||||||
|
|
||||||
|
package com.lagradost.cloudstream3.extractors
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
import com.lagradost.cloudstream3.*
|
||||||
|
import com.lagradost.cloudstream3.utils.*
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty
|
||||||
|
|
||||||
|
open class PeaceMakerst : ExtractorApi() {
|
||||||
|
override val name = "PeaceMakerst"
|
||||||
|
override val mainUrl = "https://peacemakerst.com"
|
||||||
|
override val requiresReferer = true
|
||||||
|
|
||||||
|
override suspend fun getUrl(url: String, referer: String?, subtitleCallback: (SubtitleFile) -> Unit, callback: (ExtractorLink) -> Unit) {
|
||||||
|
val m3u_link:String?
|
||||||
|
val ext_ref = referer ?: ""
|
||||||
|
val post_url = "${url}?do=getVideo"
|
||||||
|
Log.d("Kekik_${this.name}", "post_url » ${post_url}")
|
||||||
|
|
||||||
|
val response = app.post(
|
||||||
|
post_url,
|
||||||
|
data = mapOf(
|
||||||
|
"hash" to url.substringAfter("video/"),
|
||||||
|
"r" to ext_ref,
|
||||||
|
"s" to ""
|
||||||
|
),
|
||||||
|
referer = ext_ref,
|
||||||
|
headers = mapOf(
|
||||||
|
"Content-Type" to "application/x-www-form-urlencoded; charset=UTF-8",
|
||||||
|
"X-Requested-With" to "XMLHttpRequest"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if (response.text.contains("teve2.com.tr\\/embed\\/")) {
|
||||||
|
val teve2_id = response.text.substringAfter("teve2.com.tr\\/embed\\/").substringBefore("\"")
|
||||||
|
val teve2_response = app.get(
|
||||||
|
"https://www.teve2.com.tr/action/media/${teve2_id}",
|
||||||
|
referer = "https://www.teve2.com.tr/embed/${teve2_id}"
|
||||||
|
).parsedSafe<Teve2ApiResponse>() ?: throw ErrorLoadingException("teve2 response is null")
|
||||||
|
|
||||||
|
m3u_link = teve2_response.media.link.serviceUrl + "//" + teve2_response.media.link.securePath
|
||||||
|
} else {
|
||||||
|
val video_response = response.parsedSafe<PeaceResponse>() ?: throw ErrorLoadingException("peace response is null")
|
||||||
|
val video_sources = video_response.videoSources
|
||||||
|
if (video_sources.isNotEmpty()) {
|
||||||
|
m3u_link = video_sources.lastOrNull()?.file
|
||||||
|
} else {
|
||||||
|
m3u_link = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
callback.invoke(
|
||||||
|
ExtractorLink(
|
||||||
|
source = this.name,
|
||||||
|
name = this.name,
|
||||||
|
url = m3u_link ?: throw ErrorLoadingException("m3u link not found"),
|
||||||
|
referer = ext_ref,
|
||||||
|
quality = Qualities.Unknown.value,
|
||||||
|
type = INFER_TYPE
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
data class PeaceResponse(
|
||||||
|
@JsonProperty("videoImage") val videoImage: String?,
|
||||||
|
@JsonProperty("videoSources") val videoSources: List<VideoSource>,
|
||||||
|
@JsonProperty("sIndex") val sIndex: String,
|
||||||
|
@JsonProperty("sourceList") val sourceList: Map<String, String>
|
||||||
|
)
|
||||||
|
|
||||||
|
data class VideoSource(
|
||||||
|
@JsonProperty("file") val file: String,
|
||||||
|
@JsonProperty("label") val label: String,
|
||||||
|
@JsonProperty("type") val type: String
|
||||||
|
)
|
||||||
|
|
||||||
|
data class Teve2ApiResponse(
|
||||||
|
@JsonProperty("Media") val media: Teve2Media
|
||||||
|
)
|
||||||
|
|
||||||
|
data class Teve2Media(
|
||||||
|
@JsonProperty("Link") val link: Teve2Link
|
||||||
|
)
|
||||||
|
|
||||||
|
data class Teve2Link(
|
||||||
|
@JsonProperty("ServiceUrl") val serviceUrl: String,
|
||||||
|
@JsonProperty("SecurePath") val securePath: String
|
||||||
|
)
|
||||||
|
}
|
|
@ -5,6 +5,7 @@ import com.lagradost.cloudstream3.amap
|
||||||
import com.lagradost.cloudstream3.app
|
import com.lagradost.cloudstream3.app
|
||||||
import com.lagradost.cloudstream3.mvvm.suspendSafeApiCall
|
import com.lagradost.cloudstream3.mvvm.suspendSafeApiCall
|
||||||
import com.lagradost.cloudstream3.utils.ExtractorLink
|
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||||
|
import com.lagradost.cloudstream3.utils.INFER_TYPE
|
||||||
import com.lagradost.cloudstream3.utils.extractorApis
|
import com.lagradost.cloudstream3.utils.extractorApis
|
||||||
import com.lagradost.cloudstream3.utils.getQualityFromName
|
import com.lagradost.cloudstream3.utils.getQualityFromName
|
||||||
import com.lagradost.cloudstream3.utils.loadExtractor
|
import com.lagradost.cloudstream3.utils.loadExtractor
|
||||||
|
@ -66,7 +67,7 @@ open class Pelisplus(val mainUrl: String) {
|
||||||
href,
|
href,
|
||||||
page.url,
|
page.url,
|
||||||
getQualityFromName(qual),
|
getQualityFromName(qual),
|
||||||
element.attr("href").contains(".m3u8")
|
type = INFER_TYPE
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,25 @@
|
||||||
|
// ! Bu araç @keyiflerolsun tarafından | @KekikAkademi için yazılmıştır.
|
||||||
|
|
||||||
|
package com.lagradost.cloudstream3.extractors
|
||||||
|
|
||||||
|
import com.lagradost.cloudstream3.*
|
||||||
|
import com.lagradost.cloudstream3.utils.*
|
||||||
|
|
||||||
|
open class PixelDrain : ExtractorApi() {
|
||||||
|
override val name = "PixelDrain"
|
||||||
|
override val mainUrl = "https://pixeldrain.com"
|
||||||
|
override val requiresReferer = true
|
||||||
|
|
||||||
|
override suspend fun getUrl(url: String, referer: String?, subtitleCallback: (SubtitleFile) -> Unit, callback: (ExtractorLink) -> Unit) {
|
||||||
|
val mId = Regex("/([ul]/[\\da-zA-Z\\-]+)(?:\\?download)?").find(url)?.groupValues?.get(1)?.split("/")
|
||||||
|
callback.invoke(
|
||||||
|
ExtractorLink(
|
||||||
|
this.name,
|
||||||
|
this.name,
|
||||||
|
"$mainUrl/api/file/${mId?.last() ?: return}?download",
|
||||||
|
url,
|
||||||
|
Qualities.Unknown.value,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,199 @@
|
||||||
|
package com.lagradost.cloudstream3.extractors
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty
|
||||||
|
import com.lagradost.cloudstream3.ErrorLoadingException
|
||||||
|
import com.lagradost.cloudstream3.SubtitleFile
|
||||||
|
import com.lagradost.cloudstream3.app
|
||||||
|
import com.lagradost.cloudstream3.base64DecodeArray
|
||||||
|
import com.lagradost.cloudstream3.base64Encode
|
||||||
|
import com.lagradost.cloudstream3.utils.AppUtils
|
||||||
|
import com.lagradost.cloudstream3.utils.AppUtils.parseJson
|
||||||
|
import com.lagradost.cloudstream3.utils.ExtractorApi
|
||||||
|
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||||
|
import com.lagradost.cloudstream3.utils.M3u8Helper
|
||||||
|
import java.nio.charset.StandardCharsets
|
||||||
|
import java.security.MessageDigest
|
||||||
|
import javax.crypto.Cipher
|
||||||
|
import javax.crypto.spec.IvParameterSpec
|
||||||
|
import javax.crypto.spec.SecretKeySpec
|
||||||
|
|
||||||
|
class Megacloud : Rabbitstream() {
|
||||||
|
override val name = "Megacloud"
|
||||||
|
override val mainUrl = "https://megacloud.tv"
|
||||||
|
override val embed = "embed-2/ajax/e-1"
|
||||||
|
private val scriptUrl = "$mainUrl/js/player/a/prod/e1-player.min.js"
|
||||||
|
|
||||||
|
override suspend fun extractRealKey(sources: String): Pair<String, String> {
|
||||||
|
val rawKeys = getKeys()
|
||||||
|
val sourcesArray = sources.toCharArray()
|
||||||
|
|
||||||
|
var extractedKey = ""
|
||||||
|
var currentIndex = 0
|
||||||
|
for (index in rawKeys) {
|
||||||
|
val start = index[0] + currentIndex
|
||||||
|
val end = start + index[1]
|
||||||
|
for (i in start until end) {
|
||||||
|
extractedKey += sourcesArray[i].toString()
|
||||||
|
sourcesArray[i] = ' '
|
||||||
|
}
|
||||||
|
currentIndex += index[1]
|
||||||
|
}
|
||||||
|
|
||||||
|
return extractedKey to sourcesArray.joinToString("").replace(" ", "")
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun getKeys(): List<List<Int>> {
|
||||||
|
val script = app.get(scriptUrl).text
|
||||||
|
fun matchingKey(value: String): String {
|
||||||
|
return Regex(",$value=((?:0x)?([0-9a-fA-F]+))").find(script)?.groupValues?.get(1)
|
||||||
|
?.removePrefix("0x") ?: throw ErrorLoadingException("Failed to match the key")
|
||||||
|
}
|
||||||
|
|
||||||
|
val regex = Regex("case\\s*0x[0-9a-f]+:(?![^;]*=partKey)\\s*\\w+\\s*=\\s*(\\w+)\\s*,\\s*\\w+\\s*=\\s*(\\w+);")
|
||||||
|
val indexPairs = regex.findAll(script).toList().map { match ->
|
||||||
|
val matchKey1 = matchingKey(match.groupValues[1])
|
||||||
|
val matchKey2 = matchingKey(match.groupValues[2])
|
||||||
|
try {
|
||||||
|
listOf(matchKey1.toInt(16), matchKey2.toInt(16))
|
||||||
|
} catch (e: NumberFormatException) {
|
||||||
|
emptyList()
|
||||||
|
}
|
||||||
|
}.filter { it.isNotEmpty() }
|
||||||
|
|
||||||
|
return indexPairs
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
class Dokicloud : Rabbitstream() {
|
||||||
|
override val name = "Dokicloud"
|
||||||
|
override val mainUrl = "https://dokicloud.one"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Code found in https://github.com/eatmynerds/key
|
||||||
|
// special credits to @eatmynerds for providing key
|
||||||
|
open class Rabbitstream : ExtractorApi() {
|
||||||
|
override val name = "Rabbitstream"
|
||||||
|
override val mainUrl = "https://rabbitstream.net"
|
||||||
|
override val requiresReferer = false
|
||||||
|
open val embed = "ajax/embed-4"
|
||||||
|
open val key = "https://raw.githubusercontent.com/eatmynerds/key/e4/key.txt"
|
||||||
|
|
||||||
|
override suspend fun getUrl(
|
||||||
|
url: String,
|
||||||
|
referer: String?,
|
||||||
|
subtitleCallback: (SubtitleFile) -> Unit,
|
||||||
|
callback: (ExtractorLink) -> Unit
|
||||||
|
) {
|
||||||
|
val id = url.substringAfterLast("/").substringBefore("?")
|
||||||
|
|
||||||
|
val response = app.get(
|
||||||
|
"$mainUrl/$embed/getSources?id=$id",
|
||||||
|
referer = mainUrl,
|
||||||
|
headers = mapOf("X-Requested-With" to "XMLHttpRequest")
|
||||||
|
)
|
||||||
|
|
||||||
|
val encryptedMap = response.parsedSafe<SourcesEncrypted>()
|
||||||
|
val sources = encryptedMap?.sources
|
||||||
|
val decryptedSources = if (sources == null || encryptedMap.encrypted == false) {
|
||||||
|
response.parsedSafe()
|
||||||
|
} else {
|
||||||
|
val (key, encData) = extractRealKey(sources)
|
||||||
|
val decrypted = decryptMapped<List<Sources>>(encData, key)
|
||||||
|
SourcesResponses(
|
||||||
|
sources = decrypted,
|
||||||
|
tracks = encryptedMap.tracks
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
decryptedSources?.sources?.map { source ->
|
||||||
|
M3u8Helper.generateM3u8(
|
||||||
|
name,
|
||||||
|
source?.file ?: return@map,
|
||||||
|
"$mainUrl/",
|
||||||
|
).forEach(callback)
|
||||||
|
}
|
||||||
|
|
||||||
|
decryptedSources?.tracks?.map { track ->
|
||||||
|
subtitleCallback.invoke(
|
||||||
|
SubtitleFile(
|
||||||
|
track?.label ?: return@map,
|
||||||
|
track.file ?: return@map
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
open suspend fun extractRealKey(sources: String): Pair<String, String> {
|
||||||
|
val rawKeys = parseJson<List<Int>>(app.get(key).text)
|
||||||
|
val extractedKey = base64Encode(rawKeys.map { it.toByte() }.toByteArray())
|
||||||
|
return extractedKey to sources
|
||||||
|
}
|
||||||
|
|
||||||
|
private inline fun <reified T> decryptMapped(input: String, key: String): T? {
|
||||||
|
val decrypt = decrypt(input, key)
|
||||||
|
return AppUtils.tryParseJson(decrypt)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun decrypt(input: String, key: String): String {
|
||||||
|
return decryptSourceUrl(
|
||||||
|
generateKey(
|
||||||
|
base64DecodeArray(input).copyOfRange(8, 16),
|
||||||
|
key.toByteArray()
|
||||||
|
), input
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun generateKey(salt: ByteArray, secret: ByteArray): ByteArray {
|
||||||
|
var key = md5(secret + salt)
|
||||||
|
var currentKey = key
|
||||||
|
while (currentKey.size < 48) {
|
||||||
|
key = md5(key + secret + salt)
|
||||||
|
currentKey += key
|
||||||
|
}
|
||||||
|
return currentKey
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun md5(input: ByteArray): ByteArray {
|
||||||
|
return MessageDigest.getInstance("MD5").digest(input)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun decryptSourceUrl(decryptionKey: ByteArray, sourceUrl: String): String {
|
||||||
|
val cipherData = base64DecodeArray(sourceUrl)
|
||||||
|
val encrypted = cipherData.copyOfRange(16, cipherData.size)
|
||||||
|
val aesCBC = Cipher.getInstance("AES/CBC/PKCS5Padding")
|
||||||
|
aesCBC.init(
|
||||||
|
Cipher.DECRYPT_MODE,
|
||||||
|
SecretKeySpec(decryptionKey.copyOfRange(0, 32), "AES"),
|
||||||
|
IvParameterSpec(decryptionKey.copyOfRange(32, decryptionKey.size))
|
||||||
|
)
|
||||||
|
val decryptedData = aesCBC?.doFinal(encrypted) ?: throw ErrorLoadingException("Cipher not found")
|
||||||
|
return String(decryptedData, StandardCharsets.UTF_8)
|
||||||
|
}
|
||||||
|
|
||||||
|
data class Tracks(
|
||||||
|
@JsonProperty("file") val file: String? = null,
|
||||||
|
@JsonProperty("label") val label: String? = null,
|
||||||
|
@JsonProperty("kind") val kind: String? = null,
|
||||||
|
)
|
||||||
|
|
||||||
|
data class Sources(
|
||||||
|
@JsonProperty("file") val file: String? = null,
|
||||||
|
@JsonProperty("type") val type: String? = null,
|
||||||
|
@JsonProperty("label") val label: String? = null,
|
||||||
|
)
|
||||||
|
|
||||||
|
data class SourcesResponses(
|
||||||
|
@JsonProperty("sources") val sources: List<Sources?>? = emptyList(),
|
||||||
|
@JsonProperty("tracks") val tracks: List<Tracks?>? = emptyList(),
|
||||||
|
)
|
||||||
|
|
||||||
|
data class SourcesEncrypted(
|
||||||
|
@JsonProperty("sources") val sources: String? = null,
|
||||||
|
@JsonProperty("encrypted") val encrypted: Boolean? = null,
|
||||||
|
@JsonProperty("tracks") val tracks: List<Tracks?>? = emptyList(),
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,50 @@
|
||||||
|
// ! Bu araç @keyiflerolsun tarafından | @KekikAkademi için yazılmıştır.
|
||||||
|
|
||||||
|
package com.lagradost.cloudstream3.extractors
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
import com.lagradost.cloudstream3.*
|
||||||
|
import com.lagradost.cloudstream3.utils.*
|
||||||
|
|
||||||
|
open class RapidVid : ExtractorApi() {
|
||||||
|
override val name = "RapidVid"
|
||||||
|
override val mainUrl = "https://rapidvid.net"
|
||||||
|
override val requiresReferer = true
|
||||||
|
|
||||||
|
override suspend fun getUrl(url: String, referer: String?, subtitleCallback: (SubtitleFile) -> Unit, callback: (ExtractorLink) -> Unit) {
|
||||||
|
val ext_ref = referer ?: ""
|
||||||
|
val video_req = app.get(url, referer=ext_ref).text
|
||||||
|
|
||||||
|
val sub_urls = mutableSetOf<String>()
|
||||||
|
Regex("""captions\",\"file\":\"([^\"]+)\",\"label\":\"([^\"]+)\"""").findAll(video_req).forEach {
|
||||||
|
val (sub_url, sub_lang) = it.destructured
|
||||||
|
|
||||||
|
if (sub_url in sub_urls) { return@forEach }
|
||||||
|
sub_urls.add(sub_url)
|
||||||
|
|
||||||
|
subtitleCallback.invoke(
|
||||||
|
SubtitleFile(
|
||||||
|
lang = sub_lang.replace("\\u0131", "ı").replace("\\u0130", "İ").replace("\\u00fc", "ü").replace("\\u00e7", "ç"),
|
||||||
|
url = fixUrl(sub_url.replace("\\", ""))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val extracted_value = Regex("""file": "(.*)",""").find(video_req)?.groupValues?.get(1) ?: throw ErrorLoadingException("File not found")
|
||||||
|
|
||||||
|
val bytes = extracted_value.split("\\x").filter { it.isNotEmpty() }.map { it.toInt(16).toByte() }.toByteArray()
|
||||||
|
val decoded = String(bytes, Charsets.UTF_8)
|
||||||
|
Log.d("Kekik_${this.name}", "decoded » ${decoded}")
|
||||||
|
|
||||||
|
callback.invoke(
|
||||||
|
ExtractorLink(
|
||||||
|
source = this.name,
|
||||||
|
name = this.name,
|
||||||
|
url = decoded,
|
||||||
|
referer = ext_ref,
|
||||||
|
quality = Qualities.Unknown.value,
|
||||||
|
isM3u8 = true
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,28 @@
|
||||||
|
package com.lagradost.cloudstream3.extractors
|
||||||
|
|
||||||
|
import com.lagradost.cloudstream3.SubtitleFile
|
||||||
|
import com.lagradost.cloudstream3.utils.*
|
||||||
|
import com.lagradost.cloudstream3.app
|
||||||
|
import com.lagradost.cloudstream3.utils.M3u8Helper.Companion.generateM3u8
|
||||||
|
|
||||||
|
open class Sendvid : ExtractorApi() {
|
||||||
|
override var name = "Sendvid"
|
||||||
|
override val mainUrl = "https://sendvid.com"
|
||||||
|
override val requiresReferer = false
|
||||||
|
override suspend fun getUrl(
|
||||||
|
url: String,
|
||||||
|
referer: String?,
|
||||||
|
subtitleCallback: (SubtitleFile) -> Unit,
|
||||||
|
callback: (ExtractorLink) -> Unit
|
||||||
|
) {
|
||||||
|
val doc = app.get(url).document
|
||||||
|
val urlString = doc.select("head meta[property=og:video:secure_url]").attr("content")
|
||||||
|
if (urlString.contains("m3u8")) {
|
||||||
|
generateM3u8(
|
||||||
|
name,
|
||||||
|
urlString,
|
||||||
|
mainUrl,
|
||||||
|
).forEach(callback)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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.ExtractorApi
|
||||||
import com.lagradost.cloudstream3.utils.ExtractorLink
|
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||||
import com.lagradost.cloudstream3.utils.M3u8Helper
|
import com.lagradost.cloudstream3.utils.M3u8Helper
|
||||||
|
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() {
|
class Sbspeed : StreamSB() {
|
||||||
override var name = "Sbspeed"
|
override var name = "Sbspeed"
|
||||||
|
@ -77,24 +122,70 @@ class StreamSB10 : StreamSB() {
|
||||||
override var mainUrl = "https://sbplay2.xyz"
|
override var mainUrl = "https://sbplay2.xyz"
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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
|
class StreamSB11 : StreamSB() {
|
||||||
// The following code is under the Apache License 2.0 https://github.com/jmir1/aniyomi-extensions/blob/master/LICENSE
|
override var mainUrl = "https://sbbrisk.com"
|
||||||
|
}
|
||||||
|
|
||||||
|
class Sblongvu : StreamSB() {
|
||||||
|
override var mainUrl = "https://sblongvu.com"
|
||||||
|
}
|
||||||
|
|
||||||
open class StreamSB : ExtractorApi() {
|
open class StreamSB : ExtractorApi() {
|
||||||
override var name = "StreamSB"
|
override var name = "StreamSB"
|
||||||
override var mainUrl = "https://watchsb.com"
|
override var mainUrl = "https://watchsb.com"
|
||||||
override val requiresReferer = false
|
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 {
|
mapped.streamData.subs?.map {sub ->
|
||||||
val hexChars = CharArray(bytes.size * 2)
|
subtitleCallback.invoke(
|
||||||
for (j in bytes.indices) {
|
SubtitleFile(
|
||||||
val v = bytes[j].toInt() and 0xFF
|
sub.label.toString(),
|
||||||
|
sub.file ?: return@map null,
|
||||||
hexChars[j * 2] = hexArray[v ushr 4]
|
)
|
||||||
hexChars[j * 2 + 1] = hexArray[v and 0x0F]
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 (
|
data class Subs (
|
||||||
|
@ -118,42 +209,4 @@ open class StreamSB : ExtractorApi() {
|
||||||
@JsonProperty("status_code") val statusCode: Int,
|
@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/sources50/" + 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"
|
override var mainUrl = "https://streamtape.net"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class StreamTapeXyz : StreamTape() {
|
||||||
|
override var mainUrl = "https://streamtape.xyz"
|
||||||
|
}
|
||||||
|
|
||||||
class ShaveTape : StreamTape(){
|
class ShaveTape : StreamTape(){
|
||||||
override var mainUrl = "https://shavetape.cash"
|
override var mainUrl = "https://shavetape.cash"
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,34 @@
|
||||||
|
package com.lagradost.cloudstream3.extractors
|
||||||
|
|
||||||
|
import com.lagradost.cloudstream3.app
|
||||||
|
import com.lagradost.cloudstream3.network.WebViewResolver
|
||||||
|
import com.lagradost.cloudstream3.utils.ExtractorApi
|
||||||
|
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||||
|
import com.lagradost.cloudstream3.utils.Qualities
|
||||||
|
|
||||||
|
open class StreamWishExtractor : ExtractorApi() {
|
||||||
|
override var name = "StreamWish"
|
||||||
|
override var mainUrl = "https://streamwish.to"
|
||||||
|
override val requiresReferer = false
|
||||||
|
|
||||||
|
override suspend fun getUrl(url: String, referer: String?): List<ExtractorLink>? {
|
||||||
|
val response = app.get(
|
||||||
|
url, referer = referer ?: "$mainUrl/", interceptor = WebViewResolver(
|
||||||
|
Regex("""master\.m3u8""")
|
||||||
|
)
|
||||||
|
)
|
||||||
|
val sources = mutableListOf<ExtractorLink>()
|
||||||
|
if (response.url.contains("m3u8"))
|
||||||
|
sources.add(
|
||||||
|
ExtractorLink(
|
||||||
|
source = name,
|
||||||
|
name = name,
|
||||||
|
url = response.url,
|
||||||
|
referer = referer ?: "$mainUrl/",
|
||||||
|
quality = Qualities.Unknown.value,
|
||||||
|
isM3u8 = true
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return sources
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,42 @@
|
||||||
|
package com.lagradost.cloudstream3.extractors
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty
|
||||||
|
import com.lagradost.cloudstream3.app
|
||||||
|
import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson
|
||||||
|
import com.lagradost.cloudstream3.utils.ExtractorApi
|
||||||
|
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||||
|
import com.lagradost.cloudstream3.utils.getAndUnpack
|
||||||
|
import com.lagradost.cloudstream3.utils.Qualities
|
||||||
|
import com.lagradost.cloudstream3.utils.M3u8Helper
|
||||||
|
|
||||||
|
open class StreamoUpload : ExtractorApi() {
|
||||||
|
override val name = "StreamoUpload"
|
||||||
|
override val mainUrl = "https://streamoupload.xyz"
|
||||||
|
override val requiresReferer = true
|
||||||
|
|
||||||
|
override suspend fun getUrl(url: String, referer: String?): List<ExtractorLink> {
|
||||||
|
val sources = mutableListOf<ExtractorLink>()
|
||||||
|
val response = app.get(url, referer = referer)
|
||||||
|
val scriptElements = response.document.select("script").map { script ->
|
||||||
|
if (script.data().contains("eval(function(p,a,c,k,e,d)")) {
|
||||||
|
val data = getAndUnpack(script.data())
|
||||||
|
.substringAfter("sources:[")
|
||||||
|
.substringBefore("],")
|
||||||
|
.replace("file", "\"file\"")
|
||||||
|
.trim()
|
||||||
|
tryParseJson<File>(data)?.let {
|
||||||
|
M3u8Helper.generateM3u8(
|
||||||
|
name,
|
||||||
|
it.file,
|
||||||
|
"$mainUrl/",
|
||||||
|
).forEach { m3uData -> sources.add(m3uData) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return sources
|
||||||
|
}
|
||||||
|
|
||||||
|
private data class File(
|
||||||
|
@JsonProperty("file") val file: String,
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,73 @@
|
||||||
|
// ! Bu araç @keyiflerolsun tarafından | @KekikAkademi için yazılmıştır.
|
||||||
|
|
||||||
|
package com.lagradost.cloudstream3.extractors
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
import com.lagradost.cloudstream3.*
|
||||||
|
import com.lagradost.cloudstream3.utils.*
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty
|
||||||
|
|
||||||
|
open class TRsTX : ExtractorApi() {
|
||||||
|
override val name = "TRsTX"
|
||||||
|
override val mainUrl = "https://trstx.org"
|
||||||
|
override val requiresReferer = true
|
||||||
|
|
||||||
|
override suspend fun getUrl(url: String, referer: String?, subtitleCallback: (SubtitleFile) -> Unit, callback: (ExtractorLink) -> Unit) {
|
||||||
|
val ext_ref = referer ?: ""
|
||||||
|
|
||||||
|
val video_req = app.get(url, referer=ext_ref).text
|
||||||
|
|
||||||
|
val file = Regex("""file\":\"([^\"]+)""").find(video_req)?.groupValues?.get(1) ?: throw ErrorLoadingException("File not found")
|
||||||
|
val postLink = "${mainUrl}/" + file.replace("\\", "")
|
||||||
|
val rawList = app.post(postLink, referer=ext_ref).parsedSafe<List<Any>>() ?: throw ErrorLoadingException("Post link not found")
|
||||||
|
|
||||||
|
val postJson: List<TrstxVideoData> = rawList.drop(1).map { item ->
|
||||||
|
val mapItem = item as Map<*, *>
|
||||||
|
TrstxVideoData(
|
||||||
|
title = mapItem["title"] as? String,
|
||||||
|
file = mapItem["file"] as? String
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Log.d("Kekik_${this.name}", "postJson » ${postJson}")
|
||||||
|
|
||||||
|
val vid_links = mutableSetOf<String>()
|
||||||
|
val vid_map = mutableListOf<Map<String, String>>()
|
||||||
|
for (item in postJson) {
|
||||||
|
if (item.file == null || item.title == null) continue
|
||||||
|
|
||||||
|
val fileUrl = "${mainUrl}/playlist/" + item.file.substring(1) + ".txt"
|
||||||
|
val videoData = app.post(fileUrl, referer=ext_ref).text
|
||||||
|
|
||||||
|
if (videoData in vid_links) { continue }
|
||||||
|
vid_links.add(videoData)
|
||||||
|
|
||||||
|
vid_map.add(mapOf(
|
||||||
|
"title" to item.title,
|
||||||
|
"videoData" to videoData
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
for (mapEntry in vid_map) {
|
||||||
|
Log.d("Kekik_${this.name}", "mapEntry » ${mapEntry}")
|
||||||
|
val title = mapEntry["title"] ?: continue
|
||||||
|
val m3u_link = mapEntry["videoData"] ?: continue
|
||||||
|
|
||||||
|
callback.invoke(
|
||||||
|
ExtractorLink(
|
||||||
|
source = this.name,
|
||||||
|
name = "${this.name} - ${title}",
|
||||||
|
url = m3u_link,
|
||||||
|
referer = ext_ref,
|
||||||
|
quality = Qualities.Unknown.value,
|
||||||
|
type = INFER_TYPE
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data class TrstxVideoData(
|
||||||
|
@JsonProperty("title") val title: String? = null,
|
||||||
|
@JsonProperty("file") val file: String? = null
|
||||||
|
)
|
||||||
|
}
|
|
@ -30,7 +30,7 @@ open class Tantifilm : ExtractorApi() {
|
||||||
val jsonvideodata = parseJson<TantifilmJsonData>(response)
|
val jsonvideodata = parseJson<TantifilmJsonData>(response)
|
||||||
return jsonvideodata.data.map {
|
return jsonvideodata.data.map {
|
||||||
ExtractorLink(
|
ExtractorLink(
|
||||||
it.file+".${it.type}",
|
this.name,
|
||||||
this.name,
|
this.name,
|
||||||
it.file+".${it.type}",
|
it.file+".${it.type}",
|
||||||
mainUrl,
|
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,
|
||||||
|
)
|
||||||
|
}
|
|
@ -7,6 +7,10 @@ class Uqload1 : Uqload() {
|
||||||
override var mainUrl = "https://uqload.com"
|
override var mainUrl = "https://uqload.com"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class Uqload2 : Uqload() {
|
||||||
|
override var mainUrl = "https://uqload.co"
|
||||||
|
}
|
||||||
|
|
||||||
open class Uqload : ExtractorApi() {
|
open class Uqload : ExtractorApi() {
|
||||||
override val name: String = "Uqload"
|
override val name: String = "Uqload"
|
||||||
override val mainUrl: String = "https://www.uqload.com"
|
override val mainUrl: String = "https://www.uqload.com"
|
||||||
|
@ -15,30 +19,14 @@ open class Uqload : ExtractorApi() {
|
||||||
|
|
||||||
|
|
||||||
override suspend fun getUrl(url: String, referer: String?): List<ExtractorLink>? {
|
override suspend fun getUrl(url: String, referer: String?): List<ExtractorLink>? {
|
||||||
val lang = url.substring(0, 2)
|
with(app.get(url)) { // raised error ERROR_CODE_PARSING_CONTAINER_UNSUPPORTED (3003) is due to the response: "error_nofile"
|
||||||
val flag =
|
|
||||||
if (lang == "vo") {
|
|
||||||
" \uD83C\uDDEC\uD83C\uDDE7"
|
|
||||||
}
|
|
||||||
else if (lang == "vf"){
|
|
||||||
" \uD83C\uDDE8\uD83C\uDDF5"
|
|
||||||
} else {
|
|
||||||
""
|
|
||||||
}
|
|
||||||
|
|
||||||
val cleaned_url = if (lang == "ht") { // if url doesn't contain a flag and the url starts with http://
|
|
||||||
url
|
|
||||||
} else {
|
|
||||||
url.substring(2, url.length)
|
|
||||||
}
|
|
||||||
with(app.get(cleaned_url)) { // raised error ERROR_CODE_PARSING_CONTAINER_UNSUPPORTED (3003) is due to the response: "error_nofile"
|
|
||||||
srcRegex.find(this.text)?.groupValues?.get(1)?.replace("\"", "")?.let { link ->
|
srcRegex.find(this.text)?.groupValues?.get(1)?.replace("\"", "")?.let { link ->
|
||||||
return listOf(
|
return listOf(
|
||||||
ExtractorLink(
|
ExtractorLink(
|
||||||
name,
|
name,
|
||||||
name + flag,
|
name,
|
||||||
link,
|
link,
|
||||||
cleaned_url,
|
url,
|
||||||
Qualities.Unknown.value,
|
Qualities.Unknown.value,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
|
@ -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
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -59,8 +59,8 @@ open class VidSrcExtractor : ExtractorApi() {
|
||||||
if (datahash.isNotBlank()) {
|
if (datahash.isNotBlank()) {
|
||||||
val links = try {
|
val links = try {
|
||||||
app.get(
|
app.get(
|
||||||
"$absoluteUrl/src/$datahash",
|
"$absoluteUrl/srcrcp/$datahash",
|
||||||
referer = "https://source.vidsrc.me/"
|
referer = "https://rcp.vidsrc.me/"
|
||||||
).url
|
).url
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
""
|
""
|
||||||
|
@ -71,7 +71,7 @@ open class VidSrcExtractor : ExtractorApi() {
|
||||||
|
|
||||||
serverslist.amap { server ->
|
serverslist.amap { server ->
|
||||||
val linkfixed = server.replace("https://vidsrc.xyz/", "https://embedsito.com/")
|
val linkfixed = server.replace("https://vidsrc.xyz/", "https://embedsito.com/")
|
||||||
if (linkfixed.contains("/pro")) {
|
if (linkfixed.contains("/prorcp")) {
|
||||||
val srcresponse = app.get(server, referer = absoluteUrl).text
|
val srcresponse = app.get(server, referer = absoluteUrl).text
|
||||||
val m3u8Regex = Regex("((https:|http:)//.*\\.m3u8)")
|
val m3u8Regex = Regex("((https:|http:)//.*\\.m3u8)")
|
||||||
val srcm3u8 = m3u8Regex.find(srcresponse)?.value ?: return@amap
|
val srcm3u8 = m3u8Regex.find(srcresponse)?.value ?: return@amap
|
||||||
|
|
|
@ -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,
|
subtitleCallback: (SubtitleFile) -> Unit,
|
||||||
callback: (ExtractorLink) -> 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(
|
val script = app.get(
|
||||||
url,
|
url,
|
||||||
|
headers = headers,
|
||||||
referer = referer,
|
referer = referer,
|
||||||
).document.select("script")
|
).document.select("script")
|
||||||
.find { it.data().contains("sources:") }?.data()
|
.find { it.data().contains("sources:") }?.data()
|
||||||
|
@ -66,4 +70,4 @@ open class Vidmoly : ExtractorApi() {
|
||||||
@JsonProperty("kind") val kind: String? = null,
|
@JsonProperty("kind") val kind: String? = null,
|
||||||
)
|
)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,34 @@
|
||||||
|
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
|
||||||
|
import com.lagradost.cloudstream3.utils.getAndUnpack
|
||||||
|
|
||||||
|
class Vido : ExtractorApi() {
|
||||||
|
override var name = "Vido"
|
||||||
|
override var mainUrl = "https://vido.lol"
|
||||||
|
private val srcRegex = Regex("""sources:\s*\["(.*?)"\]""")
|
||||||
|
override val requiresReferer = true
|
||||||
|
|
||||||
|
override suspend fun getUrl(url: String, referer: String?): List<ExtractorLink>? {
|
||||||
|
val methode = app.get(url.replace("/e/", "/embed-")) // fix wiflix and mesfilms
|
||||||
|
with(methode) {
|
||||||
|
if (!methode.isSuccessful) return null
|
||||||
|
//val quality = unpackedText.lowercase().substringAfter(" height=").substringBefore(" ").toIntOrNull()
|
||||||
|
srcRegex.find(this.text)?.groupValues?.get(1)?.let { link ->
|
||||||
|
return listOf(
|
||||||
|
ExtractorLink(
|
||||||
|
name,
|
||||||
|
name,
|
||||||
|
link,
|
||||||
|
url,
|
||||||
|
Qualities.Unknown.value,
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 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.app
|
||||||
import com.lagradost.cloudstream3.argamap
|
import com.lagradost.cloudstream3.argamap
|
||||||
import com.lagradost.cloudstream3.utils.ExtractorLink
|
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||||
|
import com.lagradost.cloudstream3.utils.INFER_TYPE
|
||||||
import com.lagradost.cloudstream3.utils.extractorApis
|
import com.lagradost.cloudstream3.utils.extractorApis
|
||||||
import com.lagradost.cloudstream3.utils.getQualityFromName
|
import com.lagradost.cloudstream3.utils.getQualityFromName
|
||||||
import com.lagradost.cloudstream3.utils.loadExtractor
|
import com.lagradost.cloudstream3.utils.loadExtractor
|
||||||
|
@ -70,7 +71,7 @@ class Vidstream(val mainUrl: String) {
|
||||||
href,
|
href,
|
||||||
page.url,
|
page.url,
|
||||||
getQualityFromName(qual),
|
getQualityFromName(qual),
|
||||||
element.attr("href").contains(".m3u8")
|
type = INFER_TYPE
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,15 +1,46 @@
|
||||||
package com.lagradost.cloudstream3.extractors
|
package com.lagradost.cloudstream3.extractors
|
||||||
|
|
||||||
|
import android.util.Base64
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty
|
||||||
import com.lagradost.cloudstream3.SubtitleFile
|
import com.lagradost.cloudstream3.SubtitleFile
|
||||||
import com.lagradost.cloudstream3.app
|
import com.lagradost.cloudstream3.app
|
||||||
|
import com.lagradost.cloudstream3.utils.AppUtils
|
||||||
import com.lagradost.cloudstream3.utils.ExtractorApi
|
import com.lagradost.cloudstream3.utils.ExtractorApi
|
||||||
import com.lagradost.cloudstream3.utils.ExtractorLink
|
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||||
import com.lagradost.cloudstream3.utils.M3u8Helper
|
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() {
|
open class Voe : ExtractorApi() {
|
||||||
override val name = "Voe"
|
override val name = "Voe"
|
||||||
override val mainUrl = "https://voe.sx"
|
override val mainUrl = "https://voe.sx"
|
||||||
override val requiresReferer = true
|
override val requiresReferer = true
|
||||||
|
|
||||||
|
private val linkRegex = "(http|https)://([\\w_-]+(?:\\.[\\w_-]+)+)([\\w.,@?^=%&:/~+#-]*[\\w@?^=%&/~+#-])".toRegex()
|
||||||
|
private val base64Regex = Regex("'.*'")
|
||||||
|
|
||||||
override suspend fun getUrl(
|
override suspend fun getUrl(
|
||||||
url: String,
|
url: String,
|
||||||
|
@ -18,15 +49,36 @@ open class Voe : ExtractorApi() {
|
||||||
callback: (ExtractorLink) -> Unit
|
callback: (ExtractorLink) -> Unit
|
||||||
) {
|
) {
|
||||||
val res = app.get(url, referer = referer).document
|
val res = app.get(url, referer = referer).document
|
||||||
val link = res.select("script").find { it.data().contains("const sources") }?.data()
|
val script = res.select("script").find { it.data().contains("sources =") }?.data()
|
||||||
?.substringAfter("\"hls\": \"")?.substringBefore("\",")
|
val link = Regex("[\"']hls[\"']:\\s*[\"'](.*)[\"']").find(script ?: return)?.groupValues?.get(1)
|
||||||
|
|
||||||
M3u8Helper.generateM3u8(
|
|
||||||
name,
|
|
||||||
link ?: return,
|
|
||||||
"$mainUrl/",
|
|
||||||
headers = mapOf("Origin" to "$mainUrl/")
|
|
||||||
).forEach(callback)
|
|
||||||
|
|
||||||
|
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.app
|
||||||
import com.lagradost.cloudstream3.utils.ExtractorApi
|
import com.lagradost.cloudstream3.utils.ExtractorApi
|
||||||
import com.lagradost.cloudstream3.utils.ExtractorLink
|
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||||
|
import com.lagradost.cloudstream3.utils.INFER_TYPE
|
||||||
import com.lagradost.cloudstream3.utils.Qualities
|
import com.lagradost.cloudstream3.utils.Qualities
|
||||||
|
|
||||||
class Vidstreamz : WcoStream() {
|
class Vidstreamz : WcoStream() {
|
||||||
|
@ -126,8 +127,7 @@ open class WcoStream : ExtractorApi() {
|
||||||
|
|
||||||
if (!response.text.startsWith("{")) throw ErrorLoadingException("Seems like 9Anime kiddies changed stuff again, Go touch some grass for bout an hour Or use a different Server")
|
if (!response.text.startsWith("{")) throw ErrorLoadingException("Seems like 9Anime kiddies changed stuff again, Go touch some grass for bout an hour Or use a different Server")
|
||||||
return response.parsed<Response>().data.media.sources.map {
|
return response.parsed<Response>().data.media.sources.map {
|
||||||
ExtractorLink(name, it.file,it.file,host,Qualities.Unknown.value,it.file.contains(".m3u8"))
|
ExtractorLink(name, it.file, it.file, host, Qualities.Unknown.value, type = INFER_TYPE)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,35 @@
|
||||||
|
package com.lagradost.cloudstream3.extractors
|
||||||
|
|
||||||
|
import com.lagradost.cloudstream3.SubtitleFile
|
||||||
|
import com.lagradost.cloudstream3.app
|
||||||
|
import com.lagradost.cloudstream3.utils.ExtractorApi
|
||||||
|
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||||
|
import com.lagradost.cloudstream3.utils.INFER_TYPE
|
||||||
|
import com.lagradost.cloudstream3.utils.Qualities
|
||||||
|
|
||||||
|
open class Wibufile : ExtractorApi() {
|
||||||
|
override val name: String = "Wibufile"
|
||||||
|
override val mainUrl: String = "https://wibufile.com"
|
||||||
|
override val requiresReferer = false
|
||||||
|
|
||||||
|
override suspend fun getUrl(
|
||||||
|
url: String,
|
||||||
|
referer: String?,
|
||||||
|
subtitleCallback: (SubtitleFile) -> Unit,
|
||||||
|
callback: (ExtractorLink) -> Unit
|
||||||
|
) {
|
||||||
|
val res = app.get(url).text
|
||||||
|
val video = Regex("src: ['\"](.*?)['\"]").find(res)?.groupValues?.get(1)
|
||||||
|
|
||||||
|
callback.invoke(
|
||||||
|
ExtractorLink(
|
||||||
|
name,
|
||||||
|
name,
|
||||||
|
video ?: return,
|
||||||
|
"$mainUrl/",
|
||||||
|
Qualities.Unknown.value,
|
||||||
|
type = INFER_TYPE
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -8,6 +8,16 @@ import com.lagradost.cloudstream3.utils.ExtractorApi
|
||||||
import com.lagradost.cloudstream3.utils.ExtractorLink
|
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||||
import com.lagradost.cloudstream3.utils.getQualityFromName
|
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() {
|
class Cdnplayer: XStreamCdn() {
|
||||||
override val name: String = "Cdnplayer"
|
override val name: String = "Cdnplayer"
|
||||||
override val mainUrl: String = "https://cdnplayer.online"
|
override val mainUrl: String = "https://cdnplayer.online"
|
||||||
|
|
|
@ -70,19 +70,18 @@ open class YoutubeExtractor : ExtractorApi() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ytVideos[url]?.mapNotNull {
|
ytVideos[url]?.mapNotNull {
|
||||||
if (it.isVideoOnly || it.height <= 0) return@mapNotNull null
|
if (it.isVideoOnly() || it.height <= 0) return@mapNotNull null
|
||||||
|
|
||||||
ExtractorLink(
|
ExtractorLink(
|
||||||
this.name,
|
this.name,
|
||||||
this.name,
|
this.name,
|
||||||
it.url ?: return@mapNotNull null,
|
it.content ?: return@mapNotNull null,
|
||||||
"",
|
"",
|
||||||
it.height
|
it.height
|
||||||
)
|
)
|
||||||
}?.forEach(callback)
|
}?.forEach(callback)
|
||||||
ytVideosSubtitles[url]?.mapNotNull {
|
ytVideosSubtitles[url]?.mapNotNull {
|
||||||
SubtitleFile(it.languageTag ?: return@mapNotNull null, it.url ?: return@mapNotNull null)
|
SubtitleFile(it.languageTag ?: return@mapNotNull null, it.content ?: return@mapNotNull null)
|
||||||
}?.forEach(subtitleCallback)
|
}?.forEach(subtitleCallback)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
|
@ -0,0 +1,100 @@
|
||||||
|
package com.lagradost.cloudstream3.extractors.helper
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty
|
||||||
|
import com.lagradost.cloudstream3.base64DecodeArray
|
||||||
|
import com.lagradost.cloudstream3.base64Encode
|
||||||
|
import com.lagradost.cloudstream3.utils.AppUtils
|
||||||
|
import java.security.DigestException
|
||||||
|
import java.security.MessageDigest
|
||||||
|
import javax.crypto.Cipher
|
||||||
|
import javax.crypto.spec.IvParameterSpec
|
||||||
|
import javax.crypto.spec.SecretKeySpec
|
||||||
|
|
||||||
|
object AesHelper {
|
||||||
|
|
||||||
|
private const val HASH = "AES/CBC/PKCS5PADDING"
|
||||||
|
private const val KDF = "MD5"
|
||||||
|
|
||||||
|
fun cryptoAESHandler(
|
||||||
|
data: String,
|
||||||
|
pass: ByteArray,
|
||||||
|
encrypt: Boolean = true,
|
||||||
|
padding: String = HASH,
|
||||||
|
): String? {
|
||||||
|
val parse = AppUtils.tryParseJson<AesData>(data) ?: return null
|
||||||
|
val (key, iv) = generateKeyAndIv(
|
||||||
|
pass,
|
||||||
|
parse.s.hexToByteArray(),
|
||||||
|
ivLength = parse.iv.length / 2,
|
||||||
|
saltLength = parse.s.length / 2
|
||||||
|
) ?: return null
|
||||||
|
val cipher = Cipher.getInstance(padding)
|
||||||
|
return if (!encrypt) {
|
||||||
|
cipher.init(Cipher.DECRYPT_MODE, SecretKeySpec(key, "AES"), IvParameterSpec(iv))
|
||||||
|
String(cipher.doFinal(base64DecodeArray(parse.ct)))
|
||||||
|
} else {
|
||||||
|
cipher.init(Cipher.ENCRYPT_MODE, SecretKeySpec(key, "AES"), IvParameterSpec(iv))
|
||||||
|
base64Encode(cipher.doFinal(parse.ct.toByteArray()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// https://stackoverflow.com/a/41434590/8166854
|
||||||
|
fun generateKeyAndIv(
|
||||||
|
password: ByteArray,
|
||||||
|
salt: ByteArray,
|
||||||
|
hashAlgorithm: String = KDF,
|
||||||
|
keyLength: Int = 32,
|
||||||
|
ivLength: Int,
|
||||||
|
saltLength: Int,
|
||||||
|
iterations: Int = 1
|
||||||
|
): Pair<ByteArray,ByteArray>? {
|
||||||
|
|
||||||
|
val md = MessageDigest.getInstance(hashAlgorithm)
|
||||||
|
val digestLength = md.digestLength
|
||||||
|
val targetKeySize = keyLength + ivLength
|
||||||
|
val requiredLength = (targetKeySize + digestLength - 1) / digestLength * digestLength
|
||||||
|
val generatedData = ByteArray(requiredLength)
|
||||||
|
var generatedLength = 0
|
||||||
|
|
||||||
|
try {
|
||||||
|
md.reset()
|
||||||
|
|
||||||
|
while (generatedLength < targetKeySize) {
|
||||||
|
if (generatedLength > 0)
|
||||||
|
md.update(
|
||||||
|
generatedData,
|
||||||
|
generatedLength - digestLength,
|
||||||
|
digestLength
|
||||||
|
)
|
||||||
|
|
||||||
|
md.update(password)
|
||||||
|
md.update(salt, 0, saltLength)
|
||||||
|
md.digest(generatedData, generatedLength, digestLength)
|
||||||
|
|
||||||
|
for (i in 1 until iterations) {
|
||||||
|
md.update(generatedData, generatedLength, digestLength)
|
||||||
|
md.digest(generatedData, generatedLength, digestLength)
|
||||||
|
}
|
||||||
|
|
||||||
|
generatedLength += digestLength
|
||||||
|
}
|
||||||
|
return generatedData.copyOfRange(0, keyLength) to generatedData.copyOfRange(keyLength, targetKeySize)
|
||||||
|
} catch (e: DigestException) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun String.hexToByteArray(): ByteArray {
|
||||||
|
check(length % 2 == 0) { "Must have an even length" }
|
||||||
|
return chunked(2)
|
||||||
|
.map { it.toInt(16).toByte() }
|
||||||
|
.toByteArray()
|
||||||
|
}
|
||||||
|
|
||||||
|
private data class AesData(
|
||||||
|
@JsonProperty("ct") val ct: String,
|
||||||
|
@JsonProperty("iv") val iv: String,
|
||||||
|
@JsonProperty("s") val s: String
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
|
@ -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
|
||||||
|
)
|
||||||
|
}
|
|
@ -1,30 +0,0 @@
|
||||||
package com.lagradost.cloudstream3.metaproviders
|
|
||||||
|
|
||||||
import com.lagradost.cloudstream3.ErrorLoadingException
|
|
||||||
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.SyncApis
|
|
||||||
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.aniListApi
|
|
||||||
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.malApi
|
|
||||||
import com.lagradost.cloudstream3.utils.SyncUtil
|
|
||||||
|
|
||||||
object SyncRedirector {
|
|
||||||
val syncApis = SyncApis
|
|
||||||
|
|
||||||
suspend fun redirect(url: String, preferredUrl: String): String {
|
|
||||||
for (api in syncApis) {
|
|
||||||
if (url.contains(api.mainUrl)) {
|
|
||||||
val otherApi = when (api.name) {
|
|
||||||
aniListApi.name -> "anilist"
|
|
||||||
malApi.name -> "myanimelist"
|
|
||||||
else -> return url
|
|
||||||
}
|
|
||||||
|
|
||||||
return SyncUtil.getUrlsFromId(api.getIdFromUrl(url), otherApi).firstOrNull { realUrl ->
|
|
||||||
realUrl.contains(preferredUrl)
|
|
||||||
} ?: run {
|
|
||||||
throw ErrorLoadingException("Page does not exist on $preferredUrl")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return url
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -21,10 +21,11 @@ class CrossTmdbProvider : TmdbProvider() {
|
||||||
return Regex("""[^a-zA-Z0-9-]""").replace(name, "")
|
return Regex("""[^a-zA-Z0-9-]""").replace(name, "")
|
||||||
}
|
}
|
||||||
|
|
||||||
private val validApis by lazy {
|
private val validApis
|
||||||
apis.filter { it.lang == this.lang && it::class.java != this::class.java }
|
get() =
|
||||||
//.distinctBy { it.uniqueId }
|
synchronized(apis) { apis.filter { it.lang == this.lang && it::class.java != this::class.java } }
|
||||||
}
|
//.distinctBy { it.uniqueId }
|
||||||
|
|
||||||
|
|
||||||
data class CrossMetaData(
|
data class CrossMetaData(
|
||||||
@JsonProperty("isSuccess") val isSuccess: Boolean,
|
@JsonProperty("isSuccess") val isSuccess: Boolean,
|
||||||
|
@ -60,7 +61,8 @@ class CrossTmdbProvider : TmdbProvider() {
|
||||||
|
|
||||||
override suspend fun load(url: String): LoadResponse? {
|
override suspend fun load(url: String): LoadResponse? {
|
||||||
val base = super.load(url)?.apply {
|
val base = super.load(url)?.apply {
|
||||||
this.recommendations = this.recommendations?.filterIsInstance<MovieSearchResponse>() // TODO REMOVE
|
this.recommendations =
|
||||||
|
this.recommendations?.filterIsInstance<MovieSearchResponse>() // TODO REMOVE
|
||||||
val matchName = filterName(this.name)
|
val matchName = filterName(this.name)
|
||||||
when (this) {
|
when (this) {
|
||||||
is MovieLoadResponse -> {
|
is MovieLoadResponse -> {
|
||||||
|
@ -98,6 +100,7 @@ class CrossTmdbProvider : TmdbProvider() {
|
||||||
this.dataUrl =
|
this.dataUrl =
|
||||||
CrossMetaData(true, data.map { it.apiName to it.dataUrl }).toJson()
|
CrossMetaData(true, data.map { it.apiName to it.dataUrl }).toJson()
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
throw ErrorLoadingException("Nothing besides movies are implemented for this provider")
|
throw ErrorLoadingException("Nothing besides movies are implemented for this provider")
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,70 +0,0 @@
|
||||||
package com.lagradost.cloudstream3.metaproviders
|
|
||||||
|
|
||||||
import com.lagradost.cloudstream3.*
|
|
||||||
import com.lagradost.cloudstream3.LoadResponse.Companion.addAniListId
|
|
||||||
import com.lagradost.cloudstream3.LoadResponse.Companion.addTrailer
|
|
||||||
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.aniListApi
|
|
||||||
import com.lagradost.cloudstream3.syncproviders.SyncAPI
|
|
||||||
import com.lagradost.cloudstream3.syncproviders.providers.AniListApi
|
|
||||||
import com.lagradost.cloudstream3.syncproviders.providers.MALApi
|
|
||||||
import com.lagradost.cloudstream3.utils.SyncUtil
|
|
||||||
|
|
||||||
// wont be implemented
|
|
||||||
class MultiAnimeProvider : MainAPI() {
|
|
||||||
override var name = "MultiAnime"
|
|
||||||
override var lang = "en"
|
|
||||||
override val usesWebView = true
|
|
||||||
override val supportedTypes = setOf(TvType.Anime)
|
|
||||||
private val syncApi: SyncAPI = aniListApi
|
|
||||||
|
|
||||||
private val syncUtilType by lazy {
|
|
||||||
when (syncApi) {
|
|
||||||
is AniListApi -> "anilist"
|
|
||||||
is MALApi -> "myanimelist"
|
|
||||||
else -> throw ErrorLoadingException("Invalid Api")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private val validApis by lazy {
|
|
||||||
APIHolder.apis.filter {
|
|
||||||
it.lang == this.lang && it::class.java != this::class.java && it.supportedTypes.contains(
|
|
||||||
TvType.Anime
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun filterName(name: String): String {
|
|
||||||
return Regex("""[^a-zA-Z0-9-]""").replace(name, "")
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun search(query: String): List<SearchResponse>? {
|
|
||||||
return syncApi.search(query)?.map {
|
|
||||||
AnimeSearchResponse(it.name, it.url, this.name, TvType.Anime, it.posterUrl)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun load(url: String): LoadResponse? {
|
|
||||||
return syncApi.getResult(url)?.let { res ->
|
|
||||||
val data = SyncUtil.getUrlsFromId(res.id, syncUtilType).amap { url ->
|
|
||||||
validApis.firstOrNull { api -> url.startsWith(api.mainUrl) }?.load(url)
|
|
||||||
}.filterNotNull()
|
|
||||||
|
|
||||||
val type =
|
|
||||||
if (data.any { it.type == TvType.AnimeMovie }) TvType.AnimeMovie else TvType.Anime
|
|
||||||
|
|
||||||
newAnimeLoadResponse(
|
|
||||||
res.title ?: throw ErrorLoadingException("No Title found"),
|
|
||||||
url,
|
|
||||||
type
|
|
||||||
) {
|
|
||||||
posterUrl = res.posterUrl
|
|
||||||
plot = res.synopsis
|
|
||||||
tags = res.genres
|
|
||||||
rating = res.publicScore
|
|
||||||
addTrailer(res.trailers)
|
|
||||||
addAniListId(res.id.toIntOrNull())
|
|
||||||
recommendations = res.recommendations
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,56 @@
|
||||||
|
package com.lagradost.cloudstream3.metaproviders
|
||||||
|
|
||||||
|
import com.lagradost.cloudstream3.MainAPI
|
||||||
|
import com.lagradost.cloudstream3.mvvm.suspendSafeApiCall
|
||||||
|
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.SyncApis
|
||||||
|
import com.lagradost.cloudstream3.syncproviders.SyncIdName
|
||||||
|
|
||||||
|
object SyncRedirector {
|
||||||
|
val syncApis = SyncApis
|
||||||
|
private val syncIds =
|
||||||
|
listOf(
|
||||||
|
SyncIdName.MyAnimeList to Regex("""myanimelist\.net\/anime\/(\d+)"""),
|
||||||
|
SyncIdName.Anilist to Regex("""anilist\.co\/anime\/(\d+)""")
|
||||||
|
)
|
||||||
|
|
||||||
|
suspend fun redirect(
|
||||||
|
url: String,
|
||||||
|
providerApi: MainAPI
|
||||||
|
): String {
|
||||||
|
// Deprecated since providers should do this instead!
|
||||||
|
|
||||||
|
// Tries built in ID -> ProviderUrl
|
||||||
|
/*
|
||||||
|
for (api in syncApis) {
|
||||||
|
if (url.contains(api.mainUrl)) {
|
||||||
|
val otherApi = when (api.name) {
|
||||||
|
aniListApi.name -> "anilist"
|
||||||
|
malApi.name -> "myanimelist"
|
||||||
|
else -> return url
|
||||||
|
}
|
||||||
|
|
||||||
|
SyncUtil.getUrlsFromId(api.getIdFromUrl(url), otherApi).firstOrNull { realUrl ->
|
||||||
|
realUrl.contains(providerApi.mainUrl)
|
||||||
|
}?.let {
|
||||||
|
return it
|
||||||
|
}
|
||||||
|
// ?: run {
|
||||||
|
// throw ErrorLoadingException("Page does not exist on $preferredUrl")
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Tries provider solution
|
||||||
|
// This goes through all sync ids and finds supported id by said provider
|
||||||
|
return syncIds.firstNotNullOfOrNull { (syncName, syncRegex) ->
|
||||||
|
if (providerApi.supportedSyncNames.contains(syncName)) {
|
||||||
|
syncRegex.find(url)?.value?.let {
|
||||||
|
suspendSafeApiCall {
|
||||||
|
providerApi.getLoadUrl(syncName, it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else null
|
||||||
|
} ?: url
|
||||||
|
}
|
||||||
|
}
|
|
@ -105,6 +105,7 @@ open class TmdbProvider : MainAPI() {
|
||||||
this.id,
|
this.id,
|
||||||
episode.episode_number,
|
episode.episode_number,
|
||||||
episode.season_number,
|
episode.season_number,
|
||||||
|
this.name ?: this.original_name,
|
||||||
).toJson(),
|
).toJson(),
|
||||||
episode.name,
|
episode.name,
|
||||||
episode.season_number,
|
episode.season_number,
|
||||||
|
@ -122,6 +123,7 @@ open class TmdbProvider : MainAPI() {
|
||||||
this.id,
|
this.id,
|
||||||
episodeNum,
|
episodeNum,
|
||||||
season.season_number,
|
season.season_number,
|
||||||
|
this.name ?: this.original_name,
|
||||||
).toJson(),
|
).toJson(),
|
||||||
season = season.season_number
|
season = season.season_number
|
||||||
)
|
)
|
||||||
|
@ -151,6 +153,8 @@ open class TmdbProvider : MainAPI() {
|
||||||
recommendations = (this@toLoadResponse.recommendations
|
recommendations = (this@toLoadResponse.recommendations
|
||||||
?: this@toLoadResponse.similar)?.results?.map { it.toSearchResponse() }
|
?: this@toLoadResponse.similar)?.results?.map { it.toSearchResponse() }
|
||||||
addActors(credits?.cast?.toList().toActors())
|
addActors(credits?.cast?.toList().toActors())
|
||||||
|
|
||||||
|
contentRating = fetchContentRating(id, "US")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -193,6 +197,8 @@ open class TmdbProvider : MainAPI() {
|
||||||
recommendations = (this@toLoadResponse.recommendations
|
recommendations = (this@toLoadResponse.recommendations
|
||||||
?: this@toLoadResponse.similar)?.results?.map { it.toSearchResponse() }
|
?: this@toLoadResponse.similar)?.results?.map { it.toSearchResponse() }
|
||||||
addActors(credits?.cast?.toList().toActors())
|
addActors(credits?.cast?.toList().toActors())
|
||||||
|
|
||||||
|
contentRating = fetchContentRating(id, "US")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -264,6 +270,26 @@ open class TmdbProvider : MainAPI() {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
open suspend fun fetchContentRating(id: Int?, country: String): String? {
|
||||||
|
id ?: return null
|
||||||
|
|
||||||
|
val contentRatings = tmdb.tvService().content_ratings(id).awaitResponse().body()?.results
|
||||||
|
return if (!contentRatings.isNullOrEmpty()) {
|
||||||
|
contentRatings.firstOrNull { it: ContentRating ->
|
||||||
|
it.iso_3166_1 == country
|
||||||
|
}?.rating
|
||||||
|
} else {
|
||||||
|
val releaseDates = tmdb.moviesService().releaseDates(id).awaitResponse().body()?.results
|
||||||
|
val certification = releaseDates?.firstOrNull { it: ReleaseDatesResult ->
|
||||||
|
it.iso_3166_1 == country
|
||||||
|
}?.release_dates?.firstOrNull { it: ReleaseDate ->
|
||||||
|
!it.certification.isNullOrBlank()
|
||||||
|
}?.certification
|
||||||
|
|
||||||
|
certification
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Possible to add recommendations and such here.
|
// Possible to add recommendations and such here.
|
||||||
override suspend fun load(url: String): LoadResponse? {
|
override suspend fun load(url: String): LoadResponse? {
|
||||||
// https://www.themoviedb.org/movie/7445-brothers
|
// https://www.themoviedb.org/movie/7445-brothers
|
||||||
|
|
|
@ -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) }
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue