Compare commits
1912 Commits
source/kom
...
master
| Author | SHA1 | Date |
|---|---|---|
|
|
5fc0327df7 | 6 months ago |
|
|
df1cab3f9d | 6 months ago |
|
|
8effefcd50 | 6 months ago |
|
|
7f98a7fb5c | 6 months ago |
|
|
a39721eaf7 | 6 months ago |
|
|
65c61c0c01 | 7 months ago |
|
|
ac977a6cbb | 7 months ago |
|
|
8908031eee | 7 months ago |
|
|
4141a8f429 | 7 months ago |
|
|
ad7f1eddca | 7 months ago |
|
|
472ddab27c | 7 months ago |
|
|
560cf63a5e | 7 months ago |
|
|
d4e1acd515 | 7 months ago |
|
|
f66b97edc1 | 7 months ago |
|
|
a084909507 | 7 months ago |
|
|
58357a3745 | 7 months ago |
|
|
8a6a529023 | 7 months ago |
|
|
1b9ff47ab3 | 7 months ago |
|
|
229f321299 | 7 months ago |
|
|
a5233ede48 | 7 months ago |
|
|
d9025dbf81 | 7 months ago |
|
|
2cc0af17d5 | 7 months ago |
|
|
a3f384e3b3 | 7 months ago |
|
|
5882a5d54d | 7 months ago |
|
|
f62809407b | 7 months ago |
|
|
14c0eaf273 | 7 months ago |
|
|
0817a82f36 | 7 months ago |
|
|
727e326379 | 7 months ago |
|
|
ccaf52b74e | 7 months ago |
|
|
fc988cdd1e | 7 months ago |
|
|
cb2e9841ae | 7 months ago |
|
|
775063d36f | 7 months ago |
|
|
86b8642456 | 7 months ago |
|
|
185d9b6e6e | 7 months ago |
|
|
e3495fc41f | 7 months ago |
|
|
d8e47b2b52 | 7 months ago |
|
|
be64bf2328 | 7 months ago |
|
|
d85bb64b1c | 7 months ago |
|
|
de2289eb75 | 7 months ago |
|
|
7cb069d108 | 7 months ago |
|
|
8a18df148d | 7 months ago |
|
|
9007852d31 | 7 months ago |
|
|
60e451303e | 7 months ago |
|
|
170a11ae26 | 7 months ago |
|
|
642ac08338 | 7 months ago |
|
|
ff256cb3ec | 7 months ago |
|
|
08d46988a3 | 7 months ago |
|
|
a3abc81dfc | 7 months ago |
|
|
41a4c90e75 | 7 months ago |
|
|
23fdfbbef9 | 8 months ago |
|
|
a4f41e41f8 | 8 months ago |
|
|
db586045d7 | 8 months ago |
|
|
e636eda840 | 8 months ago |
|
|
406992a6c9 | 8 months ago |
|
|
30f97c5c82 | 8 months ago |
|
|
b9ecdb2db6 | 8 months ago |
|
|
e044463f91 | 8 months ago |
|
|
a93c760133 | 8 months ago |
|
|
f2b6cab251 | 8 months ago |
|
|
07225568ee | 8 months ago |
|
|
860dd32a53 | 8 months ago |
|
|
74f5891ffe | 8 months ago |
|
|
7bae2602c7 | 8 months ago |
|
|
4c697b6406 | 8 months ago |
|
|
b61698d405 | 8 months ago |
|
|
260e5acd5c | 8 months ago |
|
|
e599f431c5 | 8 months ago |
|
|
0ec4fe42c1 | 8 months ago |
|
|
c9e4c7273e | 8 months ago |
|
|
3e2515ac6a | 8 months ago |
|
|
fe5534b006 | 8 months ago |
|
|
6ca07aeff7 | 8 months ago |
|
|
19567f9642 | 8 months ago |
|
|
4a854c7a23 | 8 months ago |
|
|
312b617b95 | 8 months ago |
|
|
0a0a869e88 | 8 months ago |
|
|
8573921243 | 8 months ago |
|
|
b8d3124227 | 8 months ago |
|
|
14e23694fd | 8 months ago |
|
|
7e25002dba | 8 months ago |
|
|
a94d9bf8b8 | 8 months ago |
|
|
7430c7386e | 8 months ago |
|
|
5f99e694c8 | 8 months ago |
|
|
25b23854d8 | 8 months ago |
|
|
45d39fe94d | 8 months ago |
|
|
21b63c1d77 | 8 months ago |
|
|
f64ce0f4a2 | 8 months ago |
|
|
b3e7d8e8d5 | 8 months ago |
|
|
8a147dbdd3 | 8 months ago |
|
|
5a039acf82 | 8 months ago |
|
|
e9469ae0e0 | 8 months ago |
|
|
48cfc3347b | 8 months ago |
|
|
1566b807b7 | 8 months ago |
|
|
8bed45dd2f | 8 months ago |
|
|
36c3a88d63 | 8 months ago |
|
|
16f5129b69 | 8 months ago |
|
|
674290d029 | 8 months ago |
|
|
6cb6873fcb | 8 months ago |
|
|
705a684cc2 | 8 months ago |
|
|
de76ccf753 | 8 months ago |
|
|
cbd74dd7df | 9 months ago |
|
|
7f39798a8c | 9 months ago |
|
|
31999d97a0 | 9 months ago |
|
|
fd0df2414e | 9 months ago |
|
|
a93140fc36 | 9 months ago |
|
|
7c264b6af7 | 9 months ago |
|
|
134656b835 | 9 months ago |
|
|
8964bb1584 | 9 months ago |
|
|
8bee71342b | 9 months ago |
|
|
a4824a3582 | 9 months ago |
|
|
6fb4cc608c | 9 months ago |
|
|
12aa4479cd | 9 months ago |
|
|
bfdb10d002 | 9 months ago |
|
|
f61f5329e3 | 9 months ago |
|
|
0477fe0659 | 9 months ago |
|
|
6b3329436f | 9 months ago |
|
|
35fd5042a7 | 9 months ago |
|
|
d366628a20 | 9 months ago |
|
|
5483453f35 | 9 months ago |
|
|
02ac1cb896 | 9 months ago |
|
|
1563783afe | 9 months ago |
|
|
f5436977a9 | 9 months ago |
|
|
b58cf32b7a | 9 months ago |
|
|
38025a6b35 | 9 months ago |
|
|
96a2d9e0ac | 9 months ago |
|
|
c9ebbfa1bf | 9 months ago |
|
|
91ec95448c | 9 months ago |
|
|
b5512e7574 | 9 months ago |
|
|
8bc7480d84 | 9 months ago |
|
|
db9a7161f4 | 9 months ago |
|
|
74d0951d3d | 9 months ago |
|
|
91d5eff20a | 9 months ago |
|
|
54d6b2a2bb | 9 months ago |
|
|
66cd27a673 | 9 months ago |
|
|
137490001a | 9 months ago |
|
|
c85dac5e0e | 9 months ago |
|
|
9a9102fa33 | 9 months ago |
|
|
0d31adb20b | 9 months ago |
|
|
df7f2d1a89 | 9 months ago |
|
|
eaca421132 | 9 months ago |
|
|
5f1f2df7f0 | 9 months ago |
|
|
ed21a5b397 | 9 months ago |
|
|
39703e3ebb | 9 months ago |
|
|
14cff0d651 | 9 months ago |
|
|
af89f42251 | 9 months ago |
|
|
036ce155ae | 9 months ago |
|
|
177d014b13 | 9 months ago |
|
|
7c74ff6177 | 9 months ago |
|
|
b139586df5 | 9 months ago |
|
|
15c7f97432 | 9 months ago |
|
|
f22c8f9e7c | 9 months ago |
|
|
b24baf7d93 | 9 months ago |
|
|
98e0f6cd6c | 9 months ago |
|
|
e7316b5cd0 | 9 months ago |
|
|
33a0a68ac7 | 9 months ago |
|
|
a83baf4c12 | 9 months ago |
|
|
307f2090f6 | 9 months ago |
|
|
e73d636979 | 9 months ago |
|
|
77fb7270ce | 9 months ago |
|
|
9d35f26252 | 9 months ago |
|
|
4eeb879451 | 9 months ago |
|
|
06aa701ab1 | 9 months ago |
|
|
3c55b68beb | 9 months ago |
|
|
63f37d159a | 9 months ago |
|
|
cb74c43f71 | 9 months ago |
|
|
798b7570fe | 9 months ago |
|
|
9197b82e0d | 9 months ago |
|
|
ad45480439 | 9 months ago |
|
|
9493ff1630 | 9 months ago |
|
|
8ee46ad2d1 | 9 months ago |
|
|
ba1b35522c | 9 months ago |
|
|
41c37c025a | 9 months ago |
|
|
9995657da5 | 9 months ago |
|
|
1569275cd4 | 9 months ago |
|
|
352d998680 | 9 months ago |
|
|
d0b2002a1b | 9 months ago |
|
|
5962efb7df | 9 months ago |
|
|
8b18d1f0f8 | 10 months ago |
|
|
97ca214815 | 10 months ago |
|
|
5d9b56296b | 10 months ago |
|
|
66f25c30fc | 10 months ago |
|
|
fd197de86a | 10 months ago |
|
|
5f7478b33c | 10 months ago |
|
|
b058477b67 | 10 months ago |
|
|
149d9e5a81 | 10 months ago |
|
|
6420def95a | 10 months ago |
|
|
5dae26ce0b | 10 months ago |
|
|
b5fddadb18 | 10 months ago |
|
|
613d1cd1a1 | 10 months ago |
|
|
a6a72064bb | 10 months ago |
|
|
73c0a4f606 | 10 months ago |
|
|
1d2ee20d17 | 10 months ago |
|
|
67b556c2eb | 10 months ago |
|
|
3962c87e1d | 10 months ago |
|
|
e6ca455254 | 10 months ago |
|
|
d481ff416f | 10 months ago |
|
|
b0236e492d | 10 months ago |
|
|
497321bfa8 | 10 months ago |
|
|
eff26f6303 | 10 months ago |
|
|
b5bd5b8cdf | 10 months ago |
|
|
4cc15daba3 | 10 months ago |
|
|
248d51321a | 10 months ago |
|
|
7cb9a264de | 10 months ago |
|
|
b58e9ff328 | 10 months ago |
|
|
958e0bed5c | 10 months ago |
|
|
e8d4143d72 | 10 months ago |
|
|
079b2346f1 | 10 months ago |
|
|
553615b3ff | 10 months ago |
|
|
f8992457dc | 10 months ago |
|
|
06a9209b9e | 10 months ago |
|
|
4c0b873013 | 10 months ago |
|
|
dba7f46f73 | 10 months ago |
|
|
8665186426 | 10 months ago |
|
|
b5c6a94311 | 10 months ago |
|
|
1540021f6f | 10 months ago |
|
|
0e1247476e | 10 months ago |
|
|
bf83909349 | 10 months ago |
|
|
e398a01f14 | 10 months ago |
|
|
0e77938ade | 10 months ago |
|
|
a4a9a1cf67 | 10 months ago |
|
|
3f9544fdb8 | 10 months ago |
|
|
bd30fc35a1 | 11 months ago |
|
|
b6f82cc282 | 11 months ago |
|
|
9222079e98 | 11 months ago |
|
|
49158b99d5 | 11 months ago |
|
|
3fe61a09b7 | 11 months ago |
|
|
b82ad1356e | 11 months ago |
|
|
b825d06b22 | 11 months ago |
|
|
28f5856f9e | 11 months ago |
|
|
19751674ee | 11 months ago |
|
|
a78afba15e | 11 months ago |
|
|
0e946cf84c | 11 months ago |
|
|
6cd29603ac | 11 months ago |
|
|
d94cf53624 | 11 months ago |
|
|
c81628c027 | 11 months ago |
|
|
bcde8ef2a2 | 11 months ago |
|
|
66df6caa67 | 11 months ago |
|
|
08e53b2f5f | 11 months ago |
|
|
7c83730ad0 | 11 months ago |
|
|
d2a026c054 | 11 months ago |
|
|
faf8f6ffd7 | 11 months ago |
|
|
6c372548db | 11 months ago |
|
|
0029697350 | 11 months ago |
|
|
bdcf27b955 | 11 months ago |
|
|
bb7ac26c0c | 11 months ago |
|
|
7b42317996 | 11 months ago |
|
|
103b0e7eb6 | 11 months ago |
|
|
fa80598e7c | 11 months ago |
|
|
3cb5367028 | 11 months ago |
|
|
9417d1f973 | 11 months ago |
|
|
173d30737b | 11 months ago |
|
|
0db45877b7 | 11 months ago |
|
|
e7fac812a7 | 11 months ago |
|
|
ab26e6b3d0 | 11 months ago |
|
|
baafc29967 | 11 months ago |
|
|
b0bd3e99a4 | 11 months ago |
|
|
6bf377a654 | 11 months ago |
|
|
3d78b9003d | 11 months ago |
|
|
f57722ec54 | 11 months ago |
|
|
9558a34b00 | 11 months ago |
|
|
522e3af41d | 11 months ago |
|
|
7b1a0b8d0d | 11 months ago |
|
|
5856681753 | 11 months ago |
|
|
f066be47fe | 11 months ago |
|
|
884142a15e | 11 months ago |
|
|
d7267b20fd | 11 months ago |
|
|
0537cdd533 | 11 months ago |
|
|
c5f939df70 | 11 months ago |
|
|
6854164e5a | 11 months ago |
|
|
1e942c650b | 11 months ago |
|
|
30a06b67d8 | 11 months ago |
|
|
34a1dabae4 | 11 months ago |
|
|
23227a0427 | 11 months ago |
|
|
02d1aef77c | 11 months ago |
|
|
39ebdacef9 | 11 months ago |
|
|
276349e23c | 12 months ago |
|
|
0507ed8d59 | 12 months ago |
|
|
2f8a0fe775 | 12 months ago |
|
|
aab7e9360a | 12 months ago |
|
|
9db443f7f7 | 12 months ago |
|
|
70ce9eb008 | 12 months ago |
|
|
a58947376c | 12 months ago |
|
|
cbb70f94dd | 12 months ago |
|
|
9408b9ba1b | 12 months ago |
|
|
409828d22f | 12 months ago |
|
|
c9d32a804c | 12 months ago |
|
|
a65cbf4981 | 12 months ago |
|
|
e325063260 | 12 months ago |
|
|
cf0177364c | 12 months ago |
|
|
8eaad2270b | 12 months ago |
|
|
362f9c6786 | 12 months ago |
|
|
3047949b22 | 12 months ago |
|
|
8d7c81c8ef | 12 months ago |
|
|
7d6e12d1fc | 12 months ago |
|
|
7a0f1af06a | 12 months ago |
|
|
21aaa23586 | 12 months ago |
|
|
95fcbd44fd | 12 months ago |
|
|
cc98931147 | 12 months ago |
|
|
8f61beadb2 | 12 months ago |
|
|
6d404c6997 | 12 months ago |
|
|
27060cfef0 | 12 months ago |
|
|
ca3d8f555a | 12 months ago |
|
|
7ea59a7bf1 | 12 months ago |
|
|
cea4db9619 | 12 months ago |
|
|
e7c3168099 | 12 months ago |
|
|
b165a0d611 | 1 year ago |
|
|
43e6c7c86f | 1 year ago |
|
|
346828402b | 1 year ago |
|
|
01d67eca2e | 1 year ago |
|
|
4445b666aa | 1 year ago |
|
|
adf0b4e397 | 1 year ago |
|
|
e797cabd88 | 1 year ago |
|
|
0c32b56378 | 1 year ago |
|
|
5c9c2c3a7a | 1 year ago |
|
|
7cd7180adb | 1 year ago |
|
|
4b6b585801 | 1 year ago |
|
|
1cf2153df0 | 1 year ago |
|
|
6ed104aeca | 1 year ago |
|
|
9ba1629065 | 1 year ago |
|
|
669709dfbb | 1 year ago |
|
|
b5cb310aab | 1 year ago |
|
|
50771771a8 | 1 year ago |
|
|
dce24e2005 | 1 year ago |
|
|
d3a61f6556 | 1 year ago |
|
|
8cdd7effe5 | 1 year ago |
|
|
e389b01d35 | 1 year ago |
|
|
2c9632e3e5 | 1 year ago |
|
|
2fff92b023 | 1 year ago |
|
|
c949b95dab | 1 year ago |
|
|
da145c7f41 | 1 year ago |
|
|
16b8bf9328 | 1 year ago |
|
|
f076c8095a | 1 year ago |
|
|
d2b2578a3a | 1 year ago |
|
|
b24741678c | 1 year ago |
|
|
fecb1db3be | 1 year ago |
|
|
6418422157 | 1 year ago |
|
|
7718ff28e7 | 1 year ago |
|
|
3771b8d26b | 1 year ago |
|
|
c1357b46fb | 1 year ago |
|
|
6c9f6eaae5 | 1 year ago |
|
|
309b6405f9 | 1 year ago |
|
|
9058ead462 | 1 year ago |
|
|
162436c913 | 1 year ago |
|
|
876f3a3d3a | 1 year ago |
|
|
02acc7e9d4 | 1 year ago |
|
|
e874837efb | 1 year ago |
|
|
c294f5bb61 | 1 year ago |
|
|
826c948260 | 1 year ago |
|
|
a5c70f0b51 | 1 year ago |
|
|
c2f9c18e29 | 1 year ago |
|
|
91cd239c97 | 1 year ago |
|
|
4d255d0d53 | 1 year ago |
|
|
d5a1e1a52f | 1 year ago |
|
|
9abc80880f | 1 year ago |
|
|
8d91adbd6e | 1 year ago |
|
|
20a24db949 | 1 year ago |
|
|
b837659408 | 1 year ago |
|
|
696e0bc8f6 | 1 year ago |
|
|
2ec2484982 | 1 year ago |
|
|
738e2649d6 | 1 year ago |
|
|
8e9da4e39f | 1 year ago |
|
|
da3a9a7d9b | 1 year ago |
|
|
8f9c0d93f6 | 1 year ago |
|
|
8bb0c4f4f1 | 1 year ago |
|
|
dbb04d2051 | 1 year ago |
|
|
84562d7b36 | 1 year ago |
|
|
29327dafd0 | 1 year ago |
|
|
c0d12b0c83 | 1 year ago |
|
|
a0041f3c93 | 1 year ago |
|
|
2175e91db7 | 1 year ago |
|
|
5fa7590550 | 1 year ago |
|
|
f3ae64ed1f | 1 year ago |
|
|
cc437b8cd4 | 1 year ago |
|
|
db0782c61a | 1 year ago |
|
|
bbf059ff35 | 1 year ago |
|
|
bebc615376 | 1 year ago |
|
|
77616a5404 | 1 year ago |
|
|
45584f2eea | 1 year ago |
|
|
1c3e59f9f0 | 1 year ago |
|
|
a88de5b1b6 | 1 year ago |
|
|
91977ba025 | 1 year ago |
|
|
5420290564 | 1 year ago |
|
|
98c21db8da | 1 year ago |
|
|
23953cae11 | 1 year ago |
|
|
9cd310ba51 | 1 year ago |
|
|
51311a17fb | 1 year ago |
|
|
6799d79754 | 1 year ago |
|
|
e429cdd811 | 1 year ago |
|
|
2e5ce98bac | 1 year ago |
|
|
68c3a7148f | 1 year ago |
|
|
2a1c5bc6da | 1 year ago |
|
|
6fd18543ef | 1 year ago |
|
|
ae76631aaf | 1 year ago |
|
|
ca59e8e577 | 1 year ago |
|
|
3648b36d16 | 1 year ago |
|
|
439b7862a3 | 1 year ago |
|
|
f4b975c865 | 1 year ago |
|
|
da02a82d74 | 1 year ago |
|
|
0d5498e60d | 1 year ago |
|
|
0847baf17b | 1 year ago |
|
|
adf5794ad3 | 1 year ago |
|
|
cfeea09a05 | 1 year ago |
|
|
e9bcb1abed | 1 year ago |
|
|
35cf827c90 | 1 year ago |
|
|
a10c34115b | 1 year ago |
|
|
2f4e93e391 | 1 year ago |
|
|
6ed42bc28b | 1 year ago |
|
|
dc7c859344 | 1 year ago |
|
|
a82e41aabb | 1 year ago |
|
|
3c62f4280f | 1 year ago |
|
|
f806cdd118 | 1 year ago |
|
|
224c8a6c53 | 1 year ago |
|
|
408be7fa3c | 1 year ago |
|
|
a02b001768 | 1 year ago |
|
|
5edbdf801a | 1 year ago |
|
|
cc3701250a | 1 year ago |
|
|
e94d434f92 | 1 year ago |
|
|
4d3adaa143 | 1 year ago |
|
|
36eecf781f | 1 year ago |
|
|
76f44c98be | 1 year ago |
|
|
d660d9a30f | 1 year ago |
|
|
8326ee8c29 | 1 year ago |
|
|
690a004de6 | 1 year ago |
|
|
98c6454eba | 1 year ago |
|
|
8e42ef1229 | 1 year ago |
|
|
02359a2630 | 1 year ago |
|
|
158f6f4232 | 1 year ago |
|
|
77de194b83 | 1 year ago |
|
|
85cd13c8b2 | 1 year ago |
|
|
bfbd01b1c2 | 1 year ago |
|
|
e83636edc0 | 1 year ago |
|
|
63dc67b6fd | 1 year ago |
|
|
b0de4c3b66 | 1 year ago |
|
|
d5a4cf68c6 | 1 year ago |
|
|
288d6e0b8f | 1 year ago |
|
|
aa98db9ac6 | 1 year ago |
|
|
c19e1fd5e5 | 1 year ago |
|
|
ef10e3cafc | 1 year ago |
|
|
400c423dde | 1 year ago |
|
|
85ba4fa0de | 1 year ago |
|
|
8cb913d538 | 1 year ago |
|
|
2128bbe1b6 | 1 year ago |
|
|
4df07a50fb | 1 year ago |
|
|
8fa63bca64 | 1 year ago |
|
|
e191e0c9c1 | 1 year ago |
|
|
5970509886 | 1 year ago |
|
|
b2c7d1028c | 1 year ago |
|
|
377606d1aa | 1 year ago |
|
|
688bfdaa82 | 1 year ago |
|
|
bc60629e66 | 1 year ago |
|
|
f3372751d1 | 1 year ago |
|
|
77a5216ebf | 1 year ago |
|
|
7ced91beea | 1 year ago |
|
|
41f5cee6c6 | 1 year ago |
|
|
ba5086c409 | 1 year ago |
|
|
531145c7f9 | 1 year ago |
|
|
f26fecb714 | 1 year ago |
|
|
dd7568659f | 1 year ago |
|
|
843d1f1bea | 1 year ago |
|
|
29263ff59b | 1 year ago |
|
|
3b91a3883e | 1 year ago |
|
|
ddb9b13df7 | 1 year ago |
|
|
ed578e5bff | 1 year ago |
|
|
cdbb004ca1 | 1 year ago |
|
|
f6145bc412 | 1 year ago |
|
|
86e7c21e4d | 1 year ago |
|
|
76638c6aab | 1 year ago |
|
|
3a950f401b | 1 year ago |
|
|
eeba47ea03 | 1 year ago |
|
|
762f5cbec2 | 1 year ago |
|
|
5b6c9be6d6 | 1 year ago |
|
|
f681ed270b | 1 year ago |
|
|
a0168a7d49 | 1 year ago |
|
|
47263e641e | 1 year ago |
|
|
70c78f1cab | 1 year ago |
|
|
e7cb83f7b1 | 1 year ago |
|
|
0212e1e18f | 1 year ago |
|
|
265c411df2 | 1 year ago |
|
|
69a14b6fc5 | 1 year ago |
|
|
168446e6d5 | 1 year ago |
|
|
23af28f4dd | 1 year ago |
|
|
ac80384ba1 | 1 year ago |
|
|
c4be0b58ca | 1 year ago |
|
|
023d13e44b | 1 year ago |
|
|
f4909ade09 | 1 year ago |
|
|
26fb2b9dcb | 1 year ago |
|
|
44c2ebd4a5 | 1 year ago |
|
|
47874032a3 | 1 year ago |
|
|
6dfa5ad1c2 | 1 year ago |
|
|
111ad58b57 | 1 year ago |
|
|
f1086c93f7 | 1 year ago |
|
|
5e27a59414 | 1 year ago |
|
|
cece556669 | 1 year ago |
|
|
9252da8004 | 1 year ago |
|
|
a9fdfee7ad | 1 year ago |
|
|
6904aecea3 | 1 year ago |
|
|
b516632731 | 1 year ago |
|
|
87ad5d0c3a | 1 year ago |
|
|
b95bb46d00 | 1 year ago |
|
|
60c91f9cc4 | 1 year ago |
|
|
20ce9cb8d9 | 1 year ago |
|
|
a2625410f9 | 1 year ago |
|
|
e2697012ea | 1 year ago |
|
|
f28cd3aad9 | 1 year ago |
|
|
fd82a54d63 | 1 year ago |
|
|
d95d26a280 | 1 year ago |
|
|
33310bdc42 | 1 year ago |
|
|
6281d770c1 | 1 year ago |
|
|
c37f3bdad7 | 1 year ago |
|
|
5630ca5982 | 1 year ago |
|
|
ceb75d98d0 | 1 year ago |
|
|
75ad3b5657 | 1 year ago |
|
|
5adf27eab1 | 1 year ago |
|
|
18fbbed723 | 1 year ago |
|
|
201bbf755d | 1 year ago |
|
|
b9fe35b8c3 | 1 year ago |
|
|
fd0787152e | 1 year ago |
|
|
29cf04c804 | 1 year ago |
|
|
a4827d1b7d | 1 year ago |
|
|
f107e11528 | 1 year ago |
|
|
52b6f75dd0 | 1 year ago |
|
|
fd719221ed | 1 year ago |
|
|
a2c04fa954 | 1 year ago |
|
|
6db5ac5f26 | 1 year ago |
|
|
cedce23cf5 | 1 year ago |
|
|
645816f614 | 1 year ago |
|
|
00a769bfd5 | 1 year ago |
|
|
0fe5133992 | 1 year ago |
|
|
2994f5936e | 1 year ago |
|
|
e8a96fa0ad | 1 year ago |
|
|
8c8cf5dcd9 | 1 year ago |
|
|
19ff5db70d | 1 year ago |
|
|
c7b689842e | 1 year ago |
|
|
df03c65d5a | 1 year ago |
|
|
4a88a93e8d | 1 year ago |
|
|
950b9e5510 | 1 year ago |
|
|
70230de8e1 | 1 year ago |
|
|
c0ea9cadd7 | 1 year ago |
|
|
8c966c3e23 | 1 year ago |
|
|
9f87e77ede | 1 year ago |
|
|
6165c9b7cf | 1 year ago |
|
|
88ea5215c0 | 1 year ago |
|
|
522b3d53cc | 1 year ago |
|
|
9a1d37166a | 1 year ago |
|
|
07ae87f72b | 1 year ago |
|
|
a16a202800 | 1 year ago |
|
|
09ff9c0a7d | 1 year ago |
|
|
8c4a93d1dc | 1 year ago |
|
|
2da8a4af84 | 1 year ago |
|
|
7d5f0d3187 | 1 year ago |
|
|
a65c1810a2 | 1 year ago |
|
|
09b7d27b40 | 1 year ago |
|
|
e1175d01cc | 1 year ago |
|
|
51fe77ff33 | 1 year ago |
|
|
c540266e48 | 1 year ago |
|
|
198e859850 | 1 year ago |
|
|
87944719a5 | 1 year ago |
|
|
6b44656819 | 1 year ago |
|
|
35334e82ae | 1 year ago |
|
|
32e7b7184f | 1 year ago |
|
|
4098183be7 | 1 year ago |
|
|
e25b03b68f | 1 year ago |
|
|
7076b2d443 | 1 year ago |
|
|
1933200372 | 1 year ago |
|
|
0359afe574 | 1 year ago |
|
|
cd8282bbe0 | 1 year ago |
|
|
8350b3c70e | 1 year ago |
|
|
8488d80c6f | 1 year ago |
|
|
affd4646af | 1 year ago |
|
|
9853638c6b | 1 year ago |
|
|
faf2f2a312 | 1 year ago |
|
|
4adf485208 | 1 year ago |
|
|
eaab76aa9e | 1 year ago |
|
|
2ca740a85d | 1 year ago |
|
|
569aa73cd4 | 1 year ago |
|
|
d3858ff5ff | 1 year ago |
|
|
2760dc8a76 | 1 year ago |
|
|
c1990dde75 | 1 year ago |
|
|
af5b62b8fd | 1 year ago |
|
|
fcd6e92dba | 1 year ago |
|
|
dad098ff34 | 1 year ago |
|
|
aa9a499a09 | 1 year ago |
|
|
f10e6f021c | 1 year ago |
|
|
09596331fd | 1 year ago |
|
|
c8f9962def | 1 year ago |
|
|
e91e8fcccb | 1 year ago |
|
|
a702f1f7ef | 1 year ago |
|
|
9654a68dad | 1 year ago |
|
|
a2f18e2284 | 1 year ago |
|
|
fceb927bce | 1 year ago |
|
|
f192c1716d | 1 year ago |
|
|
f2960c2229 | 1 year ago |
|
|
552a68c1d4 | 1 year ago |
|
|
e5f277c957 | 1 year ago |
|
|
ff05dd7694 | 1 year ago |
|
|
7c604647bf | 1 year ago |
|
|
0fd0c38971 | 1 year ago |
|
|
a454613ce8 | 1 year ago |
|
|
73dd8c4688 | 1 year ago |
|
|
1a07cfc144 | 1 year ago |
|
|
715fe9e645 | 1 year ago |
|
|
6316ac055c | 1 year ago |
|
|
ff64db7a7c | 1 year ago |
|
|
d5a299d7ff | 1 year ago |
|
|
0e3c3f89d0 | 1 year ago |
|
|
234e23b7a5 | 1 year ago |
|
|
69093b8214 | 1 year ago |
|
|
708d7908aa | 1 year ago |
|
|
b08051789d | 1 year ago |
|
|
794a737b6d | 1 year ago |
|
|
b1ef29fadf | 1 year ago |
|
|
0475fc81ed | 1 year ago |
|
|
6aefb603ae | 1 year ago |
|
|
92bdefcf9d | 1 year ago |
|
|
576de54ef8 | 1 year ago |
|
|
319225d645 | 1 year ago |
|
|
9addea65d6 | 1 year ago |
|
|
95c43a06d5 | 1 year ago |
|
|
72c7317672 | 1 year ago |
|
|
1cec3e5436 | 1 year ago |
|
|
6c18056cef | 1 year ago |
|
|
f4b678083d | 1 year ago |
|
|
50eb6f7b7e | 1 year ago |
|
|
0b6af2549f | 1 year ago |
|
|
f229d95469 | 1 year ago |
|
|
7ba725de6d | 1 year ago |
|
|
677fd51e91 | 1 year ago |
|
|
45b843fadc | 1 year ago |
|
|
9ec62f0be0 | 1 year ago |
|
|
9baba17692 | 1 year ago |
|
|
d86d0a5d5b | 1 year ago |
|
|
543e8e3267 | 1 year ago |
|
|
267dcf8fa7 | 1 year ago |
|
|
fec4594480 | 1 year ago |
|
|
f669df2ff5 | 1 year ago |
|
|
9dfa652125 | 1 year ago |
|
|
87f1cd8cdc | 1 year ago |
|
|
f30f8ee563 | 1 year ago |
|
|
64eceba9fa | 1 year ago |
|
|
58e09bdaba | 1 year ago |
|
|
d4e9040ccf | 1 year ago |
|
|
653894cb82 | 1 year ago |
|
|
581cb0c1fe | 1 year ago |
|
|
4e545ca7af | 1 year ago |
|
|
b4050e0335 | 1 year ago |
|
|
1cecd1fe94 | 1 year ago |
|
|
51ed1b2db8 | 1 year ago |
|
|
3e4d712dca | 1 year ago |
|
|
d284b647ab | 1 year ago |
|
|
3bcd05fc5f | 1 year ago |
|
|
b0a1cc48a6 | 1 year ago |
|
|
2eba38b289 | 1 year ago |
|
|
8481fadbd0 | 1 year ago |
|
|
6abcdd8d4b | 1 year ago |
|
|
5df1445e29 | 1 year ago |
|
|
a94adf4d90 | 1 year ago |
|
|
f21aa282dd | 1 year ago |
|
|
8ce6694232 | 1 year ago |
|
|
d8bd51e17e | 1 year ago |
|
|
b1b04d2953 | 1 year ago |
|
|
8852020ca8 | 1 year ago |
|
|
deb4f763d0 | 1 year ago |
|
|
58593de53d | 1 year ago |
|
|
040be87a54 | 1 year ago |
|
|
1703ac39d5 | 1 year ago |
|
|
839869061e | 1 year ago |
|
|
764c65563b | 1 year ago |
|
|
2550b9cac1 | 1 year ago |
|
|
04225170d3 | 1 year ago |
|
|
326a3f78b2 | 1 year ago |
|
|
f86d31f811 | 1 year ago |
|
|
10dac6c0d4 | 1 year ago |
|
|
fece09b781 | 1 year ago |
|
|
6b1fb90584 | 1 year ago |
|
|
5055cfddbd | 1 year ago |
|
|
878d20e612 | 1 year ago |
|
|
336f64712d | 1 year ago |
|
|
56989c6b2f | 1 year ago |
|
|
7b1d3dcd46 | 1 year ago |
|
|
f1e66daf06 | 1 year ago |
|
|
ca087290b9 | 1 year ago |
|
|
883886bc32 | 1 year ago |
|
|
9e60253eb1 | 1 year ago |
|
|
dba85f2bff | 1 year ago |
|
|
e5b852793e | 1 year ago |
|
|
d4cbd86b96 | 1 year ago |
|
|
8d82bacaa3 | 1 year ago |
|
|
4af760c172 | 1 year ago |
|
|
7747bc53b1 | 1 year ago |
|
|
0456a92f15 | 1 year ago |
|
|
677afa9a41 | 1 year ago |
|
|
ebcce4f2ec | 1 year ago |
|
|
91e53e4872 | 1 year ago |
|
|
8bc51b3b79 | 1 year ago |
|
|
733d3ca69f | 1 year ago |
|
|
4a2e465e5d | 1 year ago |
|
|
7ed3c5a175 | 1 year ago |
|
|
8567cedf0a | 1 year ago |
|
|
e02b563bf4 | 1 year ago |
|
|
27d2814ef9 | 1 year ago |
|
|
de95fa2e3e | 1 year ago |
|
|
5a4f9d8914 | 1 year ago |
|
|
23271c4623 | 1 year ago |
|
|
f3d14e101c | 1 year ago |
|
|
275d7f5419 | 1 year ago |
|
|
ac163085e1 | 1 year ago |
|
|
8b4bac3cc2 | 1 year ago |
|
|
35f4db7905 | 1 year ago |
|
|
08b1241a68 | 1 year ago |
|
|
3ffcefaa1b | 1 year ago |
|
|
3b173dc6fc | 1 year ago |
|
|
60f1fb1f70 | 1 year ago |
|
|
f610ae6412 | 1 year ago |
|
|
f80b586081 | 2 years ago |
|
|
7999064dd4 | 2 years ago |
|
|
8c387e7983 | 2 years ago |
|
|
1d29cacf04 | 2 years ago |
|
|
ce4b4d06d0 | 2 years ago |
|
|
79e1d59482 | 2 years ago |
|
|
47e1c0fa89 | 2 years ago |
|
|
3ec23a56ab | 2 years ago |
|
|
a01493e071 | 2 years ago |
|
|
16052210c1 | 2 years ago |
|
|
7dd9658864 | 2 years ago |
|
|
f794f411b7 | 2 years ago |
|
|
94fbbad6f3 | 2 years ago |
|
|
4c5ed57958 | 2 years ago |
|
|
3d5cc5ceff | 2 years ago |
|
|
c506153737 | 2 years ago |
|
|
95ee8a4a16 | 2 years ago |
|
|
3f7b0695f4 | 2 years ago |
|
|
b3bb0939de | 2 years ago |
|
|
08fe54c36d | 2 years ago |
|
|
166c5be5d6 | 2 years ago |
|
|
7ce2a97c1f | 2 years ago |
|
|
b23e241973 | 2 years ago |
|
|
b8b54b7139 | 2 years ago |
|
|
5853e54684 | 2 years ago |
|
|
32e3b07e5d | 2 years ago |
|
|
3b99676e7a | 2 years ago |
|
|
3c829302c7 | 2 years ago |
|
|
b579b01db5 | 2 years ago |
|
|
7db798c420 | 2 years ago |
|
|
77ed11b58e | 2 years ago |
|
|
3ae6ffb8d0 | 2 years ago |
|
|
0e5e4d57c5 | 2 years ago |
|
|
d8cb38a9be | 2 years ago |
|
|
07b0a2da9e | 2 years ago |
|
|
1d040e8291 | 2 years ago |
|
|
797a91a037 | 2 years ago |
|
|
9c0b57835f | 2 years ago |
|
|
72564f5449 | 2 years ago |
|
|
cc18692538 | 2 years ago |
|
|
3e102e9d69 | 2 years ago |
|
|
b9b53bcc91 | 2 years ago |
|
|
00ef8e5cdf | 2 years ago |
|
|
a738591dd0 | 2 years ago |
|
|
85771bcd89 | 2 years ago |
|
|
0150ede4cb | 2 years ago |
|
|
1406165789 | 2 years ago |
|
|
a62eadb581 | 2 years ago |
|
|
197b148fca | 2 years ago |
|
|
fd4a1cda2d | 2 years ago |
|
|
108920c4fd | 2 years ago |
|
|
49c7702707 | 2 years ago |
|
|
15361654b7 | 2 years ago |
|
|
c2b64d40d1 | 2 years ago |
|
|
ce9404da84 | 2 years ago |
|
|
5c55d65eb3 | 2 years ago |
|
|
6f9180545b | 2 years ago |
|
|
e966aa07cd | 2 years ago |
|
|
102993cb10 | 2 years ago |
|
|
d0627b83d0 | 2 years ago |
|
|
a6602a0f5a | 2 years ago |
|
|
55b9a72795 | 2 years ago |
|
|
d4f0c9d116 | 2 years ago |
|
|
bbb3184454 | 2 years ago |
|
|
7d81e68757 | 2 years ago |
|
|
487388c950 | 2 years ago |
|
|
9d507eb10c | 2 years ago |
|
|
cf9b19ffa7 | 2 years ago |
|
|
9591269a68 | 2 years ago |
|
|
27fe426213 | 2 years ago |
|
|
b23d899f5e | 2 years ago |
|
|
8bf0c03fe2 | 2 years ago |
|
|
1ebb298cd7 | 2 years ago |
|
|
9f588ab9fd | 2 years ago |
|
|
91a1198900 | 2 years ago |
|
|
321ac087dc | 2 years ago |
|
|
5d81c030b5 | 2 years ago |
|
|
e798bc7787 | 2 years ago |
|
|
057b053d58 | 2 years ago |
|
|
e9a9ef6b95 | 2 years ago |
|
|
bd7062a479 | 2 years ago |
|
|
a659068ef3 | 2 years ago |
|
|
6f7e1fcfb2 | 2 years ago |
|
|
eff26c8889 | 2 years ago |
|
|
037059d0e6 | 2 years ago |
|
|
9b78a3ecf2 | 2 years ago |
|
|
4df8311e18 | 2 years ago |
|
|
645006fde8 | 2 years ago |
|
|
95bbfefe90 | 2 years ago |
|
|
3181a5860b | 2 years ago |
|
|
39c83444b7 | 2 years ago |
|
|
7356d2036d | 2 years ago |
|
|
a8df8665ae | 2 years ago |
|
|
6355252699 | 2 years ago |
|
|
6ec65e4aa4 | 2 years ago |
|
|
f58bb421e5 | 2 years ago |
|
|
38ecde185a | 2 years ago |
|
|
e3924d5b06 | 2 years ago |
|
|
c1a7fc2624 | 2 years ago |
|
|
818d14fdbb | 2 years ago |
|
|
e93a6865e7 | 2 years ago |
|
|
c6325eef50 | 2 years ago |
|
|
06d6653c78 | 2 years ago |
|
|
aac722909b | 2 years ago |
|
|
78d64713a2 | 2 years ago |
|
|
7d71421c30 | 2 years ago |
|
|
7a571e596d | 2 years ago |
|
|
1718a034ee | 2 years ago |
|
|
5443daabb6 | 2 years ago |
|
|
de4f8ef2f9 | 2 years ago |
|
|
b9fb4a5e93 | 2 years ago |
|
|
3cdd391410 | 2 years ago |
|
|
ad77e8afa4 | 2 years ago |
|
|
c85d17d60d | 2 years ago |
|
|
955c75a99f | 2 years ago |
|
|
d32d1f5044 | 2 years ago |
|
|
091c5247d5 | 2 years ago |
|
|
c593798d83 | 2 years ago |
|
|
edac81f51a | 2 years ago |
|
|
8da3b4b7ac | 2 years ago |
|
|
844e5fdb50 | 2 years ago |
|
|
3f502de1f7 | 2 years ago |
|
|
6532f392b5 | 2 years ago |
|
|
9f340b2dfb | 2 years ago |
|
|
30d34de653 | 2 years ago |
|
|
52329e658f | 2 years ago |
|
|
1c0df02c56 | 2 years ago |
|
|
2061a971b8 | 2 years ago |
|
|
cc62981f12 | 2 years ago |
|
|
613623fa53 | 2 years ago |
|
|
fffdbfdbee | 2 years ago |
|
|
a55a4720fe | 2 years ago |
|
|
42f2813e44 | 2 years ago |
|
|
c58d35288b | 2 years ago |
|
|
f410df40f1 | 2 years ago |
|
|
7e95949ab7 | 2 years ago |
|
|
7c4c3a3c97 | 2 years ago |
|
|
7f25c5f82d | 2 years ago |
|
|
f1939391e8 | 2 years ago |
|
|
d1f9b0d829 | 2 years ago |
|
|
f2354957e6 | 2 years ago |
|
|
600eab20a1 | 2 years ago |
|
|
336c4a4d49 | 2 years ago |
|
|
a1e47edfb2 | 2 years ago |
|
|
5269659c73 | 2 years ago |
|
|
ab1b549a64 | 2 years ago |
|
|
2867dffc5a | 2 years ago |
|
|
6d972a13fe | 2 years ago |
|
|
3918bfafdf | 2 years ago |
|
|
1c15e569bf | 2 years ago |
|
|
5030548500 | 2 years ago |
|
|
c5d3a7b0c1 | 2 years ago |
|
|
75c46130ed | 2 years ago |
|
|
62c17155ca | 2 years ago |
|
|
3a52062ce4 | 2 years ago |
|
|
74ccbdcaee | 2 years ago |
|
|
fbd610349a | 2 years ago |
|
|
8af75d8c6a | 2 years ago |
|
|
0bafd117ff | 2 years ago |
|
|
ae9a7c6090 | 2 years ago |
|
|
481ad02e01 | 2 years ago |
|
|
9042074c50 | 2 years ago |
|
|
821e51ff7d | 2 years ago |
|
|
0f4808f5b5 | 2 years ago |
|
|
aba8a80d8f | 2 years ago |
|
|
2b0edfde60 | 2 years ago |
|
|
01a496768a | 2 years ago |
|
|
82a57cb3c8 | 2 years ago |
|
|
27c07d86bc | 2 years ago |
|
|
e916f2a66e | 2 years ago |
|
|
494ecdfec8 | 2 years ago |
|
|
8a3c0e02f6 | 2 years ago |
|
|
37428bf9c0 | 2 years ago |
|
|
62b4a21cef | 2 years ago |
|
|
b566e4e7e4 | 2 years ago |
|
|
4ff2061bb7 | 2 years ago |
|
|
03d7e138e9 | 2 years ago |
|
|
0540da57e6 | 2 years ago |
|
|
e0e9d25a4f | 2 years ago |
|
|
fd7684866e | 2 years ago |
|
|
7c8b427d2a | 2 years ago |
|
|
4fcc68d149 | 2 years ago |
|
|
ad726a3fd7 | 2 years ago |
|
|
f167a8c8f6 | 2 years ago |
|
|
b404b44008 | 2 years ago |
|
|
c6dc51bb5d | 2 years ago |
|
|
69ee6be246 | 2 years ago |
|
|
e08fab2938 | 2 years ago |
|
|
5db275bce2 | 2 years ago |
|
|
9c05e3f8e8 | 2 years ago |
|
|
ac2c9b8621 | 2 years ago |
|
|
02852ac4e7 | 2 years ago |
|
|
1163541ac5 | 2 years ago |
|
|
2da0c17fe2 | 2 years ago |
|
|
7528480f54 | 2 years ago |
|
|
a45a270479 | 2 years ago |
|
|
380804361b | 2 years ago |
|
|
e072ee8b44 | 2 years ago |
|
|
d812644e61 | 2 years ago |
|
|
71deb11e1f | 2 years ago |
|
|
2e138da3d5 | 2 years ago |
|
|
939b6b1e46 | 2 years ago |
|
|
d937c7e6ab | 2 years ago |
|
|
f91ff0b9d0 | 2 years ago |
|
|
98cbee11b9 | 2 years ago |
|
|
7f0431d493 | 2 years ago |
|
|
3227c53699 | 2 years ago |
|
|
2ee8784d7d | 2 years ago |
|
|
0fac0f450c | 2 years ago |
|
|
b4fb202db4 | 2 years ago |
|
|
93ba163ea2 | 2 years ago |
|
|
bd3ff20146 | 2 years ago |
|
|
b3a0b97f0e | 2 years ago |
|
|
ca212ca692 | 2 years ago |
|
|
3b809202b3 | 2 years ago |
|
|
9858419586 | 2 years ago |
|
|
1f7fe2aed3 | 2 years ago |
|
|
71affd155c | 2 years ago |
|
|
a54f030c4e | 2 years ago |
|
|
3b5a018f8c | 2 years ago |
|
|
8d5fc945d4 | 2 years ago |
|
|
50194df24d | 2 years ago |
|
|
5f771973a8 | 2 years ago |
|
|
4fcfbb374f | 2 years ago |
|
|
853c21e49f | 2 years ago |
|
|
3efb5d8520 | 2 years ago |
|
|
e74985d870 | 2 years ago |
|
|
32eab42c26 | 2 years ago |
|
|
0d3d8232f9 | 2 years ago |
|
|
fd3dc389df | 2 years ago |
|
|
2d274f37ee | 2 years ago |
|
|
d1a12df18f | 2 years ago |
|
|
017a9a58a3 | 2 years ago |
|
|
e33c57f51b | 2 years ago |
|
|
da041df054 | 2 years ago |
|
|
99b57e1297 | 2 years ago |
|
|
6ce8a22514 | 2 years ago |
|
|
a9fc534ea7 | 2 years ago |
|
|
6e04fa1251 | 2 years ago |
|
|
8f851282b4 | 2 years ago |
|
|
d774935a6a | 2 years ago |
|
|
712d42b328 | 2 years ago |
|
|
f66aab03f2 | 2 years ago |
|
|
53ca9c9677 | 2 years ago |
|
|
7bcc624a74 | 2 years ago |
|
|
fa2d73348e | 2 years ago |
|
|
26c3edbc17 | 2 years ago |
|
|
b9d89edfc2 | 2 years ago |
|
|
7ccc6438d5 | 2 years ago |
|
|
cd9f65596b | 2 years ago |
|
|
5404743f17 | 2 years ago |
|
|
c0296d0882 | 2 years ago |
|
|
dec07bd27d | 2 years ago |
|
|
1be1e9843d | 2 years ago |
|
|
5b5afcfe4c | 2 years ago |
|
|
4f5dc9ffad | 2 years ago |
|
|
9c5b00ecf0 | 2 years ago |
|
|
048ba99323 | 2 years ago |
|
|
35bdc15aa9 | 2 years ago |
|
|
b06288e7eb | 2 years ago |
|
|
426c7ad708 | 2 years ago |
|
|
fadf8861e7 | 2 years ago |
|
|
dceedf019a | 2 years ago |
|
|
f49ddf7437 | 2 years ago |
|
|
33a3c8350d | 2 years ago |
|
|
052fa146d2 | 2 years ago |
|
|
1c85782690 | 2 years ago |
|
|
cf5abdf7d3 | 2 years ago |
|
|
df562bedbb | 2 years ago |
|
|
11c1eafa0d | 2 years ago |
|
|
cc8369e02a | 2 years ago |
|
|
cd468df9ad | 2 years ago |
|
|
64666e42e8 | 2 years ago |
|
|
ad7c953d29 | 2 years ago |
|
|
b345efe2d1 | 2 years ago |
|
|
74b8aaa94e | 2 years ago |
|
|
1ace1ba3ec | 2 years ago |
|
|
b822574b70 | 2 years ago |
|
|
466ca0f0e9 | 2 years ago |
|
|
0bcac0c639 | 2 years ago |
|
|
7433fb8fa0 | 2 years ago |
|
|
b1ac1cf238 | 2 years ago |
|
|
a88a861d82 | 2 years ago |
|
|
41ced8edee | 2 years ago |
|
|
0f73f74539 | 2 years ago |
|
|
bb9902e3b2 | 2 years ago |
|
|
c03b0fc981 | 2 years ago |
|
|
e8733f15e4 | 2 years ago |
|
|
26519c71a6 | 2 years ago |
|
|
f923acc5a7 | 2 years ago |
|
|
55b9b6aac4 | 2 years ago |
|
|
52db07a33b | 2 years ago |
|
|
7ed8c9f787 | 2 years ago |
|
|
cc12b193d3 | 2 years ago |
|
|
4d9c51bcd9 | 2 years ago |
|
|
7de48393b6 | 2 years ago |
|
|
283aa92fd5 | 2 years ago |
|
|
9d0393ca72 | 2 years ago |
|
|
c5fc7f76d3 | 2 years ago |
|
|
6b6325895b | 2 years ago |
|
|
39e9f5a2ff | 2 years ago |
|
|
204223e2a8 | 2 years ago |
|
|
b39abcc4a0 | 2 years ago |
|
|
f49e9fa66b | 2 years ago |
|
|
56fd22b43f | 2 years ago |
|
|
0b2bf607f7 | 2 years ago |
|
|
3ff9e69585 | 2 years ago |
|
|
9ceb90204c | 2 years ago |
|
|
ab3c4b2b2d | 2 years ago |
|
|
060ca4b7ef | 2 years ago |
|
|
a7a29ef029 | 2 years ago |
|
|
e7826c2570 | 2 years ago |
|
|
77a733a062 | 2 years ago |
|
|
96bd99650f | 2 years ago |
|
|
2d1899f1f2 | 2 years ago |
|
|
42b1740b0f | 2 years ago |
|
|
fbf613c4e6 | 2 years ago |
|
|
bf18070bc1 | 2 years ago |
|
|
c9c41fdd33 | 2 years ago |
|
|
949d822385 | 2 years ago |
|
|
83b8f28055 | 2 years ago |
|
|
c602d817d9 | 2 years ago |
|
|
a50b217b6f | 2 years ago |
|
|
23c031780f | 2 years ago |
|
|
e3b50d5dfa | 2 years ago |
|
|
9b1bee567f | 2 years ago |
|
|
3534973357 | 2 years ago |
|
|
c4c4bdd547 | 2 years ago |
|
|
528fadceb8 | 2 years ago |
|
|
e15aecaa52 | 2 years ago |
|
|
62e635ebe5 | 2 years ago |
|
|
be846b43ae | 2 years ago |
|
|
7bfe168eee | 2 years ago |
|
|
e8d4a91ad7 | 2 years ago |
|
|
bc1804c030 | 2 years ago |
|
|
d18c9fe0da | 2 years ago |
|
|
a7ea87dd8c | 2 years ago |
|
|
e3a0ab8513 | 2 years ago |
|
|
3a77144a5e | 2 years ago |
|
|
69e311829b | 2 years ago |
|
|
b1effdb7c7 | 2 years ago |
|
|
124537eb18 | 2 years ago |
|
|
b81924b144 | 2 years ago |
|
|
dd18605678 | 2 years ago |
|
|
6bce31d43f | 2 years ago |
|
|
0da5b47ce5 | 2 years ago |
|
|
d0d891b112 | 2 years ago |
|
|
2292c392e9 | 2 years ago |
|
|
cc35aa6bc3 | 2 years ago |
|
|
9e4a124a58 | 2 years ago |
|
|
d63afb0917 | 2 years ago |
|
|
80a3f61a1f | 2 years ago |
|
|
303c817d2d | 2 years ago |
|
|
d9cf94c6b5 | 2 years ago |
|
|
d72414d531 | 2 years ago |
|
|
26be293f24 | 2 years ago |
|
|
36b8633c1e | 2 years ago |
|
|
6bdbf2c65e | 2 years ago |
|
|
80ec615548 | 2 years ago |
|
|
ffc45ff79b | 2 years ago |
|
|
48cb28d9f8 | 2 years ago |
|
|
51da0b62c1 | 2 years ago |
|
|
e391ed52f3 | 2 years ago |
|
|
a5e6e4255d | 2 years ago |
|
|
590e7e3ba3 | 2 years ago |
|
|
d218ad5a67 | 2 years ago |
|
|
350bc0ad58 | 2 years ago |
|
|
a0f9bc032d | 2 years ago |
|
|
4edcd70871 | 2 years ago |
|
|
90c0bf46f4 | 2 years ago |
|
|
9d4fc1980f | 2 years ago |
|
|
332524f1f3 | 2 years ago |
|
|
078b59b1e2 | 2 years ago |
|
|
0551ed5f0b | 2 years ago |
|
|
915d4093b9 | 2 years ago |
|
|
952e9c39ac | 2 years ago |
|
|
3e32a6280a | 2 years ago |
|
|
288b67e250 | 2 years ago |
|
|
7d2f5696f5 | 2 years ago |
|
|
8c3fec0933 | 2 years ago |
|
|
33b00fe65f | 2 years ago |
|
|
f0f50a37b5 | 2 years ago |
|
|
c2b2148190 | 2 years ago |
|
|
f22362dc53 | 2 years ago |
|
|
68cc1d4c4f | 2 years ago |
|
|
0f84ef1e58 | 2 years ago |
|
|
a245574dee | 2 years ago |
|
|
fd90970173 | 2 years ago |
|
|
75a55e4748 | 2 years ago |
|
|
bbd4867830 | 2 years ago |
|
|
3463c8a49e | 2 years ago |
|
|
7829a2ad3b | 2 years ago |
|
|
fb387dbcd9 | 2 years ago |
|
|
cd0b3cd1dc | 2 years ago |
|
|
fe500a27c0 | 2 years ago |
|
|
da860bc250 | 2 years ago |
|
|
e3a8eeb647 | 2 years ago |
|
|
6dbc709f98 | 2 years ago |
|
|
49ca54d68a | 2 years ago |
|
|
e81b8127af | 2 years ago |
|
|
d1b490ed98 | 2 years ago |
|
|
893ed6afd2 | 2 years ago |
|
|
9df2cf5bd7 | 2 years ago |
|
|
b5ceaf4e65 | 2 years ago |
|
|
44ea9fe709 | 2 years ago |
|
|
8174229d28 | 2 years ago |
|
|
f628ed2e1b | 2 years ago |
|
|
c90a0a77bc | 2 years ago |
|
|
10b3f2e65b | 2 years ago |
|
|
9821e93d25 | 2 years ago |
|
|
6c16c9c55f | 2 years ago |
|
|
ab679bccc6 | 2 years ago |
|
|
6fcf3785ef | 2 years ago |
|
|
e605b325c7 | 2 years ago |
|
|
a714d0927e | 2 years ago |
|
|
fe1ef89d30 | 2 years ago |
|
|
639895f511 | 2 years ago |
|
|
4fdc02de35 | 2 years ago |
|
|
103ef11f3d | 2 years ago |
|
|
69e7efe6d1 | 2 years ago |
|
|
83e971d85b | 2 years ago |
|
|
fec60955ed | 2 years ago |
|
|
c5b980a406 | 2 years ago |
|
|
99483268d6 | 2 years ago |
|
|
67fef2cc1c | 2 years ago |
|
|
f450111c1c | 2 years ago |
|
|
718e7eab82 | 2 years ago |
|
|
eb031b00ab | 2 years ago |
|
|
a10c1101ab | 2 years ago |
|
|
9c0c20f86b | 2 years ago |
|
|
c6d1f1b525 | 2 years ago |
|
|
9a84791f5c | 2 years ago |
|
|
14fc02cb23 | 2 years ago |
|
|
39ae6a406c | 2 years ago |
|
|
ba8682f79e | 2 years ago |
|
|
986e4d8fcc | 2 years ago |
|
|
016ced24e0 | 2 years ago |
|
|
b60b2d8355 | 2 years ago |
|
|
f733b85878 | 2 years ago |
|
|
1926a73dee | 2 years ago |
|
|
071f4f0911 | 2 years ago |
|
|
813c5236b3 | 2 years ago |
|
|
f39a9f191a | 2 years ago |
|
|
6d8d757798 | 2 years ago |
|
|
81975977ad | 2 years ago |
|
|
b7613606c0 | 2 years ago |
|
|
0aa4ea01f7 | 2 years ago |
|
|
20685598e3 | 2 years ago |
|
|
b1fb1bdc6b | 2 years ago |
|
|
a80bdcc611 | 2 years ago |
|
|
4571e3e001 | 2 years ago |
|
|
38d7d97167 | 2 years ago |
|
|
3913e95b53 | 2 years ago |
|
|
954c926a9f | 2 years ago |
|
|
0973444c47 | 2 years ago |
|
|
368b61c21a | 2 years ago |
|
|
efc9b3502c | 2 years ago |
|
|
2e86c480ec | 2 years ago |
|
|
39eeaacdcb | 2 years ago |
|
|
ea45e91aa7 | 2 years ago |
|
|
a65e41e796 | 2 years ago |
|
|
91648756c9 | 2 years ago |
|
|
a225234800 | 2 years ago |
|
|
de58333dc4 | 2 years ago |
|
|
24a25e3dbf | 2 years ago |
|
|
8e5ae51067 | 2 years ago |
|
|
86c981ae8e | 2 years ago |
|
|
b716c35438 | 2 years ago |
|
|
9e984510d4 | 2 years ago |
|
|
3c08119ccc | 2 years ago |
|
|
ada1b7b54c | 2 years ago |
|
|
a7ed4fbcc3 | 2 years ago |
|
|
1330feaea5 | 2 years ago |
|
|
103f578c61 | 2 years ago |
|
|
deee80ae71 | 2 years ago |
|
|
54350040ad | 2 years ago |
|
|
3472b8ba00 | 2 years ago |
|
|
f2dd9af66c | 2 years ago |
|
|
da46f7967a | 2 years ago |
|
|
ed3d6c5f98 | 2 years ago |
|
|
c4ea825df9 | 2 years ago |
|
|
4120e3946c | 2 years ago |
|
|
c84b6acff2 | 2 years ago |
|
|
159110c032 | 2 years ago |
|
|
93e1b38da2 | 2 years ago |
|
|
3ff028c4e9 | 2 years ago |
|
|
9b46963015 | 2 years ago |
|
|
09e1c14f37 | 2 years ago |
|
|
58af01b90a | 2 years ago |
|
|
e338237e2a | 2 years ago |
|
|
014ea5ef49 | 2 years ago |
|
|
4a8c7fa36a | 2 years ago |
|
|
1251a6cd75 | 2 years ago |
|
|
0ce8eae969 | 2 years ago |
|
|
8b9c76fca5 | 2 years ago |
|
|
52e4e6b802 | 2 years ago |
|
|
57c9d26916 | 2 years ago |
|
|
19d7003a9e | 2 years ago |
|
|
b11e50ab22 | 2 years ago |
|
|
a03ab0e0c0 | 2 years ago |
|
|
05d4db00b3 | 2 years ago |
|
|
f4aaa14fc5 | 2 years ago |
|
|
a8f9423307 | 2 years ago |
|
|
7c871edbca | 2 years ago |
|
|
f7d9579cee | 2 years ago |
|
|
b7e6ca8a26 | 2 years ago |
|
|
8852d1e22e | 2 years ago |
|
|
fcaa0ea442 | 2 years ago |
|
|
8e7d7e0bde | 2 years ago |
|
|
50c6c9f842 | 2 years ago |
|
|
a2979753a9 | 2 years ago |
|
|
789e39b6cb | 2 years ago |
|
|
2f5441ef20 | 2 years ago |
|
|
6ad08f13d4 | 2 years ago |
|
|
656f621711 | 2 years ago |
|
|
6dc0e12fb0 | 2 years ago |
|
|
0e8579017b | 2 years ago |
|
|
e03d0efe71 | 2 years ago |
|
|
0b497c4e0b | 2 years ago |
|
|
704f589b6d | 2 years ago |
|
|
3feb84ac9e | 2 years ago |
|
|
e375654009 | 2 years ago |
|
|
ce7ac5d8f4 | 2 years ago |
|
|
a172fa5a5b | 2 years ago |
|
|
7c89f53988 | 2 years ago |
|
|
4e2d739840 | 2 years ago |
|
|
5cdbda700b | 2 years ago |
|
|
b274b51699 | 2 years ago |
|
|
efb5d34279 | 2 years ago |
|
|
9ecb75b51c | 2 years ago |
|
|
69332a85da | 2 years ago |
|
|
ea095084cc | 2 years ago |
|
|
1f176ebab2 | 2 years ago |
|
|
327c05d03e | 2 years ago |
|
|
2d7c120d19 | 2 years ago |
|
|
a2e9f19814 | 2 years ago |
|
|
c602355e65 | 2 years ago |
|
|
a390e0de49 | 2 years ago |
|
|
a228d71d57 | 2 years ago |
|
|
b2f0eaccc4 | 2 years ago |
|
|
07aaec7d3c | 2 years ago |
|
|
392e9af944 | 2 years ago |
|
|
904e0719eb | 2 years ago |
|
|
52ac3d2e6c | 2 years ago |
|
|
859b07e454 | 2 years ago |
|
|
d4b252fd71 | 2 years ago |
|
|
4a0e7221b0 | 2 years ago |
|
|
43bdbe5a01 | 2 years ago |
|
|
3d10456a87 | 2 years ago |
|
|
d5a6c95ffb | 2 years ago |
|
|
edc07b5dbd | 2 years ago |
|
|
a5219ceb6c | 2 years ago |
|
|
039075086e | 2 years ago |
|
|
495c9fad33 | 2 years ago |
|
|
b61c5e8f12 | 2 years ago |
|
|
a40a8d329a | 2 years ago |
|
|
3f3ab5a7be | 2 years ago |
|
|
d8c32047d0 | 2 years ago |
|
|
f5f2854f36 | 2 years ago |
|
|
86a2b95141 | 2 years ago |
|
|
4db7ccc853 | 2 years ago |
|
|
b1da5c2924 | 2 years ago |
|
|
0fe6fed838 | 2 years ago |
|
|
a3f8c0a236 | 2 years ago |
|
|
c88d7de138 | 2 years ago |
|
|
eeafb3c6fd | 2 years ago |
|
|
0208f5be97 | 2 years ago |
|
|
42ecf8a958 | 2 years ago |
|
|
d780623b98 | 2 years ago |
|
|
1b1a167580 | 2 years ago |
|
|
49d0c80070 | 2 years ago |
|
|
a35c2f3394 | 2 years ago |
|
|
3034a87929 | 2 years ago |
|
|
33b96043c9 | 2 years ago |
|
|
4d02dc0d12 | 2 years ago |
|
|
79fd2a58c1 | 2 years ago |
|
|
ab82d0eba0 | 2 years ago |
|
|
75cc0716fd | 2 years ago |
|
|
87f99addbb | 2 years ago |
|
|
3e1b15a23c | 2 years ago |
|
|
5758e9f68f | 2 years ago |
|
|
0964c02a44 | 2 years ago |
|
|
bf06ab7492 | 2 years ago |
|
|
e992566899 | 2 years ago |
|
|
97338f31c5 | 2 years ago |
|
|
0ff4f976e1 | 2 years ago |
|
|
496b3388ee | 2 years ago |
|
|
d6b578acb0 | 2 years ago |
|
|
b3be87f541 | 2 years ago |
|
|
786cd12dba | 2 years ago |
|
|
a1a17c02d7 | 2 years ago |
|
|
b8caf8e572 | 2 years ago |
|
|
44c2c074a8 | 2 years ago |
|
|
3ed960254d | 2 years ago |
|
|
f1e8fbec5d | 2 years ago |
|
|
c5ed9cf54b | 2 years ago |
|
|
65c567dbcb | 2 years ago |
|
|
b312823afd | 2 years ago |
|
|
56eb059bcd | 2 years ago |
|
|
6ad78f1cba | 2 years ago |
|
|
26b758ae50 | 2 years ago |
|
|
019b8cc087 | 2 years ago |
|
|
c95845083a | 2 years ago |
|
|
9c7f314f65 | 2 years ago |
|
|
743a92a3e8 | 2 years ago |
|
|
37e9b33c24 | 2 years ago |
|
|
7c2ac033d7 | 2 years ago |
|
|
0efd5437f9 | 2 years ago |
|
|
db770351f3 | 2 years ago |
|
|
14b2457627 | 2 years ago |
|
|
f62ea43de8 | 2 years ago |
|
|
ab2aad6bd0 | 2 years ago |
|
|
1293233ad9 | 2 years ago |
|
|
5fff2adbfb | 2 years ago |
|
|
7d67b718a5 | 2 years ago |
|
|
b8ca21d588 | 2 years ago |
|
|
44d91e43ad | 2 years ago |
|
|
3bb10ee948 | 2 years ago |
|
|
81caa59a36 | 2 years ago |
|
|
46e863ef79 | 2 years ago |
|
|
cecba7623e | 2 years ago |
|
|
7dee865c8a | 2 years ago |
|
|
5ac3cdb03c | 2 years ago |
|
|
c3613f3ba4 | 2 years ago |
|
|
e7ac1c1feb | 2 years ago |
|
|
24573d12f5 | 2 years ago |
|
|
9d2f633e14 | 2 years ago |
|
|
570b0c19c5 | 2 years ago |
|
|
a11e42a4d6 | 2 years ago |
|
|
410c73a74f | 2 years ago |
|
|
3ea088371b | 2 years ago |
|
|
09a0bb364a | 2 years ago |
|
|
d91947ae15 | 2 years ago |
|
|
0a8a9e5935 | 2 years ago |
|
|
59226b298f | 2 years ago |
|
|
dc41bf3e83 | 2 years ago |
|
|
a790a18f7f | 2 years ago |
|
|
1fcc1096f5 | 2 years ago |
|
|
86aaca271f | 2 years ago |
|
|
b8cc2d6953 | 2 years ago |
|
|
11f110b0bb | 2 years ago |
|
|
31459ea6e7 | 2 years ago |
|
|
7ab84688c2 | 2 years ago |
|
|
d18f287804 | 2 years ago |
|
|
1c3b41acba | 2 years ago |
|
|
ecfc7e40c9 | 2 years ago |
|
|
41eea1c420 | 2 years ago |
|
|
594524afc3 | 2 years ago |
|
|
70a5337604 | 2 years ago |
|
|
924fde8790 | 2 years ago |
|
|
dea70974bf | 2 years ago |
|
|
43fd7b8d47 | 2 years ago |
|
|
6c2a526aeb | 2 years ago |
|
|
43a8caa687 | 2 years ago |
|
|
f3fc848b6c | 2 years ago |
|
|
98f5d115b3 | 2 years ago |
|
|
885d98cebd | 2 years ago |
|
|
6f73ff23e8 | 2 years ago |
|
|
2cd2d7dbc4 | 2 years ago |
|
|
d3afdad95b | 2 years ago |
|
|
48ac46eb5c | 2 years ago |
|
|
2d6e6b359b | 2 years ago |
|
|
7462743be8 | 3 years ago |
|
|
db2acb752a | 3 years ago |
|
|
335229e61b | 3 years ago |
|
|
2acdd66f70 | 3 years ago |
|
|
d3bec9006e | 3 years ago |
|
|
44f70c1388 | 3 years ago |
|
|
02463e5833 | 3 years ago |
|
|
778d14a1a2 | 3 years ago |
|
|
8b3814c485 | 3 years ago |
|
|
6bcd9dd65c | 3 years ago |
|
|
154ae09c6e | 3 years ago |
|
|
58c9d14d42 | 3 years ago |
|
|
3e69b88518 | 3 years ago |
|
|
9d3671e117 | 3 years ago |
|
|
0731d2487a | 3 years ago |
|
|
5e4dd82b8d | 3 years ago |
|
|
9d33d1a560 | 3 years ago |
|
|
e788cff04a | 3 years ago |
|
|
face1d5b26 | 3 years ago |
|
|
18f8472baa | 3 years ago |
|
|
f14edd871f | 3 years ago |
|
|
c892a38c52 | 3 years ago |
|
|
e9d473711d | 3 years ago |
|
|
97a8bb1ca7 | 3 years ago |
|
|
66d47bf579 | 3 years ago |
|
|
508647eb5a | 3 years ago |
|
|
4ca3a492b0 | 3 years ago |
|
|
fffe104cbe | 3 years ago |
|
|
6258476a58 | 3 years ago |
|
|
af1aca8725 | 3 years ago |
|
|
bdb7fbc039 | 3 years ago |
|
|
6bf0ae92e4 | 3 years ago |
|
|
17b8fd51b7 | 3 years ago |
|
|
d089fad6ce | 3 years ago |
|
|
ec11b79b18 | 3 years ago |
|
|
0f216c6de9 | 3 years ago |
|
|
0606ace83a | 3 years ago |
|
|
746d857550 | 3 years ago |
|
|
cf1d0b8d7e | 3 years ago |
|
|
d94145c6e5 | 3 years ago |
|
|
b71a0ac3cf | 3 years ago |
|
|
32afa26be5 | 3 years ago |
|
|
23cc86e429 | 3 years ago |
|
|
87032a6835 | 3 years ago |
|
|
a4d9c6e36c | 3 years ago |
|
|
76af0b0edb | 3 years ago |
|
|
0054d06e6e | 3 years ago |
|
|
e2e027df81 | 3 years ago |
|
|
a61e441e79 | 3 years ago |
|
|
9b4153aacc | 3 years ago |
|
|
b1d2d0a475 | 3 years ago |
|
|
aeec98db2a | 3 years ago |
|
|
00f21de9eb | 3 years ago |
|
|
2d595bb6ee | 3 years ago |
|
|
e43c9f6489 | 3 years ago |
|
|
81004b53a9 | 3 years ago |
|
|
85a0918da5 | 3 years ago |
|
|
b10fa0fd10 | 3 years ago |
|
|
a2f4bb35cd | 3 years ago |
|
|
4120514256 | 3 years ago |
|
|
29ef717ece | 3 years ago |
|
|
d34cfbf405 | 3 years ago |
|
|
94c2326153 | 3 years ago |
|
|
06c2287b71 | 3 years ago |
|
|
8cb0d3d6fe | 3 years ago |
|
|
76860f82b9 | 3 years ago |
|
|
f134137a05 | 3 years ago |
|
|
fc231ec330 | 3 years ago |
|
|
d3c32faf01 | 3 years ago |
|
|
d16f4cd892 | 3 years ago |
|
|
9a8de28a1d | 3 years ago |
|
|
939f08b8a1 | 3 years ago |
|
|
09500c0734 | 3 years ago |
|
|
400a90464e | 3 years ago |
|
|
9103f9d473 | 3 years ago |
|
|
27c29f99d9 | 3 years ago |
|
|
753c27a90c | 3 years ago |
|
|
3d7e62d2fe | 3 years ago |
|
|
e8289277a7 | 3 years ago |
|
|
887fc691d1 | 3 years ago |
|
|
543e3da194 | 3 years ago |
|
|
908cc22619 | 3 years ago |
|
|
c09d484a14 | 3 years ago |
|
|
07c3f22f75 | 3 years ago |
|
|
d494bf19ee | 3 years ago |
|
|
eda4f75219 | 3 years ago |
|
|
931f126119 | 3 years ago |
|
|
bc171ce06e | 3 years ago |
|
|
191d1fcec8 | 3 years ago |
|
|
eaea9e54d1 | 3 years ago |
|
|
c9b3912fd4 | 3 years ago |
|
|
1611dc3429 | 3 years ago |
|
|
88dc2145e0 | 3 years ago |
|
|
8979987344 | 3 years ago |
|
|
1cec2cc187 | 3 years ago |
|
|
a1279b64a2 | 3 years ago |
|
|
2ce5f2563a | 3 years ago |
|
|
75a60ff738 | 3 years ago |
|
|
74eb7f83d6 | 3 years ago |
|
|
5b2ac5f6fb | 3 years ago |
|
|
fde4ba1d85 | 3 years ago |
|
|
8278a8f5c3 | 3 years ago |
|
|
a39d644094 | 3 years ago |
|
|
0004be15ba | 3 years ago |
|
|
8fef459346 | 3 years ago |
|
|
109603f124 | 3 years ago |
|
|
91879a8405 | 3 years ago |
|
|
f471a3513e | 3 years ago |
|
|
7362111318 | 3 years ago |
|
|
609c7188c0 | 3 years ago |
|
|
a106b34379 | 3 years ago |
|
|
853b95363e | 3 years ago |
|
|
45113e419e | 3 years ago |
|
|
519bb3053b | 3 years ago |
|
|
366cc862ba | 3 years ago |
|
|
611aaf0d38 | 3 years ago |
|
|
712d829b54 | 3 years ago |
|
|
e25656c090 | 3 years ago |
|
|
7debe6d7e2 | 3 years ago |
|
|
55e14e4cb3 | 3 years ago |
|
|
7fbeb2e266 | 3 years ago |
|
|
7be6d779da | 3 years ago |
|
|
96585fbdf7 | 3 years ago |
|
|
1a0d623c02 | 3 years ago |
|
|
67cf2c2a6e | 3 years ago |
|
|
b6dd7fb5f6 | 3 years ago |
|
|
e0c5562600 | 3 years ago |
|
|
2447f66834 | 3 years ago |
|
|
11ed27e397 | 3 years ago |
|
|
6158547571 | 3 years ago |
|
|
a24dfde1ba | 3 years ago |
|
|
d805b49a4d | 3 years ago |
|
|
f871bf77fa | 3 years ago |
|
|
be610b825d | 3 years ago |
|
|
f791cc6f9c | 3 years ago |
|
|
30b2e81b15 | 3 years ago |
|
|
067c7f2d0d | 3 years ago |
|
|
aae3fa3b05 | 3 years ago |
|
|
41cd8ac7d7 | 3 years ago |
|
|
61f2a499da | 3 years ago |
|
|
2f7e704e21 | 3 years ago |
|
|
4d22e7d1e8 | 3 years ago |
|
|
18125ea982 | 3 years ago |
|
|
1498541b45 | 3 years ago |
|
|
4faaa9fc13 | 3 years ago |
|
|
c6c4b7190a | 3 years ago |
|
|
c0bf3ad56c | 3 years ago |
|
|
ad51271c40 | 3 years ago |
|
|
fce952d240 | 3 years ago |
|
|
0dc5032a7a | 3 years ago |
|
|
0395f29503 | 3 years ago |
|
|
03581add21 | 3 years ago |
|
|
e0249accf8 | 3 years ago |
|
|
a3afbafdcc | 3 years ago |
|
|
4d4c126327 | 3 years ago |
|
|
c4bf7b2ae0 | 3 years ago |
|
|
5d5f804c76 | 3 years ago |
|
|
3a76504380 | 3 years ago |
|
|
5035c90c73 | 3 years ago |
|
|
7f1cd30ce8 | 3 years ago |
|
|
155a91a13d | 3 years ago |
|
|
8c291e274d | 3 years ago |
|
|
10e1eb8aac | 3 years ago |
|
|
06a2aa6f97 | 3 years ago |
|
|
f08a2e4551 | 3 years ago |
|
|
e6839b1270 | 3 years ago |
|
|
270890f841 | 3 years ago |
|
|
02581c9e2d | 3 years ago |
|
|
ea4e69df19 | 3 years ago |
|
|
cb0ebb31f7 | 3 years ago |
|
|
497cd0bd15 | 3 years ago |
|
|
b74c7841dd | 3 years ago |
|
|
407ef5b655 | 3 years ago |
|
|
f034b1874b | 3 years ago |
|
|
eac086b935 | 3 years ago |
|
|
ea310b184e | 3 years ago |
|
|
ea3386eda5 | 3 years ago |
|
|
2efdb1940a | 3 years ago |
|
|
896139a8ed | 3 years ago |
|
|
da344233f4 | 3 years ago |
|
|
9cf3551356 | 3 years ago |
|
|
10aaae1ca8 | 3 years ago |
|
|
dcb5206272 | 3 years ago |
|
|
ccdd2b6c19 | 3 years ago |
|
|
16f3e1c180 | 3 years ago |
|
|
7283cde5ff | 3 years ago |
|
|
4c20384d39 | 3 years ago |
|
|
5e9bc4fcc7 | 3 years ago |
|
|
e5448649c3 | 3 years ago |
|
|
951e89d429 | 3 years ago |
|
|
f34dd7926a | 3 years ago |
|
|
b900c05d9b | 3 years ago |
|
|
06948934ac | 3 years ago |
|
|
e82915ef17 | 3 years ago |
|
|
bb37da403f | 3 years ago |
|
|
065263a9bd | 3 years ago |
|
|
f3359cc830 | 3 years ago |
|
|
1b3b5d95c0 | 3 years ago |
|
|
c290ba5436 | 3 years ago |
|
|
bb4ea05375 | 3 years ago |
|
|
47dde65f65 | 3 years ago |
|
|
08eb9552d6 | 3 years ago |
|
|
6058221fec | 3 years ago |
|
|
5bf4ecb394 | 3 years ago |
|
|
41fa80e8b8 | 3 years ago |
|
|
0dd3a26080 | 3 years ago |
|
|
fb400d83e3 | 3 years ago |
|
|
37e5c43e5c | 3 years ago |
|
|
341aa0e13d | 3 years ago |
|
|
3c4359096d | 3 years ago |
|
|
d56240086c | 3 years ago |
|
|
bfea5b314e | 3 years ago |
|
|
03b4fc9f00 | 3 years ago |
|
|
826563694e | 3 years ago |
|
|
880ff923bc | 3 years ago |
|
|
cd0d4b103a | 3 years ago |
|
|
fefec4985d | 3 years ago |
|
|
ba655f3d64 | 3 years ago |
|
|
be3812fa2f | 3 years ago |
|
|
87d4326411 | 3 years ago |
|
|
ef6ede6b12 | 3 years ago |
|
|
eeccda24b0 | 3 years ago |
|
|
2bf29da2ea | 3 years ago |
|
|
0b54029e26 | 3 years ago |
|
|
d24ab347cb | 3 years ago |
|
|
74ffe9418b | 3 years ago |
|
|
c945434d71 | 3 years ago |
|
|
b28545c852 | 3 years ago |
|
|
c8d33d06a1 | 3 years ago |
|
|
2089c3cc7b | 3 years ago |
|
|
5df1da2bfd | 3 years ago |
|
|
70fda48983 | 3 years ago |
|
|
36fe5cb625 | 3 years ago |
|
|
7e2a00520b | 3 years ago |
|
|
caeeb699d1 | 3 years ago |
|
|
f6dc4f9a00 | 3 years ago |
|
|
28135aed66 | 3 years ago |
|
|
8e452f4271 | 3 years ago |
|
|
f4656efebd | 3 years ago |
|
|
42cc0430f8 | 3 years ago |
|
|
81dd8b58c8 | 3 years ago |
|
|
415da81b1a | 3 years ago |
|
|
74a20455f2 | 3 years ago |
|
|
0bdce39def | 3 years ago |
|
|
24f7d8b5bd | 3 years ago |
|
|
18adc66f53 | 3 years ago |
|
|
8c1c855531 | 3 years ago |
|
|
02ae76baf2 | 3 years ago |
|
|
b5c44383e8 | 3 years ago |
|
|
c48ebb937b | 3 years ago |
|
|
49c30d1d26 | 3 years ago |
|
|
cb1a7ece8c | 3 years ago |
|
|
4d60ae3cb6 | 3 years ago |
|
|
fe64620e9f | 3 years ago |
|
|
a42ca9e2da | 3 years ago |
|
|
506f6f7d30 | 3 years ago |
|
|
380861f29e | 3 years ago |
|
|
9afc046d7b | 3 years ago |
|
|
aec87a9a67 | 3 years ago |
|
|
69e0a531df | 3 years ago |
|
|
c85101991f | 3 years ago |
|
|
99e87fd97a | 3 years ago |
|
|
665ec6b7ed | 3 years ago |
|
|
01fbd209c8 | 3 years ago |
|
|
f0fa8d59a1 | 3 years ago |
|
|
a1598fd712 | 3 years ago |
|
|
db96a1ff2e | 3 years ago |
|
|
833a42e798 | 3 years ago |
|
|
922c665ef5 | 3 years ago |
|
|
01842e9870 | 3 years ago |
|
|
a235f6281c | 3 years ago |
|
|
c9e7d4f483 | 3 years ago |
|
|
94496ecc02 | 3 years ago |
|
|
5104a8b542 | 3 years ago |
|
|
e67beae235 | 3 years ago |
|
|
7c4bffede2 | 3 years ago |
|
|
8df620b1cd | 3 years ago |
|
|
cbbdd13c8e | 3 years ago |
|
|
3874fe31d5 | 3 years ago |
|
|
a8c086b340 | 3 years ago |
|
|
06a043d290 | 3 years ago |
|
|
5a15014369 | 3 years ago |
|
|
7fe1f4b3ff | 3 years ago |
|
|
0055146264 | 3 years ago |
|
|
cc26897fb9 | 3 years ago |
|
|
46535f82af | 3 years ago |
|
|
23a7b945af | 3 years ago |
|
|
4fb6ff63b9 | 3 years ago |
|
|
c2e90a210c | 3 years ago |
|
|
22d59e46c8 | 3 years ago |
|
|
e462b7df97 | 3 years ago |
|
|
af8fb952df | 3 years ago |
|
|
a3fa04bcb7 | 3 years ago |
|
|
adf6fea4ac | 3 years ago |
|
|
69af4fe586 | 3 years ago |
|
|
31476a55f9 | 3 years ago |
|
|
c833563907 | 3 years ago |
|
|
74a4a3a8b4 | 3 years ago |
|
|
141bca517d | 3 years ago |
|
|
b9eb6363b7 | 3 years ago |
|
|
c3bed26739 | 3 years ago |
|
|
a15d54fe11 | 3 years ago |
|
|
1998ceac5a | 3 years ago |
|
|
4192ffdbfe | 3 years ago |
|
|
5ec7227886 | 3 years ago |
|
|
a9ebc372f6 | 3 years ago |
|
|
1e98104eca | 3 years ago |
|
|
d05d5fa911 | 3 years ago |
|
|
07df5a81cf | 3 years ago |
|
|
9861a9aa6b | 3 years ago |
|
|
b65e1c498e | 3 years ago |
|
|
240562037d | 3 years ago |
|
|
b3f79e77c3 | 3 years ago |
|
|
db3c73156e | 3 years ago |
|
|
44b3b9b0c1 | 3 years ago |
|
|
ea8c1de44d | 3 years ago |
|
|
8195d0190a | 3 years ago |
|
|
8fd10eabd1 | 3 years ago |
|
|
7b28072ee5 | 3 years ago |
|
|
b37db4d680 | 3 years ago |
|
|
92bfc7e9fa | 3 years ago |
|
|
b3e97760db | 3 years ago |
|
|
3895312bfc | 3 years ago |
|
|
06d2976bb5 | 3 years ago |
|
|
da4566f82f | 3 years ago |
|
|
b8f0ea4690 | 3 years ago |
|
|
bb5e49758a | 3 years ago |
|
|
e617904b62 | 3 years ago |
|
|
b0e455e163 | 3 years ago |
|
|
6c31cea155 | 3 years ago |
|
|
9c71d1b57c | 3 years ago |
|
|
e4f12149ed | 3 years ago |
|
|
f6d264f37d | 3 years ago |
|
|
aa734e5ad3 | 3 years ago |
|
|
591291e9d6 | 3 years ago |
|
|
2311a56658 | 3 years ago |
|
|
514e4473a1 | 3 years ago |
|
|
7ccacf4c27 | 3 years ago |
|
|
15f226fc73 | 3 years ago |
|
|
1388196288 | 3 years ago |
|
|
fe024cb306 | 3 years ago |
|
|
04e7edaa3e | 3 years ago |
|
|
87abdf6346 | 3 years ago |
|
|
c4fad321db | 3 years ago |
|
|
c17c9d9ccc | 3 years ago |
|
|
d27c208ab7 | 3 years ago |
|
|
90966aa7e4 | 3 years ago |
|
|
553a6c709a | 3 years ago |
|
|
16698201f4 | 3 years ago |
|
|
33f27a8af7 | 3 years ago |
|
|
48ec877ad5 | 3 years ago |
|
|
54002ce27f | 3 years ago |
|
|
de03d80e10 | 3 years ago |
|
|
60954c38b6 | 3 years ago |
|
|
ba41d5f4ce | 3 years ago |
|
|
9b76f42138 | 3 years ago |
|
|
5a0306b4b5 | 3 years ago |
|
|
b70ddf0a7c | 3 years ago |
|
|
f53174258c | 3 years ago |
|
|
36706d998e | 3 years ago |
|
|
65ec3ca5c6 | 3 years ago |
|
|
0b0068e113 | 3 years ago |
|
|
553777ef08 | 3 years ago |
|
|
3cda7ffc18 | 3 years ago |
|
|
48d98548cd | 3 years ago |
|
|
b878824d15 | 3 years ago |
|
|
16a99d7b8c | 3 years ago |
|
|
c34f250ac9 | 3 years ago |
|
|
e74b276c98 | 3 years ago |
|
|
f0e61daa63 | 3 years ago |
|
|
34b6f4aa32 | 3 years ago |
|
|
c2b79b55f8 | 3 years ago |
|
|
875b08b1fc | 3 years ago |
|
|
0e3788d943 | 3 years ago |
|
|
396508da6f | 3 years ago |
|
|
6fc6beb067 | 3 years ago |
|
|
939cdf71ad | 3 years ago |
|
|
8d7eabdd71 | 3 years ago |
|
|
d28d6b17d8 | 3 years ago |
|
|
f096ca2ad3 | 3 years ago |
|
|
32914eb36d | 3 years ago |
|
|
b58190ed17 | 3 years ago |
|
|
86a82970fc | 3 years ago |
|
|
9976ea5dfc | 3 years ago |
|
|
f732582d55 | 3 years ago |
|
|
44e28b40d3 | 3 years ago |
|
|
f969e2a8d1 | 3 years ago |
|
|
fa7ea5b16a | 3 years ago |
|
|
3d7a39cf67 | 3 years ago |
|
|
46f8b8e700 | 3 years ago |
|
|
ebcc6391d6 | 3 years ago |
|
|
12522ce8d8 | 3 years ago |
|
|
cae7073f87 | 3 years ago |
|
|
0366ad0e57 | 3 years ago |
|
|
6b95a8c55e | 3 years ago |
|
|
6183f6edf3 | 3 years ago |
|
|
83cca4ef12 | 3 years ago |
|
|
20ad9d0472 | 3 years ago |
|
|
96b9ac36f3 | 3 years ago |
|
|
65170c0fe9 | 3 years ago |
|
|
3e349d3db3 | 3 years ago |
|
|
b2675c3cac | 3 years ago |
|
|
7414cf4a4e | 3 years ago |
|
|
f43c482539 | 3 years ago |
|
|
f0fbbf4333 | 3 years ago |
|
|
0d03c68c8b | 3 years ago |
|
|
b4a4c86049 | 3 years ago |
|
|
306d46ea93 | 3 years ago |
|
|
5b94badfc2 | 3 years ago |
|
|
9adc0c5358 | 3 years ago |
|
|
66adc65a96 | 3 years ago |
|
|
8def9b5446 | 3 years ago |
|
|
e6511061a7 | 3 years ago |
|
|
647a694d91 | 3 years ago |
|
|
e5ececa42d | 3 years ago |
|
|
c321faeaa9 | 3 years ago |
|
|
3112b7937c | 3 years ago |
|
|
a3565abdc3 | 3 years ago |
|
|
34d2e15302 | 3 years ago |
|
|
1b6d1456f3 | 3 years ago |
|
|
b49aee3188 | 3 years ago |
|
|
749f682ef9 | 3 years ago |
|
|
1b269efaa4 | 3 years ago |
|
|
2db1a8d958 | 3 years ago |
|
|
ece7d71e09 | 3 years ago |
|
|
129602ea70 | 3 years ago |
|
|
5e3fddb652 | 3 years ago |
|
|
2340100999 | 3 years ago |
|
|
dc0151720e | 3 years ago |
|
|
36e88b2c0c | 3 years ago |
|
|
35695904d1 | 3 years ago |
|
|
a3ffecc00f | 3 years ago |
|
|
cc418570d5 | 3 years ago |
|
|
481af22ae7 | 3 years ago |
|
|
96b9dc8b5c | 3 years ago |
|
|
93f5f70d79 | 3 years ago |
|
|
fc53b19915 | 3 years ago |
|
|
413f4a2f10 | 3 years ago |
|
|
e8d299782b | 3 years ago |
|
|
489a4ab586 | 3 years ago |
|
|
32fa3a89e2 | 3 years ago |
|
|
f2614dd3d7 | 3 years ago |
|
|
1093584202 | 3 years ago |
|
|
f62d024342 | 3 years ago |
|
|
eea87d8607 | 3 years ago |
|
|
f4c47b5b84 | 3 years ago |
|
|
fa78d5dd6f | 3 years ago |
|
|
779c6664be | 3 years ago |
|
|
38efd7935a | 3 years ago |
|
|
03f021377d | 3 years ago |
|
|
cf345d2d0c | 3 years ago |
|
|
fb3a3f49dd | 3 years ago |
|
|
f3b731114e | 3 years ago |
|
|
454b24ec88 | 3 years ago |
|
|
2c6c64fbe9 | 3 years ago |
|
|
a51d6d6485 | 3 years ago |
|
|
0da57c041c | 3 years ago |
|
|
503573f388 | 3 years ago |
|
|
29cd1f1fdf | 3 years ago |
|
|
e51b33c74a | 3 years ago |
|
|
624a3dd991 | 3 years ago |
|
|
05d705ac03 | 3 years ago |
|
|
1e1756aa17 | 3 years ago |
|
|
91109bf6dc | 3 years ago |
|
|
7f852e793a | 3 years ago |
|
|
aae3747925 | 3 years ago |
|
|
5e23d52e8a | 3 years ago |
|
|
3bc8013d2f | 3 years ago |
|
|
00abaea324 | 3 years ago |
|
|
d14168e325 | 3 years ago |
|
|
c3b1556816 | 3 years ago |
|
|
80a2a10e71 | 3 years ago |
|
|
029f554d6e | 3 years ago |
|
|
841d8f05d9 | 3 years ago |
|
|
7253699529 | 3 years ago |
|
|
5fe546c94d | 3 years ago |
|
|
31c26f16ac | 3 years ago |
|
|
5eda6f96b4 | 3 years ago |
|
|
56a54d5eb4 | 3 years ago |
|
|
7a78a67740 | 3 years ago |
|
|
742533977f | 3 years ago |
|
|
2dedaea0ab | 3 years ago |
|
|
cf7f55181f | 3 years ago |
|
|
f68f0009a2 | 3 years ago |
|
|
4799f54fd1 | 3 years ago |
|
|
06ff8df2e4 | 3 years ago |
|
|
9b78944f9b | 3 years ago |
|
|
fc2781c83a | 3 years ago |
|
|
176437531d | 3 years ago |
|
|
c28e2a72d5 | 3 years ago |
|
|
7f630184c0 | 3 years ago |
|
|
e5a6b82853 | 3 years ago |
|
|
e1d22b1110 | 3 years ago |
|
|
918318252e | 3 years ago |
|
|
e153463c35 | 3 years ago |
|
|
cf00732023 | 3 years ago |
|
|
ef838068ff | 3 years ago |
|
|
c4acb9725f | 3 years ago |
|
|
04e219ff69 | 3 years ago |
|
|
106a85aa80 | 3 years ago |
|
|
ad6e074cce | 3 years ago |
|
|
e89f06a35b | 3 years ago |
|
|
238cc7529a | 3 years ago |
|
|
fb22eb5fac | 3 years ago |
|
|
9ee1c21a67 | 3 years ago |
|
|
c7c142f4a0 | 3 years ago |
|
|
efd0394398 | 3 years ago |
|
|
0091a8280a | 3 years ago |
|
|
f9b0e82473 | 3 years ago |
|
|
63676c21a8 | 3 years ago |
|
|
add70b4790 | 3 years ago |
|
|
d18f665556 | 3 years ago |
|
|
42253e43b3 | 3 years ago |
|
|
ad500a9db6 | 3 years ago |
|
|
2ab03cb668 | 3 years ago |
|
|
b78ddc6166 | 3 years ago |
|
|
1e49d4095b | 3 years ago |
|
|
bf8a1f3db2 | 4 years ago |
|
|
321cc7529e | 4 years ago |
|
|
7907b3126c | 4 years ago |
|
|
ccf9af0e51 | 4 years ago |
|
|
b38b43edc0 | 4 years ago |
|
|
5d13c55dd5 | 4 years ago |
|
|
a1441e7ed7 | 4 years ago |
|
|
a62662d116 | 4 years ago |
|
|
5cb953eb86 | 4 years ago |
|
|
505ffcf405 | 4 years ago |
|
|
a45f9bfc58 | 4 years ago |
|
|
b1990c7918 | 4 years ago |
|
|
e8b801684f | 4 years ago |
|
|
de0c12c078 | 4 years ago |
|
|
b3a9c5fcda | 4 years ago |
|
|
c73d34b04f | 4 years ago |
|
|
583b24854f | 4 years ago |
|
|
1c79b29845 | 4 years ago |
|
|
c348d68983 | 4 years ago |
|
|
6d7506cc1b | 4 years ago |
@ -1,65 +1,66 @@
|
|||||||
name: 🐞 Issue report
|
name: 🐞 Issue report
|
||||||
description: Report a source issue in Kotatsu
|
description: Report a source issue with a source
|
||||||
labels: [bug]
|
labels: [ bug ]
|
||||||
body:
|
body:
|
||||||
|
|
||||||
- type: input
|
- type: input
|
||||||
id: source
|
id: source
|
||||||
attributes:
|
attributes:
|
||||||
label: Source information
|
label: Source information
|
||||||
description: |
|
description: |
|
||||||
You can find the source name in navigation drawer.
|
You can find the source name in navigation drawer.
|
||||||
placeholder: |
|
placeholder: |
|
||||||
Example: "MangaDex"
|
Example: "MangaDex"
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
|
|
||||||
- type: textarea
|
- type: textarea
|
||||||
id: reproduce-steps
|
id: reproduce-steps
|
||||||
attributes:
|
attributes:
|
||||||
label: Steps to reproduce
|
label: Steps to reproduce
|
||||||
description: Provide an example of the issue.
|
description: Provide an example of the issue.
|
||||||
placeholder: |
|
placeholder: |
|
||||||
Example:
|
Example:
|
||||||
1. First step
|
1. First step
|
||||||
2. Second step
|
2. Second step
|
||||||
3. Issue here
|
3. Issue here
|
||||||
validations:
|
Please use English language
|
||||||
required: false
|
validations:
|
||||||
|
required: false
|
||||||
|
|
||||||
- type: input
|
- type: input
|
||||||
id: kotatsu-version
|
id: kotatsu-version
|
||||||
attributes:
|
attributes:
|
||||||
label: Kotatsu version
|
label: Kotatsu version
|
||||||
description: |
|
description: |
|
||||||
You can find your Kotatsu version in **Settings → About**.
|
You can find your Kotatsu version in **Settings → About**.
|
||||||
placeholder: |
|
placeholder: |
|
||||||
Example: "3.3"
|
Example: "3.3"
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
|
|
||||||
- type: input
|
- type: input
|
||||||
id: android-version
|
id: android-version
|
||||||
attributes:
|
attributes:
|
||||||
label: Android version
|
label: Android version
|
||||||
description: |
|
description: |
|
||||||
You can find this somewhere in your Android settings.
|
You can find this somewhere in your Android settings.
|
||||||
placeholder: |
|
placeholder: |
|
||||||
Example: "Android 12"
|
Example: "Android 12"
|
||||||
validations:
|
validations:
|
||||||
required: false
|
required: false
|
||||||
|
|
||||||
- type: textarea
|
- type: textarea
|
||||||
id: other-details
|
id: other-details
|
||||||
attributes:
|
attributes:
|
||||||
label: Other details
|
label: Other details
|
||||||
placeholder: |
|
placeholder: |
|
||||||
Additional details and attachments.
|
Additional details and attachments.
|
||||||
|
|
||||||
- type: checkboxes
|
- type: checkboxes
|
||||||
id: acknowledgements
|
id: acknowledgements
|
||||||
attributes:
|
attributes:
|
||||||
label: Acknowledgements
|
label: Acknowledgements
|
||||||
options:
|
options:
|
||||||
- label: I have searched the existing issues and this is a new ticket, **NOT** a duplicate or related to another open issue.
|
- label: I have searched the existing issues and this is a new ticket, **NOT** a duplicate or related to another open issue.
|
||||||
required: true
|
required: true
|
||||||
|
|||||||
@ -1,30 +1,31 @@
|
|||||||
name: ⭐ Feature request
|
name: ⭐ Feature request
|
||||||
description: Suggest a feature to improve a source
|
description: Suggest a feature to improve a source
|
||||||
labels: [feature request]
|
labels: [ feature request ]
|
||||||
body:
|
body:
|
||||||
|
|
||||||
- type: textarea
|
- type: textarea
|
||||||
id: feature-description
|
id: feature-description
|
||||||
attributes:
|
attributes:
|
||||||
label: Describe your suggested feature
|
label: Describe your suggested feature
|
||||||
description: How can an existing source be improved?
|
description: How can an existing source be improved?
|
||||||
placeholder: |
|
placeholder: |
|
||||||
Example:
|
Example:
|
||||||
"It should work like this..."
|
"It should work like this..."
|
||||||
validations:
|
Please use English language
|
||||||
required: true
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
- type: textarea
|
- type: textarea
|
||||||
id: other-details
|
id: other-details
|
||||||
attributes:
|
attributes:
|
||||||
label: Other details
|
label: Other details
|
||||||
placeholder: |
|
placeholder: |
|
||||||
Additional details and attachments.
|
Additional details and attachments.
|
||||||
|
|
||||||
- type: checkboxes
|
- type: checkboxes
|
||||||
id: acknowledgements
|
id: acknowledgements
|
||||||
attributes:
|
attributes:
|
||||||
label: Acknowledgements
|
label: Acknowledgements
|
||||||
options:
|
options:
|
||||||
- label: I have searched the existing issues and this is a new ticket, **NOT** a duplicate or related to another open issue.
|
- label: I have searched the existing issues and this is a new ticket, **NOT** a duplicate or related to another open issue.
|
||||||
required: true
|
required: true
|
||||||
|
|||||||
@ -1,33 +1,31 @@
|
|||||||
name: 🗑 Source removal request
|
name: 🗑 Source removal request
|
||||||
description: Scanlators can request their site to be removed
|
description: Scanlators can request their site to be removed
|
||||||
labels: [source removal]
|
labels: [ source removal ]
|
||||||
body:
|
body:
|
||||||
|
|
||||||
- type: input
|
- type: input
|
||||||
id: link
|
id: link
|
||||||
attributes:
|
attributes:
|
||||||
label: Source link
|
label: Source link
|
||||||
placeholder: |
|
placeholder: |
|
||||||
Example: "https://example.org"
|
Example: "https://example.org"
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
|
|
||||||
- type: textarea
|
- type: textarea
|
||||||
id: other-details
|
id: other-details
|
||||||
attributes:
|
attributes:
|
||||||
label: Other details
|
label: Other details (reason for removal, etc)
|
||||||
placeholder: |
|
placeholder: |
|
||||||
Additional details and attachments.
|
Additional details and attachments.
|
||||||
|
|
||||||
- type: checkboxes
|
- type: checkboxes
|
||||||
id: requirements
|
id: requirements
|
||||||
attributes:
|
attributes:
|
||||||
label: Requirements
|
label: Requirements
|
||||||
description: Your request will be denied if you don't meet these requirements.
|
description: Your request will be denied if you don't meet these requirements.
|
||||||
options:
|
options:
|
||||||
- label: Proof of ownership/intent to remove sent to a Kotatsu Discord server mod via DM
|
- label: Proof of ownership of the website is sent to a Kotatsu [Discord server](https://discord.gg/NNJ5RgVBC5) or [Telegram community](https://t.me/kotatsuapp)
|
||||||
required: true
|
required: true
|
||||||
- label: Site only hosts content scanlated by the group and not stolen from other scanlators or official releases (i.e., not an aggregator site)
|
- label: Site only hosts content scanlated by the group and not stolen from other scanlators or official releases (i.e., not an aggregator site)
|
||||||
required: true
|
required: true
|
||||||
- label: Site is not infested with user-hostile features (e.g., invasive or malicious ads)
|
|
||||||
required: true
|
|
||||||
|
|||||||
@ -1,50 +1,53 @@
|
|||||||
name: 🌐 Source request
|
name: 🌐 Source request
|
||||||
description: Suggest a new source for Kotatsu
|
description: Suggest a new source for Kotatsu
|
||||||
labels: [source request]
|
labels: [ source request ]
|
||||||
body:
|
body:
|
||||||
|
|
||||||
- type: input
|
- type: markdown
|
||||||
id: name
|
attributes:
|
||||||
attributes:
|
value: Please specify source **name** and **language** in the issue title
|
||||||
label: Source name
|
- type: input
|
||||||
placeholder: |
|
id: name
|
||||||
Example: "Example Scans"
|
attributes:
|
||||||
validations:
|
label: Source name
|
||||||
required: true
|
placeholder: |
|
||||||
|
Example: "Example Scans"
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
- type: input
|
- type: input
|
||||||
id: link
|
id: link
|
||||||
attributes:
|
attributes:
|
||||||
label: Source link
|
label: Source link
|
||||||
placeholder: |
|
placeholder: |
|
||||||
Example: "https://example.org"
|
Example: "https://example.org"
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
|
|
||||||
- type: input
|
- type: input
|
||||||
id: language
|
id: language
|
||||||
attributes:
|
attributes:
|
||||||
label: Language
|
label: Language
|
||||||
placeholder: |
|
placeholder: |
|
||||||
Example: "English"
|
Example: "English"
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
|
|
||||||
- type: textarea
|
- type: textarea
|
||||||
id: other-details
|
id: other-details
|
||||||
attributes:
|
attributes:
|
||||||
label: Other details
|
label: Other details
|
||||||
placeholder: |
|
placeholder: |
|
||||||
Additional details and attachments.
|
Additional details and attachments.
|
||||||
|
|
||||||
- type: checkboxes
|
- type: checkboxes
|
||||||
id: acknowledgements
|
id: acknowledgements
|
||||||
attributes:
|
attributes:
|
||||||
label: Acknowledgements
|
label: Acknowledgements
|
||||||
options:
|
options:
|
||||||
- label: I have searched the existing issues and this is a new ticket, **NOT** a duplicate or related to another open issue.
|
- label: I have searched the existing issues and this is a new ticket, **NOT** a duplicate or related to another open issue.
|
||||||
required: true
|
required: true
|
||||||
- label: I have checked that the source does not already exist on the app.
|
- label: I have checked that the source does not already exist on the app.
|
||||||
required: true
|
required: true
|
||||||
- label: I have checked that the source does not already exist by searching the [GitHub repository](https://github.com/KotatsuApp/kotatsu-parsers) and verified it does not appear in the code base.
|
- label: I have checked that the source does not already exist by searching the [GitHub repository](https://github.com/KotatsuApp/kotatsu-parsers) and verified it does not appear in the code base.
|
||||||
required: true
|
required: true
|
||||||
|
|||||||
@ -0,0 +1 @@
|
|||||||
|
total: 1256
|
||||||
@ -0,0 +1,25 @@
|
|||||||
|
name: Check & Test latest parsers
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
check-and-build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository 🌏
|
||||||
|
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
|
|
||||||
|
- name: Set up enviroment 🔧
|
||||||
|
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
|
||||||
|
with:
|
||||||
|
java-version: '21'
|
||||||
|
distribution: 'temurin'
|
||||||
|
|
||||||
|
- name: Set up Gradle 📦
|
||||||
|
uses: gradle/actions/setup-gradle@ed408507eac070d1f99cc633dbcf757c94c7933a # v4.4.3
|
||||||
|
|
||||||
|
- name: Compile parsers 🚀
|
||||||
|
run: ./gradlew compileKotlin
|
||||||
@ -1,27 +1,29 @@
|
|||||||
name: Parsers test
|
name: Parsers test for PRs
|
||||||
|
|
||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
pull_request:
|
pull_request:
|
||||||
paths:
|
paths:
|
||||||
- 'src/main/kotlin/org/koitharu/kotatsu/parsers/site/*'
|
- 'src/main/kotlin/org/koitharu/kotatsu/parsers/**'
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build-and-test:
|
build-and-test:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- name: Checkout repository 🌏
|
||||||
- uses: actions/setup-java@v3
|
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
with:
|
|
||||||
java-version: '11'
|
- name: Set up enviroment 🔧
|
||||||
distribution: 'temurin'
|
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
|
||||||
cache: 'gradle'
|
with:
|
||||||
- run: ./gradlew :test --tests "org.koitharu.kotatsu.parsers.MangaParserTest" || true
|
java-version: '21'
|
||||||
- run: ./gradlew generateTestsReport
|
distribution: 'temurin'
|
||||||
- uses: actions/upload-artifact@v3
|
|
||||||
with:
|
- name: Set up Gradle 📦
|
||||||
name: Report
|
uses: gradle/actions/setup-gradle@ed408507eac070d1f99cc633dbcf757c94c7933a # v4.4.3
|
||||||
path: build/test-results-html/TEST-org.koitharu.kotatsu.parsers.MangaParserTest.htm
|
|
||||||
|
- name: Compile parsers 🚀
|
||||||
|
run: ./gradlew compileKotlin
|
||||||
|
|||||||
@ -1,3 +1,8 @@
|
|||||||
# Default ignored files
|
# Default ignored files
|
||||||
/shelf/
|
/shelf/
|
||||||
/workspace.xml
|
/workspace.xml
|
||||||
|
# GitHub Copilot persisted chat sessions
|
||||||
|
/copilot/chatSessions
|
||||||
|
|
||||||
|
.name
|
||||||
|
deviceManager.xml
|
||||||
|
|||||||
@ -1,6 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
|
||||||
<component name="KotlinJpsPluginSettings">
|
|
||||||
<option name="version" value="1.6.21" />
|
|
||||||
</component>
|
|
||||||
</project>
|
|
||||||
@ -0,0 +1,96 @@
|
|||||||
|
# Contributing
|
||||||
|
|
||||||
|
The following is a guide for creating Kotatsu parsers. Thanks for taking the time to contribute!
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
Before you start, please note that the ability to use the following technologies is **required**.
|
||||||
|
|
||||||
|
- Basic [Android development](https://developer.android.com/)
|
||||||
|
- [Kotlin](https://kotlinlang.org/)
|
||||||
|
- Web scraping ([JSoup](https://jsoup.org/)) or JSON API
|
||||||
|
|
||||||
|
### Tools
|
||||||
|
|
||||||
|
- [Android Studio](https://developer.android.com/studio)
|
||||||
|
- [IntelliJ IDEA](https://www.jetbrains.com/idea/) (Community edition is enough)
|
||||||
|
- Android device (or emulator)
|
||||||
|
|
||||||
|
Kotatsu parsers are not a part of the Android application, but you can easily develop and test it directly inside an
|
||||||
|
Android application project and relocate it to the library project when done.
|
||||||
|
|
||||||
|
### Before you start
|
||||||
|
|
||||||
|
First, take a look at the `kotatsu-parsers` project structure. Each parser is a single class that
|
||||||
|
extends the `MangaParser` class and has a `MangaSourceParser` annotation.
|
||||||
|
Also, pay attention to extensions in the `util` package. For example, extensions from the `Jsoup` file
|
||||||
|
should be used instead of existing JSoup functions because they have better nullability support
|
||||||
|
and improved error messages.
|
||||||
|
|
||||||
|
## Writing your parser
|
||||||
|
|
||||||
|
So, you want to create a parser, that will provide access to manga from a website.
|
||||||
|
First, you should explore a website to learn about API availability.
|
||||||
|
If it does not contain any documentation about
|
||||||
|
API, [explore network requests](https://firefox-source-docs.mozilla.org/devtools-user/):
|
||||||
|
some websites use AJAX.
|
||||||
|
|
||||||
|
- [Example](https://github.com/KotatsuApp/kotatsu-parsers/blob/master/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ru/DesuMeParser.kt)
|
||||||
|
of Json API usage.
|
||||||
|
- [Example](https://github.com/KotatsuApp/kotatsu-parsers/blob/master/src/main/kotlin/org/koitharu/kotatsu/parsers/site/be/AnibelParser.kt)
|
||||||
|
of GraphQL API usage
|
||||||
|
- [Example](https://github.com/KotatsuApp/kotatsu-parsers/blob/master/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/MangaTownParser.kt)
|
||||||
|
of pure HTML parsing.
|
||||||
|
|
||||||
|
If the website is based on some engine it is rationally to use a common base class for this one (for example, Madara
|
||||||
|
Wordpress theme and the `MadaraParser` class)
|
||||||
|
|
||||||
|
### Parser class skeleton
|
||||||
|
|
||||||
|
The parser class must have exactly one primary constructor parameter of type `MangaLoaderContext` and have an
|
||||||
|
`MangaSourceParser` annotation that provides the internal name, title, and language of a manga source.
|
||||||
|
|
||||||
|
All members of the `MangaParser` class are documented. Pay attention to some peculiarities:
|
||||||
|
|
||||||
|
- Never hardcode domain. Specify the default domain in the `configKeyDomain` field and obtain an actual one using
|
||||||
|
`domain`.
|
||||||
|
- All IDs must be unique and domain-independent. Use `generateUid` functions with a relative URL or some internal id
|
||||||
|
that is unique across the manga source.
|
||||||
|
- The `availableSortOrders` set should not be empty. If your source does not support sorting, specify one most relevant
|
||||||
|
value.
|
||||||
|
- If you cannot obtain direct links to page images inside the `getPages` method, it is ok to use an intermediate URL
|
||||||
|
as `Page.url` and fetch a direct link in the `getPageUrl` function.
|
||||||
|
- You can use _asserts_ to check some optional fields. For example, the `Manga.author` field is not required, but if
|
||||||
|
your source provides this information, add `assert(it != null)`. This will not have any effect on production but help
|
||||||
|
to find issues during unit testing.
|
||||||
|
- Your parser may also implement the `Interceptor` interface for additional manipulation of all network requests and
|
||||||
|
responses, including image loading.
|
||||||
|
- If your source website (or its API) uses pages for pagination instead of offset you should extend `PagedMangaParser`
|
||||||
|
instead of `MangaParser`.
|
||||||
|
- If your source website (or its API) does not provide pagination (has only one page of content) you should extend
|
||||||
|
`SinglePageMangaParser` instead of `MangaParser` or `PagedMangaParser`.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## Development process
|
||||||
|
|
||||||
|
During the development, it is recommended (but not necessary) to write it directly
|
||||||
|
in the Kotatsu Android application project. You can use the `core.parser.DummyParser` class as a sandbox. The `Dummy`
|
||||||
|
manga source is available in the debug Kotatsu build.
|
||||||
|
|
||||||
|
Once the parser is ready you can relocate your code into the `kotatsu-parsers` library project in a `site` package and
|
||||||
|
create a Pull Request.
|
||||||
|
|
||||||
|
### Testing
|
||||||
|
|
||||||
|
It is recommended that unit tests be run before submitting a PR.
|
||||||
|
|
||||||
|
- Temporary modify the `MangaSources` annotation class: specify your parser(s) name(s) and change mode
|
||||||
|
to `EnumSource.Mode.INCLUDE`
|
||||||
|
- Run the `MangaParserTest` (`gradlew :test --tests "org.koitharu.kotatsu.parsers.MangaParserTest"`)
|
||||||
|
- Optionally, you can run the `generateTestsReport` gradle task to get a pretty readable html report from test results.
|
||||||
|
|
||||||
|
## Help
|
||||||
|
|
||||||
|
If you need help or have some questions, ask a community in our [Telegram chat](https://t.me/kotatsuapp)
|
||||||
|
or [Discord server](https://discord.gg/NNJ5RgVBC5).
|
||||||
@ -1,73 +0,0 @@
|
|||||||
import tasks.ReportGenerateTask
|
|
||||||
|
|
||||||
plugins {
|
|
||||||
id 'java-library'
|
|
||||||
id 'org.jetbrains.kotlin.jvm'
|
|
||||||
id 'com.google.devtools.ksp'
|
|
||||||
id 'maven-publish'
|
|
||||||
}
|
|
||||||
|
|
||||||
group = 'org.koitharu'
|
|
||||||
version = '1.0'
|
|
||||||
|
|
||||||
test {
|
|
||||||
useJUnitPlatform()
|
|
||||||
}
|
|
||||||
|
|
||||||
compileKotlin {
|
|
||||||
kotlinOptions {
|
|
||||||
jvmTarget = '1.8'
|
|
||||||
freeCompilerArgs += [
|
|
||||||
'-opt-in=kotlin.RequiresOptIn',
|
|
||||||
'-opt-in=kotlin.contracts.ExperimentalContracts',
|
|
||||||
'-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi',
|
|
||||||
'-opt-in=org.koitharu.kotatsu.parsers.InternalParsersApi',
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
compileTestKotlin {
|
|
||||||
kotlinOptions {
|
|
||||||
jvmTarget = '1.8'
|
|
||||||
freeCompilerArgs += [
|
|
||||||
'-opt-in=kotlin.RequiresOptIn',
|
|
||||||
'-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi',
|
|
||||||
'-opt-in=org.koitharu.kotatsu.parsers.InternalParsersApi',
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
kotlin {
|
|
||||||
sourceSets {
|
|
||||||
main.kotlin.srcDirs += 'build/generated/ksp/main/kotlin'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
afterEvaluate {
|
|
||||||
publishing {
|
|
||||||
publications {
|
|
||||||
mavenJava(MavenPublication) {
|
|
||||||
from components.java
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
dependencies {
|
|
||||||
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4'
|
|
||||||
implementation 'com.squareup.okhttp3:okhttp:4.10.0'
|
|
||||||
implementation 'com.squareup.okio:okio:3.2.0'
|
|
||||||
api 'org.jsoup:jsoup:1.15.2'
|
|
||||||
implementation 'org.json:json:20220320'
|
|
||||||
implementation 'androidx.collection:collection-ktx:1.2.0'
|
|
||||||
|
|
||||||
ksp project(':kotatsu-parsers-ksp')
|
|
||||||
|
|
||||||
testImplementation 'org.junit.jupiter:junit-jupiter-api:5.9.0'
|
|
||||||
testImplementation 'org.junit.jupiter:junit-jupiter-engine:5.9.0'
|
|
||||||
testImplementation 'org.junit.jupiter:junit-jupiter-params:5.9.0'
|
|
||||||
testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.4'
|
|
||||||
testImplementation 'io.webfolder:quickjs:1.1.0'
|
|
||||||
}
|
|
||||||
|
|
||||||
task generateTestsReport(type: ReportGenerateTask)
|
|
||||||
@ -0,0 +1,64 @@
|
|||||||
|
import tasks.ReportGenerateTask
|
||||||
|
|
||||||
|
plugins {
|
||||||
|
`java-library`
|
||||||
|
`maven-publish`
|
||||||
|
alias(libs.plugins.kotlin.jvm)
|
||||||
|
alias(libs.plugins.ksp)
|
||||||
|
}
|
||||||
|
|
||||||
|
group = "org.koitharu"
|
||||||
|
version = "1.0"
|
||||||
|
|
||||||
|
tasks.test {
|
||||||
|
useJUnitPlatform()
|
||||||
|
}
|
||||||
|
|
||||||
|
ksp {
|
||||||
|
arg("summaryOutputDir", "${projectDir}/.github")
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile>().configureEach {
|
||||||
|
compilerOptions {
|
||||||
|
freeCompilerArgs.addAll(
|
||||||
|
"-opt-in=kotlin.RequiresOptIn",
|
||||||
|
"-opt-in=kotlin.contracts.ExperimentalContracts",
|
||||||
|
"-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi",
|
||||||
|
"-opt-in=org.koitharu.kotatsu.parsers.InternalParsersApi",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
kotlin {
|
||||||
|
jvmToolchain(8)
|
||||||
|
explicitApiWarning()
|
||||||
|
sourceSets["main"].kotlin.srcDirs("build/generated/ksp/main/kotlin")
|
||||||
|
}
|
||||||
|
|
||||||
|
publishing {
|
||||||
|
publications {
|
||||||
|
create<MavenPublication>("mavenJava") {
|
||||||
|
from(components["java"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation(libs.kotlinx.coroutines.core)
|
||||||
|
implementation(libs.okhttp)
|
||||||
|
implementation(libs.okio)
|
||||||
|
implementation(libs.json)
|
||||||
|
implementation(libs.androidx.collection)
|
||||||
|
api(libs.jsoup)
|
||||||
|
|
||||||
|
ksp(project(":kotatsu-parsers-ksp"))
|
||||||
|
|
||||||
|
testImplementation(libs.junit.api)
|
||||||
|
testImplementation(libs.junit.engine)
|
||||||
|
testImplementation(libs.junit.params)
|
||||||
|
testRuntimeOnly(libs.junit.launcher)
|
||||||
|
testImplementation(libs.kotlinx.coroutines.test)
|
||||||
|
testImplementation(libs.quickjs)
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.register<ReportGenerateTask>("generateTestsReport")
|
||||||
@ -1,13 +0,0 @@
|
|||||||
plugins {
|
|
||||||
id('org.jetbrains.kotlin.jvm') version '1.6.21'
|
|
||||||
}
|
|
||||||
repositories {
|
|
||||||
mavenCentral()
|
|
||||||
}
|
|
||||||
|
|
||||||
dependencies {
|
|
||||||
implementation gradleApi()
|
|
||||||
implementation 'org.simpleframework:simple-xml:2.7.1'
|
|
||||||
implementation 'com.soywiz.korlibs.korte:korte-jvm:3.0.0-Beta5'
|
|
||||||
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.3'
|
|
||||||
}
|
|
||||||
@ -0,0 +1,17 @@
|
|||||||
|
plugins {
|
||||||
|
`kotlin-dsl`
|
||||||
|
}
|
||||||
|
|
||||||
|
repositories {
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
|
||||||
|
kotlin {
|
||||||
|
jvmToolchain(8)
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation(libs.korte)
|
||||||
|
implementation(libs.simplexml)
|
||||||
|
implementation(libs.kotlinx.coroutines.core)
|
||||||
|
}
|
||||||
Binary file not shown.
@ -1,5 +1,7 @@
|
|||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-bin.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-9.0.0-bin.zip
|
||||||
|
networkTimeout=10000
|
||||||
|
validateDistributionUrl=true
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
zipStorePath=wrapper/dists
|
zipStorePath=wrapper/dists
|
||||||
|
|||||||
@ -0,0 +1,7 @@
|
|||||||
|
dependencyResolutionManagement {
|
||||||
|
versionCatalogs {
|
||||||
|
create("libs") {
|
||||||
|
from(files("../gradle/libs.versions.toml"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,804 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<gaphor xmlns="https://gaphor.org/model" xmlns:Core="https://gaphor.org/modelinglanguage/Core" xmlns:UML="https://gaphor.org/modelinglanguage/UML" xmlns:general="https://gaphor.org/modelinglanguage/general" version="4" gaphor-version="3.1.0">
|
||||||
|
<model>
|
||||||
|
<Core:StyleSheet id="58d6989a-66f8-11ec-b4c8-0456e5e540ed"/>
|
||||||
|
<UML:Package id="58d6c2e8-66f8-11ec-b4c8-0456e5e540ed">
|
||||||
|
<name>
|
||||||
|
<val>Новая модель</val>
|
||||||
|
</name>
|
||||||
|
<ownedDiagram>
|
||||||
|
<reflist>
|
||||||
|
<ref refid="58d6c536-66f8-11ec-b4c8-0456e5e540ed"/>
|
||||||
|
</reflist>
|
||||||
|
</ownedDiagram>
|
||||||
|
<ownedType>
|
||||||
|
<reflist>
|
||||||
|
<ref refid="0b54a350-f59d-11ef-bfb1-4cbb5880a0b8"/>
|
||||||
|
<ref refid="a300f58a-f5bd-11ef-9ec2-4cbb5880a0b8"/>
|
||||||
|
<ref refid="ad4c68d0-f5bd-11ef-9ec2-4cbb5880a0b8"/>
|
||||||
|
<ref refid="198a3108-f5be-11ef-9ec2-4cbb5880a0b8"/>
|
||||||
|
<ref refid="32081654-f5bf-11ef-9ec2-4cbb5880a0b8"/>
|
||||||
|
</reflist>
|
||||||
|
</ownedType>
|
||||||
|
<packagedElement>
|
||||||
|
<reflist>
|
||||||
|
<ref refid="0b54a350-f59d-11ef-bfb1-4cbb5880a0b8"/>
|
||||||
|
<ref refid="a300f58a-f5bd-11ef-9ec2-4cbb5880a0b8"/>
|
||||||
|
<ref refid="ad4c68d0-f5bd-11ef-9ec2-4cbb5880a0b8"/>
|
||||||
|
<ref refid="198a3108-f5be-11ef-9ec2-4cbb5880a0b8"/>
|
||||||
|
<ref refid="32081654-f5bf-11ef-9ec2-4cbb5880a0b8"/>
|
||||||
|
<ref refid="25a17c58-f5be-11ef-9ec2-4cbb5880a0b8"/>
|
||||||
|
<ref refid="41318a02-f5bf-11ef-9ec2-4cbb5880a0b8"/>
|
||||||
|
</reflist>
|
||||||
|
</packagedElement>
|
||||||
|
</UML:Package>
|
||||||
|
<UML:Diagram id="58d6c536-66f8-11ec-b4c8-0456e5e540ed">
|
||||||
|
<element>
|
||||||
|
<ref refid="58d6c2e8-66f8-11ec-b4c8-0456e5e540ed"/>
|
||||||
|
</element>
|
||||||
|
<name>
|
||||||
|
<val>Новая диаграмма</val>
|
||||||
|
</name>
|
||||||
|
<ownedPresentation>
|
||||||
|
<reflist>
|
||||||
|
<ref refid="0b54edc4-f59d-11ef-bfb1-4cbb5880a0b8"/>
|
||||||
|
<ref refid="a3018fc2-f5bd-11ef-9ec2-4cbb5880a0b8"/>
|
||||||
|
<ref refid="ad4d2aae-f5bd-11ef-9ec2-4cbb5880a0b8"/>
|
||||||
|
<ref refid="198aace6-f5be-11ef-9ec2-4cbb5880a0b8"/>
|
||||||
|
<ref refid="320868c0-f5bf-11ef-9ec2-4cbb5880a0b8"/>
|
||||||
|
<ref refid="b760bcd4-f5bf-11ef-9ec2-4cbb5880a0b8"/>
|
||||||
|
<ref refid="bff32eae-f5bf-11ef-9ec2-4cbb5880a0b8"/>
|
||||||
|
<ref refid="d5113ca4-f5bf-11ef-9ec2-4cbb5880a0b8"/>
|
||||||
|
<ref refid="e8042ad8-f5bf-11ef-9ec2-4cbb5880a0b8"/>
|
||||||
|
<ref refid="531831f2-f5c0-11ef-9ec2-4cbb5880a0b8"/>
|
||||||
|
<ref refid="5ccae8d4-f5c0-11ef-9ec2-4cbb5880a0b8"/>
|
||||||
|
<ref refid="b6e0240e-f5bd-11ef-9ec2-4cbb5880a0b8"/>
|
||||||
|
<ref refid="254e40f6-f5be-11ef-9ec2-4cbb5880a0b8"/>
|
||||||
|
<ref refid="2bb6e87a-f5bf-11ef-9ec2-4cbb5880a0b8"/>
|
||||||
|
<ref refid="40e36d2c-f5bf-11ef-9ec2-4cbb5880a0b8"/>
|
||||||
|
<ref refid="bcc07c64-f5bf-11ef-9ec2-4cbb5880a0b8"/>
|
||||||
|
<ref refid="f6b48e4c-f5bf-11ef-9ec2-4cbb5880a0b8"/>
|
||||||
|
<ref refid="f89ba010-f5bf-11ef-9ec2-4cbb5880a0b8"/>
|
||||||
|
<ref refid="fabe0540-f5bf-11ef-9ec2-4cbb5880a0b8"/>
|
||||||
|
<ref refid="6f993af6-f5c0-11ef-9ec2-4cbb5880a0b8"/>
|
||||||
|
</reflist>
|
||||||
|
</ownedPresentation>
|
||||||
|
</UML:Diagram>
|
||||||
|
<UML:Class id="0b54a350-f59d-11ef-bfb1-4cbb5880a0b8">
|
||||||
|
<clientDependency>
|
||||||
|
<reflist>
|
||||||
|
<ref refid="25a17c58-f5be-11ef-9ec2-4cbb5880a0b8"/>
|
||||||
|
</reflist>
|
||||||
|
</clientDependency>
|
||||||
|
<comment>
|
||||||
|
<reflist>
|
||||||
|
<ref refid="bff31afe-f5bf-11ef-9ec2-4cbb5880a0b8"/>
|
||||||
|
</reflist>
|
||||||
|
</comment>
|
||||||
|
<isAbstract>
|
||||||
|
<val>1</val>
|
||||||
|
</isAbstract>
|
||||||
|
<name>
|
||||||
|
<val>AbstractMangaParser</val>
|
||||||
|
</name>
|
||||||
|
<owningPackage>
|
||||||
|
<ref refid="58d6c2e8-66f8-11ec-b4c8-0456e5e540ed"/>
|
||||||
|
</owningPackage>
|
||||||
|
<package>
|
||||||
|
<ref refid="58d6c2e8-66f8-11ec-b4c8-0456e5e540ed"/>
|
||||||
|
</package>
|
||||||
|
<presentation>
|
||||||
|
<reflist>
|
||||||
|
<ref refid="0b54edc4-f59d-11ef-bfb1-4cbb5880a0b8"/>
|
||||||
|
</reflist>
|
||||||
|
</presentation>
|
||||||
|
<specialization>
|
||||||
|
<reflist>
|
||||||
|
<ref refid="b969dac6-f5bd-11ef-9ec2-4cbb5880a0b8"/>
|
||||||
|
<ref refid="2c236356-f5bf-11ef-9ec2-4cbb5880a0b8"/>
|
||||||
|
</reflist>
|
||||||
|
</specialization>
|
||||||
|
</UML:Class>
|
||||||
|
<UML:ClassItem id="0b54edc4-f59d-11ef-bfb1-4cbb5880a0b8">
|
||||||
|
<matrix>
|
||||||
|
<val>(1.0, 0.0, 0.0, 1.0, 405.16796875, 388.8671875)</val>
|
||||||
|
</matrix>
|
||||||
|
<top-left>
|
||||||
|
<val>(0.0, 0.0)</val>
|
||||||
|
</top-left>
|
||||||
|
<width>
|
||||||
|
<val>158.0</val>
|
||||||
|
</width>
|
||||||
|
<height>
|
||||||
|
<val>60.0</val>
|
||||||
|
</height>
|
||||||
|
<diagram>
|
||||||
|
<ref refid="58d6c536-66f8-11ec-b4c8-0456e5e540ed"/>
|
||||||
|
</diagram>
|
||||||
|
<show_attributes>
|
||||||
|
<val>0</val>
|
||||||
|
</show_attributes>
|
||||||
|
<show_operations>
|
||||||
|
<val>0</val>
|
||||||
|
</show_operations>
|
||||||
|
<subject>
|
||||||
|
<ref refid="0b54a350-f59d-11ef-bfb1-4cbb5880a0b8"/>
|
||||||
|
</subject>
|
||||||
|
</UML:ClassItem>
|
||||||
|
<UML:Class id="a300f58a-f5bd-11ef-9ec2-4cbb5880a0b8">
|
||||||
|
<comment>
|
||||||
|
<reflist>
|
||||||
|
<ref refid="d5112a70-f5bf-11ef-9ec2-4cbb5880a0b8"/>
|
||||||
|
</reflist>
|
||||||
|
</comment>
|
||||||
|
<generalization>
|
||||||
|
<reflist>
|
||||||
|
<ref refid="b969dac6-f5bd-11ef-9ec2-4cbb5880a0b8"/>
|
||||||
|
</reflist>
|
||||||
|
</generalization>
|
||||||
|
<isAbstract>
|
||||||
|
<val>1</val>
|
||||||
|
</isAbstract>
|
||||||
|
<name>
|
||||||
|
<val>PagedMangaParser</val>
|
||||||
|
</name>
|
||||||
|
<owningPackage>
|
||||||
|
<ref refid="58d6c2e8-66f8-11ec-b4c8-0456e5e540ed"/>
|
||||||
|
</owningPackage>
|
||||||
|
<package>
|
||||||
|
<ref refid="58d6c2e8-66f8-11ec-b4c8-0456e5e540ed"/>
|
||||||
|
</package>
|
||||||
|
<presentation>
|
||||||
|
<reflist>
|
||||||
|
<ref refid="a3018fc2-f5bd-11ef-9ec2-4cbb5880a0b8"/>
|
||||||
|
</reflist>
|
||||||
|
</presentation>
|
||||||
|
</UML:Class>
|
||||||
|
<UML:ClassItem id="a3018fc2-f5bd-11ef-9ec2-4cbb5880a0b8">
|
||||||
|
<matrix>
|
||||||
|
<val>(1.0, 0.0, 0.0, 1.0, 476.3368367667698, 525.76953125)</val>
|
||||||
|
</matrix>
|
||||||
|
<top-left>
|
||||||
|
<val>(0.0, 0.0)</val>
|
||||||
|
</top-left>
|
||||||
|
<width>
|
||||||
|
<val>142.0</val>
|
||||||
|
</width>
|
||||||
|
<height>
|
||||||
|
<val>60.0</val>
|
||||||
|
</height>
|
||||||
|
<diagram>
|
||||||
|
<ref refid="58d6c536-66f8-11ec-b4c8-0456e5e540ed"/>
|
||||||
|
</diagram>
|
||||||
|
<show_attributes>
|
||||||
|
<val>0</val>
|
||||||
|
</show_attributes>
|
||||||
|
<show_operations>
|
||||||
|
<val>0</val>
|
||||||
|
</show_operations>
|
||||||
|
<subject>
|
||||||
|
<ref refid="a300f58a-f5bd-11ef-9ec2-4cbb5880a0b8"/>
|
||||||
|
</subject>
|
||||||
|
</UML:ClassItem>
|
||||||
|
<UML:Class id="ad4c68d0-f5bd-11ef-9ec2-4cbb5880a0b8">
|
||||||
|
<comment>
|
||||||
|
<reflist>
|
||||||
|
<ref refid="e80418f4-f5bf-11ef-9ec2-4cbb5880a0b8"/>
|
||||||
|
</reflist>
|
||||||
|
</comment>
|
||||||
|
<generalization>
|
||||||
|
<reflist>
|
||||||
|
<ref refid="2c236356-f5bf-11ef-9ec2-4cbb5880a0b8"/>
|
||||||
|
</reflist>
|
||||||
|
</generalization>
|
||||||
|
<isAbstract>
|
||||||
|
<val>1</val>
|
||||||
|
</isAbstract>
|
||||||
|
<name>
|
||||||
|
<val>SinglePageMangaParser</val>
|
||||||
|
</name>
|
||||||
|
<owningPackage>
|
||||||
|
<ref refid="58d6c2e8-66f8-11ec-b4c8-0456e5e540ed"/>
|
||||||
|
</owningPackage>
|
||||||
|
<package>
|
||||||
|
<ref refid="58d6c2e8-66f8-11ec-b4c8-0456e5e540ed"/>
|
||||||
|
</package>
|
||||||
|
<presentation>
|
||||||
|
<reflist>
|
||||||
|
<ref refid="ad4d2aae-f5bd-11ef-9ec2-4cbb5880a0b8"/>
|
||||||
|
</reflist>
|
||||||
|
</presentation>
|
||||||
|
</UML:Class>
|
||||||
|
<UML:ClassItem id="ad4d2aae-f5bd-11ef-9ec2-4cbb5880a0b8">
|
||||||
|
<matrix>
|
||||||
|
<val>(1.0, 0.0, 0.0, 1.0, 405.16796875, 627.46875)</val>
|
||||||
|
</matrix>
|
||||||
|
<top-left>
|
||||||
|
<val>(0.0, 0.0)</val>
|
||||||
|
</top-left>
|
||||||
|
<width>
|
||||||
|
<val>175.0</val>
|
||||||
|
</width>
|
||||||
|
<height>
|
||||||
|
<val>60.0</val>
|
||||||
|
</height>
|
||||||
|
<diagram>
|
||||||
|
<ref refid="58d6c536-66f8-11ec-b4c8-0456e5e540ed"/>
|
||||||
|
</diagram>
|
||||||
|
<show_attributes>
|
||||||
|
<val>0</val>
|
||||||
|
</show_attributes>
|
||||||
|
<show_operations>
|
||||||
|
<val>0</val>
|
||||||
|
</show_operations>
|
||||||
|
<subject>
|
||||||
|
<ref refid="ad4c68d0-f5bd-11ef-9ec2-4cbb5880a0b8"/>
|
||||||
|
</subject>
|
||||||
|
</UML:ClassItem>
|
||||||
|
<UML:GeneralizationItem id="b6e0240e-f5bd-11ef-9ec2-4cbb5880a0b8">
|
||||||
|
<diagram>
|
||||||
|
<ref refid="58d6c536-66f8-11ec-b4c8-0456e5e540ed"/>
|
||||||
|
</diagram>
|
||||||
|
<horizontal>
|
||||||
|
<val>False</val>
|
||||||
|
</horizontal>
|
||||||
|
<orthogonal>
|
||||||
|
<val>False</val>
|
||||||
|
</orthogonal>
|
||||||
|
<subject>
|
||||||
|
<ref refid="b969dac6-f5bd-11ef-9ec2-4cbb5880a0b8"/>
|
||||||
|
</subject>
|
||||||
|
<matrix>
|
||||||
|
<val>(1.0, 0.0, 0.0, 1.0, 499.2109069824219, 463.45703125)</val>
|
||||||
|
</matrix>
|
||||||
|
<points>
|
||||||
|
<val>[(28.486861756586336, 62.3125), (25.111328125, -14.58984375)]</val>
|
||||||
|
</points>
|
||||||
|
<head-connection>
|
||||||
|
<ref refid="a3018fc2-f5bd-11ef-9ec2-4cbb5880a0b8"/>
|
||||||
|
</head-connection>
|
||||||
|
<tail-connection>
|
||||||
|
<ref refid="0b54edc4-f59d-11ef-bfb1-4cbb5880a0b8"/>
|
||||||
|
</tail-connection>
|
||||||
|
</UML:GeneralizationItem>
|
||||||
|
<UML:Generalization id="b969dac6-f5bd-11ef-9ec2-4cbb5880a0b8">
|
||||||
|
<general>
|
||||||
|
<ref refid="0b54a350-f59d-11ef-bfb1-4cbb5880a0b8"/>
|
||||||
|
</general>
|
||||||
|
<presentation>
|
||||||
|
<reflist>
|
||||||
|
<ref refid="b6e0240e-f5bd-11ef-9ec2-4cbb5880a0b8"/>
|
||||||
|
</reflist>
|
||||||
|
</presentation>
|
||||||
|
<specific>
|
||||||
|
<ref refid="a300f58a-f5bd-11ef-9ec2-4cbb5880a0b8"/>
|
||||||
|
</specific>
|
||||||
|
</UML:Generalization>
|
||||||
|
<UML:Interface id="198a3108-f5be-11ef-9ec2-4cbb5880a0b8">
|
||||||
|
<name>
|
||||||
|
<val>MangaParser</val>
|
||||||
|
</name>
|
||||||
|
<owningPackage>
|
||||||
|
<ref refid="58d6c2e8-66f8-11ec-b4c8-0456e5e540ed"/>
|
||||||
|
</owningPackage>
|
||||||
|
<package>
|
||||||
|
<ref refid="58d6c2e8-66f8-11ec-b4c8-0456e5e540ed"/>
|
||||||
|
</package>
|
||||||
|
<presentation>
|
||||||
|
<reflist>
|
||||||
|
<ref refid="198aace6-f5be-11ef-9ec2-4cbb5880a0b8"/>
|
||||||
|
</reflist>
|
||||||
|
</presentation>
|
||||||
|
<supplierDependency>
|
||||||
|
<reflist>
|
||||||
|
<ref refid="25a17c58-f5be-11ef-9ec2-4cbb5880a0b8"/>
|
||||||
|
<ref refid="41318a02-f5bf-11ef-9ec2-4cbb5880a0b8"/>
|
||||||
|
</reflist>
|
||||||
|
</supplierDependency>
|
||||||
|
</UML:Interface>
|
||||||
|
<UML:InterfaceItem id="198aace6-f5be-11ef-9ec2-4cbb5880a0b8">
|
||||||
|
<matrix>
|
||||||
|
<val>(1.0, 0.0, 0.0, 1.0, 278.00391387939453, 232.92578125)</val>
|
||||||
|
</matrix>
|
||||||
|
<top-left>
|
||||||
|
<val>(0.0, 0.0)</val>
|
||||||
|
</top-left>
|
||||||
|
<width>
|
||||||
|
<val>105.0</val>
|
||||||
|
</width>
|
||||||
|
<height>
|
||||||
|
<val>80.0</val>
|
||||||
|
</height>
|
||||||
|
<diagram>
|
||||||
|
<ref refid="58d6c536-66f8-11ec-b4c8-0456e5e540ed"/>
|
||||||
|
</diagram>
|
||||||
|
<show_attributes>
|
||||||
|
<val>0</val>
|
||||||
|
</show_attributes>
|
||||||
|
<show_operations>
|
||||||
|
<val>0</val>
|
||||||
|
</show_operations>
|
||||||
|
<subject>
|
||||||
|
<ref refid="198a3108-f5be-11ef-9ec2-4cbb5880a0b8"/>
|
||||||
|
</subject>
|
||||||
|
<folded>
|
||||||
|
<val>0</val>
|
||||||
|
</folded>
|
||||||
|
</UML:InterfaceItem>
|
||||||
|
<UML:InterfaceRealizationItem id="254e40f6-f5be-11ef-9ec2-4cbb5880a0b8">
|
||||||
|
<diagram>
|
||||||
|
<ref refid="58d6c536-66f8-11ec-b4c8-0456e5e540ed"/>
|
||||||
|
</diagram>
|
||||||
|
<horizontal>
|
||||||
|
<val>False</val>
|
||||||
|
</horizontal>
|
||||||
|
<orthogonal>
|
||||||
|
<val>False</val>
|
||||||
|
</orthogonal>
|
||||||
|
<subject>
|
||||||
|
<ref refid="25a17c58-f5be-11ef-9ec2-4cbb5880a0b8"/>
|
||||||
|
</subject>
|
||||||
|
<matrix>
|
||||||
|
<val>(1.0, 0.0, 0.0, 1.0, 306.1445007324219, 270.0625)</val>
|
||||||
|
</matrix>
|
||||||
|
<points>
|
||||||
|
<val>[(55.866059373910275, 42.86328125), (164.5765002560883, 118.8046875)]</val>
|
||||||
|
</points>
|
||||||
|
<head-connection>
|
||||||
|
<ref refid="198aace6-f5be-11ef-9ec2-4cbb5880a0b8"/>
|
||||||
|
</head-connection>
|
||||||
|
<tail-connection>
|
||||||
|
<ref refid="0b54edc4-f59d-11ef-bfb1-4cbb5880a0b8"/>
|
||||||
|
</tail-connection>
|
||||||
|
</UML:InterfaceRealizationItem>
|
||||||
|
<UML:InterfaceRealization id="25a17c58-f5be-11ef-9ec2-4cbb5880a0b8">
|
||||||
|
<client>
|
||||||
|
<reflist>
|
||||||
|
<ref refid="0b54a350-f59d-11ef-bfb1-4cbb5880a0b8"/>
|
||||||
|
</reflist>
|
||||||
|
</client>
|
||||||
|
<owningPackage>
|
||||||
|
<ref refid="58d6c2e8-66f8-11ec-b4c8-0456e5e540ed"/>
|
||||||
|
</owningPackage>
|
||||||
|
<presentation>
|
||||||
|
<reflist>
|
||||||
|
<ref refid="254e40f6-f5be-11ef-9ec2-4cbb5880a0b8"/>
|
||||||
|
</reflist>
|
||||||
|
</presentation>
|
||||||
|
<supplier>
|
||||||
|
<reflist>
|
||||||
|
<ref refid="198a3108-f5be-11ef-9ec2-4cbb5880a0b8"/>
|
||||||
|
</reflist>
|
||||||
|
</supplier>
|
||||||
|
</UML:InterfaceRealization>
|
||||||
|
<UML:GeneralizationItem id="2bb6e87a-f5bf-11ef-9ec2-4cbb5880a0b8">
|
||||||
|
<diagram>
|
||||||
|
<ref refid="58d6c536-66f8-11ec-b4c8-0456e5e540ed"/>
|
||||||
|
</diagram>
|
||||||
|
<horizontal>
|
||||||
|
<val>False</val>
|
||||||
|
</horizontal>
|
||||||
|
<orthogonal>
|
||||||
|
<val>False</val>
|
||||||
|
</orthogonal>
|
||||||
|
<subject>
|
||||||
|
<ref refid="2c236356-f5bf-11ef-9ec2-4cbb5880a0b8"/>
|
||||||
|
</subject>
|
||||||
|
<matrix>
|
||||||
|
<val>(1.0, 0.0, 0.0, 1.0, 436.2929382324219, 439.1913757324219)</val>
|
||||||
|
</matrix>
|
||||||
|
<points>
|
||||||
|
<val>[(20.37646032737257, 188.27737426757812), (18.488327026367188, 9.675811767578125)]</val>
|
||||||
|
</points>
|
||||||
|
<head-connection>
|
||||||
|
<ref refid="ad4d2aae-f5bd-11ef-9ec2-4cbb5880a0b8"/>
|
||||||
|
</head-connection>
|
||||||
|
<tail-connection>
|
||||||
|
<ref refid="0b54edc4-f59d-11ef-bfb1-4cbb5880a0b8"/>
|
||||||
|
</tail-connection>
|
||||||
|
</UML:GeneralizationItem>
|
||||||
|
<UML:Generalization id="2c236356-f5bf-11ef-9ec2-4cbb5880a0b8">
|
||||||
|
<general>
|
||||||
|
<ref refid="0b54a350-f59d-11ef-bfb1-4cbb5880a0b8"/>
|
||||||
|
</general>
|
||||||
|
<presentation>
|
||||||
|
<reflist>
|
||||||
|
<ref refid="2bb6e87a-f5bf-11ef-9ec2-4cbb5880a0b8"/>
|
||||||
|
</reflist>
|
||||||
|
</presentation>
|
||||||
|
<specific>
|
||||||
|
<ref refid="ad4c68d0-f5bd-11ef-9ec2-4cbb5880a0b8"/>
|
||||||
|
</specific>
|
||||||
|
</UML:Generalization>
|
||||||
|
<UML:Class id="32081654-f5bf-11ef-9ec2-4cbb5880a0b8">
|
||||||
|
<clientDependency>
|
||||||
|
<reflist>
|
||||||
|
<ref refid="41318a02-f5bf-11ef-9ec2-4cbb5880a0b8"/>
|
||||||
|
</reflist>
|
||||||
|
</clientDependency>
|
||||||
|
<name>
|
||||||
|
<val>MangaParserWrapper</val>
|
||||||
|
</name>
|
||||||
|
<note>
|
||||||
|
<val></val>
|
||||||
|
</note>
|
||||||
|
<owningPackage>
|
||||||
|
<ref refid="58d6c2e8-66f8-11ec-b4c8-0456e5e540ed"/>
|
||||||
|
</owningPackage>
|
||||||
|
<package>
|
||||||
|
<ref refid="58d6c2e8-66f8-11ec-b4c8-0456e5e540ed"/>
|
||||||
|
</package>
|
||||||
|
<presentation>
|
||||||
|
<reflist>
|
||||||
|
<ref refid="320868c0-f5bf-11ef-9ec2-4cbb5880a0b8"/>
|
||||||
|
</reflist>
|
||||||
|
</presentation>
|
||||||
|
</UML:Class>
|
||||||
|
<UML:ClassItem id="320868c0-f5bf-11ef-9ec2-4cbb5880a0b8">
|
||||||
|
<matrix>
|
||||||
|
<val>(1.0, 0.0, 0.0, 1.0, 128.5008992667698, 410.48990205860804)</val>
|
||||||
|
</matrix>
|
||||||
|
<top-left>
|
||||||
|
<val>(0.0, 0.0)</val>
|
||||||
|
</top-left>
|
||||||
|
<width>
|
||||||
|
<val>158.0</val>
|
||||||
|
</width>
|
||||||
|
<height>
|
||||||
|
<val>60.0</val>
|
||||||
|
</height>
|
||||||
|
<diagram>
|
||||||
|
<ref refid="58d6c536-66f8-11ec-b4c8-0456e5e540ed"/>
|
||||||
|
</diagram>
|
||||||
|
<show_attributes>
|
||||||
|
<val>0</val>
|
||||||
|
</show_attributes>
|
||||||
|
<show_operations>
|
||||||
|
<val>0</val>
|
||||||
|
</show_operations>
|
||||||
|
<subject>
|
||||||
|
<ref refid="32081654-f5bf-11ef-9ec2-4cbb5880a0b8"/>
|
||||||
|
</subject>
|
||||||
|
</UML:ClassItem>
|
||||||
|
<UML:InterfaceRealizationItem id="40e36d2c-f5bf-11ef-9ec2-4cbb5880a0b8">
|
||||||
|
<diagram>
|
||||||
|
<ref refid="58d6c536-66f8-11ec-b4c8-0456e5e540ed"/>
|
||||||
|
</diagram>
|
||||||
|
<horizontal>
|
||||||
|
<val>False</val>
|
||||||
|
</horizontal>
|
||||||
|
<orthogonal>
|
||||||
|
<val>False</val>
|
||||||
|
</orthogonal>
|
||||||
|
<subject>
|
||||||
|
<ref refid="41318a02-f5bf-11ef-9ec2-4cbb5880a0b8"/>
|
||||||
|
</subject>
|
||||||
|
<matrix>
|
||||||
|
<val>(1.0, 0.0, 0.0, 1.0, 306.0585632324219, 249.69920349121094)</val>
|
||||||
|
</matrix>
|
||||||
|
<points>
|
||||||
|
<val>[(11.759223915218172, 63.22657775878906), (-98.55766396565207, 160.7906985673971)]</val>
|
||||||
|
</points>
|
||||||
|
<head-connection>
|
||||||
|
<ref refid="198aace6-f5be-11ef-9ec2-4cbb5880a0b8"/>
|
||||||
|
</head-connection>
|
||||||
|
<tail-connection>
|
||||||
|
<ref refid="320868c0-f5bf-11ef-9ec2-4cbb5880a0b8"/>
|
||||||
|
</tail-connection>
|
||||||
|
</UML:InterfaceRealizationItem>
|
||||||
|
<UML:InterfaceRealization id="41318a02-f5bf-11ef-9ec2-4cbb5880a0b8">
|
||||||
|
<client>
|
||||||
|
<reflist>
|
||||||
|
<ref refid="32081654-f5bf-11ef-9ec2-4cbb5880a0b8"/>
|
||||||
|
</reflist>
|
||||||
|
</client>
|
||||||
|
<owningPackage>
|
||||||
|
<ref refid="58d6c2e8-66f8-11ec-b4c8-0456e5e540ed"/>
|
||||||
|
</owningPackage>
|
||||||
|
<presentation>
|
||||||
|
<reflist>
|
||||||
|
<ref refid="40e36d2c-f5bf-11ef-9ec2-4cbb5880a0b8"/>
|
||||||
|
</reflist>
|
||||||
|
</presentation>
|
||||||
|
<supplier>
|
||||||
|
<reflist>
|
||||||
|
<ref refid="198a3108-f5be-11ef-9ec2-4cbb5880a0b8"/>
|
||||||
|
</reflist>
|
||||||
|
</supplier>
|
||||||
|
</UML:InterfaceRealization>
|
||||||
|
<UML:Comment id="b760ac44-f5bf-11ef-9ec2-4cbb5880a0b8">
|
||||||
|
<body>
|
||||||
|
<val>Used for providing external api. Do not use this class directly</val>
|
||||||
|
</body>
|
||||||
|
<presentation>
|
||||||
|
<reflist>
|
||||||
|
<ref refid="b760bcd4-f5bf-11ef-9ec2-4cbb5880a0b8"/>
|
||||||
|
</reflist>
|
||||||
|
</presentation>
|
||||||
|
</UML:Comment>
|
||||||
|
<UML:CommentItem id="b760bcd4-f5bf-11ef-9ec2-4cbb5880a0b8">
|
||||||
|
<matrix>
|
||||||
|
<val>(1.0, 0.0, 0.0, 1.0, 108.0561294327963, 550.1347579956054)</val>
|
||||||
|
</matrix>
|
||||||
|
<top-left>
|
||||||
|
<val>(0.0, 0.0)</val>
|
||||||
|
</top-left>
|
||||||
|
<width>
|
||||||
|
<val>183.21868896484375</val>
|
||||||
|
</width>
|
||||||
|
<height>
|
||||||
|
<val>91.23829650878906</val>
|
||||||
|
</height>
|
||||||
|
<diagram>
|
||||||
|
<ref refid="58d6c536-66f8-11ec-b4c8-0456e5e540ed"/>
|
||||||
|
</diagram>
|
||||||
|
<subject>
|
||||||
|
<ref refid="b760ac44-f5bf-11ef-9ec2-4cbb5880a0b8"/>
|
||||||
|
</subject>
|
||||||
|
</UML:CommentItem>
|
||||||
|
<UML:CommentLineItem id="bcc07c64-f5bf-11ef-9ec2-4cbb5880a0b8">
|
||||||
|
<diagram>
|
||||||
|
<ref refid="58d6c536-66f8-11ec-b4c8-0456e5e540ed"/>
|
||||||
|
</diagram>
|
||||||
|
<horizontal>
|
||||||
|
<val>False</val>
|
||||||
|
</horizontal>
|
||||||
|
<orthogonal>
|
||||||
|
<val>False</val>
|
||||||
|
</orthogonal>
|
||||||
|
<matrix>
|
||||||
|
<val>(1.0, 0.0, 0.0, 1.0, 549.205520203852, 278.05499559311954)</val>
|
||||||
|
</matrix>
|
||||||
|
<points>
|
||||||
|
<val>[(-349.5400462886338, 192.4349064654885), (-349.5400462886338, 272.0797624024858)]</val>
|
||||||
|
</points>
|
||||||
|
<tail-connection>
|
||||||
|
<ref refid="b760bcd4-f5bf-11ef-9ec2-4cbb5880a0b8"/>
|
||||||
|
</tail-connection>
|
||||||
|
</UML:CommentLineItem>
|
||||||
|
<UML:Comment id="bff31afe-f5bf-11ef-9ec2-4cbb5880a0b8">
|
||||||
|
<annotatedElement>
|
||||||
|
<reflist>
|
||||||
|
<ref refid="0b54a350-f59d-11ef-bfb1-4cbb5880a0b8"/>
|
||||||
|
</reflist>
|
||||||
|
</annotatedElement>
|
||||||
|
<body>
|
||||||
|
<val>Extend this class if your manga source provides standard limit-offset based lists (get manga list by offset)</val>
|
||||||
|
</body>
|
||||||
|
<presentation>
|
||||||
|
<reflist>
|
||||||
|
<ref refid="bff32eae-f5bf-11ef-9ec2-4cbb5880a0b8"/>
|
||||||
|
</reflist>
|
||||||
|
</presentation>
|
||||||
|
</UML:Comment>
|
||||||
|
<UML:CommentItem id="bff32eae-f5bf-11ef-9ec2-4cbb5880a0b8">
|
||||||
|
<matrix>
|
||||||
|
<val>(1.0, 0.0, 0.0, 1.0, 673.0610499890082, 367.0515553989646)</val>
|
||||||
|
</matrix>
|
||||||
|
<top-left>
|
||||||
|
<val>(0.0, 0.0)</val>
|
||||||
|
</top-left>
|
||||||
|
<width>
|
||||||
|
<val>228.8028016098773</val>
|
||||||
|
</width>
|
||||||
|
<height>
|
||||||
|
<val>88.0</val>
|
||||||
|
</height>
|
||||||
|
<diagram>
|
||||||
|
<ref refid="58d6c536-66f8-11ec-b4c8-0456e5e540ed"/>
|
||||||
|
</diagram>
|
||||||
|
<subject>
|
||||||
|
<ref refid="bff31afe-f5bf-11ef-9ec2-4cbb5880a0b8"/>
|
||||||
|
</subject>
|
||||||
|
</UML:CommentItem>
|
||||||
|
<UML:Comment id="d5112a70-f5bf-11ef-9ec2-4cbb5880a0b8">
|
||||||
|
<annotatedElement>
|
||||||
|
<reflist>
|
||||||
|
<ref refid="a300f58a-f5bd-11ef-9ec2-4cbb5880a0b8"/>
|
||||||
|
</reflist>
|
||||||
|
</annotatedElement>
|
||||||
|
<body>
|
||||||
|
<val>Extend this class if your manga source provides paged-based lists (get manga list by page number)</val>
|
||||||
|
</body>
|
||||||
|
<presentation>
|
||||||
|
<reflist>
|
||||||
|
<ref refid="d5113ca4-f5bf-11ef-9ec2-4cbb5880a0b8"/>
|
||||||
|
</reflist>
|
||||||
|
</presentation>
|
||||||
|
</UML:Comment>
|
||||||
|
<UML:CommentItem id="d5113ca4-f5bf-11ef-9ec2-4cbb5880a0b8">
|
||||||
|
<matrix>
|
||||||
|
<val>(1.0, 0.0, 0.0, 1.0, 666.7924311664914, 507.7539062499999)</val>
|
||||||
|
</matrix>
|
||||||
|
<top-left>
|
||||||
|
<val>(0.0, 0.0)</val>
|
||||||
|
</top-left>
|
||||||
|
<width>
|
||||||
|
<val>214.34368896484375</val>
|
||||||
|
</width>
|
||||||
|
<height>
|
||||||
|
<val>88.0</val>
|
||||||
|
</height>
|
||||||
|
<diagram>
|
||||||
|
<ref refid="58d6c536-66f8-11ec-b4c8-0456e5e540ed"/>
|
||||||
|
</diagram>
|
||||||
|
<subject>
|
||||||
|
<ref refid="d5112a70-f5bf-11ef-9ec2-4cbb5880a0b8"/>
|
||||||
|
</subject>
|
||||||
|
</UML:CommentItem>
|
||||||
|
<UML:Comment id="e80418f4-f5bf-11ef-9ec2-4cbb5880a0b8">
|
||||||
|
<annotatedElement>
|
||||||
|
<reflist>
|
||||||
|
<ref refid="ad4c68d0-f5bd-11ef-9ec2-4cbb5880a0b8"/>
|
||||||
|
</reflist>
|
||||||
|
</annotatedElement>
|
||||||
|
<body>
|
||||||
|
<val>Extend this class if your manga source does not provide pagination (all manga provided in one list)</val>
|
||||||
|
</body>
|
||||||
|
<presentation>
|
||||||
|
<reflist>
|
||||||
|
<ref refid="e8042ad8-f5bf-11ef-9ec2-4cbb5880a0b8"/>
|
||||||
|
</reflist>
|
||||||
|
</presentation>
|
||||||
|
</UML:Comment>
|
||||||
|
<UML:CommentItem id="e8042ad8-f5bf-11ef-9ec2-4cbb5880a0b8">
|
||||||
|
<matrix>
|
||||||
|
<val>(1.0, 0.0, 0.0, 1.0, 666.7924311664914, 560.9671898788581)</val>
|
||||||
|
</matrix>
|
||||||
|
<top-left>
|
||||||
|
<val>(0.0, 58.00435704705592)</val>
|
||||||
|
</top-left>
|
||||||
|
<width>
|
||||||
|
<val>263.9307954323941</val>
|
||||||
|
</width>
|
||||||
|
<height>
|
||||||
|
<val>78.01706672440287</val>
|
||||||
|
</height>
|
||||||
|
<diagram>
|
||||||
|
<ref refid="58d6c536-66f8-11ec-b4c8-0456e5e540ed"/>
|
||||||
|
</diagram>
|
||||||
|
<subject>
|
||||||
|
<ref refid="e80418f4-f5bf-11ef-9ec2-4cbb5880a0b8"/>
|
||||||
|
</subject>
|
||||||
|
</UML:CommentItem>
|
||||||
|
<UML:CommentLineItem id="f6b48e4c-f5bf-11ef-9ec2-4cbb5880a0b8">
|
||||||
|
<diagram>
|
||||||
|
<ref refid="58d6c536-66f8-11ec-b4c8-0456e5e540ed"/>
|
||||||
|
</diagram>
|
||||||
|
<horizontal>
|
||||||
|
<val>False</val>
|
||||||
|
</horizontal>
|
||||||
|
<orthogonal>
|
||||||
|
<val>False</val>
|
||||||
|
</orthogonal>
|
||||||
|
<matrix>
|
||||||
|
<val>(1.0, 0.0, 0.0, 1.0, 561.8951626340418, 549.6101338901756)</val>
|
||||||
|
</matrix>
|
||||||
|
<points>
|
||||||
|
<val>[(56.44167413272805, 7.038279316310902), (104.89726853244963, 8.304008355003589)]</val>
|
||||||
|
</points>
|
||||||
|
<head-connection>
|
||||||
|
<ref refid="a3018fc2-f5bd-11ef-9ec2-4cbb5880a0b8"/>
|
||||||
|
</head-connection>
|
||||||
|
<tail-connection>
|
||||||
|
<ref refid="d5113ca4-f5bf-11ef-9ec2-4cbb5880a0b8"/>
|
||||||
|
</tail-connection>
|
||||||
|
</UML:CommentLineItem>
|
||||||
|
<UML:CommentLineItem id="f89ba010-f5bf-11ef-9ec2-4cbb5880a0b8">
|
||||||
|
<diagram>
|
||||||
|
<ref refid="58d6c536-66f8-11ec-b4c8-0456e5e540ed"/>
|
||||||
|
</diagram>
|
||||||
|
<horizontal>
|
||||||
|
<val>False</val>
|
||||||
|
</horizontal>
|
||||||
|
<orthogonal>
|
||||||
|
<val>False</val>
|
||||||
|
</orthogonal>
|
||||||
|
<matrix>
|
||||||
|
<val>(1.0, 0.0, 0.0, 1.0, 559.3873501340418, 413.0007588901755)</val>
|
||||||
|
</matrix>
|
||||||
|
<points>
|
||||||
|
<val>[(3.7806186159582467, 0.0), (113.67369985496646, 1.6012844908540842)]</val>
|
||||||
|
</points>
|
||||||
|
<head-connection>
|
||||||
|
<ref refid="0b54edc4-f59d-11ef-bfb1-4cbb5880a0b8"/>
|
||||||
|
</head-connection>
|
||||||
|
<tail-connection>
|
||||||
|
<ref refid="bff32eae-f5bf-11ef-9ec2-4cbb5880a0b8"/>
|
||||||
|
</tail-connection>
|
||||||
|
</UML:CommentLineItem>
|
||||||
|
<UML:CommentLineItem id="fabe0540-f5bf-11ef-9ec2-4cbb5880a0b8">
|
||||||
|
<diagram>
|
||||||
|
<ref refid="58d6c536-66f8-11ec-b4c8-0456e5e540ed"/>
|
||||||
|
</diagram>
|
||||||
|
<horizontal>
|
||||||
|
<val>False</val>
|
||||||
|
</horizontal>
|
||||||
|
<orthogonal>
|
||||||
|
<val>False</val>
|
||||||
|
</orthogonal>
|
||||||
|
<matrix>
|
||||||
|
<val>(1.0, 0.0, 0.0, 1.0, 522.3600063840418, 652.6882588901756)</val>
|
||||||
|
</matrix>
|
||||||
|
<points>
|
||||||
|
<val>[(57.80796236595825, 5.29182139794003), (144.43242478244963, 5.657840086725969)]</val>
|
||||||
|
</points>
|
||||||
|
<head-connection>
|
||||||
|
<ref refid="ad4d2aae-f5bd-11ef-9ec2-4cbb5880a0b8"/>
|
||||||
|
</head-connection>
|
||||||
|
<tail-connection>
|
||||||
|
<ref refid="e8042ad8-f5bf-11ef-9ec2-4cbb5880a0b8"/>
|
||||||
|
</tail-connection>
|
||||||
|
</UML:CommentLineItem>
|
||||||
|
<general:Box id="531831f2-f5c0-11ef-9ec2-4cbb5880a0b8">
|
||||||
|
<matrix>
|
||||||
|
<val>(1.0, 0.0, 0.0, 1.0, 375.05802564891326, 349.05453145170736)</val>
|
||||||
|
</matrix>
|
||||||
|
<top-left>
|
||||||
|
<val>(0.0, 3.15625)</val>
|
||||||
|
</top-left>
|
||||||
|
<width>
|
||||||
|
<val>590.6594026101285</val>
|
||||||
|
</width>
|
||||||
|
<height>
|
||||||
|
<val>368.44140625</val>
|
||||||
|
</height>
|
||||||
|
<diagram>
|
||||||
|
<ref refid="58d6c536-66f8-11ec-b4c8-0456e5e540ed"/>
|
||||||
|
</diagram>
|
||||||
|
</general:Box>
|
||||||
|
<UML:Comment id="5ccad4ca-f5c0-11ef-9ec2-4cbb5880a0b8">
|
||||||
|
<body>
|
||||||
|
<val>To create your own parser you have to extends one of these classes</val>
|
||||||
|
</body>
|
||||||
|
<presentation>
|
||||||
|
<reflist>
|
||||||
|
<ref refid="5ccae8d4-f5c0-11ef-9ec2-4cbb5880a0b8"/>
|
||||||
|
</reflist>
|
||||||
|
</presentation>
|
||||||
|
</UML:Comment>
|
||||||
|
<UML:CommentItem id="5ccae8d4-f5c0-11ef-9ec2-4cbb5880a0b8">
|
||||||
|
<matrix>
|
||||||
|
<val>(1.0, 0.0, 0.0, 1.0, 756.725301794198, 225.57697659840966)</val>
|
||||||
|
</matrix>
|
||||||
|
<top-left>
|
||||||
|
<val>(0.0, 0.0)</val>
|
||||||
|
</top-left>
|
||||||
|
<width>
|
||||||
|
<val>208.99212646484375</val>
|
||||||
|
</width>
|
||||||
|
<height>
|
||||||
|
<val>73.47482464883183</val>
|
||||||
|
</height>
|
||||||
|
<diagram>
|
||||||
|
<ref refid="58d6c536-66f8-11ec-b4c8-0456e5e540ed"/>
|
||||||
|
</diagram>
|
||||||
|
<subject>
|
||||||
|
<ref refid="5ccad4ca-f5c0-11ef-9ec2-4cbb5880a0b8"/>
|
||||||
|
</subject>
|
||||||
|
</UML:CommentItem>
|
||||||
|
<UML:CommentLineItem id="6f993af6-f5c0-11ef-9ec2-4cbb5880a0b8">
|
||||||
|
<diagram>
|
||||||
|
<ref refid="58d6c536-66f8-11ec-b4c8-0456e5e540ed"/>
|
||||||
|
</diagram>
|
||||||
|
<horizontal>
|
||||||
|
<val>False</val>
|
||||||
|
</horizontal>
|
||||||
|
<orthogonal>
|
||||||
|
<val>False</val>
|
||||||
|
</orthogonal>
|
||||||
|
<matrix>
|
||||||
|
<val>(1.0, 0.0, 0.0, 1.0, 943.6141683885666, 419.2772168262177)</val>
|
||||||
|
</matrix>
|
||||||
|
<points>
|
||||||
|
<val>[(-27.404961772030788, -67.06643537451032), (-27.404961772030788, -120.2254155789762)]</val>
|
||||||
|
</points>
|
||||||
|
<head-connection>
|
||||||
|
<ref refid="531831f2-f5c0-11ef-9ec2-4cbb5880a0b8"/>
|
||||||
|
</head-connection>
|
||||||
|
<tail-connection>
|
||||||
|
<ref refid="5ccae8d4-f5c0-11ef-9ec2-4cbb5880a0b8"/>
|
||||||
|
</tail-connection>
|
||||||
|
</UML:CommentLineItem>
|
||||||
|
</model>
|
||||||
|
</gaphor>
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 41 KiB |
@ -1 +1,15 @@
|
|||||||
|
## Following this blog:
|
||||||
|
# https://proandroiddev.com/how-we-reduced-our-gradle-build-times-by-over-80-51f2b6d6b05b
|
||||||
kotlin.code.style=official
|
kotlin.code.style=official
|
||||||
|
systemProp.org.gradle.unsafe.configuration-cache=false
|
||||||
|
org.gradle.jvmargs=-Xmx4g -XX:MaxMetaspaceSize=4096m -XX:+UseParallelGC
|
||||||
|
org.gradle.configureondemand=true
|
||||||
|
org.gradle.configuration-cache.problems=warn
|
||||||
|
|
||||||
|
## Use these flags on local machine for faster build time
|
||||||
|
# org.gradle.caching=true
|
||||||
|
# org.gradle.configuration-cache=true
|
||||||
|
# org.gradle.vfs.watch=true
|
||||||
|
# org.gradle.parallel=true
|
||||||
|
# org.gradle.workers.max=8
|
||||||
|
# org.gradle.configuration-cache.max-problems=8
|
||||||
|
|||||||
@ -0,0 +1,34 @@
|
|||||||
|
[versions]
|
||||||
|
kotlin = "2.2.10"
|
||||||
|
ksp = "2.2.10-2.0.2"
|
||||||
|
coroutines = "1.10.2"
|
||||||
|
junit = "5.10.1"
|
||||||
|
okhttp = "5.1.0"
|
||||||
|
okio = "3.16.0"
|
||||||
|
json = "20240303"
|
||||||
|
androidx-collection = "1.5.0"
|
||||||
|
jsoup = "1.21.2"
|
||||||
|
quickjs = "1.1.0"
|
||||||
|
korte = "4.0.10"
|
||||||
|
simplexml = "2.7.1"
|
||||||
|
|
||||||
|
[plugins]
|
||||||
|
kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
|
||||||
|
ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
|
||||||
|
|
||||||
|
[libraries]
|
||||||
|
ksp-symbol-processing-api = { module = "com.google.devtools.ksp:symbol-processing-api", version.ref = "ksp" }
|
||||||
|
kotlinx-coroutines-core = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-core", version.ref = "coroutines" }
|
||||||
|
kotlinx-coroutines-test = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-test", version.ref = "coroutines" }
|
||||||
|
junit-launcher = { group = "org.junit.platform", name = "junit-platform-launcher" }
|
||||||
|
junit-api = { group = "org.junit.jupiter", name = "junit-jupiter-api", version.ref = "junit" }
|
||||||
|
junit-engine = { group = "org.junit.jupiter", name = "junit-jupiter-engine", version.ref = "junit" }
|
||||||
|
junit-params = { group = "org.junit.jupiter", name = "junit-jupiter-params", version.ref = "junit" }
|
||||||
|
okhttp = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp" }
|
||||||
|
okio = { module = "com.squareup.okio:okio", version.ref = "okio" }
|
||||||
|
json = { module = "org.json:json", version.ref = "json" }
|
||||||
|
androidx-collection = { module = "androidx.collection:collection", version.ref = "androidx-collection" }
|
||||||
|
jsoup = { module = "org.jsoup:jsoup", version.ref = "jsoup" }
|
||||||
|
quickjs = { module = "io.webfolder:quickjs", version.ref = "quickjs" }
|
||||||
|
korte = { module = "com.soywiz.korlibs.korte:korte-jvm", version.ref = "korte" }
|
||||||
|
simplexml = { module = "org.simpleframework:simple-xml", version.ref = "simplexml" }
|
||||||
Binary file not shown.
@ -1,5 +1,7 @@
|
|||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-bin.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-9.0.0-bin.zip
|
||||||
|
networkTimeout=10000
|
||||||
|
validateDistributionUrl=true
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
zipStorePath=wrapper/dists
|
zipStorePath=wrapper/dists
|
||||||
|
|||||||
@ -0,0 +1,2 @@
|
|||||||
|
jdk:
|
||||||
|
- openjdk17
|
||||||
@ -1,7 +0,0 @@
|
|||||||
plugins {
|
|
||||||
id 'org.jetbrains.kotlin.jvm'
|
|
||||||
}
|
|
||||||
|
|
||||||
dependencies {
|
|
||||||
implementation 'com.google.devtools.ksp:symbol-processing-api:1.6.21-1.0.5'
|
|
||||||
}
|
|
||||||
@ -0,0 +1,11 @@
|
|||||||
|
plugins {
|
||||||
|
alias(libs.plugins.kotlin.jvm)
|
||||||
|
}
|
||||||
|
|
||||||
|
kotlin {
|
||||||
|
jvmToolchain(8)
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation(libs.ksp.symbol.processing.api)
|
||||||
|
}
|
||||||
@ -1,22 +0,0 @@
|
|||||||
pluginManagement {
|
|
||||||
plugins {
|
|
||||||
id 'com.google.devtools.ksp' version '1.6.21-1.0.5'
|
|
||||||
id 'org.jetbrains.kotlin.jvm' version '1.6.21'
|
|
||||||
}
|
|
||||||
repositories {
|
|
||||||
gradlePluginPortal()
|
|
||||||
google()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
dependencyResolutionManagement {
|
|
||||||
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
|
|
||||||
repositories {
|
|
||||||
google()
|
|
||||||
mavenCentral()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
rootProject.name = 'kotatsu-parsers'
|
|
||||||
include 'kotatsu-parsers-ksp'
|
|
||||||
|
|
||||||
@ -0,0 +1,18 @@
|
|||||||
|
pluginManagement {
|
||||||
|
repositories {
|
||||||
|
google()
|
||||||
|
mavenCentral()
|
||||||
|
gradlePluginPortal()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencyResolutionManagement {
|
||||||
|
repositories {
|
||||||
|
google()
|
||||||
|
mavenCentral()
|
||||||
|
gradlePluginPortal()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rootProject.name = "kotatsu-parsers"
|
||||||
|
include("kotatsu-parsers-ksp")
|
||||||
@ -0,0 +1,14 @@
|
|||||||
|
package org.koitharu.kotatsu.parsers
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Annotate [MangaParser] implementation to mark this parser as broken instead of removing it
|
||||||
|
*/
|
||||||
|
@Target(AnnotationTarget.CLASS)
|
||||||
|
@Retention(AnnotationRetention.SOURCE)
|
||||||
|
internal annotation class Broken(
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reason why this parser is broken
|
||||||
|
*/
|
||||||
|
val message: String = "",
|
||||||
|
)
|
||||||
@ -0,0 +1,18 @@
|
|||||||
|
package org.koitharu.kotatsu.parsers
|
||||||
|
|
||||||
|
public object ErrorMessages {
|
||||||
|
|
||||||
|
public const val FILTER_MULTIPLE_STATES_NOT_SUPPORTED: String = "Multiple states are not supported by this source"
|
||||||
|
public const val FILTER_MULTIPLE_GENRES_NOT_SUPPORTED: String = "Multiple genres are not supported by this source"
|
||||||
|
public const val FILTER_MULTIPLE_CONTENT_RATING_NOT_SUPPORTED: String =
|
||||||
|
"Multiple Content ratings are not supported by this source"
|
||||||
|
public const val FILTER_MULTIPLE_CONTENT_TYPES_NOT_SUPPORTED: String =
|
||||||
|
"Multiple Content types are not supported by this source"
|
||||||
|
public const val FILTER_MULTIPLE_DEMOGRAPHICS_NOT_SUPPORTED: String =
|
||||||
|
"Multiple Demographics are not supported by this source"
|
||||||
|
public const val FILTER_BOTH_LOCALE_GENRES_NOT_SUPPORTED: String =
|
||||||
|
"Filtering by both genres and locale is not supported by this source"
|
||||||
|
public const val FILTER_BOTH_STATES_GENRES_NOT_SUPPORTED: String =
|
||||||
|
"Filtering by both genres and states is not supported by this source"
|
||||||
|
public const val SEARCH_NOT_SUPPORTED: String = "Search is not supported by this source"
|
||||||
|
}
|
||||||
@ -1,166 +1,78 @@
|
|||||||
package org.koitharu.kotatsu.parsers
|
package org.koitharu.kotatsu.parsers
|
||||||
|
|
||||||
import okhttp3.*
|
import okhttp3.CookieJar
|
||||||
|
import okhttp3.HttpUrl
|
||||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||||
import okhttp3.MediaType.Companion.toMediaType
|
import okhttp3.OkHttpClient
|
||||||
import okhttp3.RequestBody.Companion.toRequestBody
|
import okhttp3.Response
|
||||||
import org.json.JSONObject
|
import org.koitharu.kotatsu.parsers.bitmap.Bitmap
|
||||||
import org.jsoup.HttpStatusException
|
|
||||||
import org.koitharu.kotatsu.parsers.config.MangaSourceConfig
|
import org.koitharu.kotatsu.parsers.config.MangaSourceConfig
|
||||||
import org.koitharu.kotatsu.parsers.exception.GraphQLException
|
import org.koitharu.kotatsu.parsers.model.MangaParserSource
|
||||||
import org.koitharu.kotatsu.parsers.exception.NotFoundException
|
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||||
import org.koitharu.kotatsu.parsers.util.await
|
import org.koitharu.kotatsu.parsers.util.LinkResolver
|
||||||
import org.koitharu.kotatsu.parsers.util.parseJson
|
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
abstract class MangaLoaderContext {
|
public abstract class MangaLoaderContext {
|
||||||
|
|
||||||
protected abstract val httpClient: OkHttpClient
|
public abstract val httpClient: OkHttpClient
|
||||||
|
|
||||||
abstract val cookieJar: CookieJar
|
public abstract val cookieJar: CookieJar
|
||||||
|
|
||||||
/**
|
public fun newParserInstance(source: MangaParserSource): MangaParser = source.newParser(this)
|
||||||
* Do a GET http request to specific url
|
|
||||||
* @param url
|
|
||||||
* @param headers an additional headers for request, may be null
|
|
||||||
*/
|
|
||||||
suspend fun httpGet(url: HttpUrl, headers: Headers? = null): Response {
|
|
||||||
val request = Request.Builder()
|
|
||||||
.get()
|
|
||||||
.url(url)
|
|
||||||
if (headers != null) {
|
|
||||||
request.headers(headers)
|
|
||||||
}
|
|
||||||
return httpClient.newCall(request.build()).await().ensureSuccess()
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun httpGet(url: String, headers: Headers? = null): Response {
|
public fun newLinkResolver(link: HttpUrl): LinkResolver = LinkResolver(this, link)
|
||||||
return httpGet(url.toHttpUrl(), headers)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
public fun newLinkResolver(link: String): LinkResolver = newLinkResolver(link.toHttpUrl())
|
||||||
|
|
||||||
/**
|
public open fun encodeBase64(data: ByteArray): String = Base64.getEncoder().encodeToString(data)
|
||||||
* Do a HEAD http request to specific url
|
|
||||||
* @param url
|
public open fun decodeBase64(data: String): ByteArray = Base64.getDecoder().decode(data)
|
||||||
* @param headers an additional headers for request, may be null
|
|
||||||
*/
|
public open fun getPreferredLocales(): List<Locale> = listOf(Locale.getDefault())
|
||||||
suspend fun httpHead(url: String, headers: Headers? = null): Response {
|
|
||||||
val request = Request.Builder()
|
|
||||||
.head()
|
|
||||||
.url(url)
|
|
||||||
if (headers != null) {
|
|
||||||
request.headers(headers)
|
|
||||||
}
|
|
||||||
return httpClient.newCall(request.build()).await().ensureSuccess()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Do a POST http request to specific url with `multipart/form-data` payload
|
* Execute JavaScript code and return result
|
||||||
* @param url
|
* @param script JavaScript source code
|
||||||
* @param form payload as key=>value map
|
* @return execution result as string, may be null
|
||||||
* @param headers an additional headers for request, may be null
|
|
||||||
*/
|
*/
|
||||||
suspend fun httpPost(
|
@Deprecated("Provide a base url")
|
||||||
url: String,
|
public abstract suspend fun evaluateJs(script: String): String?
|
||||||
form: Map<String, String>,
|
|
||||||
headers: Headers? = null,
|
|
||||||
): Response {
|
|
||||||
val body = FormBody.Builder()
|
|
||||||
form.forEach { (k, v) ->
|
|
||||||
body.addEncoded(k, v)
|
|
||||||
}
|
|
||||||
val request = Request.Builder()
|
|
||||||
.post(body.build())
|
|
||||||
.url(url)
|
|
||||||
if (headers != null) {
|
|
||||||
request.headers(headers)
|
|
||||||
}
|
|
||||||
return httpClient.newCall(request.build()).await().ensureSuccess()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Do a POST http request to specific url with `multipart/form-data` payload
|
* Execute JavaScript code and return result
|
||||||
* @param url
|
* @param script JavaScript source code
|
||||||
* @param payload payload as `key=value` string with `&` separator
|
* @param baseUrl url of page script will be executed in context of
|
||||||
* @param headers an additional headers for request, may be null
|
* @return execution result as string, may be null
|
||||||
*/
|
*/
|
||||||
suspend fun httpPost(
|
public abstract suspend fun evaluateJs(baseUrl: String, script: String): String?
|
||||||
url: String,
|
|
||||||
payload: String,
|
|
||||||
headers: Headers?,
|
|
||||||
): Response {
|
|
||||||
val body = FormBody.Builder()
|
|
||||||
payload.split('&').forEach {
|
|
||||||
val pos = it.indexOf('=')
|
|
||||||
if (pos != -1) {
|
|
||||||
val k = it.substring(0, pos)
|
|
||||||
val v = it.substring(pos + 1)
|
|
||||||
body.addEncoded(k, v)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
val request = Request.Builder()
|
|
||||||
.post(body.build())
|
|
||||||
.url(url)
|
|
||||||
if (headers != null) {
|
|
||||||
request.headers(headers)
|
|
||||||
}
|
|
||||||
return httpClient.newCall(request.build()).await().ensureSuccess()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Do a GraphQL request to specific url
|
* Open [url] in browser for some external action (e.g. captcha solving or non cookie-based authorization)
|
||||||
* @param endpoint an url
|
|
||||||
* @param query GraphQL request payload
|
|
||||||
*/
|
*/
|
||||||
suspend fun graphQLQuery(endpoint: String, query: String): JSONObject {
|
public open fun requestBrowserAction(parser: MangaParser, url: String): Nothing {
|
||||||
val body = JSONObject()
|
throw UnsupportedOperationException("Browser is not available")
|
||||||
body.put("operationName", null as Any?)
|
|
||||||
body.put("variables", JSONObject())
|
|
||||||
body.put("query", "{$query}")
|
|
||||||
val mediaType = "application/json; charset=utf-8".toMediaType()
|
|
||||||
val requestBody = body.toString().toRequestBody(mediaType)
|
|
||||||
val request = Request.Builder()
|
|
||||||
.post(requestBody)
|
|
||||||
.url(endpoint)
|
|
||||||
val json = httpClient.newCall(request.build()).await().ensureSuccess().parseJson()
|
|
||||||
json.optJSONArray("errors")?.let {
|
|
||||||
if (it.length() != 0) {
|
|
||||||
throw GraphQLException(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return json
|
|
||||||
}
|
}
|
||||||
|
|
||||||
open fun encodeBase64(data: ByteArray): String = Base64.getEncoder().encodeToString(data)
|
public abstract fun getConfig(source: MangaSource): MangaSourceConfig
|
||||||
|
|
||||||
open fun decodeBase64(data: String): ByteArray = Base64.getDecoder().decode(data)
|
public abstract fun getDefaultUserAgent(): String
|
||||||
|
|
||||||
open fun getPreferredLocales(): List<Locale> = listOf(Locale.getDefault())
|
/**
|
||||||
|
* Helper function to be used in an interceptor
|
||||||
|
* to descramble images
|
||||||
|
* @param response Image response
|
||||||
|
* @param redraw lambda function to implement descrambling logic
|
||||||
|
*/
|
||||||
|
public abstract fun redrawImageResponse(
|
||||||
|
response: Response,
|
||||||
|
redraw: (image: Bitmap) -> Bitmap,
|
||||||
|
): Response
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Execute JavaScript code and return result
|
* create a new empty Bitmap with given dimensions
|
||||||
* @param script JavaScript source code
|
|
||||||
* @return execution result as string, may be null
|
|
||||||
*/
|
*/
|
||||||
abstract suspend fun evaluateJs(script: String): String?
|
public abstract fun createBitmap(
|
||||||
|
width: Int,
|
||||||
abstract fun getConfig(source: MangaSource): MangaSourceConfig
|
height: Int,
|
||||||
|
): Bitmap
|
||||||
private fun Response.ensureSuccess(): Response {
|
}
|
||||||
val exception: Exception? = when (code) { // Catch some error codes, not all
|
|
||||||
404 -> NotFoundException(message, request.url.toString())
|
|
||||||
in 500..599 -> HttpStatusException(message, code, request.url.toString())
|
|
||||||
else -> null
|
|
||||||
}
|
|
||||||
if (exception != null) {
|
|
||||||
runCatching {
|
|
||||||
close()
|
|
||||||
}.onFailure {
|
|
||||||
exception.addSuppressed(it)
|
|
||||||
}
|
|
||||||
throw exception
|
|
||||||
}
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@ -1,200 +1,88 @@
|
|||||||
package org.koitharu.kotatsu.parsers
|
package org.koitharu.kotatsu.parsers
|
||||||
|
|
||||||
import androidx.annotation.CallSuper
|
|
||||||
import androidx.annotation.VisibleForTesting
|
|
||||||
import okhttp3.Headers
|
import okhttp3.Headers
|
||||||
import okhttp3.HttpUrl
|
import okhttp3.HttpUrl
|
||||||
import org.jsoup.nodes.Element
|
import okhttp3.Interceptor
|
||||||
import org.koitharu.kotatsu.parsers.config.ConfigKey
|
import org.koitharu.kotatsu.parsers.config.ConfigKey
|
||||||
import org.koitharu.kotatsu.parsers.exception.ParseException
|
import org.koitharu.kotatsu.parsers.config.MangaSourceConfig
|
||||||
import org.koitharu.kotatsu.parsers.model.*
|
import org.koitharu.kotatsu.parsers.model.*
|
||||||
import org.koitharu.kotatsu.parsers.util.FaviconParser
|
import org.koitharu.kotatsu.parsers.model.search.MangaSearchQuery
|
||||||
import org.koitharu.kotatsu.parsers.util.toAbsoluteUrl
|
import org.koitharu.kotatsu.parsers.model.search.MangaSearchQueryCapabilities
|
||||||
|
import org.koitharu.kotatsu.parsers.util.LinkResolver
|
||||||
|
import org.koitharu.kotatsu.parsers.util.convertToMangaSearchQuery
|
||||||
|
import org.koitharu.kotatsu.parsers.util.toMangaListFilterCapabilities
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
abstract class MangaParser @InternalParsersApi constructor(val source: MangaSource) {
|
public interface MangaParser : Interceptor {
|
||||||
|
|
||||||
protected abstract val context: MangaLoaderContext
|
public val source: MangaParserSource
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Supported [SortOrder] variants. Must not be empty.
|
* Supported [SortOrder] variants. Must not be empty.
|
||||||
*
|
*
|
||||||
* For better performance use [EnumSet] for more than one item.
|
* For better performance use [EnumSet] for more than one item.
|
||||||
*/
|
*/
|
||||||
abstract val sortOrders: Set<SortOrder>
|
public val availableSortOrders: Set<SortOrder>
|
||||||
|
|
||||||
val config by lazy { context.getConfig(source) }
|
@Deprecated("Too complex. Use filterCapabilities instead")
|
||||||
|
public val searchQueryCapabilities: MangaSearchQueryCapabilities
|
||||||
|
|
||||||
val sourceLocale: Locale?
|
public val filterCapabilities: MangaListFilterCapabilities
|
||||||
get() = source.locale?.let { Locale(it) }
|
|
||||||
|
|
||||||
/**
|
public val config: MangaSourceConfig
|
||||||
* Provide default domain and available alternatives, if any.
|
|
||||||
*
|
|
||||||
* Never hardcode domain in requests, use [getDomain] instead.
|
|
||||||
*/
|
|
||||||
protected abstract val configKeyDomain: ConfigKey.Domain
|
|
||||||
|
|
||||||
@VisibleForTesting(otherwise = VisibleForTesting.PROTECTED)
|
public val authorizationProvider: MangaParserAuthProvider?
|
||||||
internal open val headers: Headers? = null
|
get() = this as? MangaParserAuthProvider
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Used as fallback if value of `sortOrder` passed to [getList] is null
|
* Provide default domain and available alternatives, if any.
|
||||||
*/
|
|
||||||
protected open val defaultSortOrder: SortOrder
|
|
||||||
get() {
|
|
||||||
val supported = sortOrders
|
|
||||||
return SortOrder.values().first { it in supported }
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parse list of manga by specified criteria
|
|
||||||
*
|
*
|
||||||
* @param offset starting from 0 and used for pagination.
|
* Never hardcode domain in requests, use [domain] instead.
|
||||||
* Note than passed value may not be divisible by internal page size, so you should adjust it manually.
|
|
||||||
* @param query search query, may be null or empty if no search needed
|
|
||||||
* @param tags genres for filtering, values from [getTags] and [Manga.tags]. May be null or empty
|
|
||||||
* @param sortOrder one of [sortOrders] or null for default value
|
|
||||||
*/
|
*/
|
||||||
@JvmSynthetic
|
public val configKeyDomain: ConfigKey.Domain
|
||||||
@InternalParsersApi
|
|
||||||
abstract suspend fun getList(
|
|
||||||
offset: Int,
|
|
||||||
query: String?,
|
|
||||||
tags: Set<MangaTag>?,
|
|
||||||
sortOrder: SortOrder,
|
|
||||||
): List<Manga>
|
|
||||||
|
|
||||||
/**
|
public val domain: String
|
||||||
* Parse list of manga with search by text query
|
|
||||||
*
|
|
||||||
* @param offset starting from 0 and used for pagination.
|
|
||||||
* @param query search query
|
|
||||||
*/
|
|
||||||
open suspend fun getList(offset: Int, query: String): List<Manga> {
|
|
||||||
return getList(offset, query, null, defaultSortOrder)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
@Deprecated("Too complex. Use getList with filter instead")
|
||||||
* Parse list of manga by specified criteria
|
public suspend fun getList(query: MangaSearchQuery): List<Manga>
|
||||||
*
|
|
||||||
* @param offset starting from 0 and used for pagination.
|
public suspend fun getList(offset: Int, order: SortOrder, filter: MangaListFilter): List<Manga>
|
||||||
* Note than passed value may not be divisible by internal page size, so you should adjust it manually.
|
|
||||||
* @param tags genres for filtering, values from [getTags] and [Manga.tags]. May be null or empty
|
|
||||||
* @param sortOrder one of [sortOrders] or null for default value
|
|
||||||
*/
|
|
||||||
open suspend fun getList(offset: Int, tags: Set<MangaTag>?, sortOrder: SortOrder?): List<Manga> {
|
|
||||||
return getList(offset, null, tags, sortOrder ?: defaultSortOrder)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parse details for [Manga]: chapters list, description, large cover, etc.
|
* Parse details for [Manga]: chapters list, description, large cover, etc.
|
||||||
* Must return the same manga, may change any fields excepts id, url and source
|
* Must return the same manga, may change any fields excepts id, url and source
|
||||||
* @see Manga.copy
|
* @see Manga.copy
|
||||||
*/
|
*/
|
||||||
abstract suspend fun getDetails(manga: Manga): Manga
|
public suspend fun getDetails(manga: Manga): Manga
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parse pages list for specified chapter.
|
* Parse pages list for specified chapter.
|
||||||
* @see MangaPage for details
|
* @see MangaPage for details
|
||||||
*/
|
*/
|
||||||
abstract suspend fun getPages(chapter: MangaChapter): List<MangaPage>
|
public suspend fun getPages(chapter: MangaChapter): List<MangaPage>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetch direct link to the page image.
|
* Fetch direct link to the page image.
|
||||||
*/
|
*/
|
||||||
open suspend fun getPageUrl(page: MangaPage): String = page.url.toAbsoluteUrl(getDomain())
|
public suspend fun getPageUrl(page: MangaPage): String
|
||||||
|
|
||||||
/**
|
public suspend fun getFilterOptions(): MangaListFilterOptions
|
||||||
* Fetch available tags (genres) for source
|
|
||||||
*/
|
|
||||||
abstract suspend fun getTags(): Set<MangaTag>
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns direct link to the website favicon
|
|
||||||
*/
|
|
||||||
@Deprecated(
|
|
||||||
message = "Use parseFavicons() to get multiple favicons with different size",
|
|
||||||
replaceWith = ReplaceWith("parseFavicons()"),
|
|
||||||
)
|
|
||||||
open fun getFaviconUrl() = "https://${getDomain()}/favicon.ico"
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parse favicons from the main page of the source`s website
|
* Parse favicons from the main page of the source`s website
|
||||||
*/
|
*/
|
||||||
open suspend fun getFavicons(): Favicons {
|
public suspend fun getFavicons(): Favicons
|
||||||
return FaviconParser(context, getDomain(), headers).parseFavicons()
|
|
||||||
}
|
|
||||||
|
|
||||||
@CallSuper
|
|
||||||
open fun onCreateConfig(keys: MutableCollection<ConfigKey<*>>) {
|
|
||||||
keys.add(configKeyDomain)
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Utils */
|
public fun onCreateConfig(keys: MutableCollection<ConfigKey<*>>)
|
||||||
|
|
||||||
fun getDomain(): String {
|
public suspend fun getRelatedManga(seed: Manga): List<Manga>
|
||||||
return config[configKeyDomain]
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getDomain(subdomain: String): String {
|
public fun getRequestHeaders(): Headers
|
||||||
val domain = getDomain()
|
|
||||||
return subdomain + "." + domain.removePrefix("www.")
|
|
||||||
}
|
|
||||||
|
|
||||||
fun urlBuilder(): HttpUrl.Builder {
|
|
||||||
return HttpUrl.Builder()
|
|
||||||
.scheme("https")
|
|
||||||
.host(getDomain())
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a unique id for [Manga]/[MangaChapter]/[MangaPage].
|
* Return [Manga] object by web link to it
|
||||||
* @param url must be relative url, without a domain
|
* @see [Manga.publicUrl]
|
||||||
* @see [Manga.id]
|
|
||||||
* @see [MangaChapter.id]
|
|
||||||
* @see [MangaPage.id]
|
|
||||||
*/
|
*/
|
||||||
@InternalParsersApi
|
@InternalParsersApi
|
||||||
protected fun generateUid(url: String): Long {
|
public suspend fun resolveLink(resolver: LinkResolver, link: HttpUrl): Manga?
|
||||||
var h = 1125899906842597L
|
}
|
||||||
source.name.forEach { c ->
|
|
||||||
h = 31 * h + c.code
|
|
||||||
}
|
|
||||||
url.forEach { c ->
|
|
||||||
h = 31 * h + c.code
|
|
||||||
}
|
|
||||||
return h
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a unique id for [Manga]/[MangaChapter]/[MangaPage].
|
|
||||||
* @param id an internal identifier
|
|
||||||
* @see [Manga.id]
|
|
||||||
* @see [MangaChapter.id]
|
|
||||||
* @see [MangaPage.id]
|
|
||||||
*/
|
|
||||||
@InternalParsersApi
|
|
||||||
protected fun generateUid(id: Long): Long {
|
|
||||||
var h = 1125899906842597L
|
|
||||||
source.name.forEach { c ->
|
|
||||||
h = 31 * h + c.code
|
|
||||||
}
|
|
||||||
h = 31 * h + id
|
|
||||||
return h
|
|
||||||
}
|
|
||||||
|
|
||||||
@InternalParsersApi
|
|
||||||
protected fun Element.parseFailed(message: String? = null): Nothing {
|
|
||||||
throw ParseException(message, ownerDocument()?.location() ?: baseUri(), null)
|
|
||||||
}
|
|
||||||
|
|
||||||
@InternalParsersApi
|
|
||||||
protected fun Set<MangaTag>?.oneOrThrowIfMany(): MangaTag? {
|
|
||||||
return when {
|
|
||||||
isNullOrEmpty() -> null
|
|
||||||
size == 1 -> first()
|
|
||||||
else -> throw IllegalArgumentException("Multiple genres are not supported by this source")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@ -1,20 +1,28 @@
|
|||||||
package org.koitharu.kotatsu.parsers
|
package org.koitharu.kotatsu.parsers
|
||||||
|
|
||||||
|
import org.koitharu.kotatsu.parsers.model.ContentType
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Annotate each [MangaParser] implementation with this annotation, used by codegen
|
* Annotate each [MangaParser] implementation with this annotation, used by codegen
|
||||||
*/
|
*/
|
||||||
@Target(AnnotationTarget.CLASS)
|
@Target(AnnotationTarget.CLASS)
|
||||||
annotation class MangaSourceParser(
|
@Retention(AnnotationRetention.SOURCE)
|
||||||
|
internal annotation class MangaSourceParser(
|
||||||
/**
|
/**
|
||||||
* Name of manga source. Used as an Enum value, must be UPPER_CASE and unique.
|
* Name of manga source. Used as an Enum value, must be UPPER_CASE and unique.
|
||||||
*/
|
*/
|
||||||
val name: String,
|
val name: String,
|
||||||
/**
|
/**
|
||||||
* User-friendly title of manga source. In most case equals the website name.
|
* User-friendly title of manga source. In most case equals the website name.
|
||||||
|
* Avoid extra whitespaces between the words if it is not required.
|
||||||
*/
|
*/
|
||||||
val title: String,
|
val title: String,
|
||||||
/**
|
/**
|
||||||
* Language code (for example "en" or "ru") or blank if parser provide manga on different languages.
|
* Language code (for example "en" or "ru") or blank if parser provide manga on different languages.
|
||||||
*/
|
*/
|
||||||
val locale: String = "",
|
val locale: String = "",
|
||||||
)
|
/**
|
||||||
|
* Type of content provided by parser. See [ContentType] for more info
|
||||||
|
*/
|
||||||
|
val type: ContentType = ContentType.MANGA,
|
||||||
|
)
|
||||||
|
|||||||
@ -1,50 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.parsers
|
|
||||||
|
|
||||||
import org.koitharu.kotatsu.parsers.model.Manga
|
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaTag
|
|
||||||
import org.koitharu.kotatsu.parsers.model.SortOrder
|
|
||||||
import org.koitharu.kotatsu.parsers.util.Paginator
|
|
||||||
|
|
||||||
@InternalParsersApi
|
|
||||||
abstract class PagedMangaParser(
|
|
||||||
source: MangaSource,
|
|
||||||
pageSize: Int,
|
|
||||||
searchPageSize: Int = pageSize,
|
|
||||||
) : MangaParser(source) {
|
|
||||||
|
|
||||||
protected val paginator = Paginator(pageSize)
|
|
||||||
protected val searchPaginator = Paginator(searchPageSize)
|
|
||||||
|
|
||||||
override suspend fun getList(offset: Int, query: String): List<Manga> {
|
|
||||||
return getList(searchPaginator, offset, query, null, defaultSortOrder)
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun getList(offset: Int, tags: Set<MangaTag>?, sortOrder: SortOrder?): List<Manga> {
|
|
||||||
return getList(paginator, offset, null, tags, sortOrder ?: defaultSortOrder)
|
|
||||||
}
|
|
||||||
|
|
||||||
@InternalParsersApi
|
|
||||||
@Deprecated("You should use getListPage for PagedMangaParser", level = DeprecationLevel.HIDDEN)
|
|
||||||
final override suspend fun getList(
|
|
||||||
offset: Int,
|
|
||||||
query: String?,
|
|
||||||
tags: Set<MangaTag>?,
|
|
||||||
sortOrder: SortOrder,
|
|
||||||
): List<Manga> = throw UnsupportedOperationException("You should use getListPage for PagedMangaParser")
|
|
||||||
|
|
||||||
abstract suspend fun getListPage(page: Int, query: String?, tags: Set<MangaTag>?, sortOrder: SortOrder): List<Manga>
|
|
||||||
|
|
||||||
private suspend fun getList(
|
|
||||||
paginator: Paginator,
|
|
||||||
offset: Int,
|
|
||||||
query: String?,
|
|
||||||
tags: Set<MangaTag>?,
|
|
||||||
sortOrder: SortOrder,
|
|
||||||
): List<Manga> {
|
|
||||||
val page = paginator.getPage(offset)
|
|
||||||
val list = getListPage(page, query, tags, sortOrder)
|
|
||||||
paginator.onListReceived(offset, page, list.size)
|
|
||||||
return list
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -0,0 +1,9 @@
|
|||||||
|
package org.koitharu.kotatsu.parsers.bitmap
|
||||||
|
|
||||||
|
public interface Bitmap {
|
||||||
|
|
||||||
|
public val width: Int
|
||||||
|
public val height: Int
|
||||||
|
|
||||||
|
public fun drawBitmap(sourceBitmap: Bitmap, src: Rect, dst: Rect)
|
||||||
|
}
|
||||||
@ -0,0 +1,15 @@
|
|||||||
|
package org.koitharu.kotatsu.parsers.bitmap
|
||||||
|
|
||||||
|
public data class Rect(
|
||||||
|
val left: Int = 0,
|
||||||
|
val top: Int = 0,
|
||||||
|
val right: Int = 0,
|
||||||
|
val bottom: Int = 0,
|
||||||
|
) {
|
||||||
|
|
||||||
|
val width: Int
|
||||||
|
get() = right - left
|
||||||
|
|
||||||
|
val height: Int
|
||||||
|
get() = bottom - top
|
||||||
|
}
|
||||||
@ -1,13 +1,37 @@
|
|||||||
package org.koitharu.kotatsu.parsers.config
|
package org.koitharu.kotatsu.parsers.config
|
||||||
|
|
||||||
sealed class ConfigKey<T>(
|
public sealed class ConfigKey<T>(
|
||||||
val key: String,
|
@JvmField public val key: String,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
abstract val defaultValue: T
|
public abstract val defaultValue: T
|
||||||
|
|
||||||
class Domain(
|
public class Domain(
|
||||||
|
@JvmField @JvmSuppressWildcards public vararg val presetValues: String,
|
||||||
|
) : ConfigKey<String>("domain") {
|
||||||
|
|
||||||
|
init {
|
||||||
|
require(presetValues.isNotEmpty()) { "You must provide at least one domain" }
|
||||||
|
}
|
||||||
|
|
||||||
|
override val defaultValue: String
|
||||||
|
get() = presetValues.first()
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ShowSuspiciousContent(
|
||||||
|
override val defaultValue: Boolean,
|
||||||
|
) : ConfigKey<Boolean>("show_suspicious")
|
||||||
|
|
||||||
|
public class UserAgent(
|
||||||
override val defaultValue: String,
|
override val defaultValue: String,
|
||||||
val presetValues: Array<String>?,
|
) : ConfigKey<String>("user_agent")
|
||||||
) : ConfigKey<String>("domain")
|
|
||||||
}
|
public class SplitByTranslations(
|
||||||
|
override val defaultValue: Boolean,
|
||||||
|
) : ConfigKey<Boolean>("split_translations")
|
||||||
|
|
||||||
|
public class PreferredImageServer(
|
||||||
|
public val presetValues: Map<String?, String?>,
|
||||||
|
override val defaultValue: String?,
|
||||||
|
) : ConfigKey<String?>("img_server")
|
||||||
|
}
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
package org.koitharu.kotatsu.parsers.config
|
package org.koitharu.kotatsu.parsers.config
|
||||||
|
|
||||||
interface MangaSourceConfig {
|
public interface MangaSourceConfig {
|
||||||
|
|
||||||
operator fun <T> get(key: ConfigKey<T>): T
|
public operator fun <T> get(key: ConfigKey<T>): T
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,105 @@
|
|||||||
|
package org.koitharu.kotatsu.parsers.core
|
||||||
|
|
||||||
|
import androidx.annotation.CallSuper
|
||||||
|
import okhttp3.Headers
|
||||||
|
import okhttp3.HttpUrl
|
||||||
|
import okhttp3.Interceptor
|
||||||
|
import okhttp3.Response
|
||||||
|
import org.koitharu.kotatsu.parsers.InternalParsersApi
|
||||||
|
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
||||||
|
import org.koitharu.kotatsu.parsers.MangaParser
|
||||||
|
import org.koitharu.kotatsu.parsers.config.ConfigKey
|
||||||
|
import org.koitharu.kotatsu.parsers.config.MangaSourceConfig
|
||||||
|
import org.koitharu.kotatsu.parsers.model.*
|
||||||
|
import org.koitharu.kotatsu.parsers.model.search.MangaSearchQuery
|
||||||
|
import org.koitharu.kotatsu.parsers.model.search.MangaSearchQueryCapabilities
|
||||||
|
import org.koitharu.kotatsu.parsers.network.OkHttpWebClient
|
||||||
|
import org.koitharu.kotatsu.parsers.network.WebClient
|
||||||
|
import org.koitharu.kotatsu.parsers.util.*
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
@Suppress("OVERRIDE_DEPRECATION")
|
||||||
|
@InternalParsersApi
|
||||||
|
public abstract class AbstractMangaParser @InternalParsersApi constructor(
|
||||||
|
@property:InternalParsersApi public val context: MangaLoaderContext,
|
||||||
|
public final override val source: MangaParserSource,
|
||||||
|
) : MangaParser {
|
||||||
|
|
||||||
|
public final override val searchQueryCapabilities: MangaSearchQueryCapabilities
|
||||||
|
get() = filterCapabilities.toMangaSearchQueryCapabilities()
|
||||||
|
|
||||||
|
public override val config: MangaSourceConfig by lazy { context.getConfig(source) }
|
||||||
|
|
||||||
|
public open val sourceLocale: Locale
|
||||||
|
get() = if (source.locale.isEmpty()) Locale.ROOT else Locale(source.locale)
|
||||||
|
|
||||||
|
protected val sourceContentRating: ContentRating?
|
||||||
|
get() = if (source.contentType == ContentType.HENTAI) {
|
||||||
|
ContentRating.ADULT
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
|
protected val isNsfwSource: Boolean = source.contentType == ContentType.HENTAI
|
||||||
|
|
||||||
|
protected open val userAgentKey: ConfigKey.UserAgent = ConfigKey.UserAgent(context.getDefaultUserAgent())
|
||||||
|
|
||||||
|
override fun getRequestHeaders(): Headers = Headers.Builder()
|
||||||
|
.add("User-Agent", config[userAgentKey])
|
||||||
|
.build()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used as fallback if value of `order` passed to [getList] is null
|
||||||
|
*/
|
||||||
|
public open val defaultSortOrder: SortOrder
|
||||||
|
get() {
|
||||||
|
val supported = availableSortOrders
|
||||||
|
return SortOrder.entries.first { it in supported }
|
||||||
|
}
|
||||||
|
|
||||||
|
final override val domain: String
|
||||||
|
get() = config[configKeyDomain]
|
||||||
|
|
||||||
|
@JvmField
|
||||||
|
protected val webClient: WebClient = OkHttpWebClient(context.httpClient, source)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Search list of manga by specified searchQuery
|
||||||
|
*
|
||||||
|
* @param query searchQuery
|
||||||
|
*/
|
||||||
|
public final override suspend fun getList(query: MangaSearchQuery): List<Manga> = getList(
|
||||||
|
offset = query.offset,
|
||||||
|
order = query.order ?: defaultSortOrder,
|
||||||
|
filter = convertToMangaListFilter(query),
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch direct link to the page image.
|
||||||
|
*/
|
||||||
|
public override suspend fun getPageUrl(page: MangaPage): String = page.url.toAbsoluteUrl(domain)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse favicons from the main page of the source`s website
|
||||||
|
*/
|
||||||
|
public override suspend fun getFavicons(): Favicons {
|
||||||
|
return FaviconParser(webClient, domain).parseFavicons()
|
||||||
|
}
|
||||||
|
|
||||||
|
@CallSuper
|
||||||
|
public override fun onCreateConfig(keys: MutableCollection<ConfigKey<*>>) {
|
||||||
|
keys.add(configKeyDomain)
|
||||||
|
}
|
||||||
|
|
||||||
|
public override suspend fun getRelatedManga(seed: Manga): List<Manga> {
|
||||||
|
return RelatedMangaFinder(listOf(this)).invoke(seed)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return [Manga] object by web link to it
|
||||||
|
* @see [Manga.publicUrl]
|
||||||
|
*/
|
||||||
|
override suspend fun resolveLink(resolver: LinkResolver, link: HttpUrl): Manga? = null
|
||||||
|
|
||||||
|
override fun intercept(chain: Interceptor.Chain): Response = chain.proceed(chain.request())
|
||||||
|
}
|
||||||
@ -0,0 +1,94 @@
|
|||||||
|
package org.koitharu.kotatsu.parsers.core
|
||||||
|
|
||||||
|
import androidx.annotation.CallSuper
|
||||||
|
import okhttp3.Headers
|
||||||
|
import okhttp3.HttpUrl
|
||||||
|
import okhttp3.Interceptor
|
||||||
|
import okhttp3.Response
|
||||||
|
import org.koitharu.kotatsu.parsers.InternalParsersApi
|
||||||
|
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
||||||
|
import org.koitharu.kotatsu.parsers.MangaParser
|
||||||
|
import org.koitharu.kotatsu.parsers.config.ConfigKey
|
||||||
|
import org.koitharu.kotatsu.parsers.config.MangaSourceConfig
|
||||||
|
import org.koitharu.kotatsu.parsers.model.*
|
||||||
|
import org.koitharu.kotatsu.parsers.network.OkHttpWebClient
|
||||||
|
import org.koitharu.kotatsu.parsers.network.WebClient
|
||||||
|
import org.koitharu.kotatsu.parsers.util.*
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
@Deprecated("Too complex. Use AbstractMangaParser instead")
|
||||||
|
internal abstract class FlexibleMangaParser @InternalParsersApi constructor(
|
||||||
|
@property:InternalParsersApi val context: MangaLoaderContext,
|
||||||
|
final override val source: MangaParserSource,
|
||||||
|
) : MangaParser {
|
||||||
|
|
||||||
|
override val config: MangaSourceConfig by lazy { context.getConfig(source) }
|
||||||
|
|
||||||
|
open val sourceLocale: Locale
|
||||||
|
get() = if (source.locale.isEmpty()) Locale.ROOT else Locale(source.locale)
|
||||||
|
|
||||||
|
protected open val userAgentKey: ConfigKey.UserAgent = ConfigKey.UserAgent(context.getDefaultUserAgent())
|
||||||
|
|
||||||
|
final override val filterCapabilities: MangaListFilterCapabilities
|
||||||
|
get() = searchQueryCapabilities.toMangaListFilterCapabilities()
|
||||||
|
|
||||||
|
protected val sourceContentRating: ContentRating?
|
||||||
|
get() = if (source.contentType == ContentType.HENTAI) {
|
||||||
|
ContentRating.ADULT
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
|
final override val domain: String
|
||||||
|
get() = config[configKeyDomain]
|
||||||
|
|
||||||
|
@Deprecated("Override intercept() instead")
|
||||||
|
override fun getRequestHeaders(): Headers = Headers.Builder()
|
||||||
|
.add("User-Agent", config[userAgentKey])
|
||||||
|
.build()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used as fallback if value of `order` passed to [getList] is null
|
||||||
|
*/
|
||||||
|
open val defaultSortOrder: SortOrder
|
||||||
|
get() {
|
||||||
|
val supported = availableSortOrders
|
||||||
|
return SortOrder.entries.first { it in supported }
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmField
|
||||||
|
protected val webClient: WebClient = OkHttpWebClient(context.httpClient, source)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch direct link to the page image.
|
||||||
|
*/
|
||||||
|
override suspend fun getPageUrl(page: MangaPage): String = page.url.toAbsoluteUrl(domain)
|
||||||
|
|
||||||
|
final override suspend fun getList(offset: Int, order: SortOrder, filter: MangaListFilter): List<Manga> {
|
||||||
|
return getList(convertToMangaSearchQuery(offset, order, filter))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse favicons from the main page of the source`s website
|
||||||
|
*/
|
||||||
|
override suspend fun getFavicons(): Favicons {
|
||||||
|
return FaviconParser(webClient, domain).parseFavicons()
|
||||||
|
}
|
||||||
|
|
||||||
|
@CallSuper
|
||||||
|
override fun onCreateConfig(keys: MutableCollection<ConfigKey<*>>) {
|
||||||
|
keys.add(configKeyDomain)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun getRelatedManga(seed: Manga): List<Manga> {
|
||||||
|
return RelatedMangaFinder(listOf(this)).invoke(seed)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return [Manga] object by web link to it
|
||||||
|
* @see [Manga.publicUrl]
|
||||||
|
*/
|
||||||
|
override suspend fun resolveLink(resolver: LinkResolver, link: HttpUrl): Manga? = null
|
||||||
|
|
||||||
|
override fun intercept(chain: Interceptor.Chain): Response = chain.proceed(chain.request())
|
||||||
|
}
|
||||||
@ -0,0 +1,60 @@
|
|||||||
|
package org.koitharu.kotatsu.parsers.core
|
||||||
|
|
||||||
|
import androidx.annotation.VisibleForTesting
|
||||||
|
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
||||||
|
import org.koitharu.kotatsu.parsers.model.Manga
|
||||||
|
import org.koitharu.kotatsu.parsers.model.MangaParserSource
|
||||||
|
import org.koitharu.kotatsu.parsers.model.search.MangaSearchQuery
|
||||||
|
import org.koitharu.kotatsu.parsers.model.search.SearchableField
|
||||||
|
import org.koitharu.kotatsu.parsers.util.Paginator
|
||||||
|
|
||||||
|
@Deprecated("Too complex. Use PagedMangaParser instead")
|
||||||
|
internal abstract class FlexiblePagedMangaParser(
|
||||||
|
context: MangaLoaderContext,
|
||||||
|
source: MangaParserSource,
|
||||||
|
@VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) @JvmField public val pageSize: Int,
|
||||||
|
searchPageSize: Int = pageSize,
|
||||||
|
) : FlexibleMangaParser(context, source) {
|
||||||
|
|
||||||
|
@JvmField
|
||||||
|
protected val paginator: Paginator = Paginator(pageSize)
|
||||||
|
|
||||||
|
@JvmField
|
||||||
|
protected val searchPaginator: Paginator = Paginator(searchPageSize)
|
||||||
|
|
||||||
|
final override suspend fun getList(query: MangaSearchQuery): List<Manga> {
|
||||||
|
var containTitleNameCriteria = false
|
||||||
|
query.criteria.forEach {
|
||||||
|
if (it.field == SearchableField.TITLE_NAME) {
|
||||||
|
containTitleNameCriteria = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return searchManga(
|
||||||
|
paginator = if (containTitleNameCriteria) {
|
||||||
|
paginator
|
||||||
|
} else {
|
||||||
|
searchPaginator
|
||||||
|
},
|
||||||
|
query = query,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
public abstract suspend fun getListPage(query: MangaSearchQuery, page: Int): List<Manga>
|
||||||
|
|
||||||
|
protected fun setFirstPage(firstPage: Int, firstPageForSearch: Int = firstPage) {
|
||||||
|
paginator.firstPage = firstPage
|
||||||
|
searchPaginator.firstPage = firstPageForSearch
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun searchManga(
|
||||||
|
paginator: Paginator,
|
||||||
|
query: MangaSearchQuery,
|
||||||
|
): List<Manga> {
|
||||||
|
val offset: Int = query.offset
|
||||||
|
val page = paginator.getPage(offset)
|
||||||
|
val list = getListPage(query, page)
|
||||||
|
paginator.onListReceived(offset, page, list.size)
|
||||||
|
return list
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,77 @@
|
|||||||
|
package org.koitharu.kotatsu.parsers.core
|
||||||
|
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import okhttp3.Interceptor
|
||||||
|
import okhttp3.Request
|
||||||
|
import okhttp3.Response
|
||||||
|
import org.koitharu.kotatsu.parsers.MangaParser
|
||||||
|
import org.koitharu.kotatsu.parsers.MangaParserAuthProvider
|
||||||
|
import org.koitharu.kotatsu.parsers.model.*
|
||||||
|
import org.koitharu.kotatsu.parsers.model.search.MangaSearchQuery
|
||||||
|
import org.koitharu.kotatsu.parsers.util.mergeWith
|
||||||
|
|
||||||
|
internal class MangaParserWrapper(
|
||||||
|
private val delegate: MangaParser,
|
||||||
|
) : MangaParser by delegate {
|
||||||
|
|
||||||
|
override val authorizationProvider: MangaParserAuthProvider?
|
||||||
|
get() = delegate as? MangaParserAuthProvider
|
||||||
|
|
||||||
|
@Deprecated("Too complex. Use getList with filter instead")
|
||||||
|
override suspend fun getList(query: MangaSearchQuery): List<Manga> = withContext(Dispatchers.Default) {
|
||||||
|
if (!query.skipValidation) {
|
||||||
|
searchQueryCapabilities.validate(query)
|
||||||
|
}
|
||||||
|
delegate.getList(query)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun getList(
|
||||||
|
offset: Int,
|
||||||
|
order: SortOrder,
|
||||||
|
filter: MangaListFilter,
|
||||||
|
): List<Manga> = withContext(Dispatchers.Default) {
|
||||||
|
delegate.getList(offset, order, filter)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun getDetails(manga: Manga): Manga = withContext(Dispatchers.Default) {
|
||||||
|
delegate.getDetails(manga)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun getPages(chapter: MangaChapter): List<MangaPage> = withContext(Dispatchers.Default) {
|
||||||
|
delegate.getPages(chapter)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun getPageUrl(page: MangaPage): String = withContext(Dispatchers.Default) {
|
||||||
|
delegate.getPageUrl(page)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun getFilterOptions(): MangaListFilterOptions = withContext(Dispatchers.Default) {
|
||||||
|
delegate.getFilterOptions()
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun getFavicons(): Favicons = withContext(Dispatchers.Default) {
|
||||||
|
delegate.getFavicons()
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun getRelatedManga(seed: Manga): List<Manga> = withContext(Dispatchers.Default) {
|
||||||
|
delegate.getRelatedManga(seed)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun intercept(chain: Interceptor.Chain): Response {
|
||||||
|
val request = chain.request()
|
||||||
|
val headers = request.headers.newBuilder()
|
||||||
|
.mergeWith(delegate.getRequestHeaders(), replaceExisting = false)
|
||||||
|
.build()
|
||||||
|
val newRequest = request.newBuilder().headers(headers).build()
|
||||||
|
return delegate.intercept(ProxyChain(chain, newRequest))
|
||||||
|
}
|
||||||
|
|
||||||
|
private class ProxyChain(
|
||||||
|
private val delegate: Interceptor.Chain,
|
||||||
|
private val request: Request,
|
||||||
|
) : Interceptor.Chain by delegate {
|
||||||
|
|
||||||
|
override fun request(): Request = request
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,57 @@
|
|||||||
|
package org.koitharu.kotatsu.parsers.core
|
||||||
|
|
||||||
|
import androidx.annotation.VisibleForTesting
|
||||||
|
import org.koitharu.kotatsu.parsers.InternalParsersApi
|
||||||
|
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
||||||
|
import org.koitharu.kotatsu.parsers.model.Manga
|
||||||
|
import org.koitharu.kotatsu.parsers.model.MangaListFilter
|
||||||
|
import org.koitharu.kotatsu.parsers.model.MangaParserSource
|
||||||
|
import org.koitharu.kotatsu.parsers.model.SortOrder
|
||||||
|
import org.koitharu.kotatsu.parsers.util.Paginator
|
||||||
|
|
||||||
|
@InternalParsersApi
|
||||||
|
public abstract class PagedMangaParser(
|
||||||
|
context: MangaLoaderContext,
|
||||||
|
source: MangaParserSource,
|
||||||
|
@VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) @JvmField public val pageSize: Int,
|
||||||
|
searchPageSize: Int = pageSize,
|
||||||
|
) : AbstractMangaParser(context, source) {
|
||||||
|
|
||||||
|
@JvmField
|
||||||
|
protected val paginator: Paginator = Paginator(pageSize)
|
||||||
|
|
||||||
|
@JvmField
|
||||||
|
protected val searchPaginator: Paginator = Paginator(searchPageSize)
|
||||||
|
|
||||||
|
final override suspend fun getList(offset: Int, order: SortOrder, filter: MangaListFilter): List<Manga> {
|
||||||
|
return getList(
|
||||||
|
paginator = if (filter.query.isNullOrEmpty()) {
|
||||||
|
paginator
|
||||||
|
} else {
|
||||||
|
searchPaginator
|
||||||
|
},
|
||||||
|
offset = offset,
|
||||||
|
order = order,
|
||||||
|
filter = filter,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
public abstract suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List<Manga>
|
||||||
|
|
||||||
|
protected fun setFirstPage(firstPage: Int, firstPageForSearch: Int = firstPage) {
|
||||||
|
paginator.firstPage = firstPage
|
||||||
|
searchPaginator.firstPage = firstPageForSearch
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun getList(
|
||||||
|
paginator: Paginator,
|
||||||
|
offset: Int,
|
||||||
|
order: SortOrder,
|
||||||
|
filter: MangaListFilter,
|
||||||
|
): List<Manga> {
|
||||||
|
val page = paginator.getPage(offset)
|
||||||
|
val list = getListPage(page, order, filter)
|
||||||
|
paginator.onListReceived(offset, page, list.size)
|
||||||
|
return list
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,24 @@
|
|||||||
|
package org.koitharu.kotatsu.parsers.core
|
||||||
|
|
||||||
|
import org.koitharu.kotatsu.parsers.InternalParsersApi
|
||||||
|
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
||||||
|
import org.koitharu.kotatsu.parsers.model.Manga
|
||||||
|
import org.koitharu.kotatsu.parsers.model.MangaListFilter
|
||||||
|
import org.koitharu.kotatsu.parsers.model.MangaParserSource
|
||||||
|
import org.koitharu.kotatsu.parsers.model.SortOrder
|
||||||
|
|
||||||
|
@InternalParsersApi
|
||||||
|
public abstract class SinglePageMangaParser(
|
||||||
|
context: MangaLoaderContext,
|
||||||
|
source: MangaParserSource,
|
||||||
|
) : AbstractMangaParser(context, source) {
|
||||||
|
|
||||||
|
final override suspend fun getList(offset: Int, order: SortOrder, filter: MangaListFilter): List<Manga> {
|
||||||
|
if (offset > 0) {
|
||||||
|
return emptyList()
|
||||||
|
}
|
||||||
|
return getList(order, filter)
|
||||||
|
}
|
||||||
|
|
||||||
|
public abstract suspend fun getList(order: SortOrder, filter: MangaListFilter): List<Manga>
|
||||||
|
}
|
||||||
@ -1,11 +1,13 @@
|
|||||||
package org.koitharu.kotatsu.parsers.exception
|
package org.koitharu.kotatsu.parsers.exception
|
||||||
|
|
||||||
|
import okio.IOException
|
||||||
import org.koitharu.kotatsu.parsers.InternalParsersApi
|
import org.koitharu.kotatsu.parsers.InternalParsersApi
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Authorization is required for access to the requested content
|
* Authorization is required for access to the requested content
|
||||||
*/
|
*/
|
||||||
class AuthRequiredException @InternalParsersApi constructor(
|
public class AuthRequiredException @InternalParsersApi @JvmOverloads constructor(
|
||||||
val source: MangaSource,
|
public val source: MangaSource,
|
||||||
) : RuntimeException("Authorization required")
|
cause: Throwable? = null,
|
||||||
|
) : IOException("Authorization required", cause)
|
||||||
|
|||||||
@ -1,7 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.parsers.exception
|
|
||||||
|
|
||||||
import okio.IOException
|
|
||||||
|
|
||||||
class CloudFlareProtectedException(
|
|
||||||
val url: String,
|
|
||||||
) : IOException("Protected by CloudFlare: $url")
|
|
||||||
@ -1,3 +1,3 @@
|
|||||||
package org.koitharu.kotatsu.parsers.exception
|
package org.koitharu.kotatsu.parsers.exception
|
||||||
|
|
||||||
class ContentUnavailableException(message: String) : RuntimeException(message)
|
public class ContentUnavailableException(message: String) : RuntimeException(message)
|
||||||
|
|||||||
@ -0,0 +1,31 @@
|
|||||||
|
package org.koitharu.kotatsu.parsers.exception
|
||||||
|
|
||||||
|
import okio.IOException
|
||||||
|
import java.time.Instant
|
||||||
|
import java.time.temporal.ChronoUnit
|
||||||
|
|
||||||
|
public class TooManyRequestExceptions(
|
||||||
|
public val url: String,
|
||||||
|
retryAfter: Long,
|
||||||
|
) : IOException("Too man requests") {
|
||||||
|
|
||||||
|
public val retryAt: Instant? = if (retryAfter > 0 && retryAfter < Long.MAX_VALUE) {
|
||||||
|
Instant.now().plusMillis(retryAfter)
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
|
public fun getRetryDelay(): Long {
|
||||||
|
if (retryAt == null) {
|
||||||
|
return -1L
|
||||||
|
}
|
||||||
|
return Instant.now().until(retryAt, ChronoUnit.MILLIS).coerceAtLeast(0L)
|
||||||
|
}
|
||||||
|
|
||||||
|
override val message: String?
|
||||||
|
get() = if (retryAt != null) {
|
||||||
|
"${super.message}, retry at $retryAt"
|
||||||
|
} else {
|
||||||
|
super.message
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,7 @@
|
|||||||
|
package org.koitharu.kotatsu.parsers.model
|
||||||
|
|
||||||
|
public enum class ContentRating {
|
||||||
|
SAFE,
|
||||||
|
SUGGESTIVE,
|
||||||
|
ADULT
|
||||||
|
}
|
||||||
@ -0,0 +1,36 @@
|
|||||||
|
package org.koitharu.kotatsu.parsers.model
|
||||||
|
|
||||||
|
public enum class ContentType {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Standard manga, manhua, webtoons, etc
|
||||||
|
*/
|
||||||
|
MANGA,
|
||||||
|
|
||||||
|
MANHWA,
|
||||||
|
|
||||||
|
MANHUA,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Use this if the source provides mostly nsfw content.
|
||||||
|
*/
|
||||||
|
HENTAI,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Western comics
|
||||||
|
*/
|
||||||
|
COMICS,
|
||||||
|
|
||||||
|
NOVEL,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Use this type if no other suits your needs. For example, for an indie manga
|
||||||
|
*/
|
||||||
|
|
||||||
|
ONE_SHOT,
|
||||||
|
DOUJINSHI,
|
||||||
|
IMAGE_SET,
|
||||||
|
ARTIST_CG,
|
||||||
|
GAME_CG,
|
||||||
|
OTHER,
|
||||||
|
}
|
||||||
@ -0,0 +1,10 @@
|
|||||||
|
package org.koitharu.kotatsu.parsers.model
|
||||||
|
|
||||||
|
public enum class Demographic {
|
||||||
|
SHOUNEN,
|
||||||
|
SHOUJO,
|
||||||
|
SEINEN,
|
||||||
|
JOSEI,
|
||||||
|
KODOMO,
|
||||||
|
NONE,
|
||||||
|
}
|
||||||
@ -1,158 +1,203 @@
|
|||||||
package org.koitharu.kotatsu.parsers.model
|
package org.koitharu.kotatsu.parsers.model
|
||||||
|
|
||||||
import org.koitharu.kotatsu.parsers.InternalParsersApi
|
import androidx.collection.ArrayMap
|
||||||
|
import org.koitharu.kotatsu.parsers.util.findById
|
||||||
|
import org.koitharu.kotatsu.parsers.util.nullIfEmpty
|
||||||
|
|
||||||
class Manga(
|
public data class Manga(
|
||||||
/**
|
/**
|
||||||
* Unique identifier for manga
|
* Unique identifier for manga
|
||||||
*/
|
*/
|
||||||
val id: Long,
|
@JvmField public val id: Long,
|
||||||
/**
|
/**
|
||||||
* Manga title, human-readable
|
* Manga title, human-readable
|
||||||
*/
|
*/
|
||||||
val title: String,
|
@JvmField public val title: String,
|
||||||
/**
|
/**
|
||||||
* Alternative title (for example on other language), may be null
|
* Alternative titles (for example on other language), may be empty
|
||||||
*/
|
*/
|
||||||
val altTitle: String?,
|
@JvmField public val altTitles: Set<String>,
|
||||||
/**
|
/**
|
||||||
* Relative url to manga (**without** a domain) or any other uri.
|
* Relative url to manga (**without** a domain) or any other uri.
|
||||||
* Used principally in parsers
|
* Used principally in parsers
|
||||||
*/
|
*/
|
||||||
val url: String,
|
@JvmField public val url: String,
|
||||||
/**
|
/**
|
||||||
* Absolute url to manga, must be ready to open in browser
|
* Absolute url to manga, must be ready to open in browser
|
||||||
*/
|
*/
|
||||||
val publicUrl: String,
|
@JvmField public val publicUrl: String,
|
||||||
/**
|
/**
|
||||||
* Normalized manga rating, must be in range of 0..1 or [RATING_UNKNOWN] if rating s unknown
|
* Normalized manga rating, must be in range of 0..1 or [RATING_UNKNOWN] if rating s unknown
|
||||||
* @see hasRating
|
* @see hasRating
|
||||||
*/
|
*/
|
||||||
val rating: Float,
|
@JvmField public val rating: Float,
|
||||||
/**
|
/**
|
||||||
* Indicates that manga may contain sensitive information (18+, NSFW)
|
* Indicates that manga may contain sensitive information (18+, NSFW)
|
||||||
*/
|
*/
|
||||||
val isNsfw: Boolean,
|
@JvmField public val contentRating: ContentRating?,
|
||||||
/**
|
/**
|
||||||
* Absolute link to the cover
|
* Absolute link to the cover
|
||||||
* @see largeCoverUrl
|
* @see largeCoverUrl
|
||||||
*/
|
*/
|
||||||
val coverUrl: String,
|
@JvmField public val coverUrl: String?,
|
||||||
/**
|
/**
|
||||||
* Tags (genres) of the manga
|
* Tags (genres) of the manga
|
||||||
*/
|
*/
|
||||||
val tags: Set<MangaTag>,
|
@JvmField public val tags: Set<MangaTag>,
|
||||||
/**
|
/**
|
||||||
* Manga status (ongoing, finished) or null if unknown
|
* Manga status (ongoing, finished) or null if unknown
|
||||||
*/
|
*/
|
||||||
val state: MangaState?,
|
@JvmField public val state: MangaState?,
|
||||||
/**
|
/**
|
||||||
* Author of the manga, may be null
|
* Authors of the manga
|
||||||
*/
|
*/
|
||||||
val author: String?,
|
@JvmField public val authors: Set<String>,
|
||||||
/**
|
/**
|
||||||
* Large cover url (absolute), null if is no large cover
|
* Large cover url (absolute), null if is no large cover
|
||||||
* @see coverUrl
|
* @see coverUrl
|
||||||
*/
|
*/
|
||||||
val largeCoverUrl: String? = null,
|
@JvmField public val largeCoverUrl: String? = null,
|
||||||
/**
|
/**
|
||||||
* Manga description, may be html or null
|
* Manga description, may be html or null
|
||||||
*/
|
*/
|
||||||
val description: String? = null,
|
@JvmField public val description: String? = null,
|
||||||
/**
|
/**
|
||||||
* List of chapters
|
* List of chapters
|
||||||
*/
|
*/
|
||||||
val chapters: List<MangaChapter>? = null,
|
@JvmField public val chapters: List<MangaChapter>? = null,
|
||||||
/**
|
/**
|
||||||
* Manga source
|
* Manga source
|
||||||
*/
|
*/
|
||||||
val source: MangaSource,
|
@JvmField public val source: MangaSource,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
/**
|
@Deprecated("Use other constructor")
|
||||||
* Return if manga has a specified rating
|
public constructor(
|
||||||
* @see rating
|
/**
|
||||||
*/
|
* Unique identifier for manga
|
||||||
val hasRating: Boolean
|
*/
|
||||||
get() = rating > 0f && rating <= 1f
|
id: Long,
|
||||||
|
/**
|
||||||
fun getChapters(branch: String?): List<MangaChapter>? {
|
* Manga title, human-readable
|
||||||
return chapters?.filter { x -> x.branch == branch }
|
*/
|
||||||
}
|
title: String,
|
||||||
|
/**
|
||||||
@InternalParsersApi
|
* Alternative title (for example on other language), may be null
|
||||||
fun copy(
|
*/
|
||||||
title: String = this.title,
|
altTitle: String?,
|
||||||
altTitle: String? = this.altTitle,
|
/**
|
||||||
publicUrl: String = this.publicUrl,
|
* Relative url to manga (**without** a domain) or any other uri.
|
||||||
rating: Float = this.rating,
|
* Used principally in parsers
|
||||||
isNsfw: Boolean = this.isNsfw,
|
*/
|
||||||
coverUrl: String = this.coverUrl,
|
url: String,
|
||||||
tags: Set<MangaTag> = this.tags,
|
/**
|
||||||
state: MangaState? = this.state,
|
* Absolute url to manga, must be ready to open in browser
|
||||||
author: String? = this.author,
|
*/
|
||||||
largeCoverUrl: String? = this.largeCoverUrl,
|
publicUrl: String,
|
||||||
description: String? = this.description,
|
/**
|
||||||
chapters: List<MangaChapter>? = this.chapters,
|
* Normalized manga rating, must be in range of 0..1 or [RATING_UNKNOWN] if rating s unknown
|
||||||
) = Manga(
|
* @see hasRating
|
||||||
|
*/
|
||||||
|
rating: Float,
|
||||||
|
/**
|
||||||
|
* Indicates that manga may contain sensitive information (18+, NSFW)
|
||||||
|
*/
|
||||||
|
isNsfw: Boolean,
|
||||||
|
/**
|
||||||
|
* Absolute link to the cover
|
||||||
|
* @see largeCoverUrl
|
||||||
|
*/
|
||||||
|
coverUrl: String?,
|
||||||
|
/**
|
||||||
|
* Tags (genres) of the manga
|
||||||
|
*/
|
||||||
|
tags: Set<MangaTag>,
|
||||||
|
/**
|
||||||
|
* Manga status (ongoing, finished) or null if unknown
|
||||||
|
*/
|
||||||
|
state: MangaState?,
|
||||||
|
/**
|
||||||
|
* Authors of the manga
|
||||||
|
*/
|
||||||
|
author: String?,
|
||||||
|
/**
|
||||||
|
* Large cover url (absolute), null if is no large cover
|
||||||
|
* @see coverUrl
|
||||||
|
*/
|
||||||
|
largeCoverUrl: String? = null,
|
||||||
|
/**
|
||||||
|
* Manga description, may be html or null
|
||||||
|
*/
|
||||||
|
description: String? = null,
|
||||||
|
/**
|
||||||
|
* List of chapters
|
||||||
|
*/
|
||||||
|
chapters: List<MangaChapter>? = null,
|
||||||
|
/**
|
||||||
|
* Manga source
|
||||||
|
*/
|
||||||
|
source: MangaSource,
|
||||||
|
) : this(
|
||||||
id = id,
|
id = id,
|
||||||
title = title,
|
title = title,
|
||||||
altTitle = altTitle,
|
altTitles = setOfNotNull(altTitle?.nullIfEmpty()),
|
||||||
url = url,
|
url = url,
|
||||||
publicUrl = publicUrl,
|
publicUrl = publicUrl,
|
||||||
rating = rating,
|
rating = rating,
|
||||||
isNsfw = isNsfw,
|
contentRating = if (isNsfw) ContentRating.ADULT else null,
|
||||||
coverUrl = coverUrl,
|
coverUrl = coverUrl?.nullIfEmpty(),
|
||||||
tags = tags,
|
tags = tags,
|
||||||
state = state,
|
state = state,
|
||||||
author = author,
|
authors = setOfNotNull(author),
|
||||||
largeCoverUrl = largeCoverUrl,
|
largeCoverUrl = largeCoverUrl?.nullIfEmpty(),
|
||||||
description = description,
|
description = description?.nullIfEmpty(),
|
||||||
chapters = chapters,
|
chapters = chapters,
|
||||||
source = source,
|
source = source,
|
||||||
)
|
)
|
||||||
|
|
||||||
override fun equals(other: Any?): Boolean {
|
/**
|
||||||
if (this === other) return true
|
* Author of the manga, may be null
|
||||||
if (javaClass != other?.javaClass) return false
|
*/
|
||||||
|
@Deprecated("Please use authors")
|
||||||
|
public val author: String?
|
||||||
|
get() = authors.firstOrNull()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Alternative title (for example on other language), may be null
|
||||||
|
*/
|
||||||
|
@Deprecated("Please use altTitles")
|
||||||
|
public val altTitle: String?
|
||||||
|
get() = altTitles.firstOrNull()
|
||||||
|
|
||||||
other as Manga
|
/**
|
||||||
|
* Return if manga has a specified rating
|
||||||
|
* @see rating
|
||||||
|
*/
|
||||||
|
public val hasRating: Boolean
|
||||||
|
get() = rating > 0f && rating <= 1f
|
||||||
|
|
||||||
if (id != other.id) return false
|
@Deprecated("Use contentRating instead", ReplaceWith("contentRating == ContentRating.ADULT"))
|
||||||
if (title != other.title) return false
|
public val isNsfw: Boolean
|
||||||
if (altTitle != other.altTitle) return false
|
get() = contentRating == ContentRating.ADULT
|
||||||
if (url != other.url) return false
|
|
||||||
if (publicUrl != other.publicUrl) return false
|
|
||||||
if (rating != other.rating) return false
|
|
||||||
if (isNsfw != other.isNsfw) return false
|
|
||||||
if (coverUrl != other.coverUrl) return false
|
|
||||||
if (tags != other.tags) return false
|
|
||||||
if (state != other.state) return false
|
|
||||||
if (author != other.author) return false
|
|
||||||
if (largeCoverUrl != other.largeCoverUrl) return false
|
|
||||||
if (description != other.description) return false
|
|
||||||
if (chapters != other.chapters) return false
|
|
||||||
if (source != other.source) return false
|
|
||||||
|
|
||||||
return true
|
public fun getChapters(branch: String?): List<MangaChapter> {
|
||||||
|
return chapters?.filter { x -> x.branch == branch }.orEmpty()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun hashCode(): Int {
|
public fun findChapterById(id: Long): MangaChapter? = chapters?.findById(id)
|
||||||
var result = id.hashCode()
|
|
||||||
result = 31 * result + title.hashCode()
|
public fun requireChapterById(id: Long): MangaChapter = findChapterById(id)
|
||||||
result = 31 * result + (altTitle?.hashCode() ?: 0)
|
?: throw NoSuchElementException("Chapter with id $id not found")
|
||||||
result = 31 * result + url.hashCode()
|
|
||||||
result = 31 * result + publicUrl.hashCode()
|
public fun getBranches(): Map<String?, Int> {
|
||||||
result = 31 * result + rating.hashCode()
|
if (chapters.isNullOrEmpty()) {
|
||||||
result = 31 * result + isNsfw.hashCode()
|
return emptyMap()
|
||||||
result = 31 * result + coverUrl.hashCode()
|
}
|
||||||
result = 31 * result + tags.hashCode()
|
val result = ArrayMap<String?, Int>()
|
||||||
result = 31 * result + (state?.hashCode() ?: 0)
|
chapters.forEach {
|
||||||
result = 31 * result + (author?.hashCode() ?: 0)
|
val key = it.branch
|
||||||
result = 31 * result + (largeCoverUrl?.hashCode() ?: 0)
|
result[key] = result.getOrDefault(key, 0) + 1
|
||||||
result = 31 * result + (description?.hashCode() ?: 0)
|
}
|
||||||
result = 31 * result + (chapters?.hashCode() ?: 0)
|
|
||||||
result = 31 * result + source.hashCode()
|
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,70 +1,65 @@
|
|||||||
package org.koitharu.kotatsu.parsers.model
|
package org.koitharu.kotatsu.parsers.model
|
||||||
|
|
||||||
class MangaChapter(
|
import org.koitharu.kotatsu.parsers.util.formatSimple
|
||||||
|
import org.koitharu.kotatsu.parsers.util.ifNullOrEmpty
|
||||||
|
|
||||||
|
public data class MangaChapter(
|
||||||
/**
|
/**
|
||||||
* An unique id of chapter
|
* An unique id of chapter
|
||||||
*/
|
*/
|
||||||
val id: Long,
|
@JvmField public val id: Long,
|
||||||
|
/**
|
||||||
|
* User-readable name of chapter if provided by parser or null instead
|
||||||
|
* Do not pass manga title or chapter number here
|
||||||
|
*/
|
||||||
|
@JvmField public val title: String?,
|
||||||
/**
|
/**
|
||||||
* User-readable name of chapter
|
* Chapter number starting from 1, 0 if unknown
|
||||||
*/
|
*/
|
||||||
val name: String,
|
@JvmField public val number: Float,
|
||||||
/**
|
/**
|
||||||
* Chapter number starting from 1
|
* Volume number starting from 1, 0 if unknown
|
||||||
*/
|
*/
|
||||||
val number: Int,
|
@JvmField public val volume: Int,
|
||||||
/**
|
/**
|
||||||
* Relative url to chapter (**without** a domain) or any other uri.
|
* Relative url to chapter (**without** a domain) or any other uri.
|
||||||
* Used principally in parsers
|
* Used principally in parsers
|
||||||
*/
|
*/
|
||||||
val url: String,
|
@JvmField public val url: String,
|
||||||
/**
|
/**
|
||||||
* User-readable name of scanlator (releaser) or null if unknown
|
* User-readable name of scanlator (releaser) or null if unknown
|
||||||
*/
|
*/
|
||||||
val scanlator: String?,
|
@JvmField public val scanlator: String?,
|
||||||
/**
|
/**
|
||||||
* Chapter upload date in milliseconds
|
* Chapter upload date in milliseconds
|
||||||
*/
|
*/
|
||||||
val uploadDate: Long,
|
@JvmField public val uploadDate: Long,
|
||||||
/**
|
/**
|
||||||
* User-readable name of branch.
|
* User-readable name of branch.
|
||||||
* A branch is a group of chapters that overlap (e.g. different languages)
|
* A branch is a group of chapters that overlap (e.g. different languages)
|
||||||
*/
|
*/
|
||||||
val branch: String?,
|
@JvmField public val branch: String?,
|
||||||
val source: MangaSource,
|
@JvmField public val source: MangaSource,
|
||||||
) : Comparable<MangaChapter> {
|
) {
|
||||||
|
|
||||||
override fun compareTo(other: MangaChapter): Int {
|
|
||||||
return number.compareTo(other.number)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun equals(other: Any?): Boolean {
|
|
||||||
if (this === other) return true
|
|
||||||
if (javaClass != other?.javaClass) return false
|
|
||||||
|
|
||||||
other as MangaChapter
|
|
||||||
|
|
||||||
if (id != other.id) return false
|
@Deprecated("Use title instead", ReplaceWith("title"))
|
||||||
if (name != other.name) return false
|
val name: String
|
||||||
if (number != other.number) return false
|
get() = title.ifNullOrEmpty {
|
||||||
if (url != other.url) return false
|
buildString {
|
||||||
if (scanlator != other.scanlator) return false
|
if (volume > 0) append("Vol ").append(volume).append(' ')
|
||||||
if (uploadDate != other.uploadDate) return false
|
if (number > 0) append("Chapter ").append(number.formatSimple()) else append("Unnamed")
|
||||||
if (branch != other.branch) return false
|
}
|
||||||
if (source != other.source) return false
|
}
|
||||||
|
|
||||||
return true
|
public fun numberString(): String? = if (number > 0f) {
|
||||||
|
number.formatSimple()
|
||||||
|
} else {
|
||||||
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun hashCode(): Int {
|
public fun volumeString(): String? = if (volume > 0) {
|
||||||
var result = id.hashCode()
|
volume.toString()
|
||||||
result = 31 * result + name.hashCode()
|
} else {
|
||||||
result = 31 * result + number
|
null
|
||||||
result = 31 * result + url.hashCode()
|
|
||||||
result = 31 * result + (scanlator?.hashCode() ?: 0)
|
|
||||||
result = 31 * result + uploadDate.hashCode()
|
|
||||||
result = 31 * result + (branch?.hashCode() ?: 0)
|
|
||||||
result = 31 * result + source.hashCode()
|
|
||||||
return result
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,88 @@
|
|||||||
|
package org.koitharu.kotatsu.parsers.model
|
||||||
|
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
public data class MangaListFilter(
|
||||||
|
@JvmField val query: String? = null,
|
||||||
|
@JvmField val tags: Set<MangaTag> = emptySet(),
|
||||||
|
@JvmField val tagsExclude: Set<MangaTag> = emptySet(),
|
||||||
|
@JvmField val locale: Locale? = null,
|
||||||
|
@JvmField val originalLocale: Locale? = null,
|
||||||
|
@JvmField val states: Set<MangaState> = emptySet(),
|
||||||
|
@JvmField val contentRating: Set<ContentRating> = emptySet(),
|
||||||
|
@JvmField val types: Set<ContentType> = emptySet(),
|
||||||
|
@JvmField val demographics: Set<Demographic> = emptySet(),
|
||||||
|
@JvmField val year: Int = YEAR_UNKNOWN,
|
||||||
|
@JvmField val yearFrom: Int = YEAR_UNKNOWN,
|
||||||
|
@JvmField val yearTo: Int = YEAR_UNKNOWN,
|
||||||
|
@JvmField val author: String? = null,
|
||||||
|
) {
|
||||||
|
|
||||||
|
private fun isNonSearchOptionsEmpty(): Boolean = tags.isEmpty() &&
|
||||||
|
tagsExclude.isEmpty() &&
|
||||||
|
locale == null &&
|
||||||
|
originalLocale == null &&
|
||||||
|
states.isEmpty() &&
|
||||||
|
contentRating.isEmpty() &&
|
||||||
|
year == YEAR_UNKNOWN &&
|
||||||
|
yearFrom == YEAR_UNKNOWN &&
|
||||||
|
yearTo == YEAR_UNKNOWN &&
|
||||||
|
types.isEmpty() &&
|
||||||
|
demographics.isEmpty() &&
|
||||||
|
author.isNullOrEmpty()
|
||||||
|
|
||||||
|
public fun isEmpty(): Boolean = isNonSearchOptionsEmpty() && query.isNullOrEmpty()
|
||||||
|
|
||||||
|
public fun isNotEmpty(): Boolean = !isEmpty()
|
||||||
|
|
||||||
|
public fun hasNonSearchOptions(): Boolean = !isNonSearchOptionsEmpty()
|
||||||
|
|
||||||
|
public companion object {
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
public val EMPTY: MangaListFilter = MangaListFilter()
|
||||||
|
}
|
||||||
|
|
||||||
|
internal class Builder {
|
||||||
|
private var query: String? = null
|
||||||
|
private val tags: MutableSet<MangaTag> = mutableSetOf()
|
||||||
|
private val tagsExclude: MutableSet<MangaTag> = mutableSetOf()
|
||||||
|
private var locale: Locale? = null
|
||||||
|
private var originalLocale: Locale? = null
|
||||||
|
private val states: MutableSet<MangaState> = mutableSetOf()
|
||||||
|
private val contentRating: MutableSet<ContentRating> = mutableSetOf()
|
||||||
|
private val types: MutableSet<ContentType> = mutableSetOf()
|
||||||
|
private val demographics: MutableSet<Demographic> = mutableSetOf()
|
||||||
|
private var year: Int = YEAR_UNKNOWN
|
||||||
|
private var yearFrom: Int = YEAR_UNKNOWN
|
||||||
|
private var yearTo: Int = YEAR_UNKNOWN
|
||||||
|
|
||||||
|
fun query(query: String?): Builder = apply { this.query = query }
|
||||||
|
fun addTag(tag: MangaTag): Builder = apply { tags.add(tag) }
|
||||||
|
fun addTags(tags: Collection<MangaTag>): Builder = apply { this.tags.addAll(tags) }
|
||||||
|
fun excludeTag(tag: MangaTag): Builder = apply { tagsExclude.add(tag) }
|
||||||
|
fun excludeTags(tags: Collection<MangaTag>): Builder = apply { this.tagsExclude.addAll(tags) }
|
||||||
|
fun locale(locale: Locale?): Builder = apply { this.locale = locale }
|
||||||
|
fun originalLocale(locale: Locale?): Builder = apply { this.originalLocale = locale }
|
||||||
|
fun addState(state: MangaState): Builder = apply { states.add(state) }
|
||||||
|
fun addStates(states: Collection<MangaState>): Builder = apply { this.states.addAll(states) }
|
||||||
|
fun addContentRating(rating: ContentRating): Builder = apply { contentRating.add(rating) }
|
||||||
|
fun addContentRatings(ratings: Collection<ContentRating>): Builder =
|
||||||
|
apply { this.contentRating.addAll(ratings) }
|
||||||
|
|
||||||
|
fun addType(type: ContentType): Builder = apply { types.add(type) }
|
||||||
|
fun addTypes(types: Collection<ContentType>): Builder = apply { this.types.addAll(types) }
|
||||||
|
fun addDemographic(demographic: Demographic): Builder = apply { demographics.add(demographic) }
|
||||||
|
fun addDemographics(demographics: Collection<Demographic>): Builder =
|
||||||
|
apply { this.demographics.addAll(demographics) }
|
||||||
|
|
||||||
|
fun year(year: Int): Builder = apply { this.year = year }
|
||||||
|
fun yearFrom(year: Int): Builder = apply { this.yearFrom = year }
|
||||||
|
fun yearTo(year: Int): Builder = apply { this.yearTo = year }
|
||||||
|
|
||||||
|
fun build(): MangaListFilter = MangaListFilter(
|
||||||
|
query, tags, tagsExclude, locale, originalLocale, states,
|
||||||
|
contentRating, types, demographics, year, yearFrom, yearTo,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,56 @@
|
|||||||
|
package org.koitharu.kotatsu.parsers.model
|
||||||
|
|
||||||
|
import org.koitharu.kotatsu.parsers.InternalParsersApi
|
||||||
|
|
||||||
|
public data class MangaListFilterCapabilities @InternalParsersApi constructor(
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether parser supports filtering by more than one tag
|
||||||
|
* @see [MangaListFilter.tags]
|
||||||
|
* @see [MangaListFilterOptions.availableTags]
|
||||||
|
*/
|
||||||
|
val isMultipleTagsSupported: Boolean = false,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether parser supports tagsExclude field in filter
|
||||||
|
* @see [MangaListFilter.tagsExclude]
|
||||||
|
* @see [MangaListFilterOptions.availableTags]
|
||||||
|
*/
|
||||||
|
val isTagsExclusionSupported: Boolean = false,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether parser supports searching by string query
|
||||||
|
* @see [MangaListFilter.query]
|
||||||
|
*/
|
||||||
|
val isSearchSupported: Boolean = false,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether parser supports searching by string query combined within other filters
|
||||||
|
*/
|
||||||
|
val isSearchWithFiltersSupported: Boolean = false,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether parser supports searching/filtering by year
|
||||||
|
* @see [MangaListFilter.year]
|
||||||
|
*/
|
||||||
|
val isYearSupported: Boolean = false,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether parser supports searching by year range
|
||||||
|
* @see [MangaListFilter.yearFrom] and [MangaListFilter.yearTo]
|
||||||
|
*/
|
||||||
|
val isYearRangeSupported: Boolean = false,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether parser supports searching Original Languages
|
||||||
|
* @see [MangaListFilter.originalLocale]
|
||||||
|
* @see [MangaListFilterOptions.availableLocales]
|
||||||
|
*/
|
||||||
|
val isOriginalLocaleSupported: Boolean = false,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether parser supports searching by author name
|
||||||
|
* @see [MangaListFilter.author]
|
||||||
|
*/
|
||||||
|
val isAuthorSearchSupported: Boolean = false,
|
||||||
|
)
|
||||||
@ -0,0 +1,45 @@
|
|||||||
|
package org.koitharu.kotatsu.parsers.model
|
||||||
|
|
||||||
|
import org.koitharu.kotatsu.parsers.InternalParsersApi
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
public data class MangaListFilterOptions @InternalParsersApi constructor(
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Available tags (genres)
|
||||||
|
*/
|
||||||
|
public val availableTags: Set<MangaTag> = emptySet(),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Supported [MangaState] variants for filtering. May be empty.
|
||||||
|
*
|
||||||
|
* For better performance use [EnumSet] for more than one item.
|
||||||
|
*/
|
||||||
|
public val availableStates: Set<MangaState> = emptySet(),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Supported [ContentRating] variants for filtering. May be empty.
|
||||||
|
*
|
||||||
|
* For better performance use [EnumSet] for more than one item.
|
||||||
|
*/
|
||||||
|
public val availableContentRating: Set<ContentRating> = emptySet(),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Supported [ContentType] variants for filtering. May be empty.
|
||||||
|
*
|
||||||
|
* For better performance use [EnumSet] for more than one item.
|
||||||
|
*/
|
||||||
|
public val availableContentTypes: Set<ContentType> = emptySet(),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Supported [Demographic] variants for filtering. May be empty.
|
||||||
|
*
|
||||||
|
* For better performance use [EnumSet] for more than one item.
|
||||||
|
*/
|
||||||
|
public val availableDemographics: Set<Demographic> = emptySet(),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Supported content locales for multilingual sources
|
||||||
|
*/
|
||||||
|
public val availableLocales: Set<Locale> = emptySet(),
|
||||||
|
)
|
||||||
@ -0,0 +1,6 @@
|
|||||||
|
package org.koitharu.kotatsu.parsers.model
|
||||||
|
|
||||||
|
public interface MangaSource {
|
||||||
|
|
||||||
|
public val name: String
|
||||||
|
}
|
||||||
@ -1,5 +1,5 @@
|
|||||||
package org.koitharu.kotatsu.parsers.model
|
package org.koitharu.kotatsu.parsers.model
|
||||||
|
|
||||||
enum class MangaState {
|
public enum class MangaState {
|
||||||
ONGOING, FINISHED
|
ONGOING, FINISHED, ABANDONED, PAUSED, UPCOMING, RESTRICTED
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,9 +1,22 @@
|
|||||||
package org.koitharu.kotatsu.parsers.model
|
package org.koitharu.kotatsu.parsers.model
|
||||||
|
|
||||||
enum class SortOrder {
|
public enum class SortOrder {
|
||||||
UPDATED,
|
UPDATED,
|
||||||
|
UPDATED_ASC,
|
||||||
POPULARITY,
|
POPULARITY,
|
||||||
|
POPULARITY_ASC,
|
||||||
RATING,
|
RATING,
|
||||||
|
RATING_ASC,
|
||||||
NEWEST,
|
NEWEST,
|
||||||
ALPHABETICAL
|
NEWEST_ASC,
|
||||||
}
|
ALPHABETICAL,
|
||||||
|
ALPHABETICAL_DESC,
|
||||||
|
ADDED,
|
||||||
|
ADDED_ASC,
|
||||||
|
RELEVANCE,
|
||||||
|
POPULARITY_HOUR,
|
||||||
|
POPULARITY_TODAY,
|
||||||
|
POPULARITY_WEEK,
|
||||||
|
POPULARITY_MONTH,
|
||||||
|
POPULARITY_YEAR,
|
||||||
|
}
|
||||||
|
|||||||
@ -0,0 +1,90 @@
|
|||||||
|
package org.koitharu.kotatsu.parsers.model.search
|
||||||
|
|
||||||
|
import androidx.collection.ArrayMap
|
||||||
|
import androidx.collection.ArraySet
|
||||||
|
import org.koitharu.kotatsu.parsers.model.SortOrder
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a search query for filtering and sorting manga search results.
|
||||||
|
* This class is immutable and must be constructed using the [Builder].
|
||||||
|
*
|
||||||
|
* @property criteria The set of search criteria applied to the query.
|
||||||
|
* @property order The sorting order for the results (optional).
|
||||||
|
* @property offset The offset number for paginated search results (optional).
|
||||||
|
*/
|
||||||
|
|
||||||
|
@Deprecated("Too complex. Use MangaListFilter instead")
|
||||||
|
@ConsistentCopyVisibility
|
||||||
|
public data class MangaSearchQuery private constructor(
|
||||||
|
@JvmField public val criteria: Set<QueryCriteria<*>>,
|
||||||
|
@JvmField public val order: SortOrder?,
|
||||||
|
@JvmField public val offset: Int,
|
||||||
|
@JvmField public val skipValidation: Boolean,
|
||||||
|
) {
|
||||||
|
|
||||||
|
public fun newBuilder(): Builder = Builder(this)
|
||||||
|
|
||||||
|
public class Builder {
|
||||||
|
|
||||||
|
private val criteria = ArraySet<QueryCriteria<*>>()
|
||||||
|
private var order: SortOrder? = null
|
||||||
|
private var offset: Int = 0
|
||||||
|
private var skipValidation: Boolean = false
|
||||||
|
|
||||||
|
public constructor()
|
||||||
|
|
||||||
|
public constructor(query: MangaSearchQuery) : this() {
|
||||||
|
criteria.addAll(query.criteria)
|
||||||
|
order = query.order
|
||||||
|
offset = query.offset
|
||||||
|
}
|
||||||
|
|
||||||
|
public fun criterion(criterion: QueryCriteria<*>): Builder = apply { criteria.add(criterion) }
|
||||||
|
|
||||||
|
public fun order(order: SortOrder?): Builder = apply { this.order = order }
|
||||||
|
|
||||||
|
public fun offset(offset: Int): Builder = apply { this.offset = offset }
|
||||||
|
|
||||||
|
public fun skipValidation(skip: Boolean): Builder = apply { this.skipValidation = skip }
|
||||||
|
|
||||||
|
@Throws(IllegalArgumentException::class)
|
||||||
|
public fun build(): MangaSearchQuery {
|
||||||
|
return MangaSearchQuery(deduplicateCriteria(criteria), order, offset, skipValidation)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun deduplicateCriteria(criteria: Set<QueryCriteria<*>>): Set<QueryCriteria<*>> {
|
||||||
|
val uniqueCriteria =
|
||||||
|
ArrayMap<Pair<SearchableField, Class<out QueryCriteria<*>>>, QueryCriteria<*>>(criteria.size)
|
||||||
|
|
||||||
|
for (criterion in criteria) {
|
||||||
|
val key = criterion.field to criterion::class.java
|
||||||
|
val existing = uniqueCriteria[key]
|
||||||
|
|
||||||
|
when {
|
||||||
|
existing == null -> uniqueCriteria[key] = criterion
|
||||||
|
|
||||||
|
existing is QueryCriteria.Include<*> && criterion is QueryCriteria.Include<*> -> {
|
||||||
|
uniqueCriteria[key] =
|
||||||
|
QueryCriteria.Include(criterion.field, existing.values union criterion.values)
|
||||||
|
}
|
||||||
|
|
||||||
|
existing is QueryCriteria.Exclude<*> && criterion is QueryCriteria.Exclude<*> -> {
|
||||||
|
uniqueCriteria[key] =
|
||||||
|
QueryCriteria.Exclude(criterion.field, existing.values union criterion.values)
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> throw IllegalArgumentException(
|
||||||
|
"Match and Range have only one criterion per type, but found duplicates for: ${criterion.field} in ${criterion::class.simpleName}",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return uniqueCriteria.values.toSet()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public companion object {
|
||||||
|
|
||||||
|
public val EMPTY: MangaSearchQuery = MangaSearchQuery(emptySet(), null, 0, false)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,48 @@
|
|||||||
|
package org.koitharu.kotatsu.parsers.model.search
|
||||||
|
|
||||||
|
import androidx.collection.ArraySet
|
||||||
|
import org.koitharu.kotatsu.parsers.model.search.QueryCriteria.*
|
||||||
|
import org.koitharu.kotatsu.parsers.util.mapToSet
|
||||||
|
|
||||||
|
@Deprecated("Too complex. Use MangaListFilterCapabilities instead")
|
||||||
|
@ExposedCopyVisibility
|
||||||
|
public data class MangaSearchQueryCapabilities internal constructor(
|
||||||
|
public val capabilities: Set<SearchCapability>,
|
||||||
|
) {
|
||||||
|
|
||||||
|
public constructor(vararg capabilities: SearchCapability) : this(ArraySet(capabilities))
|
||||||
|
|
||||||
|
internal fun validate(query: MangaSearchQuery) {
|
||||||
|
val strictFields = capabilities.filter { it.isExclusive }.mapToSet { it.field }
|
||||||
|
val usedStrictFields = query.criteria.mapToSet { it.field }.intersect(strictFields)
|
||||||
|
|
||||||
|
require(usedStrictFields.isEmpty() || query.criteria.size <= 1) {
|
||||||
|
"Query contains multiple criteria, but at least one field (${usedStrictFields.joinToString()}) does not support multiple criteria."
|
||||||
|
}
|
||||||
|
for (criterion in query.criteria) {
|
||||||
|
val capability = requireNotNull(capabilities.find { it.field == criterion.field }) {
|
||||||
|
"Unsupported search field: ${criterion.field}"
|
||||||
|
}
|
||||||
|
|
||||||
|
require(criterion::class in capability.criteriaTypes) {
|
||||||
|
"Unsupported search criterion: ${criterion::class.simpleName} for field ${criterion.field}"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure single value per criterion if supportMultiValue is false
|
||||||
|
if (!capability.isMultiple) {
|
||||||
|
when (criterion) {
|
||||||
|
is Include<*> -> require(criterion.values.size <= 1) {
|
||||||
|
"Multiple values are not allowed for field ${criterion.field}"
|
||||||
|
}
|
||||||
|
|
||||||
|
is Exclude<*> -> require(criterion.values.size <= 1) {
|
||||||
|
"Multiple values are not allowed for field ${criterion.field}"
|
||||||
|
}
|
||||||
|
|
||||||
|
is Range<*> -> Unit // Range is always valid (from, to)
|
||||||
|
is Match<*> -> Unit // Match always has a single value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,106 @@
|
|||||||
|
package org.koitharu.kotatsu.parsers.model.search
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a generic search criterion used for filtering manga search results.
|
||||||
|
* Each criterion applies a specific condition to a [SearchableField] and operates on values of type [T].
|
||||||
|
*
|
||||||
|
* @param T The type of value associated with the search criterion.
|
||||||
|
* @property field The field to which this search criterion applies.
|
||||||
|
*/
|
||||||
|
@Deprecated("Too complex")
|
||||||
|
public sealed interface QueryCriteria<T> {
|
||||||
|
|
||||||
|
public val field: SearchableField
|
||||||
|
|
||||||
|
override fun equals(other: Any?): Boolean
|
||||||
|
|
||||||
|
override fun hashCode(): Int
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents an inclusion criterion that allows search results based on a set of allowed values.
|
||||||
|
*
|
||||||
|
* @param T The type of value being included in the search.
|
||||||
|
* @property values The set of values that should be included in the search results.
|
||||||
|
*
|
||||||
|
* ### Example Usage:
|
||||||
|
* ```kotlin
|
||||||
|
* val genreFilter = QueryCriteria.Include(SearchableField.STATE, setOf(MangaState.ONGOING, MangaState.FINISHED))
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
public data class Include<T : Any>(
|
||||||
|
public override val field: SearchableField,
|
||||||
|
@JvmField public val values: Set<T>,
|
||||||
|
) : QueryCriteria<T> {
|
||||||
|
|
||||||
|
init {
|
||||||
|
check(values.all { x -> field.type.isInstance(x) })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents an exclusion criterion that exclude results containing certain values.
|
||||||
|
*
|
||||||
|
* @param T The type of value being excluded from the search.
|
||||||
|
* @property values The set of values that should be excluded from the search results.
|
||||||
|
*
|
||||||
|
* ### Example Usage:
|
||||||
|
* ```kotlin
|
||||||
|
* val excludeTag = QueryCriteria.Exclude(SearchableField.TAG, setOf(MangaTag(key, title, source)))
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
public data class Exclude<T : Any>(
|
||||||
|
public override val field: SearchableField,
|
||||||
|
@JvmField public val values: Set<T>,
|
||||||
|
) : QueryCriteria<T> {
|
||||||
|
|
||||||
|
init {
|
||||||
|
check(values.all { x -> field.type.isInstance(x) })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a range criterion that allows search based on a range of values.
|
||||||
|
*
|
||||||
|
* @param T The type of value used in the range (must be comparable).
|
||||||
|
* @property from The starting value of the range (inclusive).
|
||||||
|
* @property to The ending value of the range (inclusive).
|
||||||
|
*
|
||||||
|
* ### Example Usage:
|
||||||
|
* ```kotlin
|
||||||
|
* val yearRange = QueryCriteria.Range(SearchableField.PUBLICATION_YEAR, 2000, 2020)
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
public data class Range<T : Comparable<T>>(
|
||||||
|
public override val field: SearchableField,
|
||||||
|
@JvmField public val from: T,
|
||||||
|
@JvmField public val to: T,
|
||||||
|
) : QueryCriteria<T> {
|
||||||
|
|
||||||
|
init {
|
||||||
|
check(field.type.isInstance(from))
|
||||||
|
check(field.type.isInstance(to))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a match criterion that search results based on an exact match of a value.
|
||||||
|
*
|
||||||
|
* @param T The type of value being matched.
|
||||||
|
* @property value The exact value that must be matched.
|
||||||
|
*
|
||||||
|
* ### Example Usage:
|
||||||
|
* ```kotlin
|
||||||
|
* val titleMatch = QueryCriteria.Match(SearchableField.TITLE, "manga title")
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
public data class Match<T : Any>(
|
||||||
|
public override val field: SearchableField,
|
||||||
|
@JvmField public val value: T,
|
||||||
|
) : QueryCriteria<T> {
|
||||||
|
|
||||||
|
init {
|
||||||
|
check(field.type.isInstance(value))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,34 @@
|
|||||||
|
package org.koitharu.kotatsu.parsers.model.search
|
||||||
|
|
||||||
|
import kotlin.reflect.KClass
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Defines the search capabilities of a given field in the manga search query.
|
||||||
|
*
|
||||||
|
* @property field The searchable field that this capability applies to.
|
||||||
|
* Example values:
|
||||||
|
* - `SearchableField.TITLE_NAME` for searching by title.
|
||||||
|
* - `SearchableField.AUTHOR` for searching by author names.
|
||||||
|
* - `SearchableField.TAG` for filtering by tags.
|
||||||
|
* @property criteriaTypes The set of supported criteria types for the field.
|
||||||
|
* Example values:
|
||||||
|
* - `setOf(Include::class, Exclude::class)` selected field supports inclusion/exclusion criteria.
|
||||||
|
* - `setOf(Range::class)` selected field support numerical range criteria.
|
||||||
|
* @property isMultiValue Indicates whether the field supports multiple values.
|
||||||
|
* - `true` if multiple values can be provided (e.g., multiple tags or authors).
|
||||||
|
* - `false` if only a single value is allowed (e.g., only one tag or author).
|
||||||
|
* @property isExclusive Specifies whether the field can be used alongside other criteria.
|
||||||
|
* - `true` if this field can be used with other search criteria.
|
||||||
|
* - `false` if using this field requires it to be the only criterion in query.
|
||||||
|
*/
|
||||||
|
@Deprecated("Too complex")
|
||||||
|
public data class SearchCapability(
|
||||||
|
/** The searchable field that this capability applies to. */
|
||||||
|
@JvmField public val field: SearchableField,
|
||||||
|
/** The set of supported criteria types for this field. */
|
||||||
|
@JvmField public val criteriaTypes: Set<KClass<out QueryCriteria<*>>>,
|
||||||
|
/** Indicates whether the field supports multiple values. */
|
||||||
|
@JvmField public val isMultiple: Boolean,
|
||||||
|
/** Specifies whether the field can be used alongside other criteria. */
|
||||||
|
@JvmField public val isExclusive: Boolean = false,
|
||||||
|
)
|
||||||
@ -0,0 +1,24 @@
|
|||||||
|
package org.koitharu.kotatsu.parsers.model.search
|
||||||
|
|
||||||
|
import org.koitharu.kotatsu.parsers.model.*
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents the various fields that can be used for searching manga.
|
||||||
|
* Each field is associated with a specific data type that defines its expected values.
|
||||||
|
*
|
||||||
|
* @property type The Java class representing the expected type of values for this field.
|
||||||
|
*/
|
||||||
|
@Deprecated("Too complex")
|
||||||
|
public enum class SearchableField(public val type: Class<*>) {
|
||||||
|
TITLE_NAME(String::class.java),
|
||||||
|
TAG(MangaTag::class.java),
|
||||||
|
AUTHOR(MangaTag::class.java),
|
||||||
|
LANGUAGE(Locale::class.java),
|
||||||
|
ORIGINAL_LANGUAGE(Locale::class.java),
|
||||||
|
STATE(MangaState::class.java),
|
||||||
|
CONTENT_TYPE(ContentType::class.java),
|
||||||
|
CONTENT_RATING(ContentRating::class.java),
|
||||||
|
DEMOGRAPHIC(Demographic::class.java),
|
||||||
|
PUBLICATION_YEAR(Int::class.javaObjectType);
|
||||||
|
}
|
||||||
@ -0,0 +1,47 @@
|
|||||||
|
package org.koitharu.kotatsu.parsers.network
|
||||||
|
|
||||||
|
import okhttp3.CookieJar
|
||||||
|
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||||
|
import okhttp3.Response
|
||||||
|
import org.jsoup.Jsoup
|
||||||
|
import java.net.HttpURLConnection.HTTP_FORBIDDEN
|
||||||
|
import java.net.HttpURLConnection.HTTP_UNAVAILABLE
|
||||||
|
|
||||||
|
public object CloudFlareHelper {
|
||||||
|
|
||||||
|
public const val PROTECTION_NOT_DETECTED: Int = 0
|
||||||
|
public const val PROTECTION_CAPTCHA: Int = 1
|
||||||
|
public const val PROTECTION_BLOCKED: Int = 2
|
||||||
|
|
||||||
|
private const val CF_CLEARANCE = "cf_clearance"
|
||||||
|
|
||||||
|
public fun checkResponseForProtection(response: Response): Int {
|
||||||
|
if (response.code != HTTP_FORBIDDEN && response.code != HTTP_UNAVAILABLE) {
|
||||||
|
return PROTECTION_NOT_DETECTED
|
||||||
|
}
|
||||||
|
val content = try {
|
||||||
|
response.peekBody(Long.MAX_VALUE).use {
|
||||||
|
Jsoup.parse(it.byteStream(), Charsets.UTF_8.name(), response.request.url.toString())
|
||||||
|
}
|
||||||
|
} catch (_: IllegalStateException) {
|
||||||
|
return PROTECTION_NOT_DETECTED
|
||||||
|
}
|
||||||
|
return when {
|
||||||
|
content.selectFirst("h2[data-translate=\"blocked_why_headline\"]") != null -> PROTECTION_BLOCKED
|
||||||
|
content.getElementById("challenge-error-title") != null || content.getElementById("challenge-error-text") != null -> PROTECTION_CAPTCHA
|
||||||
|
|
||||||
|
else -> PROTECTION_NOT_DETECTED
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public fun getClearanceCookie(cookieJar: CookieJar, url: String): String? {
|
||||||
|
return cookieJar.loadForRequest(url.toHttpUrl()).find { it.name == CF_CLEARANCE }?.value
|
||||||
|
}
|
||||||
|
|
||||||
|
public fun isCloudFlareCookie(name: String): Boolean {
|
||||||
|
return name.startsWith("cf_")
|
||||||
|
|| name.startsWith("_cf")
|
||||||
|
|| name.startsWith("__cf")
|
||||||
|
|| name == "csrftoken"
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,133 @@
|
|||||||
|
package org.koitharu.kotatsu.parsers.network
|
||||||
|
|
||||||
|
import okhttp3.*
|
||||||
|
import okhttp3.MediaType.Companion.toMediaType
|
||||||
|
import okhttp3.RequestBody.Companion.toRequestBody
|
||||||
|
import org.json.JSONObject
|
||||||
|
import org.jsoup.HttpStatusException
|
||||||
|
import org.koitharu.kotatsu.parsers.exception.AuthRequiredException
|
||||||
|
import org.koitharu.kotatsu.parsers.exception.GraphQLException
|
||||||
|
import org.koitharu.kotatsu.parsers.exception.NotFoundException
|
||||||
|
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||||
|
import org.koitharu.kotatsu.parsers.util.await
|
||||||
|
import org.koitharu.kotatsu.parsers.util.parseJson
|
||||||
|
import java.net.HttpURLConnection
|
||||||
|
|
||||||
|
public class OkHttpWebClient(
|
||||||
|
private val httpClient: OkHttpClient,
|
||||||
|
private val mangaSource: MangaSource,
|
||||||
|
) : WebClient {
|
||||||
|
|
||||||
|
override suspend fun httpGet(url: HttpUrl, extraHeaders: Headers?): Response {
|
||||||
|
val request = Request.Builder()
|
||||||
|
.get()
|
||||||
|
.url(url)
|
||||||
|
.addTags()
|
||||||
|
.addExtraHeaders(extraHeaders)
|
||||||
|
return httpClient.newCall(request.build()).await().ensureSuccess()
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun httpHead(url: HttpUrl): Response {
|
||||||
|
val request = Request.Builder()
|
||||||
|
.head()
|
||||||
|
.url(url)
|
||||||
|
.addTags()
|
||||||
|
return httpClient.newCall(request.build()).await().ensureSuccess()
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun httpPost(url: HttpUrl, form: Map<String, String>, extraHeaders: Headers?): Response {
|
||||||
|
val body = FormBody.Builder()
|
||||||
|
form.forEach { (k, v) ->
|
||||||
|
body.addEncoded(k, v)
|
||||||
|
}
|
||||||
|
val request = Request.Builder()
|
||||||
|
.post(body.build())
|
||||||
|
.url(url)
|
||||||
|
.addTags()
|
||||||
|
.addExtraHeaders(extraHeaders)
|
||||||
|
return httpClient.newCall(request.build()).await().ensureSuccess()
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun httpPost(url: HttpUrl, payload: String, extraHeaders: Headers?): Response {
|
||||||
|
val body = FormBody.Builder()
|
||||||
|
payload.split('&').forEach {
|
||||||
|
val pos = it.indexOf('=')
|
||||||
|
if (pos != -1) {
|
||||||
|
val k = it.substring(0, pos)
|
||||||
|
val v = it.substring(pos + 1)
|
||||||
|
body.addEncoded(k, v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val request = Request.Builder()
|
||||||
|
.post(body.build())
|
||||||
|
.url(url)
|
||||||
|
.addTags()
|
||||||
|
.addExtraHeaders(extraHeaders)
|
||||||
|
return httpClient.newCall(request.build()).await().ensureSuccess()
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun httpPost(url: HttpUrl, body: JSONObject, extraHeaders: Headers?): Response {
|
||||||
|
val mediaType = "application/json; charset=utf-8".toMediaType()
|
||||||
|
val requestBody = body.toString().toRequestBody(mediaType)
|
||||||
|
val request = Request.Builder()
|
||||||
|
.post(requestBody)
|
||||||
|
.url(url)
|
||||||
|
.addTags()
|
||||||
|
.addExtraHeaders(extraHeaders)
|
||||||
|
return httpClient.newCall(request.build()).await().ensureSuccess()
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun graphQLQuery(endpoint: String, query: String): JSONObject {
|
||||||
|
val body = JSONObject()
|
||||||
|
body.put("operationName", null as Any?)
|
||||||
|
body.put("variables", JSONObject())
|
||||||
|
body.put("query", "{$query}")
|
||||||
|
|
||||||
|
val mediaType = "application/json; charset=utf-8".toMediaType()
|
||||||
|
val requestBody = body.toString().toRequestBody(mediaType)
|
||||||
|
val request = Request.Builder()
|
||||||
|
.post(requestBody)
|
||||||
|
.url(endpoint)
|
||||||
|
.addTags()
|
||||||
|
val json = httpClient.newCall(request.build()).await().parseJson()
|
||||||
|
json.optJSONArray("errors")?.let {
|
||||||
|
if (it.length() != 0) {
|
||||||
|
throw GraphQLException(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return json
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Request.Builder.addTags(): Request.Builder {
|
||||||
|
tag(MangaSource::class.java, mangaSource)
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Request.Builder.addExtraHeaders(headers: Headers?): Request.Builder {
|
||||||
|
if (headers != null) {
|
||||||
|
headers(headers)
|
||||||
|
}
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Response.ensureSuccess(): Response {
|
||||||
|
val exception: Exception? = when (code) { // Catch some error codes, not all
|
||||||
|
HttpURLConnection.HTTP_NOT_FOUND -> NotFoundException(message, request.url.toString())
|
||||||
|
HttpURLConnection.HTTP_UNAUTHORIZED -> request.tag(MangaSource::class.java)?.let {
|
||||||
|
AuthRequiredException(it)
|
||||||
|
} ?: HttpStatusException(message, code, request.url.toString())
|
||||||
|
|
||||||
|
in 400..599 -> HttpStatusException(message, code, request.url.toString())
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
if (exception != null) {
|
||||||
|
runCatching {
|
||||||
|
close()
|
||||||
|
}.onFailure {
|
||||||
|
exception.addSuppressed(it)
|
||||||
|
}
|
||||||
|
throw exception
|
||||||
|
}
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,17 @@
|
|||||||
|
package org.koitharu.kotatsu.parsers.network
|
||||||
|
|
||||||
|
public object UserAgents {
|
||||||
|
|
||||||
|
public const val CHROME_MOBILE: String =
|
||||||
|
"Mozilla/5.0 (Linux; Android 13) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.5735.196 Mobile Safari/537.36"
|
||||||
|
|
||||||
|
public const val FIREFOX_MOBILE: String =
|
||||||
|
"Mozilla/5.0 (Android 14; Mobile; LG-M255; rv:123.0) Gecko/123.0 Firefox/123.0"
|
||||||
|
|
||||||
|
public const val CHROME_DESKTOP: String =
|
||||||
|
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36"
|
||||||
|
|
||||||
|
public const val FIREFOX_DESKTOP: String = "Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/116.0"
|
||||||
|
|
||||||
|
public const val KOTATSU: String = "Kotatsu/6.8 (Android 13;;; en)"
|
||||||
|
}
|
||||||
@ -0,0 +1,117 @@
|
|||||||
|
package org.koitharu.kotatsu.parsers.network
|
||||||
|
|
||||||
|
import okhttp3.Headers
|
||||||
|
import okhttp3.HttpUrl
|
||||||
|
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||||
|
import okhttp3.Response
|
||||||
|
import org.json.JSONObject
|
||||||
|
|
||||||
|
public interface WebClient {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Do a GET http request to specific url
|
||||||
|
* @param url
|
||||||
|
*/
|
||||||
|
public suspend fun httpGet(url: String): Response = httpGet(url.toHttpUrl())
|
||||||
|
|
||||||
|
public suspend fun httpGet(url: String, extraHeaders: Headers?): Response = httpGet(url.toHttpUrl(), extraHeaders)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Do a GET http request to specific url
|
||||||
|
* @param url
|
||||||
|
*/
|
||||||
|
public suspend fun httpGet(url: HttpUrl): Response = httpGet(url, null)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Do a GET http request to specific url
|
||||||
|
* @param url
|
||||||
|
* @param extraHeaders additional HTTP headers for request
|
||||||
|
*/
|
||||||
|
public suspend fun httpGet(url: HttpUrl, extraHeaders: Headers?): Response
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Do a HEAD http request to specific url
|
||||||
|
* @param url
|
||||||
|
*/
|
||||||
|
public suspend fun httpHead(url: String): Response = httpHead(url.toHttpUrl())
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Do a HEAD http request to specific url
|
||||||
|
* @param url
|
||||||
|
*/
|
||||||
|
public suspend fun httpHead(url: HttpUrl): Response
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Do a POST http request to specific url with `multipart/form-data` payload
|
||||||
|
* @param url
|
||||||
|
* @param form payload as key=>value map
|
||||||
|
*/
|
||||||
|
public suspend fun httpPost(url: String, form: Map<String, String>): Response =
|
||||||
|
httpPost(url.toHttpUrl(), form, null)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Do a POST http request to specific url with `multipart/form-data` payload
|
||||||
|
* @param url
|
||||||
|
* @param form payload as key=>value map
|
||||||
|
*/
|
||||||
|
public suspend fun httpPost(url: HttpUrl, form: Map<String, String>): Response = httpPost(url, form, null)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Do a POST http request to specific url with `multipart/form-data` payload
|
||||||
|
* @param url
|
||||||
|
* @param form payload as key=>value map
|
||||||
|
* @param extraHeaders additional HTTP headers for request
|
||||||
|
*/
|
||||||
|
public suspend fun httpPost(url: HttpUrl, form: Map<String, String>, extraHeaders: Headers?): Response
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Do a POST http request to specific url with `multipart/form-data` payload
|
||||||
|
* @param url
|
||||||
|
* @param payload payload as `key=value` string with `&` separator
|
||||||
|
*/
|
||||||
|
public suspend fun httpPost(url: String, payload: String): Response = httpPost(url.toHttpUrl(), payload, null)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Do a POST http request to specific url with `multipart/form-data` payload
|
||||||
|
* @param url
|
||||||
|
* @param payload payload as `key=value` string with `&` separator
|
||||||
|
*/
|
||||||
|
public suspend fun httpPost(url: HttpUrl, payload: String): Response = httpPost(url, payload, null)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Do a POST http request to specific url with `multipart/form-data` payload
|
||||||
|
* @param url
|
||||||
|
* @param payload payload as `key=value` string with `&` separator
|
||||||
|
* @param extraHeaders additional HTTP headers for request
|
||||||
|
*/
|
||||||
|
public suspend fun httpPost(url: HttpUrl, payload: String, extraHeaders: Headers?): Response
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Do a POST http request to specific url with json payload
|
||||||
|
* @param url
|
||||||
|
* @param body
|
||||||
|
*/
|
||||||
|
public suspend fun httpPost(url: String, body: JSONObject): Response = httpPost(url.toHttpUrl(), body, null)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Do a POST http request to specific url with json payload
|
||||||
|
* @param url
|
||||||
|
* @param body
|
||||||
|
*/
|
||||||
|
public suspend fun httpPost(url: HttpUrl, body: JSONObject): Response = httpPost(url, body, null)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Do a POST http request to specific url with json payload
|
||||||
|
* @param url
|
||||||
|
* @param body
|
||||||
|
* @param extraHeaders additional HTTP headers for request
|
||||||
|
*/
|
||||||
|
public suspend fun httpPost(url: HttpUrl, body: JSONObject, extraHeaders: Headers?): Response
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Do a GraphQL request to specific url
|
||||||
|
* @param endpoint an url
|
||||||
|
* @param query GraphQL request payload
|
||||||
|
*/
|
||||||
|
public suspend fun graphQLQuery(endpoint: String, query: String): JSONObject
|
||||||
|
}
|
||||||
@ -1,248 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.parsers.site
|
|
||||||
|
|
||||||
import androidx.collection.ArrayMap
|
|
||||||
import kotlinx.coroutines.sync.Mutex
|
|
||||||
import kotlinx.coroutines.sync.withLock
|
|
||||||
import org.json.JSONArray
|
|
||||||
import org.jsoup.nodes.Document
|
|
||||||
import org.jsoup.nodes.Element
|
|
||||||
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
|
||||||
import org.koitharu.kotatsu.parsers.MangaSourceParser
|
|
||||||
import org.koitharu.kotatsu.parsers.PagedMangaParser
|
|
||||||
import org.koitharu.kotatsu.parsers.config.ConfigKey
|
|
||||||
import org.koitharu.kotatsu.parsers.model.*
|
|
||||||
import org.koitharu.kotatsu.parsers.util.*
|
|
||||||
import java.text.SimpleDateFormat
|
|
||||||
import java.util.*
|
|
||||||
import kotlin.collections.HashSet
|
|
||||||
|
|
||||||
@MangaSourceParser("BLOGTRUYEN", "BlogTruyen", "vi")
|
|
||||||
class BlogTruyenParser(override val context: MangaLoaderContext) :
|
|
||||||
PagedMangaParser(MangaSource.BLOGTRUYEN, pageSize = 20) {
|
|
||||||
|
|
||||||
override val configKeyDomain: ConfigKey.Domain
|
|
||||||
get() = ConfigKey.Domain("blogtruyen.vn", null)
|
|
||||||
|
|
||||||
override val sortOrders: Set<SortOrder>
|
|
||||||
get() = EnumSet.of(SortOrder.UPDATED)
|
|
||||||
|
|
||||||
private val mutex = Mutex()
|
|
||||||
private val dateFormat = SimpleDateFormat("dd/MM/yyyy HH:mm", Locale.US)
|
|
||||||
private var cacheTags: ArrayMap<String, MangaTag>? = null
|
|
||||||
|
|
||||||
override suspend fun getDetails(manga: Manga): Manga {
|
|
||||||
val doc = context.httpGet(manga.url.toAbsoluteUrl(getDomain())).parseHtml()
|
|
||||||
val descriptionElement = doc.selectFirstOrThrow("div.description")
|
|
||||||
val statusText = descriptionElement
|
|
||||||
.selectFirst("p:contains(Trạng thái) > span.color-red")
|
|
||||||
?.text()
|
|
||||||
|
|
||||||
val state = when (statusText) {
|
|
||||||
"Đang tiến hành" -> MangaState.ONGOING
|
|
||||||
"Đã hoàn thành" -> MangaState.FINISHED
|
|
||||||
else -> null
|
|
||||||
}
|
|
||||||
|
|
||||||
val rating = doc.selectFirst("span.total-vote")?.attr("ng-init")?.let { text ->
|
|
||||||
val like = text.substringAfter("TotalLike=")
|
|
||||||
.substringBefore(';')
|
|
||||||
.toIntOrNull() ?: return@let RATING_UNKNOWN
|
|
||||||
val dislike = text.substringAfter("TotalDisLike=")
|
|
||||||
.toIntOrNull() ?: return@let RATING_UNKNOWN
|
|
||||||
|
|
||||||
when {
|
|
||||||
like == 0 && dislike == 0 -> RATING_UNKNOWN
|
|
||||||
else -> like.toFloat() / (like + dislike)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val tagMap = getOrCreateTagMap()
|
|
||||||
val tags = descriptionElement.select("p > span.category").mapNotNullToSet {
|
|
||||||
val tagName = it.selectFirst("a")?.text()?.trim() ?: return@mapNotNullToSet null
|
|
||||||
tagMap[tagName]
|
|
||||||
}
|
|
||||||
|
|
||||||
return manga.copy(
|
|
||||||
tags = tags,
|
|
||||||
author = descriptionElement.selectFirst("p:contains(Tác giả) > a")?.text(),
|
|
||||||
description = doc.selectFirst(".detail .content")?.html(),
|
|
||||||
chapters = parseChapterList(doc),
|
|
||||||
largeCoverUrl = doc.selectLast("div.thumbnail > img")?.attrAsAbsoluteUrlOrNull("src"),
|
|
||||||
state = state,
|
|
||||||
rating = rating ?: RATING_UNKNOWN,
|
|
||||||
isNsfw = doc.getElementById("warningCategory") != null
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun parseChapterList(doc: Document): List<MangaChapter> {
|
|
||||||
val chapterList = doc.select("#list-chapters > p")
|
|
||||||
return chapterList.asReversed().mapChapters { index, element ->
|
|
||||||
val titleElement = element.selectFirst("span.title > a") ?: return@mapChapters null
|
|
||||||
val name = titleElement.text()
|
|
||||||
val relativeUrl = titleElement.attrAsRelativeUrl("href")
|
|
||||||
val id = relativeUrl.substringAfter('/').substringBefore('/')
|
|
||||||
val uploadDate = dateFormat.tryParse(element.select("span.publishedDate").text())
|
|
||||||
MangaChapter(
|
|
||||||
id = generateUid(id),
|
|
||||||
name = name,
|
|
||||||
number = index + 1,
|
|
||||||
url = relativeUrl,
|
|
||||||
scanlator = null,
|
|
||||||
uploadDate = uploadDate,
|
|
||||||
branch = null,
|
|
||||||
source = source
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun getListPage(
|
|
||||||
page: Int,
|
|
||||||
query: String?,
|
|
||||||
tags: Set<MangaTag>?,
|
|
||||||
sortOrder: SortOrder,
|
|
||||||
): List<Manga> {
|
|
||||||
return when {
|
|
||||||
!query.isNullOrEmpty() -> {
|
|
||||||
val searchUrl = "https://${getDomain()}/timkiem/nangcao/1/0/-1/-1?txt=${query.urlEncoded()}&p=$page"
|
|
||||||
val searchContent = context.httpGet(searchUrl).parseHtml()
|
|
||||||
.selectFirst("section.list-manga-bycate > div.list")
|
|
||||||
parseMangaList(searchContent)
|
|
||||||
}
|
|
||||||
|
|
||||||
!tags.isNullOrEmpty() -> {
|
|
||||||
val tag = tags.oneOrThrowIfMany()!!
|
|
||||||
val categoryAjax = "https://${getDomain()}/ajax/Category/AjaxLoadMangaByCategory?id=${tag.key}&orderBy=5&p=$page"
|
|
||||||
val listContent = context.httpGet(categoryAjax).parseHtml().selectFirst("div.list")
|
|
||||||
parseMangaList(listContent)
|
|
||||||
}
|
|
||||||
else -> getNormalList(page)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun getNormalList(page: Int): List<Manga> {
|
|
||||||
val pageLink = "https://${getDomain()}/page-$page"
|
|
||||||
val doc = context.httpGet(pageLink).parseHtml()
|
|
||||||
val listElements = doc.selectFirstOrThrow("section.list-mainpage.listview")
|
|
||||||
.select("div.bg-white.storyitem")
|
|
||||||
|
|
||||||
return listElements.mapNotNull {
|
|
||||||
val linkTag = it.selectFirst("div.fl-l > a") ?: return@mapNotNull null
|
|
||||||
val relativeUrl = linkTag.attrAsRelativeUrl("href")
|
|
||||||
val tagMap = getOrCreateTagMap()
|
|
||||||
val tags = it.select("footer > div.category > a").mapNotNullToSet { a ->
|
|
||||||
tagMap[a.text()]
|
|
||||||
}
|
|
||||||
|
|
||||||
Manga(
|
|
||||||
id = generateUid(relativeUrl),
|
|
||||||
title = linkTag.attr("title"),
|
|
||||||
altTitle = null,
|
|
||||||
description = it.selectFirst("p.al-j.break.line-height-15")?.text(),
|
|
||||||
url = relativeUrl,
|
|
||||||
publicUrl = relativeUrl.toAbsoluteUrl(getDomain()),
|
|
||||||
coverUrl = linkTag.selectLast("img")?.attr("src").orEmpty(),
|
|
||||||
source = source,
|
|
||||||
tags = tags,
|
|
||||||
isNsfw = false,
|
|
||||||
rating = RATING_UNKNOWN,
|
|
||||||
author = null,
|
|
||||||
state = null,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun parseMangaList(listElement: Element?): List<Manga> {
|
|
||||||
listElement ?: return emptyList()
|
|
||||||
|
|
||||||
return listElement.select("span.tiptip[data-tiptip]").mapNotNull {
|
|
||||||
val mangaInfo = listElement.getElementById(it.attr("data-tiptip")) ?: return@mapNotNull null
|
|
||||||
val a = it.selectFirst("a") ?: return@mapNotNull null
|
|
||||||
val relativeUrl = a.attrAsRelativeUrl("href")
|
|
||||||
Manga(
|
|
||||||
id = generateUid(relativeUrl),
|
|
||||||
title = a.text(),
|
|
||||||
altTitle = null,
|
|
||||||
description = mangaInfo.select("div.al-j.fs-12").text(),
|
|
||||||
url = relativeUrl,
|
|
||||||
publicUrl = relativeUrl.toAbsoluteUrl(getDomain()),
|
|
||||||
coverUrl = mangaInfo.selectFirst("div > img.img")?.absUrl("src").orEmpty(),
|
|
||||||
isNsfw = false,
|
|
||||||
rating = RATING_UNKNOWN,
|
|
||||||
tags = emptySet(),
|
|
||||||
author = null,
|
|
||||||
state = null,
|
|
||||||
source = source,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun getPages(chapter: MangaChapter): List<MangaPage> {
|
|
||||||
fun generateImageId(index: Int) = generateUid("${chapter.url}/$index")
|
|
||||||
|
|
||||||
val doc = context.httpGet(chapter.url.toAbsoluteUrl(getDomain())).parseHtml()
|
|
||||||
val pages = ArrayList<MangaPage>()
|
|
||||||
val referer = chapter.url.toAbsoluteUrl(getDomain())
|
|
||||||
doc.select("#content > img").forEach { img ->
|
|
||||||
val url = img.attrAsRelativeUrl("src")
|
|
||||||
pages.add(
|
|
||||||
MangaPage(
|
|
||||||
id = generateImageId(pages.lastIndex),
|
|
||||||
url = url,
|
|
||||||
referer = referer,
|
|
||||||
preview = null,
|
|
||||||
source = source,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Some chapters use js script to render images
|
|
||||||
val script = doc.selectLast("#content > script")
|
|
||||||
if (script != null && script.data().contains("listImageCaption")) {
|
|
||||||
val imagesStr = script.data().substringBefore(';').substringAfterLast('=').trim()
|
|
||||||
val imageArr = JSONArray(imagesStr)
|
|
||||||
for (i in 0 until imageArr.length()) {
|
|
||||||
val imageUrl = imageArr.getJSONObject(i).getString("url")
|
|
||||||
pages.add(
|
|
||||||
MangaPage(
|
|
||||||
id = generateImageId(pages.lastIndex),
|
|
||||||
url = imageUrl,
|
|
||||||
referer = referer,
|
|
||||||
preview = null,
|
|
||||||
source = source
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return pages
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun getTags(): Set<MangaTag> {
|
|
||||||
val map = getOrCreateTagMap()
|
|
||||||
val tags = HashSet<MangaTag>(map.size)
|
|
||||||
for (entry in map) {
|
|
||||||
tags.add(entry.value)
|
|
||||||
}
|
|
||||||
|
|
||||||
return tags
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private suspend fun getOrCreateTagMap(): ArrayMap<String, MangaTag> = mutex.withLock {
|
|
||||||
cacheTags?.let { return@withLock it }
|
|
||||||
val doc = context.httpGet("/timkiem/nangcao".toAbsoluteUrl(getDomain())).parseHtml()
|
|
||||||
val tagItems = doc.select("li[data-id]")
|
|
||||||
val tagMap = ArrayMap<String, MangaTag>(tagItems.size)
|
|
||||||
for (tag in tagItems) {
|
|
||||||
val title = tag.text().trim()
|
|
||||||
tagMap[tag.text().trim()] = MangaTag(
|
|
||||||
title = title,
|
|
||||||
key = tag.attr("data-id"),
|
|
||||||
source = source
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
cacheTags = tagMap
|
|
||||||
tagMap
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,216 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.parsers.site
|
|
||||||
|
|
||||||
import androidx.collection.ArraySet
|
|
||||||
import androidx.collection.SparseArrayCompat
|
|
||||||
import org.json.JSONArray
|
|
||||||
import org.json.JSONObject
|
|
||||||
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
|
||||||
import org.koitharu.kotatsu.parsers.MangaParser
|
|
||||||
import org.koitharu.kotatsu.parsers.MangaSourceParser
|
|
||||||
import org.koitharu.kotatsu.parsers.config.ConfigKey
|
|
||||||
import org.koitharu.kotatsu.parsers.model.*
|
|
||||||
import org.koitharu.kotatsu.parsers.util.*
|
|
||||||
import org.koitharu.kotatsu.parsers.util.json.*
|
|
||||||
import java.text.SimpleDateFormat
|
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
/**
|
|
||||||
* https://api.comick.fun/docs/static/index.html
|
|
||||||
*/
|
|
||||||
|
|
||||||
private const val PAGE_SIZE = 20
|
|
||||||
private const val CHAPTERS_LIMIT = 99999
|
|
||||||
|
|
||||||
@MangaSourceParser("COMICK_FUN", "ComicK")
|
|
||||||
internal class ComickFunParser(override val context: MangaLoaderContext) : MangaParser(MangaSource.COMICK_FUN) {
|
|
||||||
|
|
||||||
override val configKeyDomain = ConfigKey.Domain("comick.fun", null)
|
|
||||||
|
|
||||||
override val sortOrders: Set<SortOrder> = EnumSet.of(
|
|
||||||
SortOrder.POPULARITY,
|
|
||||||
SortOrder.UPDATED,
|
|
||||||
SortOrder.RATING,
|
|
||||||
)
|
|
||||||
|
|
||||||
@Volatile
|
|
||||||
private var cachedTags: SparseArrayCompat<MangaTag>? = null
|
|
||||||
|
|
||||||
override suspend fun getList(
|
|
||||||
offset: Int,
|
|
||||||
query: String?,
|
|
||||||
tags: Set<MangaTag>?,
|
|
||||||
sortOrder: SortOrder,
|
|
||||||
): List<Manga> {
|
|
||||||
val domain = getDomain()
|
|
||||||
val url = buildString {
|
|
||||||
append("https://api.")
|
|
||||||
append(domain)
|
|
||||||
append("/search?tachiyomi=true")
|
|
||||||
if (!query.isNullOrEmpty()) {
|
|
||||||
if (offset > 0) {
|
|
||||||
return emptyList()
|
|
||||||
}
|
|
||||||
append("&q=")
|
|
||||||
append(query.urlEncoded())
|
|
||||||
} else {
|
|
||||||
append("&limit=")
|
|
||||||
append(PAGE_SIZE)
|
|
||||||
append("&page=")
|
|
||||||
append((offset / PAGE_SIZE) + 1)
|
|
||||||
if (!tags.isNullOrEmpty()) {
|
|
||||||
append("&genres=")
|
|
||||||
appendAll(tags, "&genres=", MangaTag::key)
|
|
||||||
}
|
|
||||||
append("&sort=") // view, uploaded, rating, follow, user_follow_count
|
|
||||||
append(
|
|
||||||
when (sortOrder) {
|
|
||||||
SortOrder.POPULARITY -> "view"
|
|
||||||
SortOrder.RATING -> "rating"
|
|
||||||
else -> "uploaded"
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
val ja = context.httpGet(url).parseJsonArray()
|
|
||||||
val tagsMap = cachedTags ?: loadTags()
|
|
||||||
return ja.mapJSON { jo ->
|
|
||||||
val slug = jo.getString("slug")
|
|
||||||
Manga(
|
|
||||||
id = generateUid(slug),
|
|
||||||
title = jo.getString("title"),
|
|
||||||
altTitle = null,
|
|
||||||
url = slug,
|
|
||||||
publicUrl = "https://$domain/comic/$slug",
|
|
||||||
rating = jo.getDoubleOrDefault("rating", -10.0).toFloat() / 10f,
|
|
||||||
isNsfw = false,
|
|
||||||
coverUrl = jo.getString("cover_url"),
|
|
||||||
largeCoverUrl = null,
|
|
||||||
description = jo.getStringOrNull("desc"),
|
|
||||||
tags = jo.selectGenres("genres", tagsMap),
|
|
||||||
state = runCatching {
|
|
||||||
if (jo.getBoolean("translation_completed")) {
|
|
||||||
MangaState.FINISHED
|
|
||||||
} else {
|
|
||||||
MangaState.ONGOING
|
|
||||||
}
|
|
||||||
}.getOrNull(),
|
|
||||||
author = null,
|
|
||||||
source = source,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun getDetails(manga: Manga): Manga {
|
|
||||||
val domain = getDomain()
|
|
||||||
val url = "https://api.$domain/comic/${manga.url}?tachiyomi=true"
|
|
||||||
val jo = context.httpGet(url).parseJson()
|
|
||||||
val comic = jo.getJSONObject("comic")
|
|
||||||
return manga.copy(
|
|
||||||
title = comic.getString("title"),
|
|
||||||
altTitle = null, // TODO
|
|
||||||
isNsfw = jo.getBoolean("matureContent") || comic.getBoolean("hentai"),
|
|
||||||
description = comic.getStringOrNull("parsed") ?: comic.getString("desc"),
|
|
||||||
tags = manga.tags + jo.getJSONArray("genres").mapJSONToSet {
|
|
||||||
MangaTag(
|
|
||||||
title = it.getString("name"),
|
|
||||||
key = it.getString("slug"),
|
|
||||||
source = source,
|
|
||||||
)
|
|
||||||
},
|
|
||||||
author = jo.getJSONArray("artists").optJSONObject(0)?.getString("name"),
|
|
||||||
chapters = getChapters(comic.getLong("id")),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun getPages(chapter: MangaChapter): List<MangaPage> {
|
|
||||||
val jo = context.httpGet(
|
|
||||||
"https://api.${getDomain()}/chapter/${chapter.url}?tachiyomi=true",
|
|
||||||
).parseJson().getJSONObject("chapter")
|
|
||||||
val referer = "https://${getDomain()}/"
|
|
||||||
return jo.getJSONArray("images").mapJSON {
|
|
||||||
val url = it.getString("url")
|
|
||||||
MangaPage(
|
|
||||||
id = generateUid(url),
|
|
||||||
url = url,
|
|
||||||
referer = referer,
|
|
||||||
preview = null,
|
|
||||||
source = source,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun getTags(): Set<MangaTag> {
|
|
||||||
val sparseArray = cachedTags ?: loadTags()
|
|
||||||
val set = ArraySet<MangaTag>(sparseArray.size())
|
|
||||||
for (i in 0 until sparseArray.size()) {
|
|
||||||
set.add(sparseArray.valueAt(i))
|
|
||||||
}
|
|
||||||
return set
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun loadTags(): SparseArrayCompat<MangaTag> {
|
|
||||||
val ja = context.httpGet("https://api.${getDomain()}/genre").parseJsonArray()
|
|
||||||
val tags = SparseArrayCompat<MangaTag>(ja.length())
|
|
||||||
for (jo in ja.JSONIterator()) {
|
|
||||||
tags.append(
|
|
||||||
jo.getInt("id"),
|
|
||||||
MangaTag(
|
|
||||||
title = jo.getString("name"),
|
|
||||||
key = jo.getString("slug"),
|
|
||||||
source = source,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
cachedTags = tags
|
|
||||||
return tags
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun getChapters(id: Long): List<MangaChapter> {
|
|
||||||
val ja = context.httpGet(
|
|
||||||
url = "https://api.${getDomain()}/comic/$id/chapter?tachiyomi=true&limit=$CHAPTERS_LIMIT",
|
|
||||||
).parseJson().getJSONArray("chapters")
|
|
||||||
val dateFormat = SimpleDateFormat("yyyy-MM-dd")
|
|
||||||
val counters = HashMap<Locale, Int>()
|
|
||||||
return ja.mapReversed { jo ->
|
|
||||||
val locale = Locale.forLanguageTag(jo.getString("lang"))
|
|
||||||
var number = counters[locale] ?: 0
|
|
||||||
number++
|
|
||||||
counters[locale] = number
|
|
||||||
MangaChapter(
|
|
||||||
id = generateUid(jo.getLong("id")),
|
|
||||||
name = buildString {
|
|
||||||
jo.getStringOrNull("vol")?.let { append("Vol ").append(it).append(' ') }
|
|
||||||
jo.getStringOrNull("chap")?.let { append("Chap ").append(it) }
|
|
||||||
jo.getStringOrNull("title")?.let { append(": ").append(it) }
|
|
||||||
},
|
|
||||||
number = number,
|
|
||||||
url = jo.getString("hid"),
|
|
||||||
scanlator = jo.optJSONArray("group_name")?.optString(0),
|
|
||||||
uploadDate = dateFormat.tryParse(jo.getString("created_at").substringBefore('T')),
|
|
||||||
branch = locale.getDisplayName(locale).toTitleCase(locale),
|
|
||||||
source = source,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private inline fun <R> JSONArray.mapReversed(block: (JSONObject) -> R): List<R> {
|
|
||||||
val len = length()
|
|
||||||
val destination = ArrayList<R>(len)
|
|
||||||
for (i in (0 until len).reversed()) {
|
|
||||||
val jo = getJSONObject(i)
|
|
||||||
destination.add(block(jo))
|
|
||||||
}
|
|
||||||
return destination
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun JSONObject.selectGenres(name: String, tags: SparseArrayCompat<MangaTag>): Set<MangaTag> {
|
|
||||||
val array = optJSONArray(name) ?: return emptySet()
|
|
||||||
val res = ArraySet<MangaTag>(array.length())
|
|
||||||
for (i in 0 until array.length()) {
|
|
||||||
val id = array.getInt(i)
|
|
||||||
val tag = tags.get(id) ?: continue
|
|
||||||
res.add(tag)
|
|
||||||
}
|
|
||||||
return res
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,161 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.parsers.site
|
|
||||||
|
|
||||||
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
|
||||||
import org.koitharu.kotatsu.parsers.MangaSourceParser
|
|
||||||
import org.koitharu.kotatsu.parsers.PagedMangaParser
|
|
||||||
import org.koitharu.kotatsu.parsers.config.ConfigKey
|
|
||||||
import org.koitharu.kotatsu.parsers.exception.ParseException
|
|
||||||
import org.koitharu.kotatsu.parsers.model.*
|
|
||||||
import org.koitharu.kotatsu.parsers.util.*
|
|
||||||
import org.koitharu.kotatsu.parsers.util.json.mapJSON
|
|
||||||
import org.koitharu.kotatsu.parsers.util.json.mapJSONIndexed
|
|
||||||
import org.koitharu.kotatsu.parsers.util.json.mapJSONToSet
|
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
@MangaSourceParser("DESUME", "Desu.me", "ru")
|
|
||||||
internal class DesuMeParser(override val context: MangaLoaderContext) : PagedMangaParser(MangaSource.DESUME, 20) {
|
|
||||||
|
|
||||||
override val configKeyDomain = ConfigKey.Domain("desu.me", null)
|
|
||||||
|
|
||||||
override val sortOrders: Set<SortOrder> = EnumSet.of(
|
|
||||||
SortOrder.UPDATED,
|
|
||||||
SortOrder.POPULARITY,
|
|
||||||
SortOrder.NEWEST,
|
|
||||||
SortOrder.ALPHABETICAL,
|
|
||||||
)
|
|
||||||
|
|
||||||
override suspend fun getListPage(
|
|
||||||
page: Int,
|
|
||||||
query: String?,
|
|
||||||
tags: Set<MangaTag>?,
|
|
||||||
sortOrder: SortOrder,
|
|
||||||
): List<Manga> {
|
|
||||||
if (query != null && page != searchPaginator.firstPage) {
|
|
||||||
return emptyList()
|
|
||||||
}
|
|
||||||
val domain = getDomain()
|
|
||||||
val url = buildString {
|
|
||||||
append("https://")
|
|
||||||
append(domain)
|
|
||||||
append("/manga/api/?limit=20&order=")
|
|
||||||
append(getSortKey(sortOrder))
|
|
||||||
append("&page=")
|
|
||||||
append(page)
|
|
||||||
if (!tags.isNullOrEmpty()) {
|
|
||||||
append("&genres=")
|
|
||||||
appendAll(tags, ",") { it.key }
|
|
||||||
}
|
|
||||||
if (query != null) {
|
|
||||||
append("&search=")
|
|
||||||
append(query)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
val json = context.httpGet(url).parseJson().getJSONArray("response")
|
|
||||||
?: throw ParseException("Invalid response", url)
|
|
||||||
val total = json.length()
|
|
||||||
val list = ArrayList<Manga>(total)
|
|
||||||
for (i in 0 until total) {
|
|
||||||
val jo = json.getJSONObject(i)
|
|
||||||
val cover = jo.getJSONObject("image")
|
|
||||||
val id = jo.getLong("id")
|
|
||||||
list += Manga(
|
|
||||||
url = "/manga/api/$id",
|
|
||||||
publicUrl = jo.getString("url"),
|
|
||||||
source = MangaSource.DESUME,
|
|
||||||
title = jo.getString("russian"),
|
|
||||||
altTitle = jo.getString("name"),
|
|
||||||
coverUrl = cover.getString("preview"),
|
|
||||||
largeCoverUrl = cover.getString("original"),
|
|
||||||
state = when {
|
|
||||||
jo.getInt("ongoing") == 1 -> MangaState.ONGOING
|
|
||||||
else -> null
|
|
||||||
},
|
|
||||||
rating = jo.getDouble("score").toFloat().coerceIn(0f, 1f),
|
|
||||||
id = generateUid(id),
|
|
||||||
isNsfw = false,
|
|
||||||
tags = emptySet(),
|
|
||||||
author = null,
|
|
||||||
description = jo.getString("description"),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
return list
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun getDetails(manga: Manga): Manga {
|
|
||||||
val url = manga.url.toAbsoluteUrl(getDomain())
|
|
||||||
val json = context.httpGet(url).parseJson().getJSONObject("response")
|
|
||||||
?: throw ParseException("Invalid response", url)
|
|
||||||
val baseChapterUrl = manga.url + "/chapter/"
|
|
||||||
val chaptersList = json.getJSONObject("chapters").getJSONArray("list")
|
|
||||||
val totalChapters = chaptersList.length()
|
|
||||||
return manga.copy(
|
|
||||||
tags = json.getJSONArray("genres").mapJSONToSet {
|
|
||||||
MangaTag(
|
|
||||||
key = it.getString("text"),
|
|
||||||
title = it.getString("russian").toTitleCase(),
|
|
||||||
source = manga.source,
|
|
||||||
)
|
|
||||||
},
|
|
||||||
publicUrl = json.getString("url"),
|
|
||||||
description = json.getString("description"),
|
|
||||||
chapters = chaptersList.mapJSONIndexed { i, it ->
|
|
||||||
val chid = it.getLong("id")
|
|
||||||
val volChap = "Том " + it.optString("vol", "0") + ". " + "Глава " + it.optString("ch", "0")
|
|
||||||
val title = it.optString("title", "null").takeUnless { it == "null" }
|
|
||||||
MangaChapter(
|
|
||||||
id = generateUid(chid),
|
|
||||||
source = manga.source,
|
|
||||||
url = "$baseChapterUrl$chid",
|
|
||||||
uploadDate = it.getLong("date") * 1000,
|
|
||||||
name = if (title.isNullOrEmpty()) volChap else "$volChap: $title",
|
|
||||||
number = totalChapters - i,
|
|
||||||
scanlator = null,
|
|
||||||
branch = null,
|
|
||||||
)
|
|
||||||
}.reversed(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun getPages(chapter: MangaChapter): List<MangaPage> {
|
|
||||||
val fullUrl = chapter.url.toAbsoluteUrl(getDomain())
|
|
||||||
val json = context.httpGet(fullUrl)
|
|
||||||
.parseJson()
|
|
||||||
.getJSONObject("response") ?: throw ParseException("Invalid response", fullUrl)
|
|
||||||
return json.getJSONObject("pages").getJSONArray("list").mapJSON { jo ->
|
|
||||||
MangaPage(
|
|
||||||
id = generateUid(jo.getLong("id")),
|
|
||||||
referer = fullUrl,
|
|
||||||
preview = null,
|
|
||||||
source = chapter.source,
|
|
||||||
url = jo.getString("img"),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun getTags(): Set<MangaTag> {
|
|
||||||
val doc = context.httpGet("https://${getDomain()}/manga/").parseHtml()
|
|
||||||
val root = doc.body().requireElementById("animeFilter")
|
|
||||||
.selectFirstOrThrow(".catalog-genres")
|
|
||||||
return root.select("li").mapToSet {
|
|
||||||
val input = it.selectFirstOrThrow("input")
|
|
||||||
MangaTag(
|
|
||||||
source = source,
|
|
||||||
key = input.attr("data-genre-slug").ifEmpty {
|
|
||||||
it.parseFailed("data-genre-slug is empty")
|
|
||||||
},
|
|
||||||
title = input.attr("data-genre-name").toTitleCase().ifEmpty {
|
|
||||||
it.parseFailed("data-genre-name is empty")
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getSortKey(sortOrder: SortOrder) =
|
|
||||||
when (sortOrder) {
|
|
||||||
SortOrder.ALPHABETICAL -> "name"
|
|
||||||
SortOrder.POPULARITY -> "popular"
|
|
||||||
SortOrder.UPDATED -> "updated"
|
|
||||||
SortOrder.NEWEST -> "id"
|
|
||||||
else -> "updated"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,279 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.parsers.site
|
|
||||||
|
|
||||||
import org.jsoup.nodes.Element
|
|
||||||
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
|
||||||
import org.koitharu.kotatsu.parsers.MangaParserAuthProvider
|
|
||||||
import org.koitharu.kotatsu.parsers.MangaSourceParser
|
|
||||||
import org.koitharu.kotatsu.parsers.PagedMangaParser
|
|
||||||
import org.koitharu.kotatsu.parsers.config.ConfigKey
|
|
||||||
import org.koitharu.kotatsu.parsers.exception.AuthRequiredException
|
|
||||||
import org.koitharu.kotatsu.parsers.model.*
|
|
||||||
import org.koitharu.kotatsu.parsers.util.*
|
|
||||||
import java.util.*
|
|
||||||
import kotlin.math.pow
|
|
||||||
|
|
||||||
private const val DOMAIN_UNAUTHORIZED = "e-hentai.org"
|
|
||||||
private const val DOMAIN_AUTHORIZED = "exhentai.org"
|
|
||||||
|
|
||||||
@MangaSourceParser("EXHENTAI", "ExHentai")
|
|
||||||
internal class ExHentaiParser(
|
|
||||||
override val context: MangaLoaderContext,
|
|
||||||
) : PagedMangaParser(MangaSource.EXHENTAI, pageSize = 25), MangaParserAuthProvider {
|
|
||||||
|
|
||||||
override val sortOrders: Set<SortOrder> = Collections.singleton(
|
|
||||||
SortOrder.NEWEST,
|
|
||||||
)
|
|
||||||
|
|
||||||
override val configKeyDomain: ConfigKey.Domain
|
|
||||||
get() = ConfigKey.Domain(if (isAuthorized) DOMAIN_AUTHORIZED else DOMAIN_UNAUTHORIZED, null)
|
|
||||||
|
|
||||||
override val authUrl: String
|
|
||||||
get() = "https://${getDomain()}/bounce_login.php"
|
|
||||||
|
|
||||||
private val ratingPattern = Regex("-?[0-9]+px")
|
|
||||||
private val authCookies = arrayOf("ipb_member_id", "ipb_pass_hash")
|
|
||||||
private var updateDm = false
|
|
||||||
|
|
||||||
override val isAuthorized: Boolean
|
|
||||||
get() {
|
|
||||||
val authorized = isAuthorized(DOMAIN_UNAUTHORIZED)
|
|
||||||
if (authorized) {
|
|
||||||
if (!isAuthorized(DOMAIN_AUTHORIZED)) {
|
|
||||||
context.cookieJar.copyCookies(
|
|
||||||
DOMAIN_UNAUTHORIZED,
|
|
||||||
DOMAIN_AUTHORIZED,
|
|
||||||
authCookies,
|
|
||||||
)
|
|
||||||
context.cookieJar.insertCookies(DOMAIN_AUTHORIZED, "yay=louder")
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
init {
|
|
||||||
context.cookieJar.insertCookies(DOMAIN_AUTHORIZED, "nw=1", "sl=dm_2")
|
|
||||||
context.cookieJar.insertCookies(DOMAIN_UNAUTHORIZED, "nw=1", "sl=dm_2")
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun getListPage(
|
|
||||||
page: Int,
|
|
||||||
query: String?,
|
|
||||||
tags: Set<MangaTag>?,
|
|
||||||
sortOrder: SortOrder,
|
|
||||||
): List<Manga> {
|
|
||||||
var search = query?.urlEncoded().orEmpty()
|
|
||||||
val url = buildString {
|
|
||||||
append("https://")
|
|
||||||
append(getDomain())
|
|
||||||
append("/?page=")
|
|
||||||
append(page)
|
|
||||||
if (!tags.isNullOrEmpty()) {
|
|
||||||
var fCats = 0
|
|
||||||
for (tag in tags) {
|
|
||||||
tag.key.toIntOrNull()?.let { fCats = fCats or it } ?: run {
|
|
||||||
search += tag.key + " "
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (fCats != 0) {
|
|
||||||
append("&f_cats=")
|
|
||||||
append(1023 - fCats)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (search.isNotEmpty()) {
|
|
||||||
append("&f_search=")
|
|
||||||
append(search.trim().replace(' ', '+'))
|
|
||||||
}
|
|
||||||
// by unknown reason cookie "sl=dm_2" is ignored, so, we should request it again
|
|
||||||
if (updateDm) {
|
|
||||||
append("&inline_set=dm_e")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
val body = context.httpGet(url).parseHtml().body()
|
|
||||||
val root = body.selectFirst("table.itg")
|
|
||||||
?.selectFirst("tbody")
|
|
||||||
?: if (updateDm) {
|
|
||||||
body.parseFailed("Cannot find root")
|
|
||||||
} else {
|
|
||||||
updateDm = true
|
|
||||||
return getListPage(page, query, tags, sortOrder)
|
|
||||||
}
|
|
||||||
updateDm = false
|
|
||||||
return root.children().mapNotNull { tr ->
|
|
||||||
if (tr.childrenSize() != 2) return@mapNotNull null
|
|
||||||
val (td1, td2) = tr.children()
|
|
||||||
val glink = td2.selectFirstOrThrow("div.glink")
|
|
||||||
val a = glink.parents().select("a").first() ?: glink.parseFailed("link not found")
|
|
||||||
val href = a.attrAsRelativeUrl("href")
|
|
||||||
val tagsDiv = glink.nextElementSibling() ?: glink.parseFailed("tags div not found")
|
|
||||||
val mainTag = td2.selectFirst("div.cn")?.let { div ->
|
|
||||||
MangaTag(
|
|
||||||
title = div.text().toTitleCase(),
|
|
||||||
key = tagIdByClass(div.classNames()) ?: return@let null,
|
|
||||||
source = source,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
Manga(
|
|
||||||
id = generateUid(href),
|
|
||||||
title = glink.text().cleanupTitle(),
|
|
||||||
altTitle = null,
|
|
||||||
url = href,
|
|
||||||
publicUrl = a.absUrl("href"),
|
|
||||||
rating = td2.selectFirst("div.ir")?.parseRating() ?: RATING_UNKNOWN,
|
|
||||||
isNsfw = true,
|
|
||||||
coverUrl = td1.selectFirst("img")?.absUrl("src").orEmpty(),
|
|
||||||
tags = setOfNotNull(mainTag),
|
|
||||||
state = null,
|
|
||||||
author = tagsDiv.getElementsContainingOwnText("artist:").first()
|
|
||||||
?.nextElementSibling()?.text(),
|
|
||||||
source = source,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun getDetails(manga: Manga): Manga {
|
|
||||||
val doc = context.httpGet(manga.url.toAbsoluteUrl(getDomain())).parseHtml()
|
|
||||||
val root = doc.body().selectFirstOrThrow("div.gm")
|
|
||||||
val cover = root.getElementById("gd1")?.children()?.first()
|
|
||||||
val title = root.getElementById("gd2")
|
|
||||||
val taglist = root.getElementById("taglist")
|
|
||||||
val tabs = doc.body().selectFirst("table.ptt")?.selectFirst("tr")
|
|
||||||
return manga.copy(
|
|
||||||
title = title?.getElementById("gn")?.text()?.cleanupTitle() ?: manga.title,
|
|
||||||
altTitle = title?.getElementById("gj")?.text()?.cleanupTitle() ?: manga.altTitle,
|
|
||||||
publicUrl = doc.baseUri().ifEmpty { manga.publicUrl },
|
|
||||||
rating = root.getElementById("rating_label")?.text()
|
|
||||||
?.substringAfterLast(' ')
|
|
||||||
?.toFloatOrNull()
|
|
||||||
?.div(5f) ?: manga.rating,
|
|
||||||
largeCoverUrl = cover?.styleValueOrNull("background")?.cssUrl(),
|
|
||||||
description = taglist?.select("tr")?.joinToString("<br>") { tr ->
|
|
||||||
val (tc, td) = tr.children()
|
|
||||||
val subtags = td.select("a").joinToString { it.html() }
|
|
||||||
"<b>${tc.html()}</b> $subtags"
|
|
||||||
},
|
|
||||||
chapters = tabs?.select("a")?.findLast { a ->
|
|
||||||
a.text().toIntOrNull() != null
|
|
||||||
}?.let { a ->
|
|
||||||
val count = a.text().toInt()
|
|
||||||
val chapters = ChaptersListBuilder(count)
|
|
||||||
for (i in 1..count) {
|
|
||||||
val url = "${manga.url}?p=${i - 1}"
|
|
||||||
chapters += MangaChapter(
|
|
||||||
id = generateUid(url),
|
|
||||||
name = "${manga.title} #$i",
|
|
||||||
number = i,
|
|
||||||
url = url,
|
|
||||||
uploadDate = 0L,
|
|
||||||
source = source,
|
|
||||||
scanlator = null,
|
|
||||||
branch = null,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
chapters.toList()
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun getPages(chapter: MangaChapter): List<MangaPage> {
|
|
||||||
val doc = context.httpGet(chapter.url.toAbsoluteUrl(getDomain())).parseHtml()
|
|
||||||
val root = doc.body().requireElementById("gdt")
|
|
||||||
return root.select("a").map { a ->
|
|
||||||
val url = a.attrAsRelativeUrl("href")
|
|
||||||
MangaPage(
|
|
||||||
id = generateUid(url),
|
|
||||||
url = url,
|
|
||||||
referer = a.absUrl("href"),
|
|
||||||
preview = null,
|
|
||||||
source = source,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun getPageUrl(page: MangaPage): String {
|
|
||||||
val doc = context.httpGet(page.url.toAbsoluteUrl(getDomain())).parseHtml()
|
|
||||||
return doc.body().requireElementById("img").attrAsAbsoluteUrl("src")
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun getTags(): Set<MangaTag> {
|
|
||||||
val doc = context.httpGet("https://${getDomain()}").parseHtml()
|
|
||||||
val root = doc.body().requireElementById("searchbox").selectFirstOrThrow("table")
|
|
||||||
return root.select("div.cs").mapNotNullToSet { div ->
|
|
||||||
val id = div.id().substringAfterLast('_').toIntOrNull()
|
|
||||||
?: return@mapNotNullToSet null
|
|
||||||
MangaTag(
|
|
||||||
title = div.text().toTitleCase(),
|
|
||||||
key = id.toString(),
|
|
||||||
source = source,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun getUsername(): String {
|
|
||||||
val doc = context.httpGet("https://forums.$DOMAIN_UNAUTHORIZED/").parseHtml().body()
|
|
||||||
val username = doc.getElementById("userlinks")
|
|
||||||
?.getElementsByAttributeValueContaining("href", "showuser=")
|
|
||||||
?.firstOrNull()
|
|
||||||
?.ownText()
|
|
||||||
?: if (doc.getElementById("userlinksguest") != null) {
|
|
||||||
throw AuthRequiredException(source)
|
|
||||||
} else {
|
|
||||||
doc.parseFailed()
|
|
||||||
}
|
|
||||||
return username
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun isAuthorized(domain: String): Boolean {
|
|
||||||
val cookies = context.cookieJar.getCookies(domain).mapToSet { x -> x.name }
|
|
||||||
return authCookies.all { it in cookies }
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun Element.parseRating(): Float {
|
|
||||||
return runCatching {
|
|
||||||
val style = requireNotNull(attr("style"))
|
|
||||||
val (v1, v2) = ratingPattern.find(style)!!.destructured
|
|
||||||
var p1 = v1.dropLast(2).toInt()
|
|
||||||
val p2 = v2.dropLast(2).toInt()
|
|
||||||
if (p2 != -1) {
|
|
||||||
p1 += 8
|
|
||||||
}
|
|
||||||
(80 - p1) / 80f
|
|
||||||
}.getOrDefault(RATING_UNKNOWN)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun String.cleanupTitle(): String {
|
|
||||||
val result = StringBuilder(length)
|
|
||||||
var skip = false
|
|
||||||
for (c in this) {
|
|
||||||
when {
|
|
||||||
c == '[' -> skip = true
|
|
||||||
c == ']' -> skip = false
|
|
||||||
c.isWhitespace() && result.isEmpty() -> continue
|
|
||||||
!skip -> result.append(c)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
while (result.lastOrNull()?.isWhitespace() == true) {
|
|
||||||
result.deleteCharAt(result.lastIndex)
|
|
||||||
}
|
|
||||||
return result.toString()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun String.cssUrl(): String? {
|
|
||||||
val fromIndex = indexOf("url(")
|
|
||||||
if (fromIndex == -1) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
val toIndex = indexOf(')', startIndex = fromIndex)
|
|
||||||
return if (toIndex == -1) {
|
|
||||||
null
|
|
||||||
} else {
|
|
||||||
substring(fromIndex + 4, toIndex).trim()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun tagIdByClass(classNames: Collection<String>): String? {
|
|
||||||
val className = classNames.find { x -> x.startsWith("ct") } ?: return null
|
|
||||||
val num = className.drop(2).toIntOrNull(16) ?: return null
|
|
||||||
return 2.0.pow(num).toInt().toString()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,269 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.parsers.site
|
|
||||||
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.async
|
|
||||||
import kotlinx.coroutines.awaitAll
|
|
||||||
import kotlinx.coroutines.coroutineScope
|
|
||||||
import org.json.JSONObject
|
|
||||||
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
|
||||||
import org.koitharu.kotatsu.parsers.MangaParser
|
|
||||||
import org.koitharu.kotatsu.parsers.MangaSourceParser
|
|
||||||
import org.koitharu.kotatsu.parsers.config.ConfigKey
|
|
||||||
import org.koitharu.kotatsu.parsers.exception.ParseException
|
|
||||||
import org.koitharu.kotatsu.parsers.model.*
|
|
||||||
import org.koitharu.kotatsu.parsers.util.*
|
|
||||||
import org.koitharu.kotatsu.parsers.util.json.*
|
|
||||||
import java.text.SimpleDateFormat
|
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
private const val PAGE_SIZE = 20
|
|
||||||
private const val CHAPTERS_FIRST_PAGE_SIZE = 120
|
|
||||||
private const val CHAPTERS_MAX_PAGE_SIZE = 500
|
|
||||||
private const val CHAPTERS_PARALLELISM = 3
|
|
||||||
private const val CONTENT_RATING =
|
|
||||||
"contentRating[]=safe&contentRating[]=suggestive&contentRating[]=erotica&contentRating[]=pornographic"
|
|
||||||
private const val LOCALE_FALLBACK = "en"
|
|
||||||
|
|
||||||
@MangaSourceParser("MANGADEX", "MangaDex")
|
|
||||||
internal class MangaDexParser(override val context: MangaLoaderContext) : MangaParser(MangaSource.MANGADEX) {
|
|
||||||
|
|
||||||
override val configKeyDomain = ConfigKey.Domain("mangadex.org", null)
|
|
||||||
|
|
||||||
override val sortOrders: EnumSet<SortOrder> = EnumSet.of(
|
|
||||||
SortOrder.UPDATED,
|
|
||||||
SortOrder.ALPHABETICAL,
|
|
||||||
SortOrder.NEWEST,
|
|
||||||
SortOrder.POPULARITY,
|
|
||||||
)
|
|
||||||
|
|
||||||
override suspend fun getList(
|
|
||||||
offset: Int,
|
|
||||||
query: String?,
|
|
||||||
tags: Set<MangaTag>?,
|
|
||||||
sortOrder: SortOrder,
|
|
||||||
): List<Manga> {
|
|
||||||
val domain = getDomain()
|
|
||||||
val url = buildString {
|
|
||||||
append("https://api.")
|
|
||||||
append(domain)
|
|
||||||
append("/manga?limit=")
|
|
||||||
append(PAGE_SIZE)
|
|
||||||
append("&offset=")
|
|
||||||
append(offset)
|
|
||||||
append("&includes[]=cover_art&includes[]=author&includes[]=artist&")
|
|
||||||
tags?.forEach { tag ->
|
|
||||||
append("includedTags[]=")
|
|
||||||
append(tag.key)
|
|
||||||
append('&')
|
|
||||||
}
|
|
||||||
if (!query.isNullOrEmpty()) {
|
|
||||||
append("title=")
|
|
||||||
append(query.urlEncoded())
|
|
||||||
append('&')
|
|
||||||
}
|
|
||||||
append(CONTENT_RATING)
|
|
||||||
append("&order")
|
|
||||||
append(
|
|
||||||
when (sortOrder) {
|
|
||||||
SortOrder.UPDATED,
|
|
||||||
-> "[latestUploadedChapter]=desc"
|
|
||||||
|
|
||||||
SortOrder.ALPHABETICAL -> "[title]=asc"
|
|
||||||
SortOrder.NEWEST -> "[createdAt]=desc"
|
|
||||||
SortOrder.POPULARITY -> "[followedCount]=desc"
|
|
||||||
else -> "[followedCount]=desc"
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
val json = context.httpGet(url).parseJson().getJSONArray("data")
|
|
||||||
return json.mapJSON { jo ->
|
|
||||||
val id = jo.getString("id")
|
|
||||||
val attrs = jo.getJSONObject("attributes")
|
|
||||||
val relations = jo.getJSONArray("relationships").associateByKey("type")
|
|
||||||
val cover = relations["cover_art"]
|
|
||||||
?.getJSONObject("attributes")
|
|
||||||
?.getString("fileName")
|
|
||||||
?.let {
|
|
||||||
"https://uploads.$domain/covers/$id/$it"
|
|
||||||
}
|
|
||||||
Manga(
|
|
||||||
id = generateUid(id),
|
|
||||||
title = requireNotNull(attrs.getJSONObject("title").selectByLocale()) {
|
|
||||||
"Title should not be null"
|
|
||||||
},
|
|
||||||
altTitle = attrs.optJSONObject("altTitles")?.selectByLocale(),
|
|
||||||
url = id,
|
|
||||||
publicUrl = "https://$domain/title/$id",
|
|
||||||
rating = RATING_UNKNOWN,
|
|
||||||
isNsfw = attrs.getStringOrNull("contentRating") == "erotica",
|
|
||||||
coverUrl = cover?.plus(".256.jpg").orEmpty(),
|
|
||||||
largeCoverUrl = cover,
|
|
||||||
description = attrs.optJSONObject("description")?.selectByLocale(),
|
|
||||||
tags = attrs.getJSONArray("tags").mapJSONToSet { tag ->
|
|
||||||
MangaTag(
|
|
||||||
title = tag.getJSONObject("attributes")
|
|
||||||
.getJSONObject("name")
|
|
||||||
.firstStringValue()
|
|
||||||
.toTitleCase(),
|
|
||||||
key = tag.getString("id"),
|
|
||||||
source = source,
|
|
||||||
)
|
|
||||||
},
|
|
||||||
state = when (jo.getStringOrNull("status")) {
|
|
||||||
"ongoing" -> MangaState.ONGOING
|
|
||||||
"completed" -> MangaState.FINISHED
|
|
||||||
else -> null
|
|
||||||
},
|
|
||||||
author = (relations["author"] ?: relations["artist"])
|
|
||||||
?.getJSONObject("attributes")
|
|
||||||
?.getStringOrNull("name"),
|
|
||||||
source = source,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun getDetails(manga: Manga): Manga = coroutineScope {
|
|
||||||
val domain = getDomain()
|
|
||||||
val mangaId = manga.url.removePrefix("/")
|
|
||||||
val attrsDeferred = async {
|
|
||||||
context.httpGet(
|
|
||||||
"https://api.$domain/manga/${mangaId}?includes[]=artist&includes[]=author&includes[]=cover_art",
|
|
||||||
).parseJson().getJSONObject("data").getJSONObject("attributes")
|
|
||||||
}
|
|
||||||
val feedDeferred = async { loadChapters(mangaId) }
|
|
||||||
val mangaAttrs = attrsDeferred.await()
|
|
||||||
val feed = feedDeferred.await()
|
|
||||||
// 2022-01-02T00:27:11+00:00
|
|
||||||
val dateFormat = SimpleDateFormat(
|
|
||||||
"yyyy-MM-dd'T'HH:mm:ss'+00:00'",
|
|
||||||
Locale.ROOT,
|
|
||||||
)
|
|
||||||
manga.copy(
|
|
||||||
description = mangaAttrs.getJSONObject("description").selectByLocale()
|
|
||||||
?: manga.description,
|
|
||||||
chapters = feed.mapChapters { _, jo ->
|
|
||||||
val id = jo.getString("id")
|
|
||||||
val attrs = jo.getJSONObject("attributes")
|
|
||||||
if (!attrs.isNull("externalUrl")) {
|
|
||||||
return@mapChapters null
|
|
||||||
}
|
|
||||||
val locale = attrs.getStringOrNull("translatedLanguage")?.let { Locale.forLanguageTag(it) }
|
|
||||||
val relations = jo.getJSONArray("relationships").associateByKey("type")
|
|
||||||
val number = attrs.getIntOrDefault("chapter", 0)
|
|
||||||
MangaChapter(
|
|
||||||
id = generateUid(id),
|
|
||||||
name = attrs.getStringOrNull("title")?.takeUnless(String::isEmpty)
|
|
||||||
?: "Chapter #$number",
|
|
||||||
number = number,
|
|
||||||
url = id,
|
|
||||||
scanlator = relations["scanlation_group"]?.getStringOrNull("name"),
|
|
||||||
uploadDate = dateFormat.tryParse(attrs.getString("publishAt")),
|
|
||||||
branch = locale?.getDisplayName(locale)?.toTitleCase(locale),
|
|
||||||
source = source,
|
|
||||||
)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun getPages(chapter: MangaChapter): List<MangaPage> {
|
|
||||||
val domain = getDomain()
|
|
||||||
val chapterJson = context.httpGet("https://api.$domain/at-home/server/${chapter.url}?forcePort443=false")
|
|
||||||
.parseJson()
|
|
||||||
.getJSONObject("chapter")
|
|
||||||
val pages = chapterJson.getJSONArray("data")
|
|
||||||
val prefix = "https://uploads.$domain/data/${chapterJson.getString("hash")}/"
|
|
||||||
val referer = "https://$domain/"
|
|
||||||
return List(pages.length()) { i ->
|
|
||||||
val url = prefix + pages.getString(i)
|
|
||||||
MangaPage(
|
|
||||||
id = generateUid(url),
|
|
||||||
url = url,
|
|
||||||
referer = referer,
|
|
||||||
preview = null, // TODO prefix + dataSaver.getString(i),
|
|
||||||
source = source,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun getTags(): Set<MangaTag> {
|
|
||||||
val tags = context.httpGet("https://api.${getDomain()}/manga/tag").parseJson()
|
|
||||||
.getJSONArray("data")
|
|
||||||
return tags.mapJSONToSet { jo ->
|
|
||||||
MangaTag(
|
|
||||||
title = jo.getJSONObject("attributes").getJSONObject("name").firstStringValue().toTitleCase(),
|
|
||||||
key = jo.getString("id"),
|
|
||||||
source = source,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun JSONObject.firstStringValue() = values().next() as String
|
|
||||||
|
|
||||||
private fun JSONObject.selectByLocale(): String? {
|
|
||||||
val preferredLocales = context.getPreferredLocales()
|
|
||||||
for (locale in preferredLocales) {
|
|
||||||
getStringOrNull(locale.language)?.let { return it }
|
|
||||||
getStringOrNull(locale.toLanguageTag())?.let { return it }
|
|
||||||
}
|
|
||||||
return getStringOrNull(LOCALE_FALLBACK) ?: values().nextOrNull() as? String
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun loadChapters(mangaId: String): List<JSONObject> {
|
|
||||||
val firstPage = loadChapters(mangaId, offset = 0, limit = CHAPTERS_FIRST_PAGE_SIZE)
|
|
||||||
if (firstPage.size >= firstPage.total) {
|
|
||||||
return firstPage.data
|
|
||||||
}
|
|
||||||
val tail = coroutineScope {
|
|
||||||
val leftCount = firstPage.total - firstPage.size
|
|
||||||
val pages = (leftCount / CHAPTERS_MAX_PAGE_SIZE.toFloat()).toIntUp()
|
|
||||||
val dispatcher = Dispatchers.Default.limitedParallelism(CHAPTERS_PARALLELISM)
|
|
||||||
List(pages) { page ->
|
|
||||||
val offset = page * CHAPTERS_MAX_PAGE_SIZE + firstPage.size
|
|
||||||
async(dispatcher) {
|
|
||||||
loadChapters(mangaId, offset, CHAPTERS_MAX_PAGE_SIZE)
|
|
||||||
}
|
|
||||||
}.awaitAll()
|
|
||||||
}
|
|
||||||
val result = ArrayList<JSONObject>(firstPage.total)
|
|
||||||
result += firstPage.data
|
|
||||||
tail.flatMapTo(result) { it.data }
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun loadChapters(mangaId: String, offset: Int, limit: Int): Chapters {
|
|
||||||
val url = buildString {
|
|
||||||
append("https://api.")
|
|
||||||
append(getDomain())
|
|
||||||
append("/manga/")
|
|
||||||
append(mangaId)
|
|
||||||
append("/feed")
|
|
||||||
append("?limit=")
|
|
||||||
append(limit)
|
|
||||||
append("&includes[]=scanlation_group&order[volume]=asc&order[chapter]=asc&offset=")
|
|
||||||
append(offset)
|
|
||||||
append('&')
|
|
||||||
append(CONTENT_RATING)
|
|
||||||
}
|
|
||||||
val json = context.httpGet(url).parseJson()
|
|
||||||
if (json.getString("result") == "ok") {
|
|
||||||
return Chapters(
|
|
||||||
data = json.optJSONArray("data")?.toJSONList().orEmpty(),
|
|
||||||
total = json.getInt("total"),
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
val error = json.optJSONArray("errors").mapJSON { jo ->
|
|
||||||
jo.getString("detail")
|
|
||||||
}.joinToString("\n")
|
|
||||||
throw ParseException(error, url)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private class Chapters(
|
|
||||||
val data: List<JSONObject>,
|
|
||||||
val total: Int,
|
|
||||||
) {
|
|
||||||
|
|
||||||
val size: Int
|
|
||||||
get() = data.size
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,152 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.parsers.site
|
|
||||||
|
|
||||||
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
|
||||||
import org.koitharu.kotatsu.parsers.MangaSourceParser
|
|
||||||
import org.koitharu.kotatsu.parsers.PagedMangaParser
|
|
||||||
import org.koitharu.kotatsu.parsers.config.ConfigKey
|
|
||||||
import org.koitharu.kotatsu.parsers.model.*
|
|
||||||
import org.koitharu.kotatsu.parsers.util.*
|
|
||||||
import java.text.SimpleDateFormat
|
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
private const val DEF_BRANCH_NAME = "Основний переклад"
|
|
||||||
|
|
||||||
@MangaSourceParser("MANGAINUA", "MANGA/in/UA", "uk")
|
|
||||||
class MangaInUaParser(override val context: MangaLoaderContext) : PagedMangaParser(
|
|
||||||
source = MangaSource.MANGAINUA,
|
|
||||||
pageSize = 24,
|
|
||||||
searchPageSize = 10,
|
|
||||||
) {
|
|
||||||
|
|
||||||
override val sortOrders: Set<SortOrder>
|
|
||||||
get() = Collections.singleton(SortOrder.UPDATED)
|
|
||||||
|
|
||||||
override val configKeyDomain: ConfigKey.Domain = ConfigKey.Domain("manga.in.ua", null)
|
|
||||||
|
|
||||||
override suspend fun getListPage(
|
|
||||||
page: Int,
|
|
||||||
query: String?,
|
|
||||||
tags: Set<MangaTag>?,
|
|
||||||
sortOrder: SortOrder,
|
|
||||||
): List<Manga> {
|
|
||||||
val url = when {
|
|
||||||
!query.isNullOrEmpty() -> (
|
|
||||||
"/index.php?do=search" +
|
|
||||||
"&subaction=search" +
|
|
||||||
"&search_start=$page" +
|
|
||||||
"&full_search=1" +
|
|
||||||
"&story=$query" +
|
|
||||||
"&titleonly=3"
|
|
||||||
).toAbsoluteUrl(getDomain())
|
|
||||||
|
|
||||||
tags.isNullOrEmpty() -> "/mangas/page/$page".toAbsoluteUrl(getDomain())
|
|
||||||
tags.size == 1 -> "${tags.first().key}/page/$page"
|
|
||||||
tags.size > 1 -> throw IllegalArgumentException("This source supports only 1 genre")
|
|
||||||
else -> "/mangas/page/$page".toAbsoluteUrl(getDomain())
|
|
||||||
}
|
|
||||||
val doc = context.httpGet(url).parseHtml()
|
|
||||||
val container = doc.body().requireElementById("dle-content")
|
|
||||||
val items = container.select("div.col-6")
|
|
||||||
return items.mapNotNull { item ->
|
|
||||||
val href = item.selectFirst("a")?.attrAsRelativeUrl("href") ?: return@mapNotNull null
|
|
||||||
Manga(
|
|
||||||
id = generateUid(href),
|
|
||||||
title = item.selectFirst("h3.card__title")?.text() ?: return@mapNotNull null,
|
|
||||||
coverUrl = item.selectFirst("header.card__cover")?.selectFirst("img")?.run {
|
|
||||||
attrAsAbsoluteUrlOrNull("data-src") ?: attrAsAbsoluteUrlOrNull("src")
|
|
||||||
}.orEmpty(),
|
|
||||||
altTitle = null,
|
|
||||||
author = null,
|
|
||||||
rating = item.selectFirst("div.card__short-rate--num")
|
|
||||||
?.text()
|
|
||||||
?.toFloatOrNull()
|
|
||||||
?.div(10F) ?: RATING_UNKNOWN,
|
|
||||||
url = href,
|
|
||||||
isNsfw = item.selectFirst("ul.card__list")?.select("li")?.lastOrNull()?.text() == "18+",
|
|
||||||
tags = runCatching {
|
|
||||||
item.selectFirst("div.card__category")?.select("a")?.mapToSet {
|
|
||||||
MangaTag(
|
|
||||||
title = it.ownText(),
|
|
||||||
key = it.attr("href").removeSuffix("/"),
|
|
||||||
source = source,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}.getOrNull().orEmpty(),
|
|
||||||
state = null,
|
|
||||||
publicUrl = href.toAbsoluteUrl(container.host ?: getDomain()),
|
|
||||||
source = source,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun getDetails(manga: Manga): Manga {
|
|
||||||
val doc = context.httpGet(manga.url.toAbsoluteUrl(getDomain())).parseHtml()
|
|
||||||
val root = doc.body().requireElementById("dle-content")
|
|
||||||
val dateFormat = SimpleDateFormat("dd.MM.yyyy", Locale.US)
|
|
||||||
val chapterNodes = root.selectFirstOrThrow(".linkstocomics").select(".ltcitems")
|
|
||||||
var prevChapterName: String? = null
|
|
||||||
var i = 0
|
|
||||||
return manga.copy(
|
|
||||||
description = root.selectFirst("div.item__full-description")?.text(),
|
|
||||||
largeCoverUrl = root.selectFirst("div.item__full-sidebar--poster")?.selectFirst("img")
|
|
||||||
?.attrAsAbsoluteUrlOrNull("src"),
|
|
||||||
chapters = chapterNodes.mapChapters { _, item ->
|
|
||||||
val href = item?.selectFirst("a")?.attrAsRelativeUrlOrNull("href")
|
|
||||||
?: return@mapChapters null
|
|
||||||
val isAlternative = item.styleValueOrNull("background") != null
|
|
||||||
val name = item.selectFirst("a")?.text().orEmpty()
|
|
||||||
if (!isAlternative) i++
|
|
||||||
MangaChapter(
|
|
||||||
id = generateUid(href),
|
|
||||||
name = if (isAlternative) {
|
|
||||||
prevChapterName ?: return@mapChapters null
|
|
||||||
} else {
|
|
||||||
prevChapterName = name
|
|
||||||
name
|
|
||||||
},
|
|
||||||
number = i,
|
|
||||||
url = href,
|
|
||||||
scanlator = null,
|
|
||||||
branch = if (isAlternative) {
|
|
||||||
name.substringAfterLast(':').trim()
|
|
||||||
} else {
|
|
||||||
DEF_BRANCH_NAME
|
|
||||||
},
|
|
||||||
uploadDate = dateFormat.tryParse(item.selectFirst("div.ltcright")?.text()),
|
|
||||||
source = source,
|
|
||||||
)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun getPages(chapter: MangaChapter): List<MangaPage> {
|
|
||||||
val fullUrl = chapter.url.toAbsoluteUrl(getDomain())
|
|
||||||
val doc = context.httpGet(fullUrl).parseHtml()
|
|
||||||
val root = doc.body().requireElementById("comics").selectFirstOrThrow("ul.xfieldimagegallery")
|
|
||||||
return root.select("li").map { ul ->
|
|
||||||
val img = ul.selectFirstOrThrow("img")
|
|
||||||
val url = img.attrAsAbsoluteUrl("data-src")
|
|
||||||
MangaPage(
|
|
||||||
id = generateUid(url),
|
|
||||||
url = url,
|
|
||||||
preview = null,
|
|
||||||
referer = fullUrl,
|
|
||||||
source = source,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun getTags(): Set<MangaTag> {
|
|
||||||
val domain = getDomain()
|
|
||||||
val doc = context.httpGet("https://$domain/mangas").parseHtml()
|
|
||||||
val root = doc.body().requireElementById("menu_1").selectFirstOrThrow("div.menu__wrapper")
|
|
||||||
return root.select("li").mapNotNullToSet { li ->
|
|
||||||
val a = li.selectFirst("a") ?: return@mapNotNullToSet null
|
|
||||||
MangaTag(
|
|
||||||
title = a.ownText(),
|
|
||||||
key = a.attr("href").removeSuffix("/"),
|
|
||||||
source = source,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,186 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.parsers.site
|
|
||||||
|
|
||||||
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
|
||||||
import org.koitharu.kotatsu.parsers.MangaParser
|
|
||||||
import org.koitharu.kotatsu.parsers.MangaSourceParser
|
|
||||||
import org.koitharu.kotatsu.parsers.config.ConfigKey
|
|
||||||
import org.koitharu.kotatsu.parsers.model.*
|
|
||||||
import org.koitharu.kotatsu.parsers.util.*
|
|
||||||
import java.text.SimpleDateFormat
|
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
@MangaSourceParser("MANGAOWL", "MangaOwl", "en")
|
|
||||||
internal class MangaOwlParser(override val context: MangaLoaderContext) : MangaParser(MangaSource.MANGAOWL) {
|
|
||||||
|
|
||||||
override val configKeyDomain = ConfigKey.Domain("mangaowls.com", null)
|
|
||||||
|
|
||||||
override val sortOrders: Set<SortOrder> = EnumSet.of(
|
|
||||||
SortOrder.POPULARITY,
|
|
||||||
SortOrder.NEWEST,
|
|
||||||
SortOrder.UPDATED,
|
|
||||||
)
|
|
||||||
|
|
||||||
private val regexNsfw = Regex("(yaoi)|(yuri)|(smut)|(mature)|(adult)", RegexOption.IGNORE_CASE)
|
|
||||||
|
|
||||||
override suspend fun getList(
|
|
||||||
offset: Int,
|
|
||||||
query: String?,
|
|
||||||
tags: Set<MangaTag>?,
|
|
||||||
sortOrder: SortOrder,
|
|
||||||
): List<Manga> {
|
|
||||||
val page = (offset / 36f).toIntUp().inc()
|
|
||||||
val link = buildString {
|
|
||||||
append("https://")
|
|
||||||
append(getDomain())
|
|
||||||
when {
|
|
||||||
!query.isNullOrEmpty() -> {
|
|
||||||
append("/search/$page?search=")
|
|
||||||
append(query.urlEncoded())
|
|
||||||
}
|
|
||||||
|
|
||||||
!tags.isNullOrEmpty() -> {
|
|
||||||
for (tag in tags) {
|
|
||||||
append(tag.key)
|
|
||||||
}
|
|
||||||
append("/$page?type=${getAlternativeSortKey(sortOrder)}")
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> {
|
|
||||||
append("/${getSortKey(sortOrder)}/$page")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
val doc = context.httpGet(link).parseHtml()
|
|
||||||
val slides = doc.body().selectOrThrow("ul.slides")
|
|
||||||
val items = slides.select("div.col-md-2")
|
|
||||||
return items.mapNotNull { item ->
|
|
||||||
val href = item.selectFirst("h6 a")?.attrAsRelativeUrlOrNull("href") ?: return@mapNotNull null
|
|
||||||
Manga(
|
|
||||||
id = generateUid(href),
|
|
||||||
title = item.selectFirst("h6 a")?.text() ?: return@mapNotNull null,
|
|
||||||
coverUrl = item.select("div.img-responsive").attr("abs:data-background-image"),
|
|
||||||
altTitle = null,
|
|
||||||
author = null,
|
|
||||||
rating = runCatching {
|
|
||||||
item.selectFirst("div.block-stars")
|
|
||||||
?.text()
|
|
||||||
?.toFloatOrNull()
|
|
||||||
?.div(10f)
|
|
||||||
}.getOrNull() ?: RATING_UNKNOWN,
|
|
||||||
url = href,
|
|
||||||
isNsfw = false,
|
|
||||||
tags = emptySet(),
|
|
||||||
state = null,
|
|
||||||
publicUrl = href.toAbsoluteUrl(getDomain()),
|
|
||||||
source = source,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun getDetails(manga: Manga): Manga {
|
|
||||||
val doc = context.httpGet(manga.publicUrl).parseHtml()
|
|
||||||
val info = doc.body().selectFirstOrThrow("div.single_detail")
|
|
||||||
val table = doc.body().selectFirstOrThrow("div.single-grid-right")
|
|
||||||
val dateFormat = SimpleDateFormat("MM/dd/yyyy", Locale.US)
|
|
||||||
val trRegex = "window\\['tr'] = '([^']*)';".toRegex(RegexOption.IGNORE_CASE)
|
|
||||||
val trElement = doc.getElementsByTag("script").find { trRegex.find(it.data()) != null }
|
|
||||||
?: doc.parseFailed("Oops, tr not found")
|
|
||||||
val tr = trRegex.find(trElement.data())!!.groups[1]!!.value
|
|
||||||
val s = context.encodeBase64(getDomain().toByteArray())
|
|
||||||
var isNsfw = manga.isNsfw
|
|
||||||
val parsedTags = info.select("div.col-xs-12.col-md-8.single-right-grid-right > p > a[href*=genres]")
|
|
||||||
.mapNotNullToSet {
|
|
||||||
val a = it.selectFirst("a") ?: return@mapNotNullToSet null
|
|
||||||
val name = a.text()
|
|
||||||
if (!isNsfw && isNsfwGenre(name)) {
|
|
||||||
isNsfw = true
|
|
||||||
}
|
|
||||||
MangaTag(
|
|
||||||
title = name.toTitleCase(),
|
|
||||||
key = a.attr("href"),
|
|
||||||
source = source,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
return manga.copy(
|
|
||||||
description = info.selectFirst(".description")?.html(),
|
|
||||||
largeCoverUrl = info.select("img").first()?.let { img ->
|
|
||||||
if (img.hasAttr("data-src")) img.attr("abs:data-src") else img.attr("abs:src")
|
|
||||||
},
|
|
||||||
isNsfw = isNsfw,
|
|
||||||
author = info.selectFirst("p.fexi_header_para a.author_link")?.text(),
|
|
||||||
state = parseStatus(info.select("p.fexi_header_para:contains(status)").first()?.ownText()),
|
|
||||||
tags = manga.tags + parsedTags,
|
|
||||||
chapters = table.select("div.table.table-chapter-list").select("li.list-group-item.chapter_list")
|
|
||||||
.asReversed().mapChapters { i, li ->
|
|
||||||
val a = li.select("a")
|
|
||||||
val href = a.attr("data-href").ifEmpty {
|
|
||||||
li.parseFailed("Link is missing")
|
|
||||||
}
|
|
||||||
MangaChapter(
|
|
||||||
id = generateUid(href),
|
|
||||||
name = a.select("label").text(),
|
|
||||||
number = i + 1,
|
|
||||||
url = "$href?tr=$tr&s=$s",
|
|
||||||
scanlator = null,
|
|
||||||
branch = null,
|
|
||||||
uploadDate = dateFormat.tryParse(li.selectFirst("small:last-of-type")?.text()),
|
|
||||||
source = MangaSource.MANGAOWL,
|
|
||||||
)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun getPages(chapter: MangaChapter): List<MangaPage> {
|
|
||||||
val fullUrl = chapter.url.toAbsoluteUrl(getDomain())
|
|
||||||
val doc = context.httpGet(fullUrl).parseHtml()
|
|
||||||
val root = doc.body().selectOrThrow("div.item img.owl-lazy")
|
|
||||||
return root.map { div ->
|
|
||||||
val url = div?.attrAsRelativeUrlOrNull("data-src") ?: doc.parseFailed("Page image not found")
|
|
||||||
MangaPage(
|
|
||||||
id = generateUid(url),
|
|
||||||
url = url,
|
|
||||||
preview = null,
|
|
||||||
referer = url,
|
|
||||||
source = MangaSource.MANGAOWL,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun parseStatus(status: String?) = when {
|
|
||||||
status == null -> null
|
|
||||||
status.contains("Ongoing") -> MangaState.ONGOING
|
|
||||||
status.contains("Completed") -> MangaState.FINISHED
|
|
||||||
else -> null
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun getTags(): Set<MangaTag> {
|
|
||||||
val doc = context.httpGet("https://${getDomain()}/").parseHtml()
|
|
||||||
val root = doc.body().select("ul.dropdown-menu.multi-column.columns-3").select("li")
|
|
||||||
return root.mapToSet { p ->
|
|
||||||
val a = p.selectFirstOrThrow("a")
|
|
||||||
MangaTag(
|
|
||||||
title = a.text().toTitleCase(),
|
|
||||||
key = a.attr("href"),
|
|
||||||
source = source,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getSortKey(sortOrder: SortOrder) =
|
|
||||||
when (sortOrder) {
|
|
||||||
SortOrder.POPULARITY -> "popular"
|
|
||||||
SortOrder.NEWEST -> "new_release"
|
|
||||||
SortOrder.UPDATED -> "lastest"
|
|
||||||
else -> "lastest"
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getAlternativeSortKey(sortOrder: SortOrder) =
|
|
||||||
when (sortOrder) {
|
|
||||||
SortOrder.POPULARITY -> "0"
|
|
||||||
SortOrder.NEWEST -> "2"
|
|
||||||
SortOrder.UPDATED -> "3"
|
|
||||||
else -> "3"
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun isNsfwGenre(name: String): Boolean = regexNsfw.containsMatchIn(name)
|
|
||||||
}
|
|
||||||
@ -1,217 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.parsers.site
|
|
||||||
|
|
||||||
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
|
||||||
import org.koitharu.kotatsu.parsers.MangaParser
|
|
||||||
import org.koitharu.kotatsu.parsers.MangaSourceParser
|
|
||||||
import org.koitharu.kotatsu.parsers.config.ConfigKey
|
|
||||||
import org.koitharu.kotatsu.parsers.model.*
|
|
||||||
import org.koitharu.kotatsu.parsers.util.*
|
|
||||||
import java.text.DateFormat
|
|
||||||
import java.text.SimpleDateFormat
|
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
@MangaSourceParser("MANGATOWN", "MangaTown", "en")
|
|
||||||
internal class MangaTownParser(override val context: MangaLoaderContext) : MangaParser(MangaSource.MANGATOWN) {
|
|
||||||
|
|
||||||
override val configKeyDomain = ConfigKey.Domain("www.mangatown.com", null)
|
|
||||||
|
|
||||||
override val sortOrders: Set<SortOrder> = EnumSet.of(
|
|
||||||
SortOrder.ALPHABETICAL,
|
|
||||||
SortOrder.RATING,
|
|
||||||
SortOrder.POPULARITY,
|
|
||||||
SortOrder.UPDATED,
|
|
||||||
)
|
|
||||||
|
|
||||||
private val regexTag = Regex("[^\\-]+-[^\\-]+-[^\\-]+-[^\\-]+-[^\\-]+-[^\\-]+")
|
|
||||||
|
|
||||||
override suspend fun getList(
|
|
||||||
offset: Int,
|
|
||||||
query: String?,
|
|
||||||
tags: Set<MangaTag>?,
|
|
||||||
sortOrder: SortOrder,
|
|
||||||
): List<Manga> {
|
|
||||||
val sortKey = when (sortOrder) {
|
|
||||||
SortOrder.ALPHABETICAL -> "?name.az"
|
|
||||||
SortOrder.RATING -> "?rating.za"
|
|
||||||
SortOrder.UPDATED -> "?last_chapter_time.za"
|
|
||||||
else -> ""
|
|
||||||
}
|
|
||||||
val page = (offset / 30) + 1
|
|
||||||
val url = when {
|
|
||||||
!query.isNullOrEmpty() -> {
|
|
||||||
if (offset != 0) {
|
|
||||||
return emptyList()
|
|
||||||
}
|
|
||||||
"/search?name=${query.urlEncoded()}".toAbsoluteUrl(getDomain())
|
|
||||||
}
|
|
||||||
|
|
||||||
tags.isNullOrEmpty() -> "/directory/$page.htm$sortKey".toAbsoluteUrl(getDomain())
|
|
||||||
tags.size == 1 -> "/directory/${tags.first().key}/$page.htm$sortKey".toAbsoluteUrl(getDomain())
|
|
||||||
else -> tags.joinToString(
|
|
||||||
prefix = "/search?page=$page".toAbsoluteUrl(getDomain()),
|
|
||||||
) { tag ->
|
|
||||||
"&genres[${tag.key}]=1"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
val doc = context.httpGet(url).parseHtml()
|
|
||||||
val root = doc.body().selectFirstOrThrow("ul.manga_pic_list")
|
|
||||||
return root.select("li").mapNotNull { li ->
|
|
||||||
val a = li.selectFirst("a.manga_cover")
|
|
||||||
val href = a?.attrAsRelativeUrlOrNull("href")
|
|
||||||
?: return@mapNotNull null
|
|
||||||
val views = li.select("p.view")
|
|
||||||
val status = views.firstNotNullOfOrNull { it.ownText().takeIf { x -> x.startsWith("Status:") } }
|
|
||||||
?.substringAfter(':')?.trim()?.lowercase(Locale.ROOT)
|
|
||||||
Manga(
|
|
||||||
id = generateUid(href),
|
|
||||||
title = a.attr("title"),
|
|
||||||
coverUrl = a.selectFirst("img")?.absUrl("src").orEmpty(),
|
|
||||||
source = MangaSource.MANGATOWN,
|
|
||||||
altTitle = null,
|
|
||||||
rating = li.selectFirst("p.score")?.selectFirst("b")
|
|
||||||
?.ownText()?.toFloatOrNull()?.div(5f) ?: RATING_UNKNOWN,
|
|
||||||
author = views.firstNotNullOfOrNull { it.text().takeIf { x -> x.startsWith("Author:") } }
|
|
||||||
?.substringAfter(':')
|
|
||||||
?.trim(),
|
|
||||||
state = when (status) {
|
|
||||||
"ongoing" -> MangaState.ONGOING
|
|
||||||
"completed" -> MangaState.FINISHED
|
|
||||||
else -> null
|
|
||||||
},
|
|
||||||
tags = li.selectFirst("p.keyWord")?.select("a")?.mapNotNullToSet tags@{ x ->
|
|
||||||
MangaTag(
|
|
||||||
title = x.attr("title").toTitleCase(),
|
|
||||||
key = x.attr("href").parseTagKey() ?: return@tags null,
|
|
||||||
source = MangaSource.MANGATOWN,
|
|
||||||
)
|
|
||||||
}.orEmpty(),
|
|
||||||
url = href,
|
|
||||||
isNsfw = false,
|
|
||||||
publicUrl = href.toAbsoluteUrl(a.host ?: getDomain()),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun getDetails(manga: Manga): Manga {
|
|
||||||
val doc = context.httpGet(manga.url.toAbsoluteUrl(getDomain())).parseHtml()
|
|
||||||
val root = doc.body().selectFirstOrThrow("section.main")
|
|
||||||
.selectFirstOrThrow("div.article_content")
|
|
||||||
val info = root.selectFirst("div.detail_info")?.selectFirst("ul")
|
|
||||||
val chaptersList = root.selectFirst("div.chapter_content")
|
|
||||||
?.selectFirst("ul.chapter_list")?.select("li")?.asReversed()
|
|
||||||
val dateFormat = SimpleDateFormat("MMM dd,yyyy", Locale.US)
|
|
||||||
return manga.copy(
|
|
||||||
tags = manga.tags + info?.select("li")?.find { x ->
|
|
||||||
x.selectFirst("b")?.ownText() == "Genre(s):"
|
|
||||||
}?.select("a")?.mapNotNull { a ->
|
|
||||||
MangaTag(
|
|
||||||
title = a.attr("title").toTitleCase(),
|
|
||||||
key = a.attr("href").parseTagKey() ?: return@mapNotNull null,
|
|
||||||
source = MangaSource.MANGATOWN,
|
|
||||||
)
|
|
||||||
}.orEmpty(),
|
|
||||||
description = info?.getElementById("show")?.ownText(),
|
|
||||||
chapters = chaptersList?.mapChapters { i, li ->
|
|
||||||
val href = li.selectFirst("a")?.attrAsRelativeUrlOrNull("href")
|
|
||||||
?: return@mapChapters null
|
|
||||||
val name = li.select("span")
|
|
||||||
.filter { x -> x.className().isEmpty() }
|
|
||||||
.joinToString(" - ") { it.text() }.trim()
|
|
||||||
MangaChapter(
|
|
||||||
id = generateUid(href),
|
|
||||||
url = href,
|
|
||||||
source = MangaSource.MANGATOWN,
|
|
||||||
number = i + 1,
|
|
||||||
uploadDate = parseChapterDate(
|
|
||||||
dateFormat,
|
|
||||||
li.selectFirst("span.time")?.text(),
|
|
||||||
),
|
|
||||||
name = name.ifEmpty { "${manga.title} - ${i + 1}" },
|
|
||||||
scanlator = null,
|
|
||||||
branch = null,
|
|
||||||
)
|
|
||||||
} ?: bypassLicensedChapters(manga),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun getPages(chapter: MangaChapter): List<MangaPage> {
|
|
||||||
val fullUrl = chapter.url.toAbsoluteUrl(getDomain())
|
|
||||||
val doc = context.httpGet(fullUrl).parseHtml()
|
|
||||||
val root = doc.body().selectFirstOrThrow("div.page_select")
|
|
||||||
return root.selectFirstOrThrow("select").selectOrThrow("option").mapNotNull {
|
|
||||||
val href = it.attrAsRelativeUrlOrNull("value")
|
|
||||||
if (href == null || href.endsWith("featured.html")) {
|
|
||||||
return@mapNotNull null
|
|
||||||
}
|
|
||||||
MangaPage(
|
|
||||||
id = generateUid(href),
|
|
||||||
url = href,
|
|
||||||
preview = null,
|
|
||||||
referer = fullUrl,
|
|
||||||
source = MangaSource.MANGATOWN,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun getPageUrl(page: MangaPage): String {
|
|
||||||
val doc = context.httpGet(page.url.toAbsoluteUrl(getDomain())).parseHtml()
|
|
||||||
return doc.requireElementById("image").attrAsAbsoluteUrl("src")
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun getTags(): Set<MangaTag> {
|
|
||||||
val doc = context.httpGet("/directory/".toAbsoluteUrl(getDomain())).parseHtml()
|
|
||||||
val root = doc.body().selectFirst("aside.right")
|
|
||||||
?.getElementsContainingOwnText("Genres")
|
|
||||||
?.first()
|
|
||||||
?.nextElementSibling() ?: doc.parseFailed("Root not found")
|
|
||||||
return root.select("li").mapNotNullToSet { li ->
|
|
||||||
val a = li.selectFirst("a") ?: return@mapNotNullToSet null
|
|
||||||
val key = a.attr("href").parseTagKey()
|
|
||||||
if (key.isNullOrEmpty()) {
|
|
||||||
return@mapNotNullToSet null
|
|
||||||
}
|
|
||||||
MangaTag(
|
|
||||||
source = MangaSource.MANGATOWN,
|
|
||||||
key = key,
|
|
||||||
title = a.text().toTitleCase(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun parseChapterDate(dateFormat: DateFormat, date: String?): Long {
|
|
||||||
return when {
|
|
||||||
date.isNullOrEmpty() -> 0L
|
|
||||||
date.contains("Today") -> Calendar.getInstance().timeInMillis
|
|
||||||
date.contains("Yesterday") -> Calendar.getInstance().apply { add(Calendar.DAY_OF_MONTH, -1) }.timeInMillis
|
|
||||||
else -> dateFormat.tryParse(date)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun bypassLicensedChapters(manga: Manga): List<MangaChapter> {
|
|
||||||
val doc = context.httpGet(manga.url.toAbsoluteUrl(getDomain("m"))).parseHtml()
|
|
||||||
val list = doc.body().selectFirst("ul.detail-ch-list") ?: return emptyList()
|
|
||||||
val dateFormat = SimpleDateFormat("MMM dd,yyyy", Locale.US)
|
|
||||||
return list.select("li").asReversed().mapIndexedNotNull { i, li ->
|
|
||||||
val a = li.selectFirst("a") ?: return@mapIndexedNotNull null
|
|
||||||
val href = a.attrAsRelativeUrl("href")
|
|
||||||
val name = a.selectFirst("span.vol")?.text().orEmpty().ifEmpty {
|
|
||||||
a.ownText()
|
|
||||||
}
|
|
||||||
MangaChapter(
|
|
||||||
id = generateUid(href),
|
|
||||||
url = href,
|
|
||||||
source = MangaSource.MANGATOWN,
|
|
||||||
number = i + 1,
|
|
||||||
uploadDate = parseChapterDate(
|
|
||||||
dateFormat,
|
|
||||||
li.selectFirst("span.time")?.text(),
|
|
||||||
),
|
|
||||||
name = name.ifEmpty { "${manga.title} - ${i + 1}" },
|
|
||||||
scanlator = null,
|
|
||||||
branch = null,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun String.parseTagKey() = split('/').findLast { regexTag matches it }
|
|
||||||
}
|
|
||||||
@ -1,188 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.parsers.site
|
|
||||||
|
|
||||||
import androidx.collection.ArraySet
|
|
||||||
import kotlinx.coroutines.async
|
|
||||||
import kotlinx.coroutines.awaitAll
|
|
||||||
import kotlinx.coroutines.coroutineScope
|
|
||||||
import org.jsoup.nodes.Element
|
|
||||||
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
|
||||||
import org.koitharu.kotatsu.parsers.MangaSourceParser
|
|
||||||
import org.koitharu.kotatsu.parsers.PagedMangaParser
|
|
||||||
import org.koitharu.kotatsu.parsers.config.ConfigKey
|
|
||||||
import org.koitharu.kotatsu.parsers.model.*
|
|
||||||
import org.koitharu.kotatsu.parsers.util.*
|
|
||||||
import java.text.SimpleDateFormat
|
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
@MangaSourceParser("NHENTAI", "N-Hentai")
|
|
||||||
class NHentaiParser(override val context: MangaLoaderContext) : PagedMangaParser(MangaSource.NHENTAI, pageSize = 25) {
|
|
||||||
|
|
||||||
override val configKeyDomain: ConfigKey.Domain
|
|
||||||
get() = ConfigKey.Domain("nhentai.net", null)
|
|
||||||
|
|
||||||
override val sortOrders: Set<SortOrder>
|
|
||||||
get() = EnumSet.of(SortOrder.NEWEST, SortOrder.POPULARITY)
|
|
||||||
|
|
||||||
override suspend fun getListPage(
|
|
||||||
page: Int,
|
|
||||||
query: String?,
|
|
||||||
tags: Set<MangaTag>?,
|
|
||||||
sortOrder: SortOrder,
|
|
||||||
): List<Manga> {
|
|
||||||
if (query.isNullOrEmpty() && tags != null && tags.size > 1) {
|
|
||||||
return getListPage(page, buildQuery(tags), emptySet(), sortOrder)
|
|
||||||
}
|
|
||||||
val domain = getDomain()
|
|
||||||
val url = buildString {
|
|
||||||
append("https://")
|
|
||||||
append(domain)
|
|
||||||
if (!query.isNullOrEmpty()) {
|
|
||||||
append("/search/?q=")
|
|
||||||
append(query.urlEncoded())
|
|
||||||
append("&page=")
|
|
||||||
append(page)
|
|
||||||
if (sortOrder == SortOrder.POPULARITY) {
|
|
||||||
append("&sort=popular")
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
append('/')
|
|
||||||
if (!tags.isNullOrEmpty()) {
|
|
||||||
val tag = tags.single()
|
|
||||||
append("tag/")
|
|
||||||
append(tag.key)
|
|
||||||
append('/')
|
|
||||||
if (sortOrder == SortOrder.POPULARITY) {
|
|
||||||
append("popular")
|
|
||||||
}
|
|
||||||
append("?page=")
|
|
||||||
append(page)
|
|
||||||
} else {
|
|
||||||
if (sortOrder == SortOrder.POPULARITY) {
|
|
||||||
append("?sort=popular&page=")
|
|
||||||
} else {
|
|
||||||
append("?page=")
|
|
||||||
}
|
|
||||||
append(page)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
val root = context.httpGet(url).parseHtml().body().requireElementById("content")
|
|
||||||
.selectLastOrThrow("div.index-container")
|
|
||||||
val regexBrackets = Regex("\\[[^]]+]|\\([^)]+\\)")
|
|
||||||
val regexSpaces = Regex("\\s+")
|
|
||||||
return root.select(".gallery").map { div ->
|
|
||||||
val a = div.selectFirstOrThrow("a.cover")
|
|
||||||
val href = a.attrAsRelativeUrl("href")
|
|
||||||
val img = div.selectFirstOrThrow("img")
|
|
||||||
val title = div.selectFirstOrThrow(".caption").text()
|
|
||||||
Manga(
|
|
||||||
id = generateUid(href),
|
|
||||||
title = title.replace(regexBrackets, "")
|
|
||||||
.replace(regexSpaces, " ")
|
|
||||||
.trim(),
|
|
||||||
altTitle = null,
|
|
||||||
url = href,
|
|
||||||
publicUrl = href.toAbsoluteUrl(domain),
|
|
||||||
rating = RATING_UNKNOWN,
|
|
||||||
isNsfw = true,
|
|
||||||
coverUrl = img.attrAsAbsoluteUrlOrNull("data-src")
|
|
||||||
?: img.attrAsAbsoluteUrl("src"),
|
|
||||||
tags = setOf(),
|
|
||||||
state = null,
|
|
||||||
author = null,
|
|
||||||
largeCoverUrl = null,
|
|
||||||
description = null,
|
|
||||||
chapters = listOf(),
|
|
||||||
source = source,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun getDetails(manga: Manga): Manga {
|
|
||||||
val root = context.httpGet(
|
|
||||||
url = manga.url.toAbsoluteUrl(getDomain()),
|
|
||||||
).parseHtml().body().requireElementById("bigcontainer")
|
|
||||||
val img = root.requireElementById("cover").selectFirstOrThrow("img")
|
|
||||||
val tagContainers = root.requireElementById("tags").select(".tag-container")
|
|
||||||
val dateFormat = SimpleDateFormat(
|
|
||||||
"yyyy-MM-dd'T'HH:mm:ss.SSSSSS'+00:00'",
|
|
||||||
Locale.ROOT,
|
|
||||||
)
|
|
||||||
return manga.copy(
|
|
||||||
tags = tagContainers.find { x -> x.ownText() == "Tags:" }?.parseTags() ?: manga.tags,
|
|
||||||
author = tagContainers.find { x -> x.ownText() == "Artists:" }
|
|
||||||
?.selectFirst("span.name")?.text()?.toCamelCase(),
|
|
||||||
largeCoverUrl = img.attrAsAbsoluteUrlOrNull("data-src")
|
|
||||||
?: img.attrAsAbsoluteUrl("src"),
|
|
||||||
description = null,
|
|
||||||
chapters = listOf(
|
|
||||||
MangaChapter(
|
|
||||||
id = manga.id,
|
|
||||||
name = manga.title,
|
|
||||||
number = 1,
|
|
||||||
url = manga.url,
|
|
||||||
scanlator = null,
|
|
||||||
uploadDate = dateFormat.tryParse(
|
|
||||||
tagContainers.find { x -> x.ownText() == "Uploaded:" }
|
|
||||||
?.selectFirst("time")
|
|
||||||
?.attr("datetime"),
|
|
||||||
),
|
|
||||||
branch = null,
|
|
||||||
source = source,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun getPages(chapter: MangaChapter): List<MangaPage> {
|
|
||||||
val url = chapter.url.toAbsoluteUrl(getDomain())
|
|
||||||
val root = context.httpGet(url).parseHtml().requireElementById("thumbnail-container")
|
|
||||||
return root.select(".thumb-container").map { div ->
|
|
||||||
val a = div.selectFirstOrThrow("a")
|
|
||||||
val img = div.selectFirstOrThrow("img")
|
|
||||||
val href = a.attrAsRelativeUrl("href")
|
|
||||||
MangaPage(
|
|
||||||
id = generateUid(href),
|
|
||||||
url = href,
|
|
||||||
referer = url,
|
|
||||||
preview = img.attrAsAbsoluteUrlOrNull("data-src")
|
|
||||||
?: img.attrAsAbsoluteUrl("src"),
|
|
||||||
source = source,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun getPageUrl(page: MangaPage): String {
|
|
||||||
val root = context.httpGet(page.url.toAbsoluteUrl(getDomain())).parseHtml().body()
|
|
||||||
.requireElementById("image-container")
|
|
||||||
return root.selectFirstOrThrow("img").attrAsAbsoluteUrl("src")
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun getTags(): Set<MangaTag> {
|
|
||||||
return coroutineScope {
|
|
||||||
// parse first 3 pages of tags
|
|
||||||
(1..3).map { page ->
|
|
||||||
async { getTags(page) }
|
|
||||||
}
|
|
||||||
}.awaitAll().flattenTo(ArraySet(360))
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun getTags(page: Int): Set<MangaTag> {
|
|
||||||
val root = context.httpGet("https://${getDomain()}/tags/popular?page=$page").parseHtml().body()
|
|
||||||
.getElementById("tag-container")
|
|
||||||
return root?.parseTags().orEmpty()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun Element.parseTags() = select("a.tag").mapToSet { a ->
|
|
||||||
val href = a.attr("href").removeSuffix('/')
|
|
||||||
MangaTag(
|
|
||||||
title = a.selectFirstOrThrow(".name").text().toTitleCase(),
|
|
||||||
key = href.substringAfterLast('/'),
|
|
||||||
source = source,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun buildQuery(tags: Collection<MangaTag>) = tags.joinToString(separator = " ") { tag ->
|
|
||||||
"tag:\"${tag.key}\""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,221 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.parsers.site
|
|
||||||
|
|
||||||
import androidx.collection.ArrayMap
|
|
||||||
import androidx.collection.ArraySet
|
|
||||||
import kotlinx.coroutines.sync.Mutex
|
|
||||||
import kotlinx.coroutines.sync.withLock
|
|
||||||
import org.koitharu.kotatsu.parsers.*
|
|
||||||
import org.koitharu.kotatsu.parsers.config.ConfigKey
|
|
||||||
import org.koitharu.kotatsu.parsers.exception.NotFoundException
|
|
||||||
import org.koitharu.kotatsu.parsers.model.*
|
|
||||||
import org.koitharu.kotatsu.parsers.util.*
|
|
||||||
import java.text.SimpleDateFormat
|
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
@MangaSourceParser("NETTRUYEN", "NetTruyen", "vi")
|
|
||||||
class NetTruyenParser(override val context: MangaLoaderContext) :
|
|
||||||
PagedMangaParser(MangaSource.NETTRUYEN, pageSize = 36) {
|
|
||||||
|
|
||||||
override val configKeyDomain: ConfigKey.Domain
|
|
||||||
get() = ConfigKey.Domain("www.nettruyenme.com", null)
|
|
||||||
|
|
||||||
override val sortOrders: Set<SortOrder>
|
|
||||||
get() = EnumSet.of(SortOrder.UPDATED, SortOrder.POPULARITY, SortOrder.NEWEST, SortOrder.RATING)
|
|
||||||
|
|
||||||
private val mutex = Mutex()
|
|
||||||
private val dateFormat = SimpleDateFormat("dd/MM/yy", Locale.US)
|
|
||||||
private var tagCache: ArrayMap<String, MangaTag>? = null
|
|
||||||
|
|
||||||
override suspend fun getDetails(manga: Manga): Manga {
|
|
||||||
val doc = context.httpGet(manga.url.toAbsoluteUrl(getDomain())).parseHtml()
|
|
||||||
val rating = doc.selectFirst("span[itemprop=ratingValue]")
|
|
||||||
?.ownText()
|
|
||||||
?.toFloatOrNull() ?: 0f
|
|
||||||
|
|
||||||
val chapterElements = doc.getElementById("nt_listchapter")?.select("ul > li") ?: doc.parseFailed()
|
|
||||||
val chapters = chapterElements.asReversed().mapChapters { index, element ->
|
|
||||||
val a = element.selectFirst("div.chapter > a") ?: return@mapChapters null
|
|
||||||
val relativeUrl = a.attrAsRelativeUrlOrNull("href") ?: return@mapChapters null
|
|
||||||
val timeText = element.selectFirst("div.col-xs-4.text-center.no-wrap.small")?.text()
|
|
||||||
|
|
||||||
MangaChapter(
|
|
||||||
id = generateUid(relativeUrl),
|
|
||||||
name = a.text(),
|
|
||||||
number = index + 1,
|
|
||||||
url = relativeUrl,
|
|
||||||
scanlator = null,
|
|
||||||
uploadDate = parseChapterTime(timeText),
|
|
||||||
branch = null,
|
|
||||||
source = source,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return manga.copy(
|
|
||||||
rating = rating / 5,
|
|
||||||
chapters = chapters,
|
|
||||||
description = doc.selectFirst("div.detail-content > p")?.html(),
|
|
||||||
isNsfw = doc.selectFirst("div.alert.alert-danger > strong:contains(Cảnh báo độ tuổi)") != null,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 20 giây trước
|
|
||||||
// 52 phút trước
|
|
||||||
// 6 giờ trước
|
|
||||||
// 2 ngày trước
|
|
||||||
// 19:09 30/07
|
|
||||||
// 23/12/21
|
|
||||||
private fun parseChapterTime(timeText: String?): Long {
|
|
||||||
if (timeText.isNullOrEmpty()) {
|
|
||||||
return 0L
|
|
||||||
}
|
|
||||||
|
|
||||||
val timeWords = arrayOf("giây", "phút", "giờ", "ngày")
|
|
||||||
val calendar = Calendar.getInstance()
|
|
||||||
val timeArr = timeText.split(' ')
|
|
||||||
if (WordSet(*timeWords).anyWordIn(timeText)) {
|
|
||||||
val timeSuffix = timeArr.getOrNull(1)
|
|
||||||
val timeDiff = timeArr.getOrNull(0)?.toIntOrNull() ?: return 0L
|
|
||||||
when (timeSuffix) {
|
|
||||||
timeWords[0] -> calendar.add(Calendar.SECOND, -timeDiff)
|
|
||||||
timeWords[1] -> calendar.add(Calendar.MINUTE, -timeDiff)
|
|
||||||
timeWords[2] -> calendar.add(Calendar.HOUR, -timeDiff)
|
|
||||||
timeWords[3] -> calendar.add(Calendar.DATE, -timeDiff)
|
|
||||||
else -> return 0L
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
val relativeDate = timeArr.lastOrNull() ?: return 0L
|
|
||||||
val dateString = when (relativeDate.split('/').size) {
|
|
||||||
2 -> {
|
|
||||||
val currentYear = calendar.get(Calendar.YEAR).toString().takeLast(2)
|
|
||||||
"$relativeDate/$currentYear"
|
|
||||||
}
|
|
||||||
3 -> relativeDate
|
|
||||||
else -> return 0L
|
|
||||||
}
|
|
||||||
|
|
||||||
calendar.timeInMillis = dateFormat.tryParse(dateString)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
return calendar.time.time
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun getListPage(
|
|
||||||
page: Int,
|
|
||||||
query: String?,
|
|
||||||
tags: Set<MangaTag>?,
|
|
||||||
sortOrder: SortOrder,
|
|
||||||
): List<Manga> {
|
|
||||||
val isSearching = !query.isNullOrEmpty()
|
|
||||||
val url = buildString {
|
|
||||||
append("https://")
|
|
||||||
append(getDomain())
|
|
||||||
if (isSearching) {
|
|
||||||
append("/tim-truyen?keyword=")
|
|
||||||
append(query!!.urlEncoded())
|
|
||||||
append("&page=")
|
|
||||||
append(page)
|
|
||||||
} else {
|
|
||||||
val tagQuery = tags.orEmpty().joinToString(",") { it.key }
|
|
||||||
append("/tim-truyen-nang-cao?genres=$tagQuery")
|
|
||||||
append("¬genres=&gender=-1&status=-1&minchapter=1&sort=${getSortOrderKey(sortOrder)}")
|
|
||||||
append("&page=$page")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val response = if (isSearching) {
|
|
||||||
val result = runCatching { context.httpGet(url) }
|
|
||||||
val exception = result.exceptionOrNull()
|
|
||||||
if (exception is NotFoundException) {
|
|
||||||
return emptyList()
|
|
||||||
}
|
|
||||||
|
|
||||||
result.getOrThrow()
|
|
||||||
} else {
|
|
||||||
context.httpGet(url)
|
|
||||||
}
|
|
||||||
|
|
||||||
val itemsElements = response.parseHtml()
|
|
||||||
.select("div.ModuleContent > div.items")
|
|
||||||
.select("div.item")
|
|
||||||
return itemsElements.mapNotNull { item ->
|
|
||||||
val tooltipElement = item.selectFirst("div.box_tootip") ?: return@mapNotNull null
|
|
||||||
val absUrl = item.selectFirst("div.image > a")?.attrAsAbsoluteUrlOrNull("href") ?: return@mapNotNull null
|
|
||||||
val slug = absUrl.substringAfterLast('/')
|
|
||||||
val mangaState = when (tooltipElement.selectFirst("div.message_main > p:contains(Tình trạng)")?.ownText()) {
|
|
||||||
"Đang tiến hành" -> MangaState.ONGOING
|
|
||||||
"Hoàn thành" -> MangaState.FINISHED
|
|
||||||
else -> null
|
|
||||||
}
|
|
||||||
|
|
||||||
val tagMap = getOrCreateTagMap()
|
|
||||||
val tagsElement = tooltipElement.selectFirst("div.message_main > p:contains(Thể loại)")?.ownText().orEmpty()
|
|
||||||
val mangaTags = tagsElement.split(',').mapNotNullToSet { tagMap[it.trim()] }
|
|
||||||
Manga(
|
|
||||||
id = generateUid(slug),
|
|
||||||
title = tooltipElement.selectFirst("div.title")?.text().orEmpty(),
|
|
||||||
altTitle = null,
|
|
||||||
url = absUrl.toRelativeUrl(getDomain()),
|
|
||||||
publicUrl = absUrl,
|
|
||||||
rating = RATING_UNKNOWN,
|
|
||||||
isNsfw = false,
|
|
||||||
coverUrl = item.selectFirst("div.image a img")?.absUrl("data-original").orEmpty(),
|
|
||||||
largeCoverUrl = null,
|
|
||||||
tags = mangaTags,
|
|
||||||
state = mangaState,
|
|
||||||
author = tooltipElement.selectFirst("div.message_main > p:contains(Tác giả)")?.ownText(),
|
|
||||||
description = tooltipElement.selectFirst("div.box_text")?.text(),
|
|
||||||
chapters = null,
|
|
||||||
source = source,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
override suspend fun getPages(chapter: MangaChapter): List<MangaPage> {
|
|
||||||
val pageElements = context.httpGet(chapter.url.toAbsoluteUrl(getDomain())).parseHtml()
|
|
||||||
.select("div.reading-detail.box_doc > div img")
|
|
||||||
return pageElements.map { element ->
|
|
||||||
val url = element.attrAsAbsoluteUrl("data-original")
|
|
||||||
MangaPage(
|
|
||||||
id = generateUid(url),
|
|
||||||
url = url,
|
|
||||||
referer = getDomain(),
|
|
||||||
preview = null,
|
|
||||||
source = source,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun getTags(): Set<MangaTag> {
|
|
||||||
val map = getOrCreateTagMap()
|
|
||||||
val tagSet = ArraySet<MangaTag>(map.size)
|
|
||||||
for (entry in map) {
|
|
||||||
tagSet.add(entry.value)
|
|
||||||
}
|
|
||||||
|
|
||||||
return tagSet
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun getOrCreateTagMap(): ArrayMap<String, MangaTag> = mutex.withLock {
|
|
||||||
tagCache?.let { return@withLock it }
|
|
||||||
val doc = context.httpGet("/tim-truyen-nang-cao".toAbsoluteUrl(getDomain())).parseHtml()
|
|
||||||
val tagItems = doc.select("div.genre-item")
|
|
||||||
val result = ArrayMap<String, MangaTag>(tagItems.size)
|
|
||||||
for (item in tagItems) {
|
|
||||||
val title = item.text().trim()
|
|
||||||
val key = item.select("span[data-id]").attr("data-id")
|
|
||||||
result[title] = MangaTag(title = title, key = key, source = source)
|
|
||||||
}
|
|
||||||
tagCache = result
|
|
||||||
result
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getSortOrderKey(sortOrder: SortOrder) = when (sortOrder) {
|
|
||||||
SortOrder.UPDATED -> 0
|
|
||||||
SortOrder.POPULARITY -> 10
|
|
||||||
SortOrder.NEWEST -> 15
|
|
||||||
SortOrder.RATING -> 20
|
|
||||||
else -> throw IllegalArgumentException("Sort order ${sortOrder.name} not supported")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue