Compare commits
542 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
720c9bb651 | ||
|
|
f05cf5040c | ||
|
|
65191cfc8d | ||
|
|
bd2dec0392 | ||
|
|
5f9fbbb30d | ||
|
|
43f04455f1 | ||
|
|
43637a2ff4 | ||
|
|
b87bf62362 | ||
|
|
89e57d48d9 | ||
|
|
ad47aecea3 | ||
|
|
f192fd0b75 | ||
|
|
557efd2079 | ||
|
|
373c6c1d4c | ||
|
|
4c7da5dc20 | ||
|
|
bed5b00300 | ||
|
|
e36d460d57 | ||
|
|
5cffa112ba | ||
|
|
66ae60ca44 | ||
|
|
b60496bf22 | ||
|
|
ff39c59475 | ||
|
|
b6b67568b5 | ||
|
|
eecf9c42c5 | ||
|
|
fc3d61b05a | ||
|
|
b2b9760445 | ||
|
|
7afff18629 | ||
|
|
ee3effaf00 | ||
|
|
296f0478b0 | ||
|
|
693a77e73e | ||
|
|
b769cd0aa1 | ||
|
|
9eca05efc5 | ||
|
|
ca0110cdbc | ||
|
|
66ee72baf0 | ||
|
|
3c08d69bc3 | ||
|
|
0f004dc878 | ||
|
|
b7093ed4ae | ||
|
|
20289eaeaf | ||
|
|
3b16fe0390 | ||
|
|
c78c1ad628 | ||
|
|
8fe4cc5a9b | ||
|
|
930e8191be | ||
|
|
2734fb4d0d | ||
|
|
b5223148c7 | ||
|
|
ad3d0b317e | ||
|
|
e90b5583a7 | ||
|
|
394dc91bfa | ||
|
|
7de046838d | ||
|
|
955872a825 | ||
|
|
d17780f0a7 | ||
|
|
737a8063a8 | ||
|
|
423abf2479 | ||
|
|
4e436509bf | ||
|
|
f58e0e1f4f | ||
|
|
d2e72b0756 | ||
|
|
edebd860aa | ||
|
|
6b8363bd9c | ||
|
|
d0c40ffa00 | ||
|
|
b36e1484eb | ||
|
|
a5e429fea3 | ||
|
|
6d5870ccb2 | ||
|
|
6cc82520a0 | ||
|
|
7dfc599de9 | ||
|
|
aa820e1f64 | ||
|
|
38d0ac5305 | ||
|
|
438387faee | ||
|
|
5a1786d3e5 | ||
|
|
bbb80f3cf0 | ||
|
|
8ea6d4fe37 | ||
|
|
282b3d05b3 | ||
|
|
168d0280dc | ||
|
|
50e024a3ea | ||
|
|
e1d582036a | ||
|
|
354c2731d8 | ||
|
|
ce4637ea80 | ||
|
|
73ce5f372d | ||
|
|
221fa989f4 | ||
|
|
a4dfad6235 | ||
|
|
ae10810caa | ||
|
|
964e979469 | ||
|
|
556b20e669 | ||
|
|
6c102362fd | ||
|
|
49faa5d116 | ||
|
|
cf8fb4cfa4 | ||
|
|
3bb7a800e3 | ||
|
|
26b6143152 | ||
|
|
728ed7ec06 | ||
|
|
5bfbd94e51 | ||
|
|
23db19d073 | ||
|
|
bd160eb32c | ||
|
|
fb354b4746 | ||
|
|
245dcf6007 | ||
|
|
94f5c7438d | ||
|
|
d0f3e0f931 | ||
|
|
524630d671 | ||
|
|
65a0e57ab9 | ||
|
|
eccea520b7 | ||
|
|
7ec9c5b147 | ||
|
|
1ae12fa0f7 | ||
|
|
ccdd4600bb | ||
|
|
3f35ecf6f3 | ||
|
|
de13209709 | ||
|
|
34500ea3fc | ||
|
|
72c191fbc1 | ||
|
|
8c2897a0b1 | ||
|
|
31da8aa92c | ||
|
|
92c45d553e | ||
|
|
ae5829dcb0 | ||
|
|
3f5a82d663 | ||
|
|
2612860f15 | ||
|
|
e6adf6549a | ||
|
|
c23ffa382d | ||
|
|
1f4d81bfc9 | ||
|
|
455d84d5b1 | ||
|
|
72c19e7f59 | ||
|
|
e52cee2125 | ||
|
|
5c87c5d9e2 | ||
|
|
8f8ecb868e | ||
|
|
a876c9b144 | ||
|
|
2ba6e4fa35 | ||
|
|
50a7e5559b | ||
|
|
db860c25c6 | ||
|
|
074311107a | ||
|
|
19a73b3866 | ||
|
|
75c9509793 | ||
|
|
bc98d64969 | ||
|
|
a90d7b1cad | ||
|
|
43e16f5bfa | ||
|
|
622b1b3bcc | ||
|
|
12ea135feb | ||
|
|
e5197fdb8b | ||
|
|
a5539bcf59 | ||
|
|
d3db3fc512 | ||
|
|
9945ed4615 | ||
|
|
28a83e2f29 | ||
|
|
93fe1ae871 | ||
|
|
1ddc8d99d6 | ||
|
|
3c979e77cc | ||
|
|
a00fa2c3d6 | ||
|
|
7c886583c9 | ||
|
|
92a4ed5578 | ||
|
|
e8ed29eb9d | ||
|
|
ef2cc7f16b | ||
|
|
646e75d5f8 | ||
|
|
d259587491 | ||
|
|
7c99e4d1c1 | ||
|
|
aeb15d8640 | ||
|
|
771d664e5a | ||
|
|
7b708e2385 | ||
|
|
a9c281c8c4 | ||
|
|
8aa1344fa5 | ||
|
|
5fd62028ce | ||
|
|
5b49dd0735 | ||
|
|
7bb38fda3e | ||
|
|
507b96014c | ||
|
|
986db2cc67 | ||
|
|
546dee9799 | ||
|
|
4cfbf731b8 | ||
|
|
231a12f685 | ||
|
|
e54b54bfb3 | ||
|
|
58966ccff1 | ||
|
|
59c84952ed | ||
|
|
90c56debfe | ||
|
|
9cf9c4498c | ||
|
|
31ca9597e5 | ||
|
|
17e70c169b | ||
|
|
79c8e7fc32 | ||
|
|
26278aadba | ||
|
|
5ef6119294 | ||
|
|
96c4b1f16b | ||
|
|
4cf08fc6d5 | ||
|
|
bad1170421 | ||
|
|
7593d3d62e | ||
|
|
ce11a80ba9 | ||
|
|
5103be82a4 | ||
|
|
9ee91dff31 | ||
|
|
381fb89cfb | ||
|
|
9354c0ee9b | ||
|
|
51aa127a33 | ||
|
|
9d6d138fff | ||
|
|
a189cddd37 | ||
|
|
f7cf9da686 | ||
|
|
d00cc8a94a | ||
|
|
20ce2ac831 | ||
|
|
c6a6a70eaf | ||
|
|
9e256186aa | ||
|
|
f047287fe9 | ||
|
|
816123ab83 | ||
|
|
815d3b6b1e | ||
|
|
05b80a2d63 | ||
|
|
de79dc014f | ||
|
|
1a99a9e99b | ||
|
|
f49769f3c4 | ||
|
|
77cf6767b1 | ||
|
|
d1fcd671c9 | ||
|
|
5cb4944413 | ||
|
|
559ee6ed4e | ||
|
|
cc1e96f3cf | ||
|
|
b273e3a71d | ||
|
|
66fb0e2e6d | ||
|
|
baf5c9143d | ||
|
|
28a8a1554d | ||
|
|
515ca0a98c | ||
|
|
dcad7db29c | ||
|
|
4bec9e556f | ||
|
|
da7397fe75 | ||
|
|
86eb3d498c | ||
|
|
ca8caa7795 | ||
|
|
f6f71b043b | ||
|
|
281fe00a35 | ||
|
|
e5d68c5214 | ||
|
|
04f3fc42e0 | ||
|
|
ed50cb2f6a | ||
|
|
e1e8f37862 | ||
|
|
7146c6cf11 | ||
|
|
41bc53b9da | ||
|
|
bfe8c36e87 | ||
|
|
430e5b7fce | ||
|
|
8d1991ed84 | ||
|
|
c075fe9481 | ||
|
|
7e4891a93f | ||
|
|
f8f62db174 | ||
|
|
93a098e438 | ||
|
|
1f4d7c752d | ||
|
|
9b3c7c76cf | ||
|
|
ac4cc8a346 | ||
|
|
e7b1e2a4fc | ||
|
|
f8b63f4cce | ||
|
|
30c60261af | ||
|
|
9079feebbd | ||
|
|
a7d93590c6 | ||
|
|
d20c568e85 | ||
|
|
c8e38d73ff | ||
|
|
135d90a233 | ||
|
|
9f5ac02ec6 | ||
|
|
6f24b7928d | ||
|
|
b5fc29f150 | ||
|
|
6cc24c0332 | ||
|
|
924265b964 | ||
|
|
7264fd2ed4 | ||
|
|
0610b8556a | ||
|
|
9fd563450e | ||
|
|
ddf4f227b3 | ||
|
|
36a15f0091 | ||
|
|
58bd6250c9 | ||
|
|
10081a2276 | ||
|
|
fa6bee4c83 | ||
|
|
40f9f9ec8c | ||
|
|
01649955b1 | ||
|
|
5d412b759f | ||
|
|
4f7a9d725e | ||
|
|
e3ea65d7f4 | ||
|
|
d71aa240e7 | ||
|
|
e438f3cbbd | ||
|
|
d44e0a2588 | ||
|
|
258e35c94d | ||
|
|
d607b7fb6b | ||
|
|
42b9cbe38f | ||
|
|
d7edb51698 | ||
|
|
1bd85fb4ad | ||
|
|
580845ae77 | ||
|
|
32b49d49a9 | ||
|
|
5ff8a49be5 | ||
|
|
3955820eb9 | ||
|
|
609bd23cd8 | ||
|
|
0f7bc7c07f | ||
|
|
cf0be4712e | ||
|
|
50c5d3a1cd | ||
|
|
8dae887eca | ||
|
|
3be37008b7 | ||
|
|
ae00c43106 | ||
|
|
d7392c09d2 | ||
|
|
f549a32dd2 | ||
|
|
e092d7247d | ||
|
|
d3dbedf18a | ||
|
|
90ab3d0a31 | ||
|
|
cba3c8bbae | ||
|
|
4a14ad7fb0 | ||
|
|
5c04104ad2 | ||
|
|
ef2b6721c7 | ||
|
|
d7e82ceff3 | ||
|
|
973eb06584 | ||
|
|
825d97adc4 | ||
|
|
8835219c26 | ||
|
|
1dd9d324e6 | ||
|
|
afbc16a3ec | ||
|
|
98b9d5a81d | ||
|
|
6f6a3ae2fd | ||
|
|
5300e6c6e7 | ||
|
|
9f7155f42e | ||
|
|
362bef9f57 | ||
|
|
4cd1e09466 | ||
|
|
1765311586 | ||
|
|
68cfa65135 | ||
|
|
b7930014ab | ||
|
|
67d0508fd7 | ||
|
|
280abc4dea | ||
|
|
e1c727b7de | ||
|
|
d07d31d94a | ||
|
|
09f795b862 | ||
|
|
f5f47403cc | ||
|
|
31e46c5c56 | ||
|
|
219cb4b5f6 | ||
|
|
683e26e32a | ||
|
|
9bec5b5f3b | ||
|
|
4815054fed | ||
|
|
c50e095781 | ||
|
|
b4ba098158 | ||
|
|
836021c57e | ||
|
|
0a85b0dc1e | ||
|
|
3b78186702 | ||
|
|
dbbf310661 | ||
|
|
7eb760b5d6 | ||
|
|
e2268a325a | ||
|
|
3f0c661dae | ||
|
|
7dca36fa83 | ||
|
|
159be1cac4 | ||
|
|
e478a45bc0 | ||
|
|
d80ca63dad | ||
|
|
57bc4640e5 | ||
|
|
33dbeb9776 | ||
|
|
3c398d4a1d | ||
|
|
943f4c3453 | ||
|
|
8e1ea10c5a | ||
|
|
a7bc742035 | ||
|
|
82b810ed36 | ||
|
|
6eceaf474d | ||
|
|
88a1fe10a1 | ||
|
|
240504816c | ||
|
|
053dce0729 | ||
|
|
a36d152b61 | ||
|
|
e5a5c62106 | ||
|
|
db25f98251 | ||
|
|
ad7c9cc545 | ||
|
|
331e5bd5d1 | ||
|
|
c4bf32f991 | ||
|
|
20c43a3ea1 | ||
|
|
172edaf0fc | ||
|
|
4bc5e42e50 | ||
|
|
88b6a5ef45 | ||
|
|
5078609c86 | ||
|
|
e6ef4b870a | ||
|
|
68dcab39fb | ||
|
|
79c16d530d | ||
|
|
365c2de4e5 | ||
|
|
7cabb4d4ab | ||
|
|
73476779ae | ||
|
|
bd1e184023 | ||
|
|
cc7b0be150 | ||
|
|
e2eaa9143b | ||
|
|
23da8dc637 | ||
|
|
6203863523 | ||
|
|
e6d8d5076c | ||
|
|
cc3d30be2f | ||
|
|
77164dfaa7 | ||
|
|
56e72b73fe | ||
|
|
c8b6cf2732 | ||
|
|
a9b81d00c0 | ||
|
|
03bb9fea79 | ||
|
|
bb733fca96 | ||
|
|
ae7b784783 | ||
|
|
d59658fb7b | ||
|
|
20a4eb2f94 | ||
|
|
ad61be8626 | ||
|
|
209ec2673f | ||
|
|
6fffb57a8d | ||
|
|
5d6a8145c3 | ||
|
|
00fcda4f12 | ||
|
|
e989fc05d5 | ||
|
|
ea2c128937 | ||
|
|
a93799036d | ||
|
|
ce2fac6c4c | ||
|
|
bcd868850d | ||
|
|
eaa95e8cd9 | ||
|
|
a76e7254ff | ||
|
|
14da572830 | ||
|
|
c7b507ca6b | ||
|
|
6f60c49bd1 | ||
|
|
0018ccc929 | ||
|
|
177739e7cd | ||
|
|
5bf8096158 | ||
|
|
066424e087 | ||
|
|
7e08a8090c | ||
|
|
a4fb495e28 | ||
|
|
9ab69d8461 | ||
|
|
1a56d7ef91 | ||
|
|
c88b3798f0 | ||
|
|
076f853a5b | ||
|
|
405a8efad3 | ||
|
|
6f3233eefd | ||
|
|
3007698b1b | ||
|
|
0830635947 | ||
|
|
c7040e9936 | ||
|
|
da24e96fb4 | ||
|
|
a42edcd438 | ||
|
|
cf1f3e9eed | ||
|
|
c5c4c1a41c | ||
|
|
fbffe3aa67 | ||
|
|
8761eee22f | ||
|
|
068017941b | ||
|
|
a3b90bf19a | ||
|
|
22c6754efc | ||
|
|
5e1c3eea24 | ||
|
|
e11cf8986e | ||
|
|
b8c5c1a4f3 | ||
|
|
06e0ba23d7 | ||
|
|
8fb3320868 | ||
|
|
b3db546e00 | ||
|
|
312f60660a | ||
|
|
bab0da2a8f | ||
|
|
a7360b68f0 | ||
|
|
f1c00e2684 | ||
|
|
298ac96558 | ||
|
|
a54ae2b151 | ||
|
|
d0958e8771 | ||
|
|
8b4aef94be | ||
|
|
c19d46f406 | ||
|
|
5170dde1d6 | ||
|
|
c27c1f9541 | ||
|
|
424c2fd1e0 | ||
|
|
bb21bbc7fa | ||
|
|
d1493cbb57 | ||
|
|
30a709d619 | ||
|
|
c610a995c2 | ||
|
|
78125c7f37 | ||
|
|
7191c59b8a | ||
|
|
eea8cac018 | ||
|
|
5a4f28e50a | ||
|
|
f7bb497432 | ||
|
|
b8360405dd | ||
|
|
753e90bbe2 | ||
|
|
ac7d2480ea | ||
|
|
26120d1662 | ||
|
|
5ac359f1eb | ||
|
|
34b7fddfc7 | ||
|
|
390adea9d4 | ||
|
|
7e4657c6ad | ||
|
|
04069d83ea | ||
|
|
0c0c2987e8 | ||
|
|
012b1648e1 | ||
|
|
04b8987c30 | ||
|
|
9f46b7fffa | ||
|
|
b5d7e95a92 | ||
|
|
c3721c428e | ||
|
|
d6fb4c971b | ||
|
|
5466472b21 | ||
|
|
f0a66e1984 | ||
|
|
4598151034 | ||
|
|
8f2e8e492f | ||
|
|
e327f3c734 | ||
|
|
20648ae4cd | ||
|
|
4bf2cf632b | ||
|
|
0879531478 | ||
|
|
4bb8e132f5 | ||
|
|
fb689bf7da | ||
|
|
6c44fccf66 | ||
|
|
b670fc7109 | ||
|
|
d8a4375783 | ||
|
|
60530343b4 | ||
|
|
fcced2e122 | ||
|
|
af5c3f2ae6 | ||
|
|
be7c19a54f | ||
|
|
708d3d785d | ||
|
|
25b326ee48 | ||
|
|
447153ca3c | ||
|
|
1fb7d0c66b | ||
|
|
ae342845f8 | ||
|
|
0ff0758b74 | ||
|
|
7760c13ea9 | ||
|
|
3757a3514b | ||
|
|
6214df7e8f | ||
|
|
42d46accb6 | ||
|
|
ddc8133c5b | ||
|
|
a96d015a0c | ||
|
|
b1244086cd | ||
|
|
da556d3523 | ||
|
|
6f412d736b | ||
|
|
4697d150cf | ||
|
|
4c5f5a3514 | ||
|
|
0a0bedacb4 | ||
|
|
7e771c2644 | ||
|
|
9709c31bcd | ||
|
|
2e3b6602af | ||
|
|
6d5a511cb4 | ||
|
|
1a2b40d6f2 | ||
|
|
b7714d7d60 | ||
|
|
953a7914c9 | ||
|
|
092d53ef9d | ||
|
|
48b0924bd8 | ||
|
|
a2af1ae21e | ||
|
|
833c4870f8 | ||
|
|
a30b271a57 | ||
|
|
f9d8dc0b86 | ||
|
|
60f44c9603 | ||
|
|
1433d032f3 | ||
|
|
d90cd8f984 | ||
|
|
c10ca29fda | ||
|
|
c819dd6bb7 | ||
|
|
d06c2934a8 | ||
|
|
261897c699 | ||
|
|
6ef80d3166 | ||
|
|
aeb7ce5f54 | ||
|
|
1028c663eb | ||
|
|
e33aa42b19 | ||
|
|
48b6f9efb0 | ||
|
|
f4d905833f | ||
|
|
65c73e8b6e | ||
|
|
737bf6aa94 | ||
|
|
f89d90619a | ||
|
|
a3c25adc7a | ||
|
|
ae32376758 | ||
|
|
aded562304 | ||
|
|
8b479be6c5 | ||
|
|
8acbd37df3 | ||
|
|
cf6946bc4a | ||
|
|
78db179957 | ||
|
|
9a279b53a4 | ||
|
|
2aa9ad270e | ||
|
|
4ca87f0ff1 | ||
|
|
dc610bfa64 | ||
|
|
58b50b49de | ||
|
|
a79b0bdfdd | ||
|
|
b67e1b85d6 | ||
|
|
2421913587 | ||
|
|
4078f0c404 | ||
|
|
731bd33470 | ||
|
|
4351ddc00d | ||
|
|
941a61fb74 | ||
|
|
a729dc85e0 | ||
|
|
491595934f | ||
|
|
d087318286 | ||
|
|
5429b0cb7a | ||
|
|
69da82b139 | ||
|
|
fdb9bff86b | ||
|
|
cb256de9f8 | ||
|
|
b27126fd8e | ||
|
|
b9c13b8a89 | ||
|
|
2a47d97d5c | ||
|
|
0f00aecde5 | ||
|
|
7ee87ceb9e | ||
|
|
81208ab99b | ||
|
|
b513a8926b | ||
|
|
2f370d1b9c | ||
|
|
ffd62a5d16 |
732 changed files with 102118 additions and 43353 deletions
38
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
38
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
---
|
||||||
|
name: Bug report
|
||||||
|
about: Create a report to help us improve
|
||||||
|
title: ''
|
||||||
|
labels: bug
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Describe the bug**
|
||||||
|
A clear and concise description of what the bug is.
|
||||||
|
|
||||||
|
**To Reproduce**
|
||||||
|
Steps to reproduce the behavior:
|
||||||
|
1. Go to '...'
|
||||||
|
2. Click on '....'
|
||||||
|
3. Scroll down to '....'
|
||||||
|
4. See error
|
||||||
|
|
||||||
|
**Expected behavior**
|
||||||
|
A clear and concise description of what you expected to happen.
|
||||||
|
|
||||||
|
**Screenshots**
|
||||||
|
If applicable, add screenshots to help explain your problem.
|
||||||
|
|
||||||
|
**Desktop (please complete the following information):**
|
||||||
|
- OS: [e.g. iOS]
|
||||||
|
- Browser [e.g. chrome, safari]
|
||||||
|
- Version [e.g. 22]
|
||||||
|
|
||||||
|
**Smartphone (please complete the following information):**
|
||||||
|
- Device: [e.g. iPhone6]
|
||||||
|
- OS: [e.g. iOS8.1]
|
||||||
|
- Browser [e.g. stock browser, safari]
|
||||||
|
- Version [e.g. 22]
|
||||||
|
|
||||||
|
**Additional context**
|
||||||
|
Add any other context about the problem here.
|
||||||
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
---
|
||||||
|
name: Feature request
|
||||||
|
about: Suggest an idea for this project
|
||||||
|
title: ''
|
||||||
|
labels: enhancement
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Is your feature request related to a problem? Please describe.**
|
||||||
|
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||||
|
|
||||||
|
**Describe the solution you'd like**
|
||||||
|
A clear and concise description of what you want to happen.
|
||||||
|
|
||||||
|
**Describe alternatives you've considered**
|
||||||
|
A clear and concise description of any alternative solutions or features you've considered.
|
||||||
|
|
||||||
|
**Additional context**
|
||||||
|
Add any other context or screenshots about the feature request here.
|
||||||
23
.github/ISSUE_TEMPLATE/request-source-extractor.md
vendored
Normal file
23
.github/ISSUE_TEMPLATE/request-source-extractor.md
vendored
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
---
|
||||||
|
name: Request source/extractor
|
||||||
|
about: Request a source or extractor to get URLs from
|
||||||
|
title: Request to add <source/extractor>
|
||||||
|
labels: enhancement
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**What is the site you want to get added?**
|
||||||
|
A link to the source or embed hoster
|
||||||
|
|
||||||
|
**What is the benefit of adding this?**
|
||||||
|
More content is not a good reason. If it's already covered via an existing source/extractor, why should we add another one?
|
||||||
|
|
||||||
|
**Are you aware of any protection the site uses?**
|
||||||
|
E.g. rate-limits or Cloudflare captchas and such
|
||||||
|
|
||||||
|
**What languages does this relate to?**
|
||||||
|
E.g. multi, German, ..
|
||||||
|
|
||||||
|
**Example**
|
||||||
|
A movie/series IMDb/TMDB ID + source link or embed URL to test with.
|
||||||
28
.github/workflows/build-tag.yml
vendored
Normal file
28
.github/workflows/build-tag.yml
vendored
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
name: Build and push latest
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- 'v*'
|
||||||
|
jobs:
|
||||||
|
docker:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
-
|
||||||
|
name: Login to Docker Hub
|
||||||
|
uses: docker/login-action@v4
|
||||||
|
with:
|
||||||
|
username: ${{ vars.DOCKERHUB_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
-
|
||||||
|
name: Set up QEMU (for cross-arch)
|
||||||
|
uses: docker/setup-qemu-action@v4
|
||||||
|
-
|
||||||
|
name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v4
|
||||||
|
-
|
||||||
|
name: Build and push
|
||||||
|
uses: docker/build-push-action@v7
|
||||||
|
with:
|
||||||
|
push: true
|
||||||
|
platforms: linux/amd64,linux/arm64
|
||||||
|
tags: webstreamr/webstreamr:latest
|
||||||
9
.github/workflows/build.yml
vendored
9
.github/workflows/build.yml
vendored
|
|
@ -1,15 +1,18 @@
|
||||||
name: Build
|
name: Build
|
||||||
on: push
|
on:
|
||||||
|
push:
|
||||||
|
pull_request:
|
||||||
|
workflow_dispatch:
|
||||||
jobs:
|
jobs:
|
||||||
docker:
|
docker:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
-
|
-
|
||||||
name: Set up Docker Buildx
|
name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v3
|
uses: docker/setup-buildx-action@v4
|
||||||
-
|
-
|
||||||
name: Build and push
|
name: Build and push
|
||||||
uses: docker/build-push-action@v6
|
uses: docker/build-push-action@v7
|
||||||
with:
|
with:
|
||||||
push: false
|
push: false
|
||||||
tags: webstreamr/webstreamr:latest
|
tags: webstreamr/webstreamr:latest
|
||||||
|
|
|
||||||
23
.github/workflows/tests.yml
vendored
23
.github/workflows/tests.yml
vendored
|
|
@ -1,26 +1,17 @@
|
||||||
name: Tests
|
name: Tests
|
||||||
on: push
|
on:
|
||||||
|
push:
|
||||||
|
pull_request:
|
||||||
|
workflow_dispatch:
|
||||||
jobs:
|
jobs:
|
||||||
test:
|
test:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v6
|
||||||
- name: Setup Node.js
|
- name: Setup Node.js
|
||||||
uses: actions/setup-node@v5
|
uses: actions/setup-node@v6
|
||||||
with:
|
with:
|
||||||
node-version: "22"
|
node-version: "24"
|
||||||
|
|
||||||
- name: Cache node modules
|
|
||||||
uses: actions/cache@v4
|
|
||||||
env:
|
|
||||||
cache-name: cache-node-modules
|
|
||||||
with:
|
|
||||||
path: ~/.npm
|
|
||||||
key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }}
|
|
||||||
restore-keys: |
|
|
||||||
${{ runner.os }}-build-${{ env.cache-name }}-
|
|
||||||
${{ runner.os }}-build-
|
|
||||||
${{ runner.os }}-
|
|
||||||
|
|
||||||
- name: Install Dependencies
|
- name: Install Dependencies
|
||||||
run: npm install
|
run: npm install
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
{
|
{
|
||||||
".": "0.49.7"
|
".": "0.69.1"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
1361
CHANGELOG.md
1361
CHANGELOG.md
File diff suppressed because it is too large
Load diff
|
|
@ -1,4 +1,4 @@
|
||||||
FROM node:22.19-alpine AS builder
|
FROM node:24 AS builder
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
COPY package*.json ./
|
COPY package*.json ./
|
||||||
|
|
@ -10,7 +10,7 @@ RUN npm run build
|
||||||
|
|
||||||
RUN npm ci --only=production
|
RUN npm ci --only=production
|
||||||
|
|
||||||
FROM node:22.19-alpine
|
FROM node:24
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
COPY --from=builder /app/dist ./dist
|
COPY --from=builder /app/dist ./dist
|
||||||
|
|
|
||||||
98
README.md
98
README.md
|
|
@ -1,15 +1,47 @@
|
||||||
# WebStreamr
|
# WebStreamr
|
||||||
|
|
||||||
[](https://github.com/webstreamr/webstreamr/actions/workflows/tests.yml)
|
[](https://github.com/webstreamr/webstreamr/actions/workflows/tests.yml)
|
||||||
[](https://github.com/webstreamr/webstreamr/releases)
|
[](https://github.com/webstreamr/webstreamr/releases)
|
||||||

|

|
||||||
|
|
||||||
[Stremio](https://www.stremio.com/) add-on which provides HTTP URLs from streaming websites.
|
[Stremio](https://www.stremio.com/) add-on which provides HTTP URLs from streaming websites.
|
||||||
|
|
||||||
|
HTTP streams have limitations.
|
||||||
|
For a better experience, I'd advise using a Debrid service and WebStreamr as fallback.
|
||||||
|
[TorBox](https://torbox.app/subscription?referral=f22eb00d-27ce-4e20-85fc-68da3d018b99) is working very well.
|
||||||
|
|
||||||
## Public instance
|
## Public instance
|
||||||
|
|
||||||
A public instance is available at https://webstreamr.hayd.uk. Hosting infrastructure for this instance is donated by [ElfHosted](https://elfhosted.com), and independently maintained by [Hayduk](https://hayd.uk).
|
A public instance is available at https://webstreamr.hayd.uk. Hosting infrastructure for this instance is donated by [ElfHosted](https://elfhosted.com), and independently maintained by [Hayduk](https://hayd.uk).
|
||||||
|
|
||||||
|
## Known issues / limitations
|
||||||
|
|
||||||
|
- PixelServer / pixeldrain has a daily limit of 6 GB per IP: https://pixeldrain.dev
|
||||||
|
- Dropload and SuperVideo on Android do not work because Stremio does not use the `Referer` header properly via HLS playlists: https://github.com/Stremio/stremio-bugs/issues/2389, maybe https://github.com/Stremio/stremio-bugs/issues/1579
|
||||||
|
- MediaFlow proxy has to be used in an inefficient way because Stremio on Android or its players cannot deal with HLS playlist with redirects: https://github.com/Stremio/stremio-bugs/issues/1574
|
||||||
|
- FlareSolverr cookies cannot be used because Cloudflare does techniques like TLS fingerprinting most likely. But FlareSolverr uses a session per host and should be quick.
|
||||||
|
- VidSrc works but rate limits heavily and is therefore only queried as fallback if nothing else is found.
|
||||||
|
- RgShows detects shared usage and blocks IPs. It therefore only works on private instances.
|
||||||
|
|
||||||
|
## MediaFlow Proxy
|
||||||
|
|
||||||
|
[MediaFlow Proxy](https://github.com/mhdzumair/mediaflow-proxy/) can be added when configuration the add-on to gain access to a couple of more file hosters.
|
||||||
|
It depends highly on the language / source used if that unlocks more streams or not.
|
||||||
|
|
||||||
|
MediaFlow proxy is needed because some hosters ip-lock streams and the add-on does not run on the same device that will stream the video.
|
||||||
|
|
||||||
|
The following hosters can be used only with MediaFlow Proxy:
|
||||||
|
|
||||||
|
- Fastream
|
||||||
|
- FileLions
|
||||||
|
- FileMoon
|
||||||
|
- LuluStream
|
||||||
|
- Mixdrop
|
||||||
|
- Streamtape
|
||||||
|
- VOE
|
||||||
|
|
||||||
|
MediaFlow proxy can either be self-hosted or acquired via bundle from [ElfHosted](https://docs.elfhosted.com/app/mediaflow-proxy/).
|
||||||
|
|
||||||
## Hosting
|
## Hosting
|
||||||
|
|
||||||
Don't want to use the public instance, or concerned about reliability during periods of high use? It's open-source, you can host it yourself!
|
Don't want to use the public instance, or concerned about reliability during periods of high use? It's open-source, you can host it yourself!
|
||||||
|
|
@ -22,12 +54,64 @@ Self-hosting to stressful? ElfHosted offer [ready-to-go, turn-key WebStreamr ins
|
||||||
|
|
||||||
### Self-Hosting
|
### Self-Hosting
|
||||||
|
|
||||||
You can build an image using the [Dockerfile](./Dockerfile) and run it.
|
You can run the latest WebStreamr via Docker. E.g.
|
||||||
|
|
||||||
Alternatively, you can also start WebStreamr directly via
|
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
npm install
|
docker run \
|
||||||
npm run build
|
--detach=true \
|
||||||
npm start
|
--name webstreamr \
|
||||||
|
--rm \
|
||||||
|
--pull always \
|
||||||
|
--publish 51546:51546 \
|
||||||
|
--env TMDB_ACCESS_TOKEN="YOUR_TOKEN" \
|
||||||
|
--volume /tmp:/tmp \
|
||||||
|
webstreamr/webstreamr
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Environment variables
|
||||||
|
|
||||||
|
#### `CACHE_DIR`
|
||||||
|
|
||||||
|
Optional. Directory for persistent caches using SQLite files. Default: OS tmp dir.
|
||||||
|
|
||||||
|
#### `CONFIGURATION_DESCRIPTION`
|
||||||
|
|
||||||
|
Optional. To customize the description shown on the configuration page.
|
||||||
|
|
||||||
|
#### `DISABLED_EXTRACTORS`
|
||||||
|
|
||||||
|
Optional. Comma separated list of extractors which should be disabled. E.g. `doodstream,vidsrc`
|
||||||
|
|
||||||
|
#### `DISABLED_SOURCES`
|
||||||
|
|
||||||
|
Optional. Comma separated list of sources which should be disabled. E.g. `frembed,vidsrc`
|
||||||
|
|
||||||
|
#### `FLARESOLVERR_ENDPOINT`
|
||||||
|
|
||||||
|
Optional. If domains show Cloudflare challenges, FlareSolverr can be used to work around them. E.g. `http://flaresolverr:8191`
|
||||||
|
Proxy configuration is passed-through and only a single session is used to save resources. Byparr is not supported.
|
||||||
|
|
||||||
|
#### `MANIFEST_ID`
|
||||||
|
|
||||||
|
Optional. Add-on manifest ID. Default: `webstreamr`
|
||||||
|
|
||||||
|
#### `MANIFEST_NAME`
|
||||||
|
|
||||||
|
Optional. Add-on manifest name. Default: `WebStreamr`
|
||||||
|
|
||||||
|
#### `PORT`
|
||||||
|
|
||||||
|
Optional. Port of the node web server. Default: `51546`
|
||||||
|
|
||||||
|
#### `PROXY_CONFIG`
|
||||||
|
|
||||||
|
Optional. Proxies which should be used based on domain. Supports minimatch. E.g. `dood.to:http://USERNAME:PASSWORD@IP:PORT,*:socks5://172.17.0.1:1080` would use an http proxy for dood.to and a socks5 proxy for all other domains.
|
||||||
|
|
||||||
|
Some hosters are a bit picky when it comes to IPs. Best case is if you use a residential IP.
|
||||||
|
If you can't do that, then I suggest to use a VPN / proxy like Cloudflare WARP.
|
||||||
|
DoodStream is not working with WARP.
|
||||||
|
Free Webshare proxies seem to work with it.
|
||||||
|
|
||||||
|
#### `TMDB_ACCESS_TOKEN`
|
||||||
|
|
||||||
|
**Required**. TMDB access token to get information like title and year for content. Use the [API Read Access Token](https://www.themoviedb.org/settings/api).
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ const config: Config = {
|
||||||
coverageDirectory: '<rootDir>/coverage',
|
coverageDirectory: '<rootDir>/coverage',
|
||||||
coveragePathIgnorePatterns: [
|
coveragePathIgnorePatterns: [
|
||||||
'/src/controller/',
|
'/src/controller/',
|
||||||
|
'/src/utils/dispatcher.ts',
|
||||||
],
|
],
|
||||||
coverageProvider: 'babel',
|
coverageProvider: 'babel',
|
||||||
coverageThreshold: {
|
coverageThreshold: {
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,10 @@
|
||||||
import { socksDispatcher } from 'fetch-socks';
|
|
||||||
import { Agent, Dispatcher, interceptors, ProxyAgent, setGlobalDispatcher } from 'undici';
|
|
||||||
|
|
||||||
process.env['CACHE_DIR'] = '/dev/null';
|
process.env['CACHE_DIR'] = '/dev/null';
|
||||||
|
process.env['HOST'] = 'example.test';
|
||||||
|
process.env['PROTOCOL'] = 'https';
|
||||||
|
|
||||||
|
if (!process.env['TMDB_ACCESS_TOKEN']) {
|
||||||
|
process.env['TMDB_ACCESS_TOKEN'] = 'some access token';
|
||||||
|
}
|
||||||
|
|
||||||
jest.mock('randomstring', () => ({
|
jest.mock('randomstring', () => ({
|
||||||
generate: jest.fn(() => 'mocked-random-string'),
|
generate: jest.fn(() => 'mocked-random-string'),
|
||||||
|
|
@ -12,20 +15,3 @@ beforeEach(() => {
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log = console.warn = console.error = console.info = console.debug = () => { /* disable in favor of logger */ };
|
console.log = console.warn = console.error = console.info = console.debug = () => { /* disable in favor of logger */ };
|
||||||
|
|
||||||
let dispatcher: Dispatcher;
|
|
||||||
if (process.env['ALL_PROXY']) {
|
|
||||||
const proxyUrl = new URL(process.env['ALL_PROXY']);
|
|
||||||
if (proxyUrl.protocol === 'socks5:') {
|
|
||||||
dispatcher = socksDispatcher({ type: 5, host: proxyUrl.hostname, port: parseInt(proxyUrl.port) }, { allowH2: true });
|
|
||||||
} else {
|
|
||||||
dispatcher = new ProxyAgent({ uri: proxyUrl.href, allowH2: true });
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
dispatcher = new Agent({ allowH2: true });
|
|
||||||
}
|
|
||||||
dispatcher.compose(
|
|
||||||
interceptors.dns(),
|
|
||||||
interceptors.retry({ maxRetries: 3 }),
|
|
||||||
);
|
|
||||||
setGlobalDispatcher(dispatcher);
|
|
||||||
|
|
|
||||||
4219
package-lock.json
generated
4219
package-lock.json
generated
File diff suppressed because it is too large
Load diff
41
package.json
41
package.json
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "webstreamr",
|
"name": "webstreamr",
|
||||||
"description": "Provides HTTP URLs from streaming websites.",
|
"description": "Provides HTTP URLs from streaming websites.",
|
||||||
"version": "0.49.7",
|
"version": "0.69.1",
|
||||||
"type": "commonjs",
|
"type": "commonjs",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"analyse": "tsc --noEmit --project ./tsconfig.dev.json",
|
"analyse": "tsc --noEmit --project ./tsconfig.dev.json",
|
||||||
|
|
@ -28,43 +28,60 @@
|
||||||
"url": "https://github.com/webstreamr/webstreamr/issues"
|
"url": "https://github.com/webstreamr/webstreamr/issues"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^22.0.0"
|
"node": "^22.0.0 || ^24.0.0"
|
||||||
|
},
|
||||||
|
"devEngines": {
|
||||||
|
"runtime": {
|
||||||
|
"name": "node",
|
||||||
|
"onFail": "error"
|
||||||
|
},
|
||||||
|
"packageManager": {
|
||||||
|
"name": "npm",
|
||||||
|
"onFail": "error"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@keyv/sqlite": "^4.0.5",
|
"@keyv/sqlite": "^4.0.5",
|
||||||
"@types/jsdom": "^21.1.7",
|
|
||||||
"async-mutex": "^0.5.0",
|
"async-mutex": "^0.5.0",
|
||||||
|
"axios": "^1.13.2",
|
||||||
|
"axios-cache-interceptor": "^1.8.3",
|
||||||
|
"axios-mock-adapter": "^2.1.0",
|
||||||
|
"axios-retry": "^4.5.0",
|
||||||
"bytes": "^3.1.2",
|
"bytes": "^3.1.2",
|
||||||
"cacheable": "^2.0.0",
|
"cacheable": "^2.0.0",
|
||||||
"cheerio": "^1.0.0",
|
"cheerio": "^1.0.0",
|
||||||
"express": "^5.1.0",
|
"express": "^5.1.0",
|
||||||
"fetch-socks": "^1.3.2",
|
"express-rate-limit": "^8.2.1",
|
||||||
"http-cache-semantics": "^4.2.0",
|
"fast-levenshtein": "^3.0.0",
|
||||||
"jsdom": "^26.1.0",
|
"glob": "^13.0.0",
|
||||||
|
"http-proxy-agent": "^7.0.2",
|
||||||
|
"https-proxy-agent": "^7.0.6",
|
||||||
|
"memoizee": "^0.4.17",
|
||||||
|
"minimatch": "^10.0.3",
|
||||||
"randomstring": "^1.3.1",
|
"randomstring": "^1.3.1",
|
||||||
"rot13-cipher": "^1.0.0",
|
"rot13-cipher": "^1.0.0",
|
||||||
"slugify": "^1.6.6",
|
"slugify": "^1.6.6",
|
||||||
|
"socks-proxy-agent": "^8.0.5",
|
||||||
|
"sqlite3": "^5.1.7",
|
||||||
"tough-cookie": "^6.0.0",
|
"tough-cookie": "^6.0.0",
|
||||||
"undici": "^7.10.0",
|
|
||||||
"unpacker": "^1.0.1",
|
"unpacker": "^1.0.1",
|
||||||
"winston": "^3.17.0"
|
"winston": "^3.17.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.26.0",
|
"@eslint/js": "^9.26.0",
|
||||||
"@stylistic/eslint-plugin": "^5.0.0",
|
"@stylistic/eslint-plugin": "^5.0.0",
|
||||||
"@tsconfig/node22": "^22.0.2",
|
"@tsconfig/node24": "^24.0.0",
|
||||||
"@types/bytes": "^3.1.5",
|
"@types/bytes": "^3.1.5",
|
||||||
"@types/express": "^5.0.1",
|
"@types/express": "^5.0.1",
|
||||||
"@types/http-cache-semantics": "^4.0.4",
|
"@types/fast-levenshtein": "^0.0.4",
|
||||||
"@types/jest": "^30.0.0",
|
"@types/jest": "^30.0.0",
|
||||||
"@types/make-fetch-happen": "^10.0.4",
|
"@types/memoizee": "^0.4.12",
|
||||||
"@types/node": "^22.15.3",
|
"@types/node": "^24.0.0",
|
||||||
"@types/randomstring": "^1.3.0",
|
"@types/randomstring": "^1.3.0",
|
||||||
"@types/stremio-addon-sdk": "^1.6.11",
|
"@types/stremio-addon-sdk": "^1.6.11",
|
||||||
"babel-jest": "^30.0.0",
|
"babel-jest": "^30.0.0",
|
||||||
"eslint": "^9.26.0",
|
"eslint": "^9.26.0",
|
||||||
"eslint-plugin-import": "^2.32.0",
|
"eslint-plugin-import": "^2.32.0",
|
||||||
"fetch-mock": "^12.5.4",
|
|
||||||
"jest": "^30.0.0",
|
"jest": "^30.0.0",
|
||||||
"nodemon": "^3.1.10",
|
"nodemon": "^3.1.10",
|
||||||
"ts-jest": "^29.3.2",
|
"ts-jest": "^29.3.2",
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,7 @@ export class ConfigureController {
|
||||||
}
|
}
|
||||||
|
|
||||||
private getConfigure(req: Request, res: Response) {
|
private getConfigure(req: Request, res: Response) {
|
||||||
const config: Config = JSON.parse(req.params['config'] || JSON.stringify(getDefaultConfig()));
|
const config: Config = JSON.parse(req.params['config'] as string || JSON.stringify(getDefaultConfig()));
|
||||||
|
|
||||||
// Convenience preset for ElfHosted WebStreamr bundle including Media Flow Proxy
|
// Convenience preset for ElfHosted WebStreamr bundle including Media Flow Proxy
|
||||||
if (!req.params['config'] && isElfHostedInstance(req)) {
|
if (!req.params['config'] && isElfHostedInstance(req)) {
|
||||||
|
|
|
||||||
53
src/controller/ExtractController.ts
Normal file
53
src/controller/ExtractController.ts
Normal file
|
|
@ -0,0 +1,53 @@
|
||||||
|
import { Mutex } from 'async-mutex';
|
||||||
|
import { Request, Response, Router } from 'express';
|
||||||
|
import winston from 'winston';
|
||||||
|
import { ExtractorRegistry } from '../extractor';
|
||||||
|
import { contextFromRequestAndResponse, Fetcher } from '../utils';
|
||||||
|
|
||||||
|
export class ExtractController {
|
||||||
|
public readonly router: Router;
|
||||||
|
|
||||||
|
private readonly logger: winston.Logger;
|
||||||
|
private readonly extractorRegistry: ExtractorRegistry;
|
||||||
|
|
||||||
|
private readonly locks = new Map<string, Mutex>();
|
||||||
|
|
||||||
|
public constructor(logger: winston.Logger, _fetcher: Fetcher, extractorRegistry: ExtractorRegistry) {
|
||||||
|
this.router = Router();
|
||||||
|
|
||||||
|
this.logger = logger;
|
||||||
|
this.extractorRegistry = extractorRegistry;
|
||||||
|
|
||||||
|
this.router.get('/extract', this.extract.bind(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
private async extract(req: Request, res: Response) {
|
||||||
|
if (req.method !== 'GET') {
|
||||||
|
res.status(405).send('Method Not Allowed');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ctx = contextFromRequestAndResponse(req, res);
|
||||||
|
|
||||||
|
const index = parseInt(req.query['index'] as string);
|
||||||
|
const url = new URL(req.query['url'] as string);
|
||||||
|
|
||||||
|
this.logger.info(`Lazy extract index ${index} of URL ${url} for ip ${ctx.ip}`, ctx);
|
||||||
|
|
||||||
|
let mutex = this.locks.get(url.href);
|
||||||
|
if (!mutex) {
|
||||||
|
mutex = new Mutex();
|
||||||
|
this.locks.set(url.href, mutex);
|
||||||
|
}
|
||||||
|
|
||||||
|
await mutex.runExclusive(async () => {
|
||||||
|
const urlResults = await this.extractorRegistry.handle(ctx, url);
|
||||||
|
|
||||||
|
res.redirect(urlResults[index]?.url.href as string);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!mutex.isLocked()) {
|
||||||
|
this.locks.delete(url.href);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -21,7 +21,7 @@ export class ManifestController {
|
||||||
}
|
}
|
||||||
|
|
||||||
private getManifest(req: Request, res: Response) {
|
private getManifest(req: Request, res: Response) {
|
||||||
const config: Config = JSON.parse(req.params['config'] || JSON.stringify(getDefaultConfig()));
|
const config: Config = JSON.parse(req.params['config'] as string || JSON.stringify(getDefaultConfig()));
|
||||||
|
|
||||||
const manifest = buildManifest(this.sources, this.extractors, config);
|
const manifest = buildManifest(this.sources, this.extractors, config);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@ export class StreamController {
|
||||||
|
|
||||||
private async getStream(req: Request, res: Response) {
|
private async getStream(req: Request, res: Response) {
|
||||||
const type: ContentType = (req.params['type'] || '') as ContentType;
|
const type: ContentType = (req.params['type'] || '') as ContentType;
|
||||||
const rawId: string = req.params['id'] || '';
|
const rawId: string = req.params['id'] as string || '';
|
||||||
|
|
||||||
let id: Id;
|
let id: Id;
|
||||||
if (rawId.startsWith('tmdb:')) {
|
if (rawId.startsWith('tmdb:')) {
|
||||||
|
|
@ -35,7 +35,9 @@ export class StreamController {
|
||||||
} else if (rawId.startsWith('tt')) {
|
} else if (rawId.startsWith('tt')) {
|
||||||
id = ImdbId.fromString(rawId);
|
id = ImdbId.fromString(rawId);
|
||||||
} else {
|
} else {
|
||||||
throw new Error(`Unsupported ID: ${rawId}`);
|
res.status(400).send(`Unsupported ID: ${rawId}`);
|
||||||
|
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ctx = contextFromRequestAndResponse(req, res);
|
const ctx = contextFromRequestAndResponse(req, res);
|
||||||
|
|
@ -54,7 +56,7 @@ export class StreamController {
|
||||||
const { streams, ttl } = await this.streamResolver.resolve(ctx, sources, type, id);
|
const { streams, ttl } = await this.streamResolver.resolve(ctx, sources, type, id);
|
||||||
|
|
||||||
if (ttl && envIsProd()) {
|
if (ttl && envIsProd()) {
|
||||||
res.setHeader('Cache-Control', `max-age=${Math.floor(ttl / 1000)}, public`);
|
res.setHeader('Cache-Control', `public, max-age=${Math.floor(ttl / 1000)}, immutable`);
|
||||||
}
|
}
|
||||||
|
|
||||||
res.setHeader('Content-Type', 'application/json');
|
res.setHeader('Content-Type', 'application/json');
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
export * from './ExtractController';
|
||||||
export * from './ConfigureController';
|
export * from './ConfigureController';
|
||||||
export * from './ManifestController';
|
export * from './ManifestController';
|
||||||
export * from './StreamController';
|
export * from './StreamController';
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,15 @@
|
||||||
|
import { AxiosResponse } from 'axios';
|
||||||
import { BlockedReason } from '../types';
|
import { BlockedReason } from '../types';
|
||||||
|
|
||||||
export class BlockedError extends Error {
|
export class BlockedError extends Error {
|
||||||
|
public readonly url: URL;
|
||||||
public readonly reason: BlockedReason;
|
public readonly reason: BlockedReason;
|
||||||
public readonly headers: Record<string, string[] | string | undefined>;
|
public readonly headers: AxiosResponse['headers'];
|
||||||
|
|
||||||
public constructor(reason: BlockedReason, headers: Record<string, string[] | string | undefined>) {
|
public constructor(url: URL, reason: BlockedReason, headers: AxiosResponse['headers']) {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
|
this.url = url;
|
||||||
this.reason = reason;
|
this.reason = reason;
|
||||||
this.headers = headers;
|
this.headers = headers;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,15 @@
|
||||||
|
import { AxiosResponse } from 'axios';
|
||||||
|
|
||||||
export class HttpError extends Error {
|
export class HttpError extends Error {
|
||||||
|
public readonly url: URL;
|
||||||
public readonly status: number;
|
public readonly status: number;
|
||||||
public readonly statusText: string;
|
public readonly statusText: string;
|
||||||
public readonly headers: Record<string, string[] | string | undefined>;
|
public readonly headers: AxiosResponse['headers'];
|
||||||
|
|
||||||
public constructor(status: number, statusText: string, headers: Record<string, string[] | string | undefined>) {
|
public constructor(url: URL, status: number, statusText: string, headers: AxiosResponse['headers']) {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
|
this.url = url;
|
||||||
this.status = status;
|
this.status = status;
|
||||||
this.statusText = statusText;
|
this.statusText = statusText;
|
||||||
this.headers = headers;
|
this.headers = headers;
|
||||||
|
|
|
||||||
|
|
@ -1 +1,9 @@
|
||||||
export class QueueIsFullError extends Error {}
|
export class QueueIsFullError extends Error {
|
||||||
|
public readonly url: URL;
|
||||||
|
|
||||||
|
public constructor(url: URL) {
|
||||||
|
super();
|
||||||
|
|
||||||
|
this.url = url;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1 +1,9 @@
|
||||||
export class TimeoutError extends Error {}
|
export class TimeoutError extends Error {
|
||||||
|
public readonly url: URL;
|
||||||
|
|
||||||
|
public constructor(url: URL) {
|
||||||
|
super();
|
||||||
|
|
||||||
|
this.url = url;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,11 @@
|
||||||
export class TooManyRequestsError extends Error {
|
export class TooManyRequestsError extends Error {
|
||||||
|
public readonly url: URL;
|
||||||
public readonly retryAfter: number;
|
public readonly retryAfter: number;
|
||||||
|
|
||||||
public constructor(retryAfter: number) {
|
public constructor(url: URL, retryAfter: number) {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
|
this.url = url;
|
||||||
this.retryAfter = retryAfter;
|
this.retryAfter = retryAfter;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1 +1,9 @@
|
||||||
export class TooManyTimeoutsError extends Error {}
|
export class TooManyTimeoutsError extends Error {
|
||||||
|
public readonly url: URL;
|
||||||
|
|
||||||
|
public constructor(url: URL) {
|
||||||
|
super();
|
||||||
|
|
||||||
|
this.url = url;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,54 +17,54 @@ export * from './TooManyTimeoutsError';
|
||||||
|
|
||||||
export const logErrorAndReturnNiceString = (ctx: Context, logger: winston.Logger, source: string, error: unknown): string => {
|
export const logErrorAndReturnNiceString = (ctx: Context, logger: winston.Logger, source: string, error: unknown): string => {
|
||||||
if (error instanceof BlockedError) {
|
if (error instanceof BlockedError) {
|
||||||
if (error.reason === BlockedReason.cloudflare_challenge) {
|
if (error.reason === BlockedReason.media_flow_proxy_auth) {
|
||||||
logger.warn(`${source}: Request was blocked via Cloudflare challenge.`, ctx);
|
|
||||||
} else if (error.reason === BlockedReason.cloudflare_censor) {
|
|
||||||
logger.warn(`${source}: Request was censored by Cloudflare.`, ctx);
|
|
||||||
} else if (error.reason === BlockedReason.media_flow_proxy_auth) {
|
|
||||||
return '⚠️ MediaFlow Proxy authentication failed. Please set the correct password.';
|
return '⚠️ MediaFlow Proxy authentication failed. Please set the correct password.';
|
||||||
} else {
|
|
||||||
logger.warn(`${source}: Request was blocked, headers: ${JSON.stringify(error.headers)}.`, ctx);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return `⚠️ Request was blocked. Reason: ${error.reason}`;
|
logger.warn(`${source}: Request to ${error.url} was blocked, reason: ${error.reason}, headers: ${JSON.stringify(error.headers)}.`, ctx);
|
||||||
|
|
||||||
|
return `⚠️ Request to ${error.url.host} was blocked. Reason: ${error.reason}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (error instanceof TooManyRequestsError) {
|
if (error instanceof TooManyRequestsError) {
|
||||||
logger.warn(`${source}: Rate limited for ${error.retryAfter} seconds.`, ctx);
|
logger.warn(`${source}: Request to ${error.url} was rate limited for ${error.retryAfter} seconds.`, ctx);
|
||||||
|
|
||||||
return '🚦 Request was rate-limited. Please try again later or consider self-hosting.';
|
return `🚦 Request to ${error.url.host} was rate-limited. Please try again later or consider self-hosting.`;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (error instanceof TooManyTimeoutsError) {
|
if (error instanceof TooManyTimeoutsError) {
|
||||||
logger.warn(`${source}: Too many timeouts.`, ctx);
|
logger.warn(`${source}: Too many timeouts when requesting ${error.url}.`, ctx);
|
||||||
|
|
||||||
return '🚦 Too many recent timeouts. Please try again later.';
|
return `🚦 Too many recent timeouts when requesting ${error.url.host}. Please try again later.`;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (error instanceof TimeoutError) {
|
||||||
error instanceof TimeoutError
|
logger.warn(`${source}: Request to ${error.url} timed out.`, ctx);
|
||||||
|| (error instanceof DOMException && ['AbortError', 'TimeoutError'].includes(error.name)) // sometimes this gets through, no idea why..
|
|
||||||
) {
|
return `🐢 Request to ${error.url.host} timed out.`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error instanceof DOMException && ['AbortError', 'TimeoutError'].includes(error.name)) {
|
||||||
|
// sometimes this gets through, no idea why..
|
||||||
logger.warn(`${source}: Request timed out.`, ctx);
|
logger.warn(`${source}: Request timed out.`, ctx);
|
||||||
|
|
||||||
return '🐢 Request timed out.';
|
return '🐢 Request timed out.';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (error instanceof QueueIsFullError) {
|
if (error instanceof QueueIsFullError) {
|
||||||
logger.warn(`${source}: Request queue is full.`, ctx);
|
logger.warn(`${source}: Request queue for ${error.url.host} is full.`, ctx);
|
||||||
|
|
||||||
return '⏳ Request queue is full. Please try again later or consider self-hosting.';
|
return `⏳ Request queue for ${error.url.host} is full. Please try again later or consider self-hosting.`;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (error instanceof HttpError) {
|
if (error instanceof HttpError) {
|
||||||
logger.error(`${source}: HTTP status ${error.status} (${error.statusText}), headers: ${JSON.stringify(error.headers)}, stack: ${error.stack}.`, ctx);
|
logger.error(`${source}: Error when requesting url ${error.url}, HTTP status ${error.status} (${error.statusText}), headers: ${JSON.stringify(error.headers)}, stack: ${error.stack}.`, ctx);
|
||||||
|
|
||||||
if (error.status >= 500) {
|
if (error.status >= 500) {
|
||||||
return `❌ Remote server has issues. We can't fix this, please try later again.`;
|
return `❌ Remote server ${error.url.host} has issues. We can't fix this, please try later again.`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return `❌ Request failed with status ${error.status} (${error.statusText}). Request-id: ${ctx.id}.`;
|
return `❌ Request to ${error.url.host} failed with status ${error.status} (${error.statusText}). Request-id: ${ctx.id}.`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const cause = (error as Error & { cause?: unknown }).cause;
|
const cause = (error as Error & { cause?: unknown }).cause;
|
||||||
|
|
|
||||||
|
|
@ -7,26 +7,18 @@ import { ExtractorRegistry } from './ExtractorRegistry';
|
||||||
const logger = winston.createLogger({ transports: [new winston.transports.Console({ level: 'nope' })] });
|
const logger = winston.createLogger({ transports: [new winston.transports.Console({ level: 'nope' })] });
|
||||||
const extractorRegistry = new ExtractorRegistry(logger, [new DoodStream(new FetcherMock(`${__dirname}/__fixtures__/DoodStream`))]);
|
const extractorRegistry = new ExtractorRegistry(logger, [new DoodStream(new FetcherMock(`${__dirname}/__fixtures__/DoodStream`))]);
|
||||||
|
|
||||||
const ctx = createTestContext();
|
const ctx = createTestContext({ mediaFlowProxyUrl: 'https://mediaflow.test.org', mediaFlowProxyPassword: 'test' });
|
||||||
|
|
||||||
describe('DoodStream', () => {
|
describe('DoodStream', () => {
|
||||||
test('dood.to', async () => {
|
test('dood.to', async () => {
|
||||||
expect(await extractorRegistry.handle(ctx, new URL('http://dood.to/e/sk1m9eumzyjj'))).toMatchSnapshot();
|
expect(await extractorRegistry.handle(ctx, new URL('http://dood.to/e/sk1m9eumzyjj'))).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('doodster', async () => {
|
|
||||||
expect(await extractorRegistry.handle(ctx, new URL('https://dooodster.com/e/1cfcevn6dg8shrfvht22odxw2lty18hr'))).toMatchSnapshot();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('missing pass_md5 -> not found', async () => {
|
|
||||||
expect(await extractorRegistry.handle(ctx, new URL('https://dood.to/e/gy8l8mb2i311'))).toMatchSnapshot();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('can guess height from title', async () => {
|
test('can guess height from title', async () => {
|
||||||
expect(await extractorRegistry.handle(ctx, new URL('https://do7go.com/e/dfx8me4un4ul'))).toMatchSnapshot();
|
expect(await extractorRegistry.handle(ctx, new URL('https://do7go.com/e/dfx8me4un4ul'))).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('cloudflare storage', async () => {
|
test('not found', async () => {
|
||||||
expect(await extractorRegistry.handle(ctx, new URL('https://doodstream.com/e/wfpwtsgyr1xi'))).toMatchSnapshot();
|
expect(await extractorRegistry.handle(ctx, new URL('https://myvidplay.com/e/cz7cus0bvlzr'))).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
|
import bytes from 'bytes';
|
||||||
import * as cheerio from 'cheerio';
|
import * as cheerio from 'cheerio';
|
||||||
import randomstring from 'randomstring';
|
|
||||||
import { NotFoundError } from '../error';
|
import { NotFoundError } from '../error';
|
||||||
import { Context, Format, Meta, UrlResult } from '../types';
|
import { Context, Format, InternalUrlResult, Meta } from '../types';
|
||||||
import { guessSizeFromMp4 } from '../utils/size';
|
import { buildMediaFlowProxyExtractorRedirectUrl, supportsMediaFlowProxy } from '../utils';
|
||||||
import { Extractor } from './Extractor';
|
import { Extractor } from './Extractor';
|
||||||
|
|
||||||
export class DoodStream extends Extractor {
|
export class DoodStream extends Extractor {
|
||||||
|
|
@ -12,55 +12,42 @@ export class DoodStream extends Extractor {
|
||||||
|
|
||||||
public override readonly ttl: number = 21600000; // 6h
|
public override readonly ttl: number = 21600000; // 6h
|
||||||
|
|
||||||
|
public override viaMediaFlowProxy = true;
|
||||||
|
|
||||||
/** @see https://github.com/Gujal00/ResolveURL/blob/master/script.module.resolveurl/lib/resolveurl/plugins/doodstream.py */
|
/** @see https://github.com/Gujal00/ResolveURL/blob/master/script.module.resolveurl/lib/resolveurl/plugins/doodstream.py */
|
||||||
public supports(_ctx: Context, url: URL): boolean {
|
public supports(ctx: Context, url: URL): boolean {
|
||||||
return null !== url.host.match(/dood|do[0-9]go|doood|dooood|ds2play|ds2video|d0o0d|do0od|d0000d|d000d|vidply|all3do|doply|vide0|vvide0|d-s/);
|
return null !== url.host.match(/dood|do[0-9]go|doood|dooood|ds2play|ds2video|dsvplay|d0o0d|do0od|d0000d|d000d|myvidplay|vidply|all3do|doply|vide0|vvide0|d-s/) && supportsMediaFlowProxy(ctx);
|
||||||
};
|
};
|
||||||
|
|
||||||
public override normalize(url: URL): URL {
|
public override normalize(url: URL): URL {
|
||||||
const videoId = url.pathname.split('/').slice(-1)[0] as string;
|
const videoId = url.pathname.replace(/\/+$/, '').split('/').at(-1) as string;
|
||||||
|
|
||||||
return new URL(`http://dood.to/e/${videoId}`);
|
return new URL(`http://dood.to/e/${videoId}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
protected async extractInternal(ctx: Context, url: URL, meta: Meta): Promise<UrlResult[]> {
|
protected async extractInternal(ctx: Context, url: URL, meta: Meta): Promise<InternalUrlResult[]> {
|
||||||
const html = await this.fetcher.text(ctx, url);
|
const headers = { Referer: meta.referer ?? url.href };
|
||||||
|
|
||||||
const passMd5Match = html.match(/\/pass_md5\/[\w-]+\/([\w-]+)/);
|
const html = await this.fetcher.text(ctx, url, { headers });
|
||||||
if (!passMd5Match) {
|
|
||||||
|
if (/Video not found/.test(html)) {
|
||||||
throw new NotFoundError();
|
throw new NotFoundError();
|
||||||
}
|
}
|
||||||
|
|
||||||
const token = passMd5Match[1] as string;
|
|
||||||
|
|
||||||
const baseUrl = await this.fetcher.text(ctx, new URL(passMd5Match[0], url.origin));
|
|
||||||
|
|
||||||
const $ = cheerio.load(html);
|
const $ = cheerio.load(html);
|
||||||
const title = $('title').text().trim().replace(/ - DoodStream$/, '').trim();
|
const title = $('title').text().trim().replace(/ - DoodStream$/, '').trim();
|
||||||
|
|
||||||
let mp4Url: URL;
|
const downloadHtml = await this.fetcher.text(ctx, new URL(url.href.replace('/e/', '/d/')));
|
||||||
let bytes: number | undefined;
|
const sizeMatch = downloadHtml.match(/([\d.]+ ?[GM]B)/);
|
||||||
if (baseUrl.includes('cloudflarestorage')) {
|
|
||||||
mp4Url = new URL(baseUrl);
|
|
||||||
} else {
|
|
||||||
mp4Url = new URL(`${baseUrl}${randomstring.generate(10)}?token=${token}&expiry=${Date.now()}`);
|
|
||||||
bytes = await guessSizeFromMp4(ctx, this.fetcher, mp4Url, { headers: { Referer: url.origin } });
|
|
||||||
}
|
|
||||||
|
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
url: mp4Url,
|
url: buildMediaFlowProxyExtractorRedirectUrl(ctx, 'Doodstream', url, headers),
|
||||||
format: Format.mp4,
|
format: Format.mp4,
|
||||||
label: this.label,
|
|
||||||
sourceId: `${this.id}_${meta.countryCodes?.join('_')}`,
|
|
||||||
ttl: this.ttl,
|
|
||||||
meta: {
|
meta: {
|
||||||
...meta,
|
...meta,
|
||||||
title,
|
title,
|
||||||
...(bytes && { bytes }),
|
...(sizeMatch && { bytes: bytes.parse(sizeMatch[1] as string) as number }),
|
||||||
},
|
|
||||||
requestHeaders: {
|
|
||||||
Referer: url.origin,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,10 @@ describe('Dropload', () => {
|
||||||
expect(await extractorRegistry.handle(ctx, new URL('https://dropload.io/embed-lyo2h1snpe5c.html'))).toMatchSnapshot();
|
expect(await extractorRegistry.handle(ctx, new URL('https://dropload.io/embed-lyo2h1snpe5c.html'))).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('dropload unknown height and size', async () => {
|
||||||
|
expect(await extractorRegistry.handle(ctx, new URL('https://dropload.io/gf2apij3ql37'))).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
test('file not found', async () => {
|
test('file not found', async () => {
|
||||||
expect(await extractorRegistry.handle(ctx, new URL('https://dropload.io/asdfghijklmn.html'))).toMatchSnapshot();
|
expect(await extractorRegistry.handle(ctx, new URL('https://dropload.io/asdfghijklmn.html'))).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
import bytes from 'bytes';
|
import bytes from 'bytes';
|
||||||
import * as cheerio from 'cheerio';
|
import * as cheerio from 'cheerio';
|
||||||
import { NotFoundError } from '../error';
|
import { NotFoundError } from '../error';
|
||||||
import { Context, Format, Meta, UrlResult } from '../types';
|
import { Context, Format, InternalUrlResult, Meta } from '../types';
|
||||||
import { extractUrlFromPacked } from '../utils';
|
import { extractUrlFromPacked, guessHeightFromPlaylist } from '../utils';
|
||||||
import { Extractor } from './Extractor';
|
import { Extractor } from './Extractor';
|
||||||
|
|
||||||
export class Dropload extends Extractor {
|
export class Dropload extends Extractor {
|
||||||
|
|
@ -13,38 +13,45 @@ export class Dropload extends Extractor {
|
||||||
public override readonly ttl: number = 10800000; // 3h
|
public override readonly ttl: number = 10800000; // 3h
|
||||||
|
|
||||||
public supports(_ctx: Context, url: URL): boolean {
|
public supports(_ctx: Context, url: URL): boolean {
|
||||||
return null !== url.host.match(/dropload/);
|
return null !== url.host.match(/dropload|dr0pstream/);
|
||||||
}
|
}
|
||||||
|
|
||||||
public override readonly normalize = (url: URL): URL => new URL(url.href.replace('/d/', '/').replace('/e/', '/').replace('/embed-', '/'));
|
public override readonly normalize = (url: URL): URL => new URL(url.href.replace('/d/', '/').replace('/e/', '/').replace('/embed-', '/'));
|
||||||
|
|
||||||
protected async extractInternal(ctx: Context, url: URL, meta: Meta): Promise<UrlResult[]> {
|
protected async extractInternal(ctx: Context, url: URL, meta: Meta): Promise<InternalUrlResult[]> {
|
||||||
const html = await this.fetcher.text(ctx, url);
|
const headers = { Referer: meta.referer ?? url.href };
|
||||||
|
|
||||||
|
const html = await this.fetcher.text(ctx, url, { headers });
|
||||||
|
|
||||||
if (html.includes('File Not Found') || html.includes('Pending in queue')) {
|
if (html.includes('File Not Found') || html.includes('Pending in queue')) {
|
||||||
throw new NotFoundError();
|
throw new NotFoundError();
|
||||||
}
|
}
|
||||||
|
|
||||||
const heightMatch = html.match(/\d{3,}x(\d{3,}),/) as string[];
|
const playlistUrl = extractUrlFromPacked(html, [/sources:\[{file:"(.*?)"/]);
|
||||||
|
const playlistHeaders = { Referer: 'https://dr0pstream.com/' };
|
||||||
|
|
||||||
const sizeMatch = html.match(/([\d.]+ ?[GM]B)/) as string[];
|
const heightMatch = html.match(/\d{3,}x(\d{3,}),/);
|
||||||
|
const height = heightMatch
|
||||||
|
? parseInt(heightMatch[1] as string)
|
||||||
|
: meta.height ?? await guessHeightFromPlaylist(ctx, this.fetcher, playlistUrl, { headers: playlistHeaders });
|
||||||
|
|
||||||
|
const sizeMatch = html.match(/([\d.]+ ?[GM]B)/);
|
||||||
|
const size = sizeMatch ? bytes.parse(sizeMatch[1] as string) as number : undefined;
|
||||||
|
|
||||||
const $ = cheerio.load(html);
|
const $ = cheerio.load(html);
|
||||||
const title = $('.videoplayer h1').text().trim();
|
const title = $('.videoplayer h1').text().trim();
|
||||||
|
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
url: extractUrlFromPacked(html, [/sources:\[{file:"(.*?)"/]),
|
url: playlistUrl,
|
||||||
format: Format.hls,
|
format: Format.hls,
|
||||||
label: this.label,
|
|
||||||
sourceId: `${this.id}_${meta.countryCodes?.join('_')}`,
|
|
||||||
ttl: this.ttl,
|
|
||||||
meta: {
|
meta: {
|
||||||
...meta,
|
...meta,
|
||||||
bytes: bytes.parse(sizeMatch[1] as string) as number,
|
|
||||||
height: parseInt(heightMatch[1] as string),
|
|
||||||
title,
|
title,
|
||||||
|
...(size && { bytes: size }),
|
||||||
|
...(height && { height }),
|
||||||
},
|
},
|
||||||
|
requestHeaders: playlistHeaders,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
import { BlockedError } from '../error';
|
import { Context, Format, InternalUrlResult, Meta } from '../types';
|
||||||
import { Context, Format, Meta, UrlResult } from '../types';
|
|
||||||
import { showExternalUrls } from '../utils';
|
import { showExternalUrls } from '../utils';
|
||||||
import { Extractor } from './Extractor';
|
import { Extractor } from './Extractor';
|
||||||
|
|
||||||
|
|
@ -14,25 +13,13 @@ export class ExternalUrl extends Extractor {
|
||||||
return showExternalUrls(ctx.config) && null !== url.host.match(/.*/);
|
return showExternalUrls(ctx.config) && null !== url.host.match(/.*/);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async extractInternal(ctx: Context, url: URL, meta: Meta): Promise<UrlResult[]> {
|
protected async extractInternal(_ctx: Context, url: URL, meta: Meta): Promise<InternalUrlResult[]> {
|
||||||
try {
|
|
||||||
// Make sure the URL is accessible, but avoid causing noise and delays doing this
|
|
||||||
await this.fetcher.head(ctx, url, { noFlareSolverr: true, timeout: 1000, headers: { Referer: url.origin } });
|
|
||||||
} catch (error) {
|
|
||||||
/* istanbul ignore if */
|
|
||||||
if (!(error instanceof BlockedError)) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
url: url,
|
url: url,
|
||||||
format: Format.unknown,
|
format: Format.unknown,
|
||||||
isExternal: true,
|
isExternal: true,
|
||||||
label: `${url.host}`,
|
label: `${url.host}`,
|
||||||
sourceId: `${this.id}_${meta.countryCodes?.join('_')}`,
|
|
||||||
ttl: this.ttl,
|
|
||||||
meta,
|
meta,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { NotFoundError } from '../error';
|
import { NotFoundError } from '../error';
|
||||||
import { Context, Format, Meta, UrlResult } from '../types';
|
import { Context, Format, InternalUrlResult, Meta, UrlResult } from '../types';
|
||||||
import { Fetcher } from '../utils';
|
import { Fetcher } from '../utils';
|
||||||
|
|
||||||
export abstract class Extractor {
|
export abstract class Extractor {
|
||||||
|
|
@ -9,6 +9,8 @@ export abstract class Extractor {
|
||||||
|
|
||||||
public readonly ttl: number = 900000; // 15m
|
public readonly ttl: number = 900000; // 15m
|
||||||
|
|
||||||
|
public readonly cacheVersion: number | undefined = undefined;
|
||||||
|
|
||||||
public readonly viaMediaFlowProxy: boolean = false;
|
public readonly viaMediaFlowProxy: boolean = false;
|
||||||
|
|
||||||
protected readonly fetcher: Fetcher;
|
protected readonly fetcher: Fetcher;
|
||||||
|
|
@ -23,11 +25,18 @@ export abstract class Extractor {
|
||||||
return url;
|
return url;
|
||||||
};
|
};
|
||||||
|
|
||||||
protected abstract extractInternal(ctx: Context, url: URL, meta: Meta): Promise<UrlResult[]>;
|
protected abstract extractInternal(ctx: Context, url: URL, meta: Meta): Promise<InternalUrlResult[]>;
|
||||||
|
|
||||||
public async extract(ctx: Context, url: URL, meta: Meta): Promise<UrlResult[]> {
|
public async extract(ctx: Context, url: URL, meta: Meta): Promise<UrlResult[]> {
|
||||||
try {
|
try {
|
||||||
return await this.extractInternal(ctx, url, meta);
|
return (await this.extractInternal(ctx, url, meta)).map(
|
||||||
|
urlResult => ({
|
||||||
|
...urlResult,
|
||||||
|
label: this.formatLabel(urlResult.label ?? this.label),
|
||||||
|
ttl: this.ttl,
|
||||||
|
|
||||||
|
}),
|
||||||
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof NotFoundError) {
|
if (error instanceof NotFoundError) {
|
||||||
return [];
|
return [];
|
||||||
|
|
@ -39,11 +48,15 @@ export abstract class Extractor {
|
||||||
format: Format.unknown,
|
format: Format.unknown,
|
||||||
isExternal: true,
|
isExternal: true,
|
||||||
error,
|
error,
|
||||||
label: this.label,
|
label: this.formatLabel(this.label),
|
||||||
sourceId: `${this.id}`,
|
ttl: 0,
|
||||||
...(meta && meta),
|
meta,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private formatLabel(label: string): string {
|
||||||
|
return this.viaMediaFlowProxy ? `${label} (MFP)` : label;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -49,6 +49,11 @@ describe('ExtractorRegistry', () => {
|
||||||
expect(urlResults).toMatchSnapshot();
|
expect(urlResults).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('empty results are cached', async () => {
|
||||||
|
const urlResults = await extractorRegistry.handle(ctx, new URL('https://dropload.io/asdfghijklmn.html'), { title: 'title' });
|
||||||
|
expect(urlResults).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
test('stats returns something', async () => {
|
test('stats returns something', async () => {
|
||||||
const stats = extractorRegistry.stats();
|
const stats = extractorRegistry.stats();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,23 +1,31 @@
|
||||||
// eslint-disable-next-line import/no-named-as-default
|
|
||||||
import KeyvSqlite from '@keyv/sqlite';
|
|
||||||
import { Cacheable, CacheableMemory, Keyv } from 'cacheable';
|
import { Cacheable, CacheableMemory, Keyv } from 'cacheable';
|
||||||
import winston from 'winston';
|
import winston from 'winston';
|
||||||
import { Context, Meta, UrlResult } from '../types';
|
import { Context, Format, Meta, UrlResult } from '../types';
|
||||||
import { getCacheDir, isExtractorDisabled } from '../utils';
|
import { createKeyvSqlite, envGet, isExtractorDisabled } from '../utils';
|
||||||
import { Extractor } from './Extractor';
|
import { Extractor } from './Extractor';
|
||||||
|
|
||||||
export class ExtractorRegistry {
|
export class ExtractorRegistry {
|
||||||
private readonly logger: winston.Logger;
|
private readonly logger: winston.Logger;
|
||||||
private readonly extractors: Extractor[];
|
private readonly extractors: Extractor[];
|
||||||
|
|
||||||
private readonly urlResultCache: Cacheable;
|
private readonly urlResultCache: Cacheable;
|
||||||
|
private readonly lazyUrlResultCache: Cacheable;
|
||||||
|
|
||||||
public constructor(logger: winston.Logger, extractors: Extractor[]) {
|
public constructor(logger: winston.Logger, extractors: Extractor[]) {
|
||||||
this.logger = logger;
|
this.logger = logger;
|
||||||
this.extractors = extractors;
|
this.extractors = extractors;
|
||||||
|
|
||||||
this.urlResultCache = new Cacheable({
|
this.urlResultCache = new Cacheable({
|
||||||
|
nonBlocking: true,
|
||||||
primary: new Keyv({ store: new CacheableMemory({ lruSize: 1024 }) }),
|
primary: new Keyv({ store: new CacheableMemory({ lruSize: 1024 }) }),
|
||||||
secondary: new Keyv(new KeyvSqlite(`sqlite://${getCacheDir()}/webstreamr-extractor-cache.sqlite`)),
|
secondary: createKeyvSqlite('extractor-cache'),
|
||||||
|
stats: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.lazyUrlResultCache = new Cacheable({
|
||||||
|
nonBlocking: true,
|
||||||
|
primary: new Keyv({ store: new CacheableMemory({ lruSize: 1024 }) }),
|
||||||
|
secondary: createKeyvSqlite('extractor-lazy-cache'),
|
||||||
stats: true,
|
stats: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -25,45 +33,80 @@ export class ExtractorRegistry {
|
||||||
public stats() {
|
public stats() {
|
||||||
return {
|
return {
|
||||||
urlResultCache: this.urlResultCache.stats,
|
urlResultCache: this.urlResultCache.stats,
|
||||||
|
lazyUrlResultCache: this.lazyUrlResultCache.stats,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
public async handle(ctx: Context, url: URL, meta?: Meta): Promise<UrlResult[]> {
|
public async handle(ctx: Context, url: URL, meta?: Meta, allowLazy?: boolean): Promise<UrlResult[]> {
|
||||||
const extractor = this.extractors.find(extractor => !isExtractorDisabled(ctx.config, extractor) && extractor.supports(ctx, url));
|
const extractor = this.extractors.find(extractor => !isExtractorDisabled(ctx.config, extractor) && extractor.supports(ctx, url));
|
||||||
if (!extractor) {
|
if (!extractor) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
const normalizedUrl = extractor.normalize(url);
|
const normalizedUrl = extractor.normalize(url);
|
||||||
const cacheKey = extractor.viaMediaFlowProxy
|
const cacheKey = this.determineCacheKey(ctx, extractor, normalizedUrl);
|
||||||
? `${extractor.id}_${normalizedUrl}_${ctx.config.mediaFlowProxyUrl}`
|
|
||||||
: `${extractor.id}_${normalizedUrl}`;
|
|
||||||
|
|
||||||
const storedDataRaw = await this.urlResultCache.getRaw<UrlResult[]>(cacheKey);
|
const storedDataRaw = await this.urlResultCache.getRaw<UrlResult[]>(cacheKey);
|
||||||
const expires = storedDataRaw?.expires;
|
const expires = storedDataRaw?.expires;
|
||||||
if (storedDataRaw && expires) {
|
if (storedDataRaw && expires) {
|
||||||
// Ignore the cache randomly after at least 2/3 of the TTL passed to start refreshing results slowly
|
const ttl = expires - Date.now();
|
||||||
const refreshTimestamp = this.randomInteger(expires - extractor.ttl * (2 / 3), expires);
|
|
||||||
const now = Date.now();
|
|
||||||
|
|
||||||
/* istanbul ignore if */
|
/* istanbul ignore if */
|
||||||
if (refreshTimestamp > now) {
|
if (ttl > 0) {
|
||||||
return (storedDataRaw.value as UrlResult[]).map(urlResult => ({ ...urlResult, ttl: expires - now, url: new URL(urlResult.url) }));
|
return (storedDataRaw.value as UrlResult[]).map(urlResult => ({ ...urlResult, ttl, url: new URL(urlResult.url) }));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.logger.info(`Extract stream URL using ${extractor.id} extractor from ${url}`, ctx);
|
const lazyUrlResults = await this.lazyUrlResultCache.get<UrlResult[]>(normalizedUrl.href) ?? [];
|
||||||
|
|
||||||
const urlResults = await extractor.extract(ctx, normalizedUrl, { ...meta, countryCodes: meta?.countryCodes ?? [] });
|
/* istanbul ignore next */
|
||||||
|
if (
|
||||||
|
lazyUrlResults.length && allowLazy && !extractor.viaMediaFlowProxy
|
||||||
|
&& lazyUrlResults.every(urlResult => urlResult.format !== Format.hls) // related to Android issues, e.g. https://github.com/Stremio/stremio-bugs/issues/1574 or https://github.com/Stremio/stremio-bugs/issues/1579
|
||||||
|
) {
|
||||||
|
// generate lazy extract urls
|
||||||
|
return lazyUrlResults.map((urlResult, index) => {
|
||||||
|
const extractUrl = new URL(`${envGet('PROTOCOL')}:${envGet('HOST')}/extract/`);
|
||||||
|
|
||||||
if (!urlResults.some(urlResult => urlResult.error) && extractor.ttl) {
|
extractUrl.searchParams.set('index', `${index}`);
|
||||||
await this.urlResultCache.set<UrlResult[]>(cacheKey, urlResults, extractor.ttl);
|
extractUrl.searchParams.set('url', url.href);
|
||||||
|
|
||||||
|
return { ...urlResult, url: extractUrl };
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
this.logger.info(`Extract ${url} using ${extractor.id} extractor`, ctx);
|
||||||
|
|
||||||
|
const mergedMeta: Meta = { ...meta, ...lazyUrlResults[0]?.meta };
|
||||||
|
const urlResults = await extractor.extract(ctx, normalizedUrl, { extractorId: extractor.id, ...mergedMeta });
|
||||||
|
|
||||||
|
if (!Object.keys(mergedMeta).length || urlResults.some(urlResult => urlResult.error)) {
|
||||||
|
await this.urlResultCache.delete(cacheKey);
|
||||||
|
await this.lazyUrlResultCache.delete(normalizedUrl.href);
|
||||||
|
|
||||||
|
return urlResults;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ttl = urlResults.length ? extractor.ttl : 43200000; // 12h
|
||||||
|
|
||||||
|
await this.urlResultCache.set<UrlResult[]>(cacheKey, urlResults, ttl);
|
||||||
|
|
||||||
|
if (extractor.id !== 'external') {
|
||||||
|
await this.lazyUrlResultCache.set<UrlResult[]>(normalizedUrl.href, urlResults, 2629800000); // 1 month
|
||||||
}
|
}
|
||||||
|
|
||||||
return urlResults;
|
return urlResults;
|
||||||
};
|
};
|
||||||
|
|
||||||
private randomInteger(min: number, max: number) {
|
private determineCacheKey(ctx: Context, extractor: Extractor, url: URL): string {
|
||||||
return Math.floor(Math.random() * (max - min + 1)) + min;
|
let suffix = '';
|
||||||
|
if (extractor.viaMediaFlowProxy) {
|
||||||
|
suffix += `_${ctx.config.mediaFlowProxyUrl}`;
|
||||||
|
}
|
||||||
|
if (extractor.cacheVersion) {
|
||||||
|
suffix += `_${extractor.cacheVersion}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${extractor.id}_${url}${suffix}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,4 +13,8 @@ describe('Fastream', () => {
|
||||||
test('fastream.to embed', async () => {
|
test('fastream.to embed', async () => {
|
||||||
expect(await extractorRegistry.handle(ctx, new URL('https://fastream.to/embed-3aooif4ozt10.html'))).toMatchSnapshot();
|
expect(await extractorRegistry.handle(ctx, new URL('https://fastream.to/embed-3aooif4ozt10.html'))).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('expired or deleted', async () => {
|
||||||
|
expect(await extractorRegistry.handle(ctx, new URL('https://fastream.to/embed-a9rp1d1pqom7.html'))).toMatchSnapshot();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,13 @@
|
||||||
import bytes from 'bytes';
|
import bytes from 'bytes';
|
||||||
import { Context, Format, Meta, UrlResult } from '../types';
|
import { NotFoundError } from '../error';
|
||||||
|
import { Context, Format, InternalUrlResult, Meta } from '../types';
|
||||||
import { buildMediaFlowProxyExtractorStreamUrl, supportsMediaFlowProxy } from '../utils';
|
import { buildMediaFlowProxyExtractorStreamUrl, supportsMediaFlowProxy } from '../utils';
|
||||||
import { Extractor } from './Extractor';
|
import { Extractor } from './Extractor';
|
||||||
|
|
||||||
export class Fastream extends Extractor {
|
export class Fastream extends Extractor {
|
||||||
public readonly id = 'fastream';
|
public readonly id = 'fastream';
|
||||||
|
|
||||||
public readonly label = 'Fastream (via MediaFlow Proxy)';
|
public readonly label = 'Fastream';
|
||||||
|
|
||||||
public override viaMediaFlowProxy = true;
|
public override viaMediaFlowProxy = true;
|
||||||
|
|
||||||
|
|
@ -18,11 +19,17 @@ export class Fastream extends Extractor {
|
||||||
return new URL(url.href.replace('/e/', '/embed-').replace('/d/', '/embed-'));
|
return new URL(url.href.replace('/e/', '/embed-').replace('/d/', '/embed-'));
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async extractInternal(ctx: Context, url: URL, meta: Meta): Promise<UrlResult[]> {
|
protected async extractInternal(ctx: Context, url: URL, meta: Meta): Promise<InternalUrlResult[]> {
|
||||||
const playlistUrl = await buildMediaFlowProxyExtractorStreamUrl(ctx, this.fetcher, 'Fastream', url);
|
const headers = { Referer: meta.referer ?? url.href };
|
||||||
|
|
||||||
const downloadUrl = new URL(url.href.replace('/embed-', '/d/'));
|
const downloadUrl = new URL(url.href.replace('/embed-', '/d/'));
|
||||||
const html = await this.fetcher.text(ctx, downloadUrl);
|
const html = await this.fetcher.text(ctx, downloadUrl, { headers });
|
||||||
|
|
||||||
|
if (/No such file/.test(html)) {
|
||||||
|
throw new NotFoundError();
|
||||||
|
}
|
||||||
|
|
||||||
|
const playlistUrl = await buildMediaFlowProxyExtractorStreamUrl(ctx, this.fetcher, 'Fastream', url, headers);
|
||||||
|
|
||||||
const heightAndSizeMatch = html.match(/\d{3,}x(\d{3,}), ([\d.]+ ?[GM]B)/) as string[];
|
const heightAndSizeMatch = html.match(/\d{3,}x(\d{3,}), ([\d.]+ ?[GM]B)/) as string[];
|
||||||
const titleMatch = html.match(/>Download (.*?)</) as string[];
|
const titleMatch = html.match(/>Download (.*?)</) as string[];
|
||||||
|
|
@ -31,9 +38,6 @@ export class Fastream extends Extractor {
|
||||||
{
|
{
|
||||||
url: playlistUrl,
|
url: playlistUrl,
|
||||||
format: Format.hls,
|
format: Format.hls,
|
||||||
label: this.label,
|
|
||||||
sourceId: `${this.id}_${meta.countryCodes?.join('_')}`,
|
|
||||||
ttl: this.ttl,
|
|
||||||
meta: {
|
meta: {
|
||||||
...meta,
|
...meta,
|
||||||
bytes: bytes.parse(heightAndSizeMatch[2] as string) as number,
|
bytes: bytes.parse(heightAndSizeMatch[2] as string) as number,
|
||||||
|
|
|
||||||
|
|
@ -17,4 +17,12 @@ describe('FileLions', () => {
|
||||||
test('mivalyo v referer lock', async () => {
|
test('mivalyo v referer lock', async () => {
|
||||||
expect(await extractorRegistry.handle(ctx, new URL('https://mivalyo.com/v/tah5znapz3e5'), { referer: 'https://kinoger.com' })).toMatchSnapshot();
|
expect(await extractorRegistry.handle(ctx, new URL('https://mivalyo.com/v/tah5znapz3e5'), { referer: 'https://kinoger.com' })).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('file not found', async () => {
|
||||||
|
expect(await extractorRegistry.handle(ctx, new URL('https://filelions.to/v/ylcp2cu5qanb'))).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('deleted by administration', async () => {
|
||||||
|
expect(await extractorRegistry.handle(ctx, new URL('https://callistanise.com/f/cy4t5nkerjrt'))).toMatchSnapshot();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,15 @@
|
||||||
import bytes from 'bytes';
|
import bytes from 'bytes';
|
||||||
import * as cheerio from 'cheerio';
|
import * as cheerio from 'cheerio';
|
||||||
import { Context, Format, Meta, UrlResult } from '../types';
|
import { NotFoundError } from '../error';
|
||||||
import { buildMediaFlowProxyExtractorStreamUrl, guessHeightFromPlaylist, supportsMediaFlowProxy } from '../utils';
|
import { Context, Format, InternalUrlResult, Meta } from '../types';
|
||||||
|
import { buildMediaFlowProxyExtractorStreamUrl, supportsMediaFlowProxy, unpackEval } from '../utils';
|
||||||
import { Extractor } from './Extractor';
|
import { Extractor } from './Extractor';
|
||||||
|
|
||||||
/** @see https://github.com/Gujal00/ResolveURL/commits/master/script.module.resolveurl/lib/resolveurl/plugins/filelions.py */
|
/** @see https://github.com/Gujal00/ResolveURL/commits/master/script.module.resolveurl/lib/resolveurl/plugins/filelions.py */
|
||||||
export class FileLions extends Extractor {
|
export class FileLions extends Extractor {
|
||||||
public readonly id = 'filelions';
|
public readonly id = 'filelions';
|
||||||
|
|
||||||
public readonly label = 'FileLions (via MediaFlow Proxy)';
|
public readonly label = 'FileLions';
|
||||||
|
|
||||||
public override readonly ttl = 0;
|
|
||||||
|
|
||||||
public override viaMediaFlowProxy = true;
|
public override viaMediaFlowProxy = true;
|
||||||
|
|
||||||
|
|
@ -22,16 +21,20 @@ export class FileLions extends Extractor {
|
||||||
'alhayabambi.sbs',
|
'alhayabambi.sbs',
|
||||||
'anime7u.com',
|
'anime7u.com',
|
||||||
'azipcdn.com',
|
'azipcdn.com',
|
||||||
|
'bingezove.com',
|
||||||
|
'callistanise.com',
|
||||||
'coolciima.online',
|
'coolciima.online',
|
||||||
'dhtpre.com',
|
'dhtpre.com',
|
||||||
|
'dingtezuni.com',
|
||||||
|
'dintezuvio.com',
|
||||||
'e4xb5c2xnz.sbs',
|
'e4xb5c2xnz.sbs',
|
||||||
'egsyxutd.sbs',
|
'egsyxutd.sbs',
|
||||||
'fdewsdc.sbs',
|
'fdewsdc.sbs',
|
||||||
'gsfomqu.sbs',
|
'gsfomqu.sbs',
|
||||||
'javplaya.com',
|
'javplaya.com',
|
||||||
'katomen.online',
|
'katomen.online',
|
||||||
'kinoger.be',
|
|
||||||
'lumiawatch.top',
|
'lumiawatch.top',
|
||||||
|
'minochinos.com',
|
||||||
'mivalyo.com',
|
'mivalyo.com',
|
||||||
'moflix-stream.click',
|
'moflix-stream.click',
|
||||||
'motvy55.store',
|
'motvy55.store',
|
||||||
|
|
@ -56,35 +59,38 @@ export class FileLions extends Extractor {
|
||||||
}
|
}
|
||||||
|
|
||||||
public override normalize(url: URL): URL {
|
public override normalize(url: URL): URL {
|
||||||
return new URL(url.href.replace('/f/', '/v/').replace('/download/', '/v/').replace('/file/', '/v/'));
|
return new URL(url.href.replace('/v/', '/f/').replace('/download/', '/f/').replace('/file/', '/f/'));
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async extractInternal(ctx: Context, url: URL, meta: Meta): Promise<UrlResult[]> {
|
protected async extractInternal(ctx: Context, url: URL, meta: Meta): Promise<InternalUrlResult[]> {
|
||||||
const headers = { ...(meta.referer && { Referer: meta.referer }) };
|
const headers = { Referer: meta.referer ?? url.href };
|
||||||
|
|
||||||
const playlistUrl = await buildMediaFlowProxyExtractorStreamUrl(ctx, this.fetcher, 'FileLions', url, headers);
|
const html = await this.fetcher.text(ctx, url, { headers });
|
||||||
|
|
||||||
const fileUrl = new URL(url.href.replace('/v/', '/f/'));
|
if (html.includes('This video can be watched as embed only')) {
|
||||||
const html = await this.fetcher.text(ctx, fileUrl, { headers });
|
return await this.extractInternal(ctx, new URL(url.href.replace('/f/', '/v/')), meta);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (/File Not Found|deleted by administration/.test(html)) {
|
||||||
|
throw new NotFoundError();
|
||||||
|
}
|
||||||
|
|
||||||
|
const unpacked = unpackEval(html);
|
||||||
|
const heightMatch = unpacked.match(/(\d{3,})p/) as string[];
|
||||||
|
|
||||||
const sizeMatch = html.match(/([\d.]+ ?[GM]B)/);
|
const sizeMatch = html.match(/([\d.]+ ?[GM]B)/);
|
||||||
|
|
||||||
const $ = cheerio.load(html);
|
const $ = cheerio.load(html);
|
||||||
const title = $('title').text().trim().replace(/^Watch /, '').trim();
|
const title = $('meta[name="description"]').attr('content') as string;
|
||||||
|
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
url: playlistUrl,
|
url: await buildMediaFlowProxyExtractorStreamUrl(ctx, this.fetcher, 'FileLions', url, headers),
|
||||||
format: Format.hls,
|
format: Format.hls,
|
||||||
label: this.label,
|
|
||||||
sourceId: `${this.id}_${meta.countryCodes?.join('_')}`,
|
|
||||||
ttl: this.ttl,
|
|
||||||
meta: {
|
meta: {
|
||||||
...meta,
|
...meta,
|
||||||
height: await guessHeightFromPlaylist(ctx, this.fetcher, playlistUrl),
|
height: parseInt(heightMatch[1] as string),
|
||||||
...(sizeMatch && {
|
...(sizeMatch && { bytes: bytes.parse(sizeMatch[1] as string) as number }),
|
||||||
bytes: bytes.parse(sizeMatch[1] as string) as number,
|
|
||||||
}),
|
|
||||||
...(title && { title }),
|
...(title && { title }),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
||||||
24
src/extractor/FileMoon.test.ts
Normal file
24
src/extractor/FileMoon.test.ts
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
import winston from 'winston';
|
||||||
|
import { createTestContext } from '../test';
|
||||||
|
import { FetcherMock } from '../utils';
|
||||||
|
import { ExtractorRegistry } from './ExtractorRegistry';
|
||||||
|
import { FileMoon } from './FileMoon';
|
||||||
|
|
||||||
|
const logger = winston.createLogger({ transports: [new winston.transports.Console({ level: 'nope' })] });
|
||||||
|
const extractorRegistry = new ExtractorRegistry(logger, [new FileMoon(new FetcherMock(`${__dirname}/__fixtures__/FileMoon`))]);
|
||||||
|
|
||||||
|
const ctx = createTestContext({ mediaFlowProxyUrl: 'https://mediaflow.test.org', mediaFlowProxyPassword: 'test' });
|
||||||
|
|
||||||
|
describe('FileMoon', () => {
|
||||||
|
test('z1ekv717 d', async () => {
|
||||||
|
expect(await extractorRegistry.handle(ctx, new URL('https://z1ekv717.fun/d/wkhcbggdxf1d'))).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('page not found', async () => {
|
||||||
|
expect(await extractorRegistry.handle(ctx, new URL('https://filemoon.sx/e/n7i8zodwjqr9'))).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('missing height', async () => {
|
||||||
|
expect(await extractorRegistry.handle(ctx, new URL('https://filemoon.to/d/ugj7f1kq94p1'))).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
});
|
||||||
82
src/extractor/FileMoon.ts
Normal file
82
src/extractor/FileMoon.ts
Normal file
|
|
@ -0,0 +1,82 @@
|
||||||
|
import * as cheerio from 'cheerio';
|
||||||
|
import { NotFoundError } from '../error';
|
||||||
|
import { Context, Format, InternalUrlResult, Meta } from '../types';
|
||||||
|
import {
|
||||||
|
buildMediaFlowProxyExtractorStreamUrl,
|
||||||
|
supportsMediaFlowProxy,
|
||||||
|
unpackEval,
|
||||||
|
} from '../utils';
|
||||||
|
import { Extractor } from './Extractor';
|
||||||
|
|
||||||
|
/** @see https://github.com/Gujal00/ResolveURL/blob/master/script.module.resolveurl/lib/resolveurl/plugins/filemoon.py */
|
||||||
|
export class FileMoon extends Extractor {
|
||||||
|
public readonly id = 'filemoon';
|
||||||
|
|
||||||
|
public readonly label = 'FileMoon';
|
||||||
|
|
||||||
|
public override viaMediaFlowProxy = true;
|
||||||
|
|
||||||
|
public supports(ctx: Context, url: URL): boolean {
|
||||||
|
const supportedDomain = null !== url.host.match(/filemoon/)
|
||||||
|
|| [
|
||||||
|
'1azayf9w.xyz',
|
||||||
|
'222i8x.lol',
|
||||||
|
'81u6xl9d.xyz',
|
||||||
|
'8mhlloqo.fun',
|
||||||
|
'96ar.com',
|
||||||
|
'bf0skv.org',
|
||||||
|
'boosteradx.online',
|
||||||
|
'c1z39.com',
|
||||||
|
'cinegrab.com',
|
||||||
|
'f51rm.com',
|
||||||
|
'furher.in',
|
||||||
|
'kerapoxy.cc',
|
||||||
|
'l1afav.net',
|
||||||
|
'moonmov.pro',
|
||||||
|
'smdfs40r.skin',
|
||||||
|
'xcoic.com',
|
||||||
|
'z1ekv717.fun',
|
||||||
|
].includes(url.host);
|
||||||
|
|
||||||
|
return supportedDomain && supportsMediaFlowProxy(ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override normalize(url: URL): URL {
|
||||||
|
return new URL(url.href.replace('/e/', '/d/'));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async extractInternal(ctx: Context, url: URL, meta: Meta, originalUrl?: URL): Promise<InternalUrlResult[]> {
|
||||||
|
const headers = { Referer: meta.referer ?? url.href };
|
||||||
|
|
||||||
|
const html = await this.fetcher.text(ctx, url, { headers });
|
||||||
|
|
||||||
|
if (/Page not found/.test(html)) {
|
||||||
|
throw new NotFoundError();
|
||||||
|
}
|
||||||
|
|
||||||
|
const $ = cheerio.load(html);
|
||||||
|
const title = $('h3').text().trim();
|
||||||
|
|
||||||
|
const iframeUrlMatches = Array.from(html.matchAll(/iframe.*?src=["'](.*?)["']/g));
|
||||||
|
if (iframeUrlMatches.length) {
|
||||||
|
// Use last match because there can be fake adblock catcher urls before
|
||||||
|
return await this.extractInternal(ctx, new URL((iframeUrlMatches[iframeUrlMatches.length - 1] as RegExpExecArray)[1] as string), { title, ...meta }, url);
|
||||||
|
}
|
||||||
|
|
||||||
|
const playlistUrl = await buildMediaFlowProxyExtractorStreamUrl(ctx, this.fetcher, 'FileMoon', originalUrl as URL, headers);
|
||||||
|
|
||||||
|
const unpacked = unpackEval(html);
|
||||||
|
const heightMatch = unpacked.match(/(\d{3,})p/);
|
||||||
|
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
url: playlistUrl,
|
||||||
|
format: Format.hls,
|
||||||
|
meta: {
|
||||||
|
...meta,
|
||||||
|
...(heightMatch && { height: parseInt(heightMatch[1] as string) }),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -10,15 +10,7 @@ const extractorRegistry = new ExtractorRegistry(logger, [new Fsst(new FetcherMoc
|
||||||
const ctx = createTestContext();
|
const ctx = createTestContext();
|
||||||
|
|
||||||
describe('Fsst', () => {
|
describe('Fsst', () => {
|
||||||
test('Blood & Sinners', async () => {
|
test('Wake up Dead Man', async () => {
|
||||||
expect(await extractorRegistry.handle(ctx, new URL('https://fsst.online/embed/900576/'))).toMatchSnapshot();
|
expect(await extractorRegistry.handle(ctx, new URL('https://fsst.online/embed/948429/'))).toMatchSnapshot();
|
||||||
});
|
|
||||||
|
|
||||||
test('Dead City', async () => {
|
|
||||||
expect(await extractorRegistry.handle(ctx, new URL('https://fsst.online/embed/901994/'))).toMatchSnapshot();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('How to Train Your Dragon', async () => {
|
|
||||||
expect(await extractorRegistry.handle(ctx, new URL('https://fsst.online/embed/902668/'))).toMatchSnapshot();
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import * as cheerio from 'cheerio';
|
import * as cheerio from 'cheerio';
|
||||||
import { Context, Format, Meta, UrlResult } from '../types';
|
import { Context, Format, InternalUrlResult, Meta } from '../types';
|
||||||
import { Extractor } from './Extractor';
|
import { Extractor } from './Extractor';
|
||||||
|
|
||||||
export class Fsst extends Extractor {
|
export class Fsst extends Extractor {
|
||||||
|
|
@ -7,36 +7,35 @@ export class Fsst extends Extractor {
|
||||||
|
|
||||||
public readonly label = 'Fsst';
|
public readonly label = 'Fsst';
|
||||||
|
|
||||||
|
public override readonly ttl: number = 10800000; // 3h
|
||||||
|
|
||||||
public supports(_ctx: Context, url: URL): boolean {
|
public supports(_ctx: Context, url: URL): boolean {
|
||||||
return null !== url.host.match(/fsst/);
|
return null !== url.host.match(/fsst/);
|
||||||
};
|
};
|
||||||
|
|
||||||
protected async extractInternal(ctx: Context, url: URL, meta: Meta): Promise<UrlResult[]> {
|
protected async extractInternal(ctx: Context, url: URL, meta: Meta): Promise<InternalUrlResult[]> {
|
||||||
const headers = { Referer: meta.referer ?? url.origin };
|
const headers = { Referer: meta.referer ?? url.href };
|
||||||
|
|
||||||
const html = await this.fetcher.text(ctx, url, { headers });
|
const html = await this.fetcher.text(ctx, url, { headers, noProxyHeaders: true });
|
||||||
|
|
||||||
const $ = cheerio.load(html);
|
const $ = cheerio.load(html);
|
||||||
const title = $('title').text().trim();
|
const title = $('title').text().trim();
|
||||||
|
|
||||||
const filesMatch = html.match(/file:"(.*)"/) as string[];
|
const filesMatch = html.match(/file:"(.*)"/) as string[];
|
||||||
|
|
||||||
return (filesMatch[1] as string).split(',').map((fileString) => {
|
const lastFile = (filesMatch[1] as string).split(',').pop() as string;
|
||||||
const heightAndUrlMatch = fileString.match(/\[?([\d]*)p?]?(.*)/) as string[];
|
|
||||||
const fileHref = heightAndUrlMatch[2] as string;
|
|
||||||
const heightFromFileHrefMatch = fileHref.match(/([\d]+)p/) as string[];
|
|
||||||
|
|
||||||
return {
|
const heightAndUrlMatch = lastFile.match(/\[?([\d]*)p?]?(.*)/) as string[];
|
||||||
url: new URL(fileHref),
|
const fileHref = heightAndUrlMatch[2] as string;
|
||||||
format: Format.mp4,
|
|
||||||
label: this.label,
|
return [{
|
||||||
sourceId: `${this.id}_${meta.countryCodes?.join('_')}`,
|
url: await this.fetcher.getFinalRedirectUrl(ctx, new URL(fileHref), { headers, noProxyHeaders: true }, 1),
|
||||||
meta: {
|
format: Format.mp4,
|
||||||
...meta,
|
meta: {
|
||||||
height: parseInt(heightAndUrlMatch[1] || heightFromFileHrefMatch[1] as string),
|
...meta,
|
||||||
title,
|
height: parseInt(heightAndUrlMatch[1] as string),
|
||||||
},
|
title,
|
||||||
};
|
},
|
||||||
});
|
}];
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -21,4 +21,8 @@ describe('HubCloud', () => {
|
||||||
test('handle dark 2017 s03e08', async () => {
|
test('handle dark 2017 s03e08', async () => {
|
||||||
expect(await extractorRegistry.handle(ctx, new URL('https://hubcloud.one/drive/nknlofk8snfnknh'))).toMatchSnapshot();
|
expect(await extractorRegistry.handle(ctx, new URL('https://hubcloud.one/drive/nknlofk8snfnknh'))).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('handle goat 2026', async () => {
|
||||||
|
expect(await extractorRegistry.handle(ctx, new URL('https://hubcloud.foo/drive/p94k4dccjwxjcx4'))).toMatchSnapshot();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import bytes from 'bytes';
|
import bytes from 'bytes';
|
||||||
import * as cheerio from 'cheerio';
|
import * as cheerio from 'cheerio';
|
||||||
import { Context, Format, Meta, UrlResult } from '../types';
|
import { Context, Format, InternalUrlResult, Meta } from '../types';
|
||||||
|
import { findCountryCodes, findHeight } from '../utils';
|
||||||
import { Extractor } from './Extractor';
|
import { Extractor } from './Extractor';
|
||||||
|
|
||||||
export class HubCloud extends Extractor {
|
export class HubCloud extends Extractor {
|
||||||
|
|
@ -8,25 +9,33 @@ export class HubCloud extends Extractor {
|
||||||
|
|
||||||
public readonly label = 'HubCloud';
|
public readonly label = 'HubCloud';
|
||||||
|
|
||||||
public override readonly ttl: number = 259200000; // 3d
|
public override readonly ttl: number = 43200000; // 12h
|
||||||
|
|
||||||
|
public override readonly cacheVersion = 1;
|
||||||
|
|
||||||
public supports(_ctx: Context, url: URL): boolean {
|
public supports(_ctx: Context, url: URL): boolean {
|
||||||
return null !== url.host.match(/hubcloud/);
|
return null !== url.host.match(/hubcloud/);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async extractInternal(ctx: Context, url: URL, meta: Meta): Promise<UrlResult[]> {
|
protected async extractInternal(ctx: Context, url: URL, meta: Meta): Promise<InternalUrlResult[]> {
|
||||||
const redirectHtml = await this.fetcher.text(ctx, url);
|
const headers = { Referer: meta.referer ?? url.href };
|
||||||
|
|
||||||
|
const redirectHtml = await this.fetcher.text(ctx, url, { headers });
|
||||||
const redirectUrlMatch = redirectHtml.match(/var url ?= ?'(.*?)'/) as string[];
|
const redirectUrlMatch = redirectHtml.match(/var url ?= ?'(.*?)'/) as string[];
|
||||||
|
|
||||||
const linksHtml = await this.fetcher.text(ctx, new URL(redirectUrlMatch[1] as string));
|
const linksHtml = await this.fetcher.text(ctx, new URL(redirectUrlMatch[1] as string), { headers: { Referer: url.href } });
|
||||||
const $ = cheerio.load(linksHtml);
|
const $ = cheerio.load(linksHtml);
|
||||||
|
|
||||||
return [
|
const title = $('title').text().trim();
|
||||||
|
const countryCodes = [...new Set([...meta.countryCodes ?? [], ...findCountryCodes(title)])];
|
||||||
|
const height = meta.height ?? findHeight(title);
|
||||||
|
|
||||||
|
return Promise.all([
|
||||||
...$('a')
|
...$('a')
|
||||||
.filter((_i, el) => {
|
.filter((_i, el) => {
|
||||||
const text = $(el).text();
|
const text = $(el).text();
|
||||||
|
|
||||||
return text.includes('FSL') || text.includes('Download File');
|
return text.includes('FSL') && !text.includes('FSLv2');
|
||||||
})
|
})
|
||||||
.map((_i, el) => {
|
.map((_i, el) => {
|
||||||
const url = new URL($(el).attr('href') as string);
|
const url = new URL($(el).attr('href') as string);
|
||||||
|
|
@ -34,32 +43,60 @@ export class HubCloud extends Extractor {
|
||||||
url,
|
url,
|
||||||
format: Format.unknown,
|
format: Format.unknown,
|
||||||
label: `${this.label} (FSL)`,
|
label: `${this.label} (FSL)`,
|
||||||
sourceId: `${this.id}_fsl`,
|
|
||||||
ttl: this.ttl,
|
|
||||||
meta: {
|
meta: {
|
||||||
...meta,
|
...meta,
|
||||||
bytes: bytes.parse($('#size').text()) as number,
|
bytes: bytes.parse($('#size').text()) as number,
|
||||||
title: $('title').text().trim(),
|
extractorId: `${this.id}_fsl`,
|
||||||
|
countryCodes,
|
||||||
|
height,
|
||||||
|
title,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}).toArray(),
|
||||||
|
...$('a')
|
||||||
|
.filter((_i, el) => {
|
||||||
|
const text = $(el).text();
|
||||||
|
|
||||||
|
return text.includes('FSLv2');
|
||||||
|
})
|
||||||
|
.map((_i, el) => {
|
||||||
|
const url = new URL($(el).attr('href') as string);
|
||||||
|
return {
|
||||||
|
url,
|
||||||
|
format: Format.unknown,
|
||||||
|
label: `${this.label} (FSLv2)`,
|
||||||
|
meta: {
|
||||||
|
...meta,
|
||||||
|
bytes: bytes.parse($('#size').text()) as number,
|
||||||
|
extractorId: `${this.id}_fslv2`,
|
||||||
|
countryCodes,
|
||||||
|
height,
|
||||||
|
title,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}).toArray(),
|
}).toArray(),
|
||||||
...$('a')
|
...$('a')
|
||||||
.filter((_i, el) => $(el).text().includes('PixelServer'))
|
.filter((_i, el) => $(el).text().includes('PixelServer'))
|
||||||
.map((_i, el) => {
|
.map((_i, el) => {
|
||||||
const url = new URL(($(el).attr('href') as string).replace('/u/', '/api/file/'));
|
const userUrl = new URL(($(el).attr('href') as string).replace('/api/file/', '/u/'));
|
||||||
|
const url = new URL(userUrl.href.replace('/u/', '/api/file/'));
|
||||||
|
url.searchParams.set('download', '');
|
||||||
|
|
||||||
return {
|
return {
|
||||||
url,
|
url,
|
||||||
format: Format.unknown,
|
format: Format.unknown,
|
||||||
label: `${this.label} (PixelServer)`,
|
label: `${this.label} (PixelServer)`,
|
||||||
sourceId: `${this.id}_pixelserver`,
|
|
||||||
ttl: this.ttl,
|
|
||||||
meta: {
|
meta: {
|
||||||
...meta,
|
...meta,
|
||||||
bytes: bytes.parse($('#size').text()) as number,
|
bytes: bytes.parse($('#size').text()) as number,
|
||||||
title: $('title').text().trim(),
|
extractorId: `${this.id}_pixelserver`,
|
||||||
|
countryCodes,
|
||||||
|
height,
|
||||||
|
title,
|
||||||
},
|
},
|
||||||
|
requestHeaders: { Referer: userUrl.href },
|
||||||
};
|
};
|
||||||
}).toArray(),
|
}).toArray(),
|
||||||
];
|
]);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
29
src/extractor/HubDrive.test.ts
Normal file
29
src/extractor/HubDrive.test.ts
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
import winston from 'winston';
|
||||||
|
import { createTestContext } from '../test';
|
||||||
|
import { FetcherMock } from '../utils';
|
||||||
|
import { ExtractorRegistry } from './ExtractorRegistry';
|
||||||
|
import { HubCloud } from './HubCloud';
|
||||||
|
import { HubDrive } from './HubDrive';
|
||||||
|
|
||||||
|
const logger = winston.createLogger({ transports: [new winston.transports.Console({ level: 'nope' })] });
|
||||||
|
const extractorRegistry = new ExtractorRegistry(
|
||||||
|
logger,
|
||||||
|
[
|
||||||
|
new HubDrive(
|
||||||
|
new FetcherMock(`${__dirname}/__fixtures__/HubDrive`),
|
||||||
|
new HubCloud(new FetcherMock(`${__dirname}/__fixtures__/HubDrive/HubCloud`)),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
const ctx = createTestContext();
|
||||||
|
|
||||||
|
describe('HubDrive', () => {
|
||||||
|
test('handle avatar', async () => {
|
||||||
|
expect(await extractorRegistry.handle(ctx, new URL('https://hubdrive.space/file/7283903021'))).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('handle missing hubcloud', async () => {
|
||||||
|
expect(await extractorRegistry.handle(ctx, new URL('https://hubdrive.space/file/2243124026'))).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
});
|
||||||
40
src/extractor/HubDrive.ts
Normal file
40
src/extractor/HubDrive.ts
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
import * as cheerio from 'cheerio';
|
||||||
|
import { Context, InternalUrlResult, Meta } from '../types';
|
||||||
|
import { Fetcher } from '../utils';
|
||||||
|
import { Extractor } from './Extractor';
|
||||||
|
import { HubCloud } from './HubCloud';
|
||||||
|
|
||||||
|
export class HubDrive extends Extractor {
|
||||||
|
public readonly id = 'hubdrive';
|
||||||
|
|
||||||
|
public readonly label = 'HubDrive';
|
||||||
|
|
||||||
|
public override readonly ttl: number = 43200000; // 12h
|
||||||
|
|
||||||
|
public override readonly cacheVersion = 1;
|
||||||
|
|
||||||
|
private readonly hubCloud: HubCloud;
|
||||||
|
|
||||||
|
public constructor(fetcher: Fetcher, hubCloud: HubCloud) {
|
||||||
|
super(fetcher);
|
||||||
|
|
||||||
|
this.hubCloud = hubCloud;
|
||||||
|
}
|
||||||
|
|
||||||
|
public supports(_ctx: Context, url: URL): boolean {
|
||||||
|
return null !== url.host.match(/hubdrive/);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async extractInternal(ctx: Context, url: URL, meta: Meta): Promise<InternalUrlResult[]> {
|
||||||
|
const headers = { Referer: meta.referer ?? url.href };
|
||||||
|
|
||||||
|
const html = await this.fetcher.text(ctx, url, { headers });
|
||||||
|
const $ = cheerio.load(html);
|
||||||
|
|
||||||
|
const hubCloudUrl = $('a:contains("HubCloud")')
|
||||||
|
.map((_i, el) => new URL($(el).attr('href') as string))
|
||||||
|
.get(0);
|
||||||
|
|
||||||
|
return hubCloudUrl ? this.hubCloud.extract(ctx, hubCloudUrl, meta) : [];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -17,4 +17,12 @@ describe('KinoGer', () => {
|
||||||
test('Dead City', async () => {
|
test('Dead City', async () => {
|
||||||
expect(await extractorRegistry.handle(ctx, new URL('https://kinoger.re/#x6tsx9'))).toMatchSnapshot();
|
expect(await extractorRegistry.handle(ctx, new URL('https://kinoger.re/#x6tsx9'))).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('filma365.strp2p.site', async () => {
|
||||||
|
expect(await extractorRegistry.handle(ctx, new URL('https://filma365.strp2p.site/#vih6bx'))).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('kinoger.p2pplay.pro', async () => {
|
||||||
|
expect(await extractorRegistry.handle(ctx, new URL('https://kinoger.p2pplay.pro/#k53k9'))).toMatchSnapshot();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import crypto from 'crypto';
|
import crypto from 'crypto';
|
||||||
import { Context, Format, Meta, UrlResult } from '../types';
|
import { Context, Format, InternalUrlResult, Meta } from '../types';
|
||||||
import { guessHeightFromPlaylist } from '../utils';
|
import { guessHeightFromPlaylist } from '../utils';
|
||||||
import { Extractor } from './Extractor';
|
import { Extractor } from './Extractor';
|
||||||
|
|
||||||
|
|
@ -17,11 +17,19 @@ export class KinoGer extends Extractor {
|
||||||
'disneycdn.net',
|
'disneycdn.net',
|
||||||
'dzo.vidplayer.live',
|
'dzo.vidplayer.live',
|
||||||
'filedecrypt.link',
|
'filedecrypt.link',
|
||||||
|
'filma365.strp2p.site',
|
||||||
|
'flimmer.rpmvip.com',
|
||||||
|
'flixfilmesonline.strp2p.site',
|
||||||
|
'kinoger.p2pplay.pro',
|
||||||
'kinoger.re',
|
'kinoger.re',
|
||||||
'moflix.rpmplay.xyz',
|
'moflix.rpmplay.xyz',
|
||||||
'moflix.upns.xyz',
|
'moflix.upns.xyz',
|
||||||
'player.upn.one',
|
'player.upn.one',
|
||||||
|
'securecdn.shop',
|
||||||
'shiid4u.upn.one',
|
'shiid4u.upn.one',
|
||||||
|
'srbe84.vidplayer.live',
|
||||||
|
'strp2p.site',
|
||||||
|
't1.p2pplay.pro',
|
||||||
'tuktuk.rpmvid.com',
|
'tuktuk.rpmvid.com',
|
||||||
'ultrastream.online',
|
'ultrastream.online',
|
||||||
'videoland.cfd',
|
'videoland.cfd',
|
||||||
|
|
@ -35,36 +43,35 @@ export class KinoGer extends Extractor {
|
||||||
return new URL(`${url.origin}/api/v1/video?id=${url.hash.slice(1)}`);
|
return new URL(`${url.origin}/api/v1/video?id=${url.hash.slice(1)}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async extractInternal(ctx: Context, url: URL, meta: Meta): Promise<UrlResult[]> {
|
protected async extractInternal(ctx: Context, url: URL, meta: Meta): Promise<InternalUrlResult[]> {
|
||||||
const headers = { 'Referer': meta.referer ?? url.origin, 'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36' };
|
const headers = {
|
||||||
|
'Origin': url.origin,
|
||||||
|
'Referer': url.origin + '/',
|
||||||
|
'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36',
|
||||||
|
};
|
||||||
|
|
||||||
const hexData = await this.fetcher.text(ctx, url, { headers });
|
const hexData = await this.fetcher.text(ctx, url, { headers });
|
||||||
|
|
||||||
const encrypted = Buffer.from(hexData, 'hex');
|
const encrypted = Buffer.from(hexData.slice(0, -1), 'hex');
|
||||||
const key = Buffer.from('6b69656d7469656e6d75613931316361', 'hex');
|
const key = Buffer.from('6b69656d7469656e6d75613931316361', 'hex');
|
||||||
const iv = Buffer.from('313233343536373839306f6975797472', 'hex');
|
const iv = Buffer.from('313233343536373839306f6975797472', 'hex');
|
||||||
const decipher = crypto.createDecipheriv('aes-128-cbc', key, iv);
|
const decipher = crypto.createDecipheriv('aes-128-cbc', key, iv);
|
||||||
const decrypted = Buffer.concat([decipher.update(encrypted), decipher.final()]).toString();
|
const decrypted = Buffer.concat([decipher.update(encrypted), decipher.final()]).toString();
|
||||||
|
|
||||||
const { cf, title } = JSON.parse(decrypted) as { cf: string; title: string };
|
const { source, title } = JSON.parse(decrypted) as { source: string; title: string };
|
||||||
|
|
||||||
const m3u8Url = new URL(cf);
|
const m3u8Url = new URL(source);
|
||||||
|
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
url: new URL(cf),
|
url: m3u8Url,
|
||||||
format: Format.hls,
|
format: Format.hls,
|
||||||
label: this.label,
|
|
||||||
sourceId: `${this.id}_${meta.countryCodes?.join('_')}`,
|
|
||||||
ttl: this.ttl,
|
|
||||||
meta: {
|
meta: {
|
||||||
...meta,
|
...meta,
|
||||||
height: await guessHeightFromPlaylist(ctx, this.fetcher, m3u8Url, { headers }),
|
height: meta.height ?? await guessHeightFromPlaylist(ctx, this.fetcher, m3u8Url, { headers }),
|
||||||
title,
|
title,
|
||||||
},
|
},
|
||||||
requestHeaders: {
|
requestHeaders: headers,
|
||||||
Referer: url.origin,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
|
||||||
24
src/extractor/LuluStream.test.ts
Normal file
24
src/extractor/LuluStream.test.ts
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
import winston from 'winston';
|
||||||
|
import { createTestContext } from '../test';
|
||||||
|
import { FetcherMock } from '../utils';
|
||||||
|
import { ExtractorRegistry } from './ExtractorRegistry';
|
||||||
|
import { LuluStream } from './LuluStream';
|
||||||
|
|
||||||
|
const logger = winston.createLogger({ transports: [new winston.transports.Console({ level: 'nope' })] });
|
||||||
|
const extractorRegistry = new ExtractorRegistry(logger, [new LuluStream(new FetcherMock(`${__dirname}/__fixtures__/LuluStream`))]);
|
||||||
|
|
||||||
|
const ctx = createTestContext({ mediaFlowProxyUrl: 'https://mediaflow.test.org', mediaFlowProxyPassword: 'test' });
|
||||||
|
|
||||||
|
describe('LuluStream', () => {
|
||||||
|
test('streamhihi d', async () => {
|
||||||
|
expect(await extractorRegistry.handle(ctx, new URL('https://streamhihi.com/d/mk9m58lz8ts6'))).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('streamhihi', async () => {
|
||||||
|
expect(await extractorRegistry.handle(ctx, new URL('https://streamhihi.com/mk9m58lz8ts6'))).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('no such file', async () => {
|
||||||
|
expect(await extractorRegistry.handle(ctx, new URL('https://lulustream.com/e/uthq0o0sljnx'))).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
});
|
||||||
67
src/extractor/LuluStream.ts
Normal file
67
src/extractor/LuluStream.ts
Normal file
|
|
@ -0,0 +1,67 @@
|
||||||
|
import bytes from 'bytes';
|
||||||
|
import * as cheerio from 'cheerio';
|
||||||
|
import { NotFoundError } from '../error';
|
||||||
|
import { Context, Format, InternalUrlResult, Meta } from '../types';
|
||||||
|
import {
|
||||||
|
buildMediaFlowProxyExtractorStreamUrl,
|
||||||
|
supportsMediaFlowProxy,
|
||||||
|
} from '../utils';
|
||||||
|
import { Extractor } from './Extractor';
|
||||||
|
|
||||||
|
/** @see https://github.com/Gujal00/ResolveURL/blob/master/script.module.resolveurl/lib/resolveurl/plugins/lulustream.py */
|
||||||
|
export class LuluStream extends Extractor {
|
||||||
|
public readonly id = 'lulustream';
|
||||||
|
|
||||||
|
public readonly label = 'LuluStream';
|
||||||
|
|
||||||
|
public override viaMediaFlowProxy = true;
|
||||||
|
|
||||||
|
public supports(ctx: Context, url: URL): boolean {
|
||||||
|
const supportedDomain = null !== url.host.match(/lulu/)
|
||||||
|
|| [
|
||||||
|
'732eg54de642sa.sbs',
|
||||||
|
'cdn1.site',
|
||||||
|
'd00ds.site',
|
||||||
|
'streamhihi.com',
|
||||||
|
].includes(url.host);
|
||||||
|
|
||||||
|
return supportedDomain && supportsMediaFlowProxy(ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override normalize(url: URL): URL {
|
||||||
|
const videoId = url.pathname.replace(/\/+$/, '').split('/').at(-1) as string;
|
||||||
|
|
||||||
|
return new URL(`/e/${videoId}`, url);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async extractInternal(ctx: Context, url: URL, meta: Meta): Promise<InternalUrlResult[]> {
|
||||||
|
const headers = { Referer: meta.referer ?? url.href };
|
||||||
|
|
||||||
|
const fileUrl = new URL(url.href.replace('/e/', '/d/'));
|
||||||
|
const html = await this.fetcher.text(ctx, fileUrl, { headers });
|
||||||
|
|
||||||
|
if (/No such file|File Not Found/.test(html)) {
|
||||||
|
throw new NotFoundError();
|
||||||
|
}
|
||||||
|
|
||||||
|
const $ = cheerio.load(html);
|
||||||
|
const title = $('h1').text().trim();
|
||||||
|
|
||||||
|
const heightAndSizeMatch = html.match(/\d{3,}x(\d{3,}), ([\d.]+ ?[GM]B)/) as string[];
|
||||||
|
|
||||||
|
const playlistUrl = await buildMediaFlowProxyExtractorStreamUrl(ctx, this.fetcher, 'LuluStream', url, headers);
|
||||||
|
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
url: playlistUrl,
|
||||||
|
format: Format.hls,
|
||||||
|
meta: {
|
||||||
|
...meta,
|
||||||
|
title,
|
||||||
|
bytes: bytes.parse(heightAndSizeMatch[2] as string) as number,
|
||||||
|
height: parseInt(heightAndSizeMatch[1] as string),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -1,24 +1,24 @@
|
||||||
import bytes from 'bytes';
|
import bytes from 'bytes';
|
||||||
import * as cheerio from 'cheerio';
|
import * as cheerio from 'cheerio';
|
||||||
import { NotFoundError } from '../error';
|
import { NotFoundError } from '../error';
|
||||||
import { Context, Format, Meta, UrlResult } from '../types';
|
import { Context, Format, InternalUrlResult, Meta } from '../types';
|
||||||
import { buildMediaFlowProxyExtractorRedirectUrl, supportsMediaFlowProxy } from '../utils';
|
import { buildMediaFlowProxyExtractorRedirectUrl, supportsMediaFlowProxy } from '../utils';
|
||||||
import { Extractor } from './Extractor';
|
import { Extractor } from './Extractor';
|
||||||
|
|
||||||
export class Mixdrop extends Extractor {
|
export class Mixdrop extends Extractor {
|
||||||
public readonly id = 'mixdrop';
|
public readonly id = 'mixdrop';
|
||||||
|
|
||||||
public readonly label = 'Mixdrop (via MediaFlow Proxy)';
|
public readonly label = 'Mixdrop';
|
||||||
|
|
||||||
public override viaMediaFlowProxy = true;
|
public override viaMediaFlowProxy = true;
|
||||||
|
|
||||||
public supports(ctx: Context, url: URL): boolean {
|
public supports(ctx: Context, url: URL): boolean {
|
||||||
return null !== url.host.match(/mixdrop/) && supportsMediaFlowProxy(ctx);
|
return null !== url.host.match(/mixdrop|mixdrp|mixdroop|m1xdrop/) && supportsMediaFlowProxy(ctx);
|
||||||
}
|
}
|
||||||
|
|
||||||
public override readonly normalize = (url: URL): URL => new URL(url.href.replace('/f/', '/e/'));
|
public override readonly normalize = (url: URL): URL => new URL(url.href.replace('/f/', '/e/'));
|
||||||
|
|
||||||
protected async extractInternal(ctx: Context, url: URL, meta: Meta): Promise<UrlResult[]> {
|
protected async extractInternal(ctx: Context, url: URL, meta: Meta): Promise<InternalUrlResult[]> {
|
||||||
const fileUrl = new URL(url.href.replace('/e/', '/f/'));
|
const fileUrl = new URL(url.href.replace('/e/', '/f/'));
|
||||||
const html = await this.fetcher.text(ctx, fileUrl);
|
const html = await this.fetcher.text(ctx, fileUrl);
|
||||||
|
|
||||||
|
|
@ -35,9 +35,6 @@ export class Mixdrop extends Extractor {
|
||||||
{
|
{
|
||||||
url: buildMediaFlowProxyExtractorRedirectUrl(ctx, 'Mixdrop', url),
|
url: buildMediaFlowProxyExtractorRedirectUrl(ctx, 'Mixdrop', url),
|
||||||
format: Format.mp4,
|
format: Format.mp4,
|
||||||
label: this.label,
|
|
||||||
sourceId: `${this.id}_${meta.countryCodes?.join('_')}`,
|
|
||||||
ttl: this.ttl,
|
|
||||||
meta: {
|
meta: {
|
||||||
...meta,
|
...meta,
|
||||||
bytes: bytes.parse((sizeMatch[1] as string).replace(',', '')) as number,
|
bytes: bytes.parse((sizeMatch[1] as string).replace(',', '')) as number,
|
||||||
|
|
|
||||||
|
|
@ -2,23 +2,23 @@ import winston from 'winston';
|
||||||
import { createTestContext } from '../test';
|
import { createTestContext } from '../test';
|
||||||
import { FetcherMock } from '../utils';
|
import { FetcherMock } from '../utils';
|
||||||
import { ExtractorRegistry } from './ExtractorRegistry';
|
import { ExtractorRegistry } from './ExtractorRegistry';
|
||||||
import { Soaper } from './Soaper';
|
import { RgShows } from './RgShows';
|
||||||
|
|
||||||
const logger = winston.createLogger({ transports: [new winston.transports.Console({ level: 'nope' })] });
|
const logger = winston.createLogger({ transports: [new winston.transports.Console({ level: 'nope' })] });
|
||||||
const extractorRegistry = new ExtractorRegistry(logger, [new Soaper(new FetcherMock(`${__dirname}/__fixtures__/Soaper`))]);
|
const extractorRegistry = new ExtractorRegistry(logger, [new RgShows(new FetcherMock(`${__dirname}/__fixtures__/RgShows`))]);
|
||||||
|
|
||||||
const ctx = createTestContext();
|
const ctx = createTestContext();
|
||||||
|
|
||||||
describe('Soaper', () => {
|
describe('RgShows', () => {
|
||||||
test('Full Metal Jacket', async () => {
|
test('handle one battle after another', async () => {
|
||||||
expect(await extractorRegistry.handle(ctx, new URL('https://soaper.live/movie_d8kdeypDY9.html'))).toMatchSnapshot();
|
expect(await extractorRegistry.handle(ctx, new URL('https://api.rgshows.ru/main/movie/1054867'))).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Black Mirror', async () => {
|
test('handle stranger things s05e01', async () => {
|
||||||
expect(await extractorRegistry.handle(ctx, new URL('https://soaper.live/episode_5KDq78eGp1.html'))).toMatchSnapshot();
|
expect(await extractorRegistry.handle(ctx, new URL('https://api.rgshows.ru/main/tv/66732/5/1'))).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('last of us s2e3', async () => {
|
test('handle parasite 2019 (mp4)', async () => {
|
||||||
expect(await extractorRegistry.handle(ctx, new URL('https://soaper.live/episode_rYg3vMEDL1.html'))).toMatchSnapshot();
|
expect(await extractorRegistry.handle(ctx, new URL('https://api.rgshows.ru/main/movie/496243'))).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
49
src/extractor/RgShows.ts
Normal file
49
src/extractor/RgShows.ts
Normal file
|
|
@ -0,0 +1,49 @@
|
||||||
|
import { BlockedError } from '../error';
|
||||||
|
import { BlockedReason, Context, Format, InternalUrlResult, Meta } from '../types';
|
||||||
|
import { guessHeightFromPlaylist } from '../utils';
|
||||||
|
import { Extractor } from './Extractor';
|
||||||
|
|
||||||
|
interface RgShowsApiData {
|
||||||
|
stream: {
|
||||||
|
url: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export class RgShows extends Extractor {
|
||||||
|
public readonly id = 'rgshows';
|
||||||
|
|
||||||
|
public readonly label = 'RgShows';
|
||||||
|
|
||||||
|
public override readonly ttl: number = 10800000; // 3h
|
||||||
|
|
||||||
|
public supports(_ctx: Context, url: URL): boolean {
|
||||||
|
return null !== url.host.match(/rgshows/);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async extractInternal(ctx: Context, url: URL, meta: Meta): Promise<InternalUrlResult[]> {
|
||||||
|
const headers = { 'Referer': 'https://www.rgshows.ru/', 'Origin': 'https://www.rgshows.ru', 'User-Agent': 'Mozilla' };
|
||||||
|
|
||||||
|
const data = await this.fetcher.json(ctx, url, { headers }) as RgShowsApiData;
|
||||||
|
|
||||||
|
const streamUrl = new URL(data.stream.url);
|
||||||
|
/* istanbul ignore if */
|
||||||
|
if (streamUrl.host.includes('vidzee')) {
|
||||||
|
throw new BlockedError(url, BlockedReason.unknown, {});
|
||||||
|
}
|
||||||
|
|
||||||
|
const isMp4 = streamUrl.href.includes('mp4');
|
||||||
|
const isHls = streamUrl.href.includes('m3u8') || streamUrl.href.includes('txt');
|
||||||
|
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
url: streamUrl,
|
||||||
|
format: isMp4 ? Format.mp4 : (isHls ? Format.hls : /* istanbul ignore next */ Format.unknown),
|
||||||
|
meta: {
|
||||||
|
...meta,
|
||||||
|
...(isHls && { height: meta.height ?? await guessHeightFromPlaylist(ctx, this.fetcher, streamUrl, { headers }) }),
|
||||||
|
},
|
||||||
|
requestHeaders: headers,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import * as cheerio from 'cheerio';
|
import * as cheerio from 'cheerio';
|
||||||
import { NotFoundError } from '../error';
|
import { NotFoundError } from '../error';
|
||||||
import { Context, Format, Meta, UrlResult } from '../types';
|
import { Context, Format, InternalUrlResult, Meta } from '../types';
|
||||||
import { Extractor } from './Extractor';
|
import { Extractor } from './Extractor';
|
||||||
|
|
||||||
export class SaveFiles extends Extractor {
|
export class SaveFiles extends Extractor {
|
||||||
|
|
@ -18,8 +18,10 @@ export class SaveFiles extends Extractor {
|
||||||
return new URL(url.href.replace('/e/', '/').replace('/d/', '/'));
|
return new URL(url.href.replace('/e/', '/').replace('/d/', '/'));
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async extractInternal(ctx: Context, url: URL, meta: Meta): Promise<UrlResult[]> {
|
protected async extractInternal(ctx: Context, url: URL, meta: Meta): Promise<InternalUrlResult[]> {
|
||||||
const html = await this.fetcher.text(ctx, url);
|
const headers = { Referer: meta.referer ?? url.href };
|
||||||
|
|
||||||
|
const html = await this.fetcher.text(ctx, url, { headers });
|
||||||
|
|
||||||
if (/file was locked|file was deleted/i.test(html)) {
|
if (/file was locked|file was deleted/i.test(html)) {
|
||||||
throw new NotFoundError();
|
throw new NotFoundError();
|
||||||
|
|
@ -35,9 +37,6 @@ export class SaveFiles extends Extractor {
|
||||||
{
|
{
|
||||||
url: new URL(fileMatch[1] as string),
|
url: new URL(fileMatch[1] as string),
|
||||||
format: Format.hls,
|
format: Format.hls,
|
||||||
label: this.label,
|
|
||||||
sourceId: `${this.id}_${meta.countryCodes?.join('_')}`,
|
|
||||||
ttl: this.ttl,
|
|
||||||
meta: {
|
meta: {
|
||||||
...meta,
|
...meta,
|
||||||
title,
|
title,
|
||||||
|
|
|
||||||
|
|
@ -1,60 +0,0 @@
|
||||||
import { Context, Format, Meta, UrlResult } from '../types';
|
|
||||||
import { guessHeightFromPlaylist } from '../utils';
|
|
||||||
import { Extractor } from './Extractor';
|
|
||||||
|
|
||||||
interface SoaperInfoResponsePartial {
|
|
||||||
val: string;
|
|
||||||
val_bak: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class Soaper extends Extractor {
|
|
||||||
public readonly id = 'soaper';
|
|
||||||
|
|
||||||
public readonly label = 'Soaper';
|
|
||||||
|
|
||||||
public override readonly ttl: number = 43200000; // 12h
|
|
||||||
|
|
||||||
public supports(_ctx: Context, url: URL): boolean {
|
|
||||||
return null !== url.host.match(/soaper/) && null !== url.pathname.match(/^\/(episode|movie)_/);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected async extractInternal(ctx: Context, url: URL, meta: Meta): Promise<UrlResult[]> {
|
|
||||||
const movieOrEpisodeId = (url.pathname.match(/\/\w+_(\w+)/) as string[])[1] as string;
|
|
||||||
|
|
||||||
const form = new URLSearchParams();
|
|
||||||
form.append('pass', movieOrEpisodeId);
|
|
||||||
form.append('param', '');
|
|
||||||
form.append('extra', '');
|
|
||||||
form.append('e2', '');
|
|
||||||
form.append('server', '0');
|
|
||||||
|
|
||||||
const response = await this.fetcher.textPost(
|
|
||||||
ctx,
|
|
||||||
new URL(url.pathname.includes('episode') ? '/home/index/GetEInfoAjax' : '/home/index/GetMInfoAjax', url.origin),
|
|
||||||
form.toString(),
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/x-www-form-urlencoded',
|
|
||||||
'Referer': url.origin,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
);
|
|
||||||
const jsonResponse = JSON.parse(response) as SoaperInfoResponsePartial;
|
|
||||||
|
|
||||||
const m3u8Url = new URL(jsonResponse['val'], url.origin);
|
|
||||||
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
url: m3u8Url,
|
|
||||||
format: Format.hls,
|
|
||||||
label: this.label,
|
|
||||||
sourceId: `${this.id}_${meta.countryCodes?.join('_')}`,
|
|
||||||
ttl: this.ttl,
|
|
||||||
meta: {
|
|
||||||
...meta,
|
|
||||||
height: await guessHeightFromPlaylist(ctx, this.fetcher, m3u8Url),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
];
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
@ -13,4 +13,8 @@ describe('StreamEmbed', () => {
|
||||||
test('watch.gxplayer.xyz', async () => {
|
test('watch.gxplayer.xyz', async () => {
|
||||||
expect(await extractorRegistry.handle(ctx, new URL('https://watch.gxplayer.xyz/watch?v=MEKI92PU'))).toMatchSnapshot();
|
expect(await extractorRegistry.handle(ctx, new URL('https://watch.gxplayer.xyz/watch?v=MEKI92PU'))).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('video is not ready', async () => {
|
||||||
|
expect(await extractorRegistry.handle(ctx, new URL('https://watch.gxplayer.xyz/watch?v=PBO90WAS'))).toMatchSnapshot();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import { Context, Format, Meta, UrlResult } from '../types';
|
import { NotFoundError } from '../error';
|
||||||
|
import { Context, Format, InternalUrlResult, Meta } from '../types';
|
||||||
import { Extractor } from './Extractor';
|
import { Extractor } from './Extractor';
|
||||||
|
|
||||||
export class StreamEmbed extends Extractor {
|
export class StreamEmbed extends Extractor {
|
||||||
|
|
@ -6,12 +7,20 @@ export class StreamEmbed extends Extractor {
|
||||||
|
|
||||||
public readonly label = 'StreamEmbed';
|
public readonly label = 'StreamEmbed';
|
||||||
|
|
||||||
|
public override readonly ttl: number = 259200000; // 3d
|
||||||
|
|
||||||
public supports(_ctx: Context, url: URL): boolean {
|
public supports(_ctx: Context, url: URL): boolean {
|
||||||
return null !== url.host.match(/bullstream|mp4player|watch\.gxplayer/);
|
return null !== url.host.match(/bullstream|mp4player|watch\.gxplayer/);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async extractInternal(ctx: Context, url: URL, meta: Meta): Promise<UrlResult[]> {
|
protected async extractInternal(ctx: Context, url: URL, meta: Meta): Promise<InternalUrlResult[]> {
|
||||||
const html = await this.fetcher.text(ctx, url);
|
const headers = { Referer: meta.referer ?? url.href };
|
||||||
|
|
||||||
|
const html = await this.fetcher.text(ctx, url, { headers });
|
||||||
|
|
||||||
|
if (/Video is not ready/.test(html)) {
|
||||||
|
throw new NotFoundError();
|
||||||
|
}
|
||||||
|
|
||||||
const video = JSON.parse((html.match(/video ?= ?(.*);/) as string[])[1] as string);
|
const video = JSON.parse((html.match(/video ?= ?(.*);/) as string[])[1] as string);
|
||||||
|
|
||||||
|
|
@ -19,9 +28,6 @@ export class StreamEmbed extends Extractor {
|
||||||
{
|
{
|
||||||
url: new URL(`/m3u8/${video.uid}/${video.md5}/master.txt?s=1&id=${video.id}&cache=${video.status}`, url.origin),
|
url: new URL(`/m3u8/${video.uid}/${video.md5}/master.txt?s=1&id=${video.id}&cache=${video.status}`, url.origin),
|
||||||
format: Format.hls,
|
format: Format.hls,
|
||||||
label: this.label,
|
|
||||||
sourceId: `${this.id}_${meta.countryCodes?.join('_')}`,
|
|
||||||
ttl: this.ttl,
|
|
||||||
meta: {
|
meta: {
|
||||||
...meta,
|
...meta,
|
||||||
height: parseInt(JSON.parse(video.quality)[0]),
|
height: parseInt(JSON.parse(video.quality)[0]),
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,6 @@ const ctx = createTestContext({ mediaFlowProxyUrl: 'https://mediaflow.test.org',
|
||||||
|
|
||||||
describe('Streamtape', () => {
|
describe('Streamtape', () => {
|
||||||
test('streamtape.com /e', async () => {
|
test('streamtape.com /e', async () => {
|
||||||
expect(await extractorRegistry.handle(ctx, new URL('https://streamtape.com/e/84Kb3mkoYAiokmW'))).toMatchSnapshot();
|
expect(await extractorRegistry.handle(ctx, new URL('https://streamtape.com/e/DjmZBG0K9gHkJ7z/WALL-E.2008.1080p.BrRip.x264.YIFY.mp4'))).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,40 +1,68 @@
|
||||||
|
import bytes from 'bytes';
|
||||||
import * as cheerio from 'cheerio';
|
import * as cheerio from 'cheerio';
|
||||||
import { Context, Format, Meta, UrlResult } from '../types';
|
import { Context, Format, InternalUrlResult, Meta } from '../types';
|
||||||
import {
|
import { buildMediaFlowProxyExtractorRedirectUrl, supportsMediaFlowProxy } from '../utils';
|
||||||
buildMediaFlowProxyExtractorRedirectUrl,
|
|
||||||
supportsMediaFlowProxy,
|
|
||||||
} from '../utils';
|
|
||||||
import { guessSizeFromMp4 } from '../utils/size';
|
|
||||||
import { Extractor } from './Extractor';
|
import { Extractor } from './Extractor';
|
||||||
|
|
||||||
export class Streamtape extends Extractor {
|
export class Streamtape extends Extractor {
|
||||||
public readonly id = 'streamtape';
|
public readonly id = 'streamtape';
|
||||||
|
|
||||||
public readonly label = 'Streamtape (via MediaFlow Proxy)';
|
public readonly label = 'Streamtape';
|
||||||
|
|
||||||
|
public override viaMediaFlowProxy = true;
|
||||||
|
|
||||||
public supports(ctx: Context, url: URL): boolean {
|
public supports(ctx: Context, url: URL): boolean {
|
||||||
return null !== url.host.match(/streamtape/) && supportsMediaFlowProxy(ctx);
|
const supportedDomain = null !== url.host.match(/streamtape/)
|
||||||
|
|| [
|
||||||
|
'strtape.cloud',
|
||||||
|
'streamta.pe',
|
||||||
|
'strcloud.link',
|
||||||
|
'strcloud.club',
|
||||||
|
'strtpe.link',
|
||||||
|
'scloud.online',
|
||||||
|
'stape.fun',
|
||||||
|
'streamadblockplus.com',
|
||||||
|
'shavetape.cash',
|
||||||
|
'streamta.site',
|
||||||
|
'streamadblocker.xyz',
|
||||||
|
'tapewithadblock.org',
|
||||||
|
'adblocktape.wiki',
|
||||||
|
'antiadtape.com',
|
||||||
|
'tapeblocker.com',
|
||||||
|
'streamnoads.com',
|
||||||
|
'tapeadvertisement.com',
|
||||||
|
'tapeadsenjoyer.com',
|
||||||
|
'watchadsontape.com',
|
||||||
|
].includes(url.host);
|
||||||
|
|
||||||
|
return supportedDomain && supportsMediaFlowProxy(ctx);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async extractInternal(ctx: Context, url: URL, meta: Meta): Promise<UrlResult[]> {
|
public override normalize(url: URL): URL {
|
||||||
const html = await this.fetcher.text(ctx, url);
|
return new URL(url.href.replace('/e/', '/v/'));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async extractInternal(ctx: Context, url: URL, meta: Meta): Promise<InternalUrlResult[]> {
|
||||||
|
const headers = { Referer: meta.referer ?? url.href };
|
||||||
|
|
||||||
|
// Only needed to properly find non-existing files via 404 response
|
||||||
|
await this.fetcher.text(ctx, new URL(url.href.replace('/v/', '/e/')), { headers });
|
||||||
|
|
||||||
|
const html = await this.fetcher.text(ctx, url, { headers });
|
||||||
|
|
||||||
|
const sizeMatch = html.match(/([\d.]+ ?[GM]B)/) as string[];
|
||||||
|
|
||||||
const $ = cheerio.load(html);
|
const $ = cheerio.load(html);
|
||||||
const title = $('meta[name="og:title"]').attr('content') as string;
|
const title = $('meta[name="og:title"]').attr('content') as string;
|
||||||
|
|
||||||
const mp4Url = buildMediaFlowProxyExtractorRedirectUrl(ctx, 'Streamtape', url);
|
|
||||||
|
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
url: mp4Url,
|
url: buildMediaFlowProxyExtractorRedirectUrl(ctx, 'Streamtape', url, headers),
|
||||||
format: Format.mp4,
|
format: Format.mp4,
|
||||||
label: this.label,
|
|
||||||
sourceId: `${this.id}_${meta.countryCodes?.join('_')}`,
|
|
||||||
ttl: this.ttl,
|
|
||||||
meta: {
|
meta: {
|
||||||
...meta,
|
...meta,
|
||||||
title,
|
title,
|
||||||
bytes: await guessSizeFromMp4(ctx, this.fetcher, mp4Url),
|
bytes: bytes.parse(sizeMatch[1] as string) as number,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
|
import bytes from 'bytes';
|
||||||
|
import * as cheerio from 'cheerio';
|
||||||
import { NotFoundError } from '../error';
|
import { NotFoundError } from '../error';
|
||||||
import { Context, Format, Meta, UrlResult } from '../types';
|
import { Context, Format, InternalUrlResult, Meta } from '../types';
|
||||||
import { extractUrlFromPacked, guessHeightFromPlaylist } from '../utils';
|
import { extractUrlFromPacked, guessHeightFromPlaylist } from '../utils';
|
||||||
import { Extractor } from './Extractor';
|
import { Extractor } from './Extractor';
|
||||||
|
|
||||||
|
|
@ -15,33 +17,45 @@ export class SuperVideo extends Extractor {
|
||||||
}
|
}
|
||||||
|
|
||||||
public override normalize(url: URL): URL {
|
public override normalize(url: URL): URL {
|
||||||
const fileUrl = new URL(url.href.replace('/e/', '/').replace('/k/', '/').replace('/embed-', '/').replace('.html', ''));
|
return new URL(url.href.replace('/e/', '/').replace('/k/', '/').replace('/embed-', '/'));
|
||||||
|
|
||||||
return new URL(`/e${fileUrl.pathname}`, fileUrl.origin);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async extractInternal(ctx: Context, url: URL, meta: Meta): Promise<UrlResult[]> {
|
protected async extractInternal(ctx: Context, url: URL, meta: Meta): Promise<InternalUrlResult[]> {
|
||||||
const headers = { Referer: meta.referer ?? url.origin };
|
const headers = { Referer: meta.referer ?? url.href };
|
||||||
|
|
||||||
const html = await this.fetcher.text(ctx, url, { headers });
|
const html = await this.fetcher.text(ctx, url, { headers });
|
||||||
|
|
||||||
if (/'The file was deleted|The file expired|Video is processing/.test(html) || !html.includes('p,a,c,k,e,d')) {
|
if (html.includes('This video can be watched as embed only')) {
|
||||||
|
return await this.extractInternal(ctx, new URL(`/e${url.pathname}`, url.origin), meta);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (/'The file was deleted|The file expired|Video is processing/.test(html)) {
|
||||||
throw new NotFoundError();
|
throw new NotFoundError();
|
||||||
}
|
}
|
||||||
|
|
||||||
const m3u8Url = extractUrlFromPacked(html, [/sources:\[{file:"(.*?)"/]);
|
const playlistUrl = extractUrlFromPacked(html, [/sources:\[{file:"(.*?)"/]);
|
||||||
|
const playlistHeaders = { Referer: 'https://supervideo.cc/' };
|
||||||
|
|
||||||
|
const heightAndSizeMatch = html.match(/\d{3,}x(\d{3,}), ([\d.]+ ?[GM]B)/);
|
||||||
|
const size = heightAndSizeMatch ? bytes.parse(heightAndSizeMatch[2] as string) as number : undefined;
|
||||||
|
const height = heightAndSizeMatch
|
||||||
|
? parseInt(heightAndSizeMatch[1] as string)
|
||||||
|
: meta.height ?? await guessHeightFromPlaylist(ctx, this.fetcher, playlistUrl, { headers: playlistHeaders });
|
||||||
|
|
||||||
|
const $ = cheerio.load(html);
|
||||||
|
const title = $('.download__title').text().trim();
|
||||||
|
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
url: m3u8Url,
|
url: playlistUrl,
|
||||||
format: Format.hls,
|
format: Format.hls,
|
||||||
label: this.label,
|
|
||||||
sourceId: `${this.id}_${meta.countryCodes?.join('_')}`,
|
|
||||||
ttl: this.ttl,
|
|
||||||
meta: {
|
meta: {
|
||||||
...meta,
|
...meta,
|
||||||
height: await guessHeightFromPlaylist(ctx, this.fetcher, m3u8Url, { headers }),
|
title,
|
||||||
|
...(size && { bytes: size }),
|
||||||
|
...(height && { height }),
|
||||||
},
|
},
|
||||||
|
requestHeaders: playlistHeaders,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -13,4 +13,8 @@ describe('Uqload', () => {
|
||||||
test('uqload.net /embed-', async () => {
|
test('uqload.net /embed-', async () => {
|
||||||
expect(await extractorRegistry.handle(ctx, new URL('https://uqload.net/embed-z0xbr87oz637.html'))).toMatchSnapshot();
|
expect(await extractorRegistry.handle(ctx, new URL('https://uqload.net/embed-z0xbr87oz637.html'))).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('file not found', async () => {
|
||||||
|
expect(await extractorRegistry.handle(ctx, new URL('https://uqload.cx/2ua47fdfkjgd.html'))).toMatchSnapshot();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,13 @@
|
||||||
import * as cheerio from 'cheerio';
|
import * as cheerio from 'cheerio';
|
||||||
import { Context, Format, Meta, UrlResult } from '../types';
|
import { NotFoundError } from '../error';
|
||||||
|
import { Context, Format, InternalUrlResult, Meta } from '../types';
|
||||||
import { buildMediaFlowProxyExtractorRedirectUrl, supportsMediaFlowProxy } from '../utils';
|
import { buildMediaFlowProxyExtractorRedirectUrl, supportsMediaFlowProxy } from '../utils';
|
||||||
import { guessSizeFromMp4 } from '../utils/size';
|
|
||||||
import { Extractor } from './Extractor';
|
import { Extractor } from './Extractor';
|
||||||
|
|
||||||
export class Uqload extends Extractor {
|
export class Uqload extends Extractor {
|
||||||
public readonly id = 'uqload';
|
public readonly id = 'uqload';
|
||||||
|
|
||||||
public readonly label = 'Uqload (via MediaFlow Proxy)';
|
public readonly label = 'Uqload';
|
||||||
|
|
||||||
public override viaMediaFlowProxy = true;
|
public override viaMediaFlowProxy = true;
|
||||||
|
|
||||||
|
|
@ -19,27 +19,25 @@ export class Uqload extends Extractor {
|
||||||
return new URL(url.href.replace('/embed-', '/'));
|
return new URL(url.href.replace('/embed-', '/'));
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async extractInternal(ctx: Context, url: URL, meta: Meta): Promise<UrlResult[]> {
|
protected async extractInternal(ctx: Context, url: URL, meta: Meta): Promise<InternalUrlResult[]> {
|
||||||
const html = await this.fetcher.text(ctx, url);
|
const html = await this.fetcher.text(ctx, url);
|
||||||
|
|
||||||
|
if (/File Not Found/.test(html)) {
|
||||||
|
throw new NotFoundError();
|
||||||
|
}
|
||||||
|
|
||||||
const heightMatch = html.match(/\d{3,}x(\d{3,})/);
|
const heightMatch = html.match(/\d{3,}x(\d{3,})/);
|
||||||
|
|
||||||
const $ = cheerio.load(html);
|
const $ = cheerio.load(html);
|
||||||
const title = $('h1').text().trim();
|
const title = $('h1').text().trim();
|
||||||
|
|
||||||
const mp4Url = buildMediaFlowProxyExtractorRedirectUrl(ctx, 'Uqload', url);
|
|
||||||
|
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
url: mp4Url,
|
url: buildMediaFlowProxyExtractorRedirectUrl(ctx, 'Uqload', url),
|
||||||
format: Format.mp4,
|
format: Format.mp4,
|
||||||
label: this.label,
|
|
||||||
sourceId: `${this.id}_${meta.countryCodes?.join('_')}`,
|
|
||||||
ttl: this.ttl,
|
|
||||||
meta: {
|
meta: {
|
||||||
...meta,
|
...meta,
|
||||||
title,
|
title,
|
||||||
bytes: await guessSizeFromMp4(ctx, this.fetcher, mp4Url),
|
|
||||||
...(heightMatch && {
|
...(heightMatch && {
|
||||||
height: parseInt(heightMatch[1] as string),
|
height: parseInt(heightMatch[1] as string),
|
||||||
}),
|
}),
|
||||||
|
|
|
||||||
|
|
@ -1,59 +1,20 @@
|
||||||
import fetchMock from 'fetch-mock';
|
|
||||||
import winston from 'winston';
|
import winston from 'winston';
|
||||||
import { createTestContext } from '../test';
|
import { createTestContext } from '../test';
|
||||||
import { Fetcher, FetcherMock } from '../utils';
|
import { FetcherMock } from '../utils';
|
||||||
import { ExtractorRegistry } from './ExtractorRegistry';
|
import { ExtractorRegistry } from './ExtractorRegistry';
|
||||||
import { VidSrc } from './VidSrc';
|
import { VidSrc } from './VidSrc';
|
||||||
|
|
||||||
const logger = winston.createLogger({ transports: [new winston.transports.Console({ level: 'nope' })] });
|
const logger = winston.createLogger({ transports: [new winston.transports.Console({ level: 'nope' })] });
|
||||||
const extractorRegistry = new ExtractorRegistry(logger, [new VidSrc(new FetcherMock(`${__dirname}/__fixtures__/VidSrc`), ['xyz'])]);
|
const extractorRegistry = new ExtractorRegistry(logger, [new VidSrc(new FetcherMock(`${__dirname}/__fixtures__/VidSrc`), ['vidsrc-embed.ru'])]);
|
||||||
|
|
||||||
const ctx = createTestContext();
|
const ctx = createTestContext();
|
||||||
|
|
||||||
describe('VidSrc', () => {
|
describe('VidSrc', () => {
|
||||||
beforeAll(() => {
|
|
||||||
fetchMock.mockGlobal();
|
|
||||||
});
|
|
||||||
|
|
||||||
afterAll(() => {
|
|
||||||
fetchMock.unmockGlobal();
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
fetchMock.clearHistory();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Full Metal Jacket', async () => {
|
test('Full Metal Jacket', async () => {
|
||||||
expect(await extractorRegistry.handle(ctx, new URL('https://vidsrc.xyz/embed/movie/tt0093058'))).toMatchSnapshot();
|
expect(await extractorRegistry.handle(ctx, new URL('https://vidsrc-embed.ru/embed/movie/tt0093058'))).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Black Mirror', async () => {
|
test('Black Mirror', async () => {
|
||||||
expect(await extractorRegistry.handle(ctx, new URL('https://vidsrc.xyz/embed/tv/tt2085059/4/2'))).toMatchSnapshot();
|
expect(await extractorRegistry.handle(ctx, new URL('https://vidsrc-embed.ru/embed/tv/tt2085059/4-2'))).toMatchSnapshot();
|
||||||
});
|
|
||||||
|
|
||||||
test('not found', async () => {
|
|
||||||
expect(await extractorRegistry.handle(ctx, new URL('https://vidsrc.xyz/embed/movie/tt35628853'))).toMatchSnapshot();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('rate limit issues are retried and fail if no tlds are left', async () => {
|
|
||||||
fetchMock
|
|
||||||
.getOnce('https://vidsrc.xyz/embed/movie/tt33043892/1/1', 429)
|
|
||||||
.getOnce('https://vidsrc.net/embed/movie/tt33043892/1/1', 429);
|
|
||||||
|
|
||||||
const fetcher = new Fetcher(winston.createLogger({ transports: [new winston.transports.Console({ level: 'nope' })] }));
|
|
||||||
const vidSrc = new VidSrc(fetcher, ['net', 'xyz']);
|
|
||||||
|
|
||||||
expect(await vidSrc.extract(ctx, new URL('https://vidsrc.xyz/embed/movie/tt33043892/1/1'), {})).toMatchSnapshot();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('blocking issues are retried and fail if no tlds are left', async () => {
|
|
||||||
fetchMock
|
|
||||||
.getOnce('https://vidsrc.xyz/embed/movie/tt33043892/1/2', 403)
|
|
||||||
.getOnce('https://vidsrc.net/embed/movie/tt33043892/1/2', 403);
|
|
||||||
|
|
||||||
const fetcher = new Fetcher(winston.createLogger({ transports: [new winston.transports.Console({ level: 'nope' })] }));
|
|
||||||
const vidSrc = new VidSrc(fetcher, ['net', 'xyz']);
|
|
||||||
|
|
||||||
expect(await vidSrc.extract(ctx, new URL('https://vidsrc.xyz/embed/movie/tt33043892/1/2'), {})).toMatchSnapshot();
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
import * as cheerio from 'cheerio';
|
import * as cheerio from 'cheerio';
|
||||||
import slugify from 'slugify';
|
import { BlockedError, TooManyRequestsError } from '../error';
|
||||||
import { BlockedError, NotFoundError, TooManyRequestsError } from '../error';
|
import { Context, Format, InternalUrlResult, Meta, NonEmptyArray } from '../types';
|
||||||
import { Context, Format, Meta, NonEmptyArray, UrlResult } from '../types';
|
|
||||||
import { Fetcher, guessHeightFromPlaylist } from '../utils';
|
import { Fetcher, guessHeightFromPlaylist } from '../utils';
|
||||||
import { Extractor } from './Extractor';
|
import { Extractor } from './Extractor';
|
||||||
|
|
||||||
|
|
@ -12,46 +11,49 @@ export class VidSrc extends Extractor {
|
||||||
|
|
||||||
public override readonly ttl: number = 10800000; // 3h
|
public override readonly ttl: number = 10800000; // 3h
|
||||||
|
|
||||||
private readonly tlds: NonEmptyArray<string>;
|
private readonly domains: NonEmptyArray<string>;
|
||||||
|
|
||||||
public constructor(fetcher: Fetcher, tlds: NonEmptyArray<string>) {
|
public constructor(fetcher: Fetcher, domains: NonEmptyArray<string>) {
|
||||||
super(fetcher);
|
super(fetcher);
|
||||||
|
|
||||||
this.tlds = tlds;
|
this.domains = domains;
|
||||||
}
|
}
|
||||||
|
|
||||||
public supports(_ctx: Context, url: URL): boolean {
|
public supports(_ctx: Context, url: URL): boolean {
|
||||||
return null !== url.host.match(/vidsrc/);
|
return null !== url.host.match(/vidsrc|vsrc/);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async extractInternal(ctx: Context, url: URL, meta: Meta): Promise<UrlResult[]> {
|
protected async extractInternal(ctx: Context, url: URL, meta: Meta): Promise<InternalUrlResult[]> {
|
||||||
return this.extractUsingRandomTld(ctx, url, meta, [...this.tlds]);
|
// While this is a crappy thing to do, they seem to be blocking overly strict IMO
|
||||||
|
const randomIp = `${Math.floor(Math.random() * 223) + 1}.${Math.floor(Math.random() * 256)}.${Math.floor(Math.random() * 256)}.${Math.floor(Math.random() * 256)}`;
|
||||||
|
const newCtx = { ...ctx, ip: randomIp };
|
||||||
|
|
||||||
|
return this.extractUsingRandomDomain(newCtx, url, meta, [...this.domains]);
|
||||||
};
|
};
|
||||||
|
|
||||||
private async extractUsingRandomTld(ctx: Context, url: URL, meta: Meta, tlds: string[]): Promise<UrlResult[]> {
|
private async extractUsingRandomDomain(ctx: Context, url: URL, meta: Meta, domains: string[]): Promise<InternalUrlResult[]> {
|
||||||
const tldIndex = Math.floor(Math.random() * tlds.length);
|
const domainIndex = Math.floor(Math.random() * domains.length);
|
||||||
const [tld] = tlds.splice(tldIndex, 1) as [string];
|
const [domain] = domains.splice(domainIndex, 1) as [string];
|
||||||
|
|
||||||
const newUrl = new URL(url);
|
const newUrl = new URL(url);
|
||||||
const hostnameParts = newUrl.hostname.split('.');
|
newUrl.hostname = domain;
|
||||||
hostnameParts[hostnameParts.length - 1] = tld;
|
|
||||||
newUrl.hostname = hostnameParts.join('.');
|
|
||||||
|
|
||||||
let html: string;
|
let html: string;
|
||||||
try {
|
try {
|
||||||
html = await this.fetcher.text(ctx, newUrl, { headers: { Referer: meta.referer ?? newUrl.origin }, queueLimit: 1 });
|
html = await this.fetcher.text(ctx, newUrl, { queueLimit: 1 });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (tlds.length && (error instanceof TooManyRequestsError || error instanceof BlockedError)) {
|
/* istanbul ignore next */
|
||||||
return this.extractUsingRandomTld(ctx, url, meta, tlds);
|
if (domains.length && (error instanceof TooManyRequestsError || error instanceof BlockedError)) {
|
||||||
|
return this.extractUsingRandomDomain(ctx, url, meta, domains);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* istanbul ignore next */
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
||||||
const $ = cheerio.load(html);
|
const $ = cheerio.load(html.replace('<!--', '').replace('-->', '')); // server HTML is commented-out
|
||||||
|
|
||||||
const iframeUrl = new URL(($('#player_iframe').attr('src') as string).replace(/^\/\//, 'https://'));
|
const iframeUrl = new URL(($('#player_iframe').attr('src') as string).replace(/^\/\//, 'https://'));
|
||||||
const headers = { Referer: iframeUrl.origin };
|
|
||||||
const title = $('title').text().trim();
|
const title = $('title').text().trim();
|
||||||
|
|
||||||
return Promise.all(
|
return Promise.all(
|
||||||
|
|
@ -60,26 +62,21 @@ export class VidSrc extends Extractor {
|
||||||
.toArray()
|
.toArray()
|
||||||
.filter(({ serverName }) => serverName === 'CloudStream Pro')
|
.filter(({ serverName }) => serverName === 'CloudStream Pro')
|
||||||
.map(async ({ serverName, dataHash }) => {
|
.map(async ({ serverName, dataHash }) => {
|
||||||
const iframeHtml = await this.fetcher.text(ctx, new URL(`/rcp/${dataHash}`, iframeUrl.origin), { headers });
|
const rcpUrl = new URL(`/rcp/${dataHash}`, iframeUrl.origin);
|
||||||
|
const iframeHtml = await this.fetcher.text(ctx, rcpUrl, { headers: { Referer: newUrl.origin } });
|
||||||
const srcMatch = iframeHtml.match(`src:\\s?'(.*)'`) as string[];
|
const srcMatch = iframeHtml.match(`src:\\s?'(.*)'`) as string[];
|
||||||
|
|
||||||
const playerHtml = await this.fetcher.text(ctx, new URL(srcMatch[1] as string, iframeUrl.origin), { headers });
|
const playerHtml = await this.fetcher.text(ctx, new URL(srcMatch[1] as string, iframeUrl.origin), { headers: { Referer: rcpUrl.href } });
|
||||||
const fileMatch = playerHtml.match(`file:\\s?'(.*)'`);
|
const fileMatch = playerHtml.match(`(https:\\/\\/.*?{v\\d}.*?) or`) as string[];
|
||||||
if (!fileMatch) {
|
const m3u8Url = new URL((fileMatch[1] as string).replace(/{v\d}/, iframeUrl.host));
|
||||||
throw new NotFoundError();
|
|
||||||
}
|
|
||||||
|
|
||||||
const m3u8Url = new URL(fileMatch[1] as string);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
url: m3u8Url,
|
url: m3u8Url,
|
||||||
format: Format.hls,
|
format: Format.hls,
|
||||||
label: `${this.label} (${serverName})`,
|
label: serverName,
|
||||||
sourceId: `${this.id}_${slugify(serverName)}_${meta.countryCodes?.join('_')}`,
|
|
||||||
ttl: this.ttl,
|
|
||||||
meta: {
|
meta: {
|
||||||
...meta,
|
...meta,
|
||||||
height: await guessHeightFromPlaylist(ctx, this.fetcher, m3u8Url),
|
height: await guessHeightFromPlaylist(ctx, this.fetcher, m3u8Url, { headers: { Referer: iframeUrl.href } }),
|
||||||
title,
|
title,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
||||||
20
src/extractor/Vidora.test.ts
Normal file
20
src/extractor/Vidora.test.ts
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
import winston from 'winston';
|
||||||
|
import { createTestContext } from '../test';
|
||||||
|
import { FetcherMock } from '../utils';
|
||||||
|
import { ExtractorRegistry } from './ExtractorRegistry';
|
||||||
|
import { Vidora } from './Vidora';
|
||||||
|
|
||||||
|
const logger = winston.createLogger({ transports: [new winston.transports.Console({ level: 'nope' })] });
|
||||||
|
const extractorRegistry = new ExtractorRegistry(logger, [new Vidora(new FetcherMock(`${__dirname}/__fixtures__/Vidora`))]);
|
||||||
|
|
||||||
|
const ctx = createTestContext();
|
||||||
|
|
||||||
|
describe('Vidora', () => {
|
||||||
|
test('vidora.stream', async () => {
|
||||||
|
expect(await extractorRegistry.handle(ctx, new URL('https://vidora.stream/5iz6c1elaqgq'))).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('vidora.stream /embed/', async () => {
|
||||||
|
expect(await extractorRegistry.handle(ctx, new URL('https://vidora.stream/embed/5iz6c1elaqgq'))).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
});
|
||||||
43
src/extractor/Vidora.ts
Normal file
43
src/extractor/Vidora.ts
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
import * as cheerio from 'cheerio';
|
||||||
|
import { Context, Format, InternalUrlResult, Meta } from '../types';
|
||||||
|
import { extractUrlFromPacked, guessHeightFromPlaylist } from '../utils';
|
||||||
|
import { Extractor } from './Extractor';
|
||||||
|
|
||||||
|
export class Vidora extends Extractor {
|
||||||
|
public readonly id = 'vidora';
|
||||||
|
|
||||||
|
public readonly label = 'Vidora';
|
||||||
|
|
||||||
|
public override readonly ttl: number = 43200000; // 12h
|
||||||
|
|
||||||
|
public supports(_ctx: Context, url: URL): boolean {
|
||||||
|
return null !== url.host.match(/vidora/);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override normalize(url: URL): URL {
|
||||||
|
return new URL(url.href.replace('/embed/', '/'));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async extractInternal(ctx: Context, url: URL, meta: Meta): Promise<InternalUrlResult[]> {
|
||||||
|
const html = await this.fetcher.text(ctx, url);
|
||||||
|
|
||||||
|
const $ = cheerio.load(html);
|
||||||
|
const title = $('title').text().trim().replace(/^Watch /, '').trim();
|
||||||
|
|
||||||
|
const m3u8Url = extractUrlFromPacked(html, [/file: ?"(.*?)"/]);
|
||||||
|
const headers = { Origin: url.origin };
|
||||||
|
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
url: m3u8Url,
|
||||||
|
format: Format.hls,
|
||||||
|
meta: {
|
||||||
|
...meta,
|
||||||
|
height: meta.height ?? await guessHeightFromPlaylist(ctx, this.fetcher, m3u8Url, { headers }),
|
||||||
|
title,
|
||||||
|
},
|
||||||
|
requestHeaders: headers,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -7,7 +7,7 @@ import { VixSrc } from './VixSrc';
|
||||||
const logger = winston.createLogger({ transports: [new winston.transports.Console({ level: 'nope' })] });
|
const logger = winston.createLogger({ transports: [new winston.transports.Console({ level: 'nope' })] });
|
||||||
const extractorRegistry = new ExtractorRegistry(logger, [new VixSrc(new FetcherMock(`${__dirname}/__fixtures__/VixSrc`))]);
|
const extractorRegistry = new ExtractorRegistry(logger, [new VixSrc(new FetcherMock(`${__dirname}/__fixtures__/VixSrc`))]);
|
||||||
|
|
||||||
const ctx = createTestContext({ mediaFlowProxyUrl: 'https://mediaflow-proxy.test', mediaFlowProxyPassword: 'asdfg', de: 'on', en: 'on', it: 'on' });
|
const ctx = createTestContext();
|
||||||
|
|
||||||
describe('VixSrc', () => {
|
describe('VixSrc', () => {
|
||||||
test('Full Metal Jacket', async () => {
|
test('Full Metal Jacket', async () => {
|
||||||
|
|
@ -18,9 +18,13 @@ describe('VixSrc', () => {
|
||||||
expect(await extractorRegistry.handle(ctx, new URL('https://vixsrc.to/tv/42009/4/2'))).toMatchSnapshot();
|
expect(await extractorRegistry.handle(ctx, new URL('https://vixsrc.to/tv/42009/4/2'))).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Black Mirror is excluded if no matching language was found', async () => {
|
test('Rental Family', async () => {
|
||||||
const ctx = createTestContext({ mediaFlowProxyUrl: 'https://mediaflow-proxy2.test', mediaFlowProxyPassword: 'asdfg', de: 'on' });
|
expect(await extractorRegistry.handle(ctx, new URL('https://vixsrc.to/movie/1208348'))).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
expect(await extractorRegistry.handle(ctx, new URL('https://vixsrc.to/tv/42009/4/2'))).toMatchSnapshot();
|
test('Black Mirror is excluded if no matching language was found', async () => {
|
||||||
|
const ctx = createTestContext({ de: 'on' });
|
||||||
|
|
||||||
|
expect(await extractorRegistry.handle(ctx, new URL('https://vixsrc.to/tv/42009/4/3'))).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,26 +1,34 @@
|
||||||
import { Context, CountryCode, Format, Meta, UrlResult } from '../types';
|
import { Context, CountryCode, Format, InternalUrlResult, Meta } from '../types';
|
||||||
import {
|
import { CustomRequestConfig, guessHeightFromPlaylist, hasMultiEnabled, iso639FromCountryCode } from '../utils';
|
||||||
buildMediaFlowProxyExtractorStreamUrl, guessHeightFromPlaylist,
|
|
||||||
hasMultiEnabled,
|
|
||||||
iso639FromCountryCode,
|
|
||||||
supportsMediaFlowProxy,
|
|
||||||
} from '../utils';
|
|
||||||
import { Extractor } from './Extractor';
|
import { Extractor } from './Extractor';
|
||||||
|
|
||||||
export class VixSrc extends Extractor {
|
export class VixSrc extends Extractor {
|
||||||
public readonly id = 'vixsrc';
|
public readonly id = 'vixsrc';
|
||||||
|
|
||||||
public readonly label = 'VixSrc (via MediaFlow Proxy)';
|
public readonly label = 'VixSrc';
|
||||||
|
|
||||||
public override viaMediaFlowProxy = true;
|
public override readonly ttl: number = 21600000; // 6h
|
||||||
|
|
||||||
public supports(ctx: Context, url: URL): boolean {
|
public supports(_ctx: Context, url: URL): boolean {
|
||||||
return null !== url.host.match(/vixsrc/) && supportsMediaFlowProxy(ctx);
|
return null !== url.host.match(/vixsrc/);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async extractInternal(ctx: Context, url: URL, meta: Meta): Promise<UrlResult[]> {
|
protected async extractInternal(ctx: Context, url: URL, meta: Meta): Promise<InternalUrlResult[]> {
|
||||||
const playlistUrl = await buildMediaFlowProxyExtractorStreamUrl(ctx, this.fetcher, 'VixCloud', url);
|
const headers = { Referer: url.href };
|
||||||
const countryCodes = await this.determineCountryCodesFromPlaylist(ctx, playlistUrl);
|
|
||||||
|
const html = await this.fetcher.text(ctx, url);
|
||||||
|
|
||||||
|
const tokenMatch = html.match(/['"]token['"]: ?['"](.*?)['"]/) as string[];
|
||||||
|
const expiresMatch = html.match(/['"]expires['"]: ?['"](.*?)['"]/) as string[];
|
||||||
|
const urlMatch = html.match(/url: ?['"](.*?)['"]/) as string[];
|
||||||
|
|
||||||
|
const baseUrl = new URL(`${urlMatch[1]}`);
|
||||||
|
const playlistUrl = new URL(`${baseUrl.origin}${baseUrl.pathname}.m3u8?${baseUrl.searchParams}`);
|
||||||
|
playlistUrl.searchParams.append('token', tokenMatch[1] as string);
|
||||||
|
playlistUrl.searchParams.append('expires', expiresMatch[1] as string);
|
||||||
|
playlistUrl.searchParams.append('h', '1');
|
||||||
|
|
||||||
|
const countryCodes = meta.countryCodes ?? [CountryCode.multi, ...(await this.determineCountryCodesFromPlaylist(ctx, playlistUrl, { headers }))];
|
||||||
|
|
||||||
if (!hasMultiEnabled(ctx.config) && !countryCodes.some(countryCode => countryCode in ctx.config)) {
|
if (!hasMultiEnabled(ctx.config) && !countryCodes.some(countryCode => countryCode in ctx.config)) {
|
||||||
return [];
|
return [];
|
||||||
|
|
@ -30,21 +38,19 @@ export class VixSrc extends Extractor {
|
||||||
{
|
{
|
||||||
url: playlistUrl,
|
url: playlistUrl,
|
||||||
format: Format.hls,
|
format: Format.hls,
|
||||||
label: this.label,
|
|
||||||
sourceId: `${this.id}_${meta.countryCodes?.join('_')}`,
|
|
||||||
ttl: this.ttl,
|
|
||||||
meta: {
|
meta: {
|
||||||
|
...meta,
|
||||||
countryCodes,
|
countryCodes,
|
||||||
height: await guessHeightFromPlaylist(ctx, this.fetcher, playlistUrl),
|
height: meta.height ?? await guessHeightFromPlaylist(ctx, this.fetcher, playlistUrl, { headers }),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
private async determineCountryCodesFromPlaylist(ctx: Context, playlistUrl: URL): Promise<CountryCode[]> {
|
private async determineCountryCodesFromPlaylist(ctx: Context, playlistUrl: URL, init?: CustomRequestConfig): Promise<CountryCode[]> {
|
||||||
const playlist = await this.fetcher.text(ctx, playlistUrl);
|
const playlist = await this.fetcher.text(ctx, playlistUrl, init);
|
||||||
|
|
||||||
const countryCodes: CountryCode[] = [CountryCode.it];
|
const countryCodes: CountryCode[] = [];
|
||||||
|
|
||||||
(Object.keys(CountryCode) as CountryCode[]).forEach((countryCode) => {
|
(Object.keys(CountryCode) as CountryCode[]).forEach((countryCode) => {
|
||||||
const iso639 = iso639FromCountryCode(countryCode);
|
const iso639 = iso639FromCountryCode(countryCode);
|
||||||
|
|
|
||||||
28
src/extractor/Voe.test.ts
Normal file
28
src/extractor/Voe.test.ts
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
import winston from 'winston';
|
||||||
|
import { createTestContext } from '../test';
|
||||||
|
import { FetcherMock } from '../utils';
|
||||||
|
import { ExtractorRegistry } from './ExtractorRegistry';
|
||||||
|
import { Voe } from './Voe';
|
||||||
|
|
||||||
|
const logger = winston.createLogger({ transports: [new winston.transports.Console({ level: 'nope' })] });
|
||||||
|
const extractorRegistry = new ExtractorRegistry(logger, [new Voe(new FetcherMock(`${__dirname}/__fixtures__/Voe`))]);
|
||||||
|
|
||||||
|
const ctx = createTestContext({ mediaFlowProxyUrl: 'https://mediaflow.test.org', mediaFlowProxyPassword: 'test' });
|
||||||
|
|
||||||
|
describe('Voe', () => {
|
||||||
|
test('jilliandescribecompany', async () => {
|
||||||
|
expect(await extractorRegistry.handle(ctx, new URL('https://jilliandescribecompany.com/e/ea21l02gcygw'))).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('premium only without resolution', async () => {
|
||||||
|
expect(await extractorRegistry.handle(ctx, new URL('https://jilliandescribecompany.com/qqfyi04w52mj'))).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('encoding error', async () => {
|
||||||
|
expect(await extractorRegistry.handle(ctx, new URL('https://voe.sx/e/c2yxvit4f6bz'))).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('embed only urls which otherwise lead to 404', async () => {
|
||||||
|
expect(await extractorRegistry.handle(ctx, new URL('https://mikaylaarealike.com/e/gqlhm9hbobwu'))).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
});
|
||||||
180
src/extractor/Voe.ts
Normal file
180
src/extractor/Voe.ts
Normal file
|
|
@ -0,0 +1,180 @@
|
||||||
|
import bytes from 'bytes';
|
||||||
|
import * as cheerio from 'cheerio';
|
||||||
|
import { NotFoundError } from '../error';
|
||||||
|
import { Context, Format, InternalUrlResult, Meta } from '../types';
|
||||||
|
import {
|
||||||
|
buildMediaFlowProxyExtractorStreamUrl, guessHeightFromPlaylist,
|
||||||
|
supportsMediaFlowProxy,
|
||||||
|
} from '../utils';
|
||||||
|
import { Extractor } from './Extractor';
|
||||||
|
|
||||||
|
/** @see https://github.com/Gujal00/ResolveURL/blob/master/script.module.resolveurl/lib/resolveurl/plugins/voesx.py */
|
||||||
|
export class Voe extends Extractor {
|
||||||
|
public readonly id = 'voe';
|
||||||
|
|
||||||
|
public readonly label = 'VOE';
|
||||||
|
|
||||||
|
public override viaMediaFlowProxy = true;
|
||||||
|
|
||||||
|
public supports(ctx: Context, url: URL): boolean {
|
||||||
|
const supportedDomain = null !== url.host.match(/voe/)
|
||||||
|
|| [
|
||||||
|
'19turanosephantasia.com',
|
||||||
|
'20demidistance9elongations.com',
|
||||||
|
'30sensualizeexpression.com',
|
||||||
|
'321naturelikefurfuroid.com',
|
||||||
|
'35volitantplimsoles5.com',
|
||||||
|
'449unceremoniousnasoseptal.com',
|
||||||
|
'745mingiestblissfully.com',
|
||||||
|
'adrianmissionminute.com',
|
||||||
|
'alleneconomicmatter.com',
|
||||||
|
'antecoxalbobbing1010.com',
|
||||||
|
'apinchcaseation.com',
|
||||||
|
'audaciousdefaulthouse.com',
|
||||||
|
'availedsmallest.com',
|
||||||
|
'bigclatterhomesguideservice.com',
|
||||||
|
'boonlessbestselling244.com',
|
||||||
|
'bradleyviewdoctor.com',
|
||||||
|
'brittneystandardwestern.com',
|
||||||
|
'brucevotewithin.com',
|
||||||
|
'christopheruntilpoint.com',
|
||||||
|
'chromotypic.com',
|
||||||
|
'chuckle-tube.com',
|
||||||
|
'cindyeyefinal.com',
|
||||||
|
'counterclockwisejacky.com',
|
||||||
|
'crownmakermacaronicism.com',
|
||||||
|
'crystaltreatmenteast.com',
|
||||||
|
'cyamidpulverulence530.com',
|
||||||
|
'diananatureforeign.com',
|
||||||
|
'donaldlineelse.com',
|
||||||
|
'edwardarriveoften.com',
|
||||||
|
'erikcoldperson.com',
|
||||||
|
'figeterpiazine.com',
|
||||||
|
'fittingcentermondaysunday.com',
|
||||||
|
'fraudclatterflyingcar.com',
|
||||||
|
'gamoneinterrupted.com',
|
||||||
|
'generatesnitrosate.com',
|
||||||
|
'goofy-banana.com',
|
||||||
|
'graceaddresscommunity.com',
|
||||||
|
'greaseball6eventual20.com',
|
||||||
|
'guidon40hyporadius9.com',
|
||||||
|
'heatherdiscussionwhen.com',
|
||||||
|
'housecardsummerbutton.com',
|
||||||
|
'jamessoundcost.com',
|
||||||
|
'jamiesamewalk.com',
|
||||||
|
'jasminetesttry.com',
|
||||||
|
'jayservicestuff.com',
|
||||||
|
'jennifercertaindevelopment.com',
|
||||||
|
'jilliandescribecompany.com',
|
||||||
|
'johnalwayssame.com',
|
||||||
|
'jonathansociallike.com',
|
||||||
|
'josephseveralconcern.com',
|
||||||
|
'kathleenmemberhistory.com',
|
||||||
|
'kellywhatcould.com',
|
||||||
|
'kennethofficialitem.com',
|
||||||
|
'kinoger.ru',
|
||||||
|
'kristiesoundsimply.com',
|
||||||
|
'lancewhosedifficult.com',
|
||||||
|
'launchreliantcleaverriver.com',
|
||||||
|
'lauradaydo.com',
|
||||||
|
'lisatrialidea.com',
|
||||||
|
'loriwithinfamily.com',
|
||||||
|
'lukecomparetwo.com',
|
||||||
|
'lukesitturn.com',
|
||||||
|
'mariatheserepublican.com',
|
||||||
|
'matriculant401merited.com',
|
||||||
|
'maxfinishseveral.com',
|
||||||
|
'metagnathtuggers.com',
|
||||||
|
'michaelapplysome.com',
|
||||||
|
'mikaylaarealike.com',
|
||||||
|
'nathanfromsubject.com',
|
||||||
|
'nectareousoverelate.com',
|
||||||
|
'nonesnanking.com',
|
||||||
|
'paulkitchendark.com',
|
||||||
|
'realfinanceblogcenter.com',
|
||||||
|
'rebeccaneverbase.com',
|
||||||
|
'reputationsheriffkennethsand.com',
|
||||||
|
'richardsignfish.com',
|
||||||
|
'roberteachfinal.com',
|
||||||
|
'robertordercharacter.com',
|
||||||
|
'robertplacespace.com',
|
||||||
|
'sandratableother.com',
|
||||||
|
'sandrataxeight.com',
|
||||||
|
'scatch176duplicities.com',
|
||||||
|
'sethniceletter.com',
|
||||||
|
'shannonpersonalcost.com',
|
||||||
|
'simpulumlamerop.com',
|
||||||
|
'smoki.cc',
|
||||||
|
'stevenimaginelittle.com',
|
||||||
|
'strawberriesporail.com',
|
||||||
|
'telyn610zoanthropy.com',
|
||||||
|
'timberwoodanotia.com',
|
||||||
|
'toddpartneranimal.com',
|
||||||
|
'toxitabellaeatrebates306.com',
|
||||||
|
'uptodatefinishconferenceroom.com',
|
||||||
|
'v-o-e-unblock.com',
|
||||||
|
'valeronevijao.com',
|
||||||
|
'walterprettytheir.com',
|
||||||
|
'wolfdyslectic.com',
|
||||||
|
'yodelswartlike.com',
|
||||||
|
].includes(url.host);
|
||||||
|
|
||||||
|
return supportedDomain && supportsMediaFlowProxy(ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override normalize(url: URL): URL {
|
||||||
|
return new URL(`/${url.pathname.replace(/\/+$/, '').split('/').at(-1)}`, url);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async extractInternal(ctx: Context, url: URL, meta: Meta): Promise<InternalUrlResult[]> {
|
||||||
|
const headers = { Referer: meta.referer ?? url.href };
|
||||||
|
|
||||||
|
let html: string;
|
||||||
|
try {
|
||||||
|
html = await this.fetcher.text(ctx, url, { headers });
|
||||||
|
} catch (error) {
|
||||||
|
/* istanbul ignore next */
|
||||||
|
if (error instanceof NotFoundError && !url.href.includes('/e/')) {
|
||||||
|
return await this.extractInternal(ctx, new URL(`/e${url.pathname}`, url.origin), meta);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* istanbul ignore next */
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
const redirectMatch = html.match(/window\.location\.href\s*=\s*'([^']+)/);
|
||||||
|
if (redirectMatch && redirectMatch[1]) {
|
||||||
|
return await this.extractInternal(ctx, new URL(redirectMatch[1]), meta);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (/An error occurred during encoding/.test(html)) {
|
||||||
|
throw new NotFoundError();
|
||||||
|
}
|
||||||
|
|
||||||
|
const $ = cheerio.load(html);
|
||||||
|
const title = $('meta[name="description"]').attr('content')?.trim().replace(/^Watch /, '').replace(/ at VOE$/, '').trim();
|
||||||
|
|
||||||
|
const sizeMatch = html.matchAll(/[\d.]+ ?[GM]B/g).toArray().at(-1);
|
||||||
|
const size = sizeMatch ? bytes.parse(sizeMatch[0] as string) as number : /* istanbul ignore next */null;
|
||||||
|
|
||||||
|
const playlistUrl = await buildMediaFlowProxyExtractorStreamUrl(ctx, this.fetcher, 'Voe', url, headers);
|
||||||
|
|
||||||
|
const heightMatch = html.match(/<b>(\d{3,})p<\/b>/);
|
||||||
|
const height = heightMatch
|
||||||
|
? parseInt(heightMatch[1] as string)
|
||||||
|
: meta.height ?? await guessHeightFromPlaylist(ctx, this.fetcher, playlistUrl);
|
||||||
|
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
url: playlistUrl,
|
||||||
|
format: Format.hls,
|
||||||
|
meta: {
|
||||||
|
...meta,
|
||||||
|
height,
|
||||||
|
title,
|
||||||
|
...(size && size > 16777216 && { bytes: size }),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -1,28 +0,0 @@
|
||||||
import winston from 'winston';
|
|
||||||
import { createTestContext } from '../test';
|
|
||||||
import { FetcherMock } from '../utils';
|
|
||||||
import { ExtractorRegistry } from './ExtractorRegistry';
|
|
||||||
import { XPrime } from './XPrime';
|
|
||||||
|
|
||||||
const logger = winston.createLogger({ transports: [new winston.transports.Console({ level: 'nope' })] });
|
|
||||||
const extractorRegistry = new ExtractorRegistry(logger, [new XPrime(new FetcherMock(`${__dirname}/__fixtures__/XPrime`))]);
|
|
||||||
|
|
||||||
const ctx = createTestContext();
|
|
||||||
|
|
||||||
describe('XPrime', () => {
|
|
||||||
test('unknown backend is ignored', async () => {
|
|
||||||
expect(await extractorRegistry.handle(ctx, new URL('https://backend.xprime.tv/unknown?name=Superman&year=2025&id=1061474&imdbId=tt5950044'))).toMatchSnapshot();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('primebox Superman', async () => {
|
|
||||||
expect(await extractorRegistry.handle(ctx, new URL('https://backend.xprime.tv/primebox?name=Superman&year=2025&id=1061474&imdbId=tt5950044'))).toMatchSnapshot();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('primebox Alien: Earth', async () => {
|
|
||||||
expect(await extractorRegistry.handle(ctx, new URL('https://backend.xprime.tv/primebox?name=Alien%3A%20Earth&year=2025&id=157239&imdb=tt13623632&season=1&episode=1'))).toMatchSnapshot();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('primebox not found', async () => {
|
|
||||||
expect(await extractorRegistry.handle(ctx, new URL('https://backend.xprime.tv/primebox?name=Jurassic%20World%20Rebirth&year=2025'))).toMatchSnapshot();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
@ -1,62 +0,0 @@
|
||||||
import { Context, Format, Meta, UrlResult } from '../types';
|
|
||||||
import { guessSizeFromMp4 } from '../utils/size';
|
|
||||||
import { Extractor } from './Extractor';
|
|
||||||
|
|
||||||
interface XPrimePrimeboxResponsePartial {
|
|
||||||
streams: Record<string, string>;
|
|
||||||
series_info?: {
|
|
||||||
episode: number;
|
|
||||||
season: number;
|
|
||||||
title: string;
|
|
||||||
year: string;
|
|
||||||
};
|
|
||||||
title?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class XPrime extends Extractor {
|
|
||||||
public readonly id = 'xprime';
|
|
||||||
|
|
||||||
public readonly label = 'XPrime';
|
|
||||||
|
|
||||||
public override readonly ttl: number = 21600000; // 6h
|
|
||||||
|
|
||||||
public supports(_ctx: Context, url: URL): boolean {
|
|
||||||
return null !== url.host.match(/xprime/);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected async extractInternal(ctx: Context, url: URL, meta: Meta): Promise<UrlResult[]> {
|
|
||||||
const urlResults: UrlResult[] = [];
|
|
||||||
|
|
||||||
const headers = {
|
|
||||||
Referer: url.protocol + '//' + url.hostname.split('.').slice(-2).join('.'), // Strip subdomains
|
|
||||||
};
|
|
||||||
|
|
||||||
if (url.href.includes('primebox')) {
|
|
||||||
const jsonResponse = JSON.parse(await this.fetcher.text(ctx, url)) as XPrimePrimeboxResponsePartial;
|
|
||||||
|
|
||||||
for (const [resolution, stream] of Object.entries(jsonResponse.streams)) {
|
|
||||||
const url = new URL(stream);
|
|
||||||
const title = jsonResponse.series_info
|
|
||||||
? `${jsonResponse.series_info.title} ${jsonResponse.series_info.season}x${jsonResponse.series_info.episode} (${jsonResponse.series_info.year})`
|
|
||||||
: jsonResponse.title;
|
|
||||||
|
|
||||||
urlResults.push({
|
|
||||||
url,
|
|
||||||
format: Format.mp4,
|
|
||||||
label: `${this.label} Primebox`,
|
|
||||||
sourceId: `${this.id}_primebox_${meta.countryCodes?.join('_')}`,
|
|
||||||
ttl: this.ttl,
|
|
||||||
meta: {
|
|
||||||
...meta,
|
|
||||||
bytes: await guessSizeFromMp4(ctx, this.fetcher, url, { headers, minCacheTtl: this.ttl }),
|
|
||||||
height: parseInt(resolution),
|
|
||||||
title,
|
|
||||||
},
|
|
||||||
requestHeaders: headers,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return urlResults;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { Context, Format, Meta, UrlResult } from '../types';
|
import { Context, Format, InternalUrlResult, Meta } from '../types';
|
||||||
import { Extractor } from './Extractor';
|
import { Extractor } from './Extractor';
|
||||||
|
|
||||||
export class YouTube extends Extractor {
|
export class YouTube extends Extractor {
|
||||||
|
|
@ -12,8 +12,10 @@ export class YouTube extends Extractor {
|
||||||
return null !== url.host.match(/youtube/) && url.searchParams.has('v');
|
return null !== url.host.match(/youtube/) && url.searchParams.has('v');
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async extractInternal(ctx: Context, url: URL, meta: Meta): Promise<UrlResult[]> {
|
protected async extractInternal(ctx: Context, url: URL, meta: Meta): Promise<InternalUrlResult[]> {
|
||||||
const html = await this.fetcher.text(ctx, url);
|
const headers = { Referer: meta.referer ?? url.href };
|
||||||
|
|
||||||
|
const html = await this.fetcher.text(ctx, url, { headers });
|
||||||
|
|
||||||
const titleMatch = html.match(/"title":{"runs":\[{"text":"(.*?)"/) as string[];
|
const titleMatch = html.match(/"title":{"runs":\[{"text":"(.*?)"/) as string[];
|
||||||
|
|
||||||
|
|
@ -22,9 +24,6 @@ export class YouTube extends Extractor {
|
||||||
url,
|
url,
|
||||||
format: Format.unknown,
|
format: Format.unknown,
|
||||||
ytId: url.searchParams.get('v') as string,
|
ytId: url.searchParams.get('v') as string,
|
||||||
label: this.label,
|
|
||||||
sourceId: `${this.id}_${meta.countryCodes?.join('_')}`,
|
|
||||||
ttl: this.ttl,
|
|
||||||
meta: {
|
meta: {
|
||||||
...meta,
|
...meta,
|
||||||
title: titleMatch[1] as string,
|
title: titleMatch[1] as string,
|
||||||
|
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
{"server":"nginx","date":"Sun, 08 Jun 2025 19:20:59 GMT","content-type":"video/mp4","content-length":791702409,"last-modified":"Wed, 03 Apr 2024 09:53:31 GMT","connection":"keep-alive","etag":"660d271b-2f306b89","access-control-allow-origin":"*","accept-ranges":"bytes"}
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
{}
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
{}
|
|
||||||
2
src/extractor/__fixtures__/DoodStream/http:dood.toddfx8me4un4ul
generated
Normal file
2
src/extractor/__fixtures__/DoodStream/http:dood.toddfx8me4un4ul
generated
Normal file
File diff suppressed because one or more lines are too long
2
src/extractor/__fixtures__/DoodStream/http:dood.todsk1m9eumzyjj
generated
Normal file
2
src/extractor/__fixtures__/DoodStream/http:dood.todsk1m9eumzyjj
generated
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -1 +1,2 @@
|
||||||
<!doctype html><html lang="en"><head> <meta charset="utf-8"> <title>Video not found | DoodStream</title> <link rel="preconnect" href="//i.doodcdn.io"><link rel="preconnect" href="//cdnjs.cloudflare.com"> <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> <script src="//cdnjs.cloudflare.com/ajax/libs/jquery/3.4.1/jquery.min.js"></script> <link rel="stylesheet" href="//i.doodcdn.io/theme_2/css/bootstrap.min.css"> <link rel="stylesheet" type="text/css" href="//i.doodcdn.io/theme_2/css/style.css" /></head><body> <style type="text/css"> .not_found img {height:250px !important;} @media (max-width: 768px){.not_found h1{font-size:1.8rem;}.not_found img {height:200px !important;margin: 0 auto 15px;}} @media (max-width: 480px){.not_found h1{font-size:1.4rem;}.not_found img {height:120px !important;margin: 0 auto 15px;}.not_found p{margin-top: -5px;}} </style> <div class="container pt-5 not_found"> <div class="row"> <div class="col-md-12 pt-2 text-center"> <img src="//i.doodcdn.io/img/no_video_3.svg"> <h1>Not Found</h1> <p>video you are looking for is not found.</p> </div> </div> </div><script data-cfasync="false" src="//d1f05vr3sjsuy7.cloudfront.net/?srvfd=908056"></script><script data-cfasync="false" async type="text/javascript" src="//faqirsgoliard.top/gHzOaAdOhbZ/71405"></script><script data-cfasync="false" async type="text/javascript" src="//faqirsgoliard.top/r67c0fc81985e5/70849"></script><script type="text/javascript" data-cfasync="false">/*<![CDATA[/* */(function(){var g=window,r="ca0967284e67bc53cb5b626985619403",w=[["siteId",317+185*82*507-3600486],["minBid",0],["popundersPerIP","0"],["delayBetween",300],["default","https://strettechoco.com/iYa7dlULceKw/27615"],["defaultPerDay",0],["topmostLayer","never"]],n=["d3d3LmJsb2NrYWRzbm90LmNvbS9iYWphLm1pbi5jc3M=","ZG5oZmk1bm4yZHQ2Ny5jbG91ZGZyb250Lm5ldC9RVHd0eC91YWxnZWJyYS5taW4uanM=","d3d3LmJibHZwbGd1b21hLmNvbS9mYWphLm1pbi5jc3M=","d3d3LndqZ3BzcGt4empiZGd1LmNvbS9NTmlIWS9rYWxnZWJyYS5taW4uanM="],f=-1,v,q,h=function(){clearTimeout(q);f++;if(n[f]&&!(1766620801000<(new Date).getTime()&&1<f)){v=g.document.createElement("script");v.type="text/javascript";v.async=!0;var o=g.document.getElementsByTagName("script")[0];v.src="https://"+atob(n[f]);v.crossOrigin="anonymous";v.onerror=h;v.onload=function(){clearTimeout(q);g[r.slice(0,16)+r.slice(0,16)]||h()};q=setTimeout(h,5E3);o.parentNode.insertBefore(v,o)}};if(!g[r]){try{Object.freeze(g[r]=w)}catch(e){}h()}})();/*]]>/* */</script></body></html>
|
<!doctype html><html lang="en"><head> <meta charset="utf-8"> <title>Video not found | DoodStream</title> <link rel="preconnect" href="//i.doodcdn.io"><link rel="preconnect" href="//cdnjs.cloudflare.com"> <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> <script src="//cdnjs.cloudflare.com/ajax/libs/jquery/3.4.1/jquery.min.js"></script> <link rel="stylesheet" href="//i.doodcdn.io/theme_2/css/bootstrap.min.css"> <link rel="stylesheet" type="text/css" href="//i.doodcdn.io/theme_2/css/style.css" /></head><body> <style type="text/css"> .not_found img {height:250px !important;} @media (max-width: 768px){.not_found h1{font-size:1.8rem;}.not_found img {height:200px !important;margin: 0 auto 15px;}} @media (max-width: 480px){.not_found h1{font-size:1.4rem;}.not_found img {height:120px !important;margin: 0 auto 15px;}.not_found p{margin-top: -5px;}} </style> <div class="container pt-5 not_found"> <div class="row"> <div class="col-md-12 pt-2 text-center"> <img src="//i.doodcdn.io/img/no_video_3.svg"> <h1>Not Found</h1> <p>video you are looking for is not found.</p> </div> </div> </div><script data-cfasync="false" src="//d1f05vr3sjsuy7.cloudfront.net/?srvfd=908056"></script><script data-cfasync="false" async type="text/javascript" src="//faqirsgoliard.top/gHzOaAdOhbZ/71405"></script><script data-cfasync="false" async type="text/javascript" src="//faqirsgoliard.top/r67c0fc81985e5/70849"></script><script type="text/javascript" data-cfasync="false">/*<![CDATA[/* */(function(){var g=window,r="ca0967284e67bc53cb5b626985619403",w=[["siteId",317+185*82*507-3600486],["minBid",0],["popundersPerIP","0"],["delayBetween",300],["default","https://strettechoco.com/iYa7dlULceKw/27615"],["defaultPerDay",0],["topmostLayer","never"]],n=["d3d3LmJsb2NrYWRzbm90LmNvbS9iYWphLm1pbi5jc3M=","ZG5oZmk1bm4yZHQ2Ny5jbG91ZGZyb250Lm5ldC9RVHd0eC91YWxnZWJyYS5taW4uanM=","d3d3LmJibHZwbGd1b21hLmNvbS9mYWphLm1pbi5jc3M=","d3d3LndqZ3BzcGt4empiZGd1LmNvbS9NTmlIWS9rYWxnZWJyYS5taW4uanM="],f=-1,v,q,h=function(){clearTimeout(q);f++;if(n[f]&&!(1766620801000<(new Date).getTime()&&1<f)){v=g.document.createElement("script");v.type="text/javascript";v.async=!0;var o=g.document.getElementsByTagName("script")[0];v.src="https://"+atob(n[f]);v.crossOrigin="anonymous";v.onerror=h;v.onload=function(){clearTimeout(q);g[r.slice(0,16)+r.slice(0,16)]||h()};q=setTimeout(h,5E3);o.parentNode.insertBefore(v,o)}};if(!g[r]){try{Object.freeze(g[r]=w)}catch(e){}h()}})();/*]]>/* */</script><script defer src="https://static.cloudflareinsights.com/beacon.min.js/vcd15cbe7772f49c399c6a5babf22c1241717689176015" integrity="sha512-ZpsOmlRQV6y907TI0dKBHq9Md29nnaEIPlkf84rnaERnq6zvWvPUqr2ft8M1aS28oN72PdrCzSjY4U6VaAw1EQ==" data-cf-beacon='{"version":"2024.11.0","token":"1c76230f8ab248eab0b0f2bf7c205c51","server_timing":{"name":{"cfCacheStatus":true,"cfEdge":true,"cfExtPri":true,"cfL4":true,"cfOrigin":true,"cfSpeedBrain":true},"location_startswith":null}}' crossorigin="anonymous"></script>
|
||||||
|
</body></html>
|
||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -1 +0,0 @@
|
||||||
https://ijs155l.cloudatacdn.com/u5kj2rz5qtdlsdgge4hrgj2flz5cjodea6auatveaphxogr2vvukxfzmwzva/egprvdjvpr~
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
https://ee317r.cloudatacdn.com/u5kj63yvxddlsdgge7qremqqka27irwiupc4k7o5cikbhjliz6xwv2xr53uq/lco4d3heif~
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
https://wec6bnh7rx7rw11bv9e9vslw4rud8nam.5f1ebd98099ce35faeeddb30c1752191.r2.cloudflarestorage.com/icxxqqdsfdc4?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=ab085b515b950aae0a86cae59456cacd%2F20250714%2Fauto%2Fs3%2Faws4_request&X-Amz-Date=20250714T154400Z&X-Amz-Expires=3600&X-Amz-SignedHeaders=host&X-Amz-Signature=845ebfa5feedd840027e431f31e013513dd4c4f74364b4e75a2c8ddb213bd97a
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
https://aa360cc.cloudatacdn.com/u5kjv4jxytd3sdgge5uogji5dg4huat2pxrm2qibdbm5rlyusvl46iz3u3ta/1f4isxg0tw~
|
|
||||||
460
src/extractor/__fixtures__/Dropload/https:dropload.iogf2apij3ql37
generated
Normal file
460
src/extractor/__fixtures__/Dropload/https:dropload.iogf2apij3ql37
generated
Normal file
File diff suppressed because one or more lines are too long
|
|
@ -0,0 +1,5 @@
|
||||||
|
#EXTM3U
|
||||||
|
#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=3955353,RESOLUTION=1920x800,FRAME-RATE=23.974,CODECS="avc1.640028,mp4a.40.2",VIDEO-RANGE=SDR
|
||||||
|
index-v1-a1.m3u8?t=jq0VCV2dOyLIIH07IGIpzruCXvbfdVqzGLWfMeo00AM&s=1759845990&e=14400&f=1571895&srv=ds3&i=0.0&sp=0&ii=2.184.0.222&p1=ds3&p2=ds3
|
||||||
|
|
||||||
|
#EXT-X-I-FRAME-STREAM-INF:BANDWIDTH=68483,RESOLUTION=1920x800,CODECS="avc1.640028",URI="iframes-v1-a1.m3u8?t=jq0VCV2dOyLIIH07IGIpzruCXvbfdVqzGLWfMeo00AM&s=1759845990&e=14400&f=1571895&srv=ds3&i=0.0&sp=0&ii=2.184.0.222&p1=ds3&p2=ds3",VIDEO-RANGE=SDR
|
||||||
230
src/extractor/__fixtures__/Fastream/https:fastream.toda9rp1d1pqom7.html
generated
Normal file
230
src/extractor/__fixtures__/Fastream/https:fastream.toda9rp1d1pqom7.html
generated
Normal file
|
|
@ -0,0 +1,230 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>Fastream</title>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||||
|
<meta name="theme-color" content="#FFF">
|
||||||
|
<link rel="apple-touch-icon" href="https://fastream.to/images2/favicon/apple-touch-icon.png" sizes="180x180">
|
||||||
|
<link rel="icon" href="https://fastream.to/images2/favicon/favicon-32x32.png" sizes="32x32">
|
||||||
|
<link rel="icon" href="https://fastream.to/images2/favicon/favicon-16x16.png" sizes="16x16">
|
||||||
|
<link rel="manifest" href="https://fastream.to/images2/favicon/site.webmanifest" crossorigin="use-credentials">
|
||||||
|
<link rel="icon" href="https://fastream.to/images2/favicon/favicon.ico">
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Manrope:wght@400..800&display=swap" rel="stylesheet">
|
||||||
|
<link href="https://fastream.to/css2/bootstrap.css?" rel="stylesheet">
|
||||||
|
|
||||||
|
<link href="https://fastream.to/css2/style.css?" rel="stylesheet">
|
||||||
|
|
||||||
|
<script src="https://fastream.to/js2/jquery.min.js?"></script>
|
||||||
|
<script src="https://fastream.to/js2/bootstrap.bundle.min.js?"></script>
|
||||||
|
<script src="https://fastream.to/js2/app.js?aс1"></script>
|
||||||
|
<script src="https://fastream.to/js2/xupload.js?aс1"></script>
|
||||||
|
|
||||||
|
</head>
|
||||||
|
<body class="">
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<header class="header">
|
||||||
|
<div class="container">
|
||||||
|
<div class="row header-row align-items-center flex-nowrap">
|
||||||
|
<div class="col-auto flex-grow-1 flex-shrink-1">
|
||||||
|
<div class="row align-items-center">
|
||||||
|
<div class="col-auto d-lg-none">
|
||||||
|
<div class="dropdown">
|
||||||
|
<button class="btn icon-btn" data-bs-toggle="dropdown" data-bs-offset="0, 16">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" width="22" height="16" viewBox="0 0 22 16">
|
||||||
|
<path d="M0 0h22v2H0V0zm0 7h22v2H0V7zm0 7h22v2H0v-2z"></path>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<ul class="dropdown-menu dropdown-menu-end">
|
||||||
|
|
||||||
|
<a href="/premium" class="dropdown-item">Premium</a>
|
||||||
|
|
||||||
|
<a href="/make_money" class="dropdown-item">Make Money</a>
|
||||||
|
<a href="/check_files" class="dropdown-item">Link Checker</a>
|
||||||
|
|
||||||
|
<a href="/api.html" class="dropdown-item">API</a>
|
||||||
|
<a href="/tos" class="dropdown-item">Terms of service</a>
|
||||||
|
|
||||||
|
<a href="/contact" class="dropdown-item">Contact Us</a>
|
||||||
|
<a href="/faq" class="dropdown-item">FAQ</a>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-auto me-lg-4">
|
||||||
|
<a href="/" class="logo">
|
||||||
|
<img src="https://fastream.to/images2/logo.svg?" alt="">
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="col-auto d-none d-lg-block">
|
||||||
|
<nav class="nav">
|
||||||
|
|
||||||
|
|
||||||
|
<a class="nav-link link-body-emphasis" href="/premium">Premium</a>
|
||||||
|
|
||||||
|
<a class="nav-link link-body-emphasis" href="/make_money">Make Money</a>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-auto">
|
||||||
|
<div class="dropdown header-lang">
|
||||||
|
<button type="button" class="btn icon-btn" data-bs-toggle="dropdown" data-bs-offset="0, 16">
|
||||||
|
<img src="https://fastream.to/images2/flags/english.svg" alt="">
|
||||||
|
</button>
|
||||||
|
<div class="dropdown-menu">
|
||||||
|
<a href="/?op=change_lang&lang=English" class="dropdown-item"><img src="https://fastream.to/images2/flags/english.svg" alt="">English</a>
|
||||||
|
<a href="/?op=change_lang&lang=Russian" class="dropdown-item"><img src="https://fastream.to/images2/flags/russian.svg" alt="">Russian</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="col-auto d-none d-lg-block">
|
||||||
|
<a href="/login.html" class="btn me-2 login-btn link-body-emphasis">Login</a>
|
||||||
|
<a href="/?op=registration" class="btn btn-primary reg-btn">Sign Up</a>
|
||||||
|
</div>
|
||||||
|
<div class="col-auto d-lg-none">
|
||||||
|
<div class="dropdown">
|
||||||
|
<button class="btn icon-btn" data-bs-toggle="dropdown" data-bs-offset="0, 16">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" width="22" height="22" viewBox="0 0 22 22">
|
||||||
|
<path d="M11 22C4.9 22 0 17.1 0 11S4.9 0 11 0s11 4.9 11 11-4.9 11-11 11zm0-20c-5 0-9 4-9 9s4 9 9 9 9-4 9-9-4-9-9-9z"></path>
|
||||||
|
<path d="M11 15.3c-3 0-5.4-2.4-5.4-5.4S8 4.4 11 4.4s5.4 2.4 5.4 5.4-2.4 5.5-5.4 5.5zm0-8.9c-1.9 0-3.4 1.5-3.4 3.4s1.5 3.4 3.4 3.4 3.4-1.5 3.4-3.4-1.5-3.4-3.4-3.4z"></path>
|
||||||
|
<path d="M4.9 19.8c-.2 0-.3 0-.5-.1-.5-.3-.6-.9-.4-1.4 1.5-2.5 3.9-3.8 7-3.8s5.5 1.3 7 3.8c.3.5.1 1.1-.3 1.4-.5.3-1.1.1-1.4-.3-1.1-1.9-2.9-2.8-5.2-2.8-2.4 0-4.1.9-5.3 2.8-.2.2-.6.4-.9.4z"></path>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<div class="dropdown-menu dropdown-menu-end">
|
||||||
|
<a class="dropdown-item" href="/login.html">Login</a>
|
||||||
|
<a class="dropdown-item" href="/?op=registration">Sign Up</a>
|
||||||
|
<a class="dropdown-item" href="/?op=forgot_pass">Forgot your password?</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<main>
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
|
||||||
|
<div class="alert alert-danger" role="alert">
|
||||||
|
No such file=a9rp1d1pqom7
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
</main>
|
||||||
|
|
||||||
|
|
||||||
|
<footer class="footer">
|
||||||
|
<div class="container">
|
||||||
|
<div class="footer-main">
|
||||||
|
<div class="row justify-content-center justify-content-lg-start">
|
||||||
|
<div class="col-12 col-lg-4 col-xl-3 text-center text-lg-start">
|
||||||
|
<div class="mb-4">
|
||||||
|
<a href="https://fastream.to/" class="logo">
|
||||||
|
<img src="https://fastream.to/images2/logo-w.svg?" alt="">
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="mb-2 text-uppercase"><!--Share us: --></div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<!--span class="me-1">
|
||||||
|
<a href="#"><img src="https://fastream.to/images2/social/fb.svg" alt=""></a>
|
||||||
|
</span>
|
||||||
|
<span class="me-1">
|
||||||
|
<a href="#"><img src="https://fastream.to/images2/social/red.svg" alt=""></a>
|
||||||
|
</span>
|
||||||
|
<span class="me-1">
|
||||||
|
<a href="#"><img src="https://fastream.to/images2/social/wha.svg" alt=""></a>
|
||||||
|
</span>
|
||||||
|
<span class="me-1">
|
||||||
|
<a href="#"><img src="https://fastream.to/images2/social/x.svg" alt=""></a>
|
||||||
|
</span>
|
||||||
|
<span class="me-1">
|
||||||
|
<a href="#"><img src="https://fastream.to/images2/social/msg.svg" alt=""></a>
|
||||||
|
</span>
|
||||||
|
<span class="me-1">
|
||||||
|
<a href="#"><img src="https://fastream.to/images2/social/in.svg" alt=""></a>
|
||||||
|
</span-->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-lg-8 col-xl-7 d-none d-lg-block">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col">
|
||||||
|
<div class="text-uppercase text-dark large">Info</div>
|
||||||
|
<div class="nav flex-column">
|
||||||
|
<a href="https://fastream.to/" class="nav-link">Home</a>
|
||||||
|
<a href="/api.html" class="nav-link">API</a>
|
||||||
|
<a href="/tos" class="nav-link">Terms of service</a>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col">
|
||||||
|
<div class="text-uppercase text-dark large">Features</div>
|
||||||
|
<div class="nav flex-column">
|
||||||
|
|
||||||
|
<a href="/premium" class="nav-link">Premium</a>
|
||||||
|
|
||||||
|
<a href="/make_money" class="nav-link">Make Money</a>
|
||||||
|
<a href="/check_files" class="nav-link">Link Checker</a>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col">
|
||||||
|
<div class="text-uppercase text-dark large">Support</div>
|
||||||
|
<div class="nav flex-column">
|
||||||
|
<a href="/contact" class="nav-link">Contact Us</a>
|
||||||
|
<a href="/faq" class="nav-link">FAQ</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="col-xl-auto text-nowrap text-end d-none d-xl-block">
|
||||||
|
<a href="https://fastream.to/login.html" class="btn btn-light login-btn me-2">Login</a>
|
||||||
|
<a href="https://fastream.to/?op=registration" class="btn btn-primary reg-btn">Sign Up</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="footer-bottom small">
|
||||||
|
<div class="container">
|
||||||
|
<div class="row align-items-center justify-content-between">
|
||||||
|
<div class="col-auto">
|
||||||
|
<div class="text-muted">© 2024 Fastream</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-auto">
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<div style ="display:none"><script id="_wau1xg">var _wau = _wau || []; _wau.push(["classic", "y7jk20u14x", "1xg"]);</script><script async src="//waust.at/c.js"></script></div>
|
||||||
|
|
||||||
|
<script>(function(){function c(){var b=a.contentDocument||a.contentWindow.document;if(b){var d=b.createElement('script');d.innerHTML="window.__CF$cv$params={r:'993a4e2d4ac5d2ab',t:'MTc2MTMxNzYyMw=='};var a=document.createElement('script');a.src='/cdn-cgi/challenge-platform/scripts/jsd/main.js';document.getElementsByTagName('head')[0].appendChild(a);";b.getElementsByTagName('head')[0].appendChild(d)}}if(document.body){var a=document.createElement('iframe');a.height=1;a.width=1;a.style.position='absolute';a.style.top=0;a.style.left=0;a.style.border='none';a.style.visibility='hidden';document.body.appendChild(a);if('loading'!==document.readyState)c();else if(window.addEventListener)document.addEventListener('DOMContentLoaded',c);else{var e=document.onreadystatechange||function(){};document.onreadystatechange=function(b){e(b);'loading'!==document.readyState&&(document.onreadystatechange=e,c())}}}})();</script></body>
|
||||||
|
</html>
|
||||||
145
src/extractor/__fixtures__/FileLions/https:callistanise.comfcy4t5nkerjrt
generated
Normal file
145
src/extractor/__fixtures__/FileLions/https:callistanise.comfcy4t5nkerjrt
generated
Normal file
|
|
@ -0,0 +1,145 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>
|
||||||
|
|
||||||
|
Watch 300938 570f5a24 c281 484d 93fa 734dabbec813 jkgg 2651705 vidhide mp4
|
||||||
|
|
||||||
|
</title>
|
||||||
|
<META NAME="description" CONTENT="">
|
||||||
|
<META NAME="keywords" CONTENT="">
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||||
|
<meta name="theme-color" content="#FFF">
|
||||||
|
|
||||||
|
<link rel="apple-touch-icon" href="/static/images/favicon/apple-touch-icon.png" sizes="180x180">
|
||||||
|
<link rel="icon" href="/static/images/favicon/favicon-32x32.png" sizes="32x32">
|
||||||
|
<link rel="icon" href="/static/images/favicon/favicon-16x16.png" sizes="16x16">
|
||||||
|
<link rel="manifest" href="/static/images/favicon/manifest.json">
|
||||||
|
<link rel="icon" href="/static/images/favicon/favicon.ico">
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin="crossorigin">
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;700&display=swap" rel="stylesheet">
|
||||||
|
<link href="/static/css/style.css?" rel="stylesheet">
|
||||||
|
|
||||||
|
<script src="/static/js/jquery-3.2.1.min.js?"></script>
|
||||||
|
<script src="/static/js/bootstrap.bundle.min.js?"></script>
|
||||||
|
<script src="/static/js/app.js?"></script>
|
||||||
|
<script src="/static/js/xupload.js?"></script>
|
||||||
|
|
||||||
|
|
||||||
|
<!-- Google tag (gtag.js) -->
|
||||||
|
<script async src="https://www.googletagmanager.com/gtag/js?id=G-48ZJD1VPGZ"></script>
|
||||||
|
<script>
|
||||||
|
window.dataLayer = window.dataLayer || [];
|
||||||
|
function gtag(){dataLayer.push(arguments);}
|
||||||
|
gtag('js', new Date());
|
||||||
|
|
||||||
|
gtag('config', 'G-48ZJD1VPGZ');
|
||||||
|
</script>
|
||||||
|
<!-- Yandex.Metrika counter -->
|
||||||
|
<script type="text/javascript" >
|
||||||
|
(function(m,e,t,r,i,k,a){m[i]=m[i]||function(){(m[i].a=m[i].a||[]).push(arguments)};
|
||||||
|
m[i].l=1*new Date();
|
||||||
|
for (var j = 0; j < document.scripts.length; j++) {if (document.scripts[j].src === r) { return; }}
|
||||||
|
k=e.createElement(t),a=e.getElementsByTagName(t)[0],k.async=1,k.src=r,a.parentNode.insertBefore(k,a)})
|
||||||
|
(window, document, "script", "https://mc.yandex.ru/metrika/tag.js", "ym");
|
||||||
|
|
||||||
|
ym(93381808, "init", {
|
||||||
|
clickmap:true,
|
||||||
|
trackLinks:true,
|
||||||
|
accurateTrackBounce:true,
|
||||||
|
webvisor:true
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<noscript><div><img src="https://mc.yandex.ru/watch/93381808" style="position:absolute; left:-9999px;" alt="" /></div></noscript>
|
||||||
|
<!-- /Yandex.Metrika counter -->
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<header class="header">
|
||||||
|
<div class="container">
|
||||||
|
<div class="row align-items-center flex-nowrap">
|
||||||
|
<div class="col-auto flex-grow-1 flex-shrink-1">
|
||||||
|
<div class="row align-items-center">
|
||||||
|
|
||||||
|
<div class="col-auto" style="margin: 0 auto;">
|
||||||
|
<a href="/" class="logo">
|
||||||
|
|
||||||
|
<img src="/static/images/logo.svg" alt="">
|
||||||
|
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
|
||||||
|
<main>
|
||||||
|
<section class="section">
|
||||||
|
<div class="container">
|
||||||
|
<div class="block">
|
||||||
|
<div class="mb-3">
|
||||||
|
The file was deleted by administration because it didn't comply with our Terms of Use:
|
||||||
|
</div>
|
||||||
|
<h4>
|
||||||
|
300938--570f5a24-c281-484d-93fa-734dabbec813--jkgg--2651705-vidhide.mp4
|
||||||
|
</h4>
|
||||||
|
<br>
|
||||||
|
Reason for deletion: <br>
|
||||||
|
<b class="text-danger">
|
||||||
|
DMCA request
|
||||||
|
</b>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
<footer class="footer">
|
||||||
|
<div class="container">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-lg-4 col-xs-12">
|
||||||
|
<div class="mb-3 text-center">
|
||||||
|
<a href="#" class="logo">
|
||||||
|
|
||||||
|
<img src="/static/images/logo.svg" alt="">
|
||||||
|
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-lg-8 col-xs-12" style="position: relative">
|
||||||
|
<div class="mb-4 text-center">
|
||||||
|
<p class="text-muted ">
|
||||||
|
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
<style>
|
||||||
|
@media (min-width:481px) {
|
||||||
|
.text-muted {}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width:961px) {
|
||||||
|
.text-muted {
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
top: 15px
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
|
|
||||||
File diff suppressed because one or more lines are too long
161
src/extractor/__fixtures__/FileLions/https:filelions.tofylcp2cu5qanb
generated
Normal file
161
src/extractor/__fixtures__/FileLions/https:filelions.tofylcp2cu5qanb
generated
Normal file
|
|
@ -0,0 +1,161 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>
|
||||||
|
|
||||||
|
Watch
|
||||||
|
|
||||||
|
</title>
|
||||||
|
<META NAME="description" CONTENT="">
|
||||||
|
<META NAME="keywords" CONTENT="">
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||||
|
<meta name="theme-color" content="#FFF">
|
||||||
|
|
||||||
|
<link rel="apple-touch-icon" href="/static/images/favicon/apple-touch-icon.png" sizes="180x180">
|
||||||
|
<link rel="icon" href="/static/images/favicon/favicon-32x32.png" sizes="32x32">
|
||||||
|
<link rel="icon" href="/static/images/favicon/favicon-16x16.png" sizes="16x16">
|
||||||
|
<link rel="manifest" href="/static/images/favicon/manifest.json">
|
||||||
|
<link rel="icon" href="/static/images/favicon/favicon.ico">
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin="crossorigin">
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;700&display=swap" rel="stylesheet">
|
||||||
|
<link href="/static/css/style.css?" rel="stylesheet">
|
||||||
|
|
||||||
|
<script src="/static/js/jquery-3.2.1.min.js?"></script>
|
||||||
|
<script src="/static/js/bootstrap.bundle.min.js?"></script>
|
||||||
|
<script src="/static/js/app.js?"></script>
|
||||||
|
<script src="/static/js/xupload.js?"></script>
|
||||||
|
|
||||||
|
|
||||||
|
<!-- Google tag (gtag.js) -->
|
||||||
|
<script async src="https://www.googletagmanager.com/gtag/js?id=G-48ZJD1VPGZ"></script>
|
||||||
|
<script>
|
||||||
|
window.dataLayer = window.dataLayer || [];
|
||||||
|
function gtag(){dataLayer.push(arguments);}
|
||||||
|
gtag('js', new Date());
|
||||||
|
|
||||||
|
gtag('config', 'G-48ZJD1VPGZ');
|
||||||
|
</script>
|
||||||
|
<!-- Yandex.Metrika counter -->
|
||||||
|
<script type="text/javascript" >
|
||||||
|
(function(m,e,t,r,i,k,a){m[i]=m[i]||function(){(m[i].a=m[i].a||[]).push(arguments)};
|
||||||
|
m[i].l=1*new Date();
|
||||||
|
for (var j = 0; j < document.scripts.length; j++) {if (document.scripts[j].src === r) { return; }}
|
||||||
|
k=e.createElement(t),a=e.getElementsByTagName(t)[0],k.async=1,k.src=r,a.parentNode.insertBefore(k,a)})
|
||||||
|
(window, document, "script", "https://mc.yandex.ru/metrika/tag.js", "ym");
|
||||||
|
|
||||||
|
ym(93381808, "init", {
|
||||||
|
clickmap:true,
|
||||||
|
trackLinks:true,
|
||||||
|
accurateTrackBounce:true,
|
||||||
|
webvisor:true
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<noscript><div><img src="https://mc.yandex.ru/watch/93381808" style="position:absolute; left:-9999px;" alt="" /></div></noscript>
|
||||||
|
<!-- /Yandex.Metrika counter -->
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<header class="header">
|
||||||
|
<div class="container">
|
||||||
|
<div class="row align-items-center flex-nowrap">
|
||||||
|
<div class="col-auto flex-grow-1 flex-shrink-1">
|
||||||
|
<div class="row align-items-center">
|
||||||
|
|
||||||
|
<div class="col-auto" style="margin: 0 auto;">
|
||||||
|
<a href="/" class="logo">
|
||||||
|
|
||||||
|
<img src="/static/images/logo.svg" alt="">
|
||||||
|
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
|
||||||
|
<main>
|
||||||
|
<section class="section">
|
||||||
|
<div class="container">
|
||||||
|
<div class="block">
|
||||||
|
<div class="text-center">
|
||||||
|
<div class="mb-4">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64"><path fill-rule="evenodd" fill="#DC3545" d="M32 64C14.326 64 0 49.674 0 32S14.326 0 32 0c17.666.019 31.981 14.334 32 32 0 17.674-14.326 32-32 32Zm0-61C15.984 3 3 15.984 3 32s12.984 29 29 29c16.008-.019 28.981-12.992 29-29C61 15.984 48.016 3 32 3Zm14.065 43.125a1.504 1.504 0 0 1-2.125 0l-11.94-12L20.065 46.12A1.502 1.502 0 0 1 17.941 44L29.88 32 17.941 20a1.503 1.503 0 0 1 2.124-2.125l11.935 12 11.94-11.98a1.502 1.502 0 0 1 2.12 0A1.5 1.5 0 0 1 46.065 20L34.12 32l11.945 12a1.504 1.504 0 0 1 0 2.125Z"/></svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<h3 class="text-center mb-3">
|
||||||
|
File Not Found
|
||||||
|
</h3>
|
||||||
|
<div>
|
||||||
|
<p class="text-center">The file you were looking for could not be found, sorry for any inconvenience.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="text-muted">
|
||||||
|
<p class="text-center"><b>Possible causes of this error could be:</b></p>
|
||||||
|
<div class="row justify-content-center">
|
||||||
|
<div class="col-auto">
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
The file expired
|
||||||
|
<li>
|
||||||
|
The file was deleted by its owner
|
||||||
|
<li>
|
||||||
|
The file was deleted by administration because it didn't comply with our Terms of Use
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
<footer class="footer">
|
||||||
|
<div class="container">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-lg-4 col-xs-12">
|
||||||
|
<div class="mb-3 text-center">
|
||||||
|
<a href="#" class="logo">
|
||||||
|
|
||||||
|
<img src="/static/images/logo.svg" alt="">
|
||||||
|
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-lg-8 col-xs-12" style="position: relative">
|
||||||
|
<div class="mb-4 text-center">
|
||||||
|
<p class="text-muted ">
|
||||||
|
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
<style>
|
||||||
|
@media (min-width:481px) {
|
||||||
|
.text-muted {}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width:961px) {
|
||||||
|
.text-muted {
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
top: 15px
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
|
|
||||||
|
|
@ -1,6 +0,0 @@
|
||||||
#EXTM3U
|
|
||||||
#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="audio0",NAME="Deutsch",LANGUAGE="de",AUTOSELECT=YES,DEFAULT=YES,CHANNELS="2",URI="https://mediaflow.test.org/proxy/hls/manifest.m3u8?api_password=test&h_user-agent=Mozilla%2F5.0+%28Windows+NT+10.0%3B+Win64%3B+x64%29+AppleWebKit%2F537.36+%28KHTML%2C+like+Gecko%29+Chrome%2F136.0.0.0+Safari%2F537.36&h_referer=https%3A%2F%2Fkinoger.com&d=https%3A%2F%2F6dkdxt1udlqhbho.ovaltinecdn.com%2Fhls2%2F01%2F01552%2Ftah5znapz3e5_n%2Findex-a1.m3u8"
|
|
||||||
#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="audio0",NAME="English",LANGUAGE="en",AUTOSELECT=NO,DEFAULT=NO,CHANNELS="2",URI="https://mediaflow.test.org/proxy/hls/manifest.m3u8?api_password=test&h_user-agent=Mozilla%2F5.0+%28Windows+NT+10.0%3B+Win64%3B+x64%29+AppleWebKit%2F537.36+%28KHTML%2C+like+Gecko%29+Chrome%2F136.0.0.0+Safari%2F537.36&h_referer=https%3A%2F%2Fkinoger.com&d=https%3A%2F%2F6dkdxt1udlqhbho.ovaltinecdn.com%2Fhls2%2F01%2F01552%2Ftah5znapz3e5_n%2Findex-a2.m3u8"
|
|
||||||
#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=2862563,RESOLUTION=1920x1080,FRAME-RATE=25.000,CODECS="avc1.640028,mp4a.40.2",VIDEO-RANGE=SDR,AUDIO="audio0"
|
|
||||||
https://mediaflow.test.org/proxy/hls/manifest.m3u8?api_password=test&h_user-agent=Mozilla%2F5.0+%28Windows+NT+10.0%3B+Win64%3B+x64%29+AppleWebKit%2F537.36+%28KHTML%2C+like+Gecko%29+Chrome%2F136.0.0.0+Safari%2F537.36&h_referer=https%3A%2F%2Fkinoger.com&d=https%3A%2F%2F6dkdxt1udlqhbho.ovaltinecdn.com%2Fhls2%2F01%2F01552%2Ftah5znapz3e5_n%2Findex-v1-a1.m3u8
|
|
||||||
#EXT-X-I-FRAME-STREAM-INF:BANDWIDTH=123231,RESOLUTION=1920x1080,CODECS="avc1.640028",URI="https://mediaflow.test.org/proxy/hls/manifest.m3u8?api_password=test&h_user-agent=Mozilla%2F5.0+%28Windows+NT+10.0%3B+Win64%3B+x64%29+AppleWebKit%2F537.36+%28KHTML%2C+like+Gecko%29+Chrome%2F136.0.0.0+Safari%2F537.36&h_referer=https%3A%2F%2Fkinoger.com&d=https%3A%2F%2F6dkdxt1udlqhbho.ovaltinecdn.com%2Fhls2%2F01%2F01552%2Ftah5znapz3e5_n%2Fiframes-v1-a1.m3u8",VIDEO-RANGE=SDR
|
|
||||||
|
|
@ -1,4 +0,0 @@
|
||||||
#EXTM3U
|
|
||||||
#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=1242387,RESOLUTION=1728x720,FRAME-RATE=23.974,CODECS="avc1.640020,mp4a.40.2",VIDEO-RANGE=SDR
|
|
||||||
https://mediaflow.test.org/proxy/hls/manifest.m3u8?api_password=test&h_user-agent=Mozilla%2F5.0+%28Windows+NT+10.0%3B+Win64%3B+x64%29+AppleWebKit%2F537.36+%28KHTML%2C+like+Gecko%29+Chrome%2F136.0.0.0+Safari%2F537.36&h_referer=https%3A%2F%2Ffilelions.to%2Fv%2Ftyn45apubte2&d=https%3A%2F%2Frnzt2t4xvku08.acek-cdn.com%2Fhls2%2F01%2F05594%2Ftyn45apubte2_n%2Findex-v1-a1.m3u8%3Ft%3D8y6hK40boUynXnkyW5s63kFaWpt6_A5UcG5LBUX3j_4%26s%3D1756131487%26e%3D129600%26f%3D27974684%26srv%3DF6vE2P5Wjncn%26i%3D0.4%26sp%3D500%26p1%3DF6vE2P5Wjncn%26p2%3DF6vE2P5Wjncn%26asn%3D31898
|
|
||||||
#EXT-X-I-FRAME-STREAM-INF:BANDWIDTH=69531,RESOLUTION=1728x720,CODECS="avc1.640020",URI="https://mediaflow.test.org/proxy/hls/manifest.m3u8?api_password=test&h_user-agent=Mozilla%2F5.0+%28Windows+NT+10.0%3B+Win64%3B+x64%29+AppleWebKit%2F537.36+%28KHTML%2C+like+Gecko%29+Chrome%2F136.0.0.0+Safari%2F537.36&h_referer=https%3A%2F%2Ffilelions.to%2Fv%2Ftyn45apubte2&d=https%3A%2F%2Frnzt2t4xvku08.acek-cdn.com%2Fhls2%2F01%2F05594%2Ftyn45apubte2_n%2Fiframes-v1-a1.m3u8%3Ft%3D8y6hK40boUynXnkyW5s63kFaWpt6_A5UcG5LBUX3j_4%26s%3D1756131487%26e%3D129600%26f%3D27974684%26srv%3DF6vE2P5Wjncn%26i%3D0.4%26sp%3D500%26p1%3DF6vE2P5Wjncn%26p2%3DF6vE2P5Wjncn%26asn%3D31898",VIDEO-RANGE=SDR
|
|
||||||
150
src/extractor/__fixtures__/FileLions/https:mivalyo.comvtah5znapz3e5
generated
Normal file
150
src/extractor/__fixtures__/FileLions/https:mivalyo.comvtah5znapz3e5
generated
Normal file
File diff suppressed because one or more lines are too long
57
src/extractor/__fixtures__/FileMoon/https:filemoon.sxdn7i8zodwjqr9
generated
Normal file
57
src/extractor/__fixtures__/FileMoon/https:filemoon.sxdn7i8zodwjqr9
generated
Normal file
|
|
@ -0,0 +1,57 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<script>
|
||||||
|
(function () {
|
||||||
|
// Get all script tags in the <head>
|
||||||
|
const scripts = document.head.querySelectorAll('script');
|
||||||
|
|
||||||
|
// Iterate through the scripts to find the one injecting the Proxy
|
||||||
|
scripts.forEach((script) => {
|
||||||
|
if (script.innerHTML.includes('window.open=new Proxy(window.open')) {
|
||||||
|
console.log('Detected and removing injected script:', script);
|
||||||
|
script.remove(); // Remove the script from the <head>
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Restore the original window.open functionality (optional)
|
||||||
|
const originalOpen = Function.prototype.apply.bind(window.open);
|
||||||
|
window.open = function (...args) {
|
||||||
|
console.log('Restored window.open called:', args);
|
||||||
|
return originalOpen(this, args);
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log('Injected script removed and window.open restored.');
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
<title>Not Found</title>
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Open+Sans:wght@400;700&display=swap" rel="stylesheet">
|
||||||
|
<link rel="stylesheet" href="/assets/css/404.css?v=2">
|
||||||
|
<link rel="apple-touch-icon" href="https://filemoon.to/assets/images/favicon/apple-touch-icon.png" sizes="180x180">
|
||||||
|
<link rel="icon" href="https://filemoon.to/assets/images/favicon/favicon-32x32.png" sizes="32x32">
|
||||||
|
<link rel="icon" href="https://filemoon.to/assets/images/favicon/favicon-16x16.png" sizes="16x16">
|
||||||
|
<link rel="icon" href="https://filemoon.to/assets/images/favicon/favicon.ico">
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
</body>
|
||||||
|
<script src="/ad.js" type="text/javascript"></script>
|
||||||
|
<div id="adb" class="ad doubleclick skyscraper" style="height:1px;width:1px;position:absolute;left:-999px;top:-999px;">ad</div>
|
||||||
|
<script>
|
||||||
|
window.fmn7i8zodwjqr9 = { token: "bed5d8dfa2e30a9a04", adblock: false, blockadblock: true, fileid: "n7i8zodwjqr9", ampallow: false };
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
<div class="error e404">
|
||||||
|
<div class="error-footer">
|
||||||
|
<h1>Page not found</h1>
|
||||||
|
<a href="/" class="button">Return home</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</html>
|
||||||
227
src/extractor/__fixtures__/FileMoon/https:filemoon.todugj7f1kq94p1
generated
Normal file
227
src/extractor/__fixtures__/FileMoon/https:filemoon.todugj7f1kq94p1
generated
Normal file
|
|
@ -0,0 +1,227 @@
|
||||||
|
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>Watch tt0307453</title>
|
||||||
|
<meta name="description" content="">
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||||
|
<meta name="theme-color" content="#FFF">
|
||||||
|
|
||||||
|
<link rel="apple-touch-icon" href="//filemoon.to/assets/images/favicon/apple-touch-icon.png" sizes="180x180">
|
||||||
|
<link rel="icon" href="//filemoon.to/assets/images/favicon/favicon-32x32.png" sizes="32x32">
|
||||||
|
<link rel="icon" href="//filemoon.to/assets/images/favicon/favicon-16x16.png" sizes="16x16">
|
||||||
|
<link rel="icon" href="//filemoon.to/assets/images/favicon/favicon.ico">
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com">
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Open+Sans:wght@400;600;700&display=swap" rel="stylesheet">
|
||||||
|
<link rel="stylesheet" href="//filemoon.to/assets/css/front.css?v=51">
|
||||||
|
<META NAME="description" CONTENT="Watch video tt0307453">
|
||||||
|
<META NAME="keywords" CONTENT="tt0307453">
|
||||||
|
<meta name="robots" content="noindex, nofollow">
|
||||||
|
|
||||||
|
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body class="is-home d-flex flex-column justify-content-between ">
|
||||||
|
<header class="header py-3 py-lg-4">
|
||||||
|
<div class="container">
|
||||||
|
<div class="row align-items-center justify-content-between justify-content-lg-start">
|
||||||
|
<div class="col-auto d-lg-none">
|
||||||
|
<button class="btn text-muted ssm-toggle-nav px-1" type="button">
|
||||||
|
<i class="icon icon-menu icon-size-22 text-muted"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="col-auto">
|
||||||
|
<a href="/" class="logo d-block"><img src="//filemoon.to/assets/images/logo.svg" alt=""></a>
|
||||||
|
</div>
|
||||||
|
<div class="col-auto d-none d-lg-block flex-lg-grow-1">
|
||||||
|
<nav class="nav ml-lg-3">
|
||||||
|
<a class="nav-link" href="/premium">Premium</a>
|
||||||
|
<a class="nav-link" href="/affiliate">Make Money</a>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
<div class="col-auto">
|
||||||
|
<div class="d-none d-lg-flex">
|
||||||
|
|
||||||
|
<a href="/login" class="btn btn-dark sign-btn rounded-pill mr-2">Log In</a>
|
||||||
|
<a href="/register" class="btn btn-success sign-btn rounded-pill">Sign Up</a>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div class="d-lg-none">
|
||||||
|
<button class="btn text-muted usrmobile-toggle-nav px-1" type="button">
|
||||||
|
<i class="icon icon-user icon-size-18 text-muted"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<script src="https://filemoon.to/js/jquery.cookie.js"></script>
|
||||||
|
<script>
|
||||||
|
$.cookie('file_id', '48721931', { expires: 10 });
|
||||||
|
$.cookie('aff', '89667', { expires: 10 });
|
||||||
|
$.cookie('ref_url', 'https://filemoon.to/d/ugj7f1kq94p1', { expires: 10 });
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script type="text/javascript" src='/js/ls.js'></script>
|
||||||
|
<main class="flex-grow-1 a-bean">
|
||||||
|
<div class="container mt-5">
|
||||||
|
<div class="mb-4 mb-lg-5 sm-bg px-4 py-5 p-lg-0 mx-lg-5 rounded-lg">
|
||||||
|
<div class="row justify-content-between align-items-center text-muted mb-4 mb-lg-5">
|
||||||
|
<div class="col-auto mb-2">
|
||||||
|
<h3 class="text-light mb-0 font-weight-bold">tt0307453</h3>
|
||||||
|
</div>
|
||||||
|
<div class="col-auto mb-2"> Uploaded: <span class="text-success">2025-08-24 00:12:36</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mb-4 mb-lg-5 ">
|
||||||
|
<div class="position-relative">
|
||||||
|
|
||||||
|
<div class="bimg-top text-center">
|
||||||
|
<div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="height: 640px;" id="iframe-holder">
|
||||||
|
|
||||||
|
<iframe src="https://ico3c.com/bkg/ugj7f1kq94p1" frameborder="0" marginwidth="0" marginheight="0" scrolling="no" width="100%" height="100%" allowfullscreen></iframe>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="bimg-bottom text-center">
|
||||||
|
<div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="mb-4 mb-lg-5 text-center">
|
||||||
|
<a href="/download/ugj7f1kq94p1" class="btn btn-success px-5">Download</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
#vplayer {padding-top: 56.25%;}
|
||||||
|
.bimg-top,
|
||||||
|
.bimg-bottom {
|
||||||
|
display: -webkit-flex;
|
||||||
|
display: -ms-flex;
|
||||||
|
display: flex;
|
||||||
|
-ms-align-items: center;
|
||||||
|
align-items: center;
|
||||||
|
-webkit-flex-wrap: wrap;
|
||||||
|
-ms-flex-wrap: wrap;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
.bimg-top div,
|
||||||
|
.bimg-bottom div {
|
||||||
|
margin: .5rem;
|
||||||
|
}
|
||||||
|
@media(min-width:1600px){
|
||||||
|
.bimg-top,
|
||||||
|
.bimg-bottom {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
.bimg-top {
|
||||||
|
left: -310px;
|
||||||
|
}
|
||||||
|
.bimg-bottom {
|
||||||
|
right: -310px;
|
||||||
|
}
|
||||||
|
.bimg-top div,
|
||||||
|
.bimg-bottom div {
|
||||||
|
margin: .5rem 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@media(min-width:1800px){
|
||||||
|
.bimg-top {
|
||||||
|
left: -350px;
|
||||||
|
}
|
||||||
|
.bimg-bottom {
|
||||||
|
right: -350px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<footer class="footer py-4 py-lg-5">
|
||||||
|
<div class="container">
|
||||||
|
<div class="row align-items-center justify-content-between">
|
||||||
|
<div class="col-auto my-1">
|
||||||
|
<div class="text-muted"> FileMoon © 2022 </div>
|
||||||
|
</div>
|
||||||
|
<div class="col-auto my-1">
|
||||||
|
<nav class="nav">
|
||||||
|
<a class="nav-link text-muted footer-link px-0 mr-4" href="/">Home</a>
|
||||||
|
<a class="nav-link text-muted footer-link px-0 mr-4" href="/tos">Terms of Service</a>
|
||||||
|
<a class="nav-link text-muted footer-link px-0 mr-4" href="/privacy">Privacy Policy</a>
|
||||||
|
<a class="nav-link text-muted footer-link px-0 mr-4" href="/copyright">Copyright Policy</a>
|
||||||
|
<a class="nav-link text-muted footer-link px-0 mr-4" href="/affiliate">Make Money</a>
|
||||||
|
<a class="nav-link text-muted footer-link px-0 mr-4" href="/api">API Documentation</a>
|
||||||
|
<a class="nav-link text-muted footer-link px-0 mr-4" href="https://status.filemoon.sx">Server Status</a>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
<div class="ssm">
|
||||||
|
<nav class="nav ssm-nav flex-column">
|
||||||
|
<a class="nav-link ssm-nav-link active" href="/"><i class="icon icon-home icon-size-16 ssm-nav-icon mr-3"></i>Home</a>
|
||||||
|
<a class="nav-link ssm-nav-link" href="/dashboard"><i class="icon icon-account icon-size-16 ssm-nav-icon mr-3"></i>My account</a>
|
||||||
|
<a class="nav-link ssm-nav-link" href="/premium"><i class="icon icon-money icon-size-16 ssm-nav-icon mr-3"></i>Premium</a>
|
||||||
|
<a class="nav-link ssm-nav-link" href="/affiliate"><i class="icon icon-money icon-size-16 ssm-nav-icon mr-3"></i>Make Money</a>
|
||||||
|
</nav>
|
||||||
|
<hr class="mx-3">
|
||||||
|
<nav class="nav flex-column ssm-nav">
|
||||||
|
<a class="nav-link ssm-nav-link" href="/tos"><i class="icon icon-pen icon-size-16 ssm-nav-icon mr-3"></i>Terms of Service</a>
|
||||||
|
<a class="nav-link ssm-nav-link" href="/privacy"><i class="icon icon-key icon-size-16 ssm-nav-icon mr-3"></i>Privacy Policy</a>
|
||||||
|
<a class="nav-link ssm-nav-link" href="/api"><i class="icon icon-note icon-size-16 ssm-nav-icon mr-3"></i>API Documentation</a>
|
||||||
|
<a class="nav-link ssm-nav-link" href="https://status.filemoon.sx"><i class="icon icon-note icon-size-16 ssm-nav-icon mr-3"></i>Server Status</a>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
<div class="ssm-overlay ssm-toggle-nav"></div>
|
||||||
|
<div class="usrmobile p-5 usrmobile-toggle-nav">
|
||||||
|
|
||||||
|
<a href="/login" class="btn btn-success btn-success-custom btn-lg btn-block text-uppercase shadow">Sign in</a>
|
||||||
|
<a href="/register" class="btn btn-outline-success btn-lg btn-block mb-2 text-uppercase">Sign up</a>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<script src="//filemoon.to/assets/js/bootstrap.bundle.min.js"></script>
|
||||||
|
<script src="//filemoon.to/assets/js/jquery.slideandswipe.min.js"></script>
|
||||||
|
<script src="//filemoon.to/assets/js/jquery.touchSwipe.min.js"></script>
|
||||||
|
<script src="//filemoon.to/assets/js/front.js"></script>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue