Compare commits
559 commits
screenshot
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dff3d68bac | ||
|
|
8834f32d40 | ||
|
|
57ad661c74 | ||
|
|
cc12b0a9a9 | ||
|
|
f737ea3d5d | ||
|
|
17205d5285 | ||
|
|
5435f605c9 | ||
|
|
faddc125f6 | ||
|
|
e4e490cf63 | ||
|
|
b5807a2928 | ||
|
|
354fd00768 | ||
|
|
7684b0e0ba | ||
|
|
68401c05f1 | ||
|
|
55542460d7 | ||
|
|
602276cd35 | ||
|
|
a03726e0a9 | ||
|
|
61b3c81373 | ||
|
|
e4da9ad453 | ||
|
|
666e58aa4f | ||
|
|
f4ab4a1354 | ||
|
|
c21045ffed | ||
|
|
34d0c6c6a4 | ||
|
|
0d9a818e6d | ||
|
|
e3c5fd0deb | ||
|
|
ba3170bba0 | ||
|
|
67a32248e0 | ||
|
|
6285f9b05a | ||
|
|
7b06e8061e | ||
|
|
90647a7f0a | ||
|
|
9f170b7a76 | ||
|
|
24271a087e | ||
|
|
f3e845b3b7 | ||
|
|
b7ea56d72d | ||
|
|
2801a25ddf | ||
|
|
39dfd2b586 | ||
|
|
814f585057 | ||
|
|
5202a7740e | ||
|
|
6375baee7a | ||
|
|
c8a773c9fd | ||
|
|
1744099791 | ||
|
|
cc90b4105b | ||
|
|
7495be975e | ||
|
|
de9bc757c4 | ||
|
|
8265ec53e3 | ||
|
|
d570df212d | ||
|
|
4b76a30e3f | ||
|
|
791826c3c9 | ||
|
|
dffb779403 | ||
|
|
499197a59d | ||
|
|
e869c3cfee | ||
|
|
cea82dd040 | ||
|
|
970fe5d81c | ||
|
|
3adc787cf6 | ||
|
|
0b89b4a917 | ||
|
|
37ef74bf5d | ||
|
|
ce2cc568a2 | ||
|
|
5d54fe8b25 | ||
|
|
900c891713 | ||
|
|
5fe963dce4 | ||
|
|
76c1a241a0 | ||
|
|
3f361d56ae | ||
|
|
ca6d0d04a1 | ||
|
|
2cae2d260a | ||
|
|
c27b6649b3 | ||
|
|
752056a02e | ||
|
|
458f266f18 | ||
|
|
c7edf0fd01 | ||
|
|
e83cec01e2 | ||
|
|
301d94c269 | ||
|
|
6ae0c65e68 | ||
|
|
3f284a05fc | ||
|
|
3bbb1599ee | ||
|
|
1b69828fb6 | ||
|
|
e2e3b526c3 | ||
|
|
7f2eca5840 | ||
|
|
c910bb9f7c | ||
|
|
c0f96a3e6d | ||
|
|
7bd3c80c60 | ||
|
|
463425746e | ||
|
|
b85ec3aba2 | ||
|
|
d5113bedc5 | ||
|
|
a5122ac96e | ||
|
|
1b84c01390 | ||
|
|
01835f3958 | ||
|
|
48dbe1386a | ||
|
|
9ac4a08379 | ||
|
|
24605be091 | ||
|
|
7ead8db4d8 | ||
|
|
80130a357f | ||
|
|
16bb2126f1 | ||
|
|
979e998f55 | ||
|
|
92f716c547 | ||
|
|
21c0e197d1 | ||
|
|
c2ebf8cce6 | ||
|
|
263fc76e3e | ||
|
|
d2752504e5 | ||
|
|
9ef890773e | ||
|
|
36451c230c | ||
|
|
3e3cdb94c4 | ||
|
|
4bc64789c5 | ||
|
|
59ce118eec | ||
|
|
09e79f6e59 | ||
|
|
40e83490c7 | ||
|
|
4afd9461ad | ||
|
|
3c667e59f5 | ||
|
|
d972abbe87 | ||
|
|
eebb7d72ed | ||
|
|
2ca9be2319 | ||
|
|
decd0b9759 | ||
|
|
c62e132126 | ||
|
|
7eedf0449c | ||
|
|
11b9223b88 | ||
|
|
fd4ffedef3 | ||
|
|
9e370b16aa | ||
|
|
8bde27c857 | ||
|
|
a9a6aed1bb | ||
|
|
71c81f448a | ||
|
|
333d7aef69 | ||
|
|
55d4f1745f | ||
|
|
8671db9ef3 | ||
|
|
e76efcbd7c | ||
|
|
37c3b1b179 | ||
|
|
ea22b7f4cd | ||
|
|
34c818a2cd | ||
|
|
e6d839506f | ||
|
|
94f64a2e79 | ||
|
|
ce8be66d86 | ||
|
|
81517a2bcd | ||
|
|
85881230e3 | ||
|
|
4cc8f54fa4 | ||
|
|
6d971cb4fb | ||
|
|
9cf14d0d46 | ||
|
|
f54f71d3ff | ||
|
|
e59d703484 | ||
|
|
d8f5cb3975 | ||
|
|
86d4b73257 | ||
|
|
8046a81d16 | ||
|
|
64635de462 | ||
|
|
73c6509c4e | ||
|
|
e936b60623 | ||
|
|
ff2a7fa557 | ||
|
|
8eb3ccb793 | ||
|
|
3906233019 | ||
|
|
96f40a2e9c | ||
|
|
6bba7e0d4b | ||
|
|
1664c238ff | ||
|
|
aa0a0563d6 | ||
|
|
76e17aed9d | ||
|
|
e18af3aa79 | ||
|
|
26bb82df8a | ||
|
|
c970fb211e | ||
|
|
fb5640ec3b | ||
|
|
bfdebcfa5e | ||
|
|
d731680b1d | ||
|
|
e523e14ca3 | ||
|
|
8084885244 | ||
|
|
7695344aee | ||
|
|
b69c5dfa5f | ||
|
|
cbdb59f795 | ||
|
|
2b764e40ef | ||
|
|
8b68030674 | ||
|
|
2d6b41c90e | ||
|
|
f44ce1b6f9 | ||
|
|
9a626c5d9f | ||
|
|
d7bce6a7fc | ||
|
|
2403ce71c2 | ||
|
|
68e5849bc9 | ||
|
|
3e6b0a4f16 | ||
|
|
7dd7b2e8ab | ||
|
|
8395d2df9e | ||
|
|
7584c28bb5 | ||
|
|
0ff2f005be | ||
|
|
ebba6a94a5 | ||
|
|
fb18ee975f | ||
|
|
10c9d4b15e | ||
|
|
7bf707d8bc | ||
|
|
2a829ea3fd | ||
|
|
5bd1347feb | ||
|
|
3df56430c1 | ||
|
|
dee597d41d | ||
|
|
c74156e01a | ||
|
|
6232ed7aed | ||
|
|
b021c84478 | ||
|
|
9f1f00dd18 | ||
|
|
567b59f67c | ||
|
|
b05a2bfb4e | ||
|
|
85a3996ff6 | ||
|
|
a37074b6dd | ||
|
|
e3812c168f | ||
|
|
21dba5bf72 | ||
|
|
20d6ba6c19 | ||
|
|
e159fe9b16 | ||
|
|
65a07d6796 | ||
|
|
bab407b3e3 | ||
|
|
423e1da501 | ||
|
|
44c8514b4e | ||
|
|
37e3b206a1 | ||
|
|
a5a48a8760 | ||
|
|
36c1fbbe94 | ||
|
|
5c85c33dae | ||
|
|
bf58a599ec | ||
|
|
54bfec70e1 | ||
|
|
2392a10c5f | ||
|
|
62741a79f2 | ||
|
|
abd7e06eea | ||
|
|
c8d5613288 | ||
|
|
8f121c136e | ||
|
|
95945137da | ||
|
|
0a81a9ab82 | ||
|
|
e89de932e3 | ||
|
|
915d339351 | ||
|
|
8b70217248 | ||
|
|
36d6304837 | ||
|
|
2e14f3ad8e | ||
|
|
966cea943b | ||
|
|
468c9757e4 | ||
|
|
ca0cd15152 | ||
|
|
e6d1dbe356 | ||
|
|
3d4284baad | ||
|
|
a3a4479922 | ||
|
|
6514cdd36e | ||
|
|
67c14d0f95 | ||
|
|
ebcd1065b2 | ||
|
|
f3f8aef7d0 | ||
|
|
66969de042 | ||
|
|
683effe550 | ||
|
|
e5cfb41c40 | ||
|
|
a7faa05038 | ||
|
|
076005c715 | ||
|
|
90c8ce01cb | ||
|
|
f6386c5da8 | ||
|
|
49363694a4 | ||
|
|
8a5ecf0a85 | ||
|
|
9c5127f54e | ||
|
|
fd97370fcc | ||
|
|
37222cbb84 | ||
|
|
07545f8843 | ||
|
|
9c4260cef2 | ||
|
|
5e6f35dd22 | ||
|
|
9942e332a7 | ||
|
|
2c2c04a897 | ||
|
|
c1d2b9f38c | ||
|
|
a51943bac4 | ||
|
|
1b496ac313 | ||
|
|
e0901a7154 | ||
|
|
3c5696e6a5 | ||
|
|
6bf8fa6018 | ||
|
|
86922fff77 | ||
|
|
8784a20560 | ||
|
|
9b9150366f | ||
|
|
9f614fe664 | ||
|
|
2428842da3 | ||
|
|
6f9727883f | ||
|
|
2940e029cd | ||
|
|
6161949090 | ||
|
|
d5e911ad26 | ||
|
|
ac1a8f19cc | ||
|
|
1e01f73fb4 | ||
|
|
2c97ce81cb | ||
|
|
c0edb5c0c5 | ||
|
|
b9224f725a | ||
|
|
baa8ddd6a5 | ||
|
|
c83d3a7472 | ||
|
|
8da19368c7 | ||
|
|
f423f60380 | ||
|
|
6f47acd819 | ||
|
|
8149b4c0b1 | ||
|
|
2d1def7ac9 | ||
|
|
1141e53c0b | ||
|
|
fd074790e8 | ||
|
|
c19638eb6d | ||
|
|
433ab9ceea | ||
|
|
a6eaf30579 | ||
|
|
0c7eccf180 | ||
|
|
c635cffdf2 | ||
|
|
7414887be2 | ||
|
|
749861af61 | ||
|
|
8c6cd36d9c | ||
|
|
20668f2829 | ||
|
|
c7d2cae26b | ||
|
|
9f97699b53 | ||
|
|
7778873a09 | ||
|
|
92529e06a9 | ||
|
|
e2d52be133 | ||
|
|
4e6ac0bdb4 | ||
|
|
ed39ba33f7 | ||
|
|
52c55a8d85 | ||
|
|
8522088783 | ||
|
|
8541a92f83 | ||
|
|
c163b9fed4 | ||
|
|
afd8d6ba31 | ||
|
|
cf661ba909 | ||
|
|
7d1fd73dc4 | ||
|
|
f4bf7c24b0 | ||
|
|
caf6f36eef | ||
|
|
5075d6b75a | ||
|
|
c5e181a82b | ||
|
|
5eb0fbd513 | ||
|
|
16122da36d | ||
|
|
5ee227b6de | ||
|
|
dc979b30d9 | ||
|
|
d173331829 | ||
|
|
53bf4ac3b3 | ||
|
|
a4d2b620a3 | ||
|
|
a5a85aee35 | ||
|
|
828b9b89b0 | ||
|
|
12287770b0 | ||
|
|
88dc90f2d7 | ||
|
|
5a539c5d0b | ||
|
|
cfd05732c6 | ||
|
|
e9865029b4 | ||
|
|
a97e16a072 | ||
|
|
f5eb6611dc | ||
|
|
a82dcee6b2 | ||
|
|
e6f460d86d | ||
|
|
ae46720fbf | ||
|
|
7d18d6c1eb | ||
|
|
81031978a0 | ||
|
|
4388fb03ff | ||
|
|
1b61f97fae | ||
|
|
c1e1da1bf8 | ||
|
|
c88780ccfb | ||
|
|
eb9bb3b8f2 | ||
|
|
d54e6a5794 | ||
|
|
099e52f5b2 | ||
|
|
d7e8142433 | ||
|
|
1e935c6082 | ||
|
|
f862f29c26 | ||
|
|
f775ee2118 | ||
|
|
806b1fed32 | ||
|
|
e80a70f2b0 | ||
|
|
63797dcfcd | ||
|
|
62913d55f1 | ||
|
|
0c6dc2b586 | ||
|
|
528ba54c6f | ||
|
|
c8ca805ecb | ||
|
|
d2ecb573b7 | ||
|
|
d914a25cce | ||
|
|
576a2e5b1e | ||
|
|
8b7039b5e3 | ||
|
|
60f1e30475 | ||
|
|
c877345aac | ||
|
|
86919cdff2 | ||
|
|
9eea568829 | ||
|
|
c03aa7986e | ||
|
|
2e830863ff | ||
|
|
9957b9b213 | ||
|
|
eb222dedb8 | ||
|
|
d551f71bf9 | ||
|
|
7c345c3279 | ||
|
|
1dc88b7d7a | ||
|
|
cfc7da44dd | ||
|
|
ab6b766459 | ||
|
|
8ea195e5f1 | ||
|
|
b1f2dc23aa | ||
|
|
d7d9c859cf | ||
|
|
9b8ba7b7f1 | ||
|
|
1302f70b66 | ||
|
|
96203e8a93 | ||
|
|
924976161c | ||
|
|
d87042a0a0 | ||
|
|
820e089c59 | ||
|
|
4ec2bdbb50 | ||
|
|
806482c5a3 | ||
|
|
906ad5f57e | ||
|
|
43fcb00405 | ||
|
|
4f009be6a4 | ||
|
|
b2ee317592 | ||
|
|
af4b241523 | ||
|
|
db029c0806 | ||
|
|
6e08887d1f | ||
|
|
17bd4a761a | ||
|
|
258eed505b | ||
|
|
722dcdea3d | ||
|
|
36c270edd8 | ||
|
|
057cf25dd8 | ||
|
|
b361b887c2 | ||
|
|
a59b478d2d | ||
|
|
46a8ba3266 | ||
|
|
c7c2863f9e | ||
|
|
b2d331834a | ||
|
|
e5802aaac3 | ||
|
|
bc95f1b8fd | ||
|
|
13c5b002d0 | ||
|
|
24825d0d9c | ||
|
|
235dc9ca6f | ||
|
|
6a3d306911 | ||
|
|
5a921411e7 | ||
|
|
c37049b75c | ||
|
|
810d083cc0 | ||
|
|
f55b797ff5 | ||
|
|
19b9a298bc | ||
|
|
97bcc51d92 | ||
|
|
8b1504296b | ||
|
|
d0a4a40da9 | ||
|
|
e452ef9fa1 | ||
|
|
d5713877d1 | ||
|
|
9d9ce1c6a4 | ||
|
|
3f779fb496 | ||
|
|
efa6ebf066 | ||
|
|
99c896ce8a | ||
|
|
9616e0a5b0 | ||
|
|
619c8694a4 | ||
|
|
ec0ea73cb2 | ||
|
|
b23811eb60 | ||
|
|
c635ead19a | ||
|
|
5bded6dd5f | ||
|
|
dc5dbe8941 | ||
|
|
70673d5fb6 | ||
|
|
10b68b370e | ||
|
|
4b1ebb62b4 | ||
|
|
90bc78d887 | ||
|
|
268eae032d | ||
|
|
cec0889b51 | ||
|
|
7f30b24cce | ||
|
|
96d33aa3bf | ||
|
|
719614cf3a | ||
|
|
070b1fed65 | ||
|
|
cdf4ab6f45 | ||
|
|
cc7cac1aa5 | ||
|
|
8d40365b16 | ||
|
|
b1590c5cfd | ||
|
|
781448d7e0 | ||
|
|
9255328005 | ||
|
|
0bd8dc206e | ||
|
|
a5c8639dbf | ||
|
|
2d01092aa5 | ||
|
|
d89329155d | ||
|
|
4c816b4d2f | ||
|
|
4f5c43d259 | ||
|
|
a14cdc1b42 | ||
|
|
a02a184321 | ||
|
|
2171bf0f4c | ||
|
|
7be0b71393 | ||
|
|
6d8706e0f0 | ||
|
|
90e2605a78 | ||
|
|
7972cf8660 | ||
|
|
d9d720cc91 | ||
|
|
4040390767 | ||
|
|
21af7707e3 | ||
|
|
5103964110 | ||
|
|
5ceb56b097 | ||
|
|
0454a95617 | ||
|
|
42dcef9e3b | ||
|
|
b4355830e6 | ||
|
|
d661cd7d51 | ||
|
|
50bd8c65fd | ||
|
|
4720c5bfea | ||
|
|
66970d2ccb | ||
|
|
6cf198e4ce | ||
|
|
c7c002df47 | ||
|
|
912d7d18a5 | ||
|
|
e501914fb6 | ||
|
|
e36f6955ca | ||
|
|
749164ceb6 | ||
|
|
afb2db57ec | ||
|
|
b454d7c644 | ||
|
|
3be880e7bf | ||
|
|
f8cf21d64a | ||
|
|
245338a2ff | ||
|
|
0529d1cdb9 | ||
|
|
c567a70c6b | ||
|
|
85fb4df439 | ||
|
|
0255a20e09 | ||
|
|
4a9900237d | ||
|
|
c871514bde | ||
|
|
2df62af242 | ||
|
|
42a9353d68 | ||
|
|
c8bc5bd3ff | ||
|
|
2ca6b5ef2c | ||
|
|
eaa434f025 | ||
|
|
8a5a74725d | ||
|
|
daad7fcaa2 | ||
|
|
6027e0d548 | ||
|
|
670f00cc86 | ||
|
|
82952b0e7e | ||
|
|
c9223b7461 | ||
|
|
b89ae8402a | ||
|
|
6aa707c109 | ||
|
|
9fb95e820a | ||
|
|
c620cb095a | ||
|
|
b76835a0a7 | ||
|
|
73bc8ce75c | ||
|
|
b4542881ab | ||
|
|
1661ef9149 | ||
|
|
9341b1d0a3 | ||
|
|
e420a10df9 | ||
|
|
d4dd99a3cd | ||
|
|
b28c106d87 | ||
|
|
bc03517eff | ||
|
|
de8e3cd7ae | ||
|
|
45113c5cec | ||
|
|
419426b804 | ||
|
|
21daead997 | ||
|
|
8f01b54e0c | ||
|
|
cba8e6d226 | ||
|
|
ce00c7c6e4 | ||
|
|
074fd3a682 | ||
|
|
15ed18b6e1 | ||
|
|
9ca3f7111f | ||
|
|
a482eaffff | ||
|
|
97b15a98da | ||
|
|
f090e07c87 | ||
|
|
0151b18632 | ||
|
|
7fc352dccd | ||
|
|
b07a153189 | ||
|
|
e30014431f | ||
|
|
3fb11d396d | ||
|
|
3768fc19b5 | ||
|
|
0ba0d5044e | ||
|
|
c8f78a4b58 | ||
|
|
f0731f18d2 | ||
|
|
cf02442fde | ||
|
|
adab4b4528 | ||
|
|
a7aab58977 | ||
|
|
2101a7da10 | ||
|
|
2555a554c0 | ||
|
|
fe9ade5589 | ||
|
|
a0b22d1587 | ||
|
|
daa8d05e76 | ||
|
|
81d87815a6 | ||
|
|
f461f09f7e | ||
|
|
e652e827e2 | ||
|
|
cb9ab18d97 | ||
|
|
7aa39bcbc6 | ||
|
|
ad3f44c7bb | ||
|
|
cf0e35367a | ||
|
|
02f666db10 | ||
|
|
a88ab18abd | ||
|
|
a0df41e8b8 | ||
|
|
cbf5bffde4 | ||
|
|
e9863dc307 | ||
|
|
a5f7eb27f6 | ||
|
|
67bb9c88ed | ||
|
|
dd876615f5 | ||
|
|
a5d04c67c9 | ||
|
|
f5f98fe65c | ||
|
|
c99e481807 | ||
|
|
f4b0f893ae | ||
|
|
0f47a8ce8f | ||
|
|
870140dae4 | ||
|
|
c65f1d8813 | ||
|
|
9affe93790 | ||
|
|
6ddbbc80ae | ||
|
|
09977b86be | ||
|
|
975178da9b | ||
|
|
d90d33b6b1 | ||
|
|
1dd038bdd2 | ||
|
|
83762012fb | ||
|
|
ea0a6fb38c | ||
|
|
5f93542bcd | ||
|
|
bd5adfdccc | ||
|
|
0ca21162b3 | ||
|
|
84fbd77bad | ||
|
|
52461baf30 | ||
|
|
d4e777483c | ||
|
|
3662f5028a | ||
|
|
7db7e20916 | ||
|
|
538d82f926 |
8
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: ⚠️ Application issue
|
||||
url: https://github.com/kodjodevf/mangayomi/issues/new/choose
|
||||
about: Issues and requests about the app itself should be opened in the mangayomi repository instead
|
||||
- name: Mangayomi app GitHub repository
|
||||
url: https://github.com/kodjodevf/mangayomi
|
||||
about: Issues about the app itself should be opened here instead.
|
||||
100
.github/ISSUE_TEMPLATE/report_issue.yml
vendored
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
name: 🐞 Issue report
|
||||
description: Report a source issue in Mangayomi
|
||||
labels: [Bug]
|
||||
body:
|
||||
|
||||
- type: input
|
||||
id: source
|
||||
attributes:
|
||||
label: Source information
|
||||
description: |
|
||||
You can find the extension name and version in **Browse → Extensions**.
|
||||
placeholder: |
|
||||
Example: "Gogoanime 0.0.35 (English)"
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: language
|
||||
attributes:
|
||||
label: Source language
|
||||
placeholder: |
|
||||
Example: "English"
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: reproduce-steps
|
||||
attributes:
|
||||
label: Steps to reproduce
|
||||
description: Provide an example of the issue.
|
||||
placeholder: |
|
||||
Example:
|
||||
1. First step
|
||||
2. Second step
|
||||
3. Issue here
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: expected-behavior
|
||||
attributes:
|
||||
label: Expected behavior
|
||||
placeholder: |
|
||||
Example:
|
||||
"This should happen..."
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: actual-behavior
|
||||
attributes:
|
||||
label: Actual behavior
|
||||
placeholder: |
|
||||
Example:
|
||||
"This happened instead..."
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: mangayomi-version
|
||||
attributes:
|
||||
label: Mangayomi version
|
||||
description: |
|
||||
You can find your Mangayomi version in **More → About**.
|
||||
placeholder: |
|
||||
Example: "0.0.67"
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: device
|
||||
attributes:
|
||||
label: Device
|
||||
description: List your device, model and the OS version.
|
||||
placeholder: |
|
||||
Example: "Google Pixel 5 Android 11"
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: other-details
|
||||
attributes:
|
||||
label: Other details
|
||||
placeholder: |
|
||||
Additional details and attachments.
|
||||
|
||||
- type: checkboxes
|
||||
id: acknowledgements
|
||||
attributes:
|
||||
label: Acknowledgements
|
||||
description: Your issue will be closed if you haven't done these steps.
|
||||
options:
|
||||
- label: I have searched the existing issues and this is a new ticket, **NOT** a duplicate or related to another open or closed issue.
|
||||
required: true
|
||||
- label: I have written a short but informative title.
|
||||
required: true
|
||||
- label: I have updated all installed extensions.
|
||||
required: true
|
||||
- label: If this is an issue with the app itself, I should be opening an issue in the [app repository](https://github.com/kodjodevf/mangayomi/issues/new/choose).
|
||||
required: true
|
||||
55
.github/ISSUE_TEMPLATE/request_feature.yml
vendored
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
name: ⭐ Feature request
|
||||
description: Suggest a feature to improve an existing source
|
||||
labels: [Feature request]
|
||||
body:
|
||||
|
||||
- type: input
|
||||
id: source
|
||||
attributes:
|
||||
label: Source name
|
||||
description: |
|
||||
You can find the extension name in **Browse → Extensions**.
|
||||
placeholder: |
|
||||
Example: "AniWatch"
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: language
|
||||
attributes:
|
||||
label: Source language
|
||||
placeholder: |
|
||||
Example: "English"
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: feature-description
|
||||
attributes:
|
||||
label: Describe your suggested feature
|
||||
description: How can an existing extension be improved?
|
||||
placeholder: |
|
||||
Example:
|
||||
"It should work like this..."
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: other-details
|
||||
attributes:
|
||||
label: Other details
|
||||
placeholder: |
|
||||
Additional details and attachments.
|
||||
|
||||
- type: checkboxes
|
||||
id: acknowledgements
|
||||
attributes:
|
||||
label: Acknowledgements
|
||||
description: Your issue will be closed if you haven't done these steps.
|
||||
options:
|
||||
- label: I have searched the existing issues and this is a new ticket, **NOT** a duplicate or related to another open or closed issue.
|
||||
required: true
|
||||
- label: I have written a short but informative title.
|
||||
required: true
|
||||
- label: If this is an issue with the app itself, I should be opening an issue in the [app repository](https://github.com/kodjodevf/mangayomi/issues/new/choose).
|
||||
required: true
|
||||
51
.github/ISSUE_TEMPLATE/request_source.yml
vendored
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
name: 🌐 Source request
|
||||
description: Suggest a new source for Mangayomi
|
||||
labels: [Source request]
|
||||
body:
|
||||
|
||||
- type: input
|
||||
id: name
|
||||
attributes:
|
||||
label: Source name
|
||||
placeholder: |
|
||||
Example: "Not Real Source"
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: link
|
||||
attributes:
|
||||
label: Source link
|
||||
placeholder: |
|
||||
Example: "https://notrealsource.org"
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: language
|
||||
attributes:
|
||||
label: Source language
|
||||
placeholder: |
|
||||
Example: "English"
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: other-details
|
||||
attributes:
|
||||
label: Other details
|
||||
placeholder: |
|
||||
Additional details and attachments.
|
||||
|
||||
- type: checkboxes
|
||||
id: acknowledgements
|
||||
attributes:
|
||||
label: Acknowledgements
|
||||
description: Your issue will be closed if you haven't done these steps.
|
||||
options:
|
||||
- label: I have searched the existing issues and this is a new ticket, **NOT** a duplicate or related to another open or closed issue.
|
||||
required: true
|
||||
- label: I have written a title with source name.
|
||||
required: true
|
||||
- label: I have checked that the extension does not already exist by searching the [GitHub repository](https://github.com/kodjodevf/mangayomi-extensions/) and verified it does not appear in the code base.
|
||||
required: true
|
||||
32
.github/workflows/gen_index.yml
vendored
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
name: Generate json index
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Setup Dart
|
||||
uses: dart-lang/setup-dart@9a04e6d73cca37bd455e0608d7e5092f881fd603
|
||||
|
||||
- name: Generage
|
||||
run: |
|
||||
dart run source_generator.dart
|
||||
|
||||
- name: Commit and Push Changes
|
||||
run: |
|
||||
git config --local user.email "github-actions[bot]@users.noreply.github.com"
|
||||
git config --local user.name "github-actions[bot]"
|
||||
git checkout main
|
||||
git add index.json
|
||||
git add anime_index.json
|
||||
git add novel_index.json
|
||||
git commit -m "Update extensions"
|
||||
git push origin main --force
|
||||
BIN
1.png
|
Before Width: | Height: | Size: 125 KiB |
BIN
2.png
|
Before Width: | Height: | Size: 46 KiB |
BIN
3.png
|
Before Width: | Height: | Size: 52 KiB |
BIN
4.png
|
Before Width: | Height: | Size: 122 KiB |
BIN
5.png
|
Before Width: | Height: | Size: 38 KiB |
BIN
6.png
|
Before Width: | Height: | Size: 201 KiB |
278
CONTRIBUTING-DART.md
Normal file
|
|
@ -0,0 +1,278 @@
|
|||
# Contributing
|
||||
|
||||
This guide have some instructions and tips on how to create a new Mangayomi extension on Dart extension.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Before starting please have installed the recent desktop version of the mangayomi application preferably or if you want with a tablet too.
|
||||
|
||||
|
||||
### Writing your extension
|
||||
1. Open the app.
|
||||
2. Go to extension tab :
|
||||

|
||||
3. then click `+` and you will see :
|
||||

|
||||
4. Fill in the fields with your new source that you would like to create,
|
||||

|
||||
NB: only the `ApiUrl` field is optional
|
||||
then click on save
|
||||
5. you will see your new source in the extension list
|
||||

|
||||
click to open settings
|
||||
6. After click on edit code
|
||||

|
||||
7. Finally you can now write the extension
|
||||

|
||||
- This page contains three parts:
|
||||
- Code editor: where you will write your code
|
||||
- Fecth result: where you will test the different implemented methods by having a result in the expected format
|
||||
- Console: which will show you the logs
|
||||
|
||||
Once extension is ready you can relocate your code into `mangayomi-extension` project in a `src` or `multisrc` package
|
||||
Create the folder with the name of the source such as [this example](https://github.com/kodjodevf/mangayomi-extensions/blob/main/dart/anime/src/en/kisskh)
|
||||
after go either to the anime_source_list.dart file for anime or manga_source_list.dart for the manga and import the extension then
|
||||
create a Pull Request.
|
||||
|
||||
### Source
|
||||
|
||||
| Field | Description |
|
||||
| ----- | ----------- |
|
||||
| `name` | Name displayed in the "Sources" tab in Mangayomi. |
|
||||
| `baseUrl` | Base URL of the source without any trailing slashes. |
|
||||
| `apiUrl` | (Optional, defaults is empty) Api URL of the source with trailing slashes. |
|
||||
| `lang` | An ISO 639-1 compliant language code (two letters in lower case in most cases, but can also include the country/dialect part by using a simple dash character). |
|
||||
| `id` | Identifier of your source, automatically set in `Source`. It should only be manually overriden if you need to copy an existing autogenerated ID. |
|
||||
| `sourceCodeUrl` | contains the URL where the extension source code can be downloaded |
|
||||
| `sourceCode` | contains the extension source code |
|
||||
| `isManga` | (Optional, defaults to `true`) specify source type (false for anime and true for manga)|
|
||||
| `dateFormat` | (Optional, defaults is empty) |
|
||||
| `iconUrl` | The extension icon URL |
|
||||
| `version` | The extension version code. This must be incremented with any change to the code. |
|
||||
| `dateFormatLocale` | (Optional, defaults is empty) |
|
||||
| `isNsfw` | (Optional, defaults to `false`) Flag to indicate that a source contains NSFW content. |
|
||||
|
||||
### Extension call flow
|
||||
|
||||
#### Popular manga
|
||||
|
||||
a.k.a. the Browse source entry point in the app (invoked by tapping on the source name).
|
||||
|
||||
- The app calls `getPopular` which should return a `MPages` containing the first batch of found `MManga` entries.
|
||||
- This method supports pagination. When user scrolls the manga list and more results must be fetched, the app calls it again with increasing `page` values(starting with `page=1`). This continues while `MPages.hasNextPage` is passed as `true` and `MPages.list` is not empty.
|
||||
- To show the list properly, the app needs `url`, `title` and `imageUrl`. You **must** set them here. The rest of the fields could be filled later.(refer to Manga Details below).
|
||||
|
||||
#### Latest manga
|
||||
|
||||
a.k.a. the Latest source entry point in the app (invoked by tapping on the "Latest" button beside the source name).
|
||||
|
||||
- Similar to popular manga, but should be fetching the latest entries from a source.
|
||||
|
||||
#### Search manga
|
||||
|
||||
- When the user searches inside the app, `search` will be called and the rest of the flow is similar to what happens with `getPopular`.
|
||||
- If search functionality is not available, return `MPages([], false)`
|
||||
- `getFilterList` will be called to get all filters and filter types.
|
||||
|
||||
### Filters
|
||||
|
||||
The search flow have support to filters that can be added to a `FilterList` inside the `getFilterList` method. When the user changes the filter's state, they will be passed to the `search` method, and they can be iterated to create the request (by getting the `filter.state` value, where the type varies depending on the `Filter` used). You can check the filter types available [here](https://github.com/kodjodevf/mangayomi/blob/main/lib/eval/model/filter.dart) and in the table below.
|
||||
|
||||
| Filter | Description |
|
||||
| ------ | ----------- |
|
||||
| `HeaderFilter` | A simple header. Useful for separating sections in the list or showing any note or warning to the user. |
|
||||
| `SeparatorFilter` | A line separator. Useful for visual distinction between sections. |
|
||||
| `SelectFilter` | A select control, similar to HTML's `<select>`. Only one item can be selected, and the state is the index of the selected one. |
|
||||
| `TextFilter` | A text control, similar to HTML's `<input type="text">`. |
|
||||
| `CheckBoxFilter` | A checkbox control, similar to HTML's `<input type="checkbox">`. The state is `true` if it's checked. |
|
||||
| `TriStateFilter` | A enhanced checkbox control that supports an excluding state |
|
||||
| `GroupFilter` | A group of filters (preferentially of the same type). The state will be a `List` with all the states. |
|
||||
| `SortFilter` | A control for sorting, with support for the ordering. The state indicates which item index is selected and if the sorting is `ascending`. |
|
||||
|
||||
All control filters can have a default state set. It's usually recommended if the source have filters to make the initial state match the popular manga list, so when the user open the filter sheet, the state is equal and represents the current manga showing.
|
||||
|
||||
#### Manga Details
|
||||
|
||||
- When user taps on an manga, `getDetail` will be called and the results will be cached.
|
||||
- A `MManga` entry is identified by its `url`.
|
||||
- `getDetail` is called to update an manga's details from when it was initialized earlier.
|
||||
- `MManga.title` is a string containing title.
|
||||
- `MManga.description` is a string containing description.
|
||||
- `MManga.author` is a string containing author.
|
||||
- `MManga.genre` contain list of all genres.
|
||||
- `MManga.status` is an "enum" value.
|
||||
- To get the status in enum value from a status string, you can use a `parseStatus` function like in the example below.
|
||||
`Status` parseStatus(`String status`, `List<dynamic> statusList`)
|
||||
```bash
|
||||
final statusList = [
|
||||
{ "ongoing": 0,
|
||||
"complete": 1,
|
||||
"hiatus": 2,
|
||||
"canceled": 3,
|
||||
"publishingFinished": 4,
|
||||
}
|
||||
];
|
||||
final status = parseStatus('ongoing', statusList);
|
||||
print(status); // Status.ongoing
|
||||
```
|
||||
Refer to [the values in the `MManga` model](https://github.com/kodjodevf/mangayomi/blob/main/lib/models/manga.dart).
|
||||
- During a backup, only `url` and `title` are stored. To restore the rest of the manga data, the app calls `getDetail`, so all fields should be (re)filled in if possible.
|
||||
- If a `MManga` is cached `getDetail` will be only called when the user does a manual update(Swipe-to-Refresh).
|
||||
- `MManga.chapters` contain list of all manga chapters.
|
||||
- `MChapter.name` is a string containing a chapter name.
|
||||
- `MChapter.url` is a string containing a chapter url.
|
||||
- `MChapter.scanlator` is a string containing a chapter scanlator.
|
||||
- `MChapter.dateUpload` is a string containing date **expressed in millisecondsSinceEpoch**.
|
||||
- To get the time in millisecondsSinceEpoch from a date string, you can use a `parseDates` function like in the example below.
|
||||
`List<dynamic>` parseDates(`List<dynamic> values`, `String dateFormat`, `String dateFormatLocale`,)
|
||||
```bash
|
||||
final dates = parseDates(["2023-12-10T11:49:02+000"], "yyyy-MM-dd'T'HH:mm:ss+SSS", "en_US");
|
||||
```
|
||||
- If you don't pass `MChapter.dateUpload` and leave it null, the app will use the default date instead, but it's recommended to always fill it if it's available.
|
||||
|
||||
|
||||
#### Chapter pages
|
||||
|
||||
- When user opens an chapter, `getPageList` will be called and it will return a `List<String>` or `List<Map<String,dynamic>>` with a list of `{"url":string,"headers":Map<String,String>}` that are used by the reader.
|
||||
|
||||
#### Episode Videos
|
||||
|
||||
- When user opens an episode, `getVideoList` will be called and it will return a `List<MVideo>` that are used by the player.
|
||||
|
||||
## Example sources that can help you understand how to create your source
|
||||
|
||||
- [Example](https://github.com/kodjodevf/mangayomi-extensions/blob/main/dart/anime/src/en/kisskh/kisskh.dart)
|
||||
of Json API usage.
|
||||
- [Example](https://github.com/kodjodevf/mangayomi-extensions/blob/main/dart/manga/src/en/mangahere/mangahere.dart)
|
||||
of HTML parsing using xpath selector.
|
||||
- [Example](https://github.com/kodjodevf/mangayomi-extensions/blob/main/dart/manga/multisrc/madara/madara.dart)
|
||||
of HTML parsing using HTML DOM selector.
|
||||
|
||||
|
||||
## Some functions already available and usable
|
||||
|
||||
|
||||
### http client
|
||||
|
||||
Return Response
|
||||
```bash
|
||||
- Simple request
|
||||
|
||||
final Client client = Client();
|
||||
|
||||
final res = await client.get(Uri.parse("http://example.com"));
|
||||
|
||||
print(res.body);
|
||||
|
||||
- With headers
|
||||
|
||||
final Client client = Client();
|
||||
|
||||
final res = await client.get(Uri.parse("http://example.com"),headers:{"Referer": "http://example.com"});
|
||||
|
||||
print(res.body);
|
||||
|
||||
- With body
|
||||
|
||||
final Client client = Client();
|
||||
|
||||
final res = await client.post(Uri.parse("http://example.com"),headers:{"Referer": "http://example.com"},'body':{'name':'test'});
|
||||
|
||||
print(res.body);
|
||||
|
||||
```
|
||||
|
||||
### xpath selector
|
||||
Return result as `List<String>`
|
||||
|
||||
Example:
|
||||
```bash
|
||||
final String htmlString = '''
|
||||
<html lang="en">
|
||||
<body>
|
||||
<div><a href='https://github.com/kodjodevf'>author</a></div>
|
||||
<div class="head">div head</div>
|
||||
<div class="container">
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td id="td1" class="first1">1</td>
|
||||
<td id="td2" class="first1">2</td>
|
||||
<td id="td3" class="first2">3</td>
|
||||
<td id="td4" class="first2 form">4</td>
|
||||
<td id="td5" class="second1">one</td>
|
||||
<td id="td6" class="second1">two</td>
|
||||
<td id="td7" class="second2">three</td>
|
||||
<td id="td8" class="second2">four</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="end">end</div>
|
||||
</body>
|
||||
</html>
|
||||
''';
|
||||
|
||||
|
||||
List<String> xpathRes = xpath(htmlString,'//div/a/@href');
|
||||
print(xpathRes); // [https://github.com/kodjodevf]
|
||||
print(xpathRes.first); // https://github.com/kodjodevf
|
||||
|
||||
```
|
||||
### HTML DOM selector
|
||||
|
||||
Example:
|
||||
```bash
|
||||
final String htmlString = '''
|
||||
<html lang="en">
|
||||
<body>
|
||||
<div><a href='https://github.com/kodjodevf'>author</a></div>
|
||||
<div class="head">div head</div>
|
||||
<div class="container">
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td id="td1" class="first1">1</td>
|
||||
<td id="td2" class="first1">2</td>
|
||||
<td id="td3" class="first2">3</td>
|
||||
<td id="td4" class="first2 form">4</td>
|
||||
<td id="td5" class="second1">one</td>
|
||||
<td id="td6" class="second1">two</td>
|
||||
<td id="td7" class="second2">three</td>
|
||||
<td id="td8" class="second2">four</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="end">end</div>
|
||||
</body>
|
||||
</html>
|
||||
''';
|
||||
|
||||
|
||||
MDocument document = parseHtml(htmlString);
|
||||
print(document.selectFirst("a").attr("href")); // https://github.com/kodjodevf
|
||||
print(document.selectFirst("td").text); // 1
|
||||
|
||||
```
|
||||
See [`MDocument` model](https://github.com/kodjodevf/mangayomi/blob/main/lib/eval/model/document.dart) and [`MElement` model](https://github.com/kodjodevf/mangayomi/blob/main/lib/eval/model/element.dart) to see available methods.
|
||||
|
||||
|
||||
### String utils
|
||||
- `String` substringAfter(`String text`, `String pattern`)
|
||||
- `String` substringAfterLast(`String text`, `String pattern`)
|
||||
- `String` substringBefore(`String text`, `String pattern`)
|
||||
- `String` substringBeforeLast(`String text`, `String pattern`)
|
||||
- `String` getUrlWithoutDomain(`String url`)
|
||||
|
||||
### Crypto utils
|
||||
- `String` unpackJs(`String code`);
|
||||
- `Future<String>` evalJs(`String code`);
|
||||
- `String` deobfuscateJsPassword(`String inputString`)
|
||||
- `String` encryptAESCryptoJS(`String plainText`, `String passphrase`)
|
||||
- `String` decryptAESCryptoJS(`String encrypted`, `String passphrase`)
|
||||
- `String` cryptoHandler(`String text`, `String iv`, `String secretKeyString`, `bool encrypt`)
|
||||
|
||||
## Help
|
||||
|
||||
If you need a help or have some questions, ask a community in our [Discord server](https://discord.com/invite/EjfBuYahsP).
|
||||
205
CONTRIBUTING-JS.md
Normal file
|
|
@ -0,0 +1,205 @@
|
|||
# Contributing
|
||||
|
||||
This guide have some instructions and tips on how to create a new Mangayomi extension on JavaScript extension.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Before starting please have installed the recent desktop version of the mangayomi application preferably or if you want with a tablet too.
|
||||
|
||||
|
||||
### Writing your extension
|
||||
1. Open the app.
|
||||
2. Go to extension tab :
|
||||

|
||||
3. then click `+` and you will see :
|
||||

|
||||
4. Fill in the fields with your new source that you would like to create,
|
||||

|
||||
NB: only the `ApiUrl` field is optional
|
||||
then click on save
|
||||
5. you will see your new source in the extension list
|
||||

|
||||
click to open settings
|
||||
6. After click on edit code
|
||||

|
||||
7. Finally you can now write the extension
|
||||

|
||||
- This page contains three parts:
|
||||
- Code editor: where you will write your code
|
||||
- Fecth result: where you will test the different implemented methods by having a result in the expected format
|
||||
- Console: which will show you the logs
|
||||
|
||||
Once extension is ready you can relocate your code into `mangayomi-extension` project in a `src` or `multisrc` package and create a Pull Request.
|
||||
|
||||
### Source
|
||||
|
||||
| Field | Description |
|
||||
| ----- | ----------- |
|
||||
| `name` | Name displayed in the "Sources" tab in Mangayomi. |
|
||||
| `baseUrl` | Base URL of the source without any trailing slashes. |
|
||||
| `apiUrl` | (Optional, defaults is empty) Api URL of the source with trailing slashes. |
|
||||
| `lang` | An ISO 639-1 compliant language code (two letters in lower case in most cases, but can also include the country/dialect part by using a simple dash character). |
|
||||
| `id` | Identifier of your source, automatically set in `Source`. It should only be manually overriden if you need to copy an existing autogenerated ID. |
|
||||
| `isManga` | (Optional, defaults to `true`) specify source type (false for anime and true for manga)|
|
||||
| `dateFormat` | (Optional, defaults is empty) |
|
||||
| `iconUrl` | The extension icon URL |
|
||||
| `version` | The extension version code. This must be incremented with any change to the code. |
|
||||
| `dateFormatLocale` | (Optional, defaults is empty) |
|
||||
| `isNsfw` | (Optional, defaults to `false`) Flag to indicate that a source contains NSFW content. |
|
||||
|
||||
### Extension call flow
|
||||
|
||||
#### Popular manga
|
||||
|
||||
a.k.a. the Browse source entry point in the app (invoked by tapping on the source name).
|
||||
|
||||
- The app calls `getPopular` which should return a JSON
|
||||
```bash
|
||||
{
|
||||
'list': array of {'url':string,'name':string,'link':string},
|
||||
hasNextPage: Boolean
|
||||
}
|
||||
```
|
||||
- This method supports pagination. When user scrolls the manga list and more results must be fetched, the app calls it again with increasing `page` values(starting with `page=1`). This continues while `hasNextPage` is passed as `true` and `list` is not empty.
|
||||
|
||||
#### Latest manga
|
||||
|
||||
a.k.a. the Latest source entry point in the app (invoked by tapping on the "Latest" button beside the source name).
|
||||
|
||||
- Similar to popular manga, but should be fetching the latest entries from a source.
|
||||
|
||||
#### Search manga
|
||||
|
||||
- When the user searches inside the app, `search` will be called and the rest of the flow is similar to what happens with `getPopular`.
|
||||
- `getFilterList` will be called to get all filters and filter types.
|
||||
|
||||
|
||||
#### Manga Details
|
||||
|
||||
- When user taps on an manga, `getDetail` will be called and the results will be cached.
|
||||
- `getDetail` is called to update an manga's details from when it was initialized earlier.
|
||||
- `title` is a string containing title.
|
||||
- `description` is a string containing description.
|
||||
- `author` is a string containing author.
|
||||
- `genre` contain array of all genres.
|
||||
- `status` is an "integer" value.
|
||||
You can refer to this example to see the correspondence:
|
||||
```bash
|
||||
0=>"ongoing", 1=>"complete", 2=>"hiatus", 3=>"canceled", 4=>"publishingFinished", 5=>unknow
|
||||
```
|
||||
|
||||
- `chapters` or `episodes` contain all of all manga chapters or anime episodes.
|
||||
- `name` is a string containing a chapter name.
|
||||
- `url` is a string containing a chapter url.
|
||||
- `scanlator` is a string containing a chapter scanlator.
|
||||
- `dateUpload` is a string containing date **expressed in millisecondsSinceEpoch**.
|
||||
- If you don't pass `dateUpload` and leave it null, the app will use the default date instead, but it's recommended to always fill it if it's available.
|
||||
|
||||
#### Chapter pages
|
||||
|
||||
- When user opens an chapter, `getPageList` will be called and it will return an array of string or an array of map like `{ url:string,headers:map }` that are used by the reader.
|
||||
|
||||
#### Episode Videos
|
||||
|
||||
- When user opens an episode, `getVideoList` will be called and it will return a
|
||||
```bash
|
||||
array of {'url':string,'originalUrl':string,'quality':string}
|
||||
```
|
||||
|
||||
that are used by the player.
|
||||
|
||||
## Example sources that can help you understand how to create your source
|
||||
|
||||
- [Example](https://github.com/kodjodevf/mangayomi-extensions/blob/main/javascript/anime/src/de/aniworld.js)
|
||||
of HTML parsing using HTML DOM selector.
|
||||
- [Example](https://github.com/kodjodevf/mangayomi-extensions/blob/main/javascript/anime/src/en/allanime.js)
|
||||
of Json API usage.
|
||||
|
||||
|
||||
## Some functions already available and usable
|
||||
|
||||
|
||||
### http client
|
||||
|
||||
Return Response
|
||||
```bash
|
||||
- Simple request
|
||||
|
||||
const client = new Client();
|
||||
|
||||
const res = await client.get("http://example.com");
|
||||
|
||||
console.log(res.body);
|
||||
|
||||
- With headers
|
||||
|
||||
const client = new Client();
|
||||
|
||||
const res = await client.get("http://example.com",{"Referer": "http://example.com"});
|
||||
|
||||
console.log(res.body);
|
||||
|
||||
- With body
|
||||
|
||||
const client = new Client();
|
||||
|
||||
const res = await client.post("http://example.com",{"Referer": "http://example.com"},{'name':'test'});
|
||||
|
||||
console.log(res.body);
|
||||
|
||||
```
|
||||
|
||||
### HTML DOM selector
|
||||
|
||||
Example:
|
||||
```bash
|
||||
const htmlString = `
|
||||
<html lang="en">
|
||||
<body>
|
||||
<div><a href='https://github.com/kodjodevf'>author</a></div>
|
||||
<div class="head">div head</div>
|
||||
<div class="container">
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td id="td1" class="first1">1</td>
|
||||
<td id="td2" class="first1">2</td>
|
||||
<td id="td3" class="first2">3</td>
|
||||
<td id="td4" class="first2 form">4</td>
|
||||
<td id="td5" class="second1">one</td>
|
||||
<td id="td6" class="second1">two</td>
|
||||
<td id="td7" class="second2">three</td>
|
||||
<td id="td8" class="second2">four</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="end">end</div>
|
||||
</body>
|
||||
</html>`
|
||||
|
||||
const document = new Document(htmlString);
|
||||
console.log(document.selectFirst("a").attr("href")); // https://github.com/kodjodevf
|
||||
console.log(document.selectFirst("td").text); // 1
|
||||
|
||||
```
|
||||
See [`dom_selector`](https://github.com/kodjodevf/mangayomi/blob/main/lib/eval/javascript/dom_selector.dart) to see available methods.
|
||||
|
||||
|
||||
### String utils
|
||||
- this.substringAfter(`string: pattern`)
|
||||
- this.substringAfterLast(`string: pattern`)
|
||||
- this.substringBefore(`string: pattern`)
|
||||
- this.substringBeforeLast(`string: pattern`)
|
||||
- this.substringBetween(`string: left`, `string: right`)
|
||||
|
||||
### Crypto utils
|
||||
- unpackJs(`string: code`);
|
||||
- deobfuscateJsPassword(`string: inputString`)
|
||||
- encryptAESCryptoJS(`string: plainText`, `string: passphrase`)
|
||||
- decryptAESCryptoJS(`string: encrypted`, `string: passphrase`)
|
||||
- cryptoHandler(`string: text`, `string: iv`, `string: secretKeyString`, `Boolean: encrypt`)
|
||||
|
||||
## Help
|
||||
|
||||
If you need a help or have some questions, ask a community in our [Discord server](https://discord.com/invite/EjfBuYahsP).
|
||||
177
LICENSE
Normal file
|
|
@ -0,0 +1,177 @@
|
|||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
32
README.md
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
# Mangayomi Extensions
|
||||
|
||||
This repository contains the available extension catalogues for the [Mangayomi](https://github.com/kodjodevf/mangayomi) app.
|
||||
|
||||
|
||||
# Contributing
|
||||
|
||||
Contributions are welcome!
|
||||
|
||||
To get started with development, see [CONTRIBUTING-DART.md](./CONTRIBUTING-DART.md) for create sources in Dart or [CONTRIBUTING-JS.md](./CONTRIBUTING-JS.md) for create sources in JavaScript.
|
||||
|
||||
## License
|
||||
|
||||
Copyright 2023 Moustapha Kodjo Amadou
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
|
||||
|
||||
## Disclaimer
|
||||
|
||||
The developer of this application does not have any affiliation with the content providers available.
|
||||
1
anime_index.json
Normal file
BIN
dart-3.png
|
Before Width: | Height: | Size: 50 KiB |
BIN
dart-4.png
|
Before Width: | Height: | Size: 141 KiB |
BIN
dart-5.png
|
Before Width: | Height: | Size: 112 KiB |
BIN
dart-6.png
|
Before Width: | Height: | Size: 194 KiB |
58
dart/anime/anime_source_list.dart
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
import '../../model/source.dart';
|
||||
import 'multisrc/datalifeengine/sources.dart';
|
||||
import 'multisrc/dopeflix/sources.dart';
|
||||
import 'multisrc/zorotheme/sources.dart';
|
||||
import 'src/all/animeworldindia/sources.dart';
|
||||
import 'src/all/nyaa/source.dart';
|
||||
import 'src/ar/okanime/source.dart';
|
||||
import 'src/de/animetoast/source.dart';
|
||||
import 'src/en/animepahe/source.dart';
|
||||
import 'src/en/dramacool/source.dart';
|
||||
import 'src/en/gogoanime/source.dart';
|
||||
import 'src/en/nineanimetv/source.dart';
|
||||
import 'src/es/animeonlineninja/source.dart';
|
||||
import 'src/fr/animesama/source.dart';
|
||||
import 'src/fr/anizone/source.dart';
|
||||
import 'src/hi/yomovies/source.dart';
|
||||
import 'src/en/kisskh/source.dart';
|
||||
import 'src/en/uhdmovies/source.dart';
|
||||
import 'src/fr/animesultra/source.dart';
|
||||
import 'src/fr/franime/source.dart';
|
||||
import 'src/fr/otakufr/source.dart';
|
||||
import 'src/id/nimegami/source.dart';
|
||||
import 'src/id/oploverz/source.dart';
|
||||
import 'src/id/otakudesu/source.dart';
|
||||
import 'src/it/animesaturn/source.dart';
|
||||
import 'src/pt/animesvision/source.dart';
|
||||
import 'src/sq/filma24/source.dart';
|
||||
import 'src/tr/diziwatch/source.dart';
|
||||
|
||||
List<Source> dartAnimesourceList = [
|
||||
gogoanimeSource,
|
||||
franimeSource,
|
||||
otakufr,
|
||||
animesultraSource,
|
||||
...zorothemeSourcesList,
|
||||
kisskhSource,
|
||||
okanimeSource,
|
||||
otakudesu,
|
||||
nimegami,
|
||||
oploverz,
|
||||
...dopeflixSourcesList,
|
||||
animesaturn,
|
||||
uhdmoviesSource,
|
||||
...datalifeengineSourcesList,
|
||||
filma24,
|
||||
dramacoolSource,
|
||||
yomoviesSource,
|
||||
animesamaSource,
|
||||
nineanimetv,
|
||||
...animeworldindiaSourcesList,
|
||||
nyaaSource,
|
||||
animepaheSource,
|
||||
animetoast,
|
||||
animesvision,
|
||||
diziwatchSource,
|
||||
aniZoneSource,
|
||||
animeonlineninjaSource
|
||||
];
|
||||
415
dart/anime/multisrc/datalifeengine/datalifeengine.dart
Normal file
|
|
@ -0,0 +1,415 @@
|
|||
import 'dart:convert';
|
||||
import 'package:mangayomi/bridge_lib.dart';
|
||||
|
||||
class DataLifeEngine extends MProvider {
|
||||
DataLifeEngine({required this.source});
|
||||
|
||||
MSource source;
|
||||
|
||||
final Client client =
|
||||
Client(source, json.encode({"useDartHttpClient": true}));
|
||||
|
||||
@override
|
||||
bool get supportsLatest => false;
|
||||
|
||||
@override
|
||||
String get baseUrl => getPreferenceValue(source.id, "overrideBaseUrl");
|
||||
|
||||
@override
|
||||
Future<MPages> getPopular(int page) async {
|
||||
final res =
|
||||
(await client.get(Uri.parse("$baseUrl${getPath(source)}page/$page")))
|
||||
.body;
|
||||
return animeFromElement(res);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<MPages> getLatestUpdates(int page) async {
|
||||
return MPages([], false);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<MPages> search(String query, int page, FilterList filterList) async {
|
||||
final filters = filterList.filters;
|
||||
String res = "";
|
||||
if (query.isNotEmpty) {
|
||||
if (query.length < 4)
|
||||
throw "La recherche est suspendue! La chaîne de recherche est vide ou contient moins de 4 caractères.";
|
||||
final headers = {
|
||||
"Host": Uri.parse(baseUrl).host,
|
||||
"Origin": baseUrl,
|
||||
"Referer": "$baseUrl/"
|
||||
};
|
||||
final cleanQuery = query.replaceAll(" ", "+");
|
||||
if (page == 1) {
|
||||
res = (await client.post(
|
||||
Uri.parse(
|
||||
"$baseUrl?do=search&subaction=search&story=$cleanQuery"),
|
||||
headers: headers))
|
||||
.body;
|
||||
} else {
|
||||
res = (await client.post(
|
||||
Uri.parse(
|
||||
"$baseUrl?do=search&subaction=search&search_start=$page&full_search=0&result_from=11&story=$cleanQuery"),
|
||||
headers: headers))
|
||||
.body;
|
||||
}
|
||||
} else {
|
||||
String url = "";
|
||||
for (var filter in filters) {
|
||||
if (filter.type == "CategoriesFilter") {
|
||||
if (filter.state != 0) {
|
||||
url = "$baseUrl${filter.values[filter.state].value}page/$page/";
|
||||
}
|
||||
} else if (filter.type == "GenresFilter") {
|
||||
if (filter.state != 0) {
|
||||
url = "$baseUrl${filter.values[filter.state].value}page/$page/";
|
||||
}
|
||||
}
|
||||
}
|
||||
res = (await client.get(Uri.parse(url))).body;
|
||||
}
|
||||
|
||||
return animeFromElement(res);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<MManga> getDetail(String url) async {
|
||||
String res =
|
||||
(await client.get(Uri.parse("$baseUrl${getUrlWithoutDomain(url)}")))
|
||||
.body;
|
||||
MManga anime = MManga();
|
||||
final description = xpath(res, '//span[@itemprop="description"]/text()');
|
||||
anime.description = description.isNotEmpty ? description.first : "";
|
||||
anime.genre = xpath(res, '//span[@itemprop="genre"]/a/text()');
|
||||
|
||||
List<MChapter>? episodesList = [];
|
||||
|
||||
if (source.name == "French Anime") {
|
||||
final epsData = xpath(res, '//div[@class="eps"]/text()');
|
||||
for (var epData in epsData.first.split('\n')) {
|
||||
final data = epData.split('!');
|
||||
MChapter ep = MChapter();
|
||||
ep.name = "Episode ${data.first}";
|
||||
ep.url = data.last;
|
||||
episodesList.add(ep);
|
||||
}
|
||||
} else {
|
||||
final doc = parseHtml(res);
|
||||
final elements = doc
|
||||
.select(".hostsblock div:has(a)")
|
||||
.where((MElement e) => e.outerHtml.contains("loadVideo('https://"))
|
||||
.toList();
|
||||
if (elements.isNotEmpty) {
|
||||
for (var element in elements) {
|
||||
element = element as MElement;
|
||||
MChapter ep = MChapter();
|
||||
ep.name = element.className
|
||||
.replaceAll("ep", "Episode ")
|
||||
.replaceAll("vs", " VOSTFR")
|
||||
.replaceAll("vf", " VF");
|
||||
ep.url = element
|
||||
.select("a")
|
||||
.map((MElement e) => substringBefore(
|
||||
substringAfter(e.attr('onclick'), "loadVideo('"), "')"))
|
||||
.toList()
|
||||
.join(",")
|
||||
.replaceAll("/vd.php?u=", "");
|
||||
ep.scanlator = element.className.contains('vf') ? 'VF' : 'VOSTFR';
|
||||
episodesList.add(ep);
|
||||
}
|
||||
} else {
|
||||
MChapter ep = MChapter();
|
||||
ep.name = "Film";
|
||||
ep.url = doc
|
||||
.select("a")
|
||||
.where((MElement e) => e.outerHtml.contains("loadVideo('https://"))
|
||||
.map((MElement e) => substringBefore(
|
||||
substringAfter(e.attr('onclick'), "loadVideo('"), "')"))
|
||||
.toList()
|
||||
.join(",")
|
||||
.replaceAll("/vd.php?u=", "");
|
||||
episodesList.add(ep);
|
||||
}
|
||||
}
|
||||
|
||||
anime.chapters = episodesList.reversed.toList();
|
||||
return anime;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<MVideo>> getVideoList(String url) async {
|
||||
List<MVideo> videos = [];
|
||||
final sUrls = url.split(',');
|
||||
for (var sUrl in sUrls) {
|
||||
List<MVideo> a = [];
|
||||
if (sUrl.contains("dood") || sUrl.contains("d000")) {
|
||||
a = await doodExtractor(sUrl, "DoodStream");
|
||||
} else if (["streamhide", "guccihide", "streamvid", "dhtpre"]
|
||||
.any((a) => sUrl.contains(a))) {
|
||||
a = await streamHideExtractor(sUrl);
|
||||
} else if (sUrl.contains("uqload")) {
|
||||
a = await uqloadExtractor(sUrl);
|
||||
} else if (sUrl.contains("upstream")) {
|
||||
a = await upstreamExtractor(sUrl);
|
||||
} else if (sUrl.contains("sibnet")) {
|
||||
a = await sibnetExtractor(sUrl);
|
||||
} else if (sUrl.contains("ok.ru")) {
|
||||
a = await okruExtractor(sUrl);
|
||||
} else if (sUrl.contains("vidmoly")) {
|
||||
a = await vidmolyExtractor(sUrl);
|
||||
} else if (sUrl.contains("streamtape")) {
|
||||
a = await streamTapeExtractor(sUrl, "");
|
||||
} else if (sUrl.contains("voe.sx")) {
|
||||
a = await voeExtractor(sUrl, "");
|
||||
}
|
||||
videos.addAll(a);
|
||||
}
|
||||
return videos;
|
||||
}
|
||||
|
||||
MPages animeFromElement(String res) {
|
||||
final htmls = parseHtml(res).select("div#dle-content > div.mov");
|
||||
List<MManga> animeList = [];
|
||||
for (var h in htmls) {
|
||||
final html = h.innerHtml;
|
||||
final url = xpath(html, '//a/@href').first;
|
||||
final name = xpath(html, '//a/text()').first;
|
||||
final image = xpath(html, '//div[contains(@class,"mov")]/img/@src').first;
|
||||
final season = xpath(html, '//div/span[@class="block-sai"]/text()');
|
||||
MManga anime = MManga();
|
||||
anime.name =
|
||||
"$name ${season.isNotEmpty ? season.first.replaceAll("\n", " ") : ""}";
|
||||
anime.imageUrl = "$baseUrl$image";
|
||||
anime.link = url;
|
||||
animeList.add(anime);
|
||||
}
|
||||
final hasNextPage = xpath(res, '//span[@class="pnext"]/a/@href').isNotEmpty;
|
||||
return MPages(animeList, hasNextPage);
|
||||
}
|
||||
|
||||
Future<List<MVideo>> streamHideExtractor(String url) async {
|
||||
final res = (await client.get(Uri.parse(url))).body;
|
||||
final masterUrl = substringBefore(
|
||||
substringAfter(
|
||||
substringAfter(
|
||||
substringAfter(unpackJs(res), "sources:"), "file:\""),
|
||||
"src:\""),
|
||||
'"');
|
||||
final masterPlaylistRes = (await client.get(Uri.parse(masterUrl))).body;
|
||||
List<MVideo> videos = [];
|
||||
for (var it in substringAfter(masterPlaylistRes, "#EXT-X-STREAM-INF:")
|
||||
.split("#EXT-X-STREAM-INF:")) {
|
||||
final quality =
|
||||
"${substringBefore(substringBefore(substringAfter(substringAfter(it, "RESOLUTION="), "x"), ","), "\n")}p";
|
||||
|
||||
String videoUrl = substringBefore(substringAfter(it, "\n"), "\n");
|
||||
|
||||
if (!videoUrl.startsWith("http")) {
|
||||
videoUrl =
|
||||
"${masterUrl.split("/").sublist(0, masterUrl.split("/").length - 1).join("/")}/$videoUrl";
|
||||
}
|
||||
|
||||
MVideo video = MVideo();
|
||||
video
|
||||
..url = videoUrl
|
||||
..originalUrl = videoUrl
|
||||
..quality = "StreamHideVid - $quality";
|
||||
videos.add(video);
|
||||
}
|
||||
return videos;
|
||||
}
|
||||
|
||||
Future<List<MVideo>> upstreamExtractor(String url) async {
|
||||
final res = (await client.get(Uri.parse(url))).body;
|
||||
final js = xpath(res, '//script[contains(text(), "m3u8")]/text()');
|
||||
if (js.isEmpty) {
|
||||
return [];
|
||||
}
|
||||
final masterUrl =
|
||||
substringBefore(substringAfter(unpackJs(js.first), "{file:\""), "\"}");
|
||||
final masterPlaylistRes = (await client.get(Uri.parse(masterUrl))).body;
|
||||
List<MVideo> videos = [];
|
||||
for (var it in substringAfter(masterPlaylistRes, "#EXT-X-STREAM-INF:")
|
||||
.split("#EXT-X-STREAM-INF:")) {
|
||||
final quality =
|
||||
"${substringBefore(substringBefore(substringAfter(substringAfter(it, "RESOLUTION="), "x"), ","), "\n")}p";
|
||||
|
||||
String videoUrl = substringBefore(substringAfter(it, "\n"), "\n");
|
||||
|
||||
if (!videoUrl.startsWith("http")) {
|
||||
videoUrl =
|
||||
"${masterUrl.split("/").sublist(0, masterUrl.split("/").length - 1).join("/")}/$videoUrl";
|
||||
}
|
||||
|
||||
MVideo video = MVideo();
|
||||
video
|
||||
..url = videoUrl
|
||||
..originalUrl = videoUrl
|
||||
..quality = "Upstream - $quality";
|
||||
videos.add(video);
|
||||
}
|
||||
return videos;
|
||||
}
|
||||
|
||||
Future<List<MVideo>> uqloadExtractor(String url) async {
|
||||
final res = (await client.get(Uri.parse(url))).body;
|
||||
final js = xpath(res, '//script[contains(text(), "sources:")]/text()');
|
||||
if (js.isEmpty) {
|
||||
return [];
|
||||
}
|
||||
|
||||
final videoUrl =
|
||||
substringBefore(substringAfter(js.first, "sources: [\""), '"');
|
||||
MVideo video = MVideo();
|
||||
video
|
||||
..url = videoUrl
|
||||
..originalUrl = videoUrl
|
||||
..quality = "Uqload"
|
||||
..headers = {"Referer": "${Uri.parse(url).origin}/"};
|
||||
return [video];
|
||||
}
|
||||
|
||||
Future<List<MVideo>> vidmolyExtractor(String url) async {
|
||||
final headers = {
|
||||
'Referer': 'https://vidmoly.to',
|
||||
};
|
||||
List<MVideo> videos = [];
|
||||
final playListUrlResponse = (await client.get(Uri.parse(url))).body;
|
||||
final playlistUrl =
|
||||
RegExp(r'file:"(\S+?)"').firstMatch(playListUrlResponse)?.group(1) ??
|
||||
"";
|
||||
if (playlistUrl.isEmpty) return [];
|
||||
final masterPlaylistRes =
|
||||
await client.get(Uri.parse(playlistUrl), headers: headers);
|
||||
|
||||
if (masterPlaylistRes.statusCode == 200) {
|
||||
for (var it
|
||||
in substringAfter(masterPlaylistRes.body, "#EXT-X-STREAM-INF:")
|
||||
.split("#EXT-X-STREAM-INF:")) {
|
||||
final quality =
|
||||
"${substringBefore(substringBefore(substringAfter(substringAfter(it, "RESOLUTION="), "x"), ","), "\n")}p";
|
||||
|
||||
String videoUrl = substringBefore(substringAfter(it, "\n"), "\n");
|
||||
|
||||
MVideo video = MVideo();
|
||||
video
|
||||
..url = videoUrl
|
||||
..originalUrl = videoUrl
|
||||
..quality = "Vidmoly $quality"
|
||||
..headers = headers;
|
||||
videos.add(video);
|
||||
}
|
||||
}
|
||||
|
||||
return videos;
|
||||
}
|
||||
|
||||
String getPath() {
|
||||
if (source.name == "French Anime") return "/animes-vostfr/";
|
||||
return "/serie-en-streaming/";
|
||||
}
|
||||
|
||||
@override
|
||||
List<dynamic> getSourcePreferences() {
|
||||
return [
|
||||
if (source.name == "Wiflix")
|
||||
EditTextPreference(
|
||||
key: "overrideBaseUrl",
|
||||
title: "Changer l'url de base",
|
||||
summary: "",
|
||||
value: "https://wiflix-hd.vip",
|
||||
dialogTitle: "Changer l'url de base",
|
||||
dialogMessage: "",
|
||||
text: "https://wiflix-hd.vip"),
|
||||
if (source.name == "French Anime")
|
||||
EditTextPreference(
|
||||
key: "overrideBaseUrl",
|
||||
title: "Changer l'url de base",
|
||||
summary: "",
|
||||
value: "https://french-anime.com",
|
||||
dialogTitle: "Changer l'url de base",
|
||||
dialogMessage: "",
|
||||
text: "https://french-anime.com"),
|
||||
];
|
||||
}
|
||||
|
||||
@override
|
||||
List<dynamic> getFilterList() {
|
||||
return [
|
||||
HeaderFilter("La recherche de texte ignore les filtres"),
|
||||
if (source.name == "French Anime")
|
||||
SelectFilter("CategoriesFilter", "Catégories", 0, [
|
||||
SelectFilterOption("<Sélectionner>", ""),
|
||||
SelectFilterOption("Action", "/genre/action/"),
|
||||
SelectFilterOption("Aventure", "/genre/aventure/"),
|
||||
SelectFilterOption("Arts martiaux", "/genre/arts-martiaux/"),
|
||||
SelectFilterOption("Combat", "/genre/combat/"),
|
||||
SelectFilterOption("Comédie", "/genre/comedie/"),
|
||||
SelectFilterOption("Drame", "/genre/drame/"),
|
||||
SelectFilterOption("Epouvante", "/genre/epouvante/"),
|
||||
SelectFilterOption("Fantastique", "/genre/fantastique/"),
|
||||
SelectFilterOption("Fantasy", "/genre/fantasy/"),
|
||||
SelectFilterOption("Mystère", "/genre/mystere/"),
|
||||
SelectFilterOption("Romance", "/genre/romance/"),
|
||||
SelectFilterOption("Shonen", "/genre/shonen/"),
|
||||
SelectFilterOption("Surnaturel", "/genre/surnaturel/"),
|
||||
SelectFilterOption("Sci-Fi", "/genre/sci-fi/"),
|
||||
SelectFilterOption("School life", "/genre/school-life/"),
|
||||
SelectFilterOption("Ninja", "/genre/ninja/"),
|
||||
SelectFilterOption("Seinen", "/genre/seinen/"),
|
||||
SelectFilterOption("Horreur", "/genre/horreur/"),
|
||||
SelectFilterOption("Tranche de vie", "/genre/tranchedevie/"),
|
||||
SelectFilterOption("Psychologique", "/genre/psychologique/")
|
||||
]),
|
||||
if (source.name == "French Anime")
|
||||
SelectFilter("GenresFilter", "Genres", 0, [
|
||||
SelectFilterOption("<Sélectionner>", ""),
|
||||
SelectFilterOption("Animes VF", "/animes-vf/"),
|
||||
SelectFilterOption("Animes VOSTFR", "/animes-vostfr/"),
|
||||
SelectFilterOption("Films VF et VOSTFR", "/films-vf-vostfr/")
|
||||
]),
|
||||
if (source.name == "Wiflix")
|
||||
SelectFilter("CategoriesFilter", "Catégories", 0, [
|
||||
SelectFilterOption("<Sélectionner>", ""),
|
||||
SelectFilterOption("Séries", "/serie-en-streaming/"),
|
||||
SelectFilterOption("Films", "/film-en-streaming/")
|
||||
]),
|
||||
if (source.name == "Wiflix")
|
||||
SelectFilter("GenresFilter", "Genres", 0, [
|
||||
SelectFilterOption("<Sélectionner>", ""),
|
||||
SelectFilterOption("Action", "/film-en-streaming/action/"),
|
||||
SelectFilterOption("Animation", "/film-en-streaming/animation/"),
|
||||
SelectFilterOption(
|
||||
"Arts Martiaux", "/film-en-streaming/arts-martiaux/"),
|
||||
SelectFilterOption("Aventure", "/film-en-streaming/aventure/"),
|
||||
SelectFilterOption("Biopic", "/film-en-streaming/biopic/"),
|
||||
SelectFilterOption("Comédie", "/film-en-streaming/comedie/"),
|
||||
SelectFilterOption(
|
||||
"Comédie Dramatique", "/film-en-streaming/comedie-dramatique/"),
|
||||
SelectFilterOption(
|
||||
"Épouvante Horreur", "/film-en-streaming/horreur/"),
|
||||
SelectFilterOption("Drame", "/film-en-streaming/drame/"),
|
||||
SelectFilterOption(
|
||||
"Documentaire", "/film-en-streaming/documentaire/"),
|
||||
SelectFilterOption("Espionnage", "/film-en-streaming/espionnage/"),
|
||||
SelectFilterOption("Famille", "/film-en-streaming/famille/"),
|
||||
SelectFilterOption("Fantastique", "/film-en-streaming/fantastique/"),
|
||||
SelectFilterOption("Guerre", "/film-en-streaming/guerre/"),
|
||||
SelectFilterOption("Historique", "/film-en-streaming/historique/"),
|
||||
SelectFilterOption("Musical", "/film-en-streaming/musical/"),
|
||||
SelectFilterOption("Policier", "/film-en-streaming/policier/"),
|
||||
SelectFilterOption("Romance", "/film-en-streaming/romance/"),
|
||||
SelectFilterOption(
|
||||
"Science-Fiction", "/film-en-streaming/science-fiction/"),
|
||||
SelectFilterOption("Spectacles", "/film-en-streaming/spectacles/"),
|
||||
SelectFilterOption("Thriller", "/film-en-streaming/thriller/"),
|
||||
SelectFilterOption("Western", "/film-en-streaming/western/"),
|
||||
]),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
DataLifeEngine main(MSource source) {
|
||||
return DataLifeEngine(source: source);
|
||||
}
|
||||
19
dart/anime/multisrc/datalifeengine/sources.dart
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
import '../../../../model/source.dart';
|
||||
import 'src/frenchanime/frenchanime.dart';
|
||||
import 'src/wiflix/wiflix.dart';
|
||||
|
||||
const _datalifeengineVersion = "0.0.6";
|
||||
const _datalifeengineSourceCodeUrl =
|
||||
"https://raw.githubusercontent.com/kodjodevf/mangayomi-extensions/$branchName/dart/anime/multisrc/datalifeengine/datalifeengine.dart";
|
||||
|
||||
List<Source> get datalifeengineSourcesList => _datalifeengineSourcesList;
|
||||
List<Source> _datalifeengineSourcesList = [
|
||||
//French Anime (FR)
|
||||
frenchanimeSource,
|
||||
//Wiflix (FR)
|
||||
wiflixSource,
|
||||
]
|
||||
.map((e) => e
|
||||
..sourceCodeUrl = _datalifeengineSourceCodeUrl
|
||||
..version = _datalifeengineVersion)
|
||||
.toList();
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
import '../../../../../../model/source.dart';
|
||||
|
||||
Source get frenchanimeSource => _frenchanimeSource;
|
||||
|
||||
Source _frenchanimeSource = Source(
|
||||
name: "French Anime",
|
||||
baseUrl: "https://french-anime.com",
|
||||
lang: "fr",
|
||||
typeSource: "datalifeengine",
|
||||
itemType: ItemType.anime,
|
||||
iconUrl:
|
||||
"https://raw.githubusercontent.com/kodjodevf/mangayomi-extensions/$branchName/dart/anime/multisrc/datalifeengine/src/frenchanime/icon.png",
|
||||
);
|
||||
BIN
dart/anime/multisrc/datalifeengine/src/frenchanime/icon.png
Normal file
|
After Width: | Height: | Size: 5.1 KiB |
BIN
dart/anime/multisrc/datalifeengine/src/wiflix/icon.png
Normal file
|
After Width: | Height: | Size: 4.5 KiB |
13
dart/anime/multisrc/datalifeengine/src/wiflix/wiflix.dart
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
import '../../../../../../model/source.dart';
|
||||
|
||||
Source get wiflixSource => _wiflixSource;
|
||||
|
||||
Source _wiflixSource = Source(
|
||||
name: "Wiflix",
|
||||
baseUrl: "https://wiflix-hd.vip",
|
||||
lang: "fr",
|
||||
typeSource: "datalifeengine",
|
||||
itemType: ItemType.anime,
|
||||
iconUrl:
|
||||
"https://raw.githubusercontent.com/kodjodevf/mangayomi-extensions/$branchName/dart/anime/multisrc/datalifeengine/src/wiflix/icon.png",
|
||||
);
|
||||
548
dart/anime/multisrc/dopeflix/dopeflix.dart
Normal file
|
|
@ -0,0 +1,548 @@
|
|||
import 'package:mangayomi/bridge_lib.dart';
|
||||
import 'dart:convert';
|
||||
|
||||
class DopeFlix extends MProvider {
|
||||
DopeFlix({required this.source});
|
||||
|
||||
MSource source;
|
||||
|
||||
final Client client = Client(source);
|
||||
|
||||
@override
|
||||
String get baseUrl => getPreferenceValue(source.id, "preferred_domain");
|
||||
|
||||
@override
|
||||
Future<MPages> getPopular(int page) async {
|
||||
final res = (await client.get(Uri.parse(
|
||||
"$baseUrl/${getPreferenceValue(source.id, "preferred_popular_page")}?page=$page")))
|
||||
.body;
|
||||
return parseAnimeList(res);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<MPages> getLatestUpdates(int page) async {
|
||||
final res = (await client.get(Uri.parse("$baseUrl/home"))).body;
|
||||
List<MManga> animeList = [];
|
||||
final path =
|
||||
'//section[contains(text(),"${getPreferenceValue(source.id, "preferred_latest_page")}")]/div/div[@class="film_list-wrap"]/div[@class="flw-item"]/div[@class="film-poster"]';
|
||||
final urls = xpath(res, '$path/a/@href');
|
||||
final names = xpath(res, '$path/a/@title');
|
||||
final images = xpath(res, '$path/img/@data-src');
|
||||
|
||||
for (var i = 0; i < names.length; i++) {
|
||||
MManga anime = MManga();
|
||||
anime.name = names[i];
|
||||
anime.imageUrl = images[i];
|
||||
anime.link = urls[i];
|
||||
animeList.add(anime);
|
||||
}
|
||||
return MPages(animeList, false);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<MPages> search(String query, int page, FilterList filterList) async {
|
||||
final filters = filterList.filters;
|
||||
String url = "$baseUrl";
|
||||
|
||||
if (query.isNotEmpty) {
|
||||
url += "/search/${query.replaceAll(" ", "-")}?page=$page";
|
||||
} else {
|
||||
url += "/filter/?page=$page";
|
||||
for (var filter in filters) {
|
||||
if (filter.type == "TypeFilter") {
|
||||
final type = filter.values[filter.state].value;
|
||||
url += "${ll(url)}type=$type";
|
||||
} else if (filter.type == "QualityFilter") {
|
||||
final quality = filter.values[filter.state].value;
|
||||
url += "${ll(url)}quality=$quality";
|
||||
} else if (filter.type == "ReleaseYearFilter") {
|
||||
final year = filter.values[filter.state].value;
|
||||
url += "${ll(url)}release_year=$year";
|
||||
} else if (filter.type == "GenresFilter") {
|
||||
final genre = (filter.state as List).where((e) => e.state).toList();
|
||||
if (genre.isNotEmpty) {
|
||||
url += "${ll(url)}genre=";
|
||||
for (var st in genre) {
|
||||
url += "${st.value}-";
|
||||
}
|
||||
}
|
||||
} else if (filter.type == "CountriesFilter") {
|
||||
final country = (filter.state as List).where((e) => e.state).toList();
|
||||
if (country.isNotEmpty) {
|
||||
url += "${ll(url)}country=";
|
||||
for (var st in country) {
|
||||
url += "${st.value}-";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final res = (await client.get(Uri.parse(url))).body;
|
||||
return parseAnimeList(res);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<MManga> getDetail(String url) async {
|
||||
url = getUrlWithoutDomain(url);
|
||||
final res = (await client.get(Uri.parse("$baseUrl$url"))).body;
|
||||
MManga anime = MManga();
|
||||
final description = xpath(res, '//div[@class="description"]/text()');
|
||||
if (description.isNotEmpty) {
|
||||
anime.description = description.first.replaceAll("Overview:", "");
|
||||
}
|
||||
final author = xpath(res, '//div[contains(text(),"Production")]/a/text()');
|
||||
if (author.isNotEmpty) {
|
||||
anime.author = author.first;
|
||||
}
|
||||
anime.genre = xpath(res, '//div[contains(text(),"Genre")]/a/text()');
|
||||
List<MChapter> episodesList = [];
|
||||
final id = xpath(res, '//div[@class="detail_page-watch"]/@data-id').first;
|
||||
final dataType =
|
||||
xpath(res, '//div[@class="detail_page-watch"]/@data-type').first;
|
||||
if (dataType == "1") {
|
||||
MChapter episode = MChapter();
|
||||
episode.name = "Movie";
|
||||
episode.url = "$baseUrl/ajax/movie/episodes/$id";
|
||||
episodesList.add(episode);
|
||||
} else {
|
||||
final resS =
|
||||
(await client.get(Uri.parse("$baseUrl/ajax/v2/tv/seasons/$id"))).body;
|
||||
|
||||
final seasonIds =
|
||||
xpath(resS, '//a[@class="dropdown-item ss-item"]/@data-id');
|
||||
final seasonNames =
|
||||
xpath(resS, '//a[@class="dropdown-item ss-item"]/text()');
|
||||
for (int i = 0; i < seasonIds.length; i++) {
|
||||
final seasonId = seasonIds[i];
|
||||
final seasonName = seasonNames[i];
|
||||
|
||||
final html = (await client
|
||||
.get(Uri.parse("$baseUrl/ajax/v2/season/episodes/$seasonId")))
|
||||
.body;
|
||||
|
||||
final epsHtmls = parseHtml(html).select("div.eps-item");
|
||||
|
||||
for (var epH in epsHtmls) {
|
||||
final epHtml = epH.outerHtml;
|
||||
final episodeId =
|
||||
xpath(epHtml, '//div[contains(@class,"eps-item")]/@data-id')
|
||||
.first;
|
||||
final epNum =
|
||||
xpath(epHtml, '//div[@class="episode-number"]/text()').first;
|
||||
final epName = xpath(epHtml, '//h3[@class="film-name"]/text()').first;
|
||||
MChapter episode = MChapter();
|
||||
episode.name = "$seasonName $epNum $epName";
|
||||
episode.url = "$baseUrl/ajax/v2/episode/servers/$episodeId";
|
||||
episodesList.add(episode);
|
||||
}
|
||||
}
|
||||
}
|
||||
anime.chapters = episodesList.reversed.toList();
|
||||
return anime;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<MVideo>> getVideoList(String url) async {
|
||||
url = getUrlWithoutDomain(url);
|
||||
final res = (await client.get(Uri.parse("$baseUrl/$url"))).body;
|
||||
|
||||
final vidsHtmls = parseHtml(res).select("ul.fss-list a.btn-play");
|
||||
|
||||
List<MVideo> videos = [];
|
||||
for (var vidH in vidsHtmls) {
|
||||
final vidHtml = vidH.outerHtml;
|
||||
final id = xpath(vidHtml, '//a/@data-id').first;
|
||||
final name = xpath(vidHtml, '//span/text()').first;
|
||||
final resSource =
|
||||
(await client.get(Uri.parse("$baseUrl/ajax/sources/$id"))).body;
|
||||
|
||||
final vidUrl =
|
||||
substringBefore(substringAfter(resSource, "\"link\":\""), "\"");
|
||||
List<MVideo> a = [];
|
||||
String masterUrl = "";
|
||||
String type = "";
|
||||
if (name.contains("DoodStream")) {
|
||||
a = await doodExtractor(vidUrl, "DoodStream");
|
||||
} else if (["Vidcloud", "UpCloud"].contains(name)) {
|
||||
final id = substringBefore(substringAfter(vidUrl, "/embed-4/"), "?");
|
||||
final serverUrl = substringBefore(vidUrl, "/embed");
|
||||
|
||||
final resServer = (await client.get(
|
||||
Uri.parse("$serverUrl/ajax/embed-4/getSources?id=$id"),
|
||||
headers: {"X-Requested-With": "XMLHttpRequest"}))
|
||||
.body;
|
||||
final encrypted = getMapValue(resServer, "encrypted");
|
||||
|
||||
String videoResJson = "";
|
||||
if (encrypted == "true") {
|
||||
final ciphered = getMapValue(resServer, "sources");
|
||||
|
||||
List<List<int>> indexPairs = await generateIndexPairs();
|
||||
|
||||
var password = '';
|
||||
String ciphertext = ciphered;
|
||||
int index = 0;
|
||||
for (List<int> item in json.decode(json.encode(indexPairs))) {
|
||||
int start = item.first + index;
|
||||
int end = start + item.last;
|
||||
String passSubstr = ciphered.substring(start, end);
|
||||
password += passSubstr;
|
||||
ciphertext = ciphertext.replaceFirst(passSubstr, "");
|
||||
index += item.last;
|
||||
}
|
||||
videoResJson = decryptAESCryptoJS(ciphertext, password);
|
||||
masterUrl = ((json.decode(videoResJson) as List<Map<String, dynamic>>)
|
||||
.first)['file'];
|
||||
|
||||
type = ((json.decode(videoResJson) as List<Map<String, dynamic>>)
|
||||
.first)['type'];
|
||||
} else {
|
||||
masterUrl =
|
||||
((json.decode(resServer)["sources"] as List<Map<String, dynamic>>)
|
||||
.first)['file'];
|
||||
|
||||
type =
|
||||
((json.decode(resServer)["sources"] as List<Map<String, dynamic>>)
|
||||
.first)['type'];
|
||||
}
|
||||
|
||||
final tracks = (json.decode(resServer)['tracks'] as List)
|
||||
.where((e) => e['kind'] == 'captions' ? true : false)
|
||||
.toList();
|
||||
List<MTrack> subtitles = [];
|
||||
|
||||
for (var sub in tracks) {
|
||||
try {
|
||||
MTrack subtitle = MTrack();
|
||||
subtitle
|
||||
..label = sub["label"]
|
||||
..file = sub["file"];
|
||||
subtitles.add(subtitle);
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
subtitles = sortSubs(subtitles, source.id);
|
||||
if (type == "hls") {
|
||||
final masterPlaylistRes =
|
||||
(await client.get(Uri.parse(masterUrl))).body;
|
||||
|
||||
for (var it in substringAfter(masterPlaylistRes, "#EXT-X-STREAM-INF:")
|
||||
.split("#EXT-X-STREAM-INF:")) {
|
||||
final quality =
|
||||
"${substringBefore(substringBefore(substringAfter(substringAfter(it, "RESOLUTION="), "x"), ","), "\n")}p";
|
||||
|
||||
String videoUrl = substringBefore(substringAfter(it, "\n"), "\n");
|
||||
|
||||
if (!videoUrl.startsWith("http")) {
|
||||
videoUrl =
|
||||
"${(masterUrl as String).split("/").sublist(0, (masterUrl as String).split("/").length - 1).join("/")}/$videoUrl";
|
||||
}
|
||||
|
||||
MVideo video = MVideo();
|
||||
video
|
||||
..url = videoUrl
|
||||
..originalUrl = videoUrl
|
||||
..quality = "$name - $quality"
|
||||
..subtitles = subtitles;
|
||||
a.add(video);
|
||||
}
|
||||
} else {
|
||||
MVideo video = MVideo();
|
||||
video
|
||||
..url = masterUrl
|
||||
..originalUrl = masterUrl
|
||||
..quality = "$name - Default"
|
||||
..subtitles = subtitles;
|
||||
a.add(video);
|
||||
}
|
||||
}
|
||||
videos.addAll(a);
|
||||
}
|
||||
|
||||
return sortVideos(videos, source.id);
|
||||
}
|
||||
|
||||
Future<List<List<int>>> generateIndexPairs() async {
|
||||
final res = (await client.get(Uri.parse(
|
||||
"https://rabbitstream.net/js/player/prod/e4-player.min.js")))
|
||||
.body;
|
||||
|
||||
String script = substringBefore(substringAfter(res, "const "), "()");
|
||||
script = script.substring(0, script.lastIndexOf(','));
|
||||
final list = script
|
||||
.split(",")
|
||||
.map((String e) {
|
||||
String value = substringAfter(e, "=");
|
||||
if (value.contains("0x")) {
|
||||
return int.parse(substringAfter(value, "0x"), radix: 16);
|
||||
} else {
|
||||
return int.parse(value);
|
||||
}
|
||||
})
|
||||
.toList()
|
||||
.skip(1)
|
||||
.toList();
|
||||
return chunked(list, 2)
|
||||
.map((List<int> list) => list.reversed.toList())
|
||||
.toList();
|
||||
}
|
||||
|
||||
List<List<int>> chunked(List<int> list, int size) {
|
||||
List<List<int>> chunks = [];
|
||||
for (int i = 0; i < list.length; i += size) {
|
||||
int end = list.length;
|
||||
if (i + size < list.length) {
|
||||
end = i + size;
|
||||
}
|
||||
chunks.add(list.sublist(i, end));
|
||||
}
|
||||
return chunks;
|
||||
}
|
||||
|
||||
MPages parseAnimeList(String res) {
|
||||
List<MManga> animeList = [];
|
||||
final path =
|
||||
'//div[@class="film_list-wrap"]/div[@class="flw-item"]/div[@class="film-poster"]';
|
||||
final urls = xpath(res, '$path/a/@href');
|
||||
final names = xpath(res, '$path/a/@title');
|
||||
final images = xpath(res, '$path/img/@data-src');
|
||||
|
||||
for (var i = 0; i < names.length; i++) {
|
||||
MManga anime = MManga();
|
||||
anime.name = names[i];
|
||||
anime.imageUrl = images[i];
|
||||
anime.link = urls[i];
|
||||
animeList.add(anime);
|
||||
}
|
||||
final pages = xpath(
|
||||
res, '//ul[contains(@class,"pagination")]/li/a[@title="Next"]/@title');
|
||||
return MPages(animeList, pages.isNotEmpty);
|
||||
}
|
||||
|
||||
@override
|
||||
List<dynamic> getFilterList() {
|
||||
return [
|
||||
SelectFilter("TypeFilter", "Type", 0, [
|
||||
SelectFilterOption("All", "all"),
|
||||
SelectFilterOption("Movies", "movies"),
|
||||
SelectFilterOption("TV Shows", "tv")
|
||||
]),
|
||||
SelectFilter("QualityFilter", "Quality", 0, [
|
||||
SelectFilterOption("All", "all"),
|
||||
SelectFilterOption("HD", "HD"),
|
||||
SelectFilterOption("SD", "SD"),
|
||||
SelectFilterOption("CAM", "CAM")
|
||||
]),
|
||||
SelectFilter("ReleaseYearFilter", "Released at", 0, [
|
||||
SelectFilterOption("All", "all"),
|
||||
SelectFilterOption("2024", "2024"),
|
||||
SelectFilterOption("2023", "2023"),
|
||||
SelectFilterOption("2022", "2022"),
|
||||
SelectFilterOption("2021", "2021"),
|
||||
SelectFilterOption("2020", "2020"),
|
||||
SelectFilterOption("2019", "2019"),
|
||||
SelectFilterOption("2018", "2018"),
|
||||
SelectFilterOption("Older", "older-2018")
|
||||
]),
|
||||
SeparatorFilter(),
|
||||
GroupFilter("GenresFilter", "Genre", [
|
||||
CheckBoxFilter("Action", "10"),
|
||||
CheckBoxFilter("Action & Adventure", "24"),
|
||||
CheckBoxFilter("Adventure", "18"),
|
||||
CheckBoxFilter("Animation", "3"),
|
||||
CheckBoxFilter("Biography", "37"),
|
||||
CheckBoxFilter("Comedy", "7"),
|
||||
CheckBoxFilter("Crime", "2"),
|
||||
CheckBoxFilter("Documentary", "11"),
|
||||
CheckBoxFilter("Drama", "4"),
|
||||
CheckBoxFilter("Family", "9"),
|
||||
CheckBoxFilter("Fantasy", "13"),
|
||||
CheckBoxFilter("History", "19"),
|
||||
CheckBoxFilter("Horror", "14"),
|
||||
CheckBoxFilter("Kids", "27"),
|
||||
CheckBoxFilter("Music", "15"),
|
||||
CheckBoxFilter("Mystery", "1"),
|
||||
CheckBoxFilter("News", "34"),
|
||||
CheckBoxFilter("Reality", "22"),
|
||||
CheckBoxFilter("Romance", "12"),
|
||||
CheckBoxFilter("Sci-Fi & Fantasy", "31"),
|
||||
CheckBoxFilter("Science Fiction", "5"),
|
||||
CheckBoxFilter("Soap", "35"),
|
||||
CheckBoxFilter("Talk", "29"),
|
||||
CheckBoxFilter("Thriller", "16"),
|
||||
CheckBoxFilter("TV Movie", "8"),
|
||||
CheckBoxFilter("War", "17"),
|
||||
CheckBoxFilter("War & Politics", "28"),
|
||||
CheckBoxFilter("Western", "6")
|
||||
]),
|
||||
GroupFilter("CountriesFilter", "Countries", [
|
||||
CheckBoxFilter("Argentina", "11"),
|
||||
CheckBoxFilter("Australia", "151"),
|
||||
CheckBoxFilter("Austria", "4"),
|
||||
CheckBoxFilter("Belgium", "44"),
|
||||
CheckBoxFilter("Brazil", "190"),
|
||||
CheckBoxFilter("Canada", "147"),
|
||||
CheckBoxFilter("China", "101"),
|
||||
CheckBoxFilter("Czech Republic", "231"),
|
||||
CheckBoxFilter("Denmark", "222"),
|
||||
CheckBoxFilter("Finland", "158"),
|
||||
CheckBoxFilter("France", "3"),
|
||||
CheckBoxFilter("Germany", "96"),
|
||||
CheckBoxFilter("Hong Kong", "93"),
|
||||
CheckBoxFilter("Hungary", "72"),
|
||||
CheckBoxFilter("India", "105"),
|
||||
CheckBoxFilter("Ireland", "196"),
|
||||
CheckBoxFilter("Israel", "24"),
|
||||
CheckBoxFilter("Italy", "205"),
|
||||
CheckBoxFilter("Japan", "173"),
|
||||
CheckBoxFilter("Luxembourg", "91"),
|
||||
CheckBoxFilter("Mexico", "40"),
|
||||
CheckBoxFilter("Netherlands", "172"),
|
||||
CheckBoxFilter("New Zealand", "122"),
|
||||
CheckBoxFilter("Norway", "219"),
|
||||
CheckBoxFilter("Poland", "23"),
|
||||
CheckBoxFilter("Romania", "170"),
|
||||
CheckBoxFilter("Russia", "109"),
|
||||
CheckBoxFilter("South Africa", "200"),
|
||||
CheckBoxFilter("South Korea", "135"),
|
||||
CheckBoxFilter("Spain", "62"),
|
||||
CheckBoxFilter("Sweden", "114"),
|
||||
CheckBoxFilter("Switzerland", "41"),
|
||||
CheckBoxFilter("Taiwan", "119"),
|
||||
CheckBoxFilter("Thailand", "57"),
|
||||
CheckBoxFilter("United Kingdom", "180"),
|
||||
CheckBoxFilter("United States of America", "129")
|
||||
]),
|
||||
];
|
||||
}
|
||||
|
||||
@override
|
||||
List<dynamic> getSourcePreferences() {
|
||||
return [
|
||||
if (source.name == "DopeBox")
|
||||
ListPreference(
|
||||
key: "preferred_domain",
|
||||
title: "Preferred domain",
|
||||
summary: "",
|
||||
valueIndex: 0,
|
||||
entries: ["dopebox.to", "dopebox.se"],
|
||||
entryValues: ["https://dopebox.to", "https://dopebox.se"]),
|
||||
if (source.name == "SFlix")
|
||||
ListPreference(
|
||||
key: "preferred_domain",
|
||||
title: "Preferred domain",
|
||||
summary: "",
|
||||
valueIndex: 0,
|
||||
entries: ["sflix.to", "sflix.se"],
|
||||
entryValues: ["https://sflix.to", "https://sflix.se"]),
|
||||
ListPreference(
|
||||
key: "preferred_quality",
|
||||
title: "Preferred Quality",
|
||||
summary: "",
|
||||
valueIndex: 0,
|
||||
entries: ["1080p", "720p", "480p", "360p"],
|
||||
entryValues: ["1080p", "720p", "480p", "360p"]),
|
||||
ListPreference(
|
||||
key: "preferred_subLang",
|
||||
title: "Preferred sub language",
|
||||
summary: "",
|
||||
valueIndex: 1,
|
||||
entries: [
|
||||
"Arabic",
|
||||
"English",
|
||||
"French",
|
||||
"German",
|
||||
"Hungarian",
|
||||
"Italian",
|
||||
"Japanese",
|
||||
"Portuguese",
|
||||
"Romanian",
|
||||
"Russian",
|
||||
"Spanish"
|
||||
],
|
||||
entryValues: [
|
||||
"Arabic",
|
||||
"English",
|
||||
"French",
|
||||
"German",
|
||||
"Hungarian",
|
||||
"Italian",
|
||||
"Japanese",
|
||||
"Portuguese",
|
||||
"Romanian",
|
||||
"Russian",
|
||||
"Spanish"
|
||||
]),
|
||||
ListPreference(
|
||||
key: "preferred_latest_page",
|
||||
title: "Preferred latest page",
|
||||
summary: "",
|
||||
valueIndex: 0,
|
||||
entries: ["Movies", "TV Shows"],
|
||||
entryValues: ["Latest Movies", "Latest TV Shows"]),
|
||||
ListPreference(
|
||||
key: "preferred_popular_page",
|
||||
title: "Preferred popular page",
|
||||
summary: "",
|
||||
valueIndex: 0,
|
||||
entries: ["Movies", "TV Shows"],
|
||||
entryValues: ["movie", "tv-show"]),
|
||||
];
|
||||
}
|
||||
|
||||
List<MVideo> sortVideos(List<MVideo> videos, int sourceId) {
|
||||
String quality = getPreferenceValue(sourceId, "preferred_quality");
|
||||
|
||||
videos.sort((MVideo a, MVideo b) {
|
||||
int qualityMatchA = 0;
|
||||
if (a.quality.contains(quality)) {
|
||||
qualityMatchA = 1;
|
||||
}
|
||||
int qualityMatchB = 0;
|
||||
if (b.quality.contains(quality)) {
|
||||
qualityMatchB = 1;
|
||||
}
|
||||
if (qualityMatchA != qualityMatchB) {
|
||||
return qualityMatchB - qualityMatchA;
|
||||
}
|
||||
|
||||
final regex = RegExp(r'(\d+)p');
|
||||
final matchA = regex.firstMatch(a.quality);
|
||||
final matchB = regex.firstMatch(b.quality);
|
||||
final int qualityNumA = int.tryParse(matchA?.group(1) ?? '0') ?? 0;
|
||||
final int qualityNumB = int.tryParse(matchB?.group(1) ?? '0') ?? 0;
|
||||
return qualityNumB - qualityNumA;
|
||||
});
|
||||
|
||||
return videos;
|
||||
}
|
||||
|
||||
List<MTrack> sortSubs(List<MTrack> subs, int sourceId) {
|
||||
String lang = getPreferenceValue(sourceId, "preferred_subLang");
|
||||
|
||||
subs.sort((MTrack a, MTrack b) {
|
||||
int langMatchA = 0;
|
||||
if (a.label.toLowerCase().contains(lang.toLowerCase())) {
|
||||
langMatchA = 1;
|
||||
}
|
||||
int langMatchB = 0;
|
||||
if (b.label.toLowerCase().contains(lang.toLowerCase())) {
|
||||
langMatchB = 1;
|
||||
}
|
||||
return langMatchB - langMatchA;
|
||||
});
|
||||
return subs;
|
||||
}
|
||||
|
||||
String ll(String url) {
|
||||
if (url.contains("?")) {
|
||||
return "&";
|
||||
}
|
||||
return "?";
|
||||
}
|
||||
}
|
||||
|
||||
DopeFlix main(MSource source) {
|
||||
return DopeFlix(source: source);
|
||||
}
|
||||
19
dart/anime/multisrc/dopeflix/sources.dart
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
import '../../../../model/source.dart';
|
||||
import 'src/dopebox/dopebox.dart';
|
||||
import 'src/sflix/sflix.dart';
|
||||
|
||||
const _dopeflixVersion = "0.0.55";
|
||||
const _dopeflixSourceCodeUrl =
|
||||
"https://raw.githubusercontent.com/kodjodevf/mangayomi-extensions/$branchName/dart/anime/multisrc/dopeflix/dopeflix.dart";
|
||||
|
||||
List<Source> get dopeflixSourcesList => _dopeflixSourcesList;
|
||||
List<Source> _dopeflixSourcesList = [
|
||||
//DopeBox (EN)
|
||||
dopeboxSource,
|
||||
//SFlix (EN)
|
||||
sflixSource,
|
||||
]
|
||||
.map((e) => e
|
||||
..sourceCodeUrl = _dopeflixSourceCodeUrl
|
||||
..version = _dopeflixVersion)
|
||||
.toList();
|
||||
13
dart/anime/multisrc/dopeflix/src/dopebox/dopebox.dart
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
import '../../../../../../model/source.dart';
|
||||
|
||||
Source get dopeboxSource => _dopeboxSource;
|
||||
|
||||
Source _dopeboxSource = Source(
|
||||
name: "DopeBox",
|
||||
baseUrl: "https://dopebox.to",
|
||||
lang: "en",
|
||||
typeSource: "dopeflix",
|
||||
itemType: ItemType.anime,
|
||||
iconUrl:
|
||||
"https://raw.githubusercontent.com/kodjodevf/mangayomi-extensions/$branchName/dart/anime/multisrc/dopeflix/src/dopebox/icon.png",
|
||||
);
|
||||
BIN
dart/anime/multisrc/dopeflix/src/dopebox/icon.png
Normal file
|
After Width: | Height: | Size: 4 KiB |
BIN
dart/anime/multisrc/dopeflix/src/sflix/icon.png
Normal file
|
After Width: | Height: | Size: 6.6 KiB |
13
dart/anime/multisrc/dopeflix/src/sflix/sflix.dart
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
import '../../../../../../model/source.dart';
|
||||
|
||||
Source get sflixSource => _sflixSource;
|
||||
|
||||
Source _sflixSource = Source(
|
||||
name: "SFlix",
|
||||
baseUrl: "https://sflix.to",
|
||||
lang: "en",
|
||||
typeSource: "dopeflix",
|
||||
itemType: ItemType.anime,
|
||||
iconUrl:
|
||||
"https://raw.githubusercontent.com/kodjodevf/mangayomi-extensions/$branchName/dart/anime/multisrc/dopeflix/src/sflix/icon.png",
|
||||
);
|
||||
20
dart/anime/multisrc/zorotheme/sources.dart
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
import '../../../../model/source.dart';
|
||||
import 'src/hianime/hianime.dart';
|
||||
import 'src/kaido/kaido.dart';
|
||||
|
||||
const _zorothemeVersion = "0.1.2";
|
||||
const _zorothemeSourceCodeUrl =
|
||||
"https://raw.githubusercontent.com/kodjodevf/mangayomi-extensions/$branchName/dart/anime/multisrc/zorotheme/zorotheme.dart";
|
||||
|
||||
List<Source> get zorothemeSourcesList => _zorothemeSourcesList;
|
||||
List<Source> _zorothemeSourcesList = [
|
||||
//AniWatch.to (EN)
|
||||
aniwatchSource,
|
||||
//Kaido.to (EN)
|
||||
kaidoSource,
|
||||
]
|
||||
.map((e) => e
|
||||
..sourceCodeUrl = _zorothemeSourceCodeUrl
|
||||
..appMinVerReq = "0.4.0"
|
||||
..version = _zorothemeVersion)
|
||||
.toList();
|
||||
14
dart/anime/multisrc/zorotheme/src/hianime/hianime.dart
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
import '../../../../../../model/source.dart';
|
||||
|
||||
Source get aniwatchSource => _aniwatchSource;
|
||||
|
||||
Source _aniwatchSource = Source(
|
||||
id: 814067600,
|
||||
name: "HiAnime",
|
||||
baseUrl: "https://hianime.to",
|
||||
itemType: ItemType.anime,
|
||||
lang: "en",
|
||||
typeSource: "zorotheme",
|
||||
iconUrl:
|
||||
"https://raw.githubusercontent.com/kodjodevf/mangayomi-extensions/$branchName/dart/anime/multisrc/zorotheme/src/hianime/icon.png",
|
||||
);
|
||||
BIN
dart/anime/multisrc/zorotheme/src/hianime/icon.png
Normal file
|
After Width: | Height: | Size: 5 KiB |
BIN
dart/anime/multisrc/zorotheme/src/kaido/icon.png
Normal file
|
After Width: | Height: | Size: 23 KiB |
13
dart/anime/multisrc/zorotheme/src/kaido/kaido.dart
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
import '../../../../../../model/source.dart';
|
||||
|
||||
Source get kaidoSource => _kaidoSource;
|
||||
|
||||
Source _kaidoSource = Source(
|
||||
name: "Kaido.to",
|
||||
baseUrl: "https://kaido.to",
|
||||
lang: "en",
|
||||
itemType: ItemType.anime,
|
||||
typeSource: "zorotheme",
|
||||
iconUrl:
|
||||
"https://raw.githubusercontent.com/kodjodevf/mangayomi-extensions/$branchName/dart/anime/multisrc/zorotheme/src/kaido/icon.png",
|
||||
);
|
||||
1979
dart/anime/multisrc/zorotheme/zorotheme.dart
Normal file
396
dart/anime/src/all/animeworldindia/animeworldindia.dart
Normal file
|
|
@ -0,0 +1,396 @@
|
|||
import 'package:mangayomi/bridge_lib.dart';
|
||||
import 'dart:convert';
|
||||
|
||||
class AnimeWorldIndia extends MProvider {
|
||||
AnimeWorldIndia({required this.source});
|
||||
|
||||
MSource source;
|
||||
|
||||
final Client client = Client(source);
|
||||
|
||||
@override
|
||||
Future<MPages> getPopular(int page) async {
|
||||
final res = (await client.get(Uri.parse(
|
||||
"${source.baseUrl}/advanced-search/page/$page/?s_lang=${source.lang}&s_orderby=viewed")))
|
||||
.body;
|
||||
|
||||
return parseAnimeList(res);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<MPages> getLatestUpdates(int page) async {
|
||||
final res = (await client.get(Uri.parse(
|
||||
"${source.baseUrl}/advanced-search/page/$page/?s_lang=${source.lang}&s_orderby=update")))
|
||||
.body;
|
||||
|
||||
return parseAnimeList(res);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<MPages> search(String query, int page, FilterList filterList) async {
|
||||
final filters = filterList.filters;
|
||||
String url =
|
||||
"${source.baseUrl}/advanced-search/page/$page/?s_keyword=$query&s_lang=${source.lang}";
|
||||
for (var filter in filters) {
|
||||
if (filter.type == "TypeFilter") {
|
||||
final type = filter.values[filter.state].value;
|
||||
url += "${ll(url)}s_type=$type";
|
||||
} else if (filter.type == "StatusFilter") {
|
||||
final status = filter.values[filter.state].value;
|
||||
url += "${ll(url)}s_status=$status";
|
||||
} else if (filter.type == "StyleFilter") {
|
||||
final style = filter.values[filter.state].value;
|
||||
url += "${ll(url)}s_sub_type=$style";
|
||||
} else if (filter.type == "YearFilter") {
|
||||
final year = filter.values[filter.state].value;
|
||||
url += "${ll(url)}s_year=$year";
|
||||
} else if (filter.type == "SortFilter") {
|
||||
final sort = filter.values[filter.state].value;
|
||||
url += "${ll(url)}s_orderby=$sort";
|
||||
} else if (filter.type == "GenresFilter") {
|
||||
final genre = (filter.state as List).where((e) => e.state).toList();
|
||||
url += "${ll(url)}s_genre=";
|
||||
if (genre.isNotEmpty) {
|
||||
for (var st in genre) {
|
||||
String value = st.value;
|
||||
url += value.toLowerCase().replaceAll(" ", "-");
|
||||
if (genre.length > 1) {
|
||||
url += "%2C";
|
||||
}
|
||||
}
|
||||
if (genre.length > 1) {
|
||||
url = substringBeforeLast(url, '%2C');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final res = (await client.get(Uri.parse(url))).body;
|
||||
return parseAnimeList(res);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<MManga> getDetail(String url) async {
|
||||
final res = (await client.get(Uri.parse(url))).body;
|
||||
MManga anime = MManga();
|
||||
final document = parseHtml(res);
|
||||
final isMovie =
|
||||
document.xpath('//li/a[contains(text(),"Movie")]/text()').isNotEmpty;
|
||||
if (isMovie) {
|
||||
anime.status = MStatus.completed;
|
||||
} else {
|
||||
final eps = xpath(
|
||||
res, '//ul/li/a[contains(@href,"${source.baseUrl}/watch")]/text()');
|
||||
if (eps.isNotEmpty) {
|
||||
final epParts = eps.first
|
||||
.substring(3)
|
||||
.replaceAll(" ", "")
|
||||
.replaceAll("\n", "")
|
||||
.split('/');
|
||||
if (epParts.length == 2) {
|
||||
if (epParts[0].compareTo(epParts[1]) == 0) {
|
||||
anime.status = MStatus.completed;
|
||||
} else {
|
||||
anime.status = MStatus.ongoing;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
anime.description = document.selectFirst("div[data-synopsis]")?.text ?? "";
|
||||
anime.author = document
|
||||
.xpath('//li[contains(text(),"Producers:")]/span/a/text()')
|
||||
.join(', ');
|
||||
anime.genre = document.xpath(
|
||||
'//span[@class="leading-6"]/a[contains(@class,"border-opacity-30")]/text()');
|
||||
final seasonsJson = json.decode(substringBeforeLast(
|
||||
substringBefore(
|
||||
substringAfter(res, "var season_list = "), "var season_label ="),
|
||||
";")) as List<Map<String, dynamic>>;
|
||||
bool isSingleSeason = seasonsJson.length == 1;
|
||||
List<MChapter>? episodesList = [];
|
||||
for (var i = 0; i < seasonsJson.length; i++) {
|
||||
final seasonJson = seasonsJson[i];
|
||||
final seasonName = isSingleSeason ? "" : "Season ${i + 1}";
|
||||
final episodesJson =
|
||||
(seasonJson["episodes"]["all"] as List<Map<String, dynamic>>)
|
||||
.reversed
|
||||
.toList();
|
||||
for (var j = 0; j < episodesJson.length; j++) {
|
||||
final episodeJson = episodesJson[j];
|
||||
final episodeTitle = episodeJson["metadata"]["title"] ?? "";
|
||||
String episodeName = "";
|
||||
if (isMovie) {
|
||||
episodeName = "Movie";
|
||||
} else {
|
||||
if (seasonName.isNotEmpty) {
|
||||
episodeName = "$seasonName - ";
|
||||
}
|
||||
episodeName += "Episode ${j + 1} ";
|
||||
if (episodeTitle.isNotEmpty) {
|
||||
episodeName += "- $episodeTitle";
|
||||
}
|
||||
}
|
||||
MChapter episode = MChapter();
|
||||
episode.name = episodeName;
|
||||
|
||||
episode.dateUpload =
|
||||
"${int.parse(episodeJson["metadata"]["released"] ?? "0") * 1000}";
|
||||
episode.url = "/wp-json/kiranime/v1/episode?id=${episodeJson["id"]}";
|
||||
episodesList.add(episode);
|
||||
}
|
||||
}
|
||||
|
||||
anime.chapters = episodesList.reversed.toList();
|
||||
return anime;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<MVideo>> getVideoList(String url) async {
|
||||
final res = (await client.get(Uri.parse("${source.baseUrl}$url"))).body;
|
||||
var resJson = substringBefore(
|
||||
substringAfterLast(res, "\"players\":"), ",\"noplayer\":");
|
||||
var streams = (json.decode(resJson) as List<Map<String, dynamic>>)
|
||||
.where((e) =>
|
||||
(e["type"] == "stream" ? true : false) &&
|
||||
(e["url"] as String).isNotEmpty)
|
||||
.toList()
|
||||
.where((e) => language(source.lang).isEmpty ||
|
||||
language(source.lang) == e["language"]
|
||||
? true
|
||||
: false)
|
||||
.toList();
|
||||
List<MVideo> videos = [];
|
||||
for (var stream in streams) {
|
||||
String videoUrl = stream["url"];
|
||||
final language = stream["language"];
|
||||
final video = await mystreamExtractor(videoUrl, language);
|
||||
videos.addAll(video);
|
||||
}
|
||||
|
||||
return sortVideos(videos, source.id);
|
||||
}
|
||||
|
||||
MPages parseAnimeList(String res) {
|
||||
List<MManga> animeList = [];
|
||||
final document = parseHtml(res);
|
||||
|
||||
for (var element in document.select("div.col-span-1")) {
|
||||
MManga anime = MManga();
|
||||
anime.name =
|
||||
element.selectFirst("div.font-medium.line-clamp-2.mb-3").text;
|
||||
anime.link = element.selectFirst("a").getHref;
|
||||
anime.imageUrl =
|
||||
"${source.baseUrl}${getUrlWithoutDomain(element.selectFirst("img").getSrc)}";
|
||||
animeList.add(anime);
|
||||
}
|
||||
final hasNextPage = xpath(res,
|
||||
'//li/span[@class="page-numbers current"]/parent::li//following-sibling::li/a/@href')
|
||||
.isNotEmpty;
|
||||
return MPages(animeList, hasNextPage);
|
||||
}
|
||||
|
||||
String language(String lang) {
|
||||
final languages = {
|
||||
"all": "",
|
||||
"bn": "bengali",
|
||||
"en": "english",
|
||||
"hi": "hindi",
|
||||
"ja": "japanese",
|
||||
"ml": "malayalam",
|
||||
"mr": "marathi",
|
||||
"ta": "tamil",
|
||||
"te": "telugu"
|
||||
};
|
||||
return languages[lang] ?? "";
|
||||
}
|
||||
|
||||
Future<List<MVideo>> mystreamExtractor(String url, String language) async {
|
||||
List<MVideo> videos = [];
|
||||
final res = (await client.get(Uri.parse(url))).body;
|
||||
final streamCode = substringBefore(
|
||||
substringAfter(substringAfter(res, "sniff("), ", \""), '"');
|
||||
|
||||
final streamUrl =
|
||||
"${substringBefore(url, "/watch")}/m3u8/$streamCode/master.txt?s=1&cache=1";
|
||||
final masterPlaylistRes = (await client.get(Uri.parse(streamUrl))).body;
|
||||
|
||||
List<MTrack> audios = [];
|
||||
for (var it in substringAfter(masterPlaylistRes, "#EXT-X-MEDIA:TYPE=AUDIO")
|
||||
.split("#EXT-X-MEDIA:TYPE=AUDIO")) {
|
||||
final line =
|
||||
substringBefore(substringAfter(it, "#EXT-X-MEDIA:TYPE=AUDIO"), "\n");
|
||||
final audioUrl = substringBefore(substringAfter(line, "URI=\""), "\"");
|
||||
MTrack audio = MTrack();
|
||||
audio
|
||||
..label = substringBefore(substringAfter(line, "NAME=\""), "\"")
|
||||
..file = audioUrl;
|
||||
audios.add(audio);
|
||||
}
|
||||
|
||||
for (var it in substringAfter(masterPlaylistRes, "#EXT-X-STREAM-INF:")
|
||||
.split("#EXT-X-STREAM-INF:")) {
|
||||
final quality =
|
||||
"${substringBefore(substringBefore(substringAfter(substringAfter(it, "RESOLUTION="), "x"), ","), "\n")}p";
|
||||
|
||||
String videoUrl = substringBefore(substringAfter(it, "\n"), "\n");
|
||||
|
||||
MVideo video = MVideo();
|
||||
video
|
||||
..url = videoUrl
|
||||
..originalUrl = videoUrl
|
||||
..quality = "[$language] MyStream - $quality"
|
||||
..audios = audios;
|
||||
videos.add(video);
|
||||
}
|
||||
return videos;
|
||||
}
|
||||
|
||||
@override
|
||||
List<dynamic> getFilterList() {
|
||||
return [
|
||||
SelectFilter("TypeFilter", "Type", 0, [
|
||||
SelectFilterOption("Any", "all"),
|
||||
SelectFilterOption("TV", "tv"),
|
||||
SelectFilterOption("Movie", "movies"),
|
||||
]),
|
||||
SelectFilter("StatusFilter", "Status", 0, [
|
||||
SelectFilterOption("Any", "all"),
|
||||
SelectFilterOption("Currently Airing", "airing"),
|
||||
SelectFilterOption("Finished Airing", "completed"),
|
||||
]),
|
||||
SelectFilter("StyleFilter", "Style", 0, [
|
||||
SelectFilterOption("Any", "all"),
|
||||
SelectFilterOption("Anime", "anime"),
|
||||
SelectFilterOption("Cartoon", "cartoon"),
|
||||
]),
|
||||
SelectFilter("YearFilter", "Year", 0, [
|
||||
SelectFilterOption("Any", "all"),
|
||||
SelectFilterOption("2024", "2024"),
|
||||
SelectFilterOption("2023", "2023"),
|
||||
SelectFilterOption("2022", "2022"),
|
||||
SelectFilterOption("2021", "2021"),
|
||||
SelectFilterOption("2020", "2020"),
|
||||
SelectFilterOption("2019", "2019"),
|
||||
SelectFilterOption("2018", "2018"),
|
||||
SelectFilterOption("2017", "2017"),
|
||||
SelectFilterOption("2016", "2016"),
|
||||
SelectFilterOption("2015", "2015"),
|
||||
SelectFilterOption("2014", "2014"),
|
||||
SelectFilterOption("2013", "2013"),
|
||||
SelectFilterOption("2012", "2012"),
|
||||
SelectFilterOption("2011", "2011"),
|
||||
SelectFilterOption("2010", "2010"),
|
||||
SelectFilterOption("2009", "2009"),
|
||||
SelectFilterOption("2008", "2008"),
|
||||
SelectFilterOption("2007", "2007"),
|
||||
SelectFilterOption("2006", "2006"),
|
||||
SelectFilterOption("2005", "2005"),
|
||||
SelectFilterOption("2004", "2004"),
|
||||
SelectFilterOption("2003", "2003"),
|
||||
SelectFilterOption("2002", "2002"),
|
||||
SelectFilterOption("2001", "2001"),
|
||||
SelectFilterOption("2000", "2000"),
|
||||
SelectFilterOption("1999", "1999"),
|
||||
SelectFilterOption("1998", "1998"),
|
||||
SelectFilterOption("1997", "1997"),
|
||||
SelectFilterOption("1996", "1996"),
|
||||
SelectFilterOption("1995", "1995"),
|
||||
SelectFilterOption("1994", "1994"),
|
||||
SelectFilterOption("1993", "1993"),
|
||||
SelectFilterOption("1992", "1992"),
|
||||
SelectFilterOption("1991", "1991"),
|
||||
SelectFilterOption("1990", "1990")
|
||||
]),
|
||||
SelectFilter("SortFilter", "Sort", 0, [
|
||||
SelectFilterOption("Default", "default"),
|
||||
SelectFilterOption("Ascending", "title_a_z"),
|
||||
SelectFilterOption("Descending", "title_z_a"),
|
||||
SelectFilterOption("Updated", "update"),
|
||||
SelectFilterOption("Published", "date"),
|
||||
SelectFilterOption("Most Viewed", "viewed"),
|
||||
SelectFilterOption("Favourite", "favorite"),
|
||||
]),
|
||||
GroupFilter("GenresFilter", "Genres", [
|
||||
CheckBoxFilter("Action", "Action"),
|
||||
CheckBoxFilter("Adult Cast", "Adult Cast"),
|
||||
CheckBoxFilter("Adventure", "Adventure"),
|
||||
CheckBoxFilter("Animation", "Animation"),
|
||||
CheckBoxFilter("Comedy", "Comedy"),
|
||||
CheckBoxFilter("Detective", "Detective"),
|
||||
CheckBoxFilter("Drama", "Drama"),
|
||||
CheckBoxFilter("Ecchi", "Ecchi"),
|
||||
CheckBoxFilter("Family", "Family"),
|
||||
CheckBoxFilter("Fantasy", "Fantasy"),
|
||||
CheckBoxFilter("Isekai", "Isekai"),
|
||||
CheckBoxFilter("Kids", "Kids"),
|
||||
CheckBoxFilter("Martial Arts", "Martial Arts"),
|
||||
CheckBoxFilter("Mecha", "Mecha"),
|
||||
CheckBoxFilter("Military", "Military"),
|
||||
CheckBoxFilter("Mystery", "Mystery"),
|
||||
CheckBoxFilter("Otaku Culture", "Otaku Culture"),
|
||||
CheckBoxFilter("Reality", "Reality"),
|
||||
CheckBoxFilter("Romance", "Romance"),
|
||||
CheckBoxFilter("School", "School"),
|
||||
CheckBoxFilter("Sci-Fi", "Sci-Fi"),
|
||||
CheckBoxFilter("Seinen", "Seinen"),
|
||||
CheckBoxFilter("Shounen", "Shounen"),
|
||||
CheckBoxFilter("Slice of Life", "Slice of Life"),
|
||||
CheckBoxFilter("Sports", "Sports"),
|
||||
CheckBoxFilter("Super Power", "Super Power"),
|
||||
CheckBoxFilter("SuperHero", "SuperHero"),
|
||||
CheckBoxFilter("Supernatural", "Supernatural"),
|
||||
CheckBoxFilter("TV Movie", "TV Movie"),
|
||||
]),
|
||||
];
|
||||
}
|
||||
|
||||
@override
|
||||
List<dynamic> getSourcePreferences() {
|
||||
return [
|
||||
ListPreference(
|
||||
key: "preferred_quality",
|
||||
title: "Preferred Quality",
|
||||
summary: "",
|
||||
valueIndex: 0,
|
||||
entries: ["1080p", "720p", "480p", "360p", "240p"],
|
||||
entryValues: ["1080", "720", "480", "360", "240"]),
|
||||
];
|
||||
}
|
||||
|
||||
List<MVideo> sortVideos(List<MVideo> videos, int sourceId) {
|
||||
String quality = getPreferenceValue(sourceId, "preferred_quality");
|
||||
videos.sort((MVideo a, MVideo b) {
|
||||
int qualityMatchA = 0;
|
||||
|
||||
if (a.quality.contains(quality)) {
|
||||
qualityMatchA = 1;
|
||||
}
|
||||
int qualityMatchB = 0;
|
||||
if (b.quality.contains(quality)) {
|
||||
qualityMatchB = 1;
|
||||
}
|
||||
if (qualityMatchA != qualityMatchB) {
|
||||
return qualityMatchB - qualityMatchA;
|
||||
}
|
||||
|
||||
final regex = RegExp(r'(\d+)p');
|
||||
final matchA = regex.firstMatch(a.quality);
|
||||
final matchB = regex.firstMatch(b.quality);
|
||||
final int qualityNumA = int.tryParse(matchA?.group(1) ?? '0') ?? 0;
|
||||
final int qualityNumB = int.tryParse(matchB?.group(1) ?? '0') ?? 0;
|
||||
return qualityNumB - qualityNumA;
|
||||
});
|
||||
return videos;
|
||||
}
|
||||
|
||||
String ll(String url) {
|
||||
if (url.contains("?")) {
|
||||
return "&";
|
||||
}
|
||||
return "?";
|
||||
}
|
||||
}
|
||||
|
||||
AnimeWorldIndia main(MSource source) {
|
||||
return AnimeWorldIndia(source: source);
|
||||
}
|
||||
BIN
dart/anime/src/all/animeworldindia/icon.png
Normal file
|
After Width: | Height: | Size: 7.4 KiB |
33
dart/anime/src/all/animeworldindia/sources.dart
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
import '../../../../../model/source.dart';
|
||||
|
||||
const _animeworldindiaVersion = "0.0.3";
|
||||
const _animeworldindiaSourceCodeUrl =
|
||||
"https://raw.githubusercontent.com/kodjodevf/mangayomi-extensions/$branchName/dart/anime/src/all/animeworldindia/animeworldindia.dart";
|
||||
|
||||
String _iconUrl =
|
||||
"https://raw.githubusercontent.com/kodjodevf/mangayomi-extensions/$branchName/dart/anime/src/all/animeworldindia/icon.png";
|
||||
|
||||
List<String> _languages = [
|
||||
"all",
|
||||
"en",
|
||||
"bn",
|
||||
"hi",
|
||||
"ja",
|
||||
"ml",
|
||||
"mr",
|
||||
"ta",
|
||||
"te",
|
||||
];
|
||||
|
||||
List<Source> get animeworldindiaSourcesList => _animeworldindiaSourcesList;
|
||||
List<Source> _animeworldindiaSourcesList = _languages
|
||||
.map((e) => Source(
|
||||
name: 'AnimeWorld India',
|
||||
baseUrl: "https://anime-world.in",
|
||||
lang: e,
|
||||
typeSource: "multiple",
|
||||
iconUrl: _iconUrl,
|
||||
version: _animeworldindiaVersion,
|
||||
itemType: ItemType.anime,
|
||||
sourceCodeUrl: _animeworldindiaSourceCodeUrl))
|
||||
.toList();
|
||||
BIN
dart/anime/src/all/nyaa/icon.png
Normal file
|
After Width: | Height: | Size: 3.1 KiB |
138
dart/anime/src/all/nyaa/nyaa.dart
Normal file
|
|
@ -0,0 +1,138 @@
|
|||
import 'package:mangayomi/bridge_lib.dart';
|
||||
|
||||
class Nyaa extends MProvider {
|
||||
Nyaa({required this.source});
|
||||
|
||||
MSource source;
|
||||
|
||||
final Client client = Client(source);
|
||||
|
||||
@override
|
||||
Future<MPages> getPopular(int page) async {
|
||||
final res = (await client.get(Uri.parse(
|
||||
"${source.baseUrl}/?f=0&c=${getPreferenceValue(source.id, "preferred_categorie_page")}&q=&s=downloads&o=desc&p=$page")))
|
||||
.body;
|
||||
return parseAnimeList(res);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<MPages> getLatestUpdates(int page) async {
|
||||
final res = (await client.get(Uri.parse(
|
||||
"${source.baseUrl}/?f=0&c=${getPreferenceValue(source.id, "preferred_categorie_page")}&q=$page")))
|
||||
.body;
|
||||
return parseAnimeList(res);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<MPages> search(String query, int page, FilterList filterList) async {
|
||||
final filters = filterList.filters;
|
||||
String url = "";
|
||||
url =
|
||||
"${source.baseUrl}/?f=0&c=${getPreferenceValue(source.id, "preferred_categorie_page")}&q=${query.replaceAll(" ", "+")}&p=$page";
|
||||
for (var filter in filters) {
|
||||
if (filter.type == "SortFilter") {
|
||||
url += "${ll(url)}s=${filter.values[filter.state.index].value}";
|
||||
final asc = filter.state.ascending ? "&o=asc" : "&o=desc";
|
||||
url += "${ll(url)}$asc";
|
||||
}
|
||||
}
|
||||
final res = (await client.get(Uri.parse(url))).body;
|
||||
return parseAnimeList(res);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<MManga> getDetail(String url) async {
|
||||
MManga anime = MManga();
|
||||
final res = (await client.get(Uri.parse(url))).body;
|
||||
final document = parseHtml(res);
|
||||
String description =
|
||||
(document.xpathFirst('//div[@class="panel-body"]/text()') ?? "")
|
||||
.replaceAll("\n", "");
|
||||
description +=
|
||||
"\n\n${(document.xpathFirst('//div[@class="panel panel-default"]/text()') ?? "").trim().replaceAll("\n", "")}";
|
||||
anime.description = description;
|
||||
MChapter ep = MChapter();
|
||||
ep.name = "Torrent";
|
||||
ep.url =
|
||||
"${source.baseUrl}/download/${substringAfterLast(url, '/')}.torrent";
|
||||
anime.chapters = [ep];
|
||||
return anime;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<MVideo>> getVideoList(String url) async {
|
||||
var video = MVideo();
|
||||
video
|
||||
..url = url
|
||||
..originalUrl = url
|
||||
..quality = "";
|
||||
return [video];
|
||||
}
|
||||
|
||||
@override
|
||||
List<dynamic> getFilterList() {
|
||||
return [
|
||||
SortFilter("SortFilter", "Sort by", SortState(0, true), [
|
||||
SelectFilterOption("None", ""),
|
||||
SelectFilterOption("Size", "size"),
|
||||
SelectFilterOption("Date", "id"),
|
||||
SelectFilterOption("Seeders", "seeders"),
|
||||
SelectFilterOption("Leechers", "leechers"),
|
||||
SelectFilterOption("Download", "downloads")
|
||||
])
|
||||
];
|
||||
}
|
||||
|
||||
@override
|
||||
List<dynamic> getSourcePreferences() {
|
||||
return [
|
||||
ListPreference(
|
||||
key: "preferred_categorie_page",
|
||||
title: "Preferred categorie page",
|
||||
summary: "",
|
||||
valueIndex: 0,
|
||||
entries: ["Anime", "Live Action"],
|
||||
entryValues: ["1_0", "4_0"]),
|
||||
];
|
||||
}
|
||||
|
||||
MPages parseAnimeList(String res) {
|
||||
List<MManga> animeList = [];
|
||||
final document = parseHtml(res);
|
||||
|
||||
final values = document
|
||||
.select("body > div > div.table-responsive > table > tbody > tr");
|
||||
for (var value in values) {
|
||||
MManga anime = MManga();
|
||||
anime.imageUrl =
|
||||
"${source.baseUrl}${getUrlWithoutDomain(value.selectFirst("td:nth-child(1) > a > img").getSrc)}";
|
||||
MElement firstElement = value
|
||||
.select("td > a")
|
||||
.where((MElement e) =>
|
||||
e.outerHtml.contains("/view/") &&
|
||||
!e.outerHtml.contains("#comments"))
|
||||
.toList()
|
||||
.first;
|
||||
anime.link =
|
||||
"${source.baseUrl}${getUrlWithoutDomain(firstElement.getHref)}";
|
||||
anime.name = firstElement.attr("title");
|
||||
animeList.add(anime);
|
||||
}
|
||||
|
||||
final hasNextPage =
|
||||
xpath(res, '//ul[@class="pagination"]/li[contains(text(),"»")]/a/@href')
|
||||
.isNotEmpty;
|
||||
return MPages(animeList, hasNextPage);
|
||||
}
|
||||
|
||||
String ll(String url) {
|
||||
if (url.contains("?")) {
|
||||
return "&";
|
||||
}
|
||||
return "?";
|
||||
}
|
||||
}
|
||||
|
||||
Nyaa main(MSource source) {
|
||||
return Nyaa(source: source);
|
||||
}
|
||||
19
dart/anime/src/all/nyaa/source.dart
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
import '../../../../../model/source.dart';
|
||||
|
||||
const _nyaaVersion = "0.0.25";
|
||||
const _nyaaSourceCodeUrl =
|
||||
"https://raw.githubusercontent.com/kodjodevf/mangayomi-extensions/$branchName/dart/anime/src/all/nyaa/nyaa.dart";
|
||||
|
||||
String _iconUrl =
|
||||
"https://raw.githubusercontent.com/kodjodevf/mangayomi-extensions/$branchName/dart/anime/src/all/nyaa/icon.png";
|
||||
|
||||
Source get nyaaSource => _nyaaSource;
|
||||
Source _nyaaSource = Source(
|
||||
name: 'Nyaa',
|
||||
baseUrl: "https://nyaa.si",
|
||||
lang: "all",
|
||||
typeSource: "torrent",
|
||||
iconUrl: _iconUrl,
|
||||
version: _nyaaVersion,
|
||||
itemType: ItemType.anime,
|
||||
sourceCodeUrl: _nyaaSourceCodeUrl);
|
||||
BIN
dart/anime/src/ar/okanime/icon.png
Normal file
|
After Width: | Height: | Size: 5.2 KiB |
223
dart/anime/src/ar/okanime/okanime.dart
Normal file
|
|
@ -0,0 +1,223 @@
|
|||
import 'package:mangayomi/bridge_lib.dart';
|
||||
import 'dart:convert';
|
||||
|
||||
class OkAnime extends MProvider {
|
||||
OkAnime({required this.source});
|
||||
|
||||
MSource source;
|
||||
|
||||
final Client client = Client(source);
|
||||
|
||||
@override
|
||||
Future<MPages> getPopular(int page) async {
|
||||
final res = (await client.get(Uri.parse(source.baseUrl))).body;
|
||||
List<MManga> animeList = [];
|
||||
String path =
|
||||
'//div[@class="section" and contains(text(),"افضل انميات")]/div[@class="section-content"]/div/div/div[contains(@class,"anime-card")]';
|
||||
final urls = xpath(res, '$path/div[@class="anime-title")]/h4/a/@href');
|
||||
final names = xpath(res, '$path/div[@class="anime-title")]/h4/a/text()');
|
||||
final images = xpath(res, '$path/div[@class="anime-image")]/img/@src');
|
||||
|
||||
for (var i = 0; i < names.length; i++) {
|
||||
MManga anime = MManga();
|
||||
anime.name = names[i];
|
||||
anime.imageUrl = images[i];
|
||||
anime.link = urls[i];
|
||||
animeList.add(anime);
|
||||
}
|
||||
return MPages(animeList, false);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<MPages> getLatestUpdates(int page) async {
|
||||
final res = (await client
|
||||
.get(Uri.parse("${source.baseUrl}/espisode-list?page=$page")))
|
||||
.body;
|
||||
List<MManga> animeList = [];
|
||||
String path = '//*[contains(@class,"anime-card")]';
|
||||
final urls = xpath(res, '$path/div[@class="anime-title")]/h4/a/@href');
|
||||
final names = xpath(res, '$path/div[@class="anime-title")]/h4/a/text()');
|
||||
final images = xpath(res, '$path/div[@class="episode-image")]/img/@src');
|
||||
|
||||
for (var i = 0; i < names.length; i++) {
|
||||
MManga anime = MManga();
|
||||
anime.name = names[i];
|
||||
anime.imageUrl = images[i];
|
||||
anime.link = urls[i];
|
||||
animeList.add(anime);
|
||||
}
|
||||
final nextPage =
|
||||
xpath(res, '//li[@class="page-item"]/a[@rel="next"]/@href');
|
||||
return MPages(animeList, nextPage.isNotEmpty);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<MPages> search(String query, int page, FilterList filterList) async {
|
||||
String url = "${source.baseUrl}/search/?s=$query";
|
||||
if (page > 1) {
|
||||
url += "&page=$page";
|
||||
}
|
||||
|
||||
final res = (await client.get(Uri.parse(url))).body;
|
||||
|
||||
List<MManga> animeList = [];
|
||||
String path = '//*[contains(@class,"anime-card")]';
|
||||
final urls = xpath(res, '$path/div[@class="anime-title")]/h4/a/@href');
|
||||
final names = xpath(res, '$path/div[@class="anime-title")]/h4/a/text()');
|
||||
final images = xpath(res, '$path/div[@class="anime-image")]/img/@src');
|
||||
|
||||
for (var i = 0; i < names.length; i++) {
|
||||
MManga anime = MManga();
|
||||
anime.name = names[i];
|
||||
anime.imageUrl = images[i];
|
||||
anime.link = urls[i];
|
||||
animeList.add(anime);
|
||||
}
|
||||
final nextPage =
|
||||
xpath(res, '//li[@class="page-item"]/a[@rel="next"]/@href');
|
||||
return MPages(animeList, nextPage.isNotEmpty);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<MManga> getDetail(String url) async {
|
||||
final statusList = [
|
||||
{"يعرض الان": 0, "مكتمل": 1}
|
||||
];
|
||||
final res = (await client.get(Uri.parse(url))).body;
|
||||
MManga anime = MManga();
|
||||
final status = xpath(res,
|
||||
'//*[@class="full-list-info" and contains(text(),"حالة الأنمي")]/small/a/text()');
|
||||
if (status.isNotEmpty) {
|
||||
anime.status = parseStatus(status.first, statusList);
|
||||
}
|
||||
anime.description = xpath(res, '//*[@class="review-content"]/text()').first;
|
||||
|
||||
anime.genre = xpath(res, '//*[@class="review-author-info"]/a/text()');
|
||||
final epUrls = xpath(res,
|
||||
'//*[contains(@class,"anime-card")]/div[@class="anime-title")]/h5/a/@href')
|
||||
.reversed
|
||||
.toList();
|
||||
final names = xpath(res,
|
||||
'//*[contains(@class,"anime-card")]/div[@class="anime-title")]/h5/a/text()')
|
||||
.reversed
|
||||
.toList();
|
||||
|
||||
List<MChapter>? episodesList = [];
|
||||
for (var i = 0; i < epUrls.length; i++) {
|
||||
MChapter episode = MChapter();
|
||||
episode.name = names[i];
|
||||
episode.url = epUrls[i];
|
||||
episodesList.add(episode);
|
||||
}
|
||||
|
||||
anime.chapters = episodesList;
|
||||
return anime;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<MVideo>> getVideoList(String url) async {
|
||||
final res = (await client.get(Uri.parse(url))).body;
|
||||
final urls = xpath(res, '//*[@id="streamlinks"]/a/@data-src');
|
||||
final qualities = xpath(res, '//*[@id="streamlinks"]/a/span/text()');
|
||||
final hosterSelection = preferenceHosterSelection(source.id);
|
||||
List<MVideo> videos = [];
|
||||
for (var i = 0; i < urls.length; i++) {
|
||||
final url = urls[i];
|
||||
final quality = getQuality(qualities[i]);
|
||||
List<MVideo> a = [];
|
||||
|
||||
if (url.contains("https://doo") && hosterSelection.contains("Dood")) {
|
||||
a = await doodExtractor(url, "DoodStream - $quality");
|
||||
} else if (url.contains("mp4upload") &&
|
||||
hosterSelection.contains("Mp4upload")) {
|
||||
a = await mp4UploadExtractor(url, null, "", "");
|
||||
} else if (url.contains("ok.ru") && hosterSelection.contains("Okru")) {
|
||||
a = await okruExtractor(url);
|
||||
} else if (url.contains("voe.sx") && hosterSelection.contains("Voe")) {
|
||||
a = await voeExtractor(url, "VoeSX $quality");
|
||||
} else if (containsVidBom(url) && hosterSelection.contains("VidBom")) {
|
||||
a = await vidBomExtractor(url);
|
||||
}
|
||||
videos.addAll(a);
|
||||
}
|
||||
return sortVideos(videos, source.id);
|
||||
}
|
||||
|
||||
@override
|
||||
List<dynamic> getSourcePreferences() {
|
||||
return [
|
||||
ListPreference(
|
||||
key: "preferred_quality",
|
||||
title: "Preferred Quality",
|
||||
summary: "",
|
||||
valueIndex: 1,
|
||||
entries: ["1080p", "720p", "480p", "360p"],
|
||||
entryValues: ["1080", "720", "480", "360"]),
|
||||
MultiSelectListPreference(
|
||||
key: "hoster_selection",
|
||||
title: "Enable/Disable Hosts",
|
||||
summary: "",
|
||||
entries: ["Dood", "Voe", "Mp4upload", "VidBom", "Okru"],
|
||||
entryValues: ["Dood", "Voe", "Mp4upload", "VidBom", "Okru"],
|
||||
values: ["Dood", "Voe", "Mp4upload", "VidBom", "Okru"]),
|
||||
];
|
||||
}
|
||||
|
||||
List<MVideo> sortVideos(List<MVideo> videos, int sourceId) {
|
||||
String quality = getPreferenceValue(sourceId, "preferred_quality");
|
||||
|
||||
videos.sort((MVideo a, MVideo b) {
|
||||
int qualityMatchA = 0;
|
||||
if (a.quality.contains(quality)) {
|
||||
qualityMatchA = 1;
|
||||
}
|
||||
int qualityMatchB = 0;
|
||||
if (b.quality.contains(quality)) {
|
||||
qualityMatchB = 1;
|
||||
}
|
||||
if (qualityMatchA != qualityMatchB) {
|
||||
return qualityMatchB - qualityMatchA;
|
||||
}
|
||||
|
||||
final regex = RegExp(r'(\d+)p');
|
||||
final matchA = regex.firstMatch(a.quality);
|
||||
final matchB = regex.firstMatch(b.quality);
|
||||
final int qualityNumA = int.tryParse(matchA?.group(1) ?? '0') ?? 0;
|
||||
final int qualityNumB = int.tryParse(matchB?.group(1) ?? '0') ?? 0;
|
||||
return qualityNumB - qualityNumA;
|
||||
});
|
||||
|
||||
return videos;
|
||||
}
|
||||
|
||||
List<String> preferenceHosterSelection(int sourceId) {
|
||||
return getPreferenceValue(sourceId, "hoster_selection");
|
||||
}
|
||||
|
||||
String getQuality(String quality) {
|
||||
quality = quality.replaceAll(" ", "");
|
||||
if (quality == "HD") {
|
||||
return "720p";
|
||||
} else if (quality == "FHD") {
|
||||
return "1080p";
|
||||
} else if (quality == "SD") {
|
||||
return "480p";
|
||||
}
|
||||
return "240p";
|
||||
}
|
||||
|
||||
bool containsVidBom(String url) {
|
||||
url = url;
|
||||
final list = ["vidbam", "vadbam", "vidbom", "vidbm"];
|
||||
for (var n in list) {
|
||||
if (url.contains(n)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
OkAnime main(MSource source) {
|
||||
return OkAnime(source: source);
|
||||
}
|
||||
16
dart/anime/src/ar/okanime/source.dart
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
import '../../../../../model/source.dart';
|
||||
|
||||
Source get okanimeSource => _okanimeSource;
|
||||
const _okanimeVersion = "0.0.55";
|
||||
const _okanimeSourceCodeUrl =
|
||||
"https://raw.githubusercontent.com/kodjodevf/mangayomi-extensions/$branchName/dart/anime/src/ar/okanime/okanime.dart";
|
||||
Source _okanimeSource = Source(
|
||||
name: "Okanime",
|
||||
baseUrl: "https://www.okanime.xyz",
|
||||
lang: "ar",
|
||||
typeSource: "single",
|
||||
iconUrl:
|
||||
"https://raw.githubusercontent.com/kodjodevf/mangayomi-extensions/$branchName/dart/anime/src/ar/okanime/icon.png",
|
||||
sourceCodeUrl: _okanimeSourceCodeUrl,
|
||||
version: _okanimeVersion,
|
||||
itemType: ItemType.anime);
|
||||
235
dart/anime/src/de/animetoast/animetoast.dart
Normal file
|
|
@ -0,0 +1,235 @@
|
|||
import 'package:mangayomi/bridge_lib.dart';
|
||||
|
||||
class AnimeToast extends MProvider {
|
||||
AnimeToast({required this.source});
|
||||
|
||||
MSource source;
|
||||
|
||||
final Client client = Client(source);
|
||||
|
||||
@override
|
||||
bool get supportsLatest => false;
|
||||
|
||||
@override
|
||||
String get baseUrl => source.baseUrl;
|
||||
|
||||
@override
|
||||
Future<MPages> getPopular(int page) async {
|
||||
final res = (await client.get(Uri.parse(baseUrl))).body;
|
||||
final document = parseHtml(res);
|
||||
final elements = document.select("div.row div.col-md-4 div.video-item");
|
||||
List<MManga> animeList = [];
|
||||
for (var element in elements) {
|
||||
MManga anime = MManga();
|
||||
anime.name = element.selectFirst("div.item-thumbnail a").attr("title");
|
||||
anime.link = getUrlWithoutDomain(
|
||||
element.selectFirst("div.item-thumbnail a").attr("href"));
|
||||
anime.imageUrl =
|
||||
element.selectFirst("div.item-thumbnail a img").attr("src");
|
||||
animeList.add(anime);
|
||||
}
|
||||
return MPages(animeList, false);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<MPages> search(String query, int page, FilterList filterList) async {
|
||||
final res =
|
||||
(await client.get(Uri.parse("$baseUrl/page/$page/?s=$query"))).body;
|
||||
final document = parseHtml(res);
|
||||
final elements = document.select("div.item-thumbnail a[href]");
|
||||
List<MManga> animeList = [];
|
||||
for (var element in elements) {
|
||||
MManga anime = MManga();
|
||||
anime.name = element.attr("title");
|
||||
anime.link = getUrlWithoutDomain(element.attr("href"));
|
||||
anime.imageUrl = element.selectFirst("a img").attr("src");
|
||||
animeList.add(anime);
|
||||
}
|
||||
return MPages(
|
||||
animeList, document.selectFirst("li.next a")?.attr("href") != null);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<MManga> getDetail(String url) async {
|
||||
MManga anime = MManga();
|
||||
final res = (await client.get(Uri.parse("$baseUrl$url"))).body;
|
||||
final document = parseHtml(res);
|
||||
anime.imageUrl = document.selectFirst(".item-content p img").attr("src");
|
||||
anime.genre =
|
||||
(document.xpathFirst('//p[contains(text(),"Genre:")]/text()') ?? "")
|
||||
.replaceAll("Genre:", "")
|
||||
.split(",");
|
||||
anime.description = document.selectFirst("div.item-content div + p").text;
|
||||
final categoryTag = document.xpath('//*[@rel="category tag"]/text()');
|
||||
if (categoryTag.isNotEmpty) {
|
||||
if (categoryTag.contains("Airing")) {
|
||||
anime.status = MStatus.ongoing;
|
||||
} else {
|
||||
anime.status = MStatus.completed;
|
||||
}
|
||||
}
|
||||
List<MChapter>? episodesList = [];
|
||||
if (categoryTag.contains("Serie")) {
|
||||
List<MElement> elements = [];
|
||||
if (document.selectFirst("#multi_link_tab0")?.attr("id") != null) {
|
||||
elements = document.select("#multi_link_tab0");
|
||||
} else {
|
||||
elements = document.select("#multi_link_tab1");
|
||||
}
|
||||
|
||||
for (var element in elements) {
|
||||
final episodeElement = element.selectFirst("a");
|
||||
final epT = episodeElement.text;
|
||||
if (epT.contains(":") || epT.contains("-")) {
|
||||
final url = episodeElement.attr("href");
|
||||
final document = parseHtml((await client.get(Uri.parse(url))).body);
|
||||
final nUrl = document.selectFirst("#player-embed a").attr("href");
|
||||
final nDoc = parseHtml((await client.get(Uri.parse(nUrl))).body);
|
||||
final nEpEl = nDoc.select("div.tab-pane a");
|
||||
for (var epElement in nEpEl) {
|
||||
MChapter ep = MChapter();
|
||||
ep.name = epElement.text;
|
||||
ep.url = getUrlWithoutDomain(epElement.attr("href"));
|
||||
episodesList.add(ep);
|
||||
}
|
||||
} else {
|
||||
final episodeElements = element.select("a");
|
||||
for (var epElement in episodeElements) {
|
||||
MChapter ep = MChapter();
|
||||
ep.name = epElement.text;
|
||||
ep.url = getUrlWithoutDomain(epElement.attr("href"));
|
||||
episodesList.add(ep);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
MChapter ep = MChapter();
|
||||
ep.name = document.selectFirst("h1.light-title")?.text ?? "Film";
|
||||
ep.url = getUrlWithoutDomain(
|
||||
document.selectFirst("link[rel=canonical]").attr("href"));
|
||||
episodesList.add(ep);
|
||||
}
|
||||
anime.chapters = episodesList.reversed.toList();
|
||||
return anime;
|
||||
}
|
||||
|
||||
List<String> preferenceHosterSelection() {
|
||||
return getPreferenceValue(source.id, "hoster_selection");
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<MVideo>> getVideoList(String url) async {
|
||||
final res = (await client.get(Uri.parse("$baseUrl$url"))).body;
|
||||
final document = parseHtml(res);
|
||||
final fEp = document.selectFirst("div.tab-pane");
|
||||
List<MVideo> videos = [];
|
||||
List<MElement> ep = [];
|
||||
int epcu = 100;
|
||||
|
||||
if (fEp.text.contains(":") || fEp.text.contains("-")) {
|
||||
final tx = document.select("div.tab-pane");
|
||||
|
||||
for (var e in tx) {
|
||||
final sUrl = e.selectFirst("a").attr("href");
|
||||
final doc = parseHtml((await client.get(Uri.parse(sUrl))).body);
|
||||
final nUrl = doc.selectFirst("#player-embed a").attr("href");
|
||||
final nDoc = parseHtml((await client.get(Uri.parse(nUrl))).body);
|
||||
epcu = int.tryParse(substringAfter(
|
||||
document.selectFirst("div.tab-pane a.current-link")?.text ?? "",
|
||||
"Ep.")) ??
|
||||
100;
|
||||
ep = nDoc.select("div.tab-pane a");
|
||||
}
|
||||
} else {
|
||||
epcu = int.tryParse(substringAfter(
|
||||
document.selectFirst("div.tab-pane a.current-link")?.text ?? "",
|
||||
"Ep.")) ??
|
||||
100;
|
||||
ep = document.select("div.tab-pane a");
|
||||
}
|
||||
final hosterSelection = preferenceHosterSelection();
|
||||
for (var e in ep) {
|
||||
if (int.tryParse(substringAfter(e.text, "Ep.")) == epcu) {
|
||||
final epUrl = e.attr("href");
|
||||
final newdoc = parseHtml((await client.get(Uri.parse(epUrl))).body);
|
||||
final elements = newdoc.select("#player-embed");
|
||||
for (var element in elements) {
|
||||
final link = element.selectFirst("a").getHref ?? "";
|
||||
if (link.contains("https://voe.sx") &&
|
||||
hosterSelection.contains("voe")) {
|
||||
videos.addAll(await voeExtractor(link, "Voe"));
|
||||
}
|
||||
}
|
||||
for (var element in elements) {
|
||||
List<MVideo> a = [];
|
||||
final link = element.selectFirst("iframe").getSrc ?? "";
|
||||
if ((link.contains("https://dood") ||
|
||||
link.contains("https://ds2play") ||
|
||||
link.contains("https://d0")) &&
|
||||
hosterSelection.contains("dood")) {
|
||||
a = await doodExtractor(link, "DoodStream");
|
||||
} else if (link.contains("filemoon") &&
|
||||
hosterSelection.contains("filemoon")) {
|
||||
a = await filemoonExtractor(link, "", "");
|
||||
} else if (link.contains("mp4upload") &&
|
||||
hosterSelection.contains("mp4upload")) {
|
||||
a = await mp4UploadExtractor(url, null, "", "");
|
||||
}
|
||||
videos.addAll(a);
|
||||
}
|
||||
}
|
||||
}
|
||||
return sortVideos(videos);
|
||||
}
|
||||
|
||||
List<MVideo> sortVideos(List<MVideo> videos) {
|
||||
String server = getPreferenceValue(source.id, "preferred_hoster");
|
||||
|
||||
videos.sort((MVideo a, MVideo b) {
|
||||
int qualityMatchA = 0;
|
||||
if (a.quality.toLowerCase().contains(server)) {
|
||||
qualityMatchA = 1;
|
||||
}
|
||||
int qualityMatchB = 0;
|
||||
if (b.quality.toLowerCase().contains(server)) {
|
||||
qualityMatchB = 1;
|
||||
}
|
||||
if (qualityMatchA != qualityMatchB) {
|
||||
return qualityMatchB - qualityMatchA;
|
||||
}
|
||||
|
||||
final regex = RegExp(r'(\d+)p');
|
||||
final matchA = regex.firstMatch(a.quality);
|
||||
final matchB = regex.firstMatch(b.quality);
|
||||
final int qualityNumA = int.tryParse(matchA?.group(1) ?? '0') ?? 0;
|
||||
final int qualityNumB = int.tryParse(matchB?.group(1) ?? '0') ?? 0;
|
||||
return qualityNumB - qualityNumA;
|
||||
});
|
||||
|
||||
return videos;
|
||||
}
|
||||
|
||||
@override
|
||||
List<dynamic> getSourcePreferences() {
|
||||
return [
|
||||
ListPreference(
|
||||
key: "preferred_hoster",
|
||||
title: "Standard-Hoster",
|
||||
summary: "",
|
||||
valueIndex: 0,
|
||||
entries: ["Voe", "DoodStream", "Filemoon", "Mp4upload"],
|
||||
entryValues: ["voe", "doodStream", "filemoon", "mp4upload"]),
|
||||
MultiSelectListPreference(
|
||||
key: "hoster_selection",
|
||||
title: "Hoster auswählen",
|
||||
summary: "",
|
||||
entries: ["Voe", "DoodStream", "Filemoon", "Mp4upload"],
|
||||
entryValues: ["voe", "dood", "filemoon", "mp4upload"],
|
||||
values: ["voe", "dood", "filemoon", "mp4upload"]),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
AnimeToast main(MSource source) {
|
||||
return AnimeToast(source: source);
|
||||
}
|
||||
BIN
dart/anime/src/de/animetoast/icon.png
Normal file
|
After Width: | Height: | Size: 5 KiB |
16
dart/anime/src/de/animetoast/source.dart
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
import '../../../../../model/source.dart';
|
||||
|
||||
Source get animetoast => _animetoast;
|
||||
const _animetoastVersion = "0.0.2";
|
||||
const _animetoastCodeUrl =
|
||||
"https://raw.githubusercontent.com/kodjodevf/mangayomi-extensions/$branchName/dart/anime/src/de/animetoast/animetoast.dart";
|
||||
Source _animetoast = Source(
|
||||
name: "AnimeToast",
|
||||
baseUrl: "https://animetoast.cc",
|
||||
lang: "de",
|
||||
typeSource: "single",
|
||||
iconUrl:
|
||||
"https://raw.githubusercontent.com/kodjodevf/mangayomi-extensions/$branchName/dart/anime/src/de/animetoast/icon.png",
|
||||
sourceCodeUrl: _animetoastCodeUrl,
|
||||
version: _animetoastVersion,
|
||||
itemType: ItemType.anime);
|
||||
353
dart/anime/src/en/animepahe/animepahe.dart
Normal file
|
|
@ -0,0 +1,353 @@
|
|||
import 'package:mangayomi/bridge_lib.dart';
|
||||
import 'dart:convert';
|
||||
import 'dart:math';
|
||||
|
||||
class AnimePahe extends MProvider {
|
||||
AnimePahe(this.source);
|
||||
|
||||
final MSource source;
|
||||
|
||||
final Client client = Client(source);
|
||||
|
||||
@override
|
||||
String get baseUrl => getPreferenceValue(source.id, "preferred_domain");
|
||||
|
||||
@override
|
||||
Map<String, String> get headers => {'cookie': '__ddg1_=;__ddg2_=;'};
|
||||
|
||||
@override
|
||||
Future<MPages> getPopular(int page) async {
|
||||
return await getLatestUpdates(page);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<MPages> getLatestUpdates(int page) async {
|
||||
final res = (await client.get(Uri.parse("$baseUrl/api?m=airing&page=$page"),
|
||||
headers: headers))
|
||||
.body;
|
||||
final jsonResult = json.decode(res);
|
||||
final hasNextPage = jsonResult["current_page"] < jsonResult["last_page"];
|
||||
List<MManga> animeList = [];
|
||||
for (var item in jsonResult["data"]) {
|
||||
MManga anime = MManga();
|
||||
anime.name = item["anime_title"];
|
||||
anime.imageUrl = item["snapshot"];
|
||||
anime.link = "/anime/?anime_id=${item["id"]}&name=${item["anime_title"]}";
|
||||
anime.artist = item["fansub"];
|
||||
animeList.add(anime);
|
||||
}
|
||||
return MPages(animeList, hasNextPage);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<MPages> search(String query, int page, FilterList filterList) async {
|
||||
final res = (await client.get(
|
||||
Uri.parse("$baseUrl/api?m=search&l=8&q=$query"),
|
||||
headers: headers))
|
||||
.body;
|
||||
final jsonResult = json.decode(res);
|
||||
List<MManga> animeList = [];
|
||||
for (var item in jsonResult["data"]) {
|
||||
MManga anime = MManga();
|
||||
anime.name = item["title"];
|
||||
anime.imageUrl = item["poster"];
|
||||
anime.link = "/anime/?anime_id=${item["id"]}&name=${item["title"]}";
|
||||
animeList.add(anime);
|
||||
}
|
||||
return MPages(animeList, false);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<MManga> getDetail(String url) async {
|
||||
final statusList = [
|
||||
{"Currently Airing": 0, "Finished Airing": 1}
|
||||
];
|
||||
MManga anime = MManga();
|
||||
final id = substringBefore(substringAfterLast(url, "?anime_id="), "&name=");
|
||||
final name = substringAfterLast(url, "&name=");
|
||||
final session = await getSession(name, id);
|
||||
final res = (await client.get(
|
||||
Uri.parse("$baseUrl/anime/$session?anime_id=$id"),
|
||||
headers: headers))
|
||||
.body;
|
||||
final document = parseHtml(res);
|
||||
final status =
|
||||
(document.xpathFirst('//div/p[contains(text(),"Status:")]/text()') ??
|
||||
"")
|
||||
.replaceAll("Status:\n", "")
|
||||
.trim();
|
||||
anime.status = parseStatus(status, statusList);
|
||||
|
||||
anime.name = document.selectFirst("div.title-wrapper > h1 > span").text;
|
||||
anime.author =
|
||||
(document.xpathFirst('//div/p[contains(text(),"Studio:")]/text()') ??
|
||||
"")
|
||||
.replaceAll("Studio:\n", "")
|
||||
.trim();
|
||||
anime.imageUrl = document.selectFirst("div.anime-poster a").attr("href");
|
||||
anime.genre =
|
||||
xpath(res, '//*[contains(@class,"anime-genre")]/ul/li/text()');
|
||||
final synonyms =
|
||||
(document.xpathFirst('//div/p[contains(text(),"Synonyms:")]/text()') ??
|
||||
"")
|
||||
.replaceAll("Synonyms:\n", "")
|
||||
.trim();
|
||||
anime.description = document.selectFirst("div.anime-summary").text;
|
||||
if (synonyms.isNotEmpty) {
|
||||
anime.description += "\n\n$synonyms";
|
||||
}
|
||||
final epUrl = "$baseUrl/api?m=release&id=$session&sort=episode_desc&page=1";
|
||||
final resEp = (await client.get(Uri.parse(epUrl), headers: headers)).body;
|
||||
final episodes = await recursivePages(epUrl, resEp, session);
|
||||
|
||||
anime.chapters = episodes;
|
||||
return anime;
|
||||
}
|
||||
|
||||
Future<List<MChapter>> recursivePages(
|
||||
String url, String res, String session) async {
|
||||
final jsonResult = json.decode(res);
|
||||
final page = jsonResult["current_page"];
|
||||
final hasNextPage = page < jsonResult["last_page"];
|
||||
List<MManga> animeList = [];
|
||||
for (var item in jsonResult["data"]) {
|
||||
MChapter episode = MChapter();
|
||||
episode.name = "Episode ${item["episode"]}";
|
||||
episode.url = "/play/$session/${item["session"]}";
|
||||
episode.dateUpload =
|
||||
parseDates([item["created_at"]], "yyyy-MM-dd HH:mm:ss", "en")[0];
|
||||
animeList.add(episode);
|
||||
}
|
||||
if (hasNextPage) {
|
||||
final newUrl = "${substringBeforeLast(url, "&page=")}&page=${page + 1}";
|
||||
final newRes =
|
||||
(await client.get(Uri.parse(newUrl), headers: headers)).body;
|
||||
animeList.addAll(await recursivePages(newUrl, newRes, session));
|
||||
}
|
||||
return animeList;
|
||||
}
|
||||
|
||||
Future<String> getSession(String title, String animeId) async {
|
||||
final res = (await client.get(Uri.parse("$baseUrl/api?m=search&q=$title"),
|
||||
headers: headers))
|
||||
.body;
|
||||
return substringBefore(
|
||||
substringAfter(
|
||||
substringAfter(res, "\"id\":$animeId"), "\"session\":\""),
|
||||
"\"");
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<MVideo>> getVideoList(String url) async {
|
||||
//by default we use rhttp package but it does not support `followRedirects`
|
||||
//so setting `useDartHttpClient` to true allows us to use a Dart http package that supports `followRedirects`
|
||||
final client = Client(source, json.encode({"useDartHttpClient": true}));
|
||||
final res = (await client.get(Uri.parse("$baseUrl$url"), headers: headers));
|
||||
final document = parseHtml(res.body);
|
||||
final downloadLinks = document.select("div#pickDownload > a");
|
||||
final buttons = document.select("div#resolutionMenu > button");
|
||||
List<MVideo> videos = [];
|
||||
|
||||
for (var i = 0; i < buttons.length; i++) {
|
||||
final btn = buttons[i];
|
||||
final audio = btn.attr("data-audio"); // Get audio type (jpn/eng). Japanese or Dubbed.
|
||||
final kwikLink = btn.attr("data-src");
|
||||
final quality = btn.text;
|
||||
final paheWinLink = downloadLinks[i].attr("href");
|
||||
|
||||
if (getPreferenceValue(source.id, "preffered_link_type")) {
|
||||
final noRedirectClient = Client(source,
|
||||
json.encode({"followRedirects": false, "useDartHttpClient": true}));
|
||||
final kwikHeaders =
|
||||
(await noRedirectClient.get(Uri.parse("${paheWinLink}/i"))).headers;
|
||||
final kwikUrl =
|
||||
"https://${substringAfterLast(getMapValue(json.encode(kwikHeaders), "location"), "https://")}";
|
||||
final reskwik = (await client
|
||||
.get(Uri.parse(kwikUrl), headers: {"Referer": "https://kwik.cx/"}));
|
||||
final matches = RegExp(r'\("(\S+)",\d+,"(\S+)",(\d+),(\d+)')
|
||||
.firstMatch(reskwik.body);
|
||||
final token = decrypt(matches!.group(1)!, matches.group(2)!,
|
||||
matches.group(3)!, int.parse(matches.group(4)!));
|
||||
final url = RegExp(r'action="([^"]+)"').firstMatch(token)!.group(1)!;
|
||||
final tok = RegExp(r'value="([^"]+)"').firstMatch(token)!.group(1)!;
|
||||
var code = 419;
|
||||
var tries = 0;
|
||||
String location = "";
|
||||
|
||||
while (code != 302 && tries < 20) {
|
||||
String cookie =
|
||||
getMapValue(json.encode(res.request.headers), "cookie");
|
||||
cookie +=
|
||||
"; ${getMapValue(json.encode(reskwik.headers), "set-cookie").replaceAll("path=/;", "")}";
|
||||
final resNo = await Client(
|
||||
source,
|
||||
json.encode(
|
||||
{"followRedirects": false, "useDartHttpClient": true}))
|
||||
.post(Uri.parse(url), headers: {
|
||||
"referer": reskwik.request.url.toString(),
|
||||
"cookie": cookie,
|
||||
"user-agent":
|
||||
getMapValue(json.encode(res.request.headers), "user-agent")
|
||||
}, body: {
|
||||
"_token": tok
|
||||
});
|
||||
code = resNo.statusCode;
|
||||
tries++;
|
||||
location = getMapValue(json.encode(resNo.headers), "location");
|
||||
}
|
||||
if (tries > 19) {
|
||||
throw ("Failed to extract the stream uri from kwik.");
|
||||
}
|
||||
MVideo video = MVideo();
|
||||
video
|
||||
..url = location
|
||||
..originalUrl = location
|
||||
..quality = quality;
|
||||
videos.add(video);
|
||||
} else {
|
||||
final ress = (await client.get(Uri.parse(kwikLink),
|
||||
headers: {"Referer": "https://animepahe.com"}));
|
||||
final script = substringAfterLast(
|
||||
xpath(ress.body,
|
||||
'//script[contains(text(),"eval(function")]/text()')
|
||||
.first,
|
||||
"eval(function(");
|
||||
final videoUrl = substringBefore(
|
||||
substringAfter(unpackJsAndCombine("eval(function($script"),
|
||||
"const source=\\'"),
|
||||
"\\';");
|
||||
MVideo video = MVideo();
|
||||
video
|
||||
..url = videoUrl
|
||||
..originalUrl = videoUrl
|
||||
..quality = quality
|
||||
..headers = {"referer": "https://kwik.cx"};
|
||||
videos.add(video);
|
||||
}
|
||||
}
|
||||
return sortVideos(videos);
|
||||
}
|
||||
|
||||
String getString(String ctn, int sep) {
|
||||
int b = 10;
|
||||
String cm =
|
||||
"0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ+/";
|
||||
final n = cm.substring(0, b);
|
||||
double mx = 0;
|
||||
for (var index = 0; index < ctn.length; index++) {
|
||||
mx += (int.tryParse(ctn[ctn.length - index - 1], radix: 10) ?? 0.0)
|
||||
.toInt() *
|
||||
(pow(sep, index));
|
||||
}
|
||||
var m = '';
|
||||
while (mx > 0) {
|
||||
m = n[(mx % b).toInt()] + m;
|
||||
mx = (mx - (mx % b)) / b;
|
||||
}
|
||||
return m.isNotEmpty ? m : '0';
|
||||
}
|
||||
|
||||
String decrypt(String fS, String key, String v1, int v2) {
|
||||
var html = "";
|
||||
var i = 0;
|
||||
final ld = int.parse(v1);
|
||||
while (i < fS.length) {
|
||||
var s = "";
|
||||
while (fS[i] != key[v2]) {
|
||||
s += fS[i];
|
||||
i++;
|
||||
}
|
||||
for (var index = 0; index < key.length; index++) {
|
||||
s = s.replaceAll(key[index], index.toString());
|
||||
}
|
||||
html += String.fromCharCode(int.parse(getString(s, v2)) - ld);
|
||||
i++;
|
||||
}
|
||||
|
||||
return html;
|
||||
}
|
||||
|
||||
List<MVideo> sortVideos(List<MVideo> videos) {
|
||||
String quality = getPreferenceValue(source.id, "preferred_quality");
|
||||
String preferredAudio = getPreferenceValue(source.id, "preferred_audio"); // get user's audio preference
|
||||
|
||||
|
||||
videos.sort((MVideo a, MVideo b) {
|
||||
// Prioritize audio first.
|
||||
// Preferred Audio: Videos with matching preferred audio are ranked highest.
|
||||
int audioMatchA = a.quality.contains(preferredAudio) ? 1 : 0;
|
||||
int audioMatchB = b.quality.contains(preferredAudio) ? 1 : 0;
|
||||
if (audioMatchA != audioMatchB) {
|
||||
return audioMatchB - audioMatchA;
|
||||
}
|
||||
|
||||
// quality prioritized next
|
||||
// Preferred Video Quality: If audio matches, videos with preferred video quality are ranked higher.
|
||||
int qualityMatchA = 0;
|
||||
if (a.quality.contains(quality)) {
|
||||
qualityMatchA = 1;
|
||||
}
|
||||
int qualityMatchB = 0;
|
||||
if (b.quality.contains(quality)) {
|
||||
qualityMatchB = 1;
|
||||
}
|
||||
if (qualityMatchA != qualityMatchB) {
|
||||
return qualityMatchB - qualityMatchA;
|
||||
}
|
||||
|
||||
final regex = RegExp(r'(\d+)p');
|
||||
final matchA = regex.firstMatch(a.quality);
|
||||
final matchB = regex.firstMatch(b.quality);
|
||||
final int qualityNumA = int.tryParse(matchA?.group(1) ?? '0') ?? 0;
|
||||
final int qualityNumB = int.tryParse(matchB?.group(1) ?? '0') ?? 0;
|
||||
return qualityNumB - qualityNumA;
|
||||
});
|
||||
|
||||
return videos;
|
||||
}
|
||||
|
||||
@override
|
||||
List<dynamic> getSourcePreferences() {
|
||||
return [
|
||||
ListPreference(
|
||||
key: "preferred_domain",
|
||||
title: "Preferred domain",
|
||||
summary: "",
|
||||
valueIndex: 1,
|
||||
entries: [
|
||||
"animepahe.com",
|
||||
"animepahe.ru",
|
||||
"animepahe.org"
|
||||
],
|
||||
entryValues: [
|
||||
"https://animepahe.com",
|
||||
"https://animepahe.ru",
|
||||
"https://animepahe.org"
|
||||
]),
|
||||
SwitchPreferenceCompat(
|
||||
key: "preffered_link_type",
|
||||
title: "Use HLS links",
|
||||
summary: "Enable this if you are having Cloudflare issues.",
|
||||
value: false),
|
||||
ListPreference(
|
||||
key: "preferred_quality",
|
||||
title: "Preferred Quality",
|
||||
summary: "",
|
||||
valueIndex: 0,
|
||||
entries: ["1080p", "720p", "360p"],
|
||||
entryValues: ["1080", "720", "360"]),
|
||||
|
||||
ListPreference(
|
||||
key: "preferred_audio", // Add new preference for audio
|
||||
title: "Preferred Audio",
|
||||
summary: "Select your preferred audio language (Japanese or English).",
|
||||
valueIndex: 0, // Default to Japanese (or whichever you prefer)
|
||||
entries: ["Japanese", "English"],
|
||||
entryValues: ["jpn", "eng"]),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
AnimePahe main(MSource source) {
|
||||
return AnimePahe(source);
|
||||
}
|
||||
BIN
dart/anime/src/en/animepahe/icon.png
Normal file
|
After Width: | Height: | Size: 1.9 KiB |
16
dart/anime/src/en/animepahe/source.dart
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
import '../../../../../model/source.dart';
|
||||
|
||||
Source get animepaheSource => _animepaheSource;
|
||||
const _animepaheVersion = "0.0.5";
|
||||
const _animepaheSourceCodeUrl =
|
||||
"https://raw.githubusercontent.com/kodjodevf/mangayomi-extensions/$branchName/dart/anime/src/en/animepahe/animepahe.dart";
|
||||
Source _animepaheSource = Source(
|
||||
name: "AnimePahe",
|
||||
baseUrl: "https://www.animepahe.ru",
|
||||
lang: "en",
|
||||
typeSource: "single",
|
||||
iconUrl:
|
||||
"https://raw.githubusercontent.com/kodjodevf/mangayomi-extensions/$branchName/dart/anime/src/en/animepahe/icon.png",
|
||||
sourceCodeUrl: _animepaheSourceCodeUrl,
|
||||
version: _animepaheVersion,
|
||||
itemType: ItemType.anime);
|
||||
210
dart/anime/src/en/dramacool/dramacool.dart
Normal file
|
|
@ -0,0 +1,210 @@
|
|||
import 'package:mangayomi/bridge_lib.dart';
|
||||
import 'dart:convert';
|
||||
|
||||
class DramaCool extends MProvider {
|
||||
DramaCool({required this.source});
|
||||
|
||||
MSource source;
|
||||
|
||||
final Client client = Client(source);
|
||||
|
||||
@override
|
||||
String get baseUrl => getPreferenceValue(source.id, "overrideBaseUrl");
|
||||
|
||||
@override
|
||||
Future<MPages> getPopular(int page) async {
|
||||
final res =
|
||||
(await client.get(Uri.parse("$baseUrl/most-popular-drama?page=$page")))
|
||||
.body;
|
||||
final document = parseHtml(res);
|
||||
return animeFromElement(document.select("ul.list-episode-item li a"),
|
||||
document.selectFirst("li.next a")?.attr("href") != null);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<MPages> getLatestUpdates(int page) async {
|
||||
final res =
|
||||
(await client.get(Uri.parse("$baseUrl/recently-added?page=$page")))
|
||||
.body;
|
||||
final document = parseHtml(res);
|
||||
return animeFromElement(document.select("ul.switch-block a"),
|
||||
document.selectFirst("li.next a")?.attr("href") != null);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<MPages> search(String query, int page, FilterList filterList) async {
|
||||
final res = (await client
|
||||
.get(Uri.parse("$baseUrl/search?keyword=$query&page=$page")))
|
||||
.body;
|
||||
final document = parseHtml(res);
|
||||
return animeFromElement(document.select("ul.list-episode-item li a"),
|
||||
document.selectFirst("li.next a")?.attr("href") != null);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<MManga> getDetail(String url) async {
|
||||
final statusList = [
|
||||
{"Ongoing": 0, "Completed": 1}
|
||||
];
|
||||
url = getUrlWithoutDomain(url);
|
||||
if (url.contains("-episode-") && url.endsWith(".html")) {
|
||||
final res = (await client.get(Uri.parse("$baseUrl$url"))).body;
|
||||
url = parseHtml(res).selectFirst("div.category a").attr("href");
|
||||
}
|
||||
url = getUrlWithoutDomain(url);
|
||||
|
||||
final res = (await client.get(Uri.parse("$baseUrl$url"))).body;
|
||||
final document = parseHtml(res);
|
||||
MManga anime = MManga();
|
||||
anime.description = document
|
||||
.selectFirst("div.info")
|
||||
.select("p")
|
||||
.map((MElement e) {
|
||||
if (!e.outerHtml.contains("<span")) {
|
||||
return e.text;
|
||||
}
|
||||
return "";
|
||||
})
|
||||
.toList()
|
||||
.join("\n");
|
||||
final author =
|
||||
xpath(res, '//p[contains(text(),"Original Network:")]/a/text()');
|
||||
if (author.isNotEmpty) {
|
||||
anime.author = author.first;
|
||||
}
|
||||
anime.genre = xpath(res, '//p[contains(text(),"Genre:")]/a/text()');
|
||||
final status = xpath(res, '//p[contains(text(),"Status")]/a/text()');
|
||||
if (status.isNotEmpty) {
|
||||
anime.status = parseStatus(status.first, statusList);
|
||||
}
|
||||
List<MChapter> episodesList = [];
|
||||
final episodeListElements = document.select("ul.all-episode li a");
|
||||
|
||||
for (var element in episodeListElements) {
|
||||
var epNum =
|
||||
substringAfterLast(element.selectFirst("h3").text, "Episode ");
|
||||
var type = element.selectFirst("span.type")?.text ?? "RAW";
|
||||
var date = element.selectFirst("span.time")?.text ?? "";
|
||||
MChapter ep = MChapter();
|
||||
ep.name = "$type: Episode $epNum".trim();
|
||||
ep.url = element.getHref;
|
||||
if (date.isNotEmpty)
|
||||
ep.dateUpload = parseDates([element.selectFirst("span.time")?.text],
|
||||
"yyyy-MM-dd HH:mm:ss", "en")
|
||||
.first;
|
||||
episodesList.add(ep);
|
||||
}
|
||||
|
||||
anime.chapters = episodesList;
|
||||
return anime;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<MVideo>> getVideoList(String url) async {
|
||||
url = getUrlWithoutDomain(url);
|
||||
|
||||
final res = (await client.get(Uri.parse("$baseUrl$url"))).body;
|
||||
final document = parseHtml(res);
|
||||
String iframeUrl = document.selectFirst("iframe")?.getSrc ?? "";
|
||||
if (iframeUrl.isEmpty) return [];
|
||||
if (iframeUrl.startsWith("//")) {
|
||||
iframeUrl = "https:$iframeUrl";
|
||||
}
|
||||
var iframeDoc = parseHtml((await client.get(Uri.parse(iframeUrl))).body);
|
||||
final serverElements = iframeDoc.select("ul.list-server-items li");
|
||||
List<MVideo> videos = [];
|
||||
for (var serverElement in serverElements) {
|
||||
var url = serverElement.attr("data-video");
|
||||
List<MVideo> a = [];
|
||||
if (url.contains("dood")) {
|
||||
a = await doodExtractor(url, "DoodStream");
|
||||
} else if (url.contains("dwish")) {
|
||||
a = await streamWishExtractor(url, "StreamWish");
|
||||
} else if (url.contains("streamtape")) {
|
||||
a = await streamTapeExtractor(url, "StreamTape");
|
||||
}
|
||||
videos.addAll(a);
|
||||
}
|
||||
return sortVideos(videos, source.id);
|
||||
}
|
||||
|
||||
@override
|
||||
List<dynamic> getSourcePreferences() {
|
||||
return [
|
||||
EditTextPreference(
|
||||
key: "overrideBaseUrl",
|
||||
title: "Override BaseUrl",
|
||||
summary: "",
|
||||
value: "https://dramacool.pa",
|
||||
dialogTitle: "Override BaseUrl",
|
||||
dialogMessage: "",
|
||||
text: "https://dramacool.pa"),
|
||||
ListPreference(
|
||||
key: "preferred_quality",
|
||||
title: "Preferred quality",
|
||||
summary: "",
|
||||
valueIndex: 0,
|
||||
entries: [
|
||||
"1080p",
|
||||
"720p",
|
||||
"480p",
|
||||
"360p",
|
||||
"Doodstream",
|
||||
"StreamTape"
|
||||
],
|
||||
entryValues: [
|
||||
"1080",
|
||||
"720",
|
||||
"480",
|
||||
"360",
|
||||
"Doodstream",
|
||||
"StreamTape"
|
||||
])
|
||||
];
|
||||
}
|
||||
|
||||
MPages animeFromElement(List<MElement> elements, bool hasNextPage) {
|
||||
List<MManga> animeList = [];
|
||||
for (var element in elements) {
|
||||
MManga anime = MManga();
|
||||
anime.name = element.selectFirst("h3")?.text ?? "Serie";
|
||||
anime.imageUrl = (element.selectFirst("img")?.attr("data-original") ?? "")
|
||||
.replaceAll(" ", "%20") ??
|
||||
"";
|
||||
anime.link = element.getHref;
|
||||
animeList.add(anime);
|
||||
}
|
||||
return MPages(animeList, hasNextPage);
|
||||
}
|
||||
|
||||
List<MVideo> sortVideos(List<MVideo> videos, int sourceId) {
|
||||
String quality = getPreferenceValue(sourceId, "preferred_quality");
|
||||
|
||||
videos.sort((MVideo a, MVideo b) {
|
||||
int qualityMatchA = 0;
|
||||
if (a.quality.contains(quality)) {
|
||||
qualityMatchA = 1;
|
||||
}
|
||||
int qualityMatchB = 0;
|
||||
if (b.quality.contains(quality)) {
|
||||
qualityMatchB = 1;
|
||||
}
|
||||
if (qualityMatchA != qualityMatchB) {
|
||||
return qualityMatchB - qualityMatchA;
|
||||
}
|
||||
|
||||
final regex = RegExp(r'(\d+)p');
|
||||
final matchA = regex.firstMatch(a.quality);
|
||||
final matchB = regex.firstMatch(b.quality);
|
||||
final int qualityNumA = int.tryParse(matchA?.group(1) ?? '0') ?? 0;
|
||||
final int qualityNumB = int.tryParse(matchB?.group(1) ?? '0') ?? 0;
|
||||
return qualityNumB - qualityNumA;
|
||||
});
|
||||
|
||||
return videos;
|
||||
}
|
||||
}
|
||||
|
||||
DramaCool main(MSource source) {
|
||||
return DramaCool(source: source);
|
||||
}
|
||||
BIN
dart/anime/src/en/dramacool/icon.png
Normal file
|
After Width: | Height: | Size: 6.8 KiB |
16
dart/anime/src/en/dramacool/source.dart
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
import '../../../../../model/source.dart';
|
||||
|
||||
Source get dramacoolSource => _dramacoolSource;
|
||||
const _dramacoolVersion = "0.0.25";
|
||||
const _dramacoolSourceCodeUrl =
|
||||
"https://raw.githubusercontent.com/kodjodevf/mangayomi-extensions/$branchName/dart/anime/src/en/dramacool/dramacool.dart";
|
||||
Source _dramacoolSource = Source(
|
||||
name: "DramaCool",
|
||||
baseUrl: "https://dramacool.pa",
|
||||
lang: "en",
|
||||
typeSource: "single",
|
||||
iconUrl:
|
||||
"https://raw.githubusercontent.com/kodjodevf/mangayomi-extensions/$branchName/dart/anime/src/en/dramacool/icon.png",
|
||||
sourceCodeUrl: _dramacoolSourceCodeUrl,
|
||||
version: _dramacoolVersion,
|
||||
itemType: ItemType.anime);
|
||||
1137
dart/anime/src/en/gogoanime/gogoanime.dart
Normal file
BIN
dart/anime/src/en/gogoanime/icon.png
Normal file
|
After Width: | Height: | Size: 2.9 KiB |
16
dart/anime/src/en/gogoanime/source.dart
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
import '../../../../../model/source.dart';
|
||||
|
||||
Source get gogoanimeSource => _gogoanimeSource;
|
||||
const _gogoanimeVersion = "0.1.15";
|
||||
const _gogoanimeSourceCodeUrl =
|
||||
"https://raw.githubusercontent.com/kodjodevf/mangayomi-extensions/$branchName/dart/anime/src/en/gogoanime/gogoanime.dart";
|
||||
Source _gogoanimeSource = Source(
|
||||
name: "Gogoanime",
|
||||
baseUrl: "https://anitaku.to",
|
||||
lang: "en",
|
||||
typeSource: "single",
|
||||
iconUrl:
|
||||
"https://raw.githubusercontent.com/kodjodevf/mangayomi-extensions/$branchName/dart/anime/src/en/gogoanime/icon.png",
|
||||
sourceCodeUrl: _gogoanimeSourceCodeUrl,
|
||||
version: _gogoanimeVersion,
|
||||
itemType: ItemType.anime);
|
||||
BIN
dart/anime/src/en/kisskh/icon.png
Normal file
|
After Width: | Height: | Size: 2.9 KiB |
184
dart/anime/src/en/kisskh/kisskh.dart
Normal file
|
|
@ -0,0 +1,184 @@
|
|||
import 'package:mangayomi/bridge_lib.dart';
|
||||
import 'dart:convert';
|
||||
|
||||
class KissKh extends MProvider {
|
||||
KissKh({required this.source});
|
||||
|
||||
MSource source;
|
||||
|
||||
final Client client = Client(source);
|
||||
|
||||
@override
|
||||
Future<MPages> getPopular(int page) async {
|
||||
final res = (await client.get(Uri.parse(
|
||||
"${source.baseUrl}/api/DramaList/List?page=$page&type=0&sub=0&country=0&status=0&order=1&pageSize=40")))
|
||||
.body;
|
||||
final jsonRes = json.decode(res);
|
||||
final datas = jsonRes["data"];
|
||||
List<MManga> animeList = [];
|
||||
|
||||
for (var data in datas) {
|
||||
var anime = MManga();
|
||||
anime.name = data["title"];
|
||||
anime.imageUrl = data["thumbnail"] ?? "";
|
||||
anime.link =
|
||||
"${source.baseUrl}/api/DramaList/Drama/${data["id"]}?isq=false";
|
||||
animeList.add(anime);
|
||||
}
|
||||
|
||||
int lastPage = jsonRes["totalCount"];
|
||||
int pages = jsonRes["page"];
|
||||
return MPages(animeList, pages < lastPage);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<MPages> getLatestUpdates(int page) async {
|
||||
final res = (await client.get(Uri.parse(
|
||||
"${source.baseUrl}/api/DramaList/List?page=$page&type=0&sub=0&country=0&status=0&order=12&pageSize=40")))
|
||||
.body;
|
||||
final jsonRes = json.decode(res);
|
||||
final datas = jsonRes["data"];
|
||||
|
||||
List<MManga> animeList = [];
|
||||
|
||||
for (var data in datas) {
|
||||
var anime = MManga();
|
||||
anime.name = data["title"];
|
||||
anime.imageUrl = data["thumbnail"] ?? "";
|
||||
anime.link =
|
||||
"${source.baseUrl}/api/DramaList/Drama/${data["id"]}?isq=false";
|
||||
animeList.add(anime);
|
||||
}
|
||||
|
||||
int lastPage = jsonRes["totalCount"];
|
||||
int pages = jsonRes["page"];
|
||||
return MPages(animeList, pages < lastPage);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<MPages> search(String query, int page, FilterList filterList) async {
|
||||
final res = (await client.get(Uri.parse(
|
||||
"${source.baseUrl}/api/DramaList/Search?q=$query&type=0")))
|
||||
.body;
|
||||
final jsonRes = json.decode(res);
|
||||
List<MManga> animeList = [];
|
||||
for (var data in jsonRes) {
|
||||
var anime = MManga();
|
||||
anime.name = data["title"];
|
||||
anime.imageUrl = data["thumbnail"] ?? "";
|
||||
anime.link =
|
||||
"${source.baseUrl}/api/DramaList/Drama/${data["id"]}?isq=false";
|
||||
animeList.add(anime);
|
||||
}
|
||||
return MPages(animeList, false);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<MManga> getDetail(String url) async {
|
||||
final statusList = [
|
||||
{"Ongoing": 0, "Completed": 1}
|
||||
];
|
||||
final res = (await client.get(Uri.parse(url))).body;
|
||||
var anime = MManga();
|
||||
final jsonRes = json.decode(res);
|
||||
final status = jsonRes["status"] ?? "";
|
||||
anime.description = jsonRes["description"];
|
||||
anime.status = parseStatus(status, statusList);
|
||||
anime.imageUrl = jsonRes["thumbnail"];
|
||||
var episodes = jsonRes["episodes"];
|
||||
String type = jsonRes["type"];
|
||||
final episodesCount = jsonRes["episodesCount"] as int;
|
||||
final containsAnime = type.contains("Anime");
|
||||
final containsTVSeries = type.contains("TVSeries");
|
||||
final containsHollywood = type.contains("Hollywood");
|
||||
final containsMovie = type.contains("Movie");
|
||||
List<MChapter>? episodesList = [];
|
||||
|
||||
for (var a in episodes) {
|
||||
MChapter episode = MChapter();
|
||||
String number = (a["number"] as double).toString().replaceAll(".0", "");
|
||||
final id = a["id"];
|
||||
if (containsAnime || containsTVSeries) {
|
||||
episode.name = "Episode $number";
|
||||
} else if (containsHollywood && episodesCount == 1 || containsMovie) {
|
||||
episode.name = "Movie";
|
||||
} else if (containsHollywood && episodesCount > 1) {
|
||||
episode.name = "Episode $number";
|
||||
}
|
||||
episode.url =
|
||||
"${source.baseUrl}/api/DramaList/Episode/$id.png?err=false&ts=&time=";
|
||||
episodesList.add(episode);
|
||||
}
|
||||
|
||||
anime.chapters = episodesList;
|
||||
return anime;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<MVideo>> getVideoList(String url) async {
|
||||
final res = (await client.get(Uri.parse(url))).body;
|
||||
final id = substringAfter(substringBefore(url, ".png"), "Episode/");
|
||||
final jsonRes = json.decode(res);
|
||||
|
||||
final subRes =
|
||||
(await client.get(Uri.parse("${source.baseUrl}/api/Sub/$id"))).body;
|
||||
var jsonSubRes = json.decode(subRes);
|
||||
List<MTrack> subtitles = [];
|
||||
|
||||
for (var sub in jsonSubRes) {
|
||||
final subUrl = sub["src"] as String;
|
||||
final label = sub["label"];
|
||||
if (subUrl.endsWith("txt")) {
|
||||
var subtitle = await getSubtitle(subUrl, label);
|
||||
subtitles.add(subtitle);
|
||||
} else {
|
||||
var subtitle = MTrack();
|
||||
subtitle
|
||||
..label = label
|
||||
..file = subUrl;
|
||||
subtitles.add(subtitle);
|
||||
}
|
||||
}
|
||||
final videoUrl = jsonRes["Video"];
|
||||
var video = MVideo();
|
||||
video
|
||||
..url = videoUrl
|
||||
..originalUrl = videoUrl
|
||||
..quality = "kisskh"
|
||||
..subtitles = subtitles
|
||||
..headers = {
|
||||
"referer": "https://kisskh.me/",
|
||||
"origin": "https://kisskh.me"
|
||||
};
|
||||
return [video];
|
||||
}
|
||||
|
||||
Future<MTrack> getSubtitle(String subUrl, String subLang) async {
|
||||
final response = await client.get(Uri.parse(subUrl), headers: {
|
||||
"referer": "https://kisskh.me/",
|
||||
"origin": "https://kisskh.me"
|
||||
});
|
||||
final subtitleData = response.body;
|
||||
String decrypted = "\n";
|
||||
for (String line in subtitleData.split('\n')) {
|
||||
decrypted += "${decrypt(line.trim())}\n";
|
||||
}
|
||||
var subtitle = MTrack();
|
||||
subtitle
|
||||
..label = subLang
|
||||
..file = decrypted;
|
||||
return subtitle;
|
||||
}
|
||||
|
||||
String decrypt(String data) {
|
||||
final key = utf8.decode(
|
||||
[56, 48, 53, 54, 52, 56, 51, 54, 52, 54, 51, 50, 56, 55, 54, 51]);
|
||||
final iv = utf8.decode(
|
||||
[54, 56, 53, 50, 54, 49, 50, 51, 55, 48, 49, 56, 53, 50, 55, 51]);
|
||||
return cryptoHandler(data, iv, key, false);
|
||||
}
|
||||
}
|
||||
|
||||
KissKh main(MSource source) {
|
||||
return KissKh(source: source);
|
||||
}
|
||||
16
dart/anime/src/en/kisskh/source.dart
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
import '../../../../../model/source.dart';
|
||||
|
||||
Source get kisskhSource => _kisskhSource;
|
||||
const _kisskhVersion = "0.0.6";
|
||||
const _kisskhSourceCodeUrl =
|
||||
"https://raw.githubusercontent.com/kodjodevf/mangayomi-extensions/$branchName/dart/anime/src/en/kisskh/kisskh.dart";
|
||||
Source _kisskhSource = Source(
|
||||
name: "KissKH",
|
||||
baseUrl: "https://kisskh.co",
|
||||
lang: "en",
|
||||
typeSource: "single",
|
||||
iconUrl:
|
||||
"https://raw.githubusercontent.com/kodjodevf/mangayomi-extensions/$branchName/dart/anime/src/en/kisskh/icon.png",
|
||||
sourceCodeUrl: _kisskhSourceCodeUrl,
|
||||
version: _kisskhVersion,
|
||||
itemType: ItemType.anime);
|
||||
BIN
dart/anime/src/en/nineanimetv/icon.png
Normal file
|
After Width: | Height: | Size: 6.5 KiB |
548
dart/anime/src/en/nineanimetv/nineanimetv.dart
Normal file
|
|
@ -0,0 +1,548 @@
|
|||
import 'package:mangayomi/bridge_lib.dart';
|
||||
import 'dart:convert';
|
||||
|
||||
class NineAnimeTv extends MProvider {
|
||||
NineAnimeTv({required this.source});
|
||||
|
||||
MSource source;
|
||||
|
||||
final Client client = Client(source);
|
||||
|
||||
@override
|
||||
Future<MPages> getPopular(int page) async {
|
||||
final res = (await client
|
||||
.get(Uri.parse("${source.baseUrl}/filter?sort=all&page=$page")))
|
||||
.body;
|
||||
return parseAnimeList(res);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<MPages> getLatestUpdates(int page) async {
|
||||
final res = (await client.get(Uri.parse(
|
||||
"${source.baseUrl}/filter?sort=recently_updated&page=$page")))
|
||||
.body;
|
||||
return parseAnimeList(res);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<MPages> search(String query, int page, FilterList filterList) async {
|
||||
final filters = filterList.filters;
|
||||
String url = "${source.baseUrl}/filter?keyword=$query";
|
||||
|
||||
for (var filter in filters) {
|
||||
if (filter.type == "GenreFilter") {
|
||||
final genre = (filter.state as List).where((e) => e.state).toList();
|
||||
url += "${ll(url)}genre=";
|
||||
if (genre.isNotEmpty) {
|
||||
for (var st in genre) {
|
||||
url += "${st.value}";
|
||||
if (genre.length > 1) {
|
||||
url += "%2C";
|
||||
}
|
||||
}
|
||||
if (genre.length > 1) {
|
||||
url = substringBeforeLast(url, '%2C');
|
||||
}
|
||||
}
|
||||
} else if (filter.type == "SeasonFilter") {
|
||||
final season = (filter.state as List).where((e) => e.state).toList();
|
||||
url += "${ll(url)}season=";
|
||||
if (season.isNotEmpty) {
|
||||
for (var st in season) {
|
||||
url += "${st.value}";
|
||||
if (season.length > 1) {
|
||||
url += "%2C";
|
||||
}
|
||||
}
|
||||
if (season.length > 1) {
|
||||
url = substringBeforeLast(url, '%2C');
|
||||
}
|
||||
}
|
||||
} else if (filter.type == "YearFilter") {
|
||||
final year = (filter.state as List).where((e) => e.state).toList();
|
||||
url += "${ll(url)}year=";
|
||||
if (year.isNotEmpty) {
|
||||
for (var st in year) {
|
||||
url += "${st.value}";
|
||||
if (year.length > 1) {
|
||||
url += "%2C";
|
||||
}
|
||||
}
|
||||
if (year.length > 1) {
|
||||
url = substringBeforeLast(url, '%2C');
|
||||
}
|
||||
}
|
||||
} else if (filter.type == "TypeFilter") {
|
||||
final type = (filter.state as List).where((e) => e.state).toList();
|
||||
url += "${ll(url)}type=";
|
||||
if (type.isNotEmpty) {
|
||||
for (var st in type) {
|
||||
url += "${st.value}";
|
||||
if (type.length > 1) {
|
||||
url += "%2C";
|
||||
}
|
||||
}
|
||||
if (type.length > 1) {
|
||||
url = substringBeforeLast(url, '%2C');
|
||||
}
|
||||
}
|
||||
} else if (filter.type == "StatusFilter") {
|
||||
final status = filter.values[filter.state].value;
|
||||
url += "${ll(url)}status=$status";
|
||||
} else if (filter.type == "LanguageFilter") {
|
||||
final language = (filter.state as List).where((e) => e.state).toList();
|
||||
url += "${ll(url)}language=";
|
||||
if (language.isNotEmpty) {
|
||||
for (var st in language) {
|
||||
url += "${st.value}";
|
||||
if (language.length > 1) {
|
||||
url += "%2C";
|
||||
}
|
||||
}
|
||||
if (language.length > 1) {
|
||||
url = substringBeforeLast(url, '%2C');
|
||||
}
|
||||
}
|
||||
} else if (filter.type == "SortFilter") {
|
||||
final sort = filter.values[filter.state].value;
|
||||
url += "${ll(url)}sort=$sort";
|
||||
}
|
||||
}
|
||||
|
||||
final res = (await client.get(Uri.parse("$url&page=$page"))).body;
|
||||
return parseAnimeList(res);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<MManga> getDetail(String url) async {
|
||||
final statusList = [
|
||||
{"Currently Airing": 0, "Finished Airing": 1}
|
||||
];
|
||||
|
||||
final res = (await client.get(Uri.parse("${source.baseUrl}$url"))).body;
|
||||
MManga anime = MManga();
|
||||
final document = parseHtml(res);
|
||||
final infoElement = document.selectFirst("div.film-infor");
|
||||
final status = infoElement.xpathFirst(
|
||||
'//div[contains(text(),"Status:")]/following-sibling::div/span/text()') ??
|
||||
"";
|
||||
anime.status = parseStatus(status, statusList);
|
||||
anime.description =
|
||||
infoElement.selectFirst("div.film-description > p")?.text ?? "";
|
||||
anime.author = infoElement.xpathFirst(
|
||||
'//div[contains(text(),"Studios:")]/following-sibling::div/a/text()') ??
|
||||
"";
|
||||
|
||||
anime.genre = infoElement.xpath(
|
||||
'//div[contains(text(),"Genre:")]/following-sibling::div/a/text()');
|
||||
final id = parseHtml(res).selectFirst("div[data-id]").attr("data-id");
|
||||
|
||||
final resEp =
|
||||
(await client.get(Uri.parse("${source.baseUrl}/ajax/episode/list/$id")))
|
||||
.body;
|
||||
final html = json.decode(resEp)["html"];
|
||||
|
||||
List<MChapter>? episodesList = [];
|
||||
|
||||
final epsElements = parseHtml(html).select("a");
|
||||
for (var epElement in epsElements) {
|
||||
final id = epElement.attr('data-id');
|
||||
|
||||
final title = epElement.attr('title') ?? "";
|
||||
|
||||
final epNum = epElement.attr('data-number');
|
||||
|
||||
MChapter episode = MChapter();
|
||||
episode.name = "Episode $epNum $title";
|
||||
episode.url = id;
|
||||
episodesList.add(episode);
|
||||
}
|
||||
anime.chapters = episodesList.reversed.toList();
|
||||
return anime;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<MVideo>> getVideoList(String url) async {
|
||||
final res = (await client.get(
|
||||
Uri.parse("${source.baseUrl}/ajax/episode/servers?episodeId=$url")))
|
||||
.body;
|
||||
|
||||
final html = json.decode(res)["html"];
|
||||
|
||||
final serverElements = parseHtml(html).select("div.server-item");
|
||||
|
||||
List<MVideo> videos = [];
|
||||
final hosterSelection = preferenceHosterSelection(source.id);
|
||||
final typeSelection = preferenceTypeSelection(source.id);
|
||||
for (var serverElement in serverElements) {
|
||||
final name = serverElement.text;
|
||||
final id = serverElement.attr("data-id");
|
||||
final subDub = serverElement.attr("data-type");
|
||||
final res = (await client
|
||||
.get(Uri.parse("${source.baseUrl}/ajax/episode/sources?id=$id")))
|
||||
.body;
|
||||
final epUrl = json.decode(res)["link"];
|
||||
List<MVideo> a = [];
|
||||
|
||||
if (hosterSelection.contains(name) && typeSelection.contains(subDub)) {
|
||||
if (name.contains("Vidstreaming")) {
|
||||
a = await rapidCloudExtractor(epUrl, "Vidstreaming - $subDub");
|
||||
} else if (name.contains("Vidcloud")) {
|
||||
a = await rapidCloudExtractor(epUrl, "Vidcloud - $subDub");
|
||||
}
|
||||
videos.addAll(a);
|
||||
}
|
||||
}
|
||||
|
||||
return sortVideos(videos, source.id);
|
||||
}
|
||||
|
||||
MPages parseAnimeList(String res) {
|
||||
final elements = parseHtml(res).select("div.film_list-wrap > div");
|
||||
List<MManga> animeList = [];
|
||||
for (var element in elements) {
|
||||
MManga anime = MManga();
|
||||
anime.name = element.selectFirst("div.film-detail > h3 > a").text;
|
||||
anime.imageUrl = element.selectFirst(" div.film-poster > img").getSrc;
|
||||
anime.link = element.selectFirst("div.film-detail > h3 > a").getHref;
|
||||
animeList.add(anime);
|
||||
}
|
||||
|
||||
return MPages(animeList, true);
|
||||
}
|
||||
|
||||
Future<List<MVideo>> rapidCloudExtractor(String url, String name) async {
|
||||
final serverUrl = ['https://megacloud.tv', 'https://rapid-cloud.co'];
|
||||
|
||||
final serverType = url.startsWith('https://megacloud.tv') ? 0 : 1;
|
||||
final sourceUrl = [
|
||||
'/embed-2/ajax/e-1/getSources?id=',
|
||||
'/ajax/embed-6-v2/getSources?id='
|
||||
];
|
||||
final sourceSpliter = ['/e-1/', '/embed-6-v2/'];
|
||||
final id = url.split(sourceSpliter[serverType]).last.split('?').first;
|
||||
final resServer = (await client.get(
|
||||
Uri.parse('${serverUrl[serverType]}${sourceUrl[serverType]}$id'),
|
||||
headers: {"X-Requested-With": "XMLHttpRequest"}))
|
||||
.body;
|
||||
final encrypted = getMapValue(resServer, "encrypted");
|
||||
String videoResJson = "";
|
||||
List<MVideo> videos = [];
|
||||
if (encrypted == "true") {
|
||||
final ciphered = getMapValue(resServer, "sources");
|
||||
List<List<int>> indexPairs = await generateIndexPairs(serverType);
|
||||
var password = '';
|
||||
String ciphertext = ciphered;
|
||||
int index = 0;
|
||||
for (List<int> item in json.decode(json.encode(indexPairs))) {
|
||||
int start = item.first + index;
|
||||
int end = start + item.last;
|
||||
String passSubstr = ciphered.substring(start, end);
|
||||
password += passSubstr;
|
||||
ciphertext = ciphertext.replaceFirst(passSubstr, "");
|
||||
index += item.last;
|
||||
}
|
||||
videoResJson = decryptAESCryptoJS(ciphertext, password);
|
||||
} else {
|
||||
videoResJson = json.encode(
|
||||
(json.decode(resServer)["sources"] as List<Map<String, dynamic>>));
|
||||
}
|
||||
|
||||
String masterUrl =
|
||||
((json.decode(videoResJson) as List<Map<String, dynamic>>)
|
||||
.first)['file'];
|
||||
String type = ((json.decode(videoResJson) as List<Map<String, dynamic>>)
|
||||
.first)['type'];
|
||||
|
||||
final tracks = (json.decode(resServer)['tracks'] as List)
|
||||
.where((e) => e['kind'] == 'captions' ? true : false)
|
||||
.toList();
|
||||
List<MTrack> subtitles = [];
|
||||
|
||||
for (var sub in tracks) {
|
||||
try {
|
||||
MTrack subtitle = MTrack();
|
||||
subtitle
|
||||
..label = sub["label"]
|
||||
..file = sub["file"];
|
||||
subtitles.add(subtitle);
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
if (type == "hls") {
|
||||
final masterPlaylistRes = (await client.get(Uri.parse(masterUrl))).body;
|
||||
|
||||
for (var it in substringAfter(masterPlaylistRes, "#EXT-X-STREAM-INF:")
|
||||
.split("#EXT-X-STREAM-INF:")) {
|
||||
final quality =
|
||||
"${substringBefore(substringBefore(substringAfter(substringAfter(it, "RESOLUTION="), "x"), ","), "\n")}p";
|
||||
|
||||
String videoUrl = substringBefore(substringAfter(it, "\n"), "\n");
|
||||
|
||||
if (!videoUrl.startsWith("http")) {
|
||||
videoUrl =
|
||||
"${masterUrl.split("/").sublist(0, masterUrl.split("/").length - 1).join("/")}/$videoUrl";
|
||||
}
|
||||
|
||||
MVideo video = MVideo();
|
||||
video
|
||||
..url = videoUrl
|
||||
..originalUrl = videoUrl
|
||||
..quality = "$name - $quality"
|
||||
..subtitles = subtitles;
|
||||
videos.add(video);
|
||||
}
|
||||
} else {
|
||||
MVideo video = MVideo();
|
||||
video
|
||||
..url = masterUrl
|
||||
..originalUrl = masterUrl
|
||||
..quality = "$name - Default"
|
||||
..subtitles = subtitles;
|
||||
videos.add(video);
|
||||
}
|
||||
return videos;
|
||||
}
|
||||
|
||||
Future<List<List<int>>> generateIndexPairs(int serverType) async {
|
||||
final jsPlayerUrl = [
|
||||
"https://megacloud.tv/js/player/a/prod/e1-player.min.js",
|
||||
"https://rapid-cloud.co/js/player/prod/e6-player-v2.min.js"
|
||||
];
|
||||
final scriptText =
|
||||
(await client.get(Uri.parse(jsPlayerUrl[serverType]))).body;
|
||||
|
||||
final switchCode = scriptText.substring(
|
||||
scriptText.lastIndexOf('switch'), scriptText.indexOf('=partKey'));
|
||||
|
||||
List<int> indexes = [];
|
||||
for (var variableMatch
|
||||
in RegExp(r'=(\w+)').allMatches(switchCode).toList()) {
|
||||
final regex = RegExp(
|
||||
',${(variableMatch as RegExpMatch).group(1)}=((?:0x)?([0-9a-fA-F]+))');
|
||||
Match? match = regex.firstMatch(scriptText);
|
||||
|
||||
if (match != null) {
|
||||
String value = match.group(1);
|
||||
if (value.contains("0x")) {
|
||||
indexes.add(int.parse(substringAfter(value, "0x"), radix: 16));
|
||||
} else {
|
||||
indexes.add(int.parse(value));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return chunked(indexes, 2);
|
||||
}
|
||||
|
||||
List<List<int>> chunked(List<int> list, int size) {
|
||||
List<List<int>> chunks = [];
|
||||
for (int i = 0; i < list.length; i += size) {
|
||||
int end = list.length;
|
||||
if (i + size < list.length) {
|
||||
end = i + size;
|
||||
}
|
||||
chunks.add(list.sublist(i, end));
|
||||
}
|
||||
return chunks;
|
||||
}
|
||||
|
||||
@override
|
||||
List<dynamic> getFilterList() {
|
||||
return [
|
||||
GroupFilter("GenreFilter", "Genre", [
|
||||
CheckBoxFilter("Action", "1"),
|
||||
CheckBoxFilter("Adventure", "2"),
|
||||
CheckBoxFilter("Cars", "3"),
|
||||
CheckBoxFilter("Comedy", "4"),
|
||||
CheckBoxFilter("Dementia", "5"),
|
||||
CheckBoxFilter("Demons", "6"),
|
||||
CheckBoxFilter("Drama", "8"),
|
||||
CheckBoxFilter("Ecchi", "9"),
|
||||
CheckBoxFilter("Fantasy", "10"),
|
||||
CheckBoxFilter("Game", "11"),
|
||||
CheckBoxFilter("Harem", "35"),
|
||||
CheckBoxFilter("Historical", "13"),
|
||||
CheckBoxFilter("Horror", "14"),
|
||||
CheckBoxFilter("Isekai", "44"),
|
||||
CheckBoxFilter("Josei", "43"),
|
||||
CheckBoxFilter("Kids", "15"),
|
||||
CheckBoxFilter("Magic", "16"),
|
||||
CheckBoxFilter("Martial Arts", "17"),
|
||||
CheckBoxFilter("Mecha", "18"),
|
||||
CheckBoxFilter("Military", "38"),
|
||||
CheckBoxFilter("Music", "19"),
|
||||
CheckBoxFilter("Mystery", "7"),
|
||||
CheckBoxFilter("Parody", "20"),
|
||||
CheckBoxFilter("Police", "39"),
|
||||
CheckBoxFilter("Psychological", "40"),
|
||||
CheckBoxFilter("Romance", "22"),
|
||||
CheckBoxFilter("Samurai", "21"),
|
||||
CheckBoxFilter("School", "23"),
|
||||
CheckBoxFilter("Sci-Fi", "24"),
|
||||
CheckBoxFilter("Seinen", "42"),
|
||||
CheckBoxFilter("Shoujo", "25"),
|
||||
CheckBoxFilter("Shoujo Ai", "26"),
|
||||
CheckBoxFilter("Shounen", "27"),
|
||||
CheckBoxFilter("Shounen Ai", "28"),
|
||||
CheckBoxFilter("Slice of Life", "36"),
|
||||
CheckBoxFilter("Space", "29"),
|
||||
CheckBoxFilter("Sports", "30"),
|
||||
CheckBoxFilter("Super Power", "31"),
|
||||
CheckBoxFilter("Supernatural", "37"),
|
||||
CheckBoxFilter("Thriller", "41"),
|
||||
CheckBoxFilter("Vampire", "32")
|
||||
]),
|
||||
GroupFilter("SeasonFilter", "Season", [
|
||||
CheckBoxFilter("Fall", "3"),
|
||||
CheckBoxFilter("Summer", "2"),
|
||||
CheckBoxFilter("Spring", "1"),
|
||||
CheckBoxFilter("Winter", "4")
|
||||
]),
|
||||
GroupFilter("YearFilter", "Year", [
|
||||
CheckBoxFilter("2024", "2024"),
|
||||
CheckBoxFilter("2023", "2023"),
|
||||
CheckBoxFilter("2022", "2022"),
|
||||
CheckBoxFilter("2021", "2021"),
|
||||
CheckBoxFilter("2020", "2020"),
|
||||
CheckBoxFilter("2019", "2019"),
|
||||
CheckBoxFilter("2018", "2018"),
|
||||
CheckBoxFilter("2017", "2017"),
|
||||
CheckBoxFilter("2016", "2016"),
|
||||
CheckBoxFilter("2015", "2015"),
|
||||
CheckBoxFilter("2014", "2014"),
|
||||
CheckBoxFilter("2013", "2013"),
|
||||
CheckBoxFilter("2012", "2012"),
|
||||
CheckBoxFilter("2011", "2011"),
|
||||
CheckBoxFilter("2010", "2010"),
|
||||
CheckBoxFilter("2009", "2009"),
|
||||
CheckBoxFilter("2008", "2008"),
|
||||
CheckBoxFilter("2007", "2007"),
|
||||
CheckBoxFilter("2006", "2006"),
|
||||
CheckBoxFilter("2005", "2005"),
|
||||
CheckBoxFilter("2004", "2004"),
|
||||
CheckBoxFilter("2003", "2003"),
|
||||
CheckBoxFilter("2002", "2002"),
|
||||
CheckBoxFilter("2001", "2001")
|
||||
]),
|
||||
SelectFilter("SortFilter", "Sort by", 0, [
|
||||
SelectFilterOption("All", "all"),
|
||||
SelectFilterOption("Default", "default"),
|
||||
SelectFilterOption("Recently Added", "recently_added"),
|
||||
SelectFilterOption("Recently Updated", "recently_updated"),
|
||||
SelectFilterOption("Score", "score"),
|
||||
SelectFilterOption("Name A-Z", "name_az"),
|
||||
SelectFilterOption("Released Date", "released_date"),
|
||||
SelectFilterOption("Most Watched", "most_watched")
|
||||
]),
|
||||
GroupFilter("TypeFilter", "Type", [
|
||||
CheckBoxFilter("Movie", "1"),
|
||||
CheckBoxFilter("TV Series", "2"),
|
||||
CheckBoxFilter("OVA", "3"),
|
||||
CheckBoxFilter("ONA", "4"),
|
||||
CheckBoxFilter("Special", "5"),
|
||||
CheckBoxFilter("Music", "6")
|
||||
]),
|
||||
SelectFilter("StatusFilter", "Status", 0, [
|
||||
SelectFilterOption("All", "all"),
|
||||
SelectFilterOption("Finished Airing", "1"),
|
||||
SelectFilterOption("Currently Airing", "2"),
|
||||
SelectFilterOption("Not yet aired", "3")
|
||||
]),
|
||||
GroupFilter("LanguageFilter", "Language",
|
||||
[CheckBoxFilter("Sub", "sub"), CheckBoxFilter("Dub", "dub")]),
|
||||
];
|
||||
}
|
||||
|
||||
@override
|
||||
List<dynamic> getSourcePreferences() {
|
||||
return [
|
||||
ListPreference(
|
||||
key: "preferred_quality",
|
||||
title: "Preferred Quality",
|
||||
summary: "",
|
||||
valueIndex: 1,
|
||||
entries: ["1080p", "720p", "480p", "360p"],
|
||||
entryValues: ["1080", "720", "480", "360"]),
|
||||
ListPreference(
|
||||
key: "preferred_server",
|
||||
title: "Preferred server",
|
||||
summary: "",
|
||||
valueIndex: 0,
|
||||
entries: ["Vidstreaming", "VidCloud"],
|
||||
entryValues: ["Vidstreaming", "VidCloud"]),
|
||||
ListPreference(
|
||||
key: "preferred_type",
|
||||
title: "Preferred Type",
|
||||
summary: "",
|
||||
valueIndex: 0,
|
||||
entries: ["Sub", "Dub"],
|
||||
entryValues: ["sub", "dub"]),
|
||||
MultiSelectListPreference(
|
||||
key: "hoster_selection",
|
||||
title: "Enable/Disable Hosts",
|
||||
summary: "",
|
||||
entries: ["Vidstreaming", "VidCloud"],
|
||||
entryValues: ["Vidstreaming", "Vidcloud"],
|
||||
values: ["Vidstreaming", "Vidcloud"]),
|
||||
MultiSelectListPreference(
|
||||
key: "type_selection",
|
||||
title: "Enable/Disable Types",
|
||||
summary: "",
|
||||
entries: ["Sub", "Dub"],
|
||||
entryValues: ["sub", "dub"],
|
||||
values: ["sub", "dub"]),
|
||||
];
|
||||
}
|
||||
|
||||
List<MVideo> sortVideos(List<MVideo> videos, int sourceId) {
|
||||
String quality = getPreferenceValue(sourceId, "preferred_quality");
|
||||
String server = getPreferenceValue(sourceId, "preferred_server");
|
||||
String type = getPreferenceValue(sourceId, "preferred_type");
|
||||
videos.sort((MVideo a, MVideo b) {
|
||||
int qualityMatchA = 0;
|
||||
|
||||
if (a.quality.contains(quality) &&
|
||||
a.quality.toLowerCase().contains(type.toLowerCase()) &&
|
||||
a.quality.toLowerCase().contains(server.toLowerCase())) {
|
||||
qualityMatchA = 1;
|
||||
}
|
||||
int qualityMatchB = 0;
|
||||
if (b.quality.contains(quality) &&
|
||||
b.quality.toLowerCase().contains(type.toLowerCase()) &&
|
||||
b.quality.toLowerCase().contains(server.toLowerCase())) {
|
||||
qualityMatchB = 1;
|
||||
}
|
||||
if (qualityMatchA != qualityMatchB) {
|
||||
return qualityMatchB - qualityMatchA;
|
||||
}
|
||||
|
||||
final regex = RegExp(r'(\d+)p');
|
||||
final matchA = regex.firstMatch(a.quality);
|
||||
final matchB = regex.firstMatch(b.quality);
|
||||
final int qualityNumA = int.tryParse(matchA?.group(1) ?? '0') ?? 0;
|
||||
final int qualityNumB = int.tryParse(matchB?.group(1) ?? '0') ?? 0;
|
||||
return qualityNumB - qualityNumA;
|
||||
});
|
||||
return videos;
|
||||
}
|
||||
|
||||
List<String> preferenceHosterSelection(int sourceId) {
|
||||
return getPreferenceValue(sourceId, "hoster_selection");
|
||||
}
|
||||
|
||||
List<String> preferenceTypeSelection(int sourceId) {
|
||||
return getPreferenceValue(sourceId, "type_selection");
|
||||
}
|
||||
|
||||
String ll(String url) {
|
||||
if (url.contains("?")) {
|
||||
return "&";
|
||||
}
|
||||
return "?";
|
||||
}
|
||||
}
|
||||
|
||||
NineAnimeTv main(MSource source) {
|
||||
return NineAnimeTv(source: source);
|
||||
}
|
||||
16
dart/anime/src/en/nineanimetv/source.dart
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
import '../../../../../model/source.dart';
|
||||
|
||||
Source get nineanimetv => _nineanimetv;
|
||||
const _nineanimetvVersion = "0.0.4";
|
||||
const _nineanimetvCodeUrl =
|
||||
"https://raw.githubusercontent.com/kodjodevf/mangayomi-extensions/$branchName/dart/anime/src/en/nineanimetv/nineanimetv.dart";
|
||||
Source _nineanimetv = Source(
|
||||
name: "9AnimeTv",
|
||||
baseUrl: "https://9animetv.to",
|
||||
lang: "en",
|
||||
typeSource: "single",
|
||||
iconUrl:
|
||||
"https://raw.githubusercontent.com/kodjodevf/mangayomi-extensions/$branchName/dart/anime/src/en/nineanimetv/icon.png",
|
||||
sourceCodeUrl: _nineanimetvCodeUrl,
|
||||
version: _nineanimetvVersion,
|
||||
itemType: ItemType.anime);
|
||||
BIN
dart/anime/src/en/uhdmovies/icon.png
Normal file
|
After Width: | Height: | Size: 3.5 KiB |
16
dart/anime/src/en/uhdmovies/source.dart
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
import '../../../../../model/source.dart';
|
||||
|
||||
Source get uhdmoviesSource => _uhdmoviesSource;
|
||||
const _uhdmoviesVersion = "0.0.45";
|
||||
const _uhdmoviesSourceCodeUrl =
|
||||
"https://raw.githubusercontent.com/kodjodevf/mangayomi-extensions/$branchName/dart/anime/src/en/uhdmovies/uhdmovies.dart";
|
||||
Source _uhdmoviesSource = Source(
|
||||
name: "UHD Movies",
|
||||
baseUrl: "https://uhdmovies.fans",
|
||||
lang: "en",
|
||||
typeSource: "single",
|
||||
iconUrl:
|
||||
"https://raw.githubusercontent.com/kodjodevf/mangayomi-extensions/$branchName/dart/anime/src/en/uhdmovies/icon.png",
|
||||
sourceCodeUrl: _uhdmoviesSourceCodeUrl,
|
||||
version: _uhdmoviesVersion,
|
||||
itemType: ItemType.anime);
|
||||
234
dart/anime/src/en/uhdmovies/uhdmovies.dart
Normal file
|
|
@ -0,0 +1,234 @@
|
|||
import 'package:mangayomi/bridge_lib.dart';
|
||||
import 'dart:convert';
|
||||
|
||||
class UHDMovies extends MProvider {
|
||||
UHDMovies({required this.source});
|
||||
|
||||
MSource source;
|
||||
|
||||
final Client client = Client(source);
|
||||
|
||||
@override
|
||||
bool get supportsLatest => false;
|
||||
|
||||
@override
|
||||
String get baseUrl => getPreferenceValue(source.id, "pref_domain_new");
|
||||
|
||||
@override
|
||||
Future<MPages> getPopular(int page) async {
|
||||
final res = (await client.get(Uri.parse("$baseUrl/page/$page"))).body;
|
||||
return animeFromElement(res);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<MPages> getLatestUpdates(int page) async {
|
||||
return MPages([], false);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<MPages> search(String query, int page, FilterList filterList) async {
|
||||
final res = (await client.get(
|
||||
Uri.parse("$baseUrl/page/$page/?s=${query.replaceAll(" ", "+")}")))
|
||||
.body;
|
||||
return animeFromElement(res);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<MManga> getDetail(String url) async {
|
||||
url = getUrlWithoutDomain(url);
|
||||
final res = (await client.get(Uri.parse("$baseUrl${url}"))).body;
|
||||
MManga anime = MManga();
|
||||
final description = xpath(res, '//pre/span/text()');
|
||||
if (description.isNotEmpty) {
|
||||
anime.description = description.first;
|
||||
}
|
||||
anime.status = MStatus.ongoing;
|
||||
final episodesTitles = xpath(res,
|
||||
'//*[contains(@style, "center") or contains(@class, "maxbutton")]/a[contains(@class, "maxbutton") or contains(@href, "?sid=")]/text()');
|
||||
final episodesUrls = xpath(res,
|
||||
'//*[contains(@style, "center") or contains(@class, "maxbutton")]/a[contains(@class, "maxbutton") or contains(@href, "?sid=")]/@href');
|
||||
bool isSeries = false;
|
||||
if (episodesTitles.first.contains("Episode") ||
|
||||
episodesTitles.first.contains("Zip") ||
|
||||
episodesTitles.first.contains("Pack")) {
|
||||
isSeries = true;
|
||||
}
|
||||
List<MChapter>? episodesList = [];
|
||||
if (!isSeries) {
|
||||
List<String> moviesTitles = [];
|
||||
moviesTitles = xpath(res,
|
||||
'//*[contains(@style, "center") or contains(@class, "maxbutton")]/parent::p//preceding-sibling::p[contains(@style, "center")]/text()');
|
||||
List<String> titles = [];
|
||||
if (moviesTitles.isEmpty) {
|
||||
moviesTitles = xpath(res, '//p[contains(@style, "center")]/text()');
|
||||
}
|
||||
for (var title in moviesTitles) {
|
||||
if (title.isNotEmpty &&
|
||||
!title.contains('Download') &&
|
||||
!title.contains('Note:') &&
|
||||
!title.contains('Copyright')) {
|
||||
titles.add(title.split('[').first.trim());
|
||||
}
|
||||
}
|
||||
for (var i = 0; i < titles.length; i++) {
|
||||
final title = titles[i];
|
||||
final quality = RegExp(r'\d{3,4}p').firstMatch(title)?.group(0) ?? "";
|
||||
final url = episodesUrls[i];
|
||||
MChapter ep = MChapter();
|
||||
ep.name = title;
|
||||
ep.url = url;
|
||||
ep.scanlator = quality;
|
||||
episodesList.add(ep);
|
||||
}
|
||||
} else {
|
||||
List<String> seasonTitles = [];
|
||||
final episodeTitles = xpath(res,
|
||||
'//*[contains(@style, "center") or contains(@class, "maxbutton")]/parent::p//preceding-sibling::p[contains(@style, "center") and not(text()^="Episode")]/text()');
|
||||
List<String> titles = [];
|
||||
for (var title in episodeTitles) {
|
||||
if (title.isNotEmpty) {
|
||||
titles.add(title.split('[').first.trim());
|
||||
}
|
||||
}
|
||||
int number = 0;
|
||||
for (var i = 0; i < episodesTitles.length; i++) {
|
||||
final episode = episodesTitles[i];
|
||||
final episodeUrl = episodesUrls[i];
|
||||
if (!episode.contains("Zip") || !episode.contains("Pack")) {
|
||||
if (episode == "Episode 1" && seasonTitles.contains("Episode 1")) {
|
||||
number++;
|
||||
} else if (episode == "Episode 1") {
|
||||
seasonTitles.add(episode);
|
||||
}
|
||||
final season =
|
||||
RegExp(r'S(\d{2})').firstMatch(titles[number])?.group(1) ?? "";
|
||||
final quality =
|
||||
RegExp(r'\d{3,4}p').firstMatch(titles[number])?.group(0) ?? "";
|
||||
MChapter ep = MChapter();
|
||||
ep.name = "Season $season $episode $quality";
|
||||
ep.url = episodeUrl;
|
||||
ep.scanlator = quality;
|
||||
episodesList.add(ep);
|
||||
}
|
||||
}
|
||||
}
|
||||
anime.chapters = episodesList.reversed.toList();
|
||||
return anime;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<MVideo>> getVideoList(String url) async {
|
||||
final res = await getMediaUrl(url);
|
||||
return await extractVideos(res);
|
||||
}
|
||||
|
||||
@override
|
||||
List<dynamic> getSourcePreferences() {
|
||||
return [
|
||||
EditTextPreference(
|
||||
key: "pref_domain_new",
|
||||
title: "Currently used domain",
|
||||
summary: "",
|
||||
value: "https://uhdmovies.fans",
|
||||
dialogTitle: "Currently used domain",
|
||||
dialogMessage: "",
|
||||
text: "https://uhdmovies.fans"),
|
||||
];
|
||||
}
|
||||
|
||||
Future<List<MVideo>> extractVideos(String url) async {
|
||||
List<MVideo> videos = [];
|
||||
for (int type = 1; type < 3; type++) {
|
||||
url = url.replaceAll("/file/", "/wfile/") + "?type=$type";
|
||||
final res = (await client.get(Uri.parse(url))).body;
|
||||
final links = xpath(res, '//div[@class="mb-4"]/a/@href');
|
||||
for (int i = 0; i < links.length; i++) {
|
||||
final link = links[i];
|
||||
String decodedLink = link;
|
||||
if (!link.contains("workers.dev")) {
|
||||
decodedLink = utf8
|
||||
.decode(base64Url.decode(substringAfter(link, "download?url=")));
|
||||
}
|
||||
MVideo video = MVideo();
|
||||
video
|
||||
..url = decodedLink
|
||||
..originalUrl = decodedLink
|
||||
..quality = "CF $type Worker ${i + 1}";
|
||||
videos.add(video);
|
||||
}
|
||||
}
|
||||
return videos;
|
||||
}
|
||||
|
||||
Future<String> getMediaUrl(String url) async {
|
||||
String res = "";
|
||||
String host = "";
|
||||
if (url.contains("?sid=")) {
|
||||
final finalUrl = await redirectorBypasser(url);
|
||||
host = Uri.parse(finalUrl).host;
|
||||
res = (await client.get(Uri.parse(finalUrl))).body;
|
||||
} else if (url.contains("r?key=")) {
|
||||
res = (await client.get(Uri.parse(url))).body;
|
||||
host = Uri.parse(url).host;
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
final path = substringBefore(substringAfter(res, "replace(\""), "\"");
|
||||
if (path == "/404") return "";
|
||||
return "https://$host$path";
|
||||
}
|
||||
|
||||
Future<String> redirectorBypasser(String url) async {
|
||||
final res = (await client.get(Uri.parse(url))).body;
|
||||
String lastDoc = await recursiveDoc(url, res);
|
||||
final js = xpath(lastDoc, '//script[contains(text(), "/?go=")]/text()');
|
||||
if (js.isEmpty) return "";
|
||||
String script = js.first;
|
||||
String nextUrl =
|
||||
substringBefore(substringAfter(script, "\"href\",\""), '"');
|
||||
if (!nextUrl.contains("http")) return "";
|
||||
String cookieName = substringAfter(nextUrl, "go=");
|
||||
String cookieValue =
|
||||
substringBefore(substringAfter(script, "'$cookieName', '"), "'");
|
||||
final response = (await client.get(Uri.parse(nextUrl),
|
||||
headers: {"referer": url, "Cookie": "$cookieName=$cookieValue"}))
|
||||
.body;
|
||||
|
||||
final lastRes =
|
||||
parseHtml(response).selectFirst("meta[http-equiv]").attr("content");
|
||||
return substringAfter(lastRes, "url=");
|
||||
}
|
||||
|
||||
MPages animeFromElement(String res) {
|
||||
List<MManga> animeList = [];
|
||||
final urls = xpath(res, '//*[@class="entry-image"]/a/@href');
|
||||
final names = xpath(res, '//*[@class="entry-image"]/a/@title');
|
||||
final images = xpath(res, '//*[@class="entry-image"]/a/img/@src');
|
||||
|
||||
for (var i = 0; i < names.length; i++) {
|
||||
MManga anime = MManga();
|
||||
anime.name = names[i].replaceAll("Download", "");
|
||||
anime.imageUrl = images[i];
|
||||
anime.link = urls[i];
|
||||
animeList.add(anime);
|
||||
}
|
||||
final nextPage = xpath(res, '//a[@class="next page-numbers"]/@href');
|
||||
return MPages(animeList, nextPage.isNotEmpty);
|
||||
}
|
||||
|
||||
Future<String> recursiveDoc(String url, String html) async {
|
||||
final urlR = xpath(html, '//form[@id="landing"]/@action');
|
||||
if (urlR.isEmpty) return html;
|
||||
final name = xpath(html, '//input/@name').first;
|
||||
final value = xpath(html, '//input/@value').first;
|
||||
final body = {"$name": value};
|
||||
final response = (await client.post(Uri.parse(urlR.first),
|
||||
headers: {"referer": url}, body: body))
|
||||
.body;
|
||||
return recursiveDoc(url, response);
|
||||
}
|
||||
}
|
||||
|
||||
UHDMovies main(MSource source) {
|
||||
return UHDMovies(source: source);
|
||||
}
|
||||
311
dart/anime/src/es/animeonlineninja/animeonlineninja.dart
Normal file
|
|
@ -0,0 +1,311 @@
|
|||
import 'package:mangayomi/bridge_lib.dart';
|
||||
import 'dart:convert';
|
||||
|
||||
class AnimeOnlineNinja extends MProvider {
|
||||
AnimeOnlineNinja({required this.source});
|
||||
|
||||
MSource source;
|
||||
|
||||
final Client client = Client(source);
|
||||
|
||||
@override
|
||||
bool get supportsLatest => false;
|
||||
|
||||
@override
|
||||
Future<MPages> getPopular(int page) async {
|
||||
final res =
|
||||
(await client.get(Uri.parse("${source.baseUrl}/tendencias"))).body;
|
||||
return parseAnimeList(res);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<MPages> search(String query, int page, FilterList filterList) async {
|
||||
String pageStr = page == 1 ? "" : "page/$page/";
|
||||
final res = (await client.get(Uri.parse(
|
||||
"${source.baseUrl}/$pageStr?s=${query.replaceAll(" ", "+")}")))
|
||||
.body;
|
||||
return parseAnimeList(res,
|
||||
selector: "div.result-item div.image a",
|
||||
hasNextPage: parseHtml(res)
|
||||
.selectFirst(
|
||||
"div.pagination > *:last-child:not(span):not(.current)")
|
||||
?.text !=
|
||||
null);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<MManga> getDetail(String url) async {
|
||||
final res = (await client.get(Uri.parse("${source.baseUrl}$url"))).body;
|
||||
MManga anime = MManga();
|
||||
final document = parseHtml(res);
|
||||
anime.description = document.selectFirst("div#info").text;
|
||||
anime.genre = document
|
||||
.selectFirst("div.sheader")
|
||||
.select("div.data > div.sgeneros > a")
|
||||
.map((e) => e.text)
|
||||
.toList();
|
||||
|
||||
List<MChapter>? episodesList = [];
|
||||
final seasonElements = document.select("div#seasons > div");
|
||||
if (seasonElements.isEmpty) {
|
||||
MChapter episode = MChapter();
|
||||
episode.name = "Película";
|
||||
episode.url = getUrlWithoutDomain(url);
|
||||
episodesList.add(episode);
|
||||
} else {
|
||||
for (var seasonElement in seasonElements) {
|
||||
final seasonName = seasonElement.selectFirst("span.se-t").text;
|
||||
for (var epElement in seasonElement.select("ul.episodios > li")) {
|
||||
final href = epElement.selectFirst("a[href]");
|
||||
final epNum = epElement.selectFirst('div.numerando')?.text ?? "0 - 0";
|
||||
MChapter episode = MChapter();
|
||||
episode.name =
|
||||
"Season $seasonName x ${substringAfter(epNum, '- ')} ${href.text}";
|
||||
episode.url = getUrlWithoutDomain(href!.getHref);
|
||||
episodesList.add(episode);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
anime.chapters = episodesList.reversed.toList();
|
||||
return anime;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<MVideo>> getVideoList(String url) async {
|
||||
final res = (await client.get(Uri.parse("${source.baseUrl}$url"))).body;
|
||||
final document = parseHtml(res);
|
||||
final players = document.select("ul#playeroptionsul li");
|
||||
List<MVideo> videos = [];
|
||||
for (var player in players) {
|
||||
final name = player.selectFirst("span.title").text;
|
||||
final type = player.attr("data-type");
|
||||
final id = player.attr("data-post");
|
||||
final num = player.attr("data-nume");
|
||||
final resUrl = (await client.get(Uri.parse(
|
||||
"${source.baseUrl}/wp-json/dooplayer/v1/post/$id?type=$type&source=$num")))
|
||||
.body;
|
||||
final url =
|
||||
substringBefore(substringAfter(resUrl, "\"embed_url\":\""), "\",")
|
||||
.replaceAll("\\", "");
|
||||
videos.addAll(await extractVideos(url, name));
|
||||
}
|
||||
return sortVideos(videos, source.id);
|
||||
}
|
||||
|
||||
Future<List<MVideo>> extractVideos(String url, String lang) async {
|
||||
List<MVideo> videos = [];
|
||||
List<MVideo> a = [];
|
||||
if (url.contains("saidochesto.top") || lang == "MULTISERVER") {
|
||||
return await extractFromMulti(url);
|
||||
} else if (["filemoon", "moon", "filemooon"].any((a) => url.contains(a))) {
|
||||
a = await filemoonExtractor(url, "$lang Filemoon - ", "");
|
||||
} else if (["https://dood", "https://ds2play", "https://d0"]
|
||||
.any((a) => url.contains(a))) {
|
||||
a = await doodExtractor(url, "$lang DoodStream");
|
||||
} else if (["streamtape", "stp", "stape"].any((a) => url.contains(a))) {
|
||||
a = await streamTapeExtractor(url, "$lang StreamTape");
|
||||
} else if (url.contains("uqload")) {
|
||||
a = await uqloadExtractor(url, lang);
|
||||
} else if (url.contains("wolfstream")) {
|
||||
final resUrl = (await client.get(Uri.parse(url))).body;
|
||||
final jsData =
|
||||
parseHtml(resUrl).selectFirst("script:contains(sources)").text;
|
||||
final videoUrl =
|
||||
substringBefore(substringAfter(jsData, "{file:\""), "\"");
|
||||
|
||||
MVideo video = MVideo();
|
||||
video
|
||||
..url = videoUrl
|
||||
..originalUrl = videoUrl
|
||||
..quality = "$lang WolfStream";
|
||||
|
||||
a = [video];
|
||||
} else if (["wishembed", "streamwish", "strwish", "wish"]
|
||||
.any((a) => url.contains(a))) {
|
||||
a = await streamWishExtractor(url, "$lang StreamWish");
|
||||
} else if (url.contains("mp4upload")) {
|
||||
a = await mp4UploadExtractor(url, null, "$lang", "");
|
||||
} else if (["vidhide", "filelions.top", "vid."]
|
||||
.any((a) => url.contains(a))) {
|
||||
a = await streamHideExtractor(url, lang);
|
||||
}
|
||||
videos.addAll(a);
|
||||
|
||||
return videos;
|
||||
}
|
||||
|
||||
Future<List<MVideo>> streamHideExtractor(String url, String prefix) async {
|
||||
final res = (await client.get(Uri.parse(url))).body;
|
||||
final masterUrl = substringBefore(
|
||||
substringAfter(
|
||||
substringAfter(
|
||||
substringAfter(unpackJs(res), "sources:"), "file:\""),
|
||||
"src:\""),
|
||||
'"');
|
||||
final masterPlaylistRes = (await client.get(Uri.parse(masterUrl))).body;
|
||||
List<MVideo> videos = [];
|
||||
for (var it in substringAfter(masterPlaylistRes, "#EXT-X-STREAM-INF:")
|
||||
.split("#EXT-X-STREAM-INF:")) {
|
||||
final quality =
|
||||
"${substringBefore(substringBefore(substringAfter(substringAfter(it, "RESOLUTION="), "x"), ","), "\n")}p";
|
||||
|
||||
String videoUrl = substringBefore(substringAfter(it, "\n"), "\n");
|
||||
|
||||
if (!videoUrl.startsWith("http")) {
|
||||
videoUrl =
|
||||
"${masterUrl.split("/").sublist(0, masterUrl.split("/").length - 1).join("/")}/$videoUrl";
|
||||
}
|
||||
|
||||
MVideo video = MVideo();
|
||||
video
|
||||
..url = videoUrl
|
||||
..originalUrl = videoUrl
|
||||
..quality = "$prefix StreamHideVid - $quality";
|
||||
videos.add(video);
|
||||
}
|
||||
return videos;
|
||||
}
|
||||
|
||||
Future<List<MVideo>> uqloadExtractor(String url, String lang) async {
|
||||
final Client client =
|
||||
Client(source, json.encode({"useDartHttpClient": true}));
|
||||
final res = (await client.get(Uri.parse(url))).body;
|
||||
final js = xpath(res, '//script[contains(text(), "sources:")]/text()');
|
||||
if (js.isEmpty) {
|
||||
return [];
|
||||
}
|
||||
|
||||
final videoUrl =
|
||||
substringBefore(substringAfter(js.first, "sources: [\""), '"');
|
||||
MVideo video = MVideo();
|
||||
video
|
||||
..url = videoUrl
|
||||
..originalUrl = videoUrl
|
||||
..quality = "$lang Uqload"
|
||||
..headers = {"Referer": "${Uri.parse(url).origin}/"};
|
||||
return [video];
|
||||
}
|
||||
|
||||
Future<List<MVideo>> extractFromMulti(String url) async {
|
||||
final res = (await client.get(Uri.parse(url))).body;
|
||||
final document = parseHtml(res);
|
||||
|
||||
final prefLang = getPreferenceValue(source.id, "preferred_lang");
|
||||
String langSelector = "";
|
||||
if (prefLang.isEmpty) {
|
||||
langSelector = "div";
|
||||
} else {
|
||||
langSelector = "div.OD_$prefLang";
|
||||
}
|
||||
List<MVideo> videos = [];
|
||||
for (var element in document.select("div.ODDIV $langSelector > li")) {
|
||||
final hosterUrl =
|
||||
substringBefore(substringAfter(element.attr("onclick"), "('"), "')");
|
||||
String lang = "";
|
||||
if (langSelector == "div") {
|
||||
lang = substringBefore(
|
||||
substringAfter(element.parent?.attr("class"), "OD_", ""), " ");
|
||||
} else {
|
||||
lang = prefLang;
|
||||
}
|
||||
videos.addAll(await extractVideos(hosterUrl, lang));
|
||||
}
|
||||
|
||||
return videos;
|
||||
}
|
||||
|
||||
MPages parseAnimeList(String res,
|
||||
{String selector = "article.w_item_a > a", bool hasNextPage = false}) {
|
||||
final elements = parseHtml(res).select(selector);
|
||||
List<MManga> animeList = [];
|
||||
for (var element in elements) {
|
||||
final url = getUrlWithoutDomain(element.getHref);
|
||||
if (!url.startsWith("/episodio/")) {
|
||||
MManga anime = MManga();
|
||||
final img = element.selectFirst("img");
|
||||
anime.name = img.attr("alt");
|
||||
anime.imageUrl = img?.attr("data-src") ??
|
||||
img?.attr("data-lazy-src") ??
|
||||
img?.attr("srcset") ??
|
||||
img?.getSrc;
|
||||
anime.link = url;
|
||||
animeList.add(anime);
|
||||
}
|
||||
}
|
||||
return MPages(animeList, hasNextPage);
|
||||
}
|
||||
|
||||
@override
|
||||
List<dynamic> getSourcePreferences() {
|
||||
return [
|
||||
ListPreference(
|
||||
key: "preferred_lang",
|
||||
title: "Preferred language",
|
||||
summary: "",
|
||||
valueIndex: 0,
|
||||
entries: ["SUB", "All", "ES", "LAT"],
|
||||
entryValues: ["SUB", "", "ES", "LAT"]),
|
||||
ListPreference(
|
||||
key: "preferred_server1",
|
||||
title: "Preferred server",
|
||||
summary: "",
|
||||
valueIndex: 0,
|
||||
entries: [
|
||||
"Filemoon",
|
||||
"DoodStream",
|
||||
"StreamTape",
|
||||
"Uqload",
|
||||
"WolfStream",
|
||||
"saidochesto.top",
|
||||
"VidHide",
|
||||
"StreamWish",
|
||||
"Mp4Upload"
|
||||
],
|
||||
entryValues: [
|
||||
"Filemoon",
|
||||
"DoodStream",
|
||||
"StreamTape",
|
||||
"Uqload",
|
||||
"WolfStream",
|
||||
"saidochesto.top",
|
||||
"VidHide",
|
||||
"StreamWish",
|
||||
"Mp4Upload"
|
||||
]),
|
||||
];
|
||||
}
|
||||
|
||||
List<MVideo> sortVideos(List<MVideo> videos, int sourceId) {
|
||||
String prefLang = getPreferenceValue(source.id, "preferred_lang");
|
||||
String server = getPreferenceValue(sourceId, "preferred_server1");
|
||||
videos.sort((MVideo a, MVideo b) {
|
||||
int qualityMatchA = 0;
|
||||
|
||||
if (a.quality.toLowerCase().contains(prefLang.toLowerCase()) &&
|
||||
a.quality.toLowerCase().contains(server.toLowerCase())) {
|
||||
qualityMatchA = 1;
|
||||
}
|
||||
int qualityMatchB = 0;
|
||||
if (b.quality.toLowerCase().contains(prefLang.toLowerCase()) &&
|
||||
b.quality.toLowerCase().contains(server.toLowerCase())) {
|
||||
qualityMatchB = 1;
|
||||
}
|
||||
if (qualityMatchA != qualityMatchB) {
|
||||
return qualityMatchB - qualityMatchA;
|
||||
}
|
||||
|
||||
final regex = RegExp(r'(\d+)p');
|
||||
final matchA = regex.firstMatch(a.quality);
|
||||
final matchB = regex.firstMatch(b.quality);
|
||||
final int qualityNumA = int.tryParse(matchA?.group(1) ?? '0') ?? 0;
|
||||
final int qualityNumB = int.tryParse(matchB?.group(1) ?? '0') ?? 0;
|
||||
return qualityNumB - qualityNumA;
|
||||
});
|
||||
return videos;
|
||||
}
|
||||
}
|
||||
|
||||
AnimeOnlineNinja main(MSource source) {
|
||||
return AnimeOnlineNinja(source: source);
|
||||
}
|
||||
BIN
dart/anime/src/es/animeonlineninja/icon.png
Normal file
|
After Width: | Height: | Size: 3.9 KiB |
16
dart/anime/src/es/animeonlineninja/source.dart
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
import '../../../../../model/source.dart';
|
||||
|
||||
Source get animeonlineninjaSource => _animeonlineninjaSource;
|
||||
const _animeonlineninjaVersion = "0.0.3";
|
||||
const _animeonlineninjaSourceCodeUrl =
|
||||
"https://raw.githubusercontent.com/kodjodevf/mangayomi-extensions/$branchName/dart/anime/src/es/animeonlineninja/animeonlineninja.dart";
|
||||
Source _animeonlineninjaSource = Source(
|
||||
name: "AnimeOnline.Ninja",
|
||||
baseUrl: "https://ww3.animeonline.ninja",
|
||||
lang: "es",
|
||||
typeSource: "single",
|
||||
iconUrl:
|
||||
"https://raw.githubusercontent.com/kodjodevf/mangayomi-extensions/$branchName/dart/anime/src/es/animeonlineninja/icon.png",
|
||||
sourceCodeUrl: _animeonlineninjaSourceCodeUrl,
|
||||
version: _animeonlineninjaVersion,
|
||||
itemType: ItemType.anime);
|
||||
498
dart/anime/src/fr/animesama/animesama.dart
Normal file
|
|
@ -0,0 +1,498 @@
|
|||
import 'package:mangayomi/bridge_lib.dart';
|
||||
import 'dart:convert';
|
||||
|
||||
class AnimeSama extends MProvider {
|
||||
AnimeSama({required this.source});
|
||||
|
||||
MSource source;
|
||||
|
||||
final Client client = Client(source);
|
||||
|
||||
@override
|
||||
Future<MPages> getPopular(int page) async {
|
||||
final doc = (await client.get(Uri.parse("${source.baseUrl}/#$page"))).body;
|
||||
final regex = RegExp(r"""^\s*carteClassique\(\s*.*?\s*,\s*"(.*?)".*\)""",
|
||||
multiLine: true);
|
||||
var matches = regex.allMatches(doc).toList();
|
||||
List<List<RegExpMatch>> chunks = chunked(matches, 5);
|
||||
List<MManga> seasons = [];
|
||||
if (page > 0 && page <= chunks.length) {
|
||||
for (RegExpMatch match in chunks[page - 1]) {
|
||||
seasons.addAll(await fetchAnimeSeasons(
|
||||
"${source.baseUrl}/catalogue/${match.group(1)}"));
|
||||
}
|
||||
}
|
||||
return MPages(seasons, page < chunks.length);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<MPages> getLatestUpdates(int page) async {
|
||||
final res = (await client.get(Uri.parse(source.baseUrl))).body;
|
||||
var document = parseHtml(res);
|
||||
final latest = document
|
||||
.select("h2")
|
||||
.where((MElement e) =>
|
||||
e.outerHtml.toLowerCase().contains("derniers épisodes ajoutés"))
|
||||
.toList();
|
||||
final seasonElements = (latest.first.parent.nextElementSibling as MElement)
|
||||
.select("div")
|
||||
.toList();
|
||||
List<MManga> seasons = [];
|
||||
for (var seasonElement in seasonElements) {
|
||||
seasons.addAll(await fetchAnimeSeasons(
|
||||
(seasonElement as MElement).getElementsByTagName("a").first.getHref));
|
||||
}
|
||||
return MPages(seasons, false);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<MPages> search(String query, int page, FilterList filterList) async {
|
||||
final filters = filterList.filters;
|
||||
final res = (await client
|
||||
.get(Uri.parse("${source.baseUrl}/catalogue/listing_all.php")))
|
||||
.body;
|
||||
var databaseElements = parseHtml(res).select(".cardListAnime");
|
||||
List<MElement> elements = [];
|
||||
elements = databaseElements
|
||||
.where((MElement element) => element.select("h1, p").any((MElement e) =>
|
||||
e.text.toLowerCase().contains(query.toLowerCase().trim())))
|
||||
.toList();
|
||||
for (var filter in filters) {
|
||||
if (filter.type == "TypeFilter") {
|
||||
final types = (filter.state as List).where((e) => e.state).toList();
|
||||
elements = elements
|
||||
.where((MElement element) =>
|
||||
types.isEmpty ||
|
||||
types.any((p) => element.className.contains(p.value)))
|
||||
.toList();
|
||||
} else if (filter.type == "LanguageFilter") {
|
||||
final language = (filter.state as List).where((e) => e.state).toList();
|
||||
elements = elements
|
||||
.where((MElement element) =>
|
||||
language.isEmpty ||
|
||||
language.any((p) => element.className.contains(p.value)))
|
||||
.toList();
|
||||
} else if (filter.type == "GenreFilter") {
|
||||
final included = (filter.state as List)
|
||||
.where((e) => e.state == 1 ? true : false)
|
||||
.toList();
|
||||
final excluded = (filter.state as List)
|
||||
.where((e) => e.state == 2 ? true : false)
|
||||
.toList();
|
||||
if (included.isNotEmpty) {
|
||||
elements = elements
|
||||
.where((MElement element) =>
|
||||
included.every((p) => element.className.contains(p.value)))
|
||||
.toList();
|
||||
}
|
||||
if (excluded.isNotEmpty) {
|
||||
elements = elements
|
||||
.where((MElement element) =>
|
||||
excluded.every((p) => element.className.contains(p.value)))
|
||||
.toList();
|
||||
}
|
||||
}
|
||||
}
|
||||
List<List<MElement>> chunks = chunked(elements, 5);
|
||||
if (chunks.isEmpty) return MPages([], false);
|
||||
List<MManga> seasons = [];
|
||||
for (var seasonElement in chunks[page - 1]) {
|
||||
seasons.addAll(await fetchAnimeSeasons(
|
||||
seasonElement.getElementsByTagName("a").first.getHref));
|
||||
}
|
||||
|
||||
return MPages(seasons, page < chunks.length);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<MManga> getDetail(String url) async {
|
||||
var animeUrl =
|
||||
"${source.baseUrl}${substringBeforeLast(getUrlWithoutDomain(url), "/")}";
|
||||
var movie =
|
||||
int.tryParse(url.split("#").length >= 2 ? url.split("#")[1] : "");
|
||||
List<Map<String, dynamic>> playersList = [];
|
||||
for (var lang in ["vostfr", "vf"]) {
|
||||
final players = await fetchPlayers("$animeUrl/$lang");
|
||||
if (players.isNotEmpty) {
|
||||
playersList.add({"players": players, "lang": lang});
|
||||
}
|
||||
}
|
||||
int maxLength = 0;
|
||||
for (var sublist in playersList) {
|
||||
for (var innerList in sublist["players"]) {
|
||||
if (innerList.length > maxLength) {
|
||||
maxLength = innerList.length;
|
||||
}
|
||||
}
|
||||
}
|
||||
List<MChapter>? episodesList = [];
|
||||
for (var episodeNumber = 0; episodeNumber < maxLength; episodeNumber++) {
|
||||
List<String> langs = [];
|
||||
bool isVf = false;
|
||||
int iVostfr = 0;
|
||||
int iVf = 0;
|
||||
List<Map<String, dynamic>> players = [];
|
||||
for (var playerList in playersList) {
|
||||
for (var player in playerList["players"]) {
|
||||
if (player.length > episodeNumber) {
|
||||
isVf = playerList["lang"] == "vf";
|
||||
if ((isVf && iVf < 2) || (!isVf && iVostfr < 2)) {
|
||||
var lang = playerList["lang"];
|
||||
if (!langs.contains(lang)) {
|
||||
langs.add(lang);
|
||||
}
|
||||
players.add({"lang": lang, "player": player[episodeNumber]});
|
||||
isVf ? iVf++ : iVostfr++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MChapter episode = MChapter();
|
||||
episode.name = movie == null ? 'Episode ${episodeNumber + 1}' : 'Film';
|
||||
episode.scanlator = langs.toSet().toList().join(', ').toUpperCase();
|
||||
episode.url = json.encode(players);
|
||||
episodesList.add(episode);
|
||||
}
|
||||
|
||||
MManga anime = MManga();
|
||||
anime.chapters =
|
||||
movie == null ? episodesList.reversed.toList() : [episodesList[movie]];
|
||||
return anime;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<MVideo>> getVideoList(String url) async {
|
||||
final players = json.decode(url);
|
||||
List<MVideo> videos = [];
|
||||
for (var player in players) {
|
||||
String lang = (player["lang"] as String).toUpperCase();
|
||||
String playerUrl = player["player"];
|
||||
List<MVideo> a = [];
|
||||
if (playerUrl.contains("sendvid")) {
|
||||
a = await sendVidExtractorr(playerUrl, "$lang ");
|
||||
} else if (playerUrl.contains("vidmoly")) {
|
||||
a = await vidmolyExtractor(playerUrl, lang);
|
||||
}
|
||||
videos.addAll(a);
|
||||
}
|
||||
|
||||
return sortVideos(videos, source.id);
|
||||
}
|
||||
|
||||
Future<List<MVideo>> vidmolyExtractor(String url, String lang) async {
|
||||
final headers = {
|
||||
'Referer': 'https://vidmoly.to',
|
||||
};
|
||||
List<MVideo> videos = [];
|
||||
final playListUrlResponse = (await client.get(Uri.parse(url))).body;
|
||||
final playlistUrl =
|
||||
RegExp(r'file:"(\S+?)"').firstMatch(playListUrlResponse)?.group(1) ??
|
||||
"";
|
||||
if (playlistUrl.isEmpty) return [];
|
||||
final masterPlaylistRes =
|
||||
await client.get(Uri.parse(playlistUrl), headers: headers);
|
||||
|
||||
if (masterPlaylistRes.statusCode == 200) {
|
||||
for (var it
|
||||
in substringAfter(masterPlaylistRes.body, "#EXT-X-STREAM-INF:")
|
||||
.split("#EXT-X-STREAM-INF:")) {
|
||||
final quality =
|
||||
"${substringBefore(substringBefore(substringAfter(substringAfter(it, "RESOLUTION="), "x"), ","), "\n")}p";
|
||||
|
||||
String videoUrl = substringBefore(substringAfter(it, "\n"), "\n");
|
||||
|
||||
MVideo video = MVideo();
|
||||
video
|
||||
..url = videoUrl
|
||||
..originalUrl = videoUrl
|
||||
..quality = "$lang Vidmoly $quality"
|
||||
..headers = headers;
|
||||
videos.add(video);
|
||||
}
|
||||
}
|
||||
|
||||
return videos;
|
||||
}
|
||||
|
||||
Future<List<MVideo>> sendVidExtractorr(String url, String prefix) async {
|
||||
final res = (await client.get(Uri.parse(url))).body;
|
||||
final document = parseHtml(res);
|
||||
final masterUrl = document.selectFirst("source#video_source")?.attr("src");
|
||||
if (masterUrl == null) return [];
|
||||
final masterHeaders = {
|
||||
"Accept": "*/*",
|
||||
"Host": Uri.parse(masterUrl).host,
|
||||
"Origin": "https://${Uri.parse(url).host}",
|
||||
"Referer": "https://${Uri.parse(url).host}/",
|
||||
};
|
||||
List<MVideo> videos = [];
|
||||
if (masterUrl.contains(".m3u8")) {
|
||||
final masterPlaylistRes = (await client.get(Uri.parse(masterUrl))).body;
|
||||
|
||||
for (var it in substringAfter(masterPlaylistRes, "#EXT-X-STREAM-INF:")
|
||||
.split("#EXT-X-STREAM-INF:")) {
|
||||
final quality =
|
||||
"${substringBefore(substringBefore(substringAfter(substringAfter(it, "RESOLUTION="), "x"), ","), "\n")}p";
|
||||
|
||||
String videoUrl = substringBefore(substringAfter(it, "\n"), "\n");
|
||||
|
||||
if (!videoUrl.startsWith("http")) {
|
||||
videoUrl =
|
||||
"${masterUrl.split("/").sublist(0, masterUrl.split("/").length - 1).join("/")}/$videoUrl";
|
||||
}
|
||||
final videoHeaders = {
|
||||
"Accept": "*/*",
|
||||
"Host": Uri.parse(videoUrl).host,
|
||||
"Origin": "https://${Uri.parse(url).host}",
|
||||
"Referer": "https://${Uri.parse(url).host}/",
|
||||
};
|
||||
var video = MVideo();
|
||||
video
|
||||
..url = videoUrl
|
||||
..originalUrl = videoUrl
|
||||
..quality = prefix + "Sendvid:$quality"
|
||||
..headers = videoHeaders;
|
||||
videos.add(video);
|
||||
}
|
||||
} else {
|
||||
var video = MVideo();
|
||||
video
|
||||
..url = masterUrl
|
||||
..originalUrl = masterUrl
|
||||
..quality = prefix + "Sendvid:default"
|
||||
..headers = masterHeaders;
|
||||
videos.add(video);
|
||||
}
|
||||
|
||||
return videos;
|
||||
}
|
||||
|
||||
@override
|
||||
List<dynamic> getFilterList() {
|
||||
return [
|
||||
GroupFilter("TypeFilter", "Type", [
|
||||
CheckBoxFilter("Anime", "Anime"),
|
||||
CheckBoxFilter("Film", "Film"),
|
||||
CheckBoxFilter("Autres", "Autres"),
|
||||
]),
|
||||
GroupFilter("LanguageFilter", "Langue", [
|
||||
CheckBoxFilter("VF", "VF"),
|
||||
CheckBoxFilter("VOSTFR", "VOSTFR"),
|
||||
]),
|
||||
GroupFilter("GenreFilter", "Genre", [
|
||||
TriStateFilter("Action", "Action"),
|
||||
TriStateFilter("Aventure", "Aventure"),
|
||||
TriStateFilter("Combats", "Combats"),
|
||||
TriStateFilter("Comédie", "Comédie"),
|
||||
TriStateFilter("Drame", "Drame"),
|
||||
TriStateFilter("Ecchi", "Ecchi"),
|
||||
TriStateFilter("École", "School-Life"),
|
||||
TriStateFilter("Fantaisie", "Fantasy"),
|
||||
TriStateFilter("Horreur", "Horreur"),
|
||||
TriStateFilter("Isekai", "Isekai"),
|
||||
TriStateFilter("Josei", "Josei"),
|
||||
TriStateFilter("Mystère", "Mystère"),
|
||||
TriStateFilter("Psychologique", "Psychologique"),
|
||||
TriStateFilter("Quotidien", "Slice-of-Life"),
|
||||
TriStateFilter("Romance", "Romance"),
|
||||
TriStateFilter("Seinen", "Seinen"),
|
||||
TriStateFilter("Shônen", "Shônen"),
|
||||
TriStateFilter("Shôjo", "Shôjo"),
|
||||
TriStateFilter("Sports", "Sports"),
|
||||
TriStateFilter("Surnaturel", "Surnaturel"),
|
||||
TriStateFilter("Tournois", "Tournois"),
|
||||
TriStateFilter("Yaoi", "Yaoi"),
|
||||
TriStateFilter("Yuri", "Yuri"),
|
||||
]),
|
||||
];
|
||||
}
|
||||
|
||||
@override
|
||||
List<dynamic> getSourcePreferences() {
|
||||
return [
|
||||
ListPreference(
|
||||
key: "preferred_quality",
|
||||
title: "Qualité préférée",
|
||||
summary: "",
|
||||
valueIndex: 0,
|
||||
entries: ["1080p", "720p", "480p", "360p"],
|
||||
entryValues: ["1080", "720", "480", "360"]),
|
||||
ListPreference(
|
||||
key: "voices_preference",
|
||||
title: "Préférence des voix",
|
||||
summary: "",
|
||||
valueIndex: 0,
|
||||
entries: ["Préférer VOSTFR", "Préférer VF"],
|
||||
entryValues: ["vostfr", "vf"]),
|
||||
];
|
||||
}
|
||||
|
||||
Future<List<MManga>> fetchAnimeSeasons(String url) async {
|
||||
final res = (await client.get(Uri.parse(url))).body;
|
||||
var document = parseHtml(res);
|
||||
String animeName = document.getElementById("titreOeuvre")?.text ?? "";
|
||||
|
||||
var seasonRegex = RegExp(r'panneauAnime\("(.*?)",\s*"(.*?)"\);');
|
||||
var scripts = document
|
||||
.select("h2 + p + div > script, h2 + div > script")
|
||||
.map((MElement element) => element.text)
|
||||
.toList()
|
||||
.join("");
|
||||
List<MManga> animeList = [];
|
||||
List<RegExpMatch> seasonRegexReg = seasonRegex.allMatches(scripts).toList();
|
||||
for (var animeIndex = 0; animeIndex < seasonRegexReg.length; animeIndex++) {
|
||||
final seasonName = seasonRegexReg[animeIndex].group(1);
|
||||
final seasonStem = seasonRegexReg[animeIndex].group(2);
|
||||
if (seasonName != "nom" && seasonStem != "url") {
|
||||
if (seasonStem.toLowerCase().contains("film")) {
|
||||
var moviesUrl = "$url/$seasonStem";
|
||||
var movies = await fetchPlayers(moviesUrl);
|
||||
if (movies.isNotEmpty) {
|
||||
var movieNameRegex =
|
||||
RegExp("^\\s*newSPF\\(\"(.*)\"\\);", multiLine: true);
|
||||
var moviesDoc = (await client.get(Uri.parse(moviesUrl))).body;
|
||||
List<RegExpMatch> matches =
|
||||
movieNameRegex.allMatches(moviesDoc).toList();
|
||||
|
||||
for (var i = 0; i < movies.length; i++) {
|
||||
var title = "";
|
||||
if (animeIndex == 0 && movies.length == 1) {
|
||||
title = animeName;
|
||||
} else if (matches.length > i) {
|
||||
title = "$animeName ${(matches[i]).group(1)}";
|
||||
} else if (movies.length == 1) {
|
||||
title = "$animeName Film";
|
||||
} else {
|
||||
title = "$animeName Film ${i + 1}";
|
||||
}
|
||||
MManga anime = MManga();
|
||||
anime.imageUrl = document.getElementById("coverOeuvre")?.getSrc;
|
||||
anime.genre = (document.xpathFirst(
|
||||
'//h2[contains(text(),"Genres")]/following-sibling::a/text()') ??
|
||||
"")
|
||||
.split(",");
|
||||
anime.description = document.xpathFirst(
|
||||
'//h2[contains(text(),"Synopsis")]/following-sibling::p/text()') ??
|
||||
"";
|
||||
|
||||
anime.name = title;
|
||||
anime.link = "$moviesUrl#$i";
|
||||
anime.status = MStatus.completed;
|
||||
animeList.add(anime);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
MManga anime = MManga();
|
||||
anime.imageUrl = document.getElementById("coverOeuvre")?.getSrc;
|
||||
anime.genre = (document.xpathFirst(
|
||||
'//h2[contains(text(),"Genres")]/following-sibling::a/text()') ??
|
||||
"")
|
||||
.split(",");
|
||||
anime.description = document.xpathFirst(
|
||||
'//h2[contains(text(),"Synopsis")]/following-sibling::p/text()') ??
|
||||
"";
|
||||
anime.name =
|
||||
'$animeName ${substringBefore(seasonName, ',').replaceAll('"', "")}';
|
||||
anime.link = "$url/$seasonStem";
|
||||
animeList.add(anime);
|
||||
}
|
||||
}
|
||||
}
|
||||
return animeList;
|
||||
}
|
||||
|
||||
Future<List<List<String>>> fetchPlayers(String url) async {
|
||||
var docUrl = "$url/episodes.js";
|
||||
List<List<String>> players = [];
|
||||
var response = (await client.get(Uri.parse(docUrl))).body;
|
||||
|
||||
if (response == "error") {
|
||||
return [];
|
||||
}
|
||||
|
||||
var sanitizedDoc = sanitizeEpisodesJs(response);
|
||||
for (var i = 1; i <= 8; i++) {
|
||||
final numPlayers = getPlayers("eps$i", sanitizedDoc);
|
||||
|
||||
if (numPlayers != null) players.add(numPlayers);
|
||||
}
|
||||
|
||||
final asPlayers = getPlayers("epsAS", sanitizedDoc);
|
||||
if (asPlayers != null) players.add(asPlayers);
|
||||
|
||||
if (players.isEmpty) return [];
|
||||
List<List<String>> finalPlayers = [];
|
||||
for (var i = 0; i <= players[0].length; i++) {
|
||||
for (var playerList in players) {
|
||||
if (playerList.length > i) {
|
||||
finalPlayers.add(playerList);
|
||||
}
|
||||
}
|
||||
}
|
||||
return finalPlayers.toSet().toList();
|
||||
}
|
||||
|
||||
List<String>? getPlayers(String playerName, String doc) {
|
||||
var playerRegex = RegExp('$playerName\\s*=\\s*(\\[.*?\\])', dotAll: true);
|
||||
var match = playerRegex.firstMatch(doc);
|
||||
if (match == null) return null;
|
||||
final regex = RegExp(r"""https?://[^\s\',\[\]]+""");
|
||||
final matches = regex.allMatches(match.group(1));
|
||||
List<String> urls = [];
|
||||
for (var match in matches.toList()) {
|
||||
urls.add((match as RegExpMatch).group(0).toString());
|
||||
}
|
||||
return urls;
|
||||
}
|
||||
|
||||
String sanitizeEpisodesJs(String doc) {
|
||||
return doc.replaceAll(
|
||||
RegExp(r'(?<=\[|\,)\s*\"\s*(https?://[^\s\"]+)\s*\"\s*(?=\,|\])'), '');
|
||||
}
|
||||
|
||||
List<List<dynamic>> chunked(List<dynamic> list, int size) {
|
||||
List<List<dynamic>> chunks = [];
|
||||
for (int i = 0; i < list.length; i += size) {
|
||||
int end = list.length;
|
||||
if (i + size < list.length) {
|
||||
end = i + size;
|
||||
}
|
||||
chunks.add(list.sublist(i, end));
|
||||
}
|
||||
return chunks;
|
||||
}
|
||||
|
||||
List<MVideo> sortVideos(List<MVideo> videos, int sourceId) {
|
||||
String quality = getPreferenceValue(sourceId, "preferred_quality");
|
||||
String voice = getPreferenceValue(sourceId, "voices_preference");
|
||||
|
||||
videos.sort((MVideo a, MVideo b) {
|
||||
int qualityMatchA = 0;
|
||||
if (a.quality.contains(quality) &&
|
||||
a.quality.toLowerCase().contains(voice)) {
|
||||
qualityMatchA = 1;
|
||||
}
|
||||
int qualityMatchB = 0;
|
||||
if (b.quality.contains(quality) &&
|
||||
b.quality.toLowerCase().contains(voice)) {
|
||||
qualityMatchB = 1;
|
||||
}
|
||||
if (qualityMatchA != qualityMatchB) {
|
||||
return qualityMatchB - qualityMatchA;
|
||||
}
|
||||
|
||||
final regex = RegExp(r'(\d+)p');
|
||||
final matchA = regex.firstMatch(a.quality);
|
||||
final matchB = regex.firstMatch(b.quality);
|
||||
final int qualityNumA = int.tryParse(matchA?.group(1) ?? '0') ?? 0;
|
||||
final int qualityNumB = int.tryParse(matchB?.group(1) ?? '0') ?? 0;
|
||||
return qualityNumB - qualityNumA;
|
||||
});
|
||||
return videos;
|
||||
}
|
||||
}
|
||||
|
||||
AnimeSama main(MSource source) {
|
||||
return AnimeSama(source: source);
|
||||
}
|
||||
BIN
dart/anime/src/fr/animesama/icon.png
Normal file
|
After Width: | Height: | Size: 4.6 KiB |
16
dart/anime/src/fr/animesama/source.dart
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
import '../../../../../model/source.dart';
|
||||
|
||||
Source get animesamaSource => _animesama;
|
||||
const animesamaVersion = "0.0.4";
|
||||
const animesamaCodeUrl =
|
||||
"https://raw.githubusercontent.com/kodjodevf/mangayomi-extensions/$branchName/dart/anime/src/fr/animesama/animesama.dart";
|
||||
Source _animesama = Source(
|
||||
name: "Anime-Sama",
|
||||
baseUrl: "https://anime-sama.fr",
|
||||
lang: "fr",
|
||||
typeSource: "single",
|
||||
iconUrl:
|
||||
"https://raw.githubusercontent.com/kodjodevf/mangayomi-extensions/$branchName/dart/anime/src/fr/animesama/icon.png",
|
||||
sourceCodeUrl: animesamaCodeUrl,
|
||||
version: animesamaVersion,
|
||||
itemType: ItemType.anime);
|
||||
214
dart/anime/src/fr/animesultra/animesultra.dart
Normal file
|
|
@ -0,0 +1,214 @@
|
|||
import 'package:mangayomi/bridge_lib.dart';
|
||||
import 'dart:convert';
|
||||
|
||||
class AnimesUltra extends MProvider {
|
||||
AnimesUltra({required this.source});
|
||||
|
||||
MSource source;
|
||||
|
||||
final Client client = Client(source);
|
||||
|
||||
@override
|
||||
String get baseUrl => source.baseUrl;
|
||||
|
||||
@override
|
||||
Future<MPages> getPopular(int page) async {
|
||||
final res = (await client.get(Uri.parse(baseUrl))).body;
|
||||
|
||||
List<MManga> animeList = [];
|
||||
final urls = xpath(res,
|
||||
'//*[contains(@class,"swiper-slide item-qtip")]/div[@class="item"]/a/@href');
|
||||
final names = xpath(res,
|
||||
'//*[contains(@class,"swiper-slide item-qtip")]/div[@class="item"]/a/img/@title');
|
||||
final images = xpath(res,
|
||||
'//*[contains(@class,"swiper-slide item-qtip")]/div[@class="item"]/a/img/@data-src');
|
||||
|
||||
for (var i = 0; i < names.length; i++) {
|
||||
MManga anime = MManga();
|
||||
anime.name = names[i];
|
||||
anime.imageUrl = images[i];
|
||||
anime.link = urls[i];
|
||||
animeList.add(anime);
|
||||
}
|
||||
|
||||
return MPages(animeList, false);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<MPages> getLatestUpdates(int page) async {
|
||||
final res = (await client.get(Uri.parse(baseUrl))).body;
|
||||
|
||||
List<MManga> animeList = [];
|
||||
final urls = xpath(res,
|
||||
'//*[@class="block_area block_area_home"]/div[@class="tab-content"]/div[contains(@class,"block_area-content block_area-list")]/div[@class="film_list-wrap"]/div[@class="flw-item"]/div[@class="film-poster"]/a/@href');
|
||||
final names = xpath(res,
|
||||
'//*[@class="block_area block_area_home"]/div[@class="tab-content"]/div[contains(@class,"block_area-content block_area-list")]/div[@class="film_list-wrap"]/div[@class="flw-item"]/div[@class="film-poster"]/a/@title');
|
||||
final images = xpath(res,
|
||||
'//*[@class="block_area block_area_home"]/div[@class="tab-content"]/div[contains(@class,"block_area-content block_area-list")]/div[@class="film_list-wrap"]/div[@class="flw-item"]/div[@class="film-poster"]/img/@data-src');
|
||||
|
||||
for (var i = 0; i < names.length; i++) {
|
||||
MManga anime = MManga();
|
||||
anime.name = names[i];
|
||||
anime.imageUrl = images[i];
|
||||
anime.link = urls[i];
|
||||
animeList.add(anime);
|
||||
}
|
||||
|
||||
return MPages(animeList, false);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<MPages> search(String query, int page, FilterList filterList) async {
|
||||
query = query.trim().replaceAll(" ", "+");
|
||||
final res = (await client.get(Uri.parse(
|
||||
"$baseUrl/index.php?do=search&subaction=search&story=$query")))
|
||||
.body;
|
||||
|
||||
List<MManga> animeList = [];
|
||||
final urls = xpath(res, '//*[@class="film-poster"]/a/@href');
|
||||
final names = xpath(res, '//*[@class="film-poster"]/a/@title');
|
||||
final images = xpath(res, '//*[@class="film-poster"]/img/@data-src');
|
||||
|
||||
for (var i = 0; i < names.length; i++) {
|
||||
MManga anime = MManga();
|
||||
anime.name = names[i];
|
||||
anime.imageUrl = images[i];
|
||||
anime.link = urls[i];
|
||||
animeList.add(anime);
|
||||
}
|
||||
|
||||
return MPages(animeList.reversed.toList(), false);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<MManga> getDetail(String url) async {
|
||||
final statusList = [
|
||||
{"En cours": 0, "Terminé": 1}
|
||||
];
|
||||
|
||||
final res = (await client.get(Uri.parse(url))).body;
|
||||
var anime = MManga();
|
||||
final doc = parseHtml(res);
|
||||
anime.description =
|
||||
xpath(res, '//*[@class="film-description m-hide"]/text()').first;
|
||||
|
||||
final status = xpath(res,
|
||||
'//*[@class="item item-title" and contains(text(),"Status:")]/span[2]/text()')
|
||||
.first;
|
||||
anime.status = parseStatus(status, statusList);
|
||||
anime.genre = xpath(res,
|
||||
'//*[@class="item item-list" and contains(text(),"Genres:")]/a/text()');
|
||||
anime.author = doc.xpathFirst(
|
||||
'//*[@class="item item-title" and contains(text(),"Studio:")]/span[2]/text()');
|
||||
final episodesLength = int.parse(substringBefore(
|
||||
doc.xpathFirst('//*[@class="film-stats"]/span[7]/text()'), "/")
|
||||
.replaceAll("Ep", ""));
|
||||
List<MChapter>? episodesList = [];
|
||||
|
||||
for (var i = 0; i < episodesLength; i++) {
|
||||
var episode = MChapter();
|
||||
episode.name = "Episode ${i + 1}";
|
||||
episode.url = url.replaceAll('.html', '/episode-${i + 1}.html');
|
||||
episodesList.add(episode);
|
||||
}
|
||||
anime.chapters = episodesList.reversed.toList();
|
||||
return anime;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<MVideo>> getVideoList(String url) async {
|
||||
final resHtml = (await client.get(Uri.parse(url))).body;
|
||||
final id = url.split('/')[4].split('-')[0];
|
||||
final resServer = (await client
|
||||
.get(Uri.parse("$baseUrl/engine/ajax/full-story.php?newsId=$id")))
|
||||
.body;
|
||||
|
||||
final serverIds =
|
||||
xpath(resHtml, '//*[@class="ps__-list"]/div/@data-server-id');
|
||||
final serverNames = xpath(resHtml, '//*[@class="ps__-list"]/div/a/text()');
|
||||
List<String> serverUrls = [];
|
||||
for (var id in serverIds) {
|
||||
final serversUrls = xpath(jsonDecode(resServer)["html"],
|
||||
'//*[@id="content_player_${id}"]/text()')
|
||||
.first;
|
||||
serverUrls.add(serversUrls);
|
||||
}
|
||||
List<MVideo> videos = [];
|
||||
for (var i = 0; i < serverNames.length; i++) {
|
||||
final name = serverNames[i];
|
||||
final url = serverUrls[i];
|
||||
List<MVideo> a = [];
|
||||
if (name.contains("Sendvid")) {
|
||||
a = await sendVidExtractorr(
|
||||
url.replaceAll("https:////", "https://"), "");
|
||||
} else if (name.contains("Sibnet")) {
|
||||
a = await sibnetExtractor(
|
||||
"https://video.sibnet.ru/shell.php?videoid=$url");
|
||||
} else if (name.contains("Mytv")) {
|
||||
a = await myTvExtractor("https://www.myvi.tv/embed/$url");
|
||||
} else if (name.contains("Fmoon")) {
|
||||
a = await filemoonExtractor(url, "", "");
|
||||
}
|
||||
videos.addAll(a);
|
||||
}
|
||||
|
||||
return videos;
|
||||
}
|
||||
|
||||
Future<List<MVideo>> sendVidExtractorr(String url, String prefix) async {
|
||||
final res = (await client.get(Uri.parse(url))).body;
|
||||
final document = parseHtml(res);
|
||||
final masterUrl = document.selectFirst("source#video_source")?.attr("src");
|
||||
if (masterUrl == null) return [];
|
||||
final masterHeaders = {
|
||||
"Accept": "*/*",
|
||||
"Host": Uri.parse(masterUrl).host,
|
||||
"Origin": "https://${Uri.parse(url).host}",
|
||||
"Referer": "https://${Uri.parse(url).host}/",
|
||||
};
|
||||
List<MVideo> videos = [];
|
||||
if (masterUrl.contains(".m3u8")) {
|
||||
final masterPlaylistRes = (await client.get(Uri.parse(masterUrl))).body;
|
||||
|
||||
for (var it in substringAfter(masterPlaylistRes, "#EXT-X-STREAM-INF:")
|
||||
.split("#EXT-X-STREAM-INF:")) {
|
||||
final quality =
|
||||
"${substringBefore(substringBefore(substringAfter(substringAfter(it, "RESOLUTION="), "x"), ","), "\n")}p";
|
||||
|
||||
String videoUrl = substringBefore(substringAfter(it, "\n"), "\n");
|
||||
|
||||
if (!videoUrl.startsWith("http")) {
|
||||
videoUrl =
|
||||
"${masterUrl.split("/").sublist(0, masterUrl.split("/").length - 1).join("/")}/$videoUrl";
|
||||
}
|
||||
final videoHeaders = {
|
||||
"Accept": "*/*",
|
||||
"Host": Uri.parse(videoUrl).host,
|
||||
"Origin": "https://${Uri.parse(url).host}",
|
||||
"Referer": "https://${Uri.parse(url).host}/",
|
||||
};
|
||||
var video = MVideo();
|
||||
video
|
||||
..url = videoUrl
|
||||
..originalUrl = videoUrl
|
||||
..quality = prefix + "Sendvid:$quality"
|
||||
..headers = videoHeaders;
|
||||
videos.add(video);
|
||||
}
|
||||
} else {
|
||||
var video = MVideo();
|
||||
video
|
||||
..url = masterUrl
|
||||
..originalUrl = masterUrl
|
||||
..quality = prefix + "Sendvid:default"
|
||||
..headers = masterHeaders;
|
||||
videos.add(video);
|
||||
}
|
||||
|
||||
return videos;
|
||||
}
|
||||
}
|
||||
|
||||
AnimesUltra main(MSource source) {
|
||||
return AnimesUltra(source: source);
|
||||
}
|
||||
BIN
dart/anime/src/fr/animesultra/icon.png
Normal file
|
After Width: | Height: | Size: 9.6 KiB |
17
dart/anime/src/fr/animesultra/source.dart
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
import '../../../../../model/source.dart';
|
||||
|
||||
Source get animesultraSource => _animesultraSource;
|
||||
const _animesultraVersion = "0.0.75";
|
||||
const _animesultraSourceCodeUrl =
|
||||
"https://raw.githubusercontent.com/kodjodevf/mangayomi-extensions/$branchName/dart/anime/src/fr/animesultra/animesultra.dart";
|
||||
Source _animesultraSource = Source(
|
||||
name: "AnimesUltra",
|
||||
baseUrl: "https://w2.animesultra.net",
|
||||
lang: "fr",
|
||||
typeSource: "single",
|
||||
iconUrl:
|
||||
"https://raw.githubusercontent.com/kodjodevf/mangayomi-extensions/$branchName/dart/anime/src/fr/animesultra/icon.png",
|
||||
sourceCodeUrl: _animesultraSourceCodeUrl,
|
||||
version: _animesultraVersion,
|
||||
itemType: ItemType.anime,
|
||||
isFullData: false);
|
||||
490
dart/anime/src/fr/anizone/anizone.dart
Normal file
|
|
@ -0,0 +1,490 @@
|
|||
import 'package:mangayomi/bridge_lib.dart';
|
||||
import 'dart:convert';
|
||||
|
||||
class AniZone extends MProvider {
|
||||
AniZone({required this.source});
|
||||
|
||||
final MSource source;
|
||||
final Client client = Client(source);
|
||||
|
||||
// Constants for the xpath
|
||||
static const String urlXpath =
|
||||
'//*[contains(@class,"flw-item item-qtip")]/div[@class="film-poster"]/a/@href';
|
||||
static const String nameXpath =
|
||||
'//*[contains(@class,"flw-item item-qtip")]/div[@class="film-detail"]/h3/text()';
|
||||
static const String imageXpath =
|
||||
'//*[contains(@class,"flw-item item-qtip")]/div[@class="film-poster"]/img/@data-src';
|
||||
|
||||
// Methods for fetching the manga list (popular, latest & search)
|
||||
Future<MPages> _getMangaList(String url) async {
|
||||
final doc = (await client.get(Uri.parse(url))).body;
|
||||
List<MManga> animeList = [];
|
||||
|
||||
final urls = xpath(doc, urlXpath);
|
||||
final names = xpath(doc, nameXpath);
|
||||
final images = xpath(doc, imageXpath);
|
||||
|
||||
if (urls.isEmpty || names.isEmpty || images.isEmpty) {
|
||||
return MPages([], false);
|
||||
}
|
||||
|
||||
for (var i = 0; i < names.length; i++) {
|
||||
MManga anime = MManga();
|
||||
anime.name = names[i];
|
||||
anime.imageUrl = images[i];
|
||||
anime.link = urls[i];
|
||||
animeList.add(anime);
|
||||
}
|
||||
|
||||
return MPages(animeList, urls.isNotEmpty);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<MPages> getPopular(int page) async {
|
||||
return _getMangaList("${source.baseUrl}/most-popular/?page=$page");
|
||||
}
|
||||
|
||||
@override
|
||||
Future<MPages> getLatestUpdates(int page) async {
|
||||
return _getMangaList("${source.baseUrl}/recently-added/?page=$page");
|
||||
}
|
||||
|
||||
@override
|
||||
Future<MPages> search(String query, int page, FilterList filterList) async {
|
||||
String baseUrl = "${source.baseUrl}/filter?keyword=$query";
|
||||
|
||||
Map<String, List<String>> filterMap = {
|
||||
"type": [],
|
||||
"status": [],
|
||||
"season": [],
|
||||
"lang": [],
|
||||
"genre": []
|
||||
};
|
||||
|
||||
// Regroupement des filtres avec une logique générique
|
||||
final filterHandlers = {
|
||||
"TypeFilter": "type",
|
||||
"LanguageFilter": "lang",
|
||||
"SaisonFilter": "season",
|
||||
"StatusFilter": "status",
|
||||
"GenreFilter": "genre"
|
||||
};
|
||||
|
||||
for (var filter in filterList.filters) {
|
||||
if (filterHandlers.containsKey(filter.type)) {
|
||||
var key = filterHandlers[filter.type]!;
|
||||
for (var stateItem in filter.state as List) {
|
||||
if (stateItem.state == true) {
|
||||
filterMap[key]?.add(stateItem.value as String);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//add filters to the url dynamically
|
||||
for (var entry in filterMap.entries) {
|
||||
List<String> values = entry.value;
|
||||
if (values.isNotEmpty) {
|
||||
baseUrl += '&${entry.key}=${values.join("%2C")}';
|
||||
}
|
||||
}
|
||||
|
||||
return _getMangaList("$baseUrl&page=$page");
|
||||
}
|
||||
|
||||
Future<MManga> getDetail(String url) async {
|
||||
MManga anime = MManga();
|
||||
try {
|
||||
final doc = (await client.get(Uri.parse(url))).body;
|
||||
final description = xpath(doc, '//p[contains(@class,"short")]/text()');
|
||||
anime.description = description.isNotEmpty ? description.first : "";
|
||||
|
||||
final statusList = xpath(doc,
|
||||
'//div[contains(@class,"col2")]//div[contains(@class,"item")]//div[contains(@class,"item-content")]/text()');
|
||||
if (statusList.isNotEmpty) {
|
||||
if (statusList[0] == "Terminer") {
|
||||
anime.status = MStatus.completed;
|
||||
} else if (statusList[0] == "En cours") {
|
||||
anime.status = MStatus.ongoing;
|
||||
} else {
|
||||
anime.status = MStatus.unknown;
|
||||
}
|
||||
} else {
|
||||
anime.status = MStatus.unknown;
|
||||
}
|
||||
|
||||
anime.genre = xpath(doc,
|
||||
'//div[contains(@class,"item")]//div[contains(@class,"item-content")]//a[contains(@href,"genre")]/text()');
|
||||
|
||||
final regex = RegExp(r'(\d+)$');
|
||||
final match = regex.firstMatch(url);
|
||||
|
||||
if (match == null) {
|
||||
throw Exception('Numéro de l\'épisode non trouvé dans l\'URL.');
|
||||
}
|
||||
|
||||
final res = (await client.get(Uri.parse(
|
||||
"${source.baseUrl}/ajax/episode/list/${match.group(1)}")))
|
||||
.body;
|
||||
|
||||
List<MChapter> episodesList = [];
|
||||
|
||||
final episodeElements =
|
||||
parseHtml(json.decode(res)["html"]).select(".ep-item");
|
||||
|
||||
// Associer chaque titre à son URL et récupérer les vidéos
|
||||
for (var element in episodeElements) {
|
||||
MChapter episode = MChapter();
|
||||
episode.name = element.attr("title");
|
||||
|
||||
String id = substringAfterLast(element.attr("href"), "=");
|
||||
episode.url = "${source.baseUrl}/ajax/episode/servers?episodeId=$id";
|
||||
episodesList.add(episode);
|
||||
}
|
||||
|
||||
anime.chapters = episodesList.reversed.toList();
|
||||
|
||||
return anime;
|
||||
} catch (e) {
|
||||
throw Exception('Erreur lors de la récupération des détails: $e');
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<MVideo>> getVideoList(String url) async {
|
||||
final videoRes = (await client
|
||||
.get(Uri.parse(url), headers: {"Referer": "${source.baseUrl}/"}))
|
||||
.body;
|
||||
|
||||
final lang = xpath(videoRes.replaceAll(r'\', ''),
|
||||
'//div[contains(@class,"item server-item")]/@data-type');
|
||||
final links = xpath(videoRes.replaceAll(r'\', ''),
|
||||
'//div[contains(@class,"item server-item")]/@data-id');
|
||||
final playersNames = xpath(videoRes.replaceAll(r'\', ''),
|
||||
'//div[contains(@class,"item server-item")]/text()');
|
||||
List<Map<String, String>> players = [];
|
||||
for (int j = 0; j < links.length; j++) {
|
||||
// schema of players https://v1.animesz.xyz/ajax/episode/servers?episodeId=(id_episode)
|
||||
// schema or url https://v1.animesz.xyz/ajax/episode/sources?id=(player_id)&epid=(id_episode)
|
||||
if (playersNames.isNotEmpty && playersNames[j] == "sibnet") {
|
||||
final playerUrl =
|
||||
"https://video.sibnet.ru/shell.php?videoid=${links[j]}";
|
||||
players.add({"lang": lang[j], "player": playerUrl});
|
||||
} else if (playersNames.isNotEmpty && playersNames[j] == "sendvid") {
|
||||
final playerUrl = "https://sendvid.com/embed/${links[j]}";
|
||||
players.add({"lang": lang[j], "player": playerUrl});
|
||||
} else if (playersNames.isNotEmpty && playersNames[j] == "VidCDN") {
|
||||
final playerUrl =
|
||||
"https://r.vidcdn.xyz/v1/api/get_sources/${links[j].replaceFirst(RegExp(r'vidcdn$'), '')}";
|
||||
players.add({"lang": lang[j], "player": playerUrl});
|
||||
} else if (playersNames.isNotEmpty && playersNames[j] == "Voe") {
|
||||
final playerUrl = "https://voe.sx/e/${links[j]}";
|
||||
players.add({"lang": lang[j], "player": playerUrl});
|
||||
} else if (playersNames.isNotEmpty && playersNames[j] == "Fmoon") {
|
||||
final playerUrl =
|
||||
"https://filemoon.sx/e/${links[j]}&data-realid=${links[j]}&epid=${substringAfter(url, "episodeId=")}";
|
||||
players.add({"lang": lang[j], "player": playerUrl});
|
||||
}
|
||||
}
|
||||
|
||||
List<MVideo> videos = [];
|
||||
for (var player in players) {
|
||||
String lang = (player["lang"] as String).toUpperCase();
|
||||
String playerUrl = player["player"];
|
||||
List<MVideo> a = [];
|
||||
if (playerUrl.contains("sendvid")) {
|
||||
a = await sendVidExtractorr(playerUrl, "$lang ");
|
||||
} else if (playerUrl.contains("sibnet.ru")) {
|
||||
a = await sibnetExtractor(playerUrl, lang);
|
||||
} else if (playerUrl.contains("voe.sx")) {
|
||||
a = await voeExtractor(playerUrl, "$lang ");
|
||||
} else if (playerUrl.contains("vidcdn")) {
|
||||
a = await vidcdnExtractor(playerUrl, lang);
|
||||
} else if (playerUrl.contains("filemoon")) {
|
||||
a = await filemoonExtractor(playerUrl, "$lang Filemoon - ", "");
|
||||
} else if (playerUrl.contains("vidhide")) {
|
||||
a = await streamHideExtractor(playerUrl, lang);
|
||||
}
|
||||
videos.addAll(a);
|
||||
}
|
||||
|
||||
return sortVideos(videos, source.id);
|
||||
}
|
||||
|
||||
Future<List<MVideo>> streamHideExtractor(String url, String prefix) async {
|
||||
final res = (await client.get(Uri.parse(url))).body;
|
||||
final masterUrl = substringBefore(
|
||||
substringAfter(
|
||||
substringAfter(
|
||||
substringAfter(unpackJs(res), "sources:"), "file:\""),
|
||||
"src:\""),
|
||||
'"');
|
||||
final masterPlaylistRes = (await client.get(Uri.parse(masterUrl))).body;
|
||||
List<MVideo> videos = [];
|
||||
for (var it in substringAfter(masterPlaylistRes, "#EXT-X-STREAM-INF:")
|
||||
.split("#EXT-X-STREAM-INF:")) {
|
||||
final quality =
|
||||
"${substringBefore(substringBefore(substringAfter(substringAfter(it, "RESOLUTION="), "x"), ","), "\n")}p";
|
||||
|
||||
String videoUrl = substringBefore(substringAfter(it, "\n"), "\n");
|
||||
|
||||
if (!videoUrl.startsWith("http")) {
|
||||
videoUrl =
|
||||
"${masterUrl.split("/").sublist(0, masterUrl.split("/").length - 1).join("/")}/$videoUrl";
|
||||
}
|
||||
|
||||
MVideo video = MVideo();
|
||||
video
|
||||
..url = videoUrl
|
||||
..originalUrl = videoUrl
|
||||
..quality = "$prefix StreamHideVid - $quality";
|
||||
videos.add(video);
|
||||
}
|
||||
return videos;
|
||||
}
|
||||
|
||||
@override
|
||||
List<dynamic> getFilterList() {
|
||||
return [
|
||||
GroupFilter("TypeFilter", "Type", [
|
||||
CheckBoxFilter("Film", "1"),
|
||||
CheckBoxFilter("Anime", "2"),
|
||||
CheckBoxFilter("OVA", "3"),
|
||||
CheckBoxFilter("ONA", "4"),
|
||||
CheckBoxFilter("Special", "5"),
|
||||
CheckBoxFilter("Music", "6"),
|
||||
]),
|
||||
GroupFilter("LanguageFilter", "Langue", [
|
||||
CheckBoxFilter("VF", "3"),
|
||||
CheckBoxFilter("VOSTFR", "4"),
|
||||
CheckBoxFilter("Multicc", "2"),
|
||||
CheckBoxFilter("EN", "1"),
|
||||
]),
|
||||
GroupFilter("SaisonFilter", "Saison", [
|
||||
CheckBoxFilter("Printemps", "1"),
|
||||
CheckBoxFilter("Été", "2"),
|
||||
CheckBoxFilter("Automne", "3"),
|
||||
CheckBoxFilter("Hiver", "4"),
|
||||
]),
|
||||
GroupFilter("StatusFilter", "Statut", [
|
||||
CheckBoxFilter("Terminés", "1"),
|
||||
CheckBoxFilter("En cours", "2"),
|
||||
CheckBoxFilter("Pas encore diffusés", "3"),
|
||||
]),
|
||||
GroupFilter("GenreFilter", "Genre", [
|
||||
CheckBoxFilter("Action", "1"),
|
||||
CheckBoxFilter("Aventure", "2"),
|
||||
CheckBoxFilter("Voitures", "3"),
|
||||
CheckBoxFilter("Comédie", "4"),
|
||||
CheckBoxFilter("Démence", "5"),
|
||||
CheckBoxFilter("Démons", "6"),
|
||||
CheckBoxFilter("Drame", "8"),
|
||||
CheckBoxFilter("Ecchi", "9"),
|
||||
CheckBoxFilter("Fantastique", "10"),
|
||||
CheckBoxFilter("Jeu", "11"),
|
||||
CheckBoxFilter("Harem", "35"),
|
||||
CheckBoxFilter("Historique", "13"),
|
||||
CheckBoxFilter("Horreur", "14"),
|
||||
CheckBoxFilter("Isekai", "44"),
|
||||
CheckBoxFilter("Josei", "43"),
|
||||
CheckBoxFilter("Enfants", "25"),
|
||||
CheckBoxFilter("Magie", "16"),
|
||||
CheckBoxFilter("Arts martiaux", "17"),
|
||||
CheckBoxFilter("Mecha", "18"),
|
||||
CheckBoxFilter("Militaire", "38"),
|
||||
CheckBoxFilter("Musique", "19"),
|
||||
CheckBoxFilter("Mystère", "7"),
|
||||
CheckBoxFilter("Parodie", "20"),
|
||||
CheckBoxFilter("Police", "39"),
|
||||
CheckBoxFilter("Psychologique", "40"),
|
||||
CheckBoxFilter("Romance", "22"),
|
||||
CheckBoxFilter("Samouraï", "21"),
|
||||
CheckBoxFilter("École", "23"),
|
||||
CheckBoxFilter("Science-Fiction", "24"),
|
||||
CheckBoxFilter("Seinen", "42"),
|
||||
CheckBoxFilter("Shoujo Ai", "26"),
|
||||
CheckBoxFilter("Shoujo", "25"),
|
||||
CheckBoxFilter("Shounen Ai", "28"),
|
||||
CheckBoxFilter("Tranche de vie", "36"),
|
||||
CheckBoxFilter("Shounen", "27"),
|
||||
CheckBoxFilter("Espace", "29"),
|
||||
CheckBoxFilter("Sports", "30"),
|
||||
CheckBoxFilter("Super Pouvoir", "31"),
|
||||
CheckBoxFilter("Surnaturel", "37"),
|
||||
CheckBoxFilter("Vampire", "32"),
|
||||
CheckBoxFilter("Yaoi", "33"),
|
||||
CheckBoxFilter("Yuri", "34"),
|
||||
])
|
||||
];
|
||||
}
|
||||
|
||||
@override
|
||||
List<dynamic> getSourcePreferences() {
|
||||
return [
|
||||
ListPreference(
|
||||
key: "preferred_quality",
|
||||
title: "Qualité préférée",
|
||||
summary: "",
|
||||
valueIndex: 0,
|
||||
entries: ["1080p", "720p", "480p", "360p"],
|
||||
entryValues: ["1080", "720", "480", "360"]),
|
||||
ListPreference(
|
||||
key: "voices_preference",
|
||||
title: "Préférence des voix",
|
||||
summary: "",
|
||||
valueIndex: 0,
|
||||
entries: ["Préférer VOSTFR", "Préférer VF"],
|
||||
entryValues: ["vostfr", "vf"]),
|
||||
];
|
||||
}
|
||||
|
||||
List<MVideo> sortVideos(List<MVideo> videos, int sourceId) {
|
||||
String quality = getPreferenceValue(sourceId, "preferred_quality");
|
||||
String voice = getPreferenceValue(sourceId, "voices_preference");
|
||||
|
||||
videos.sort((MVideo a, MVideo b) {
|
||||
int qualityMatchA = 0;
|
||||
if (a.quality.contains(quality) &&
|
||||
a.quality.toLowerCase().contains(voice)) {
|
||||
qualityMatchA = 1;
|
||||
}
|
||||
int qualityMatchB = 0;
|
||||
if (b.quality.contains(quality) &&
|
||||
b.quality.toLowerCase().contains(voice)) {
|
||||
qualityMatchB = 1;
|
||||
}
|
||||
if (qualityMatchA != qualityMatchB) {
|
||||
return qualityMatchB - qualityMatchA;
|
||||
}
|
||||
|
||||
final regex = RegExp(r'(\d+)p');
|
||||
final matchA = regex.firstMatch(a.quality);
|
||||
final matchB = regex.firstMatch(b.quality);
|
||||
final int qualityNumA = int.tryParse(matchA?.group(1) ?? '0') ?? 0;
|
||||
final int qualityNumB = int.tryParse(matchB?.group(1) ?? '0') ?? 0;
|
||||
return qualityNumB - qualityNumA;
|
||||
});
|
||||
return videos;
|
||||
}
|
||||
|
||||
Future<List<MVideo>> sendVidExtractorr(String url, String prefix) async {
|
||||
final res = (await client.get(Uri.parse(url))).body;
|
||||
final document = parseHtml(res);
|
||||
final masterUrl = document.selectFirst("source#video_source")?.attr("src");
|
||||
print(masterUrl);
|
||||
if (masterUrl == null) return [];
|
||||
final masterHeaders = {
|
||||
"Accept": "*/*",
|
||||
"Host": Uri.parse(masterUrl).host,
|
||||
"Origin": "https://${Uri.parse(url).host}",
|
||||
"Referer": "https://${Uri.parse(url).host}/",
|
||||
};
|
||||
List<MVideo> videos = [];
|
||||
if (masterUrl.contains(".m3u8")) {
|
||||
final masterPlaylistRes = (await client.get(Uri.parse(masterUrl))).body;
|
||||
|
||||
for (var it in substringAfter(masterPlaylistRes, "#EXT-X-STREAM-INF:")
|
||||
.split("#EXT-X-STREAM-INF:")) {
|
||||
final quality =
|
||||
"${substringBefore(substringBefore(substringAfter(substringAfter(it, "RESOLUTION="), "x"), ","), "\n")}p";
|
||||
|
||||
String videoUrl = substringBefore(substringAfter(it, "\n"), "\n");
|
||||
|
||||
if (!videoUrl.startsWith("http")) {
|
||||
videoUrl =
|
||||
"${masterUrl.split("/").sublist(0, masterUrl.split("/").length - 1).join("/")}/$videoUrl";
|
||||
}
|
||||
final videoHeaders = {
|
||||
"Accept": "*/*",
|
||||
"Host": Uri.parse(videoUrl).host,
|
||||
"Origin": "https://${Uri.parse(url).host}",
|
||||
"Referer": "https://${Uri.parse(url).host}/",
|
||||
};
|
||||
var video = MVideo();
|
||||
video
|
||||
..url = videoUrl
|
||||
..originalUrl = videoUrl
|
||||
..quality = prefix + "Sendvid:$quality"
|
||||
..headers = videoHeaders;
|
||||
videos.add(video);
|
||||
}
|
||||
} else {
|
||||
var video = MVideo();
|
||||
video
|
||||
..url = masterUrl
|
||||
..originalUrl = masterUrl
|
||||
..quality = prefix + "Sendvid:default"
|
||||
..headers = masterHeaders;
|
||||
videos.add(video);
|
||||
}
|
||||
|
||||
return videos;
|
||||
}
|
||||
|
||||
Future<List<MVideo>> vidcdnExtractor(String url, String prefix) async {
|
||||
final res = await client.get(Uri.parse(url));
|
||||
if (res.statusCode != 200) {
|
||||
print("Erreur lors de la récupération de la page : ${res.statusCode}");
|
||||
return [];
|
||||
}
|
||||
final jsonResponse = jsonDecode(res.body);
|
||||
|
||||
String masterUrl = jsonResponse['sources'][0]['file'] ?? '';
|
||||
final quality = jsonResponse['quality'] ?? '';
|
||||
|
||||
List<MVideo> videos = [];
|
||||
|
||||
final masterPlaylistRes = await client.get(Uri.parse(masterUrl));
|
||||
if (masterPlaylistRes.statusCode != 200) {
|
||||
print(
|
||||
"Error lors de la récupération de la playlist M3U8 : ${masterPlaylistRes.statusCode}");
|
||||
return [];
|
||||
}
|
||||
|
||||
final masterPlaylistBody = masterPlaylistRes.body;
|
||||
|
||||
final playlistLines = masterPlaylistBody.split("\n");
|
||||
|
||||
for (int i = 0; i < playlistLines.length; i++) {
|
||||
final line = playlistLines[i];
|
||||
if (line.startsWith("#EXT-X-STREAM-INF")) {
|
||||
final resolutionLine = line.split("RESOLUTION=").last;
|
||||
final resolution = resolutionLine.split(",").first;
|
||||
final width = int.parse(resolution.split("x").first);
|
||||
final height = int.parse(resolution.split("x").last);
|
||||
|
||||
String videoQuality;
|
||||
if (height >= 1080) {
|
||||
videoQuality = "1080p";
|
||||
} else if (height >= 720) {
|
||||
videoQuality = "720p";
|
||||
} else if (height >= 480) {
|
||||
videoQuality = "480p";
|
||||
} else if (height >= 360) {
|
||||
videoQuality = "360p";
|
||||
} else {
|
||||
videoQuality = "${height}p";
|
||||
}
|
||||
|
||||
String videoUrl = playlistLines[i + 1].trim();
|
||||
|
||||
if (!videoUrl.startsWith("http")) {
|
||||
videoUrl =
|
||||
"${masterUrl.substring(0, masterUrl.lastIndexOf('/'))}/$videoUrl";
|
||||
}
|
||||
|
||||
var video = MVideo();
|
||||
video
|
||||
..url = masterUrl
|
||||
..originalUrl = masterUrl
|
||||
..quality = "$prefix VidCDN:$videoQuality";
|
||||
videos.add(video);
|
||||
}
|
||||
}
|
||||
return videos;
|
||||
}
|
||||
}
|
||||
|
||||
AniZone main(MSource source) {
|
||||
return AniZone(source: source);
|
||||
}
|
||||
BIN
dart/anime/src/fr/anizone/icon.png
Normal file
|
After Width: | Height: | Size: 26 KiB |
16
dart/anime/src/fr/anizone/source.dart
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
import '../../../../../model/source.dart';
|
||||
|
||||
Source get aniZoneSource => _aniZoneSource;
|
||||
const _aniZoneVersion = "0.0.2";
|
||||
const _aniZoneSourceCodeUrl =
|
||||
"https://raw.githubusercontent.com/kodjodevf/mangayomi-extensions/$branchName/dart/anime/src/fr/anizone/anizone.dart";
|
||||
Source _aniZoneSource = Source(
|
||||
name: "AniZone",
|
||||
baseUrl: "https://v1.animesz.xyz",
|
||||
lang: "fr",
|
||||
typeSource: "single",
|
||||
iconUrl:
|
||||
"https://raw.githubusercontent.com/kodjodevf/mangayomi-extensions/$branchName/dart/anime/src/fr/anizone/icon.png",
|
||||
sourceCodeUrl: _aniZoneSourceCodeUrl,
|
||||
version: _aniZoneVersion,
|
||||
itemType: ItemType.anime);
|
||||
395
dart/anime/src/fr/franime/franime.dart
Normal file
|
|
@ -0,0 +1,395 @@
|
|||
import 'package:mangayomi/bridge_lib.dart';
|
||||
import 'dart:convert';
|
||||
|
||||
class FrAnime extends MProvider {
|
||||
FrAnime({required this.source});
|
||||
|
||||
MSource source;
|
||||
|
||||
final Client client = Client(source);
|
||||
|
||||
@override
|
||||
Future<MPages> getPopular(int page) async {
|
||||
final res = await dataBase();
|
||||
|
||||
return animeResList(res);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<MPages> getLatestUpdates(int page) async {
|
||||
final res = await dataBase();
|
||||
|
||||
List list = json.decode(res);
|
||||
return animeResList(json.encode(list.reversed.toList()));
|
||||
}
|
||||
|
||||
@override
|
||||
Future<MPages> search(String query, int page, FilterList filterList) async {
|
||||
final res = await dataBase();
|
||||
|
||||
return animeSeachFetch(res, query);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<MManga> getDetail(String url) async {
|
||||
MManga anime = MManga();
|
||||
String language = "vo".toString();
|
||||
if (url.contains("lang=")) {
|
||||
language = substringBefore(substringAfter(url, "lang="), "&");
|
||||
}
|
||||
String stem = substringBefore(substringAfterLast(url, "/"), "?");
|
||||
final res = await dataBase();
|
||||
|
||||
final animeByTitleOJson = databaseAnimeByTitleO(res, stem);
|
||||
final seasons = json.decode(animeByTitleOJson)["saisons"];
|
||||
|
||||
var seasonsJson = seasons.first;
|
||||
|
||||
if (url.contains("s=")) {
|
||||
int seasonNumber =
|
||||
int.parse(substringBefore(substringAfter(url, "s="), "&"));
|
||||
seasonsJson = seasons[seasonNumber - 1];
|
||||
}
|
||||
|
||||
List<MChapter>? episodesList = [];
|
||||
|
||||
final episodes = seasonsJson["episodes"];
|
||||
|
||||
for (int i = 0; i < episodes.length; i++) {
|
||||
final ep = episodes[i];
|
||||
|
||||
final lang = ep["lang"];
|
||||
|
||||
final vo = lang["vo"];
|
||||
final vf = lang["vf"];
|
||||
bool hasVostfr = vo["lecteurs"].isNotEmpty;
|
||||
bool hasVf = vf["lecteurs"].isNotEmpty;
|
||||
bool playerIsNotEmpty = false;
|
||||
|
||||
if (language == "vo" && hasVostfr) {
|
||||
playerIsNotEmpty = true;
|
||||
} else if (language == "vf" && hasVf) {
|
||||
playerIsNotEmpty = true;
|
||||
}
|
||||
if (playerIsNotEmpty) {
|
||||
MChapter episode = MChapter();
|
||||
episode.url = "$url&ep=${i + 1}";
|
||||
String title = ep["title"];
|
||||
episode.name = title.replaceAll('"', "");
|
||||
episodesList.add(episode);
|
||||
}
|
||||
}
|
||||
|
||||
anime.chapters = episodesList.reversed.toList();
|
||||
return anime;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<MVideo>> getVideoList(String url) async {
|
||||
String language = "vo";
|
||||
String videoBaseUrl = "https://api.franime.fr/api/anime";
|
||||
if (url.contains("lang=")) {
|
||||
language = substringBefore(substringAfter(url, "lang="), "&");
|
||||
}
|
||||
String stem = substringBefore(substringAfterLast(url, "/"), "?");
|
||||
final res = await dataBase();
|
||||
|
||||
final animeByTitleOJson = databaseAnimeByTitleO(res, stem);
|
||||
final animeId = json.decode(animeByTitleOJson)["id"];
|
||||
final seasons = json.decode(animeByTitleOJson)["saisons"];
|
||||
|
||||
var seasonsJson = seasons.first;
|
||||
|
||||
videoBaseUrl += "/$animeId/";
|
||||
|
||||
if (url.contains("s=")) {
|
||||
int seasonNumber =
|
||||
int.parse(substringBefore(substringAfter(url, "s="), "&"));
|
||||
videoBaseUrl += "${seasonNumber - 1}/";
|
||||
seasonsJson = seasons[seasonNumber - 1];
|
||||
} else {
|
||||
videoBaseUrl += "0/";
|
||||
}
|
||||
final episodesJson = seasonsJson["episodes"];
|
||||
var episode = episodesJson.first;
|
||||
if (url.contains("ep=")) {
|
||||
int episodeNumber = int.parse(substringAfter(url, "ep="));
|
||||
episode = episodesJson[episodeNumber - 1];
|
||||
videoBaseUrl += "${episodeNumber - 1}";
|
||||
} else {
|
||||
videoBaseUrl += "0";
|
||||
}
|
||||
final lang = episode["lang"];
|
||||
|
||||
final vo = lang["vo"];
|
||||
final vf = lang["vf"];
|
||||
bool hasVostfr = vo["lecteurs"].isNotEmpty;
|
||||
bool hasVf = vf["lecteurs"].isNotEmpty;
|
||||
List<String> vostfrPlayers = vo["lecteurs"];
|
||||
List<String> vfPlayers = vf["lecteurs"];
|
||||
List<String> players = [];
|
||||
if (language == "vo" && hasVostfr) {
|
||||
players = vostfrPlayers;
|
||||
} else if (language == "vf" && hasVf) {
|
||||
players = vfPlayers;
|
||||
}
|
||||
|
||||
List<MVideo> videos = [];
|
||||
for (var i = 0; i < players.length; i++) {
|
||||
String apiUrl = "$videoBaseUrl/$language/$i";
|
||||
String playerName = players[i];
|
||||
|
||||
MVideo video = MVideo();
|
||||
|
||||
final playerUrl = (await client.get(Uri.parse(apiUrl),
|
||||
headers: {"Referer": "https://franime.fr/"}))
|
||||
.body;
|
||||
|
||||
List<MVideo> a = [];
|
||||
print(playerName);
|
||||
if (playerName.contains("vido")) {
|
||||
videos.add(video
|
||||
..url = playerUrl
|
||||
..originalUrl = playerUrl
|
||||
..quality = "FRAnime (Vido)");
|
||||
} else if (playerName.contains("sendvid")) {
|
||||
a = await sendVidExtractorr(playerUrl, "");
|
||||
} else if (playerName.contains("sibnet")) {
|
||||
a = await sibnetExtractor(playerUrl);
|
||||
}
|
||||
videos.addAll(a);
|
||||
}
|
||||
|
||||
return videos;
|
||||
}
|
||||
|
||||
MPages animeResList(String res) {
|
||||
final statusList = [
|
||||
{"EN COURS": 0, "TERMINÉ": 1}
|
||||
];
|
||||
List<MManga> animeList = [];
|
||||
|
||||
var jsonResList = json.decode(res);
|
||||
|
||||
for (var animeJson in jsonResList) {
|
||||
final seasons = animeJson["saisons"];
|
||||
List<bool> vostfrListName = [];
|
||||
List<bool> vfListName = [];
|
||||
for (var season in seasons) {
|
||||
for (var episode in season["episodes"]) {
|
||||
final lang = episode["lang"];
|
||||
final vo = lang["vo"];
|
||||
final vf = lang["vf"];
|
||||
vostfrListName.add(vo["lecteurs"].isNotEmpty);
|
||||
vfListName.add(vf["lecteurs"].isNotEmpty);
|
||||
}
|
||||
}
|
||||
|
||||
String titleO = animeJson["titleO"];
|
||||
final title = animeJson["title"];
|
||||
final genre = animeJson["themes"];
|
||||
final description = animeJson["description"];
|
||||
final status = parseStatus(animeJson["status"], statusList);
|
||||
final imageUrl = animeJson["affiche"];
|
||||
bool hasVostfr = vostfrListName.contains(true);
|
||||
bool hasVf = vfListName.contains(true);
|
||||
if (hasVostfr || hasVf) {
|
||||
for (int i = 0; i < seasons.length; i++) {
|
||||
MManga anime = MManga();
|
||||
int ind = i + 1;
|
||||
anime.genre = genre;
|
||||
anime.description = description;
|
||||
String seasonTitle = "".toString();
|
||||
String lang = "";
|
||||
if (title.isEmpty) {
|
||||
seasonTitle = titleO;
|
||||
} else {
|
||||
seasonTitle = title;
|
||||
}
|
||||
if (seasons.length > 1) {
|
||||
seasonTitle += " S$ind";
|
||||
}
|
||||
if (hasVf) {
|
||||
seasonTitle += " VF";
|
||||
lang = "vf".toString();
|
||||
}
|
||||
if (hasVostfr) {
|
||||
seasonTitle += " VOSTFR";
|
||||
lang = "vo".toString();
|
||||
}
|
||||
|
||||
anime.status = status;
|
||||
anime.name = seasonTitle;
|
||||
anime.imageUrl = imageUrl;
|
||||
anime.link =
|
||||
"/anime/${titleO.replaceAll(RegExp("[^A-Za-z0-9 ]"), "").replaceAll(" ", "-").toLowerCase()}?lang=$lang&s=$ind";
|
||||
|
||||
animeList.add(anime);
|
||||
}
|
||||
}
|
||||
}
|
||||
return MPages(animeList, true);
|
||||
}
|
||||
|
||||
MPages animeSeachFetch(String res, String query) {
|
||||
final statusList = [
|
||||
{"EN COURS": 0, "TERMINÉ": 1}
|
||||
];
|
||||
List<MManga> animeList = [];
|
||||
final jsonResList = json.decode(res);
|
||||
for (var animeJson in jsonResList) {
|
||||
MManga anime = MManga();
|
||||
|
||||
final titleO = getMapValue(json.encode(animeJson), "titleO");
|
||||
final titleAlt =
|
||||
getMapValue(json.encode(animeJson), "titles", encode: true);
|
||||
final containsEn = getMapValue(titleAlt, "en")
|
||||
.toString()
|
||||
.toLowerCase()
|
||||
.contains(query.toLowerCase());
|
||||
final containsEnJp = getMapValue(titleAlt, "en_jp")
|
||||
.toString()
|
||||
.toLowerCase()
|
||||
.contains(query.toLowerCase());
|
||||
final containsJaJp = getMapValue(titleAlt, "ja_jp")
|
||||
.toString()
|
||||
.toLowerCase()
|
||||
.contains(query.toLowerCase());
|
||||
final containsTitleO = titleO.toLowerCase().contains(query.toLowerCase());
|
||||
|
||||
if (containsEn || containsEnJp || containsJaJp || containsTitleO) {
|
||||
final seasons = animeJson["saisons"];
|
||||
List<bool> vostfrListName = [];
|
||||
List<bool> vfListName = [];
|
||||
for (var season in seasons) {
|
||||
for (var episode in season["episodes"]) {
|
||||
final lang = episode["lang"];
|
||||
final vo = lang["vo"];
|
||||
final vf = lang["vf"];
|
||||
vostfrListName.add(vo["lecteurs"].isNotEmpty);
|
||||
vfListName.add(vf["lecteurs"].isNotEmpty);
|
||||
}
|
||||
}
|
||||
String titleO = animeJson["titleO"];
|
||||
final title = animeJson["title"];
|
||||
final genre = animeJson["themes"];
|
||||
final description = animeJson["description"];
|
||||
final status = parseStatus(animeJson["status"], statusList);
|
||||
final imageUrl = animeJson["affiche"];
|
||||
|
||||
bool hasVostfr = vostfrListName.contains(true);
|
||||
bool hasVf = vfListName.contains(true);
|
||||
if (hasVostfr || hasVf) {
|
||||
for (int i = 0; i < seasons.length; i++) {
|
||||
MManga anime = MManga();
|
||||
int ind = i + 1;
|
||||
anime.genre = genre;
|
||||
anime.description = description;
|
||||
String seasonTitle = "".toString();
|
||||
String lang = "";
|
||||
if (title.isEmpty) {
|
||||
seasonTitle = titleO;
|
||||
} else {
|
||||
seasonTitle = title;
|
||||
}
|
||||
if (seasons.length > 1) {
|
||||
seasonTitle += " S$ind";
|
||||
}
|
||||
if (hasVf) {
|
||||
seasonTitle += " VF";
|
||||
lang = "vf".toString();
|
||||
}
|
||||
if (hasVostfr) {
|
||||
seasonTitle += " VOSTFR";
|
||||
lang = "vo".toString();
|
||||
}
|
||||
|
||||
anime.status = status;
|
||||
anime.name = seasonTitle;
|
||||
anime.imageUrl = imageUrl;
|
||||
anime.link =
|
||||
"/anime/${titleO.replaceAll(RegExp("[^A-Za-z0-9 ]"), "").replaceAll(" ", "-").toLowerCase()}?lang=$lang&s=$ind";
|
||||
|
||||
animeList.add(anime);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return MPages(animeList, true);
|
||||
}
|
||||
|
||||
Future<String> dataBase() async {
|
||||
return (await client.get(Uri.parse("https://api.franime.fr/api/animes/"),
|
||||
headers: {"Referer": "https://franime.fr/"}))
|
||||
.body;
|
||||
}
|
||||
|
||||
String databaseAnimeByTitleO(String res, String titleO) {
|
||||
final datas = json.decode(res) as List<Map<String, dynamic>>;
|
||||
for (var data in datas) {
|
||||
String title =
|
||||
(data["titleO"] as String).replaceAll(RegExp("[^A-Za-z0-9 ]"), "");
|
||||
if (title.replaceAll(" ", "-").toLowerCase() == "${titleO}") {
|
||||
return json.encode(data);
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
Future<List<MVideo>> sendVidExtractorr(String url, String prefix) async {
|
||||
final res = (await client.get(Uri.parse(url))).body;
|
||||
final document = parseHtml(res);
|
||||
final masterUrl = document.selectFirst("source#video_source")?.attr("src");
|
||||
if (masterUrl == null) return [];
|
||||
final masterHeaders = {
|
||||
"Accept": "*/*",
|
||||
"Host": Uri.parse(masterUrl).host,
|
||||
"Origin": "https://${Uri.parse(url).host}",
|
||||
"Referer": "https://${Uri.parse(url).host}/",
|
||||
};
|
||||
List<MVideo> videos = [];
|
||||
if (masterUrl.contains(".m3u8")) {
|
||||
final masterPlaylistRes = (await client.get(Uri.parse(masterUrl))).body;
|
||||
|
||||
for (var it in substringAfter(masterPlaylistRes, "#EXT-X-STREAM-INF:")
|
||||
.split("#EXT-X-STREAM-INF:")) {
|
||||
final quality =
|
||||
"${substringBefore(substringBefore(substringAfter(substringAfter(it, "RESOLUTION="), "x"), ","), "\n")}p";
|
||||
|
||||
String videoUrl = substringBefore(substringAfter(it, "\n"), "\n");
|
||||
|
||||
if (!videoUrl.startsWith("http")) {
|
||||
videoUrl =
|
||||
"${masterUrl.split("/").sublist(0, masterUrl.split("/").length - 1).join("/")}/$videoUrl";
|
||||
}
|
||||
final videoHeaders = {
|
||||
"Accept": "*/*",
|
||||
"Host": Uri.parse(videoUrl).host,
|
||||
"Origin": "https://${Uri.parse(url).host}",
|
||||
"Referer": "https://${Uri.parse(url).host}/",
|
||||
};
|
||||
var video = MVideo();
|
||||
video
|
||||
..url = videoUrl
|
||||
..originalUrl = videoUrl
|
||||
..quality = prefix + "Sendvid:$quality"
|
||||
..headers = videoHeaders;
|
||||
videos.add(video);
|
||||
}
|
||||
} else {
|
||||
var video = MVideo();
|
||||
video
|
||||
..url = masterUrl
|
||||
..originalUrl = masterUrl
|
||||
..quality = prefix + "Sendvid:default"
|
||||
..headers = masterHeaders;
|
||||
videos.add(video);
|
||||
}
|
||||
|
||||
return videos;
|
||||
}
|
||||
}
|
||||
|
||||
FrAnime main(MSource source) {
|
||||
return FrAnime(source: source);
|
||||
}
|
||||
BIN
dart/anime/src/fr/franime/icon.png
Normal file
|
After Width: | Height: | Size: 3.3 KiB |
18
dart/anime/src/fr/franime/source.dart
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
import '../../../../../model/source.dart';
|
||||
|
||||
Source get franimeSource => _franimeSource;
|
||||
const _franimeVersion = "0.0.75";
|
||||
const _franimeSourceCodeUrl =
|
||||
"https://raw.githubusercontent.com/kodjodevf/mangayomi-extensions/$branchName/dart/anime/src/fr/franime/franime.dart";
|
||||
Source _franimeSource = Source(
|
||||
name: "FrAnime",
|
||||
baseUrl: "https://franime.fr",
|
||||
apiUrl: "https://api.franime.fr",
|
||||
lang: "fr",
|
||||
typeSource: "single",
|
||||
iconUrl:
|
||||
"https://raw.githubusercontent.com/kodjodevf/mangayomi-extensions/$branchName/dart/anime/src/fr/franime/icon.png",
|
||||
sourceCodeUrl: _franimeSourceCodeUrl,
|
||||
version: _franimeVersion,
|
||||
itemType: ItemType.anime,
|
||||
isFullData: true);
|
||||
BIN
dart/anime/src/fr/otakufr/icon.png
Normal file
|
After Width: | Height: | Size: 15 KiB |
465
dart/anime/src/fr/otakufr/otakufr.dart
Normal file
|
|
@ -0,0 +1,465 @@
|
|||
import 'package:mangayomi/bridge_lib.dart';
|
||||
import 'dart:convert';
|
||||
|
||||
class OtakuFr extends MProvider {
|
||||
OtakuFr({required this.source});
|
||||
|
||||
MSource source;
|
||||
|
||||
final Client client = Client(source);
|
||||
|
||||
@override
|
||||
String get baseUrl => getPreferenceValue(source.id, "overrideBaseUrl");
|
||||
|
||||
@override
|
||||
Future<MPages> getPopular(int page) async {
|
||||
final res =
|
||||
(await client.get(Uri.parse("$baseUrl/en-cours/page/$page"))).body;
|
||||
List<MManga> animeList = [];
|
||||
final urls =
|
||||
xpath(res, '//*[@class="list"]/article/div/div/figure/a/@href');
|
||||
final names =
|
||||
xpath(res, '//*[@class="list"]/article/div/div/figure/a/img/@title');
|
||||
final images =
|
||||
xpath(res, '//*[@class="list"]/article/div/div/figure/a/img/@src');
|
||||
|
||||
for (var i = 0; i < names.length; i++) {
|
||||
MManga anime = MManga();
|
||||
anime.name = names[i];
|
||||
anime.imageUrl = images[i];
|
||||
anime.link = urls[i];
|
||||
animeList.add(anime);
|
||||
}
|
||||
final nextPage = xpath(res, '//a[@class="next page-link"]/@href');
|
||||
return MPages(animeList, nextPage.isNotEmpty);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<MPages> getLatestUpdates(int page) async {
|
||||
final res = (await client.get(Uri.parse("$baseUrl/page/$page/"))).body;
|
||||
|
||||
List<MManga> animeList = [];
|
||||
final urls = xpath(res, '//*[@class="episode"]/div/a/@href');
|
||||
final namess = xpath(res, '//*[@class="episode"]/div/a/text()');
|
||||
List<String> names = [];
|
||||
for (var name in namess) {
|
||||
names.add(regExp(
|
||||
name,
|
||||
r'(?<=\bS\d\s*|)\d{2}\s*(?=\b(Vostfr|vostfr|VF|Vf|vf|\(VF\)|\(vf\)|\(Vf\)|\(Vostfr\)\b))?',
|
||||
'',
|
||||
0,
|
||||
0)
|
||||
.replaceAll(' vostfr', '')
|
||||
.replaceAll(' Vostfr', '')
|
||||
.replaceAll(' VF', '')
|
||||
.replaceAll(' Vf', '')
|
||||
.replaceAll(' vf', '')
|
||||
.replaceAll(' (VF)', '')
|
||||
.replaceAll(' (vf)', '')
|
||||
.replaceAll(' (vf)', '')
|
||||
.replaceAll(' (Vf)', '')
|
||||
.replaceAll(' (Vostfr)', ''));
|
||||
}
|
||||
final images = xpath(res, '//*[@class="episode"]/div/figure/a/img/@src');
|
||||
|
||||
for (var i = 0; i < names.length; i++) {
|
||||
MManga anime = MManga();
|
||||
anime.name = names[i];
|
||||
anime.imageUrl = images[i];
|
||||
anime.link = urls[i];
|
||||
animeList.add(anime);
|
||||
}
|
||||
final nextPage = xpath(res, '//a[@class="next page-link"]/@href');
|
||||
return MPages(animeList, nextPage.isNotEmpty);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<MPages> search(String query, int page, FilterList filterList) async {
|
||||
final filters = filterList.filters;
|
||||
String url = "";
|
||||
if (query.isNotEmpty) {
|
||||
url = "$baseUrl/toute-la-liste-affiches/page/$page/?q=$query";
|
||||
} else {
|
||||
for (var filter in filters) {
|
||||
if (filter.type == "GenreFilter") {
|
||||
if (filter.state != 0) {
|
||||
url = "$baseUrl/${filter.values[filter.state].value}page/$page";
|
||||
}
|
||||
} else if (filter.type == "SubPageFilter") {
|
||||
if (url.isEmpty) {
|
||||
if (filter.state != 0) {
|
||||
url = "$baseUrl/${filter.values[filter.state].value}page/$page";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final res = (await client.get(Uri.parse(url))).body;
|
||||
|
||||
List<MManga> animeList = [];
|
||||
final urls =
|
||||
xpath(res, '//*[@class="list"]/article/div/div/figure/a/@href');
|
||||
final names =
|
||||
xpath(res, '//*[@class="list"]/article/div/div/figure/a/img/@title');
|
||||
final images =
|
||||
xpath(res, '//*[@class="list"]/article/div/div/figure/a/img/@src');
|
||||
|
||||
for (var i = 0; i < names.length; i++) {
|
||||
MManga anime = MManga();
|
||||
anime.name = names[i];
|
||||
anime.imageUrl = images[i];
|
||||
anime.link = urls[i];
|
||||
animeList.add(anime);
|
||||
}
|
||||
final nextPage = xpath(res, '//a[@class="next page-link"]/@href');
|
||||
return MPages(animeList, nextPage.isNotEmpty);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<MManga> getDetail(String url) async {
|
||||
final statusList = [
|
||||
{"En cours": 0, "Terminé": 1}
|
||||
];
|
||||
String res =
|
||||
(await client.get(Uri.parse("$baseUrl${getUrlWithoutDomain(url)}")))
|
||||
.body;
|
||||
MManga anime = MManga();
|
||||
final originalUrl = xpath(res,
|
||||
'//*[@class="breadcrumb"]/li[@class="breadcrumb-item"][2]/a/@href');
|
||||
if (originalUrl.isNotEmpty) {
|
||||
res = (await client.get(Uri.parse(originalUrl.first))).body;
|
||||
}
|
||||
final description =
|
||||
xpath(res, '//*[@class="episode fz-sm synop"]/p/text()');
|
||||
if (description.isNotEmpty) {
|
||||
anime.description = description.first.replaceAll("Synopsis:", "");
|
||||
}
|
||||
final status = xpath(res,
|
||||
'//*[@class="list-unstyled"]/li[contains(text(),"Statut")]/text()');
|
||||
if (status.isNotEmpty) {
|
||||
anime.status =
|
||||
parseStatus(status.first.replaceAll("Statut: ", ""), statusList);
|
||||
}
|
||||
|
||||
anime.genre = xpath(res,
|
||||
'//*[@class="list-unstyled"]/li[contains(text(),"Genre")]/ul/li/a/text()');
|
||||
|
||||
final epUrls = xpath(res, '//*[@class="list-episodes list-group"]/a/@href');
|
||||
final dates =
|
||||
xpath(res, '//*[@class="list-episodes list-group"]/a/span/text()');
|
||||
final names = xpath(res, '//*[@class="list-episodes list-group"]/a/text()');
|
||||
List<String> episodes = [];
|
||||
|
||||
for (var i = 0; i < names.length; i++) {
|
||||
final date = dates[i];
|
||||
final name = names[i];
|
||||
episodes.add(
|
||||
"Episode ${regExp(name.replaceAll(date, ""), r".* (\d*) [VvfF]{1,1}", '', 1, 1)}");
|
||||
}
|
||||
final dateUploads = parseDates(dates, "dd MMMM yyyy", "fr");
|
||||
|
||||
List<MChapter>? episodesList = [];
|
||||
for (var i = 0; i < episodes.length; i++) {
|
||||
MChapter episode = MChapter();
|
||||
episode.name = episodes[i];
|
||||
episode.url = epUrls[i];
|
||||
episode.dateUpload = dateUploads[i];
|
||||
episodesList.add(episode);
|
||||
}
|
||||
|
||||
anime.chapters = episodesList;
|
||||
return anime;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<MVideo>> getVideoList(String url) async {
|
||||
final res =
|
||||
(await client.get(Uri.parse("$baseUrl${getUrlWithoutDomain(url)}")))
|
||||
.body;
|
||||
|
||||
final servers = parseHtml(res).select("div.tab-content iframe[src]");
|
||||
List<MVideo> videos = [];
|
||||
final hosterSelection = preferenceHosterSelection(source.id);
|
||||
for (var url in servers) {
|
||||
final urll = url.getSrc != "about:blank" ? url.getSrc : url.getDataSrc;
|
||||
final resServer = (await client.get(Uri.parse(fixUrl(urll)),
|
||||
headers: {"X-Requested-With": "XMLHttpRequest"}))
|
||||
.body;
|
||||
final serverUrl =
|
||||
fixUrl(regExp(resServer, r"data-url='([^']+)'", '', 1, 1));
|
||||
List<MVideo> a = [];
|
||||
if (serverUrl.contains("https://streamwish") &&
|
||||
hosterSelection.contains("Streamwish")) {
|
||||
a = await streamWishExtractor(serverUrl, "StreamWish");
|
||||
} else if (serverUrl.contains("https://doo") &&
|
||||
hosterSelection.contains("Doodstream")) {
|
||||
a = await doodExtractor(serverUrl);
|
||||
} else if (containsVidBom(serverUrl) &&
|
||||
hosterSelection.contains("VidBom")) {
|
||||
a = await vidBomExtractor(serverUrl);
|
||||
} else if (serverUrl.contains("https://ok.ru") &&
|
||||
hosterSelection.contains("Okru")) {
|
||||
a = await okruExtractor(serverUrl);
|
||||
} else if (serverUrl.contains("upstream") &&
|
||||
hosterSelection.contains("Upstream")) {
|
||||
a = await upstreamExtractor(serverUrl);
|
||||
} else if (serverUrl.contains("sendvid") &&
|
||||
hosterSelection.contains("Sendvid")) {
|
||||
a = await sendVidExtractorr(serverUrl, "");
|
||||
} else if (serverUrl.contains("voe.sx") &&
|
||||
hosterSelection.contains("Voe")) {
|
||||
a = await voeExtractor(serverUrl, "");
|
||||
}
|
||||
videos.addAll(a);
|
||||
}
|
||||
|
||||
return videos;
|
||||
}
|
||||
|
||||
String fixUrl(String url) {
|
||||
return regExp(url, r"^(?:(?:https?:)?//|www\.)", 'https://', 0, 0);
|
||||
}
|
||||
|
||||
bool containsVidBom(String url) {
|
||||
url = url;
|
||||
final list = ["vidbam", "vadbam", "vidbom", "vidbm"];
|
||||
for (var n in list) {
|
||||
if (url.contains(n)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@override
|
||||
List<dynamic> getFilterList() {
|
||||
return [
|
||||
HeaderFilter("La recherche de texte ignore les filtres"),
|
||||
SelectFilter("GenreFilter", "Genre", 0, [
|
||||
SelectFilterOption("<Selectionner>", ""),
|
||||
SelectFilterOption("Action", "/genre/action/"),
|
||||
SelectFilterOption("Aventure", "/genre/aventure/"),
|
||||
SelectFilterOption("Comedie", "/genre/comedie/"),
|
||||
SelectFilterOption("Crime", "/genre/crime/"),
|
||||
SelectFilterOption("Démons", "/genre/demons/"),
|
||||
SelectFilterOption("Drame", "/genre/drame/"),
|
||||
SelectFilterOption("Ecchi", "/genre/ecchi/"),
|
||||
SelectFilterOption("Espace", "/genre/espace/"),
|
||||
SelectFilterOption("Fantastique", "/genre/fantastique/"),
|
||||
SelectFilterOption("Gore", "/genre/gore/"),
|
||||
SelectFilterOption("Harem", "/genre/harem/"),
|
||||
SelectFilterOption("Historique", "/genre/historique/"),
|
||||
SelectFilterOption("Horreur", "/genre/horreur/"),
|
||||
SelectFilterOption("Isekai", "/genre/isekai/"),
|
||||
SelectFilterOption("Jeux", "/genre/jeu/"),
|
||||
SelectFilterOption("L'école", "/genre/lecole/"),
|
||||
SelectFilterOption("Magical girls", "/genre/magical-girls/"),
|
||||
SelectFilterOption("Magie", "/genre/magie/"),
|
||||
SelectFilterOption("Martial Arts", "/genre/martial-arts/"),
|
||||
SelectFilterOption("Mecha", "/genre/mecha/"),
|
||||
SelectFilterOption("Militaire", "/genre/militaire/"),
|
||||
SelectFilterOption("Musique", "/genre/musique/"),
|
||||
SelectFilterOption("Mysterieux", "/genre/mysterieux/"),
|
||||
SelectFilterOption("Parodie", "/genre/Parodie/"),
|
||||
SelectFilterOption("Police", "/genre/police/"),
|
||||
SelectFilterOption("Psychologique", "/genre/psychologique/"),
|
||||
SelectFilterOption("Romance", "/genre/romance/"),
|
||||
SelectFilterOption("Samurai", "/genre/samurai/"),
|
||||
SelectFilterOption("Sci-Fi", "/genre/sci-fi/"),
|
||||
SelectFilterOption("Seinen", "/genre/seinen/"),
|
||||
SelectFilterOption("Shoujo", "/genre/shoujo/"),
|
||||
SelectFilterOption("Shoujo Ai", "/genre/shoujo-ai/"),
|
||||
SelectFilterOption("Shounen", "/genre/shounen/"),
|
||||
SelectFilterOption("Shounen Ai", "/genre/shounen-ai/"),
|
||||
SelectFilterOption("Sport", "/genre/sport/"),
|
||||
SelectFilterOption("Super Power", "/genre/super-power/"),
|
||||
SelectFilterOption("Surnaturel", "/genre/surnaturel/"),
|
||||
SelectFilterOption("Suspense", "/genre/suspense/"),
|
||||
SelectFilterOption("Thriller", "/genre/thriller/"),
|
||||
SelectFilterOption("Tranche de vie", "/genre/tranche-de-vie/"),
|
||||
SelectFilterOption("Vampire", "/genre/vampire/")
|
||||
]),
|
||||
SelectFilter("SubPageFilter", "Sous page", 0, [
|
||||
SelectFilterOption("<Selectionner>", ""),
|
||||
SelectFilterOption("Terminé", "/termine/"),
|
||||
SelectFilterOption("Film", "/film/"),
|
||||
])
|
||||
];
|
||||
}
|
||||
|
||||
@override
|
||||
List<dynamic> getSourcePreferences() {
|
||||
return [
|
||||
EditTextPreference(
|
||||
key: "overrideBaseUrl",
|
||||
title: "Changer l'url de base",
|
||||
summary: "",
|
||||
value: "https://otakufr.cc",
|
||||
dialogTitle: "Changer l'url de base",
|
||||
dialogMessage: "",
|
||||
text: "https://otakufr.cc"),
|
||||
ListPreference(
|
||||
key: "preferred_quality",
|
||||
title: "Qualité préférée",
|
||||
summary: "",
|
||||
valueIndex: 1,
|
||||
entries: ["1080p", "720p", "480p", "360p"],
|
||||
entryValues: ["1080", "720", "480", "360"]),
|
||||
MultiSelectListPreference(
|
||||
key: "hoster_selection_",
|
||||
title: "Enable/Disable Hosts",
|
||||
summary: "",
|
||||
entries: [
|
||||
"Streamwish",
|
||||
"Doodstream",
|
||||
"Sendvid",
|
||||
"VidBom",
|
||||
"Okru",
|
||||
"Voe",
|
||||
"Sibnet",
|
||||
"Upstream"
|
||||
],
|
||||
entryValues: [
|
||||
"Streamwish",
|
||||
"Doodstream",
|
||||
"Sendvid",
|
||||
"VidBom",
|
||||
"Okru",
|
||||
"Voe",
|
||||
"Sibnet",
|
||||
"Upstream"
|
||||
],
|
||||
values: [
|
||||
"Streamwish",
|
||||
"Doodstream",
|
||||
"Sendvid",
|
||||
"Vidbm",
|
||||
"Okru",
|
||||
"Voe",
|
||||
"Sibnet",
|
||||
"Upstream"
|
||||
]),
|
||||
];
|
||||
}
|
||||
|
||||
List<MVideo> sortVideos(List<MVideo> videos, int sourceId) {
|
||||
String quality = getPreferenceValue(sourceId, "preferred_quality");
|
||||
|
||||
videos.sort((MVideo a, MVideo b) {
|
||||
int qualityMatchA = 0;
|
||||
if (a.quality.contains(quality)) {
|
||||
qualityMatchA = 1;
|
||||
}
|
||||
int qualityMatchB = 0;
|
||||
if (b.quality.contains(quality)) {
|
||||
qualityMatchB = 1;
|
||||
}
|
||||
if (qualityMatchA != qualityMatchB) {
|
||||
return qualityMatchB - qualityMatchA;
|
||||
}
|
||||
|
||||
final regex = RegExp(r'(\d+)p');
|
||||
final matchA = regex.firstMatch(a.quality);
|
||||
final matchB = regex.firstMatch(b.quality);
|
||||
final int qualityNumA = int.tryParse(matchA?.group(1) ?? '0') ?? 0;
|
||||
final int qualityNumB = int.tryParse(matchB?.group(1) ?? '0') ?? 0;
|
||||
return qualityNumB - qualityNumA;
|
||||
});
|
||||
|
||||
return videos;
|
||||
}
|
||||
|
||||
List<String> preferenceHosterSelection(int sourceId) {
|
||||
return getPreferenceValue(sourceId, "hoster_selection_");
|
||||
}
|
||||
|
||||
Future<List<MVideo>> sendVidExtractorr(String url, String prefix) async {
|
||||
final res = (await client.get(Uri.parse(url))).body;
|
||||
final document = parseHtml(res);
|
||||
final masterUrl = document.selectFirst("source#video_source")?.attr("src");
|
||||
if (masterUrl == null) return [];
|
||||
final masterHeaders = {
|
||||
"Accept": "*/*",
|
||||
"Host": Uri.parse(masterUrl).host,
|
||||
"Origin": "https://${Uri.parse(url).host}",
|
||||
"Referer": "https://${Uri.parse(url).host}/",
|
||||
};
|
||||
List<MVideo> videos = [];
|
||||
if (masterUrl.contains(".m3u8")) {
|
||||
final masterPlaylistRes = (await client.get(Uri.parse(masterUrl))).body;
|
||||
|
||||
for (var it in substringAfter(masterPlaylistRes, "#EXT-X-STREAM-INF:")
|
||||
.split("#EXT-X-STREAM-INF:")) {
|
||||
final quality =
|
||||
"${substringBefore(substringBefore(substringAfter(substringAfter(it, "RESOLUTION="), "x"), ","), "\n")}p";
|
||||
|
||||
String videoUrl = substringBefore(substringAfter(it, "\n"), "\n");
|
||||
|
||||
if (!videoUrl.startsWith("http")) {
|
||||
videoUrl =
|
||||
"${masterUrl.split("/").sublist(0, masterUrl.split("/").length - 1).join("/")}/$videoUrl";
|
||||
}
|
||||
final videoHeaders = {
|
||||
"Accept": "*/*",
|
||||
"Host": Uri.parse(videoUrl).host,
|
||||
"Origin": "https://${Uri.parse(url).host}",
|
||||
"Referer": "https://${Uri.parse(url).host}/",
|
||||
};
|
||||
var video = MVideo();
|
||||
video
|
||||
..url = videoUrl
|
||||
..originalUrl = videoUrl
|
||||
..quality = prefix + "Sendvid:$quality"
|
||||
..headers = videoHeaders;
|
||||
videos.add(video);
|
||||
}
|
||||
} else {
|
||||
var video = MVideo();
|
||||
video
|
||||
..url = masterUrl
|
||||
..originalUrl = masterUrl
|
||||
..quality = prefix + "Sendvid:default"
|
||||
..headers = masterHeaders;
|
||||
videos.add(video);
|
||||
}
|
||||
|
||||
return videos;
|
||||
}
|
||||
|
||||
Future<List<MVideo>> upstreamExtractor(String url) async {
|
||||
final res = (await client.get(Uri.parse(url))).body;
|
||||
final js = xpath(res, '//script[contains(text(), "m3u8")]/text()');
|
||||
if (js.isEmpty) {
|
||||
return [];
|
||||
}
|
||||
final masterUrl =
|
||||
substringBefore(substringAfter(unpackJs(js.first), "{file:\""), "\"}");
|
||||
final masterPlaylistRes = (await client.get(Uri.parse(masterUrl))).body;
|
||||
List<MVideo> videos = [];
|
||||
for (var it in substringAfter(masterPlaylistRes, "#EXT-X-STREAM-INF:")
|
||||
.split("#EXT-X-STREAM-INF:")) {
|
||||
final quality =
|
||||
"${substringBefore(substringBefore(substringAfter(substringAfter(it, "RESOLUTION="), "x"), ","), "\n")}p";
|
||||
|
||||
String videoUrl = substringBefore(substringAfter(it, "\n"), "\n");
|
||||
|
||||
if (!videoUrl.startsWith("http")) {
|
||||
videoUrl =
|
||||
"${masterUrl.split("/").sublist(0, masterUrl.split("/").length - 1).join("/")}/$videoUrl";
|
||||
}
|
||||
|
||||
MVideo video = MVideo();
|
||||
video
|
||||
..url = videoUrl
|
||||
..originalUrl = videoUrl
|
||||
..quality = "Upstream - $quality";
|
||||
videos.add(video);
|
||||
}
|
||||
return videos;
|
||||
}
|
||||
}
|
||||
|
||||
OtakuFr main(MSource source) {
|
||||
return OtakuFr(source: source);
|
||||
}
|
||||
17
dart/anime/src/fr/otakufr/source.dart
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
import '../../../../../model/source.dart';
|
||||
|
||||
Source get otakufr => _otakufr;
|
||||
const otakufrVersion = "0.0.95";
|
||||
const otakufrCodeUrl =
|
||||
"https://raw.githubusercontent.com/kodjodevf/mangayomi-extensions/$branchName/dart/anime/src/fr/otakufr/otakufr.dart";
|
||||
Source _otakufr = Source(
|
||||
name: "OtakuFr",
|
||||
baseUrl: "https://otakufr.cc",
|
||||
lang: "fr",
|
||||
typeSource: "single",
|
||||
iconUrl:
|
||||
"https://raw.githubusercontent.com/kodjodevf/mangayomi-extensions/$branchName/dart/anime/src/fr/otakufr/icon.png",
|
||||
sourceCodeUrl: otakufrCodeUrl,
|
||||
version: otakufrVersion,
|
||||
itemType: ItemType.anime,
|
||||
isFullData: false);
|
||||
BIN
dart/anime/src/hi/yomovies/icon.png
Normal file
|
After Width: | Height: | Size: 3.4 KiB |
16
dart/anime/src/hi/yomovies/source.dart
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
import '../../../../../model/source.dart';
|
||||
|
||||
Source get yomoviesSource => _yomoviesSource;
|
||||
const _yomoviesVersion = "0.0.25";
|
||||
const _yomoviesSourceCodeUrl =
|
||||
"https://raw.githubusercontent.com/kodjodevf/mangayomi-extensions/$branchName/dart/anime/src/hi/yomovies/yomovies.dart";
|
||||
Source _yomoviesSource = Source(
|
||||
name: "YoMovies",
|
||||
baseUrl: "https://yomovies.boo",
|
||||
lang: "hi",
|
||||
typeSource: "single",
|
||||
iconUrl:
|
||||
"https://raw.githubusercontent.com/kodjodevf/mangayomi-extensions/$branchName/dart/anime/src/hi/yomovies/icon.png",
|
||||
sourceCodeUrl: _yomoviesSourceCodeUrl,
|
||||
version: _yomoviesVersion,
|
||||
itemType: ItemType.anime);
|
||||
342
dart/anime/src/hi/yomovies/yomovies.dart
Normal file
|
|
@ -0,0 +1,342 @@
|
|||
import 'package:mangayomi/bridge_lib.dart';
|
||||
import 'dart:convert';
|
||||
|
||||
class YoMovies extends MProvider {
|
||||
YoMovies({required this.source});
|
||||
|
||||
MSource source;
|
||||
|
||||
final Client client = Client(source);
|
||||
|
||||
@override
|
||||
String get baseUrl => getPreferenceValue(source.id, "overrideBaseUrl");
|
||||
|
||||
@override
|
||||
bool get supportsLatest => false;
|
||||
|
||||
@override
|
||||
Future<MPages> getPopular(int page) async {
|
||||
String pageNu = page == 1 ? "" : "page/$page/";
|
||||
|
||||
final res =
|
||||
(await client.get(Uri.parse("$baseUrl/most-favorites/$pageNu"))).body;
|
||||
final document = parseHtml(res);
|
||||
return animeFromElement(
|
||||
document.select("div.movies-list > div.ml-item"),
|
||||
document.selectFirst("ul.pagination > li.active + li")?.getHref !=
|
||||
null);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<MPages> getLatestUpdates(int page) async {
|
||||
return MPages([], false);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<MPages> search(String query, int page, FilterList filterList) async {
|
||||
final filters = filterList.filters;
|
||||
String url = "";
|
||||
String pageNu = page == 1 ? "" : "/page/$page";
|
||||
if (query.isNotEmpty) {
|
||||
url = "$baseUrl$pageNu/?s=$query";
|
||||
} else {
|
||||
for (var filter in filters) {
|
||||
if (filter.type.isNotEmpty) {
|
||||
final first = filter.values[filter.state].value;
|
||||
if (first.isNotEmpty) {
|
||||
url = first;
|
||||
}
|
||||
}
|
||||
}
|
||||
url = "$baseUrl$url$pageNu";
|
||||
}
|
||||
final res = (await client.get(Uri.parse(url))).body;
|
||||
final document = parseHtml(res);
|
||||
return animeFromElement(
|
||||
document.select("div.movies-list > div.ml-item"),
|
||||
document.selectFirst("ul.pagination > li.active + li")?.getHref !=
|
||||
null);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<MManga> getDetail(String url) async {
|
||||
url = getUrlWithoutDomain(url);
|
||||
|
||||
final res = (await client.get(Uri.parse("$baseUrl$url"))).body;
|
||||
final document = parseHtml(res);
|
||||
MManga anime = MManga();
|
||||
var infoElement = document.selectFirst("div.mvi-content");
|
||||
anime.description = infoElement.selectFirst("p.f-desc")?.text ?? "";
|
||||
|
||||
anime.genre = xpath(res,
|
||||
'//div[@class="mvici-left" and contains(text(),"Genre:")]/p/a/text()');
|
||||
|
||||
List<MChapter> episodeList = [];
|
||||
final seasonListElements = document.select("div#seasons > div.tvseason");
|
||||
if (seasonListElements.isEmpty) {
|
||||
MChapter ep = MChapter();
|
||||
ep.name = "Movie";
|
||||
ep.url = url;
|
||||
episodeList.add(ep);
|
||||
} else {
|
||||
for (var season in seasonListElements) {
|
||||
var seasonText = season.selectFirst("div.les-title").text.trim();
|
||||
for (var episode in season.select("div.les-content > a")) {
|
||||
var epNumber = substringAfter(episode.text.trim(), "pisode ");
|
||||
MChapter ep = MChapter();
|
||||
ep.name = "$seasonText Ep. $epNumber";
|
||||
ep.url = episode.getHref;
|
||||
|
||||
episodeList.add(ep);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
anime.chapters = episodeList.reversed.toList();
|
||||
return anime;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<MVideo>> getVideoList(String url) async {
|
||||
url = getUrlWithoutDomain(url);
|
||||
final res = (await client.get(Uri.parse("$baseUrl$url"))).body;
|
||||
final document = parseHtml(res);
|
||||
final serverElements = document.select("div.movieplay > iframe");
|
||||
List<MVideo> videos = [];
|
||||
for (var serverElement in serverElements) {
|
||||
var url = serverElement.getSrc;
|
||||
List<MVideo> a = [];
|
||||
if (url.contains("minoplres")) {
|
||||
a = await minoplresExtractor(url);
|
||||
}
|
||||
videos.addAll(a);
|
||||
}
|
||||
return sortVideos(videos, source.id);
|
||||
}
|
||||
|
||||
@override
|
||||
List<dynamic> getSourcePreferences() {
|
||||
return [
|
||||
EditTextPreference(
|
||||
key: "overrideBaseUrl",
|
||||
title: "Override BaseUrl",
|
||||
summary: "",
|
||||
value: "https://yomovies.boo",
|
||||
dialogTitle: "Override BaseUrl",
|
||||
dialogMessage: "",
|
||||
text: "https://yomovies.boo"),
|
||||
ListPreference(
|
||||
key: "preferred_quality",
|
||||
title: "Preferred quality",
|
||||
summary: "",
|
||||
valueIndex: 0,
|
||||
entries: ["1080p", "720p", "480p", "360p"],
|
||||
entryValues: ["1080", "720", "480", "360"])
|
||||
];
|
||||
}
|
||||
|
||||
Future<List<MVideo>> minoplresExtractor(String url) async {
|
||||
List<MVideo> videos = [];
|
||||
|
||||
final res =
|
||||
(await client.get(Uri.parse(url), headers: {"Referer": url})).body;
|
||||
final script = xpath(res, '//script[contains(text(),"sources:")]/text()');
|
||||
if (script.isEmpty) return [];
|
||||
final masterUrl =
|
||||
substringBefore(substringAfter(script.first, "file:\""), '"');
|
||||
final masterPlaylistRes = (await client.get(Uri.parse(masterUrl))).body;
|
||||
for (var it in substringAfter(masterPlaylistRes, "#EXT-X-STREAM-INF:")
|
||||
.split("#EXT-X-STREAM-INF:")) {
|
||||
final quality =
|
||||
"${substringBefore(substringBefore(substringAfter(substringAfter(it, "RESOLUTION="), "x"), ","), "\n")}p";
|
||||
|
||||
String videoUrl = substringBefore(substringAfter(it, "\n"), "\n");
|
||||
|
||||
MVideo video = MVideo();
|
||||
video
|
||||
..url = videoUrl
|
||||
..originalUrl = videoUrl
|
||||
..quality = "Minoplres - $quality";
|
||||
videos.add(video);
|
||||
}
|
||||
return videos;
|
||||
}
|
||||
|
||||
MPages animeFromElement(List<MElement> elements, bool hasNextPage) {
|
||||
List<MManga> animeList = [];
|
||||
for (var element in elements) {
|
||||
MManga anime = MManga();
|
||||
anime.name = element.selectFirst("div.qtip-title").text;
|
||||
anime.imageUrl =
|
||||
element.selectFirst("img[data-original]")?.attr("data-original") ??
|
||||
"";
|
||||
anime.link = element.selectFirst("a[href]").getHref;
|
||||
animeList.add(anime);
|
||||
}
|
||||
return MPages(animeList, hasNextPage);
|
||||
}
|
||||
|
||||
List<MVideo> sortVideos(List<MVideo> videos, int sourceId) {
|
||||
String quality = getPreferenceValue(sourceId, "preferred_quality");
|
||||
|
||||
videos.sort((MVideo a, MVideo b) {
|
||||
int qualityMatchA = 0;
|
||||
if (a.quality.contains(quality)) {
|
||||
qualityMatchA = 1;
|
||||
}
|
||||
int qualityMatchB = 0;
|
||||
if (b.quality.contains(quality)) {
|
||||
qualityMatchB = 1;
|
||||
}
|
||||
if (qualityMatchA != qualityMatchB) {
|
||||
return qualityMatchB - qualityMatchA;
|
||||
}
|
||||
|
||||
final regex = RegExp(r'(\d+)p');
|
||||
final matchA = regex.firstMatch(a.quality);
|
||||
final matchB = regex.firstMatch(b.quality);
|
||||
final int qualityNumA = int.tryParse(matchA?.group(1) ?? '0') ?? 0;
|
||||
final int qualityNumB = int.tryParse(matchB?.group(1) ?? '0') ?? 0;
|
||||
return qualityNumB - qualityNumA;
|
||||
});
|
||||
|
||||
return videos;
|
||||
}
|
||||
|
||||
@override
|
||||
List<dynamic> getFilterList() {
|
||||
return [
|
||||
HeaderFilter(
|
||||
"Note: Only one selection at a time works, and it ignores text search"),
|
||||
SeparatorFilter(),
|
||||
SelectFilter("BollywoodFilter", "Bollywood", 0, [
|
||||
SelectFilterOption("<select>", ""),
|
||||
SelectFilterOption("Bollywood", "/genre/bollywood"),
|
||||
SelectFilterOption("Trending", "/genre/top-rated"),
|
||||
SelectFilterOption("Bollywood (2024)",
|
||||
"/account/?ptype=post&tax_category%5B%5D=bollywood&tax_release-year=2024&wpas=1"),
|
||||
SelectFilterOption("Bollywood (2023)",
|
||||
"/account/?ptype=post&tax_category%5B%5D=bollywood&tax_release-year=2023&wpas=1"),
|
||||
SelectFilterOption("Bollywood (2022)",
|
||||
"/account/?ptype=post&tax_category%5B%5D=bollywood&tax_release-year=2022&wpas=1"),
|
||||
SelectFilterOption("Bollywood (2021)",
|
||||
"/account/?ptype=post&tax_category%5B%5D=bollywood&tax_release-year=2021&wpas=1"),
|
||||
]),
|
||||
SelectFilter("DualAudioFilter", "Dual Audio", 0, [
|
||||
SelectFilterOption("<select>", ""),
|
||||
SelectFilterOption("Dual Audio", "/genre/dual-audio"),
|
||||
SelectFilterOption("Hollywood Dubbed",
|
||||
"/account/?ptype=post&tax_category%5B%5D=dual-audio&wpas=1"),
|
||||
SelectFilterOption("South Dubbed",
|
||||
"/account/?ptype=post&tax_category%5B%5D=dual-audio&tax_category%5B%5D=south-special&wpas=1"),
|
||||
]),
|
||||
SelectFilter("HollywoodFilter", "Hollywood", 0, [
|
||||
SelectFilterOption("<select>", ""),
|
||||
SelectFilterOption("Hollywood", "/genre/hollywood"),
|
||||
SelectFilterOption("Hollywood (2023)",
|
||||
"/account/?ptype=post&tax_category%5B%5D=hollywood&tax_release-year=2023&wpas=1"),
|
||||
SelectFilterOption("Hollywood (2022)",
|
||||
"/account/?ptype=post&tax_category%5B%5D=hollywood&tax_release-year=2022&wpas=1"),
|
||||
SelectFilterOption("Hollywood (2021)",
|
||||
"/account/?ptype=post&tax_category%5B%5D=hollywood&tax_release-year=2021&wpas=1"),
|
||||
]),
|
||||
SelectFilter("EnglishSeriesFilter", "Hindi Series", 0, [
|
||||
SelectFilterOption("<select>", ""),
|
||||
SelectFilterOption("English Series", "/series"),
|
||||
]),
|
||||
SelectFilter("HindiSeriesFilter", "English Series", 0, [
|
||||
SelectFilterOption("<select>", ""),
|
||||
SelectFilterOption("Hindi Series", "/genre/web-series"),
|
||||
SelectFilterOption("Netflix", "/director/netflix"),
|
||||
SelectFilterOption("Amazon", "/director/amazon-prime"),
|
||||
SelectFilterOption("Altbalaji", "/director/altbalaji"),
|
||||
SelectFilterOption("Zee5", "/director/zee5"),
|
||||
SelectFilterOption("Voot", "/director/voot-originals"),
|
||||
SelectFilterOption("Mx Player", "/director/mx-player"),
|
||||
SelectFilterOption("Hotstar", "/director/hotstar"),
|
||||
SelectFilterOption("Viu", "/director/viu-originals"),
|
||||
SelectFilterOption("Sony Liv", "/director/sonyliv-original"),
|
||||
]),
|
||||
SelectFilter("GenreFilter", "Genre", 0, [
|
||||
SelectFilterOption("<select>", ""),
|
||||
SelectFilterOption("Action", "/genre/action"),
|
||||
SelectFilterOption("Adventure", "/genre/adventure"),
|
||||
SelectFilterOption("Animation", "/genre/animation"),
|
||||
SelectFilterOption("Biography", "/genre/biography"),
|
||||
SelectFilterOption("Comedy", "/genre/comedy"),
|
||||
SelectFilterOption("Crime", "/genre/crime"),
|
||||
SelectFilterOption("Drama", "/genre/drama"),
|
||||
SelectFilterOption("Music", "/genre/music"),
|
||||
SelectFilterOption("Mystery", "/genre/mystery"),
|
||||
SelectFilterOption("Family", "/genre/family"),
|
||||
SelectFilterOption("Fantasy", "/genre/fantasy"),
|
||||
SelectFilterOption("Horror", "/genre/horror"),
|
||||
SelectFilterOption("History", "/genre/history"),
|
||||
SelectFilterOption("Romance", "/genre/romantic"),
|
||||
SelectFilterOption("Science Fiction", "/genre/science-fiction"),
|
||||
SelectFilterOption("Thriller", "/genre/thriller"),
|
||||
SelectFilterOption("War", "/genre/war"),
|
||||
]),
|
||||
SelectFilter("ExtraMoviesFilter", "ExtraMovies", 0, [
|
||||
SelectFilterOption("<select>", ""),
|
||||
SelectFilterOption("ExtraMovies", "/genre/south-special"),
|
||||
SelectFilterOption("Bengali", "/genre/bengali"),
|
||||
SelectFilterOption("Marathi", "/genre/marathi"),
|
||||
SelectFilterOption("Gujarati", "/genre/gujarati"),
|
||||
SelectFilterOption("Punjabi", "/genre/punjabi"),
|
||||
SelectFilterOption("Tamil", "/genre/tamil"),
|
||||
SelectFilterOption("Telugu", "/genre/telugu"),
|
||||
SelectFilterOption("Malayalam", "/genre/malayalam"),
|
||||
SelectFilterOption("Kannada", "/genre/kannada"),
|
||||
SelectFilterOption("Pakistani", "/genre/pakistani"),
|
||||
]),
|
||||
SelectFilter("EroticFilter", "Erotic", 0, [
|
||||
SelectFilterOption("<select>", ""),
|
||||
SelectFilterOption("Erotic", "/genre/erotic-movies"),
|
||||
]),
|
||||
SelectFilter("HotSeriesFilter", "Hot Series", 0, [
|
||||
SelectFilterOption("<select>", ""),
|
||||
SelectFilterOption("Hot Series", "/genre/tv-shows"),
|
||||
SelectFilterOption("Uncut", "/?s=uncut"),
|
||||
SelectFilterOption("Fliz Movies", "/director/fliz-movies"),
|
||||
SelectFilterOption("Nuefliks", "/director/nuefliks-exclusive"),
|
||||
SelectFilterOption("Hotshots", "/director/hotshots"),
|
||||
SelectFilterOption("Ullu Originals", "/?s=ullu"),
|
||||
SelectFilterOption("Kooku", "/director/kooku-originals"),
|
||||
SelectFilterOption("Gupchup", "/director/gupchup-exclusive"),
|
||||
SelectFilterOption("Feneomovies", "/director/feneomovies"),
|
||||
SelectFilterOption("Cinemadosti", "/director/cinemadosti"),
|
||||
SelectFilterOption("Primeflix", "/director/primeflix"),
|
||||
SelectFilterOption("Gemplex", "/director/gemplex"),
|
||||
SelectFilterOption("Rabbit", "/director/rabbit-original"),
|
||||
SelectFilterOption("HotMasti", "/director/hotmasti-originals"),
|
||||
SelectFilterOption("BoomMovies", "/director/boommovies-original"),
|
||||
SelectFilterOption("CliffMovies", "/director/cliff-movies"),
|
||||
SelectFilterOption("MastiPrime", "/director/masti-prime-originals"),
|
||||
SelectFilterOption("Ek Night Show", "/director/ek-night-show"),
|
||||
SelectFilterOption("Flixsksmovies", "/director/flixsksmovies"),
|
||||
SelectFilterOption("Lootlo", "/director/lootlo-original"),
|
||||
SelectFilterOption("Hootzy", "/director/hootzy-channel"),
|
||||
SelectFilterOption("Balloons", "/director/balloons-originals"),
|
||||
SelectFilterOption(
|
||||
"Big Movie Zoo", "/director/big-movie-zoo-originals"),
|
||||
SelectFilterOption("Bambooflix", "/director/bambooflix"),
|
||||
SelectFilterOption("Piliflix", "/director/piliflix-originals"),
|
||||
SelectFilterOption("11upmovies", "/director/11upmovies-originals"),
|
||||
SelectFilterOption("Eightshots", "/director/eightshots-originals"),
|
||||
SelectFilterOption(
|
||||
"I-Entertainment", "/director/i-entertainment-exclusive"),
|
||||
SelectFilterOption("Hotprime", "/director/hotprime-originals"),
|
||||
SelectFilterOption("BananaPrime", "/director/banana-prime"),
|
||||
SelectFilterOption("HotHitFilms", "/director/hothitfilms"),
|
||||
SelectFilterOption("Chikooflix", "/director/chikooflix-originals"),
|
||||
SelectFilterOption("Glamheart", "/?s=glamheart"),
|
||||
SelectFilterOption("Worldprime", "/director/worldprime-originals"),
|
||||
]),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
YoMovies main(MSource source) {
|
||||
return YoMovies(source: source);
|
||||
}
|
||||
BIN
dart/anime/src/id/nimegami/icon.png
Normal file
|
After Width: | Height: | Size: 3.6 KiB |
182
dart/anime/src/id/nimegami/nimegami.dart
Normal file
|
|
@ -0,0 +1,182 @@
|
|||
import 'package:mangayomi/bridge_lib.dart';
|
||||
import 'dart:convert';
|
||||
|
||||
class NimeGami extends MProvider {
|
||||
NimeGami({required this.source});
|
||||
|
||||
MSource source;
|
||||
|
||||
final Client client = Client(source);
|
||||
|
||||
@override
|
||||
Future<MPages> getPopular(int page) async {
|
||||
final res =
|
||||
(await client.get(Uri.parse("${source.baseUrl}/page/$page"))).body;
|
||||
List<MManga> animeList = [];
|
||||
final urls = xpath(res, '//div[@class="wrapper-2-a"]/article/a/@href');
|
||||
final names = xpath(res, '//div[@class="wrapper-2-a"]/article/a/@title');
|
||||
final images =
|
||||
xpath(res, '//div[@class="wrapper-2-a"]/article/a/div/img/@src');
|
||||
|
||||
for (var i = 0; i < names.length; i++) {
|
||||
MManga anime = MManga();
|
||||
anime.name = names[i];
|
||||
anime.imageUrl = images[i];
|
||||
anime.link = urls[i];
|
||||
animeList.add(anime);
|
||||
}
|
||||
return MPages(animeList, true);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<MPages> getLatestUpdates(int page) async {
|
||||
final res =
|
||||
(await client.get(Uri.parse("${source.baseUrl}/page/$page"))).body;
|
||||
List<MManga> animeList = [];
|
||||
final urls = xpath(res, '//div[@class="post-article"]/article/div/a/@href');
|
||||
final names =
|
||||
xpath(res, '//div[@class="post-article"]/article/div/a/@title');
|
||||
final images =
|
||||
xpath(res, '//div[@class="post-article"]/article/div/a/img/@src');
|
||||
|
||||
for (var i = 0; i < names.length; i++) {
|
||||
MManga anime = MManga();
|
||||
anime.name = names[i];
|
||||
anime.imageUrl = images[i];
|
||||
anime.link = urls[i];
|
||||
animeList.add(anime);
|
||||
}
|
||||
return MPages(animeList, true);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<MPages> search(String query, int page, FilterList filterList) async {
|
||||
final res = (await client.get(
|
||||
Uri.parse("${source.baseUrl}/page/$page/?s=$query&post_type=post")))
|
||||
.body;
|
||||
List<MManga> animeList = [];
|
||||
final urls = xpath(res, '//div[@class="archive-a"]/article/div/a/@href');
|
||||
final names = xpath(res, '//div[@class="archive-a"]/article/h2/a/@title');
|
||||
final images =
|
||||
xpath(res, '//div[@class="archive-a"]/article/div/a/img/@src');
|
||||
|
||||
for (var i = 0; i < names.length; i++) {
|
||||
MManga anime = MManga();
|
||||
anime.name = names[i];
|
||||
anime.imageUrl = images[i];
|
||||
anime.link = urls[i];
|
||||
animeList.add(anime);
|
||||
}
|
||||
return MPages(animeList, true);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<MManga> getDetail(String url) async {
|
||||
final res = (await client.get(Uri.parse(url))).body;
|
||||
MManga anime = MManga();
|
||||
final description = xpath(res, '//*[@id="Sinopsis"]/p/text()');
|
||||
if (description.isNotEmpty) {
|
||||
anime.description = description.first;
|
||||
}
|
||||
|
||||
final author = xpath(res, '//tbody/tr[5]/td[2]/text()');
|
||||
if (author.isNotEmpty) {
|
||||
anime.author = author.first;
|
||||
}
|
||||
anime.genre = xpath(res, '//tr/td[@class="info_a"]/a/text()');
|
||||
final epUrls = xpath(res, '//div[@class="list_eps_stream"]/li/@data')
|
||||
.reversed
|
||||
.toList();
|
||||
final epNums =
|
||||
xpath(res, '//div[@class="list_eps_stream"]/li/@id').reversed.toList();
|
||||
final names = xpath(res, '//div[@class="list_eps_stream"]/li/text()')
|
||||
.reversed
|
||||
.toList();
|
||||
List<MChapter>? episodesList = [];
|
||||
for (var i = 0; i < epUrls.length; i++) {
|
||||
MChapter episode = MChapter();
|
||||
episode.name = names[i];
|
||||
episode.url = json.encode({
|
||||
"episodeIndex": int.parse(substringAfterLast(epNums[i], '_')),
|
||||
'urls': json.decode(utf8.decode(base64Url.decode(epUrls[i])))
|
||||
});
|
||||
episodesList.add(episode);
|
||||
}
|
||||
anime.chapters = episodesList;
|
||||
return anime;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<MVideo>> getVideoList(String url) async {
|
||||
final resJson = json.decode(url);
|
||||
final urls = resJson["urls"];
|
||||
List<MVideo> videos = [];
|
||||
List<MVideo> a = [];
|
||||
for (var data in urls) {
|
||||
final quality = data["format"];
|
||||
for (var url in data["url"]) {
|
||||
a = await extractVideos(quality, url);
|
||||
videos.addAll(a);
|
||||
}
|
||||
}
|
||||
return videos;
|
||||
}
|
||||
|
||||
Future<List<MVideo>> extractVideos(String quality, String url) async {
|
||||
List<MVideo> videos = [];
|
||||
if (url.contains("video.nimegami.id")) {
|
||||
final realUrl = utf8.decode(
|
||||
base64Url.decode(substringBefore(substringAfter(url, "url="), "&")));
|
||||
final a = await extractHXFileVideos(realUrl, quality);
|
||||
videos.addAll(a);
|
||||
} else if (url.contains("berkasdrive") || url.contains("drive.nimegami")) {
|
||||
final res = (await client.get(Uri.parse(url))).body;
|
||||
final source = xpath(res, '//source/@src');
|
||||
if (source.isNotEmpty) {
|
||||
videos.add(toVideo(source.first, "Berkasdrive - $quality"));
|
||||
}
|
||||
} else if (url.contains("hxfile.co")) {
|
||||
final a = await extractHXFileVideos(url, quality);
|
||||
videos.addAll(a);
|
||||
}
|
||||
|
||||
return videos;
|
||||
}
|
||||
|
||||
Future<List<MVideo>> extractHXFileVideos(String url, String quality) async {
|
||||
if (!url.contains("embed-")) {
|
||||
url = url.replaceAll(".co/", ".co/embed-") + ".html";
|
||||
}
|
||||
final res = (await client.get(Uri.parse(url))).body;
|
||||
final script = xpath(res,
|
||||
'//script[contains(text(), "eval") and contains(text(), "p,a,c,k,e,d")]/text()');
|
||||
if (script.isNotEmpty) {
|
||||
final videoUrl = substringBefore(
|
||||
substringAfter(
|
||||
substringAfter(unpackJs(script.first), "sources:[", ""),
|
||||
"file\":\"",
|
||||
""),
|
||||
'"');
|
||||
if (videoUrl.isNotEmpty) {
|
||||
return [toVideo(videoUrl, "HXFile - $quality")];
|
||||
}
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
MVideo toVideo(String videoUrl, String quality) {
|
||||
MVideo video = MVideo();
|
||||
video
|
||||
..url = videoUrl
|
||||
..originalUrl = videoUrl
|
||||
..quality = quality
|
||||
..subtitles = [];
|
||||
|
||||
return video;
|
||||
}
|
||||
}
|
||||
|
||||
NimeGami main(MSource source) {
|
||||
return NimeGami(source: source);
|
||||
}
|
||||
16
dart/anime/src/id/nimegami/source.dart
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
import '../../../../../model/source.dart';
|
||||
|
||||
Source get nimegami => _nimegami;
|
||||
const _nimegamiVersion = "0.0.55";
|
||||
const _nimegamiCodeUrl =
|
||||
"https://raw.githubusercontent.com/kodjodevf/mangayomi-extensions/$branchName/dart/anime/src/id/nimegami/nimegami.dart";
|
||||
Source _nimegami = Source(
|
||||
name: "NimeGami",
|
||||
baseUrl: "https://nimegami.id",
|
||||
lang: "id",
|
||||
typeSource: "single",
|
||||
iconUrl:
|
||||
"https://raw.githubusercontent.com/kodjodevf/mangayomi-extensions/$branchName/dart/anime/src/id/nimegami/icon.png",
|
||||
sourceCodeUrl: _nimegamiCodeUrl,
|
||||
version: _nimegamiVersion,
|
||||
itemType: ItemType.anime);
|
||||
BIN
dart/anime/src/id/oploverz/icon.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
154
dart/anime/src/id/oploverz/oploverz.dart
Normal file
|
|
@ -0,0 +1,154 @@
|
|||
import 'package:mangayomi/bridge_lib.dart';
|
||||
import 'dart:convert';
|
||||
|
||||
class OploVerz extends MProvider {
|
||||
OploVerz({required this.source});
|
||||
|
||||
MSource source;
|
||||
|
||||
final Client client = Client(source);
|
||||
|
||||
@override
|
||||
Future<MPages> getPopular(int page) async {
|
||||
final res = (await client.get(Uri.parse(
|
||||
"${source.baseUrl}/anime-list/page/$page/?order=popular")))
|
||||
.body;
|
||||
return parseAnimeList(res);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<MPages> getLatestUpdates(int page) async {
|
||||
final res = (await client.get(
|
||||
Uri.parse("${source.baseUrl}/anime-list/page/$page/?order=latest")))
|
||||
.body;
|
||||
return parseAnimeList(res);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<MPages> search(String query, int page, FilterList filterList) async {
|
||||
final res = (await client.get(
|
||||
Uri.parse("${source.baseUrl}/anime-list/page/$page/?title=$query")))
|
||||
.body;
|
||||
return parseAnimeList(res);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<MManga> getDetail(String url) async {
|
||||
final statusList = [
|
||||
{"ongoing": 0, "completed": 1}
|
||||
];
|
||||
|
||||
final res = (await client.get(Uri.parse(url))).body;
|
||||
MManga anime = MManga();
|
||||
final status = xpath(res, '//*[@class="alternati"]/span[2]/text()');
|
||||
if (status.isNotEmpty) {
|
||||
anime.status = parseStatus(status.first, statusList);
|
||||
}
|
||||
anime.description = xpath(res, '//*[@class="desc"]/div/text()').first;
|
||||
|
||||
anime.genre = xpath(res, '//*[@class="genre-info"]/a/text()');
|
||||
final epUrls =
|
||||
xpath(res, '//div[@class="epsleft")]/span[@class="lchx"]/a/@href');
|
||||
final names =
|
||||
xpath(res, '//div[@class="epsleft")]/span[@class="lchx"]/a/text()');
|
||||
final dates =
|
||||
xpath(res, '//div[@class="epsleft")]/span[@class="date"]/text()');
|
||||
final dateUploads = parseDates(dates, "dd/MM/yyyy", "id");
|
||||
List<MChapter>? episodesList = [];
|
||||
for (var i = 0; i < epUrls.length; i++) {
|
||||
MChapter episode = MChapter();
|
||||
episode.name = names[i];
|
||||
episode.dateUpload = dateUploads[i];
|
||||
episode.url = epUrls[i];
|
||||
episodesList.add(episode);
|
||||
}
|
||||
|
||||
anime.chapters = episodesList;
|
||||
return anime;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<MVideo>> getVideoList(String url) async {
|
||||
final res = (await client.get(Uri.parse(url))).body;
|
||||
final dataPost = xpath(res,
|
||||
'//*[@id="server"]/ul/li/div[contains(@id,"player-option")]/@data-post')
|
||||
.first;
|
||||
final dataNume = xpath(res,
|
||||
'//*[@id="server"]/ul/li/div[contains(@id,"player-option")]/@data-nume')
|
||||
.first;
|
||||
final dataType = xpath(res,
|
||||
'//*[@id="server"]/ul/li/div[contains(@id,"player-option")]/@data-type')
|
||||
.first;
|
||||
|
||||
final ress = (await client.post(
|
||||
Uri.parse("${source.baseUrl}/wp-admin/admin-ajax.php"),
|
||||
headers: {
|
||||
"_": "_"
|
||||
},
|
||||
body: {
|
||||
"action": "player_ajax",
|
||||
"post": dataPost,
|
||||
"nume": dataNume,
|
||||
"type": dataType
|
||||
}))
|
||||
.body;
|
||||
|
||||
final playerLink =
|
||||
xpath(ress, '//iframe[@class="playeriframe"]/@src').first;
|
||||
final resPlayer = (await client.get(Uri.parse(playerLink))).body;
|
||||
var resJson = substringBefore(substringAfter(resPlayer, "= "), "<");
|
||||
var streams = json.decode(resJson)["streams"] as List<Map<String, dynamic>>;
|
||||
List<MVideo> videos = [];
|
||||
for (var stream in streams) {
|
||||
String videoUrl = stream["play_url"];
|
||||
final quality = getQuality(stream["format_id"]);
|
||||
|
||||
MVideo video = MVideo();
|
||||
video
|
||||
..url = videoUrl
|
||||
..originalUrl = videoUrl
|
||||
..quality = quality;
|
||||
videos.add(video);
|
||||
}
|
||||
|
||||
return videos;
|
||||
}
|
||||
|
||||
String getQuality(int formatId) {
|
||||
if (formatId == 18) {
|
||||
return "Google - 360p";
|
||||
} else if (formatId == 22) {
|
||||
return "Google - 720p";
|
||||
}
|
||||
return "Unknown Resolution";
|
||||
}
|
||||
|
||||
MPages parseAnimeList(String res) {
|
||||
List<MManga> animeList = [];
|
||||
final urls = xpath(res, '//div[@class="relat"]/article/div/div/a/@href');
|
||||
final names = xpath(res, '//div[@class="relat"]/article/div/div/a/@title');
|
||||
final images =
|
||||
xpath(res, '//div[@class="relat"]/article/div/div/a/div/img/@src');
|
||||
|
||||
for (var i = 0; i < names.length; i++) {
|
||||
MManga anime = MManga();
|
||||
anime.name = names[i];
|
||||
anime.imageUrl = images[i];
|
||||
anime.link = urls[i];
|
||||
animeList.add(anime);
|
||||
}
|
||||
final pages = xpath(res, '//div[@class="pagination"]/a/@href');
|
||||
final pageNumberCurrent = xpath(res,
|
||||
'//div[@class="pagination"]/span[@class="page-numbers current"]/text()');
|
||||
|
||||
bool hasNextPage = true;
|
||||
if (pageNumberCurrent.isNotEmpty && pages.isNotEmpty) {
|
||||
hasNextPage = !(pages.length == int.parse(pageNumberCurrent.first));
|
||||
}
|
||||
return MPages(animeList, hasNextPage);
|
||||
}
|
||||
}
|
||||
|
||||
OploVerz main(MSource source) {
|
||||
return OploVerz(source: source);
|
||||
}
|
||||
16
dart/anime/src/id/oploverz/source.dart
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
import '../../../../../model/source.dart';
|
||||
|
||||
Source get oploverz => _oploverz;
|
||||
const _oploverzVersion = "0.0.5";
|
||||
const _oploverzCodeUrl =
|
||||
"https://raw.githubusercontent.com/kodjodevf/mangayomi-extensions/$branchName/dart/anime/src/id/oploverz/oploverz.dart";
|
||||
Source _oploverz = Source(
|
||||
name: "Oploverz",
|
||||
baseUrl: "https://oploverz.gold",
|
||||
lang: "id",
|
||||
typeSource: "single",
|
||||
iconUrl:
|
||||
"https://raw.githubusercontent.com/kodjodevf/mangayomi-extensions/$branchName/dart/anime/src/id/oploverz/icon.png",
|
||||
sourceCodeUrl: _oploverzCodeUrl,
|
||||
version: _oploverzVersion,
|
||||
itemType: ItemType.anime);
|
||||
BIN
dart/anime/src/id/otakudesu/icon.png
Normal file
|
After Width: | Height: | Size: 6.3 KiB |
241
dart/anime/src/id/otakudesu/otakudesu.dart
Normal file
|
|
@ -0,0 +1,241 @@
|
|||
import 'package:mangayomi/bridge_lib.dart';
|
||||
import 'dart:convert';
|
||||
|
||||
class OtakuDesu extends MProvider {
|
||||
OtakuDesu({required this.source});
|
||||
|
||||
MSource source;
|
||||
|
||||
final Client client = Client(source);
|
||||
|
||||
@override
|
||||
String get baseUrl => getPreferenceValue(source.id, "overrideBaseUrl");
|
||||
|
||||
@override
|
||||
Future<MPages> getPopular(int page) async {
|
||||
final res =
|
||||
(await client.get(Uri.parse("$baseUrl/complete-anime/page/$page")))
|
||||
.body;
|
||||
return parseAnimeList(res);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<MPages> getLatestUpdates(int page) async {
|
||||
final res =
|
||||
(await client.get(Uri.parse("$baseUrl/ongoing-anime/page/$page"))).body;
|
||||
return parseAnimeList(res);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<MPages> search(String query, int page, FilterList filterList) async {
|
||||
final res =
|
||||
(await client.get(Uri.parse("$baseUrl/?s=$query&post_type=anime")))
|
||||
.body;
|
||||
List<MManga> animeList = [];
|
||||
final images = xpath(res, '//ul[@class="chivsrc"]/li/img/@src');
|
||||
final names = xpath(res, '//ul[@class="chivsrc"]/li/h2/a/text()');
|
||||
final urls = xpath(res, '//ul[@class="chivsrc"]/li/h2/a/@href');
|
||||
|
||||
for (var i = 0; i < names.length; i++) {
|
||||
MManga anime = MManga();
|
||||
anime.name = names[i];
|
||||
anime.imageUrl = images[i];
|
||||
anime.link = urls[i];
|
||||
animeList.add(anime);
|
||||
}
|
||||
return MPages(animeList, false);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<MManga> getDetail(String url) async {
|
||||
final statusList = [
|
||||
{"Ongoing": 0, "Completed": 1}
|
||||
];
|
||||
final res = (await client.get(Uri.parse(url))).body;
|
||||
MManga anime = MManga();
|
||||
final status = xpath(
|
||||
res, '//*[@class="infozingle"]/p[contains(text(), "Status")]/text()');
|
||||
if (status.isNotEmpty) {
|
||||
anime.status = parseStatus(status.first.split(':').last, statusList);
|
||||
}
|
||||
final description = xpath(res, '//*[@class="sinopc"]/text()');
|
||||
if (description.isNotEmpty) {
|
||||
anime.description = description.first;
|
||||
}
|
||||
|
||||
final genre = xpath(
|
||||
res, '//*[@class="infozingle"]/p[contains(text(), "Genre")]/text()');
|
||||
if (genre.isNotEmpty) {
|
||||
anime.genre = genre.first.split(':').last.split(',');
|
||||
}
|
||||
|
||||
final epUrls = xpath(res, '//div[@class="episodelist"]/ul/li/span/a/@href');
|
||||
final names = xpath(res, '//div[@class="episodelist"]/ul/li/span/a/text()');
|
||||
|
||||
final dates = xpath(
|
||||
res, '//div[@class="episodelist"]/ul/li/span[@class="zeebr"]/text()');
|
||||
final dateUploads = parseDates(dates, "d MMMM,yyyy", "id");
|
||||
List<MChapter>? episodesList = [];
|
||||
for (var i = 1; i < epUrls.length; i++) {
|
||||
MChapter episode = MChapter();
|
||||
episode.name = names[i];
|
||||
episode.dateUpload = dateUploads[i];
|
||||
episode.url = epUrls[i];
|
||||
episodesList.add(episode);
|
||||
}
|
||||
anime.chapters = episodesList;
|
||||
return anime;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<MVideo>> getVideoList(String url) async {
|
||||
List<MVideo> videos = [];
|
||||
final res = (await client.get(Uri.parse(url))).body;
|
||||
final script =
|
||||
xpath(res, '//script[contains(text(), "{action:")]/text()').first;
|
||||
final nonceAction =
|
||||
substringBefore(substringAfter(script, "{action:\""), '"');
|
||||
final action = substringBefore(substringAfter(script, "action:\""), '"');
|
||||
|
||||
final resNonceAction = (await client.post(
|
||||
Uri.parse("$baseUrl/wp-admin/admin-ajax.php"),
|
||||
headers: {"_": "_"},
|
||||
body: {"action": nonceAction}))
|
||||
.body;
|
||||
final nonce = substringBefore(substringAfter(resNonceAction, ":\""), '"');
|
||||
final mirrorstream =
|
||||
xpath(res, '//*[@class="mirrorstream"]/ul/li/a/@data-content');
|
||||
for (var stream in mirrorstream) {
|
||||
List<MVideo> a = [];
|
||||
final decodedData = json.decode(utf8.decode(base64Url.decode(stream)));
|
||||
final q = decodedData["q"];
|
||||
final id = decodedData["id"];
|
||||
final i = decodedData["i"];
|
||||
|
||||
final res = (await client
|
||||
.post(Uri.parse("$baseUrl/wp-admin/admin-ajax.php"), headers: {
|
||||
"_": "_"
|
||||
}, body: {
|
||||
"i": i,
|
||||
"id": id,
|
||||
"q": q,
|
||||
"nonce": nonce,
|
||||
"action": action
|
||||
}))
|
||||
.body;
|
||||
final html = utf8.decode(
|
||||
base64Url.decode(substringBefore(substringAfter(res, ":\""), '"')));
|
||||
String url = xpath(html, '//iframe/@src').first;
|
||||
if (url.contains("yourupload")) {
|
||||
final id = substringBefore(substringAfter(url, "id="), "&");
|
||||
url = "https://yourupload.com/embed/$id";
|
||||
a = await yourUploadExtractor(url, null, "YourUpload - $q", null);
|
||||
} else if (url.contains("filelions")) {
|
||||
a = await streamWishExtractor(url, "FileLions");
|
||||
} else if (url.contains("desustream")) {
|
||||
final response = (await Client().get(Uri.parse(url)));
|
||||
final res = response.body;
|
||||
final script =
|
||||
xpath(res, '//script[contains(text(), "sources")]/text()').first;
|
||||
final videoUrl = substringBefore(
|
||||
substringAfter(substringAfter(script, "sources:[{"), "file':'"),
|
||||
"'");
|
||||
if (videoUrl.endsWith(".mp4")) {
|
||||
MVideo video = MVideo();
|
||||
video
|
||||
..url = videoUrl
|
||||
..originalUrl = videoUrl
|
||||
..quality = "DesuStream - $q";
|
||||
videos.add(video);
|
||||
}
|
||||
} else if (url.contains("mp4upload")) {
|
||||
final res = (await client.get(Uri.parse(url))).body;
|
||||
final script =
|
||||
xpath(res, '//script[contains(text(), "player.src")]/text()').first;
|
||||
final videoUrl =
|
||||
substringBefore(substringAfter(script, "src: \""), '"');
|
||||
MVideo video = MVideo();
|
||||
video
|
||||
..url = videoUrl
|
||||
..originalUrl = videoUrl
|
||||
..quality = "Mp4upload - $q";
|
||||
videos.add(video);
|
||||
}
|
||||
videos.addAll(a);
|
||||
}
|
||||
|
||||
return sortVideos(videos);
|
||||
}
|
||||
|
||||
List<MVideo> sortVideos(List<MVideo> videos) {
|
||||
String quality = getPreferenceValue(source.id, "preferred_quality");
|
||||
|
||||
videos.sort((MVideo a, MVideo b) {
|
||||
int qualityMatchA = 0;
|
||||
if (a.quality.contains(quality)) {
|
||||
qualityMatchA = 1;
|
||||
}
|
||||
int qualityMatchB = 0;
|
||||
if (b.quality.contains(quality)) {
|
||||
qualityMatchB = 1;
|
||||
}
|
||||
if (qualityMatchA != qualityMatchB) {
|
||||
return qualityMatchB - qualityMatchA;
|
||||
}
|
||||
|
||||
final regex = RegExp(r'(\d+)p');
|
||||
final matchA = regex.firstMatch(a.quality);
|
||||
final matchB = regex.firstMatch(b.quality);
|
||||
final int qualityNumA = int.tryParse(matchA?.group(1) ?? '0') ?? 0;
|
||||
final int qualityNumB = int.tryParse(matchB?.group(1) ?? '0') ?? 0;
|
||||
return qualityNumB - qualityNumA;
|
||||
});
|
||||
|
||||
return videos;
|
||||
}
|
||||
|
||||
MPages parseAnimeList(String res) {
|
||||
List<MManga> animeList = [];
|
||||
final urls =
|
||||
xpath(res, '//div[@class="detpost"]/div[@class="thumb"]/a/@href');
|
||||
final names = xpath(res,
|
||||
'//div[@class="detpost"]/div[@class="thumb"]/a/div[@class="thumbz"]/h2/text()');
|
||||
final images = xpath(res,
|
||||
'//div[@class="detpost"]/div[@class="thumb"]/a/div[@class="thumbz"]/img/@src');
|
||||
|
||||
for (var i = 0; i < names.length; i++) {
|
||||
MManga anime = MManga();
|
||||
anime.name = names[i];
|
||||
anime.imageUrl = images[i];
|
||||
anime.link = urls[i];
|
||||
animeList.add(anime);
|
||||
}
|
||||
final pages = xpath(
|
||||
res, '//div[@class="pagenavix"]/a[@class="next page-numbers"]/@href');
|
||||
return MPages(animeList, pages.isNotEmpty);
|
||||
}
|
||||
|
||||
List<dynamic> getSourcePreferences() {
|
||||
return [
|
||||
EditTextPreference(
|
||||
key: "overrideBaseUrl",
|
||||
title: "Override BaseUrl",
|
||||
summary: "",
|
||||
value: "https://otakudesu.cloud",
|
||||
dialogTitle: "Override BaseUrl",
|
||||
dialogMessage: "",
|
||||
text: "https://otakudesu.cloud"),
|
||||
ListPreference(
|
||||
key: "preferred_quality",
|
||||
title: "Preferred quality",
|
||||
summary: "",
|
||||
valueIndex: 1,
|
||||
entries: ["1080p", "720p", "480p", "360p"],
|
||||
entryValues: ["1080", "720", "480", "360"])
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
OtakuDesu main(MSource source) {
|
||||
return OtakuDesu(source: source);
|
||||
}
|
||||
16
dart/anime/src/id/otakudesu/source.dart
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
import '../../../../../model/source.dart';
|
||||
|
||||
Source get otakudesu => _otakudesu;
|
||||
const _otakudesuVersion = "0.0.55";
|
||||
const _otakudesuCodeUrl =
|
||||
"https://raw.githubusercontent.com/kodjodevf/mangayomi-extensions/$branchName/dart/anime/src/id/otakudesu/otakudesu.dart";
|
||||
Source _otakudesu = Source(
|
||||
name: "OtakuDesu",
|
||||
baseUrl: "https://otakudesu.cloud",
|
||||
lang: "id",
|
||||
typeSource: "single",
|
||||
iconUrl:
|
||||
"https://raw.githubusercontent.com/kodjodevf/mangayomi-extensions/$branchName/dart/anime/src/id/otakudesu/icon.png",
|
||||
sourceCodeUrl: _otakudesuCodeUrl,
|
||||
version: _otakudesuVersion,
|
||||
itemType: ItemType.anime);
|
||||
362
dart/anime/src/it/animesaturn/animesaturn.dart
Normal file
|
|
@ -0,0 +1,362 @@
|
|||
import 'package:mangayomi/bridge_lib.dart';
|
||||
import 'dart:convert';
|
||||
|
||||
class AnimeSaturn extends MProvider {
|
||||
AnimeSaturn({required this.source});
|
||||
|
||||
MSource source;
|
||||
|
||||
final Client client = Client(source);
|
||||
|
||||
@override
|
||||
Future<MPages> getPopular(int page) async {
|
||||
final res = (await client
|
||||
.get(Uri.parse("${source.baseUrl}/animeincorso?page=$page")))
|
||||
.body;
|
||||
|
||||
List<MManga> animeList = [];
|
||||
|
||||
final urls = xpath(res,
|
||||
'//*[@class="sebox"]/div[@class="msebox"]/div[@class="headsebox"]/div[@class="tisebox"]/h2/a/@href');
|
||||
|
||||
final names = xpath(res,
|
||||
'//*[@class="sebox"]/div[@class="msebox"]/div[@class="headsebox"]/div[@class="tisebox"]/h2/a/text()');
|
||||
|
||||
final images = xpath(res,
|
||||
'//*[@class="sebox"]/div[@class="msebox"]/div[@class="bigsebox"]/div/img[@class="attachment-post-thumbnail size-post-thumbnail wp-post-image"]/@src');
|
||||
|
||||
for (var i = 0; i < names.length; i++) {
|
||||
MManga anime = MManga();
|
||||
anime.name = formatTitle(names[i]);
|
||||
anime.imageUrl = images[i];
|
||||
anime.link = urls[i];
|
||||
animeList.add(anime);
|
||||
}
|
||||
return MPages(animeList, true);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<MPages> getLatestUpdates(int page) async {
|
||||
final res =
|
||||
(await client.get(Uri.parse("${source.baseUrl}/newest?page=$page")))
|
||||
.body;
|
||||
|
||||
List<MManga> animeList = [];
|
||||
|
||||
final urls = xpath(res, '//*[@class="card mb-4 shadow-sm"]/a/@href');
|
||||
|
||||
final names = xpath(res, '//*[@class="card mb-4 shadow-sm"]/a/@title');
|
||||
|
||||
final images = xpath(res,
|
||||
'//*[@class="card mb-4 shadow-sm"]/a/img[@class="new-anime"]/@src');
|
||||
|
||||
for (var i = 0; i < names.length; i++) {
|
||||
MManga anime = MManga();
|
||||
anime.name = formatTitle(names[i]);
|
||||
anime.imageUrl = images[i];
|
||||
anime.link = urls[i];
|
||||
animeList.add(anime);
|
||||
}
|
||||
return MPages(animeList, true);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<MPages> search(String query, int page, FilterList filterList) async {
|
||||
final filters = filterList.filters;
|
||||
String url = "";
|
||||
|
||||
if (query.isNotEmpty) {
|
||||
url = "${source.baseUrl}/animelist?search=$query";
|
||||
} else {
|
||||
url = "${source.baseUrl}/filter?";
|
||||
int variantgenre = 0;
|
||||
int variantstate = 0;
|
||||
int variantyear = 0;
|
||||
for (var filter in filters) {
|
||||
if (filter.type == "GenreFilter") {
|
||||
final genre = (filter.state as List).where((e) => e.state).toList();
|
||||
if (genre.isNotEmpty) {
|
||||
for (var st in genre) {
|
||||
url += "&categories%5B${variantgenre}%5D=${st.value}";
|
||||
variantgenre++;
|
||||
}
|
||||
}
|
||||
} else if (filter.type == "YearList") {
|
||||
final years = (filter.state as List).where((e) => e.state).toList();
|
||||
if (years.isNotEmpty) {
|
||||
for (var st in years) {
|
||||
url += "&years%5B${variantyear}%5D=${st.value}";
|
||||
variantyear++;
|
||||
}
|
||||
}
|
||||
} else if (filter.type == "StateList") {
|
||||
final states = (filter.state as List).where((e) => e.state).toList();
|
||||
if (states.isNotEmpty) {
|
||||
for (var st in states) {
|
||||
url += "&states%5B${variantstate}%5D=${st.value}";
|
||||
variantstate++;
|
||||
}
|
||||
}
|
||||
} else if (filter.type == "LangList") {
|
||||
final lang = filter.values[filter.state].value;
|
||||
if (lang.isNotEmpty) {
|
||||
url += "&language%5B0%5D=$lang";
|
||||
}
|
||||
}
|
||||
}
|
||||
url += "&page=$page";
|
||||
}
|
||||
|
||||
final res = (await client.get(Uri.parse(url))).body;
|
||||
|
||||
List<MManga> animeList = [];
|
||||
List<String> urls = [];
|
||||
List<String> names = [];
|
||||
List<String> images = [];
|
||||
if (query.isNotEmpty) {
|
||||
urls = xpath(res,
|
||||
'//*[@class="list-group"]/li[@class="list-group-item bg-dark-as-box-shadow"]/div[@class="item-archivio"]/div[@class="info-archivio"]/h3/a[@class="badge badge-archivio badge-light"]/@href');
|
||||
|
||||
names = xpath(res,
|
||||
'//*[@class="list-group"]/li[@class="list-group-item bg-dark-as-box-shadow"]/div[@class="item-archivio"]/div[@class="info-archivio"]/h3/a[@class="badge badge-archivio badge-light"]/text()');
|
||||
|
||||
images = xpath(res,
|
||||
'//*[@class="list-group"]/li[@class="list-group-item bg-dark-as-box-shadow"]/div[@class="item-archivio"]/a/img/@src');
|
||||
} else {
|
||||
urls = xpath(res, '//*[@class="card mb-4 shadow-sm"]/a/@href');
|
||||
|
||||
names = xpath(res, '//*[@class="card mb-4 shadow-sm"]/a/text()');
|
||||
|
||||
images = xpath(res,
|
||||
'//*[@class="card mb-4 shadow-sm"]/a/img[@class="new-anime"]/@src');
|
||||
}
|
||||
|
||||
for (var i = 0; i < names.length; i++) {
|
||||
MManga anime = MManga();
|
||||
anime.name = formatTitle(names[i]);
|
||||
anime.imageUrl = images[i];
|
||||
anime.link = urls[i];
|
||||
animeList.add(anime);
|
||||
}
|
||||
return MPages(animeList, query.isEmpty);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<MManga> getDetail(String url) async {
|
||||
final statusList = [
|
||||
{"In corso": 0, "Finito": 1}
|
||||
];
|
||||
|
||||
final res = (await client.get(Uri.parse(url))).body;
|
||||
MManga anime = MManga();
|
||||
final detailsList = xpath(res,
|
||||
'//div[@class="container shadow rounded bg-dark-as-box mb-3 p-3 w-100 text-white"]/text()');
|
||||
if (detailsList.isNotEmpty) {
|
||||
final details = detailsList.first;
|
||||
|
||||
anime.status = parseStatus(
|
||||
details.substring(
|
||||
details.indexOf("Stato:") + 6, details.indexOf("Data di uscita:")),
|
||||
statusList);
|
||||
anime.author = details.substring(7, details.indexOf("Stato:"));
|
||||
}
|
||||
|
||||
final description = xpath(res, '//*[@id="shown-trama"]/text()');
|
||||
final descriptionFull = xpath(res, '//*[@id="full-trama"]/text()');
|
||||
if (description.isNotEmpty) {
|
||||
anime.description = description.first;
|
||||
} else {
|
||||
anime.description = "";
|
||||
}
|
||||
if (descriptionFull.isNotEmpty) {
|
||||
if (descriptionFull.first.length > anime.description.length) {
|
||||
anime.description = descriptionFull.first;
|
||||
}
|
||||
}
|
||||
|
||||
anime.genre = xpath(res,
|
||||
'//*[@class="container shadow rounded bg-dark-as-box mb-3 p-3 w-100"]/a/text()');
|
||||
|
||||
final epUrls = xpath(res,
|
||||
'//*[@class="btn-group episodes-button episodi-link-button"]/a/@href');
|
||||
|
||||
final titles = xpath(res,
|
||||
'//*[@class="btn-group episodes-button episodi-link-button"]/a/text()');
|
||||
|
||||
List<MChapter>? episodesList = [];
|
||||
for (var i = 0; i < epUrls.length; i++) {
|
||||
MChapter episode = MChapter();
|
||||
episode.name = titles[i];
|
||||
episode.url = epUrls[i];
|
||||
episodesList.add(episode);
|
||||
}
|
||||
|
||||
anime.chapters = episodesList.reversed.toList();
|
||||
return anime;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<MVideo>> getVideoList(String url) async {
|
||||
final res = (await client.get(Uri.parse(url))).body;
|
||||
|
||||
final urlVid = xpath(res, '//a[contains(@href,"/watch")]/@href').first;
|
||||
final resVid = (await client.get(Uri.parse(urlVid))).body;
|
||||
String masterUrl = "";
|
||||
if (resVid.contains("jwplayer(")) {
|
||||
masterUrl = substringBefore(substringAfter(resVid, "file: \""), "\"");
|
||||
} else {
|
||||
masterUrl = parseHtml(resVid).selectFirst("source").attr("src");
|
||||
}
|
||||
|
||||
List<MVideo> videos = [];
|
||||
if (masterUrl.endsWith("playlist.m3u8")) {
|
||||
final masterPlaylistRes = (await client.get(Uri.parse(masterUrl))).body;
|
||||
for (var it in substringAfter(masterPlaylistRes, "#EXT-X-STREAM-INF:")
|
||||
.split("#EXT-X-STREAM-INF:")) {
|
||||
final quality =
|
||||
"${substringBefore(substringBefore(substringAfter(substringAfter(it, "RESOLUTION="), "x"), ","), "\n")}p";
|
||||
|
||||
String videoUrl = substringBefore(substringAfter(it, "\n"), "\n");
|
||||
|
||||
if (!videoUrl.startsWith("http")) {
|
||||
videoUrl =
|
||||
"${masterUrl.split("/").sublist(0, masterUrl.split("/").length - 1).join("/")}/$videoUrl";
|
||||
}
|
||||
|
||||
MVideo video = MVideo();
|
||||
video
|
||||
..url = videoUrl
|
||||
..originalUrl = videoUrl
|
||||
..quality = quality;
|
||||
videos.add(video);
|
||||
}
|
||||
} else {
|
||||
MVideo video = MVideo();
|
||||
video
|
||||
..url = masterUrl
|
||||
..originalUrl = masterUrl
|
||||
..quality = "Qualità predefinita";
|
||||
videos.add(video);
|
||||
}
|
||||
return sortVideos(videos, source.id);
|
||||
}
|
||||
|
||||
String formatTitle(String titlestring) {
|
||||
return titlestring
|
||||
.replaceAll("(ITA) ITA", "Dub ITA")
|
||||
.replaceAll("(ITA)", "Dub ITA")
|
||||
.replaceAll("Sub ITA", "");
|
||||
}
|
||||
|
||||
@override
|
||||
List<dynamic> getFilterList() {
|
||||
return [
|
||||
HeaderFilter("Ricerca per titolo ignora i filtri e viceversa"),
|
||||
GroupFilter("GenreFilter", "Generi", [
|
||||
CheckBoxFilter("Arti Marziali", "Arti Marziali"),
|
||||
CheckBoxFilter("Avventura", "Avventura"),
|
||||
CheckBoxFilter("Azione", "Azione"),
|
||||
CheckBoxFilter("Bambini", "Bambini"),
|
||||
CheckBoxFilter("Commedia", "Commedia"),
|
||||
CheckBoxFilter("Demenziale", "Demenziale"),
|
||||
CheckBoxFilter("Demoni", "Demoni"),
|
||||
CheckBoxFilter("Drammatico", "Drammatico"),
|
||||
CheckBoxFilter("Ecchi", "Ecchi"),
|
||||
CheckBoxFilter("Fantasy", "Fantasy"),
|
||||
CheckBoxFilter("Gioco", "Gioco"),
|
||||
CheckBoxFilter("Harem", "Harem"),
|
||||
CheckBoxFilter("Hentai", "Hentai"),
|
||||
CheckBoxFilter("Horror", "Horror"),
|
||||
CheckBoxFilter("Josei", "Josei"),
|
||||
CheckBoxFilter("Magia", "Magia"),
|
||||
CheckBoxFilter("Mecha", "Mecha"),
|
||||
CheckBoxFilter("Militari", "Militari"),
|
||||
CheckBoxFilter("Mistero", "Mistero"),
|
||||
CheckBoxFilter("Musicale", "Musicale"),
|
||||
CheckBoxFilter("Parodia", "Parodia"),
|
||||
CheckBoxFilter("Polizia", "Polizia"),
|
||||
CheckBoxFilter("Psicologico", "Psicologico"),
|
||||
CheckBoxFilter("Romantico", "Romantico"),
|
||||
CheckBoxFilter("Samurai", "Samurai"),
|
||||
CheckBoxFilter("Sci-Fi", "Sci-Fi"),
|
||||
CheckBoxFilter("Scolastico", "Scolastico"),
|
||||
CheckBoxFilter("Seinen", "Seinen"),
|
||||
CheckBoxFilter("Sentimentale", "Sentimentale"),
|
||||
CheckBoxFilter("Shoujo Ai", "Shoujo Ai"),
|
||||
CheckBoxFilter("Shoujo", "Shoujo"),
|
||||
CheckBoxFilter("Shounen Ai", "Shounen Ai"),
|
||||
CheckBoxFilter("Shounen", "Shounen"),
|
||||
CheckBoxFilter("Slice of Life", "Slice of Life"),
|
||||
CheckBoxFilter("Soprannaturale", "Soprannaturale"),
|
||||
CheckBoxFilter("Spazio", "Spazio"),
|
||||
CheckBoxFilter("Sport", "Sport"),
|
||||
CheckBoxFilter("Storico", "Storico"),
|
||||
CheckBoxFilter("Superpoteri", "Superpoteri"),
|
||||
CheckBoxFilter("Thriller", "Thriller"),
|
||||
CheckBoxFilter("Vampiri", "Vampiri"),
|
||||
CheckBoxFilter("Veicoli", "Veicoli"),
|
||||
CheckBoxFilter("Yaoi", "Yaoi"),
|
||||
CheckBoxFilter("Yuri", "Yuri"),
|
||||
]),
|
||||
GroupFilter("YearList", "Anno di Uscita", [
|
||||
for (var i = 1969; i < 2022; i++)
|
||||
CheckBoxFilter(i.toString(), i.toString()),
|
||||
]),
|
||||
GroupFilter("StateList", "Stato", [
|
||||
CheckBoxFilter("In corso", "0"),
|
||||
CheckBoxFilter("Finito", "1"),
|
||||
CheckBoxFilter("Non rilasciato", "2"),
|
||||
CheckBoxFilter("Droppato", "3"),
|
||||
]),
|
||||
SelectFilter("LangList", "Lingua", 0, [
|
||||
SelectFilterOption("", ""),
|
||||
SelectFilterOption("Subbato", "0"),
|
||||
SelectFilterOption("Doppiato", "1"),
|
||||
]),
|
||||
];
|
||||
}
|
||||
|
||||
@override
|
||||
List<dynamic> getSourcePreferences() {
|
||||
return [
|
||||
ListPreference(
|
||||
key: "preferred_quality",
|
||||
title: "Qualità preferita",
|
||||
summary: "",
|
||||
valueIndex: 0,
|
||||
entries: ["1080p", "720p", "480p", "360p", "240p", "144p"],
|
||||
entryValues: ["1080", "720", "480", "360", "240", "144"]),
|
||||
];
|
||||
}
|
||||
|
||||
List<MVideo> sortVideos(List<MVideo> videos, int sourceId) {
|
||||
String quality = getPreferenceValue(sourceId, "preferred_quality");
|
||||
|
||||
videos.sort((MVideo a, MVideo b) {
|
||||
int qualityMatchA = 0;
|
||||
if (a.quality.contains(quality)) {
|
||||
qualityMatchA = 1;
|
||||
}
|
||||
int qualityMatchB = 0;
|
||||
if (b.quality.contains(quality)) {
|
||||
qualityMatchB = 1;
|
||||
}
|
||||
if (qualityMatchA != qualityMatchB) {
|
||||
return qualityMatchB - qualityMatchA;
|
||||
}
|
||||
|
||||
final regex = RegExp(r'(\d+)p');
|
||||
final matchA = regex.firstMatch(a.quality);
|
||||
final matchB = regex.firstMatch(b.quality);
|
||||
final int qualityNumA = int.tryParse(matchA?.group(1) ?? '0') ?? 0;
|
||||
final int qualityNumB = int.tryParse(matchB?.group(1) ?? '0') ?? 0;
|
||||
return qualityNumB - qualityNumA;
|
||||
});
|
||||
|
||||
return videos;
|
||||
}
|
||||
}
|
||||
|
||||
AnimeSaturn main(MSource source) {
|
||||
return AnimeSaturn(source: source);
|
||||
}
|
||||