Compare commits
736 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
64c927c761 | ||
|
|
16e7fcb71e | ||
|
|
d61d7e471f | ||
|
|
64272b7689 | ||
|
|
35777ecb58 | ||
|
|
423ad7b2c9 | ||
|
|
5cbead06bc | ||
|
|
1704bfb4ec | ||
|
|
b488834c0f | ||
|
|
8d59666a6c | ||
|
|
c14a963024 | ||
|
|
0026de73bf | ||
|
|
d3238d22ba | ||
|
|
ab090a6858 | ||
|
|
64783a0529 | ||
|
|
05d679e6ca | ||
|
|
cd9586ab13 | ||
|
|
0a7cfcd917 | ||
|
|
ff978e2c88 | ||
|
|
d81ca76594 | ||
|
|
9457ee6d26 | ||
|
|
9a94c33c8b | ||
|
|
e88352af3f | ||
|
|
870b775175 | ||
|
|
78f5016dd3 | ||
|
|
eaec9e62a7 | ||
|
|
f92e8dfacb | ||
|
|
a6d740e9e9 | ||
|
|
436a4ca4d1 | ||
|
|
ea7df30aa7 | ||
|
|
240ff3870b | ||
|
|
1340624742 | ||
|
|
b0b7cffddf | ||
|
|
fe3f978082 | ||
|
|
0045abe08c | ||
|
|
ef975868a3 | ||
|
|
345aa0f267 | ||
|
|
1f8ddb27a1 | ||
|
|
575ea260b6 | ||
|
|
9fdc1ac4db | ||
|
|
cf921295f8 | ||
|
|
ed22970346 | ||
|
|
5f034dc348 | ||
|
|
c294cdc280 | ||
|
|
16dbc4f1eb | ||
|
|
fba1b1cf22 | ||
|
|
141fdcf552 | ||
|
|
3aa844f90b | ||
|
|
be95c1f3bc | ||
|
|
6275d5abe3 | ||
|
|
85c5d45829 | ||
|
|
9feb3d2f13 | ||
|
|
8b5cafff3d | ||
|
|
9ea6258fec | ||
|
|
fc0736c686 | ||
|
|
dbc2c7d52b | ||
|
|
38f849f1a8 | ||
|
|
33afc263e7 | ||
|
|
ab73931fb9 | ||
|
|
87c7de7417 | ||
|
|
f1042ded9f | ||
|
|
8dd0725f9a | ||
|
|
5730450e11 | ||
|
|
8da4074b1b | ||
|
|
130fa5ee11 | ||
|
|
b2488edc02 | ||
|
|
773bbf034c | ||
|
|
d507135eaa | ||
|
|
24191c91d0 | ||
|
|
2fe20c35f8 | ||
|
|
afefbbf9a5 | ||
|
|
b2a602e96e | ||
|
|
e47deb7c57 | ||
|
|
5b8c497800 | ||
|
|
fb58d306bc | ||
|
|
282fc1ac1a | ||
|
|
c74e6fcb18 | ||
|
|
39fec7c35c | ||
|
|
591fac8b08 | ||
|
|
f1982d1cd5 | ||
|
|
ba08c7b4e7 | ||
|
|
78630bb5f7 | ||
|
|
e3a64abfd8 | ||
|
|
0403ee0690 | ||
|
|
bc1f69b3b1 | ||
|
|
78a16410cc | ||
|
|
8103bc09d2 | ||
|
|
4d134a613d | ||
|
|
0927e8287c | ||
|
|
52aaef0515 | ||
|
|
e5c62a9ed4 | ||
|
|
e7dfc28513 | ||
|
|
4b8e683bba | ||
|
|
3957977a2c | ||
|
|
56be50c43a | ||
|
|
de0b33c7c6 | ||
|
|
6093c202d2 | ||
|
|
da2a8ffbff | ||
|
|
1fa4d5bbbb | ||
|
|
3486ec67a7 | ||
|
|
0883776580 | ||
|
|
6629a2ac87 | ||
|
|
3d3a94c991 | ||
|
|
6c71f0b808 | ||
|
|
2efc3683b2 | ||
|
|
b453d1927a | ||
|
|
6aa37c3426 | ||
|
|
b0790145cc | ||
|
|
a472ab5dd3 | ||
|
|
533818b812 | ||
|
|
257c3b6266 | ||
|
|
e61c1717ae | ||
|
|
82146f2c51 | ||
|
|
ac06400d2a | ||
|
|
a1feba6e6f | ||
|
|
bc79628f4f | ||
|
|
f39645166e | ||
|
|
d260d8f3e6 | ||
|
|
cd0476b700 | ||
|
|
01abffa85f | ||
|
|
71ae48000b | ||
|
|
b42a543ed5 | ||
|
|
3952ee4376 | ||
|
|
7107cef7a4 | ||
|
|
c06b990bf5 | ||
|
|
a29807895c | ||
|
|
f5f32fa701 | ||
|
|
e9c040ceb7 | ||
|
|
d2117a1390 | ||
|
|
67cdc42d64 | ||
|
|
4b5b3919f5 | ||
|
|
79fc6584d7 | ||
|
|
44381a04be | ||
|
|
9972a48366 | ||
|
|
4c4436814b | ||
|
|
b1bae92308 | ||
|
|
84ebabffc8 | ||
|
|
6b913d6e85 | ||
|
|
5226b963ee | ||
|
|
3567edae4b | ||
|
|
d6db234ad1 | ||
|
|
cee5207ac1 | ||
|
|
00a4f7b9ee | ||
|
|
93ace68398 | ||
|
|
66b219ea0a | ||
|
|
0d065fdd6a | ||
|
|
68e4a344d8 | ||
|
|
a022855400 | ||
|
|
469fd1b4a4 | ||
|
|
413dea6564 | ||
|
|
551b27280e | ||
|
|
6e4e10930b | ||
|
|
16d2277d3e | ||
|
|
0a3b638c55 | ||
|
|
e9e14aef2f | ||
|
|
23f185c877 | ||
|
|
036440b0e5 | ||
|
|
ea51c8a7ea | ||
|
|
645cdc2068 | ||
|
|
dce10bc7fc | ||
|
|
5846a13c10 | ||
|
|
2b65f3067e | ||
|
|
7d5e8fa461 | ||
|
|
42da4ebd30 | ||
|
|
d83b2b55c8 | ||
|
|
1e6406774d | ||
|
|
adc1147ca4 | ||
|
|
866bd26067 | ||
|
|
2885913205 | ||
|
|
01888286da | ||
|
|
3ca4000ed1 | ||
|
|
4c5bdb4226 | ||
|
|
d5272e7108 | ||
|
|
90100a077d | ||
|
|
6bf7e9721f | ||
|
|
80c7f5ba77 | ||
|
|
9aefa9c598 | ||
|
|
652e0feeca | ||
|
|
a852cb37c5 | ||
|
|
0f3cf5fbb8 | ||
|
|
c990e744d3 | ||
|
|
dc87b23c0d | ||
|
|
b54d54d374 | ||
|
|
25cf19f65d | ||
|
|
c82197a585 | ||
|
|
8ccb1301d0 | ||
|
|
a777b26517 | ||
|
|
cd7db09804 | ||
|
|
d298521202 | ||
|
|
4726bd0416 | ||
|
|
f1bb6c8a64 | ||
|
|
3c8f9f2d46 | ||
|
|
175ffbc71f | ||
|
|
b96fca8ed8 | ||
|
|
eb2e9c2425 | ||
|
|
ed02017bca | ||
|
|
69336c5c6e | ||
|
|
74da730dc4 | ||
|
|
250c8925a4 | ||
|
|
978159c80e | ||
|
|
d2a69fdc4d | ||
|
|
aae69fa2a1 | ||
|
|
421cd5fcee | ||
|
|
e98c770dab | ||
|
|
6009bdaaf9 | ||
|
|
0e674fcd67 | ||
|
|
34c8215bd0 | ||
|
|
f7f8806b14 | ||
|
|
3318fbba23 | ||
|
|
48224c7b53 | ||
|
|
edd80a1569 | ||
|
|
8cd863c885 | ||
|
|
cd9a83d3fc | ||
|
|
4eea557c44 | ||
|
|
30771374e5 | ||
|
|
201a772ecf | ||
|
|
818d60e196 | ||
|
|
6a4566be17 | ||
|
|
73ddab8b2e | ||
|
|
886a0bd85b | ||
|
|
15f2651b20 | ||
|
|
19f0a3cc7d | ||
|
|
fbc1e43260 | ||
|
|
8dea7cc400 | ||
|
|
59d086006b | ||
|
|
97b9778801 | ||
|
|
0b22b4ec36 | ||
|
|
1852ce1282 | ||
|
|
97d64f6021 | ||
|
|
a5df6bee2f | ||
|
|
83d410378a | ||
|
|
4693b60af4 | ||
|
|
b830a73e04 | ||
|
|
0c7c047a7b | ||
|
|
510847d3d5 | ||
|
|
789ed2c5b0 | ||
|
|
b497fb40df | ||
|
|
6280d13d36 | ||
|
|
63f0b496f1 | ||
|
|
3e071b1386 | ||
|
|
ef567903ad | ||
|
|
9e9fb6eb76 | ||
|
|
1e09625b85 | ||
|
|
50c6ca66a3 | ||
|
|
caab478956 | ||
|
|
e5bbc09f25 | ||
|
|
9590aa56d1 | ||
|
|
1500daf207 | ||
|
|
f8307620ce | ||
|
|
48c9528116 | ||
|
|
546a5327e8 | ||
|
|
becaed79d1 | ||
|
|
49519f1e17 | ||
|
|
7d62224d11 | ||
|
|
9645f6255b | ||
|
|
5b134978c7 | ||
|
|
a61d7d315d | ||
|
|
12780eec26 | ||
|
|
c8b39301cb | ||
|
|
48d8a276d7 | ||
|
|
34fa052bc2 | ||
|
|
c7addc1c1a | ||
|
|
824154594d | ||
|
|
211a593796 | ||
|
|
f3fd33d241 | ||
|
|
1c39e349ad | ||
|
|
5e95f600d9 | ||
|
|
19521be3ff | ||
|
|
3c41414f4a | ||
|
|
f06dc21d56 | ||
|
|
172e6ac6ac | ||
|
|
d5c72c5c86 | ||
|
|
0a8c29189e | ||
|
|
31096899e1 | ||
|
|
4742ea8d55 | ||
|
|
5143f2db4a | ||
|
|
6575e2343d | ||
|
|
15b616c880 | ||
|
|
fcba060c90 | ||
|
|
275b559f06 | ||
|
|
1f4d73aa0e | ||
|
|
8d312025c1 | ||
|
|
f0cf404c1d | ||
|
|
fcb192f7fe | ||
|
|
5417db41fd | ||
|
|
7cab2cbab3 | ||
|
|
3582f1829f | ||
|
|
d2b4adf09b | ||
|
|
66e1ee5702 | ||
|
|
205232f6d9 | ||
|
|
03e3c96f3c | ||
|
|
103b17f449 | ||
|
|
27e02a8548 | ||
|
|
b146ee71ae | ||
|
|
3502141c62 | ||
|
|
9bc62ba91e | ||
|
|
1e2b396ed5 | ||
|
|
2e77593e14 | ||
|
|
4d9b1c7480 | ||
|
|
1e0e414ae6 | ||
|
|
b491ba1917 | ||
|
|
02620ec5b5 | ||
|
|
7be22ec132 | ||
|
|
ec8d8b3ab8 | ||
|
|
388204cc43 | ||
|
|
169d48aa47 | ||
|
|
1b5dfa0f3e | ||
|
|
615cef700a | ||
|
|
f204b068ea | ||
|
|
d07603de8b | ||
|
|
fe997ecb32 | ||
|
|
1996e44ef5 | ||
|
|
1c6e4640c5 | ||
|
|
0e91c59197 | ||
|
|
acc91a0eed | ||
|
|
a1a9729134 | ||
|
|
70f8e36ffc | ||
|
|
5ab4d3e0c4 | ||
|
|
7998395c56 | ||
|
|
60da30c92b | ||
|
|
fb0559abd3 | ||
|
|
3174a4db5e | ||
|
|
e1c5ad2f2a | ||
|
|
5c751eb551 | ||
|
|
350337fdda | ||
|
|
c3335db366 | ||
|
|
f9adcdea7f | ||
|
|
ca01c04961 | ||
|
|
942f673934 | ||
|
|
ee38658d0d | ||
|
|
7524f4c08f | ||
|
|
3b37703d7a | ||
|
|
64b2bf0b08 | ||
|
|
28518bb461 | ||
|
|
d1d9840629 | ||
|
|
63e1d1dda8 | ||
|
|
788afbc6f5 | ||
|
|
7121d254f6 | ||
|
|
abc5747614 | ||
|
|
a27c0209c1 | ||
|
|
5b25b89622 | ||
|
|
dd20ff0a7d | ||
|
|
ef2de8e372 | ||
|
|
49a1932930 | ||
|
|
834e157d70 | ||
|
|
5f082f10ee | ||
|
|
5ec231b951 | ||
|
|
7e8bb06a8b | ||
|
|
dfdd8c634e | ||
|
|
df7dd06235 | ||
|
|
31867e216f | ||
|
|
7de7f22775 | ||
|
|
b7643c15d2 | ||
|
|
24f330838e | ||
|
|
8b6c488396 | ||
|
|
b8362a618c | ||
|
|
c254b0d82c | ||
|
|
e02fd59f43 | ||
|
|
485433cd74 | ||
|
|
54d3a14bea | ||
|
|
5962c556ec | ||
|
|
02b4385db3 | ||
|
|
4824367f39 | ||
|
|
a87c345fdc | ||
|
|
dbf1640c1b | ||
|
|
e9e368f197 | ||
|
|
b6187c48a8 | ||
|
|
7872bc343d | ||
|
|
cf83eb18ba | ||
|
|
ac77e4b3e0 | ||
|
|
b64a568352 | ||
|
|
2b969971e6 | ||
|
|
8ddb23fc6b | ||
|
|
3f5bc07de1 | ||
|
|
814dcb778d | ||
|
|
ea5ba76eba | ||
|
|
11c32ba4bc | ||
|
|
f08c23a97b | ||
|
|
c3dab33f6b | ||
|
|
f7ddaf1176 | ||
|
|
b4d0c65cd2 | ||
|
|
bd2574e05a | ||
|
|
f73172b056 | ||
|
|
ae2c9eedb3 | ||
|
|
47078b2f93 | ||
|
|
f3144475f4 | ||
|
|
f7f725392e | ||
|
|
88d5c11032 | ||
|
|
3f3d9edaee | ||
|
|
a25434b059 | ||
|
|
2b491372fc | ||
|
|
9d8665dfbe | ||
|
|
1816c2521d | ||
|
|
ea06ad576a | ||
|
|
a356e98aa8 | ||
|
|
95374bd8f1 | ||
|
|
8aa2323697 | ||
|
|
c29fd0e5f5 | ||
|
|
e25f469fb9 | ||
|
|
573fa99626 | ||
|
|
0fbd6ec5f4 | ||
|
|
57a777c4a3 | ||
|
|
2149df6fe5 | ||
|
|
2dc6b3968c | ||
|
|
e32d2fdefe | ||
|
|
3c840e46f0 | ||
|
|
4a135092e9 | ||
|
|
bfe4ae6164 | ||
|
|
32fb7aea68 | ||
|
|
4022909f14 | ||
|
|
b9222325d1 | ||
|
|
517b0cdc1b | ||
|
|
fd10455400 | ||
|
|
3683d93691 | ||
|
|
46c1f9432f | ||
|
|
371b8336f1 | ||
|
|
4eb3a4a916 | ||
|
|
a0d0a7c27c | ||
|
|
94d12f0eb3 | ||
|
|
2b05222cda | ||
|
|
a78461a2ac | ||
|
|
5dfea78822 | ||
|
|
0fcf1e95bf | ||
|
|
3c422cfd3e | ||
|
|
61f17726a9 | ||
|
|
88066690fd | ||
|
|
ad12b775bb | ||
|
|
e12d9a30f6 | ||
|
|
a528378218 | ||
|
|
5c5f6cfb7a | ||
|
|
580dbe2384 | ||
|
|
112cde5544 | ||
|
|
a17677c3bf | ||
|
|
abefc563d6 | ||
|
|
f6506fefcc | ||
|
|
4c870f0640 | ||
|
|
38fe235b2c | ||
|
|
dd483dddb4 | ||
|
|
612bdff774 | ||
|
|
8414edfab9 | ||
|
|
a8bdef3bc7 | ||
|
|
0da2fc232d | ||
|
|
190c59d1a9 | ||
|
|
3913d9dbe3 | ||
|
|
eab22402dc | ||
|
|
338ce2d904 | ||
|
|
adc21af7f1 | ||
|
|
9d069b0586 | ||
|
|
ff034ed02c | ||
|
|
e2cd317a40 | ||
|
|
47455e3807 | ||
|
|
92531715cd | ||
|
|
c6f56d27bb | ||
|
|
be27f3c113 | ||
|
|
de192a21b4 | ||
|
|
921f346bb4 | ||
|
|
9468f928e7 | ||
|
|
f32e7b19a2 | ||
|
|
1e99c80f81 | ||
|
|
f99deeb4ba | ||
|
|
758f1c4e4e | ||
|
|
fafa586905 | ||
|
|
d873b6754e | ||
|
|
62f2e5dea2 | ||
|
|
d18a9c6d53 | ||
|
|
7e4f7e426a | ||
|
|
9bdf49e6cf | ||
|
|
42ac8d85c4 | ||
|
|
8f9a21c773 | ||
|
|
bba20bb255 | ||
|
|
a249342007 | ||
|
|
913012d1e7 | ||
|
|
db38051cf4 | ||
|
|
e0a6546df5 | ||
|
|
2908e4f259 | ||
|
|
e27e5ff1b2 | ||
|
|
87e6990c7b | ||
|
|
d1e5a77e3a | ||
|
|
50d48ca7cd | ||
|
|
d872bc1fdb | ||
|
|
dd9c112911 | ||
|
|
afe964c006 | ||
|
|
31a1c36ad7 | ||
|
|
54e150f713 | ||
|
|
3c2bde7431 | ||
|
|
cd62595518 | ||
|
|
6707b7fdd4 | ||
|
|
02a1874ef5 | ||
|
|
7fe6f61614 | ||
|
|
08cbe31fc7 | ||
|
|
ed82a60a84 | ||
|
|
6ceb80f949 | ||
|
|
be8d6a5b6c | ||
|
|
c7a330a255 | ||
|
|
bb623e52ea | ||
|
|
fcaca8c394 | ||
|
|
1b2972066c | ||
|
|
ba614d220e | ||
|
|
ff39e811b4 | ||
|
|
ef8ce1d609 | ||
|
|
99ff19e180 | ||
|
|
758f7e232d | ||
|
|
cceae5e1fe | ||
|
|
98a074fd0d | ||
|
|
f0102c0279 | ||
|
|
68c19738e6 | ||
|
|
ce41a4de79 | ||
|
|
43928b6246 | ||
|
|
c7f404bc18 | ||
|
|
0a785934b7 | ||
|
|
144d2395de | ||
|
|
cb4d9ed621 | ||
|
|
16d661f6ef | ||
|
|
54e8e26bd6 | ||
|
|
24f24945a5 | ||
|
|
df7cc55b64 | ||
|
|
4d960cf726 | ||
|
|
5cd8986ac2 | ||
|
|
736d4ae0d1 | ||
|
|
9d38278955 | ||
|
|
a0873e4937 | ||
|
|
e97e47d395 | ||
|
|
caeea00737 | ||
|
|
170f526cc2 | ||
|
|
f23159fb71 | ||
|
|
486c7d0666 | ||
|
|
6afb8a45a6 | ||
|
|
28a85aa085 | ||
|
|
736c403575 | ||
|
|
e07a4c4228 | ||
|
|
5fc1c1ad2a | ||
|
|
40b5fde746 | ||
|
|
47fcaeed50 | ||
|
|
f97cc151e0 | ||
|
|
cdc5fae88e | ||
|
|
64a69d953e | ||
|
|
614c382402 | ||
|
|
d4fac8e6ce | ||
|
|
56a66bdfbb | ||
|
|
ab670d08e7 | ||
|
|
95b8c14b89 | ||
|
|
d66d86fb7c | ||
|
|
cf8b02421f | ||
|
|
b34bb417b9 | ||
|
|
8956926ea0 | ||
|
|
3b7cff85cf | ||
|
|
31c2e6eb26 | ||
|
|
6c10aa1719 | ||
|
|
45b2fc2624 | ||
|
|
67ccb5daaa | ||
|
|
cce7956b07 | ||
|
|
f6303c1a6e | ||
|
|
c3835cb79b | ||
|
|
f7d9c9a1af | ||
|
|
e7ec2b9f92 | ||
|
|
b43375a377 | ||
|
|
efea1d9bd2 | ||
|
|
1ec0b19db6 | ||
|
|
263728a7d7 | ||
|
|
115b2628e0 | ||
|
|
7a6b152b7a | ||
|
|
b7bbbe8be8 | ||
|
|
effcec2db8 | ||
|
|
ea4f202ee5 | ||
|
|
7658259836 | ||
|
|
663fe567ef | ||
|
|
9b4b968730 | ||
|
|
8b02f3efc1 | ||
|
|
6413409ef3 | ||
|
|
0acac420d9 | ||
|
|
28d1b835e3 | ||
|
|
c0aa3fa5df | ||
|
|
e7abd13bb1 | ||
|
|
5751676e16 | ||
|
|
4416e34398 | ||
|
|
c3fbc34478 | ||
|
|
1d00b42b0b | ||
|
|
e80dd03951 | ||
|
|
55f34fb388 | ||
|
|
01a490a730 | ||
|
|
9fb131a3f1 | ||
|
|
5b29bd381c | ||
|
|
9dbd30a5f4 | ||
|
|
d8917d918a | ||
|
|
7e67a8f61a | ||
|
|
b8432270b6 | ||
|
|
4984a2bfbf | ||
|
|
936f86b1f0 | ||
|
|
b076d8f615 | ||
|
|
250709dc1d | ||
|
|
602d39e97b | ||
|
|
9fefe5ecff | ||
|
|
8f97971632 | ||
|
|
4f3dda667c | ||
|
|
c87bd71dd9 | ||
|
|
8ae9cdef64 | ||
|
|
c7056418b2 | ||
|
|
5a00353c11 | ||
|
|
48cb7434cc | ||
|
|
11372c041f | ||
|
|
86728d08b9 | ||
|
|
f2dd58910b | ||
|
|
b8972a8526 | ||
|
|
24aae3e8e8 | ||
|
|
8424e12449 | ||
|
|
32a52a1a1b | ||
|
|
23ae20a3da | ||
|
|
eaa24985ad | ||
|
|
4caf8f87d0 | ||
|
|
8902f20b81 | ||
|
|
012db6ca76 | ||
|
|
c2284b85cc | ||
|
|
89bd225391 | ||
|
|
3854cb0d4e | ||
|
|
552e16774b | ||
|
|
fb056aeb16 | ||
|
|
c40eb332ea | ||
|
|
ea3337b94a | ||
|
|
ad1876c800 | ||
|
|
8ad994943d | ||
|
|
13e0cd7b07 | ||
|
|
bcd926048f | ||
|
|
272177e176 | ||
|
|
6f2096eb71 | ||
|
|
78d999f2d4 | ||
|
|
589d382569 | ||
|
|
0218356073 | ||
|
|
7d013462bd | ||
|
|
2f2333051b | ||
|
|
169376e9d7 | ||
|
|
9c5095f7a8 | ||
|
|
84da65120f | ||
|
|
d4328eda0f | ||
|
|
a17d61d464 | ||
|
|
9855faa91a | ||
|
|
5931760449 | ||
|
|
3271548987 | ||
|
|
5386800473 | ||
|
|
ef054dafed | ||
|
|
8021dbea5e | ||
|
|
9a805f995a | ||
|
|
431e840913 | ||
|
|
f1c2f84f66 | ||
|
|
d273d8f495 | ||
|
|
6e04f93110 | ||
|
|
5c28ff9402 | ||
|
|
c39e3e40d8 | ||
|
|
eaf636061b | ||
|
|
1d1ffd324b | ||
|
|
1b697bf13b | ||
|
|
8c6eafe552 | ||
|
|
fe835c2b37 | ||
|
|
3160d1ba55 | ||
|
|
9440587b20 | ||
|
|
b8ff1105d6 | ||
|
|
86fb936bf2 | ||
|
|
a1f89c8722 | ||
|
|
4ac53d55f0 | ||
|
|
439c8d14ce | ||
|
|
dea190ec83 | ||
|
|
6122a3eaa0 | ||
|
|
9ef4d43674 | ||
|
|
acd427a251 | ||
|
|
2e6710dc34 | ||
|
|
115781c92a | ||
|
|
ebf683a9a8 | ||
|
|
54b031e516 | ||
|
|
30b4b571a1 | ||
|
|
aa1ec94f5d | ||
|
|
2d139acffb | ||
|
|
d1e88c120e | ||
|
|
ee6a302b82 | ||
|
|
59b47a6e7c | ||
|
|
0b82788b24 | ||
|
|
19800b59e3 | ||
|
|
9f888a1cdb | ||
|
|
abacbd2596 | ||
|
|
e6555d51d5 | ||
|
|
bc89271a69 | ||
|
|
eac922863b | ||
|
|
a3c324aef5 | ||
|
|
87ca363ff0 | ||
|
|
56b595f05c | ||
|
|
620e2085e2 | ||
|
|
25079a2f79 | ||
|
|
45c1810fe6 | ||
|
|
fed44e1cea | ||
|
|
9d8488ff24 | ||
|
|
599f2572db | ||
|
|
6562dede2a | ||
|
|
b8b355da04 | ||
|
|
a7c7451b0d | ||
|
|
a307b9b1ed | ||
|
|
750bcc0e8a | ||
|
|
59b87d22e9 | ||
|
|
e6d5dab21a | ||
|
|
73f7a2dde0 | ||
|
|
b24a55c8a3 | ||
|
|
7423fac392 | ||
|
|
c5fd5cf313 | ||
|
|
f665a9b76b | ||
|
|
8b4e33f558 | ||
|
|
9eaf940cb6 | ||
|
|
203bd9d8f5 | ||
|
|
89f34f83f5 | ||
|
|
66e97648da | ||
|
|
511bd9a131 | ||
|
|
f85753fd9b | ||
|
|
0a948788a4 | ||
|
|
e8eeee5aa4 | ||
|
|
be25e331f6 | ||
|
|
6fabaf489e | ||
|
|
e9852ade1a | ||
|
|
f076a7af7e | ||
|
|
40d07bb40d | ||
|
|
7dc72a1507 | ||
|
|
66c56f3522 | ||
|
|
d7b43e398a | ||
|
|
fde33eec76 | ||
|
|
14de1b243a | ||
|
|
ea9001d0b3 | ||
|
|
a9e2908ece | ||
|
|
a011ef17be | ||
|
|
64d9f64fb8 | ||
|
|
1a5939791e | ||
|
|
162a89efc9 | ||
|
|
57836d7a91 | ||
|
|
1546300832 | ||
|
|
df764a8620 | ||
|
|
71f7eb2a69 | ||
|
|
75888c23a5 | ||
|
|
0d5d0f0102 | ||
|
|
3c34eb06dc | ||
|
|
3faf9e496f | ||
|
|
52cff06388 | ||
|
|
2e7271afcb |
153 changed files with 36365 additions and 14080 deletions
1
.dockerignore
Normal file
1
.dockerignore
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
**/node_modules
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
lib
|
|
||||||
/videos/*.ts
|
|
||||||
crunchy
|
|
||||||
|
|
@ -1,33 +0,0 @@
|
||||||
{
|
|
||||||
"env": {
|
|
||||||
"es2021": true,
|
|
||||||
"node": true
|
|
||||||
},
|
|
||||||
"extends": [
|
|
||||||
"eslint:recommended",
|
|
||||||
"plugin:@typescript-eslint/recommended"
|
|
||||||
],
|
|
||||||
"parser": "@typescript-eslint/parser",
|
|
||||||
"parserOptions": {
|
|
||||||
"ecmaVersion": 12,
|
|
||||||
"sourceType": "module"
|
|
||||||
},
|
|
||||||
"plugins": [
|
|
||||||
"@typescript-eslint"
|
|
||||||
],
|
|
||||||
"rules": {
|
|
||||||
"@typescript-eslint/no-explicit-any": "off",
|
|
||||||
"indent": [
|
|
||||||
"error",
|
|
||||||
2
|
|
||||||
],
|
|
||||||
"quotes": [
|
|
||||||
"error",
|
|
||||||
"single"
|
|
||||||
],
|
|
||||||
"semi": [
|
|
||||||
"error",
|
|
||||||
"always"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
41
.github/ISSUE_TEMPLATE/bug.yml
vendored
41
.github/ISSUE_TEMPLATE/bug.yml
vendored
|
|
@ -1,9 +1,11 @@
|
||||||
name: Bug
|
name: Bug
|
||||||
description: File a bug report
|
description: File a bug report
|
||||||
assignees:
|
assignees:
|
||||||
- izu-co
|
- AnimeDL
|
||||||
labels: bug
|
- AnidlSupport
|
||||||
title: "BUG"
|
labels:
|
||||||
|
- bug
|
||||||
|
title: "[BUG]: "
|
||||||
body:
|
body:
|
||||||
- type: markdown
|
- type: markdown
|
||||||
attributes:
|
attributes:
|
||||||
|
|
@ -17,16 +19,39 @@ body:
|
||||||
description: "Which version of the program do you use?"
|
description: "Which version of the program do you use?"
|
||||||
placeholder: "1.0.0"
|
placeholder: "1.0.0"
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
|
- type: dropdown
|
||||||
|
id: opsystem
|
||||||
|
attributes:
|
||||||
|
label: Operating System
|
||||||
|
description: "Please tell us what OS you are using."
|
||||||
|
options:
|
||||||
|
- Windows
|
||||||
|
- Linux
|
||||||
|
- MacOS
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: dropdown
|
||||||
|
id: gui
|
||||||
|
attributes:
|
||||||
|
label: Type
|
||||||
|
description: "Please tell us if you are using the gui or the cli version."
|
||||||
|
options:
|
||||||
|
- CLI
|
||||||
|
- GUI
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
- type: dropdown
|
- type: dropdown
|
||||||
id: service
|
id: service
|
||||||
attributes:
|
attributes:
|
||||||
label: Service
|
label: Service
|
||||||
description: "Please tell us what service the bug occured in."
|
description: "Please tell us what service the bug occured in."
|
||||||
options:
|
options:
|
||||||
- Funimation
|
|
||||||
- Crunchyroll
|
- Crunchyroll
|
||||||
- Both
|
- Hidive
|
||||||
|
- AnimationDigitalNetwork
|
||||||
|
- AnimeOnegai
|
||||||
|
- All
|
||||||
- Irrelevant
|
- Irrelevant
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
|
|
@ -57,7 +82,8 @@ body:
|
||||||
id: output
|
id: output
|
||||||
attributes:
|
attributes:
|
||||||
label: Console Output
|
label: Console Output
|
||||||
description: "Please paste the console output from the beginning till termination here."
|
description: "Please paste the console output from the beginning till termination here. If you are using the gui open the log folder under 'Debug > Open Log Folder' in the Menu. Please copy the content of latest.log here."
|
||||||
|
render: Shell
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
- type: textarea
|
- type: textarea
|
||||||
|
|
@ -65,4 +91,3 @@ body:
|
||||||
attributes:
|
attributes:
|
||||||
label: Additional Information
|
label: Additional Information
|
||||||
description: "Do you have any additional information you can provide?"
|
description: "Do you have any additional information you can provide?"
|
||||||
render: Text
|
|
||||||
29
.github/ISSUE_TEMPLATE/feature.yml
vendored
Normal file
29
.github/ISSUE_TEMPLATE/feature.yml
vendored
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
name: Enhancement
|
||||||
|
description: Suggest a enhancement or feature
|
||||||
|
labels:
|
||||||
|
- enhancement
|
||||||
|
title: "[Feedback]: "
|
||||||
|
body:
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
Thank you for giving feedback with this program.
|
||||||
|
This template will guide you through all the information we need.
|
||||||
|
- type: dropdown
|
||||||
|
id: programversion
|
||||||
|
attributes:
|
||||||
|
label: Type
|
||||||
|
description: "Is this suggestion for the CLI, GUI, or Both?"
|
||||||
|
options:
|
||||||
|
- CLI
|
||||||
|
- GUI
|
||||||
|
- Both
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
id: suggestion
|
||||||
|
attributes:
|
||||||
|
label: Suggestion
|
||||||
|
description: "What is your suggestion?"
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
6
.github/dependabot.yml
vendored
Normal file
6
.github/dependabot.yml
vendored
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
version: 2
|
||||||
|
updates:
|
||||||
|
- package-ecosystem: "npm"
|
||||||
|
directory: "/"
|
||||||
|
schedule:
|
||||||
|
interval: "daily"
|
||||||
18
.github/workflows/auto-documentation.yml
vendored
18
.github/workflows/auto-documentation.yml
vendored
|
|
@ -3,8 +3,6 @@ name: auto-documentation
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [ master ]
|
branches: [ master ]
|
||||||
pull_request:
|
|
||||||
branches: [ master ]
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
documentation:
|
documentation:
|
||||||
|
|
@ -14,13 +12,15 @@ jobs:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
with:
|
with:
|
||||||
ref: ${{ github.head_ref }}
|
ref: ${{ github.head_ref }}
|
||||||
- name: Use Node.js 16
|
- uses: pnpm/action-setup@v2
|
||||||
uses: actions/setup-node@v2
|
|
||||||
with:
|
with:
|
||||||
node-version: 16
|
version: 8.6.6
|
||||||
cache: 'npm'
|
- name: Use Node.js
|
||||||
- run: npm i
|
uses: actions/setup-node@v3
|
||||||
- run: npm run docs
|
with:
|
||||||
|
node-version: 20
|
||||||
|
- run: pnpm i
|
||||||
|
- run: pnpm run docs
|
||||||
- uses: stefanzweifel/git-auto-commit-action@v4
|
- uses: stefanzweifel/git-auto-commit-action@v4
|
||||||
with:
|
with:
|
||||||
commit_message: Build Documentaion
|
commit_message: ${{ github.event.head_commit.message }} + Documentation
|
||||||
32
.github/workflows/docker.yml
vendored
Normal file
32
.github/workflows/docker.yml
vendored
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
# This workflow will build a Node project with Docker
|
||||||
|
|
||||||
|
name: build and push docker image
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ master ]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-node:
|
||||||
|
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
id: buildx
|
||||||
|
uses: docker/setup-buildx-action@v1
|
||||||
|
- name: Login to DockerHub
|
||||||
|
if: ${{ github.ref == 'refs/heads/master' }}
|
||||||
|
uses: docker/login-action@v1
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
- name: Build and push Docker images
|
||||||
|
uses: docker/build-push-action@v2.9.0
|
||||||
|
with:
|
||||||
|
github-token: ${{ github.token }}
|
||||||
|
push: ${{ github.ref == 'refs/heads/master' }}
|
||||||
|
tags: |
|
||||||
|
"multidl/multi-downloader-nx:latest"
|
||||||
|
- name: Image digest
|
||||||
|
run: echo ${{ steps.docker_build.outputs.digest }}
|
||||||
54
.github/workflows/release-matrix.yml
vendored
54
.github/workflows/release-matrix.yml
vendored
|
|
@ -2,30 +2,30 @@ name: Release Builds
|
||||||
|
|
||||||
on:
|
on:
|
||||||
release:
|
release:
|
||||||
types: [ created ]
|
types: [ published ]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
runs-on: ubuntu-latest
|
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
build_type: [ linux64, macos64, win64 ]
|
build_type: [ linux, macos, windows ]
|
||||||
|
build_arch: [ x64 ]
|
||||||
|
gui: [ gui, cli ]
|
||||||
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Set build type
|
|
||||||
run: |
|
|
||||||
echo BUILD_TYPE=${{ matrix.build_type }} >> $GITHUB_ENV
|
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
|
- uses: pnpm/action-setup@v2
|
||||||
- name: Set up Node.js
|
|
||||||
uses: actions/setup-node@v2
|
|
||||||
with:
|
with:
|
||||||
node-version: 14
|
version: 8.6.6
|
||||||
|
- name: Set up Node.js
|
||||||
|
uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
node-version: 20
|
||||||
check-latest: true
|
check-latest: true
|
||||||
|
|
||||||
- name: Install Node modules
|
- name: Install Node modules
|
||||||
run: npm install
|
run: |
|
||||||
|
pnpm install
|
||||||
- name: Get name and version from package.json
|
- name: Get name and version from package.json
|
||||||
run: |
|
run: |
|
||||||
test -n $(node -p -e "require('./package.json').name") &&
|
test -n $(node -p -e "require('./package.json').name") &&
|
||||||
|
|
@ -33,14 +33,34 @@ jobs:
|
||||||
echo PACKAGE_NAME=$(node -p -e "require('./package.json').name") >> $GITHUB_ENV &&
|
echo PACKAGE_NAME=$(node -p -e "require('./package.json').name") >> $GITHUB_ENV &&
|
||||||
echo PACKAGE_VERSION=$(node -p -e "require('./package.json').version") >> $GITHUB_ENV || exit 1
|
echo PACKAGE_VERSION=$(node -p -e "require('./package.json').version") >> $GITHUB_ENV || exit 1
|
||||||
- name: Make build
|
- name: Make build
|
||||||
run: npm run build-${{ env.BUILD_TYPE }}
|
run: pnpm run build-${{ matrix.build_type }}-${{ matrix.gui }}
|
||||||
|
|
||||||
- name: Upload release
|
- name: Upload release
|
||||||
uses: actions/upload-release-asset@v1
|
uses: actions/upload-release-asset@v1
|
||||||
with:
|
with:
|
||||||
upload_url: ${{ github.event.release.upload_url }}
|
upload_url: ${{ github.event.release.upload_url }}
|
||||||
asset_name: ${{ env.PACKAGE_NAME }}-${{ env.PACKAGE_VERSION }}-${{ env.BUILD_TYPE }}.7z
|
asset_name: multi-downloader-nx-${{ matrix.build_type }}-${{ matrix.gui }}.7z
|
||||||
asset_path: ./lib/_builds/${{ env.PACKAGE_NAME }}-${{ env.PACKAGE_VERSION }}-${{ env.BUILD_TYPE }}.7z
|
asset_path: ./lib/_builds/multi-downloader-nx-${{ matrix.build_type }}-${{ matrix.build_arch }}-${{ matrix.gui }}.7z
|
||||||
asset_content_type: application/x-7z-compressed
|
asset_content_type: application/x-7z-compressed
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
build-docker:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
id: buildx
|
||||||
|
uses: docker/setup-buildx-action@v1
|
||||||
|
- name: Login to DockerHub
|
||||||
|
uses: docker/login-action@v1
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
- name: Build and push Docker images
|
||||||
|
uses: docker/build-push-action@v2.9.0
|
||||||
|
with:
|
||||||
|
github-token: ${{ github.token }}
|
||||||
|
push: true
|
||||||
|
tags: |
|
||||||
|
"multidl/multi-downloader-nx:${{ github.event.release.tag_name }}"
|
||||||
|
- name: Image digest
|
||||||
|
run: echo ${{ steps.docker_build.outputs.digest }}
|
||||||
|
|
|
||||||
28
.github/workflows/test.yml
vendored
28
.github/workflows/test.yml
vendored
|
|
@ -11,23 +11,29 @@ jobs:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
- name: Use Node.js 14
|
- uses: pnpm/action-setup@v2
|
||||||
uses: actions/setup-node@v2
|
|
||||||
with:
|
with:
|
||||||
node-version: 14
|
version: 8.6.6
|
||||||
cache: 'npm'
|
- name: Set up Node.js
|
||||||
- run: npm i
|
uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
node-version: 20
|
||||||
|
check-latest: true
|
||||||
|
- run: pnpm i
|
||||||
- run: npx eslint .
|
- run: npx eslint .
|
||||||
test:
|
test:
|
||||||
needs: eslint
|
needs: eslint
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
- name: Use Node.js 14
|
- uses: pnpm/action-setup@v2
|
||||||
uses: actions/setup-node@v2
|
|
||||||
with:
|
with:
|
||||||
node-version: 14
|
version: 8.6.6
|
||||||
cache: 'npm'
|
- name: Set up Node.js
|
||||||
- run: npm i
|
uses: actions/setup-node@v3
|
||||||
- run: npm run test
|
with:
|
||||||
|
node-version: 20
|
||||||
|
check-latest: true
|
||||||
|
- run: pnpm i
|
||||||
|
- run: pnpm run test
|
||||||
|
|
||||||
|
|
|
||||||
28
.gitignore
vendored
28
.gitignore
vendored
|
|
@ -1,9 +1,11 @@
|
||||||
/bin/ff*
|
/bin/ff*
|
||||||
/bin/mkv*
|
/bin/mkv*
|
||||||
/_builds/*
|
/_builds/*
|
||||||
/node_modules/
|
**/node_modules/
|
||||||
/videos/*.json
|
/videos/*.json
|
||||||
/videos/*.ts
|
/videos/*.ts
|
||||||
|
/videos/*.m4s
|
||||||
|
/videos/*.txt
|
||||||
.DS_Store
|
.DS_Store
|
||||||
ffmpeg
|
ffmpeg
|
||||||
mkvmerge
|
mkvmerge
|
||||||
|
|
@ -19,7 +21,25 @@ token.yml
|
||||||
lib
|
lib
|
||||||
test.*
|
test.*
|
||||||
updates.json
|
updates.json
|
||||||
funi_token.yml
|
*_token.yml
|
||||||
cr_token.yml
|
*_profile.yml
|
||||||
|
*_sess.yml
|
||||||
archive.json
|
archive.json
|
||||||
fonts
|
guistate.json
|
||||||
|
fonts
|
||||||
|
.webpack/
|
||||||
|
out/
|
||||||
|
dist/
|
||||||
|
gui/react/build/
|
||||||
|
docker-compose.yml
|
||||||
|
crunchyendpoints
|
||||||
|
.vscode
|
||||||
|
.idea
|
||||||
|
/logs
|
||||||
|
/tmp/*/
|
||||||
|
!videos/.gitkeep
|
||||||
|
/videos/*
|
||||||
|
/tmp/*.*
|
||||||
|
bin
|
||||||
|
widevine/*
|
||||||
|
!widevine/.gitkeep
|
||||||
50
@types/adnPlayerConfig.d.ts
vendored
Normal file
50
@types/adnPlayerConfig.d.ts
vendored
Normal file
|
|
@ -0,0 +1,50 @@
|
||||||
|
export interface ADNPlayerConfig {
|
||||||
|
player: Player;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Player {
|
||||||
|
image: string;
|
||||||
|
options: Options;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Options {
|
||||||
|
user: User;
|
||||||
|
chromecast: Chromecast;
|
||||||
|
ios: Ios;
|
||||||
|
video: Video;
|
||||||
|
dock: any[];
|
||||||
|
preference: Preference;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Chromecast {
|
||||||
|
appId: string;
|
||||||
|
refreshTokenUrl: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Ios {
|
||||||
|
videoUrl: string;
|
||||||
|
appUrl: string;
|
||||||
|
title: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Preference {
|
||||||
|
quality: string;
|
||||||
|
autoplay: boolean;
|
||||||
|
language: string;
|
||||||
|
green: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface User {
|
||||||
|
hasAccess: boolean;
|
||||||
|
profileId: number;
|
||||||
|
refreshToken: string;
|
||||||
|
refreshTokenUrl: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Video {
|
||||||
|
startDate: null;
|
||||||
|
currentDate: Date;
|
||||||
|
available: boolean;
|
||||||
|
free: boolean;
|
||||||
|
url: string;
|
||||||
|
}
|
||||||
46
@types/adnSearch.d.ts
vendored
Normal file
46
@types/adnSearch.d.ts
vendored
Normal file
|
|
@ -0,0 +1,46 @@
|
||||||
|
export interface ADNSearch {
|
||||||
|
shows: ADNSearchShow[];
|
||||||
|
total: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ADNSearchShow {
|
||||||
|
id: number;
|
||||||
|
title: string;
|
||||||
|
type: string;
|
||||||
|
originalTitle: string;
|
||||||
|
shortTitle: string;
|
||||||
|
reference: string;
|
||||||
|
age: string;
|
||||||
|
languages: string[];
|
||||||
|
summary: string;
|
||||||
|
image: string;
|
||||||
|
image2x: string;
|
||||||
|
imageHorizontal: string;
|
||||||
|
imageHorizontal2x: string;
|
||||||
|
url: string;
|
||||||
|
urlPath: string;
|
||||||
|
episodeCount: number;
|
||||||
|
genres: string[];
|
||||||
|
copyright: string;
|
||||||
|
rating: number;
|
||||||
|
ratingsCount: number;
|
||||||
|
commentsCount: number;
|
||||||
|
qualities: string[];
|
||||||
|
simulcast: boolean;
|
||||||
|
free: boolean;
|
||||||
|
available: boolean;
|
||||||
|
download: boolean;
|
||||||
|
basedOn: string;
|
||||||
|
tagline: null;
|
||||||
|
firstReleaseYear: string;
|
||||||
|
productionStudio: string;
|
||||||
|
countryOfOrigin: string;
|
||||||
|
productionTeam: ProductionTeam[];
|
||||||
|
nextVideoReleaseDate: null;
|
||||||
|
indexable: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ProductionTeam {
|
||||||
|
role: string;
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
51
@types/adnStreams.d.ts
vendored
Normal file
51
@types/adnStreams.d.ts
vendored
Normal file
|
|
@ -0,0 +1,51 @@
|
||||||
|
export interface ADNStreams {
|
||||||
|
links: Links;
|
||||||
|
video: Video;
|
||||||
|
metadata: Metadata;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Links {
|
||||||
|
streaming: Streaming;
|
||||||
|
subtitles: Subtitles;
|
||||||
|
history: string;
|
||||||
|
nextVideoUrl: string;
|
||||||
|
previousVideoUrl: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Streaming {
|
||||||
|
[streams: string]: Streams;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Streams {
|
||||||
|
mobile: string;
|
||||||
|
sd: string;
|
||||||
|
hd: string;
|
||||||
|
fhd: string;
|
||||||
|
auto: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Subtitles {
|
||||||
|
all: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Metadata {
|
||||||
|
title: string;
|
||||||
|
subtitle: string;
|
||||||
|
summary: null;
|
||||||
|
rating: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Video {
|
||||||
|
guid: string;
|
||||||
|
id: number;
|
||||||
|
currentTime: number;
|
||||||
|
duration: number;
|
||||||
|
url: string;
|
||||||
|
image: string;
|
||||||
|
tcEpisodeStart?:string;
|
||||||
|
tcEpisodeEnd?: string;
|
||||||
|
tcIntroStart?: string;
|
||||||
|
tcIntroEnd?: string;
|
||||||
|
tcEndingStart?: string;
|
||||||
|
tcEndingEnd?: string;
|
||||||
|
}
|
||||||
11
@types/adnSubtitles.d.ts
vendored
Normal file
11
@types/adnSubtitles.d.ts
vendored
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
export interface ADNSubtitles {
|
||||||
|
[subtitleLang: string]: Subtitle[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Subtitle {
|
||||||
|
startTime: number;
|
||||||
|
endTime: number;
|
||||||
|
positionAlign: string;
|
||||||
|
lineAlign: string;
|
||||||
|
text: string;
|
||||||
|
}
|
||||||
77
@types/adnVideos.d.ts
vendored
Normal file
77
@types/adnVideos.d.ts
vendored
Normal file
|
|
@ -0,0 +1,77 @@
|
||||||
|
export interface ADNVideos {
|
||||||
|
videos: ADNVideo[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ADNVideo {
|
||||||
|
id: number;
|
||||||
|
title: string;
|
||||||
|
name: string;
|
||||||
|
number: string;
|
||||||
|
shortNumber: string;
|
||||||
|
season: string;
|
||||||
|
reference: string;
|
||||||
|
type: string;
|
||||||
|
order: number;
|
||||||
|
image: string;
|
||||||
|
image2x: string;
|
||||||
|
summary: string;
|
||||||
|
releaseDate: Date;
|
||||||
|
duration: number;
|
||||||
|
url: string;
|
||||||
|
urlPath: string;
|
||||||
|
embeddedUrl: string;
|
||||||
|
languages: string[];
|
||||||
|
qualities: string[];
|
||||||
|
rating: number;
|
||||||
|
ratingsCount: number;
|
||||||
|
commentsCount: number;
|
||||||
|
available: boolean;
|
||||||
|
download: boolean;
|
||||||
|
free: boolean;
|
||||||
|
freeWithAds: boolean;
|
||||||
|
show: Show;
|
||||||
|
indexable: boolean;
|
||||||
|
isSelected?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Show {
|
||||||
|
id: number;
|
||||||
|
title: string;
|
||||||
|
type: string;
|
||||||
|
originalTitle: string;
|
||||||
|
shortTitle: string;
|
||||||
|
reference: string;
|
||||||
|
age: string;
|
||||||
|
languages: string[];
|
||||||
|
summary: string;
|
||||||
|
image: string;
|
||||||
|
image2x: string;
|
||||||
|
imageHorizontal: string;
|
||||||
|
imageHorizontal2x: string;
|
||||||
|
url: string;
|
||||||
|
urlPath: string;
|
||||||
|
episodeCount: number;
|
||||||
|
genres: string[];
|
||||||
|
copyright: string;
|
||||||
|
rating: number;
|
||||||
|
ratingsCount: number;
|
||||||
|
commentsCount: number;
|
||||||
|
qualities: string[];
|
||||||
|
simulcast: boolean;
|
||||||
|
free: boolean;
|
||||||
|
available: boolean;
|
||||||
|
download: boolean;
|
||||||
|
basedOn: string;
|
||||||
|
tagline: string;
|
||||||
|
firstReleaseYear: string;
|
||||||
|
productionStudio: string;
|
||||||
|
countryOfOrigin: string;
|
||||||
|
productionTeam: ProductionTeam[];
|
||||||
|
nextVideoReleaseDate: Date;
|
||||||
|
indexable: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ProductionTeam {
|
||||||
|
role: string;
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
88
@types/animeOnegaiSearch.d.ts
vendored
Normal file
88
@types/animeOnegaiSearch.d.ts
vendored
Normal file
|
|
@ -0,0 +1,88 @@
|
||||||
|
export interface AnimeOnegaiSearch {
|
||||||
|
text: string;
|
||||||
|
list: AOSearchResult[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AOSearchResult {
|
||||||
|
/**
|
||||||
|
* Asset ID
|
||||||
|
*/
|
||||||
|
ID: number;
|
||||||
|
CreatedAt: Date;
|
||||||
|
UpdatedAt: Date;
|
||||||
|
DeletedAt: null;
|
||||||
|
title: string;
|
||||||
|
active: boolean;
|
||||||
|
excerpt: string;
|
||||||
|
description: string;
|
||||||
|
bg: string;
|
||||||
|
poster: string;
|
||||||
|
entry: string;
|
||||||
|
code_name: string;
|
||||||
|
/**
|
||||||
|
* The Video ID required to get the streams
|
||||||
|
*/
|
||||||
|
video_entry: string;
|
||||||
|
trailer: string;
|
||||||
|
year: number;
|
||||||
|
/**
|
||||||
|
* Asset Type, Known Possibilities
|
||||||
|
* * 1 - Video
|
||||||
|
* * 2 - Series
|
||||||
|
*/
|
||||||
|
asset_type: 1 | 2;
|
||||||
|
status: number;
|
||||||
|
permalink: string;
|
||||||
|
duration: string;
|
||||||
|
subtitles: boolean;
|
||||||
|
price: number;
|
||||||
|
rent_price: number;
|
||||||
|
rating: number;
|
||||||
|
color: number | null;
|
||||||
|
classification: number;
|
||||||
|
brazil_classification: null | string;
|
||||||
|
likes: number;
|
||||||
|
views: number;
|
||||||
|
button: string;
|
||||||
|
stream_url: string;
|
||||||
|
stream_url_backup: string;
|
||||||
|
copyright: null | string;
|
||||||
|
skip_intro: null | string;
|
||||||
|
ending: null | string;
|
||||||
|
bumper_intro: string;
|
||||||
|
ads: string;
|
||||||
|
age_restriction: boolean | null;
|
||||||
|
epg: null;
|
||||||
|
allow_languages: string[] | null;
|
||||||
|
allow_countries: string[] | null;
|
||||||
|
classification_text: string;
|
||||||
|
locked: boolean;
|
||||||
|
resign: boolean;
|
||||||
|
favorite: boolean;
|
||||||
|
actors_list: null;
|
||||||
|
voiceactors_list: null;
|
||||||
|
artdirectors_list: null;
|
||||||
|
audios_list: null;
|
||||||
|
awards_list: null;
|
||||||
|
companies_list: null;
|
||||||
|
countries_list: null;
|
||||||
|
directors_list: null;
|
||||||
|
edition_list: null;
|
||||||
|
genres_list: null;
|
||||||
|
music_list: null;
|
||||||
|
photograpy_list: null;
|
||||||
|
producer_list: null;
|
||||||
|
screenwriter_list: null;
|
||||||
|
season_list: null;
|
||||||
|
tags_list: null;
|
||||||
|
chapter_id: number;
|
||||||
|
chapter_entry: string;
|
||||||
|
chapter_poster: string;
|
||||||
|
progress_time: number;
|
||||||
|
progress_percent: number;
|
||||||
|
included_subscription: number;
|
||||||
|
paid_content: number;
|
||||||
|
rent_content: number;
|
||||||
|
objectID: string;
|
||||||
|
lang: string;
|
||||||
|
}
|
||||||
36
@types/animeOnegaiSeasons.d.ts
vendored
Normal file
36
@types/animeOnegaiSeasons.d.ts
vendored
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
export interface AnimeOnegaiSeasons {
|
||||||
|
ID: number;
|
||||||
|
CreatedAt: Date;
|
||||||
|
UpdatedAt: Date;
|
||||||
|
DeletedAt: null;
|
||||||
|
name: string;
|
||||||
|
number: number;
|
||||||
|
asset_id: number;
|
||||||
|
entry: string;
|
||||||
|
description: string;
|
||||||
|
active: boolean;
|
||||||
|
allow_languages: string[];
|
||||||
|
allow_countries: string[];
|
||||||
|
list: Episode[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Episode {
|
||||||
|
ID: number;
|
||||||
|
CreatedAt: Date;
|
||||||
|
UpdatedAt: Date;
|
||||||
|
DeletedAt: null;
|
||||||
|
name: string;
|
||||||
|
number: number;
|
||||||
|
description: string;
|
||||||
|
thumbnail: string;
|
||||||
|
entry: string;
|
||||||
|
video_entry: string;
|
||||||
|
active: boolean;
|
||||||
|
season_id: number;
|
||||||
|
stream_url: string;
|
||||||
|
skip_intro: null;
|
||||||
|
ending: null;
|
||||||
|
open_free: boolean;
|
||||||
|
asset_id: number;
|
||||||
|
age_restriction: boolean;
|
||||||
|
}
|
||||||
111
@types/animeOnegaiSeries.d.ts
vendored
Normal file
111
@types/animeOnegaiSeries.d.ts
vendored
Normal file
|
|
@ -0,0 +1,111 @@
|
||||||
|
export interface AnimeOnegaiSeries {
|
||||||
|
ID: number;
|
||||||
|
CreatedAt: Date;
|
||||||
|
UpdatedAt: Date;
|
||||||
|
DeletedAt: null;
|
||||||
|
title: string;
|
||||||
|
active: boolean;
|
||||||
|
excerpt: string;
|
||||||
|
description: string;
|
||||||
|
bg: string;
|
||||||
|
poster: string;
|
||||||
|
entry: string;
|
||||||
|
code_name: string;
|
||||||
|
/**
|
||||||
|
* The Video ID required to get the streams
|
||||||
|
*/
|
||||||
|
video_entry: string;
|
||||||
|
trailer: string;
|
||||||
|
year: number;
|
||||||
|
asset_type: number;
|
||||||
|
status: number;
|
||||||
|
permalink: string;
|
||||||
|
duration: string;
|
||||||
|
subtitles: boolean;
|
||||||
|
price: number;
|
||||||
|
rent_price: number;
|
||||||
|
rating: number;
|
||||||
|
color: number;
|
||||||
|
classification: number;
|
||||||
|
brazil_classification: string;
|
||||||
|
likes: number;
|
||||||
|
views: number;
|
||||||
|
button: string;
|
||||||
|
stream_url: string;
|
||||||
|
stream_url_backup: string;
|
||||||
|
copyright: string;
|
||||||
|
skip_intro: null;
|
||||||
|
ending: null;
|
||||||
|
bumper_intro: string;
|
||||||
|
ads: string;
|
||||||
|
age_restriction: boolean;
|
||||||
|
epg: null;
|
||||||
|
allow_languages: string[];
|
||||||
|
allow_countries: string[];
|
||||||
|
classification_text: string;
|
||||||
|
locked: boolean;
|
||||||
|
resign: boolean;
|
||||||
|
favorite: boolean;
|
||||||
|
actors_list: CtorsList[];
|
||||||
|
voiceactors_list: CtorsList[];
|
||||||
|
artdirectors_list: any[];
|
||||||
|
audios_list: SList[];
|
||||||
|
awards_list: any[];
|
||||||
|
companies_list: any[];
|
||||||
|
countries_list: any[];
|
||||||
|
directors_list: CtorsList[];
|
||||||
|
edition_list: any[];
|
||||||
|
genres_list: SList[];
|
||||||
|
music_list: any[];
|
||||||
|
photograpy_list: any[];
|
||||||
|
producer_list: any[];
|
||||||
|
screenwriter_list: any[];
|
||||||
|
season_list: any[];
|
||||||
|
tags_list: TagsList[];
|
||||||
|
chapter_id: number;
|
||||||
|
chapter_entry: string;
|
||||||
|
chapter_poster: string;
|
||||||
|
progress_time: number;
|
||||||
|
progress_percent: number;
|
||||||
|
included_subscription: number;
|
||||||
|
paid_content: number;
|
||||||
|
rent_content: number;
|
||||||
|
objectID: string;
|
||||||
|
lang: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CtorsList {
|
||||||
|
ID: number;
|
||||||
|
CreatedAt: Date;
|
||||||
|
UpdatedAt: Date;
|
||||||
|
DeletedAt: null;
|
||||||
|
name: string;
|
||||||
|
Permalink?: string;
|
||||||
|
country: number | null;
|
||||||
|
year: number | null;
|
||||||
|
death: number | null;
|
||||||
|
image: string;
|
||||||
|
genre: null;
|
||||||
|
description: string;
|
||||||
|
permalink?: string;
|
||||||
|
background?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SList {
|
||||||
|
ID: number;
|
||||||
|
CreatedAt: Date;
|
||||||
|
UpdatedAt: Date;
|
||||||
|
DeletedAt: null;
|
||||||
|
name: string;
|
||||||
|
age_restriction?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TagsList {
|
||||||
|
ID: number;
|
||||||
|
CreatedAt: Date;
|
||||||
|
UpdatedAt: Date;
|
||||||
|
DeletedAt: null;
|
||||||
|
name: string;
|
||||||
|
position: number;
|
||||||
|
status: boolean;
|
||||||
|
}
|
||||||
41
@types/animeOnegaiStream.d.ts
vendored
Normal file
41
@types/animeOnegaiStream.d.ts
vendored
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
export interface AnimeOnegaiStream {
|
||||||
|
ID: number;
|
||||||
|
CreatedAt: Date;
|
||||||
|
UpdatedAt: Date;
|
||||||
|
DeletedAt: null;
|
||||||
|
name: string;
|
||||||
|
source_url: string;
|
||||||
|
backup_url: string;
|
||||||
|
live: boolean;
|
||||||
|
token_handler: number;
|
||||||
|
entry: string;
|
||||||
|
job: string;
|
||||||
|
drm: boolean;
|
||||||
|
transcoding_content_id: string;
|
||||||
|
transcoding_asset_id: string;
|
||||||
|
status: number;
|
||||||
|
thumbnail: string;
|
||||||
|
hls: string;
|
||||||
|
dash: string;
|
||||||
|
widevine_proxy: string;
|
||||||
|
playready_proxy: string;
|
||||||
|
apple_licence: string;
|
||||||
|
apple_certificate: string;
|
||||||
|
dpath: string;
|
||||||
|
dbin: string;
|
||||||
|
subtitles: Subtitle[];
|
||||||
|
origin: number;
|
||||||
|
offline_entry: string;
|
||||||
|
offline_status: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Subtitle {
|
||||||
|
ID: number;
|
||||||
|
CreatedAt: Date;
|
||||||
|
UpdatedAt: Date;
|
||||||
|
DeletedAt: null;
|
||||||
|
name: string;
|
||||||
|
lang: string;
|
||||||
|
entry_id: string;
|
||||||
|
url: string;
|
||||||
|
}
|
||||||
139
@types/crunchyAndroidEpisodes.d.ts
vendored
Normal file
139
@types/crunchyAndroidEpisodes.d.ts
vendored
Normal file
|
|
@ -0,0 +1,139 @@
|
||||||
|
import { Images } from './crunchyEpisodeList';
|
||||||
|
|
||||||
|
export interface CrunchyAndroidEpisodes {
|
||||||
|
__class__: string;
|
||||||
|
__href__: string;
|
||||||
|
__resource_key__: string;
|
||||||
|
__links__: Actions;
|
||||||
|
__actions__: Actions;
|
||||||
|
total: number;
|
||||||
|
items: CrunchyAndroidEpisode[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Actions {
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CrunchyAndroidEpisode {
|
||||||
|
__class__: string;
|
||||||
|
__href__: string;
|
||||||
|
__resource_key__: string;
|
||||||
|
__links__: Links;
|
||||||
|
__actions__: Actions;
|
||||||
|
playback: string;
|
||||||
|
id: string;
|
||||||
|
channel_id: ChannelID;
|
||||||
|
series_id: string;
|
||||||
|
series_title: string;
|
||||||
|
series_slug_title: string;
|
||||||
|
season_id: string;
|
||||||
|
season_title: string;
|
||||||
|
season_slug_title: string;
|
||||||
|
season_number: number;
|
||||||
|
episode: string;
|
||||||
|
episode_number: number;
|
||||||
|
sequence_number: number;
|
||||||
|
production_episode_id: string;
|
||||||
|
title: string;
|
||||||
|
slug_title: string;
|
||||||
|
description: string;
|
||||||
|
next_episode_id: string;
|
||||||
|
next_episode_title: string;
|
||||||
|
hd_flag: boolean;
|
||||||
|
maturity_ratings: MaturityRating[];
|
||||||
|
extended_maturity_rating: Actions;
|
||||||
|
is_mature: boolean;
|
||||||
|
mature_blocked: boolean;
|
||||||
|
episode_air_date: Date;
|
||||||
|
upload_date: Date;
|
||||||
|
availability_starts: Date;
|
||||||
|
availability_ends: Date;
|
||||||
|
eligible_region: string;
|
||||||
|
available_date: Date;
|
||||||
|
free_available_date: Date;
|
||||||
|
premium_date: Date;
|
||||||
|
premium_available_date: Date;
|
||||||
|
is_subbed: boolean;
|
||||||
|
is_dubbed: boolean;
|
||||||
|
is_clip: boolean;
|
||||||
|
seo_title: string;
|
||||||
|
seo_description: string;
|
||||||
|
season_tags: string[];
|
||||||
|
available_offline: boolean;
|
||||||
|
subtitle_locales: Locale[];
|
||||||
|
availability_notes: string;
|
||||||
|
audio_locale: Locale;
|
||||||
|
versions: Version[];
|
||||||
|
closed_captions_available: boolean;
|
||||||
|
identifier: string;
|
||||||
|
media_type: MediaType;
|
||||||
|
slug: string;
|
||||||
|
images: Images;
|
||||||
|
duration_ms: number;
|
||||||
|
is_premium_only: boolean;
|
||||||
|
listing_id: string;
|
||||||
|
hide_season_title?: boolean;
|
||||||
|
hide_season_number?: boolean;
|
||||||
|
isSelected?: boolean;
|
||||||
|
seq_id: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Links {
|
||||||
|
'episode/channel': Link;
|
||||||
|
'episode/next_episode': Link;
|
||||||
|
'episode/season': Link;
|
||||||
|
'episode/series': Link;
|
||||||
|
streams: Link;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Link {
|
||||||
|
href: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Thumbnail {
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
type: string;
|
||||||
|
source: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum Locale {
|
||||||
|
enUS = 'en-US',
|
||||||
|
esLA = 'es-LA',
|
||||||
|
es419 = 'es-419',
|
||||||
|
esES = 'es-ES',
|
||||||
|
ptBR = 'pt-BR',
|
||||||
|
frFR = 'fr-FR',
|
||||||
|
deDE = 'de-DE',
|
||||||
|
arME = 'ar-ME',
|
||||||
|
arSA = 'ar-SA',
|
||||||
|
itIT = 'it-IT',
|
||||||
|
ruRU = 'ru-RU',
|
||||||
|
trTR = 'tr-TR',
|
||||||
|
hiIN = 'hi-IN',
|
||||||
|
zhCN = 'zh-CN',
|
||||||
|
koKR = 'ko-KR',
|
||||||
|
jaJP = 'ja-JP',
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum MediaType {
|
||||||
|
Episode = 'episode',
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum ChannelID {
|
||||||
|
Crunchyroll = 'crunchyroll',
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum MaturityRating {
|
||||||
|
Tv14 = 'TV-14',
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Version {
|
||||||
|
audio_locale: Locale;
|
||||||
|
guid: string;
|
||||||
|
original: boolean;
|
||||||
|
variant: string;
|
||||||
|
season_guid: string;
|
||||||
|
media_guid: string;
|
||||||
|
is_premium_only: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
189
@types/crunchyAndroidObject.d.ts
vendored
Normal file
189
@types/crunchyAndroidObject.d.ts
vendored
Normal file
|
|
@ -0,0 +1,189 @@
|
||||||
|
import { ImageType, Images, Image } from './objectInfo';
|
||||||
|
|
||||||
|
export interface CrunchyAndroidObject {
|
||||||
|
__class__: string;
|
||||||
|
__href__: string;
|
||||||
|
__resource_key__: string;
|
||||||
|
__links__: Actions;
|
||||||
|
__actions__: Actions;
|
||||||
|
total: number;
|
||||||
|
items: AndroidObject[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Actions {
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AndroidObject {
|
||||||
|
__class__: string;
|
||||||
|
__href__: string;
|
||||||
|
__links__: Links;
|
||||||
|
__actions__: Actions;
|
||||||
|
id: string;
|
||||||
|
external_id: string;
|
||||||
|
channel_id: string;
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
promo_title: string;
|
||||||
|
promo_description: string;
|
||||||
|
type: string;
|
||||||
|
slug: string;
|
||||||
|
slug_title: string;
|
||||||
|
images: Images;
|
||||||
|
movie_listing_metadata?: MovieListingMetadata;
|
||||||
|
movie_metadata?: MovieMetadata;
|
||||||
|
playback?: string;
|
||||||
|
episode_metadata?: EpisodeMetadata;
|
||||||
|
streams_link?: string;
|
||||||
|
season_metadata?: SeasonMetadata;
|
||||||
|
linked_resource_key: string;
|
||||||
|
isSelected?: boolean;
|
||||||
|
f_num: string;
|
||||||
|
s_num: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Links {
|
||||||
|
'episode/season': LinkData;
|
||||||
|
'episode/series': LinkData;
|
||||||
|
resource: LinkData;
|
||||||
|
'resource/channel': LinkData;
|
||||||
|
streams: LinkData;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LinkData {
|
||||||
|
href: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EpisodeMetadata {
|
||||||
|
audio_locale: Locale;
|
||||||
|
availability_ends: Date;
|
||||||
|
availability_notes: string;
|
||||||
|
availability_starts: Date;
|
||||||
|
available_date: null;
|
||||||
|
available_offline: boolean;
|
||||||
|
closed_captions_available: boolean;
|
||||||
|
duration_ms: number;
|
||||||
|
eligible_region: string;
|
||||||
|
episode: string;
|
||||||
|
episode_air_date: Date;
|
||||||
|
episode_number: number;
|
||||||
|
extended_maturity_rating: Record<unknown>;
|
||||||
|
free_available_date: Date;
|
||||||
|
identifier: string;
|
||||||
|
is_clip: boolean;
|
||||||
|
is_dubbed: boolean;
|
||||||
|
is_mature: boolean;
|
||||||
|
is_premium_only: boolean;
|
||||||
|
is_subbed: boolean;
|
||||||
|
mature_blocked: boolean;
|
||||||
|
maturity_ratings: string[];
|
||||||
|
premium_available_date: Date;
|
||||||
|
premium_date: null;
|
||||||
|
season_id: string;
|
||||||
|
season_number: number;
|
||||||
|
season_slug_title: string;
|
||||||
|
season_title: string;
|
||||||
|
sequence_number: number;
|
||||||
|
series_id: string;
|
||||||
|
series_slug_title: string;
|
||||||
|
series_title: string;
|
||||||
|
subtitle_locales: Locale[];
|
||||||
|
tenant_categories?: string[];
|
||||||
|
upload_date: Date;
|
||||||
|
versions: EpisodeMetadataVersion[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MovieListingMetadata {
|
||||||
|
availability_notes: string;
|
||||||
|
available_date: null;
|
||||||
|
available_offline: boolean;
|
||||||
|
duration_ms: number;
|
||||||
|
extended_description: string;
|
||||||
|
extended_maturity_rating: Record<unknown>;
|
||||||
|
first_movie_id: string;
|
||||||
|
free_available_date: Date;
|
||||||
|
is_dubbed: boolean;
|
||||||
|
is_mature: boolean;
|
||||||
|
is_premium_only: boolean;
|
||||||
|
is_subbed: boolean;
|
||||||
|
mature_blocked: boolean;
|
||||||
|
maturity_ratings: string[];
|
||||||
|
movie_release_year: number;
|
||||||
|
premium_available_date: Date;
|
||||||
|
premium_date: null;
|
||||||
|
subtitle_locales: Locale[];
|
||||||
|
tenant_categories: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MovieMetadata {
|
||||||
|
availability_notes: string;
|
||||||
|
available_offline: boolean;
|
||||||
|
closed_captions_available: boolean;
|
||||||
|
duration_ms: number;
|
||||||
|
extended_maturity_rating: Record<unknown>;
|
||||||
|
is_dubbed: boolean;
|
||||||
|
is_mature: boolean;
|
||||||
|
is_premium_only: boolean;
|
||||||
|
is_subbed: boolean;
|
||||||
|
mature_blocked: boolean;
|
||||||
|
maturity_ratings: string[];
|
||||||
|
movie_listing_id: string;
|
||||||
|
movie_listing_slug_title: string;
|
||||||
|
movie_listing_title: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SeasonMetadata {
|
||||||
|
audio_locale: Locale;
|
||||||
|
audio_locales: Locale[];
|
||||||
|
extended_maturity_rating: Record<unknown>;
|
||||||
|
identifier: string;
|
||||||
|
is_mature: boolean;
|
||||||
|
mature_blocked: boolean;
|
||||||
|
maturity_ratings: string[];
|
||||||
|
season_display_number: string;
|
||||||
|
season_sequence_number: number;
|
||||||
|
subtitle_locales: Locale[];
|
||||||
|
versions: SeasonMetadataVersion[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SeasonMetadataVersion {
|
||||||
|
audio_locale: Locale;
|
||||||
|
guid: string;
|
||||||
|
original: boolean;
|
||||||
|
variant: string;
|
||||||
|
}
|
||||||
|
export interface SeriesMetadata {
|
||||||
|
audio_locales: Locale[];
|
||||||
|
availability_notes: string;
|
||||||
|
episode_count: number;
|
||||||
|
extended_description: string;
|
||||||
|
extended_maturity_rating: Record<unknown>;
|
||||||
|
is_dubbed: boolean;
|
||||||
|
is_mature: boolean;
|
||||||
|
is_simulcast: boolean;
|
||||||
|
is_subbed: boolean;
|
||||||
|
mature_blocked: boolean;
|
||||||
|
maturity_ratings: string[];
|
||||||
|
season_count: number;
|
||||||
|
series_launch_year: number;
|
||||||
|
subtitle_locales: Locale[];
|
||||||
|
tenant_categories?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum Locale {
|
||||||
|
enUS = 'en-US',
|
||||||
|
esLA = 'es-LA',
|
||||||
|
es419 = 'es-419',
|
||||||
|
esES = 'es-ES',
|
||||||
|
ptBR = 'pt-BR',
|
||||||
|
frFR = 'fr-FR',
|
||||||
|
deDE = 'de-DE',
|
||||||
|
arME = 'ar-ME',
|
||||||
|
arSA = 'ar-SA',
|
||||||
|
itIT = 'it-IT',
|
||||||
|
ruRU = 'ru-RU',
|
||||||
|
trTR = 'tr-TR',
|
||||||
|
hiIN = 'hi-IN',
|
||||||
|
zhCN = 'zh-CN',
|
||||||
|
koKR = 'ko-KR',
|
||||||
|
jaJP = 'ja-JP',
|
||||||
|
}
|
||||||
93
@types/crunchyAndroidStreams.d.ts
vendored
Normal file
93
@types/crunchyAndroidStreams.d.ts
vendored
Normal file
|
|
@ -0,0 +1,93 @@
|
||||||
|
export interface CrunchyAndroidStreams {
|
||||||
|
__class__: string;
|
||||||
|
__href__: string;
|
||||||
|
__resource_key__: string;
|
||||||
|
__links__: Links;
|
||||||
|
__actions__: Record<unknown, unknown>;
|
||||||
|
media_id: string;
|
||||||
|
audio_locale: Locale;
|
||||||
|
subtitles: Subtitles;
|
||||||
|
closed_captions: Subtitles;
|
||||||
|
streams: Streams;
|
||||||
|
bifs: string[];
|
||||||
|
versions: Version[];
|
||||||
|
captions: Record<unknown, unknown>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Subtitles {
|
||||||
|
'': Subtitle;
|
||||||
|
'en-US'?: Subtitle;
|
||||||
|
'es-LA'?: Subtitle;
|
||||||
|
'es-419'?: Subtitle;
|
||||||
|
'es-ES'?: Subtitle;
|
||||||
|
'pt-BR'?: Subtitle;
|
||||||
|
'fr-FR'?: Subtitle;
|
||||||
|
'de-DE'?: Subtitle;
|
||||||
|
'ar-ME'?: Subtitle;
|
||||||
|
'ar-SA'?: Subtitle;
|
||||||
|
'it-IT'?: Subtitle;
|
||||||
|
'ru-RU'?: Subtitle;
|
||||||
|
'tr-TR'?: Subtitle;
|
||||||
|
'hi-IN'?: Subtitle;
|
||||||
|
'zh-CN'?: Subtitle;
|
||||||
|
'ko-KR'?: Subtitle;
|
||||||
|
'ja-JP'?: Subtitle;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Links {
|
||||||
|
resource: Resource;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Resource {
|
||||||
|
href: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Streams {
|
||||||
|
[key: string]: { [key: string]: Download };
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Download {
|
||||||
|
hardsub_locale: Locale;
|
||||||
|
hardsub_lang?: string;
|
||||||
|
url: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Urls {
|
||||||
|
'': Download;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Subtitle {
|
||||||
|
locale: Locale;
|
||||||
|
url: string;
|
||||||
|
format: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Version {
|
||||||
|
audio_locale: Locale;
|
||||||
|
guid: string;
|
||||||
|
original: boolean;
|
||||||
|
variant: string;
|
||||||
|
season_guid: string;
|
||||||
|
media_guid: string;
|
||||||
|
is_premium_only: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum Locale {
|
||||||
|
default = '',
|
||||||
|
enUS = 'en-US',
|
||||||
|
esLA = 'es-LA',
|
||||||
|
es419 = 'es-419',
|
||||||
|
esES = 'es-ES',
|
||||||
|
ptBR = 'pt-BR',
|
||||||
|
frFR = 'fr-FR',
|
||||||
|
deDE = 'de-DE',
|
||||||
|
arME = 'ar-ME',
|
||||||
|
arSA = 'ar-SA',
|
||||||
|
itIT = 'it-IT',
|
||||||
|
ruRU = 'ru-RU',
|
||||||
|
trTR = 'tr-TR',
|
||||||
|
hiIN = 'hi-IN',
|
||||||
|
zhCN = 'zh-CN',
|
||||||
|
koKR = 'ko-KR',
|
||||||
|
jaJP = 'ja-JP',
|
||||||
|
}
|
||||||
26
@types/crunchyChapters.d.ts
vendored
Normal file
26
@types/crunchyChapters.d.ts
vendored
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
export interface CrunchyChapters {
|
||||||
|
[key: string]: CrunchyChapter;
|
||||||
|
lastUpdate: Date;
|
||||||
|
mediaId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CrunchyChapter {
|
||||||
|
approverId: string;
|
||||||
|
distributionNumber: string;
|
||||||
|
end: number;
|
||||||
|
start: number;
|
||||||
|
title: string;
|
||||||
|
seriesId: string;
|
||||||
|
new: boolean;
|
||||||
|
type: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CrunchyOldChapter {
|
||||||
|
media_id: string;
|
||||||
|
startTime: number;
|
||||||
|
endTime: number;
|
||||||
|
duration: number;
|
||||||
|
comparedWith: string;
|
||||||
|
ordering: string;
|
||||||
|
last_updated: Date;
|
||||||
|
}
|
||||||
253
@types/crunchyEpisodeList.d.ts
vendored
253
@types/crunchyEpisodeList.d.ts
vendored
|
|
@ -1,119 +1,134 @@
|
||||||
export interface CrunchyEpisodeList {
|
import { Links } from './crunchyAndroidEpisodes';
|
||||||
__class__: string;
|
|
||||||
__href__: string;
|
export interface CrunchyEpisodeList {
|
||||||
__resource_key__: string;
|
total: number;
|
||||||
__links__: unknown;
|
data: CrunchyEpisode[];
|
||||||
__actions__: unknown;
|
meta: Meta;
|
||||||
total: number;
|
}
|
||||||
items: Item[];
|
|
||||||
}
|
export interface CrunchyEpisode {
|
||||||
|
next_episode_id: string;
|
||||||
export interface Item {
|
series_id: string;
|
||||||
__class__: Class;
|
season_number: number;
|
||||||
__href__: string;
|
next_episode_title: string;
|
||||||
__resource_key__: string;
|
availability_notes: string;
|
||||||
__links__: Links;
|
duration_ms: number;
|
||||||
__actions__: unknown;
|
series_slug_title: string;
|
||||||
id: string;
|
series_title: string;
|
||||||
channel_id: ChannelID;
|
is_dubbed: boolean;
|
||||||
series_id: string;
|
versions: Version[] | null;
|
||||||
series_title: string;
|
identifier: string;
|
||||||
series_slug_title: string;
|
sequence_number: number;
|
||||||
season_id: string;
|
eligible_region: Record<unknown>;
|
||||||
season_title: string;
|
availability_starts: Date;
|
||||||
season_slug_title: string;
|
images: Images;
|
||||||
season_number: number;
|
season_id: string;
|
||||||
episode: string;
|
seo_title: string;
|
||||||
episode_number: number | null;
|
is_premium_only: boolean;
|
||||||
sequence_number: number;
|
extended_maturity_rating: Record<unknown>;
|
||||||
production_episode_id: string;
|
title: string;
|
||||||
title: string;
|
production_episode_id: string;
|
||||||
slug_title: string;
|
premium_available_date: Date;
|
||||||
description: string;
|
season_title: string;
|
||||||
next_episode_id?: string;
|
seo_description: string;
|
||||||
next_episode_title?: string;
|
audio_locale: Locale;
|
||||||
hd_flag: boolean;
|
id: string;
|
||||||
is_mature: boolean;
|
media_type: MediaType;
|
||||||
mature_blocked: boolean;
|
availability_ends: Date;
|
||||||
episode_air_date: string;
|
free_available_date: Date;
|
||||||
is_subbed: boolean;
|
playback: string;
|
||||||
is_dubbed: boolean;
|
channel_id: ChannelID;
|
||||||
is_clip: boolean;
|
episode: string;
|
||||||
seo_title: string;
|
is_mature: boolean;
|
||||||
seo_description: string;
|
listing_id: string;
|
||||||
season_tags: string[];
|
episode_air_date: Date;
|
||||||
available_offline: boolean;
|
slug: string;
|
||||||
media_type: Class;
|
available_date: Date;
|
||||||
slug: string;
|
subtitle_locales: Locale[];
|
||||||
images: Images;
|
slug_title: string;
|
||||||
duration_ms: number;
|
available_offline: boolean;
|
||||||
ad_breaks: AdBreak[];
|
description: string;
|
||||||
is_premium_only: boolean;
|
is_subbed: boolean;
|
||||||
listing_id: string;
|
premium_date: Date;
|
||||||
subtitle_locales: SubtitleLocale[];
|
upload_date: Date;
|
||||||
playback?: string;
|
season_slug_title: string;
|
||||||
availability_notes: string;
|
closed_captions_available: boolean;
|
||||||
available_date?: string;
|
episode_number: number;
|
||||||
hide_season_title?: boolean;
|
season_tags: any[];
|
||||||
hide_season_number?: boolean;
|
maturity_ratings: MaturityRating[];
|
||||||
isSelected?: boolean;
|
streams_link?: string;
|
||||||
seq_id: string;
|
mature_blocked: boolean;
|
||||||
}
|
is_clip: boolean;
|
||||||
|
hd_flag: boolean;
|
||||||
export enum Class {
|
hide_season_title?: boolean;
|
||||||
Episode = 'episode',
|
hide_season_number?: boolean;
|
||||||
}
|
isSelected?: boolean;
|
||||||
|
seq_id: string;
|
||||||
export interface Links {
|
__links__?: Links;
|
||||||
ads: Ads;
|
}
|
||||||
'episode/channel': Ads;
|
|
||||||
'episode/next_episode'?: Ads;
|
export enum Locale {
|
||||||
'episode/season': Ads;
|
enUS = 'en-US',
|
||||||
'episode/series': Ads;
|
esLA = 'es-LA',
|
||||||
streams?: Ads;
|
es419 = 'es-419',
|
||||||
}
|
esES = 'es-ES',
|
||||||
|
ptBR = 'pt-BR',
|
||||||
export interface Ads {
|
frFR = 'fr-FR',
|
||||||
href: string;
|
deDE = 'de-DE',
|
||||||
}
|
arME = 'ar-ME',
|
||||||
|
arSA = 'ar-SA',
|
||||||
export interface AdBreak {
|
itIT = 'it-IT',
|
||||||
type: AdBreakType;
|
ruRU = 'ru-RU',
|
||||||
offset_ms: number;
|
trTR = 'tr-TR',
|
||||||
}
|
hiIN = 'hi-IN',
|
||||||
|
zhCN = 'zh-CN',
|
||||||
export enum AdBreakType {
|
koKR = 'ko-KR',
|
||||||
Midroll = 'midroll',
|
jaJP = 'ja-JP',
|
||||||
Preroll = 'preroll',
|
}
|
||||||
}
|
|
||||||
|
export enum ChannelID {
|
||||||
export enum ChannelID {
|
Crunchyroll = 'crunchyroll',
|
||||||
Crunchyroll = 'crunchyroll',
|
}
|
||||||
}
|
|
||||||
|
export interface Images {
|
||||||
export interface Images {
|
poster_tall?: Array<Image[]>;
|
||||||
thumbnail: Array<Thumbnail[]>;
|
poster_wide?: Array<Image[]>;
|
||||||
}
|
promo_image?: Array<Image[]>;
|
||||||
|
thumbnail?: Array<Image[]>;
|
||||||
export interface Thumbnail {
|
}
|
||||||
width: number;
|
|
||||||
height: number;
|
export interface Image {
|
||||||
type: ThumbnailType;
|
height: number;
|
||||||
source: string;
|
source: string;
|
||||||
}
|
type: ImageType;
|
||||||
|
width: number;
|
||||||
export enum ThumbnailType {
|
}
|
||||||
Thumbnail = 'thumbnail',
|
|
||||||
}
|
export enum ImageType {
|
||||||
|
PosterTall = 'poster_tall',
|
||||||
export enum SubtitleLocale {
|
PosterWide = 'poster_wide',
|
||||||
ArSA = 'ar-SA',
|
PromoImage = 'promo_image',
|
||||||
DeDE = 'de-DE',
|
Thumbnail = 'thumbnail',
|
||||||
EnUS = 'en-US',
|
}
|
||||||
Es419 = 'es-419',
|
|
||||||
EsES = 'es-ES',
|
export enum MaturityRating {
|
||||||
FrFR = 'fr-FR',
|
Tv14 = 'TV-14',
|
||||||
ItIT = 'it-IT',
|
}
|
||||||
PtBR = 'pt-BR',
|
|
||||||
RuRU = 'ru-RU',
|
export enum MediaType {
|
||||||
}
|
Episode = 'episode',
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Version {
|
||||||
|
audio_locale: Locale;
|
||||||
|
guid: string;
|
||||||
|
is_premium_only: boolean;
|
||||||
|
media_guid: string;
|
||||||
|
original: boolean;
|
||||||
|
season_guid: string;
|
||||||
|
variant: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Meta {
|
||||||
|
versions_considered?: boolean;
|
||||||
|
}
|
||||||
44
@types/crunchyPlayStreams.d.ts
vendored
Normal file
44
@types/crunchyPlayStreams.d.ts
vendored
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
import { Locale } from './playbackData';
|
||||||
|
|
||||||
|
export interface CrunchyPlayStream {
|
||||||
|
assetId: string;
|
||||||
|
audioLocale: Locale;
|
||||||
|
bifs: string;
|
||||||
|
burnedInLocale: string;
|
||||||
|
captions: { [key: string]: Caption };
|
||||||
|
hardSubs: { [key: string]: HardSub };
|
||||||
|
playbackType: string;
|
||||||
|
session: Session;
|
||||||
|
subtitles: { [key: string]: Subtitle };
|
||||||
|
token: string;
|
||||||
|
url: string;
|
||||||
|
versions: any[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Caption {
|
||||||
|
format: string;
|
||||||
|
language: string;
|
||||||
|
url: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface HardSub {
|
||||||
|
hlang: string;
|
||||||
|
url: string;
|
||||||
|
quality: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Session {
|
||||||
|
renewSeconds: number;
|
||||||
|
noNetworkRetryIntervalSeconds: number;
|
||||||
|
noNetworkTimeoutSeconds: number;
|
||||||
|
maximumPauseSeconds: number;
|
||||||
|
endOfVideoUnloadSeconds: number;
|
||||||
|
sessionExpirationSeconds: number;
|
||||||
|
usesStreamLimits: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Subtitle {
|
||||||
|
format: string;
|
||||||
|
language: string;
|
||||||
|
url: string;
|
||||||
|
}
|
||||||
346
@types/crunchySearch.d.ts
vendored
346
@types/crunchySearch.d.ts
vendored
|
|
@ -1,165 +1,183 @@
|
||||||
// Generated by https://quicktype.io
|
// Generated by https://quicktype.io
|
||||||
|
|
||||||
export interface CrunchySearch {
|
export interface CrunchySearch {
|
||||||
__class__: string;
|
total: number;
|
||||||
__href__: string;
|
data: CrunchySearchData[];
|
||||||
__resource_key__: string;
|
meta: Record<string, unknown>;
|
||||||
__links__: CrunchySearchLinks;
|
}
|
||||||
__actions__: unknown;
|
|
||||||
total: number;
|
export interface CrunchySearchData {
|
||||||
items: CrunchySearchItem[];
|
type: string;
|
||||||
}
|
count: number;
|
||||||
|
items: CrunchySearchItem[];
|
||||||
|
}
|
||||||
export interface CrunchySearchLinks {
|
|
||||||
continuation?: Continuation;
|
export interface CrunchySearchItem {
|
||||||
}
|
title: string;
|
||||||
|
images: Images;
|
||||||
export interface Continuation {
|
series_metadata?: SeriesMetadata;
|
||||||
href: string;
|
promo_description: string;
|
||||||
}
|
external_id: string;
|
||||||
|
slug: string;
|
||||||
export interface CrunchySearchItem {
|
new: boolean;
|
||||||
__class__: string;
|
slug_title: string;
|
||||||
__href__: string;
|
channel_id: ChannelID;
|
||||||
__resource_key__: string;
|
description: string;
|
||||||
__links__: CrunchySearchLinks;
|
linked_resource_key: string;
|
||||||
__actions__: unknown;
|
type: ItemType;
|
||||||
type: string;
|
id: string;
|
||||||
total: number;
|
promo_title: string;
|
||||||
items: ItemItem[];
|
search_metadata: SearchMetadata;
|
||||||
}
|
movie_listing_metadata?: MovieListingMetadata;
|
||||||
|
playback?: string;
|
||||||
export interface ItemItem {
|
streams_link?: string;
|
||||||
__actions__: unknown;
|
episode_metadata?: EpisodeMetadata;
|
||||||
__class__: Class;
|
}
|
||||||
__href__: string;
|
|
||||||
__links__: PurpleLinks;
|
export enum ChannelID {
|
||||||
channel_id: ChannelID;
|
Crunchyroll = 'crunchyroll',
|
||||||
description: string;
|
}
|
||||||
external_id: string;
|
|
||||||
id: string;
|
export interface EpisodeMetadata {
|
||||||
images: Images;
|
audio_locale: Locale;
|
||||||
linked_resource_key: string;
|
availability_ends: Date;
|
||||||
new: boolean;
|
availability_notes: string;
|
||||||
new_content: boolean;
|
availability_starts: Date;
|
||||||
promo_description: string;
|
available_date: null;
|
||||||
promo_title: string;
|
available_offline: boolean;
|
||||||
search_metadata: SearchMetadata;
|
closed_captions_available: boolean;
|
||||||
series_metadata?: SeriesMetadata;
|
duration_ms: number;
|
||||||
slug: string;
|
eligible_region: string[];
|
||||||
slug_title: string;
|
episode: string;
|
||||||
title: string;
|
episode_air_date: Date;
|
||||||
type: ItemType;
|
episode_number: number;
|
||||||
episode_metadata?: EpisodeMetadata;
|
extended_maturity_rating: Record<unknown>;
|
||||||
playback?: string;
|
free_available_date: Date;
|
||||||
isSelected?: boolean;
|
identifier: string;
|
||||||
season_number?: string;
|
is_clip: boolean;
|
||||||
is_premium_only?: boolean;
|
is_dubbed: boolean;
|
||||||
hide_metadata?: boolean;
|
is_mature: boolean;
|
||||||
seq_id?: string;
|
is_premium_only: boolean;
|
||||||
f_num?: string;
|
is_subbed: boolean;
|
||||||
s_num?: string;
|
mature_blocked: boolean;
|
||||||
ep_num?: string;
|
maturity_ratings: MaturityRating[];
|
||||||
last_public?: string;
|
premium_available_date: Date;
|
||||||
subtitle_locales?: string[];
|
premium_date: null;
|
||||||
availability_notes?: string
|
season_id: string;
|
||||||
}
|
season_number: number;
|
||||||
|
season_slug_title: string;
|
||||||
export enum Class {
|
season_title: string;
|
||||||
Panel = 'panel',
|
sequence_number: number;
|
||||||
}
|
series_id: string;
|
||||||
|
series_slug_title: string;
|
||||||
export interface PurpleLinks {
|
series_title: string;
|
||||||
resource: Continuation;
|
subtitle_locales: Locale[];
|
||||||
'resource/channel': Continuation;
|
upload_date: Date;
|
||||||
'episode/season'?: Continuation;
|
versions: Version[] | null;
|
||||||
'episode/series'?: Continuation;
|
tenant_categories?: string[];
|
||||||
streams?: Continuation;
|
}
|
||||||
}
|
|
||||||
|
export enum Locale {
|
||||||
export enum ChannelID {
|
enUS = 'en-US',
|
||||||
Crunchyroll = 'crunchyroll',
|
esLA = 'es-LA',
|
||||||
}
|
es419 = 'es-419',
|
||||||
|
esES = 'es-ES',
|
||||||
export interface EpisodeMetadata {
|
ptBR = 'pt-BR',
|
||||||
ad_breaks: AdBreak[];
|
frFR = 'fr-FR',
|
||||||
availability_notes: string;
|
deDE = 'de-DE',
|
||||||
available_offline: boolean;
|
arME = 'ar-ME',
|
||||||
duration_ms: number;
|
arSA = 'ar-SA',
|
||||||
episode: string;
|
itIT = 'it-IT',
|
||||||
episode_air_date: string;
|
ruRU = 'ru-RU',
|
||||||
episode_number: number;
|
trTR = 'tr-TR',
|
||||||
is_clip: boolean;
|
hiIN = 'hi-IN',
|
||||||
is_dubbed: boolean;
|
zhCN = 'zh-CN',
|
||||||
is_mature: boolean;
|
koKR = 'ko-KR',
|
||||||
is_premium_only: boolean;
|
jaJP = 'ja-JP',
|
||||||
is_subbed: boolean;
|
}
|
||||||
mature_blocked: boolean;
|
|
||||||
maturity_ratings: string[];
|
export enum MaturityRating {
|
||||||
season_id: string;
|
Tv14 = 'TV-14',
|
||||||
season_number: number;
|
TvMa = 'TV-MA',
|
||||||
season_slug_title: string;
|
}
|
||||||
season_title: string;
|
|
||||||
sequence_number: number;
|
export interface Version {
|
||||||
series_id: string;
|
audio_locale: Locale;
|
||||||
series_slug_title: string;
|
guid: string;
|
||||||
series_title: string;
|
is_premium_only: boolean;
|
||||||
subtitle_locales: string[];
|
media_guid: string;
|
||||||
tenant_categories?: TenantCategory[];
|
original: boolean;
|
||||||
available_date?: string;
|
season_guid: string;
|
||||||
free_available_date?: string;
|
variant: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AdBreak {
|
export interface Images {
|
||||||
offset_ms: number;
|
poster_tall?: Array<Image[]>;
|
||||||
type: AdBreakType;
|
poster_wide?: Array<Image[]>;
|
||||||
}
|
promo_image?: Array<Image[]>;
|
||||||
|
thumbnail?: Array<Image[]>;
|
||||||
export enum AdBreakType {
|
}
|
||||||
Midroll = 'midroll',
|
|
||||||
Preroll = 'preroll',
|
export interface Image {
|
||||||
}
|
height: number;
|
||||||
|
source: string;
|
||||||
export enum TenantCategory {
|
type: ImageType;
|
||||||
Action = 'Action',
|
width: number;
|
||||||
Drama = 'Drama',
|
}
|
||||||
SciFi = 'Sci-Fi',
|
|
||||||
}
|
export enum ImageType {
|
||||||
|
PosterTall = 'poster_tall',
|
||||||
export interface Images {
|
PosterWide = 'poster_wide',
|
||||||
poster_tall?: Array<PosterTall[]>;
|
PromoImage = 'promo_image',
|
||||||
poster_wide?: Array<PosterTall[]>;
|
Thumbnail = 'thumbnail',
|
||||||
thumbnail?: Array<PosterTall[]>;
|
}
|
||||||
}
|
|
||||||
|
export interface MovieListingMetadata {
|
||||||
export interface PosterTall {
|
availability_notes: string;
|
||||||
height: number;
|
available_date: null;
|
||||||
source: string;
|
available_offline: boolean;
|
||||||
type: PosterTallType;
|
duration_ms: number;
|
||||||
width: number;
|
extended_description: string;
|
||||||
}
|
extended_maturity_rating: Record<unknown>;
|
||||||
|
first_movie_id: string;
|
||||||
export enum PosterTallType {
|
free_available_date: Date;
|
||||||
PosterTall = 'poster_tall',
|
is_dubbed: boolean;
|
||||||
PosterWide = 'poster_wide',
|
is_mature: boolean;
|
||||||
Thumbnail = 'thumbnail',
|
is_premium_only: boolean;
|
||||||
}
|
is_subbed: boolean;
|
||||||
|
mature_blocked: boolean;
|
||||||
export interface SearchMetadata {
|
maturity_ratings: string[];
|
||||||
score: number;
|
movie_release_year: number;
|
||||||
}
|
premium_available_date: Date;
|
||||||
|
premium_date: null;
|
||||||
export interface SeriesMetadata {
|
subtitle_locales: any[];
|
||||||
availability_notes: string;
|
tenant_categories: string[];
|
||||||
episode_count: number;
|
}
|
||||||
extended_description: string;
|
|
||||||
is_dubbed: boolean;
|
export interface SearchMetadata {
|
||||||
is_mature: boolean;
|
score: number;
|
||||||
is_simulcast: boolean;
|
}
|
||||||
is_subbed: boolean;
|
|
||||||
mature_blocked: boolean;
|
export interface SeriesMetadata {
|
||||||
maturity_ratings: string[];
|
audio_locales: Locale[];
|
||||||
season_count: number;
|
availability_notes: string;
|
||||||
tenant_categories: TenantCategory[];
|
episode_count: number;
|
||||||
|
extended_description: string;
|
||||||
|
extended_maturity_rating: Record<unknown>;
|
||||||
|
is_dubbed: boolean;
|
||||||
|
is_mature: boolean;
|
||||||
|
is_simulcast: boolean;
|
||||||
|
is_subbed: boolean;
|
||||||
|
mature_blocked: boolean;
|
||||||
|
maturity_ratings: MaturityRating[];
|
||||||
|
season_count: number;
|
||||||
|
series_launch_year: number;
|
||||||
|
subtitle_locales: Locale[];
|
||||||
|
tenant_categories?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum ItemType {
|
||||||
|
Episode = 'episode',
|
||||||
|
MovieListing = 'movie_listing',
|
||||||
|
Series = 'series',
|
||||||
}
|
}
|
||||||
301
@types/crunchyTypes.d.ts
vendored
301
@types/crunchyTypes.d.ts
vendored
|
|
@ -1,90 +1,211 @@
|
||||||
import { sxItem } from '../crunchy';
|
import { HLSCallback } from 'hls-download';
|
||||||
import { LanguageItem } from '../modules/module.langsData';
|
import { sxItem } from '../crunchy';
|
||||||
|
import { LanguageItem } from '../modules/module.langsData';
|
||||||
export type CrunchyEpMeta = {
|
import { DownloadInfo } from './messageHandler';
|
||||||
data: {
|
import { CrunchyPlayStreams } from './enums';
|
||||||
mediaId: string,
|
|
||||||
lang?: LanguageItem,
|
export type CrunchyDownloadOptions = {
|
||||||
playback?: string
|
hslang: string,
|
||||||
}[],
|
kstream: number,
|
||||||
seasonTitle: string,
|
cstream: keyof typeof CrunchyPlayStreams | 'none',
|
||||||
episodeNumber: string,
|
novids?: boolean,
|
||||||
episodeTitle: string,
|
noaudio?: boolean,
|
||||||
seasonID: string,
|
x: number,
|
||||||
season: number
|
q: number,
|
||||||
}
|
fileName: string,
|
||||||
|
numbers: number,
|
||||||
export type DownloadedMedia = {
|
partsize: number,
|
||||||
type: 'Video',
|
callbackMaker?: (data: DownloadInfo) => HLSCallback,
|
||||||
lang: LanguageItem,
|
timeout: number,
|
||||||
path: string
|
waittime: number,
|
||||||
} | ({
|
fsRetryTime: number,
|
||||||
type: 'Subtitle'
|
dlsubs: string[],
|
||||||
} & sxItem )
|
skipsubs: boolean,
|
||||||
|
nosubs?: boolean,
|
||||||
export type ParseItem = {
|
mp4: boolean,
|
||||||
__class__?: string;
|
override: string[],
|
||||||
isSelected?: boolean,
|
videoTitle: string,
|
||||||
type?: string,
|
force: 'Y'|'y'|'N'|'n'|'C'|'c',
|
||||||
id: string,
|
ffmpegOptions: string[],
|
||||||
title: string,
|
mkvmergeOptions: string[],
|
||||||
playback?: string,
|
defaultSub: LanguageItem,
|
||||||
season_number?: number|string,
|
defaultAudio: LanguageItem,
|
||||||
is_premium_only?: boolean,
|
ccTag: string,
|
||||||
hide_metadata?: boolean,
|
dlVideoOnce: boolean,
|
||||||
seq_id?: string,
|
skipmux?: boolean,
|
||||||
f_num?: string,
|
syncTiming: boolean,
|
||||||
s_num?: string
|
nocleanup: boolean,
|
||||||
external_id?: string,
|
chapters: boolean,
|
||||||
ep_num?: string
|
fontName: string | undefined,
|
||||||
last_public?: string,
|
originalFontSize: boolean,
|
||||||
subtitle_locales?: string[],
|
fontSize: number,
|
||||||
availability_notes?: string
|
dubLang: string[],
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SeriesSearch {
|
export type CrunchyMultiDownload = {
|
||||||
__class__: string;
|
dubLang: string[],
|
||||||
__href__: string;
|
all?: boolean,
|
||||||
__resource_key__: string;
|
but?: boolean,
|
||||||
__links__: Actions;
|
e?: string,
|
||||||
__actions__: Actions;
|
s?: string
|
||||||
total: number;
|
}
|
||||||
items: SeriesSearchItem[];
|
|
||||||
}
|
export type CrunchyMuxOptions = {
|
||||||
|
output: string,
|
||||||
export interface SeriesSearchItem {
|
skipSubMux?: boolean
|
||||||
__class__: string;
|
keepAllVideos?: bolean
|
||||||
__href__: string;
|
novids?: boolean,
|
||||||
__resource_key__: string;
|
mp4: boolean,
|
||||||
__links__: Links;
|
forceMuxer?: 'ffmpeg'|'mkvmerge',
|
||||||
__actions__: string[];
|
nocleanup?: boolean,
|
||||||
id: string;
|
videoTitle: string,
|
||||||
channel_id: string;
|
ffmpegOptions: string[],
|
||||||
title: string;
|
mkvmergeOptions: string[],
|
||||||
slug_title: string;
|
defaultSub: LanguageItem,
|
||||||
series_id: string;
|
defaultAudio: LanguageItem,
|
||||||
season_number: number;
|
ccTag: string,
|
||||||
is_complete: boolean;
|
syncTiming: boolean,
|
||||||
description: string;
|
}
|
||||||
keywords: any[];
|
|
||||||
season_tags: string[];
|
export type CrunchyEpMeta = {
|
||||||
images: Actions;
|
data: {
|
||||||
is_mature: boolean;
|
mediaId: string,
|
||||||
mature_blocked: boolean;
|
lang?: LanguageItem,
|
||||||
is_subbed: boolean;
|
playback?: string,
|
||||||
is_dubbed: boolean;
|
versions?: EpisodeVersion[] | null,
|
||||||
is_simulcast: boolean;
|
isSubbed: boolean,
|
||||||
seo_title: string;
|
isDubbed: boolean,
|
||||||
seo_description: string;
|
}[],
|
||||||
availability_notes: string;
|
seriesTitle: string,
|
||||||
}
|
seasonTitle: string,
|
||||||
|
episodeNumber: string,
|
||||||
export interface Links {
|
episodeTitle: string,
|
||||||
'season/channel': Season;
|
seasonID: string,
|
||||||
'season/episodes': Season;
|
season: number,
|
||||||
'season/series': Season;
|
showID: string,
|
||||||
}
|
e: string,
|
||||||
|
image: string,
|
||||||
export interface Season {
|
}
|
||||||
href: string;
|
|
||||||
}
|
export type DownloadedMedia = {
|
||||||
|
type: 'Video',
|
||||||
|
lang: LanguageItem,
|
||||||
|
path: string,
|
||||||
|
isPrimary?: boolean
|
||||||
|
} | {
|
||||||
|
type: 'Audio',
|
||||||
|
lang: LanguageItem,
|
||||||
|
path: string,
|
||||||
|
isPrimary?: boolean
|
||||||
|
} | {
|
||||||
|
type: 'Chapters',
|
||||||
|
lang: LanguageItem,
|
||||||
|
path: string
|
||||||
|
} | ({
|
||||||
|
type: 'Subtitle',
|
||||||
|
signs: boolean,
|
||||||
|
cc: boolean
|
||||||
|
} & sxItem )
|
||||||
|
|
||||||
|
export type ParseItem = {
|
||||||
|
__class__?: string;
|
||||||
|
isSelected?: boolean,
|
||||||
|
type?: string,
|
||||||
|
id: string,
|
||||||
|
title: string,
|
||||||
|
playback?: string,
|
||||||
|
season_number?: number|string,
|
||||||
|
episode_number?: number|string,
|
||||||
|
season_count?: number|string,
|
||||||
|
is_premium_only?: boolean,
|
||||||
|
hide_metadata?: boolean,
|
||||||
|
seq_id?: string,
|
||||||
|
f_num?: string,
|
||||||
|
s_num?: string
|
||||||
|
external_id?: string,
|
||||||
|
ep_num?: string
|
||||||
|
last_public?: string,
|
||||||
|
subtitle_locales?: string[],
|
||||||
|
availability_notes?: string,
|
||||||
|
identifier?: string,
|
||||||
|
versions?: Version[] | null,
|
||||||
|
media_type?: string | null,
|
||||||
|
movie_release_year?: number | null,
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SeriesSearch {
|
||||||
|
total: number;
|
||||||
|
data: SeriesSearchItem[];
|
||||||
|
meta: Meta;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SeriesSearchItem {
|
||||||
|
description: string;
|
||||||
|
seo_description: string;
|
||||||
|
number_of_episodes: number;
|
||||||
|
is_dubbed: boolean;
|
||||||
|
identifier: string;
|
||||||
|
channel_id: string;
|
||||||
|
slug_title: string;
|
||||||
|
season_sequence_number: number;
|
||||||
|
season_tags: string[];
|
||||||
|
extended_maturity_rating: Record<unknown>;
|
||||||
|
is_mature: boolean;
|
||||||
|
audio_locale: string;
|
||||||
|
season_number: number;
|
||||||
|
images: Record<unknown>;
|
||||||
|
mature_blocked: boolean;
|
||||||
|
versions: Version[];
|
||||||
|
title: string;
|
||||||
|
is_subbed: boolean;
|
||||||
|
id: string;
|
||||||
|
audio_locales: string[];
|
||||||
|
subtitle_locales: string[];
|
||||||
|
availability_notes: string;
|
||||||
|
series_id: string;
|
||||||
|
season_display_number: string;
|
||||||
|
is_complete: boolean;
|
||||||
|
keywords: any[];
|
||||||
|
maturity_ratings: string[];
|
||||||
|
is_simulcast: boolean;
|
||||||
|
seo_title: string;
|
||||||
|
}
|
||||||
|
export interface Version {
|
||||||
|
audio_locale: Locale;
|
||||||
|
guid: string;
|
||||||
|
original: boolean;
|
||||||
|
variant: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EpisodeVersion {
|
||||||
|
audio_locale: Locale;
|
||||||
|
guid: string;
|
||||||
|
is_premium_only: boolean;
|
||||||
|
media_guid: string;
|
||||||
|
original: boolean;
|
||||||
|
season_guid: string;
|
||||||
|
variant: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum Locale {
|
||||||
|
enUS = 'en-US',
|
||||||
|
esLA = 'es-LA',
|
||||||
|
es419 = 'es-419',
|
||||||
|
esES = 'es-ES',
|
||||||
|
ptBR = 'pt-BR',
|
||||||
|
frFR = 'fr-FR',
|
||||||
|
deDE = 'de-DE',
|
||||||
|
arME = 'ar-ME',
|
||||||
|
arSA = 'ar-SA',
|
||||||
|
itIT = 'it-IT',
|
||||||
|
ruRU = 'ru-RU',
|
||||||
|
trTR = 'tr-TR',
|
||||||
|
hiIN = 'hi-IN',
|
||||||
|
zhCN = 'zh-CN',
|
||||||
|
koKR = 'ko-KR',
|
||||||
|
jaJP = 'ja-JP',
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Meta {
|
||||||
|
versions_considered: boolean;
|
||||||
|
}
|
||||||
|
|
|
||||||
10
@types/downloadedFile.d.ts
vendored
10
@types/downloadedFile.d.ts
vendored
|
|
@ -1,6 +1,6 @@
|
||||||
import { LanguageItem } from '../modules/module.langsData';
|
import { LanguageItem } from '../modules/module.langsData';
|
||||||
|
|
||||||
export type DownloadedFile = {
|
export type DownloadedFile = {
|
||||||
path: string,
|
path: string,
|
||||||
lang: LanguageItem
|
lang: LanguageItem
|
||||||
}
|
}
|
||||||
16
@types/enums.ts
Normal file
16
@types/enums.ts
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
export enum CrunchyPlayStreams {
|
||||||
|
'chrome' = 'web/chrome',
|
||||||
|
'firefox' = 'web/firefox',
|
||||||
|
'safari' = 'web/safari',
|
||||||
|
'edge' = 'web/edge',
|
||||||
|
'fallback' = 'web/fallback',
|
||||||
|
'ps4' = 'console/ps4',
|
||||||
|
'ps5' = 'console/ps5',
|
||||||
|
'switch' = 'console/switch',
|
||||||
|
'samsungtv' = 'tv/samsung',
|
||||||
|
'lgtv' = 'tv/lg',
|
||||||
|
'rokutv' = 'tv/roku',
|
||||||
|
'android' = 'android/phone',
|
||||||
|
'iphone' = 'ios/iphone',
|
||||||
|
'ipad' = 'ios/ipad',
|
||||||
|
}
|
||||||
782
@types/episode.d.ts
vendored
782
@types/episode.d.ts
vendored
|
|
@ -1,391 +1,391 @@
|
||||||
// Generated by https://quicktype.io
|
// Generated by https://quicktype.io
|
||||||
|
|
||||||
export interface EpisodeData {
|
export interface EpisodeData {
|
||||||
id: number;
|
id: number;
|
||||||
title: string;
|
title: string;
|
||||||
mediaDict: { [key: string]: string };
|
mediaDict: { [key: string]: string };
|
||||||
episodeSlug: string;
|
episodeSlug: string;
|
||||||
starRating: number;
|
starRating: number;
|
||||||
parent: EpisodeDataParent;
|
parent: EpisodeDataParent;
|
||||||
number: string;
|
number: string;
|
||||||
description: string;
|
description: string;
|
||||||
filename: string;
|
filename: string;
|
||||||
seriesBanner: string;
|
seriesBanner: string;
|
||||||
media: Media[];
|
media: Media[];
|
||||||
externalItemId: string;
|
externalItemId: string;
|
||||||
contentId: string;
|
contentId: string;
|
||||||
metaItems: MetaItems;
|
metaItems: MetaItems;
|
||||||
thumb: string;
|
thumb: string;
|
||||||
type: Type;
|
type: Type;
|
||||||
default: { [key: string]: Default };
|
default: { [key: string]: Default };
|
||||||
published: boolean;
|
published: boolean;
|
||||||
versions: VersionClass[];
|
versions: VersionClass[];
|
||||||
mediaCategory: string;
|
mediaCategory: string;
|
||||||
order: number;
|
order: number;
|
||||||
seriesVersions: any[];
|
seriesVersions: any[];
|
||||||
source: Source;
|
source: Source;
|
||||||
ids: EpisodeDataIDS;
|
ids: EpisodeDataIDS;
|
||||||
runtime: string;
|
runtime: string;
|
||||||
siblings: PreviousSeasonEpisode[];
|
siblings: PreviousSeasonEpisode[];
|
||||||
seriesTitle: string;
|
seriesTitle: string;
|
||||||
seriesSlug: string;
|
seriesSlug: string;
|
||||||
next: Next;
|
next: Next;
|
||||||
previousSeasonEpisode: PreviousSeasonEpisode;
|
previousSeasonEpisode: PreviousSeasonEpisode;
|
||||||
seasonTitle: string;
|
seasonTitle: string;
|
||||||
quality: Quality;
|
quality: Quality;
|
||||||
ratings: Array<string[]>;
|
ratings: Array<string[]>;
|
||||||
languages: TitleElement[];
|
languages: TitleElement[];
|
||||||
releaseDate: string;
|
releaseDate: string;
|
||||||
historicalSelections: HistoricalSelections;
|
historicalSelections: HistoricalSelections;
|
||||||
userRating: UserRating;
|
userRating: UserRating;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Default {
|
export interface Default {
|
||||||
items: DefaultItem[];
|
items: DefaultItem[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DefaultItem {
|
export interface DefaultItem {
|
||||||
languages: string[];
|
languages: string[];
|
||||||
territories: string[];
|
territories: string[];
|
||||||
version: null;
|
version: null;
|
||||||
value: Value[];
|
value: Value[];
|
||||||
devices: any[];
|
devices: any[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Value {
|
export interface Value {
|
||||||
name: MetaType;
|
name: MetaType;
|
||||||
value: string;
|
value: string;
|
||||||
label: Label;
|
label: Label;
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum Label {
|
export enum Label {
|
||||||
Rating = 'Rating',
|
Rating = 'Rating',
|
||||||
RatingSystem = 'Rating System',
|
RatingSystem = 'Rating System',
|
||||||
ReleaseDate = 'Release Date',
|
ReleaseDate = 'Release Date',
|
||||||
Synopsis = 'Synopsis',
|
Synopsis = 'Synopsis',
|
||||||
SynopsisType = 'Synopsis Type',
|
SynopsisType = 'Synopsis Type',
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum MetaType {
|
export enum MetaType {
|
||||||
Rating = 'rating',
|
Rating = 'rating',
|
||||||
RatingSystemType = 'RatingSystemType',
|
RatingSystemType = 'RatingSystemType',
|
||||||
ReleaseDate = 'release-date',
|
ReleaseDate = 'release-date',
|
||||||
Synopsis = 'synopsis',
|
Synopsis = 'synopsis',
|
||||||
Synopsistype = 'synopsistype',
|
Synopsistype = 'synopsistype',
|
||||||
VideoRatingType = 'VideoRatingType',
|
VideoRatingType = 'VideoRatingType',
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface HistoricalSelections {
|
export interface HistoricalSelections {
|
||||||
version: string;
|
version: string;
|
||||||
language: string;
|
language: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface EpisodeDataIDS {
|
export interface EpisodeDataIDS {
|
||||||
externalShowId: string;
|
externalShowId: string;
|
||||||
externalSeasonId: string;
|
externalSeasonId: string;
|
||||||
externalEpisodeId: string;
|
externalEpisodeId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum TitleElement {
|
export enum TitleElement {
|
||||||
Empty = '',
|
Empty = '',
|
||||||
English = 'English',
|
English = 'English',
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Media {
|
export interface Media {
|
||||||
id: number;
|
id: number;
|
||||||
title: string;
|
title: string;
|
||||||
experienceType: string;
|
experienceType: string;
|
||||||
created: string;
|
created: string;
|
||||||
createdBy: string;
|
createdBy: string;
|
||||||
itemFieldData: Next;
|
itemFieldData: Next;
|
||||||
keyPath: string;
|
keyPath: string;
|
||||||
filename: string;
|
filename: string;
|
||||||
complianceStatus: null;
|
complianceStatus: null;
|
||||||
events: any[];
|
events: any[];
|
||||||
clients: string[];
|
clients: string[];
|
||||||
qcStatus: null;
|
qcStatus: null;
|
||||||
qcStatusDate: null;
|
qcStatusDate: null;
|
||||||
image: string;
|
image: string;
|
||||||
thumb: string;
|
thumb: string;
|
||||||
ext: string;
|
ext: string;
|
||||||
avails: Avail[];
|
avails: Avail[];
|
||||||
version: string;
|
version: string;
|
||||||
startTimecode: null;
|
startTimecode: null;
|
||||||
endTimecode: null;
|
endTimecode: null;
|
||||||
versionId: string;
|
versionId: string;
|
||||||
mediaType: string;
|
mediaType: string;
|
||||||
status: string;
|
status: string;
|
||||||
languages: LanguageClass[];
|
languages: LanguageClass[];
|
||||||
territories: any[];
|
territories: any[];
|
||||||
devices: any[];
|
devices: any[];
|
||||||
keyType: string;
|
keyType: string;
|
||||||
purpose: null;
|
purpose: null;
|
||||||
externalItemId: null | string;
|
externalItemId: null | string;
|
||||||
proxyId: null;
|
proxyId: null;
|
||||||
externalDbId: null;
|
externalDbId: null;
|
||||||
mediaChildren: MediaChild[];
|
mediaChildren: MediaChild[];
|
||||||
isDefault: boolean;
|
isDefault: boolean;
|
||||||
parent: MediaChildParent;
|
parent: MediaChildParent;
|
||||||
filePath: null | string;
|
filePath: null | string;
|
||||||
mediaInfo: Next;
|
mediaInfo: Next;
|
||||||
type: string;
|
type: string;
|
||||||
approved: boolean;
|
approved: boolean;
|
||||||
mediaKey: string;
|
mediaKey: string;
|
||||||
itemFields: any[];
|
itemFields: any[];
|
||||||
source: Source;
|
source: Source;
|
||||||
fieldData: Next;
|
fieldData: Next;
|
||||||
sourceId: null | string;
|
sourceId: null | string;
|
||||||
timecodeOverride: null;
|
timecodeOverride: null;
|
||||||
seriesTitle: string;
|
seriesTitle: string;
|
||||||
episodeTitle: string;
|
episodeTitle: string;
|
||||||
genre: any[];
|
genre: any[];
|
||||||
txDate: string;
|
txDate: string;
|
||||||
description: string;
|
description: string;
|
||||||
synopsis: string;
|
synopsis: string;
|
||||||
resolution: null;
|
resolution: null;
|
||||||
restrictedAccess: boolean;
|
restrictedAccess: boolean;
|
||||||
createdById: string;
|
createdById: string;
|
||||||
userIdsWithAccess: any[];
|
userIdsWithAccess: any[];
|
||||||
runtime?: number;
|
runtime?: number;
|
||||||
language?: TitleElement;
|
language?: TitleElement;
|
||||||
purchased: boolean;
|
purchased: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Avail {
|
export interface Avail {
|
||||||
id: number;
|
id: number;
|
||||||
description: string;
|
description: string;
|
||||||
endDate: string;
|
endDate: string;
|
||||||
startDate: string;
|
startDate: string;
|
||||||
ids: AvailIDS;
|
ids: AvailIDS;
|
||||||
originalAirDate: null;
|
originalAirDate: null;
|
||||||
physicalReleaseDate: null;
|
physicalReleaseDate: null;
|
||||||
preorderDate: null;
|
preorderDate: null;
|
||||||
language: TitleElement;
|
language: TitleElement;
|
||||||
territory: string;
|
territory: string;
|
||||||
territoryCode: string;
|
territoryCode: string;
|
||||||
license: string;
|
license: string;
|
||||||
parentAvail: null;
|
parentAvail: null;
|
||||||
item: number;
|
item: number;
|
||||||
version: string;
|
version: string;
|
||||||
applyToLevel: null;
|
applyToLevel: null;
|
||||||
availLevel: string;
|
availLevel: string;
|
||||||
availDisplayCode: string;
|
availDisplayCode: string;
|
||||||
availStatus: string;
|
availStatus: string;
|
||||||
bundleOnly: boolean;
|
bundleOnly: boolean;
|
||||||
contentOwnerOrganization: string;
|
contentOwnerOrganization: string;
|
||||||
currency: null;
|
currency: null;
|
||||||
price: null;
|
price: null;
|
||||||
purchase: string;
|
purchase: string;
|
||||||
priceValue: string;
|
priceValue: string;
|
||||||
resolutionFormat: null;
|
resolutionFormat: null;
|
||||||
runtimeMilliseconds: null;
|
runtimeMilliseconds: null;
|
||||||
seasonOrEpisodeNumber: null;
|
seasonOrEpisodeNumber: null;
|
||||||
tmsid: null;
|
tmsid: null;
|
||||||
deviceList: string;
|
deviceList: string;
|
||||||
tvodSku: null;
|
tvodSku: null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AvailIDS {
|
export interface AvailIDS {
|
||||||
externalSeasonId: string;
|
externalSeasonId: string;
|
||||||
externalAsianId: null;
|
externalAsianId: null;
|
||||||
externalShowId: string;
|
externalShowId: string;
|
||||||
externalEpisodeId: string;
|
externalEpisodeId: string;
|
||||||
externalEnglishId: string;
|
externalEnglishId: string;
|
||||||
externalAlphaId: string;
|
externalAlphaId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Next = Record<string, unknown>
|
export type Next = Record<string, unknown>
|
||||||
|
|
||||||
export interface LanguageClass {
|
export interface LanguageClass {
|
||||||
code: string;
|
code: string;
|
||||||
id: number;
|
id: number;
|
||||||
title: TitleElement;
|
title: TitleElement;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MediaChild {
|
export interface MediaChild {
|
||||||
id: number;
|
id: number;
|
||||||
title: string;
|
title: string;
|
||||||
experienceType: string;
|
experienceType: string;
|
||||||
created: string;
|
created: string;
|
||||||
createdBy: string;
|
createdBy: string;
|
||||||
itemFieldData: Next;
|
itemFieldData: Next;
|
||||||
keyPath: null;
|
keyPath: null;
|
||||||
filename: string;
|
filename: string;
|
||||||
complianceStatus: null;
|
complianceStatus: null;
|
||||||
events: any[];
|
events: any[];
|
||||||
clients: string[];
|
clients: string[];
|
||||||
qcStatus: null;
|
qcStatus: null;
|
||||||
qcStatusDate: null;
|
qcStatusDate: null;
|
||||||
image: string;
|
image: string;
|
||||||
ext: string;
|
ext: string;
|
||||||
avails: any[];
|
avails: any[];
|
||||||
version: string;
|
version: string;
|
||||||
startTimecode: null;
|
startTimecode: null;
|
||||||
endTimecode: null;
|
endTimecode: null;
|
||||||
versionId: string;
|
versionId: string;
|
||||||
mediaType: string;
|
mediaType: string;
|
||||||
status: string;
|
status: string;
|
||||||
languages: LanguageClass[];
|
languages: LanguageClass[];
|
||||||
territories: any[];
|
territories: any[];
|
||||||
devices: any[];
|
devices: any[];
|
||||||
keyType: string;
|
keyType: string;
|
||||||
purpose: null;
|
purpose: null;
|
||||||
externalItemId: string;
|
externalItemId: string;
|
||||||
proxyId: null;
|
proxyId: null;
|
||||||
externalDbId: null;
|
externalDbId: null;
|
||||||
mediaChildren: any[];
|
mediaChildren: any[];
|
||||||
isDefault: boolean;
|
isDefault: boolean;
|
||||||
parent: MediaChildParent;
|
parent: MediaChildParent;
|
||||||
filePath: string;
|
filePath: string;
|
||||||
mediaInfo: MediaInfo;
|
mediaInfo: MediaInfo;
|
||||||
type: string;
|
type: string;
|
||||||
approved: boolean;
|
approved: boolean;
|
||||||
mediaKey: null;
|
mediaKey: null;
|
||||||
itemFields: any[];
|
itemFields: any[];
|
||||||
source: Source;
|
source: Source;
|
||||||
fieldData: Next;
|
fieldData: Next;
|
||||||
sourceId: null;
|
sourceId: null;
|
||||||
timecodeOverride: null;
|
timecodeOverride: null;
|
||||||
seriesTitle: string;
|
seriesTitle: string;
|
||||||
episodeTitle: string;
|
episodeTitle: string;
|
||||||
genre: any[];
|
genre: any[];
|
||||||
txDate: string;
|
txDate: string;
|
||||||
description: string;
|
description: string;
|
||||||
synopsis: string;
|
synopsis: string;
|
||||||
resolution: null | string;
|
resolution: null | string;
|
||||||
restrictedAccess: boolean;
|
restrictedAccess: boolean;
|
||||||
createdById: string;
|
createdById: string;
|
||||||
userIdsWithAccess: any[];
|
userIdsWithAccess: any[];
|
||||||
language: TitleElement;
|
language: TitleElement;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MediaInfo {
|
export interface MediaInfo {
|
||||||
imageAspectRatio: null | string;
|
imageAspectRatio: null | string;
|
||||||
format: string;
|
format: string;
|
||||||
scanMode: null | string;
|
scanMode: null | string;
|
||||||
burnedInSubtitleLanguage: string;
|
burnedInSubtitleLanguage: string;
|
||||||
screenAspectRatio: null | string;
|
screenAspectRatio: null | string;
|
||||||
subtitleFormat: null | string;
|
subtitleFormat: null | string;
|
||||||
subtitleContent: null | string;
|
subtitleContent: null | string;
|
||||||
frameHeight: number | null;
|
frameHeight: number | null;
|
||||||
frameWidth: number | null;
|
frameWidth: number | null;
|
||||||
video: Video;
|
video: Video;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Video {
|
export interface Video {
|
||||||
codecId: null | string;
|
codecId: null | string;
|
||||||
container: null | string;
|
container: null | string;
|
||||||
encodingRate: number | null;
|
encodingRate: number | null;
|
||||||
frameRate: null | string;
|
frameRate: null | string;
|
||||||
height: number | null;
|
height: number | null;
|
||||||
width: number | null;
|
width: number | null;
|
||||||
duration: number | null;
|
duration: number | null;
|
||||||
bitRate: number | null;
|
bitRate: number | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MediaChildParent {
|
export interface MediaChildParent {
|
||||||
title: string;
|
title: string;
|
||||||
type: string;
|
type: string;
|
||||||
catalogParent: CatalogParent;
|
catalogParent: CatalogParent;
|
||||||
slug: string;
|
slug: string;
|
||||||
grandparentId: number;
|
grandparentId: number;
|
||||||
id: number;
|
id: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CatalogParent {
|
export interface CatalogParent {
|
||||||
id: number;
|
id: number;
|
||||||
title: string;
|
title: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum Source {
|
export enum Source {
|
||||||
Dbb = 'dbb',
|
Dbb = 'dbb',
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MetaItems {
|
export interface MetaItems {
|
||||||
items: Items;
|
items: Items;
|
||||||
filters: Filters;
|
filters: Filters;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Filters {
|
export interface Filters {
|
||||||
territory: any[];
|
territory: any[];
|
||||||
language: any[];
|
language: any[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Items {
|
export interface Items {
|
||||||
'release-date': AnimationProductionStudio;
|
'release-date': AnimationProductionStudio;
|
||||||
rating: AnimationProductionStudio;
|
rating: AnimationProductionStudio;
|
||||||
synopsis: AnimationProductionStudio;
|
synopsis: AnimationProductionStudio;
|
||||||
'animation-production-studio': AnimationProductionStudio;
|
'animation-production-studio': AnimationProductionStudio;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AnimationProductionStudio {
|
export interface AnimationProductionStudio {
|
||||||
items: AnimationProductionStudioItem[];
|
items: AnimationProductionStudioItem[];
|
||||||
label: string;
|
label: string;
|
||||||
id: number;
|
id: number;
|
||||||
slug: string;
|
slug: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AnimationProductionStudioItem {
|
export interface AnimationProductionStudioItem {
|
||||||
id: number;
|
id: number;
|
||||||
metaType: MetaType;
|
metaType: MetaType;
|
||||||
metaTypeId: string;
|
metaTypeId: string;
|
||||||
client: null;
|
client: null;
|
||||||
languages: TitleElement;
|
languages: TitleElement;
|
||||||
territories: string;
|
territories: string;
|
||||||
devices: string;
|
devices: string;
|
||||||
isDefault: boolean;
|
isDefault: boolean;
|
||||||
value: Value[];
|
value: Value[];
|
||||||
approved: boolean;
|
approved: boolean;
|
||||||
version: null;
|
version: null;
|
||||||
source: Source;
|
source: Source;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface EpisodeDataParent {
|
export interface EpisodeDataParent {
|
||||||
seasonId: number;
|
seasonId: number;
|
||||||
seasonNumber: string;
|
seasonNumber: string;
|
||||||
title: string;
|
title: string;
|
||||||
titleSlug: string;
|
titleSlug: string;
|
||||||
titleType: string;
|
titleType: string;
|
||||||
titleId: number;
|
titleId: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PreviousSeasonEpisode {
|
export interface PreviousSeasonEpisode {
|
||||||
seasonTitle?: string;
|
seasonTitle?: string;
|
||||||
mediaCategory: Type;
|
mediaCategory: Type;
|
||||||
thumb: string;
|
thumb: string;
|
||||||
title: string;
|
title: string;
|
||||||
image: string;
|
image: string;
|
||||||
number: string;
|
number: string;
|
||||||
id: number;
|
id: number;
|
||||||
version: string[];
|
version: string[];
|
||||||
order: number;
|
order: number;
|
||||||
slug: string;
|
slug: string;
|
||||||
season?: number;
|
season?: number;
|
||||||
languages?: TitleElement[];
|
languages?: TitleElement[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum Type {
|
export enum Type {
|
||||||
Episode = 'episode',
|
Episode = 'episode',
|
||||||
Ova = 'ova',
|
Ova = 'ova',
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Quality {
|
export interface Quality {
|
||||||
quality: string;
|
quality: string;
|
||||||
height: number;
|
height: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UserRating {
|
export interface UserRating {
|
||||||
overall: number;
|
overall: number;
|
||||||
ja: number;
|
ja: number;
|
||||||
eng: number;
|
eng: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface VersionClass {
|
export interface VersionClass {
|
||||||
compliance_approved: boolean;
|
compliance_approved: boolean;
|
||||||
title: string;
|
title: string;
|
||||||
version_id: string;
|
version_id: string;
|
||||||
is_default: boolean;
|
is_default: boolean;
|
||||||
runtime: string;
|
runtime: string;
|
||||||
external_id: string;
|
external_id: string;
|
||||||
id: number;
|
id: number;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
15
@types/funiTypes.d.ts
vendored
15
@types/funiTypes.d.ts
vendored
|
|
@ -1,15 +0,0 @@
|
||||||
import { LanguageItem } from '../modules/module.langsData';
|
|
||||||
|
|
||||||
export type FunimationMediaDownload = {
|
|
||||||
id: string,
|
|
||||||
title: string,
|
|
||||||
showTitle: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export type Subtitle = {
|
|
||||||
url: string,
|
|
||||||
lang: LanguageItem,
|
|
||||||
ext: string,
|
|
||||||
out?: string,
|
|
||||||
closedCaption?: boolean
|
|
||||||
}
|
|
||||||
212
@types/github.d.ts
vendored
212
@types/github.d.ts
vendored
|
|
@ -1,106 +1,106 @@
|
||||||
export type GithubTag = {
|
export type GithubTag = {
|
||||||
name: string,
|
name: string,
|
||||||
zipball_url: string,
|
zipball_url: string,
|
||||||
tarball_url: string,
|
tarball_url: string,
|
||||||
commit: {
|
commit: {
|
||||||
sha: string,
|
sha: string,
|
||||||
url: string
|
url: string
|
||||||
},
|
},
|
||||||
node_id: string
|
node_id: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TagCompare {
|
export interface TagCompare {
|
||||||
url: string;
|
url: string;
|
||||||
html_url: string;
|
html_url: string;
|
||||||
permalink_url: string;
|
permalink_url: string;
|
||||||
diff_url: string;
|
diff_url: string;
|
||||||
patch_url: string;
|
patch_url: string;
|
||||||
base_commit: BaseCommitClass;
|
base_commit: BaseCommitClass;
|
||||||
merge_base_commit: BaseCommitClass;
|
merge_base_commit: BaseCommitClass;
|
||||||
status: string;
|
status: string;
|
||||||
ahead_by: number;
|
ahead_by: number;
|
||||||
behind_by: number;
|
behind_by: number;
|
||||||
total_commits: number;
|
total_commits: number;
|
||||||
commits: BaseCommitClass[];
|
commits: BaseCommitClass[];
|
||||||
files: File[];
|
files: File[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface BaseCommitClass {
|
export interface BaseCommitClass {
|
||||||
sha: string;
|
sha: string;
|
||||||
node_id: string;
|
node_id: string;
|
||||||
commit: BaseCommitCommit;
|
commit: BaseCommitCommit;
|
||||||
url: string;
|
url: string;
|
||||||
html_url: string;
|
html_url: string;
|
||||||
comments_url: string;
|
comments_url: string;
|
||||||
author: BaseCommitAuthor;
|
author: BaseCommitAuthor;
|
||||||
committer: BaseCommitAuthor;
|
committer: BaseCommitAuthor;
|
||||||
parents: Parent[];
|
parents: Parent[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface BaseCommitAuthor {
|
export interface BaseCommitAuthor {
|
||||||
login: string;
|
login: string;
|
||||||
id: number;
|
id: number;
|
||||||
node_id: string;
|
node_id: string;
|
||||||
avatar_url: string;
|
avatar_url: string;
|
||||||
gravatar_id: string;
|
gravatar_id: string;
|
||||||
url: string;
|
url: string;
|
||||||
html_url: string;
|
html_url: string;
|
||||||
followers_url: string;
|
followers_url: string;
|
||||||
following_url: string;
|
following_url: string;
|
||||||
gists_url: string;
|
gists_url: string;
|
||||||
starred_url: string;
|
starred_url: string;
|
||||||
subscriptions_url: string;
|
subscriptions_url: string;
|
||||||
organizations_url: string;
|
organizations_url: string;
|
||||||
repos_url: string;
|
repos_url: string;
|
||||||
events_url: string;
|
events_url: string;
|
||||||
received_events_url: string;
|
received_events_url: string;
|
||||||
type: string;
|
type: string;
|
||||||
site_admin: boolean;
|
site_admin: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface BaseCommitCommit {
|
export interface BaseCommitCommit {
|
||||||
author: PurpleAuthor;
|
author: PurpleAuthor;
|
||||||
committer: PurpleAuthor;
|
committer: PurpleAuthor;
|
||||||
message: string;
|
message: string;
|
||||||
tree: Tree;
|
tree: Tree;
|
||||||
url: string;
|
url: string;
|
||||||
comment_count: number;
|
comment_count: number;
|
||||||
verification: Verification;
|
verification: Verification;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PurpleAuthor {
|
export interface PurpleAuthor {
|
||||||
name: string;
|
name: string;
|
||||||
email: string;
|
email: string;
|
||||||
date: string;
|
date: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Tree {
|
export interface Tree {
|
||||||
sha: string;
|
sha: string;
|
||||||
url: string;
|
url: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Verification {
|
export interface Verification {
|
||||||
verified: boolean;
|
verified: boolean;
|
||||||
reason: string;
|
reason: string;
|
||||||
signature: string;
|
signature: string;
|
||||||
payload: string;
|
payload: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Parent {
|
export interface Parent {
|
||||||
sha: string;
|
sha: string;
|
||||||
url: string;
|
url: string;
|
||||||
html_url: string;
|
html_url: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface File {
|
export interface File {
|
||||||
sha: string;
|
sha: string;
|
||||||
filename: string;
|
filename: string;
|
||||||
status: string;
|
status: string;
|
||||||
additions: number;
|
additions: number;
|
||||||
deletions: number;
|
deletions: number;
|
||||||
changes: number;
|
changes: number;
|
||||||
blob_url: string;
|
blob_url: string;
|
||||||
raw_url: string;
|
raw_url: string;
|
||||||
contents_url: string;
|
contents_url: string;
|
||||||
patch: string;
|
patch: string;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
73
@types/hidiveDashboard.d.ts
vendored
Normal file
73
@types/hidiveDashboard.d.ts
vendored
Normal file
|
|
@ -0,0 +1,73 @@
|
||||||
|
export interface HidiveDashboard {
|
||||||
|
Code: number;
|
||||||
|
Status: string;
|
||||||
|
Message: null;
|
||||||
|
Messages: Messages;
|
||||||
|
Data: Data;
|
||||||
|
Timestamp: string;
|
||||||
|
IPAddress: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Data {
|
||||||
|
TitleRows: TitleRow[];
|
||||||
|
LoadTime: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TitleRow {
|
||||||
|
Name: string;
|
||||||
|
Titles: Title[];
|
||||||
|
LoadTime: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Title {
|
||||||
|
Id: number;
|
||||||
|
Name: string;
|
||||||
|
ShortSynopsis: string;
|
||||||
|
MediumSynopsis: string;
|
||||||
|
LongSynopsis: string;
|
||||||
|
KeyArtUrl: string;
|
||||||
|
MasterArtUrl: string;
|
||||||
|
Rating: null | string;
|
||||||
|
OverallRating: number;
|
||||||
|
RatingCount: number;
|
||||||
|
MALScore: null;
|
||||||
|
UserRating: number;
|
||||||
|
RunTime: number | null;
|
||||||
|
ShowInfoTitle: string;
|
||||||
|
FirstPremiereDate: Date;
|
||||||
|
EpisodeCount: number;
|
||||||
|
SeasonName: string;
|
||||||
|
RokuHDArtUrl: string;
|
||||||
|
RokuSDArtUrl: string;
|
||||||
|
IsRateable: boolean;
|
||||||
|
InQueue: boolean;
|
||||||
|
IsFavorite: boolean;
|
||||||
|
IsContinueWatching: boolean;
|
||||||
|
ContinueWatching: ContinueWatching;
|
||||||
|
Episodes: any[];
|
||||||
|
LoadTime: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ContinueWatching {
|
||||||
|
Id: string;
|
||||||
|
ProfileId: number;
|
||||||
|
EpisodeId: number;
|
||||||
|
Status: Status | null;
|
||||||
|
CurrentTime: number;
|
||||||
|
UserId: number;
|
||||||
|
TitleId: number;
|
||||||
|
SeasonId: number;
|
||||||
|
VideoId: number;
|
||||||
|
TotalSeconds: number;
|
||||||
|
CreatedDT: Date;
|
||||||
|
ModifiedDT: Date | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum Status {
|
||||||
|
Paused = 'Paused',
|
||||||
|
Playing = 'Playing',
|
||||||
|
Watching = 'Watching',
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Messages {
|
||||||
|
}
|
||||||
84
@types/hidiveEpisodeList.d.ts
vendored
Normal file
84
@types/hidiveEpisodeList.d.ts
vendored
Normal file
|
|
@ -0,0 +1,84 @@
|
||||||
|
export interface HidiveEpisodeList {
|
||||||
|
Code: number;
|
||||||
|
Status: string;
|
||||||
|
Message: null;
|
||||||
|
Messages: Record<unknown, unknown>;
|
||||||
|
Data: Data;
|
||||||
|
Timestamp: string;
|
||||||
|
IPAddress: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Data {
|
||||||
|
Title: HidiveTitle;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface HidiveTitle {
|
||||||
|
Id: number;
|
||||||
|
Name: string;
|
||||||
|
ShortSynopsis: string;
|
||||||
|
MediumSynopsis: string;
|
||||||
|
LongSynopsis: string;
|
||||||
|
KeyArtUrl: string;
|
||||||
|
MasterArtUrl: string;
|
||||||
|
Rating: string;
|
||||||
|
OverallRating: number;
|
||||||
|
RatingCount: number;
|
||||||
|
MALScore: null;
|
||||||
|
UserRating: number;
|
||||||
|
RunTime: number;
|
||||||
|
ShowInfoTitle: string;
|
||||||
|
FirstPremiereDate: Date;
|
||||||
|
EpisodeCount: number;
|
||||||
|
SeasonName: string;
|
||||||
|
RokuHDArtUrl: string;
|
||||||
|
RokuSDArtUrl: string;
|
||||||
|
IsRateable: boolean;
|
||||||
|
InQueue: boolean;
|
||||||
|
IsFavorite: boolean;
|
||||||
|
IsContinueWatching: boolean;
|
||||||
|
ContinueWatching: ContinueWatching;
|
||||||
|
Episodes: HidiveEpisode[];
|
||||||
|
LoadTime: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ContinueWatching {
|
||||||
|
Id: string;
|
||||||
|
ProfileId: number;
|
||||||
|
EpisodeId: number;
|
||||||
|
Status: string;
|
||||||
|
CurrentTime: number;
|
||||||
|
UserId: number;
|
||||||
|
TitleId: number;
|
||||||
|
SeasonId: number;
|
||||||
|
VideoId: number;
|
||||||
|
TotalSeconds: number;
|
||||||
|
CreatedDT: Date;
|
||||||
|
ModifiedDT: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface HidiveEpisode {
|
||||||
|
Id: number;
|
||||||
|
Number: number;
|
||||||
|
Name: string;
|
||||||
|
Summary: string;
|
||||||
|
HIDIVEPremiereDate: Date;
|
||||||
|
ScreenShotSmallUrl: string;
|
||||||
|
ScreenShotCompressedUrl: string;
|
||||||
|
SeasonNumber: number;
|
||||||
|
TitleId: number;
|
||||||
|
SeasonNumberValue: number;
|
||||||
|
EpisodeNumberValue: number;
|
||||||
|
VideoKey: string;
|
||||||
|
DisplayNameLong: string;
|
||||||
|
PercentProgress: number;
|
||||||
|
LoadTime: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface HidiveEpisodeExtra extends HidiveEpisode {
|
||||||
|
titleId: number;
|
||||||
|
epKey: string;
|
||||||
|
nameLong: string;
|
||||||
|
seriesTitle: string;
|
||||||
|
seriesId?: number;
|
||||||
|
isSelected: boolean;
|
||||||
|
}
|
||||||
47
@types/hidiveSearch.d.ts
vendored
Normal file
47
@types/hidiveSearch.d.ts
vendored
Normal file
|
|
@ -0,0 +1,47 @@
|
||||||
|
export interface HidiveSearch {
|
||||||
|
Code: number;
|
||||||
|
Status: string;
|
||||||
|
Message: null;
|
||||||
|
Messages: Record<unknown, unknown>;
|
||||||
|
Data: HidiveSearchData;
|
||||||
|
Timestamp: string;
|
||||||
|
IPAddress: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface HidiveSearchData {
|
||||||
|
Query: string;
|
||||||
|
Slug: string;
|
||||||
|
TitleResults: HidiveSearchItem[];
|
||||||
|
SearchId: number;
|
||||||
|
IsSearchPinned: boolean;
|
||||||
|
IsPinnedSearchAvailable: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface HidiveSearchItem {
|
||||||
|
Id: number;
|
||||||
|
Name: string;
|
||||||
|
ShortSynopsis: string;
|
||||||
|
MediumSynopsis: string;
|
||||||
|
LongSynopsis: string;
|
||||||
|
KeyArtUrl: string;
|
||||||
|
MasterArtUrl: string;
|
||||||
|
Rating: string;
|
||||||
|
OverallRating: number;
|
||||||
|
RatingCount: number;
|
||||||
|
MALScore: null;
|
||||||
|
UserRating: number;
|
||||||
|
RunTime: number | null;
|
||||||
|
ShowInfoTitle: string;
|
||||||
|
FirstPremiereDate: Date;
|
||||||
|
EpisodeCount: number;
|
||||||
|
SeasonName: string;
|
||||||
|
RokuHDArtUrl: string;
|
||||||
|
RokuSDArtUrl: string;
|
||||||
|
IsRateable: boolean;
|
||||||
|
InQueue: boolean;
|
||||||
|
IsFavorite: boolean;
|
||||||
|
IsContinueWatching: boolean;
|
||||||
|
ContinueWatching: null;
|
||||||
|
Episodes: any[];
|
||||||
|
LoadTime: number;
|
||||||
|
}
|
||||||
61
@types/hidiveTypes.d.ts
vendored
Normal file
61
@types/hidiveTypes.d.ts
vendored
Normal file
|
|
@ -0,0 +1,61 @@
|
||||||
|
export interface HidiveVideoList {
|
||||||
|
Code: number;
|
||||||
|
Status: string;
|
||||||
|
Message: null;
|
||||||
|
Messages: Record<unknown, unknown>;
|
||||||
|
Data: HidiveVideo;
|
||||||
|
Timestamp: string;
|
||||||
|
IPAddress: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface HidiveVideo {
|
||||||
|
ShowAds: boolean;
|
||||||
|
CaptionCssUrl: string;
|
||||||
|
FontSize: number;
|
||||||
|
FontScale: number;
|
||||||
|
CaptionLanguages: string[];
|
||||||
|
CaptionLanguage: string;
|
||||||
|
CaptionVttUrls: Record<string, string>;
|
||||||
|
VideoLanguages: string[];
|
||||||
|
VideoLanguage: string;
|
||||||
|
VideoUrls: Record<string, HidiveStreamList>;
|
||||||
|
FontColorName: string;
|
||||||
|
AutoPlayNextEpisode: boolean;
|
||||||
|
MaxStreams: number;
|
||||||
|
CurrentTime: number;
|
||||||
|
FontColorCode: string;
|
||||||
|
RunTime: number;
|
||||||
|
AdUrl: null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface HidiveStreamList {
|
||||||
|
hls: string[];
|
||||||
|
drm: string[];
|
||||||
|
drmEnabled: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface HidiveStreamInfo extends HidiveStreamList {
|
||||||
|
language?: string;
|
||||||
|
episodeTitle?: string;
|
||||||
|
seriesTitle?: string;
|
||||||
|
season?: number;
|
||||||
|
episodeNumber?: number;
|
||||||
|
uncut?: boolean;
|
||||||
|
image?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface HidiveSubtitleInfo {
|
||||||
|
language: string;
|
||||||
|
cc: boolean;
|
||||||
|
url: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type DownloadedMedia = {
|
||||||
|
type: 'Video',
|
||||||
|
lang: LanguageItem,
|
||||||
|
path: string,
|
||||||
|
uncut: boolean
|
||||||
|
} | ({
|
||||||
|
type: 'Subtitle',
|
||||||
|
cc: boolean
|
||||||
|
} & sxItem )
|
||||||
27
@types/hls-download.d.ts
vendored
27
@types/hls-download.d.ts
vendored
|
|
@ -1,27 +0,0 @@
|
||||||
declare module 'hls-download' {
|
|
||||||
export default class hlsDownload {
|
|
||||||
constructor(options: {
|
|
||||||
m3u8json: {
|
|
||||||
segments: Record<string, unknown>[],
|
|
||||||
mediaSequence?: number,
|
|
||||||
},
|
|
||||||
output?: string,
|
|
||||||
threads?: number,
|
|
||||||
retries?: number,
|
|
||||||
offset?: number,
|
|
||||||
baseurl?: string,
|
|
||||||
proxy?: string,
|
|
||||||
skipInit?: boolean,
|
|
||||||
timeout?: number,
|
|
||||||
fsRetryTime?: number
|
|
||||||
})
|
|
||||||
async download() : Promise<{
|
|
||||||
ok: boolean,
|
|
||||||
parts: {
|
|
||||||
first: number,
|
|
||||||
total: number,
|
|
||||||
compleated: number
|
|
||||||
}
|
|
||||||
}>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
16
@types/iso639.d.ts
vendored
16
@types/iso639.d.ts
vendored
|
|
@ -1,9 +1,9 @@
|
||||||
declare module 'iso-639' {
|
declare module 'iso-639' {
|
||||||
export type iso639Type = {
|
export type iso639Type = {
|
||||||
[key: string]: {
|
[key: string]: {
|
||||||
'639-1'?: string,
|
'639-1'?: string,
|
||||||
'639-2'?: string
|
'639-2'?: string
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
export const iso_639_2: iso639Type;
|
export const iso_639_2: iso639Type;
|
||||||
}
|
}
|
||||||
336
@types/items.d.ts
vendored
336
@types/items.d.ts
vendored
|
|
@ -1,169 +1,169 @@
|
||||||
export interface Item {
|
export interface Item {
|
||||||
// Added later
|
// Added later
|
||||||
id: string,
|
id: string,
|
||||||
id_split: (number|string)[]
|
id_split: (number|string)[]
|
||||||
// Added from the start
|
// Added from the start
|
||||||
mostRecentSvodJpnUs: MostRecentSvodJpnUs;
|
mostRecentSvodJpnUs: MostRecentSvodJpnUs;
|
||||||
synopsis: string;
|
synopsis: string;
|
||||||
mediaCategory: ContentType;
|
mediaCategory: ContentType;
|
||||||
mostRecentSvodUsEndTimestamp: number;
|
mostRecentSvodUsEndTimestamp: number;
|
||||||
quality: QualityClass;
|
quality: QualityClass;
|
||||||
genres: Genre[];
|
genres: Genre[];
|
||||||
titleImages: TitleImages;
|
titleImages: TitleImages;
|
||||||
engAllTerritoryAvail: EngAllTerritoryAvail;
|
engAllTerritoryAvail: EngAllTerritoryAvail;
|
||||||
thumb: string;
|
thumb: string;
|
||||||
mostRecentSvodJpnAllTerrStartTimestamp: number;
|
mostRecentSvodJpnAllTerrStartTimestamp: number;
|
||||||
title: string;
|
title: string;
|
||||||
starRating: number;
|
starRating: number;
|
||||||
primaryAvail: PrimaryAvail;
|
primaryAvail: PrimaryAvail;
|
||||||
access: Access[];
|
access: Access[];
|
||||||
version: Version[];
|
version: Version[];
|
||||||
mostRecentSvodJpnAllTerrEndTimestamp: number;
|
mostRecentSvodJpnAllTerrEndTimestamp: number;
|
||||||
itemId: number;
|
itemId: number;
|
||||||
versionAudio: VersionAudio;
|
versionAudio: VersionAudio;
|
||||||
contentType: ContentType;
|
contentType: ContentType;
|
||||||
mostRecentSvodUsStartTimestamp: number;
|
mostRecentSvodUsStartTimestamp: number;
|
||||||
poster: string;
|
poster: string;
|
||||||
mostRecentSvodEngAllTerrEndTimestamp: number;
|
mostRecentSvodEngAllTerrEndTimestamp: number;
|
||||||
mostRecentSvodJpnUsStartTimestamp: number;
|
mostRecentSvodJpnUsStartTimestamp: number;
|
||||||
mostRecentSvodJpnUsEndTimestamp: number;
|
mostRecentSvodJpnUsEndTimestamp: number;
|
||||||
mostRecentSvodStartTimestamp: number;
|
mostRecentSvodStartTimestamp: number;
|
||||||
mostRecentSvod: MostRecent;
|
mostRecentSvod: MostRecent;
|
||||||
altAvail: AltAvail;
|
altAvail: AltAvail;
|
||||||
ids: IDs;
|
ids: IDs;
|
||||||
mostRecentSvodUs: MostRecent;
|
mostRecentSvodUs: MostRecent;
|
||||||
item: Item;
|
item: Item;
|
||||||
mostRecentSvodEngAllTerrStartTimestamp: number;
|
mostRecentSvodEngAllTerrStartTimestamp: number;
|
||||||
audio: Audio[];
|
audio: string[];
|
||||||
mostRecentAvod: MostRecent;
|
mostRecentAvod: MostRecent;
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum ContentType {
|
export enum ContentType {
|
||||||
Episode = 'episode',
|
Episode = 'episode',
|
||||||
Ova = 'ova',
|
Ova = 'ova',
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IDs {
|
export interface IDs {
|
||||||
externalShowId: ID;
|
externalShowId: ID;
|
||||||
externalSeasonId: ExternalSeasonID;
|
externalSeasonId: ExternalSeasonID;
|
||||||
externalEpisodeId: string;
|
externalEpisodeId: string;
|
||||||
externalAsianId?: string
|
externalAsianId?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Item {
|
export interface Item {
|
||||||
seasonTitle: string;
|
seasonTitle: string;
|
||||||
seasonId: number;
|
seasonId: number;
|
||||||
episodeOrder: number;
|
episodeOrder: number;
|
||||||
episodeSlug: string;
|
episodeSlug: string;
|
||||||
created: Date;
|
created: Date;
|
||||||
titleSlug: string;
|
titleSlug: string;
|
||||||
episodeNum: string;
|
episodeNum: string;
|
||||||
episodeId: number;
|
episodeId: number;
|
||||||
titleId: number;
|
titleId: number;
|
||||||
seasonNum: string;
|
seasonNum: string;
|
||||||
ratings: Array<string[]>;
|
ratings: Array<string[]>;
|
||||||
showImage: string;
|
showImage: string;
|
||||||
titleName: string;
|
titleName: string;
|
||||||
runtime: string;
|
runtime: string;
|
||||||
episodeName: string;
|
episodeName: string;
|
||||||
seasonOrder: number;
|
seasonOrder: number;
|
||||||
titleExternalId: string;
|
titleExternalId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export interface MostRecent {
|
export interface MostRecent {
|
||||||
image?: string;
|
image?: string;
|
||||||
siblingStartTimestamp?: string;
|
siblingStartTimestamp?: string;
|
||||||
devices?: Device[];
|
devices?: Device[];
|
||||||
availId?: number;
|
availId?: number;
|
||||||
distributor?: Distributor;
|
distributor?: Distributor;
|
||||||
quality?: MostRecentAvodQuality;
|
quality?: MostRecentAvodQuality;
|
||||||
endTimestamp?: string;
|
endTimestamp?: string;
|
||||||
mediaCategory?: ContentType;
|
mediaCategory?: ContentType;
|
||||||
isPromo?: boolean;
|
isPromo?: boolean;
|
||||||
siblingType?: Purchase;
|
siblingType?: Purchase;
|
||||||
version?: Version;
|
version?: Version;
|
||||||
territory?: Territory;
|
territory?: Territory;
|
||||||
startDate?: Date;
|
startDate?: Date;
|
||||||
endDate?: Date;
|
endDate?: Date;
|
||||||
versionId?: number;
|
versionId?: number;
|
||||||
tier?: Device | null;
|
tier?: Device | null;
|
||||||
purchase?: Purchase;
|
purchase?: Purchase;
|
||||||
startTimestamp?: string;
|
startTimestamp?: string;
|
||||||
language?: Audio;
|
language?: Audio;
|
||||||
itemTitle?: string;
|
itemTitle?: string;
|
||||||
ids?: MostRecentAvodIDS;
|
ids?: MostRecentAvodIDS;
|
||||||
experience?: number;
|
experience?: number;
|
||||||
siblingEndTimestamp?: string;
|
siblingEndTimestamp?: string;
|
||||||
item?: Item;
|
item?: Item;
|
||||||
subscriptionRequired?: boolean;
|
subscriptionRequired?: boolean;
|
||||||
purchased?: boolean;
|
purchased?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MostRecentAvodIDS {
|
export interface MostRecentAvodIDS {
|
||||||
externalSeasonId: ExternalSeasonID;
|
externalSeasonId: ExternalSeasonID;
|
||||||
externalAsianId: null;
|
externalAsianId: null;
|
||||||
externalShowId: ID;
|
externalShowId: ID;
|
||||||
externalEpisodeId: string;
|
externalEpisodeId: string;
|
||||||
externalEnglishId: string;
|
externalEnglishId: string;
|
||||||
externalAlphaId: string;
|
externalAlphaId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum Purchase {
|
export enum Purchase {
|
||||||
AVOD = 'A-VOD',
|
AVOD = 'A-VOD',
|
||||||
Dfov = 'DFOV',
|
Dfov = 'DFOV',
|
||||||
Est = 'EST',
|
Est = 'EST',
|
||||||
Svod = 'SVOD',
|
Svod = 'SVOD',
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum Version {
|
export enum Version {
|
||||||
Simulcast = 'Simulcast',
|
Simulcast = 'Simulcast',
|
||||||
Uncut = 'Uncut',
|
Uncut = 'Uncut',
|
||||||
}
|
}
|
||||||
|
|
||||||
export type MostRecentSvodJpnUs = Record<string, any>
|
export type MostRecentSvodJpnUs = Record<string, any>
|
||||||
|
|
||||||
export interface QualityClass {
|
export interface QualityClass {
|
||||||
quality: QualityQuality;
|
quality: QualityQuality;
|
||||||
height: number;
|
height: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum QualityQuality {
|
export enum QualityQuality {
|
||||||
HD = 'HD',
|
HD = 'HD',
|
||||||
SD = 'SD',
|
SD = 'SD',
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TitleImages {
|
export interface TitleImages {
|
||||||
showThumbnail: string;
|
showThumbnail: string;
|
||||||
showBackgroundSite: string;
|
showBackgroundSite: string;
|
||||||
showDetailHeaderDesktop: string;
|
showDetailHeaderDesktop: string;
|
||||||
continueWatchingDesktop: string;
|
continueWatchingDesktop: string;
|
||||||
showDetailHeroSite: string;
|
showDetailHeroSite: string;
|
||||||
appleHorizontalBannerShow: string;
|
appleHorizontalBannerShow: string;
|
||||||
backgroundImageXbox_360: string;
|
backgroundImageXbox_360: string;
|
||||||
applePosterCover: string;
|
applePosterCover: string;
|
||||||
showDetailBoxArtTablet: string;
|
showDetailBoxArtTablet: string;
|
||||||
featuredShowBackgroundTablet: string;
|
featuredShowBackgroundTablet: string;
|
||||||
backgroundImageAppletvfiretv: string;
|
backgroundImageAppletvfiretv: string;
|
||||||
newShowDetailHero: string;
|
newShowDetailHero: string;
|
||||||
showDetailHeroDesktop: string;
|
showDetailHeroDesktop: string;
|
||||||
showKeyart: string;
|
showKeyart: string;
|
||||||
continueWatchingMobile: string;
|
continueWatchingMobile: string;
|
||||||
featuredSpotlightShowPhone: string;
|
featuredSpotlightShowPhone: string;
|
||||||
appleHorizontalBannerMovie: string;
|
appleHorizontalBannerMovie: string;
|
||||||
featuredSpotlightShowTablet: string;
|
featuredSpotlightShowTablet: string;
|
||||||
showDetailBoxArtPhone: string;
|
showDetailBoxArtPhone: string;
|
||||||
featuredShowBackgroundPhone: string;
|
featuredShowBackgroundPhone: string;
|
||||||
appleSquareCover: string;
|
appleSquareCover: string;
|
||||||
backgroundVideo: string;
|
backgroundVideo: string;
|
||||||
showMasterKeyArt: string;
|
showMasterKeyArt: string;
|
||||||
newShowDetailHeroPhone: string;
|
newShowDetailHeroPhone: string;
|
||||||
showDetailBoxArtXbox_360: string;
|
showDetailBoxArtXbox_360: string;
|
||||||
showDetailHeaderMobile: string;
|
showDetailHeaderMobile: string;
|
||||||
showLogo: string;
|
showLogo: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface VersionAudio {
|
export interface VersionAudio {
|
||||||
Uncut?: Audio[];
|
Uncut?: Audio[];
|
||||||
Simulcast: Audio[];
|
Simulcast: Audio[];
|
||||||
}
|
}
|
||||||
96
@types/m3u8-parsed.d.ts
vendored
96
@types/m3u8-parsed.d.ts
vendored
|
|
@ -1,49 +1,49 @@
|
||||||
declare module 'm3u8-parsed' {
|
declare module 'm3u8-parsed' {
|
||||||
export type M3U8 = {
|
export type M3U8 = {
|
||||||
allowCache: boolean,
|
allowCache: boolean,
|
||||||
discontinuityStarts: [],
|
discontinuityStarts: [],
|
||||||
segments: {
|
segments: {
|
||||||
duration: number,
|
duration: number,
|
||||||
byterange?: {
|
byterange?: {
|
||||||
length: number,
|
length: number,
|
||||||
offset: number
|
offset: number
|
||||||
},
|
},
|
||||||
uri: string,
|
uri: string,
|
||||||
key: {
|
key: {
|
||||||
method: string,
|
method: string,
|
||||||
uri: string,
|
uri: string,
|
||||||
},
|
},
|
||||||
timeline: number
|
timeline: number
|
||||||
}[],
|
}[],
|
||||||
version: number,
|
version: number,
|
||||||
mediaGroups: {
|
mediaGroups: {
|
||||||
[type: string]: {
|
[type: string]: {
|
||||||
[index: string]: {
|
[index: string]: {
|
||||||
[language: string]: {
|
[language: string]: {
|
||||||
default: boolean,
|
default: boolean,
|
||||||
autoselect: boolean,
|
autoselect: boolean,
|
||||||
language: string,
|
language: string,
|
||||||
uri: string
|
uri: string
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
playlists: {
|
playlists: {
|
||||||
uri: string,
|
uri: string,
|
||||||
timeline: number,
|
timeline: number,
|
||||||
attributes: {
|
attributes: {
|
||||||
'CLOSED-CAPTIONS': string,
|
'CLOSED-CAPTIONS': string,
|
||||||
'AUDIO': string,
|
'AUDIO': string,
|
||||||
'FRAME-RATE': number,
|
'FRAME-RATE': number,
|
||||||
'RESOLUTION': {
|
'RESOLUTION': {
|
||||||
width: number,
|
width: number,
|
||||||
height: number
|
height: number
|
||||||
},
|
},
|
||||||
'CODECS': string,
|
'CODECS': string,
|
||||||
'AVERAGE-BANDWIDTH': string,
|
'AVERAGE-BANDWIDTH': string,
|
||||||
'BANDWIDTH': number
|
'BANDWIDTH': number
|
||||||
}
|
}
|
||||||
}[],
|
}[],
|
||||||
}
|
}
|
||||||
export default function (data: string): M3U8;
|
export default function (data: string): M3U8;
|
||||||
}
|
}
|
||||||
161
@types/messageHandler.d.ts
vendored
Normal file
161
@types/messageHandler.d.ts
vendored
Normal file
|
|
@ -0,0 +1,161 @@
|
||||||
|
import { HLSCallback } from 'hls-download';
|
||||||
|
import type { FunimationSearch } from './funiSearch';
|
||||||
|
import type { AvailableMuxer } from '../modules/module.args';
|
||||||
|
import { LanguageItem } from '../modules/module.langsData';
|
||||||
|
|
||||||
|
export interface MessageHandler {
|
||||||
|
name: string
|
||||||
|
auth: (data: AuthData) => Promise<AuthResponse>;
|
||||||
|
version: () => Promise<string>;
|
||||||
|
checkToken: () => Promise<CheckTokenResponse>;
|
||||||
|
search: (data: SearchData) => Promise<SearchResponse>,
|
||||||
|
availableDubCodes: () => Promise<string[]>,
|
||||||
|
availableSubCodes: () => Promise<string[]>,
|
||||||
|
handleDefault: (name: string) => Promise<any>,
|
||||||
|
resolveItems: (data: ResolveItemsData) => Promise<boolean>,
|
||||||
|
listEpisodes: (id: string) => Promise<EpisodeListResponse>,
|
||||||
|
downloadItem: (data: QueueItem) => void,
|
||||||
|
isDownloading: () => Promise<boolean>,
|
||||||
|
openFolder: (path: FolderTypes) => void,
|
||||||
|
openFile: (data: [FolderTypes, string]) => void,
|
||||||
|
openURL: (data: string) => void;
|
||||||
|
getQueue: () => Promise<QueueItem[]>,
|
||||||
|
removeFromQueue: (index: number) => void,
|
||||||
|
clearQueue: () => void,
|
||||||
|
setDownloadQueue: (data: boolean) => void,
|
||||||
|
getDownloadQueue: () => Promise<boolean>
|
||||||
|
}
|
||||||
|
|
||||||
|
export type FolderTypes = 'content' | 'config';
|
||||||
|
|
||||||
|
export type QueueItem = {
|
||||||
|
title: string,
|
||||||
|
episode: string,
|
||||||
|
fileName: string,
|
||||||
|
dlsubs: string[],
|
||||||
|
parent: {
|
||||||
|
title: string,
|
||||||
|
season: string
|
||||||
|
},
|
||||||
|
q: number,
|
||||||
|
dlVideoOnce: boolean,
|
||||||
|
dubLang: string[],
|
||||||
|
image: string,
|
||||||
|
} & ResolveItemsData
|
||||||
|
|
||||||
|
export type ResolveItemsData = {
|
||||||
|
id: string,
|
||||||
|
dubLang: string[],
|
||||||
|
all: boolean,
|
||||||
|
but: boolean,
|
||||||
|
novids: boolean,
|
||||||
|
noaudio: boolean
|
||||||
|
dlVideoOnce: boolean,
|
||||||
|
e: string,
|
||||||
|
fileName: string,
|
||||||
|
q: number,
|
||||||
|
dlsubs: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SearchResponseItem = {
|
||||||
|
image: string,
|
||||||
|
name: string,
|
||||||
|
desc?: string,
|
||||||
|
id: string,
|
||||||
|
lang?: string[],
|
||||||
|
rating: number
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Episode = {
|
||||||
|
e: string,
|
||||||
|
lang: string[],
|
||||||
|
name: string,
|
||||||
|
season: string,
|
||||||
|
seasonTitle: string,
|
||||||
|
episode: string,
|
||||||
|
id: string,
|
||||||
|
img: string,
|
||||||
|
description: string,
|
||||||
|
time: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SearchResponse = ResponseBase<SearchResponseItem[]>
|
||||||
|
export type EpisodeListResponse = ResponseBase<Episode[]>
|
||||||
|
|
||||||
|
export type FuniEpisodeData = {
|
||||||
|
title: string,
|
||||||
|
episode: string,
|
||||||
|
epsiodeNumber: string,
|
||||||
|
episodeID: string,
|
||||||
|
seasonTitle: string,
|
||||||
|
seasonNumber: string,
|
||||||
|
ids: {
|
||||||
|
episode: string,
|
||||||
|
show: string,
|
||||||
|
season: string
|
||||||
|
},
|
||||||
|
image: string
|
||||||
|
};
|
||||||
|
|
||||||
|
export type AuthData = { username: string, password: string };
|
||||||
|
export type SearchData = { search: string, page?: number, 'search-type'?: string, 'search-locale'?: string };
|
||||||
|
export type FuniGetShowData = { id: number, e?: string, but: boolean, all: boolean };
|
||||||
|
export type FuniGetEpisodeData = { subs: FuniSubsData, fnSlug: FuniEpisodeData, simul?: boolean; dubLang: string[], s: string }
|
||||||
|
export type FuniStreamData = { force?: 'Y'|'y'|'N'|'n'|'C'|'c', callbackMaker?: (data: DownloadInfo) => HLSCallback, q: number, x: number, fileName: string, numbers: number, novids?: boolean,
|
||||||
|
timeout: number, partsize: number, fsRetryTime: number, noaudio?: boolean, mp4: boolean, ass: boolean, fontSize: number, fontName?: string, skipmux?: boolean,
|
||||||
|
forceMuxer: AvailableMuxer | undefined, simul: boolean, skipSubMux: boolean, nocleanup: boolean, override: string[], videoTitle: string,
|
||||||
|
ffmpegOptions: string[], mkvmergeOptions: string[], defaultAudio: LanguageItem, defaultSub: LanguageItem, ccTag: string }
|
||||||
|
export type FuniSubsData = { nosubs?: boolean, sub: boolean, dlsubs: string[], ccTag: string }
|
||||||
|
export type DownloadData = {
|
||||||
|
hslang?: string; id: string, e: string, dubLang: string[], dlsubs: string[], fileName: string, q: number, novids: boolean, noaudio: boolean, dlVideoOnce: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export type AuthResponse = ResponseBase<undefined>;
|
||||||
|
export type FuniSearchReponse = ResponseBase<FunimationSearch>;
|
||||||
|
export type FuniShowResponse = ResponseBase<FuniEpisodeData[]>;
|
||||||
|
export type FuniGetEpisodeResponse = ResponseBase<undefined>;
|
||||||
|
export type CheckTokenResponse = ResponseBase<undefined>;
|
||||||
|
|
||||||
|
|
||||||
|
export type ResponseBase<T> = ({
|
||||||
|
isOk: true,
|
||||||
|
value: T
|
||||||
|
} | {
|
||||||
|
isOk: false,
|
||||||
|
reason: Error
|
||||||
|
});
|
||||||
|
|
||||||
|
export type ProgressData = {
|
||||||
|
total: number,
|
||||||
|
cur: number,
|
||||||
|
percent: number|string,
|
||||||
|
time: number,
|
||||||
|
downloadSpeed: number,
|
||||||
|
bytes: number
|
||||||
|
};
|
||||||
|
|
||||||
|
export type PossibleMessages = keyof ServiceHandler;
|
||||||
|
|
||||||
|
export type DownloadInfo = {
|
||||||
|
image: string,
|
||||||
|
parent: {
|
||||||
|
title: string
|
||||||
|
},
|
||||||
|
title: string,
|
||||||
|
language: LanguageItem,
|
||||||
|
fileName: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ExtendedProgress = {
|
||||||
|
progress: ProgressData,
|
||||||
|
downloadInfo: DownloadInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
export type GuiState = {
|
||||||
|
setup: boolean,
|
||||||
|
services: Record<string, GuiStateService>
|
||||||
|
}
|
||||||
|
|
||||||
|
export type GuiStateService = {
|
||||||
|
queue: QueueItem[]
|
||||||
|
}
|
||||||
101
@types/mpd-parser.d.ts
vendored
Normal file
101
@types/mpd-parser.d.ts
vendored
Normal file
|
|
@ -0,0 +1,101 @@
|
||||||
|
declare module 'mpd-parser' {
|
||||||
|
export type Segment = {
|
||||||
|
uri: string,
|
||||||
|
timeline: number,
|
||||||
|
duration: number,
|
||||||
|
resolvedUri: string,
|
||||||
|
map: {
|
||||||
|
uri: string,
|
||||||
|
resolvedUri: string,
|
||||||
|
byterange?: {
|
||||||
|
length: number,
|
||||||
|
offset: number
|
||||||
|
}
|
||||||
|
},
|
||||||
|
byterange?: {
|
||||||
|
length: number,
|
||||||
|
offset: number
|
||||||
|
},
|
||||||
|
number: number,
|
||||||
|
presentationTime: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Sidx = {
|
||||||
|
uri: string,
|
||||||
|
resolvedUri: string,
|
||||||
|
byterange: {
|
||||||
|
length: number,
|
||||||
|
offset: number
|
||||||
|
},
|
||||||
|
map: {
|
||||||
|
uri: string,
|
||||||
|
resolvedUri: string,
|
||||||
|
byterange: {
|
||||||
|
length: number,
|
||||||
|
offset: number
|
||||||
|
}
|
||||||
|
},
|
||||||
|
duration: number,
|
||||||
|
timeline: number,
|
||||||
|
presentationTime: number,
|
||||||
|
number: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Playlist = {
|
||||||
|
attributes: {
|
||||||
|
NAME: string,
|
||||||
|
BANDWIDTH: number,
|
||||||
|
CODECS: string,
|
||||||
|
'PROGRAM-ID': number,
|
||||||
|
// Following for video only
|
||||||
|
'FRAME-RATE'?: number,
|
||||||
|
AUDIO?: string, // audio stream name
|
||||||
|
SUBTITLES?: string,
|
||||||
|
RESOLUTION?: {
|
||||||
|
width: number,
|
||||||
|
height: number
|
||||||
|
}
|
||||||
|
},
|
||||||
|
uri: string,
|
||||||
|
endList: boolean,
|
||||||
|
timeline: number,
|
||||||
|
resolvedUri: string,
|
||||||
|
targetDuration: number,
|
||||||
|
discontinuitySequence: number,
|
||||||
|
discontinuityStarts: [],
|
||||||
|
timelineStarts: {
|
||||||
|
start: number,
|
||||||
|
timeline: number
|
||||||
|
}[],
|
||||||
|
mediaSequence: number,
|
||||||
|
contentProtection?: {
|
||||||
|
[type: string]: {
|
||||||
|
pssh?: Uint8Array
|
||||||
|
}
|
||||||
|
}
|
||||||
|
segments: Segment[]
|
||||||
|
sidx?: Sidx
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Manifest = {
|
||||||
|
allowCache: boolean,
|
||||||
|
discontinuityStarts: [],
|
||||||
|
segments: [],
|
||||||
|
endList: true,
|
||||||
|
duration: number,
|
||||||
|
playlists: Playlist[],
|
||||||
|
mediaGroups: {
|
||||||
|
AUDIO: {
|
||||||
|
audio: {
|
||||||
|
[name: string]: {
|
||||||
|
language: string,
|
||||||
|
autoselect: boolean,
|
||||||
|
default: boolean,
|
||||||
|
playlists: Playlist[]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export function parse(manifest: string): Manifest
|
||||||
|
}
|
||||||
43
@types/newHidiveEpisode.d.ts
vendored
Normal file
43
@types/newHidiveEpisode.d.ts
vendored
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
export interface NewHidiveEpisode {
|
||||||
|
description: string;
|
||||||
|
duration: number;
|
||||||
|
title: string;
|
||||||
|
categories: string[];
|
||||||
|
contentDownload: ContentDownload;
|
||||||
|
favourite: boolean;
|
||||||
|
subEvents: any[];
|
||||||
|
thumbnailUrl: string;
|
||||||
|
longDescription: string;
|
||||||
|
posterUrl: string;
|
||||||
|
offlinePlaybackLanguages: string[];
|
||||||
|
externalAssetId: string;
|
||||||
|
maxHeight: number;
|
||||||
|
rating: Rating;
|
||||||
|
episodeInformation: EpisodeInformation;
|
||||||
|
id: number;
|
||||||
|
accessLevel: string;
|
||||||
|
playerUrlCallback: string;
|
||||||
|
thumbnailsPreview: string;
|
||||||
|
displayableTags: any[];
|
||||||
|
plugins: any[];
|
||||||
|
watchStatus: string;
|
||||||
|
computedReleases: any[];
|
||||||
|
licences: any[];
|
||||||
|
type: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ContentDownload {
|
||||||
|
permission: string;
|
||||||
|
period: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EpisodeInformation {
|
||||||
|
seasonNumber: number;
|
||||||
|
episodeNumber: number;
|
||||||
|
season: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Rating {
|
||||||
|
rating: string;
|
||||||
|
descriptors: any[];
|
||||||
|
}
|
||||||
33
@types/newHidivePlayback.d.ts
vendored
Normal file
33
@types/newHidivePlayback.d.ts
vendored
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
export interface NewHidivePlayback {
|
||||||
|
watermark: null;
|
||||||
|
skipMarkers: any[];
|
||||||
|
annotations: null;
|
||||||
|
dash: Format[];
|
||||||
|
hls: Format[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Format {
|
||||||
|
subtitles: Subtitle[];
|
||||||
|
url: string;
|
||||||
|
drm: DRM;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DRM {
|
||||||
|
encryptionMode: string;
|
||||||
|
containerType: string;
|
||||||
|
jwtToken: string;
|
||||||
|
url: string;
|
||||||
|
keySystems: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Subtitle {
|
||||||
|
format: Formats;
|
||||||
|
language: string;
|
||||||
|
url: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum Formats {
|
||||||
|
Scc = 'scc',
|
||||||
|
Srt = 'srt',
|
||||||
|
Vtt = 'vtt',
|
||||||
|
}
|
||||||
91
@types/newHidiveSearch.d.ts
vendored
Normal file
91
@types/newHidiveSearch.d.ts
vendored
Normal file
|
|
@ -0,0 +1,91 @@
|
||||||
|
export interface NewHidiveSearch {
|
||||||
|
results: Result[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Result {
|
||||||
|
hits: Hit[];
|
||||||
|
nbHits: number;
|
||||||
|
page: number;
|
||||||
|
nbPages: number;
|
||||||
|
hitsPerPage: number;
|
||||||
|
exhaustiveNbHits: boolean;
|
||||||
|
exhaustiveTypo: boolean;
|
||||||
|
exhaustive: Exhaustive;
|
||||||
|
query: string;
|
||||||
|
params: string;
|
||||||
|
index: string;
|
||||||
|
renderingContent: RenderingContent;
|
||||||
|
processingTimeMS: number;
|
||||||
|
processingTimingsMS: ProcessingTimingsMS;
|
||||||
|
serverTimeMS: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Exhaustive {
|
||||||
|
nbHits: boolean;
|
||||||
|
typo: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Hit {
|
||||||
|
type: string;
|
||||||
|
weight: number;
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
meta: RenderingContent;
|
||||||
|
coverUrl: string;
|
||||||
|
smallCoverUrl: string;
|
||||||
|
seasonsCount: number;
|
||||||
|
tags: string[];
|
||||||
|
localisations: HitLocalisations;
|
||||||
|
ratings: Ratings;
|
||||||
|
objectID: string;
|
||||||
|
_highlightResult: HighlightResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface HighlightResult {
|
||||||
|
name: Description;
|
||||||
|
description: Description;
|
||||||
|
tags: Description[];
|
||||||
|
localisations: HighlightResultLocalisations;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Description {
|
||||||
|
value: string;
|
||||||
|
matchLevel: string;
|
||||||
|
matchedWords: string[];
|
||||||
|
fullyHighlighted?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface HighlightResultLocalisations {
|
||||||
|
en_US: PurpleEnUS;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PurpleEnUS {
|
||||||
|
title: Description;
|
||||||
|
description: Description;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface HitLocalisations {
|
||||||
|
[language: string]: HitLocalization;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface HitLocalization {
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RenderingContent {
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Ratings {
|
||||||
|
US: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ProcessingTimingsMS {
|
||||||
|
_request: Request;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Request {
|
||||||
|
queue: number;
|
||||||
|
roundTrip: number;
|
||||||
|
}
|
||||||
89
@types/newHidiveSeason.d.ts
vendored
Normal file
89
@types/newHidiveSeason.d.ts
vendored
Normal file
|
|
@ -0,0 +1,89 @@
|
||||||
|
export interface NewHidiveSeason {
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
longDescription: string;
|
||||||
|
smallCoverUrl: string;
|
||||||
|
coverUrl: string;
|
||||||
|
titleUrl: string;
|
||||||
|
posterUrl: string;
|
||||||
|
seasonNumber: number;
|
||||||
|
episodeCount: number;
|
||||||
|
displayableTags: any[];
|
||||||
|
rating: Rating;
|
||||||
|
contentRating: Rating;
|
||||||
|
id: number;
|
||||||
|
series: Series;
|
||||||
|
episodes: Episode[];
|
||||||
|
paging: Paging;
|
||||||
|
licences: any[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Rating {
|
||||||
|
rating: string;
|
||||||
|
descriptors: any[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Episode {
|
||||||
|
accessLevel: string;
|
||||||
|
availablePurchases?: any[];
|
||||||
|
licenceIds?: any[];
|
||||||
|
type: string;
|
||||||
|
id: number;
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
thumbnailUrl: string;
|
||||||
|
posterUrl: string;
|
||||||
|
duration: number;
|
||||||
|
favourite: boolean;
|
||||||
|
contentDownload: ContentDownload;
|
||||||
|
offlinePlaybackLanguages: string[];
|
||||||
|
externalAssetId: string;
|
||||||
|
subEvents: any[];
|
||||||
|
maxHeight: number;
|
||||||
|
thumbnailsPreview: string;
|
||||||
|
longDescription: string;
|
||||||
|
episodeInformation: EpisodeInformation;
|
||||||
|
categories: string[];
|
||||||
|
displayableTags: any[];
|
||||||
|
watchStatus: string;
|
||||||
|
computedReleases: any[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ContentDownload {
|
||||||
|
permission: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EpisodeInformation {
|
||||||
|
seasonNumber: number;
|
||||||
|
episodeNumber: number;
|
||||||
|
season: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Paging {
|
||||||
|
moreDataAvailable: boolean;
|
||||||
|
lastSeen: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Series {
|
||||||
|
seriesId: number;
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
longDescription: string;
|
||||||
|
displayableTags: any[];
|
||||||
|
rating: Rating;
|
||||||
|
contentRating: Rating;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface NewHidiveSeriesExtra extends Series {
|
||||||
|
season: NewHidiveSeason;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface NewHidiveEpisodeExtra extends Episode {
|
||||||
|
titleId: number;
|
||||||
|
nameLong: string;
|
||||||
|
seasonTitle: string;
|
||||||
|
seriesTitle: string;
|
||||||
|
seriesId?: number;
|
||||||
|
isSelected: boolean;
|
||||||
|
jwtToken?: string;
|
||||||
|
}
|
||||||
35
@types/newHidiveSeries.d.ts
vendored
Normal file
35
@types/newHidiveSeries.d.ts
vendored
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
export interface NewHidiveSeries {
|
||||||
|
id: number;
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
longDescription: string;
|
||||||
|
smallCoverUrl: string;
|
||||||
|
coverUrl: string;
|
||||||
|
titleUrl: string;
|
||||||
|
posterUrl: string;
|
||||||
|
seasons: Season[];
|
||||||
|
rating: Rating;
|
||||||
|
contentRating: Rating;
|
||||||
|
displayableTags: any[];
|
||||||
|
paging: Paging;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Rating {
|
||||||
|
rating: string;
|
||||||
|
descriptors: any[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Paging {
|
||||||
|
moreDataAvailable: boolean;
|
||||||
|
lastSeen: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Season {
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
longDescription: string;
|
||||||
|
seasonNumber: number;
|
||||||
|
episodeCount: number;
|
||||||
|
displayableTags: any[];
|
||||||
|
id: number;
|
||||||
|
}
|
||||||
304
@types/objectInfo.d.ts
vendored
304
@types/objectInfo.d.ts
vendored
|
|
@ -1,93 +1,211 @@
|
||||||
// Generated by https://quicktype.io
|
// Generated by https://quicktype.io
|
||||||
|
|
||||||
export interface ObjectInfo {
|
export interface ObjectInfo {
|
||||||
__class__: string;
|
total: number;
|
||||||
__href__: string;
|
data: CrunchyObject[];
|
||||||
__resource_key__: string;
|
meta: Record<unknown>;
|
||||||
__links__: unknown;
|
}
|
||||||
__actions__: unknown;
|
|
||||||
total: number;
|
export interface CrunchyObject {
|
||||||
items: Item[];
|
__links__?: Links;
|
||||||
}
|
channel_id: string;
|
||||||
export interface Item {
|
slug: string;
|
||||||
__class__: string;
|
images: Images;
|
||||||
__href__: string;
|
linked_resource_key: string;
|
||||||
__links__: Links;
|
description: string;
|
||||||
__actions__: unknown;
|
promo_description: string;
|
||||||
id: string;
|
external_id: string;
|
||||||
external_id: string;
|
title: string;
|
||||||
channel_id: string;
|
series_metadata?: SeriesMetadata;
|
||||||
title: string;
|
id: string;
|
||||||
description: string;
|
slug_title: string;
|
||||||
promo_title: string;
|
type: string;
|
||||||
promo_description: string;
|
promo_title: string;
|
||||||
type: string;
|
movie_listing_metadata?: MovieListingMetadata;
|
||||||
slug: string;
|
movie_metadata?: MovieMetadata;
|
||||||
slug_title: string;
|
playback?: string;
|
||||||
images: Images;
|
episode_metadata?: EpisodeMetadata;
|
||||||
episode_metadata: EpisodeMetadata;
|
streams_link?: string;
|
||||||
playback: string;
|
season_metadata?: SeasonMetadata;
|
||||||
linked_resource_key: string;
|
isSelected?: boolean;
|
||||||
type: string;
|
f_num: string;
|
||||||
s_num?: string;
|
s_num: string;
|
||||||
f_num?: string;
|
}
|
||||||
movie_metadata?: {
|
|
||||||
movie_listing_id: string;
|
export interface Links {
|
||||||
movie_listing_title: string
|
'episode/season': LinkData;
|
||||||
};
|
'episode/series': LinkData;
|
||||||
isSelected?: boolean
|
resource: LinkData;
|
||||||
}
|
'resource/channel': LinkData;
|
||||||
|
streams: LinkData;
|
||||||
export interface Links {
|
}
|
||||||
'episode/season': EpisodeSeason;
|
|
||||||
'episode/series': EpisodeSeason;
|
export interface LinkData {
|
||||||
resource: EpisodeSeason;
|
href: string;
|
||||||
'resource/channel': EpisodeSeason;
|
}
|
||||||
streams: EpisodeSeason;
|
|
||||||
}
|
export interface EpisodeMetadata {
|
||||||
|
audio_locale: Locale;
|
||||||
export interface EpisodeSeason {
|
availability_ends: Date;
|
||||||
href: string;
|
availability_notes: string;
|
||||||
}
|
availability_starts: Date;
|
||||||
|
available_date: null;
|
||||||
export interface EpisodeMetadata {
|
available_offline: boolean;
|
||||||
series_id: string;
|
closed_captions_available: boolean;
|
||||||
series_title: string;
|
duration_ms: number;
|
||||||
series_slug_title: string;
|
eligible_region: string;
|
||||||
season_id: string;
|
episode: string;
|
||||||
season_title: string;
|
episode_air_date: Date;
|
||||||
season_slug_title: string;
|
episode_number: number;
|
||||||
season_number: number;
|
extended_maturity_rating: Record<unknown>;
|
||||||
episode_number: number;
|
free_available_date: Date;
|
||||||
episode: string;
|
identifier: string;
|
||||||
sequence_number: number;
|
is_clip: boolean;
|
||||||
duration_ms: number;
|
is_dubbed: boolean;
|
||||||
ad_breaks: AdBreak[];
|
is_mature: boolean;
|
||||||
episode_air_date: string;
|
is_premium_only: boolean;
|
||||||
is_premium_only: boolean;
|
is_subbed: boolean;
|
||||||
is_mature: boolean;
|
mature_blocked: boolean;
|
||||||
mature_blocked: boolean;
|
maturity_ratings: string[];
|
||||||
is_subbed: boolean;
|
premium_available_date: Date;
|
||||||
is_dubbed: boolean;
|
premium_date: null;
|
||||||
is_clip: boolean;
|
season_id: string;
|
||||||
available_offline: boolean;
|
season_number: number;
|
||||||
maturity_ratings: string[];
|
season_slug_title: string;
|
||||||
subtitle_locales: string[];
|
season_title: string;
|
||||||
availability_notes: string;
|
sequence_number: number;
|
||||||
}
|
series_id: string;
|
||||||
|
series_slug_title: string;
|
||||||
export interface AdBreak {
|
series_title: string;
|
||||||
type: string;
|
subtitle_locales: Locale[];
|
||||||
offset_ms: number;
|
tenant_categories?: string[];
|
||||||
}
|
upload_date: Date;
|
||||||
|
versions: EpisodeMetadataVersion[];
|
||||||
export interface Images {
|
}
|
||||||
thumbnail: Array<Thumbnail[]>;
|
|
||||||
}
|
export interface EpisodeMetadataVersion {
|
||||||
|
audio_locale: Locale;
|
||||||
export interface Thumbnail {
|
guid: string;
|
||||||
width: number;
|
is_premium_only: boolean;
|
||||||
height: number;
|
media_guid: string;
|
||||||
type: string;
|
original: boolean;
|
||||||
source: string;
|
season_guid: string;
|
||||||
}
|
variant: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Images {
|
||||||
|
poster_tall?: Array<Image[]>;
|
||||||
|
poster_wide?: Array<Image[]>;
|
||||||
|
promo_image?: Array<Image[]>;
|
||||||
|
thumbnail?: Array<Image[]>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Image {
|
||||||
|
height: number;
|
||||||
|
source: string;
|
||||||
|
type: ImageType;
|
||||||
|
width: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum ImageType {
|
||||||
|
PosterTall = 'poster_tall',
|
||||||
|
PosterWide = 'poster_wide',
|
||||||
|
PromoImage = 'promo_image',
|
||||||
|
Thumbnail = 'thumbnail',
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MovieListingMetadata {
|
||||||
|
availability_notes: string;
|
||||||
|
available_date: null;
|
||||||
|
available_offline: boolean;
|
||||||
|
duration_ms: number;
|
||||||
|
extended_description: string;
|
||||||
|
extended_maturity_rating: Record<unknown>;
|
||||||
|
first_movie_id: string;
|
||||||
|
free_available_date: Date;
|
||||||
|
is_dubbed: boolean;
|
||||||
|
is_mature: boolean;
|
||||||
|
is_premium_only: boolean;
|
||||||
|
is_subbed: boolean;
|
||||||
|
mature_blocked: boolean;
|
||||||
|
maturity_ratings: string[];
|
||||||
|
movie_release_year: number;
|
||||||
|
premium_available_date: Date;
|
||||||
|
premium_date: null;
|
||||||
|
subtitle_locales: Locale[];
|
||||||
|
tenant_categories: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MovieMetadata {
|
||||||
|
availability_notes: string;
|
||||||
|
available_offline: boolean;
|
||||||
|
closed_captions_available: boolean;
|
||||||
|
duration_ms: number;
|
||||||
|
extended_maturity_rating: Record<unknown>;
|
||||||
|
is_dubbed: boolean;
|
||||||
|
is_mature: boolean;
|
||||||
|
is_premium_only: boolean;
|
||||||
|
is_subbed: boolean;
|
||||||
|
mature_blocked: boolean;
|
||||||
|
maturity_ratings: string[];
|
||||||
|
movie_listing_id: string;
|
||||||
|
movie_listing_slug_title: string;
|
||||||
|
movie_listing_title: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SeasonMetadata {
|
||||||
|
audio_locale: Locale;
|
||||||
|
audio_locales: Locale[];
|
||||||
|
extended_maturity_rating: Record<unknown>;
|
||||||
|
identifier: string;
|
||||||
|
is_mature: boolean;
|
||||||
|
mature_blocked: boolean;
|
||||||
|
maturity_ratings: string[];
|
||||||
|
season_display_number: string;
|
||||||
|
season_sequence_number: number;
|
||||||
|
subtitle_locales: Locale[];
|
||||||
|
versions: SeasonMetadataVersion[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SeasonMetadataVersion {
|
||||||
|
audio_locale: Locale;
|
||||||
|
guid: string;
|
||||||
|
original: boolean;
|
||||||
|
variant: string;
|
||||||
|
}
|
||||||
|
export interface SeriesMetadata {
|
||||||
|
audio_locales: Locale[];
|
||||||
|
availability_notes: string;
|
||||||
|
episode_count: number;
|
||||||
|
extended_description: string;
|
||||||
|
extended_maturity_rating: Record<unknown>;
|
||||||
|
is_dubbed: boolean;
|
||||||
|
is_mature: boolean;
|
||||||
|
is_simulcast: boolean;
|
||||||
|
is_subbed: boolean;
|
||||||
|
mature_blocked: boolean;
|
||||||
|
maturity_ratings: string[];
|
||||||
|
season_count: number;
|
||||||
|
series_launch_year: number;
|
||||||
|
subtitle_locales: Locale[];
|
||||||
|
tenant_categories?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum Locale {
|
||||||
|
enUS = 'en-US',
|
||||||
|
esLA = 'es-LA',
|
||||||
|
es419 = 'es-419',
|
||||||
|
esES = 'es-ES',
|
||||||
|
ptBR = 'pt-BR',
|
||||||
|
frFR = 'fr-FR',
|
||||||
|
deDE = 'de-DE',
|
||||||
|
arME = 'ar-ME',
|
||||||
|
arSA = 'ar-SA',
|
||||||
|
itIT = 'it-IT',
|
||||||
|
ruRU = 'ru-RU',
|
||||||
|
trTR = 'tr-TR',
|
||||||
|
hiIN = 'hi-IN',
|
||||||
|
zhCN = 'zh-CN',
|
||||||
|
koKR = 'ko-KR',
|
||||||
|
jaJP = 'ja-JP',
|
||||||
|
}
|
||||||
4
@types/pkg.d.ts
vendored
4
@types/pkg.d.ts
vendored
|
|
@ -1,3 +1,3 @@
|
||||||
declare module 'pkg' {
|
declare module 'pkg' {
|
||||||
export async function exec(config: string[]);
|
export async function exec(config: string[]);
|
||||||
}
|
}
|
||||||
153
@types/playbackData.d.ts
vendored
153
@types/playbackData.d.ts
vendored
|
|
@ -1,34 +1,119 @@
|
||||||
// Generated by https://quicktype.io
|
// Generated by https://quicktype.io
|
||||||
|
export interface PlaybackData {
|
||||||
export interface PlaybackData {
|
total: number;
|
||||||
audio_locale: string;
|
data: { [key: string]: { [key: string]: StreamDetails } }[];
|
||||||
subtitles: { [key: string]: Subtitle };
|
meta: Meta;
|
||||||
streams: { [key: string]: { [key: string]: Stream } };
|
}
|
||||||
QoS: QoS;
|
|
||||||
}
|
export interface StreamList {
|
||||||
|
download_hls: CrunchyStreams;
|
||||||
export interface QoS {
|
drm_adaptive_hls: CrunchyStreams;
|
||||||
region: string;
|
multitrack_adaptive_hls_v2: CrunchyStreams;
|
||||||
cloudFrontRequestId: string;
|
vo_adaptive_hls: CrunchyStreams;
|
||||||
lambdaRunTime: number;
|
vo_drm_adaptive_hls: CrunchyStreams;
|
||||||
}
|
adaptive_hls: CrunchyStreams;
|
||||||
|
drm_download_dash: CrunchyStreams;
|
||||||
export interface Stream {
|
drm_download_hls: CrunchyStreams;
|
||||||
hardsub_locale: string;
|
drm_multitrack_adaptive_hls_v2: CrunchyStreams;
|
||||||
url: string;
|
vo_drm_adaptive_dash: CrunchyStreams;
|
||||||
vcodec: Vcodec;
|
adaptive_dash: CrunchyStreams;
|
||||||
hardsub_lang?: string;
|
urls: CrunchyStreams;
|
||||||
audio_lang?: string;
|
vo_adaptive_dash: CrunchyStreams;
|
||||||
type?: string;
|
download_dash: CrunchyStreams;
|
||||||
}
|
drm_adaptive_dash: CrunchyStreams;
|
||||||
|
}
|
||||||
|
|
||||||
export enum Vcodec {
|
export interface CrunchyStreams {
|
||||||
H264 = 'h264',
|
'': StreamDetails;
|
||||||
}
|
'en-US'?: StreamDetails;
|
||||||
|
'es-LA'?: StreamDetails;
|
||||||
export interface Subtitle {
|
'es-419'?: StreamDetails;
|
||||||
locale: Locale;
|
'es-ES'?: StreamDetails;
|
||||||
url: string;
|
'pt-BR'?: StreamDetails;
|
||||||
format: string;
|
'fr-FR'?: StreamDetails;
|
||||||
}
|
'de-DE'?: StreamDetails;
|
||||||
|
'ar-ME'?: StreamDetails;
|
||||||
|
'ar-SA'?: StreamDetails;
|
||||||
|
'it-IT'?: StreamDetails;
|
||||||
|
'ru-RU'?: StreamDetails;
|
||||||
|
'tr-TR'?: StreamDetails;
|
||||||
|
'hi-IN'?: StreamDetails;
|
||||||
|
'zh-CN'?: StreamDetails;
|
||||||
|
'ko-KR'?: StreamDetails;
|
||||||
|
'ja-JP'?: StreamDetails;
|
||||||
|
[string: string]: StreamDetails;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface StreamDetails {
|
||||||
|
//hardsub_locale: Locale;
|
||||||
|
hardsub_locale: string;
|
||||||
|
url: string;
|
||||||
|
hardsub_lang?: string;
|
||||||
|
audio_lang?: string;
|
||||||
|
type?: string;
|
||||||
|
}
|
||||||
|
export interface Meta {
|
||||||
|
media_id: string;
|
||||||
|
subtitles: Subtitles;
|
||||||
|
bifs: string[];
|
||||||
|
versions: Version[];
|
||||||
|
audio_locale: Locale;
|
||||||
|
closed_captions: Subtitles;
|
||||||
|
captions: Subtitles;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Subtitles {
|
||||||
|
''?: SubtitleInfo;
|
||||||
|
'en-US'?: SubtitleInfo;
|
||||||
|
'es-LA'?: SubtitleInfo;
|
||||||
|
'es-419'?: SubtitleInfo;
|
||||||
|
'es-ES'?: SubtitleInfo;
|
||||||
|
'pt-BR'?: SubtitleInfo;
|
||||||
|
'fr-FR'?: SubtitleInfo;
|
||||||
|
'de-DE'?: SubtitleInfo;
|
||||||
|
'ar-ME'?: SubtitleInfo;
|
||||||
|
'ar-SA'?: SubtitleInfo;
|
||||||
|
'it-IT'?: SubtitleInfo;
|
||||||
|
'ru-RU'?: SubtitleInfo;
|
||||||
|
'tr-TR'?: SubtitleInfo;
|
||||||
|
'hi-IN'?: SubtitleInfo;
|
||||||
|
'zh-CN'?: SubtitleInfo;
|
||||||
|
'ko-KR'?: SubtitleInfo;
|
||||||
|
'ja-JP'?: SubtitleInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export interface SubtitleInfo {
|
||||||
|
format: string;
|
||||||
|
locale: Locale;
|
||||||
|
url: string;
|
||||||
|
}
|
||||||
|
export interface Version {
|
||||||
|
audio_locale: Locale;
|
||||||
|
guid: string;
|
||||||
|
is_premium_only: boolean;
|
||||||
|
media_guid: string;
|
||||||
|
original: boolean;
|
||||||
|
season_guid: string;
|
||||||
|
variant: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum Locale {
|
||||||
|
default = '',
|
||||||
|
enUS = 'en-US',
|
||||||
|
esLA = 'es-LA',
|
||||||
|
es419 = 'es-419',
|
||||||
|
esES = 'es-ES',
|
||||||
|
ptBR = 'pt-BR',
|
||||||
|
frFR = 'fr-FR',
|
||||||
|
deDE = 'de-DE',
|
||||||
|
arME = 'ar-ME',
|
||||||
|
arSA = 'ar-SA',
|
||||||
|
itIT = 'it-IT',
|
||||||
|
ruRU = 'ru-RU',
|
||||||
|
trTR = 'tr-TR',
|
||||||
|
hiIN = 'hi-IN',
|
||||||
|
zhCN = 'zh-CN',
|
||||||
|
koKR = 'ko-KR',
|
||||||
|
jaJP = 'ja-JP',
|
||||||
|
}
|
||||||
15
@types/randomEvents.d.ts
vendored
Normal file
15
@types/randomEvents.d.ts
vendored
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
import { ExtendedProgress, QueueItem } from './messageHandler';
|
||||||
|
|
||||||
|
export type RandomEvents = {
|
||||||
|
progress: ExtendedProgress,
|
||||||
|
finish: undefined,
|
||||||
|
queueChange: QueueItem[],
|
||||||
|
current: QueueItem|undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RandomEvent<T extends keyof RandomEvents> {
|
||||||
|
name: T,
|
||||||
|
data: RandomEvents[T]
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Handler<T extends keyof RandomEvents> = (data: RandomEvent<T>) => unknown;
|
||||||
4
@types/removeNPMAbsolutePaths.d.ts
vendored
4
@types/removeNPMAbsolutePaths.d.ts
vendored
|
|
@ -1,3 +1,3 @@
|
||||||
declare module 'removeNPMAbsolutePaths' {
|
declare module 'removeNPMAbsolutePaths' {
|
||||||
export default async function modulesCleanup(path: string);
|
export default async function modulesCleanup(path: string);
|
||||||
}
|
}
|
||||||
27
@types/sei-helper.d.ts
vendored
27
@types/sei-helper.d.ts
vendored
|
|
@ -1,15 +1,14 @@
|
||||||
declare module 'sei-helper' {
|
declare module 'sei-helper' {
|
||||||
export async function question(qStr: string): Promise<string>;
|
export async function question(qStr: string): Promise<string>;
|
||||||
export function cleanupFilename(str: string): string;
|
export function cleanupFilename(str: string): string;
|
||||||
export function exec(str: string, str1: string, str2: string);
|
export const cookie: {
|
||||||
export const cookie: {
|
parse: (data: Record<string, string>) => Record<string, {
|
||||||
parse: (data: Record<string, string>) => Record<string, {
|
value: string;
|
||||||
value: string;
|
expires: Date;
|
||||||
expires: Date;
|
path: string;
|
||||||
path: string;
|
domain: string;
|
||||||
domain: string;
|
secure: boolean;
|
||||||
secure: boolean;
|
}>
|
||||||
}>
|
};
|
||||||
};
|
export function formatTime(time: number): string
|
||||||
export function formatTime(time: number): string
|
|
||||||
}
|
}
|
||||||
3
@types/serviceClassInterface.d.ts
vendored
Normal file
3
@types/serviceClassInterface.d.ts
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
export interface ServiceClass {
|
||||||
|
cli: () => Promise<boolean|undefined|void>
|
||||||
|
}
|
||||||
56
@types/streamData.d.ts
vendored
56
@types/streamData.d.ts
vendored
|
|
@ -1,28 +1,28 @@
|
||||||
// Generated by https://quicktype.io
|
// Generated by https://quicktype.io
|
||||||
|
|
||||||
export interface StreamData {
|
export interface StreamData {
|
||||||
items: Item[];
|
items: Item[];
|
||||||
watchHistorySaveInterval: number;
|
watchHistorySaveInterval: number;
|
||||||
errors?: Error[]
|
errors?: Error[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Error {
|
export interface Error {
|
||||||
detail: string,
|
detail: string,
|
||||||
code: number
|
code: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Item {
|
export interface Item {
|
||||||
src: string;
|
src: string;
|
||||||
kind: string;
|
kind: string;
|
||||||
isPromo: boolean;
|
isPromo: boolean;
|
||||||
videoType: string;
|
videoType: string;
|
||||||
aips: Aip[];
|
aips: Aip[];
|
||||||
experienceId: string;
|
experienceId: string;
|
||||||
showAds: boolean;
|
showAds: boolean;
|
||||||
id: number;
|
id: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Aip {
|
export interface Aip {
|
||||||
out: number;
|
out: number;
|
||||||
in: number;
|
in: number;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
6
@types/updateFile.d.ts
vendored
6
@types/updateFile.d.ts
vendored
|
|
@ -1,4 +1,4 @@
|
||||||
export type UpdateFile = {
|
export type UpdateFile = {
|
||||||
lastCheck: number,
|
lastCheck: number,
|
||||||
nextCheck: number
|
nextCheck: number
|
||||||
}
|
}
|
||||||
45
@types/ws.d.ts
vendored
Normal file
45
@types/ws.d.ts
vendored
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
import { GUIConfig } from '../modules/module.cfg-loader';
|
||||||
|
import { AuthResponse, CheckTokenResponse, EpisodeListResponse, FolderTypes, QueueItem, ResolveItemsData, SearchData, SearchResponse } from './messageHandler';
|
||||||
|
|
||||||
|
export type WSMessage<T extends keyof MessageTypes, P extends 0|1 = 0> = {
|
||||||
|
name: T,
|
||||||
|
data: MessageTypes[T][P]
|
||||||
|
}
|
||||||
|
|
||||||
|
export type WSMessageWithID<T extends keyof MessageTypes, P extends 0|1 = 0> = WSMessage<T, P> & {
|
||||||
|
id: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type UnknownWSMessage = {
|
||||||
|
name: keyof MessageTypes,
|
||||||
|
data: MessageTypes[keyof MessageTypes][0],
|
||||||
|
id: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type MessageTypes = {
|
||||||
|
'auth': [AuthData, AuthResponse],
|
||||||
|
'version': [undefined, string],
|
||||||
|
'checkToken': [undefined, CheckTokenResponse],
|
||||||
|
'search': [SearchData, SearchResponse],
|
||||||
|
'default': [string, unknown],
|
||||||
|
'availableDubCodes': [undefined, string[]],
|
||||||
|
'availableSubCodes': [undefined, string[]],
|
||||||
|
'resolveItems': [ResolveItemsData, boolean],
|
||||||
|
'listEpisodes': [string, EpisodeListResponse],
|
||||||
|
'downloadItem': [QueueItem, undefined],
|
||||||
|
'isDownloading': [undefined, boolean],
|
||||||
|
'openFolder': [FolderTypes, undefined],
|
||||||
|
'changeProvider': [undefined, boolean],
|
||||||
|
'type': [undefined, 'crunchy'|'hidive'|'ao'|'adn'|undefined],
|
||||||
|
'setup': ['crunchy'|'hidive'|'ao'|'adn'|undefined, undefined],
|
||||||
|
'openFile': [[FolderTypes, string], undefined],
|
||||||
|
'openURL': [string, undefined],
|
||||||
|
'isSetup': [undefined, boolean],
|
||||||
|
'setupServer': [GUIConfig, boolean],
|
||||||
|
'requirePassword': [undefined, boolean],
|
||||||
|
'getQueue': [undefined, QueueItem[]],
|
||||||
|
'removeFromQueue': [number, undefined],
|
||||||
|
'clearQueue': [undefined, undefined],
|
||||||
|
'setDownloadQueue': [boolean, undefined],
|
||||||
|
'getDownloadQueue': [undefined, boolean]
|
||||||
|
}
|
||||||
39
Dockerfile
Normal file
39
Dockerfile
Normal file
|
|
@ -0,0 +1,39 @@
|
||||||
|
FROM node AS builder
|
||||||
|
WORKDIR "/app"
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# Install 7z for packaging
|
||||||
|
|
||||||
|
RUN apt-get update
|
||||||
|
RUN apt-get install p7zip-full -y
|
||||||
|
|
||||||
|
# Update bin-path for docker/linux
|
||||||
|
|
||||||
|
RUN echo 'ffmpeg: "./bin/ffmpeg/ffmpeg"\nmkvmerge: "./bin/mkvtoolnix/mkvmerge"' > /app/config/bin-path.yml
|
||||||
|
|
||||||
|
#Build AniDL
|
||||||
|
|
||||||
|
RUN npm install -g pnpm
|
||||||
|
RUN pnpm i
|
||||||
|
RUN pnpm run build-linux-gui
|
||||||
|
|
||||||
|
# Move build to new Clean Image
|
||||||
|
|
||||||
|
FROM node
|
||||||
|
WORKDIR "/app"
|
||||||
|
COPY --from=builder /app/lib/_builds/multi-downloader-nx-linux-x64-gui ./
|
||||||
|
|
||||||
|
# Install mkvmerge and ffmpeg
|
||||||
|
|
||||||
|
RUN mkdir -p /app/bin/mkvtoolnix
|
||||||
|
RUN mkdir -p /app/bin/ffmpeg
|
||||||
|
|
||||||
|
RUN apt-get update
|
||||||
|
RUN apt-get install xdg-utils -y
|
||||||
|
RUN apt-get install mkvtoolnix -y
|
||||||
|
#RUN apt-get install ffmpeg -y
|
||||||
|
|
||||||
|
RUN mv /usr/bin/mkvmerge /app/bin/mkvtoolnix/mkvmerge
|
||||||
|
#RUN mv /usr/bin/ffmpeg /app/bin/ffmpeg/ffmpeg
|
||||||
|
|
||||||
|
CMD [ "/app/aniDL" ]
|
||||||
15
TODO.md
Normal file
15
TODO.md
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
# Todo/Future Ideas list
|
||||||
|
|
||||||
|
- [ ] Look into implementing wvd file support
|
||||||
|
- [ ] Merge sync branch with latest master
|
||||||
|
- [ ] Finish implementing old algorithm
|
||||||
|
- [ ] Look into adding suggested algorithm [#599](https://github.com/anidl/multi-downloader-nx/issues/599)
|
||||||
|
- [ ] Remove Funimation
|
||||||
|
- [ ] Remove old hidive API or find a way to make it work
|
||||||
|
- [ ] Look into adding other services
|
||||||
|
- [ ] Refactor downloading code
|
||||||
|
- [ ] Allow audio and video download at the same time
|
||||||
|
- [ ] Reduce/Refactor the amount of duplicate/boilerplate code required
|
||||||
|
- [ ] Create a generic service class for the CLI with set inputs/outputs
|
||||||
|
- [ ] Modularize site modules to ease addition of new sites
|
||||||
|
- [ ] Create generic MPD/M3U8 playlist downloader
|
||||||
924
adn.ts
Normal file
924
adn.ts
Normal file
|
|
@ -0,0 +1,924 @@
|
||||||
|
// Package Info
|
||||||
|
import packageJson from './package.json';
|
||||||
|
|
||||||
|
// Node
|
||||||
|
import path from 'path';
|
||||||
|
import fs from 'fs-extra';
|
||||||
|
import crypto from 'crypto';
|
||||||
|
|
||||||
|
// Plugins
|
||||||
|
import shlp from 'sei-helper';
|
||||||
|
import m3u8 from 'm3u8-parsed';
|
||||||
|
|
||||||
|
// Modules
|
||||||
|
import * as fontsData from './modules/module.fontsData';
|
||||||
|
import * as langsData from './modules/module.langsData';
|
||||||
|
import * as yamlCfg from './modules/module.cfg-loader';
|
||||||
|
import * as yargs from './modules/module.app-args';
|
||||||
|
import * as reqModule from './modules/module.fetch';
|
||||||
|
import Merger, { Font, MergerInput, SubtitleInput } from './modules/module.merger';
|
||||||
|
import streamdl from './modules/hls-download';
|
||||||
|
import { console } from './modules/log';
|
||||||
|
import { domain } from './modules/module.api-urls';
|
||||||
|
import { downloaded } from './modules/module.downloadArchive';
|
||||||
|
import parseSelect from './modules/module.parseSelect';
|
||||||
|
import parseFileName, { Variable } from './modules/module.filename';
|
||||||
|
import { AvailableFilenameVars } from './modules/module.args';
|
||||||
|
|
||||||
|
// Types
|
||||||
|
import { ServiceClass } from './@types/serviceClassInterface';
|
||||||
|
import { AuthData, AuthResponse, SearchData, SearchResponse, SearchResponseItem } from './@types/messageHandler';
|
||||||
|
import { sxItem } from './crunchy';
|
||||||
|
import { DownloadedMedia } from './@types/hidiveTypes';
|
||||||
|
import { ADNSearch, ADNSearchShow } from './@types/adnSearch';
|
||||||
|
import { ADNVideo, ADNVideos } from './@types/adnVideos';
|
||||||
|
import { ADNPlayerConfig } from './@types/adnPlayerConfig';
|
||||||
|
import { ADNStreams } from './@types/adnStreams';
|
||||||
|
import { ADNSubtitles } from './@types/adnSubtitles';
|
||||||
|
|
||||||
|
export default class AnimationDigitalNetwork implements ServiceClass {
|
||||||
|
public cfg: yamlCfg.ConfigObject;
|
||||||
|
public locale: string;
|
||||||
|
private token: Record<string, any>;
|
||||||
|
private req: reqModule.Req;
|
||||||
|
private posAlignMap: { [key: string]: number } = {
|
||||||
|
'start': 1,
|
||||||
|
'end': 3
|
||||||
|
};
|
||||||
|
private lineAlignMap: { [key: string]: number } = {
|
||||||
|
'middle': 8,
|
||||||
|
'end': 4
|
||||||
|
};
|
||||||
|
private jpnStrings: string[] = [
|
||||||
|
'vostf',
|
||||||
|
'vostde'
|
||||||
|
];
|
||||||
|
private deuStrings: string[] = [
|
||||||
|
'vde'
|
||||||
|
];
|
||||||
|
private fraStrings: string[] = [
|
||||||
|
'vf'
|
||||||
|
];
|
||||||
|
private deuSubStrings: string[] = [
|
||||||
|
'vde',
|
||||||
|
'vostde'
|
||||||
|
];
|
||||||
|
private fraSubStrings: string[] = [
|
||||||
|
'vf',
|
||||||
|
'vostf'
|
||||||
|
];
|
||||||
|
|
||||||
|
constructor(private debug = false) {
|
||||||
|
this.cfg = yamlCfg.loadCfg();
|
||||||
|
this.token = yamlCfg.loadADNToken();
|
||||||
|
this.req = new reqModule.Req(domain, debug, false, 'adn');
|
||||||
|
this.locale = 'fr';
|
||||||
|
}
|
||||||
|
|
||||||
|
public async cli() {
|
||||||
|
console.info(`\n=== Multi Downloader NX ${packageJson.version} ===\n`);
|
||||||
|
const argv = yargs.appArgv(this.cfg.cli);
|
||||||
|
if (['fr', 'de'].includes(argv.locale))
|
||||||
|
this.locale = argv.locale;
|
||||||
|
if (argv.debug)
|
||||||
|
this.debug = true;
|
||||||
|
|
||||||
|
// load binaries
|
||||||
|
this.cfg.bin = await yamlCfg.loadBinCfg();
|
||||||
|
if (argv.allDubs) {
|
||||||
|
argv.dubLang = langsData.dubLanguageCodes;
|
||||||
|
}
|
||||||
|
if (argv.auth) {
|
||||||
|
//Authenticate
|
||||||
|
await this.doAuth({
|
||||||
|
username: argv.username ?? await shlp.question('[Q] LOGIN/EMAIL'),
|
||||||
|
password: argv.password ?? await shlp.question('[Q] PASSWORD ')
|
||||||
|
});
|
||||||
|
} else if (argv.search && argv.search.length > 2) {
|
||||||
|
//Search
|
||||||
|
await this.doSearch({ ...argv, search: argv.search as string });
|
||||||
|
} else if (argv.s && !isNaN(parseInt(argv.s,10)) && parseInt(argv.s,10) > 0) {
|
||||||
|
const selected = await this.selectShow(parseInt(argv.s), argv.e, argv.but, argv.all);
|
||||||
|
if (selected.isOk) {
|
||||||
|
for (const select of selected.value) {
|
||||||
|
if (!(await this.getEpisode(select, {...argv, skipsubs: false}))) {
|
||||||
|
console.error(`Unable to download selected episode ${select.shortNumber}`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
console.info('No option selected or invalid value entered. Try --help.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private generateRandomString(length: number) {
|
||||||
|
const characters = '0123456789abcdef';
|
||||||
|
let result = '';
|
||||||
|
for (let i = 0; i < length; i++) {
|
||||||
|
result += characters.charAt(Math.floor(Math.random() * characters.length));
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private parseCookies(cookiesString: string | null): Record<string, string> {
|
||||||
|
const cookies: Record<string, string> = {};
|
||||||
|
if (cookiesString) {
|
||||||
|
cookiesString.split(';').forEach(cookie => {
|
||||||
|
const parts = cookie.split('=');
|
||||||
|
const name = parts.shift()?.trim();
|
||||||
|
const value = decodeURIComponent(parts.join('='));
|
||||||
|
if (name) {
|
||||||
|
cookies[name] = value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return cookies;
|
||||||
|
}
|
||||||
|
|
||||||
|
private convertToSSATimestamp(timestamp: number): string {
|
||||||
|
const seconds = Math.floor(timestamp);
|
||||||
|
const centiseconds = Math.round((timestamp - seconds) * 100);
|
||||||
|
|
||||||
|
const hours = Math.floor(seconds / 3600);
|
||||||
|
const minutes = Math.floor((seconds % 3600) / 60);
|
||||||
|
const remainingSeconds = seconds % 60;
|
||||||
|
|
||||||
|
return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${remainingSeconds.toString().padStart(2, '0')}.${centiseconds.toString().padStart(2, '0')}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async doSearch(data: SearchData): Promise<SearchResponse> {
|
||||||
|
const limit = 12;
|
||||||
|
const offset = data.page ? data.page * limit : 0;
|
||||||
|
const searchReq = await this.req.getData(`https://gw.api.animationdigitalnetwork.fr/show/catalog?maxAgeCategory=18&offset=${offset}&limit=${limit}&search=${encodeURIComponent(data.search)}`, {
|
||||||
|
'headers': {
|
||||||
|
'X-Target-Distribution': this.locale
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (!searchReq.ok || !searchReq.res) {
|
||||||
|
console.error('Search FAILED!');
|
||||||
|
return { isOk: false, reason: new Error('Search failed. No more information provided') };
|
||||||
|
}
|
||||||
|
const searchData = await searchReq.res.json() as ADNSearch;
|
||||||
|
const searchItems: ADNSearchShow[] = [];
|
||||||
|
console.info('Search Results:');
|
||||||
|
for (const show of searchData.shows) {
|
||||||
|
searchItems.push(show);
|
||||||
|
let fullType: string;
|
||||||
|
if (show.type == 'EPS') {
|
||||||
|
fullType = `S.${show.id}`;
|
||||||
|
} else if (show.type == 'MOV' || show.type == 'OAV') {
|
||||||
|
fullType = `E.${show.id}`;
|
||||||
|
} else {
|
||||||
|
fullType = 'Unknown';
|
||||||
|
console.warn(`Unknown type ${show.type}, please report this.`);
|
||||||
|
}
|
||||||
|
console.log(`[${fullType}] ${show.title}`);
|
||||||
|
}
|
||||||
|
return { isOk: true, value: searchItems.flatMap((a): SearchResponseItem => {
|
||||||
|
return {
|
||||||
|
id: a.id+'',
|
||||||
|
image: a.image ?? '/notFound.png',
|
||||||
|
name: a.title,
|
||||||
|
rating: a.rating,
|
||||||
|
desc: a.summary
|
||||||
|
};
|
||||||
|
})};
|
||||||
|
}
|
||||||
|
|
||||||
|
public async doAuth(data: AuthData): Promise<AuthResponse> {
|
||||||
|
const authData = new URLSearchParams({
|
||||||
|
'username': data.username,
|
||||||
|
'password': data.password,
|
||||||
|
'source': 'Web',
|
||||||
|
'rememberMe': 'true'
|
||||||
|
}).toString();
|
||||||
|
const authReqOpts: reqModule.Params = {
|
||||||
|
method: 'POST',
|
||||||
|
body: authData
|
||||||
|
};
|
||||||
|
const authReq = await this.req.getData('https://gw.api.animationdigitalnetwork.fr/authentication/login', authReqOpts);
|
||||||
|
if(!authReq.ok || !authReq.res){
|
||||||
|
console.error('Authentication failed!');
|
||||||
|
return { isOk: false, reason: new Error('Authentication failed') };
|
||||||
|
}
|
||||||
|
this.token = await authReq.res.json();
|
||||||
|
yamlCfg.saveADNToken(this.token);
|
||||||
|
console.info('Authentication Success');
|
||||||
|
return { isOk: true, value: undefined };
|
||||||
|
}
|
||||||
|
|
||||||
|
public async refreshToken() {
|
||||||
|
const authReq = await this.req.getData('https://gw.api.animationdigitalnetwork.fr/authentication/refresh', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${this.token.accessToken}`,
|
||||||
|
'X-Access-Token': this.token.accessToken,
|
||||||
|
'content-type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({refreshToken: this.token.refreshToken})
|
||||||
|
});
|
||||||
|
if(!authReq.ok || !authReq.res){
|
||||||
|
console.error('Token refresh failed!');
|
||||||
|
return { isOk: false, reason: new Error('Token refresh failed') };
|
||||||
|
}
|
||||||
|
this.token = await authReq.res.json();
|
||||||
|
yamlCfg.saveADNToken(this.token);
|
||||||
|
return { isOk: true, value: undefined };
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getShow(id: number) {
|
||||||
|
const getShowData = await this.req.getData(`https://gw.api.animationdigitalnetwork.fr/video/show/${id}?maxAgeCategory=18&limit=-1&order=asc`, {
|
||||||
|
'headers': {
|
||||||
|
'X-Target-Distribution': this.locale
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (!getShowData.ok || !getShowData.res) {
|
||||||
|
console.error('Failed to get Series Data');
|
||||||
|
return { isOk: false };
|
||||||
|
}
|
||||||
|
const showData = await getShowData.res.json() as ADNVideos;
|
||||||
|
return { isOk: true, value: showData };
|
||||||
|
}
|
||||||
|
|
||||||
|
public async listShow(id: number) {
|
||||||
|
const show = await this.getShow(id);
|
||||||
|
if (!show.isOk || !show.value) {
|
||||||
|
console.error('Failed to list show data: Failed to get show');
|
||||||
|
return { isOk: false };
|
||||||
|
}
|
||||||
|
if (show.value.videos.length == 0) {
|
||||||
|
console.error('No episodes found!');
|
||||||
|
return { isOk: false };
|
||||||
|
}
|
||||||
|
const showData = show.value.videos[0].show;
|
||||||
|
console.info(`[S.${showData.id}] ${showData.title}`);
|
||||||
|
const specials: ADNVideo[] = [];
|
||||||
|
let episodeIndex = 0, specialIndex = 0;
|
||||||
|
for (const episode of show.value.videos) {
|
||||||
|
episode.season = episode.season+'';
|
||||||
|
const seasonNumberTitleParse = episode.season.match(/\d+/);
|
||||||
|
const seriesNumberTitleParse = episode.show.title.match(/\d+/);
|
||||||
|
const episodeNumber = parseInt(episode.shortNumber);
|
||||||
|
if (seasonNumberTitleParse && !isNaN(parseInt(seasonNumberTitleParse[0]))) {
|
||||||
|
episode.season = seasonNumberTitleParse[0];
|
||||||
|
} else if (seriesNumberTitleParse && !isNaN(parseInt(seriesNumberTitleParse[0]))) {
|
||||||
|
episode.season = seriesNumberTitleParse[0];
|
||||||
|
} else {
|
||||||
|
episode.season = '1';
|
||||||
|
}
|
||||||
|
show.value.videos[episodeIndex].season = episode.season;
|
||||||
|
if (!episodeNumber) {
|
||||||
|
specialIndex++;
|
||||||
|
const special = show.value.videos.splice(episodeIndex, 1);
|
||||||
|
special[0].shortNumber = 'S'+specialIndex;
|
||||||
|
specials.push(...special);
|
||||||
|
episodeIndex--;
|
||||||
|
} else {
|
||||||
|
console.info(` (${episode.id}) [E${episode.shortNumber}] ${episode.number} - ${episode.name}`);
|
||||||
|
}
|
||||||
|
episodeIndex++;
|
||||||
|
}
|
||||||
|
for (const special of specials) {
|
||||||
|
console.info(` (${special.id}) [${special.shortNumber}] ${special.number} - ${special.name}`);
|
||||||
|
}
|
||||||
|
show.value.videos.push(...specials);
|
||||||
|
return { isOk: true, value: show.value };
|
||||||
|
}
|
||||||
|
|
||||||
|
public async selectShow(id: number, e: string | undefined, but: boolean, all: boolean) {
|
||||||
|
const getShowData = await this.listShow(id);
|
||||||
|
if (!getShowData.isOk || !getShowData.value) {
|
||||||
|
return { isOk: false, value: [] };
|
||||||
|
}
|
||||||
|
console.info('');
|
||||||
|
console.info('-'.repeat(30));
|
||||||
|
console.info('');
|
||||||
|
const showData = getShowData.value;
|
||||||
|
const doEpsFilter = parseSelect(e as string);
|
||||||
|
const selEpsArr: ADNVideo[] = [];
|
||||||
|
for (const episode of showData.videos) {
|
||||||
|
if (
|
||||||
|
all ||
|
||||||
|
but && !doEpsFilter.isSelected([episode.shortNumber, episode.id+'']) ||
|
||||||
|
!but && doEpsFilter.isSelected([episode.shortNumber, episode.id+''])
|
||||||
|
) {
|
||||||
|
selEpsArr.push({ isSelected: true, ...episode });
|
||||||
|
console.info('%s[S%sE%s] %s',
|
||||||
|
'✓ ',
|
||||||
|
episode.season,
|
||||||
|
episode.shortNumber,
|
||||||
|
episode.name,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { isOk: true, value: selEpsArr };
|
||||||
|
}
|
||||||
|
|
||||||
|
public async muxStreams(data: DownloadedMedia[], options: yargs.ArgvType) {
|
||||||
|
this.cfg.bin = await yamlCfg.loadBinCfg();
|
||||||
|
let hasAudioStreams = false;
|
||||||
|
if (options.novids || data.filter(a => a.type === 'Video').length === 0)
|
||||||
|
return console.info('Skip muxing since no vids are downloaded');
|
||||||
|
if (data.some(a => a.type === 'Audio')) {
|
||||||
|
hasAudioStreams = true;
|
||||||
|
}
|
||||||
|
const merger = new Merger({
|
||||||
|
onlyVid: hasAudioStreams ? data.filter(a => a.type === 'Video').map((a) : MergerInput => {
|
||||||
|
if (a.type === 'Subtitle')
|
||||||
|
throw new Error('Never');
|
||||||
|
return {
|
||||||
|
lang: a.lang,
|
||||||
|
path: a.path,
|
||||||
|
};
|
||||||
|
}) : [],
|
||||||
|
skipSubMux: options.skipSubMux,
|
||||||
|
inverseTrackOrder: false,
|
||||||
|
keepAllVideos: options.keepAllVideos,
|
||||||
|
onlyAudio: hasAudioStreams ? data.filter(a => a.type === 'Audio').map((a) : MergerInput => {
|
||||||
|
if (a.type === 'Subtitle')
|
||||||
|
throw new Error('Never');
|
||||||
|
return {
|
||||||
|
lang: a.lang,
|
||||||
|
path: a.path,
|
||||||
|
};
|
||||||
|
}) : [],
|
||||||
|
output: `${options.output}.${options.mp4 ? 'mp4' : 'mkv'}`,
|
||||||
|
subtitles: data.filter(a => a.type === 'Subtitle').map((a) : SubtitleInput => {
|
||||||
|
if (a.type === 'Video')
|
||||||
|
throw new Error('Never');
|
||||||
|
if (a.type === 'Audio')
|
||||||
|
throw new Error('Never');
|
||||||
|
return {
|
||||||
|
file: a.path,
|
||||||
|
language: a.language,
|
||||||
|
closedCaption: a.cc
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
simul: data.filter(a => a.type === 'Video').map((a) : boolean => {
|
||||||
|
if (a.type === 'Subtitle')
|
||||||
|
throw new Error('Never');
|
||||||
|
return !a.uncut as boolean;
|
||||||
|
})[0],
|
||||||
|
fonts: Merger.makeFontsList(this.cfg.dir.fonts, data.filter(a => a.type === 'Subtitle') as sxItem[]),
|
||||||
|
videoAndAudio: hasAudioStreams ? [] : data.filter(a => a.type === 'Video').map((a) : MergerInput => {
|
||||||
|
if (a.type === 'Subtitle')
|
||||||
|
throw new Error('Never');
|
||||||
|
return {
|
||||||
|
lang: a.lang,
|
||||||
|
path: a.path,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
chapters: data.filter(a => a.type === 'Chapters').map((a) : MergerInput => {
|
||||||
|
return {
|
||||||
|
path: a.path,
|
||||||
|
lang: a.lang
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
videoTitle: options.videoTitle,
|
||||||
|
options: {
|
||||||
|
ffmpeg: options.ffmpegOptions,
|
||||||
|
mkvmerge: options.mkvmergeOptions
|
||||||
|
},
|
||||||
|
defaults: {
|
||||||
|
audio: options.defaultAudio,
|
||||||
|
sub: options.defaultSub
|
||||||
|
},
|
||||||
|
ccTag: options.ccTag
|
||||||
|
});
|
||||||
|
const bin = Merger.checkMerger(this.cfg.bin, options.mp4, options.forceMuxer);
|
||||||
|
// collect fonts info
|
||||||
|
// mergers
|
||||||
|
let isMuxed = false;
|
||||||
|
if (options.syncTiming) {
|
||||||
|
await merger.createDelays();
|
||||||
|
}
|
||||||
|
if (bin.MKVmerge) {
|
||||||
|
await merger.merge('mkvmerge', bin.MKVmerge);
|
||||||
|
isMuxed = true;
|
||||||
|
} else if (bin.FFmpeg) {
|
||||||
|
await merger.merge('ffmpeg', bin.FFmpeg);
|
||||||
|
isMuxed = true;
|
||||||
|
} else{
|
||||||
|
console.info('\nDone!\n');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (isMuxed && !options.nocleanup)
|
||||||
|
merger.cleanUp();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public async getEpisode(data: ADNVideo, options: yargs.ArgvType) {
|
||||||
|
//TODO: Move all the requests for getting the m3u8 here
|
||||||
|
const res = await this.downloadEpisode(data, options);
|
||||||
|
if (res === undefined || res.error) {
|
||||||
|
console.error('Failed to download media list');
|
||||||
|
return { isOk: false, reason: new Error('Failed to download media list') };
|
||||||
|
} else {
|
||||||
|
if (!options.skipmux) {
|
||||||
|
await this.muxStreams(res.data, { ...options, output: res.fileName });
|
||||||
|
} else {
|
||||||
|
console.info('Skipping mux');
|
||||||
|
}
|
||||||
|
downloaded({
|
||||||
|
service: 'adn',
|
||||||
|
type: 's'
|
||||||
|
}, data.id+'', [data.shortNumber]);
|
||||||
|
return { isOk: res, value: undefined };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async downloadEpisode(data: ADNVideo, options: yargs.ArgvType) {
|
||||||
|
if(!this.token.accessToken){
|
||||||
|
console.error('Authentication required!');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.cfg.bin.ffmpeg)
|
||||||
|
this.cfg.bin = await yamlCfg.loadBinCfg();
|
||||||
|
|
||||||
|
let mediaName = '...';
|
||||||
|
let fileName;
|
||||||
|
const variables: Variable[] = [];
|
||||||
|
if(data.show.title && data.shortNumber && data.title){
|
||||||
|
mediaName = `${data.show.shortTitle ?? data.show.title} - ${data.shortNumber} - ${data.title}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const files: DownloadedMedia[] = [];
|
||||||
|
|
||||||
|
let dlFailed = false;
|
||||||
|
let dlVideoOnce = false; // Variable to save if best selected video quality was downloaded
|
||||||
|
|
||||||
|
const refreshToken = await this.refreshToken();
|
||||||
|
if (!refreshToken.isOk) {
|
||||||
|
console.error('Failed to refresh token');
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const configReq = await this.req.getData(`https://gw.api.animationdigitalnetwork.fr/player/video/${data.id}/configuration`, {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${this.token.accessToken}`,
|
||||||
|
'X-Target-Distribution': this.locale
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if(!configReq.ok || !configReq.res){
|
||||||
|
console.error('Player Config Request failed!');
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
const configuration = await configReq.res.json() as ADNPlayerConfig;
|
||||||
|
if (!configuration.player.options.user.hasAccess) {
|
||||||
|
console.error('You don\'t have access to this video!');
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
const tokenReq = await this.req.getData(configuration.player.options.user.refreshTokenUrl || 'https://gw.api.animationdigitalnetwork.fr/player/refresh/token', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'X-Player-Refresh-Token': `${configuration.player.options.user.refreshToken}`
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if(!tokenReq.ok || !tokenReq.res){
|
||||||
|
console.error('Player Token Request failed!');
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
const token = await tokenReq.res.json() as {
|
||||||
|
refreshToken: string,
|
||||||
|
accessToken: string,
|
||||||
|
token: string
|
||||||
|
};
|
||||||
|
|
||||||
|
const linksUrl = configuration.player.options.video.url || `https://gw.api.animationdigitalnetwork.fr/player/video/${data.id}/link`;
|
||||||
|
const key = this.generateRandomString(16);
|
||||||
|
const decryptionKey = key + '7fac1178830cfe0c';
|
||||||
|
|
||||||
|
const authorization = crypto.publicEncrypt({
|
||||||
|
'key': '-----BEGIN PUBLIC KEY-----\nMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCbQrCJBRmaXM4gJidDmcpWDssg\nnumHinCLHAgS4buMtdH7dEGGEUfBofLzoEdt1jqcrCDT6YNhM0aFCqbLOPFtx9cg\n/X2G/G5bPVu8cuFM0L+ehp8s6izK1kjx3OOPH/kWzvstM5tkqgJkNyNEvHdeJl6\nKhS+IFEqwvZqgbBpKuwIDAQAB\n-----END PUBLIC KEY-----',
|
||||||
|
padding: crypto.constants.RSA_PKCS1_PADDING
|
||||||
|
}, Buffer.from(JSON.stringify({
|
||||||
|
k: key,
|
||||||
|
t: token.token
|
||||||
|
}), 'utf-8')).toString('base64');
|
||||||
|
|
||||||
|
//TODO: Add chapter support
|
||||||
|
const streamsRequest = await this.req.getData(linksUrl+'?freeWithAds=true&adaptive=true&withMetadata=true&source=Web', {
|
||||||
|
'headers': {
|
||||||
|
'X-Player-Token': authorization,
|
||||||
|
'X-Target-Distribution': this.locale
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if(!streamsRequest.ok || !streamsRequest.res){
|
||||||
|
if (streamsRequest.error?.res.status == 403 || streamsRequest.res?.status == 403) {
|
||||||
|
console.error('Georestricted!');
|
||||||
|
} else {
|
||||||
|
console.error('Streams request failed!');
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
const streams = await streamsRequest.res.json() as ADNStreams;
|
||||||
|
for (const streamName in streams.links.streaming) {
|
||||||
|
let audDub: langsData.LanguageItem;
|
||||||
|
if (this.jpnStrings.includes(streamName)) {
|
||||||
|
audDub = langsData.languages.find(a=>a.code == 'jpn') as langsData.LanguageItem;
|
||||||
|
} else if (this.deuStrings.includes(streamName)) {
|
||||||
|
audDub = langsData.languages.find(a=>a.code == 'deu') as langsData.LanguageItem;
|
||||||
|
} else if (this.fraStrings.includes(streamName)) {
|
||||||
|
audDub = langsData.languages.find(a=>a.code == 'fra') as langsData.LanguageItem;
|
||||||
|
} else {
|
||||||
|
console.error(`Language ${streamName} not recognized, please report this.`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!options.dubLang.includes(audDub.code)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.info(`Requesting: [${data.id}] ${mediaName} (${audDub.name})`);
|
||||||
|
|
||||||
|
variables.push(...([
|
||||||
|
['title', data.title, true],
|
||||||
|
['episode', isNaN(parseFloat(data.shortNumber)) ? data.shortNumber : parseFloat(data.shortNumber), false],
|
||||||
|
['service', 'ADN', false],
|
||||||
|
['seriesTitle', data.show.shortTitle ?? data.show.title, true],
|
||||||
|
['showTitle', data.show.title, true],
|
||||||
|
['season', isNaN(parseFloat(data.season)) ? data.season : parseFloat(data.season), false]
|
||||||
|
] as [AvailableFilenameVars, string|number, boolean][]).map((a): Variable => {
|
||||||
|
return {
|
||||||
|
name: a[0],
|
||||||
|
replaceWith: a[1],
|
||||||
|
type: typeof a[1],
|
||||||
|
sanitize: a[2]
|
||||||
|
} as Variable;
|
||||||
|
}));
|
||||||
|
|
||||||
|
console.info('Playlists URL: %s', streams.links.streaming[streamName].auto);
|
||||||
|
|
||||||
|
let tsFile = undefined;
|
||||||
|
|
||||||
|
if (!dlFailed && !options.novids) {
|
||||||
|
const streamPlaylistsLocationReq = await this.req.getData(streams.links.streaming[streamName].auto);
|
||||||
|
if (!streamPlaylistsLocationReq.ok || !streamPlaylistsLocationReq.res) {
|
||||||
|
console.error('CAN\'T FETCH VIDEO PLAYLIST LOCATION!');
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
const streamPlaylistLocation = await streamPlaylistsLocationReq.res.json() as {'location': string};
|
||||||
|
const streamPlaylistsReq = await this.req.getData(streamPlaylistLocation.location);
|
||||||
|
if (!streamPlaylistsReq.ok || !streamPlaylistsReq.res) {
|
||||||
|
console.error('CAN\'T FETCH VIDEO PLAYLISTS!');
|
||||||
|
dlFailed = true;
|
||||||
|
} else {
|
||||||
|
const streamPlaylistBody = await streamPlaylistsReq.res.text();
|
||||||
|
const streamPlaylists = m3u8(streamPlaylistBody);
|
||||||
|
const plServerList: string[] = [],
|
||||||
|
plStreams: Record<string, Record<string, string>> = {},
|
||||||
|
plQuality: {
|
||||||
|
str: string,
|
||||||
|
dim: string,
|
||||||
|
CODECS: string,
|
||||||
|
RESOLUTION: {
|
||||||
|
width: number,
|
||||||
|
height: number
|
||||||
|
}
|
||||||
|
}[] = [];
|
||||||
|
for(const pl of streamPlaylists.playlists){
|
||||||
|
// set quality
|
||||||
|
const plResolution = pl.attributes.RESOLUTION;
|
||||||
|
const plResolutionText = `${plResolution.width}x${plResolution.height}`;
|
||||||
|
// set codecs
|
||||||
|
const plCodecs = pl.attributes.CODECS;
|
||||||
|
// parse uri
|
||||||
|
const plUri = new URL(pl.uri);
|
||||||
|
let plServer = plUri.hostname;
|
||||||
|
// set server list
|
||||||
|
if (plUri.searchParams.get('cdn')){
|
||||||
|
plServer += ` (${plUri.searchParams.get('cdn')})`;
|
||||||
|
}
|
||||||
|
if (!plServerList.includes(plServer)){
|
||||||
|
plServerList.push(plServer);
|
||||||
|
}
|
||||||
|
// add to server
|
||||||
|
if (!Object.keys(plStreams).includes(plServer)){
|
||||||
|
plStreams[plServer] = {};
|
||||||
|
}
|
||||||
|
if(
|
||||||
|
plStreams[plServer][plResolutionText]
|
||||||
|
&& plStreams[plServer][plResolutionText] != pl.uri
|
||||||
|
&& typeof plStreams[plServer][plResolutionText] != 'undefined'
|
||||||
|
) {
|
||||||
|
console.error(`Non duplicate url for ${plServer} detected, please report to developer!`);
|
||||||
|
} else{
|
||||||
|
plStreams[plServer][plResolutionText] = pl.uri;
|
||||||
|
}
|
||||||
|
// set plQualityStr
|
||||||
|
const plBandwidth = Math.round(pl.attributes.BANDWIDTH/1024);
|
||||||
|
const qualityStrAdd = `${plResolutionText} (${plBandwidth}KiB/s)`;
|
||||||
|
const qualityStrRegx = new RegExp(qualityStrAdd.replace(/([:()/])/g, '\\$1'), 'm');
|
||||||
|
const qualityStrMatch = !plQuality.map(a => a.str).join('\r\n').match(qualityStrRegx);
|
||||||
|
if(qualityStrMatch){
|
||||||
|
plQuality.push({
|
||||||
|
str: qualityStrAdd,
|
||||||
|
dim: plResolutionText,
|
||||||
|
CODECS: plCodecs,
|
||||||
|
RESOLUTION: plResolution
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
options.x = options.x > plServerList.length ? 1 : options.x;
|
||||||
|
|
||||||
|
const plSelectedServer = plServerList[options.x - 1];
|
||||||
|
const plSelectedList = plStreams[plSelectedServer];
|
||||||
|
plQuality.sort((a, b) => {
|
||||||
|
const aMatch: RegExpMatchArray | never[] = a.dim.match(/[0-9]+/) || [];
|
||||||
|
const bMatch: RegExpMatchArray | never[] = b.dim.match(/[0-9]+/) || [];
|
||||||
|
return parseInt(aMatch[0]) - parseInt(bMatch[0]);
|
||||||
|
});
|
||||||
|
let quality = options.q === 0 ? plQuality.length : options.q;
|
||||||
|
if(quality > plQuality.length) {
|
||||||
|
console.warn(`The requested quality of ${options.q} is greater than the maximum ${plQuality.length}.\n[WARN] Therefor the maximum will be capped at ${plQuality.length}.`);
|
||||||
|
quality = plQuality.length;
|
||||||
|
}
|
||||||
|
// When best selected video quality is already downloaded
|
||||||
|
if(dlVideoOnce && options.dlVideoOnce) {
|
||||||
|
// Select the lowest resolution with the same codecs
|
||||||
|
while(quality !=1 && plQuality[quality - 1].CODECS == plQuality[quality - 2].CODECS) {
|
||||||
|
quality--;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const selPlUrl = plSelectedList[plQuality.map(a => a.dim)[quality - 1]] ? plSelectedList[plQuality.map(a => a.dim)[quality - 1]] : '';
|
||||||
|
console.info(`Servers available:\n\t${plServerList.join('\n\t')}`);
|
||||||
|
console.info(`Available qualities:\n\t${plQuality.map((a, ind) => `[${ind+1}] ${a.str}`).join('\n\t')}`);
|
||||||
|
|
||||||
|
if(selPlUrl != ''){
|
||||||
|
variables.push({
|
||||||
|
name: 'height',
|
||||||
|
type: 'number',
|
||||||
|
replaceWith: quality === 0 ? plQuality[plQuality.length - 1].RESOLUTION.height as number : plQuality[quality - 1].RESOLUTION.height
|
||||||
|
}, {
|
||||||
|
name: 'width',
|
||||||
|
type: 'number',
|
||||||
|
replaceWith: quality === 0 ? plQuality[plQuality.length - 1].RESOLUTION.width as number : plQuality[quality - 1].RESOLUTION.width
|
||||||
|
});
|
||||||
|
|
||||||
|
console.info(`Selected quality: ${Object.keys(plSelectedList).find(a => plSelectedList[a] === selPlUrl)} @ ${plSelectedServer}`);
|
||||||
|
console.info('Stream URL:', selPlUrl);
|
||||||
|
// TODO check filename
|
||||||
|
fileName = parseFileName(options.fileName, variables, options.numbers, options.override).join(path.sep);
|
||||||
|
const outFile = parseFileName(options.fileName + '.' + audDub.name, variables, options.numbers, options.override).join(path.sep);
|
||||||
|
console.info(`Output filename: ${outFile}`);
|
||||||
|
const chunkPage = await this.req.getData(selPlUrl);
|
||||||
|
if(!chunkPage.ok || !chunkPage.res){
|
||||||
|
console.error('CAN\'T FETCH VIDEO PLAYLIST!');
|
||||||
|
dlFailed = true;
|
||||||
|
} else {
|
||||||
|
const chunkPageBody = await chunkPage.res.text();
|
||||||
|
const chunkPlaylist = m3u8(chunkPageBody);
|
||||||
|
const totalParts = chunkPlaylist.segments.length;
|
||||||
|
const mathParts = Math.ceil(totalParts / options.partsize);
|
||||||
|
const mathMsg = `(${mathParts}*${options.partsize})`;
|
||||||
|
console.info('Total parts in stream:', totalParts, mathMsg);
|
||||||
|
tsFile = path.isAbsolute(outFile as string) ? outFile : path.join(this.cfg.dir.content, outFile);
|
||||||
|
const split = outFile.split(path.sep).slice(0, -1);
|
||||||
|
split.forEach((val, ind, arr) => {
|
||||||
|
const isAbsolut = path.isAbsolute(outFile as string);
|
||||||
|
if (!fs.existsSync(path.join(isAbsolut ? '' : this.cfg.dir.content, ...arr.slice(0, ind), val)))
|
||||||
|
fs.mkdirSync(path.join(isAbsolut ? '' : this.cfg.dir.content, ...arr.slice(0, ind), val));
|
||||||
|
});
|
||||||
|
const dlStreamByPl = await new streamdl({
|
||||||
|
output: `${tsFile}.ts`,
|
||||||
|
timeout: options.timeout,
|
||||||
|
m3u8json: chunkPlaylist,
|
||||||
|
baseurl: selPlUrl.replace('playlist.m3u8',''),
|
||||||
|
threads: options.partsize,
|
||||||
|
fsRetryTime: options.fsRetryTime * 1000,
|
||||||
|
override: options.force,
|
||||||
|
callback: options.callbackMaker ? options.callbackMaker({
|
||||||
|
fileName: `${path.isAbsolute(outFile) ? outFile.slice(this.cfg.dir.content.length) : outFile}`,
|
||||||
|
image: data.image,
|
||||||
|
parent: {
|
||||||
|
title: data.show.title
|
||||||
|
},
|
||||||
|
title: data.title,
|
||||||
|
language: audDub
|
||||||
|
}) : undefined
|
||||||
|
}).download();
|
||||||
|
if (!dlStreamByPl.ok) {
|
||||||
|
console.error(`DL Stats: ${JSON.stringify(dlStreamByPl.parts)}\n`);
|
||||||
|
dlFailed = true;
|
||||||
|
}
|
||||||
|
files.push({
|
||||||
|
type: 'Video',
|
||||||
|
path: `${tsFile}.ts`,
|
||||||
|
lang: audDub
|
||||||
|
});
|
||||||
|
dlVideoOnce = true;
|
||||||
|
}
|
||||||
|
} else{
|
||||||
|
console.error('Quality not selected!\n');
|
||||||
|
dlFailed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (options.novids) {
|
||||||
|
console.info('Downloading skipped!');
|
||||||
|
fileName = parseFileName(options.fileName, variables, options.numbers, options.override).join(path.sep);
|
||||||
|
}
|
||||||
|
await this.sleep(options.waittime);
|
||||||
|
}
|
||||||
|
|
||||||
|
const compiledChapters: string[] = [];
|
||||||
|
if (options.chapters) {
|
||||||
|
if (streams.video.tcIntroStart) {
|
||||||
|
if (streams.video.tcIntroStart != '00:00:00') {
|
||||||
|
compiledChapters.push(
|
||||||
|
`CHAPTER${(compiledChapters.length/2)+1}=00:00:00.00`,
|
||||||
|
`CHAPTER${(compiledChapters.length/2)+1}NAME=Prologue`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
compiledChapters.push(
|
||||||
|
`CHAPTER${(compiledChapters.length/2)+1}=${streams.video.tcIntroStart+'.00'}`,
|
||||||
|
`CHAPTER${(compiledChapters.length/2)+1}NAME=Opening`
|
||||||
|
);
|
||||||
|
compiledChapters.push(
|
||||||
|
`CHAPTER${(compiledChapters.length/2)+1}=${streams.video.tcIntroEnd+'.00'}`,
|
||||||
|
`CHAPTER${(compiledChapters.length/2)+1}NAME=Episode`
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
compiledChapters.push(
|
||||||
|
`CHAPTER${(compiledChapters.length/2)+1}=00:00:00.00`,
|
||||||
|
`CHAPTER${(compiledChapters.length/2)+1}NAME=Episode`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (streams.video.tcEndingStart) {
|
||||||
|
compiledChapters.push(
|
||||||
|
`CHAPTER${(compiledChapters.length/2)+1}=${streams.video.tcEndingStart+'.00'}`,
|
||||||
|
`CHAPTER${(compiledChapters.length/2)+1}NAME=Ending Start`
|
||||||
|
);
|
||||||
|
compiledChapters.push(
|
||||||
|
`CHAPTER${(compiledChapters.length/2)+1}=${streams.video.tcEndingEnd+'.00'}`,
|
||||||
|
`CHAPTER${(compiledChapters.length/2)+1}NAME=Ending End`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (compiledChapters.length > 0) {
|
||||||
|
try {
|
||||||
|
fileName = parseFileName(options.fileName, variables, options.numbers, options.override).join(path.sep);
|
||||||
|
const outFile = parseFileName(options.fileName, variables, options.numbers, options.override).join(path.sep);
|
||||||
|
const tsFile = path.isAbsolute(outFile as string) ? outFile : path.join(this.cfg.dir.content, outFile);
|
||||||
|
const split = outFile.split(path.sep).slice(0, -1);
|
||||||
|
split.forEach((val, ind, arr) => {
|
||||||
|
const isAbsolut = path.isAbsolute(outFile as string);
|
||||||
|
if (!fs.existsSync(path.join(isAbsolut ? '' : this.cfg.dir.content, ...arr.slice(0, ind), val)))
|
||||||
|
fs.mkdirSync(path.join(isAbsolut ? '' : this.cfg.dir.content, ...arr.slice(0, ind), val));
|
||||||
|
});
|
||||||
|
fs.writeFileSync(`${tsFile}.txt`, compiledChapters.join('\r\n'));
|
||||||
|
files.push({
|
||||||
|
path: `${tsFile}.txt`,
|
||||||
|
lang: langsData.languages.find(a=>a.code=='jpn'),
|
||||||
|
type: 'Chapters'
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
console.error('Failed to write chapter file');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(options.dlsubs.indexOf('all') > -1){
|
||||||
|
options.dlsubs = ['all'];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.nosubs) {
|
||||||
|
console.info('Subtitles downloading disabled from nosubs flag.');
|
||||||
|
options.skipsubs = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!options.skipsubs && options.dlsubs.indexOf('none') == -1) {
|
||||||
|
if (Object.keys(streams.links.subtitles).length !== 0) {
|
||||||
|
const subtitlesUrlReq = await this.req.getData(streams.links.subtitles.all);
|
||||||
|
if(!subtitlesUrlReq.ok || !subtitlesUrlReq.res){
|
||||||
|
console.error('Subtitle location request failed!');
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
const subtitleUrl = await subtitlesUrlReq.res.json() as {'location': string};
|
||||||
|
const encryptedSubtitlesReq = await this.req.getData(subtitleUrl.location);
|
||||||
|
if(!encryptedSubtitlesReq.ok || !encryptedSubtitlesReq.res){
|
||||||
|
console.error('Subtitle request failed!');
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
const encryptedSubtitles = await encryptedSubtitlesReq.res.text();
|
||||||
|
const iv = Buffer.from(encryptedSubtitles.slice(0, 24), 'base64');
|
||||||
|
const derivedKey = Buffer.from(decryptionKey, 'hex');
|
||||||
|
const encryptedData = Buffer.from(encryptedSubtitles.slice(24), 'base64');
|
||||||
|
const decipher = crypto.createDecipheriv('aes-128-cbc', derivedKey, iv);
|
||||||
|
const decryptedData = Buffer.concat([decipher.update(encryptedData), decipher.final()]).toString('utf8');
|
||||||
|
|
||||||
|
let subIndex = 0;
|
||||||
|
const subtitles = JSON.parse(decryptedData) as ADNSubtitles;
|
||||||
|
if (Object.keys(subtitles).length === 0) {
|
||||||
|
console.warn('No subtitles found.');
|
||||||
|
}
|
||||||
|
for (const subName in subtitles) {
|
||||||
|
let subLang: langsData.LanguageItem;
|
||||||
|
if (this.deuSubStrings.includes(subName)) {
|
||||||
|
subLang = langsData.languages.find(a=>a.code == 'deu') as langsData.LanguageItem;
|
||||||
|
} else if (this.fraSubStrings.includes(subName)) {
|
||||||
|
subLang = langsData.languages.find(a=>a.code == 'fra') as langsData.LanguageItem;
|
||||||
|
} else {
|
||||||
|
console.error(`Language ${subName} not recognized, please report this.`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!options.dlsubs.includes(subLang.locale) && !options.dlsubs.includes('all')) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const sxData: Partial<sxItem> = {};
|
||||||
|
sxData.file = langsData.subsFile(fileName as string, subIndex+'', subLang, false, options.ccTag);
|
||||||
|
sxData.path = path.join(this.cfg.dir.content, sxData.file);
|
||||||
|
const split = sxData.path.split(path.sep).slice(0, -1);
|
||||||
|
split.forEach((val, ind, arr) => {
|
||||||
|
const isAbsolut = path.isAbsolute(sxData.path as string);
|
||||||
|
if (!fs.existsSync(path.join(isAbsolut ? '' : this.cfg.dir.content, ...arr.slice(0, ind), val)))
|
||||||
|
fs.mkdirSync(path.join(isAbsolut ? '' : this.cfg.dir.content, ...arr.slice(0, ind), val));
|
||||||
|
});
|
||||||
|
sxData.language = subLang;
|
||||||
|
if(options.dlsubs.includes('all') || options.dlsubs.includes(subLang.locale)) {
|
||||||
|
let subBody = '[Script Info]'
|
||||||
|
+ '\nScriptType:V4.00+'
|
||||||
|
+ '\nWrapStyle: 0'
|
||||||
|
+ '\nPlayResX: 1280'
|
||||||
|
+ '\nPlayResY: 720'
|
||||||
|
+ '\nScaledBorderAndShadow: yes'
|
||||||
|
+ ''
|
||||||
|
+ '\n[V4+ Styles]'
|
||||||
|
+ '\nFormat: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding'
|
||||||
|
+ `\nStyle: Default,${options.fontName ?? 'Arial'},${options.fontSize ?? 50},&H00FFFFFF,&H00FFFFFF,&H00000000,&H00000000,-1,0,0,0,100,100,0,0,1,1.95,0,2,0,0,70,0`
|
||||||
|
+ '\n[Events]'
|
||||||
|
+ '\nFormat: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text';
|
||||||
|
|
||||||
|
for (const sub of subtitles[subName]) {
|
||||||
|
const [start, end, text, lineAlign, positionAlign] =
|
||||||
|
[sub.startTime, sub.endTime, sub.text, sub.lineAlign, sub.positionAlign];
|
||||||
|
for (const subProp in sub) {
|
||||||
|
switch (subProp) {
|
||||||
|
case 'startTime':
|
||||||
|
case 'endTime':
|
||||||
|
case 'text':
|
||||||
|
case 'lineAlign':
|
||||||
|
case 'positionAlign':
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
console.warn(`json2ass: Unknown style: ${subProp}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const alignment = (this.posAlignMap[positionAlign] || 2) + (this.lineAlignMap[lineAlign] || 0);
|
||||||
|
const xtext = text
|
||||||
|
.replace(/ \\N$/g, '\\N')
|
||||||
|
.replace(/\\N$/, '')
|
||||||
|
.replace(/\r/g, '')
|
||||||
|
.replace(/\n/g, '\\N')
|
||||||
|
.replace(/\\N +/g, '\\N')
|
||||||
|
.replace(/ +\\N/g, '\\N')
|
||||||
|
.replace(/(\\N)+/g, '\\N')
|
||||||
|
.replace(/<b[^>]*>([^<]*)<\/b>/g, '{\\b1}$1{\\b0}')
|
||||||
|
.replace(/<i[^>]*>([^<]*)<\/i>/g, '{\\i1}$1{\\i0}')
|
||||||
|
.replace(/<u[^>]*>([^<]*)<\/u>/g, '{\\u1}$1{\\u0}')
|
||||||
|
.replace(/</g, '<')
|
||||||
|
.replace(/>/g, '>')
|
||||||
|
.replace(/&/g, '&')
|
||||||
|
.replace(/<[^>]>/g, '')
|
||||||
|
.replace(/\\N$/, '')
|
||||||
|
.replace(/ +$/, '');
|
||||||
|
subBody += `\nDialogue: 0,${this.convertToSSATimestamp(start)},${this.convertToSSATimestamp(end)},Default,,0,0,0,,${(alignment !== 2 ? `{\\a${alignment}}` : '')}${xtext}`;
|
||||||
|
}
|
||||||
|
sxData.title = `${subLang.language}`;
|
||||||
|
sxData.fonts = fontsData.assFonts(subBody) as Font[];
|
||||||
|
fs.writeFileSync(sxData.path, subBody);
|
||||||
|
console.info(`Subtitle converted: ${sxData.file}`);
|
||||||
|
files.push({
|
||||||
|
type: 'Subtitle',
|
||||||
|
...sxData as sxItem,
|
||||||
|
cc: false
|
||||||
|
});
|
||||||
|
}
|
||||||
|
subIndex++;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.warn('Couldn\'t find subtitles.');
|
||||||
|
}
|
||||||
|
} else{
|
||||||
|
console.info('Subtitles downloading skipped!');
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
error: dlFailed,
|
||||||
|
data: files,
|
||||||
|
fileName: fileName ? (path.isAbsolute(fileName) ? fileName : path.join(this.cfg.dir.content, fileName)) || './unknown' : './unknown'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public sleep(ms: number) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
setTimeout(resolve, ms);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
825
ao.ts
Normal file
825
ao.ts
Normal file
|
|
@ -0,0 +1,825 @@
|
||||||
|
// Package Info
|
||||||
|
import packageJson from './package.json';
|
||||||
|
|
||||||
|
// Node
|
||||||
|
import path from 'path';
|
||||||
|
import fs from 'fs-extra';
|
||||||
|
|
||||||
|
// Plugins
|
||||||
|
import shlp from 'sei-helper';
|
||||||
|
|
||||||
|
// Modules
|
||||||
|
import * as fontsData from './modules/module.fontsData';
|
||||||
|
import * as langsData from './modules/module.langsData';
|
||||||
|
import * as yamlCfg from './modules/module.cfg-loader';
|
||||||
|
import * as yargs from './modules/module.app-args';
|
||||||
|
import * as reqModule from './modules/module.fetch';
|
||||||
|
import Merger, { Font, MergerInput, SubtitleInput } from './modules/module.merger';
|
||||||
|
import getKeys, { canDecrypt } from './modules/widevine';
|
||||||
|
import streamdl, { M3U8Json } from './modules/hls-download';
|
||||||
|
import { exec } from './modules/sei-helper-fixes';
|
||||||
|
import { console } from './modules/log';
|
||||||
|
import { domain } from './modules/module.api-urls';
|
||||||
|
import { downloaded } from './modules/module.downloadArchive';
|
||||||
|
import parseSelect from './modules/module.parseSelect';
|
||||||
|
import parseFileName, { Variable } from './modules/module.filename';
|
||||||
|
import { AvailableFilenameVars } from './modules/module.args';
|
||||||
|
import { parse } from './modules/module.transform-mpd';
|
||||||
|
|
||||||
|
// Types
|
||||||
|
import { ServiceClass } from './@types/serviceClassInterface';
|
||||||
|
import { AuthData, AuthResponse, SearchData, SearchResponse, SearchResponseItem } from './@types/messageHandler';
|
||||||
|
import { AOSearchResult, AnimeOnegaiSearch } from './@types/animeOnegaiSearch';
|
||||||
|
import { AnimeOnegaiSeries } from './@types/animeOnegaiSeries';
|
||||||
|
import { AnimeOnegaiSeasons, Episode } from './@types/animeOnegaiSeasons';
|
||||||
|
import { DownloadedMedia } from './@types/hidiveTypes';
|
||||||
|
import { AnimeOnegaiStream } from './@types/animeOnegaiStream';
|
||||||
|
import { sxItem } from './crunchy';
|
||||||
|
|
||||||
|
type parsedMultiDubDownload = {
|
||||||
|
data: {
|
||||||
|
lang: string,
|
||||||
|
videoId: string
|
||||||
|
episode: Episode
|
||||||
|
}[],
|
||||||
|
seriesTitle: string,
|
||||||
|
seasonTitle: string,
|
||||||
|
episodeTitle: string,
|
||||||
|
episodeNumber: number,
|
||||||
|
seasonNumber: number,
|
||||||
|
seriesID: number,
|
||||||
|
seasonID: number,
|
||||||
|
image: string,
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class AnimeOnegai implements ServiceClass {
|
||||||
|
public cfg: yamlCfg.ConfigObject;
|
||||||
|
private token: Record<string, any>;
|
||||||
|
private req: reqModule.Req;
|
||||||
|
public locale: string;
|
||||||
|
public jpnStrings: string[] = [
|
||||||
|
'Japonés con Subtítulos en Español',
|
||||||
|
'Japonés con Subtítulos en Portugués',
|
||||||
|
'Japonês com legendas em espanhol',
|
||||||
|
'Japonês com legendas em português',
|
||||||
|
'Japonés'
|
||||||
|
];
|
||||||
|
public spaStrings: string[] = [
|
||||||
|
'Doblaje en Español',
|
||||||
|
'Dublagem em espanhol',
|
||||||
|
'Español',
|
||||||
|
];
|
||||||
|
public porStrings: string[] = [
|
||||||
|
'Doblaje en Portugués',
|
||||||
|
'Dublagem em português'
|
||||||
|
];
|
||||||
|
private defaultOptions: RequestInit = {
|
||||||
|
'headers': {
|
||||||
|
'origin': 'https://www.animeonegai.com',
|
||||||
|
'referer': 'https://www.animeonegai.com/',
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
constructor(private debug = false) {
|
||||||
|
this.cfg = yamlCfg.loadCfg();
|
||||||
|
this.token = yamlCfg.loadAOToken();
|
||||||
|
this.req = new reqModule.Req(domain, debug, false, 'ao');
|
||||||
|
this.locale = 'es';
|
||||||
|
}
|
||||||
|
|
||||||
|
public async cli() {
|
||||||
|
console.info(`\n=== Multi Downloader NX ${packageJson.version} ===\n`);
|
||||||
|
const argv = yargs.appArgv(this.cfg.cli);
|
||||||
|
if (['pt', 'es'].includes(argv.locale))
|
||||||
|
this.locale = argv.locale;
|
||||||
|
if (argv.debug)
|
||||||
|
this.debug = true;
|
||||||
|
|
||||||
|
// load binaries
|
||||||
|
this.cfg.bin = await yamlCfg.loadBinCfg();
|
||||||
|
if (argv.allDubs) {
|
||||||
|
argv.dubLang = langsData.dubLanguageCodes;
|
||||||
|
}
|
||||||
|
if (argv.auth) {
|
||||||
|
//Authenticate
|
||||||
|
await this.doAuth({
|
||||||
|
username: argv.username ?? await shlp.question('[Q] LOGIN/EMAIL'),
|
||||||
|
password: argv.password ?? await shlp.question('[Q] PASSWORD ')
|
||||||
|
});
|
||||||
|
} else if (argv.search && argv.search.length > 2) {
|
||||||
|
//Search
|
||||||
|
await this.doSearch({ ...argv, search: argv.search as string });
|
||||||
|
} else if (argv.s && !isNaN(parseInt(argv.s,10)) && parseInt(argv.s,10) > 0) {
|
||||||
|
const selected = await this.selectShow(parseInt(argv.s), argv.e, argv.but, argv.all, argv);
|
||||||
|
if (selected.isOk) {
|
||||||
|
for (const select of selected.value) {
|
||||||
|
if (!(await this.downloadEpisode(select, {...argv, skipsubs: false}))) {
|
||||||
|
console.error(`Unable to download selected episode ${select.episodeNumber}`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
} else if (argv.token) {
|
||||||
|
this.token = {token: argv.token};
|
||||||
|
yamlCfg.saveAOToken(this.token);
|
||||||
|
console.info('Saved token');
|
||||||
|
} else {
|
||||||
|
console.info('No option selected or invalid value entered. Try --help.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async doSearch(data: SearchData): Promise<SearchResponse> {
|
||||||
|
const searchReq = await this.req.getData(`https://api.animeonegai.com/v1/search/algolia/${encodeURIComponent(data.search)}?lang=${this.locale}`, this.defaultOptions);
|
||||||
|
if (!searchReq.ok || !searchReq.res) {
|
||||||
|
console.error('Search FAILED!');
|
||||||
|
return { isOk: false, reason: new Error('Search failed. No more information provided') };
|
||||||
|
}
|
||||||
|
const searchData = await searchReq.res.json() as AnimeOnegaiSearch;
|
||||||
|
const searchItems: AOSearchResult[] = [];
|
||||||
|
console.info('Search Results:');
|
||||||
|
for (const hit of searchData.list) {
|
||||||
|
searchItems.push(hit);
|
||||||
|
let fullType: string;
|
||||||
|
if (hit.asset_type == 2) {
|
||||||
|
fullType = `S.${hit.ID}`;
|
||||||
|
} else if (hit.asset_type == 1) {
|
||||||
|
fullType = `E.${hit.ID}`;
|
||||||
|
} else {
|
||||||
|
fullType = 'Unknown';
|
||||||
|
console.warn(`Unknown asset type ${hit.asset_type}, please report this.`);
|
||||||
|
}
|
||||||
|
console.log(`[${fullType}] ${hit.title}`);
|
||||||
|
}
|
||||||
|
return { isOk: true, value: searchItems.filter(a => a.asset_type == 2).flatMap((a): SearchResponseItem => {
|
||||||
|
return {
|
||||||
|
id: a.ID+'',
|
||||||
|
image: a.poster ?? '/notFound.png',
|
||||||
|
name: a.title,
|
||||||
|
rating: a.likes,
|
||||||
|
desc: a.description
|
||||||
|
};
|
||||||
|
})};
|
||||||
|
}
|
||||||
|
|
||||||
|
public async doAuth(data: AuthData): Promise<AuthResponse> {
|
||||||
|
data;
|
||||||
|
console.error('Authentication not possible, manual authentication required due to recaptcha. In order to login use the --token flag. You can get the token by logging into the website, and opening the dev console and running the command "localStorage.ott_token"');
|
||||||
|
return { isOk: false, reason: new Error('Authentication not possible, manual authentication required do to recaptcha.') };
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getShow(id: number) {
|
||||||
|
const getSeriesData = await this.req.getData(`https://api.animeonegai.com/v1/asset/${id}?lang=${this.locale}`, this.defaultOptions);
|
||||||
|
if (!getSeriesData.ok || !getSeriesData.res) {
|
||||||
|
console.error('Failed to get Show Data');
|
||||||
|
return { isOk: false };
|
||||||
|
}
|
||||||
|
const seriesData = await getSeriesData.res.json() as AnimeOnegaiSeries;
|
||||||
|
|
||||||
|
const getSeasonData = await this.req.getData(`https://api.animeonegai.com/v1/asset/content/${id}?lang=${this.locale}`, this.defaultOptions);
|
||||||
|
if (!getSeasonData.ok || !getSeasonData.res) {
|
||||||
|
console.error('Failed to get Show Data');
|
||||||
|
return { isOk: false };
|
||||||
|
}
|
||||||
|
const seasonData = await getSeasonData.res.json() as AnimeOnegaiSeasons[];
|
||||||
|
|
||||||
|
return { isOk: true, data: seriesData, seasons: seasonData };
|
||||||
|
}
|
||||||
|
|
||||||
|
public async listShow(id: number, outputEpisode: boolean = true) {
|
||||||
|
const series = await this.getShow(id);
|
||||||
|
if (!series.isOk || !series.data) {
|
||||||
|
console.error('Failed to list series data: Failed to get series');
|
||||||
|
return { isOk: false };
|
||||||
|
}
|
||||||
|
console.info(`[S.${series.data.ID}] ${series.data.title} (${series.seasons.length} Seasons)`);
|
||||||
|
if (series.seasons.length === 0 && series.data.asset_type !== 1) {
|
||||||
|
console.info(' No Seasons found!');
|
||||||
|
return { isOk: false };
|
||||||
|
}
|
||||||
|
const episodes: { [key: string]: (Episode & { lang?: string })[] } = {};
|
||||||
|
for (const season of series.seasons) {
|
||||||
|
let lang: string | undefined = undefined;
|
||||||
|
if (this.jpnStrings.includes(season.name.trim())) lang = 'ja';
|
||||||
|
else if (this.porStrings.includes(season.name.trim())) lang = 'pt';
|
||||||
|
else if (this.spaStrings.includes(season.name.trim())) lang = 'es';
|
||||||
|
else {lang = 'unknown';console.error(`Language (${season.name.trim()}) not known, please report this!`);}
|
||||||
|
for (const episode of season.list) {
|
||||||
|
if (!episodes[episode.number]) {
|
||||||
|
episodes[episode.number] = [];
|
||||||
|
}
|
||||||
|
/*if (!episodes[episode.number].find(a=>a.lang == lang))*/ episodes[episode.number].push({...episode, lang});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//Item is movie, lets define it manually
|
||||||
|
if (series.data.asset_type === 1 && series.seasons.length === 0) {
|
||||||
|
let lang: string | undefined;
|
||||||
|
if (this.jpnStrings.some(str => series.data.title.includes(str))) lang = 'ja';
|
||||||
|
else if (this.porStrings.some(str => series.data.title.includes(str))) lang = 'pt';
|
||||||
|
else if (this.spaStrings.some(str => series.data.title.includes(str))) lang = 'es';
|
||||||
|
else {lang = 'unknown';console.error('Language could not be parsed from movie title, please report this!');}
|
||||||
|
episodes[1] = [{
|
||||||
|
'video_entry': series.data.video_entry,
|
||||||
|
'number': 1,
|
||||||
|
'season_id': 1,
|
||||||
|
'name': series.data.title,
|
||||||
|
'ID': series.data.ID,
|
||||||
|
'CreatedAt': series.data.CreatedAt,
|
||||||
|
'DeletedAt': series.data.DeletedAt,
|
||||||
|
'UpdatedAt': series.data.UpdatedAt,
|
||||||
|
'active': series.data.active,
|
||||||
|
'description': series.data.description,
|
||||||
|
'age_restriction': series.data.age_restriction,
|
||||||
|
'asset_id': series.data.ID,
|
||||||
|
'ending': null,
|
||||||
|
'entry': series.data.entry,
|
||||||
|
'stream_url': series.data.stream_url,
|
||||||
|
'skip_intro': null,
|
||||||
|
'thumbnail': series.data.bg,
|
||||||
|
'open_free': false,
|
||||||
|
lang
|
||||||
|
}]; // as unknown as (Episode & { lang?: string })[];
|
||||||
|
// The above needs to be uncommented if the episode number should be M1 instead of 1
|
||||||
|
}
|
||||||
|
//Enable to output episodes seperate from selection
|
||||||
|
if (outputEpisode) {
|
||||||
|
for (const episodeKey in episodes) {
|
||||||
|
const episode = episodes[episodeKey][0];
|
||||||
|
const langs = Array.from(new Set(episodes[episodeKey].map(a=>a.lang)));
|
||||||
|
console.info(` [E.${episode.ID}] E${episode.number} - ${episode.name} (${langs.map(a=>{
|
||||||
|
if (a) return langsData.languages.find(b=>b.ao_locale === a)?.name;
|
||||||
|
return 'Unknown';
|
||||||
|
}).join(', ')})`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { isOk: true, value: episodes, series: series };
|
||||||
|
}
|
||||||
|
|
||||||
|
public async selectShow(id: number, e: string | undefined, but: boolean, all: boolean, options: yargs.ArgvType) {
|
||||||
|
const getShowData = await this.listShow(id, false);
|
||||||
|
if (!getShowData.isOk || !getShowData.value) {
|
||||||
|
return { isOk: false, value: [] };
|
||||||
|
}
|
||||||
|
//const showData = getShowData.value;
|
||||||
|
const doEpsFilter = parseSelect(e as string);
|
||||||
|
// build selected episodes
|
||||||
|
const selEpsArr: parsedMultiDubDownload[] = [];
|
||||||
|
const episodes = getShowData.value;
|
||||||
|
const seasonNumberTitleParse = getShowData.series.data.title.match(/\d+/);
|
||||||
|
const seasonNumber = seasonNumberTitleParse ? parseInt(seasonNumberTitleParse[0]) : 1;
|
||||||
|
for (const episodeKey in getShowData.value) {
|
||||||
|
const episode = episodes[episodeKey][0];
|
||||||
|
const selectedLangs: string[] = [];
|
||||||
|
const selected: {
|
||||||
|
lang: string,
|
||||||
|
videoId: string
|
||||||
|
episode: Episode
|
||||||
|
}[] = [];
|
||||||
|
for (const episode of episodes[episodeKey]) {
|
||||||
|
const lang = langsData.languages.find(a=>a.ao_locale === episode.lang);
|
||||||
|
let isSelected = false;
|
||||||
|
if (typeof selected.find(a=>a.lang == episode.lang) == 'undefined') {
|
||||||
|
if (options.dubLang.includes(lang?.code ?? 'Unknown')) {
|
||||||
|
if ((but && !doEpsFilter.isSelected([episode.number+'', episode.ID+''])) || all || (!but && doEpsFilter.isSelected([episode.number+'', episode.ID+'']))) {
|
||||||
|
isSelected = true;
|
||||||
|
selected.push({lang: episode.lang as string, videoId: episode.video_entry, episode: episode });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const selectedLang = isSelected ? `✓ ${lang?.name ?? 'Unknown'}` : `${lang?.name ?? 'Unknown'}`;
|
||||||
|
if (!selectedLangs.includes(selectedLang)) {
|
||||||
|
selectedLangs.push(selectedLang);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (selected.length > 0) {
|
||||||
|
selEpsArr.push({
|
||||||
|
'data': selected,
|
||||||
|
'seasonNumber': seasonNumber,
|
||||||
|
'episodeNumber': episode.number,
|
||||||
|
'episodeTitle': episode.name,
|
||||||
|
'image': episode.thumbnail,
|
||||||
|
'seasonID': episode.season_id,
|
||||||
|
'seasonTitle': getShowData.series.data.title,
|
||||||
|
'seriesTitle': getShowData.series.data.title,
|
||||||
|
'seriesID': getShowData.series.data.ID
|
||||||
|
});
|
||||||
|
}
|
||||||
|
console.info(` [S${seasonNumber}E${episode.number}] - ${episode.name} (${selectedLangs.join(', ')})`);
|
||||||
|
}
|
||||||
|
return { isOk: true, value: selEpsArr, showData: getShowData.series };
|
||||||
|
}
|
||||||
|
|
||||||
|
public async downloadEpisode(data: parsedMultiDubDownload, options: yargs.ArgvType): Promise<boolean> {
|
||||||
|
const res = await this.downloadMediaList(data, options);
|
||||||
|
if (res === undefined || res.error) {
|
||||||
|
return false;
|
||||||
|
} else {
|
||||||
|
if (!options.skipmux) {
|
||||||
|
await this.muxStreams(res.data, { ...options, output: res.fileName });
|
||||||
|
} else {
|
||||||
|
console.info('Skipping mux');
|
||||||
|
}
|
||||||
|
downloaded({
|
||||||
|
service: 'ao',
|
||||||
|
type: 's'
|
||||||
|
}, data.seasonID+'', [data.episodeNumber+'']);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async muxStreams(data: DownloadedMedia[], options: yargs.ArgvType) {
|
||||||
|
this.cfg.bin = await yamlCfg.loadBinCfg();
|
||||||
|
let hasAudioStreams = false;
|
||||||
|
if (options.novids || data.filter(a => a.type === 'Video').length === 0)
|
||||||
|
return console.info('Skip muxing since no vids are downloaded');
|
||||||
|
if (data.some(a => a.type === 'Audio')) {
|
||||||
|
hasAudioStreams = true;
|
||||||
|
}
|
||||||
|
const merger = new Merger({
|
||||||
|
onlyVid: hasAudioStreams ? data.filter(a => a.type === 'Video').map((a) : MergerInput => {
|
||||||
|
if (a.type === 'Subtitle')
|
||||||
|
throw new Error('Never');
|
||||||
|
return {
|
||||||
|
lang: a.lang,
|
||||||
|
path: a.path,
|
||||||
|
};
|
||||||
|
}) : [],
|
||||||
|
skipSubMux: options.skipSubMux,
|
||||||
|
inverseTrackOrder: false,
|
||||||
|
keepAllVideos: options.keepAllVideos,
|
||||||
|
onlyAudio: hasAudioStreams ? data.filter(a => a.type === 'Audio').map((a) : MergerInput => {
|
||||||
|
if (a.type === 'Subtitle')
|
||||||
|
throw new Error('Never');
|
||||||
|
return {
|
||||||
|
lang: a.lang,
|
||||||
|
path: a.path,
|
||||||
|
};
|
||||||
|
}) : [],
|
||||||
|
output: `${options.output}.${options.mp4 ? 'mp4' : 'mkv'}`,
|
||||||
|
subtitles: data.filter(a => a.type === 'Subtitle').map((a) : SubtitleInput => {
|
||||||
|
if (a.type === 'Video')
|
||||||
|
throw new Error('Never');
|
||||||
|
if (a.type === 'Audio')
|
||||||
|
throw new Error('Never');
|
||||||
|
return {
|
||||||
|
file: a.path,
|
||||||
|
language: a.language,
|
||||||
|
closedCaption: a.cc
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
simul: data.filter(a => a.type === 'Video').map((a) : boolean => {
|
||||||
|
if (a.type === 'Subtitle')
|
||||||
|
throw new Error('Never');
|
||||||
|
return !a.uncut as boolean;
|
||||||
|
})[0],
|
||||||
|
fonts: Merger.makeFontsList(this.cfg.dir.fonts, data.filter(a => a.type === 'Subtitle') as sxItem[]),
|
||||||
|
videoAndAudio: hasAudioStreams ? [] : data.filter(a => a.type === 'Video').map((a) : MergerInput => {
|
||||||
|
if (a.type === 'Subtitle')
|
||||||
|
throw new Error('Never');
|
||||||
|
return {
|
||||||
|
lang: a.lang,
|
||||||
|
path: a.path,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
videoTitle: options.videoTitle,
|
||||||
|
options: {
|
||||||
|
ffmpeg: options.ffmpegOptions,
|
||||||
|
mkvmerge: options.mkvmergeOptions
|
||||||
|
},
|
||||||
|
defaults: {
|
||||||
|
audio: options.defaultAudio,
|
||||||
|
sub: options.defaultSub
|
||||||
|
},
|
||||||
|
ccTag: options.ccTag
|
||||||
|
});
|
||||||
|
const bin = Merger.checkMerger(this.cfg.bin, options.mp4, options.forceMuxer);
|
||||||
|
// collect fonts info
|
||||||
|
// mergers
|
||||||
|
let isMuxed = false;
|
||||||
|
if (options.syncTiming) {
|
||||||
|
await merger.createDelays();
|
||||||
|
}
|
||||||
|
if (bin.MKVmerge) {
|
||||||
|
await merger.merge('mkvmerge', bin.MKVmerge);
|
||||||
|
isMuxed = true;
|
||||||
|
} else if (bin.FFmpeg) {
|
||||||
|
await merger.merge('ffmpeg', bin.FFmpeg);
|
||||||
|
isMuxed = true;
|
||||||
|
} else{
|
||||||
|
console.info('\nDone!\n');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (isMuxed && !options.nocleanup)
|
||||||
|
merger.cleanUp();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async downloadMediaList(medias: parsedMultiDubDownload, options: yargs.ArgvType) : Promise<{
|
||||||
|
data: DownloadedMedia[],
|
||||||
|
fileName: string,
|
||||||
|
error: boolean
|
||||||
|
} | undefined> {
|
||||||
|
if(!this.token.token){
|
||||||
|
console.error('Authentication required!');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.cfg.bin.ffmpeg)
|
||||||
|
this.cfg.bin = await yamlCfg.loadBinCfg();
|
||||||
|
|
||||||
|
let mediaName = '...';
|
||||||
|
let fileName;
|
||||||
|
const variables: Variable[] = [];
|
||||||
|
if(medias.seasonTitle && medias.episodeNumber && medias.episodeTitle){
|
||||||
|
mediaName = `${medias.seasonTitle} - ${medias.episodeNumber} - ${medias.episodeTitle}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const files: DownloadedMedia[] = [];
|
||||||
|
|
||||||
|
let subIndex = 0;
|
||||||
|
let dlFailed = false;
|
||||||
|
let dlVideoOnce = false; // Variable to save if best selected video quality was downloaded
|
||||||
|
|
||||||
|
for (const media of medias.data) {
|
||||||
|
console.info(`Requesting: [E.${media.episode.ID}] ${mediaName}`);
|
||||||
|
|
||||||
|
const AuthHeaders = {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${this.token.token}`,
|
||||||
|
'Referer': 'https://www.animeonegai.com/',
|
||||||
|
'Origin': 'https://www.animeonegai.com',
|
||||||
|
'Referrer-Policy': 'strict-origin-when-cross-origin',
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const playbackReq = await this.req.getData(`https://api.animeonegai.com/v1/media/${media.videoId}?lang=${this.locale}`, AuthHeaders);
|
||||||
|
if(!playbackReq.ok || !playbackReq.res){
|
||||||
|
console.error('Request Stream URLs FAILED!');
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
const streamData = await playbackReq.res.json() as AnimeOnegaiStream;
|
||||||
|
|
||||||
|
variables.push(...([
|
||||||
|
['title', medias.episodeTitle, true],
|
||||||
|
['episode', medias.episodeNumber, false],
|
||||||
|
['service', 'AO', false],
|
||||||
|
['seriesTitle', medias.seriesTitle, true],
|
||||||
|
['showTitle', medias.seasonTitle, true],
|
||||||
|
['season', medias.seasonNumber, false]
|
||||||
|
] as [AvailableFilenameVars, string|number, boolean][]).map((a): Variable => {
|
||||||
|
return {
|
||||||
|
name: a[0],
|
||||||
|
replaceWith: a[1],
|
||||||
|
type: typeof a[1],
|
||||||
|
sanitize: a[2]
|
||||||
|
} as Variable;
|
||||||
|
}));
|
||||||
|
|
||||||
|
if (!canDecrypt) {
|
||||||
|
console.warn('Decryption not enabled!');
|
||||||
|
}
|
||||||
|
|
||||||
|
const lang = langsData.languages.find(a=>a.ao_locale == media.lang) as langsData.LanguageItem;
|
||||||
|
if (!lang) {
|
||||||
|
console.error(`Unable to find language for code ${media.lang}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let tsFile = undefined;
|
||||||
|
|
||||||
|
if (!streamData.dash) {
|
||||||
|
console.error('You don\'t have access to download this content');
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.info('Playlists URL: %s', streamData.dash);
|
||||||
|
|
||||||
|
if(!dlFailed && !(options.novids && options.noaudio)){
|
||||||
|
const streamPlaylistsReq = await this.req.getData(streamData.dash, AuthHeaders);
|
||||||
|
if(!streamPlaylistsReq.ok || !streamPlaylistsReq.res){
|
||||||
|
console.error('CAN\'T FETCH VIDEO PLAYLISTS!');
|
||||||
|
dlFailed = true;
|
||||||
|
} else {
|
||||||
|
const streamPlaylistBody = (await streamPlaylistsReq.res.text()).replace(/<BaseURL>(.*?)<\/BaseURL>/g, `<BaseURL>${streamData.dash.split('/dash/')[0]}/dash/$1</BaseURL>`);
|
||||||
|
//Parse MPD Playlists
|
||||||
|
const streamPlaylists = await parse(streamPlaylistBody, lang as langsData.LanguageItem, streamData.dash.split('/dash/')[0]+'/dash/');
|
||||||
|
|
||||||
|
//Get name of CDNs/Servers
|
||||||
|
const streamServers = Object.keys(streamPlaylists);
|
||||||
|
|
||||||
|
options.x = options.x > streamServers.length ? 1 : options.x;
|
||||||
|
|
||||||
|
const selectedServer = streamServers[options.x - 1];
|
||||||
|
const selectedList = streamPlaylists[selectedServer];
|
||||||
|
|
||||||
|
//set Video Qualities
|
||||||
|
const videos = selectedList.video.map(item => {
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
resolutionText: `${item.quality.width}x${item.quality.height} (${Math.round(item.bandwidth/1024)}KiB/s)`
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const audios = selectedList.audio.map(item => {
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
resolutionText: `${Math.round(item.bandwidth/1024)}kB/s`
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
videos.sort((a, b) => {
|
||||||
|
return a.quality.width - b.quality.width;
|
||||||
|
});
|
||||||
|
|
||||||
|
audios.sort((a, b) => {
|
||||||
|
return a.bandwidth - b.bandwidth;
|
||||||
|
});
|
||||||
|
|
||||||
|
let chosenVideoQuality = options.q === 0 ? videos.length : options.q;
|
||||||
|
if(chosenVideoQuality > videos.length) {
|
||||||
|
console.warn(`The requested quality of ${options.q} is greater than the maximum ${videos.length}.\n[WARN] Therefor the maximum will be capped at ${videos.length}.`);
|
||||||
|
chosenVideoQuality = videos.length;
|
||||||
|
}
|
||||||
|
chosenVideoQuality--;
|
||||||
|
|
||||||
|
let chosenAudioQuality = options.q === 0 ? audios.length : options.q;
|
||||||
|
if(chosenAudioQuality > audios.length) {
|
||||||
|
chosenAudioQuality = audios.length;
|
||||||
|
}
|
||||||
|
chosenAudioQuality--;
|
||||||
|
|
||||||
|
const chosenVideoSegments = videos[chosenVideoQuality];
|
||||||
|
const chosenAudioSegments = audios[chosenAudioQuality];
|
||||||
|
|
||||||
|
console.info(`Servers available:\n\t${streamServers.join('\n\t')}`);
|
||||||
|
console.info(`Available Video Qualities:\n\t${videos.map((a, ind) => `[${ind+1}] ${a.resolutionText}`).join('\n\t')}`);
|
||||||
|
console.info(`Available Audio Qualities:\n\t${audios.map((a, ind) => `[${ind+1}] ${a.resolutionText}`).join('\n\t')}`);
|
||||||
|
|
||||||
|
variables.push({
|
||||||
|
name: 'height',
|
||||||
|
type: 'number',
|
||||||
|
replaceWith: chosenVideoSegments.quality.height
|
||||||
|
}, {
|
||||||
|
name: 'width',
|
||||||
|
type: 'number',
|
||||||
|
replaceWith: chosenVideoSegments.quality.width
|
||||||
|
});
|
||||||
|
|
||||||
|
console.info(`Selected quality: \n\tVideo: ${chosenVideoSegments.resolutionText}\n\tAudio: ${chosenAudioSegments.resolutionText}\n\tServer: ${selectedServer}`);
|
||||||
|
//console.info('Stream URL:', chosenVideoSegments.segments[0].uri);
|
||||||
|
// TODO check filename
|
||||||
|
fileName = parseFileName(options.fileName, variables, options.numbers, options.override).join(path.sep);
|
||||||
|
const outFile = parseFileName(options.fileName + '.' + lang.name, variables, options.numbers, options.override).join(path.sep);
|
||||||
|
const tempFile = parseFileName(`temp-${media.videoId}`, variables, options.numbers, options.override).join(path.sep);
|
||||||
|
const tempTsFile = path.isAbsolute(tempFile as string) ? tempFile : path.join(this.cfg.dir.content, tempFile);
|
||||||
|
|
||||||
|
let [audioDownloaded, videoDownloaded] = [false, false];
|
||||||
|
|
||||||
|
// When best selected video quality is already downloaded
|
||||||
|
if(dlVideoOnce && options.dlVideoOnce) {
|
||||||
|
console.info('Already downloaded video, skipping video download...');
|
||||||
|
} else if (options.novids) {
|
||||||
|
console.info('Skipping video download...');
|
||||||
|
} else {
|
||||||
|
//Download Video
|
||||||
|
const totalParts = chosenVideoSegments.segments.length;
|
||||||
|
const mathParts = Math.ceil(totalParts / options.partsize);
|
||||||
|
const mathMsg = `(${mathParts}*${options.partsize})`;
|
||||||
|
console.info('Total parts in video stream:', totalParts, mathMsg);
|
||||||
|
tsFile = path.isAbsolute(outFile as string) ? outFile : path.join(this.cfg.dir.content, outFile);
|
||||||
|
const split = outFile.split(path.sep).slice(0, -1);
|
||||||
|
split.forEach((val, ind, arr) => {
|
||||||
|
const isAbsolut = path.isAbsolute(outFile as string);
|
||||||
|
if (!fs.existsSync(path.join(isAbsolut ? '' : this.cfg.dir.content, ...arr.slice(0, ind), val)))
|
||||||
|
fs.mkdirSync(path.join(isAbsolut ? '' : this.cfg.dir.content, ...arr.slice(0, ind), val));
|
||||||
|
});
|
||||||
|
const videoJson: M3U8Json = {
|
||||||
|
segments: chosenVideoSegments.segments
|
||||||
|
};
|
||||||
|
try {
|
||||||
|
const videoDownload = await new streamdl({
|
||||||
|
output: chosenVideoSegments.pssh ? `${tempTsFile}.video.enc.mp4` : `${tsFile}.video.mp4`,
|
||||||
|
timeout: options.timeout,
|
||||||
|
m3u8json: videoJson,
|
||||||
|
// baseurl: chunkPlaylist.baseUrl,
|
||||||
|
threads: options.partsize,
|
||||||
|
fsRetryTime: options.fsRetryTime * 1000,
|
||||||
|
override: options.force,
|
||||||
|
callback: options.callbackMaker ? options.callbackMaker({
|
||||||
|
fileName: `${path.isAbsolute(outFile) ? outFile.slice(this.cfg.dir.content.length) : outFile}`,
|
||||||
|
image: medias.image,
|
||||||
|
parent: {
|
||||||
|
title: medias.seasonTitle
|
||||||
|
},
|
||||||
|
title: medias.episodeTitle,
|
||||||
|
language: lang
|
||||||
|
}) : undefined
|
||||||
|
}).download();
|
||||||
|
if(!videoDownload.ok){
|
||||||
|
console.error(`DL Stats: ${JSON.stringify(videoDownload.parts)}\n`);
|
||||||
|
dlFailed = true;
|
||||||
|
} else {
|
||||||
|
dlVideoOnce = true;
|
||||||
|
videoDownloaded = true;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
dlFailed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (chosenAudioSegments && !options.noaudio) {
|
||||||
|
//Download Audio (if available)
|
||||||
|
const totalParts = chosenAudioSegments.segments.length;
|
||||||
|
const mathParts = Math.ceil(totalParts / options.partsize);
|
||||||
|
const mathMsg = `(${mathParts}*${options.partsize})`;
|
||||||
|
console.info('Total parts in audio stream:', totalParts, mathMsg);
|
||||||
|
tsFile = path.isAbsolute(outFile as string) ? outFile : path.join(this.cfg.dir.content, outFile);
|
||||||
|
const split = outFile.split(path.sep).slice(0, -1);
|
||||||
|
split.forEach((val, ind, arr) => {
|
||||||
|
const isAbsolut = path.isAbsolute(outFile as string);
|
||||||
|
if (!fs.existsSync(path.join(isAbsolut ? '' : this.cfg.dir.content, ...arr.slice(0, ind), val)))
|
||||||
|
fs.mkdirSync(path.join(isAbsolut ? '' : this.cfg.dir.content, ...arr.slice(0, ind), val));
|
||||||
|
});
|
||||||
|
const audioJson: M3U8Json = {
|
||||||
|
segments: chosenAudioSegments.segments
|
||||||
|
};
|
||||||
|
try {
|
||||||
|
const audioDownload = await new streamdl({
|
||||||
|
output: chosenAudioSegments.pssh ? `${tempTsFile}.audio.enc.mp4` : `${tsFile}.audio.mp4`,
|
||||||
|
timeout: options.timeout,
|
||||||
|
m3u8json: audioJson,
|
||||||
|
// baseurl: chunkPlaylist.baseUrl,
|
||||||
|
threads: options.partsize,
|
||||||
|
fsRetryTime: options.fsRetryTime * 1000,
|
||||||
|
override: options.force,
|
||||||
|
callback: options.callbackMaker ? options.callbackMaker({
|
||||||
|
fileName: `${path.isAbsolute(outFile) ? outFile.slice(this.cfg.dir.content.length) : outFile}`,
|
||||||
|
image: medias.image,
|
||||||
|
parent: {
|
||||||
|
title: medias.seasonTitle
|
||||||
|
},
|
||||||
|
title: medias.episodeTitle,
|
||||||
|
language: lang
|
||||||
|
}) : undefined
|
||||||
|
}).download();
|
||||||
|
if(!audioDownload.ok){
|
||||||
|
console.error(`DL Stats: ${JSON.stringify(audioDownload.parts)}\n`);
|
||||||
|
dlFailed = true;
|
||||||
|
} else {
|
||||||
|
audioDownloaded = true;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
dlFailed = true;
|
||||||
|
}
|
||||||
|
} else if (options.noaudio) {
|
||||||
|
console.info('Skipping audio download...');
|
||||||
|
}
|
||||||
|
|
||||||
|
//Handle Decryption if needed
|
||||||
|
if ((chosenVideoSegments.pssh || chosenAudioSegments.pssh) && (videoDownloaded || audioDownloaded)) {
|
||||||
|
console.info('Decryption Needed, attempting to decrypt');
|
||||||
|
const encryptionKeys = await getKeys(chosenVideoSegments.pssh, streamData.widevine_proxy, {});
|
||||||
|
if (encryptionKeys.length == 0) {
|
||||||
|
console.error('Failed to get encryption keys');
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
/*const keys = {} as Record<string, string>;
|
||||||
|
encryptionKeys.forEach(function(key) {
|
||||||
|
keys[key.kid] = key.key;
|
||||||
|
});*/
|
||||||
|
|
||||||
|
if (this.cfg.bin.mp4decrypt) {
|
||||||
|
const commandBase = `--show-progress --key ${encryptionKeys[1].kid}:${encryptionKeys[1].key} `;
|
||||||
|
const commandVideo = commandBase+`"${tempTsFile}.video.enc.mp4" "${tempTsFile}.video.mp4"`;
|
||||||
|
const commandAudio = commandBase+`"${tempTsFile}.audio.enc.mp4" "${tempTsFile}.audio.mp4"`;
|
||||||
|
|
||||||
|
if (videoDownloaded) {
|
||||||
|
console.info('Started decrypting video');
|
||||||
|
const decryptVideo = exec('mp4decrypt', `"${this.cfg.bin.mp4decrypt}"`, commandVideo);
|
||||||
|
if (!decryptVideo.isOk) {
|
||||||
|
console.error(decryptVideo.err);
|
||||||
|
console.error(`Decryption failed with exit code ${decryptVideo.err.code}`);
|
||||||
|
fs.renameSync(`${tempTsFile}.video.enc.mp4`, `${tsFile}.video.enc.mp4`);
|
||||||
|
return undefined;
|
||||||
|
} else {
|
||||||
|
console.info('Decryption done for video');
|
||||||
|
if (!options.nocleanup) {
|
||||||
|
fs.removeSync(`${tempTsFile}.video.enc.mp4`);
|
||||||
|
}
|
||||||
|
fs.renameSync(`${tempTsFile}.video.mp4`, `${tsFile}.video.mp4`);
|
||||||
|
files.push({
|
||||||
|
type: 'Video',
|
||||||
|
path: `${tsFile}.video.mp4`,
|
||||||
|
lang: lang
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (audioDownloaded) {
|
||||||
|
console.info('Started decrypting audio');
|
||||||
|
const decryptAudio = exec('mp4decrypt', `"${this.cfg.bin.mp4decrypt}"`, commandAudio);
|
||||||
|
if (!decryptAudio.isOk) {
|
||||||
|
console.error(decryptAudio.err);
|
||||||
|
console.error(`Decryption failed with exit code ${decryptAudio.err.code}`);
|
||||||
|
fs.renameSync(`${tempTsFile}.audio.enc.mp4`, `${tsFile}.audio.enc.mp4`);
|
||||||
|
return undefined;
|
||||||
|
} else {
|
||||||
|
if (!options.nocleanup) {
|
||||||
|
fs.removeSync(`${tempTsFile}.audio.enc.mp4`);
|
||||||
|
}
|
||||||
|
fs.renameSync(`${tempTsFile}.audio.mp4`, `${tsFile}.audio.mp4`);
|
||||||
|
files.push({
|
||||||
|
type: 'Audio',
|
||||||
|
path: `${tsFile}.audio.mp4`,
|
||||||
|
lang: lang
|
||||||
|
});
|
||||||
|
console.info('Decryption done for audio');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.warn('mp4decrypt not found, files need decryption. Decryption Keys:', encryptionKeys);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (videoDownloaded) {
|
||||||
|
files.push({
|
||||||
|
type: 'Video',
|
||||||
|
path: `${tsFile}.video.mp4`,
|
||||||
|
lang: lang
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (audioDownloaded) {
|
||||||
|
files.push({
|
||||||
|
type: 'Audio',
|
||||||
|
path: `${tsFile}.audio.mp4`,
|
||||||
|
lang: lang
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (options.novids && options.noaudio) {
|
||||||
|
fileName = parseFileName(options.fileName, variables, options.numbers, options.override).join(path.sep);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(options.dlsubs.indexOf('all') > -1){
|
||||||
|
options.dlsubs = ['all'];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.nosubs) {
|
||||||
|
console.info('Subtitles downloading disabled from nosubs flag.');
|
||||||
|
options.skipsubs = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!options.skipsubs && options.dlsubs.indexOf('none') == -1) {
|
||||||
|
if(streamData.subtitles.length > 0) {
|
||||||
|
for(const sub of streamData.subtitles) {
|
||||||
|
const subLang = langsData.languages.find(a => a.ao_locale === sub.lang);
|
||||||
|
if (!subLang) {
|
||||||
|
console.warn(`Language not found for subtitle language: ${sub.lang}, Skipping`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const sxData: Partial<sxItem> = {};
|
||||||
|
sxData.file = langsData.subsFile(fileName as string, subIndex+'', subLang, false, options.ccTag);
|
||||||
|
sxData.path = path.join(this.cfg.dir.content, sxData.file);
|
||||||
|
sxData.language = subLang;
|
||||||
|
if((options.dlsubs.includes('all') || options.dlsubs.includes(subLang.locale)) && sub.url.includes('.ass')) {
|
||||||
|
const getSubtitle = await this.req.getData(sub.url, AuthHeaders);
|
||||||
|
if (getSubtitle.ok && getSubtitle.res) {
|
||||||
|
console.info(`Subtitle Downloaded: ${sub.url}`);
|
||||||
|
const sBody = await getSubtitle.res.text();
|
||||||
|
sxData.title = `${subLang.language}`;
|
||||||
|
sxData.fonts = fontsData.assFonts(sBody) as Font[];
|
||||||
|
fs.writeFileSync(sxData.path, sBody);
|
||||||
|
files.push({
|
||||||
|
type: 'Subtitle',
|
||||||
|
...sxData as sxItem,
|
||||||
|
cc: false
|
||||||
|
});
|
||||||
|
} else{
|
||||||
|
console.warn(`Failed to download subtitle: ${sxData.file}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
subIndex++;
|
||||||
|
}
|
||||||
|
} else{
|
||||||
|
console.warn('Can\'t find urls for subtitles!');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else{
|
||||||
|
console.info('Subtitles downloading skipped!');
|
||||||
|
}
|
||||||
|
await this.sleep(options.waittime);
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
error: dlFailed,
|
||||||
|
data: files,
|
||||||
|
fileName: fileName ? (path.isAbsolute(fileName) ? fileName : path.join(this.cfg.dir.content, fileName)) || './unknown' : './unknown'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public sleep(ms: number) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
setTimeout(resolve, ms);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,2 +1,4 @@
|
||||||
ffmpeg: "ffmpeg.exe"
|
ffmpeg: "ffmpeg.exe"
|
||||||
mkvmerge: "mkvmerge.exe"
|
mkvmerge: "mkvmerge.exe"
|
||||||
|
ffprobe: "ffprobe.exe"
|
||||||
|
mp4decrypt: "mp4decrypt.exe"
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,24 @@
|
||||||
|
# Set the quality of the stream, 0 is highest available.
|
||||||
q: 0
|
q: 0
|
||||||
nServer: 1
|
# Set which stream to use
|
||||||
mp4mux: false
|
kstream: 1
|
||||||
noCleanUp: false
|
# Set which server to use
|
||||||
|
server: 1
|
||||||
|
# How many parts to download at once. Increasing may improve download speed.
|
||||||
|
partsize: 10
|
||||||
|
# Set whether to mux into an mp4 or not. Not recommended.
|
||||||
|
mp4: false
|
||||||
|
# Whether to delete any created files or not
|
||||||
|
nocleanup: false
|
||||||
|
# Whether to only download the relevant video once
|
||||||
|
dlVideoOnce: false
|
||||||
|
# Whether to keep all downloaded videos or only a single copy
|
||||||
|
keepAllVideos: false
|
||||||
|
# What to use as the file name template
|
||||||
|
fileName: "[${service}] ${showTitle} - S${season}E${episode} [${height}p]"
|
||||||
|
# What Audio languages to download
|
||||||
|
dubLang: ["jpn"]
|
||||||
|
# What Subtitle languages to download
|
||||||
|
dlsubs: ["all"]
|
||||||
|
# What language Audio to set as default
|
||||||
|
defaultAudio: "jpn"
|
||||||
1
config/gui.yml
Normal file
1
config/gui.yml
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
port: 3000
|
||||||
4015
crunchy.ts
4015
crunchy.ts
File diff suppressed because it is too large
Load diff
21
dev.js
Normal file
21
dev.js
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
const { exec } = require('child_process');
|
||||||
|
const path = require('path');
|
||||||
|
const toRun = process.argv.slice(2).join(' ').split('---');
|
||||||
|
|
||||||
|
const waitForProcess = async (proc) => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
proc.stdout?.on('data', (data) => process.stdout.write(data));
|
||||||
|
proc.stderr?.on('data', (data) => process.stderr.write(data));
|
||||||
|
proc.on('close', resolve);
|
||||||
|
proc.on('error', reject);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
await waitForProcess(exec('pnpm run tsc test false'));
|
||||||
|
for (let command of toRun) {
|
||||||
|
await waitForProcess(exec(`node index.js --service hidive ${command}`, {
|
||||||
|
cwd: path.join(__dirname, 'lib')
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
# multi-downloader-nx (2.0.18v)
|
# multi-downloader-nx (5.1.5v)
|
||||||
|
|
||||||
If you find any bugs in this documentation or in the programm itself please report it [over on GitHub](https://github.com/anidl/multi-downloader-nx/issues).
|
If you find any bugs in this documentation or in the program itself please report it [over on GitHub](https://github.com/anidl/multi-downloader-nx/issues).
|
||||||
|
|
||||||
## Legal Warning
|
## Legal Warning
|
||||||
|
|
||||||
This application is not endorsed by or affiliated with *Funimation* or *Crunchyroll*.
|
This application is not endorsed by or affiliated with *Crunchyroll*, *Hidive*, *AnimeOnegai*, or *AnimationDigitalNetwork*.
|
||||||
This application enables you to download videos for offline viewing which may be forbidden by law in your country.
|
This application enables you to download videos for offline viewing which may be forbidden by law in your country.
|
||||||
The usage of this application may also cause a violation of the *Terms of Service* between you and the stream provider.
|
The usage of this application may also cause a violation of the *Terms of Service* between you and the stream provider.
|
||||||
This tool is not responsible for your actions; please make an informed decision before using this application.
|
This tool is not responsible for your actions; please make an informed decision before using this application.
|
||||||
|
|
@ -18,7 +18,7 @@ This tool is not responsible for your actions; please make an informed decision
|
||||||
#### `--auth`
|
#### `--auth`
|
||||||
| **Service** | **Usage** | **Type** | **Required** | **Alias** | **cli-default Entry**
|
| **Service** | **Usage** | **Type** | **Required** | **Alias** | **cli-default Entry**
|
||||||
| --- | --- | --- | --- | --- | ---|
|
| --- | --- | --- | --- | --- | ---|
|
||||||
| Both | `--auth ` | `boolean` | `No`| `NaN` | `NaN` |
|
| All | `--auth ` | `boolean` | `No`| `NaN` | `NaN` |
|
||||||
|
|
||||||
Most of the shows on both services are only accessible if you payed for the service.
|
Most of the shows on both services are only accessible if you payed for the service.
|
||||||
In order for them to know who you are you are required to log in.
|
In order for them to know who you are you are required to log in.
|
||||||
|
|
@ -26,21 +26,27 @@ If you trigger this command, you will be prompted for the username and password
|
||||||
#### `--username`
|
#### `--username`
|
||||||
| **Service** | **Usage** | **Type** | **Required** | **Alias** | **Default** |**cli-default Entry**
|
| **Service** | **Usage** | **Type** | **Required** | **Alias** | **Default** |**cli-default Entry**
|
||||||
| --- | --- | --- | --- | --- | --- | ---|
|
| --- | --- | --- | --- | --- | --- | ---|
|
||||||
| Both | `--username ${username}` | `string` | `No`| `NaN` | `undefined`| `username: ` |
|
| All | `--username ${username}` | `string` | `No`| `NaN` | `undefined`| `username: ` |
|
||||||
|
|
||||||
Set the username to use for the authentication. If not provided, you will be prompted for the input
|
Set the username to use for the authentication. If not provided, you will be prompted for the input
|
||||||
#### `--password`
|
#### `--password`
|
||||||
| **Service** | **Usage** | **Type** | **Required** | **Alias** | **Default** |**cli-default Entry**
|
| **Service** | **Usage** | **Type** | **Required** | **Alias** | **Default** |**cli-default Entry**
|
||||||
| --- | --- | --- | --- | --- | --- | ---|
|
| --- | --- | --- | --- | --- | --- | ---|
|
||||||
| Both | `--password ${password}` | `string` | `No`| `NaN` | `undefined`| `password: ` |
|
| All | `--password ${password}` | `string` | `No`| `NaN` | `undefined`| `password: ` |
|
||||||
|
|
||||||
Set the password to use for the authentication. If not provided, you will be prompted for the input
|
Set the password to use for the authentication. If not provided, you will be prompted for the input
|
||||||
#### `--silentAuth`
|
#### `--silentAuth`
|
||||||
| **Service** | **Usage** | **Type** | **Required** | **Alias** | **Default** |**cli-default Entry**
|
| **Service** | **Usage** | **Type** | **Required** | **Alias** | **Default** |**cli-default Entry**
|
||||||
| --- | --- | --- | --- | --- | --- | ---|
|
| --- | --- | --- | --- | --- | --- | ---|
|
||||||
| Both | `--silentAuth ` | `boolean` | `No`| `NaN` | `false`| `silentAuth: ` |
|
| Crunchyroll | `--silentAuth ` | `boolean` | `No`| `NaN` | `false`| `silentAuth: ` |
|
||||||
|
|
||||||
Authenticate every time the script runs. Use at your own risk.
|
Authenticate every time the script runs. Use at your own risk.
|
||||||
|
#### `--token`
|
||||||
|
| **Service** | **Usage** | **Type** | **Required** | **Alias** | **Default** |**cli-default Entry**
|
||||||
|
| --- | --- | --- | --- | --- | --- | ---|
|
||||||
|
| Crunchyroll, AnimeOnegai | `--token ${token}` | `string` | `No`| `NaN` | `undefined`| `token: ` |
|
||||||
|
|
||||||
|
Allows you to login with your token (Example on crunchy is Refresh Token/etp-rt cookie)
|
||||||
### Fonts
|
### Fonts
|
||||||
#### `--dlFonts`
|
#### `--dlFonts`
|
||||||
| **Service** | **Usage** | **Type** | **Required** | **Alias** | **cli-default Entry**
|
| **Service** | **Usage** | **Type** | **Required** | **Alias** | **cli-default Entry**
|
||||||
|
|
@ -52,14 +58,14 @@ Use this command to download all the fonts and add them to the muxed **mkv** fil
|
||||||
#### `--fontName`
|
#### `--fontName`
|
||||||
| **Service** | **Usage** | **Type** | **Required** | **Alias** | **cli-default Entry**
|
| **Service** | **Usage** | **Type** | **Required** | **Alias** | **cli-default Entry**
|
||||||
| --- | --- | --- | --- | --- | ---|
|
| --- | --- | --- | --- | --- | ---|
|
||||||
| Funimation | `--fontName ${fontName}` | `string` | `No`| `NaN` | `NaN` |
|
| Hidive, AnimationDigitalNetwork | `--fontName ${fontName}` | `string` | `No`| `NaN` | `NaN` |
|
||||||
|
|
||||||
Set the font to use in subtiles
|
Set the font to use in subtiles
|
||||||
### Search
|
### Search
|
||||||
#### `--search`
|
#### `--search`
|
||||||
| **Service** | **Usage** | **Type** | **Required** | **Alias** | **cli-default Entry**
|
| **Service** | **Usage** | **Type** | **Required** | **Alias** | **cli-default Entry**
|
||||||
| --- | --- | --- | --- | --- | ---|
|
| --- | --- | --- | --- | --- | ---|
|
||||||
| Both | `--search ${search}` | `string` | `No`| `-f` | `NaN` |
|
| All | `--search ${search}` | `string` | `No`| `-f` | `NaN` |
|
||||||
|
|
||||||
Search of an anime by the given string
|
Search of an anime by the given string
|
||||||
#### `--search-type`
|
#### `--search-type`
|
||||||
|
|
@ -71,19 +77,19 @@ Search only for type of anime listings (e.g. episodes, series)
|
||||||
#### `--page`
|
#### `--page`
|
||||||
| **Service** | **Usage** | **Type** | **Required** | **Alias** | **cli-default Entry**
|
| **Service** | **Usage** | **Type** | **Required** | **Alias** | **cli-default Entry**
|
||||||
| --- | --- | --- | --- | --- | ---|
|
| --- | --- | --- | --- | --- | ---|
|
||||||
| Crunchyroll | `--page ${page}` | `number` | `No`| `-p` | `NaN` |
|
| Crunchyroll, Hidive | `--page ${page}` | `number` | `No`| `-p` | `NaN` |
|
||||||
|
|
||||||
The output is organized in pages. Use this command to output the items for the given page
|
The output is organized in pages. Use this command to output the items for the given page
|
||||||
#### `--search-locale`
|
#### `--locale`
|
||||||
| **Service** | **Usage** | **Type** | **Required** | **Alias** | **Choices** | **Default** |**cli-default Entry**
|
| **Service** | **Usage** | **Type** | **Required** | **Alias** | **Choices** | **Default** |**cli-default Entry**
|
||||||
| --- | --- | --- | --- | --- | --- | --- | ---|
|
| --- | --- | --- | --- | --- | --- | --- | ---|
|
||||||
| Crunchyroll | `--search-locale ${locale}` | `string` | `No`| `NaN` | [`''`, `es-LA`, `es-419`, `es-ES`, `pt-BR`, `fr-FR`, `de-DE`, `ar-ME`, `ar-SA`, `it-IT`, `ru-RU`, `tr-TR`, `en-US`] | ``| `search-locale: ` |
|
| Crunchyroll, AnimeOnegai, AnimationDigitalNetwork | `--locale ${locale}` | `string` | `No`| `NaN` | [`''`, `en-US`, `en-IN`, `es-LA`, `es-419`, `es-ES`, `pt-BR`, `pt-PT`, `fr-FR`, `de-DE`, `ar-ME`, `ar-SA`, `it-IT`, `ru-RU`, `tr-TR`, `hi-IN`, `zh-CN`, `zh-TW`, `zh-HK`, `ko-KR`, `ca-ES`, `pl-PL`, `th-TH`, `ta-IN`, `ms-MY`, `vi-VN`, `id-ID`, `te-IN`, `fr`, `de`, `''`, `es`, `pt`] | `en-US`| `locale: ` |
|
||||||
|
|
||||||
Set the search local that will be used for searching for items.
|
Set the local that will be used for the API.
|
||||||
#### `--new`
|
#### `--new`
|
||||||
| **Service** | **Usage** | **Type** | **Required** | **Alias** | **cli-default Entry**
|
| **Service** | **Usage** | **Type** | **Required** | **Alias** | **cli-default Entry**
|
||||||
| --- | --- | --- | --- | --- | ---|
|
| --- | --- | --- | --- | --- | ---|
|
||||||
| Crunchyroll | `--new ` | `boolean` | `No`| `NaN` | `NaN` |
|
| Crunchyroll, Hidive | `--new ` | `boolean` | `No`| `NaN` | `NaN` |
|
||||||
|
|
||||||
Get last updated series list
|
Get last updated series list
|
||||||
### Downloading
|
### Downloading
|
||||||
|
|
@ -98,220 +104,350 @@ Get video list by Movie Listing ID
|
||||||
| --- | --- | --- | --- | --- | ---|
|
| --- | --- | --- | --- | --- | ---|
|
||||||
| Crunchyroll | `--series ${ID}` | `string` | `No`| `--srz` | `NaN` |
|
| Crunchyroll | `--series ${ID}` | `string` | `No`| `--srz` | `NaN` |
|
||||||
|
|
||||||
This command is used only for crunchyroll.
|
Requested is the ID of a show not a season.
|
||||||
Requested is the ID of a show not a season.
|
|
||||||
#### `-s`
|
#### `-s`
|
||||||
| **Service** | **Usage** | **Type** | **Required** | **Alias** | **cli-default Entry**
|
| **Service** | **Usage** | **Type** | **Required** | **Alias** | **cli-default Entry**
|
||||||
| --- | --- | --- | --- | --- | ---|
|
| --- | --- | --- | --- | --- | ---|
|
||||||
| Both | `-s ${ID}` | `string` | `No`| `NaN` | `NaN` |
|
| All | `-s ${ID}` | `string` | `No`| `NaN` | `NaN` |
|
||||||
|
|
||||||
Used to set the season ID to download from
|
Used to set the season ID to download from
|
||||||
#### `-e`
|
#### `-e`
|
||||||
| **Service** | **Usage** | **Type** | **Required** | **Alias** | **cli-default Entry**
|
| **Service** | **Usage** | **Type** | **Required** | **Alias** | **cli-default Entry**
|
||||||
| --- | --- | --- | --- | --- | ---|
|
| --- | --- | --- | --- | --- | ---|
|
||||||
| Both | `-e ${selection}` | `string` | `No`| `--epsisode` | `NaN` |
|
| All | `-e ${selection}` | `string` | `No`| `--episode` | `NaN` |
|
||||||
|
|
||||||
Set the episode(s) to download from any given show.
|
Set the episode(s) to download from any given show.
|
||||||
For multiple selection: 1-4 OR 1,2,3,4
|
For multiple selection: 1-4 OR 1,2,3,4
|
||||||
For special episodes: S1-4 OR S1,S2,S3,S4 where S is the special letter
|
For special episodes: S1-4 OR S1,S2,S3,S4 where S is the special letter
|
||||||
|
#### `--extid`
|
||||||
|
| **Service** | **Usage** | **Type** | **Required** | **Alias** | **cli-default Entry**
|
||||||
|
| --- | --- | --- | --- | --- | ---|
|
||||||
|
| Crunchyroll | `--extid ${selection}` | `string` | `No`| `--externalid` | `NaN` |
|
||||||
|
|
||||||
|
Set the external id to lookup/download.
|
||||||
|
Allows you to download or view legacy Crunchyroll Ids
|
||||||
#### `-q`
|
#### `-q`
|
||||||
| **Service** | **Usage** | **Type** | **Required** | **Alias** | **Default** |**cli-default Entry**
|
| **Service** | **Usage** | **Type** | **Required** | **Alias** | **Default** |**cli-default Entry**
|
||||||
| --- | --- | --- | --- | --- | --- | ---|
|
| --- | --- | --- | --- | --- | --- | ---|
|
||||||
| Both | `-q ${qualityLevel}` | `number` | `No`| `NaN` | `7`| `q: ` |
|
| All | `-q ${qualityLevel}` | `number` | `No`| `NaN` | `0`| `q: ` |
|
||||||
|
|
||||||
Set the quality level. Use 0 to use the maximum quality.
|
Set the quality level. Use 0 to use the maximum quality.
|
||||||
|
#### `--dlVideoOnce`
|
||||||
|
| **Service** | **Usage** | **Type** | **Required** | **Alias** | **Default** |**cli-default Entry**
|
||||||
|
| --- | --- | --- | --- | --- | --- | ---|
|
||||||
|
| Crunchyroll, AnimeOnegai | `--dlVideoOnce ` | `boolean` | `No`| `NaN` | `false`| `dlVideoOnce: ` |
|
||||||
|
|
||||||
|
If selected, the best selected quality will be downloaded only for the first language,
|
||||||
|
then the worst video quality with the same audio quality will be downloaded for every other language.
|
||||||
|
By the later merge of the videos, no quality difference will be present.
|
||||||
|
This will speed up the download speed, if multiple languages are selected.
|
||||||
|
#### `--chapters`
|
||||||
|
| **Service** | **Usage** | **Type** | **Required** | **Alias** | **Default** |**cli-default Entry**
|
||||||
|
| --- | --- | --- | --- | --- | --- | ---|
|
||||||
|
| Crunchyroll, AnimationDigitalNetwork | `--chapters ` | `boolean` | `No`| `NaN` | `true`| `chapters: ` |
|
||||||
|
|
||||||
|
Will fetch the chapters and add them into the final video.
|
||||||
|
Currently only works with mkvmerge.
|
||||||
|
#### `--crapi`
|
||||||
|
| **Service** | **Usage** | **Type** | **Required** | **Alias** | **Choices** | **Default** |**cli-default Entry**
|
||||||
|
| --- | --- | --- | --- | --- | --- | --- | ---|
|
||||||
|
| Crunchyroll | `--crapi ` | `string` | `No`| `NaN` | [`android`, `web`] | `web`| `crapi: ` |
|
||||||
|
|
||||||
|
If set to Android, it has lower quality, but Non-DRM streams,
|
||||||
|
If set to Web, it has a higher quality adaptive stream, but everything is DRM.
|
||||||
|
#### `--removeBumpers`
|
||||||
|
| **Service** | **Usage** | **Type** | **Required** | **Alias** | **Default** |**cli-default Entry**
|
||||||
|
| --- | --- | --- | --- | --- | --- | ---|
|
||||||
|
| Hidive | `--removeBumpers ` | `boolean` | `No`| `NaN` | `true`| `removeBumpers: ` |
|
||||||
|
|
||||||
|
If selected, it will remove the bumpers such as the hidive intro from the final file.
|
||||||
|
Currently disabling this sometimes results in bugs such as video/audio desync
|
||||||
|
#### `--originalFontSize`
|
||||||
|
| **Service** | **Usage** | **Type** | **Required** | **Alias** | **Default** |**cli-default Entry**
|
||||||
|
| --- | --- | --- | --- | --- | --- | ---|
|
||||||
|
| Hidive | `--originalFontSize ` | `boolean` | `No`| `NaN` | `true`| `originalFontSize: ` |
|
||||||
|
|
||||||
|
If selected, it will prefer to keep the original Font Size defined by the service.
|
||||||
#### `-x`
|
#### `-x`
|
||||||
| **Service** | **Usage** | **Type** | **Required** | **Alias** | **Choices** | **Default** |**cli-default Entry**
|
| **Service** | **Usage** | **Type** | **Required** | **Alias** | **Choices** | **Default** |**cli-default Entry**
|
||||||
| --- | --- | --- | --- | --- | --- | --- | ---|
|
| --- | --- | --- | --- | --- | --- | --- | ---|
|
||||||
| Both | `-x ${server}` | `number` | `No`| `--server` | [`1`, `2`, `3`, `4`] | `1`| `x: ` |
|
| Crunchyroll | `-x ${server}` | `number` | `No`| `--server` | [`1`, `2`, `3`, `4`] | `1`| `x: ` |
|
||||||
|
|
||||||
Select the server to use
|
Select the server to use
|
||||||
#### `--kstream`
|
#### `--kstream`
|
||||||
| **Service** | **Usage** | **Type** | **Required** | **Alias** | **Choices** | **Default** |**cli-default Entry**
|
| **Service** | **Usage** | **Type** | **Required** | **Alias** | **Choices** | **Default** |**cli-default Entry**
|
||||||
| --- | --- | --- | --- | --- | --- | --- | ---|
|
| --- | --- | --- | --- | --- | --- | --- | ---|
|
||||||
| Crunchyroll | `--kstream ${stream}` | `number` | `No`| `-k` | [`1`, `2`, `3`, `4`, `5`, `6`, `7`] | `1`| `kstream: ` |
|
| Crunchyroll | `--kstream ${stream}` | `number` | `No`| `-k` | [`1`, `2`, `3`, `4`, `5`, `6`, `7`, `8`, `9`, `10`] | `1`| `kstream: ` |
|
||||||
|
|
||||||
Select specific stream
|
Select specific stream
|
||||||
|
#### `--cstream`
|
||||||
|
| **Service** | **Usage** | **Type** | **Required** | **Alias** | **Choices** | **Default** |**cli-default Entry**
|
||||||
|
| --- | --- | --- | --- | --- | --- | --- | ---|
|
||||||
|
| Crunchyroll | `--cstream ${device}` | `string` | `No`| `--cs` | [`chrome`, `firefox`, `safari`, `edge`, `fallback`, `ps4`, `ps5`, `switch`, `samsungtv`, `lgtv`, `rokutv`, `android`, `iphone`, `ipad`, `none`] | `chrome`| `cstream: ` |
|
||||||
|
|
||||||
|
Select specific crunchy play stream by device, or disable stream with "none"
|
||||||
#### `--hslang`
|
#### `--hslang`
|
||||||
| **Service** | **Usage** | **Type** | **Required** | **Alias** | **Choices** | **Default** |**cli-default Entry**
|
| **Service** | **Usage** | **Type** | **Required** | **Alias** | **Choices** | **Default** |**cli-default Entry**
|
||||||
| --- | --- | --- | --- | --- | --- | --- | ---|
|
| --- | --- | --- | --- | --- | --- | --- | ---|
|
||||||
| Crunchyroll | `--hslang ${hslang}` | `string` | `No`| `NaN` | [`none`, `es-419`, `es`, `pt-BR`, `fr`, `de`, `ar`, `it`, `ru`, `tr`, `zh`, `en`, `ja`] | `none`| `hslang: ` |
|
| Crunchyroll | `--hslang ${hslang}` | `string` | `No`| `NaN` | [`none`, `en`, `en-IN`, `es-419`, `es-ES`, `pt-BR`, `pt-PT`, `fr`, `de`, `ar`, `it`, `ru`, `tr`, `hi`, `zh`, `zh-CN`, `zh-TW`, `zh-HK`, `ko`, `ca-ES`, `pl-PL`, `th-TH`, `ta-IN`, `ms-MY`, `vi-VN`, `id-ID`, `te-IN`, `ja`] | `none`| `hslang: ` |
|
||||||
|
|
||||||
Download video with specific hardsubs
|
Download video with specific hardsubs
|
||||||
#### `--dlsubs`
|
#### `--dlsubs`
|
||||||
| **Service** | **Usage** | **Type** | **Required** | **Alias** | **Choices** | **Default** |**cli-default Entry**
|
| **Service** | **Usage** | **Type** | **Required** | **Alias** | **Choices** | **Default** |**cli-default Entry**
|
||||||
| --- | --- | --- | --- | --- | --- | --- | ---|
|
| --- | --- | --- | --- | --- | --- | --- | ---|
|
||||||
| Both | `--dlsubs ${sub1} ${sub2}` | `array` | `No`| `NaN` | [`all`, `none`, `es-419`, `es`, `pt-BR`, `fr`, `de`, `ar`, `it`, `ru`, `tr`, `zh`, `en`, `ja`] | `all`| `dlsubs: ` |
|
| All | `--dlsubs ${sub1} ${sub2}` | `array` | `No`| `NaN` | [`all`, `none`, `en`, `en-IN`, `es-419`, `es-ES`, `pt-BR`, `pt-PT`, `fr`, `de`, `ar`, `it`, `ru`, `tr`, `hi`, `zh`, `zh-CN`, `zh-TW`, `zh-HK`, `ko`, `ca-ES`, `pl-PL`, `th-TH`, `ta-IN`, `ms-MY`, `vi-VN`, `id-ID`, `te-IN`, `ja`] | `all`| `dlsubs: ` |
|
||||||
|
|
||||||
Download subtitles by language tag (space-separated)
|
Download subtitles by language tag (space-separated)
|
||||||
Funi Only: zh
|
Crunchy Only: en, en-IN, es-419, es-419, es-ES, pt-BR, pt-PT, fr, de, ar, ar, it, ru, tr, hi, zh-CN, zh-TW, zh-HK, ko, ca-ES, pl-PL, th-TH, ta-IN, ms-MY, vi-VN, id-ID, te-IN, ja
|
||||||
Crunchy Only: es-419, es, fr, de, ar, ar, it, ru, tr
|
|
||||||
#### `--novids`
|
#### `--novids`
|
||||||
| **Service** | **Usage** | **Type** | **Required** | **Alias** | **cli-default Entry**
|
| **Service** | **Usage** | **Type** | **Required** | **Alias** | **cli-default Entry**
|
||||||
| --- | --- | --- | --- | --- | ---|
|
| --- | --- | --- | --- | --- | ---|
|
||||||
| Both | `--novids ` | `boolean` | `No`| `NaN` | `NaN` |
|
| All | `--novids ` | `boolean` | `No`| `NaN` | `NaN` |
|
||||||
|
|
||||||
Skip downloading videos
|
Skip downloading videos
|
||||||
#### `--noaudio`
|
#### `--noaudio`
|
||||||
| **Service** | **Usage** | **Type** | **Required** | **Alias** | **cli-default Entry**
|
| **Service** | **Usage** | **Type** | **Required** | **Alias** | **cli-default Entry**
|
||||||
| --- | --- | --- | --- | --- | ---|
|
| --- | --- | --- | --- | --- | ---|
|
||||||
| Both | `--noaudio ` | `boolean` | `No`| `NaN` | `NaN` |
|
| Crunchyroll, Hidive | `--noaudio ` | `boolean` | `No`| `NaN` | `NaN` |
|
||||||
|
|
||||||
Skip downloading audio
|
Skip downloading audio
|
||||||
#### `--nosubs`
|
#### `--nosubs`
|
||||||
| **Service** | **Usage** | **Type** | **Required** | **Alias** | **cli-default Entry**
|
| **Service** | **Usage** | **Type** | **Required** | **Alias** | **cli-default Entry**
|
||||||
| --- | --- | --- | --- | --- | ---|
|
| --- | --- | --- | --- | --- | ---|
|
||||||
| Both | `--nosubs ` | `boolean` | `No`| `NaN` | `NaN` |
|
| All | `--nosubs ` | `boolean` | `No`| `NaN` | `NaN` |
|
||||||
|
|
||||||
Skip downloading subtitles
|
Skip downloading subtitles
|
||||||
#### `--dubLang`
|
#### `--dubLang`
|
||||||
| **Service** | **Usage** | **Type** | **Required** | **Alias** | **Choices** | **Default** |**cli-default Entry**
|
| **Service** | **Usage** | **Type** | **Required** | **Alias** | **Choices** | **Default** |**cli-default Entry**
|
||||||
| --- | --- | --- | --- | --- | --- | --- | ---|
|
| --- | --- | --- | --- | --- | --- | --- | ---|
|
||||||
| Both | `--dubLang ${dub1} ${dub2}` | `array` | `No`| `NaN` | [`spa`, `por`, `fra`, `deu`, `ara`, `ita`, `rus`, `tur`, `cmn`, `eng`, `jpn`] | `jpn`| `dubLang: ` |
|
| All | `--dubLang ${dub1} ${dub2}` | `array` | `No`| `NaN` | [`eng`, `spa`, `spa-419`, `spa-ES`, `por`, `fra`, `deu`, `ara-ME`, `ara`, `ita`, `rus`, `tur`, `hin`, `cmn`, `zho`, `chi`, `zh-HK`, `kor`, `cat`, `pol`, `tha`, `tam`, `may`, `vie`, `ind`, `tel`, `jpn`] | `jpn`| `dubLang: ` |
|
||||||
|
|
||||||
Set the language to download:
|
Set the language to download:
|
||||||
Funi Only: cmn
|
Crunchy Only: eng, eng, spa, spa-419, spa-ES, por, por, fra, deu, ara-ME, ara, ita, rus, tur, hin, zho, chi, zh-HK, kor, cat, pol, tha, tam, may, vie, ind, tel, jpn
|
||||||
Crunchy Only: spa, spa, fra, deu, ara, ara, ita, rus, tur
|
|
||||||
#### `--all`
|
#### `--all`
|
||||||
| **Service** | **Usage** | **Type** | **Required** | **Alias** | **Default** |**cli-default Entry**
|
| **Service** | **Usage** | **Type** | **Required** | **Alias** | **Default** |**cli-default Entry**
|
||||||
| --- | --- | --- | --- | --- | --- | ---|
|
| --- | --- | --- | --- | --- | --- | ---|
|
||||||
| Both | `--all ` | `boolean` | `No`| `NaN` | `false`| `all: ` |
|
| All | `--all ` | `boolean` | `No`| `NaN` | `false`| `all: ` |
|
||||||
|
|
||||||
Used to download all episodes from the show
|
Used to download all episodes from the show
|
||||||
#### `--fontSize`
|
#### `--fontSize`
|
||||||
| **Service** | **Usage** | **Type** | **Required** | **Alias** | **Default** |**cli-default Entry**
|
| **Service** | **Usage** | **Type** | **Required** | **Alias** | **Default** |**cli-default Entry**
|
||||||
| --- | --- | --- | --- | --- | --- | ---|
|
| --- | --- | --- | --- | --- | --- | ---|
|
||||||
| Both | `--fontSize ${fontSize}` | `number` | `No`| `NaN` | `55`| `fontSize: ` |
|
| All | `--fontSize ${fontSize}` | `number` | `No`| `NaN` | `55`| `fontSize: ` |
|
||||||
|
|
||||||
Used to set the fontsize of the subtitles
|
When converting the subtitles to ass, this will change the font size
|
||||||
|
In most cases, requires "--originaFontSize false" to take effect
|
||||||
|
#### `--combineLines`
|
||||||
|
| **Service** | **Usage** | **Type** | **Required** | **Alias** | **cli-default Entry**
|
||||||
|
| --- | --- | --- | --- | --- | ---|
|
||||||
|
| Hidive | `--combineLines ` | `boolean` | `No`| `NaN` | `NaN` |
|
||||||
|
|
||||||
|
If selected, will prevent a line from shifting downwards
|
||||||
#### `--allDubs`
|
#### `--allDubs`
|
||||||
| **Service** | **Usage** | **Type** | **Required** | **Alias** | **cli-default Entry**
|
| **Service** | **Usage** | **Type** | **Required** | **Alias** | **cli-default Entry**
|
||||||
| --- | --- | --- | --- | --- | ---|
|
| --- | --- | --- | --- | --- | ---|
|
||||||
| Both | `--allDubs ` | `boolean` | `No`| `NaN` | `NaN` |
|
| All | `--allDubs ` | `boolean` | `No`| `NaN` | `NaN` |
|
||||||
|
|
||||||
If selected, all available dubs will get downloaded
|
If selected, all available dubs will get downloaded
|
||||||
#### `--timeout`
|
#### `--timeout`
|
||||||
| **Service** | **Usage** | **Type** | **Required** | **Alias** | **Default** |**cli-default Entry**
|
| **Service** | **Usage** | **Type** | **Required** | **Alias** | **Default** |**cli-default Entry**
|
||||||
| --- | --- | --- | --- | --- | --- | ---|
|
| --- | --- | --- | --- | --- | --- | ---|
|
||||||
| Both | `--timeout ${timeout}` | `number` | `No`| `NaN` | `60000`| `timeout: ` |
|
| All | `--timeout ${timeout}` | `number` | `No`| `NaN` | `15000`| `timeout: ` |
|
||||||
|
|
||||||
Set the timeout of all download reqests. Set in millisecods
|
Set the timeout of all download reqests. Set in millisecods
|
||||||
|
#### `--waittime`
|
||||||
|
| **Service** | **Usage** | **Type** | **Required** | **Alias** | **Default** |**cli-default Entry**
|
||||||
|
| --- | --- | --- | --- | --- | --- | ---|
|
||||||
|
| Crunchyroll, Hidive | `--waittime ${waittime}` | `number` | `No`| `NaN` | `0`| `waittime: ` |
|
||||||
|
|
||||||
|
Set the time the program waits between downloads. Set in millisecods
|
||||||
#### `--simul`
|
#### `--simul`
|
||||||
| **Service** | **Usage** | **Type** | **Required** | **Alias** | **Default** |**cli-default Entry**
|
| **Service** | **Usage** | **Type** | **Required** | **Alias** | **Default** |**cli-default Entry**
|
||||||
| --- | --- | --- | --- | --- | --- | ---|
|
| --- | --- | --- | --- | --- | --- | ---|
|
||||||
| Funimation | `--simul ` | `boolean` | `No`| `NaN` | `false`| `simul: ` |
|
| Hidive | `--simul ` | `boolean` | `No`| `NaN` | `false`| `simul: ` |
|
||||||
|
|
||||||
Force downloading simulcast version instead of uncut version (if available).
|
Force downloading simulcast version instead of uncut version (if available).
|
||||||
#### `--but`
|
#### `--but`
|
||||||
| **Service** | **Usage** | **Type** | **Required** | **Alias** | **cli-default Entry**
|
| **Service** | **Usage** | **Type** | **Required** | **Alias** | **cli-default Entry**
|
||||||
| --- | --- | --- | --- | --- | ---|
|
| --- | --- | --- | --- | --- | ---|
|
||||||
| Both | `--but ` | `boolean` | `No`| `NaN` | `NaN` |
|
| All | `--but ` | `boolean` | `No`| `NaN` | `NaN` |
|
||||||
|
|
||||||
Download everything but the -e selection
|
Download everything but the -e selection
|
||||||
#### `--downloadArchive`
|
#### `--downloadArchive`
|
||||||
| **Service** | **Usage** | **Type** | **Required** | **Alias** | **cli-default Entry**
|
| **Service** | **Usage** | **Type** | **Required** | **Alias** | **cli-default Entry**
|
||||||
| --- | --- | --- | --- | --- | ---|
|
| --- | --- | --- | --- | --- | ---|
|
||||||
| Both | `--downloadArchive ` | `boolean` | `No`| `NaN` | `NaN` |
|
| All | `--downloadArchive ` | `boolean` | `No`| `NaN` | `NaN` |
|
||||||
|
|
||||||
Used to download all archived shows
|
Used to download all archived shows
|
||||||
#### `--addArchive`
|
#### `--addArchive`
|
||||||
| **Service** | **Usage** | **Type** | **Required** | **Alias** | **cli-default Entry**
|
| **Service** | **Usage** | **Type** | **Required** | **Alias** | **cli-default Entry**
|
||||||
| --- | --- | --- | --- | --- | ---|
|
| --- | --- | --- | --- | --- | ---|
|
||||||
| Both | `--addArchive ` | `boolean` | `No`| `NaN` | `NaN` |
|
| All | `--addArchive ` | `boolean` | `No`| `NaN` | `NaN` |
|
||||||
|
|
||||||
Used to add the `-s` and `--srz` to downloadArchive
|
Used to add the `-s` and `--srz` to downloadArchive
|
||||||
#### `--partsize`
|
#### `--partsize`
|
||||||
| **Service** | **Usage** | **Type** | **Required** | **Alias** | **Default** |**cli-default Entry**
|
| **Service** | **Usage** | **Type** | **Required** | **Alias** | **Default** |**cli-default Entry**
|
||||||
| --- | --- | --- | --- | --- | --- | ---|
|
| --- | --- | --- | --- | --- | --- | ---|
|
||||||
| Both | `--partsize ${amount}` | `number` | `No`| `NaN` | `10`| `partsize: ` |
|
| All | `--partsize ${amount}` | `number` | `No`| `NaN` | `10`| `partsize: ` |
|
||||||
|
|
||||||
Set the amount of parts to download at once
|
Set the amount of parts to download at once
|
||||||
If you have a good connection try incresing this number to get a higher overall speed
|
If you have a good connection try incresing this number to get a higher overall speed
|
||||||
#### `--fsRetryTime`
|
#### `--fsRetryTime`
|
||||||
| **Service** | **Usage** | **Type** | **Required** | **Alias** | **Default** |**cli-default Entry**
|
| **Service** | **Usage** | **Type** | **Required** | **Alias** | **Default** |**cli-default Entry**
|
||||||
| --- | --- | --- | --- | --- | --- | ---|
|
| --- | --- | --- | --- | --- | --- | ---|
|
||||||
| Both | `--fsRetryTime ${time in seconds}` | `number` | `No`| `NaN` | `5`| `fsRetryTime: ` |
|
| All | `--fsRetryTime ${time in seconds}` | `number` | `No`| `NaN` | `5`| `fsRetryTime: ` |
|
||||||
|
|
||||||
Set the time the downloader waits before retrying if an error while writing the file occurs
|
Set the time the downloader waits before retrying if an error while writing the file occurs
|
||||||
|
#### `--force`
|
||||||
|
| **Service** | **Usage** | **Type** | **Required** | **Alias** | **Choices** | **cli-default Entry**
|
||||||
|
| --- | --- | --- | --- | --- | --- | ---|
|
||||||
|
| All | `--force ${option}` | `string` | `No`| `NaN` | [`y`, `Y`, `n`, `N`, `c`, `C`] | `NaN` |
|
||||||
|
|
||||||
|
If a file already exists, the tool will ask you how to proceed. With this, you can answer in advance.
|
||||||
### Muxing
|
### Muxing
|
||||||
#### `--mp4`
|
#### `--mp4`
|
||||||
| **Service** | **Usage** | **Type** | **Required** | **Alias** | **Default** |**cli-default Entry**
|
| **Service** | **Usage** | **Type** | **Required** | **Alias** | **Default** |**cli-default Entry**
|
||||||
| --- | --- | --- | --- | --- | --- | ---|
|
| --- | --- | --- | --- | --- | --- | ---|
|
||||||
| Both | `--mp4 ` | `boolean` | `No`| `NaN` | `false`| `mp4: ` |
|
| All | `--mp4 ` | `boolean` | `No`| `NaN` | `false`| `mp4: ` |
|
||||||
|
|
||||||
If selected, the output file will be an mp4 file (not recommended tho)
|
If selected, the output file will be an mp4 file (not recommended tho)
|
||||||
|
#### `--keepAllVideos`
|
||||||
|
| **Service** | **Usage** | **Type** | **Required** | **Alias** | **Default** |**cli-default Entry**
|
||||||
|
| --- | --- | --- | --- | --- | --- | ---|
|
||||||
|
| Crunchyroll, Hidive | `--keepAllVideos ` | `boolean` | `No`| `NaN` | `false`| `keepAllVideos: ` |
|
||||||
|
|
||||||
|
If set to true, it will keep all videos in the merge process, rather than discarding the extra videos.
|
||||||
|
#### `--syncTiming`
|
||||||
|
| **Service** | **Usage** | **Type** | **Required** | **Alias** | **Default** |**cli-default Entry**
|
||||||
|
| --- | --- | --- | --- | --- | --- | ---|
|
||||||
|
| Crunchyroll, Hidive | `--syncTiming ` | `boolean` | `No`| `NaN` | `false`| `syncTiming: ` |
|
||||||
|
|
||||||
|
If enabled attempts to sync timing for multi-dub downloads.
|
||||||
|
NOTE: This is currently experimental and syncs audio and subtitles, though subtitles has a lot of guesswork
|
||||||
|
If you find bugs with this, please report it in the discord or github
|
||||||
#### `--skipmux`
|
#### `--skipmux`
|
||||||
| **Service** | **Usage** | **Type** | **Required** | **Alias** | **cli-default Entry**
|
| **Service** | **Usage** | **Type** | **Required** | **Alias** | **cli-default Entry**
|
||||||
| --- | --- | --- | --- | --- | ---|
|
| --- | --- | --- | --- | --- | ---|
|
||||||
| Both | `--skipmux ` | `boolean` | `No`| `NaN` | `NaN` |
|
| All | `--skipmux ` | `boolean` | `No`| `NaN` | `NaN` |
|
||||||
|
|
||||||
Skip muxing video, audio and subtitles
|
Skip muxing video, audio and subtitles
|
||||||
#### `--nocleanup`
|
#### `--nocleanup`
|
||||||
| **Service** | **Usage** | **Type** | **Required** | **Alias** | **Default** |**cli-default Entry**
|
| **Service** | **Usage** | **Type** | **Required** | **Alias** | **Default** |**cli-default Entry**
|
||||||
| --- | --- | --- | --- | --- | --- | ---|
|
| --- | --- | --- | --- | --- | --- | ---|
|
||||||
| Both | `--nocleanup ` | `boolean` | `No`| `NaN` | `false`| `nocleanup: ` |
|
| All | `--nocleanup ` | `boolean` | `No`| `NaN` | `false`| `nocleanup: ` |
|
||||||
|
|
||||||
Don't delete subtitle, audio and video files after muxing
|
Don't delete subtitle, audio and video files after muxing
|
||||||
#### `--skipSubMux`
|
#### `--skipSubMux`
|
||||||
| **Service** | **Usage** | **Type** | **Required** | **Alias** | **Default** |**cli-default Entry**
|
| **Service** | **Usage** | **Type** | **Required** | **Alias** | **Default** |**cli-default Entry**
|
||||||
| --- | --- | --- | --- | --- | --- | ---|
|
| --- | --- | --- | --- | --- | --- | ---|
|
||||||
| Both | `--skipSubMux ` | `boolean` | `No`| `NaN` | `false`| `skipSubMux: ` |
|
| All | `--skipSubMux ` | `boolean` | `No`| `NaN` | `false`| `skipSubMux: ` |
|
||||||
|
|
||||||
Skip muxing the subtitles
|
Skip muxing the subtitles
|
||||||
#### `--forceMuxer`
|
#### `--forceMuxer`
|
||||||
| **Service** | **Usage** | **Type** | **Required** | **Alias** | **Choices** | **Default** |**cli-default Entry**
|
| **Service** | **Usage** | **Type** | **Required** | **Alias** | **Choices** | **Default** |**cli-default Entry**
|
||||||
| --- | --- | --- | --- | --- | --- | --- | ---|
|
| --- | --- | --- | --- | --- | --- | --- | ---|
|
||||||
| Both | `--forceMuxer ${muxer}` | `string` | `No`| `NaN` | [`ffmpeg`, `mkvmerge`] | `undefined`| `forceMuxer: ` |
|
| All | `--forceMuxer ${muxer}` | `string` | `No`| `NaN` | [`ffmpeg`, `mkvmerge`] | `undefined`| `forceMuxer: ` |
|
||||||
|
|
||||||
Force the program to use said muxer or don't mux if the given muxer is not present
|
Force the program to use said muxer or don't mux if the given muxer is not present
|
||||||
|
#### `--videoTitle`
|
||||||
|
| **Service** | **Usage** | **Type** | **Required** | **Alias** | **cli-default Entry**
|
||||||
|
| --- | --- | --- | --- | --- | ---|
|
||||||
|
| All | `--videoTitle ${title}` | `string` | `No`| `NaN` | `NaN` |
|
||||||
|
|
||||||
|
Set the video track name of the merged file
|
||||||
|
#### `--mkvmergeOptions`
|
||||||
|
| **Service** | **Usage** | **Type** | **Required** | **Alias** | **Default** |**cli-default Entry**
|
||||||
|
| --- | --- | --- | --- | --- | --- | ---|
|
||||||
|
| All | `--mkvmergeOptions ${args}` | `array` | `No`| `NaN` | `--no-date,--disable-track-statistics-tags,--engage no_variable_data`| `mkvmergeOptions: ` |
|
||||||
|
|
||||||
|
Set the options given to mkvmerge
|
||||||
|
#### `--ffmpegOptions`
|
||||||
|
| **Service** | **Usage** | **Type** | **Required** | **Alias** | **Default** |**cli-default Entry**
|
||||||
|
| --- | --- | --- | --- | --- | --- | ---|
|
||||||
|
| All | `--ffmpegOptions ${args}` | `array` | `No`| `NaN` | ``| `ffmpegOptions: ` |
|
||||||
|
|
||||||
|
Set the options given to ffmpeg
|
||||||
|
#### `--defaultAudio`
|
||||||
|
| **Service** | **Usage** | **Type** | **Required** | **Alias** | **Default** |**cli-default Entry**
|
||||||
|
| --- | --- | --- | --- | --- | --- | ---|
|
||||||
|
| All | `--defaultAudio ${args}` | `string` | `No`| `NaN` | `eng`| `defaultAudio: ` |
|
||||||
|
|
||||||
|
Set the default audio track by language code
|
||||||
|
Possible Values: eng, eng, spa, spa-419, spa-ES, por, por, fra, deu, ara-ME, ara, ita, rus, tur, hin, cmn, zho, chi, zh-HK, kor, cat, pol, tha, tam, may, vie, ind, tel, jpn
|
||||||
|
#### `--defaultSub`
|
||||||
|
| **Service** | **Usage** | **Type** | **Required** | **Alias** | **Default** |**cli-default Entry**
|
||||||
|
| --- | --- | --- | --- | --- | --- | ---|
|
||||||
|
| All | `--defaultSub ${args}` | `string` | `No`| `NaN` | `eng`| `defaultSub: ` |
|
||||||
|
|
||||||
|
Set the default subtitle track by language code
|
||||||
|
Possible Values: eng, eng, spa, spa-419, spa-ES, por, por, fra, deu, ara-ME, ara, ita, rus, tur, hin, cmn, zho, chi, zh-HK, kor, cat, pol, tha, tam, may, vie, ind, tel, jpn
|
||||||
### Filename Template
|
### Filename Template
|
||||||
#### `--fileName`
|
#### `--fileName`
|
||||||
| **Service** | **Usage** | **Type** | **Required** | **Alias** | **Default** |**cli-default Entry**
|
| **Service** | **Usage** | **Type** | **Required** | **Alias** | **Default** |**cli-default Entry**
|
||||||
| --- | --- | --- | --- | --- | --- | ---|
|
| --- | --- | --- | --- | --- | --- | ---|
|
||||||
| Both | `--fileName ${fileName}` | `string` | `No`| `NaN` | `[${service}] ${showTitle} - S${season}E${episode} [${height}p]`| `fileName: ` |
|
| All | `--fileName ${fileName}` | `string` | `No`| `NaN` | `[${service}] ${showTitle} - S${season}E${episode} [${height}p]`| `fileName: ` |
|
||||||
|
|
||||||
Set the filename template. Use ${variable_name} to insert variables.
|
Set the filename template. Use ${variable_name} to insert variables.
|
||||||
You can also create folders by inserting a path seperator in the filename
|
You can also create folders by inserting a path seperator in the filename
|
||||||
You may use 'title', 'episode', 'showTitle', 'season', 'width', 'height', 'service' as variables.
|
You may use 'title', 'episode', 'showTitle', 'seriesTitle', 'season', 'width', 'height', 'service' as variables.
|
||||||
#### `--numbers`
|
#### `--numbers`
|
||||||
| **Service** | **Usage** | **Type** | **Required** | **Alias** | **Default** |**cli-default Entry**
|
| **Service** | **Usage** | **Type** | **Required** | **Alias** | **Default** |**cli-default Entry**
|
||||||
| --- | --- | --- | --- | --- | --- | ---|
|
| --- | --- | --- | --- | --- | --- | ---|
|
||||||
| Both | `--numbers ${number}` | `number` | `No`| `NaN` | `2`| `numbers: ` |
|
| All | `--numbers ${number}` | `number` | `No`| `NaN` | `2`| `numbers: ` |
|
||||||
|
|
||||||
Set how long a number in the title should be at least.
|
Set how long a number in the title should be at least.
|
||||||
Set in config: 3; Episode number: 5; Output: 005
|
Set in config: 3; Episode number: 5; Output: 005
|
||||||
Set in config: 2; Episode number: 1; Output: 01
|
Set in config: 2; Episode number: 1; Output: 01
|
||||||
Set in config: 1; Episode number: 20; Output: 20
|
Set in config: 1; Episode number: 20; Output: 20
|
||||||
|
#### `--override`
|
||||||
|
| **Service** | **Usage** | **Type** | **Required** | **Alias** | **Default** |**cli-default Entry**
|
||||||
|
| --- | --- | --- | --- | --- | --- | ---|
|
||||||
|
| All | `--override "${toOverride}='${value}'"` | `array` | `No`| `NaN` | ``| `override: ` |
|
||||||
|
|
||||||
|
Override a template variable
|
||||||
|
#### `--ccTag`
|
||||||
|
| **Service** | **Usage** | **Type** | **Required** | **Alias** | **Default** |**cli-default Entry**
|
||||||
|
| --- | --- | --- | --- | --- | --- | ---|
|
||||||
|
| All | `--ccTag ${tag}` | `string` | `No`| `NaN` | `cc`| `ccTag: ` |
|
||||||
|
|
||||||
|
Used to set the name for subtitles that contain tranlations for none verbal communication (e.g. signs)
|
||||||
### Debug
|
### Debug
|
||||||
#### `--nosess`
|
#### `--nosess`
|
||||||
| **Service** | **Usage** | **Type** | **Required** | **Alias** | **Default** |**cli-default Entry**
|
| **Service** | **Usage** | **Type** | **Required** | **Alias** | **Default** |**cli-default Entry**
|
||||||
| --- | --- | --- | --- | --- | --- | ---|
|
| --- | --- | --- | --- | --- | --- | ---|
|
||||||
| Both | `--nosess ` | `boolean` | `No`| `NaN` | `false`| `nosess: ` |
|
| All | `--nosess ` | `boolean` | `No`| `NaN` | `false`| `nosess: ` |
|
||||||
|
|
||||||
Reset session cookie for testing purposes
|
Reset session cookie for testing purposes
|
||||||
#### `--debug`
|
#### `--debug`
|
||||||
| **Service** | **Usage** | **Type** | **Required** | **Alias** | **Default** |**cli-default Entry**
|
| **Service** | **Usage** | **Type** | **Required** | **Alias** | **Default** |**cli-default Entry**
|
||||||
| --- | --- | --- | --- | --- | --- | ---|
|
| --- | --- | --- | --- | --- | --- | ---|
|
||||||
| Both | `--debug ` | `boolean` | `No`| `NaN` | `false`| `debug: ` |
|
| All | `--debug ` | `boolean` | `No`| `NaN` | `false`| `debug: ` |
|
||||||
|
|
||||||
Debug mode (tokens may be revealed in the console output)
|
Debug mode (tokens may be revealed in the console output)
|
||||||
### Utilities
|
### Utilities
|
||||||
#### `--service`
|
#### `--service`
|
||||||
| **Service** | **Usage** | **Type** | **Required** | **Alias** | **Choices** | **Default** |**cli-default Entry**
|
| **Service** | **Usage** | **Type** | **Required** | **Alias** | **Choices** | **Default** |**cli-default Entry**
|
||||||
| --- | --- | --- | --- | --- | --- | --- | ---|
|
| --- | --- | --- | --- | --- | --- | --- | ---|
|
||||||
| Both | `--service ${service}` | `string` | `Yes`| `NaN` | [`funi`, `crunchy`] | ``| `service: ` |
|
| All | `--service ${service}` | `string` | `Yes`| `NaN` | [`crunchy`, `hidive`, `ao`, `adn`] | ``| `service: ` |
|
||||||
|
|
||||||
Set the service you want to use
|
Set the service you want to use
|
||||||
#### `--update`
|
#### `--update`
|
||||||
| **Service** | **Usage** | **Type** | **Required** | **Alias** | **cli-default Entry**
|
| **Service** | **Usage** | **Type** | **Required** | **Alias** | **cli-default Entry**
|
||||||
| --- | --- | --- | --- | --- | ---|
|
| --- | --- | --- | --- | --- | ---|
|
||||||
| Both | `--update ` | `boolean` | `No`| `NaN` | `NaN` |
|
| All | `--update ` | `boolean` | `No`| `NaN` | `NaN` |
|
||||||
|
|
||||||
Force the tool to check for updates (code version only)
|
Force the tool to check for updates (code version only)
|
||||||
|
#### `--skipUpdate`
|
||||||
|
| **Service** | **Usage** | **Type** | **Required** | **Alias** | **Default** |**cli-default Entry**
|
||||||
|
| --- | --- | --- | --- | --- | --- | ---|
|
||||||
|
| All | `--skipUpdate ` | `boolean` | `No`| `NaN` | `false`| `skipUpdate: ` |
|
||||||
|
|
||||||
|
If true, the tool won't check for updates
|
||||||
### Help
|
### Help
|
||||||
#### `--help`
|
#### `--help`
|
||||||
| **Service** | **Usage** | **Type** | **Required** | **Alias** | **cli-default Entry**
|
| **Service** | **Usage** | **Type** | **Required** | **Alias** | **cli-default Entry**
|
||||||
| --- | --- | --- | --- | --- | ---|
|
| --- | --- | --- | --- | --- | ---|
|
||||||
| Both | `--help ` | `boolean` | `No`| `-h` | `NaN` |
|
| All | `--help ` | `boolean` | `No`| `-h` | `NaN` |
|
||||||
|
|
||||||
Show the help output
|
Show the help output
|
||||||
|
### GUI
|
||||||
|
|
|
||||||
|
|
@ -1,30 +1,107 @@
|
||||||
# Anime Downloader NX by AniDL
|
# Anime Downloader NX by AniDL
|
||||||
|
|
||||||
This downloader can download anime from diffrent sites. Currently supported are *Funimation* and *Crunchyroll*.
|
[](https://discord.gg/qEpbWen5vq)
|
||||||
|
|
||||||
|
This downloader can download anime from different sites. Currently supported are *Crunchyroll*, *Hidive*, *AnimeOnegai*, and *AnimationDigitalNetwork*.
|
||||||
|
|
||||||
## Legal Warning
|
## Legal Warning
|
||||||
|
|
||||||
This application is not endorsed by or affiliated with *Funimation* or *Crunchyroll*. This application enables you to download videos for offline viewing which may be forbidden by law in your country. The usage of this application may also cause a violation of the *Terms of Service* between you and the stream provider. This tool is not responsible for your actions; please make an informed decision before using this application.
|
This application is not endorsed by or affiliated with *Crunchyroll*, *Hidive*, *AnimeOnegai*, or *AnimationDigitalNetwork*. This application enables you to download videos for offline viewing which may be forbidden by law in your country. The usage of this application may also cause a violation of the *Terms of Service* between you and the stream provider. This tool is not responsible for your actions; please make an informed decision before using this application.
|
||||||
|
|
||||||
## Prerequisites
|
## Dependencies
|
||||||
|
|
||||||
* NodeJS >= 12.4.0 (https://nodejs.org/)
|
|
||||||
* NPM >= 6.9.0 (https://www.npmjs.org/)
|
|
||||||
* ffmpeg >= 4.0.0 (https://www.videohelp.com/software/ffmpeg)
|
* ffmpeg >= 4.0.0 (https://www.videohelp.com/software/ffmpeg)
|
||||||
* MKVToolNix >= 20.0.0 (https://www.videohelp.com/software/MKVToolNix)
|
* MKVToolNix >= 60.0.0 (https://www.videohelp.com/software/MKVToolNix)
|
||||||
|
|
||||||
### Paths Configuration
|
### Paths Configuration
|
||||||
|
|
||||||
By default this application uses the following paths to programs (main executables):
|
By default this application uses the following paths to programs (main executables):
|
||||||
|
|
||||||
* `ffmpeg.exe` (From PATH)
|
* `ffmpeg.exe` (From PATH)
|
||||||
|
* `ffprobe.exe` (From PATH)
|
||||||
* `mkvmerge.exe` (From PATH)
|
* `mkvmerge.exe` (From PATH)
|
||||||
|
* `mp4decrypt.exe` (From PATH)
|
||||||
|
|
||||||
To change these paths you need to edit `bin-path.yml` in `./config/` directory.
|
To change these paths you need to edit `bin-path.yml` in `./config/` directory.
|
||||||
|
|
||||||
### Node Modules
|
## CLI Information
|
||||||
|
|
||||||
After installing NodeJS with NPM go to directory with `package.json` file and type: `npm i`. Afterwards run `npm run tsc`. You can now find a lib folder containing the js code execute.
|
See [the documentation](https://github.com/anidl/multi-downloader-nx/blob/master/docs/DOCUMENTATION.md) for a complete list of what options are available. You can define defaults for the commands by editing the `cli-defaults.yml` file in the `./config/` directory.
|
||||||
* [check dependencies](https://david-dm.org/anidl/funimation-downloader-nx)
|
|
||||||
|
|
||||||
## CLI Options
|
### Example usage
|
||||||
See [the documentation](https://github.com/anidl/multi-downloader-nx/blob/master/docs/DOCUMENTATION.md)
|
|
||||||
|
#### Logging in
|
||||||
|
|
||||||
|
Most services require you to be logged in, in order to download from, an example of how you would login is:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
AniDL --service {ServiceName} --auth
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Searching
|
||||||
|
|
||||||
|
In order to find the IDs to download, you can search from each service by using the `--search` flag like this:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
AniDL --service {ServiceName} --search {SearchTerm}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Downloading
|
||||||
|
|
||||||
|
Once you have the ID which you can obtain from using the search or other means, you are ready to download, which you can do like this:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
AniDL --service {ServiceName} -s {SeasonID} -e {EpisodeNumber}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Building and running from source
|
||||||
|
|
||||||
|
### Build Dependencies
|
||||||
|
|
||||||
|
Dependencies that are only required for running from code. These are not required if you are using the prebuilt binaries.
|
||||||
|
|
||||||
|
* NodeJS >= 18.0.0 (https://nodejs.org/)
|
||||||
|
* NPM >= 6.9.0 (https://www.npmjs.org/)
|
||||||
|
* PNPM >= 7.0.0 (https://pnpm.io/)
|
||||||
|
|
||||||
|
### Build Setup
|
||||||
|
|
||||||
|
Please note that NodeJS, NPM, and PNPM must be installed on your system. For instructions on how to install pnpm, check (https://pnpm.io/installation)
|
||||||
|
|
||||||
|
First clone this repo `git clone https://github.com/anidl/multi-downloader-nx.git`.
|
||||||
|
|
||||||
|
`cd` into the cloned directory and run `pnpm i`. Next, decide if you want to package the application, build the code, or run from typescript.
|
||||||
|
|
||||||
|
### Run from TypeScript
|
||||||
|
|
||||||
|
You can run the code from native TypeScript, this requires ts-node which you can install with pnpm with the following command: `pnpm -g i ts-node`
|
||||||
|
|
||||||
|
Afterwords, you can run the application like this:
|
||||||
|
|
||||||
|
* CLI: `ts-node -T ./index.ts --help`
|
||||||
|
|
||||||
|
### Run as JavaScript
|
||||||
|
|
||||||
|
If you want to build the application into JavaScript code to run, you can do that as well like this:
|
||||||
|
|
||||||
|
* CLI: `pnpm run prebuild-cli`
|
||||||
|
* GUI: `pnpm run prebuild-gui`
|
||||||
|
|
||||||
|
Then you can cd into the `lib` folder and you will be able to run the CLI or GUI as follows:
|
||||||
|
|
||||||
|
* CLI: `node ./index.js --help`
|
||||||
|
* GUI: `node ./gui.js`
|
||||||
|
|
||||||
|
### Build the application into an executable
|
||||||
|
|
||||||
|
If you want to package the application, run pnpm run build-`{platform}`-`{type}` where `{platform}` is the operating system (currently the choices are windows, linux, macos, alpine, android, and arm) and `{type}` is cli or gui.
|
||||||
|
|
||||||
|
## DRM Decryption
|
||||||
|
|
||||||
|
### Decryption Requirements
|
||||||
|
|
||||||
|
* mp4decrypt >= Any (http://www.bento4.com/) - Only required for decrypting
|
||||||
|
|
||||||
|
### Instructions
|
||||||
|
|
||||||
|
In order to decrypt DRM content, you will need to have a dumped CDM, after that you will need to place the CDM files (`device_client_id_blob` and `device_private_key`) into the `./widevine/` directory. For legal reasons we do not include the CDM with the software, and you will have to source one yourself.
|
||||||
|
|
|
||||||
64
eslint.config.mjs
Normal file
64
eslint.config.mjs
Normal file
|
|
@ -0,0 +1,64 @@
|
||||||
|
// @ts-check
|
||||||
|
|
||||||
|
import eslint from '@eslint/js';
|
||||||
|
import tseslint from 'typescript-eslint';
|
||||||
|
import react from 'eslint-plugin-react';
|
||||||
|
|
||||||
|
export default tseslint.config(
|
||||||
|
eslint.configs.recommended,
|
||||||
|
...tseslint.configs.recommended,
|
||||||
|
{
|
||||||
|
rules: {
|
||||||
|
'no-console': 2,
|
||||||
|
'react/prop-types': 0,
|
||||||
|
'react-hooks/exhaustive-deps': 0,
|
||||||
|
'@typescript-eslint/no-explicit-any': 'off',
|
||||||
|
'@typescript-eslint/no-unsafe-declaration-merging': 'warn',
|
||||||
|
'@typescript-eslint/no-unused-vars' : 'warn',
|
||||||
|
'indent': [
|
||||||
|
'error',
|
||||||
|
2
|
||||||
|
],
|
||||||
|
'linebreak-style': [
|
||||||
|
'warn',
|
||||||
|
'windows'
|
||||||
|
],
|
||||||
|
'quotes': [
|
||||||
|
'error',
|
||||||
|
'single'
|
||||||
|
],
|
||||||
|
'semi': [
|
||||||
|
'error',
|
||||||
|
'always'
|
||||||
|
]
|
||||||
|
},
|
||||||
|
plugins: {
|
||||||
|
react
|
||||||
|
},
|
||||||
|
languageOptions: {
|
||||||
|
parserOptions: {
|
||||||
|
ecmaFeatures: {
|
||||||
|
jsx: true,
|
||||||
|
},
|
||||||
|
ecmaVersion: 2020,
|
||||||
|
sourceType: 'module'
|
||||||
|
},
|
||||||
|
parser: tseslint.parser
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ignores: [
|
||||||
|
'**/lib',
|
||||||
|
'**/videos/*.ts',
|
||||||
|
'**/build',
|
||||||
|
'dev.js',
|
||||||
|
'tsc.ts'
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
files: ['gui/react/**/*'],
|
||||||
|
rules: {
|
||||||
|
'no-console': 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
791
funi.ts
791
funi.ts
|
|
@ -1,791 +0,0 @@
|
||||||
// modules build-in
|
|
||||||
import fs from 'fs';
|
|
||||||
import path from 'path';
|
|
||||||
|
|
||||||
// package json
|
|
||||||
import packageJson from './package.json';
|
|
||||||
|
|
||||||
// program name
|
|
||||||
const api_host = 'https://prod-api-funimationnow.dadcdigital.com/api';
|
|
||||||
|
|
||||||
// modules extra
|
|
||||||
import * as shlp from 'sei-helper';
|
|
||||||
import m3u8 from 'm3u8-parsed';
|
|
||||||
import hlsDownload from 'hls-download';
|
|
||||||
|
|
||||||
// extra
|
|
||||||
import * as appYargs from './modules/module.app-args';
|
|
||||||
import * as yamlCfg from './modules/module.cfg-loader';
|
|
||||||
import vttConvert from './modules/module.vttconvert';
|
|
||||||
|
|
||||||
// types
|
|
||||||
import { Item } from './@types/items';
|
|
||||||
|
|
||||||
// params
|
|
||||||
const cfg = yamlCfg.loadCfg();
|
|
||||||
const token = yamlCfg.loadFuniToken();
|
|
||||||
// cli
|
|
||||||
const argv = appYargs.appArgv(cfg.cli);
|
|
||||||
// Import modules after argv has been exported
|
|
||||||
import getData from './modules/module.getdata.js';
|
|
||||||
import merger from './modules/module.merger';
|
|
||||||
import parseSelect from './modules/module.parseSelect';
|
|
||||||
import { EpisodeData, MediaChild } from './@types/episode';
|
|
||||||
import { Subtitle } from './@types/funiTypes';
|
|
||||||
import { StreamData } from './@types/streamData';
|
|
||||||
import { DownloadedFile } from './@types/downloadedFile';
|
|
||||||
import parseFileName, { Variable } from './modules/module.filename';
|
|
||||||
import { downloaded } from './modules/module.downloadArchive';
|
|
||||||
import { FunimationMediaDownload } from './@types/funiTypes';
|
|
||||||
import * as langsData from './modules/module.langsData';
|
|
||||||
import { TitleElement } from './@types/episode';
|
|
||||||
import { AvailableFilenameVars } from './modules/module.args';
|
|
||||||
// check page
|
|
||||||
argv.p = 1;
|
|
||||||
|
|
||||||
// fn variables
|
|
||||||
let fnEpNum: string|number = 0,
|
|
||||||
fnOutput: string[] = [],
|
|
||||||
season = 0,
|
|
||||||
tsDlPath: {
|
|
||||||
path: string,
|
|
||||||
lang: langsData.LanguageItem
|
|
||||||
}[] = [],
|
|
||||||
stDlPath: Subtitle[] = [];
|
|
||||||
|
|
||||||
// main
|
|
||||||
export default (async () => {
|
|
||||||
// load binaries
|
|
||||||
console.log(`\n=== Multi Downloader NX ${packageJson.version} ===\n`);
|
|
||||||
cfg.bin = await yamlCfg.loadBinCfg();
|
|
||||||
if (argv.allDubs) {
|
|
||||||
argv.dubLang = langsData.dubLanguageCodes;
|
|
||||||
}
|
|
||||||
// select mode
|
|
||||||
if (argv.silentAuth && !argv.auth) {
|
|
||||||
await auth();
|
|
||||||
}
|
|
||||||
if(argv.auth){
|
|
||||||
auth();
|
|
||||||
}
|
|
||||||
else if(argv.search){
|
|
||||||
searchShow();
|
|
||||||
}
|
|
||||||
else if(argv.s && !isNaN(parseInt(argv.s)) && parseInt(argv.s) > 0){
|
|
||||||
return getShow();
|
|
||||||
}
|
|
||||||
else{
|
|
||||||
appYargs.showHelp();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// auth
|
|
||||||
async function auth(){
|
|
||||||
const authOpts = {
|
|
||||||
user: argv.username ?? await shlp.question('[Q] LOGIN/EMAIL'),
|
|
||||||
pass: argv.password ?? await shlp.question('[Q] PASSWORD ')
|
|
||||||
};
|
|
||||||
const authData = await getData({
|
|
||||||
baseUrl: api_host,
|
|
||||||
url: '/auth/login/',
|
|
||||||
auth: authOpts,
|
|
||||||
debug: argv.debug,
|
|
||||||
});
|
|
||||||
if(authData.ok && authData.res){
|
|
||||||
const resJSON = JSON.parse(authData.res.body);
|
|
||||||
if(resJSON.token){
|
|
||||||
console.log('[INFO] Authentication success, your token: %s%s\n', resJSON.token.slice(0,8),'*'.repeat(32));
|
|
||||||
yamlCfg.saveFuniToken({'token': resJSON.token});
|
|
||||||
} else {
|
|
||||||
console.log('[ERROR]%s\n', ' No token found');
|
|
||||||
if (argv.debug) {
|
|
||||||
console.log(resJSON);
|
|
||||||
}
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// search show
|
|
||||||
async function searchShow(){
|
|
||||||
const qs = {unique: true, limit: 100, q: argv.search, offset: 0 };
|
|
||||||
const searchData = await getData({
|
|
||||||
baseUrl: api_host,
|
|
||||||
url: '/source/funimation/search/auto/',
|
|
||||||
querystring: qs,
|
|
||||||
token: token,
|
|
||||||
useToken: true,
|
|
||||||
debug: argv.debug,
|
|
||||||
});
|
|
||||||
if(!searchData.ok || !searchData.res){return;}
|
|
||||||
const searchDataJSON = JSON.parse(searchData.res.body);
|
|
||||||
if(searchDataJSON.detail){
|
|
||||||
console.log(`[ERROR] ${searchDataJSON.detail}`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if(searchDataJSON.items && searchDataJSON.items.hits){
|
|
||||||
const shows = searchDataJSON.items.hits;
|
|
||||||
console.log('[INFO] Search Results:');
|
|
||||||
for(const ssn in shows){
|
|
||||||
console.log(`[#${shows[ssn].id}] ${shows[ssn].title}` + (shows[ssn].tx_date?` (${shows[ssn].tx_date})`:''));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
console.log('[INFO] Total shows found: %s\n',searchDataJSON.count);
|
|
||||||
}
|
|
||||||
|
|
||||||
// get show
|
|
||||||
async function getShow(){
|
|
||||||
let ok = true;
|
|
||||||
const showData = await getData({
|
|
||||||
baseUrl: api_host,
|
|
||||||
url: `/source/catalog/title/${argv.s}`,
|
|
||||||
token: token,
|
|
||||||
useToken: true,
|
|
||||||
debug: argv.debug,
|
|
||||||
});
|
|
||||||
// check errors
|
|
||||||
if(!showData.ok || !showData.res){return;}
|
|
||||||
const showDataJSON = JSON.parse(showData.res.body);
|
|
||||||
if(showDataJSON.status){
|
|
||||||
console.log('[ERROR] Error #%d: %s\n', showDataJSON.status, showDataJSON.data.errors[0].detail);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
else if(!showDataJSON.items || showDataJSON.items.length<1){
|
|
||||||
console.log('[ERROR] Show not found\n');
|
|
||||||
process.exit(0);
|
|
||||||
}
|
|
||||||
const showDataItem = showDataJSON.items[0];
|
|
||||||
console.log('[#%s] %s (%s)',showDataItem.id,showDataItem.title,showDataItem.releaseYear);
|
|
||||||
// show episodes
|
|
||||||
const qs: {
|
|
||||||
limit: number,
|
|
||||||
sort: string,
|
|
||||||
sort_direction: string,
|
|
||||||
title_id: number,
|
|
||||||
language?: string
|
|
||||||
} = { limit: -1, sort: 'order', sort_direction: 'ASC', title_id: parseInt(argv.s as string) };
|
|
||||||
if(argv.alt){ qs.language = 'English'; }
|
|
||||||
const episodesData = await getData({
|
|
||||||
baseUrl: api_host,
|
|
||||||
url: '/funimation/episodes/',
|
|
||||||
querystring: qs,
|
|
||||||
token: token,
|
|
||||||
useToken: true,
|
|
||||||
debug: argv.debug,
|
|
||||||
});
|
|
||||||
if(!episodesData.ok || !episodesData.res){return;}
|
|
||||||
|
|
||||||
let epsDataArr: Item[] = JSON.parse(episodesData.res.body).items;
|
|
||||||
const epNumRegex = /^([A-Z0-9]*[A-Z])?(\d+)$/i;
|
|
||||||
const epSelEpsTxt = []; let typeIdLen = 0, epIdLen = 4;
|
|
||||||
|
|
||||||
const parseEpStr = (epStr: string) => {
|
|
||||||
const match = epStr.match(epNumRegex);
|
|
||||||
if (!match) {
|
|
||||||
console.error('[ERROR] No match found');
|
|
||||||
return ['', ''];
|
|
||||||
}
|
|
||||||
if(match.length > 2){
|
|
||||||
const spliced = [...match].splice(1);
|
|
||||||
spliced[0] = spliced[0] ? spliced[0] : '';
|
|
||||||
return spliced;
|
|
||||||
}
|
|
||||||
else return [ '', match[0] ];
|
|
||||||
};
|
|
||||||
|
|
||||||
epsDataArr = epsDataArr.map(e => {
|
|
||||||
const baseId = e.ids.externalAsianId ? e.ids.externalAsianId : e.ids.externalEpisodeId;
|
|
||||||
e.id = baseId.replace(new RegExp('^' + e.ids.externalShowId), '');
|
|
||||||
if(e.id.match(epNumRegex)){
|
|
||||||
const epMatch = parseEpStr(e.id);
|
|
||||||
epIdLen = epMatch[1].length > epIdLen ? epMatch[1].length : epIdLen;
|
|
||||||
typeIdLen = epMatch[0].length > typeIdLen ? epMatch[0].length : typeIdLen;
|
|
||||||
e.id_split = epMatch;
|
|
||||||
}
|
|
||||||
else{
|
|
||||||
typeIdLen = 3 > typeIdLen? 3 : typeIdLen;
|
|
||||||
console.log('[ERROR] FAILED TO PARSE: ', e.id);
|
|
||||||
e.id_split = [ 'ZZZ', 9999 ];
|
|
||||||
}
|
|
||||||
return e;
|
|
||||||
});
|
|
||||||
|
|
||||||
const epSelList = parseSelect(argv.e as string, argv.but);
|
|
||||||
|
|
||||||
const fnSlug: {
|
|
||||||
title: string,
|
|
||||||
episode: string,
|
|
||||||
episodeID: string
|
|
||||||
}[] = []; let is_selected = false;
|
|
||||||
|
|
||||||
const eps = epsDataArr;
|
|
||||||
epsDataArr.sort((a, b) => {
|
|
||||||
if (a.item.seasonOrder < b.item.seasonOrder && a.id.localeCompare(b.id) < 0) {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
if (a.item.seasonOrder > b.item.seasonOrder && a.id.localeCompare(b.id) > 0) {
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
});
|
|
||||||
|
|
||||||
for(const e in eps){
|
|
||||||
eps[e].id_split[1] = parseInt(eps[e].id_split[1].toString()).toString().padStart(epIdLen, '0');
|
|
||||||
let epStrId = eps[e].id_split.join('');
|
|
||||||
// select
|
|
||||||
is_selected = false;
|
|
||||||
if (argv.all || epSelList.isSelected(epStrId)) {
|
|
||||||
fnSlug.push({title:eps[e].item.titleSlug,episode:eps[e].item.episodeSlug, episodeID:epStrId});
|
|
||||||
epSelEpsTxt.push(epStrId);
|
|
||||||
is_selected = true;
|
|
||||||
}
|
|
||||||
else{
|
|
||||||
is_selected = false;
|
|
||||||
}
|
|
||||||
// console vars
|
|
||||||
const tx_snum = eps[e].item.seasonNum=='1'?'':` S${eps[e].item.seasonNum}`;
|
|
||||||
const tx_type = eps[e].mediaCategory != 'episode' ? eps[e].mediaCategory : '';
|
|
||||||
const tx_enum = eps[e].item.episodeNum && eps[e].item.episodeNum !== '' ?
|
|
||||||
`#${(parseInt(eps[e].item.episodeNum) < 10 ? '0' : '')+eps[e].item.episodeNum}` : '#'+eps[e].item.episodeId;
|
|
||||||
const qua_str = eps[e].quality.height ? eps[e].quality.quality + eps[e].quality.height : 'UNK';
|
|
||||||
const aud_str = eps[e].audio.length > 0 ? `, ${eps[e].audio.join(', ')}` : '';
|
|
||||||
const rtm_str = eps[e].item.runtime !== '' ? eps[e].item.runtime : '??:??';
|
|
||||||
// console string
|
|
||||||
eps[e].id_split[0] = eps[e].id_split[0].toString().padStart(typeIdLen, ' ');
|
|
||||||
epStrId = eps[e].id_split.join('');
|
|
||||||
let conOut = `[${epStrId}] `;
|
|
||||||
conOut += `${eps[e].item.titleName+tx_snum} - ${tx_type+tx_enum} ${eps[e].item.episodeName} `;
|
|
||||||
conOut += `(${rtm_str}) [${qua_str+aud_str}]`;
|
|
||||||
conOut += is_selected ? ' (selected)' : '';
|
|
||||||
conOut += eps.length-1 == parseInt(e) ? '\n' : '';
|
|
||||||
console.log(conOut);
|
|
||||||
}
|
|
||||||
if(fnSlug.length < 1){
|
|
||||||
console.log('[INFO] Episodes not selected!\n');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
else{
|
|
||||||
console.log('[INFO] Selected Episodes: %s\n',epSelEpsTxt.join(', '));
|
|
||||||
for(let fnEp=0;fnEp<fnSlug.length;fnEp++){
|
|
||||||
if (await getEpisode(fnSlug[fnEp]) !== true)
|
|
||||||
ok = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ok;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getEpisode(fnSlug: {
|
|
||||||
title: string,
|
|
||||||
episode: string,
|
|
||||||
episodeID: string
|
|
||||||
}) {
|
|
||||||
const episodeData = await getData({
|
|
||||||
baseUrl: api_host,
|
|
||||||
url: `/source/catalog/episode/${fnSlug.title}/${fnSlug.episode}/`,
|
|
||||||
token: token,
|
|
||||||
useToken: true,
|
|
||||||
debug: argv.debug,
|
|
||||||
});
|
|
||||||
if(!episodeData.ok || !episodeData.res){return;}
|
|
||||||
const ep = JSON.parse(episodeData.res.body).items[0] as EpisodeData, streamIds = [];
|
|
||||||
// build fn
|
|
||||||
season = parseInt(ep.parent.seasonNumber);
|
|
||||||
if(ep.mediaCategory != 'Episode'){
|
|
||||||
ep.number = ep.number !== '' ? ep.mediaCategory+ep.number : ep.mediaCategory+'#'+ep.id;
|
|
||||||
}
|
|
||||||
fnEpNum = isNaN(parseInt(ep.number)) ? ep.number : parseInt(ep.number);
|
|
||||||
|
|
||||||
// is uncut
|
|
||||||
const uncut = {
|
|
||||||
Japanese: false,
|
|
||||||
English: false
|
|
||||||
};
|
|
||||||
|
|
||||||
// end
|
|
||||||
console.log(
|
|
||||||
'[INFO] %s - S%sE%s - %s',
|
|
||||||
ep.parent.title,
|
|
||||||
(ep.parent.seasonNumber ? ep.parent.seasonNumber : '?'),
|
|
||||||
(ep.number ? ep.number : '?'),
|
|
||||||
ep.title
|
|
||||||
);
|
|
||||||
|
|
||||||
console.log('[INFO] Available streams (Non-Encrypted):');
|
|
||||||
|
|
||||||
// map medias
|
|
||||||
const media = ep.media.map(function(m){
|
|
||||||
if(m.mediaType == 'experience'){
|
|
||||||
if(m.version.match(/uncut/i) && m.language){
|
|
||||||
uncut[m.language] = true;
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
id: m.id,
|
|
||||||
language: m.language,
|
|
||||||
version: m.version,
|
|
||||||
type: m.experienceType,
|
|
||||||
subtitles: getSubsUrl(m.mediaChildren, m.language)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
else{
|
|
||||||
return { id: 0, type: '' };
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// select
|
|
||||||
stDlPath = [];
|
|
||||||
for(const m of media){
|
|
||||||
let selected = false;
|
|
||||||
if(m.id > 0 && m.type == 'Non-Encrypted'){
|
|
||||||
const dub_type = m.language;
|
|
||||||
if (!dub_type)
|
|
||||||
continue;
|
|
||||||
let localSubs: Subtitle[] = [];
|
|
||||||
const selUncut = !argv.simul && uncut[dub_type] && m.version?.match(/uncut/i)
|
|
||||||
? true
|
|
||||||
: (!uncut[dub_type] || argv.simul && m.version?.match(/simulcast/i) ? true : false);
|
|
||||||
for (const curDub of argv.dubLang) {
|
|
||||||
const item = langsData.languages.find(a => a.code === curDub);
|
|
||||||
if(item && dub_type == (item.funi_name || item.name) && selUncut){
|
|
||||||
streamIds.push({
|
|
||||||
id: m.id,
|
|
||||||
lang: item
|
|
||||||
});
|
|
||||||
stDlPath.push(...m.subtitles);
|
|
||||||
localSubs = m.subtitles;
|
|
||||||
selected = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
console.log(`[#${m.id}] ${dub_type} [${m.version}]${(selected?' (selected)':'')}${
|
|
||||||
localSubs && localSubs.length > 0 && selected ? ` (using ${localSubs.map(a => `'${a.lang.name}'`).join(', ')} for subtitles)` : ''
|
|
||||||
}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const already: string[] = [];
|
|
||||||
stDlPath = stDlPath.filter(a => {
|
|
||||||
if (already.includes(`${a.closedCaption ? 'cc' : ''}-${a.lang.code}`)) {
|
|
||||||
return false;
|
|
||||||
} else {
|
|
||||||
already.push(`${a.closedCaption ? 'cc' : ''}-${a.lang.code}`);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
if(streamIds.length < 1){
|
|
||||||
console.log('[ERROR] Track not selected\n');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
else{
|
|
||||||
tsDlPath = [];
|
|
||||||
for (const streamId of streamIds) {
|
|
||||||
const streamData = await getData({
|
|
||||||
baseUrl: api_host,
|
|
||||||
url: `/source/catalog/video/${streamId.id}/signed`,
|
|
||||||
token: token,
|
|
||||||
dinstid: 'uuid',
|
|
||||||
useToken: true,
|
|
||||||
debug: argv.debug,
|
|
||||||
});
|
|
||||||
if(!streamData.ok || !streamData.res){return;}
|
|
||||||
const streamDataRes = JSON.parse(streamData.res.body) as StreamData;
|
|
||||||
if(streamDataRes.errors){
|
|
||||||
console.log('[ERROR] Error #%s: %s\n',streamDataRes.errors[0].code,streamDataRes.errors[0].detail);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
else{
|
|
||||||
for(const u in streamDataRes.items){
|
|
||||||
if(streamDataRes.items[u].videoType == 'm3u8'){
|
|
||||||
tsDlPath.push({
|
|
||||||
path: streamDataRes.items[u].src,
|
|
||||||
lang: streamId.lang
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if(tsDlPath.length < 1){
|
|
||||||
console.log('[ERROR] Unknown error\n');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
else{
|
|
||||||
const res = await downloadStreams({
|
|
||||||
id: fnSlug.episodeID,
|
|
||||||
title: ep.title,
|
|
||||||
showTitle: ep.parent.title
|
|
||||||
});
|
|
||||||
if (res === true) {
|
|
||||||
downloaded({
|
|
||||||
service: 'funi',
|
|
||||||
type: 's'
|
|
||||||
}, argv.s as string, [fnSlug.episodeID]);
|
|
||||||
}
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getSubsUrl(m: MediaChild[], parentLanguage: TitleElement|undefined) : Subtitle[] {
|
|
||||||
if((argv.nosubs && !argv.sub) || argv.dlsubs.includes('none')){
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
const found: Subtitle[] = [];
|
|
||||||
|
|
||||||
const media = m.filter(a => a.filePath.split('.').pop() === 'vtt');
|
|
||||||
for (const me of media) {
|
|
||||||
const lang = langsData.languages.find(a => me.language === (a.funi_name || a.name));
|
|
||||||
if (!lang) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const pLang = langsData.languages.find(a => (a.funi_name || a.name) === parentLanguage);
|
|
||||||
if (argv.dlsubs.includes('all') || argv.dlsubs.some(a => a === lang.locale)) {
|
|
||||||
found.push({
|
|
||||||
url: me.filePath,
|
|
||||||
ext: `.${lang.code}${pLang?.code === lang.code ? '.cc' : ''}`,
|
|
||||||
lang,
|
|
||||||
closedCaption: pLang?.code === lang.code
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return found;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function downloadStreams(epsiode: FunimationMediaDownload){
|
|
||||||
|
|
||||||
// req playlist
|
|
||||||
|
|
||||||
const purvideo: DownloadedFile[] = [];
|
|
||||||
const puraudio: DownloadedFile[] = [];
|
|
||||||
const audioAndVideo: DownloadedFile[] = [];
|
|
||||||
for (const streamPath of tsDlPath) {
|
|
||||||
const plQualityReq = await getData({
|
|
||||||
url: streamPath.path,
|
|
||||||
debug: argv.debug,
|
|
||||||
});
|
|
||||||
if(!plQualityReq.ok || !plQualityReq.res){return;}
|
|
||||||
|
|
||||||
const plQualityLinkList = m3u8(plQualityReq.res.body);
|
|
||||||
|
|
||||||
const mainServersList = [
|
|
||||||
'vmfst-api.prd.funimationsvc.com',
|
|
||||||
'd33et77evd9bgg.cloudfront.net',
|
|
||||||
'd132fumi6di1wa.cloudfront.net',
|
|
||||||
'funiprod.akamaized.net',
|
|
||||||
];
|
|
||||||
|
|
||||||
const plServerList: string[] = [],
|
|
||||||
plStreams: Record<string|number, {
|
|
||||||
[key: string]: string
|
|
||||||
}> = {},
|
|
||||||
plLayersStr = [],
|
|
||||||
plLayersRes: Record<string|number, {
|
|
||||||
width: number,
|
|
||||||
height: number
|
|
||||||
}> = {};
|
|
||||||
let plMaxLayer = 1,
|
|
||||||
plNewIds = 1,
|
|
||||||
plAud: undefined|{
|
|
||||||
uri: string
|
|
||||||
language: langsData.LanguageItem
|
|
||||||
};
|
|
||||||
|
|
||||||
// new uris
|
|
||||||
const vplReg = /streaming_video_(\d+)_(\d+)_(\d+)_index\.m3u8/;
|
|
||||||
if(plQualityLinkList.playlists[0].uri.match(vplReg)){
|
|
||||||
const audioKey = Object.keys(plQualityLinkList.mediaGroups.AUDIO).pop();
|
|
||||||
if (!audioKey)
|
|
||||||
return console.log('[ERROR] No audio key found');
|
|
||||||
if(plQualityLinkList.mediaGroups.AUDIO[audioKey]){
|
|
||||||
const audioDataParts = plQualityLinkList.mediaGroups.AUDIO[audioKey],
|
|
||||||
audioEl = Object.keys(audioDataParts);
|
|
||||||
const audioData = audioDataParts[audioEl[0]];
|
|
||||||
let language = langsData.languages.find(a => a.locale === audioData.language);
|
|
||||||
if (!language) {
|
|
||||||
language = langsData.languages.find(a => a.funi_name || a.name === audioEl[0]);
|
|
||||||
if (!language) {
|
|
||||||
console.log(`[ERROR] Unable to find language for locale ${audioData.language} or name ${audioEl[0]}`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
plAud = {
|
|
||||||
uri: audioData.uri,
|
|
||||||
language: language
|
|
||||||
};
|
|
||||||
}
|
|
||||||
plQualityLinkList.playlists.sort((a, b) => {
|
|
||||||
const aMatch = a.uri.match(vplReg), bMatch = b.uri.match(vplReg);
|
|
||||||
if (!aMatch || !bMatch) {
|
|
||||||
console.log('[ERROR] Unable to match');
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
const av = parseInt(aMatch[3]);
|
|
||||||
const bv = parseInt(bMatch[3]);
|
|
||||||
if(av > bv){
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
if (av < bv) {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
for(const s of plQualityLinkList.playlists){
|
|
||||||
if(s.uri.match(/_Layer(\d+)\.m3u8/) || s.uri.match(vplReg)){
|
|
||||||
// set layer and max layer
|
|
||||||
let plLayerId: number|string = 0;
|
|
||||||
const match = s.uri.match(/_Layer(\d+)\.m3u8/);
|
|
||||||
if(match){
|
|
||||||
plLayerId = parseInt(match[1]);
|
|
||||||
}
|
|
||||||
else{
|
|
||||||
plLayerId = plNewIds, plNewIds++;
|
|
||||||
}
|
|
||||||
plMaxLayer = plMaxLayer < plLayerId ? plLayerId : plMaxLayer;
|
|
||||||
// set urls and servers
|
|
||||||
const plUrlDl = s.uri;
|
|
||||||
const plServer = new URL(plUrlDl).host;
|
|
||||||
if(!plServerList.includes(plServer)){
|
|
||||||
plServerList.push(plServer);
|
|
||||||
}
|
|
||||||
if(!Object.keys(plStreams).includes(plServer)){
|
|
||||||
plStreams[plServer] = {};
|
|
||||||
}
|
|
||||||
if(plStreams[plServer][plLayerId] && plStreams[plServer][plLayerId] != plUrlDl){
|
|
||||||
console.log(`[WARN] Non duplicate url for ${plServer} detected, please report to developer!`);
|
|
||||||
}
|
|
||||||
else{
|
|
||||||
plStreams[plServer][plLayerId] = plUrlDl;
|
|
||||||
}
|
|
||||||
// set plLayersStr
|
|
||||||
const plResolution = s.attributes.RESOLUTION;
|
|
||||||
plLayersRes[plLayerId] = plResolution;
|
|
||||||
const plBandwidth = Math.round(s.attributes.BANDWIDTH/1024);
|
|
||||||
if(plLayerId<10){
|
|
||||||
plLayerId = plLayerId.toString().padStart(2,' ');
|
|
||||||
}
|
|
||||||
const qualityStrAdd = `${plLayerId}: ${plResolution.width}x${plResolution.height} (${plBandwidth}KiB/s)`;
|
|
||||||
const qualityStrRegx = new RegExp(qualityStrAdd.replace(/(:|\(|\)|\/)/g,'\\$1'),'m');
|
|
||||||
const qualityStrMatch = !plLayersStr.join('\r\n').match(qualityStrRegx);
|
|
||||||
if(qualityStrMatch){
|
|
||||||
plLayersStr.push(qualityStrAdd);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
console.log(s.uri);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for(const s of mainServersList){
|
|
||||||
if(plServerList.includes(s)){
|
|
||||||
plServerList.splice(plServerList.indexOf(s), 1);
|
|
||||||
plServerList.unshift(s);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
const plSelectedServer = plServerList[argv.x-1];
|
|
||||||
const plSelectedList = plStreams[plSelectedServer];
|
|
||||||
|
|
||||||
plLayersStr.sort();
|
|
||||||
console.log(`[INFO] Servers available:\n\t${plServerList.join('\n\t')}`);
|
|
||||||
console.log(`[INFO] Available qualities:\n\t${plLayersStr.join('\n\t')}`);
|
|
||||||
|
|
||||||
const selectedQuality = argv.q === 0 || argv.q > Object.keys(plLayersRes).length
|
|
||||||
? Object.keys(plLayersRes).pop() as string
|
|
||||||
: argv.q;
|
|
||||||
const videoUrl = argv.x < plServerList.length+1 && plSelectedList[selectedQuality] ? plSelectedList[selectedQuality] : '';
|
|
||||||
|
|
||||||
if(videoUrl != ''){
|
|
||||||
console.log(`[INFO] Selected layer: ${selectedQuality} (${plLayersRes[selectedQuality].width}x${plLayersRes[selectedQuality].height}) @ ${plSelectedServer}`);
|
|
||||||
console.log('[INFO] Stream URL:',videoUrl);
|
|
||||||
|
|
||||||
fnOutput = parseFileName(argv.fileName, ([
|
|
||||||
['episode', isNaN(parseInt(fnEpNum as string)) ? fnEpNum : parseInt(fnEpNum as string)],
|
|
||||||
['title', epsiode.title],
|
|
||||||
['showTitle', epsiode.showTitle],
|
|
||||||
['season', season],
|
|
||||||
['width', plLayersRes[selectedQuality].width],
|
|
||||||
['height', plLayersRes[selectedQuality].height],
|
|
||||||
['service', 'Funimation']
|
|
||||||
] as [AvailableFilenameVars, string|number][]).map((a): Variable => {
|
|
||||||
return {
|
|
||||||
name: a[0],
|
|
||||||
replaceWith: a[1],
|
|
||||||
type: typeof a[1],
|
|
||||||
} as Variable;
|
|
||||||
}), argv.numbers);
|
|
||||||
if (fnOutput.length < 1)
|
|
||||||
throw new Error(`Invalid path generated for input ${argv.fileName}`);
|
|
||||||
console.log(`[INFO] Output filename: ${fnOutput.join(path.sep)}.ts`);
|
|
||||||
}
|
|
||||||
else if(argv.x > plServerList.length){
|
|
||||||
console.log('[ERROR] Server not selected!\n');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
else{
|
|
||||||
console.log('[ERROR] Layer not selected!\n');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let dlFailed = false;
|
|
||||||
let dlFailedA = false;
|
|
||||||
|
|
||||||
await fs.promises.mkdir(path.join(cfg.dir.content, ...fnOutput.slice(0, -1)), { recursive: true });
|
|
||||||
|
|
||||||
video: if (!argv.novids) {
|
|
||||||
if (plAud && (purvideo.length > 0 || audioAndVideo.length > 0)) {
|
|
||||||
break video;
|
|
||||||
} else if (!plAud && (audioAndVideo.some(a => a.lang === streamPath.lang) || puraudio.some(a => a.lang === streamPath.lang))) {
|
|
||||||
break video;
|
|
||||||
}
|
|
||||||
// download video
|
|
||||||
const reqVideo = await getData({
|
|
||||||
url: videoUrl,
|
|
||||||
debug: argv.debug,
|
|
||||||
});
|
|
||||||
if (!reqVideo.ok || !reqVideo.res) { break video; }
|
|
||||||
|
|
||||||
const chunkList = m3u8(reqVideo.res.body);
|
|
||||||
|
|
||||||
const tsFile = path.join(cfg.dir.content, ...fnOutput.slice(0, -1), `${fnOutput.slice(-1)}.video${(plAud?.uri ? '' : '.' + streamPath.lang.code )}`);
|
|
||||||
dlFailed = !await downloadFile(tsFile, chunkList);
|
|
||||||
if (!dlFailed) {
|
|
||||||
if (plAud) {
|
|
||||||
purvideo.push({
|
|
||||||
path: `${tsFile}.ts`,
|
|
||||||
lang: plAud.language
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
audioAndVideo.push({
|
|
||||||
path: `${tsFile}.ts`,
|
|
||||||
lang: streamPath.lang
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else{
|
|
||||||
console.log('[INFO] Skip video downloading...\n');
|
|
||||||
}
|
|
||||||
audio: if (plAud && !argv.noaudio) {
|
|
||||||
// download audio
|
|
||||||
if (audioAndVideo.some(a => a.lang === plAud?.language) || puraudio.some(a => a.lang === plAud?.language))
|
|
||||||
break audio;
|
|
||||||
const reqAudio = await getData({
|
|
||||||
url: plAud.uri,
|
|
||||||
debug: argv.debug,
|
|
||||||
});
|
|
||||||
if (!reqAudio.ok || !reqAudio.res) { return; }
|
|
||||||
|
|
||||||
const chunkListA = m3u8(reqAudio.res.body);
|
|
||||||
|
|
||||||
const tsFileA = path.join(cfg.dir.content, ...fnOutput.slice(0, -1), `${fnOutput.slice(-1)}.audio.${plAud.language.code}`);
|
|
||||||
|
|
||||||
dlFailedA = !await downloadFile(tsFileA, chunkListA);
|
|
||||||
if (!dlFailedA)
|
|
||||||
puraudio.push({
|
|
||||||
path: `${tsFileA}.ts`,
|
|
||||||
lang: plAud.language
|
|
||||||
});
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// add subs
|
|
||||||
const subsExt = !argv.mp4 || argv.mp4 && argv.ass ? '.ass' : '.srt';
|
|
||||||
let addSubs = true;
|
|
||||||
|
|
||||||
// download subtitles
|
|
||||||
if(stDlPath.length > 0){
|
|
||||||
console.log('[INFO] Downloading subtitles...');
|
|
||||||
for (const subObject of stDlPath) {
|
|
||||||
const subsSrc = await getData({
|
|
||||||
url: subObject.url,
|
|
||||||
debug: argv.debug,
|
|
||||||
});
|
|
||||||
if(subsSrc.ok && subsSrc.res){
|
|
||||||
const assData = vttConvert(subsSrc.res.body, (subsExt == '.srt' ? true : false), subObject.lang.name, argv.fontSize, argv.fontName);
|
|
||||||
subObject.out = path.join(cfg.dir.content, ...fnOutput.slice(0, -1), `${fnOutput.slice(-1)}.subtitle${subObject.ext}${subsExt}`);
|
|
||||||
fs.writeFileSync(subObject.out, assData);
|
|
||||||
}
|
|
||||||
else{
|
|
||||||
console.log('[ERROR] Failed to download subtitles!');
|
|
||||||
addSubs = false;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (addSubs)
|
|
||||||
console.log('[INFO] Subtitles downloaded!');
|
|
||||||
}
|
|
||||||
|
|
||||||
if((puraudio.length < 1 && audioAndVideo.length < 1) || (purvideo.length < 1 && audioAndVideo.length < 1)){
|
|
||||||
console.log('\n[INFO] Unable to locate a video AND audio file\n');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(argv.skipmux){
|
|
||||||
console.log('[INFO] Skipping muxing...');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// check exec
|
|
||||||
const mergerBin = merger.checkMerger(cfg.bin, argv.mp4, argv.forceMuxer);
|
|
||||||
|
|
||||||
if ( argv.novids ){
|
|
||||||
console.log('[INFO] Video not downloaded. Skip muxing video.');
|
|
||||||
}
|
|
||||||
|
|
||||||
const ffext = !argv.mp4 ? 'mkv' : 'mp4';
|
|
||||||
const mergeInstance = new merger({
|
|
||||||
onlyAudio: puraudio,
|
|
||||||
onlyVid: purvideo,
|
|
||||||
output: `${path.join(cfg.dir.content, ...fnOutput)}.${ffext}`,
|
|
||||||
subtitles: stDlPath.map(a => {
|
|
||||||
return {
|
|
||||||
file: a.out as string,
|
|
||||||
language: a.lang,
|
|
||||||
title: a.lang.name,
|
|
||||||
closedCaption: a.closedCaption
|
|
||||||
};
|
|
||||||
}),
|
|
||||||
videoAndAudio: audioAndVideo,
|
|
||||||
simul: argv.simul,
|
|
||||||
skipSubMux: argv.skipSubMux
|
|
||||||
});
|
|
||||||
|
|
||||||
if(mergerBin.MKVmerge){
|
|
||||||
const command = mergeInstance.MkvMerge();
|
|
||||||
shlp.exec('mkvmerge', `"${mergerBin.MKVmerge}"`, command);
|
|
||||||
}
|
|
||||||
else if(mergerBin.FFmpeg){
|
|
||||||
const command = mergeInstance.FFmpeg();
|
|
||||||
shlp.exec('ffmpeg',`"${mergerBin.FFmpeg}"`,command);
|
|
||||||
}
|
|
||||||
else{
|
|
||||||
console.log('\n[INFO] Done!\n');
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (argv.nocleanup) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
mergeInstance.cleanUp();
|
|
||||||
console.log('\n[INFO] Done!\n');
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function downloadFile(filename: string, chunkList: {
|
|
||||||
segments: Record<string, unknown>[],
|
|
||||||
}) {
|
|
||||||
const downloadStatus = await new hlsDownload({
|
|
||||||
m3u8json: chunkList,
|
|
||||||
output: `${filename + '.ts'}`,
|
|
||||||
timeout: argv.timeout,
|
|
||||||
threads: argv.partsize,
|
|
||||||
fsRetryTime: argv.fsRetryTime * 1000
|
|
||||||
}).download();
|
|
||||||
|
|
||||||
return downloadStatus.ok;
|
|
||||||
}
|
|
||||||
3
gui.ts
Normal file
3
gui.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
process.env.isGUI = 'true';
|
||||||
|
import './modules/log';
|
||||||
|
import './gui/server/index';
|
||||||
3
gui/react/.babelrc
Normal file
3
gui/react/.babelrc
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
{
|
||||||
|
"presets": ["@babel/preset-env","@babel/preset-react", "@babel/preset-typescript"]
|
||||||
|
}
|
||||||
2
gui/react/.env
Normal file
2
gui/react/.env
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
PORT=3002
|
||||||
|
CI=false
|
||||||
57
gui/react/package.json
Normal file
57
gui/react/package.json
Normal file
|
|
@ -0,0 +1,57 @@
|
||||||
|
{
|
||||||
|
"name": "anidl-gui",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"private": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@emotion/react": "^11.11.4",
|
||||||
|
"@emotion/styled": "^11.11.5",
|
||||||
|
"@mui/icons-material": "^5.15.20",
|
||||||
|
"@mui/lab": "^5.0.0-alpha.170",
|
||||||
|
"@mui/material": "^5.15.20",
|
||||||
|
"concurrently": "^8.2.2",
|
||||||
|
"notistack": "^2.0.8",
|
||||||
|
"react": "^18.3.1",
|
||||||
|
"react-dom": "^18.3.1",
|
||||||
|
"typescript": "^5.5.2",
|
||||||
|
"uuid": "^9.0.1",
|
||||||
|
"ws": "^8.17.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@babel/cli": "^7.24.7",
|
||||||
|
"@babel/core": "^7.24.7",
|
||||||
|
"@babel/preset-env": "^7.24.7",
|
||||||
|
"@babel/preset-react": "^7.24.7",
|
||||||
|
"@babel/preset-typescript": "^7.24.7",
|
||||||
|
"@types/node": "^20.14.6",
|
||||||
|
"@types/react": "^18.3.3",
|
||||||
|
"@types/react-dom": "^18.3.0",
|
||||||
|
"@types/uuid": "^9.0.8",
|
||||||
|
"babel-loader": "^9.1.3",
|
||||||
|
"css-loader": "^7.1.2",
|
||||||
|
"html-webpack-plugin": "^5.6.0",
|
||||||
|
"style-loader": "^3.3.4",
|
||||||
|
"ts-node": "^10.9.2",
|
||||||
|
"webpack": "^5.92.1",
|
||||||
|
"webpack-cli": "^5.1.4",
|
||||||
|
"webpack-dev-server": "^5.0.4"
|
||||||
|
},
|
||||||
|
"proxy": "http://localhost:3000",
|
||||||
|
"scripts": {
|
||||||
|
"build": "npx tsc && npx webpack",
|
||||||
|
"start": "npx concurrently -k npm:frontend npm:backend",
|
||||||
|
"frontend": "npx webpack-dev-server",
|
||||||
|
"backend": "npx ts-node -T ../../gui.ts"
|
||||||
|
},
|
||||||
|
"browserslist": {
|
||||||
|
"production": [
|
||||||
|
">0.2%",
|
||||||
|
"not dead",
|
||||||
|
"not op_mini all"
|
||||||
|
],
|
||||||
|
"development": [
|
||||||
|
"last 1 chrome version",
|
||||||
|
"last 1 firefox version",
|
||||||
|
"last 1 safari version"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
5482
gui/react/pnpm-lock.yaml
Normal file
5482
gui/react/pnpm-lock.yaml
Normal file
File diff suppressed because it is too large
Load diff
BIN
gui/react/public/favicon.webp
Normal file
BIN
gui/react/public/favicon.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.2 KiB |
15
gui/react/public/index.html
Normal file
15
gui/react/public/index.html
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Multi Downloader</title>
|
||||||
|
<link rel="icon" type="image/webp" href="favicon.webp">
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta
|
||||||
|
http-equiv="Content-Security-Policy"
|
||||||
|
content="script-src 'self' 'unsafe-eval'"
|
||||||
|
/>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
BIN
gui/react/public/notFound.png
Normal file
BIN
gui/react/public/notFound.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.3 KiB |
3
gui/react/src/@types/FC.d.ts
vendored
Normal file
3
gui/react/src/@types/FC.d.ts
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
type FCWithChildren<T = object> = React.FC<{
|
||||||
|
children?: React.ReactNode[]|React.ReactNode
|
||||||
|
} & T>
|
||||||
10
gui/react/src/App.tsx
Normal file
10
gui/react/src/App.tsx
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
import React from 'react';
|
||||||
|
import Layout from './Layout';
|
||||||
|
|
||||||
|
const App: React.FC = () => {
|
||||||
|
return (
|
||||||
|
<Layout />
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default App;
|
||||||
38
gui/react/src/Layout.tsx
Normal file
38
gui/react/src/Layout.tsx
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
import React from 'react';
|
||||||
|
import AuthButton from './components/AuthButton';
|
||||||
|
import { Box, Button } from '@mui/material';
|
||||||
|
import MainFrame from './components/MainFrame/MainFrame';
|
||||||
|
import LogoutButton from './components/LogoutButton';
|
||||||
|
import AddToQueue from './components/AddToQueue/AddToQueue';
|
||||||
|
import { messageChannelContext } from './provider/MessageChannel';
|
||||||
|
import { ClearAll, Folder } from '@mui/icons-material';
|
||||||
|
import StartQueueButton from './components/StartQueue';
|
||||||
|
import MenuBar from './components/MenuBar/MenuBar';
|
||||||
|
|
||||||
|
const Layout: React.FC = () => {
|
||||||
|
|
||||||
|
const messageHandler = React.useContext(messageChannelContext);
|
||||||
|
|
||||||
|
return <Box sx={{ display: 'flex', flexDirection: 'column', justifyContent: 'center', width: '100%', alignItems: 'center',}}>
|
||||||
|
<MenuBar />
|
||||||
|
<Box sx={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
width: '93vw',
|
||||||
|
maxWidth: '93rem',
|
||||||
|
maxHeight: '3rem'
|
||||||
|
//backgroundColor: '#ffffff',
|
||||||
|
}}>
|
||||||
|
<LogoutButton />
|
||||||
|
<AuthButton />
|
||||||
|
<Button variant="contained" startIcon={<Folder />} onClick={() => messageHandler?.openFolder('content')} sx={{ height: '37px' }}>Open Output Directory</Button>
|
||||||
|
<Button variant="contained" startIcon={<ClearAll />} onClick={() => messageHandler?.clearQueue() } sx={{ height: '37px' }}>Clear Queue</Button>
|
||||||
|
<AddToQueue />
|
||||||
|
<StartQueueButton />
|
||||||
|
</Box>
|
||||||
|
<MainFrame />
|
||||||
|
</Box>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Layout;
|
||||||
19
gui/react/src/Style.tsx
Normal file
19
gui/react/src/Style.tsx
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { Container, Box, ThemeProvider, createTheme, Theme } from '@mui/material';
|
||||||
|
|
||||||
|
const makeTheme = (mode: 'dark'|'light') : Partial<Theme> => {
|
||||||
|
return createTheme({
|
||||||
|
palette: {
|
||||||
|
mode,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const Style: FCWithChildren = ({children}) => {
|
||||||
|
return <ThemeProvider theme={makeTheme('dark')}>
|
||||||
|
<Box sx={{ }}/>
|
||||||
|
{children}
|
||||||
|
</ThemeProvider>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Style;
|
||||||
27
gui/react/src/components/AddToQueue/AddToQueue.tsx
Normal file
27
gui/react/src/components/AddToQueue/AddToQueue.tsx
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
import { Add } from '@mui/icons-material';
|
||||||
|
import { Box, Button, Dialog, Divider, Typography } from '@mui/material';
|
||||||
|
import React from 'react';
|
||||||
|
import DownloadSelector from './DownloadSelector/DownloadSelector';
|
||||||
|
import EpisodeListing from './DownloadSelector/Listing/EpisodeListing';
|
||||||
|
import SearchBox from './SearchBox/SearchBox';
|
||||||
|
|
||||||
|
const AddToQueue: React.FC = () => {
|
||||||
|
const [isOpen, setOpen] = React.useState(false);
|
||||||
|
|
||||||
|
return <Box>
|
||||||
|
<EpisodeListing />
|
||||||
|
<Dialog open={isOpen} onClose={() => setOpen(false)} maxWidth='md' PaperProps={{ elevation:4 }}>
|
||||||
|
<Box>
|
||||||
|
<SearchBox />
|
||||||
|
<Divider variant='middle'/>
|
||||||
|
<DownloadSelector onFinish={() => setOpen(false)} />
|
||||||
|
</Box>
|
||||||
|
</Dialog>
|
||||||
|
<Button variant='contained' onClick={() => setOpen(true)} sx={{ maxHeight: '2.3rem' }}>
|
||||||
|
<Add />
|
||||||
|
Add to Queue
|
||||||
|
</Button>
|
||||||
|
</Box>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AddToQueue;
|
||||||
|
|
@ -0,0 +1,327 @@
|
||||||
|
import React, { ChangeEvent } from 'react';
|
||||||
|
import { Box, Button, Divider, FormControl, InputBase, InputLabel, Link, MenuItem, Select, TextField, Tooltip, Typography } from '@mui/material';
|
||||||
|
import useStore from '../../../hooks/useStore';
|
||||||
|
import MultiSelect from '../../reusable/MultiSelect';
|
||||||
|
import { messageChannelContext } from '../../../provider/MessageChannel';
|
||||||
|
import LoadingButton from '@mui/lab/LoadingButton';
|
||||||
|
import { useSnackbar } from 'notistack';
|
||||||
|
import InfoOutlinedIcon from '@mui/icons-material/InfoOutlined';
|
||||||
|
|
||||||
|
type DownloadSelectorProps = {
|
||||||
|
onFinish?: () => unknown
|
||||||
|
}
|
||||||
|
|
||||||
|
const DownloadSelector: React.FC<DownloadSelectorProps> = ({ onFinish }) => {
|
||||||
|
const messageHandler = React.useContext(messageChannelContext);
|
||||||
|
const [store, dispatch] = useStore();
|
||||||
|
const [availableDubs, setAvailableDubs] = React.useState<string[]>([]);
|
||||||
|
const [availableSubs, setAvailableSubs ] = React.useState<string[]>([]);
|
||||||
|
const [ loading, setLoading ] = React.useState(false);
|
||||||
|
const { enqueueSnackbar } = useSnackbar();
|
||||||
|
const ITEM_HEIGHT = 48;
|
||||||
|
const ITEM_PADDING_TOP = 8;
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
(async () => {
|
||||||
|
/* If we don't wait the response is undefined? */
|
||||||
|
await new Promise((resolve) => setTimeout(() => resolve(undefined), 100));
|
||||||
|
const dubLang = messageHandler?.handleDefault('dubLang');
|
||||||
|
const subLang = messageHandler?.handleDefault('dlsubs');
|
||||||
|
const q = messageHandler?.handleDefault('q');
|
||||||
|
const fileName = messageHandler?.handleDefault('fileName');
|
||||||
|
const dlVideoOnce = messageHandler?.handleDefault('dlVideoOnce');
|
||||||
|
const result = await Promise.all([dubLang, subLang, q, fileName, dlVideoOnce]);
|
||||||
|
dispatch({
|
||||||
|
type: 'downloadOptions',
|
||||||
|
payload: {
|
||||||
|
...store.downloadOptions,
|
||||||
|
dubLang: result[0],
|
||||||
|
dlsubs: result[1],
|
||||||
|
q: result[2],
|
||||||
|
fileName: result[3],
|
||||||
|
dlVideoOnce: result[4],
|
||||||
|
}
|
||||||
|
});
|
||||||
|
setAvailableDubs(await messageHandler?.availableDubCodes() ?? []);
|
||||||
|
setAvailableSubs(await messageHandler?.availableSubCodes() ?? []);
|
||||||
|
})();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const addToQueue = async () => {
|
||||||
|
setLoading(true);
|
||||||
|
const res = await messageHandler?.resolveItems(store.downloadOptions);
|
||||||
|
if (!res)
|
||||||
|
return enqueueSnackbar('The request failed. Please check if the ID is correct.', {
|
||||||
|
variant: 'error'
|
||||||
|
});
|
||||||
|
setLoading(false);
|
||||||
|
if (onFinish)
|
||||||
|
onFinish();
|
||||||
|
};
|
||||||
|
|
||||||
|
const listEpisodes = async () => {
|
||||||
|
if (!store.downloadOptions.id) {
|
||||||
|
return enqueueSnackbar('Please enter a ID', {
|
||||||
|
variant: 'error'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
setLoading(true);
|
||||||
|
const res = await messageHandler?.listEpisodes(store.downloadOptions.id);
|
||||||
|
if (!res || !res.isOk) {
|
||||||
|
setLoading(false);
|
||||||
|
return enqueueSnackbar('The request failed. Please check if the ID is correct.', {
|
||||||
|
variant: 'error'
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
dispatch({
|
||||||
|
type: 'episodeListing',
|
||||||
|
payload: res.value
|
||||||
|
});
|
||||||
|
}
|
||||||
|
setLoading(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
return <Box sx={{ display: 'flex', flexDirection: 'column', justifyContent: 'center', alignItems: 'center' }}>
|
||||||
|
<Box sx={{display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
alignItems: 'center',
|
||||||
|
margin: '5px',
|
||||||
|
}}>
|
||||||
|
<Box sx={{
|
||||||
|
width: '50rem',
|
||||||
|
height: '21rem',
|
||||||
|
margin: '10px',
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
//backgroundColor: '#ffffff30',
|
||||||
|
}}>
|
||||||
|
<Box sx={{
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: '0.7rem',
|
||||||
|
//backgroundColor: '#ff000030'
|
||||||
|
}}>
|
||||||
|
<Typography sx={{fontSize: '1.4rem'}}>
|
||||||
|
General Options
|
||||||
|
</Typography>
|
||||||
|
<TextField value={store.downloadOptions.id} required onChange={e => {
|
||||||
|
dispatch({
|
||||||
|
type: 'downloadOptions',
|
||||||
|
payload: { ...store.downloadOptions, id: e.target.value }
|
||||||
|
});
|
||||||
|
}} label='Show ID'/>
|
||||||
|
<TextField type='number' value={store.downloadOptions.q} required onChange={e => {
|
||||||
|
const parsed = parseInt(e.target.value);
|
||||||
|
if (isNaN(parsed) || parsed < 0 || parsed > 10)
|
||||||
|
return;
|
||||||
|
dispatch({
|
||||||
|
type: 'downloadOptions',
|
||||||
|
payload: { ...store.downloadOptions, q: parsed }
|
||||||
|
});
|
||||||
|
}} label='Quality Level (0 for max)'/>
|
||||||
|
<Box sx={{ display: 'flex', gap: '5px' }}>
|
||||||
|
<Button sx={{ textTransform: 'none'}} onClick={() => dispatch({ type: 'downloadOptions', payload: { ...store.downloadOptions, noaudio: !store.downloadOptions.noaudio } })} variant={store.downloadOptions.noaudio ? 'contained' : 'outlined'}>Skip Audio</Button>
|
||||||
|
<Button sx={{ textTransform: 'none'}} onClick={() => dispatch({ type: 'downloadOptions', payload: { ...store.downloadOptions, novids: !store.downloadOptions.novids } })} variant={store.downloadOptions.novids ? 'contained' : 'outlined'}>Skip Video</Button>
|
||||||
|
</Box>
|
||||||
|
<Button sx={{ textTransform: 'none'}} onClick={() => dispatch({ type: 'downloadOptions', payload: { ...store.downloadOptions, dlVideoOnce: !store.downloadOptions.dlVideoOnce } })} variant={store.downloadOptions.dlVideoOnce ? 'contained' : 'outlined'}>Skip Unnecessary</Button>
|
||||||
|
<Tooltip title={store.service == 'hidive' ? '' :
|
||||||
|
<Typography>
|
||||||
|
Simulcast is only supported on Hidive
|
||||||
|
</Typography>}
|
||||||
|
arrow placement='top'
|
||||||
|
>
|
||||||
|
<Box>
|
||||||
|
<Button sx={{ textTransform: 'none'}} disabled={store.service != 'hidive'} onClick={() => dispatch({ type: 'downloadOptions', payload: { ...store.downloadOptions, simul: !store.downloadOptions.simul } })} variant={store.downloadOptions.simul ? 'contained' : 'outlined'}>Download Simulcast ver.</Button>
|
||||||
|
</Box>
|
||||||
|
</Tooltip>
|
||||||
|
</Box>
|
||||||
|
<Box sx={{
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: '0.7rem',
|
||||||
|
//backgroundColor: '#00000020'
|
||||||
|
}}>
|
||||||
|
<Typography sx={{fontSize: '1.4rem'}}>
|
||||||
|
Episode Options
|
||||||
|
</Typography>
|
||||||
|
<Box sx={{
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
gap: '1px'
|
||||||
|
}}>
|
||||||
|
<Box sx={{
|
||||||
|
borderColor: '#595959',
|
||||||
|
borderStyle: 'solid',
|
||||||
|
borderWidth: '1px',
|
||||||
|
borderRadius: '5px',
|
||||||
|
//backgroundColor: '#ff4567',
|
||||||
|
width: '15rem',
|
||||||
|
height: '3.5rem',
|
||||||
|
display: 'flex',
|
||||||
|
'&:hover' : {
|
||||||
|
borderColor: '#ffffff',
|
||||||
|
},
|
||||||
|
}}>
|
||||||
|
<InputBase sx={{
|
||||||
|
ml: 2,
|
||||||
|
flex: 1,
|
||||||
|
}}
|
||||||
|
disabled={store.downloadOptions.all} value={store.downloadOptions.e} required onChange={e => {
|
||||||
|
dispatch({
|
||||||
|
type: 'downloadOptions',
|
||||||
|
payload: { ...store.downloadOptions, e: e.target.value }
|
||||||
|
});
|
||||||
|
}} placeholder='Episode Select'/>
|
||||||
|
<Divider orientation='vertical'/>
|
||||||
|
<LoadingButton loading={loading} disableElevation disableFocusRipple disableRipple disableTouchRipple onClick={listEpisodes} variant='text' sx={{ textTransform: 'none'}}><Typography>List<br/>Episodes</Typography></LoadingButton>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
<Button sx={{ textTransform: 'none'}} onClick={() => dispatch({ type: 'downloadOptions', payload: { ...store.downloadOptions, all: !store.downloadOptions.all } })} variant={store.downloadOptions.all ? 'contained' : 'outlined'}>Download All</Button>
|
||||||
|
<Button sx={{ textTransform: 'none'}} onClick={() => dispatch({ type: 'downloadOptions', payload: { ...store.downloadOptions, but: !store.downloadOptions.but } })} variant={store.downloadOptions.but ? 'contained' : 'outlined'}>Download All but</Button>
|
||||||
|
</Box>
|
||||||
|
<Box sx={{
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: '0.7rem',
|
||||||
|
//backgroundColor: '#00ff0020'
|
||||||
|
}}>
|
||||||
|
<Typography sx={{fontSize: '1.4rem'}}>
|
||||||
|
Language Options
|
||||||
|
</Typography>
|
||||||
|
<MultiSelect
|
||||||
|
title='Dub Languages'
|
||||||
|
values={availableDubs}
|
||||||
|
selected={store.downloadOptions.dubLang}
|
||||||
|
onChange={(e) => {
|
||||||
|
dispatch({
|
||||||
|
type: 'downloadOptions',
|
||||||
|
payload: { ...store.downloadOptions, dubLang: e }
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
allOption
|
||||||
|
/>
|
||||||
|
|
||||||
|
<MultiSelect
|
||||||
|
title='Sub Languages'
|
||||||
|
values={availableSubs}
|
||||||
|
selected={store.downloadOptions.dlsubs}
|
||||||
|
onChange={(e) => {
|
||||||
|
dispatch({
|
||||||
|
type: 'downloadOptions',
|
||||||
|
payload: { ...store.downloadOptions, dlsubs: e }
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Tooltip title={store.service == 'crunchy' ? '' :
|
||||||
|
<Typography>
|
||||||
|
Hardsubs are only supported on Crunchyroll
|
||||||
|
</Typography>
|
||||||
|
}
|
||||||
|
arrow placement='top'>
|
||||||
|
<Box sx={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
width: '100%',
|
||||||
|
gap: '1rem'
|
||||||
|
}}>
|
||||||
|
|
||||||
|
<Box sx={{
|
||||||
|
borderRadius: '5px',
|
||||||
|
//backgroundColor: '#ff4567',
|
||||||
|
width: '15rem',
|
||||||
|
height: '3.5rem',
|
||||||
|
display: 'flex',
|
||||||
|
}}>
|
||||||
|
<FormControl fullWidth>
|
||||||
|
<InputLabel id='hsLabel'>Hardsub Language</InputLabel>
|
||||||
|
<Select
|
||||||
|
MenuProps={{
|
||||||
|
PaperProps: {
|
||||||
|
style: {
|
||||||
|
maxHeight: ITEM_HEIGHT * 4.5 + ITEM_PADDING_TOP,
|
||||||
|
width: 250
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
labelId='hsLabel'
|
||||||
|
label='Hardsub Language'
|
||||||
|
disabled={store.service != 'crunchy'}
|
||||||
|
value={store.downloadOptions.hslang}
|
||||||
|
onChange={(e) => {
|
||||||
|
dispatch({
|
||||||
|
type: 'downloadOptions',
|
||||||
|
payload: { ...store.downloadOptions, hslang: (e.target.value as string) === '' ? undefined : e.target.value as string }
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<MenuItem value=''>No Hardsub</MenuItem>
|
||||||
|
{availableSubs.map((lang) => {
|
||||||
|
if(lang === 'all' || lang === 'none')
|
||||||
|
return undefined;
|
||||||
|
return <MenuItem value={lang}>{lang}</MenuItem>;
|
||||||
|
})}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
|
||||||
|
<Tooltip title={
|
||||||
|
<Typography>
|
||||||
|
Downloads the hardsub version of the selected subtitle.<br/>Subtitles are displayed <b>PERMANENTLY!</b><br/>You can choose only <b>1</b> subtitle per video!
|
||||||
|
</Typography>
|
||||||
|
} arrow placement='top'>
|
||||||
|
<InfoOutlinedIcon sx={{
|
||||||
|
transition: '100ms',
|
||||||
|
ml: '0.35rem',
|
||||||
|
mr: '0.65rem',
|
||||||
|
'&:hover' : {
|
||||||
|
color: '#ffffff30',
|
||||||
|
}
|
||||||
|
}} />
|
||||||
|
</Tooltip>
|
||||||
|
</Box>
|
||||||
|
</Tooltip>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
<Box sx={{width: '95%', height: '0.3rem', backgroundColor: '#ffffff50', borderRadius: '10px', marginBottom: '20px'}}/>
|
||||||
|
<Box sx={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
width: '100%',
|
||||||
|
gap: '15px'
|
||||||
|
}}>
|
||||||
|
<TextField value={store.downloadOptions.fileName} onChange={e => {
|
||||||
|
dispatch({
|
||||||
|
type: 'downloadOptions',
|
||||||
|
payload: { ...store.downloadOptions, fileName: e.target.value }
|
||||||
|
});
|
||||||
|
}} sx={{ width: '87%' }} label='Filename Overwrite' />
|
||||||
|
<Tooltip title={
|
||||||
|
<Typography>
|
||||||
|
Click here to see the documentation
|
||||||
|
</Typography>
|
||||||
|
} arrow placement='top'>
|
||||||
|
<Link href='https://github.com/anidl/multi-downloader-nx/blob/master/docs/DOCUMENTATION.md#filename-template' rel="noopener noreferrer" target="_blank">
|
||||||
|
<InfoOutlinedIcon sx={{
|
||||||
|
transition: '100ms',
|
||||||
|
'&:hover' : {
|
||||||
|
color: '#ffffff30',
|
||||||
|
}
|
||||||
|
}} />
|
||||||
|
</Link>
|
||||||
|
</Tooltip>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
<Box sx={{width: '95%', height: '0.3rem', backgroundColor: '#ffffff50', borderRadius: '10px', marginTop: '10px'}}/>
|
||||||
|
|
||||||
|
<LoadingButton sx={{ margin: '15px', textTransform: 'none' }} loading={loading} onClick={addToQueue} variant='contained'>Add to Queue</LoadingButton>
|
||||||
|
|
||||||
|
</Box>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DownloadSelector;
|
||||||
|
|
@ -0,0 +1,191 @@
|
||||||
|
import { Box, List, ListItem, Typography, Divider, Dialog, Select, MenuItem, FormControl, InputLabel, Checkbox } from '@mui/material';
|
||||||
|
import { CheckBox, CheckBoxOutlineBlank } from '@mui/icons-material';
|
||||||
|
import React from 'react';
|
||||||
|
import useStore from '../../../../hooks/useStore';
|
||||||
|
import ContextMenu from '../../../reusable/ContextMenu';
|
||||||
|
import { useSnackbar } from 'notistack';
|
||||||
|
|
||||||
|
|
||||||
|
const EpisodeListing: React.FC = () => {
|
||||||
|
const [store, dispatch] = useStore();
|
||||||
|
|
||||||
|
const [season, setSeason] = React.useState<'all'|string>('all');
|
||||||
|
const { enqueueSnackbar } = useSnackbar();
|
||||||
|
|
||||||
|
const seasons = React.useMemo(() => {
|
||||||
|
const s: string[] = [];
|
||||||
|
for (const {season} of store.episodeListing) {
|
||||||
|
if (s.includes(season))
|
||||||
|
continue;
|
||||||
|
s.push(season);
|
||||||
|
}
|
||||||
|
return s;
|
||||||
|
}, [ store.episodeListing ]);
|
||||||
|
|
||||||
|
const [selected, setSelected] = React.useState<string[]>([]);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
setSelected(parseSelect(store.downloadOptions.e));
|
||||||
|
}, [ store.episodeListing ]);
|
||||||
|
|
||||||
|
const close = () => {
|
||||||
|
dispatch({
|
||||||
|
type: 'episodeListing',
|
||||||
|
payload: []
|
||||||
|
});
|
||||||
|
dispatch({
|
||||||
|
type: 'downloadOptions',
|
||||||
|
payload: {
|
||||||
|
...store.downloadOptions,
|
||||||
|
e: `${([...new Set([...parseSelect(store.downloadOptions.e), ...selected])]).join(',')}`
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const getEpisodesForSeason = (season: string|'all') => {
|
||||||
|
return store.episodeListing.filter((a) => season === 'all' ? true : a.season === season);
|
||||||
|
};
|
||||||
|
|
||||||
|
return <Dialog open={store.episodeListing.length > 0} onClose={close} scroll='paper' maxWidth='xl' sx={{ p: 2 }}>
|
||||||
|
<Box sx={{ display: 'grid', gridTemplateColumns: '1fr 200px 20px' }}>
|
||||||
|
<Typography color='text.primary' variant="h5" sx={{ textAlign: 'center', alignItems: 'center', justifyContent: 'center', display: 'flex' }}>
|
||||||
|
Episodes
|
||||||
|
</Typography>
|
||||||
|
<FormControl sx={{ mr: 2, mt: 2 }}>
|
||||||
|
<InputLabel id='seasonSelectLabel'>Season</InputLabel>
|
||||||
|
<Select labelId="seasonSelectLabel" label='Season' value={season} onChange={(e) => setSeason(e.target.value)}>
|
||||||
|
<MenuItem value='all'>Show all Epsiodes</MenuItem>
|
||||||
|
{seasons.map((a, index) => {
|
||||||
|
return <MenuItem value={a} key={`MenuItem_SeasonSelect_${index}`}>
|
||||||
|
{a}
|
||||||
|
</MenuItem>;
|
||||||
|
})}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
</Box>
|
||||||
|
<List>
|
||||||
|
<ListItem sx={{ display: 'grid', gridTemplateColumns: '25px 1fr 5fr' }}>
|
||||||
|
<Checkbox
|
||||||
|
indeterminate={store.episodeListing.some(a => selected.includes(a.e)) && !store.episodeListing.every(a => selected.includes(a.e))}
|
||||||
|
checked={store.episodeListing.every(a => selected.includes(a.e))}
|
||||||
|
onChange={() => {
|
||||||
|
if (selected.length > 0) {
|
||||||
|
setSelected([]);
|
||||||
|
} else {
|
||||||
|
setSelected(getEpisodesForSeason(season).map(a => a.e));
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</ListItem>
|
||||||
|
{getEpisodesForSeason(season).map((item, index, { length }) => {
|
||||||
|
const e = isNaN(parseInt(item.e)) ? item.e : parseInt(item.e);
|
||||||
|
const idStr = `S${item.season}E${e}`;
|
||||||
|
const isSelected = selected.includes(e.toString());
|
||||||
|
const imageRef = React.createRef<HTMLImageElement>();
|
||||||
|
const summaryRef = React.createRef<HTMLParagraphElement>();
|
||||||
|
return <Box {...{ mouseData: isSelected }} key={`Episode_List_Item_${index}`}>
|
||||||
|
<ListItem sx={{backdropFilter: isSelected ? 'brightness(1.5)' : '', '&:hover': {backdropFilter: 'brightness(1.5)'}, display: 'grid', gridTemplateColumns: '25px 50px 1fr 5fr' }}
|
||||||
|
onClick={() => {
|
||||||
|
let arr: string[] = [];
|
||||||
|
if (isSelected) {
|
||||||
|
arr = [...selected.filter(a => a !== e.toString())];
|
||||||
|
} else {
|
||||||
|
arr = [...selected, e.toString()];
|
||||||
|
}
|
||||||
|
setSelected(arr.filter(a => a.length > 0));
|
||||||
|
}}>
|
||||||
|
{ isSelected ? <CheckBox /> : <CheckBoxOutlineBlank /> }
|
||||||
|
<Typography color='text.primary' sx={{ textAlign: 'center' }}>
|
||||||
|
{idStr}
|
||||||
|
</Typography>
|
||||||
|
<img ref={imageRef} style={{ width: 'inherit', maxHeight: '200px', minWidth: '150px' }} src={item.img} alt="thumbnail" />
|
||||||
|
<Box sx={{ display: 'flex', flexDirection: 'column', pl: 1 }}>
|
||||||
|
<Box sx={{ display: 'grid', gridTemplateColumns: '1fr min-content' }}>
|
||||||
|
<Typography color='text.primary' variant="h5">
|
||||||
|
{item.name}
|
||||||
|
</Typography>
|
||||||
|
<Typography color='text.primary'>
|
||||||
|
{item.time.startsWith('00:') ? item.time.slice(3) : item.time}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
<Typography color='text.primary' ref={summaryRef}>
|
||||||
|
{item.description}
|
||||||
|
</Typography>
|
||||||
|
<Box sx={{ display: 'grid', gridTemplateColumns: 'fit-content 1fr' }}>
|
||||||
|
<Typography>
|
||||||
|
<br />
|
||||||
|
Available audio languages: {item.lang.join(', ')}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</ListItem>
|
||||||
|
<ContextMenu options={[ { text: 'Copy image URL', onClick: async () => {
|
||||||
|
await navigator.clipboard.writeText(item.img);
|
||||||
|
enqueueSnackbar('Copied URL to clipboard', {
|
||||||
|
variant: 'info'
|
||||||
|
});
|
||||||
|
}},
|
||||||
|
{
|
||||||
|
text: 'Open image in new tab',
|
||||||
|
onClick: () => {
|
||||||
|
window.open(item.img);
|
||||||
|
}
|
||||||
|
} ]} popupItem={imageRef} />
|
||||||
|
<ContextMenu options={[
|
||||||
|
{
|
||||||
|
onClick: async () => {
|
||||||
|
await navigator.clipboard.writeText(item.description!);
|
||||||
|
enqueueSnackbar('Copied summary to clipboard', {
|
||||||
|
variant: 'info'
|
||||||
|
});
|
||||||
|
},
|
||||||
|
text: 'Copy summary to clipboard'
|
||||||
|
}
|
||||||
|
]} popupItem={summaryRef} />
|
||||||
|
{index < length - 1 && <Divider />}
|
||||||
|
</Box>;
|
||||||
|
})}
|
||||||
|
</List>
|
||||||
|
</Dialog>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const parseSelect = (s: string): string[] => {
|
||||||
|
const ret: string[] = [];
|
||||||
|
s.split(',').forEach(item => {
|
||||||
|
if (item.includes('-')) {
|
||||||
|
const split = item.split('-');
|
||||||
|
if (split.length !== 2)
|
||||||
|
return;
|
||||||
|
const match = split[0].match(/[A-Za-z]+/);
|
||||||
|
if (match && match.length > 0) {
|
||||||
|
if (match.index && match.index !== 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const letters = split[0].substring(0, match[0].length);
|
||||||
|
const number = parseInt(split[0].substring(match[0].length));
|
||||||
|
const b = parseInt(split[1]);
|
||||||
|
if (isNaN(number) || isNaN(b)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (let i = number; i <= b; i++) {
|
||||||
|
ret.push(`${letters}${i}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
const a = parseInt(split[0]);
|
||||||
|
const b = parseInt(split[1]);
|
||||||
|
if (isNaN(a) || isNaN(b)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (let i = a; i <= b; i++) {
|
||||||
|
ret.push(`${i}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ret.push(item);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return [...new Set(ret)];
|
||||||
|
};
|
||||||
|
|
||||||
|
export default EpisodeListing;
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
.listitem-hover:hover {
|
||||||
|
-webkit-filter: brightness(70%);
|
||||||
|
filter: brightness(70%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.listitem-hover {
|
||||||
|
transition: filter 0.1s ease-in;
|
||||||
|
}
|
||||||
119
gui/react/src/components/AddToQueue/SearchBox/SearchBox.tsx
Normal file
119
gui/react/src/components/AddToQueue/SearchBox/SearchBox.tsx
Normal file
|
|
@ -0,0 +1,119 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { Box, ClickAwayListener, Divider, List, ListItem, Paper, TextField, Typography } from '@mui/material';
|
||||||
|
import { SearchResponse } from '../../../../../../@types/messageHandler';
|
||||||
|
import useStore from '../../../hooks/useStore';
|
||||||
|
import { messageChannelContext } from '../../../provider/MessageChannel';
|
||||||
|
import './SearchBox.css';
|
||||||
|
import ContextMenu from '../../reusable/ContextMenu';
|
||||||
|
import { useSnackbar } from 'notistack';
|
||||||
|
|
||||||
|
const SearchBox: React.FC = () => {
|
||||||
|
const messageHandler = React.useContext(messageChannelContext);
|
||||||
|
const [store, dispatch] = useStore();
|
||||||
|
const [search, setSearch] = React.useState('');
|
||||||
|
|
||||||
|
const [focus, setFocus] = React.useState(false);
|
||||||
|
|
||||||
|
const [searchResult, setSearchResult] = React.useState<undefined|SearchResponse>();
|
||||||
|
const anchor = React.useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
const { enqueueSnackbar } = useSnackbar();
|
||||||
|
|
||||||
|
const selectItem = (id: string) => {
|
||||||
|
dispatch({
|
||||||
|
type: 'downloadOptions',
|
||||||
|
payload: {
|
||||||
|
...store.downloadOptions,
|
||||||
|
id
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (search.trim().length === 0)
|
||||||
|
return setSearchResult({ isOk: true, value: [] });
|
||||||
|
|
||||||
|
const timeOutId = setTimeout(async () => {
|
||||||
|
if (search.trim().length > 3) {
|
||||||
|
const s = await messageHandler?.search({search});
|
||||||
|
if (s && s.isOk)
|
||||||
|
s.value = s.value.slice(0, 10);
|
||||||
|
setSearchResult(s);
|
||||||
|
}
|
||||||
|
}, 500);
|
||||||
|
return () => clearTimeout(timeOutId);
|
||||||
|
}, [search]);
|
||||||
|
|
||||||
|
const anchorBounding = anchor.current?.getBoundingClientRect();
|
||||||
|
return <ClickAwayListener onClickAway={() => setFocus(false)}>
|
||||||
|
<Box sx={{ m: 2 }}>
|
||||||
|
<TextField ref={anchor} value={search} onClick={() => setFocus(true)} onChange={e => setSearch(e.target.value)} variant='outlined' label='Search' fullWidth />
|
||||||
|
{searchResult !== undefined && searchResult.isOk && searchResult.value.length > 0 && focus &&
|
||||||
|
<Paper sx={{ position: 'fixed', maxHeight: '50%', width: `${anchorBounding?.width}px`,
|
||||||
|
left: anchorBounding?.x, top: (anchorBounding?.y ?? 0) + (anchorBounding?.height ?? 0), zIndex: 99, overflowY: 'scroll'}}>
|
||||||
|
<List>
|
||||||
|
{searchResult && searchResult.isOk ?
|
||||||
|
searchResult.value.map((a, ind, arr) => {
|
||||||
|
const imageRef = React.createRef<HTMLImageElement>();
|
||||||
|
const summaryRef = React.createRef<HTMLParagraphElement>();
|
||||||
|
return <Box key={a.id}>
|
||||||
|
<ListItem className='listitem-hover' onClick={() => {
|
||||||
|
selectItem(a.id);
|
||||||
|
setFocus(false);
|
||||||
|
}}>
|
||||||
|
<Box sx={{ display: 'flex' }}>
|
||||||
|
<Box sx={{ width: '20%', height: '100%', pr: 2 }}>
|
||||||
|
<img ref={imageRef} src={a.image} style={{ width: '100%', height: 'auto' }} alt="thumbnail"/>
|
||||||
|
</Box>
|
||||||
|
<Box sx={{ display: 'flex', flexDirection: 'column', maxWidth: '70%' }}>
|
||||||
|
<Typography variant='h6' component='h6' color='text.primary' sx={{ }}>
|
||||||
|
{a.name}
|
||||||
|
</Typography>
|
||||||
|
{a.desc && <Typography variant='caption' component='p' color='text.primary' sx={{ pt: 1, pb: 1 }} ref={summaryRef}>
|
||||||
|
{a.desc}
|
||||||
|
</Typography>}
|
||||||
|
{a.lang && <Typography variant='caption' component='p' color='text.primary' sx={{ }}>
|
||||||
|
Languages: {a.lang.join(', ')}
|
||||||
|
</Typography>}
|
||||||
|
<Typography variant='caption' component='p' color='text.primary' sx={{ }}>
|
||||||
|
ID: {a.id}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</ListItem>
|
||||||
|
<ContextMenu options={[ { text: 'Copy image URL', onClick: async () => {
|
||||||
|
await navigator.clipboard.writeText(a.image);
|
||||||
|
enqueueSnackbar('Copied URL to clipboard', {
|
||||||
|
variant: 'info'
|
||||||
|
});
|
||||||
|
}},
|
||||||
|
{
|
||||||
|
text: 'Open image in new tab',
|
||||||
|
onClick: () => {
|
||||||
|
window.open(a.image);
|
||||||
|
}
|
||||||
|
} ]} popupItem={imageRef} />
|
||||||
|
{a.desc &&
|
||||||
|
<ContextMenu options={[
|
||||||
|
{
|
||||||
|
onClick: async () => {
|
||||||
|
await navigator.clipboard.writeText(a.desc!);
|
||||||
|
enqueueSnackbar('Copied summary to clipboard', {
|
||||||
|
variant: 'info'
|
||||||
|
});
|
||||||
|
},
|
||||||
|
text: 'Copy summary to clipboard'
|
||||||
|
}
|
||||||
|
]} popupItem={summaryRef} />
|
||||||
|
}
|
||||||
|
{(ind < arr.length - 1) && <Divider />}
|
||||||
|
</Box>;
|
||||||
|
})
|
||||||
|
: <></>}
|
||||||
|
</List>
|
||||||
|
</Paper>}
|
||||||
|
</Box>
|
||||||
|
</ClickAwayListener>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SearchBox;
|
||||||
112
gui/react/src/components/AuthButton.tsx
Normal file
112
gui/react/src/components/AuthButton.tsx
Normal file
|
|
@ -0,0 +1,112 @@
|
||||||
|
import { Button, CircularProgress, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle, TextField } from '@mui/material';
|
||||||
|
import { Check, Close } from '@mui/icons-material';
|
||||||
|
import React from 'react';
|
||||||
|
import { messageChannelContext } from '../provider/MessageChannel';
|
||||||
|
import Require from './Require';
|
||||||
|
import { useSnackbar } from 'notistack';
|
||||||
|
|
||||||
|
const AuthButton: React.FC = () => {
|
||||||
|
const snackbar = useSnackbar();
|
||||||
|
|
||||||
|
const [open, setOpen] = React.useState(false);
|
||||||
|
|
||||||
|
const [username, setUsername] = React.useState('');
|
||||||
|
const [password, setPassword] = React.useState('');
|
||||||
|
|
||||||
|
const [usernameError, setUsernameError] = React.useState(false);
|
||||||
|
const [passwordError, setPasswordError] = React.useState(false);
|
||||||
|
|
||||||
|
const messageChannel = React.useContext(messageChannelContext);
|
||||||
|
|
||||||
|
const [loading, setLoading] = React.useState(false);
|
||||||
|
const [error, setError] = React.useState<Error|undefined>(undefined);
|
||||||
|
const [authed, setAuthed] = React.useState(false);
|
||||||
|
|
||||||
|
const checkAuth = async () => {
|
||||||
|
setAuthed((await messageChannel?.checkToken())?.isOk ?? false);
|
||||||
|
};
|
||||||
|
|
||||||
|
React.useEffect(() => { checkAuth(); }, []);
|
||||||
|
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
if (!messageChannel)
|
||||||
|
throw new Error('Invalid state'); //The components to confirm only render if the messageChannel is not undefinded
|
||||||
|
if (username.trim().length === 0)
|
||||||
|
return setUsernameError(true);
|
||||||
|
if (password.trim().length === 0)
|
||||||
|
return setPasswordError(true);
|
||||||
|
setUsernameError(false);
|
||||||
|
setPasswordError(false);
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
const res = await messageChannel.auth({ username, password });
|
||||||
|
if (res.isOk) {
|
||||||
|
setOpen(false);
|
||||||
|
snackbar.enqueueSnackbar('Logged in', {
|
||||||
|
variant: 'success'
|
||||||
|
});
|
||||||
|
setUsername('');
|
||||||
|
setPassword('');
|
||||||
|
} else {
|
||||||
|
setError(res.reason);
|
||||||
|
}
|
||||||
|
await checkAuth();
|
||||||
|
setLoading(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
return <Require value={messageChannel}>
|
||||||
|
<Dialog open={open}>
|
||||||
|
<Dialog open={!!error}>
|
||||||
|
<DialogTitle>Error during Authentication</DialogTitle>
|
||||||
|
<DialogContentText>
|
||||||
|
{error?.name}
|
||||||
|
{error?.message}
|
||||||
|
</DialogContentText>
|
||||||
|
<DialogActions>
|
||||||
|
<Button onClick={() => setError(undefined)}>Close</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</Dialog>
|
||||||
|
<DialogTitle sx={{ flexGrow: 1 }}>Authentication</DialogTitle>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogContentText>
|
||||||
|
Here, you need to enter your username (most likely your Email) and your password.<br />
|
||||||
|
These information are not stored anywhere and are only used to authenticate with the service once.
|
||||||
|
</DialogContentText>
|
||||||
|
<TextField
|
||||||
|
error={usernameError}
|
||||||
|
helperText={usernameError ? 'Please enter something before submiting' : undefined}
|
||||||
|
margin="dense"
|
||||||
|
id="username"
|
||||||
|
label="Username"
|
||||||
|
type="text"
|
||||||
|
fullWidth
|
||||||
|
variant="standard"
|
||||||
|
value={username}
|
||||||
|
onChange={(e) => setUsername(e.target.value)}
|
||||||
|
disabled={loading}
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
error={passwordError}
|
||||||
|
helperText={passwordError ? 'Please enter something before submiting' : undefined}
|
||||||
|
margin="dense"
|
||||||
|
id="password"
|
||||||
|
label="Password"
|
||||||
|
type="password"
|
||||||
|
fullWidth
|
||||||
|
variant="standard"
|
||||||
|
value={password}
|
||||||
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
|
disabled={loading}
|
||||||
|
/>
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
{loading && <CircularProgress size={30}/>}
|
||||||
|
<Button disabled={loading} onClick={() => setOpen(false)}>Close</Button>
|
||||||
|
<Button disabled={loading} onClick={() => handleSubmit()}>Authenticate</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</Dialog>
|
||||||
|
<Button startIcon={authed ? <Check />: <Close />} variant="contained" onClick={() => setOpen(true)}>Authenticate</Button>
|
||||||
|
</Require>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AuthButton;
|
||||||
37
gui/react/src/components/LogoutButton.tsx
Normal file
37
gui/react/src/components/LogoutButton.tsx
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
import { ExitToApp } from '@mui/icons-material';
|
||||||
|
import { Button } from '@mui/material';
|
||||||
|
import React from 'react';
|
||||||
|
import useStore from '../hooks/useStore';
|
||||||
|
import { messageChannelContext } from '../provider/MessageChannel';
|
||||||
|
import Require from './Require';
|
||||||
|
|
||||||
|
const LogoutButton: React.FC = () => {
|
||||||
|
const messageChannel = React.useContext(messageChannelContext);
|
||||||
|
const [, dispatch] = useStore();
|
||||||
|
|
||||||
|
const logout = async () => {
|
||||||
|
if (await messageChannel?.isDownloading())
|
||||||
|
return alert('You are currently downloading. Please finish the download first.');
|
||||||
|
if (await messageChannel?.logout())
|
||||||
|
dispatch({
|
||||||
|
type: 'service',
|
||||||
|
payload: undefined
|
||||||
|
});
|
||||||
|
else
|
||||||
|
alert('Unable to change service');
|
||||||
|
};
|
||||||
|
|
||||||
|
return <Require value={messageChannel}>
|
||||||
|
<Button
|
||||||
|
startIcon={<ExitToApp />}
|
||||||
|
variant='contained'
|
||||||
|
onClick={logout}
|
||||||
|
sx={{ maxHeight: '2.3rem' }}
|
||||||
|
>
|
||||||
|
Service select
|
||||||
|
</Button>
|
||||||
|
</Require>;
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
export default LogoutButton;
|
||||||
|
|
@ -0,0 +1,40 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { ExtendedProgress, QueueItem } from '../../../../../../@types/messageHandler';
|
||||||
|
import { RandomEvent } from '../../../../../../@types/randomEvents';
|
||||||
|
import { messageChannelContext } from '../../../provider/MessageChannel';
|
||||||
|
|
||||||
|
const useDownloadManager = () => {
|
||||||
|
const messageHandler = React.useContext(messageChannelContext);
|
||||||
|
|
||||||
|
const [progressData, setProgressData] = React.useState<ExtendedProgress|undefined>();
|
||||||
|
const [current, setCurrent] = React.useState<undefined|QueueItem>();
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
const handler = (ev: RandomEvent<'progress'>) => {
|
||||||
|
console.log(ev.data);
|
||||||
|
setProgressData(ev.data);
|
||||||
|
};
|
||||||
|
|
||||||
|
const currentHandler = (ev: RandomEvent<'current'>) => {
|
||||||
|
setCurrent(ev.data);
|
||||||
|
};
|
||||||
|
|
||||||
|
const finishHandler = () => {
|
||||||
|
setProgressData(undefined);
|
||||||
|
};
|
||||||
|
|
||||||
|
messageHandler?.randomEvents.on('progress', handler);
|
||||||
|
messageHandler?.randomEvents.on('current', currentHandler);
|
||||||
|
messageHandler?.randomEvents.on('finish', finishHandler);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
messageHandler?.randomEvents.removeListener('progress', handler);
|
||||||
|
messageHandler?.randomEvents.removeListener('finish', finishHandler);
|
||||||
|
messageHandler?.randomEvents.removeListener('current', currentHandler);
|
||||||
|
};
|
||||||
|
}, [messageHandler]);
|
||||||
|
|
||||||
|
return { data: progressData, current};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useDownloadManager;
|
||||||
11
gui/react/src/components/MainFrame/MainFrame.tsx
Normal file
11
gui/react/src/components/MainFrame/MainFrame.tsx
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
import { Box } from '@mui/material';
|
||||||
|
import React from 'react';
|
||||||
|
import Queue from './Queue/Queue';
|
||||||
|
|
||||||
|
const MainFrame: React.FC = () => {
|
||||||
|
return <Box sx={{ }}>
|
||||||
|
<Queue />
|
||||||
|
</Box>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MainFrame;
|
||||||
420
gui/react/src/components/MainFrame/Queue/Queue.tsx
Normal file
420
gui/react/src/components/MainFrame/Queue/Queue.tsx
Normal file
|
|
@ -0,0 +1,420 @@
|
||||||
|
import { Badge, Box, Button, CircularProgress, Divider, IconButton, LinearProgress, Skeleton, Tooltip, Typography } from '@mui/material';
|
||||||
|
import React from 'react';
|
||||||
|
import { messageChannelContext } from '../../../provider/MessageChannel';
|
||||||
|
import { queueContext } from '../../../provider/QueueProvider';
|
||||||
|
import DeleteIcon from '@mui/icons-material/Delete';
|
||||||
|
|
||||||
|
import useDownloadManager from '../DownloadManager/DownloadManager';
|
||||||
|
|
||||||
|
const Queue: React.FC = () => {
|
||||||
|
const { data, current } = useDownloadManager();
|
||||||
|
const queue = React.useContext(queueContext);
|
||||||
|
const msg = React.useContext(messageChannelContext);
|
||||||
|
|
||||||
|
|
||||||
|
if (!msg)
|
||||||
|
return <>Never</>;
|
||||||
|
|
||||||
|
return data || queue.length > 0 ? <>
|
||||||
|
{data && <>
|
||||||
|
<Box sx={{
|
||||||
|
display: 'flex',
|
||||||
|
width: '100%',
|
||||||
|
flexDirection: 'column',
|
||||||
|
alignItems: 'center',
|
||||||
|
}}>
|
||||||
|
<Box sx={{
|
||||||
|
marginTop: '2rem',
|
||||||
|
marginBottom: '1rem',
|
||||||
|
height: '12rem',
|
||||||
|
width: '93vw',
|
||||||
|
maxWidth: '93rem',
|
||||||
|
backgroundColor: '#282828',
|
||||||
|
boxShadow: '0px 0px 50px #00000090',
|
||||||
|
borderRadius: '10px',
|
||||||
|
display: 'flex',
|
||||||
|
transition: '250ms'
|
||||||
|
}}>
|
||||||
|
<img style={{
|
||||||
|
borderRadius: '5px',
|
||||||
|
margin: '5px',
|
||||||
|
boxShadow: '0px 0px 10px #00000090',
|
||||||
|
userSelect: 'none',
|
||||||
|
}}
|
||||||
|
src={data.downloadInfo.image} height='auto' width='auto' alt="Thumbnail" />
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
width: '100%',
|
||||||
|
justifyContent: 'center'
|
||||||
|
}}>
|
||||||
|
<Box sx={{
|
||||||
|
display: 'flex',
|
||||||
|
}}>
|
||||||
|
<Box sx={{
|
||||||
|
//backgroundColor: '#ff0000',
|
||||||
|
width: '70%',
|
||||||
|
marginLeft: '10px'
|
||||||
|
}}>
|
||||||
|
<Box sx={{
|
||||||
|
flexDirection: 'column',
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
}}>
|
||||||
|
<Typography color='text.primary' sx={{
|
||||||
|
fontSize: '1.8rem',
|
||||||
|
}}>
|
||||||
|
{data.downloadInfo.parent.title}
|
||||||
|
</Typography>
|
||||||
|
<Typography color='text.primary' sx={{
|
||||||
|
fontSize: '1.2rem',
|
||||||
|
}}>
|
||||||
|
{data.downloadInfo.title}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
<Box sx={{
|
||||||
|
//backgroundColor: '#00ff00',
|
||||||
|
width: '30%',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
}}>
|
||||||
|
<Typography color='text.primary' sx={{
|
||||||
|
fontSize: '1.8rem',
|
||||||
|
}}>
|
||||||
|
Downloading: {data.downloadInfo.language.name}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
<Box sx={{
|
||||||
|
height: '50%',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
//backgroundColor: '#0000ff',
|
||||||
|
}}>
|
||||||
|
<LinearProgress variant='determinate'
|
||||||
|
sx={{
|
||||||
|
height: '20px',
|
||||||
|
width: '97.53%',
|
||||||
|
margin: '10px',
|
||||||
|
boxShadow: '0px 0px 10px #00000090',
|
||||||
|
borderRadius: '10px',
|
||||||
|
}} value={(typeof data.progress.percent === 'string' ? parseInt(data.progress.percent) : data.progress.percent)}
|
||||||
|
/>
|
||||||
|
<Box>
|
||||||
|
<Typography color='text.primary'
|
||||||
|
sx={{
|
||||||
|
fontSize: '1.3rem',
|
||||||
|
}}>
|
||||||
|
{data.progress.cur} / {(data.progress.total)} parts ({data.progress.percent}% | {formatTime(data.progress.time)} | {(data.progress.downloadSpeed / 1024 / 1024).toFixed(2)} MB/s | {(data.progress.bytes / 1024 / 1024).toFixed(2)}MB)
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
{
|
||||||
|
current && !data && <>
|
||||||
|
<Box sx={{
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
alignItems: 'center',
|
||||||
|
}}>
|
||||||
|
<Box sx={{
|
||||||
|
marginTop: '2rem',
|
||||||
|
marginBottom: '1rem',
|
||||||
|
height: '12rem',
|
||||||
|
width: '93vw',
|
||||||
|
maxWidth: '93rem',
|
||||||
|
backgroundColor: '#282828',
|
||||||
|
boxShadow: '0px 0px 50px #00000090',
|
||||||
|
borderRadius: '10px',
|
||||||
|
display: 'flex',
|
||||||
|
overflow: 'hidden',
|
||||||
|
transition: '250ms'
|
||||||
|
}}>
|
||||||
|
<img style={{
|
||||||
|
borderRadius: '5px',
|
||||||
|
margin: '5px',
|
||||||
|
boxShadow: '0px 0px 10px #00000090',
|
||||||
|
userSelect: 'none',
|
||||||
|
maxWidth: '20.5rem',
|
||||||
|
}}
|
||||||
|
src={current.image} height='auto' width='auto' alt="Thumbnail" />
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
width: '100%',
|
||||||
|
justifyContent: 'center',
|
||||||
|
//backgroundColor: '#ffffff0f'
|
||||||
|
}}>
|
||||||
|
<Box sx={{
|
||||||
|
display: 'flex',
|
||||||
|
|
||||||
|
}}>
|
||||||
|
<Box sx={{
|
||||||
|
width: '70%',
|
||||||
|
marginLeft: '10px'
|
||||||
|
}}>
|
||||||
|
<Box sx={{
|
||||||
|
flexDirection: 'column',
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
}}>
|
||||||
|
<Typography color='text.primary' sx={{
|
||||||
|
fontSize: '1.8rem',
|
||||||
|
}}>
|
||||||
|
{current.parent.title}
|
||||||
|
</Typography>
|
||||||
|
<Typography color='text.primary' sx={{
|
||||||
|
fontSize: '1.2rem',
|
||||||
|
}}>
|
||||||
|
{current.title}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
<Box sx={{
|
||||||
|
//backgroundColor: '#00ff00',
|
||||||
|
width: '30%',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
}}>
|
||||||
|
<Box sx={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
position: 'relative',
|
||||||
|
}}>
|
||||||
|
<Typography color='text.primary' sx={{
|
||||||
|
fontSize: '1.8rem',
|
||||||
|
}}>
|
||||||
|
Downloading:
|
||||||
|
</Typography>
|
||||||
|
<CircularProgress variant="indeterminate" sx={{
|
||||||
|
marginLeft: '2rem',
|
||||||
|
}}/>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
<Box sx={{
|
||||||
|
height: '50%',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
//backgroundColor: '#0000ff',
|
||||||
|
}}>
|
||||||
|
<LinearProgress variant='indeterminate'
|
||||||
|
sx={{
|
||||||
|
height: '20px',
|
||||||
|
width: '97.53%',
|
||||||
|
margin: '10px',
|
||||||
|
boxShadow: '0px 0px 10px #00000090',
|
||||||
|
borderRadius: '10px',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Box>
|
||||||
|
<Typography color='text.primary'
|
||||||
|
sx={{
|
||||||
|
fontSize: '1.3rem',
|
||||||
|
}}>
|
||||||
|
0 / ? parts (0% | XX:XX | 0 MB/s | 0MB)
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
{queue.map((queueItem, index, { length }) => {
|
||||||
|
return <Box key={`queue_item_${index}`} sx={{
|
||||||
|
display: 'flex',
|
||||||
|
mb: '-1.5rem',
|
||||||
|
flexDirection: 'column',
|
||||||
|
alignItems: 'center',
|
||||||
|
}}>
|
||||||
|
<Box sx={{
|
||||||
|
marginTop: '1.5rem',
|
||||||
|
marginBottom: '1.5rem',
|
||||||
|
height: '11rem',
|
||||||
|
width: '90vw',
|
||||||
|
maxWidth: '90rem',
|
||||||
|
backgroundColor: '#282828',
|
||||||
|
boxShadow: '0px 0px 10px #00000090',
|
||||||
|
borderRadius: '10px',
|
||||||
|
display: 'flex',
|
||||||
|
overflow: 'hidden',
|
||||||
|
}}>
|
||||||
|
<img style={{
|
||||||
|
borderRadius: '5px',
|
||||||
|
margin: '5px',
|
||||||
|
boxShadow: '0px 0px 5px #00000090',
|
||||||
|
userSelect: 'none',
|
||||||
|
maxWidth: '18.5rem'
|
||||||
|
}}
|
||||||
|
src={queueItem.image} height='auto' width='auto' alt="Thumbnail" />
|
||||||
|
<Box sx={{
|
||||||
|
margin: '5px',
|
||||||
|
display: 'flex',
|
||||||
|
width: '100%',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
}}>
|
||||||
|
<Box sx={{
|
||||||
|
width: '30%',
|
||||||
|
marginRight: '5px',
|
||||||
|
marginLeft: '5px',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
overflow: 'hidden',
|
||||||
|
textOverflow: 'ellipsis',
|
||||||
|
}}>
|
||||||
|
<Typography color='text.primary' sx={{
|
||||||
|
fontSize: '1.8rem',
|
||||||
|
overflow: 'hidden',
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
textOverflow: 'ellipsis',
|
||||||
|
}}>
|
||||||
|
{queueItem.parent.title}
|
||||||
|
</Typography>
|
||||||
|
<Typography color='text.primary' sx={{
|
||||||
|
fontSize: '1.6rem',
|
||||||
|
marginTop: '-0.4rem',
|
||||||
|
marginBottom: '0.4rem',
|
||||||
|
}}>
|
||||||
|
S{queueItem.parent.season}E{queueItem.episode}
|
||||||
|
</Typography>
|
||||||
|
<Typography color='text.primary' sx={{
|
||||||
|
fontSize: '1.2rem',
|
||||||
|
marginTop: '-0.4rem',
|
||||||
|
marginBottom: '0.4rem',
|
||||||
|
textOverflow: 'ellipsis',
|
||||||
|
}}>
|
||||||
|
{queueItem.title}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
<Box sx={{
|
||||||
|
width: '40%',
|
||||||
|
marginRight: '5px',
|
||||||
|
marginLeft: '5px',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
overflow: 'hidden',
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
}}>
|
||||||
|
<Typography color='text.primary' sx={{
|
||||||
|
fontSize: '1.8rem',
|
||||||
|
overflow: 'hidden',
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
textOverflow: 'ellipsis',
|
||||||
|
}}>
|
||||||
|
Dub(s): {queueItem.dubLang.join(', ')}
|
||||||
|
</Typography>
|
||||||
|
<Typography color='text.primary' sx={{
|
||||||
|
fontSize: '1.8rem',
|
||||||
|
overflow: 'hidden',
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
textOverflow: 'ellipsis',
|
||||||
|
}}>
|
||||||
|
Sub(s): {queueItem.dlsubs.join(', ')}
|
||||||
|
</Typography>
|
||||||
|
<Typography color='text.primary' sx={{
|
||||||
|
fontSize: '1.8rem',
|
||||||
|
|
||||||
|
}}>
|
||||||
|
Quality: {queueItem.q}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
<Box sx={{
|
||||||
|
marginRight: '5px',
|
||||||
|
marginLeft: '5px',
|
||||||
|
width: '30%',
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
display: 'flex'
|
||||||
|
}}>
|
||||||
|
<Tooltip title="Delete from queue" arrow placement='top'>
|
||||||
|
<IconButton
|
||||||
|
onClick={() => {
|
||||||
|
msg.removeFromQueue(index);
|
||||||
|
}}
|
||||||
|
sx={{
|
||||||
|
backgroundColor: '#ff573a25',
|
||||||
|
height: '40px',
|
||||||
|
transition: '250ms',
|
||||||
|
'&:hover' : {
|
||||||
|
backgroundColor: '#ff573a',
|
||||||
|
}
|
||||||
|
}}>
|
||||||
|
<DeleteIcon />
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
;
|
||||||
|
})}
|
||||||
|
</> : <Box sx={{
|
||||||
|
display: 'flex',
|
||||||
|
width: '100%',
|
||||||
|
height: '12rem',
|
||||||
|
flexDirection: 'column',
|
||||||
|
alignItems: 'center',
|
||||||
|
}}>
|
||||||
|
<Typography color='text.primary' sx={{
|
||||||
|
fontSize: '2rem',
|
||||||
|
margin: '10px'
|
||||||
|
}}>
|
||||||
|
Selected episodes will be shown here
|
||||||
|
</Typography>
|
||||||
|
<Box sx={{
|
||||||
|
display: 'flex',
|
||||||
|
margin: '10px'
|
||||||
|
}}>
|
||||||
|
<Skeleton variant='rectangular' height={'10rem'} width={'20rem'} sx={{ margin: '5px', borderRadius: '5px' }}/>
|
||||||
|
<Box sx={{
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
}}>
|
||||||
|
<Skeleton variant='text' height={'100%'} width={'30rem'} sx={{ margin: '5px', borderRadius: '5px' }}/>
|
||||||
|
<Skeleton variant='text' height={'100%'} width={'30rem'} sx={{ margin: '5px', borderRadius: '5px' }}/>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
<Box sx={{
|
||||||
|
display: 'flex',
|
||||||
|
margin: '10px'
|
||||||
|
}}>
|
||||||
|
<Skeleton variant='rectangular' height={'10rem'} width={'20rem'} sx={{ margin: '5px', borderRadius: '5px' }}/>
|
||||||
|
<Box sx={{
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
}}>
|
||||||
|
<Skeleton variant='text' height={'100%'} width={'30rem'} sx={{ margin: '5px', borderRadius: '5px' }}/>
|
||||||
|
<Skeleton variant='text' height={'100%'} width={'30rem'} sx={{ margin: '5px', borderRadius: '5px' }}/>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</Box>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatTime = (time: number) => {
|
||||||
|
time = Math.floor(time / 1000);
|
||||||
|
const minutes = Math.floor(time / 60);
|
||||||
|
time = time % 60;
|
||||||
|
|
||||||
|
return `${minutes.toFixed(0).length < 2 ? `0${minutes}` : minutes}m${time.toFixed(0).length < 2 ? `0${time}` : time}s`;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Queue;
|
||||||
124
gui/react/src/components/MenuBar/MenuBar.tsx
Normal file
124
gui/react/src/components/MenuBar/MenuBar.tsx
Normal file
|
|
@ -0,0 +1,124 @@
|
||||||
|
import { Box, Button, Menu, MenuItem, Typography } from '@mui/material';
|
||||||
|
import React from 'react';
|
||||||
|
import { messageChannelContext } from '../../provider/MessageChannel';
|
||||||
|
import useStore from '../../hooks/useStore';
|
||||||
|
import { StoreState } from '../../provider/Store';
|
||||||
|
|
||||||
|
const MenuBar: React.FC = () => {
|
||||||
|
const [ openMenu, setMenuOpen ] = React.useState<'settings'|'help'|undefined>();
|
||||||
|
const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null);
|
||||||
|
const [store, dispatch] = useStore();
|
||||||
|
|
||||||
|
const messageChannel = React.useContext(messageChannelContext);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
(async () => {
|
||||||
|
if (!messageChannel || store.version !== '')
|
||||||
|
return;
|
||||||
|
dispatch({
|
||||||
|
type: 'version',
|
||||||
|
payload: await messageChannel.version()
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
}, [messageChannel]);
|
||||||
|
|
||||||
|
const transformService = (service: StoreState['service']) => {
|
||||||
|
switch(service) {
|
||||||
|
case 'crunchy':
|
||||||
|
return 'Crunchyroll';
|
||||||
|
case 'hidive':
|
||||||
|
return 'Hidive';
|
||||||
|
case 'ao':
|
||||||
|
return 'AnimeOnegai';
|
||||||
|
case 'adn':
|
||||||
|
return 'AnimationDigitalNetwork';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const msg = React.useContext(messageChannelContext);
|
||||||
|
|
||||||
|
const handleClick = (event: React.MouseEvent<HTMLElement>, n: 'settings'|'help') => {
|
||||||
|
setAnchorEl(event.currentTarget);
|
||||||
|
setMenuOpen(n);
|
||||||
|
};
|
||||||
|
const handleClose = () => {
|
||||||
|
setAnchorEl(null);
|
||||||
|
setMenuOpen(undefined);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!msg)
|
||||||
|
return <></>;
|
||||||
|
|
||||||
|
return <Box sx={{ display: 'flex', marginBottom: '1rem', width: '100%', alignItems: 'center' }}>
|
||||||
|
<Box sx={{ position: 'relative', left: '0%', width: '50%'}}>
|
||||||
|
<Button onClick={(e) => handleClick(e, 'settings')}>
|
||||||
|
Settings
|
||||||
|
</Button>
|
||||||
|
<Button onClick={(e) => handleClick(e, 'help')}>
|
||||||
|
Help
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
<Menu open={openMenu === 'settings'} anchorEl={anchorEl} onClose={handleClose}>
|
||||||
|
<MenuItem onClick={() => {
|
||||||
|
msg.openFolder('config');
|
||||||
|
handleClose();
|
||||||
|
}}>
|
||||||
|
Open settings folder
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem onClick={() => {
|
||||||
|
msg.openFile(['config', 'bin-path.yml']);
|
||||||
|
handleClose();
|
||||||
|
}}>
|
||||||
|
Open FFmpeg/Mkvmerge file
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem onClick={() => {
|
||||||
|
msg.openFile(['config', 'cli-defaults.yml']);
|
||||||
|
handleClose();
|
||||||
|
}}>
|
||||||
|
Open advanced options
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem onClick={() => {
|
||||||
|
msg.openFolder('content');
|
||||||
|
handleClose();
|
||||||
|
}}>
|
||||||
|
Open output path
|
||||||
|
</MenuItem>
|
||||||
|
</Menu>
|
||||||
|
<Menu open={openMenu === 'help'} anchorEl={anchorEl} onClose={handleClose}>
|
||||||
|
<MenuItem onClick={() => {
|
||||||
|
msg.openURL('https://github.com/anidl/multi-downloader-nx');
|
||||||
|
handleClose();
|
||||||
|
}}>
|
||||||
|
GitHub
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem onClick={() => {
|
||||||
|
msg.openURL('https://github.com/anidl/multi-downloader-nx/issues/new?assignees=AnimeDL,AnidlSupport&labels=bug&template=bug.yml&title=BUG');
|
||||||
|
handleClose();
|
||||||
|
}}>
|
||||||
|
Report a bug
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem onClick={() => {
|
||||||
|
msg.openURL('https://github.com/anidl/multi-downloader-nx/graphs/contributors');
|
||||||
|
handleClose();
|
||||||
|
}}>
|
||||||
|
Contributors
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem onClick={() => {
|
||||||
|
msg.openURL('https://discord.gg/qEpbWen5vq');
|
||||||
|
handleClose();
|
||||||
|
}}>
|
||||||
|
Discord
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem onClick={() => {
|
||||||
|
handleClose();
|
||||||
|
}}>
|
||||||
|
Version: {store.version}
|
||||||
|
</MenuItem>
|
||||||
|
</Menu>
|
||||||
|
<Typography variant="h5" color="text.primary">
|
||||||
|
{transformService(store.service)}
|
||||||
|
</Typography>
|
||||||
|
</Box>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MenuBar;
|
||||||
14
gui/react/src/components/Require.tsx
Normal file
14
gui/react/src/components/Require.tsx
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { Box, Backdrop, CircularProgress } from '@mui/material';
|
||||||
|
|
||||||
|
export type RequireType<T> = {
|
||||||
|
value?: T
|
||||||
|
}
|
||||||
|
|
||||||
|
const Require = <T, >(props: React.PropsWithChildren<RequireType<T>>) => {
|
||||||
|
return props.value === undefined ? <Backdrop open>
|
||||||
|
<CircularProgress />
|
||||||
|
</Backdrop> : <Box>{props.children}</Box>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Require;
|
||||||
42
gui/react/src/components/StartQueue.tsx
Normal file
42
gui/react/src/components/StartQueue.tsx
Normal file
|
|
@ -0,0 +1,42 @@
|
||||||
|
import { PauseCircleFilled, PlayCircleFilled } from '@mui/icons-material';
|
||||||
|
import { Button } from '@mui/material';
|
||||||
|
import React from 'react';
|
||||||
|
import { messageChannelContext } from '../provider/MessageChannel';
|
||||||
|
import Require from './Require';
|
||||||
|
|
||||||
|
const StartQueueButton: React.FC = () => {
|
||||||
|
const messageChannel = React.useContext(messageChannelContext);
|
||||||
|
const [start, setStart] = React.useState(false);
|
||||||
|
const msg = React.useContext(messageChannelContext);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
(async () => {
|
||||||
|
if (!msg)
|
||||||
|
return alert('Invalid state: msg not found');
|
||||||
|
setStart(await msg.getDownloadQueue());
|
||||||
|
})();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const change = async () => {
|
||||||
|
if (await messageChannel?.isDownloading())
|
||||||
|
alert('The current download will be finished before the queue stops');
|
||||||
|
msg?.setDownloadQueue(!start);
|
||||||
|
setStart(!start);
|
||||||
|
};
|
||||||
|
|
||||||
|
return <Require value={messageChannel}>
|
||||||
|
<Button
|
||||||
|
startIcon={start ? <PauseCircleFilled /> : <PlayCircleFilled /> }
|
||||||
|
variant='contained'
|
||||||
|
onClick={change}
|
||||||
|
sx={{ maxHeight: '2.3rem' }}
|
||||||
|
>
|
||||||
|
{
|
||||||
|
start ? 'Stop Queue' : 'Start Queue'
|
||||||
|
}
|
||||||
|
</Button>
|
||||||
|
</Require>;
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
export default StartQueueButton;
|
||||||
65
gui/react/src/components/reusable/ContextMenu.tsx
Normal file
65
gui/react/src/components/reusable/ContextMenu.tsx
Normal file
|
|
@ -0,0 +1,65 @@
|
||||||
|
import { Box, Button, Divider, List, SxProps } from '@mui/material';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
export type Option = {
|
||||||
|
text: string,
|
||||||
|
onClick: () => unknown
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ContextMenuProps<T extends HTMLElement> = {
|
||||||
|
options: ('divider'|Option)[],
|
||||||
|
popupItem: React.RefObject<T>
|
||||||
|
}
|
||||||
|
|
||||||
|
const buttonSx: SxProps = {
|
||||||
|
'&:hover': {
|
||||||
|
background: 'rgb(0, 30, 60)'
|
||||||
|
},
|
||||||
|
fontSize: '0.7rem',
|
||||||
|
minHeight: '30px',
|
||||||
|
justifyContent: 'center',
|
||||||
|
p: 0
|
||||||
|
};
|
||||||
|
|
||||||
|
function ContextMenu<T extends HTMLElement, >(props: ContextMenuProps<T>) {
|
||||||
|
const [anchor, setAnchor] = React.useState( { x: 0, y: 0 } );
|
||||||
|
|
||||||
|
const [show, setShow] = React.useState(false);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
const { popupItem: ref } = props;
|
||||||
|
if (ref.current === null)
|
||||||
|
return;
|
||||||
|
const listener = (ev: MouseEvent) => {
|
||||||
|
ev.preventDefault();
|
||||||
|
setAnchor({ x: ev.x + 10, y: ev.y + 10 });
|
||||||
|
setShow(true);
|
||||||
|
};
|
||||||
|
ref.current.addEventListener('contextmenu', listener);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (ref.current)
|
||||||
|
ref.current.removeEventListener('contextmenu', listener);
|
||||||
|
};
|
||||||
|
}, [ props.popupItem ]);
|
||||||
|
|
||||||
|
return show ? <Box sx={{ zIndex: 1400, p: 1, background: 'rgba(0, 0, 0, 0.75)', backdropFilter: 'blur(5px)', position: 'fixed', left: anchor.x, top: anchor.y }}>
|
||||||
|
<List sx={{ p: 0, m: 0, display: 'flex', flexDirection: 'column' }}>
|
||||||
|
{props.options.map((item, i) => {
|
||||||
|
return item === 'divider' ? <Divider key={`ContextMenu_Divider_${i}_${item}`}/> :
|
||||||
|
<Button color='inherit' key={`ContextMenu_Value_${i}_${item}`} onClick={() => {
|
||||||
|
item.onClick();
|
||||||
|
setShow(false);
|
||||||
|
}} sx={buttonSx}>
|
||||||
|
{item.text}
|
||||||
|
</Button>;
|
||||||
|
})}
|
||||||
|
<Divider />
|
||||||
|
<Button fullWidth color='inherit' onClick={() => setShow(false)} sx={buttonSx} >
|
||||||
|
Close
|
||||||
|
</Button>
|
||||||
|
</List>
|
||||||
|
</Box> : <></>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ContextMenu;
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
import { LinearProgressProps, Box, LinearProgress, Typography } from '@mui/material';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
// The following code has been taken from the mui example
|
||||||
|
// Thanks for that mui
|
||||||
|
|
||||||
|
export type LinearProgressWithLabelProps = LinearProgressProps & { value: number };
|
||||||
|
|
||||||
|
const LinearProgressWithLabel: React.FC<LinearProgressWithLabelProps> = (props) => {
|
||||||
|
return (
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'center' }}>
|
||||||
|
<Box sx={{ width: '100%', mr: 1 }}>
|
||||||
|
<LinearProgress variant="determinate" {...props} />
|
||||||
|
</Box>
|
||||||
|
<Box sx={{ minWidth: 35 }}>
|
||||||
|
<Typography variant="body2" color="text.secondary">{`${Math.round(
|
||||||
|
props.value,
|
||||||
|
)}%`}</Typography>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default LinearProgressWithLabel;
|
||||||
74
gui/react/src/components/reusable/MultiSelect.tsx
Normal file
74
gui/react/src/components/reusable/MultiSelect.tsx
Normal file
|
|
@ -0,0 +1,74 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { FormControl, InputLabel, MenuItem, OutlinedInput, Select, Theme, useTheme } from '@mui/material';
|
||||||
|
|
||||||
|
export type MultiSelectProps = {
|
||||||
|
values: string[],
|
||||||
|
selected: string[],
|
||||||
|
onChange: (values: string[]) => unknown,
|
||||||
|
title: string,
|
||||||
|
allOption?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const ITEM_HEIGHT = 48;
|
||||||
|
const ITEM_PADDING_TOP = 8;
|
||||||
|
const MenuProps = {
|
||||||
|
PaperProps: {
|
||||||
|
style: {
|
||||||
|
maxHeight: ITEM_HEIGHT * 4.5 + ITEM_PADDING_TOP,
|
||||||
|
width: 250
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
function getStyles(name: string, personName: readonly string[], theme: Theme) {
|
||||||
|
return {
|
||||||
|
fontWeight:
|
||||||
|
(personName ?? []).indexOf(name) === -1
|
||||||
|
? theme.typography.fontWeightRegular
|
||||||
|
: theme.typography.fontWeightMedium
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const MultiSelect: React.FC<MultiSelectProps> = (props) => {
|
||||||
|
const theme = useTheme();
|
||||||
|
|
||||||
|
return <div>
|
||||||
|
<FormControl sx={{ width: 300 }}>
|
||||||
|
<InputLabel id="multi-select-label">{props.title}</InputLabel>
|
||||||
|
<Select
|
||||||
|
labelId="multi-select-label"
|
||||||
|
id="multi-select"
|
||||||
|
multiple
|
||||||
|
value={(props.selected ?? [])}
|
||||||
|
onChange={e => {
|
||||||
|
const val = typeof e.target.value === 'string' ? e.target.value.split(',') : e.target.value;
|
||||||
|
if (props.allOption && val.includes('all')) {
|
||||||
|
if (props.values.length === val.length - 1)
|
||||||
|
props.onChange([]);
|
||||||
|
else
|
||||||
|
props.onChange(props.values);
|
||||||
|
} else {
|
||||||
|
props.onChange(val);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
input={<OutlinedInput id="select-multiple-chip" label={props.title} />}
|
||||||
|
renderValue={(selected) => (
|
||||||
|
selected.join(', ')
|
||||||
|
)}
|
||||||
|
MenuProps={MenuProps}
|
||||||
|
>
|
||||||
|
{props.values.concat(props.allOption ? 'all' : []).map((name) => (
|
||||||
|
<MenuItem
|
||||||
|
key={`${props.title}_${name}`}
|
||||||
|
value={name}
|
||||||
|
style={getStyles(name, props.selected, theme)}
|
||||||
|
>
|
||||||
|
{name}
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
</div>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MultiSelect;
|
||||||
12
gui/react/src/hooks/useStore.tsx
Normal file
12
gui/react/src/hooks/useStore.tsx
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { StoreAction, StoreContext, StoreState } from '../provider/Store';
|
||||||
|
|
||||||
|
const useStore = () => {
|
||||||
|
const context = React.useContext(StoreContext as unknown as React.Context<[StoreState, React.Dispatch<StoreAction<keyof StoreState>>]>);
|
||||||
|
if (!context) {
|
||||||
|
throw new Error('useStore must be used under Store');
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useStore;
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue